@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,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
|
+
});
|