@graphenedata/cli 0.0.12 → 0.0.14
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli/cli.js +8591 -1214
- package/dist/docs/base.md +98 -0
- package/dist/docs/cli.md +22 -0
- package/dist/docs/graphene.md +10 -10
- package/dist/ui/component-utilities/echarts.js +2 -3
- package/dist/ui/component-utilities/formatting.js +3 -11
- package/dist/ui/component-utilities/getSeriesConfig.js +2 -1
- package/dist/ui/components/Area.svelte +188 -151
- package/dist/ui/components/AreaChart.svelte +43 -79
- package/dist/ui/components/Bar.svelte +273 -255
- package/dist/ui/components/BarChart.svelte +58 -112
- package/dist/ui/components/BigValue.svelte +13 -7
- package/dist/ui/components/Chart.svelte +280 -317
- package/dist/ui/components/Column.svelte +102 -113
- package/dist/ui/components/DateRange.svelte +37 -27
- package/dist/ui/components/Dropdown.svelte +77 -57
- package/dist/ui/components/DropdownOption.svelte +10 -7
- package/dist/ui/components/ECharts.svelte +23 -16
- package/dist/ui/components/ErrorChart.svelte +85 -21
- package/dist/ui/components/GrapheneQuery.svelte +7 -3
- package/dist/ui/components/InlineDelta.svelte +53 -34
- package/dist/ui/components/Line.svelte +192 -178
- package/dist/ui/components/LineChart.svelte +53 -96
- package/dist/ui/components/PieChart.svelte +26 -15
- package/dist/ui/components/QueryLoad.svelte +15 -10
- package/dist/ui/components/SortIcon.svelte +5 -1
- package/dist/ui/components/Table.svelte +15 -9
- package/dist/ui/components/TableCell.svelte +30 -17
- package/dist/ui/components/TableGroupRow.svelte +26 -19
- package/dist/ui/components/TableGroupToggle.svelte +9 -6
- package/dist/ui/components/TableHeader.svelte +37 -27
- package/dist/ui/components/TableRow.svelte +30 -20
- package/dist/ui/components/TableSubtotalRow.svelte +16 -9
- package/dist/ui/components/TableTotalRow.svelte +18 -11
- package/dist/ui/components/TextInput.svelte +23 -20
- package/dist/ui/components/_Table.svelte +303 -260
- package/dist/ui/internal/LocalApp.svelte +40 -0
- package/dist/ui/internal/NavSidebar.svelte +27 -30
- package/dist/ui/internal/PageError.svelte +23 -0
- package/dist/ui/internal/checkSocket.ts +48 -0
- package/dist/ui/internal/queryEngine.ts +9 -2
- package/dist/ui/internal/telemetry.ts +1 -0
- package/dist/ui/web.js +5 -55
- package/package.json +9 -10
- package/cli.ts +0 -156
- package/dist/ui/internal/NavSidebarHMR.svelte +0 -8
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
<script>
|
|
2
|
+
import {errorProvider} from './telemetry.ts'
|
|
3
|
+
import navFiles from 'virtual:nav'
|
|
4
|
+
import NavSidebar from './NavSidebar.svelte'
|
|
5
|
+
import PageError from './PageError.svelte'
|
|
6
|
+
|
|
7
|
+
// Nav sidebar with HMR support for the virtual file list
|
|
8
|
+
let navData = $state(navFiles)
|
|
9
|
+
import.meta.hot?.accept('virtual:nav', mod => navData = mod.default)
|
|
10
|
+
|
|
11
|
+
// Track compile errors from both initial load and subsequent HMR failures.
|
|
12
|
+
// Uses errorProvider so `check` can report compilation errors.
|
|
13
|
+
let compileError = $state(null)
|
|
14
|
+
errorProvider('compile', () => compileError ? [compileError] : [])
|
|
15
|
+
import.meta.hot?.on('vite:error', (payload) => {
|
|
16
|
+
compileError = payload.err
|
|
17
|
+
compileError.type = 'compile'
|
|
18
|
+
compileError.file = payload.err.id
|
|
19
|
+
Page = null
|
|
20
|
+
})
|
|
21
|
+
|
|
22
|
+
// The md file is dynamically imported, so even if there's a compile error, we'll still load LocalApp and can show the user the issue
|
|
23
|
+
let Page = $state(null)
|
|
24
|
+
let pathName = window.location.pathname.replace(/^\//, '') || 'index'
|
|
25
|
+
if (pathName !== '__ct') {
|
|
26
|
+
import(/* @vite-ignore */ '/' + pathName + '.md').then(mod => {
|
|
27
|
+
Page = mod.default
|
|
28
|
+
compileError = null
|
|
29
|
+
}).catch(() => {})
|
|
30
|
+
}
|
|
31
|
+
</script>
|
|
32
|
+
|
|
33
|
+
<nav id="nav"><NavSidebar files={navData} /></nav>
|
|
34
|
+
<main id="content">
|
|
35
|
+
{#if compileError}
|
|
36
|
+
<PageError error={compileError} />
|
|
37
|
+
{:else if Page}
|
|
38
|
+
<Page />
|
|
39
|
+
{/if}
|
|
40
|
+
</main>
|
|
@@ -1,25 +1,22 @@
|
|
|
1
1
|
<script>
|
|
2
|
+
import {SvelteSet, SvelteMap} from 'svelte/reactivity'
|
|
3
|
+
|
|
2
4
|
/** @type {string} */
|
|
3
|
-
|
|
4
|
-
/** @type {string[]} */
|
|
5
|
-
export let files = []
|
|
6
|
-
/** @type {((href: string) => void) | undefined} */
|
|
7
|
-
export let onNavigate = undefined
|
|
8
|
-
/** @type {string} */
|
|
9
|
-
export let baseRoute = ''
|
|
5
|
+
let {currentFile = '', files = [], onNavigate = undefined, baseRoute = ''} = $props()
|
|
10
6
|
|
|
11
|
-
let tree = []
|
|
12
|
-
let flatNodes = []
|
|
13
|
-
|
|
14
|
-
let
|
|
15
|
-
let
|
|
7
|
+
let tree = $state([])
|
|
8
|
+
let flatNodes = $state([])
|
|
9
|
+
// eslint-disable-next-line svelte/no-unnecessary-state-wrap -- openFolders is reassigned, needs $state
|
|
10
|
+
let openFolders = $state(new SvelteSet())
|
|
11
|
+
let treeSignature = $state('')
|
|
12
|
+
let lastCurrent = $state('')
|
|
16
13
|
|
|
17
|
-
|
|
18
|
-
.map((file) => file.replace(/^\.\//, '').replace(/\\/g, '/'))
|
|
14
|
+
let normalizedFiles = $derived((files || [])
|
|
15
|
+
.map((file) => file.replace(/^\.\//, '').replace(/\\/g, '/')))
|
|
19
16
|
|
|
20
17
|
|
|
21
|
-
|
|
22
|
-
|
|
18
|
+
let normalizedCurrent = $derived(deriveCurrentFile(currentFile, normalizedFiles, baseRoute))
|
|
19
|
+
let currentRoute = $derived(normalizedCurrent ? pathToRoute(normalizedCurrent) : '/')
|
|
23
20
|
|
|
24
21
|
function deriveCurrentFile (_currentFile, _normalizedFiles, _baseRoute) {
|
|
25
22
|
let fromProp = normalizeFilePath(currentFile)
|
|
@@ -42,7 +39,7 @@
|
|
|
42
39
|
return route
|
|
43
40
|
}
|
|
44
41
|
|
|
45
|
-
|
|
42
|
+
$effect(() => {
|
|
46
43
|
let nextSignature = normalizedFiles.join('|')
|
|
47
44
|
if (nextSignature !== treeSignature) {
|
|
48
45
|
treeSignature = nextSignature
|
|
@@ -50,18 +47,18 @@
|
|
|
50
47
|
flatNodes = flattenTree(tree)
|
|
51
48
|
openFolders = createDefaultOpenFolders(tree, normalizedCurrent)
|
|
52
49
|
}
|
|
53
|
-
}
|
|
50
|
+
})
|
|
54
51
|
|
|
55
|
-
|
|
52
|
+
$effect(() => {
|
|
56
53
|
if (normalizedCurrent !== lastCurrent) {
|
|
57
54
|
openFolders = mergeAncestorFolders(openFolders, normalizedCurrent)
|
|
58
55
|
lastCurrent = normalizedCurrent
|
|
59
56
|
}
|
|
60
|
-
}
|
|
57
|
+
})
|
|
61
58
|
|
|
62
59
|
function toggleFolder (path) {
|
|
63
60
|
if (!path) return
|
|
64
|
-
let next = new
|
|
61
|
+
let next = new SvelteSet(openFolders)
|
|
65
62
|
if (next.has(path)) next.delete(path)
|
|
66
63
|
else next.add(path)
|
|
67
64
|
openFolders = next
|
|
@@ -84,7 +81,7 @@
|
|
|
84
81
|
|
|
85
82
|
function buildTree (paths) {
|
|
86
83
|
let root = []
|
|
87
|
-
let folderMap = new
|
|
84
|
+
let folderMap = new SvelteMap()
|
|
88
85
|
|
|
89
86
|
for (let filePath of paths) {
|
|
90
87
|
let cleanPath = filePath.replace(/^\.\//, '').replace(/^\//, '')
|
|
@@ -167,16 +164,16 @@
|
|
|
167
164
|
}
|
|
168
165
|
|
|
169
166
|
function createDefaultOpenFolders (_treeNodes, currentPath) {
|
|
170
|
-
let next = new
|
|
167
|
+
let next = new SvelteSet()
|
|
171
168
|
return mergeAncestorFolders(next, currentPath)
|
|
172
169
|
}
|
|
173
170
|
|
|
174
171
|
function mergeAncestorFolders (openSet, filePath) {
|
|
175
|
-
if (!filePath) return new
|
|
172
|
+
if (!filePath) return new SvelteSet(openSet)
|
|
176
173
|
let parts = filePath.split('/')
|
|
177
174
|
parts.pop()
|
|
178
175
|
let aggregate = []
|
|
179
|
-
let next = new
|
|
176
|
+
let next = new SvelteSet(openSet)
|
|
180
177
|
for (let part of parts) {
|
|
181
178
|
aggregate.push(part)
|
|
182
179
|
next.add(aggregate.join('/'))
|
|
@@ -217,15 +214,15 @@
|
|
|
217
214
|
class={node.route ? 'folder-row' : 'folder-row clickable'}
|
|
218
215
|
role={node.route ? undefined : 'button'}
|
|
219
216
|
aria-expanded={node.route ? undefined : String(isOpen(node.path, openFolders))}
|
|
220
|
-
|
|
221
|
-
|
|
217
|
+
onclick={node.route ? undefined : () => toggleFolder(node.path)}
|
|
218
|
+
onkeydown={node.route ? undefined : (event) => handleFolderRowKey(event, node.path)}
|
|
222
219
|
>
|
|
223
220
|
<button
|
|
224
221
|
class="toggle"
|
|
225
222
|
type="button"
|
|
226
223
|
data-folder-toggle={node.path}
|
|
227
224
|
aria-expanded={isOpen(node.path, openFolders)}
|
|
228
|
-
|
|
225
|
+
onclick={(event) => { event.stopPropagation(); toggleFolder(node.path) }}
|
|
229
226
|
aria-label={(isOpen(node.path, openFolders) ? 'Collapse' : 'Expand') + ' ' + node.label}
|
|
230
227
|
>
|
|
231
228
|
<span class={isOpen(node.path, openFolders) ? 'chevron open' : 'chevron'}>▸</span>
|
|
@@ -235,7 +232,7 @@
|
|
|
235
232
|
href={node.route}
|
|
236
233
|
class={node.route === currentRoute ? 'active' : ''}
|
|
237
234
|
aria-current={node.route === currentRoute ? 'page' : undefined}
|
|
238
|
-
|
|
235
|
+
onclick={(e) => handleLinkClick(e, node.route)}
|
|
239
236
|
>
|
|
240
237
|
{node.label}
|
|
241
238
|
</a>
|
|
@@ -250,7 +247,7 @@
|
|
|
250
247
|
href={node.route}
|
|
251
248
|
class={node.path === normalizedCurrent ? 'active' : ''}
|
|
252
249
|
aria-current={node.path === normalizedCurrent ? 'page' : undefined}
|
|
253
|
-
|
|
250
|
+
onclick={(e) => handleLinkClick(e, node.route)}
|
|
254
251
|
>
|
|
255
252
|
<span>{node.label}</span>
|
|
256
253
|
</a>
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
<script>
|
|
2
|
+
let {error} = $props()
|
|
3
|
+
</script>
|
|
4
|
+
|
|
5
|
+
<h1>Error loading page</h1>
|
|
6
|
+
<p class="message">{error.message}</p>
|
|
7
|
+
{#if error.frame}<pre>{error.frame}</pre>{/if}
|
|
8
|
+
{#if error.file}<p class="file">{error.file}</p>{/if}
|
|
9
|
+
|
|
10
|
+
<style>
|
|
11
|
+
h1 { margin-top: 0; }
|
|
12
|
+
.message { color: var(--red-700); }
|
|
13
|
+
pre {
|
|
14
|
+
background: var(--grey-100);
|
|
15
|
+
border: 1px solid var(--grey-200);
|
|
16
|
+
border-radius: 4px;
|
|
17
|
+
padding: 1rem;
|
|
18
|
+
overflow-x: auto;
|
|
19
|
+
font-size: 0.875rem;
|
|
20
|
+
line-height: 1.6;
|
|
21
|
+
}
|
|
22
|
+
.file { color: var(--grey-500); font-size: 0.875rem; }
|
|
23
|
+
</style>
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
// WebSocket connection for the `graphene check` command.
|
|
2
|
+
// Listens for check requests, waits for queries to finish, captures screenshots, and reports errors.
|
|
3
|
+
|
|
4
|
+
import {getErrors} from './telemetry.ts'
|
|
5
|
+
import {isLoading} from './queryEngine.ts'
|
|
6
|
+
|
|
7
|
+
let socket: WebSocket | null = null
|
|
8
|
+
connect()
|
|
9
|
+
|
|
10
|
+
function captureChart (chartTitle: string) {
|
|
11
|
+
let escaped = window.CSS.escape(chartTitle)
|
|
12
|
+
let canvas = document.querySelector(`[data-chart-title="${escaped}"] canvas`) as HTMLCanvasElement | null
|
|
13
|
+
return canvas?.toDataURL('image/png')
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
async function takeScreenshot () {
|
|
17
|
+
if (!(window as any).html2canvas) {
|
|
18
|
+
let html2canvas = await import('@graphenedata/html2canvas')
|
|
19
|
+
;(window as any).html2canvas = html2canvas.default
|
|
20
|
+
}
|
|
21
|
+
let canvas = await (window as any).html2canvas(document.body, {useCORS: true, allowTaint: true, scale: 1, liveDOM: true})
|
|
22
|
+
return canvas?.toDataURL('image/png')
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
async function waitForQueriesToFinish () {
|
|
26
|
+
let startTime = Date.now()
|
|
27
|
+
while (isLoading() && Date.now() - startTime < 20_000) {
|
|
28
|
+
await new Promise(resolve => setTimeout(resolve, 100))
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function connect () {
|
|
33
|
+
let wsUrl = `ws://${window.location.host}/_api/ws`
|
|
34
|
+
socket = new WebSocket(wsUrl)
|
|
35
|
+
socket.onclose = () => setTimeout(connect, 2000)
|
|
36
|
+
socket.onopen = () => socket!.send(JSON.stringify({type: 'register', url: window.location.href}))
|
|
37
|
+
|
|
38
|
+
socket.onmessage = async (event) => {
|
|
39
|
+
let {type, requestId, chart} = JSON.parse(event.data)
|
|
40
|
+
if (type !== 'check') return
|
|
41
|
+
|
|
42
|
+
await waitForQueriesToFinish()
|
|
43
|
+
let errors = getErrors().map((e: any) => ({type: e.type, message: e.message, id: e.id, file: e.file, line: e.loc?.line, frame: e.frame, from: e.from, to: e.to}))
|
|
44
|
+
let stillLoading = isLoading()
|
|
45
|
+
let screenshot = chart ? captureChart(chart) : await takeScreenshot()
|
|
46
|
+
socket!.send(JSON.stringify({type: 'checkResponse', requestId, errors, stillLoading, screenshot}))
|
|
47
|
+
}
|
|
48
|
+
}
|
|
@@ -30,6 +30,7 @@ interface QueryNode {
|
|
|
30
30
|
let runPending: Promise<void> | null = null
|
|
31
31
|
let params = {} as Record<string, any>
|
|
32
32
|
let queries = [] as QueryNode[]
|
|
33
|
+
let queryResults = {} as Record<string, {rows: any[], fields?: Field[]}>
|
|
33
34
|
|
|
34
35
|
function registerQuery (name: string, contents: string) {
|
|
35
36
|
queries = queries.filter(q => q.name !== name)
|
|
@@ -87,11 +88,16 @@ async function runNode (n: QueryNode) {
|
|
|
87
88
|
|
|
88
89
|
if (response.status == 304) { // cache hit. Read it out and use that
|
|
89
90
|
let body = await cacheRead(hash)
|
|
90
|
-
|
|
91
|
+
let result = translateData(body, n)
|
|
92
|
+
if (n.source) queryResults[n.source] = {rows: result.rows, fields: body.fields}
|
|
93
|
+
n.callback(result)
|
|
91
94
|
} else if (response.ok) { // cache miss. write it to the cache, and return the data
|
|
92
95
|
cacheWrite(hash, response.clone()) // clone allows us to write the raw response into the cache
|
|
93
96
|
let body = await response.json()
|
|
94
|
-
|
|
97
|
+
let fields = body.fields // grab before translateData mutates
|
|
98
|
+
let result = translateData(body, n) // nb that translateData modifies in place for performance
|
|
99
|
+
if (n.source) queryResults[n.source] = {rows: result.rows, fields}
|
|
100
|
+
n.callback(result)
|
|
95
101
|
} else { // request failed. Record it
|
|
96
102
|
let isJson = response.headers.get('Content-Type') === 'application/json'
|
|
97
103
|
let body = isJson ? await response.json() : await response.text()
|
|
@@ -192,4 +198,5 @@ Object.assign(window.$GRAPHENE, {
|
|
|
192
198
|
query,
|
|
193
199
|
unsubscribe,
|
|
194
200
|
waitForQueries,
|
|
201
|
+
queryResults,
|
|
195
202
|
})
|
|
@@ -7,6 +7,7 @@ let staticErrors: Error[] = []
|
|
|
7
7
|
let errorProviders: Record<string, ErrorProvider> = {}
|
|
8
8
|
|
|
9
9
|
window.addEventListener('error', (event) => {
|
|
10
|
+
if ((event.error?.message || '').match(/Failed to fetch dynamically imported module.*\.md\?import/)) return
|
|
10
11
|
staticErrors.push(event.error)
|
|
11
12
|
})
|
|
12
13
|
window.addEventListener('unhandledrejection', (event) => {
|
package/dist/ui/web.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
import
|
|
1
|
+
import './internal/telemetry.ts'
|
|
2
|
+
import './internal/checkSocket.ts'
|
|
2
3
|
import './app.css'
|
|
3
|
-
import {
|
|
4
|
-
import
|
|
4
|
+
import {mount} from 'svelte'
|
|
5
|
+
import LocalApp from './internal/LocalApp.svelte'
|
|
5
6
|
|
|
6
7
|
import Area from './components/Area.svelte'
|
|
7
8
|
import AreaChart from './components/AreaChart.svelte'
|
|
@@ -67,55 +68,4 @@ window.$GRAPHENE.components = {
|
|
|
67
68
|
TextInput,
|
|
68
69
|
}
|
|
69
70
|
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
if (document.getElementById('nav')) {
|
|
73
|
-
new NavSidebar({target: document.getElementById('nav')})
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
connectWebSocket()
|
|
77
|
-
|
|
78
|
-
function captureChart (chartTitle) {
|
|
79
|
-
let escaped = window.CSS.escape(chartTitle)
|
|
80
|
-
let canvas = document.querySelector(`[data-chart-title="${escaped}"] canvas`)
|
|
81
|
-
return canvas?.toDataURL('image/png')
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
async function takeScreenshot () {
|
|
85
|
-
if (!window.html2canvas) {
|
|
86
|
-
let html2canvas = await import('@graphenedata/html2canvas')
|
|
87
|
-
window.html2canvas = html2canvas.default
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
let canvas = await window.html2canvas(document.body, {useCORS: true, allowTaint: true, scale: 1, liveDOM: true})
|
|
91
|
-
return canvas?.toDataURL('image/png')
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
async function waitForQueriesToFinish () {
|
|
95
|
-
let startTime = Date.now()
|
|
96
|
-
while (isLoading() && Date.now() - startTime < 20_000) {
|
|
97
|
-
await new Promise(resolve => setTimeout(resolve, 100))
|
|
98
|
-
}
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
function connectWebSocket () {
|
|
102
|
-
let wsUrl = `ws://${window.location.host}/_api/ws`
|
|
103
|
-
socket = new WebSocket(wsUrl)
|
|
104
|
-
socket.onclose = () => setTimeout(connectWebSocket, 2000)
|
|
105
|
-
|
|
106
|
-
socket.onopen = () => {
|
|
107
|
-
socket.send(JSON.stringify({type: 'register', url: window.location.href}))
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
socket.onmessage = async (event) => {
|
|
111
|
-
let {type, requestId, chart} = JSON.parse(event.data)
|
|
112
|
-
|
|
113
|
-
if (type === 'check') {
|
|
114
|
-
await waitForQueriesToFinish()
|
|
115
|
-
let errors = getErrors().map(e => ({message: e.message, id: e.id}))
|
|
116
|
-
let stillLoading = isLoading()
|
|
117
|
-
let screenshot = chart ? captureChart(chart) : await takeScreenshot()
|
|
118
|
-
socket.send(JSON.stringify({type: 'checkResponse', requestId, errors, stillLoading, screenshot}))
|
|
119
|
-
}
|
|
120
|
-
}
|
|
121
|
-
}
|
|
71
|
+
mount(LocalApp, {target: document.body})
|
package/package.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@graphenedata/cli",
|
|
3
|
-
"main": "
|
|
3
|
+
"main": "bin.js",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"author": "Graphene Systems Inc",
|
|
6
|
-
"version": "0.0.
|
|
6
|
+
"version": "0.0.14",
|
|
7
7
|
"license": "Elastic-2.0",
|
|
8
8
|
"engines": {
|
|
9
|
-
"node": ">=
|
|
9
|
+
"node": ">=20"
|
|
10
10
|
},
|
|
11
11
|
"files": [
|
|
12
12
|
"dist",
|
|
@@ -27,10 +27,9 @@
|
|
|
27
27
|
"@duckdb/node-api": "1.3.2-alpha.26",
|
|
28
28
|
"@google-cloud/bigquery": "^8.1.1",
|
|
29
29
|
"@graphenedata/html2canvas": "^1.4.1",
|
|
30
|
-
"@graphenedata/malloy": "0.0.304",
|
|
31
30
|
"@lezer/common": "^1.2.3",
|
|
32
31
|
"@lezer/lr": "^1.4.2",
|
|
33
|
-
"@sveltejs/vite-plugin-svelte": "
|
|
32
|
+
"@sveltejs/vite-plugin-svelte": "^6.2.4",
|
|
34
33
|
"@tidyjs/tidy": "^2.5.2",
|
|
35
34
|
"chalk": "^5.3.0",
|
|
36
35
|
"chokidar": "3.6.0",
|
|
@@ -41,16 +40,16 @@
|
|
|
41
40
|
"dotenv": "^17.2.3",
|
|
42
41
|
"echarts": "^5.5.0",
|
|
43
42
|
"fs-extra": "11.2.0",
|
|
44
|
-
"glob": "^
|
|
43
|
+
"glob": "^13.0.1",
|
|
45
44
|
"marked": "^16.3.0",
|
|
46
45
|
"mdsvex": "^0.12.6",
|
|
47
46
|
"nanoid": "3.3.8",
|
|
48
47
|
"sanitize-html": "^2.17.0",
|
|
49
|
-
"snowflake-sdk": "^2.3.
|
|
48
|
+
"snowflake-sdk": "^2.3.4",
|
|
50
49
|
"ssf": "^0.11.2",
|
|
51
|
-
"svelte": "
|
|
50
|
+
"svelte": "5.48.0",
|
|
52
51
|
"unist-util-visit": "4.1.2",
|
|
53
|
-
"vite": "
|
|
52
|
+
"vite": "7.3.1",
|
|
54
53
|
"ws": "^8.18.0"
|
|
55
54
|
},
|
|
56
55
|
"devDependencies": {
|
|
@@ -58,7 +57,7 @@
|
|
|
58
57
|
"@types/node": "^20.0.0",
|
|
59
58
|
"@types/sanitize-html": "^2.16.0",
|
|
60
59
|
"@types/ws": "^8.18.1",
|
|
61
|
-
"esbuild": "^0.
|
|
60
|
+
"esbuild": "^0.27.2",
|
|
62
61
|
"vitest": "4.0.15",
|
|
63
62
|
"vscode-languageserver-types": "^3.17.0"
|
|
64
63
|
},
|
package/cli.ts
DELETED
|
@@ -1,156 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
import {Command} from 'commander'
|
|
4
|
-
import {printDiagnostics, printTable} from './printer.ts'
|
|
5
|
-
import {analyze, getDiagnostics, loadWorkspace, toSql, type Query} from '../lang/core.ts'
|
|
6
|
-
import fs from 'fs-extra'
|
|
7
|
-
import path from 'path'
|
|
8
|
-
import {fileURLToPath} from 'url'
|
|
9
|
-
import dotenv from 'dotenv'
|
|
10
|
-
import {config, loadConfig} from '../lang/config.ts'
|
|
11
|
-
import {runServeInBackground, stopGrapheneIfRunning} from './background.ts'
|
|
12
|
-
import {check} from './check.ts'
|
|
13
|
-
import {getConnection, runQuery} from './connections/index.ts'
|
|
14
|
-
import {loginPkce} from './auth.ts'
|
|
15
|
-
|
|
16
|
-
dotenv.config({quiet: true})
|
|
17
|
-
const program = new Command()
|
|
18
|
-
const __dirname = path.dirname(fileURLToPath(import.meta.url))
|
|
19
|
-
// in dev: cli/cli.ts -> cli/package.json. in dist: cli/dist/cli/cli.js -> cli/package.json
|
|
20
|
-
const pkgPath = fs.existsSync(path.join(__dirname, 'package.json')) ? path.join(__dirname, 'package.json') : path.join(__dirname, '../../package.json')
|
|
21
|
-
const pkg = fs.readJsonSync(pkgPath)
|
|
22
|
-
|
|
23
|
-
program.name('graphene').description('Graphene CLI').version(pkg.version, '-v, --version')
|
|
24
|
-
|
|
25
|
-
program.hook('preAction', async () => {
|
|
26
|
-
if (process.env.CLI_DELAY) { // useful if you want to attach a debugger
|
|
27
|
-
await new Promise(r => setTimeout(r, 1000))
|
|
28
|
-
}
|
|
29
|
-
loadConfig(process.cwd())
|
|
30
|
-
})
|
|
31
|
-
|
|
32
|
-
program.command('compile')
|
|
33
|
-
.description('Translate a query to SQL and print it')
|
|
34
|
-
.argument('[input]', 'Path to file, a raw string, or "-" for stdin')
|
|
35
|
-
.action(async (input: string | undefined) => {
|
|
36
|
-
await loadWorkspace(process.cwd(), false)
|
|
37
|
-
let sql = await readInput(input)
|
|
38
|
-
let queries = analyze(sql)
|
|
39
|
-
if (!validQuery(queries)) return
|
|
40
|
-
console.log(toSql(queries[0]))
|
|
41
|
-
})
|
|
42
|
-
|
|
43
|
-
program.command('run')
|
|
44
|
-
.description('Run a query against your database')
|
|
45
|
-
.argument('[input]', 'Path to file, a raw string, or "-" for stdin')
|
|
46
|
-
.action(async (input: string | undefined) => {
|
|
47
|
-
await loadWorkspace(process.cwd(), false)
|
|
48
|
-
let gsql = await readInput(input)
|
|
49
|
-
let queries = analyze(gsql)
|
|
50
|
-
if (!validQuery(queries)) return
|
|
51
|
-
let sql = toSql(queries[0])
|
|
52
|
-
let res = await runQuery(sql)
|
|
53
|
-
printTable(res.rows)
|
|
54
|
-
})
|
|
55
|
-
|
|
56
|
-
program.command('schema')
|
|
57
|
-
.description('Inspect database tables or describe a table')
|
|
58
|
-
.argument('[schema | table]', 'Optional schema or table name to describe')
|
|
59
|
-
.action(async (tableArg: string) => {
|
|
60
|
-
let connection = await getConnection()
|
|
61
|
-
let datasets = await connection.listDatasets()
|
|
62
|
-
|
|
63
|
-
// if there's no arg and more than one dataset, just list the datasets
|
|
64
|
-
if (!tableArg && datasets.length > 1) {
|
|
65
|
-
return console.log(`Datasets available:\n${datasets.join('\n')}`)
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
// figure out if you're wanting to list tables in a schema/dataset
|
|
69
|
-
let dsToList: string | null = null
|
|
70
|
-
let parts = tableArg ? tableArg.split('.') : []
|
|
71
|
-
if (datasets.includes(tableArg)) dsToList = tableArg // you gave the name of a dataset
|
|
72
|
-
else if (!tableArg && datasets.length == 1) dsToList = datasets[0] // only one dataset, and no args
|
|
73
|
-
else if (!tableArg && config.namespace) dsToList = config.namespace // default namespace configured
|
|
74
|
-
else if (!tableArg && config.dialect == 'duckdb') dsToList = '<default>'
|
|
75
|
-
else if (tableArg && config.dialect == 'snowflake' && parts.length == 2) dsToList = tableArg
|
|
76
|
-
|
|
77
|
-
if (dsToList) {
|
|
78
|
-
let tables = await connection.listTables(dsToList)
|
|
79
|
-
return console.log(`Tables${dsToList ? ` in ${dsToList}` : ''}:\n${tables.join('\n')}`)
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
// otherwise, assume you're wanting to see tables
|
|
83
|
-
let cols = await connection.describeTable(tableArg)
|
|
84
|
-
if (!cols.length) return console.log(`Table ${tableArg} not found`)
|
|
85
|
-
console.log(`table ${tableArg} (`)
|
|
86
|
-
cols.forEach(col => console.log(` ${col.name} ${col.dataType}`))
|
|
87
|
-
console.log(')')
|
|
88
|
-
})
|
|
89
|
-
|
|
90
|
-
program.command('serve')
|
|
91
|
-
.description('Run the local server')
|
|
92
|
-
.option('--bg', 'Run the server in the background')
|
|
93
|
-
.action(async (options: {bg?: boolean}) => {
|
|
94
|
-
await stopGrapheneIfRunning()
|
|
95
|
-
if (options.bg) {
|
|
96
|
-
await runServeInBackground()
|
|
97
|
-
process.exit(0)
|
|
98
|
-
} else {
|
|
99
|
-
let mod = await import('./serve2.ts') // load dynamically, so we're not pulling in a bunch of deps we might not need
|
|
100
|
-
await mod.serve2()
|
|
101
|
-
}
|
|
102
|
-
})
|
|
103
|
-
|
|
104
|
-
program.command('stop')
|
|
105
|
-
.description('Stop the local server')
|
|
106
|
-
.action(async () => { await stopGrapheneIfRunning() })
|
|
107
|
-
|
|
108
|
-
program.command('check')
|
|
109
|
-
.description('Check the project for errors, optionally capturing a page screenshot')
|
|
110
|
-
.argument('[mdFile]', 'Markdown file to check (e.g., index.md)')
|
|
111
|
-
.option('-c, --chart <chartTitle>', 'Title of a specific chart to capture')
|
|
112
|
-
.action(async (mdArg: string | undefined, options: {chart?: string}) => {
|
|
113
|
-
let res = await check({mdArg, chart: options.chart})
|
|
114
|
-
process.exit(res ? 0 : 1) // import to call `exit`, bc if we started the server in the background, just returning won't actually exit the process.
|
|
115
|
-
})
|
|
116
|
-
|
|
117
|
-
program.command('login')
|
|
118
|
-
.description('Log in to Graphene Cloud')
|
|
119
|
-
.action(async () => {
|
|
120
|
-
await loginPkce()
|
|
121
|
-
console.log('Successfully logged in')
|
|
122
|
-
process.exit(0)
|
|
123
|
-
})
|
|
124
|
-
|
|
125
|
-
program.parse(process.argv)
|
|
126
|
-
|
|
127
|
-
async function readInput (arg): Promise<string> {
|
|
128
|
-
if (!arg || arg === '-') {
|
|
129
|
-
return await new Promise<string>((resolve) => {
|
|
130
|
-
let data = ''
|
|
131
|
-
process.stdin.setEncoding('utf-8')
|
|
132
|
-
process.stdin.on('data', (chunk) => (data += chunk))
|
|
133
|
-
process.stdin.on('end', () => resolve(data))
|
|
134
|
-
process.stdin.resume()
|
|
135
|
-
})
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
let absolutePath = path.resolve(arg)
|
|
139
|
-
if (fs.existsSync(absolutePath)) {
|
|
140
|
-
return await fs.promises.readFile(absolutePath, 'utf-8')
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
return arg
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
function validQuery (queries: Query[]): boolean {
|
|
147
|
-
if (getDiagnostics().length) {
|
|
148
|
-
printDiagnostics(getDiagnostics())
|
|
149
|
-
process.exit(1)
|
|
150
|
-
}
|
|
151
|
-
if (queries.length == 0) {
|
|
152
|
-
console.warn('No queries found')
|
|
153
|
-
process.exit(1)
|
|
154
|
-
}
|
|
155
|
-
return true
|
|
156
|
-
}
|