@prisma-next/migration-tools 0.4.1 → 0.4.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/dist/{attestation-DtF8tEOM.mjs → attestation-BnzTb0Qp.mjs} +2 -2
- package/dist/{attestation-DtF8tEOM.mjs.map → attestation-BnzTb0Qp.mjs.map} +1 -1
- package/dist/{errors-BKbRGCJM.mjs → errors-BmiSgz1j.mjs} +7 -7
- package/dist/{errors-BKbRGCJM.mjs.map → errors-BmiSgz1j.mjs.map} +1 -1
- package/dist/exports/attestation.mjs +2 -2
- package/dist/exports/dag.mjs +1 -1
- package/dist/exports/io.mjs +1 -1
- package/dist/exports/migration-ts.d.mts +5 -1
- package/dist/exports/migration-ts.d.mts.map +1 -1
- package/dist/exports/migration-ts.mjs +5 -1
- package/dist/exports/migration-ts.mjs.map +1 -1
- package/dist/exports/migration.d.mts +47 -17
- package/dist/exports/migration.d.mts.map +1 -1
- package/dist/exports/migration.mjs +58 -75
- package/dist/exports/migration.mjs.map +1 -1
- package/dist/exports/refs.d.mts +11 -5
- package/dist/exports/refs.d.mts.map +1 -1
- package/dist/exports/refs.mjs +106 -30
- package/dist/exports/refs.mjs.map +1 -1
- package/dist/exports/types.mjs +1 -1
- package/dist/{io-CCnYsUHU.mjs → io-Cd6GLyjK.mjs} +2 -2
- package/dist/{io-CCnYsUHU.mjs.map → io-Cd6GLyjK.mjs.map} +1 -1
- package/package.json +4 -4
- package/src/exports/migration.ts +8 -1
- package/src/exports/refs.ts +10 -2
- package/src/migration-base.ts +80 -90
- package/src/migration-ts.ts +5 -1
- package/src/refs.ts +148 -37
package/src/refs.ts
CHANGED
|
@@ -1,14 +1,19 @@
|
|
|
1
|
-
import { mkdir, readFile, rename, writeFile } from 'node:fs/promises';
|
|
1
|
+
import { mkdir, readdir, readFile, rename, rmdir, unlink, writeFile } from 'node:fs/promises';
|
|
2
2
|
import { type } from 'arktype';
|
|
3
|
-
import { dirname, join } from 'pathe';
|
|
3
|
+
import { dirname, join, relative } from 'pathe';
|
|
4
4
|
import {
|
|
5
|
+
errorInvalidRefFile,
|
|
5
6
|
errorInvalidRefName,
|
|
6
|
-
errorInvalidRefs,
|
|
7
7
|
errorInvalidRefValue,
|
|
8
8
|
MigrationToolsError,
|
|
9
9
|
} from './errors';
|
|
10
10
|
|
|
11
|
-
export
|
|
11
|
+
export interface RefEntry {
|
|
12
|
+
readonly hash: string;
|
|
13
|
+
readonly invariants: readonly string[];
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export type Refs = Readonly<Record<string, RefEntry>>;
|
|
12
17
|
|
|
13
18
|
const REF_NAME_PATTERN = /^[a-z0-9]([a-z0-9-]*[a-z0-9])?(\/[a-z0-9]([a-z0-9-]*[a-z0-9])?)*$/;
|
|
14
19
|
const REF_VALUE_PATTERN = /^sha256:(empty|[0-9a-f]{64})$/;
|
|
@@ -25,22 +30,40 @@ export function validateRefValue(value: string): boolean {
|
|
|
25
30
|
return REF_VALUE_PATTERN.test(value);
|
|
26
31
|
}
|
|
27
32
|
|
|
28
|
-
const
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
33
|
+
const RefEntrySchema = type({
|
|
34
|
+
hash: 'string',
|
|
35
|
+
invariants: 'string[]',
|
|
36
|
+
}).narrow((entry, ctx) => {
|
|
37
|
+
if (!validateRefValue(entry.hash))
|
|
38
|
+
return ctx.mustBe(`a valid contract hash (got "${entry.hash}")`);
|
|
34
39
|
return true;
|
|
35
40
|
});
|
|
36
41
|
|
|
37
|
-
|
|
42
|
+
function refFilePath(refsDir: string, name: string): string {
|
|
43
|
+
return join(refsDir, `${name}.json`);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function refNameFromPath(refsDir: string, filePath: string): string {
|
|
47
|
+
const rel = relative(refsDir, filePath);
|
|
48
|
+
return rel.replace(/\.json$/, '');
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export async function readRef(refsDir: string, name: string): Promise<RefEntry> {
|
|
52
|
+
if (!validateRefName(name)) {
|
|
53
|
+
throw errorInvalidRefName(name);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const filePath = refFilePath(refsDir, name);
|
|
38
57
|
let raw: string;
|
|
39
58
|
try {
|
|
40
|
-
raw = await readFile(
|
|
59
|
+
raw = await readFile(filePath, 'utf-8');
|
|
41
60
|
} catch (error) {
|
|
42
61
|
if (error instanceof Error && (error as { code?: string }).code === 'ENOENT') {
|
|
43
|
-
|
|
62
|
+
throw new MigrationToolsError('MIGRATION.UNKNOWN_REF', `Unknown ref "${name}"`, {
|
|
63
|
+
why: `No ref file found at "${filePath}".`,
|
|
64
|
+
fix: `Create the ref with: prisma-next migration ref set ${name} <hash>`,
|
|
65
|
+
details: { refName: name, filePath },
|
|
66
|
+
});
|
|
44
67
|
}
|
|
45
68
|
throw error;
|
|
46
69
|
}
|
|
@@ -49,54 +72,142 @@ export async function readRefs(refsPath: string): Promise<Refs> {
|
|
|
49
72
|
try {
|
|
50
73
|
parsed = JSON.parse(raw);
|
|
51
74
|
} catch {
|
|
52
|
-
throw
|
|
75
|
+
throw errorInvalidRefFile(filePath, 'Failed to parse as JSON');
|
|
53
76
|
}
|
|
54
77
|
|
|
55
|
-
const result =
|
|
78
|
+
const result = RefEntrySchema(parsed);
|
|
56
79
|
if (result instanceof type.errors) {
|
|
57
|
-
throw
|
|
80
|
+
throw errorInvalidRefFile(filePath, result.summary);
|
|
58
81
|
}
|
|
59
82
|
|
|
60
83
|
return result;
|
|
61
84
|
}
|
|
62
85
|
|
|
63
|
-
export async function
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
86
|
+
export async function readRefs(refsDir: string): Promise<Refs> {
|
|
87
|
+
let entries: string[];
|
|
88
|
+
try {
|
|
89
|
+
entries = await readdir(refsDir, { recursive: true, encoding: 'utf-8' });
|
|
90
|
+
} catch (error) {
|
|
91
|
+
if (error instanceof Error && (error as { code?: string }).code === 'ENOENT') {
|
|
92
|
+
return {};
|
|
93
|
+
}
|
|
94
|
+
throw error;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const jsonFiles = entries.filter((entry) => entry.endsWith('.json'));
|
|
98
|
+
const refs: Record<string, RefEntry> = {};
|
|
99
|
+
|
|
100
|
+
for (const jsonFile of jsonFiles) {
|
|
101
|
+
const filePath = join(refsDir, jsonFile);
|
|
102
|
+
const name = refNameFromPath(refsDir, filePath);
|
|
103
|
+
|
|
104
|
+
let raw: string;
|
|
105
|
+
try {
|
|
106
|
+
raw = await readFile(filePath, 'utf-8');
|
|
107
|
+
} catch (error) {
|
|
108
|
+
// Tolerate the TOCTOU race between `readdir` and `readFile` (ENOENT) and
|
|
109
|
+
// benign EISDIR if a directory happens to end in `.json`. Anything else
|
|
110
|
+
// (EACCES, EIO, EMFILE, …) is a real failure and propagates so the CLI
|
|
111
|
+
// surfaces it rather than silently dropping the ref.
|
|
112
|
+
const code = error instanceof Error ? (error as { code?: string }).code : undefined;
|
|
113
|
+
if (code === 'ENOENT' || code === 'EISDIR') {
|
|
114
|
+
continue;
|
|
115
|
+
}
|
|
116
|
+
throw error;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
let parsed: unknown;
|
|
120
|
+
try {
|
|
121
|
+
parsed = JSON.parse(raw);
|
|
122
|
+
} catch {
|
|
123
|
+
throw errorInvalidRefFile(filePath, 'Failed to parse as JSON');
|
|
67
124
|
}
|
|
68
|
-
|
|
69
|
-
|
|
125
|
+
|
|
126
|
+
const result = RefEntrySchema(parsed);
|
|
127
|
+
if (result instanceof type.errors) {
|
|
128
|
+
throw errorInvalidRefFile(filePath, result.summary);
|
|
70
129
|
}
|
|
130
|
+
|
|
131
|
+
refs[name] = result;
|
|
71
132
|
}
|
|
72
133
|
|
|
73
|
-
|
|
134
|
+
return refs;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
export async function writeRef(refsDir: string, name: string, entry: RefEntry): Promise<void> {
|
|
138
|
+
if (!validateRefName(name)) {
|
|
139
|
+
throw errorInvalidRefName(name);
|
|
140
|
+
}
|
|
141
|
+
if (!validateRefValue(entry.hash)) {
|
|
142
|
+
throw errorInvalidRefValue(entry.hash);
|
|
143
|
+
}
|
|
74
144
|
|
|
75
|
-
const
|
|
145
|
+
const filePath = refFilePath(refsDir, name);
|
|
146
|
+
const dir = dirname(filePath);
|
|
76
147
|
await mkdir(dir, { recursive: true });
|
|
77
148
|
|
|
78
|
-
const tmpPath = join(dir,
|
|
79
|
-
await writeFile(
|
|
80
|
-
|
|
149
|
+
const tmpPath = join(dir, `.${name.split('/').pop()}.json.${Date.now()}.tmp`);
|
|
150
|
+
await writeFile(
|
|
151
|
+
tmpPath,
|
|
152
|
+
`${JSON.stringify({ hash: entry.hash, invariants: [...entry.invariants] }, null, 2)}\n`,
|
|
153
|
+
);
|
|
154
|
+
await rename(tmpPath, filePath);
|
|
81
155
|
}
|
|
82
156
|
|
|
83
|
-
export function
|
|
157
|
+
export async function deleteRef(refsDir: string, name: string): Promise<void> {
|
|
84
158
|
if (!validateRefName(name)) {
|
|
85
159
|
throw errorInvalidRefName(name);
|
|
86
160
|
}
|
|
87
161
|
|
|
88
|
-
const
|
|
89
|
-
|
|
162
|
+
const filePath = refFilePath(refsDir, name);
|
|
163
|
+
try {
|
|
164
|
+
await unlink(filePath);
|
|
165
|
+
} catch (error) {
|
|
166
|
+
if (error instanceof Error && (error as { code?: string }).code === 'ENOENT') {
|
|
167
|
+
throw new MigrationToolsError('MIGRATION.UNKNOWN_REF', `Unknown ref "${name}"`, {
|
|
168
|
+
why: `No ref file found at "${filePath}".`,
|
|
169
|
+
fix: 'Run `prisma-next migration ref list` to see available refs.',
|
|
170
|
+
details: { refName: name, filePath },
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
throw error;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// Clean empty parent directories up to refsDir. Stop walking on the expected
|
|
177
|
+
// "directory has siblings" signal (ENOTEMPTY on Linux, EEXIST on some BSDs)
|
|
178
|
+
// and on ENOENT (concurrent removal). Anything else (EACCES, EIO, …) is a
|
|
179
|
+
// real failure and propagates.
|
|
180
|
+
let dir = dirname(filePath);
|
|
181
|
+
while (dir !== refsDir && dir.startsWith(refsDir)) {
|
|
182
|
+
try {
|
|
183
|
+
await rmdir(dir);
|
|
184
|
+
dir = dirname(dir);
|
|
185
|
+
} catch (error) {
|
|
186
|
+
const code = error instanceof Error ? (error as { code?: string }).code : undefined;
|
|
187
|
+
if (code === 'ENOTEMPTY' || code === 'EEXIST' || code === 'ENOENT') {
|
|
188
|
+
break;
|
|
189
|
+
}
|
|
190
|
+
throw error;
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
export function resolveRef(refs: Refs, name: string): RefEntry {
|
|
196
|
+
if (!validateRefName(name)) {
|
|
197
|
+
throw errorInvalidRefName(name);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// Object.hasOwn gate: plain-object `refs` would otherwise let
|
|
201
|
+
// `refs['constructor']` return Object.prototype.constructor and bypass the
|
|
202
|
+
// UNKNOWN_REF throw. validateRefName accepts `"constructor"` as a name shape.
|
|
203
|
+
if (!Object.hasOwn(refs, name)) {
|
|
90
204
|
throw new MigrationToolsError('MIGRATION.UNKNOWN_REF', `Unknown ref "${name}"`, {
|
|
91
|
-
why: `No ref named "${name}" exists
|
|
92
|
-
fix: `Available refs: ${Object.keys(refs).join(', ') || '(none)'}. Create a ref with: set
|
|
205
|
+
why: `No ref named "${name}" exists.`,
|
|
206
|
+
fix: `Available refs: ${Object.keys(refs).join(', ') || '(none)'}. Create a ref with: prisma-next migration ref set ${name} <hash>`,
|
|
93
207
|
details: { refName: name, availableRefs: Object.keys(refs) },
|
|
94
208
|
});
|
|
95
209
|
}
|
|
96
210
|
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
return hash;
|
|
211
|
+
// biome-ignore lint/style/noNonNullAssertion: Object.hasOwn gate above guarantees this is defined
|
|
212
|
+
return refs[name]!;
|
|
102
213
|
}
|