@payload-auth-link/migrate 0.4.2 → 0.5.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/LICENSE +21 -0
- package/README.md +7 -1
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +103 -19
- package/dist/cli.js.map +1 -1
- package/dist/doctor/database.d.ts.map +1 -1
- package/dist/doctor/database.js +31 -4
- package/dist/doctor/database.js.map +1 -1
- package/dist/doctor/run-doctor.d.ts.map +1 -1
- package/dist/doctor/run-doctor.js +67 -0
- package/dist/doctor/run-doctor.js.map +1 -1
- package/dist/doctor/types.d.ts +12 -0
- package/dist/doctor/types.d.ts.map +1 -1
- package/dist/doctor/users.d.ts.map +1 -1
- package/dist/doctor/users.js +20 -4
- package/dist/doctor/users.js.map +1 -1
- package/dist/importer/apply.d.ts +23 -5
- package/dist/importer/apply.d.ts.map +1 -1
- package/dist/importer/apply.js +154 -178
- package/dist/importer/apply.js.map +1 -1
- package/dist/importer/host-writes.d.ts +47 -0
- package/dist/importer/host-writes.d.ts.map +1 -0
- package/dist/importer/host-writes.js +198 -0
- package/dist/importer/host-writes.js.map +1 -0
- package/dist/importer/lifecycle.d.ts +24 -0
- package/dist/importer/lifecycle.d.ts.map +1 -1
- package/dist/importer/lifecycle.js +104 -22
- package/dist/importer/lifecycle.js.map +1 -1
- package/dist/importer/prelink.d.ts +55 -0
- package/dist/importer/prelink.d.ts.map +1 -0
- package/dist/importer/prelink.js +152 -0
- package/dist/importer/prelink.js.map +1 -0
- package/dist/importer/run-import.d.ts +13 -0
- package/dist/importer/run-import.d.ts.map +1 -1
- package/dist/importer/run-import.js +147 -14
- package/dist/importer/run-import.js.map +1 -1
- package/dist/importer/source.d.ts +35 -7
- package/dist/importer/source.d.ts.map +1 -1
- package/dist/importer/source.js +37 -6
- package/dist/importer/source.js.map +1 -1
- package/dist/importer/types.d.ts +26 -4
- package/dist/importer/types.d.ts.map +1 -1
- package/dist/importer/types.js.map +1 -1
- package/dist/index.d.ts +5 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -2
- package/dist/index.js.map +1 -1
- package/dist/private-file.d.ts +8 -0
- package/dist/private-file.d.ts.map +1 -0
- package/dist/private-file.js +19 -0
- package/dist/private-file.js.map +1 -0
- package/package.json +6 -6
- package/src/cli.ts +111 -9
- package/src/doctor/database.ts +36 -4
- package/src/doctor/run-doctor.ts +69 -0
- package/src/doctor/types.ts +12 -0
- package/src/doctor/users.ts +23 -4
- package/src/importer/apply.ts +186 -180
- package/src/importer/host-writes.ts +231 -0
- package/src/importer/lifecycle.ts +126 -24
- package/src/importer/prelink.ts +228 -0
- package/src/importer/run-import.ts +196 -29
- package/src/importer/source.ts +72 -14
- package/src/importer/types.ts +27 -4
- package/src/index.ts +13 -1
- package/src/private-file.ts +18 -0
package/src/importer/apply.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { Payload } from 'payload'
|
|
2
|
+
import { capturedKeys, captureBefore } from './host-writes.js'
|
|
2
3
|
import {
|
|
3
4
|
readFlatRows,
|
|
4
5
|
openSource,
|
|
@@ -17,6 +18,7 @@ import {
|
|
|
17
18
|
type ImportPlan,
|
|
18
19
|
type ImportResult,
|
|
19
20
|
type PlanRow,
|
|
21
|
+
type Unmappable,
|
|
20
22
|
} from './types.js'
|
|
21
23
|
|
|
22
24
|
/**
|
|
@@ -25,16 +27,26 @@ import {
|
|
|
25
27
|
*
|
|
26
28
|
* - Password hashes, OAuth tokens, API-key hashes, and jwks carry over
|
|
27
29
|
* VERBATIM (payload-auth used Better Auth's own hashing).
|
|
28
|
-
* -
|
|
29
|
-
*
|
|
30
|
+
* - Row timestamps (`createdAt`/`updatedAt`) carry from the source rows
|
|
31
|
+
* for every model whose source table has them (issue #88).
|
|
32
|
+
* - Better Auth plugin user state carries (issue #88): admin-plugin
|
|
33
|
+
* `banned`/`banReason`/`banExpires`/`role` and the anonymous plugin's
|
|
34
|
+
* `isAnonymous`, whenever the source has the column AND the target
|
|
35
|
+
* `ba-users` collection declares the field. A banned or anonymous
|
|
36
|
+
* source user whose state the target cannot hold is UNMAPPABLE — the
|
|
37
|
+
* run aborts before writing; a banned user never imports unbanned.
|
|
38
|
+
* - `linkable` + `linkableEmail` are granted from VERIFIED emails only
|
|
39
|
+
* (grant-only: an existing proof is never revoked); unverified users
|
|
40
|
+
* are reported by the plan, never linked.
|
|
30
41
|
* - Sessions default to `carry` so nobody is logged out: the carried
|
|
31
42
|
* rows keep their tokens, and with the same `advanced.cookiePrefix`
|
|
32
43
|
* the existing cookies keep working.
|
|
33
44
|
* - Idempotent and resumable: every target row carries a deterministic
|
|
34
45
|
* `baId` derived from the source id (`acct-<source id>`), so a
|
|
35
46
|
* re-run after a crash finds its own earlier rows and skips them.
|
|
36
|
-
* - Every created row and every field write lands in the
|
|
37
|
-
*
|
|
47
|
+
* - Every created row and every host field write lands in the
|
|
48
|
+
* manifest — the FIRST captured prior value per field wins, sealed —
|
|
49
|
+
* so `rollback` is exact and the source tables stay untouched.
|
|
38
50
|
*/
|
|
39
51
|
|
|
40
52
|
export type ApplyOptions = {
|
|
@@ -76,18 +88,96 @@ function numberOrNull(value: unknown): number | undefined {
|
|
|
76
88
|
return Number.isFinite(numeric) ? numeric : undefined
|
|
77
89
|
}
|
|
78
90
|
|
|
91
|
+
/** The source row's timestamps, when its layout has them (issue #88). */
|
|
92
|
+
function stamps(row: SourceRow): { createdAt?: string; updatedAt?: string } {
|
|
93
|
+
return {
|
|
94
|
+
...(row.created_at instanceof Date ? { createdAt: row.created_at.toISOString() } : {}),
|
|
95
|
+
...(row.updated_at instanceof Date ? { updatedAt: row.updated_at.toISOString() } : {}),
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const timestampColumns = ['created_at', 'updated_at'] as const
|
|
100
|
+
|
|
79
101
|
/**
|
|
80
102
|
* Rows for one mapped model. A `null` mapping records the model as
|
|
81
103
|
* not installed (issue #68) — no read, no rows; anything else goes
|
|
82
104
|
* through the strict `readFlatRows` (a missing mapped table throws;
|
|
83
|
-
* the probe aborts the run first).
|
|
105
|
+
* the probe aborts the run first). Row timestamps are read when the
|
|
106
|
+
* layout has them.
|
|
84
107
|
*/
|
|
85
108
|
async function readMapped(
|
|
86
109
|
client: Awaited<ReturnType<typeof openSource>>,
|
|
87
110
|
table: string | null,
|
|
88
111
|
columns: readonly string[]
|
|
89
112
|
): Promise<SourceRow[]> {
|
|
90
|
-
return table === null ? [] : readFlatRows(client, table, columns)
|
|
113
|
+
return table === null ? [] : readFlatRows(client, table, columns, timestampColumns)
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/** Field names a target collection declares (top level, flattened). */
|
|
117
|
+
function targetFieldNames(payload: Payload, slug: string): ReadonlySet<string> {
|
|
118
|
+
const collection = payload.collections[slug]
|
|
119
|
+
const names = new Set<string>()
|
|
120
|
+
for (const field of collection?.config.flattenedFields ?? []) {
|
|
121
|
+
names.add(field.name)
|
|
122
|
+
}
|
|
123
|
+
return names
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Plugin user state the target must be able to hold (issue #88),
|
|
128
|
+
* checked BEFORE any write: a banned user whose target has no `banned`
|
|
129
|
+
* field would import unbanned; an anonymous user without `isAnonymous`
|
|
130
|
+
* would import as a full account.
|
|
131
|
+
*/
|
|
132
|
+
export function pluginStateUnmappable(
|
|
133
|
+
users: readonly SourceUser[],
|
|
134
|
+
targetFields: ReadonlySet<string>,
|
|
135
|
+
targetSlug: string
|
|
136
|
+
): Unmappable[] {
|
|
137
|
+
const found: Unmappable[] = []
|
|
138
|
+
for (const user of users) {
|
|
139
|
+
if (user.banned === true && !targetFields.has('banned')) {
|
|
140
|
+
found.push({
|
|
141
|
+
model: 'user',
|
|
142
|
+
key: user.id,
|
|
143
|
+
reason: `user #${user.id} is banned on the source, but '${targetSlug}' declares no 'banned' field — add Better Auth's admin() plugin to the engine (autoDeclare derives banned/banReason/banExpires) so the ban carries`,
|
|
144
|
+
})
|
|
145
|
+
}
|
|
146
|
+
if (user.isAnonymous === true && !targetFields.has('isAnonymous')) {
|
|
147
|
+
found.push({
|
|
148
|
+
model: 'user',
|
|
149
|
+
key: user.id,
|
|
150
|
+
reason: `user #${user.id} is anonymous on the source, but '${targetSlug}' declares no 'isAnonymous' field — add Better Auth's anonymous() plugin to the engine, or remove anonymous users before importing`,
|
|
151
|
+
})
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
return found
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/** Plugin user state carried onto the ba-users row (issue #88). */
|
|
158
|
+
function pluginUserState(
|
|
159
|
+
user: SourceUser,
|
|
160
|
+
targetFields: ReadonlySet<string>
|
|
161
|
+
): Record<string, unknown> {
|
|
162
|
+
const state: Record<string, unknown> = {}
|
|
163
|
+
if (user.banned !== null && targetFields.has('banned')) {
|
|
164
|
+
state.banned = user.banned
|
|
165
|
+
}
|
|
166
|
+
if (user.banReason !== null && targetFields.has('banReason')) {
|
|
167
|
+
state.banReason = user.banReason
|
|
168
|
+
}
|
|
169
|
+
if (user.banExpires !== null && targetFields.has('banExpires')) {
|
|
170
|
+
state.banExpires = user.banExpires.toISOString()
|
|
171
|
+
}
|
|
172
|
+
if (user.isAnonymous !== null && targetFields.has('isAnonymous')) {
|
|
173
|
+
state.isAnonymous = user.isAnonymous
|
|
174
|
+
}
|
|
175
|
+
// Better Auth's admin plugin stores roles as one comma-separated
|
|
176
|
+
// string; payload-auth kept them in the users_role child table.
|
|
177
|
+
if (user.roles.length > 0 && targetFields.has('role')) {
|
|
178
|
+
state.role = user.roles.join(',')
|
|
179
|
+
}
|
|
180
|
+
return state
|
|
91
181
|
}
|
|
92
182
|
|
|
93
183
|
/** Find an existing target row by `baId` — the resumable dedupe key. */
|
|
@@ -129,24 +219,6 @@ async function createDeduped(
|
|
|
129
219
|
return true
|
|
130
220
|
}
|
|
131
221
|
|
|
132
|
-
/** Capture a host field's current value for the rollback manifest. */
|
|
133
|
-
async function captureBefore(
|
|
134
|
-
payload: Payload,
|
|
135
|
-
hostId: number,
|
|
136
|
-
field: string
|
|
137
|
-
): Promise<string | null> {
|
|
138
|
-
const doc: unknown = await payload.findByID({
|
|
139
|
-
collection: 'users',
|
|
140
|
-
id: hostId,
|
|
141
|
-
depth: 0,
|
|
142
|
-
overrideAccess: true,
|
|
143
|
-
})
|
|
144
|
-
if (typeof doc !== 'object' || doc === null || !(field in doc)) {
|
|
145
|
-
return null
|
|
146
|
-
}
|
|
147
|
-
return JSON.stringify((doc as Record<string, unknown>)[field] ?? null)
|
|
148
|
-
}
|
|
149
|
-
|
|
150
222
|
/**
|
|
151
223
|
* The identity rows pre-populated on the host user (issue #69): ONE
|
|
152
224
|
* `better-auth` identity per imported user, `providerUserId` set to
|
|
@@ -192,9 +264,14 @@ export async function applyImport(options: ApplyOptions): Promise<ImportResult>
|
|
|
192
264
|
const sourceTables: ResolvedSourceTables = resolveSourceTables(options.sourceTables)
|
|
193
265
|
const created: CreatedRow[] = [...(options.manifest?.created ?? [])]
|
|
194
266
|
const fieldWrites: FieldWrite[] = [...(options.manifest?.fieldWrites ?? [])]
|
|
195
|
-
|
|
267
|
+
// First capture wins (issue #88): fields captured by an earlier pass
|
|
268
|
+
// keep their pre-import value.
|
|
269
|
+
const captured = capturedKeys(fieldWrites)
|
|
270
|
+
// Per-PASS totals (issue #88): a re-apply reports what this pass
|
|
271
|
+
// planned, never the sum of every pass.
|
|
272
|
+
const totals: Record<string, number> = {}
|
|
196
273
|
const planRows: PlanRow[] = []
|
|
197
|
-
const unmappable:
|
|
274
|
+
const unmappable: Unmappable[] = []
|
|
198
275
|
let createdCount = 0
|
|
199
276
|
let skippedCount = 0
|
|
200
277
|
let fieldWriteCount = 0
|
|
@@ -206,6 +283,24 @@ export async function applyImport(options: ApplyOptions): Promise<ImportResult>
|
|
|
206
283
|
const client = await openSource(sourceConnectionString)
|
|
207
284
|
try {
|
|
208
285
|
const users = await readSourceUsers(client, sourceTables)
|
|
286
|
+
const baUserFields = targetFieldNames(payload, slugs.user)
|
|
287
|
+
|
|
288
|
+
// --- 0. target-capability gate (issue #88) ---------------------------
|
|
289
|
+
// Runs before ANY write: plugin state the target cannot hold aborts
|
|
290
|
+
// the run instead of importing a banned user unbanned.
|
|
291
|
+
const blocked = pluginStateUnmappable(users, baUserFields, slugs.user)
|
|
292
|
+
if (blocked.length > 0) {
|
|
293
|
+
log(
|
|
294
|
+
`ABORT: ${blocked.length} user(s) carry state the target cannot hold; nothing was written`
|
|
295
|
+
)
|
|
296
|
+
return {
|
|
297
|
+
created: 0,
|
|
298
|
+
skipped: 0,
|
|
299
|
+
fieldWrites: 0,
|
|
300
|
+
plan: { rows: [], unmappable: blocked, sessions, warnings: [] },
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
|
|
209
304
|
const seenUsers = new Set<string>()
|
|
210
305
|
|
|
211
306
|
// --- 1. ba-users + host field writes ---------------------------------
|
|
@@ -227,6 +322,7 @@ export async function applyImport(options: ApplyOptions): Promise<ImportResult>
|
|
|
227
322
|
name: user.name ?? '',
|
|
228
323
|
emailVerified: user.emailVerified,
|
|
229
324
|
...(user.image === null ? {} : { image: user.image }),
|
|
325
|
+
...pluginUserState(user, baUserFields),
|
|
230
326
|
createdAt: user.createdAt.toISOString(),
|
|
231
327
|
updatedAt: user.updatedAt.toISOString(),
|
|
232
328
|
},
|
|
@@ -242,30 +338,32 @@ export async function applyImport(options: ApplyOptions): Promise<ImportResult>
|
|
|
242
338
|
}
|
|
243
339
|
// Host-side writes on the EXISTING Payload user (the source
|
|
244
340
|
// layout's user id IS the host id): identities, roles, and the
|
|
245
|
-
// verified-email linking proof.
|
|
246
|
-
// rollback
|
|
341
|
+
// verified-email linking proof. The prior values (and the
|
|
342
|
+
// doc's updatedAt) are captured first — once — so rollback
|
|
343
|
+
// restores the pre-import document exactly.
|
|
247
344
|
const hostId = Number(baId)
|
|
248
345
|
if (Number.isSafeInteger(hostId)) {
|
|
346
|
+
const { present, newlyCaptured } = await captureBefore({
|
|
347
|
+
payload,
|
|
348
|
+
collection: 'users',
|
|
349
|
+
id: hostId,
|
|
350
|
+
fields: ['identities', 'roles', 'linkable', 'linkableEmail'],
|
|
351
|
+
captured,
|
|
352
|
+
into: fieldWrites,
|
|
353
|
+
})
|
|
354
|
+
fieldWriteCount += newlyCaptured
|
|
249
355
|
const writes: Record<string, unknown> = {
|
|
250
356
|
identities: identityRows(user, now),
|
|
251
|
-
linkable: user.emailVerified,
|
|
252
357
|
}
|
|
253
|
-
|
|
254
|
-
for (const field of fields) {
|
|
255
|
-
fieldWrites.push({
|
|
256
|
-
collection: 'users',
|
|
257
|
-
id: hostId,
|
|
258
|
-
field,
|
|
259
|
-
before: await captureBefore(payload, hostId, field),
|
|
260
|
-
})
|
|
261
|
-
}
|
|
262
|
-
fieldWriteCount += fields.length
|
|
263
|
-
if (user.roles.length > 0) {
|
|
358
|
+
if (user.roles.length > 0 && present.has('roles')) {
|
|
264
359
|
// Stored role values pass through unchanged (issue #56: the
|
|
265
360
|
// storage edge owns translation; the map is the app's).
|
|
266
361
|
writes.roles = user.roles
|
|
267
362
|
}
|
|
268
363
|
if (user.emailVerified) {
|
|
364
|
+
// Grant-only: the proof is bound to the address (both
|
|
365
|
+
// fields), and an existing proof is never revoked here.
|
|
366
|
+
writes.linkable = true
|
|
269
367
|
writes.linkableEmail = user.email
|
|
270
368
|
}
|
|
271
369
|
await payload.update({
|
|
@@ -286,6 +384,24 @@ export async function applyImport(options: ApplyOptions): Promise<ImportResult>
|
|
|
286
384
|
})
|
|
287
385
|
}
|
|
288
386
|
|
|
387
|
+
const tally = (model: string, sourceTable: string, target: string, isNew: boolean): void => {
|
|
388
|
+
if (apply) {
|
|
389
|
+
if (isNew) {
|
|
390
|
+
createdCount += 1
|
|
391
|
+
} else {
|
|
392
|
+
skippedCount += 1
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
record({
|
|
396
|
+
model,
|
|
397
|
+
sourceTable,
|
|
398
|
+
targetCollection: target,
|
|
399
|
+
rows: 1,
|
|
400
|
+
skipped: isNew ? 0 : apply ? 1 : 0,
|
|
401
|
+
unmappable: [],
|
|
402
|
+
})
|
|
403
|
+
}
|
|
404
|
+
|
|
289
405
|
// --- 2. accounts (hashes verbatim) ------------------------------------
|
|
290
406
|
const accountsTable = sourceTables['accounts']
|
|
291
407
|
for (const user of users) {
|
|
@@ -312,26 +428,18 @@ export async function applyImport(options: ApplyOptions): Promise<ImportResult>
|
|
|
312
428
|
...(account.refreshTokenExpiresAt === null
|
|
313
429
|
? {}
|
|
314
430
|
: { refreshTokenExpiresAt: account.refreshTokenExpiresAt.toISOString() }),
|
|
431
|
+
...(account.createdAt === null
|
|
432
|
+
? {}
|
|
433
|
+
: { createdAt: account.createdAt.toISOString() }),
|
|
434
|
+
...(account.updatedAt === null
|
|
435
|
+
? {}
|
|
436
|
+
: { updatedAt: account.updatedAt.toISOString() }),
|
|
315
437
|
},
|
|
316
438
|
created,
|
|
317
439
|
`account:${account.id}`
|
|
318
440
|
)
|
|
319
441
|
: false
|
|
320
|
-
|
|
321
|
-
if (isNew) {
|
|
322
|
-
createdCount += 1
|
|
323
|
-
} else {
|
|
324
|
-
skippedCount += 1
|
|
325
|
-
}
|
|
326
|
-
}
|
|
327
|
-
record({
|
|
328
|
-
model: 'account',
|
|
329
|
-
sourceTable: accountsTable ?? 'accounts',
|
|
330
|
-
targetCollection: slugs.account,
|
|
331
|
-
rows: 1,
|
|
332
|
-
skipped: isNew ? 0 : apply ? 1 : 0,
|
|
333
|
-
unmappable: [],
|
|
334
|
-
})
|
|
442
|
+
tally('account', accountsTable ?? 'accounts', slugs.account, isNew)
|
|
335
443
|
}
|
|
336
444
|
}
|
|
337
445
|
|
|
@@ -345,8 +453,6 @@ export async function applyImport(options: ApplyOptions): Promise<ImportResult>
|
|
|
345
453
|
'expires_at',
|
|
346
454
|
'ip_address',
|
|
347
455
|
'user_agent',
|
|
348
|
-
'created_at',
|
|
349
|
-
'updated_at',
|
|
350
456
|
])
|
|
351
457
|
for (const row of rows) {
|
|
352
458
|
totals.session = (totals.session ?? 0) + 1
|
|
@@ -369,32 +475,13 @@ export async function applyImport(options: ApplyOptions): Promise<ImportResult>
|
|
|
369
475
|
...(textOrNull(row.user_agent) === undefined
|
|
370
476
|
? {}
|
|
371
477
|
: { userAgent: textOrNull(row.user_agent) }),
|
|
372
|
-
...(row
|
|
373
|
-
? { createdAt: row.created_at.toISOString() }
|
|
374
|
-
: {}),
|
|
375
|
-
...(row.updated_at instanceof Date
|
|
376
|
-
? { updatedAt: row.updated_at.toISOString() }
|
|
377
|
-
: {}),
|
|
478
|
+
...stamps(row),
|
|
378
479
|
},
|
|
379
480
|
created,
|
|
380
481
|
`session:${String(row.id)}`
|
|
381
482
|
)
|
|
382
483
|
: false
|
|
383
|
-
|
|
384
|
-
if (isNew) {
|
|
385
|
-
createdCount += 1
|
|
386
|
-
} else {
|
|
387
|
-
skippedCount += 1
|
|
388
|
-
}
|
|
389
|
-
}
|
|
390
|
-
record({
|
|
391
|
-
model: 'session',
|
|
392
|
-
sourceTable: sessionsTable ?? 'sessions',
|
|
393
|
-
targetCollection: slugs.session,
|
|
394
|
-
rows: 1,
|
|
395
|
-
skipped: isNew ? 0 : apply ? 1 : 0,
|
|
396
|
-
unmappable: [],
|
|
397
|
-
})
|
|
484
|
+
tally('session', sessionsTable ?? 'sessions', slugs.session, isNew)
|
|
398
485
|
}
|
|
399
486
|
}
|
|
400
487
|
|
|
@@ -419,26 +506,13 @@ export async function applyImport(options: ApplyOptions): Promise<ImportResult>
|
|
|
419
506
|
value: String(row.value),
|
|
420
507
|
expiresAt:
|
|
421
508
|
row.expires_at instanceof Date ? row.expires_at.toISOString() : now().toISOString(),
|
|
509
|
+
...stamps(row),
|
|
422
510
|
},
|
|
423
511
|
created,
|
|
424
512
|
`verification:${String(row.id)}`
|
|
425
513
|
)
|
|
426
514
|
: false
|
|
427
|
-
|
|
428
|
-
if (isNew) {
|
|
429
|
-
createdCount += 1
|
|
430
|
-
} else {
|
|
431
|
-
skippedCount += 1
|
|
432
|
-
}
|
|
433
|
-
}
|
|
434
|
-
record({
|
|
435
|
-
model: 'verification',
|
|
436
|
-
sourceTable: verificationsTable ?? 'verifications',
|
|
437
|
-
targetCollection: slugs.verification,
|
|
438
|
-
rows: 1,
|
|
439
|
-
skipped: isNew ? 0 : apply ? 1 : 0,
|
|
440
|
-
unmappable: [],
|
|
441
|
-
})
|
|
515
|
+
tally('verification', verificationsTable ?? 'verifications', slugs.verification, isNew)
|
|
442
516
|
}
|
|
443
517
|
|
|
444
518
|
// --- 5. api keys (hash, prefix, metadata, rate-limit state) --------------
|
|
@@ -450,8 +524,8 @@ export async function applyImport(options: ApplyOptions): Promise<ImportResult>
|
|
|
450
524
|
'start',
|
|
451
525
|
'prefix',
|
|
452
526
|
'key',
|
|
453
|
-
// payload-auth stores the satellite's
|
|
454
|
-
//
|
|
527
|
+
// payload-auth stores the satellite's owner as reference_id — the
|
|
528
|
+
// same name @better-auth/api-key 1.7 uses (`referenceId`).
|
|
455
529
|
'reference_id',
|
|
456
530
|
'refill_interval',
|
|
457
531
|
'refill_amount',
|
|
@@ -481,9 +555,9 @@ export async function applyImport(options: ApplyOptions): Promise<ImportResult>
|
|
|
481
555
|
...(textOrNull(row.start) === undefined ? {} : { start: textOrNull(row.start) }),
|
|
482
556
|
...(textOrNull(row.prefix) === undefined ? {} : { prefix: textOrNull(row.prefix) }),
|
|
483
557
|
key: String(row.key),
|
|
484
|
-
//
|
|
485
|
-
//
|
|
486
|
-
//
|
|
558
|
+
// @better-auth/api-key 1.7 names the owner `referenceId`;
|
|
559
|
+
// `userId` is offered for 1.6-era target schemas (Payload
|
|
560
|
+
// drops it where the collection has no such field).
|
|
487
561
|
userId: String(row.reference_id),
|
|
488
562
|
referenceId: String(row.reference_id),
|
|
489
563
|
...(numberOrNull(row.refill_interval) === undefined
|
|
@@ -523,26 +597,13 @@ export async function applyImport(options: ApplyOptions): Promise<ImportResult>
|
|
|
523
597
|
...(textOrNull(row.metadata) === undefined
|
|
524
598
|
? {}
|
|
525
599
|
: { metadata: textOrNull(row.metadata) }),
|
|
600
|
+
...stamps(row),
|
|
526
601
|
},
|
|
527
602
|
created,
|
|
528
603
|
`apikey:${String(row.id)}`
|
|
529
604
|
)
|
|
530
605
|
: false
|
|
531
|
-
|
|
532
|
-
if (isNew) {
|
|
533
|
-
createdCount += 1
|
|
534
|
-
} else {
|
|
535
|
-
skippedCount += 1
|
|
536
|
-
}
|
|
537
|
-
}
|
|
538
|
-
record({
|
|
539
|
-
model: 'apikey',
|
|
540
|
-
sourceTable: apiKeysTable ?? 'api_keys',
|
|
541
|
-
targetCollection: slugs.apikey,
|
|
542
|
-
rows: 1,
|
|
543
|
-
skipped: isNew ? 0 : apply ? 1 : 0,
|
|
544
|
-
unmappable: [],
|
|
545
|
-
})
|
|
606
|
+
tally('apikey', apiKeysTable ?? 'api_keys', slugs.apikey, isNew)
|
|
546
607
|
}
|
|
547
608
|
|
|
548
609
|
// --- 6. jwks --------------------------------------------------------------
|
|
@@ -559,26 +620,13 @@ export async function applyImport(options: ApplyOptions): Promise<ImportResult>
|
|
|
559
620
|
{
|
|
560
621
|
publicKey: String(row.public_key),
|
|
561
622
|
privateKey: String(row.private_key),
|
|
623
|
+
...stamps(row),
|
|
562
624
|
},
|
|
563
625
|
created,
|
|
564
626
|
`jwks:${String(row.id)}`
|
|
565
627
|
)
|
|
566
628
|
: false
|
|
567
|
-
|
|
568
|
-
if (isNew) {
|
|
569
|
-
createdCount += 1
|
|
570
|
-
} else {
|
|
571
|
-
skippedCount += 1
|
|
572
|
-
}
|
|
573
|
-
}
|
|
574
|
-
record({
|
|
575
|
-
model: 'jwks',
|
|
576
|
-
sourceTable: jwksTable ?? 'jwks',
|
|
577
|
-
targetCollection: slugs.jwks,
|
|
578
|
-
rows: 1,
|
|
579
|
-
skipped: isNew ? 0 : apply ? 1 : 0,
|
|
580
|
-
unmappable: [],
|
|
581
|
-
})
|
|
629
|
+
tally('jwks', jwksTable ?? 'jwks', slugs.jwks, isNew)
|
|
582
630
|
}
|
|
583
631
|
|
|
584
632
|
// --- 7. organization plugin rows ---------------------------------------
|
|
@@ -605,26 +653,13 @@ export async function applyImport(options: ApplyOptions): Promise<ImportResult>
|
|
|
605
653
|
...(textOrNull(row.metadata) === undefined
|
|
606
654
|
? {}
|
|
607
655
|
: { metadata: textOrNull(row.metadata) }),
|
|
656
|
+
...stamps(row),
|
|
608
657
|
},
|
|
609
658
|
created,
|
|
610
659
|
`organization:${String(row.id)}`
|
|
611
660
|
)
|
|
612
661
|
: false
|
|
613
|
-
|
|
614
|
-
if (isNew) {
|
|
615
|
-
createdCount += 1
|
|
616
|
-
} else {
|
|
617
|
-
skippedCount += 1
|
|
618
|
-
}
|
|
619
|
-
}
|
|
620
|
-
record({
|
|
621
|
-
model: 'organization',
|
|
622
|
-
sourceTable: organizationsTable ?? 'organizations',
|
|
623
|
-
targetCollection: slugs.organization,
|
|
624
|
-
rows: 1,
|
|
625
|
-
skipped: isNew ? 0 : apply ? 1 : 0,
|
|
626
|
-
unmappable: [],
|
|
627
|
-
})
|
|
662
|
+
tally('organization', organizationsTable ?? 'organizations', slugs.organization, isNew)
|
|
628
663
|
}
|
|
629
664
|
|
|
630
665
|
const membersTable = sourceTables['members']
|
|
@@ -646,26 +681,13 @@ export async function applyImport(options: ApplyOptions): Promise<ImportResult>
|
|
|
646
681
|
organizationId: `org-${String(row.organization_id)}`,
|
|
647
682
|
userId: String(row.user_id),
|
|
648
683
|
...(textOrNull(row.role) === undefined ? {} : { role: textOrNull(row.role) }),
|
|
684
|
+
...stamps(row),
|
|
649
685
|
},
|
|
650
686
|
created,
|
|
651
687
|
`member:${String(row.id)}`
|
|
652
688
|
)
|
|
653
689
|
: false
|
|
654
|
-
|
|
655
|
-
if (isNew) {
|
|
656
|
-
createdCount += 1
|
|
657
|
-
} else {
|
|
658
|
-
skippedCount += 1
|
|
659
|
-
}
|
|
660
|
-
}
|
|
661
|
-
record({
|
|
662
|
-
model: 'member',
|
|
663
|
-
sourceTable: membersTable ?? 'members',
|
|
664
|
-
targetCollection: slugs.member,
|
|
665
|
-
rows: 1,
|
|
666
|
-
skipped: isNew ? 0 : apply ? 1 : 0,
|
|
667
|
-
unmappable: [],
|
|
668
|
-
})
|
|
690
|
+
tally('member', membersTable ?? 'members', slugs.member, isNew)
|
|
669
691
|
}
|
|
670
692
|
|
|
671
693
|
const invitationsTable = sourceTables['invitations']
|
|
@@ -695,36 +717,24 @@ export async function applyImport(options: ApplyOptions): Promise<ImportResult>
|
|
|
695
717
|
? { expiresAt: row.expires_at.toISOString() }
|
|
696
718
|
: {}),
|
|
697
719
|
...(typeof row.inviter_id === 'number' ? { inviterId: String(row.inviter_id) } : {}),
|
|
720
|
+
...stamps(row),
|
|
698
721
|
},
|
|
699
722
|
created,
|
|
700
723
|
`invitation:${String(row.id)}`
|
|
701
724
|
)
|
|
702
725
|
: false
|
|
703
|
-
|
|
704
|
-
if (isNew) {
|
|
705
|
-
createdCount += 1
|
|
706
|
-
} else {
|
|
707
|
-
skippedCount += 1
|
|
708
|
-
}
|
|
709
|
-
}
|
|
710
|
-
record({
|
|
711
|
-
model: 'invitation',
|
|
712
|
-
sourceTable: invitationsTable ?? 'invitations',
|
|
713
|
-
targetCollection: slugs.invitation,
|
|
714
|
-
rows: 1,
|
|
715
|
-
skipped: isNew ? 0 : apply ? 1 : 0,
|
|
716
|
-
unmappable: [],
|
|
717
|
-
})
|
|
726
|
+
tally('invitation', invitationsTable ?? 'invitations', slugs.invitation, isNew)
|
|
718
727
|
}
|
|
719
728
|
|
|
720
729
|
log(
|
|
721
|
-
`plan: ${planRows.length} row group(s), ${createdCount} created, ${skippedCount} skipped, ${fieldWriteCount} field
|
|
730
|
+
`plan: ${planRows.length} row group(s), ${createdCount} created, ${skippedCount} skipped, ${fieldWriteCount} field value(s) captured`
|
|
722
731
|
)
|
|
723
732
|
if (apply) {
|
|
724
733
|
// The manifest is the run's contract: merge this pass onto any
|
|
725
734
|
// resumed run's rows and write it back (issue #50).
|
|
726
735
|
const manifest: ImportManifest = {
|
|
727
|
-
version:
|
|
736
|
+
version: 2,
|
|
737
|
+
kind: 'import',
|
|
728
738
|
createdAt: options.manifest?.createdAt ?? now().toISOString(),
|
|
729
739
|
sourceDatabase: options.sourceDatabase,
|
|
730
740
|
options: { sessions },
|
|
@@ -732,11 +742,7 @@ export async function applyImport(options: ApplyOptions): Promise<ImportResult>
|
|
|
732
742
|
fieldWrites,
|
|
733
743
|
finalized: options.manifest?.finalized ?? false,
|
|
734
744
|
sourceCounts: options.sourceCounts,
|
|
735
|
-
totals
|
|
736
|
-
...options.sourceCounts,
|
|
737
|
-
...(options.manifest?.totals ?? {}),
|
|
738
|
-
...totals,
|
|
739
|
-
},
|
|
745
|
+
totals,
|
|
740
746
|
}
|
|
741
747
|
const { writeManifest } = await import('./lifecycle.js')
|
|
742
748
|
writeManifest(manifest, options.manifestPath)
|