{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "firebase-driver",
  "title": "Firebase Data Source Driver",
  "description": "Optional Fable UI data source driver for allowlisted Firestore resources.",
  "dependencies": [
    "firebase"
  ],
  "registryDependencies": [
    "shobky/fable-ui/core"
  ],
  "files": [
    {
      "path": "lib/fable-ui/drivers/firebase/firebase-driver.types.ts",
      "content": "import type { DataSourceContext } from \"@/lib/fable-ui/core\"\n\nexport type FirestorePathParam =\n  | string\n  | ((ctx: DataSourceContext) => string | Promise<string>)\n\nexport type FirestoreResourceSource = {\n  collection: string\n  pathParams?: Record<string, FirestorePathParam>\n  idField?: string\n  requireAuth?: boolean\n}\n\nexport type FirebaseAuthLike = {\n  currentUser?: {\n    uid?: string | null\n  } | null\n}\n\nexport type FirebaseDriverConfig = {\n  db: unknown\n  auth?: FirebaseAuthLike\n}\n\nexport type FirestoreSdk = {\n  collection: (...args: unknown[]) => unknown\n  doc: (...args: unknown[]) => unknown\n  getDoc: (ref: unknown) => Promise<unknown>\n  getDocs: (query: unknown) => Promise<unknown>\n  query: (...args: unknown[]) => unknown\n  where: (...args: unknown[]) => unknown\n  orderBy: (...args: unknown[]) => unknown\n  limit: (count: number) => unknown\n  startAfter: (cursor: unknown) => unknown\n  updateDoc: (ref: unknown, values: Record<string, unknown>) => Promise<void>\n}\n",
      "type": "registry:lib",
      "target": "@lib/fable-ui/drivers/firebase/firebase-driver.types.ts"
    },
    {
      "path": "lib/fable-ui/drivers/firebase/firebase-driver.ts",
      "content": "import {\n  getDataValue,\n  queryLocalRows,\n  type DataActionInput,\n  type DataActionResult,\n  type DataFilter,\n  type DataQuery,\n  type DataQueryResult,\n  type DataRow,\n  type DataSourceContext,\n  type DataSourceDriver,\n  type ResourceConfig,\n  type ResourceRuntime,\n} from \"@/lib/fable-ui/core\"\nimport type {\n  FirebaseAuthLike,\n  FirebaseDriverConfig,\n  FirestorePathParam,\n  FirestoreResourceSource,\n  FirestoreSdk,\n} from \"./firebase-driver.types\"\n\nasync function loadFirestoreSdk(): Promise<FirestoreSdk> {\n  const moduleName = \"firebase/firestore\"\n  return (await import(moduleName)) as FirestoreSdk\n}\n\nfunction mapFirestoreError(error: unknown) {\n  const message = error instanceof Error ? error.message : String(error || \"\")\n  const code =\n    typeof error === \"object\" && error && \"code\" in error\n      ? String((error as { code?: unknown }).code)\n      : \"\"\n\n  if (/permission-denied|permission|forbidden/i.test(`${code} ${message}`)) {\n    return new Error(\"Firestore permission denied. Check authentication and security rules.\")\n  }\n\n  if (/failed-precondition|index/i.test(`${code} ${message}`)) {\n    return new Error(\"Firestore query requires an index. Check the Firebase console index suggestion.\")\n  }\n\n  if (/invalid-argument|invalid/i.test(`${code} ${message}`)) {\n    return new Error(`Firestore query is invalid: ${message}`)\n  }\n\n  return error instanceof Error ? error : new Error(\"Firestore request failed.\")\n}\n\nfunction assertSafePathParam(key: string, value: string) {\n  if (!value || value.includes(\"/\") || value.includes(\"..\")) {\n    throw new Error(`Invalid Firestore path parameter \"${key}\".`)\n  }\n\n  return value\n}\n\nasync function resolvePathParam(key: string, param: FirestorePathParam, ctx: DataSourceContext) {\n  const value = typeof param === \"function\" ? await param(ctx) : param\n  return assertSafePathParam(key, String(value))\n}\n\nasync function resolveCollectionPath(source: FirestoreResourceSource, ctx: DataSourceContext) {\n  const params: Record<string, string> = {}\n\n  if (ctx.orgId) {\n    params.orgId = assertSafePathParam(\"orgId\", String(ctx.orgId))\n  }\n\n  if (ctx.tenantId) {\n    params.tenantId = assertSafePathParam(\"tenantId\", String(ctx.tenantId))\n  }\n\n  for (const [key, param] of Object.entries(source.pathParams ?? {})) {\n    params[key] = await resolvePathParam(key, param, ctx)\n  }\n\n  return source.collection.replace(/\\{([^}]+)\\}/g, (_match, key: string) => {\n    const value = params[key]\n\n    if (!value) {\n      throw new Error(`Missing Firestore path parameter \"${key}\".`)\n    }\n\n    return value\n  })\n}\n\nfunction assertAllowedFilter(\n  resource: Pick<ResourceConfig<FirestoreResourceSource>, \"id\" | \"filters\">,\n  key: string,\n) {\n  const allowed = new Set((resource.filters ?? []).map((filter) => filter.key))\n\n  if (allowed.size > 0 && !allowed.has(key)) {\n    throw new Error(`Filter \"${key}\" is not allowed for resource \"${resource.id}\".`)\n  }\n}\n\nfunction assertAllowedSort(\n  resource: Pick<ResourceConfig<FirestoreResourceSource>, \"id\" | \"sort\">,\n  key: string,\n) {\n  const allowed = new Set((resource.sort ?? []).map((sort) => sort.key))\n\n  if (allowed.size > 0 && !allowed.has(key)) {\n    throw new Error(`Sort \"${key}\" is not allowed for resource \"${resource.id}\".`)\n  }\n}\n\nfunction getFilterConfig(resource: Pick<ResourceConfig<FirestoreResourceSource>, \"filters\">, key: string) {\n  return (resource.filters ?? []).find((filter) => filter.key === key)\n}\n\nfunction getDatePresetRange(value: unknown) {\n  if (typeof value !== \"string\") {\n    return null\n  }\n\n  const now = new Date()\n  const start = new Date(now)\n  const end = new Date(now)\n\n  if (value === \"today\") {\n    start.setHours(0, 0, 0, 0)\n    end.setHours(23, 59, 59, 999)\n  } else if (value === \"yesterday\") {\n    start.setDate(start.getDate() - 1)\n    start.setHours(0, 0, 0, 0)\n    end.setDate(end.getDate() - 1)\n    end.setHours(23, 59, 59, 999)\n  } else if (value === \"this_week\") {\n    const day = start.getDay()\n    start.setDate(start.getDate() - day)\n    start.setHours(0, 0, 0, 0)\n    end.setHours(23, 59, 59, 999)\n  } else if (value === \"this_month\") {\n    start.setDate(1)\n    start.setHours(0, 0, 0, 0)\n    end.setMonth(end.getMonth() + 1, 0)\n    end.setHours(23, 59, 59, 999)\n  } else {\n    return null\n  }\n\n  return { start, end }\n}\n\nfunction appendFilterConstraint(\n  sdk: FirestoreSdk,\n  constraints: unknown[],\n  resource: Pick<ResourceConfig<FirestoreResourceSource>, \"id\" | \"filters\">,\n  key: string,\n  value: unknown,\n) {\n  if (value == null || value === \"\" || (Array.isArray(value) && value.length === 0)) {\n    return\n  }\n\n  assertAllowedFilter(resource, key)\n  const filter = getFilterConfig(resource, key)\n  const datePreset = filter?.type === \"date-preset\" ? getDatePresetRange(value) : null\n\n  if (datePreset) {\n    constraints.push(sdk.where(key, \">=\", datePreset.start))\n    constraints.push(sdk.where(key, \"<=\", datePreset.end))\n    return\n  }\n\n  if (Array.isArray(value)) {\n    constraints.push(sdk.where(key, \"in\", value.slice(0, 10)))\n    return\n  }\n\n  constraints.push(sdk.where(key, \"==\", value))\n}\n\nfunction appendSearchConstraints(\n  sdk: FirestoreSdk,\n  constraints: unknown[],\n  resource: Pick<ResourceConfig<FirestoreResourceSource>, \"search\">,\n  query: DataQuery,\n) {\n  const search = query.search?.trim()\n\n  if (!search || resource.search?.mode === \"client\") {\n    return\n  }\n\n  const field = resource.search?.fields?.[0]\n\n  if (!field) {\n    return\n  }\n\n  if (resource.search?.mode === \"prefix\") {\n    constraints.push(sdk.where(field, \">=\", search))\n    constraints.push(sdk.where(field, \"<\", `${search}\\uf8ff`))\n    return\n  }\n\n  constraints.push(sdk.where(field, \"==\", search))\n}\n\nfunction serializeFirestoreValue(value: unknown): unknown {\n  if (value && typeof value === \"object\" && \"toDate\" in value && typeof (value as { toDate?: unknown }).toDate === \"function\") {\n    return ((value as { toDate: () => Date }).toDate()).toISOString()\n  }\n\n  if (Array.isArray(value)) {\n    return value.map(serializeFirestoreValue)\n  }\n\n  if (value && typeof value === \"object\") {\n    return Object.fromEntries(\n      Object.entries(value as Record<string, unknown>).map(([key, nextValue]) => [\n        key,\n        serializeFirestoreValue(nextValue),\n      ]),\n    )\n  }\n\n  return value\n}\n\nfunction docToRow<Row extends DataRow>(docSnapshot: unknown, idField = \"id\") {\n  const doc = docSnapshot as {\n    id: string\n    data: () => Record<string, unknown>\n  }\n  const data = serializeFirestoreValue(doc.data()) as Row\n\n  return {\n    ...data,\n    [idField]: (data as DataRow)[idField] ?? doc.id,\n    id: (data as DataRow).id ?? doc.id,\n  } as Row\n}\n\nfunction getDocsFromSnapshot<Row extends DataRow>(snapshot: unknown, idField?: string) {\n  const docs = (snapshot as { docs?: unknown[] }).docs ?? []\n  return docs.map((doc) => docToRow<Row>(doc, idField))\n}\n\nfunction applyClientSearch<Row extends DataRow>(\n  rows: Row[],\n  resource: ResourceConfig<FirestoreResourceSource, Row>,\n  query: DataQuery,\n): DataQueryResult<Row> {\n  if (resource.search?.mode !== \"client\" || !query.search) {\n    return {\n      rows,\n      totalRows: rows.length,\n      page: query.page ?? 1,\n      pageSize: query.pageSize ?? rows.length,\n    }\n  }\n\n  return queryLocalRows(rows, query, resource.columns)\n}\n\nfunction getUpdateValues(input: DataActionInput) {\n  const values = input.values ?? {}\n\n  if (typeof values !== \"object\" || Array.isArray(values)) {\n    throw new Error(\"Firestore update action values must be an object.\")\n  }\n\n  return values\n}\n\nfunction requireFirebaseUser(source: FirestoreResourceSource, auth: FirebaseAuthLike | undefined) {\n  if (source.requireAuth === false) {\n    return\n  }\n\n  if (!auth?.currentUser?.uid) {\n    throw new Error(\"Firestore resource requires a signed-in Firebase Auth user.\")\n  }\n}\n\nexport function createFirebaseDriver({ db, auth }: FirebaseDriverConfig): DataSourceDriver<FirestoreResourceSource> {\n  return {\n    async list<Row extends DataRow>(\n      resource: ResourceConfig<FirestoreResourceSource, Row>,\n      query: DataQuery,\n      ctx: DataSourceContext,\n      runtime?: ResourceRuntime<Row, FirestoreResourceSource>,\n    ) {\n      if (runtime?.list) {\n        return runtime.list(resource, query, ctx)\n      }\n\n      requireFirebaseUser(resource.source, auth)\n\n      try {\n        const sdk = await loadFirestoreSdk()\n        const collectionPath = await resolveCollectionPath(resource.source, ctx)\n        const constraints: unknown[] = []\n\n        for (const [key, value] of Object.entries(query.filters ?? {})) {\n          appendFilterConstraint(sdk, constraints, resource, key, value)\n        }\n\n        appendSearchConstraints(sdk, constraints, resource, query)\n\n        if (query.sort) {\n          assertAllowedSort(resource, query.sort.key)\n          constraints.push(sdk.orderBy(query.sort.key, query.sort.direction))\n        }\n\n        if (query.cursor) {\n          constraints.push(sdk.startAfter(query.cursor))\n        }\n\n        if (query.pageSize) {\n          constraints.push(sdk.limit(query.pageSize))\n        }\n\n        const collectionRef = sdk.collection(db, collectionPath)\n        const firestoreQuery = constraints.length > 0 ? sdk.query(collectionRef, ...constraints) : collectionRef\n        const snapshot = await sdk.getDocs(firestoreQuery)\n        const rows = getDocsFromSnapshot<Row>(snapshot, resource.source.idField)\n        const result = applyClientSearch(rows, resource, query)\n\n        return {\n          ...result,\n          nextCursor: rows.length === query.pageSize ? String(getDataValue(rows[rows.length - 1], \"id\")) : undefined,\n        }\n      } catch (error) {\n        throw mapFirestoreError(error)\n      }\n    },\n\n    async get<Row extends DataRow>(\n      resource: ResourceConfig<FirestoreResourceSource, Row>,\n      rowId: string,\n      ctx: DataSourceContext,\n      runtime?: ResourceRuntime<Row, FirestoreResourceSource>,\n    ) {\n      if (runtime?.get) {\n        return runtime.get(resource, rowId, ctx)\n      }\n\n      requireFirebaseUser(resource.source, auth)\n\n      try {\n        const sdk = await loadFirestoreSdk()\n        const collectionPath = await resolveCollectionPath(resource.source, ctx)\n        const ref = sdk.doc(db, collectionPath, assertSafePathParam(\"rowId\", rowId))\n        const snapshot = await sdk.getDoc(ref)\n\n        if (!(snapshot as { exists?: () => boolean }).exists?.()) {\n          return null\n        }\n\n        return docToRow<Row>(snapshot, resource.source.idField)\n      } catch (error) {\n        throw mapFirestoreError(error)\n      }\n    },\n\n    async executeAction(\n      input: DataActionInput,\n      resource: ResourceConfig<FirestoreResourceSource>,\n      ctx: DataSourceContext,\n      runtime?: ResourceRuntime<DataRow, FirestoreResourceSource>,\n    ): Promise<DataActionResult> {\n      if (runtime?.executeAction) {\n        return runtime.executeAction(input, resource, ctx)\n      }\n\n      const action = resource.actions?.find((candidate) => candidate.id === input.actionId)\n\n      if (!action) {\n        throw new Error(`Action \"${input.actionId}\" is not allowed for resource \"${resource.id}\".`)\n      }\n\n      if (!input.rowId) {\n        throw new Error(\"Firestore update actions require a row id.\")\n      }\n\n      if (action.variant === \"destructive\") {\n        throw new Error(\"Destructive Firestore actions require a custom resource runtime handler.\")\n      }\n\n      requireFirebaseUser(resource.source, auth)\n\n      try {\n        const sdk = await loadFirestoreSdk()\n        const collectionPath = await resolveCollectionPath(resource.source, ctx)\n        const ref = sdk.doc(db, collectionPath, assertSafePathParam(\"rowId\", input.rowId))\n        await sdk.updateDoc(ref, getUpdateValues(input))\n\n        return {\n          ok: true,\n          message: `${action.label} completed.`,\n          invalidate: true,\n        }\n      } catch (error) {\n        throw mapFirestoreError(error)\n      }\n    },\n  }\n}\n",
      "type": "registry:lib",
      "target": "@lib/fable-ui/drivers/firebase/firebase-driver.ts"
    },
    {
      "path": "lib/fable-ui/drivers/firebase/index.ts",
      "content": "export * from \"./firebase-driver\"\nexport * from \"./firebase-driver.types\"\n",
      "type": "registry:lib",
      "target": "@lib/fable-ui/drivers/firebase/index.ts"
    }
  ],
  "type": "registry:lib"
}