Files
inkreach-official-website/apps/api/scripts/import-data.mjs
T
yeuimu 6c61a4e871 feat(deploy): production deployment setup and fixes
- Debian-based api image (bookworm-slim), docker/debian mirrors, prisma
  binaryTargets for openssl 3.0
- nginx: admin SPA under /admin, TLS via acme.sh (ZeroSSL) + auto-renewal
  cron, http->https redirect
- prisma: add origin_goods.delisted migration, sync missing schema
  (good_image/tag_font_color/good_tags), fix users.createdAt Timestamptz
- api: CORS wildcard reflection, helmet CORP cross-origin, price
  backfill in persistProductDetail, categoryIcon ancestor fallback,
  mediaByColor per-color gallery in public goods detail
- admin: /admin base path (vite + router)
- import-data.mjs: udt_name casting, serial sequence advance fix
2026-08-26 14:23:09 +08:00

107 lines
3.5 KiB
JavaScript

/**
* Import a dump produced by export-data.mjs into the current database.
* Tables are truncated first (order-independent via session_replication_role)
* and columns are cast back to their real types using information_schema.
*
* Usage (inside the api container):
* node scripts/import-data.mjs <dump.json>
*/
import { PrismaClient, Prisma } from '@prisma/client';
import { readFileSync } from 'node:fs';
const prisma = new PrismaClient();
function toLiteral(value, udtName) {
if (value === null) return Prisma.sql`NULL`;
const target = Prisma.raw(`"${udtName}"`);
switch (udtName) {
case 'int2':
case 'int4':
case 'int8':
return Prisma.sql`${BigInt(value)}::${target}`;
case 'float4':
case 'float8':
case 'numeric':
return Prisma.sql`${Number(value)}::${target}`;
case 'bool':
return Prisma.sql`${!!value}::${target}`;
case 'timestamptz':
case 'timestamp':
return Prisma.sql`${new Date(value).toISOString()}::${target}`;
case 'date':
return Prisma.sql`${String(value)}::${target}`;
case 'jsonb':
case 'json':
return Prisma.sql`${typeof value === 'string' ? value : JSON.stringify(value)}::${target}`;
case 'bytea':
return Prisma.sql`${Buffer.from(value, 'base64')}::bytea`;
default:
// text, varchar, enums and anything else: pass as text and cast
return Prisma.sql`${String(value)}::${target}`;
}
}
async function main() {
const file = process.argv[2];
if (!file) {
console.error('Usage: node scripts/import-data.mjs <dump.json>');
process.exit(1);
}
const dump = JSON.parse(readFileSync(file, 'utf8'));
// Suspend FK checks during bulk load (postgres superuser not required for
// session_replication_role in the compose postgres where app user owns db).
await prisma.$executeRawUnsafe(`SET session_replication_role = replica`);
const summary = {};
for (const [table, rows] of Object.entries(dump.tables)) {
if (rows.length === 0) {
summary[table] = 0;
continue;
}
await prisma.$executeRawUnsafe(`TRUNCATE TABLE "${table}" CASCADE`);
const colTypes = {};
const info = await prisma.$queryRawUnsafe(
`SELECT column_name, udt_name FROM information_schema.columns WHERE table_name = '${table}'`,
);
for (const c of info) colTypes[c.column_name] = c.udt_name;
const columns = Object.keys(rows[0]);
const colList = Prisma.raw(columns.map((c) => `"${c}"`).join(', '));
const CHUNK = 200;
for (let i = 0; i < rows.length; i += CHUNK) {
const tuples = rows.slice(i, i + CHUNK).map(
(r) =>
Prisma.sql`(${Prisma.join(
columns.map((c) => toLiteral(r[c], colTypes[c])),
)})`,
);
await prisma.$executeRaw(
Prisma.sql`INSERT INTO ${Prisma.raw(`"${table}"`)} (${colList}) VALUES ${Prisma.join(tuples)}`,
);
}
// Keep sequences ahead of imported serial ids
// Keep serial sequences ahead of imported ids
const idCol = columns.find(
(c) => c === 'id' || c === `${table.replace(/s$/, '')}_id`,
);
if (idCol) {
await prisma.$executeRawUnsafe(
`SELECT setval(pg_get_serial_sequence('"${table}"', '${idCol}'), COALESCE((SELECT MAX("${idCol}") FROM "${table}"), 1))`,
);
}
summary[table] = rows.length;
}
await prisma.$executeRawUnsafe(`SET session_replication_role = DEFAULT`);
console.log('Imported:', JSON.stringify(summary, null, 2));
}
main()
.catch((e) => {
console.error(e);
process.exit(1);
})
.finally(() => prisma.$disconnect());