From 1fb3aa3aeb553b7bda5df87acabf6941ae33e38a Mon Sep 17 00:00:00 2001 From: Matt Ciaccio Date: Wed, 6 May 2026 14:56:59 +0200 Subject: [PATCH] fix(regressions): client-bundle ioredis + Drizzle ANY() array bindings MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two regressions from yesterday's audit-tier-0 work that broke the dev server and every clients API call. - baseListQuerySchema lived in route-helpers.ts, which was made server-only by the rate-limit import. Every validator imported it, pulling ioredis (and dns/net/tls/fs/node:async_hooks) into the client bundle — every form/detail page returned 500 in dev. Extracted the schema to src/lib/api/list-query.ts and updated all 14 validators. - clients.service.listClients and email-compose used raw SQL ANY(\${jsArray}) which Drizzle binds as JSON — Postgres rejects with 42809 "op ANY/ALL (array) requires array on right side". Switched to the inArray helper. GET /api/v1/clients now returns 200 again. Affects every form/detail page that imports a validator. Co-Authored-By: Claude Opus 4.7 (1M context) --- src/lib/api/list-query.ts | 15 +++++++++++++++ src/lib/api/route-helpers.ts | 17 ----------------- src/lib/services/clients.service.ts | 2 +- src/lib/services/email-compose.service.ts | 4 ++-- src/lib/validators/berths.ts | 2 +- src/lib/validators/clients.ts | 2 +- src/lib/validators/companies.ts | 2 +- src/lib/validators/document-templates.ts | 2 +- src/lib/validators/documents.ts | 2 +- src/lib/validators/expenses.ts | 2 +- src/lib/validators/files.ts | 2 +- src/lib/validators/interests.ts | 2 +- src/lib/validators/invoices.ts | 2 +- src/lib/validators/reminders.ts | 2 +- src/lib/validators/reservations.ts | 2 +- src/lib/validators/residential.ts | 2 +- src/lib/validators/webhooks.ts | 2 +- src/lib/validators/yachts.ts | 2 +- 18 files changed, 32 insertions(+), 34 deletions(-) create mode 100644 src/lib/api/list-query.ts diff --git a/src/lib/api/list-query.ts b/src/lib/api/list-query.ts new file mode 100644 index 0000000..08cb071 --- /dev/null +++ b/src/lib/api/list-query.ts @@ -0,0 +1,15 @@ +import { z } from 'zod'; + +export const baseListQuerySchema = z.object({ + page: z.coerce.number().int().min(1).default(1), + limit: z.coerce.number().int().min(1).max(100).default(25), + sort: z.string().optional(), + order: z.enum(['asc', 'desc']).default('desc'), + search: z.string().optional(), + includeArchived: z + .enum(['true', 'false']) + .transform((v) => v === 'true') + .default('false'), +}); + +export type BaseListQuery = z.infer; diff --git a/src/lib/api/route-helpers.ts b/src/lib/api/route-helpers.ts index 31c34d3..e63c5d6 100644 --- a/src/lib/api/route-helpers.ts +++ b/src/lib/api/route-helpers.ts @@ -8,23 +8,6 @@ import { type RateLimiterName, } from '@/lib/rate-limit'; -/** - * Base list query schema shared by all paginated list endpoints. - */ -export const baseListQuerySchema = z.object({ - page: z.coerce.number().int().min(1).default(1), - limit: z.coerce.number().int().min(1).max(100).default(25), - sort: z.string().optional(), - order: z.enum(['asc', 'desc']).default('desc'), - search: z.string().optional(), - includeArchived: z - .enum(['true', 'false']) - .transform((v) => v === 'true') - .default('false'), -}); - -export type BaseListQuery = z.infer; - /** * Parses URL search params against a Zod schema. * Throws a ZodError on validation failure (caught by `errorResponse`). diff --git a/src/lib/services/clients.service.ts b/src/lib/services/clients.service.ts index caa1bd5..f166406 100644 --- a/src/lib/services/clients.service.ts +++ b/src/lib/services/clients.service.ts @@ -158,7 +158,7 @@ export async function listClients(portId: string, query: ListClientsInput) { is_primary AS "isPrimary", created_at AS "createdAt" FROM client_contacts - WHERE client_id = ANY(${ids}) + WHERE ${inArray(clientContacts.clientId, ids)} AND channel IN ('email', 'phone') ORDER BY client_id, channel, is_primary DESC, created_at DESC `), diff --git a/src/lib/services/email-compose.service.ts b/src/lib/services/email-compose.service.ts index 7166d5c..4cfc04b 100644 --- a/src/lib/services/email-compose.service.ts +++ b/src/lib/services/email-compose.service.ts @@ -1,5 +1,5 @@ import nodemailer from 'nodemailer'; -import { and, eq, sql } from 'drizzle-orm'; +import { and, eq, inArray, sql } from 'drizzle-orm'; import { db } from '@/lib/db'; import { emailAccounts, emailMessages, emailThreads } from '@/lib/db/schema/email'; @@ -295,7 +295,7 @@ async function sendSystem( const matchingDocs = await db .select({ id: documents.id, signedFileId: documents.signedFileId }) .from(documents) - .where(and(eq(documents.portId, portId), sql`${documents.signedFileId} = ANY(${fileIds})`)); + .where(and(eq(documents.portId, portId), inArray(documents.signedFileId, fileIds))); for (const doc of matchingDocs) { await db.insert(documentEvents).values({ diff --git a/src/lib/validators/berths.ts b/src/lib/validators/berths.ts index f5b7a67..9b3f198 100644 --- a/src/lib/validators/berths.ts +++ b/src/lib/validators/berths.ts @@ -1,6 +1,6 @@ import { z } from 'zod'; import { BERTH_STATUSES } from '@/lib/constants'; -import { baseListQuerySchema } from '@/lib/api/route-helpers'; +import { baseListQuerySchema } from '@/lib/api/list-query'; // ─── Create Berth ──────────────────────────────────────────────────────────── diff --git a/src/lib/validators/clients.ts b/src/lib/validators/clients.ts index 8efbcb6..4588a9d 100644 --- a/src/lib/validators/clients.ts +++ b/src/lib/validators/clients.ts @@ -1,6 +1,6 @@ import { z } from 'zod'; -import { baseListQuerySchema } from '@/lib/api/route-helpers'; +import { baseListQuerySchema } from '@/lib/api/list-query'; import { optionalCountryIsoSchema, optionalIanaTimezoneSchema, diff --git a/src/lib/validators/companies.ts b/src/lib/validators/companies.ts index 6b59372..55319cd 100644 --- a/src/lib/validators/companies.ts +++ b/src/lib/validators/companies.ts @@ -1,5 +1,5 @@ import { z } from 'zod'; -import { baseListQuerySchema } from '@/lib/api/route-helpers'; +import { baseListQuerySchema } from '@/lib/api/list-query'; import { optionalCountryIsoSchema, optionalSubdivisionIsoSchema } from '@/lib/validators/i18n'; export const createCompanySchema = z.object({ diff --git a/src/lib/validators/document-templates.ts b/src/lib/validators/document-templates.ts index be6d084..2c9558e 100644 --- a/src/lib/validators/document-templates.ts +++ b/src/lib/validators/document-templates.ts @@ -1,6 +1,6 @@ import { z } from 'zod'; -import { baseListQuerySchema } from '@/lib/api/route-helpers'; +import { baseListQuerySchema } from '@/lib/api/list-query'; import { VALID_MERGE_TOKENS } from '@/lib/templates/merge-fields'; const mergeFieldsSchema = z diff --git a/src/lib/validators/documents.ts b/src/lib/validators/documents.ts index 19a13f3..39af876 100644 --- a/src/lib/validators/documents.ts +++ b/src/lib/validators/documents.ts @@ -1,6 +1,6 @@ import { z } from 'zod'; -import { baseListQuerySchema } from '@/lib/api/route-helpers'; +import { baseListQuerySchema } from '@/lib/api/list-query'; import { DOCUMENT_TYPES, DOCUMENT_STATUSES } from '@/lib/constants'; export const createDocumentSchema = z.object({ diff --git a/src/lib/validators/expenses.ts b/src/lib/validators/expenses.ts index eb4cddb..ac145ad 100644 --- a/src/lib/validators/expenses.ts +++ b/src/lib/validators/expenses.ts @@ -1,5 +1,5 @@ import { z } from 'zod'; -import { baseListQuerySchema } from '@/lib/api/route-helpers'; +import { baseListQuerySchema } from '@/lib/api/list-query'; import { EXPENSE_CATEGORIES, PAYMENT_METHODS } from '@/lib/constants'; /** diff --git a/src/lib/validators/files.ts b/src/lib/validators/files.ts index 2d86ca7..55edf43 100644 --- a/src/lib/validators/files.ts +++ b/src/lib/validators/files.ts @@ -1,6 +1,6 @@ import { z } from 'zod'; -import { baseListQuerySchema } from '@/lib/api/route-helpers'; +import { baseListQuerySchema } from '@/lib/api/list-query'; export const uploadFileSchema = z.object({ filename: z.string().min(1).max(255), diff --git a/src/lib/validators/interests.ts b/src/lib/validators/interests.ts index e23f400..9c00189 100644 --- a/src/lib/validators/interests.ts +++ b/src/lib/validators/interests.ts @@ -1,6 +1,6 @@ import { z } from 'zod'; -import { baseListQuerySchema } from '@/lib/api/route-helpers'; +import { baseListQuerySchema } from '@/lib/api/list-query'; import { PIPELINE_STAGES, LEAD_CATEGORIES } from '@/lib/constants'; import { optionalCountryIsoSchema, diff --git a/src/lib/validators/invoices.ts b/src/lib/validators/invoices.ts index 92d2908..b1a774c 100644 --- a/src/lib/validators/invoices.ts +++ b/src/lib/validators/invoices.ts @@ -1,5 +1,5 @@ import { z } from 'zod'; -import { baseListQuerySchema } from '@/lib/api/route-helpers'; +import { baseListQuerySchema } from '@/lib/api/list-query'; export const INVOICE_KINDS = ['general', 'deposit'] as const; export type InvoiceKind = (typeof INVOICE_KINDS)[number]; diff --git a/src/lib/validators/reminders.ts b/src/lib/validators/reminders.ts index 545e2ec..7695076 100644 --- a/src/lib/validators/reminders.ts +++ b/src/lib/validators/reminders.ts @@ -1,5 +1,5 @@ import { z } from 'zod'; -import { baseListQuerySchema } from '@/lib/api/route-helpers'; +import { baseListQuerySchema } from '@/lib/api/list-query'; export const createReminderSchema = z.object({ title: z.string().min(1).max(300), diff --git a/src/lib/validators/reservations.ts b/src/lib/validators/reservations.ts index 9e5b188..fa9ef61 100644 --- a/src/lib/validators/reservations.ts +++ b/src/lib/validators/reservations.ts @@ -1,5 +1,5 @@ import { z } from 'zod'; -import { baseListQuerySchema } from '@/lib/api/route-helpers'; +import { baseListQuerySchema } from '@/lib/api/list-query'; export const RESERVATION_STATUSES = ['pending', 'active', 'ended', 'cancelled'] as const; export const TENURE_TYPES = ['permanent', 'fixed_term', 'seasonal'] as const; diff --git a/src/lib/validators/residential.ts b/src/lib/validators/residential.ts index 5f6971d..b24ba16 100644 --- a/src/lib/validators/residential.ts +++ b/src/lib/validators/residential.ts @@ -1,6 +1,6 @@ import { z } from 'zod'; -import { baseListQuerySchema } from '@/lib/api/route-helpers'; +import { baseListQuerySchema } from '@/lib/api/list-query'; import { optionalCountryIsoSchema, optionalIanaTimezoneSchema, diff --git a/src/lib/validators/webhooks.ts b/src/lib/validators/webhooks.ts index 24503f3..3e23bc8 100644 --- a/src/lib/validators/webhooks.ts +++ b/src/lib/validators/webhooks.ts @@ -1,6 +1,6 @@ import { z } from 'zod'; -import { baseListQuerySchema } from '@/lib/api/route-helpers'; +import { baseListQuerySchema } from '@/lib/api/list-query'; import { WEBHOOK_EVENTS } from '@/lib/services/webhook-event-map'; // ─── SSRF guards ────────────────────────────────────────────────────────────── diff --git a/src/lib/validators/yachts.ts b/src/lib/validators/yachts.ts index cb4d1a0..03825e7 100644 --- a/src/lib/validators/yachts.ts +++ b/src/lib/validators/yachts.ts @@ -1,5 +1,5 @@ import { z } from 'zod'; -import { baseListQuerySchema } from '@/lib/api/route-helpers'; +import { baseListQuerySchema } from '@/lib/api/list-query'; export const ownerRefSchema = z.object({ type: z.enum(['client', 'company']),