uql-orm 0.58.0 → 0.60.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.
@@ -186,17 +186,10 @@ export function defineEntity(entity, opts = {}) {
186
186
  meta.derivedName = true;
187
187
  }
188
188
  meta.schema = opts.schema ?? meta.schema;
189
- let proto = Object.getPrototypeOf(entity.prototype);
190
- while (proto.constructor !== Object) {
191
- const parent = proto.constructor;
192
- // An `abstract class BaseEntity` carrying `@Field`s but no `@Entity()` has nobody to drain its
193
- // registrations, so do it here. Walking the *class* prototype chain rather than reading through the
194
- // metadata object's is what makes this work on every transformer: tsc and esbuild chain metadata
195
- // across `extends`, SWC does not.
196
- applyMembers(parent, ownRegistrations(parent));
197
- extendMeta(meta, ensureMeta(parent));
198
- proto = Object.getPrototypeOf(proto);
199
- }
189
+ // The class's real chain first: where a class both extends a base and names one, the one it extends
190
+ // is the nearer, and nearer wins every merge.
191
+ inheritFrom(meta, parentOf(entity));
192
+ inheritFrom(meta, opts.extends);
200
193
  // Derive soft-delete from the (inheritance-merged) fields, so own and inherited markers are handled
201
194
  // uniformly. Exactly one field may be marked; it auto-registers the built-in `softDelete` read
202
195
  // filter (a reserved name - see defineFilter - so it never clobbers a user filter).
