@dudousxd/nestjs-catalog 0.1.0

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 (47) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +123 -0
  3. package/dist/catalog.controller.d.ts +8 -0
  4. package/dist/catalog.controller.js +482 -0
  5. package/dist/catalog.decorators.d.ts +37 -0
  6. package/dist/catalog.decorators.js +50 -0
  7. package/dist/catalog.environment.d.ts +442 -0
  8. package/dist/catalog.environment.js +645 -0
  9. package/dist/catalog.events.d.ts +179 -0
  10. package/dist/catalog.events.js +110 -0
  11. package/dist/catalog.module.d.ts +5 -0
  12. package/dist/catalog.module.js +71 -0
  13. package/dist/catalog.options.d.ts +79 -0
  14. package/dist/catalog.options.js +4 -0
  15. package/dist/catalog.overlay-store.d.ts +25 -0
  16. package/dist/catalog.overlay-store.js +44 -0
  17. package/dist/catalog.overlay-store.token.d.ts +1 -0
  18. package/dist/catalog.overlay-store.token.js +4 -0
  19. package/dist/catalog.pipeline.d.ts +800 -0
  20. package/dist/catalog.pipeline.js +606 -0
  21. package/dist/catalog.principal.d.ts +209 -0
  22. package/dist/catalog.principal.js +245 -0
  23. package/dist/catalog.query-cache.d.ts +25 -0
  24. package/dist/catalog.query-cache.js +0 -0
  25. package/dist/catalog.query.d.ts +76 -0
  26. package/dist/catalog.query.js +64 -0
  27. package/dist/catalog.registry.base.d.ts +21 -0
  28. package/dist/catalog.registry.base.js +17 -0
  29. package/dist/catalog.registry.d.ts +44 -0
  30. package/dist/catalog.registry.js +359 -0
  31. package/dist/catalog.service.d.ts +115 -0
  32. package/dist/catalog.service.js +366 -0
  33. package/dist/catalog.store.d.ts +419 -0
  34. package/dist/catalog.store.js +175 -0
  35. package/dist/catalog.types.d.ts +165 -0
  36. package/dist/catalog.types.js +19 -0
  37. package/dist/catalog.workspace.d.ts +426 -0
  38. package/dist/catalog.workspace.js +87 -0
  39. package/dist/client.d.ts +86 -0
  40. package/dist/client.js +83 -0
  41. package/dist/index.d.ts +19 -0
  42. package/dist/index.js +109 -0
  43. package/dist/stores/mikro-orm-read.store.d.ts +20 -0
  44. package/dist/stores/mikro-orm-read.store.js +120 -0
  45. package/dist/transform-runner.d.ts +54 -0
  46. package/dist/transform-runner.js +280 -0
  47. package/package.json +54 -0
