Files
pn-new-crm/src/lib/validators/yachts.ts
Matt Ciaccio 1fb3aa3aeb fix(regressions): client-bundle ioredis + Drizzle ANY() array bindings
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) <noreply@anthropic.com>
2026-05-06 14:56:59 +02:00

53 lines
1.9 KiB
TypeScript

import { z } from 'zod';
import { baseListQuerySchema } from '@/lib/api/list-query';
export const ownerRefSchema = z.object({
type: z.enum(['client', 'company']),
id: z.string().min(1),
});
export const createYachtSchema = z.object({
name: z.string().min(1).max(200),
hullNumber: z.string().optional(),
registration: z.string().optional(),
flag: z.string().optional(),
yearBuilt: z.number().int().min(1800).max(2100).optional(),
builder: z.string().optional(),
model: z.string().optional(),
hullMaterial: z.string().optional(),
lengthFt: z.string().optional(),
widthFt: z.string().optional(),
draftFt: z.string().optional(),
lengthM: z.string().optional(),
widthM: z.string().optional(),
draftM: z.string().optional(),
owner: ownerRefSchema, // required; yacht must have an owner
status: z.enum(['active', 'retired', 'sold_away']).optional().default('active'),
notes: z.string().optional(),
tagIds: z.array(z.string()).optional().default([]),
});
export const updateYachtSchema = createYachtSchema.partial().omit({ owner: true });
// Owner changes go through /transfer, not PATCH.
export const transferOwnershipSchema = z.object({
newOwner: ownerRefSchema,
effectiveDate: z.coerce.date(),
transferReason: z
.enum(['sale', 'inheritance', 'gift', 'company_restructure', 'other'])
.optional(),
transferNotes: z.string().optional(),
});
export const listYachtsSchema = baseListQuerySchema.extend({
ownerType: z.enum(['client', 'company']).optional(),
ownerId: z.string().optional(),
status: z.enum(['active', 'retired', 'sold_away']).optional(),
search: z.string().optional(),
});
export type CreateYachtInput = z.infer<typeof createYachtSchema>;
export type UpdateYachtInput = z.infer<typeof updateYachtSchema>;
export type TransferOwnershipInput = z.infer<typeof transferOwnershipSchema>;
export type ListYachtsInput = z.infer<typeof listYachtsSchema>;