@nest-yalc-2/omnikernel-module 1.1.0

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 (230) hide show
  1. package/LICENSE +661 -0
  2. package/README.md +193 -0
  3. package/package.json +52 -0
  4. package/src/__tests__/omni-collection.entity.spec.ts +49 -0
  5. package/src/__tests__/omni-collection.service.spec.ts +83 -0
  6. package/src/__tests__/omni-document.entity.spec.ts +55 -0
  7. package/src/__tests__/omni-document.service.spec.ts +83 -0
  8. package/src/__tests__/omni-extension-projection-constraints.spec.ts +518 -0
  9. package/src/__tests__/omni-extension-projection.registration.spec.ts +946 -0
  10. package/src/__tests__/omni-extension-projection.service.spec.ts +806 -0
  11. package/src/__tests__/omni-external-ref.entity.spec.ts +36 -0
  12. package/src/__tests__/omni-external-ref.service.spec.ts +252 -0
  13. package/src/__tests__/omni-named.entity.spec.ts +33 -0
  14. package/src/__tests__/omni-record.entity.spec.ts +32 -0
  15. package/src/__tests__/omni-record.service.spec.ts +107 -0
  16. package/src/__tests__/omni-relation-semantics.spec.ts +102 -0
  17. package/src/__tests__/omni-relation.entity.spec.ts +38 -0
  18. package/src/__tests__/omni-scope-substrate.spec.ts +81 -0
  19. package/src/__tests__/omni-scoped.backend.spec.ts +115 -0
  20. package/src/__tests__/omni-scoped.service.spec.ts +354 -0
  21. package/src/__tests__/omnikernel.diagnostics.spec.ts +94 -0
  22. package/src/__tests__/omnikernel.module.spec.ts +40 -0
  23. package/src/__tests__/omnikernel.persistence.spec.ts +154 -0
  24. package/src/__tests__/omnikernel.public-api.spec.ts +238 -0
  25. package/src/__tests__/omnikernel.query.service.spec.ts +155 -0
  26. package/src/__tests__/tsconfig.json +6 -0
  27. package/src/base/omni-base.entity.d.ts +9 -0
  28. package/src/base/omni-base.entity.js +53 -0
  29. package/src/base/omni-base.entity.js.map +1 -0
  30. package/src/base/omni-base.entity.ts +41 -0
  31. package/src/base/omni-external-ref.entity.d.ts +13 -0
  32. package/src/base/omni-external-ref.entity.js +72 -0
  33. package/src/base/omni-external-ref.entity.js.map +1 -0
  34. package/src/base/omni-external-ref.entity.ts +53 -0
  35. package/src/base/omni-named.entity.d.ts +6 -0
  36. package/src/base/omni-named.entity.js +35 -0
  37. package/src/base/omni-named.entity.js.map +1 -0
  38. package/src/base/omni-named.entity.ts +16 -0
  39. package/src/base/omni-record.entity.d.ts +13 -0
  40. package/src/base/omni-record.entity.js +71 -0
  41. package/src/base/omni-record.entity.js.map +1 -0
  42. package/src/base/omni-record.entity.ts +51 -0
  43. package/src/base/omni-relation.entity.d.ts +15 -0
  44. package/src/base/omni-relation.entity.js +91 -0
  45. package/src/base/omni-relation.entity.js.map +1 -0
  46. package/src/base/omni-relation.entity.ts +72 -0
  47. package/src/index.d.ts +49 -0
  48. package/src/index.js +66 -0
  49. package/src/index.js.map +1 -0
  50. package/src/index.ts +49 -0
  51. package/src/omni-collection-kind.enum.d.ts +4 -0
  52. package/src/omni-collection-kind.enum.js +13 -0
  53. package/src/omni-collection-kind.enum.js.map +1 -0
  54. package/src/omni-collection-kind.enum.ts +10 -0
  55. package/src/omni-collection.backend.d.ts +2 -0
  56. package/src/omni-collection.backend.js +15 -0
  57. package/src/omni-collection.backend.js.map +1 -0
  58. package/src/omni-collection.backend.ts +16 -0
  59. package/src/omni-collection.dto.d.ts +32 -0
  60. package/src/omni-collection.dto.js +184 -0
  61. package/src/omni-collection.dto.js.map +1 -0
  62. package/src/omni-collection.dto.ts +170 -0
  63. package/src/omni-collection.entity.d.ts +7 -0
  64. package/src/omni-collection.entity.js +41 -0
  65. package/src/omni-collection.entity.js.map +1 -0
  66. package/src/omni-collection.entity.ts +21 -0
  67. package/src/omni-collection.service.d.ts +14 -0
  68. package/src/omni-collection.service.js +24 -0
  69. package/src/omni-collection.service.js.map +1 -0
  70. package/src/omni-collection.service.ts +75 -0
  71. package/src/omni-document-kind.enum.d.ts +6 -0
  72. package/src/omni-document-kind.enum.js +15 -0
  73. package/src/omni-document-kind.enum.js.map +1 -0
  74. package/src/omni-document-kind.enum.ts +12 -0
  75. package/src/omni-document.backend.d.ts +2 -0
  76. package/src/omni-document.backend.js +15 -0
  77. package/src/omni-document.backend.js.map +1 -0
  78. package/src/omni-document.backend.ts +16 -0
  79. package/src/omni-document.dto.d.ts +35 -0
  80. package/src/omni-document.dto.js +213 -0
  81. package/src/omni-document.dto.js.map +1 -0
  82. package/src/omni-document.dto.ts +198 -0
  83. package/src/omni-document.entity.d.ts +10 -0
  84. package/src/omni-document.entity.js +53 -0
  85. package/src/omni-document.entity.js.map +1 -0
  86. package/src/omni-document.entity.ts +30 -0
  87. package/src/omni-document.service.d.ts +14 -0
  88. package/src/omni-document.service.js +24 -0
  89. package/src/omni-document.service.js.map +1 -0
  90. package/src/omni-document.service.ts +75 -0
  91. package/src/omni-dto.helpers.d.ts +1 -0
  92. package/src/omni-dto.helpers.js +10 -0
  93. package/src/omni-dto.helpers.js.map +1 -0
  94. package/src/omni-dto.helpers.ts +21 -0
  95. package/src/omni-extension-projection.definition.d.ts +17 -0
  96. package/src/omni-extension-projection.definition.js +160 -0
  97. package/src/omni-extension-projection.definition.js.map +1 -0
  98. package/src/omni-extension-projection.definition.ts +294 -0
  99. package/src/omni-extension-projection.resource.d.ts +42 -0
  100. package/src/omni-extension-projection.resource.js +127 -0
  101. package/src/omni-extension-projection.resource.js.map +1 -0
  102. package/src/omni-extension-projection.resource.ts +274 -0
  103. package/src/omni-extension-projection.service.d.ts +33 -0
  104. package/src/omni-extension-projection.service.js +332 -0
  105. package/src/omni-extension-projection.service.js.map +1 -0
  106. package/src/omni-extension-projection.service.ts +452 -0
  107. package/src/omni-external-ref-binding.validator.d.ts +14 -0
  108. package/src/omni-external-ref-binding.validator.js +34 -0
  109. package/src/omni-external-ref-binding.validator.js.map +1 -0
  110. package/src/omni-external-ref-binding.validator.ts +53 -0
  111. package/src/omni-external-ref-internal-type.enum.d.ts +5 -0
  112. package/src/omni-external-ref-internal-type.enum.js +14 -0
  113. package/src/omni-external-ref-internal-type.enum.js.map +1 -0
  114. package/src/omni-external-ref-internal-type.enum.ts +11 -0
  115. package/src/omni-external-ref.backend.d.ts +2 -0
  116. package/src/omni-external-ref.backend.js +19 -0
  117. package/src/omni-external-ref.backend.js.map +1 -0
  118. package/src/omni-external-ref.backend.ts +25 -0
  119. package/src/omni-external-ref.dto.d.ts +26 -0
  120. package/src/omni-external-ref.dto.js +140 -0
  121. package/src/omni-external-ref.dto.js.map +1 -0
  122. package/src/omni-external-ref.dto.ts +128 -0
  123. package/src/omni-external-ref.service.d.ts +33 -0
  124. package/src/omni-external-ref.service.js +133 -0
  125. package/src/omni-external-ref.service.js.map +1 -0
  126. package/src/omni-external-ref.service.ts +255 -0
  127. package/src/omni-migration.d.ts +28 -0
  128. package/src/omni-migration.js +239 -0
  129. package/src/omni-migration.js.map +1 -0
  130. package/src/omni-migration.ts +365 -0
  131. package/src/omni-named.backend.d.ts +2 -0
  132. package/src/omni-named.backend.js +13 -0
  133. package/src/omni-named.backend.js.map +1 -0
  134. package/src/omni-named.backend.ts +11 -0
  135. package/src/omni-named.dto.d.ts +19 -0
  136. package/src/omni-named.dto.js +94 -0
  137. package/src/omni-named.dto.js.map +1 -0
  138. package/src/omni-named.dto.ts +86 -0
  139. package/src/omni-projection.catalog.d.ts +49 -0
  140. package/src/omni-projection.catalog.js +177 -0
  141. package/src/omni-projection.catalog.js.map +1 -0
  142. package/src/omni-projection.catalog.ts +345 -0
  143. package/src/omni-projection.lifecycle.d.ts +20 -0
  144. package/src/omni-projection.lifecycle.js +3 -0
  145. package/src/omni-projection.lifecycle.js.map +1 -0
  146. package/src/omni-projection.lifecycle.ts +53 -0
  147. package/src/omni-record-status.enum.d.ts +5 -0
  148. package/src/omni-record-status.enum.js +14 -0
  149. package/src/omni-record-status.enum.js.map +1 -0
  150. package/src/omni-record-status.enum.ts +11 -0
  151. package/src/omni-record.backend.d.ts +2 -0
  152. package/src/omni-record.backend.js +13 -0
  153. package/src/omni-record.backend.js.map +1 -0
  154. package/src/omni-record.backend.ts +19 -0
  155. package/src/omni-record.dto.d.ts +29 -0
  156. package/src/omni-record.dto.js +169 -0
  157. package/src/omni-record.dto.js.map +1 -0
  158. package/src/omni-record.dto.ts +157 -0
  159. package/src/omni-record.service.d.ts +18 -0
  160. package/src/omni-record.service.js +45 -0
  161. package/src/omni-record.service.js.map +1 -0
  162. package/src/omni-record.service.ts +113 -0
  163. package/src/omni-relation-kind.contract.d.ts +9 -0
  164. package/src/omni-relation-kind.contract.js +33 -0
  165. package/src/omni-relation-kind.contract.js.map +1 -0
  166. package/src/omni-relation-kind.contract.ts +49 -0
  167. package/src/omni-relation-kind.enum.d.ts +6 -0
  168. package/src/omni-relation-kind.enum.js +15 -0
  169. package/src/omni-relation-kind.enum.js.map +1 -0
  170. package/src/omni-relation-kind.enum.ts +12 -0
  171. package/src/omni-relation-projection.definition.d.ts +25 -0
  172. package/src/omni-relation-projection.definition.js +83 -0
  173. package/src/omni-relation-projection.definition.js.map +1 -0
  174. package/src/omni-relation-projection.definition.ts +166 -0
  175. package/src/omni-relation-projection.resource.d.ts +51 -0
  176. package/src/omni-relation-projection.resource.js +203 -0
  177. package/src/omni-relation-projection.resource.js.map +1 -0
  178. package/src/omni-relation-projection.resource.ts +360 -0
  179. package/src/omni-relation-projection.service.d.ts +47 -0
  180. package/src/omni-relation-projection.service.js +385 -0
  181. package/src/omni-relation-projection.service.js.map +1 -0
  182. package/src/omni-relation-projection.service.ts +610 -0
  183. package/src/omni-relation-semantics.d.ts +11 -0
  184. package/src/omni-relation-semantics.js +34 -0
  185. package/src/omni-relation-semantics.js.map +1 -0
  186. package/src/omni-relation-semantics.ts +52 -0
  187. package/src/omni-relation-status.enum.d.ts +5 -0
  188. package/src/omni-relation-status.enum.js +14 -0
  189. package/src/omni-relation-status.enum.js.map +1 -0
  190. package/src/omni-relation-status.enum.ts +11 -0
  191. package/src/omni-relation.backend.d.ts +2 -0
  192. package/src/omni-relation.backend.js +17 -0
  193. package/src/omni-relation.backend.js.map +1 -0
  194. package/src/omni-relation.backend.ts +21 -0
  195. package/src/omni-relation.dto.d.ts +28 -0
  196. package/src/omni-relation.dto.js +153 -0
  197. package/src/omni-relation.dto.js.map +1 -0
  198. package/src/omni-relation.dto.ts +142 -0
  199. package/src/omni-relation.service.d.ts +19 -0
  200. package/src/omni-relation.service.js +76 -0
  201. package/src/omni-relation.service.js.map +1 -0
  202. package/src/omni-relation.service.ts +159 -0
  203. package/src/omni-scope.d.ts +33 -0
  204. package/src/omni-scope.js +91 -0
  205. package/src/omni-scope.js.map +1 -0
  206. package/src/omni-scope.ts +161 -0
  207. package/src/omni-scoped.backend.d.ts +21 -0
  208. package/src/omni-scoped.backend.js +47 -0
  209. package/src/omni-scoped.backend.js.map +1 -0
  210. package/src/omni-scoped.backend.ts +110 -0
  211. package/src/omni-scoped.repository.d.ts +10 -0
  212. package/src/omni-scoped.repository.js +26 -0
  213. package/src/omni-scoped.repository.js.map +1 -0
  214. package/src/omni-scoped.repository.ts +34 -0
  215. package/src/omni-scoped.service.d.ts +36 -0
  216. package/src/omni-scoped.service.js +171 -0
  217. package/src/omni-scoped.service.js.map +1 -0
  218. package/src/omni-scoped.service.ts +338 -0
  219. package/src/omnikernel.diagnostics.d.ts +16 -0
  220. package/src/omnikernel.diagnostics.js +50 -0
  221. package/src/omnikernel.diagnostics.js.map +1 -0
  222. package/src/omnikernel.diagnostics.ts +83 -0
  223. package/src/omnikernel.module.d.ts +5 -0
  224. package/src/omnikernel.module.js +87 -0
  225. package/src/omnikernel.module.js.map +1 -0
  226. package/src/omnikernel.module.ts +97 -0
  227. package/src/omnikernel.query.service.d.ts +17 -0
  228. package/src/omnikernel.query.service.js +85 -0
  229. package/src/omnikernel.query.service.js.map +1 -0
  230. package/src/omnikernel.query.service.ts +108 -0
