ATlast — you'll never need to find your favorites on another platform again. Find your favs in the ATmosphere.
atproto

replace duplicate validation with Zod schemas

Optimizations #6 & #7:
- #6: verified early exit optimization already implemented in FollowService
- #7: created validation.utils.ts with Zod schemas for array validation
- replaced 3 duplicate validation blocks with reusable Zod schemas
- updated batch-follow-users, check-follow-status, batch-search-actors

byarielm.fyi 093b47d6 0b453335

verified
+6 -11
netlify/functions/batch-follow-users.ts
··· 2 2 import { SessionService } from "./services/SessionService"; 3 3 import { FollowService } from "./services/FollowService"; 4 4 import { MatchRepository } from "./repositories"; 5 - import { successResponse } from "./utils"; 5 + import { successResponse, validateArrayInput, ValidationSchemas } from "./utils"; 6 6 import { withAuthErrorHandling } from "./core/middleware"; 7 - import { ValidationError } from "./core/errors"; 8 7 9 8 const batchFollowHandler: AuthenticatedHandler = async (context) => { 10 9 const body = JSON.parse(context.event.body || "{}"); 11 - const dids: string[] = body.dids || []; 10 + const dids = validateArrayInput<string>( 11 + context.event.body, 12 + "dids", 13 + ValidationSchemas.didsArray, 14 + ); 12 15 const followLexicon: string = body.followLexicon || "app.bsky.graph.follow"; 13 - 14 - if (!Array.isArray(dids) || dids.length === 0) { 15 - throw new ValidationError("dids array is required and must not be empty"); 16 - } 17 - 18 - if (dids.length > 100) { 19 - throw new ValidationError("Maximum 100 DIDs per batch"); 20 - } 21 16 22 17 const { agent } = await SessionService.getAgentForSession( 23 18 context.sessionId,
+6 -13
netlify/functions/batch-search-actors.ts
··· 1 1 import { AuthenticatedHandler } from "./core/types"; 2 2 import { SessionService } from "./services/SessionService"; 3 - import { successResponse } from "./utils"; 3 + import { successResponse, validateArrayInput, ValidationSchemas } from "./utils"; 4 4 import { withAuthErrorHandling } from "./core/middleware"; 5 - import { ValidationError } from "./core/errors"; 6 5 import { normalize } from "./utils/string.utils"; 7 6 import { FollowService } from "./services/FollowService"; 8 7 9 8 const batchSearchHandler: AuthenticatedHandler = async (context) => { 10 9 const body = JSON.parse(context.event.body || "{}"); 11 - const usernames: string[] = body.usernames || []; 12 - 13 - if (!Array.isArray(usernames) || usernames.length === 0) { 14 - throw new ValidationError( 15 - "usernames array is required and must not be empty", 16 - ); 17 - } 18 - 19 - if (usernames.length > 50) { 20 - throw new ValidationError("Maximum 50 usernames per batch"); 21 - } 10 + const usernames = validateArrayInput<string>( 11 + context.event.body, 12 + "usernames", 13 + ValidationSchemas.usernamesArray, 14 + ); 22 15 23 16 const { agent } = await SessionService.getAgentForSession( 24 17 context.sessionId,
+6 -11
netlify/functions/check-follow-status.ts
··· 1 1 import { AuthenticatedHandler } from "./core/types"; 2 2 import { SessionService } from "./services/SessionService"; 3 3 import { FollowService } from "./services/FollowService"; 4 - import { successResponse } from "./utils"; 4 + import { successResponse, validateArrayInput, ValidationSchemas } from "./utils"; 5 5 import { withAuthErrorHandling } from "./core/middleware"; 6 - import { ValidationError } from "./core/errors"; 7 6 8 7 const checkFollowStatusHandler: AuthenticatedHandler = async (context) => { 9 8 const body = JSON.parse(context.event.body || "{}"); 10 - const dids: string[] = body.dids || []; 9 + const dids = validateArrayInput<string>( 10 + context.event.body, 11 + "dids", 12 + ValidationSchemas.didsArray, 13 + ); 11 14 const followLexicon: string = body.followLexicon || "app.bsky.graph.follow"; 12 - 13 - if (!Array.isArray(dids) || dids.length === 0) { 14 - throw new ValidationError("dids array is required and must not be empty"); 15 - } 16 - 17 - if (dids.length > 100) { 18 - throw new ValidationError("Maximum 100 DIDs per batch"); 19 - } 20 15 21 16 const { agent } = await SessionService.getAgentForSession( 22 17 context.sessionId,
+1
netlify/functions/utils/index.ts
··· 1 1 export * from "./response.utils"; 2 2 export * from "./string.utils"; 3 3 export * from "./encryption.utils"; 4 + export * from "./validation.utils";
+74
netlify/functions/utils/validation.utils.ts
··· 1 + import { z } from "zod"; 2 + import { ValidationError } from "../core/errors"; 3 + 4 + /** 5 + * Validation utility schemas using Zod 6 + * Provides type-safe validation with clear error messages 7 + */ 8 + 9 + /** 10 + * Generic array validation schema factory 11 + * @param itemSchema - Zod schema for array items 12 + * @param maxLength - Maximum array length 13 + * @param fieldName - Name of field for error messages 14 + */ 15 + export function createArraySchema<T extends z.ZodTypeAny>( 16 + itemSchema: T, 17 + maxLength: number, 18 + fieldName: string = "items", 19 + ) { 20 + return z 21 + .array(itemSchema) 22 + .min(1, `${fieldName} array is required and must not be empty`) 23 + .max(maxLength, `Maximum ${maxLength} ${fieldName} per batch`); 24 + } 25 + 26 + /** 27 + * Common validation schemas 28 + */ 29 + export const ValidationSchemas = { 30 + // DIDs array (max 100) 31 + didsArray: createArraySchema(z.string(), 100, "DIDs"), 32 + 33 + // Usernames array (max 50) 34 + usernamesArray: createArraySchema(z.string(), 50, "usernames"), 35 + 36 + // Generic string array with custom max 37 + stringArray: (maxLength: number, fieldName: string = "items") => 38 + createArraySchema(z.string(), maxLength, fieldName), 39 + }; 40 + 41 + /** 42 + * Validates input against a Zod schema and throws ValidationError on failure 43 + * @param schema - Zod schema to validate against 44 + * @param data - Data to validate 45 + * @returns Parsed and validated data 46 + * @throws ValidationError if validation fails 47 + */ 48 + export function validateInput<T>(schema: z.ZodSchema<T>, data: unknown): T { 49 + const result = schema.safeParse(data); 50 + 51 + if (!result.success) { 52 + // Extract first error message for cleaner API responses 53 + const firstError = result.error.issues[0]; 54 + const message = firstError.message; 55 + throw new ValidationError(message); 56 + } 57 + 58 + return result.data; 59 + } 60 + 61 + /** 62 + * Parses request body and validates array input 63 + * Common pattern: JSON.parse(body) -> extract array -> validate 64 + */ 65 + export function validateArrayInput<T>( 66 + body: string | null, 67 + fieldName: string, 68 + schema: z.ZodArray<any>, 69 + ): T[] { 70 + const parsed = JSON.parse(body || "{}"); 71 + const data = parsed[fieldName]; 72 + 73 + return validateInput(schema, data); 74 + }