@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.
Files changed (173) hide show
  1. package/README.md +30 -5
  2. package/dist/db/models/query_history.model.d.ts +1 -0
  3. package/dist/db/models/query_history.model.js +6 -0
  4. package/dist/db/models/query_history.model.js.map +1 -1
  5. package/dist/db/tables/query_history.table.d.ts +3 -0
  6. package/dist/db/tables/query_history.table.js +12 -0
  7. package/dist/db/tables/query_history.table.js.map +1 -1
  8. package/dist/db/tables/saved_queries.table.d.ts +1 -0
  9. package/dist/db/tables/saved_queries.table.js +4 -0
  10. package/dist/db/tables/saved_queries.table.js.map +1 -1
  11. package/dist/i18n/messages.d.ts +39 -0
  12. package/dist/i18n/messages.js +64 -0
  13. package/dist/i18n/messages.js.map +1 -0
  14. package/dist/index.d.ts +1 -0
  15. package/dist/index.js +1 -7
  16. package/dist/index.js.map +1 -1
  17. package/dist/module.d.ts +11 -0
  18. package/dist/module.js +55 -0
  19. package/dist/module.js.map +1 -0
  20. package/dist/pages/data.d.ts +1 -1
  21. package/dist/pages/data.js +9 -6
  22. package/dist/pages/data.js.map +1 -1
  23. package/dist/pages/diagram.d.ts +9 -0
  24. package/dist/pages/diagram.js +45 -0
  25. package/dist/pages/diagram.js.map +1 -0
  26. package/dist/pages/index.d.ts +1 -0
  27. package/dist/pages/index.js +1 -0
  28. package/dist/pages/index.js.map +1 -1
  29. package/dist/pages/overview.d.ts +4 -1
  30. package/dist/pages/overview.js +113 -6
  31. package/dist/pages/overview.js.map +1 -1
  32. package/dist/pages/query.d.ts +1 -1
  33. package/dist/pages/query.js +22 -6
  34. package/dist/pages/query.js.map +1 -1
  35. package/dist/pages/schemas.d.ts +2 -1
  36. package/dist/pages/schemas.js +143 -6
  37. package/dist/pages/schemas.js.map +1 -1
  38. package/dist/routes/data.d.ts +8 -0
  39. package/dist/routes/data.js +84 -21
  40. package/dist/routes/data.js.map +1 -1
  41. package/dist/routes/index.d.ts +1 -0
  42. package/dist/routes/index.js +1 -0
  43. package/dist/routes/index.js.map +1 -1
  44. package/dist/routes/overview.d.ts +12 -0
  45. package/dist/routes/overview.js +55 -0
  46. package/dist/routes/overview.js.map +1 -0
  47. package/dist/routes/query.d.ts +5 -1
  48. package/dist/routes/query.js +162 -27
  49. package/dist/routes/query.js.map +1 -1
  50. package/dist/routes/tables.d.ts +15 -12
  51. package/dist/routes/tables.js +95 -55
  52. package/dist/routes/tables.js.map +1 -1
  53. package/dist/service/catalog.d.ts +3 -0
  54. package/dist/service/catalog.js +26 -0
  55. package/dist/service/catalog.js.map +1 -0
  56. package/dist/service/health.d.ts +3 -3
  57. package/dist/service/health.js +17 -14
  58. package/dist/service/health.js.map +1 -1
  59. package/dist/service/introspect.d.ts +3 -1
  60. package/dist/service/introspect.js +131 -43
  61. package/dist/service/introspect.js.map +1 -1
  62. package/dist/service/overview.d.ts +9 -0
  63. package/dist/service/overview.js +113 -0
  64. package/dist/service/overview.js.map +1 -0
  65. package/dist/service/queryInspection.d.ts +15 -0
  66. package/dist/service/queryInspection.js +107 -0
  67. package/dist/service/queryInspection.js.map +1 -0
  68. package/dist/service/references.d.ts +9 -0
  69. package/dist/service/references.js +52 -0
  70. package/dist/service/references.js.map +1 -0
  71. package/dist/service/schemaFilters.d.ts +26 -0
  72. package/dist/service/schemaFilters.js +42 -0
  73. package/dist/service/schemaFilters.js.map +1 -0
  74. package/dist/service/stagedSerialization.d.ts +2 -0
  75. package/dist/service/stagedSerialization.js +38 -10
  76. package/dist/service/stagedSerialization.js.map +1 -1
  77. package/dist/service/types.d.ts +11 -1
  78. package/dist/types/constants.d.ts +2 -0
  79. package/dist/types/constants.js +3 -1
  80. package/dist/types/constants.js.map +1 -1
  81. package/dist/utils/query.d.ts +0 -2
  82. package/dist/utils/query.js +0 -20
  83. package/dist/utils/query.js.map +1 -1
  84. package/frontend-vue/app/{composables → build/composables}/useDataBrowserGrid.ts +65 -50
  85. package/frontend-vue/app/build/composables/useDataBrowserTabs.ts +366 -0
  86. package/frontend-vue/app/build/composables/useDatabaseHealth.ts +50 -0
  87. package/frontend-vue/app/build/composables/useDatabaseSchemas.ts +140 -0
  88. package/frontend-vue/app/{composables → build/composables}/useDiagramPersistence.ts +47 -39
  89. package/frontend-vue/app/build/composables/useQueryRuntime.ts +217 -0
  90. package/frontend-vue/app/build/composables/useQueryStore.ts +162 -0
  91. package/frontend-vue/app/build/data/cellValues.ts +132 -0
  92. package/frontend-vue/app/build/data/instanceOptions.ts +36 -0
  93. package/frontend-vue/app/build/data/pickerMenus.ts +175 -0
  94. package/frontend-vue/app/build/data/recentTables.ts +48 -0
  95. package/frontend-vue/app/build/data/stagedEdits.ts +117 -0
  96. package/frontend-vue/app/build/data/stagedWrites.ts +64 -0
  97. package/frontend-vue/app/build/data/useStagedSave.ts +298 -0
  98. package/frontend-vue/app/build/diagram/autosave.ts +72 -0
  99. package/frontend-vue/app/build/diagram/graph.ts +585 -0
  100. package/frontend-vue/app/build/diagram/history.ts +178 -0
  101. package/frontend-vue/app/build/diagram/shortcuts.ts +62 -0
  102. package/frontend-vue/app/build/diagram/sync.ts +137 -0
  103. package/frontend-vue/app/build/query/completion.ts +100 -0
  104. package/frontend-vue/app/build/query/csv.ts +32 -0
  105. package/frontend-vue/app/build/query/examples.ts +38 -0
  106. package/frontend-vue/app/build/query/runs.ts +168 -0
  107. package/frontend-vue/app/build/query/tableColumns.ts +35 -0
  108. package/frontend-vue/app/build/query/target.ts +143 -0
  109. package/frontend-vue/app/build/schemas/filterState.ts +128 -0
  110. package/frontend-vue/app/build/schemas/useSchemasListing.ts +62 -0
  111. package/frontend-vue/app/build/utils/databaseLinks.ts +98 -0
  112. package/frontend-vue/app/build/utils/fieldTypes.ts +198 -0
  113. package/frontend-vue/app/build/utils/relativeTime.ts +19 -0
  114. package/frontend-vue/app/{utils → build/utils}/stagedSerialization.ts +7 -1
  115. package/frontend-vue/app/components/ConnectionStatus.vue +122 -0
  116. package/frontend-vue/app/components/DataBrowser.vue +126 -0
  117. package/frontend-vue/app/components/DataBrowserSidebar.vue +293 -210
  118. package/frontend-vue/app/components/DataBrowserTabBar.vue +185 -93
  119. package/frontend-vue/app/components/DataFilterBar.vue +163 -0
  120. package/frontend-vue/app/components/DataGrid.vue +737 -499
  121. package/frontend-vue/app/components/DataGridCell.vue +125 -77
  122. package/frontend-vue/app/components/DataGridHeaderCell.vue +119 -81
  123. package/frontend-vue/app/components/DiagramNoteNode.vue +81 -63
  124. package/frontend-vue/app/components/DiagramRelationEdge.vue +57 -27
  125. package/frontend-vue/app/components/DiagramStubNode.vue +16 -17
  126. package/frontend-vue/app/components/DiagramTableNode.vue +106 -68
  127. package/frontend-vue/app/components/InstancePicker.vue +195 -0
  128. package/frontend-vue/app/components/JsonCellPanel.vue +30 -29
  129. package/frontend-vue/app/components/QueryBuilderPanel.vue +290 -238
  130. package/frontend-vue/app/components/QueryConsole.vue +726 -0
  131. package/frontend-vue/app/components/QueryLibrary.vue +314 -0
  132. package/frontend-vue/app/components/QueryResultPanel.vue +556 -126
  133. package/frontend-vue/app/components/RowDrawer.vue +520 -0
  134. package/frontend-vue/app/components/SchemaDiagram.vue +1016 -1051
  135. package/frontend-vue/app/components/SchemaPicker.vue +86 -0
  136. package/frontend-vue/app/components/SchemasFilterBar.vue +201 -0
  137. package/frontend-vue/app/components/TableInspector.vue +741 -0
  138. package/frontend-vue/app/config/shortcuts-registry.ts +65 -0
  139. package/frontend-vue/dms.frontend.ts +5 -14
  140. package/frontend-vue/i18n/locales/database-en-GB.json +526 -193
  141. package/frontend-vue/i18n/locales/database-fr-FR.json +528 -195
  142. package/frontend-vue/package.json +2 -1
  143. package/frontend-vue/pnpm-lock.yaml +2601 -858
  144. package/frontend-vue/pnpm-workspace.yaml +1 -1
  145. package/frontend-vue/tests/dataBrowser.test.ts +206 -0
  146. package/frontend-vue/tests/diagram.test.ts +516 -0
  147. package/frontend-vue/tests/helpers.test.ts +205 -0
  148. package/frontend-vue/tests/instanceOptions.test.ts +53 -0
  149. package/frontend-vue/tests/locales.test.ts +56 -0
  150. package/frontend-vue/tests/persistence.test.ts +106 -5
  151. package/frontend-vue/tests/pickerMenus.test.ts +254 -0
  152. package/frontend-vue/tests/queryConsole.test.ts +282 -0
  153. package/frontend-vue/tests/queryRuntime.test.ts +2 -2
  154. package/frontend-vue/tests/registration.test.ts +36 -25
  155. package/frontend-vue/tests/schemasFilters.test.ts +115 -0
  156. package/package.json +9 -6
  157. package/frontend-vue/app/components/QueryFavoritesPanel.vue +0 -127
  158. package/frontend-vue/app/components/QueryHistoryPanel.vue +0 -134
  159. package/frontend-vue/app/components/SchemaTableInspector.vue +0 -278
  160. package/frontend-vue/app/composables/useDataBrowserTabs.ts +0 -255
  161. package/frontend-vue/app/composables/useDatabaseHealth.ts +0 -50
  162. package/frontend-vue/app/composables/useDatabaseSchemas.ts +0 -92
  163. package/frontend-vue/app/composables/useQueryRuntime.ts +0 -118
  164. package/frontend-vue/app/composables/useQueryStore.ts +0 -117
  165. package/frontend-vue/app/composables/useTableViewRowClick.ts +0 -22
  166. package/frontend-vue/app/custom-pages/database/data.vue +0 -41
  167. package/frontend-vue/app/custom-pages/database/overview.vue +0 -367
  168. package/frontend-vue/app/custom-pages/database/query.vue +0 -436
  169. package/frontend-vue/app/custom-pages/database/schemas.vue +0 -218
  170. package/frontend-vue/app/utils/databaseFieldModifiers.ts +0 -33
  171. package/frontend-vue/app/utils/databaseFieldTypeIcons.ts +0 -224
  172. package/frontend-vue/app/utils/dmsTableView.ts +0 -83
  173. package/frontend-vue/app/utils/relativeTime.ts +0 -18
@@ -0,0 +1,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
+ }