@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,946 @@
1
+ import { Scope, type INestApplication } from '@nestjs/common';
2
+ import { afterAll, afterEach, beforeAll, describe, expect, it, jest } from '@jest/globals';
3
+ import { ApolloDriver, type ApolloDriverConfig } from '@nestjs/apollo';
4
+ import { EventEmitterModule } from '@nestjs/event-emitter';
5
+ import { GraphQLModule, GraphQLSchemaHost } from '@nestjs/graphql';
6
+ import { ModuleRef } from '@nestjs/core';
7
+ import { getDataSourceToken, TypeOrmModule } from '@nestjs/typeorm';
8
+ import { Test } from '@nestjs/testing';
9
+ import {
10
+ ConflictException,
11
+ NotFoundException,
12
+ } from '@nestjs/common';
13
+ import {
14
+ createProjectionDialect,
15
+ CrudGenResourceFactory,
16
+ } from "@nest-yalc-2/crud-gen";
17
+ import { EventModule } from '@nest-yalc-2/event-manager';
18
+ import { UUIDScalar } from '@nest-yalc-2/graphql/scalars/uuid.scalar.js';
19
+ import request from 'supertest';
20
+ import { DataSource } from "typeorm";
21
+ import type { OmniMigrationRunner } from '../omni-migration.js';
22
+
23
+ import { OmniNamedEntity } from "../base/omni-named.entity.js";
24
+ import { OmniRecordEntity } from "../base/omni-record.entity.js";
25
+ import { OmniRelationEntity } from "../base/omni-relation.entity.js";
26
+ import { OmniExternalRefEntity } from "../base/omni-external-ref.entity.js";
27
+ import { OmniCollectionEntity } from "../omni-collection.entity.js";
28
+ import { OmniDocumentEntity } from "../omni-document.entity.js";
29
+ import { OmniRecordStatus } from "../omni-record-status.enum.js";
30
+ import { OmniKernelModule } from '../omnikernel.module.js';
31
+ import {
32
+ OmniRecordCondition,
33
+ OmniRecordCreateInput,
34
+ OmniRecordType,
35
+ OmniRecordUpdateInput,
36
+ } from '../omni-record.dto.js';
37
+ import {
38
+ createOmniExtensionProjectionEntity,
39
+ defineOmniExtensionProjection,
40
+ } from "../omni-extension-projection.definition.js";
41
+ import { createOmniExtensionProjectionRegistration } from "../omni-extension-projection.resource.js";
42
+ import { defineOmniRelationProjection } from '../omni-relation-projection.definition.js';
43
+ import { createOmniRelationProjectionRegistration } from '../omni-relation-projection.resource.js';
44
+ import {
45
+ captureOmniMigrationSnapshot,
46
+ createOmniMigrationPlan,
47
+ defineOmniMigrationSnapshot,
48
+ } from "../omni-migration.js";
49
+ import { createOmniProjectionReaderCatalogProvider } from '../omni-projection.catalog.js';
50
+
51
+ type ExtensionRow = {
52
+ scopeId: string;
53
+ guid: string;
54
+ label: string;
55
+ payload: Record<string, unknown>;
56
+ };
57
+
58
+ class ExampleExtensionApi {
59
+ guid!: string;
60
+ label!: string;
61
+ enabled!: boolean;
62
+ revision!: number;
63
+ payload!: Record<string, unknown>;
64
+ }
65
+
66
+ class ExampleLinkApi {
67
+ guid!: string;
68
+ sourceRecordId!: string;
69
+ targetRecordId!: string;
70
+ revision!: number;
71
+ payload!: Record<string, unknown>;
72
+ }
73
+
74
+ const rawRecordResource = CrudGenResourceFactory<typeof OmniRecordEntity>({
75
+ entityModel: OmniRecordEntity,
76
+ backend: false,
77
+ graphql: {
78
+ resolver: {
79
+ dto: OmniRecordType,
80
+ input: {
81
+ create: OmniRecordCreateInput,
82
+ update: OmniRecordUpdateInput,
83
+ conditions: OmniRecordCondition,
84
+ },
85
+ prefix: 'Raw_',
86
+ },
87
+ },
88
+ rest: {
89
+ dto: OmniRecordType,
90
+ path: 'raw-records',
91
+ idField: 'guid',
92
+ },
93
+ });
94
+
95
+ const definition = defineOmniExtensionProjection({
96
+ id: "example.nest-extension.v1",
97
+ tableName: "example-nest-extension",
98
+ identity: { column: "guid", uniqueWithinScope: true },
99
+ scope: { column: "scopeId", serverOwned: true },
100
+ revision: { column: "revision" },
101
+ payload: { column: "payload", allowCreate: true },
102
+ deletion: "hard",
103
+ fields: [
104
+ {
105
+ name: "guid",
106
+ storage: "column",
107
+ column: "guid",
108
+ codec: "uuid",
109
+ nullable: false,
110
+ requiredOnCreate: true,
111
+ },
112
+ {
113
+ name: "label",
114
+ storage: "column",
115
+ column: "label",
116
+ codec: "string",
117
+ nullable: false,
118
+ requiredOnCreate: true,
119
+ query: { filter: ["eq"], sort: true },
120
+ },
121
+ {
122
+ name: "enabled",
123
+ storage: "json",
124
+ path: ["state", "enabled"],
125
+ codec: "boolean",
126
+ nullable: false,
127
+ requiredOnCreate: true,
128
+ index: { name: "example_nest_extension_enabled_idx" },
129
+ query: { filter: ["eq"], sort: true },
130
+ },
131
+ ],
132
+ owner: {
133
+ kind: "example_nest_extension",
134
+ title: "Example Nest extension",
135
+ status: OmniRecordStatus.Active,
136
+ schema: { id: "example.nest-extension.v1", version: 1 },
137
+ },
138
+ });
139
+
140
+ const relationDefinition = defineOmniRelationProjection({
141
+ id: 'example.nest-extension-link.v1',
142
+ relation: {
143
+ allowedKinds: ['example_link', 'example_blocks'],
144
+ sourceKind: definition.owner.kind,
145
+ targetKind: definition.owner.kind,
146
+ schema: { id: 'example.nest-extension-link.v1', version: 1 },
147
+ },
148
+ aliases: {
149
+ kind: 'linkKind',
150
+ source: 'sourceWorkItemId',
151
+ target: 'targetWorkItemId',
152
+ payload: 'metadata',
153
+ },
154
+ });
155
+
156
+ const scope = {
157
+ scopeId: "scope-registration",
158
+ cacheKey: (key: string) => `scope-registration:${key}`,
159
+ };
160
+
161
+ const events = {
162
+ errorBadRequest: (_event: string, options: any) =>
163
+ new ConflictException(options.response.message),
164
+ errorConflict: (_event: string, options: any) =>
165
+ new ConflictException(options.response.message),
166
+ errorNotFound: (_event: string, options: any) =>
167
+ new NotFoundException(options.response.message),
168
+ };
169
+
170
+ const lifecycleToken = Symbol('example-projection-lifecycle');
171
+ const catalogToken = Symbol('example-projection-catalog');
172
+ const moduleRefToken = Symbol('example-consumer-module-ref');
173
+ const lifecycleBeforeCreate = jest.fn();
174
+ const lifecycleBeforeUpdate = jest.fn();
175
+ const lifecycleBeforeDelete = jest.fn();
176
+
177
+ const postgresUrl = process.env.OMNI_EXTENSION_PROJECTION_POSTGRES_URL;
178
+ const dialectCases: Array<{ name: "sqlite" | "postgres"; url?: string }> = [
179
+ { name: "sqlite" },
180
+ ...(postgresUrl ? [{ name: "postgres" as const, url: postgresUrl }] : []),
181
+ ];
182
+
183
+ describe("Omni extension projection registration", () => {
184
+ it("returns generated CrudGen REST and GraphQL surfaces with request-scoped providers", () => {
185
+ const entity = createOmniExtensionProjectionEntity<ExtensionRow>(
186
+ definition,
187
+ createProjectionDialect("sqlite"),
188
+ );
189
+ const registration = createOmniExtensionProjectionRegistration({
190
+ entity,
191
+ apiModel: ExampleExtensionApi,
192
+ definition,
193
+ dbConnection: "default",
194
+ rest: { path: "example-extensions" },
195
+ });
196
+
197
+ expect(Object.isFrozen(registration)).toBe(true);
198
+ expect(registration.definition).toBe(definition);
199
+ expect(registration.entities).toEqual([entity]);
200
+ expect(registration.reservedRecordKinds).toEqual([definition.owner.kind]);
201
+ expect(registration.controllers).toHaveLength(1);
202
+ expect(registration.resource.providers).toHaveLength(1);
203
+ expect(registration.providers).toHaveLength(3);
204
+ expect(
205
+ registration.providers.filter(
206
+ (provider) =>
207
+ typeof provider === 'object' &&
208
+ provider !== null &&
209
+ 'provide' in provider &&
210
+ provider.provide === registration.serviceToken,
211
+ ),
212
+ ).toHaveLength(1);
213
+ expect(
214
+ registration.providers.filter(
215
+ (provider) =>
216
+ typeof provider === 'object' &&
217
+ provider !== null &&
218
+ 'provide' in provider &&
219
+ provider.provide === registration.dataLoaderToken,
220
+ ),
221
+ ).toHaveLength(1);
222
+
223
+ const relationRegistration = createOmniRelationProjectionRegistration({
224
+ apiModel: ExampleLinkApi,
225
+ definition: relationDefinition,
226
+ dbConnection: 'default',
227
+ rest: { path: 'example-links' },
228
+ });
229
+ expect(Object.isFrozen(relationRegistration)).toBe(true);
230
+ expect(relationRegistration.entities).toEqual([]);
231
+ expect(relationRegistration.relationKinds).toEqual([
232
+ 'example_link',
233
+ 'example_blocks',
234
+ ]);
235
+ expect(relationRegistration.controllers).toHaveLength(1);
236
+ expect(relationRegistration.providers).toHaveLength(3);
237
+ });
238
+ });
239
+
240
+ describe('Omni extension projection generated transport', () => {
241
+ let app: INestApplication;
242
+ let dataSource: DataSource;
243
+ const restGuid = '10000000-0000-4000-8000-000000000201';
244
+ const graphqlGuid = '10000000-0000-4000-8000-000000000202';
245
+ const relationGuid = '10000000-0000-4000-8000-000000000206';
246
+ const normalRecordGuid = '10000000-0000-4000-8000-000000000203';
247
+ const rejectedRecordGuid = '10000000-0000-4000-8000-000000000204';
248
+
249
+ beforeAll(async () => {
250
+ const entity = createOmniExtensionProjectionEntity<ExtensionRow>(
251
+ definition,
252
+ createProjectionDialect('sqlite'),
253
+ );
254
+ const registration = createOmniExtensionProjectionRegistration({
255
+ entity,
256
+ apiModel: ExampleExtensionApi,
257
+ definition,
258
+ dbConnection: 'default',
259
+ catalog: { token: catalogToken },
260
+ lifecycle: { token: lifecycleToken },
261
+ moduleRefToken,
262
+ rest: { path: 'example-extensions' },
263
+ graphql: {
264
+ names: {
265
+ object: 'TransportExampleExtensionApi',
266
+ create: 'TransportExampleExtensionApiCreateInput',
267
+ patch: 'TransportExampleExtensionApiUpdateInput',
268
+ conditions: 'TransportExampleExtensionApiCondition',
269
+ },
270
+ },
271
+ });
272
+ const relationRegistration = createOmniRelationProjectionRegistration({
273
+ apiModel: ExampleLinkApi,
274
+ definition: relationDefinition,
275
+ dbConnection: 'default',
276
+ catalog: { token: catalogToken },
277
+ lifecycle: { token: lifecycleToken },
278
+ moduleRefToken,
279
+ rest: { path: 'example-links' },
280
+ graphql: {
281
+ names: {
282
+ object: 'TransportExampleLinkApi',
283
+ create: 'TransportExampleLinkApiCreateInput',
284
+ patch: 'TransportExampleLinkApiUpdateInput',
285
+ conditions: 'TransportExampleLinkApiCondition',
286
+ },
287
+ },
288
+ });
289
+ const moduleRef = await Test.createTestingModule({
290
+ imports: [
291
+ GraphQLModule.forRoot<ApolloDriverConfig>({
292
+ driver: ApolloDriver,
293
+ autoSchemaFile: true,
294
+ path: '/graphql',
295
+ }),
296
+ EventEmitterModule.forRoot(),
297
+ EventModule.forRootAsync(),
298
+ TypeOrmModule.forRoot({
299
+ type: 'sqlite',
300
+ database: ':memory:',
301
+ synchronize: false,
302
+ autoLoadEntities: true,
303
+ }),
304
+ OmniKernelModule.register({
305
+ dbConnection: 'default',
306
+ defaultScopeId: scope.scopeId,
307
+ reservedRecordKinds: registration.reservedRecordKinds,
308
+ relationKinds: relationRegistration.relationKinds,
309
+ }),
310
+ TypeOrmModule.forFeature(registration.entities),
311
+ ],
312
+ controllers: [
313
+ ...registration.controllers,
314
+ ...relationRegistration.controllers,
315
+ ...rawRecordResource.controllers,
316
+ ],
317
+ providers: [
318
+ UUIDScalar,
319
+ { provide: moduleRefToken, useExisting: ModuleRef },
320
+ createOmniProjectionReaderCatalogProvider(
321
+ [registration, relationRegistration],
322
+ catalogToken,
323
+ ),
324
+ {
325
+ provide: lifecycleToken,
326
+ scope: Scope.REQUEST,
327
+ useFactory: () => ({
328
+ beforeCreate: async (context: any) => {
329
+ lifecycleBeforeCreate(context);
330
+ await context.readers.extension(definition.id).list({
331
+ where: { label: '__missing__' },
332
+ take: 1,
333
+ });
334
+ },
335
+ beforeUpdate: async (context: any) => {
336
+ lifecycleBeforeUpdate(context);
337
+ },
338
+ beforeDelete: async (context: any) => {
339
+ lifecycleBeforeDelete(context);
340
+ },
341
+ }),
342
+ },
343
+ ...registration.providers,
344
+ ...relationRegistration.providers,
345
+ ...rawRecordResource.providers,
346
+ ],
347
+ }).compile();
348
+
349
+ dataSource = moduleRef.get<DataSource>(getDataSourceToken());
350
+ const snapshot = captureOmniMigrationSnapshot(
351
+ 'example-nest-extension-transport-v1',
352
+ dataSource,
353
+ [registration],
354
+ );
355
+ const migration = createOmniMigrationPlan(snapshot);
356
+ const queryRunner = dataSource.createQueryRunner();
357
+ const migrationRunner: OmniMigrationRunner = queryRunner;
358
+ await queryRunner.connect();
359
+ try {
360
+ await migration.create(migrationRunner);
361
+ } finally {
362
+ await queryRunner.release();
363
+ }
364
+
365
+ app = moduleRef.createNestApplication();
366
+ await app.init();
367
+ });
368
+
369
+ afterAll(async () => {
370
+ await app?.close();
371
+ });
372
+
373
+ it('boots with EventModule, a consumer ModuleRef token, and generated CRUD', async () => {
374
+ const schema = app.get(GraphQLSchemaHost).schema;
375
+ const queryFields = Object.keys(schema.getQueryType()!.getFields());
376
+ const mutationFields = Object.keys(schema.getMutationType()!.getFields());
377
+ expect(queryFields).toEqual(
378
+ expect.arrayContaining([
379
+ 'getExampleExtensionApi',
380
+ 'getExampleExtensionApiGrid',
381
+ 'getExampleLinkApi',
382
+ 'getExampleLinkApiGrid',
383
+ ]),
384
+ );
385
+ expect(mutationFields).toEqual(
386
+ expect.arrayContaining([
387
+ 'createExampleExtensionApi',
388
+ 'updateExampleExtensionApi',
389
+ 'deleteExampleExtensionApi',
390
+ 'createExampleLinkApi',
391
+ 'updateExampleLinkApi',
392
+ 'deleteExampleLinkApi',
393
+ ]),
394
+ );
395
+
396
+ await request(app.getHttpServer())
397
+ .post('/example-extensions')
398
+ .send({ guid: restGuid, label: 'REST extension', enabled: true })
399
+ .expect(201)
400
+ .expect(({ body }) => {
401
+ expect(body).toMatchObject({
402
+ guid: restGuid,
403
+ label: 'REST extension',
404
+ enabled: true,
405
+ revision: 1,
406
+ });
407
+ });
408
+
409
+ await request(app.getHttpServer())
410
+ .get(`/example-extensions/${restGuid}`)
411
+ .expect(200)
412
+ .expect(({ body }) => {
413
+ expect(body.enabled).toBe(true);
414
+ });
415
+ await request(app.getHttpServer())
416
+ .get('/example-extensions')
417
+ .expect(200)
418
+ .expect(({ body }) => {
419
+ expect(body.list).toEqual(
420
+ expect.arrayContaining([expect.objectContaining({ guid: restGuid })]),
421
+ );
422
+ });
423
+ await request(app.getHttpServer())
424
+ .put(`/example-extensions/${restGuid}`)
425
+ .send({ label: 'REST extension patched', enabled: false, expectedRevision: 1 })
426
+ .expect(200)
427
+ .expect(({ body }) => {
428
+ expect(body).toMatchObject({
429
+ guid: restGuid,
430
+ label: 'REST extension patched',
431
+ enabled: false,
432
+ revision: 2,
433
+ });
434
+ });
435
+
436
+ const graphCreate = await request(app.getHttpServer())
437
+ .post('/graphql')
438
+ .send({
439
+ query: `mutation CreateExample($input: TransportExampleExtensionApiCreateInput!) {
440
+ createExampleExtensionApi(input: $input) { guid label enabled revision }
441
+ }`,
442
+ variables: {
443
+ input: {
444
+ guid: graphqlGuid,
445
+ label: 'GraphQL extension',
446
+ enabled: true,
447
+ },
448
+ },
449
+ })
450
+ .expect(200);
451
+ expect(graphCreate.body.errors).toBeUndefined();
452
+ expect(graphCreate.body.data.createExampleExtensionApi).toMatchObject({
453
+ guid: graphqlGuid,
454
+ enabled: true,
455
+ revision: 1,
456
+ });
457
+
458
+ await request(app.getHttpServer())
459
+ .post('/example-links')
460
+ .send({
461
+ guid: relationGuid,
462
+ linkKind: 'example_link',
463
+ sourceWorkItemId: restGuid,
464
+ targetWorkItemId: graphqlGuid,
465
+ metadata: { origin: 'generated REST' },
466
+ })
467
+ .expect(201)
468
+ .expect(({ body }) => {
469
+ expect(body).toMatchObject({
470
+ guid: relationGuid,
471
+ linkKind: 'example_link',
472
+ sourceWorkItemId: restGuid,
473
+ targetWorkItemId: graphqlGuid,
474
+ metadata: { origin: 'generated REST' },
475
+ });
476
+ expect(body.kind).toBeUndefined();
477
+ expect(body.status).toBeUndefined();
478
+ });
479
+
480
+ const graphRead = await request(app.getHttpServer())
481
+ .post('/graphql')
482
+ .send({
483
+ query: `query {
484
+ getExampleExtensionApi(guid: "${restGuid}") { guid label enabled revision }
485
+ getExampleExtensionApiGrid { nodes { guid label enabled } pageData { count } }
486
+ }`,
487
+ })
488
+ .expect(200);
489
+ expect(graphRead.body.errors).toBeUndefined();
490
+ expect(graphRead.body.data.getExampleExtensionApi).toMatchObject({
491
+ guid: restGuid,
492
+ enabled: false,
493
+ revision: 2,
494
+ });
495
+ expect(graphRead.body.data.getExampleExtensionApiGrid.nodes).toEqual(
496
+ expect.arrayContaining([
497
+ expect.objectContaining({ guid: restGuid }),
498
+ expect.objectContaining({ guid: graphqlGuid }),
499
+ ]),
500
+ );
501
+
502
+ const graphRelation = await request(app.getHttpServer())
503
+ .post('/graphql')
504
+ .send({
505
+ query: `query {
506
+ getExampleLinkApi(guid: "${relationGuid}") {
507
+ guid linkKind sourceWorkItemId targetWorkItemId metadata
508
+ }
509
+ getExampleLinkApiGrid { nodes { guid } pageData { count } }
510
+ }`,
511
+ })
512
+ .expect(200);
513
+ expect(graphRelation.body.errors).toBeUndefined();
514
+ expect(graphRelation.body.data.getExampleLinkApi).toMatchObject({
515
+ guid: relationGuid,
516
+ linkKind: 'example_link',
517
+ sourceWorkItemId: restGuid,
518
+ targetWorkItemId: graphqlGuid,
519
+ });
520
+ expect(graphRelation.body.data.getExampleLinkApiGrid.nodes).toEqual(
521
+ expect.arrayContaining([expect.objectContaining({ guid: relationGuid })]),
522
+ );
523
+
524
+ const graphRelationUpdate = await request(app.getHttpServer())
525
+ .post('/graphql')
526
+ .send({
527
+ query: `mutation UpdateLink(
528
+ $conditions: TransportExampleLinkApiCondition!
529
+ $input: TransportExampleLinkApiUpdateInput!
530
+ ) {
531
+ updateExampleLinkApi(conditions: $conditions, input: $input) {
532
+ guid metadata revision
533
+ }
534
+ }`,
535
+ variables: {
536
+ conditions: { guid: relationGuid },
537
+ input: {
538
+ metadata: { origin: 'generated GraphQL' },
539
+ expectedRevision: 1,
540
+ },
541
+ },
542
+ })
543
+ .expect(200);
544
+ expect(graphRelationUpdate.body.errors).toBeUndefined();
545
+ expect(graphRelationUpdate.body.data.updateExampleLinkApi).toMatchObject({
546
+ guid: relationGuid,
547
+ metadata: { origin: 'generated GraphQL' },
548
+ });
549
+
550
+ const graphUpdate = await request(app.getHttpServer())
551
+ .post('/graphql')
552
+ .send({
553
+ query: `mutation UpdateExample(
554
+ $conditions: TransportExampleExtensionApiCondition!
555
+ $input: TransportExampleExtensionApiUpdateInput!
556
+ ) {
557
+ updateExampleExtensionApi(conditions: $conditions, input: $input) {
558
+ guid label enabled revision
559
+ }
560
+ }`,
561
+ variables: {
562
+ conditions: { guid: graphqlGuid },
563
+ input: { label: 'GraphQL extension patched', enabled: false, expectedRevision: 1 },
564
+ },
565
+ })
566
+ .expect(200);
567
+ expect(graphUpdate.body.errors).toBeUndefined();
568
+ expect(graphUpdate.body.data.updateExampleExtensionApi).toMatchObject({
569
+ guid: graphqlGuid,
570
+ enabled: false,
571
+ revision: 2,
572
+ });
573
+
574
+ await request(app.getHttpServer())
575
+ .post('/raw-records')
576
+ .send({
577
+ guid: rejectedRecordGuid,
578
+ title: 'attempted owner',
579
+ kind: definition.owner.kind,
580
+ status: OmniRecordStatus.Active,
581
+ payloadSchemaId: definition.owner.schema.id,
582
+ payloadSchemaVersion: definition.owner.schema.version,
583
+ })
584
+ .expect(400);
585
+ await request(app.getHttpServer())
586
+ .post('/raw-records')
587
+ .send({
588
+ guid: normalRecordGuid,
589
+ title: 'normal record',
590
+ kind: 'normal_record',
591
+ status: OmniRecordStatus.Active,
592
+ })
593
+ .expect(201);
594
+ await request(app.getHttpServer())
595
+ .post('/example-links')
596
+ .send({
597
+ guid: '10000000-0000-4000-8000-000000000207',
598
+ linkKind: 'example_link',
599
+ sourceWorkItemId: normalRecordGuid,
600
+ targetWorkItemId: graphqlGuid,
601
+ })
602
+ .expect(400);
603
+ await request(app.getHttpServer())
604
+ .put(`/raw-records/${normalRecordGuid}`)
605
+ .send({ kind: definition.owner.kind })
606
+ .expect(400);
607
+ await request(app.getHttpServer())
608
+ .put(`/raw-records/${restGuid}`)
609
+ .send({ kind: 'normal_record' })
610
+ .expect(400);
611
+ await request(app.getHttpServer())
612
+ .delete(`/raw-records/${restGuid}`)
613
+ .expect(400);
614
+
615
+ for (const requestBody of [
616
+ {
617
+ query: `mutation CreateRaw($input: OmniRecordCreateInput!) {
618
+ Raw_createOmniRecordEntity(input: $input) { guid }
619
+ }`,
620
+ variables: {
621
+ input: {
622
+ guid: '10000000-0000-4000-8000-000000000205',
623
+ title: 'attempted GraphQL owner',
624
+ kind: definition.owner.kind,
625
+ status: 'Active',
626
+ },
627
+ },
628
+ },
629
+ {
630
+ query: `mutation UpdateRaw(
631
+ $conditions: OmniRecordCondition!
632
+ $input: OmniRecordUpdateInput!
633
+ ) {
634
+ Raw_updateOmniRecordEntity(conditions: $conditions, input: $input) { guid }
635
+ }`,
636
+ variables: {
637
+ conditions: { guid: restGuid },
638
+ input: { kind: 'normal_record' },
639
+ },
640
+ },
641
+ {
642
+ query: `mutation DeleteRaw($conditions: OmniRecordCondition!) {
643
+ Raw_deleteOmniRecordEntity(conditions: $conditions)
644
+ }`,
645
+ variables: { conditions: { guid: restGuid } },
646
+ },
647
+ ]) {
648
+ const response = await request(app.getHttpServer())
649
+ .post('/graphql')
650
+ .send(requestBody);
651
+ expect([200, 400]).toContain(response.status);
652
+ expect(response.body.data ?? null).toBeNull();
653
+ expect(response.body.errors?.[0]?.message ?? response.body.message).toContain(
654
+ 'owned by a registered extension projection',
655
+ );
656
+ }
657
+
658
+ const graphRelationDelete = await request(app.getHttpServer())
659
+ .post('/graphql')
660
+ .send({
661
+ query: `mutation DeleteLink($conditions: TransportExampleLinkApiCondition!) {
662
+ deleteExampleLinkApi(conditions: $conditions)
663
+ }`,
664
+ variables: { conditions: { guid: relationGuid } },
665
+ })
666
+ .expect(200);
667
+ expect(graphRelationDelete.body.errors).toBeUndefined();
668
+ expect(graphRelationDelete.body.data.deleteExampleLinkApi).toBe(true);
669
+
670
+ const graphDelete = await request(app.getHttpServer())
671
+ .post('/graphql')
672
+ .send({
673
+ query: `mutation DeleteExample($conditions: TransportExampleExtensionApiCondition!) {
674
+ deleteExampleExtensionApi(conditions: $conditions)
675
+ }`,
676
+ variables: { conditions: { guid: graphqlGuid } },
677
+ })
678
+ .expect(200);
679
+ expect(graphDelete.body.errors).toBeUndefined();
680
+ expect(graphDelete.body.data.deleteExampleExtensionApi).toBe(true);
681
+
682
+ await request(app.getHttpServer())
683
+ .delete(`/example-extensions/${restGuid}`)
684
+ .expect(200)
685
+ .expect({ deleted: true });
686
+ expect(
687
+ await dataSource.getRepository(OmniRecordEntity).findOneBy({
688
+ scopeId: scope.scopeId,
689
+ guid: restGuid,
690
+ }),
691
+ ).toBeNull();
692
+ expect(lifecycleBeforeCreate).toHaveBeenCalledTimes(3);
693
+ expect(lifecycleBeforeUpdate).toHaveBeenCalledTimes(3);
694
+ expect(lifecycleBeforeDelete).toHaveBeenCalledTimes(3);
695
+ });
696
+ });
697
+
698
+ describe('Omni migration snapshot ordering', () => {
699
+ it('orders referenced extension tables independently of snapshot order', () => {
700
+ const parent = {
701
+ name: 'projection_parent',
702
+ columns: [],
703
+ };
704
+ const child = {
705
+ name: 'projection_child',
706
+ columns: [],
707
+ foreignKeys: [
708
+ {
709
+ name: 'projection_child_parent_fk',
710
+ columnNames: ['scopeId', 'parentGuid'],
711
+ referencedTableName: 'projection_parent',
712
+ referencedColumnNames: ['scopeId', 'guid'],
713
+ onDelete: 'RESTRICT' as const,
714
+ },
715
+ ],
716
+ };
717
+ const plan = createOmniMigrationPlan(
718
+ defineOmniMigrationSnapshot({
719
+ version: 'projection-order-v1',
720
+ dialect: 'sqlite',
721
+ tables: [child, parent],
722
+ indexStatements: [],
723
+ }),
724
+ );
725
+
726
+ expect(plan.tableNames).toEqual(['projection_parent', 'projection_child']);
727
+ expect(() =>
728
+ createOmniMigrationPlan(
729
+ defineOmniMigrationSnapshot({
730
+ version: 'projection-order-cycle-v1',
731
+ dialect: 'sqlite',
732
+ tables: [
733
+ child,
734
+ {
735
+ ...parent,
736
+ foreignKeys: [
737
+ {
738
+ name: 'projection_parent_child_fk',
739
+ columnNames: ['scopeId', 'childGuid'],
740
+ referencedTableName: 'projection_child',
741
+ referencedColumnNames: ['scopeId', 'guid'],
742
+ onDelete: 'RESTRICT',
743
+ },
744
+ ],
745
+ },
746
+ ],
747
+ indexStatements: [],
748
+ }),
749
+ ),
750
+ ).toThrow('cross-table foreign-key cycle');
751
+ });
752
+ });
753
+
754
+ describe.each(dialectCases)(
755
+ "Omni migration plan (%s)",
756
+ ({ name, url }) => {
757
+ let dataSource: DataSource;
758
+
759
+ afterEach(async () => {
760
+ if (dataSource?.isInitialized) await dataSource.destroy();
761
+ });
762
+
763
+ it("uses a frozen metadata snapshot to create Omni base and extension tables without synchronize", async () => {
764
+ const entity = createOmniExtensionProjectionEntity<ExtensionRow>(
765
+ definition,
766
+ createProjectionDialect(name),
767
+ );
768
+ const registration = createOmniExtensionProjectionRegistration({
769
+ entity,
770
+ apiModel: ExampleExtensionApi,
771
+ definition,
772
+ dbConnection: "default",
773
+ });
774
+ dataSource = new DataSource({
775
+ type: name,
776
+ ...(name === "sqlite"
777
+ ? { database: ":memory:" }
778
+ : { url: url!, ssl: false }),
779
+ synchronize: false,
780
+ entities: [
781
+ OmniNamedEntity,
782
+ OmniRecordEntity,
783
+ OmniDocumentEntity,
784
+ OmniCollectionEntity,
785
+ OmniRelationEntity,
786
+ OmniExternalRefEntity,
787
+ ...registration.entities,
788
+ ],
789
+ });
790
+ await dataSource.initialize();
791
+ if (name === "postgres") await dataSource.dropDatabase();
792
+
793
+ const captured = captureOmniMigrationSnapshot(
794
+ `example-nest-extension-${name}-v1`,
795
+ dataSource,
796
+ [registration],
797
+ );
798
+ const migrationSource = structuredClone(captured);
799
+ const plan = createOmniMigrationPlan(
800
+ defineOmniMigrationSnapshot(migrationSource),
801
+ );
802
+ expect(dataSource.options.synchronize).toBe(false);
803
+ expect(Object.isFrozen(plan)).toBe(true);
804
+ expect(Object.isFrozen(plan.tableNames)).toBe(true);
805
+ expect(Object.isFrozen(plan.indexStatements)).toBe(true);
806
+ expect(Object.isFrozen(captured)).toBe(true);
807
+ expect(Object.isFrozen(captured.tables)).toBe(true);
808
+ expect(Object.isFrozen(captured.indexStatements)).toBe(true);
809
+ expect(plan.tableNames).toEqual(
810
+ expect.arrayContaining([
811
+ "omni-named",
812
+ "omni-record",
813
+ "omni-relation",
814
+ "omni-external-ref",
815
+ definition.tableName,
816
+ ]),
817
+ );
818
+ const shuffledPlan = createOmniMigrationPlan(
819
+ defineOmniMigrationSnapshot({
820
+ ...captured,
821
+ tables: [...captured.tables].reverse(),
822
+ }),
823
+ );
824
+ expect(
825
+ shuffledPlan.tableNames.indexOf('omni-record'),
826
+ ).toBeLessThan(shuffledPlan.tableNames.indexOf(definition.tableName));
827
+
828
+ const firstSnapshot = plan.createTables();
829
+ firstSnapshot[0].name = "mutated-copy";
830
+ migrationSource.tables[0].name = "mutated-migration-source";
831
+ migrationSource.indexStatements[0] = "mutated-index-statement";
832
+ expect(plan.createTables()[0].name).not.toBe("mutated-copy");
833
+ expect(plan.createTables()[0].name).not.toBe(
834
+ "mutated-migration-source",
835
+ );
836
+ expect(plan.indexStatements[0]).not.toBe("mutated-index-statement");
837
+ const enabledIndex = definition.fields.find(
838
+ (field) => field.name === "enabled",
839
+ )!.index!.name;
840
+ expect(plan.indexStatements).toEqual(
841
+ expect.arrayContaining([
842
+ expect.stringContaining(`CREATE INDEX "${enabledIndex}"`),
843
+ ]),
844
+ );
845
+
846
+ const queryRunner = dataSource.createQueryRunner();
847
+ await queryRunner.connect();
848
+ try {
849
+ await plan.create(queryRunner);
850
+ } finally {
851
+ await queryRunner.release();
852
+ }
853
+
854
+ const verificationRunner = dataSource.createQueryRunner();
855
+ await verificationRunner.connect();
856
+ try {
857
+ expect(await verificationRunner.hasTable(definition.tableName)).toBe(
858
+ true,
859
+ );
860
+ } finally {
861
+ await verificationRunner.release();
862
+ }
863
+ const extensionTable = plan
864
+ .createTables()
865
+ .find((table) => table.name === definition.tableName)!;
866
+ expect(extensionTable.foreignKeys).toEqual([
867
+ expect.objectContaining({
868
+ columnNames: ["scopeId", "guid"],
869
+ referencedTableName: "omni-record",
870
+ referencedColumnNames: ["scopeId", "guid"],
871
+ onDelete: "CASCADE",
872
+ }),
873
+ ]);
874
+ expect(extensionTable.findColumnByName("payload")?.type).toBe(
875
+ name === "postgres" ? "jsonb" : "text",
876
+ );
877
+
878
+ const serviceProvider = registration.providers.find(
879
+ (provider) =>
880
+ typeof provider === "object" &&
881
+ provider !== null &&
882
+ "provide" in provider &&
883
+ provider.provide === registration.serviceToken,
884
+ ) as any;
885
+ const service = serviceProvider.useFactory(dataSource, scope, events);
886
+ await service.createEntity({
887
+ guid: "10000000-0000-4000-8000-000000000101",
888
+ label: "migration-owned",
889
+ enabled: true,
890
+ });
891
+ expect(
892
+ await dataSource.getRepository(OmniRecordEntity).countBy({
893
+ scopeId: scope.scopeId,
894
+ guid: "10000000-0000-4000-8000-000000000101",
895
+ }),
896
+ ).toBe(1);
897
+
898
+ const projectionDialect = createProjectionDialect(name);
899
+ const evidence = await projectionDialect.inspect(dataSource, definition);
900
+ expect(evidence.indexes).toContain(enabledIndex);
901
+ const indexStatement = plan.indexStatements.find((statement) =>
902
+ statement.includes(`CREATE INDEX "${enabledIndex}"`),
903
+ )!;
904
+ const prefix = `CREATE INDEX "${enabledIndex}" ON "${definition.tableName}" ("scopeId", `;
905
+ const suffix = `, "${definition.identity.column}")`;
906
+ expect(indexStatement.startsWith(prefix)).toBe(true);
907
+ expect(indexStatement.endsWith(suffix)).toBe(true);
908
+ const expression = indexStatement.slice(prefix.length, -suffix.length);
909
+ const planRunner = dataSource.createQueryRunner();
910
+ await planRunner.connect();
911
+ try {
912
+ if (name === "postgres") {
913
+ await planRunner.startTransaction();
914
+ await planRunner.query("SET LOCAL enable_seqscan = off");
915
+ const rows = await planRunner.query(
916
+ `EXPLAIN (COSTS OFF) SELECT "${definition.identity.column}" FROM "${definition.tableName}" WHERE "${definition.scope.column}" = $1 AND ${expression} = $2 ORDER BY "${definition.identity.column}" ASC`,
917
+ [scope.scopeId, true],
918
+ );
919
+ expect(JSON.stringify(rows)).toContain(enabledIndex);
920
+ } else {
921
+ const rows = await planRunner.query(
922
+ `EXPLAIN QUERY PLAN SELECT "${definition.identity.column}" FROM "${definition.tableName}" INDEXED BY "${enabledIndex}" WHERE "${definition.scope.column}" = ? AND ${expression} = ? ORDER BY "${definition.identity.column}" ASC`,
923
+ [scope.scopeId, 1],
924
+ );
925
+ expect(JSON.stringify(rows)).toContain(enabledIndex);
926
+ }
927
+ } finally {
928
+ if (planRunner.isTransactionActive) await planRunner.rollbackTransaction();
929
+ await planRunner.release();
930
+ }
931
+
932
+ const downRunner = dataSource.createQueryRunner();
933
+ await downRunner.connect();
934
+ try {
935
+ await plan.drop(downRunner);
936
+ expect(await downRunner.hasTable(definition.tableName)).toBe(false);
937
+ expect(await downRunner.hasTable('omni-record')).toBe(false);
938
+ await plan.create(downRunner);
939
+ expect(await downRunner.hasTable(definition.tableName)).toBe(true);
940
+ expect(await downRunner.hasTable('omni-record')).toBe(true);
941
+ } finally {
942
+ await downRunner.release();
943
+ }
944
+ });
945
+ },
946
+ );