mirror of
https://github.com/djdevin/recflare.git
synced 2026-10-27 07:58:55 -07:00
[api] catalog loader
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
/**
|
||||
* Turns an export of first-party custom avatar items into a D1 migration that seeds the
|
||||
* `custom_avatar_item` table with them, one `INSERT` per record, the way
|
||||
* `apps/rooms/migrations/0002_import_rooms.sql` seeds the rooms.
|
||||
* `custom_avatar_item` table with them, the way `apps/rooms/migrations/0002_import_rooms.sql`
|
||||
* seeds the rooms.
|
||||
*
|
||||
* bun apps/api/scripts/import-custom-avatar-items.ts <export.json> <NNNN>
|
||||
*
|
||||
@@ -9,11 +9,11 @@
|
||||
* them (a `{ Results: [...] }` page or a single record are accepted too). `NNNN` is the
|
||||
* migration number; the file is written to `apps/api/migrations/NNNN_import_custom_avatar_items.sql`.
|
||||
*
|
||||
* Each record is stored VERBATIM as the row's JSON (the table is a JSON blob per row, see
|
||||
* src/custom-avatar-items-db.ts) minus `PurchaseInfo`, which is a store-side projection and
|
||||
* never stored. A record whose id is already present is REPLACED, so re-running an import
|
||||
* with corrected data lands; a `DELETE` is not issued, so player-made items and previously
|
||||
* imported items the new export lacks are left alone.
|
||||
* For a load that needs no migration and no deploy — the usual case — use
|
||||
* `just admin cai-load` instead, which runs the same statements straight against a database.
|
||||
* The statements come from src/custom-avatar-items-load.ts either way: each record stored as
|
||||
* the row's JSON minus `PurchaseInfo` and with `CreatorAccountId` forced to the Coach account
|
||||
* (1), an existing id REPLACED, nothing deleted.
|
||||
*
|
||||
* The saves name assetbundles and thumbnails by bare filename; the migration does nothing
|
||||
* about those files, which have to be served from wherever the client fetches them.
|
||||
@@ -21,11 +21,10 @@
|
||||
|
||||
import { readFileSync, writeFileSync } from 'node:fs'
|
||||
|
||||
interface Record_ {
|
||||
CustomAvatarItemId: string
|
||||
PurchaseInfo?: unknown
|
||||
[key: string]: unknown
|
||||
}
|
||||
import {
|
||||
customAvatarItemUpsertStatements,
|
||||
readCustomAvatarItemExport,
|
||||
} from '../src/custom-avatar-items-load'
|
||||
|
||||
const [input, number] = process.argv.slice(2)
|
||||
if (!input || !number || !/^\d{4}$/.test(number)) {
|
||||
@@ -33,34 +32,18 @@ if (!input || !number || !/^\d{4}$/.test(number)) {
|
||||
process.exit(2)
|
||||
}
|
||||
|
||||
const parsed: unknown = JSON.parse(readFileSync(input, 'utf8'))
|
||||
const records: Record_[] = Array.isArray(parsed)
|
||||
? parsed
|
||||
: parsed && typeof parsed === 'object' && Array.isArray((parsed as { Results?: unknown }).Results)
|
||||
? (parsed as { Results: Record_[] }).Results
|
||||
: [parsed as Record_]
|
||||
|
||||
const seen = new Set<string>()
|
||||
const lines: string[] = [
|
||||
// Tolerate the BOM a .NET export carries (`JSON.parse` rejects U+FEFF).
|
||||
const records = readCustomAvatarItemExport(
|
||||
JSON.parse(readFileSync(input, 'utf8').replace(/^/, ''))
|
||||
)
|
||||
const lines = [
|
||||
`-- Seed first-party custom avatar items (${records.length} records) from ${input.split('/').pop()}.`,
|
||||
'-- Generated by apps/api/scripts/import-custom-avatar-items.ts — do not edit by hand.',
|
||||
'-- Each record is the official `CustomAvatarItem` verbatim; an existing id is replaced.',
|
||||
'-- Each record is the official `CustomAvatarItem`, filed under the Coach account (CreatorAccountId 1);',
|
||||
'-- an existing id is replaced.',
|
||||
'',
|
||||
...customAvatarItemUpsertStatements(records),
|
||||
]
|
||||
for (const record of records) {
|
||||
if (typeof record.CustomAvatarItemId !== 'string' || record.CustomAvatarItemId === '') {
|
||||
throw new Error(`record without a CustomAvatarItemId: ${JSON.stringify(record).slice(0, 80)}`)
|
||||
}
|
||||
if (seen.has(record.CustomAvatarItemId)) {
|
||||
throw new Error(`duplicate CustomAvatarItemId in export: ${record.CustomAvatarItemId}`)
|
||||
}
|
||||
seen.add(record.CustomAvatarItemId)
|
||||
const { PurchaseInfo: _purchaseInfo, ...stored } = record
|
||||
const json = JSON.stringify(stored).replaceAll("'", "''")
|
||||
lines.push(
|
||||
`INSERT INTO custom_avatar_item (data) VALUES ('${json}') ON CONFLICT(custom_avatar_item_id) DO UPDATE SET data = excluded.data;`
|
||||
)
|
||||
}
|
||||
|
||||
const out = decodeURIComponent(
|
||||
new URL(`../migrations/${number}_import_custom_avatar_items.sql`, import.meta.url).pathname
|
||||
|
||||
Reference in New Issue
Block a user