@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.
Files changed (69) hide show
  1. package/README.md +6 -3
  2. package/dist/cli.d.ts.map +1 -1
  3. package/dist/cli.js +149 -101
  4. package/dist/cli.js.map +1 -1
  5. package/dist/codemods.d.ts +19 -8
  6. package/dist/codemods.d.ts.map +1 -1
  7. package/dist/codemods.js +440 -21
  8. package/dist/codemods.js.map +1 -1
  9. package/dist/doctor/database.d.ts +5 -1
  10. package/dist/doctor/database.d.ts.map +1 -1
  11. package/dist/doctor/database.js +98 -34
  12. package/dist/doctor/database.js.map +1 -1
  13. package/dist/doctor/plugins.d.ts +15 -0
  14. package/dist/doctor/plugins.d.ts.map +1 -1
  15. package/dist/doctor/plugins.js +43 -0
  16. package/dist/doctor/plugins.js.map +1 -1
  17. package/dist/doctor/run-doctor.d.ts +11 -0
  18. package/dist/doctor/run-doctor.d.ts.map +1 -1
  19. package/dist/doctor/run-doctor.js +66 -20
  20. package/dist/doctor/run-doctor.js.map +1 -1
  21. package/dist/doctor/source-scan.d.ts +5 -0
  22. package/dist/doctor/source-scan.d.ts.map +1 -1
  23. package/dist/doctor/source-scan.js +9 -0
  24. package/dist/doctor/source-scan.js.map +1 -1
  25. package/dist/doctor/types.d.ts +12 -0
  26. package/dist/doctor/types.d.ts.map +1 -1
  27. package/dist/exporters.d.ts +7 -0
  28. package/dist/exporters.d.ts.map +1 -1
  29. package/dist/exporters.js +36 -1
  30. package/dist/exporters.js.map +1 -1
  31. package/dist/importer/apply.d.ts +5 -0
  32. package/dist/importer/apply.d.ts.map +1 -1
  33. package/dist/importer/apply.js +61 -33
  34. package/dist/importer/apply.js.map +1 -1
  35. package/dist/importer/lifecycle.d.ts +15 -6
  36. package/dist/importer/lifecycle.d.ts.map +1 -1
  37. package/dist/importer/lifecycle.js +131 -39
  38. package/dist/importer/lifecycle.js.map +1 -1
  39. package/dist/importer/run-import.d.ts +8 -2
  40. package/dist/importer/run-import.d.ts.map +1 -1
  41. package/dist/importer/run-import.js +73 -21
  42. package/dist/importer/run-import.js.map +1 -1
  43. package/dist/importer/source.d.ts +47 -4
  44. package/dist/importer/source.d.ts.map +1 -1
  45. package/dist/importer/source.js +159 -27
  46. package/dist/importer/source.js.map +1 -1
  47. package/dist/importer/types.d.ts +33 -0
  48. package/dist/importer/types.d.ts.map +1 -1
  49. package/dist/importer/types.js +48 -0
  50. package/dist/importer/types.js.map +1 -1
  51. package/dist/index.d.ts +5 -3
  52. package/dist/index.d.ts.map +1 -1
  53. package/dist/index.js +5 -3
  54. package/dist/index.js.map +1 -1
  55. package/package.json +8 -11
  56. package/src/cli.ts +153 -100
  57. package/src/codemods.ts +540 -36
  58. package/src/doctor/database.ts +129 -46
  59. package/src/doctor/plugins.ts +51 -0
  60. package/src/doctor/run-doctor.ts +90 -25
  61. package/src/doctor/source-scan.ts +10 -0
  62. package/src/doctor/types.ts +9 -0
  63. package/src/exporters.ts +46 -1
  64. package/src/importer/apply.ts +78 -32
  65. package/src/importer/lifecycle.ts +163 -43
  66. package/src/importer/run-import.ts +107 -22
  67. package/src/importer/source.ts +214 -30
  68. package/src/importer/types.ts +86 -0
  69. 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). Today: the
4
- * read-only `doctor` (#52). The importer and exporters ride the same
5
- * entrypoint as they land (#50, #58).
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
- doctor is read-only: it loads the config, inspects the users
74
- collection and Better Auth plugins, scans sources for call sites
75
- that must migrate, and (with a database URL) reports row counts,
76
- unverified users, and orphans.
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 four named payload-auth call-site patterns
87
- (getPayloadAuth, getSession, toNextJsHandler, importMap entries) and
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(` - ${finding.id}: ${finding.status} — ${finding.detail}`)
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
- `doctor failed: ${error instanceof Error ? error.message : String(error)}\n`
433
+ `${command ?? 'cli'} failed: ${error instanceof Error ? error.message : String(error)}\n`
381
434
  )
382
435
  process.exitCode = 1
383
436
  })