@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,205 @@
|
|
|
1
|
+
import { afterEach, describe, expect, it, vi } from 'vitest'
|
|
2
|
+
import { ref } from 'vue'
|
|
3
|
+
import { parseDraft } from '../app/build/data/cellValues'
|
|
4
|
+
import { undoBodies, useStagedEdits } from '../app/build/data/stagedEdits'
|
|
5
|
+
import { DiagramHistory } from '../app/build/diagram/history'
|
|
6
|
+
import { toCsv } from '../app/build/query/csv'
|
|
7
|
+
import { groupByDay, runKind } from '../app/build/query/runs'
|
|
8
|
+
import { readQueryTarget } from '../app/build/query/target'
|
|
9
|
+
import {
|
|
10
|
+
decodeMatch,
|
|
11
|
+
tableAccess,
|
|
12
|
+
tableLink,
|
|
13
|
+
} from '../app/build/utils/databaseLinks'
|
|
14
|
+
import type { HistoryEntry } from '../app/build/composables/useQueryStore'
|
|
15
|
+
|
|
16
|
+
afterEach(() => vi.unstubAllGlobals())
|
|
17
|
+
|
|
18
|
+
describe('cell drafts', () => {
|
|
19
|
+
it('keeps the stored type of a cell', () => {
|
|
20
|
+
expect(parseDraft('42.5', 1, 'number')).toEqual({
|
|
21
|
+
ok: true,
|
|
22
|
+
value: 42.5,
|
|
23
|
+
unchanged: false,
|
|
24
|
+
})
|
|
25
|
+
expect(parseDraft('nope', 1, 'number')).toEqual({
|
|
26
|
+
ok: false,
|
|
27
|
+
reason: 'number',
|
|
28
|
+
})
|
|
29
|
+
expect(parseDraft('TRUE', false, 'boolean')).toMatchObject({
|
|
30
|
+
ok: true,
|
|
31
|
+
value: true,
|
|
32
|
+
})
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
it('types an empty cell from its column and leaves it empty when nothing is typed', () => {
|
|
36
|
+
expect(parseDraft('7', null, 'number')).toMatchObject({
|
|
37
|
+
ok: true,
|
|
38
|
+
value: 7,
|
|
39
|
+
})
|
|
40
|
+
expect(parseDraft('', null, 'string')).toMatchObject({
|
|
41
|
+
ok: true,
|
|
42
|
+
unchanged: true,
|
|
43
|
+
})
|
|
44
|
+
})
|
|
45
|
+
|
|
46
|
+
it('reports an echo of the stored value as unchanged', () => {
|
|
47
|
+
expect(parseDraft('42', 42, 'number')).toMatchObject({ unchanged: true })
|
|
48
|
+
})
|
|
49
|
+
})
|
|
50
|
+
|
|
51
|
+
describe('staged edits', () => {
|
|
52
|
+
function staged() {
|
|
53
|
+
vi.stubGlobal('useDmsState', (_key: string, init: () => unknown) =>
|
|
54
|
+
ref(init()),
|
|
55
|
+
)
|
|
56
|
+
return useStagedEdits()
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
it('keeps the first stored value and drops a cell set back to it', () => {
|
|
60
|
+
const edits = staged()
|
|
61
|
+
edits.stage('tab', {
|
|
62
|
+
rowId: 'r1',
|
|
63
|
+
field: 'status',
|
|
64
|
+
before: 'pending',
|
|
65
|
+
after: 'paid',
|
|
66
|
+
})
|
|
67
|
+
edits.stage('tab', {
|
|
68
|
+
rowId: 'r1',
|
|
69
|
+
field: 'status',
|
|
70
|
+
before: 'paid',
|
|
71
|
+
after: 'refunded',
|
|
72
|
+
})
|
|
73
|
+
expect(edits.get('tab', 'r1', 'status')).toEqual({
|
|
74
|
+
before: 'pending',
|
|
75
|
+
after: 'refunded',
|
|
76
|
+
})
|
|
77
|
+
edits.stage('tab', {
|
|
78
|
+
rowId: 'r1',
|
|
79
|
+
field: 'status',
|
|
80
|
+
before: 'refunded',
|
|
81
|
+
after: 'pending',
|
|
82
|
+
})
|
|
83
|
+
expect(edits.count('tab')).toBe(0)
|
|
84
|
+
})
|
|
85
|
+
|
|
86
|
+
it('writes each row once with all its fields, and undoes with the old values', () => {
|
|
87
|
+
const edits = staged()
|
|
88
|
+
edits.stage('tab', { rowId: 'r1', field: 'a', before: 1, after: 2 })
|
|
89
|
+
edits.stage('tab', { rowId: 'r1', field: 'b', before: null, after: 'x' })
|
|
90
|
+
expect(edits.rowBody('tab', 'r1')).toEqual({ a: 2, b: 'x' })
|
|
91
|
+
expect(undoBodies(edits.list('tab')).get('r1')).toEqual({ a: 1, b: null })
|
|
92
|
+
})
|
|
93
|
+
})
|
|
94
|
+
|
|
95
|
+
describe('diagram history', () => {
|
|
96
|
+
it('squashes a burst of moves of one table into one step', () => {
|
|
97
|
+
const state = { positions: {}, notes: [] }
|
|
98
|
+
const history = new DiagramHistory()
|
|
99
|
+
history.push(
|
|
100
|
+
{
|
|
101
|
+
kind: 'tableMove',
|
|
102
|
+
tableName: 't',
|
|
103
|
+
from: { x: 0, y: 0 },
|
|
104
|
+
to: { x: 5, y: 0 },
|
|
105
|
+
},
|
|
106
|
+
1000,
|
|
107
|
+
)
|
|
108
|
+
history.push(
|
|
109
|
+
{
|
|
110
|
+
kind: 'tableMove',
|
|
111
|
+
tableName: 't',
|
|
112
|
+
from: { x: 5, y: 0 },
|
|
113
|
+
to: { x: 9, y: 0 },
|
|
114
|
+
},
|
|
115
|
+
1100,
|
|
116
|
+
)
|
|
117
|
+
expect(history.undo(state)).toBe(true)
|
|
118
|
+
expect(state.positions).toEqual({ t: { x: 0, y: 0 } })
|
|
119
|
+
expect(history.canUndo).toBe(false)
|
|
120
|
+
expect(history.redo(state)).toBe(true)
|
|
121
|
+
expect(state.positions).toEqual({ t: { x: 9, y: 0 } })
|
|
122
|
+
})
|
|
123
|
+
})
|
|
124
|
+
|
|
125
|
+
describe('query console helpers', () => {
|
|
126
|
+
const entry = (patch: Partial<HistoryEntry>): HistoryEntry => ({
|
|
127
|
+
id: '1',
|
|
128
|
+
userId: 'u',
|
|
129
|
+
query: {},
|
|
130
|
+
source: '',
|
|
131
|
+
language: 'aql',
|
|
132
|
+
executedAt: '2026-10-07T10:00:00.000Z',
|
|
133
|
+
durationMs: 10,
|
|
134
|
+
rowCount: 1,
|
|
135
|
+
status: 'ok',
|
|
136
|
+
mutation: false,
|
|
137
|
+
...patch,
|
|
138
|
+
})
|
|
139
|
+
|
|
140
|
+
it('tells runs apart', () => {
|
|
141
|
+
expect(runKind(entry({}))).toBe('fast')
|
|
142
|
+
expect(runKind(entry({ durationMs: 400 }))).toBe('slow')
|
|
143
|
+
expect(runKind(entry({ mutation: true }))).toBe('changed')
|
|
144
|
+
expect(runKind(entry({ status: 'error' }))).toBe('failed')
|
|
145
|
+
})
|
|
146
|
+
|
|
147
|
+
it('groups runs by day, newest first', () => {
|
|
148
|
+
const groups = groupByDay(
|
|
149
|
+
[
|
|
150
|
+
entry({ id: 'a', executedAt: '2026-10-07T10:00:00' }),
|
|
151
|
+
entry({ id: 'b', executedAt: '2026-10-06T10:00:00' }),
|
|
152
|
+
],
|
|
153
|
+
(run) => run.executedAt,
|
|
154
|
+
)
|
|
155
|
+
expect(groups.map((group) => group.items.map((run) => run.id))).toEqual([
|
|
156
|
+
['a'],
|
|
157
|
+
['b'],
|
|
158
|
+
])
|
|
159
|
+
})
|
|
160
|
+
|
|
161
|
+
it('reads the target of an encoded query', () => {
|
|
162
|
+
const encoded = {
|
|
163
|
+
__cls: 'Selection',
|
|
164
|
+
stages: [
|
|
165
|
+
{ stage: 'schema', options: { id: 'shop' }, args: [] },
|
|
166
|
+
{ stage: 'instance', options: { id: 'eu' }, args: [] },
|
|
167
|
+
{ stage: 'table', options: { id: 'orders' }, args: [] },
|
|
168
|
+
],
|
|
169
|
+
}
|
|
170
|
+
expect(readQueryTarget(encoded)).toEqual({
|
|
171
|
+
schema: 'shop',
|
|
172
|
+
instance: 'eu',
|
|
173
|
+
table: 'orders',
|
|
174
|
+
})
|
|
175
|
+
})
|
|
176
|
+
|
|
177
|
+
it('quotes CSV cells that need it', () => {
|
|
178
|
+
expect(
|
|
179
|
+
toCsv([
|
|
180
|
+
{ a: 'x,y', b: { c: 1 } },
|
|
181
|
+
{ a: null, d: 'say "hi"' },
|
|
182
|
+
]),
|
|
183
|
+
).toBe('a,b,d\n"x,y","{""c"":1}",\n,,"say ""hi"""\n')
|
|
184
|
+
})
|
|
185
|
+
})
|
|
186
|
+
|
|
187
|
+
describe('links', () => {
|
|
188
|
+
it('builds the DSL and page links of a table', () => {
|
|
189
|
+
expect(tableAccess({ schema: 'dms-core', table: 'users' })).toBe(
|
|
190
|
+
'schemas["dms-core"].instance().table("users")',
|
|
191
|
+
)
|
|
192
|
+
expect(
|
|
193
|
+
tableLink('data', {
|
|
194
|
+
schema: 'shop',
|
|
195
|
+
table: 'orders',
|
|
196
|
+
instance: 'eu',
|
|
197
|
+
match: { field: '_id', value: 'a:b' },
|
|
198
|
+
}),
|
|
199
|
+
).toBe(
|
|
200
|
+
'/modules/database/data?schema=shop&table=orders&instance=eu&match=_id%3Aa%3Ab',
|
|
201
|
+
)
|
|
202
|
+
expect(decodeMatch('_id:a:b')).toEqual({ field: '_id', value: 'a:b' })
|
|
203
|
+
expect(decodeMatch('nope')).toBeNull()
|
|
204
|
+
})
|
|
205
|
+
})
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest'
|
|
2
|
+
import {
|
|
3
|
+
PICKER_LIMIT,
|
|
4
|
+
matchNames,
|
|
5
|
+
sortNames,
|
|
6
|
+
} from '../app/build/data/instanceOptions'
|
|
7
|
+
|
|
8
|
+
describe('instance picker', () => {
|
|
9
|
+
it('sorts instances by name, numbers in order, once each', () => {
|
|
10
|
+
expect(sortNames(['us', 'tenant-10', 'eu', 'tenant-9', 'eu'])).toEqual([
|
|
11
|
+
'eu',
|
|
12
|
+
'tenant-9',
|
|
13
|
+
'tenant-10',
|
|
14
|
+
'us',
|
|
15
|
+
])
|
|
16
|
+
})
|
|
17
|
+
|
|
18
|
+
it('lists every instance without a search', () => {
|
|
19
|
+
expect(matchNames(['eu', 'us'], ' ')).toEqual({
|
|
20
|
+
shown: ['eu', 'us'],
|
|
21
|
+
total: 2,
|
|
22
|
+
})
|
|
23
|
+
})
|
|
24
|
+
|
|
25
|
+
it('matches a part of the name, whatever the case', () => {
|
|
26
|
+
const sorted = sortNames(['acme-EU', 'acme-us', 'globex-eu'])
|
|
27
|
+
expect(matchNames(sorted, 'Eu')).toEqual({
|
|
28
|
+
shown: ['acme-EU', 'globex-eu'],
|
|
29
|
+
total: 2,
|
|
30
|
+
})
|
|
31
|
+
expect(matchNames(sorted, 'initech')).toEqual({ shown: [], total: 0 })
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
it('renders a bounded slice of many instances and counts the rest', () => {
|
|
35
|
+
const sorted = sortNames(
|
|
36
|
+
Array.from({ length: 1_500 }, (_, index) => `tenant-${index}`),
|
|
37
|
+
)
|
|
38
|
+
const all = matchNames(sorted, '')
|
|
39
|
+
expect(all.shown).toHaveLength(PICKER_LIMIT)
|
|
40
|
+
expect(all.total).toBe(1_500)
|
|
41
|
+
expect(all.shown[0]).toBe('tenant-0')
|
|
42
|
+
|
|
43
|
+
const some = matchNames(sorted, 'tenant-14', 5)
|
|
44
|
+
expect(some.shown).toEqual([
|
|
45
|
+
'tenant-14',
|
|
46
|
+
'tenant-140',
|
|
47
|
+
'tenant-141',
|
|
48
|
+
'tenant-142',
|
|
49
|
+
'tenant-143',
|
|
50
|
+
])
|
|
51
|
+
expect(some.total).toBe(111)
|
|
52
|
+
})
|
|
53
|
+
})
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { readdirSync, readFileSync } from 'node:fs'
|
|
2
|
+
import { join } from 'node:path'
|
|
3
|
+
import { baseCompile } from '@intlify/message-compiler'
|
|
4
|
+
import { describe, expect, it } from 'vitest'
|
|
5
|
+
|
|
6
|
+
// vue-i18n compiles a message the first time it shows it: a stray `@`, `{`,
|
|
7
|
+
// `}` or `|` then fails at runtime, on the page that shows it (S-1). Compile
|
|
8
|
+
// every message of every locale here instead; literals go through `{'@'}`.
|
|
9
|
+
|
|
10
|
+
const LOCALES_DIR = join(import.meta.dirname, '../i18n/locales')
|
|
11
|
+
|
|
12
|
+
type Messages = { [key: string]: string | Messages }
|
|
13
|
+
|
|
14
|
+
function leaves(messages: Messages, prefix = ''): [string, string][] {
|
|
15
|
+
return Object.entries(messages).flatMap(([key, value]) => {
|
|
16
|
+
const path = prefix ? `${prefix}.${key}` : key
|
|
17
|
+
return typeof value === 'string' ? [[path, value]] : leaves(value, path)
|
|
18
|
+
})
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function compileErrors(message: string): string[] {
|
|
22
|
+
const errors: string[] = []
|
|
23
|
+
baseCompile(message, {
|
|
24
|
+
onError: (error) => errors.push(error.message),
|
|
25
|
+
onWarn: () => undefined,
|
|
26
|
+
})
|
|
27
|
+
return errors
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const files = readdirSync(LOCALES_DIR).filter((file) => file.endsWith('.json'))
|
|
31
|
+
|
|
32
|
+
describe('locale messages', () => {
|
|
33
|
+
it('finds the locale files', () => {
|
|
34
|
+
expect(files.length).toBeGreaterThan(0)
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
it.each(files)('compiles every message of %s', (file) => {
|
|
38
|
+
const messages = JSON.parse(
|
|
39
|
+
readFileSync(join(LOCALES_DIR, file), 'utf8'),
|
|
40
|
+
) as Messages
|
|
41
|
+
const failures = leaves(messages)
|
|
42
|
+
.map(([key, message]) => ({ key, errors: compileErrors(message) }))
|
|
43
|
+
.filter((entry) => entry.errors.length > 0)
|
|
44
|
+
.map((entry) => `${entry.key}: ${entry.errors.join(', ')}`)
|
|
45
|
+
expect(failures).toEqual([])
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
it('reports an unescaped linked-message sign', () => {
|
|
49
|
+
expect(compileErrors('declares tables with @RegisterTable.')).not.toEqual(
|
|
50
|
+
[],
|
|
51
|
+
)
|
|
52
|
+
expect(compileErrors("declares tables with {'@'}RegisterTable.")).toEqual(
|
|
53
|
+
[],
|
|
54
|
+
)
|
|
55
|
+
})
|
|
56
|
+
})
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { afterEach, expect, it, vi } from 'vitest'
|
|
2
|
-
import { computed, ref } from 'vue'
|
|
3
|
-
import { useDataBrowserTabs } from '../app/composables/useDataBrowserTabs'
|
|
1
|
+
import { afterEach, describe, expect, it, vi } from 'vitest'
|
|
2
|
+
import { computed, reactive, ref } from 'vue'
|
|
3
|
+
import { useDataBrowserTabs } from '../app/build/composables/useDataBrowserTabs'
|
|
4
4
|
|
|
5
5
|
afterEach(() => vi.unstubAllGlobals())
|
|
6
6
|
|
|
@@ -57,7 +57,12 @@ it('does not revisit an already selected deep link while restoring tabs', () =>
|
|
|
57
57
|
const replace = vi.fn()
|
|
58
58
|
vi.stubGlobal('useDmsRouter', () => ({ replace }))
|
|
59
59
|
vi.stubGlobal('useDmsRoute', () => ({
|
|
60
|
-
query: {
|
|
60
|
+
query: {
|
|
61
|
+
schema: 'sales',
|
|
62
|
+
table: 'orders',
|
|
63
|
+
instance: 'eu',
|
|
64
|
+
panel: 'detail',
|
|
65
|
+
},
|
|
61
66
|
}))
|
|
62
67
|
const browser = useDataBrowserTabs()
|
|
63
68
|
browser.restore()
|
|
@@ -65,6 +70,102 @@ it('does not revisit an already selected deep link while restoring tabs', () =>
|
|
|
65
70
|
expect(replace).not.toHaveBeenCalled()
|
|
66
71
|
browser.openTab('sales', 'us', 'orders', false)
|
|
67
72
|
expect(replace).toHaveBeenCalledExactlyOnceWith({
|
|
68
|
-
query: {
|
|
73
|
+
query: {
|
|
74
|
+
schema: 'sales',
|
|
75
|
+
table: 'orders',
|
|
76
|
+
instance: 'us',
|
|
77
|
+
panel: 'detail',
|
|
78
|
+
},
|
|
79
|
+
})
|
|
80
|
+
})
|
|
81
|
+
|
|
82
|
+
it('restores the preview tab as a preview, the URL naming it included', () => {
|
|
83
|
+
setup()
|
|
84
|
+
const replace = vi.fn()
|
|
85
|
+
vi.stubGlobal('useDmsRouter', () => ({ replace }))
|
|
86
|
+
vi.stubGlobal('useDmsRoute', () => ({
|
|
87
|
+
query: { schema: 'sales', table: 'orders', instance: 'eu' },
|
|
88
|
+
}))
|
|
89
|
+
vi.stubGlobal('window', {
|
|
90
|
+
sessionStorage: {
|
|
91
|
+
getItem: () =>
|
|
92
|
+
JSON.stringify({
|
|
93
|
+
tabs: [
|
|
94
|
+
{ schema: 'sales', instance: 'eu', table: 'customers' },
|
|
95
|
+
{
|
|
96
|
+
schema: 'sales',
|
|
97
|
+
instance: 'eu',
|
|
98
|
+
table: 'orders',
|
|
99
|
+
preview: true,
|
|
100
|
+
},
|
|
101
|
+
],
|
|
102
|
+
activeId: 'sales::eu::orders',
|
|
103
|
+
}),
|
|
104
|
+
setItem: vi.fn(),
|
|
105
|
+
},
|
|
106
|
+
})
|
|
107
|
+
const browser = useDataBrowserTabs()
|
|
108
|
+
browser.restore()
|
|
109
|
+
expect(browser.activeId.value).toBe('sales::eu::orders')
|
|
110
|
+
expect(browser.tabs.value.map((tab) => tab.preview)).toEqual([false, true])
|
|
111
|
+
expect(replace).not.toHaveBeenCalled()
|
|
112
|
+
})
|
|
113
|
+
|
|
114
|
+
describe('data browser: the route it writes itself', () => {
|
|
115
|
+
// A reactive route the router writes to, as the DMS's stable
|
|
116
|
+
// useDmsRoute() reflects a landed visit.
|
|
117
|
+
function routed(query: Record<string, string> = {}) {
|
|
118
|
+
setup()
|
|
119
|
+
vi.stubGlobal('window', {
|
|
120
|
+
sessionStorage: { getItem: () => null, setItem: vi.fn() },
|
|
121
|
+
})
|
|
122
|
+
const route = reactive({ query })
|
|
123
|
+
const replace = vi.fn(({ query: next }: { query: typeof query }) => {
|
|
124
|
+
route.query = next
|
|
125
|
+
})
|
|
126
|
+
vi.stubGlobal('useDmsRoute', () => route)
|
|
127
|
+
vi.stubGlobal('useDmsRouter', () => ({ replace }))
|
|
128
|
+
return { route, replace, browser: useDataBrowserTabs() }
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
it('keeps a preview tab a preview once its URL lands', () => {
|
|
132
|
+
const { route, replace, browser } = routed()
|
|
133
|
+
browser.openTab('sales', 'eu', 'orders')
|
|
134
|
+
expect(replace).toHaveBeenCalledExactlyOnceWith({
|
|
135
|
+
query: { schema: 'sales', table: 'orders', instance: 'eu' },
|
|
136
|
+
})
|
|
137
|
+
// The page's route watcher sees the URL the browser wrote.
|
|
138
|
+
expect(route.query.table).toBe('orders')
|
|
139
|
+
expect(browser.openFromRoute()).toBe(true)
|
|
140
|
+
expect(browser.tabs.value).toEqual([
|
|
141
|
+
expect.objectContaining({ id: 'sales::eu::orders', preview: true }),
|
|
142
|
+
])
|
|
143
|
+
})
|
|
144
|
+
|
|
145
|
+
it('opens a linked table pinned, the preview slot left alone', () => {
|
|
146
|
+
const { route, browser } = routed()
|
|
147
|
+
browser.openTab('sales', 'eu', 'orders')
|
|
148
|
+
route.query = { schema: 'sales', table: 'customers', instance: 'eu' }
|
|
149
|
+
browser.openFromRoute()
|
|
150
|
+
expect(browser.activeId.value).toBe('sales::eu::customers')
|
|
151
|
+
expect(browser.tabs.value.map((tab) => [tab.id, tab.preview])).toEqual([
|
|
152
|
+
['sales::eu::orders', true],
|
|
153
|
+
['sales::eu::customers', false],
|
|
154
|
+
])
|
|
155
|
+
})
|
|
156
|
+
|
|
157
|
+
it('pins the active preview tab when a link filters it on a row', () => {
|
|
158
|
+
const { route, browser } = routed()
|
|
159
|
+
browser.openTab('sales', 'eu', 'orders')
|
|
160
|
+
route.query = {
|
|
161
|
+
schema: 'sales',
|
|
162
|
+
table: 'orders',
|
|
163
|
+
instance: 'eu',
|
|
164
|
+
match: '_id:42',
|
|
165
|
+
}
|
|
166
|
+
browser.openFromRoute()
|
|
167
|
+
expect(browser.tabs.value).toEqual([
|
|
168
|
+
expect.objectContaining({ id: 'sales::eu::orders', preview: false }),
|
|
169
|
+
])
|
|
69
170
|
})
|
|
70
171
|
})
|
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest'
|
|
2
|
+
import {
|
|
3
|
+
CROSS_INSTANCE_VALUE,
|
|
4
|
+
DEFAULT_INSTANCE_VALUE,
|
|
5
|
+
instanceChoiceOf,
|
|
6
|
+
tabInstanceOf,
|
|
7
|
+
} from '../app/build/composables/useDataBrowserTabs'
|
|
8
|
+
import {
|
|
9
|
+
PICK_ALL,
|
|
10
|
+
PICK_DEFAULT,
|
|
11
|
+
instanceMenu,
|
|
12
|
+
instancePickChoice,
|
|
13
|
+
instancePickValue,
|
|
14
|
+
schemaMenu,
|
|
15
|
+
type InstanceMenu,
|
|
16
|
+
type PickerItem,
|
|
17
|
+
} from '../app/build/data/pickerMenus'
|
|
18
|
+
import { sortNames } from '../app/build/data/instanceOptions'
|
|
19
|
+
|
|
20
|
+
// The schema and instance pickers the Schemas page's filter bar and the data
|
|
21
|
+
// browser's sidebar share (SchemaPicker.vue, InstancePicker.vue).
|
|
22
|
+
|
|
23
|
+
const values = (groups: PickerItem[][]) =>
|
|
24
|
+
groups.map((group) => group.map((item) => item.value ?? `#${item.label}`))
|
|
25
|
+
|
|
26
|
+
const schemaLabels = {
|
|
27
|
+
more: (shown: number, total: number) => `First ${shown} of ${total}`,
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
describe('schema picker menu', () => {
|
|
31
|
+
const ids = sortNames(['shop', 'demo', 'core'])
|
|
32
|
+
|
|
33
|
+
it('pins "All schemas" first when the page lists every schema', () => {
|
|
34
|
+
const groups = schemaMenu({
|
|
35
|
+
ids,
|
|
36
|
+
search: '',
|
|
37
|
+
allLabel: 'All schemas',
|
|
38
|
+
...schemaLabels,
|
|
39
|
+
})
|
|
40
|
+
expect(values(groups)).toEqual([[PICK_ALL], ['core', 'demo', 'shop']])
|
|
41
|
+
expect(groups[0]?.[0]?.class).toBe('pin-top')
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
it('offers no "All schemas" when a schema is required', () => {
|
|
45
|
+
expect(
|
|
46
|
+
values(
|
|
47
|
+
schemaMenu({
|
|
48
|
+
ids,
|
|
49
|
+
search: '',
|
|
50
|
+
allLabel: null,
|
|
51
|
+
...schemaLabels,
|
|
52
|
+
}),
|
|
53
|
+
),
|
|
54
|
+
).toEqual([['core', 'demo', 'shop']])
|
|
55
|
+
})
|
|
56
|
+
|
|
57
|
+
it('searches every schema', () => {
|
|
58
|
+
expect(
|
|
59
|
+
values(
|
|
60
|
+
schemaMenu({
|
|
61
|
+
ids,
|
|
62
|
+
search: 'o',
|
|
63
|
+
allLabel: null,
|
|
64
|
+
...schemaLabels,
|
|
65
|
+
}),
|
|
66
|
+
),
|
|
67
|
+
).toEqual([['core', 'demo', 'shop']])
|
|
68
|
+
})
|
|
69
|
+
|
|
70
|
+
it('says how many matched beyond the ones listed', () => {
|
|
71
|
+
const many = sortNames(
|
|
72
|
+
Array.from({ length: 150 }, (_, index) => `module-${index}`),
|
|
73
|
+
)
|
|
74
|
+
const groups = schemaMenu({
|
|
75
|
+
ids: many,
|
|
76
|
+
search: '',
|
|
77
|
+
allLabel: null,
|
|
78
|
+
...schemaLabels,
|
|
79
|
+
})
|
|
80
|
+
expect(groups[0]?.at(-1)).toEqual({
|
|
81
|
+
label: 'First 100 of 150',
|
|
82
|
+
type: 'label',
|
|
83
|
+
})
|
|
84
|
+
})
|
|
85
|
+
})
|
|
86
|
+
|
|
87
|
+
function menu(overrides: Partial<InstanceMenu>): InstanceMenu {
|
|
88
|
+
return {
|
|
89
|
+
search: '',
|
|
90
|
+
hasSchema: true,
|
|
91
|
+
names: [],
|
|
92
|
+
matched: 0,
|
|
93
|
+
total: null,
|
|
94
|
+
status: 'idle',
|
|
95
|
+
allMode: 'filter',
|
|
96
|
+
labels: {
|
|
97
|
+
all: 'All instances',
|
|
98
|
+
readOnly: 'Read-only view',
|
|
99
|
+
default: 'default',
|
|
100
|
+
pickSchema: 'Pick a schema',
|
|
101
|
+
loading: 'Loading',
|
|
102
|
+
failed: 'Failed',
|
|
103
|
+
noNamed: 'No named instance',
|
|
104
|
+
noMatch: 'No instance matches',
|
|
105
|
+
more: (shown, total) => `First ${shown} of ${total}`,
|
|
106
|
+
count: (value) => String(value),
|
|
107
|
+
},
|
|
108
|
+
...overrides,
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
describe('instance picker menu', () => {
|
|
113
|
+
it('pins every instance, counted, and default first as a filter', () => {
|
|
114
|
+
const groups = instanceMenu(
|
|
115
|
+
menu({ names: ['eu', 'us'], matched: 2, total: 2, status: 'success' }),
|
|
116
|
+
)
|
|
117
|
+
expect(values(groups)).toEqual([
|
|
118
|
+
[PICK_ALL, PICK_DEFAULT],
|
|
119
|
+
['eu', 'us'],
|
|
120
|
+
])
|
|
121
|
+
expect(groups[0]?.[0]).toMatchObject({
|
|
122
|
+
label: 'All instances (3)',
|
|
123
|
+
icon: 'i-ph-stack-simple',
|
|
124
|
+
class: 'pin-top',
|
|
125
|
+
})
|
|
126
|
+
})
|
|
127
|
+
|
|
128
|
+
it('offers every instance first, locked, as the read-only view', () => {
|
|
129
|
+
const groups = instanceMenu(
|
|
130
|
+
menu({
|
|
131
|
+
names: ['eu'],
|
|
132
|
+
matched: 1,
|
|
133
|
+
total: 1,
|
|
134
|
+
status: 'success',
|
|
135
|
+
allMode: 'readonly',
|
|
136
|
+
}),
|
|
137
|
+
)
|
|
138
|
+
expect(values(groups)).toEqual([[PICK_ALL, PICK_DEFAULT], ['eu']])
|
|
139
|
+
expect(groups[0]?.[0]).toMatchObject({
|
|
140
|
+
label: 'All instances (2)',
|
|
141
|
+
description: 'Read-only view',
|
|
142
|
+
icon: 'i-ph-lock-simple',
|
|
143
|
+
class: 'pin-top',
|
|
144
|
+
})
|
|
145
|
+
})
|
|
146
|
+
|
|
147
|
+
it('keeps the read-only view after the matches of a search', () => {
|
|
148
|
+
const groups = instanceMenu(
|
|
149
|
+
menu({
|
|
150
|
+
search: 'zz',
|
|
151
|
+
total: 4,
|
|
152
|
+
status: 'success',
|
|
153
|
+
allMode: 'readonly',
|
|
154
|
+
}),
|
|
155
|
+
)
|
|
156
|
+
expect(values(groups)).toEqual([['#No instance matches'], [PICK_ALL]])
|
|
157
|
+
expect(groups[1]?.[0]?.class).toBeUndefined()
|
|
158
|
+
expect(
|
|
159
|
+
values(
|
|
160
|
+
instanceMenu(
|
|
161
|
+
menu({
|
|
162
|
+
search: 'e',
|
|
163
|
+
names: ['eu'],
|
|
164
|
+
matched: 1,
|
|
165
|
+
total: 4,
|
|
166
|
+
status: 'success',
|
|
167
|
+
allMode: 'readonly',
|
|
168
|
+
}),
|
|
169
|
+
),
|
|
170
|
+
),
|
|
171
|
+
).toEqual([[PICK_DEFAULT], ['eu'], [PICK_ALL]])
|
|
172
|
+
})
|
|
173
|
+
|
|
174
|
+
it('lists the "all" filter after the matches, or drops it', () => {
|
|
175
|
+
expect(
|
|
176
|
+
values(
|
|
177
|
+
instanceMenu(
|
|
178
|
+
menu({
|
|
179
|
+
search: 'all',
|
|
180
|
+
names: ['ball'],
|
|
181
|
+
matched: 1,
|
|
182
|
+
status: 'success',
|
|
183
|
+
}),
|
|
184
|
+
),
|
|
185
|
+
),
|
|
186
|
+
).toEqual([['ball'], [PICK_ALL]])
|
|
187
|
+
})
|
|
188
|
+
|
|
189
|
+
it('drops the "all" filter a search does not match', () => {
|
|
190
|
+
expect(
|
|
191
|
+
values(
|
|
192
|
+
instanceMenu(
|
|
193
|
+
menu({ search: 'eu', names: ['eu'], matched: 1, status: 'success' }),
|
|
194
|
+
),
|
|
195
|
+
),
|
|
196
|
+
).toEqual([['eu']])
|
|
197
|
+
})
|
|
198
|
+
|
|
199
|
+
it('says the instances load, failed, or are more than listed', () => {
|
|
200
|
+
expect(values(instanceMenu(menu({ status: 'pending' })))[1]).toEqual([
|
|
201
|
+
'#Loading',
|
|
202
|
+
])
|
|
203
|
+
expect(values(instanceMenu(menu({ status: 'error' })))[1]).toEqual([
|
|
204
|
+
'#Failed',
|
|
205
|
+
])
|
|
206
|
+
expect(
|
|
207
|
+
values(
|
|
208
|
+
instanceMenu(
|
|
209
|
+
menu({ status: 'success', total: 0, matched: 0, names: [] }),
|
|
210
|
+
),
|
|
211
|
+
)[1],
|
|
212
|
+
).toEqual(['#No named instance'])
|
|
213
|
+
expect(
|
|
214
|
+
values(
|
|
215
|
+
instanceMenu(
|
|
216
|
+
menu({
|
|
217
|
+
status: 'success',
|
|
218
|
+
names: ['tenant-1'],
|
|
219
|
+
matched: 500,
|
|
220
|
+
total: 500,
|
|
221
|
+
}),
|
|
222
|
+
),
|
|
223
|
+
)[1],
|
|
224
|
+
).toEqual(['tenant-1', '#First 1 of 500'])
|
|
225
|
+
})
|
|
226
|
+
|
|
227
|
+
it('asks for a schema before listing named instances', () => {
|
|
228
|
+
for (const allMode of ['filter', 'readonly'] as const)
|
|
229
|
+
expect(values(instanceMenu(menu({ hasSchema: false, allMode })))).toEqual(
|
|
230
|
+
[[PICK_ALL, PICK_DEFAULT], ['#Pick a schema']],
|
|
231
|
+
)
|
|
232
|
+
})
|
|
233
|
+
|
|
234
|
+
it('maps choices to menu values and back', () => {
|
|
235
|
+
for (const choice of [
|
|
236
|
+
{ kind: 'all' },
|
|
237
|
+
{ kind: 'default' },
|
|
238
|
+
{ kind: 'named', id: 'eu' },
|
|
239
|
+
] as const)
|
|
240
|
+
expect(instancePickChoice(instancePickValue(choice))).toEqual(choice)
|
|
241
|
+
})
|
|
242
|
+
|
|
243
|
+
it('maps a data-browser tab instance to a choice and back', () => {
|
|
244
|
+
for (const instance of [
|
|
245
|
+
DEFAULT_INSTANCE_VALUE,
|
|
246
|
+
CROSS_INSTANCE_VALUE,
|
|
247
|
+
'eu',
|
|
248
|
+
'all',
|
|
249
|
+
])
|
|
250
|
+
expect(tabInstanceOf(instanceChoiceOf(instance))).toBe(instance)
|
|
251
|
+
expect(instanceChoiceOf(CROSS_INSTANCE_VALUE)).toEqual({ kind: 'all' })
|
|
252
|
+
expect(instanceChoiceOf('all')).toEqual({ kind: 'named', id: 'all' })
|
|
253
|
+
})
|
|
254
|
+
})
|