@antelopejs/dms-database 0.1.1 → 0.2.0

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.
Files changed (36) hide show
  1. package/README.md +1 -1
  2. package/dist/pages/schemas.js +2 -1
  3. package/dist/pages/schemas.js.map +1 -1
  4. package/dist/routes/tables.d.ts +1 -0
  5. package/dist/routes/tables.js +35 -10
  6. package/dist/routes/tables.js.map +1 -1
  7. package/frontend-vue/app/build/composables/useDataBrowserGrid.ts +4 -38
  8. package/frontend-vue/app/build/composables/useDatabaseHealth.ts +7 -4
  9. package/frontend-vue/app/build/composables/useDatabaseSchemas.ts +1 -2
  10. package/frontend-vue/app/build/composables/useDiagramPersistence.ts +49 -16
  11. package/frontend-vue/app/build/composables/useQueryStore.ts +59 -27
  12. package/frontend-vue/app/build/composables/useTableCounts.ts +50 -0
  13. package/frontend-vue/app/build/query/runs.ts +7 -27
  14. package/frontend-vue/app/build/schemas/useSchemasListing.ts +10 -33
  15. package/frontend-vue/app/components/ConnectionStatus.vue +14 -11
  16. package/frontend-vue/app/components/DataBrowserSidebar.vue +20 -42
  17. package/frontend-vue/app/components/DataBrowserTabBar.vue +1 -1
  18. package/frontend-vue/app/components/DataGrid.vue +43 -89
  19. package/frontend-vue/app/components/InstancePicker.vue +63 -55
  20. package/frontend-vue/app/components/QueryConsole.vue +47 -37
  21. package/frontend-vue/app/components/QueryLibrary.vue +20 -17
  22. package/frontend-vue/app/components/RowDrawer.vue +15 -18
  23. package/frontend-vue/app/components/SchemaDiagram.vue +7 -7
  24. package/frontend-vue/app/components/TableInspector.vue +134 -88
  25. package/frontend-vue/package.json +2 -1
  26. package/frontend-vue/pnpm-lock.yaml +883 -2587
  27. package/frontend-vue/tests/fixtures/inertia.ts +8 -0
  28. package/frontend-vue/tests/fixtures/serverRender.ts +219 -0
  29. package/frontend-vue/tests/fixtures/vueI18n.ts +4 -0
  30. package/frontend-vue/tests/helpers.test.ts +1 -0
  31. package/frontend-vue/tests/locales.test.ts +24 -0
  32. package/frontend-vue/tests/onDemandReads.test.ts +106 -0
  33. package/frontend-vue/tests/registration.test.ts +0 -12
  34. package/frontend-vue/tests/serverRender.test.ts +235 -0
  35. package/frontend-vue/vitest.config.ts +24 -0
  36. package/package.json +6 -5
@@ -1,6 +1,5 @@
1
1
  <script setup lang="ts">
2
2
  import {
3
- dayLabel,
4
3
  groupByDay,
5
4
  HISTORY_KEPT,
6
5
  RUN_DOT_CLASSES,
@@ -38,7 +37,10 @@ const emit = defineEmits<{
38
37
  'clear-history': []
39
38
  }>()
40
39
 
41
- const { t, n, locale } = useI18n()
40
+ const { t, n } = useI18n()
41
+ // Times and days in the user's time zone, which a server render writes in
42
+ // the browser's: the HTML shows what the browser shows.
43
+ const { dateTimeFormat } = useRegionalFormat()
42
44
 
43
45
  const tab = ref<LibraryTab>('history')
44
46
  const search = ref('')
@@ -76,15 +78,18 @@ const historyGroups = computed(() =>
76
78
  matches(entry.source, entry.error, target),
77
79
  ),
78
80
  ({ entry }) => entry.executedAt,
79
- ).map((group) => ({
80
- ...group,
81
- label: dayLabel(group.date, locale.value, {
82
- today: t('dms_database.query.library.today'),
83
- yesterday: t('dms_database.query.library.yesterday'),
84
- }),
85
- })),
81
+ regionalDayKey,
82
+ ).map((group) => ({ ...group, label: dayLabel(group.date) })),
86
83
  )
