@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,159 +1,159 @@
|
|
|
1
|
-
import { BadRequestException } from '@nestjs/common';
|
|
2
|
-
import type { CrudGenFindManyOptions } from '@nest-yalc-2/crud-gen/api-graphql/crud-gen-gql.interface.js';
|
|
3
|
-
import {
|
|
4
|
-
IsNull,
|
|
5
|
-
type DeepPartial,
|
|
6
|
-
type FindOptionsWhere,
|
|
7
|
-
type Repository,
|
|
8
|
-
} from 'typeorm';
|
|
9
|
-
import { OmniRecordEntity } from './base/omni-record.entity.js';
|
|
10
|
-
import { OmniRelationEntity } from './base/omni-relation.entity.js';
|
|
11
|
-
import {
|
|
12
|
-
canonicalOmniRelationKinds,
|
|
13
|
-
type OmniRelationKindContract,
|
|
14
|
-
} from './omni-relation-kind.contract.js';
|
|
15
|
-
import { isAllowedOmniRelation } from './omni-relation-semantics.js';
|
|
16
|
-
import type { OmniDeletePolicy, OmniScope } from './omni-scope.js';
|
|
17
|
-
import { OmniScopedService } from './omni-scoped.service.js';
|
|
18
|
-
|
|
19
|
-
/**
|
|
20
|
-
* Scoped graph CRUD with immutable endpoints. The database composite foreign
|
|
21
|
-
* keys prevent cross-scope edges; this service additionally turns missing or
|
|
22
|
-
* invalid endpoints into normal API failures before persistence.
|
|
23
|
-
*/
|
|
24
|
-
export class OmniRelationService extends OmniScopedService<OmniRelationEntity> {
|
|
25
|
-
constructor(
|
|
26
|
-
repository: any,
|
|
27
|
-
scope: OmniScope,
|
|
28
|
-
deletion: OmniDeletePolicy,
|
|
29
|
-
protected readonly recordRepository: Repository<OmniRecordEntity>,
|
|
30
|
-
protected readonly kinds: OmniRelationKindContract,
|
|
31
|
-
) {
|
|
32
|
-
super(repository, scope, deletion);
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
override async createEntity(
|
|
36
|
-
input: DeepPartial<OmniRelationEntity>,
|
|
37
|
-
findOptions?: CrudGenFindManyOptions<OmniRelationEntity>,
|
|
38
|
-
returnEntity?: true,
|
|
39
|
-
): Promise<OmniRelationEntity>;
|
|
40
|
-
override async createEntity(
|
|
41
|
-
input: DeepPartial<OmniRelationEntity>,
|
|
42
|
-
findOptions?: CrudGenFindManyOptions<OmniRelationEntity>,
|
|
43
|
-
returnEntity?: boolean,
|
|
44
|
-
): Promise<OmniRelationEntity | boolean>;
|
|
45
|
-
override async createEntity(
|
|
46
|
-
input: DeepPartial<OmniRelationEntity>,
|
|
47
|
-
findOptions?: CrudGenFindManyOptions<OmniRelationEntity>,
|
|
48
|
-
returnEntity = true,
|
|
49
|
-
): Promise<OmniRelationEntity | boolean> {
|
|
50
|
-
this.rejectServerFields(input as object);
|
|
51
|
-
await this.assertRelation(input);
|
|
52
|
-
return super.createEntity(input, findOptions, returnEntity);
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
override async updateEntity(
|
|
56
|
-
conditions: FindOptionsWhere<OmniRelationEntity>,
|
|
57
|
-
input: DeepPartial<OmniRelationEntity>,
|
|
58
|
-
findOptions?: CrudGenFindManyOptions<OmniRelationEntity>,
|
|
59
|
-
returnEntity?: true,
|
|
60
|
-
): Promise<OmniRelationEntity>;
|
|
61
|
-
override async updateEntity(
|
|
62
|
-
conditions: FindOptionsWhere<OmniRelationEntity>,
|
|
63
|
-
input: DeepPartial<OmniRelationEntity>,
|
|
64
|
-
findOptions?: CrudGenFindManyOptions<OmniRelationEntity>,
|
|
65
|
-
returnEntity?: boolean,
|
|
66
|
-
): Promise<OmniRelationEntity | boolean>;
|
|
67
|
-
override async updateEntity(
|
|
68
|
-
conditions: FindOptionsWhere<OmniRelationEntity>,
|
|
69
|
-
input: DeepPartial<OmniRelationEntity>,
|
|
70
|
-
findOptions?: CrudGenFindManyOptions<OmniRelationEntity>,
|
|
71
|
-
returnEntity = true,
|
|
72
|
-
): Promise<OmniRelationEntity | boolean> {
|
|
73
|
-
for (const field of ['guid', 'sourceRecordId', 'targetRecordId']) {
|
|
74
|
-
if (Object.prototype.hasOwnProperty.call(input, field)) {
|
|
75
|
-
throw new BadRequestException(`Omni relation ${field} is immutable.`);
|
|
76
|
-
}
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
const current = await super.getEntity(
|
|
80
|
-
conditions,
|
|
81
|
-
undefined,
|
|
82
|
-
undefined,
|
|
83
|
-
undefined,
|
|
84
|
-
{
|
|
85
|
-
failOnNull: true,
|
|
86
|
-
},
|
|
87
|
-
);
|
|
88
|
-
if (!current) this.notFound();
|
|
89
|
-
await this.assertRelation({ ...current, ...input });
|
|
90
|
-
return super.updateEntity(conditions, input, findOptions, returnEntity);
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
protected async assertRelation(
|
|
94
|
-
input: DeepPartial<OmniRelationEntity>,
|
|
95
|
-
recordRepository: Repository<OmniRecordEntity> = this.recordRepository,
|
|
96
|
-
): Promise<void> {
|
|
97
|
-
const sourceRecordId = this.requiredIdentifier(
|
|
98
|
-
input.sourceRecordId,
|
|
99
|
-
'sourceRecordId',
|
|
100
|
-
);
|
|
101
|
-
const targetRecordId = this.requiredIdentifier(
|
|
102
|
-
input.targetRecordId,
|
|
103
|
-
'targetRecordId',
|
|
104
|
-
);
|
|
105
|
-
const kind = this.requiredIdentifier(input.kind, 'kind');
|
|
106
|
-
try {
|
|
107
|
-
this.kinds.assert(kind);
|
|
108
|
-
} catch (error) {
|
|
109
|
-
throw new BadRequestException(
|
|
110
|
-
error instanceof Error
|
|
111
|
-
? error.message
|
|
112
|
-
: 'Omni relation kind is invalid.',
|
|
113
|
-
);
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
const records = await recordRepository.find({
|
|
117
|
-
where: [
|
|
118
|
-
{ scopeId: this.scopeId, guid: sourceRecordId, deletedAt: IsNull() },
|
|
119
|
-
{ scopeId: this.scopeId, guid: targetRecordId, deletedAt: IsNull() },
|
|
120
|
-
],
|
|
121
|
-
});
|
|
122
|
-
const source = records.find((record) => record.guid === sourceRecordId);
|
|
123
|
-
const target = records.find((record) => record.guid === targetRecordId);
|
|
124
|
-
if (!source || !target) this.notFound();
|
|
125
|
-
|
|
126
|
-
this.assertEndpointKinds(source, target);
|
|
127
|
-
|
|
128
|
-
const isCanonical = canonicalOmniRelationKinds.includes(
|
|
129
|
-
kind as (typeof canonicalOmniRelationKinds)[number],
|
|
130
|
-
);
|
|
131
|
-
if (
|
|
132
|
-
isCanonical &&
|
|
133
|
-
!isAllowedOmniRelation({
|
|
134
|
-
sourceKind: source.kind,
|
|
135
|
-
targetKind: target.kind,
|
|
136
|
-
relationKind: kind,
|
|
137
|
-
})
|
|
138
|
-
) {
|
|
139
|
-
throw new BadRequestException(
|
|
140
|
-
'Omni relation kind is not valid for these endpoint kinds.',
|
|
141
|
-
);
|
|
142
|
-
}
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
/** Resource-specific relation projections may narrow endpoint kinds. */
|
|
146
|
-
protected assertEndpointKinds(
|
|
147
|
-
_source: OmniRecordEntity,
|
|
148
|
-
_target: OmniRecordEntity,
|
|
149
|
-
): void {}
|
|
150
|
-
|
|
151
|
-
protected requiredIdentifier(value: unknown, field: string): string {
|
|
152
|
-
if (typeof value !== 'string' || value.trim().length === 0) {
|
|
153
|
-
throw new BadRequestException(
|
|
154
|
-
`Omni relation ${field} must be a non-empty string.`,
|
|
155
|
-
);
|
|
156
|
-
}
|
|
157
|
-
return value;
|
|
158
|
-
}
|
|
159
|
-
}
|
|
1
|
+
import { BadRequestException } from '@nestjs/common';
|
|
2
|
+
import type { CrudGenFindManyOptions } from '@nest-yalc-2/crud-gen/api-graphql/crud-gen-gql.interface.js';
|
|
3
|
+
import {
|
|
4
|
+
IsNull,
|
|
5
|
+
type DeepPartial,
|
|
6
|
+
type FindOptionsWhere,
|
|
7
|
+
type Repository,
|
|
8
|
+
} from 'typeorm';
|
|
9
|
+
import { OmniRecordEntity } from './base/omni-record.entity.js';
|
|
10
|
+
import { OmniRelationEntity } from './base/omni-relation.entity.js';
|
|
11
|
+
import {
|
|
12
|
+
canonicalOmniRelationKinds,
|
|
13
|
+
type OmniRelationKindContract,
|
|
14
|
+
} from './omni-relation-kind.contract.js';
|
|
15
|
+
import { isAllowedOmniRelation } from './omni-relation-semantics.js';
|
|
16
|
+
import type { OmniDeletePolicy, OmniScope } from './omni-scope.js';
|
|
17
|
+
import { OmniScopedService } from './omni-scoped.service.js';
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Scoped graph CRUD with immutable endpoints. The database composite foreign
|
|
21
|
+
* keys prevent cross-scope edges; this service additionally turns missing or
|
|
22
|
+
* invalid endpoints into normal API failures before persistence.
|
|
23
|
+
*/
|
|
24
|
+
export class OmniRelationService extends OmniScopedService<OmniRelationEntity> {
|
|
25
|
+
constructor(
|
|
26
|
+
repository: any,
|
|
27
|
+
scope: OmniScope,
|
|
28
|
+
deletion: OmniDeletePolicy,
|
|
29
|
+
protected readonly recordRepository: Repository<OmniRecordEntity>,
|
|
30
|
+
protected readonly kinds: OmniRelationKindContract,
|
|
31
|
+
) {
|
|
32
|
+
super(repository, scope, deletion);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
override async createEntity(
|
|
36
|
+
input: DeepPartial<OmniRelationEntity>,
|
|
37
|
+
findOptions?: CrudGenFindManyOptions<OmniRelationEntity>,
|
|
38
|
+
returnEntity?: true,
|
|
39
|
+
): Promise<OmniRelationEntity>;
|
|
40
|
+
override async createEntity(
|
|
41
|
+
input: DeepPartial<OmniRelationEntity>,
|
|
42
|
+
findOptions?: CrudGenFindManyOptions<OmniRelationEntity>,
|
|
43
|
+
returnEntity?: boolean,
|
|
44
|
+
): Promise<OmniRelationEntity | boolean>;
|
|
45
|
+
override async createEntity(
|
|
46
|
+
input: DeepPartial<OmniRelationEntity>,
|
|
47
|
+
findOptions?: CrudGenFindManyOptions<OmniRelationEntity>,
|
|
48
|
+
returnEntity = true,
|
|
49
|
+
): Promise<OmniRelationEntity | boolean> {
|
|
50
|
+
this.rejectServerFields(input as object);
|
|
51
|
+
await this.assertRelation(input);
|
|
52
|
+
return super.createEntity(input, findOptions, returnEntity);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
override async updateEntity(
|
|
56
|
+
conditions: FindOptionsWhere<OmniRelationEntity>,
|
|
57
|
+
input: DeepPartial<OmniRelationEntity>,
|
|
58
|
+
findOptions?: CrudGenFindManyOptions<OmniRelationEntity>,
|
|
59
|
+
returnEntity?: true,
|
|
60
|
+
): Promise<OmniRelationEntity>;
|
|
61
|
+
override async updateEntity(
|
|
62
|
+
conditions: FindOptionsWhere<OmniRelationEntity>,
|
|
63
|
+
input: DeepPartial<OmniRelationEntity>,
|
|
64
|
+
findOptions?: CrudGenFindManyOptions<OmniRelationEntity>,
|
|
65
|
+
returnEntity?: boolean,
|
|
66
|
+
): Promise<OmniRelationEntity | boolean>;
|
|
67
|
+
override async updateEntity(
|
|
68
|
+
conditions: FindOptionsWhere<OmniRelationEntity>,
|
|
69
|
+
input: DeepPartial<OmniRelationEntity>,
|
|
70
|
+
findOptions?: CrudGenFindManyOptions<OmniRelationEntity>,
|
|
71
|
+
returnEntity = true,
|
|
72
|
+
): Promise<OmniRelationEntity | boolean> {
|
|
73
|
+
for (const field of ['guid', 'sourceRecordId', 'targetRecordId']) {
|
|
74
|
+
if (Object.prototype.hasOwnProperty.call(input, field)) {
|
|
75
|
+
throw new BadRequestException(`Omni relation ${field} is immutable.`);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const current = await super.getEntity(
|
|
80
|
+
conditions,
|
|
81
|
+
undefined,
|
|
82
|
+
undefined,
|
|
83
|
+
undefined,
|
|
84
|
+
{
|
|
85
|
+
failOnNull: true,
|
|
86
|
+
},
|
|
87
|
+
);
|
|
88
|
+
if (!current) this.notFound();
|
|
89
|
+
await this.assertRelation({ ...current, ...input });
|
|
90
|
+
return super.updateEntity(conditions, input, findOptions, returnEntity);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
protected async assertRelation(
|
|
94
|
+
input: DeepPartial<OmniRelationEntity>,
|
|
95
|
+
recordRepository: Repository<OmniRecordEntity> = this.recordRepository,
|
|
96
|
+
): Promise<void> {
|
|
97
|
+
const sourceRecordId = this.requiredIdentifier(
|
|
98
|
+
input.sourceRecordId,
|
|
99
|
+
'sourceRecordId',
|
|
100
|
+
);
|
|
101
|
+
const targetRecordId = this.requiredIdentifier(
|
|
102
|
+
input.targetRecordId,
|
|
103
|
+
'targetRecordId',
|
|
104
|
+
);
|
|
105
|
+
const kind = this.requiredIdentifier(input.kind, 'kind');
|
|
106
|
+
try {
|
|
107
|
+
this.kinds.assert(kind);
|
|
108
|
+
} catch (error) {
|
|
109
|
+
throw new BadRequestException(
|
|
110
|
+
error instanceof Error
|
|
111
|
+
? error.message
|
|
112
|
+
: 'Omni relation kind is invalid.',
|
|
113
|
+
);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const records = await recordRepository.find({
|
|
117
|
+
where: [
|
|
118
|
+
{ scopeId: this.scopeId, guid: sourceRecordId, deletedAt: IsNull() },
|
|
119
|
+
{ scopeId: this.scopeId, guid: targetRecordId, deletedAt: IsNull() },
|
|
120
|
+
],
|
|
121
|
+
});
|
|
122
|
+
const source = records.find((record) => record.guid === sourceRecordId);
|
|
123
|
+
const target = records.find((record) => record.guid === targetRecordId);
|
|
124
|
+
if (!source || !target) this.notFound();
|
|
125
|
+
|
|
126
|
+
this.assertEndpointKinds(source, target);
|
|
127
|
+
|
|
128
|
+
const isCanonical = canonicalOmniRelationKinds.includes(
|
|
129
|
+
kind as (typeof canonicalOmniRelationKinds)[number],
|
|
130
|
+
);
|
|
131
|
+
if (
|
|
132
|
+
isCanonical &&
|
|
133
|
+
!isAllowedOmniRelation({
|
|
134
|
+
sourceKind: source.kind,
|
|
135
|
+
targetKind: target.kind,
|
|
136
|
+
relationKind: kind,
|
|
137
|
+
})
|
|
138
|
+
) {
|
|
139
|
+
throw new BadRequestException(
|
|
140
|
+
'Omni relation kind is not valid for these endpoint kinds.',
|
|
141
|
+
);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/** Resource-specific relation projections may narrow endpoint kinds. */
|
|
146
|
+
protected assertEndpointKinds(
|
|
147
|
+
_source: OmniRecordEntity,
|
|
148
|
+
_target: OmniRecordEntity,
|
|
149
|
+
): void {}
|
|
150
|
+
|
|
151
|
+
protected requiredIdentifier(value: unknown, field: string): string {
|
|
152
|
+
if (typeof value !== 'string' || value.trim().length === 0) {
|
|
153
|
+
throw new BadRequestException(
|
|
154
|
+
`Omni relation ${field} must be a non-empty string.`,
|
|
155
|
+
);
|
|
156
|
+
}
|
|
157
|
+
return value;
|
|
158
|
+
}
|
|
159
|
+
}
|
package/src/omni-scope.ts
CHANGED
|
@@ -1,161 +1,161 @@
|
|
|
1
|
-
import { Inject, Injectable, Scope } from '@nestjs/common';
|
|
2
|
-
import { REQUEST } from '@nestjs/core';
|
|
3
|
-
|
|
4
|
-
export interface OmniScope {
|
|
5
|
-
readonly scopeId: string;
|
|
6
|
-
cacheKey(key: string): string;
|
|
7
|
-
}
|
|
8
|
-
|
|
9
|
-
export type OmniScopeResolver = (request: unknown) => string | undefined;
|
|
10
|
-
|
|
11
|
-
export interface OmniKernelRegistrationOptions {
|
|
12
|
-
dbConnection: string;
|
|
13
|
-
/**
|
|
14
|
-
* Authentication adapters resolve a trusted server scope from the request.
|
|
15
|
-
* The adapter owns authentication; OmniKernel never reads a client DTO field.
|
|
16
|
-
*/
|
|
17
|
-
resolveScope?: OmniScopeResolver;
|
|
18
|
-
/**
|
|
19
|
-
* Compatibility/default partition for applications that do not yet have an
|
|
20
|
-
* authentication adapter. It is server configuration, never request input.
|
|
21
|
-
*/
|
|
22
|
-
defaultScopeId?: string;
|
|
23
|
-
/** Kinds exclusively owned by registered extension projections. */
|
|
24
|
-
reservedRecordKinds?: readonly string[];
|
|
25
|
-
relationKinds?: readonly string[];
|
|
26
|
-
deletion?: Partial<OmniDeletionPolicies>;
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
export type OmniDeletePolicy = 'hard' | 'tombstone';
|
|
30
|
-
|
|
31
|
-
export interface OmniDeletionPolicies {
|
|
32
|
-
named: OmniDeletePolicy;
|
|
33
|
-
record: OmniDeletePolicy;
|
|
34
|
-
document: OmniDeletePolicy;
|
|
35
|
-
collection: OmniDeletePolicy;
|
|
36
|
-
relation: OmniDeletePolicy;
|
|
37
|
-
externalRef: OmniDeletePolicy;
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
export const OMNI_KERNEL_OPTIONS = Symbol('OMNI_KERNEL_OPTIONS');
|
|
41
|
-
|
|
42
|
-
export const defaultOmniDeletionPolicies: OmniDeletionPolicies = {
|
|
43
|
-
named: 'hard',
|
|
44
|
-
record: 'tombstone',
|
|
45
|
-
document: 'tombstone',
|
|
46
|
-
collection: 'tombstone',
|
|
47
|
-
relation: 'hard',
|
|
48
|
-
externalRef: 'hard',
|
|
49
|
-
};
|
|
50
|
-
|
|
51
|
-
function normalizeOmniDeletionPolicies(
|
|
52
|
-
deletion: Partial<OmniDeletionPolicies> | undefined,
|
|
53
|
-
): OmniDeletionPolicies {
|
|
54
|
-
const knownResources = new Set(Object.keys(defaultOmniDeletionPolicies));
|
|
55
|
-
for (const [resource, policy] of Object.entries(deletion ?? {})) {
|
|
56
|
-
if (!knownResources.has(resource)) {
|
|
57
|
-
throw new TypeError(
|
|
58
|
-
`Unknown OmniKernel deletion policy resource: ${resource}.`,
|
|
59
|
-
);
|
|
60
|
-
}
|
|
61
|
-
if (policy !== 'hard' && policy !== 'tombstone') {
|
|
62
|
-
throw new TypeError(
|
|
63
|
-
`OmniKernel deletion policy for ${resource} must be hard or tombstone.`,
|
|
64
|
-
);
|
|
65
|
-
}
|
|
66
|
-
}
|
|
67
|
-
return {
|
|
68
|
-
...defaultOmniDeletionPolicies,
|
|
69
|
-
...deletion,
|
|
70
|
-
};
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
export function normalizeOmniKernelRegistrationOptions(
|
|
74
|
-
options: string | OmniKernelRegistrationOptions,
|
|
75
|
-
): Required<
|
|
76
|
-
Pick<
|
|
77
|
-
OmniKernelRegistrationOptions,
|
|
78
|
-
'dbConnection' | 'defaultScopeId' | 'relationKinds'
|
|
79
|
-
>
|
|
80
|
-
> &
|
|
81
|
-
Omit<
|
|
82
|
-
OmniKernelRegistrationOptions,
|
|
83
|
-
| 'dbConnection'
|
|
84
|
-
| 'defaultScopeId'
|
|
85
|
-
| 'relationKinds'
|
|
86
|
-
| 'reservedRecordKinds'
|
|
87
|
-
| 'deletion'
|
|
88
|
-
> & {
|
|
89
|
-
reservedRecordKinds: readonly string[];
|
|
90
|
-
deletion: OmniDeletionPolicies;
|
|
91
|
-
} {
|
|
92
|
-
const candidate =
|
|
93
|
-
typeof options === 'string' ? { dbConnection: options } : options;
|
|
94
|
-
if (!candidate.dbConnection) {
|
|
95
|
-
throw new TypeError(
|
|
96
|
-
'OmniKernelModule requires a database connection name.',
|
|
97
|
-
);
|
|
98
|
-
}
|
|
99
|
-
if (
|
|
100
|
-
candidate.defaultScopeId !== undefined &&
|
|
101
|
-
(candidate.defaultScopeId.trim().length === 0 ||
|
|
102
|
-
candidate.defaultScopeId.length > 64)
|
|
103
|
-
) {
|
|
104
|
-
throw new TypeError('OmniKernel defaultScopeId must be 1-64 characters.');
|
|
105
|
-
}
|
|
106
|
-
const reservedRecordKinds = candidate.reservedRecordKinds ?? [];
|
|
107
|
-
if (
|
|
108
|
-
!Array.isArray(reservedRecordKinds) ||
|
|
109
|
-
reservedRecordKinds.some(
|
|
110
|
-
(kind) =>
|
|
111
|
-
typeof kind !== 'string' ||
|
|
112
|
-
kind.trim().length === 0 ||
|
|
113
|
-
kind.length > 64,
|
|
114
|
-
) ||
|
|
115
|
-
new Set(reservedRecordKinds).size !== reservedRecordKinds.length
|
|
116
|
-
) {
|
|
117
|
-
throw new TypeError(
|
|
118
|
-
'OmniKernel reservedRecordKinds must be unique 1-64 character strings.',
|
|
119
|
-
);
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
return {
|
|
123
|
-
...candidate,
|
|
124
|
-
defaultScopeId: candidate.defaultScopeId ?? 'default',
|
|
125
|
-
relationKinds: candidate.relationKinds ?? [],
|
|
126
|
-
reservedRecordKinds: Object.freeze([...reservedRecordKinds]),
|
|
127
|
-
deletion: normalizeOmniDeletionPolicies(candidate.deletion),
|
|
128
|
-
};
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
type RequestEnvelope = { req?: unknown };
|
|
132
|
-
|
|
133
|
-
/**
|
|
134
|
-
* Request-scoped server context used by every Omni generated API and loader.
|
|
135
|
-
* It is intentionally unavailable when neither the trusted adapter nor a
|
|
136
|
-
* configured default provides a scope.
|
|
137
|
-
*/
|
|
138
|
-
@Injectable({ scope: Scope.REQUEST })
|
|
139
|
-
export class OmniScopeContext implements OmniScope {
|
|
140
|
-
readonly scopeId: string;
|
|
141
|
-
|
|
142
|
-
constructor(
|
|
143
|
-
@Inject(REQUEST) request: unknown,
|
|
144
|
-
@Inject(OMNI_KERNEL_OPTIONS)
|
|
145
|
-
options: ReturnType<typeof normalizeOmniKernelRegistrationOptions>,
|
|
146
|
-
) {
|
|
147
|
-
const requestForResolver =
|
|
148
|
-
(request as RequestEnvelope | undefined)?.req ?? request;
|
|
149
|
-
const scopeId = options.resolveScope
|
|
150
|
-
? options.resolveScope(requestForResolver)
|
|
151
|
-
: options.defaultScopeId;
|
|
152
|
-
if (!scopeId || scopeId.trim().length === 0 || scopeId.length > 64) {
|
|
153
|
-
throw new TypeError('Omni scope context is unavailable.');
|
|
154
|
-
}
|
|
155
|
-
this.scopeId = scopeId;
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
cacheKey(key: string): string {
|
|
159
|
-
return `${this.scopeId}:${key}`;
|
|
160
|
-
}
|
|
161
|
-
}
|
|
1
|
+
import { Inject, Injectable, Scope } from '@nestjs/common';
|
|
2
|
+
import { REQUEST } from '@nestjs/core';
|
|
3
|
+
|
|
4
|
+
export interface OmniScope {
|
|
5
|
+
readonly scopeId: string;
|
|
6
|
+
cacheKey(key: string): string;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export type OmniScopeResolver = (request: unknown) => string | undefined;
|
|
10
|
+
|
|
11
|
+
export interface OmniKernelRegistrationOptions {
|
|
12
|
+
dbConnection: string;
|
|
13
|
+
/**
|
|
14
|
+
* Authentication adapters resolve a trusted server scope from the request.
|
|
15
|
+
* The adapter owns authentication; OmniKernel never reads a client DTO field.
|
|
16
|
+
*/
|
|
17
|
+
resolveScope?: OmniScopeResolver;
|
|
18
|
+
/**
|
|
19
|
+
* Compatibility/default partition for applications that do not yet have an
|
|
20
|
+
* authentication adapter. It is server configuration, never request input.
|
|
21
|
+
*/
|
|
22
|
+
defaultScopeId?: string;
|
|
23
|
+
/** Kinds exclusively owned by registered extension projections. */
|
|
24
|
+
reservedRecordKinds?: readonly string[];
|
|
25
|
+
relationKinds?: readonly string[];
|
|
26
|
+
deletion?: Partial<OmniDeletionPolicies>;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export type OmniDeletePolicy = 'hard' | 'tombstone';
|
|
30
|
+
|
|
31
|
+
export interface OmniDeletionPolicies {
|
|
32
|
+
named: OmniDeletePolicy;
|
|
33
|
+
record: OmniDeletePolicy;
|
|
34
|
+
document: OmniDeletePolicy;
|
|
35
|
+
collection: OmniDeletePolicy;
|
|
36
|
+
relation: OmniDeletePolicy;
|
|
37
|
+
externalRef: OmniDeletePolicy;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export const OMNI_KERNEL_OPTIONS = Symbol('OMNI_KERNEL_OPTIONS');
|
|
41
|
+
|
|
42
|
+
export const defaultOmniDeletionPolicies: OmniDeletionPolicies = {
|
|
43
|
+
named: 'hard',
|
|
44
|
+
record: 'tombstone',
|
|
45
|
+
document: 'tombstone',
|
|
46
|
+
collection: 'tombstone',
|
|
47
|
+
relation: 'hard',
|
|
48
|
+
externalRef: 'hard',
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
function normalizeOmniDeletionPolicies(
|
|
52
|
+
deletion: Partial<OmniDeletionPolicies> | undefined,
|
|
53
|
+
): OmniDeletionPolicies {
|
|
54
|
+
const knownResources = new Set(Object.keys(defaultOmniDeletionPolicies));
|
|
55
|
+
for (const [resource, policy] of Object.entries(deletion ?? {})) {
|
|
56
|
+
if (!knownResources.has(resource)) {
|
|
57
|
+
throw new TypeError(
|
|
58
|
+
`Unknown OmniKernel deletion policy resource: ${resource}.`,
|
|
59
|
+
);
|
|
60
|
+
}
|
|
61
|
+
if (policy !== 'hard' && policy !== 'tombstone') {
|
|
62
|
+
throw new TypeError(
|
|
63
|
+
`OmniKernel deletion policy for ${resource} must be hard or tombstone.`,
|
|
64
|
+
);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
return {
|
|
68
|
+
...defaultOmniDeletionPolicies,
|
|
69
|
+
...deletion,
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export function normalizeOmniKernelRegistrationOptions(
|
|
74
|
+
options: string | OmniKernelRegistrationOptions,
|
|
75
|
+
): Required<
|
|
76
|
+
Pick<
|
|
77
|
+
OmniKernelRegistrationOptions,
|
|
78
|
+
'dbConnection' | 'defaultScopeId' | 'relationKinds'
|
|
79
|
+
>
|
|
80
|
+
> &
|
|
81
|
+
Omit<
|
|
82
|
+
OmniKernelRegistrationOptions,
|
|
83
|
+
| 'dbConnection'
|
|
84
|
+
| 'defaultScopeId'
|
|
85
|
+
| 'relationKinds'
|
|
86
|
+
| 'reservedRecordKinds'
|
|
87
|
+
| 'deletion'
|
|
88
|
+
> & {
|
|
89
|
+
reservedRecordKinds: readonly string[];
|
|
90
|
+
deletion: OmniDeletionPolicies;
|
|
91
|
+
} {
|
|
92
|
+
const candidate =
|
|
93
|
+
typeof options === 'string' ? { dbConnection: options } : options;
|
|
94
|
+
if (!candidate.dbConnection) {
|
|
95
|
+
throw new TypeError(
|
|
96
|
+
'OmniKernelModule requires a database connection name.',
|
|
97
|
+
);
|
|
98
|
+
}
|
|
99
|
+
if (
|
|
100
|
+
candidate.defaultScopeId !== undefined &&
|
|
101
|
+
(candidate.defaultScopeId.trim().length === 0 ||
|
|
102
|
+
candidate.defaultScopeId.length > 64)
|
|
103
|
+
) {
|
|
104
|
+
throw new TypeError('OmniKernel defaultScopeId must be 1-64 characters.');
|
|
105
|
+
}
|
|
106
|
+
const reservedRecordKinds = candidate.reservedRecordKinds ?? [];
|
|
107
|
+
if (
|
|
108
|
+
!Array.isArray(reservedRecordKinds) ||
|
|
109
|
+
reservedRecordKinds.some(
|
|
110
|
+
(kind) =>
|
|
111
|
+
typeof kind !== 'string' ||
|
|
112
|
+
kind.trim().length === 0 ||
|
|
113
|
+
kind.length > 64,
|
|
114
|
+
) ||
|
|
115
|
+
new Set(reservedRecordKinds).size !== reservedRecordKinds.length
|
|
116
|
+
) {
|
|
117
|
+
throw new TypeError(
|
|
118
|
+
'OmniKernel reservedRecordKinds must be unique 1-64 character strings.',
|
|
119
|
+
);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
return {
|
|
123
|
+
...candidate,
|
|
124
|
+
defaultScopeId: candidate.defaultScopeId ?? 'default',
|
|
125
|
+
relationKinds: candidate.relationKinds ?? [],
|
|
126
|
+
reservedRecordKinds: Object.freeze([...reservedRecordKinds]),
|
|
127
|
+
deletion: normalizeOmniDeletionPolicies(candidate.deletion),
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
type RequestEnvelope = { req?: unknown };
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Request-scoped server context used by every Omni generated API and loader.
|
|
135
|
+
* It is intentionally unavailable when neither the trusted adapter nor a
|
|
136
|
+
* configured default provides a scope.
|
|
137
|
+
*/
|
|
138
|
+
@Injectable({ scope: Scope.REQUEST })
|
|
139
|
+
export class OmniScopeContext implements OmniScope {
|
|
140
|
+
readonly scopeId: string;
|
|
141
|
+
|
|
142
|
+
constructor(
|
|
143
|
+
@Inject(REQUEST) request: unknown,
|
|
144
|
+
@Inject(OMNI_KERNEL_OPTIONS)
|
|
145
|
+
options: ReturnType<typeof normalizeOmniKernelRegistrationOptions>,
|
|
146
|
+
) {
|
|
147
|
+
const requestForResolver =
|
|
148
|
+
(request as RequestEnvelope | undefined)?.req ?? request;
|
|
149
|
+
const scopeId = options.resolveScope
|
|
150
|
+
? options.resolveScope(requestForResolver)
|
|
151
|
+
: options.defaultScopeId;
|
|
152
|
+
if (!scopeId || scopeId.trim().length === 0 || scopeId.length > 64) {
|
|
153
|
+
throw new TypeError('Omni scope context is unavailable.');
|
|
154
|
+
}
|
|
155
|
+
this.scopeId = scopeId;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
cacheKey(key: string): string {
|
|
159
|
+
return `${this.scopeId}:${key}`;
|
|
160
|
+
}
|
|
161
|
+
}
|