prisma-next 0.5.0-dev.67 → 0.5.0-dev.68
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.
- package/dist/cli.mjs +8 -8
- package/dist/cli.mjs.map +1 -1
- package/dist/client-0ZX24FXF.mjs +1398 -0
- package/dist/client-0ZX24FXF.mjs.map +1 -0
- package/dist/commands/contract-emit.mjs +1 -1
- package/dist/commands/contract-infer.mjs +1 -1
- package/dist/commands/db-init.d.mts.map +1 -1
- package/dist/commands/db-init.mjs +7 -5
- package/dist/commands/db-init.mjs.map +1 -1
- package/dist/commands/db-schema.mjs +2 -2
- package/dist/commands/db-sign.mjs +2 -2
- package/dist/commands/db-update.d.mts.map +1 -1
- package/dist/commands/db-update.mjs +7 -5
- package/dist/commands/db-update.mjs.map +1 -1
- package/dist/commands/db-verify.d.mts.map +1 -1
- package/dist/commands/db-verify.mjs +1 -320
- package/dist/commands/migration-apply.mjs +11 -11
- package/dist/commands/migration-apply.mjs.map +1 -1
- package/dist/commands/migration-new.mjs +5 -5
- package/dist/commands/migration-new.mjs.map +1 -1
- package/dist/commands/migration-plan.d.mts.map +1 -1
- package/dist/commands/migration-plan.mjs +1 -344
- package/dist/commands/migration-ref.d.mts +1 -1
- package/dist/commands/migration-ref.mjs +1 -1
- package/dist/commands/migration-show.d.mts +1 -1
- package/dist/commands/migration-show.d.mts.map +1 -1
- package/dist/commands/migration-show.mjs +8 -7
- package/dist/commands/migration-show.mjs.map +1 -1
- package/dist/commands/migration-status.mjs +1 -1
- package/dist/{contract-emit-B0nGrDtk.mjs → contract-emit-DkMqO7f2.mjs} +2 -2
- package/dist/{contract-emit-B0nGrDtk.mjs.map → contract-emit-DkMqO7f2.mjs.map} +1 -1
- package/dist/{contract-infer-BjMJaOOa.mjs → contract-infer-BDKAE0B0.mjs} +3 -3
- package/dist/{contract-infer-BjMJaOOa.mjs.map → contract-infer-BDKAE0B0.mjs.map} +1 -1
- package/dist/db-verify-B4TdDKOI.mjs +403 -0
- package/dist/db-verify-B4TdDKOI.mjs.map +1 -0
- package/dist/exports/control-api.d.mts +201 -3
- package/dist/exports/control-api.d.mts.map +1 -1
- package/dist/exports/control-api.mjs +2 -2
- package/dist/exports/index.d.mts.map +1 -1
- package/dist/exports/index.mjs +17 -17
- package/dist/exports/index.mjs.map +1 -1
- package/dist/exports/init-output.mjs +1 -1
- package/dist/{init-C3qdc0Sh.mjs → init-Deo7U8_U.mjs} +2 -2
- package/dist/{init-C3qdc0Sh.mjs.map → init-Deo7U8_U.mjs.map} +1 -1
- package/dist/{inspect-live-schema-33rxnu0K.mjs → inspect-live-schema-BAgQMYpD.mjs} +3 -3
- package/dist/{inspect-live-schema-33rxnu0K.mjs.map → inspect-live-schema-BAgQMYpD.mjs.map} +1 -1
- package/dist/{migration-command-scaffold-DQo4R0XT.mjs → migration-command-scaffold-B8J702Uh.mjs} +3 -3
- package/dist/{migration-command-scaffold-DQo4R0XT.mjs.map → migration-command-scaffold-B8J702Uh.mjs.map} +1 -1
- package/dist/migration-plan-BcKNnTM7.mjs +530 -0
- package/dist/migration-plan-BcKNnTM7.mjs.map +1 -0
- package/dist/{migration-status-C_2FSkbf.mjs → migration-status-CjwB2of-.mjs} +6 -6
- package/dist/{migration-status-C_2FSkbf.mjs.map → migration-status-CjwB2of-.mjs.map} +1 -1
- package/dist/{output-BTgnZ5c_.mjs → output-DnjfCC_u.mjs} +1 -1
- package/dist/{output-BTgnZ5c_.mjs.map → output-DnjfCC_u.mjs.map} +1 -1
- package/dist/{result-handler-C0QeiqKO.mjs → result-handler-DWb1rFS-.mjs} +18 -3
- package/dist/result-handler-DWb1rFS-.mjs.map +1 -0
- package/package.json +11 -11
- package/dist/client-CW1hcUtM.mjs +0 -1025
- package/dist/client-CW1hcUtM.mjs.map +0 -1
- package/dist/commands/db-verify.mjs.map +0 -1
- package/dist/commands/migration-plan.mjs.map +0 -1
- package/dist/result-handler-C0QeiqKO.mjs.map +0 -1
- /package/dist/{cli-errors-BWn943z2.d.mts → cli-errors-QH8kf-C2.d.mts} +0 -0
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { t as loadConfig } from "./config-loader-B6sJjXTv.mjs";
|
|
2
2
|
import { _ as errorUnexpected, c as errorDriverRequired, o as errorDatabaseConnectionRequired, t as CliStructuredError } from "./cli-errors-D3_sMh2K.mjs";
|
|
3
|
-
import { o as maskConnectionUrl, u as sanitizeErrorMessage, y as formatStyledHeader } from "./result-handler-
|
|
3
|
+
import { o as maskConnectionUrl, u as sanitizeErrorMessage, y as formatStyledHeader } from "./result-handler-DWb1rFS-.mjs";
|
|
4
4
|
import { t as createProgressAdapter } from "./progress-adapter-xASh41wr.mjs";
|
|
5
|
-
import { t as createControlClient } from "./client-
|
|
5
|
+
import { t as createControlClient } from "./client-0ZX24FXF.mjs";
|
|
6
6
|
import { notOk, ok } from "@prisma-next/utils/result";
|
|
7
7
|
import { relative, resolve } from "pathe";
|
|
8
8
|
//#region src/commands/inspect-live-schema.ts
|
|
@@ -87,4 +87,4 @@ async function inspectLiveSchema(options, flags, ui, startTime, context) {
|
|
|
87
87
|
//#endregion
|
|
88
88
|
export { inspectLiveSchema as t };
|
|
89
89
|
|
|
90
|
-
//# sourceMappingURL=inspect-live-schema-
|
|
90
|
+
//# sourceMappingURL=inspect-live-schema-BAgQMYpD.mjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"inspect-live-schema-
|
|
1
|
+
{"version":3,"file":"inspect-live-schema-BAgQMYpD.mjs","names":[],"sources":["../src/commands/inspect-live-schema.ts"],"sourcesContent":["import type { CoreSchemaView } from '@prisma-next/framework-components/control';\nimport type { PslDocumentAst } from '@prisma-next/framework-components/psl-ast';\nimport { notOk, ok, type Result } from '@prisma-next/utils/result';\nimport { relative, resolve } from 'pathe';\nimport { loadConfig } from '../config-loader';\nimport { createControlClient } from '../control-api/client';\nimport {\n CliStructuredError,\n errorDatabaseConnectionRequired,\n errorDriverRequired,\n errorUnexpected,\n} from '../utils/cli-errors';\nimport { maskConnectionUrl, sanitizeErrorMessage } from '../utils/command-helpers';\nimport { formatStyledHeader } from '../utils/formatters/styled';\nimport type { CommonCommandOptions, GlobalFlags } from '../utils/global-flags';\nimport { createProgressAdapter } from '../utils/progress-adapter';\nimport type { TerminalUI } from '../utils/terminal-ui';\n\nexport interface InspectLiveSchemaOptions extends CommonCommandOptions {\n readonly db?: string;\n readonly config?: string;\n}\n\ninterface InspectLiveSchemaContext {\n readonly commandName: string;\n readonly description: string;\n readonly url: string;\n}\n\ntype LoadedCliConfig = Awaited<ReturnType<typeof loadConfig>>;\n\nexport interface InspectLiveSchemaResult {\n readonly config: LoadedCliConfig;\n readonly schema: unknown;\n readonly schemaView: CoreSchemaView | undefined;\n /**\n * PSL AST inferred from the introspected schema, when the configured family\n * implements `PslContractInferCapable`. `undefined` for families that do not\n * support inference (e.g. Mongo today).\n */\n readonly pslContractAst: PslDocumentAst | undefined;\n readonly target: {\n readonly familyId: string;\n readonly id: string;\n };\n readonly meta: {\n readonly configPath?: string;\n readonly dbUrl?: string;\n };\n readonly timings: {\n readonly total: number;\n };\n}\n\nexport async function inspectLiveSchema(\n options: InspectLiveSchemaOptions,\n flags: GlobalFlags,\n ui: TerminalUI,\n startTime: number,\n context: InspectLiveSchemaContext,\n): Promise<Result<InspectLiveSchemaResult, CliStructuredError>> {\n let config: LoadedCliConfig;\n try {\n config = await loadConfig(options.config);\n } catch (error) {\n if (CliStructuredError.is(error)) {\n return notOk(error);\n }\n\n return notOk(\n errorUnexpected(error instanceof Error ? error.message : String(error), {\n why: 'Failed to load config',\n }),\n );\n }\n\n const configPath = options.config\n ? relative(process.cwd(), resolve(options.config))\n : 'prisma-next.config.ts';\n\n if (!flags.json && !flags.quiet) {\n const details: Array<{ label: string; value: string }> = [\n { label: 'config', value: configPath },\n ];\n\n if (options.db) {\n details.push({ label: 'database', value: maskConnectionUrl(options.db) });\n } else if (config.db?.connection && typeof config.db.connection === 'string') {\n details.push({ label: 'database', value: maskConnectionUrl(config.db.connection) });\n }\n\n ui.stderr(\n formatStyledHeader({\n command: context.commandName,\n description: context.description,\n url: context.url,\n details,\n flags,\n }),\n );\n }\n\n const dbConnection = options.db ?? config.db?.connection;\n if (!dbConnection) {\n return notOk(\n errorDatabaseConnectionRequired({\n why: `Database connection is required for ${context.commandName} (set db.connection in ${configPath}, or pass --db <url>)`,\n commandName: context.commandName,\n }),\n );\n }\n\n if (!config.driver) {\n return notOk(\n errorDriverRequired({\n why: `Config.driver is required for ${context.commandName}`,\n }),\n );\n }\n\n const client = createControlClient({\n family: config.family,\n target: config.target,\n adapter: config.adapter,\n driver: config.driver,\n extensionPacks: config.extensionPacks ?? [],\n });\n const onProgress = createProgressAdapter({ ui, flags });\n\n try {\n const schema = await client.introspect({\n connection: dbConnection,\n onProgress,\n });\n const schemaView = client.toSchemaView(schema);\n const pslContractAst = client.inferPslContract(schema);\n\n const dbUrl = typeof dbConnection === 'string' ? maskConnectionUrl(dbConnection) : undefined;\n\n return ok({\n config,\n schema,\n schemaView,\n pslContractAst,\n target: {\n familyId: config.family.familyId,\n id: config.target.targetId,\n },\n meta: {\n configPath,\n ...(dbUrl ? { dbUrl } : {}),\n },\n timings: {\n total: Date.now() - startTime,\n },\n });\n } catch (error) {\n if (CliStructuredError.is(error)) {\n return notOk(error);\n }\n\n const rawMessage = error instanceof Error ? error.message : String(error);\n const safeMessage = sanitizeErrorMessage(\n rawMessage,\n typeof dbConnection === 'string' ? dbConnection : undefined,\n );\n return notOk(\n errorUnexpected(safeMessage, {\n why: `Unexpected error during ${context.commandName}: ${safeMessage}`,\n }),\n );\n } finally {\n await client.close();\n }\n}\n"],"mappings":";;;;;;;;AAsDA,eAAsB,kBACpB,SACA,OACA,IACA,WACA,SAC8D;CAC9D,IAAI;CACJ,IAAI;EACF,SAAS,MAAM,WAAW,QAAQ,OAAO;UAClC,OAAO;EACd,IAAI,mBAAmB,GAAG,MAAM,EAC9B,OAAO,MAAM,MAAM;EAGrB,OAAO,MACL,gBAAgB,iBAAiB,QAAQ,MAAM,UAAU,OAAO,MAAM,EAAE,EACtE,KAAK,yBACN,CAAC,CACH;;CAGH,MAAM,aAAa,QAAQ,SACvB,SAAS,QAAQ,KAAK,EAAE,QAAQ,QAAQ,OAAO,CAAC,GAChD;CAEJ,IAAI,CAAC,MAAM,QAAQ,CAAC,MAAM,OAAO;EAC/B,MAAM,UAAmD,CACvD;GAAE,OAAO;GAAU,OAAO;GAAY,CACvC;EAED,IAAI,QAAQ,IACV,QAAQ,KAAK;GAAE,OAAO;GAAY,OAAO,kBAAkB,QAAQ,GAAG;GAAE,CAAC;OACpE,IAAI,OAAO,IAAI,cAAc,OAAO,OAAO,GAAG,eAAe,UAClE,QAAQ,KAAK;GAAE,OAAO;GAAY,OAAO,kBAAkB,OAAO,GAAG,WAAW;GAAE,CAAC;EAGrF,GAAG,OACD,mBAAmB;GACjB,SAAS,QAAQ;GACjB,aAAa,QAAQ;GACrB,KAAK,QAAQ;GACb;GACA;GACD,CAAC,CACH;;CAGH,MAAM,eAAe,QAAQ,MAAM,OAAO,IAAI;CAC9C,IAAI,CAAC,cACH,OAAO,MACL,gCAAgC;EAC9B,KAAK,uCAAuC,QAAQ,YAAY,yBAAyB,WAAW;EACpG,aAAa,QAAQ;EACtB,CAAC,CACH;CAGH,IAAI,CAAC,OAAO,QACV,OAAO,MACL,oBAAoB,EAClB,KAAK,iCAAiC,QAAQ,eAC/C,CAAC,CACH;CAGH,MAAM,SAAS,oBAAoB;EACjC,QAAQ,OAAO;EACf,QAAQ,OAAO;EACf,SAAS,OAAO;EAChB,QAAQ,OAAO;EACf,gBAAgB,OAAO,kBAAkB,EAAE;EAC5C,CAAC;CACF,MAAM,aAAa,sBAAsB;EAAE;EAAI;EAAO,CAAC;CAEvD,IAAI;EACF,MAAM,SAAS,MAAM,OAAO,WAAW;GACrC,YAAY;GACZ;GACD,CAAC;EACF,MAAM,aAAa,OAAO,aAAa,OAAO;EAC9C,MAAM,iBAAiB,OAAO,iBAAiB,OAAO;EAEtD,MAAM,QAAQ,OAAO,iBAAiB,WAAW,kBAAkB,aAAa,GAAG,KAAA;EAEnF,OAAO,GAAG;GACR;GACA;GACA;GACA;GACA,QAAQ;IACN,UAAU,OAAO,OAAO;IACxB,IAAI,OAAO,OAAO;IACnB;GACD,MAAM;IACJ;IACA,GAAI,QAAQ,EAAE,OAAO,GAAG,EAAE;IAC3B;GACD,SAAS,EACP,OAAO,KAAK,KAAK,GAAG,WACrB;GACF,CAAC;UACK,OAAO;EACd,IAAI,mBAAmB,GAAG,MAAM,EAC9B,OAAO,MAAM,MAAM;EAIrB,MAAM,cAAc,qBADD,iBAAiB,QAAQ,MAAM,UAAU,OAAO,MAAM,EAGvE,OAAO,iBAAiB,WAAW,eAAe,KAAA,EACnD;EACD,OAAO,MACL,gBAAgB,aAAa,EAC3B,KAAK,2BAA2B,QAAQ,YAAY,IAAI,eACzD,CAAC,CACH;WACO;EACR,MAAM,OAAO,OAAO"}
|
package/dist/{migration-command-scaffold-DQo4R0XT.mjs → migration-command-scaffold-B8J702Uh.mjs}
RENAMED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { t as loadConfig } from "./config-loader-B6sJjXTv.mjs";
|
|
2
2
|
import { _ as errorUnexpected, a as errorContractValidationFailed, c as errorDriverRequired, h as errorTargetMigrationNotSupported, l as errorFileNotFound, o as errorDatabaseConnectionRequired } from "./cli-errors-D3_sMh2K.mjs";
|
|
3
|
-
import { c as resolveContractPath, n as addGlobalOptions, o as maskConnectionUrl, y as formatStyledHeader } from "./result-handler-
|
|
3
|
+
import { c as resolveContractPath, n as addGlobalOptions, o as maskConnectionUrl, y as formatStyledHeader } from "./result-handler-DWb1rFS-.mjs";
|
|
4
4
|
import { t as createProgressAdapter } from "./progress-adapter-xASh41wr.mjs";
|
|
5
|
-
import { t as createControlClient } from "./client-
|
|
5
|
+
import { t as createControlClient } from "./client-0ZX24FXF.mjs";
|
|
6
6
|
import { notOk, ok } from "@prisma-next/utils/result";
|
|
7
7
|
import { readFile } from "node:fs/promises";
|
|
8
8
|
import { hasMigrations } from "@prisma-next/framework-components/control";
|
|
@@ -101,4 +101,4 @@ function addMigrationCommandOptions(command) {
|
|
|
101
101
|
//#endregion
|
|
102
102
|
export { prepareMigrationContext as n, addMigrationCommandOptions as t };
|
|
103
103
|
|
|
104
|
-
//# sourceMappingURL=migration-command-scaffold-
|
|
104
|
+
//# sourceMappingURL=migration-command-scaffold-B8J702Uh.mjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"migration-command-scaffold-
|
|
1
|
+
{"version":3,"file":"migration-command-scaffold-B8J702Uh.mjs","names":[],"sources":["../src/utils/migration-command-scaffold.ts"],"sourcesContent":["import { readFile } from 'node:fs/promises';\nimport { relative, resolve } from 'node:path';\nimport { hasMigrations } from '@prisma-next/framework-components/control';\nimport { notOk, ok, type Result } from '@prisma-next/utils/result';\nimport type { Command } from 'commander';\nimport { loadConfig } from '../config-loader';\nimport { createControlClient } from '../control-api/client';\nimport type { ControlClient } from '../control-api/types';\nimport {\n type CliStructuredError,\n errorContractValidationFailed,\n errorDatabaseConnectionRequired,\n errorDriverRequired,\n errorFileNotFound,\n errorTargetMigrationNotSupported,\n errorUnexpected,\n} from './cli-errors';\nimport { addGlobalOptions, maskConnectionUrl, resolveContractPath } from './command-helpers';\nimport { formatStyledHeader } from './formatters/styled';\nimport type { GlobalFlags } from './global-flags';\nimport { createProgressAdapter } from './progress-adapter';\nimport type { TerminalUI } from './terminal-ui';\n\n/**\n * Resolved context for a migration command.\n * Contains everything needed to invoke a control-api operation.\n */\nexport interface MigrationContext {\n readonly client: ControlClient;\n readonly contractJson: Record<string, unknown>;\n readonly dbConnection: unknown;\n readonly onProgress: ReturnType<typeof createProgressAdapter>;\n readonly configPath: string;\n readonly contractPath: string;\n readonly contractPathAbsolute: string;\n readonly config: Awaited<ReturnType<typeof loadConfig>>;\n}\n\n/**\n * Command-specific configuration for the shared scaffold.\n */\nexport interface MigrationCommandDescriptor {\n readonly commandName: string;\n readonly description: string;\n readonly url: string;\n}\n\n/**\n * Prepares the shared context for migration commands (db init, db update).\n *\n * Handles: config loading, contract file reading, JSON parsing, connection resolution,\n * driver/migration-support validation, client creation, and header output.\n *\n * Returns a Result with either the resolved context or a structured error.\n */\nexport async function prepareMigrationContext(\n options: { readonly db?: string; readonly config?: string; readonly dryRun?: boolean },\n flags: GlobalFlags,\n ui: TerminalUI,\n descriptor: MigrationCommandDescriptor,\n): Promise<Result<MigrationContext, CliStructuredError>> {\n // Load config\n const config = await loadConfig(options.config);\n const configPath = options.config\n ? relative(process.cwd(), resolve(options.config))\n : 'prisma-next.config.ts';\n const contractPathAbsolute = resolveContractPath(config);\n const contractPath = relative(process.cwd(), contractPathAbsolute);\n\n // Output header to stderr (decoration)\n if (!flags.json && !flags.quiet) {\n const details: Array<{ label: string; value: string }> = [\n { label: 'config', value: configPath },\n { label: 'contract', value: contractPath },\n ];\n if (options.db) {\n details.push({ label: 'database', value: maskConnectionUrl(options.db) });\n }\n if (options.dryRun) {\n details.push({ label: 'mode', value: 'dry run' });\n }\n const header = formatStyledHeader({\n command: descriptor.commandName,\n description: descriptor.description,\n url: descriptor.url,\n details,\n flags,\n });\n ui.stderr(header);\n }\n\n // Load contract file\n let contractJsonContent: string;\n try {\n contractJsonContent = await readFile(contractPathAbsolute, 'utf-8');\n } catch (error) {\n if (error instanceof Error && (error as { code?: string }).code === 'ENOENT') {\n return notOk(\n errorFileNotFound(contractPathAbsolute, {\n why: `Contract file not found at ${contractPathAbsolute}`,\n fix: `Run \\`prisma-next contract emit\\` to generate ${contractPath}, or update \\`config.contract.output\\` in ${configPath}`,\n }),\n );\n }\n return notOk(\n errorUnexpected(error instanceof Error ? error.message : String(error), {\n why: `Failed to read contract file: ${error instanceof Error ? error.message : String(error)}`,\n }),\n );\n }\n\n // Parse contract JSON\n let contractJson: Record<string, unknown>;\n try {\n contractJson = JSON.parse(contractJsonContent) as Record<string, unknown>;\n } catch (error) {\n return notOk(\n errorContractValidationFailed(\n `Contract JSON is invalid: ${error instanceof Error ? error.message : String(error)}`,\n { where: { path: contractPathAbsolute } },\n ),\n );\n }\n\n // Resolve database connection (--db flag or config.db.connection)\n const dbConnection = options.db ?? config.db?.connection;\n if (!dbConnection) {\n return notOk(\n errorDatabaseConnectionRequired({\n why: `Database connection is required for ${descriptor.commandName} (set db.connection in ${configPath}, or pass --db <url>)`,\n commandName: descriptor.commandName,\n }),\n );\n }\n\n // Check for driver\n if (!config.driver) {\n return notOk(\n errorDriverRequired({ why: `Config.driver is required for ${descriptor.commandName}` }),\n );\n }\n\n if (!hasMigrations(config.target)) {\n return notOk(\n errorTargetMigrationNotSupported({\n why: `Target \"${config.target.id}\" does not support migrations`,\n }),\n );\n }\n\n // Create control client\n const client = createControlClient({\n family: config.family,\n target: config.target,\n adapter: config.adapter,\n driver: config.driver,\n extensionPacks: config.extensionPacks ?? [],\n });\n\n // Create progress adapter\n const onProgress = createProgressAdapter({ ui, flags });\n\n return ok({\n client,\n contractJson,\n dbConnection,\n onProgress,\n configPath,\n contractPath,\n contractPathAbsolute,\n config,\n });\n}\n\n/**\n * Registers the shared CLI options for migration commands (db init, db update).\n */\nexport function addMigrationCommandOptions(command: Command): Command {\n addGlobalOptions(command);\n return command\n .option('--db <url>', 'Database connection string')\n .option('--config <path>', 'Path to prisma-next.config.ts')\n .option('--dry-run', 'Preview planned operations without applying', false);\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;AAuDA,eAAsB,wBACpB,SACA,OACA,IACA,YACuD;CAEvD,MAAM,SAAS,MAAM,WAAW,QAAQ,OAAO;CAC/C,MAAM,aAAa,QAAQ,SACvB,SAAS,QAAQ,KAAK,EAAE,QAAQ,QAAQ,OAAO,CAAC,GAChD;CACJ,MAAM,uBAAuB,oBAAoB,OAAO;CACxD,MAAM,eAAe,SAAS,QAAQ,KAAK,EAAE,qBAAqB;CAGlE,IAAI,CAAC,MAAM,QAAQ,CAAC,MAAM,OAAO;EAC/B,MAAM,UAAmD,CACvD;GAAE,OAAO;GAAU,OAAO;GAAY,EACtC;GAAE,OAAO;GAAY,OAAO;GAAc,CAC3C;EACD,IAAI,QAAQ,IACV,QAAQ,KAAK;GAAE,OAAO;GAAY,OAAO,kBAAkB,QAAQ,GAAG;GAAE,CAAC;EAE3E,IAAI,QAAQ,QACV,QAAQ,KAAK;GAAE,OAAO;GAAQ,OAAO;GAAW,CAAC;EAEnD,MAAM,SAAS,mBAAmB;GAChC,SAAS,WAAW;GACpB,aAAa,WAAW;GACxB,KAAK,WAAW;GAChB;GACA;GACD,CAAC;EACF,GAAG,OAAO,OAAO;;CAInB,IAAI;CACJ,IAAI;EACF,sBAAsB,MAAM,SAAS,sBAAsB,QAAQ;UAC5D,OAAO;EACd,IAAI,iBAAiB,SAAU,MAA4B,SAAS,UAClE,OAAO,MACL,kBAAkB,sBAAsB;GACtC,KAAK,8BAA8B;GACnC,KAAK,iDAAiD,aAAa,4CAA4C;GAChH,CAAC,CACH;EAEH,OAAO,MACL,gBAAgB,iBAAiB,QAAQ,MAAM,UAAU,OAAO,MAAM,EAAE,EACtE,KAAK,iCAAiC,iBAAiB,QAAQ,MAAM,UAAU,OAAO,MAAM,IAC7F,CAAC,CACH;;CAIH,IAAI;CACJ,IAAI;EACF,eAAe,KAAK,MAAM,oBAAoB;UACvC,OAAO;EACd,OAAO,MACL,8BACE,6BAA6B,iBAAiB,QAAQ,MAAM,UAAU,OAAO,MAAM,IACnF,EAAE,OAAO,EAAE,MAAM,sBAAsB,EAAE,CAC1C,CACF;;CAIH,MAAM,eAAe,QAAQ,MAAM,OAAO,IAAI;CAC9C,IAAI,CAAC,cACH,OAAO,MACL,gCAAgC;EAC9B,KAAK,uCAAuC,WAAW,YAAY,yBAAyB,WAAW;EACvG,aAAa,WAAW;EACzB,CAAC,CACH;CAIH,IAAI,CAAC,OAAO,QACV,OAAO,MACL,oBAAoB,EAAE,KAAK,iCAAiC,WAAW,eAAe,CAAC,CACxF;CAGH,IAAI,CAAC,cAAc,OAAO,OAAO,EAC/B,OAAO,MACL,iCAAiC,EAC/B,KAAK,WAAW,OAAO,OAAO,GAAG,gCAClC,CAAC,CACH;CAIH,MAAM,SAAS,oBAAoB;EACjC,QAAQ,OAAO;EACf,QAAQ,OAAO;EACf,SAAS,OAAO;EAChB,QAAQ,OAAO;EACf,gBAAgB,OAAO,kBAAkB,EAAE;EAC5C,CAAC;CAGF,MAAM,aAAa,sBAAsB;EAAE;EAAI;EAAO,CAAC;CAEvD,OAAO,GAAG;EACR;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACD,CAAC;;;;;AAMJ,SAAgB,2BAA2B,SAA2B;CACpE,iBAAiB,QAAQ;CACzB,OAAO,QACJ,OAAO,cAAc,6BAA6B,CAClD,OAAO,mBAAmB,gCAAgC,CAC1D,OAAO,aAAa,+CAA+C,MAAM"}
|
|
@@ -0,0 +1,530 @@
|
|
|
1
|
+
import { t as loadConfig } from "./config-loader-B6sJjXTv.mjs";
|
|
2
|
+
import { _ as errorUnexpected, a as errorContractValidationFailed, f as errorMigrationPlanningFailed, h as errorTargetMigrationNotSupported, l as errorFileNotFound, m as errorRuntime, t as CliStructuredError, v as mapMigrationToolsError } from "./cli-errors-D3_sMh2K.mjs";
|
|
3
|
+
import { t as assertFrameworkComponentsCompatible } from "./framework-components-gwAHl7ml.mjs";
|
|
4
|
+
import { t as TerminalUI } from "./terminal-ui-zaRDhJnP.mjs";
|
|
5
|
+
import { a as loadMigrationPackages, c as resolveContractPath, d as setCommandDescriptions, f as setCommandExamples, g as parseGlobalFlags, i as getTargetMigrations, l as resolveMigrationPaths, n as addGlobalOptions, t as handleResult, y as formatStyledHeader } from "./result-handler-DWb1rFS-.mjs";
|
|
6
|
+
import { Command } from "commander";
|
|
7
|
+
import { getEmittedArtifactPaths } from "@prisma-next/emitter";
|
|
8
|
+
import { notOk, ok } from "@prisma-next/utils/result";
|
|
9
|
+
import { join, relative } from "pathe";
|
|
10
|
+
import { readFile } from "node:fs/promises";
|
|
11
|
+
import { APP_SPACE_ID, createControlStack, hasOperationPreview } from "@prisma-next/framework-components/control";
|
|
12
|
+
import { copyFilesWithRename, formatMigrationDirName, materialiseExtensionMigrationPackageIfMissing, writeMigrationPackage } from "@prisma-next/migration-tools/io";
|
|
13
|
+
import { findLatestMigration } from "@prisma-next/migration-tools/migration-graph";
|
|
14
|
+
import { detectSpaceContractDrift, emitContractSpaceArtefacts, planAllSpaces, readContractSpaceHeadRef, spaceMigrationDirectory } from "@prisma-next/migration-tools/spaces";
|
|
15
|
+
import { MigrationToolsError } from "@prisma-next/migration-tools/errors";
|
|
16
|
+
import { computeMigrationHash } from "@prisma-next/migration-tools/hash";
|
|
17
|
+
import { writeMigrationTs } from "@prisma-next/migration-tools/migration-ts";
|
|
18
|
+
import { deriveProvidedInvariants } from "@prisma-next/migration-tools/invariants";
|
|
19
|
+
//#region src/utils/contract-space-extension-migrations-pass.ts
|
|
20
|
+
/**
|
|
21
|
+
* Materialise an extension's pre-built migration packages onto disk
|
|
22
|
+
* under `migrations/<spaceId>/<dirName>/` for every package that does
|
|
23
|
+
* not yet exist there.
|
|
24
|
+
*
|
|
25
|
+
* Helper-location pattern — the per-space "planner" for extension
|
|
26
|
+
* spaces is a no-op that just returns the descriptor's `migrations`
|
|
27
|
+
* verbatim; the value `planAllSpaces` brings to this consumer site is
|
|
28
|
+
* **deterministic ordering** (alphabetical by spaceId) and
|
|
29
|
+
* **duplicate-spaceId detection**. The actual write is performed via
|
|
30
|
+
* `materialiseMigrationPackage` per package.
|
|
31
|
+
*
|
|
32
|
+
* Idempotent: an existing `migrations/<spaceId>/<dirName>/` is left
|
|
33
|
+
* untouched and reported in `result.skipped` — the helper never
|
|
34
|
+
* overwrites authored migration content, ensuring re-running
|
|
35
|
+
* `migrate` does not corrupt or churn extension migration packages.
|
|
36
|
+
*
|
|
37
|
+
* Per-space artefacts (`contract.json`, `contract.d.ts`,
|
|
38
|
+
* `refs/head.json`) are emitted by
|
|
39
|
+
* {@link import('./contract-space-migrate-pass').runContractSpaceMigratePass}
|
|
40
|
+
* separately — they cover the head-pointer side of the ledger. This
|
|
41
|
+
* helper covers the migration-graph side.
|
|
42
|
+
*/
|
|
43
|
+
async function runContractSpaceExtensionMigrationsPass(inputs) {
|
|
44
|
+
const planned = planAllSpaces(inputs.extensionPacks.filter((pack) => pack.contractSpace !== void 0).map((pack) => ({
|
|
45
|
+
spaceId: pack.id,
|
|
46
|
+
priorContract: null,
|
|
47
|
+
newContract: pack.contractSpace.contractJson,
|
|
48
|
+
__migrations: pack.contractSpace.migrations
|
|
49
|
+
})), (input) => input.__migrations);
|
|
50
|
+
const emitted = [];
|
|
51
|
+
const skipped = [];
|
|
52
|
+
for (const space of planned) {
|
|
53
|
+
const spaceDir = spaceMigrationDirectory(inputs.migrationsDir, space.spaceId);
|
|
54
|
+
for (const pkg of space.migrationPackages) {
|
|
55
|
+
const { written } = await materialiseExtensionMigrationPackageIfMissing(spaceDir, pkg);
|
|
56
|
+
if (written) emitted.push({
|
|
57
|
+
spaceId: space.spaceId,
|
|
58
|
+
dirName: pkg.dirName
|
|
59
|
+
});
|
|
60
|
+
else skipped.push({
|
|
61
|
+
spaceId: space.spaceId,
|
|
62
|
+
dirName: pkg.dirName
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
return {
|
|
67
|
+
emitted,
|
|
68
|
+
skipped
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
//#endregion
|
|
72
|
+
//#region src/utils/contract-space-migrate-pass.ts
|
|
73
|
+
/**
|
|
74
|
+
* Run drift detection + on-disk artefact emission for every loaded
|
|
75
|
+
* extension space at `migrate` time.
|
|
76
|
+
*
|
|
77
|
+
* Per sub-spec § 3:
|
|
78
|
+
*
|
|
79
|
+
* - For each declared extension that exposes a `contractSpace`:
|
|
80
|
+
* - Read the on-disk head hash from `migrations/<spaceId>/refs/head.json`
|
|
81
|
+
* (returns `null` on first emit).
|
|
82
|
+
* - Compare against the descriptor's `headRef.hash` via
|
|
83
|
+
* `detectSpaceContractDrift`. The `kind` discriminant decides whether
|
|
84
|
+
* the user sees a warning (`drift`), a no-op silent emit (`firstEmit`,
|
|
85
|
+
* `noDrift`), or nothing at all.
|
|
86
|
+
* - Always re-emit the on-disk artefacts (`contract.json`, `contract.d.ts`,
|
|
87
|
+
* `refs/head.json`). The framework owns these files and the helper is
|
|
88
|
+
* idempotent.
|
|
89
|
+
*
|
|
90
|
+
* Drift warnings are returned to the caller for formatting (TerminalUI,
|
|
91
|
+
* structured-output envelope, etc.) — the helper does not print directly,
|
|
92
|
+
* keeping it framework-neutral and unit-testable.
|
|
93
|
+
*
|
|
94
|
+
* Extension migration packages (the descriptor's pre-canned `migrations`
|
|
95
|
+
* array → `migrations/<spaceId>/<dirName>/`) are intentionally not
|
|
96
|
+
* materialised here — that interaction will be wired in a follow-on round
|
|
97
|
+
* once the runner-side single-tx slice (sub-spec § 6) is in place.
|
|
98
|
+
* On-disk artefacts are sufficient to lock the drift-warning behaviour
|
|
99
|
+
* and the always-on re-emit AC for R2.
|
|
100
|
+
*
|
|
101
|
+
* @see specs/framework-mechanism.spec.md § 3 — Drift detection (T1.9).
|
|
102
|
+
*/
|
|
103
|
+
async function runContractSpaceMigratePass(inputs) {
|
|
104
|
+
const drifts = [];
|
|
105
|
+
const emittedSpaceIds = [];
|
|
106
|
+
for (const pack of inputs.extensionPacks) {
|
|
107
|
+
if (pack.contractSpace === void 0) continue;
|
|
108
|
+
const { contractJson, headRef } = pack.contractSpace;
|
|
109
|
+
const onDiskHeadRef = await readContractSpaceHeadRef(inputs.migrationsDir, pack.id);
|
|
110
|
+
const drift = detectSpaceContractDrift(pack.id, {
|
|
111
|
+
descriptorHash: headRef.hash,
|
|
112
|
+
priorHeadHash: onDiskHeadRef?.hash ?? null
|
|
113
|
+
});
|
|
114
|
+
drifts.push(drift);
|
|
115
|
+
await emitContractSpaceArtefacts(inputs.migrationsDir, pack.id, {
|
|
116
|
+
contract: contractJson,
|
|
117
|
+
contractDts: buildPlaceholderContractDts(pack.id),
|
|
118
|
+
headRef: {
|
|
119
|
+
hash: headRef.hash,
|
|
120
|
+
invariants: headRef.invariants
|
|
121
|
+
}
|
|
122
|
+
});
|
|
123
|
+
emittedSpaceIds.push(pack.id);
|
|
124
|
+
}
|
|
125
|
+
return {
|
|
126
|
+
drifts,
|
|
127
|
+
emittedSpaceIds
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
/**
|
|
131
|
+
* Format the user-facing drift warning for a single space. Callers
|
|
132
|
+
* funnel this through their preferred output channel (TerminalUI line,
|
|
133
|
+
* structured-output envelope `warnings[]`, etc.).
|
|
134
|
+
*
|
|
135
|
+
* Locks AM7 — drift warning surfaces the extension name and the diff
|
|
136
|
+
* direction (descriptor → on-disk head).
|
|
137
|
+
*/
|
|
138
|
+
function formatContractSpaceDriftWarning(drift) {
|
|
139
|
+
if (drift.kind !== "drift") throw new Error(`formatContractSpaceDriftWarning called with non-drift result: ${drift.kind}`);
|
|
140
|
+
return `Contract-space drift detected for "${drift.spaceId}": descriptor hash ${drift.descriptorHash} differs from on-disk head hash ${drift.priorHeadHash ?? "<none>"}. The on-disk artefacts under migrations/${drift.spaceId}/ will be refreshed to match the descriptor.`;
|
|
141
|
+
}
|
|
142
|
+
/**
|
|
143
|
+
* Placeholder `.d.ts` content for an extension space's on-disk mirror.
|
|
144
|
+
*
|
|
145
|
+
* Rendering a fully-typed `.d.ts` for an extension contract requires the
|
|
146
|
+
* SQL-family renderer with the codec / typemap registry threaded
|
|
147
|
+
* through; that integration is tracked under sub-spec Open Question 3
|
|
148
|
+
* (see `projects/extension-contract-spaces/specs/framework-mechanism.spec.md`).
|
|
149
|
+
*
|
|
150
|
+
* Until that ships, the on-disk `.d.ts` is a `@ts-nocheck` stub. The
|
|
151
|
+
* spec gap closing alongside the typed renderer is **AC2 / AC14**
|
|
152
|
+
* (byte-equivalence of per-space artefacts under `migrate`):
|
|
153
|
+
* a placeholder cannot be byte-equal to a fully-rendered `.d.ts` from
|
|
154
|
+
* the same descriptor, so AC2 / AC14 are PARTIAL today and become
|
|
155
|
+
* fully-PASS once OQ3 closes.
|
|
156
|
+
*
|
|
157
|
+
* Scheduled to close in **M3** (cipherstash editor tooling) — that's
|
|
158
|
+
* the milestone where the typed renderer gets its first real
|
|
159
|
+
* extension-space consumer and the byte-equivalence guarantee is
|
|
160
|
+
* practically required.
|
|
161
|
+
*/
|
|
162
|
+
function buildPlaceholderContractDts(spaceId) {
|
|
163
|
+
return [
|
|
164
|
+
"// @ts-nocheck",
|
|
165
|
+
"/**",
|
|
166
|
+
` * Placeholder \`.d.ts\` for extension space "${spaceId}".`,
|
|
167
|
+
" *",
|
|
168
|
+
" * The framework re-emits this file on every `migrate` run alongside",
|
|
169
|
+
" * `contract.json` and `refs/head.json`. A typed `.d.ts` rendering",
|
|
170
|
+
" * pass for extension contracts is tracked under the project's open",
|
|
171
|
+
" * questions; until that ships, consumers should import",
|
|
172
|
+
" * `contract.json` directly with `validateContract<…>(…)`.",
|
|
173
|
+
" */",
|
|
174
|
+
"export {};",
|
|
175
|
+
""
|
|
176
|
+
].join("\n");
|
|
177
|
+
}
|
|
178
|
+
//#endregion
|
|
179
|
+
//#region src/commands/migration-plan.ts
|
|
180
|
+
async function executeMigrationPlanCommand(options, flags, ui, startTime) {
|
|
181
|
+
const config = await loadConfig(options.config);
|
|
182
|
+
const { configPath, migrationsDir, appMigrationsDir, appMigrationsRelative } = resolveMigrationPaths(options.config, config);
|
|
183
|
+
const contractPathAbsolute = resolveContractPath(config);
|
|
184
|
+
const contractPath = relative(process.cwd(), contractPathAbsolute);
|
|
185
|
+
if (!flags.json && !flags.quiet) {
|
|
186
|
+
const details = [
|
|
187
|
+
{
|
|
188
|
+
label: "config",
|
|
189
|
+
value: configPath
|
|
190
|
+
},
|
|
191
|
+
{
|
|
192
|
+
label: "contract",
|
|
193
|
+
value: contractPath
|
|
194
|
+
},
|
|
195
|
+
{
|
|
196
|
+
label: "migrations",
|
|
197
|
+
value: appMigrationsRelative
|
|
198
|
+
}
|
|
199
|
+
];
|
|
200
|
+
if (options.from) details.push({
|
|
201
|
+
label: "from",
|
|
202
|
+
value: options.from
|
|
203
|
+
});
|
|
204
|
+
if (options.name) details.push({
|
|
205
|
+
label: "name",
|
|
206
|
+
value: options.name
|
|
207
|
+
});
|
|
208
|
+
const header = formatStyledHeader({
|
|
209
|
+
command: "migration plan",
|
|
210
|
+
description: "Plan a migration from contract changes",
|
|
211
|
+
url: "https://pris.ly/migration-plan",
|
|
212
|
+
details,
|
|
213
|
+
flags
|
|
214
|
+
});
|
|
215
|
+
ui.stderr(header);
|
|
216
|
+
}
|
|
217
|
+
let contractJsonContent;
|
|
218
|
+
try {
|
|
219
|
+
contractJsonContent = await readFile(contractPathAbsolute, "utf-8");
|
|
220
|
+
} catch (error) {
|
|
221
|
+
if (error instanceof Error && error.code === "ENOENT") return notOk(errorFileNotFound(contractPathAbsolute, {
|
|
222
|
+
why: `Contract file not found at ${contractPathAbsolute}`,
|
|
223
|
+
fix: `Run \`prisma-next contract emit\` to generate ${contractPath}, or update \`config.contract.output\` in ${configPath}`
|
|
224
|
+
}));
|
|
225
|
+
return notOk(errorUnexpected(error instanceof Error ? error.message : String(error), { why: `Failed to read contract file: ${error instanceof Error ? error.message : String(error)}` }));
|
|
226
|
+
}
|
|
227
|
+
let toContractJson;
|
|
228
|
+
try {
|
|
229
|
+
toContractJson = JSON.parse(contractJsonContent);
|
|
230
|
+
} catch (error) {
|
|
231
|
+
return notOk(errorContractValidationFailed(`Contract JSON is invalid: ${error instanceof Error ? error.message : String(error)}`, { where: { path: contractPathAbsolute } }));
|
|
232
|
+
}
|
|
233
|
+
const rawStorageHash = toContractJson.storage?.storageHash;
|
|
234
|
+
if (typeof rawStorageHash !== "string") return notOk(errorContractValidationFailed("Contract is missing storageHash", { where: { path: contractPathAbsolute } }));
|
|
235
|
+
const toStorageHash = rawStorageHash;
|
|
236
|
+
let fromContract = null;
|
|
237
|
+
let fromHash = null;
|
|
238
|
+
let fromContractSourceDir = null;
|
|
239
|
+
try {
|
|
240
|
+
const { bundles, graph } = await loadMigrationPackages(appMigrationsDir);
|
|
241
|
+
if (options.from) {
|
|
242
|
+
const resolved = resolveBundleByPrefix(bundles, options.from);
|
|
243
|
+
if (!resolved.ok) {
|
|
244
|
+
const f = resolved.failure;
|
|
245
|
+
return notOk(f.reason === "ambiguous" ? errorRuntime("Multiple matching migrations found", {
|
|
246
|
+
why: `Prefix "${options.from}" matches ${f.count} migrations in ${appMigrationsRelative}`,
|
|
247
|
+
fix: "Provide a longer prefix to disambiguate, or omit --from to use the latest migration target."
|
|
248
|
+
}) : errorRuntime("Starting contract not found", {
|
|
249
|
+
why: `No migration with to hash matching "${options.from}" exists in ${appMigrationsRelative}`,
|
|
250
|
+
fix: "Check that the --from hash matches a known migration target hash, or omit --from to use the latest migration target."
|
|
251
|
+
}));
|
|
252
|
+
}
|
|
253
|
+
fromHash = resolved.value.metadata.to;
|
|
254
|
+
fromContract = resolved.value.metadata.toContract;
|
|
255
|
+
fromContractSourceDir = resolved.value.dirPath;
|
|
256
|
+
} else {
|
|
257
|
+
const latestMigration = findLatestMigration(graph);
|
|
258
|
+
if (latestMigration) {
|
|
259
|
+
fromHash = latestMigration.to;
|
|
260
|
+
const leafPkg = bundles.find((p) => p.metadata.migrationHash === latestMigration.migrationHash);
|
|
261
|
+
if (leafPkg) {
|
|
262
|
+
fromContract = leafPkg.metadata.toContract;
|
|
263
|
+
fromContractSourceDir = leafPkg.dirPath;
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
} catch (error) {
|
|
268
|
+
if (MigrationToolsError.is(error)) return notOk(mapMigrationToolsError(error));
|
|
269
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
270
|
+
return notOk(errorUnexpected(message, { why: `Unexpected error while loading migrations: ${message}` }));
|
|
271
|
+
}
|
|
272
|
+
const migratePass = await runContractSpaceMigratePass({
|
|
273
|
+
migrationsDir,
|
|
274
|
+
extensionPacks: (config.extensionPacks ?? []).map((pack) => {
|
|
275
|
+
const cs = pack.contractSpace;
|
|
276
|
+
return cs !== void 0 ? {
|
|
277
|
+
id: pack.id,
|
|
278
|
+
contractSpace: cs
|
|
279
|
+
} : { id: pack.id };
|
|
280
|
+
})
|
|
281
|
+
});
|
|
282
|
+
if (!flags.json && !flags.quiet) {
|
|
283
|
+
for (const drift of migratePass.drifts) if (drift.kind === "drift") ui.stderr(formatContractSpaceDriftWarning(drift));
|
|
284
|
+
}
|
|
285
|
+
const extensionMigrationsResult = await runContractSpaceExtensionMigrationsPass({
|
|
286
|
+
migrationsDir,
|
|
287
|
+
extensionPacks: (config.extensionPacks ?? []).map((pack) => {
|
|
288
|
+
const cs = pack.contractSpace;
|
|
289
|
+
return cs !== void 0 ? {
|
|
290
|
+
id: pack.id,
|
|
291
|
+
contractSpace: cs
|
|
292
|
+
} : { id: pack.id };
|
|
293
|
+
})
|
|
294
|
+
});
|
|
295
|
+
if (!flags.json && !flags.quiet) for (const entry of extensionMigrationsResult.emitted) ui.step(`Emitted ${entry.spaceId}/${entry.dirName}`);
|
|
296
|
+
if (fromHash === toStorageHash) return ok({
|
|
297
|
+
ok: true,
|
|
298
|
+
noOp: true,
|
|
299
|
+
from: fromHash,
|
|
300
|
+
to: toStorageHash,
|
|
301
|
+
operations: [],
|
|
302
|
+
summary: "No changes detected between contracts",
|
|
303
|
+
timings: { total: Date.now() - startTime }
|
|
304
|
+
});
|
|
305
|
+
const migrations = getTargetMigrations(config.target);
|
|
306
|
+
if (!migrations) return notOk(errorTargetMigrationNotSupported({ why: `Target "${config.target.id}" does not support migrations` }));
|
|
307
|
+
const frameworkComponents = assertFrameworkComponentsCompatible(config.family.familyId, config.target.targetId, [
|
|
308
|
+
config.target,
|
|
309
|
+
config.adapter,
|
|
310
|
+
...config.extensionPacks ?? []
|
|
311
|
+
]);
|
|
312
|
+
const timestamp = /* @__PURE__ */ new Date();
|
|
313
|
+
const packageDir = join(appMigrationsDir, formatMigrationDirName(timestamp, options.name ?? "migration"));
|
|
314
|
+
const baseMetadata = {
|
|
315
|
+
from: fromHash,
|
|
316
|
+
to: toStorageHash,
|
|
317
|
+
fromContract,
|
|
318
|
+
toContract: toContractJson,
|
|
319
|
+
hints: {
|
|
320
|
+
used: [],
|
|
321
|
+
applied: [],
|
|
322
|
+
plannerVersion: "2.0.0"
|
|
323
|
+
},
|
|
324
|
+
labels: [],
|
|
325
|
+
createdAt: timestamp.toISOString()
|
|
326
|
+
};
|
|
327
|
+
try {
|
|
328
|
+
const stack = createControlStack(config);
|
|
329
|
+
const familyInstance = config.family.create(stack);
|
|
330
|
+
const planner = migrations.createPlanner(familyInstance);
|
|
331
|
+
const fromSchema = migrations.contractToSchema(fromContract, frameworkComponents);
|
|
332
|
+
const plannerResult = planner.plan({
|
|
333
|
+
contract: toContractJson,
|
|
334
|
+
schema: fromSchema,
|
|
335
|
+
policy: { allowedOperationClasses: [
|
|
336
|
+
"additive",
|
|
337
|
+
"widening",
|
|
338
|
+
"destructive",
|
|
339
|
+
"data"
|
|
340
|
+
] },
|
|
341
|
+
fromContract,
|
|
342
|
+
frameworkComponents,
|
|
343
|
+
spaceId: APP_SPACE_ID
|
|
344
|
+
});
|
|
345
|
+
if (plannerResult.kind === "failure") return notOk(errorMigrationPlanningFailed({ conflicts: plannerResult.conflicts }));
|
|
346
|
+
let plannedOps = [];
|
|
347
|
+
let hasPlaceholders = false;
|
|
348
|
+
try {
|
|
349
|
+
plannedOps = plannerResult.plan.operations;
|
|
350
|
+
if (plannedOps.length === 0) return notOk(errorMigrationPlanningFailed({ conflicts: [{
|
|
351
|
+
kind: "unsupportedChange",
|
|
352
|
+
summary: "Contract changed but planner produced no operations. This indicates unsupported or ignored changes."
|
|
353
|
+
}] }));
|
|
354
|
+
} catch (e) {
|
|
355
|
+
if (CliStructuredError.is(e) && e.domain === "MIG" && e.code === "2001") hasPlaceholders = true;
|
|
356
|
+
else throw e;
|
|
357
|
+
}
|
|
358
|
+
const migrationTsContent = plannerResult.plan.renderTypeScript();
|
|
359
|
+
const opsForWrite = hasPlaceholders ? [] : plannedOps;
|
|
360
|
+
const metadataWithInvariants = {
|
|
361
|
+
...baseMetadata,
|
|
362
|
+
providedInvariants: deriveProvidedInvariants(opsForWrite)
|
|
363
|
+
};
|
|
364
|
+
await writeMigrationPackage(packageDir, {
|
|
365
|
+
...metadataWithInvariants,
|
|
366
|
+
migrationHash: computeMigrationHash(metadataWithInvariants, opsForWrite)
|
|
367
|
+
}, opsForWrite);
|
|
368
|
+
const destinationArtifacts = getEmittedArtifactPaths(contractPathAbsolute);
|
|
369
|
+
await copyFilesWithRename(packageDir, [{
|
|
370
|
+
sourcePath: destinationArtifacts.jsonPath,
|
|
371
|
+
destName: "end-contract.json"
|
|
372
|
+
}, {
|
|
373
|
+
sourcePath: destinationArtifacts.dtsPath,
|
|
374
|
+
destName: "end-contract.d.ts"
|
|
375
|
+
}]);
|
|
376
|
+
if (fromContractSourceDir !== null) {
|
|
377
|
+
const sourceArtifacts = getEmittedArtifactPaths(join(fromContractSourceDir, "end-contract.json"));
|
|
378
|
+
await copyFilesWithRename(packageDir, [{
|
|
379
|
+
sourcePath: sourceArtifacts.jsonPath,
|
|
380
|
+
destName: "start-contract.json"
|
|
381
|
+
}, {
|
|
382
|
+
sourcePath: sourceArtifacts.dtsPath,
|
|
383
|
+
destName: "start-contract.d.ts"
|
|
384
|
+
}]);
|
|
385
|
+
}
|
|
386
|
+
await writeMigrationTs(packageDir, migrationTsContent);
|
|
387
|
+
if (hasPlaceholders) return ok({
|
|
388
|
+
ok: true,
|
|
389
|
+
noOp: false,
|
|
390
|
+
from: fromHash,
|
|
391
|
+
to: toStorageHash,
|
|
392
|
+
dir: relative(process.cwd(), packageDir),
|
|
393
|
+
operations: [],
|
|
394
|
+
pendingPlaceholders: true,
|
|
395
|
+
summary: "Planned migration with placeholder(s) — edit migration.ts then run `node migration.ts` to self-emit",
|
|
396
|
+
timings: { total: Date.now() - startTime }
|
|
397
|
+
});
|
|
398
|
+
const preview = hasOperationPreview(familyInstance) ? familyInstance.toOperationPreview(plannedOps) : void 0;
|
|
399
|
+
return ok({
|
|
400
|
+
ok: true,
|
|
401
|
+
noOp: false,
|
|
402
|
+
from: fromHash,
|
|
403
|
+
to: toStorageHash,
|
|
404
|
+
dir: relative(process.cwd(), packageDir),
|
|
405
|
+
operations: plannedOps.map((op) => ({
|
|
406
|
+
id: op.id,
|
|
407
|
+
label: op.label,
|
|
408
|
+
operationClass: op.operationClass
|
|
409
|
+
})),
|
|
410
|
+
...preview !== void 0 ? { preview } : {},
|
|
411
|
+
summary: `Planned ${plannedOps.length} operation(s)`,
|
|
412
|
+
timings: { total: Date.now() - startTime }
|
|
413
|
+
});
|
|
414
|
+
} catch (error) {
|
|
415
|
+
if (CliStructuredError.is(error)) return notOk(error);
|
|
416
|
+
if (MigrationToolsError.is(error)) return notOk(mapMigrationToolsError(error));
|
|
417
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
418
|
+
return notOk(errorUnexpected(message, { why: `Unexpected error during migration plan: ${message}` }));
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
function createMigrationPlanCommand() {
|
|
422
|
+
const command = new Command("plan");
|
|
423
|
+
setCommandDescriptions(command, "Plan a migration from contract changes", "Compares the emitted contract against the latest on-disk migration state and\nproduces a new migration package with the required operations. No database\nconnection is needed — this is a fully offline operation.");
|
|
424
|
+
setCommandExamples(command, ["prisma-next migration plan", "prisma-next migration plan --name add-users-table"]);
|
|
425
|
+
addGlobalOptions(command).option("--config <path>", "Path to prisma-next.config.ts").option("--name <slug>", "Name slug for the migration directory", "migration").option("--from <hash>", "Explicit starting contract hash (overrides latest migration target)").action(async (options) => {
|
|
426
|
+
const flags = parseGlobalFlags(options);
|
|
427
|
+
const startTime = Date.now();
|
|
428
|
+
const ui = new TerminalUI({
|
|
429
|
+
color: flags.color,
|
|
430
|
+
interactive: flags.interactive
|
|
431
|
+
});
|
|
432
|
+
const exitCode = handleResult(await executeMigrationPlanCommand(options, flags, ui, startTime), flags, ui, (planResult) => {
|
|
433
|
+
if (flags.json) ui.output(JSON.stringify(planResult, null, 2));
|
|
434
|
+
else if (!flags.quiet) ui.log(formatMigrationPlanOutput(planResult, flags));
|
|
435
|
+
});
|
|
436
|
+
process.exit(exitCode);
|
|
437
|
+
});
|
|
438
|
+
return command;
|
|
439
|
+
}
|
|
440
|
+
function formatMigrationPlanOutput(result, flags) {
|
|
441
|
+
const lines = [];
|
|
442
|
+
const useColor = flags.color !== false;
|
|
443
|
+
const green_ = useColor ? (s) => `\x1b[32m${s}\x1b[0m` : (s) => s;
|
|
444
|
+
const yellow_ = useColor ? (s) => `\x1b[33m${s}\x1b[0m` : (s) => s;
|
|
445
|
+
const dim_ = useColor ? (s) => `\x1b[2m${s}\x1b[0m` : (s) => s;
|
|
446
|
+
if (result.noOp) {
|
|
447
|
+
lines.push(`${green_("✔")} No changes detected`);
|
|
448
|
+
lines.push(dim_(` from: ${result.from}`));
|
|
449
|
+
lines.push(dim_(` to: ${result.to}`));
|
|
450
|
+
return lines.join("\n");
|
|
451
|
+
}
|
|
452
|
+
if (result.pendingPlaceholders) {
|
|
453
|
+
lines.push(`${yellow_("⚠")} ${result.summary}`);
|
|
454
|
+
lines.push("");
|
|
455
|
+
lines.push(dim_(`from: ${result.from}`));
|
|
456
|
+
lines.push(dim_(`to: ${result.to}`));
|
|
457
|
+
if (result.dir) lines.push(dim_(`dir: ${result.dir}`));
|
|
458
|
+
lines.push("");
|
|
459
|
+
lines.push("Open migration.ts and replace each `placeholder(...)` call with your actual query.");
|
|
460
|
+
lines.push(`Then run: ${green_(`node ${result.dir ?? "<dir>"}/migration.ts`)}`);
|
|
461
|
+
return lines.join("\n");
|
|
462
|
+
}
|
|
463
|
+
lines.push(`${green_("✔")} ${result.summary}`);
|
|
464
|
+
lines.push("");
|
|
465
|
+
if (result.operations.length > 0) {
|
|
466
|
+
lines.push(dim_("│"));
|
|
467
|
+
for (let i = 0; i < result.operations.length; i++) {
|
|
468
|
+
const op = result.operations[i];
|
|
469
|
+
const treeChar = i === result.operations.length - 1 ? "└" : "├";
|
|
470
|
+
const opClassLabel = op.operationClass === "destructive" ? yellow_(`[${op.operationClass}]`) : dim_(`[${op.operationClass}]`);
|
|
471
|
+
lines.push(`${dim_(treeChar)}─ ${op.label} ${opClassLabel}`);
|
|
472
|
+
}
|
|
473
|
+
if (result.operations.some((op) => op.operationClass === "destructive")) {
|
|
474
|
+
lines.push("");
|
|
475
|
+
lines.push(`${yellow_("⚠")} This migration contains destructive operations that may cause data loss.`);
|
|
476
|
+
}
|
|
477
|
+
lines.push("");
|
|
478
|
+
}
|
|
479
|
+
lines.push(dim_(`from: ${result.from}`));
|
|
480
|
+
lines.push(dim_(`to: ${result.to}`));
|
|
481
|
+
if (result.dir) lines.push(dim_(`dir: ${result.dir}`));
|
|
482
|
+
lines.push("");
|
|
483
|
+
lines.push(`Next: review ${green_(result.dir ?? "<dir>")} if needed, then run ${green_("prisma-next migration apply")}.`);
|
|
484
|
+
if (result.preview && result.preview.statements.length > 0) {
|
|
485
|
+
const allSql = result.preview.statements.every((s) => s.language === "sql");
|
|
486
|
+
lines.push("");
|
|
487
|
+
lines.push(dim_(allSql ? "DDL preview" : "Operation preview"));
|
|
488
|
+
lines.push("");
|
|
489
|
+
for (const statement of result.preview.statements) {
|
|
490
|
+
const trimmed = statement.text.trim();
|
|
491
|
+
if (!trimmed) continue;
|
|
492
|
+
const line = statement.language === "sql" && !trimmed.endsWith(";") ? `${trimmed};` : trimmed;
|
|
493
|
+
lines.push(line);
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
if (flags.verbose && result.timings) {
|
|
497
|
+
lines.push("");
|
|
498
|
+
lines.push(dim_(`Total time: ${result.timings.total}ms`));
|
|
499
|
+
}
|
|
500
|
+
return lines.join("\n");
|
|
501
|
+
}
|
|
502
|
+
/**
|
|
503
|
+
* Resolve a migration package by **target contract hash** (`metadata.to`)
|
|
504
|
+
* using exact match or prefix match.
|
|
505
|
+
*
|
|
506
|
+
* Note: matches `metadata.to` (the contract hash this migration produces),
|
|
507
|
+
* not `metadata.migrationHash` (the package's content-addressed identity).
|
|
508
|
+
* Tries exact match first, then prefix match (auto-prepending `sha256:` when
|
|
509
|
+
* the needle omits the scheme). Returns the matched package on success, or a
|
|
510
|
+
* discriminated failure indicating whether the prefix was ambiguous or simply
|
|
511
|
+
* not found.
|
|
512
|
+
*
|
|
513
|
+
* @internal Exported for testing only.
|
|
514
|
+
*/
|
|
515
|
+
function resolveBundleByPrefix(bundles, needle) {
|
|
516
|
+
const exact = bundles.find((p) => p.metadata.to === needle);
|
|
517
|
+
if (exact) return ok(exact);
|
|
518
|
+
const prefixWithScheme = needle.startsWith("sha256:") ? needle : `sha256:${needle}`;
|
|
519
|
+
const candidates = bundles.filter((p) => p.metadata.to.startsWith(prefixWithScheme));
|
|
520
|
+
if (candidates.length === 1) return ok(candidates[0]);
|
|
521
|
+
if (candidates.length > 1) return notOk({
|
|
522
|
+
reason: "ambiguous",
|
|
523
|
+
count: candidates.length
|
|
524
|
+
});
|
|
525
|
+
return notOk({ reason: "not-found" });
|
|
526
|
+
}
|
|
527
|
+
//#endregion
|
|
528
|
+
export { resolveBundleByPrefix as n, createMigrationPlanCommand as t };
|
|
529
|
+
|
|
530
|
+
//# sourceMappingURL=migration-plan-BcKNnTM7.mjs.map
|