@nest-yalc-2/omnikernel-module 2.2.2 → 2.2.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (74) hide show
  1. package/package.json +17 -15
  2. package/src/__tests__/omni-collection.entity.spec.ts +49 -49
  3. package/src/__tests__/omni-collection.service.spec.ts +83 -83
  4. package/src/__tests__/omni-document.entity.spec.ts +55 -55
  5. package/src/__tests__/omni-document.service.spec.ts +83 -83
  6. package/src/__tests__/omni-extension-projection-constraints.spec.ts +518 -518
  7. package/src/__tests__/omni-extension-projection.registration.spec.ts +947 -946
  8. package/src/__tests__/omni-extension-projection.service.spec.ts +806 -806
  9. package/src/__tests__/omni-external-ref.entity.spec.ts +36 -36
  10. package/src/__tests__/omni-external-ref.service.spec.ts +252 -252
  11. package/src/__tests__/omni-named.entity.spec.ts +33 -33
  12. package/src/__tests__/omni-record.entity.spec.ts +32 -32
  13. package/src/__tests__/omni-record.service.spec.ts +107 -107
  14. package/src/__tests__/omni-relation-semantics.spec.ts +102 -102
  15. package/src/__tests__/omni-relation.entity.spec.ts +38 -38
  16. package/src/__tests__/omni-scope-substrate.spec.ts +81 -81
  17. package/src/__tests__/omni-scoped.backend.spec.ts +115 -115
  18. package/src/__tests__/omni-scoped.service.spec.ts +354 -354
  19. package/src/__tests__/omnikernel.diagnostics.spec.ts +94 -94
  20. package/src/__tests__/omnikernel.module.spec.ts +38 -40
  21. package/src/__tests__/omnikernel.persistence.spec.ts +154 -154
  22. package/src/__tests__/omnikernel.public-api.spec.ts +238 -238
  23. package/src/__tests__/omnikernel.query.service.spec.ts +155 -155
  24. package/src/base/omni-base.entity.ts +41 -41
  25. package/src/base/omni-external-ref.entity.ts +53 -53
  26. package/src/base/omni-named.entity.ts +16 -16
  27. package/src/base/omni-record.entity.ts +51 -51
  28. package/src/base/omni-relation.entity.ts +72 -72
  29. package/src/index.ts +49 -49
  30. package/src/omni-collection-kind.enum.ts +10 -10
  31. package/src/omni-collection.backend.ts +16 -16
  32. package/src/omni-collection.dto.ts +170 -170
  33. package/src/omni-collection.entity.ts +21 -21
  34. package/src/omni-collection.service.ts +75 -75
  35. package/src/omni-document-kind.enum.ts +12 -12
  36. package/src/omni-document.backend.ts +16 -16
  37. package/src/omni-document.dto.ts +198 -198
  38. package/src/omni-document.entity.ts +30 -30
  39. package/src/omni-document.service.ts +75 -75
  40. package/src/omni-dto.helpers.ts +21 -21
  41. package/src/omni-extension-projection.definition.ts +294 -294
  42. package/src/omni-extension-projection.resource.ts +274 -274
  43. package/src/omni-extension-projection.service.ts +452 -452
  44. package/src/omni-external-ref-binding.validator.ts +53 -53
  45. package/src/omni-external-ref-internal-type.enum.ts +11 -11
  46. package/src/omni-external-ref.backend.ts +25 -25
  47. package/src/omni-external-ref.dto.ts +128 -128
  48. package/src/omni-external-ref.service.ts +255 -255
  49. package/src/omni-migration.ts +365 -365
  50. package/src/omni-named.backend.ts +11 -11
  51. package/src/omni-named.dto.ts +86 -86
  52. package/src/omni-projection.catalog.ts +345 -345
  53. package/src/omni-projection.lifecycle.ts +53 -53
  54. package/src/omni-record-status.enum.ts +11 -11
  55. package/src/omni-record.backend.ts +19 -19
  56. package/src/omni-record.dto.ts +157 -157
  57. package/src/omni-record.service.ts +113 -113
  58. package/src/omni-relation-kind.contract.ts +49 -49
  59. package/src/omni-relation-kind.enum.ts +12 -12
  60. package/src/omni-relation-projection.definition.ts +166 -166
  61. package/src/omni-relation-projection.resource.ts +360 -360
  62. package/src/omni-relation-projection.service.ts +610 -610
  63. package/src/omni-relation-semantics.ts +52 -52
  64. package/src/omni-relation-status.enum.ts +11 -11
  65. package/src/omni-relation.backend.ts +21 -21
  66. package/src/omni-relation.dto.ts +142 -142
  67. package/src/omni-relation.service.ts +159 -159
  68. package/src/omni-scope.ts +161 -161
  69. package/src/omni-scoped.backend.ts +110 -110
  70. package/src/omni-scoped.repository.ts +34 -34
  71. package/src/omni-scoped.service.ts +338 -338
  72. package/src/omnikernel.diagnostics.ts +83 -83
  73. package/src/omnikernel.module.ts +97 -97
  74. package/src/omnikernel.query.service.ts +108 -108