@@ -0,0 +1,44 @@
1
+ import type { EntityClass } from '@mikro-orm/core';
2
+ import { MikroORM } from '@mikro-orm/core';
3
+ import { type OnModuleInit } from '@nestjs/common';
4
+ import { type CatalogModuleOptions } from './catalog.options';
5
+ import type { CatalogOverlayStore } from './catalog.overlay-store';
6
+ import { CatalogRegistry } from './catalog.registry.base';
7
+ import type { CatalogGraph, CatalogObjectTypeDef, CatalogOverlay, CatalogSnapshot } from './catalog.types';
8
+ export declare class MikroOrmCatalogRegistry extends CatalogRegistry implements OnModuleInit {
9
+ private readonly orm;
10
+ private readonly options;
11
+ private readonly overlayStore;
12
+ private readonly logger;
13
+ private snapshot;
14
+ private overlay;
15
+ private version;
16
+ /**
17
+ * Class name -> entity constructor, collected while walking the metadata.
18
+ *
19
+ * Kept here rather than looked up on demand because MikroORM's metadata Map
20
+ * is typed as keyed by `EntityName` (a class), so a `.get(someString)` does
21
+ * not typecheck even though the runtime keys are class names.
22
+ */
23
+ private readonly entityClasses;
24
+ constructor(orm: MikroORM, options: CatalogModuleOptions, overlayStore: CatalogOverlayStore);
25
+ onModuleInit(): Promise<void>;
26
+ getSnapshot(): CatalogSnapshot;
27
+ getType(name: string): CatalogObjectTypeDef | undefined;
28
+ /**
29
+ * The entity constructor behind a catalogued type. Only types that passed
30
+ * `shouldInclude` are here, so an excluded entity cannot be read through the
31
+ * generic object endpoint by guessing its name.
32
+ */
33
+ getEntityClass(name: string): EntityClass<Record<string, unknown>> | undefined;
34
+ getGraph(): CatalogGraph;
35
+ /** Tier-0 edit on a type. Never touches the database. */
36
+ patchType(typeName: string, patch: Partial<CatalogOverlay['types'][string]>): Promise<CatalogObjectTypeDef | undefined>;
37
+ /** Tier-0 edit on a property. Never touches the database. */
38
+ patchProperty(typeName: string, propertyName: string, patch: NonNullable<CatalogOverlay['types'][string]['properties']>[string]): Promise<CatalogObjectTypeDef | undefined>;
39
+ resetOverlay(): Promise<void>;
40
+ private persist;
41
+ private rebuild;
42
+ private shouldInclude;
43
+ private buildType;
44
+ }
@@ -0,0 +1,359 @@
1
+ "use strict";
2
+ var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
3
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
4
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
5
+ else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
6
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
7
+ };
8
+ var __metadata = (this && this.__metadata) || function (k, v) {
9
+ if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
10
+ };
11
+ var __param = (this && this.__param) || function (paramIndex, decorator) {
12
+ return function (target, key) { decorator(target, key, paramIndex); }
13
+ };
14
+ var MikroOrmCatalogRegistry_1;
15
+ Object.defineProperty(exports, "__esModule", { value: true });
16
+ exports.MikroOrmCatalogRegistry = void 0;
17
+ // Value import, not type-only: Nest resolves this constructor parameter from
18
+ // the emitted `design:paramtypes`, which a type-only import erases.
19
+ const core_1 = require("@mikro-orm/core");
20
+ const common_1 = require("@nestjs/common");
21
+ const catalog_decorators_1 = require("./catalog.decorators");
22
+ const catalog_events_1 = require("./catalog.events");
23
+ const catalog_options_1 = require("./catalog.options");
24
+ const catalog_overlay_store_token_1 = require("./catalog.overlay-store.token");
25
+ const catalog_registry_base_1 = require("./catalog.registry.base");
26
+ const RELATION_KINDS = ['1:1', '1:m', 'm:1', 'm:n'];
27
+ function isRelationKind(kind) {
28
+ return RELATION_KINDS.includes(kind);
29
+ }
30
+ /** Classify one type name. Returns "unknown" when nothing matches. */
31
+ function classify(raw) {
32
+ const t = raw.toLowerCase();
33
+ if (!t)
34
+ return 'unknown';
35
+ if (t.includes('uuid'))
36
+ return 'uuid';
37
+ // tinyint(1) is how MySQL spells boolean, so it has to beat the "int" test.
38
+ if (t.includes('bool') || t.startsWith('tinyint(1)'))
39
+ return 'boolean';
40
+ if (t.includes('date') || t.includes('time'))
41
+ return 'date';
42
+ if (t.includes('int') ||
43
+ t.includes('float') ||
44
+ t.includes('double') ||
45
+ t.includes('decimal') ||
46
+ t.includes('number') ||
47
+ t.includes('numeric')) {
48
+ return 'number';
49
+ }
50
+ if (t.includes('json') || t.includes('array'))
51
+ return 'json';
52
+ if (t.includes('string') || t.includes('char') || t.includes('text') || t.includes('enum')) {
53
+ return 'string';
54
+ }
55
+ return 'unknown';
56
+ }
57
+ /**
58
+ * Turn an ORM property into something a UI can switch on.
59
+ *
60
+ * Three sources are tried in order, because no single one is reliable. A field
61
+ * declared `Opt<string>` (MikroORM's optional brand) emits `Object` through
62
+ * `emitDecoratorMetadata`, so the TypeScript-side type is useless for roughly
63
+ * half the columns in a real schema — but the SQL column type always knows.
64
+ * Hence the fallback to `columnTypes`.
65
+ *
66
+ * Kept deliberately coarse. This is not reproducing the SQL type system, it is
67
+ * answering "can I right-align this, and does a date picker make sense".
68
+ */
69
+ function toScalarType(prop) {
70
+ const candidates = [prop.runtimeType, prop.type, prop.columnTypes?.[0]].filter((c) => typeof c === 'string' && c.length > 0);
71
+ for (const candidate of candidates) {
72
+ const resolved = classify(candidate);
73
+ // `Object` is what a branded or optional type erases to; it tells us
74
+ // nothing, so keep looking rather than calling it json.
75
+ if (resolved !== 'unknown' && candidate.toLowerCase() !== 'object') {
76
+ return resolved;
77
+ }
78
+ }
79
+ return 'unknown';
80
+ }
81
+ /**
82
+ * `PriBuyBuyListDetail` -> `Pri Buy Buy List Detail`.
83
+ *
84
+ * A guess, and a visibly imperfect one. That is the point: the derived name is
85
+ * a starting value, and the whole reason the overlay exists is so a human can
86
+ * correct it in ten seconds without opening an editor.
87
+ */
88
+ function humanize(name) {
89
+ const spaced = name
90
+ .replace(/([a-z0-9])([A-Z])/g, '$1 $2')
91
+ .replace(/([A-Z]+)([A-Z][a-z])/g, '$1 $2')
92
+ .replace(/[_-]+/g, ' ')
93
+ .replace(/\s+/g, ' ')
94
+ .trim();
95
+ if (!spaced)
96
+ return name;
97
+ // `id` -> `ID` rather than `Id`: it is the single most common column in any
98
+ // schema, and getting it wrong is the first thing anyone notices.
99
+ if (spaced.toLowerCase() === 'id')
100
+ return 'ID';
101
+ return spaced.charAt(0).toUpperCase() + spaced.slice(1);
102
+ }
103
+ function pluralize(name) {
104
+ if (/(s|x|z|ch|sh)$/i.test(name))
105
+ return `${name}es`;
106
+ if (/[^aeiou]y$/i.test(name))
107
+ return `${name.slice(0, -1)}ies`;
108
+ return `${name}s`;
109
+ }
110
+ let MikroOrmCatalogRegistry = MikroOrmCatalogRegistry_1 = class MikroOrmCatalogRegistry extends catalog_registry_base_1.CatalogRegistry {
111
+ orm;
112
+ options;
113
+ overlayStore;
114
+ logger = new common_1.Logger(MikroOrmCatalogRegistry_1.name);
115
+ snapshot = null;
116
+ overlay = { types: {} };
117
+ version = 0;
118
+ /**
119
+ * Class name -> entity constructor, collected while walking the metadata.
120
+ *
121
+ * Kept here rather than looked up on demand because MikroORM's metadata Map
122
+ * is typed as keyed by `EntityName` (a class), so a `.get(someString)` does
123
+ * not typecheck even though the runtime keys are class names.
124
+ */
125
+ entityClasses = new Map();
126
+ constructor(orm, options, overlayStore) {
127
+ super();
128
+ this.orm = orm;
129
+ this.options = options;
130
+ this.overlayStore = overlayStore;
131
+ }
132
+ async onModuleInit() {
133
+ this.overlay = await this.overlayStore.load();
134
+ this.rebuild();
135
+ const { types, properties, relations, enrichedTypes } = this.getSnapshot().stats;
136
+ this.logger.log(`Catalog built: ${types} object types, ${properties} properties, ${relations} relations (${enrichedTypes} enriched)`);
137
+ }
138
+ getSnapshot() {
139
+ if (!this.snapshot)
140
+ this.rebuild();
141
+ // rebuild() always assigns, so this is total.
142
+ return this.snapshot;
143
+ }
144
+ getType(name) {
145
+ return this.getSnapshot().types.find((t) => t.name.toLowerCase() === name.toLowerCase());
146
+ }
147
+ /**
148
+ * The entity constructor behind a catalogued type. Only types that passed
149
+ * `shouldInclude` are here, so an excluded entity cannot be read through the
150
+ * generic object endpoint by guessing its name.
151
+ */
152
+ getEntityClass(name) {
153
+ if (!this.snapshot)
154
+ this.rebuild();
155
+ return this.entityClasses.get(name);
156
+ }
157
+ getGraph() {
158
+ const snapshot = this.getSnapshot();
159
+ const known = new Set(snapshot.types.map((t) => t.name));
160
+ const nodes = snapshot.types.map((t) => ({
161
+ id: t.name,
162
+ label: t.displayName,
163
+ group: t.group,
164
+ icon: t.icon,
165
+ propertyCount: t.properties.length,
166
+ relationCount: t.relations.length,
167
+ }));
168
+ // Both ends of a relation are declared, so every link shows up twice. Keep
169
+ // one edge per unordered pair per name so the graph does not double up.
170
+ const seen = new Set();
171
+ const edges = [];
172
+ for (const type of snapshot.types) {
173
+ for (const relation of type.relations) {
174
+ if (!known.has(relation.targetType))
175
+ continue;
176
+ const pair = [type.name, relation.targetType].sort().join('::');
177
+ const key = `${pair}::${relation.name}`;
178
+ if (seen.has(key))
179
+ continue;
180
+ seen.add(key);
181
+ edges.push({
182
+ id: `${type.name}.${relation.name}`,
183
+ source: type.name,
184
+ target: relation.targetType,
185
+ label: relation.displayName,
186
+ kind: relation.kind,
187
+ });
188
+ }
189
+ }
190
+ return { nodes, edges };
191
+ }
192
+ /** Tier-0 edit on a type. Never touches the database. */
193
+ async patchType(typeName, patch) {
194
+ const type = this.getType(typeName);
195
+ if (!type)
196
+ return undefined;
197
+ const current = this.overlay.types[type.name] ?? {};
198
+ // `properties` is patched through patchProperty; a type patch must not
199
+ // clobber it.
200
+ const { properties: _ignored, ...rest } = patch;
201
+ this.overlay.types[type.name] = { ...current, ...rest };
202
+ await this.persist();
203
+ (0, catalog_events_1.emitCatalog)('type.curated', {
204
+ typeName: type.name,
205
+ changed: Object.keys(rest),
206
+ });
207
+ return this.getType(type.name);
208
+ }
209
+ /** Tier-0 edit on a property. Never touches the database. */
210
+ async patchProperty(typeName, propertyName, patch) {
211
+ const type = this.getType(typeName);
212
+ if (!type)
213
+ return undefined;
214
+ const known = type.properties.some((p) => p.name === propertyName) ||
215
+ type.relations.some((r) => r.name === propertyName);
216
+ if (!known)
217
+ return undefined;
218
+ const currentType = this.overlay.types[type.name] ?? {};
219
+ const currentProps = currentType.properties ?? {};
220
+ this.overlay.types[type.name] = {
221
+ ...currentType,
222
+ properties: {
223
+ ...currentProps,
224
+ [propertyName]: { ...currentProps[propertyName], ...patch },
225
+ },
226
+ };
227
+ await this.persist();
228
+ (0, catalog_events_1.emitCatalog)('type.curated', {
229
+ typeName: type.name,
230
+ property: propertyName,
231
+ changed: Object.keys(patch),
232
+ });
233
+ return this.getType(type.name);
234
+ }
235
+ async resetOverlay() {
236
+ this.overlay = { types: {} };
237
+ await this.persist();
238
+ }
239
+ async persist() {
240
+ await this.overlayStore.save(this.overlay);
241
+ this.rebuild();
242
+ }
243
+ rebuild() {
244
+ // `getAll()` returns a Map, not a plain object. Reaching for Object.values
245
+ // here yields an empty array and a catalog that silently contains nothing.
246
+ const all = this.orm.getMetadata().getAll();
247
+ const types = [];
248
+ this.entityClasses.clear();
249
+ for (const meta of all.values()) {
250
+ if (!this.shouldInclude(meta))
251
+ continue;
252
+ this.entityClasses.set(meta.className, meta.class);
253
+ types.push(this.buildType(meta));
254
+ }
255
+ types.sort((a, b) => a.group.localeCompare(b.group) || a.displayName.localeCompare(b.displayName));
256
+ this.version += 1;
257
+ this.snapshot = {
258
+ version: this.version,
259
+ generatedAt: new Date().toISOString(),
260
+ stats: {
261
+ types: types.length,
262
+ properties: types.reduce((n, t) => n + t.properties.length, 0),
263
+ relations: types.reduce((n, t) => n + t.relations.length, 0),
264
+ enrichedTypes: types.filter((t) => t.enriched).length,
265
+ },
266
+ types,
267
+ };
268
+ }
269
+ shouldInclude(meta) {
270
+ if (meta.abstract || meta.pivotTable || meta.virtual)
271
+ return false;
272
+ if (!meta.className || !meta.tableName)
273
+ return false;
274
+ const { include, exclude } = this.options;
275
+ if (include && include.length > 0 && !include.includes(meta.className)) {
276
+ return false;
277
+ }
278
+ if (exclude?.includes(meta.className))
279
+ return false;
280
+ return true;
281
+ }
282
+ buildType(meta) {
283
+ const entityClass = meta.class;
284
+ const declared = (0, catalog_decorators_1.readTypeOptions)(entityClass);
285
+ const declaredProps = (0, catalog_decorators_1.readPropertyOptions)(entityClass);
286
+ const overlay = this.overlay.types[meta.className] ?? {};
287
+ const overlayProps = overlay.properties ?? {};
288
+ const properties = [];
289
+ const relations = [];
290
+ meta.props.forEach((prop, index) => {
291
+ // Persisted-only view: `persist: false` marks derived/virtual fields that
292
+ // have no column to read, and embedded roots duplicate their children.
293
+ if (prop.persist === false && prop.kind === 'scalar')
294
+ return;
295
+ if (prop.kind === 'embedded')
296
+ return;
297
+ const fromDecorator = declaredProps[prop.name];
298
+ const fromOverlay = overlayProps[prop.name];
299
+ const displayName = fromOverlay?.displayName ?? fromDecorator?.displayName ?? humanize(prop.name);
300
+ const description = fromOverlay?.description ?? fromDecorator?.description;
301
+ const hidden = fromOverlay?.hidden ?? fromDecorator?.hidden ?? false;
302
+ const order = fromOverlay?.order ?? fromDecorator?.order ?? index;
303
+ if (isRelationKind(prop.kind)) {
304
+ relations.push({
305
+ name: prop.name,
306
+ displayName,
307
+ description,
308
+ kind: prop.kind,
309
+ targetType: prop.targetMeta?.className ?? String(prop.type),
310
+ localKey: prop.fieldNames?.[0],
311
+ nullable: Boolean(prop.nullable),
312
+ hidden,
313
+ order,
314
+ });
315
+ return;
316
+ }
317
+ properties.push({
318
+ name: prop.name,
319
+ displayName,
320
+ description,
321
+ type: toScalarType(prop),
322
+ columnName: prop.fieldNames?.[0] ?? prop.name,
323
+ nullable: Boolean(prop.nullable),
324
+ primary: Boolean(prop.primary),
325
+ hidden,
326
+ order,
327
+ classification: fromOverlay?.classification ?? fromDecorator?.classification,
328
+ unit: fromOverlay?.unit ?? fromDecorator?.unit,
329
+ enriched: Boolean(fromDecorator || fromOverlay),
330
+ });
331
+ });
332
+ properties.sort((a, b) => a.order - b.order);
333
+ relations.sort((a, b) => a.order - b.order);
334
+ const displayName = overlay.displayName ?? declared.displayName ?? humanize(meta.className);
335
+ return {
336
+ name: meta.className,
337
+ displayName,
338
+ pluralDisplayName: overlay.pluralDisplayName ?? declared.pluralDisplayName ?? pluralize(displayName),
339
+ description: overlay.description ?? declared.description,
340
+ tableName: meta.tableName,
341
+ icon: overlay.icon ?? declared.icon,
342
+ group: overlay.group ?? declared.group ?? this.options.defaultGroup ?? 'Ungrouped',
343
+ titleProperty: overlay.titleProperty ?? declared.titleProperty,
344
+ primaryKey: meta.primaryKeys ?? [],
345
+ enriched: Object.keys(declared).length > 0 ||
346
+ Object.keys(overlay).length > 0 ||
347
+ properties.some((p) => p.enriched),
348
+ properties,
349
+ relations,
350
+ };
351
+ }
352
+ };
353
+ exports.MikroOrmCatalogRegistry = MikroOrmCatalogRegistry;
354
+ exports.MikroOrmCatalogRegistry = MikroOrmCatalogRegistry = MikroOrmCatalogRegistry_1 = __decorate([
355
+ (0, common_1.Injectable)(),
356
+ __param(1, (0, common_1.Inject)(catalog_options_1.CATALOG_OPTIONS)),
357
+ __param(2, (0, common_1.Inject)(catalog_overlay_store_token_1.CATALOG_OVERLAY_STORE)),
358
+ __metadata("design:paramtypes", [core_1.MikroORM, Object, Object])
359
+ ], MikroOrmCatalogRegistry);
@@ -0,0 +1,115 @@
1
+ import { type CatalogModuleOptions } from './catalog.options';
2
+ import { type CatalogQueryRelation, type CatalogQueryResult } from './catalog.query';
3
+ import { CatalogRegistry } from './catalog.registry.base';
4
+ import { type CatalogReadStore, type SnapshotRef } from './catalog.store';
5
+ import type { CatalogGraph, CatalogObjectPage, CatalogObjectQuery, CatalogObjectTypeDef, CatalogOverlay, CatalogSnapshot } from './catalog.types';
6
+ import { type AuditQuery, type CatalogAuditEvent, type CatalogWorkspaceStore, type Dashboard, type DashboardCard, type EmbeddedChart, type EmbeddedDashboard, type SaveQueryInput, type SavedQuery } from './catalog.workspace';
7
+ /**
8
+ * Reads objects of any catalogued type through one endpoint.
9
+ *
10
+ * This layer owns the decisions that must hold no matter where the rows live:
11
+ * which type names are real, which columns may be returned, how large a page
12
+ * may be. The store below it only fetches. Keeping the guardrails here means a
13
+ * new store cannot accidentally relax them — the appeal of a generic read
14
+ * endpoint is also its whole risk.
15
+ */
16
+ export declare class CatalogService {
17
+ private readonly registry;
18
+ private readonly store;
19
+ private readonly options;
20
+ private readonly workspace?;
21
+ constructor(registry: CatalogRegistry, store: CatalogReadStore, options: CatalogModuleOptions, workspace?: CatalogWorkspaceStore | undefined);
22
+ private readonly cache;
23
+ /** The whole model, as data. */
24
+ getSnapshot(): CatalogSnapshot;
25
+ getType(name: string): CatalogObjectTypeDef | undefined;
26
+ /** Nodes and edges, for drawing the model. */
27
+ getGraph(): CatalogGraph;
28
+ /** Presentation-only. Never a schema change. */
29
+ patchType(typeName: string, patch: Partial<CatalogOverlay['types'][string]>): Promise<CatalogObjectTypeDef | undefined>;
30
+ patchProperty(typeName: string, propertyName: string, patch: NonNullable<CatalogOverlay['types'][string]['properties']>[string]): Promise<CatalogObjectTypeDef | undefined>;
31
+ /** Drops every runtime edit, where the registry supports it. */
32
+ resetOverlay(): Promise<void>;
33
+ /** Columns a generic UI may render: visible, and not a blob. */
34
+ visibleColumns(type: CatalogObjectTypeDef): import("./catalog.types").CatalogPropertyDef[];
35
+ readObjects(typeName: string, query: CatalogObjectQuery & {
36
+ snapshot?: string;
37
+ }): Promise<CatalogObjectPage>;
38
+ /** Empty when the store keeps no history. */
39
+ listSnapshots(typeName: string): Promise<SnapshotRef[]>;
40
+ /** What the mounted store can do — the screens branch on this. */
41
+ capabilities(): {
42
+ query: boolean;
43
+ snapshots: import("./catalog.store").CatalogSnapshotMode;
44
+ writable: boolean;
45
+ timeTravel: boolean;
46
+ atomicCutover?: boolean;
47
+ atomicBatchReplace?: boolean;
48
+ transactional?: boolean;
49
+ };
50
+ /** What a query may select from. Empty when the store offers no SQL. */
51
+ queryRelations(): Promise<CatalogQueryRelation[]>;
52
+ /**
53
+ * Run a read-only statement.
54
+ *
55
+ * The shape check here produces a readable error; the actual guarantee is the
56
+ * read-only transaction the store opens, because a keyword denylist is a
57
+ * guess about a parser and the parser wins eventually.
58
+ */
59
+ runQuery(input: {
60
+ sql: string;
61
+ maxRows?: number;
62
+ /** Reuse a result for this long. Zero (the default) never caches. */
63
+ cacheTtlSeconds?: number;
64
+ }): Promise<CatalogQueryResult>;
65
+ /** Drops every cached result. Exposed so a worker can warm from cold. */
66
+ clearQueryCache(): void;
67
+ workspaceAvailable(): boolean;
68
+ private requireWorkspace;
69
+ listSavedQueries(): Promise<SavedQuery[]>;
70
+ getSavedQuery(id: string): Promise<SavedQuery>;
71
+ saveQuery(input: SaveQueryInput, createdBy: string): Promise<SavedQuery>;
72
+ updateSavedQuery(id: string, input: Partial<SaveQueryInput>): Promise<SavedQuery>;
73
+ deleteSavedQuery(id: string): Promise<boolean>;
74
+ /** Runs a saved query, honouring the TTL it was saved with. */
75
+ runSavedQuery(id: string, maxRows?: number): Promise<{
76
+ savedQuery: SavedQuery;
77
+ result: CatalogQueryResult;
78
+ }>;
79
+ listDashboards(): Promise<Dashboard[]>;
80
+ getDashboard(id: string): Promise<Dashboard>;
81
+ saveDashboard(input: {
82
+ name: string;
83
+ description?: string;
84
+ cards?: DashboardCard[];
85
+ }, createdBy: string): Promise<Dashboard>;
86
+ updateDashboard(id: string, input: Partial<{
87
+ name: string;
88
+ description: string;
89
+ cards: DashboardCard[];
90
+ }>): Promise<Dashboard>;
91
+ deleteDashboard(id: string): Promise<boolean>;
92
+ /** Everything shared, so a consumer can discover what it may render. */
93
+ listEmbeddable(): Promise<{
94
+ dashboards: Array<{
95
+ id: string;
96
+ name: string;
97
+ description?: string;
98
+ charts: number;
99
+ }>;
100
+ charts: Array<{
101
+ id: string;
102
+ name: string;
103
+ description?: string;
104
+ kind: string;
105
+ }>;
106
+ }>;
107
+ /** One chart, rendered. */
108
+ embedChart(savedQueryId: string, layout?: {
109
+ width: number;
110
+ position: number;
111
+ }): Promise<EmbeddedChart>;
112
+ /** A whole dashboard, every chart resolved. */
113
+ embedDashboard(dashboardId: string): Promise<EmbeddedDashboard>;
114
+ listEvents(query: AuditQuery): Promise<CatalogAuditEvent[]>;
115
+ }