@payload-auth-link/migrate 0.3.0 → 0.4.0
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 +6 -3
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +149 -101
- package/dist/cli.js.map +1 -1
- package/dist/codemods.d.ts +19 -8
- package/dist/codemods.d.ts.map +1 -1
- package/dist/codemods.js +440 -21
- package/dist/codemods.js.map +1 -1
- package/dist/doctor/database.d.ts +5 -1
- package/dist/doctor/database.d.ts.map +1 -1
- package/dist/doctor/database.js +98 -34
- package/dist/doctor/database.js.map +1 -1
- package/dist/doctor/plugins.d.ts +15 -0
- package/dist/doctor/plugins.d.ts.map +1 -1
- package/dist/doctor/plugins.js +43 -0
- package/dist/doctor/plugins.js.map +1 -1
- package/dist/doctor/run-doctor.d.ts +11 -0
- package/dist/doctor/run-doctor.d.ts.map +1 -1
- package/dist/doctor/run-doctor.js +66 -20
- package/dist/doctor/run-doctor.js.map +1 -1
- package/dist/doctor/source-scan.d.ts +5 -0
- package/dist/doctor/source-scan.d.ts.map +1 -1
- package/dist/doctor/source-scan.js +9 -0
- package/dist/doctor/source-scan.js.map +1 -1
- package/dist/doctor/types.d.ts +12 -0
- package/dist/doctor/types.d.ts.map +1 -1
- package/dist/exporters.d.ts +7 -0
- package/dist/exporters.d.ts.map +1 -1
- package/dist/exporters.js +36 -1
- package/dist/exporters.js.map +1 -1
- package/dist/importer/apply.d.ts +5 -0
- package/dist/importer/apply.d.ts.map +1 -1
- package/dist/importer/apply.js +61 -33
- package/dist/importer/apply.js.map +1 -1
- package/dist/importer/lifecycle.d.ts +15 -6
- package/dist/importer/lifecycle.d.ts.map +1 -1
- package/dist/importer/lifecycle.js +131 -39
- package/dist/importer/lifecycle.js.map +1 -1
- package/dist/importer/run-import.d.ts +8 -2
- package/dist/importer/run-import.d.ts.map +1 -1
- package/dist/importer/run-import.js +73 -21
- package/dist/importer/run-import.js.map +1 -1
- package/dist/importer/source.d.ts +47 -4
- package/dist/importer/source.d.ts.map +1 -1
- package/dist/importer/source.js +159 -27
- package/dist/importer/source.js.map +1 -1
- package/dist/importer/types.d.ts +33 -0
- package/dist/importer/types.d.ts.map +1 -1
- package/dist/importer/types.js +48 -0
- package/dist/importer/types.js.map +1 -1
- package/dist/index.d.ts +5 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +5 -3
- package/dist/index.js.map +1 -1
- package/package.json +8 -11
- package/src/cli.ts +153 -100
- package/src/codemods.ts +540 -36
- package/src/doctor/database.ts +129 -46
- package/src/doctor/plugins.ts +51 -0
- package/src/doctor/run-doctor.ts +90 -25
- package/src/doctor/source-scan.ts +10 -0
- package/src/doctor/types.ts +9 -0
- package/src/exporters.ts +46 -1
- package/src/importer/apply.ts +78 -32
- package/src/importer/lifecycle.ts +163 -43
- package/src/importer/run-import.ts +107 -22
- package/src/importer/source.ts +214 -30
- package/src/importer/types.ts +86 -0
- package/src/index.ts +25 -2
package/src/cli.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
#!/usr/bin/env bun
|
|
2
2
|
/**
|
|
3
|
-
* The `@payload-auth-link/migrate` CLI (issues #49–#59)
|
|
4
|
-
* read-only `doctor` (#52)
|
|
5
|
-
*
|
|
3
|
+
* The `@payload-auth-link/migrate` CLI (issues #49–#59): the
|
|
4
|
+
* read-only `doctor` (#52), the importer lifecycle (#50), the
|
|
5
|
+
* codemods (#53), and the JSONL exporter (#58).
|
|
6
6
|
*
|
|
7
7
|
* Exit codes: 0 clean, 1 blockers, 2 warnings only, 3 usage errors.
|
|
8
8
|
* Never logs credential material — connection strings print only in
|
|
@@ -61,19 +61,29 @@ function usage(): string {
|
|
|
61
61
|
|
|
62
62
|
Commands:
|
|
63
63
|
doctor --config ./src/payload.config.ts [--src ./src] [--json] [--database-url URL]
|
|
64
|
+
[--source-map '{"accounts":"user_accounts","sessions":"user_sessions"}']
|
|
64
65
|
export --database-url URL [--out file.jsonl]
|
|
66
|
+
[--tables users,accounts | '["users","accounts"]']
|
|
65
67
|
(lossless JSONL of every auth table — the off-ramp, #58)
|
|
66
68
|
codemod --src ./src [--write]
|
|
67
69
|
from-payload-auth --config ./src/payload.config.ts [--apply] [--sessions=carry|drop]
|
|
68
70
|
[--manifest ./payload-auth-link-migration.json] [--database-url URL] [--json]
|
|
71
|
+
[--source-map '{"accounts":"user_accounts","sessions":"user_sessions"}']
|
|
69
72
|
verify --config ./src/payload.config.ts [--database-url URL] [--json]
|
|
73
|
+
[--source-map '{"accounts":"user_accounts","sessions":"user_sessions"}']
|
|
70
74
|
rollback --config ./src/payload.config.ts [--manifest ...]
|
|
71
75
|
finalize [--manifest ...]
|
|
72
76
|
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
+
--source-map renames source models to your layout's tables (custom
|
|
78
|
+
slugs like user_accounts/user_sessions); a null value records a
|
|
79
|
+
model as not installed. Without it, a missing source table aborts
|
|
80
|
+
the run — nothing is silently read as empty.
|
|
81
|
+
|
|
82
|
+
doctor is read-only: it loads the config (an unloadable config is a
|
|
83
|
+
blocker, not a crash), inspects the users collection and Better
|
|
84
|
+
Auth plugins, scans sources for call sites that must migrate, and
|
|
85
|
+
(with a database URL) reports row counts, unverified users, and
|
|
86
|
+
orphans.
|
|
77
87
|
|
|
78
88
|
from-payload-auth is a DRY RUN by default: it prints planned row
|
|
79
89
|
counts per model, collisions, and anything unmappable. --apply
|
|
@@ -83,9 +93,9 @@ Commands:
|
|
|
83
93
|
rollback undoes exactly the manifest; finalize marks it read-only
|
|
84
94
|
so the legacy tables may be dropped later, in their own migration.
|
|
85
95
|
|
|
86
|
-
codemod rewrites the
|
|
87
|
-
(getPayloadAuth, getSession, toNextJsHandler
|
|
88
|
-
reports what needs a human. Dry run by default; --write rewrites.
|
|
96
|
+
codemod rewrites the three named payload-auth call-site patterns
|
|
97
|
+
(getPayloadAuth, payload.betterAuth.api.getSession, toNextJsHandler)
|
|
98
|
+
and reports what needs a human. Dry run by default; --write rewrites.
|
|
89
99
|
|
|
90
100
|
Exit codes: 0 clean, 1 blockers/mismatch, 2 warnings only, 3 usage errors.
|
|
91
101
|
`
|
|
@@ -105,7 +115,13 @@ function formatReport(report: DoctorReport): string {
|
|
|
105
115
|
lines.push(' none declared')
|
|
106
116
|
}
|
|
107
117
|
for (const finding of report.plugins) {
|
|
108
|
-
lines.push(
|
|
118
|
+
lines.push(
|
|
119
|
+
` - ${finding.id}: ${finding.status} — ${finding.detail}${
|
|
120
|
+
finding.files !== undefined && finding.files.length > 0
|
|
121
|
+
? ` (files: ${finding.files.join(', ')})`
|
|
122
|
+
: ''
|
|
123
|
+
}`
|
|
124
|
+
)
|
|
109
125
|
}
|
|
110
126
|
lines.push('')
|
|
111
127
|
lines.push('== users collection ==')
|
|
@@ -208,15 +224,127 @@ async function main(
|
|
|
208
224
|
return 3
|
|
209
225
|
}
|
|
210
226
|
if (importCommands.includes(command)) {
|
|
227
|
+
const databaseUrl =
|
|
228
|
+
typeof flags.get('database-url') === 'string'
|
|
229
|
+
? (flags.get('database-url') as string)
|
|
230
|
+
: (process.env.DATABASE_URL ?? '')
|
|
231
|
+
// export --database-url URL [--out file.jsonl]
|
|
232
|
+
// (no --config: it reads the source database directly, #58)
|
|
233
|
+
if (command === 'export') {
|
|
234
|
+
if (databaseUrl === '') {
|
|
235
|
+
err.write('export requires --database-url or DATABASE_URL\n')
|
|
236
|
+
return 3
|
|
237
|
+
}
|
|
238
|
+
const tablesFlag = flags.get('tables')
|
|
239
|
+
let tables: readonly string[] | undefined
|
|
240
|
+
if (typeof tablesFlag === 'string') {
|
|
241
|
+
try {
|
|
242
|
+
const { parseExportTables } = await import('./exporters.js')
|
|
243
|
+
tables = parseExportTables(tablesFlag)
|
|
244
|
+
} catch (error) {
|
|
245
|
+
err.write(`${error instanceof Error ? error.message : String(error)}\n`)
|
|
246
|
+
return 3
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
const { exportToJsonl } = await import('./exporters.js')
|
|
250
|
+
const result = await exportToJsonl({
|
|
251
|
+
connectionString: databaseUrl,
|
|
252
|
+
format: 'jsonl',
|
|
253
|
+
...(tables === undefined ? {} : { tables }),
|
|
254
|
+
log: (message) => out.write(`${message}\n`),
|
|
255
|
+
})
|
|
256
|
+
// A requested table that doesn't exist is an error, not a gap
|
|
257
|
+
// (issue #68): nothing is silently omitted.
|
|
258
|
+
if (tables !== undefined && result.missing.length > 0) {
|
|
259
|
+
for (const table of result.missing) {
|
|
260
|
+
err.write(`export: table '${table}' does not exist on the source\n`)
|
|
261
|
+
}
|
|
262
|
+
err.write('nothing silently omitted; fix --tables or drop it\n')
|
|
263
|
+
return 1
|
|
264
|
+
}
|
|
265
|
+
if (typeof flags.get('out') === 'string') {
|
|
266
|
+
writeFileSync(flags.get('out') as string, result.jsonl)
|
|
267
|
+
out.write(`wrote ${result.totalRows} row(s) to ${String(flags.get('out'))}\n`)
|
|
268
|
+
} else {
|
|
269
|
+
out.write(result.jsonl)
|
|
270
|
+
}
|
|
271
|
+
return 0
|
|
272
|
+
}
|
|
273
|
+
// codemod --src ./src [--write]
|
|
274
|
+
// (no --config: a pure source rewrite, #53)
|
|
275
|
+
if (command === 'codemod') {
|
|
276
|
+
const srcFlag = flags.get('src')
|
|
277
|
+
if (typeof srcFlag !== 'string' || srcFlag === '') {
|
|
278
|
+
err.write('codemod requires --src <directory>\n')
|
|
279
|
+
return 3
|
|
280
|
+
}
|
|
281
|
+
const srcDir = new URL(srcFlag, `file://${process.cwd()}/`).pathname
|
|
282
|
+
const write = flags.get('write') === true
|
|
283
|
+
const changed: string[] = []
|
|
284
|
+
const manual: string[] = []
|
|
285
|
+
const walk = (dir: string): void => {
|
|
286
|
+
let entries: string[]
|
|
287
|
+
try {
|
|
288
|
+
entries = readdirSync(dir)
|
|
289
|
+
} catch {
|
|
290
|
+
return
|
|
291
|
+
}
|
|
292
|
+
for (const entry of entries) {
|
|
293
|
+
if (['node_modules', 'dist', '.next'].includes(entry) || entry.startsWith('.')) {
|
|
294
|
+
continue
|
|
295
|
+
}
|
|
296
|
+
const full = join(dir, entry)
|
|
297
|
+
let stats
|
|
298
|
+
try {
|
|
299
|
+
stats = statSync(full)
|
|
300
|
+
} catch {
|
|
301
|
+
continue
|
|
302
|
+
}
|
|
303
|
+
if (stats.isDirectory()) {
|
|
304
|
+
walk(full)
|
|
305
|
+
} else if (['.ts', '.tsx', '.js', '.jsx', '.mts', '.mjs'].includes(extname(entry))) {
|
|
306
|
+
const before = readFileSync(full, 'utf8')
|
|
307
|
+
const result = applyCodemods(relative(srcDir, full), before)
|
|
308
|
+
if (result.changed) {
|
|
309
|
+
if (write) {
|
|
310
|
+
writeFileSync(full, result.after)
|
|
311
|
+
}
|
|
312
|
+
changed.push(relative(srcDir, full))
|
|
313
|
+
}
|
|
314
|
+
for (const site of result.manual) {
|
|
315
|
+
manual.push(`${relative(srcDir, full)}: ${site}`)
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
walk(srcDir)
|
|
321
|
+
out.write(
|
|
322
|
+
`${changed.length} file(s) ${write ? 'rewritten' : 'would be rewritten (dry run; pass --write)'}:\n` +
|
|
323
|
+
changed.map((f) => ` ${f}`).join('\n') +
|
|
324
|
+
(manual.length > 0
|
|
325
|
+
? `\nmanual follow-up:\n${manual.map((m) => ` ${m}`).join('\n')}`
|
|
326
|
+
: '') +
|
|
327
|
+
'\n'
|
|
328
|
+
)
|
|
329
|
+
return 0
|
|
330
|
+
}
|
|
331
|
+
// finalize [--manifest ...]
|
|
332
|
+
// (no --config: manifest-only; no Payload instance is loaded)
|
|
333
|
+
if (command === 'finalize') {
|
|
334
|
+
await runFinalize(
|
|
335
|
+
typeof flags.get('manifest') === 'string'
|
|
336
|
+
? { manifestPath: flags.get('manifest') as string }
|
|
337
|
+
: {}
|
|
338
|
+
)
|
|
339
|
+
return 0
|
|
340
|
+
}
|
|
341
|
+
// The remaining commands (from-payload-auth, verify, rollback) all
|
|
342
|
+
// load a Payload instance, so they need the config.
|
|
211
343
|
const configFlag = flags.get('config')
|
|
212
344
|
if (typeof configFlag !== 'string' || configFlag === '') {
|
|
213
345
|
err.write(`${command} requires --config <path to payload.config.ts>\n`)
|
|
214
346
|
return 3
|
|
215
347
|
}
|
|
216
|
-
const databaseUrl =
|
|
217
|
-
typeof flags.get('database-url') === 'string'
|
|
218
|
-
? (flags.get('database-url') as string)
|
|
219
|
-
: (process.env.DATABASE_URL ?? '')
|
|
220
348
|
if ((command === 'from-payload-auth' || command === 'verify') && databaseUrl === '') {
|
|
221
349
|
err.write(`${command} requires --database-url or DATABASE_URL\n`)
|
|
222
350
|
return 3
|
|
@@ -234,6 +362,9 @@ async function main(
|
|
|
234
362
|
...(typeof flags.get('slugs') === 'string'
|
|
235
363
|
? { slugsJson: flags.get('slugs') as string }
|
|
236
364
|
: {}),
|
|
365
|
+
...(typeof flags.get('source-map') === 'string'
|
|
366
|
+
? { sourceMapJson: flags.get('source-map') as string }
|
|
367
|
+
: {}),
|
|
237
368
|
json: flags.get('json') === true,
|
|
238
369
|
}
|
|
239
370
|
if (command === 'from-payload-auth') {
|
|
@@ -254,90 +385,6 @@ async function main(
|
|
|
254
385
|
await runRollback(importOptions)
|
|
255
386
|
return 0
|
|
256
387
|
}
|
|
257
|
-
if (command === 'finalize') {
|
|
258
|
-
await runFinalize(importOptions)
|
|
259
|
-
return 0
|
|
260
|
-
}
|
|
261
|
-
// export --database-url URL [--out file.jsonl]
|
|
262
|
-
if (command === 'export') {
|
|
263
|
-
if (databaseUrl === '') {
|
|
264
|
-
err.write('export requires --database-url or DATABASE_URL\n')
|
|
265
|
-
return 3
|
|
266
|
-
}
|
|
267
|
-
const { exportToJsonl } = await import('./exporters.js')
|
|
268
|
-
const result = await exportToJsonl({
|
|
269
|
-
connectionString: databaseUrl,
|
|
270
|
-
format: 'jsonl',
|
|
271
|
-
log: (message) => out.write(`${message}\n`),
|
|
272
|
-
})
|
|
273
|
-
if (typeof flags.get('out') === 'string') {
|
|
274
|
-
writeFileSync(flags.get('out') as string, result.jsonl)
|
|
275
|
-
out.write(`wrote ${result.totalRows} row(s) to ${String(flags.get('out'))}\n`)
|
|
276
|
-
} else {
|
|
277
|
-
out.write(result.jsonl)
|
|
278
|
-
}
|
|
279
|
-
return 0
|
|
280
|
-
}
|
|
281
|
-
// codemod --src ./src [--write]
|
|
282
|
-
const srcFlagC = flags.get('src')
|
|
283
|
-
if (typeof srcFlagC !== 'string' || srcFlagC === '') {
|
|
284
|
-
err.write('codemod requires --src <directory>\n')
|
|
285
|
-
return 3
|
|
286
|
-
}
|
|
287
|
-
const srcDir = new URL(srcFlagC, `file://${process.cwd()}/`).pathname
|
|
288
|
-
const write = flags.get('write') === true
|
|
289
|
-
const changed: string[] = []
|
|
290
|
-
const manual: string[] = []
|
|
291
|
-
const walk = (dir: string): void => {
|
|
292
|
-
let entries: string[]
|
|
293
|
-
try {
|
|
294
|
-
entries = readdirSync(dir)
|
|
295
|
-
} catch {
|
|
296
|
-
return
|
|
297
|
-
}
|
|
298
|
-
for (const entry of entries) {
|
|
299
|
-
if (['node_modules', 'dist', '.next'].includes(entry) || entry.startsWith('.')) {
|
|
300
|
-
continue
|
|
301
|
-
}
|
|
302
|
-
const full = join(dir, entry)
|
|
303
|
-
let stats
|
|
304
|
-
try {
|
|
305
|
-
stats = statSync(full)
|
|
306
|
-
} catch {
|
|
307
|
-
continue
|
|
308
|
-
}
|
|
309
|
-
if (stats.isDirectory()) {
|
|
310
|
-
walk(full)
|
|
311
|
-
} else if (['.ts', '.tsx', '.js', '.jsx', '.mts', '.mjs'].includes(extname(entry))) {
|
|
312
|
-
const before = readFileSync(full, 'utf8')
|
|
313
|
-
const result = applyCodemods(relative(srcDir, full), before)
|
|
314
|
-
if (result.changed) {
|
|
315
|
-
if (write) {
|
|
316
|
-
writeFileSync(full, before)
|
|
317
|
-
}
|
|
318
|
-
changed.push(relative(srcDir, full))
|
|
319
|
-
}
|
|
320
|
-
for (const site of result.manual) {
|
|
321
|
-
manual.push(`${relative(srcDir, full)}: ${site}`)
|
|
322
|
-
}
|
|
323
|
-
}
|
|
324
|
-
}
|
|
325
|
-
}
|
|
326
|
-
// Two passes: report first, then rewrite on --write (the rules are
|
|
327
|
-
// deterministic, so the report and the rewrite always agree).
|
|
328
|
-
walk(srcDir)
|
|
329
|
-
if (write && changed.length > 0) {
|
|
330
|
-
walk(srcDir)
|
|
331
|
-
}
|
|
332
|
-
out.write(
|
|
333
|
-
`${changed.length} file(s) ${write ? 'rewritten' : 'would be rewritten (dry run; pass --write)'}:\n` +
|
|
334
|
-
changed.map((f) => ` ${f}`).join('\n') +
|
|
335
|
-
(manual.length > 0
|
|
336
|
-
? `\nmanual follow-up:\n${manual.map((m) => ` ${m}`).join('\n')}`
|
|
337
|
-
: '') +
|
|
338
|
-
'\n'
|
|
339
|
-
)
|
|
340
|
-
return 0
|
|
341
388
|
}
|
|
342
389
|
const configPath = flags.get('config')
|
|
343
390
|
if (typeof configPath !== 'string' || configPath === '') {
|
|
@@ -353,6 +400,9 @@ async function main(
|
|
|
353
400
|
? { srcDir: new URL(srcFlag, `file://${process.cwd()}/`).pathname }
|
|
354
401
|
: {}),
|
|
355
402
|
...(typeof dbFlag === 'string' ? { databaseUrl: dbFlag } : {}),
|
|
403
|
+
...(typeof flags.get('source-map') === 'string'
|
|
404
|
+
? { sourceMapJson: flags.get('source-map') as string }
|
|
405
|
+
: {}),
|
|
356
406
|
})
|
|
357
407
|
if (flags.get('json') === true) {
|
|
358
408
|
out.write(`${JSON.stringify(report, null, 2)}\n`)
|
|
@@ -376,8 +426,11 @@ if (invokedDirectly) {
|
|
|
376
426
|
process.exitCode = code
|
|
377
427
|
})
|
|
378
428
|
.catch((error: unknown) => {
|
|
429
|
+
// Name the command that failed (issue #71) — 'doctor failed:' was
|
|
430
|
+
// wrong for every other subcommand.
|
|
431
|
+
const { command } = parseArgs(process.argv.slice(2))
|
|
379
432
|
process.stderr.write(
|
|
380
|
-
|
|
433
|
+
`${command ?? 'cli'} failed: ${error instanceof Error ? error.message : String(error)}\n`
|
|
381
434
|
)
|
|
382
435
|
process.exitCode = 1
|
|
383
436
|
})
|