@@ -1,365 +1,365 @@
1
- import type { DataSource, EntityTarget, ObjectLiteral } from 'typeorm';
2
- import { Table, TableForeignKey, type TableOptions } from 'typeorm';
3
- import {
4
- createProjectionDialect,
5
- type ProjectionResourceDefinition,
6
- } from '@nest-yalc-2/crud-gen';
7
- import { OmniExternalRefEntity } from './base/omni-external-ref.entity.js';
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 { OmniCollectionEntity } from './omni-collection.entity.js';
12
- import { OmniDocumentEntity } from './omni-document.entity.js';
13
-
14
- export interface OmniMigrationSnapshot {
15
- /** A migration-owned identifier, for example `omni-v1`. */
16
- readonly version: string;
17
- /** The driver whose physical column types were captured. */
18
- readonly dialect: 'sqlite' | 'postgres';
19
- /** Immutable, serializable TypeORM TableOptions captured during authoring. */
20
- readonly tables: readonly TableOptions[];
21
- /** Immutable ProjectionDialect DDL for JSON/expression indexes. */
22
- readonly indexStatements: readonly string[];
23
- }
24
-
25
- /**
26
- * The portable subset of a migration runner used by an Omni migration plan.
27
- *
28
- * It deliberately does not reference TypeORM's `QueryRunner` type. A package
29
- * manager may give the module and a consuming migration distinct physical
30
- * TypeORM installations, even at the same version; exposing that branded
31
- * framework type would then reject the application's runner. TypeORM runners
32
- * satisfy this structural capability contract without a consumer cast.
33
- */
34
- export interface OmniMigrationRunner {
35
- createTable(
36
- table: unknown,
37
- ifNotExist?: boolean,
38
- createForeignKeys?: boolean,
39
- createIndices?: boolean,
40
- ): Promise<void>;
41
- query(statement: string): Promise<unknown>;
42
- }
43
-
44
- export interface OmniMigrationPlan {
45
- readonly version: string;
46
- readonly tableNames: readonly string[];
47
- /** Dialect-compiled index DDL that the plan executes after all tables exist. */
48
- readonly indexStatements: readonly string[];
49
- /** Returns fresh mutable Table instances from the versioned source snapshot. */
50
- createTables(): Table[];
51
- /** Intended only from a TypeORM migration up() method. */
52
- create(queryRunner: OmniMigrationRunner): Promise<void>;
53
- /** Intended only from a TypeORM migration down() method. */
54
- drop(queryRunner: OmniMigrationRunner): Promise<void>;
55
- }
56
-
57
- /**
58
- * The migration-relevant subset of a registered extension composition. It
59
- * keeps schema authoring tied to the same immutable projection definition as
60
- * the generated transport and service, without making runtime metadata a
61
- * migration dependency.
62
- */
63
- export interface OmniMigrationExtensionRegistration {
64
- readonly entities: readonly EntityTarget<any>[];
65
- readonly definition: ProjectionResourceDefinition;
66
- }
67
-
68
- const omniBaseEntities = [
69
- OmniNamedEntity,
70
- OmniRecordEntity,
71
- OmniDocumentEntity,
72
- OmniCollectionEntity,
73
- OmniRelationEntity,
74
- OmniExternalRefEntity,
75
- ] as const;
76
-
77
- function freezeDeep<T>(value: T): T {
78
- if (value && typeof value === 'object' && !Object.isFrozen(value)) {
79
- for (const child of Object.values(value as Record<string, unknown>)) {
80
- freezeDeep(child);
81
- }
82
- Object.freeze(value);
83
- }
84
- return value;
85
- }
86
-
87
- function dialectFor(dataSource: DataSource): 'sqlite' | 'postgres' {
88
- if (dataSource.options.type === 'sqlite') return 'sqlite';
89
- if (dataSource.options.type === 'postgres') return 'postgres';
90
- throw new TypeError('Omni migration snapshots support SQLite or PostgreSQL.');
91
- }
92
-
93
- function tableOptions(table: Table): TableOptions {
94
- return {
95
- database: table.database,
96
- schema: table.schema,
97
- name: table.name,
98
- withoutRowid: table.withoutRowid,
99
- engine: table.engine,
100
- comment: table.comment,
101
- columns: table.columns.map((column) => ({
102
- name: column.name,
103
- type: column.type,
104
- default: column.default,
105
- onUpdate: column.onUpdate,
106
- isNullable: column.isNullable,
107
- isGenerated: column.isGenerated,
108
- generationStrategy: column.generationStrategy,
109
- isPrimary: column.isPrimary,
110
- isUnique: column.isUnique,
111
- isArray: column.isArray,
112
- comment: column.comment,
113
- length: column.length,
114
- width: column.width,
115
- charset: column.charset,
116
- collation: column.collation,
117
- precision: column.precision,
118
- scale: column.scale,
119
- zerofill: column.zerofill,
120
- unsigned: column.unsigned,
121
- enum: column.enum ? [...column.enum] : undefined,
122
- enumName: column.enumName,
123
- primaryKeyConstraintName: column.primaryKeyConstraintName,
124
- asExpression: column.asExpression,
125
- generatedType: column.generatedType,
126
- generatedIdentity: column.generatedIdentity,
127
- spatialFeatureType: column.spatialFeatureType,
128
- srid: column.srid,
129
- })),
130
- indices: table.indices.map((index) => ({
131
- name: index.name,
132
- columnNames: [...index.columnNames],
133
- isUnique: index.isUnique,
134
- isSpatial: index.isSpatial,
135
- isConcurrent: index.isConcurrent,
136
- isFulltext: index.isFulltext,
137
- isNullFiltered: index.isNullFiltered,
138
- parser: index.parser,
139
- where: index.where,
140
- })),
141
- foreignKeys: table.foreignKeys.map((foreignKey) => ({
142
- name: foreignKey.name,
143
- columnNames: [...foreignKey.columnNames],
144
- referencedDatabase: foreignKey.referencedDatabase,
145
- referencedSchema: foreignKey.referencedSchema,
146
- referencedTableName: foreignKey.referencedTableName,
147
- referencedColumnNames: [...foreignKey.referencedColumnNames],
148
- onDelete: foreignKey.onDelete,
149
- onUpdate: foreignKey.onUpdate,
150
- deferrable: foreignKey.deferrable,
151
- })),
152
- uniques: table.uniques.map((unique) => ({
153
- name: unique.name,
154
- columnNames: [...unique.columnNames],
155
- deferrable: unique.deferrable,
156
- })),
157
- checks: table.checks.map((check) => ({
158
- name: check.name,
159
- columnNames: check.columnNames ? [...check.columnNames] : undefined,
160
- expression: check.expression,
161
- })),
162
- exclusions: table.exclusions.map((exclusion) => ({
163
- name: exclusion.name,
164
- expression: exclusion.expression,
165
- })),
166
- };
167
- }
168
-
169
- function tableSnapshots(
170
- dataSource: DataSource,
171
- entities: readonly EntityTarget<ObjectLiteral>[],
172
- ): TableOptions[] {
173
- const byTableName = new Map<string, Table>();
174
- for (const entity of entities) {
175
- const metadata = dataSource.getMetadata(entity);
176
- const table = Table.create(metadata, dataSource.driver);
177
- table.foreignKeys = metadata.foreignKeys.map((foreignKey) =>
178
- TableForeignKey.create(foreignKey, dataSource.driver),
179
- );
180
- const current = byTableName.get(table.name);
181
- if (!current || current.columns.length < table.columns.length) {
182
- byTableName.set(table.name, table);
183
- }
184
- }
185
-
186
- return orderTablesTopologically([...byTableName.values()].map(tableOptions));
187
- }
188
-
189
- /**
190
- * Orders a complete migration snapshot from FK parents to children. A
191
- * migration may not defer this to registration order: PostgreSQL requires the
192
- * referenced table to exist at FK creation time. Self references are valid;
193
- * cross-table cycles are rejected during migration authoring.
194
- */
195
- function orderTablesTopologically(
196
- tables: readonly TableOptions[],
197
- ): TableOptions[] {
198
- const byName = new Map<string, TableOptions>();
199
- for (const table of tables) {
200
- if (!table.name)
201
- throw new TypeError('Omni migration table name is required.');
202
- byName.set(table.name, table);
203
- }
204
- const dependencies = new Map<string, Set<string>>();
205
- for (const table of tables) {
206
- const tableName = table.name!;
207
- const parents = new Set<string>();
208
- for (const foreignKey of table.foreignKeys ?? []) {
209
- const parent = foreignKey.referencedTableName;
210
- if (!parent || parent === tableName) continue;
211
- if (!byName.has(parent)) {
212
- throw new TypeError(
213
- `Omni migration table ${tableName} references ${parent}, which is absent from this snapshot.`,
214
- );
215
- }
216
- parents.add(parent);
217
- }
218
- dependencies.set(tableName, parents);
219
- }
220
- const remaining = new Map(
221
- [...dependencies.entries()].map(([name, parents]) => [
222
- name,
223
- new Set(parents),
224
- ]),
225
- );
226
- const ordered: TableOptions[] = [];
227
- while (remaining.size > 0) {
228
- const ready = tables.filter(
229
- (table) =>
230
- table.name !== undefined && remaining.get(table.name)?.size === 0,
231
- );
232
- if (ready.length === 0) {
233
- throw new TypeError(
234
- `Omni migration snapshot has a cross-table foreign-key cycle: ${[
235
- ...remaining.keys(),
236
- ].join(', ')}.`,
237
- );
238
- }
239
- for (const table of ready) {
240
- const name = table.name!;
241
- remaining.delete(name);
242
- ordered.push(table);
243
- for (const parents of remaining.values()) parents.delete(name);
244
- }
245
- }
246
- return ordered;
247
- }
248
-
249
- function quotedIdentifier(identifier: string): string {
250
- return `"${identifier.replaceAll('"', '""')}"`;
251
- }
252
-
253
- function quotedTableName(table: TableOptions): string {
254
- if (!table.name)
255
- throw new TypeError('Omni migration table name is required.');
256
- return [table.schema, table.name]
257
- .filter(
258
- (part): part is string => typeof part === 'string' && part.length > 0,
259
- )
260
- .map(quotedIdentifier)
261
- .join('.');
262
- }
263
-
264
- /**
265
- * Authoring-only helper. Run this against a metadata-only DataSource and copy
266
- * the returned value into a versioned migration source using
267
- * defineOmniMigrationSnapshot. Never invoke it from a migration at runtime.
268
- */
269
- export function captureOmniMigrationSnapshot(
270
- version: string,
271
- dataSource: DataSource,
272
- extensions: readonly OmniMigrationExtensionRegistration[] = [],
273
- ): OmniMigrationSnapshot {
274
- const dialect = dialectFor(dataSource);
275
- return defineOmniMigrationSnapshot({
276
- version,
277
- dialect,
278
- tables: tableSnapshots(dataSource, [
279
- ...omniBaseEntities,
280
- ...extensions.flatMap((extension) => extension.entities),
281
- ]),
282
- indexStatements: extensions.flatMap((extension) =>
283
- createProjectionDialect(dialect).compileIndexStatements(
284
- extension.definition,
285
- ),
286
- ),
287
- });
288
- }
289
-
290
- /**
291
- * Freezes a migration-owned table snapshot. Old migration source calls this
292
- * with a literal captured at authoring time, so future entity changes cannot
293
- * alter its DDL.
294
- */
295
- export function defineOmniMigrationSnapshot(
296
- snapshot: OmniMigrationSnapshot,
297
- ): OmniMigrationSnapshot {
298
- if (
299
- typeof snapshot.version !== 'string' ||
300
- snapshot.version.trim().length === 0
301
- ) {
302
- throw new TypeError('Omni migration snapshot version is required.');
303
- }
304
- if (snapshot.dialect !== 'sqlite' && snapshot.dialect !== 'postgres') {
305
- throw new TypeError('Omni migration snapshot dialect is unsupported.');
306
- }
307
- if (!Array.isArray(snapshot.tables) || snapshot.tables.length === 0) {
308
- throw new TypeError('Omni migration snapshot requires at least one table.');
309
- }
310
- const names = snapshot.tables.map((table) => table.name);
311
- if (
312
- names.some((name) => typeof name !== 'string' || name.length === 0) ||
313
- new Set(names).size !== names.length
314
- ) {
315
- throw new TypeError('Omni migration snapshot table names must be unique.');
316
- }
317
- if (
318
- !Array.isArray(snapshot.indexStatements) ||
319
- snapshot.indexStatements.some(
320
- (statement) =>
321
- typeof statement !== 'string' || statement.trim().length === 0,
322
- ) ||
323
- new Set(snapshot.indexStatements).size !== snapshot.indexStatements.length
324
- ) {
325
- throw new TypeError(
326
- 'Omni migration snapshot index statements must be unique non-empty strings.',
327
- );
328
- }
329
- return freezeDeep(structuredClone(snapshot));
330
- }
331
-
332
- /**
333
- * Creates a migration executor from an already-versioned snapshot. This API
334
- * deliberately accepts no DataSource or entity classes: runtime metadata is
335
- * never a migration authority.
336
- */
337
- export function createOmniMigrationPlan(
338
- snapshot: OmniMigrationSnapshot,
339
- ): OmniMigrationPlan {
340
- const source = defineOmniMigrationSnapshot(snapshot);
341
- const tables = orderTablesTopologically(source.tables);
342
-
343
- return Object.freeze({
344
- version: source.version,
345
- tableNames: Object.freeze(tables.map((table) => table.name)),
346
- indexStatements: Object.freeze([...source.indexStatements]),
347
- createTables: () =>
348
- tables.map((table) => new Table(structuredClone(table))),
349
- create: async (queryRunner: OmniMigrationRunner) => {
350
- for (const table of tables) {
351
- await queryRunner.createTable(new Table(structuredClone(table)));
352
- }
353
- for (const statement of source.indexStatements) {
354
- await queryRunner.query(statement);
355
- }
356
- },
357
- drop: async (queryRunner: OmniMigrationRunner) => {
358
- for (const table of [...tables].reverse()) {
359
- await queryRunner.query(
360
- `DROP TABLE IF EXISTS ${quotedTableName(table)}`,
361
- );
362
- }
363
- },
364
- });
365
- }
1
+ import type { DataSource, EntityTarget, ObjectLiteral } from 'typeorm';
2
+ import { Table, TableForeignKey, type TableOptions } from 'typeorm';
3
+ import {
4
+ createProjectionDialect,
5
+ type ProjectionResourceDefinition,
6
+ } from '@nest-yalc-2/crud-gen';
7
+ import { OmniExternalRefEntity } from './base/omni-external-ref.entity.js';
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 { OmniCollectionEntity } from './omni-collection.entity.js';
12
+ import { OmniDocumentEntity } from './omni-document.entity.js';
13
+
14
+ export interface OmniMigrationSnapshot {
15
+ /** A migration-owned identifier, for example `omni-v1`. */
16
+ readonly version: string;
17
+ /** The driver whose physical column types were captured. */
18
+ readonly dialect: 'sqlite' | 'postgres';
19
+ /** Immutable, serializable TypeORM TableOptions captured during authoring. */
20
+ readonly tables: readonly TableOptions[];
21
+ /** Immutable ProjectionDialect DDL for JSON/expression indexes. */
22
+ readonly indexStatements: readonly string[];
23
+ }
24
+
25
+ /**
26
+ * The portable subset of a migration runner used by an Omni migration plan.
27
+ *
28
+ * It deliberately does not reference TypeORM's `QueryRunner` type. A package
29
+ * manager may give the module and a consuming migration distinct physical
30
+ * TypeORM installations, even at the same version; exposing that branded
31
+ * framework type would then reject the application's runner. TypeORM runners
32
+ * satisfy this structural capability contract without a consumer cast.
33
+ */
34
+ export interface OmniMigrationRunner {
35
+ createTable(
36
+ table: unknown,
37
+ ifNotExist?: boolean,
38
+ createForeignKeys?: boolean,
39
+ createIndices?: boolean,
40
+ ): Promise<void>;
41
+ query(statement: string): Promise<unknown>;
42
+ }
43
+
44
+ export interface OmniMigrationPlan {
45
+ readonly version: string;
46
+ readonly tableNames: readonly string[];
47
+ /** Dialect-compiled index DDL that the plan executes after all tables exist. */
48
+ readonly indexStatements: readonly string[];
49
+ /** Returns fresh mutable Table instances from the versioned source snapshot. */
50
+ createTables(): Table[];
51
+ /** Intended only from a TypeORM migration up() method. */
52
+ create(queryRunner: OmniMigrationRunner): Promise<void>;
53
+ /** Intended only from a TypeORM migration down() method. */
54
+ drop(queryRunner: OmniMigrationRunner): Promise<void>;
55
+ }
56
+
57
+ /**
58
+ * The migration-relevant subset of a registered extension composition. It
59
+ * keeps schema authoring tied to the same immutable projection definition as
60
+ * the generated transport and service, without making runtime metadata a
61
+ * migration dependency.
62
+ */
63
+ export interface OmniMigrationExtensionRegistration {
64
+ readonly entities: readonly EntityTarget<any>[];
65
+ readonly definition: ProjectionResourceDefinition;
66
+ }
67
+
68
+ const omniBaseEntities = [
69
+ OmniNamedEntity,
70
+ OmniRecordEntity,
71
+ OmniDocumentEntity,
72
+ OmniCollectionEntity,
73
+ OmniRelationEntity,
74
+ OmniExternalRefEntity,
75
+ ] as const;
76
+
77
+ function freezeDeep<T>(value: T): T {
78
+ if (value && typeof value === 'object' && !Object.isFrozen(value)) {
79
+ for (const child of Object.values(value as Record<string, unknown>)) {
80
+ freezeDeep(child);
81
+ }
82
+ Object.freeze(value);
83
+ }
84
+ return value;
85
+ }
86
+
87
+ function dialectFor(dataSource: DataSource): 'sqlite' | 'postgres' {
88
+ if (dataSource.options.type === 'sqlite') return 'sqlite';
89
+ if (dataSource.options.type === 'postgres') return 'postgres';
90
+ throw new TypeError('Omni migration snapshots support SQLite or PostgreSQL.');
91
+ }
92
+
93
+ function tableOptions(table: Table): TableOptions {
94
+ return {
95
+ database: table.database,
96
+ schema: table.schema,
97
+ name: table.name,
98
+ withoutRowid: table.withoutRowid,
99
+ engine: table.engine,
100
+ comment: table.comment,
101
+ columns: table.columns.map((column) => ({
102
+ name: column.name,
103
+ type: column.type,
104
+ default: column.default,
105
+ onUpdate: column.onUpdate,
106
+ isNullable: column.isNullable,
107
+ isGenerated: column.isGenerated,
108
+ generationStrategy: column.generationStrategy,
109
+ isPrimary: column.isPrimary,
110
+ isUnique: column.isUnique,
111
+ isArray: column.isArray,
112
+ comment: column.comment,
113
+ length: column.length,
114
+ width: column.width,
115
+ charset: column.charset,
116
+ collation: column.collation,
117
+ precision: column.precision,
118
+ scale: column.scale,
119
+ zerofill: column.zerofill,
120
+ unsigned: column.unsigned,
121
+ enum: column.enum ? [...column.enum] : undefined,
122
+ enumName: column.enumName,
123
+ primaryKeyConstraintName: column.primaryKeyConstraintName,
124
+ asExpression: column.asExpression,
125
+ generatedType: column.generatedType,
126
+ generatedIdentity: column.generatedIdentity,
127
+ spatialFeatureType: column.spatialFeatureType,
128
+ srid: column.srid,
129
+ })),
130
+ indices: table.indices.map((index) => ({
131
+ name: index.name,
132
+ columnNames: [...index.columnNames],
133
+ isUnique: index.isUnique,
134
+ isSpatial: index.isSpatial,
135
+ isConcurrent: index.isConcurrent,
136
+ isFulltext: index.isFulltext,
137
+ isNullFiltered: index.isNullFiltered,
138
+ parser: index.parser,
139
+ where: index.where,
140
+ })),
141
+ foreignKeys: table.foreignKeys.map((foreignKey) => ({
142
+ name: foreignKey.name,
143
+ columnNames: [...foreignKey.columnNames],
144
+ referencedDatabase: foreignKey.referencedDatabase,
145
+ referencedSchema: foreignKey.referencedSchema,
146
+ referencedTableName: foreignKey.referencedTableName,
147
+ referencedColumnNames: [...foreignKey.referencedColumnNames],
148
+ onDelete: foreignKey.onDelete,
149
+ onUpdate: foreignKey.onUpdate,
150
+ deferrable: foreignKey.deferrable,
151
+ })),
152
+ uniques: table.uniques.map((unique) => ({
153
+ name: unique.name,
154
+ columnNames: [...unique.columnNames],
155
+ deferrable: unique.deferrable,
156
+ })),
157
+ checks: table.checks.map((check) => ({
158
+ name: check.name,
159
+ columnNames: check.columnNames ? [...check.columnNames] : undefined,
160
+ expression: check.expression,
161
+ })),
162
+ exclusions: table.exclusions.map((exclusion) => ({
163
+ name: exclusion.name,
164
+ expression: exclusion.expression,
165
+ })),
166
+ };
167
+ }
168
+
169
+ function tableSnapshots(
170
+ dataSource: DataSource,
171
+ entities: readonly EntityTarget<ObjectLiteral>[],
172
+ ): TableOptions[] {
173
+ const byTableName = new Map<string, Table>();
174
+ for (const entity of entities) {
175
+ const metadata = dataSource.getMetadata(entity);
176
+ const table = Table.create(metadata, dataSource.driver);
177
+ table.foreignKeys = metadata.foreignKeys.map((foreignKey) =>
178
+ TableForeignKey.create(foreignKey, dataSource.driver),
179
+ );
180
+ const current = byTableName.get(table.name);
181
+ if (!current || current.columns.length < table.columns.length) {
182
+ byTableName.set(table.name, table);
183
+ }
184
+ }
185
+
186
+ return orderTablesTopologically([...byTableName.values()].map(tableOptions));
187
+ }
188
+
189
+ /**
190
+ * Orders a complete migration snapshot from FK parents to children. A
191
+ * migration may not defer this to registration order: PostgreSQL requires the
192
+ * referenced table to exist at FK creation time. Self references are valid;
193
+ * cross-table cycles are rejected during migration authoring.
194
+ */
195
+ function orderTablesTopologically(
196
+ tables: readonly TableOptions[],
197
+ ): TableOptions[] {
198
+ const byName = new Map<string, TableOptions>();
199
+ for (const table of tables) {
200
+ if (!table.name)
201
+ throw new TypeError('Omni migration table name is required.');
202
+ byName.set(table.name, table);
203
+ }
204
+ const dependencies = new Map<string, Set<string>>();
205
+ for (const table of tables) {
206
+ const tableName = table.name!;
207
+ const parents = new Set<string>();
208
+ for (const foreignKey of table.foreignKeys ?? []) {
209
+ const parent = foreignKey.referencedTableName;
210
+ if (!parent || parent === tableName) continue;
211
+ if (!byName.has(parent)) {
212
+ throw new TypeError(
213
+ `Omni migration table ${tableName} references ${parent}, which is absent from this snapshot.`,
214
+ );
215
+ }
216
+ parents.add(parent);
217
+ }
218
+ dependencies.set(tableName, parents);
219
+ }
220
+ const remaining = new Map(
221
+ [...dependencies.entries()].map(([name, parents]) => [
222
+ name,
223
+ new Set(parents),
224
+ ]),
225
+ );
226
+ const ordered: TableOptions[] = [];
227
+ while (remaining.size > 0) {
228
+ const ready = tables.filter(
229
+ (table) =>
230
+ table.name !== undefined && remaining.get(table.name)?.size === 0,
231
+ );
232
+ if (ready.length === 0) {
233
+ throw new TypeError(
234
+ `Omni migration snapshot has a cross-table foreign-key cycle: ${[
235
+ ...remaining.keys(),
236
+ ].join(', ')}.`,
237
+ );
238
+ }
239
+ for (const table of ready) {
240
+ const name = table.name!;
241
+ remaining.delete(name);
242
+ ordered.push(table);
243
+ for (const parents of remaining.values()) parents.delete(name);
244
+ }
245
+ }
246
+ return ordered;
247
+ }
248
+
249
+ function quotedIdentifier(identifier: string): string {
250
+ return `"${identifier.replaceAll('"', '""')}"`;
251
+ }
252
+
253
+ function quotedTableName(table: TableOptions): string {
254
+ if (!table.name)
255
+ throw new TypeError('Omni migration table name is required.');
256
+ return [table.schema, table.name]
257
+ .filter(
258
+ (part): part is string => typeof part === 'string' && part.length > 0,
259
+ )
260
+ .map(quotedIdentifier)
261
+ .join('.');
262
+ }
263
+
264
+ /**
265
+ * Authoring-only helper. Run this against a metadata-only DataSource and copy
266
+ * the returned value into a versioned migration source using
267
+ * defineOmniMigrationSnapshot. Never invoke it from a migration at runtime.
268
+ */
269
+ export function captureOmniMigrationSnapshot(
270
+ version: string,
271
+ dataSource: DataSource,
272
+ extensions: readonly OmniMigrationExtensionRegistration[] = [],
273
+ ): OmniMigrationSnapshot {
274
+ const dialect = dialectFor(dataSource);
275
+ return defineOmniMigrationSnapshot({
276
+ version,
277
+ dialect,
278
+ tables: tableSnapshots(dataSource, [
279
+ ...omniBaseEntities,
280
+ ...extensions.flatMap((extension) => extension.entities),
281
+ ]),
282
+ indexStatements: extensions.flatMap((extension) =>
283
+ createProjectionDialect(dialect).compileIndexStatements(
284
+ extension.definition,
285
+ ),
286
+ ),
287
+ });
288
+ }
289
+
290
+ /**
291
+ * Freezes a migration-owned table snapshot. Old migration source calls this
292
+ * with a literal captured at authoring time, so future entity changes cannot
293
+ * alter its DDL.
294
+ */
295
+ export function defineOmniMigrationSnapshot(
296
+ snapshot: OmniMigrationSnapshot,
297
+ ): OmniMigrationSnapshot {
298
+ if (
299
+ typeof snapshot.version !== 'string' ||
300
+ snapshot.version.trim().length === 0
301
+ ) {
302
+ throw new TypeError('Omni migration snapshot version is required.');
303
+ }
304
+ if (snapshot.dialect !== 'sqlite' && snapshot.dialect !== 'postgres') {
305
+ throw new TypeError('Omni migration snapshot dialect is unsupported.');
306
+ }
307
+ if (!Array.isArray(snapshot.tables) || snapshot.tables.length === 0) {
308
+ throw new TypeError('Omni migration snapshot requires at least one table.');
309
+ }
310
+ const names = snapshot.tables.map((table) => table.name);
311
+ if (
312
+ names.some((name) => typeof name !== 'string' || name.length === 0) ||
313
+ new Set(names).size !== names.length
314
+ ) {
315
+ throw new TypeError('Omni migration snapshot table names must be unique.');
316
+ }
317
+ if (
318
+ !Array.isArray(snapshot.indexStatements) ||
319
+ snapshot.indexStatements.some(
320
+ (statement) =>
321
+ typeof statement !== 'string' || statement.trim().length === 0,
322
+ ) ||
323
+ new Set(snapshot.indexStatements).size !== snapshot.indexStatements.length
324
+ ) {
325
+ throw new TypeError(
326
+ 'Omni migration snapshot index statements must be unique non-empty strings.',
327
+ );
328
+ }
329
+ return freezeDeep(structuredClone(snapshot));
330
+ }
331
+
332
+ /**
333
+ * Creates a migration executor from an already-versioned snapshot. This API
334
+ * deliberately accepts no DataSource or entity classes: runtime metadata is
335
+ * never a migration authority.
336
+ */
337
+ export function createOmniMigrationPlan(
338
+ snapshot: OmniMigrationSnapshot,
339
+ ): OmniMigrationPlan {
340
+ const source = defineOmniMigrationSnapshot(snapshot);
341
+ const tables = orderTablesTopologically(source.tables);
342
+
343
+ return Object.freeze({
344
+ version: source.version,
345
+ tableNames: Object.freeze(tables.map((table) => table.name)),
346
+ indexStatements: Object.freeze([...source.indexStatements]),
347
+ createTables: () =>
348
+ tables.map((table) => new Table(structuredClone(table))),
349
+ create: async (queryRunner: OmniMigrationRunner) => {
350
+ for (const table of tables) {
351
+ await queryRunner.createTable(new Table(structuredClone(table)));
352
+ }
353
+ for (const statement of source.indexStatements) {
354
+ await queryRunner.query(statement);
355
+ }
356
+ },
357
+ drop: async (queryRunner: OmniMigrationRunner) => {
358
+ for (const table of [...tables].reverse()) {
359
+ await queryRunner.query(
360
+ `DROP TABLE IF EXISTS ${quotedTableName(table)}`,
361
+ );
362
+ }
363
+ },
364
+ });
365
+ }