├── .eslintrc.json
├── .github
├── FUNDING.yml
└── workflows
│ ├── publish.yaml
│ └── test.yaml
├── .gitignore
├── .tool-versions
├── .vscode
├── extensions.json
├── launch.json
├── settings.json
└── tasks.json
├── .vscodeignore
├── CHANGELOG.md
├── CONTRIBUTING.md
├── LICENSE
├── README.md
├── package-lock.json
├── package.json
├── packages
├── codegen
│ ├── assets-src
│ │ └── icon.svg
│ ├── package.json
│ └── src
│ │ ├── OrigConfig.d.ts.ejs
│ │ └── README.md.ejs
├── core
│ ├── package.json
│ ├── src
│ │ ├── bigquery.test.ts
│ │ ├── bigquery.ts
│ │ ├── flat.test.ts
│ │ ├── flat.ts
│ │ ├── formatter.test.ts
│ │ ├── formatter.ts
│ │ ├── gcloud.ts
│ │ ├── index.ts
│ │ ├── metadata.json
│ │ ├── parser.test.ts
│ │ ├── parser.ts
│ │ ├── table.json
│ │ ├── transform.test.ts
│ │ └── transform.ts
│ └── tsconfig.json
├── extension
│ ├── package.json
│ ├── src
│ │ ├── OrigConfig.d.ts
│ │ ├── checksum.ts
│ │ ├── configManager.ts
│ │ ├── downloader.ts
│ │ ├── dryRunner.ts
│ │ ├── errorManager.ts
│ │ ├── errorMarker.ts
│ │ ├── getQueryText.ts
│ │ ├── index.ts
│ │ ├── isBigQuery.ts
│ │ ├── logger.ts
│ │ ├── paramManager.ts
│ │ ├── previewer.ts
│ │ ├── quickfix.ts
│ │ ├── renderer.ts
│ │ ├── runner.ts
│ │ ├── statusManager.ts
│ │ ├── tree.ts
│ │ └── window.ts
│ └── tsconfig.json
├── misc
│ ├── assets-src
│ │ ├── icon-copy.svg
│ │ ├── icon-end.svg
│ │ ├── icon-next.svg
│ │ ├── icon-panel.svg
│ │ ├── icon-prev.svg
│ │ ├── icon-start.svg
│ │ ├── icon.svg
│ │ └── loading.svg
│ ├── assets
│ │ ├── icon-activity-bar.svg
│ │ ├── icon-panel.png
│ │ └── icon.png
│ ├── docs
│ │ └── flow.md
│ ├── mock
│ │ ├── rows.json
│ │ └── vscode.css
│ ├── package.json
│ ├── queries
│ │ ├── array-join.bqsql
│ │ ├── create-or-replace-a.bqsql
│ │ ├── create-or-replace-b.bqsql
│ │ ├── create-or-replace.bqsql
│ │ ├── create-struct-array.bqsql
│ │ ├── empty.bqsql
│ │ ├── error-quickfix.bqsql
│ │ ├── error-syntax.bqsql
│ │ ├── error-type.bqsql
│ │ ├── error.bqsql
│ │ ├── geom.bqsql
│ │ ├── in-code.ts
│ │ ├── join-differect-pj-tables.bqsql
│ │ ├── json.bqsql
│ │ ├── merge.bqsql
│ │ ├── nullable-struct.bqsql
│ │ ├── params-arrays.bqsql
│ │ ├── params-named-duplicated.bqsql
│ │ ├── params-named-with-comments.bqsql
│ │ ├── params-named-with-strings.bqsql
│ │ ├── params-named.bqsql
│ │ ├── params-positional-with-comments.bqsql
│ │ ├── params-positional-with-strings.bqsql
│ │ ├── params-positional.bqsql
│ │ ├── params-structs.bqsql
│ │ ├── params-timestamps.bqsql
│ │ ├── procedure.bqsql
│ │ ├── select-a.bqsql
│ │ ├── select-b.bqsql
│ │ ├── select.bqsql
│ │ ├── spreadsheet.bqsql
│ │ ├── spreadsheet.tsv
│ │ ├── system-variables.bqsql
│ │ ├── types.bqsql
│ │ ├── variables.bqsql
│ │ ├── wikipedia.bqsql
│ │ └── without-dataset.bqsql
│ └── screenshots
│ │ ├── dry-run.gif
│ │ ├── query-validation.gif
│ │ └── run.gif
├── shared
│ ├── package.json
│ ├── src
│ │ ├── error.ts
│ │ ├── funcs.test.ts
│ │ ├── funcs.ts
│ │ ├── index.ts
│ │ ├── result.ts
│ │ └── types.ts
│ └── tsconfig.json
└── viewer
│ ├── config
│ ├── env.js
│ ├── getHttpsConfig.js
│ ├── jest
│ │ ├── babelTransform.js
│ │ ├── cssTransform.js
│ │ └── fileTransform.js
│ ├── modules.js
│ └── paths.js
│ ├── package.json
│ ├── public
│ └── index.html
│ ├── scripts
│ └── test.js
│ ├── src
│ ├── App.test.tsx
│ ├── App.tsx
│ ├── context
│ │ └── Clipboard.tsx
│ ├── domain
│ │ ├── Footer.tsx
│ │ ├── Header.tsx
│ │ ├── Job.test.tsx
│ │ ├── Job.tsx
│ │ ├── Routine.tsx
│ │ ├── Rows.tsx
│ │ └── Table.tsx
│ ├── index.tsx
│ ├── jobEvent.json
│ ├── react-app-env.d.ts
│ ├── reportWebVitals.ts
│ ├── routineEvent.json
│ ├── setupTests.ts
│ ├── tableEvent.json
│ ├── theme.ts
│ ├── types.ts
│ └── ui
│ │ ├── Breakable.tsx
│ │ └── CopyButton.tsx
│ └── tsconfig.json
└── tsconfig.json
/.eslintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "root": true,
3 | "parser": "@typescript-eslint/parser",
4 | "parserOptions": {
5 | "ecmaVersion": 12,
6 | "sourceType": "module",
7 | "project": "tsconfig.json"
8 | },
9 | "extends": [
10 | "eslint:recommended",
11 | "plugin:@typescript-eslint/recommended",
12 | "plugin:import/recommended",
13 | "plugin:react/recommended",
14 | "prettier"
15 | ],
16 | "plugins": [
17 | "@typescript-eslint",
18 | "import",
19 | "unused-imports",
20 | "react",
21 | "react-hooks",
22 | "strict-dependencies",
23 | "sonarjs"
24 | ],
25 | "rules": {
26 | // Do with TypeScript
27 | "no-undef": "off",
28 |
29 | "sonarjs/no-ignored-return": "error",
30 |
31 | // https://github.com/typescript-eslint/typescript-eslint/issues/2063#issuecomment-675156492
32 | "@typescript-eslint/ban-types": [
33 | "error",
34 | {
35 | "extendDefaults": true,
36 | "types": {
37 | "{}": false
38 | }
39 | }
40 | ],
41 | "@typescript-eslint/naming-convention": [
42 | "warn",
43 | {
44 | "selector": "function",
45 | "format": ["PascalCase", "camelCase"]
46 | }
47 | ],
48 | "@typescript-eslint/semi": "warn",
49 | "@typescript-eslint/no-floating-promises": "error",
50 | "@typescript-eslint/no-invalid-void-type": "error",
51 | "@typescript-eslint/consistent-type-imports": [
52 | "error",
53 | {
54 | "prefer": "type-imports"
55 | }
56 | ],
57 |
58 | // Use unused-imports/no-unused-vars
59 | "@typescript-eslint/no-unused-vars": "off",
60 |
61 | "no-useless-rename": "error",
62 | "object-shorthand": "error",
63 |
64 | "import/order": [
65 | "error",
66 | {
67 | "groups": ["builtin", "external", "parent", "sibling", "index"],
68 | "newlines-between": "never",
69 | "alphabetize": {
70 | "order": "asc",
71 | "caseInsensitive": false
72 | }
73 | }
74 | ],
75 | "import/no-unresolved": "off",
76 | "unused-imports/no-unused-imports": "error",
77 | "unused-imports/no-unused-vars": [
78 | "warn",
79 | {
80 | "vars": "all",
81 | "varsIgnorePattern": "^_",
82 | "args": "after-used",
83 | "argsIgnorePattern": "^_"
84 | }
85 | ],
86 | "curly": "error",
87 | "eqeqeq": "error",
88 | "no-throw-literal": "warn",
89 | "semi": "off",
90 | "react-hooks/rules-of-hooks": "error",
91 | "react-hooks/exhaustive-deps": "error",
92 | "react/prop-types": "off",
93 | "react/forbid-elements": [
94 | "error",
95 | {
96 | "forbid": [
97 | "a",
98 | "abbr",
99 | "address",
100 | "area",
101 | "article",
102 | "aside",
103 | "audio",
104 | "b",
105 | "base",
106 | "bdi",
107 | "bdo",
108 | "big",
109 | "blockquote",
110 | "body",
111 | "br",
112 | "button",
113 | "canvas",
114 | "caption",
115 | "cite",
116 | "code",
117 | "col",
118 | "colgroup",
119 | "data",
120 | "datalist",
121 | "dd",
122 | "del",
123 | "details",
124 | "dfn",
125 | "dialog",
126 | "div",
127 | "dl",
128 | "dt",
129 | "em",
130 | "embed",
131 | "fieldset",
132 | "figcaption",
133 | "figure",
134 | "footer",
135 | // "form",
136 | "h1",
137 | "h2",
138 | "h3",
139 | "h4",
140 | "h5",
141 | "h6",
142 | "head",
143 | "header",
144 | "hr",
145 | "html",
146 | "i",
147 | "iframe",
148 | "img",
149 | "input",
150 | "ins",
151 | "kbd",
152 | "keygen",
153 | "label",
154 | "legend",
155 | "li",
156 | "link",
157 | "main",
158 | "map",
159 | "mark",
160 | "menu",
161 | "menuitem",
162 | "meta",
163 | "meter",
164 | "nav",
165 | "noscript",
166 | "object",
167 | "ol",
168 | "optgroup",
169 | "option",
170 | "output",
171 | "p",
172 | "param",
173 | "picture",
174 | "pre",
175 | "progress",
176 | "q",
177 | "rp",
178 | "rt",
179 | "ruby",
180 | "s",
181 | "samp",
182 | "script",
183 | "section",
184 | "select",
185 | "small",
186 | "source",
187 | "span",
188 | "strong",
189 | "style",
190 | "sub",
191 | "summary",
192 | "sup",
193 | "table",
194 | "tbody",
195 | "td",
196 | "textarea",
197 | "tfoot",
198 | "th",
199 | "thead",
200 | "time",
201 | "title",
202 | "tr",
203 | "track",
204 | "u",
205 | "ul",
206 | "var",
207 | "video",
208 | "wbr"
209 | ]
210 | }
211 | ],
212 |
213 | "strict-dependencies/strict-dependencies": [
214 | "error",
215 | [
216 | {
217 | "module": "src/pages",
218 | "allowReferenceFrom": ["src/App.tsx"],
219 | "allowSameModule": false
220 | },
221 | {
222 | "module": "next/domain",
223 | "allowReferenceFrom": ["src/pages"],
224 | "allowSameModule": false
225 | },
226 | {
227 | "module": "src/ui",
228 | "allowReferenceFrom": ["src/domain"],
229 | "allowSameModule": true
230 | }
231 | ],
232 | {
233 | "resolveRelativeImport": true
234 | }
235 | ]
236 | },
237 | "ignorePatterns": ["out", "dist", "**/*.d.ts"],
238 | "settings": {
239 | "react": {
240 | "version": "detect"
241 | }
242 | }
243 | }
244 |
--------------------------------------------------------------------------------
/.github/FUNDING.yml:
--------------------------------------------------------------------------------
1 | github: minodisk
2 |
--------------------------------------------------------------------------------
/.github/workflows/publish.yaml:
--------------------------------------------------------------------------------
1 | name: Publish
2 |
3 | on:
4 | push:
5 | tags:
6 | - "*"
7 |
8 | jobs:
9 | publish:
10 | runs-on: ubuntu-latest
11 | steps:
12 | - uses: actions/checkout@v2
13 | - uses: actions/setup-node@v1
14 | with:
15 | node-version: 16
16 | - name: Install modules
17 | run: npm ci
18 | - name: "Authenticate to Google Cloud"
19 | uses: "google-github-actions/auth@v0.4.0"
20 | with:
21 | credentials_json: "${{ secrets.GOOGLE_CREDENTIALS }}"
22 | - name: Check
23 | run: npm run check
24 | - name: Lint
25 | run: npm run lint
26 | - name: Run headless test
27 | uses: GabrielBB/xvfb-action@v1.0
28 | with:
29 | run: npm test
30 | - name: Build
31 | run: npm run build
32 | - name: Publish to Open VSX Registry
33 | uses: HaaLeo/publish-vscode-extension@v1
34 | with:
35 | pat: ${{ secrets.OPEN_VSX_TOKEN }}
36 | - name: Publish to Visual Studio Marketplace
37 | uses: HaaLeo/publish-vscode-extension@v1
38 | with:
39 | pat: ${{ secrets.VS_MARKETPLACE_TOKEN }}
40 | registryUrl: https://marketplace.visualstudio.com
41 |
--------------------------------------------------------------------------------
/.github/workflows/test.yaml:
--------------------------------------------------------------------------------
1 | name: Test
2 |
3 | on:
4 | push:
5 | branches:
6 | - "*"
7 |
8 | jobs:
9 | test:
10 | runs-on: ubuntu-latest
11 | steps:
12 | - uses: actions/checkout@v2
13 | - uses: actions/setup-node@v1
14 | with:
15 | node-version: 16
16 | - name: Install modules
17 | run: npm ci
18 | - name: "Authenticate to Google Cloud"
19 | uses: "google-github-actions/auth@v0.4.0"
20 | with:
21 | credentials_json: "${{ secrets.GOOGLE_CREDENTIALS }}"
22 | - name: Check
23 | run: npm run check
24 | - name: Lint
25 | run: npm run lint
26 | - name: Run headless test
27 | uses: GabrielBB/xvfb-action@v1.0
28 | with:
29 | run: npm test
30 | - name: Coverage
31 | uses: codecov/codecov-action@v2
32 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | .vscode-test
3 | coverage
4 | dist
5 | out
6 | output
7 | node_modules
8 | service-account.json
9 | *.jsonl
10 | *.csv
11 | *.txt
12 | 名称未設定.*
13 |
--------------------------------------------------------------------------------
/.tool-versions:
--------------------------------------------------------------------------------
1 | python 3.9.13
2 | nodejs 20.10.0
3 |
--------------------------------------------------------------------------------
/.vscode/extensions.json:
--------------------------------------------------------------------------------
1 | {
2 | // See http://go.microsoft.com/fwlink/?LinkId=827846
3 | // for the documentation about the extensions.json format
4 | "recommendations": ["dbaeumer.vscode-eslint"]
5 | }
6 |
--------------------------------------------------------------------------------
/.vscode/launch.json:
--------------------------------------------------------------------------------
1 | // A launch configuration that compiles the extension and then opens it inside a new window
2 | // Use IntelliSense to learn about possible attributes.
3 | // Hover to view descriptions of existing attributes.
4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
5 | {
6 | "version": "0.2.0",
7 | "configurations": [
8 | {
9 | "name": "Run Extension",
10 | "type": "extensionHost",
11 | "request": "launch",
12 | "args": ["--extensionDevelopmentPath=${workspaceFolder}"],
13 | "outFiles": ["${workspaceFolder}/out/**/*.js"],
14 | "preLaunchTask": "${defaultBuildTask}"
15 | },
16 | {
17 | "name": "Extension Tests",
18 | "type": "extensionHost",
19 | "request": "launch",
20 | "args": [
21 | "--extensionDevelopmentPath=${workspaceFolder}",
22 | "--extensionTestsPath=${workspaceFolder}/out/test/suite/index"
23 | ],
24 | "outFiles": ["${workspaceFolder}/out/test/**/*.js"],
25 | "preLaunchTask": "${defaultBuildTask}"
26 | }
27 | ]
28 | }
29 |
--------------------------------------------------------------------------------
/.vscode/settings.json:
--------------------------------------------------------------------------------
1 | // Place your settings in this file to overwrite default and user settings.
2 | {
3 | "files.exclude": {
4 | "out": false // set this to true to hide the "out" folder with the compiled JS files
5 | },
6 | "search.exclude": {
7 | "out": true // set this to false to include "out" folder in search results
8 | },
9 | // Turn off tsc task auto detection since we have the necessary tasks as npm scripts
10 | "typescript.tsc.autoDetect": "off",
11 |
12 | "[html]": {
13 | "editor.formatOnSave": false
14 | },
15 |
16 | "editor.codeActionsOnSave": {
17 | "source.fixAll.eslint": "explicit"
18 | },
19 | "editor.defaultFormatter": "esbenp.prettier-vscode",
20 | "eslint.validate": ["javascript", "typescript"],
21 |
22 | "Prettier-SQL.SQLFlavourOverride": "bigquery",
23 | "Prettier-SQL.tabSizeOverride": 2,
24 | "Prettier-SQL.linesBetweenQueries": 1,
25 | "[sql]": {
26 | "editor.defaultFormatter": "inferrinizzard.prettier-sql-vscode",
27 | "editor.tabSize": 2
28 | },
29 |
30 | // "bigqueryRunner.keyFilename": "./service-account.json",
31 | "bigqueryRunner.projectId": "minodisk-api",
32 | "bigqueryRunner.tree.projectIds": ["vertect"]
33 | }
34 |
--------------------------------------------------------------------------------
/.vscode/tasks.json:
--------------------------------------------------------------------------------
1 | // See https://go.microsoft.com/fwlink/?LinkId=733558
2 | // for the documentation about the tasks.json format
3 | {
4 | "version": "2.0.0",
5 | "tasks": [
6 | {
7 | "type": "npm",
8 | "script": "watch",
9 | "problemMatcher": "$tsc-watch",
10 | "isBackground": true,
11 | "presentation": {
12 | "reveal": "never"
13 | },
14 | "group": {
15 | "kind": "build",
16 | "isDefault": true
17 | }
18 | }
19 | ]
20 | }
21 |
--------------------------------------------------------------------------------
/.vscodeignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | .github
3 | .vscode
4 | coverage
5 | dist
6 | node_modules
7 | output
8 | packages
9 | .eslintrc.json
10 | .gitignore
11 | package-lock.json
12 | service-account.json
13 |
--------------------------------------------------------------------------------
/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 | # Contributing Guide
2 |
3 | First, this VSCode extension is responsible for the following as it relates to BigQuery:
4 |
5 | - Dry-run the query
6 | - Run the query
7 | - Feedback errors to the editor
8 | - Display the results of the query
9 | - Limited static analysis of queries
10 | - For finding query parameters
11 |
12 | And it does not do the following:
13 |
14 | - Syntax highlighting
15 | - Static analysis of queries
16 | - For other than finding query parameters
17 |
18 | ## Issues
19 |
20 | ### Bugs
21 |
22 | When reporting a bug, please provide the smallest query that can reproduce the bug.
23 |
24 | ### Features
25 |
26 | New features and feature improvements should be proposed in an issue first. This may solve the problem faster than making a Pull Request without doing so.
27 |
28 | ## Pull Request
29 |
30 | If you send a Pull Request, please test and debug it.
31 |
32 | ### Install packages
33 |
34 | First, please install the dependent packages:
35 |
36 | ```
37 | npm install
38 | ```
39 |
40 | ### Test
41 |
42 | Unit test:
43 |
44 | ```
45 | npm test
46 | ```
47 |
48 | Compiler/Formatter check:
49 |
50 | ```
51 | npm run check
52 | ```
53 |
54 | Lint:
55 |
56 | ```
57 | npm run check
58 | ```
59 |
60 | ### Debug
61 |
62 | Debug it to check its behavior around the VSCode extension:
63 |
64 | 1. [Create a service account and its key](https://cloud.google.com/docs/authentication/getting-started) and save it in the project root with the name `service-account.json`.
65 | 1. Run `Shell Command: Install 'code' command in PATH` in VSCode command palette.
66 | 1. Run `npm run debug` to build BigQuery Runner and install it into VSCode.
67 | 1. Run the command `Developer: Reload Window` in VSCode to activate the newly installed BigQuery Runner.
68 | 1. Open a query file and run the command `BigQuery Runner: Run` in VSCode.
69 |
--------------------------------------------------------------------------------
/packages/codegen/assets-src/icon.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/packages/codegen/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "private": true,
3 | "name": "codegen",
4 | "version": "0.0.0",
5 | "description": "",
6 | "main": "index.js",
7 | "scripts": {
8 | "dev": "nodemon --exec 'npm run build'",
9 | "build": "run-p build:*",
10 | "build:readme": "ejs ./src/README.md.ejs -f ../../package.json -o ../../README.md",
11 | "build:dts": "ejs ./src/OrigConfig.d.ts.ejs -f ../../package.json -o ../extension/src/OrigConfig.d.ts"
12 | },
13 | "nodemonConfig": {
14 | "watch": [
15 | "./src/",
16 | "../../package.json"
17 | ],
18 | "ext": "ejs,json"
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/packages/codegen/src/OrigConfig.d.ts.ejs:
--------------------------------------------------------------------------------
1 | // DO NOT EDIT
2 | // This file is generated from gen-src/config.d.ts.ejs.
3 |
4 | <%
5 | const config = Object.keys(contributes.configuration.properties).reduce((obj, property) => {
6 | const value = contributes.configuration.properties[property];
7 | const properties = property.split('.');
8 | const last = properties.length - 1;
9 | properties.reduce((o, property, i) => {
10 | if (i === 0) {
11 | return o
12 | }
13 | if (i === last) {
14 | o[property] = value
15 | return o
16 | }
17 | if (!o[property]) {
18 | o[property] = {}
19 | }
20 | return o[property]
21 | }, obj)
22 | return obj
23 | }, {})
24 | function build(config, indent = 0) {
25 | return `Readonly<{
26 | ${Object.keys(config).map((key) =>
27 | `${' '.repeat(indent + 1)}${key}: ${(config[key].description != null) ? buildType(config[key]) : build(config[key], indent + 1)};`
28 | ).join('\n')}
29 | ${' '.repeat(indent)}}>`
30 | }
31 | function buildType(value) {
32 | if (value.enum) {
33 | return value.enum.map(JSON.stringify, JSON).map((t) => t === 'null' ? 'undefined' : t).join(' | ')
34 | }
35 | if (Array.isArray(value.type)) {
36 | return value.type.map((t) => t === 'null' ? 'undefined' : t).join(' | ')
37 | }
38 | if (value.type === 'array') {
39 | if (!value.default) {
40 | return `Array`;
41 | }
42 | if (value.items) {
43 | return `Array<${value.items.type}>`;
44 | }
45 | const typeSet = value.default.reduce((s, v) => {
46 | s.add(typeof v)
47 | return s
48 | }, new Set());
49 | return `Array<${ Array.from(typeSet).join(" | ") }>`
50 | }
51 | return value.type
52 | }
53 | -%>
54 | export type OrigConfig = <%- build(config) %>;
55 |
--------------------------------------------------------------------------------
/packages/codegen/src/README.md.ejs:
--------------------------------------------------------------------------------
1 |
3 | <% const base = 'https://storage.googleapis.com/bigquery-runner/' -%>
4 | # BigQuery Runner [](https://actions-badge.atrox.dev/minodisk/bigquery-runner/goto?ref=main) [](https://marketplace.visualstudio.com/items?itemName=minodisk.bigquery-runner) [](https://app.codecov.io/gh/minodisk/bigquery-runner/)
5 |
6 | An extension to query BigQuery directly and view the results in VSCode.
7 |
8 | 
9 |
10 | ## Features
11 |
12 | - Mark errors in queries.
13 | - If the query error can be corrected automatically, suggest a candidate for a quick fix.
14 | - Run queries:
15 | - from files.
16 | - from selected text.
17 | - with query parameters.
18 | - Display the results in viewers:
19 | - Rows
20 | - Fast rendering of large result tables.
21 | - Pagination.
22 | - Can be downloaded as a file.
23 | - Table
24 | - Temporary tables can be opened in yet another viewer.
25 | - Schema
26 | - Routine
27 | - Job
28 | - Download the rows in a variety of formats, both from the viewer and from the query file:
29 | - JSON Lines
30 | - JSON
31 | - CSV
32 | - Markdown
33 | - Plain text
34 | - Pretty formatted text like a table.
35 | - All operations can be executed from [commands](#commands).
36 | - Therefore, it can be set to be performed with [keyboard shortcuts](#keyboard-shortcuts).
37 | - Of course, it can also be operated from the GUI.
38 |
39 | ## Installation
40 |
41 | 1. Go to [the page of this extension in Visual Studio Marketplace](https://marketplace.visualstudio.com/items?itemName=minodisk.bigquery-runner).
42 | 2. Click the `Install` button.
43 | 3. This will open the VSCode page for this extension, and click the `Install` button.
44 |
45 | ## Authentication
46 |
47 | This extension requires authentication to the Google Cloud API. You can get started by authenticating in one of the following two ways.
48 |
49 | ### Gcloud Credential ([Recommended](https://cloud.google.com/iam/docs/best-practices-service-accounts#development))
50 |
51 |
56 |
57 | 1. [Install the gcloud CLI](https://cloud.google.com/sdk/docs/install).
58 | 1. Run [`gcloud auth application-default login`](https://cloud.google.com/sdk/gcloud/reference/auth/application-default) in your terminal.
59 | 1. Set `bigqueryRunner.projectId` in `setting.json`.
60 |
61 | - Don't set `bigqueryRunner.keyFilename` in `setting.json`.
62 | - Don't set `GOOGLE_APPLICATION_CREDENTIALS` as an environment variable.
63 |
64 | ### Service Account Key
65 |
66 | 1. [Create a service account and its key](https://cloud.google.com/docs/authentication/getting-started).
67 | - Give the service account the necessary roles. Such as [`roles/bigquery.user`](https://cloud.google.com/bigquery/docs/access-control#bigquery.user) for example.
68 | 1. Tell the key path to this extension in one of the following two ways:
69 | - Set the path to the key `bigqueryRunner.keyFilename` in `settings.json`.
70 | - [Set the path to the key as the environment variable `GOOGLE_APPLICATION_CREDENTIALS`](https://cloud.google.com/docs/authentication/getting-started#setting_the_environment_variable).
71 |
72 | ## Usage
73 |
74 | 1. Open a query file with `.bqsql` extension.
75 | 1. Open the command palette.
76 | 1. Run `BigQuery Runner: Run`.
77 |
78 | ### Query parameters
79 |
80 | If query has one or more named parameters, the extension will ask you for the values of that parameter. The values must be given in JSON format, e.g. quotation marks should be used for simple values such as `"20231224"`. See below for more complex examples.
81 |
82 | Once set, the parameters are saved for future use and should be reset if necessary using the [bigqueryRunner.clearParams](#bigquery-runner-clear-parameters) command.
83 |
84 | 
85 |
86 | ## Commands
87 |
88 | <% contributes.commands.forEach((command) => { -%>
89 | ### <%- command.title %>
90 |
91 | |ID|
92 | |---|
93 | |<%- command.command %>|
94 |
95 | <%- command.description %>
96 |
97 | <% }) -%>
98 | ## Configuration
99 |
100 | The extension can be customized by modifying your `settings.json` file. The available configuration options, and their defaults, are below.
101 |
102 | <% Object.keys(contributes.configuration.properties).forEach((property) => {
103 | const value = contributes.configuration.properties[property]
104 | -%>
105 | ### `<%- property -%>`
106 |
107 | |Type|Default|<% if (value.enum) { %>Enum|<% } %>
108 | |---|---|<% if (value.enum) { %>---|<% } %>
109 | |<%- Array.isArray(value.type) ? value.type.join(' | ') : value.type %>|<%- JSON.stringify(value.default) %>|<% if (value.enum) { %><%- value.enum.map(JSON.stringify, JSON).join(' | ') %>|<% } %>
110 |
111 | <%- value.description -%>
112 |
113 | <% if (value.screenshot) { -%>
114 | 
115 | <% } -%>
116 |
117 | <% }) -%>
118 |
119 | ## Additional Settings
120 |
121 | ### Keyboard shortcuts
122 |
123 | `keybindings.json`:
124 |
125 | ```json:keybindings.json
126 | [
127 | {
128 | "key": "cmd+enter",
129 | "command": "bigqueryRunner.run",
130 | "when": "resourceLangId in bigqueryRunner.languageIds || resourceExtname in bigqueryRunner.extensions"
131 | },
132 | {
133 | "key": "space h",
134 | "command": "bigqueryRunner.prevPage",
135 | "when": "resourceLangId in bigqueryRunner.languageIds || resourceExtname in bigqueryRunner.extensions && vim.mode == 'Normal' || vim.mode == 'Visual' || vim.mode == 'VisualBlock' || vim.mode == 'VisualLine'"
136 | },
137 | {
138 | "key": "space l",
139 | "command": "bigqueryRunner.nextPage",
140 | "when": "resourceLangId in bigqueryRunner.languageIds || resourceExtname in bigqueryRunner.extensions && vim.mode == 'Normal' || vim.mode == 'Visual' || vim.mode == 'VisualBlock' || vim.mode == 'VisualLine'"
141 | }
142 | ]
143 | ```
144 |
145 | ### Syntax highlighting `.bqsql` files as SQL
146 |
147 | `settings.json`:
148 |
149 | ```json:settings.json
150 | {
151 | "files.associations": {
152 | "*.bqsql": "sql"
153 | }
154 | }
155 | ```
156 |
157 | ## More documents
158 |
159 | ### Changelog
160 |
161 | If you want to know the difference between each release, see [CHANGELOG.md](CHANGELOG.md)
162 |
163 | ### Contributing
164 |
165 | When you create an issue, pull request, or fork see [CONTRIBUTING.md](CONTRIBUTING.md)
166 |
167 | ### License
168 |
169 | Apache 2.0 licensed. See the [LICENSE](LICENSE) file for details.
170 | This extension is forked from [google/vscode-bigquery](https://github.com/google/vscode-bigquery).
171 |
--------------------------------------------------------------------------------
/packages/core/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "private": true,
3 | "name": "core",
4 | "main": "src/index.ts",
5 | "scripts": {
6 | "lint": "eslint \"./src/**/*.ts\"",
7 | "check": "run-p check:*",
8 | "check:tsc": "tsc",
9 | "check:format": "npm run format -- --check",
10 | "fix": "run-p fix:*",
11 | "fix:lint": "npm run lint -- --fix",
12 | "fix:format": "npm run format -- --write",
13 | "test": "jest --silent=false",
14 | "test-coverage": "npm run test -- --coverage --coverageDirectory ../../coverage/core",
15 | "test-watch": "npm run test -- --watchAll",
16 | "format": "prettier src"
17 | },
18 | "jest": {
19 | "roots": [
20 | "/src"
21 | ],
22 | "testMatch": [
23 | "**/?(*.)+(spec|test).+(ts|tsx|js)"
24 | ],
25 | "transform": {
26 | "^.+\\.(ts|tsx)$": "ts-jest"
27 | }
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/packages/core/src/bigquery.test.ts:
--------------------------------------------------------------------------------
1 | import type { JobResponse } from "@google-cloud/bigquery";
2 | import {
3 | checkAuthentication,
4 | runQuery,
5 | getPage,
6 | toSerializablePage,
7 | } from "./bigquery";
8 |
9 | describe("bigquery", () => {
10 | describe("checkAuthentication", () => {
11 | it("should pass with valid setting", async () => {
12 | const getCredentials = jest.fn(() =>
13 | Promise.resolve({
14 | client_email: "",
15 | private_key: "",
16 | })
17 | );
18 | const result = await checkAuthentication({
19 | keyFilename: "XXXXX",
20 | getCredentials,
21 | });
22 | expect(result.success).toBeTruthy();
23 | expect(result.value).toEqual(undefined);
24 | });
25 |
26 | it("should fail invalid keyFilename", async () => {
27 | const getCredentials = jest.fn(() =>
28 | Promise.reject(
29 | new Error("Unable to detect a Project ID in the current environment.")
30 | )
31 | );
32 | const result = await checkAuthentication({
33 | keyFilename: "XXXXX",
34 | getCredentials,
35 | });
36 | expect(result.success).toBeFalsy();
37 | expect(result.value).toEqual({
38 | type: "Authentication",
39 | reason: `Unable to detect a Project ID in the current environment.`,
40 | hasKeyFilename: true,
41 | });
42 | });
43 |
44 | it("should fail without keyFilename", async () => {
45 | const getProjectId = jest.fn(() =>
46 | Promise.reject(
47 | new Error("Unable to detect a Project ID in the current environment.")
48 | )
49 | );
50 | const result = await checkAuthentication({
51 | getCredentials: getProjectId,
52 | });
53 | expect(result.success).toBeFalsy();
54 | expect(result.value).toEqual({
55 | type: "Authentication",
56 | reason: `Unable to detect a Project ID in the current environment.`,
57 | hasKeyFilename: false,
58 | });
59 | });
60 |
61 | it("should fail with unknown error", async () => {
62 | const getProjectId = jest.fn(() => Promise.reject(new Error("foo")));
63 | const result = await checkAuthentication({
64 | getCredentials: getProjectId,
65 | });
66 | expect(result.success).toBeFalsy();
67 | expect(result.value).toEqual({
68 | type: "Authentication",
69 | reason: `foo`,
70 | hasKeyFilename: false,
71 | });
72 | });
73 | });
74 |
75 | describe("createQueryJob", () => {
76 | it("should return job", async () => {
77 | const options = { dryRun: false };
78 | const job = {};
79 | const createQueryJobMock = jest.fn(() =>
80 | Promise.resolve([job] as unknown as JobResponse)
81 | );
82 | const result = await runQuery({
83 | createQueryJob: createQueryJobMock,
84 | options,
85 | });
86 | expect(createQueryJobMock).toBeCalledWith(options);
87 | expect(result.success).toBeTruthy();
88 | expect(result.value).toStrictEqual(job);
89 | });
90 |
91 | it("should fail with no position error", async () => {
92 | const options = { dryRun: false };
93 | const createQueryJobMock = jest.fn(() =>
94 | Promise.reject(new Error("foo"))
95 | );
96 | const result = await runQuery({
97 | createQueryJob: createQueryJobMock,
98 | options,
99 | });
100 | expect(createQueryJobMock).toBeCalledWith(options);
101 | expect(result.success).toBeFalsy();
102 | expect(result.value).toStrictEqual({
103 | type: "Query",
104 | reason: "foo",
105 | });
106 | });
107 |
108 | it("should fail with a position but no suggestion error", async () => {
109 | const options = { dryRun: false };
110 | const createQueryJobMock = jest.fn(() =>
111 | Promise.reject(new Error("foo bar baz at [3:40]"))
112 | );
113 | const result = await runQuery({
114 | createQueryJob: createQueryJobMock,
115 | options,
116 | });
117 | expect(createQueryJobMock).toBeCalledWith(options);
118 | expect(result.success).toBeFalsy();
119 | expect(result.value).toStrictEqual({
120 | type: "QueryWithPosition",
121 | reason: "foo bar baz",
122 | position: { line: 2, character: 39 },
123 | });
124 | });
125 |
126 | it("should fail with a position and a suggestion error", async () => {
127 | const options = { dryRun: false };
128 | const createQueryJobMock = jest.fn(() =>
129 | Promise.reject(
130 | new Error("Unrecognized name: foo; Did you mean bar? at [3:40]")
131 | )
132 | );
133 | const result = await runQuery({
134 | createQueryJob: createQueryJobMock,
135 | options,
136 | });
137 | expect(createQueryJobMock).toBeCalledWith(options);
138 | expect(result.success).toBeFalsy();
139 | expect(result.value).toStrictEqual({
140 | type: "QueryWithPosition",
141 | reason: "Unrecognized name: foo; Did you mean bar?",
142 | position: { line: 2, character: 39 },
143 | suggestion: {
144 | before: "foo",
145 | after: "bar",
146 | },
147 | });
148 | });
149 | });
150 |
151 | // Since just-worker uses JSON in its messaging interface,
152 | // it must be asserted in a serializable state.
153 | // Therefore, not only getPage but also toSerializablePage is tested.
154 | describe("getPage and toSerializablePage", () => {
155 | it("should return no page", () => {
156 | expect(
157 | toSerializablePage(
158 | getPage({
159 | totalRows: "0",
160 | rows: 0,
161 | })
162 | )
163 | ).toStrictEqual({
164 | hasPrev: false,
165 | hasNext: false,
166 | startRowNumber: "0",
167 | endRowNumber: "0",
168 | totalRows: "0",
169 | });
170 | });
171 |
172 | it("should return 1st page without prevPage", () => {
173 | expect(
174 | toSerializablePage(
175 | getPage({
176 | totalRows: "99999",
177 | rows: 100,
178 | })
179 | )
180 | ).toStrictEqual({
181 | hasPrev: false,
182 | hasNext: false,
183 | startRowNumber: "1",
184 | endRowNumber: "100",
185 | totalRows: "99999",
186 | });
187 | });
188 |
189 | it("should return n-th page with prevPage", () => {
190 | expect(
191 | toSerializablePage(
192 | getPage({
193 | totalRows: "99999",
194 | rows: 100,
195 | prevPage: { endRowNumber: 831n },
196 | })
197 | )
198 | ).toStrictEqual({
199 | hasPrev: true,
200 | hasNext: false,
201 | startRowNumber: "832",
202 | endRowNumber: "931",
203 | totalRows: "99999",
204 | });
205 | });
206 |
207 | it("should return hasNext true with nextPageToken", () => {
208 | expect(
209 | toSerializablePage(
210 | getPage({
211 | totalRows: "99999",
212 | rows: 100,
213 | nextPageToken: "XXXXXXXXXX",
214 | })
215 | )
216 | ).toStrictEqual({
217 | hasPrev: false,
218 | hasNext: true,
219 | startRowNumber: "1",
220 | endRowNumber: "100",
221 | totalRows: "99999",
222 | });
223 | });
224 | });
225 | });
226 |
--------------------------------------------------------------------------------
/packages/core/src/flat.ts:
--------------------------------------------------------------------------------
1 | /* eslint-disable @typescript-eslint/no-non-null-assertion */
2 |
3 | import type {
4 | Accessor,
5 | Column,
6 | Field,
7 | NumberedRows,
8 | Value,
9 | Row,
10 | StructuralRow,
11 | } from "shared";
12 | import { valueToPrimitive } from "./transform";
13 |
14 | export type Flat = Readonly<{
15 | heads: ReadonlyArray;
16 | getNumberedRows(
17 | props: Readonly<{
18 | structs: ReadonlyArray;
19 | rowNumberStart: bigint;
20 | }>
21 | ): ReadonlyArray;
22 | }>;
23 |
24 | export function createFlat(fields: ReadonlyArray): Flat {
25 | const heads = fieldsToHeads(fields, []);
26 | const columns = fieldsToColumns(fields);
27 | return {
28 | heads,
29 | getNumberedRows({ structs, rowNumberStart }) {
30 | return structs.map((struct, i) => {
31 | const rows = structToRows({ heads, columns, struct });
32 | return {
33 | rowNumber: `${rowNumberStart + BigInt(i)}`,
34 | rows,
35 | };
36 | });
37 | },
38 | };
39 | }
40 |
41 | function fieldsToHeads(
42 | fields: ReadonlyArray,
43 | names: Array
44 | ): ReadonlyArray {
45 | return fields.flatMap((field) => {
46 | if (field.type === "STRUCT" || field.type === "RECORD") {
47 | return fieldsToHeads(field.fields, [...names, field.name]);
48 | }
49 | return {
50 | ...field,
51 | id: [...names, field.name].join("."),
52 | };
53 | });
54 | }
55 |
56 | function fieldsToColumns(fields: ReadonlyArray): ReadonlyArray {
57 | return fields.flatMap((field) => {
58 | if (field.type === "STRUCT" || field.type === "RECORD") {
59 | return fieldsToColumns(field.fields).map((columns) => [
60 | {
61 | ...field,
62 | id: field.name,
63 | },
64 | ...columns.map((column) => ({
65 | ...column,
66 | id: `${field.name}.${column.id}`,
67 | })),
68 | ]);
69 | }
70 | return [[{ ...field, id: field.name }]];
71 | });
72 | }
73 |
74 | function structToRows({
75 | heads,
76 | columns,
77 | struct,
78 | }: Readonly<{
79 | heads: ReadonlyArray;
80 | columns: ReadonlyArray;
81 | struct: StructuralRow;
82 | }>): ReadonlyArray {
83 | const rows: Array = [];
84 | const depths = new Array(columns.length).fill(0);
85 | const createFillWithRow = ({ columnIndex }: { columnIndex: number }) => {
86 | return ({ accessor, value }: { value: Value; accessor: Accessor }) => {
87 | if (!rows[depths[columnIndex]!]) {
88 | rows[depths[columnIndex]!] = heads.map(({ id }) => ({
89 | id,
90 | value: undefined,
91 | }));
92 | }
93 | rows[depths[columnIndex]!]![columnIndex] = {
94 | id: accessor.id,
95 | value: valueToPrimitive(value),
96 | };
97 | depths[columnIndex]! += 1;
98 | };
99 | };
100 |
101 | columns.forEach((column, columnIndex) =>
102 | walk({
103 | struct,
104 | column,
105 | accessorIndex: 0,
106 | fill: createFillWithRow({ columnIndex }),
107 | })
108 | );
109 |
110 | return rows;
111 | }
112 |
113 | function walk({
114 | struct,
115 | column,
116 | accessorIndex,
117 | fill,
118 | }: Readonly<{
119 | struct: StructuralRow;
120 | column: Column;
121 | accessorIndex: number;
122 | fill(props: { accessor: Accessor; value: Value }): void;
123 | }>): void {
124 | let s: StructuralRow = struct;
125 | let isNull = false;
126 | for (let ai = accessorIndex; ai < column.length; ai += 1) {
127 | const accessor = column[ai]!;
128 | if (accessor.mode === "REPEATED") {
129 | if (accessor.type === "STRUCT" || accessor.type === "RECORD") {
130 | (s[accessor.name] as ReadonlyArray).forEach((struct) => {
131 | walk({
132 | struct,
133 | column,
134 | accessorIndex: ai + 1,
135 | fill,
136 | });
137 | });
138 | break;
139 | }
140 | (s[accessor.name] as ReadonlyArray).forEach((value) =>
141 | fill({
142 | accessor,
143 | value,
144 | })
145 | );
146 | } else {
147 | if (accessor.type === "STRUCT" || accessor.type === "RECORD") {
148 | if (!isNull) {
149 | s = s[accessor.name] as StructuralRow;
150 | isNull = s === null;
151 | }
152 | continue;
153 | }
154 | fill({
155 | accessor,
156 | value: isNull ? null : (s[accessor.name] as Value),
157 | });
158 | }
159 | }
160 | }
161 |
--------------------------------------------------------------------------------
/packages/core/src/formatter.ts:
--------------------------------------------------------------------------------
1 | import type { Options } from "csv-stringify";
2 | import { stringify } from "csv-stringify";
3 | import EasyTable from "easy-table";
4 | import type { StructuralRow } from "shared";
5 | import type { Flat } from "./flat";
6 |
7 | export type Formatter = Readonly<{
8 | head(): void;
9 | body(
10 | props: Readonly<{
11 | structs: ReadonlyArray;
12 | rowNumberStart: bigint;
13 | }>
14 | ): void;
15 | foot(): Promise;
16 | }>;
17 |
18 | export function createJSONLinesFormatter({
19 | writer,
20 | }: {
21 | writer: NodeJS.WritableStream;
22 | }): Formatter {
23 | return {
24 | head() {
25 | // do nothing
26 | },
27 | body({
28 | structs,
29 | }: Readonly<{
30 | structs: ReadonlyArray;
31 | }>) {
32 | structs.forEach((struct) => {
33 | writer.write(JSON.stringify(struct));
34 | writer.write("\n");
35 | });
36 | },
37 | async foot(): Promise {
38 | return new Promise((resolve) => writer.end(resolve));
39 | },
40 | };
41 | }
42 |
43 | export function createJSONFormatter({
44 | writer,
45 | }: {
46 | writer: NodeJS.WritableStream;
47 | }): Formatter {
48 | let first = true;
49 | return {
50 | head() {
51 | writer.write("[");
52 | },
53 | body({
54 | structs,
55 | }: Readonly<{
56 | structs: ReadonlyArray;
57 | }>) {
58 | structs.forEach((struct) => {
59 | if (!first) {
60 | writer.write(",");
61 | }
62 | writer.write(JSON.stringify(struct));
63 | first = false;
64 | });
65 | },
66 | async foot(): Promise {
67 | writer.write("]\n");
68 | return new Promise((resolve) => writer.end(resolve));
69 | },
70 | };
71 | }
72 |
73 | export function createCSVFormatter({
74 | flat,
75 | writer,
76 | options,
77 | }: {
78 | flat: Flat;
79 | writer: NodeJS.WritableStream;
80 | options: Options;
81 | }): Formatter {
82 | const stringifier = stringify(
83 | options.header
84 | ? { ...options, columns: flat.heads.map(({ id }) => id) }
85 | : options
86 | );
87 | stringifier.pipe(writer);
88 | const promise = new Promise((resolve, reject) => {
89 | stringifier.on("error", reject);
90 | stringifier.on("finish", () => resolve());
91 | });
92 | return {
93 | head() {
94 | // do nothing
95 | },
96 | body({ structs, rowNumberStart }) {
97 | if (structs.length === 0) {
98 | return;
99 | }
100 | flat.getNumberedRows({ structs, rowNumberStart }).forEach(({ rows }) =>
101 | rows.forEach((row) => {
102 | stringifier.write(
103 | row.map(({ value }) =>
104 | value === null || value === undefined ? "" : `${value}`
105 | )
106 | );
107 | })
108 | );
109 | },
110 | async foot() {
111 | stringifier.end();
112 | return promise;
113 | },
114 | };
115 | }
116 |
117 | export function createMarkdownFormatter({
118 | flat,
119 | writer,
120 | }: {
121 | flat: Flat;
122 | writer: NodeJS.WritableStream;
123 | }): Formatter {
124 | return {
125 | head() {
126 | if (flat.heads.length === 0) {
127 | return;
128 | }
129 | writer.write(`|`);
130 | flat.heads.forEach(({ id }) => writer.write(`${id}|`));
131 | writer.write(`\n`);
132 | writer.write(`|`);
133 | flat.heads.forEach(() => writer.write("---|"));
134 | writer.write(`\n`);
135 | },
136 | body({
137 | structs,
138 | rowNumberStart,
139 | }: Readonly<{
140 | structs: ReadonlyArray;
141 | rowNumberStart: bigint;
142 | }>) {
143 | flat.getNumberedRows({ structs, rowNumberStart }).forEach(({ rows }) =>
144 | rows.forEach((row) => {
145 | writer.write(`|`);
146 | row.forEach(({ value }) => {
147 | if (value === undefined) {
148 | writer.write("|");
149 | return;
150 | }
151 | if (typeof value !== "string") {
152 | writer.write(`${value}|`);
153 | return;
154 | }
155 | writer.write(value.replace(/\n/g, "
"));
156 | writer.write("|");
157 | });
158 | writer.write(`\n`);
159 | })
160 | );
161 | },
162 | async foot(): Promise {
163 | return new Promise((resolve) => writer.end(resolve));
164 | },
165 | };
166 | }
167 |
168 | export function createTableFormatter({
169 | flat,
170 | writer,
171 | }: {
172 | flat: Flat;
173 | writer: NodeJS.WritableStream;
174 | }): Formatter {
175 | return {
176 | head() {
177 | // do nothing
178 | },
179 | body({
180 | structs,
181 | rowNumberStart,
182 | }: Readonly<{
183 | structs: ReadonlyArray;
184 | rowNumberStart: bigint;
185 | }>) {
186 | const t = new EasyTable();
187 | flat.getNumberedRows({ structs, rowNumberStart }).forEach(({ rows }) => {
188 | rows.forEach((row) => {
189 | row.forEach((cell) => t.cell(cell.id, cell.value));
190 | t.newRow();
191 | });
192 | });
193 | writer.write(t.toString().trimEnd());
194 | writer.write("\n");
195 | },
196 | async foot(): Promise {
197 | return new Promise((resolve) => writer.end(resolve));
198 | },
199 | };
200 | }
201 |
--------------------------------------------------------------------------------
/packages/core/src/gcloud.ts:
--------------------------------------------------------------------------------
1 | import { spawn } from "child_process";
2 | import type { Logger } from "extension/src/logger";
3 | import { errorToString, tryCatch } from "shared";
4 |
5 | export type Gcloud = ReturnType;
6 |
7 | export const createGcloud = ({ logger }: { logger: Logger }) => {
8 | const l = logger.createChild("Gcloud");
9 | const gcloud = (args: ReadonlyArray) => {
10 | return new Promise>((resolve, reject) => {
11 | const stream = spawn("gcloud", args);
12 | const outs: Array = [];
13 | const errs: Array = [];
14 | stream.stdout.on("data", (data) => {
15 | l.log(`stdout: ${data}`);
16 | outs.push(data);
17 | });
18 | stream.stderr.on("data", (data) => {
19 | l.log(`stderr: ${data}`);
20 | errs.push(data);
21 | });
22 | stream.on("close", (code) => {
23 | l.log(`close: ${code}`);
24 | if (code !== 0) {
25 | reject(errs);
26 | return;
27 | }
28 | resolve(outs);
29 | });
30 | });
31 | };
32 | return {
33 | async login() {
34 | return tryCatch(
35 | () => gcloud(["auth", "application-default", "login"]),
36 | (err) => ({
37 | type: "LoginFailed",
38 | message: errorToString(err),
39 | })
40 | );
41 | },
42 |
43 | async logout() {
44 | return tryCatch(
45 | () => gcloud(["auth", "application-default", "revoke", "--quiet"]),
46 | (err) => ({
47 | type: "LogoutFailed",
48 | message: errorToString(err),
49 | })
50 | );
51 | },
52 |
53 | dispose() {
54 | // do nothing
55 | },
56 | };
57 | };
58 |
--------------------------------------------------------------------------------
/packages/core/src/index.ts:
--------------------------------------------------------------------------------
1 | export * from "./bigquery";
2 | export * from "./flat";
3 | export * from "./formatter";
4 | export * from "./gcloud";
5 | export * from "./parser";
6 |
--------------------------------------------------------------------------------
/packages/core/src/metadata.json:
--------------------------------------------------------------------------------
1 | {
2 | "kind": "bigquery#job",
3 | "etag": "1PYwXDe/uYzFCOCwqlpyWw==",
4 | "id": "minodisk-api:US.9552c26b-2151-45b2-9536-ceb50fc7b5a1",
5 | "selfLink": "https://bigquery.googleapis.com/bigquery/v2/projects/minodisk-api/jobs/9552c26b-2151-45b2-9536-ceb50fc7b5a1?location=US",
6 | "user_email": "bigquery-runner@minodisk-api.iam.gserviceaccount.com",
7 | "configuration": {
8 | "query": {
9 | "query": "select\n en_label,\n labels,\nfrom\n `bigquery-public-data.wikipedia.wikidata`\nwhere\n array_length(labels) < 4\nlimit\n 580\n",
10 | "destinationTable": {
11 | "projectId": "minodisk-api",
12 | "datasetId": "_974002322e1183b3df64c0f31d9b6832d25246ef",
13 | "tableId": "anond8e3e2a367e0234e97050be371e91fe8dd62ed85"
14 | },
15 | "writeDisposition": "WRITE_TRUNCATE",
16 | "priority": "INTERACTIVE",
17 | "useLegacySql": false
18 | },
19 | "jobType": "QUERY"
20 | },
21 | "jobReference": {
22 | "projectId": "minodisk-api",
23 | "jobId": "9552c26b-2151-45b2-9536-ceb50fc7b5a1",
24 | "location": "US"
25 | },
26 | "statistics": {
27 | "creationTime": "1649770220681",
28 | "startTime": "1649770220849",
29 | "endTime": "1649770220887",
30 | "totalBytesProcessed": "0",
31 | "query": {
32 | "totalBytesProcessed": "0",
33 | "totalBytesBilled": "0",
34 | "cacheHit": true,
35 | "statementType": "SELECT"
36 | }
37 | },
38 | "status": { "state": "DONE" }
39 | }
40 |
--------------------------------------------------------------------------------
/packages/core/src/parser.test.ts:
--------------------------------------------------------------------------------
1 | import { createParser } from "./parser";
2 |
3 | describe("createParser", () => {
4 | it("should parse params", () => {
5 | const query = `
6 | SELECT
7 | corpus,
8 | word,
9 | word_count
10 | FROM
11 | \`bigquery-public-data.samples.shakespeare\`
12 | WHERE
13 | corpus = @corpus -- "romeoandjuliet"
14 | AND word_count >= @min_word_count -- 250
15 | ORDER BY
16 | word_count DESC
17 | `;
18 | const { parse } = createParser();
19 | const tokens = parse(query);
20 | expect(tokens).toStrictEqual([
21 | {
22 | type: "RESERVED_SELECT",
23 | raw: "SELECT",
24 | text: "SELECT",
25 | start: 1,
26 | precedingWhitespace: "\n",
27 | line: 1,
28 | character: 0,
29 | },
30 | {
31 | type: "IDENTIFIER",
32 | raw: "corpus",
33 | text: "corpus",
34 | start: 12,
35 | precedingWhitespace: "\n ",
36 | line: 2,
37 | character: 4,
38 | },
39 | {
40 | type: "COMMA",
41 | raw: ",",
42 | text: ",",
43 | start: 18,
44 | precedingWhitespace: undefined,
45 | line: 2,
46 | character: 10,
47 | },
48 | {
49 | type: "IDENTIFIER",
50 | raw: "word",
51 | text: "word",
52 | start: 24,
53 | precedingWhitespace: "\n ",
54 | line: 3,
55 | character: 4,
56 | },
57 | {
58 | type: "COMMA",
59 | raw: ",",
60 | text: ",",
61 | start: 28,
62 | precedingWhitespace: undefined,
63 | line: 3,
64 | character: 8,
65 | },
66 | {
67 | type: "IDENTIFIER",
68 | raw: "word_count",
69 | text: "word_count",
70 | start: 34,
71 | precedingWhitespace: "\n ",
72 | line: 4,
73 | character: 4,
74 | },
75 | {
76 | type: "RESERVED_CLAUSE",
77 | raw: "FROM",
78 | text: "FROM",
79 | start: 45,
80 | precedingWhitespace: "\n",
81 | line: 5,
82 | character: 0,
83 | },
84 | {
85 | type: "QUOTED_IDENTIFIER",
86 | raw: "`bigquery-public-data.samples.shakespeare`",
87 | text: "`bigquery-public-data.samples.shakespeare`",
88 | start: 54,
89 | precedingWhitespace: "\n ",
90 | line: 6,
91 | character: 4,
92 | },
93 | {
94 | type: "RESERVED_CLAUSE",
95 | raw: "WHERE",
96 | text: "WHERE",
97 | start: 97,
98 | precedingWhitespace: "\n",
99 | line: 7,
100 | character: 0,
101 | },
102 | {
103 | type: "IDENTIFIER",
104 | raw: "corpus",
105 | text: "corpus",
106 | start: 107,
107 | precedingWhitespace: "\n ",
108 | line: 8,
109 | character: 4,
110 | },
111 | {
112 | type: "OPERATOR",
113 | raw: "=",
114 | text: "=",
115 | start: 114,
116 | precedingWhitespace: " ",
117 | line: 8,
118 | character: 11,
119 | },
120 | {
121 | type: "NAMED_PARAMETER",
122 | raw: "@corpus",
123 | text: "@corpus",
124 | start: 116,
125 | key: "corpus",
126 | precedingWhitespace: " ",
127 | line: 8,
128 | character: 13,
129 | },
130 | {
131 | type: "LINE_COMMENT",
132 | raw: '-- "romeoandjuliet"',
133 | text: '-- "romeoandjuliet"',
134 | start: 124,
135 | precedingWhitespace: " ",
136 | line: 8,
137 | character: 21,
138 | },
139 | {
140 | type: "AND",
141 | raw: "AND",
142 | text: "AND",
143 | start: 148,
144 | precedingWhitespace: "\n ",
145 | line: 9,
146 | character: 4,
147 | },
148 | {
149 | type: "IDENTIFIER",
150 | raw: "word_count",
151 | text: "word_count",
152 | start: 152,
153 | precedingWhitespace: " ",
154 | line: 9,
155 | character: 8,
156 | },
157 | {
158 | type: "OPERATOR",
159 | raw: ">=",
160 | text: ">=",
161 | start: 163,
162 | precedingWhitespace: " ",
163 | line: 9,
164 | character: 19,
165 | },
166 | {
167 | type: "NAMED_PARAMETER",
168 | raw: "@min_word_count",
169 | text: "@min_word_count",
170 | start: 166,
171 | key: "min_word_count",
172 | precedingWhitespace: " ",
173 | line: 9,
174 | character: 22,
175 | },
176 | {
177 | type: "LINE_COMMENT",
178 | raw: "-- 250",
179 | text: "-- 250",
180 | start: 182,
181 | precedingWhitespace: " ",
182 | line: 9,
183 | character: 38,
184 | },
185 | {
186 | type: "RESERVED_CLAUSE",
187 | raw: "ORDER BY",
188 | text: "ORDER BY",
189 | start: 189,
190 | precedingWhitespace: "\n",
191 | line: 10,
192 | character: 0,
193 | },
194 | {
195 | type: "IDENTIFIER",
196 | raw: "word_count",
197 | text: "word_count",
198 | start: 202,
199 | precedingWhitespace: "\n ",
200 | line: 11,
201 | character: 4,
202 | },
203 | {
204 | type: "RESERVED_KEYWORD",
205 | raw: "DESC",
206 | text: "DESC",
207 | start: 213,
208 | precedingWhitespace: " ",
209 | line: 11,
210 | character: 15,
211 | },
212 | ]);
213 | });
214 | });
215 |
--------------------------------------------------------------------------------
/packages/core/src/parser.ts:
--------------------------------------------------------------------------------
1 | import { formatters } from "sql-formatter";
2 | import type { Token as OrigToken } from "sql-formatter/lib/src/lexer/token";
3 |
4 | export type Token = OrigToken & {
5 | line: number;
6 | character: number;
7 | };
8 |
9 | export const createParser = () => {
10 | const tokenizer = formatters.bigquery.prototype.tokenizer();
11 |
12 | const breaks = (precedingWhitespace: string) => {
13 | let newLines = 0;
14 | let charactersBeforeLastLine = 0;
15 | for (let i = 0; i < precedingWhitespace.length; i++) {
16 | const char = precedingWhitespace[i];
17 | if (char === "\n") {
18 | newLines += 1;
19 | charactersBeforeLastLine = i + 1;
20 | }
21 | }
22 | return { newLines, charactersBeforeLastLine };
23 | };
24 |
25 | return {
26 | parse(query: string): Array {
27 | const tokens = tokenizer.tokenize(query, {});
28 | let line = 0;
29 | let caret = 0;
30 | let lineStart = 0;
31 | return tokens.map((token) => {
32 | if (!token.precedingWhitespace) {
33 | const t = {
34 | ...token,
35 | line,
36 | character: token.start - lineStart,
37 | };
38 | caret += token.raw.length;
39 | return t;
40 | }
41 | const { newLines, charactersBeforeLastLine } = breaks(
42 | token.precedingWhitespace
43 | );
44 | line += newLines;
45 | if (newLines) {
46 | lineStart = caret + charactersBeforeLastLine;
47 | }
48 | const t = {
49 | ...token,
50 | line,
51 | character: token.start - lineStart,
52 | };
53 | caret += token.precedingWhitespace.length + token.raw.length;
54 | return t;
55 | });
56 | },
57 | };
58 | };
59 |
--------------------------------------------------------------------------------
/packages/core/src/table.json:
--------------------------------------------------------------------------------
1 | {
2 | "kind": "bigquery#table",
3 | "etag": "Ps9LfjYICT3GQ95Gociing==",
4 | "id": "minodisk-api:_974002322e1183b3df64c0f31d9b6832d25246ef.anond8e3e2a367e0234e97050be371e91fe8dd62ed85",
5 | "selfLink": "https://bigquery.googleapis.com/bigquery/v2/projects/minodisk-api/datasets/_974002322e1183b3df64c0f31d9b6832d25246ef/tables/anond8e3e2a367e0234e97050be371e91fe8dd62ed85",
6 | "tableReference": {
7 | "projectId": "minodisk-api",
8 | "datasetId": "_974002322e1183b3df64c0f31d9b6832d25246ef",
9 | "tableId": "anond8e3e2a367e0234e97050be371e91fe8dd62ed85"
10 | },
11 | "schema": {
12 | "fields": [
13 | { "name": "en_label", "type": "STRING", "mode": "NULLABLE" },
14 | {
15 | "name": "labels",
16 | "type": "RECORD",
17 | "mode": "REPEATED",
18 | "fields": [
19 | { "name": "language", "type": "STRING", "mode": "NULLABLE" },
20 | { "name": "value", "type": "STRING", "mode": "NULLABLE" }
21 | ]
22 | }
23 | ]
24 | },
25 | "numBytes": "0",
26 | "numLongTermBytes": "0",
27 | "numRows": "580",
28 | "creationTime": "1649767720933",
29 | "expirationTime": "1649856620804",
30 | "lastModifiedTime": "1649770220804",
31 | "type": "TABLE",
32 | "location": "US",
33 | "numTotalLogicalBytes": "0",
34 | "numActiveLogicalBytes": "0",
35 | "numLongTermLogicalBytes": "0"
36 | }
37 |
--------------------------------------------------------------------------------
/packages/core/src/transform.test.ts:
--------------------------------------------------------------------------------
1 | import {
2 | BigQueryDate,
3 | BigQueryDatetime,
4 | BigQueryInt,
5 | BigQueryTime,
6 | BigQueryTimestamp,
7 | Geography,
8 | } from "@google-cloud/bigquery";
9 | import type { Value } from "shared";
10 | import { valueToPrimitive } from "./transform";
11 |
12 | describe("transform", () => {
13 | describe("valueToPrimitive", () => {
14 | const cases: Array<
15 | Readonly<{
16 | message: string;
17 | value: Value;
18 | expected: null | boolean | number | string;
19 | }>
20 | > = [
21 | {
22 | message: "should return boolean when boolean is input",
23 | value: true,
24 | expected: true,
25 | },
26 | {
27 | message: "should return number when number is input",
28 | value: 123.45,
29 | expected: 123.45,
30 | },
31 | {
32 | message: "should return string when string is input",
33 | value: "foo",
34 | expected: "foo",
35 | },
36 | {
37 | message: "should return null when null is input",
38 | value: null,
39 | expected: null,
40 | },
41 | {
42 | message: "should return number when BigInt is input",
43 | value: BigInt("99999999999999999999"),
44 | expected: "99999999999999999999",
45 | },
46 | {
47 | message: "should return string when Buffer is input",
48 | value: Buffer.from("foo"),
49 | expected: "foo",
50 | },
51 | {
52 | message: "should return string when BigQueryDate is input",
53 | value: new Geography("POINT(-70.8754261 42.0625498)"),
54 | expected: "POINT(-70.8754261 42.0625498)",
55 | },
56 | {
57 | message: "should return string when BigQueryDate is input",
58 | value: new BigQueryDate("2006-01-02"),
59 | expected: "2006-01-02",
60 | },
61 | {
62 | message: "should return string when BigQueryDatetime is input",
63 | value: new BigQueryDatetime("2006-01-02T15:04:05Z"),
64 | expected: "2006-01-02 15:04:05",
65 | },
66 | {
67 | message: "should return string when BigQueryInt is input",
68 | value: new BigQueryInt(10),
69 | expected: "10",
70 | },
71 | {
72 | message: "should return string when BigQueryTime is input",
73 | value: new BigQueryTime("15:04:05"),
74 | expected: "15:04:05",
75 | },
76 | {
77 | message: "should return string when BigQueryTimestamp is input",
78 | value: new BigQueryTimestamp("2006-01-02T15:04:05Z"),
79 | expected: "2006-01-02T15:04:05.000Z",
80 | },
81 | ];
82 | cases.forEach(({ message, value, expected }) =>
83 | it(message, () => {
84 | expect(valueToPrimitive(value)).toStrictEqual(expected);
85 | })
86 | );
87 | });
88 | });
89 |
--------------------------------------------------------------------------------
/packages/core/src/transform.ts:
--------------------------------------------------------------------------------
1 | import {
2 | BigQueryDate,
3 | BigQueryDatetime,
4 | BigQueryInt,
5 | BigQueryTime,
6 | BigQueryTimestamp,
7 | Geography,
8 | } from "@google-cloud/bigquery";
9 | import type { Primitive, Value } from "shared";
10 |
11 | export function valueToPrimitive(value: Value): Primitive {
12 | if (
13 | value === null ||
14 | typeof value === "number" ||
15 | typeof value === "string" ||
16 | typeof value === "boolean"
17 | ) {
18 | return value;
19 | }
20 | if (
21 | value instanceof BigQueryDate ||
22 | value instanceof BigQueryDatetime ||
23 | value instanceof BigQueryInt ||
24 | value instanceof BigQueryTime ||
25 | value instanceof BigQueryTimestamp ||
26 | value instanceof Geography
27 | ) {
28 | return value.value;
29 | }
30 | if (Buffer.isBuffer(value)) {
31 | return value.toString();
32 | }
33 | return `${value}`;
34 | }
35 |
--------------------------------------------------------------------------------
/packages/core/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "../../tsconfig.json",
3 | "compilerOptions": {
4 | "rootDir": "src"
5 | },
6 | "include": ["src"]
7 | }
8 |
--------------------------------------------------------------------------------
/packages/extension/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "private": true,
3 | "name": "extension",
4 | "main": "src/extension.ts",
5 | "scripts": {
6 | "dev": "run-p build:watch",
7 | "build": "esbuild ./src/index.ts --bundle --outfile=../../out/extension/index.js --external:vscode --format=cjs --platform=node",
8 | "build:watch": "npm run build -- --sourcemap --watch",
9 | "build-production": "npm run build -- --minify",
10 | "build-debug": "npm run build -- --sourcemap",
11 | "lint": "eslint \"./src/**/*.ts\"",
12 | "fix": "run-p fix:*",
13 | "fix:lint": "npm run lint -- --fix",
14 | "fix:format": "npm run format -- --write",
15 | "check": "run-p check:*",
16 | "check:tsc": "tsc",
17 | "check:format": "npm run format -- --check",
18 | "format": "prettier --write src"
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/packages/extension/src/OrigConfig.d.ts:
--------------------------------------------------------------------------------
1 | // DO NOT EDIT
2 | // This file is generated from gen-src/config.d.ts.ejs.
3 |
4 | export type OrigConfig = Readonly<{
5 | keyFilename: string | undefined;
6 | projectId: string | undefined;
7 | location: string | undefined;
8 | useLegacySql: boolean;
9 | maximumBytesBilled: string | undefined;
10 | extensions: Array;
11 | languageIds: Array;
12 | icon: boolean;
13 | defaultDataset: Readonly<{
14 | datasetId: string | undefined;
15 | projectId: string | undefined;
16 | }>;
17 | downloader: Readonly<{
18 | csv: Readonly<{
19 | delimiter: string;
20 | header: boolean;
21 | }>;
22 | rowsPerPage: number | undefined;
23 | }>;
24 | tree: Readonly<{
25 | projectIds: Array;
26 | }>;
27 | viewer: Readonly<{
28 | column: string | number;
29 | rowsPerPage: number | undefined;
30 | }>;
31 | previewer: Readonly<{
32 | rowsPerPage: number | undefined;
33 | }>;
34 | statusBarItem: Readonly<{
35 | align: "left" | "right" | undefined;
36 | priority: number | undefined;
37 | }>;
38 | validation: Readonly<{
39 | enabled: boolean;
40 | debounceInterval: number;
41 | }>;
42 | }>;
43 |
--------------------------------------------------------------------------------
/packages/extension/src/checksum.ts:
--------------------------------------------------------------------------------
1 | import { createHash } from "crypto";
2 |
3 | export const checksum = (text: string): string =>
4 | createHash("md5")
5 | .update(text.trimStart().trimEnd().replace(/\s+/g, " "))
6 | .digest("hex");
7 |
--------------------------------------------------------------------------------
/packages/extension/src/configManager.ts:
--------------------------------------------------------------------------------
1 | import { isAbsolute, join } from "path";
2 | import type { Disposable } from "vscode";
3 | import { commands, workspace } from "vscode";
4 | import type { OrigConfig } from "./OrigConfig";
5 |
6 | export type ConfigManager = ReturnType;
7 |
8 | export type Config = Omit & {
9 | defaultDataset?: OrigConfig["defaultDataset"];
10 | };
11 |
12 | type Callback = (config: Config) => unknown;
13 |
14 | export function createConfigManager(section: string) {
15 | let config = getConfig(section);
16 | setContext(config);
17 |
18 | const callbacks = new Set();
19 |
20 | const subscriptions = [
21 | workspace.onDidChangeConfiguration((e) => {
22 | if (!e.affectsConfiguration(section)) {
23 | return;
24 | }
25 |
26 | config = getConfig(section);
27 | setContext(config);
28 |
29 | callbacks.forEach((cb) => cb(config));
30 | }),
31 | ];
32 |
33 | return {
34 | get(): Config {
35 | return config;
36 | },
37 |
38 | onChange(callback: Callback): Disposable {
39 | callbacks.add(callback);
40 | return {
41 | dispose() {
42 | callbacks.delete(callback);
43 | },
44 | };
45 | },
46 |
47 | dispose(): void {
48 | subscriptions.forEach((s) => s.dispose());
49 | callbacks.clear();
50 | },
51 | };
52 | }
53 |
54 | function getConfig(section: string): Config {
55 | const config = workspace.getConfiguration(section) as unknown as OrigConfig;
56 | return {
57 | ...config,
58 | defaultDataset:
59 | config.defaultDataset.datasetId || config.defaultDataset.projectId
60 | ? config.defaultDataset
61 | : undefined,
62 | downloader: {
63 | ...config.downloader,
64 | rowsPerPage:
65 | config.downloader.rowsPerPage === undefined ||
66 | config.downloader.rowsPerPage === null
67 | ? undefined
68 | : config.downloader.rowsPerPage,
69 | },
70 | keyFilename:
71 | config.keyFilename === null || config.keyFilename === undefined
72 | ? undefined
73 | : isAbsolute(config.keyFilename) ||
74 | !workspace.workspaceFolders ||
75 | !workspace.workspaceFolders[0] ||
76 | workspace.workspaceFolders.length === 0
77 | ? config.keyFilename
78 | : join(workspace.workspaceFolders[0].uri.fsPath, config.keyFilename),
79 | statusBarItem: {
80 | align:
81 | config.statusBarItem.align === null ||
82 | config.statusBarItem.align === undefined
83 | ? undefined
84 | : config.statusBarItem.align,
85 | priority:
86 | config.statusBarItem.priority === null ||
87 | config.statusBarItem.priority === undefined
88 | ? undefined
89 | : config.statusBarItem.priority,
90 | },
91 | viewer: {
92 | ...config.viewer,
93 | rowsPerPage:
94 | config.viewer.rowsPerPage === undefined ||
95 | config.viewer.rowsPerPage === null
96 | ? undefined
97 | : config.viewer.rowsPerPage,
98 | },
99 | };
100 | }
101 |
102 | function setContext(config: Config): void {
103 | const map = flatten(config, "bigqueryRunner");
104 | Object.keys(map).forEach((k) =>
105 | commands.executeCommand("setContext", k, map[k])
106 | );
107 | }
108 |
109 | function flatten(
110 | source: { [key: string]: unknown },
111 | parentKey: string,
112 | target: { [key: string]: unknown } = {}
113 | ): { [key: string]: unknown } {
114 | return Object.keys(source).reduce((t, k) => {
115 | const v = source[k];
116 | const type = Object.prototype.toString.call(v);
117 | if (type === "[object Object]" && !Array.isArray(v)) {
118 | t = flatten(v as { [key: string]: unknown }, parentKey + "." + k, t);
119 | } else if (type !== "[object Function]") {
120 | t[parentKey + "." + k] = v;
121 | }
122 | return t;
123 | }, target);
124 | }
125 |
--------------------------------------------------------------------------------
/packages/extension/src/dryRunner.ts:
--------------------------------------------------------------------------------
1 | import { format as formatBytes, parse } from "bytes";
2 | import { createClient } from "core";
3 | import type { RunnerID } from "shared";
4 | import { unwrap } from "shared";
5 | import type { TextDocument, TextEditor } from "vscode";
6 | import { window } from "vscode";
7 | import type { ConfigManager } from "./configManager";
8 | import type { ErrorManager } from "./errorManager";
9 | import type { ErrorMarkerManager } from "./errorMarker";
10 | import { getQueryText } from "./getQueryText";
11 | import { isBigQuery } from "./isBigQuery";
12 | import type { Logger } from "./logger";
13 | import type { QuickFixManager } from "./quickfix";
14 | import type { StatusManager } from "./statusManager";
15 |
16 | export type DryRunner = ReturnType;
17 |
18 | export function createDryRunner({
19 | logger,
20 | configManager,
21 | statusManager,
22 | errorManager,
23 | errorMarkerManager,
24 | quickFixManager,
25 | }: Readonly<{
26 | logger: Logger;
27 | configManager: ConfigManager;
28 | statusManager: StatusManager;
29 | errorManager: ErrorManager;
30 | errorMarkerManager: ErrorMarkerManager;
31 | quickFixManager: QuickFixManager;
32 | }>) {
33 | const pathTimeoutId = new Map();
34 |
35 | return {
36 | async validateWithDocument(document: TextDocument): Promise {
37 | await Promise.all(
38 | window.visibleTextEditors
39 | .filter((editor) => editor.document === document)
40 | .map((editor) => this.validate(editor))
41 | );
42 | },
43 |
44 | async validate(editor: TextEditor): Promise {
45 | const { document } = editor;
46 | const { fileName } = document;
47 |
48 | const config = configManager.get();
49 | if (!isBigQuery({ config, document }) || !config.validation.enabled) {
50 | return;
51 | }
52 |
53 | const runnerId: RunnerID = `file://${fileName}`;
54 | const timeoutId = pathTimeoutId.get(runnerId);
55 | if (timeoutId) {
56 | clearTimeout(timeoutId);
57 | pathTimeoutId.delete(runnerId);
58 | }
59 | pathTimeoutId.set(
60 | runnerId,
61 | setTimeout(async () => {
62 | logger.log(`validate`);
63 | await this.run(editor);
64 | }, config.validation.debounceInterval)
65 | );
66 | },
67 |
68 | async run(editor: TextEditor): Promise {
69 | const {
70 | document: { fileName },
71 | } = editor;
72 |
73 | const runnerId: RunnerID = `file://${fileName}`;
74 | const status = statusManager.get(runnerId);
75 | const errorMarker = errorMarkerManager.get({
76 | runnerId,
77 | editor,
78 | });
79 | const quickFix = quickFixManager.get(editor.document);
80 |
81 | const queryTextResult = await getQueryText(editor);
82 | if (!queryTextResult.success) {
83 | errorMarker.clear();
84 | quickFix.clear();
85 | return;
86 | }
87 | const query = queryTextResult.value;
88 |
89 | logger.log(`run`);
90 | status.loadProcessed();
91 |
92 | const config = configManager.get();
93 | logger.log("dry-run with:", JSON.stringify(config, null, " "));
94 |
95 | const clientResult = await createClient({
96 | keyFilename: config.keyFilename,
97 | projectId: config.projectId,
98 | location: config.location,
99 | });
100 | if (!clientResult.success) {
101 | logger.log("failed to create client");
102 | logger.error(clientResult);
103 | status.errorProcessed();
104 | const err = unwrap(clientResult);
105 | errorManager.show(err);
106 | return;
107 | }
108 | const client = unwrap(clientResult);
109 |
110 | const dryRunJobResult = await client.createDryRunJob({
111 | query,
112 | useLegacySql: config.useLegacySql,
113 | maximumBytesBilled: config.maximumBytesBilled
114 | ? parse(config.maximumBytesBilled).toString()
115 | : undefined,
116 | defaultDataset: config.defaultDataset,
117 | maxResults: config.viewer.rowsPerPage,
118 | });
119 |
120 | errorMarker.clear();
121 | quickFix.clear();
122 | if (!dryRunJobResult.success) {
123 | logger.log("failed to dry-run");
124 | logger.error(dryRunJobResult);
125 | const err = unwrap(dryRunJobResult);
126 | status.errorProcessed();
127 |
128 | if (err.type === "QueryWithPosition") {
129 | if (err.suggestion) {
130 | quickFix.register({
131 | start: err.position,
132 | ...err.suggestion,
133 | });
134 | }
135 | errorMarker.markAt({
136 | reason: err.reason,
137 | position: err.position,
138 | });
139 | return;
140 | }
141 | if (err.type === "Query") {
142 | errorMarker.markAll({
143 | reason: err.reason,
144 | });
145 | return;
146 | }
147 |
148 | errorManager.show(err);
149 | return;
150 | }
151 | errorMarker.clear();
152 | quickFix.clear();
153 |
154 | const job = unwrap(dryRunJobResult);
155 |
156 | logger.log(`job ID: ${job.id}`);
157 | const bytes = formatBytes(job.totalBytesProcessed);
158 | logger.log(`result: ${bytes} estimated to be read`);
159 |
160 | status.succeedProcessed({
161 | bytes,
162 | });
163 | },
164 |
165 | dispose() {
166 | pathTimeoutId.forEach((timeoutId) => {
167 | clearTimeout(timeoutId);
168 | });
169 | pathTimeoutId.clear();
170 | },
171 | };
172 | }
173 |
--------------------------------------------------------------------------------
/packages/extension/src/errorManager.ts:
--------------------------------------------------------------------------------
1 | import type { AuthenticationError, Gcloud, NoProjectIDError } from "core";
2 | import type { Err } from "shared";
3 | import { errorToString } from "shared";
4 | import { commands, env, Uri } from "vscode";
5 | import type { Logger } from "./logger";
6 | import { showError, showInformation } from "./window";
7 |
8 | export type ErrorManager = ReturnType;
9 |
10 | export const createErrorManager = ({
11 | logger,
12 | gcloud,
13 | }: {
14 | logger: Logger;
15 | gcloud: Gcloud;
16 | }) => {
17 | const l = logger.createChild("ErrorManager");
18 | const isAuthenticationError = (
19 | err: Err
20 | ): err is AuthenticationError => err.type === "Authentication";
21 | const isNoProjectIDError = (err: Err): err is NoProjectIDError =>
22 | err.type === "NoProjectID";
23 |
24 | const userSettings = async () => {
25 | l.log("open user settings");
26 | await commands.executeCommand("workbench.action.openSettingsJson");
27 | };
28 | const workspaceSettings = async () => {
29 | l.log("open workspace settings");
30 | await commands.executeCommand("workbench.action.openWorkspaceSettingsFile");
31 | };
32 | const login = async () => {
33 | l.log("login");
34 | const res = await gcloud.login();
35 | if (!res.success) {
36 | showError(`Login failure: ${errorToString(res.value)}`);
37 | return;
38 | }
39 | showInformation(`Login success`);
40 | };
41 | const moreInformation = async () => {
42 | l.log("open more information");
43 | await env.openExternal(
44 | Uri.parse(
45 | "https://github.com/minodisk/bigquery-runner/blob/main/README.md#authentication"
46 | )
47 | );
48 | };
49 |
50 | const showAuthenticationError = (err: AuthenticationError) => {
51 | showError(err.reason, {
52 | ...(err.hasKeyFilename
53 | ? {
54 | "User settings": userSettings,
55 | "Workspace settings": workspaceSettings,
56 | }
57 | : {
58 | Login: login,
59 | }),
60 | "More information": moreInformation,
61 | });
62 | };
63 |
64 | const showNoProjectIDError = (err: NoProjectIDError) => {
65 | showError(err.reason, {
66 | "User settings": userSettings,
67 | "Workspace settings": workspaceSettings,
68 | "More information": moreInformation,
69 | });
70 | };
71 |
72 | return {
73 | show(err: Err) {
74 | if (isAuthenticationError(err)) {
75 | showAuthenticationError(err);
76 | return;
77 | }
78 | if (isNoProjectIDError(err)) {
79 | showNoProjectIDError(err);
80 | return;
81 | }
82 | showError(err);
83 | },
84 |
85 | dispose() {
86 | // do nothing
87 | },
88 | };
89 | };
90 |
--------------------------------------------------------------------------------
/packages/extension/src/errorMarker.ts:
--------------------------------------------------------------------------------
1 | import type { RunnerID } from "shared";
2 | import type { TextEditor } from "vscode";
3 | import { Diagnostic, languages, Position, Range, workspace } from "vscode";
4 |
5 | export type ErrorMarkerManager = ReturnType;
6 | export type ErrorMarker = Readonly<{
7 | markAt(
8 | props: Readonly<{
9 | reason: string;
10 | position: { line: number; character: number };
11 | }>
12 | ): void;
13 | markAll(
14 | props: Readonly<{
15 | reason: string;
16 | }>
17 | ): void;
18 | clear(): void;
19 | }>;
20 |
21 | export function createErrorMarkerManager(section: string) {
22 | const diagnosticCollection = languages.createDiagnosticCollection(section);
23 | const errorMarkers = new Map();
24 |
25 | return {
26 | get({
27 | runnerId,
28 | editor,
29 | }: {
30 | runnerId: RunnerID;
31 | editor: TextEditor;
32 | }): ErrorMarker {
33 | const m = errorMarkers.get(runnerId);
34 | if (m) {
35 | return m;
36 | }
37 |
38 | const marker: ErrorMarker = {
39 | markAt({ reason, position: { line, character } }) {
40 | const {
41 | document: { fileName },
42 | selections,
43 | } = editor;
44 |
45 | const document = workspace.textDocuments.find(
46 | (document) => document.fileName === fileName
47 | );
48 | if (!document) {
49 | return;
50 | }
51 |
52 | if (selections.some((s) => !s.isEmpty)) {
53 | diagnosticCollection.set(
54 | document.uri,
55 | selections.map(
56 | (selection) =>
57 | new Diagnostic(
58 | new Range(selection.start, selection.end),
59 | reason
60 | )
61 | )
62 | );
63 | return;
64 | }
65 |
66 | const wordRange = document.getWordRangeAtPosition(
67 | new Position(line, character)
68 | );
69 | diagnosticCollection.set(document.uri, [
70 | new Diagnostic(
71 | wordRange ??
72 | new Range(
73 | new Position(line, character),
74 | new Position(line, character + 1)
75 | ),
76 | reason
77 | ),
78 | ]);
79 | },
80 |
81 | markAll({ reason }) {
82 | const {
83 | document: { fileName },
84 | selections,
85 | } = editor;
86 |
87 | const document = workspace.textDocuments.find(
88 | (document) => document.fileName === fileName
89 | );
90 | if (!document) {
91 | return;
92 | }
93 |
94 | if (selections.some((s) => !s.isEmpty)) {
95 | diagnosticCollection.set(
96 | document.uri,
97 | selections.map(
98 | (selection) =>
99 | new Diagnostic(
100 | new Range(selection.start, selection.end),
101 | reason
102 | )
103 | )
104 | );
105 | return;
106 | }
107 |
108 | diagnosticCollection.set(document.uri, [
109 | new Diagnostic(
110 | new Range(
111 | document.lineAt(0).range.start,
112 | document.lineAt(document.lineCount - 1).range.end
113 | ),
114 | reason
115 | ),
116 | ]);
117 | return;
118 | },
119 |
120 | clear() {
121 | diagnosticCollection.delete(editor.document.uri);
122 | },
123 | };
124 | errorMarkers.set(runnerId, marker);
125 |
126 | return marker;
127 | },
128 |
129 | dispose() {
130 | errorMarkers.clear();
131 | diagnosticCollection.dispose();
132 | },
133 | };
134 | }
135 |
--------------------------------------------------------------------------------
/packages/extension/src/getQueryText.ts:
--------------------------------------------------------------------------------
1 | import type { Result, Err } from "shared";
2 | import { succeed, fail } from "shared";
3 | import type { TextEditor } from "vscode";
4 |
5 | export async function getQueryText(
6 | editor: TextEditor
7 | ): Promise, string>> {
8 | const text = (() => {
9 | const selections = editor.selections.filter(
10 | (selection) => !selection.isEmpty
11 | );
12 | if (selections.length === 0) {
13 | return editor.document.getText();
14 | }
15 | return selections
16 | .map((selection) => editor.document.getText(selection))
17 | .join("\n");
18 | })();
19 |
20 | if (text.trim() === "") {
21 | return fail({
22 | type: "NoText" as const,
23 | reason: `no text in the editor`,
24 | });
25 | }
26 |
27 | return succeed(text);
28 | }
29 |
--------------------------------------------------------------------------------
/packages/extension/src/isBigQuery.ts:
--------------------------------------------------------------------------------
1 | import { extname } from "path";
2 | import type { TextDocument } from "vscode";
3 | import type { Config } from "./configManager";
4 |
5 | export function isBigQuery({
6 | config,
7 | document,
8 | }: {
9 | readonly config: Config;
10 | readonly document: TextDocument;
11 | }): boolean {
12 | return (
13 | config.languageIds.includes(document.languageId) ||
14 | config.extensions.includes(extname(document.fileName))
15 | );
16 | }
17 |
--------------------------------------------------------------------------------
/packages/extension/src/logger.ts:
--------------------------------------------------------------------------------
1 | import { errorToString } from "shared";
2 | import type { OutputChannel } from "vscode";
3 |
4 | export type Logger = ReturnType;
5 |
6 | export const createLogger = (chan: OutputChannel) => {
7 | const createChild = (prefixes: string[]) => {
8 | return {
9 | log(...messages: string[]) {
10 | chan.appendLine([prefix(prefixes), ...messages].join(" "));
11 | },
12 | error(err: unknown) {
13 | chan.appendLine(
14 | [prefix(prefixes), "Error:", errorToString(err)].join(" ")
15 | );
16 | },
17 | createChild(prefix: string) {
18 | return createChild([...prefixes, prefix]);
19 | },
20 | dispose() {
21 | // do nothing
22 | },
23 | };
24 | };
25 |
26 | return createChild([]);
27 | };
28 |
29 | const prefix = (prefixes: Array): string =>
30 | prefixes.map((p) => `[${p}]`).join("");
31 |
--------------------------------------------------------------------------------
/packages/extension/src/paramManager.ts:
--------------------------------------------------------------------------------
1 | import type { Token } from "core";
2 | import ordinal from "ordinal";
3 | import type { Err, Result, RunnerID } from "shared";
4 | import { errorToString, succeed, tryCatchSync } from "shared";
5 | import type { ExtensionContext } from "vscode";
6 | import { window } from "vscode";
7 | import type { Logger } from "./logger";
8 |
9 | export type ParamManager = ReturnType;
10 | export type Manager = {
11 | get(
12 | tokens: Array
13 | ): Promise<
14 | Result<
15 | Err<"NoParameter" | "InvalidJSON">,
16 | { [key: string]: unknown } | Array | undefined
17 | >
18 | >;
19 | clearParams(): Promise;
20 | };
21 |
22 | export const createParamManager = ({
23 | state,
24 | logger,
25 | }: {
26 | state: ExtensionContext["globalState"];
27 | logger: Logger;
28 | }) => {
29 | const managers = new Map();
30 | const l = logger.createChild("ParamManager");
31 | return {
32 | get({ runnerId }: { runnerId: RunnerID }) {
33 | return managers.get(runnerId);
34 | },
35 |
36 | create({ runnerId }: { runnerId: RunnerID }) {
37 | const manager = managers.get(runnerId);
38 | if (manager) {
39 | return manager;
40 | }
41 | const m: Manager = {
42 | async get(tokens) {
43 | const cache = state.get<{ [key: string]: unknown } | Array>(
44 | runnerId
45 | );
46 | l.log("cache:", JSON.stringify(cache));
47 |
48 | {
49 | const paramTokens = tokens.filter(
50 | (token) => token.type === "NAMED_PARAMETER"
51 | );
52 | if (paramTokens.length > 0) {
53 | const map = paramTokens.reduce((m, token) => {
54 | const key = token.key ?? token.raw.replace(/^@/, "");
55 | const tokens = m.get(key);
56 | m.set(key, tokens ? [...tokens, token] : [token]);
57 | return m;
58 | }, new Map>());
59 | {
60 | if (
61 | cache &&
62 | !Array.isArray(cache) &&
63 | equals(Object.keys(cache), Array.from(map.keys()))
64 | ) {
65 | l.log("use cache:", JSON.stringify(cache));
66 | return succeed(cache);
67 | }
68 | }
69 | const values: { [key: string]: unknown } = {};
70 | for (const [key] of map) {
71 | const value = await window.showInputBox({
72 | title: `Set a parameter to "${key}"`,
73 | prompt: `Specify in JSON format`,
74 | });
75 | if (value === undefined) {
76 | return fail({
77 | type: "NoParameter" as const,
78 | reason: `Parameter "${key}" is not specified`,
79 | });
80 | }
81 | const parseJSONResult = parseJSON(value);
82 | if (!parseJSONResult.success) {
83 | return parseJSONResult;
84 | }
85 | values[key] = parseJSONResult.value;
86 | }
87 | await state.update(runnerId, values);
88 | return succeed(values);
89 | }
90 | }
91 |
92 | {
93 | const paramTokens = tokens.filter(
94 | (token) => token.type === "POSITIONAL_PARAMETER"
95 | );
96 | if (paramTokens.length > 0) {
97 | {
98 | if (
99 | cache &&
100 | Array.isArray(cache) &&
101 | cache.values.length === paramTokens.length
102 | ) {
103 | l.log("use cache:", JSON.stringify(cache));
104 | return succeed(cache);
105 | }
106 | }
107 |
108 | const values: Array = [];
109 | for (let i = 0; i < paramTokens.length; i++) {
110 | const key = ordinal(i + 1);
111 | const value = await window.showInputBox({
112 | title: `Set a parameter for the ${key} param`,
113 | prompt: `Specify in JSON format`,
114 | });
115 | if (value === undefined) {
116 | return fail({
117 | type: "NoParameter" as const,
118 | reason: `${key} parameter is not specified`,
119 | });
120 | }
121 | const parseJSONResult = parseJSON(value);
122 | if (!parseJSONResult.success) {
123 | return parseJSONResult;
124 | }
125 | values[i] = parseJSONResult.value;
126 | }
127 | await state.update(runnerId, values);
128 | return succeed(values);
129 | }
130 | }
131 |
132 | return succeed(undefined);
133 | },
134 |
135 | async clearParams() {
136 | await Promise.all([
137 | state.update(`${runnerId}`, undefined),
138 | state.update(`${runnerId}-named`, undefined),
139 | state.update(`${runnerId}-positional`, undefined),
140 | ]);
141 | },
142 | };
143 | managers.set(runnerId, m);
144 | return m;
145 | },
146 |
147 | async clearAllParams() {
148 | await Promise.all(
149 | state.keys().map((key) => state.update(key, undefined))
150 | );
151 | },
152 |
153 | dispose() {
154 | managers.clear();
155 | },
156 | };
157 | };
158 |
159 | const parseJSON = (value: string): Result, unknown> => {
160 | return tryCatchSync(
161 | () => JSON.parse(value),
162 | (err) => ({
163 | type: "InvalidJSON",
164 | reason: errorToString(err),
165 | })
166 | );
167 | };
168 | const equals = (a: Array, b: Array): boolean =>
169 | a.length === b.length && a.every((v, i) => v === b[i]);
170 |
--------------------------------------------------------------------------------
/packages/extension/src/previewer.ts:
--------------------------------------------------------------------------------
1 | import type { TableReference } from "shared";
2 | import { getTableName } from "shared";
3 | import type { ConfigManager } from "./configManager";
4 | import type { Logger } from "./logger";
5 | import type { RunnerManager } from "./runner";
6 |
7 | export type Previewer = ReturnType;
8 |
9 | export const createPreviewer = ({
10 | logger,
11 | configManager,
12 | runnerManager,
13 | }: {
14 | logger: Logger;
15 | configManager: ConfigManager;
16 | runnerManager: RunnerManager;
17 | }) => {
18 | return {
19 | async preview(tableReference: TableReference) {
20 | const id = getTableName(tableReference);
21 | const {
22 | previewer: { rowsPerPage },
23 | } = configManager.get();
24 | const query =
25 | `SELECT * FROM \`${id}\`` +
26 | (rowsPerPage ? ` LIMIT ${rowsPerPage}` : "");
27 | logger.log("query:", query);
28 |
29 | const runnerResult = await runnerManager.preview({
30 | title: tableReference.tableId,
31 | query,
32 | tableReference,
33 | });
34 | if (!runnerResult.success) {
35 | logger.error(runnerResult);
36 | return;
37 | }
38 | const runner = runnerResult.value;
39 |
40 | await runner.run();
41 | },
42 |
43 | dispose() {
44 | // do nothing
45 | },
46 | };
47 | };
48 |
--------------------------------------------------------------------------------
/packages/extension/src/quickfix.ts:
--------------------------------------------------------------------------------
1 | import type { Disposable, TextDocument } from "vscode";
2 | import {
3 | CodeAction,
4 | CodeActionKind,
5 | languages,
6 | Position,
7 | Range,
8 | WorkspaceEdit,
9 | } from "vscode";
10 | import type { Config, ConfigManager } from "./configManager";
11 |
12 | export type QuickFixManager = ReturnType;
13 |
14 | export type QuickFix = {
15 | register(params: {
16 | start: { line: number; character: number };
17 | before: string;
18 | after: string;
19 | }): void;
20 | clear(): void;
21 | };
22 |
23 | export const createQuickFixManager = ({
24 | configManager,
25 | }: {
26 | configManager: ConfigManager;
27 | }): Disposable & {
28 | get(document: TextDocument): QuickFix;
29 | } => {
30 | const codeActionsWithRange = new Map<
31 | TextDocument,
32 | Array<{ action: CodeAction; range: Range }>
33 | >();
34 |
35 | let unregister: Disposable = {
36 | dispose() {
37 | // do nothing
38 | },
39 | };
40 | let languageIds: Array = [];
41 | let extensions: Array = [];
42 | const register = (config: Config) => {
43 | // ignore when languageIds is not changed
44 | if (
45 | JSON.stringify(languageIds) === JSON.stringify(config.languageIds) &&
46 | JSON.stringify(extensions) === JSON.stringify(config.extensions)
47 | ) {
48 | return;
49 | }
50 | languageIds = config.languageIds;
51 | extensions = config.extensions;
52 |
53 | unregister.dispose();
54 | unregister = languages.registerCodeActionsProvider(
55 | [
56 | ...config.languageIds.map((language) => ({ language })),
57 | ...config.extensions.map((ext) => ({ pattern: `**/*${ext}` })),
58 | ],
59 | {
60 | provideCodeActions(document, range) {
61 | const actionsWithRange = codeActionsWithRange.get(document);
62 | if (!actionsWithRange) {
63 | return;
64 | }
65 | const actions = actionsWithRange
66 | .filter((actionWithRange) =>
67 | actionWithRange.range.intersection(range)
68 | )
69 | .map(({ action }) => action);
70 | if (!actions.length) {
71 | return;
72 | }
73 | return actions;
74 | },
75 | },
76 | {
77 | providedCodeActionKinds: [CodeActionKind.QuickFix],
78 | }
79 | );
80 | };
81 | register(configManager.get());
82 | const subscriptions = [configManager.onChange(register)];
83 |
84 | return {
85 | get(document) {
86 | return {
87 | register({ start, before, after }) {
88 | const action = new CodeAction(
89 | `Change name to '${after}'`,
90 | CodeActionKind.QuickFix
91 | );
92 | const edit = new WorkspaceEdit();
93 | const range = new Range(
94 | new Position(start.line, start.character),
95 | new Position(start.line, start.character + before.length)
96 | );
97 | edit.replace(document.uri, range, after);
98 | action.edit = edit;
99 | codeActionsWithRange.set(document, [
100 | {
101 | range,
102 | action,
103 | },
104 | ]);
105 | },
106 |
107 | clear() {
108 | codeActionsWithRange.delete(document);
109 | },
110 | };
111 | },
112 |
113 | dispose() {
114 | subscriptions.forEach((s) => s.dispose());
115 | unregister.dispose();
116 | codeActionsWithRange.clear();
117 | },
118 | };
119 | };
120 |
--------------------------------------------------------------------------------
/packages/extension/src/statusManager.ts:
--------------------------------------------------------------------------------
1 | import type { RunnerID } from "shared";
2 | import type { window } from "vscode";
3 | import { StatusBarAlignment } from "vscode";
4 | import type { Config, ConfigManager } from "./configManager";
5 |
6 | export type StatusManager = ReturnType;
7 | export type Status = Readonly<{
8 | loadProcessed(): void;
9 | errorProcessed(): void;
10 | succeedProcessed(usage: ProcessedUsage): void;
11 | loadBilled(): void;
12 | errorBilled(): void;
13 | succeedBilled(usage: BilledUsage): void;
14 | show(): void;
15 | }>;
16 |
17 | type State = "ready" | "loading" | "error" | "success";
18 |
19 | type Processed = { state: State; usage?: ProcessedUsage };
20 |
21 | type ProcessedUsage = {
22 | bytes: string;
23 | };
24 |
25 | type Billed = { state: State; usage?: BilledUsage };
26 |
27 | type BilledUsage = {
28 | bytes: string;
29 | cacheHit: boolean;
30 | };
31 |
32 | export function createStatusManager({
33 | configManager,
34 | createStatusBarItem,
35 | }: {
36 | configManager: ConfigManager;
37 | createStatusBarItem: ReturnType;
38 | }) {
39 | const statuses = new Map();
40 |
41 | let statusBarItem = createStatusBarItem(configManager.get().statusBarItem);
42 | const subscriptions = [
43 | configManager.onChange((config) => {
44 | statusBarItem.dispose();
45 | statusBarItem = createStatusBarItem(config.statusBarItem);
46 | }),
47 | ];
48 |
49 | const statusManager = {
50 | get(runnerId: RunnerID): Status {
51 | const s = statuses.get(runnerId);
52 | if (s) {
53 | return s;
54 | }
55 |
56 | const status = create();
57 | statuses.set(runnerId, status);
58 | return status;
59 | },
60 |
61 | hide() {
62 | statusBarItem.hide();
63 | statusBarItem.text = "";
64 | statusBarItem.tooltip = undefined;
65 | },
66 |
67 | dispose() {
68 | subscriptions.forEach((s) => s.dispose());
69 | statusBarItem.dispose();
70 | statuses.clear();
71 | },
72 | };
73 |
74 | const create = () => {
75 | const processed: Processed = {
76 | state: "ready",
77 | usage: {
78 | bytes: "",
79 | },
80 | };
81 | const billed: Billed = {
82 | state: "ready",
83 | usage: {
84 | bytes: "",
85 | cacheHit: false,
86 | },
87 | };
88 |
89 | const apply = () => {
90 | statusBarItem.text = [
91 | getProcessedIcon(processed?.state),
92 | processed?.usage?.bytes,
93 | getBilledIcon(billed?.state),
94 | billed?.usage?.bytes,
95 | ].join(" ");
96 | statusBarItem.tooltip = [
97 | processed?.usage
98 | ? `This query will process ${processed.usage.bytes} when run.`
99 | : undefined,
100 | billed?.usage
101 | ? `In the last query that ran,
102 | the cache ${billed.usage.cacheHit ? "was" : "wasn't"} applied and ${
103 | billed.usage.bytes
104 | } was the target of the bill.`
105 | : undefined,
106 | ].join("\n");
107 | statusBarItem.show();
108 | };
109 |
110 | return {
111 | loadProcessed() {
112 | processed.state = "loading";
113 | apply();
114 | },
115 | errorProcessed() {
116 | processed.state = "error";
117 | apply();
118 | },
119 | succeedProcessed(usage: ProcessedUsage) {
120 | processed.state = "success";
121 | processed.usage = usage;
122 | apply();
123 | },
124 | loadBilled() {
125 | billed.state = "loading";
126 | apply();
127 | },
128 | errorBilled() {
129 | billed.state = "error";
130 | apply();
131 | },
132 | succeedBilled(usage: BilledUsage) {
133 | billed.state = "success";
134 | billed.usage = usage;
135 | apply();
136 | },
137 | show() {
138 | apply();
139 | },
140 | };
141 | };
142 |
143 | return statusManager;
144 | }
145 |
146 | export function createStatusBarItemCreator(w: typeof window) {
147 | return (options: Config["statusBarItem"]) => {
148 | return w.createStatusBarItem(
149 | options.align === "left"
150 | ? StatusBarAlignment.Left
151 | : options.align === "right"
152 | ? StatusBarAlignment.Right
153 | : undefined,
154 | options.priority
155 | );
156 | };
157 | }
158 |
159 | function getProcessedIcon(state?: State) {
160 | switch (state) {
161 | case "loading":
162 | return "$(loading~spin)";
163 | case "error":
164 | return "$(error)";
165 | case "success":
166 | return "$(database)";
167 | default:
168 | return "";
169 | }
170 | }
171 |
172 | function getBilledIcon(state?: State) {
173 | switch (state) {
174 | case "loading":
175 | return "$(loading~spin)";
176 | case "error":
177 | return "$(error)";
178 | case "success":
179 | return "$(credit-card)";
180 | default:
181 | return "";
182 | }
183 | }
184 |
--------------------------------------------------------------------------------
/packages/extension/src/window.ts:
--------------------------------------------------------------------------------
1 | import type { Err, Result } from "shared";
2 | import { errorToString, tryCatch } from "shared";
3 | import type { Progress, ProgressOptions } from "vscode";
4 | import { window } from "vscode";
5 |
6 | export const showError = (
7 | err: unknown,
8 | callbacks: { [label: string]: () => unknown } = {}
9 | ): void => {
10 | const cbs = new Map(Object.entries(callbacks));
11 | // eslint-disable-next-line @typescript-eslint/no-floating-promises
12 | (async () => {
13 | const key = await window.showErrorMessage(
14 | errorToString(err),
15 | ...cbs.keys()
16 | );
17 | if (!key) {
18 | return;
19 | }
20 | const callback = cbs.get(key);
21 | if (!callback) {
22 | return;
23 | }
24 | callback();
25 | })();
26 | };
27 |
28 | export const showInformation = (
29 | message: string,
30 | callbacks: { [label: string]: () => unknown } = {}
31 | ): void => {
32 | const cbs = new Map(Object.entries(callbacks));
33 | // eslint-disable-next-line @typescript-eslint/no-floating-promises
34 | (async () => {
35 | const key = await window.showInformationMessage(message, ...cbs.keys());
36 | if (!key) {
37 | return;
38 | }
39 | const callback = cbs.get(key);
40 | if (!callback) {
41 | return;
42 | }
43 | callback();
44 | })();
45 | };
46 |
47 | export const openProgress = (options: ProgressOptions) => {
48 | let close!: () => Promise, void>>;
49 | let progress!: Progress<{ message?: string; increment?: number }>;
50 | const taskPromise = new Promise((resolve) => {
51 | close = async () => {
52 | resolve(null);
53 | return uiPromise;
54 | };
55 | });
56 | const uiPromise = tryCatch(
57 | async () =>
58 | window.withProgress(options, async (prog) => {
59 | progress = prog;
60 | await taskPromise;
61 | }),
62 | (err) => ({
63 | type: "Unknown" as const,
64 | reason: errorToString(err),
65 | })
66 | );
67 | return {
68 | report: progress.report.bind(progress),
69 | close,
70 | };
71 | };
72 |
--------------------------------------------------------------------------------
/packages/extension/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "../../tsconfig.json",
3 | "compilerOptions": {
4 | "rootDir": "src"
5 | },
6 | "include": ["src"]
7 | }
8 |
--------------------------------------------------------------------------------
/packages/misc/assets-src/icon-copy.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/packages/misc/assets-src/icon-end.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/packages/misc/assets-src/icon-next.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/packages/misc/assets-src/icon-panel.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
6 |
--------------------------------------------------------------------------------
/packages/misc/assets-src/icon-prev.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/packages/misc/assets-src/icon-start.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/packages/misc/assets-src/icon.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
13 |
--------------------------------------------------------------------------------
/packages/misc/assets-src/loading.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/packages/misc/assets/icon-activity-bar.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
17 |
--------------------------------------------------------------------------------
/packages/misc/assets/icon-panel.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/minodisk/bigquery-runner/cf3781d7bb6538342dc98e5c12af6532d7af2617/packages/misc/assets/icon-panel.png
--------------------------------------------------------------------------------
/packages/misc/assets/icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/minodisk/bigquery-runner/cf3781d7bb6538342dc98e5c12af6532d7af2617/packages/misc/assets/icon.png
--------------------------------------------------------------------------------
/packages/misc/docs/flow.md:
--------------------------------------------------------------------------------
1 | ## Resource Lifecycle
2 |
3 | ```mermaid
4 | flowchart LR
5 | subgraph usage
6 | query[/Query/]
7 | resource[Resource]
8 | ui(UI)
9 | end
10 | ```
11 |
12 | ```mermaid
13 | flowchart LR
14 | procedure[/PROCEDURE/]
15 | routine[Routine]
16 | run(Run)
17 | preview(Preview)
18 | select[/SELECT/]
19 | merge[/MERGE/]
20 | table[Table]
21 | results[Results]
22 | download(Download)
23 | format(Select\nformat)
24 | path(Select\nfile path)
25 | file[File]
26 |
27 | procedure --> routine --> run
28 | run --> select
29 | run --> merge
30 |
31 | merge --> table --> preview --> select
32 | select --> results
33 | results --> download --> format --> path --> file
34 | ```
35 |
36 | ## Processing
37 |
38 | ```mermaid
39 | flowchart LR
40 |
41 | subgraph Runner
42 | direction LR
43 | Query(Query) -- dry run --> Query -- run --> data(Rows)
44 | end
45 |
46 | subgraph Formatter
47 | direction LR
48 | Table(Table)
49 | CSV(CSV)
50 | JSON(JSON)
51 | JSONL(JSON Lines)
52 | end
53 |
54 | subgraph Renderer
55 | direction LR
56 | Viewer
57 | File
58 | Clipboard(Clipboard\nunimplemented)
59 | Log(Log\ndeprecated)
60 | end
61 |
62 | Runner --> Formatter --> Renderer
63 | ```
64 |
65 | DEPRECATED
66 |
67 | ```json
68 | "bigqueryRunner.format.type": {
69 | "type": "string",
70 | "default": "table",
71 | "enum": [
72 | "table",
73 | "markdown",
74 | "json",
75 | "json-lines",
76 | "csv"
77 | ],
78 | "description": "Formatting method."
79 | },
80 | "bigqueryRunner.output.type": {
81 | "type": "string",
82 | "default": "viewer",
83 | "enum": [
84 | "viewer",
85 | "log",
86 | "file"
87 | ],
88 | "description": "The output destination for the query results. When set to `viewer`, this extension opens the webview pane and renders the results with tags. When set to `log`, this extension opens the output panel and outputs the results in the format set in `bigqueryRunner.format.type`. When set to `file`, this extension outputs the results as a file in the directory set in `bigqueryRunner.output.file.path`, in the format set in `bigqueryRunner.format.type`."
89 | },
90 | "bigqueryRunner.output.file.path": {
91 | "type": "string",
92 | "default": ".",
93 | "description": "The output directory of the file when `bigqueryRunner.output.type` is specified as `file`."
94 | },
95 | ```
96 |
97 | ```typescript
98 | switch (config.output.type) {
99 | case "log":
100 | return createLogOutput({
101 | formatter: createFormatter({ config }),
102 | outputChannel,
103 | });
104 | case "file": {
105 | if (!workspace.workspaceFolders || !workspace.workspaceFolders[0]) {
106 | throw new Error(`no workspace folders`);
107 | }
108 |
109 | const formatter = createFormatter({ config });
110 | const dirname = join(
111 | workspace.workspaceFolders[0].uri.path ||
112 | workspace.workspaceFolders[0].uri.fsPath,
113 | config.output.file.path
114 | );
115 | const path = join(
116 | dirname,
117 | `${basename(fileName, extname(fileName))}${formatToExtension(
118 | formatter.type
119 | )}`
120 | );
121 | await mkdirp(dirname);
122 | const stream = createWriteStream(path, "utf-8");
123 |
124 | return createFileOutput({
125 | formatter,
126 | stream,
127 | });
128 | }
129 | }
130 | ```
131 |
--------------------------------------------------------------------------------
/packages/misc/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "private": true,
3 | "name": "misc",
4 | "version": "0.0.0",
5 | "scripts": {
6 | "build": "mkdirp ../../out && cpx \"./assets/*\" ../../out/assets"
7 | }
8 | }
9 |
--------------------------------------------------------------------------------
/packages/misc/queries/array-join.bqsql:
--------------------------------------------------------------------------------
1 | with
2 | orders as (
3 | select
4 | 1 as order_id,
5 | [
6 | struct (1001 as product_id, 4 as quantity),
7 | struct (1003 as product_id, 1 as quantity)
8 | ] as items
9 | union all
10 | select
11 | 2 as order_id,
12 | [
13 | struct (1003 as product_id, 2 as quantity),
14 | struct (1003 as product_id, 4 as quantity)
15 | ] as items
16 | ),
17 | products as (
18 | select
19 | 1001 as product_id,
20 | 'wallet' as name,
21 | 30000 as price
22 | union all
23 | select
24 | 1002 as product_id,
25 | 'watch' as name,
26 | 10000 as price
27 | union all
28 | select
29 | 1003 as product_id,
30 | 'bag' as name,
31 | 50000 as price
32 | )
33 | select
34 | order_id,
35 | array (
36 | select as struct
37 | *
38 | from
39 | unnest (items)
40 | left join products using (product_id)
41 | ) as items
42 | from
43 | orders
44 |
--------------------------------------------------------------------------------
/packages/misc/queries/create-or-replace-a.bqsql:
--------------------------------------------------------------------------------
1 | create or replace table
2 | `minodisk-api.testing.a` as
3 | select
4 | 1 as id,
5 | 'foo' as column_a
6 | union all
7 | select
8 | 2 as id,
9 | 'bar' as column_a
10 | union all
11 | select
12 | 3 as id,
13 | 'baz' as column_a
14 |
--------------------------------------------------------------------------------
/packages/misc/queries/create-or-replace-b.bqsql:
--------------------------------------------------------------------------------
1 | create or replace table
2 | `vertect.test.b` as
3 | select
4 | 1 as id,
5 | 'hoge' as column_b
6 | union all
7 | select
8 | 2 as id,
9 | 'fuga' as column_b
10 | union all
11 | select
12 | 3 as id,
13 | 'piyo' as column_b
14 |
--------------------------------------------------------------------------------
/packages/misc/queries/create-or-replace.bqsql:
--------------------------------------------------------------------------------
1 | create or replace table
2 | `minodisk-api.testing.create_or_replace` as
3 | select
4 | 1 as a,
5 | 'foo' as b
6 | union all
7 | select
8 | 2 as a,
9 | 'bar' as b
10 | union all
11 | select
12 | 3 as a,
13 | 'baz' as b
14 |
--------------------------------------------------------------------------------
/packages/misc/queries/create-struct-array.bqsql:
--------------------------------------------------------------------------------
1 | create or replace table
2 | testing.struct_array as
3 | select
4 | struct (
5 | 1 as a,
6 | 'foo' as b,
7 | timestamp('2022-10-08T00:00:00Z') as c
8 | ) as simple_struct,
9 | ['foo', 'bar'] as simple_array,
10 | [
11 | struct (
12 | 2 as a,
13 | 'bar' as b,
14 | timestamp('2022-10-09T00:00:00Z') as c
15 | ),
16 | struct (
17 | 3 as a,
18 | 'baz' as b,
19 | timestamp('2022-10-10T00:00:00Z') as c
20 | )
21 | ] as sharrow_array,
22 | [
23 | struct (
24 | 2 as a,
25 | 'bar' as b,
26 | [
27 | struct (
28 | true as d,
29 | 100 as e,
30 | struct ('foo' as g, 'baz' as h) as f
31 | ),
32 | struct (
33 | false as d,
34 | 200 as e,
35 | struct ('bar' as g, 'bar' as h) as f
36 | ),
37 | struct (
38 | true as d,
39 | 300 as e,
40 | struct ('baz' as g, 'foo' as h) as f
41 | )
42 | ] as c
43 | )
44 | ] as deep_array,
45 |
--------------------------------------------------------------------------------
/packages/misc/queries/empty.bqsql:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/minodisk/bigquery-runner/cf3781d7bb6538342dc98e5c12af6532d7af2617/packages/misc/queries/empty.bqsql
--------------------------------------------------------------------------------
/packages/misc/queries/error-quickfix.bqsql:
--------------------------------------------------------------------------------
1 | select
2 | en_labl,
3 | laels
4 | from
5 | `bigquery-public-data.wikipedia.wikidata`
6 | where
7 | array_length(labels) < 5
8 | limit
9 | 100012
10 |
--------------------------------------------------------------------------------
/packages/misc/queries/error-syntax.bqsql:
--------------------------------------------------------------------------------
1 | select
2 | *
3 | from
4 | (
5 | select 1 as id, 'foo' as name
6 | )
7 | left join (
8 | select 1 as id, 'bar' as type
9 | )
10 |
--------------------------------------------------------------------------------
/packages/misc/queries/error-type.bqsql:
--------------------------------------------------------------------------------
1 | select
2 | ip
3 | from
4 | (
5 | select 1 as id
6 | union all select 2 as id
7 | )
8 |
--------------------------------------------------------------------------------
/packages/misc/queries/error.bqsql:
--------------------------------------------------------------------------------
1 | select
2 | ip
3 | from
4 | (
5 | select
6 | 'a' as id
7 | )
8 |
--------------------------------------------------------------------------------
/packages/misc/queries/geom.bqsql:
--------------------------------------------------------------------------------
1 | select
2 | geo_id,
3 | name,
4 | internal_point_latitude,
5 | internal_point_longitude,
6 | from
7 | `bigquery-public-data.geo_us_boundaries.metropolitan_divisions`
8 | -- limit
9 | -- 10
10 |
--------------------------------------------------------------------------------
/packages/misc/queries/in-code.ts:
--------------------------------------------------------------------------------
1 | export function main() {
2 | const invalid = `
3 | select
4 | *
5 | from
6 | (
7 | select 1 as id, 'foo' as name
8 | ) left join (
9 | select 1 as id, 'bar' as type
10 | )
11 | `;
12 | const valid = `select
13 | id
14 | from
15 | (
16 | select 1 as id
17 | union all select 2 as id
18 | )`;
19 | console.log(invalid, valid);
20 | }
21 |
--------------------------------------------------------------------------------
/packages/misc/queries/join-differect-pj-tables.bqsql:
--------------------------------------------------------------------------------
1 | select
2 | *
3 | FROM
4 | `minodisk-api.testing.a`
5 | join `vertect.test.b` using (id)
6 |
--------------------------------------------------------------------------------
/packages/misc/queries/json.bqsql:
--------------------------------------------------------------------------------
1 | create or replace table
2 | `minodisk-api.testing.create_or_replace` as
3 | select
4 | 1 as a,
5 | JSON '{"name": "Alice", "age": 30}' as b;
6 |
--------------------------------------------------------------------------------
/packages/misc/queries/merge.bqsql:
--------------------------------------------------------------------------------
1 | create or replace table
2 | `testing.merge_target` as
3 | select
4 | 1 as id,
5 | 'foo' as val
6 | union all
7 | select
8 | 2 as id,
9 | 'bar' as val;
10 |
11 | create or replace table
12 | `testing.merge_source` as
13 | select
14 | 2 as id,
15 | 'barbar' as val
16 | union all
17 | select
18 | 3 as id,
19 | 'baz' as val;
20 |
21 | merge
22 | testing.merge_target as target using testing.merge_source as source on target.id = source.id
23 | when matched then
24 | update set
25 | id = source.id,
26 | val = source.val
27 | when not matched then
28 | insert
29 | row;
30 |
--------------------------------------------------------------------------------
/packages/misc/queries/nullable-struct.bqsql:
--------------------------------------------------------------------------------
1 | select
2 | struct(
3 | 1 as b,
4 | struct(
5 | 2 as d
6 | ) as c
7 | ) as a
8 | union all
9 | select
10 | null as a
11 | union all
12 | select
13 | struct(
14 | 1 as b,
15 | struct(
16 | null as d
17 | ) as c
18 | ) as a
19 |
--------------------------------------------------------------------------------
/packages/misc/queries/params-arrays.bqsql:
--------------------------------------------------------------------------------
1 | SELECT
2 | name,
3 | sum(number) as count
4 | FROM
5 | `bigquery-public-data.usa_names.usa_1910_2013`
6 | WHERE
7 | gender = @gender -- "M"
8 | AND state IN UNNEST(@states) -- ["WA", "WI", "WV", "WY"]
9 | GROUP BY
10 | name
11 | ORDER BY
12 | count DESC
13 | LIMIT
14 | 10;
15 |
--------------------------------------------------------------------------------
/packages/misc/queries/params-named-duplicated.bqsql:
--------------------------------------------------------------------------------
1 | SELECT
2 | *
3 | FROM
4 | `bigquery-public-data.samples.shakespeare`
5 | WHERE
6 | corpus = @corpus -- "romeoandjuliet"
7 | AND word_count >= @min_word_count -- 250
8 | AND word_count < @min_word_count + 100
9 | ORDER BY
10 | word_count DESC
11 |
--------------------------------------------------------------------------------
/packages/misc/queries/params-named-with-comments.bqsql:
--------------------------------------------------------------------------------
1 | SELECT
2 | corpus,
3 | word,
4 | # @foo,
5 | # @bar # -- @baz */ @qux,
6 | word_count
7 | -- @bar # @baz -- @foo #-- @qux
8 | FROM
9 | `bigquery-public-data.samples.shakespeare`
10 | WHERE
11 | corpus = @corpus/* @foo -- @bar -- "romeoandjuliet"
12 | @baz # /* @qux
13 | @foo
14 | @foo@bar */AND word_count >= @min_word_count -- 250
15 | ORDER BY
16 | word_count DESC
17 |
--------------------------------------------------------------------------------
/packages/misc/queries/params-named-with-strings.bqsql:
--------------------------------------------------------------------------------
1 | SELECT
2 | word,
3 | word_count
4 | FROM
5 | `bigquery-public-data.samples.shakespeare`
6 | WHERE
7 | word != "@a"
8 | AND word != '@b'
9 | AND word != '@c it\'s @c'
10 | AND word != "@d it\"s @d"
11 | AND word != """@e"""
12 | AND word != """
13 | @f
14 | """
15 | AND word != """
16 | \""" @f
17 | """
18 | AND corpus = @corpus -- "romeoandjuliet"
19 | AND word_count >= @min_word_count -- 250
20 | ORDER BY
21 | word_count DESC
22 |
--------------------------------------------------------------------------------
/packages/misc/queries/params-named.bqsql:
--------------------------------------------------------------------------------
1 | SELECT
2 | corpus,
3 | word,
4 | word_count
5 | FROM
6 | `bigquery-public-data.samples.shakespeare`
7 | WHERE
8 | corpus = @corpus -- "romeoandjuliet"
9 | AND word_count >= @min_word_count -- 250
10 | ORDER BY
11 | word_count DESC
12 |
--------------------------------------------------------------------------------
/packages/misc/queries/params-positional-with-comments.bqsql:
--------------------------------------------------------------------------------
1 | SELECT
2 | corpus,
3 | word,
4 | # @foo,
5 | # @bar # -- @baz */ @qux,
6 | word_count
7 | -- @bar # @baz -- @foo #-- @qux
8 | FROM
9 | `bigquery-public-data.samples.shakespeare`
10 | WHERE
11 | corpus = ?/* @foo -- @bar -- "romeoandjuliet"
12 | @baz # /* @qux
13 | @foo
14 | @foo@bar */AND word_count >= ? -- 250
15 | ORDER BY
16 | word_count DESC
17 |
--------------------------------------------------------------------------------
/packages/misc/queries/params-positional-with-strings.bqsql:
--------------------------------------------------------------------------------
1 | SELECT
2 | word,
3 | word_count
4 | FROM
5 | `bigquery-public-data.samples.shakespeare`
6 | WHERE
7 | word != "?"
8 | AND word != '?'
9 | AND word != '? it\'s ?'
10 | AND word != "? it\"s ?"
11 | AND word != """?"""
12 | AND word != """
13 | ?
14 | """
15 | AND word != """
16 | \""" ?
17 | """
18 | AND corpus = ? -- "romeoandjuliet"
19 | AND word_count >= ? -- 250
20 | ORDER BY
21 | word_count DESC
22 |
--------------------------------------------------------------------------------
/packages/misc/queries/params-positional.bqsql:
--------------------------------------------------------------------------------
1 | SELECT
2 | corpus,
3 | word,
4 | word_count
5 | FROM
6 | `bigquery-public-data.samples.shakespeare`
7 | WHERE
8 | corpus = ? -- "romeoandjuliet"
9 | AND word_count >= ? -- 250
10 | ORDER BY
11 | word_count DESC
12 |
--------------------------------------------------------------------------------
/packages/misc/queries/params-structs.bqsql:
--------------------------------------------------------------------------------
1 | SELECT
2 | @struct_value AS struct_obj; -- {"x": 1, "y": "foo"}
3 |
--------------------------------------------------------------------------------
/packages/misc/queries/params-timestamps.bqsql:
--------------------------------------------------------------------------------
1 | SELECT
2 | TIMESTAMP_ADD(@ts_value, INTERVAL 1 HOUR); -- "2006-01-02T15:04:05+0700"
3 |
--------------------------------------------------------------------------------
/packages/misc/queries/procedure.bqsql:
--------------------------------------------------------------------------------
1 | create or replace procedure testing.procedure()
2 | begin
3 | select * from `testing.create_or_replace`;
4 | end;
5 |
--------------------------------------------------------------------------------
/packages/misc/queries/select-a.bqsql:
--------------------------------------------------------------------------------
1 | SELECT
2 | *
3 | FROM
4 | `minodisk-api.testing.a`
5 |
--------------------------------------------------------------------------------
/packages/misc/queries/select-b.bqsql:
--------------------------------------------------------------------------------
1 | SELECT
2 | *
3 | FROM
4 | `vertect.test.b`
5 |
--------------------------------------------------------------------------------
/packages/misc/queries/select.bqsql:
--------------------------------------------------------------------------------
1 | select 1 as a
2 |
--------------------------------------------------------------------------------
/packages/misc/queries/spreadsheet.bqsql:
--------------------------------------------------------------------------------
1 | select
2 | *
3 | from
4 | testing.spreadsheet
5 |
--------------------------------------------------------------------------------
/packages/misc/queries/spreadsheet.tsv:
--------------------------------------------------------------------------------
1 | alphanumeric name phone numberrange country region postalZip address email text currency list
2 | SNL75UDJ2XX Imogene Chapman (516) 622-2461 6 Chile Zhytomyr oblast 13814 Ap #552-6241 Nascetur Avenue accumsan.sed@icloud.ca diam eu dolor egestas rhoncus. Proin nisl sem, consequat nec, $42.23 11
3 | OYI18QFY3TU Dalton Estrada 1-740-283-3671 0 France Auvergne 36958 3123 Massa Avenue ac@icloud.net Donec sollicitudin adipiscing ligula. Aenean gravida nunc sed pede. Cum $22.45 9
4 | TEJ36PSN2JN Tamekah Lindsay 1-184-543-5214 9 Sweden Katsina 74226 202-2369 Ullamcorper Rd. erat.eget@hotmail.edu lorem semper auctor. Mauris vel turpis. Aliquam adipiscing lobortis risus. $67.12 13
5 | DDB97BFI5DE Cain Floyd 1-808-389-2735 2 Nigeria Ontario 4187 Ap #577-6848 Amet Street ante.lectus.convallis@yahoo.couk nonummy ut, molestie in, tempus eu, ligula. Aenean euismod mauris $4.86 1
6 | HZG24KMK7DU Rhona Dale (112) 772-4333 5 India North Jeolla 7665 XL Ap #849-1554 Ultrices Rd. orci.luctus@aol.com eleifend, nunc risus varius orci, in consequat enim diam vel $60.71 5
7 |
--------------------------------------------------------------------------------
/packages/misc/queries/system-variables.bqsql:
--------------------------------------------------------------------------------
1 | SELECT @@project_id as project_id;
2 |
--------------------------------------------------------------------------------
/packages/misc/queries/types.bqsql:
--------------------------------------------------------------------------------
1 | declare a bignumeric(10) default cast('999999' as bignumeric(10));
2 | declare d bytes(10) default cast('foo' as bytes(10));
3 | declare e float64 default cast('3.14' as float64);
4 | declare f int64 default cast('9162371277713859723' as int64);
5 | DECLARE y NUMERIC(5, 2) DEFAULT 123.45;
6 |
7 | select
8 | a as BIGNUMERIC,
9 | true as BOOL,
10 | false as BOOLEAN,
11 | d as BYTES,
12 | current_date() as DATE,
13 | current_datetime() as DATETIME,
14 | 3.14 as FLOAT,
15 | e as FLOAT64,
16 | f as INT64,
17 | 4 as INTEGER,
18 | make_interval(6, 5, 4, 3, 2, 1) as itvl,
19 | y as NUMERIC,
20 | 'foo' as STRING,
21 | current_time() as TIME,
22 | current_timestamp() as TIMESTAMP,
23 |
--------------------------------------------------------------------------------
/packages/misc/queries/variables.bqsql:
--------------------------------------------------------------------------------
1 | DECLARE x NUMERIC(10) DEFAULT 12345;
2 |
3 | SELECT x AS x;
4 |
--------------------------------------------------------------------------------
/packages/misc/queries/wikipedia.bqsql:
--------------------------------------------------------------------------------
1 | select
2 | en_label,
3 | labels
4 | from
5 | `bigquery-public-data.wikipedia.wikidata`
6 | where
7 | array_length(labels) < 5
8 | limit
9 | 10000
10 |
--------------------------------------------------------------------------------
/packages/misc/queries/without-dataset.bqsql:
--------------------------------------------------------------------------------
1 | select
2 | en_label,
3 | labels
4 | from
5 | wikidata
6 | where
7 | array_length(labels) < 5
8 | limit
9 | 5000
10 |
--------------------------------------------------------------------------------
/packages/misc/screenshots/dry-run.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/minodisk/bigquery-runner/cf3781d7bb6538342dc98e5c12af6532d7af2617/packages/misc/screenshots/dry-run.gif
--------------------------------------------------------------------------------
/packages/misc/screenshots/query-validation.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/minodisk/bigquery-runner/cf3781d7bb6538342dc98e5c12af6532d7af2617/packages/misc/screenshots/query-validation.gif
--------------------------------------------------------------------------------
/packages/misc/screenshots/run.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/minodisk/bigquery-runner/cf3781d7bb6538342dc98e5c12af6532d7af2617/packages/misc/screenshots/run.gif
--------------------------------------------------------------------------------
/packages/shared/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "private": true,
3 | "name": "shared",
4 | "main": "src/index.ts",
5 | "scripts": {
6 | "lint": "eslint \"./src/**/*.ts\"",
7 | "check": "run-p check:*",
8 | "check:tsc": "tsc",
9 | "check:format": "npm run format -- --check",
10 | "fix": "run-p fix:*",
11 | "fix:lint": "npm run lint -- --fix",
12 | "fix:format": "npm run format -- --write",
13 | "test": "jest --silent=false",
14 | "test-coverage": "npm run test -- --coverage --coverageDirectory ../../coverage/core",
15 | "test-watch": "npm run test -- --watchAll",
16 | "format": "prettier --write src"
17 | },
18 | "jest": {
19 | "roots": [
20 | "/src"
21 | ],
22 | "testMatch": [
23 | "**/?(*.)+(spec|test).+(ts|tsx|js)"
24 | ],
25 | "transform": {
26 | "^.+\\.(ts|tsx)$": "ts-jest"
27 | }
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/packages/shared/src/error.ts:
--------------------------------------------------------------------------------
1 | import type { Failure } from "./result";
2 | import { unwrap } from "./result";
3 |
4 | export type Err = {
5 | type: T;
6 | reason: string;
7 | };
8 |
9 | export type UnknownError = Err<"Unknown">;
10 |
11 | export const errorToString = (err: unknown): string => {
12 | const failure = err as Failure>;
13 | if (failure.success === false) {
14 | const error = unwrap(failure);
15 | return `${error.type}: ${error.reason}`;
16 | }
17 |
18 | const error = err as Err;
19 | if (error.type && error.reason) {
20 | return `${error.type}: ${error.reason}`;
21 | }
22 |
23 | const e = err as { message: string; toString(): string };
24 | if (e.message) {
25 | return e.message;
26 | }
27 |
28 | if (e.toString) {
29 | return e.toString();
30 | }
31 |
32 | return String(e);
33 | };
34 |
--------------------------------------------------------------------------------
/packages/shared/src/funcs.test.ts:
--------------------------------------------------------------------------------
1 | import {
2 | commas,
3 | getJobName,
4 | getRoutineName,
5 | getTableName,
6 | isData,
7 | } from "./funcs";
8 |
9 | describe("funcs", () => {
10 | describe(getJobName.name, () => {
11 | it("should return job name", () => {
12 | expect(
13 | getJobName({
14 | projectId: "foo",
15 | location: "bar",
16 | jobId: "baz",
17 | })
18 | ).toStrictEqual(`foo:bar.baz`);
19 | });
20 | });
21 |
22 | describe(getTableName.name, () => {
23 | it("should return table name", () => {
24 | expect(
25 | getTableName({
26 | projectId: "foo",
27 | datasetId: "bar",
28 | tableId: "baz",
29 | })
30 | ).toStrictEqual(`foo.bar.baz`);
31 | });
32 | });
33 |
34 | describe(getRoutineName.name, () => {
35 | it("should return routine name", () => {
36 | expect(
37 | getRoutineName({
38 | projectId: "foo",
39 | datasetId: "bar",
40 | routineId: "baz",
41 | })
42 | ).toStrictEqual(`foo.bar.baz`);
43 | });
44 | });
45 |
46 | describe(isData.name, () => {
47 | it("should return detailed type", () => {
48 | const data = { source: "" };
49 | if (isData(data)) {
50 | // No type error
51 | data.payload.event;
52 | }
53 |
54 | expect(isData({})).toBeFalsy();
55 | expect(isData({ source: "foo" })).toBeFalsy();
56 | expect(isData({ source: "bigquery-runner" })).toBeTruthy();
57 | });
58 | });
59 |
60 | describe(commas.name, () => {
61 | it("should add comma every three digits", () => {
62 | expect(commas("1")).toBe("1");
63 | expect(commas("12")).toBe("12");
64 | expect(commas("123")).toBe("123");
65 | expect(commas("1234")).toBe("1,234");
66 | expect(commas("12345")).toBe("12,345");
67 | expect(commas("123456")).toBe("123,456");
68 | expect(commas("1234567")).toBe("1,234,567");
69 | expect(commas("123000")).toBe("123,000");
70 | });
71 | });
72 | });
73 |
--------------------------------------------------------------------------------
/packages/shared/src/funcs.ts:
--------------------------------------------------------------------------------
1 | import type {
2 | ChildDdlRoutineQuery,
3 | ChildDdlTableQuery,
4 | ChildDmlQuery,
5 | ChildQuery,
6 | Data,
7 | DownloadEvent,
8 | EndEvent,
9 | FailProcessingEvent,
10 | FocusedEvent,
11 | FocusOnTabEvent,
12 | JobHasChildrenStatistics,
13 | JobReference,
14 | JobStandaloneStatistics,
15 | JobStatistics,
16 | MetadataEvent,
17 | MoveTabFocusEvent,
18 | NextEvent,
19 | PrevEvent,
20 | PreviewEvent,
21 | RendererEvent,
22 | RoutinesEvent,
23 | RoutineReference,
24 | RowsEvent,
25 | StartEvent,
26 | StartProcessingEvent,
27 | SuccessProcessingEvent,
28 | TablesEvent,
29 | TableReference,
30 | ViewerEvent,
31 | } from "./types";
32 |
33 | export const getJobName = ({ projectId, location, jobId }: JobReference) =>
34 | `${projectId}:${location}.${jobId}`;
35 |
36 | export const getTableName = ({
37 | projectId,
38 | datasetId,
39 | tableId,
40 | }: TableReference): string => `${projectId}.${datasetId}.${tableId}`;
41 |
42 | export const getRoutineName = ({
43 | projectId,
44 | datasetId,
45 | routineId,
46 | }: RoutineReference): string => `${projectId}.${datasetId}.${routineId}`;
47 |
48 | export const isStandaloneStatistics = (
49 | statistics: JobStatistics
50 | ): statistics is JobStandaloneStatistics => {
51 | return !("numChildJobs" in statistics);
52 | };
53 | export const isJobHasChildrenStatistics = (
54 | statistics: JobStatistics
55 | ): statistics is JobHasChildrenStatistics => {
56 | return "numChildJobs" in statistics;
57 | };
58 |
59 | export const isChildDmlQuery = (query: ChildQuery): query is ChildDmlQuery => {
60 | return "dmlStats" in query;
61 | };
62 | export const isChildDdlTableQuery = (
63 | query: ChildQuery
64 | ): query is ChildDdlTableQuery => {
65 | return "ddlTargetTable" in query;
66 | };
67 | export const isChildDdlRoutineQuery = (
68 | query: ChildQuery
69 | ): query is ChildDdlRoutineQuery => {
70 | return "ddlTargetRoutine" in query;
71 | };
72 |
73 | export function isData(data: { source?: string }): data is Data {
74 | return data.source === "bigquery-runner";
75 | }
76 |
77 | export function isFocusedEvent(e: RendererEvent): e is FocusedEvent {
78 | return e.event === "focused";
79 | }
80 |
81 | export function isStartProcessingEvent(
82 | e: RendererEvent
83 | ): e is StartProcessingEvent {
84 | return e.event === "startProcessing";
85 | }
86 |
87 | export function isMetadataEvent(e: RendererEvent): e is MetadataEvent {
88 | return e.event === "metadata";
89 | }
90 |
91 | export function isTablesEvent(e: RendererEvent): e is TablesEvent {
92 | return e.event === "tables";
93 | }
94 |
95 | export function isRoutinesEvent(e: RendererEvent): e is RoutinesEvent {
96 | return e.event === "routines";
97 | }
98 |
99 | export function isRowsEvent(e: RendererEvent): e is RowsEvent {
100 | return e.event === "rows";
101 | }
102 |
103 | export function isSuccessLoadingEvent(
104 | e: RendererEvent
105 | ): e is SuccessProcessingEvent {
106 | return e.event === "successProcessing";
107 | }
108 |
109 | export function isFailProcessingEvent(
110 | e: RendererEvent
111 | ): e is FailProcessingEvent {
112 | return e.event === "failProcessing";
113 | }
114 |
115 | export function isMoveTabFocusEvent(e: RendererEvent): e is MoveTabFocusEvent {
116 | return e.event === "moveTabFocus";
117 | }
118 |
119 | export function isFocusOnTabEvent(e: RendererEvent): e is FocusOnTabEvent {
120 | return e.event === "focusOnTab";
121 | }
122 |
123 | export function isLoadedEvent(e: ViewerEvent): e is StartEvent {
124 | return e.event === "loaded";
125 | }
126 |
127 | export function isStartEvent(e: ViewerEvent): e is StartEvent {
128 | return e.event === "start";
129 | }
130 |
131 | export function isEndEvent(e: ViewerEvent): e is EndEvent {
132 | return e.event === "end";
133 | }
134 |
135 | export function isPrevEvent(e: ViewerEvent): e is PrevEvent {
136 | return e.event === "prev";
137 | }
138 |
139 | export function isNextEvent(e: ViewerEvent): e is NextEvent {
140 | return e.event === "next";
141 | }
142 |
143 | export function isDownloadEvent(e: ViewerEvent): e is DownloadEvent {
144 | return e.event === "download";
145 | }
146 |
147 | export function isPreviewEvent(e: ViewerEvent): e is PreviewEvent {
148 | return e.event === "preview";
149 | }
150 |
151 | export const commas = (num: bigint | string): string => {
152 | const text = num.toString();
153 | const len = text.length;
154 | let result = "";
155 | for (let i = 0; i < len; i++) {
156 | const char = text[len - 1 - i];
157 | if (!char) {
158 | break;
159 | }
160 | if (i === 0 || i % 3 !== 0) {
161 | result = `${char}${result}`;
162 | } else {
163 | result = `${char},${result}`;
164 | }
165 | }
166 | return result;
167 | };
168 |
--------------------------------------------------------------------------------
/packages/shared/src/index.ts:
--------------------------------------------------------------------------------
1 | export * from "./error";
2 | export * from "./funcs";
3 | export * from "./result";
4 | export * from "./types";
5 |
--------------------------------------------------------------------------------
/packages/shared/src/result.ts:
--------------------------------------------------------------------------------
1 | export type Result = Failure | Success;
2 |
3 | export type Failure = Readonly<{
4 | success: false;
5 | value: F;
6 | }>;
7 |
8 | export const fail = (value: F): Failure => ({
9 | success: false,
10 | value,
11 | });
12 |
13 | export type Success = Readonly<{
14 | success: true;
15 | value: S;
16 | }>;
17 |
18 | export const succeed = (value: S): Success => ({
19 | success: true,
20 | value,
21 | });
22 |
23 | export const unwrap = ({ value }: { value: T }): T => value;
24 |
25 | export const tryCatchSync = (
26 | tryFn: () => S,
27 | catchFn: (err: unknown) => F
28 | ): Result => {
29 | try {
30 | return succeed(tryFn());
31 | } catch (err) {
32 | return fail(catchFn(err));
33 | }
34 | };
35 |
36 | export const tryCatch = async (
37 | tryFn: () => Promise,
38 | catchFn: (err: unknown) => F
39 | ): Promise> => {
40 | try {
41 | return succeed(await tryFn());
42 | } catch (err) {
43 | return fail(catchFn(err));
44 | }
45 | };
46 |
--------------------------------------------------------------------------------
/packages/shared/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "../../tsconfig.json",
3 | "compilerOptions": {
4 | "rootDir": "src"
5 | },
6 | "include": ["src"]
7 | }
8 |
--------------------------------------------------------------------------------
/packages/viewer/config/env.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | const fs = require('fs');
4 | const path = require('path');
5 | const paths = require('./paths');
6 |
7 | // Make sure that including paths.js after env.js will read .env variables.
8 | delete require.cache[require.resolve('./paths')];
9 |
10 | const NODE_ENV = process.env.NODE_ENV;
11 | if (!NODE_ENV) {
12 | throw new Error(
13 | 'The NODE_ENV environment variable is required but was not specified.'
14 | );
15 | }
16 |
17 | // https://github.com/bkeepers/dotenv#what-other-env-files-can-i-use
18 | const dotenvFiles = [
19 | `${paths.dotenv}.${NODE_ENV}.local`,
20 | // Don't include `.env.local` for `test` environment
21 | // since normally you expect tests to produce the same
22 | // results for everyone
23 | NODE_ENV !== 'test' && `${paths.dotenv}.local`,
24 | `${paths.dotenv}.${NODE_ENV}`,
25 | paths.dotenv,
26 | ].filter(Boolean);
27 |
28 | // Load environment variables from .env* files. Suppress warnings using silent
29 | // if this file is missing. dotenv will never modify any environment variables
30 | // that have already been set. Variable expansion is supported in .env files.
31 | // https://github.com/motdotla/dotenv
32 | // https://github.com/motdotla/dotenv-expand
33 | dotenvFiles.forEach(dotenvFile => {
34 | if (fs.existsSync(dotenvFile)) {
35 | require('dotenv-expand')(
36 | require('dotenv').config({
37 | path: dotenvFile,
38 | })
39 | );
40 | }
41 | });
42 |
43 | // We support resolving modules according to `NODE_PATH`.
44 | // This lets you use absolute paths in imports inside large monorepos:
45 | // https://github.com/facebook/create-react-app/issues/253.
46 | // It works similar to `NODE_PATH` in Node itself:
47 | // https://nodejs.org/api/modules.html#modules_loading_from_the_global_folders
48 | // Note that unlike in Node, only *relative* paths from `NODE_PATH` are honored.
49 | // Otherwise, we risk importing Node.js core modules into an app instead of webpack shims.
50 | // https://github.com/facebook/create-react-app/issues/1023#issuecomment-265344421
51 | // We also resolve them to make sure all tools using them work consistently.
52 | const appDirectory = fs.realpathSync(process.cwd());
53 | process.env.NODE_PATH = (process.env.NODE_PATH || '')
54 | .split(path.delimiter)
55 | .filter(folder => folder && !path.isAbsolute(folder))
56 | .map(folder => path.resolve(appDirectory, folder))
57 | .join(path.delimiter);
58 |
59 | // Grab NODE_ENV and REACT_APP_* environment variables and prepare them to be
60 | // injected into the application via DefinePlugin in webpack configuration.
61 | const REACT_APP = /^REACT_APP_/i;
62 |
63 | function getClientEnvironment(publicUrl) {
64 | const raw = Object.keys(process.env)
65 | .filter(key => REACT_APP.test(key))
66 | .reduce(
67 | (env, key) => {
68 | env[key] = process.env[key];
69 | return env;
70 | },
71 | {
72 | // Useful for determining whether we’re running in production mode.
73 | // Most importantly, it switches React into the correct mode.
74 | NODE_ENV: process.env.NODE_ENV || 'development',
75 | // Useful for resolving the correct path to static assets in `public`.
76 | // For example,
.
77 | // This should only be used as an escape hatch. Normally you would put
78 | // images into the `src` and `import` them in code to get their paths.
79 | PUBLIC_URL: publicUrl,
80 | // We support configuring the sockjs pathname during development.
81 | // These settings let a developer run multiple simultaneous projects.
82 | // They are used as the connection `hostname`, `pathname` and `port`
83 | // in webpackHotDevClient. They are used as the `sockHost`, `sockPath`
84 | // and `sockPort` options in webpack-dev-server.
85 | WDS_SOCKET_HOST: process.env.WDS_SOCKET_HOST,
86 | WDS_SOCKET_PATH: process.env.WDS_SOCKET_PATH,
87 | WDS_SOCKET_PORT: process.env.WDS_SOCKET_PORT,
88 | // Whether or not react-refresh is enabled.
89 | // It is defined here so it is available in the webpackHotDevClient.
90 | FAST_REFRESH: process.env.FAST_REFRESH !== 'false',
91 | }
92 | );
93 | // Stringify all values so we can feed into webpack DefinePlugin
94 | const stringified = {
95 | 'process.env': Object.keys(raw).reduce((env, key) => {
96 | env[key] = JSON.stringify(raw[key]);
97 | return env;
98 | }, {}),
99 | };
100 |
101 | return { raw, stringified };
102 | }
103 |
104 | module.exports = getClientEnvironment;
105 |
--------------------------------------------------------------------------------
/packages/viewer/config/getHttpsConfig.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | const fs = require('fs');
4 | const path = require('path');
5 | const crypto = require('crypto');
6 | const chalk = require('react-dev-utils/chalk');
7 | const paths = require('./paths');
8 |
9 | // Ensure the certificate and key provided are valid and if not
10 | // throw an easy to debug error
11 | function validateKeyAndCerts({ cert, key, keyFile, crtFile }) {
12 | let encrypted;
13 | try {
14 | // publicEncrypt will throw an error with an invalid cert
15 | encrypted = crypto.publicEncrypt(cert, Buffer.from('test'));
16 | } catch (err) {
17 | throw new Error(
18 | `The certificate "${chalk.yellow(crtFile)}" is invalid.\n${err.message}`
19 | );
20 | }
21 |
22 | try {
23 | // privateDecrypt will throw an error with an invalid key
24 | crypto.privateDecrypt(key, encrypted);
25 | } catch (err) {
26 | throw new Error(
27 | `The certificate key "${chalk.yellow(keyFile)}" is invalid.\n${
28 | err.message
29 | }`
30 | );
31 | }
32 | }
33 |
34 | // Read file and throw an error if it doesn't exist
35 | function readEnvFile(file, type) {
36 | if (!fs.existsSync(file)) {
37 | throw new Error(
38 | `You specified ${chalk.cyan(
39 | type
40 | )} in your env, but the file "${chalk.yellow(file)}" can't be found.`
41 | );
42 | }
43 | return fs.readFileSync(file);
44 | }
45 |
46 | // Get the https config
47 | // Return cert files if provided in env, otherwise just true or false
48 | function getHttpsConfig() {
49 | const { SSL_CRT_FILE, SSL_KEY_FILE, HTTPS } = process.env;
50 | const isHttps = HTTPS === 'true';
51 |
52 | if (isHttps && SSL_CRT_FILE && SSL_KEY_FILE) {
53 | const crtFile = path.resolve(paths.appPath, SSL_CRT_FILE);
54 | const keyFile = path.resolve(paths.appPath, SSL_KEY_FILE);
55 | const config = {
56 | cert: readEnvFile(crtFile, 'SSL_CRT_FILE'),
57 | key: readEnvFile(keyFile, 'SSL_KEY_FILE'),
58 | };
59 |
60 | validateKeyAndCerts({ ...config, keyFile, crtFile });
61 | return config;
62 | }
63 | return isHttps;
64 | }
65 |
66 | module.exports = getHttpsConfig;
67 |
--------------------------------------------------------------------------------
/packages/viewer/config/jest/babelTransform.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | const babelJest = require('babel-jest').default;
4 |
5 | const hasJsxRuntime = (() => {
6 | if (process.env.DISABLE_NEW_JSX_TRANSFORM === 'true') {
7 | return false;
8 | }
9 |
10 | try {
11 | require.resolve('react/jsx-runtime');
12 | return true;
13 | } catch (e) {
14 | return false;
15 | }
16 | })();
17 |
18 | module.exports = babelJest.createTransformer({
19 | presets: [
20 | [
21 | require.resolve('babel-preset-react-app'),
22 | {
23 | runtime: hasJsxRuntime ? 'automatic' : 'classic',
24 | },
25 | ],
26 | ],
27 | babelrc: false,
28 | configFile: false,
29 | });
30 |
--------------------------------------------------------------------------------
/packages/viewer/config/jest/cssTransform.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | // This is a custom Jest transformer turning style imports into empty objects.
4 | // http://facebook.github.io/jest/docs/en/webpack.html
5 |
6 | module.exports = {
7 | process() {
8 | return 'module.exports = {};';
9 | },
10 | getCacheKey() {
11 | // The output is always the same.
12 | return 'cssTransform';
13 | },
14 | };
15 |
--------------------------------------------------------------------------------
/packages/viewer/config/jest/fileTransform.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | const path = require('path');
4 | const camelcase = require('camelcase');
5 |
6 | // This is a custom Jest transformer turning file imports into filenames.
7 | // http://facebook.github.io/jest/docs/en/webpack.html
8 |
9 | module.exports = {
10 | process(src, filename) {
11 | const assetFilename = JSON.stringify(path.basename(filename));
12 |
13 | if (filename.match(/\.svg$/)) {
14 | // Based on how SVGR generates a component name:
15 | // https://github.com/smooth-code/svgr/blob/01b194cf967347d43d4cbe6b434404731b87cf27/packages/core/src/state.js#L6
16 | const pascalCaseFilename = camelcase(path.parse(filename).name, {
17 | pascalCase: true,
18 | });
19 | const componentName = `Svg${pascalCaseFilename}`;
20 | return `const React = require('react');
21 | module.exports = {
22 | __esModule: true,
23 | default: ${assetFilename},
24 | ReactComponent: React.forwardRef(function ${componentName}(props, ref) {
25 | return {
26 | $$typeof: Symbol.for('react.element'),
27 | type: 'svg',
28 | ref: ref,
29 | key: null,
30 | props: Object.assign({}, props, {
31 | children: ${assetFilename}
32 | })
33 | };
34 | }),
35 | };`;
36 | }
37 |
38 | return `module.exports = ${assetFilename};`;
39 | },
40 | };
41 |
--------------------------------------------------------------------------------
/packages/viewer/config/modules.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | const fs = require('fs');
4 | const path = require('path');
5 | const paths = require('./paths');
6 | const chalk = require('react-dev-utils/chalk');
7 | const resolve = require('resolve');
8 |
9 | /**
10 | * Get additional module paths based on the baseUrl of a compilerOptions object.
11 | *
12 | * @param {Object} options
13 | */
14 | function getAdditionalModulePaths(options = {}) {
15 | const baseUrl = options.baseUrl;
16 |
17 | if (!baseUrl) {
18 | return '';
19 | }
20 |
21 | const baseUrlResolved = path.resolve(paths.appPath, baseUrl);
22 |
23 | // We don't need to do anything if `baseUrl` is set to `node_modules`. This is
24 | // the default behavior.
25 | if (path.relative(paths.appNodeModules, baseUrlResolved) === '') {
26 | return null;
27 | }
28 |
29 | // Allow the user set the `baseUrl` to `appSrc`.
30 | if (path.relative(paths.appSrc, baseUrlResolved) === '') {
31 | return [paths.appSrc];
32 | }
33 |
34 | // If the path is equal to the root directory we ignore it here.
35 | // We don't want to allow importing from the root directly as source files are
36 | // not transpiled outside of `src`. We do allow importing them with the
37 | // absolute path (e.g. `src/Components/Button.js`) but we set that up with
38 | // an alias.
39 | if (path.relative(paths.appPath, baseUrlResolved) === '') {
40 | return null;
41 | }
42 |
43 | // Otherwise, throw an error.
44 | throw new Error(
45 | chalk.red.bold(
46 | "Your project's `baseUrl` can only be set to `src` or `node_modules`." +
47 | ' Create React App does not support other values at this time.'
48 | )
49 | );
50 | }
51 |
52 | /**
53 | * Get webpack aliases based on the baseUrl of a compilerOptions object.
54 | *
55 | * @param {*} options
56 | */
57 | function getWebpackAliases(options = {}) {
58 | const baseUrl = options.baseUrl;
59 |
60 | if (!baseUrl) {
61 | return {};
62 | }
63 |
64 | const baseUrlResolved = path.resolve(paths.appPath, baseUrl);
65 |
66 | if (path.relative(paths.appPath, baseUrlResolved) === '') {
67 | return {
68 | src: paths.appSrc,
69 | };
70 | }
71 | }
72 |
73 | /**
74 | * Get jest aliases based on the baseUrl of a compilerOptions object.
75 | *
76 | * @param {*} options
77 | */
78 | function getJestAliases(options = {}) {
79 | const baseUrl = options.baseUrl;
80 |
81 | if (!baseUrl) {
82 | return {};
83 | }
84 |
85 | const baseUrlResolved = path.resolve(paths.appPath, baseUrl);
86 |
87 | if (path.relative(paths.appPath, baseUrlResolved) === '') {
88 | return {
89 | '^src/(.*)$': '/src/$1',
90 | };
91 | }
92 | }
93 |
94 | function getModules() {
95 | // Check if TypeScript is setup
96 | const hasTsConfig = fs.existsSync(paths.appTsConfig);
97 | const hasJsConfig = fs.existsSync(paths.appJsConfig);
98 |
99 | if (hasTsConfig && hasJsConfig) {
100 | throw new Error(
101 | 'You have both a tsconfig.json and a jsconfig.json. If you are using TypeScript please remove your jsconfig.json file.'
102 | );
103 | }
104 |
105 | let config;
106 |
107 | // If there's a tsconfig.json we assume it's a
108 | // TypeScript project and set up the config
109 | // based on tsconfig.json
110 | if (hasTsConfig) {
111 | const ts = require(resolve.sync('typescript', {
112 | basedir: paths.appNodeModules,
113 | }));
114 | config = ts.readConfigFile(paths.appTsConfig, ts.sys.readFile).config;
115 | // Otherwise we'll check if there is jsconfig.json
116 | // for non TS projects.
117 | } else if (hasJsConfig) {
118 | config = require(paths.appJsConfig);
119 | }
120 |
121 | config = config || {};
122 | const options = config.compilerOptions || {};
123 |
124 | const additionalModulePaths = getAdditionalModulePaths(options);
125 |
126 | return {
127 | additionalModulePaths: additionalModulePaths,
128 | webpackAliases: getWebpackAliases(options),
129 | jestAliases: getJestAliases(options),
130 | hasTsConfig,
131 | };
132 | }
133 |
134 | module.exports = getModules();
135 |
--------------------------------------------------------------------------------
/packages/viewer/config/paths.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | const path = require('path');
4 | const fs = require('fs');
5 | const getPublicUrlOrPath = require('react-dev-utils/getPublicUrlOrPath');
6 |
7 | // Make sure any symlinks in the project folder are resolved:
8 | // https://github.com/facebook/create-react-app/issues/637
9 | const appDirectory = fs.realpathSync(process.cwd());
10 | const resolveApp = relativePath => path.resolve(appDirectory, relativePath);
11 |
12 | // We use `PUBLIC_URL` environment variable or "homepage" field to infer
13 | // "public path" at which the app is served.
14 | // webpack needs to know it to put the right
12 |