fix(tracker-seed): cap dedupe list at limit=100 + auto-register products

Two bugs caused duplicate items on re-run: the dedupe list used limit=500
(server caps at 100 -> 400 -> silent empty set -> dupes), and meta productIds
weren't registered so GET /items 400'd ("Unknown product"). Now registers every
referenced product first (idempotent) and lists with limit=100; dedupe failures
are logged loudly. Verified idempotent: re-run skips all 16.

Generated with [Devin](https://cli.devin.ai/docs)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
This commit is contained in:
saravanakumardb1 2026-05-30 23:45:16 -07:00
parent ae7909018a
commit abc8a0f517

View File

@ -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)`);