@antelopejs/dms-database 0.0.14 → 0.1.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 (173) hide show
  1. package/README.md +30 -5
  2. package/dist/db/models/query_history.model.d.ts +1 -0
  3. package/dist/db/models/query_history.model.js +6 -0
  4. package/dist/db/models/query_history.model.js.map +1 -1
  5. package/dist/db/tables/query_history.table.d.ts +3 -0
  6. package/dist/db/tables/query_history.table.js +12 -0
  7. package/dist/db/tables/query_history.table.js.map +1 -1
  8. package/dist/db/tables/saved_queries.table.d.ts +1 -0
  9. package/dist/db/tables/saved_queries.table.js +4 -0
  10. package/dist/db/tables/saved_queries.table.js.map +1 -1
  11. package/dist/i18n/messages.d.ts +39 -0
  12. package/dist/i18n/messages.js +64 -0
  13. package/dist/i18n/messages.js.map +1 -0
  14. package/dist/index.d.ts +1 -0
  15. package/dist/index.js +1 -7
  16. package/dist/index.js.map +1 -1
  17. package/dist/module.d.ts +11 -0
  18. package/dist/module.js +55 -0
  19. package/dist/module.js.map +1 -0
  20. package/dist/pages/data.d.ts +1 -1
  21. package/dist/pages/data.js +9 -6
  22. package/dist/pages/data.js.map +1 -1
  23. package/dist/pages/diagram.d.ts +9 -0
  24. package/dist/pages/diagram.js +45 -0
  25. package/dist/pages/diagram.js.map +1 -0
  26. package/dist/pages/index.d.ts +1 -0
  27. package/dist/pages/index.js +1 -0
  28. package/dist/pages/index.js.map +1 -1
  29. package/dist/pages/overview.d.ts +4 -1
  30. package/dist/pages/overview.js +113 -6
  31. package/dist/pages/overview.js.map +1 -1
  32. package/dist/pages/query.d.ts +1 -1
  33. package/dist/pages/query.js +22 -6
  34. package/dist/pages/query.js.map +1 -1
  35. package/dist/pages/schemas.d.ts +2 -1
  36. package/dist/pages/schemas.js +143 -6
  37. package/dist/pages/schemas.js.map +1 -1
  38. package/dist/routes/data.d.ts +8 -0
  39. package/dist/routes/data.js +84 -21
  40. package/dist/routes/data.js.map +1 -1
  41. package/dist/routes/index.d.ts +1 -0
  42. package/dist/routes/index.js +1 -0
  43. package/dist/routes/index.js.map +1 -1
  44. package/dist/routes/overview.d.ts +12 -0
  45. package/dist/routes/overview.js +55 -0
  46. package/dist/routes/overview.js.map +1 -0
  47. package/dist/routes/query.d.ts +5 -1
  48. package/dist/routes/query.js +162 -27
  49. package/dist/routes/query.js.map +1 -1
  50. package/dist/routes/tables.d.ts +15 -12
  51. package/dist/routes/tables.js +95 -55
  52. package/dist/routes/tables.js.map +1 -1
  53. package/dist/service/catalog.d.ts +3 -0
  54. package/dist/service/catalog.js +26 -0
  55. package/dist/service/catalog.js.map +1 -0
  56. package/dist/service/health.d.ts +3 -3
  57. package/dist/service/health.js +17 -14
  58. package/dist/service/health.js.map +1 -1
  59. package/dist/service/introspect.d.ts +3 -1
  60. package/dist/service/introspect.js +131 -43
  61. package/dist/service/introspect.js.map +1 -1
  62. package/dist/service/overview.d.ts +9 -0
  63. package/dist/service/overview.js +113 -0
  64. package/dist/service/overview.js.map +1 -0
  65. package/dist/service/queryInspection.d.ts +15 -0
  66. package/dist/service/queryInspection.js +107 -0
  67. package/dist/service/queryInspection.js.map +1 -0
  68. package/dist/service/references.d.ts +9 -0
  69. package/dist/service/references.js +52 -0
  70. package/dist/service/references.js.map +1 -0
  71. package/dist/service/schemaFilters.d.ts +26 -0
  72. package/dist/service/schemaFilters.js +42 -0
  73. package/dist/service/schemaFilters.js.map +1 -0
  74. package/dist/service/stagedSerialization.d.ts +2 -0
  75. package/dist/service/stagedSerialization.js +38 -10
  76. package/dist/service/stagedSerialization.js.map +1 -1
  77. package/dist/service/types.d.ts +11 -1
  78. package/dist/types/constants.d.ts +2 -0
  79. package/dist/types/constants.js +3 -1
  80. package/dist/types/constants.js.map +1 -1
  81. package/dist/utils/query.d.ts +0 -2
  82. package/dist/utils/query.js +0 -20
  83. package/dist/utils/query.js.map +1 -1
  84. package/frontend-vue/app/{composables → build/composables}/useDataBrowserGrid.ts +65 -50
  85. package/frontend-vue/app/build/composables/useDataBrowserTabs.ts +366 -0
  86. package/frontend-vue/app/build/composables/useDatabaseHealth.ts +50 -0
  87. package/frontend-vue/app/build/composables/useDatabaseSchemas.ts +140 -0
  88. package/frontend-vue/app/{composables → build/composables}/useDiagramPersistence.ts +47 -39
  89. package/frontend-vue/app/build/composables/useQueryRuntime.ts +217 -0
  90. package/frontend-vue/app/build/composables/useQueryStore.ts +162 -0
  91. package/frontend-vue/app/build/data/cellValues.ts +132 -0
  92. package/frontend-vue/app/build/data/instanceOptions.ts +36 -0
  93. package/frontend-vue/app/build/data/pickerMenus.ts +175 -0
  94. package/frontend-vue/app/build/data/recentTables.ts +48 -0
  95. package/frontend-vue/app/build/data/stagedEdits.ts +117 -0
  96. package/frontend-vue/app/build/data/stagedWrites.ts +64 -0
  97. package/frontend-vue/app/build/data/useStagedSave.ts +298 -0
  98. package/frontend-vue/app/build/diagram/autosave.ts +72 -0
  99. package/frontend-vue/app/build/diagram/graph.ts +585 -0
  100. package/frontend-vue/app/build/diagram/history.ts +178 -0
  101. package/frontend-vue/app/build/diagram/shortcuts.ts +62 -0
  102. package/frontend-vue/app/build/diagram/sync.ts +137 -0
  103. package/frontend-vue/app/build/query/completion.ts +100 -0
  104. package/frontend-vue/app/build/query/csv.ts +32 -0
  105. package/frontend-vue/app/build/query/examples.ts +38 -0
  106. package/frontend-vue/app/build/query/runs.ts +168 -0
  107. package/frontend-vue/app/build/query/tableColumns.ts +35 -0
  108. package/frontend-vue/app/build/query/target.ts +143 -0
  109. package/frontend-vue/app/build/schemas/filterState.ts +128 -0
  110. package/frontend-vue/app/build/schemas/useSchemasListing.ts +62 -0
  111. package/frontend-vue/app/build/utils/databaseLinks.ts +98 -0
  112. package/frontend-vue/app/build/utils/fieldTypes.ts +198 -0
  113. package/frontend-vue/app/build/utils/relativeTime.ts +19 -0
  114. package/frontend-vue/app/{utils → build/utils}/stagedSerialization.ts +7 -1
  115. package/frontend-vue/app/components/ConnectionStatus.vue +122 -0
  116. package/frontend-vue/app/components/DataBrowser.vue +126 -0
  117. package/frontend-vue/app/components/DataBrowserSidebar.vue +293 -210
  118. package/frontend-vue/app/components/DataBrowserTabBar.vue +185 -93
  119. package/frontend-vue/app/components/DataFilterBar.vue +163 -0
  120. package/frontend-vue/app/components/DataGrid.vue +737 -499
  121. package/frontend-vue/app/components/DataGridCell.vue +125 -77
  122. package/frontend-vue/app/components/DataGridHeaderCell.vue +119 -81
  123. package/frontend-vue/app/components/DiagramNoteNode.vue +81 -63
  124. package/frontend-vue/app/components/DiagramRelationEdge.vue +57 -27
  125. package/frontend-vue/app/components/DiagramStubNode.vue +16 -17
  126. package/frontend-vue/app/components/DiagramTableNode.vue +106 -68
  127. package/frontend-vue/app/components/InstancePicker.vue +195 -0
  128. package/frontend-vue/app/components/JsonCellPanel.vue +30 -29
  129. package/frontend-vue/app/components/QueryBuilderPanel.vue +290 -238
  130. package/frontend-vue/app/components/QueryConsole.vue +726 -0
  131. package/frontend-vue/app/components/QueryLibrary.vue +314 -0
  132. package/frontend-vue/app/components/QueryResultPanel.vue +556 -126
  133. package/frontend-vue/app/components/RowDrawer.vue +520 -0
  134. package/frontend-vue/app/components/SchemaDiagram.vue +1016 -1051
  135. package/frontend-vue/app/components/SchemaPicker.vue +86 -0
  136. package/frontend-vue/app/components/SchemasFilterBar.vue +201 -0
  137. package/frontend-vue/app/components/TableInspector.vue +741 -0
  138. package/frontend-vue/app/config/shortcuts-registry.ts +65 -0
  139. package/frontend-vue/dms.frontend.ts +5 -14
  140. package/frontend-vue/i18n/locales/database-en-GB.json +526 -193
  141. package/frontend-vue/i18n/locales/database-fr-FR.json +528 -195
  142. package/frontend-vue/package.json +2 -1
  143. package/frontend-vue/pnpm-lock.yaml +2601 -858
  144. package/frontend-vue/pnpm-workspace.yaml +1 -1
  145. package/frontend-vue/tests/dataBrowser.test.ts +206 -0
  146. package/frontend-vue/tests/diagram.test.ts +516 -0
  147. package/frontend-vue/tests/helpers.test.ts +205 -0
  148. package/frontend-vue/tests/instanceOptions.test.ts +53 -0
  149. package/frontend-vue/tests/locales.test.ts +56 -0
  150. package/frontend-vue/tests/persistence.test.ts +106 -5
  151. package/frontend-vue/tests/pickerMenus.test.ts +254 -0
  152. package/frontend-vue/tests/queryConsole.test.ts +282 -0
  153. package/frontend-vue/tests/queryRuntime.test.ts +2 -2
  154. package/frontend-vue/tests/registration.test.ts +36 -25
  155. package/frontend-vue/tests/schemasFilters.test.ts +115 -0
  156. package/package.json +7 -7
  157. package/frontend-vue/app/components/QueryFavoritesPanel.vue +0 -127
  158. package/frontend-vue/app/components/QueryHistoryPanel.vue +0 -134
  159. package/frontend-vue/app/components/SchemaTableInspector.vue +0 -278
  160. package/frontend-vue/app/composables/useDataBrowserTabs.ts +0 -255
  161. package/frontend-vue/app/composables/useDatabaseHealth.ts +0 -50
  162. package/frontend-vue/app/composables/useDatabaseSchemas.ts +0 -92
  163. package/frontend-vue/app/composables/useQueryRuntime.ts +0 -118
  164. package/frontend-vue/app/composables/useQueryStore.ts +0 -117
  165. package/frontend-vue/app/composables/useTableViewRowClick.ts +0 -22
  166. package/frontend-vue/app/custom-pages/database/data.vue +0 -41
  167. package/frontend-vue/app/custom-pages/database/overview.vue +0 -367
  168. package/frontend-vue/app/custom-pages/database/query.vue +0 -436
  169. package/frontend-vue/app/custom-pages/database/schemas.vue +0 -218
  170. package/frontend-vue/app/utils/databaseFieldModifiers.ts +0 -33
  171. package/frontend-vue/app/utils/databaseFieldTypeIcons.ts +0 -224
  172. package/frontend-vue/app/utils/dmsTableView.ts +0 -83
  173. package/frontend-vue/app/utils/relativeTime.ts +0 -18
