Files
mozimo.in/scripts/pb-seed.mjs
T
tanshu 328a1a23e0 Agent skills
Prettier
eslint
shopify
2026-09-06 06:38:04 +00:00

483 lines
14 KiB
JavaScript

#!/usr/bin/env node
/**
* PocketBase bootstrap — idempotent.
*
* 1. ensures the editorial collections exist (gifting_tabs, collection_rows,
* gifting_page) with public read rules;
* 2. seeds each empty collection with the content currently shipped in the
* site bundle, so a fresh PocketBase looks exactly like the static site.
*
* Runs at container start (entrypoint) and can be re-run any time:
* node --env-file=.env scripts/pb-seed.mjs
*
* Env:
* PB_URL default http://127.0.0.1:8090
* PB_SUPERUSER_EMAIL required to seed
* PB_SUPERUSER_PASSWORD required to seed
*
* Never fatal: any failure logs a warning and exits 0 — the site falls back
* to its bundled defaults until PocketBase is ready.
*/
const PB_URL = (process.env.PB_URL ?? "http://127.0.0.1:8090").replace(
/\/$/,
"",
);
const EMAIL = process.env.PB_SUPERUSER_EMAIL;
const PASSWORD = process.env.PB_SUPERUSER_PASSWORD;
const IMAGE_MIME = [
"image/jpeg",
"image/png",
"image/webp",
"image/avif",
"image/gif",
];
/* ── defaults: mirror of the shipped site content ──────────── */
const GIFTING_TABS = [
{
name: "Signature",
note: "From ₹265",
title: "Small gestures, remembered.",
blurb:
"Thoughtful everyday gifting — a bar, a little box, a note of chocolate that says enough.",
panel: "products",
tile_url: "/products/mozi14.jpg",
product_handles: "mozimo-dark-chocolate-bar-85\n9-pralines-box\nbars-box",
collection_handle: "gift-collection",
cta_label: "View all Signature gifts",
cta_url: "https://shop.mozimo.in/collections/gift-collection",
sort: 1,
visible: true,
},
{
name: "Best Sellers",
note: "From ₹825",
title: "The boxes they ask for by name.",
blurb:
"The spreads, dragees and bars our regulars return for — gifting that never misses.",
panel: "products",
tile_url: "/products/mozi16.jpg",
product_handles:
"pistachio-spread-60\nmozimo-almond-dragees-60\nkunafa-chocolate-bar\nspreads-box",
collection_handle: "best-sellers",
cta_label: "Shop Best Sellers",
cta_url: "https://shop.mozimo.in/collections/best-sellers",
sort: 2,
visible: true,
},
{
name: "Exquisite",
note: "From ₹2,400",
title: "Celebrations, elevated.",
blurb:
"The Carnival boxes — layered, abundant and composed for the moments in between.",
panel: "products",
tile_url: "/products/mozi20.jpg",
product_handles:
"mozimo-carnival-celebration-s\nmozimo-carnival-celebration-m",
collection_handle: "gift-collection",
cta_label: "View the full collection",
cta_url: "https://shop.mozimo.in/collections/gift-collection",
sort: 3,
visible: true,
},
{
name: "Premium",
note: "From ₹3,900",
title: "Generosity, composed.",
blurb:
"Our grandest hampers — the ones that arrive before you speak and say everything after.",
panel: "products",
tile_url: "/products/mozi-33.jpg",
product_handles: "mozimo-carnival-celebration-l\nmozimo-celestial-s",
collection_handle: "gift-collection",
cta_label: "View the full collection",
cta_url: "https://shop.mozimo.in/collections/gift-collection",
sort: 4,
visible: true,
},
{
name: "Seasonal",
note: "Limited editions",
title: "Chocolate for the calendar.",
blurb:
"Diwali to Valentine's — editions composed for the season's light, here only while it lasts.",
panel: "seasonal",
tile_url: "/products/mozi-30.jpg",
collection_handle: "gift-collection",
cta_label: "Shop the Seasonal Edit",
cta_url: "https://shop.mozimo.in/collections/gift-collection",
sort: 5,
visible: true,
},
{
name: "Corporate & Bulk",
note: "At scale",
title: "Gratitude at scale.",
blurb:
"Branded hampers, volume pricing and door-step delivery for teams, clients and occasions of every size.",
panel: "enquiry",
tile_url: "/products/mozi21.jpg",
cta_label: "Start a corporate enquiry",
cta_url: "/bespoke#enquiry",
sort: 6,
visible: true,
},
{
name: "Bespoke Gifting",
note: "Made to order",
title: "Composed around your moment.",
blurb:
"Custom selections, packaging and personalisation — designed with our chocolatiers from the first conversation.",
panel: "enquiry",
tile_url: "/products/mozi-17.jpg",
cta_label: "Begin a bespoke commission",
cta_url: "/bespoke#enquiry",
sort: 7,
visible: true,
},
];
const COLLECTION_ROWS = [
{
shopify_handle: "bars",
name_override: "Bars",
blurb: "Single-origin tablets, 45% to 85%",
carousel_urls:
"/products/01-dark85-60gm.jpg\n/products/02-dark60-60gm.jpg\n/products/img-5469.jpg",
sort: 1,
visible: true,
},
{
shopify_handle: "bark",
name_override: "Barks",
blurb: "Crackled slabs of nuts, fruit & crunch",
carousel_urls:
"/products/01-almondbark60.jpg\n/products/02-almondbark45.jpg\n/products/20-almondpralinebark.jpg",
sort: 2,
visible: true,
},
{
shopify_handle: "pralines",
name_override: "Pralines",
blurb: "Hand-piped ganache, boxed like jewels",
carousel_urls:
"/products/mozi-27.jpg\n/products/irw05205.jpg\n/products/14-mozimopralinepanetela-9c9a80d8-e4fc-4c4b-9c56-ca7f42b57847.jpg",
sort: 3,
visible: true,
},
{
shopify_handle: "spreads",
name_override: "Spreads",
blurb: "Stone-ground, silky, single origin",
carousel_urls:
"/products/02-pistachiospread60.jpg\n/products/03-hazelnutspread45.jpg\n/products/mozi-23.jpg",
sort: 4,
visible: true,
},
{
shopify_handle: "dragees",
name_override: "Dragees",
blurb: "Roasted nuts under a thin chocolate shell",
carousel_urls:
"/products/04-almonddragees60.jpg\n/products/02-hazelnutdragees45.jpg\n/products/03-pistachiodragees.jpg",
sort: 5,
visible: true,
},
{
shopify_handle: "tuilles",
name_override: "Tuilles & Rocks",
blurb: "Lace-thin wafers, orangettes & slivers",
carousel_urls:
"/products/01-milk45-tuilles-100gm.jpg\n/products/02-whitechocolatetuilles-100gm.jpg\n/products/01-almondsliversrocks-100gm.jpg",
sort: 6,
visible: true,
},
{
shopify_handle: "gift-collection",
name_override: "Gifting",
blurb: "Hampers composed for every celebration",
carousel_urls:
"/products/mozi20.jpg\n/products/mozi-33.jpg\n/products/mozi4.jpg",
sort: 7,
visible: true,
},
];
const GIFTING_PAGE = [
{
hero_eyebrow: "Gifts",
hero_title: "The art of",
hero_script: "giving",
hero_lead:
"Some gifts are opened. Others are remembered. Every Mozimo hamper is composed by hand — chocolate first, packaging worthy of it second.",
sort: 0,
},
];
/* ── collection schemas ────────────────────────────────────── */
const SCHEMAS = {
gifting_tabs: {
name: "gifting_tabs",
type: "base",
listRule: "",
viewRule: "",
createRule: null,
updateRule: null,
deleteRule: null,
fields: [
{ name: "name", type: "text", required: true },
{ name: "note", type: "text" },
{ name: "title", type: "text" },
{ name: "blurb", type: "text" },
{
name: "panel",
type: "select",
values: ["products", "seasonal", "enquiry"],
maxSelect: 1,
},
{ name: "tile", type: "file", maxSelect: 1, mimeTypes: IMAGE_MIME },
{ name: "tile_url", type: "text" },
{ name: "product_handles", type: "text" },
{ name: "collection_handle", type: "text" },
{ name: "parent_slug", type: "text" },
{ name: "cta_label", type: "text" },
{ name: "cta_url", type: "text" },
{ name: "sort", type: "number" },
{ name: "visible", type: "bool" },
],
},
collection_rows: {
name: "collection_rows",
type: "base",
listRule: "",
viewRule: "",
createRule: null,
updateRule: null,
deleteRule: null,
fields: [
{ name: "shopify_handle", type: "text", required: true },
{ name: "name_override", type: "text" },
{ name: "blurb", type: "text" },
{ name: "carousel", type: "file", maxSelect: 3, mimeTypes: IMAGE_MIME },
{ name: "carousel_urls", type: "text" },
{ name: "sort", type: "number" },
{ name: "visible", type: "bool" },
],
},
site_settings: {
name: "site_settings",
type: "base",
listRule: "",
viewRule: "",
createRule: null,
updateRule: null,
deleteRule: null,
fields: [
{ name: "key", type: "text", required: true },
{ name: "value", type: "text" },
{ name: "sort", type: "number" },
],
},
gifting_page: {
name: "gifting_page",
type: "base",
listRule: "",
viewRule: "",
createRule: null,
updateRule: null,
deleteRule: null,
fields: [
{ name: "hero_eyebrow", type: "text" },
{ name: "hero_title", type: "text" },
{ name: "hero_script", type: "text" },
{ name: "hero_lead", type: "text" },
{ name: "sort", type: "number" },
],
},
};
const SITE_SETTINGS = [
{
key: "announcement",
value:
"Complimentary delivery in Chandigarh Tricity on orders above ₹1,500 · Intercity shipped at actuals",
sort: 0,
},
];
const CORPORATE_CHILDREN = [
[
"Welcome Gifts",
"corporate-welcome-gifts",
"Day-one boxes that say the onboarding was personal.",
0,
],
[
"Joining Anniversaries",
"corporate-joining-anniversaries",
"Milestone chocolate for the day they said yes to you.",
1,
],
[
"Farewell & Leaving Gifts",
"corporate-farewell-gifts",
"A graceful send-off, wrapped properly.",
2,
],
[
"5-Year Milestones",
"corporate-5-year-milestones",
"Half a decade deserves more than a certificate.",
3,
],
[
"10-Year Milestones",
"corporate-10-year-milestones",
"A decade of craft, honoured in kind.",
4,
],
[
"CXO & VIP Gifting",
"corporate-cxo-vip-gifting",
"Executive gifting composed to the person, not the policy.",
5,
],
].map(([name, handle, blurb, sort]) => ({
name,
note: "",
title: name,
blurb,
panel: "products",
tile_url: "",
product_handles: "",
collection_handle: handle,
parent_slug: "corporate-bulk",
cta_label: "Enquire",
cta_url: "/bespoke#enquiry",
sort,
visible: true,
}));
const SEEDS = {
gifting_tabs: [...GIFTING_TABS, ...CORPORATE_CHILDREN],
collection_rows: COLLECTION_ROWS,
gifting_page: GIFTING_PAGE,
site_settings: SITE_SETTINGS,
};
/* ── helpers ───────────────────────────────────────────────── */
const api = async (path, options = {}) => {
const res = await fetch(`${PB_URL}${path}`, {
...options,
headers: { "Content-Type": "application/json", ...(options.headers ?? {}) },
signal: AbortSignal.timeout(10_000),
});
const body = await res.json().catch(() => ({}));
return { status: res.status, ok: res.ok, body };
};
async function main() {
if (!EMAIL || !PASSWORD) {
console.log(
"[pb-seed] PB_SUPERUSER_EMAIL/PASSWORD not set — skipping seed",
);
return;
}
const auth = await api("/api/collections/_superusers/auth-with-password", {
method: "POST",
body: JSON.stringify({ identity: EMAIL, password: PASSWORD }),
});
if (!auth.ok) {
console.warn(
`[pb-seed] superuser auth failed (${auth.status}) — skipping seed`,
);
return;
}
const token = auth.body.token;
const authed = (path, options = {}) =>
api(path, {
...options,
headers: { Authorization: token, ...(options.headers ?? {}) },
});
for (const [name, schema] of Object.entries(SCHEMAS)) {
const created = await authed("/api/collections", {
method: "POST",
body: JSON.stringify(schema),
});
if (created.ok) {
console.log(`[pb-seed] created collection "${name}"`);
} else if (created.status === 400) {
// exists — add any fields the schema gained, keep public read rules
const cur = await authed(`/api/collections/${name}`);
if (cur.ok) {
const curFields = cur.body.fields ?? [];
const have = new Set(curFields.map((f) => f.name));
const missing = (schema.fields ?? []).filter((f) => !have.has(f.name));
const body = missing.length
? { fields: [...curFields, ...missing], listRule: "", viewRule: "" }
: { listRule: "", viewRule: "" };
const patched = await authed(`/api/collections/${name}`, {
method: "PATCH",
body: JSON.stringify(body),
});
if (!patched.ok && missing.length) {
console.warn(
`[pb-seed] field merge for "${name}" failed:`,
patched.status,
JSON.stringify(patched.body).slice(0, 160),
);
}
}
} else {
console.warn(
`[pb-seed] collection "${name}":`,
created.status,
JSON.stringify(created.body).slice(0, 200),
);
continue;
}
// seed only when empty
const existing = await authed(`/api/collections/${name}/records?perPage=1`);
if (!existing.ok) {
console.warn(`[pb-seed] cannot read "${name}" (${existing.status})`);
continue;
}
if (existing.body.totalItems > 0) {
console.log(
`[pb-seed] "${name}" already has ${existing.body.totalItems} record(s) — leaving as is`,
);
continue;
}
for (const record of SEEDS[name]) {
const inserted = await authed(`/api/collections/${name}/records`, {
method: "POST",
body: JSON.stringify(record),
});
if (!inserted.ok) {
console.warn(
`[pb-seed] seed record in "${name}":`,
inserted.status,
JSON.stringify(inserted.body).slice(0, 200),
);
}
}
console.log(
`[pb-seed] seeded ${SEEDS[name].length} record(s) into "${name}"`,
);
}
console.log("[pb-seed] done");
}
main().catch((error) => {
console.warn("[pb-seed] failed:", error?.message ?? error);
});