← Files WixARCHIVED FILE

skills/wix-headless-templates/cms/rest/items.ts

9.26 KB · Oct 8, 2026 · 12:02 UTC

↓ Download file

See the change to this file →

// 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