@payload-auth-link/migrate 0.0.0-stage → 0.2.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 +49 -2
- package/dist/cli.d.ts +7 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +257 -0
- package/dist/cli.js.map +1 -0
- package/dist/doctor/bands.d.ts +16 -0
- package/dist/doctor/bands.d.ts.map +1 -0
- package/dist/doctor/bands.js +49 -0
- package/dist/doctor/bands.js.map +1 -0
- package/dist/doctor/database.d.ts +3 -0
- package/dist/doctor/database.d.ts.map +1 -0
- package/dist/doctor/database.js +110 -0
- package/dist/doctor/database.js.map +1 -0
- package/dist/doctor/plugins.d.ts +12 -0
- package/dist/doctor/plugins.d.ts.map +1 -0
- package/dist/doctor/plugins.js +202 -0
- package/dist/doctor/plugins.js.map +1 -0
- package/dist/doctor/run-doctor.d.ts +48 -0
- package/dist/doctor/run-doctor.d.ts.map +1 -0
- package/dist/doctor/run-doctor.js +236 -0
- package/dist/doctor/run-doctor.js.map +1 -0
- package/dist/doctor/source-scan.d.ts +10 -0
- package/dist/doctor/source-scan.d.ts.map +1 -0
- package/dist/doctor/source-scan.js +142 -0
- package/dist/doctor/source-scan.js.map +1 -0
- package/dist/doctor/types.d.ts +86 -0
- package/dist/doctor/types.d.ts.map +1 -0
- package/dist/doctor/types.js +7 -0
- package/dist/doctor/types.js.map +1 -0
- package/dist/doctor/users.d.ts +10 -0
- package/dist/doctor/users.d.ts.map +1 -0
- package/dist/doctor/users.js +176 -0
- package/dist/doctor/users.js.map +1 -0
- package/dist/importer/apply.d.ts +39 -0
- package/dist/importer/apply.d.ts.map +1 -0
- package/dist/importer/apply.js +588 -0
- package/dist/importer/apply.js.map +1 -0
- package/dist/importer/lifecycle.d.ts +52 -0
- package/dist/importer/lifecycle.d.ts.map +1 -0
- package/dist/importer/lifecycle.js +265 -0
- package/dist/importer/lifecycle.js.map +1 -0
- package/dist/importer/run-import.d.ts +40 -0
- package/dist/importer/run-import.d.ts.map +1 -0
- package/dist/importer/run-import.js +176 -0
- package/dist/importer/run-import.js.map +1 -0
- package/dist/importer/source.d.ts +49 -0
- package/dist/importer/source.d.ts.map +1 -0
- package/dist/importer/source.js +147 -0
- package/dist/importer/source.js.map +1 -0
- package/dist/importer/types.d.ts +69 -0
- package/dist/importer/types.d.ts.map +1 -0
- package/dist/importer/types.js +13 -0
- package/dist/importer/types.js.map +1 -0
- package/dist/index.d.ts +19 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +17 -0
- package/dist/index.js.map +1 -0
- package/package.json +70 -4
- package/src/cli.ts +287 -0
- package/src/doctor/bands.ts +66 -0
- package/src/doctor/database.ts +130 -0
- package/src/doctor/plugins.ts +216 -0
- package/src/doctor/run-doctor.ts +278 -0
- package/src/doctor/source-scan.ts +148 -0
- package/src/doctor/types.ts +92 -0
- package/src/doctor/users.ts +207 -0
- package/src/importer/apply.ts +731 -0
- package/src/importer/lifecycle.ts +328 -0
- package/src/importer/run-import.ts +237 -0
- package/src/importer/source.ts +211 -0
- package/src/importer/types.ts +96 -0
- package/src/index.ts +53 -0
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
import { Client } from 'pg';
|
|
2
|
+
export async function openSource(connectionString) {
|
|
3
|
+
const client = new Client({ connectionString });
|
|
4
|
+
await client.connect();
|
|
5
|
+
return client;
|
|
6
|
+
}
|
|
7
|
+
/** The columns one table actually has (customized layouts drop columns). */
|
|
8
|
+
async function columnsOf(client, table) {
|
|
9
|
+
const result = await client.query(`SELECT column_name FROM information_schema.columns
|
|
10
|
+
WHERE table_schema = 'public' AND table_name = $1`, [table]);
|
|
11
|
+
return new Set(result.rows.map((row) => String(row.column_name)));
|
|
12
|
+
}
|
|
13
|
+
async function tableExists(client, table) {
|
|
14
|
+
const result = await client.query(`SELECT 1 FROM information_schema.tables WHERE table_schema = 'public' AND table_name = $1`, [table]);
|
|
15
|
+
return result.rows.length > 0;
|
|
16
|
+
}
|
|
17
|
+
export async function countTables(client, tables) {
|
|
18
|
+
const counts = {};
|
|
19
|
+
for (const table of tables) {
|
|
20
|
+
if (await tableExists(client, table)) {
|
|
21
|
+
const counted = await client.query(`SELECT count(*)::int AS n FROM ${table}`);
|
|
22
|
+
counts[table] = counted.rows[0]?.n ?? 0;
|
|
23
|
+
}
|
|
24
|
+
else {
|
|
25
|
+
counts[table] = 0;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
return counts;
|
|
29
|
+
}
|
|
30
|
+
function iso(value) {
|
|
31
|
+
return value instanceof Date ? value : null;
|
|
32
|
+
}
|
|
33
|
+
function text(value) {
|
|
34
|
+
return typeof value === 'string' && value !== '' ? value : null;
|
|
35
|
+
}
|
|
36
|
+
/** Every source user, with role rows and accounts attached. */
|
|
37
|
+
export async function readSourceUsers(client) {
|
|
38
|
+
const hasRoleTable = await tableExists(client, 'users_role');
|
|
39
|
+
const users = await client.query(`SELECT id, email, name, email_verified, image, created_at, updated_at FROM users ORDER BY id`);
|
|
40
|
+
const roleRows = hasRoleTable
|
|
41
|
+
? await client.query(`SELECT parent_id, value FROM users_role ORDER BY parent_id, "order"`)
|
|
42
|
+
: { rows: [] };
|
|
43
|
+
const rolesByUser = new Map();
|
|
44
|
+
for (const row of roleRows.rows) {
|
|
45
|
+
const list = rolesByUser.get(String(row.parent_id)) ?? [];
|
|
46
|
+
list.push(row.value);
|
|
47
|
+
rolesByUser.set(String(row.parent_id), list);
|
|
48
|
+
}
|
|
49
|
+
const accounts = await client.query(`SELECT id, account_id, provider_id, password, access_token, refresh_token, id_token,
|
|
50
|
+
scope, access_token_expires_at, refresh_token_expires_at, user_id
|
|
51
|
+
FROM accounts ORDER BY id`);
|
|
52
|
+
const accountsByUser = new Map();
|
|
53
|
+
for (const row of accounts.rows) {
|
|
54
|
+
const userId = String(row.user_id);
|
|
55
|
+
const account = {
|
|
56
|
+
id: String(row.id),
|
|
57
|
+
accountId: String(row.account_id),
|
|
58
|
+
providerId: String(row.provider_id),
|
|
59
|
+
password: text(row.password),
|
|
60
|
+
accessToken: text(row.access_token),
|
|
61
|
+
refreshToken: text(row.refresh_token),
|
|
62
|
+
idToken: text(row.id_token),
|
|
63
|
+
scope: text(row.scope),
|
|
64
|
+
accessTokenExpiresAt: iso(row.access_token_expires_at),
|
|
65
|
+
refreshTokenExpiresAt: iso(row.refresh_token_expires_at),
|
|
66
|
+
};
|
|
67
|
+
const list = accountsByUser.get(userId) ?? [];
|
|
68
|
+
list.push(account);
|
|
69
|
+
accountsByUser.set(userId, list);
|
|
70
|
+
}
|
|
71
|
+
const out = [];
|
|
72
|
+
for (const row of users.rows) {
|
|
73
|
+
const id = String(row.id);
|
|
74
|
+
out.push({
|
|
75
|
+
id,
|
|
76
|
+
email: String(row.email),
|
|
77
|
+
name: text(row.name),
|
|
78
|
+
emailVerified: row.email_verified === true,
|
|
79
|
+
image: text(row.image),
|
|
80
|
+
createdAt: iso(row.created_at) ?? new Date(),
|
|
81
|
+
updatedAt: iso(row.updated_at) ?? new Date(),
|
|
82
|
+
roles: rolesByUser.get(id) ?? [],
|
|
83
|
+
accounts: accountsByUser.get(id) ?? [],
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
return out;
|
|
87
|
+
}
|
|
88
|
+
/** Rows of one flat plugin table (sessions, verifications, apiKeys, jwks, …). */
|
|
89
|
+
export async function readFlatRows(client, table, columns) {
|
|
90
|
+
if (!(await tableExists(client, table))) {
|
|
91
|
+
return [];
|
|
92
|
+
}
|
|
93
|
+
const result = await client.query(`SELECT ${columns.join(', ')} FROM ${table} ORDER BY id`);
|
|
94
|
+
return result.rows;
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* Members and invitations reference organizations and users by numeric
|
|
98
|
+
* id; the importer remaps to the target's baIds. Orphaned references
|
|
99
|
+
* are unmappable — they abort the run rather than skip silently.
|
|
100
|
+
*/
|
|
101
|
+
export async function findOrphans(client) {
|
|
102
|
+
const orphans = [];
|
|
103
|
+
const checks = [
|
|
104
|
+
{ model: 'account', table: 'accounts', column: 'user_id', target: 'users' },
|
|
105
|
+
{ model: 'session', table: 'sessions', column: 'user_id', target: 'users' },
|
|
106
|
+
{ model: 'member', table: 'members', column: 'organization_id', target: 'organizations' },
|
|
107
|
+
{ model: 'member', table: 'members', column: 'user_id', target: 'users' },
|
|
108
|
+
{
|
|
109
|
+
model: 'invitation',
|
|
110
|
+
table: 'invitations',
|
|
111
|
+
column: 'organization_id',
|
|
112
|
+
target: 'organizations',
|
|
113
|
+
},
|
|
114
|
+
{ model: 'invitation', table: 'invitations', column: 'inviter_id', target: 'users' },
|
|
115
|
+
{ model: 'apikey', table: 'api_keys', column: 'user_id', target: 'users' },
|
|
116
|
+
];
|
|
117
|
+
for (const check of checks) {
|
|
118
|
+
const exists = await Promise.all([
|
|
119
|
+
tableExists(client, check.table),
|
|
120
|
+
tableExists(client, check.target),
|
|
121
|
+
]);
|
|
122
|
+
if (!exists[0] || !exists[1]) {
|
|
123
|
+
continue;
|
|
124
|
+
}
|
|
125
|
+
// Customized layouts drop columns; a missing reference column is
|
|
126
|
+
// not an orphan farm.
|
|
127
|
+
if (!(await columnsOf(client, check.table)).has(check.column)) {
|
|
128
|
+
continue;
|
|
129
|
+
}
|
|
130
|
+
const result = await client.query(`SELECT t.id FROM ${check.table} t
|
|
131
|
+
WHERE t.${check.column} IS NOT NULL
|
|
132
|
+
AND NOT EXISTS (SELECT 1 FROM ${check.target} s WHERE s.id = t.${check.column})`);
|
|
133
|
+
for (const row of result.rows) {
|
|
134
|
+
orphans.push({
|
|
135
|
+
model: check.model,
|
|
136
|
+
key: String(row.id),
|
|
137
|
+
reason: `${check.table}.${check.column} references a missing ${check.target} row`,
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
return orphans;
|
|
142
|
+
}
|
|
143
|
+
/** Users whose email never verified — reported, never granted `linkable`. */
|
|
144
|
+
export function unverifiedEmails(users) {
|
|
145
|
+
return users.filter((user) => !user.emailVerified).map((user) => user.email);
|
|
146
|
+
}
|
|
147
|
+
//# sourceMappingURL=source.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"source.js","sourceRoot":"","sources":["../../src/importer/source.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,IAAI,CAAA;AAuC3B,MAAM,CAAC,KAAK,UAAU,UAAU,CAAC,gBAAwB;IACvD,MAAM,MAAM,GAAG,IAAI,MAAM,CAAC,EAAE,gBAAgB,EAAE,CAAC,CAAA;IAC/C,MAAM,MAAM,CAAC,OAAO,EAAE,CAAA;IACtB,OAAO,MAAM,CAAA;AACf,CAAC;AAED,4EAA4E;AAC5E,KAAK,UAAU,SAAS,CAAC,MAAc,EAAE,KAAa;IACpD,MAAM,MAAM,GAAG,MAAM,MAAM,CAAC,KAAK,CAC/B;uDACmD,EACnD,CAAC,KAAK,CAAC,CACR,CAAA;IACD,OAAO,IAAI,GAAG,CAAC,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,MAAM,CAAE,GAAgC,CAAC,WAAW,CAAC,CAAC,CAAC,CAAA;AACjG,CAAC;AAED,KAAK,UAAU,WAAW,CAAC,MAAc,EAAE,KAAa;IACtD,MAAM,MAAM,GAAG,MAAM,MAAM,CAAC,KAAK,CAC/B,2FAA2F,EAC3F,CAAC,KAAK,CAAC,CACR,CAAA;IACD,OAAO,MAAM,CAAC,IAAI,CAAC,MAAM,GAAG,CAAC,CAAA;AAC/B,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,WAAW,CAC/B,MAAc,EACd,MAAyB;IAEzB,MAAM,MAAM,GAA2B,EAAE,CAAA;IACzC,KAAK,MAAM,KAAK,IAAI,MAAM,EAAE,CAAC;QAC3B,IAAI,MAAM,WAAW,CAAC,MAAM,EAAE,KAAK,CAAC,EAAE,CAAC;YACrC,MAAM,OAAO,GAAG,MAAM,MAAM,CAAC,KAAK,CAAC,kCAAkC,KAAK,EAAE,CAAC,CAAA;YAC7E,MAAM,CAAC,KAAK,CAAC,GAAI,OAAO,CAAC,IAAI,CAAC,CAAC,CAAgC,EAAE,CAAC,IAAI,CAAC,CAAA;QACzE,CAAC;aAAM,CAAC;YACN,MAAM,CAAC,KAAK,CAAC,GAAG,CAAC,CAAA;QACnB,CAAC;IACH,CAAC;IACD,OAAO,MAAM,CAAA;AACf,CAAC;AAED,SAAS,GAAG,CAAC,KAAc;IACzB,OAAO,KAAK,YAAY,IAAI,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,IAAI,CAAA;AAC7C,CAAC;AAED,SAAS,IAAI,CAAC,KAAc;IAC1B,OAAO,OAAO,KAAK,KAAK,QAAQ,IAAI,KAAK,KAAK,EAAE,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,IAAI,CAAA;AACjE,CAAC;AAED,+DAA+D;AAC/D,MAAM,CAAC,KAAK,UAAU,eAAe,CAAC,MAAc;IAClD,MAAM,YAAY,GAAG,MAAM,WAAW,CAAC,MAAM,EAAE,YAAY,CAAC,CAAA;IAC5D,MAAM,KAAK,GAAG,MAAM,MAAM,CAAC,KAAK,CAC9B,8FAA8F,CAC/F,CAAA;IACD,MAAM,QAAQ,GAAG,YAAY;QAC3B,CAAC,CAAC,MAAM,MAAM,CAAC,KAAK,CAAC,qEAAqE,CAAC;QAC3F,CAAC,CAAC,EAAE,IAAI,EAAE,EAAe,EAAE,CAAA;IAC7B,MAAM,WAAW,GAAG,IAAI,GAAG,EAAoB,CAAA;IAC/C,KAAK,MAAM,GAAG,IAAI,QAAQ,CAAC,IAA8C,EAAE,CAAC;QAC1E,MAAM,IAAI,GAAG,WAAW,CAAC,GAAG,CAAC,MAAM,CAAC,GAAG,CAAC,SAAS,CAAC,CAAC,IAAI,EAAE,CAAA;QACzD,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,KAAK,CAAC,CAAA;QACpB,WAAW,CAAC,GAAG,CAAC,MAAM,CAAC,GAAG,CAAC,SAAS,CAAC,EAAE,IAAI,CAAC,CAAA;IAC9C,CAAC;IACD,MAAM,QAAQ,GAAG,MAAM,MAAM,CAAC,KAAK,CACjC;;+BAE2B,CAC5B,CAAA;IACD,MAAM,cAAc,GAAG,IAAI,GAAG,EAA2B,CAAA;IACzD,KAAK,MAAM,GAAG,IAAI,QAAQ,CAAC,IAAiC,EAAE,CAAC;QAC7D,MAAM,MAAM,GAAG,MAAM,CAAC,GAAG,CAAC,OAAO,CAAC,CAAA;QAClC,MAAM,OAAO,GAAkB;YAC7B,EAAE,EAAE,MAAM,CAAC,GAAG,CAAC,EAAE,CAAC;YAClB,SAAS,EAAE,MAAM,CAAC,GAAG,CAAC,UAAU,CAAC;YACjC,UAAU,EAAE,MAAM,CAAC,GAAG,CAAC,WAAW,CAAC;YACnC,QAAQ,EAAE,IAAI,CAAC,GAAG,CAAC,QAAQ,CAAC;YAC5B,WAAW,EAAE,IAAI,CAAC,GAAG,CAAC,YAAY,CAAC;YACnC,YAAY,EAAE,IAAI,CAAC,GAAG,CAAC,aAAa,CAAC;YACrC,OAAO,EAAE,IAAI,CAAC,GAAG,CAAC,QAAQ,CAAC;YAC3B,KAAK,EAAE,IAAI,CAAC,GAAG,CAAC,KAAK,CAAC;YACtB,oBAAoB,EAAE,GAAG,CAAC,GAAG,CAAC,uBAAuB,CAAC;YACtD,qBAAqB,EAAE,GAAG,CAAC,GAAG,CAAC,wBAAwB,CAAC;SACzD,CAAA;QACD,MAAM,IAAI,GAAG,cAAc,CAAC,GAAG,CAAC,MAAM,CAAC,IAAI,EAAE,CAAA;QAC7C,IAAI,CAAC,IAAI,CAAC,OAAO,CAAC,CAAA;QAClB,cAAc,CAAC,GAAG,CAAC,MAAM,EAAE,IAAI,CAAC,CAAA;IAClC,CAAC;IACD,MAAM,GAAG,GAAiB,EAAE,CAAA;IAC5B,KAAK,MAAM,GAAG,IAAI,KAAK,CAAC,IAAiC,EAAE,CAAC;QAC1D,MAAM,EAAE,GAAG,MAAM,CAAC,GAAG,CAAC,EAAE,CAAC,CAAA;QACzB,GAAG,CAAC,IAAI,CAAC;YACP,EAAE;YACF,KAAK,EAAE,MAAM,CAAC,GAAG,CAAC,KAAK,CAAC;YACxB,IAAI,EAAE,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC;YACpB,aAAa,EAAE,GAAG,CAAC,cAAc,KAAK,IAAI;YAC1C,KAAK,EAAE,IAAI,CAAC,GAAG,CAAC,KAAK,CAAC;YACtB,SAAS,EAAE,GAAG,CAAC,GAAG,CAAC,UAAU,CAAC,IAAI,IAAI,IAAI,EAAE;YAC5C,SAAS,EAAE,GAAG,CAAC,GAAG,CAAC,UAAU,CAAC,IAAI,IAAI,IAAI,EAAE;YAC5C,KAAK,EAAE,WAAW,CAAC,GAAG,CAAC,EAAE,CAAC,IAAI,EAAE;YAChC,QAAQ,EAAE,cAAc,CAAC,GAAG,CAAC,EAAE,CAAC,IAAI,EAAE;SACvC,CAAC,CAAA;IACJ,CAAC;IACD,OAAO,GAAG,CAAA;AACZ,CAAC;AAED,iFAAiF;AACjF,MAAM,CAAC,KAAK,UAAU,YAAY,CAChC,MAAc,EACd,KAAa,EACb,OAA0B;IAE1B,IAAI,CAAC,CAAC,MAAM,WAAW,CAAC,MAAM,EAAE,KAAK,CAAC,CAAC,EAAE,CAAC;QACxC,OAAO,EAAE,CAAA;IACX,CAAC;IACD,MAAM,MAAM,GAAG,MAAM,MAAM,CAAC,KAAK,CAAC,UAAU,OAAO,CAAC,IAAI,CAAC,IAAI,CAAC,SAAS,KAAK,cAAc,CAAC,CAAA;IAC3F,OAAO,MAAM,CAAC,IAAmB,CAAA;AACnC,CAAC;AAED;;;;GAIG;AACH,MAAM,CAAC,KAAK,UAAU,WAAW,CAAC,MAAc;IAC9C,MAAM,OAAO,GAAiB,EAAE,CAAA;IAChC,MAAM,MAAM,GAAgF;QAC1F,EAAE,KAAK,EAAE,SAAS,EAAE,KAAK,EAAE,UAAU,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,EAAE,OAAO,EAAE;QAC3E,EAAE,KAAK,EAAE,SAAS,EAAE,KAAK,EAAE,UAAU,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,EAAE,OAAO,EAAE;QAC3E,EAAE,KAAK,EAAE,QAAQ,EAAE,KAAK,EAAE,SAAS,EAAE,MAAM,EAAE,iBAAiB,EAAE,MAAM,EAAE,eAAe,EAAE;QACzF,EAAE,KAAK,EAAE,QAAQ,EAAE,KAAK,EAAE,SAAS,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,EAAE,OAAO,EAAE;QACzE;YACE,KAAK,EAAE,YAAY;YACnB,KAAK,EAAE,aAAa;YACpB,MAAM,EAAE,iBAAiB;YACzB,MAAM,EAAE,eAAe;SACxB;QACD,EAAE,KAAK,EAAE,YAAY,EAAE,KAAK,EAAE,aAAa,EAAE,MAAM,EAAE,YAAY,EAAE,MAAM,EAAE,OAAO,EAAE;QACpF,EAAE,KAAK,EAAE,QAAQ,EAAE,KAAK,EAAE,UAAU,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,EAAE,OAAO,EAAE;KAC3E,CAAA;IACD,KAAK,MAAM,KAAK,IAAI,MAAM,EAAE,CAAC;QAC3B,MAAM,MAAM,GAAG,MAAM,OAAO,CAAC,GAAG,CAAC;YAC/B,WAAW,CAAC,MAAM,EAAE,KAAK,CAAC,KAAK,CAAC;YAChC,WAAW,CAAC,MAAM,EAAE,KAAK,CAAC,MAAM,CAAC;SAClC,CAAC,CAAA;QACF,IAAI,CAAC,MAAM,CAAC,CAAC,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC;YAC7B,SAAQ;QACV,CAAC;QACD,iEAAiE;QACjE,sBAAsB;QACtB,IAAI,CAAC,CAAC,MAAM,SAAS,CAAC,MAAM,EAAE,KAAK,CAAC,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,KAAK,CAAC,MAAM,CAAC,EAAE,CAAC;YAC9D,SAAQ;QACV,CAAC;QACD,MAAM,MAAM,GAAG,MAAM,MAAM,CAAC,KAAK,CAC/B,oBAAoB,KAAK,CAAC,KAAK;iBACpB,KAAK,CAAC,MAAM;yCACY,KAAK,CAAC,MAAM,qBAAqB,KAAK,CAAC,MAAM,GAAG,CACpF,CAAA;QACD,KAAK,MAAM,GAAG,IAAI,MAAM,CAAC,IAAyB,EAAE,CAAC;YACnD,OAAO,CAAC,IAAI,CAAC;gBACX,KAAK,EAAE,KAAK,CAAC,KAAK;gBAClB,GAAG,EAAE,MAAM,CAAC,GAAG,CAAC,EAAE,CAAC;gBACnB,MAAM,EAAE,GAAG,KAAK,CAAC,KAAK,IAAI,KAAK,CAAC,MAAM,yBAAyB,KAAK,CAAC,MAAM,MAAM;aAClF,CAAC,CAAA;QACJ,CAAC;IACH,CAAC;IACD,OAAO,OAAO,CAAA;AAChB,CAAC;AAED,6EAA6E;AAC7E,MAAM,UAAU,gBAAgB,CAAC,KAA4B;IAC3D,OAAO,KAAK,CAAC,MAAM,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,CAAC,IAAI,CAAC,aAAa,CAAC,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,KAAK,CAAC,CAAA;AAC9E,CAAC"}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Importer manifest (issue #50): every row created and every field
|
|
3
|
+
* changed, so `rollback` is exact. The manifest is the run's contract —
|
|
4
|
+
* `verify`, `rollback`, and `finalize` all read it.
|
|
5
|
+
*/
|
|
6
|
+
export type CreatedRow = {
|
|
7
|
+
/** Target collection slug. */
|
|
8
|
+
readonly collection: string;
|
|
9
|
+
/** Payload document id of the created row. */
|
|
10
|
+
readonly id: string | number;
|
|
11
|
+
/** The source identity this row was created for (dedupe on resume). */
|
|
12
|
+
readonly sourceKey: string;
|
|
13
|
+
};
|
|
14
|
+
export type FieldWrite = {
|
|
15
|
+
readonly collection: string;
|
|
16
|
+
readonly id: string | number;
|
|
17
|
+
readonly field: string;
|
|
18
|
+
/** The prior value, JSON-serialized, restored by rollback. */
|
|
19
|
+
readonly before: string | null;
|
|
20
|
+
};
|
|
21
|
+
export type ImportManifest = {
|
|
22
|
+
readonly version: 1;
|
|
23
|
+
readonly createdAt: string;
|
|
24
|
+
readonly sourceDatabase: string;
|
|
25
|
+
readonly options: {
|
|
26
|
+
readonly sessions: 'carry' | 'drop';
|
|
27
|
+
};
|
|
28
|
+
readonly created: CreatedRow[];
|
|
29
|
+
readonly fieldWrites: FieldWrite[];
|
|
30
|
+
readonly finalized: boolean;
|
|
31
|
+
/** Counts per source table at plan time (the rollback baseline). */
|
|
32
|
+
readonly sourceCounts: Readonly<Record<string, number>>;
|
|
33
|
+
/** Cumulative plan totals (rows created + skipped across resumes). */
|
|
34
|
+
readonly totals: Readonly<Record<string, number>>;
|
|
35
|
+
};
|
|
36
|
+
/** Per-model plan row for the dry run. */
|
|
37
|
+
export type PlanRow = {
|
|
38
|
+
readonly model: string;
|
|
39
|
+
readonly sourceTable: string;
|
|
40
|
+
readonly targetCollection: string;
|
|
41
|
+
readonly rows: number;
|
|
42
|
+
readonly skipped: number;
|
|
43
|
+
readonly unmappable: readonly string[];
|
|
44
|
+
};
|
|
45
|
+
/** Why a row cannot be imported — the run aborts rather than skips. */
|
|
46
|
+
export type Unmappable = {
|
|
47
|
+
readonly model: string;
|
|
48
|
+
readonly key: string;
|
|
49
|
+
readonly reason: string;
|
|
50
|
+
};
|
|
51
|
+
export type ImportPlan = {
|
|
52
|
+
readonly rows: readonly PlanRow[];
|
|
53
|
+
readonly unmappable: readonly Unmappable[];
|
|
54
|
+
readonly sessions: 'carry' | 'drop';
|
|
55
|
+
readonly warnings: readonly string[];
|
|
56
|
+
};
|
|
57
|
+
export type ImportResult = {
|
|
58
|
+
readonly created: number;
|
|
59
|
+
readonly skipped: number;
|
|
60
|
+
readonly fieldWrites: number;
|
|
61
|
+
readonly plan: ImportPlan;
|
|
62
|
+
};
|
|
63
|
+
/** The models the importer copies. */
|
|
64
|
+
export type TargetModel = 'user' | 'session' | 'account' | 'verification' | 'apikey' | 'jwks' | 'organization' | 'member' | 'invitation';
|
|
65
|
+
/** Target slugs with the known models always present (index-safe). */
|
|
66
|
+
export type TargetSlugMap = Record<TargetModel, string> & Record<string, string>;
|
|
67
|
+
/** Default target slugs; must match the app's engine wiring. */
|
|
68
|
+
export declare const defaultTargetSlugs: TargetSlugMap;
|
|
69
|
+
//# sourceMappingURL=types.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/importer/types.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/** Default target slugs; must match the app's engine wiring. */
|
|
2
|
+
export const defaultTargetSlugs = {
|
|
3
|
+
user: 'ba-users',
|
|
4
|
+
session: 'ba-sessions',
|
|
5
|
+
account: 'ba-accounts',
|
|
6
|
+
verification: 'ba-verifications',
|
|
7
|
+
apikey: 'ba-apikey',
|
|
8
|
+
jwks: 'ba-jwks',
|
|
9
|
+
organization: 'ba-organization',
|
|
10
|
+
member: 'ba-member',
|
|
11
|
+
invitation: 'ba-invitation',
|
|
12
|
+
};
|
|
13
|
+
//# sourceMappingURL=types.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.js","sourceRoot":"","sources":["../../src/importer/types.ts"],"names":[],"mappings":"AAoFA,gEAAgE;AAChE,MAAM,CAAC,MAAM,kBAAkB,GAAkB;IAC/C,IAAI,EAAE,UAAU;IAChB,OAAO,EAAE,aAAa;IACtB,OAAO,EAAE,aAAa;IACtB,YAAY,EAAE,kBAAkB;IAChC,MAAM,EAAE,WAAW;IACnB,IAAI,EAAE,SAAS;IACf,YAAY,EAAE,iBAAiB;IAC/B,MAAM,EAAE,WAAW;IACnB,UAAU,EAAE,eAAe;CAC5B,CAAA"}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Public API of `@payload-auth-link/migrate` (issues #49–#59). The
|
|
3
|
+
* CLI is the primary surface; these exports let tools and tests run
|
|
4
|
+
* the doctor in-process and consume its typed report.
|
|
5
|
+
*/
|
|
6
|
+
export { runDoctor, behaviourChanges, findPayloadAuthPlugin, betterAuthPluginIds, usersSlugOf, } from './doctor/run-doctor.js';
|
|
7
|
+
export type { DoctorOptions } from './doctor/run-doctor.js';
|
|
8
|
+
export { checkVersions, payloadBand, betterAuthBand, nextBand } from './doctor/bands.js';
|
|
9
|
+
export { classifyPlugins } from './doctor/plugins.js';
|
|
10
|
+
export { scanSource, projectedUserFields } from './doctor/source-scan.js';
|
|
11
|
+
export { analyzeUsersCollection, findUsersCollection, localStrategyOn } from './doctor/users.js';
|
|
12
|
+
export { introspectDatabase } from './doctor/database.js';
|
|
13
|
+
export { runFromPayloadAuth, runVerify, runRollback, runFinalize } from './importer/run-import.js';
|
|
14
|
+
export { applyImport, summarizePlanRows } from './importer/apply.js';
|
|
15
|
+
export { readManifest, writeManifest, verifyImport, rollbackImport, finalizeManifest, defaultManifestPath, type VerifyIssue, type VerifyResult, } from './importer/lifecycle.js';
|
|
16
|
+
export { defaultTargetSlugs, type CreatedRow, type TargetModel, type TargetSlugMap, type FieldWrite, type ImportManifest, type ImportPlan, type ImportResult, type PlanRow, type Unmappable, } from './importer/types.js';
|
|
17
|
+
export { cliMain } from './cli.js';
|
|
18
|
+
export type { BehaviourChange, DatabaseFinding, DoctorIssue, DoctorReport, PluginFinding, SourceFinding, UsersFinding, VersionFinding, } from './doctor/types.js';
|
|
19
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AACH,OAAO,EACL,SAAS,EACT,gBAAgB,EAChB,qBAAqB,EACrB,mBAAmB,EACnB,WAAW,GACZ,MAAM,wBAAwB,CAAA;AAC/B,YAAY,EAAE,aAAa,EAAE,MAAM,wBAAwB,CAAA;AAC3D,OAAO,EAAE,aAAa,EAAE,WAAW,EAAE,cAAc,EAAE,QAAQ,EAAE,MAAM,mBAAmB,CAAA;AACxF,OAAO,EAAE,eAAe,EAAE,MAAM,qBAAqB,CAAA;AACrD,OAAO,EAAE,UAAU,EAAE,mBAAmB,EAAE,MAAM,yBAAyB,CAAA;AACzE,OAAO,EAAE,sBAAsB,EAAE,mBAAmB,EAAE,eAAe,EAAE,MAAM,mBAAmB,CAAA;AAChG,OAAO,EAAE,kBAAkB,EAAE,MAAM,sBAAsB,CAAA;AACzD,OAAO,EAAE,kBAAkB,EAAE,SAAS,EAAE,WAAW,EAAE,WAAW,EAAE,MAAM,0BAA0B,CAAA;AAClG,OAAO,EAAE,WAAW,EAAE,iBAAiB,EAAE,MAAM,qBAAqB,CAAA;AACpE,OAAO,EACL,YAAY,EACZ,aAAa,EACb,YAAY,EACZ,cAAc,EACd,gBAAgB,EAChB,mBAAmB,EACnB,KAAK,WAAW,EAChB,KAAK,YAAY,GAClB,MAAM,yBAAyB,CAAA;AAChC,OAAO,EACL,kBAAkB,EAClB,KAAK,UAAU,EACf,KAAK,WAAW,EAChB,KAAK,aAAa,EAClB,KAAK,UAAU,EACf,KAAK,cAAc,EACnB,KAAK,UAAU,EACf,KAAK,YAAY,EACjB,KAAK,OAAO,EACZ,KAAK,UAAU,GAChB,MAAM,qBAAqB,CAAA;AAC5B,OAAO,EAAE,OAAO,EAAE,MAAM,UAAU,CAAA;AAClC,YAAY,EACV,eAAe,EACf,eAAe,EACf,WAAW,EACX,YAAY,EACZ,aAAa,EACb,aAAa,EACb,YAAY,EACZ,cAAc,GACf,MAAM,mBAAmB,CAAA"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Public API of `@payload-auth-link/migrate` (issues #49–#59). The
|
|
3
|
+
* CLI is the primary surface; these exports let tools and tests run
|
|
4
|
+
* the doctor in-process and consume its typed report.
|
|
5
|
+
*/
|
|
6
|
+
export { runDoctor, behaviourChanges, findPayloadAuthPlugin, betterAuthPluginIds, usersSlugOf, } from './doctor/run-doctor.js';
|
|
7
|
+
export { checkVersions, payloadBand, betterAuthBand, nextBand } from './doctor/bands.js';
|
|
8
|
+
export { classifyPlugins } from './doctor/plugins.js';
|
|
9
|
+
export { scanSource, projectedUserFields } from './doctor/source-scan.js';
|
|
10
|
+
export { analyzeUsersCollection, findUsersCollection, localStrategyOn } from './doctor/users.js';
|
|
11
|
+
export { introspectDatabase } from './doctor/database.js';
|
|
12
|
+
export { runFromPayloadAuth, runVerify, runRollback, runFinalize } from './importer/run-import.js';
|
|
13
|
+
export { applyImport, summarizePlanRows } from './importer/apply.js';
|
|
14
|
+
export { readManifest, writeManifest, verifyImport, rollbackImport, finalizeManifest, defaultManifestPath, } from './importer/lifecycle.js';
|
|
15
|
+
export { defaultTargetSlugs, } from './importer/types.js';
|
|
16
|
+
export { cliMain } from './cli.js';
|
|
17
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AACH,OAAO,EACL,SAAS,EACT,gBAAgB,EAChB,qBAAqB,EACrB,mBAAmB,EACnB,WAAW,GACZ,MAAM,wBAAwB,CAAA;AAE/B,OAAO,EAAE,aAAa,EAAE,WAAW,EAAE,cAAc,EAAE,QAAQ,EAAE,MAAM,mBAAmB,CAAA;AACxF,OAAO,EAAE,eAAe,EAAE,MAAM,qBAAqB,CAAA;AACrD,OAAO,EAAE,UAAU,EAAE,mBAAmB,EAAE,MAAM,yBAAyB,CAAA;AACzE,OAAO,EAAE,sBAAsB,EAAE,mBAAmB,EAAE,eAAe,EAAE,MAAM,mBAAmB,CAAA;AAChG,OAAO,EAAE,kBAAkB,EAAE,MAAM,sBAAsB,CAAA;AACzD,OAAO,EAAE,kBAAkB,EAAE,SAAS,EAAE,WAAW,EAAE,WAAW,EAAE,MAAM,0BAA0B,CAAA;AAClG,OAAO,EAAE,WAAW,EAAE,iBAAiB,EAAE,MAAM,qBAAqB,CAAA;AACpE,OAAO,EACL,YAAY,EACZ,aAAa,EACb,YAAY,EACZ,cAAc,EACd,gBAAgB,EAChB,mBAAmB,GAGpB,MAAM,yBAAyB,CAAA;AAChC,OAAO,EACL,kBAAkB,GAUnB,MAAM,qBAAqB,CAAA;AAC5B,OAAO,EAAE,OAAO,EAAE,MAAM,UAAU,CAAA"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,72 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@payload-auth-link/migrate",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"description": "payload-auth-link migration kit: read-only doctor/preflight, the payload-auth importer, and exporters.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"repository": {
|
|
7
|
+
"type": "git",
|
|
8
|
+
"url": "git+https://github.com/jellologic/payload-auth-link.git",
|
|
9
|
+
"directory": "packages/migrate"
|
|
10
|
+
},
|
|
11
|
+
"homepage": "https://github.com/jellologic/payload-auth-link/tree/main/packages/migrate#readme",
|
|
12
|
+
"bugs": {
|
|
13
|
+
"url": "https://github.com/jellologic/payload-auth-link/issues"
|
|
14
|
+
},
|
|
15
|
+
"type": "module",
|
|
16
|
+
"sideEffects": false,
|
|
17
|
+
"bin": {
|
|
18
|
+
"payload-auth-link-migrate": "./dist/cli.js"
|
|
19
|
+
},
|
|
20
|
+
"exports": {
|
|
21
|
+
".": {
|
|
22
|
+
"types": "./dist/index.d.ts",
|
|
23
|
+
"default": "./dist/index.js"
|
|
24
|
+
}
|
|
25
|
+
},
|
|
26
|
+
"main": "./dist/index.js",
|
|
27
|
+
"types": "./dist/index.d.ts",
|
|
28
|
+
"files": [
|
|
29
|
+
"dist",
|
|
30
|
+
"!dist/*.tsbuildinfo",
|
|
31
|
+
"src",
|
|
32
|
+
"!src/**/__tests__",
|
|
33
|
+
"!dist/**/__tests__",
|
|
34
|
+
"!dist/**/__fixtures__",
|
|
35
|
+
"!src/**/__fixtures__"
|
|
36
|
+
],
|
|
37
|
+
"scripts": {
|
|
38
|
+
"build": "tsc -b",
|
|
39
|
+
"typecheck": "tsc -b",
|
|
40
|
+
"clean": "rm -rf dist *.tsbuildinfo"
|
|
41
|
+
},
|
|
42
|
+
"dependencies": {
|
|
43
|
+
"@payload-auth-link/core": "workspace:*",
|
|
44
|
+
"pg": "^8.16.3"
|
|
45
|
+
},
|
|
46
|
+
"publishConfig": {
|
|
47
|
+
"access": "public",
|
|
48
|
+
"provenance": true
|
|
49
|
+
},
|
|
50
|
+
"engines": {
|
|
51
|
+
"node": ">=20.9.0"
|
|
52
|
+
},
|
|
53
|
+
"devDependencies": {
|
|
54
|
+
"@better-auth/api-key": "1.7.7",
|
|
55
|
+
"@payload-auth-link/core": "workspace:*",
|
|
56
|
+
"@payload-auth-link/plugin": "workspace:*",
|
|
57
|
+
"@payload-auth-link/provider-better-auth": "workspace:*",
|
|
58
|
+
"@payloadcms/db-postgres": "3.90.2",
|
|
59
|
+
"@types/node": "^22",
|
|
60
|
+
"@types/pg": "^8.15.4",
|
|
61
|
+
"better-auth": "1.7.7",
|
|
62
|
+
"payload": "3.90.2"
|
|
63
|
+
},
|
|
64
|
+
"peerDependencies": {
|
|
65
|
+
"payload": "^3.83.0 || 4.0.0-beta.0"
|
|
66
|
+
},
|
|
67
|
+
"peerDependenciesMeta": {
|
|
68
|
+
"payload": {
|
|
69
|
+
"optional": false
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
package/src/cli.ts
ADDED
|
@@ -0,0 +1,287 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
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).
|
|
6
|
+
*
|
|
7
|
+
* Exit codes: 0 clean, 1 blockers, 2 warnings only, 3 usage errors.
|
|
8
|
+
* Never logs credential material — connection strings print only in
|
|
9
|
+
* the `--json` echo of what the caller passed, and secrets inside them
|
|
10
|
+
* are the caller's own input.
|
|
11
|
+
*/
|
|
12
|
+
import { runDoctor } from './doctor/run-doctor.js'
|
|
13
|
+
import type { DoctorReport } from './doctor/types.js'
|
|
14
|
+
import {
|
|
15
|
+
runFinalize,
|
|
16
|
+
runFromPayloadAuth,
|
|
17
|
+
runRollback,
|
|
18
|
+
runVerify,
|
|
19
|
+
type ImportCommandOptions,
|
|
20
|
+
} from './importer/run-import.js'
|
|
21
|
+
|
|
22
|
+
type ParsedArgs = {
|
|
23
|
+
readonly command: string | null
|
|
24
|
+
readonly flags: ReadonlyMap<string, string | boolean>
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function parseArgs(argv: readonly string[]): ParsedArgs {
|
|
28
|
+
const positional: string[] = []
|
|
29
|
+
const flags = new Map<string, string | boolean>()
|
|
30
|
+
for (let index = 0; index < argv.length; index += 1) {
|
|
31
|
+
const arg: string | undefined = argv[index]
|
|
32
|
+
if (arg === undefined) {
|
|
33
|
+
continue
|
|
34
|
+
}
|
|
35
|
+
if (arg.startsWith('--')) {
|
|
36
|
+
const eq = arg.indexOf('=')
|
|
37
|
+
if (eq > 0) {
|
|
38
|
+
flags.set(arg.slice(2, eq), arg.slice(eq + 1))
|
|
39
|
+
} else {
|
|
40
|
+
const name = arg.slice(2)
|
|
41
|
+
const next: string | undefined = argv[index + 1]
|
|
42
|
+
if (next !== undefined && !next.startsWith('--')) {
|
|
43
|
+
flags.set(name, next)
|
|
44
|
+
index += 1
|
|
45
|
+
} else {
|
|
46
|
+
flags.set(name, true)
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
} else {
|
|
50
|
+
positional.push(arg)
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
return { command: positional[0] ?? null, flags }
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function usage(): string {
|
|
57
|
+
return `@payload-auth-link/migrate — the payload-auth → payload-auth-link migration kit
|
|
58
|
+
|
|
59
|
+
Commands:
|
|
60
|
+
doctor --config ./src/payload.config.ts [--src ./src] [--json] [--database-url URL]
|
|
61
|
+
from-payload-auth --config ./src/payload.config.ts [--apply] [--sessions=carry|drop]
|
|
62
|
+
[--manifest ./payload-auth-link-migration.json] [--database-url URL] [--json]
|
|
63
|
+
verify --config ./src/payload.config.ts [--database-url URL] [--json]
|
|
64
|
+
rollback --config ./src/payload.config.ts [--manifest ...]
|
|
65
|
+
finalize [--manifest ...]
|
|
66
|
+
|
|
67
|
+
doctor is read-only: it loads the config, inspects the users
|
|
68
|
+
collection and Better Auth plugins, scans sources for call sites
|
|
69
|
+
that must migrate, and (with a database URL) reports row counts,
|
|
70
|
+
unverified users, and orphans.
|
|
71
|
+
|
|
72
|
+
from-payload-auth is a DRY RUN by default: it prints planned row
|
|
73
|
+
counts per model, collisions, and anything unmappable. --apply
|
|
74
|
+
writes the payload-auth-link collections through the Payload Local
|
|
75
|
+
API, records every created row and field change in the manifest,
|
|
76
|
+
and never modifies the source tables. verify checks both sides;
|
|
77
|
+
rollback undoes exactly the manifest; finalize marks it read-only
|
|
78
|
+
so the legacy tables may be dropped later, in their own migration.
|
|
79
|
+
|
|
80
|
+
Exit codes: 0 clean, 1 blockers/mismatch, 2 warnings only, 3 usage errors.
|
|
81
|
+
`
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function formatReport(report: DoctorReport): string {
|
|
85
|
+
const lines: string[] = []
|
|
86
|
+
lines.push('== versions ==')
|
|
87
|
+
for (const finding of report.versions) {
|
|
88
|
+
lines.push(
|
|
89
|
+
`${finding.status === 'ok' ? ' ok ' : finding.status === 'unknown' ? ' ? ' : ' !! '} ${finding.name}: ${finding.installed ?? 'unresolved'} (band ${finding.band})`
|
|
90
|
+
)
|
|
91
|
+
}
|
|
92
|
+
lines.push('')
|
|
93
|
+
lines.push('== Better Auth plugins ==')
|
|
94
|
+
if (report.plugins.length === 0) {
|
|
95
|
+
lines.push(' none declared')
|
|
96
|
+
}
|
|
97
|
+
for (const finding of report.plugins) {
|
|
98
|
+
lines.push(` - ${finding.id}: ${finding.status} — ${finding.detail}`)
|
|
99
|
+
}
|
|
100
|
+
lines.push('')
|
|
101
|
+
lines.push('== users collection ==')
|
|
102
|
+
if (report.users === null) {
|
|
103
|
+
lines.push(' not found')
|
|
104
|
+
} else {
|
|
105
|
+
lines.push(` slug: ${report.users.slug}`)
|
|
106
|
+
const role = report.users.roleField
|
|
107
|
+
if (role !== null) {
|
|
108
|
+
lines.push(
|
|
109
|
+
` role field '${role.name}': ${role.type}${role.hasMany ? ' (hasMany)' : ''} values [${role.values.join(', ')}]${role.nonCanonical.length > 0 ? ` — NON-CANONICAL: ${role.nonCanonical.join(', ')}` : ''}`
|
|
110
|
+
)
|
|
111
|
+
} else {
|
|
112
|
+
lines.push(' role field: none found')
|
|
113
|
+
}
|
|
114
|
+
lines.push(` local strategy: ${report.users.localStrategy ? 'ON' : 'off'}`)
|
|
115
|
+
if (report.users.collisions.length > 0) {
|
|
116
|
+
lines.push(` collisions with injected fields: ${report.users.collisions.join(', ')}`)
|
|
117
|
+
}
|
|
118
|
+
for (const note of report.users.notes) {
|
|
119
|
+
lines.push(` note: ${note}`)
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
lines.push('')
|
|
123
|
+
lines.push('== source scan ==')
|
|
124
|
+
if (report.source.length === 0) {
|
|
125
|
+
lines.push(' no migration call sites found')
|
|
126
|
+
}
|
|
127
|
+
for (const finding of report.source) {
|
|
128
|
+
lines.push(
|
|
129
|
+
` - ${finding.kind}: ${finding.files.length} file(s)${finding.fields.length > 0 ? ` (fields: ${finding.fields.join(', ')})` : ''}`
|
|
130
|
+
)
|
|
131
|
+
for (const file of finding.files.slice(0, 8)) {
|
|
132
|
+
lines.push(` ${file}`)
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
lines.push('')
|
|
136
|
+
lines.push('== database ==')
|
|
137
|
+
if (report.database === null) {
|
|
138
|
+
lines.push(' not inspected')
|
|
139
|
+
} else {
|
|
140
|
+
for (const table of report.database.tables) {
|
|
141
|
+
lines.push(` ${table.name}: ${table.rows}`)
|
|
142
|
+
}
|
|
143
|
+
if (report.database.unverifiedUsers !== null) {
|
|
144
|
+
lines.push(` unverified users: ${report.database.unverifiedUsers}`)
|
|
145
|
+
}
|
|
146
|
+
for (const orphan of report.database.orphans) {
|
|
147
|
+
lines.push(` ORPHANS ${orphan.kind}: ${orphan.count}`)
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
lines.push('')
|
|
151
|
+
lines.push('== behaviour changes that will bite (0.7.0) ==')
|
|
152
|
+
for (const change of report.behaviourChanges) {
|
|
153
|
+
lines.push(` * ${change.title}: ${change.detail}`)
|
|
154
|
+
}
|
|
155
|
+
lines.push('')
|
|
156
|
+
lines.push('== blockers ==')
|
|
157
|
+
if (report.blockers.length === 0) {
|
|
158
|
+
lines.push(' none')
|
|
159
|
+
}
|
|
160
|
+
for (const issue of report.blockers) {
|
|
161
|
+
lines.push(` [${issue.code}] ${issue.detail}`)
|
|
162
|
+
}
|
|
163
|
+
lines.push('')
|
|
164
|
+
lines.push('== warnings ==')
|
|
165
|
+
if (report.warnings.length === 0) {
|
|
166
|
+
lines.push(' none')
|
|
167
|
+
}
|
|
168
|
+
for (const issue of report.warnings) {
|
|
169
|
+
lines.push(` [${issue.code}] ${issue.detail}`)
|
|
170
|
+
}
|
|
171
|
+
return lines.join('\n')
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
type StdSink = {
|
|
175
|
+
readonly write: (chunk: string) => void
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
async function main(
|
|
179
|
+
argv: readonly string[],
|
|
180
|
+
out: StdSink = process.stdout,
|
|
181
|
+
err: StdSink = process.stderr
|
|
182
|
+
): Promise<number> {
|
|
183
|
+
const { command, flags } = parseArgs(argv)
|
|
184
|
+
if (command === null || command === 'help' || flags.get('help') === true) {
|
|
185
|
+
out.write(usage())
|
|
186
|
+
return command === null && argv.length === 0 ? 3 : 0
|
|
187
|
+
}
|
|
188
|
+
const importCommands = ['from-payload-auth', 'verify', 'rollback', 'finalize']
|
|
189
|
+
if (command !== 'doctor' && !importCommands.includes(command)) {
|
|
190
|
+
err.write(`unknown command '${command}'\n\n${usage()}`)
|
|
191
|
+
return 3
|
|
192
|
+
}
|
|
193
|
+
if (importCommands.includes(command)) {
|
|
194
|
+
const configFlag = flags.get('config')
|
|
195
|
+
if (typeof configFlag !== 'string' || configFlag === '') {
|
|
196
|
+
err.write(`${command} requires --config <path to payload.config.ts>\n`)
|
|
197
|
+
return 3
|
|
198
|
+
}
|
|
199
|
+
const databaseUrl =
|
|
200
|
+
typeof flags.get('database-url') === 'string'
|
|
201
|
+
? (flags.get('database-url') as string)
|
|
202
|
+
: (process.env.DATABASE_URL ?? '')
|
|
203
|
+
if ((command === 'from-payload-auth' || command === 'verify') && databaseUrl === '') {
|
|
204
|
+
err.write(`${command} requires --database-url or DATABASE_URL\n`)
|
|
205
|
+
return 3
|
|
206
|
+
}
|
|
207
|
+
const sessionsFlag = flags.get('sessions')
|
|
208
|
+
const sessions = sessionsFlag === 'drop' ? ('drop' as const) : ('carry' as const)
|
|
209
|
+
const importOptions: ImportCommandOptions = {
|
|
210
|
+
configPath: new URL(configFlag, `file://${process.cwd()}/`).pathname,
|
|
211
|
+
databaseUrl,
|
|
212
|
+
apply: flags.get('apply') === true,
|
|
213
|
+
sessions,
|
|
214
|
+
...(typeof flags.get('manifest') === 'string'
|
|
215
|
+
? { manifestPath: flags.get('manifest') as string }
|
|
216
|
+
: {}),
|
|
217
|
+
...(typeof flags.get('slugs') === 'string'
|
|
218
|
+
? { slugsJson: flags.get('slugs') as string }
|
|
219
|
+
: {}),
|
|
220
|
+
json: flags.get('json') === true,
|
|
221
|
+
}
|
|
222
|
+
if (command === 'from-payload-auth') {
|
|
223
|
+
const outcome = await runFromPayloadAuth(importOptions)
|
|
224
|
+
if (outcome.plan.unmappable.length > 0) {
|
|
225
|
+
err.write(
|
|
226
|
+
`ABORT: ${outcome.plan.unmappable.length} unmappable row(s); nothing was silently skipped\n`
|
|
227
|
+
)
|
|
228
|
+
return 1
|
|
229
|
+
}
|
|
230
|
+
return 0
|
|
231
|
+
}
|
|
232
|
+
if (command === 'verify') {
|
|
233
|
+
const result = await runVerify(importOptions)
|
|
234
|
+
return result.ok ? 0 : 1
|
|
235
|
+
}
|
|
236
|
+
if (command === 'rollback') {
|
|
237
|
+
await runRollback(importOptions)
|
|
238
|
+
return 0
|
|
239
|
+
}
|
|
240
|
+
await runFinalize(importOptions)
|
|
241
|
+
return 0
|
|
242
|
+
}
|
|
243
|
+
const configPath = flags.get('config')
|
|
244
|
+
if (typeof configPath !== 'string' || configPath === '') {
|
|
245
|
+
err.write('doctor requires --config <path to payload.config.ts>\n')
|
|
246
|
+
return 3
|
|
247
|
+
}
|
|
248
|
+
const resolvedConfig = new URL(configPath, `file://${process.cwd()}/`).pathname
|
|
249
|
+
const srcFlag = flags.get('src')
|
|
250
|
+
const dbFlag = flags.get('database-url')
|
|
251
|
+
const report = await runDoctor({
|
|
252
|
+
configPath: resolvedConfig,
|
|
253
|
+
...(typeof srcFlag === 'string'
|
|
254
|
+
? { srcDir: new URL(srcFlag, `file://${process.cwd()}/`).pathname }
|
|
255
|
+
: {}),
|
|
256
|
+
...(typeof dbFlag === 'string' ? { databaseUrl: dbFlag } : {}),
|
|
257
|
+
})
|
|
258
|
+
if (flags.get('json') === true) {
|
|
259
|
+
out.write(`${JSON.stringify(report, null, 2)}\n`)
|
|
260
|
+
} else {
|
|
261
|
+
out.write(`${formatReport(report)}\n`)
|
|
262
|
+
}
|
|
263
|
+
if (report.blockers.length > 0) {
|
|
264
|
+
return 1
|
|
265
|
+
}
|
|
266
|
+
if (report.warnings.length > 0) {
|
|
267
|
+
return 2
|
|
268
|
+
}
|
|
269
|
+
return 0
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
const invokedDirectly =
|
|
273
|
+
process.argv[1]?.endsWith('cli.js') === true || process.argv[1]?.endsWith('cli.ts') === true
|
|
274
|
+
if (invokedDirectly) {
|
|
275
|
+
main(process.argv.slice(2))
|
|
276
|
+
.then((code) => {
|
|
277
|
+
process.exitCode = code
|
|
278
|
+
})
|
|
279
|
+
.catch((error: unknown) => {
|
|
280
|
+
process.stderr.write(
|
|
281
|
+
`doctor failed: ${error instanceof Error ? error.message : String(error)}\n`
|
|
282
|
+
)
|
|
283
|
+
process.exitCode = 1
|
|
284
|
+
})
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
export { main as cliMain }
|