@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,518 @@
1
+ import { afterEach, describe, expect, it } from "@jest/globals";
2
+ import {
3
+ compileProjectionUniqueConstraintPredicate,
4
+ createProjectionDialect,
5
+ } from "@nest-yalc-2/crud-gen";
6
+ import { DataSource } from "typeorm";
7
+
8
+ import { OmniNamedEntity } from '../base/omni-named.entity.js';
9
+ import { OmniRecordEntity } from '../base/omni-record.entity.js';
10
+ import { OmniRelationEntity } from '../base/omni-relation.entity.js';
11
+ import { OmniExternalRefEntity } from '../base/omni-external-ref.entity.js';
12
+ import { OmniCollectionEntity } from '../omni-collection.entity.js';
13
+ import { OmniDocumentEntity } from '../omni-document.entity.js';
14
+ import { OmniRecordStatus } from '../omni-record-status.enum.js';
15
+ import {
16
+ createOmniExtensionProjectionEntity,
17
+ createOmniExtensionProjectionTable,
18
+ defineOmniExtensionProjection,
19
+ } from '../omni-extension-projection.definition.js';
20
+ import { captureOmniMigrationSnapshot } from '../omni-migration.js';
21
+
22
+ const constraintName = "projection_status_initial_kind_unique";
23
+ const fullConstraintName = "projection_status_kind_state_unique";
24
+ const referenceName = "projection_status_parent_fk";
25
+ const tableName = "projection-status-constraint-probe";
26
+ const alphaScope = "scope-alpha";
27
+ const bravoScope = "scope-bravo";
28
+
29
+ type ConstraintRow = {
30
+ scopeId: string;
31
+ guid: string;
32
+ parentGuid: string;
33
+ key: string;
34
+ kind: string;
35
+ state: string;
36
+ isInitial: boolean;
37
+ payload: Record<string, unknown>;
38
+ };
39
+
40
+ function definitionInput() {
41
+ return {
42
+ id: "projection.status.constraint-probe.v1",
43
+ tableName,
44
+ identity: { column: "guid", uniqueWithinScope: true as const },
45
+ scope: { column: "scopeId", serverOwned: true as const },
46
+ revision: { column: "revision" },
47
+ payload: { column: "payload", allowCreate: true },
48
+ deletion: "hard" as const,
49
+ fields: [
50
+ {
51
+ name: "guid",
52
+ storage: "column" as const,
53
+ column: "guid",
54
+ codec: "uuid" as const,
55
+ nullable: false,
56
+ requiredOnCreate: true,
57
+ },
58
+ {
59
+ name: "parentGuid",
60
+ storage: "column" as const,
61
+ column: "parentGuid",
62
+ codec: "uuid" as const,
63
+ nullable: false,
64
+ requiredOnCreate: true,
65
+ },
66
+ {
67
+ name: "key",
68
+ storage: "column" as const,
69
+ column: "key",
70
+ codec: "string" as const,
71
+ nullable: false,
72
+ requiredOnCreate: true,
73
+ },
74
+ {
75
+ name: "kind",
76
+ storage: "column" as const,
77
+ column: "kind",
78
+ codec: "string" as const,
79
+ nullable: false,
80
+ requiredOnCreate: true,
81
+ },
82
+ {
83
+ name: "state",
84
+ storage: "column" as const,
85
+ column: "state",
86
+ codec: "string" as const,
87
+ nullable: false,
88
+ requiredOnCreate: true,
89
+ },
90
+ {
91
+ name: "isInitial",
92
+ storage: "column" as const,
93
+ column: "isInitial",
94
+ codec: "boolean" as const,
95
+ nullable: false,
96
+ requiredOnCreate: true,
97
+ },
98
+ {
99
+ name: "metadata",
100
+ storage: "json" as const,
101
+ path: ["metadata"],
102
+ codec: "json" as const,
103
+ nullable: true,
104
+ },
105
+ ],
106
+ references: [
107
+ {
108
+ name: referenceName,
109
+ fields: ["parentGuid"],
110
+ target: {
111
+ tableName: "omni-record",
112
+ scopeColumn: "scopeId",
113
+ identityColumns: ["guid"],
114
+ },
115
+ onDelete: "RESTRICT" as const,
116
+ },
117
+ ],
118
+ uniqueConstraints: [
119
+ {
120
+ name: constraintName,
121
+ fields: ["kind"],
122
+ predicate: { isInitial: true, state: "active" },
123
+ },
124
+ {
125
+ name: fullConstraintName,
126
+ fields: ["key", "kind"],
127
+ },
128
+ ],
129
+ owner: {
130
+ kind: "projection_status_constraint_probe",
131
+ title: "Projection status constraint probe",
132
+ status: OmniRecordStatus.Active,
133
+ schema: { id: "projection.status.constraint-probe.v1", version: 1 },
134
+ },
135
+ };
136
+ }
137
+
138
+ const definition = defineOmniExtensionProjection(definitionInput());
139
+
140
+ function expectInvalidDefinition(mutate: (input: any) => void): void {
141
+ const input = definitionInput();
142
+ mutate(input);
143
+ expect(() => defineOmniExtensionProjection(input)).toThrow(TypeError);
144
+ }
145
+
146
+ function expectConstraintMetadata(
147
+ table: ReturnType<typeof createOmniExtensionProjectionTable>,
148
+ dialect: "sqlite" | "postgres",
149
+ ): void {
150
+ const reference = table.foreignKeys.find(
151
+ (foreignKey) => foreignKey.name === referenceName,
152
+ );
153
+ expect(reference).toMatchObject({
154
+ columnNames: ["scopeId", "parentGuid"],
155
+ referencedTableName: "omni-record",
156
+ referencedColumnNames: ["scopeId", "guid"],
157
+ onDelete: "RESTRICT",
158
+ });
159
+ expect(table.indices).toEqual(
160
+ expect.arrayContaining([
161
+ expect.objectContaining({
162
+ name: `${referenceName}_idx`,
163
+ columnNames: ["scopeId", "parentGuid"],
164
+ isUnique: false,
165
+ }),
166
+ expect.objectContaining({
167
+ name: constraintName,
168
+ columnNames: ["scopeId", "kind"],
169
+ isUnique: true,
170
+ where:
171
+ dialect === "sqlite"
172
+ ? '"isInitial" = 1 AND "state" = \'active\''
173
+ : '"isInitial" = TRUE AND "state" = \'active\'',
174
+ }),
175
+ expect.objectContaining({
176
+ name: fullConstraintName,
177
+ columnNames: ["scopeId", "key", "kind"],
178
+ isUnique: true,
179
+ where: "",
180
+ }),
181
+ ]),
182
+ );
183
+ }
184
+
185
+ describe("Omni extension projection declarative constraints", () => {
186
+ it("deep-freezes valid metadata and rejects unsafe declarative constraints", () => {
187
+ expect(Object.isFrozen(definition)).toBe(true);
188
+ expect(Object.isFrozen(definition.references)).toBe(true);
189
+ expect(Object.isFrozen(definition.references![0]!)).toBe(true);
190
+ expect(Object.isFrozen(definition.uniqueConstraints)).toBe(true);
191
+ expect(Object.isFrozen(definition.uniqueConstraints![0]!.predicate)).toBe(
192
+ true,
193
+ );
194
+
195
+ expectInvalidDefinition((input) => {
196
+ input.references[0].fields = ["metadata"];
197
+ });
198
+ expectInvalidDefinition((input) => {
199
+ input.uniqueConstraints[0].predicate = { metadata: "not portable" };
200
+ });
201
+ expectInvalidDefinition((input) => {
202
+ input.references[0].fields = ["missing"];
203
+ });
204
+ expectInvalidDefinition((input) => {
205
+ input.references[0].fields = ["guid"];
206
+ });
207
+ expectInvalidDefinition((input) => {
208
+ input.uniqueConstraints[0].fields = ["scopeId"];
209
+ });
210
+ expectInvalidDefinition((input) => {
211
+ input.references.push({ ...input.references[0] });
212
+ });
213
+ expectInvalidDefinition((input) => {
214
+ input.uniqueConstraints[0].name = referenceName;
215
+ });
216
+ expectInvalidDefinition((input) => {
217
+ input.references[0].target.identityColumns = ["guid", "kind"];
218
+ });
219
+ expectInvalidDefinition((input) => {
220
+ input.references[0].onDelete = "CASCADE";
221
+ });
222
+ expectInvalidDefinition((input) => {
223
+ input.references[0].name = "unsafe;reference";
224
+ });
225
+ });
226
+
227
+ it("compiles portable predicate literals and Table metadata for both dialects", () => {
228
+ const uniqueConstraint = definition.uniqueConstraints![0]!;
229
+ expect(
230
+ compileProjectionUniqueConstraintPredicate(
231
+ definition,
232
+ uniqueConstraint,
233
+ "sqlite",
234
+ ),
235
+ ).toBe('"isInitial" = 1 AND "state" = \'active\'');
236
+ expect(
237
+ compileProjectionUniqueConstraintPredicate(
238
+ definition,
239
+ uniqueConstraint,
240
+ "postgres",
241
+ ),
242
+ ).toBe('"isInitial" = TRUE AND "state" = \'active\'');
243
+ expect(
244
+ compileProjectionUniqueConstraintPredicate(
245
+ definition,
246
+ definition.uniqueConstraints![1]!,
247
+ "sqlite",
248
+ ),
249
+ ).toBeUndefined();
250
+
251
+ const sqliteTable = createOmniExtensionProjectionTable(
252
+ definition,
253
+ createProjectionDialect("sqlite"),
254
+ );
255
+ const postgresTable = createOmniExtensionProjectionTable(
256
+ definition,
257
+ createProjectionDialect("postgres"),
258
+ );
259
+ expect(sqliteTable.findColumnByName("payload")?.type).toBe("text");
260
+ expect(postgresTable.findColumnByName("payload")?.type).toBe("jsonb");
261
+ expectConstraintMetadata(sqliteTable, "sqlite");
262
+ expectConstraintMetadata(postgresTable, "postgres");
263
+ });
264
+ });
265
+
266
+ describe("Omni extension projection constraints in SQLite", () => {
267
+ let dataSource: DataSource;
268
+
269
+ afterEach(async () => {
270
+ if (dataSource?.isInitialized) await dataSource.destroy();
271
+ });
272
+
273
+ it("enforces same-scope references and partial uniqueness in live metadata and snapshots", async () => {
274
+ const entity = createOmniExtensionProjectionEntity<ConstraintRow>(
275
+ definition,
276
+ createProjectionDialect("sqlite"),
277
+ );
278
+ dataSource = new DataSource({
279
+ type: "sqlite",
280
+ database: ":memory:",
281
+ dropSchema: true,
282
+ synchronize: true,
283
+ entities: [
284
+ OmniNamedEntity,
285
+ OmniRecordEntity,
286
+ OmniDocumentEntity,
287
+ OmniCollectionEntity,
288
+ OmniRelationEntity,
289
+ OmniExternalRefEntity,
290
+ entity,
291
+ ],
292
+ });
293
+ await dataSource.initialize();
294
+
295
+ const metadata = dataSource.getMetadata(entity);
296
+ expect(metadata.foreignKeys).toEqual(
297
+ expect.arrayContaining([
298
+ expect.objectContaining({
299
+ name: referenceName,
300
+ columnNames: ["scopeId", "parentGuid"],
301
+ referencedTablePath: "omni-record",
302
+ referencedColumnNames: ["scopeId", "guid"],
303
+ onDelete: "RESTRICT",
304
+ }),
305
+ ]),
306
+ );
307
+ expect(metadata.indices).toEqual(
308
+ expect.arrayContaining([
309
+ expect.objectContaining({
310
+ givenName: `${referenceName}_idx`,
311
+ where: undefined,
312
+ isUnique: false,
313
+ }),
314
+ expect.objectContaining({
315
+ givenName: constraintName,
316
+ where: '"isInitial" = 1 AND "state" = \'active\'',
317
+ isUnique: true,
318
+ }),
319
+ ]),
320
+ );
321
+
322
+ const owners = dataSource.getRepository(OmniRecordEntity);
323
+ const extensions = dataSource.getRepository<ConstraintRow>(entity);
324
+ const parentGuid = "10000000-0000-4000-8000-000000000001";
325
+ const crossScopeParentGuid = "10000000-0000-4000-8000-000000000002";
326
+ const ownerGuid = "10000000-0000-4000-8000-000000000003";
327
+ const duplicateOwnerGuid = "10000000-0000-4000-8000-000000000004";
328
+ const excludedOwnerGuid = "10000000-0000-4000-8000-000000000005";
329
+ const missingParentOwnerGuid = "10000000-0000-4000-8000-000000000006";
330
+ const crossScopeOwnerGuid = "10000000-0000-4000-8000-000000000007";
331
+
332
+ const createOwner = async (
333
+ scopeId: string,
334
+ guid: string,
335
+ ): Promise<void> => {
336
+ await owners.insert({
337
+ scopeId,
338
+ guid,
339
+ title: `Owner ${guid}`,
340
+ kind: "projection_status_constraint_probe",
341
+ status: OmniRecordStatus.Active,
342
+ payloadSchemaId: definition.owner.schema.id,
343
+ payloadSchemaVersion: definition.owner.schema.version,
344
+ });
345
+ };
346
+ await createOwner(alphaScope, parentGuid);
347
+ await createOwner(bravoScope, crossScopeParentGuid);
348
+ await createOwner(alphaScope, ownerGuid);
349
+ await createOwner(alphaScope, duplicateOwnerGuid);
350
+ await createOwner(alphaScope, excludedOwnerGuid);
351
+ await createOwner(alphaScope, missingParentOwnerGuid);
352
+ await createOwner(alphaScope, crossScopeOwnerGuid);
353
+
354
+ await extensions.insert({
355
+ scopeId: alphaScope,
356
+ guid: ownerGuid,
357
+ parentGuid,
358
+ key: "primary",
359
+ kind: "approval",
360
+ state: "active",
361
+ isInitial: true,
362
+ payload: {},
363
+ });
364
+ await expect(
365
+ extensions.insert({
366
+ scopeId: alphaScope,
367
+ guid: missingParentOwnerGuid,
368
+ parentGuid: "10000000-0000-4000-8000-000000000099",
369
+ key: "missing-parent",
370
+ kind: "missing-parent",
371
+ state: "active",
372
+ isInitial: false,
373
+ payload: {},
374
+ }),
375
+ ).rejects.toThrow();
376
+ await expect(
377
+ extensions.insert({
378
+ scopeId: alphaScope,
379
+ guid: crossScopeOwnerGuid,
380
+ parentGuid: crossScopeParentGuid,
381
+ key: "cross-scope",
382
+ kind: "cross-scope-parent",
383
+ state: "active",
384
+ isInitial: false,
385
+ payload: {},
386
+ }),
387
+ ).rejects.toThrow();
388
+ await expect(
389
+ extensions.insert({
390
+ scopeId: alphaScope,
391
+ guid: duplicateOwnerGuid,
392
+ parentGuid,
393
+ key: "duplicate-partial",
394
+ kind: "approval",
395
+ state: "active",
396
+ isInitial: true,
397
+ payload: {},
398
+ }),
399
+ ).rejects.toThrow();
400
+ await extensions.insert({
401
+ scopeId: alphaScope,
402
+ guid: excludedOwnerGuid,
403
+ parentGuid,
404
+ key: "inactive",
405
+ kind: "approval",
406
+ state: "active",
407
+ isInitial: false,
408
+ payload: {},
409
+ });
410
+ await expect(
411
+ owners.delete({ scopeId: alphaScope, guid: parentGuid }),
412
+ ).rejects.toThrow();
413
+
414
+ const snapshot = captureOmniMigrationSnapshot(
415
+ "projection-status-constraint-probe-sqlite-v1",
416
+ dataSource,
417
+ [{ entities: [entity], definition }],
418
+ );
419
+ const capturedTable = snapshot.tables.find(
420
+ (table) => table.name === tableName,
421
+ )!;
422
+ expect(capturedTable.foreignKeys).toEqual(
423
+ expect.arrayContaining([
424
+ expect.objectContaining({
425
+ name: referenceName,
426
+ columnNames: ["scopeId", "parentGuid"],
427
+ referencedTableName: "omni-record",
428
+ referencedColumnNames: ["scopeId", "guid"],
429
+ onDelete: "RESTRICT",
430
+ }),
431
+ ]),
432
+ );
433
+ expect(capturedTable.indices).toEqual(
434
+ expect.arrayContaining([
435
+ expect.objectContaining({
436
+ name: `${referenceName}_idx`,
437
+ columnNames: ["scopeId", "parentGuid"],
438
+ isUnique: false,
439
+ }),
440
+ expect.objectContaining({
441
+ name: constraintName,
442
+ columnNames: ["scopeId", "kind"],
443
+ isUnique: true,
444
+ where: '"isInitial" = 1 AND "state" = \'active\'',
445
+ }),
446
+ ]),
447
+ );
448
+ });
449
+ });
450
+
451
+ const postgresUrl = process.env.OMNI_EXTENSION_PROJECTION_POSTGRES_URL;
452
+ const postgresIt = postgresUrl ? it : it.skip;
453
+
454
+ describe("Omni extension projection constraints in PostgreSQL", () => {
455
+ postgresIt(
456
+ "initializes an ephemeral database with PostgreSQL metadata",
457
+ async () => {
458
+ const entity = createOmniExtensionProjectionEntity<ConstraintRow>(
459
+ definition,
460
+ createProjectionDialect("postgres"),
461
+ );
462
+ const dataSource = new DataSource({
463
+ type: "postgres",
464
+ url: postgresUrl!,
465
+ ssl: false,
466
+ dropSchema: true,
467
+ synchronize: true,
468
+ entities: [
469
+ OmniNamedEntity,
470
+ OmniRecordEntity,
471
+ OmniDocumentEntity,
472
+ OmniCollectionEntity,
473
+ OmniRelationEntity,
474
+ OmniExternalRefEntity,
475
+ entity,
476
+ ],
477
+ });
478
+ try {
479
+ await dataSource.initialize();
480
+ const metadata = dataSource.getMetadata(entity);
481
+ expect(metadata.indices).toEqual(
482
+ expect.arrayContaining([
483
+ expect.objectContaining({
484
+ givenName: constraintName,
485
+ where: '"isInitial" = TRUE AND "state" = \'active\'',
486
+ isUnique: true,
487
+ }),
488
+ ]),
489
+ );
490
+ const columns = await dataSource.query(
491
+ "SELECT column_name, data_type FROM information_schema.columns WHERE table_name = $1 AND column_name IN ($2, $3)",
492
+ [tableName, "payload", "isInitial"],
493
+ );
494
+ expect(columns).toEqual(
495
+ expect.arrayContaining([
496
+ expect.objectContaining({
497
+ column_name: "payload",
498
+ data_type: "jsonb",
499
+ }),
500
+ expect.objectContaining({
501
+ column_name: "isInitial",
502
+ data_type: "boolean",
503
+ }),
504
+ ]),
505
+ );
506
+ const indexes = await dataSource.query(
507
+ "SELECT indexdef FROM pg_indexes WHERE tablename = $1 AND indexname = $2",
508
+ [tableName, constraintName],
509
+ );
510
+ expect(indexes[0]?.indexdef).toContain("WHERE");
511
+ expect(indexes[0]?.indexdef).toContain('"isInitial"');
512
+ expect(indexes[0]?.indexdef).toContain("state");
513
+ } finally {
514
+ if (dataSource.isInitialized) await dataSource.destroy();
515
+ }
516
+ },
517
+ );
518
+ });