@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,224 @@
1
+ import type { FieldDescriptor } from "../composables/useDatabaseSchemas";
2
+
3
+ export interface TypeIcon {
4
+ icon: string;
5
+ color: string;
6
+ label: string;
7
+ }
8
+
9
+ const NAMED_TYPE_ICONS: Record<string, TypeIcon> = {
10
+ email: { icon: "i-ph-at", color: "text-sky-500", label: "Email" },
11
+ password: { icon: "i-ph-lock", color: "text-amber-500", label: "Password" },
12
+ secret: { icon: "i-ph-lock", color: "text-amber-500", label: "Secret" },
13
+ };
14
+
15
+ const FALLBACK_ICON: TypeIcon = {
16
+ icon: "i-ph-question",
17
+ color: "text-muted",
18
+ label: "Unknown",
19
+ };
20
+
21
+ const STRING_ICON: TypeIcon = {
22
+ icon: "i-ph-text-aa",
23
+ color: "text-neutral-500",
24
+ label: "String",
25
+ };
26
+ const NUMBER_ICON: TypeIcon = {
27
+ icon: "i-ph-hash",
28
+ color: "text-blue-500",
29
+ label: "Number",
30
+ };
31
+ const BOOLEAN_ICON: TypeIcon = {
32
+ icon: "i-ph-toggle-right",
33
+ color: "text-emerald-500",
34
+ label: "Boolean",
35
+ };
36
+ const DATE_ICON: TypeIcon = {
37
+ icon: "i-ph-calendar-blank",
38
+ color: "text-violet-500",
39
+ label: "Date",
40
+ };
41
+ const ARRAY_ICON: TypeIcon = {
42
+ icon: "i-ph-brackets-square",
43
+ color: "text-orange-500",
44
+ label: "Array",
45
+ };
46
+ const OBJECT_ICON: TypeIcon = {
47
+ icon: "i-ph-brackets-curly",
48
+ color: "text-amber-600",
49
+ label: "Object",
50
+ };
51
+ const RECORD_ICON: TypeIcon = {
52
+ icon: "i-ph-brackets-curly",
53
+ color: "text-amber-600",
54
+ label: "Record",
55
+ };
56
+ const TUPLE_ICON: TypeIcon = {
57
+ icon: "i-ph-brackets-square",
58
+ color: "text-orange-500",
59
+ label: "Tuple",
60
+ };
61
+ const ENUM_ICON: TypeIcon = {
62
+ icon: "i-ph-list-checks",
63
+ color: "text-cyan-500",
64
+ label: "Enum",
65
+ };
66
+ const UNION_ICON: TypeIcon = {
67
+ icon: "i-ph-split",
68
+ color: "text-fuchsia-500",
69
+ label: "Union",
70
+ };
71
+ const NULL_ICON: TypeIcon = {
72
+ icon: "i-ph-circle-dashed",
73
+ color: "text-muted",
74
+ label: "Null",
75
+ };
76
+
77
+ const PRIMARY_KEY_NAMES = new Set(["id", "_id"]);
78
+ const PRIMARY_KEY_INDEXES = ["pk", "primary"] as const;
79
+
80
+ function literalLabel(value: string | number | boolean): string {
81
+ if (typeof value === "string") return `"${value}"`;
82
+ return String(value);
83
+ }
84
+
85
+ function describeDescriptor(d: FieldDescriptor): string {
86
+ switch (d.kind) {
87
+ case "string":
88
+ case "number":
89
+ case "boolean":
90
+ case "date":
91
+ case "any":
92
+ case "unknown":
93
+ case "null":
94
+ case "undefined":
95
+ return d.kind;
96
+ case "literal":
97
+ return literalLabel(d.value);
98
+ case "array":
99
+ return `${d.element ? describeDescriptor(d.element) : "any"}[]`;
100
+ case "tuple":
101
+ return `[${d.elements.map(describeDescriptor).join(", ")}]`;
102
+ case "object":
103
+ return d.partial ? "Partial object" : "Object";
104
+ case "record":
105
+ return `Record<${d.key ? describeDescriptor(d.key) : "string"}, ${describeDescriptor(
106
+ d.value,
107
+ )}>`;
108
+ case "union":
109
+ return d.members.map(describeDescriptor).join(" | ");
110
+ case "intersection":
111
+ return d.members.map(describeDescriptor).join(" & ");
112
+ case "keyof":
113
+ return d.keys.length > 0 ? d.keys.map((k) => `"${k}"`).join(" | ") : "keyof";
114
+ case "refinement":
115
+ return d.name;
116
+ case "recursive":
117
+ return d.name;
118
+ }
119
+ }
120
+
121
+ function descriptorIcon(d: FieldDescriptor): TypeIcon {
122
+ switch (d.kind) {
123
+ case "string":
124
+ return STRING_ICON;
125
+ case "number":
126
+ return NUMBER_ICON;
127
+ case "boolean":
128
+ return BOOLEAN_ICON;
129
+ case "date":
130
+ return DATE_ICON;
131
+ case "null":
132
+ case "undefined":
133
+ return NULL_ICON;
134
+ case "any":
135
+ case "unknown":
136
+ return FALLBACK_ICON;
137
+ case "array":
138
+ return d.element
139
+ ? { ...ARRAY_ICON, label: `${describeDescriptor(d.element)}[]` }
140
+ : ARRAY_ICON;
141
+ case "tuple":
142
+ return { ...TUPLE_ICON, label: describeDescriptor(d) };
143
+ case "object":
144
+ return d.partial ? { ...OBJECT_ICON, label: "Partial object" } : OBJECT_ICON;
145
+ case "record":
146
+ return { ...RECORD_ICON, label: describeDescriptor(d) };
147
+ case "literal": {
148
+ const base =
149
+ typeof d.value === "string"
150
+ ? STRING_ICON
151
+ : typeof d.value === "number"
152
+ ? NUMBER_ICON
153
+ : BOOLEAN_ICON;
154
+ return { ...base, label: literalLabel(d.value) };
155
+ }
156
+ case "union": {
157
+ const allLiteral =
158
+ d.members.length > 0 && d.members.every((m) => m.kind === "literal");
159
+ if (allLiteral) {
160
+ return { ...ENUM_ICON, label: describeDescriptor(d) };
161
+ }
162
+ return { ...UNION_ICON, label: describeDescriptor(d) };
163
+ }
164
+ case "intersection":
165
+ return { ...UNION_ICON, label: describeDescriptor(d) };
166
+ case "keyof":
167
+ return { ...ENUM_ICON, label: describeDescriptor(d) };
168
+ case "refinement": {
169
+ const base = descriptorIcon(d.base);
170
+ return { ...base, label: d.name };
171
+ }
172
+ case "recursive":
173
+ return { ...FALLBACK_ICON, label: d.name };
174
+ }
175
+ }
176
+
177
+ function isDescriptor(value: unknown): value is FieldDescriptor {
178
+ return (
179
+ value !== null &&
180
+ typeof value === "object" &&
181
+ typeof (value as { kind?: unknown }).kind === "string"
182
+ );
183
+ }
184
+
185
+ const RUNTIME_TYPE_ICONS: Record<string, TypeIcon> = {
186
+ string: STRING_ICON,
187
+ number: NUMBER_ICON,
188
+ boolean: BOOLEAN_ICON,
189
+ array: ARRAY_ICON,
190
+ object: OBJECT_ICON,
191
+ null: NULL_ICON,
192
+ undefined: NULL_ICON,
193
+ };
194
+
195
+ // Icon for a COARSE RUNTIME type as sampled by the browse `/columns` endpoint
196
+ // (string | number | boolean | object | array | null | undefined) — the data
197
+ // grid's headers use this so runtime-sampled and schema-declared columns share
198
+ // one icon set. Named-field overrides (email, password…) still apply.
199
+ export function runtimeTypeIcon(type: string, fieldName: string): TypeIcon {
200
+ const named = NAMED_TYPE_ICONS[fieldName.toLowerCase()];
201
+ if (named) return named;
202
+ return RUNTIME_TYPE_ICONS[type] ?? FALLBACK_ICON;
203
+ }
204
+
205
+ export function inferFieldTypeIcon(
206
+ field: unknown,
207
+ fieldName: string,
208
+ ): TypeIcon {
209
+ const named = NAMED_TYPE_ICONS[fieldName.toLowerCase()];
210
+ if (named) return named;
211
+ if (isDescriptor(field)) return descriptorIcon(field);
212
+ return FALLBACK_ICON;
213
+ }
214
+
215
+ export function isPrimaryKey(
216
+ fieldName: string,
217
+ indexes: Record<string, { fields?: string[] }>,
218
+ ): boolean {
219
+ if (PRIMARY_KEY_NAMES.has(fieldName)) return true;
220
+ for (const indexName of PRIMARY_KEY_INDEXES) {
221
+ if (indexes[indexName]?.fields?.includes(fieldName)) return true;
222
+ }
223
+ return false;
224
+ }
@@ -0,0 +1,83 @@
1
+ // Helpers that build, by hand, the *serialised* column config the shared
2
+ // dms-ui `DmsTableView` component expects.
3
+ //
4
+ // The DMS normally generates these objects on the backend from a
5
+ // `DataController` (one fixed table per controller). The module's navigation
6
+ // lists (overview, schema inspector) render read-only tables against module
7
+ // endpoints without such a controller, so we reproduce the same serialised
8
+ // shapes the DMS emits for its own tables.
9
+ //
10
+ // Reference shapes:
11
+ // DataTypeConfig — dms-core/app/types/data-type.ts
12
+ // TableViewColumn — dms-ui/.../table-view/types/column.ts
13
+ // ComponentInfo — { componentName, options? }
14
+
15
+ // The component name a serialised `FormComponents.InputText()` resolves to.
16
+ const TEXT_INPUT_COMPONENT = "dms-input-text";
17
+
18
+ export interface DmsComponentInfo {
19
+ componentName: string;
20
+ options?: Record<string, unknown>;
21
+ }
22
+
23
+ export interface DmsDataTypeConfig {
24
+ id: string;
25
+ compareModes: string[];
26
+ defaultCompareMode?: string;
27
+ filterComponents: Record<string, DmsComponentInfo | "noInput">;
28
+ inputComponent: DmsComponentInfo;
29
+ }
30
+
31
+ export interface DmsTableViewColumn {
32
+ id: string;
33
+ header: string;
34
+ accessorKey: string;
35
+ listable: boolean;
36
+ type: DmsDataTypeConfig;
37
+ visible?: boolean;
38
+ enableSorting?: boolean;
39
+ enableColumnFilter?: boolean;
40
+ readonlyBehavior?: string;
41
+ }
42
+
43
+ // A string/text data type — the safe default for an introspected, schemaless
44
+ // column. `id` is the load-bearing field: DmsTableView dereferences
45
+ // `column.type.id` during setup, so every column MUST carry a valid type config
46
+ // or the component throws immediately.
47
+ export function stringDataType(): DmsDataTypeConfig {
48
+ return {
49
+ id: "string",
50
+ // Filter modes offered when column filtering is enabled; the browse backend
51
+ // decodes `is` (exact) and `contains` (substring).
52
+ compareModes: ["is", "contains"],
53
+ defaultCompareMode: "contains",
54
+ filterComponents: {
55
+ default: { componentName: TEXT_INPUT_COMPONENT },
56
+ is: { componentName: TEXT_INPUT_COMPONENT },
57
+ contains: { componentName: TEXT_INPUT_COMPONENT },
58
+ },
59
+ inputComponent: { componentName: TEXT_INPUT_COMPONENT },
60
+ };
61
+ }
62
+
63
+ export interface ReadonlyColumnSpec {
64
+ key: string;
65
+ header: string;
66
+ enableSorting?: boolean;
67
+ }
68
+
69
+ // Build read-only, sortable columns with explicit (i18n) headers — used by the
70
+ // table-navigation lists, which are display-only (no form components).
71
+ export function buildReadonlyColumns(
72
+ specs: ReadonlyColumnSpec[],
73
+ ): DmsTableViewColumn[] {
74
+ return specs.map((spec) => ({
75
+ id: spec.key,
76
+ accessorKey: spec.key,
77
+ header: spec.header,
78
+ listable: true,
79
+ type: stringDataType(),
80
+ enableSorting: spec.enableSorting ?? true,
81
+ enableColumnFilter: false,
82
+ }));
83
+ }
@@ -0,0 +1,15 @@
1
+ const relativeFormatter = new Intl.RelativeTimeFormat(undefined, {
2
+ numeric: "auto",
3
+ });
4
+
5
+ // Human "3 minutes ago" style label for an ISO timestamp, relative to now.
6
+ // Shared by the query history and favorites panels.
7
+ export function formatRelativeTime(iso: string): string {
8
+ const diffMs = new Date(iso).getTime() - Date.now();
9
+ const minutes = Math.round(diffMs / 60_000);
10
+ if (Math.abs(minutes) < 60) return relativeFormatter.format(minutes, "minute");
11
+ const hours = Math.round(minutes / 60);
12
+ if (Math.abs(hours) < 24) return relativeFormatter.format(hours, "hour");
13
+ const days = Math.round(hours / 24);
14
+ return relativeFormatter.format(days, "day");
15
+ }
@@ -0,0 +1,91 @@
1
+ import {
2
+ Datum,
3
+ Query,
4
+ Schema,
5
+ SchemaInstance,
6
+ Selection,
7
+ SingleSelection,
8
+ Stream,
9
+ Table,
10
+ ValueProxy,
11
+ } from '@antelopejs/interface-database/staged-query'
12
+
13
+ export interface QueryStage {
14
+ stage: string
15
+ options?: unknown
16
+ args: unknown[]
17
+ }
18
+
19
+ type StagedCtor = new (...args: never[]) => unknown
20
+
21
+ const STAGED_CLASSES: Record<string, StagedCtor> = {
22
+ Schema,
23
+ SchemaInstance,
24
+ Table,
25
+ Selection,
26
+ SingleSelection,
27
+ Stream,
28
+ Datum,
29
+ Query,
30
+ ValueProxy,
31
+ }
32
+
33
+ function classNameOf(obj: object): string | undefined {
34
+ for (const [name, ctor] of Object.entries(STAGED_CLASSES)) {
35
+ if (obj instanceof ctor) return name
36
+ }
37
+ return undefined
38
+ }
39
+
40
+ export function encodeStaged(node: unknown): unknown {
41
+ if (node === null || node === undefined) return node
42
+ if (typeof node !== 'object') return node
43
+ if (Array.isArray(node)) return node.map(encodeStaged)
44
+
45
+ const cls = classNameOf(node)
46
+ if (cls) {
47
+ const stages = (node as { stages?: unknown[] }).stages ?? []
48
+ return {
49
+ __cls: cls,
50
+ stages: stages.map(encodeStage),
51
+ }
52
+ }
53
+
54
+ const out: Record<string, unknown> = {}
55
+ for (const [key, value] of Object.entries(node as Record<string, unknown>)) {
56
+ out[key] = encodeStaged(value)
57
+ }
58
+ return out
59
+ }
60
+
61
+ function encodeStage(stage: unknown): unknown {
62
+ if (!stage || typeof stage !== 'object') return stage
63
+ const { stage: name, options, args } = stage as Partial<QueryStage>
64
+ const encoded: QueryStage = {
65
+ stage: typeof name === 'string' ? name : '',
66
+ args: Array.isArray(args) ? args.map(encodeStaged) : [],
67
+ }
68
+ if (options !== undefined) encoded.options = encodeStaged(options)
69
+ return encoded
70
+ }
71
+
72
+ const MUTATING_STAGES = new Set(['insert', 'update', 'replace', 'delete'])
73
+
74
+ export function containsMutations(root: unknown): boolean {
75
+ if (!root || typeof root !== 'object') return false
76
+ const stages = (root as { stages?: unknown }).stages
77
+ if (!Array.isArray(stages)) return false
78
+ for (const stage of stages) {
79
+ if (!stage || typeof stage !== 'object') continue
80
+ const s = stage as { stage?: unknown; args?: unknown }
81
+ if (typeof s.stage === 'string' && MUTATING_STAGES.has(s.stage)) {
82
+ return true
83
+ }
84
+ if (Array.isArray(s.args)) {
85
+ for (const arg of s.args) {
86
+ if (containsMutations(arg)) return true
87
+ }
88
+ }
89
+ }
90
+ return false
91
+ }
@@ -0,0 +1,38 @@
1
+ import { defineAsyncComponent, type Component } from 'vue'
2
+ import type { DmsFrontendModule } from '#dms-inertia/frontend-module'
3
+
4
+ interface VueModule {
5
+ default: Component
6
+ }
7
+
8
+ const components = import.meta.glob<VueModule>('./app/components/**/*.vue')
9
+ const pages = import.meta.glob<VueModule>('./app/custom-pages/**/*.vue')
10
+
11
+ const frontendModule: DmsFrontendModule = {
12
+ setup(sdk) {
13
+ for (const [path, loader] of Object.entries(components).sort()) {
14
+ const name = path
15
+ .split('/')
16
+ .at(-1)!
17
+ .replace(/\.vue$/, '')
18
+ sdk.registerComponent(
19
+ `DmsDatabase${name}`,
20
+ defineAsyncComponent(async () => (await loader()).default),
21
+ )
22
+ }
23
+ for (const [path, loader] of Object.entries(pages).sort()) {
24
+ const name = path.replace('./app/custom-pages/', '').replace(/\.vue$/, '')
25
+ const component = defineAsyncComponent(
26
+ async () => (await loader()).default,
27
+ )
28
+ const componentName = name
29
+ .split('/')
30
+ .map((part) => part[0].toUpperCase() + part.slice(1))
31
+ .join('')
32
+ sdk.registerPage(name, component, loader)
33
+ sdk.registerComponent(`Dms${componentName}`, component)
34
+ }
35
+ },
36
+ }
37
+
38
+ export default frontendModule
@@ -0,0 +1,16 @@
1
+ import vue from 'eslint-plugin-vue'
2
+ import typescript from '@typescript-eslint/parser'
3
+
4
+ export default [
5
+ { ignores: ['node_modules/**', 'dist/**'] },
6
+ ...vue.configs['flat/essential'],
7
+ {
8
+ files: ['**/*.ts'],
9
+ languageOptions: { parser: typescript },
10
+ },
11
+ {
12
+ files: ['**/*.vue'],
13
+ languageOptions: { parserOptions: { parser: typescript } },
14
+ rules: { 'vue/multi-word-component-names': 'off' },
15
+ },
16
+ ]