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