WFDASH-WEBAPP/scripts/migrations/2026-06-30-add-price-list-search-text.ts

84 lines
2.8 KiB
TypeScript
Raw Normal View History

feat(pricelist): Catalogue search hits every field (incl. synonyms) The Catalogue tab's search box used to OR across just sku, description, and brand. Now it hits: - sku, description, brand, supplier, category, subcategory (PB's case-insensitive `~` operator, each indexed) - search_text (a denormalised text blob) The denormalised column is the key part: PB can't substring- match a JSON field, so without this, synonyms from `specifications.synonyms` ("slugger cutter", "turbo tough", etc.) would never match in the search box. The n8n workflow's Code node now flattens every searchable bit — including the synonyms array — into search_text on every POST. The 46 pre- existing rows were backfilled once with the same formula. The `price_list.supplier` field also stored the PB id on the 46 pre-existing rows (n8n was setting the resolved name, but the original LLM tests that seeded the table didn't). Fixed that in the same backfill — supplier now reads 'JEISA - WESTSCOTT GROUP' instead of 'q6hpwbxsicyoiuu', which makes the supplier filter dropdown work AND makes 'supplier ~ "jeisa"' match. Migration `2026-06-30-add-price-list-search-text`: - Adds `search_text` (text) column - Adds `idx_pl_search_text` non-unique index - Idempotent — re-runs are safe and self-heal the index Updates: - `getPriceLists` helper ORs across all 6 columns + search_text - `PriceList` TS type gains `search_text: string` - `pricelist-extraction.md` Code node snippet shows how the n8n workflow should populate `search_text` on every POST - Search input placeholder reads 'Search anything — SKU, description, brand, supplier, synonyms…' Tests: 229/230 (same pre-existing failure, unrelated). TypeScript clean.
2026-06-30 10:10:27 +00:00
/**
* Migration — add `search_text` (text) to `price_list`.
*
* PB can't substring-match a JSON field, so for the Catalogue tab's
* "search everything" feature, we denormalize the searchable bits of
* each row into a flat text field. The WFDASH Catalogue helper
* (`getPriceLists`) then ORs against `search_text` and the other
* string columns.
*
* What's in search_text (per row, joined with spaces):
* - sku
* - description
* - brand
* - supplier
* - category
* - subcategory
* - specifications.synonyms (the buyer-friendly aliases)
*
* Idempotent: re-runs are safe.
*
* Run with:
* npm run pb:migrate:price-list-search-text
*/
import PocketBase from "pocketbase";
const PB_URL = process.env.NEXT_PUBLIC_POCKETBASE_URL || "http://127.0.0.1:8090";
const ADMIN_EMAIL = process.env.PB_ADMIN_EMAIL;
const ADMIN_PASSWORD = process.env.PB_ADMIN_PASSWORD;
if (!ADMIN_EMAIL || !ADMIN_PASSWORD) {
throw new Error("PB_ADMIN_EMAIL and PB_ADMIN_PASSWORD are required.");
}
async function main() {
const pb = new PocketBase(PB_URL);
await pb.collection("_superusers").authWithPassword(ADMIN_EMAIL, ADMIN_PASSWORD);
const cols = await pb.collections.getFullList();
const priceList = cols.find((c) => c.name === "price_list");
if (!priceList) {
throw new Error("price_list collection not found.");
}
// Always re-check the index — running the migration on a
// collection that already has `search_text` should still ensure
// the index exists (handles the case where the field was added
// before the index step was introduced).
const INDEX_SQL = "CREATE INDEX `idx_pl_search_text` ON `price_list` (`search_text`)";
const hasIndex = (priceList.indexes || []).some(
(i) => typeof i === "string" && i.includes("idx_pl_search_text"),
);
if (priceList.fields.some((f) => f.name === "search_text")) {
if (hasIndex) {
console.log("price_list.search_text + index already present, skipping.");
return;
}
await pb.collections.update(priceList.id, { indexes: [...(priceList.indexes || []), INDEX_SQL] });
console.log("Added price_list index on search_text (field already existed).");
console.log("search_text index migration complete.");
return;
}
await pb.collections.update(priceList.id, {
fields: [
...priceList.fields,
{ name: "search_text", type: "text", required: false, options: { max: 0 } },
],
// Add a non-unique index so the search box doesn't full-scan
// the table once we have thousands of rows.
indexes: [
...(priceList.indexes || []),
"CREATE INDEX `idx_pl_search_text` ON `price_list` (`search_text`)",
],
});
console.log("Added price_list.search_text (+ index)");
console.log("search_text migration complete.");
}
main().catch((err) => {
console.error(err);
process.exit(1);
});