mirror of
https://github.com/docmost/docmost.git
synced 2026-05-07 06:23:06 +08:00
chore(server): one-shot script to clean poisoned base view configs
This commit is contained in:
@@ -0,0 +1,101 @@
|
|||||||
|
import * as path from 'path';
|
||||||
|
import * as dotenv from 'dotenv';
|
||||||
|
import { Kysely } from 'kysely';
|
||||||
|
import { PostgresJSDialect } from 'kysely-postgres-js';
|
||||||
|
import postgres from 'postgres';
|
||||||
|
|
||||||
|
/*
|
||||||
|
* One-shot cleanup for `base_views.config` rows that were poisoned by an
|
||||||
|
* earlier bug where `{...config}` spread a jsonb-stored string `"{}"`
|
||||||
|
* into character-indexed keys (`"0": "{"`, `"1": "}"`). Strips any
|
||||||
|
* all-digit string keys from each view's config.
|
||||||
|
*/
|
||||||
|
|
||||||
|
const envFilePath = path.resolve(process.cwd(), '..', '..', '.env');
|
||||||
|
dotenv.config({ path: envFilePath });
|
||||||
|
|
||||||
|
function normalizePostgresUrl(url: string): string {
|
||||||
|
const parsed = new URL(url);
|
||||||
|
const newParams = new URLSearchParams();
|
||||||
|
for (const [key, value] of parsed.searchParams) {
|
||||||
|
if (key === 'sslmode' && value === 'no-verify') continue;
|
||||||
|
if (key === 'schema') continue;
|
||||||
|
newParams.append(key, value);
|
||||||
|
}
|
||||||
|
parsed.search = newParams.toString();
|
||||||
|
return parsed.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
const db = new Kysely<any>({
|
||||||
|
dialect: new PostgresJSDialect({
|
||||||
|
postgres: postgres(normalizePostgresUrl(process.env.DATABASE_URL!)),
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
function hasDigitKeys(config: unknown): boolean {
|
||||||
|
if (!config || typeof config !== 'object' || Array.isArray(config)) return false;
|
||||||
|
return Object.keys(config).some((k) => /^\d+$/.test(k));
|
||||||
|
}
|
||||||
|
|
||||||
|
function stripDigitKeys(config: Record<string, unknown>): Record<string, unknown> {
|
||||||
|
const out: Record<string, unknown> = {};
|
||||||
|
for (const [k, v] of Object.entries(config)) {
|
||||||
|
if (/^\d+$/.test(k)) continue;
|
||||||
|
out[k] = v;
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
const views = await db
|
||||||
|
.selectFrom('base_views')
|
||||||
|
.select(['id', 'name', 'base_id', 'config'])
|
||||||
|
.execute();
|
||||||
|
|
||||||
|
console.log(`Scanning ${views.length} views...`);
|
||||||
|
|
||||||
|
let fixed = 0;
|
||||||
|
let skipped = 0;
|
||||||
|
let stringConfigs = 0;
|
||||||
|
|
||||||
|
for (const v of views) {
|
||||||
|
let config = v.config;
|
||||||
|
|
||||||
|
// Case 1: config is a STRING (e.g. the original "{}" bug). Replace
|
||||||
|
// with an empty object.
|
||||||
|
if (typeof config === 'string') {
|
||||||
|
stringConfigs++;
|
||||||
|
await db
|
||||||
|
.updateTable('base_views')
|
||||||
|
.set({ config: {} as any })
|
||||||
|
.where('id', '=', v.id)
|
||||||
|
.execute();
|
||||||
|
console.log(` [string→{}] ${v.id} ${v.name}`);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Case 2: config is an object with poisoned digit keys.
|
||||||
|
if (hasDigitKeys(config)) {
|
||||||
|
const cleaned = stripDigitKeys(config as Record<string, unknown>);
|
||||||
|
await db
|
||||||
|
.updateTable('base_views')
|
||||||
|
.set({ config: cleaned as any })
|
||||||
|
.where('id', '=', v.id)
|
||||||
|
.execute();
|
||||||
|
fixed++;
|
||||||
|
console.log(` [strip digit] ${v.id} ${v.name}`);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
skipped++;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`\nDone. fixed=${fixed} stringified=${stringConfigs} clean=${skipped}`);
|
||||||
|
await db.destroy();
|
||||||
|
process.exit(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
main().catch((err) => {
|
||||||
|
console.error('Cleanup failed:', err);
|
||||||
|
db.destroy().finally(() => process.exit(1));
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user