Files
pn-new-crm/src/lib/validators/documents.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

107 lines
3.6 KiB
TypeScript

import { z } from 'zod';
import { baseListQuerySchema } from '@/lib/api/list-query';
import { DOCUMENT_TYPES, DOCUMENT_STATUSES } from '@/lib/constants';
export const createDocumentSchema = z.object({
interestId: z.string().optional(),
clientId: z.string().optional(),
documentType: z.enum(DOCUMENT_TYPES),
title: z.string().min(1).max(200),
notes: z.string().optional(),
});
export const updateDocumentSchema = z.object({
title: z.string().min(1).max(200).optional(),
notes: z.string().optional(),
status: z.enum(DOCUMENT_STATUSES).optional(),
});
const wizardSignerSchema = z.object({
signerName: z.string().min(1),
signerEmail: z.string().email(),
signerRole: z.enum(['client', 'sales', 'approver', 'developer', 'other']),
signingOrder: z.number().int().min(1),
});
export const createDocumentWizardSchema = z
.object({
source: z.enum(['template', 'upload']).default('template'),
templateId: z.string().optional(),
uploadedFileId: z.string().optional(),
documentType: z.enum(DOCUMENT_TYPES),
title: z.string().min(1).max(200),
notes: z.string().optional(),
interestId: z.string().optional(),
reservationId: z.string().optional(),
clientId: z.string().optional(),
companyId: z.string().optional(),
yachtId: z.string().optional(),
signers: z.array(wizardSignerSchema).optional(),
signingMode: z.enum(['sequential', 'parallel']).default('sequential'),
pathway: z.enum(['documenso-template', 'inapp', 'upload']).default('documenso-template'),
watchers: z.array(z.string()).default([]),
reminderCadenceOverride: z.number().int().min(1).max(365).nullable().optional(),
remindersDisabled: z.boolean().default(false),
autoPlaceFields: z.boolean().default(true),
sendImmediately: z.boolean().default(true),
})
.refine(
(d) =>
[d.interestId, d.reservationId, d.clientId, d.companyId, d.yachtId].filter(Boolean).length ===
1,
{ message: 'Exactly one subject (interest/reservation/client/company/yacht) is required' },
)
.refine((d) => d.source !== 'template' || Boolean(d.templateId), {
path: ['templateId'],
message: 'templateId is required when source=template',
})
.refine((d) => d.source !== 'upload' || Boolean(d.uploadedFileId), {
path: ['uploadedFileId'],
message: 'uploadedFileId is required when source=upload',
});
export type CreateDocumentWizardInput = z.infer<typeof createDocumentWizardSchema>;
export const documentsHubTabs = [
'all',
'eoi_queue',
'awaiting_them',
'awaiting_me',
'completed',
'expired',
] as const;
export type DocumentsHubTab = (typeof documentsHubTabs)[number];
export const listDocumentsSchema = baseListQuerySchema.extend({
interestId: z.string().optional(),
clientId: z.string().optional(),
documentType: z.string().optional(),
status: z.string().optional(),
/** Hub tab filter - applies tab-specific status / signer-membership constraints. */
tab: z.enum(documentsHubTabs).optional(),
/** Restrict to docs being watched by this user id. */
watcherUserId: z.string().optional(),
/** When true, only docs intended for signing (default true on hub). */
signatureOnly: z
.enum(['true', 'false'])
.optional()
.transform((v) => (v === undefined ? undefined : v === 'true')),
sentSince: z.string().datetime().optional(),
sentUntil: z.string().datetime().optional(),
});
export const uploadSignedSchema = z.object({
documentId: z.string().min(1),
});
export type CreateDocumentInput = z.infer<typeof createDocumentSchema>;
export type UpdateDocumentInput = z.infer<typeof updateDocumentSchema>;
export type ListDocumentsInput = z.infer<typeof listDocumentsSchema>;