@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,178 @@
|
|
|
1
|
+
import type { DiagramPositions } from '../composables/useDiagramPersistence'
|
|
2
|
+
import type { NoteDraft } from './graph'
|
|
3
|
+
|
|
4
|
+
// The diagram's undo stack. Every change is an operation that knows how to
|
|
5
|
+
// apply itself and how to take itself back; the canvas saves whatever state
|
|
6
|
+
// results, so undo and redo are saved like any other change.
|
|
7
|
+
|
|
8
|
+
interface Point {
|
|
9
|
+
x: number
|
|
10
|
+
y: number
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export type DiagramOperation =
|
|
14
|
+
| { kind: 'tableMove'; tableName: string; from: Point; to: Point }
|
|
15
|
+
| { kind: 'noteMove'; noteId: string; from: Point; to: Point }
|
|
16
|
+
| { kind: 'noteText'; noteId: string; from: string; to: string }
|
|
17
|
+
| { kind: 'noteCreate'; snapshot: NoteDraft }
|
|
18
|
+
| { kind: 'noteDelete'; snapshot: NoteDraft }
|
|
19
|
+
| { kind: 'layout'; from: DiagramPositions; to: DiagramPositions }
|
|
20
|
+
|
|
21
|
+
export interface DiagramState {
|
|
22
|
+
positions: DiagramPositions
|
|
23
|
+
notes: NoteDraft[]
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
type OperationOf<K extends DiagramOperation['kind']> = Extract<
|
|
27
|
+
DiagramOperation,
|
|
28
|
+
{ kind: K }
|
|
29
|
+
>
|
|
30
|
+
type Apply<K extends DiagramOperation['kind']> = (
|
|
31
|
+
state: DiagramState,
|
|
32
|
+
op: OperationOf<K>,
|
|
33
|
+
) => void
|
|
34
|
+
interface Handlers<K extends DiagramOperation['kind']> {
|
|
35
|
+
forward: Apply<K>
|
|
36
|
+
reverse: Apply<K>
|
|
37
|
+
}
|
|
38
|
+
type OperationHandlers = { [K in DiagramOperation['kind']]: Handlers<K> }
|
|
39
|
+
|
|
40
|
+
// Moves and text edits made in quick succession are one step.
|
|
41
|
+
const SQUASH_WINDOW_MS = 600
|
|
42
|
+
|
|
43
|
+
export function clonePositions(positions: DiagramPositions): DiagramPositions {
|
|
44
|
+
return Object.fromEntries(
|
|
45
|
+
Object.entries(positions).map(([name, point]) => [
|
|
46
|
+
name,
|
|
47
|
+
{ x: point.x, y: point.y },
|
|
48
|
+
]),
|
|
49
|
+
)
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function findNote(state: DiagramState, noteId: string): NoteDraft | undefined {
|
|
53
|
+
return state.notes.find((note) => note.id === noteId)
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function moveNote(state: DiagramState, noteId: string, point: Point) {
|
|
57
|
+
const note = findNote(state, noteId)
|
|
58
|
+
if (note) Object.assign(note, { x: point.x, y: point.y })
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function setNoteText(state: DiagramState, noteId: string, text: string) {
|
|
62
|
+
const note = findNote(state, noteId)
|
|
63
|
+
if (note) note.text = text
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function addNote(state: DiagramState, snapshot: NoteDraft) {
|
|
67
|
+
if (!findNote(state, snapshot.id)) state.notes.push({ ...snapshot })
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function removeNote(state: DiagramState, noteId: string) {
|
|
71
|
+
const index = state.notes.findIndex((note) => note.id === noteId)
|
|
72
|
+
if (index >= 0) state.notes.splice(index, 1)
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const HANDLERS: OperationHandlers = {
|
|
76
|
+
tableMove: {
|
|
77
|
+
forward: (state, op) => (state.positions[op.tableName] = { ...op.to }),
|
|
78
|
+
reverse: (state, op) => (state.positions[op.tableName] = { ...op.from }),
|
|
79
|
+
},
|
|
80
|
+
noteMove: {
|
|
81
|
+
forward: (state, op) => moveNote(state, op.noteId, op.to),
|
|
82
|
+
reverse: (state, op) => moveNote(state, op.noteId, op.from),
|
|
83
|
+
},
|
|
84
|
+
noteText: {
|
|
85
|
+
forward: (state, op) => setNoteText(state, op.noteId, op.to),
|
|
86
|
+
reverse: (state, op) => setNoteText(state, op.noteId, op.from),
|
|
87
|
+
},
|
|
88
|
+
noteCreate: {
|
|
89
|
+
forward: (state, op) => addNote(state, op.snapshot),
|
|
90
|
+
reverse: (state, op) => removeNote(state, op.snapshot.id),
|
|
91
|
+
},
|
|
92
|
+
noteDelete: {
|
|
93
|
+
forward: (state, op) => removeNote(state, op.snapshot.id),
|
|
94
|
+
reverse: (state, op) => addNote(state, op.snapshot),
|
|
95
|
+
},
|
|
96
|
+
layout: {
|
|
97
|
+
forward: (state, op) => (state.positions = clonePositions(op.to)),
|
|
98
|
+
reverse: (state, op) => (state.positions = clonePositions(op.from)),
|
|
99
|
+
},
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function run(
|
|
103
|
+
state: DiagramState,
|
|
104
|
+
op: DiagramOperation,
|
|
105
|
+
direction: keyof Handlers<never>,
|
|
106
|
+
) {
|
|
107
|
+
const handlers = HANDLERS[op.kind] as Handlers<typeof op.kind>
|
|
108
|
+
;(handlers[direction] as (s: DiagramState, o: DiagramOperation) => void)(
|
|
109
|
+
state,
|
|
110
|
+
op,
|
|
111
|
+
)
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/** Folds `next` into `head` when both edit the same thing in a burst. */
|
|
115
|
+
function squash(head: DiagramOperation, next: DiagramOperation): boolean {
|
|
116
|
+
if (head.kind !== next.kind) return false
|
|
117
|
+
if (
|
|
118
|
+
next.kind === 'tableMove' &&
|
|
119
|
+
head.kind === 'tableMove' &&
|
|
120
|
+
head.tableName === next.tableName
|
|
121
|
+
) {
|
|
122
|
+
head.to = next.to
|
|
123
|
+
return true
|
|
124
|
+
}
|
|
125
|
+
if (
|
|
126
|
+
(next.kind === 'noteMove' || next.kind === 'noteText') &&
|
|
127
|
+
(head.kind === 'noteMove' || head.kind === 'noteText') &&
|
|
128
|
+
head.noteId === next.noteId
|
|
129
|
+
) {
|
|
130
|
+
head.to = next.to as never
|
|
131
|
+
return true
|
|
132
|
+
}
|
|
133
|
+
return false
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
export class DiagramHistory {
|
|
137
|
+
private operations: DiagramOperation[] = []
|
|
138
|
+
private cursor = 0
|
|
139
|
+
private lastPushAt = 0
|
|
140
|
+
|
|
141
|
+
get canUndo(): boolean {
|
|
142
|
+
return this.cursor > 0
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
get canRedo(): boolean {
|
|
146
|
+
return this.cursor < this.operations.length
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
clear() {
|
|
150
|
+
this.operations = []
|
|
151
|
+
this.cursor = 0
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/** Records an operation already applied to the state. */
|
|
155
|
+
push(op: DiagramOperation, now = Date.now()) {
|
|
156
|
+
this.operations.splice(this.cursor)
|
|
157
|
+
const head = this.operations[this.cursor - 1]
|
|
158
|
+
const recent = now - this.lastPushAt < SQUASH_WINDOW_MS
|
|
159
|
+
this.lastPushAt = now
|
|
160
|
+
if (head && recent && squash(head, op)) return
|
|
161
|
+
this.operations.push(op)
|
|
162
|
+
this.cursor += 1
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
undo(state: DiagramState): boolean {
|
|
166
|
+
if (!this.canUndo) return false
|
|
167
|
+
this.cursor -= 1
|
|
168
|
+
run(state, this.operations[this.cursor] as DiagramOperation, 'reverse')
|
|
169
|
+
return true
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
redo(state: DiagramState): boolean {
|
|
173
|
+
if (!this.canRedo) return false
|
|
174
|
+
run(state, this.operations[this.cursor] as DiagramOperation, 'forward')
|
|
175
|
+
this.cursor += 1
|
|
176
|
+
return true
|
|
177
|
+
}
|
|
178
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
// The diagram's keyboard shortcuts, the ones its toolbar's tooltips name.
|
|
2
|
+
// Letters match whatever their case (Caps Lock, Shift); undo and redo take
|
|
3
|
+
// Ctrl or ⌘, with Shift+Z or Y for redo.
|
|
4
|
+
|
|
5
|
+
export type DiagramCommand =
|
|
6
|
+
| 'search'
|
|
7
|
+
| 'note'
|
|
8
|
+
| 'select'
|
|
9
|
+
| 'pan'
|
|
10
|
+
| 'fit'
|
|
11
|
+
| 'resetZoom'
|
|
12
|
+
| 'undo'
|
|
13
|
+
| 'redo'
|
|
14
|
+
| 'escape'
|
|
15
|
+
|
|
16
|
+
type KeyInput = Pick<
|
|
17
|
+
KeyboardEvent,
|
|
18
|
+
'key' | 'ctrlKey' | 'metaKey' | 'altKey' | 'shiftKey'
|
|
19
|
+
>
|
|
20
|
+
|
|
21
|
+
const PLAIN_KEYS: Record<string, DiagramCommand> = {
|
|
22
|
+
f: 'search',
|
|
23
|
+
n: 'note',
|
|
24
|
+
v: 'select',
|
|
25
|
+
h: 'pan',
|
|
26
|
+
'1': 'fit',
|
|
27
|
+
'0': 'resetZoom',
|
|
28
|
+
escape: 'escape',
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function diagramCommand(event: KeyInput): DiagramCommand | null {
|
|
32
|
+
if (event.altKey) return null
|
|
33
|
+
const key = event.key.toLowerCase()
|
|
34
|
+
if (event.ctrlKey || event.metaKey) {
|
|
35
|
+
if (key === 'z') return event.shiftKey ? 'redo' : 'undo'
|
|
36
|
+
if (key === 'y' && !event.shiftKey) return 'redo'
|
|
37
|
+
return null
|
|
38
|
+
}
|
|
39
|
+
return PLAIN_KEYS[key] ?? null
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const EDITABLE =
|
|
43
|
+
'input, textarea, select, [contenteditable=""], [contenteditable="true"]'
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Whether a key pressed on `target` is the canvas's to handle: not while
|
|
47
|
+
* typing, not while a dialog such as the inspector's drawer is open, and not
|
|
48
|
+
* from an element outside the canvas that has the focus.
|
|
49
|
+
*/
|
|
50
|
+
export function canvasOwnsKey(
|
|
51
|
+
target: EventTarget | null,
|
|
52
|
+
canvas: Element | null,
|
|
53
|
+
): boolean {
|
|
54
|
+
const element = target as Element | null
|
|
55
|
+
if (!element || typeof element.closest !== 'function') return true
|
|
56
|
+
if (element.closest(EDITABLE)) return false
|
|
57
|
+
if (element.ownerDocument?.querySelector('[role="dialog"]')) return false
|
|
58
|
+
const isPage =
|
|
59
|
+
element === element.ownerDocument?.body ||
|
|
60
|
+
element === element.ownerDocument?.documentElement
|
|
61
|
+
return isPage || Boolean(canvas?.contains(element))
|
|
62
|
+
}
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
DiagramNote,
|
|
3
|
+
DiagramNoteCreate,
|
|
4
|
+
DiagramNoteUpdate,
|
|
5
|
+
DiagramPositions,
|
|
6
|
+
} from '../composables/useDiagramPersistence'
|
|
7
|
+
import type { NoteDraft } from './graph'
|
|
8
|
+
import { clonePositions, type DiagramState } from './history'
|
|
9
|
+
|
|
10
|
+
// Saves the diagram as it changes (D-10): the layout and the notes are
|
|
11
|
+
// compared with what was last stored, and only the differences are written.
|
|
12
|
+
// One DiagramSync belongs to one schema: it only ever writes under its id.
|
|
13
|
+
|
|
14
|
+
export interface DiagramStore {
|
|
15
|
+
saveLayout(schema: string, positions: DiagramPositions): Promise<void>
|
|
16
|
+
createNote(payload: DiagramNoteCreate): Promise<DiagramNote>
|
|
17
|
+
updateNote(id: string, patch: DiagramNoteUpdate): Promise<DiagramNote>
|
|
18
|
+
deleteNote(id: string): Promise<void>
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const NOTE_FIELDS = ['text', 'x', 'y', 'width', 'height', 'color'] as const
|
|
22
|
+
|
|
23
|
+
function noteChanged(saved: NoteDraft, current: NoteDraft): boolean {
|
|
24
|
+
return NOTE_FIELDS.some((field) => saved[field] !== current[field])
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function notePatch(note: NoteDraft): DiagramNoteUpdate {
|
|
28
|
+
return {
|
|
29
|
+
text: note.text,
|
|
30
|
+
x: note.x,
|
|
31
|
+
y: note.y,
|
|
32
|
+
width: note.width,
|
|
33
|
+
height: note.height,
|
|
34
|
+
color: note.color ?? undefined,
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function noteFromStored(note: DiagramNote): NoteDraft {
|
|
39
|
+
return {
|
|
40
|
+
id: note.id,
|
|
41
|
+
schemaName: note.schemaName,
|
|
42
|
+
text: note.text,
|
|
43
|
+
x: note.x,
|
|
44
|
+
y: note.y,
|
|
45
|
+
width: note.width,
|
|
46
|
+
height: note.height,
|
|
47
|
+
color: note.color,
|
|
48
|
+
isTemp: false,
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export class DiagramSync {
|
|
53
|
+
private saved: DiagramState = { positions: {}, notes: [] }
|
|
54
|
+
// The id the store gave each note created on the canvas, by its temporary
|
|
55
|
+
// id. The canvas keeps the temporary one, so the note's node is not
|
|
56
|
+
// remounted (and its edit lost) when it is first saved.
|
|
57
|
+
private readonly created = new Map<string, string>()
|
|
58
|
+
|
|
59
|
+
constructor(
|
|
60
|
+
private readonly store: DiagramStore,
|
|
61
|
+
readonly schemaId: string,
|
|
62
|
+
) {}
|
|
63
|
+
|
|
64
|
+
/** What is stored now, as loaded or as last written. */
|
|
65
|
+
reset(state: DiagramState) {
|
|
66
|
+
this.saved = {
|
|
67
|
+
positions: clonePositions(state.positions),
|
|
68
|
+
notes: state.notes.map((note) => ({ ...note, id: this.idOf(note) })),
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
hasChanges(state: DiagramState): boolean {
|
|
73
|
+
return this.layoutChanged(state) || this.notesChanged(state)
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/** Writes the differences; `state` should be a copy the canvas won't change. */
|
|
77
|
+
async save(state: DiagramState): Promise<void> {
|
|
78
|
+
if (this.layoutChanged(state)) {
|
|
79
|
+
await this.store.saveLayout(
|
|
80
|
+
this.schemaId,
|
|
81
|
+
clonePositions(state.positions),
|
|
82
|
+
)
|
|
83
|
+
}
|
|
84
|
+
await this.saveNotes(state)
|
|
85
|
+
this.reset(state)
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
private idOf(note: NoteDraft): string {
|
|
89
|
+
return this.created.get(note.id) ?? note.id
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
private layoutChanged(state: DiagramState): boolean {
|
|
93
|
+
return (
|
|
94
|
+
JSON.stringify(this.saved.positions) !== JSON.stringify(state.positions)
|
|
95
|
+
)
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
private notesChanged(state: DiagramState): boolean {
|
|
99
|
+
const saved = new Map(this.saved.notes.map((note) => [note.id, note]))
|
|
100
|
+
return (
|
|
101
|
+
saved.size !== state.notes.length ||
|
|
102
|
+
state.notes.some((note) => {
|
|
103
|
+
const before = saved.get(this.idOf(note))
|
|
104
|
+
return !before || noteChanged(before, note)
|
|
105
|
+
})
|
|
106
|
+
)
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
private async saveNotes(state: DiagramState) {
|
|
110
|
+
const saved = new Map(this.saved.notes.map((note) => [note.id, note]))
|
|
111
|
+
const current = new Set(state.notes.map((note) => this.idOf(note)))
|
|
112
|
+
for (const note of this.saved.notes) {
|
|
113
|
+
if (!current.has(note.id)) await this.store.deleteNote(note.id)
|
|
114
|
+
}
|
|
115
|
+
for (const note of state.notes) {
|
|
116
|
+
const id = this.idOf(note)
|
|
117
|
+
const before = saved.get(id)
|
|
118
|
+
if (!before) await this.createNote(note)
|
|
119
|
+
else if (noteChanged(before, note))
|
|
120
|
+
await this.store.updateNote(id, notePatch(note))
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
private async createNote(note: NoteDraft) {
|
|
125
|
+
const created = await this.store.createNote({
|
|
126
|
+
schemaName: this.schemaId,
|
|
127
|
+
text: note.text,
|
|
128
|
+
x: note.x,
|
|
129
|
+
y: note.y,
|
|
130
|
+
width: note.width,
|
|
131
|
+
height: note.height,
|
|
132
|
+
color: note.color ?? undefined,
|
|
133
|
+
})
|
|
134
|
+
this.created.set(note.id, created.id)
|
|
135
|
+
Object.assign(note, noteFromStored(created))
|
|
136
|
+
}
|
|
137
|
+
}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import type { SchemaSummary } from '../composables/useDatabaseSchemas'
|
|
2
|
+
import type { QueryTarget } from './target'
|
|
3
|
+
|
|
4
|
+
// What the editor completes, by place in the chain (D-12): the methods a
|
|
5
|
+
// query step answers, and the schemas, instances, tables and fields of the
|
|
6
|
+
// workspace, indexed so a table's fields are its own.
|
|
7
|
+
|
|
8
|
+
// Schema also exposes createInstance/destroyInstance/listInstances, but only
|
|
9
|
+
// instance(id?) continues the browse chain, so it is the only one offered.
|
|
10
|
+
export const SCHEMA_METHODS = ['instance']
|
|
11
|
+
export const INSTANCE_METHODS = ['table'] // a SchemaInstance only exposes table(name)
|
|
12
|
+
// The steps of a Table, Selection, Stream or Datum: each is a method of the
|
|
13
|
+
// staged query classes, so a completion never names one that does not exist.
|
|
14
|
+
export const QUERY_METHODS = [
|
|
15
|
+
'slice',
|
|
16
|
+
'orderBy',
|
|
17
|
+
'filter',
|
|
18
|
+
'map',
|
|
19
|
+
'pluck',
|
|
20
|
+
'without',
|
|
21
|
+
'get',
|
|
22
|
+
'getAll',
|
|
23
|
+
'between',
|
|
24
|
+
'count',
|
|
25
|
+
'distinct',
|
|
26
|
+
'nth',
|
|
27
|
+
'sum',
|
|
28
|
+
'avg',
|
|
29
|
+
'min',
|
|
30
|
+
'max',
|
|
31
|
+
'insert',
|
|
32
|
+
'update',
|
|
33
|
+
'replace',
|
|
34
|
+
'delete',
|
|
35
|
+
'do',
|
|
36
|
+
'default',
|
|
37
|
+
'key',
|
|
38
|
+
'lookup',
|
|
39
|
+
'changes',
|
|
40
|
+
]
|
|
41
|
+
|
|
42
|
+
export interface CompletionIndex {
|
|
43
|
+
schemaIds: string[]
|
|
44
|
+
tablesBySchema: Record<string, string[]>
|
|
45
|
+
/** Fields keyed `schema.table`. */
|
|
46
|
+
fieldsByTable: Record<string, string[]>
|
|
47
|
+
/** Every field of a schema's tables, when the chain names no table yet. */
|
|
48
|
+
fieldsBySchema: Record<string, string[]>
|
|
49
|
+
instancesBySchema: Record<string, string[]>
|
|
50
|
+
allTables: string[]
|
|
51
|
+
allFields: string[]
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function buildCompletionIndex(
|
|
55
|
+
schemas: SchemaSummary[],
|
|
56
|
+
): CompletionIndex {
|
|
57
|
+
const index: CompletionIndex = {
|
|
58
|
+
schemaIds: [],
|
|
59
|
+
tablesBySchema: {},
|
|
60
|
+
fieldsByTable: {},
|
|
61
|
+
fieldsBySchema: {},
|
|
62
|
+
instancesBySchema: {},
|
|
63
|
+
allTables: [],
|
|
64
|
+
allFields: [],
|
|
65
|
+
}
|
|
66
|
+
const allTables = new Set<string>()
|
|
67
|
+
const allFields = new Set<string>()
|
|
68
|
+
for (const schema of schemas) {
|
|
69
|
+
index.schemaIds.push(schema.id)
|
|
70
|
+
index.instancesBySchema[schema.id] = schema.instances ?? []
|
|
71
|
+
const tables: string[] = []
|
|
72
|
+
const schemaFields = new Set<string>()
|
|
73
|
+
for (const table of schema.tables ?? []) {
|
|
74
|
+
tables.push(table.name)
|
|
75
|
+
allTables.add(table.name)
|
|
76
|
+
const fields = Object.keys(table.fields ?? {})
|
|
77
|
+
index.fieldsByTable[`${schema.id}.${table.name}`] = fields
|
|
78
|
+
for (const field of fields) {
|
|
79
|
+
schemaFields.add(field)
|
|
80
|
+
allFields.add(field)
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
index.tablesBySchema[schema.id] = tables
|
|
84
|
+
index.fieldsBySchema[schema.id] = [...schemaFields]
|
|
85
|
+
}
|
|
86
|
+
index.allTables = [...allTables]
|
|
87
|
+
index.allFields = [...allFields]
|
|
88
|
+
return index
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/** The fields to offer where the chain stands: its table's, else wider. */
|
|
92
|
+
export function fieldsAt(index: CompletionIndex, chain: QueryTarget): string[] {
|
|
93
|
+
if (chain.schema && chain.table) {
|
|
94
|
+
const own = index.fieldsByTable[`${chain.schema}.${chain.table}`]
|
|
95
|
+
if (own) return own
|
|
96
|
+
}
|
|
97
|
+
if (chain.schema && index.fieldsBySchema[chain.schema])
|
|
98
|
+
return index.fieldsBySchema[chain.schema] as string[]
|
|
99
|
+
return index.allFields
|
|
100
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
// A result as CSV, the columns of every row in the order they first appear.
|
|
2
|
+
|
|
3
|
+
const NEEDS_QUOTES = /[",\n\r]/
|
|
4
|
+
|
|
5
|
+
function csvCell(value: unknown): string {
|
|
6
|
+
if (value === null || value === undefined) return ''
|
|
7
|
+
const text = typeof value === 'object' ? JSON.stringify(value) : String(value)
|
|
8
|
+
return NEEDS_QUOTES.test(text) ? `"${text.replace(/"/g, '""')}"` : text
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function resultColumns(rows: Record<string, unknown>[]): string[] {
|
|
12
|
+
const columns = new Set<string>()
|
|
13
|
+
for (const row of rows) for (const key of Object.keys(row)) columns.add(key)
|
|
14
|
+
return [...columns]
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function toCsv(rows: Record<string, unknown>[]): string {
|
|
18
|
+
const columns = resultColumns(rows)
|
|
19
|
+
const lines = [columns.map(csvCell).join(',')]
|
|
20
|
+
for (const row of rows)
|
|
21
|
+
lines.push(columns.map((column) => csvCell(row[column])).join(','))
|
|
22
|
+
return `${lines.join('\n')}\n`
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function downloadText(filename: string, text: string, type: string) {
|
|
26
|
+
const url = URL.createObjectURL(new Blob([text], { type }))
|
|
27
|
+
const link = document.createElement('a')
|
|
28
|
+
link.href = url
|
|
29
|
+
link.download = filename
|
|
30
|
+
link.click()
|
|
31
|
+
URL.revokeObjectURL(url)
|
|
32
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
// The example queries an empty result offers, built on the workspace's own
|
|
2
|
+
// tables (D-13).
|
|
3
|
+
|
|
4
|
+
interface ExampleTable {
|
|
5
|
+
name: string
|
|
6
|
+
fields?: Record<string, { kind?: string } | unknown>
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
// Field names that date a row, most telling first.
|
|
10
|
+
const DATE_FIELDS = [
|
|
11
|
+
'created_at',
|
|
12
|
+
'createdAt',
|
|
13
|
+
'created',
|
|
14
|
+
'inserted_at',
|
|
15
|
+
'updated_at',
|
|
16
|
+
'updatedAt',
|
|
17
|
+
'date',
|
|
18
|
+
]
|
|
19
|
+
|
|
20
|
+
function kindOf(descriptor: unknown): string | undefined {
|
|
21
|
+
const kind = (descriptor as { kind?: unknown } | null)?.kind
|
|
22
|
+
return typeof kind === 'string' ? kind : undefined
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* The field that orders a table's rows by age, for "Latest rows": a known
|
|
27
|
+
* creation-date name, else the first date field; null when none dates them.
|
|
28
|
+
*/
|
|
29
|
+
export function latestOrderField(table: ExampleTable): string | null {
|
|
30
|
+
const fields = table.fields ?? {}
|
|
31
|
+
const named = DATE_FIELDS.find((name) => Object.hasOwn(fields, name))
|
|
32
|
+
if (named) return named
|
|
33
|
+
return (
|
|
34
|
+
Object.entries(fields).find(
|
|
35
|
+
([, descriptor]) => kindOf(descriptor) === 'date',
|
|
36
|
+
)?.[0] ?? null
|
|
37
|
+
)
|
|
38
|
+
}
|