87
84
 
85
+ /** "Today", "Yesterday" or the date. */
86
+ function dayLabel(date: Date): string {
87
+ const daysAgo = regionalDayNumber(new Date()) - regionalDayNumber(date)
88
+ if (daysAgo === 0) return t('dms_database.query.library.today')
89
+ if (daysAgo === 1) return t('dms_database.query.library.yesterday')
90
+ return dateTimeFormat({ day: 'numeric', month: 'short' }).format(date)
91
+ }
92
+
88
93
  const savedItems = computed(() =>
89
94
  (tab.value === 'team' ? props.team : props.saved).filter((query) =>
90
95
  matches(query.name, query.description, query.source, query.ownerName),
@@ -92,10 +97,9 @@ const savedItems = computed(() =>
92
97
  )
93
98
 
94
99
  function time(iso: string): string {
95
- return new Intl.DateTimeFormat(locale.value, {
96
- hour: '2-digit',
97
- minute: '2-digit',
98
- }).format(new Date(iso))
100
+ return dateTimeFormat({ hour: '2-digit', minute: '2-digit' }).format(
101
+ new Date(iso),
102
+ )
99
103
  }
100
104
 
101
105
  function runMeta(entry: HistoryEntry): string {
@@ -116,10 +120,9 @@ function runMeta(entry: HistoryEntry): string {
116
120
  }
117
121
 
118
122
  function savedMeta(query: SavedQuery): string {
119
- const date = new Intl.DateTimeFormat(locale.value, {
120
- day: 'numeric',
121
- month: 'short',
122
- }).format(new Date(query.updatedAt ?? query.createdAt))
123
+ const date = dateTimeFormat({ day: 'numeric', month: 'short' }).format(
124
+ new Date(query.updatedAt ?? query.createdAt),
125
+ )
123
126
  return query.ownerName ? `${query.ownerName} · ${date}` : date
124
127
  }
125
128
 
@@ -58,7 +58,6 @@ const props = defineProps<{
58
58
  const emit = defineEmits<{ success: [] }>()
59
59
 
60
60
  const { t, n } = useI18n()
61
- const { $authFetch } = useAuthFetch()
62
61
  const staged = useStagedEdits()
63
62
 
64
63
  const active = ref<DrawerTab>('fields')
@@ -169,23 +168,21 @@ function setBoolean(field: string, value: boolean) {
169
168
  }
170
169
 
171
170
  // --- referenced by ---
172
- const references = ref<RowReference[] | null>(null)
173
- const referencesFailed = ref(false)
174
-
175
- async function loadReferences() {
176
- if (!props.rowId || props.readOnly) return
177
- referencesFailed.value = false
178
- try {
179
- const res = await $authFetch<{ items: RowReference[] }>(
180
- '/api/database/browse/references',
181
- {
182
- query: { ...browseSelectionQuery(props.tab), id: props.rowId },
183
- },
184
- )
185
- references.value = res.items
186
- } catch {
187
- referencesFailed.value = true
188
- }
171
+ // Read once the drawer opens, in the browser.
172
+ const referencesRead = useApiData<{ items: RowReference[] }>(
173
+ () =>
174
+ props.rowId && !props.readOnly ? '/api/database/browse/references' : null,
175
+ {
176
+ query: () => ({ ...browseSelectionQuery(props.tab), id: props.rowId }),
177
+ immediate: false,
178
+ server: false,
179
+ },
180
+ )
181
+ const references = computed(() => referencesRead.data.value?.items ?? null)
182
+ const referencesFailed = computed(() => referencesRead.status.value === 'error')
183
+
184
+ function loadReferences() {
185
+ void referencesRead.execute()
189
186
  }
190
187
 
191
188
  onMounted(loadReferences)
@@ -263,13 +263,10 @@ async function load(id: string) {
263
263
  await leaving
264
264
  if (sync !== owner) return
265
265
  try {
266
- const [positions, notes] = await Promise.all([
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
- watch(schemaId, (id) => id && load(id), { immediate: true })
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)
@@ -31,7 +31,8 @@ import { ALL_INSTANCES_PARAM, tableLink } from '../build/utils/databaseLinks'
31
31
  // The Schemas page's row drawer (D-07), also opened from the diagram: the
32
32
  // facts of one table, its columns, indexes and relations both ways, and a
33
33
  // sample row. The table view hands it the row and the means to step to the
34
- // next one (J / K); a relation opens the table it names in its place.
34
+ // next one (J / K), or to show another row in place; a relation opens the
35
+ // table it names in its place.
35
36
 
36
37
  interface TableRow {
37
38
  schema: string
@@ -50,6 +51,7 @@ interface RowNavigation {
50
51
  const props = defineProps<{
51
52
  rowData?: TableRow
52
53
  navigation?: RowNavigation
54
+ showRow?: (row: object | string) => Promise<void>
53
55
  }>()
54
56
 
55
57
  // A link out closes the drawer.
@@ -66,7 +68,6 @@ const BROWSE_LIST = '/api/database/browse/list'
66
68
  const BROWSE_COUNT = '/api/database/browse/count'
67
69
 
68
70
  const { t, n } = useI18n()
69
- const { $authFetch } = useAuthFetch()
70
71
  const { copy } = useClipboard()
71
72
  const toast = useToast()
72
73
 
@@ -100,7 +101,8 @@ const failed = computed(
100
101
  () => schemasSettled.value && schemas.value === undefined,
101
102
  )
102
103
 
103
- // The table a relation was followed to, in place of the row's.
104
+ // The table a relation was followed to, in place of the row's, where the
105
+ // container cannot show another row (the diagram's drawer).
104
106
  const followed = ref<TableRow | null>(null)
105
107
  const shown = computed(() => followed.value ?? props.rowData)
106
108
  // J / K step from the row the table view opened: its position is not the
@@ -199,8 +201,19 @@ const incoming = computed(() =>
199
201
  ),
200
202
  )
201
203
 
202
- // The row's own table again closes the detour.
203
- function follow(target: TableRow) {
204
+ // The table view shows the relation's table as its own row: the URL, the
205
+ // position and J / K follow it. Elsewhere, or should it fail to read the
206
+ // row, the inspector shows it itself, and the row's own table again closes
207
+ // the detour.
208
+ async function follow(target: TableRow) {
209
+ if (props.showRow) {
210
+ try {
211
+ await props.showRow(`${target.schema}::${target.name}`)
212
+ return
213
+ } catch {
214
+ // Shown in place below.
215
+ }
216
+ }
204
217
  const own =
205
218
  target.schema === props.rowData?.schema &&
206
219
  target.name === props.rowData?.name
@@ -208,26 +221,23 @@ function follow(target: TableRow) {
208
221
  }
209
222
 
210
223
  // --- rows: in every instance at once; the data browser shows each one's ---
211
- const total = ref<number | null>(null)
212
-
213
- async function loadTotal(key: string) {
214
- total.value = null
215
- if (!schemaId.value || !tableName.value) return
216
- try {
217
- const result = await $authFetch<{ total: number }>(BROWSE_COUNT, {
218
- query: {
219
- filter_schema: `is:${schemaId.value}`,
220
- filter_table: `is:${tableName.value}`,
221
- filter_instance: `is:${CROSS_INSTANCE_VALUE}`,
222
- },
223
- })
224
- if (key === tableKey.value) total.value = result.total
225
- } catch {
226
- // The total the list gave, if any, stays.
227
- }
228
- }
229
-
230
- 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
+ )
231
241
 
232
242
  const rowsFact = computed(() => {
233
243
  const rows = total.value
@@ -269,20 +279,94 @@ const tabs = computed(() => [
269
279
  ])
270
280
 
271
281
  // --- sample row: the latest row, or the next one on demand ---
272
- const sample = ref<Record<string, unknown> | null>(null)
273
- const sampleOffset = ref(0)
274
- const sampleTotal = ref(0)
275
- const sampleLoading = ref(false)
276
- const sampleFailed = ref(false)
277
-
278
282
  interface SamplePage {
279
283
  results: Record<string, unknown>[]
280
284
  total: number
281
285
  }
282
286
 
283
- // The instance the sample is read from: the default one, or the first named
284
- // instance holding rows when the default one holds none.
285
- const sampleInstance = ref<string | null>(null)
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)
286
370
 
287
371
  const sampleInstanceLabel = computed(() =>
288
372
  sampleInstance.value === null
@@ -292,72 +376,34 @@ const sampleInstanceLabel = computed(() =>
292
376
  }),
293
377
  )
294
378
 
295
- function fetchSample(instance: string | null) {
296
- const query: Record<string, string | number> = {
297
- filter_schema: `is:${schemaId.value}`,
298
- filter_table: `is:${tableName.value}`,
299
- offset: sampleOffset.value,
300
- limit: 1,
301
- }
302
- if (instance) query.filter_instance = `is:${instance}`
303
- return $authFetch<SamplePage>(BROWSE_LIST, { query })
304
- }
305
-
306
- async function firstFilledInstance(): Promise<SamplePage> {
307
- const page = await fetchSample(null)
308
- if (page.total > 0) return page
309
- for (const instance of schema.value?.instances ?? []) {
310
- const named = await fetchSample(instance)
311
- if (named.total > 0) {
312
- sampleInstance.value = instance
313
- return named
314
- }
315
- }
316
- return page
317
- }
318
-
319
- async function loadSample() {
320
- if (!schemaId.value || !tableName.value) return
321
- sampleLoading.value = true
322
- sampleFailed.value = false
323
- try {
324
- const res =
325
- sampleInstance.value === null && sampleOffset.value === 0
326
- ? await firstFilledInstance()
327
- : await fetchSample(sampleInstance.value)
328
- sample.value = res.results?.[0] ?? null
329
- sampleTotal.value = res.total ?? 0
330
- } catch {
331
- sampleFailed.value = true
332
- sample.value = null
333
- } finally {
334
- sampleLoading.value = false
335
- }
379
+ function loadSample() {
380
+ if (schemaId.value && tableName.value) void sampleRead.execute()
336
381
  }
337
382
 
338
383
  function nextSample() {
339
- sampleOffset.value =
340
- sampleTotal.value > 0 ? (sampleOffset.value + 1) % sampleTotal.value : 0
384
+ const total = sampleTotal.value
385
+ requested.value = {
386
+ instance: sampleInstance.value,
387
+ offset: total > 0 ? (requested.value.offset + 1) % total : 0,
388
+ }
341
389
  loadSample()
342
390
  }
343
391
 
344
392
  // Another table (a relation followed) starts over from its latest row.
345
393
  watch(tableKey, () => {
346
- sample.value = null
347
- sampleOffset.value = 0
348
- sampleTotal.value = 0
349
- sampleInstance.value = null
350
- sampleFailed.value = false
394
+ requested.value = { instance: null, offset: 0 }
351
395
  if (tab.value === 'sample' && !loading.value) loadSample()
352
396
  })
353
397
 
354
398
  // The sample waits for the schemas: they name the instances to look in.
355
- watch(
356
- () => tab.value === 'sample' && !loading.value,
357
- (ready) => {
358
- if (ready && !sample.value && !sampleLoading.value) loadSample()
359
- },
360
- { immediate: true },
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
+ ),
361
407
  )
362
408
 
363
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.5.2 <0.6.0"
9
+ "@antelopejs/dms-frontend": ">=0.6.0 <0.7.0"
10
10
  },
11
11
  "scripts": {
12
12
  "dev": "pnpm --dir ../playground frontend:dev",
@@ -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",