← Files WixARCHIVED FILE
skills/wix-headless-templates/cms/rest/items.ts
9.26 KB · Oct 8, 2026 · 12:02 UTC
// Wix Data reads/writes over REST — the twin of app/wix/cms/items.ts. Same exports, same DTOs; the
// rules and mappers come from items-core (the SAME file the SDK transport uses, deployed flat next
// to this one by deploy.mjs --stack static), so this file is only the transport: one fetch with a
// literal body per function. The reads are safe from a browser with a visitor token on any
// collection whose read permission is ANYONE; the writes succeed only where the collection's
// insert/update/remove permission covers the caller (403 WDE0027 otherwise — a permissions step,
// not a code bug). Porting: keep the paths, keep the bodies, port the core once.
// docs: https://dev.wix.com/docs/api-reference/business-solutions/cms/data-items/query-data-items.md
// docs: https://dev.wix.com/docs/api-reference/business-solutions/cms/data-items/get-data-item.md
// docs: https://dev.wix.com/docs/api-reference/business-solutions/cms/data-items/patch-data-item.md
// docs: https://dev.wix.com/docs/api-reference/business-solutions/cms/data-items/bulk-insert-data-item-references.md
import { WixApiError, wixRequest } from "./client.js";
import { imgRatio, imgSrc } from "./media.js";
import {
DEFAULT_LIMIT,
DISTINCT_LIMIT,
countBody,
distinctBody,
patchModifications,
queryBody,
referencesBody,
restHasNext,
restWriteData,
toItem,
toPage,
toValues,
type Media,
type Raw,
} from "./items-core.js";
import type { CmsFilter, CmsItem, CmsPage, CmsQuery } from "./types.js";
const media: Media = { imgSrc, imgRatio };
const notFound = (e: unknown): boolean => e instanceof WixApiError && e.status === 404;
/**
* Query one page of a collection. An empty result on a PUBLIC collection is a seed permissions bug
* (read must be "ANYONE"), not a query bug.
* POST /wix-data/v2/items/query { dataCollectionId, query: { filter, sort, paging: { limit, offset } }, includeReferences: [{ field }], returnTotalCount }
* → { dataItems: [{ id, dataCollectionId, data: { _id, …fields } }], pagingMetadata: { count, offset, total?, hasNext } }
*/
export async function queryItems(collectionId: string, query: CmsQuery = {}): Promise<CmsPage> {
const { limit = DEFAULT_LIMIT, skip = 0 } = query;
const res = await wixRequest<Raw>("/wix-data/v2/items/query", { body: queryBody(collectionId, query) });
const items = ((res?.dataItems ?? []) as Raw[]).map((d) => d.data ?? {});
return toPage(items, restHasNext(res?.pagingMetadata, skip, limit), res?.pagingMetadata?.total, media);
}
/**
* Fetch one item by `_id`. Null when not found (404 WDE0073). `includeReferences.field` is the
* request's key per the installed typings — confirm once on a live site that get honours it; if the
* referenced fields come back as ids, route through getItemBy("_id", …).
* GET /wix-data/v2/items/{id}?dataCollectionId=…&includeReferences.field=… → { dataItem: { data } }
*/
export async function getItemById(
collectionId: string,
itemId: string,
{ include = [] }: { include?: string[] } = {},
): Promise<CmsItem | null> {
try {
const res = await wixRequest<Raw>(`/wix-data/v2/items/${encodeURIComponent(itemId)}`, {
method: "GET",
query: { dataCollectionId: collectionId, ...(include.length ? { "includeReferences.field": include } : {}) },
});
return res?.dataItem?.data ? toItem(res.dataItem.data, media) : null;
} catch (e) {
if (notFound(e)) return null;
throw e;
}
}
/**
* Fetch the first item whose `field` equals `value` — slug-style routing (Wix Data has no native
* get-by-slug). Null when not found → render a not-found state, never invent an item.
*/
export async function getItemBy(
collectionId: string,
field: string,
value: string | number,
{ include = [] }: { include?: string[] } = {},
): Promise<CmsItem | null> {
const page = await queryItems(collectionId, { filters: [{ field, op: "eq", value }], limit: 1, include });
return page.items[0] ?? null;
}
/**
* Count items matching the filters — no items transferred.
* POST /wix-data/v2/items/count { dataCollectionId, filter } → { totalCount }
*/
export async function countItems(collectionId: string, filters: CmsFilter[] = []): Promise<number> {
const res = await wixRequest<Raw>("/wix-data/v2/items/count", { body: countBody(collectionId, filters) });
return typeof res?.totalCount === "number" ? res.totalCount : 0;
}
/**
* The distinct values a field holds across the collection (optionally within `filters`) — filter
* chips and selects from live data. Values normalized like item values; a reference field yields ids.
* POST /wix-data/v2/items/query-distinct-values { dataCollectionId, fieldName, filter, paging: { limit, offset } } → { distinctValues: [...] }
*/
export async function distinctValues(
collectionId: string,
field: string,
{ filters = [], limit = DISTINCT_LIMIT }: { filters?: CmsFilter[]; limit?: number } = {},
): Promise<unknown[]> {
const res = await wixRequest<Raw>("/wix-data/v2/items/query-distinct-values", { body: distinctBody(collectionId, field, filters, limit) });
return toValues((res?.distinctValues ?? []) as unknown[], media);
}
/**
* Insert an item (visitor form / member submission). Never set `_owner` — Wix stamps it from the
* caller's identity. Date fields must be Date objects (the core spells them `{ $date }` on the wire;
* an ISO string is stored as text and breaks date queries). A MULTI_REFERENCE value inside `data` is
* DROPPED by the endpoint (200, no error) — pass those as `link: { field: [referencedIds] }` and they
* are linked right after the insert. The returned item predates the links.
* POST /wix-data/v2/items { dataCollectionId, dataItem: { data } } → { dataItem: { data } }
*/
export async function insertItem(
collectionId: string,
data: Record<string, unknown>,
{ link = {} }: { link?: Record<string, string[]> } = {},
): Promise<CmsItem> {
const res = await wixRequest<Raw>("/wix-data/v2/items", { body: { dataCollectionId: collectionId, dataItem: { data: restWriteData(data) } } });
const item = toItem(res?.dataItem?.data ?? {}, media);
for (const [field, refIds] of Object.entries(link)) await linkItems(collectionId, field, item._id, refIds);
return item;
}
/**
* REPLACE an item — fields omitted from `item` are WIPED (update does not patch). Spread the full
* item you hold and override; for an id + a few changed fields use patchItemFields. Wrap each date
* field back into a Date (`new Date(iso)`) before updating.
* PUT /wix-data/v2/items/{id} { dataCollectionId, dataItem: { id, data } } → { dataItem: { data } }
*/
export async function updateItem(collectionId: string, item: CmsItem): Promise<CmsItem> {
const res = await wixRequest<Raw>(`/wix-data/v2/items/${encodeURIComponent(item._id)}`, {
method: "PUT",
body: { dataCollectionId: collectionId, dataItem: { id: item._id, data: restWriteData(item) } },
});
return toItem(res?.dataItem?.data ?? {}, media);
}
/**
* Patch only the named fields — the safe partial change.
* PATCH /wix-data/v2/items/{id} { dataCollectionId, patch: { dataItemId, fieldModifications: [{ fieldPath, action: "SET_FIELD", setFieldOptions: { value } }] } }
*/
export async function patchItemFields(
collectionId: string,
itemId: string,
fields: Record<string, unknown>,
): Promise<CmsItem> {
const res = await wixRequest<Raw>(`/wix-data/v2/items/${encodeURIComponent(itemId)}`, {
method: "PATCH",
body: { dataCollectionId: collectionId, patch: { dataItemId: itemId, fieldModifications: patchModifications(fields) } },
});
return toItem(res?.dataItem?.data ?? {}, media);
}
/**
* Remove an item by `_id`. Irreversible. Returns the removed item (null if it didn't exist).
* DELETE /wix-data/v2/items/{id}?dataCollectionId=… → { dataItem: { data } }
*/
export async function removeItem(collectionId: string, itemId: string): Promise<CmsItem | null> {
try {
const res = await wixRequest<Raw>(`/wix-data/v2/items/${encodeURIComponent(itemId)}`, { method: "DELETE", query: { dataCollectionId: collectionId } });
return res?.dataItem?.data ? toItem(res.dataItem.data, media) : null;
} catch (e) {
if (notFound(e)) return null;
throw e;
}
}
/**
* Add references to a MULTI_REFERENCE field of `itemId` — the only way a multi-reference is set
* (insert/update drop the value silently). Existing references stay; needs the collection's update
* permission. A no-op for an empty list.
* POST /wix-data/v2/bulk/items/insert-references { dataCollectionId, dataItemReferences: [{ referringItemFieldName, referringItemId, referencedItemId }] }
*/
export async function linkItems(collectionId: string, field: string, itemId: string, refIds: string[]): Promise<void> {
if (!refIds.length) return;
await wixRequest<Raw>("/wix-data/v2/bulk/items/insert-references", { body: referencesBody(collectionId, field, itemId, refIds) });
}
/**
* Remove references from a MULTI_REFERENCE field of `itemId`. Other references stay. A no-op for an empty list.
* POST /wix-data/v2/bulk/items/remove-references { dataCollectionId, dataItemReferences: [...] } (same body as the insert)
*/
export async function unlinkItems(collectionId: string, field: string, itemId: string, refIds: string[]): Promise<void> {
if (!refIds.length) return;
await wixRequest<Raw>("/wix-data/v2/bulk/items/remove-references", { body: referencesBody(collectionId, field, itemId, refIds) });
}
SHA-256: 8c2e2c0ce7b6cbebbe23574489005e6e95c7befdbababcae672f0a3dd2880656