@@ -471,6 +464,24 @@ function junctionColumn(meta, idKey) {
471
464
  function getIdKeys(meta) {
472
465
  return getKeys(meta.fields).filter((key) => meta.fields[key]?.isId);
473
466
  }
467
+ /**
468
+ * Merges `ancestor` and its own ancestors into `meta`, nearest first, so a further one never overwrites
469
+ * a nearer. An `abstract class BaseEntity` carrying `@Field`s but no `@Entity()` has nobody to drain
470
+ * its registrations, so do it here. Walking the *class* prototype chain rather than the metadata
471
+ * object's is what makes this work on every transformer: tsc and esbuild chain metadata across
472
+ * `extends`, SWC does not.
473
+ */
474
+ function inheritFrom(meta, ancestor) {
475
+ for (let parent = ancestor; parent && parent !== Object; parent = parentOf(parent)) {
476
+ const base = parent;
477
+ applyMembers(base, ownRegistrations(base));
478
+ extendMeta(meta, ensureMeta(base));
479
+ }
480
+ }
481
+ /** The class `entity` extends, `Object` where it extends nothing. */
482
+ function parentOf(entity) {
483
+ return Object.getPrototypeOf(entity.prototype)?.constructor;
484
+ }
474
485
  function extendMeta(target, source) {
475
486
  const sourceFields = { ...source.fields };
476
487
  // A subclass declaring its own primary key drops the parent's - every column of it, or a composite
@@ -1,6 +1,6 @@
1
- import { type Querier, type QuerierPool, type SqlQuerier } from '../type/index.js';
1
+ import { type MongoQuerier, type Querier, type QuerierPool, type SqlQuerier } from '../type/index.js';
2
2
  /**
3
- * Querier used for schema migrations and the migration journal (`DatabaseMigrationStorage`).
3
+ * Querier used for schema migrations and the migration journal.
4
4
  *
5
5
  * Pools may override {@link QuerierPool.getMigrationQuerier} so DDL runs on a different target than
6
6
  * app traffic (e.g. LibSQL embedded replica: local `file:` + remote `syncUrl`).
@@ -13,8 +13,7 @@ export declare function acquireQuerierForMigrations(pool: QuerierPool): Promise<
13
13
  * traffic, but the ownership rule is the same one: whoever acquires, releases.
14
14
  */
15
15
  export declare function withQuerierForMigrations<T>(pool: QuerierPool, task: (querier: Querier) => Promise<T>): Promise<T>;
16
- /**
17
- * Same, for the paths that only work against SQL. `requiredBy` names the caller in the error, which is
18
- * the only thing the five copies of this acquire-assert-release dance used to differ by.
19
- */
16
+ /** Same, for the paths that only work against SQL. `requiredBy` names the caller in the error. */
20
17
  export declare function withSqlQuerierForMigrations<T>(pool: QuerierPool, requiredBy: string, task: (querier: SqlQuerier) => Promise<T>): Promise<T>;
18
+ /** Same, for the paths that only work against MongoDB. */
19
+ export declare function withMongoQuerierForMigrations<T>(pool: QuerierPool, requiredBy: string, task: (querier: MongoQuerier) => Promise<T>): Promise<T>;
@@ -1,6 +1,6 @@
1
- import { isSqlQuerier } from '../type/index.js';
1
+ import { isMongoQuerier, isSqlQuerier, } from '../type/index.js';
2
2
  /**
3
- * Querier used for schema migrations and the migration journal (`DatabaseMigrationStorage`).
3
+ * Querier used for schema migrations and the migration journal.
4
4
  *
5
5
  * Pools may override {@link QuerierPool.getMigrationQuerier} so DDL runs on a different target than
6
6
  * app traffic (e.g. LibSQL embedded replica: local `file:` + remote `syncUrl`).
@@ -23,14 +23,18 @@ export async function withQuerierForMigrations(pool, task) {
23
23
  await querier.release();
24
24
  }
25
25
  }
26
- /**
27
- * Same, for the paths that only work against SQL. `requiredBy` names the caller in the error, which is
28
- * the only thing the five copies of this acquire-assert-release dance used to differ by.
29
- */
26
+ /** Same, for the paths that only work against SQL. `requiredBy` names the caller in the error. */
30
27
  export function withSqlQuerierForMigrations(pool, requiredBy, task) {
28
+ return withQuerierOfKind(pool, isSqlQuerier, `${requiredBy} requires a SQL-based querier`, task);
29
+ }
30
+ /** Same, for the paths that only work against MongoDB. */
31
+ export function withMongoQuerierForMigrations(pool, requiredBy, task) {
32
+ return withQuerierOfKind(pool, isMongoQuerier, `${requiredBy} requires a MongoDB querier`, task);
33
+ }
34
+ function withQuerierOfKind(pool, isKind, error, task) {
31
35
  return withQuerierForMigrations(pool, (querier) => {
32
- if (!isSqlQuerier(querier)) {
33
- throw new TypeError(`${requiredBy} requires a SQL-based querier`);
36
+ if (!isKind(querier)) {
37
+ throw new TypeError(error);
34
38
  }
35
39
  return task(querier);
36
40
  });
@@ -414,6 +414,4 @@ export interface IMigrationBuilder {
414
414
  dropForeignKey(tableName: string, constraintName: string): Promise<void>;
415
415
  /** Execute raw SQL */
416
416
  raw(sql: string): Promise<void>;
417
- /** Get all recorded operations */
418
- getOperations(): AnyMigrationOperation[];
419
417
  }
@@ -5,4 +5,4 @@
5
5
  */
6
6
  export { createEntityCodeGenerator, EntityCodeGenerator, type EntityCodeGeneratorOptions, type GeneratedEntity, } from './entityCodeGenerator.js';
7
7
  export { entityTypesSource } from './entityTypes.js';
8
- export { buildSqlQuerierMigrationModule, EMPTY_MANUAL_MIGRATION_DOWN_INNER, EMPTY_MANUAL_MIGRATION_UP_INNER, emitSqlRunCall, emitSqlRunCalls, type SqlMigrationModuleOptions, } from './migrationFile.js';
8
+ export { buildMigrationModule, buildSqlQuerierMigrationModule, EMPTY_MANUAL_MIGRATION_DOWN_INNER, EMPTY_MANUAL_MIGRATION_UP_INNER, emitMongoCommandCalls, emitSqlRunCall, emitSqlRunCalls, type MigrationModuleOptions, type MigrationQuerierType, type SqlMigrationModuleOptions, } from './migrationFile.js';
@@ -6,4 +6,4 @@
6
6
  // Entity code generator
7
7
  export { createEntityCodeGenerator, EntityCodeGenerator, } from './entityCodeGenerator.js';
8
8
  export { entityTypesSource } from './entityTypes.js';
9
- export { buildSqlQuerierMigrationModule, EMPTY_MANUAL_MIGRATION_DOWN_INNER, EMPTY_MANUAL_MIGRATION_UP_INNER, emitSqlRunCall, emitSqlRunCalls, } from './migrationFile.js';
9
+ export { buildMigrationModule, buildSqlQuerierMigrationModule, EMPTY_MANUAL_MIGRATION_DOWN_INNER, EMPTY_MANUAL_MIGRATION_UP_INNER, emitMongoCommandCalls, emitSqlRunCall, emitSqlRunCalls, } from './migrationFile.js';
@@ -1,9 +1,13 @@
1
1
  /**
2
- * Source code generation for default-export SQL migrations (`SqlQuerier` / `uql-migrate`).
2
+ * Source code generation for default-export migrations (`uql-migrate`), on a `SqlQuerier` or a `MongoQuerier`.
3
3
  */
4
- export type SqlMigrationModuleOptions = {
4
+ /** The querier a migration module is written against. */
5
+ export type MigrationQuerierType = 'SqlQuerier' | 'MongoQuerier';
6
+ export type MigrationModuleOptions = {
5
7
  migrationName: string;
6
8
  createdAt: Date;
9
+ /** Defaults to `SqlQuerier`. */
10
+ querier?: MigrationQuerierType;
7
11
  /** Extra lines in the file header comment (without leading ` * `). */
8
12
  docExtraLines?: string[];
9
13
  /** Indented body inside `async up` (including newlines). */
@@ -11,6 +15,8 @@ export type SqlMigrationModuleOptions = {
11
15
  /** Indented body inside `async down` (including newlines). */
12
16
  downInner: string;
13
17
  };
18
+ /** @deprecated Use {@link MigrationModuleOptions}. */
19
+ export type SqlMigrationModuleOptions = MigrationModuleOptions;
14
20
  /**
15
21
  * Emit one `await querier.run(...)` line for entity-generated migrations.
16
22
  * Uses `JSON.stringify` so SQL with backticks (SQLite/LibSQL), quotes, `${`, etc. stays valid TS source.
@@ -18,11 +24,28 @@ export type SqlMigrationModuleOptions = {
18
24
  export declare function emitSqlRunCall(sql: string): string;
19
25
  /** Indented `up`/`down` body: one `await querier.run(...)` per SQL string (entity-generated migrations, #87). */
20
26
  export declare function emitSqlRunCalls(statements: string[]): string;
27
+ /** Indented `up`/`down` body: one awaited driver call on `querier.db` per MongoDB command. */
28
+ export declare function emitMongoCommandCalls(statements: string[]): string;
21
29
  /** Body for `up` in a manual (empty) migration scaffold. */
22
30
  export declare const EMPTY_MANUAL_MIGRATION_UP_INNER = " // Add your migration logic here.\n // Use one await querier.run(\"...\") per SQL statement when possible (same style as generate:entities).\n // Example (Postgres):\n // await querier.run(\"CREATE TABLE \\\"users\\\" (\\\"id\\\" SERIAL PRIMARY KEY);\");\n";
23
31
  /** Body for `down` in a manual (empty) migration scaffold. */
24
32
  export declare const EMPTY_MANUAL_MIGRATION_DOWN_INNER = " // Add your rollback logic here.\n // await querier.run(\"DROP TABLE IF EXISTS \\\"users\\\";\");\n";
33
+ /** How a migration on each querier is scaffolded empty, and how a generated statement is spelled in it. */
34
+ export declare const migrationSource: {
35
+ SqlQuerier: {
36
+ emptyUp: string;
37
+ emptyDown: string;
38
+ emit: typeof emitSqlRunCalls;
39
+ };
40
+ MongoQuerier: {
41
+ emptyUp: string;
42
+ emptyDown: string;
43
+ emit: typeof emitMongoCommandCalls;
44
+ };
45
+ };
25
46
  /**
26
47
  * Full contents of a `export default { async up/down(querier) { ... } }` migration module.
27
48
  */
28
- export declare function buildSqlQuerierMigrationModule(options: SqlMigrationModuleOptions): string;
49
+ export declare function buildMigrationModule(options: MigrationModuleOptions): string;
50
+ /** @deprecated Use {@link buildMigrationModule}. */
51
+ export declare const buildSqlQuerierMigrationModule: typeof buildMigrationModule;
@@ -1,6 +1,4 @@
1
- /**
2
- * Source code generation for default-export SQL migrations (`SqlQuerier` / `uql-migrate`).
3
- */
1
+ import { mongoCommandSource } from '../generator/mongoCommand.js';
4
2
  /**
5
3
  * Emit one `await querier.run(...)` line for entity-generated migrations.
6
4
  * Uses `JSON.stringify` so SQL with backticks (SQLite/LibSQL), quotes, `${`, etc. stays valid TS source.
@@ -12,6 +10,10 @@ export function emitSqlRunCall(sql) {
12
10
  export function emitSqlRunCalls(statements) {
13
11
  return statements.map(emitSqlRunCall).join('\n');
14
12
  }
13
+ /** Indented `up`/`down` body: one awaited driver call on `querier.db` per MongoDB command. */
14
+ export function emitMongoCommandCalls(statements) {
15
+ return statements.map((statement) => /*ts*/ ` await ${mongoCommandSource(statement, 'querier.db')};`).join('\n');
16
+ }
15
17
  /** Body for `up` in a manual (empty) migration scaffold. */
16
18
  export const EMPTY_MANUAL_MIGRATION_UP_INNER = ` // Add your migration logic here.
17
19
  // Use one await querier.run("...") per SQL statement when possible (same style as generate:entities).
@@ -22,26 +24,46 @@ export const EMPTY_MANUAL_MIGRATION_UP_INNER = ` // Add your migration logic
22
24
  export const EMPTY_MANUAL_MIGRATION_DOWN_INNER = ` // Add your rollback logic here.
23
25
  // await querier.run("DROP TABLE IF EXISTS \\"users\\";");
24
26
  `;
27
+ /** How a migration on each querier is scaffolded empty, and how a generated statement is spelled in it. */
28
+ export const migrationSource = {
29
+ SqlQuerier: {
30
+ emptyUp: EMPTY_MANUAL_MIGRATION_UP_INNER,
31
+ emptyDown: EMPTY_MANUAL_MIGRATION_DOWN_INNER,
32
+ emit: emitSqlRunCalls,
33
+ },
34
+ MongoQuerier: {
35
+ emptyUp: ` // Add your migration logic here, through the database handle.
36
+ // await querier.db.collection('users').updateMany({}, { $set: { active: true } });
37
+ `,
38
+ emptyDown: ` // Add your rollback logic here.
39
+ // await querier.db.collection('users').updateMany({}, { $unset: { active: '' } });
40
+ `,
41
+ emit: emitMongoCommandCalls,
42
+ },
43
+ };
25
44
  /**
26
45
  * Full contents of a `export default { async up/down(querier) { ... } }` migration module.
27
46
  */
28
- export function buildSqlQuerierMigrationModule(options) {
47
+ export function buildMigrationModule(options) {
48
+ const querier = options.querier ?? 'SqlQuerier';
29
49
  const iso = options.createdAt.toISOString();
30
50
  const extra = options.docExtraLines?.map((line) => `\n * ${line}`).join('') ?? '';
31
- return /*ts*/ `import type { SqlQuerier } from 'uql-orm/migrate';
51
+ return /*ts*/ `import type { ${querier} } from 'uql-orm/migrate';
32
52
 
33
53
  /**
34
54
  * Migration: ${options.migrationName}
35
55
  * Created: ${iso}${extra}
36
56
  */
37
57
  export default {
38
- async up(querier: SqlQuerier): Promise<void> {
58
+ async up(querier: ${querier}): Promise<void> {
39
59
  ${options.upInner}
40
60
  },
41
61
 
42
- async down(querier: SqlQuerier): Promise<void> {
62
+ async down(querier: ${querier}): Promise<void> {
43
63
  ${options.downInner}
44
64
  },
45
65
  };
46
66
  `;
47
67
  }
68
+ /** @deprecated Use {@link buildMigrationModule}. */
69
+ export const buildSqlQuerierMigrationModule = buildMigrationModule;
@@ -50,8 +50,7 @@ export type MongoCommandTarget = {
50
50
  dropIndex(name: string): Promise<unknown>;
51
51
  };
52
52
  };
53
- /**
54
- * Execute one emitted command. The single cast lives here, where the union it casts to is defined
55
- * alongside the only code that writes these strings.
56
- */
53
+ /** Execute one emitted command. */
57
54
  export declare function runMongoCommand(db: MongoCommandTarget, statement: string): Promise<unknown>;
55
+ /** The driver call {@link runMongoCommand} makes, as source on the handle `db` names, for a generated migration. */
56
+ export declare function mongoCommandSource(statement: string, db: string): string;
@@ -2,11 +2,18 @@ export function serializeMongoCommand(command) {
2
2
  return JSON.stringify(command);
3
3
  }
4
4
  /**
5
- * Execute one emitted command. The single cast lives here, where the union it casts to is defined
5
+ * Read one emitted command back. The single cast lives here, where the union it casts to is defined
6
6
  * alongside the only code that writes these strings.
7
7
  */
8
+ function parseMongoCommand(statement) {
9
+ return JSON.parse(statement);
10
+ }
11
+ function unsupportedMongoCommand(statement) {
12
+ return new TypeError(`unsupported MongoDB migration command: ${statement}`);
13
+ }
14
+ /** Execute one emitted command. */
8
15
  export function runMongoCommand(db, statement) {
9
- const command = JSON.parse(statement);
16
+ const command = parseMongoCommand(statement);
10
17
  switch (command.action) {
11
18
  case 'createCollection':
12
19
  return db.createCollection(command.name);
@@ -21,6 +28,25 @@ export function runMongoCommand(db, statement) {
21
28
  default:
22
29
  // Unreachable for a command this module produced; a hand-written statement lands here rather
23
30
  // than being silently skipped, which is how `renameCollection` went unnoticed.
24
- throw new TypeError(`unsupported MongoDB migration command: ${statement}`);
31
+ throw unsupportedMongoCommand(statement);
32
+ }
33
+ }
34
+ /** The driver call {@link runMongoCommand} makes, as source on the handle `db` names, for a generated migration. */
35
+ export function mongoCommandSource(statement, db) {
36
+ const command = parseMongoCommand(statement);
37
+ const literal = JSON.stringify;
38
+ switch (command.action) {
39
+ case 'createCollection':
40
+ return `${db}.createCollection(${literal(command.name)})`;
41
+ case 'dropCollection':
42
+ return `${db}.collection(${literal(command.name)}).drop()`;
43
+ case 'renameCollection':
44
+ return `${db}.renameCollection(${literal(command.from)}, ${literal(command.to)})`;
45
+ case 'createIndex':
46
+ return `${db}.collection(${literal(command.collection)}).createIndex(${literal(command.key)}, ${literal(command.options)})`;
47
+ case 'dropIndex':
48
+ return `${db}.collection(${literal(command.collection)}).dropIndex(${literal(command.name)})`;
49
+ default:
50
+ throw unsupportedMongoCommand(statement);
25
51
  }
26
52
  }
@@ -13,3 +13,4 @@ export { createSchemaGenerator, SqlSchemaGenerator } from './schemaGenerator.js'
13
13
  export { createSchemaGeneratorAsync } from './schemaGeneratorAsync.js';
14
14
  export { DatabaseMigrationStorage } from './storage/databaseStorage.js';
15
15
  export { JsonMigrationStorage } from './storage/jsonStorage.js';
16
+ export { MongoMigrationStorage } from './storage/mongoStorage.js';
@@ -20,4 +20,5 @@ export { createSchemaGeneratorAsync } from './schemaGeneratorAsync.js';
20
20
  // Storage implementations
21
21
  export { DatabaseMigrationStorage } from './storage/databaseStorage.js';
22
22
  export { JsonMigrationStorage } from './storage/jsonStorage.js';
23
+ export { MongoMigrationStorage } from './storage/mongoStorage.js';
23
24
  // Schema sync
@@ -1,4 +1,4 @@
1
- import type { DialectName, LoggingOptions, Migration, MigrationDefinition, MigrationResult, MigrationStorage, MigratorDialect, MigratorOptions, MongoQuerier, Querier, QuerierPool, SchemaDiff, SchemaGenerator, SchemaIntrospector, SyncOptions, Type } from '../type/index.js';
1
+ import type { DialectName, LoggingOptions, Migration, MigrationDefinition, MigrationResult, MigrationStorage, MigratorDialect, MigratorOptions, MongoQuerier, Querier, QuerierPool, SchemaDiff, SchemaGenerator, SchemaIntrospector, SqlQuerier, SyncOptions, Type } from '../type/index.js';
2
2
  import { LoggerWrapper } from '../util/index.js';
3
3
  import type { IMigrationBuilder } from './builder/types.js';
4
4
  /**
@@ -34,11 +34,11 @@ export declare class Migrator {
34
34
  /**
35
35
  * Get all discovered migrations from the migrations directory
36
36
  */
37
- getMigrations(): Promise<Migration[]>;
37
+ getMigrations(): Promise<Migration<Querier>[]>;
38
38
  /**
39
39
  * Get list of pending migrations (not yet executed)
40
40
  */
41
- pending(): Promise<Migration[]>;
41
+ pending(): Promise<Migration<Querier>[]>;
42
42
  /**
43
43
  * Get list of executed migrations
44
44
  */
@@ -66,13 +66,23 @@ export declare class Migrator {
66
66
  */
67
67
  private runInOrder;
68
68
  /**
69
- * Run a single migration within a transaction
69
+ * Run a single migration, within a transaction where the dialect has one for it
70
70
  */
71
- runMigration(migration: Migration, direction: 'up' | 'down'): Promise<MigrationResult>;
71
+ runMigration(migration: Migration<Querier>, direction: 'up' | 'down'): Promise<MigrationResult>;
72
+ /**
73
+ * A migration querier, and how to run work in one transaction on it. MongoDB gets none: it creates
74
+ * collections and indexes outside any transaction. SQL asserts its querier before opening one, so a
75
+ * wrong querier reports which one the dialect needs rather than a missing `transaction`.
76
+ */
77
+ private withMigrationQuerier;
78
+ /** What this dialect's migration files are written against. */
79
+ private get migrationQuerier();
72
80
  /**
73
81
  * Generate a new migration file
74
82
  */
75
83
  generate(name: string): Promise<string>;
84
+ /** Writes a migration module on this dialect's querier to a new timestamped file, and returns its path. */
85
+ private writeMigration;
76
86
  /**
77
87
  * Generate a migration based on entity schema differences
78
88
  */
@@ -174,11 +184,11 @@ export declare class Migrator {
174
184
  /**
175
185
  * Load a migration from a file
176
186
  */
177
- loadMigration(fileName: string): Promise<Migration | undefined>;
187
+ loadMigration(fileName: string): Promise<Migration<Querier> | undefined>;
178
188
  /**
179
189
  * Check if an object is a valid migration
180
190
  */
181
- isMigration(obj: unknown): obj is MigrationDefinition;
191
+ isMigration(obj: unknown): obj is MigrationDefinition<Querier>;
182
192
  /**
183
193
  * Extract migration name from filename
184
194
  */
@@ -195,14 +205,15 @@ export declare class Migrator {
195
205
  /**
196
206
  * Helper function to define a migration with proper typing
197
207
  */
198
- export declare function defineMigration(migration: MigrationDefinition): MigrationDefinition;
208
+ export declare function defineMigration<Q extends Querier = SqlQuerier>(migration: MigrationDefinition<Q>): MigrationDefinition<Q>;
199
209
  /**
200
- * Migration definition that uses the type-safe builder API.
210
+ * Migration definition that uses the type-safe builder API. The querier is the builder's own, so a
211
+ * data backfill it runs lands in the same transaction as the schema change.
201
212
  */
202
213
  export interface BuilderMigrationDefinition {
203
214
  readonly name?: string;
204
- readonly up: (builder: IMigrationBuilder) => Promise<void>;
205
- readonly down: (builder: IMigrationBuilder) => Promise<void>;
215
+ up(builder: IMigrationBuilder, querier: SqlQuerier): Promise<void>;
216
+ down(builder: IMigrationBuilder, querier: SqlQuerier): Promise<void>;
206
217
  }
207
218
  /**
208
219
  * Define a migration using the type-safe builder API.
@@ -223,4 +234,4 @@ export interface BuilderMigrationDefinition {
223
234
  * });
224
235
  * ```
225
236
  */
226
- export declare function defineBuilderMigration(migration: BuilderMigrationDefinition): BuilderMigrationDefinition;
237
+ export declare function defineBuilderMigration(migration: BuilderMigrationDefinition): MigrationDefinition;
@@ -3,14 +3,16 @@ import { basename, extname, join } from 'node:path';
3
3
  import { pathToFileURL } from 'node:url';
4
4
  import { getEntities, getMeta } from '../entity/index.js';
5
5
  import { introspectSchema, SchemaAST } from '../schema/index.js';
6
- import { isKnownMigratorDialect, isSqlQuerier } from '../type/index.js';
6
+ import { isKnownMigratorDialect, isMongoQuerier, isSqlQuerier } from '../type/index.js';
7
7
  import { LoggerWrapper } from '../util/index.js';
8
- import { withQuerierForMigrations, withSqlQuerierForMigrations } from './acquireQuerierForMigrations.js';
9
- import { buildSqlQuerierMigrationModule, EMPTY_MANUAL_MIGRATION_DOWN_INNER, EMPTY_MANUAL_MIGRATION_UP_INNER, emitSqlRunCalls, } from './codegen/migrationFile.js';
8
+ import { withMongoQuerierForMigrations, withSqlQuerierForMigrations } from './acquireQuerierForMigrations.js';
9
+ import { MigrationBuilder } from './builder/migrationBuilder.js';
10
+ import { buildMigrationModule, migrationSource, } from './codegen/migrationFile.js';
10
11
  import { runMongoCommand } from './generator/mongoCommand.js';
11
12
  import { introspectorFor } from './introspection/registry.js';
12
13
  import { createSchemaGenerator } from './schemaGenerator.js';
13
14
  import { DatabaseMigrationStorage } from './storage/databaseStorage.js';
15
+ import { MongoMigrationStorage } from './storage/mongoStorage.js';
14
16
  /**
15
17
  * Main class for managing database migrations
16
18
  */
@@ -40,9 +42,9 @@ export class Migrator {
40
42
  this._defaultForeignKeyAction = options.defaultForeignKeyAction;
41
43
  this.storage =
42
44
  options.storage ??
43
- new DatabaseMigrationStorage(pool, {
44
- tableName: options.tableName,
45
- });
45
+ (this.dialectName === 'mongodb'
46
+ ? new MongoMigrationStorage(pool, { tableName: options.tableName })
47
+ : new DatabaseMigrationStorage(pool, { tableName: options.tableName }));
46
48
  this.migrationsPath = options.migrationsPath ?? './migrations';
47
49
  this._logger = new LoggerWrapper(options.logger, { logValues: options.logValues, slowQuery: options.slowQuery });
48
50
  this._entities = options.entities;
@@ -153,22 +155,20 @@ export class Migrator {
153
155
  return results;
154
156
  }
155
157
  /**
156
- * Run a single migration within a transaction
158
+ * Run a single migration, within a transaction where the dialect has one for it
157
159
  */
158
160
  async runMigration(migration, direction) {
159
161
  const startTime = Date.now();
160
- return withSqlQuerierForMigrations(this.pool, 'Migrator', async (querier) => {
162
+ return this.withMigrationQuerier(async (querier, inTransaction) => {
161
163
  try {
162
164
  this.logger.logMigration(`${direction === 'up' ? 'Running' : 'Reverting'} migration: ${migration.name}`);
163
- await querier.transaction(async () => {
165
+ await inTransaction(async () => {
164
166
  if (direction === 'up') {
165
167
  await migration.up(querier);
166
- // Log within the same transaction
167
168
  await this.storage.logWithQuerier(querier, migration.name);
168
169
  }
169
170
  else {
170
171
  await migration.down(querier);
171
- // Unlog within the same transaction
172
172
  await this.storage.unlogWithQuerier(querier, migration.name);
173
173
  }
174
174
  });
@@ -194,22 +194,40 @@ export class Migrator {
194
194
  }
195
195
  });
196
196
  }
197
+ /**
198
+ * A migration querier, and how to run work in one transaction on it. MongoDB gets none: it creates
199
+ * collections and indexes outside any transaction. SQL asserts its querier before opening one, so a
200
+ * wrong querier reports which one the dialect needs rather than a missing `transaction`.
201
+ */
202
+ withMigrationQuerier(task) {
203
+ return this.dialectName === 'mongodb'
204
+ ? withMongoQuerierForMigrations(this.pool, 'Migrator', (querier) => task(querier, (work) => work()))
205
+ : withSqlQuerierForMigrations(this.pool, 'Migrator', (querier) => task(querier, (work) => querier.transaction(work)));
206
+ }
207
+ /** What this dialect's migration files are written against. */
208
+ get migrationQuerier() {
209
+ return this.dialectName === 'mongodb' ? 'MongoQuerier' : 'SqlQuerier';
210
+ }
197
211
  /**
198
212
  * Generate a new migration file
199
213
  */
200
214
  async generate(name) {
201
- const timestamp = this.getTimestamp();
202
- const fileName = `${timestamp}_${this.slugify(name)}.ts`;
203
- const filePath = join(this.migrationsPath, fileName);
204
- const content = buildSqlQuerierMigrationModule({
215
+ const { emptyUp, emptyDown } = migrationSource[this.migrationQuerier];
216
+ const filePath = await this.writeMigration(name, { upInner: emptyUp, downInner: emptyDown });
217
+ this.logger.logInfo(`Created migration: ${filePath}`);
218
+ return filePath;
219
+ }
220
+ /** Writes a migration module on this dialect's querier to a new timestamped file, and returns its path. */
221
+ async writeMigration(name, body) {
222
+ const filePath = join(this.migrationsPath, `${this.getTimestamp()}_${this.slugify(name)}.ts`);
223
+ const content = buildMigrationModule({
205
224
  migrationName: name,
206
225
  createdAt: new Date(),
207
- upInner: EMPTY_MANUAL_MIGRATION_UP_INNER,
208
- downInner: EMPTY_MANUAL_MIGRATION_DOWN_INNER,
226
+ querier: this.migrationQuerier,
227
+ ...body,
209
228
  });
210
229
  await mkdir(this.migrationsPath, { recursive: true });
211
230
  await writeFile(filePath, content, 'utf-8');
212
- this.logger.logInfo(`Created migration: ${filePath}`);
213
231
  return filePath;
214
232
  }
215
233
  /**
@@ -236,19 +254,12 @@ export class Migrator {
236
254
  this.logger.logInfo('No schema changes detected.');
237
255
  return '';
238
256
  }
239
- const timestamp = this.getTimestamp();
240
- const fileName = `${timestamp}_${this.slugify(name)}.ts`;
241
- const filePath = join(this.migrationsPath, fileName);
242
- const down = [...downStatements].reverse();
243
- const content = buildSqlQuerierMigrationModule({
244
- migrationName: name,
245
- createdAt: new Date(),
257
+ const { emit } = migrationSource[this.migrationQuerier];
258
+ const filePath = await this.writeMigration(name, {
246
259
  docExtraLines: ['Generated from entity definitions'],
247
- upInner: emitSqlRunCalls(upStatements),
248
- downInner: emitSqlRunCalls(down),
260
+ upInner: emit(upStatements),
261
+ downInner: emit([...downStatements].reverse()),
249
262
  });
250
- await mkdir(this.migrationsPath, { recursive: true });
251
- await writeFile(filePath, content, 'utf-8');
252
263
  this.logger.logInfo(`Created migration from entities: ${filePath}`);
253
264
  return filePath;
254
265
  }
@@ -474,15 +485,9 @@ export class Migrator {
474
485
  return filteredDiff;
475
486
  }
476
487
  async executeSyncStatements(statements, options) {
477
- // Mongo creates collections and indexes outside any transaction, so only the SQL path opens one -
478
- // and asks for a SQL querier before it opens it, since `transaction` is what a Mongo one lacks and
479
- // reaching for it first reports that instead of which querier the dialect needs.
480
- if (this.dialectName === 'mongodb') {
481
- await withQuerierForMigrations(this.pool, (querier) => this.executeMongoSyncStatements(statements, options, querier));
482
- }
483
- else {
484
- await withSqlQuerierForMigrations(this.pool, 'Migrator', (querier) => querier.transaction(() => this.executeSqlSyncStatements(statements, options, querier)));
485
- }
488
+ await this.withMigrationQuerier((querier, inTransaction) => inTransaction(() => isMongoQuerier(querier)
489
+ ? this.executeMongoSyncStatements(statements, options, querier)
490
+ : this.executeSqlSyncStatements(statements, options, querier)));
486
491
  if (options.logging)
487
492
  this.logger.logSchema('Schema synchronization completed');
488
493
  }
@@ -618,5 +623,9 @@ export function defineMigration(migration) {
618
623
  * ```
619
624
  */
620
625
  export function defineBuilderMigration(migration) {
621
- return migration;
626
+ return {
627
+ ...migration,
628
+ up: (querier) => migration.up(new MigrationBuilder(querier), querier),
629
+ down: (querier) => migration.down(new MigrationBuilder(querier), querier),
630
+ };
622
631
  }
@@ -1,4 +1,4 @@
1
- import type { MigrationStorage, SqlQuerier } from '../../type/index.js';
1
+ import type { MigrationStorage, Querier } from '../../type/index.js';
2
2
  /**
3
3
  * Stores migration state in a JSON file.
4
4
  * Useful for development or environments without a database.
@@ -9,7 +9,7 @@ export declare class JsonMigrationStorage implements MigrationStorage {
9
9
  constructor(filePath?: string);
10
10
  ensureStorage(): Promise<void>;
11
11
  executed(): Promise<string[]>;
12
- logWithQuerier(_querier: SqlQuerier, migrationName: string): Promise<void>;
13
- unlogWithQuerier(_querier: SqlQuerier, migrationName: string): Promise<void>;
12
+ logWithQuerier(_querier: Querier, migrationName: string): Promise<void>;
13
+ unlogWithQuerier(_querier: Querier, migrationName: string): Promise<void>;
14
14
  private save;
15
15
  }
@@ -0,0 +1,17 @@
1
+ import type { MigrationStorage, MongoQuerier, QuerierPool } from '../../type/index.js';
2
+ /**
3
+ * Stores migration state in a MongoDB collection, named as the SQL table would be.
4
+ */
5
+ export declare class MongoMigrationStorage implements MigrationStorage {
6
+ private readonly pool;
7
+ private readonly collectionName;
8
+ constructor(pool: QuerierPool, options?: {
9
+ tableName?: string;
10
+ });
11
+ /** Nothing to prepare: MongoDB creates the collection on its first insert. */
12
+ ensureStorage(): Promise<void>;
13
+ executed(): Promise<string[]>;
14
+ logWithQuerier(querier: MongoQuerier, migrationName: string): Promise<void>;
15
+ unlogWithQuerier(querier: MongoQuerier, migrationName: string): Promise<void>;
16
+ private collection;
17
+ }
@@ -0,0 +1,32 @@
1
+ import { withMongoQuerierForMigrations } from '../acquireQuerierForMigrations.js';
2
+ import { DEFAULT_MIGRATIONS_TABLE } from './databaseStorage.js';
3
+ /**
4
+ * Stores migration state in a MongoDB collection, named as the SQL table would be.
5
+ */
6
+ export class MongoMigrationStorage {
7
+ pool;
8
+ collectionName;
9
+ constructor(pool, options = {}) {
10
+ this.pool = pool;
11
+ this.collectionName = options.tableName ?? DEFAULT_MIGRATIONS_TABLE;
12
+ }
13
+ /** Nothing to prepare: MongoDB creates the collection on its first insert. */
14
+ async ensureStorage() { }
15
+ executed() {
16
+ return withMongoQuerierForMigrations(this.pool, 'MongoMigrationStorage', async (querier) => {
17
+ const documents = await this.collection(querier)
18
+ .find({}, { sort: { _id: 1 } })
19
+ .toArray();
20
+ return documents.map((document) => document._id);
21
+ });
22
+ }
23
+ async logWithQuerier(querier, migrationName) {
24
+ await this.collection(querier).insertOne({ _id: migrationName, executed_at: new Date() });
25
+ }
26
+ async unlogWithQuerier(querier, migrationName) {
27
+ await this.collection(querier).deleteOne({ _id: migrationName });
28
+ }
29
+ collection(querier) {
30
+ return querier.db.collection(this.collectionName);
31
+ }
32
+ }
@@ -878,6 +878,13 @@ type EntityRelationOptions<E> = {
878
878
  */
879
879
  export type EntityOptions<E = unknown> = {
880
880
  readonly name?: string;
881
+ /**
882
+ * The base to inherit fields, relations, hooks and filters from, for a class that cannot extend one
883
+ * (minted at runtime, or its base chosen from data): the merge `class Child extends Base` does, with
884
+ * the class's real base nearer, so it wins. Checked against whatever properties the entity declares,
885
+ * which a class behind an index signature has none of. See the Inheritance guide.
886
+ */
887
+ readonly extends?: string extends keyof E ? Type<object> : Type<Partial<E>>;
881
888
  /**
882
889
  * The schema (in MySQL terms, database) this table lives in, pinning it whichever pool reads it;
883
890
  * unset follows the pool's own. Not in `name`: a dotted `name` is rejected.
@@ -3,19 +3,19 @@ import type { FullColumnDefinition, TableDefinition } from '../migrate/builder/t
3
3
  import type { IndexFacet } from '../schema/indexDifferences.js';
4
4
  import type { SchemaAST } from '../schema/schemaAST.js';
5
5
  import type { ColumnNode, ForeignKeyAction, IndexNode, IndexType, TableNode } from '../schema/types.js';
6
- import type { EntityMeta, FieldOptions, IndexColumnSchema, LoggingOptions, SqlQuerier, Type, VectorIndexOptions } from './index.js';
6
+ import type { EntityMeta, FieldOptions, IndexColumnSchema, LoggingOptions, Querier, SqlQuerier, Type, VectorIndexOptions } from './index.js';
7
7
  /**
8
- * Defines a migration using a simple object literal
8
+ * Defines a migration using a simple object literal. `Q` is `MongoQuerier` for a MongoDB migration.
9
9
  */
10
- export interface MigrationDefinition {
10
+ export interface MigrationDefinition<Q extends Querier = SqlQuerier> {
11
11
  readonly name?: string;
12
- readonly up: (querier: SqlQuerier) => Promise<void>;
13
- readonly down: (querier: SqlQuerier) => Promise<void>;
12
+ up(querier: Q): Promise<void>;
13
+ down(querier: Q): Promise<void>;
14
14
  }
15
15
  /**
16
16
  * Represents a single database migration
17
17
  */
18
- export interface Migration extends MigrationDefinition {
18
+ export interface Migration<Q extends Querier = SqlQuerier> extends MigrationDefinition<Q> {
19
19
  /**
20
20
  * Unique name/identifier for this migration (typically timestamp + description)
21
21
  */
@@ -30,13 +30,13 @@ export interface MigrationStorage {
30
30
  */
31
31
  executed(): Promise<string[]>;
32
32
  /**
33
- * Mark a migration as executed (called within migration transaction)
33
+ * Mark a migration as executed, on the querier that ran it (inside its transaction, where there is one)
34
34
  */
35
- logWithQuerier(querier: SqlQuerier, migrationName: string): Promise<void>;
35
+ logWithQuerier(querier: Querier, migrationName: string): Promise<void>;
36
36
  /**
37
- * Remove a migration from the executed list (called within migration transaction)
37
+ * Remove a migration from the executed list, on the querier that reverted it
38
38
  */
39
- unlogWithQuerier(querier: SqlQuerier, migrationName: string): Promise<void>;
39
+ unlogWithQuerier(querier: Querier, migrationName: string): Promise<void>;
40
40
  /**
41
41
  * Ensure the storage is initialized (e.g., create migrations table)
42
42
  */
@@ -51,11 +51,11 @@ export interface MigratorOptions {
51
51
  */
52
52
  readonly migrationsPath?: string;
53
53
  /**
54
- * Custom storage implementation. Defaults to DatabaseMigrationStorage.
54
+ * Custom storage implementation. Defaults to DatabaseMigrationStorage, or MongoMigrationStorage on MongoDB.
55
55
  */
56
56
  readonly storage?: MigrationStorage;
57
57
  /**
58
- * Table name for storing migration state. Defaults to 'uql_migrations'.
58
+ * Table, or MongoDB collection, name for storing migration state. Defaults to 'uql_migrations'.
59
59
  */
60
60
  readonly tableName?: string;
61
61
  /**
@@ -144,6 +144,11 @@ export interface MongoQuerier extends Querier {
144
144
  */
145
145
  readonly db: Db;
146
146
  }
147
+ /**
148
+ * Type guard for a querier over a MongoDB database. A handle alone does not tell: the SQLite, D1 and
149
+ * Turso queriers carry a `db` of their own.
150
+ */
151
+ export declare function isMongoQuerier(querier: Querier): querier is MongoQuerier;
147
152
  /**
148
153
  * Context passed to global querier listeners.
149
154
  */
@@ -8,3 +8,10 @@ export function isSqlQuerier(querier) {
8
8
  q.dialect !== undefined &&
9
9
  typeof q.dialect.escapeIdChar === 'string');
10
10
  }
11
+ /**
12
+ * Type guard for a querier over a MongoDB database. A handle alone does not tell: the SQLite, D1 and
13
+ * Turso queriers carry a `db` of their own.
14
+ */
15
+ export function isMongoQuerier(querier) {
16
+ return 'db' in querier && !isSqlQuerier(querier);
17
+ }
package/package.json CHANGED
@@ -3,7 +3,7 @@
3
3
  "homepage": "https://uql-orm.dev",
4
4
  "description": "The JSON-native TypeScript ORM for Bun, Browsers, Edge, Deno, Node, Workers. Supports PostgreSQL, PGlite, MySQL, MariaDB, SQLite, CockroachDB, SQL Server, Turso, Neon, Cloudflare D1 and MongoDB. Queries are plain JSON, typed to the leaf.",
5
5
  "license": "MIT",
6
- "version": "0.58.0",
6
+ "version": "0.60.0",
7
7
  "type": "module",
8
8
  "engines": {
9
9
  "node": ">=24"