@prisma-next/migration-tools 0.3.0 → 0.4.0-dev.10
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/README.md +4 -0
- package/dist/attestation-DnebS4XZ.mjs +64 -0
- package/dist/attestation-DnebS4XZ.mjs.map +1 -0
- package/dist/{constants-9f-bCq8Y.mjs → constants-BRi0X7B_.mjs} +1 -1
- package/dist/{constants-9f-bCq8Y.mjs.map → constants-BRi0X7B_.mjs.map} +1 -1
- package/dist/{errors-CSAAto11.mjs → errors-C_XuSbX7.mjs} +1 -1
- package/dist/{errors-CSAAto11.mjs.map → errors-C_XuSbX7.mjs.map} +1 -1
- package/dist/exports/attestation.d.mts +9 -1
- package/dist/exports/attestation.d.mts.map +1 -1
- package/dist/exports/attestation.mjs +3 -61
- package/dist/exports/constants.mjs +1 -1
- package/dist/exports/dag.d.mts +1 -1
- package/dist/exports/dag.mjs +2 -2
- package/dist/exports/io.d.mts +14 -2
- package/dist/exports/io.d.mts.map +1 -1
- package/dist/exports/io.mjs +2 -2
- package/dist/exports/migration-ts.d.mts +27 -18
- package/dist/exports/migration-ts.d.mts.map +1 -1
- package/dist/exports/migration-ts.mjs +39 -85
- package/dist/exports/migration-ts.mjs.map +1 -1
- package/dist/exports/migration.d.mts +57 -0
- package/dist/exports/migration.d.mts.map +1 -0
- package/dist/exports/migration.mjs +159 -0
- package/dist/exports/migration.mjs.map +1 -0
- package/dist/exports/refs.mjs +1 -1
- package/dist/exports/types.d.mts +1 -1
- package/dist/exports/types.mjs +1 -1
- package/dist/{io-LsuurzNb.mjs → io-Cun81AIZ.mjs} +25 -4
- package/dist/io-Cun81AIZ.mjs.map +1 -0
- package/dist/{types-BW_pJEe8.d.mts → types-D2uX4ql7.d.mts} +1 -1
- package/dist/{types-BW_pJEe8.d.mts.map → types-D2uX4ql7.d.mts.map} +1 -1
- package/package.json +10 -6
- package/src/attestation.ts +10 -11
- package/src/exports/io.ts +1 -0
- package/src/exports/migration-ts.ts +3 -6
- package/src/exports/migration.ts +1 -0
- package/src/io.ts +26 -1
- package/src/migration-base.ts +229 -0
- package/src/migration-ts.ts +27 -144
- package/src/runtime-detection.ts +18 -0
- package/dist/exports/attestation.mjs.map +0 -1
- package/dist/io-LsuurzNb.mjs.map +0 -1
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
import { readFileSync, realpathSync, writeFileSync } from 'node:fs';
|
|
2
|
+
import { fileURLToPath } from 'node:url';
|
|
3
|
+
import type { Contract } from '@prisma-next/contract/types';
|
|
4
|
+
import type {
|
|
5
|
+
MigrationPlan,
|
|
6
|
+
MigrationPlanOperation,
|
|
7
|
+
} from '@prisma-next/framework-components/control';
|
|
8
|
+
import { ifDefined } from '@prisma-next/utils/defined';
|
|
9
|
+
import { type } from 'arktype';
|
|
10
|
+
import { dirname, join } from 'pathe';
|
|
11
|
+
import { computeMigrationId } from './attestation';
|
|
12
|
+
import type { MigrationManifest, MigrationOps } from './types';
|
|
13
|
+
|
|
14
|
+
export interface MigrationMeta {
|
|
15
|
+
readonly from: string;
|
|
16
|
+
readonly to: string;
|
|
17
|
+
readonly kind?: 'regular' | 'baseline';
|
|
18
|
+
readonly labels?: readonly string[];
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const MigrationMetaSchema = type({
|
|
22
|
+
from: 'string',
|
|
23
|
+
to: 'string',
|
|
24
|
+
'kind?': "'regular' | 'baseline'",
|
|
25
|
+
'labels?': type('string').array(),
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Base class for class-flow migrations.
|
|
30
|
+
*
|
|
31
|
+
* A `Migration` subclass is itself a `MigrationPlan`: CLI commands and the
|
|
32
|
+
* runner can consume it directly via `targetId`, `operations`, `origin`, and
|
|
33
|
+
* `destination`. The manifest-shaped inputs come from `describe()`, which
|
|
34
|
+
* every migration must implement — `migration.json` is required for a
|
|
35
|
+
* migration to be valid.
|
|
36
|
+
*/
|
|
37
|
+
export abstract class Migration<TOperation extends MigrationPlanOperation = MigrationPlanOperation>
|
|
38
|
+
implements MigrationPlan
|
|
39
|
+
{
|
|
40
|
+
abstract readonly targetId: string;
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Ordered list of operations this migration performs.
|
|
44
|
+
*
|
|
45
|
+
* Implemented as a getter so that subclasses can either precompute the list
|
|
46
|
+
* in their constructor or build it lazily per access.
|
|
47
|
+
*/
|
|
48
|
+
abstract get operations(): readonly TOperation[];
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Metadata inputs used to build `migration.json` and to derive the plan's
|
|
52
|
+
* origin/destination identities. Every migration must provide this —
|
|
53
|
+
* omitting it would produce an invalid on-disk migration package.
|
|
54
|
+
*/
|
|
55
|
+
abstract describe(): MigrationMeta;
|
|
56
|
+
|
|
57
|
+
get origin(): { readonly storageHash: string } | null {
|
|
58
|
+
const from = this.describe().from;
|
|
59
|
+
// An empty `from` represents a migration with no prior origin (e.g.
|
|
60
|
+
// initial baseline, or an in-process plan that was never persisted).
|
|
61
|
+
// Surface that as a null origin so runners treat the plan as
|
|
62
|
+
// origin-less rather than matching against an empty storage hash.
|
|
63
|
+
return from === '' ? null : { storageHash: from };
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
get destination(): { readonly storageHash: string } {
|
|
67
|
+
return { storageHash: this.describe().to };
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Entrypoint guard for migration files. When called at module scope,
|
|
72
|
+
* detects whether the file is being run directly (e.g. `node migration.ts`)
|
|
73
|
+
* and if so, serializes the migration plan to `ops.json` and
|
|
74
|
+
* `migration.json` in the same directory. When the file is imported by
|
|
75
|
+
* another module, this is a no-op.
|
|
76
|
+
*
|
|
77
|
+
* Usage (at module scope, after the class definition):
|
|
78
|
+
*
|
|
79
|
+
* class MyMigration extends Migration { ... }
|
|
80
|
+
* export default MyMigration;
|
|
81
|
+
* Migration.run(import.meta.url, MyMigration);
|
|
82
|
+
*/
|
|
83
|
+
static run(importMetaUrl: string, MigrationClass: new () => Migration): void {
|
|
84
|
+
if (!importMetaUrl) return;
|
|
85
|
+
|
|
86
|
+
const metaFilename = fileURLToPath(importMetaUrl);
|
|
87
|
+
const argv1 = process.argv[1];
|
|
88
|
+
if (!argv1) return;
|
|
89
|
+
|
|
90
|
+
let isEntrypoint: boolean;
|
|
91
|
+
try {
|
|
92
|
+
isEntrypoint = realpathSync(metaFilename) === realpathSync(argv1);
|
|
93
|
+
} catch {
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
if (!isEntrypoint) return;
|
|
97
|
+
|
|
98
|
+
const args = process.argv.slice(2);
|
|
99
|
+
|
|
100
|
+
if (args.includes('--help')) {
|
|
101
|
+
printHelp();
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const dryRun = args.includes('--dry-run');
|
|
106
|
+
const migrationDir = dirname(metaFilename);
|
|
107
|
+
|
|
108
|
+
try {
|
|
109
|
+
serializeMigration(MigrationClass, migrationDir, dryRun);
|
|
110
|
+
} catch (err) {
|
|
111
|
+
process.stderr.write(`${err instanceof Error ? err.message : String(err)}\n`);
|
|
112
|
+
process.exitCode = 1;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function printHelp(): void {
|
|
118
|
+
process.stdout.write(
|
|
119
|
+
[
|
|
120
|
+
'Usage: node <migration-file> [options]',
|
|
121
|
+
'',
|
|
122
|
+
'Options:',
|
|
123
|
+
' --dry-run Print operations to stdout without writing files',
|
|
124
|
+
' --help Show this help message',
|
|
125
|
+
'',
|
|
126
|
+
].join('\n'),
|
|
127
|
+
);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Build the attested manifest written by `Migration.run()`.
|
|
132
|
+
*
|
|
133
|
+
* When a `migration.json` already exists in the directory (the common case:
|
|
134
|
+
* the package was scaffolded by `migration plan`), preserve the contract
|
|
135
|
+
* bookends, hints, labels, and `createdAt` set there — those fields are
|
|
136
|
+
* owned by the CLI scaffolder, not the authored class. Only the
|
|
137
|
+
* `describe()`-derived fields (`from`, `to`, `kind`) and the operations
|
|
138
|
+
* change as the author iterates. When no manifest exists yet (a bare
|
|
139
|
+
* `migration.ts` run from scratch), synthesize a minimal but
|
|
140
|
+
* schema-conformant manifest so the resulting package can still be read,
|
|
141
|
+
* verified, and applied.
|
|
142
|
+
*
|
|
143
|
+
* In both cases the `migrationId` is recomputed against the current
|
|
144
|
+
* manifest + ops so the on-disk artifacts are always fully attested — no
|
|
145
|
+
* draft (`migrationId: null`) ever leaves this function.
|
|
146
|
+
*/
|
|
147
|
+
function buildAttestedManifest(
|
|
148
|
+
migrationDir: string,
|
|
149
|
+
meta: MigrationMeta,
|
|
150
|
+
ops: MigrationOps,
|
|
151
|
+
): MigrationManifest {
|
|
152
|
+
const existing = readExistingManifest(join(migrationDir, 'migration.json'));
|
|
153
|
+
|
|
154
|
+
const baseManifest: MigrationManifest = {
|
|
155
|
+
migrationId: null,
|
|
156
|
+
from: meta.from,
|
|
157
|
+
to: meta.to,
|
|
158
|
+
kind: meta.kind ?? 'regular',
|
|
159
|
+
labels: meta.labels ?? existing?.labels ?? [],
|
|
160
|
+
createdAt: existing?.createdAt ?? new Date().toISOString(),
|
|
161
|
+
fromContract: existing?.fromContract ?? null,
|
|
162
|
+
// When no scaffolded manifest exists we synthesize a minimal contract
|
|
163
|
+
// stub so the package is still readable end-to-end. The cast is
|
|
164
|
+
// intentional: only the storage bookend matters for hash computation
|
|
165
|
+
// (everything else is stripped by `computeMigrationId`), and a real
|
|
166
|
+
// contract bookend would only be available after `migration plan`.
|
|
167
|
+
toContract: existing?.toContract ?? ({ storage: { storageHash: meta.to } } as Contract),
|
|
168
|
+
hints: existing?.hints ?? {
|
|
169
|
+
used: [],
|
|
170
|
+
applied: [],
|
|
171
|
+
plannerVersion: '2.0.0',
|
|
172
|
+
planningStrategy: 'class-based',
|
|
173
|
+
},
|
|
174
|
+
...ifDefined('authorship', existing?.authorship),
|
|
175
|
+
};
|
|
176
|
+
|
|
177
|
+
const migrationId = computeMigrationId(baseManifest, ops);
|
|
178
|
+
return { ...baseManifest, migrationId };
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
function readExistingManifest(manifestPath: string): Partial<MigrationManifest> | null {
|
|
182
|
+
let raw: string;
|
|
183
|
+
try {
|
|
184
|
+
raw = readFileSync(manifestPath, 'utf-8');
|
|
185
|
+
} catch {
|
|
186
|
+
return null;
|
|
187
|
+
}
|
|
188
|
+
try {
|
|
189
|
+
return JSON.parse(raw) as Partial<MigrationManifest>;
|
|
190
|
+
} catch {
|
|
191
|
+
return null;
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
function serializeMigration(
|
|
196
|
+
MigrationClass: new () => Migration,
|
|
197
|
+
migrationDir: string,
|
|
198
|
+
dryRun: boolean,
|
|
199
|
+
): void {
|
|
200
|
+
const instance = new MigrationClass();
|
|
201
|
+
|
|
202
|
+
const ops = instance.operations;
|
|
203
|
+
|
|
204
|
+
if (!Array.isArray(ops)) {
|
|
205
|
+
throw new Error('operations must be an array');
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
const serializedOps = JSON.stringify(ops, null, 2);
|
|
209
|
+
|
|
210
|
+
const rawMeta: unknown = instance.describe();
|
|
211
|
+
const parsed = MigrationMetaSchema(rawMeta);
|
|
212
|
+
if (parsed instanceof type.errors) {
|
|
213
|
+
throw new Error(`describe() returned invalid metadata: ${parsed.summary}`);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
const manifest = buildAttestedManifest(migrationDir, parsed, ops);
|
|
217
|
+
|
|
218
|
+
if (dryRun) {
|
|
219
|
+
process.stdout.write(`--- migration.json ---\n${JSON.stringify(manifest, null, 2)}\n`);
|
|
220
|
+
process.stdout.write('--- ops.json ---\n');
|
|
221
|
+
process.stdout.write(`${serializedOps}\n`);
|
|
222
|
+
return;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
writeFileSync(join(migrationDir, 'ops.json'), serializedOps);
|
|
226
|
+
writeFileSync(join(migrationDir, 'migration.json'), JSON.stringify(manifest, null, 2));
|
|
227
|
+
|
|
228
|
+
process.stdout.write(`Wrote ops.json + migration.json to ${migrationDir}\n`);
|
|
229
|
+
}
|
package/src/migration-ts.ts
CHANGED
|
@@ -1,153 +1,33 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Utilities for
|
|
2
|
+
* Utilities for reading/writing `migration.ts` files.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
4
|
+
* Rendering migration.ts source is now the target's responsibility — the CLI
|
|
5
|
+
* obtains source strings either from a class-flow planner's
|
|
6
|
+
* `plan.renderTypeScript()` or from a descriptor-flow target's
|
|
7
|
+
* `migrations.renderDescriptorTypeScript(descriptors, context)`. The helper
|
|
8
|
+
* here is limited to file I/O: writing the returned source with the right
|
|
9
|
+
* executable bit, probing for existence, and evaluating legacy descriptor-
|
|
10
|
+
* flow files.
|
|
9
11
|
*/
|
|
10
12
|
|
|
11
13
|
import { stat, writeFile } from 'node:fs/promises';
|
|
12
|
-
import
|
|
13
|
-
import { join, relative, resolve } from 'pathe';
|
|
14
|
+
import { join, resolve } from 'pathe';
|
|
14
15
|
|
|
15
16
|
const MIGRATION_TS_FILE = 'migration.ts';
|
|
16
17
|
|
|
17
18
|
/**
|
|
18
|
-
*
|
|
19
|
+
* Writes a pre-rendered `migration.ts` source string to the given package
|
|
20
|
+
* directory. If the source begins with a shebang, the file is written with
|
|
21
|
+
* executable permissions (0o755) so it can be run directly via
|
|
22
|
+
* `./migration.ts` by the authoring class's `Migration.run(...)` guard.
|
|
19
23
|
*/
|
|
20
|
-
export
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
function serializeQueryInput(input: unknown): string {
|
|
28
|
-
if (typeof input === 'boolean') return String(input);
|
|
29
|
-
if (typeof input === 'symbol') return 'TODO /* fill in using db.sql.from(...) */';
|
|
30
|
-
if (input === null || input === undefined) return 'null';
|
|
31
|
-
if (Array.isArray(input)) {
|
|
32
|
-
if (input.length === 0) return '[]';
|
|
33
|
-
if (input.every((item) => typeof item === 'symbol'))
|
|
34
|
-
return '[TODO /* fill in using db.sql.from(...) */]';
|
|
35
|
-
return `[${input.map(serializeQueryInput).join(', ')}]`;
|
|
36
|
-
}
|
|
37
|
-
return JSON.stringify(input);
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
function descriptorToBuilderCall(desc: OperationDescriptor): string {
|
|
41
|
-
switch (desc.kind) {
|
|
42
|
-
case 'createTable':
|
|
43
|
-
return `createTable(${JSON.stringify(desc['table'])})`;
|
|
44
|
-
case 'dropTable':
|
|
45
|
-
return `dropTable(${JSON.stringify(desc['table'])})`;
|
|
46
|
-
case 'addColumn': {
|
|
47
|
-
const args = [JSON.stringify(desc['table']), JSON.stringify(desc['column'])];
|
|
48
|
-
if (desc['overrides']) {
|
|
49
|
-
args.push(JSON.stringify(desc['overrides']));
|
|
50
|
-
}
|
|
51
|
-
return `addColumn(${args.join(', ')})`;
|
|
52
|
-
}
|
|
53
|
-
case 'dropColumn':
|
|
54
|
-
return `dropColumn(${JSON.stringify(desc['table'])}, ${JSON.stringify(desc['column'])})`;
|
|
55
|
-
case 'alterColumnType': {
|
|
56
|
-
const opts: Record<string, unknown> = {};
|
|
57
|
-
if (desc['using']) opts['using'] = desc['using'];
|
|
58
|
-
if (desc['toType']) opts['toType'] = desc['toType'];
|
|
59
|
-
const hasOpts = Object.keys(opts).length > 0;
|
|
60
|
-
return hasOpts
|
|
61
|
-
? `alterColumnType(${JSON.stringify(desc['table'])}, ${JSON.stringify(desc['column'])}, ${JSON.stringify(opts)})`
|
|
62
|
-
: `alterColumnType(${JSON.stringify(desc['table'])}, ${JSON.stringify(desc['column'])})`;
|
|
63
|
-
}
|
|
64
|
-
case 'setNotNull':
|
|
65
|
-
return `setNotNull(${JSON.stringify(desc['table'])}, ${JSON.stringify(desc['column'])})`;
|
|
66
|
-
case 'dropNotNull':
|
|
67
|
-
return `dropNotNull(${JSON.stringify(desc['table'])}, ${JSON.stringify(desc['column'])})`;
|
|
68
|
-
case 'setDefault':
|
|
69
|
-
return `setDefault(${JSON.stringify(desc['table'])}, ${JSON.stringify(desc['column'])})`;
|
|
70
|
-
case 'dropDefault':
|
|
71
|
-
return `dropDefault(${JSON.stringify(desc['table'])}, ${JSON.stringify(desc['column'])})`;
|
|
72
|
-
case 'addPrimaryKey':
|
|
73
|
-
return `addPrimaryKey(${JSON.stringify(desc['table'])})`;
|
|
74
|
-
case 'addUnique':
|
|
75
|
-
return `addUnique(${JSON.stringify(desc['table'])}, ${JSON.stringify(desc['columns'])})`;
|
|
76
|
-
case 'addForeignKey':
|
|
77
|
-
return `addForeignKey(${JSON.stringify(desc['table'])}, ${JSON.stringify(desc['columns'])})`;
|
|
78
|
-
case 'dropConstraint':
|
|
79
|
-
return `dropConstraint(${JSON.stringify(desc['table'])}, ${JSON.stringify(desc['constraintName'])})`;
|
|
80
|
-
case 'createIndex':
|
|
81
|
-
return `createIndex(${JSON.stringify(desc['table'])}, ${JSON.stringify(desc['columns'])})`;
|
|
82
|
-
case 'dropIndex':
|
|
83
|
-
return `dropIndex(${JSON.stringify(desc['table'])}, ${JSON.stringify(desc['indexName'])})`;
|
|
84
|
-
case 'createEnumType':
|
|
85
|
-
return desc['values']
|
|
86
|
-
? `createEnumType(${JSON.stringify(desc['typeName'])}, ${JSON.stringify(desc['values'])})`
|
|
87
|
-
: `createEnumType(${JSON.stringify(desc['typeName'])})`;
|
|
88
|
-
case 'addEnumValues':
|
|
89
|
-
return `addEnumValues(${JSON.stringify(desc['typeName'])}, ${JSON.stringify(desc['values'])})`;
|
|
90
|
-
case 'dropEnumType':
|
|
91
|
-
return `dropEnumType(${JSON.stringify(desc['typeName'])})`;
|
|
92
|
-
case 'renameType':
|
|
93
|
-
return `renameType(${JSON.stringify(desc['fromName'])}, ${JSON.stringify(desc['toName'])})`;
|
|
94
|
-
case 'createDependency':
|
|
95
|
-
return `createDependency(${JSON.stringify(desc['dependencyId'])})`;
|
|
96
|
-
case 'dataTransform':
|
|
97
|
-
return `dataTransform(${JSON.stringify(desc['name'])}, {\n check: ${serializeQueryInput(desc['check'])},\n run: ${serializeQueryInput(desc['run'])},\n })`;
|
|
98
|
-
default:
|
|
99
|
-
throw new Error(`Unknown descriptor kind: ${desc.kind}`);
|
|
100
|
-
}
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
/**
|
|
104
|
-
* Scaffolds a migration.ts file in the given package directory.
|
|
105
|
-
* Serializes operation descriptors as builder calls that the user can edit.
|
|
106
|
-
* On verify, this file is re-evaluated to produce the final ops.
|
|
107
|
-
*/
|
|
108
|
-
export async function scaffoldMigrationTs(
|
|
109
|
-
packageDir: string,
|
|
110
|
-
options: ScaffoldOptions = {},
|
|
111
|
-
): Promise<void> {
|
|
112
|
-
const filePath = join(packageDir, MIGRATION_TS_FILE);
|
|
113
|
-
|
|
114
|
-
const descriptors = options.descriptors ?? [];
|
|
115
|
-
const hasDataTransform = descriptors.some((d) => d.kind === 'dataTransform');
|
|
116
|
-
|
|
117
|
-
const lines: string[] = [];
|
|
118
|
-
|
|
119
|
-
if (hasDataTransform && options.contractJsonPath) {
|
|
120
|
-
const relativeContractDts = relative(packageDir, options.contractJsonPath).replace(
|
|
121
|
-
/\.json$/,
|
|
122
|
-
'.d',
|
|
123
|
-
);
|
|
124
|
-
lines.push(`import type { Contract } from "${relativeContractDts}"`);
|
|
125
|
-
lines.push(`import { createBuilders } from "@prisma-next/target-postgres/migration-builders"`);
|
|
126
|
-
lines.push('');
|
|
127
|
-
const importList = [...new Set(descriptors.map((d) => d.kind))];
|
|
128
|
-
importList.push('TODO');
|
|
129
|
-
lines.push(`const { ${importList.join(', ')} } = createBuilders<Contract>()`);
|
|
130
|
-
} else {
|
|
131
|
-
const importList = [...new Set(descriptors.map((d) => d.kind))];
|
|
132
|
-
if (importList.length === 0) {
|
|
133
|
-
importList.push('createTable');
|
|
134
|
-
}
|
|
135
|
-
if (hasDataTransform) {
|
|
136
|
-
importList.push('TODO');
|
|
137
|
-
}
|
|
138
|
-
lines.push(
|
|
139
|
-
`import { ${importList.join(', ')} } from "@prisma-next/target-postgres/migration-builders"`,
|
|
140
|
-
);
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
const calls = descriptors.map((d) => ` ${descriptorToBuilderCall(d)},`).join('\n');
|
|
144
|
-
const body = calls.length > 0 ? `\n${calls}\n` : '';
|
|
145
|
-
|
|
146
|
-
lines.push('');
|
|
147
|
-
lines.push(`export default () => [${body}]`);
|
|
148
|
-
lines.push('');
|
|
149
|
-
|
|
150
|
-
await writeFile(filePath, lines.join('\n'));
|
|
24
|
+
export async function writeMigrationTs(packageDir: string, content: string): Promise<void> {
|
|
25
|
+
const isExecutable = content.startsWith('#!');
|
|
26
|
+
await writeFile(
|
|
27
|
+
join(packageDir, MIGRATION_TS_FILE),
|
|
28
|
+
content,
|
|
29
|
+
isExecutable ? { mode: 0o755 } : undefined,
|
|
30
|
+
);
|
|
151
31
|
}
|
|
152
32
|
|
|
153
33
|
/**
|
|
@@ -163,9 +43,13 @@ export async function hasMigrationTs(packageDir: string): Promise<boolean> {
|
|
|
163
43
|
}
|
|
164
44
|
|
|
165
45
|
/**
|
|
166
|
-
* Evaluates a migration.ts file by loading it via native
|
|
167
|
-
* Returns the result of calling the default export (expected
|
|
168
|
-
* function returning an array of operation descriptors).
|
|
46
|
+
* Evaluates a descriptor-flow migration.ts file by loading it via native
|
|
47
|
+
* Node import. Returns the result of calling the default export (expected
|
|
48
|
+
* to be a function returning an array of operation descriptors).
|
|
49
|
+
*
|
|
50
|
+
* Class-flow migration.ts files use a different shape — their default
|
|
51
|
+
* export is a class that extends `Migration` — and are evaluated by the
|
|
52
|
+
* target's `emit` capability, not this helper.
|
|
169
53
|
*
|
|
170
54
|
* Requires Node ≥24 for native TypeScript support.
|
|
171
55
|
*/
|
|
@@ -178,7 +62,6 @@ export async function evaluateMigrationTs(packageDir: string): Promise<readonly
|
|
|
178
62
|
throw new Error(`migration.ts not found at "${filePath}"`);
|
|
179
63
|
}
|
|
180
64
|
|
|
181
|
-
// Use native Node TS import (Node ≥24, stable type stripping)
|
|
182
65
|
const mod = (await import(filePath)) as { default?: unknown };
|
|
183
66
|
|
|
184
67
|
if (typeof mod.default !== 'function') {
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export type ScaffoldRuntime = 'node' | 'bun' | 'deno';
|
|
2
|
+
|
|
3
|
+
export function detectScaffoldRuntime(): ScaffoldRuntime {
|
|
4
|
+
if (typeof (globalThis as { Bun?: unknown }).Bun !== 'undefined') return 'bun';
|
|
5
|
+
if (typeof (globalThis as { Deno?: unknown }).Deno !== 'undefined') return 'deno';
|
|
6
|
+
return 'node';
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function shebangLineFor(runtime: ScaffoldRuntime): string {
|
|
10
|
+
switch (runtime) {
|
|
11
|
+
case 'bun':
|
|
12
|
+
return '#!/usr/bin/env -S bun';
|
|
13
|
+
case 'deno':
|
|
14
|
+
return '#!/usr/bin/env -S deno run -A';
|
|
15
|
+
case 'node':
|
|
16
|
+
return '#!/usr/bin/env -S node';
|
|
17
|
+
}
|
|
18
|
+
}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"attestation.mjs","names":["sorted: Record<string, unknown>"],"sources":["../../src/canonicalize-json.ts","../../src/attestation.ts"],"sourcesContent":["function sortKeys(value: unknown): unknown {\n if (value === null || typeof value !== 'object') {\n return value;\n }\n if (Array.isArray(value)) {\n return value.map(sortKeys);\n }\n const sorted: Record<string, unknown> = {};\n for (const key of Object.keys(value).sort()) {\n sorted[key] = sortKeys((value as Record<string, unknown>)[key]);\n }\n return sorted;\n}\n\nexport function canonicalizeJson(value: unknown): string {\n return JSON.stringify(sortKeys(value));\n}\n","import { createHash } from 'node:crypto';\nimport { canonicalizeContract } from '@prisma-next/contract/hashing';\nimport { canonicalizeJson } from './canonicalize-json';\nimport { readMigrationPackage, writeMigrationManifest } from './io';\nimport type { MigrationManifest, MigrationOps } from './types';\n\nexport interface VerifyResult {\n readonly ok: boolean;\n readonly reason?: 'draft' | 'mismatch';\n readonly storedMigrationId?: string;\n readonly computedMigrationId?: string;\n}\n\nfunction sha256Hex(input: string): string {\n return createHash('sha256').update(input).digest('hex');\n}\n\nexport function computeMigrationId(manifest: MigrationManifest, ops: MigrationOps): string {\n const {\n migrationId: _migrationId,\n signature: _signature,\n fromContract: _fromContract,\n toContract: _toContract,\n ...strippedMeta\n } = manifest;\n\n const canonicalManifest = canonicalizeJson(strippedMeta);\n const canonicalOps = canonicalizeJson(ops);\n\n const canonicalFromContract =\n manifest.fromContract !== null ? canonicalizeContract(manifest.fromContract) : 'null';\n const canonicalToContract = canonicalizeContract(manifest.toContract);\n\n const partHashes = [\n canonicalManifest,\n canonicalOps,\n canonicalFromContract,\n canonicalToContract,\n ].map(sha256Hex);\n const hash = sha256Hex(canonicalizeJson(partHashes));\n\n return `sha256:${hash}`;\n}\n\nexport async function attestMigration(dir: string): Promise<string> {\n const pkg = await readMigrationPackage(dir);\n const migrationId = computeMigrationId(pkg.manifest, pkg.ops);\n\n const updated = { ...pkg.manifest, migrationId };\n await writeMigrationManifest(dir, updated);\n\n return migrationId;\n}\n\nexport async function verifyMigration(dir: string): Promise<VerifyResult> {\n const pkg = await readMigrationPackage(dir);\n\n if (pkg.manifest.migrationId === null) {\n return { ok: false, reason: 'draft' };\n }\n\n const computed = computeMigrationId(pkg.manifest, pkg.ops);\n\n if (pkg.manifest.migrationId === computed) {\n return { ok: true, storedMigrationId: pkg.manifest.migrationId, computedMigrationId: computed };\n }\n\n return {\n ok: false,\n reason: 'mismatch',\n storedMigrationId: pkg.manifest.migrationId,\n computedMigrationId: computed,\n };\n}\n"],"mappings":";;;;;AAAA,SAAS,SAAS,OAAyB;AACzC,KAAI,UAAU,QAAQ,OAAO,UAAU,SACrC,QAAO;AAET,KAAI,MAAM,QAAQ,MAAM,CACtB,QAAO,MAAM,IAAI,SAAS;CAE5B,MAAMA,SAAkC,EAAE;AAC1C,MAAK,MAAM,OAAO,OAAO,KAAK,MAAM,CAAC,MAAM,CACzC,QAAO,OAAO,SAAU,MAAkC,KAAK;AAEjE,QAAO;;AAGT,SAAgB,iBAAiB,OAAwB;AACvD,QAAO,KAAK,UAAU,SAAS,MAAM,CAAC;;;;;ACFxC,SAAS,UAAU,OAAuB;AACxC,QAAO,WAAW,SAAS,CAAC,OAAO,MAAM,CAAC,OAAO,MAAM;;AAGzD,SAAgB,mBAAmB,UAA6B,KAA2B;CACzF,MAAM,EACJ,aAAa,cACb,WAAW,YACX,cAAc,eACd,YAAY,aACZ,GAAG,iBACD;AAiBJ,QAAO,UAFM,UAAU,iBANJ;EAPO,iBAAiB,aAAa;EACnC,iBAAiB,IAAI;EAGxC,SAAS,iBAAiB,OAAO,qBAAqB,SAAS,aAAa,GAAG;EACrD,qBAAqB,SAAS,WAAW;EAOpE,CAAC,IAAI,UAAU,CACmC,CAAC;;AAKtD,eAAsB,gBAAgB,KAA8B;CAClE,MAAM,MAAM,MAAM,qBAAqB,IAAI;CAC3C,MAAM,cAAc,mBAAmB,IAAI,UAAU,IAAI,IAAI;AAG7D,OAAM,uBAAuB,KADb;EAAE,GAAG,IAAI;EAAU;EAAa,CACN;AAE1C,QAAO;;AAGT,eAAsB,gBAAgB,KAAoC;CACxE,MAAM,MAAM,MAAM,qBAAqB,IAAI;AAE3C,KAAI,IAAI,SAAS,gBAAgB,KAC/B,QAAO;EAAE,IAAI;EAAO,QAAQ;EAAS;CAGvC,MAAM,WAAW,mBAAmB,IAAI,UAAU,IAAI,IAAI;AAE1D,KAAI,IAAI,SAAS,gBAAgB,SAC/B,QAAO;EAAE,IAAI;EAAM,mBAAmB,IAAI,SAAS;EAAa,qBAAqB;EAAU;AAGjG,QAAO;EACL,IAAI;EACJ,QAAQ;EACR,mBAAmB,IAAI,SAAS;EAChC,qBAAqB;EACtB"}
|
package/dist/io-LsuurzNb.mjs.map
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"io-LsuurzNb.mjs","names":["manifestRaw: string","opsRaw: string","manifest: MigrationManifest","ops: MigrationOps","entries: string[]","packages: BaseMigrationBundle[]"],"sources":["../src/io.ts"],"sourcesContent":["import { mkdir, readdir, readFile, stat, writeFile } from 'node:fs/promises';\nimport { type } from 'arktype';\nimport { basename, dirname, join } from 'pathe';\nimport {\n errorDirectoryExists,\n errorInvalidJson,\n errorInvalidManifest,\n errorInvalidSlug,\n errorMissingFile,\n} from './errors';\nimport type { BaseMigrationBundle, MigrationManifest, MigrationOps } from './types';\n\nconst MANIFEST_FILE = 'migration.json';\nconst OPS_FILE = 'ops.json';\nconst MAX_SLUG_LENGTH = 64;\n\nfunction hasErrnoCode(error: unknown, code: string): boolean {\n return error instanceof Error && (error as { code?: string }).code === code;\n}\n\nconst MigrationHintsSchema = type({\n used: 'string[]',\n applied: 'string[]',\n plannerVersion: 'string',\n planningStrategy: 'string',\n});\n\nconst MigrationManifestSchema = type({\n from: 'string',\n to: 'string',\n migrationId: 'string | null',\n kind: \"'regular' | 'baseline'\",\n fromContract: 'object | null',\n toContract: 'object',\n hints: MigrationHintsSchema,\n labels: 'string[]',\n 'authorship?': type({\n 'author?': 'string',\n 'email?': 'string',\n }),\n 'signature?': type({\n keyId: 'string',\n value: 'string',\n }).or('null'),\n createdAt: 'string',\n});\n\nconst MigrationOpSchema = type({\n id: 'string',\n label: 'string',\n operationClass: \"'additive' | 'widening' | 'destructive' | 'data'\",\n});\n\n// Intentionally shallow: operation-specific payload validation is owned by planner/runner layers.\nconst MigrationOpsSchema = MigrationOpSchema.array();\n\nexport async function writeMigrationPackage(\n dir: string,\n manifest: MigrationManifest,\n ops: MigrationOps,\n): Promise<void> {\n await mkdir(dirname(dir), { recursive: true });\n\n try {\n await mkdir(dir);\n } catch (error) {\n if (hasErrnoCode(error, 'EEXIST')) {\n throw errorDirectoryExists(dir);\n }\n throw error;\n }\n\n await writeFile(join(dir, MANIFEST_FILE), JSON.stringify(manifest, null, 2), { flag: 'wx' });\n await writeFile(join(dir, OPS_FILE), JSON.stringify(ops, null, 2), { flag: 'wx' });\n}\n\nexport async function writeMigrationManifest(\n dir: string,\n manifest: MigrationManifest,\n): Promise<void> {\n await writeFile(join(dir, MANIFEST_FILE), `${JSON.stringify(manifest, null, 2)}\\n`);\n}\n\nexport async function writeMigrationOps(dir: string, ops: MigrationOps): Promise<void> {\n await writeFile(join(dir, OPS_FILE), `${JSON.stringify(ops, null, 2)}\\n`);\n}\n\nexport async function readMigrationPackage(dir: string): Promise<BaseMigrationBundle> {\n const manifestPath = join(dir, MANIFEST_FILE);\n const opsPath = join(dir, OPS_FILE);\n\n let manifestRaw: string;\n try {\n manifestRaw = await readFile(manifestPath, 'utf-8');\n } catch (error) {\n if (hasErrnoCode(error, 'ENOENT')) {\n throw errorMissingFile(MANIFEST_FILE, dir);\n }\n throw error;\n }\n\n let opsRaw: string;\n try {\n opsRaw = await readFile(opsPath, 'utf-8');\n } catch (error) {\n if (hasErrnoCode(error, 'ENOENT')) {\n throw errorMissingFile(OPS_FILE, dir);\n }\n throw error;\n }\n\n let manifest: MigrationManifest;\n try {\n manifest = JSON.parse(manifestRaw);\n } catch (e) {\n throw errorInvalidJson(manifestPath, e instanceof Error ? e.message : String(e));\n }\n\n let ops: MigrationOps;\n try {\n ops = JSON.parse(opsRaw);\n } catch (e) {\n throw errorInvalidJson(opsPath, e instanceof Error ? e.message : String(e));\n }\n\n validateManifest(manifest, manifestPath);\n validateOps(ops, opsPath);\n\n return {\n dirName: basename(dir),\n dirPath: dir,\n manifest,\n ops,\n };\n}\n\nfunction validateManifest(\n manifest: unknown,\n filePath: string,\n): asserts manifest is MigrationManifest {\n const result = MigrationManifestSchema(manifest);\n if (result instanceof type.errors) {\n throw errorInvalidManifest(filePath, result.summary);\n }\n}\n\nfunction validateOps(ops: unknown, filePath: string): asserts ops is MigrationOps {\n const result = MigrationOpsSchema(ops);\n if (result instanceof type.errors) {\n throw errorInvalidManifest(filePath, result.summary);\n }\n}\n\nexport async function readMigrationsDir(\n migrationsRoot: string,\n): Promise<readonly BaseMigrationBundle[]> {\n let entries: string[];\n try {\n entries = await readdir(migrationsRoot);\n } catch (error) {\n if (hasErrnoCode(error, 'ENOENT')) {\n return [];\n }\n throw error;\n }\n\n const packages: BaseMigrationBundle[] = [];\n\n for (const entry of entries.sort()) {\n const entryPath = join(migrationsRoot, entry);\n const entryStat = await stat(entryPath);\n if (!entryStat.isDirectory()) continue;\n\n const manifestPath = join(entryPath, MANIFEST_FILE);\n try {\n await stat(manifestPath);\n } catch {\n continue; // skip non-migration directories\n }\n\n packages.push(await readMigrationPackage(entryPath));\n }\n\n return packages;\n}\n\nexport function formatMigrationDirName(timestamp: Date, slug: string): string {\n const sanitized = slug\n .toLowerCase()\n .replace(/[^a-z0-9]/g, '_')\n .replace(/_+/g, '_')\n .replace(/^_|_$/g, '');\n\n if (sanitized.length === 0) {\n throw errorInvalidSlug(slug);\n }\n\n const truncated = sanitized.slice(0, MAX_SLUG_LENGTH);\n\n const y = timestamp.getUTCFullYear();\n const mo = String(timestamp.getUTCMonth() + 1).padStart(2, '0');\n const d = String(timestamp.getUTCDate()).padStart(2, '0');\n const h = String(timestamp.getUTCHours()).padStart(2, '0');\n const mi = String(timestamp.getUTCMinutes()).padStart(2, '0');\n\n return `${y}${mo}${d}T${h}${mi}_${truncated}`;\n}\n"],"mappings":"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"}
|