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.
83 lines
2.8 KiB
TypeScript
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);
|
|
});
|