@@ -0,0 +1,282 @@
1
+ import { describe, expect, it } from 'vitest'
2
+ import type { SchemaSummary } from '../app/build/composables/useDatabaseSchemas'
3
+ import {
4
+ findSyntaxPlace,
5
+ prepareQueryPayload,
6
+ QueryInputError,
7
+ } from '../app/build/composables/useQueryRuntime'
8
+ import {
9
+ buildCompletionIndex,
10
+ fieldsAt,
11
+ QUERY_METHODS,
12
+ } from '../app/build/query/completion'
13
+ import { latestOrderField } from '../app/build/query/examples'
14
+ import {
15
+ closestName,
16
+ replaceName,
17
+ RUN_DOT_CLASSES,
18
+ unknownName,
19
+ } from '../app/build/query/runs'
20
+ import { filledColumns, numericColumns } from '../app/build/query/tableColumns'
21
+ import {
22
+ instanceArgument,
23
+ rowInstance,
24
+ runTarget,
25
+ targetFromSource,
26
+ withInstance,
27
+ } from '../app/build/query/target'
28
+
29
+ function table(name: string, fields: string[]) {
30
+ return {
31
+ name,
32
+ fields: Object.fromEntries(
33
+ fields.map((field) => [field, { kind: 'string' }]),
34
+ ),
35
+ indexes: {},
36
+ relations: [],
37
+ } as SchemaSummary['tables'][number]
38
+ }
39
+
40
+ const shop: SchemaSummary = {
41
+ id: 'shop',
42
+ options: {},
43
+ tables: [
44
+ table('orders', ['status', 'total']),
45
+ table('products', ['sku', 'price']),
46
+ ],
47
+ stats: { tableCount: 2, elementCount: 0, instanceCount: 2 },
48
+ instances: ['eu', 'us'],
49
+ }
50
+
51
+ describe('unknown names', () => {
52
+ it('reads the schema, instance or table an error names', () => {
53
+ expect(unknownName('Unknown instance "zz" in schema shop')).toEqual({
54
+ kind: 'instance',
55
+ name: 'zz',
56
+ })
57
+ expect(unknownName('Unknown schema "shoop". Did you mean')).toEqual({
58
+ kind: 'schema',
59
+ name: 'shoop',
60
+ })
61
+ expect(unknownName('Unknown table "oders" in schema shop')).toEqual({
62
+ kind: 'table',
63
+ name: 'oders',
64
+ })
65
+ expect(unknownName('Syntax error')).toBeNull()
66
+ })
67
+
68
+ it('suggests the closest instance, as for a table', () => {
69
+ expect(closestName('zz', ['eu', 'us'])).toBe('eu')
70
+ expect(closestName('euu', ['eu', 'us'])).toBe('eu')
71
+ expect(closestName('germany', ['eu', 'us'])).toBeNull()
72
+ expect(closestName('shoop', ['shop', 'demo'])).toBe('shop')
73
+ })
74
+
75
+ it('replaces the mistyped name where the query names it', () => {
76
+ const source = 'schemas.shoop.instance("zz").table(\'oders\').count()'
77
+ expect(replaceName(source, 'schema', 'shoop', 'shop')).toBe(
78
+ 'schemas.shop.instance("zz").table(\'oders\').count()',
79
+ )
80
+ expect(replaceName(source, 'instance', 'zz', 'eu')).toBe(
81
+ 'schemas.shoop.instance("eu").table(\'oders\').count()',
82
+ )
83
+ expect(replaceName(source, 'table', 'oders', 'orders')).toBe(
84
+ 'schemas.shoop.instance("zz").table(\'orders\').count()',
85
+ )
86
+ expect(
87
+ replaceName('schemas["shoop"].instance()', 'schema', 'shoop', 'dms-core'),
88
+ ).toBe('schemas["dms-core"].instance()')
89
+ // A longer schema id sharing the prefix is left alone.
90
+ expect(replaceName('schemas.shoppers', 'schema', 'shop', 'x')).toBe(
91
+ 'schemas.shoppers',
92
+ )
93
+ })
94
+ })
95
+
96
+ describe('query runtime errors', () => {
97
+ it('names an unknown schema', () => {
98
+ let error: unknown
99
+ try {
100
+ prepareQueryPayload('schemas.shoop.instance().table("orders")', [shop])
101
+ } catch (caught) {
102
+ error = caught
103
+ }
104
+ expect(error).toBeInstanceOf(QueryInputError)
105
+ expect((error as QueryInputError).problem).toEqual({
106
+ kind: 'unknown_schema',
107
+ schema: 'shoop',
108
+ })
109
+ expect((error as Error).message).toBe('Unknown schema "shoop"')
110
+ })
111
+
112
+ it('points a syntax error where the text breaks', () => {
113
+ let error: unknown
114
+ try {
115
+ prepareQueryPayload('schemas.shop.instance("eu").table("orders"', [shop])
116
+ } catch (caught) {
117
+ error = caught
118
+ }
119
+ expect(error).toBeInstanceOf(QueryInputError)
120
+ expect((error as QueryInputError).problem).toMatchObject({
121
+ kind: 'syntax',
122
+ place: { line: 1, column: 43 },
123
+ })
124
+ expect((error as Error).message).toMatch(/^Syntax error at line 1/)
125
+ })
126
+
127
+ it('accepts a closing semicolon', () => {
128
+ const prepared = prepareQueryPayload(
129
+ 'schemas.shop.instance().table("orders").count();\n',
130
+ [shop],
131
+ )
132
+ expect(prepared.source).toBe(
133
+ 'schemas.shop.instance().table("orders").count();',
134
+ )
135
+ })
136
+ })
137
+
138
+ describe('syntax places', () => {
139
+ it('finds where the parser stops, with its line and column', () => {
140
+ expect(findSyntaxPlace('a.b("x").c(1)')).toBeNull()
141
+ expect(findSyntaxPlace('a.b("x"\n .c(1))')).toBeNull()
142
+ expect(findSyntaxPlace('a.b(1))')).toEqual({ line: 1, column: 7 })
143
+ // An unterminated string breaks where the text ends.
144
+ expect(findSyntaxPlace('a.b(\n "open)')).toEqual({ line: 2, column: 9 })
145
+ // Brackets in strings and comments do not count.
146
+ expect(findSyntaxPlace('a.b(")") // (\n')).toBeNull()
147
+ })
148
+ })
149
+
150
+ describe('query targets', () => {
151
+ it('reads the chain a query text names', () => {
152
+ expect(
153
+ targetFromSource('schemas.shop.instance("eu").table("orders").count()'),
154
+ ).toEqual({ schema: 'shop', instance: 'eu', table: 'orders' })
155
+ expect(
156
+ targetFromSource(
157
+ 'schemas["dms-core"].instance(CROSS_INSTANCE).table(\'users\')',
158
+ ),
159
+ ).toEqual({ schema: 'dms-core', instance: '*', table: 'users' })
160
+ expect(targetFromSource('schemas.demo.instance().table("tags')).toEqual({
161
+ schema: 'demo',
162
+ instance: '',
163
+ table: undefined,
164
+ })
165
+ expect(targetFromSource('1 + 1')).toEqual({})
166
+ })
167
+
168
+ it('names where a past run pointed, failed ones included', () => {
169
+ expect(
170
+ runTarget({
171
+ query: {
172
+ stages: [
173
+ { stage: 'schema', options: { id: 'shop' } },
174
+ { stage: 'instance', options: { id: 'eu' } },
175
+ { stage: 'table', options: { id: 'orders' } },
176
+ ],
177
+ },
178
+ source: 'whatever',
179
+ }),
180
+ ).toEqual({ schema: 'shop', instance: 'eu', table: 'orders' })
181
+ expect(
182
+ runTarget({ query: {}, source: 'schemas.demo.instance().table("tags"' }),
183
+ ).toEqual({ schema: 'demo', instance: '', table: undefined })
184
+ })
185
+
186
+ it('opens a row of a cross-instance read in its own instance', () => {
187
+ const all = { schema: 'shop', table: 'orders', instance: '*' }
188
+ expect(rowInstance(all, { _id: 'o1', _instance: 'eu' })).toBe('eu')
189
+ expect(rowInstance(all, { _id: 'o1', _instance: null })).toBe('')
190
+ expect(rowInstance(all, { _id: 'o1' })).toBeNull()
191
+ expect(rowInstance({ ...all, instance: 'us' }, { _instance: 'eu' })).toBe(
192
+ 'us',
193
+ )
194
+ expect(rowInstance({ ...all, instance: '' }, {})).toBe('')
195
+ })
196
+
197
+ it('reads the instance a link into the console names', () => {
198
+ expect(instanceArgument(undefined)).toBe('')
199
+ expect(instanceArgument('eu')).toBe('"eu"')
200
+ expect(instanceArgument('all')).toBe('CROSS_INSTANCE')
201
+ // No alias: any other value is a named instance.
202
+ expect(instanceArgument('__CROSS_INSTANCE__')).toBe('"__CROSS_INSTANCE__"')
203
+ })
204
+
205
+ it('reads an empty default-instance read in a named instance', () => {
206
+ expect(
207
+ withInstance(
208
+ 'schemas.shop.instance().table("orders").filter((r) => r.key("x"))',
209
+ 'shop',
210
+ 'eu',
211
+ ),
212
+ ).toBe(
213
+ 'schemas.shop.instance("eu").table("orders").filter((r) => r.key("x"))',
214
+ )
215
+ expect(
216
+ withInstance(
217
+ 'schemas["dms-core"] . instance( ).table("users")',
218
+ 'dms-core',
219
+ 'us',
220
+ ),
221
+ ).toBe('schemas["dms-core"] . instance("us").table("users")')
222
+ // Another schema, or one sharing the prefix, is left alone.
223
+ expect(
224
+ withInstance('schemas.shopper.instance().table("x")', 'shop', 'eu'),
225
+ ).toBe('schemas.shopper.instance().table("x")')
226
+ })
227
+ })
228
+
229
+ describe('completions', () => {
230
+ const index = buildCompletionIndex([shop])
231
+
232
+ it('offers only methods the query language has', () => {
233
+ expect(QUERY_METHODS).not.toContain('limit')
234
+ expect(QUERY_METHODS).toContain('slice')
235
+ })
236
+
237
+ it('offers the fields of the chain table only', () => {
238
+ expect(fieldsAt(index, { schema: 'shop', table: 'orders' })).toEqual([
239
+ 'status',
240
+ 'total',
241
+ ])
242
+ expect(fieldsAt(index, { schema: 'shop' })).toEqual([
243
+ 'status',
244
+ 'total',
245
+ 'sku',
246
+ 'price',
247
+ ])
248
+ expect(fieldsAt(index, { schema: 'shop', table: 'nope' })).toHaveLength(4)
249
+ })
250
+ })
251
+
252
+ describe('result rendering', () => {
253
+ it('leaves out columns null in every row and right-aligns numbers', () => {
254
+ const rows = [
255
+ { _id: 'a', total: 3, _instance: null, note: null },
256
+ { _id: 'b', total: null, _instance: null, note: 'x' },
257
+ ]
258
+ const columns = ['_id', 'total', '_instance', 'note']
259
+ expect(filledColumns(rows, columns)).toEqual(['_id', 'total', 'note'])
260
+ expect(filledColumns([{ a: null }], ['a'])).toEqual(['a'])
261
+ expect(numericColumns(rows, columns)).toEqual(['total'])
262
+ })
263
+
264
+ it('orders "latest rows" by a date only when the table has one', () => {
265
+ expect(
266
+ latestOrderField({
267
+ name: 'orders',
268
+ fields: { status: { kind: 'string' }, created_at: { kind: 'date' } },
269
+ }),
270
+ ).toBe('created_at')
271
+ expect(
272
+ latestOrderField({ name: 'logs', fields: { at: { kind: 'date' } } }),
273
+ ).toBe('at')
274
+ expect(
275
+ latestOrderField({ name: 'tags', fields: { label: { kind: 'string' } } }),
276
+ ).toBeNull()
277
+ })
278
+
279
+ it('tells a run that changed data from a fast one', () => {
280
+ expect(RUN_DOT_CLASSES.changed).not.toBe(RUN_DOT_CLASSES.fast)
281
+ })
282
+ })
@@ -1,6 +1,6 @@
1
1
  import { expect, it } from 'vitest'
