@prisma/orm-postgres 8.0.0-rc.13-dev.3 → 8.0.0-rc.13-dev.4
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 +8 -8
- package/skills/prisma-8/SKILL.md +1 -1
- package/skills/prisma-8/upgrading/app/upgrades/8.0.0-rc.12-to-8.0.0-rc.13/instructions.md +10 -1
- package/skills/prisma-8/upgrading/app/upgrades/8.0.0-rc.12-to-8.0.0-rc.13/scripts/execution-ref-entry-field/rename-execution-ref-keys.ts +193 -0
- package/skills/prisma-8/upgrading/extension/upgrades/8.0.0-rc.12-to-8.0.0-rc.13/instructions.md +10 -1
- package/skills/prisma-8/upgrading/extension/upgrades/8.0.0-rc.12-to-8.0.0-rc.13/scripts/execution-ref-entry-field/rename-execution-ref-keys.ts +193 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@prisma/orm-postgres",
|
|
3
|
-
"version": "8.0.0-rc.13-dev.
|
|
3
|
+
"version": "8.0.0-rc.13-dev.4",
|
|
4
4
|
"license": "Apache-2.0",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"sideEffects": false,
|
|
@@ -10,10 +10,10 @@
|
|
|
10
10
|
"skills"
|
|
11
11
|
],
|
|
12
12
|
"dependencies": {
|
|
13
|
-
"@prisma/orm-family-sql": "8.0.0-rc.13-dev.
|
|
14
|
-
"@prisma/orm-framework": "8.0.0-rc.13-dev.
|
|
15
|
-
"@prisma/orm-target-postgres": "8.0.0-rc.13-dev.
|
|
16
|
-
"@prisma/orm-toolchain": "8.0.0-rc.13-dev.
|
|
13
|
+
"@prisma/orm-family-sql": "8.0.0-rc.13-dev.4",
|
|
14
|
+
"@prisma/orm-framework": "8.0.0-rc.13-dev.4",
|
|
15
|
+
"@prisma/orm-target-postgres": "8.0.0-rc.13-dev.4",
|
|
16
|
+
"@prisma/orm-toolchain": "8.0.0-rc.13-dev.4",
|
|
17
17
|
"@types/node": "26.1.2",
|
|
18
18
|
"@types/pg": "8.20.4",
|
|
19
19
|
"pathe": "^2.0.3",
|
|
@@ -21,9 +21,9 @@
|
|
|
21
21
|
"tinyglobby": "^0.2.17"
|
|
22
22
|
},
|
|
23
23
|
"devDependencies": {
|
|
24
|
-
"@internal/postgres": "8.0.0-rc.13-dev.
|
|
25
|
-
"@repo/tsconfig": "8.0.0-rc.13-dev.
|
|
26
|
-
"@repo/tsdown": "8.0.0-rc.13-dev.
|
|
24
|
+
"@internal/postgres": "8.0.0-rc.13-dev.4",
|
|
25
|
+
"@repo/tsconfig": "8.0.0-rc.13-dev.4",
|
|
26
|
+
"@repo/tsdown": "8.0.0-rc.13-dev.4",
|
|
27
27
|
"tsdown": "0.22.14",
|
|
28
28
|
"typescript": "5.9.3"
|
|
29
29
|
},
|
package/skills/prisma-8/SKILL.md
CHANGED
|
@@ -14,6 +14,7 @@ changes:
|
|
|
14
14
|
- '"ref"\s*:\s*\{(?![^{}]*"kind")[^{}]*"(?:table|column)"\s*:'
|
|
15
15
|
- '\.ref\??\.(?:table|column)\b'
|
|
16
16
|
- '\bref\s*:\s*\{(?![^{}]*\bkind\b)[^{}]*\b(?:table|column)\s*:[^{}]*\b(?:table|column)\s*:'
|
|
17
|
+
script: ./scripts/execution-ref-entry-field/rename-execution-ref-keys.ts
|
|
17
18
|
- id: ts-defaults-encoded-by-codec
|
|
18
19
|
summary: |
|
|
19
20
|
`defineContract` from the Postgres and SQLite packages now encodes every literal `.default(value)` through the column's codec. The literal is the codec's input type. TypeScript checks it for fields built inside the `defineContract` factory, and the build fails with `CONTRACT.DEFAULT_INVALID` for a value the codec refuses. Pass a value of the codec's input type, or choose the field preset whose codec takes the value you have.
|
|
@@ -120,7 +121,15 @@ A contract with generated defaults (`temporal.createdAt()`, `temporal.updatedAt(
|
|
|
120
121
|
```
|
|
121
122
|
|
|
122
123
|
1. Run `prisma contract emit` so `contract.json` and `contract.d.ts` use the new keys. The runtime rejects a contract whose refs still say `table` and `column` with `Contract structural validation failed: execution.mutations.defaults[0].ref.entry must be a string`.
|
|
123
|
-
2.
|
|
124
|
+
2. Contracts stored under `migrations/` carry the old keys too: the snapshots in `migrations/snapshots/<hash>/contract.json` and `contract.d.ts`, and any intermediate contract a migration imports from its own directory, such as `migrations/app/<dir>/intermediate.json` and `intermediate.d.ts`. From the project root, run the script that sits next to this guide. `<skill>` is the directory of the synced `prisma-8` skill:
|
|
125
|
+
|
|
126
|
+
```bash
|
|
127
|
+
pnpm exec tsx <skill>/upgrading/app/upgrades/8.0.0-rc.12-to-8.0.0-rc.13/scripts/execution-ref-entry-field/rename-execution-ref-keys.ts
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
The script reads every `.json` and `.d.ts` file under a `migrations/` directory, skipping `node_modules`, `.git` and `dist`, and changes only `execution.mutations.defaults[].ref` entries. In each one it renames `table` to `entry` and `column` to `field`, and writes the keys in the order `entry`, `field`, `namespace`, which is the order `prisma contract emit` writes. The old key order varies between files: older `contract.d.ts` files list `namespace`, `table`, `column`, newer ones `column`, `namespace`, `table`. The script handles any order; a hand edit or a search-and-replace that assumes one order misses some refs. A `contract.json` in canonical form stays canonical, so a snapshot matches a fresh emit apart from `executionHash`; an indented JSON file keeps its indentation. The script leaves every `executionHash` as it is. That hash no longer matches the renamed content, but the snapshot loader re-hashes only the storage section, so nothing checks it. `storageHash` and `profileHash` do not move, so snapshot directory names stay the same. A second run changes nothing. Pass `--check` to list the files it would change without writing them; it exits 1 if any would change. A `.json` file it cannot parse is listed as `NOT JSON` and also makes it exit 1.
|
|
131
|
+
|
|
132
|
+
To confirm the rewrite, name one migration directory under `migrations/app/` as both ends of a plan: `prisma migration plan --from <dir> --to <dir>`. It loads that migration's snapshot without touching the database. Before the rewrite it fails with `CONTRACT.VALIDATION_FAILED` and `execution.mutations.defaults[0].ref.entry must be a string`; after the rewrite it prints `No changes detected` and writes no migration. In a project with extensions it also re-pins their files under `migrations/<space-id>/`, as every `migration plan` does. `prisma db migrate --to <hash>` does not confirm it: it applies migrations, and on a database already past that snapshot it has no route back, so it fails either way. `prisma migration check` and `prisma db verify` never validate an app snapshot's `execution` section, so they pass before and after the rewrite.
|
|
124
133
|
3. Code that reads the section directly changes `.ref.table` to `.ref.entry` and `.ref.column` to `.ref.field`.
|
|
125
134
|
|
|
126
135
|
`executionHash` changes for every contract with generated defaults, because the canonical JSON changes. Nothing compares it against the database, so no migration or re-sign is needed.
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
// Renames `table`/`column` to `entry`/`field` in execution default refs of contracts under `migrations/`.
|
|
2
|
+
import { readdir, readFile, rename, rm, writeFile } from 'node:fs/promises';
|
|
3
|
+
import { basename, dirname, join, relative, sep } from 'node:path';
|
|
4
|
+
|
|
5
|
+
const SKIP_DIRS = new Set(['node_modules', '.git', 'dist']);
|
|
6
|
+
const RENAMES = new Map([
|
|
7
|
+
['table', 'entry'],
|
|
8
|
+
['column', 'field'],
|
|
9
|
+
]);
|
|
10
|
+
|
|
11
|
+
const check = process.argv.includes('--check');
|
|
12
|
+
const projectRoot = process.cwd();
|
|
13
|
+
|
|
14
|
+
function isContractFile(name: string): boolean {
|
|
15
|
+
return name.endsWith('.json') || name.endsWith('.d.ts');
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
async function findMigrationFiles(dir: string, found: string[] = []): Promise<string[]> {
|
|
19
|
+
for (const entry of await readdir(dir, { withFileTypes: true })) {
|
|
20
|
+
const path = join(dir, entry.name);
|
|
21
|
+
if (entry.isDirectory() && !SKIP_DIRS.has(entry.name)) {
|
|
22
|
+
await findMigrationFiles(path, found);
|
|
23
|
+
} else if (
|
|
24
|
+
entry.isFile() &&
|
|
25
|
+
isContractFile(entry.name) &&
|
|
26
|
+
relative(projectRoot, dir).split(sep).includes('migrations')
|
|
27
|
+
) {
|
|
28
|
+
found.push(path);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
return found;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function isObject(value: unknown): value is Record<string, unknown> {
|
|
35
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function sortKeys(value: unknown): unknown {
|
|
39
|
+
if (Array.isArray(value)) return value.map(sortKeys);
|
|
40
|
+
if (!isObject(value)) return value;
|
|
41
|
+
const sorted: Record<string, unknown> = {};
|
|
42
|
+
for (const key of Object.keys(value).sort()) sorted[key] = sortKeys(value[key]);
|
|
43
|
+
return sorted;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function parseJson(text: string): unknown {
|
|
47
|
+
try {
|
|
48
|
+
return JSON.parse(text);
|
|
49
|
+
} catch {
|
|
50
|
+
return undefined;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function serializeLike(original: string, value: unknown): string {
|
|
55
|
+
if (original === `${JSON.stringify(sortKeys(parseJson(original)))}\n`) {
|
|
56
|
+
return `${JSON.stringify(sortKeys(value))}\n`;
|
|
57
|
+
}
|
|
58
|
+
const indent = /\n([ \t]+)/.exec(original)?.[1] ?? 2;
|
|
59
|
+
return `${JSON.stringify(value, null, indent)}${original.endsWith('\n') ? '\n' : ''}`;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function renameInJson(text: string): string | undefined {
|
|
63
|
+
const contract = parseJson(text);
|
|
64
|
+
if (contract === undefined) return undefined;
|
|
65
|
+
if (!isObject(contract) || !isObject(contract['execution'])) return text;
|
|
66
|
+
const mutations = contract['execution']['mutations'];
|
|
67
|
+
const defaults = isObject(mutations) ? mutations['defaults'] : undefined;
|
|
68
|
+
if (!Array.isArray(defaults)) return text;
|
|
69
|
+
let changed = false;
|
|
70
|
+
for (const entry of defaults) {
|
|
71
|
+
if (!isObject(entry) || !isObject(entry['ref'])) continue;
|
|
72
|
+
const keys = Object.keys(entry['ref']);
|
|
73
|
+
if (!keys.some((key) => RENAMES.has(key))) continue;
|
|
74
|
+
entry['ref'] = sortKeys(
|
|
75
|
+
Object.fromEntries(
|
|
76
|
+
Object.entries(entry['ref']).map(([key, value]) => [RENAMES.get(key) ?? key, value]),
|
|
77
|
+
),
|
|
78
|
+
);
|
|
79
|
+
changed = true;
|
|
80
|
+
}
|
|
81
|
+
return changed ? serializeLike(text, contract) : text;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const DEFAULT_ELEMENT_PATH = ['execution', 'mutations', 'defaults', ''];
|
|
85
|
+
const MEMBER_BEFORE_OPENER = /readonly (\w+): (?:readonly )?$/;
|
|
86
|
+
const REF_MEMBER = /readonly (\w+): ('(?:[^'\\]|\\.)*'|"(?:[^"\\]|\\.)*")/g;
|
|
87
|
+
|
|
88
|
+
interface OpenBlock {
|
|
89
|
+
readonly member: string;
|
|
90
|
+
readonly start: number;
|
|
91
|
+
readonly isDefaultRef: boolean;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
interface Span {
|
|
95
|
+
readonly start: number;
|
|
96
|
+
readonly end: number;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function closingQuote(text: string, start: number): number {
|
|
100
|
+
let i = start + 1;
|
|
101
|
+
while (i < text.length && text[i] !== text[start]) i += text[i] === '\\' ? 2 : 1;
|
|
102
|
+
return i;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function commentEnd(text: string, start: number): number {
|
|
106
|
+
const end =
|
|
107
|
+
text[start + 1] === '/' ? text.indexOf('\n', start) : text.indexOf('*/', start + 2) + 1;
|
|
108
|
+
return end > start ? end : text.length;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function isDefaultElement(open: readonly OpenBlock[]): boolean {
|
|
112
|
+
const members = open.slice(-DEFAULT_ELEMENT_PATH.length).map((block) => block.member);
|
|
113
|
+
return members.join('/') === DEFAULT_ELEMENT_PATH.join('/');
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function executionDefaultRefs(text: string): Span[] {
|
|
117
|
+
const open: OpenBlock[] = [];
|
|
118
|
+
const refs: Span[] = [];
|
|
119
|
+
for (let i = 0; i < text.length; i++) {
|
|
120
|
+
const char = text[i];
|
|
121
|
+
if (char === "'" || char === '"' || char === '`') {
|
|
122
|
+
i = closingQuote(text, i);
|
|
123
|
+
} else if (char === '/' && (text[i + 1] === '/' || text[i + 1] === '*')) {
|
|
124
|
+
i = commentEnd(text, i);
|
|
125
|
+
} else if (char === '{' || char === '[') {
|
|
126
|
+
const member = MEMBER_BEFORE_OPENER.exec(text.slice(Math.max(0, i - 64), i))?.[1] ?? '';
|
|
127
|
+
const isDefaultRef = char === '{' && member === 'ref' && isDefaultElement(open);
|
|
128
|
+
open.push({ member, start: i, isDefaultRef });
|
|
129
|
+
} else if (char === '}' || char === ']') {
|
|
130
|
+
const block = open.pop();
|
|
131
|
+
if (block?.isDefaultRef) refs.push({ start: block.start, end: i + 1 });
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
return refs;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function renameRefMembers(block: string): string {
|
|
138
|
+
const members = [...block.matchAll(REF_MEMBER)];
|
|
139
|
+
if (!members.some(([, key = '']) => RENAMES.has(key))) return block;
|
|
140
|
+
const sorted = members
|
|
141
|
+
.map(([, key = '', value = '']) => `readonly ${RENAMES.get(key) ?? key}: ${value}`)
|
|
142
|
+
.sort();
|
|
143
|
+
let i = 0;
|
|
144
|
+
return block.replace(REF_MEMBER, () => sorted[i++] ?? '');
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function renameInDts(text: string): string {
|
|
148
|
+
let renamed = text;
|
|
149
|
+
for (const { start, end } of executionDefaultRefs(text).reverse()) {
|
|
150
|
+
const block = renameRefMembers(renamed.slice(start, end));
|
|
151
|
+
renamed = `${renamed.slice(0, start)}${block}${renamed.slice(end)}`;
|
|
152
|
+
}
|
|
153
|
+
return renamed;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
async function replaceFile(path: string, content: string): Promise<void> {
|
|
157
|
+
const temporary = join(dirname(path), `.${basename(path)}.${process.pid}.tmp`);
|
|
158
|
+
try {
|
|
159
|
+
await writeFile(temporary, content, 'utf-8');
|
|
160
|
+
await rename(temporary, path);
|
|
161
|
+
} catch (error) {
|
|
162
|
+
await rm(temporary, { force: true });
|
|
163
|
+
throw error;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
async function main(): Promise<void> {
|
|
168
|
+
const files = await findMigrationFiles(projectRoot);
|
|
169
|
+
let changed = 0;
|
|
170
|
+
let unparseable = 0;
|
|
171
|
+
for (const path of files.sort()) {
|
|
172
|
+
const before = await readFile(path, 'utf-8');
|
|
173
|
+
const after = path.endsWith('.json') ? renameInJson(before) : renameInDts(before);
|
|
174
|
+
if (after === undefined) {
|
|
175
|
+
unparseable += 1;
|
|
176
|
+
console.log(`NOT JSON ${relative(projectRoot, path)}`);
|
|
177
|
+
continue;
|
|
178
|
+
}
|
|
179
|
+
if (after === before) continue;
|
|
180
|
+
changed += 1;
|
|
181
|
+
console.log(`${check ? 'WOULD FIX' : 'FIXED'} ${relative(projectRoot, path)}`);
|
|
182
|
+
if (!check) await replaceFile(path, after);
|
|
183
|
+
}
|
|
184
|
+
console.log(
|
|
185
|
+
`${files.length} file(s) under migrations/ scanned, ${changed} ${check ? 'need' : 'got'} the rename.`,
|
|
186
|
+
);
|
|
187
|
+
if (unparseable > 0) {
|
|
188
|
+
console.log(`${unparseable} .json file(s) could not be parsed; check them by hand.`);
|
|
189
|
+
}
|
|
190
|
+
if (unparseable > 0 || (check && changed > 0)) process.exit(1);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
void main();
|
package/skills/prisma-8/upgrading/extension/upgrades/8.0.0-rc.12-to-8.0.0-rc.13/instructions.md
CHANGED
|
@@ -14,6 +14,7 @@ changes:
|
|
|
14
14
|
- '"ref"\s*:\s*\{(?![^{}]*"kind")[^{}]*"(?:table|column)"\s*:'
|
|
15
15
|
- '\.ref\??\.(?:table|column)\b'
|
|
16
16
|
- '\bref\s*:\s*\{(?![^{}]*\bkind\b)[^{}]*\b(?:table|column)\s*:[^{}]*\b(?:table|column)\s*:'
|
|
17
|
+
script: ./scripts/execution-ref-entry-field/rename-execution-ref-keys.ts
|
|
17
18
|
- id: mutation-default-generator-types-move-to-framework
|
|
18
19
|
summary: |
|
|
19
20
|
`GeneratorStability` and `RuntimeMutationDefaultGenerator` are no longer exported by the SQL
|
|
@@ -185,7 +186,15 @@ A contract with generated defaults (`temporal.createdAt()`, `temporal.updatedAt(
|
|
|
185
186
|
```
|
|
186
187
|
|
|
187
188
|
1. Run `prisma contract emit` so `contract.json` and `contract.d.ts` use the new keys. The runtime rejects a contract whose refs still say `table` and `column` with `Contract structural validation failed: execution.mutations.defaults[0].ref.entry must be a string`.
|
|
188
|
-
2.
|
|
189
|
+
2. Contracts stored under `migrations/` carry the old keys too: the snapshots in `migrations/snapshots/<hash>/contract.json` and `contract.d.ts`, and any intermediate contract a migration imports from its own directory, such as `migrations/<dir>/intermediate.json` and `intermediate.d.ts`. From the extension package root, run the script that sits next to this guide. `<skill>` is the directory of the synced `prisma-8` skill:
|
|
190
|
+
|
|
191
|
+
```bash
|
|
192
|
+
pnpm exec tsx <skill>/upgrading/extension/upgrades/8.0.0-rc.12-to-8.0.0-rc.13/scripts/execution-ref-entry-field/rename-execution-ref-keys.ts
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
The script reads every `.json` and `.d.ts` file under a `migrations/` directory, skipping `node_modules`, `.git` and `dist`, and changes only `execution.mutations.defaults[].ref` entries. In each one it renames `table` to `entry` and `column` to `field`, and writes the keys in the order `entry`, `field`, `namespace`, which is the order `prisma contract emit` writes. The old key order varies between files: older `contract.d.ts` files list `namespace`, `table`, `column`, newer ones `column`, `namespace`, `table`. The script handles any order; a hand edit or a search-and-replace that assumes one order misses some refs. A `contract.json` in canonical form stays canonical, so a snapshot matches a fresh emit apart from `executionHash`; an indented JSON file keeps its indentation. The script leaves every `executionHash` as it is. That hash no longer matches the renamed content, but the snapshot loader re-hashes only the storage section, so nothing checks it. `storageHash` and `profileHash` do not move, so snapshot directory names stay the same. A second run changes nothing. Pass `--check` to list the files it would change without writing them; it exits 1 if any would change. A `.json` file it cannot parse is listed as `NOT JSON` and also makes it exit 1.
|
|
196
|
+
|
|
197
|
+
To confirm the rewrite, run the script again with `--check`; it exits 0 when no ref is left to rename. No CLI command reads the package's own `migrations/snapshots/`: run from the package root, the CLI looks for migrations in `migrations/app/`, and a contract-space package keeps them directly under `migrations/`. The CLI reads an extension's contract space in an app that installs the extension, from the app's `migrations/<space-id>/` and `migrations/snapshots/`. There, `prisma migration check --space <space-id>` loads the contract at the space's head and fails with `MIGRATION.CHECK_CONTRACT_UNREADABLE` while its refs still say `table` and `column`. The head is the only snapshot of an extension space the CLI loads; `prisma db migrate --to <hash>` selects a snapshot of the app space, not of an extension.
|
|
189
198
|
3. Code that reads the section directly changes `.ref.table` to `.ref.entry` and `.ref.column` to `.ref.field`.
|
|
190
199
|
|
|
191
200
|
`executionHash` changes for every contract with generated defaults, because the canonical JSON changes. Nothing compares it against the database, so no migration or re-sign is needed.
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
// Renames `table`/`column` to `entry`/`field` in execution default refs of contracts under `migrations/`.
|
|
2
|
+
import { readdir, readFile, rename, rm, writeFile } from 'node:fs/promises';
|
|
3
|
+
import { basename, dirname, join, relative, sep } from 'node:path';
|
|
4
|
+
|
|
5
|
+
const SKIP_DIRS = new Set(['node_modules', '.git', 'dist']);
|
|
6
|
+
const RENAMES = new Map([
|
|
7
|
+
['table', 'entry'],
|
|
8
|
+
['column', 'field'],
|
|
9
|
+
]);
|
|
10
|
+
|
|
11
|
+
const check = process.argv.includes('--check');
|
|
12
|
+
const projectRoot = process.cwd();
|
|
13
|
+
|
|
14
|
+
function isContractFile(name: string): boolean {
|
|
15
|
+
return name.endsWith('.json') || name.endsWith('.d.ts');
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
async function findMigrationFiles(dir: string, found: string[] = []): Promise<string[]> {
|
|
19
|
+
for (const entry of await readdir(dir, { withFileTypes: true })) {
|
|
20
|
+
const path = join(dir, entry.name);
|
|
21
|
+
if (entry.isDirectory() && !SKIP_DIRS.has(entry.name)) {
|
|
22
|
+
await findMigrationFiles(path, found);
|
|
23
|
+
} else if (
|
|
24
|
+
entry.isFile() &&
|
|
25
|
+
isContractFile(entry.name) &&
|
|
26
|
+
relative(projectRoot, dir).split(sep).includes('migrations')
|
|
27
|
+
) {
|
|
28
|
+
found.push(path);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
return found;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function isObject(value: unknown): value is Record<string, unknown> {
|
|
35
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function sortKeys(value: unknown): unknown {
|
|
39
|
+
if (Array.isArray(value)) return value.map(sortKeys);
|
|
40
|
+
if (!isObject(value)) return value;
|
|
41
|
+
const sorted: Record<string, unknown> = {};
|
|
42
|
+
for (const key of Object.keys(value).sort()) sorted[key] = sortKeys(value[key]);
|
|
43
|
+
return sorted;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function parseJson(text: string): unknown {
|
|
47
|
+
try {
|
|
48
|
+
return JSON.parse(text);
|
|
49
|
+
} catch {
|
|
50
|
+
return undefined;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function serializeLike(original: string, value: unknown): string {
|
|
55
|
+
if (original === `${JSON.stringify(sortKeys(parseJson(original)))}\n`) {
|
|
56
|
+
return `${JSON.stringify(sortKeys(value))}\n`;
|
|
57
|
+
}
|
|
58
|
+
const indent = /\n([ \t]+)/.exec(original)?.[1] ?? 2;
|
|
59
|
+
return `${JSON.stringify(value, null, indent)}${original.endsWith('\n') ? '\n' : ''}`;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function renameInJson(text: string): string | undefined {
|
|
63
|
+
const contract = parseJson(text);
|
|
64
|
+
if (contract === undefined) return undefined;
|
|
65
|
+
if (!isObject(contract) || !isObject(contract['execution'])) return text;
|
|
66
|
+
const mutations = contract['execution']['mutations'];
|
|
67
|
+
const defaults = isObject(mutations) ? mutations['defaults'] : undefined;
|
|
68
|
+
if (!Array.isArray(defaults)) return text;
|
|
69
|
+
let changed = false;
|
|
70
|
+
for (const entry of defaults) {
|
|
71
|
+
if (!isObject(entry) || !isObject(entry['ref'])) continue;
|
|
72
|
+
const keys = Object.keys(entry['ref']);
|
|
73
|
+
if (!keys.some((key) => RENAMES.has(key))) continue;
|
|
74
|
+
entry['ref'] = sortKeys(
|
|
75
|
+
Object.fromEntries(
|
|
76
|
+
Object.entries(entry['ref']).map(([key, value]) => [RENAMES.get(key) ?? key, value]),
|
|
77
|
+
),
|
|
78
|
+
);
|
|
79
|
+
changed = true;
|
|
80
|
+
}
|
|
81
|
+
return changed ? serializeLike(text, contract) : text;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const DEFAULT_ELEMENT_PATH = ['execution', 'mutations', 'defaults', ''];
|
|
85
|
+
const MEMBER_BEFORE_OPENER = /readonly (\w+): (?:readonly )?$/;
|
|
86
|
+
const REF_MEMBER = /readonly (\w+): ('(?:[^'\\]|\\.)*'|"(?:[^"\\]|\\.)*")/g;
|
|
87
|
+
|
|
88
|
+
interface OpenBlock {
|
|
89
|
+
readonly member: string;
|
|
90
|
+
readonly start: number;
|
|
91
|
+
readonly isDefaultRef: boolean;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
interface Span {
|
|
95
|
+
readonly start: number;
|
|
96
|
+
readonly end: number;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function closingQuote(text: string, start: number): number {
|
|
100
|
+
let i = start + 1;
|
|
101
|
+
while (i < text.length && text[i] !== text[start]) i += text[i] === '\\' ? 2 : 1;
|
|
102
|
+
return i;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function commentEnd(text: string, start: number): number {
|
|
106
|
+
const end =
|
|
107
|
+
text[start + 1] === '/' ? text.indexOf('\n', start) : text.indexOf('*/', start + 2) + 1;
|
|
108
|
+
return end > start ? end : text.length;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function isDefaultElement(open: readonly OpenBlock[]): boolean {
|
|
112
|
+
const members = open.slice(-DEFAULT_ELEMENT_PATH.length).map((block) => block.member);
|
|
113
|
+
return members.join('/') === DEFAULT_ELEMENT_PATH.join('/');
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function executionDefaultRefs(text: string): Span[] {
|
|
117
|
+
const open: OpenBlock[] = [];
|
|
118
|
+
const refs: Span[] = [];
|
|
119
|
+
for (let i = 0; i < text.length; i++) {
|
|
120
|
+
const char = text[i];
|
|
121
|
+
if (char === "'" || char === '"' || char === '`') {
|
|
122
|
+
i = closingQuote(text, i);
|
|
123
|
+
} else if (char === '/' && (text[i + 1] === '/' || text[i + 1] === '*')) {
|
|
124
|
+
i = commentEnd(text, i);
|
|
125
|
+
} else if (char === '{' || char === '[') {
|
|
126
|
+
const member = MEMBER_BEFORE_OPENER.exec(text.slice(Math.max(0, i - 64), i))?.[1] ?? '';
|
|
127
|
+
const isDefaultRef = char === '{' && member === 'ref' && isDefaultElement(open);
|
|
128
|
+
open.push({ member, start: i, isDefaultRef });
|
|
129
|
+
} else if (char === '}' || char === ']') {
|
|
130
|
+
const block = open.pop();
|
|
131
|
+
if (block?.isDefaultRef) refs.push({ start: block.start, end: i + 1 });
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
return refs;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function renameRefMembers(block: string): string {
|
|
138
|
+
const members = [...block.matchAll(REF_MEMBER)];
|
|
139
|
+
if (!members.some(([, key = '']) => RENAMES.has(key))) return block;
|
|
140
|
+
const sorted = members
|
|
141
|
+
.map(([, key = '', value = '']) => `readonly ${RENAMES.get(key) ?? key}: ${value}`)
|
|
142
|
+
.sort();
|
|
143
|
+
let i = 0;
|
|
144
|
+
return block.replace(REF_MEMBER, () => sorted[i++] ?? '');
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function renameInDts(text: string): string {
|
|
148
|
+
let renamed = text;
|
|
149
|
+
for (const { start, end } of executionDefaultRefs(text).reverse()) {
|
|
150
|
+
const block = renameRefMembers(renamed.slice(start, end));
|
|
151
|
+
renamed = `${renamed.slice(0, start)}${block}${renamed.slice(end)}`;
|
|
152
|
+
}
|
|
153
|
+
return renamed;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
async function replaceFile(path: string, content: string): Promise<void> {
|
|
157
|
+
const temporary = join(dirname(path), `.${basename(path)}.${process.pid}.tmp`);
|
|
158
|
+
try {
|
|
159
|
+
await writeFile(temporary, content, 'utf-8');
|
|
160
|
+
await rename(temporary, path);
|
|
161
|
+
} catch (error) {
|
|
162
|
+
await rm(temporary, { force: true });
|
|
163
|
+
throw error;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
async function main(): Promise<void> {
|
|
168
|
+
const files = await findMigrationFiles(projectRoot);
|
|
169
|
+
let changed = 0;
|
|
170
|
+
let unparseable = 0;
|
|
171
|
+
for (const path of files.sort()) {
|
|
172
|
+
const before = await readFile(path, 'utf-8');
|
|
173
|
+
const after = path.endsWith('.json') ? renameInJson(before) : renameInDts(before);
|
|
174
|
+
if (after === undefined) {
|
|
175
|
+
unparseable += 1;
|
|
176
|
+
console.log(`NOT JSON ${relative(projectRoot, path)}`);
|
|
177
|
+
continue;
|
|
178
|
+
}
|
|
179
|
+
if (after === before) continue;
|
|
180
|
+
changed += 1;
|
|
181
|
+
console.log(`${check ? 'WOULD FIX' : 'FIXED'} ${relative(projectRoot, path)}`);
|
|
182
|
+
if (!check) await replaceFile(path, after);
|
|
183
|
+
}
|
|
184
|
+
console.log(
|
|
185
|
+
`${files.length} file(s) under migrations/ scanned, ${changed} ${check ? 'need' : 'got'} the rename.`,
|
|
186
|
+
);
|
|
187
|
+
if (unparseable > 0) {
|
|
188
|
+
console.log(`${unparseable} .json file(s) could not be parsed; check them by hand.`);
|
|
189
|
+
}
|
|
190
|
+
if (unparseable > 0 || (check && changed > 0)) process.exit(1);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
void main();
|