@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,50 @@
|
|
|
1
|
+
export interface StorageEntry {
|
|
2
|
+
schema: string;
|
|
3
|
+
table: string;
|
|
4
|
+
rows: number;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export interface OverviewHealth {
|
|
8
|
+
status: "ok" | "down";
|
|
9
|
+
latencyMs: number | null;
|
|
10
|
+
driver: string | null;
|
|
11
|
+
pool: string | null;
|
|
12
|
+
collections: number;
|
|
13
|
+
schemaCount: number;
|
|
14
|
+
tableCount: number;
|
|
15
|
+
indexCount: number;
|
|
16
|
+
totalRows: number;
|
|
17
|
+
sizeBytes: number | null;
|
|
18
|
+
storage: StorageEntry[];
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const HEALTH_ENDPOINT = "/api/database/health";
|
|
22
|
+
|
|
23
|
+
const EMPTY_HEALTH: OverviewHealth = {
|
|
24
|
+
status: "down",
|
|
25
|
+
latencyMs: null,
|
|
26
|
+
driver: null,
|
|
27
|
+
pool: null,
|
|
28
|
+
collections: 0,
|
|
29
|
+
schemaCount: 0,
|
|
30
|
+
tableCount: 0,
|
|
31
|
+
indexCount: 0,
|
|
32
|
+
totalRows: 0,
|
|
33
|
+
sizeBytes: null,
|
|
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,92 @@
|
|
|
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
|
+
interface SchemaLabel {
|
|
51
|
+
text: string;
|
|
52
|
+
color?: SchemaLabelColor;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
interface SchemaStats {
|
|
56
|
+
tableCount: number;
|
|
57
|
+
elementCount: number;
|
|
58
|
+
instanceCount: number;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
interface SchemaSummary {
|
|
62
|
+
id: string;
|
|
63
|
+
options: Record<string, never>;
|
|
64
|
+
tables: TableSummary[];
|
|
65
|
+
label?: SchemaLabel;
|
|
66
|
+
stats: SchemaStats;
|
|
67
|
+
instances: string[];
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const SCHEMAS_ENDPOINT = "/api/database/schemas";
|
|
71
|
+
const EMPTY_SCHEMAS = { schemas: [] as SchemaSummary[] };
|
|
72
|
+
|
|
73
|
+
export function useDatabaseSchemas() {
|
|
74
|
+
const { useFetchAuth } = useAuthFetch();
|
|
75
|
+
const { data, pending, error, refresh } = useFetchAuth<{
|
|
76
|
+
schemas: SchemaSummary[];
|
|
77
|
+
}>(SCHEMAS_ENDPOINT, { default: () => EMPTY_SCHEMAS });
|
|
78
|
+
|
|
79
|
+
return {
|
|
80
|
+
schemas: computed(() => data.value?.schemas ?? []),
|
|
81
|
+
isLoading: pending,
|
|
82
|
+
error,
|
|
83
|
+
refresh,
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export type {
|
|
88
|
+
FieldDescriptor,
|
|
89
|
+
RelationDeclaration,
|
|
90
|
+
SchemaSummary,
|
|
91
|
+
TableSummary,
|
|
92
|
+
};
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
export interface DiagramPosition {
|
|
2
|
+
x: number;
|
|
3
|
+
y: number;
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
export type DiagramPositions = Record<string, DiagramPosition>;
|
|
7
|
+
|
|
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;
|
|
19
|
+
}
|
|
20
|
+
|
|
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;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export type DiagramNoteUpdate = Partial<
|
|
32
|
+
Pick<DiagramNote, "text" | "x" | "y" | "width" | "height" | "color">
|
|
33
|
+
>;
|
|
34
|
+
|
|
35
|
+
const BASE = "/api/database/diagram";
|
|
36
|
+
|
|
37
|
+
export function useDiagramPersistence() {
|
|
38
|
+
const { $authFetch } = useAuthFetch();
|
|
39
|
+
|
|
40
|
+
async function fetchLayout(schema: string): Promise<DiagramPositions> {
|
|
41
|
+
const res = await $authFetch<{ positions: DiagramPositions }>(
|
|
42
|
+
`${BASE}/layout/${encodeURIComponent(schema)}`,
|
|
43
|
+
);
|
|
44
|
+
return res.positions ?? {};
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
async function saveLayout(
|
|
48
|
+
schema: string,
|
|
49
|
+
positions: DiagramPositions,
|
|
50
|
+
): Promise<void> {
|
|
51
|
+
await $authFetch<{ success: boolean }>(
|
|
52
|
+
`${BASE}/layout/${encodeURIComponent(schema)}`,
|
|
53
|
+
{
|
|
54
|
+
method: "PUT",
|
|
55
|
+
body: { positions },
|
|
56
|
+
},
|
|
57
|
+
);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
async function fetchNotes(schema: string): Promise<DiagramNote[]> {
|
|
61
|
+
const res = await $authFetch<{ items: DiagramNote[] }>(`${BASE}/notes`, {
|
|
62
|
+
query: { schema },
|
|
63
|
+
});
|
|
64
|
+
return res.items ?? [];
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
async function createNote(payload: DiagramNoteCreate): Promise<DiagramNote> {
|
|
68
|
+
return $authFetch<DiagramNote>(`${BASE}/notes`, {
|
|
69
|
+
method: "POST",
|
|
70
|
+
body: payload,
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
async function updateNote(
|
|
75
|
+
id: string,
|
|
76
|
+
patch: DiagramNoteUpdate,
|
|
77
|
+
): Promise<DiagramNote> {
|
|
78
|
+
return $authFetch<DiagramNote>(
|
|
79
|
+
`${BASE}/notes/${encodeURIComponent(id)}`,
|
|
80
|
+
{ method: "PUT", body: patch },
|
|
81
|
+
);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
async function deleteNote(id: string): Promise<void> {
|
|
85
|
+
await $authFetch<{ success: boolean }>(
|
|
86
|
+
`${BASE}/notes/${encodeURIComponent(id)}`,
|
|
87
|
+
{ method: "DELETE" },
|
|
88
|
+
);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
return {
|
|
92
|
+
fetchLayout,
|
|
93
|
+
saveLayout,
|
|
94
|
+
fetchNotes,
|
|
95
|
+
createNote,
|
|
96
|
+
updateNote,
|
|
97
|
+
deleteNote,
|
|
98
|
+
};
|
|
99
|
+
}
|
|
@@ -0,0 +1,118 @@
|
|
|
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 { containsMutations, encodeStaged } from '../utils/stagedSerialization'
|
|
14
|
+
import type { SchemaSummary } from './useDatabaseSchemas'
|
|
15
|
+
|
|
16
|
+
interface PreparedQuery {
|
|
17
|
+
query: Record<string, unknown>
|
|
18
|
+
source: string
|
|
19
|
+
hasMutations: boolean
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
interface SchemaDefLike {
|
|
23
|
+
[tableName: string]: { fields: unknown; indexes: unknown }
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function toSchemaDefinition(summary: SchemaSummary): SchemaDefLike {
|
|
27
|
+
const def: SchemaDefLike = {}
|
|
28
|
+
for (const table of summary.tables) {
|
|
29
|
+
def[table.name] = {
|
|
30
|
+
fields: table.fields ?? {},
|
|
31
|
+
indexes: table.indexes ?? {},
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
return def
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function buildSchemasMap(
|
|
38
|
+
summaries: SchemaSummary[],
|
|
39
|
+
): Record<string, Schema<unknown, SchemaDefLike>> {
|
|
40
|
+
const bindings: Record<string, Schema<unknown, SchemaDefLike>> = {}
|
|
41
|
+
for (const summary of summaries) {
|
|
42
|
+
bindings[summary.id] =
|
|
43
|
+
Schema.get<unknown, SchemaDefLike>(summary.id) ??
|
|
44
|
+
new Schema(summary.id, toSchemaDefinition(summary))
|
|
45
|
+
}
|
|
46
|
+
return bindings
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function evaluateUserCode(
|
|
50
|
+
code: string,
|
|
51
|
+
schemas: Record<string, Schema<unknown, SchemaDefLike>>,
|
|
52
|
+
): unknown {
|
|
53
|
+
const wrapped = `"use strict"; return (${code}\n);`
|
|
54
|
+
const fn = new Function(
|
|
55
|
+
'schemas',
|
|
56
|
+
'Schema',
|
|
57
|
+
'SchemaInstance',
|
|
58
|
+
'Table',
|
|
59
|
+
'Selection',
|
|
60
|
+
'SingleSelection',
|
|
61
|
+
'Stream',
|
|
62
|
+
'Datum',
|
|
63
|
+
'Query',
|
|
64
|
+
'ValueProxy',
|
|
65
|
+
'CROSS_INSTANCE',
|
|
66
|
+
wrapped,
|
|
67
|
+
)
|
|
68
|
+
return fn(
|
|
69
|
+
schemas,
|
|
70
|
+
Schema,
|
|
71
|
+
SchemaInstance,
|
|
72
|
+
Table,
|
|
73
|
+
Selection,
|
|
74
|
+
SingleSelection,
|
|
75
|
+
Stream,
|
|
76
|
+
Datum,
|
|
77
|
+
Query,
|
|
78
|
+
ValueProxy,
|
|
79
|
+
CROSS_INSTANCE,
|
|
80
|
+
)
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export function prepareQueryPayload(
|
|
84
|
+
editorText: string,
|
|
85
|
+
summaries: SchemaSummary[],
|
|
86
|
+
): PreparedQuery {
|
|
87
|
+
const trimmed = editorText.trim()
|
|
88
|
+
if (!trimmed) {
|
|
89
|
+
throw new Error('Query is empty')
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const schemas = buildSchemasMap(summaries)
|
|
93
|
+
const result = evaluateUserCode(trimmed, schemas)
|
|
94
|
+
|
|
95
|
+
if (!result || typeof result !== 'object') {
|
|
96
|
+
throw new TypeError(
|
|
97
|
+
'Query must evaluate to a staged object (e.g. schemas.<id>.instance().table("...").slice(0, 10))',
|
|
98
|
+
)
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const stages = (result as { stages?: unknown }).stages
|
|
102
|
+
if (!Array.isArray(stages)) {
|
|
103
|
+
throw new TypeError(
|
|
104
|
+
'Query result is not a staged object — check that the chain ends on a Table/Selection/Stream/Datum/Query.',
|
|
105
|
+
)
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const encoded = encodeStaged(result)
|
|
109
|
+
if (!encoded || typeof encoded !== 'object' || Array.isArray(encoded)) {
|
|
110
|
+
throw new TypeError('Failed to encode query')
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
return {
|
|
114
|
+
query: encoded as Record<string, unknown>,
|
|
115
|
+
source: trimmed,
|
|
116
|
+
hasMutations: containsMutations(result),
|
|
117
|
+
}
|
|
118
|
+
}
|
|
@@ -0,0 +1,117 @@
|
|
|
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
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface HistoryEntry {
|
|
17
|
+
id: string;
|
|
18
|
+
userId: string;
|
|
19
|
+
query: Record<string, unknown>;
|
|
20
|
+
source: string;
|
|
21
|
+
language: QueryLanguage;
|
|
22
|
+
executedAt: string;
|
|
23
|
+
durationMs: number;
|
|
24
|
+
rowCount: number;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface ExecuteResult {
|
|
28
|
+
rows: Record<string, unknown>[];
|
|
29
|
+
executedAt: string;
|
|
30
|
+
durationMs: number;
|
|
31
|
+
error?: string;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export interface SaveQueryInput {
|
|
35
|
+
name: string;
|
|
36
|
+
description: string;
|
|
37
|
+
query: Record<string, unknown>;
|
|
38
|
+
source: string;
|
|
39
|
+
language: QueryLanguage;
|
|
40
|
+
shared: boolean;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const QUERY_BASE = "/api/database/query";
|
|
44
|
+
|
|
45
|
+
export function useQueryStore() {
|
|
46
|
+
const { $authFetch } = useAuthFetch();
|
|
47
|
+
|
|
48
|
+
const savedQueries = ref<SavedQuery[]>([]);
|
|
49
|
+
const historyItems = ref<HistoryEntry[]>([]);
|
|
50
|
+
const historyTotal = ref(0);
|
|
51
|
+
|
|
52
|
+
async function fetchSaved(scope: QueryScope): Promise<void> {
|
|
53
|
+
const response = await $authFetch<{ items: SavedQuery[] }>(
|
|
54
|
+
`${QUERY_BASE}/saved`,
|
|
55
|
+
{ query: { scope } },
|
|
56
|
+
);
|
|
57
|
+
savedQueries.value = response.items ?? [];
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
async function fetchHistory(limit: number, offset: number): Promise<void> {
|
|
61
|
+
const response = await $authFetch<{
|
|
62
|
+
items: HistoryEntry[];
|
|
63
|
+
total: number;
|
|
64
|
+
}>(`${QUERY_BASE}/history`, {
|
|
65
|
+
query: { limit, offset },
|
|
66
|
+
});
|
|
67
|
+
historyItems.value = response.items ?? [];
|
|
68
|
+
historyTotal.value = response.total ?? 0;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
async function executeQuery(
|
|
72
|
+
query: Record<string, unknown>,
|
|
73
|
+
source: string,
|
|
74
|
+
language: QueryLanguage,
|
|
75
|
+
): Promise<ExecuteResult> {
|
|
76
|
+
return $authFetch<ExecuteResult>(`${QUERY_BASE}/execute`, {
|
|
77
|
+
method: "POST",
|
|
78
|
+
body: { query, source, language },
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
async function saveQuery(input: SaveQueryInput): Promise<SavedQuery> {
|
|
83
|
+
return $authFetch<SavedQuery>(`${QUERY_BASE}/saved`, {
|
|
84
|
+
method: "POST",
|
|
85
|
+
body: input,
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
async function updateSaved(
|
|
90
|
+
id: string,
|
|
91
|
+
input: SaveQueryInput,
|
|
92
|
+
): Promise<SavedQuery> {
|
|
93
|
+
return $authFetch<SavedQuery>(
|
|
94
|
+
`${QUERY_BASE}/saved/${encodeURIComponent(id)}`,
|
|
95
|
+
{ method: "PUT", body: input },
|
|
96
|
+
);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
async function deleteSaved(id: string): Promise<void> {
|
|
100
|
+
await $authFetch<{ success: boolean }>(
|
|
101
|
+
`${QUERY_BASE}/saved/${encodeURIComponent(id)}`,
|
|
102
|
+
{ method: "DELETE" },
|
|
103
|
+
);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
return {
|
|
107
|
+
savedQueries,
|
|
108
|
+
historyItems,
|
|
109
|
+
historyTotal,
|
|
110
|
+
fetchSaved,
|
|
111
|
+
fetchHistory,
|
|
112
|
+
executeQuery,
|
|
113
|
+
saveQuery,
|
|
114
|
+
updateSaved,
|
|
115
|
+
deleteSaved,
|
|
116
|
+
};
|
|
117
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
// A read-only DmsTableView has no edit form, so a row (double-)click only
|
|
2
|
+
// broadcasts the `DmsComponent.TableView.RowClick` event on `window`
|
|
3
|
+
// (detail = { component, data: { row } }). This composable subscribes to it for a
|
|
4
|
+
// single table instance and invokes `onRow` with the clicked row.
|
|
5
|
+
const ROW_CLICK_EVENT = "DmsComponent.TableView.RowClick";
|
|
6
|
+
|
|
7
|
+
export function useTableViewRowClick<T = Record<string, unknown>>(
|
|
8
|
+
componentId: string,
|
|
9
|
+
onRow: (row: T) => void,
|
|
10
|
+
): void {
|
|
11
|
+
function handle(event: Event) {
|
|
12
|
+
const detail = (event as CustomEvent).detail as {
|
|
13
|
+
component?: string;
|
|
14
|
+
data?: { row?: T };
|
|
15
|
+
};
|
|
16
|
+
if (detail?.component !== componentId) return;
|
|
17
|
+
const row = detail.data?.row;
|
|
18
|
+
if (row) onRow(row);
|
|
19
|
+
}
|
|
20
|
+
onMounted(() => window.addEventListener(ROW_CLICK_EVENT, handle));
|
|
21
|
+
onBeforeUnmount(() => window.removeEventListener(ROW_CLICK_EVENT, handle));
|
|
22
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
const { schemas } = useDatabaseSchemas();
|
|
3
|
+
const { activeTab, restore } = useDataBrowserTabs();
|
|
4
|
+
|
|
5
|
+
onMounted(() => restore());
|
|
6
|
+
|
|
7
|
+
// Schema-introspected field names for the active tab's table: the grid falls
|
|
8
|
+
// back to them when the backend column sampling returns nothing (empty table).
|
|
9
|
+
const fallbackFields = computed(() => {
|
|
10
|
+
const tab = activeTab.value;
|
|
11
|
+
if (!tab) return [];
|
|
12
|
+
const schema = schemas.value.find((s) => s.id === tab.schema);
|
|
13
|
+
const table = schema?.tables.find((tbl) => tbl.name === tab.table);
|
|
14
|
+
return Object.keys(table?.fields ?? {});
|
|
15
|
+
});
|
|
16
|
+
</script>
|
|
17
|
+
|
|
18
|
+
<template>
|
|
19
|
+
<!-- Studio layout: full height, only the grid scrolls. The offset accounts
|
|
20
|
+
for the DMS shell chrome + page header — tune it at the visual
|
|
21
|
+
checkpoint so the page itself never scrolls. -->
|
|
22
|
+
<div
|
|
23
|
+
class="flex h-[calc(100dvh-13.5rem)] min-h-[30rem] overflow-hidden rounded-lg border border-default bg-default"
|
|
24
|
+
>
|
|
25
|
+
<DmsDatabaseDataBrowserSidebar :schemas="schemas" />
|
|
26
|
+
<div class="flex min-w-0 flex-1 flex-col">
|
|
27
|
+
<DmsDatabaseDataBrowserTabBar />
|
|
28
|
+
<DmsDatabaseDataGrid
|
|
29
|
+
v-if="activeTab"
|
|
30
|
+
:key="activeTab.id"
|
|
31
|
+
:tab="activeTab"
|
|
32
|
+
:fallback-fields="fallbackFields"
|
|
33
|
+
/>
|
|
34
|
+
<div
|
|
35
|
+
v-else
|
|
36
|
+
class="flex flex-1 items-center justify-center text-sm text-muted"
|
|
37
|
+
>
|
|
38
|
+
{{ $t("dms_database.data.pickPrompt") }}
|
|
39
|
+
</div>
|
|
40
|
+
</div>
|
|
41
|
+
</div>
|
|
42
|
+
</template>
|