Skip to content

Commit 617a9ea

Browse files
committed
remove validateJsonBody
1 parent d8434a1 commit 617a9ea

64 files changed

Lines changed: 348 additions & 871 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.cursor/rules/sim-architecture.mdc

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,7 @@ Boundary HTTP request and response shapes for all routes under `apps/sim/app/api
6161

6262
- Each contract is built with `defineRouteContract({ method, path, params?, query?, body?, headers?, response: { mode: 'json', schema } })` and exports both schemas and named TypeScript type aliases (e.g., `export type CreateFolderBody = z.input<typeof createFolderBodySchema>`).
6363
- Shared identifier schemas live in `apps/sim/lib/api/contracts/primitives.ts`.
64-
- Routes validate via canonical helpers in `apps/sim/lib/api/server/validation.ts` (`parseRequest`, `validateJsonBody`, `validateSchema`, `validationErrorResponse`, `getValidationErrorMessage`, `isZodError`). Routes never `import { z } from 'zod'` and never use `instanceof z.ZodError`.
64+
- Routes validate via canonical helpers in `apps/sim/lib/api/server/validation.ts` (`parseRequest`, `validateSchema`, `validationErrorResponse`, `getValidationErrorMessage`, `isZodError`). Routes never `import { z } from 'zod'` and never use `instanceof z.ZodError`.
6565
- Clients call `requestJson(contract, ...)` from `apps/sim/lib/api/client/request.ts`; hooks import named type aliases from contracts, never `z.input/z.output`.
6666
- Routes under `apps/sim/app/api/v1/**` use `apps/sim/app/api/v1/middleware.ts` for shared auth, rate-limit, and workspace access. Compose contract validation inside that middleware.
6767
- `bun run check:api-validation` enforces this policy and must pass on PRs.

AGENTS.md

Lines changed: 0 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -156,7 +156,6 @@ const provider = config as unknown as LegacyProvider
156156
Routes never `import { z } from 'zod'` and never define route-local boundary schemas. They consume the contract from `@/lib/api/contracts/**` and validate with canonical helpers from `@/lib/api/server`:
157157

158158
- `parseRequest(contract, request, context)` — fully contract-bound routes; parses params, query, body, and headers in one call
159-
- `validateJsonBody(request, schema)` — when the body schema comes from a contract but you need to assemble query/headers manually
160159
- `validateSchema(schema, data)` — for ad-hoc validation against a contract schema or primitive
161160
- `validationErrorResponse(error)` and `getValidationErrorMessage(error, fallback)` — produce 400 responses from a `ZodError`
162161
- `validationErrorResponseFromError(error)` — when handling unknown caught errors that may or may not be a `ZodError`
@@ -185,30 +184,6 @@ export const POST = withRouteHandler(async (request: NextRequest) => {
185184
})
186185
```
187186

188-
### Partial validation (`validateJsonBody`)
189-
190-
```typescript
191-
import type { NextRequest } from 'next/server'
192-
import { NextResponse } from 'next/server'
193-
import { updateFolderBodySchema } from '@/lib/api/contracts/folders'
194-
import { isZodError, validateJsonBody, validationErrorResponse } from '@/lib/api/server'
195-
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
196-
197-
export const PATCH = withRouteHandler(async (
198-
request: NextRequest,
199-
{ params }: { params: Promise<{ id: string }> }
200-
) => {
201-
const { id } = await params
202-
try {
203-
const body = await validateJsonBody(request, updateFolderBodySchema)
204-
return NextResponse.json({ id, ...body })
205-
} catch (error) {
206-
if (isZodError(error)) return validationErrorResponse(error)
207-
throw error
208-
}
209-
})
210-
```
211-
212187
Routes under `apps/sim/app/api/v1/**` use the shared middleware in `apps/sim/app/api/v1/middleware.ts` for auth, rate-limit, and workspace access. Compose contract validation inside that middleware — never reimplement auth/rate-limit per-route.
213188

214189
## Hooks

CLAUDE.md

Lines changed: 0 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -137,7 +137,6 @@ Every API route handler must be wrapped with `withRouteHandler`. This sets up `A
137137
Routes never `import { z } from 'zod'` and never define route-local boundary schemas. They consume the contract from `@/lib/api/contracts/**` and validate with canonical helpers from `@/lib/api/server`:
138138

139139
- `parseRequest(contract, request, context)` — fully contract-bound routes; parses params, query, body, and headers in one call
140-
- `validateJsonBody(request, schema)` — when the body schema comes from a contract but you need to assemble query/headers manually
141140
- `validateSchema(schema, data)` — for ad-hoc validation against a contract schema or primitive
142141
- `validationErrorResponse(error)` and `getValidationErrorMessage(error, fallback)` — produce 400 responses from a `ZodError`
143142
- `validationErrorResponseFromError(error)` — when handling unknown caught errors that may or may not be a `ZodError`
@@ -166,30 +165,6 @@ export const POST = withRouteHandler(async (request: NextRequest) => {
166165
})
167166
```
168167

169-
### Partial validation (`validateJsonBody`)
170-
171-
```typescript
172-
import type { NextRequest } from 'next/server'
173-
import { NextResponse } from 'next/server'
174-
import { updateFolderBodySchema } from '@/lib/api/contracts/folders'
175-
import { isZodError, validateJsonBody, validationErrorResponse } from '@/lib/api/server'
176-
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
177-
178-
export const PATCH = withRouteHandler(async (
179-
request: NextRequest,
180-
{ params }: { params: Promise<{ id: string }> }
181-
) => {
182-
const { id } = await params
183-
try {
184-
const body = await validateJsonBody(request, updateFolderBodySchema)
185-
return NextResponse.json({ id, ...body })
186-
} catch (error) {
187-
if (isZodError(error)) return validationErrorResponse(error)
188-
throw error
189-
}
190-
})
191-
```
192-
193168
### Composing with other middleware
194169

195170
```typescript

apps/sim/AGENTS.md

Lines changed: 0 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -107,7 +107,6 @@ const provider = config as unknown as LegacyProvider
107107
Routes never `import { z } from 'zod'` and never define route-local boundary schemas. They consume the contract from `@/lib/api/contracts/**` and validate with canonical helpers from `@/lib/api/server`:
108108

109109
- `parseRequest(contract, request, context)` — fully contract-bound routes; parses params, query, body, and headers in one call.
110-
- `validateJsonBody(request, schema)` — when the body schema comes from a contract but you need to assemble query/headers manually.
111110
- `validateSchema(schema, data)` — for ad-hoc validation against a contract schema or primitive.
112111
- `validationErrorResponse(error)` and `getValidationErrorMessage(error, fallback)` — produce 400 responses from a `ZodError`.
113112
- `validationErrorResponseFromError(error)` — when handling unknown caught errors that may or may not be a `ZodError`.
@@ -136,30 +135,6 @@ export const POST = withRouteHandler(async (request: NextRequest) => {
136135
})
137136
```
138137

139-
### Partial validation (`validateJsonBody`)
140-
141-
```typescript
142-
import type { NextRequest } from 'next/server'
143-
import { NextResponse } from 'next/server'
144-
import { updateFolderBodySchema } from '@/lib/api/contracts/folders'
145-
import { isZodError, validateJsonBody, validationErrorResponse } from '@/lib/api/server'
146-
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
147-
148-
export const PATCH = withRouteHandler(async (
149-
request: NextRequest,
150-
{ params }: { params: Promise<{ id: string }> }
151-
) => {
152-
const { id } = await params
153-
try {
154-
const body = await validateJsonBody(request, updateFolderBodySchema)
155-
return NextResponse.json({ id, ...body })
156-
} catch (error) {
157-
if (isZodError(error)) return validationErrorResponse(error)
158-
throw error
159-
}
160-
})
161-
```
162-
163138
Routes under `apps/sim/app/api/v1/**` use the shared middleware in `apps/sim/app/api/v1/middleware.ts` for auth, rate-limit, and workspace access. Compose contract validation inside that middleware — never reimplement auth/rate-limit per-route.
164139

165140
## React Query Client Boundary

apps/sim/app/api/a2a/agents/[agentId]/route.ts

Lines changed: 9 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,10 @@ import { generateAgentCard, generateSkillsFromWorkflow } from '@/lib/a2a/agent-c
77
import type { AgentCapabilities, AgentSkill } from '@/lib/a2a/types'
88
import {
99
a2aAgentParamsSchema,
10-
publishA2AAgentBodySchema,
11-
updateA2AAgentBodySchema,
10+
publishA2AAgentContract,
11+
updateA2AAgentContract,
1212
} from '@/lib/api/contracts/a2a-agents'
13-
import { getValidationErrorMessage, validateJsonBody } from '@/lib/api/server'
13+
import { parseRequest } from '@/lib/api/server'
1414
import { checkSessionOrInternalAuth } from '@/lib/auth/hybrid'
1515
import { getRedisClient } from '@/lib/core/config/redis'
1616
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
@@ -117,18 +117,9 @@ export const PUT = withRouteHandler(
117117
return NextResponse.json({ error: 'Forbidden' }, { status: 403 })
118118
}
119119

120-
const parseResult = await validateJsonBody(request, updateA2AAgentBodySchema)
121-
if (!parseResult.success) {
122-
return NextResponse.json(
123-
{
124-
error: parseResult.error
125-
? getValidationErrorMessage(parseResult.error)
126-
: 'Invalid request body',
127-
},
128-
{ status: 400 }
129-
)
130-
}
131-
const body = parseResult.data
120+
const parsed = await parseRequest(updateA2AAgentContract, request, { params })
121+
if (!parsed.success) return parsed.response
122+
const body = parsed.data.body
132123

133124
let skills = body.skills ?? existingAgent.skills
134125
if (body.skillTags !== undefined) {
@@ -247,18 +238,9 @@ export const POST = withRouteHandler(
247238
return NextResponse.json({ error: 'Forbidden' }, { status: 403 })
248239
}
249240

250-
const actionResult = await validateJsonBody(request, publishA2AAgentBodySchema)
251-
if (!actionResult.success) {
252-
return NextResponse.json(
253-
{
254-
error: actionResult.error
255-
? getValidationErrorMessage(actionResult.error)
256-
: 'Invalid action',
257-
},
258-
{ status: 400 }
259-
)
260-
}
261-
const { action } = actionResult.data
241+
const parsed = await parseRequest(publishA2AAgentContract, request, { params })
242+
if (!parsed.success) return parsed.response
243+
const { action } = parsed.data.body
262244

263245
if (action === 'publish') {
264246
const [wf] = await db

apps/sim/app/api/a2a/agents/route.ts

Lines changed: 5 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,8 @@ import { type NextRequest, NextResponse } from 'next/server'
1313
import { generateSkillsFromWorkflow } from '@/lib/a2a/agent-card'
1414
import { A2A_DEFAULT_CAPABILITIES } from '@/lib/a2a/constants'
1515
import { sanitizeAgentName } from '@/lib/a2a/utils'
16-
import { createA2AAgentBodySchema, listA2AAgentsQuerySchema } from '@/lib/api/contracts/a2a-agents'
17-
import { getValidationErrorMessage, validateJsonBody } from '@/lib/api/server'
16+
import { createA2AAgentContract, listA2AAgentsQuerySchema } from '@/lib/api/contracts/a2a-agents'
17+
import { getValidationErrorMessage, parseRequest } from '@/lib/api/server'
1818
import { checkSessionOrInternalAuth } from '@/lib/auth/hybrid'
1919
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
2020
import { captureServerEvent } from '@/lib/posthog/server'
@@ -104,20 +104,11 @@ export const POST = withRouteHandler(async (request: NextRequest) => {
104104
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
105105
}
106106

107-
const parseResult = await validateJsonBody(request, createA2AAgentBodySchema)
108-
if (!parseResult.success) {
109-
return NextResponse.json(
110-
{
111-
error: parseResult.error
112-
? getValidationErrorMessage(parseResult.error)
113-
: 'Invalid request body',
114-
},
115-
{ status: 400 }
116-
)
117-
}
107+
const parsed = await parseRequest(createA2AAgentContract, request, {})
108+
if (!parsed.success) return parsed.response
118109

119110
const { workspaceId, workflowId, name, description, capabilities, authentication, skillTags } =
120-
parseResult.data
111+
parsed.data.body
121112

122113
const workspaceAccess = await checkWorkspaceAccess(workspaceId, auth.userId)
123114
if (!workspaceAccess.exists) {

apps/sim/app/api/credentials/[id]/members/route.ts

Lines changed: 5 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,8 @@ import { createLogger } from '@sim/logger'
44
import { generateId } from '@sim/utils/id'
55
import { and, eq } from 'drizzle-orm'
66
import { type NextRequest, NextResponse } from 'next/server'
7-
import { addCredentialMemberBodySchema } from '@/lib/api/contracts/credentials'
8-
import { getValidationErrorMessage, validateJsonBody } from '@/lib/api/server'
7+
import { upsertWorkspaceCredentialMemberContract } from '@/lib/api/contracts/credentials'
8+
import { parseRequest } from '@/lib/api/server'
99
import { getSession } from '@/lib/auth'
1010
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
1111
import { getUserEntityPermissions } from '@/lib/workspaces/permissions/utils'
@@ -105,19 +105,10 @@ export const POST = withRouteHandler(async (request: NextRequest, context: Route
105105
return NextResponse.json({ error: 'Admin access required' }, { status: 403 })
106106
}
107107

108-
const parsed = await validateJsonBody(request, addCredentialMemberBodySchema)
109-
if (!parsed.success) {
110-
return NextResponse.json(
111-
{
112-
error: parsed.error
113-
? getValidationErrorMessage(parsed.error, 'Invalid request body')
114-
: 'Invalid request body',
115-
},
116-
{ status: 400 }
117-
)
118-
}
108+
const parsed = await parseRequest(upsertWorkspaceCredentialMemberContract, request, context)
109+
if (!parsed.success) return parsed.response
119110

120-
const { userId, role } = parsed.data
111+
const { userId, role } = parsed.data.body
121112
const now = new Date()
122113

123114
const [existing] = await db

apps/sim/app/api/credentials/draft/route.ts

Lines changed: 7 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,9 @@ import { credential, credentialMember, pendingCredentialDraft } from '@sim/db/sc
33
import { createLogger } from '@sim/logger'
44
import { generateId } from '@sim/utils/id'
55
import { and, eq, lt } from 'drizzle-orm'
6-
import { NextResponse } from 'next/server'
7-
import { createCredentialDraftBodySchema } from '@/lib/api/contracts/credentials'
8-
import { getValidationErrorMessage, validateJsonBody } from '@/lib/api/server'
6+
import { type NextRequest, NextResponse } from 'next/server'
7+
import { createCredentialDraftContract } from '@/lib/api/contracts/credentials'
8+
import { parseRequest } from '@/lib/api/server'
99
import { getSession } from '@/lib/auth'
1010
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
1111
import { checkWorkspaceAccess } from '@/lib/workspaces/permissions/utils'
@@ -14,26 +14,17 @@ const logger = createLogger('CredentialDraftAPI')
1414

1515
const DRAFT_TTL_MS = 15 * 60 * 1000
1616

17-
export const POST = withRouteHandler(async (request: Request) => {
17+
export const POST = withRouteHandler(async (request: NextRequest) => {
1818
try {
1919
const session = await getSession()
2020
if (!session?.user?.id) {
2121
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
2222
}
2323

24-
const parsed = await validateJsonBody(request, createCredentialDraftBodySchema)
25-
if (!parsed.success) {
26-
return NextResponse.json(
27-
{
28-
error: parsed.error
29-
? getValidationErrorMessage(parsed.error, 'Invalid request body')
30-
: 'Invalid request body',
31-
},
32-
{ status: 400 }
33-
)
34-
}
24+
const parsed = await parseRequest(createCredentialDraftContract, request, {})
25+
if (!parsed.success) return parsed.response
3526

36-
const { workspaceId, providerId, displayName, description, credentialId } = parsed.data
27+
const { workspaceId, providerId, displayName, description, credentialId } = parsed.data.body
3728
const userId = session.user.id
3829

3930
const workspaceAccess = await checkWorkspaceAccess(workspaceId, userId)

apps/sim/app/api/mothership/chats/read/route.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,8 @@ import { copilotChats } from '@sim/db/schema'
33
import { createLogger } from '@sim/logger'
44
import { and, eq, sql } from 'drizzle-orm'
55
import { type NextRequest, NextResponse } from 'next/server'
6-
import { markMothershipChatReadBodySchema } from '@/lib/api/contracts/mothership-tasks'
7-
import { validateJsonBody } from '@/lib/api/server'
6+
import { markMothershipChatReadContract } from '@/lib/api/contracts/mothership-tasks'
7+
import { parseRequest } from '@/lib/api/server'
88
import {
99
authenticateCopilotRequestSessionOnly,
1010
createInternalServerErrorResponse,
@@ -21,9 +21,9 @@ export const POST = withRouteHandler(async (request: NextRequest) => {
2121
return createUnauthorizedResponse()
2222
}
2323

24-
const validation = await validateJsonBody(request, markMothershipChatReadBodySchema)
25-
if (!validation.success) return validation.response
26-
const { chatId } = validation.data
24+
const parsed = await parseRequest(markMothershipChatReadContract, request, {})
25+
if (!parsed.success) return parsed.response
26+
const { chatId } = parsed.data.body
2727

2828
await db
2929
.update(copilotChats)

apps/sim/app/api/telemetry/route.ts

Lines changed: 5 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { createLogger } from '@sim/logger'
22
import { type NextRequest, NextResponse } from 'next/server'
3-
import { telemetryEventSchema } from '@/lib/api/contracts/telemetry'
4-
import { validateJsonBody } from '@/lib/api/server'
3+
import { telemetryContract } from '@/lib/api/contracts/telemetry'
4+
import { parseRequest } from '@/lib/api/server'
55
import { env } from '@/lib/core/config/env'
66
import { isProd } from '@/lib/core/config/feature-flags'
77
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
@@ -149,18 +149,10 @@ async function forwardToCollector(data: Record<string, unknown>): Promise<boolea
149149
*/
150150
export const POST = withRouteHandler(async (req: NextRequest) => {
151151
try {
152-
const validation = await validateJsonBody(req, telemetryEventSchema)
153-
if (!validation.success) {
154-
if (!validation.error) {
155-
return NextResponse.json({ error: 'Invalid JSON in request body' }, { status: 400 })
156-
}
157-
return NextResponse.json(
158-
{ error: 'Invalid telemetry data format or contains sensitive information' },
159-
{ status: 400 }
160-
)
161-
}
152+
const parsed = await parseRequest(telemetryContract, req, {})
153+
if (!parsed.success) return parsed.response
162154

163-
const forwarded = await forwardToCollector(validation.data as Record<string, unknown>)
155+
const forwarded = await forwardToCollector(parsed.data.body)
164156

165157
return NextResponse.json({
166158
success: true,

0 commit comments

Comments
 (0)