@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,585 @@
|
|
|
1
|
+
import dagre from '@dagrejs/dagre'
|
|
2
|
+
import { MarkerType } from '@vue-flow/core'
|
|
3
|
+
import type {
|
|
4
|
+
SchemaSummary,
|
|
5
|
+
TableSummary,
|
|
6
|
+
} from '../composables/useDatabaseSchemas'
|
|
7
|
+
import type {
|
|
8
|
+
DiagramPosition,
|
|
9
|
+
DiagramPositions,
|
|
10
|
+
} from '../composables/useDiagramPersistence'
|
|
11
|
+
|
|
12
|
+
// Turns a schema into the nodes and edges of the diagram: one node per table,
|
|
13
|
+
// a dashed stub for a table a relation points to that is not drawn, the
|
|
14
|
+
// notes, and one edge per relation column. Table positions come from the
|
|
15
|
+
// saved layout; a table without one takes the place dagre gives it in the
|
|
16
|
+
// whole schema, so it stays put when the canvas focuses on a few tables.
|
|
17
|
+
|
|
18
|
+
export const NODE_WIDTH = 232
|
|
19
|
+
const NODE_HEIGHT_PER_FIELD = 24
|
|
20
|
+
const NODE_HEIGHT_HEADER = 44
|
|
21
|
+
export const STUB_NODE_WIDTH = 180
|
|
22
|
+
const STUB_NODE_HEIGHT = 28
|
|
23
|
+
const LAYOUT = { rankdir: 'LR', nodesep: 48, ranksep: 120 } as const
|
|
24
|
+
const HYSTERESIS_PX = 15
|
|
25
|
+
// Notes sit above the tables, so a new one is never hidden under a table.
|
|
26
|
+
export const NOTE_Z_INDEX = 10
|
|
27
|
+
// Arrow colours, as theme tokens so they follow the light and dark themes.
|
|
28
|
+
export const EDGE_COLOR = 'var(--ui-border-accented)'
|
|
29
|
+
export const EDGE_MANY_COLOR = 'var(--color-dms-500)'
|
|
30
|
+
export const EDGE_ACTIVE_COLOR = 'var(--ui-primary)'
|
|
31
|
+
|
|
32
|
+
export const TABLE_NODE_TYPE = 'tableNode'
|
|
33
|
+
export const STUB_NODE_TYPE = 'stubNode'
|
|
34
|
+
export const NOTE_NODE_TYPE = 'noteNode'
|
|
35
|
+
export const RELATION_EDGE_TYPE = 'relationEdge'
|
|
36
|
+
|
|
37
|
+
export interface NoteDraft {
|
|
38
|
+
id: string
|
|
39
|
+
schemaName: string
|
|
40
|
+
text: string
|
|
41
|
+
x: number
|
|
42
|
+
y: number
|
|
43
|
+
width: number
|
|
44
|
+
height: number
|
|
45
|
+
color: string | null
|
|
46
|
+
/** Not stored yet: created on the canvas, waiting for its save. */
|
|
47
|
+
isTemp: boolean
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export interface TableNodeData {
|
|
51
|
+
schemaId: string
|
|
52
|
+
tableName: string
|
|
53
|
+
table: TableSummary
|
|
54
|
+
/** Columns of the selected table's relations, drawn highlighted. */
|
|
55
|
+
highlighted: string[]
|
|
56
|
+
dimmed: boolean
|
|
57
|
+
/** Picked on the canvas, by a search or from a link. */
|
|
58
|
+
selected: boolean
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export interface NoteNodeData {
|
|
62
|
+
noteId: string
|
|
63
|
+
text: string
|
|
64
|
+
width: number
|
|
65
|
+
height: number
|
|
66
|
+
/** Opens the note in edit mode when it mounts: a note just created. */
|
|
67
|
+
autoEdit: boolean
|
|
68
|
+
onTextChange: (noteId: string, text: string) => void
|
|
69
|
+
onDelete: (noteId: string) => void
|
|
70
|
+
/** The note left edit mode, changed or not. */
|
|
71
|
+
onEditEnd: (noteId: string) => void
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export interface DiagramNode {
|
|
75
|
+
id: string
|
|
76
|
+
type: string
|
|
77
|
+
position: { x: number; y: number }
|
|
78
|
+
selectable?: boolean
|
|
79
|
+
draggable?: boolean
|
|
80
|
+
deletable?: boolean
|
|
81
|
+
zIndex?: number
|
|
82
|
+
data: Record<string, unknown>
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export interface EdgeEnds {
|
|
86
|
+
sourceBase: string
|
|
87
|
+
targetBase: string
|
|
88
|
+
isStubTarget: boolean
|
|
89
|
+
active: boolean
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export interface DiagramEdge {
|
|
93
|
+
id: string
|
|
94
|
+
type: typeof RELATION_EDGE_TYPE
|
|
95
|
+
source: string
|
|
96
|
+
target: string
|
|
97
|
+
sourceHandle: string
|
|
98
|
+
targetHandle: string
|
|
99
|
+
label: string
|
|
100
|
+
animated: boolean
|
|
101
|
+
markerEnd: { type: MarkerType; color: string }
|
|
102
|
+
data: EdgeEnds
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
type HandleSide = 'left' | 'right'
|
|
106
|
+
export interface Sides {
|
|
107
|
+
source: HandleSide
|
|
108
|
+
target: HandleSide
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/** Opens a table, in its own schema's diagram when it belongs to another. */
|
|
112
|
+
export type OpenTable = (schemaId: string, tableName: string) => void
|
|
113
|
+
|
|
114
|
+
/** Where dagre puts each table of a schema, and each stub of another one. */
|
|
115
|
+
export interface SchemaLayout {
|
|
116
|
+
tables: DiagramPositions
|
|
117
|
+
/** Keyed by stub node id. */
|
|
118
|
+
stubs: DiagramPositions
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
export interface GraphInput {
|
|
122
|
+
schema: SchemaSummary
|
|
123
|
+
positions: DiagramPositions
|
|
124
|
+
notes: NoteDraft[]
|
|
125
|
+
/** Node id of the selected table: its edges and columns light up. */
|
|
126
|
+
selectedId: string | null
|
|
127
|
+
/** When set, only these tables are drawn (a table and its neighbours). */
|
|
128
|
+
focusIds: Set<string> | null
|
|
129
|
+
noteHandlers: Pick<NoteNodeData, 'onTextChange' | 'onDelete' | 'onEditEnd'>
|
|
130
|
+
/** The note to open in edit mode: the one just created. */
|
|
131
|
+
editNoteId?: string | null
|
|
132
|
+
/** Opens the table a stub stands for. */
|
|
133
|
+
onOpenSchema: OpenTable
|
|
134
|
+
/** Sides each edge used last, so an edge does not flip on every drag. */
|
|
135
|
+
routing: Map<string, Sides>
|
|
136
|
+
/**
|
|
137
|
+
* Where dagre puts every table of the whole schema; computed when absent.
|
|
138
|
+
* The canvas keeps it per schema, so a render does not lay out again.
|
|
139
|
+
*/
|
|
140
|
+
layout?: SchemaLayout
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
export interface BuiltGraph {
|
|
144
|
+
nodes: DiagramNode[]
|
|
145
|
+
edges: DiagramEdge[]
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
export function tableNodeId(schemaId: string, tableName: string): string {
|
|
149
|
+
return `${schemaId}::${tableName}`
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function stubNodeId(schemaId: string, tableName: string): string {
|
|
153
|
+
return `stub::${schemaId}::${tableName}`
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
export function noteNodeId(noteId: string): string {
|
|
157
|
+
return `note::${noteId}`
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
export function nodeHeight(table: TableSummary): number {
|
|
161
|
+
return (
|
|
162
|
+
NODE_HEIGHT_HEADER +
|
|
163
|
+
Object.keys(table.fields).length * NODE_HEIGHT_PER_FIELD
|
|
164
|
+
)
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/** Ids of a table and of every table it points to or is pointed at from. */
|
|
168
|
+
export function neighbourIds(
|
|
169
|
+
schema: SchemaSummary,
|
|
170
|
+
tableName: string,
|
|
171
|
+
): Set<string> {
|
|
172
|
+
const ids = new Set([tableNodeId(schema.id, tableName)])
|
|
173
|
+
for (const table of schema.tables) {
|
|
174
|
+
for (const relation of table.relations) {
|
|
175
|
+
if (table.name === tableName)
|
|
176
|
+
ids.add(tableNodeId(relation.toSchema, relation.toTable))
|
|
177
|
+
if (relation.toSchema === schema.id && relation.toTable === tableName) {
|
|
178
|
+
ids.add(tableNodeId(schema.id, table.name))
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
return ids
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* Picks the handle sides of an edge from the horizontal distance between its
|
|
187
|
+
* nodes, keeping the previous sides within a small margin.
|
|
188
|
+
*/
|
|
189
|
+
export function pickSides(dx: number, width: number, previous?: Sides): Sides {
|
|
190
|
+
const pure: Sides =
|
|
191
|
+
dx >= width
|
|
192
|
+
? { source: 'right', target: 'left' }
|
|
193
|
+
: dx >= 0
|
|
194
|
+
? { source: 'right', target: 'right' }
|
|
195
|
+
: dx >= -width
|
|
196
|
+
? { source: 'left', target: 'left' }
|
|
197
|
+
: { source: 'left', target: 'right' }
|
|
198
|
+
if (!previous) return pure
|
|
199
|
+
const keepRanges: Record<string, [number, number]> = {
|
|
200
|
+
'right-left': [width - HYSTERESIS_PX, Infinity],
|
|
201
|
+
'right-right': [-HYSTERESIS_PX, width + HYSTERESIS_PX],
|
|
202
|
+
'left-left': [-width - HYSTERESIS_PX, HYSTERESIS_PX],
|
|
203
|
+
'left-right': [-Infinity, -width + HYSTERESIS_PX],
|
|
204
|
+
}
|
|
205
|
+
const [min, max] = keepRanges[`${previous.source}-${previous.target}`] ?? [
|
|
206
|
+
0, 0,
|
|
207
|
+
]
|
|
208
|
+
return dx > min && dx < max ? previous : pure
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
interface PendingEdge {
|
|
212
|
+
id: string
|
|
213
|
+
sourceId: string
|
|
214
|
+
targetId: string
|
|
215
|
+
ends: EdgeEnds
|
|
216
|
+
label: string
|
|
217
|
+
many: boolean
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/** A node as dagre lays it out: its centre and size. */
|
|
221
|
+
interface LaidNode {
|
|
222
|
+
x: number
|
|
223
|
+
y: number
|
|
224
|
+
width: number
|
|
225
|
+
height: number
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
interface StubTarget {
|
|
229
|
+
schema: string
|
|
230
|
+
table: string
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
interface Relations {
|
|
234
|
+
stubs: Map<string, StubTarget>
|
|
235
|
+
pending: PendingEdge[]
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
function relationEdges(
|
|
239
|
+
schema: SchemaSummary,
|
|
240
|
+
selectedId: string | null,
|
|
241
|
+
visible: Set<string>,
|
|
242
|
+
): Relations {
|
|
243
|
+
const stubs = new Map<string, StubTarget>()
|
|
244
|
+
const pending: PendingEdge[] = []
|
|
245
|
+
for (const table of schema.tables) {
|
|
246
|
+
const sourceId = tableNodeId(schema.id, table.name)
|
|
247
|
+
if (!visible.has(sourceId)) continue
|
|
248
|
+
for (const relation of table.relations) {
|
|
249
|
+
const realTargetId = tableNodeId(relation.toSchema, relation.toTable)
|
|
250
|
+
const isStubTarget = !visible.has(realTargetId)
|
|
251
|
+
const targetId = isStubTarget
|
|
252
|
+
? stubNodeId(relation.toSchema, relation.toTable)
|
|
253
|
+
: realTargetId
|
|
254
|
+
if (isStubTarget && !stubs.has(targetId)) {
|
|
255
|
+
stubs.set(targetId, {
|
|
256
|
+
schema: relation.toSchema,
|
|
257
|
+
table: relation.toTable,
|
|
258
|
+
})
|
|
259
|
+
}
|
|
260
|
+
const target = schema.tables.find((t) => t.name === relation.toTable)
|
|
261
|
+
pending.push({
|
|
262
|
+
id: `${sourceId}::${relation.fromField}->${realTargetId}`,
|
|
263
|
+
sourceId,
|
|
264
|
+
targetId,
|
|
265
|
+
label: relation.fromField,
|
|
266
|
+
many: relation.many,
|
|
267
|
+
ends: {
|
|
268
|
+
sourceBase:
|
|
269
|
+
relation.fromField in table.fields
|
|
270
|
+
? relation.fromField
|
|
271
|
+
: '__node__',
|
|
272
|
+
targetBase: isStubTarget
|
|
273
|
+
? 'stub'
|
|
274
|
+
: target && relation.toField in target.fields
|
|
275
|
+
? relation.toField
|
|
276
|
+
: '__node__',
|
|
277
|
+
isStubTarget,
|
|
278
|
+
active:
|
|
279
|
+
selectedId !== null &&
|
|
280
|
+
(selectedId === sourceId || selectedId === realTargetId),
|
|
281
|
+
},
|
|
282
|
+
})
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
return { stubs, pending }
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
/**
|
|
289
|
+
* Lays out the whole schema with dagre: every table, and a stub for each
|
|
290
|
+
* table of another schema a relation points to.
|
|
291
|
+
*/
|
|
292
|
+
export function layOutSchema(schema: SchemaSummary): SchemaLayout {
|
|
293
|
+
const graph = new dagre.graphlib.Graph()
|
|
294
|
+
graph.setDefaultEdgeLabel(() => ({}))
|
|
295
|
+
graph.setGraph({ ...LAYOUT })
|
|
296
|
+
const all = new Set<string>()
|
|
297
|
+
for (const table of schema.tables) {
|
|
298
|
+
const id = tableNodeId(schema.id, table.name)
|
|
299
|
+
all.add(id)
|
|
300
|
+
graph.setNode(id, { width: NODE_WIDTH, height: nodeHeight(table) })
|
|
301
|
+
}
|
|
302
|
+
const { stubs, pending } = relationEdges(schema, null, all)
|
|
303
|
+
for (const id of stubs.keys())
|
|
304
|
+
graph.setNode(id, { width: STUB_NODE_WIDTH, height: STUB_NODE_HEIGHT })
|
|
305
|
+
for (const edge of pending) graph.setEdge(edge.sourceId, edge.targetId)
|
|
306
|
+
dagre.layout(graph)
|
|
307
|
+
const topLeft = (id: string): DiagramPosition => {
|
|
308
|
+
const laid = graph.node(id) as LaidNode
|
|
309
|
+
return { x: laid.x - laid.width / 2, y: laid.y - laid.height / 2 }
|
|
310
|
+
}
|
|
311
|
+
const tables: DiagramPositions = {}
|
|
312
|
+
for (const table of schema.tables)
|
|
313
|
+
tables[table.name] = topLeft(tableNodeId(schema.id, table.name))
|
|
314
|
+
const stubPositions: DiagramPositions = {}
|
|
315
|
+
for (const id of stubs.keys()) stubPositions[id] = topLeft(id)
|
|
316
|
+
return { tables, stubs: stubPositions }
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
/**
|
|
320
|
+
* The columns of `table` lit up by the selection: its relation columns when it
|
|
321
|
+
* or the table they point to is selected, and the columns relations point at
|
|
322
|
+
* when it or the table they come from is selected.
|
|
323
|
+
*/
|
|
324
|
+
function highlightedColumns(input: GraphInput, table: TableSummary): string[] {
|
|
325
|
+
const selected = input.selectedId
|
|
326
|
+
if (!selected) return []
|
|
327
|
+
const { schema } = input
|
|
328
|
+
const own = tableNodeId(schema.id, table.name)
|
|
329
|
+
const columns = new Set<string>()
|
|
330
|
+
for (const relation of table.relations) {
|
|
331
|
+
const target = tableNodeId(relation.toSchema, relation.toTable)
|
|
332
|
+
if (own === selected || target === selected) columns.add(relation.fromField)
|
|
333
|
+
}
|
|
334
|
+
for (const other of schema.tables) {
|
|
335
|
+
const from = tableNodeId(schema.id, other.name)
|
|
336
|
+
for (const relation of other.relations) {
|
|
337
|
+
const pointsHere =
|
|
338
|
+
relation.toSchema === schema.id && relation.toTable === table.name
|
|
339
|
+
if (pointsHere && (from === selected || own === selected))
|
|
340
|
+
columns.add(relation.toField)
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
return [...columns]
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
function tablePosition(
|
|
347
|
+
input: GraphInput,
|
|
348
|
+
layout: SchemaLayout,
|
|
349
|
+
tableName: string,
|
|
350
|
+
): DiagramPosition {
|
|
351
|
+
return (
|
|
352
|
+
input.positions[tableName] ?? layout.tables[tableName] ?? { x: 0, y: 0 }
|
|
353
|
+
)
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
function tableNodes(
|
|
357
|
+
input: GraphInput,
|
|
358
|
+
layout: SchemaLayout,
|
|
359
|
+
visible: Set<string>,
|
|
360
|
+
): DiagramNode[] {
|
|
361
|
+
return input.schema.tables
|
|
362
|
+
.filter((table) => visible.has(tableNodeId(input.schema.id, table.name)))
|
|
363
|
+
.map((table) => {
|
|
364
|
+
const id = tableNodeId(input.schema.id, table.name)
|
|
365
|
+
const highlighted = highlightedColumns(input, table)
|
|
366
|
+
const data: TableNodeData = {
|
|
367
|
+
schemaId: input.schema.id,
|
|
368
|
+
tableName: table.name,
|
|
369
|
+
table,
|
|
370
|
+
highlighted,
|
|
371
|
+
selected: input.selectedId === id,
|
|
372
|
+
// Tables unrelated to the selected one step back.
|
|
373
|
+
dimmed:
|
|
374
|
+
input.selectedId !== null &&
|
|
375
|
+
input.selectedId !== id &&
|
|
376
|
+
highlighted.length === 0,
|
|
377
|
+
}
|
|
378
|
+
return {
|
|
379
|
+
id,
|
|
380
|
+
type: TABLE_NODE_TYPE,
|
|
381
|
+
position: { ...tablePosition(input, layout, table.name) },
|
|
382
|
+
// The canvas keeps its own selection (`data.selected`), and a
|
|
383
|
+
// table leaves the diagram only with its schema.
|
|
384
|
+
selectable: false,
|
|
385
|
+
deletable: false,
|
|
386
|
+
data: data as unknown as Record<string, unknown>,
|
|
387
|
+
}
|
|
388
|
+
})
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
export interface StubNodeData {
|
|
392
|
+
targetSchema: string
|
|
393
|
+
targetTable: string
|
|
394
|
+
onOpenSchema: OpenTable
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
/**
|
|
398
|
+
* A stub for a table of this schema left out by the focus sits where that
|
|
399
|
+
* table is; one for another schema takes the place dagre gives it.
|
|
400
|
+
*/
|
|
401
|
+
function stubPosition(
|
|
402
|
+
input: GraphInput,
|
|
403
|
+
layout: SchemaLayout,
|
|
404
|
+
id: string,
|
|
405
|
+
target: StubTarget,
|
|
406
|
+
): DiagramPosition {
|
|
407
|
+
if (target.schema === input.schema.id)
|
|
408
|
+
return tablePosition(input, layout, target.table)
|
|
409
|
+
return layout.stubs[id] ?? { x: 0, y: 0 }
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
function stubNodes(
|
|
413
|
+
input: GraphInput,
|
|
414
|
+
layout: SchemaLayout,
|
|
415
|
+
stubs: Map<string, StubTarget>,
|
|
416
|
+
): DiagramNode[] {
|
|
417
|
+
return [...stubs].map(([id, target]) => ({
|
|
418
|
+
id,
|
|
419
|
+
type: STUB_NODE_TYPE,
|
|
420
|
+
position: { ...stubPosition(input, layout, id, target) },
|
|
421
|
+
selectable: false,
|
|
422
|
+
draggable: false,
|
|
423
|
+
deletable: false,
|
|
424
|
+
data: {
|
|
425
|
+
targetSchema: target.schema,
|
|
426
|
+
targetTable: target.table,
|
|
427
|
+
onOpenSchema: input.onOpenSchema,
|
|
428
|
+
} satisfies StubNodeData as unknown as Record<string, unknown>,
|
|
429
|
+
}))
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
function noteNodes(input: GraphInput): DiagramNode[] {
|
|
433
|
+
return input.notes.map((note) => {
|
|
434
|
+
const data: NoteNodeData = {
|
|
435
|
+
noteId: note.id,
|
|
436
|
+
text: note.text,
|
|
437
|
+
width: note.width,
|
|
438
|
+
height: note.height,
|
|
439
|
+
autoEdit: input.editNoteId === note.id,
|
|
440
|
+
...input.noteHandlers,
|
|
441
|
+
}
|
|
442
|
+
return {
|
|
443
|
+
id: noteNodeId(note.id),
|
|
444
|
+
type: NOTE_NODE_TYPE,
|
|
445
|
+
position: { x: note.x, y: note.y },
|
|
446
|
+
zIndex: NOTE_Z_INDEX,
|
|
447
|
+
data: data as unknown as Record<string, unknown>,
|
|
448
|
+
}
|
|
449
|
+
})
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
function nodeWidth(node: DiagramNode): number {
|
|
453
|
+
return node.type === STUB_NODE_TYPE ? STUB_NODE_WIDTH : NODE_WIDTH
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
function edgeColor(edge: PendingEdge): string {
|
|
457
|
+
if (edge.ends.active) return EDGE_ACTIVE_COLOR
|
|
458
|
+
return edge.many ? EDGE_MANY_COLOR : EDGE_COLOR
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
function routeEdges(
|
|
462
|
+
input: GraphInput,
|
|
463
|
+
pending: PendingEdge[],
|
|
464
|
+
nodes: DiagramNode[],
|
|
465
|
+
): DiagramEdge[] {
|
|
466
|
+
const byId = new Map(nodes.map((node) => [node.id, node]))
|
|
467
|
+
return pending.flatMap((edge) => {
|
|
468
|
+
const source = byId.get(edge.sourceId)
|
|
469
|
+
const target = byId.get(edge.targetId)
|
|
470
|
+
if (!source || !target) return []
|
|
471
|
+
const dx =
|
|
472
|
+
target.position.x +
|
|
473
|
+
nodeWidth(target) / 2 -
|
|
474
|
+
(source.position.x + nodeWidth(source) / 2)
|
|
475
|
+
const sides = pickSides(dx, NODE_WIDTH, input.routing.get(edge.id))
|
|
476
|
+
input.routing.set(edge.id, sides)
|
|
477
|
+
return [
|
|
478
|
+
{
|
|
479
|
+
id: edge.id,
|
|
480
|
+
type: RELATION_EDGE_TYPE,
|
|
481
|
+
source: edge.sourceId,
|
|
482
|
+
target: edge.targetId,
|
|
483
|
+
sourceHandle: `${edge.ends.sourceBase}::${sides.source}::source`,
|
|
484
|
+
targetHandle: `${edge.ends.targetBase}::${sides.target}::target`,
|
|
485
|
+
label: edge.label,
|
|
486
|
+
animated: edge.many,
|
|
487
|
+
markerEnd: { type: MarkerType.ArrowClosed, color: edgeColor(edge) },
|
|
488
|
+
data: edge.ends,
|
|
489
|
+
},
|
|
490
|
+
]
|
|
491
|
+
})
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
export function buildGraph(input: GraphInput): BuiltGraph {
|
|
495
|
+
const visible = new Set(
|
|
496
|
+
input.schema.tables
|
|
497
|
+
.map((table) => tableNodeId(input.schema.id, table.name))
|
|
498
|
+
.filter((id) => !input.focusIds || input.focusIds.has(id)),
|
|
499
|
+
)
|
|
500
|
+
const layout = input.layout ?? layOutSchema(input.schema)
|
|
501
|
+
const { stubs, pending } = relationEdges(
|
|
502
|
+
input.schema,
|
|
503
|
+
input.selectedId,
|
|
504
|
+
visible,
|
|
505
|
+
)
|
|
506
|
+
const nodes = [
|
|
507
|
+
...tableNodes(input, layout, visible),
|
|
508
|
+
...stubNodes(input, layout, stubs),
|
|
509
|
+
...noteNodes(input),
|
|
510
|
+
]
|
|
511
|
+
return { nodes, edges: routeEdges(input, pending, nodes) }
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
/**
|
|
515
|
+
* Tables a search finds: those whose name holds `needle`, then those with a
|
|
516
|
+
* column whose name holds it, each once.
|
|
517
|
+
*/
|
|
518
|
+
export function searchTables(schema: SchemaSummary, needle: string): string[] {
|
|
519
|
+
const text = needle.trim().toLowerCase()
|
|
520
|
+
if (!text) return []
|
|
521
|
+
const byName = schema.tables.filter((table) =>
|
|
522
|
+
table.name.toLowerCase().includes(text),
|
|
523
|
+
)
|
|
524
|
+
const byColumn = schema.tables.filter(
|
|
525
|
+
(table) =>
|
|
526
|
+
!byName.includes(table) &&
|
|
527
|
+
Object.keys(table.fields).some((field) =>
|
|
528
|
+
field.toLowerCase().includes(text),
|
|
529
|
+
),
|
|
530
|
+
)
|
|
531
|
+
return [...byName, ...byColumn].map((table) => table.name)
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
export interface Rect {
|
|
535
|
+
x: number
|
|
536
|
+
y: number
|
|
537
|
+
width: number
|
|
538
|
+
height: number
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
function overlaps(a: Rect, b: Rect, margin: number): boolean {
|
|
542
|
+
return (
|
|
543
|
+
a.x < b.x + b.width + margin &&
|
|
544
|
+
b.x < a.x + a.width + margin &&
|
|
545
|
+
a.y < b.y + b.height + margin &&
|
|
546
|
+
b.y < a.y + a.height + margin
|
|
547
|
+
)
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
const FREE_SPOT_STEP = 40
|
|
551
|
+
const FREE_SPOT_RINGS = 12
|
|
552
|
+
const FREE_SPOT_MARGIN = 16
|
|
553
|
+
|
|
554
|
+
/**
|
|
555
|
+
* The spot nearest `wanted` where a box of `size` covers none of `taken`,
|
|
556
|
+
* searched ring by ring around it; `wanted` itself when none is free.
|
|
557
|
+
*/
|
|
558
|
+
export function freeSpot(
|
|
559
|
+
wanted: DiagramPosition,
|
|
560
|
+
size: { width: number; height: number },
|
|
561
|
+
taken: Rect[],
|
|
562
|
+
): DiagramPosition {
|
|
563
|
+
const fits = (x: number, y: number) =>
|
|
564
|
+
!taken.some((rect) => overlaps({ x, y, ...size }, rect, FREE_SPOT_MARGIN))
|
|
565
|
+
for (let ring = 0; ring <= FREE_SPOT_RINGS; ring++) {
|
|
566
|
+
const candidates: DiagramPosition[] = []
|
|
567
|
+
for (let i = -ring; i <= ring; i++) {
|
|
568
|
+
for (let j = -ring; j <= ring; j++) {
|
|
569
|
+
if (Math.max(Math.abs(i), Math.abs(j)) !== ring) continue
|
|
570
|
+
candidates.push({
|
|
571
|
+
x: wanted.x + i * FREE_SPOT_STEP,
|
|
572
|
+
y: wanted.y + j * FREE_SPOT_STEP,
|
|
573
|
+
})
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
candidates.sort(
|
|
577
|
+
(a, b) =>
|
|
578
|
+
Math.hypot(a.x - wanted.x, a.y - wanted.y) -
|
|
579
|
+
Math.hypot(b.x - wanted.x, b.y - wanted.y),
|
|
580
|
+
)
|
|
581
|
+
const free = candidates.find((point) => fits(point.x, point.y))
|
|
582
|
+
if (free) return free
|
|
583
|
+
}
|
|
584
|
+
return wanted
|
|
585
|
+
}
|