fix: claude issue

This commit is contained in:
zk
2026-05-18 16:21:12 +00:00
parent d5531907f2
commit aa8bd4043a
2 changed files with 300 additions and 22 deletions
@@ -244,18 +244,39 @@ function tsReq(ms: number): string {
// ──────────────────────────────────────────────────────────────────────────────
// API permission helpers
// (Old MongoDB data uses dash-separated permission names, not the underscore
// form in the current APIPermissions type - kept as plain string lookups.)
// Mongo validates against ALL_PERMISSIONS (underscore keys such as customise_profile).
// Some dumps may instead use dashed legacy spellings — accept either when migrating.
// ──────────────────────────────────────────────────────────────────────────────
function permissionKeyDashVariant(canonicalUnderscoreKey: string): string {
return canonicalUnderscoreKey.replaceAll("_", "-");
}
/** Extract one Postgres pm_* column from a MongoDB requestedPermissions array. */
function perm(permissions: Array<string>, key: string): boolean | null {
return permissions.includes(key) ? true : null;
function perm(permissions: Array<string>, canonicalUnderscoreKey: string): boolean | null {
const dashed = permissionKeyDashVariant(canonicalUnderscoreKey);
return permissions.includes(canonicalUnderscoreKey) || permissions.includes(dashed)
? true
: null;
}
/** Extract one Postgres pm_* column from a MongoDB permissions record. */
function permRec(permissions: Record<string, boolean>, key: string): boolean | null {
return permissions[key] ?? null;
function permRec(
permissions: Record<string, boolean>,
canonicalUnderscoreKey: string,
): boolean | null {
const dashed = permissionKeyDashVariant(canonicalUnderscoreKey);
if (Object.hasOwn(permissions, canonicalUnderscoreKey)) {
return permissions[canonicalUnderscoreKey]!;
}
if (Object.hasOwn(permissions, dashed)) {
return permissions[dashed]!;
}
return null;
}
// ──────────────────────────────────────────────────────────────────────────────
@@ -650,14 +671,14 @@ async function main(): Promise<void> {
client_secret: c.clientSecret,
name: c.name,
author: c.author,
pm_customise_profile: perm(p, "customise-profile"),
pm_customise_score: perm(p, "customise-score"),
pm_customise_session: perm(p, "customise-session"),
pm_delete_score: perm(p, "delete-score"),
pm_manage_rivals: perm(p, "manage-rivals"),
pm_manage_targets: perm(p, "manage-targets"),
pm_submit_score: perm(p, "submit-score"),
pm_manage_challenges: perm(p, "manage-challenges"),
pm_customise_profile: perm(p, "customise_profile"),
pm_customise_score: perm(p, "customise_score"),
pm_customise_session: perm(p, "customise_session"),
pm_delete_score: perm(p, "delete_score"),
pm_manage_rivals: perm(p, "manage_rivals"),
pm_manage_targets: perm(p, "manage_targets"),
pm_submit_score: perm(p, "submit_score"),
pm_manage_challenges: perm(p, "manage_challenges"),
api_key_template: c.apiKeyTemplate,
api_key_filename: c.apiKeyFilename,
};
@@ -1352,14 +1373,14 @@ async function main(): Promise<void> {
user_id: t.userID!,
identifier: t.identifier,
pm_customise_profile: permRec(p, "customise-profile"),
pm_customise_score: permRec(p, "customise-score"),
pm_customise_session: permRec(p, "customise-session"),
pm_delete_score: permRec(p, "delete-score"),
pm_manage_rivals: permRec(p, "manage-rivals"),
pm_manage_targets: permRec(p, "manage-targets"),
pm_submit_score: permRec(p, "submit-score"),
pm_manage_challenges: permRec(p, "manage-challenges"),
pm_customise_profile: permRec(p, "customise_profile"),
pm_customise_score: permRec(p, "customise_score"),
pm_customise_session: permRec(p, "customise_session"),
pm_delete_score: permRec(p, "delete_score"),
pm_manage_rivals: permRec(p, "manage_rivals"),
pm_manage_targets: permRec(p, "manage_targets"),
pm_submit_score: permRec(p, "submit_score"),
pm_manage_challenges: permRec(p, "manage_challenges"),
from_oauth2_client: t.fromAPIClient,
};
});
@@ -0,0 +1,257 @@
/**
* Re-import Mongo `api-clients` → Postgres `priv_api_client` and Mongo `api-tokens` → `priv_api_token`.
*
* Use after correcting permission-field mapping bugs — upserts Mongo rows into Postgres (overwrite on PK conflict).
*
* Postgres dependency order:
* If you truncate tokens only: `TRUNCATE priv_api_token;`
* If you redo clients too (recommended when fixing client permission columns):
* `TRUNCATE priv_api_token, priv_api_client;`
* (ordering does not matter in one statement; tokens must be cleared if you truncate clients.)
*
* Heads-up: `priv_discord_user_map` FKs into `priv_api_token` — resetting tokens may unlink Discord API mapping rows until you reconcile.
*
* Env:
* MONGO_URL — MongoDB URL (default: mongodb://mongo/tachi)
* POSTGRES_URL — required
*
* From `typescript/server`:
* MONGO_URL=... POSTGRES_URL=... bun run src/scripts/reimport-priv-api-from-mongo.ts
*/
import type { Database, NewPrivApiClient, NewPrivApiToken } from "tachi-db";
import {
type Insertable,
Kysely,
PostgresDialect,
type RawBuilder,
sql,
} from "kysely";
import monk from "monk";
import { Pool } from "pg";
import type {
MongoApiClientsCollectionDocument,
MongoApiTokensCollectionDocument,
} from "./migrate-to-postgres.mongo-docs";
const MONGO_URL = process.env.MONGO_URL ?? "mongodb://mongo/tachi";
const POSTGRES_URL = process.env.POSTGRES_URL;
if (!POSTGRES_URL) {
console.error("[reimport-priv-api] POSTGRES_URL is not set.");
process.exit(1);
}
const mongoDB = monk(MONGO_URL);
const pg = new Kysely<Database>({
dialect: new PostgresDialect({
pool: new Pool({ connectionString: POSTGRES_URL }),
}),
});
// ── Same permission normalization as migrate-to-postgres.ts ─────────────────
function permissionKeyDashVariant(canonicalUnderscoreKey: string): string {
return canonicalUnderscoreKey.replaceAll("_", "-");
}
/** requestedPermissions entries may use underscore (`customise_profile`) or dashed legacy strings. */
function perm(permissions: Array<string>, canonicalUnderscoreKey: string): boolean | null {
const dashed = permissionKeyDashVariant(canonicalUnderscoreKey);
return permissions.includes(canonicalUnderscoreKey) || permissions.includes(dashed)
? true
: null;
}
function permRec(
permissions: Record<string, boolean>,
canonicalUnderscoreKey: string,
): boolean | null {
const dashed = permissionKeyDashVariant(canonicalUnderscoreKey);
if (Object.hasOwn(permissions, canonicalUnderscoreKey)) {
return permissions[canonicalUnderscoreKey]!;
}
if (Object.hasOwn(permissions, dashed)) {
return permissions[dashed]!;
}
return null;
}
const INSERT_CHUNK = 500;
/** ON CONFLICT DO UPDATE — set each column from the proposed row (`excluded`). */
function onConflictExcludedAll(exampleRow: Record<string, unknown>): Record<string, RawBuilder<unknown>> {
const toSet: Record<string, RawBuilder<unknown>> = {};
for (const key of Object.keys(exampleRow)) {
toSet[key] = sql`excluded.${sql.ref(key)}`;
}
return toSet;
}
async function batchUpsertPrivApiClient(rows: ReadonlyArray<Insertable<Database["priv_api_client"]>>): Promise<void> {
if (rows.length === 0) {
return;
}
for (let i = 0; i < rows.length; i = i + INSERT_CHUNK) {
const chunk = rows.slice(i, i + INSERT_CHUNK);
// Sequential chunks — avoid oversized multi-row INSERT payloads.
// eslint-disable-next-line no-await-in-loop -- bounded batch upserts
await pg
.insertInto("priv_api_client")
.values(chunk as never)
.onConflict((oc) =>
oc.column("client_id").doUpdateSet(onConflictExcludedAll(chunk[0] as Record<string, unknown>)),
)
.execute();
}
}
async function batchUpsertPrivApiToken(rows: ReadonlyArray<Insertable<Database["priv_api_token"]>>): Promise<void> {
if (rows.length === 0) {
return;
}
for (let i = 0; i < rows.length; i = i + INSERT_CHUNK) {
const chunk = rows.slice(i, i + INSERT_CHUNK);
// eslint-disable-next-line no-await-in-loop -- bounded batch upserts
await pg
.insertInto("priv_api_token")
.values(chunk as never)
.onConflict((oc) =>
oc.column("token").doUpdateSet(onConflictExcludedAll(chunk[0] as Record<string, unknown>)),
)
.execute();
}
}
function getFromApiClient(doc: MongoApiTokensCollectionDocument): string | null {
const d = doc as {
fromOAuth2Client?: string | null;
} & MongoApiTokensCollectionDocument;
if (typeof d.fromOAuth2Client === "string") {
return d.fromOAuth2Client;
}
return d.fromAPIClient;
}
function mapApiClientRow(c: MongoApiClientsCollectionDocument): NewPrivApiClient {
const extended = c as {
redirectURI?: string | null;
redirectUri?: string | null;
webhookURI?: string | null;
webhookUri?: string | null;
} & MongoApiClientsCollectionDocument;
const p = c.requestedPermissions as unknown as Array<string>;
return {
client_id: c.clientID,
client_secret: c.clientSecret,
name: c.name,
author: c.author,
pm_customise_profile: perm(p, "customise_profile"),
pm_customise_score: perm(p, "customise_score"),
pm_customise_session: perm(p, "customise_session"),
pm_delete_score: perm(p, "delete_score"),
pm_manage_rivals: perm(p, "manage_rivals"),
pm_manage_targets: perm(p, "manage_targets"),
pm_submit_score: perm(p, "submit_score"),
pm_manage_challenges: perm(p, "manage_challenges"),
api_key_template: c.apiKeyTemplate,
api_key_filename: c.apiKeyFilename,
redirect_uri: extended.redirectUri ?? extended.redirectURI ?? null,
webhook_uri: extended.webhookUri ?? extended.webhookURI ?? null,
};
}
async function main(): Promise<void> {
console.log("[reimport-priv-api] fetching api-clients from Mongo...");
const clients = await mongoDB
.get<MongoApiClientsCollectionDocument>("api-clients")
.find({});
const clientRows: Array<NewPrivApiClient> = clients.map((c) => mapApiClientRow(c));
console.log(
`[reimport-priv-api] upserting ${clientRows.length} row(s) into priv_api_client (conflict target: client_id)...`,
);
await batchUpsertPrivApiClient(clientRows);
console.log("[reimport-priv-api] fetching api-tokens from Mongo...");
const apiTokens = await mongoDB
.get<MongoApiTokensCollectionDocument>("api-tokens")
.find({});
const existingClientIds = new Set(
(await pg.selectFrom("priv_api_client").select("client_id").execute()).map(
(r) => r.client_id,
),
);
const validTokens = apiTokens.filter((t) => {
const fromClient = getFromApiClient(t);
return (
t.token !== null &&
t.userID !== null &&
(fromClient === null || fromClient === undefined || existingClientIds.has(fromClient))
);
});
if (validTokens.length !== apiTokens.length) {
console.warn(
`[reimport-priv-api] Skipping ${apiTokens.length - validTokens.length} token row(s): null token/userID or dangling oauth client`,
);
}
const tokenRows: Array<NewPrivApiToken> = validTokens.map((t) => {
const fromClient = getFromApiClient(t);
const p =
t.permissions && typeof t.permissions === "object"
? t.permissions
: ({} as Record<string, boolean>);
return {
token: t.token!,
user_id: t.userID!,
identifier: t.identifier,
pm_customise_profile: permRec(p, "customise_profile"),
pm_customise_score: permRec(p, "customise_score"),
pm_customise_session: permRec(p, "customise_session"),
pm_delete_score: permRec(p, "delete_score"),
pm_manage_rivals: permRec(p, "manage_rivals"),
pm_manage_targets: permRec(p, "manage_targets"),
pm_submit_score: permRec(p, "submit_score"),
pm_manage_challenges: permRec(p, "manage_challenges"),
from_oauth2_client: fromClient ?? null,
};
});
console.log(
`[reimport-priv-api] upserting ${tokenRows.length} row(s) into priv_api_token (conflict target: token)...`,
);
await batchUpsertPrivApiToken(tokenRows);
console.log("[reimport-priv-api] done.");
await mongoDB.close();
await pg.destroy();
}
main().catch((err) => {
console.error("[reimport-priv-api]", err);
process.exit(1);
});