@@ -0,0 +1,610 @@
1
+ import { BadRequestException, ConflictException } from '@nestjs/common';
2
+ import {
3
+ Operators,
4
+ PROJECTION_INTEGER_MAX,
5
+ type CrudGenFindManyOptions,
6
+ } from '@nest-yalc-2/crud-gen';
7
+ import {
8
+ In,
9
+ IsNull,
10
+ type DataSource,
11
+ type DeepPartial,
12
+ type EntityManager,
13
+ type FindOptionsWhere,
14
+ type ObjectLiteral,
15
+ type Repository,
16
+ } from 'typeorm';
17
+ import { OmniRecordEntity } from './base/omni-record.entity.js';
18
+ import { OmniRelationEntity } from './base/omni-relation.entity.js';
19
+ import {
20
+ createOmniProjectionReaderCatalog,
21
+ type OmniProjectionReaderCatalog,
22
+ } from './omni-projection.catalog.js';
23
+ import {
24
+ getOmniRelationProjectionAliases,
25
+ getOmniRelationProjectionAllowedKinds,
26
+ type OmniRelationProjectionDefinition,
27
+ } from './omni-relation-projection.definition.js';
28
+ import type { OmniProjectionLifecycle } from './omni-projection.lifecycle.js';
29
+ import { OmniRelationService } from './omni-relation.service.js';
30
+ import { OmniRelationStatus } from './omni-relation-status.enum.js';
31
+ import type { OmniRelationKindContract } from './omni-relation-kind.contract.js';
32
+ import type { OmniDeletePolicy, OmniScope } from './omni-scope.js';
33
+
34
+ function hasOwn(input: object, key: string): boolean {
35
+ return Object.prototype.hasOwnProperty.call(input, key);
36
+ }
37
+
38
+ function isRetryableTransactionError(error: unknown): boolean {
39
+ const candidate = error as {
40
+ code?: unknown;
41
+ driverError?: { code?: unknown };
42
+ };
43
+ const code = candidate?.driverError?.code ?? candidate?.code;
44
+ return (
45
+ code === '40001' ||
46
+ code === 'SQLITE_BUSY' ||
47
+ code === 'SQLITE_BUSY_SNAPSHOT'
48
+ );
49
+ }
50
+
51
+ /**
52
+ * One generated relation surface over OmniRelationEntity. Creation selects an
53
+ * allowed kind; every other operation constrains that field to the registered
54
+ * set and keeps endpoints, status, and schema metadata server-owned.
55
+ */
56
+ export class OmniRelationProjectionService extends OmniRelationService {
57
+ constructor(
58
+ repository: Repository<OmniRelationEntity>,
59
+ scope: OmniScope,
60
+ private readonly relationDeletion: OmniDeletePolicy,
61
+ recordRepository: Repository<OmniRecordEntity>,
62
+ kinds: OmniRelationKindContract,
63
+ private readonly definition: OmniRelationProjectionDefinition,
64
+ private readonly dataSource?: DataSource,
65
+ private readonly lifecycle?: OmniProjectionLifecycle<
66
+ OmniRelationProjectionDefinition,
67
+ OmniRelationEntity
68
+ >,
69
+ private readonly readerCatalog?: OmniProjectionReaderCatalog,
70
+ ) {
71
+ super(repository, scope, relationDeletion, recordRepository, kinds);
72
+ for (const kind of getOmniRelationProjectionAllowedKinds(definition)) {
73
+ try {
74
+ kinds.assert(kind);
75
+ } catch (error) {
76
+ throw new TypeError(
77
+ error instanceof Error
78
+ ? error.message
79
+ : 'Omni relation projection kind is not registered.',
80
+ );
81
+ }
82
+ }
83
+ }
84
+
85
+ override async createEntity(
86
+ input: DeepPartial<OmniRelationEntity>,
87
+ findOptions?: CrudGenFindManyOptions<OmniRelationEntity>,
88
+ returnEntity?: true,
89
+ ): Promise<OmniRelationEntity>;
90
+ override async createEntity(
91
+ input: DeepPartial<OmniRelationEntity>,
92
+ findOptions?: CrudGenFindManyOptions<OmniRelationEntity>,
93
+ returnEntity?: boolean,
94
+ ): Promise<OmniRelationEntity | boolean>;
95
+ override async createEntity(
96
+ input: DeepPartial<OmniRelationEntity>,
97
+ findOptions?: CrudGenFindManyOptions<OmniRelationEntity>,
98
+ returnEntity = true,
99
+ ): Promise<OmniRelationEntity | boolean> {
100
+ const normalized = this.normalizeInput(input as Record<string, unknown>);
101
+ this.rejectFixedFields(normalized, true);
102
+ this.selectCreateKind(normalized);
103
+ const created = this.createValues(normalized);
104
+ if (!this.lifecycle) {
105
+ const result = await super.createEntity(
106
+ created,
107
+ findOptions,
108
+ returnEntity,
109
+ );
110
+ return typeof result === 'boolean' ? result : this.publicEntity(result);
111
+ }
112
+ const result = await this.mutate(async (manager) => {
113
+ await this.assertRelation(
114
+ created,
115
+ manager.getRepository(OmniRecordEntity),
116
+ );
117
+ await this.lifecycle!.beforeCreate?.({
118
+ definition: this.definition,
119
+ scope: this.scope,
120
+ manager,
121
+ readers: this.readers(manager),
122
+ input: this.publicInput(normalized),
123
+ });
124
+ const repository = manager.getRepository(OmniRelationEntity);
125
+ const entity = {
126
+ ...created,
127
+ scopeId: this.scopeId,
128
+ };
129
+ const guid = this.requiredIdentifier(entity.guid, 'guid');
130
+ await repository.insert(entity as never);
131
+ if (!returnEntity) return true;
132
+ return repository.findOneOrFail({
133
+ where: {
134
+ scopeId: this.scopeId,
135
+ guid,
136
+ ...this.definitionFilters(),
137
+ } as FindOptionsWhere<OmniRelationEntity>,
138
+ });
139
+ });
140
+ return typeof result === 'boolean' ? result : this.publicEntity(result);
141
+ }
142
+
143
+ override async getEntity(
144
+ conditions:
145
+ | FindOptionsWhere<OmniRelationEntity>[]
146
+ | FindOptionsWhere<OmniRelationEntity>
147
+ | ObjectLiteral
148
+ | string,
149
+ fields?: (keyof OmniRelationEntity)[],
150
+ relations?: string[],
151
+ databaseName?: string,
152
+ options?: { failOnNull: false },
153
+ ): Promise<OmniRelationEntity | undefined>;
154
+ override async getEntity(
155
+ conditions:
156
+ | FindOptionsWhere<OmniRelationEntity>[]
157
+ | FindOptionsWhere<OmniRelationEntity>
158
+ | ObjectLiteral
159
+ | string,
160
+ fields?: (keyof OmniRelationEntity)[],
161
+ relations?: string[],
162
+ databaseName?: string,
163
+ options?: { failOnNull?: boolean },
164
+ ): Promise<OmniRelationEntity>;
165
+ override async getEntity(
166
+ conditions:
167
+ | FindOptionsWhere<OmniRelationEntity>[]
168
+ | FindOptionsWhere<OmniRelationEntity>
169
+ | ObjectLiteral
170
+ | string,
171
+ fields?: (keyof OmniRelationEntity)[],
172
+ relations?: string[],
173
+ databaseName?: string,
174
+ options?: { failOnNull?: boolean },
175
+ ): Promise<OmniRelationEntity | undefined> {
176
+ const entity = await super.getEntity(
177
+ this.withDefinitionConditions(conditions),
178
+ fields,
179
+ relations,
180
+ databaseName,
181
+ options as never,
182
+ );
183
+ return entity ? this.publicEntity(entity) : entity;
184
+ }
185
+
186
+ override async getEntityListExtended(
187
+ findOptions?: CrudGenFindManyOptions<OmniRelationEntity>,
188
+ withCount?: false,
189
+ relations?: string[],
190
+ databaseName?: string,
191
+ ): Promise<OmniRelationEntity[]>;
192
+ override async getEntityListExtended(
193
+ findOptions: CrudGenFindManyOptions<OmniRelationEntity>,
194
+ withCount: true,
195
+ relations?: string[],
196
+ databaseName?: string,
197
+ ): Promise<[OmniRelationEntity[], number]>;
198
+ override async getEntityListExtended(
199
+ findOptions: CrudGenFindManyOptions<OmniRelationEntity> = {},
200
+ withCount = false,
201
+ relations?: string[],
202
+ databaseName?: string,
203
+ ): Promise<OmniRelationEntity[] | [OmniRelationEntity[], number]> {
204
+ this.rejectFixedFilters(findOptions.where);
205
+ const where = {
206
+ operator: Operators.AND,
207
+ filters: this.definitionFilters(),
208
+ ...(findOptions.where ? { childExpressions: [findOptions.where] } : {}),
209
+ };
210
+ if (withCount) {
211
+ const result = await super.getEntityListExtended(
212
+ { ...findOptions, where },
213
+ true,
214
+ relations,
215
+ databaseName,
216
+ );
217
+ return [result[0].map((entity) => this.publicEntity(entity)), result[1]];
218
+ }
219
+ const result = await super.getEntityListExtended(
220
+ { ...findOptions, where },
221
+ false,
222
+ relations,
223
+ databaseName,
224
+ );
225
+ return result.map((entity) => this.publicEntity(entity));
226
+ }
227
+
228
+ override async updateEntity(
229
+ conditions: FindOptionsWhere<OmniRelationEntity>,
230
+ input: DeepPartial<OmniRelationEntity>,
231
+ _findOptions?: CrudGenFindManyOptions<OmniRelationEntity>,
232
+ returnEntity?: true,
233
+ ): Promise<OmniRelationEntity>;
234
+ override async updateEntity(
235
+ conditions: FindOptionsWhere<OmniRelationEntity>,
236
+ input: DeepPartial<OmniRelationEntity>,
237
+ _findOptions?: CrudGenFindManyOptions<OmniRelationEntity>,
238
+ returnEntity?: boolean,
239
+ ): Promise<OmniRelationEntity | boolean>;
240
+ override async updateEntity(
241
+ conditions: FindOptionsWhere<OmniRelationEntity>,
242
+ input: DeepPartial<OmniRelationEntity>,
243
+ _findOptions?: CrudGenFindManyOptions<OmniRelationEntity>,
244
+ returnEntity = true,
245
+ ): Promise<OmniRelationEntity | boolean> {
246
+ const normalized = this.normalizeInput(input as Record<string, unknown>);
247
+ this.rejectFixedFields(normalized);
248
+ this.rejectImmutableEndpoints(normalized);
249
+ this.validatePayload(normalized);
250
+ const expectedRevision = normalized.expectedRevision;
251
+ if (
252
+ typeof expectedRevision !== 'number' ||
253
+ !Number.isInteger(expectedRevision) ||
254
+ expectedRevision < 1 ||
255
+ expectedRevision >= PROJECTION_INTEGER_MAX
256
+ ) {
257
+ throw new BadRequestException(
258
+ `expectedRevision must be an integer between 1 and ${PROJECTION_INTEGER_MAX - 1}.`,
259
+ );
260
+ }
261
+ const fixedConditions = this.withDefinitionConditions(
262
+ this.normalizeConditions(conditions),
263
+ ) as FindOptionsWhere<OmniRelationEntity>;
264
+ const { expectedRevision: _expectedRevision, ...changes } = normalized;
265
+ if (Object.keys(changes).length === 0) {
266
+ throw new BadRequestException('Omni relation update requires metadata.');
267
+ }
268
+ const result = await this.mutate(async (manager) => {
269
+ const repository = manager.getRepository(OmniRelationEntity);
270
+ const current = await repository.findOne({ where: fixedConditions });
271
+ if (!current) this.notFound();
272
+ if (current.revision !== expectedRevision) {
273
+ throw new ConflictException('Omni relation revision conflict.');
274
+ }
275
+ await this.assertRelation(
276
+ { ...current, ...changes },
277
+ manager.getRepository(OmniRecordEntity),
278
+ );
279
+ await this.lifecycle?.beforeUpdate?.({
280
+ definition: this.definition,
281
+ scope: this.scope,
282
+ manager,
283
+ readers: this.readers(manager),
284
+ input: this.publicInput(normalized),
285
+ current,
286
+ });
287
+ const result = await repository
288
+ .createQueryBuilder()
289
+ .update()
290
+ .set({ ...changes, revision: () => '"revision" + 1' } as never)
291
+ .where('"scopeId" = :scopeId', { scopeId: this.scopeId })
292
+ .andWhere('"guid" = :guid', { guid: current.guid })
293
+ .andWhere('"revision" = :expectedRevision', { expectedRevision })
294
+ .andWhere('"kind" IN (:...kinds)', {
295
+ kinds: [...getOmniRelationProjectionAllowedKinds(this.definition)],
296
+ })
297
+ .andWhere('"status" = :status', {
298
+ status: this.definition.relation.status ?? OmniRelationStatus.Active,
299
+ })
300
+ .andWhere(
301
+ this.definition.relation.schema
302
+ ? '"payloadSchemaId" = :schemaId AND "payloadSchemaVersion" = :schemaVersion'
303
+ : '"payloadSchemaId" IS NULL AND "payloadSchemaVersion" IS NULL',
304
+ this.definition.relation.schema
305
+ ? {
306
+ schemaId: this.definition.relation.schema.id,
307
+ schemaVersion: this.definition.relation.schema.version,
308
+ }
309
+ : {},
310
+ )
311
+ .execute();
312
+ if (result.affected !== 1) {
313
+ const existing = await repository.findOne({ where: fixedConditions });
314
+ if (!existing) this.notFound();
315
+ throw new ConflictException('Omni relation revision conflict.');
316
+ }
317
+ if (!returnEntity) return true;
318
+ return repository.findOneOrFail({ where: fixedConditions });
319
+ });
320
+ return typeof result === 'boolean' ? result : this.publicEntity(result);
321
+ }
322
+
323
+ override async deleteEntity(
324
+ conditions: FindOptionsWhere<OmniRelationEntity>,
325
+ ): Promise<boolean> {
326
+ const fixedConditions = this.withDefinitionConditions(
327
+ this.normalizeConditions(conditions),
328
+ ) as FindOptionsWhere<OmniRelationEntity>;
329
+ if (!this.lifecycle) return super.deleteEntity(fixedConditions);
330
+ return this.mutate(async (manager) => {
331
+ const repository = manager.getRepository(OmniRelationEntity);
332
+ const current = await repository.findOne({ where: fixedConditions });
333
+ if (!current) this.notFound();
334
+ await this.lifecycle!.beforeDelete?.({
335
+ definition: this.definition,
336
+ scope: this.scope,
337
+ manager,
338
+ readers: this.readers(manager),
339
+ input: {},
340
+ current,
341
+ });
342
+ const result =
343
+ this.relationDeletion === 'hard'
344
+ ? await repository.delete(fixedConditions)
345
+ : await repository.update(fixedConditions, { deletedAt: new Date() });
346
+ if (result.affected !== 1) this.notFound();
347
+ return true;
348
+ });
349
+ }
350
+
351
+ protected override assertEndpointKinds(
352
+ source: OmniRecordEntity,
353
+ target: OmniRecordEntity,
354
+ ): void {
355
+ if (
356
+ source.kind !== this.definition.relation.sourceKind ||
357
+ target.kind !== this.definition.relation.targetKind
358
+ ) {
359
+ throw new BadRequestException(
360
+ 'Omni relation endpoints do not match the registered resource kinds.',
361
+ );
362
+ }
363
+ }
364
+
365
+ private createValues(
366
+ input: Record<string, unknown>,
367
+ ): DeepPartial<OmniRelationEntity> {
368
+ return {
369
+ ...input,
370
+ kind: input.kind as string,
371
+ status: this.definition.relation.status ?? OmniRelationStatus.Active,
372
+ ...(this.definition.relation.schema
373
+ ? {
374
+ payloadSchemaId: this.definition.relation.schema.id,
375
+ payloadSchemaVersion: this.definition.relation.schema.version,
376
+ }
377
+ : {}),
378
+ } as DeepPartial<OmniRelationEntity>;
379
+ }
380
+
381
+ private publicEntity(entity: OmniRelationEntity): OmniRelationEntity {
382
+ const aliases = getOmniRelationProjectionAliases(this.definition);
383
+ return {
384
+ ...entity,
385
+ ...(aliases.kind === 'kind' ? {} : { [aliases.kind]: entity.kind }),
386
+ ...(aliases.source === 'sourceRecordId'
387
+ ? {}
388
+ : { [aliases.source]: entity.sourceRecordId }),
389
+ ...(aliases.target === 'targetRecordId'
390
+ ? {}
391
+ : { [aliases.target]: entity.targetRecordId }),
392
+ ...(aliases.payload === 'payload'
393
+ ? {}
394
+ : { [aliases.payload]: entity.payload }),
395
+ } as OmniRelationEntity;
396
+ }
397
+
398
+ /** Keeps lifecycle policies transport-neutral by exposing definition aliases. */
399
+ private publicInput(
400
+ input: Readonly<Record<string, unknown>>,
401
+ ): Readonly<Record<string, unknown>> {
402
+ const aliases = getOmniRelationProjectionAliases(this.definition);
403
+ const output = { ...input };
404
+ for (const [field, alias] of [
405
+ ['kind', aliases.kind],
406
+ ['sourceRecordId', aliases.source],
407
+ ['targetRecordId', aliases.target],
408
+ ['payload', aliases.payload],
409
+ ] as const) {
410
+ if (field === alias || !hasOwn(output, field)) continue;
411
+ output[alias] = output[field];
412
+ delete output[field];
413
+ }
414
+ return output;
415
+ }
416
+
417
+ private selectCreateKind(input: Record<string, unknown>): void {
418
+ const allowed = getOmniRelationProjectionAllowedKinds(this.definition);
419
+ if (allowed.length === 1) {
420
+ if (input.kind !== undefined) {
421
+ throw new BadRequestException('Omni relation kind is server-owned.');
422
+ }
423
+ input.kind = allowed[0];
424
+ return;
425
+ }
426
+ if (typeof input.kind !== 'string' || !allowed.includes(input.kind)) {
427
+ throw new BadRequestException(
428
+ 'Omni relation kind is not allowed by this resource.',
429
+ );
430
+ }
431
+ }
432
+
433
+ private normalizeInput(
434
+ input: Record<string, unknown>,
435
+ ): Record<string, unknown> {
436
+ const normalized = { ...input };
437
+ const aliases = getOmniRelationProjectionAliases(this.definition);
438
+ for (const [alias, field] of [
439
+ [aliases.kind, 'kind'],
440
+ [aliases.source, 'sourceRecordId'],
441
+ [aliases.target, 'targetRecordId'],
442
+ [aliases.payload, 'payload'],
443
+ ] as const) {
444
+ if (alias === field) continue;
445
+ if (!hasOwn(normalized, alias)) continue;
446
+ if (hasOwn(normalized, field)) {
447
+ throw new BadRequestException(
448
+ `Omni relation ${alias} conflicts with ${field}.`,
449
+ );
450
+ }
451
+ normalized[field] = normalized[alias];
452
+ delete normalized[alias];
453
+ }
454
+ return normalized;
455
+ }
456
+
457
+ private normalizeConditions(
458
+ conditions: FindOptionsWhere<OmniRelationEntity>,
459
+ ): FindOptionsWhere<OmniRelationEntity> {
460
+ return this.normalizeInput(
461
+ conditions as Record<string, unknown>,
462
+ ) as FindOptionsWhere<OmniRelationEntity>;
463
+ }
464
+
465
+ private rejectFixedFields(
466
+ input: Record<string, unknown>,
467
+ allowKind = false,
468
+ ): void {
469
+ for (const field of [
470
+ ...(allowKind ? [] : ['kind']),
471
+ 'status',
472
+ 'payloadSchemaId',
473
+ 'payloadSchemaVersion',
474
+ ]) {
475
+ if (hasOwn(input, field)) {
476
+ throw new BadRequestException(
477
+ `Omni relation ${field} is server-owned.`,
478
+ );
479
+ }
480
+ }
481
+ }
482
+
483
+ private rejectImmutableEndpoints(input: Record<string, unknown>): void {
484
+ for (const field of ['guid', 'sourceRecordId', 'targetRecordId']) {
485
+ if (hasOwn(input, field)) {
486
+ throw new BadRequestException(`Omni relation ${field} is immutable.`);
487
+ }
488
+ }
489
+ }
490
+
491
+ private validatePayload(input: Record<string, unknown>): void {
492
+ if (
493
+ hasOwn(input, 'payload') &&
494
+ input.payload !== null &&
495
+ (typeof input.payload !== 'object' || Array.isArray(input.payload))
496
+ ) {
497
+ throw new BadRequestException('payload must be a JSON object or null.');
498
+ }
499
+ }
500
+
501
+ private withDefinitionConditions(
502
+ conditions:
503
+ | FindOptionsWhere<OmniRelationEntity>[]
504
+ | FindOptionsWhere<OmniRelationEntity>
505
+ | ObjectLiteral
506
+ | string,
507
+ ):
508
+ | FindOptionsWhere<OmniRelationEntity>[]
509
+ | FindOptionsWhere<OmniRelationEntity>
510
+ | ObjectLiteral {
511
+ if (typeof conditions === 'string') {
512
+ return { guid: conditions, ...this.definitionFilters() };
513
+ }
514
+ if (Array.isArray(conditions)) {
515
+ return conditions.map(
516
+ (condition) =>
517
+ this.withDefinitionConditions(
518
+ condition,
519
+ ) as FindOptionsWhere<OmniRelationEntity>,
520
+ );
521
+ }
522
+ this.assertFixedConditions(conditions);
523
+ return { ...conditions, ...this.definitionFilters() };
524
+ }
525
+
526
+ private definitionFilters(): Record<string, unknown> {
527
+ const relation = this.definition.relation;
528
+ return {
529
+ kind: In([...getOmniRelationProjectionAllowedKinds(this.definition)]),
530
+ status: relation.status ?? OmniRelationStatus.Active,
531
+ ...(relation.schema
532
+ ? {
533
+ payloadSchemaId: relation.schema.id,
534
+ payloadSchemaVersion: relation.schema.version,
535
+ }
536
+ : {
537
+ payloadSchemaId: IsNull(),
538
+ payloadSchemaVersion: IsNull(),
539
+ }),
540
+ };
541
+ }
542
+
543
+ private assertFixedConditions(conditions: ObjectLiteral): void {
544
+ for (const field of Object.keys(this.definitionFilters())) {
545
+ if (hasOwn(conditions, field)) {
546
+ throw new BadRequestException(
547
+ `Omni relation ${field} is server-owned.`,
548
+ );
549
+ }
550
+ }
551
+ }
552
+
553
+ private rejectFixedFilters(where: unknown): void {
554
+ if (!where || typeof where !== 'object') return;
555
+ if (Array.isArray(where)) {
556
+ where.forEach((entry) => this.rejectFixedFilters(entry));
557
+ return;
558
+ }
559
+ const candidate = where as Record<string, unknown>;
560
+ for (const field of Object.keys(this.definitionFilters())) {
561
+ if (hasOwn(candidate, field)) {
562
+ throw new BadRequestException(
563
+ `Omni relation ${field} is server-owned.`,
564
+ );
565
+ }
566
+ }
567
+ if (candidate.filters && typeof candidate.filters === 'object') {
568
+ this.rejectFixedFilters(candidate.filters);
569
+ }
570
+ if (Array.isArray(candidate.childExpressions)) {
571
+ candidate.childExpressions.forEach((entry) =>
572
+ this.rejectFixedFilters(entry),
573
+ );
574
+ }
575
+ }
576
+
577
+ private readers(manager: EntityManager) {
578
+ return (
579
+ this.readerCatalog ??
580
+ createOmniProjectionReaderCatalog([
581
+ {
582
+ type: 'relation',
583
+ id: this.definition.id,
584
+ definition: this.definition,
585
+ },
586
+ ])
587
+ ).bind(manager, this.scope);
588
+ }
589
+
590
+ private async mutate<T>(
591
+ work: (manager: EntityManager) => Promise<T>,
592
+ ): Promise<T> {
593
+ if (!this.lifecycle) return work(this.getRepositoryWrite().manager);
594
+ if (!this.dataSource) {
595
+ throw new TypeError(
596
+ 'Omni relation lifecycle requires a DataSource for serializable mutations.',
597
+ );
598
+ }
599
+ try {
600
+ return await this.dataSource.transaction('SERIALIZABLE', work);
601
+ } catch (error) {
602
+ if (isRetryableTransactionError(error)) {
603
+ throw new ConflictException(
604
+ 'Omni projection concurrent write conflict; retry the mutation.',
605
+ );
606
+ }
607
+ throw error;
608
+ }
609
+ }
610
+ }
@@ -0,0 +1,11 @@
1
+ import { OmniRelationKind } from './omni-relation-kind.enum.js';
2
+ export interface OmniRelationSemanticsInput {
3
+ sourceKind: string;
4
+ targetKind: string;
5
+ relationKind: string;
6
+ }
7
+ export declare const OMNI_COLLECTION_MEMBERSHIP_RELATION_KIND = OmniRelationKind.Contains;
8
+ export declare const isOmniCollectionRecordKind: (kind: string) => boolean;
9
+ export declare const isOmniDocumentRecordKind: (kind: string) => boolean;
10
+ export declare const isCanonicalCollectionMembershipRelation: ({ sourceKind, relationKind, }: Pick<OmniRelationSemanticsInput, "sourceKind" | "relationKind">) => boolean;
11
+ export declare const isAllowedOmniRelation: ({ sourceKind, targetKind, relationKind, }: OmniRelationSemanticsInput) => boolean;
@@ -0,0 +1,34 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.isAllowedOmniRelation = exports.isCanonicalCollectionMembershipRelation = exports.isOmniDocumentRecordKind = exports.isOmniCollectionRecordKind = exports.OMNI_COLLECTION_MEMBERSHIP_RELATION_KIND = void 0;
4
+ const omni_collection_kind_enum_js_1 = require("./omni-collection-kind.enum.js");
5
+ const omni_document_kind_enum_js_1 = require("./omni-document-kind.enum.js");
6
+ const omni_relation_kind_enum_js_1 = require("./omni-relation-kind.enum.js");
7
+ exports.OMNI_COLLECTION_MEMBERSHIP_RELATION_KIND = omni_relation_kind_enum_js_1.OmniRelationKind.Contains;
8
+ const hasKind = (kind) => kind.trim().length > 0;
9
+ const isOmniCollectionRecordKind = (kind) => kind === omni_collection_kind_enum_js_1.OmniCollectionKind.Collection;
10
+ exports.isOmniCollectionRecordKind = isOmniCollectionRecordKind;
11
+ const isOmniDocumentRecordKind = (kind) => kind === omni_document_kind_enum_js_1.OmniDocumentKind.Document;
12
+ exports.isOmniDocumentRecordKind = isOmniDocumentRecordKind;
13
+ const isCanonicalCollectionMembershipRelation = ({ sourceKind, relationKind, }) => relationKind === exports.OMNI_COLLECTION_MEMBERSHIP_RELATION_KIND &&
14
+ (0, exports.isOmniCollectionRecordKind)(sourceKind);
15
+ exports.isCanonicalCollectionMembershipRelation = isCanonicalCollectionMembershipRelation;
16
+ const isAllowedOmniRelation = ({ sourceKind, targetKind, relationKind, }) => {
17
+ if (!hasKind(sourceKind) || !hasKind(targetKind)) {
18
+ return false;
19
+ }
20
+ switch (relationKind) {
21
+ case omni_relation_kind_enum_js_1.OmniRelationKind.Contains:
22
+ return (0, exports.isOmniCollectionRecordKind)(sourceKind);
23
+ case omni_relation_kind_enum_js_1.OmniRelationKind.DerivedFrom:
24
+ return ((0, exports.isOmniDocumentRecordKind)(sourceKind) &&
25
+ (0, exports.isOmniDocumentRecordKind)(targetKind));
26
+ case omni_relation_kind_enum_js_1.OmniRelationKind.References:
27
+ case omni_relation_kind_enum_js_1.OmniRelationKind.RelatedTo:
28
+ return true;
29
+ default:
30
+ return false;
31
+ }
32
+ };
33
+ exports.isAllowedOmniRelation = isAllowedOmniRelation;
34
+ //# sourceMappingURL=omni-relation-semantics.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"omni-relation-semantics.js","sourceRoot":"","sources":["omni-relation-semantics.ts"],"names":[],"mappings":";;;AAAA,iFAAoE;AACpE,6EAAgE;AAChE,6EAAgE;AAQnD,QAAA,wCAAwC,GACnD,6CAAgB,CAAC,QAAQ,CAAC;AAE5B,MAAM,OAAO,GAAG,CAAC,IAAY,EAAW,EAAE,CAAC,IAAI,CAAC,IAAI,EAAE,CAAC,MAAM,GAAG,CAAC,CAAC;AAE3D,MAAM,0BAA0B,GAAG,CAAC,IAAY,EAAW,EAAE,CAClE,IAAI,KAAK,iDAAkB,CAAC,UAAU,CAAC;AAD5B,QAAA,0BAA0B,8BACE;AAElC,MAAM,wBAAwB,GAAG,CAAC,IAAY,EAAW,EAAE,CAChE,IAAI,KAAK,6CAAgB,CAAC,QAAQ,CAAC;AADxB,QAAA,wBAAwB,4BACA;AAE9B,MAAM,uCAAuC,GAAG,CAAC,EACtD,UAAU,EACV,YAAY,GACoD,EAAW,EAAE,CAC7E,YAAY,KAAK,gDAAwC;IACzD,IAAA,kCAA0B,EAAC,UAAU,CAAC,CAAC;AAL5B,QAAA,uCAAuC,2CAKX;AAElC,MAAM,qBAAqB,GAAG,CAAC,EACpC,UAAU,EACV,UAAU,EACV,YAAY,GACe,EAAW,EAAE;IACxC,IAAI,CAAC,OAAO,CAAC,UAAU,CAAC,IAAI,CAAC,OAAO,CAAC,UAAU,CAAC,EAAE,CAAC;QACjD,OAAO,KAAK,CAAC;IACf,CAAC;IAED,QAAQ,YAAY,EAAE,CAAC;QACrB,KAAK,6CAAgB,CAAC,QAAQ;YAC5B,OAAO,IAAA,kCAA0B,EAAC,UAAU,CAAC,CAAC;QAChD,KAAK,6CAAgB,CAAC,WAAW;YAC/B,OAAO,CACL,IAAA,gCAAwB,EAAC,UAAU,CAAC;gBACpC,IAAA,gCAAwB,EAAC,UAAU,CAAC,CACrC,CAAC;QACJ,KAAK,6CAAgB,CAAC,UAAU,CAAC;QACjC,KAAK,6CAAgB,CAAC,SAAS;YAC7B,OAAO,IAAI,CAAC;QACd;YACE,OAAO,KAAK,CAAC;IACjB,CAAC;AACH,CAAC,CAAC;AAvBW,QAAA,qBAAqB,yBAuBhC"}