@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.
- package/LICENSE +190 -0
- package/README.md +53 -0
- package/dist/automation/index.d.ts +2 -0
- package/dist/automation/index.js +13 -0
- package/dist/automation/index.js.map +1 -0
- package/dist/automation/tableWatch.d.ts +17 -0
- package/dist/automation/tableWatch.js +189 -0
- package/dist/automation/tableWatch.js.map +1 -0
- package/dist/config.d.ts +7 -0
- package/dist/config.js +12 -0
- package/dist/config.js.map +1 -0
- package/dist/db/index.d.ts +2 -0
- package/dist/db/index.js +19 -0
- package/dist/db/index.js.map +1 -0
- package/dist/db/models/diagram_layouts.model.d.ts +25 -0
- package/dist/db/models/diagram_layouts.model.js +29 -0
- package/dist/db/models/diagram_layouts.model.js.map +1 -0
- package/dist/db/models/diagram_notes.model.d.ts +23 -0
- package/dist/db/models/diagram_notes.model.js +15 -0
- package/dist/db/models/diagram_notes.model.js.map +1 -0
- package/dist/db/models/index.d.ts +4 -0
- package/dist/db/models/index.js +21 -0
- package/dist/db/models/index.js.map +1 -0
- package/dist/db/models/query_history.model.d.ts +25 -0
- package/dist/db/models/query_history.model.js +33 -0
- package/dist/db/models/query_history.model.js.map +1 -0
- package/dist/db/models/saved_queries.model.d.ts +24 -0
- package/dist/db/models/saved_queries.model.js +21 -0
- package/dist/db/models/saved_queries.model.js.map +1 -0
- package/dist/db/tables/diagram_layouts.table.d.ts +7 -0
- package/dist/db/tables/diagram_layouts.table.js +35 -0
- package/dist/db/tables/diagram_layouts.table.js.map +1 -0
- package/dist/db/tables/diagram_notes.table.d.ts +13 -0
- package/dist/db/tables/diagram_notes.table.js +59 -0
- package/dist/db/tables/diagram_notes.table.js.map +1 -0
- package/dist/db/tables/index.d.ts +4 -0
- package/dist/db/tables/index.js +21 -0
- package/dist/db/tables/index.js.map +1 -0
- package/dist/db/tables/query_history.table.d.ts +11 -0
- package/dist/db/tables/query_history.table.js +54 -0
- package/dist/db/tables/query_history.table.js.map +1 -0
- package/dist/db/tables/saved_queries.table.d.ts +12 -0
- package/dist/db/tables/saved_queries.table.js +58 -0
- package/dist/db/tables/saved_queries.table.js.map +1 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.js +56 -0
- package/dist/index.js.map +1 -0
- package/dist/pages/data.d.ts +9 -0
- package/dist/pages/data.js +29 -0
- package/dist/pages/data.js.map +1 -0
- package/dist/pages/index.d.ts +4 -0
- package/dist/pages/index.js +21 -0
- package/dist/pages/index.js.map +1 -0
- package/dist/pages/overview.d.ts +9 -0
- package/dist/pages/overview.js +29 -0
- package/dist/pages/overview.js.map +1 -0
- package/dist/pages/query.d.ts +9 -0
- package/dist/pages/query.js +29 -0
- package/dist/pages/query.js.map +1 -0
- package/dist/pages/schemas.d.ts +9 -0
- package/dist/pages/schemas.js +29 -0
- package/dist/pages/schemas.js.map +1 -0
- package/dist/routes/data.d.ts +25 -0
- package/dist/routes/data.js +324 -0
- package/dist/routes/data.js.map +1 -0
- package/dist/routes/diagram.d.ts +47 -0
- package/dist/routes/diagram.js +200 -0
- package/dist/routes/diagram.js.map +1 -0
- package/dist/routes/health.d.ts +6 -0
- package/dist/routes/health.js +35 -0
- package/dist/routes/health.js.map +1 -0
- package/dist/routes/index.d.ts +6 -0
- package/dist/routes/index.js +23 -0
- package/dist/routes/index.js.map +1 -0
- package/dist/routes/query.d.ts +26 -0
- package/dist/routes/query.js +245 -0
- package/dist/routes/query.js.map +1 -0
- package/dist/routes/schemas.d.ts +8 -0
- package/dist/routes/schemas.js +35 -0
- package/dist/routes/schemas.js.map +1 -0
- package/dist/routes/tables.d.ts +23 -0
- package/dist/routes/tables.js +94 -0
- package/dist/routes/tables.js.map +1 -0
- package/dist/service/fieldDescriptor.d.ts +66 -0
- package/dist/service/fieldDescriptor.js +156 -0
- package/dist/service/fieldDescriptor.js.map +1 -0
- package/dist/service/health.d.ts +20 -0
- package/dist/service/health.js +58 -0
- package/dist/service/health.js.map +1 -0
- package/dist/service/introspect.d.ts +6 -0
- package/dist/service/introspect.js +312 -0
- package/dist/service/introspect.js.map +1 -0
- package/dist/service/schemaLabels.d.ts +7 -0
- package/dist/service/schemaLabels.js +39 -0
- package/dist/service/schemaLabels.js.map +1 -0
- package/dist/service/schemaRegistry.d.ts +2 -0
- package/dist/service/schemaRegistry.js +8 -0
- package/dist/service/schemaRegistry.js.map +1 -0
- package/dist/service/stagedSerialization.d.ts +6 -0
- package/dist/service/stagedSerialization.js +54 -0
- package/dist/service/stagedSerialization.js.map +1 -0
- package/dist/service/types.d.ts +67 -0
- package/dist/service/types.js +3 -0
- package/dist/service/types.js.map +1 -0
- package/dist/types/constants.d.ts +3 -0
- package/dist/types/constants.js +7 -0
- package/dist/types/constants.js.map +1 -0
- package/dist/types/responses.d.ts +14 -0
- package/dist/types/responses.js +3 -0
- package/dist/types/responses.js.map +1 -0
- package/dist/utils/query.d.ts +17 -0
- package/dist/utils/query.js +116 -0
- package/dist/utils/query.js.map +1 -0
- package/dist/utils/requestValidation.d.ts +2 -0
- package/dist/utils/requestValidation.js +14 -0
- package/dist/utils/requestValidation.js.map +1 -0
- package/frontend-vue/.editorconfig +12 -0
- package/frontend-vue/.prettierrc +11 -0
- package/frontend-vue/app/components/DataBrowserSidebar.vue +254 -0
- package/frontend-vue/app/components/DataBrowserTabBar.vue +166 -0
- package/frontend-vue/app/components/DataGrid.vue +735 -0
- package/frontend-vue/app/components/DataGridCell.vue +147 -0
- package/frontend-vue/app/components/DataGridHeaderCell.vue +175 -0
- package/frontend-vue/app/components/DiagramNoteNode.vue +100 -0
- package/frontend-vue/app/components/DiagramRelationEdge.vue +62 -0
- package/frontend-vue/app/components/DiagramStubNode.vue +37 -0
- package/frontend-vue/app/components/DiagramTableNode.vue +94 -0
- package/frontend-vue/app/components/JsonCellPanel.vue +93 -0
- package/frontend-vue/app/components/QueryBuilderPanel.vue +501 -0
- package/frontend-vue/app/components/QueryFavoritesPanel.vue +127 -0
- package/frontend-vue/app/components/QueryHistoryPanel.vue +134 -0
- package/frontend-vue/app/components/QueryResultPanel.vue +204 -0
- package/frontend-vue/app/components/SchemaDiagram.vue +1150 -0
- package/frontend-vue/app/components/SchemaTableInspector.vue +278 -0
- package/frontend-vue/app/composables/useDataBrowserGrid.ts +162 -0
- package/frontend-vue/app/composables/useDataBrowserTabs.ts +255 -0
- package/frontend-vue/app/composables/useDatabaseHealth.ts +50 -0
- package/frontend-vue/app/composables/useDatabaseSchemas.ts +92 -0
- package/frontend-vue/app/composables/useDiagramPersistence.ts +99 -0
- package/frontend-vue/app/composables/useQueryRuntime.ts +118 -0
- package/frontend-vue/app/composables/useQueryStore.ts +117 -0
- package/frontend-vue/app/composables/useTableViewRowClick.ts +22 -0
- package/frontend-vue/app/custom-pages/database/data.vue +42 -0
- package/frontend-vue/app/custom-pages/database/overview.vue +367 -0
- package/frontend-vue/app/custom-pages/database/query.vue +432 -0
- package/frontend-vue/app/custom-pages/database/schemas.vue +216 -0
- package/frontend-vue/app/utils/databaseFieldModifiers.ts +33 -0
- package/frontend-vue/app/utils/databaseFieldTypeIcons.ts +224 -0
- package/frontend-vue/app/utils/dmsTableView.ts +83 -0
- package/frontend-vue/app/utils/relativeTime.ts +15 -0
- package/frontend-vue/app/utils/stagedSerialization.ts +91 -0
- package/frontend-vue/dms.frontend.ts +38 -0
- package/frontend-vue/eslint.config.mjs +16 -0
- package/frontend-vue/i18n/locales/database-en-GB.json +239 -0
- package/frontend-vue/i18n/locales/database-fr-FR.json +239 -0
- package/frontend-vue/package.json +42 -0
- package/frontend-vue/pnpm-lock.yaml +5819 -0
- package/frontend-vue/pnpm-workspace.yaml +2 -0
- package/frontend-vue/tests/persistence.test.ts +70 -0
- package/frontend-vue/tests/queryRuntime.test.ts +53 -0
- package/frontend-vue/tests/registration.test.ts +33 -0
- package/frontend-vue/tsconfig.json +13 -0
- package/frontend-vue/vitest.config.ts +5 -0
- 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>
|