@prisma-next/cli 0.5.0-dev.66 → 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-errors-By1iVE3z.mjs → cli-errors-D3_sMh2K.mjs} +2 -3
- package/dist/{cli-errors-By1iVE3z.mjs.map → cli-errors-D3_sMh2K.mjs.map} +1 -1
- package/dist/{cli-errors-DDeVsP2Y.d.mts → cli-errors-QH8kf-C2.d.mts} +0 -2
- package/dist/cli.mjs +12 -76
- 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.d.mts.map +1 -1
- package/dist/commands/contract-emit.mjs +2 -4
- package/dist/commands/contract-infer.d.mts.map +1 -1
- package/dist/commands/contract-infer.mjs +2 -4
- package/dist/commands/db-init.d.mts.map +1 -1
- package/dist/commands/db-init.mjs +11 -11
- 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 +5 -7
- 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 +8 -9
- 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 +11 -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 -321
- package/dist/commands/migration-apply.d.mts.map +1 -1
- package/dist/commands/migration-apply.mjs +16 -17
- package/dist/commands/migration-apply.mjs.map +1 -1
- package/dist/commands/migration-new.d.mts +0 -1
- package/dist/commands/migration-new.d.mts.map +1 -1
- package/dist/commands/migration-new.mjs +10 -11
- 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 -345
- package/dist/commands/migration-ref.d.mts +1 -1
- package/dist/commands/migration-ref.d.mts.map +1 -1
- package/dist/commands/migration-ref.mjs +5 -6
- package/dist/commands/migration-ref.mjs.map +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 +13 -13
- package/dist/commands/migration-show.mjs.map +1 -1
- package/dist/commands/migration-status.d.mts.map +1 -1
- package/dist/commands/migration-status.mjs +2 -4
- package/dist/{config-loader-ih8ViDb_.mjs → config-loader-B6sJjXTv.mjs} +2 -4
- package/dist/config-loader-B6sJjXTv.mjs.map +1 -0
- package/dist/config-loader.d.mts +0 -1
- package/dist/config-loader.d.mts.map +1 -1
- package/dist/config-loader.mjs +2 -3
- package/dist/{contract-emit-CnTXVVbF.mjs → contract-emit-B3ChISB_.mjs} +22 -13
- package/dist/contract-emit-B3ChISB_.mjs.map +1 -0
- package/dist/{contract-emit-CcZr3HS9.mjs → contract-emit-DkMqO7f2.mjs} +8 -10
- package/dist/contract-emit-DkMqO7f2.mjs.map +1 -0
- package/dist/{contract-enrichment-xDeJBC-o.mjs → contract-enrichment-CF6ogEJ_.mjs} +2 -2
- package/dist/contract-enrichment-CF6ogEJ_.mjs.map +1 -0
- package/dist/{contract-infer-sER84Le-.mjs → contract-infer-BDKAE0B0.mjs} +5 -7
- package/dist/{contract-infer-sER84Le-.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/config-types.mjs +1 -2
- package/dist/exports/control-api.d.mts +202 -7
- package/dist/exports/control-api.d.mts.map +1 -1
- package/dist/exports/control-api.mjs +4 -6
- package/dist/exports/index.d.mts.map +1 -1
- package/dist/exports/index.mjs +28 -30
- package/dist/exports/index.mjs.map +1 -1
- package/dist/exports/init-output.d.mts +2 -4
- package/dist/exports/init-output.d.mts.map +1 -1
- package/dist/exports/init-output.mjs +2 -3
- package/dist/{framework-components-Bgcre3Z6.mjs → framework-components-gwAHl7ml.mjs} +3 -4
- package/dist/{framework-components-Bgcre3Z6.mjs.map → framework-components-gwAHl7ml.mjs.map} +1 -1
- package/dist/{init-DC4sL4Rp.mjs → init-Deo7U8_U.mjs} +13 -30
- package/dist/init-Deo7U8_U.mjs.map +1 -0
- package/dist/{inspect-live-schema-BQN21nNO.mjs → inspect-live-schema-BAgQMYpD.mjs} +7 -8
- package/dist/inspect-live-schema-BAgQMYpD.mjs.map +1 -0
- package/dist/migration-cli.d.mts +0 -1
- package/dist/migration-cli.d.mts.map +1 -1
- package/dist/migration-cli.mjs +2 -3
- package/dist/migration-cli.mjs.map +1 -1
- package/dist/{migration-command-scaffold-DLmYGRug.mjs → migration-command-scaffold-B8J702Uh.mjs} +7 -8
- package/dist/migration-command-scaffold-B8J702Uh.mjs.map +1 -0
- package/dist/migration-plan-BcKNnTM7.mjs +530 -0
- package/dist/migration-plan-BcKNnTM7.mjs.map +1 -0
- package/dist/{migration-status-CDW4RDsO.mjs → migration-status-CjwB2of-.mjs} +10 -14
- package/dist/migration-status-CjwB2of-.mjs.map +1 -0
- package/dist/{migrations-MEoKMiV5.mjs → migrations-CIK94AJf.mjs} +3 -4
- package/dist/migrations-CIK94AJf.mjs.map +1 -0
- package/dist/{output-BpcQrnnq.mjs → output-DnjfCC_u.mjs} +9 -3
- package/dist/output-DnjfCC_u.mjs.map +1 -0
- package/dist/{progress-adapter-DgRGldpT.mjs → progress-adapter-xASh41wr.mjs} +2 -2
- package/dist/{progress-adapter-DgRGldpT.mjs.map → progress-adapter-xASh41wr.mjs.map} +1 -1
- package/dist/{result-handler-Ch6hVnOo.mjs → result-handler-DWb1rFS-.mjs} +20 -10
- package/dist/result-handler-DWb1rFS-.mjs.map +1 -0
- package/dist/{terminal-ui-u2YgKghu.mjs → terminal-ui-zaRDhJnP.mjs} +2 -6
- package/dist/{terminal-ui-u2YgKghu.mjs.map → terminal-ui-zaRDhJnP.mjs.map} +1 -1
- package/dist/{verify-BT9tgCOH.mjs → verify-BEIa9638.mjs} +3 -4
- package/dist/verify-BEIa9638.mjs.map +1 -0
- package/package.json +24 -24
- package/src/commands/db-init.ts +13 -3
- package/src/commands/db-update.ts +7 -3
- package/src/commands/db-verify.ts +47 -15
- package/src/commands/init/index.ts +1 -1
- package/src/commands/init/init.ts +2 -2
- package/src/commands/migration-apply.ts +9 -9
- package/src/commands/migration-new.ts +4 -4
- package/src/commands/migration-plan.ts +66 -9
- package/src/commands/migration-show.ts +7 -5
- package/src/commands/migration-status.ts +3 -3
- package/src/control-api/client.ts +42 -0
- package/src/control-api/operations/db-apply-aggregate.ts +446 -0
- package/src/control-api/operations/db-init.ts +51 -258
- package/src/control-api/operations/db-update.ts +66 -188
- package/src/control-api/operations/db-verify.ts +342 -0
- package/src/control-api/types.ts +56 -0
- package/src/exports/control-api.ts +13 -2
- package/src/load-ts-contract.ts +28 -26
- package/src/utils/combine-schema-results.ts +84 -0
- package/src/utils/command-helpers.ts +24 -2
- package/src/utils/contract-space-aggregate-loader.ts +236 -0
- package/src/utils/contract-space-extension-migrations-pass.ts +120 -0
- package/src/utils/contract-space-migrate-pass.ts +156 -0
- package/dist/client-hUCMXFE_.mjs +0 -1031
- package/dist/client-hUCMXFE_.mjs.map +0 -1
- package/dist/commands/db-verify.mjs.map +0 -1
- package/dist/commands/migration-plan.mjs.map +0 -1
- package/dist/config-loader-ih8ViDb_.mjs.map +0 -1
- package/dist/contract-emit-BkRH9lGt.mjs +0 -4
- package/dist/contract-emit-CcZr3HS9.mjs.map +0 -1
- package/dist/contract-emit-CnTXVVbF.mjs.map +0 -1
- package/dist/contract-enrichment-xDeJBC-o.mjs.map +0 -1
- package/dist/init-DC4sL4Rp.mjs.map +0 -1
- package/dist/inspect-live-schema-BQN21nNO.mjs.map +0 -1
- package/dist/migration-command-scaffold-DLmYGRug.mjs.map +0 -1
- package/dist/migration-status-CDW4RDsO.mjs.map +0 -1
- package/dist/migrations-MEoKMiV5.mjs.map +0 -1
- package/dist/output-BpcQrnnq.mjs.map +0 -1
- package/dist/result-handler-Ch6hVnOo.mjs.map +0 -1
- package/dist/verify-BT9tgCOH.mjs.map +0 -1
|
@@ -7,6 +7,7 @@ import { readMigrationsDir } from '@prisma-next/migration-tools/io';
|
|
|
7
7
|
import type { PathDecision } from '@prisma-next/migration-tools/migration-graph';
|
|
8
8
|
import { reconstructGraph } from '@prisma-next/migration-tools/migration-graph';
|
|
9
9
|
import type { OnDiskMigrationPackage } from '@prisma-next/migration-tools/package';
|
|
10
|
+
import { APP_SPACE_ID, spaceMigrationDirectory } from '@prisma-next/migration-tools/spaces';
|
|
10
11
|
import { ifDefined } from '@prisma-next/utils/defined';
|
|
11
12
|
import type { Command } from 'commander';
|
|
12
13
|
import { relative, resolve } from 'pathe';
|
|
@@ -79,6 +80,16 @@ export function resolveContractPath(config: { contract?: { output?: string } }):
|
|
|
79
80
|
/**
|
|
80
81
|
* Resolves the migrations directory and config path from CLI options.
|
|
81
82
|
* Shared by migration-apply, migration-plan, and migration-status.
|
|
83
|
+
*
|
|
84
|
+
* - `migrationsDir` is the project's top-level `migrations/` directory
|
|
85
|
+
* (the root that the aggregate loader walks for every contract space).
|
|
86
|
+
* - `appMigrationsDir` is the app subspace directory under it
|
|
87
|
+
* (`<migrationsDir>/<APP_SPACE_ID>/`). Every per-app reader / writer
|
|
88
|
+
* (`migration new`, `migration plan`, `migration apply`,
|
|
89
|
+
* `migration status`, `migration show`, `migration ref`) operates on
|
|
90
|
+
* this directory. Extensions own their own `migrations/<spaceId>/`.
|
|
91
|
+
* - `refsDir` is the app's refs directory (`<appMigrationsDir>/refs/`).
|
|
92
|
+
* The framework does not maintain refs at the migrations root.
|
|
82
93
|
*/
|
|
83
94
|
export function resolveMigrationPaths(
|
|
84
95
|
configOption: string | undefined,
|
|
@@ -87,6 +98,8 @@ export function resolveMigrationPaths(
|
|
|
87
98
|
configPath: string;
|
|
88
99
|
migrationsDir: string;
|
|
89
100
|
migrationsRelative: string;
|
|
101
|
+
appMigrationsDir: string;
|
|
102
|
+
appMigrationsRelative: string;
|
|
90
103
|
refsDir: string;
|
|
91
104
|
} {
|
|
92
105
|
const configPath = configOption
|
|
@@ -97,8 +110,17 @@ export function resolveMigrationPaths(
|
|
|
97
110
|
config.migrations?.dir ?? 'migrations',
|
|
98
111
|
);
|
|
99
112
|
const migrationsRelative = relative(process.cwd(), migrationsDir);
|
|
100
|
-
const
|
|
101
|
-
|
|
113
|
+
const appMigrationsDir = spaceMigrationDirectory(migrationsDir, APP_SPACE_ID);
|
|
114
|
+
const appMigrationsRelative = relative(process.cwd(), appMigrationsDir);
|
|
115
|
+
const refsDir = resolve(appMigrationsDir, 'refs');
|
|
116
|
+
return {
|
|
117
|
+
configPath,
|
|
118
|
+
migrationsDir,
|
|
119
|
+
migrationsRelative,
|
|
120
|
+
appMigrationsDir,
|
|
121
|
+
appMigrationsRelative,
|
|
122
|
+
refsDir,
|
|
123
|
+
};
|
|
102
124
|
}
|
|
103
125
|
|
|
104
126
|
/**
|
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
import type { Contract } from '@prisma-next/contract/types';
|
|
2
|
+
import type { ControlExtensionDescriptor } from '@prisma-next/framework-components/control';
|
|
3
|
+
import type {
|
|
4
|
+
ContractSpaceAggregate,
|
|
5
|
+
DeclaredExtensionEntry,
|
|
6
|
+
LoadAggregateError,
|
|
7
|
+
LoadAggregateInput,
|
|
8
|
+
LoadAggregateOutput,
|
|
9
|
+
} from '@prisma-next/migration-tools/aggregate';
|
|
10
|
+
import { loadContractSpaceAggregate } from '@prisma-next/migration-tools/aggregate';
|
|
11
|
+
import { notOk, ok, type Result } from '@prisma-next/utils/result';
|
|
12
|
+
import { CliStructuredError } from './cli-errors';
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Structural shape the aggregate loader needs from each declared
|
|
16
|
+
* `Config.extensionPacks` entry. Mirrors the SQL family's
|
|
17
|
+
* `SqlControlExtensionDescriptor.contractSpace` shape but kept
|
|
18
|
+
* structural so the loader doesn't depend on the SQL family.
|
|
19
|
+
*/
|
|
20
|
+
type ExtensionPackForAggregate = {
|
|
21
|
+
readonly id: string;
|
|
22
|
+
readonly targetId: string;
|
|
23
|
+
readonly contractSpace?: {
|
|
24
|
+
readonly contractJson: unknown;
|
|
25
|
+
readonly headRef: { readonly hash: string; readonly invariants: readonly string[] };
|
|
26
|
+
};
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Convert the CLI's `Config.extensionPacks` array into the loader's
|
|
31
|
+
* `DeclaredExtensionEntry[]` shape.
|
|
32
|
+
*
|
|
33
|
+
* The loader hashes `contractSpace.contractJson` to compare against the
|
|
34
|
+
* on-disk `refs/head.json.hash` (drift detection). Rather than re-running
|
|
35
|
+
* the canonical-JSON + SHA-256 pipeline at the CLI surface, we look up
|
|
36
|
+
* the descriptor's pre-computed `headRef.hash` via reference identity
|
|
37
|
+
* on the contract JSON value — the loader passes the same
|
|
38
|
+
* `entry.contractSpace.contractJson` reference through to the hasher,
|
|
39
|
+
* so identity-keyed lookup is safe.
|
|
40
|
+
*/
|
|
41
|
+
function toDeclaredExtensions(extensionPacks: ReadonlyArray<ExtensionPackForAggregate>): {
|
|
42
|
+
readonly entries: ReadonlyArray<DeclaredExtensionEntry>;
|
|
43
|
+
readonly hashByContractJson: Map<unknown, string>;
|
|
44
|
+
} {
|
|
45
|
+
const entries: DeclaredExtensionEntry[] = [];
|
|
46
|
+
const hashByContractJson = new Map<unknown, string>();
|
|
47
|
+
for (const pack of extensionPacks) {
|
|
48
|
+
const entry: DeclaredExtensionEntry = pack.contractSpace
|
|
49
|
+
? {
|
|
50
|
+
id: pack.id,
|
|
51
|
+
targetId: pack.targetId,
|
|
52
|
+
contractSpace: { contractJson: pack.contractSpace.contractJson },
|
|
53
|
+
}
|
|
54
|
+
: { id: pack.id, targetId: pack.targetId };
|
|
55
|
+
entries.push(entry);
|
|
56
|
+
if (pack.contractSpace) {
|
|
57
|
+
hashByContractJson.set(pack.contractSpace.contractJson, pack.contractSpace.headRef.hash);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
return { entries, hashByContractJson };
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Render a {@link LoadAggregateError} into a CLI structured-error
|
|
65
|
+
* envelope. Preserves error codes `5001` (layout) and `5002` (marker /
|
|
66
|
+
* drift / disjointness / etc.) so existing integration tests and
|
|
67
|
+
* downstream tooling continue to assert on the same `meta.violations[]`
|
|
68
|
+
* shape they did under the old precheck/marker-check helpers.
|
|
69
|
+
*/
|
|
70
|
+
export function mapLoadAggregateError(error: LoadAggregateError): CliStructuredError {
|
|
71
|
+
if (error.kind === 'layoutViolation') {
|
|
72
|
+
const lines = error.violations.map((v) => `- [${v.kind}] ${v.spaceId}`);
|
|
73
|
+
const summary =
|
|
74
|
+
error.violations.length === 1
|
|
75
|
+
? 'Contract-space layout violation detected'
|
|
76
|
+
: `Contract-space layout violations detected (${error.violations.length})`;
|
|
77
|
+
return new CliStructuredError('5001', summary, {
|
|
78
|
+
domain: 'MIG',
|
|
79
|
+
why: `The on-disk \`migrations/\` directory and your \`extensionPacks\` declaration are not in agreement.\n${lines.join('\n')}`,
|
|
80
|
+
fix: 'Run `prisma-next migrate` to materialise on-disk artefacts for declared extensions, or remove the orphan directory.',
|
|
81
|
+
docsUrl: 'https://pris.ly/contract-spaces',
|
|
82
|
+
meta: {
|
|
83
|
+
violations: error.violations.map((v) => ({
|
|
84
|
+
kind: v.kind,
|
|
85
|
+
spaceId: v.spaceId,
|
|
86
|
+
})),
|
|
87
|
+
},
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
if (error.kind === 'driftViolation') {
|
|
91
|
+
return new CliStructuredError('5002', `Contract-space drift detected for "${error.spaceId}"`, {
|
|
92
|
+
domain: 'MIG',
|
|
93
|
+
why: `The on-disk contract for space "${error.spaceId}" (hash ${error.priorHeadHash}) does not match the live extension descriptor (hash ${error.liveHash}).`,
|
|
94
|
+
fix: 'Run `prisma-next migrate` to refresh the on-disk artefacts to match the live descriptor.',
|
|
95
|
+
docsUrl: 'https://pris.ly/contract-spaces',
|
|
96
|
+
meta: {
|
|
97
|
+
violations: [
|
|
98
|
+
{
|
|
99
|
+
kind: 'drift',
|
|
100
|
+
spaceId: error.spaceId,
|
|
101
|
+
priorHeadHash: error.priorHeadHash,
|
|
102
|
+
liveHash: error.liveHash,
|
|
103
|
+
},
|
|
104
|
+
],
|
|
105
|
+
},
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
if (error.kind === 'disjointnessViolation') {
|
|
109
|
+
return new CliStructuredError(
|
|
110
|
+
'5002',
|
|
111
|
+
`Contract-space disjointness violation: storage element "${error.element}" claimed by multiple spaces`,
|
|
112
|
+
{
|
|
113
|
+
domain: 'MIG',
|
|
114
|
+
why: `Spaces ${error.claimedBy.map((s) => `"${s}"`).join(', ')} all claim the storage element "${error.element}". Each storage element must be owned by exactly one contract space.`,
|
|
115
|
+
fix: 'Update the conflicting contracts so each storage element is claimed by exactly one space.',
|
|
116
|
+
docsUrl: 'https://pris.ly/contract-spaces',
|
|
117
|
+
meta: {
|
|
118
|
+
violations: [
|
|
119
|
+
{
|
|
120
|
+
kind: 'disjointness',
|
|
121
|
+
spaceId: error.claimedBy.join(','),
|
|
122
|
+
element: error.element,
|
|
123
|
+
claimedBy: error.claimedBy,
|
|
124
|
+
},
|
|
125
|
+
],
|
|
126
|
+
},
|
|
127
|
+
},
|
|
128
|
+
);
|
|
129
|
+
}
|
|
130
|
+
if (error.kind === 'integrityFailure') {
|
|
131
|
+
return new CliStructuredError(
|
|
132
|
+
'5002',
|
|
133
|
+
`Contract-space integrity failure for "${error.spaceId}"`,
|
|
134
|
+
{
|
|
135
|
+
domain: 'MIG',
|
|
136
|
+
why: error.detail,
|
|
137
|
+
fix: 'Run `prisma-next migrate` to refresh on-disk artefacts, or restore the on-disk `migrations/` directory from version control.',
|
|
138
|
+
docsUrl: 'https://pris.ly/contract-spaces',
|
|
139
|
+
meta: {
|
|
140
|
+
violations: [{ kind: 'integrity', spaceId: error.spaceId, detail: error.detail }],
|
|
141
|
+
},
|
|
142
|
+
},
|
|
143
|
+
);
|
|
144
|
+
}
|
|
145
|
+
if (error.kind === 'validationFailure') {
|
|
146
|
+
return new CliStructuredError(
|
|
147
|
+
'5002',
|
|
148
|
+
`Contract-space contract validation failed for "${error.spaceId}"`,
|
|
149
|
+
{
|
|
150
|
+
domain: 'MIG',
|
|
151
|
+
why: error.detail,
|
|
152
|
+
fix: 'Run `prisma-next migrate` to refresh on-disk artefacts, or fix the extension descriptor producing the invalid contract.',
|
|
153
|
+
meta: {
|
|
154
|
+
violations: [{ kind: 'validation', spaceId: error.spaceId, detail: error.detail }],
|
|
155
|
+
},
|
|
156
|
+
},
|
|
157
|
+
);
|
|
158
|
+
}
|
|
159
|
+
// targetMismatch
|
|
160
|
+
return new CliStructuredError('5002', `Contract-space target mismatch for "${error.spaceId}"`, {
|
|
161
|
+
domain: 'MIG',
|
|
162
|
+
why: `Space "${error.spaceId}" targets "${error.actual}" but the project's adapter targets "${error.expected}".`,
|
|
163
|
+
fix: 'Update the extension descriptor to target the configured database, or change the project adapter.',
|
|
164
|
+
meta: {
|
|
165
|
+
violations: [
|
|
166
|
+
{
|
|
167
|
+
kind: 'targetMismatch',
|
|
168
|
+
spaceId: error.spaceId,
|
|
169
|
+
expected: error.expected,
|
|
170
|
+
actual: error.actual,
|
|
171
|
+
},
|
|
172
|
+
],
|
|
173
|
+
},
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Inputs needed to compose the aggregate loader at the CLI surface.
|
|
179
|
+
*
|
|
180
|
+
* Keeps the loader framework-neutral (no `Config` import) by accepting
|
|
181
|
+
* already-resolved structural inputs: validated app contract, target
|
|
182
|
+
* id, migrations root directory, and the set of extension descriptors.
|
|
183
|
+
*/
|
|
184
|
+
export interface BuildAggregateInputs<TFamilyId extends string, TTargetId extends string> {
|
|
185
|
+
readonly targetId: TTargetId;
|
|
186
|
+
readonly migrationsDir: string;
|
|
187
|
+
readonly appContract: Contract;
|
|
188
|
+
readonly extensionPacks: ReadonlyArray<ControlExtensionDescriptor<TFamilyId, TTargetId>>;
|
|
189
|
+
readonly validateContract: (contractJson: unknown) => Contract;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* Run the aggregate loader at the CLI surface, mapping any
|
|
194
|
+
* {@link LoadAggregateError} into a {@link CliStructuredError} envelope.
|
|
195
|
+
*
|
|
196
|
+
* App-side migration packages are intentionally not threaded through:
|
|
197
|
+
* `db init` / `db update` go through the planner's `synth` strategy for
|
|
198
|
+
* the app member (driven by `callerPolicy.ignoreGraphFor`), so the
|
|
199
|
+
* app's authored `migrations/` graph is not walked.
|
|
200
|
+
*
|
|
201
|
+
* @see specs/contract-space-aggregate-spec.md § Loader.
|
|
202
|
+
*/
|
|
203
|
+
export async function buildContractSpaceAggregate<
|
|
204
|
+
TFamilyId extends string,
|
|
205
|
+
TTargetId extends string,
|
|
206
|
+
>(
|
|
207
|
+
inputs: BuildAggregateInputs<TFamilyId, TTargetId>,
|
|
208
|
+
): Promise<Result<ContractSpaceAggregate, CliStructuredError>> {
|
|
209
|
+
const { entries, hashByContractJson } = toDeclaredExtensions(
|
|
210
|
+
inputs.extensionPacks as ReadonlyArray<ExtensionPackForAggregate>,
|
|
211
|
+
);
|
|
212
|
+
|
|
213
|
+
const loadInput: LoadAggregateInput = {
|
|
214
|
+
targetId: inputs.targetId,
|
|
215
|
+
migrationsDir: inputs.migrationsDir,
|
|
216
|
+
appContract: inputs.appContract,
|
|
217
|
+
declaredExtensions: entries,
|
|
218
|
+
validateContract: inputs.validateContract,
|
|
219
|
+
hashContract: (contractJson: unknown) => {
|
|
220
|
+
const precomputed = hashByContractJson.get(contractJson);
|
|
221
|
+
if (precomputed === undefined) {
|
|
222
|
+
throw new Error(
|
|
223
|
+
'CLI aggregate loader: encountered an extension contract without a pre-computed descriptor hash. This is a wiring bug.',
|
|
224
|
+
);
|
|
225
|
+
}
|
|
226
|
+
return precomputed;
|
|
227
|
+
},
|
|
228
|
+
appMigrationPackages: [],
|
|
229
|
+
};
|
|
230
|
+
|
|
231
|
+
const result: LoadAggregateOutput = await loadContractSpaceAggregate(loadInput);
|
|
232
|
+
if (!result.ok) {
|
|
233
|
+
return notOk(mapLoadAggregateError(result.failure));
|
|
234
|
+
}
|
|
235
|
+
return ok(result.value.aggregate);
|
|
236
|
+
}
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import { materialiseExtensionMigrationPackageIfMissing } from '@prisma-next/migration-tools/io';
|
|
2
|
+
import type { MigrationMetadata } from '@prisma-next/migration-tools/metadata';
|
|
3
|
+
import type { MigrationOps } from '@prisma-next/migration-tools/package';
|
|
4
|
+
import {
|
|
5
|
+
planAllSpaces,
|
|
6
|
+
type SpacePlanOutput,
|
|
7
|
+
spaceMigrationDirectory,
|
|
8
|
+
} from '@prisma-next/migration-tools/spaces';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* In-memory authored migration package shipped by an extension descriptor.
|
|
12
|
+
* Mirrors `MigrationPackage` from `@prisma-next/migration-tools/io`
|
|
13
|
+
* (the on-disk shape minus `dirPath`); redeclared structurally here so
|
|
14
|
+
* the CLI helper does not couple to the SQL family's `ExtensionMigrationPackage`
|
|
15
|
+
* type — any family that ships pre-built migration packages can pass them
|
|
16
|
+
* through unchanged.
|
|
17
|
+
*/
|
|
18
|
+
export interface DescriptorMigrationPackage {
|
|
19
|
+
readonly dirName: string;
|
|
20
|
+
readonly metadata: MigrationMetadata;
|
|
21
|
+
readonly ops: MigrationOps;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Minimal descriptor view consumed by the migration-materialisation pass.
|
|
26
|
+
* Mirrors {@link import('./contract-space-migrate-pass').MigrateExtensionInput}
|
|
27
|
+
* but adds the `migrations` field — the canonical set of pre-built
|
|
28
|
+
* migration packages the extension ships.
|
|
29
|
+
*/
|
|
30
|
+
export interface ExtensionMigrationsExtensionInput {
|
|
31
|
+
readonly id: string;
|
|
32
|
+
readonly contractSpace?: {
|
|
33
|
+
readonly contractJson: unknown;
|
|
34
|
+
readonly migrations: readonly DescriptorMigrationPackage[];
|
|
35
|
+
readonly headRef: { readonly hash: string; readonly invariants: readonly string[] };
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export interface ContractSpaceExtensionMigrationsPassInputs {
|
|
40
|
+
readonly migrationsDir: string;
|
|
41
|
+
readonly extensionPacks: ReadonlyArray<ExtensionMigrationsExtensionInput>;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export interface ContractSpaceExtensionMigrationsPassResult {
|
|
45
|
+
readonly emitted: readonly { readonly spaceId: string; readonly dirName: string }[];
|
|
46
|
+
readonly skipped: readonly { readonly spaceId: string; readonly dirName: string }[];
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Materialise an extension's pre-built migration packages onto disk
|
|
51
|
+
* under `migrations/<spaceId>/<dirName>/` for every package that does
|
|
52
|
+
* not yet exist there.
|
|
53
|
+
*
|
|
54
|
+
* Helper-location pattern — the per-space "planner" for extension
|
|
55
|
+
* spaces is a no-op that just returns the descriptor's `migrations`
|
|
56
|
+
* verbatim; the value `planAllSpaces` brings to this consumer site is
|
|
57
|
+
* **deterministic ordering** (alphabetical by spaceId) and
|
|
58
|
+
* **duplicate-spaceId detection**. The actual write is performed via
|
|
59
|
+
* `materialiseMigrationPackage` per package.
|
|
60
|
+
*
|
|
61
|
+
* Idempotent: an existing `migrations/<spaceId>/<dirName>/` is left
|
|
62
|
+
* untouched and reported in `result.skipped` — the helper never
|
|
63
|
+
* overwrites authored migration content, ensuring re-running
|
|
64
|
+
* `migrate` does not corrupt or churn extension migration packages.
|
|
65
|
+
*
|
|
66
|
+
* Per-space artefacts (`contract.json`, `contract.d.ts`,
|
|
67
|
+
* `refs/head.json`) are emitted by
|
|
68
|
+
* {@link import('./contract-space-migrate-pass').runContractSpaceMigratePass}
|
|
69
|
+
* separately — they cover the head-pointer side of the ledger. This
|
|
70
|
+
* helper covers the migration-graph side.
|
|
71
|
+
*/
|
|
72
|
+
export async function runContractSpaceExtensionMigrationsPass(
|
|
73
|
+
inputs: ContractSpaceExtensionMigrationsPassInputs,
|
|
74
|
+
): Promise<ContractSpaceExtensionMigrationsPassResult> {
|
|
75
|
+
const planInputs = inputs.extensionPacks
|
|
76
|
+
.filter(
|
|
77
|
+
(
|
|
78
|
+
pack,
|
|
79
|
+
): pack is ExtensionMigrationsExtensionInput & {
|
|
80
|
+
contractSpace: NonNullable<ExtensionMigrationsExtensionInput['contractSpace']>;
|
|
81
|
+
} => pack.contractSpace !== undefined,
|
|
82
|
+
)
|
|
83
|
+
.map((pack) => ({
|
|
84
|
+
spaceId: pack.id,
|
|
85
|
+
priorContract: null,
|
|
86
|
+
newContract: pack.contractSpace.contractJson,
|
|
87
|
+
__migrations: pack.contractSpace.migrations,
|
|
88
|
+
}));
|
|
89
|
+
|
|
90
|
+
// Threading the descriptor's pre-built migrations into the
|
|
91
|
+
// `planAllSpaces` callback by piggybacking on the input shape.
|
|
92
|
+
// The framework helper is generic over the per-space planner output;
|
|
93
|
+
// here the "planner" is a no-op that returns the descriptor's
|
|
94
|
+
// `migrations` array. The benefit of routing through `planAllSpaces`
|
|
95
|
+
// is duplicate-spaceId detection + alphabetical ordering — failures
|
|
96
|
+
// there throw `MIGRATION.DUPLICATE_SPACE_ID` before any write.
|
|
97
|
+
const planned: readonly SpacePlanOutput<DescriptorMigrationPackage>[] = planAllSpaces(
|
|
98
|
+
planInputs,
|
|
99
|
+
(input) =>
|
|
100
|
+
(input as typeof input & { readonly __migrations: readonly DescriptorMigrationPackage[] })
|
|
101
|
+
.__migrations,
|
|
102
|
+
);
|
|
103
|
+
|
|
104
|
+
const emitted: { spaceId: string; dirName: string }[] = [];
|
|
105
|
+
const skipped: { spaceId: string; dirName: string }[] = [];
|
|
106
|
+
|
|
107
|
+
for (const space of planned) {
|
|
108
|
+
const spaceDir = spaceMigrationDirectory(inputs.migrationsDir, space.spaceId);
|
|
109
|
+
for (const pkg of space.migrationPackages) {
|
|
110
|
+
const { written } = await materialiseExtensionMigrationPackageIfMissing(spaceDir, pkg);
|
|
111
|
+
if (written) {
|
|
112
|
+
emitted.push({ spaceId: space.spaceId, dirName: pkg.dirName });
|
|
113
|
+
} else {
|
|
114
|
+
skipped.push({ spaceId: space.spaceId, dirName: pkg.dirName });
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
return { emitted, skipped };
|
|
120
|
+
}
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
import {
|
|
2
|
+
detectSpaceContractDrift,
|
|
3
|
+
emitContractSpaceArtefacts,
|
|
4
|
+
readContractSpaceHeadRef,
|
|
5
|
+
type SpaceContractDriftResult,
|
|
6
|
+
} from '@prisma-next/migration-tools/spaces';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Minimal descriptor view consumed by the migrate-time per-space pass.
|
|
10
|
+
*
|
|
11
|
+
* The CLI receives descriptors typed against the SQL family (or any other
|
|
12
|
+
* family in the future); this helper only needs the structural shape of
|
|
13
|
+
* `contractSpace`, so it accepts an `unknown`-typed `contractJson` and
|
|
14
|
+
* a structurally-typed `headRef`. SQL-family callers pass the same
|
|
15
|
+
* `Contract<SqlStorage>` value through unchanged — `emitContractSpaceArtefacts`
|
|
16
|
+
* already serialises through `canonicalizeJson` and is framework-neutral.
|
|
17
|
+
*
|
|
18
|
+
* @see specs/framework-mechanism.spec.md § 3 — Per-space helper location.
|
|
19
|
+
*/
|
|
20
|
+
export interface MigrateExtensionInput {
|
|
21
|
+
readonly id: string;
|
|
22
|
+
readonly contractSpace?: {
|
|
23
|
+
readonly contractJson: unknown;
|
|
24
|
+
readonly headRef: { readonly hash: string; readonly invariants: readonly string[] };
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Inputs needed to compose the migrate-time per-space pass at the CLI
|
|
30
|
+
* surface — typically called once after the app-space migration package
|
|
31
|
+
* has been written, regardless of whether the app-space had structural
|
|
32
|
+
* changes (an extension bump alone should still re-pin its artefacts).
|
|
33
|
+
*/
|
|
34
|
+
export interface ContractSpaceMigratePassInputs {
|
|
35
|
+
readonly migrationsDir: string;
|
|
36
|
+
readonly extensionPacks: ReadonlyArray<MigrateExtensionInput>;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export interface ContractSpaceMigratePassResult {
|
|
40
|
+
readonly drifts: readonly SpaceContractDriftResult[];
|
|
41
|
+
readonly emittedSpaceIds: readonly string[];
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Run drift detection + on-disk artefact emission for every loaded
|
|
46
|
+
* extension space at `migrate` time.
|
|
47
|
+
*
|
|
48
|
+
* Per sub-spec § 3:
|
|
49
|
+
*
|
|
50
|
+
* - For each declared extension that exposes a `contractSpace`:
|
|
51
|
+
* - Read the on-disk head hash from `migrations/<spaceId>/refs/head.json`
|
|
52
|
+
* (returns `null` on first emit).
|
|
53
|
+
* - Compare against the descriptor's `headRef.hash` via
|
|
54
|
+
* `detectSpaceContractDrift`. The `kind` discriminant decides whether
|
|
55
|
+
* the user sees a warning (`drift`), a no-op silent emit (`firstEmit`,
|
|
56
|
+
* `noDrift`), or nothing at all.
|
|
57
|
+
* - Always re-emit the on-disk artefacts (`contract.json`, `contract.d.ts`,
|
|
58
|
+
* `refs/head.json`). The framework owns these files and the helper is
|
|
59
|
+
* idempotent.
|
|
60
|
+
*
|
|
61
|
+
* Drift warnings are returned to the caller for formatting (TerminalUI,
|
|
62
|
+
* structured-output envelope, etc.) — the helper does not print directly,
|
|
63
|
+
* keeping it framework-neutral and unit-testable.
|
|
64
|
+
*
|
|
65
|
+
* Extension migration packages (the descriptor's pre-canned `migrations`
|
|
66
|
+
* array → `migrations/<spaceId>/<dirName>/`) are intentionally not
|
|
67
|
+
* materialised here — that interaction will be wired in a follow-on round
|
|
68
|
+
* once the runner-side single-tx slice (sub-spec § 6) is in place.
|
|
69
|
+
* On-disk artefacts are sufficient to lock the drift-warning behaviour
|
|
70
|
+
* and the always-on re-emit AC for R2.
|
|
71
|
+
*
|
|
72
|
+
* @see specs/framework-mechanism.spec.md § 3 — Drift detection (T1.9).
|
|
73
|
+
*/
|
|
74
|
+
export async function runContractSpaceMigratePass(
|
|
75
|
+
inputs: ContractSpaceMigratePassInputs,
|
|
76
|
+
): Promise<ContractSpaceMigratePassResult> {
|
|
77
|
+
const drifts: SpaceContractDriftResult[] = [];
|
|
78
|
+
const emittedSpaceIds: string[] = [];
|
|
79
|
+
|
|
80
|
+
for (const pack of inputs.extensionPacks) {
|
|
81
|
+
if (pack.contractSpace === undefined) continue;
|
|
82
|
+
const { contractJson, headRef } = pack.contractSpace;
|
|
83
|
+
|
|
84
|
+
const onDiskHeadRef = await readContractSpaceHeadRef(inputs.migrationsDir, pack.id);
|
|
85
|
+
const drift = detectSpaceContractDrift(pack.id, {
|
|
86
|
+
descriptorHash: headRef.hash,
|
|
87
|
+
priorHeadHash: onDiskHeadRef?.hash ?? null,
|
|
88
|
+
});
|
|
89
|
+
drifts.push(drift);
|
|
90
|
+
|
|
91
|
+
await emitContractSpaceArtefacts(inputs.migrationsDir, pack.id, {
|
|
92
|
+
contract: contractJson,
|
|
93
|
+
contractDts: buildPlaceholderContractDts(pack.id),
|
|
94
|
+
headRef: { hash: headRef.hash, invariants: headRef.invariants },
|
|
95
|
+
});
|
|
96
|
+
emittedSpaceIds.push(pack.id);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
return { drifts, emittedSpaceIds };
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Format the user-facing drift warning for a single space. Callers
|
|
104
|
+
* funnel this through their preferred output channel (TerminalUI line,
|
|
105
|
+
* structured-output envelope `warnings[]`, etc.).
|
|
106
|
+
*
|
|
107
|
+
* Locks AM7 — drift warning surfaces the extension name and the diff
|
|
108
|
+
* direction (descriptor → on-disk head).
|
|
109
|
+
*/
|
|
110
|
+
export function formatContractSpaceDriftWarning(drift: SpaceContractDriftResult): string {
|
|
111
|
+
if (drift.kind !== 'drift') {
|
|
112
|
+
throw new Error(`formatContractSpaceDriftWarning called with non-drift result: ${drift.kind}`);
|
|
113
|
+
}
|
|
114
|
+
return (
|
|
115
|
+
`Contract-space drift detected for "${drift.spaceId}": descriptor hash ` +
|
|
116
|
+
`${drift.descriptorHash} differs from on-disk head hash ${drift.priorHeadHash ?? '<none>'}. ` +
|
|
117
|
+
`The on-disk artefacts under migrations/${drift.spaceId}/ will be refreshed to match the descriptor.`
|
|
118
|
+
);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Placeholder `.d.ts` content for an extension space's on-disk mirror.
|
|
123
|
+
*
|
|
124
|
+
* Rendering a fully-typed `.d.ts` for an extension contract requires the
|
|
125
|
+
* SQL-family renderer with the codec / typemap registry threaded
|
|
126
|
+
* through; that integration is tracked under sub-spec Open Question 3
|
|
127
|
+
* (see `projects/extension-contract-spaces/specs/framework-mechanism.spec.md`).
|
|
128
|
+
*
|
|
129
|
+
* Until that ships, the on-disk `.d.ts` is a `@ts-nocheck` stub. The
|
|
130
|
+
* spec gap closing alongside the typed renderer is **AC2 / AC14**
|
|
131
|
+
* (byte-equivalence of per-space artefacts under `migrate`):
|
|
132
|
+
* a placeholder cannot be byte-equal to a fully-rendered `.d.ts` from
|
|
133
|
+
* the same descriptor, so AC2 / AC14 are PARTIAL today and become
|
|
134
|
+
* fully-PASS once OQ3 closes.
|
|
135
|
+
*
|
|
136
|
+
* Scheduled to close in **M3** (cipherstash editor tooling) — that's
|
|
137
|
+
* the milestone where the typed renderer gets its first real
|
|
138
|
+
* extension-space consumer and the byte-equivalence guarantee is
|
|
139
|
+
* practically required.
|
|
140
|
+
*/
|
|
141
|
+
function buildPlaceholderContractDts(spaceId: string): string {
|
|
142
|
+
return [
|
|
143
|
+
'// @ts-nocheck',
|
|
144
|
+
'/**',
|
|
145
|
+
` * Placeholder \`.d.ts\` for extension space "${spaceId}".`,
|
|
146
|
+
' *',
|
|
147
|
+
' * The framework re-emits this file on every `migrate` run alongside',
|
|
148
|
+
' * `contract.json` and `refs/head.json`. A typed `.d.ts` rendering',
|
|
149
|
+
" * pass for extension contracts is tracked under the project's open",
|
|
150
|
+
' * questions; until that ships, consumers should import',
|
|
151
|
+
' * `contract.json` directly with `validateContract<…>(…)`.',
|
|
152
|
+
' */',
|
|
153
|
+
'export {};',
|
|
154
|
+
'',
|
|
155
|
+
].join('\n');
|
|
156
|
+
}
|