"// Declarations of cms-types.ts: what this file exports, their arguments and what they return.
\n
// /**
\n
// * One collection item: `_id` plus the collection's fields, flat on the item (there is no
\n
// * `item.data.*` — that is the REST shape and reads undefined here).
\n
// * Field values by type: TEXT/URL/EMAIL → string, NUMBER → number, BOOLEAN → boolean,
\n
// * DATE/DATETIME → ISO string, TIME →
\"
hh:mm:ss.SSS
\"
string (text on the wire, passed through),
\n
// * IMAGE → https URL at the image's natural aspect (
\"\"
never happens — absent fields are
\n
// * undefined), RICH_TEXT → the stored HTML string, ARRAY_STRING (tags) → string[], ADDRESS →
\n
// * object with `formatted`, MEDIA_GALLERY → array of { src, type, title } (`src` resolved),
\n
// * REFERENCE/MULTI_REFERENCE → id(s), or full CmsItem(s) when the query included the field.
\n
// * VIDEO/DOCUMENT identifiers (`wix:video://`, `wix:document://`) and RICH_CONTENT (Ricos JSON)
\n
// * pass through unresolved.
\n
// */
\n
// export interface CmsItem {
\n
// _id: string;
\n
// /** ISO string (the SDK's Date objects are serialized by the data layer). */
\n
// _createdDate?: string;
\n
// _updatedDate?: string;
\n
// /** Id of the member who created the row (member-scoped collections). */
\n
// _owner?: string;
\n
// [key: string]: unknown;
\n
// }
\n
// /**
\n
// * Predicate operators — the SDK query builder's names. `in` matches a scalar field equal to ANY of
\n
// * the values (an id list; hasSome on the wire, as Wix's own components send it); `between` takes
\n
// * `[start, end]`, start inclusive, end EXCLUSIVE (the builder spells it ge + lt — a date range
\n
// * ends at the first excluded day).
\n
// */
\n
// export type CmsFilterOp =
\"
eq
\"
|
\"
ne
\"
|
\"
gt
\"
|
\"
ge
\"
|
\"
lt
\"
|
\"
le
\"
|
\"
contains
\"
|
\"
startsWith
\"
|
\"
endsWith
\"
|
\"
in
\"
|
\"
hasSome
\"
|
\"
hasAll
\"
|
\"
between
\"
|
\"
isEmpty
\"
|
\"
isNotEmpty
\"
;
\n
// /**
\n
// * One query predicate. Comparands must match the field's stored type — a DATE/DATETIME
\n
// * field only matches a Date object (an ISO string compares as text and matches nothing).
\n
// */
\n
// export interface CmsFilter {
\n
// field: string;
\n
// op: CmsFilterOp;
\n
// /** Required except for isEmpty/isNotEmpty; in/hasSome/hasAll take an array; between takes `[start, end]`. */
\n
// value?: string | number | boolean | Date | (string | number | Date)[];
\n
// }
\n
// export interface CmsSort {
\n
// field: string;
\n
// /** Default
\"
asc
\"
. */
\n
// direction?:
\"
asc
\"
|
\"
desc
\"
;
\n
// }
\n
// export interface CmsQuery {
\n
// filters?: CmsFilter[];
\n
// sort?: CmsSort[];
\n
// /** Page size (default 20). */
\n
// limit?: number;
\n
// /** Items to skip — page N (0-based) is skip = N * limit. */
\n
// skip?: number;
\n
// /** Reference field keys to inline as full items (otherwise the field holds ids). */
\n
// include?: string[];
\n
// /** true → the page carries `total` (slower query; for counts, page numbers, empty-state logic). */
\n
// withTotal?: boolean;
\n
// }
\n
// /** One page of items. Load the next page with skip = items shown so far. */
\n
// export interface CmsPage {
\n
// items: CmsItem[];
\n
// hasNext: boolean;
\n
// /** Total matching items — only when the query asked `withTotal`, else null. */
\n
// total: number | null;
\n
// }
\n
// /** A field's type as the collection schema spells it (Wix Data `Type`; open for values added later). */
\n
// export type CmsFieldType =
\"
TEXT
\"
|
\"
NUMBER
\"
|
\"
BOOLEAN
\"
|
\"
DATE
\"
|
\"
DATETIME
\"
|
\"
TIME
\"
|
\"
URL
\"
|
\"
IMAGE
\"
|
\"
VIDEO
\"
|
\"
DOCUMENT
\"
|
\"
AUDIO
\"
|
\"
RICH_TEXT
\"
|
\"
RICH_CONTENT
\"
|
\"
MEDIA_GALLERY
\"
|
\"
ADDRESS
\"
|
\"
ARRAY_STRING
\"
|
\"
ARRAY
\"
|
\"
OBJECT
\"
|
\"
REFERENCE
\"
|
\"
MULTI_REFERENCE
\"
| (string & {});
\n
// /** Who may run a verb on a collection (Wix Data `Role`). */
\n
// export type CmsRole =
\"
ANYONE
\"
|
\"
SITE_MEMBER
\"
|
\"
SITE_MEMBER_AUTHOR
\"
|
\"
ADMIN
\"
| (string & {});
\n
// /** One field of a collection's schema. */
\n
// export interface CmsFieldSchema {
\n
// key: string;
\n
// displayName: string;
\n
// type: CmsFieldType;
\n
// /** `_id`, `_createdDate`, `_updatedDate`, `_owner` — Wix's to stamp, never written. */
\n
// systemField: boolean;
\n
// required: boolean;
\n
// /** REFERENCE/MULTI_REFERENCE: the collection the field points at. */
\n
// referencedCollectionId?: string;
\n
// }
\n
// /**
\n
// * A collection's schema — field types, reference targets, and permissions at runtime: the
\n
// * source of truth for a field's type when the seed plan isn't at hand (an imported site).
\n
// */
\n
// export interface CmsCollectionSchema {
\n
// id: string;
\n
// displayName: string;
\n
// fields: CmsFieldSchema[];
\n
// permissions: {
\n
// read: CmsRole;
\n
// insert: CmsRole;
\n
// update: CmsRole;
\n
// remove: CmsRole;
\n
// };
\n
// }
\n
// CMS DTOs — the serializable shapes every hook, component, and page consumes. CMS is
\n
// schema-generic: field keys come from the seed plan (seed/SEED.md), so an item is a flat
\n
// field map rather than a fixed interface. Plain JSON: safe as Astro island props or across
\n
// server/client boundaries. By the time an item leaves the data layer, Date fields are ISO
\n
// strings and IMAGE fields are resolved https URLs.
\n
export {};
\n
"
,
14 "js/wix/collection-store.js": "// Declarations of collection-store.ts: what this file exports, their arguments and what they return.\n// import type { CmsFilter, CmsItem, CmsSort } from \"./cms-types.js\";\n// export interface CollectionQuery {\n// collectionId?: string;\n// filters?: CmsFilter[];\n// sort?: CmsSort[];\n// limit?: number;\n// include?: string[];\n// }\n// export interface CollectionStoreOptions extends CollectionQuery {\n// collectionId: string;\n// initialItems?: CmsItem[];\n// initialHasNext?: boolean;\n// withTotal?: boolean;\n// initialTotal?: number | null;\n// }\n// export interface CollectionState {\n// items: CmsItem[] | null;\n// hasNext: boolean;\n// hasPrev: boolean;\n// page: number;\n// total: number | null;\n// pageCount: number | null;\n// loading: boolean;\n// fetching: boolean;\n// loadingMore: boolean;\n// error: string | null;\n// }\n// export interface CollectionStore {\n// getState(): CollectionState;\n// subscribe(listener: (state: CollectionState) => void): () => void;\n// start(): void;\n// stop(): void;\n// setQuery(query: CollectionQuery): void;\n// loadMore(): Promise<void>;\n// goToPage(n: number): void;\n// refresh(): void;\n// retry(): void;\n// }\n// export declare function createCollectionStore(options: CollectionStoreOptions): CollectionStore;\n// One collection as list state, framework-free — the logic behind useCollection, usable from React\n// (useCollection wraps it with useSyncExternalStore), from a static page or Vue/Svelte (subscribe\n// and render), or as the specification for a port. Same shape as the storefront stores: state,\n// actions, subscribe/getState, emit after every change.\n//\n// The query (collection, filters, sort, page size, includes) is identified BY VALUE; a changed\n// query refetches from the first page, a late response from a superseded query is dropped.\n// Stale-while-refetch: once items are on screen they STAY there while a new query, page, or\n// refresh runs — `fetching` flips instead of `items` going null; only the very first load shows\n// skeletons (`loading`). Two ways to page, both at the source: loadMore() appends the next page\n// (skip = items shown so far); goToPage(n) replaces the list with page n (0-based; `hasPrev`,\n// `page`, and — with `withTotal` — `total`/`pageCount` drive a numbered pager). refresh() re-reads\n// what is on screen after a write.\n//\n// SSR-friendly: seed with `initialItems` from the SAME query and no client fetch happens for the\n// first page. One store per mounted listing (a page can hold two collections): createCollectionStore(),\n// not a singleton.\nimport { queryItems } from \"./items.js\";\nconst queryKey = (q) => \n// Dates in filters serialize to ISO, so this is stable.\nJSON.stringify([q.collectionId, q.filters ?? null, q.sort ?? null, q.limit, q.include ?? null]);\nexport function createCollectionStore(options) {\n let collectionId = options.collectionId;\n let filters = options.filters;\n let sort = options.sort;\n let limit = options.limit ?? 20;\n let include = options.include;\n const withTotal = options.withTotal ?? false;\n const key = () => queryKey({ collectionId, filters, sort, limit, include });\n let items = options.initialItems ?? null;\n let hasNext = options.initialHasNext ?? (options.initialItems ? options.initialItems.length >= limit : false);\n let page = 0;\n let total = withTotal ? (options.initialTotal ?? null) : null;\n let loadingMore = false;\n let fetching = false;\n let error = null;\n // The query the latest request targets (null = nothing requested yet).\n let pageKey = options.initialItems ? key() : null;\n let inflight = false;\n let started = false;\n let generation = 0;\n const listeners = new Set();\n let snapshot = null;\n const emit = () => { snapshot = null; for (const fn of listeners)\n fn(); };\n function getState() {\n if (snapshot)\n return snapshot;\n snapshot = {\n items,\n hasNext,\n hasPrev: page > 0,\n page,\n total,\n pageCount: total === null ? null : Math.ceil(total / limit),\n loading: items === null,\n fetching,\n loadingMore,\n error,\n };\n return snapshot;\n }\n /**\n * Load page `nextPage` of the current query (`window` items wide — the page size, or the whole\n * shown list on a refresh). Items already on screen stay while it runs (fetching); the first load\n * has none, so it shows skeletons (loading).\n */\n function load(nextPage, window = limit) {\n if (!started)\n return;\n const k = key();\n const id = ++generation;\n pageKey = k;\n inflight = true;\n error = null;\n loadingMore = false;\n fetching = items !== null; // something is on screen → it stays, dimmed; nothing yet → skeletons (loading)\n emit();\n queryItems(collectionId, { filters, sort, limit: window, skip: nextPage * limit, include, withTotal })\n .then((res) => {\n if (generation !== id)\n return; // superseded — drop it\n items = res.items;\n hasNext = res.hasNext;\n page = nextPage;\n if (withTotal)\n total = res.total;\n inflight = false;\n fetching = false;\n emit();\n })\n .catch((e) => {\n if (generation !== id)\n return;\n // A first load that fails shows the error with an empty list; a refetch keeps what was on screen.\n if (items === null) {\n items = [];\n hasNext = false;\n }\n inflight = false;\n fetching = false;\n error = e instanceof Error ? e.message : String(e);\n emit();\n });\n }\n return {\n getState,\n subscribe(fn) {\n const listener = () => { try {\n fn(getState());\n }\n catch (e) {\n console.error(\"collection-store listener failed\", e);\n } };\n listeners.add(listener);\n return () => listeners.delete(listener);\n },\n start() {\n if (started)\n return;\n started = true;\n // The SSR seed answered this exact query — no fetch; a dropped first load reruns.\n if (pageKey !== key() || (items === null && !inflight))\n load(0);\n },\n stop() {\n started = false;\n generation++;\n inflight = false;\n fetching = false;\n },\n setQuery(q) {\n if (q.collectionId !== undefined)\n collectionId = q.collectionId;\n filters = q.filters;\n sort = q.sort;\n include = q.include;\n if (q.limit !== undefined)\n limit = q.limit;\n if (key() === pageKey)\n return; // same values — same page\n if (started)\n load(0);\n },\n async loadMore() {\n const current = items;\n if (!current || loadingMore || inflight)\n return;\n const id = generation;\n loadingMore = true;\n emit();\n try {\n const res = await queryItems(collectionId, { filters, sort, limit, include, withTotal, skip: page * limit + current.length });\n if (generation !== id)\n return;\n items = [...(items ?? []), ...res.items];\n hasNext = res.hasNext;\n if (withTotal)\n total = res.total;\n }\n catch (e) {\n if (generation !== id)\n return;\n error = e instanceof Error ? e.message : String(e);\n }\n finally {\n if (generation === id) {\n loadingMore = false;\n emit();\n }\n }\n },\n goToPage(n) {\n if (n < 0)\n return;\n // Already showing exactly that page of this query (appended pages collapse back to one).\n const shown = n === page && key() === pageKey && items !== null && !inflight && items.length <= limit;\n if (shown)\n return;\n load(n);\n },\n refresh() {\n // The whole shown window (several appended pages read as one), or the page size before any answer.\n load(page, items && items.length > limit ? items.length : limit);\n },\n retry() {\n pageKey = null;\n items = null;\n page = 0;\n load(0);\n },\n };\n}\n",
15 "js/wix/collections-core.js": "// Declarations of collections-core.ts: what this file exports, their arguments and what they return.\n// import type { CmsCollectionSchema, CmsFieldSchema, CmsFieldType } from \"./cms-types.js\";\n// export type RawCollection = Record<string, any>;\n// export declare function toFieldSchema(f: RawCollection): CmsFieldSchema;\n// export declare function toCollectionSchema(raw: RawCollection): CmsCollectionSchema;\n// export declare function fieldsOfType(schema: CmsCollectionSchema, type: CmsFieldType): CmsFieldSchema[];\n/** A verb's role as the API spells it; ADMIN (the most restrictive reading) when absent. */\nconst role = (r) => (typeof r === \"string\" && r ? r : \"ADMIN\");\n/** One raw field → DTO. The reference target sits under typeMetadata.reference | .multiReference. */\nexport function toFieldSchema(f) {\n const ref = f.typeMetadata?.reference?.referencedCollectionId ?? f.typeMetadata?.multiReference?.referencedCollectionId;\n return {\n key: f.key ?? \"\",\n displayName: f.displayName ?? f.key ?? \"\",\n type: f.type ?? \"TEXT\",\n systemField: f.systemField === true,\n required: f.required === true,\n ...(ref ? { referencedCollectionId: ref } : {}),\n };\n}\n/** A raw DataCollection → the schema DTO (id, display name, fields, the four verbs' roles). */\nexport function toCollectionSchema(raw) {\n const id = raw._id ?? raw.id ?? \"\";\n const p = raw.permissions ?? {};\n return {\n id,\n displayName: raw.displayName ?? id,\n fields: (raw.fields ?? []).map(toFieldSchema),\n permissions: { read: role(p.read), insert: role(p.insert), update: role(p.update), remove: role(p.remove) },\n };\n}\n/** The fields of one type — e.g. every MULTI_REFERENCE to link after an insert, every IMAGE to guard. */\nexport function fieldsOfType(schema, type) {\n return schema.fields.filter((f) => f.type === type);\n}\n",
16 "js/wix/collections.js": "// Declarations of collections.ts: what this file exports, their arguments and what they return.\n// import type { CmsCollectionSchema } from \"./cms-types.js\";\n// export declare function getCollectionSchema(collectionId: string): Promise<CmsCollectionSchema | null>;\n// Collection schema reads over REST — the twin of app/wix/cms/collections.ts. Same export, same\n// DTO; the mapper comes from collections-core (the SAME file the SDK transport uses, deployed flat\n// next to this one by deploy.mjs --stack static), so this file is only the transport.\n// docs: https://dev.wix.com/docs/api-reference/business-solutions/cms/data-collections/create-data-collection.md (the Data Collections API; get is GET /wix-data/v2/collections/{id} per the SDK's route table)\nimport { WixApiError, wixRequest } from \"./client.js\";\nimport { toCollectionSchema } from \"./collections-core.js\";\nconst unreadable = (e) => e instanceof WixApiError && (e.status === 404 || e.status === 403);\n/**\n * A collection's schema — field types, reference targets, permissions. Null when the collection\n * can't be read: it doesn't exist (404), or the caller may not read schemas (403 — confirm once on\n * a live site that a visitor token is allowed; if it isn't, read it where an elevated call runs).\n * GET /wix-data/v2/collections/{dataCollectionId} → { collection: { id, displayName, fields: [{ key, displayName, type, typeMetadata, systemField, required }], permissions: { insert, update, remove, read } } }\n */\nexport async function getCollectionSchema(collectionId) {\n try {\n const res = await wixRequest(`/wix-data/v2/collections/${encodeURIComponent(collectionId)}`, { method: \"GET\" });\n return res?.collection ? toCollectionSchema(res.collection) : null;\n }\n catch (e) {\n if (unreadable(e))\n return null;\n throw e;\n }\n}\n",
17 "js/wix/item-store.js": "// Declarations of item-store.ts: what this file exports, their arguments and what they return.\n// import type { CmsItem } from \"./cms-types.js\";\n// export interface ItemRef {\n// id?: string;\n// by?: {\n// field: string;\n// value: string | number;\n// };\n// }\n// export interface ItemStoreOptions {\n// collectionId: string;\n// ref: ItemRef;\n// initialItem?: CmsItem;\n// include?: string[];\n// }\n// export interface ItemState {\n// item: CmsItem | null;\n// notFound: boolean;\n// error: string | null;\n// loading: boolean;\n// }\n// export interface ItemStore {\n// getState(): ItemState;\n// subscribe(listener: (state: ItemState) => void): () => void;\n// start(): void;\n// stop(): void;\n// retry(): void;\n// }\n// export declare function createItemStore({ collectionId, ref, initialItem, include }: ItemStoreOptions): ItemStore;\n// One item as a framework-free store — the logic behind useItem: by `_id` or by a field match\n// (slug routing), usable from React (useItem wraps it), from a static page's item view, from\n// Vue/Svelte, or as the specification for a port.\n//\n// SSR-friendly: pass the server-fetched item as `initialItem` and start() fetches nothing.\n// One store per item surface: createItemStore(), not a singleton.\nimport { getItemBy, getItemById } from \"./items.js\";\nexport function createItemStore({ collectionId, ref, initialItem, include }) {\n let item = initialItem ?? null;\n let notFound = false;\n let error = null;\n let loading = !initialItem;\n let started = false;\n let generation = 0;\n const listeners = new Set();\n let snapshot = null;\n const emit = () => { snapshot = null; for (const fn of listeners)\n fn(); };\n function getState() {\n if (snapshot)\n return snapshot;\n snapshot = { item, notFound, error, loading };\n return snapshot;\n }\n function load() {\n const id = ++generation;\n loading = true;\n error = null;\n emit();\n const fetching = ref.id\n ? getItemById(collectionId, ref.id, { include })\n : ref.by\n ? getItemBy(collectionId, ref.by.field, ref.by.value, { include })\n : Promise.reject(new Error(\"useItem: pass ref.id or ref.by\"));\n fetching\n .then((found) => {\n if (generation !== id || !started)\n return;\n item = found;\n notFound = found === null;\n loading = false;\n emit();\n })\n .catch((e) => {\n if (generation !== id || !started)\n return;\n error = e instanceof Error ? e.message : String(e);\n loading = false;\n emit();\n });\n }\n return {\n getState,\n subscribe(fn) {\n const listener = () => { try {\n fn(getState());\n }\n catch (e) {\n console.error(\"item-store listener failed\", e);\n } };\n listeners.add(listener);\n return () => listeners.delete(listener);\n },\n start() {\n if (started)\n return;\n started = true;\n if (initialItem)\n return;\n load();\n },\n stop() { started = false; generation++; },\n retry() { if (started)\n load(); },\n };\n}\n",
18 "js/wix/items-core.js": "// Declarations of items-core.ts: what this file exports, their arguments and what they return.\n// import type { CmsFilter, CmsItem, CmsPage, CmsQuery, CmsSort } from \"./cms-types.js\";\n// export type Raw = Record<string, any>;\n// export type ImgSrc = (value: any, width: number, height: number) => string;\n// export type ImgRatio = (value: any) => number | null;\n// export interface Media {\n// imgSrc: ImgSrc;\n// imgRatio: ImgRatio;\n// }\n// export declare const DEFAULT_LIMIT = 20;\n// export declare const DISTINCT_LIMIT = 100;\n// export declare const IMAGE_WIDTH = 1200;\n// export declare const IMAGE_HEIGHT = 900;\n// export declare function imageSize(ratio: number | null): {\n// width: number;\n// height: number;\n// };\n// export declare function toValue(v: unknown, media: Media): unknown;\n// export declare const toItem: (raw: Raw, media: Media) => CmsItem;\n// export declare function toPage(items: Raw[], hasNext: boolean, total: number | null | undefined, media: Media): CmsPage;\n// export declare const toValues: (values: unknown[], media: Media) => unknown[];\n// export declare function formatDate(iso: unknown, locale?: string, options?: Intl.DateTimeFormatOptions): string;\n// export declare function assertFilterValue(f: CmsFilter): void;\n// export declare function betweenRange(f: CmsFilter): [unknown, unknown];\n// export declare function restValue(v: unknown): unknown;\n// export declare function restCondition(f: CmsFilter): Raw;\n// export declare function restFilter(filters?: CmsFilter[]): Raw;\n// export declare function restSort(sort?: CmsSort[]): {\n// fieldName: string;\n// order: \"ASC\" | \"DESC\";\n// }[];\n// export declare function queryBody(collectionId: string, query?: CmsQuery): Raw;\n// export declare function restHasNext(pagingMetadata: Raw | undefined, offset: number, limit: number): boolean;\n// export declare function countBody(collectionId: string, filters?: CmsFilter[]): Raw;\n// export declare function distinctBody(collectionId: string, field: string, filters?: CmsFilter[], limit?: number): Raw;\n// export declare function restWriteData(data: Record<string, unknown>): Raw;\n// export declare function patchModifications(fields: Record<string, unknown>): Raw[];\n// export declare function referencesBody(collectionId: string, field: string, itemId: string, refIds: string[]): Raw;\n/** Page size when a query names none. */\nexport const DEFAULT_LIMIT = 20;\n/** Distinct values returned when a call names no limit. */\nexport const DISTINCT_LIMIT = 100;\n/** The width every IMAGE field is resolved at — one URL per item, large enough for a detail page. */\nexport const IMAGE_WIDTH = 1200;\n/** The height when the identifier carries no size (an image imported without its dimensions): 4:3. */\nexport const IMAGE_HEIGHT = 900;\n// ---- DTO normalization ---------------------------------------------------------------------------\n/** REST spells DATE/DATETIME fields as `{ \"$date\": \"<iso>\" }`; the SDK decodes them into Date objects. */\nfunction isDateWrapper(v) {\n if (!v || typeof v !== \"object\" || Array.isArray(v))\n return false;\n const keys = Object.keys(v);\n return keys.length === 1 && keys[0] === \"$date\" && typeof v.$date === \"string\";\n}\n/**\n * The size an IMAGE field resolves at: IMAGE_WIDTH wide, at the image's own aspect when the\n * `wix:image://` id carries `#originWidth/#originHeight` (a fill at another aspect would crop it),\n * IMAGE_WIDTH × IMAGE_HEIGHT otherwise. The resolved URL carries the size (`/w_1200,h_800/`), so\n * media.ts `imgRatio(url)` reads the aspect back for srcsets.\n */\nexport function imageSize(ratio) {\n return { width: IMAGE_WIDTH, height: ratio ? Math.round(IMAGE_WIDTH * ratio) : IMAGE_HEIGHT };\n}\n/**\n * Raw → DTO value: dates become ISO strings (Date objects aren't serializable as island props;\n * `{ $date }` wrappers aren't dates at all), IMAGE fields holding a `wix:image://` identifier\n * become https URLs a browser can load. Recursive — included reference items too.\n */\nexport function toValue(v, media) {\n if (v instanceof Date)\n return v.toISOString();\n if (isDateWrapper(v)) {\n // Same spelling as the SDK path (REST omits the milliseconds: \"…T00:00:00Z\").\n const t = new Date(v.$date);\n return Number.isNaN(t.getTime()) ? v.$date : t.toISOString();\n }\n if (typeof v === \"string\" && v.startsWith(\"wix:image://\")) {\n const { width, height } = imageSize(media.imgRatio(v));\n return media.imgSrc(v, width, height);\n }\n if (Array.isArray(v))\n return v.map((x) => toValue(x, media));\n if (v && typeof v === \"object\") {\n const out = {};\n for (const [k, val] of Object.entries(v))\n out[k] = toValue(val, media);\n return out;\n }\n return v;\n}\nexport const toItem = (raw, media) => toValue(raw, media);\nexport function toPage(items, hasNext, total, media) {\n return { items: items.map((r) => toItem(r, media)), hasNext, total: total ?? null };\n}\n/** Distinct values of a field, normalized like item values (a DATE field's values → ISO strings). */\nexport const toValues = (values, media) => values.map((v) => toValue(v, media));\n/**\n * A DATE/DATETIME field for display — the DTO carries an ISO string; this is the one place it\n * becomes copy. \"\" for an absent or unparseable value (never \"Invalid Date\", never the raw ISO).\n * formatDate(item.publishDate) → \"August 20, 2026\"\n * formatDate(item.publishDate, \"en-GB\", { dateStyle: \"medium\" })\n */\nexport function formatDate(iso, locale = \"en-US\", options = { dateStyle: \"long\" }) {\n if (typeof iso !== \"string\" || !iso)\n return \"\";\n const t = new Date(iso);\n return Number.isNaN(t.getTime()) ? \"\" : new Intl.DateTimeFormat(locale, options).format(t);\n}\n// ---- filter rules --------------------------------------------------------------------------------\n/**\n * An undefined comparand silently changes what a query matches (every row, or none) with no\n * server error — the classic \"my items shows everyone's items\" bug. Throw at the call site;\n * omit the filter entirely when you don't hold a value yet. A `between` needs its two bounds.\n */\nexport function assertFilterValue(f) {\n if (f.op === \"isEmpty\" || f.op === \"isNotEmpty\")\n return;\n if (f.value === undefined) {\n throw new Error(`cms: filter on \"${f.field}\" (${f.op}) has an undefined value — pass a real value or omit the filter.`);\n }\n if (f.op === \"between\" && !(Array.isArray(f.value) && f.value.length === 2)) {\n throw new Error(`cms: filter on \"${f.field}\" (between) takes [start, end].`);\n }\n}\n/** The validated `[start, end]` of a between filter — start inclusive, end exclusive on both transports. */\nexport function betweenRange(f) {\n assertFilterValue(f);\n const [start, end] = f.value;\n return [start, end];\n}\n// ---- the REST spelling of a query ---------------------------------------------------------------\n// The SDK's query builder serializes these itself; the REST twin sends them literally. Proven live:\n// a DATE comparand must travel as `{ \"$date\": iso }` (a plain ISO string compares as text and\n// matches nothing); isEmpty is `{ field: null }` and isNotEmpty `{ field: { $ne: null } }` — the\n// `$isEmpty` keyword is rejected (WDE0076).\n/**\n * A comparand or field value on the wire: Date → `{ $date }`, arrays element-wise, everything else\n * as-is. DATE and DATETIME fields BOTH travel as `{ \"$date\": iso }` (the SDK sends a Date the same\n * way) — proven live: the seed writes DATE fields so, reads them back as dates, and date filters\n * match them. Wix's headless-components write a DATE field as a \"YYYY-MM-DD\" string instead; that\n * form is not copied here (their formatter is also a month off: getUTCMonth() without +1). A TIME\n * field is a \"hh:mm:ss.SSS\" string and passes through as text.\n */\nexport function restValue(v) {\n if (v instanceof Date)\n return { $date: v.toISOString() };\n if (Array.isArray(v))\n return v.map(restValue);\n return v;\n}\nconst REST_OPS = {\n eq: \"$eq\",\n ne: \"$ne\",\n gt: \"$gt\",\n ge: \"$gte\",\n lt: \"$lt\",\n le: \"$lte\",\n contains: \"$contains\",\n startsWith: \"$startsWith\",\n endsWith: \"$endsWith\",\n // `in` is spelled $hasSome on the wire, the same predicate the SDK transport sends (on a scalar\n // field: equal to any of the values) — one behaviour on both transports.\n in: \"$hasSome\",\n hasSome: \"$hasSome\",\n hasAll: \"$hasAll\",\n};\n/** One predicate → one filter object `{ field: condition }`. `between` is the builder's ge + lt pair. */\nexport function restCondition(f) {\n assertFilterValue(f);\n if (f.op === \"isEmpty\")\n return { [f.field]: null };\n if (f.op === \"isNotEmpty\")\n return { [f.field]: { $ne: null } };\n if (f.op === \"between\") {\n const [start, end] = betweenRange(f);\n return { [f.field]: { $gte: restValue(start), $lt: restValue(end) } };\n }\n return { [f.field]: { [REST_OPS[f.op]]: restValue(f.value) } };\n}\n/** All predicates AND-ed: `{}` for none, the one object for one, `{ $and: [...] }` otherwise. */\nexport function restFilter(filters = []) {\n const conditions = filters.map(restCondition);\n if (conditions.length === 0)\n return {};\n if (conditions.length === 1)\n return conditions[0];\n return { $and: conditions };\n}\nexport function restSort(sort = []) {\n return sort.map((s) => ({ fieldName: s.field, order: s.direction === \"desc\" ? \"DESC\" : \"ASC\" }));\n}\n/**\n * The full Query Data Items body. Offset paging (`skip` = page N × limit) so every page can carry\n * its own filter/sort and `returnTotalCount` works; `includeReferences[].field` inlines reference\n * items. POST /wix-data/v2/items/query\n */\nexport function queryBody(collectionId, query = {}) {\n const { filters = [], sort = [], limit = DEFAULT_LIMIT, skip = 0, include = [], withTotal = false } = query;\n return {\n dataCollectionId: collectionId,\n query: {\n filter: restFilter(filters),\n ...(sort.length ? { sort: restSort(sort) } : {}),\n paging: { limit, offset: skip },\n },\n ...(include.length ? { includeReferences: include.map((field) => ({ field })) } : {}),\n ...(withTotal ? { returnTotalCount: true } : {}),\n };\n}\n/** Whether another page follows — the response says so; the arithmetic is the fallback. */\nexport function restHasNext(pagingMetadata, offset, limit) {\n if (typeof pagingMetadata?.hasNext === \"boolean\")\n return pagingMetadata.hasNext;\n if (typeof pagingMetadata?.total === \"number\")\n return offset + (pagingMetadata.count ?? 0) < pagingMetadata.total;\n return (pagingMetadata?.count ?? 0) >= limit;\n}\n/**\n * The Count Data Items body — the filter in the same spelling a query carries, top-level.\n * POST /wix-data/v2/items/count → { totalCount }\n */\nexport function countBody(collectionId, filters = []) {\n return { dataCollectionId: collectionId, filter: restFilter(filters) };\n}\n/**\n * The Query Distinct Values body — `fieldName` and `filter` sit top-level (not under `query`).\n * POST /wix-data/v2/items/query-distinct-values → { distinctValues: [...], pagingMetadata }\n */\nexport function distinctBody(collectionId, field, filters = [], limit = DISTINCT_LIMIT) {\n return { dataCollectionId: collectionId, fieldName: field, filter: restFilter(filters), paging: { limit, offset: 0 } };\n}\n// ---- the REST spelling of a write ---------------------------------------------------------------\n/**\n * Field values for an insert/update body — restValue per field (dates → `{ $date }`; JSON.stringify\n * would turn a Date into a plain ISO string, which Wix stores as TEXT and never matches a date\n * query). The read-only system fields are Wix's to stamp, never sent.\n */\nexport function restWriteData(data) {\n const out = {};\n for (const [k, v] of Object.entries(data)) {\n if (k === \"_createdDate\" || k === \"_updatedDate\" || k === \"_owner\")\n continue;\n out[k] = restValue(v);\n }\n return out;\n}\n/** The Patch Data Item modifications for \"set these fields\" — one SET_FIELD per key. PATCH /wix-data/v2/items/{id} */\nexport function patchModifications(fields) {\n const entries = Object.entries(fields);\n if (!entries.length)\n throw new Error(\"cms: patchItemFields called with no fields.\");\n return entries.map(([fieldPath, value]) => ({ fieldPath, action: \"SET_FIELD\", setFieldOptions: { value: restValue(value) } }));\n}\n/**\n * The Bulk Insert / Bulk Remove Data Item References body: one entry per referenced id. The key is\n * `dataItemReferences` with `referringItemFieldName` / `referringItemId` / `referencedItemId` — the\n * natural-looking `references` shape is rejected (400 WDE0080).\n * POST /wix-data/v2/bulk/items/insert-references · POST /wix-data/v2/bulk/items/remove-references\n */\nexport function referencesBody(collectionId, field, itemId, refIds) {\n if (!refIds.length)\n throw new Error(\"cms: linkItems/unlinkItems called with no referenced ids.\");\n return {\n dataCollectionId: collectionId,\n dataItemReferences: refIds.map((referencedItemId) => ({ referringItemFieldName: field, referringItemId: itemId, referencedItemId })),\n };\n}\n",
19 "js/wix/items.js": "// Declarations of items.ts: what this file exports, their arguments and what they return.\n// import type { CmsFilter, CmsItem, CmsPage, CmsQuery } from \"./cms-types.js\";\n// export declare function queryItems(collectionId: string, query?: CmsQuery): Promise<CmsPage>;\n// export declare function getItemById(collectionId: string, itemId: string, { include }?: {\n// include?: string[];\n// }): Promise<CmsItem | null>;\n// export declare function getItemBy(collectionId: string, field: string, value: string | number, { include }?: {\n// include?: string[];\n// }): Promise<CmsItem | null>;\n// export declare function countItems(collectionId: string, filters?: CmsFilter[]): Promise<number>;\n// export declare function distinctValues(collectionId: string, field: string, { filters, limit }?: {\n// filters?: CmsFilter[];\n// limit?: number;\n// }): Promise<unknown[]>;\n// export declare function insertItem(collectionId: string, data: Record<string, unknown>, { link }?: {\n// link?: Record<string, string[]>;\n// }): Promise<CmsItem>;\n// export declare function updateItem(collectionId: string, item: CmsItem): Promise<CmsItem>;\n// export declare function patchItemFields(collectionId: string, itemId: string, fields: Record<string, unknown>): Promise<CmsItem>;\n// export declare function removeItem(collectionId: string, itemId: string): Promise<CmsItem | null>;\n// export declare function linkItems(collectionId: string, field: string, itemId: string, refIds: string[]): Promise<void>;\n// export declare function unlinkItems(collectionId: string, field: string, itemId: string, refIds: string[]): Promise<void>;\n// Wix Data reads/writes over REST — the twin of app/wix/cms/items.ts. Same exports, same DTOs; the\n// rules and mappers come from items-core (the SAME file the SDK transport uses, deployed flat next\n// to this one by deploy.mjs --stack static), so this file is only the transport: one fetch with a\n// literal body per function. The reads are safe from a browser with a visitor token on any\n// collection whose read permission is ANYONE; the writes succeed only where the collection's\n// insert/update/remove permission covers the caller (403 WDE0027 otherwise — a permissions step,\n// not a code bug). Porting: keep the paths, keep the bodies, port the core once.\n// docs: https://dev.wix.com/docs/api-reference/business-solutions/cms/data-items/query-data-items.md\n// docs: https://dev.wix.com/docs/api-reference/business-solutions/cms/data-items/get-data-item.md\n// docs: https://dev.wix.com/docs/api-reference/business-solutions/cms/data-items/patch-data-item.md\n// docs: https://dev.wix.com/docs/api-reference/business-solutions/cms/data-items/bulk-insert-data-item-references.md\nimport { WixApiError, wixRequest } from \"./client.js\";\nimport { imgRatio, imgSrc } from \"./media.js\";\nimport { DEFAULT_LIMIT, DISTINCT_LIMIT, countBody, distinctBody, patchModifications, queryBody, referencesBody, restHasNext, restWriteData, toItem, toPage, toValues, } from \"./items-core.js\";\nconst media = { imgSrc, imgRatio };\nconst notFound = (e) => e instanceof WixApiError && e.status === 404;\n/**\n * Query one page of a collection. An empty result on a PUBLIC collection is a seed permissions bug\n * (read must be \"ANYONE\"), not a query bug.\n * POST /wix-data/v2/items/query { dataCollectionId, query: { filter, sort, paging: { limit, offset } }, includeReferences: [{ field }], returnTotalCount }\n * → { dataItems: [{ id, dataCollectionId, data: { _id, …fields } }], pagingMetadata: { count, offset, total?, hasNext } }\n */\nexport async function queryItems(collectionId, query = {}) {\n const { limit = DEFAULT_LIMIT, skip = 0 } = query;\n const res = await wixRequest(\"/wix-data/v2/items/query\", { body: queryBody(collectionId, query) });\n const items = (res?.dataItems ?? []).map((d) => d.data ?? {});\n return toPage(items, restHasNext(res?.pagingMetadata, skip, limit), res?.pagingMetadata?.total, media);\n}\n/**\n * Fetch one item by `_id`. Null when not found (404 WDE0073). `includeReferences.field` is the\n * request's key per the installed typings — confirm once on a live site that get honours it; if the\n * referenced fields come back as ids, route through getItemBy(\"_id\", …).\n * GET /wix-data/v2/items/{id}?dataCollectionId=…&includeReferences.field=… → { dataItem: { data } }\n */\nexport async function getItemById(collectionId, itemId, { include = [] } = {}) {\n try {\n const res = await wixRequest(`/wix-data/v2/items/${encodeURIComponent(itemId)}`, {\n method: \"GET\",\n query: { dataCollectionId: collectionId, ...(include.length ? { \"includeReferences.field\": include } : {}) },\n });\n return res?.dataItem?.data ? toItem(res.dataItem.data, media) : null;\n }\n catch (e) {\n if (notFound(e))\n return null;\n throw e;\n }\n}\n/**\n * Fetch the first item whose `field` equals `value` — slug-style routing (Wix Data has no native\n * get-by-slug). Null when not found → render a not-found state, never invent an item.\n */\nexport async function getItemBy(collectionId, field, value, { include = [] } = {}) {\n const page = await queryItems(collectionId, { filters: [{ field, op: \"eq\", value }], limit: 1, include });\n return page.items[0] ?? null;\n}\n/**\n * Count items matching the filters — no items transferred.\n * POST /wix-data/v2/items/count { dataCollectionId, filter } → { totalCount }\n */\nexport async function countItems(collectionId, filters = []) {\n const res = await wixRequest(\"/wix-data/v2/items/count\", { body: countBody(collectionId, filters) });\n return typeof res?.totalCount === \"number\" ? res.totalCount : 0;\n}\n/**\n * The distinct values a field holds across the collection (optionally within `filters`) — filter\n * chips and selects from live data. Values normalized like item values; a reference field yields ids.\n * POST /wix-data/v2/items/query-distinct-values { dataCollectionId, fieldName, filter, paging: { limit, offset } } → { distinctValues: [...] }\n */\nexport async function distinctValues(collectionId, field, { filters = [], limit = DISTINCT_LIMIT } = {}) {\n const res = await wixRequest(\"/wix-data/v2/items/query-distinct-values\", { body: distinctBody(collectionId, field, filters, limit) });\n return toValues((res?.distinctValues ?? []), media);\n}\n/**\n * Insert an item (visitor form / member submission). Never set `_owner` — Wix stamps it from the\n * caller's identity. Date fields must be Date objects (the core spells them `{ $date }` on the wire;\n * an ISO string is stored as text and breaks date queries). A MULTI_REFERENCE value inside `data` is\n * DROPPED by the endpoint (200, no error) — pass those as `link: { field: [referencedIds] }` and they\n * are linked right after the insert. The returned item predates the links.\n * POST /wix-data/v2/items { dataCollectionId, dataItem: { data } } → { dataItem: { data } }\n */\nexport async function insertItem(collectionId, data, { link = {} } = {}) {\n const res = await wixRequest(\"/wix-data/v2/items\", { body: { dataCollectionId: collectionId, dataItem: { data: restWriteData(data) } } });\n const item = toItem(res?.dataItem?.data ?? {}, media);\n for (const [field, refIds] of Object.entries(link))\n await linkItems(collectionId, field, item._id, refIds);\n return item;\n}\n/**\n * REPLACE an item — fields omitted from `item` are WIPED (update does not patch). Spread the full\n * item you hold and override; for an id + a few changed fields use patchItemFields. Wrap each date\n * field back into a Date (`new Date(iso)`) before updating.\n * PUT /wix-data/v2/items/{id} { dataCollectionId, dataItem: { id, data } } → { dataItem: { data } }\n */\nexport async function updateItem(collectionId, item) {\n const res = await wixRequest(`/wix-data/v2/items/${encodeURIComponent(item._id)}`, {\n method: \"PUT\",\n body: { dataCollectionId: collectionId, dataItem: { id: item._id, data: restWriteData(item) } },\n });\n return toItem(res?.dataItem?.data ?? {}, media);\n}\n/**\n * Patch only the named fields — the safe partial change.\n * PATCH /wix-data/v2/items/{id} { dataCollectionId, patch: { dataItemId, fieldModifications: [{ fieldPath, action: \"SET_FIELD\", setFieldOptions: { value } }] } }\n */\nexport async function patchItemFields(collectionId, itemId, fields) {\n const res = await wixRequest(`/wix-data/v2/items/${encodeURIComponent(itemId)}`, {\n method: \"PATCH\",\n body: { dataCollectionId: collectionId, patch: { dataItemId: itemId, fieldModifications: patchModifications(fields) } },\n });\n return toItem(res?.dataItem?.data ?? {}, media);\n}\n/**\n * Remove an item by `_id`. Irreversible. Returns the removed item (null if it didn't exist).\n * DELETE /wix-data/v2/items/{id}?dataCollectionId=… → { dataItem: { data } }\n */\nexport async function removeItem(collectionId, itemId) {\n try {\n const res = await wixRequest(`/wix-data/v2/items/${encodeURIComponent(itemId)}`, { method: \"DELETE\", query: { dataCollectionId: collectionId } });\n return res?.dataItem?.data ? toItem(res.dataItem.data, media) : null;\n }\n catch (e) {\n if (notFound(e))\n return null;\n throw e;\n }\n}\n/**\n * Add references to a MULTI_REFERENCE field of `itemId` — the only way a multi-reference is set\n * (insert/update drop the value silently). Existing references stay; needs the collection's update\n * permission. A no-op for an empty list.\n * POST /wix-data/v2/bulk/items/insert-references { dataCollectionId, dataItemReferences: [{ referringItemFieldName, referringItemId, referencedItemId }] }\n */\nexport async function linkItems(collectionId, field, itemId, refIds) {\n if (!refIds.length)\n return;\n await wixRequest(\"/wix-data/v2/bulk/items/insert-references\", { body: referencesBody(collectionId, field, itemId, refIds) });\n}\n/**\n * Remove references from a MULTI_REFERENCE field of `itemId`. Other references stay. A no-op for an empty list.\n * POST /wix-data/v2/bulk/items/remove-references { dataCollectionId, dataItemReferences: [...] } (same body as the insert)\n */\nexport async function unlinkItems(collectionId, field, itemId, refIds) {\n if (!refIds.length)\n return;\n await wixRequest(\"/wix-data/v2/bulk/items/remove-references\", { body: referencesBody(collectionId, field, itemId, refIds) });\n}\n"