@antelopejs/dms-database 0.0.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 (164) hide show
  1. package/LICENSE +190 -0
  2. package/README.md +53 -0
  3. package/dist/automation/index.d.ts +2 -0
  4. package/dist/automation/index.js +13 -0
  5. package/dist/automation/index.js.map +1 -0
  6. package/dist/automation/tableWatch.d.ts +17 -0
  7. package/dist/automation/tableWatch.js +189 -0
  8. package/dist/automation/tableWatch.js.map +1 -0
  9. package/dist/config.d.ts +7 -0
  10. package/dist/config.js +12 -0
  11. package/dist/config.js.map +1 -0
  12. package/dist/db/index.d.ts +2 -0
  13. package/dist/db/index.js +19 -0
  14. package/dist/db/index.js.map +1 -0
  15. package/dist/db/models/diagram_layouts.model.d.ts +25 -0
  16. package/dist/db/models/diagram_layouts.model.js +29 -0
  17. package/dist/db/models/diagram_layouts.model.js.map +1 -0
  18. package/dist/db/models/diagram_notes.model.d.ts +23 -0
  19. package/dist/db/models/diagram_notes.model.js +15 -0
  20. package/dist/db/models/diagram_notes.model.js.map +1 -0
  21. package/dist/db/models/index.d.ts +4 -0
  22. package/dist/db/models/index.js +21 -0
  23. package/dist/db/models/index.js.map +1 -0
  24. package/dist/db/models/query_history.model.d.ts +25 -0
  25. package/dist/db/models/query_history.model.js +33 -0
  26. package/dist/db/models/query_history.model.js.map +1 -0
  27. package/dist/db/models/saved_queries.model.d.ts +24 -0
  28. package/dist/db/models/saved_queries.model.js +21 -0
  29. package/dist/db/models/saved_queries.model.js.map +1 -0
  30. package/dist/db/tables/diagram_layouts.table.d.ts +7 -0
  31. package/dist/db/tables/diagram_layouts.table.js +35 -0
  32. package/dist/db/tables/diagram_layouts.table.js.map +1 -0
  33. package/dist/db/tables/diagram_notes.table.d.ts +13 -0
  34. package/dist/db/tables/diagram_notes.table.js +59 -0
  35. package/dist/db/tables/diagram_notes.table.js.map +1 -0
  36. package/dist/db/tables/index.d.ts +4 -0
  37. package/dist/db/tables/index.js +21 -0
  38. package/dist/db/tables/index.js.map +1 -0
  39. package/dist/db/tables/query_history.table.d.ts +11 -0
  40. package/dist/db/tables/query_history.table.js +54 -0
  41. package/dist/db/tables/query_history.table.js.map +1 -0
  42. package/dist/db/tables/saved_queries.table.d.ts +12 -0
  43. package/dist/db/tables/saved_queries.table.js +58 -0
  44. package/dist/db/tables/saved_queries.table.js.map +1 -0
  45. package/dist/index.d.ts +8 -0
  46. package/dist/index.js +56 -0
  47. package/dist/index.js.map +1 -0
  48. package/dist/pages/data.d.ts +9 -0
  49. package/dist/pages/data.js +29 -0
  50. package/dist/pages/data.js.map +1 -0
  51. package/dist/pages/index.d.ts +4 -0
  52. package/dist/pages/index.js +21 -0
  53. package/dist/pages/index.js.map +1 -0
  54. package/dist/pages/overview.d.ts +9 -0
  55. package/dist/pages/overview.js +29 -0
  56. package/dist/pages/overview.js.map +1 -0
  57. package/dist/pages/query.d.ts +9 -0
  58. package/dist/pages/query.js +29 -0
  59. package/dist/pages/query.js.map +1 -0
  60. package/dist/pages/schemas.d.ts +9 -0
  61. package/dist/pages/schemas.js +29 -0
  62. package/dist/pages/schemas.js.map +1 -0
  63. package/dist/routes/data.d.ts +25 -0
  64. package/dist/routes/data.js +324 -0
  65. package/dist/routes/data.js.map +1 -0
  66. package/dist/routes/diagram.d.ts +47 -0
  67. package/dist/routes/diagram.js +200 -0
  68. package/dist/routes/diagram.js.map +1 -0
  69. package/dist/routes/health.d.ts +6 -0
  70. package/dist/routes/health.js +35 -0
  71. package/dist/routes/health.js.map +1 -0
  72. package/dist/routes/index.d.ts +6 -0
  73. package/dist/routes/index.js +23 -0
  74. package/dist/routes/index.js.map +1 -0
  75. package/dist/routes/query.d.ts +26 -0
  76. package/dist/routes/query.js +245 -0
  77. package/dist/routes/query.js.map +1 -0
  78. package/dist/routes/schemas.d.ts +8 -0
  79. package/dist/routes/schemas.js +35 -0
  80. package/dist/routes/schemas.js.map +1 -0
  81. package/dist/routes/tables.d.ts +23 -0
  82. package/dist/routes/tables.js +94 -0
  83. package/dist/routes/tables.js.map +1 -0
  84. package/dist/service/fieldDescriptor.d.ts +66 -0
  85. package/dist/service/fieldDescriptor.js +156 -0
  86. package/dist/service/fieldDescriptor.js.map +1 -0
  87. package/dist/service/health.d.ts +20 -0
  88. package/dist/service/health.js +58 -0
  89. package/dist/service/health.js.map +1 -0
  90. package/dist/service/introspect.d.ts +6 -0
  91. package/dist/service/introspect.js +312 -0
  92. package/dist/service/introspect.js.map +1 -0
  93. package/dist/service/schemaLabels.d.ts +7 -0
  94. package/dist/service/schemaLabels.js +39 -0
  95. package/dist/service/schemaLabels.js.map +1 -0
  96. package/dist/service/schemaRegistry.d.ts +2 -0
  97. package/dist/service/schemaRegistry.js +8 -0
  98. package/dist/service/schemaRegistry.js.map +1 -0
  99. package/dist/service/stagedSerialization.d.ts +6 -0
  100. package/dist/service/stagedSerialization.js +54 -0
  101. package/dist/service/stagedSerialization.js.map +1 -0
  102. package/dist/service/types.d.ts +67 -0
  103. package/dist/service/types.js +3 -0
  104. package/dist/service/types.js.map +1 -0
  105. package/dist/types/constants.d.ts +3 -0
  106. package/dist/types/constants.js +7 -0
  107. package/dist/types/constants.js.map +1 -0
  108. package/dist/types/responses.d.ts +14 -0
  109. package/dist/types/responses.js +3 -0
  110. package/dist/types/responses.js.map +1 -0
  111. package/dist/utils/query.d.ts +17 -0
  112. package/dist/utils/query.js +116 -0
  113. package/dist/utils/query.js.map +1 -0
  114. package/dist/utils/requestValidation.d.ts +2 -0
  115. package/dist/utils/requestValidation.js +14 -0
  116. package/dist/utils/requestValidation.js.map +1 -0
  117. package/frontend-vue/.editorconfig +12 -0
  118. package/frontend-vue/.prettierrc +11 -0
  119. package/frontend-vue/app/components/DataBrowserSidebar.vue +254 -0
  120. package/frontend-vue/app/components/DataBrowserTabBar.vue +166 -0
  121. package/frontend-vue/app/components/DataGrid.vue +735 -0
  122. package/frontend-vue/app/components/DataGridCell.vue +147 -0
  123. package/frontend-vue/app/components/DataGridHeaderCell.vue +175 -0
  124. package/frontend-vue/app/components/DiagramNoteNode.vue +100 -0
  125. package/frontend-vue/app/components/DiagramRelationEdge.vue +62 -0
  126. package/frontend-vue/app/components/DiagramStubNode.vue +37 -0
  127. package/frontend-vue/app/components/DiagramTableNode.vue +94 -0
  128. package/frontend-vue/app/components/JsonCellPanel.vue +93 -0
  129. package/frontend-vue/app/components/QueryBuilderPanel.vue +501 -0
  130. package/frontend-vue/app/components/QueryFavoritesPanel.vue +127 -0
  131. package/frontend-vue/app/components/QueryHistoryPanel.vue +134 -0
  132. package/frontend-vue/app/components/QueryResultPanel.vue +204 -0
  133. package/frontend-vue/app/components/SchemaDiagram.vue +1150 -0
  134. package/frontend-vue/app/components/SchemaTableInspector.vue +278 -0
  135. package/frontend-vue/app/composables/useDataBrowserGrid.ts +162 -0
  136. package/frontend-vue/app/composables/useDataBrowserTabs.ts +255 -0
  137. package/frontend-vue/app/composables/useDatabaseHealth.ts +50 -0
  138. package/frontend-vue/app/composables/useDatabaseSchemas.ts +92 -0
  139. package/frontend-vue/app/composables/useDiagramPersistence.ts +99 -0
  140. package/frontend-vue/app/composables/useQueryRuntime.ts +118 -0
  141. package/frontend-vue/app/composables/useQueryStore.ts +117 -0
  142. package/frontend-vue/app/composables/useTableViewRowClick.ts +22 -0
  143. package/frontend-vue/app/custom-pages/database/data.vue +42 -0
  144. package/frontend-vue/app/custom-pages/database/overview.vue +367 -0
  145. package/frontend-vue/app/custom-pages/database/query.vue +432 -0
  146. package/frontend-vue/app/custom-pages/database/schemas.vue +216 -0
  147. package/frontend-vue/app/utils/databaseFieldModifiers.ts +33 -0
  148. package/frontend-vue/app/utils/databaseFieldTypeIcons.ts +224 -0
  149. package/frontend-vue/app/utils/dmsTableView.ts +83 -0
  150. package/frontend-vue/app/utils/relativeTime.ts +15 -0
  151. package/frontend-vue/app/utils/stagedSerialization.ts +91 -0
  152. package/frontend-vue/dms.frontend.ts +38 -0
  153. package/frontend-vue/eslint.config.mjs +16 -0
  154. package/frontend-vue/i18n/locales/database-en-GB.json +239 -0
  155. package/frontend-vue/i18n/locales/database-fr-FR.json +239 -0
  156. package/frontend-vue/package.json +42 -0
  157. package/frontend-vue/pnpm-lock.yaml +5819 -0
  158. package/frontend-vue/pnpm-workspace.yaml +2 -0
  159. package/frontend-vue/tests/persistence.test.ts +70 -0
  160. package/frontend-vue/tests/queryRuntime.test.ts +53 -0
  161. package/frontend-vue/tests/registration.test.ts +33 -0
  162. package/frontend-vue/tsconfig.json +13 -0
  163. package/frontend-vue/vitest.config.ts +5 -0
  164. package/package.json +78 -0
