@prisma-next/cli 0.11.0 → 0.12.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +13 -9
- package/dist/cli.mjs +259 -12
- package/dist/cli.mjs.map +1 -1
- package/dist/{client-oXO2WCPD.mjs → client-KgJorIvG.mjs} +72 -60
- package/dist/client-KgJorIvG.mjs.map +1 -0
- package/dist/{command-helpers-BSb0tRC8.mjs → command-helpers-Bbw1GbwL.mjs} +646 -46
- package/dist/command-helpers-Bbw1GbwL.mjs.map +1 -0
- package/dist/commands/contract-emit.d.mts.map +1 -1
- package/dist/commands/contract-emit.mjs +1 -1
- package/dist/commands/contract-infer.d.mts.map +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 +32 -7
- package/dist/commands/db-init.mjs.map +1 -1
- package/dist/commands/db-schema.d.mts.map +1 -1
- package/dist/commands/db-schema.mjs +3 -4
- package/dist/commands/db-schema.mjs.map +1 -1
- package/dist/commands/db-sign.d.mts.map +1 -1
- package/dist/commands/db-sign.mjs +12 -10
- package/dist/commands/db-sign.mjs.map +1 -1
- package/dist/commands/db-update.d.mts.map +1 -1
- package/dist/commands/db-update.mjs +41 -11
- 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 -1
- package/dist/commands/migrate.d.mts +6 -2
- package/dist/commands/migrate.d.mts.map +1 -1
- package/dist/commands/migrate.mjs +75 -40
- package/dist/commands/migrate.mjs.map +1 -1
- package/dist/commands/migration-check.d.mts +4 -3
- package/dist/commands/migration-check.d.mts.map +1 -1
- package/dist/commands/migration-check.mjs +1 -280
- package/dist/commands/migration-graph.d.mts +13 -2
- package/dist/commands/migration-graph.d.mts.map +1 -1
- package/dist/commands/migration-graph.mjs +2 -137
- package/dist/commands/migration-list.d.mts +64 -4
- package/dist/commands/migration-list.d.mts.map +1 -1
- package/dist/commands/migration-list.mjs +143 -56
- package/dist/commands/migration-list.mjs.map +1 -1
- package/dist/commands/migration-log.d.mts +10 -1
- package/dist/commands/migration-log.d.mts.map +1 -1
- package/dist/commands/migration-log.mjs +10 -15
- package/dist/commands/migration-log.mjs.map +1 -1
- package/dist/commands/migration-new.d.mts.map +1 -1
- package/dist/commands/migration-new.mjs +32 -38
- package/dist/commands/migration-new.mjs.map +1 -1
- package/dist/commands/migration-plan.d.mts +3 -2
- package/dist/commands/migration-plan.d.mts.map +1 -1
- package/dist/commands/migration-plan.mjs +1 -1
- package/dist/commands/migration-show.d.mts +4 -55
- package/dist/commands/migration-show.d.mts.map +1 -1
- package/dist/commands/migration-show.mjs +61 -153
- package/dist/commands/migration-show.mjs.map +1 -1
- package/dist/commands/migration-status.d.mts +12 -49
- package/dist/commands/migration-status.d.mts.map +1 -1
- package/dist/commands/migration-status.mjs +85 -81
- package/dist/commands/migration-status.mjs.map +1 -1
- package/dist/commands/ref.d.mts +1 -1
- package/dist/commands/ref.d.mts.map +1 -1
- package/dist/commands/ref.mjs +38 -10
- package/dist/commands/ref.mjs.map +1 -1
- package/dist/config-loader-B6sJjXTv.mjs.map +1 -1
- package/dist/config-loader.d.mts.map +1 -1
- package/dist/contract-at-errors-BxP-TOMl.mjs +42 -0
- package/dist/contract-at-errors-BxP-TOMl.mjs.map +1 -0
- package/dist/{contract-emit-bcrpT-wD.mjs → contract-emit-D-4jrNve.mjs} +25 -10
- package/dist/contract-emit-D-4jrNve.mjs.map +1 -0
- package/dist/{contract-emit-r4y8Zhf1.mjs → contract-emit-DxcGl4Uq.mjs} +19 -14
- package/dist/contract-emit-DxcGl4Uq.mjs.map +1 -0
- package/dist/{contract-enrichment-Dani0mMW.mjs → contract-enrichment-a0V5Y_mL.mjs} +4 -25
- package/dist/contract-enrichment-a0V5Y_mL.mjs.map +1 -0
- package/dist/{contract-infer-BmySmqVT.mjs → contract-infer-D8uEbJuu.mjs} +4 -5
- package/dist/{contract-infer-BmySmqVT.mjs.map → contract-infer-D8uEbJuu.mjs.map} +1 -1
- package/dist/contract-space-aggregate-loader-DvZwdkrr.mjs +247 -0
- package/dist/contract-space-aggregate-loader-DvZwdkrr.mjs.map +1 -0
- package/dist/{db-verify-BClPs3ph.mjs → db-verify-v_vUKXTU.mjs} +5 -7
- package/dist/{db-verify-BClPs3ph.mjs.map → db-verify-v_vUKXTU.mjs.map} +1 -1
- package/dist/exports/control-api.d.mts +3 -3
- package/dist/exports/control-api.d.mts.map +1 -1
- package/dist/exports/control-api.mjs +3 -3
- package/dist/exports/index.d.mts.map +1 -1
- package/dist/exports/index.mjs +1 -1
- package/dist/exports/index.mjs.map +1 -1
- package/dist/exports/init-output.d.mts.map +1 -1
- package/dist/exports/init-output.mjs +1 -1
- package/dist/extension-pack-inputs-IDvjRCi3.mjs +62 -0
- package/dist/extension-pack-inputs-IDvjRCi3.mjs.map +1 -0
- package/dist/{framework-components-65gOHkHB.mjs → framework-components-fYXjz_in.mjs} +2 -2
- package/dist/{framework-components-65gOHkHB.mjs.map → framework-components-fYXjz_in.mjs.map} +1 -1
- package/dist/global-flags-DEHjV8_s.d.mts +34 -0
- package/dist/global-flags-DEHjV8_s.d.mts.map +1 -0
- package/dist/{graph-render-DJVv0_uf.mjs → graph-render-rFAqZujX.mjs} +2 -2
- package/dist/{graph-render-DJVv0_uf.mjs.map → graph-render-rFAqZujX.mjs.map} +1 -1
- package/dist/{init-BCJZPWE1.mjs → init-Cv9UzWL5.mjs} +20 -269
- package/dist/init-Cv9UzWL5.mjs.map +1 -0
- package/dist/{inspect-live-schema-DSRbFoOL.mjs → inspect-live-schema-C6ohV_oQ.mjs} +4 -5
- package/dist/{inspect-live-schema-DSRbFoOL.mjs.map → inspect-live-schema-C6ohV_oQ.mjs.map} +1 -1
- package/dist/migration-check-BiBJoYYW.mjs +341 -0
- package/dist/migration-check-BiBJoYYW.mjs.map +1 -0
- package/dist/migration-cli.d.mts.map +1 -1
- package/dist/migration-cli.mjs +4 -4
- package/dist/migration-cli.mjs.map +1 -1
- package/dist/{migration-command-scaffold-Bzd9La5c.mjs → migration-command-scaffold-CjvwO6at.mjs} +4 -5
- package/dist/{migration-command-scaffold-Bzd9La5c.mjs.map → migration-command-scaffold-CjvwO6at.mjs.map} +1 -1
- package/dist/migration-graph-D7DVUElV.mjs +1232 -0
- package/dist/migration-graph-D7DVUElV.mjs.map +1 -0
- package/dist/migration-list-styler-BRwF4-gy.mjs +399 -0
- package/dist/migration-list-styler-BRwF4-gy.mjs.map +1 -0
- package/dist/{migration-plan-CFwqw3Gk.mjs → migration-plan-9DJ7q7_z.mjs} +372 -133
- package/dist/migration-plan-9DJ7q7_z.mjs.map +1 -0
- package/dist/{migration-types-BXWvz12q.d.mts → migration-types-D2FW63pr.d.mts} +1 -1
- package/dist/{migration-types-BXWvz12q.d.mts.map → migration-types-D2FW63pr.d.mts.map} +1 -1
- package/dist/{migrations-CwZMa1Ck.mjs → migrations-Cv2jxNNK.mjs} +12 -13
- package/dist/migrations-Cv2jxNNK.mjs.map +1 -0
- package/dist/{output-BlsrGMEF.mjs → output-B60Gw5fu.mjs} +1 -1
- package/dist/{output-BlsrGMEF.mjs.map → output-B60Gw5fu.mjs.map} +1 -1
- package/dist/{progress-adapter-DFfvZcYL.mjs → progress-adapter-C644QK8l.mjs} +1 -1
- package/dist/{progress-adapter-DFfvZcYL.mjs.map → progress-adapter-C644QK8l.mjs.map} +1 -1
- package/dist/ref-advancement-DUZqsue6.mjs +50 -0
- package/dist/ref-advancement-DUZqsue6.mjs.map +1 -0
- package/dist/terminal-ui-5Y6mrg93.d.mts +133 -0
- package/dist/terminal-ui-5Y6mrg93.d.mts.map +1 -0
- package/dist/{types--CqjMdk0.d.mts → types-Dt_SfqFm.d.mts} +28 -28
- package/dist/types-Dt_SfqFm.d.mts.map +1 -0
- package/dist/{verify-Bom75OYI.mjs → verify-DCA9Sldu.mjs} +2 -2
- package/dist/{verify-Bom75OYI.mjs.map → verify-DCA9Sldu.mjs.map} +1 -1
- package/package.json +35 -24
- package/src/commands/contract-emit.ts +19 -7
- package/src/commands/contract-infer.ts +1 -1
- package/src/commands/db-init.ts +48 -2
- package/src/commands/db-sign.ts +9 -5
- package/src/commands/db-update.ts +54 -8
- package/src/commands/init/hygiene-gitattributes.ts +2 -2
- package/src/commands/init/index.ts +2 -1
- package/src/commands/init/templates/code-templates.ts +4 -2
- package/src/commands/init/templates/env.ts +13 -14
- package/src/commands/migrate.ts +125 -44
- package/src/commands/migration-check.ts +43 -83
- package/src/commands/migration-graph.ts +75 -60
- package/src/commands/migration-list.ts +220 -74
- package/src/commands/migration-log.ts +8 -14
- package/src/commands/migration-new.ts +44 -48
- package/src/commands/migration-plan.ts +412 -197
- package/src/commands/migration-show.ts +65 -284
- package/src/commands/migration-status.ts +127 -124
- package/src/commands/ref.ts +53 -8
- package/src/control-api/client.ts +0 -1
- package/src/control-api/contract-enrichment.ts +6 -42
- package/src/control-api/operations/{apply-aggregate.ts → apply.ts} +44 -75
- package/src/control-api/operations/contract-emit.ts +14 -6
- package/src/control-api/operations/{db-apply-aggregate.ts → db-apply.ts} +19 -19
- package/src/control-api/operations/db-init.ts +4 -4
- package/src/control-api/operations/db-update.ts +4 -4
- package/src/control-api/operations/db-verify.ts +15 -11
- package/src/control-api/operations/migration-apply.ts +56 -47
- package/src/control-api/types.ts +26 -27
- package/src/migration-cli.ts +4 -4
- package/src/utils/cli-errors.ts +234 -0
- package/src/utils/command-helpers.ts +9 -24
- package/src/utils/contract-at-errors.ts +96 -0
- package/src/utils/contract-space-aggregate-loader.ts +336 -117
- package/src/utils/formatters/migration-graph-layout.ts +1119 -0
- package/src/utils/formatters/migration-graph-rows.ts +336 -0
- package/src/utils/formatters/migration-graph-tree-render.ts +459 -0
- package/src/utils/formatters/migration-list-data-column.ts +115 -0
- package/src/utils/formatters/migration-list-graph-topology.ts +368 -0
- package/src/utils/formatters/migration-list-render.ts +191 -0
- package/src/utils/formatters/migration-list-styler.ts +63 -0
- package/src/utils/formatters/migration-list-types.ts +21 -0
- package/src/utils/formatters/migrations.ts +37 -46
- package/src/utils/glyph-mode.ts +22 -0
- package/src/utils/integrity-violation-to-check-failure.ts +130 -0
- package/src/utils/plan-resolution.ts +258 -0
- package/src/utils/ref-advancement.ts +68 -0
- package/src/utils/terminal-ui.ts +42 -1
- package/dist/cli-errors-Czmx92Zy.d.mts +0 -3
- package/dist/cli-errors-Djtz98Vm.mjs +0 -71
- package/dist/cli-errors-Djtz98Vm.mjs.map +0 -1
- package/dist/client-oXO2WCPD.mjs.map +0 -1
- package/dist/command-helpers-BSb0tRC8.mjs.map +0 -1
- package/dist/commands/migration-check.mjs.map +0 -1
- package/dist/commands/migration-graph.mjs.map +0 -1
- package/dist/contract-emit-bcrpT-wD.mjs.map +0 -1
- package/dist/contract-emit-r4y8Zhf1.mjs.map +0 -1
- package/dist/contract-enrichment-Dani0mMW.mjs.map +0 -1
- package/dist/contract-space-aggregate-loader-BmNQwlws.mjs +0 -160
- package/dist/contract-space-aggregate-loader-BmNQwlws.mjs.map +0 -1
- package/dist/global-flags-CdE7M0d9.d.mts +0 -15
- package/dist/global-flags-CdE7M0d9.d.mts.map +0 -1
- package/dist/init-BCJZPWE1.mjs.map +0 -1
- package/dist/migration-plan-CFwqw3Gk.mjs.map +0 -1
- package/dist/migrations-CwZMa1Ck.mjs.map +0 -1
- package/dist/rolldown-runtime-twds-ZHy.mjs +0 -14
- package/dist/terminal-ui-BiB_8KNo.mjs +0 -379
- package/dist/terminal-ui-BiB_8KNo.mjs.map +0 -1
- package/dist/types--CqjMdk0.d.mts.map +0 -1
|
@@ -1,110 +1,169 @@
|
|
|
1
|
+
import { readFile } from 'node:fs/promises';
|
|
2
|
+
import type { PrismaNextConfig } from '@prisma-next/config/config-types';
|
|
1
3
|
import type { Contract } from '@prisma-next/contract/types';
|
|
2
4
|
import type { ControlExtensionDescriptor } from '@prisma-next/framework-components/control';
|
|
5
|
+
import { createControlStack } from '@prisma-next/framework-components/control';
|
|
3
6
|
import type {
|
|
4
7
|
ContractSpaceAggregate,
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
+
DeclaredExtensionEntry,
|
|
9
|
+
IntegrityQueryOptions,
|
|
10
|
+
IntegrityViolation,
|
|
8
11
|
} from '@prisma-next/migration-tools/aggregate';
|
|
9
12
|
import { loadContractSpaceAggregate } from '@prisma-next/migration-tools/aggregate';
|
|
10
|
-
import
|
|
13
|
+
import { EMPTY_CONTRACT_HASH } from '@prisma-next/migration-tools/constants';
|
|
14
|
+
import { MigrationToolsError } from '@prisma-next/migration-tools/errors';
|
|
15
|
+
import { blindCast } from '@prisma-next/utils/casts';
|
|
11
16
|
import { notOk, ok, type Result } from '@prisma-next/utils/result';
|
|
12
|
-
import { CliStructuredError } from './cli-errors';
|
|
17
|
+
import { CliStructuredError, errorUnexpected, mapMigrationToolsError } from './cli-errors';
|
|
18
|
+
import { readContractEnvelope, resolveContractPath } from './command-helpers';
|
|
13
19
|
import { toDeclaredExtensionsFromRaw } from './extension-pack-inputs';
|
|
14
20
|
|
|
21
|
+
const CONTRACT_SPACES_DOCS_URL = 'https://pris.ly/contract-spaces';
|
|
22
|
+
|
|
23
|
+
function contractSpaceError5002(
|
|
24
|
+
summary: string,
|
|
25
|
+
options: {
|
|
26
|
+
readonly why: string;
|
|
27
|
+
readonly fix: string;
|
|
28
|
+
readonly violations: readonly IntegrityViolation[];
|
|
29
|
+
},
|
|
30
|
+
): CliStructuredError {
|
|
31
|
+
return new CliStructuredError('5002', summary, {
|
|
32
|
+
domain: 'MIG',
|
|
33
|
+
why: options.why,
|
|
34
|
+
fix: options.fix,
|
|
35
|
+
docsUrl: CONTRACT_SPACES_DOCS_URL,
|
|
36
|
+
meta: { violations: options.violations },
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Build the `5002` structured-error envelope for a contract-space
|
|
42
|
+
* target mismatch. Shared between the declared-extension precheck (the
|
|
43
|
+
* descriptor's configured target disagrees with the project target) and
|
|
44
|
+
* the on-disk-contract check surfaced by `checkIntegrity`.
|
|
45
|
+
*/
|
|
46
|
+
function targetMismatchError(
|
|
47
|
+
spaceId: string,
|
|
48
|
+
expected: string,
|
|
49
|
+
actual: string,
|
|
50
|
+
): CliStructuredError {
|
|
51
|
+
return contractSpaceError5002(`Contract-space target mismatch for "${spaceId}"`, {
|
|
52
|
+
why: `Space "${spaceId}" targets "${actual}" but the project's adapter targets "${expected}".`,
|
|
53
|
+
fix: 'Update the extension descriptor to target the configured database, or change the project adapter.',
|
|
54
|
+
violations: [{ kind: 'targetMismatch', spaceId, expected, actual }],
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
|
|
15
58
|
/**
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
* they did under the old precheck/marker-check helpers.
|
|
59
|
+
* Human-readable detail for an integrity violation, used as the `why`
|
|
60
|
+
* of the `integrityFailure` envelope. Mirrors the messages the prior
|
|
61
|
+
* throw-on-load loader produced so downstream consumers see the same
|
|
62
|
+
* text for the same on-disk state.
|
|
21
63
|
*/
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
64
|
+
function describeIntegrityViolation(violation: IntegrityViolation): string {
|
|
65
|
+
switch (violation.kind) {
|
|
66
|
+
case 'hashMismatch':
|
|
67
|
+
return `Migration "${violation.dirName}" stored hash "${violation.stored}" does not match computed hash "${violation.computed}".`;
|
|
68
|
+
case 'providedInvariantsMismatch':
|
|
69
|
+
return `Migration "${violation.dirName}" providedInvariants in migration.json disagrees with ops.json.`;
|
|
70
|
+
case 'packageUnloadable':
|
|
71
|
+
return `Migration "${violation.dirName}" could not be loaded: ${violation.detail}`;
|
|
72
|
+
case 'sameSourceAndTarget':
|
|
73
|
+
return `Migration "${violation.dirName}" has source equal to target (${violation.hash}) with no data invariant — a true no-op self-edge.`;
|
|
74
|
+
case 'headRefMissing':
|
|
75
|
+
return `Head ref \`refs/head.json\` is missing for contract space "${violation.spaceId}".`;
|
|
76
|
+
case 'headRefNotInGraph':
|
|
77
|
+
return `Head ref ${violation.hash} for contract space "${violation.spaceId}" is not present in the migration graph.`;
|
|
78
|
+
case 'refUnreadable':
|
|
79
|
+
return `Ref "${violation.refName}" for contract space "${violation.spaceId}" is unreadable: ${violation.detail}`;
|
|
80
|
+
case 'duplicateMigrationHash':
|
|
81
|
+
return `Multiple migrations in space "${violation.spaceId}" share migrationHash "${violation.migrationHash}" (${violation.dirNames.join(', ')}).`;
|
|
82
|
+
default: {
|
|
83
|
+
const spaceId = 'spaceId' in violation ? violation.spaceId : '*';
|
|
84
|
+
return `Integrity violation "${violation.kind}" for contract space "${spaceId}".`;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Map the integrity violations `checkIntegrity` reports into a single
|
|
91
|
+
* CLI structured-error envelope, preserving the error codes the prior
|
|
92
|
+
* throw-on-load loader emitted: `5001` (layout drift, bundled) and
|
|
93
|
+
* `5002` (target / disjointness / contract-validation / structural
|
|
94
|
+
* integrity). Returns `null` when there is nothing to refuse on.
|
|
95
|
+
*
|
|
96
|
+
* Precedence reproduces the prior loader's first-failure ordering:
|
|
97
|
+
* layout drift first (every offence bundled into one envelope), then
|
|
98
|
+
* target mismatch, then disjointness, then a contract-validation
|
|
99
|
+
* failure, then any remaining structural integrity violation.
|
|
100
|
+
*/
|
|
101
|
+
export function mapIntegrityViolations(
|
|
102
|
+
violations: readonly IntegrityViolation[],
|
|
103
|
+
): CliStructuredError | null {
|
|
104
|
+
if (violations.length === 0) return null;
|
|
105
|
+
|
|
106
|
+
const layout = violations.filter(
|
|
107
|
+
(v): v is Extract<IntegrityViolation, { kind: 'orphanSpaceDir' | 'declaredButUnmigrated' }> =>
|
|
108
|
+
v.kind === 'orphanSpaceDir' || v.kind === 'declaredButUnmigrated',
|
|
109
|
+
);
|
|
110
|
+
if (layout.length > 0) {
|
|
111
|
+
const lines = layout.map((v) => `- [${v.kind}] ${v.spaceId}`);
|
|
25
112
|
const summary =
|
|
26
|
-
|
|
113
|
+
layout.length === 1
|
|
27
114
|
? 'Contract-space layout violation detected'
|
|
28
|
-
: `Contract-space layout violations detected (${
|
|
115
|
+
: `Contract-space layout violations detected (${layout.length})`;
|
|
29
116
|
return new CliStructuredError('5001', summary, {
|
|
30
117
|
domain: 'MIG',
|
|
31
118
|
why: `The on-disk \`migrations/\` directory and your \`extensionPacks\` declaration are not in agreement.\n${lines.join('\n')}`,
|
|
32
|
-
fix: '
|
|
33
|
-
docsUrl:
|
|
34
|
-
meta: {
|
|
35
|
-
violations: error.violations.map((v) => ({
|
|
36
|
-
kind: v.kind,
|
|
37
|
-
spaceId: v.spaceId,
|
|
38
|
-
})),
|
|
39
|
-
},
|
|
119
|
+
fix: 'Declare the extension in `extensionPacks` and re-emit its contract-space artefacts, or remove the orphan `migrations/<space>` directory.',
|
|
120
|
+
docsUrl: CONTRACT_SPACES_DOCS_URL,
|
|
121
|
+
meta: { violations: layout },
|
|
40
122
|
});
|
|
41
123
|
}
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
fix: 'Update the conflicting contracts so each storage element is claimed by exactly one space.',
|
|
50
|
-
docsUrl: 'https://pris.ly/contract-spaces',
|
|
51
|
-
meta: {
|
|
52
|
-
violations: [
|
|
53
|
-
{
|
|
54
|
-
kind: 'disjointness',
|
|
55
|
-
spaceId: error.claimedBy.join(','),
|
|
56
|
-
element: error.element,
|
|
57
|
-
claimedBy: error.claimedBy,
|
|
58
|
-
},
|
|
59
|
-
],
|
|
60
|
-
},
|
|
61
|
-
},
|
|
124
|
+
|
|
125
|
+
const targetMismatch = violations.find((v) => v.kind === 'targetMismatch');
|
|
126
|
+
if (targetMismatch && targetMismatch.kind === 'targetMismatch') {
|
|
127
|
+
return targetMismatchError(
|
|
128
|
+
targetMismatch.spaceId,
|
|
129
|
+
targetMismatch.expected,
|
|
130
|
+
targetMismatch.actual,
|
|
62
131
|
);
|
|
63
132
|
}
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
133
|
+
|
|
134
|
+
const disjointness = violations.find((v) => v.kind === 'disjointness');
|
|
135
|
+
if (disjointness && disjointness.kind === 'disjointness') {
|
|
136
|
+
return contractSpaceError5002(
|
|
137
|
+
`Contract-space disjointness violation: storage element "${disjointness.element}" claimed by multiple spaces`,
|
|
68
138
|
{
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
docsUrl: 'https://pris.ly/contract-spaces',
|
|
73
|
-
meta: {
|
|
74
|
-
violations: [{ kind: 'integrity', spaceId: error.spaceId, detail: error.detail }],
|
|
75
|
-
},
|
|
139
|
+
why: `Spaces ${disjointness.claimedBy.map((s) => `"${s}"`).join(', ')} all claim the storage element "${disjointness.element}". Each storage element must be owned by exactly one contract space.`,
|
|
140
|
+
fix: 'Update the conflicting contracts so each storage element is claimed by exactly one space.',
|
|
141
|
+
violations: [disjointness],
|
|
76
142
|
},
|
|
77
143
|
);
|
|
78
144
|
}
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
145
|
+
|
|
146
|
+
const contractUnreadable = violations.find((v) => v.kind === 'contractUnreadable');
|
|
147
|
+
if (contractUnreadable && contractUnreadable.kind === 'contractUnreadable') {
|
|
148
|
+
return contractSpaceError5002(
|
|
149
|
+
`Contract-space contract validation failed for "${contractUnreadable.spaceId}"`,
|
|
83
150
|
{
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
meta: {
|
|
88
|
-
violations: [{ kind: 'validation', spaceId: error.spaceId, detail: error.detail }],
|
|
89
|
-
},
|
|
151
|
+
why: contractUnreadable.detail,
|
|
152
|
+
fix: 'Re-emit the extension contract with `prisma-next contract emit`, or fix the extension pack descriptor producing the invalid contract.',
|
|
153
|
+
violations: [contractUnreadable],
|
|
90
154
|
},
|
|
91
155
|
);
|
|
92
156
|
}
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
expected: error.expected,
|
|
104
|
-
actual: error.actual,
|
|
105
|
-
},
|
|
106
|
-
],
|
|
107
|
-
},
|
|
157
|
+
|
|
158
|
+
// Any remaining recoverable structural violation refuses as an
|
|
159
|
+
// integrity failure, surfacing the first one's detail (every violation
|
|
160
|
+
// is still computed; the gate just renders one envelope).
|
|
161
|
+
const structural = violations[0]!;
|
|
162
|
+
const spaceId = 'spaceId' in structural ? structural.spaceId : '*';
|
|
163
|
+
return contractSpaceError5002(`Contract-space integrity failure for "${spaceId}"`, {
|
|
164
|
+
why: describeIntegrityViolation(structural),
|
|
165
|
+
fix: 'Re-emit the affected migration package(s) or restore the on-disk `migrations/` directory from version control.',
|
|
166
|
+
violations: [structural],
|
|
108
167
|
});
|
|
109
168
|
}
|
|
110
169
|
|
|
@@ -121,57 +180,217 @@ export interface BuildAggregateInputs<TFamilyId extends string, TTargetId extend
|
|
|
121
180
|
readonly appContract: Contract;
|
|
122
181
|
readonly extensionPacks: ReadonlyArray<ControlExtensionDescriptor<TFamilyId, TTargetId>>;
|
|
123
182
|
readonly deserializeContract: (contractJson: unknown) => Contract;
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
*
|
|
131
|
-
* `migrate` callers thread the user's authored app-space
|
|
132
|
-
* packages (loaded via `loadMigrationPackages(appMigrationsDir)`)
|
|
133
|
-
* through here so the graph-walk strategy can plot a path through
|
|
134
|
-
* them — the prod-time replay path explicitly forbids synth.
|
|
135
|
-
*/
|
|
136
|
-
readonly appMigrationPackages?: ReadonlyArray<OnDiskMigrationPackage>;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
function declaredExtensionsFromInputs(
|
|
186
|
+
extensionPacks: BuildAggregateInputs<string, string>['extensionPacks'],
|
|
187
|
+
): readonly DeclaredExtensionEntry[] {
|
|
188
|
+
return toDeclaredExtensionsFromRaw(extensionPacks as ReadonlyArray<unknown>);
|
|
137
189
|
}
|
|
138
190
|
|
|
139
191
|
/**
|
|
140
|
-
*
|
|
141
|
-
*
|
|
142
|
-
*
|
|
143
|
-
* App-side migration packages flow through `inputs.appMigrationPackages`
|
|
144
|
-
* (defaulting to `[]`). `db init` / `db update` leave it empty: the
|
|
145
|
-
* planner's `synth` strategy is used for the app member (driven by
|
|
146
|
-
* `callerPolicy.ignoreGraphFor`), so the app's authored `migrations/`
|
|
147
|
-
* graph does not need to be walked. `migrate` threads the
|
|
148
|
-
* already-loaded app-space packages through so the graph-walk strategy
|
|
149
|
-
* can plot a path through them — replay forbids synth.
|
|
150
|
-
*
|
|
151
|
-
* @see specs/contract-space-aggregate-spec.md § Loader.
|
|
192
|
+
* Reject extension descriptors whose configured target disagrees with
|
|
193
|
+
* the project target before any on-disk read.
|
|
152
194
|
*/
|
|
153
|
-
export
|
|
195
|
+
export function refuseDeclaredExtensionTargetMismatch<
|
|
196
|
+
TFamilyId extends string,
|
|
197
|
+
TTargetId extends string,
|
|
198
|
+
>(inputs: BuildAggregateInputs<TFamilyId, TTargetId>): CliStructuredError | null {
|
|
199
|
+
for (const declared of declaredExtensionsFromInputs(inputs.extensionPacks)) {
|
|
200
|
+
if (declared.targetId !== inputs.targetId) {
|
|
201
|
+
return targetMismatchError(declared.id, inputs.targetId, declared.targetId);
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
return null;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* Load the tolerant {@link ContractSpaceAggregate} once at the CLI
|
|
209
|
+
* surface. Construction never throws on disk content; callers query
|
|
210
|
+
* {@link ContractSpaceAggregate.app} / extension facets instead of
|
|
211
|
+
* re-reading `migrations/`.
|
|
212
|
+
*/
|
|
213
|
+
export async function loadContractSpaceAggregateForCli<
|
|
154
214
|
TFamilyId extends string,
|
|
155
215
|
TTargetId extends string,
|
|
156
216
|
>(
|
|
157
217
|
inputs: BuildAggregateInputs<TFamilyId, TTargetId>,
|
|
158
218
|
): Promise<Result<ContractSpaceAggregate, CliStructuredError>> {
|
|
159
|
-
const
|
|
160
|
-
|
|
161
|
-
|
|
219
|
+
const targetFailure = refuseDeclaredExtensionTargetMismatch(inputs);
|
|
220
|
+
if (targetFailure) {
|
|
221
|
+
return notOk(targetFailure);
|
|
222
|
+
}
|
|
162
223
|
|
|
163
|
-
const
|
|
164
|
-
targetId: inputs.targetId,
|
|
224
|
+
const aggregate = await loadContractSpaceAggregate({
|
|
165
225
|
migrationsDir: inputs.migrationsDir,
|
|
226
|
+
deserializeContract: inputs.deserializeContract,
|
|
166
227
|
appContract: inputs.appContract,
|
|
228
|
+
});
|
|
229
|
+
return ok(aggregate);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* Run `checkIntegrity` on a loaded aggregate and map violations into
|
|
234
|
+
* the contract-space refusal envelope, or return `null` when the model
|
|
235
|
+
* is acceptable for the requested check scope.
|
|
236
|
+
*/
|
|
237
|
+
export function refuseContractSpaceIntegrity(
|
|
238
|
+
aggregate: ContractSpaceAggregate,
|
|
239
|
+
options: IntegrityQueryOptions,
|
|
240
|
+
): CliStructuredError | null {
|
|
241
|
+
return mapIntegrityViolations(aggregate.checkIntegrity(options));
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
const PACKAGE_CORRUPTION_KINDS = new Set<IntegrityViolation['kind']>([
|
|
245
|
+
'hashMismatch',
|
|
246
|
+
'providedInvariantsMismatch',
|
|
247
|
+
'packageUnloadable',
|
|
248
|
+
]);
|
|
249
|
+
|
|
250
|
+
/**
|
|
251
|
+
* Reader-subset integrity refusal for `migration status`: package
|
|
252
|
+
* corruptions only (`hashMismatch`, `providedInvariantsMismatch`,
|
|
253
|
+
* `packageUnloadable`).
|
|
254
|
+
*/
|
|
255
|
+
export function refusePackageCorruptionOnAggregate(
|
|
256
|
+
aggregate: ContractSpaceAggregate,
|
|
257
|
+
): CliStructuredError | null {
|
|
258
|
+
const corruption = aggregate.checkIntegrity().filter((v) => PACKAGE_CORRUPTION_KINDS.has(v.kind));
|
|
259
|
+
return mapIntegrityViolations(corruption);
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
/**
|
|
263
|
+
* Construct the tolerant {@link ContractSpaceAggregate} at the CLI
|
|
264
|
+
* surface and apply the explicit integrity refusal.
|
|
265
|
+
*
|
|
266
|
+
* App-space migration packages are read from `migrations/<app>/` by the
|
|
267
|
+
* loader itself; callers no longer thread them through.
|
|
268
|
+
*/
|
|
269
|
+
export async function buildContractSpaceAggregate<
|
|
270
|
+
TFamilyId extends string,
|
|
271
|
+
TTargetId extends string,
|
|
272
|
+
>(
|
|
273
|
+
inputs: BuildAggregateInputs<TFamilyId, TTargetId>,
|
|
274
|
+
): Promise<Result<ContractSpaceAggregate, CliStructuredError>> {
|
|
275
|
+
const declaredExtensions = declaredExtensionsFromInputs(inputs.extensionPacks);
|
|
276
|
+
const loaded = await loadContractSpaceAggregateForCli(inputs);
|
|
277
|
+
if (!loaded.ok) {
|
|
278
|
+
return loaded;
|
|
279
|
+
}
|
|
280
|
+
const failure = refuseContractSpaceIntegrity(loaded.value, {
|
|
167
281
|
declaredExtensions,
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
282
|
+
checkContracts: true,
|
|
283
|
+
});
|
|
284
|
+
if (failure) {
|
|
285
|
+
return notOk(failure);
|
|
286
|
+
}
|
|
287
|
+
return ok(loaded.value);
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
/**
|
|
291
|
+
* Build a minimal app {@link Contract} carrying only the project's
|
|
292
|
+
* contract-identity (`storage.storageHash` + `target` / `targetFamily`)
|
|
293
|
+
* when the real `contract.json` is absent or undeserializable.
|
|
294
|
+
*
|
|
295
|
+
* `loadContractSpaceAggregate` requires an `appContract` to synthesise the
|
|
296
|
+
* app space's head ref from its storage hash. Read commands query only that
|
|
297
|
+
* hash and the target — never `models` — so an empty-`models` stand-in is
|
|
298
|
+
* sufficient for them. It is *not* a valid contract for any consumer that
|
|
299
|
+
* reads schema, which is why this is confined to the read-aggregate path.
|
|
300
|
+
*/
|
|
301
|
+
export function appContractStandInFromIdentity(args: {
|
|
302
|
+
readonly contractHash: string;
|
|
303
|
+
readonly targetId: string;
|
|
304
|
+
readonly targetFamily: string;
|
|
305
|
+
}): Contract {
|
|
306
|
+
return blindCast<
|
|
307
|
+
Contract,
|
|
308
|
+
'read-aggregate consumers query only storage.storageHash and target; empty models stand in for an unreadable contract.json'
|
|
309
|
+
>({
|
|
310
|
+
storage: { storageHash: args.contractHash },
|
|
311
|
+
schemaVersion: '0.0.0',
|
|
312
|
+
target: args.targetId,
|
|
313
|
+
targetFamily: args.targetFamily,
|
|
314
|
+
models: {},
|
|
315
|
+
profileHash: EMPTY_CONTRACT_HASH,
|
|
316
|
+
});
|
|
317
|
+
}
|
|
171
318
|
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
319
|
+
export async function loadContractRawSafely(config: {
|
|
320
|
+
contract?: { output?: string };
|
|
321
|
+
}): Promise<unknown | null> {
|
|
322
|
+
try {
|
|
323
|
+
const path = resolveContractPath(config);
|
|
324
|
+
const raw = await readFile(path, 'utf-8');
|
|
325
|
+
return JSON.parse(raw);
|
|
326
|
+
} catch {
|
|
327
|
+
return null;
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
/**
|
|
332
|
+
* Tolerant {@link ContractSpaceAggregate} assembly for read-only CLI
|
|
333
|
+
* commands. No integrity gate — callers query `aggregate.app` (or other
|
|
334
|
+
* facets) without re-reading `migrations/`. When `contract.json` is absent
|
|
335
|
+
* or undeserializable, the app contract falls back to an identity-only
|
|
336
|
+
* stand-in ({@link appContractStandInFromIdentity}), so these commands
|
|
337
|
+
* load without requiring a readable contract.
|
|
338
|
+
*/
|
|
339
|
+
export async function buildReadAggregate(
|
|
340
|
+
config: PrismaNextConfig,
|
|
341
|
+
options: { readonly migrationsDir: string },
|
|
342
|
+
): Promise<
|
|
343
|
+
Result<
|
|
344
|
+
{ readonly aggregate: ContractSpaceAggregate; readonly contractHash: string },
|
|
345
|
+
CliStructuredError
|
|
346
|
+
>
|
|
347
|
+
> {
|
|
348
|
+
let contractHash: string = EMPTY_CONTRACT_HASH;
|
|
349
|
+
try {
|
|
350
|
+
const envelope = await readContractEnvelope(config);
|
|
351
|
+
contractHash = envelope.storageHash;
|
|
352
|
+
} catch {
|
|
353
|
+
// Contract unreadable — marker uses EMPTY_CONTRACT_HASH
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
try {
|
|
357
|
+
const contractRawForAggregate = await loadContractRawSafely(config);
|
|
358
|
+
const stack = createControlStack(config);
|
|
359
|
+
const familyInstance = config.family.create(stack);
|
|
360
|
+
const deserializeContract = (json: unknown): Contract =>
|
|
361
|
+
familyInstance.deserializeContract(json);
|
|
362
|
+
let appContractForLoad: Contract = appContractStandInFromIdentity({
|
|
363
|
+
contractHash,
|
|
364
|
+
targetId: config.target.id,
|
|
365
|
+
targetFamily: config.target.familyId,
|
|
366
|
+
});
|
|
367
|
+
if (contractRawForAggregate !== null) {
|
|
368
|
+
try {
|
|
369
|
+
appContractForLoad = deserializeContract(contractRawForAggregate);
|
|
370
|
+
} catch {
|
|
371
|
+
// Deserialization failed — identity-only stand-in fallback
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
const loaded = await loadContractSpaceAggregateForCli({
|
|
376
|
+
targetId: config.target.id,
|
|
377
|
+
migrationsDir: options.migrationsDir,
|
|
378
|
+
appContract: appContractForLoad,
|
|
379
|
+
extensionPacks: config.extensionPacks ?? [],
|
|
380
|
+
deserializeContract,
|
|
381
|
+
});
|
|
382
|
+
if (!loaded.ok) {
|
|
383
|
+
return loaded;
|
|
384
|
+
}
|
|
385
|
+
return ok({ aggregate: loaded.value, contractHash });
|
|
386
|
+
} catch (error) {
|
|
387
|
+
if (MigrationToolsError.is(error)) {
|
|
388
|
+
return notOk(mapMigrationToolsError(error));
|
|
389
|
+
}
|
|
390
|
+
return notOk(
|
|
391
|
+
errorUnexpected(error instanceof Error ? error.message : String(error), {
|
|
392
|
+
why: `Failed to read migrations directory: ${error instanceof Error ? error.message : String(error)}`,
|
|
393
|
+
}),
|
|
394
|
+
);
|
|
175
395
|
}
|
|
176
|
-
return ok(result.value.aggregate);
|
|
177
396
|
}
|