diff --git a/scripts/tracker-seed/seed-tracker-items.mjs b/scripts/tracker-seed/seed-tracker-items.mjs index 2b7da26..2def342 100644 --- a/scripts/tracker-seed/seed-tracker-items.mjs +++ b/scripts/tracker-seed/seed-tracker-items.mjs @@ -63,7 +63,8 @@ function mintToken() { } async function listTitles(token, productId) { - const url = `${API}/api/items?productId=${encodeURIComponent(productId)}&limit=500`; + // limit max is 100 (server caps it); 500 would 400 and silently break dedupe. + const url = `${API}/api/items?productId=${encodeURIComponent(productId)}&limit=100`; const res = await fetch(url, { headers: { authorization: `Bearer ${token}`, 'x-product-id': productId }, }); @@ -72,6 +73,36 @@ async function listTitles(token, productId) { return new Set((data.items || []).map((i) => i.title)); } +/** Derive an uppercase-letters-only license prefix (<=8) from a productId. */ +function licensePrefix(productId) { + return productId.replace(/[^a-z]/gi, '').toUpperCase().slice(0, 8) || 'PRD'; +} + +/** + * Ensure a product exists before creating items under it. POST /items skips + * product validation when productId is in the body, but GET /items validates — + * so without registration the dedupe list 400s ("Unknown product") and + * re-runs create duplicates. Idempotent: treats 409/200/201 as success. + */ +async function ensureProduct(token, productId) { + const res = await fetch(`${API}/api/products`, { + method: 'POST', + headers: { + authorization: `Bearer ${token}`, + 'content-type': 'application/json', + 'x-product-id': productId, + }, + body: JSON.stringify({ + productId, + displayName: productId.charAt(0).toUpperCase() + productId.slice(1), + licensePrefix: licensePrefix(productId), + }), + }); + if (!res.ok && res.status !== 409) { + console.warn(` warn: register ${productId} -> HTTP ${res.status}`); + } +} + async function createItem(token, item) { const res = await fetch(`${API}/api/items`, { method: 'POST', @@ -102,6 +133,13 @@ async function main() { } const token = mintToken(); + + // Register every referenced product first so the dedupe list (GET /items) + // doesn't 400 on unknown products — which would silently cause duplicates. + const productIds = [...new Set(items.map((it) => it.productId))]; + console.log(`Ensuring ${productIds.length} product(s) are registered...`); + for (const pid of productIds) await ensureProduct(token, pid); + const titleCache = new Map(); let created = 0; let skipped = 0; @@ -111,7 +149,13 @@ async function main() { try { if (!FORCE) { if (!titleCache.has(it.productId)) { - titleCache.set(it.productId, await listTitles(token, it.productId).catch(() => new Set())); + titleCache.set( + it.productId, + await listTitles(token, it.productId).catch((e) => { + console.warn(` warn: dedupe list for ${it.productId} failed (${e.message}); may create duplicates`); + return new Set(); + }) + ); } if (titleCache.get(it.productId).has(it.title)) { console.log(` SKIP ${it.productId.padEnd(16)} ${it.title} (already exists)`);