WFDASH-WEBAPP/scripts/migrations/2026-06-30-add-price-list-search-text.ts
SJCoetz 1fb67efaee 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 12:10:27 +02:00

83 lines
2.8 KiB
TypeScript

/**
* 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);
});