@prisma/orm-mongo 8.0.0-rc.5 → 8.0.0-rc.5-dev.1
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
|
@@ -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,72 @@
|
|
|
1
|
+
---
|
|
2
|
+
from: "0.17"
|
|
3
|
+
to: "8.0.0-rc.1"
|
|
4
|
+
changes:
|
|
5
|
+
- id: aggregate-results-carry-their-target-s-codec
|
|
6
|
+
summary: |
|
|
7
|
+
Aggregates read back through the codec their target declares for the result, so their
|
|
8
|
+
application types change. `count()` is a `bigint` on both targets — a count is a cardinality
|
|
9
|
+
and is not capped at 2^53. On PostgreSQL, `sum` over `int2`/`int4` widens to `int8` and reads
|
|
10
|
+
as a `bigint`, while `sum(int8)` and `avg` over any integer are `numeric` and read as decimal
|
|
11
|
+
**strings**; `min`/`max` keep the column's own type, except over `varchar`, where PostgreSQL
|
|
12
|
+
returns `text`. On SQLite, `sum` over an integer column is a `bigint` and `avg` is always a
|
|
13
|
+
`number` (real). The two targets genuinely diverge on `avg` — a portable query must handle
|
|
14
|
+
both, and there is no shim that flattens them. Include reducers
|
|
15
|
+
(`.include('posts', (p) => p.count())`) decode the same way, so an included count is a
|
|
16
|
+
`bigint` too, and an empty relation reads `0n` rather than `0`. Comparisons are where this
|
|
17
|
+
bites quietly: `count === 2` is false where `count` is `2n`, and `JSON.stringify` throws on a
|
|
18
|
+
bigint. Sweep for `toBe(<number>)` / `=== <number>` / arithmetic against an aggregate result
|
|
19
|
+
and switch to `2n` or the decimal string, and render bigints explicitly (`String(value)`)
|
|
20
|
+
wherever you serialise. `having(...)` operands are the exception and stay numbers — they are
|
|
21
|
+
compared inside SQL and never cross a codec. Regenerate your contracts
|
|
22
|
+
(`prisma-next contract emit`): the emitted `contract.d.ts` gains an `AggregateTypes` block
|
|
23
|
+
that types every aggregate per operation and input codec, and the ORM and SQL builder both
|
|
24
|
+
resolve their result types from it — against a contract emitted before 8.0.0-rc.1 an aggregate
|
|
25
|
+
resolves to `never` in the ORM and to `unknown` in the SQL builder.
|
|
26
|
+
detection:
|
|
27
|
+
glob: "**/*.{ts,tsx,mts,cts}"
|
|
28
|
+
contains:
|
|
29
|
+
- "aggregate("
|
|
30
|
+
- ".count()"
|
|
31
|
+
- "groupBy("
|
|
32
|
+
anyMatch: true
|
|
33
|
+
---
|
|
34
|
+
|
|
35
|
+
# 0.17 → 8.0.0-rc.1 — User upgrade instructions
|
|
36
|
+
|
|
37
|
+
## `aggregate-results-carry-their-target-s-codec`
|
|
38
|
+
|
|
39
|
+
An aggregate's result is a value the database computes, and 8.0.0-rc.1 reads it back through the codec its target declares for that result rather than through whatever the driver happened to hand over. What each aggregate returns is now the target's answer, stated in the contract and honoured by the runtime:
|
|
40
|
+
|
|
41
|
+
| Target | Aggregate | Reads as |
|
|
42
|
+
| --- | --- | --- |
|
|
43
|
+
| PostgreSQL | `count()` (with or without an argument) | `bigint` |
|
|
44
|
+
| PostgreSQL | `sum` over `int2` / `int4` | `bigint` (the sum widens to `int8`) |
|
|
45
|
+
| PostgreSQL | `sum` over `int8`, `avg` over any integer | decimal `string` (the result is `numeric`) |
|
|
46
|
+
| PostgreSQL | `sum` / `avg` over `float4` / `float8` | `number` |
|
|
47
|
+
| PostgreSQL | `min` / `max` | the column's own type — except over `varchar`, which returns `text` |
|
|
48
|
+
| SQLite | `count()` | `bigint` |
|
|
49
|
+
| SQLite | `sum` over an integer column | `bigint` |
|
|
50
|
+
| SQLite | `avg` over anything | `number` (SQLite's `avg` is always real) |
|
|
51
|
+
| SQLite | `min` / `max` | the column's own type |
|
|
52
|
+
|
|
53
|
+
The targets diverge on `avg`, and deliberately: PostgreSQL computes it as `numeric`, SQLite as a float. A query written against both handles both.
|
|
54
|
+
|
|
55
|
+
Include reducers follow the same rules — `.include('posts', (posts) => posts.count())` yields a `bigint`, and a parent with no related rows reads `0n` where it read `0`.
|
|
56
|
+
|
|
57
|
+
Two failure modes are worth sweeping for, because neither announces itself:
|
|
58
|
+
|
|
59
|
+
- **Equality against a number.** `row.count === 2` is `false` when `row.count` is `2n`, and `expect(count).toBe(2)` fails the same way. Change the literal (`2n`), or compare through `Number(...)` where the magnitude is known to be small.
|
|
60
|
+
- **Serialisation.** `JSON.stringify` throws `TypeError: Do not know how to serialize a BigInt`. Render explicitly — `String(count)`, or a replacer that maps bigints to strings.
|
|
61
|
+
|
|
62
|
+
Arithmetic mixing a bigint with a number also throws (`2n + 1` is a `TypeError`); convert one side deliberately.
|
|
63
|
+
|
|
64
|
+
`having(...)` is the one place that does not move. A HAVING operand is compared inside SQL against the aggregate the database is computing, so it never crosses a codec: `having.count().gte(2)` keeps the plain number it always took. Only the aggregate's *result* — the value that reaches your code — carries its target's type.
|
|
65
|
+
|
|
66
|
+
Finally, regenerate your contracts:
|
|
67
|
+
|
|
68
|
+
```bash
|
|
69
|
+
prisma-next contract emit
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
The emitted `contract.d.ts` gains an `AggregateTypes` block — the settled result identity per operation and per input codec — and both the ORM client and the SQL builder resolve their aggregate result types from it. Against a contract emitted before 8.0.0-rc.1 the block is absent, so an aggregate resolves to `never` in the ORM and to `unknown` in the SQL builder: a type error at the call site in the first case, an untyped value in the second, rather than a wrong runtime value in either.
|
|
@@ -0,0 +1,36 @@
|
|
|
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`; 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
|
+
---
|
|
15
|
+
|
|
16
|
+
# 0.8 → 0.9 — User upgrade instructions
|
|
17
|
+
|
|
18
|
+
## `strip-inline-contracts-from-migration-manifests`
|
|
19
|
+
|
|
20
|
+
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 a `MIGRATION.INVALID_MANIFEST` error from the loader (which is what powers `prisma-next migration plan` / `apply` / `verify`).
|
|
21
|
+
|
|
22
|
+
The destination contract was already being written to disk next door as `end-contract.json` (and the source as `start-contract.json`); the in-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 and existing `from` / `to` storage-hash bookends remain valid.
|
|
23
|
+
|
|
24
|
+
### What `strip-inline-contracts.ts` does
|
|
25
|
+
|
|
26
|
+
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:
|
|
27
|
+
|
|
28
|
+
- Manifests that already lack both keys are left untouched (idempotent — safe to re-run).
|
|
29
|
+
- 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.
|
|
30
|
+
- 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.
|
|
31
|
+
|
|
32
|
+
### Validation
|
|
33
|
+
|
|
34
|
+
After running the script, run `pnpm typecheck && pnpm test` per the per-step flow. The migration loader's schema check is the structural validation; the project's own test suite covers any consumer-side code that previously read `metadata.fromContract` / `metadata.toContract` (rare — the fields were unused in the apply / verify path).
|
|
35
|
+
|
|
36
|
+
If you have application code that inspected `metadata.toContract` for any reason, read the contract from the sibling `end-contract.json` file instead (and `metadata.fromContract` becomes the predecessor migration's `end-contract.json`).
|