From f34630de62d587d87bfa6852b89f1e4674168566 Mon Sep 17 00:00:00 2001 From: kovakmedya Date: Thu, 21 May 2026 20:57:59 +0300 Subject: [PATCH] fix: serialize Appwrite rows before sending to client components MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Next 16's server-to-client serializer rejects values whose prototype is not plain Object. node-appwrite returns row objects carrying internal helpers (toString etc.), so every crashed with 'Only plain objects, and a few built-ins, can be passed to Client Components from Server Components.' Added a tiny toPlain helper that JSON-roundtrips any value and applied it at the boundary of every query that returns rows consumed by 'use client' files: - connection-queries (enrich) - job-queries (inbound, outbound, approved labs) - job-file-queries (listJobFiles) - job-history-queries (listJobHistory) - prosthetic-queries (listProsthetics, listActiveProsthetics) - finance-queries (listFinanceEntries) - notification-helpers (listNotifications) - dashboard-queries (getDashboardData) - jobs/[jobId] page (direct getRow for the job prop on JobActionsPanel) Internal Maps inside queries stay live — only the data crossing the server/client boundary is normalised. --- src/app/(dashboard)/jobs/[jobId]/page.tsx | 3 ++- src/lib/appwrite/connection-queries.ts | 11 +++++++---- src/lib/appwrite/dashboard-queries.ts | 5 +++-- src/lib/appwrite/finance-queries.ts | 13 ++++++++----- src/lib/appwrite/job-file-queries.ts | 11 +++++++---- src/lib/appwrite/job-history-queries.ts | 3 ++- src/lib/appwrite/job-queries.ts | 13 ++++++++----- src/lib/appwrite/notification-helpers.ts | 3 ++- src/lib/appwrite/prosthetic-queries.ts | 5 +++-- src/lib/appwrite/serialize.ts | 15 +++++++++++++++ 10 files changed, 57 insertions(+), 25 deletions(-) create mode 100644 src/lib/appwrite/serialize.ts diff --git a/src/app/(dashboard)/jobs/[jobId]/page.tsx b/src/app/(dashboard)/jobs/[jobId]/page.tsx index 75a0004..5eab726 100644 --- a/src/app/(dashboard)/jobs/[jobId]/page.tsx +++ b/src/app/(dashboard)/jobs/[jobId]/page.tsx @@ -7,6 +7,7 @@ import { Button } from "@/components/ui/button"; import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; import { listJobFiles } from "@/lib/appwrite/job-file-queries"; import { listJobHistory } from "@/lib/appwrite/job-history-queries"; +import { toPlain } from "@/lib/appwrite/serialize"; import { JOB_STATUS_LABELS, JOB_STEP_LABELS, @@ -57,7 +58,7 @@ export default async function JobDetailPage({ let job: Job; try { const row = await tablesDB.getRow(DATABASE_ID, TABLES.jobs, jobId); - job = row as unknown as Job; + job = toPlain(row as unknown as Job); } catch { notFound(); } diff --git a/src/lib/appwrite/connection-queries.ts b/src/lib/appwrite/connection-queries.ts index 31ad8f5..91185f7 100644 --- a/src/lib/appwrite/connection-queries.ts +++ b/src/lib/appwrite/connection-queries.ts @@ -10,6 +10,7 @@ import { type TenantSettings, } from "./schema"; import { createAdminClient } from "./server"; +import { toPlain } from "./serialize"; export type CounterpartTenant = { tenantId: string; @@ -54,10 +55,12 @@ async function enrich( new Set(rows.map((r) => counterpartTenantId(r, selfTenantId))), ); const map = await fetchCounterparts(counterpartIds); - return rows.map((r) => ({ - ...r, - counterpart: map.get(counterpartTenantId(r, selfTenantId)) ?? null, - })); + return toPlain( + rows.map((r) => ({ + ...r, + counterpart: map.get(counterpartTenantId(r, selfTenantId)) ?? null, + })), + ); } async function listConnectionsByStatus( diff --git a/src/lib/appwrite/dashboard-queries.ts b/src/lib/appwrite/dashboard-queries.ts index 2f61a30..a203e3c 100644 --- a/src/lib/appwrite/dashboard-queries.ts +++ b/src/lib/appwrite/dashboard-queries.ts @@ -12,6 +12,7 @@ import { type TenantSettings, } from "./schema"; import { createAdminClient } from "./server"; +import { toPlain } from "./serialize"; export type DashboardJob = Job & { counterpartName: string | null; @@ -140,7 +141,7 @@ export async function getDashboardData( if (e.type === "payable") payablePending += e.amount; } - return { + return toPlain({ openJobsCount: openJobsRes.total, pendingActionCount: pendingActionRes.total, unreadCount: unreadRes.total, @@ -154,5 +155,5 @@ export async function getDashboardData( counterpartMap.get(isLab ? j.clinicTenantId : j.labTenantId) ?? null, })), recentNotifications: notifRes.rows as unknown as Notification[], - }; + }); } diff --git a/src/lib/appwrite/finance-queries.ts b/src/lib/appwrite/finance-queries.ts index 1df10cd..e8b980f 100644 --- a/src/lib/appwrite/finance-queries.ts +++ b/src/lib/appwrite/finance-queries.ts @@ -10,6 +10,7 @@ import { type TenantSettings, } from "./schema"; import { createAdminClient } from "./server"; +import { toPlain } from "./serialize"; export type FinanceCounterpart = { tenantId: string; @@ -40,7 +41,7 @@ export async function listFinanceEntries( new Set(rows.map((r) => r.counterpartTenantId).filter((v): v is string => Boolean(v))), ); if (counterpartIds.length === 0) { - return rows.map((r) => ({ ...r, counterpart: null })); + return toPlain(rows.map((r) => ({ ...r, counterpart: null }))); } const counterpartsRes = await tablesDB.listRows({ @@ -57,10 +58,12 @@ export async function listFinanceEntries( }); } - return rows.map((r) => ({ - ...r, - counterpart: r.counterpartTenantId ? map.get(r.counterpartTenantId) ?? null : null, - })); + return toPlain( + rows.map((r) => ({ + ...r, + counterpart: r.counterpartTenantId ? map.get(r.counterpartTenantId) ?? null : null, + })), + ); } export function summarizeFinance( diff --git a/src/lib/appwrite/job-file-queries.ts b/src/lib/appwrite/job-file-queries.ts index b43113e..5884000 100644 --- a/src/lib/appwrite/job-file-queries.ts +++ b/src/lib/appwrite/job-file-queries.ts @@ -4,6 +4,7 @@ import { Query } from "node-appwrite"; import { BUCKETS, DATABASE_ID, TABLES, type JobFile } from "./schema"; import { createAdminClient } from "./server"; +import { toPlain } from "./serialize"; import { getFileViewUrl } from "./storage"; export type JobFileWithUrl = JobFile & { @@ -22,8 +23,10 @@ export async function listJobFiles(jobId: string): Promise { ], }); const rows = result.rows as unknown as JobFile[]; - return rows.map((r) => ({ - ...r, - url: getFileViewUrl(BUCKETS.jobFiles, r.fileId), - })); + return toPlain( + rows.map((r) => ({ + ...r, + url: getFileViewUrl(BUCKETS.jobFiles, r.fileId), + })), + ); } diff --git a/src/lib/appwrite/job-history-queries.ts b/src/lib/appwrite/job-history-queries.ts index 0256cf7..0cd334c 100644 --- a/src/lib/appwrite/job-history-queries.ts +++ b/src/lib/appwrite/job-history-queries.ts @@ -4,6 +4,7 @@ import { Query } from "node-appwrite"; import { DATABASE_ID, TABLES, type JobStatusHistory } from "./schema"; import { createAdminClient } from "./server"; +import { toPlain } from "./serialize"; export async function listJobHistory(jobId: string): Promise { const { tablesDB } = createAdminClient(); @@ -16,5 +17,5 @@ export async function listJobHistory(jobId: string): Promise Query.limit(100), ], }); - return result.rows as unknown as JobStatusHistory[]; + return toPlain(result.rows as unknown as JobStatusHistory[]); } diff --git a/src/lib/appwrite/job-queries.ts b/src/lib/appwrite/job-queries.ts index cea378b..2dc8e13 100644 --- a/src/lib/appwrite/job-queries.ts +++ b/src/lib/appwrite/job-queries.ts @@ -10,6 +10,7 @@ import { type TenantSettings, } from "./schema"; import { createAdminClient } from "./server"; +import { toPlain } from "./serialize"; export type JobCounterpart = { tenantId: string; @@ -58,7 +59,7 @@ export async function listInboundJobs(labTenantId: string): Promise j.clinicTenantId)))); - return jobs.map((j) => enrichJob(j, j.clinicTenantId, map)); + return toPlain(jobs.map((j) => enrichJob(j, j.clinicTenantId, map))); } /** Outbound for a clinic tenant — jobs the clinic has sent. */ @@ -75,7 +76,7 @@ export async function listOutboundJobs(clinicTenantId: string): Promise j.labTenantId)))); - return jobs.map((j) => enrichJob(j, j.labTenantId, map)); + return toPlain(jobs.map((j) => enrichJob(j, j.labTenantId, map))); } /** For clinic's "Yeni İş Yayınla" form: list approved labs they can send to. */ @@ -96,7 +97,9 @@ export async function listApprovedLabsForClinic( (r) => r.labTenantId, ); const map = await fetchTenants(labIds); - return labIds - .map((id) => map.get(id)) - .filter((v): v is JobCounterpart => Boolean(v)); + return toPlain( + labIds + .map((id) => map.get(id)) + .filter((v): v is JobCounterpart => Boolean(v)), + ); } diff --git a/src/lib/appwrite/notification-helpers.ts b/src/lib/appwrite/notification-helpers.ts index e11ab4a..8c67e9a 100644 --- a/src/lib/appwrite/notification-helpers.ts +++ b/src/lib/appwrite/notification-helpers.ts @@ -4,6 +4,7 @@ import { ID, Permission, Query, Role } from "node-appwrite"; import { DATABASE_ID, TABLES, type Notification } from "./schema"; import { createAdminClient } from "./server"; +import { toPlain } from "./serialize"; type CreateNotificationInput = { tenantId: string; @@ -76,5 +77,5 @@ export async function listNotifications( Query.limit(limit), ], }); - return result.rows as unknown as Notification[]; + return toPlain(result.rows as unknown as Notification[]); } diff --git a/src/lib/appwrite/prosthetic-queries.ts b/src/lib/appwrite/prosthetic-queries.ts index bdbc6fc..8df77fd 100644 --- a/src/lib/appwrite/prosthetic-queries.ts +++ b/src/lib/appwrite/prosthetic-queries.ts @@ -4,6 +4,7 @@ import { Query } from "node-appwrite"; import { DATABASE_ID, TABLES, type Prosthetic } from "./schema"; import { createAdminClient } from "./server"; +import { toPlain } from "./serialize"; export async function listProsthetics(tenantId: string): Promise { const { tablesDB } = createAdminClient(); @@ -16,7 +17,7 @@ export async function listProsthetics(tenantId: string): Promise { Query.limit(200), ], }); - return result.rows as unknown as Prosthetic[]; + return toPlain(result.rows as unknown as Prosthetic[]); } export async function listActiveProsthetics(tenantId: string): Promise { @@ -31,5 +32,5 @@ export async function listActiveProsthetics(tenantId: string): Promise(value: T): T { + return JSON.parse(JSON.stringify(value)) as T; +}