← Files WixARCHIVED FILE

skills/wix-headless-templates/faq/rest/faq.ts

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

↓ Download file

See the change to this file →

// FAQ reads over REST — the twin of app/wix/faq/faq.ts. Same exports, same DTOs; the rules and mappers
// come from faq-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. Both endpoints are the FAQ app's READ scope; whether an anonymous VISITOR token may call
// them is not documented — the reference shows an elevated example — so a 403 here means the site's
// read needs a server-side path, not that the body is wrong (INSTRUCTIONS.md, verification caveats).
// Porting: keep the paths, keep the bodies, port the core once.
// docs: https://dev.wix.com/docs/rest/business-management/faq-app/faq/category-v2/query-categories.md
// docs: https://dev.wix.com/docs/rest/business-management/faq-app/faq/question-entry-v2/query-question-entries.md
import { wixRequest } from "./client.js";
import { imgSrc } from "./media.js";
import {
  FAQ_APP_ID,
  assembleFaq,
  categoriesQueryBody,
  loadAllQuestions,
  nextCursor,
  questionByIdBody,
  questionsQueryBody,
  sortCategories,
  toCategory,
  toQuestion,
  type Raw,
} from "./faq-core.js";
import type { FaqCategory, FaqData, FaqQuestion, FaqQuestionPage, FetchQuestionsOptions } from "./types.js";

export { FAQ_APP_ID };

const CATEGORIES_QUERY = "/faq/v2/categories/query";
const QUESTIONS_QUERY = "/faq/v2/question-entries/query";

/**
 * All categories in owner order, one page (a site has few). `questionCount` is 0 here — fetchFaq fills it.
 * POST /faq/v2/categories/query  { query: { sort: [{ fieldName: "sortOrder", order: "ASC" }], cursorPaging: { limit: 100 } } }
 *   → { categories: [{ id, title, sortOrder, revision, createdDate, updatedDate }], pagingMetadata }
 */
export async function fetchCategories(): Promise<FaqCategory[]> {
  const res = await wixRequest<Raw>(CATEGORIES_QUERY, { body: categoriesQueryBody() });
  return sortCategories(((res?.categories ?? []) as Raw[]).map((c) => toCategory(c)));
}

/**
 * One cursor page of questions, optionally one category's; `nextCursor` continues the same query —
 * send it alone, the filter and sort ride inside it.
 * POST /faq/v2/question-entries/query  { query: { filter?: { categoryId: { $eq } }, sort, cursorPaging: { limit } }, contentFormat: "RICH_CONTENT", fieldSet: ["SHARE_LINKS"] }  — first page
 * POST /faq/v2/question-entries/query  { query: { cursorPaging: { limit, cursor } }, contentFormat, fieldSet }                                                        — later pages
 *   → { questionEntries: [{ id, question, richContent | plainText | draftjs, categoryId, sortOrder, slug, shareLink, labels }], pagingMetadata: { count, cursors: { next, prev }, hasNext } }
 */
export async function fetchQuestions(o: FetchQuestionsOptions = {}): Promise<FaqQuestionPage> {
  const res = await wixRequest<Raw>(QUESTIONS_QUERY, { body: questionsQueryBody(o) });
  return {
    questions: ((res?.questionEntries ?? []) as Raw[]).map((r) => toQuestion(r, imgSrc)),
    nextCursor: nextCursor(res?.pagingMetadata),
  };
}

/** Everything the FAQ page needs: categories (counted) + every question (paged until done or MAX_QUESTIONS). */
export async function fetchFaq(): Promise<FaqData> {
  const [cats, { questions, truncated }] = await Promise.all([fetchCategories(), loadAllQuestions(fetchQuestions)]);
  return assembleFaq(cats, questions, truncated);
}

/**
 * One question by id — the same query read as the page, so one permission covers both. Null when missing.
 * POST /faq/v2/question-entries/query  { query: { filter: { _id: { $eq } }, cursorPaging: { limit: 1 } }, contentFormat, fieldSet }
 */
export async function fetchQuestionById(id: string): Promise<FaqQuestion | null> {
  const res = await wixRequest<Raw>(QUESTIONS_QUERY, { body: questionByIdBody(id) });
  const raw: Raw | undefined = res?.questionEntries?.[0];
  return raw ? toQuestion(raw, imgSrc) : null;
}

SHA-256: c652e38ab9ea3de67e805595b4d9c720eb29d1650da7f0ad981406739d3ef4f2