+6
-11
netlify/functions/batch-follow-users.ts
+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
+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
+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
netlify/functions/utils/index.ts
+74
netlify/functions/utils/validation.utils.ts
+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
+
}