@payload-auth-link/migrate 0.4.3 → 0.6.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 +109 -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 +25 -5
- package/dist/importer/apply.d.ts.map +1 -1
- package/dist/importer/apply.js +164 -180
- 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 +26 -0
- package/dist/importer/lifecycle.d.ts.map +1 -1
- package/dist/importer/lifecycle.js +107 -22
- package/dist/importer/lifecycle.js.map +1 -1
- package/dist/importer/manifest-mac.d.ts +15 -0
- package/dist/importer/manifest-mac.d.ts.map +1 -0
- package/dist/importer/manifest-mac.js +73 -0
- package/dist/importer/manifest-mac.js.map +1 -0
- package/dist/importer/prelink.d.ts +57 -0
- package/dist/importer/prelink.d.ts.map +1 -0
- package/dist/importer/prelink.js +159 -0
- package/dist/importer/prelink.js.map +1 -0
- package/dist/importer/run-import.d.ts +15 -0
- package/dist/importer/run-import.d.ts.map +1 -1
- package/dist/importer/run-import.js +155 -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 +34 -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 +10 -10
- package/src/cli.ts +117 -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 +198 -182
- package/src/importer/host-writes.ts +231 -0
- package/src/importer/lifecycle.ts +131 -24
- package/src/importer/manifest-mac.ts +90 -0
- package/src/importer/prelink.ts +237 -0
- package/src/importer/run-import.ts +207 -29
- package/src/importer/source.ts +72 -14
- package/src/importer/types.ts +35 -4
- package/src/index.ts +13 -1
- package/src/private-file.ts +18 -0
package/src/importer/apply.ts
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import type { Payload } from 'payload'
|
|
2
|
+
import { capturedKeys, captureBefore } from './host-writes.js'
|
|
3
|
+
import { authenticateManifest, signManifest } from './manifest-mac.js'
|
|
2
4
|
import {
|
|
3
5
|
readFlatRows,
|
|
4
6
|
openSource,
|
|
@@ -17,6 +19,7 @@ import {
|
|
|
17
19
|
type ImportPlan,
|
|
18
20
|
type ImportResult,
|
|
19
21
|
type PlanRow,
|
|
22
|
+
type Unmappable,
|
|
20
23
|
} from './types.js'
|
|
21
24
|
|
|
22
25
|
/**
|
|
@@ -25,16 +28,26 @@ import {
|
|
|
25
28
|
*
|
|
26
29
|
* - Password hashes, OAuth tokens, API-key hashes, and jwks carry over
|
|
27
30
|
* VERBATIM (payload-auth used Better Auth's own hashing).
|
|
28
|
-
* -
|
|
29
|
-
*
|
|
31
|
+
* - Row timestamps (`createdAt`/`updatedAt`) carry from the source rows
|
|
32
|
+
* for every model whose source table has them (issue #88).
|
|
33
|
+
* - Better Auth plugin user state carries (issue #88): admin-plugin
|
|
34
|
+
* `banned`/`banReason`/`banExpires`/`role` and the anonymous plugin's
|
|
35
|
+
* `isAnonymous`, whenever the source has the column AND the target
|
|
36
|
+
* `ba-users` collection declares the field. A banned or anonymous
|
|
37
|
+
* source user whose state the target cannot hold is UNMAPPABLE — the
|
|
38
|
+
* run aborts before writing; a banned user never imports unbanned.
|
|
39
|
+
* - `linkable` + `linkableEmail` are granted from VERIFIED emails only
|
|
40
|
+
* (grant-only: an existing proof is never revoked); unverified users
|
|
41
|
+
* are reported by the plan, never linked.
|
|
30
42
|
* - Sessions default to `carry` so nobody is logged out: the carried
|
|
31
43
|
* rows keep their tokens, and with the same `advanced.cookiePrefix`
|
|
32
44
|
* the existing cookies keep working.
|
|
33
45
|
* - Idempotent and resumable: every target row carries a deterministic
|
|
34
46
|
* `baId` derived from the source id (`acct-<source id>`), so a
|
|
35
47
|
* 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
|
-
*
|
|
48
|
+
* - Every created row and every host field write lands in the
|
|
49
|
+
* manifest — the FIRST captured prior value per field wins, sealed —
|
|
50
|
+
* so `rollback` is exact and the source tables stay untouched.
|
|
38
51
|
*/
|
|
39
52
|
|
|
40
53
|
export type ApplyOptions = {
|
|
@@ -57,6 +70,8 @@ export type ApplyOptions = {
|
|
|
57
70
|
/** Injectable clock for deterministic `linkedAt` stamps. */
|
|
58
71
|
readonly now?: () => Date
|
|
59
72
|
readonly log?: (message: string) => void
|
|
73
|
+
/** Resume from a pre-0.6.0 manifest that has no MAC (`--allow-unsigned-manifest`). */
|
|
74
|
+
readonly allowUnsignedManifest?: boolean | undefined
|
|
60
75
|
}
|
|
61
76
|
|
|
62
77
|
const slugsOf = (overrides?: Readonly<Record<string, string>>): TargetSlugMap => ({
|
|
@@ -76,18 +91,96 @@ function numberOrNull(value: unknown): number | undefined {
|
|
|
76
91
|
return Number.isFinite(numeric) ? numeric : undefined
|
|
77
92
|
}
|
|
78
93
|
|
|
94
|
+
/** The source row's timestamps, when its layout has them (issue #88). */
|
|
95
|
+
function stamps(row: SourceRow): { createdAt?: string; updatedAt?: string } {
|
|
96
|
+
return {
|
|
97
|
+
...(row.created_at instanceof Date ? { createdAt: row.created_at.toISOString() } : {}),
|
|
98
|
+
...(row.updated_at instanceof Date ? { updatedAt: row.updated_at.toISOString() } : {}),
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const timestampColumns = ['created_at', 'updated_at'] as const
|
|
103
|
+
|
|
79
104
|
/**
|
|
80
105
|
* Rows for one mapped model. A `null` mapping records the model as
|
|
81
106
|
* not installed (issue #68) — no read, no rows; anything else goes
|
|
82
107
|
* through the strict `readFlatRows` (a missing mapped table throws;
|
|
83
|
-
* the probe aborts the run first).
|
|
108
|
+
* the probe aborts the run first). Row timestamps are read when the
|
|
109
|
+
* layout has them.
|
|
84
110
|
*/
|
|
85
111
|
async function readMapped(
|
|
86
112
|
client: Awaited<ReturnType<typeof openSource>>,
|
|
87
113
|
table: string | null,
|
|
88
114
|
columns: readonly string[]
|
|
89
115
|
): Promise<SourceRow[]> {
|
|
90
|
-
return table === null ? [] : readFlatRows(client, table, columns)
|
|
116
|
+
return table === null ? [] : readFlatRows(client, table, columns, timestampColumns)
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/** Field names a target collection declares (top level, flattened). */
|
|
120
|
+
function targetFieldNames(payload: Payload, slug: string): ReadonlySet<string> {
|
|
121
|
+
const collection = payload.collections[slug]
|
|
122
|
+
const names = new Set<string>()
|
|
123
|
+
for (const field of collection?.config.flattenedFields ?? []) {
|
|
124
|
+
names.add(field.name)
|
|
125
|
+
}
|
|
126
|
+
return names
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Plugin user state the target must be able to hold (issue #88),
|
|
131
|
+
* checked BEFORE any write: a banned user whose target has no `banned`
|
|
132
|
+
* field would import unbanned; an anonymous user without `isAnonymous`
|
|
133
|
+
* would import as a full account.
|
|
134
|
+
*/
|
|
135
|
+
export function pluginStateUnmappable(
|
|
136
|
+
users: readonly SourceUser[],
|
|
137
|
+
targetFields: ReadonlySet<string>,
|
|
138
|
+
targetSlug: string
|
|
139
|
+
): Unmappable[] {
|
|
140
|
+
const found: Unmappable[] = []
|
|
141
|
+
for (const user of users) {
|
|
142
|
+
if (user.banned === true && !targetFields.has('banned')) {
|
|
143
|
+
found.push({
|
|
144
|
+
model: 'user',
|
|
145
|
+
key: user.id,
|
|
146
|
+
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`,
|
|
147
|
+
})
|
|
148
|
+
}
|
|
149
|
+
if (user.isAnonymous === true && !targetFields.has('isAnonymous')) {
|
|
150
|
+
found.push({
|
|
151
|
+
model: 'user',
|
|
152
|
+
key: user.id,
|
|
153
|
+
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`,
|
|
154
|
+
})
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
return found
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/** Plugin user state carried onto the ba-users row (issue #88). */
|
|
161
|
+
function pluginUserState(
|
|
162
|
+
user: SourceUser,
|
|
163
|
+
targetFields: ReadonlySet<string>
|
|
164
|
+
): Record<string, unknown> {
|
|
165
|
+
const state: Record<string, unknown> = {}
|
|
166
|
+
if (user.banned !== null && targetFields.has('banned')) {
|
|
167
|
+
state.banned = user.banned
|
|
168
|
+
}
|
|
169
|
+
if (user.banReason !== null && targetFields.has('banReason')) {
|
|
170
|
+
state.banReason = user.banReason
|
|
171
|
+
}
|
|
172
|
+
if (user.banExpires !== null && targetFields.has('banExpires')) {
|
|
173
|
+
state.banExpires = user.banExpires.toISOString()
|
|
174
|
+
}
|
|
175
|
+
if (user.isAnonymous !== null && targetFields.has('isAnonymous')) {
|
|
176
|
+
state.isAnonymous = user.isAnonymous
|
|
177
|
+
}
|
|
178
|
+
// Better Auth's admin plugin stores roles as one comma-separated
|
|
179
|
+
// string; payload-auth kept them in the users_role child table.
|
|
180
|
+
if (user.roles.length > 0 && targetFields.has('role')) {
|
|
181
|
+
state.role = user.roles.join(',')
|
|
182
|
+
}
|
|
183
|
+
return state
|
|
91
184
|
}
|
|
92
185
|
|
|
93
186
|
/** Find an existing target row by `baId` — the resumable dedupe key. */
|
|
@@ -129,24 +222,6 @@ async function createDeduped(
|
|
|
129
222
|
return true
|
|
130
223
|
}
|
|
131
224
|
|
|
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
225
|
/**
|
|
151
226
|
* The identity rows pre-populated on the host user (issue #69): ONE
|
|
152
227
|
* `better-auth` identity per imported user, `providerUserId` set to
|
|
@@ -190,11 +265,23 @@ export async function applyImport(options: ApplyOptions): Promise<ImportResult>
|
|
|
190
265
|
} = options
|
|
191
266
|
const slugs = slugsOf(options.slugs)
|
|
192
267
|
const sourceTables: ResolvedSourceTables = resolveSourceTables(options.sourceTables)
|
|
268
|
+
if (options.manifest !== null) {
|
|
269
|
+
// Resuming merges the old entries and re-signs them: never launder
|
|
270
|
+
// an edited manifest into a signed one.
|
|
271
|
+
authenticateManifest(payload.secret, options.manifest, {
|
|
272
|
+
allowUnsigned: options.allowUnsignedManifest,
|
|
273
|
+
})
|
|
274
|
+
}
|
|
193
275
|
const created: CreatedRow[] = [...(options.manifest?.created ?? [])]
|
|
194
276
|
const fieldWrites: FieldWrite[] = [...(options.manifest?.fieldWrites ?? [])]
|
|
195
|
-
|
|
277
|
+
// First capture wins (issue #88): fields captured by an earlier pass
|
|
278
|
+
// keep their pre-import value.
|
|
279
|
+
const captured = capturedKeys(fieldWrites)
|
|
280
|
+
// Per-PASS totals (issue #88): a re-apply reports what this pass
|
|
281
|
+
// planned, never the sum of every pass.
|
|
282
|
+
const totals: Record<string, number> = {}
|
|
196
283
|
const planRows: PlanRow[] = []
|
|
197
|
-
const unmappable:
|
|
284
|
+
const unmappable: Unmappable[] = []
|
|
198
285
|
let createdCount = 0
|
|
199
286
|
let skippedCount = 0
|
|
200
287
|
let fieldWriteCount = 0
|
|
@@ -206,6 +293,24 @@ export async function applyImport(options: ApplyOptions): Promise<ImportResult>
|
|
|
206
293
|
const client = await openSource(sourceConnectionString)
|
|
207
294
|
try {
|
|
208
295
|
const users = await readSourceUsers(client, sourceTables)
|
|
296
|
+
const baUserFields = targetFieldNames(payload, slugs.user)
|
|
297
|
+
|
|
298
|
+
// --- 0. target-capability gate (issue #88) ---------------------------
|
|
299
|
+
// Runs before ANY write: plugin state the target cannot hold aborts
|
|
300
|
+
// the run instead of importing a banned user unbanned.
|
|
301
|
+
const blocked = pluginStateUnmappable(users, baUserFields, slugs.user)
|
|
302
|
+
if (blocked.length > 0) {
|
|
303
|
+
log(
|
|
304
|
+
`ABORT: ${blocked.length} user(s) carry state the target cannot hold; nothing was written`
|
|
305
|
+
)
|
|
306
|
+
return {
|
|
307
|
+
created: 0,
|
|
308
|
+
skipped: 0,
|
|
309
|
+
fieldWrites: 0,
|
|
310
|
+
plan: { rows: [], unmappable: blocked, sessions, warnings: [] },
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
|
|
209
314
|
const seenUsers = new Set<string>()
|
|
210
315
|
|
|
211
316
|
// --- 1. ba-users + host field writes ---------------------------------
|
|
@@ -227,6 +332,7 @@ export async function applyImport(options: ApplyOptions): Promise<ImportResult>
|
|
|
227
332
|
name: user.name ?? '',
|
|
228
333
|
emailVerified: user.emailVerified,
|
|
229
334
|
...(user.image === null ? {} : { image: user.image }),
|
|
335
|
+
...pluginUserState(user, baUserFields),
|
|
230
336
|
createdAt: user.createdAt.toISOString(),
|
|
231
337
|
updatedAt: user.updatedAt.toISOString(),
|
|
232
338
|
},
|
|
@@ -242,30 +348,32 @@ export async function applyImport(options: ApplyOptions): Promise<ImportResult>
|
|
|
242
348
|
}
|
|
243
349
|
// Host-side writes on the EXISTING Payload user (the source
|
|
244
350
|
// layout's user id IS the host id): identities, roles, and the
|
|
245
|
-
// verified-email linking proof.
|
|
246
|
-
// rollback
|
|
351
|
+
// verified-email linking proof. The prior values (and the
|
|
352
|
+
// doc's updatedAt) are captured first — once — so rollback
|
|
353
|
+
// restores the pre-import document exactly.
|
|
247
354
|
const hostId = Number(baId)
|
|
248
355
|
if (Number.isSafeInteger(hostId)) {
|
|
356
|
+
const { present, newlyCaptured } = await captureBefore({
|
|
357
|
+
payload,
|
|
358
|
+
collection: 'users',
|
|
359
|
+
id: hostId,
|
|
360
|
+
fields: ['identities', 'roles', 'linkable', 'linkableEmail'],
|
|
361
|
+
captured,
|
|
362
|
+
into: fieldWrites,
|
|
363
|
+
})
|
|
364
|
+
fieldWriteCount += newlyCaptured
|
|
249
365
|
const writes: Record<string, unknown> = {
|
|
250
366
|
identities: identityRows(user, now),
|
|
251
|
-
linkable: user.emailVerified,
|
|
252
|
-
}
|
|
253
|
-
const fields: readonly string[] = ['identities', 'roles', 'linkable', 'linkableEmail']
|
|
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
367
|
}
|
|
262
|
-
|
|
263
|
-
if (user.roles.length > 0) {
|
|
368
|
+
if (user.roles.length > 0 && present.has('roles')) {
|
|
264
369
|
// Stored role values pass through unchanged (issue #56: the
|
|
265
370
|
// storage edge owns translation; the map is the app's).
|
|
266
371
|
writes.roles = user.roles
|
|
267
372
|
}
|
|
268
373
|
if (user.emailVerified) {
|
|
374
|
+
// Grant-only: the proof is bound to the address (both
|
|
375
|
+
// fields), and an existing proof is never revoked here.
|
|
376
|
+
writes.linkable = true
|
|
269
377
|
writes.linkableEmail = user.email
|
|
270
378
|
}
|
|
271
379
|
await payload.update({
|
|
@@ -286,6 +394,24 @@ export async function applyImport(options: ApplyOptions): Promise<ImportResult>
|
|
|
286
394
|
})
|
|
287
395
|
}
|
|
288
396
|
|
|
397
|
+
const tally = (model: string, sourceTable: string, target: string, isNew: boolean): void => {
|
|
398
|
+
if (apply) {
|
|
399
|
+
if (isNew) {
|
|
400
|
+
createdCount += 1
|
|
401
|
+
} else {
|
|
402
|
+
skippedCount += 1
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
record({
|
|
406
|
+
model,
|
|
407
|
+
sourceTable,
|
|
408
|
+
targetCollection: target,
|
|
409
|
+
rows: 1,
|
|
410
|
+
skipped: isNew ? 0 : apply ? 1 : 0,
|
|
411
|
+
unmappable: [],
|
|
412
|
+
})
|
|
413
|
+
}
|
|
414
|
+
|
|
289
415
|
// --- 2. accounts (hashes verbatim) ------------------------------------
|
|
290
416
|
const accountsTable = sourceTables['accounts']
|
|
291
417
|
for (const user of users) {
|
|
@@ -312,26 +438,18 @@ export async function applyImport(options: ApplyOptions): Promise<ImportResult>
|
|
|
312
438
|
...(account.refreshTokenExpiresAt === null
|
|
313
439
|
? {}
|
|
314
440
|
: { refreshTokenExpiresAt: account.refreshTokenExpiresAt.toISOString() }),
|
|
441
|
+
...(account.createdAt === null
|
|
442
|
+
? {}
|
|
443
|
+
: { createdAt: account.createdAt.toISOString() }),
|
|
444
|
+
...(account.updatedAt === null
|
|
445
|
+
? {}
|
|
446
|
+
: { updatedAt: account.updatedAt.toISOString() }),
|
|
315
447
|
},
|
|
316
448
|
created,
|
|
317
449
|
`account:${account.id}`
|
|
318
450
|
)
|
|
319
451
|
: 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
|
-
})
|
|
452
|
+
tally('account', accountsTable ?? 'accounts', slugs.account, isNew)
|
|
335
453
|
}
|
|
336
454
|
}
|
|
337
455
|
|
|
@@ -345,8 +463,6 @@ export async function applyImport(options: ApplyOptions): Promise<ImportResult>
|
|
|
345
463
|
'expires_at',
|
|
346
464
|
'ip_address',
|
|
347
465
|
'user_agent',
|
|
348
|
-
'created_at',
|
|
349
|
-
'updated_at',
|
|
350
466
|
])
|
|
351
467
|
for (const row of rows) {
|
|
352
468
|
totals.session = (totals.session ?? 0) + 1
|
|
@@ -369,32 +485,13 @@ export async function applyImport(options: ApplyOptions): Promise<ImportResult>
|
|
|
369
485
|
...(textOrNull(row.user_agent) === undefined
|
|
370
486
|
? {}
|
|
371
487
|
: { 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
|
-
: {}),
|
|
488
|
+
...stamps(row),
|
|
378
489
|
},
|
|
379
490
|
created,
|
|
380
491
|
`session:${String(row.id)}`
|
|
381
492
|
)
|
|
382
493
|
: 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
|
-
})
|
|
494
|
+
tally('session', sessionsTable ?? 'sessions', slugs.session, isNew)
|
|
398
495
|
}
|
|
399
496
|
}
|
|
400
497
|
|
|
@@ -419,26 +516,13 @@ export async function applyImport(options: ApplyOptions): Promise<ImportResult>
|
|
|
419
516
|
value: String(row.value),
|
|
420
517
|
expiresAt:
|
|
421
518
|
row.expires_at instanceof Date ? row.expires_at.toISOString() : now().toISOString(),
|
|
519
|
+
...stamps(row),
|
|
422
520
|
},
|
|
423
521
|
created,
|
|
424
522
|
`verification:${String(row.id)}`
|
|
425
523
|
)
|
|
426
524
|
: 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
|
-
})
|
|
525
|
+
tally('verification', verificationsTable ?? 'verifications', slugs.verification, isNew)
|
|
442
526
|
}
|
|
443
527
|
|
|
444
528
|
// --- 5. api keys (hash, prefix, metadata, rate-limit state) --------------
|
|
@@ -450,8 +534,8 @@ export async function applyImport(options: ApplyOptions): Promise<ImportResult>
|
|
|
450
534
|
'start',
|
|
451
535
|
'prefix',
|
|
452
536
|
'key',
|
|
453
|
-
// payload-auth stores the satellite's
|
|
454
|
-
//
|
|
537
|
+
// payload-auth stores the satellite's owner as reference_id — the
|
|
538
|
+
// same name @better-auth/api-key 1.7 uses (`referenceId`).
|
|
455
539
|
'reference_id',
|
|
456
540
|
'refill_interval',
|
|
457
541
|
'refill_amount',
|
|
@@ -481,9 +565,9 @@ export async function applyImport(options: ApplyOptions): Promise<ImportResult>
|
|
|
481
565
|
...(textOrNull(row.start) === undefined ? {} : { start: textOrNull(row.start) }),
|
|
482
566
|
...(textOrNull(row.prefix) === undefined ? {} : { prefix: textOrNull(row.prefix) }),
|
|
483
567
|
key: String(row.key),
|
|
484
|
-
//
|
|
485
|
-
//
|
|
486
|
-
//
|
|
568
|
+
// @better-auth/api-key 1.7 names the owner `referenceId`;
|
|
569
|
+
// `userId` is offered for 1.6-era target schemas (Payload
|
|
570
|
+
// drops it where the collection has no such field).
|
|
487
571
|
userId: String(row.reference_id),
|
|
488
572
|
referenceId: String(row.reference_id),
|
|
489
573
|
...(numberOrNull(row.refill_interval) === undefined
|
|
@@ -523,26 +607,13 @@ export async function applyImport(options: ApplyOptions): Promise<ImportResult>
|
|
|
523
607
|
...(textOrNull(row.metadata) === undefined
|
|
524
608
|
? {}
|
|
525
609
|
: { metadata: textOrNull(row.metadata) }),
|
|
610
|
+
...stamps(row),
|
|
526
611
|
},
|
|
527
612
|
created,
|
|
528
613
|
`apikey:${String(row.id)}`
|
|
529
614
|
)
|
|
530
615
|
: 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
|
-
})
|
|
616
|
+
tally('apikey', apiKeysTable ?? 'api_keys', slugs.apikey, isNew)
|
|
546
617
|
}
|
|
547
618
|
|
|
548
619
|
// --- 6. jwks --------------------------------------------------------------
|
|
@@ -559,26 +630,13 @@ export async function applyImport(options: ApplyOptions): Promise<ImportResult>
|
|
|
559
630
|
{
|
|
560
631
|
publicKey: String(row.public_key),
|
|
561
632
|
privateKey: String(row.private_key),
|
|
633
|
+
...stamps(row),
|
|
562
634
|
},
|
|
563
635
|
created,
|
|
564
636
|
`jwks:${String(row.id)}`
|
|
565
637
|
)
|
|
566
638
|
: 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
|
-
})
|
|
639
|
+
tally('jwks', jwksTable ?? 'jwks', slugs.jwks, isNew)
|
|
582
640
|
}
|
|
583
641
|
|
|
584
642
|
// --- 7. organization plugin rows ---------------------------------------
|
|
@@ -605,26 +663,13 @@ export async function applyImport(options: ApplyOptions): Promise<ImportResult>
|
|
|
605
663
|
...(textOrNull(row.metadata) === undefined
|
|
606
664
|
? {}
|
|
607
665
|
: { metadata: textOrNull(row.metadata) }),
|
|
666
|
+
...stamps(row),
|
|
608
667
|
},
|
|
609
668
|
created,
|
|
610
669
|
`organization:${String(row.id)}`
|
|
611
670
|
)
|
|
612
671
|
: 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
|
-
})
|
|
672
|
+
tally('organization', organizationsTable ?? 'organizations', slugs.organization, isNew)
|
|
628
673
|
}
|
|
629
674
|
|
|
630
675
|
const membersTable = sourceTables['members']
|
|
@@ -646,26 +691,13 @@ export async function applyImport(options: ApplyOptions): Promise<ImportResult>
|
|
|
646
691
|
organizationId: `org-${String(row.organization_id)}`,
|
|
647
692
|
userId: String(row.user_id),
|
|
648
693
|
...(textOrNull(row.role) === undefined ? {} : { role: textOrNull(row.role) }),
|
|
694
|
+
...stamps(row),
|
|
649
695
|
},
|
|
650
696
|
created,
|
|
651
697
|
`member:${String(row.id)}`
|
|
652
698
|
)
|
|
653
699
|
: 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
|
-
})
|
|
700
|
+
tally('member', membersTable ?? 'members', slugs.member, isNew)
|
|
669
701
|
}
|
|
670
702
|
|
|
671
703
|
const invitationsTable = sourceTables['invitations']
|
|
@@ -695,36 +727,24 @@ export async function applyImport(options: ApplyOptions): Promise<ImportResult>
|
|
|
695
727
|
? { expiresAt: row.expires_at.toISOString() }
|
|
696
728
|
: {}),
|
|
697
729
|
...(typeof row.inviter_id === 'number' ? { inviterId: String(row.inviter_id) } : {}),
|
|
730
|
+
...stamps(row),
|
|
698
731
|
},
|
|
699
732
|
created,
|
|
700
733
|
`invitation:${String(row.id)}`
|
|
701
734
|
)
|
|
702
735
|
: 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
|
-
})
|
|
736
|
+
tally('invitation', invitationsTable ?? 'invitations', slugs.invitation, isNew)
|
|
718
737
|
}
|
|
719
738
|
|
|
720
739
|
log(
|
|
721
|
-
`plan: ${planRows.length} row group(s), ${createdCount} created, ${skippedCount} skipped, ${fieldWriteCount} field
|
|
740
|
+
`plan: ${planRows.length} row group(s), ${createdCount} created, ${skippedCount} skipped, ${fieldWriteCount} field value(s) captured`
|
|
722
741
|
)
|
|
723
742
|
if (apply) {
|
|
724
743
|
// The manifest is the run's contract: merge this pass onto any
|
|
725
744
|
// resumed run's rows and write it back (issue #50).
|
|
726
|
-
const manifest: ImportManifest = {
|
|
727
|
-
version:
|
|
745
|
+
const manifest: ImportManifest = signManifest(payload.secret, {
|
|
746
|
+
version: 3,
|
|
747
|
+
kind: 'import',
|
|
728
748
|
createdAt: options.manifest?.createdAt ?? now().toISOString(),
|
|
729
749
|
sourceDatabase: options.sourceDatabase,
|
|
730
750
|
options: { sessions },
|
|
@@ -732,12 +752,8 @@ export async function applyImport(options: ApplyOptions): Promise<ImportResult>
|
|
|
732
752
|
fieldWrites,
|
|
733
753
|
finalized: options.manifest?.finalized ?? false,
|
|
734
754
|
sourceCounts: options.sourceCounts,
|
|
735
|
-
totals
|
|
736
|
-
|
|
737
|
-
...(options.manifest?.totals ?? {}),
|
|
738
|
-
...totals,
|
|
739
|
-
},
|
|
740
|
-
}
|
|
755
|
+
totals,
|
|
756
|
+
})
|
|
741
757
|
const { writeManifest } = await import('./lifecycle.js')
|
|
742
758
|
writeManifest(manifest, options.manifestPath)
|
|
743
759
|
}
|