mirror of
https://github.com/djdevin/recflare.git
synced 2026-10-25 15:09:18 -07:00
[econ] catalog fix
This commit is contained in:
@@ -72,9 +72,9 @@ export {
|
||||
*
|
||||
* `catalog_id` is the small NUMERIC handle the site uses where a key would be unwieldy. For a
|
||||
* row the store lists it is the listing's own `PurchasableItemId` — the number `sf3-2025.json`
|
||||
* sells it under — and for a skin the store never listed it is handed out by the loader from
|
||||
* `CATALOG_ID_BASE`, well above any real one. It is unique where set, and the migration that
|
||||
* adds it is 0016, since 0015 was already applied.
|
||||
* sells it under, including the skins we added to the store (numbered there from
|
||||
* `UNLISTED_SKIN_ID_BASE`, 1 000 000). A row the file does not list has none and is NULL. It is
|
||||
* unique where set, and the migration that adds it is 0016, since 0015 was already applied.
|
||||
*
|
||||
* Nullability is load-bearing rather than incidental: `tooltip` is genuinely NULL on some rows
|
||||
* of BOTH kinds and `""` on others, and the client's DTOs serve the distinction through, so the
|
||||
@@ -120,10 +120,9 @@ export interface CatalogRow {
|
||||
* the store's `PurchasableItemId`, so the number resolves a row here and a listing in
|
||||
* `sf3-2025.json` alike.
|
||||
*
|
||||
* Still not an identity: an unlisted skin's number is a load-order surrogate, and a store
|
||||
* could renumber. Never store it, never treat it as what an item IS — `item_key` is that,
|
||||
* and it is what the inventory holds. NULL only between a row existing and a load numbering
|
||||
* it.
|
||||
* Still not an identity: a store could renumber. Never store it, never treat it as what an
|
||||
* item IS — `item_key` is that. NULL for a row `sf3-2025.json` does not list (a merge keeps
|
||||
* such rows); every reader filters on `catalog_id IS NOT NULL`.
|
||||
*/
|
||||
catalog_id: number | null
|
||||
kind: string
|
||||
@@ -259,8 +258,8 @@ export async function getCatalogItem(db: D1Database, itemKey: string): Promise<C
|
||||
/**
|
||||
* One catalog row by its numeric handle — a store `PurchasableItemId` for a listed item.
|
||||
*
|
||||
* Fine for a request that looked the number up moments ago; never for anything stored, since
|
||||
* an unlisted skin's number is reassigned by every load.
|
||||
* The number the client looks items up by, since it is the one sf3-2025.json serves. Not for
|
||||
* anything stored: `item_key` is the identity, and a fresher store dump could renumber.
|
||||
*/
|
||||
export async function getCatalogItemById(
|
||||
db: D1Database,
|
||||
|
||||
@@ -204,17 +204,20 @@ async function verifyLoad(
|
||||
)
|
||||
}
|
||||
|
||||
// Every row must have come out with a numeric handle. A NULL here is a load that stopped
|
||||
// between writing rows and numbering them, which nothing else would notice until a lookup by
|
||||
// number quietly returned nothing.
|
||||
const unnumbered = await execSql(
|
||||
'SELECT COUNT(*) AS n FROM catalog WHERE catalog_id IS NULL',
|
||||
// Every row this load wrote must carry its `PurchasableItemId`. Ids are cleared before the
|
||||
// inserts and only the inserts set them, so exactly the loaded rows are numbered — fewer is a
|
||||
// load cut short, which nothing else would notice until a lookup by id quietly found nothing.
|
||||
// Rows the file does not list stay NULL by design and are not counted.
|
||||
const numbered = await execSql(
|
||||
'SELECT COUNT(*) AS n FROM catalog WHERE catalog_id IS NOT NULL',
|
||||
remote,
|
||||
OWNER
|
||||
)
|
||||
const missing = Number(unnumbered.results[0]?.n ?? 0)
|
||||
if (missing > 0) {
|
||||
throw new Error(`load did not finish: ${missing} row(s) have no catalog_id. Re-run it.`)
|
||||
const withId = Number(numbered.results[0]?.n ?? 0)
|
||||
if (withId !== rows.length) {
|
||||
throw new Error(
|
||||
`load did not finish: ${withId} row(s) carry a PurchasableItemId, expected ${rows.length}. Re-run it.`
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -265,8 +268,7 @@ const load = new Command('load')
|
||||
// load carries the store's numbers: without this, a row already holding one of them
|
||||
// (because it was in an earlier load under a different key, or the store renumbered)
|
||||
// collides and the whole merge fails. Nulling first also means a row left un-numbered
|
||||
// afterwards is visibly a row the store no longer lists, which the tail statement
|
||||
// below then numbers above everything this load assigned.
|
||||
// afterwards is visibly a row the store no longer lists.
|
||||
statements.push('UPDATE catalog SET catalog_id = NULL;')
|
||||
}
|
||||
for (let start = 0; start < rows.length; start += CHUNK) {
|
||||
@@ -279,18 +281,11 @@ const load = new Command('load')
|
||||
`\nON CONFLICT(item_key) DO UPDATE SET ${CONFLICT_UPDATE};`
|
||||
)
|
||||
}
|
||||
if (!opts.replace) {
|
||||
// Anything still un-numbered is a row the store no longer lists — a merge keeps those,
|
||||
// so they need handles too. They go ABOVE the highest id this load assigned, so they can
|
||||
// never collide with it, ordered by rowid for determinism. Usually this matches nothing
|
||||
// at all, and it is a single statement either way.
|
||||
const highest = Math.max(0, ...rows.map((r) => r.id))
|
||||
statements.push(
|
||||
`UPDATE catalog SET catalog_id = ${highest} +\n` +
|
||||
`\t(SELECT COUNT(*) FROM catalog c WHERE c.catalog_id IS NULL AND c.rowid <= catalog.rowid)\n` +
|
||||
`WHERE catalog_id IS NULL;`
|
||||
)
|
||||
}
|
||||
// No numbering step after the inserts. A row's `catalog_id` is its listing's
|
||||
// `PurchasableItemId` in sf3-2025.json and nothing else; a row a merge keeps that the file
|
||||
// no longer lists has no such number, so it stays NULL (every reader filters on
|
||||
// `catalog_id IS NOT NULL`). Inventing one would hand the client an id the store it is
|
||||
// served does not contain.
|
||||
const sql = statements.join('\n')
|
||||
|
||||
if (opts.dryRun) {
|
||||
|
||||
Reference in New Issue
Block a user