mirror of
https://github.com/nestriness/nestri.git
synced 2025-12-12 16:55:37 +02:00
⭐ feat(www): Finish up on the onboarding (#210)
Merging this prematurely to make sure the team is on the same boat... like dang! We need to find a better way to do this. Plus it has become too big
This commit is contained in:
246
packages/functions/src/api/common.ts
Normal file
246
packages/functions/src/api/common.ts
Normal file
@@ -0,0 +1,246 @@
|
||||
import {type Hook } from "./hook";
|
||||
import { z, ZodSchema } from "zod";
|
||||
import { ErrorCodes, ErrorResponse } from "@nestri/core/error";
|
||||
import type { MiddlewareHandler, ValidationTargets } from "hono";
|
||||
import { resolver, validator as zodValidator } from "hono-openapi/zod";
|
||||
|
||||
export function Result<T extends z.ZodTypeAny>(schema: T) {
|
||||
return resolver(
|
||||
z.object({
|
||||
data: schema,
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Custom validator wrapper around hono-openapi/zod validator that formats errors
|
||||
* according to our standard API error format
|
||||
*/
|
||||
export const validator = <
|
||||
T extends ZodSchema,
|
||||
Target extends keyof ValidationTargets
|
||||
>(
|
||||
target: Target,
|
||||
schema: T
|
||||
): MiddlewareHandler<
|
||||
any,
|
||||
string,
|
||||
{
|
||||
in: {
|
||||
[K in Target]: z.input<T>;
|
||||
};
|
||||
out: {
|
||||
[K in Target]: z.output<T>;
|
||||
};
|
||||
}
|
||||
> => {
|
||||
// Create a custom error handler that formats errors according to our standards
|
||||
// const standardErrorHandler: Parameters<typeof zodValidator>[2] = (
|
||||
const standardErrorHandler: Hook<z.infer<T>, any, any, Target> = (
|
||||
result,
|
||||
c,
|
||||
) => {
|
||||
if (!result.success) {
|
||||
// Get the validation issues
|
||||
const issues = result.error.issues || result.error.errors || [];
|
||||
if (issues.length === 0) {
|
||||
// If there are no issues, return a generic error
|
||||
return c.json(
|
||||
{
|
||||
type: "validation",
|
||||
code: ErrorCodes.Validation.INVALID_PARAMETER,
|
||||
message: "Invalid request data",
|
||||
},
|
||||
400,
|
||||
);
|
||||
}
|
||||
|
||||
// Get the first error for the main response
|
||||
const firstIssue = issues[0]!;
|
||||
const fieldPath = firstIssue.path
|
||||
? Array.isArray(firstIssue.path)
|
||||
? firstIssue.path.join(".")
|
||||
: firstIssue.path
|
||||
: undefined;
|
||||
|
||||
// Map Zod error codes to our standard error codes
|
||||
let errorCode = ErrorCodes.Validation.INVALID_PARAMETER;
|
||||
if (
|
||||
firstIssue.code === "invalid_type" &&
|
||||
firstIssue.received === "undefined"
|
||||
) {
|
||||
errorCode = ErrorCodes.Validation.MISSING_REQUIRED_FIELD;
|
||||
} else if (
|
||||
["invalid_string", "invalid_date", "invalid_regex"].includes(
|
||||
firstIssue.code,
|
||||
)
|
||||
) {
|
||||
errorCode = ErrorCodes.Validation.INVALID_FORMAT;
|
||||
}
|
||||
|
||||
// Create our standardized error response
|
||||
const response = {
|
||||
type: "validation",
|
||||
code: errorCode,
|
||||
message: firstIssue.message,
|
||||
param: fieldPath,
|
||||
details: undefined as any,
|
||||
};
|
||||
|
||||
// Add details if we have multiple issues
|
||||
if (issues.length > 0) {
|
||||
response.details = {
|
||||
issues: issues.map((issue) => ({
|
||||
path: issue.path
|
||||
? Array.isArray(issue.path)
|
||||
? issue.path.join(".")
|
||||
: issue.path
|
||||
: undefined,
|
||||
code: issue.code,
|
||||
message: issue.message,
|
||||
// @ts-expect-error
|
||||
expected: issue.expected,
|
||||
// @ts-expect-error
|
||||
received: issue.received,
|
||||
})),
|
||||
};
|
||||
}
|
||||
|
||||
console.log("Validation error in validator:", response);
|
||||
return c.json(response, 400);
|
||||
}
|
||||
};
|
||||
|
||||
// Use the original validator with our custom error handler
|
||||
return zodValidator(target, schema, standardErrorHandler);
|
||||
};
|
||||
|
||||
/**
|
||||
* Standard error responses for OpenAPI documentation
|
||||
*/
|
||||
export const ErrorResponses = {
|
||||
400: {
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: resolver(
|
||||
ErrorResponse.openapi({
|
||||
description: "Validation error",
|
||||
example: {
|
||||
type: "validation",
|
||||
code: "invalid_parameter",
|
||||
message: "The request was invalid",
|
||||
param: "email",
|
||||
},
|
||||
}),
|
||||
),
|
||||
},
|
||||
},
|
||||
description:
|
||||
"Bad Request - The request could not be understood or was missing required parameters.",
|
||||
},
|
||||
401: {
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: resolver(
|
||||
ErrorResponse.openapi({
|
||||
description: "Authentication error",
|
||||
example: {
|
||||
type: "authentication",
|
||||
code: "unauthorized",
|
||||
message: "Authentication required",
|
||||
},
|
||||
}),
|
||||
),
|
||||
},
|
||||
},
|
||||
description:
|
||||
"Unauthorized - Authentication is required and has failed or has not been provided.",
|
||||
},
|
||||
403: {
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: resolver(
|
||||
ErrorResponse.openapi({
|
||||
description: "Permission error",
|
||||
example: {
|
||||
type: "forbidden",
|
||||
code: "permission_denied",
|
||||
message: "You do not have permission to access this resource",
|
||||
},
|
||||
}),
|
||||
),
|
||||
},
|
||||
},
|
||||
description:
|
||||
"Forbidden - You do not have permission to access this resource.",
|
||||
},
|
||||
404: {
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: resolver(
|
||||
ErrorResponse.openapi({
|
||||
description: "Not found error",
|
||||
example: {
|
||||
type: "not_found",
|
||||
code: "resource_not_found",
|
||||
message: "The requested resource could not be found",
|
||||
},
|
||||
}),
|
||||
),
|
||||
},
|
||||
},
|
||||
description: "Not Found - The requested resource does not exist.",
|
||||
},
|
||||
409: {
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: resolver(
|
||||
ErrorResponse.openapi({
|
||||
description: "Conflict Error",
|
||||
example: {
|
||||
type: "already_exists",
|
||||
code: "resource_already_exists",
|
||||
message: "The resource could not be created because it already exists",
|
||||
},
|
||||
}),
|
||||
),
|
||||
},
|
||||
},
|
||||
description: "Conflict - The resource could not be created because it already exists.",
|
||||
},
|
||||
429: {
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: resolver(
|
||||
ErrorResponse.openapi({
|
||||
description: "Rate limit error",
|
||||
example: {
|
||||
type: "rate_limit",
|
||||
code: "too_many_requests",
|
||||
message: "Rate limit exceeded",
|
||||
},
|
||||
}),
|
||||
),
|
||||
},
|
||||
},
|
||||
description:
|
||||
"Too Many Requests - You have made too many requests in a short period of time.",
|
||||
},
|
||||
500: {
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: resolver(
|
||||
ErrorResponse.openapi({
|
||||
description: "Server error",
|
||||
example: {
|
||||
type: "internal",
|
||||
code: "internal_error",
|
||||
message: "Internal server error",
|
||||
},
|
||||
}),
|
||||
),
|
||||
},
|
||||
},
|
||||
description: "Internal Server Error - Something went wrong on our end.",
|
||||
},
|
||||
};
|
||||
Reference in New Issue
Block a user