@prisma/orm-mongo 8.0.0-rc.5 → 8.0.0-rc.5-dev.2
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/package.json +11 -10
- package/skills/prisma-8/SKILL.md +84 -0
- package/skills/prisma-8/references/build.md +142 -0
- package/skills/prisma-8/references/contract.md +417 -0
- package/skills/prisma-8/references/debug.md +141 -0
- package/skills/prisma-8/references/feedback.md +251 -0
- package/skills/prisma-8/references/migration-review.md +224 -0
- package/skills/prisma-8/references/migrations.md +519 -0
- package/skills/prisma-8/references/queries-mongo.md +236 -0
- package/skills/prisma-8/references/queries-postgres.md +415 -0
- package/skills/prisma-8/references/queries.md +168 -0
- package/skills/prisma-8/references/quickstart.md +326 -0
- package/skills/prisma-8/references/runtime.md +344 -0
- package/skills/prisma-8/references/supabase.md +244 -0
- package/skills/prisma-8/references/upgrade-app.md +101 -0
- package/skills/prisma-8/references/upgrade-extension.md +105 -0
- package/skills/prisma-8/upgrading/app/upgrades/0.10-to-0.11/instructions.md +56 -0
- package/skills/prisma-8/upgrading/app/upgrades/0.11-to-0.12/instructions.md +381 -0
- package/skills/prisma-8/upgrading/app/upgrades/0.11-to-0.12/re-emit-closed-mongo-contracts.ts +202 -0
- package/skills/prisma-8/upgrading/app/upgrades/0.11-to-0.12/re-emit-domain-namespaced-contracts.ts +201 -0
- package/skills/prisma-8/upgrading/app/upgrades/0.11-to-0.12/re-emit-postgres-public-default.ts +198 -0
- package/skills/prisma-8/upgrading/app/upgrades/0.11-to-0.12/strip-migration-labels-hints.ts +340 -0
- package/skills/prisma-8/upgrading/app/upgrades/0.12-to-0.13/instructions.md +339 -0
- package/skills/prisma-8/upgrading/app/upgrades/0.12-to-0.13/re-emit-mti-variant-link-columns.ts +229 -0
- package/skills/prisma-8/upgrading/app/upgrades/0.13-to-0.14/instructions.md +543 -0
- package/skills/prisma-8/upgrading/app/upgrades/0.13-to-0.14/migration-op-factories-to-methods.ts +290 -0
- package/skills/prisma-8/upgrading/app/upgrades/0.13-to-0.14/uuid-preset-rename.ts +43 -0
- package/skills/prisma-8/upgrading/app/upgrades/0.14-to-0.15/instructions.md +359 -0
- package/skills/prisma-8/upgrading/app/upgrades/0.15-to-0.16/instructions.md +173 -0
- package/skills/prisma-8/upgrading/app/upgrades/0.16-to-0.17/instructions.md +805 -0
- package/skills/prisma-8/upgrading/app/upgrades/0.16-to-0.17/strip-sha256-hash-prefixes.ts +382 -0
- package/skills/prisma-8/upgrading/app/upgrades/0.17-to-8.0.0-rc.1/instructions.md +72 -0
- package/skills/prisma-8/upgrading/app/upgrades/0.7-to-0.8/instructions.md +8 -0
- package/skills/prisma-8/upgrading/app/upgrades/0.8-to-0.9/instructions.md +36 -0
- package/skills/prisma-8/upgrading/app/upgrades/0.8-to-0.9/strip-inline-contracts.ts +226 -0
- package/skills/prisma-8/upgrading/app/upgrades/0.9-to-0.10/instructions.md +86 -0
- package/skills/prisma-8/upgrading/app/upgrades/0.9-to-0.10/stamp-storage-types-kind.ts +360 -0
- package/skills/prisma-8/upgrading/app/upgrades/8.0.0-rc.1-to-8.0.0-rc.2/instructions.md +588 -0
- package/skills/prisma-8/upgrading/app/upgrades/8.0.0-rc.2-to-8.0.0-rc.3/instructions.md +5 -0
- package/skills/prisma-8/upgrading/app/upgrades/8.0.0-rc.3-to-8.0.0-rc.4/instructions.md +158 -0
- package/skills/prisma-8/upgrading/app/upgrades/8.0.0-rc.4-to-8.0.0-rc.5/instructions.md +42 -0
- package/skills/prisma-8/upgrading/extension/upgrades/0.10-to-0.11/instructions.md +276 -0
- package/skills/prisma-8/upgrading/extension/upgrades/0.11-to-0.12/instructions.md +738 -0
- package/skills/prisma-8/upgrading/extension/upgrades/0.11-to-0.12/migrate-contract-testing-imports.ts +97 -0
- package/skills/prisma-8/upgrading/extension/upgrades/0.11-to-0.12/regenerate-extension-public-baseline.ts +223 -0
- package/skills/prisma-8/upgrading/extension/upgrades/0.11-to-0.12/strip-migration-labels-hints.ts +340 -0
- package/skills/prisma-8/upgrading/extension/upgrades/0.12-to-0.13/instructions.md +266 -0
- package/skills/prisma-8/upgrading/extension/upgrades/0.13-to-0.14/instructions.md +522 -0
- package/skills/prisma-8/upgrading/extension/upgrades/0.13-to-0.14/migration-op-factories-to-methods.ts +290 -0
- package/skills/prisma-8/upgrading/extension/upgrades/0.13-to-0.14/uuid-preset-rename.ts +43 -0
- package/skills/prisma-8/upgrading/extension/upgrades/0.14-to-0.15/instructions.md +803 -0
- package/skills/prisma-8/upgrading/extension/upgrades/0.15-to-0.16/instructions.md +219 -0
- package/skills/prisma-8/upgrading/extension/upgrades/0.16-to-0.17/instructions.md +731 -0
- package/skills/prisma-8/upgrading/extension/upgrades/0.16-to-0.17/strip-sha256-hash-prefixes.ts +382 -0
- package/skills/prisma-8/upgrading/extension/upgrades/0.17-to-8.0.0-rc.1/instructions.md +194 -0
- package/skills/prisma-8/upgrading/extension/upgrades/0.7-to-0.8/instructions.md +8 -0
- package/skills/prisma-8/upgrading/extension/upgrades/0.8-to-0.9/instructions.md +57 -0
- package/skills/prisma-8/upgrading/extension/upgrades/0.8-to-0.9/strip-inline-contracts.ts +226 -0
- package/skills/prisma-8/upgrading/extension/upgrades/0.9-to-0.10/instructions.md +150 -0
- package/skills/prisma-8/upgrading/extension/upgrades/0.9-to-0.10/stamp-storage-types-kind.ts +360 -0
- package/skills/prisma-8/upgrading/extension/upgrades/8.0.0-rc.1-to-8.0.0-rc.2/instructions.md +746 -0
- package/skills/prisma-8/upgrading/extension/upgrades/8.0.0-rc.2-to-8.0.0-rc.3/instructions.md +5 -0
- package/skills/prisma-8/upgrading/extension/upgrades/8.0.0-rc.3-to-8.0.0-rc.4/instructions.md +137 -0
- package/skills/prisma-8/upgrading/extension/upgrades/8.0.0-rc.4-to-8.0.0-rc.5/instructions.md +129 -0
package/skills/prisma-8/upgrading/extension/upgrades/0.16-to-0.17/strip-sha256-hash-prefixes.ts
ADDED
|
@@ -0,0 +1,382 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Brings checked-in `migrations/` trees onto the 0.17 bare-hex hash
|
|
3
|
+
* representation: strips the legacy `sha256:` prefix from every hash literal,
|
|
4
|
+
* maps the empty-tree sentinel `sha256:empty` to `empty`, recomputes each
|
|
5
|
+
* migration's `migrationHash` over the bare-hex content, and repoints
|
|
6
|
+
* `refs/*.json` at the recomputed hashes.
|
|
7
|
+
*
|
|
8
|
+
* Background: starting at the 0.17 release, content hashes are bare
|
|
9
|
+
* lowercase hex — the `sha256:` prefix is gone from every surface (emitted
|
|
10
|
+
* `contract.json` / `contract.d.ts`, migration manifests, refs, CLI output,
|
|
11
|
+
* and the marker/ledger tables). The algorithm never varied per hash, so the
|
|
12
|
+
* prefix carried no information; a format change is signalled by the hash
|
|
13
|
+
* value changing, not by an in-band tag. Loaders now reject the legacy
|
|
14
|
+
* prefixed form outright.
|
|
15
|
+
*
|
|
16
|
+
* Two distinct effects on checked-in migration artifacts:
|
|
17
|
+
*
|
|
18
|
+
* - Contract hashes (`from` / `to` in `migration.json`, `storageHash` /
|
|
19
|
+
* `profileHash` in contract snapshots and `.d.ts` branded literals,
|
|
20
|
+
* `storageHash` stamps inside `ops.json`) keep their VALUE — only the
|
|
21
|
+
* `sha256:` prefix drops.
|
|
22
|
+
* - `migrationHash` VALUES change, because the hashed manifest bytes embed
|
|
23
|
+
* the (now bare) `from` / `to` strings. Every manifest gets a freshly
|
|
24
|
+
* recomputed hash, and every `refs/*.json` that pointed at an old
|
|
25
|
+
* migration hash is rewritten to the recomputed one.
|
|
26
|
+
*
|
|
27
|
+
* Before 0.17 a manifest was:
|
|
28
|
+
*
|
|
29
|
+
* {
|
|
30
|
+
* "from": "sha256:8ee1e7ce…",
|
|
31
|
+
* "to": "sha256:059f3f35…",
|
|
32
|
+
* "providedInvariants": [],
|
|
33
|
+
* "createdAt": "2026-…",
|
|
34
|
+
* "migrationHash": "sha256:3c5205d2…"
|
|
35
|
+
* }
|
|
36
|
+
*
|
|
37
|
+
* Starting at 0.17 the same manifest is:
|
|
38
|
+
*
|
|
39
|
+
* {
|
|
40
|
+
* "from": "8ee1e7ce…",
|
|
41
|
+
* "to": "059f3f35…",
|
|
42
|
+
* "providedInvariants": [],
|
|
43
|
+
* "createdAt": "2026-…",
|
|
44
|
+
* "migrationHash": "2be2085f…" // recomputed over the bare-hex bytes
|
|
45
|
+
* }
|
|
46
|
+
*
|
|
47
|
+
* Format-preserving edit: hash literals are rewritten in place via a targeted
|
|
48
|
+
* pattern (`"sha256:<64 hex>"` / `"sha256:empty"`, single- or double-quoted),
|
|
49
|
+
* and the `migrationHash` value is swapped in place. Every other byte (key
|
|
50
|
+
* order, indentation, inline-vs-expanded arrays) is left exactly as the
|
|
51
|
+
* authoring tool wrote it, so diffs stay minimal.
|
|
52
|
+
*
|
|
53
|
+
* Confinement: an on-disk migration package is a `migration.json` paired
|
|
54
|
+
* with a sibling `ops.json`. The walk keys off that pair; within a package
|
|
55
|
+
* directory every `.json` / `.ts` sibling (pre-store contract snapshots,
|
|
56
|
+
* `.d.ts` branded types, the executable `migration.ts`) has its hash
|
|
57
|
+
* literals stripped. Content-addressed store entries
|
|
58
|
+
* (`migrations/snapshots/<hex>/contract.json` + `contract.d.ts`) are also
|
|
59
|
+
* covered, so the codemod handles both the pre-store sibling layout and the
|
|
60
|
+
* store layout — run it BEFORE `scripts/migrate-migrations-layout.mjs` when
|
|
61
|
+
* converting the layout in the same upgrade (the 0.17 migrator accepts only
|
|
62
|
+
* bare-hex trees). Store directory names are the hash's hex, which is
|
|
63
|
+
* unchanged by the prefix drop, so no directory is renamed. Ref files are
|
|
64
|
+
* `refs/*.json` under a directory named `migrations`; each gets old-hash →
|
|
65
|
+
* recomputed-hash repointing plus prefix stripping (which also maps the
|
|
66
|
+
* `sha256:empty` sentinel to `empty`).
|
|
67
|
+
*
|
|
68
|
+
* The hash algorithm is replicated inline (canonicalisation rules from
|
|
69
|
+
* `@internal/framework-components` `canonicalizeJson` + the
|
|
70
|
+
* migration-tools `computeMigrationHash`, which returns bare hex from 0.17)
|
|
71
|
+
* so this script stays self-contained — consumers run it via `pnpm exec tsx`
|
|
72
|
+
* from their project root with no dependency on any `@internal/*` package
|
|
73
|
+
* being resolvable from that root.
|
|
74
|
+
*
|
|
75
|
+
* The codemod is idempotent: an already-bare tree carries no `sha256:`
|
|
76
|
+
* literals and its recomputed hashes match the stored ones, so the edit is a
|
|
77
|
+
* no-op and every file is left untouched.
|
|
78
|
+
*
|
|
79
|
+
* Flags:
|
|
80
|
+
* --check dry-run; lists files that still need fixing and exits 1 if
|
|
81
|
+
* any remain.
|
|
82
|
+
*/
|
|
83
|
+
import { createHash } from 'node:crypto';
|
|
84
|
+
import { readdir, readFile, writeFile } from 'node:fs/promises';
|
|
85
|
+
import { basename, dirname, join, sep } from 'node:path';
|
|
86
|
+
|
|
87
|
+
const SKIP_DIRS = new Set(['node_modules', '.git', 'dist', 'build']);
|
|
88
|
+
|
|
89
|
+
const dryRun = process.argv.includes('--check');
|
|
90
|
+
const projectRoot = process.cwd();
|
|
91
|
+
|
|
92
|
+
// --- Inline canonicalisation + hash --------------------------------------
|
|
93
|
+
// Replicated from `@internal/framework-components` `canonicalizeJson`
|
|
94
|
+
// (sortKeys + JSON.stringify) and the migration-tools `computeMigrationHash`.
|
|
95
|
+
// Kept inline so the script has no `@internal/*` import — pnpm's strict
|
|
96
|
+
// node_modules layout won't resolve transitive framework deps from a
|
|
97
|
+
// consumer's project root.
|
|
98
|
+
|
|
99
|
+
function sortKeys(value: unknown): unknown {
|
|
100
|
+
if (value === null || typeof value !== 'object') {
|
|
101
|
+
return value;
|
|
102
|
+
}
|
|
103
|
+
if (Array.isArray(value)) {
|
|
104
|
+
return value.map(sortKeys);
|
|
105
|
+
}
|
|
106
|
+
const sorted: Record<string, unknown> = Object.create(null);
|
|
107
|
+
for (const [key, entry] of Object.entries(value).sort(([a], [b]) =>
|
|
108
|
+
a < b ? -1 : a > b ? 1 : 0,
|
|
109
|
+
)) {
|
|
110
|
+
sorted[key] = sortKeys(entry);
|
|
111
|
+
}
|
|
112
|
+
return sorted;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function canonicalizeJson(value: unknown): string {
|
|
116
|
+
return JSON.stringify(sortKeys(value));
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function sha256Hex(input: string): string {
|
|
120
|
+
return createHash('sha256').update(input).digest('hex');
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Content-addressed migration hash over (metadata envelope, ops), 0.17 form:
|
|
125
|
+
* bare hex, no prefix. The `migrationHash` field is stripped before hashing
|
|
126
|
+
* so the same function works at write time (no hash yet) and at recompute
|
|
127
|
+
* time (rehashing an already-attested record over the bare-hex envelope).
|
|
128
|
+
*/
|
|
129
|
+
function computeMigrationHash(metadata: Record<string, unknown>, ops: unknown): string {
|
|
130
|
+
const { migrationHash: _migrationHash, ...strippedMeta } = metadata;
|
|
131
|
+
|
|
132
|
+
const partHashes = [canonicalizeJson(strippedMeta), canonicalizeJson(ops)].map(sha256Hex);
|
|
133
|
+
return sha256Hex(canonicalizeJson(partHashes));
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// --- Prefix stripping ------------------------------------------------------
|
|
137
|
+
|
|
138
|
+
// A legacy hash literal: `sha256:` + 64 lowercase hex chars, or the
|
|
139
|
+
// empty-tree sentinel `sha256:empty`, in single or double quotes. Quoting is
|
|
140
|
+
// required so prose that merely mentions the prefix is never rewritten.
|
|
141
|
+
const LEGACY_HASH_LITERAL = /(["'])sha256:([0-9a-f]{64}|empty)\1/g;
|
|
142
|
+
|
|
143
|
+
function stripHashPrefixes(text: string): string {
|
|
144
|
+
return text.replace(LEGACY_HASH_LITERAL, (_full, quote: string, hash: string) => {
|
|
145
|
+
return `${quote}${hash}${quote}`;
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function replaceMigrationHash(text: string, oldHash: string, newHash: string): string {
|
|
150
|
+
if (oldHash === newHash) return text;
|
|
151
|
+
const re = new RegExp(`("migrationHash"[ \\t]*:[ \\t]*)"${oldHash}"`);
|
|
152
|
+
if (re.exec(text) === null) {
|
|
153
|
+
throw new Error('could not locate the migrationHash value to replace');
|
|
154
|
+
}
|
|
155
|
+
return text.replace(re, (_full, prefix: string) => `${prefix}"${newHash}"`);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// --- Filesystem walk ------------------------------------------------------
|
|
159
|
+
|
|
160
|
+
interface WalkResult {
|
|
161
|
+
readonly manifests: string[];
|
|
162
|
+
readonly refFiles: string[];
|
|
163
|
+
readonly snapshotFiles: string[];
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
async function findMigrationArtifacts(root: string): Promise<WalkResult> {
|
|
167
|
+
const manifests: string[] = [];
|
|
168
|
+
const refFiles: string[] = [];
|
|
169
|
+
const snapshotFiles: string[] = [];
|
|
170
|
+
|
|
171
|
+
async function walk(dir: string): Promise<void> {
|
|
172
|
+
let entries: Awaited<ReturnType<typeof readdir>>;
|
|
173
|
+
try {
|
|
174
|
+
entries = await readdir(dir, { withFileTypes: true });
|
|
175
|
+
} catch {
|
|
176
|
+
// Unreadable directory — skip silently. The consumer's project root may
|
|
177
|
+
// legitimately contain restricted directories.
|
|
178
|
+
return;
|
|
179
|
+
}
|
|
180
|
+
for (const entry of entries) {
|
|
181
|
+
const path = join(dir, entry.name);
|
|
182
|
+
if (entry.isDirectory()) {
|
|
183
|
+
if (SKIP_DIRS.has(entry.name)) continue;
|
|
184
|
+
await walk(path);
|
|
185
|
+
} else if (entry.isFile() && entry.name === 'migration.json') {
|
|
186
|
+
manifests.push(path);
|
|
187
|
+
} else if (
|
|
188
|
+
entry.isFile() &&
|
|
189
|
+
entry.name.endsWith('.json') &&
|
|
190
|
+
basename(dir) === 'refs' &&
|
|
191
|
+
dirname(dir).split(sep).includes('migrations')
|
|
192
|
+
) {
|
|
193
|
+
refFiles.push(path);
|
|
194
|
+
} else if (
|
|
195
|
+
entry.isFile() &&
|
|
196
|
+
(entry.name.endsWith('.json') || entry.name.endsWith('.ts')) &&
|
|
197
|
+
basename(dirname(dir)) === 'snapshots' &&
|
|
198
|
+
dirname(dirname(dir)).split(sep).includes('migrations')
|
|
199
|
+
) {
|
|
200
|
+
snapshotFiles.push(path);
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
await walk(root);
|
|
206
|
+
return {
|
|
207
|
+
manifests: manifests.sort(),
|
|
208
|
+
refFiles: refFiles.sort(),
|
|
209
|
+
snapshotFiles: snapshotFiles.sort(),
|
|
210
|
+
};
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// --- Per-file transforms ---------------------------------------------------
|
|
214
|
+
|
|
215
|
+
/** Narrows an arbitrary JSON-parsed value to a plain object (manifest shape). */
|
|
216
|
+
function isJsonObject(value: unknown): value is Record<string, unknown> {
|
|
217
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
type Status = 'already-clean' | 'needs-fix' | 'fixed' | 'skipped-no-ops';
|
|
221
|
+
|
|
222
|
+
interface Result {
|
|
223
|
+
readonly path: string;
|
|
224
|
+
readonly status: Status;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
const results: Result[] = [];
|
|
228
|
+
/** Old migration hash (as previously stored, prefixed) → recomputed bare hash. */
|
|
229
|
+
const migrationHashMap = new Map<string, string>();
|
|
230
|
+
|
|
231
|
+
async function emit(path: string, before: string, after: string): Promise<Result> {
|
|
232
|
+
if (after === before) {
|
|
233
|
+
return { path, status: 'already-clean' };
|
|
234
|
+
}
|
|
235
|
+
if (!dryRun) await writeFile(path, after, 'utf-8');
|
|
236
|
+
return { path, status: dryRun ? 'needs-fix' : 'fixed' };
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
async function processSibling(path: string): Promise<Result> {
|
|
240
|
+
const raw = await readFile(path, 'utf-8');
|
|
241
|
+
return emit(path, raw, stripHashPrefixes(raw));
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
async function processPackage(manifestPath: string): Promise<Result[]> {
|
|
245
|
+
const raw = await readFile(manifestPath, 'utf-8');
|
|
246
|
+
|
|
247
|
+
let parsed: unknown;
|
|
248
|
+
try {
|
|
249
|
+
parsed = JSON.parse(raw);
|
|
250
|
+
} catch (error) {
|
|
251
|
+
throw new Error(
|
|
252
|
+
`${manifestPath}: not valid JSON (${error instanceof Error ? error.message : String(error)})`,
|
|
253
|
+
);
|
|
254
|
+
}
|
|
255
|
+
if (!isJsonObject(parsed)) {
|
|
256
|
+
return [{ path: manifestPath, status: 'already-clean' }]; // not a manifest object
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
const packageDir = dirname(manifestPath);
|
|
260
|
+
|
|
261
|
+
// A complete on-disk migration package pairs `migration.json` with a sibling
|
|
262
|
+
// `ops.json` (the operations the hash is computed over); without it we cannot
|
|
263
|
+
// recompute the hash, so this is not a package we should touch.
|
|
264
|
+
const opsPath = join(packageDir, 'ops.json');
|
|
265
|
+
let opsRaw: string;
|
|
266
|
+
try {
|
|
267
|
+
opsRaw = await readFile(opsPath, 'utf-8');
|
|
268
|
+
} catch {
|
|
269
|
+
return [{ path: manifestPath, status: 'skipped-no-ops' }];
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
const out: Result[] = [];
|
|
273
|
+
|
|
274
|
+
// Ops: strip prefixes (e.g. `meta.storageHash` stamps inside operation
|
|
275
|
+
// payloads), then parse the stripped text — the recomputed hash covers the
|
|
276
|
+
// bare-hex operations exactly as they will sit on disk.
|
|
277
|
+
const strippedOpsRaw = stripHashPrefixes(opsRaw);
|
|
278
|
+
const ops: unknown = JSON.parse(strippedOpsRaw);
|
|
279
|
+
out.push(await emit(opsPath, opsRaw, strippedOpsRaw));
|
|
280
|
+
|
|
281
|
+
// Manifest: strip prefixes from `from` / `to` (and the sentinel), then
|
|
282
|
+
// recompute `migrationHash` over the bare-hex envelope + bare-hex ops.
|
|
283
|
+
// Canonicalisation is order/whitespace independent, so parsing the stripped
|
|
284
|
+
// text is the right input regardless of on-disk formatting.
|
|
285
|
+
const strippedManifestRaw = stripHashPrefixes(raw);
|
|
286
|
+
const strippedMeta = JSON.parse(strippedManifestRaw);
|
|
287
|
+
if (!isJsonObject(strippedMeta)) {
|
|
288
|
+
throw new Error(`${manifestPath}: manifest is not a JSON object`);
|
|
289
|
+
}
|
|
290
|
+
const newHash = computeMigrationHash(strippedMeta, ops);
|
|
291
|
+
|
|
292
|
+
const oldStoredHash = parsed['migrationHash'];
|
|
293
|
+
if (typeof oldStoredHash !== 'string') {
|
|
294
|
+
throw new Error(`${manifestPath}: manifest is missing a string \`migrationHash\` field`);
|
|
295
|
+
}
|
|
296
|
+
const strippedOldHash = stripHashPrefixes(`"${oldStoredHash}"`).slice(1, -1);
|
|
297
|
+
migrationHashMap.set(oldStoredHash, newHash);
|
|
298
|
+
migrationHashMap.set(strippedOldHash, newHash);
|
|
299
|
+
|
|
300
|
+
out.push(
|
|
301
|
+
await emit(
|
|
302
|
+
manifestPath,
|
|
303
|
+
raw,
|
|
304
|
+
replaceMigrationHash(strippedManifestRaw, strippedOldHash, newHash),
|
|
305
|
+
),
|
|
306
|
+
);
|
|
307
|
+
|
|
308
|
+
// Siblings: contract snapshots (`*-contract.json`), branded-literal type
|
|
309
|
+
// files (`*.d.ts`), and the executable `migration.ts` all carry contract
|
|
310
|
+
// hash literals whose value is stable — only the prefix drops.
|
|
311
|
+
let entries: Awaited<ReturnType<typeof readdir>>;
|
|
312
|
+
try {
|
|
313
|
+
entries = await readdir(packageDir, { withFileTypes: true });
|
|
314
|
+
} catch {
|
|
315
|
+
return out;
|
|
316
|
+
}
|
|
317
|
+
for (const entry of entries) {
|
|
318
|
+
if (!entry.isFile()) continue;
|
|
319
|
+
if (entry.name === 'migration.json' || entry.name === 'ops.json') continue;
|
|
320
|
+
if (!entry.name.endsWith('.json') && !entry.name.endsWith('.ts')) continue;
|
|
321
|
+
out.push(await processSibling(join(packageDir, entry.name)));
|
|
322
|
+
}
|
|
323
|
+
return out;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
async function processRefFile(path: string): Promise<Result> {
|
|
327
|
+
const raw = await readFile(path, 'utf-8');
|
|
328
|
+
|
|
329
|
+
// Repoint at recomputed migration hashes first (a ref stores the migration
|
|
330
|
+
// hash of the package it names), then strip any remaining prefixes — which
|
|
331
|
+
// covers contract-hash refs and maps `sha256:empty` → `empty`.
|
|
332
|
+
let text = raw;
|
|
333
|
+
for (const [oldHash, newHash] of migrationHashMap) {
|
|
334
|
+
if (oldHash === newHash) continue;
|
|
335
|
+
text = text.replaceAll(`"${oldHash}"`, `"${newHash}"`);
|
|
336
|
+
}
|
|
337
|
+
text = stripHashPrefixes(text);
|
|
338
|
+
|
|
339
|
+
return emit(path, raw, text);
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
// --- Driver ---------------------------------------------------------------
|
|
343
|
+
|
|
344
|
+
const { manifests, refFiles, snapshotFiles } = await findMigrationArtifacts(projectRoot);
|
|
345
|
+
if (manifests.length === 0 && refFiles.length === 0 && snapshotFiles.length === 0) {
|
|
346
|
+
console.error(`No migration artifacts found under ${projectRoot}.`);
|
|
347
|
+
process.exit(1);
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
for (const manifestPath of manifests) {
|
|
351
|
+
results.push(...(await processPackage(manifestPath)));
|
|
352
|
+
}
|
|
353
|
+
for (const snapshotPath of snapshotFiles) {
|
|
354
|
+
results.push(await processSibling(snapshotPath));
|
|
355
|
+
}
|
|
356
|
+
for (const refPath of refFiles) {
|
|
357
|
+
results.push(await processRefFile(refPath));
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
let changed = 0;
|
|
361
|
+
let alreadyClean = 0;
|
|
362
|
+
let skipped = 0;
|
|
363
|
+
for (const result of results) {
|
|
364
|
+
const rel = result.path.slice(projectRoot.length + 1);
|
|
365
|
+
if (result.status === 'already-clean') {
|
|
366
|
+
alreadyClean += 1;
|
|
367
|
+
} else if (result.status === 'skipped-no-ops') {
|
|
368
|
+
skipped += 1;
|
|
369
|
+
console.log(`SKIP ${rel} (no sibling ops.json — not a migration package)`);
|
|
370
|
+
} else {
|
|
371
|
+
changed += 1;
|
|
372
|
+
const verb = dryRun ? 'WOULD FIX' : 'FIXED';
|
|
373
|
+
console.log(`${verb} ${rel}`);
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
console.log();
|
|
378
|
+
console.log(
|
|
379
|
+
`${results.length} file(s) scanned: ${changed} ${dryRun ? 'needing fix' : 'fixed'}, ${alreadyClean} already clean${skipped > 0 ? `, ${skipped} skipped (no ops.json)` : ''}.`,
|
|
380
|
+
);
|
|
381
|
+
|
|
382
|
+
if (dryRun && changed > 0) process.exit(1);
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
---
|
|
2
|
+
from: "0.17"
|
|
3
|
+
to: "8.0.0-rc.1"
|
|
4
|
+
changes:
|
|
5
|
+
- id: codec-conformance-harness-moves-to-testkit-packages
|
|
6
|
+
summary: |
|
|
7
|
+
The database-backed codec conformance harness moves into two dedicated dev-only packages,
|
|
8
|
+
`@internal/postgres-codec-testkit` and `@internal/sqlite-codec-testkit`. Like every
|
|
9
|
+
`@internal/*` package they are workspace-private — a published home under the `@prisma/*`
|
|
10
|
+
scope is a follow-up — so today's consumers are packs developed in this repository. A pack
|
|
11
|
+
that reached the in-repo harness through a relative cross-package import
|
|
12
|
+
(`../../../3-targets/6-adapters/postgres/test/codec-conformance/harness`) adds the matching
|
|
13
|
+
testkit as a workspace devDependency and imports `runPostgresCodecProjection` /
|
|
14
|
+
`runSqliteCodecProjection` from the package instead; a `tsconfig` whose `rootDir` was widened
|
|
15
|
+
to reach across that boundary can be narrowed back to the pack. The harness API is unchanged —
|
|
16
|
+
caller-supplied connection, one case per codec and value — so the cases themselves move
|
|
17
|
+
verbatim. Production adapters take no dependency on either testkit.
|
|
18
|
+
detection:
|
|
19
|
+
glob: "**/*.{ts,tsx,json}"
|
|
20
|
+
contains:
|
|
21
|
+
- "codec-conformance"
|
|
22
|
+
- "runPostgresCodecProjection"
|
|
23
|
+
- "runSqliteCodecProjection"
|
|
24
|
+
anyMatch: true
|
|
25
|
+
- id: aggregate-result-codecs-are-target-declared
|
|
26
|
+
summary: |
|
|
27
|
+
Targets declare what each aggregate returns, through `SqlAggregateDescriptor` contributions on
|
|
28
|
+
`types.aggregateDescriptors`. A descriptor maps an operation and an input match —
|
|
29
|
+
no input, an exact codec id, a codec trait, or input-agnostic — to a result codec and a
|
|
30
|
+
declared nullability, and resolution consults exact matches, then traits, then the
|
|
31
|
+
input-agnostic entry. Emission and the runtime read the same contributions, so an extension
|
|
32
|
+
that contributes an aggregate cannot type one result and decode another. Two consequences for
|
|
33
|
+
existing packs. First, aggregate results now carry their declared codec into decoding, so a
|
|
34
|
+
pack asserting an aggregate reads back as a `number` updates that expectation — on PostgreSQL
|
|
35
|
+
`count` and widened sums are bigints and integer averages are decimal strings; on SQLite `avg`
|
|
36
|
+
is a number and integer sums are bigints. Second, a pack that asserts rendered SQL for SQLite
|
|
37
|
+
aggregates sees `CAST(… AS text)` around any aggregate whose result is `sqlite/bigint@1`:
|
|
38
|
+
SQLite computes those into an INTEGER that `node:sqlite` refuses to hand over past 2^53, so
|
|
39
|
+
the target's descriptor renders the cast that keeps the value readable. Regenerate contracts —
|
|
40
|
+
the emitted `contract.d.ts` gains an `AggregateTypes` block that the ORM and SQL builder
|
|
41
|
+
resolve their result types from.
|
|
42
|
+
detection:
|
|
43
|
+
glob: "**/*.{ts,tsx}"
|
|
44
|
+
contains:
|
|
45
|
+
- "aggregateDescriptors"
|
|
46
|
+
- "AggregateExpr"
|
|
47
|
+
- "codecTypes"
|
|
48
|
+
anyMatch: true
|
|
49
|
+
- id: rendered-ts-literals-are-double-quoted
|
|
50
|
+
summary: |
|
|
51
|
+
`renderTsLiteral` (`@internal/framework-components/codec`) now returns a double-quoted
|
|
52
|
+
TypeScript literal — `"low"` where it used to return `'low'`. It delegates to a single
|
|
53
|
+
shared renderer (`JSON.stringify` plus an explicit U+2028/U+2029 escape), which also
|
|
54
|
+
closes escaping gaps the old implementation had: `\t`, `\v`, `\b`, `\f` and the remaining
|
|
55
|
+
C0 control characters were previously emitted raw. Calling code needs no change, and the
|
|
56
|
+
emitted `contract.d.ts` is byte-identical either way because `contract emit` formats with
|
|
57
|
+
prettier at `singleQuote: true`. What does change is any assertion your pack makes on the
|
|
58
|
+
*unformatted* return value — a codec unit test pinning `renderValueLiteral` output, or a
|
|
59
|
+
test that calls `generateContractDts` directly and greps the result. Update those
|
|
60
|
+
expectations to the double-quoted form. If your pack hand-rolls a `renderValueLiteral`
|
|
61
|
+
that builds its own quoted literal, it keeps working, but switch it to `renderTsLiteral`
|
|
62
|
+
so your pack's escaping matches the framework's.
|
|
63
|
+
detection:
|
|
64
|
+
glob: "**/*.{test,test-d}.ts"
|
|
65
|
+
contains:
|
|
66
|
+
- 'renderValueLiteral'
|
|
67
|
+
- 'generateContractDts'
|
|
68
|
+
anyMatch: true
|
|
69
|
+
- id: driver-spi-splits-query-and-execute
|
|
70
|
+
summary: |
|
|
71
|
+
The relational driver contract (`SqlQueryable` in `@internal/sql-relational-core`) splits
|
|
72
|
+
into one streaming path and one statistics path: `query<Row>(request)` returns
|
|
73
|
+
`AsyncIterable<Row>`, and `execute(request)` runs a statement without row output and
|
|
74
|
+
resolves to `SqlStatementStats` (`{ affectedRows }`). The prepared-specific
|
|
75
|
+
`executePrepared` driver method and the buffered `query(sql, params)` convenience (with its
|
|
76
|
+
`SqlQueryResult` type) are gone — preparedness travels as the optional
|
|
77
|
+
`preparedStatementHandle` on `SqlExecuteRequest`, and `query()` serves ad-hoc and prepared
|
|
78
|
+
requests alike. A pack that implements a driver, wraps one, or ships a driver fake for
|
|
79
|
+
tests implements the two-method surface, reports its engine's native affected-row count
|
|
80
|
+
unnormalized, and surfaces a failed prepared-statement retry as the structural
|
|
81
|
+
`DRIVER.PREPARE_FAILED` error envelope.
|
|
82
|
+
detection:
|
|
83
|
+
glob: "**/*.{ts,tsx}"
|
|
84
|
+
contains:
|
|
85
|
+
- "SqlQueryable"
|
|
86
|
+
- "PreparedExecuteRequest"
|
|
87
|
+
- "SqlQueryResult"
|
|
88
|
+
anyMatch: true
|
|
89
|
+
---
|
|
90
|
+
|
|
91
|
+
# 0.17 → 8.0.0-rc.1 — Extension-author upgrade instructions
|
|
92
|
+
|
|
93
|
+
## `codec-conformance-harness-moves-to-testkit-packages`
|
|
94
|
+
|
|
95
|
+
The conformance harness that measures a codec's JSON projection against a real database lives in dedicated packages, `@internal/postgres-codec-testkit` and `@internal/sqlite-codec-testkit`. They are workspace-private like every `@internal/*` package (a published `@prisma/*` home is a follow-up), so packs developed in this repository that reached the harness by relative path across the adapter's `test/` directory declare the workspace dependency instead:
|
|
96
|
+
|
|
97
|
+
```jsonc
|
|
98
|
+
// package.json
|
|
99
|
+
"devDependencies": {
|
|
100
|
+
"@internal/postgres-codec-testkit": "workspace:*"
|
|
101
|
+
}
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
```ts
|
|
105
|
+
import { runPostgresCodecProjection } from '@internal/postgres-codec-testkit';
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
The harness API is the same one the in-repo version had — you supply the connection and the cases; it encodes, stores, projects through your descriptor, executes, and compares the database's JSON against `encodeJson` and back through `decodeJson`. Cases move verbatim. If your `tsconfig.json` widened `rootDir` to make the cross-package relative import resolve, narrow it back to the pack.
|
|
109
|
+
|
|
110
|
+
## `aggregate-result-codecs-are-target-declared`
|
|
111
|
+
|
|
112
|
+
Aggregate result typing is now declared rather than inferred. A target (or an extension) contributes `SqlAggregateDescriptor`s on `types.aggregateDescriptors`, a sibling of `codecTypes`:
|
|
113
|
+
|
|
114
|
+
```ts
|
|
115
|
+
import type { SqlAggregateDescriptor } from '@internal/sql-relational-core/aggregate-descriptor-registry';
|
|
116
|
+
|
|
117
|
+
const sumOverMoney: SqlAggregateDescriptor = {
|
|
118
|
+
operation: 'sum',
|
|
119
|
+
input: { kind: 'codec', codecId: 'ext/money@1' },
|
|
120
|
+
output: { kind: 'codec', codecId: 'ext/money@1' },
|
|
121
|
+
nullable: true,
|
|
122
|
+
};
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
Each descriptor claims one `(operation, input)` pair, and exactly one component may claim it. The input match is one of four kinds — `none` (an operation over rows), `codec` (an exact codec id), `trait` (any codec carrying that trait), or `any` (a result that does not depend on its input) — and resolution consults them in that order of specificity: exact, then trait, then input-agnostic. The result is either `self` (the matched input's codec, for an aggregate that returns one of its inputs) or a named codec id. Emission reads the same contributions the runtime resolves against, so an emitted result type and a decoded value cannot disagree.
|
|
126
|
+
|
|
127
|
+
Two things to check in an existing pack:
|
|
128
|
+
|
|
129
|
+
- **Expectations that an aggregate reads back as a `number`.** On PostgreSQL, `count` and sums that widen to `int8` are bigints, and integer averages are decimal strings; on SQLite, integer sums are bigints while `avg` is a number.
|
|
130
|
+
- **Assertions over rendered SQL for SQLite aggregates.** An aggregate whose declared result is `sqlite/bigint@1` renders as `CAST(… AS text)`. SQLite computes such an aggregate into an INTEGER, and `node:sqlite` raises rather than returning one past 2^53; the descriptor's lowering hook renders the cast that keeps the value readable, and the bigint codec reads the text back. If your pack contributes an aggregate whose result outruns a double, declare the same lowering.
|
|
131
|
+
|
|
132
|
+
Then regenerate contracts (`prisma-next contract emit`): `contract.d.ts` gains an `AggregateTypes` block, and both the ORM client and the SQL builder resolve aggregate result types from it.
|
|
133
|
+
|
|
134
|
+
## `rendered-ts-literals-are-double-quoted`
|
|
135
|
+
|
|
136
|
+
The framework had three separate implementations of "escape this string for a TypeScript
|
|
137
|
+
literal", each with a different idea of what needed escaping. They are now one function, and the
|
|
138
|
+
one that survived is the `JSON.stringify`-based renderer.
|
|
139
|
+
|
|
140
|
+
For an extension pack, the practical surface is `renderTsLiteral`, which most custom codecs use
|
|
141
|
+
to implement `renderValueLiteral`:
|
|
142
|
+
|
|
143
|
+
```ts
|
|
144
|
+
// Before — renderTsLiteral returned a single-quoted literal
|
|
145
|
+
expect(codec.renderValueLiteral?.('low', 'output')).toBe("'low'");
|
|
146
|
+
|
|
147
|
+
// After
|
|
148
|
+
expect(codec.renderValueLiteral?.('low', 'output')).toBe('"low"');
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
Two things worth knowing when you update these:
|
|
152
|
+
|
|
153
|
+
- **The escaping inverted, it did not merely re-quote.** Under `JSON.stringify` a single quote is
|
|
154
|
+
no longer escaped and a double quote is. So `renderTsLiteral("it's")` is `"it's"`, not
|
|
155
|
+
`'it\'s'`. A mechanical quote swap over your test expectations will get the simple cases right
|
|
156
|
+
and the escaping cases wrong — check any assertion whose input contains a quote by hand.
|
|
157
|
+
|
|
158
|
+
- **Values are unaffected; only the rendering is.** No contract hash changes, no re-emit is
|
|
159
|
+
needed, and the artefacts your pack ships (`contract.json`, `contract.d.ts`, migrations) are
|
|
160
|
+
unchanged. If your pack's committed contract artefacts do show a diff after upgrading, that is
|
|
161
|
+
a different change in this transition, not this one.
|
|
162
|
+
|
|
163
|
+
## `driver-spi-splits-query-and-execute`
|
|
164
|
+
|
|
165
|
+
The driver contract used one streaming method for every statement, a prepared-specific variant beside it, and a buffered convenience query. It is now two methods with distinct jobs:
|
|
166
|
+
|
|
167
|
+
```ts
|
|
168
|
+
// Before (0.17)
|
|
169
|
+
export interface SqlQueryable {
|
|
170
|
+
execute<Row>(request: SqlExecuteRequest): AsyncIterable<Row>;
|
|
171
|
+
executePrepared<Row>(request: PreparedExecuteRequest): AsyncIterable<Row>;
|
|
172
|
+
explain?(request: SqlExecuteRequest): Promise<SqlExplainResult>;
|
|
173
|
+
query<Row>(sql: string, params?: readonly unknown[]): Promise<SqlQueryResult<Row>>;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// After (0.18)
|
|
177
|
+
export interface SqlQueryable {
|
|
178
|
+
query<Row>(request: SqlExecuteRequest): AsyncIterable<Row>;
|
|
179
|
+
execute(request: SqlExecuteRequest): Promise<SqlStatementStats>; // { affectedRows }
|
|
180
|
+
explain?(request: SqlExecuteRequest): Promise<SqlExplainResult>;
|
|
181
|
+
}
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
For a pack that implements a driver, wraps one, or ships a driver fake:
|
|
185
|
+
|
|
186
|
+
- Move the streaming implementation from `execute(request)` to `query(request)`. The buffered `query(sql, params)` overload and its `SqlQueryResult` type are gone; callers stream and collect instead.
|
|
187
|
+
- Implement `execute(request): Promise<SqlStatementStats>` for statements executed without row output. Report your engine's native affected-row count — the in-tree PostgreSQL driver reports `rowCount`, SQLite reports `stmt.run().changes` — and do not normalize semantics across engines.
|
|
188
|
+
- Delete `executePrepared`. Preparedness is a property of the request: a prepared plan arrives at `query()` carrying `preparedStatementHandle` on the `SqlExecuteRequest`.
|
|
189
|
+
- If your engine cannot return rows from `execute()`, reject `RETURNING`-style statements up front rather than silently dropping their rows (the in-tree SQLite driver does this).
|
|
190
|
+
- If your driver retries stale prepared statements, surface a failed retry as the structural `DRIVER.PREPARE_FAILED` error envelope (ADR 239) with the normalized driver error as its `cause`.
|
|
191
|
+
|
|
192
|
+
<!--
|
|
193
|
+
TML-3171 (nested SQL ORM self-relation predicate scopes): `changes: []`. The `packages/3-extensions/sql-orm-client` diff fixes internal query planning and adds regression coverage; it changes no public API, contract shape, emitted artefact, extension-authoring surface, adapter API, or downstream source translation. Incidental substrate diff only.
|
|
194
|
+
-->
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
---
|
|
2
|
+
from: "0.8"
|
|
3
|
+
to: "0.9"
|
|
4
|
+
changes:
|
|
5
|
+
- id: strip-inline-contracts-from-migration-manifests
|
|
6
|
+
summary: Remove the inlined `fromContract` / `toContract` fields from every committed `migration.json` (extension seed migrations included); the destination contract continues to live next door as `end-contract.json`.
|
|
7
|
+
detection:
|
|
8
|
+
glob: "**/migrations/**/migration.json"
|
|
9
|
+
contains:
|
|
10
|
+
- '"fromContract"'
|
|
11
|
+
- '"toContract"'
|
|
12
|
+
anyMatch: true
|
|
13
|
+
script: ./strip-inline-contracts.ts
|
|
14
|
+
- id: drop-migration-metadata-contract-fields-from-source
|
|
15
|
+
summary: Remove `fromContract` / `toContract` from any extension source code that constructs or destructures `MigrationMetadata`; the SPI dropped both fields.
|
|
16
|
+
detection:
|
|
17
|
+
glob: "**/*.{ts,tsx}"
|
|
18
|
+
contains:
|
|
19
|
+
- "fromContract"
|
|
20
|
+
- "toContract"
|
|
21
|
+
anyMatch: true
|
|
22
|
+
---
|
|
23
|
+
|
|
24
|
+
# 0.8 → 0.9 — Extension-author upgrade instructions
|
|
25
|
+
|
|
26
|
+
## `strip-inline-contracts-from-migration-manifests`
|
|
27
|
+
|
|
28
|
+
Starting at the 0.9 release, `migration.json` no longer carries `fromContract` or `toContract`. The schema rejects those keys as unknown, so any committed manifest that still inlines them fails to load with `MIGRATION.INVALID_MANIFEST`.
|
|
29
|
+
|
|
30
|
+
This applies to **seed migrations** shipped inside an extension package (e.g. `migrations/<edge-id>/migration.json` shipped under `packages/<extension>/migrations/`) just as it does to user-app migrations. The destination contract was already being written to disk next door as `end-contract.json` (and the source as `start-contract.json`); the manifest copy was redundant. `migrationHash` is unaffected — it has always been computed without those two fields, so stripping them does not change the stored hash.
|
|
31
|
+
|
|
32
|
+
### What `strip-inline-contracts.ts` does
|
|
33
|
+
|
|
34
|
+
The colocated script walks the project root, descends into every directory named `migrations/` (skipping `node_modules`, `.git`, `dist`, `build`), and rewrites every `migration.json` whose JSON object contains either key:
|
|
35
|
+
|
|
36
|
+
- Manifests that already lack both keys are left untouched (idempotent — safe to re-run).
|
|
37
|
+
- Manifests with either key are rewritten with the two key/value spans excised at the text level. The formatting of every surviving field (whitespace, inline-vs-multiline arrays, key ordering, trailing newline) is preserved byte-for-byte; only the removed keys (and their trailing comma+newline) disappear from the diff. The script reparses the result to guard against accidental corruption.
|
|
38
|
+
- A `--check` flag turns the script into a dry-run; it lists which manifests would be modified and exits non-zero if any still need fixing.
|
|
39
|
+
|
|
40
|
+
## `drop-migration-metadata-contract-fields-from-source`
|
|
41
|
+
|
|
42
|
+
The `MigrationMetadata` type exported by `@internal/migration-tools` no longer declares `fromContract` or `toContract`. Source TypeScript that constructs (or destructures) `MigrationMetadata` from those fields will now fail to compile.
|
|
43
|
+
|
|
44
|
+
There is no codemod — extension authors construct `MigrationMetadata` in too many shapes for a deterministic transform to be safe. Instead, walk every `.ts` / `.tsx` file matched by the `detection.glob` above and apply these rules locally:
|
|
45
|
+
|
|
46
|
+
- **Object-literal construction** (e.g. `const meta: MigrationMetadata = { ..., fromContract, toContract }`): drop both properties from the literal. If the value is later needed, read the contract from the sibling `end-contract.json` (or the predecessor's `end-contract.json` for the from-side) instead of carrying it in metadata.
|
|
47
|
+
- **Spread-into-existing** (e.g. `const meta = { ...prev, fromContract: ..., toContract: ... }`): drop the two keys from the spread. If `prev` was loaded from disk via `readMigrationPackage`, it already lacks the fields under 0.9 — no further work needed.
|
|
48
|
+
- **Destructuring** (e.g. `const { fromContract, toContract, ...rest } = meta`): remove both names from the destructure. If the consuming code used those values, switch the read to the sibling `end-contract.json`.
|
|
49
|
+
- **Type-only references** (e.g. `metadata.toContract`, `Pick<MigrationMetadata, 'fromContract'>`, etc.): TypeScript will surface these as compile errors after the bump. Replace with sibling-file reads or remove the field reference entirely.
|
|
50
|
+
|
|
51
|
+
If your extension also carries seed-migration manifests (the common case for extensions that ship a `migrations/` directory), the `strip-inline-contracts-from-migration-manifests` change above handles those at the JSON layer. Run that script first; the source-code change above only covers TypeScript that produces / consumes `MigrationMetadata` programmatically.
|
|
52
|
+
|
|
53
|
+
While at it, scan any seed `migration.ts` doc-comments in your extension for stale references to `metadata.toContract` (e.g. *"preserving the full `toContract` so `MigrationCLI.run` re-attests it"*). Those references were accurate under 0.8 and are no longer accurate under 0.9 — the `MigrationCLI.run` re-attestation now reads the destination contract from sibling `end-contract.json`, not from `metadata.toContract`. Update or remove the stale prose. This is documentation hygiene, not a structural break.
|
|
54
|
+
|
|
55
|
+
### Validation
|
|
56
|
+
|
|
57
|
+
After running the script and applying the source-level rules above, run `pnpm typecheck && pnpm test` (or your extension's own equivalent). `prisma-8-check-pins` should also pass, since it does not look at `MigrationMetadata` shape.
|