@nest-yalc-2/omnikernel-module 2.2.2 → 2.2.6
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/package.json +17 -15
- package/src/__tests__/omni-collection.entity.spec.ts +49 -49
- package/src/__tests__/omni-collection.service.spec.ts +83 -83
- package/src/__tests__/omni-document.entity.spec.ts +55 -55
- package/src/__tests__/omni-document.service.spec.ts +83 -83
- package/src/__tests__/omni-extension-projection-constraints.spec.ts +518 -518
- package/src/__tests__/omni-extension-projection.registration.spec.ts +947 -946
- package/src/__tests__/omni-extension-projection.service.spec.ts +806 -806
- package/src/__tests__/omni-external-ref.entity.spec.ts +36 -36
- package/src/__tests__/omni-external-ref.service.spec.ts +252 -252
- package/src/__tests__/omni-named.entity.spec.ts +33 -33
- package/src/__tests__/omni-record.entity.spec.ts +32 -32
- package/src/__tests__/omni-record.service.spec.ts +107 -107
- package/src/__tests__/omni-relation-semantics.spec.ts +102 -102
- package/src/__tests__/omni-relation.entity.spec.ts +38 -38
- package/src/__tests__/omni-scope-substrate.spec.ts +81 -81
- package/src/__tests__/omni-scoped.backend.spec.ts +115 -115
- package/src/__tests__/omni-scoped.service.spec.ts +354 -354
- package/src/__tests__/omnikernel.diagnostics.spec.ts +94 -94
- package/src/__tests__/omnikernel.module.spec.ts +38 -40
- package/src/__tests__/omnikernel.persistence.spec.ts +154 -154
- package/src/__tests__/omnikernel.public-api.spec.ts +238 -238
- package/src/__tests__/omnikernel.query.service.spec.ts +155 -155
- package/src/base/omni-base.entity.ts +41 -41
- package/src/base/omni-external-ref.entity.ts +53 -53
- package/src/base/omni-named.entity.ts +16 -16
- package/src/base/omni-record.entity.ts +51 -51
- package/src/base/omni-relation.entity.ts +72 -72
- package/src/index.ts +49 -49
- package/src/omni-collection-kind.enum.ts +10 -10
- package/src/omni-collection.backend.ts +16 -16
- package/src/omni-collection.dto.ts +170 -170
- package/src/omni-collection.entity.ts +21 -21
- package/src/omni-collection.service.ts +75 -75
- package/src/omni-document-kind.enum.ts +12 -12
- package/src/omni-document.backend.ts +16 -16
- package/src/omni-document.dto.ts +198 -198
- package/src/omni-document.entity.ts +30 -30
- package/src/omni-document.service.ts +75 -75
- package/src/omni-dto.helpers.ts +21 -21
- package/src/omni-extension-projection.definition.ts +294 -294
- package/src/omni-extension-projection.resource.ts +274 -274
- package/src/omni-extension-projection.service.ts +452 -452
- package/src/omni-external-ref-binding.validator.ts +53 -53
- package/src/omni-external-ref-internal-type.enum.ts +11 -11
- package/src/omni-external-ref.backend.ts +25 -25
- package/src/omni-external-ref.dto.ts +128 -128
- package/src/omni-external-ref.service.ts +255 -255
- package/src/omni-migration.ts +365 -365
- package/src/omni-named.backend.ts +11 -11
- package/src/omni-named.dto.ts +86 -86
- package/src/omni-projection.catalog.ts +345 -345
- package/src/omni-projection.lifecycle.ts +53 -53
- package/src/omni-record-status.enum.ts +11 -11
- package/src/omni-record.backend.ts +19 -19
- package/src/omni-record.dto.ts +157 -157
- package/src/omni-record.service.ts +113 -113
- package/src/omni-relation-kind.contract.ts +49 -49
- package/src/omni-relation-kind.enum.ts +12 -12
- package/src/omni-relation-projection.definition.ts +166 -166
- package/src/omni-relation-projection.resource.ts +360 -360
- package/src/omni-relation-projection.service.ts +610 -610
- package/src/omni-relation-semantics.ts +52 -52
- package/src/omni-relation-status.enum.ts +11 -11
- package/src/omni-relation.backend.ts +21 -21
- package/src/omni-relation.dto.ts +142 -142
- package/src/omni-relation.service.ts +159 -159
- package/src/omni-scope.ts +161 -161
- package/src/omni-scoped.backend.ts +110 -110
- package/src/omni-scoped.repository.ts +34 -34
- package/src/omni-scoped.service.ts +338 -338
- package/src/omnikernel.diagnostics.ts +83 -83
- package/src/omnikernel.module.ts +97 -97
- package/src/omnikernel.query.service.ts +108 -108
|
@@ -1,345 +1,345 @@
|
|
|
1
|
-
import {
|
|
2
|
-
createProjectionDialect,
|
|
3
|
-
type ProjectionFilter,
|
|
4
|
-
} from '@nest-yalc-2/crud-gen';
|
|
5
|
-
import type { InjectionToken, Provider } from '@nestjs/common';
|
|
6
|
-
import {
|
|
7
|
-
In,
|
|
8
|
-
IsNull,
|
|
9
|
-
type EntityManager,
|
|
10
|
-
type EntityTarget,
|
|
11
|
-
type ObjectLiteral,
|
|
12
|
-
} from 'typeorm';
|
|
13
|
-
import { OmniRecordEntity } from './base/omni-record.entity.js';
|
|
14
|
-
import { OmniRelationEntity } from './base/omni-relation.entity.js';
|
|
15
|
-
import type { OmniExtensionProjectionDefinition } from './omni-extension-projection.definition.js';
|
|
16
|
-
import {
|
|
17
|
-
getOmniRelationProjectionAllowedKinds,
|
|
18
|
-
type OmniRelationProjectionDefinition,
|
|
19
|
-
} from './omni-relation-projection.definition.js';
|
|
20
|
-
import { OmniRelationStatus } from './omni-relation-status.enum.js';
|
|
21
|
-
import type { OmniScope } from './omni-scope.js';
|
|
22
|
-
|
|
23
|
-
export interface OmniExtensionProjectionReaderRegistration<
|
|
24
|
-
Entity extends ObjectLiteral = ObjectLiteral,
|
|
25
|
-
> {
|
|
26
|
-
readonly type: 'extension';
|
|
27
|
-
readonly id: string;
|
|
28
|
-
readonly entity: EntityTarget<Entity>;
|
|
29
|
-
readonly definition: OmniExtensionProjectionDefinition;
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
export interface OmniRelationProjectionReaderRegistration {
|
|
33
|
-
readonly type: 'relation';
|
|
34
|
-
readonly id: string;
|
|
35
|
-
readonly definition: OmniRelationProjectionDefinition;
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
export type OmniProjectionReaderRegistration =
|
|
39
|
-
| OmniExtensionProjectionReaderRegistration
|
|
40
|
-
| OmniRelationProjectionReaderRegistration;
|
|
41
|
-
|
|
42
|
-
export interface OmniExtensionProjectionReader<
|
|
43
|
-
Entity extends ObjectLiteral = ObjectLiteral,
|
|
44
|
-
> {
|
|
45
|
-
get(guid: string): Promise<Entity | undefined>;
|
|
46
|
-
list(options?: {
|
|
47
|
-
/** Equality only, on declared fields that explicitly allow eq filtering. */
|
|
48
|
-
where?: Readonly<Record<string, unknown>>;
|
|
49
|
-
/** A bounded result size; policies commonly use take: 1 for existence. */
|
|
50
|
-
take?: number;
|
|
51
|
-
}): Promise<Entity[]>;
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
export interface OmniRelationProjectionReader {
|
|
55
|
-
get(guid: string): Promise<OmniRelationEntity | undefined>;
|
|
56
|
-
list(options?: {
|
|
57
|
-
sourceRecordId?: string;
|
|
58
|
-
targetRecordId?: string;
|
|
59
|
-
/** A bounded result size for edge-existence checks. */
|
|
60
|
-
take?: number;
|
|
61
|
-
}): Promise<OmniRelationEntity[]>;
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
/**
|
|
65
|
-
* Manager-bound readers available to a lifecycle policy. They intentionally
|
|
66
|
-
* expose projection identities and constrained graph records, never TypeORM
|
|
67
|
-
* repositories or query runners.
|
|
68
|
-
*/
|
|
69
|
-
export interface OmniProjectionTransactionReaders {
|
|
70
|
-
extension<Entity extends ObjectLiteral = ObjectLiteral>(
|
|
71
|
-
id: string,
|
|
72
|
-
): OmniExtensionProjectionReader<Entity>;
|
|
73
|
-
relation(id: string): OmniRelationProjectionReader;
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
export interface OmniProjectionReaderCatalog {
|
|
77
|
-
bind(
|
|
78
|
-
manager: EntityManager,
|
|
79
|
-
scope: OmniScope,
|
|
80
|
-
): OmniProjectionTransactionReaders;
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
export interface OmniProjectionReaderCatalogProvider {
|
|
84
|
-
readonly token: InjectionToken<OmniProjectionReaderCatalog>;
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
export interface OmniProjectionReaderCatalogSource {
|
|
88
|
-
readonly reader: OmniProjectionReaderRegistration;
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
/** Default token for a composed set of generated Omni projection resources. */
|
|
92
|
-
export const OMNI_PROJECTION_READER_CATALOG = Symbol(
|
|
93
|
-
'OMNI_PROJECTION_READER_CATALOG',
|
|
94
|
-
);
|
|
95
|
-
|
|
96
|
-
function freeze<T>(value: T): Readonly<T> {
|
|
97
|
-
if (value && typeof value === 'object') Object.freeze(value);
|
|
98
|
-
return value;
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
function validateReaderTake(take: number | undefined): number | undefined {
|
|
102
|
-
if (take === undefined) return undefined;
|
|
103
|
-
if (!Number.isInteger(take) || take < 1 || take > 1_000) {
|
|
104
|
-
throw new TypeError(
|
|
105
|
-
'Omni projection reader take must be an integer from 1 to 1000.',
|
|
106
|
-
);
|
|
107
|
-
}
|
|
108
|
-
return take;
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
function relationWhere(
|
|
112
|
-
definition: OmniRelationProjectionDefinition,
|
|
113
|
-
): Record<string, unknown> {
|
|
114
|
-
const relation = definition.relation;
|
|
115
|
-
return {
|
|
116
|
-
kind: In([...getOmniRelationProjectionAllowedKinds(definition)]),
|
|
117
|
-
status: relation.status ?? OmniRelationStatus.Active,
|
|
118
|
-
...(relation.schema
|
|
119
|
-
? {
|
|
120
|
-
payloadSchemaId: relation.schema.id,
|
|
121
|
-
payloadSchemaVersion: relation.schema.version,
|
|
122
|
-
}
|
|
123
|
-
: {
|
|
124
|
-
payloadSchemaId: IsNull(),
|
|
125
|
-
payloadSchemaVersion: IsNull(),
|
|
126
|
-
}),
|
|
127
|
-
};
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
function extensionReader<Entity extends ObjectLiteral>(
|
|
131
|
-
manager: EntityManager,
|
|
132
|
-
scope: OmniScope,
|
|
133
|
-
registration: OmniExtensionProjectionReaderRegistration<Entity>,
|
|
134
|
-
): OmniExtensionProjectionReader<Entity> {
|
|
135
|
-
const ownerWhere = {
|
|
136
|
-
scopeId: scope.scopeId,
|
|
137
|
-
kind: registration.definition.owner.kind,
|
|
138
|
-
payloadSchemaId: registration.definition.owner.schema.id,
|
|
139
|
-
payloadSchemaVersion: registration.definition.owner.schema.version,
|
|
140
|
-
deletedAt: IsNull(),
|
|
141
|
-
};
|
|
142
|
-
const extensionRepository = manager.getRepository(registration.entity);
|
|
143
|
-
const ownerRepository = manager.getRepository(OmniRecordEntity);
|
|
144
|
-
const identity = registration.definition.identity.column;
|
|
145
|
-
const scopeColumn = registration.definition.scope.column;
|
|
146
|
-
|
|
147
|
-
const project = (entity: Entity, owner: OmniRecordEntity): Entity => {
|
|
148
|
-
const projected = {
|
|
149
|
-
...entity,
|
|
150
|
-
scopeId: owner.scopeId,
|
|
151
|
-
guid: owner.guid,
|
|
152
|
-
[registration.definition.revision.column]: owner.revision,
|
|
153
|
-
createdAt: owner.createdAt,
|
|
154
|
-
updatedAt: owner.updatedAt,
|
|
155
|
-
deletedAt: owner.deletedAt,
|
|
156
|
-
} as Entity;
|
|
157
|
-
return projected;
|
|
158
|
-
};
|
|
159
|
-
|
|
160
|
-
const getOwner = async (
|
|
161
|
-
guid: string,
|
|
162
|
-
): Promise<OmniRecordEntity | undefined> => {
|
|
163
|
-
const owner = await ownerRepository.findOne({
|
|
164
|
-
where: { ...ownerWhere, guid },
|
|
165
|
-
});
|
|
166
|
-
return owner ?? undefined;
|
|
167
|
-
};
|
|
168
|
-
|
|
169
|
-
const dialect = (() => {
|
|
170
|
-
const type = manager.connection.options.type;
|
|
171
|
-
if (type !== 'sqlite' && type !== 'postgres') {
|
|
172
|
-
throw new TypeError(
|
|
173
|
-
'Omni projection readers require SQLite or PostgreSQL.',
|
|
174
|
-
);
|
|
175
|
-
}
|
|
176
|
-
return createProjectionDialect(type);
|
|
177
|
-
})();
|
|
178
|
-
|
|
179
|
-
const filtersFor = (
|
|
180
|
-
where: Readonly<Record<string, unknown>> | undefined,
|
|
181
|
-
): ProjectionFilter[] => {
|
|
182
|
-
const filters: ProjectionFilter[] = [];
|
|
183
|
-
for (const [fieldName, value] of Object.entries(where ?? {})) {
|
|
184
|
-
const field = registration.definition.fields.find(
|
|
185
|
-
(candidate) => candidate.name === fieldName,
|
|
186
|
-
);
|
|
187
|
-
if (!field || !field.query?.filter?.includes('eq')) {
|
|
188
|
-
throw new TypeError(
|
|
189
|
-
`Omni extension reader only permits declared eq fields: ${fieldName}.`,
|
|
190
|
-
);
|
|
191
|
-
}
|
|
192
|
-
filters.push({ field, operator: 'eq', values: [value] });
|
|
193
|
-
}
|
|
194
|
-
return filters;
|
|
195
|
-
};
|
|
196
|
-
|
|
197
|
-
const projectMany = async (entities: Entity[]): Promise<Entity[]> => {
|
|
198
|
-
if (entities.length === 0) return [];
|
|
199
|
-
const candidates: unknown[] = entities.map(
|
|
200
|
-
(entity) => entity[identity as keyof Entity] as unknown,
|
|
201
|
-
);
|
|
202
|
-
const guids = candidates.filter(
|
|
203
|
-
(guid): guid is string => typeof guid === 'string',
|
|
204
|
-
);
|
|
205
|
-
const owners = await ownerRepository.find({
|
|
206
|
-
where: { ...ownerWhere, guid: In(guids) },
|
|
207
|
-
});
|
|
208
|
-
const ownersByGuid = new Map(owners.map((owner) => [owner.guid, owner]));
|
|
209
|
-
return entities.flatMap((entity) => {
|
|
210
|
-
const guid = entity[identity as keyof Entity];
|
|
211
|
-
const owner =
|
|
212
|
-
typeof guid === 'string' ? ownersByGuid.get(guid) : undefined;
|
|
213
|
-
return owner ? [project(entity, owner)] : [];
|
|
214
|
-
});
|
|
215
|
-
};
|
|
216
|
-
|
|
217
|
-
return freeze({
|
|
218
|
-
async get(guid: string): Promise<Entity | undefined> {
|
|
219
|
-
const entity = await extensionRepository.findOne({
|
|
220
|
-
where: { [scopeColumn]: scope.scopeId, [identity]: guid } as never,
|
|
221
|
-
});
|
|
222
|
-
if (!entity) return undefined;
|
|
223
|
-
const owner = await getOwner(guid);
|
|
224
|
-
return owner ? project(entity, owner) : undefined;
|
|
225
|
-
},
|
|
226
|
-
async list(
|
|
227
|
-
options: {
|
|
228
|
-
where?: Readonly<Record<string, unknown>>;
|
|
229
|
-
take?: number;
|
|
230
|
-
} = {},
|
|
231
|
-
): Promise<Entity[]> {
|
|
232
|
-
const [entities] = await dialect.findMany(
|
|
233
|
-
extensionRepository,
|
|
234
|
-
registration.definition,
|
|
235
|
-
scope.scopeId,
|
|
236
|
-
filtersFor(options.where),
|
|
237
|
-
[],
|
|
238
|
-
{ take: validateReaderTake(options.take) },
|
|
239
|
-
);
|
|
240
|
-
return projectMany(entities);
|
|
241
|
-
},
|
|
242
|
-
});
|
|
243
|
-
}
|
|
244
|
-
|
|
245
|
-
function relationReader(
|
|
246
|
-
manager: EntityManager,
|
|
247
|
-
scope: OmniScope,
|
|
248
|
-
registration: OmniRelationProjectionReaderRegistration,
|
|
249
|
-
): OmniRelationProjectionReader {
|
|
250
|
-
const repository = manager.getRepository(OmniRelationEntity);
|
|
251
|
-
const fixed = relationWhere(registration.definition);
|
|
252
|
-
return freeze({
|
|
253
|
-
async get(guid: string): Promise<OmniRelationEntity | undefined> {
|
|
254
|
-
const entity = await repository.findOne({
|
|
255
|
-
where: { scopeId: scope.scopeId, guid, ...fixed } as never,
|
|
256
|
-
});
|
|
257
|
-
return entity ?? undefined;
|
|
258
|
-
},
|
|
259
|
-
async list(
|
|
260
|
-
options: {
|
|
261
|
-
sourceRecordId?: string;
|
|
262
|
-
targetRecordId?: string;
|
|
263
|
-
take?: number;
|
|
264
|
-
} = {},
|
|
265
|
-
): Promise<OmniRelationEntity[]> {
|
|
266
|
-
const { take, ...conditions } = options;
|
|
267
|
-
return await repository.find({
|
|
268
|
-
where: { scopeId: scope.scopeId, ...fixed, ...conditions } as never,
|
|
269
|
-
take: validateReaderTake(take),
|
|
270
|
-
});
|
|
271
|
-
},
|
|
272
|
-
});
|
|
273
|
-
}
|
|
274
|
-
|
|
275
|
-
/**
|
|
276
|
-
* Creates an immutable catalog from resource registrations. It has no global
|
|
277
|
-
* state: each hook binds the catalog to its active transaction manager.
|
|
278
|
-
*/
|
|
279
|
-
export function createOmniProjectionReaderCatalog(
|
|
280
|
-
registrations: readonly (
|
|
281
|
-
OmniProjectionReaderRegistration | OmniProjectionReaderCatalogSource
|
|
282
|
-
)[],
|
|
283
|
-
): OmniProjectionReaderCatalog {
|
|
284
|
-
const byId = new Map<string, OmniProjectionReaderRegistration>();
|
|
285
|
-
for (const source of registrations) {
|
|
286
|
-
const registration = 'reader' in source ? source.reader : source;
|
|
287
|
-
if (!registration.id || byId.has(registration.id)) {
|
|
288
|
-
throw new TypeError(
|
|
289
|
-
'Omni projection reader registrations must have unique non-empty ids.',
|
|
290
|
-
);
|
|
291
|
-
}
|
|
292
|
-
byId.set(registration.id, registration);
|
|
293
|
-
}
|
|
294
|
-
const snapshot = freeze(new Map(byId));
|
|
295
|
-
return freeze({
|
|
296
|
-
bind(
|
|
297
|
-
manager: EntityManager,
|
|
298
|
-
scope: OmniScope,
|
|
299
|
-
): OmniProjectionTransactionReaders {
|
|
300
|
-
return freeze({
|
|
301
|
-
extension<Entity extends ObjectLiteral = ObjectLiteral>(
|
|
302
|
-
id: string,
|
|
303
|
-
): OmniExtensionProjectionReader<Entity> {
|
|
304
|
-
const registration = snapshot.get(id);
|
|
305
|
-
if (!registration || registration.type !== 'extension') {
|
|
306
|
-
throw new TypeError(
|
|
307
|
-
`Omni extension projection reader is not registered: ${id}.`,
|
|
308
|
-
);
|
|
309
|
-
}
|
|
310
|
-
return extensionReader(
|
|
311
|
-
manager,
|
|
312
|
-
scope,
|
|
313
|
-
registration,
|
|
314
|
-
) as OmniExtensionProjectionReader<Entity>;
|
|
315
|
-
},
|
|
316
|
-
relation(id: string): OmniRelationProjectionReader {
|
|
317
|
-
const registration = snapshot.get(id);
|
|
318
|
-
if (!registration || registration.type !== 'relation') {
|
|
319
|
-
throw new TypeError(
|
|
320
|
-
`Omni relation projection reader is not registered: ${id}.`,
|
|
321
|
-
);
|
|
322
|
-
}
|
|
323
|
-
return relationReader(manager, scope, registration);
|
|
324
|
-
},
|
|
325
|
-
});
|
|
326
|
-
},
|
|
327
|
-
});
|
|
328
|
-
}
|
|
329
|
-
|
|
330
|
-
/**
|
|
331
|
-
* Creates the one Nest provider shared by lifecycle-enabled registrations.
|
|
332
|
-
* Call it after creating the individual resource registrations, passing those
|
|
333
|
-
* registrations directly; the catalog captures their immutable reader entries.
|
|
334
|
-
*/
|
|
335
|
-
export function createOmniProjectionReaderCatalogProvider(
|
|
336
|
-
registrations: readonly (
|
|
337
|
-
OmniProjectionReaderRegistration | OmniProjectionReaderCatalogSource
|
|
338
|
-
)[],
|
|
339
|
-
token: InjectionToken<OmniProjectionReaderCatalog> = OMNI_PROJECTION_READER_CATALOG,
|
|
340
|
-
): Provider {
|
|
341
|
-
return {
|
|
342
|
-
provide: token,
|
|
343
|
-
useValue: createOmniProjectionReaderCatalog(registrations),
|
|
344
|
-
};
|
|
345
|
-
}
|
|
1
|
+
import {
|
|
2
|
+
createProjectionDialect,
|
|
3
|
+
type ProjectionFilter,
|
|
4
|
+
} from '@nest-yalc-2/crud-gen';
|
|
5
|
+
import type { InjectionToken, Provider } from '@nestjs/common';
|
|
6
|
+
import {
|
|
7
|
+
In,
|
|
8
|
+
IsNull,
|
|
9
|
+
type EntityManager,
|
|
10
|
+
type EntityTarget,
|
|
11
|
+
type ObjectLiteral,
|
|
12
|
+
} from 'typeorm';
|
|
13
|
+
import { OmniRecordEntity } from './base/omni-record.entity.js';
|
|
14
|
+
import { OmniRelationEntity } from './base/omni-relation.entity.js';
|
|
15
|
+
import type { OmniExtensionProjectionDefinition } from './omni-extension-projection.definition.js';
|
|
16
|
+
import {
|
|
17
|
+
getOmniRelationProjectionAllowedKinds,
|
|
18
|
+
type OmniRelationProjectionDefinition,
|
|
19
|
+
} from './omni-relation-projection.definition.js';
|
|
20
|
+
import { OmniRelationStatus } from './omni-relation-status.enum.js';
|
|
21
|
+
import type { OmniScope } from './omni-scope.js';
|
|
22
|
+
|
|
23
|
+
export interface OmniExtensionProjectionReaderRegistration<
|
|
24
|
+
Entity extends ObjectLiteral = ObjectLiteral,
|
|
25
|
+
> {
|
|
26
|
+
readonly type: 'extension';
|
|
27
|
+
readonly id: string;
|
|
28
|
+
readonly entity: EntityTarget<Entity>;
|
|
29
|
+
readonly definition: OmniExtensionProjectionDefinition;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export interface OmniRelationProjectionReaderRegistration {
|
|
33
|
+
readonly type: 'relation';
|
|
34
|
+
readonly id: string;
|
|
35
|
+
readonly definition: OmniRelationProjectionDefinition;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export type OmniProjectionReaderRegistration =
|
|
39
|
+
| OmniExtensionProjectionReaderRegistration
|
|
40
|
+
| OmniRelationProjectionReaderRegistration;
|
|
41
|
+
|
|
42
|
+
export interface OmniExtensionProjectionReader<
|
|
43
|
+
Entity extends ObjectLiteral = ObjectLiteral,
|
|
44
|
+
> {
|
|
45
|
+
get(guid: string): Promise<Entity | undefined>;
|
|
46
|
+
list(options?: {
|
|
47
|
+
/** Equality only, on declared fields that explicitly allow eq filtering. */
|
|
48
|
+
where?: Readonly<Record<string, unknown>>;
|
|
49
|
+
/** A bounded result size; policies commonly use take: 1 for existence. */
|
|
50
|
+
take?: number;
|
|
51
|
+
}): Promise<Entity[]>;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export interface OmniRelationProjectionReader {
|
|
55
|
+
get(guid: string): Promise<OmniRelationEntity | undefined>;
|
|
56
|
+
list(options?: {
|
|
57
|
+
sourceRecordId?: string;
|
|
58
|
+
targetRecordId?: string;
|
|
59
|
+
/** A bounded result size for edge-existence checks. */
|
|
60
|
+
take?: number;
|
|
61
|
+
}): Promise<OmniRelationEntity[]>;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Manager-bound readers available to a lifecycle policy. They intentionally
|
|
66
|
+
* expose projection identities and constrained graph records, never TypeORM
|
|
67
|
+
* repositories or query runners.
|
|
68
|
+
*/
|
|
69
|
+
export interface OmniProjectionTransactionReaders {
|
|
70
|
+
extension<Entity extends ObjectLiteral = ObjectLiteral>(
|
|
71
|
+
id: string,
|
|
72
|
+
): OmniExtensionProjectionReader<Entity>;
|
|
73
|
+
relation(id: string): OmniRelationProjectionReader;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export interface OmniProjectionReaderCatalog {
|
|
77
|
+
bind(
|
|
78
|
+
manager: EntityManager,
|
|
79
|
+
scope: OmniScope,
|
|
80
|
+
): OmniProjectionTransactionReaders;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export interface OmniProjectionReaderCatalogProvider {
|
|
84
|
+
readonly token: InjectionToken<OmniProjectionReaderCatalog>;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export interface OmniProjectionReaderCatalogSource {
|
|
88
|
+
readonly reader: OmniProjectionReaderRegistration;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/** Default token for a composed set of generated Omni projection resources. */
|
|
92
|
+
export const OMNI_PROJECTION_READER_CATALOG = Symbol(
|
|
93
|
+
'OMNI_PROJECTION_READER_CATALOG',
|
|
94
|
+
);
|
|
95
|
+
|
|
96
|
+
function freeze<T>(value: T): Readonly<T> {
|
|
97
|
+
if (value && typeof value === 'object') Object.freeze(value);
|
|
98
|
+
return value;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function validateReaderTake(take: number | undefined): number | undefined {
|
|
102
|
+
if (take === undefined) return undefined;
|
|
103
|
+
if (!Number.isInteger(take) || take < 1 || take > 1_000) {
|
|
104
|
+
throw new TypeError(
|
|
105
|
+
'Omni projection reader take must be an integer from 1 to 1000.',
|
|
106
|
+
);
|
|
107
|
+
}
|
|
108
|
+
return take;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function relationWhere(
|
|
112
|
+
definition: OmniRelationProjectionDefinition,
|
|
113
|
+
): Record<string, unknown> {
|
|
114
|
+
const relation = definition.relation;
|
|
115
|
+
return {
|
|
116
|
+
kind: In([...getOmniRelationProjectionAllowedKinds(definition)]),
|
|
117
|
+
status: relation.status ?? OmniRelationStatus.Active,
|
|
118
|
+
...(relation.schema
|
|
119
|
+
? {
|
|
120
|
+
payloadSchemaId: relation.schema.id,
|
|
121
|
+
payloadSchemaVersion: relation.schema.version,
|
|
122
|
+
}
|
|
123
|
+
: {
|
|
124
|
+
payloadSchemaId: IsNull(),
|
|
125
|
+
payloadSchemaVersion: IsNull(),
|
|
126
|
+
}),
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function extensionReader<Entity extends ObjectLiteral>(
|
|
131
|
+
manager: EntityManager,
|
|
132
|
+
scope: OmniScope,
|
|
133
|
+
registration: OmniExtensionProjectionReaderRegistration<Entity>,
|
|
134
|
+
): OmniExtensionProjectionReader<Entity> {
|
|
135
|
+
const ownerWhere = {
|
|
136
|
+
scopeId: scope.scopeId,
|
|
137
|
+
kind: registration.definition.owner.kind,
|
|
138
|
+
payloadSchemaId: registration.definition.owner.schema.id,
|
|
139
|
+
payloadSchemaVersion: registration.definition.owner.schema.version,
|
|
140
|
+
deletedAt: IsNull(),
|
|
141
|
+
};
|
|
142
|
+
const extensionRepository = manager.getRepository(registration.entity);
|
|
143
|
+
const ownerRepository = manager.getRepository(OmniRecordEntity);
|
|
144
|
+
const identity = registration.definition.identity.column;
|
|
145
|
+
const scopeColumn = registration.definition.scope.column;
|
|
146
|
+
|
|
147
|
+
const project = (entity: Entity, owner: OmniRecordEntity): Entity => {
|
|
148
|
+
const projected = {
|
|
149
|
+
...entity,
|
|
150
|
+
scopeId: owner.scopeId,
|
|
151
|
+
guid: owner.guid,
|
|
152
|
+
[registration.definition.revision.column]: owner.revision,
|
|
153
|
+
createdAt: owner.createdAt,
|
|
154
|
+
updatedAt: owner.updatedAt,
|
|
155
|
+
deletedAt: owner.deletedAt,
|
|
156
|
+
} as Entity;
|
|
157
|
+
return projected;
|
|
158
|
+
};
|
|
159
|
+
|
|
160
|
+
const getOwner = async (
|
|
161
|
+
guid: string,
|
|
162
|
+
): Promise<OmniRecordEntity | undefined> => {
|
|
163
|
+
const owner = await ownerRepository.findOne({
|
|
164
|
+
where: { ...ownerWhere, guid },
|
|
165
|
+
});
|
|
166
|
+
return owner ?? undefined;
|
|
167
|
+
};
|
|
168
|
+
|
|
169
|
+
const dialect = (() => {
|
|
170
|
+
const type = manager.connection.options.type;
|
|
171
|
+
if (type !== 'sqlite' && type !== 'postgres') {
|
|
172
|
+
throw new TypeError(
|
|
173
|
+
'Omni projection readers require SQLite or PostgreSQL.',
|
|
174
|
+
);
|
|
175
|
+
}
|
|
176
|
+
return createProjectionDialect(type);
|
|
177
|
+
})();
|
|
178
|
+
|
|
179
|
+
const filtersFor = (
|
|
180
|
+
where: Readonly<Record<string, unknown>> | undefined,
|
|
181
|
+
): ProjectionFilter[] => {
|
|
182
|
+
const filters: ProjectionFilter[] = [];
|
|
183
|
+
for (const [fieldName, value] of Object.entries(where ?? {})) {
|
|
184
|
+
const field = registration.definition.fields.find(
|
|
185
|
+
(candidate) => candidate.name === fieldName,
|
|
186
|
+
);
|
|
187
|
+
if (!field || !field.query?.filter?.includes('eq')) {
|
|
188
|
+
throw new TypeError(
|
|
189
|
+
`Omni extension reader only permits declared eq fields: ${fieldName}.`,
|
|
190
|
+
);
|
|
191
|
+
}
|
|
192
|
+
filters.push({ field, operator: 'eq', values: [value] });
|
|
193
|
+
}
|
|
194
|
+
return filters;
|
|
195
|
+
};
|
|
196
|
+
|
|
197
|
+
const projectMany = async (entities: Entity[]): Promise<Entity[]> => {
|
|
198
|
+
if (entities.length === 0) return [];
|
|
199
|
+
const candidates: unknown[] = entities.map(
|
|
200
|
+
(entity) => entity[identity as keyof Entity] as unknown,
|
|
201
|
+
);
|
|
202
|
+
const guids = candidates.filter(
|
|
203
|
+
(guid): guid is string => typeof guid === 'string',
|
|
204
|
+
);
|
|
205
|
+
const owners = await ownerRepository.find({
|
|
206
|
+
where: { ...ownerWhere, guid: In(guids) },
|
|
207
|
+
});
|
|
208
|
+
const ownersByGuid = new Map(owners.map((owner) => [owner.guid, owner]));
|
|
209
|
+
return entities.flatMap((entity) => {
|
|
210
|
+
const guid = entity[identity as keyof Entity];
|
|
211
|
+
const owner =
|
|
212
|
+
typeof guid === 'string' ? ownersByGuid.get(guid) : undefined;
|
|
213
|
+
return owner ? [project(entity, owner)] : [];
|
|
214
|
+
});
|
|
215
|
+
};
|
|
216
|
+
|
|
217
|
+
return freeze({
|
|
218
|
+
async get(guid: string): Promise<Entity | undefined> {
|
|
219
|
+
const entity = await extensionRepository.findOne({
|
|
220
|
+
where: { [scopeColumn]: scope.scopeId, [identity]: guid } as never,
|
|
221
|
+
});
|
|
222
|
+
if (!entity) return undefined;
|
|
223
|
+
const owner = await getOwner(guid);
|
|
224
|
+
return owner ? project(entity, owner) : undefined;
|
|
225
|
+
},
|
|
226
|
+
async list(
|
|
227
|
+
options: {
|
|
228
|
+
where?: Readonly<Record<string, unknown>>;
|
|
229
|
+
take?: number;
|
|
230
|
+
} = {},
|
|
231
|
+
): Promise<Entity[]> {
|
|
232
|
+
const [entities] = await dialect.findMany(
|
|
233
|
+
extensionRepository,
|
|
234
|
+
registration.definition,
|
|
235
|
+
scope.scopeId,
|
|
236
|
+
filtersFor(options.where),
|
|
237
|
+
[],
|
|
238
|
+
{ take: validateReaderTake(options.take) },
|
|
239
|
+
);
|
|
240
|
+
return projectMany(entities);
|
|
241
|
+
},
|
|
242
|
+
});
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
function relationReader(
|
|
246
|
+
manager: EntityManager,
|
|
247
|
+
scope: OmniScope,
|
|
248
|
+
registration: OmniRelationProjectionReaderRegistration,
|
|
249
|
+
): OmniRelationProjectionReader {
|
|
250
|
+
const repository = manager.getRepository(OmniRelationEntity);
|
|
251
|
+
const fixed = relationWhere(registration.definition);
|
|
252
|
+
return freeze({
|
|
253
|
+
async get(guid: string): Promise<OmniRelationEntity | undefined> {
|
|
254
|
+
const entity = await repository.findOne({
|
|
255
|
+
where: { scopeId: scope.scopeId, guid, ...fixed } as never,
|
|
256
|
+
});
|
|
257
|
+
return entity ?? undefined;
|
|
258
|
+
},
|
|
259
|
+
async list(
|
|
260
|
+
options: {
|
|
261
|
+
sourceRecordId?: string;
|
|
262
|
+
targetRecordId?: string;
|
|
263
|
+
take?: number;
|
|
264
|
+
} = {},
|
|
265
|
+
): Promise<OmniRelationEntity[]> {
|
|
266
|
+
const { take, ...conditions } = options;
|
|
267
|
+
return await repository.find({
|
|
268
|
+
where: { scopeId: scope.scopeId, ...fixed, ...conditions } as never,
|
|
269
|
+
take: validateReaderTake(take),
|
|
270
|
+
});
|
|
271
|
+
},
|
|
272
|
+
});
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
/**
|
|
276
|
+
* Creates an immutable catalog from resource registrations. It has no global
|
|
277
|
+
* state: each hook binds the catalog to its active transaction manager.
|
|
278
|
+
*/
|
|
279
|
+
export function createOmniProjectionReaderCatalog(
|
|
280
|
+
registrations: readonly (
|
|
281
|
+
OmniProjectionReaderRegistration | OmniProjectionReaderCatalogSource
|
|
282
|
+
)[],
|
|
283
|
+
): OmniProjectionReaderCatalog {
|
|
284
|
+
const byId = new Map<string, OmniProjectionReaderRegistration>();
|
|
285
|
+
for (const source of registrations) {
|
|
286
|
+
const registration = 'reader' in source ? source.reader : source;
|
|
287
|
+
if (!registration.id || byId.has(registration.id)) {
|
|
288
|
+
throw new TypeError(
|
|
289
|
+
'Omni projection reader registrations must have unique non-empty ids.',
|
|
290
|
+
);
|
|
291
|
+
}
|
|
292
|
+
byId.set(registration.id, registration);
|
|
293
|
+
}
|
|
294
|
+
const snapshot = freeze(new Map(byId));
|
|
295
|
+
return freeze({
|
|
296
|
+
bind(
|
|
297
|
+
manager: EntityManager,
|
|
298
|
+
scope: OmniScope,
|
|
299
|
+
): OmniProjectionTransactionReaders {
|
|
300
|
+
return freeze({
|
|
301
|
+
extension<Entity extends ObjectLiteral = ObjectLiteral>(
|
|
302
|
+
id: string,
|
|
303
|
+
): OmniExtensionProjectionReader<Entity> {
|
|
304
|
+
const registration = snapshot.get(id);
|
|
305
|
+
if (!registration || registration.type !== 'extension') {
|
|
306
|
+
throw new TypeError(
|
|
307
|
+
`Omni extension projection reader is not registered: ${id}.`,
|
|
308
|
+
);
|
|
309
|
+
}
|
|
310
|
+
return extensionReader(
|
|
311
|
+
manager,
|
|
312
|
+
scope,
|
|
313
|
+
registration,
|
|
314
|
+
) as OmniExtensionProjectionReader<Entity>;
|
|
315
|
+
},
|
|
316
|
+
relation(id: string): OmniRelationProjectionReader {
|
|
317
|
+
const registration = snapshot.get(id);
|
|
318
|
+
if (!registration || registration.type !== 'relation') {
|
|
319
|
+
throw new TypeError(
|
|
320
|
+
`Omni relation projection reader is not registered: ${id}.`,
|
|
321
|
+
);
|
|
322
|
+
}
|
|
323
|
+
return relationReader(manager, scope, registration);
|
|
324
|
+
},
|
|
325
|
+
});
|
|
326
|
+
},
|
|
327
|
+
});
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
/**
|
|
331
|
+
* Creates the one Nest provider shared by lifecycle-enabled registrations.
|
|
332
|
+
* Call it after creating the individual resource registrations, passing those
|
|
333
|
+
* registrations directly; the catalog captures their immutable reader entries.
|
|
334
|
+
*/
|
|
335
|
+
export function createOmniProjectionReaderCatalogProvider(
|
|
336
|
+
registrations: readonly (
|
|
337
|
+
OmniProjectionReaderRegistration | OmniProjectionReaderCatalogSource
|
|
338
|
+
)[],
|
|
339
|
+
token: InjectionToken<OmniProjectionReaderCatalog> = OMNI_PROJECTION_READER_CATALOG,
|
|
340
|
+
): Provider {
|
|
341
|
+
return {
|
|
342
|
+
provide: token,
|
|
343
|
+
useValue: createOmniProjectionReaderCatalog(registrations),
|
|
344
|
+
};
|
|
345
|
+
}
|