2
- import type { SchemaSummary } from '../app/composables/useDatabaseSchemas'
3
- import { prepareQueryPayload } from '../app/composables/useQueryRuntime'
2
+ import type { SchemaSummary } from '../app/build/composables/useDatabaseSchemas'
3
+ import { prepareQueryPayload } from '../app/build/composables/useQueryRuntime'
4
4
 
5
5
  const schema: SchemaSummary = {
6
6
  id: 'parity',
@@ -2,26 +2,23 @@ import { expect, it, vi } from 'vitest'
2
2
  import { readFileSync } from 'node:fs'
3
3
  import frontendModule from '../dms.frontend'
4
4
 
5
- const schemasPage = readFileSync(
6
- new URL('../app/custom-pages/database/schemas.vue', import.meta.url),
7
- 'utf8',
8
- )
9
- const relationEdge = readFileSync(
10
- new URL('../app/components/DiagramRelationEdge.vue', import.meta.url),
11
- 'utf8',
12
- )
5
+ function source(path: string): string {
6
+ return readFileSync(new URL(path, import.meta.url), 'utf8')
7
+ }
13
8
 
14
9
  it('uses the DMS client-only wrapper for the Vue Flow schema diagram', () => {
15
- expect(schemasPage).toContain('<DmsClientOnly>')
16
- expect(schemasPage).not.toContain('<ClientOnly>')
10
+ const diagram = source('../app/components/SchemaDiagram.vue')
11
+ expect(diagram).toContain('<DmsClientOnly')
12
+ expect(diagram).not.toContain('<ClientOnly>')
17
13
  })
18
14
 
19
15
  it('does not route relation labels through Vue Flow EdgeText measurement', () => {
20
- expect(relationEdge).toContain(':label="undefined"')
21
- expect(relationEdge).toContain('<text')
16
+ const edge = source('../app/components/DiagramRelationEdge.vue')
17
+ expect(edge).toContain(':label="undefined"')
18
+ expect(edge).toContain('<text')
22
19
  })
23
20
 
24
- it('preserves all database page keys, component aliases and preloaders', async () => {
21
+ it('registers every component under the module prefix the backend names', async () => {
25
22
  const registerComponent = vi.fn()
26
23
  const registerPage = vi.fn()
27
24
  await frontendModule.setup({
@@ -36,18 +33,32 @@ it('preserves all database page keys, component aliases and preloaders', async (
36
33
  provide: vi.fn(),
37
34
  use: vi.fn(),
38
35
  })
39
- expect(registerPage.mock.calls.map(([name]) => name)).toEqual([
40
- 'database/data',
41
- 'database/overview',
42
- 'database/query',
43
- 'database/schemas',
44
- ])
45
- for (const [name, component, preload] of registerPage.mock.calls) {
46
- const alias = `DmsDatabase${name.split('/')[1].replace(/^./, (character: string) => character.toUpperCase())}`
47
- expect(registerComponent).toHaveBeenCalledWith(alias, component)
48
- expect(preload).toBeTypeOf('function')
49
- }
36
+ expect(frontendModule.componentPrefix).toBe('DmsDatabase')
37
+ expect(registerPage).not.toHaveBeenCalled()
50
38
  const names = registerComponent.mock.calls.map(([name]) => name)
51
- expect(names).toContain('DmsDatabaseDataGrid')
39
+ // The names the backend pages send, without the prefix the SDK adds.
40
+ for (const name of [
41
+ 'ConnectionStatus',
42
+ 'TableInspector',
43
+ 'SchemaDiagram',
44
+ 'DataBrowser',
45
+ 'QueryConsole',
46
+ ]) {
47
+ expect(names).toContain(name)
48
+ }
49
+ expect(names.every((name) => !name.startsWith('Dms'))).toBe(true)
52
50
  expect(new Set(names).size).toBe(names.length)
53
51
  })
52
+
53
+ it('names every custom component the backend pages render', () => {
54
+ const backend = ['overview', 'schemas', 'diagram', 'data', 'query']
55
+ .map((page) => source(`../../src/pages/${page}.ts`))
56
+ .join('\n')
57
+ const named = [
58
+ ...backend.matchAll(/CustomComponent\("DmsDatabase(\w+)"\)/g),
59
+ ].map(([, name]) => name)
60
+ expect(named.length).toBeGreaterThan(0)
61
+ for (const name of named) {
62
+ expect(() => source(`../app/components/${name}.vue`)).not.toThrow()
63
+ }
64
+ })
@@ -0,0 +1,115 @@
1
+ import { describe, expect, it } from 'vitest'
2
+ import {
3
+ EMPTY_FILTER_STATE,
4
+ filterParams,
5
+ isFiltered,
6
+ parseHas,
7
+ readFilterState,
8
+ sourceQuery,
9
+ toggleHas,
10
+ withFilterState,
11
+ withScope,
12
+ } from '../app/build/schemas/filterState'
13
+
14
+ describe('Schemas filter bar state', () => {
15
+ it('reads nothing as no filter, empty values included', () => {
16
+ expect(readFilterState({})).toEqual(EMPTY_FILTER_STATE)
17
+ const empty = { scope: '', instance: '', q: '', has: '' }
18
+ expect(readFilterState(empty)).toEqual(EMPTY_FILTER_STATE)
19
+ expect(isFiltered(readFilterState(empty))).toBe(false)
20
+ })
21
+
22
+ it('reads and writes back every key', () => {
23
+ const query = {
24
+ scope: 'shop',
25
+ instance: 'eu',
26
+ q: ' invoice ',
27
+ has: 'empty,relations',
28
+ }
29
+ const state = readFilterState(query)
30
+ expect(state).toEqual({
31
+ scope: 'shop',
32
+ instance: { kind: 'named', id: 'eu' },
33
+ q: 'invoice',
34
+ has: ['relations', 'empty'],
35
+ })
36
+ expect(filterParams(state)).toEqual({
37
+ scope: 'shop',
38
+ instance: 'eu',
39
+ q: 'invoice',
40
+ has: 'relations,empty',
41
+ })
42
+ expect(isFiltered(state)).toBe(true)
43
+ })
44
+
45
+ it('keeps the other keys and drops unset and empty bar keys', () => {
46
+ const query = {
47
+ record: 'shop::orders',
48
+ scope: 'shop',
49
+ instance: '',
50
+ q: '',
51
+ has: '',
52
+ }
53
+ expect(withFilterState(query, readFilterState(query))).toEqual({
54
+ record: 'shop::orders',
55
+ scope: 'shop',
56
+ })
57
+ expect(
58
+ withFilterState(query, { ...EMPTY_FILTER_STATE, q: 'line' }),
59
+ ).toEqual({ record: 'shop::orders', q: 'line' })
60
+ })
61
+
62
+ it('reads no instance as the default one, and all as every instance', () => {
63
+ expect(readFilterState({}).instance).toEqual({ kind: 'default' })
64
+ expect(readFilterState({ instance: 'all' }).instance).toEqual({
65
+ kind: 'all',
66
+ })
67
+ const named = readFilterState({ instance: 'eu' })
68
+ expect(named.instance).toEqual({ kind: 'named', id: 'eu' })
69
+ expect(filterParams(EMPTY_FILTER_STATE).instance).toBeUndefined()
70
+ expect(
71
+ filterParams({ ...EMPTY_FILTER_STATE, instance: { kind: 'all' } })
72
+ .instance,
73
+ ).toBe('all')
74
+ expect(
75
+ isFiltered({ ...EMPTY_FILTER_STATE, instance: { kind: 'all' } }),
76
+ ).toBe(true)
77
+ })
78
+
79
+ it('keeps known structure flags once, in a fixed order', () => {
80
+ expect(parseHas('empty, bogus,relations,empty')).toEqual([
81
+ 'relations',
82
+ 'empty',
83
+ ])
84
+ let state = toggleHas(EMPTY_FILTER_STATE, 'empty')
85
+ state = toggleHas(state, 'relations')
86
+ expect(state.has).toEqual(['relations', 'empty'])
87
+ expect(toggleHas(state, 'empty').has).toEqual(['relations'])
88
+ })
89
+
90
+ it('drops a named instance with the schema it belongs to', () => {
91
+ const state = {
92
+ ...EMPTY_FILTER_STATE,
93
+ scope: 'shop',
94
+ instance: { kind: 'named' as const, id: 'eu' },
95
+ }
96
+ expect(withScope(state, 'shop').instance).toEqual(state.instance)
97
+ expect(withScope(state, 'blog').instance).toEqual({ kind: 'default' })
98
+ expect(withScope(state, null).instance).toEqual({ kind: 'default' })
99
+ const every = { ...state, instance: { kind: 'all' as const } }
100
+ expect(withScope(every, 'blog').instance).toEqual({ kind: 'all' })
101
+ })
102
+
103
+ it('queries the source as the table view does', () => {
104
+ expect(sourceQuery(EMPTY_FILTER_STATE)).toEqual({})
105
+ expect(
106
+ sourceQuery(
107
+ readFilterState({ scope: 'shop', instance: 'all', has: 'empty' }),
108
+ ),
109
+ ).toEqual({
110
+ filter_scope: 'is:shop',
111
+ filter_instance: 'is:all',
112
+ filter_has: 'is:empty',
113
+ })
114
+ })
115
+ })
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@antelopejs/dms-database",
3
- "version": "0.0.14",
3
+ "version": "0.1.0",
4
4
  "license": "Apache-2.0",
5
5
  "repository": {
6
6
  "type": "git",
@@ -44,17 +44,17 @@
44
44
  "test:frontend": "ajs dms verify-source --layer node_modules/@antelopejs/dms/frontend-vue --module frontend-vue"
45
45
  },
46
46
  "dependencies": {
47
- "@antelopejs/interface-api": ">=0.0.13 <1.0.0",
47
+ "@antelopejs/interface-api": ">=0.0.14 <1.0.0",
48
48
  "@antelopejs/interface-api-util": ">=0.1.2 <1.0.0",
49
49
  "@antelopejs/interface-core": ">=0.1.1 <1.0.0",
50
50
  "@antelopejs/interface-database": ">=0.1.8 <1.0.0",
51
51
  "@antelopejs/interface-database-decorators": ">=0.1.7 <1.0.0",
52
- "@antelopejs/interface-dms": ">=0.2.8 <1.0.0"
52
+ "@antelopejs/interface-dms": ">=0.5.1 <1.0.0"
53
53
  },
54
54
  "devDependencies": {
55
- "@antelopejs/core": ">=1.10.1 <2",
56
- "@antelopejs/dms": ">=0.5.0 <1.0.0",
57
- "@antelopejs/dms-frontend": ">=0.3.6 <0.4.0",
55
+ "@antelopejs/core": ">=1.13.5 <2",
56
+ "@antelopejs/dms": ">=0.7.4 <0.8.0",
57
+ "@antelopejs/dms-frontend": ">=0.5.2 <0.6.0",
58
58
  "@antelopejs/tooling-configs": ">=0.0.7 <1.0.0",
59
59
  "@types/chai": "^5.2.3",
60
60
  "@types/mocha": "^10.0.10",
@@ -73,7 +73,7 @@
73
73
  "typescript": "^6.0.3"
74
74
  },
75
75
  "optionalDependencies": {
76
- "@antelopejs/interface-dms-automation": ">=0.1.0 <1.0.0"
76
+ "@antelopejs/interface-dms-automation": ">=0.1.1 <1.0.0"
77
77
  },
78
78
  "packageManager": "pnpm@10.2.0",
79
79
  "antelopeJs": {
@@ -1,127 +0,0 @@
1
- <script setup lang="ts">
2
- import type { DropdownMenuItem } from "@nuxt/ui";
3
- import type { QueryScope, SavedQuery } from "../composables/useQueryStore";
4
- import { formatDatabaseRelativeTime } from "../utils/relativeTime";
5
-
6
- interface Props {
7
- items: SavedQuery[];
8
- scope: QueryScope;
9
- }
10
-
11
- const props = defineProps<Props>();
12
-
13
- const emit = defineEmits<{
14
- "update:scope": [value: QueryScope];
15
- load: [query: SavedQuery];
16
- edit: [query: SavedQuery];
17
- delete: [id: string];
18
- }>();
19
-
20
- const { t } = useI18n();
21
- const { user } = useUserSession();
22
-
23
- const internalScope = computed<QueryScope>({
24
- get: () => props.scope,
25
- set: (value) => emit("update:scope", value),
26
- });
27
-
28
- const scopeItems = computed(() => [
29
- { label: t("dms_database.query.scopeMe"), value: "me" as const },
30
- { label: t("dms_database.query.scopeShared"), value: "shared" as const },
31
- ]);
32
-
33
- function menuItems(item: SavedQuery): DropdownMenuItem[][] {
34
- const load: DropdownMenuItem = {
35
- label: t("dms_database.query.itemMenu.load"),
36
- icon: "i-ph-arrow-square-out",
37
- onSelect: () => emit("load", item),
38
- };
39
- // The Team tab also lists other users' queries; only the owner may edit
40
- // or delete them (the API enforces the same rule).
41
- if (item.userId !== user.value?._id) return [[load]];
42
- return [
43
- [
44
- load,
45
- {
46
- label: t("dms_database.query.itemMenu.edit"),
47
- icon: "i-ph-pencil-simple",
48
- onSelect: () => emit("edit", item),
49
- },
50
- ],
51
- [
52
- {
53
- label: t("dms_database.query.itemMenu.delete"),
54
- icon: "i-ph-trash",
55
- color: "error",
56
- onSelect: () => emit("delete", item.id),
57
- },
58
- ],
59
- ];
60
- }
61
- </script>
62
-
63
- <template>
64
- <DmsCard :padded="false" class="flex flex-col">
65
- <header class="flex items-center gap-2.5 px-4 pt-4 pb-2.5">
66
- <UIcon name="i-ph-star" class="text-primary shrink-0" />
67
- <h3 class="text-highlighted text-sm font-semibold">
68
- {{ $t("dms_database.query.favorites") }}
69
- </h3>
70
- <DmsSegmented
71
- v-model="internalScope"
72
- :items="scopeItems"
73
- :aria-label="$t('dms_database.query.scopeMe')"
74
- class="ml-auto"
75
- />
76
- </header>
77
-
78
- <ul
79
- v-if="items.length > 0"
80
- class="flex-1 overflow-y-auto px-1.5 pb-2 space-y-0.5"
81
- >
82
- <li
83
- v-for="item in items"
84
- :key="item.id"
85
- class="group flex items-center gap-2.5 rounded-md px-2.5 py-2.5 cursor-pointer hover:bg-elevated transition-colors"
86
- @click="emit('load', item)"
87
- @keydown.enter.prevent="emit('load', item)"
88
- >
89
- <UIcon name="i-ph-star-fill" class="text-primary shrink-0 size-3.5" />
90
- <div class="min-w-0 flex-1">
91
- <div
92
- class="text-toned text-[12.5px] font-semibold truncate flex items-center gap-1.5"
93
- >
94
- <span class="truncate">{{ item.name }}</span>
95
- <UIcon
96
- v-if="item.shared"
97
- name="i-ph-users"
98
- class="text-info shrink-0 size-3.5"
99
- :aria-label="$t('dms_database.query.sharedBadge')"
100
- />
101
- </div>
102
- </div>
103
- <span class="text-dimmed font-mono text-[10.5px] shrink-0">
104
- {{ formatDatabaseRelativeTime(item.createdAt) }}
105
- </span>
106
- <UDropdownMenu :items="menuItems(item)" :content="{ align: 'end' }">
107
- <UButton
108
- icon="i-ph-dots-three-vertical"
109
- variant="ghost"
110
- color="neutral"
111
- size="xs"
112
- class="opacity-0 group-hover:opacity-100 transition-opacity shrink-0"
113
- :aria-label="item.name"
114
- @click.stop
115
- />
116
- </UDropdownMenu>
117
- </li>
118
- </ul>
119
-
120
- <div
121
- v-else
122
- class="flex-1 flex items-center justify-center px-4 py-6 text-xs text-dimmed text-center"
123
- >
124
- {{ $t("dms_database.query.emptyFavorites") }}
125
- </div>
126
- </DmsCard>
127
- </template>