@prisma/orm-mongo 8.0.0-rc.18-dev.47 → 8.0.0-rc.19-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/dist/{mongo-static-C2JEnq37-on92QlFC.mjs → mongo-static-CIWWrpGo-NdHnpaog.mjs} +3 -3
- package/dist/mongo-static-CIWWrpGo-NdHnpaog.mjs.map +1 -0
- package/dist/runtime.mjs +1 -1
- package/dist/runtime.mjs.map +1 -1
- package/dist/static.mjs +1 -1
- package/package.json +8 -8
- package/skills/prisma-8/SKILL.md +1 -1
- package/skills/prisma-8/upgrading/app/upgrades/8.0.0-rc.18-to-8.0.0-rc.19/instructions.md +1826 -0
- package/skills/prisma-8/upgrading/app/upgrades/8.0.0-rc.18-to-8.0.0-rc.19/scripts/client-takes-contract-value/contract-from-json.ts +382 -0
- package/skills/prisma-8/upgrading/app/upgrades/8.0.0-rc.18-to-8.0.0-rc.19/scripts/data-type-values/data-type-values.ts +1117 -0
- package/skills/prisma-8/upgrading/app/upgrades/8.0.0-rc.18-to-8.0.0-rc.19/scripts/default-generators-to-on-create/rewrite-default-generators.mjs +86 -0
- package/skills/prisma-8/upgrading/app/upgrades/8.0.0-rc.18-to-8.0.0-rc.19/scripts/mongo-field-storage-names/mongo-field-storage-names.ts +250 -0
- package/skills/prisma-8/upgrading/app/upgrades/8.0.0-rc.18-to-8.0.0-rc.19/scripts/remove-nocheck/remove-nocheck.ts +813 -0
- package/skills/prisma-8/upgrading/extension/upgrades/8.0.0-rc.18-to-8.0.0-rc.19/instructions.md +2000 -0
- package/skills/prisma-8/upgrading/extension/upgrades/8.0.0-rc.18-to-8.0.0-rc.19/scripts/client-takes-contract-value/contract-from-json.ts +382 -0
- package/skills/prisma-8/upgrading/extension/upgrades/8.0.0-rc.18-to-8.0.0-rc.19/scripts/remove-nocheck/remove-nocheck.ts +813 -0
- package/dist/mongo-static-C2JEnq37-on92QlFC.mjs.map +0 -1
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Rewrites `@default(uuid(...))`, `@default(cuid(...))`, `@default(ulid())` and `@default(nanoid(...))`
|
|
3
|
+
// to `@onCreate(...)` in the Prisma 8 schema files named on the command line. The call is kept as
|
|
4
|
+
// written; string literals and `//` comments are left unchanged. A file without the `// use prisma-8` directive (or the
|
|
5
|
+
// older `// use prisma-next`) is left unchanged.
|
|
6
|
+
import { readFileSync, writeFileSync } from 'node:fs';
|
|
7
|
+
import process from 'node:process';
|
|
8
|
+
|
|
9
|
+
const CLIENT_FUNCTION_IN_DEFAULT = /@default\((\s*(?:uuid|cuid|ulid|nanoid)\s*\([^()]*\)\s*)\)/g;
|
|
10
|
+
// The directive rule contract emit uses, copied from isPrismaNextSchema in
|
|
11
|
+
// packages/1-framework/2-authoring/psl-parser/src/schema-directive.ts; this script ships standalone.
|
|
12
|
+
const PRISMA8_DIRECTIVE = /^(\s*\/\/ *use +)(prisma-8|prisma-next)( *)(?!\S)/;
|
|
13
|
+
|
|
14
|
+
/** The line cut into code, string literals and a trailing `//` comment, with only the code to rewrite. */
|
|
15
|
+
function spansOf(line) {
|
|
16
|
+
const spans = [];
|
|
17
|
+
let codeStart = 0;
|
|
18
|
+
let index = 0;
|
|
19
|
+
while (index < line.length) {
|
|
20
|
+
if (line[index] === '/' && line[index + 1] === '/') break;
|
|
21
|
+
if (line[index] !== '"') {
|
|
22
|
+
index += 1;
|
|
23
|
+
continue;
|
|
24
|
+
}
|
|
25
|
+
spans.push({ text: line.slice(codeStart, index), code: true });
|
|
26
|
+
let close = index + 1;
|
|
27
|
+
while (close < line.length && line[close] !== '"') close += line[close] === '\\' ? 2 : 1;
|
|
28
|
+
spans.push({ text: line.slice(index, close + 1), code: false });
|
|
29
|
+
index = close + 1;
|
|
30
|
+
codeStart = index;
|
|
31
|
+
}
|
|
32
|
+
spans.push({ text: line.slice(codeStart, index), code: true });
|
|
33
|
+
spans.push({ text: line.slice(index), code: false });
|
|
34
|
+
return spans;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function rewrite(text) {
|
|
38
|
+
let count = 0;
|
|
39
|
+
const lines = text.split('\n').map((line) =>
|
|
40
|
+
spansOf(line)
|
|
41
|
+
.map(({ text: span, code }) =>
|
|
42
|
+
code
|
|
43
|
+
? span.replace(CLIENT_FUNCTION_IN_DEFAULT, (_match, call) => {
|
|
44
|
+
count += 1;
|
|
45
|
+
return `@onCreate(${call})`;
|
|
46
|
+
})
|
|
47
|
+
: span,
|
|
48
|
+
)
|
|
49
|
+
.join(''),
|
|
50
|
+
);
|
|
51
|
+
return { text: lines.join('\n'), count };
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const files = process.argv.slice(2);
|
|
55
|
+
if (files.length === 0) {
|
|
56
|
+
process.stderr.write('Usage: node rewrite-default-generators.mjs <schema.prisma>...\n');
|
|
57
|
+
process.exit(1);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const texts = new Map();
|
|
61
|
+
for (const file of files) {
|
|
62
|
+
try {
|
|
63
|
+
texts.set(file, readFileSync(file, 'utf8'));
|
|
64
|
+
} catch {
|
|
65
|
+
process.stderr.write(`${file}: cannot be read; no file was changed.\n`);
|
|
66
|
+
process.exit(1);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
for (const [file, text] of texts) {
|
|
71
|
+
if (!PRISMA8_DIRECTIVE.test(text)) {
|
|
72
|
+
process.stdout.write(
|
|
73
|
+
`${file}: has no "// use prisma-8" directive, so it is not a Prisma 8 schema; left unchanged. A Prisma 7 schema keeps @default(uuid()).\n`,
|
|
74
|
+
);
|
|
75
|
+
continue;
|
|
76
|
+
}
|
|
77
|
+
const rewritten = rewrite(text);
|
|
78
|
+
if (rewritten.count === 0) {
|
|
79
|
+
process.stdout.write(`${file}: no client function in @default; left unchanged.\n`);
|
|
80
|
+
continue;
|
|
81
|
+
}
|
|
82
|
+
writeFileSync(file, rewritten.text);
|
|
83
|
+
process.stdout.write(
|
|
84
|
+
`${file}: rewrote ${rewritten.count} @default ${rewritten.count === 1 ? 'call' : 'calls'} to @onCreate.\n`,
|
|
85
|
+
);
|
|
86
|
+
}
|
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Adds `storage.fields` to every MongoDB contract written before models recorded the stored name of each field.
|
|
3
|
+
// Each field keeps its name and is recorded as stored under that name, which is what the old contract meant.
|
|
4
|
+
// Usage: node mongo-field-storage-names.ts [<project root>]
|
|
5
|
+
|
|
6
|
+
import { readdirSync, readFileSync, renameSync, rmSync, statSync, writeFileSync } from 'node:fs';
|
|
7
|
+
import { join, relative, resolve } from 'node:path';
|
|
8
|
+
|
|
9
|
+
const SKIPPED_DIRECTORIES = new Set(['node_modules', '.git', 'dist', 'build']);
|
|
10
|
+
const TEMPORARY_SUFFIX = '.mongo-field-storage-names-tmp';
|
|
11
|
+
|
|
12
|
+
type JsonObject = { [key: string]: unknown };
|
|
13
|
+
|
|
14
|
+
function isObject(value: unknown): value is JsonObject {
|
|
15
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function walk(dir: string, found: string[]): void {
|
|
19
|
+
for (const entry of readdirSync(dir)) {
|
|
20
|
+
const path = join(dir, entry);
|
|
21
|
+
const stats = statSync(path);
|
|
22
|
+
if (stats.isDirectory()) {
|
|
23
|
+
if (!SKIPPED_DIRECTORIES.has(entry)) walk(path, found);
|
|
24
|
+
continue;
|
|
25
|
+
}
|
|
26
|
+
if (entry.endsWith(TEMPORARY_SUFFIX)) {
|
|
27
|
+
rmSync(path);
|
|
28
|
+
continue;
|
|
29
|
+
}
|
|
30
|
+
if (entry.endsWith('.json')) found.push(path);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function modelsOf(contract: JsonObject): [string, JsonObject][] {
|
|
35
|
+
const domain = contract['domain'];
|
|
36
|
+
if (!isObject(domain) || !isObject(domain['namespaces'])) return [];
|
|
37
|
+
return Object.values(domain['namespaces']).flatMap((namespace) =>
|
|
38
|
+
isObject(namespace) && isObject(namespace['models'])
|
|
39
|
+
? Object.entries(namespace['models']).filter((entry): entry is [string, JsonObject] =>
|
|
40
|
+
isObject(entry[1]),
|
|
41
|
+
)
|
|
42
|
+
: [],
|
|
43
|
+
);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function needsStorageFields(model: JsonObject): boolean {
|
|
47
|
+
return isObject(model['fields']) && isObject(model['storage']) && !('fields' in model['storage']);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function withStorageFields(model: JsonObject): JsonObject {
|
|
51
|
+
const storage = model['storage'];
|
|
52
|
+
const fields = model['fields'];
|
|
53
|
+
if (!isObject(storage) || !isObject(fields)) return model;
|
|
54
|
+
const storageFields = Object.fromEntries(
|
|
55
|
+
Object.keys(fields).map((name) => [name, { field: name }]),
|
|
56
|
+
);
|
|
57
|
+
const rewritten: JsonObject = {};
|
|
58
|
+
if ('collection' in storage) rewritten['collection'] = storage['collection'];
|
|
59
|
+
rewritten['fields'] = storageFields;
|
|
60
|
+
for (const [key, value] of Object.entries(storage)) {
|
|
61
|
+
if (key !== 'collection') rewritten[key] = value;
|
|
62
|
+
}
|
|
63
|
+
return { ...model, storage: rewritten };
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function rewriteContract(contract: JsonObject, modelNames: ReadonlySet<string>): JsonObject {
|
|
67
|
+
const domain = contract['domain'];
|
|
68
|
+
if (!isObject(domain) || !isObject(domain['namespaces'])) return contract;
|
|
69
|
+
const namespaces = Object.fromEntries(
|
|
70
|
+
Object.entries(domain['namespaces']).map(([id, namespace]) => {
|
|
71
|
+
if (!isObject(namespace) || !isObject(namespace['models'])) return [id, namespace];
|
|
72
|
+
const models = Object.fromEntries(
|
|
73
|
+
Object.entries(namespace['models']).map(([name, model]) => [
|
|
74
|
+
name,
|
|
75
|
+
isObject(model) && modelNames.has(name) ? withStorageFields(model) : model,
|
|
76
|
+
]),
|
|
77
|
+
);
|
|
78
|
+
return [id, { ...namespace, models }];
|
|
79
|
+
}),
|
|
80
|
+
);
|
|
81
|
+
return { ...contract, domain: { ...domain, namespaces } };
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function serializeLike(original: string, value: JsonObject): string {
|
|
85
|
+
const trailingNewline = original.endsWith('\n') ? '\n' : '';
|
|
86
|
+
const singleLine = !original.trimEnd().includes('\n');
|
|
87
|
+
return `${singleLine ? JSON.stringify(value) : JSON.stringify(value, null, 2)}${trailingNewline}`;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function matchingBrace(text: string, open: number): number {
|
|
91
|
+
let depth = 0;
|
|
92
|
+
for (let index = open; index < text.length; index++) {
|
|
93
|
+
const char = text[index];
|
|
94
|
+
if (char === "'" || char === '"' || char === '`') {
|
|
95
|
+
index = text.indexOf(char, index + 1);
|
|
96
|
+
if (index === -1) return -1;
|
|
97
|
+
continue;
|
|
98
|
+
}
|
|
99
|
+
if (char === '{') depth++;
|
|
100
|
+
if (char === '}') {
|
|
101
|
+
depth--;
|
|
102
|
+
if (depth === 0) return index;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
return -1;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function typeKey(name: string, quote: string): string {
|
|
109
|
+
return /^[A-Za-z_$][\w$]*$/.test(name) ? name : `${quote}${name}${quote}`;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function storageFieldsType(fieldNames: readonly string[], quote: string): string {
|
|
113
|
+
if (fieldNames.length === 0) return 'readonly fields: Record<string, never>';
|
|
114
|
+
const entries = fieldNames.map(
|
|
115
|
+
(name) => `readonly ${typeKey(name, quote)}: { readonly field: ${quote}${name}${quote} }`,
|
|
116
|
+
);
|
|
117
|
+
return `readonly fields: { ${entries.join('; ')} }`;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const STORAGE_KEY = /readonly storage:\s*/g;
|
|
121
|
+
|
|
122
|
+
/** How many braces are open at `end` in `text`, outside string literals. */
|
|
123
|
+
function braceDepth(text: string, end: number): number {
|
|
124
|
+
let depth = 0;
|
|
125
|
+
for (let index = 0; index < end; index++) {
|
|
126
|
+
const char = text[index];
|
|
127
|
+
if (char === "'" || char === '"' || char === '`') {
|
|
128
|
+
const close = text.indexOf(char, index + 1);
|
|
129
|
+
if (close === -1 || close >= end) return depth;
|
|
130
|
+
index = close;
|
|
131
|
+
continue;
|
|
132
|
+
}
|
|
133
|
+
if (char === '{') depth++;
|
|
134
|
+
if (char === '}') depth--;
|
|
135
|
+
}
|
|
136
|
+
return depth;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/** The model's `readonly storage: ...` type with `readonly fields` added, or undefined when the model's block is not found. */
|
|
140
|
+
function rewriteModelType(
|
|
141
|
+
declarations: string,
|
|
142
|
+
modelName: string,
|
|
143
|
+
fieldNames: readonly string[],
|
|
144
|
+
): string | undefined {
|
|
145
|
+
const quote = declarations.includes("readonly collection: '") ? "'" : '"';
|
|
146
|
+
const header = new RegExp(`readonly (?:${modelName}|'${modelName}'|"${modelName}"):\\s*\\{`, 'g');
|
|
147
|
+
for (const match of declarations.matchAll(header)) {
|
|
148
|
+
const open = match.index + match[0].length - 1;
|
|
149
|
+
const close = matchingBrace(declarations, open);
|
|
150
|
+
if (close === -1) continue;
|
|
151
|
+
const block = declarations.slice(open, close + 1);
|
|
152
|
+
STORAGE_KEY.lastIndex = 0;
|
|
153
|
+
for (const storageMatch of block.matchAll(STORAGE_KEY)) {
|
|
154
|
+
if (braceDepth(block, storageMatch.index) !== 1) continue;
|
|
155
|
+
const typeStart = open + storageMatch.index + storageMatch[0].length;
|
|
156
|
+
const fields = storageFieldsType(fieldNames, quote);
|
|
157
|
+
if (declarations.startsWith('Record<string, never>', typeStart)) {
|
|
158
|
+
return `${declarations.slice(0, typeStart)}{ ${fields} }${declarations.slice(typeStart + 'Record<string, never>'.length)}`;
|
|
159
|
+
}
|
|
160
|
+
if (declarations[typeStart] !== '{') continue;
|
|
161
|
+
const typeEnd = matchingBrace(declarations, typeStart);
|
|
162
|
+
const inner = declarations
|
|
163
|
+
.slice(typeStart + 1, typeEnd)
|
|
164
|
+
.trim()
|
|
165
|
+
.replace(/;$/, '');
|
|
166
|
+
const members = inner.length === 0 ? [] : [inner];
|
|
167
|
+
const collectionEnd = inner.match(/^readonly collection:\s*(['"])[^'"]*\1/)?.[0];
|
|
168
|
+
const rewritten =
|
|
169
|
+
collectionEnd === undefined
|
|
170
|
+
? [fields, ...members]
|
|
171
|
+
: [collectionEnd, fields, inner.slice(collectionEnd.length).replace(/^;\s*/, '')].filter(
|
|
172
|
+
(member) => member.length > 0,
|
|
173
|
+
);
|
|
174
|
+
return `${declarations.slice(0, typeStart)}{ ${rewritten.join('; ')} }${declarations.slice(typeEnd + 1)}`;
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
return undefined;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function writeFile(path: string, content: string): void {
|
|
181
|
+
const temporary = `${path}${TEMPORARY_SUFFIX}`;
|
|
182
|
+
writeFileSync(temporary, content);
|
|
183
|
+
renameSync(temporary, path);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
function main(): number {
|
|
187
|
+
const root = resolve(process.argv[2] ?? '.');
|
|
188
|
+
const files: string[] = [];
|
|
189
|
+
walk(root, files);
|
|
190
|
+
const changed: string[] = [];
|
|
191
|
+
const warnings: string[] = [];
|
|
192
|
+
let mongoContracts = 0;
|
|
193
|
+
for (const path of files.sort()) {
|
|
194
|
+
let contract: unknown;
|
|
195
|
+
try {
|
|
196
|
+
contract = JSON.parse(readFileSync(path, 'utf8'));
|
|
197
|
+
} catch {
|
|
198
|
+
continue;
|
|
199
|
+
}
|
|
200
|
+
if (!isObject(contract) || contract['targetFamily'] !== 'mongo') continue;
|
|
201
|
+
mongoContracts++;
|
|
202
|
+
const oldModels = modelsOf(contract).filter(([, model]) => needsStorageFields(model));
|
|
203
|
+
if (oldModels.length === 0) continue;
|
|
204
|
+
const original = readFileSync(path, 'utf8');
|
|
205
|
+
const modelNames = new Set(oldModels.map(([name]) => name));
|
|
206
|
+
writeFile(path, serializeLike(original, rewriteContract(contract, modelNames)));
|
|
207
|
+
changed.push(relative(root, path));
|
|
208
|
+
|
|
209
|
+
const declarationsPath = `${path.slice(0, -'.json'.length)}.d.ts`;
|
|
210
|
+
let declarations: string;
|
|
211
|
+
try {
|
|
212
|
+
declarations = readFileSync(declarationsPath, 'utf8');
|
|
213
|
+
} catch {
|
|
214
|
+
continue;
|
|
215
|
+
}
|
|
216
|
+
let rewritten = declarations;
|
|
217
|
+
for (const [name, model] of oldModels) {
|
|
218
|
+
const fields = isObject(model['fields']) ? Object.keys(model['fields']) : [];
|
|
219
|
+
const next = rewriteModelType(rewritten, name, fields);
|
|
220
|
+
if (next === undefined) {
|
|
221
|
+
warnings.push(
|
|
222
|
+
`${relative(root, declarationsPath)}: no storage type found for model ${name}; add readonly fields to it by hand`,
|
|
223
|
+
);
|
|
224
|
+
continue;
|
|
225
|
+
}
|
|
226
|
+
rewritten = next;
|
|
227
|
+
}
|
|
228
|
+
if (rewritten !== declarations) {
|
|
229
|
+
writeFile(declarationsPath, rewritten);
|
|
230
|
+
changed.push(relative(root, declarationsPath));
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
for (const warning of warnings) process.stderr.write(`${warning}\n`);
|
|
235
|
+
if (mongoContracts === 0) {
|
|
236
|
+
process.stdout.write('No MongoDB contract found under the project root; nothing changed.\n');
|
|
237
|
+
return 0;
|
|
238
|
+
}
|
|
239
|
+
if (changed.length === 0) {
|
|
240
|
+
process.stdout.write(
|
|
241
|
+
'Every MongoDB contract already records its stored names; nothing changed.\n',
|
|
242
|
+
);
|
|
243
|
+
return 0;
|
|
244
|
+
}
|
|
245
|
+
for (const path of changed) process.stdout.write(`${path}\n`);
|
|
246
|
+
process.stdout.write(`Rewrote ${changed.length} file${changed.length === 1 ? '' : 's'}.\n`);
|
|
247
|
+
return warnings.length > 0 ? 1 : 0;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
process.exitCode = main();
|