{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "rest-driver",
  "title": "REST Data Source Driver",
  "description": "Optional Fable UI data source driver for host-owned REST endpoints.",
  "registryDependencies": [
    "shobky/fable-ui/core"
  ],
  "files": [
    {
      "path": "lib/fable-ui/drivers/rest/rest-driver.types.ts",
      "content": "import type { DataSourceContext } from \"@/lib/fable-ui/core\"\n\nexport type RestResourceSource = {\n  endpoint: string\n  rowEndpoint?: (rowId: string) => string\n  actionEndpoint?: (actionId: string, rowId?: string) => string\n}\n\nexport type RestDriverConfig = {\n  baseUrl?: string\n  headers?:\n    | Record<string, string>\n    | ((ctx: DataSourceContext) => Promise<Record<string, string>> | Record<string, string>)\n}\n\nexport type RestListResponse<Row> =\n  | Row[]\n  | {\n      rows?: Row[]\n      data?: Row[]\n      totalRows?: number\n      total?: number\n      page?: number\n      pageSize?: number\n      nextCursor?: string\n      previousCursor?: string\n    }\n",
      "type": "registry:lib",
      "target": "@lib/fable-ui/drivers/rest/rest-driver.types.ts"
    },
    {
      "path": "lib/fable-ui/drivers/rest/rest-driver.ts",
      "content": "import type {\n  DataActionInput,\n  DataActionResult,\n  DataQuery,\n  DataQueryResult,\n  DataRow,\n  DataSourceContext,\n  DataSourceDriver,\n  ResourceConfig,\n  ResourceRuntime,\n} from \"@/lib/fable-ui/core\"\nimport type { RestDriverConfig, RestListResponse, RestResourceSource } from \"./rest-driver.types\"\n\nfunction replaceTemplate(value: string, ctx: DataSourceContext) {\n  return value.replace(/\\{([^}]+)\\}/g, (_match, key: string) => {\n    const replacement = ctx[key]\n\n    if (replacement == null) {\n      throw new Error(`Missing REST path parameter \"${key}\".`)\n    }\n\n    return encodeURIComponent(String(replacement))\n  })\n}\n\nfunction buildUrl(path: string, baseUrl: string | undefined, ctx: DataSourceContext) {\n  const resolvedPath = replaceTemplate(path, ctx)\n\n  if (/^https?:\\/\\//i.test(resolvedPath)) {\n    return new URL(resolvedPath)\n  }\n\n  const base = baseUrl || (typeof window !== \"undefined\" ? window.location.origin : \"http://localhost\")\n  return new URL(resolvedPath, base)\n}\n\nfunction appendQuery(url: URL, query: DataQuery) {\n  if (query.cursor) {\n    url.searchParams.set(\"cursor\", query.cursor)\n  }\n\n  if (query.pageSize) {\n    url.searchParams.set(\"pageSize\", String(query.pageSize))\n  }\n\n  if (query.search) {\n    url.searchParams.set(\"search\", query.search)\n  }\n\n  if (query.sort) {\n    url.searchParams.set(\"sort\", `${query.sort.key}:${query.sort.direction}`)\n  }\n\n  for (const [key, value] of Object.entries(query.filters ?? {})) {\n    if (value == null || value === \"\") {\n      continue\n    }\n\n    if (Array.isArray(value)) {\n      for (const item of value) {\n        url.searchParams.append(`filter[${key}]`, String(item))\n      }\n    } else {\n      url.searchParams.set(`filter[${key}]`, String(value))\n    }\n  }\n}\n\nasync function getHeaders(config: RestDriverConfig, ctx: DataSourceContext) {\n  const configuredHeaders =\n    typeof config.headers === \"function\" ? await config.headers(ctx) : (config.headers ?? {})\n  const token = await ctx.auth?.getAccessToken?.()\n\n  return {\n    Accept: \"application/json\",\n    \"Content-Type\": \"application/json\",\n    ...configuredHeaders,\n    ...(token ? { Authorization: `Bearer ${token}` } : {}),\n  }\n}\n\nasync function readJsonResponse(response: Response) {\n  const text = await response.text()\n\n  if (!text) {\n    return null\n  }\n\n  try {\n    return JSON.parse(text) as unknown\n  } catch {\n    return text\n  }\n}\n\nasync function assertOk(response: Response) {\n  if (response.ok) {\n    return\n  }\n\n  const body = await readJsonResponse(response)\n  const message =\n    typeof body === \"object\" && body && \"message\" in body\n      ? String((body as { message?: unknown }).message)\n      : typeof body === \"string\"\n        ? body\n        : response.statusText\n\n  if (response.status === 401 || response.status === 403) {\n    throw new Error(`REST permission error: ${message}`)\n  }\n\n  if (response.status === 404) {\n    throw new Error(`REST resource not found: ${message}`)\n  }\n\n  if (response.status >= 500) {\n    throw new Error(`REST server error: ${message}`)\n  }\n\n  throw new Error(`REST request failed: ${message}`)\n}\n\nfunction normalizeListResponse<Row extends DataRow>(\n  body: RestListResponse<Row>,\n  query: DataQuery,\n): DataQueryResult<Row> {\n  if (Array.isArray(body)) {\n    return {\n      rows: body,\n      totalRows: body.length,\n      page: query.page ?? 1,\n      pageSize: query.pageSize ?? body.length,\n    }\n  }\n\n  const rows = body.rows ?? body.data ?? []\n\n  return {\n    rows,\n    totalRows: body.totalRows ?? body.total ?? rows.length,\n    page: body.page ?? query.page ?? 1,\n    pageSize: body.pageSize ?? query.pageSize ?? rows.length,\n    nextCursor: body.nextCursor,\n    previousCursor: body.previousCursor,\n  }\n}\n\nfunction defaultRowEndpoint(source: RestResourceSource, rowId: string) {\n  return source.rowEndpoint ? source.rowEndpoint(rowId) : `${source.endpoint.replace(/\\/$/, \"\")}/${encodeURIComponent(rowId)}`\n}\n\nfunction defaultActionEndpoint(source: RestResourceSource, actionId: string, rowId?: string) {\n  if (source.actionEndpoint) {\n    return source.actionEndpoint(actionId, rowId)\n  }\n\n  const base = rowId ? defaultRowEndpoint(source, rowId) : source.endpoint\n  return `${base.replace(/\\/$/, \"\")}/actions/${encodeURIComponent(actionId)}`\n}\n\nexport function createRestDriver(config: RestDriverConfig = {}): DataSourceDriver<RestResourceSource> {\n  return {\n    async list<Row extends DataRow>(\n      resource: ResourceConfig<RestResourceSource, Row>,\n      query: DataQuery,\n      ctx: DataSourceContext,\n      runtime?: ResourceRuntime<Row, RestResourceSource>,\n    ) {\n      if (runtime?.list) {\n        return runtime.list(resource, query, ctx)\n      }\n\n      const url = buildUrl(resource.source.endpoint, config.baseUrl, ctx)\n      appendQuery(url, query)\n\n      const response = await fetch(url, {\n        method: \"GET\",\n        headers: await getHeaders(config, ctx),\n        signal: ctx.signal,\n      })\n      await assertOk(response)\n\n      return normalizeListResponse<Row>((await readJsonResponse(response)) as RestListResponse<Row>, query)\n    },\n\n    async get<Row extends DataRow>(\n      resource: ResourceConfig<RestResourceSource, Row>,\n      rowId: string,\n      ctx: DataSourceContext,\n      runtime?: ResourceRuntime<Row, RestResourceSource>,\n    ) {\n      if (runtime?.get) {\n        return runtime.get(resource, rowId, ctx)\n      }\n\n      const url = buildUrl(defaultRowEndpoint(resource.source, rowId), config.baseUrl, ctx)\n      const response = await fetch(url, {\n        method: \"GET\",\n        headers: await getHeaders(config, ctx),\n        signal: ctx.signal,\n      })\n      await assertOk(response)\n\n      return (await readJsonResponse(response)) as Row\n    },\n\n    async executeAction(\n      input: DataActionInput,\n      resource: ResourceConfig<RestResourceSource>,\n      ctx: DataSourceContext,\n      runtime?: ResourceRuntime<DataRow, RestResourceSource>,\n    ): Promise<DataActionResult> {\n      if (runtime?.executeAction) {\n        return runtime.executeAction(input, resource, ctx)\n      }\n\n      const url = buildUrl(\n        defaultActionEndpoint(resource.source, input.actionId, input.rowId),\n        config.baseUrl,\n        ctx,\n      )\n      const response = await fetch(url, {\n        method: \"POST\",\n        headers: await getHeaders(config, ctx),\n        body: JSON.stringify(input.values ?? {}),\n        signal: ctx.signal,\n      })\n      await assertOk(response)\n\n      const body = await readJsonResponse(response)\n\n      if (typeof body === \"object\" && body && \"ok\" in body) {\n        return body as DataActionResult\n      }\n\n      return { ok: true, data: body, invalidate: true }\n    },\n  }\n}\n",
      "type": "registry:lib",
      "target": "@lib/fable-ui/drivers/rest/rest-driver.ts"
    },
    {
      "path": "lib/fable-ui/drivers/rest/index.ts",
      "content": "export * from \"./rest-driver\"\nexport * from \"./rest-driver.types\"\n",
      "type": "registry:lib",
      "target": "@lib/fable-ui/drivers/rest/index.ts"
    }
  ],
  "type": "registry:lib"
}