@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,354 +1,354 @@
1
- import {
2
- afterEach,
3
- beforeEach,
4
- describe,
5
- expect,
6
- it,
7
- jest,
8
- } from "@jest/globals";
9
- import { DataSource } from "typeorm";
10
-
11
- import { OmniNamedEntity } from '../base/omni-named.entity.js';
12
- import { OmniRecordEntity } from '../base/omni-record.entity.js';
13
- import { OmniRelationEntity } from '../base/omni-relation.entity.js';
14
- import { OmniExternalRefEntity } from '../base/omni-external-ref.entity.js';
15
- import { OmniRecordStatus } from '../omni-record-status.enum.js';
16
- import { OmniRelationStatus } from '../omni-relation-status.enum.js';
17
- import { OmniExternalRefInternalType } from '../omni-external-ref-internal-type.enum.js';
18
- import { createOmniRelationKindContract } from '../omni-relation-kind.contract.js';
19
- import { OmniRelationService } from '../omni-relation.service.js';
20
- import { OmniExternalRefService } from '../omni-external-ref.service.js';
21
- import { OmniExternalRefBindingValidator } from '../omni-external-ref-binding.validator.js';
22
- import { OmniScopedService } from '../omni-scoped.service.js';
23
-
24
- const alphaScope = {
25
- scopeId: "scope-alpha",
26
- cacheKey: (key: string) => `scope-alpha:${key}`,
27
- };
28
- const bravoScope = {
29
- scopeId: "scope-bravo",
30
- cacheKey: (key: string) => `scope-bravo:${key}`,
31
- };
32
-
33
- describe("OmniScopedService", () => {
34
- let dataSource: DataSource;
35
-
36
- beforeEach(async () => {
37
- dataSource = new DataSource({
38
- type: "sqlite",
39
- database: ":memory:",
40
- dropSchema: true,
41
- synchronize: true,
42
- entities: [
43
- OmniNamedEntity,
44
- OmniRecordEntity,
45
- OmniRelationEntity,
46
- OmniExternalRefEntity,
47
- ],
48
- });
49
- await dataSource.initialize();
50
- });
51
-
52
- afterEach(async () => {
53
- await dataSource.destroy();
54
- });
55
-
56
- it("enforces scope, payload metadata, scoped reads, and tombstones", async () => {
57
- const repository = dataSource.getRepository(OmniRecordEntity);
58
- const alpha = new OmniScopedService(
59
- repository as never,
60
- alphaScope,
61
- "tombstone",
62
- );
63
- const bravo = new OmniScopedService(
64
- repository as never,
65
- bravoScope,
66
- "tombstone",
67
- );
68
- const sharedGuid = "a0000000-0000-4000-8000-000000000001";
69
-
70
- await expect(
71
- alpha.createEntity({
72
- guid: sharedGuid,
73
- title: "attempted scope override",
74
- kind: "generic",
75
- status: OmniRecordStatus.Active,
76
- scopeId: "scope-bravo",
77
- } as never),
78
- ).rejects.toThrow("server-owned");
79
- await expect(
80
- alpha.createEntity({
81
- guid: sharedGuid,
82
- title: "invalid payload",
83
- kind: "generic",
84
- status: OmniRecordStatus.Active,
85
- payload: ["not-an-object"],
86
- }),
87
- ).rejects.toThrow("payload must be");
88
- await expect(
89
- alpha.createEntity({
90
- guid: sharedGuid,
91
- title: "partial schema",
92
- kind: "generic",
93
- status: OmniRecordStatus.Active,
94
- payloadSchemaId: "example.record",
95
- }),
96
- ).rejects.toThrow("supplied together");
97
-
98
- await alpha.createEntity({
99
- guid: sharedGuid,
100
- title: "alpha record",
101
- kind: "generic",
102
- status: OmniRecordStatus.Active,
103
- payload: { nested: true },
104
- payloadSchemaId: "example.record",
105
- payloadSchemaVersion: 1,
106
- });
107
- await bravo.createEntity({
108
- guid: sharedGuid,
109
- title: "bravo record",
110
- kind: "generic",
111
- status: OmniRecordStatus.Active,
112
- });
113
-
114
- expect((await alpha.getEntity({ guid: sharedGuid }))?.title).toBe(
115
- "alpha record",
116
- );
117
- expect((await bravo.getEntity({ guid: sharedGuid }))?.title).toBe(
118
- "bravo record",
119
- );
120
- expect(await alpha.getEntityListExtended({})).toHaveLength(1);
121
-
122
- await expect(
123
- alpha.updateEntity(
124
- { guid: sharedGuid },
125
- { guid: "a0000000-0000-4000-8000-000000000099" },
126
- ),
127
- ).rejects.toThrow("guid is immutable");
128
- await alpha.updateEntity(
129
- { guid: sharedGuid },
130
- { title: "alpha record updated" },
131
- );
132
- await alpha.deleteEntity({ guid: sharedGuid });
133
-
134
- await expect(
135
- alpha.getEntity({ guid: sharedGuid }, undefined, undefined, undefined, {
136
- failOnNull: true,
137
- }),
138
- ).rejects.toThrow("not found");
139
- expect((await bravo.getEntity({ guid: sharedGuid }))?.title).toBe(
140
- "bravo record",
141
- );
142
- await expect(alpha.getEntity("guid = ?" as never)).rejects.toThrow(
143
- "String where clauses",
144
- );
145
- });
146
-
147
- it("applies scope recursively to extended subquery filters", async () => {
148
- const getManyExtended = jest.fn(async (...args: any[]) => []);
149
- const service = new OmniScopedService(
150
- {
151
- getCrudGenCapabilities: () => ({
152
- extendedQueries: true,
153
- structuredGraphqlFilters: true,
154
- }),
155
- getManyExtended,
156
- getManyAndCountExtended: jest.fn(async (...args: any[]) => [[], 0]),
157
- } as never,
158
- alphaScope,
159
- "tombstone",
160
- );
161
-
162
- await service.getEntityListExtended({
163
- where: { filters: { title: "outer" as never } },
164
- subQueryFilters: {
165
- where: { filters: { title: "inner" as never } },
166
- take: 1,
167
- },
168
- });
169
-
170
- expect(getManyExtended).toHaveBeenCalledWith(
171
- expect.objectContaining({
172
- where: expect.objectContaining({
173
- filters: expect.objectContaining({ scopeId: "scope-alpha" }),
174
- }),
175
- subQueryFilters: expect.objectContaining({
176
- where: expect.objectContaining({
177
- filters: expect.objectContaining({ scopeId: "scope-alpha" }),
178
- }),
179
- }),
180
- }),
181
- );
182
- await expect(
183
- service.getEntityListExtended({
184
- subQueryFilters: {
185
- where: { filters: { scopeId: "scope-bravo" as never } },
186
- },
187
- }),
188
- ).rejects.toThrow("server context");
189
- });
190
-
191
- it("validates scoped relation endpoints and the extensible kind contract", async () => {
192
- const recordRepository = dataSource.getRepository(OmniRecordEntity);
193
- const relationRepository = dataSource.getRepository(OmniRelationEntity);
194
- const records = new OmniScopedService(
195
- recordRepository as never,
196
- alphaScope,
197
- "tombstone",
198
- );
199
- const bravoRecords = new OmniScopedService(
200
- recordRepository as never,
201
- bravoScope,
202
- "tombstone",
203
- );
204
- const sourceId = "a0000000-0000-4000-8000-000000000010";
205
- const targetId = "a0000000-0000-4000-8000-000000000011";
206
- const bravoOnlyId = "a0000000-0000-4000-8000-000000000012";
207
-
208
- await records.createEntity({
209
- guid: sourceId,
210
- title: "source",
211
- kind: "generic",
212
- status: OmniRecordStatus.Active,
213
- });
214
- await records.createEntity({
215
- guid: targetId,
216
- title: "target",
217
- kind: "generic",
218
- status: OmniRecordStatus.Active,
219
- });
220
- await bravoRecords.createEntity({
221
- guid: bravoOnlyId,
222
- title: "bravo target",
223
- kind: "generic",
224
- status: OmniRecordStatus.Active,
225
- });
226
-
227
- const relations = new OmniRelationService(
228
- relationRepository as never,
229
- alphaScope,
230
- "hard",
231
- recordRepository,
232
- createOmniRelationKindContract(["blocks"]),
233
- );
234
- const relationId = "a0000000-0000-4000-8000-000000000013";
235
-
236
- await relations.createEntity({
237
- guid: relationId,
238
- sourceRecordId: sourceId,
239
- targetRecordId: targetId,
240
- kind: "blocks",
241
- status: OmniRelationStatus.Active,
242
- });
243
- await expect(
244
- relations.createEntity({
245
- guid: "a0000000-0000-4000-8000-000000000014",
246
- sourceRecordId: sourceId,
247
- targetRecordId: bravoOnlyId,
248
- kind: "blocks",
249
- status: OmniRelationStatus.Active,
250
- }),
251
- ).rejects.toThrow("not found");
252
- await expect(
253
- relations.createEntity({
254
- guid: "a0000000-0000-4000-8000-000000000015",
255
- sourceRecordId: sourceId,
256
- targetRecordId: targetId,
257
- kind: "unregistered",
258
- status: OmniRelationStatus.Active,
259
- }),
260
- ).rejects.toThrow("not registered");
261
- await expect(
262
- relations.createEntity({
263
- guid: "a0000000-0000-4000-8000-000000000016",
264
- sourceRecordId: sourceId,
265
- targetRecordId: targetId,
266
- kind: "contains",
267
- status: OmniRelationStatus.Active,
268
- }),
269
- ).rejects.toThrow("not valid for these endpoint kinds");
270
- await expect(
271
- relations.updateEntity(
272
- { guid: relationId },
273
- { sourceRecordId: targetId },
274
- ),
275
- ).rejects.toThrow("immutable");
276
-
277
- await relations.updateEntity(
278
- { guid: relationId },
279
- { status: OmniRelationStatus.Inactive },
280
- );
281
- expect((await relations.getEntity({ guid: relationId }))?.status).toBe(
282
- OmniRelationStatus.Inactive,
283
- );
284
- });
285
-
286
- it("keeps external identities unique within scope while normalizing null partitions", async () => {
287
- const repository = dataSource.getRepository(OmniExternalRefEntity);
288
- const records = dataSource.getRepository(OmniRecordEntity);
289
- const internalId = "a0000000-0000-4000-8000-000000000019";
290
- await records.save([
291
- {
292
- scopeId: alphaScope.scopeId,
293
- guid: internalId,
294
- title: "alpha external target",
295
- kind: "generic",
296
- status: OmniRecordStatus.Active,
297
- },
298
- {
299
- scopeId: bravoScope.scopeId,
300
- guid: internalId,
301
- title: "bravo external target",
302
- kind: "generic",
303
- status: OmniRecordStatus.Active,
304
- },
305
- ]);
306
- const alpha = new OmniExternalRefService(
307
- repository as never,
308
- alphaScope,
309
- "hard",
310
- new OmniExternalRefBindingValidator(records, alphaScope),
311
- );
312
- const bravo = new OmniExternalRefService(
313
- repository as never,
314
- bravoScope,
315
- "hard",
316
- new OmniExternalRefBindingValidator(records, bravoScope),
317
- );
318
- const input = {
319
- guid: "a0000000-0000-4000-8000-000000000020",
320
- provider: "github",
321
- externalId: "42",
322
- internalType: OmniExternalRefInternalType.Record,
323
- internalId,
324
- account: null,
325
- container: null,
326
- };
327
-
328
- await alpha.createEntity(input);
329
- expect(
330
- await alpha.findByExternalIdentity({
331
- provider: "github",
332
- externalId: "42",
333
- }),
334
- ).toEqual(
335
- expect.objectContaining({ guid: input.guid, account: "", container: "" }),
336
- );
337
- await expect(
338
- alpha.createEntity({
339
- ...input,
340
- guid: "a0000000-0000-4000-8000-000000000021",
341
- }),
342
- ).rejects.toThrow("already exists");
343
- await bravo.createEntity({
344
- ...input,
345
- guid: "a0000000-0000-4000-8000-000000000022",
346
- });
347
- expect(
348
- await alpha.findForInternalRecord(
349
- OmniExternalRefInternalType.Record,
350
- internalId,
351
- ),
352
- ).toHaveLength(1);
353
- });
354
- });
1
+ import {
2
+ afterEach,
3
+ beforeEach,
4
+ describe,
5
+ expect,
6
+ it,
7
+ jest,
8
+ } from "@jest/globals";
9
+ import { DataSource } from "typeorm";
10
+
11
+ import { OmniNamedEntity } from '../base/omni-named.entity.js';
12
+ import { OmniRecordEntity } from '../base/omni-record.entity.js';
13
+ import { OmniRelationEntity } from '../base/omni-relation.entity.js';
14
+ import { OmniExternalRefEntity } from '../base/omni-external-ref.entity.js';
15
+ import { OmniRecordStatus } from '../omni-record-status.enum.js';
16
+ import { OmniRelationStatus } from '../omni-relation-status.enum.js';
17
+ import { OmniExternalRefInternalType } from '../omni-external-ref-internal-type.enum.js';
18
+ import { createOmniRelationKindContract } from '../omni-relation-kind.contract.js';
19
+ import { OmniRelationService } from '../omni-relation.service.js';
20
+ import { OmniExternalRefService } from '../omni-external-ref.service.js';
21
+ import { OmniExternalRefBindingValidator } from '../omni-external-ref-binding.validator.js';
22
+ import { OmniScopedService } from '../omni-scoped.service.js';
23
+
24
+ const alphaScope = {
25
+ scopeId: "scope-alpha",
26
+ cacheKey: (key: string) => `scope-alpha:${key}`,
27
+ };
28
+ const bravoScope = {
29
+ scopeId: "scope-bravo",
30
+ cacheKey: (key: string) => `scope-bravo:${key}`,
31
+ };
32
+
33
+ describe("OmniScopedService", () => {
34
+ let dataSource: DataSource;
35
+
36
+ beforeEach(async () => {
37
+ dataSource = new DataSource({
38
+ type: "sqlite",
39
+ database: ":memory:",
40
+ dropSchema: true,
41
+ synchronize: true,
42
+ entities: [
43
+ OmniNamedEntity,
44
+ OmniRecordEntity,
45
+ OmniRelationEntity,
46
+ OmniExternalRefEntity,
47
+ ],
48
+ });
49
+ await dataSource.initialize();
50
+ });
51
+
52
+ afterEach(async () => {
53
+ await dataSource.destroy();
54
+ });
55
+
56
+ it("enforces scope, payload metadata, scoped reads, and tombstones", async () => {
57
+ const repository = dataSource.getRepository(OmniRecordEntity);
58
+ const alpha = new OmniScopedService(
59
+ repository as never,
60
+ alphaScope,
61
+ "tombstone",
62
+ );
63
+ const bravo = new OmniScopedService(
64
+ repository as never,
65
+ bravoScope,
66
+ "tombstone",
67
+ );
68
+ const sharedGuid = "a0000000-0000-4000-8000-000000000001";
69
+
70
+ await expect(
71
+ alpha.createEntity({
72
+ guid: sharedGuid,
73
+ title: "attempted scope override",
74
+ kind: "generic",
75
+ status: OmniRecordStatus.Active,
76
+ scopeId: "scope-bravo",
77
+ } as never),
78
+ ).rejects.toThrow("server-owned");
79
+ await expect(
80
+ alpha.createEntity({
81
+ guid: sharedGuid,
82
+ title: "invalid payload",
83
+ kind: "generic",
84
+ status: OmniRecordStatus.Active,
85
+ payload: ["not-an-object"],
86
+ }),
87
+ ).rejects.toThrow("payload must be");
88
+ await expect(
89
+ alpha.createEntity({
90
+ guid: sharedGuid,
91
+ title: "partial schema",
92
+ kind: "generic",
93
+ status: OmniRecordStatus.Active,
94
+ payloadSchemaId: "example.record",
95
+ }),
96
+ ).rejects.toThrow("supplied together");
97
+
98
+ await alpha.createEntity({
99
+ guid: sharedGuid,
100
+ title: "alpha record",
101
+ kind: "generic",
102
+ status: OmniRecordStatus.Active,
103
+ payload: { nested: true },
104
+ payloadSchemaId: "example.record",
105
+ payloadSchemaVersion: 1,
106
+ });
107
+ await bravo.createEntity({
108
+ guid: sharedGuid,
109
+ title: "bravo record",
110
+ kind: "generic",
111
+ status: OmniRecordStatus.Active,
112
+ });
113
+
114
+ expect((await alpha.getEntity({ guid: sharedGuid }))?.title).toBe(
115
+ "alpha record",
116
+ );
117
+ expect((await bravo.getEntity({ guid: sharedGuid }))?.title).toBe(
118
+ "bravo record",
119
+ );
120
+ expect(await alpha.getEntityListExtended({})).toHaveLength(1);
121
+
122
+ await expect(
123
+ alpha.updateEntity(
124
+ { guid: sharedGuid },
125
+ { guid: "a0000000-0000-4000-8000-000000000099" },
126
+ ),
127
+ ).rejects.toThrow("guid is immutable");
128
+ await alpha.updateEntity(
129
+ { guid: sharedGuid },
130
+ { title: "alpha record updated" },
131
+ );
132
+ await alpha.deleteEntity({ guid: sharedGuid });
133
+
134
+ await expect(
135
+ alpha.getEntity({ guid: sharedGuid }, undefined, undefined, undefined, {
136
+ failOnNull: true,
137
+ }),
138
+ ).rejects.toThrow("not found");
139
+ expect((await bravo.getEntity({ guid: sharedGuid }))?.title).toBe(
140
+ "bravo record",
141
+ );
142
+ await expect(alpha.getEntity("guid = ?" as never)).rejects.toThrow(
143
+ "String where clauses",
144
+ );
145
+ });
146
+
147
+ it("applies scope recursively to extended subquery filters", async () => {
148
+ const getManyExtended = jest.fn(async (...args: any[]) => []);
149
+ const service = new OmniScopedService(
150
+ {
151
+ getCrudGenCapabilities: () => ({
152
+ extendedQueries: true,
153
+ structuredGraphqlFilters: true,
154
+ }),
155
+ getManyExtended,
156
+ getManyAndCountExtended: jest.fn(async (...args: any[]) => [[], 0]),
157
+ } as never,
158
+ alphaScope,
159
+ "tombstone",
160
+ );
161
+
162
+ await service.getEntityListExtended({
163
+ where: { filters: { title: "outer" as never } },
164
+ subQueryFilters: {
165
+ where: { filters: { title: "inner" as never } },
166
+ take: 1,
167
+ },
168
+ });
169
+
170
+ expect(getManyExtended).toHaveBeenCalledWith(
171
+ expect.objectContaining({
172
+ where: expect.objectContaining({
173
+ filters: expect.objectContaining({ scopeId: "scope-alpha" }),
174
+ }),
175
+ subQueryFilters: expect.objectContaining({
176
+ where: expect.objectContaining({
177
+ filters: expect.objectContaining({ scopeId: "scope-alpha" }),
178
+ }),
179
+ }),
180
+ }),
181
+ );
182
+ await expect(
183
+ service.getEntityListExtended({
184
+ subQueryFilters: {
185
+ where: { filters: { scopeId: "scope-bravo" as never } },
186
+ },
187
+ }),
188
+ ).rejects.toThrow("server context");
189
+ });
190
+
191
+ it("validates scoped relation endpoints and the extensible kind contract", async () => {
192
+ const recordRepository = dataSource.getRepository(OmniRecordEntity);
193
+ const relationRepository = dataSource.getRepository(OmniRelationEntity);
194
+ const records = new OmniScopedService(
195
+ recordRepository as never,
196
+ alphaScope,
197
+ "tombstone",
198
+ );
199
+ const bravoRecords = new OmniScopedService(
200
+ recordRepository as never,
201
+ bravoScope,
202
+ "tombstone",
203
+ );
204
+ const sourceId = "a0000000-0000-4000-8000-000000000010";
205
+ const targetId = "a0000000-0000-4000-8000-000000000011";
206
+ const bravoOnlyId = "a0000000-0000-4000-8000-000000000012";
207
+
208
+ await records.createEntity({
209
+ guid: sourceId,
210
+ title: "source",
211
+ kind: "generic",
212
+ status: OmniRecordStatus.Active,
213
+ });
214
+ await records.createEntity({
215
+ guid: targetId,
216
+ title: "target",
217
+ kind: "generic",
218
+ status: OmniRecordStatus.Active,
219
+ });
220
+ await bravoRecords.createEntity({
221
+ guid: bravoOnlyId,
222
+ title: "bravo target",
223
+ kind: "generic",
224
+ status: OmniRecordStatus.Active,
225
+ });
226
+
227
+ const relations = new OmniRelationService(
228
+ relationRepository as never,
229
+ alphaScope,
230
+ "hard",
231
+ recordRepository,
232
+ createOmniRelationKindContract(["blocks"]),
233
+ );
234
+ const relationId = "a0000000-0000-4000-8000-000000000013";
235
+
236
+ await relations.createEntity({
237
+ guid: relationId,
238
+ sourceRecordId: sourceId,
239
+ targetRecordId: targetId,
240
+ kind: "blocks",
241
+ status: OmniRelationStatus.Active,
242
+ });
243
+ await expect(
244
+ relations.createEntity({
245
+ guid: "a0000000-0000-4000-8000-000000000014",
246
+ sourceRecordId: sourceId,
247
+ targetRecordId: bravoOnlyId,
248
+ kind: "blocks",
249
+ status: OmniRelationStatus.Active,
250
+ }),
251
+ ).rejects.toThrow("not found");
252
+ await expect(
253
+ relations.createEntity({
254
+ guid: "a0000000-0000-4000-8000-000000000015",
255
+ sourceRecordId: sourceId,
256
+ targetRecordId: targetId,
257
+ kind: "unregistered",
258
+ status: OmniRelationStatus.Active,
259
+ }),
260
+ ).rejects.toThrow("not registered");
261
+ await expect(
262
+ relations.createEntity({
263
+ guid: "a0000000-0000-4000-8000-000000000016",
264
+ sourceRecordId: sourceId,
265
+ targetRecordId: targetId,
266
+ kind: "contains",
267
+ status: OmniRelationStatus.Active,
268
+ }),
269
+ ).rejects.toThrow("not valid for these endpoint kinds");
270
+ await expect(
271
+ relations.updateEntity(
272
+ { guid: relationId },
273
+ { sourceRecordId: targetId },
274
+ ),
275
+ ).rejects.toThrow("immutable");
276
+
277
+ await relations.updateEntity(
278
+ { guid: relationId },
279
+ { status: OmniRelationStatus.Inactive },
280
+ );
281
+ expect((await relations.getEntity({ guid: relationId }))?.status).toBe(
282
+ OmniRelationStatus.Inactive,
283
+ );
284
+ });
285
+
286
+ it("keeps external identities unique within scope while normalizing null partitions", async () => {
287
+ const repository = dataSource.getRepository(OmniExternalRefEntity);
288
+ const records = dataSource.getRepository(OmniRecordEntity);
289
+ const internalId = "a0000000-0000-4000-8000-000000000019";
290
+ await records.save([
291
+ {
292
+ scopeId: alphaScope.scopeId,
293
+ guid: internalId,
294
+ title: "alpha external target",
295
+ kind: "generic",
296
+ status: OmniRecordStatus.Active,
297
+ },
298
+ {
299
+ scopeId: bravoScope.scopeId,
300
+ guid: internalId,
301
+ title: "bravo external target",
302
+ kind: "generic",
303
+ status: OmniRecordStatus.Active,
304
+ },
305
+ ]);
306
+ const alpha = new OmniExternalRefService(
307
+ repository as never,
308
+ alphaScope,
309
+ "hard",
310
+ new OmniExternalRefBindingValidator(records, alphaScope),
311
+ );
312
+ const bravo = new OmniExternalRefService(
313
+ repository as never,
314
+ bravoScope,
315
+ "hard",
316
+ new OmniExternalRefBindingValidator(records, bravoScope),
317
+ );
318
+ const input = {
319
+ guid: "a0000000-0000-4000-8000-000000000020",
320
+ provider: "github",
321
+ externalId: "42",
322
+ internalType: OmniExternalRefInternalType.Record,
323
+ internalId,
324
+ account: null,
325
+ container: null,
326
+ };
327
+
328
+ await alpha.createEntity(input);
329
+ expect(
330
+ await alpha.findByExternalIdentity({
331
+ provider: "github",
332
+ externalId: "42",
333
+ }),
334
+ ).toEqual(
335
+ expect.objectContaining({ guid: input.guid, account: "", container: "" }),
336
+ );
337
+ await expect(
338
+ alpha.createEntity({
339
+ ...input,
340
+ guid: "a0000000-0000-4000-8000-000000000021",
341
+ }),
342
+ ).rejects.toThrow("already exists");
343
+ await bravo.createEntity({
344
+ ...input,
345
+ guid: "a0000000-0000-4000-8000-000000000022",
346
+ });
347
+ expect(
348
+ await alpha.findForInternalRecord(
349
+ OmniExternalRefInternalType.Record,
350
+ internalId,
351
+ ),
352
+ ).toHaveLength(1);
353
+ });
354
+ });