@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.
- package/LICENSE +21 -0
- package/README.md +123 -0
- package/dist/catalog.controller.d.ts +8 -0
- package/dist/catalog.controller.js +482 -0
- package/dist/catalog.decorators.d.ts +37 -0
- package/dist/catalog.decorators.js +50 -0
- package/dist/catalog.environment.d.ts +442 -0
- package/dist/catalog.environment.js +645 -0
- package/dist/catalog.events.d.ts +179 -0
- package/dist/catalog.events.js +110 -0
- package/dist/catalog.module.d.ts +5 -0
- package/dist/catalog.module.js +71 -0
- package/dist/catalog.options.d.ts +79 -0
- package/dist/catalog.options.js +4 -0
- package/dist/catalog.overlay-store.d.ts +25 -0
- package/dist/catalog.overlay-store.js +44 -0
- package/dist/catalog.overlay-store.token.d.ts +1 -0
- package/dist/catalog.overlay-store.token.js +4 -0
- package/dist/catalog.pipeline.d.ts +800 -0
- package/dist/catalog.pipeline.js +606 -0
- package/dist/catalog.principal.d.ts +209 -0
- package/dist/catalog.principal.js +245 -0
- package/dist/catalog.query-cache.d.ts +25 -0
- package/dist/catalog.query-cache.js +0 -0
- package/dist/catalog.query.d.ts +76 -0
- package/dist/catalog.query.js +64 -0
- package/dist/catalog.registry.base.d.ts +21 -0
- package/dist/catalog.registry.base.js +17 -0
- package/dist/catalog.registry.d.ts +44 -0
- package/dist/catalog.registry.js +359 -0
- package/dist/catalog.service.d.ts +115 -0
- package/dist/catalog.service.js +366 -0
- package/dist/catalog.store.d.ts +419 -0
- package/dist/catalog.store.js +175 -0
- package/dist/catalog.types.d.ts +165 -0
- package/dist/catalog.types.js +19 -0
- package/dist/catalog.workspace.d.ts +426 -0
- package/dist/catalog.workspace.js +87 -0
- package/dist/client.d.ts +86 -0
- package/dist/client.js +83 -0
- package/dist/index.d.ts +19 -0
- package/dist/index.js +109 -0
- package/dist/stores/mikro-orm-read.store.d.ts +20 -0
- package/dist/stores/mikro-orm-read.store.js +120 -0
- package/dist/transform-runner.d.ts +54 -0
- package/dist/transform-runner.js +280 -0
- 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
|
+
}
|