@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,518 +1,518 @@
1
- import { afterEach, describe, expect, it } from "@jest/globals";
2
- import {
3
- compileProjectionUniqueConstraintPredicate,
4
- createProjectionDialect,
5
- } from "@nest-yalc-2/crud-gen";
6
- import { DataSource } from "typeorm";
7
-
8
- import { OmniNamedEntity } from '../base/omni-named.entity.js';
9
- import { OmniRecordEntity } from '../base/omni-record.entity.js';
10
- import { OmniRelationEntity } from '../base/omni-relation.entity.js';
11
- import { OmniExternalRefEntity } from '../base/omni-external-ref.entity.js';
12
- import { OmniCollectionEntity } from '../omni-collection.entity.js';
13
- import { OmniDocumentEntity } from '../omni-document.entity.js';
14
- import { OmniRecordStatus } from '../omni-record-status.enum.js';
15
- import {
16
- createOmniExtensionProjectionEntity,
17
- createOmniExtensionProjectionTable,
18
- defineOmniExtensionProjection,
19
- } from '../omni-extension-projection.definition.js';
20
- import { captureOmniMigrationSnapshot } from '../omni-migration.js';
21
-
22
- const constraintName = "projection_status_initial_kind_unique";
23
- const fullConstraintName = "projection_status_kind_state_unique";
24
- const referenceName = "projection_status_parent_fk";
25
- const tableName = "projection-status-constraint-probe";
26
- const alphaScope = "scope-alpha";
27
- const bravoScope = "scope-bravo";
28
-
29
- type ConstraintRow = {
30
- scopeId: string;
31
- guid: string;
32
- parentGuid: string;
33
- key: string;
34
- kind: string;
35
- state: string;
36
- isInitial: boolean;
37
- payload: Record<string, unknown>;
38
- };
39
-
40
- function definitionInput() {
41
- return {
42
- id: "projection.status.constraint-probe.v1",
43
- tableName,
44
- identity: { column: "guid", uniqueWithinScope: true as const },
45
- scope: { column: "scopeId", serverOwned: true as const },
46
- revision: { column: "revision" },
47
- payload: { column: "payload", allowCreate: true },
48
- deletion: "hard" as const,
49
- fields: [
50
- {
51
- name: "guid",
52
- storage: "column" as const,
53
- column: "guid",
54
- codec: "uuid" as const,
55
- nullable: false,
56
- requiredOnCreate: true,
57
- },
58
- {
59
- name: "parentGuid",
60
- storage: "column" as const,
61
- column: "parentGuid",
62
- codec: "uuid" as const,
63
- nullable: false,
64
- requiredOnCreate: true,
65
- },
66
- {
67
- name: "key",
68
- storage: "column" as const,
69
- column: "key",
70
- codec: "string" as const,
71
- nullable: false,
72
- requiredOnCreate: true,
73
- },
74
- {
75
- name: "kind",
76
- storage: "column" as const,
77
- column: "kind",
78
- codec: "string" as const,
79
- nullable: false,
80
- requiredOnCreate: true,
81
- },
82
- {
83
- name: "state",
84
- storage: "column" as const,
85
- column: "state",
86
- codec: "string" as const,
87
- nullable: false,
88
- requiredOnCreate: true,
89
- },
90
- {
91
- name: "isInitial",
92
- storage: "column" as const,
93
- column: "isInitial",
94
- codec: "boolean" as const,
95
- nullable: false,
96
- requiredOnCreate: true,
97
- },
98
- {
99
- name: "metadata",
100
- storage: "json" as const,
101
- path: ["metadata"],
102
- codec: "json" as const,
103
- nullable: true,
104
- },
105
- ],
106
- references: [
107
- {
108
- name: referenceName,
109
- fields: ["parentGuid"],
110
- target: {
111
- tableName: "omni-record",
112
- scopeColumn: "scopeId",
113
- identityColumns: ["guid"],
114
- },
115
- onDelete: "RESTRICT" as const,
116
- },
117
- ],
118
- uniqueConstraints: [
119
- {
120
- name: constraintName,
121
- fields: ["kind"],
122
- predicate: { isInitial: true, state: "active" },
123
- },
124
- {
125
- name: fullConstraintName,
126
- fields: ["key", "kind"],
127
- },
128
- ],
129
- owner: {
130
- kind: "projection_status_constraint_probe",
131
- title: "Projection status constraint probe",
132
- status: OmniRecordStatus.Active,
133
- schema: { id: "projection.status.constraint-probe.v1", version: 1 },
134
- },
135
- };
136
- }
137
-
138
- const definition = defineOmniExtensionProjection(definitionInput());
139
-
140
- function expectInvalidDefinition(mutate: (input: any) => void): void {
141
- const input = definitionInput();
142
- mutate(input);
143
- expect(() => defineOmniExtensionProjection(input)).toThrow(TypeError);
144
- }
145
-
146
- function expectConstraintMetadata(
147
- table: ReturnType<typeof createOmniExtensionProjectionTable>,
148
- dialect: "sqlite" | "postgres",
149
- ): void {
150
- const reference = table.foreignKeys.find(
151
- (foreignKey) => foreignKey.name === referenceName,
152
- );
153
- expect(reference).toMatchObject({
154
- columnNames: ["scopeId", "parentGuid"],
155
- referencedTableName: "omni-record",
156
- referencedColumnNames: ["scopeId", "guid"],
157
- onDelete: "RESTRICT",
158
- });
159
- expect(table.indices).toEqual(
160
- expect.arrayContaining([
161
- expect.objectContaining({
162
- name: `${referenceName}_idx`,
163
- columnNames: ["scopeId", "parentGuid"],
164
- isUnique: false,
165
- }),
166
- expect.objectContaining({
167
- name: constraintName,
168
- columnNames: ["scopeId", "kind"],
169
- isUnique: true,
170
- where:
171
- dialect === "sqlite"
172
- ? '"isInitial" = 1 AND "state" = \'active\''
173
- : '"isInitial" = TRUE AND "state" = \'active\'',
174
- }),
175
- expect.objectContaining({
176
- name: fullConstraintName,
177
- columnNames: ["scopeId", "key", "kind"],
178
- isUnique: true,
179
- where: "",
180
- }),
181
- ]),
182
- );
183
- }
184
-
185
- describe("Omni extension projection declarative constraints", () => {
186
- it("deep-freezes valid metadata and rejects unsafe declarative constraints", () => {
187
- expect(Object.isFrozen(definition)).toBe(true);
188
- expect(Object.isFrozen(definition.references)).toBe(true);
189
- expect(Object.isFrozen(definition.references![0]!)).toBe(true);
190
- expect(Object.isFrozen(definition.uniqueConstraints)).toBe(true);
191
- expect(Object.isFrozen(definition.uniqueConstraints![0]!.predicate)).toBe(
192
- true,
193
- );
194
-
195
- expectInvalidDefinition((input) => {
196
- input.references[0].fields = ["metadata"];
197
- });
198
- expectInvalidDefinition((input) => {
199
- input.uniqueConstraints[0].predicate = { metadata: "not portable" };
200
- });
201
- expectInvalidDefinition((input) => {
202
- input.references[0].fields = ["missing"];
203
- });
204
- expectInvalidDefinition((input) => {
205
- input.references[0].fields = ["guid"];
206
- });
207
- expectInvalidDefinition((input) => {
208
- input.uniqueConstraints[0].fields = ["scopeId"];
209
- });
210
- expectInvalidDefinition((input) => {
211
- input.references.push({ ...input.references[0] });
212
- });
213
- expectInvalidDefinition((input) => {
214
- input.uniqueConstraints[0].name = referenceName;
215
- });
216
- expectInvalidDefinition((input) => {
217
- input.references[0].target.identityColumns = ["guid", "kind"];
218
- });
219
- expectInvalidDefinition((input) => {
220
- input.references[0].onDelete = "CASCADE";
221
- });
222
- expectInvalidDefinition((input) => {
223
- input.references[0].name = "unsafe;reference";
224
- });
225
- });
226
-
227
- it("compiles portable predicate literals and Table metadata for both dialects", () => {
228
- const uniqueConstraint = definition.uniqueConstraints![0]!;
229
- expect(
230
- compileProjectionUniqueConstraintPredicate(
231
- definition,
232
- uniqueConstraint,
233
- "sqlite",
234
- ),
235
- ).toBe('"isInitial" = 1 AND "state" = \'active\'');
236
- expect(
237
- compileProjectionUniqueConstraintPredicate(
238
- definition,
239
- uniqueConstraint,
240
- "postgres",
241
- ),
242
- ).toBe('"isInitial" = TRUE AND "state" = \'active\'');
243
- expect(
244
- compileProjectionUniqueConstraintPredicate(
245
- definition,
246
- definition.uniqueConstraints![1]!,
247
- "sqlite",
248
- ),
249
- ).toBeUndefined();
250
-
251
- const sqliteTable = createOmniExtensionProjectionTable(
252
- definition,
253
- createProjectionDialect("sqlite"),
254
- );
255
- const postgresTable = createOmniExtensionProjectionTable(
256
- definition,
257
- createProjectionDialect("postgres"),
258
- );
259
- expect(sqliteTable.findColumnByName("payload")?.type).toBe("text");
260
- expect(postgresTable.findColumnByName("payload")?.type).toBe("jsonb");
261
- expectConstraintMetadata(sqliteTable, "sqlite");
262
- expectConstraintMetadata(postgresTable, "postgres");
263
- });
264
- });
265
-
266
- describe("Omni extension projection constraints in SQLite", () => {
267
- let dataSource: DataSource;
268
-
269
- afterEach(async () => {
270
- if (dataSource?.isInitialized) await dataSource.destroy();
271
- });
272
-
273
- it("enforces same-scope references and partial uniqueness in live metadata and snapshots", async () => {
274
- const entity = createOmniExtensionProjectionEntity<ConstraintRow>(
275
- definition,
276
- createProjectionDialect("sqlite"),
277
- );
278
- dataSource = new DataSource({
279
- type: "sqlite",
280
- database: ":memory:",
281
- dropSchema: true,
282
- synchronize: true,
283
- entities: [
284
- OmniNamedEntity,
285
- OmniRecordEntity,
286
- OmniDocumentEntity,
287
- OmniCollectionEntity,
288
- OmniRelationEntity,
289
- OmniExternalRefEntity,
290
- entity,
291
- ],
292
- });
293
- await dataSource.initialize();
294
-
295
- const metadata = dataSource.getMetadata(entity);
296
- expect(metadata.foreignKeys).toEqual(
297
- expect.arrayContaining([
298
- expect.objectContaining({
299
- name: referenceName,
300
- columnNames: ["scopeId", "parentGuid"],
301
- referencedTablePath: "omni-record",
302
- referencedColumnNames: ["scopeId", "guid"],
303
- onDelete: "RESTRICT",
304
- }),
305
- ]),
306
- );
307
- expect(metadata.indices).toEqual(
308
- expect.arrayContaining([
309
- expect.objectContaining({
310
- givenName: `${referenceName}_idx`,
311
- where: undefined,
312
- isUnique: false,
313
- }),
314
- expect.objectContaining({
315
- givenName: constraintName,
316
- where: '"isInitial" = 1 AND "state" = \'active\'',
317
- isUnique: true,
318
- }),
319
- ]),
320
- );
321
-
322
- const owners = dataSource.getRepository(OmniRecordEntity);
323
- const extensions = dataSource.getRepository<ConstraintRow>(entity);
324
- const parentGuid = "10000000-0000-4000-8000-000000000001";
325
- const crossScopeParentGuid = "10000000-0000-4000-8000-000000000002";
326
- const ownerGuid = "10000000-0000-4000-8000-000000000003";
327
- const duplicateOwnerGuid = "10000000-0000-4000-8000-000000000004";
328
- const excludedOwnerGuid = "10000000-0000-4000-8000-000000000005";
329
- const missingParentOwnerGuid = "10000000-0000-4000-8000-000000000006";
330
- const crossScopeOwnerGuid = "10000000-0000-4000-8000-000000000007";
331
-
332
- const createOwner = async (
333
- scopeId: string,
334
- guid: string,
335
- ): Promise<void> => {
336
- await owners.insert({
337
- scopeId,
338
- guid,
339
- title: `Owner ${guid}`,
340
- kind: "projection_status_constraint_probe",
341
- status: OmniRecordStatus.Active,
342
- payloadSchemaId: definition.owner.schema.id,
343
- payloadSchemaVersion: definition.owner.schema.version,
344
- });
345
- };
346
- await createOwner(alphaScope, parentGuid);
347
- await createOwner(bravoScope, crossScopeParentGuid);
348
- await createOwner(alphaScope, ownerGuid);
349
- await createOwner(alphaScope, duplicateOwnerGuid);
350
- await createOwner(alphaScope, excludedOwnerGuid);
351
- await createOwner(alphaScope, missingParentOwnerGuid);
352
- await createOwner(alphaScope, crossScopeOwnerGuid);
353
-
354
- await extensions.insert({
355
- scopeId: alphaScope,
356
- guid: ownerGuid,
357
- parentGuid,
358
- key: "primary",
359
- kind: "approval",
360
- state: "active",
361
- isInitial: true,
362
- payload: {},
363
- });
364
- await expect(
365
- extensions.insert({
366
- scopeId: alphaScope,
367
- guid: missingParentOwnerGuid,
368
- parentGuid: "10000000-0000-4000-8000-000000000099",
369
- key: "missing-parent",
370
- kind: "missing-parent",
371
- state: "active",
372
- isInitial: false,
373
- payload: {},
374
- }),
375
- ).rejects.toThrow();
376
- await expect(
377
- extensions.insert({
378
- scopeId: alphaScope,
379
- guid: crossScopeOwnerGuid,
380
- parentGuid: crossScopeParentGuid,
381
- key: "cross-scope",
382
- kind: "cross-scope-parent",
383
- state: "active",
384
- isInitial: false,
385
- payload: {},
386
- }),
387
- ).rejects.toThrow();
388
- await expect(
389
- extensions.insert({
390
- scopeId: alphaScope,
391
- guid: duplicateOwnerGuid,
392
- parentGuid,
393
- key: "duplicate-partial",
394
- kind: "approval",
395
- state: "active",
396
- isInitial: true,
397
- payload: {},
398
- }),
399
- ).rejects.toThrow();
400
- await extensions.insert({
401
- scopeId: alphaScope,
402
- guid: excludedOwnerGuid,
403
- parentGuid,
404
- key: "inactive",
405
- kind: "approval",
406
- state: "active",
407
- isInitial: false,
408
- payload: {},
409
- });
410
- await expect(
411
- owners.delete({ scopeId: alphaScope, guid: parentGuid }),
412
- ).rejects.toThrow();
413
-
414
- const snapshot = captureOmniMigrationSnapshot(
415
- "projection-status-constraint-probe-sqlite-v1",
416
- dataSource,
417
- [{ entities: [entity], definition }],
418
- );
419
- const capturedTable = snapshot.tables.find(
420
- (table) => table.name === tableName,
421
- )!;
422
- expect(capturedTable.foreignKeys).toEqual(
423
- expect.arrayContaining([
424
- expect.objectContaining({
425
- name: referenceName,
426
- columnNames: ["scopeId", "parentGuid"],
427
- referencedTableName: "omni-record",
428
- referencedColumnNames: ["scopeId", "guid"],
429
- onDelete: "RESTRICT",
430
- }),
431
- ]),
432
- );
433
- expect(capturedTable.indices).toEqual(
434
- expect.arrayContaining([
435
- expect.objectContaining({
436
- name: `${referenceName}_idx`,
437
- columnNames: ["scopeId", "parentGuid"],
438
- isUnique: false,
439
- }),
440
- expect.objectContaining({
441
- name: constraintName,
442
- columnNames: ["scopeId", "kind"],
443
- isUnique: true,
444
- where: '"isInitial" = 1 AND "state" = \'active\'',
445
- }),
446
- ]),
447
- );
448
- });
449
- });
450
-
451
- const postgresUrl = process.env.OMNI_EXTENSION_PROJECTION_POSTGRES_URL;
452
- const postgresIt = postgresUrl ? it : it.skip;
453
-
454
- describe("Omni extension projection constraints in PostgreSQL", () => {
455
- postgresIt(
456
- "initializes an ephemeral database with PostgreSQL metadata",
457
- async () => {
458
- const entity = createOmniExtensionProjectionEntity<ConstraintRow>(
459
- definition,
460
- createProjectionDialect("postgres"),
461
- );
462
- const dataSource = new DataSource({
463
- type: "postgres",
464
- url: postgresUrl!,
465
- ssl: false,
466
- dropSchema: true,
467
- synchronize: true,
468
- entities: [
469
- OmniNamedEntity,
470
- OmniRecordEntity,
471
- OmniDocumentEntity,
472
- OmniCollectionEntity,
473
- OmniRelationEntity,
474
- OmniExternalRefEntity,
475
- entity,
476
- ],
477
- });
478
- try {
479
- await dataSource.initialize();
480
- const metadata = dataSource.getMetadata(entity);
481
- expect(metadata.indices).toEqual(
482
- expect.arrayContaining([
483
- expect.objectContaining({
484
- givenName: constraintName,
485
- where: '"isInitial" = TRUE AND "state" = \'active\'',
486
- isUnique: true,
487
- }),
488
- ]),
489
- );
490
- const columns = await dataSource.query(
491
- "SELECT column_name, data_type FROM information_schema.columns WHERE table_name = $1 AND column_name IN ($2, $3)",
492
- [tableName, "payload", "isInitial"],
493
- );
494
- expect(columns).toEqual(
495
- expect.arrayContaining([
496
- expect.objectContaining({
497
- column_name: "payload",
498
- data_type: "jsonb",
499
- }),
500
- expect.objectContaining({
501
- column_name: "isInitial",
502
- data_type: "boolean",
503
- }),
504
- ]),
505
- );
506
- const indexes = await dataSource.query(
507
- "SELECT indexdef FROM pg_indexes WHERE tablename = $1 AND indexname = $2",
508
- [tableName, constraintName],
509
- );
510
- expect(indexes[0]?.indexdef).toContain("WHERE");
511
- expect(indexes[0]?.indexdef).toContain('"isInitial"');
512
- expect(indexes[0]?.indexdef).toContain("state");
513
- } finally {
514
- if (dataSource.isInitialized) await dataSource.destroy();
515
- }
516
- },
517
- );
518
- });
1
+ import { afterEach, describe, expect, it } from "@jest/globals";
2
+ import {
3
+ compileProjectionUniqueConstraintPredicate,
4
+ createProjectionDialect,
5
+ } from "@nest-yalc-2/crud-gen";
6
+ import { DataSource } from "typeorm";
7
+
8
+ import { OmniNamedEntity } from '../base/omni-named.entity.js';
9
+ import { OmniRecordEntity } from '../base/omni-record.entity.js';
10
+ import { OmniRelationEntity } from '../base/omni-relation.entity.js';
11
+ import { OmniExternalRefEntity } from '../base/omni-external-ref.entity.js';
12
+ import { OmniCollectionEntity } from '../omni-collection.entity.js';
13
+ import { OmniDocumentEntity } from '../omni-document.entity.js';
14
+ import { OmniRecordStatus } from '../omni-record-status.enum.js';
15
+ import {
16
+ createOmniExtensionProjectionEntity,
17
+ createOmniExtensionProjectionTable,
18
+ defineOmniExtensionProjection,
19
+ } from '../omni-extension-projection.definition.js';
20
+ import { captureOmniMigrationSnapshot } from '../omni-migration.js';
21
+
22
+ const constraintName = "projection_status_initial_kind_unique";
23
+ const fullConstraintName = "projection_status_kind_state_unique";
24
+ const referenceName = "projection_status_parent_fk";
25
+ const tableName = "projection-status-constraint-probe";
26
+ const alphaScope = "scope-alpha";
27
+ const bravoScope = "scope-bravo";
28
+
29
+ type ConstraintRow = {
30
+ scopeId: string;
31
+ guid: string;
32
+ parentGuid: string;
33
+ key: string;
34
+ kind: string;
35
+ state: string;
36
+ isInitial: boolean;
37
+ payload: Record<string, unknown>;
38
+ };
39
+
40
+ function definitionInput() {
41
+ return {
42
+ id: "projection.status.constraint-probe.v1",
43
+ tableName,
44
+ identity: { column: "guid", uniqueWithinScope: true as const },
45
+ scope: { column: "scopeId", serverOwned: true as const },
46
+ revision: { column: "revision" },
47
+ payload: { column: "payload", allowCreate: true },
48
+ deletion: "hard" as const,
49
+ fields: [
50
+ {
51
+ name: "guid",
52
+ storage: "column" as const,
53
+ column: "guid",
54
+ codec: "uuid" as const,
55
+ nullable: false,
56
+ requiredOnCreate: true,
57
+ },
58
+ {
59
+ name: "parentGuid",
60
+ storage: "column" as const,
61
+ column: "parentGuid",
62
+ codec: "uuid" as const,
63
+ nullable: false,
64
+ requiredOnCreate: true,
65
+ },
66
+ {
67
+ name: "key",
68
+ storage: "column" as const,
69
+ column: "key",
70
+ codec: "string" as const,
71
+ nullable: false,
72
+ requiredOnCreate: true,
73
+ },
74
+ {
75
+ name: "kind",
76
+ storage: "column" as const,
77
+ column: "kind",
78
+ codec: "string" as const,
79
+ nullable: false,
80
+ requiredOnCreate: true,
81
+ },
82
+ {
83
+ name: "state",
84
+ storage: "column" as const,
85
+ column: "state",
86
+ codec: "string" as const,
87
+ nullable: false,
88
+ requiredOnCreate: true,
89
+ },
90
+ {
91
+ name: "isInitial",
92
+ storage: "column" as const,
93
+ column: "isInitial",
94
+ codec: "boolean" as const,
95
+ nullable: false,
96
+ requiredOnCreate: true,
97
+ },
98
+ {
99
+ name: "metadata",
100
+ storage: "json" as const,
101
+ path: ["metadata"],
102
+ codec: "json" as const,
103
+ nullable: true,
104
+ },
105
+ ],
106
+ references: [
107
+ {
108
+ name: referenceName,
109
+ fields: ["parentGuid"],
110
+ target: {
111
+ tableName: "omni-record",
112
+ scopeColumn: "scopeId",
113
+ identityColumns: ["guid"],
114
+ },
115
+ onDelete: "RESTRICT" as const,
116
+ },
117
+ ],
118
+ uniqueConstraints: [
119
+ {
120
+ name: constraintName,
121
+ fields: ["kind"],
122
+ predicate: { isInitial: true, state: "active" },
123
+ },
124
+ {
125
+ name: fullConstraintName,
126
+ fields: ["key", "kind"],
127
+ },
128
+ ],
129
+ owner: {
130
+ kind: "projection_status_constraint_probe",
131
+ title: "Projection status constraint probe",
132
+ status: OmniRecordStatus.Active,
133
+ schema: { id: "projection.status.constraint-probe.v1", version: 1 },
134
+ },
135
+ };
136
+ }
137
+
138
+ const definition = defineOmniExtensionProjection(definitionInput());
139
+
140
+ function expectInvalidDefinition(mutate: (input: any) => void): void {
141
+ const input = definitionInput();
142
+ mutate(input);
143
+ expect(() => defineOmniExtensionProjection(input)).toThrow(TypeError);
144
+ }
145
+
146
+ function expectConstraintMetadata(
147
+ table: ReturnType<typeof createOmniExtensionProjectionTable>,
148
+ dialect: "sqlite" | "postgres",
149
+ ): void {
150
+ const reference = table.foreignKeys.find(
151
+ (foreignKey) => foreignKey.name === referenceName,
152
+ );
153
+ expect(reference).toMatchObject({
154
+ columnNames: ["scopeId", "parentGuid"],
155
+ referencedTableName: "omni-record",
156
+ referencedColumnNames: ["scopeId", "guid"],
157
+ onDelete: "RESTRICT",
158
+ });
159
+ expect(table.indices).toEqual(
160
+ expect.arrayContaining([
161
+ expect.objectContaining({
162
+ name: `${referenceName}_idx`,
163
+ columnNames: ["scopeId", "parentGuid"],
164
+ isUnique: false,
165
+ }),
166
+ expect.objectContaining({
167
+ name: constraintName,
168
+ columnNames: ["scopeId", "kind"],
169
+ isUnique: true,
170
+ where:
171
+ dialect === "sqlite"
172
+ ? '"isInitial" = 1 AND "state" = \'active\''
173
+ : '"isInitial" = TRUE AND "state" = \'active\'',
174
+ }),
175
+ expect.objectContaining({
176
+ name: fullConstraintName,
177
+ columnNames: ["scopeId", "key", "kind"],
178
+ isUnique: true,
179
+ where: "",
180
+ }),
181
+ ]),
182
+ );
183
+ }
184
+
185
+ describe("Omni extension projection declarative constraints", () => {
186
+ it("deep-freezes valid metadata and rejects unsafe declarative constraints", () => {
187
+ expect(Object.isFrozen(definition)).toBe(true);
188
+ expect(Object.isFrozen(definition.references)).toBe(true);
189
+ expect(Object.isFrozen(definition.references![0]!)).toBe(true);
190
+ expect(Object.isFrozen(definition.uniqueConstraints)).toBe(true);
191
+ expect(Object.isFrozen(definition.uniqueConstraints![0]!.predicate)).toBe(
192
+ true,
193
+ );
194
+
195
+ expectInvalidDefinition((input) => {
196
+ input.references[0].fields = ["metadata"];
197
+ });
198
+ expectInvalidDefinition((input) => {
199
+ input.uniqueConstraints[0].predicate = { metadata: "not portable" };
200
+ });
201
+ expectInvalidDefinition((input) => {
202
+ input.references[0].fields = ["missing"];
203
+ });
204
+ expectInvalidDefinition((input) => {
205
+ input.references[0].fields = ["guid"];
206
+ });
207
+ expectInvalidDefinition((input) => {
208
+ input.uniqueConstraints[0].fields = ["scopeId"];
209
+ });
210
+ expectInvalidDefinition((input) => {
211
+ input.references.push({ ...input.references[0] });
212
+ });
213
+ expectInvalidDefinition((input) => {
214
+ input.uniqueConstraints[0].name = referenceName;
215
+ });
216
+ expectInvalidDefinition((input) => {
217
+ input.references[0].target.identityColumns = ["guid", "kind"];
218
+ });
219
+ expectInvalidDefinition((input) => {
220
+ input.references[0].onDelete = "CASCADE";
221
+ });
222
+ expectInvalidDefinition((input) => {
223
+ input.references[0].name = "unsafe;reference";
224
+ });
225
+ });
226
+
227
+ it("compiles portable predicate literals and Table metadata for both dialects", () => {
228
+ const uniqueConstraint = definition.uniqueConstraints![0]!;
229
+ expect(
230
+ compileProjectionUniqueConstraintPredicate(
231
+ definition,
232
+ uniqueConstraint,
233
+ "sqlite",
234
+ ),
235
+ ).toBe('"isInitial" = 1 AND "state" = \'active\'');
236
+ expect(
237
+ compileProjectionUniqueConstraintPredicate(
238
+ definition,
239
+ uniqueConstraint,
240
+ "postgres",
241
+ ),
242
+ ).toBe('"isInitial" = TRUE AND "state" = \'active\'');
243
+ expect(
244
+ compileProjectionUniqueConstraintPredicate(
245
+ definition,
246
+ definition.uniqueConstraints![1]!,
247
+ "sqlite",
248
+ ),
249
+ ).toBeUndefined();
250
+
251
+ const sqliteTable = createOmniExtensionProjectionTable(
252
+ definition,
253
+ createProjectionDialect("sqlite"),
254
+ );
255
+ const postgresTable = createOmniExtensionProjectionTable(
256
+ definition,
257
+ createProjectionDialect("postgres"),
258
+ );
259
+ expect(sqliteTable.findColumnByName("payload")?.type).toBe("text");
260
+ expect(postgresTable.findColumnByName("payload")?.type).toBe("jsonb");
261
+ expectConstraintMetadata(sqliteTable, "sqlite");
262
+ expectConstraintMetadata(postgresTable, "postgres");
263
+ });
264
+ });
265
+
266
+ describe("Omni extension projection constraints in SQLite", () => {
267
+ let dataSource: DataSource;
268
+
269
+ afterEach(async () => {
270
+ if (dataSource?.isInitialized) await dataSource.destroy();
271
+ });
272
+
273
+ it("enforces same-scope references and partial uniqueness in live metadata and snapshots", async () => {
274
+ const entity = createOmniExtensionProjectionEntity<ConstraintRow>(
275
+ definition,
276
+ createProjectionDialect("sqlite"),
277
+ );
278
+ dataSource = new DataSource({
279
+ type: "sqlite",
280
+ database: ":memory:",
281
+ dropSchema: true,
282
+ synchronize: true,
283
+ entities: [
284
+ OmniNamedEntity,
285
+ OmniRecordEntity,
286
+ OmniDocumentEntity,
287
+ OmniCollectionEntity,
288
+ OmniRelationEntity,
289
+ OmniExternalRefEntity,
290
+ entity,
291
+ ],
292
+ });
293
+ await dataSource.initialize();
294
+
295
+ const metadata = dataSource.getMetadata(entity);
296
+ expect(metadata.foreignKeys).toEqual(
297
+ expect.arrayContaining([
298
+ expect.objectContaining({
299
+ name: referenceName,
300
+ columnNames: ["scopeId", "parentGuid"],
301
+ referencedTablePath: "omni-record",
302
+ referencedColumnNames: ["scopeId", "guid"],
303
+ onDelete: "RESTRICT",
304
+ }),
305
+ ]),
306
+ );
307
+ expect(metadata.indices).toEqual(
308
+ expect.arrayContaining([
309
+ expect.objectContaining({
310
+ givenName: `${referenceName}_idx`,
311
+ where: undefined,
312
+ isUnique: false,
313
+ }),
314
+ expect.objectContaining({
315
+ givenName: constraintName,
316
+ where: '"isInitial" = 1 AND "state" = \'active\'',
317
+ isUnique: true,
318
+ }),
319
+ ]),
320
+ );
321
+
322
+ const owners = dataSource.getRepository(OmniRecordEntity);
323
+ const extensions = dataSource.getRepository<ConstraintRow>(entity);
324
+ const parentGuid = "10000000-0000-4000-8000-000000000001";
325
+ const crossScopeParentGuid = "10000000-0000-4000-8000-000000000002";
326
+ const ownerGuid = "10000000-0000-4000-8000-000000000003";
327
+ const duplicateOwnerGuid = "10000000-0000-4000-8000-000000000004";
328
+ const excludedOwnerGuid = "10000000-0000-4000-8000-000000000005";
329
+ const missingParentOwnerGuid = "10000000-0000-4000-8000-000000000006";
330
+ const crossScopeOwnerGuid = "10000000-0000-4000-8000-000000000007";
331
+
332
+ const createOwner = async (
333
+ scopeId: string,
334
+ guid: string,
335
+ ): Promise<void> => {
336
+ await owners.insert({
337
+ scopeId,
338
+ guid,
339
+ title: `Owner ${guid}`,
340
+ kind: "projection_status_constraint_probe",
341
+ status: OmniRecordStatus.Active,
342
+ payloadSchemaId: definition.owner.schema.id,
343
+ payloadSchemaVersion: definition.owner.schema.version,
344
+ });
345
+ };
346
+ await createOwner(alphaScope, parentGuid);
347
+ await createOwner(bravoScope, crossScopeParentGuid);
348
+ await createOwner(alphaScope, ownerGuid);
349
+ await createOwner(alphaScope, duplicateOwnerGuid);
350
+ await createOwner(alphaScope, excludedOwnerGuid);
351
+ await createOwner(alphaScope, missingParentOwnerGuid);
352
+ await createOwner(alphaScope, crossScopeOwnerGuid);
353
+
354
+ await extensions.insert({
355
+ scopeId: alphaScope,
356
+ guid: ownerGuid,
357
+ parentGuid,
358
+ key: "primary",
359
+ kind: "approval",
360
+ state: "active",
361
+ isInitial: true,
362
+ payload: {},
363
+ });
364
+ await expect(
365
+ extensions.insert({
366
+ scopeId: alphaScope,
367
+ guid: missingParentOwnerGuid,
368
+ parentGuid: "10000000-0000-4000-8000-000000000099",
369
+ key: "missing-parent",
370
+ kind: "missing-parent",
371
+ state: "active",
372
+ isInitial: false,
373
+ payload: {},
374
+ }),
375
+ ).rejects.toThrow();
376
+ await expect(
377
+ extensions.insert({
378
+ scopeId: alphaScope,
379
+ guid: crossScopeOwnerGuid,
380
+ parentGuid: crossScopeParentGuid,
381
+ key: "cross-scope",
382
+ kind: "cross-scope-parent",
383
+ state: "active",
384
+ isInitial: false,
385
+ payload: {},
386
+ }),
387
+ ).rejects.toThrow();
388
+ await expect(
389
+ extensions.insert({
390
+ scopeId: alphaScope,
391
+ guid: duplicateOwnerGuid,
392
+ parentGuid,
393
+ key: "duplicate-partial",
394
+ kind: "approval",
395
+ state: "active",
396
+ isInitial: true,
397
+ payload: {},
398
+ }),
399
+ ).rejects.toThrow();
400
+ await extensions.insert({
401
+ scopeId: alphaScope,
402
+ guid: excludedOwnerGuid,
403
+ parentGuid,
404
+ key: "inactive",
405
+ kind: "approval",
406
+ state: "active",
407
+ isInitial: false,
408
+ payload: {},
409
+ });
410
+ await expect(
411
+ owners.delete({ scopeId: alphaScope, guid: parentGuid }),
412
+ ).rejects.toThrow();
413
+
414
+ const snapshot = captureOmniMigrationSnapshot(
415
+ "projection-status-constraint-probe-sqlite-v1",
416
+ dataSource,
417
+ [{ entities: [entity], definition }],
418
+ );
419
+ const capturedTable = snapshot.tables.find(
420
+ (table) => table.name === tableName,
421
+ )!;
422
+ expect(capturedTable.foreignKeys).toEqual(
423
+ expect.arrayContaining([
424
+ expect.objectContaining({
425
+ name: referenceName,
426
+ columnNames: ["scopeId", "parentGuid"],
427
+ referencedTableName: "omni-record",
428
+ referencedColumnNames: ["scopeId", "guid"],
429
+ onDelete: "RESTRICT",
430
+ }),
431
+ ]),
432
+ );
433
+ expect(capturedTable.indices).toEqual(
434
+ expect.arrayContaining([
435
+ expect.objectContaining({
436
+ name: `${referenceName}_idx`,
437
+ columnNames: ["scopeId", "parentGuid"],
438
+ isUnique: false,
439
+ }),
440
+ expect.objectContaining({
441
+ name: constraintName,
442
+ columnNames: ["scopeId", "kind"],
443
+ isUnique: true,
444
+ where: '"isInitial" = 1 AND "state" = \'active\'',
445
+ }),
446
+ ]),
447
+ );
448
+ });
449
+ });
450
+
451
+ const postgresUrl = process.env.OMNI_EXTENSION_PROJECTION_POSTGRES_URL;
452
+ const postgresIt = postgresUrl ? it : it.skip;
453
+
454
+ describe("Omni extension projection constraints in PostgreSQL", () => {
455
+ postgresIt(
456
+ "initializes an ephemeral database with PostgreSQL metadata",
457
+ async () => {
458
+ const entity = createOmniExtensionProjectionEntity<ConstraintRow>(
459
+ definition,
460
+ createProjectionDialect("postgres"),
461
+ );
462
+ const dataSource = new DataSource({
463
+ type: "postgres",
464
+ url: postgresUrl!,
465
+ ssl: false,
466
+ dropSchema: true,
467
+ synchronize: true,
468
+ entities: [
469
+ OmniNamedEntity,
470
+ OmniRecordEntity,
471
+ OmniDocumentEntity,
472
+ OmniCollectionEntity,
473
+ OmniRelationEntity,
474
+ OmniExternalRefEntity,
475
+ entity,
476
+ ],
477
+ });
478
+ try {
479
+ await dataSource.initialize();
480
+ const metadata = dataSource.getMetadata(entity);
481
+ expect(metadata.indices).toEqual(
482
+ expect.arrayContaining([
483
+ expect.objectContaining({
484
+ givenName: constraintName,
485
+ where: '"isInitial" = TRUE AND "state" = \'active\'',
486
+ isUnique: true,
487
+ }),
488
+ ]),
489
+ );
490
+ const columns = await dataSource.query(
491
+ "SELECT column_name, data_type FROM information_schema.columns WHERE table_name = $1 AND column_name IN ($2, $3)",
492
+ [tableName, "payload", "isInitial"],
493
+ );
494
+ expect(columns).toEqual(
495
+ expect.arrayContaining([
496
+ expect.objectContaining({
497
+ column_name: "payload",
498
+ data_type: "jsonb",
499
+ }),
500
+ expect.objectContaining({
501
+ column_name: "isInitial",
502
+ data_type: "boolean",
503
+ }),
504
+ ]),
505
+ );
506
+ const indexes = await dataSource.query(
507
+ "SELECT indexdef FROM pg_indexes WHERE tablename = $1 AND indexname = $2",
508
+ [tableName, constraintName],
509
+ );
510
+ expect(indexes[0]?.indexdef).toContain("WHERE");
511
+ expect(indexes[0]?.indexdef).toContain('"isInitial"');
512
+ expect(indexes[0]?.indexdef).toContain("state");
513
+ } finally {
514
+ if (dataSource.isInitialized) await dataSource.destroy();
515
+ }
516
+ },
517
+ );
518
+ });