@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.
Files changed (74) hide show
  1. package/package.json +17 -15
  2. package/src/__tests__/omni-collection.entity.spec.ts +49 -49
  3. package/src/__tests__/omni-collection.service.spec.ts +83 -83
  4. package/src/__tests__/omni-document.entity.spec.ts +55 -55
  5. package/src/__tests__/omni-document.service.spec.ts +83 -83
  6. package/src/__tests__/omni-extension-projection-constraints.spec.ts +518 -518
  7. package/src/__tests__/omni-extension-projection.registration.spec.ts +947 -946
  8. package/src/__tests__/omni-extension-projection.service.spec.ts +806 -806
  9. package/src/__tests__/omni-external-ref.entity.spec.ts +36 -36
  10. package/src/__tests__/omni-external-ref.service.spec.ts +252 -252
  11. package/src/__tests__/omni-named.entity.spec.ts +33 -33
  12. package/src/__tests__/omni-record.entity.spec.ts +32 -32
  13. package/src/__tests__/omni-record.service.spec.ts +107 -107
  14. package/src/__tests__/omni-relation-semantics.spec.ts +102 -102
  15. package/src/__tests__/omni-relation.entity.spec.ts +38 -38
  16. package/src/__tests__/omni-scope-substrate.spec.ts +81 -81
  17. package/src/__tests__/omni-scoped.backend.spec.ts +115 -115
  18. package/src/__tests__/omni-scoped.service.spec.ts +354 -354
  19. package/src/__tests__/omnikernel.diagnostics.spec.ts +94 -94
  20. package/src/__tests__/omnikernel.module.spec.ts +38 -40
  21. package/src/__tests__/omnikernel.persistence.spec.ts +154 -154
  22. package/src/__tests__/omnikernel.public-api.spec.ts +238 -238
  23. package/src/__tests__/omnikernel.query.service.spec.ts +155 -155
  24. package/src/base/omni-base.entity.ts +41 -41
  25. package/src/base/omni-external-ref.entity.ts +53 -53
  26. package/src/base/omni-named.entity.ts +16 -16
  27. package/src/base/omni-record.entity.ts +51 -51
  28. package/src/base/omni-relation.entity.ts +72 -72
  29. package/src/index.ts +49 -49
  30. package/src/omni-collection-kind.enum.ts +10 -10
  31. package/src/omni-collection.backend.ts +16 -16
  32. package/src/omni-collection.dto.ts +170 -170
  33. package/src/omni-collection.entity.ts +21 -21
  34. package/src/omni-collection.service.ts +75 -75
  35. package/src/omni-document-kind.enum.ts +12 -12
  36. package/src/omni-document.backend.ts +16 -16
  37. package/src/omni-document.dto.ts +198 -198
  38. package/src/omni-document.entity.ts +30 -30
  39. package/src/omni-document.service.ts +75 -75
  40. package/src/omni-dto.helpers.ts +21 -21
  41. package/src/omni-extension-projection.definition.ts +294 -294
  42. package/src/omni-extension-projection.resource.ts +274 -274
  43. package/src/omni-extension-projection.service.ts +452 -452
  44. package/src/omni-external-ref-binding.validator.ts +53 -53
  45. package/src/omni-external-ref-internal-type.enum.ts +11 -11
  46. package/src/omni-external-ref.backend.ts +25 -25
  47. package/src/omni-external-ref.dto.ts +128 -128
  48. package/src/omni-external-ref.service.ts +255 -255
  49. package/src/omni-migration.ts +365 -365
  50. package/src/omni-named.backend.ts +11 -11
  51. package/src/omni-named.dto.ts +86 -86
  52. package/src/omni-projection.catalog.ts +345 -345
  53. package/src/omni-projection.lifecycle.ts +53 -53
  54. package/src/omni-record-status.enum.ts +11 -11
  55. package/src/omni-record.backend.ts +19 -19
  56. package/src/omni-record.dto.ts +157 -157
  57. package/src/omni-record.service.ts +113 -113
  58. package/src/omni-relation-kind.contract.ts +49 -49
  59. package/src/omni-relation-kind.enum.ts +12 -12
  60. package/src/omni-relation-projection.definition.ts +166 -166
  61. package/src/omni-relation-projection.resource.ts +360 -360
  62. package/src/omni-relation-projection.service.ts +610 -610
  63. package/src/omni-relation-semantics.ts +52 -52
  64. package/src/omni-relation-status.enum.ts +11 -11
  65. package/src/omni-relation.backend.ts +21 -21
  66. package/src/omni-relation.dto.ts +142 -142
  67. package/src/omni-relation.service.ts +159 -159
  68. package/src/omni-scope.ts +161 -161
  69. package/src/omni-scoped.backend.ts +110 -110
  70. package/src/omni-scoped.repository.ts +34 -34
  71. package/src/omni-scoped.service.ts +338 -338
  72. package/src/omnikernel.diagnostics.ts +83 -83
  73. package/src/omnikernel.module.ts +97 -97
  74. 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
+ }