@antelopejs/dms-database 0.0.14 → 0.1.1
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 +55 -41
- 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 +527 -193
- package/frontend-vue/i18n/locales/database-fr-FR.json +529 -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 +7 -7
- 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,50 @@
|
|
|
1
|
+
interface StorageEntry {
|
|
2
|
+
schema: string
|
|
3
|
+
table: string
|
|
4
|
+
rows: number
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
interface OverviewHealth {
|
|
8
|
+
status: 'ok' | 'down'
|
|
9
|
+
latencyMs: number | null
|
|
10
|
+
driver: string | null
|
|
11
|
+
checkedAt: string | null
|
|
12
|
+
error: string | null
|
|
13
|
+
schemaCount: number
|
|
14
|
+
tableCount: number
|
|
15
|
+
relationCount: number
|
|
16
|
+
indexCount: number
|
|
17
|
+
totalRows: number
|
|
18
|
+
storage: StorageEntry[]
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const HEALTH_ENDPOINT = '/api/database/health'
|
|
22
|
+
|
|
23
|
+
const EMPTY_HEALTH: OverviewHealth = {
|
|
24
|
+
status: 'ok',
|
|
25
|
+
latencyMs: null,
|
|
26
|
+
driver: null,
|
|
27
|
+
checkedAt: null,
|
|
28
|
+
error: null,
|
|
29
|
+
schemaCount: 0,
|
|
30
|
+
tableCount: 0,
|
|
31
|
+
relationCount: 0,
|
|
32
|
+
indexCount: 0,
|
|
33
|
+
totalRows: 0,
|
|
34
|
+
storage: [],
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function useDatabaseHealth() {
|
|
38
|
+
const { useFetchAuth } = useAuthFetch()
|
|
39
|
+
const { data, pending, error, refresh } = useFetchAuth<OverviewHealth>(
|
|
40
|
+
HEALTH_ENDPOINT,
|
|
41
|
+
{ default: () => ({ ...EMPTY_HEALTH }) },
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
return {
|
|
45
|
+
health: computed(() => data.value ?? { ...EMPTY_HEALTH }),
|
|
46
|
+
isLoading: pending,
|
|
47
|
+
error,
|
|
48
|
+
refresh,
|
|
49
|
+
}
|
|
50
|
+
}
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
interface RelationDeclaration {
|
|
2
|
+
fromField: string
|
|
3
|
+
toSchema: string
|
|
4
|
+
toTable: string
|
|
5
|
+
toField: string
|
|
6
|
+
many: boolean
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
type FieldDescriptor =
|
|
10
|
+
| { kind: 'string' }
|
|
11
|
+
| { kind: 'number' }
|
|
12
|
+
| { kind: 'boolean' }
|
|
13
|
+
| { kind: 'date' }
|
|
14
|
+
| { kind: 'unknown' }
|
|
15
|
+
| { kind: 'any' }
|
|
16
|
+
| { kind: 'null' }
|
|
17
|
+
| { kind: 'undefined' }
|
|
18
|
+
| { kind: 'literal'; value: string | number | boolean }
|
|
19
|
+
| { kind: 'union'; members: FieldDescriptor[] }
|
|
20
|
+
| { kind: 'intersection'; members: FieldDescriptor[] }
|
|
21
|
+
| { kind: 'array'; element?: FieldDescriptor }
|
|
22
|
+
| { kind: 'tuple'; elements: FieldDescriptor[] }
|
|
23
|
+
| {
|
|
24
|
+
kind: 'object'
|
|
25
|
+
fields: Record<string, FieldDescriptor>
|
|
26
|
+
partial?: boolean
|
|
27
|
+
}
|
|
28
|
+
| { kind: 'record'; key?: FieldDescriptor; value: FieldDescriptor }
|
|
29
|
+
| { kind: 'keyof'; keys: string[] }
|
|
30
|
+
| { kind: 'refinement'; base: FieldDescriptor; name: string }
|
|
31
|
+
| { kind: 'recursive'; name: string }
|
|
32
|
+
|
|
33
|
+
interface TableSummary {
|
|
34
|
+
name: string
|
|
35
|
+
fields: Record<string, FieldDescriptor>
|
|
36
|
+
indexes: Record<string, { fields?: string[]; multi?: boolean }>
|
|
37
|
+
relations: RelationDeclaration[]
|
|
38
|
+
modifiers?: Record<string, string[]>
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
type SchemaLabelColor =
|
|
42
|
+
| 'primary'
|
|
43
|
+
| 'secondary'
|
|
44
|
+
| 'success'
|
|
45
|
+
| 'info'
|
|
46
|
+
| 'warning'
|
|
47
|
+
| 'error'
|
|
48
|
+
| 'neutral'
|
|
49
|
+
|
|
50
|
+
/** A relation seen from the table it points to. */
|
|
51
|
+
interface InboundRelation extends RelationDeclaration {
|
|
52
|
+
fromSchema: string
|
|
53
|
+
fromTable: string
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
interface SchemaLabel {
|
|
57
|
+
text: string
|
|
58
|
+
color?: SchemaLabelColor
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
interface SchemaStats {
|
|
62
|
+
tableCount: number
|
|
63
|
+
elementCount: number
|
|
64
|
+
instanceCount: number
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
interface SchemaSummary {
|
|
68
|
+
id: string
|
|
69
|
+
options: Record<string, never>
|
|
70
|
+
tables: TableSummary[]
|
|
71
|
+
label?: SchemaLabel
|
|
72
|
+
stats: SchemaStats
|
|
73
|
+
instances: string[]
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const SCHEMAS_ENDPOINT = '/api/database/schemas'
|
|
77
|
+
const EMPTY_SCHEMAS = { schemas: [] as SchemaSummary[] }
|
|
78
|
+
|
|
79
|
+
/** A table of the given schemas, by schema id and table name. */
|
|
80
|
+
export function findTableIn(
|
|
81
|
+
schemas: SchemaSummary[],
|
|
82
|
+
schemaId: string,
|
|
83
|
+
tableName: string,
|
|
84
|
+
): TableSummary | undefined {
|
|
85
|
+
return schemas
|
|
86
|
+
.find((schema) => schema.id === schemaId)
|
|
87
|
+
?.tables.find((table) => table.name === tableName)
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/** Relations of every table that point at `schemaId.tableName`. */
|
|
91
|
+
export function inboundRelationsIn(
|
|
92
|
+
schemas: SchemaSummary[],
|
|
93
|
+
schemaId: string,
|
|
94
|
+
tableName: string,
|
|
95
|
+
): InboundRelation[] {
|
|
96
|
+
return schemas.flatMap((schema) =>
|
|
97
|
+
schema.tables.flatMap((table) =>
|
|
98
|
+
table.relations
|
|
99
|
+
.filter(
|
|
100
|
+
(relation) =>
|
|
101
|
+
relation.toSchema === schemaId && relation.toTable === tableName,
|
|
102
|
+
)
|
|
103
|
+
.map((relation) => ({
|
|
104
|
+
...relation,
|
|
105
|
+
fromSchema: schema.id,
|
|
106
|
+
fromTable: table.name,
|
|
107
|
+
})),
|
|
108
|
+
),
|
|
109
|
+
)
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export function useDatabaseSchemas() {
|
|
113
|
+
const { useFetchAuth } = useAuthFetch()
|
|
114
|
+
const { data, pending, error, status, refresh } = useFetchAuth<{
|
|
115
|
+
schemas: SchemaSummary[]
|
|
116
|
+
}>(SCHEMAS_ENDPOINT, { default: () => EMPTY_SCHEMAS })
|
|
117
|
+
|
|
118
|
+
const schemas = computed(() => data.value?.schemas ?? [])
|
|
119
|
+
|
|
120
|
+
return {
|
|
121
|
+
schemas,
|
|
122
|
+
findTable: (schemaId: string, tableName: string) =>
|
|
123
|
+
findTableIn(schemas.value, schemaId, tableName),
|
|
124
|
+
isLoading: pending,
|
|
125
|
+
/** The schemas arrived, or failed to: the list is what there is. */
|
|
126
|
+
isSettled: computed(
|
|
127
|
+
() => status.value === 'success' || status.value === 'error',
|
|
128
|
+
),
|
|
129
|
+
error,
|
|
130
|
+
refresh,
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
export type {
|
|
135
|
+
FieldDescriptor,
|
|
136
|
+
InboundRelation,
|
|
137
|
+
RelationDeclaration,
|
|
138
|
+
SchemaSummary,
|
|
139
|
+
TableSummary,
|
|
140
|
+
}
|
|
@@ -1,47 +1,51 @@
|
|
|
1
1
|
export interface DiagramPosition {
|
|
2
|
-
x: number
|
|
3
|
-
y: number
|
|
2
|
+
x: number
|
|
3
|
+
y: number
|
|
4
4
|
}
|
|
5
5
|
|
|
6
|
-
export type DiagramPositions = Record<string, DiagramPosition
|
|
6
|
+
export type DiagramPositions = Record<string, DiagramPosition>
|
|
7
7
|
|
|
8
8
|
export interface DiagramNote {
|
|
9
|
-
id: string
|
|
10
|
-
schemaName: string
|
|
11
|
-
text: string
|
|
12
|
-
x: number
|
|
13
|
-
y: number
|
|
14
|
-
width: number
|
|
15
|
-
height: number
|
|
16
|
-
color: string | null
|
|
17
|
-
createdAt: string
|
|
18
|
-
updatedAt: string
|
|
9
|
+
id: string
|
|
10
|
+
schemaName: string
|
|
11
|
+
text: string
|
|
12
|
+
x: number
|
|
13
|
+
y: number
|
|
14
|
+
width: number
|
|
15
|
+
height: number
|
|
16
|
+
color: string | null
|
|
17
|
+
createdAt: string
|
|
18
|
+
updatedAt: string
|
|
19
19
|
}
|
|
20
20
|
|
|
21
21
|
export interface DiagramNoteCreate {
|
|
22
|
-
schemaName: string
|
|
23
|
-
text: string
|
|
24
|
-
x: number
|
|
25
|
-
y: number
|
|
26
|
-
width: number
|
|
27
|
-
height: number
|
|
28
|
-
color?: string
|
|
22
|
+
schemaName: string
|
|
23
|
+
text: string
|
|
24
|
+
x: number
|
|
25
|
+
y: number
|
|
26
|
+
width: number
|
|
27
|
+
height: number
|
|
28
|
+
color?: string
|
|
29
29
|
}
|
|
30
30
|
|
|
31
31
|
export type DiagramNoteUpdate = Partial<
|
|
32
|
-
Pick<DiagramNote,
|
|
33
|
-
|
|
32
|
+
Pick<DiagramNote, 'text' | 'x' | 'y' | 'width' | 'height' | 'color'>
|
|
33
|
+
>
|
|
34
34
|
|
|
35
|
-
const BASE =
|
|
35
|
+
const BASE = '/api/database/diagram'
|
|
36
|
+
// A request that hangs fails after this (ofetch's own `timeout`), so the
|
|
37
|
+
// canvas shows the error and its "Try again" instead of "Saving…" forever.
|
|
38
|
+
const REQUEST_TIMEOUT_MS = 15_000
|
|
36
39
|
|
|
37
40
|
export function useDiagramPersistence() {
|
|
38
|
-
const { $authFetch } = useAuthFetch()
|
|
41
|
+
const { $authFetch } = useAuthFetch()
|
|
39
42
|
|
|
40
43
|
async function fetchLayout(schema: string): Promise<DiagramPositions> {
|
|
41
44
|
const res = await $authFetch<{ positions: DiagramPositions }>(
|
|
42
45
|
`${BASE}/layout/${encodeURIComponent(schema)}`,
|
|
43
|
-
|
|
44
|
-
|
|
46
|
+
{ timeout: REQUEST_TIMEOUT_MS },
|
|
47
|
+
)
|
|
48
|
+
return res.positions ?? {}
|
|
45
49
|
}
|
|
46
50
|
|
|
47
51
|
async function saveLayout(
|
|
@@ -51,41 +55,45 @@ export function useDiagramPersistence() {
|
|
|
51
55
|
await $authFetch<{ success: boolean }>(
|
|
52
56
|
`${BASE}/layout/${encodeURIComponent(schema)}`,
|
|
53
57
|
{
|
|
54
|
-
method:
|
|
58
|
+
method: 'PUT',
|
|
55
59
|
body: { positions },
|
|
60
|
+
timeout: REQUEST_TIMEOUT_MS,
|
|
56
61
|
},
|
|
57
|
-
)
|
|
62
|
+
)
|
|
58
63
|
}
|
|
59
64
|
|
|
60
65
|
async function fetchNotes(schema: string): Promise<DiagramNote[]> {
|
|
61
66
|
const res = await $authFetch<{ items: DiagramNote[] }>(`${BASE}/notes`, {
|
|
62
67
|
query: { schema },
|
|
63
|
-
|
|
64
|
-
|
|
68
|
+
timeout: REQUEST_TIMEOUT_MS,
|
|
69
|
+
})
|
|
70
|
+
return res.items ?? []
|
|
65
71
|
}
|
|
66
72
|
|
|
67
73
|
async function createNote(payload: DiagramNoteCreate): Promise<DiagramNote> {
|
|
68
74
|
return $authFetch<DiagramNote>(`${BASE}/notes`, {
|
|
69
|
-
method:
|
|
75
|
+
method: 'POST',
|
|
70
76
|
body: payload,
|
|
71
|
-
|
|
77
|
+
timeout: REQUEST_TIMEOUT_MS,
|
|
78
|
+
})
|
|
72
79
|
}
|
|
73
80
|
|
|
74
81
|
async function updateNote(
|
|
75
82
|
id: string,
|
|
76
83
|
patch: DiagramNoteUpdate,
|
|
77
84
|
): Promise<DiagramNote> {
|
|
78
|
-
return $authFetch<DiagramNote>(
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
85
|
+
return $authFetch<DiagramNote>(`${BASE}/notes/${encodeURIComponent(id)}`, {
|
|
86
|
+
method: 'PUT',
|
|
87
|
+
body: patch,
|
|
88
|
+
timeout: REQUEST_TIMEOUT_MS,
|
|
89
|
+
})
|
|
82
90
|
}
|
|
83
91
|
|
|
84
92
|
async function deleteNote(id: string): Promise<void> {
|
|
85
93
|
await $authFetch<{ success: boolean }>(
|
|
86
94
|
`${BASE}/notes/${encodeURIComponent(id)}`,
|
|
87
|
-
{ method:
|
|
88
|
-
)
|
|
95
|
+
{ method: 'DELETE', timeout: REQUEST_TIMEOUT_MS },
|
|
96
|
+
)
|
|
89
97
|
}
|
|
90
98
|
|
|
91
99
|
return {
|
|
@@ -95,5 +103,5 @@ export function useDiagramPersistence() {
|
|
|
95
103
|
createNote,
|
|
96
104
|
updateNote,
|
|
97
105
|
deleteNote,
|
|
98
|
-
}
|
|
106
|
+
}
|
|
99
107
|
}
|
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
import {
|
|
2
|
+
CROSS_INSTANCE,
|
|
3
|
+
Datum,
|
|
4
|
+
Query,
|
|
5
|
+
Schema,
|
|
6
|
+
SchemaInstance,
|
|
7
|
+
Selection,
|
|
8
|
+
SingleSelection,
|
|
9
|
+
Stream,
|
|
10
|
+
Table,
|
|
11
|
+
ValueProxy,
|
|
12
|
+
} from '@antelopejs/interface-database/staged-query'
|
|
13
|
+
import { javascriptLanguage } from '@codemirror/lang-javascript'
|
|
14
|
+
import { containsMutations, encodeStaged } from '../utils/stagedSerialization'
|
|
15
|
+
import type { SchemaSummary } from './useDatabaseSchemas'
|
|
16
|
+
|
|
17
|
+
interface PreparedQuery {
|
|
18
|
+
query: Record<string, unknown>
|
|
19
|
+
source: string
|
|
20
|
+
hasMutations: boolean
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
interface SchemaDefLike {
|
|
24
|
+
[tableName: string]: { fields: unknown; indexes: unknown }
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** Where a query text breaks (1-based line and column). */
|
|
28
|
+
export interface SyntaxPlace {
|
|
29
|
+
line: number
|
|
30
|
+
column: number
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export type QueryInputProblem =
|
|
34
|
+
| { kind: 'syntax'; place: SyntaxPlace | null; detail: string }
|
|
35
|
+
| { kind: 'unknown_schema'; schema: string }
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* A query the console could not send, for a reason it can name: a syntax
|
|
39
|
+
* error with its place, or a schema the workspace does not have. The message
|
|
40
|
+
* is English; the console words it from `problem`.
|
|
41
|
+
*/
|
|
42
|
+
export class QueryInputError extends Error {
|
|
43
|
+
readonly problem: QueryInputProblem
|
|
44
|
+
|
|
45
|
+
constructor(message: string, problem: QueryInputProblem) {
|
|
46
|
+
super(message)
|
|
47
|
+
this.name = 'QueryInputError'
|
|
48
|
+
this.problem = problem
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function toSchemaDefinition(summary: SchemaSummary): SchemaDefLike {
|
|
53
|
+
const def: SchemaDefLike = {}
|
|
54
|
+
for (const table of summary.tables) {
|
|
55
|
+
def[table.name] = {
|
|
56
|
+
fields: table.fields ?? {},
|
|
57
|
+
indexes: table.indexes ?? {},
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
return def
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function buildSchemasMap(
|
|
64
|
+
summaries: SchemaSummary[],
|
|
65
|
+
): Record<string, Schema<unknown, SchemaDefLike>> {
|
|
66
|
+
const bindings: Record<string, Schema<unknown, SchemaDefLike>> = {}
|
|
67
|
+
for (const summary of summaries) {
|
|
68
|
+
bindings[summary.id] =
|
|
69
|
+
Schema.get<unknown, SchemaDefLike>(summary.id) ??
|
|
70
|
+
new Schema(summary.id, toSchemaDefinition(summary))
|
|
71
|
+
}
|
|
72
|
+
return bindings
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Keys read off the schema map by the engine rather than by the query.
|
|
76
|
+
const IGNORED_KEYS = new Set(['then', 'toJSON'])
|
|
77
|
+
|
|
78
|
+
/** The schema map, naming an unknown schema instead of answering undefined. */
|
|
79
|
+
function guardSchemas<T extends object>(
|
|
80
|
+
bindings: Record<string, T>,
|
|
81
|
+
): Record<string, T> {
|
|
82
|
+
return new Proxy(bindings, {
|
|
83
|
+
get(target, key, receiver) {
|
|
84
|
+
if (
|
|
85
|
+
typeof key === 'string' &&
|
|
86
|
+
!(key in target) &&
|
|
87
|
+
!IGNORED_KEYS.has(key)
|
|
88
|
+
) {
|
|
89
|
+
throw new QueryInputError(`Unknown schema "${key}"`, {
|
|
90
|
+
kind: 'unknown_schema',
|
|
91
|
+
schema: key,
|
|
92
|
+
})
|
|
93
|
+
}
|
|
94
|
+
return Reflect.get(target, key, receiver)
|
|
95
|
+
},
|
|
96
|
+
})
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Where the JavaScript parser of the editor stops on a query text: the first
|
|
101
|
+
* error node of its tree; null when it parses.
|
|
102
|
+
*/
|
|
103
|
+
export function findSyntaxPlace(text: string): SyntaxPlace | null {
|
|
104
|
+
let at: number | null = null
|
|
105
|
+
javascriptLanguage.parser.parse(text).iterate({
|
|
106
|
+
enter(node) {
|
|
107
|
+
if (at !== null) return false
|
|
108
|
+
if (!node.type.isError) return undefined
|
|
109
|
+
at = node.from
|
|
110
|
+
return false
|
|
111
|
+
},
|
|
112
|
+
})
|
|
113
|
+
if (at === null) return null
|
|
114
|
+
const before = text.slice(0, at)
|
|
115
|
+
return {
|
|
116
|
+
line: before.split('\n').length,
|
|
117
|
+
column: at - before.lastIndexOf('\n'),
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
type QueryFunction = (...args: unknown[]) => unknown
|
|
122
|
+
|
|
123
|
+
function compile(code: string, text: string): QueryFunction {
|
|
124
|
+
const wrapped = `"use strict"; return (${code}\n);`
|
|
125
|
+
try {
|
|
126
|
+
return new Function(
|
|
127
|
+
'schemas',
|
|
128
|
+
'Schema',
|
|
129
|
+
'SchemaInstance',
|
|
130
|
+
'Table',
|
|
131
|
+
'Selection',
|
|
132
|
+
'SingleSelection',
|
|
133
|
+
'Stream',
|
|
134
|
+
'Datum',
|
|
135
|
+
'Query',
|
|
136
|
+
'ValueProxy',
|
|
137
|
+
'CROSS_INSTANCE',
|
|
138
|
+
wrapped,
|
|
139
|
+
) as QueryFunction
|
|
140
|
+
} catch (error) {
|
|
141
|
+
if (!(error instanceof SyntaxError)) throw error
|
|
142
|
+
// The engine's message points at the wrapper; the editor's parser tells
|
|
143
|
+
// where the text itself breaks.
|
|
144
|
+
const place = findSyntaxPlace(text)
|
|
145
|
+
const where = place ? ` at line ${place.line}, column ${place.column}` : ''
|
|
146
|
+
throw new QueryInputError(`Syntax error${where}: ${error.message}`, {
|
|
147
|
+
kind: 'syntax',
|
|
148
|
+
place,
|
|
149
|
+
detail: error.message,
|
|
150
|
+
})
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// `text` is the editor's, so a syntax problem points at its line and column.
|
|
155
|
+
function evaluateUserCode(
|
|
156
|
+
code: string,
|
|
157
|
+
text: string,
|
|
158
|
+
schemas: Record<string, Schema<unknown, SchemaDefLike>>,
|
|
159
|
+
): unknown {
|
|
160
|
+
return compile(code, text)(
|
|
161
|
+
guardSchemas(schemas),
|
|
162
|
+
Schema,
|
|
163
|
+
SchemaInstance,
|
|
164
|
+
Table,
|
|
165
|
+
Selection,
|
|
166
|
+
SingleSelection,
|
|
167
|
+
Stream,
|
|
168
|
+
Datum,
|
|
169
|
+
Query,
|
|
170
|
+
ValueProxy,
|
|
171
|
+
CROSS_INSTANCE,
|
|
172
|
+
)
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// A closing semicolon ends a statement, which the wrapped expression is not.
|
|
176
|
+
const TRAILING_SEMICOLONS = /[;\s]+$/
|
|
177
|
+
|
|
178
|
+
export function prepareQueryPayload(
|
|
179
|
+
editorText: string,
|
|
180
|
+
summaries: SchemaSummary[],
|
|
181
|
+
): PreparedQuery {
|
|
182
|
+
const trimmed = editorText.trim()
|
|
183
|
+
if (!trimmed) {
|
|
184
|
+
throw new Error('Query is empty')
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
const schemas = buildSchemasMap(summaries)
|
|
188
|
+
const result = evaluateUserCode(
|
|
189
|
+
trimmed.replace(TRAILING_SEMICOLONS, ''),
|
|
190
|
+
editorText,
|
|
191
|
+
schemas,
|
|
192
|
+
)
|
|
193
|
+
|
|
194
|
+
if (!result || typeof result !== 'object') {
|
|
195
|
+
throw new TypeError(
|
|
196
|
+
'Query must evaluate to a staged object (e.g. schemas.<id>.instance().table("...").slice(0, 10))',
|
|
197
|
+
)
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
const stages = (result as { stages?: unknown }).stages
|
|
201
|
+
if (!Array.isArray(stages)) {
|
|
202
|
+
throw new TypeError(
|
|
203
|
+
'Query result is not a staged object — check that the chain ends on a Table/Selection/Stream/Datum/Query.',
|
|
204
|
+
)
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
const encoded = encodeStaged(result)
|
|
208
|
+
if (!encoded || typeof encoded !== 'object' || Array.isArray(encoded)) {
|
|
209
|
+
throw new TypeError('Failed to encode query')
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
return {
|
|
213
|
+
query: encoded as Record<string, unknown>,
|
|
214
|
+
source: trimmed,
|
|
215
|
+
hasMutations: containsMutations(result),
|
|
216
|
+
}
|
|
217
|
+
}
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
export type QueryLanguage = 'aql'
|
|
2
|
+
export type QueryScope = 'me' | 'shared'
|
|
3
|
+
|
|
4
|
+
export interface SavedQuery {
|
|
5
|
+
id: string
|
|
6
|
+
userId: string
|
|
7
|
+
name: string
|
|
8
|
+
description: string
|
|
9
|
+
query: Record<string, unknown>
|
|
10
|
+
source: string
|
|
11
|
+
language: QueryLanguage
|
|
12
|
+
shared: boolean
|
|
13
|
+
createdAt: string
|
|
14
|
+
updatedAt: string
|
|
15
|
+
/** Who saved it, on the team's shared queries. */
|
|
16
|
+
ownerName?: string
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export type QueryRunStatus = 'ok' | 'error'
|
|
20
|
+
|
|
21
|
+
export interface HistoryEntry {
|
|
22
|
+
id: string
|
|
23
|
+
userId: string
|
|
24
|
+
query: Record<string, unknown>
|
|
25
|
+
source: string
|
|
26
|
+
language: QueryLanguage
|
|
27
|
+
executedAt: string
|
|
28
|
+
durationMs: number
|
|
29
|
+
rowCount: number
|
|
30
|
+
status: QueryRunStatus
|
|
31
|
+
/** Whether the query wrote. */
|
|
32
|
+
mutation: boolean
|
|
33
|
+
error?: string
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export interface ExecuteResult {
|
|
37
|
+
rows: Record<string, unknown>[]
|
|
38
|
+
/** Rows the query answered; above `rows.length` when they were cut. */
|
|
39
|
+
rowCount: number
|
|
40
|
+
truncated: boolean
|
|
41
|
+
mutation: boolean
|
|
42
|
+
executedAt: string
|
|
43
|
+
durationMs: number
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export type QueryOperation = 'read' | 'insert' | 'update' | 'replace' | 'delete'
|
|
47
|
+
|
|
48
|
+
/** What a query would write, measured before it runs. */
|
|
49
|
+
export interface DryRunResult {
|
|
50
|
+
operation: QueryOperation
|
|
51
|
+
schema?: string
|
|
52
|
+
instance?: string
|
|
53
|
+
table?: string
|
|
54
|
+
/** Null when the write is nested and cannot be measured on its own. */
|
|
55
|
+
rows: number | null
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export interface SaveQueryInput {
|
|
59
|
+
name: string
|
|
60
|
+
description: string
|
|
61
|
+
query: Record<string, unknown>
|
|
62
|
+
source: string
|
|
63
|
+
language: QueryLanguage
|
|
64
|
+
shared: boolean
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
import { HISTORY_KEPT } from '../query/runs'
|
|
68
|
+
|
|
69
|
+
const QUERY_BASE = '/api/database/query'
|
|
70
|
+
|
|
71
|
+
export function useQueryStore() {
|
|
72
|
+
const { $authFetch } = useAuthFetch()
|
|
73
|
+
|
|
74
|
+
const savedQueries = ref<SavedQuery[]>([])
|
|
75
|
+
const historyItems = ref<HistoryEntry[]>([])
|
|
76
|
+
|
|
77
|
+
async function fetchSaved(scope: QueryScope): Promise<void> {
|
|
78
|
+
const response = await $authFetch<{ items: SavedQuery[] }>(
|
|
79
|
+
`${QUERY_BASE}/saved`,
|
|
80
|
+
{ query: { scope } },
|
|
81
|
+
)
|
|
82
|
+
savedQueries.value = response.items ?? []
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
async function fetchHistoryEntry(id: string): Promise<HistoryEntry> {
|
|
86
|
+
return $authFetch<HistoryEntry>(
|
|
87
|
+
`${QUERY_BASE}/history/${encodeURIComponent(id)}`,
|
|
88
|
+
)
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
async function clearHistory(): Promise<void> {
|
|
92
|
+
await $authFetch<{ success: boolean }>(`${QUERY_BASE}/history`, {
|
|
93
|
+
method: 'DELETE',
|
|
94
|
+
})
|
|
95
|
+
historyItems.value = []
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
async function dryRun(query: Record<string, unknown>): Promise<DryRunResult> {
|
|
99
|
+
return $authFetch<DryRunResult>(`${QUERY_BASE}/dry-run`, {
|
|
100
|
+
method: 'POST',
|
|
101
|
+
body: { query },
|
|
102
|
+
})
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/** Every run the server keeps, at once. */
|
|
106
|
+
async function fetchHistory(): Promise<void> {
|
|
107
|
+
const response = await $authFetch<{ items: HistoryEntry[] }>(
|
|
108
|
+
`${QUERY_BASE}/history`,
|
|
109
|
+
{ query: { limit: HISTORY_KEPT } },
|
|
110
|
+
)
|
|
111
|
+
historyItems.value = response.items ?? []
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
async function executeQuery(
|
|
115
|
+
query: Record<string, unknown>,
|
|
116
|
+
source: string,
|
|
117
|
+
language: QueryLanguage,
|
|
118
|
+
): Promise<ExecuteResult> {
|
|
119
|
+
return $authFetch<ExecuteResult>(`${QUERY_BASE}/execute`, {
|
|
120
|
+
method: 'POST',
|
|
121
|
+
body: { query, source, language },
|
|
122
|
+
})
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
async function saveQuery(input: SaveQueryInput): Promise<SavedQuery> {
|
|
126
|
+
return $authFetch<SavedQuery>(`${QUERY_BASE}/saved`, {
|
|
127
|
+
method: 'POST',
|
|
128
|
+
body: input,
|
|
129
|
+
})
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
async function updateSaved(
|
|
133
|
+
id: string,
|
|
134
|
+
input: SaveQueryInput,
|
|
135
|
+
): Promise<SavedQuery> {
|
|
136
|
+
return $authFetch<SavedQuery>(
|
|
137
|
+
`${QUERY_BASE}/saved/${encodeURIComponent(id)}`,
|
|
138
|
+
{ method: 'PUT', body: input },
|
|
139
|
+
)
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
async function deleteSaved(id: string): Promise<void> {
|
|
143
|
+
await $authFetch<{ success: boolean }>(
|
|
144
|
+
`${QUERY_BASE}/saved/${encodeURIComponent(id)}`,
|
|
145
|
+
{ method: 'DELETE' },
|
|
146
|
+
)
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
return {
|
|
150
|
+
savedQueries,
|
|
151
|
+
historyItems,
|
|
152
|
+
fetchSaved,
|
|
153
|
+
fetchHistory,
|
|
154
|
+
fetchHistoryEntry,
|
|
155
|
+
clearHistory,
|
|
156
|
+
dryRun,
|
|
157
|
+
executeQuery,
|
|
158
|
+
saveQuery,
|
|
159
|
+
updateSaved,
|
|
160
|
+
deleteSaved,
|
|
161
|
+
}
|
|
162
|
+
}
|