@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.
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 +55 -41
  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 +527 -193
  141. package/frontend-vue/i18n/locales/database-fr-FR.json +529 -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 +7 -7
  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,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, "text" | "x" | "y" | "width" | "height" | "color">
33
- >;
32
+ Pick<DiagramNote, 'text' | 'x' | 'y' | 'width' | 'height' | 'color'>
33
+ >
34
34
 
35
- const BASE = "/api/database/diagram";
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
- return res.positions ?? {};
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: "PUT",
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
- return res.items ?? [];
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: "POST",
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
- `${BASE}/notes/${encodeURIComponent(id)}`,
80
- { method: "PUT", body: patch },
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: "DELETE" },
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
+ }