@danceroutine/tango-migrations 1.11.4 → 1.11.6

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.
@@ -7,6 +7,7 @@ import { extname, resolve } from "node:path";
7
7
  import { pathToFileURL } from "node:url";
8
8
  import { createJiti } from "jiti";
9
9
  import { ModelRegistry } from "@danceroutine/tango-schema";
10
+ import { webcrypto } from "node:crypto";
10
11
  //#region src/runtime/loadModule.ts
11
12
  const TS_EXTENSIONS = new Set([
12
13
  ".ts",
@@ -103,7 +104,11 @@ var MigrationRunner = class MigrationRunner {
103
104
  const migrations = await this.loadMigrations();
104
105
  for (const migration of migrations) {
105
106
  if (toId && migration.id > toId) break;
106
- if (applied.has(migration.id)) continue;
107
+ const storedChecksum = applied.get(migration.id);
108
+ if (storedChecksum !== void 0) {
109
+ await this.assertAppliedChecksum(migration, storedChecksum);
110
+ continue;
111
+ }
107
112
  await this.applyMigration(migration);
108
113
  }
109
114
  }
@@ -132,10 +137,17 @@ var MigrationRunner = class MigrationRunner {
132
137
  */
133
138
  async status() {
134
139
  const applied = await this.listApplied();
135
- return (await this.loadMigrations()).map((m) => ({
136
- id: m.id,
137
- applied: applied.has(m.id)
138
- }));
140
+ const migrations = await this.loadMigrations();
141
+ const statuses = [];
142
+ for (const migration of migrations) {
143
+ const storedChecksum = applied.get(migration.id);
144
+ if (storedChecksum !== void 0) await this.assertAppliedChecksum(migration, storedChecksum);
145
+ statuses.push({
146
+ id: migration.id,
147
+ applied: storedChecksum !== void 0
148
+ });
149
+ }
150
+ return statuses;
139
151
  }
140
152
  async ensureJournal() {
141
153
  const sql = this.dialect === InternalDialect.POSTGRES ? `CREATE TABLE IF NOT EXISTS "${JOURNAL}" (
@@ -151,8 +163,8 @@ var MigrationRunner = class MigrationRunner {
151
163
  }
152
164
  async listApplied() {
153
165
  const table = this.dialect === InternalDialect.POSTGRES ? `"${JOURNAL}"` : JOURNAL;
154
- const { rows } = await this.client.query(`SELECT id FROM ${table}`);
155
- return new Set(rows.map((r) => r.id));
166
+ const { rows } = await this.client.query(`SELECT id, checksum FROM ${table}`);
167
+ return new Map(rows.map((record) => [record.id, record.checksum]));
156
168
  }
157
169
  async loadMigrations() {
158
170
  const files = (await readdir(this.migrationsDir)).filter((f) => f.endsWith(".ts") || f.endsWith(".js")).sort();
@@ -182,7 +194,7 @@ var MigrationRunner = class MigrationRunner {
182
194
  const builder = new CollectingBuilder();
183
195
  await migration.up(builder);
184
196
  const sqls = this.compilerStrategy.prepareOperations(this.dialect, builder.ops).flatMap((op) => this.compileOperation(op));
185
- const checksum = String(this.hashJSON(builder.ops));
197
+ const checksum = await this.calculateBuilderChecksum(builder);
186
198
  const isOnline = (migration.mode ?? builder.getMode()) === "online";
187
199
  if (!isOnline && this.dialect === InternalDialect.POSTGRES) await this.client.query("BEGIN");
188
200
  try {
@@ -197,20 +209,35 @@ var MigrationRunner = class MigrationRunner {
197
209
  throw error;
198
210
  }
199
211
  }
212
+ async assertAppliedChecksum(migration, storedChecksum) {
213
+ const checksums = await this.calculateChecksums(migration);
214
+ if (checksums.current !== storedChecksum && checksums.legacyOperations !== storedChecksum) throw new Error(`Applied migration '${migration.id}' checksum mismatch. The migration file may have been modified after it was applied.`);
215
+ }
216
+ async calculateChecksums(migration) {
217
+ const builder = new CollectingBuilder();
218
+ await migration.up(builder);
219
+ return {
220
+ current: await this.calculateBuilderChecksum(builder),
221
+ legacyOperations: await this.calculateLegacyOperationsChecksum(builder)
222
+ };
223
+ }
224
+ async calculateBuilderChecksum(builder) {
225
+ return this.hashPayload({
226
+ operations: builder.ops,
227
+ dataFns: builder.dataFns.map((fn) => fn.toString())
228
+ });
229
+ }
230
+ async calculateLegacyOperationsChecksum(builder) {
231
+ return this.hashPayload(builder.ops);
232
+ }
200
233
  /**
201
- * Compute a simple hash of the migration's operation list.
234
+ * Compute a SHA-256 digest of a migration checksum payload.
202
235
  * Stored alongside each applied migration in the journal table to detect
203
236
  * if a migration file has been modified after it was already applied.
204
- * Uses a djb2-like hash over the JSON-serialized operations.
205
237
  */
206
- hashJSON(x) {
207
- const s = JSON.stringify(x);
208
- let h = 0;
209
- for (let i = 0; i < s.length; i++) {
210
- h = Math.imul(31, h) + s.charCodeAt(i);
211
- h = h | 0;
212
- }
213
- return h >>> 0;
238
+ async hashPayload(payload) {
239
+ const digest = await webcrypto.subtle.digest("SHA-256", Buffer.from(JSON.stringify(payload), "utf8"));
240
+ return Buffer.from(digest).toString("hex");
214
241
  }
215
242
  compileOperation(op) {
216
243
  return this.compilerStrategy.compile(this.dialect, op);
@@ -219,4 +246,4 @@ var MigrationRunner = class MigrationRunner {
219
246
  //#endregion
220
247
  export { loadModule as n, MigrationRunner as t };
221
248
 
222
- //# sourceMappingURL=MigrationRunner-vG1lVlkz.js.map
249
+ //# sourceMappingURL=MigrationRunner-B9F-yxGV.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"MigrationRunner-B9F-yxGV.js","names":[],"sources":["../src/runtime/loadModule.ts","../src/runner/MigrationRunner.ts"],"sourcesContent":["import { resolve, extname } from 'node:path';\nimport { pathToFileURL } from 'node:url';\nimport { createJiti } from 'jiti';\nimport { ModelRegistry } from '@danceroutine/tango-schema';\n\nconst TS_EXTENSIONS = new Set(['.ts', '.tsx', '.mts', '.cts']);\n\nfunction toAbsolutePath(modulePath: string, projectRoot: string): string {\n return resolve(projectRoot, modulePath);\n}\n\nfunction isTypeScriptModule(modulePath: string): boolean {\n return TS_EXTENSIONS.has(extname(modulePath).toLowerCase());\n}\n\n/**\n * Load a module from a Tango app project root.\n *\n * TypeScript modules are loaded through jiti and JavaScript/ESM modules are loaded\n * through native dynamic import so published installs behave like end-user runtime.\n */\nexport async function loadModule(\n modulePath: string,\n options?: { projectRoot?: string; registry?: ModelRegistry; moduleCache?: boolean }\n): Promise<Record<string, unknown>> {\n const projectRoot = options?.projectRoot ?? process.cwd();\n const absolutePath = toAbsolutePath(modulePath, projectRoot);\n const executeImport = async (): Promise<Record<string, unknown>> => {\n if (isTypeScriptModule(absolutePath)) {\n const jiti = createJiti(resolve(projectRoot, 'tango.config.ts'), {\n interopDefault: true,\n moduleCache: options?.moduleCache ?? true,\n });\n return (await jiti.import<Record<string, unknown>>(absolutePath)) as Record<string, unknown>;\n }\n\n return (await import(pathToFileURL(absolutePath).href)) as Record<string, unknown>;\n };\n\n if (options?.registry) {\n return ModelRegistry.runWithRegistry(options.registry, executeImport);\n }\n\n return executeImport();\n}\n\n/**\n * Load a module and return default export when present.\n */\nexport async function loadDefaultExport(modulePath: string, options?: { projectRoot?: string }): Promise<unknown> {\n const loaded = await loadModule(modulePath, options);\n return loaded.default ?? loaded;\n}\n","import { CollectingBuilder } from '../builder/runtime/CollectingBuilder';\nimport type { Dialect } from '../domain/Dialect';\nimport { Migration } from '../domain/Migration';\nimport type { SQL } from '../compilers/contracts/SQL';\nimport type { MigrationOperation } from '../domain/MigrationOperation';\nimport type { CompilerStrategy } from '../strategies/CompilerStrategy';\nimport { createDefaultCompilerStrategy } from '../strategies/CompilerStrategy';\nimport { InternalDialect } from '../domain/internal/InternalDialect';\nimport { isError } from '@danceroutine/tango-core';\nimport { readdir } from 'node:fs/promises';\nimport { resolve } from 'node:path';\nimport { loadDefaultExport } from '../runtime/loadModule';\nimport { webcrypto } from 'node:crypto';\n\nconst JOURNAL = '_tango_migrations';\n\n/** DB client contract required by migration execution. */\ninterface DBClient {\n /** Execute SQL with optional parameters. */\n query<T = unknown>(sql: string, params?: readonly unknown[]): Promise<{ rows: T[] }>;\n /** Release underlying database resources. */\n close(): Promise<void>;\n}\n\ninterface AppliedMigrationRecord {\n id: string;\n checksum: string;\n}\n\ninterface MigrationChecksums {\n current: string;\n legacyOperations: string;\n}\n\n/**\n * Manages the lifecycle of database migrations: applying, planning, and tracking status.\n *\n * The runner reads migration files from a directory, compiles operations to SQL\n * for the target dialect, and maintains a journal table to track which migrations\n * have been applied. Each applied migration is checksummed to detect tampering.\n *\n * @example\n * ```typescript\n * const runner = new MigrationRunner(client, 'postgres', './migrations');\n *\n * // Apply all pending migrations\n * await runner.apply();\n *\n * // Apply up to a specific migration\n * await runner.apply('003_add_indexes');\n *\n * // Preview the SQL that would be generated\n * const sql = await runner.plan();\n *\n * // Check which migrations are applied\n * const statuses = await runner.status();\n * ```\n */\nexport class MigrationRunner {\n static readonly BRAND = 'tango.migrations.runner' as const;\n readonly __tangoBrand: typeof MigrationRunner.BRAND = MigrationRunner.BRAND;\n private compilerStrategy: CompilerStrategy;\n\n constructor(\n private client: DBClient,\n private dialect: Dialect,\n private migrationsDir: string = 'migrations',\n compilerStrategy?: CompilerStrategy\n ) {\n this.compilerStrategy = compilerStrategy ?? createDefaultCompilerStrategy();\n }\n\n /**\n * Narrow an unknown value to `MigrationRunner`.\n */\n static isMigrationRunner(value: unknown): value is MigrationRunner {\n return (\n typeof value === 'object' &&\n value !== null &&\n (value as { __tangoBrand?: unknown }).__tangoBrand === MigrationRunner.BRAND\n );\n }\n\n /**\n * Apply all pending migrations, optionally stopping at a specific migration ID.\n * Migrations are applied in file-sort order. Already-applied migrations are skipped.\n * Non-online migrations are wrapped in a transaction on Postgres.\n */\n async apply(toId?: string): Promise<void> {\n await this.ensureJournal();\n const applied = await this.listApplied();\n const migrations = await this.loadMigrations();\n\n for (const migration of migrations) {\n if (toId && migration.id > toId) {\n break;\n }\n const storedChecksum = applied.get(migration.id);\n if (storedChecksum !== undefined) {\n await this.assertAppliedChecksum(migration, storedChecksum);\n continue;\n }\n\n await this.applyMigration(migration);\n }\n }\n\n /**\n * Generate a dry-run SQL plan for all migrations without executing anything.\n * Useful for reviewing what SQL would be run before applying.\n */\n async plan(): Promise<string> {\n const migrations = await this.loadMigrations();\n let output = '';\n\n migrations.forEach((migration) => {\n const builder = new CollectingBuilder();\n migration.up(builder);\n const preparedOps = this.compilerStrategy.prepareOperations(this.dialect, builder.ops);\n const sqls = preparedOps.flatMap((op) => this.compileOperation(op));\n\n output += `# ${migration.id}\\n`;\n sqls.forEach((statement) => {\n output += statement.sql + ';\\n';\n });\n if (builder.dataFns.length) {\n output += '-- (data step present)\\n';\n }\n output += '\\n';\n });\n\n return output;\n }\n\n /**\n * Return the applied/pending status of every migration found on disk.\n */\n async status(): Promise<{ id: string; applied: boolean }[]> {\n const applied = await this.listApplied();\n const migrations = await this.loadMigrations();\n\n const statuses: { id: string; applied: boolean }[] = [];\n\n for (const migration of migrations) {\n const storedChecksum = applied.get(migration.id);\n if (storedChecksum !== undefined) {\n await this.assertAppliedChecksum(migration, storedChecksum);\n }\n\n statuses.push({\n id: migration.id,\n applied: storedChecksum !== undefined,\n });\n }\n\n return statuses;\n }\n\n private async ensureJournal(): Promise<void> {\n const sql =\n this.dialect === InternalDialect.POSTGRES\n ? `CREATE TABLE IF NOT EXISTS \"${JOURNAL}\" (\n id TEXT PRIMARY KEY,\n applied_at TIMESTAMPTZ NOT NULL DEFAULT now(),\n checksum TEXT NOT NULL\n )`\n : `CREATE TABLE IF NOT EXISTS ${JOURNAL} (\n id TEXT PRIMARY KEY,\n applied_at TEXT NOT NULL DEFAULT (datetime('now')),\n checksum TEXT NOT NULL\n )`;\n\n await this.client.query(sql);\n }\n\n private async listApplied(): Promise<Map<string, string>> {\n const table = this.dialect === InternalDialect.POSTGRES ? `\"${JOURNAL}\"` : JOURNAL;\n const { rows } = await this.client.query<AppliedMigrationRecord>(`SELECT id, checksum FROM ${table}`);\n return new Map(rows.map((record) => [record.id, record.checksum]));\n }\n\n private async loadMigrations(): Promise<Migration[]> {\n const files = (await readdir(this.migrationsDir)).filter((f) => f.endsWith('.ts') || f.endsWith('.js')).sort();\n\n const migrations: Migration[] = [];\n\n for (const file of files) {\n const absolutePath = resolve(this.migrationsDir, file);\n let loaded: unknown;\n try {\n loaded = await loadDefaultExport(absolutePath, { projectRoot: process.cwd() });\n } catch (error) {\n const reason = isError(error) ? error.message : String(error);\n throw new Error(`Failed to load migration module '${file}': ${reason}`, { cause: error });\n }\n\n if (Migration.isMigration(loaded)) {\n migrations.push(loaded);\n continue;\n }\n\n if (Migration.isMigrationConstructor(loaded)) {\n migrations.push(new loaded());\n continue;\n }\n\n throw new Error(\n `Invalid migration module '${file}'. Default export must be a Migration subclass or instance.`\n );\n }\n\n return migrations;\n }\n\n private async applyMigration(migration: Migration): Promise<void> {\n const builder = new CollectingBuilder();\n await migration.up(builder);\n\n const preparedOps = this.compilerStrategy.prepareOperations(this.dialect, builder.ops);\n const sqls = preparedOps.flatMap((op) => this.compileOperation(op));\n const checksum = await this.calculateBuilderChecksum(builder);\n\n const isOnline = (migration.mode ?? builder.getMode()) === 'online';\n\n if (!isOnline && this.dialect === InternalDialect.POSTGRES) {\n await this.client.query('BEGIN');\n }\n\n try {\n for (const statement of sqls) {\n await this.client.query(statement.sql, statement.params);\n }\n\n for (const fn of builder.dataFns) {\n await fn({ query: (sql, params) => this.client.query(sql, params).then(() => {}) });\n }\n\n const table = this.dialect === InternalDialect.POSTGRES ? `\"${JOURNAL}\"` : JOURNAL;\n const placeholder = this.dialect === InternalDialect.POSTGRES ? '$1, $2' : '?, ?';\n await this.client.query(`INSERT INTO ${table} (id, checksum) VALUES (${placeholder})`, [\n migration.id,\n checksum,\n ]);\n\n if (!isOnline && this.dialect === InternalDialect.POSTGRES) {\n await this.client.query('COMMIT');\n }\n } catch (error) {\n if (!isOnline && this.dialect === InternalDialect.POSTGRES) {\n await this.client.query('ROLLBACK');\n }\n throw error;\n }\n }\n\n private async assertAppliedChecksum(migration: Migration, storedChecksum: string): Promise<void> {\n const checksums = await this.calculateChecksums(migration);\n\n // Older Tango versions stored operation-only checksums. Keep those\n // journals readable while writing data-aware checksums for new rows.\n if (checksums.current !== storedChecksum && checksums.legacyOperations !== storedChecksum) {\n throw new Error(\n `Applied migration '${migration.id}' checksum mismatch. The migration file may have been modified after it was applied.`\n );\n }\n }\n\n private async calculateChecksums(migration: Migration): Promise<MigrationChecksums> {\n const builder = new CollectingBuilder();\n await migration.up(builder);\n\n return {\n current: await this.calculateBuilderChecksum(builder),\n legacyOperations: await this.calculateLegacyOperationsChecksum(builder),\n };\n }\n\n private async calculateBuilderChecksum(builder: CollectingBuilder): Promise<string> {\n return this.hashPayload({\n operations: builder.ops,\n dataFns: builder.dataFns.map((fn) => fn.toString()),\n });\n }\n\n private async calculateLegacyOperationsChecksum(builder: CollectingBuilder): Promise<string> {\n return this.hashPayload(builder.ops);\n }\n\n /**\n * Compute a SHA-256 digest of a migration checksum payload.\n * Stored alongside each applied migration in the journal table to detect\n * if a migration file has been modified after it was already applied.\n */\n private async hashPayload(payload: unknown): Promise<string> {\n const digest = await webcrypto.subtle.digest('SHA-256', Buffer.from(JSON.stringify(payload), 'utf8'));\n return Buffer.from(digest).toString('hex');\n }\n\n private compileOperation(op: MigrationOperation): SQL[] {\n return this.compilerStrategy.compile(this.dialect, op);\n }\n}\n"],"mappings":"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"}
@@ -1,5 +1,5 @@
1
1
  import { r as InternalDialect } from "./CompilerStrategy-Dc5_3-nw.js";
2
- import { n as loadModule, t as MigrationRunner } from "./MigrationRunner-vG1lVlkz.js";
2
+ import { n as loadModule, t as MigrationRunner } from "./MigrationRunner-B9F-yxGV.js";
3
3
  import { t as MigrationGenerator } from "./MigrationGenerator-D-sSbsFG.js";
4
4
  import { n as buildMigrationModelMetadataProjection, r as diffSchema } from "./diff-BsQr9Sl8.js";
5
5
  import { n as createDefaultIntrospectorStrategy } from "./IntrospectorStrategy-BCTs_4wl.js";
@@ -324,4 +324,4 @@ function registerMigrationsCommands(yargsBuilder) {
324
324
  //#endregion
325
325
  export { registerMigrationsCommands as t };
326
326
 
327
- //# sourceMappingURL=cli-B3MNpH29.js.map
327
+ //# sourceMappingURL=cli-kbb2TSGO.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"cli-B3MNpH29.js","names":["fsConstants"],"sources":["../src/commands/cli.ts"],"sourcesContent":["import { access, mkdir } from 'node:fs/promises';\nimport { constants as fsConstants } from 'node:fs';\nimport { dirname, resolve } from 'node:path';\nimport type { Argv } from 'yargs';\nimport { MigrationRunner } from '../runner/MigrationRunner';\nimport { MigrationGenerator } from '../generator/MigrationGenerator';\nimport { buildMigrationModelMetadataProjection, diffSchema } from '../diff/index';\nimport type { DbSchema } from '../introspect/PostgresIntrospector';\nimport type { Dialect } from '../domain/Dialect';\nimport type { ColumnType } from '../builder/contracts/ColumnType';\nimport type { DeleteReferentialAction } from '../builder/contracts/DeleteReferentialAction';\nimport type { UpdateReferentialAction } from '../builder/contracts/UpdateReferentialAction';\nimport { createDefaultIntrospectorStrategy } from '../strategies/IntrospectorStrategy';\nimport { InternalDialect } from '../domain/internal/InternalDialect';\nimport { loadConfig } from '@danceroutine/tango-config';\nimport { loadProjectModule } from '@danceroutine/tango-codegen/commands';\nimport { writeRelationRegistryArtifacts } from '@danceroutine/tango-codegen/generators';\nimport { getLogger } from '@danceroutine/tango-core';\nimport { GENERATED_RELATION_REGISTRY_DIRNAME, ModelRegistry } from '@danceroutine/tango-schema';\nimport { loadModule } from '../runtime/loadModule';\n\nconst logger = getLogger('tango.migrations');\n\ntype ConfigEnvironment = 'development' | 'test' | 'production';\n\ntype OptionalMigrationDefaults = {\n dialect?: Dialect;\n db?: string;\n dir?: string;\n autoApply?: boolean;\n};\n\ntype ModelMetadataLike = {\n table: string;\n fields: Array<{\n name: string;\n type: ColumnType;\n notNull?: boolean;\n default?: string | { now: true } | null;\n primaryKey?: boolean;\n unique?: boolean;\n references?: {\n table: string;\n column: string;\n onDelete?: DeleteReferentialAction;\n onUpdate?: UpdateReferentialAction;\n };\n }>;\n indexes?: Array<{ name: string; on: string[]; unique?: boolean }>;\n};\n\ntype CliDbClient = {\n query<T = unknown>(sql: string, params?: readonly unknown[]): Promise<{ rows: T[] }>;\n close(): Promise<void>;\n};\n\ntype LoadedModelsResult = {\n models: ModelMetadataLike[];\n registry: ModelRegistry;\n modelTypeAccessors: Record<string, string>;\n};\n\ntype ModelContainerLike = {\n metadata: ModelMetadataLike;\n};\n\nasync function importModule(modulePath: string): Promise<Record<string, unknown>> {\n return loadModule(modulePath, { projectRoot: process.cwd() });\n}\n\nasync function tryLoadMigrationDefaults(\n configPathArg: string | undefined,\n configEnvArg: ConfigEnvironment | undefined\n): Promise<OptionalMigrationDefaults> {\n const explicitConfigPath = typeof configPathArg === 'string' && configPathArg.trim().length > 0;\n const resolvedPath = resolve(process.cwd(), configPathArg?.trim() || './tango.config.ts');\n\n try {\n await access(resolvedPath, fsConstants.F_OK);\n } catch (error) {\n if (explicitConfigPath) {\n throw new Error(`Config file not found: ${resolvedPath}`, { cause: error });\n }\n return {};\n }\n\n const module = await importModule(resolvedPath);\n const fileConfig = (module.default ?? module) as { current?: ConfigEnvironment } & Record<string, unknown>;\n\n const loaded = loadConfig(() => ({\n ...fileConfig,\n ...(configEnvArg ? { current: configEnvArg } : {}),\n }));\n\n const { db, migrations } = loaded.current;\n const inferredDialect = db.adapter as Dialect;\n const inferredDb = resolveDbTarget(db);\n\n return {\n dialect: inferredDialect,\n db: inferredDb,\n dir: migrations.dir,\n autoApply: (migrations as { autoApply?: boolean }).autoApply,\n };\n}\n\nfunction resolveDbTarget(db: {\n adapter: string;\n url?: string;\n filename?: string;\n host?: string;\n port?: number;\n database?: string;\n user?: string;\n password?: string;\n}): string | undefined {\n if (db.adapter === InternalDialect.SQLITE) {\n return db.filename ?? db.url;\n }\n\n if (db.url) {\n return db.url;\n }\n\n if (!db.database) {\n return undefined;\n }\n\n const host = db.host ?? 'localhost';\n const port = db.port ?? 5432;\n const encodedUser = db.user ? encodeURIComponent(db.user) : '';\n const encodedPassword = db.password ? encodeURIComponent(db.password) : '';\n const userInfo =\n encodedUser.length > 0\n ? encodedPassword.length > 0\n ? `${encodedUser}:${encodedPassword}@`\n : `${encodedUser}@`\n : '';\n\n return `postgres://${userInfo}${host}:${String(port)}/${db.database}`;\n}\n\nasync function resolveCommandInputs(argv: {\n dialect?: string;\n dir?: string;\n db?: string;\n config?: string;\n env?: ConfigEnvironment;\n}): Promise<{ dialect: Dialect; dir: string; db?: string; autoApply: boolean }> {\n const defaults = await tryLoadMigrationDefaults(argv.config, argv.env);\n\n const resolvedDialect = (argv.dialect as Dialect | undefined) ?? defaults.dialect ?? InternalDialect.POSTGRES;\n const resolvedDir = argv.dir ?? defaults.dir ?? 'migrations';\n const resolvedDb = argv.db ?? defaults.db;\n\n return {\n dialect: resolvedDialect,\n dir: resolvedDir,\n db: resolvedDb,\n autoApply: defaults.autoApply ?? true,\n };\n}\n\nfunction isModelContainerLike(value: unknown): value is ModelContainerLike {\n return typeof value === 'object' && value !== null && 'metadata' in value;\n}\n\nfunction collectExportedModels(moduleValue: unknown): ModelMetadataLike[] {\n if (!moduleValue || typeof moduleValue !== 'object') {\n return [];\n }\n\n const models: ModelMetadataLike[] = [];\n for (const value of Object.values(moduleValue as Record<string, unknown>)) {\n if (isModelContainerLike(value)) {\n models.push(value.metadata);\n continue;\n }\n\n if (!value || typeof value !== 'object') {\n continue;\n }\n\n for (const nestedValue of Object.values(value as Record<string, unknown>)) {\n if (isModelContainerLike(nestedValue)) {\n models.push(nestedValue.metadata);\n }\n }\n }\n\n return models;\n}\n\nasync function loadModels(modelsPath: string): Promise<LoadedModelsResult> {\n const {\n loaded: mod,\n registry,\n modelTypeAccessors,\n } = await loadProjectModule(modelsPath, {\n projectRoot: process.cwd(),\n outputDir: resolve(process.cwd(), GENERATED_RELATION_REGISTRY_DIRNAME),\n });\n const moduleValue = (mod.default ?? mod) as unknown;\n\n const models = isModelContainerLike(moduleValue)\n ? [moduleValue.metadata, ...collectExportedModels(moduleValue)]\n : collectExportedModels(moduleValue);\n\n if (models.length === 0) {\n throw new Error(`No models found in '${modelsPath}'. Ensure the module exports Model() definitions.`);\n }\n\n return {\n models,\n registry,\n modelTypeAccessors,\n };\n}\n\nasync function connectAndIntrospect(dbUrl: string, dialect: string): Promise<DbSchema> {\n const dbClient = await connectDbClient(dbUrl, dialect as Dialect);\n\n try {\n const strategy = createDefaultIntrospectorStrategy();\n return await strategy.introspect(dialect as Dialect, dbClient);\n } finally {\n await dbClient.close();\n }\n}\n\nasync function connectDbClient(db: string, dialect: Dialect): Promise<CliDbClient> {\n if (dialect === InternalDialect.POSTGRES) {\n const pg = await import('pg');\n const client = new pg.default.Client({ connectionString: db });\n await client.connect();\n\n return {\n async query<T = unknown>(sql: string, params?: readonly unknown[]): Promise<{ rows: T[] }> {\n const result = await client.query(sql, params as unknown[] | undefined);\n return { rows: result.rows as T[] };\n },\n async close(): Promise<void> {\n await client.end();\n },\n };\n }\n\n if (dialect === InternalDialect.SQLITE) {\n const sqlite = await import('better-sqlite3');\n const DatabaseCtor = (sqlite.default ?? sqlite) as new (filename: string) => {\n prepare(sql: string): {\n all(...params: unknown[]): unknown[];\n run(...params: unknown[]): unknown;\n };\n close(): void;\n };\n\n const filename = normalizeSqliteFilename(db);\n await ensureSqliteParentDirectory(filename);\n const connection = new DatabaseCtor(filename);\n\n return {\n async query<T = unknown>(sql: string, params?: readonly unknown[]): Promise<{ rows: T[] }> {\n const statement = connection.prepare(sql);\n const values = [...(params ?? [])];\n const isSelectLike = /^\\s*(SELECT|PRAGMA|WITH)\\b/i.test(sql);\n if (isSelectLike) {\n return { rows: statement.all(...values) as T[] };\n }\n\n statement.run(...values);\n return { rows: [] as T[] };\n },\n async close(): Promise<void> {\n connection.close();\n },\n };\n }\n\n throw new Error(`Unsupported dialect: ${dialect}`);\n}\n\nasync function ensureSqliteParentDirectory(filename: string): Promise<void> {\n if (filename === ':memory:' || filename === 'file::memory:') {\n return;\n }\n const directory = dirname(filename);\n if (directory === '.' || directory.length === 0) {\n return;\n }\n await mkdir(directory, { recursive: true });\n}\n\nfunction normalizeSqliteFilename(db: string): string {\n if (db.startsWith('sqlite://')) {\n return db.slice('sqlite://'.length);\n }\n return db;\n}\n\n/**\n * Register Tango's migration commands on an existing yargs parser.\n */\nexport function registerMigrationsCommands(yargsBuilder: Argv): Argv {\n return yargsBuilder\n .command(\n 'migrate',\n 'Apply pending migrations to the database',\n (builder) =>\n builder\n .option('dir', {\n type: 'string',\n describe: 'Migrations directory',\n })\n .option('db', {\n type: 'string',\n describe: 'Database connection URL',\n })\n .option('dialect', {\n type: 'string',\n choices: ['postgres', 'sqlite'] as const,\n describe: 'Database dialect',\n })\n .option('config', {\n type: 'string',\n describe: 'Path to tango.config.ts (auto-loads ./tango.config.ts when present)',\n })\n .option('env', {\n type: 'string',\n choices: ['development', 'test', 'production'] as const,\n describe: 'Config environment override',\n })\n .option('to', {\n type: 'string',\n describe: 'Target migration ID (apply up to this migration)',\n }),\n async (argv) => {\n const resolved = await resolveCommandInputs({\n dialect: argv.dialect as string | undefined,\n dir: argv.dir as string | undefined,\n db: argv.db as string | undefined,\n config: argv.config as string | undefined,\n env: argv.env as ConfigEnvironment | undefined,\n });\n\n if (!resolved.autoApply) {\n logger.info('Auto-migration disabled (autoApply: false). Skipping.');\n return;\n }\n\n if (!resolved.db) {\n throw new Error('No database target provided. Pass --db or define db settings in tango.config.ts.');\n }\n\n const dbClient = await connectDbClient(resolved.db, resolved.dialect);\n\n const runner = new MigrationRunner(dbClient, resolved.dialect, resolved.dir);\n await runner.apply(argv.to);\n\n await dbClient.close();\n logger.info('Migrations applied successfully');\n }\n )\n .command(\n 'make:migrations',\n 'Generate migration file by comparing models to database',\n (builder) =>\n builder\n .option('dir', {\n type: 'string',\n describe: 'Migrations directory',\n })\n .option('name', {\n type: 'string',\n demandOption: true,\n describe: 'Migration name (e.g. \"create_users\")',\n })\n .option('models', {\n type: 'string',\n demandOption: true,\n describe: 'Path to module exporting Model definitions (e.g. \"./src/models.ts\")',\n })\n .option('db', {\n type: 'string',\n describe: 'Database connection URL for introspection (omit for initial migration)',\n })\n .option('dialect', {\n type: 'string',\n choices: ['postgres', 'sqlite'] as const,\n describe: 'Database dialect',\n })\n .option('config', {\n type: 'string',\n describe: 'Path to tango.config.ts (auto-loads ./tango.config.ts when present)',\n })\n .option('env', {\n type: 'string',\n choices: ['development', 'test', 'production'] as const,\n describe: 'Config environment override',\n }),\n async (argv) => {\n const resolved = await resolveCommandInputs({\n dialect: argv.dialect as string | undefined,\n dir: argv.dir as string | undefined,\n db: argv.db as string | undefined,\n config: argv.config as string | undefined,\n env: argv.env as ConfigEnvironment | undefined,\n });\n const loaded = await loadModels(argv.models);\n const projectedModels = buildMigrationModelMetadataProjection(loaded.registry);\n logger.info(\n `Found ${loaded.models.length} exported model(s); ${projectedModels.length} model(s) after projection: ${projectedModels.map((m) => m.table).join(', ')}`\n );\n try {\n await writeRelationRegistryArtifacts({\n registry: loaded.registry,\n modelTypeAccessors: loaded.modelTypeAccessors,\n outputDir: resolve(process.cwd(), GENERATED_RELATION_REGISTRY_DIRNAME),\n });\n } catch (error) {\n logger.warn(\n `Unable to refresh generated relation registry during make:migrations. Continuing without updated relation artifacts: ${error instanceof Error ? error.message : String(error)}`\n );\n }\n\n let dbState: DbSchema;\n if (resolved.db) {\n logger.info('Introspecting database...');\n dbState = await connectAndIntrospect(resolved.db, resolved.dialect);\n } else {\n dbState = { tables: {} };\n }\n\n const operations = diffSchema(dbState, projectedModels);\n\n if (operations.length === 0) {\n logger.info('No changes detected — models and database are in sync');\n return;\n }\n\n const generator = new MigrationGenerator();\n const filepath = await generator.generate({\n name: argv.name,\n operations,\n directory: resolved.dir,\n });\n\n logger.info(`Generated migration: ${filepath}`);\n logger.info(` ${operations.length} operation(s)`);\n }\n )\n .command(\n 'plan',\n 'Print migration SQL without applying',\n (builder) =>\n builder\n .option('dir', {\n type: 'string',\n describe: 'Migrations directory',\n })\n .option('dialect', {\n type: 'string',\n choices: ['postgres', 'sqlite'] as const,\n describe: 'Database dialect',\n })\n .option('config', {\n type: 'string',\n describe: 'Path to tango.config.ts (auto-loads ./tango.config.ts when present)',\n })\n .option('env', {\n type: 'string',\n choices: ['development', 'test', 'production'] as const,\n describe: 'Config environment override',\n }),\n async (argv) => {\n const resolved = await resolveCommandInputs({\n dialect: argv.dialect as string | undefined,\n dir: argv.dir as string | undefined,\n db: argv.db as string | undefined,\n config: argv.config as string | undefined,\n env: argv.env as ConfigEnvironment | undefined,\n });\n const runner = new MigrationRunner(\n { query: async () => ({ rows: [] }), close: async () => {} },\n resolved.dialect,\n resolved.dir\n );\n const output = await runner.plan();\n logger.info(output);\n }\n )\n .command(\n 'status',\n 'Show applied/pending status of all migrations',\n (builder) =>\n builder\n .option('dir', {\n type: 'string',\n describe: 'Migrations directory',\n })\n .option('db', {\n type: 'string',\n describe: 'Database connection URL',\n })\n .option('dialect', {\n type: 'string',\n choices: ['postgres', 'sqlite'] as const,\n describe: 'Database dialect',\n })\n .option('config', {\n type: 'string',\n describe: 'Path to tango.config.ts (auto-loads ./tango.config.ts when present)',\n })\n .option('env', {\n type: 'string',\n choices: ['development', 'test', 'production'] as const,\n describe: 'Config environment override',\n }),\n async (argv) => {\n const resolved = await resolveCommandInputs({\n dialect: argv.dialect as string | undefined,\n dir: argv.dir as string | undefined,\n db: argv.db as string | undefined,\n config: argv.config as string | undefined,\n env: argv.env as ConfigEnvironment | undefined,\n });\n if (!resolved.db) {\n throw new Error('No database target provided. Pass --db or define db settings in tango.config.ts.');\n }\n\n const dbClient = await connectDbClient(resolved.db, resolved.dialect);\n\n const runner = new MigrationRunner(dbClient, resolved.dialect, resolved.dir);\n const statuses = await runner.status();\n\n if (statuses.length === 0) {\n logger.info('No migrations found');\n } else {\n statuses.forEach((statusItem) => {\n const marker = statusItem.applied ? '[x]' : '[ ]';\n logger.info(` ${marker} ${statusItem.id}`);\n });\n }\n\n await dbClient.close();\n }\n );\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"cli-kbb2TSGO.js","names":["fsConstants"],"sources":["../src/commands/cli.ts"],"sourcesContent":["import { access, mkdir } from 'node:fs/promises';\nimport { constants as fsConstants } from 'node:fs';\nimport { dirname, resolve } from 'node:path';\nimport type { Argv } from 'yargs';\nimport { MigrationRunner } from '../runner/MigrationRunner';\nimport { MigrationGenerator } from '../generator/MigrationGenerator';\nimport { buildMigrationModelMetadataProjection, diffSchema } from '../diff/index';\nimport type { DbSchema } from '../introspect/PostgresIntrospector';\nimport type { Dialect } from '../domain/Dialect';\nimport type { ColumnType } from '../builder/contracts/ColumnType';\nimport type { DeleteReferentialAction } from '../builder/contracts/DeleteReferentialAction';\nimport type { UpdateReferentialAction } from '../builder/contracts/UpdateReferentialAction';\nimport { createDefaultIntrospectorStrategy } from '../strategies/IntrospectorStrategy';\nimport { InternalDialect } from '../domain/internal/InternalDialect';\nimport { loadConfig } from '@danceroutine/tango-config';\nimport { loadProjectModule } from '@danceroutine/tango-codegen/commands';\nimport { writeRelationRegistryArtifacts } from '@danceroutine/tango-codegen/generators';\nimport { getLogger } from '@danceroutine/tango-core';\nimport { GENERATED_RELATION_REGISTRY_DIRNAME, ModelRegistry } from '@danceroutine/tango-schema';\nimport { loadModule } from '../runtime/loadModule';\n\nconst logger = getLogger('tango.migrations');\n\ntype ConfigEnvironment = 'development' | 'test' | 'production';\n\ntype OptionalMigrationDefaults = {\n dialect?: Dialect;\n db?: string;\n dir?: string;\n autoApply?: boolean;\n};\n\ntype ModelMetadataLike = {\n table: string;\n fields: Array<{\n name: string;\n type: ColumnType;\n notNull?: boolean;\n default?: string | { now: true } | null;\n primaryKey?: boolean;\n unique?: boolean;\n references?: {\n table: string;\n column: string;\n onDelete?: DeleteReferentialAction;\n onUpdate?: UpdateReferentialAction;\n };\n }>;\n indexes?: Array<{ name: string; on: string[]; unique?: boolean }>;\n};\n\ntype CliDbClient = {\n query<T = unknown>(sql: string, params?: readonly unknown[]): Promise<{ rows: T[] }>;\n close(): Promise<void>;\n};\n\ntype LoadedModelsResult = {\n models: ModelMetadataLike[];\n registry: ModelRegistry;\n modelTypeAccessors: Record<string, string>;\n};\n\ntype ModelContainerLike = {\n metadata: ModelMetadataLike;\n};\n\nasync function importModule(modulePath: string): Promise<Record<string, unknown>> {\n return loadModule(modulePath, { projectRoot: process.cwd() });\n}\n\nasync function tryLoadMigrationDefaults(\n configPathArg: string | undefined,\n configEnvArg: ConfigEnvironment | undefined\n): Promise<OptionalMigrationDefaults> {\n const explicitConfigPath = typeof configPathArg === 'string' && configPathArg.trim().length > 0;\n const resolvedPath = resolve(process.cwd(), configPathArg?.trim() || './tango.config.ts');\n\n try {\n await access(resolvedPath, fsConstants.F_OK);\n } catch (error) {\n if (explicitConfigPath) {\n throw new Error(`Config file not found: ${resolvedPath}`, { cause: error });\n }\n return {};\n }\n\n const module = await importModule(resolvedPath);\n const fileConfig = (module.default ?? module) as { current?: ConfigEnvironment } & Record<string, unknown>;\n\n const loaded = loadConfig(() => ({\n ...fileConfig,\n ...(configEnvArg ? { current: configEnvArg } : {}),\n }));\n\n const { db, migrations } = loaded.current;\n const inferredDialect = db.adapter as Dialect;\n const inferredDb = resolveDbTarget(db);\n\n return {\n dialect: inferredDialect,\n db: inferredDb,\n dir: migrations.dir,\n autoApply: (migrations as { autoApply?: boolean }).autoApply,\n };\n}\n\nfunction resolveDbTarget(db: {\n adapter: string;\n url?: string;\n filename?: string;\n host?: string;\n port?: number;\n database?: string;\n user?: string;\n password?: string;\n}): string | undefined {\n if (db.adapter === InternalDialect.SQLITE) {\n return db.filename ?? db.url;\n }\n\n if (db.url) {\n return db.url;\n }\n\n if (!db.database) {\n return undefined;\n }\n\n const host = db.host ?? 'localhost';\n const port = db.port ?? 5432;\n const encodedUser = db.user ? encodeURIComponent(db.user) : '';\n const encodedPassword = db.password ? encodeURIComponent(db.password) : '';\n const userInfo =\n encodedUser.length > 0\n ? encodedPassword.length > 0\n ? `${encodedUser}:${encodedPassword}@`\n : `${encodedUser}@`\n : '';\n\n return `postgres://${userInfo}${host}:${String(port)}/${db.database}`;\n}\n\nasync function resolveCommandInputs(argv: {\n dialect?: string;\n dir?: string;\n db?: string;\n config?: string;\n env?: ConfigEnvironment;\n}): Promise<{ dialect: Dialect; dir: string; db?: string; autoApply: boolean }> {\n const defaults = await tryLoadMigrationDefaults(argv.config, argv.env);\n\n const resolvedDialect = (argv.dialect as Dialect | undefined) ?? defaults.dialect ?? InternalDialect.POSTGRES;\n const resolvedDir = argv.dir ?? defaults.dir ?? 'migrations';\n const resolvedDb = argv.db ?? defaults.db;\n\n return {\n dialect: resolvedDialect,\n dir: resolvedDir,\n db: resolvedDb,\n autoApply: defaults.autoApply ?? true,\n };\n}\n\nfunction isModelContainerLike(value: unknown): value is ModelContainerLike {\n return typeof value === 'object' && value !== null && 'metadata' in value;\n}\n\nfunction collectExportedModels(moduleValue: unknown): ModelMetadataLike[] {\n if (!moduleValue || typeof moduleValue !== 'object') {\n return [];\n }\n\n const models: ModelMetadataLike[] = [];\n for (const value of Object.values(moduleValue as Record<string, unknown>)) {\n if (isModelContainerLike(value)) {\n models.push(value.metadata);\n continue;\n }\n\n if (!value || typeof value !== 'object') {\n continue;\n }\n\n for (const nestedValue of Object.values(value as Record<string, unknown>)) {\n if (isModelContainerLike(nestedValue)) {\n models.push(nestedValue.metadata);\n }\n }\n }\n\n return models;\n}\n\nasync function loadModels(modelsPath: string): Promise<LoadedModelsResult> {\n const {\n loaded: mod,\n registry,\n modelTypeAccessors,\n } = await loadProjectModule(modelsPath, {\n projectRoot: process.cwd(),\n outputDir: resolve(process.cwd(), GENERATED_RELATION_REGISTRY_DIRNAME),\n });\n const moduleValue = (mod.default ?? mod) as unknown;\n\n const models = isModelContainerLike(moduleValue)\n ? [moduleValue.metadata, ...collectExportedModels(moduleValue)]\n : collectExportedModels(moduleValue);\n\n if (models.length === 0) {\n throw new Error(`No models found in '${modelsPath}'. Ensure the module exports Model() definitions.`);\n }\n\n return {\n models,\n registry,\n modelTypeAccessors,\n };\n}\n\nasync function connectAndIntrospect(dbUrl: string, dialect: string): Promise<DbSchema> {\n const dbClient = await connectDbClient(dbUrl, dialect as Dialect);\n\n try {\n const strategy = createDefaultIntrospectorStrategy();\n return await strategy.introspect(dialect as Dialect, dbClient);\n } finally {\n await dbClient.close();\n }\n}\n\nasync function connectDbClient(db: string, dialect: Dialect): Promise<CliDbClient> {\n if (dialect === InternalDialect.POSTGRES) {\n const pg = await import('pg');\n const client = new pg.default.Client({ connectionString: db });\n await client.connect();\n\n return {\n async query<T = unknown>(sql: string, params?: readonly unknown[]): Promise<{ rows: T[] }> {\n const result = await client.query(sql, params as unknown[] | undefined);\n return { rows: result.rows as T[] };\n },\n async close(): Promise<void> {\n await client.end();\n },\n };\n }\n\n if (dialect === InternalDialect.SQLITE) {\n const sqlite = await import('better-sqlite3');\n const DatabaseCtor = (sqlite.default ?? sqlite) as new (filename: string) => {\n prepare(sql: string): {\n all(...params: unknown[]): unknown[];\n run(...params: unknown[]): unknown;\n };\n close(): void;\n };\n\n const filename = normalizeSqliteFilename(db);\n await ensureSqliteParentDirectory(filename);\n const connection = new DatabaseCtor(filename);\n\n return {\n async query<T = unknown>(sql: string, params?: readonly unknown[]): Promise<{ rows: T[] }> {\n const statement = connection.prepare(sql);\n const values = [...(params ?? [])];\n const isSelectLike = /^\\s*(SELECT|PRAGMA|WITH)\\b/i.test(sql);\n if (isSelectLike) {\n return { rows: statement.all(...values) as T[] };\n }\n\n statement.run(...values);\n return { rows: [] as T[] };\n },\n async close(): Promise<void> {\n connection.close();\n },\n };\n }\n\n throw new Error(`Unsupported dialect: ${dialect}`);\n}\n\nasync function ensureSqliteParentDirectory(filename: string): Promise<void> {\n if (filename === ':memory:' || filename === 'file::memory:') {\n return;\n }\n const directory = dirname(filename);\n if (directory === '.' || directory.length === 0) {\n return;\n }\n await mkdir(directory, { recursive: true });\n}\n\nfunction normalizeSqliteFilename(db: string): string {\n if (db.startsWith('sqlite://')) {\n return db.slice('sqlite://'.length);\n }\n return db;\n}\n\n/**\n * Register Tango's migration commands on an existing yargs parser.\n */\nexport function registerMigrationsCommands(yargsBuilder: Argv): Argv {\n return yargsBuilder\n .command(\n 'migrate',\n 'Apply pending migrations to the database',\n (builder) =>\n builder\n .option('dir', {\n type: 'string',\n describe: 'Migrations directory',\n })\n .option('db', {\n type: 'string',\n describe: 'Database connection URL',\n })\n .option('dialect', {\n type: 'string',\n choices: ['postgres', 'sqlite'] as const,\n describe: 'Database dialect',\n })\n .option('config', {\n type: 'string',\n describe: 'Path to tango.config.ts (auto-loads ./tango.config.ts when present)',\n })\n .option('env', {\n type: 'string',\n choices: ['development', 'test', 'production'] as const,\n describe: 'Config environment override',\n })\n .option('to', {\n type: 'string',\n describe: 'Target migration ID (apply up to this migration)',\n }),\n async (argv) => {\n const resolved = await resolveCommandInputs({\n dialect: argv.dialect as string | undefined,\n dir: argv.dir as string | undefined,\n db: argv.db as string | undefined,\n config: argv.config as string | undefined,\n env: argv.env as ConfigEnvironment | undefined,\n });\n\n if (!resolved.autoApply) {\n logger.info('Auto-migration disabled (autoApply: false). Skipping.');\n return;\n }\n\n if (!resolved.db) {\n throw new Error('No database target provided. Pass --db or define db settings in tango.config.ts.');\n }\n\n const dbClient = await connectDbClient(resolved.db, resolved.dialect);\n\n const runner = new MigrationRunner(dbClient, resolved.dialect, resolved.dir);\n await runner.apply(argv.to);\n\n await dbClient.close();\n logger.info('Migrations applied successfully');\n }\n )\n .command(\n 'make:migrations',\n 'Generate migration file by comparing models to database',\n (builder) =>\n builder\n .option('dir', {\n type: 'string',\n describe: 'Migrations directory',\n })\n .option('name', {\n type: 'string',\n demandOption: true,\n describe: 'Migration name (e.g. \"create_users\")',\n })\n .option('models', {\n type: 'string',\n demandOption: true,\n describe: 'Path to module exporting Model definitions (e.g. \"./src/models.ts\")',\n })\n .option('db', {\n type: 'string',\n describe: 'Database connection URL for introspection (omit for initial migration)',\n })\n .option('dialect', {\n type: 'string',\n choices: ['postgres', 'sqlite'] as const,\n describe: 'Database dialect',\n })\n .option('config', {\n type: 'string',\n describe: 'Path to tango.config.ts (auto-loads ./tango.config.ts when present)',\n })\n .option('env', {\n type: 'string',\n choices: ['development', 'test', 'production'] as const,\n describe: 'Config environment override',\n }),\n async (argv) => {\n const resolved = await resolveCommandInputs({\n dialect: argv.dialect as string | undefined,\n dir: argv.dir as string | undefined,\n db: argv.db as string | undefined,\n config: argv.config as string | undefined,\n env: argv.env as ConfigEnvironment | undefined,\n });\n const loaded = await loadModels(argv.models);\n const projectedModels = buildMigrationModelMetadataProjection(loaded.registry);\n logger.info(\n `Found ${loaded.models.length} exported model(s); ${projectedModels.length} model(s) after projection: ${projectedModels.map((m) => m.table).join(', ')}`\n );\n try {\n await writeRelationRegistryArtifacts({\n registry: loaded.registry,\n modelTypeAccessors: loaded.modelTypeAccessors,\n outputDir: resolve(process.cwd(), GENERATED_RELATION_REGISTRY_DIRNAME),\n });\n } catch (error) {\n logger.warn(\n `Unable to refresh generated relation registry during make:migrations. Continuing without updated relation artifacts: ${error instanceof Error ? error.message : String(error)}`\n );\n }\n\n let dbState: DbSchema;\n if (resolved.db) {\n logger.info('Introspecting database...');\n dbState = await connectAndIntrospect(resolved.db, resolved.dialect);\n } else {\n dbState = { tables: {} };\n }\n\n const operations = diffSchema(dbState, projectedModels);\n\n if (operations.length === 0) {\n logger.info('No changes detected — models and database are in sync');\n return;\n }\n\n const generator = new MigrationGenerator();\n const filepath = await generator.generate({\n name: argv.name,\n operations,\n directory: resolved.dir,\n });\n\n logger.info(`Generated migration: ${filepath}`);\n logger.info(` ${operations.length} operation(s)`);\n }\n )\n .command(\n 'plan',\n 'Print migration SQL without applying',\n (builder) =>\n builder\n .option('dir', {\n type: 'string',\n describe: 'Migrations directory',\n })\n .option('dialect', {\n type: 'string',\n choices: ['postgres', 'sqlite'] as const,\n describe: 'Database dialect',\n })\n .option('config', {\n type: 'string',\n describe: 'Path to tango.config.ts (auto-loads ./tango.config.ts when present)',\n })\n .option('env', {\n type: 'string',\n choices: ['development', 'test', 'production'] as const,\n describe: 'Config environment override',\n }),\n async (argv) => {\n const resolved = await resolveCommandInputs({\n dialect: argv.dialect as string | undefined,\n dir: argv.dir as string | undefined,\n db: argv.db as string | undefined,\n config: argv.config as string | undefined,\n env: argv.env as ConfigEnvironment | undefined,\n });\n const runner = new MigrationRunner(\n { query: async () => ({ rows: [] }), close: async () => {} },\n resolved.dialect,\n resolved.dir\n );\n const output = await runner.plan();\n logger.info(output);\n }\n )\n .command(\n 'status',\n 'Show applied/pending status of all migrations',\n (builder) =>\n builder\n .option('dir', {\n type: 'string',\n describe: 'Migrations directory',\n })\n .option('db', {\n type: 'string',\n describe: 'Database connection URL',\n })\n .option('dialect', {\n type: 'string',\n choices: ['postgres', 'sqlite'] as const,\n describe: 'Database dialect',\n })\n .option('config', {\n type: 'string',\n describe: 'Path to tango.config.ts (auto-loads ./tango.config.ts when present)',\n })\n .option('env', {\n type: 'string',\n choices: ['development', 'test', 'production'] as const,\n describe: 'Config environment override',\n }),\n async (argv) => {\n const resolved = await resolveCommandInputs({\n dialect: argv.dialect as string | undefined,\n dir: argv.dir as string | undefined,\n db: argv.db as string | undefined,\n config: argv.config as string | undefined,\n env: argv.env as ConfigEnvironment | undefined,\n });\n if (!resolved.db) {\n throw new Error('No database target provided. Pass --db or define db settings in tango.config.ts.');\n }\n\n const dbClient = await connectDbClient(resolved.db, resolved.dialect);\n\n const runner = new MigrationRunner(dbClient, resolved.dialect, resolved.dir);\n const statuses = await runner.status();\n\n if (statuses.length === 0) {\n logger.info('No migrations found');\n } else {\n statuses.forEach((statusItem) => {\n const marker = statusItem.applied ? '[x]' : '[ ]';\n logger.info(` ${marker} ${statusItem.id}`);\n });\n }\n\n await dbClient.close();\n }\n );\n}\n"],"mappings":"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"}
package/dist/cli.js CHANGED
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import { t as registerMigrationsCommands } from "./cli-B3MNpH29.js";
2
+ import { t as registerMigrationsCommands } from "./cli-kbb2TSGO.js";
3
3
  import yargs from "yargs";
4
4
  import { hideBin } from "yargs/helpers";
5
5
  //#region src/cli.ts
@@ -1,5 +1,5 @@
1
1
  import { t as __exportAll } from "../chunk-D7D4PA-g.js";
2
- import { t as registerMigrationsCommands } from "../cli-B3MNpH29.js";
2
+ import { t as registerMigrationsCommands } from "../cli-kbb2TSGO.js";
3
3
  //#region src/commands/index.ts
4
4
  var commands_exports = /* @__PURE__ */ __exportAll({ registerMigrationsCommands: () => registerMigrationsCommands });
5
5
  //#endregion
@@ -69,13 +69,16 @@ declare class MigrationRunner {
69
69
  private listApplied;
70
70
  private loadMigrations;
71
71
  private applyMigration;
72
+ private assertAppliedChecksum;
73
+ private calculateChecksums;
74
+ private calculateBuilderChecksum;
75
+ private calculateLegacyOperationsChecksum;
72
76
  /**
73
- * Compute a simple hash of the migration's operation list.
77
+ * Compute a SHA-256 digest of a migration checksum payload.
74
78
  * Stored alongside each applied migration in the journal table to detect
75
79
  * if a migration file has been modified after it was already applied.
76
- * Uses a djb2-like hash over the JSON-serialized operations.
77
80
  */
78
- private hashJSON;
81
+ private hashPayload;
79
82
  private compileOperation;
80
83
  }
81
84
  declare namespace index_d_exports {
@@ -83,4 +86,4 @@ declare namespace index_d_exports {
83
86
  }
84
87
  //#endregion
85
88
  export { MigrationRunner as n, index_d_exports as t };
86
- //# sourceMappingURL=index-ni7Db8Lv.d.ts.map
89
+ //# sourceMappingURL=index-j3DHd0Y6.d.ts.map
package/dist/index.d.ts CHANGED
@@ -10,7 +10,7 @@ import { t as Dialect } from "./Dialect-Cp4r7UfW.js";
10
10
  import { n as Migration, t as index_d_exports$4 } from "./index-DdCF5yCg.js";
11
11
  import { n as GenerateMigrationOptions, r as MigrationGenerator, t as index_d_exports$5 } from "./index-Dcuh1zcQ.js";
12
12
  import { n as createDefaultCompilerStrategy, t as CompilerStrategy } from "./CompilerStrategy-DqmcqAC-.js";
13
- import { n as MigrationRunner, t as index_d_exports$7 } from "./index-ni7Db8Lv.js";
13
+ import { n as MigrationRunner, t as index_d_exports$7 } from "./index-j3DHd0Y6.js";
14
14
  import { a as SqliteIntrospector, i as DbTable$1, n as DbColumn$1, r as DbSchema$1, t as index_d_exports$6 } from "./index-sywIP4pt.js";
15
15
  import { n as IntrospectorStrategy, r as createDefaultIntrospectorStrategy, t as index_d_exports$8 } from "./index-a1Y--85Y.js";
16
16
  import { TrustedSqlFragment, trustedSql } from "@danceroutine/tango-core";
package/dist/index.js CHANGED
@@ -4,7 +4,7 @@ import { a as applyFieldType, i as OpBuilder, t as builder_exports } from "./bui
4
4
  import { t as CollectingBuilder } from "./CollectingBuilder-BIfAKs_x.js";
5
5
  import { i as PostgresCompiler, n as SqliteCompiler } from "./SqliteCompilerFactory-C_56xoQM.js";
6
6
  import { n as createDefaultCompilerStrategy, t as CompilerStrategy } from "./CompilerStrategy-Dc5_3-nw.js";
7
- import { t as MigrationRunner } from "./MigrationRunner-vG1lVlkz.js";
7
+ import { t as MigrationRunner } from "./MigrationRunner-B9F-yxGV.js";
8
8
  import { t as runner_exports } from "./runner/index.js";
9
9
  import { t as MigrationGenerator } from "./MigrationGenerator-D-sSbsFG.js";
10
10
  import { t as generator_exports } from "./generator/index.js";
@@ -14,7 +14,7 @@ import { n as PostgresIntrospector, t as SqliteIntrospector } from "./SqliteIntr
14
14
  import { t as introspect_exports } from "./introspect/index.js";
15
15
  import { n as createDefaultIntrospectorStrategy, t as IntrospectorStrategy } from "./IntrospectorStrategy-BCTs_4wl.js";
16
16
  import { t as strategies_exports } from "./strategies/index.js";
17
- import { t as registerMigrationsCommands } from "./cli-B3MNpH29.js";
17
+ import { t as registerMigrationsCommands } from "./cli-kbb2TSGO.js";
18
18
  import { t as commands_exports } from "./commands/index.js";
19
19
  import { trustedSql } from "@danceroutine/tango-core";
20
20
  export { CollectingBuilder, CompilerStrategy, IntrospectorStrategy, Migration, MigrationGenerator, MigrationRunner, OpBuilder, OpBuilder as op, PostgresCompiler, PostgresIntrospector, SqliteCompiler, SqliteIntrospector, applyFieldType, builder_exports as builder, commands_exports as commands, compilers_exports as compilers, createDefaultCompilerStrategy, createDefaultIntrospectorStrategy, diff_exports as diff, diffSchema, domain_exports as domain, generator_exports as generator, introspect_exports as introspect, registerMigrationsCommands, runner_exports as runner, strategies_exports as strategies, trustedSql };
@@ -1,2 +1,2 @@
1
- import { n as MigrationRunner } from "../index-ni7Db8Lv.js";
1
+ import { n as MigrationRunner } from "../index-j3DHd0Y6.js";
2
2
  export { MigrationRunner };
@@ -1,5 +1,5 @@
1
1
  import { t as __exportAll } from "../chunk-D7D4PA-g.js";
2
- import { t as MigrationRunner } from "../MigrationRunner-vG1lVlkz.js";
2
+ import { t as MigrationRunner } from "../MigrationRunner-B9F-yxGV.js";
3
3
  //#region src/runner/index.ts
4
4
  var runner_exports = /* @__PURE__ */ __exportAll({ MigrationRunner: () => MigrationRunner });
5
5
  //#endregion
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@danceroutine/tango-migrations",
3
- "version": "1.11.4",
3
+ "version": "1.11.6",
4
4
  "description": "Auto-migration system with ops DSL for Tango",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -67,10 +67,10 @@
67
67
  "jiti": "^2.7.0",
68
68
  "kleur": "^4.1.5",
69
69
  "yargs": "^18.0.0",
70
- "@danceroutine/tango-config": "1.11.4",
71
- "@danceroutine/tango-core": "1.11.4",
72
- "@danceroutine/tango-codegen": "1.11.4",
73
- "@danceroutine/tango-schema": "1.11.4"
70
+ "@danceroutine/tango-config": "1.11.6",
71
+ "@danceroutine/tango-codegen": "1.11.6",
72
+ "@danceroutine/tango-core": "1.11.6",
73
+ "@danceroutine/tango-schema": "1.11.6"
74
74
  },
75
75
  "peerDependencies": {
76
76
  "pg": "^8.20.0",
@@ -95,8 +95,8 @@
95
95
  "typescript": "^5.6.3",
96
96
  "vitest": "^4.1.7",
97
97
  "zod": "^4.0.0",
98
- "@danceroutine/tango-testing": "1.11.4",
99
- "@danceroutine/tango-orm": "1.11.4"
98
+ "@danceroutine/tango-testing": "1.11.6",
99
+ "@danceroutine/tango-orm": "1.11.6"
100
100
  },
101
101
  "scripts": {
102
102
  "build": "tsdown",
@@ -1 +0,0 @@
1
- {"version":3,"file":"MigrationRunner-vG1lVlkz.js","names":[],"sources":["../src/runtime/loadModule.ts","../src/runner/MigrationRunner.ts"],"sourcesContent":["import { resolve, extname } from 'node:path';\nimport { pathToFileURL } from 'node:url';\nimport { createJiti } from 'jiti';\nimport { ModelRegistry } from '@danceroutine/tango-schema';\n\nconst TS_EXTENSIONS = new Set(['.ts', '.tsx', '.mts', '.cts']);\n\nfunction toAbsolutePath(modulePath: string, projectRoot: string): string {\n return resolve(projectRoot, modulePath);\n}\n\nfunction isTypeScriptModule(modulePath: string): boolean {\n return TS_EXTENSIONS.has(extname(modulePath).toLowerCase());\n}\n\n/**\n * Load a module from a Tango app project root.\n *\n * TypeScript modules are loaded through jiti and JavaScript/ESM modules are loaded\n * through native dynamic import so published installs behave like end-user runtime.\n */\nexport async function loadModule(\n modulePath: string,\n options?: { projectRoot?: string; registry?: ModelRegistry; moduleCache?: boolean }\n): Promise<Record<string, unknown>> {\n const projectRoot = options?.projectRoot ?? process.cwd();\n const absolutePath = toAbsolutePath(modulePath, projectRoot);\n const executeImport = async (): Promise<Record<string, unknown>> => {\n if (isTypeScriptModule(absolutePath)) {\n const jiti = createJiti(resolve(projectRoot, 'tango.config.ts'), {\n interopDefault: true,\n moduleCache: options?.moduleCache ?? true,\n });\n return (await jiti.import<Record<string, unknown>>(absolutePath)) as Record<string, unknown>;\n }\n\n return (await import(pathToFileURL(absolutePath).href)) as Record<string, unknown>;\n };\n\n if (options?.registry) {\n return ModelRegistry.runWithRegistry(options.registry, executeImport);\n }\n\n return executeImport();\n}\n\n/**\n * Load a module and return default export when present.\n */\nexport async function loadDefaultExport(modulePath: string, options?: { projectRoot?: string }): Promise<unknown> {\n const loaded = await loadModule(modulePath, options);\n return loaded.default ?? loaded;\n}\n","import { CollectingBuilder } from '../builder/runtime/CollectingBuilder';\nimport type { Dialect } from '../domain/Dialect';\nimport { Migration } from '../domain/Migration';\nimport type { SQL } from '../compilers/contracts/SQL';\nimport type { MigrationOperation } from '../domain/MigrationOperation';\nimport type { CompilerStrategy } from '../strategies/CompilerStrategy';\nimport { createDefaultCompilerStrategy } from '../strategies/CompilerStrategy';\nimport { InternalDialect } from '../domain/internal/InternalDialect';\nimport { isError } from '@danceroutine/tango-core';\nimport { readdir } from 'node:fs/promises';\nimport { resolve } from 'node:path';\nimport { loadDefaultExport } from '../runtime/loadModule';\n\nconst JOURNAL = '_tango_migrations';\n\n/** DB client contract required by migration execution. */\ninterface DBClient {\n /** Execute SQL with optional parameters. */\n query<T = unknown>(sql: string, params?: readonly unknown[]): Promise<{ rows: T[] }>;\n /** Release underlying database resources. */\n close(): Promise<void>;\n}\n\n/**\n * Manages the lifecycle of database migrations: applying, planning, and tracking status.\n *\n * The runner reads migration files from a directory, compiles operations to SQL\n * for the target dialect, and maintains a journal table to track which migrations\n * have been applied. Each applied migration is checksummed to detect tampering.\n *\n * @example\n * ```typescript\n * const runner = new MigrationRunner(client, 'postgres', './migrations');\n *\n * // Apply all pending migrations\n * await runner.apply();\n *\n * // Apply up to a specific migration\n * await runner.apply('003_add_indexes');\n *\n * // Preview the SQL that would be generated\n * const sql = await runner.plan();\n *\n * // Check which migrations are applied\n * const statuses = await runner.status();\n * ```\n */\nexport class MigrationRunner {\n static readonly BRAND = 'tango.migrations.runner' as const;\n readonly __tangoBrand: typeof MigrationRunner.BRAND = MigrationRunner.BRAND;\n private compilerStrategy: CompilerStrategy;\n\n constructor(\n private client: DBClient,\n private dialect: Dialect,\n private migrationsDir: string = 'migrations',\n compilerStrategy?: CompilerStrategy\n ) {\n this.compilerStrategy = compilerStrategy ?? createDefaultCompilerStrategy();\n }\n\n /**\n * Narrow an unknown value to `MigrationRunner`.\n */\n static isMigrationRunner(value: unknown): value is MigrationRunner {\n return (\n typeof value === 'object' &&\n value !== null &&\n (value as { __tangoBrand?: unknown }).__tangoBrand === MigrationRunner.BRAND\n );\n }\n\n /**\n * Apply all pending migrations, optionally stopping at a specific migration ID.\n * Migrations are applied in file-sort order. Already-applied migrations are skipped.\n * Non-online migrations are wrapped in a transaction on Postgres.\n */\n async apply(toId?: string): Promise<void> {\n await this.ensureJournal();\n const applied = await this.listApplied();\n const migrations = await this.loadMigrations();\n\n for (const migration of migrations) {\n if (toId && migration.id > toId) {\n break;\n }\n if (applied.has(migration.id)) {\n continue;\n }\n\n await this.applyMigration(migration);\n }\n }\n\n /**\n * Generate a dry-run SQL plan for all migrations without executing anything.\n * Useful for reviewing what SQL would be run before applying.\n */\n async plan(): Promise<string> {\n const migrations = await this.loadMigrations();\n let output = '';\n\n migrations.forEach((migration) => {\n const builder = new CollectingBuilder();\n migration.up(builder);\n const preparedOps = this.compilerStrategy.prepareOperations(this.dialect, builder.ops);\n const sqls = preparedOps.flatMap((op) => this.compileOperation(op));\n\n output += `# ${migration.id}\\n`;\n sqls.forEach((statement) => {\n output += statement.sql + ';\\n';\n });\n if (builder.dataFns.length) {\n output += '-- (data step present)\\n';\n }\n output += '\\n';\n });\n\n return output;\n }\n\n /**\n * Return the applied/pending status of every migration found on disk.\n */\n async status(): Promise<{ id: string; applied: boolean }[]> {\n const applied = await this.listApplied();\n const migrations = await this.loadMigrations();\n\n return migrations.map((m) => ({\n id: m.id,\n applied: applied.has(m.id),\n }));\n }\n\n private async ensureJournal(): Promise<void> {\n const sql =\n this.dialect === InternalDialect.POSTGRES\n ? `CREATE TABLE IF NOT EXISTS \"${JOURNAL}\" (\n id TEXT PRIMARY KEY,\n applied_at TIMESTAMPTZ NOT NULL DEFAULT now(),\n checksum TEXT NOT NULL\n )`\n : `CREATE TABLE IF NOT EXISTS ${JOURNAL} (\n id TEXT PRIMARY KEY,\n applied_at TEXT NOT NULL DEFAULT (datetime('now')),\n checksum TEXT NOT NULL\n )`;\n\n await this.client.query(sql);\n }\n\n private async listApplied(): Promise<Set<string>> {\n const table = this.dialect === InternalDialect.POSTGRES ? `\"${JOURNAL}\"` : JOURNAL;\n const { rows } = await this.client.query<{ id: string }>(`SELECT id FROM ${table}`);\n return new Set(rows.map((r) => r.id));\n }\n\n private async loadMigrations(): Promise<Migration[]> {\n const files = (await readdir(this.migrationsDir)).filter((f) => f.endsWith('.ts') || f.endsWith('.js')).sort();\n\n const migrations: Migration[] = [];\n\n for (const file of files) {\n const absolutePath = resolve(this.migrationsDir, file);\n let loaded: unknown;\n try {\n loaded = await loadDefaultExport(absolutePath, { projectRoot: process.cwd() });\n } catch (error) {\n const reason = isError(error) ? error.message : String(error);\n throw new Error(`Failed to load migration module '${file}': ${reason}`, { cause: error });\n }\n\n if (Migration.isMigration(loaded)) {\n migrations.push(loaded);\n continue;\n }\n\n if (Migration.isMigrationConstructor(loaded)) {\n migrations.push(new loaded());\n continue;\n }\n\n throw new Error(\n `Invalid migration module '${file}'. Default export must be a Migration subclass or instance.`\n );\n }\n\n return migrations;\n }\n\n private async applyMigration(migration: Migration): Promise<void> {\n const builder = new CollectingBuilder();\n await migration.up(builder);\n\n const preparedOps = this.compilerStrategy.prepareOperations(this.dialect, builder.ops);\n const sqls = preparedOps.flatMap((op) => this.compileOperation(op));\n const checksum = String(this.hashJSON(builder.ops));\n\n const isOnline = (migration.mode ?? builder.getMode()) === 'online';\n\n if (!isOnline && this.dialect === InternalDialect.POSTGRES) {\n await this.client.query('BEGIN');\n }\n\n try {\n for (const statement of sqls) {\n await this.client.query(statement.sql, statement.params);\n }\n\n for (const fn of builder.dataFns) {\n await fn({ query: (sql, params) => this.client.query(sql, params).then(() => {}) });\n }\n\n const table = this.dialect === InternalDialect.POSTGRES ? `\"${JOURNAL}\"` : JOURNAL;\n const placeholder = this.dialect === InternalDialect.POSTGRES ? '$1, $2' : '?, ?';\n await this.client.query(`INSERT INTO ${table} (id, checksum) VALUES (${placeholder})`, [\n migration.id,\n checksum,\n ]);\n\n if (!isOnline && this.dialect === InternalDialect.POSTGRES) {\n await this.client.query('COMMIT');\n }\n } catch (error) {\n if (!isOnline && this.dialect === InternalDialect.POSTGRES) {\n await this.client.query('ROLLBACK');\n }\n throw error;\n }\n }\n\n /**\n * Compute a simple hash of the migration's operation list.\n * Stored alongside each applied migration in the journal table to detect\n * if a migration file has been modified after it was already applied.\n * Uses a djb2-like hash over the JSON-serialized operations.\n */\n private hashJSON(x: unknown): number {\n const s = JSON.stringify(x);\n let h = 0;\n for (let i = 0; i < s.length; i++) {\n // oxlint-disable-next-line prefer-code-point\n h = Math.imul(31, h) + s.charCodeAt(i);\n // oxlint-disable-next-line prefer-math-trunc\n h = h | 0;\n }\n // oxlint-disable-next-line unicorn/prefer-math-trunc\n return h >>> 0;\n }\n\n private compileOperation(op: MigrationOperation): SQL[] {\n return this.compilerStrategy.compile(this.dialect, op);\n }\n}\n"],"mappings":"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"}