@antelopejs/dms-database 0.1.2 → 0.2.1
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/README.md +73 -49
- package/dist/db/models/diagram_layouts.model.d.ts +1 -0
- package/dist/db/models/diagram_notes.model.d.ts +1 -0
- package/dist/db/models/query_history.model.d.ts +1 -0
- package/dist/db/models/saved_queries.model.d.ts +1 -0
- package/dist/routes/data.d.ts +1 -3
- package/dist/routes/data.js +6 -1
- package/dist/routes/data.js.map +1 -1
- package/frontend-vue/app/build/composables/useDataBrowserGrid.ts +4 -38
- package/frontend-vue/app/build/composables/useDatabaseHealth.ts +7 -4
- package/frontend-vue/app/build/composables/useDatabaseSchemas.ts +1 -2
- package/frontend-vue/app/build/composables/useDiagramPersistence.ts +49 -16
- package/frontend-vue/app/build/composables/useQueryStore.ts +59 -27
- package/frontend-vue/app/build/composables/useTableCounts.ts +50 -0
- package/frontend-vue/app/build/data/stagedWrites.ts +46 -6
- package/frontend-vue/app/build/data/useStagedSave.ts +42 -12
- package/frontend-vue/app/build/query/runs.ts +7 -27
- package/frontend-vue/app/build/schemas/useSchemasListing.ts +10 -33
- package/frontend-vue/app/components/ConnectionStatus.vue +14 -11
- package/frontend-vue/app/components/DataBrowserSidebar.vue +20 -42
- package/frontend-vue/app/components/DataBrowserTabBar.vue +1 -1
- package/frontend-vue/app/components/DataGrid.vue +73 -91
- package/frontend-vue/app/components/InstancePicker.vue +63 -55
- package/frontend-vue/app/components/QueryConsole.vue +47 -37
- package/frontend-vue/app/components/QueryLibrary.vue +20 -17
- package/frontend-vue/app/components/RowDrawer.vue +15 -18
- package/frontend-vue/app/components/SchemaDiagram.vue +7 -7
- package/frontend-vue/app/components/TableInspector.vue +116 -84
- package/frontend-vue/package.json +4 -3
- package/frontend-vue/pnpm-lock.yaml +1388 -2803
- package/frontend-vue/tests/dataBrowser.test.ts +59 -1
- package/frontend-vue/tests/fixtures/inertia.ts +8 -0
- package/frontend-vue/tests/fixtures/serverRender.ts +219 -0
- package/frontend-vue/tests/fixtures/vueI18n.ts +4 -0
- package/frontend-vue/tests/helpers.test.ts +1 -0
- package/frontend-vue/tests/locales.test.ts +24 -0
- package/frontend-vue/tests/onDemandReads.test.ts +106 -0
- package/frontend-vue/tests/serverRender.test.ts +235 -0
- package/frontend-vue/vitest.config.ts +24 -0
- package/package.json +31 -14
- package/skills/dms-database/SKILL.md +35 -0
|
@@ -263,13 +263,10 @@ async function load(id: string) {
|
|
|
263
263
|
await leaving
|
|
264
264
|
if (sync !== owner) return
|
|
265
265
|
try {
|
|
266
|
-
const
|
|
267
|
-
persistence.fetchLayout(id),
|
|
268
|
-
persistence.fetchNotes(id),
|
|
269
|
-
])
|
|
266
|
+
const stored = await persistence.readDiagram(id)
|
|
270
267
|
if (sync !== owner) return
|
|
271
|
-
state.positions = positions
|
|
272
|
-
state.notes = notes.map(noteFromStored)
|
|
268
|
+
state.positions = stored.positions
|
|
269
|
+
state.notes = stored.notes.map(noteFromStored)
|
|
273
270
|
} catch {
|
|
274
271
|
if (sync !== owner) return
|
|
275
272
|
state.positions = {}
|
|
@@ -288,7 +285,10 @@ async function load(id: string) {
|
|
|
288
285
|
loaded.value = true
|
|
289
286
|
}
|
|
290
287
|
|
|
291
|
-
|
|
288
|
+
// The canvas is drawn in the browser, from a working copy of the layout and
|
|
289
|
+
// notes it reads there: a server render, which would read them without
|
|
290
|
+
// handing them over, never starts it.
|
|
291
|
+
onMounted(() => watch(schemaId, (id) => id && load(id), { immediate: true }))
|
|
292
292
|
|
|
293
293
|
function record(op: DiagramOperation) {
|
|
294
294
|
history.push(op)
|
|
@@ -68,7 +68,6 @@ const BROWSE_LIST = '/api/database/browse/list'
|
|
|
68
68
|
const BROWSE_COUNT = '/api/database/browse/count'
|
|
69
69
|
|
|
70
70
|
const { t, n } = useI18n()
|
|
71
|
-
const { $authFetch } = useAuthFetch()
|
|
72
71
|
const { copy } = useClipboard()
|
|
73
72
|
const toast = useToast()
|
|
74
73
|
|
|
@@ -222,26 +221,23 @@ async function follow(target: TableRow) {
|
|
|
222
221
|
}
|
|
223
222
|
|
|
224
223
|
// --- rows: in every instance at once; the data browser shows each one's ---
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
}
|
|
243
|
-
|
|
244
|
-
watch(tableKey, loadTotal, { immediate: true })
|
|
224
|
+
// Read with the table, on the server too when the link opens the drawer; no
|
|
225
|
+
// figure shows while another table's loads, nor once it failed.
|
|
226
|
+
const rowTotal = useApiData<{ total: number }>(
|
|
227
|
+
() => (schemaId.value && tableName.value ? BROWSE_COUNT : null),
|
|
228
|
+
{
|
|
229
|
+
query: () => ({
|
|
230
|
+
filter_schema: `is:${schemaId.value}`,
|
|
231
|
+
filter_table: `is:${tableName.value}`,
|
|
232
|
+
filter_instance: `is:${CROSS_INSTANCE_VALUE}`,
|
|
233
|
+
}),
|
|
234
|
+
},
|
|
235
|
+
)
|
|
236
|
+
const total = computed(() =>
|
|
237
|
+
rowTotal.status.value === 'success'
|
|
238
|
+
? (rowTotal.data.value?.total ?? null)
|
|
239
|
+
: null,
|
|
240
|
+
)
|
|
245
241
|
|
|
246
242
|
const rowsFact = computed(() => {
|
|
247
243
|
const rows = total.value
|
|
@@ -283,20 +279,94 @@ const tabs = computed(() => [
|
|
|
283
279
|
])
|
|
284
280
|
|
|
285
281
|
// --- sample row: the latest row, or the next one on demand ---
|
|
286
|
-
const sample = ref<Record<string, unknown> | null>(null)
|
|
287
|
-
const sampleOffset = ref(0)
|
|
288
|
-
const sampleTotal = ref(0)
|
|
289
|
-
const sampleLoading = ref(false)
|
|
290
|
-
const sampleFailed = ref(false)
|
|
291
|
-
|
|
292
282
|
interface SamplePage {
|
|
293
283
|
results: Record<string, unknown>[]
|
|
294
284
|
total: number
|
|
295
285
|
}
|
|
296
286
|
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
287
|
+
/**
|
|
288
|
+
* The row asked for: its offset in an instance. No instance at offset 0 reads
|
|
289
|
+
* the default one, or the first named instance holding rows when the default
|
|
290
|
+
* one holds none.
|
|
291
|
+
*/
|
|
292
|
+
interface SampleRequest {
|
|
293
|
+
instance: string | null
|
|
294
|
+
offset: number
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
/** A sample read, with the table and the instance it was read from. */
|
|
298
|
+
interface SampleRead {
|
|
299
|
+
table: string
|
|
300
|
+
instance: string | null
|
|
301
|
+
page: SamplePage
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
const requested = shallowRef<SampleRequest>({ instance: null, offset: 0 })
|
|
305
|
+
|
|
306
|
+
type ReadSample = (instance: string | null) => Promise<SamplePage>
|
|
307
|
+
|
|
308
|
+
async function firstFilledInstance(
|
|
309
|
+
read: ReadSample,
|
|
310
|
+
): Promise<{ instance: string | null; page: SamplePage }> {
|
|
311
|
+
const page = await read(null)
|
|
312
|
+
if (page.total > 0) return { instance: null, page }
|
|
313
|
+
for (const instance of schema.value?.instances ?? []) {
|
|
314
|
+
const named = await read(instance)
|
|
315
|
+
if (named.total > 0) return { instance, page: named }
|
|
316
|
+
}
|
|
317
|
+
return { instance: null, page }
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
// Read once the Sample tab shows, in the browser only.
|
|
321
|
+
const sampleRead = useApiData<SampleRead>(
|
|
322
|
+
() =>
|
|
323
|
+
schemaId.value && tableName.value
|
|
324
|
+
? `dms-database:sample:${JSON.stringify([
|
|
325
|
+
tableKey.value,
|
|
326
|
+
requested.value.instance,
|
|
327
|
+
requested.value.offset,
|
|
328
|
+
])}`
|
|
329
|
+
: null,
|
|
330
|
+
async ({ $authFetch, signal }) => {
|
|
331
|
+
const table = tableKey.value
|
|
332
|
+
const { instance, offset } = requested.value
|
|
333
|
+
const read: ReadSample = (from) =>
|
|
334
|
+
$authFetch<SamplePage>(BROWSE_LIST, {
|
|
335
|
+
query: {
|
|
336
|
+
filter_schema: `is:${schemaId.value}`,
|
|
337
|
+
filter_table: `is:${tableName.value}`,
|
|
338
|
+
offset,
|
|
339
|
+
limit: 1,
|
|
340
|
+
...(from ? { filter_instance: `is:${from}` } : {}),
|
|
341
|
+
},
|
|
342
|
+
signal,
|
|
343
|
+
})
|
|
344
|
+
const found =
|
|
345
|
+
instance === null && offset === 0
|
|
346
|
+
? await firstFilledInstance(read)
|
|
347
|
+
: { instance, page: await read(instance) }
|
|
348
|
+
return { table, ...found }
|
|
349
|
+
},
|
|
350
|
+
{
|
|
351
|
+
immediate: false,
|
|
352
|
+
server: false,
|
|
353
|
+
watch: false,
|
|
354
|
+
refreshOnPageRefresh: false,
|
|
355
|
+
},
|
|
356
|
+
)
|
|
357
|
+
|
|
358
|
+
// Another table (a relation followed) shows none of the previous one's row.
|
|
359
|
+
const sampleShown = computed(() => {
|
|
360
|
+
const value = sampleRead.data.value
|
|
361
|
+
return value?.table === tableKey.value ? value : null
|
|
362
|
+
})
|
|
363
|
+
const sample = computed(() => sampleShown.value?.page.results?.[0] ?? null)
|
|
364
|
+
const sampleTotal = computed(() => sampleShown.value?.page.total ?? 0)
|
|
365
|
+
const sampleOffset = computed(() => requested.value.offset)
|
|
366
|
+
const sampleLoading = computed(() => sampleRead.pending.value)
|
|
367
|
+
const sampleFailed = computed(() => sampleRead.status.value === 'error')
|
|
368
|
+
// The instance the sample is read from.
|
|
369
|
+
const sampleInstance = computed(() => sampleShown.value?.instance ?? null)
|
|
300
370
|
|
|
301
371
|
const sampleInstanceLabel = computed(() =>
|
|
302
372
|
sampleInstance.value === null
|
|
@@ -306,72 +376,34 @@ const sampleInstanceLabel = computed(() =>
|
|
|
306
376
|
}),
|
|
307
377
|
)
|
|
308
378
|
|
|
309
|
-
function
|
|
310
|
-
|
|
311
|
-
filter_schema: `is:${schemaId.value}`,
|
|
312
|
-
filter_table: `is:${tableName.value}`,
|
|
313
|
-
offset: sampleOffset.value,
|
|
314
|
-
limit: 1,
|
|
315
|
-
}
|
|
316
|
-
if (instance) query.filter_instance = `is:${instance}`
|
|
317
|
-
return $authFetch<SamplePage>(BROWSE_LIST, { query })
|
|
318
|
-
}
|
|
319
|
-
|
|
320
|
-
async function firstFilledInstance(): Promise<SamplePage> {
|
|
321
|
-
const page = await fetchSample(null)
|
|
322
|
-
if (page.total > 0) return page
|
|
323
|
-
for (const instance of schema.value?.instances ?? []) {
|
|
324
|
-
const named = await fetchSample(instance)
|
|
325
|
-
if (named.total > 0) {
|
|
326
|
-
sampleInstance.value = instance
|
|
327
|
-
return named
|
|
328
|
-
}
|
|
329
|
-
}
|
|
330
|
-
return page
|
|
331
|
-
}
|
|
332
|
-
|
|
333
|
-
async function loadSample() {
|
|
334
|
-
if (!schemaId.value || !tableName.value) return
|
|
335
|
-
sampleLoading.value = true
|
|
336
|
-
sampleFailed.value = false
|
|
337
|
-
try {
|
|
338
|
-
const res =
|
|
339
|
-
sampleInstance.value === null && sampleOffset.value === 0
|
|
340
|
-
? await firstFilledInstance()
|
|
341
|
-
: await fetchSample(sampleInstance.value)
|
|
342
|
-
sample.value = res.results?.[0] ?? null
|
|
343
|
-
sampleTotal.value = res.total ?? 0
|
|
344
|
-
} catch {
|
|
345
|
-
sampleFailed.value = true
|
|
346
|
-
sample.value = null
|
|
347
|
-
} finally {
|
|
348
|
-
sampleLoading.value = false
|
|
349
|
-
}
|
|
379
|
+
function loadSample() {
|
|
380
|
+
if (schemaId.value && tableName.value) void sampleRead.execute()
|
|
350
381
|
}
|
|
351
382
|
|
|
352
383
|
function nextSample() {
|
|
353
|
-
|
|
354
|
-
|
|
384
|
+
const total = sampleTotal.value
|
|
385
|
+
requested.value = {
|
|
386
|
+
instance: sampleInstance.value,
|
|
387
|
+
offset: total > 0 ? (requested.value.offset + 1) % total : 0,
|
|
388
|
+
}
|
|
355
389
|
loadSample()
|
|
356
390
|
}
|
|
357
391
|
|
|
358
392
|
// Another table (a relation followed) starts over from its latest row.
|
|
359
393
|
watch(tableKey, () => {
|
|
360
|
-
|
|
361
|
-
sampleOffset.value = 0
|
|
362
|
-
sampleTotal.value = 0
|
|
363
|
-
sampleInstance.value = null
|
|
364
|
-
sampleFailed.value = false
|
|
394
|
+
requested.value = { instance: null, offset: 0 }
|
|
365
395
|
if (tab.value === 'sample' && !loading.value) loadSample()
|
|
366
396
|
})
|
|
367
397
|
|
|
368
398
|
// The sample waits for the schemas: they name the instances to look in.
|
|
369
|
-
|
|
370
|
-
(
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
399
|
+
onMounted(() =>
|
|
400
|
+
watch(
|
|
401
|
+
() => tab.value === 'sample' && !loading.value,
|
|
402
|
+
(ready) => {
|
|
403
|
+
if (ready && !sample.value && !sampleLoading.value) loadSample()
|
|
404
|
+
},
|
|
405
|
+
{ immediate: true },
|
|
406
|
+
),
|
|
375
407
|
)
|
|
376
408
|
|
|
377
409
|
// The `_instance` tag the store adds to rows of named instances is not a
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
"main": "./dms.frontend.ts",
|
|
7
7
|
"packageManager": "pnpm@10.6.5",
|
|
8
8
|
"engines": {
|
|
9
|
-
"@antelopejs/dms-frontend": ">=0.
|
|
9
|
+
"@antelopejs/dms-frontend": ">=0.6.3 <0.7.0"
|
|
10
10
|
},
|
|
11
11
|
"scripts": {
|
|
12
12
|
"dev": "pnpm --dir ../playground frontend:dev",
|
|
@@ -18,8 +18,8 @@
|
|
|
18
18
|
"test": "vitest run"
|
|
19
19
|
},
|
|
20
20
|
"dependencies": {
|
|
21
|
-
"@antelopejs/interface-core": ">=0.
|
|
22
|
-
"@antelopejs/interface-database": ">=0.1.
|
|
21
|
+
"@antelopejs/interface-core": ">=0.2.4 <1.0.0",
|
|
22
|
+
"@antelopejs/interface-database": ">=0.1.10 <1.0.0",
|
|
23
23
|
"@codemirror/autocomplete": "^6.18.6",
|
|
24
24
|
"@codemirror/commands": "^6.8.1",
|
|
25
25
|
"@codemirror/lang-javascript": "^6.2.4",
|
|
@@ -36,6 +36,7 @@
|
|
|
36
36
|
"@intlify/message-compiler": "^11",
|
|
37
37
|
"@nuxt/ui": "4.10.0",
|
|
38
38
|
"@typescript-eslint/parser": "^8.0.0",
|
|
39
|
+
"@vitejs/plugin-vue": "^6.0.10",
|
|
39
40
|
"eslint": "^9.0.0",
|
|
40
41
|
"eslint-plugin-vue": "^10.0.0",
|
|
41
42
|
"prettier": "^3.5.3",
|