@antelopejs/dms-database 0.0.13 → 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.
- package/README.md +30 -5
- package/dist/db/models/query_history.model.d.ts +1 -0
- package/dist/db/models/query_history.model.js +6 -0
- package/dist/db/models/query_history.model.js.map +1 -1
- package/dist/db/tables/query_history.table.d.ts +3 -0
- package/dist/db/tables/query_history.table.js +12 -0
- package/dist/db/tables/query_history.table.js.map +1 -1
- package/dist/db/tables/saved_queries.table.d.ts +1 -0
- package/dist/db/tables/saved_queries.table.js +4 -0
- package/dist/db/tables/saved_queries.table.js.map +1 -1
- package/dist/i18n/messages.d.ts +39 -0
- package/dist/i18n/messages.js +64 -0
- package/dist/i18n/messages.js.map +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -7
- package/dist/index.js.map +1 -1
- package/dist/module.d.ts +11 -0
- package/dist/module.js +55 -0
- package/dist/module.js.map +1 -0
- package/dist/pages/data.d.ts +1 -1
- package/dist/pages/data.js +9 -6
- package/dist/pages/data.js.map +1 -1
- package/dist/pages/diagram.d.ts +9 -0
- package/dist/pages/diagram.js +45 -0
- package/dist/pages/diagram.js.map +1 -0
- package/dist/pages/index.d.ts +1 -0
- package/dist/pages/index.js +1 -0
- package/dist/pages/index.js.map +1 -1
- package/dist/pages/overview.d.ts +4 -1
- package/dist/pages/overview.js +113 -6
- package/dist/pages/overview.js.map +1 -1
- package/dist/pages/query.d.ts +1 -1
- package/dist/pages/query.js +22 -6
- package/dist/pages/query.js.map +1 -1
- package/dist/pages/schemas.d.ts +2 -1
- package/dist/pages/schemas.js +143 -6
- package/dist/pages/schemas.js.map +1 -1
- package/dist/routes/data.d.ts +8 -0
- package/dist/routes/data.js +84 -21
- package/dist/routes/data.js.map +1 -1
- package/dist/routes/index.d.ts +1 -0
- package/dist/routes/index.js +1 -0
- package/dist/routes/index.js.map +1 -1
- package/dist/routes/overview.d.ts +12 -0
- package/dist/routes/overview.js +55 -0
- package/dist/routes/overview.js.map +1 -0
- package/dist/routes/query.d.ts +5 -1
- package/dist/routes/query.js +162 -27
- package/dist/routes/query.js.map +1 -1
- package/dist/routes/tables.d.ts +15 -12
- package/dist/routes/tables.js +95 -55
- package/dist/routes/tables.js.map +1 -1
- package/dist/service/catalog.d.ts +3 -0
- package/dist/service/catalog.js +26 -0
- package/dist/service/catalog.js.map +1 -0
- package/dist/service/health.d.ts +3 -3
- package/dist/service/health.js +17 -14
- package/dist/service/health.js.map +1 -1
- package/dist/service/introspect.d.ts +3 -1
- package/dist/service/introspect.js +131 -43
- package/dist/service/introspect.js.map +1 -1
- package/dist/service/overview.d.ts +9 -0
- package/dist/service/overview.js +113 -0
- package/dist/service/overview.js.map +1 -0
- package/dist/service/queryInspection.d.ts +15 -0
- package/dist/service/queryInspection.js +107 -0
- package/dist/service/queryInspection.js.map +1 -0
- package/dist/service/references.d.ts +9 -0
- package/dist/service/references.js +52 -0
- package/dist/service/references.js.map +1 -0
- package/dist/service/schemaFilters.d.ts +26 -0
- package/dist/service/schemaFilters.js +42 -0
- package/dist/service/schemaFilters.js.map +1 -0
- package/dist/service/stagedSerialization.d.ts +2 -0
- package/dist/service/stagedSerialization.js +38 -10
- package/dist/service/stagedSerialization.js.map +1 -1
- package/dist/service/types.d.ts +11 -1
- package/dist/types/constants.d.ts +2 -0
- package/dist/types/constants.js +3 -1
- package/dist/types/constants.js.map +1 -1
- package/dist/utils/query.d.ts +0 -2
- package/dist/utils/query.js +0 -20
- package/dist/utils/query.js.map +1 -1
- package/frontend-vue/app/{composables → build/composables}/useDataBrowserGrid.ts +65 -50
- package/frontend-vue/app/build/composables/useDataBrowserTabs.ts +366 -0
- package/frontend-vue/app/build/composables/useDatabaseHealth.ts +50 -0
- package/frontend-vue/app/build/composables/useDatabaseSchemas.ts +140 -0
- package/frontend-vue/app/{composables → build/composables}/useDiagramPersistence.ts +47 -39
- package/frontend-vue/app/build/composables/useQueryRuntime.ts +217 -0
- package/frontend-vue/app/build/composables/useQueryStore.ts +162 -0
- package/frontend-vue/app/build/data/cellValues.ts +132 -0
- package/frontend-vue/app/build/data/instanceOptions.ts +36 -0
- package/frontend-vue/app/build/data/pickerMenus.ts +175 -0
- package/frontend-vue/app/build/data/recentTables.ts +48 -0
- package/frontend-vue/app/build/data/stagedEdits.ts +117 -0
- package/frontend-vue/app/build/data/stagedWrites.ts +64 -0
- package/frontend-vue/app/build/data/useStagedSave.ts +298 -0
- package/frontend-vue/app/build/diagram/autosave.ts +72 -0
- package/frontend-vue/app/build/diagram/graph.ts +585 -0
- package/frontend-vue/app/build/diagram/history.ts +178 -0
- package/frontend-vue/app/build/diagram/shortcuts.ts +62 -0
- package/frontend-vue/app/build/diagram/sync.ts +137 -0
- package/frontend-vue/app/build/query/completion.ts +100 -0
- package/frontend-vue/app/build/query/csv.ts +32 -0
- package/frontend-vue/app/build/query/examples.ts +38 -0
- package/frontend-vue/app/build/query/runs.ts +168 -0
- package/frontend-vue/app/build/query/tableColumns.ts +35 -0
- package/frontend-vue/app/build/query/target.ts +143 -0
- package/frontend-vue/app/build/schemas/filterState.ts +128 -0
- package/frontend-vue/app/build/schemas/useSchemasListing.ts +62 -0
- package/frontend-vue/app/build/utils/databaseLinks.ts +98 -0
- package/frontend-vue/app/build/utils/fieldTypes.ts +198 -0
- package/frontend-vue/app/build/utils/relativeTime.ts +19 -0
- package/frontend-vue/app/{utils → build/utils}/stagedSerialization.ts +7 -1
- package/frontend-vue/app/components/ConnectionStatus.vue +122 -0
- package/frontend-vue/app/components/DataBrowser.vue +126 -0
- package/frontend-vue/app/components/DataBrowserSidebar.vue +293 -210
- package/frontend-vue/app/components/DataBrowserTabBar.vue +185 -93
- package/frontend-vue/app/components/DataFilterBar.vue +163 -0
- package/frontend-vue/app/components/DataGrid.vue +737 -499
- package/frontend-vue/app/components/DataGridCell.vue +125 -77
- package/frontend-vue/app/components/DataGridHeaderCell.vue +119 -81
- package/frontend-vue/app/components/DiagramNoteNode.vue +81 -63
- package/frontend-vue/app/components/DiagramRelationEdge.vue +57 -27
- package/frontend-vue/app/components/DiagramStubNode.vue +16 -17
- package/frontend-vue/app/components/DiagramTableNode.vue +106 -68
- package/frontend-vue/app/components/InstancePicker.vue +195 -0
- package/frontend-vue/app/components/JsonCellPanel.vue +30 -29
- package/frontend-vue/app/components/QueryBuilderPanel.vue +290 -238
- package/frontend-vue/app/components/QueryConsole.vue +726 -0
- package/frontend-vue/app/components/QueryLibrary.vue +314 -0
- package/frontend-vue/app/components/QueryResultPanel.vue +556 -126
- package/frontend-vue/app/components/RowDrawer.vue +520 -0
- package/frontend-vue/app/components/SchemaDiagram.vue +1016 -1051
- package/frontend-vue/app/components/SchemaPicker.vue +86 -0
- package/frontend-vue/app/components/SchemasFilterBar.vue +201 -0
- package/frontend-vue/app/components/TableInspector.vue +741 -0
- package/frontend-vue/app/config/shortcuts-registry.ts +65 -0
- package/frontend-vue/dms.frontend.ts +5 -14
- package/frontend-vue/i18n/locales/database-en-GB.json +526 -193
- package/frontend-vue/i18n/locales/database-fr-FR.json +528 -195
- package/frontend-vue/package.json +2 -1
- package/frontend-vue/pnpm-lock.yaml +2601 -858
- package/frontend-vue/pnpm-workspace.yaml +1 -1
- package/frontend-vue/tests/dataBrowser.test.ts +206 -0
- package/frontend-vue/tests/diagram.test.ts +516 -0
- package/frontend-vue/tests/helpers.test.ts +205 -0
- package/frontend-vue/tests/instanceOptions.test.ts +53 -0
- package/frontend-vue/tests/locales.test.ts +56 -0
- package/frontend-vue/tests/persistence.test.ts +106 -5
- package/frontend-vue/tests/pickerMenus.test.ts +254 -0
- package/frontend-vue/tests/queryConsole.test.ts +282 -0
- package/frontend-vue/tests/queryRuntime.test.ts +2 -2
- package/frontend-vue/tests/registration.test.ts +36 -25
- package/frontend-vue/tests/schemasFilters.test.ts +115 -0
- package/package.json +9 -6
- package/frontend-vue/app/components/QueryFavoritesPanel.vue +0 -127
- package/frontend-vue/app/components/QueryHistoryPanel.vue +0 -134
- package/frontend-vue/app/components/SchemaTableInspector.vue +0 -278
- package/frontend-vue/app/composables/useDataBrowserTabs.ts +0 -255
- package/frontend-vue/app/composables/useDatabaseHealth.ts +0 -50
- package/frontend-vue/app/composables/useDatabaseSchemas.ts +0 -92
- package/frontend-vue/app/composables/useQueryRuntime.ts +0 -118
- package/frontend-vue/app/composables/useQueryStore.ts +0 -117
- package/frontend-vue/app/composables/useTableViewRowClick.ts +0 -22
- package/frontend-vue/app/custom-pages/database/data.vue +0 -41
- package/frontend-vue/app/custom-pages/database/overview.vue +0 -367
- package/frontend-vue/app/custom-pages/database/query.vue +0 -436
- package/frontend-vue/app/custom-pages/database/schemas.vue +0 -218
- package/frontend-vue/app/utils/databaseFieldModifiers.ts +0 -33
- package/frontend-vue/app/utils/databaseFieldTypeIcons.ts +0 -224
- package/frontend-vue/app/utils/dmsTableView.ts +0 -83
- 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
|
-
|
|
6
|
-
new URL(
|
|
7
|
-
|
|
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
|
-
|
|
16
|
-
expect(
|
|
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
|
-
|
|
21
|
-
expect(
|
|
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('
|
|
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(
|
|
40
|
-
|
|
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
|
-
|
|
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
|
|
3
|
+
"version": "0.1.0",
|
|
4
4
|
"license": "Apache-2.0",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -40,18 +40,21 @@
|
|
|
40
40
|
"release": "pnpm --dir frontend-vue install --frozen-lockfile && release-it",
|
|
41
41
|
"prepack": "pnpm run build",
|
|
42
42
|
"knip": "knip --dependencies",
|
|
43
|
-
"knip:all": "knip"
|
|
43
|
+
"knip:all": "knip",
|
|
44
|
+
"test:frontend": "ajs dms verify-source --layer node_modules/@antelopejs/dms/frontend-vue --module frontend-vue"
|
|
44
45
|
},
|
|
45
46
|
"dependencies": {
|
|
46
|
-
"@antelopejs/interface-api": ">=0.0.
|
|
47
|
+
"@antelopejs/interface-api": ">=0.0.14 <1.0.0",
|
|
47
48
|
"@antelopejs/interface-api-util": ">=0.1.2 <1.0.0",
|
|
48
49
|
"@antelopejs/interface-core": ">=0.1.1 <1.0.0",
|
|
49
50
|
"@antelopejs/interface-database": ">=0.1.8 <1.0.0",
|
|
50
51
|
"@antelopejs/interface-database-decorators": ">=0.1.7 <1.0.0",
|
|
51
|
-
"@antelopejs/interface-dms": ">=0.
|
|
52
|
+
"@antelopejs/interface-dms": ">=0.5.1 <1.0.0"
|
|
52
53
|
},
|
|
53
54
|
"devDependencies": {
|
|
54
|
-
"@antelopejs/core": ">=1.
|
|
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",
|
|
55
58
|
"@antelopejs/tooling-configs": ">=0.0.7 <1.0.0",
|
|
56
59
|
"@types/chai": "^5.2.3",
|
|
57
60
|
"@types/mocha": "^10.0.10",
|
|
@@ -70,7 +73,7 @@
|
|
|
70
73
|
"typescript": "^6.0.3"
|
|
71
74
|
},
|
|
72
75
|
"optionalDependencies": {
|
|
73
|
-
"@antelopejs/interface-dms-automation": ">=0.1.
|
|
76
|
+
"@antelopejs/interface-dms-automation": ">=0.1.1 <1.0.0"
|
|
74
77
|
},
|
|
75
78
|
"packageManager": "pnpm@10.2.0",
|
|
76
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>
|