@@ -0,0 +1,93 @@
1
+ <script setup lang="ts">
2
+ const props = defineProps<{
3
+ open: boolean;
4
+ column: string;
5
+ value: unknown;
6
+ saving: boolean;
7
+ readOnly: boolean;
8
+ }>();
9
+
10
+ const emit = defineEmits<{
11
+ "update:open": [open: boolean];
12
+ save: [value: unknown];
13
+ }>();
14
+
15
+ const { t } = useI18n();
16
+
17
+ const draft = ref("");
18
+
19
+ watch(
20
+ () => props.open,
21
+ (open) => {
22
+ if (!open) return;
23
+ try {
24
+ draft.value = JSON.stringify(props.value, null, 2) ?? "";
25
+ } catch {
26
+ draft.value = "";
27
+ }
28
+ },
29
+ { immediate: true },
30
+ );
31
+
32
+ const parseError = computed(() => {
33
+ try {
34
+ const parsed = JSON.parse(draft.value);
35
+ if (parsed === null || typeof parsed !== "object") {
36
+ return t("dms_database.data.jsonNotStructured");
37
+ }
38
+ return null;
39
+ } catch (error) {
40
+ // Localized prefix + the engine's parse detail (position, token…).
41
+ return error instanceof Error
42
+ ? `${t("dms_database.data.jsonInvalid")} — ${error.message}`
43
+ : t("dms_database.data.jsonInvalid");
44
+ }
45
+ });
46
+
47
+ function save() {
48
+ if (parseError.value || props.readOnly) return;
49
+ emit("save", JSON.parse(draft.value));
50
+ }
51
+ </script>
52
+
53
+ <template>
54
+ <USlideover
55
+ :open="open"
56
+ :title="t('dms_database.data.jsonPanelTitle', { column })"
57
+ @update:open="emit('update:open', $event)"
58
+ >
59
+ <template #body>
60
+ <div class="flex h-full flex-col gap-3">
61
+ <textarea
62
+ v-model="draft"
63
+ :readonly="readOnly"
64
+ class="min-h-96 flex-1 resize-none rounded-md border border-default bg-default p-3 font-mono text-xs outline-none focus:border-primary"
65
+ spellcheck="false"
66
+ />
67
+ <UAlert
68
+ v-if="!readOnly && parseError"
69
+ color="error"
70
+ icon="i-ph-warning"
71
+ :description="parseError"
72
+ />
73
+ </div>
74
+ </template>
75
+ <template #footer>
76
+ <div class="flex w-full justify-end gap-2">
77
+ <UButton
78
+ variant="ghost"
79
+ color="neutral"
80
+ :label="t('dms_database.data.cancel')"
81
+ @click="emit('update:open', false)"
82
+ />
83
+ <UButton
84
+ v-if="!readOnly"
85
+ :label="t('dms_database.data.save')"
86
+ :disabled="Boolean(parseError)"
87
+ :loading="saving"
88
+ @click="save"
89
+ />
90
+ </div>
91
+ </template>
92
+ </USlideover>
93
+ </template>
@@ -0,0 +1,501 @@
1
+ <script setup lang="ts">
2
+ import {
3
+ autocompletion,
4
+ type Completion,
5
+ type CompletionContext,
6
+ type CompletionResult,
7
+ completionKeymap,
8
+ } from "@codemirror/autocomplete";
9
+ import {
10
+ defaultKeymap,
11
+ history,
12
+ historyKeymap,
13
+ indentWithTab,
14
+ } from "@codemirror/commands";
15
+ import { javascript } from "@codemirror/lang-javascript";
16
+ import {
17
+ bracketMatching,
18
+ defaultHighlightStyle,
19
+ indentOnInput,
20
+ syntaxHighlighting,
21
+ } from "@codemirror/language";
22
+ import { Compartment, EditorState } from "@codemirror/state";
23
+ import { oneDark } from "@codemirror/theme-one-dark";
24
+ import {
25
+ EditorView,
26
+ keymap,
27
+ lineNumbers,
28
+ placeholder as cmPlaceholder,
29
+ } from "@codemirror/view";
30
+ import type { SchemaSummary } from "../composables/useDatabaseSchemas";
31
+ import type { QueryLanguage } from "../composables/useQueryStore";
32
+
33
+ interface Props {
34
+ modelValue: string;
35
+ language: QueryLanguage;
36
+ executing?: boolean;
37
+ // Drives the schema-aware autocomplete (table/field names + the query DSL).
38
+ schemas?: SchemaSummary[];
39
+ }
40
+
41
+ const props = withDefaults(defineProps<Props>(), {
42
+ executing: false,
43
+ schemas: () => [],
44
+ });
45
+
46
+ const emit = defineEmits<{
47
+ "update:modelValue": [value: string];
48
+ "update:language": [value: QueryLanguage];
49
+ execute: [];
50
+ save: [];
51
+ }>();
52
+
53
+ // Static code example for the builder DSL, used as the editor placeholder and the
54
+ // footer hint (not translatable — the angle-bracket-free call trips no i18n guard).
55
+ const BUILDER_EXAMPLE = 'schemas.shop.instance().table("users").slice(0, 10)';
56
+
57
+ const internalValue = computed<string>({
58
+ get: () => props.modelValue,
59
+ set: (value) => emit("update:modelValue", value),
60
+ });
61
+
62
+ const languageOptions = [{ label: "AQL", value: "aql" as const }];
63
+
64
+ const internalLanguage = computed<QueryLanguage>({
65
+ get: () => props.language,
66
+ set: (value) => emit("update:language", value),
67
+ });
68
+
69
+ function canExecute(): boolean {
70
+ return Boolean(internalValue.value.trim()) && !props.executing;
71
+ }
72
+
73
+ // Light tidy: strip trailing whitespace and surrounding blank lines.
74
+ function runFormat() {
75
+ internalValue.value = internalValue.value
76
+ .split("\n")
77
+ .map((line) => line.replace(/\s+$/, ""))
78
+ .join("\n")
79
+ .replace(/^\n+/, "")
80
+ .replace(/\n+$/, "");
81
+ }
82
+
83
+ // --- autocomplete -----------------------------------------------------------
84
+ // The DSL is a fluent chain — `schemas.<id>.instance().table("…").slice(…)` — so
85
+ // completions are offered by position in that chain rather than as one flat list.
86
+ // Schema also exposes createInstance/destroyInstance/listInstances, but only
87
+ // instance(id?) continues the browse chain, so it is the only one offered.
88
+ const SCHEMA_METHODS = ["instance"];
89
+ const INSTANCE_METHODS = ["table"]; // a SchemaInstance only exposes table(name)
90
+ const QUERY_METHODS = [
91
+ "slice",
92
+ "orderBy",
93
+ "filter",
94
+ "map",
95
+ "pluck",
96
+ "get",
97
+ "getAll",
98
+ "between",
99
+ "count",
100
+ "limit",
101
+ "insert",
102
+ "update",
103
+ "replace",
104
+ "delete",
105
+ "do",
106
+ "default",
107
+ "key",
108
+ "lookup",
109
+ "changes",
110
+ ];
111
+
112
+ interface CompletionItem {
113
+ label: string;
114
+ type: string;
115
+ detail?: string;
116
+ // What is actually inserted, when it differs from the label: a string (e.g.
117
+ // auto-quoting a table name) or a function (e.g. rewriting `schemas.<id>` to
118
+ // bracket form for ids that aren't valid identifiers).
119
+ apply?:
120
+ | string
121
+ | ((
122
+ view: EditorView,
123
+ completion: Completion,
124
+ from: number,
125
+ to: number,
126
+ ) => void);
127
+ }
128
+
129
+ const opt = (label: string, type: string, detail?: string): CompletionItem => ({
130
+ label,
131
+ type,
132
+ detail,
133
+ });
134
+
135
+ // Schema-derived names, indexed per schema so completions can be scoped to the
136
+ // schema in the current chain. Rebuilt whenever the live schema list changes; the
137
+ // completion source reads `.value` lazily, so no editor reconfigure is needed.
138
+ const completionData = computed(() => {
139
+ const schemaIds: string[] = [];
140
+ const tablesBySchema: Record<string, string[]> = {};
141
+ const fieldsBySchema: Record<string, string[]> = {};
142
+ const instancesBySchema: Record<string, string[]> = {};
143
+ const allTables = new Set<string>();
144
+ const allFields = new Set<string>();
145
+ for (const schema of props.schemas ?? []) {
146
+ schemaIds.push(schema.id);
147
+ instancesBySchema[schema.id] = schema.instances ?? [];
148
+ const tables: string[] = [];
149
+ const fields = new Set<string>();
150
+ for (const table of schema.tables ?? []) {
151
+ tables.push(table.name);
152
+ allTables.add(table.name);
153
+ for (const field of Object.keys(table.fields ?? {})) {
154
+ fields.add(field);
155
+ allFields.add(field);
156
+ }
157
+ }
158
+ tablesBySchema[schema.id] = tables;
159
+ fieldsBySchema[schema.id] = [...fields];
160
+ }
161
+ return {
162
+ schemaIds,
163
+ tablesBySchema,
164
+ fieldsBySchema,
165
+ instancesBySchema,
166
+ allTables: [...allTables],
167
+ allFields: [...allFields],
168
+ };
169
+ });
170
+
171
+ // `schemas.demo` (dot) or `schemas["dms-core"]` (bracket — required for ids that
172
+ // aren't valid JS identifiers). Used to scope table/field/instance completions.
173
+ function currentSchemaId(before: string): string | null {
174
+ const re =
175
+ /schemas\s*(?:\.\s*([A-Za-z_$][\w$]*)|\[\s*["'`]([\w$-]+)["'`]\s*\])/g;
176
+ let last: string | null = null;
177
+ for (
178
+ let match = re.exec(before);
179
+ match !== null;
180
+ match = re.exec(before)
181
+ ) {
182
+ last = match[1] ?? match[2] ?? null;
183
+ }
184
+ return last;
185
+ }
186
+
187
+ function listFor(
188
+ map: Record<string, string[]>,
189
+ schemaId: string | null,
190
+ fallback: string[],
191
+ ): string[] {
192
+ return (schemaId && map[schemaId]) || fallback;
193
+ }
194
+
195
+ function completionSource(ctx: CompletionContext): CompletionResult | null {
196
+ const data = completionData.value;
197
+ // A bounded look-behind window covers multi-line chains without scanning the
198
+ // whole document on every keystroke.
199
+ const before = ctx.state.sliceDoc(Math.max(0, ctx.pos - 240), ctx.pos);
200
+
201
+ // `instance(<id>)` argument → that schema's instance ids (+ CROSS_INSTANCE).
202
+ // Acts as parameter help: it surfaces what instance() accepts. Quote-aware.
203
+ const inInstance =
204
+ /(?:^|[^\w$.])schemas\s*(?:\.\s*([A-Za-z_$][\w$]*)|\[\s*["'`]([\w$-]+)["'`]\s*\])\s*\.\s*instance\(\s*(["'`])?([\w$-]*)$/.exec(
205
+ before,
206
+ );
207
+ if (inInstance) {
208
+ const schemaId = inInstance[1] ?? inInstance[2] ?? "";
209
+ const quote = inInstance[3];
210
+ const typed = inInstance[4] ?? "";
211
+ const options: CompletionItem[] = (
212
+ data.instancesBySchema[schemaId] ?? []
213
+ ).map((id) => ({
214
+ label: id,
215
+ type: "enum",
216
+ detail: "instance id",
217
+ apply: quote ? id : `"${id}"`,
218
+ }));
219
+ // CROSS_INSTANCE is a bare identifier, so only when not inside a quote.
220
+ if (!quote) {
221
+ options.push({
222
+ label: "CROSS_INSTANCE",
223
+ type: "constant",
224
+ detail: "all instances (read-only)",
225
+ });
226
+ }
227
+ if (!options.length) return null;
228
+ return { from: ctx.pos - typed.length, validFor: /[\w$-]*/, options };
229
+ }
230
+ // Schema id inside bracket access: `schemas["…`.
231
+ const inSchemaBracket = /schemas\s*\[\s*["'`]([\w$-]*)$/.exec(before);
232
+ if (inSchemaBracket) {
233
+ const typed = inSchemaBracket[1] ?? "";
234
+ return {
235
+ from: ctx.pos - typed.length,
236
+ validFor: /[\w$-]*/,
237
+ options: data.schemaIds.map((id) => opt(id, "namespace", "schema")),
238
+ };
239
+ }
240
+ // `table(<name>)` argument → that schema's table names. Quote-aware.
241
+ const inTable = /\.table\(\s*(["'`])?([\w$-]*)$/.exec(before);
242
+ if (inTable) {
243
+ const quote = inTable[1];
244
+ const typed = inTable[2] ?? "";
245
+ const tables = listFor(
246
+ data.tablesBySchema,
247
+ currentSchemaId(before),
248
+ data.allTables,
249
+ );
250
+ return {
251
+ from: ctx.pos - typed.length,
252
+ validFor: /[\w$-]*/,
253
+ options: tables.map((name) => ({
254
+ label: name,
255
+ type: "class",
256
+ detail: "table",
257
+ apply: quote ? name : `"${name}"`,
258
+ })),
259
+ };
260
+ }
261
+ // Quoted field name inside a field selector (`pluck` / `orderBy` / `key`).
262
+ const inField = /\.(?:pluck|orderBy|key)\([^)]*["'`]([\w$]*)$/.exec(before);
263
+ if (inField) {
264
+ const typed = inField[1] ?? "";
265
+ const fields = listFor(
266
+ data.fieldsBySchema,
267
+ currentSchemaId(before),
268
+ data.allFields,
269
+ );
270
+ return {
271
+ from: ctx.pos - typed.length,
272
+ validFor: /[\w$]*/,
273
+ options: fields.map((name) => opt(name, "property", "field")),
274
+ };
275
+ }
276
+
277
+ // Member access / bare identifier.
278
+ const typed = before.match(/[\w$]*$/)?.[0] ?? "";
279
+ const upto = before.slice(0, before.length - typed.length);
280
+ const from = ctx.pos - typed.length;
281
+
282
+ if (upto.endsWith(".")) {
283
+ const chain = upto.slice(0, -1).trimEnd();
284
+ if (/(?:^|[^\w$.])schemas$/.test(chain)) {
285
+ // `schemas.` → schema ids. Ids that aren't valid identifiers rewrite the
286
+ // dot to bracket form (`schemas["dms-core"]`).
287
+ return {
288
+ from,
289
+ validFor: /[\w$]*/,
290
+ options: data.schemaIds.map((id) => {
291
+ if (/^[A-Z_$][\w$]*$/i.test(id)) {
292
+ return opt(id, "namespace", "schema");
293
+ }
294
+ return {
295
+ label: id,
296
+ type: "namespace",
297
+ detail: "schema",
298
+ apply: (view, _completion, fromPos, toPos) =>
299
+ view.dispatch({
300
+ changes: {
301
+ from: fromPos - 1,
302
+ to: toPos,
303
+ insert: `[${JSON.stringify(id)}]`,
304
+ },
305
+ }),
306
+ };
307
+ }),
308
+ };
309
+ }
310
+ if (
311
+ /(?:^|[^\w$.])schemas\s*(?:\.\s*[A-Za-z_$][\w$]*|\[\s*["'`][\w$-]+["'`]\s*\])$/.test(
312
+ chain,
313
+ )
314
+ ) {
315
+ // `schemas.<id>.` / `schemas["<id>"].` → Schema methods
316
+ return {
317
+ from,
318
+ validFor: /[\w$]*/,
319
+ options: SCHEMA_METHODS.map((m) => opt(m, "method", "Schema")),
320
+ };
321
+ }
322
+ if (chain.endsWith(")")) {
323
+ // A completed call → its result's methods.
324
+ const options = /\binstance\s*\([^()]*\)$/.test(chain)
325
+ ? INSTANCE_METHODS.map((m) => opt(m, "method", "SchemaInstance"))
326
+ : QUERY_METHODS.map((m) => opt(m, "method", "Query"));
327
+ return { from, validFor: /[\w$]*/, options };
328
+ }
329
+ // A `.` after a bare, uncalled method (e.g. `…instance.`) is not a DSL step.
330
+ return null;
331
+ }
332
+
333
+ // Top level: the entry point only.
334
+ if (!typed && !ctx.explicit) return null;
335
+ return { from, validFor: /[\w$]*/, options: [opt("schemas", "variable", "root")] };
336
+ }
337
+
338
+ // --- editor -----------------------------------------------------------------
339
+ const editorContainer = ref<HTMLElement | null>(null);
340
+ let view: EditorView | null = null;
341
+ const themeCompartment = new Compartment();
342
+ const colorMode = useColorMode();
343
+
344
+ // Transparent surfaces so the editor blends into the DmsCard (bg-default).
345
+ const baseTheme = EditorView.theme({
346
+ "&": { backgroundColor: "transparent", height: "100%", fontSize: "13px" },
347
+ "&.cm-focused": { outline: "none" },
348
+ ".cm-scroller": {
349
+ fontFamily:
350
+ "ui-monospace, SFMono-Regular, Menlo, Consolas, 'Liberation Mono', monospace",
351
+ lineHeight: "1.6",
352
+ },
353
+ ".cm-content": { padding: "12px 0" },
354
+ ".cm-gutters": { backgroundColor: "transparent", border: "none" },
355
+ ".cm-activeLine, .cm-activeLineGutter": { backgroundColor: "transparent" },
356
+ });
357
+
358
+ function themeExtension() {
359
+ return colorMode.value === "dark"
360
+ ? oneDark
361
+ : syntaxHighlighting(defaultHighlightStyle, { fallback: true });
362
+ }
363
+
364
+ onMounted(() => {
365
+ if (!editorContainer.value) return;
366
+ const state = EditorState.create({
367
+ doc: internalValue.value,
368
+ extensions: [
369
+ lineNumbers(),
370
+ history(),
371
+ bracketMatching(),
372
+ indentOnInput(),
373
+ javascript({ typescript: true }),
374
+ autocompletion({ override: [completionSource], activateOnTyping: true }),
375
+ cmPlaceholder(BUILDER_EXAMPLE),
376
+ EditorView.lineWrapping,
377
+ keymap.of([
378
+ {
379
+ key: "Mod-Enter",
380
+ preventDefault: true,
381
+ run: () => {
382
+ if (canExecute()) emit("execute");
383
+ return true;
384
+ },
385
+ },
386
+ indentWithTab,
387
+ ...completionKeymap,
388
+ ...defaultKeymap,
389
+ ...historyKeymap,
390
+ ]),
391
+ themeCompartment.of(themeExtension()),
392
+ baseTheme,
393
+ EditorView.updateListener.of((update) => {
394
+ if (!update.docChanged) return;
395
+ const text = update.state.doc.toString();
396
+ if (text !== props.modelValue) emit("update:modelValue", text);
397
+ }),
398
+ ],
399
+ });
400
+ view = new EditorView({ state, parent: editorContainer.value });
401
+ });
402
+
403
+ // External edits (history load, format, clear) → push into the editor.
404
+ watch(
405
+ () => props.modelValue,
406
+ (value) => {
407
+ if (view && value !== view.state.doc.toString()) {
408
+ view.dispatch({
409
+ changes: { from: 0, to: view.state.doc.length, insert: value },
410
+ });
411
+ }
412
+ },
413
+ );
414
+
415
+ // Swap the syntax theme when the app toggles light/dark.
416
+ watch(
417
+ () => colorMode.value,
418
+ () => {
419
+ view?.dispatch({
420
+ effects: themeCompartment.reconfigure(themeExtension()),
421
+ });
422
+ },
423
+ );
424
+
425
+ onBeforeUnmount(() => {
426
+ view?.destroy();
427
+ view = null;
428
+ });
429
+ </script>
430
+
431
+ <template>
432
+ <DmsCard :padded="false" class="overflow-hidden">
433
+ <!-- editor bar -->
434
+ <div
435
+ class="flex items-center gap-2 px-3.5 py-3 border-b border-default flex-wrap"
436
+ >
437
+ <USelect
438
+ v-model="internalLanguage"
439
+ :items="languageOptions"
440
+ :aria-label="$t('dms_database.query.language')"
441
+ size="sm"
442
+ class="w-24"
443
+ />
444
+ <span class="inline-flex items-center gap-1.5 text-[11.5px] text-dimmed">
445
+ <UIcon name="i-ph-brackets-curly" class="size-3.5" />
446
+ {{ $t("dms_database.query.hint") }}
447
+ </span>
448
+ <span class="flex-1" />
449
+ <UButton
450
+ size="sm"
451
+ color="neutral"
452
+ variant="ghost"
453
+ icon="i-ph-magic-wand"
454
+ :label="$t('dms_database.query.format')"
455
+ :disabled="!internalValue.trim()"
456
+ @click="runFormat"
457
+ />
458
+ <UButton
459
+ size="sm"
460
+ color="neutral"
461
+ variant="ghost"
462
+ icon="i-ph-floppy-disk"
463
+ :label="$t('dms_database.query.save')"
464
+ :disabled="!internalValue.trim()"
465
+ @click="emit('save')"
466
+ />
467
+ <UButton
468
+ size="sm"
469
+ color="primary"
470
+ icon="i-ph-lightning"
471
+ :loading="executing"
472
+ :disabled="!internalValue.trim() || executing"
473
+ @click="emit('execute')"
474
+ >
475
+ {{ $t("dms_database.query.execute") }}
476
+ <UKbd value="meta" size="sm" class="ml-1" />
477
+ <UKbd value="enter" size="sm" />
478
+ </UButton>
479
+ </div>
480
+
481
+ <!-- editor (CodeMirror mounts here on the client) -->
482
+ <div
483
+ ref="editorContainer"
484
+ class="h-64 overflow-auto bg-default text-toned"
485
+ />
486
+
487
+ <!-- builder hint footer -->
488
+ <div
489
+ class="flex items-center gap-2 px-3.5 py-3 border-t border-default flex-wrap"
490
+ >
491
+ <span
492
+ class="font-mono text-[10px] uppercase tracking-wider text-dimmed shrink-0"
493
+ >
494
+ {{ $t("dms_database.query.builderHintLabel") }}
495
+ </span>
496
+ <code class="text-[11.5px] font-mono text-muted truncate">
497
+ {{ BUILDER_EXAMPLE }}
498
+ </code>
499
+ </div>
500
+ </DmsCard>
501
+ </template>