Merge pull request #276 from trails-cool/chore/zod-4-migration
Upgrade zod from 3 to 4
This commit is contained in:
commit
be2baa455f
17 changed files with 57 additions and 37 deletions
|
|
@ -51,14 +51,14 @@ const PersonaSchema = z
|
||||||
for (const loc of p.locales) {
|
for (const loc of p.locales) {
|
||||||
if (!p.content.names[loc]) {
|
if (!p.content.names[loc]) {
|
||||||
ctx.addIssue({
|
ctx.addIssue({
|
||||||
code: z.ZodIssueCode.custom,
|
code: "custom",
|
||||||
path: ["content", "names", loc],
|
path: ["content", "names", loc],
|
||||||
message: `missing name pool for declared locale '${loc}'`,
|
message: `missing name pool for declared locale '${loc}'`,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
if (!p.content.descriptions[loc]) {
|
if (!p.content.descriptions[loc]) {
|
||||||
ctx.addIssue({
|
ctx.addIssue({
|
||||||
code: z.ZodIssueCode.custom,
|
code: "custom",
|
||||||
path: ["content", "descriptions", loc],
|
path: ["content", "descriptions", loc],
|
||||||
message: `missing description pool for declared locale '${loc}'`,
|
message: `missing description pool for declared locale '${loc}'`,
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@ import {
|
||||||
PaginationQuerySchema,
|
PaginationQuerySchema,
|
||||||
CreateActivityRequestSchema,
|
CreateActivityRequestSchema,
|
||||||
ERROR_CODES,
|
ERROR_CODES,
|
||||||
|
zodIssuesToFieldErrors,
|
||||||
} from "@trails-cool/api";
|
} from "@trails-cool/api";
|
||||||
|
|
||||||
/** GET /api/v1/activities — paginated activity list */
|
/** GET /api/v1/activities — paginated activity list */
|
||||||
|
|
@ -59,7 +60,7 @@ export async function action({ request }: Route.ActionArgs) {
|
||||||
const parsed = CreateActivityRequestSchema.safeParse(body);
|
const parsed = CreateActivityRequestSchema.safeParse(body);
|
||||||
if (!parsed.success) {
|
if (!parsed.success) {
|
||||||
return apiError(400, ERROR_CODES.VALIDATION_ERROR, "Validation failed",
|
return apiError(400, ERROR_CODES.VALIDATION_ERROR, "Validation failed",
|
||||||
parsed.error.issues.map((i) => ({ field: i.path.join("."), message: i.message })));
|
zodIssuesToFieldErrors(parsed.error));
|
||||||
}
|
}
|
||||||
|
|
||||||
const id = await createActivity(user.id, {
|
const id = await createActivity(user.id, {
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
import type { Route } from "./+types/api.v1.routes.$id";
|
import type { Route } from "./+types/api.v1.routes.$id";
|
||||||
import { requireApiUser, apiError } from "~/lib/api-guard.server";
|
import { requireApiUser, apiError } from "~/lib/api-guard.server";
|
||||||
import { getRouteWithVersions, updateRoute, deleteRoute } from "~/lib/routes.server";
|
import { getRouteWithVersions, updateRoute, deleteRoute } from "~/lib/routes.server";
|
||||||
import { UpdateRouteRequestSchema, ERROR_CODES } from "@trails-cool/api";
|
import { UpdateRouteRequestSchema, ERROR_CODES, zodIssuesToFieldErrors } from "@trails-cool/api";
|
||||||
|
|
||||||
/** GET /api/v1/routes/:id — full route detail */
|
/** GET /api/v1/routes/:id — full route detail */
|
||||||
export async function loader({ request, params }: Route.LoaderArgs) {
|
export async function loader({ request, params }: Route.LoaderArgs) {
|
||||||
|
|
@ -45,7 +45,7 @@ export async function action({ request, params }: Route.ActionArgs) {
|
||||||
const parsed = UpdateRouteRequestSchema.safeParse(body);
|
const parsed = UpdateRouteRequestSchema.safeParse(body);
|
||||||
if (!parsed.success) {
|
if (!parsed.success) {
|
||||||
return apiError(400, ERROR_CODES.VALIDATION_ERROR, "Validation failed",
|
return apiError(400, ERROR_CODES.VALIDATION_ERROR, "Validation failed",
|
||||||
parsed.error.issues.map((i) => ({ field: i.path.join("."), message: i.message })));
|
zodIssuesToFieldErrors(parsed.error));
|
||||||
}
|
}
|
||||||
|
|
||||||
await updateRoute(params.id, user.id, parsed.data);
|
await updateRoute(params.id, user.id, parsed.data);
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@ import {
|
||||||
PaginationQuerySchema,
|
PaginationQuerySchema,
|
||||||
CreateRouteRequestSchema,
|
CreateRouteRequestSchema,
|
||||||
ERROR_CODES,
|
ERROR_CODES,
|
||||||
|
zodIssuesToFieldErrors,
|
||||||
} from "@trails-cool/api";
|
} from "@trails-cool/api";
|
||||||
|
|
||||||
/** GET /api/v1/routes — paginated route list */
|
/** GET /api/v1/routes — paginated route list */
|
||||||
|
|
@ -58,7 +59,7 @@ export async function action({ request }: Route.ActionArgs) {
|
||||||
const parsed = CreateRouteRequestSchema.safeParse(body);
|
const parsed = CreateRouteRequestSchema.safeParse(body);
|
||||||
if (!parsed.success) {
|
if (!parsed.success) {
|
||||||
return apiError(400, ERROR_CODES.VALIDATION_ERROR, "Validation failed",
|
return apiError(400, ERROR_CODES.VALIDATION_ERROR, "Validation failed",
|
||||||
parsed.error.issues.map((i) => ({ field: i.path.join("."), message: i.message })));
|
zodIssuesToFieldErrors(parsed.error));
|
||||||
}
|
}
|
||||||
|
|
||||||
const id = await createRoute(user.id, parsed.data);
|
const id = await createRoute(user.id, parsed.data);
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import type { Route } from "./+types/api.v1.routes.compute";
|
import type { Route } from "./+types/api.v1.routes.compute";
|
||||||
import { requireApiUser, apiError } from "~/lib/api-guard.server";
|
import { requireApiUser, apiError } from "~/lib/api-guard.server";
|
||||||
import { ComputeRouteRequestSchema, ERROR_CODES } from "@trails-cool/api";
|
import { ComputeRouteRequestSchema, ERROR_CODES, zodIssuesToFieldErrors } from "@trails-cool/api";
|
||||||
|
|
||||||
const PLANNER_URL = process.env.PLANNER_URL ?? "http://localhost:3001";
|
const PLANNER_URL = process.env.PLANNER_URL ?? "http://localhost:3001";
|
||||||
|
|
||||||
|
|
@ -13,7 +13,7 @@ export async function action({ request }: Route.ActionArgs) {
|
||||||
const parsed = ComputeRouteRequestSchema.safeParse(body);
|
const parsed = ComputeRouteRequestSchema.safeParse(body);
|
||||||
if (!parsed.success) {
|
if (!parsed.success) {
|
||||||
return apiError(400, ERROR_CODES.VALIDATION_ERROR, "Validation failed",
|
return apiError(400, ERROR_CODES.VALIDATION_ERROR, "Validation failed",
|
||||||
parsed.error.issues.map((i) => ({ field: i.path.join("."), message: i.message })));
|
zodIssuesToFieldErrors(parsed.error));
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import type { Route } from "./+types/api.v1.uploads";
|
import type { Route } from "./+types/api.v1.uploads";
|
||||||
import { requireApiUser, apiError } from "~/lib/api-guard.server";
|
import { requireApiUser, apiError } from "~/lib/api-guard.server";
|
||||||
import { PresignedUploadRequestSchema, ERROR_CODES } from "@trails-cool/api";
|
import { PresignedUploadRequestSchema, ERROR_CODES, zodIssuesToFieldErrors } from "@trails-cool/api";
|
||||||
import { randomUUID } from "node:crypto";
|
import { randomUUID } from "node:crypto";
|
||||||
|
|
||||||
const S3_ENDPOINT = process.env.S3_ENDPOINT ?? "http://localhost:3902";
|
const S3_ENDPOINT = process.env.S3_ENDPOINT ?? "http://localhost:3902";
|
||||||
|
|
@ -16,7 +16,7 @@ export async function action({ request }: Route.ActionArgs) {
|
||||||
const parsed = PresignedUploadRequestSchema.safeParse(body);
|
const parsed = PresignedUploadRequestSchema.safeParse(body);
|
||||||
if (!parsed.success) {
|
if (!parsed.success) {
|
||||||
return apiError(400, ERROR_CODES.VALIDATION_ERROR, "Validation failed",
|
return apiError(400, ERROR_CODES.VALIDATION_ERROR, "Validation failed",
|
||||||
parsed.error.issues.map((i) => ({ field: i.path.join("."), message: i.message })));
|
zodIssuesToFieldErrors(parsed.error));
|
||||||
}
|
}
|
||||||
|
|
||||||
const { filename, resourceType, resourceId } = parsed.data;
|
const { filename, resourceType, resourceId } = parsed.data;
|
||||||
|
|
|
||||||
|
|
@ -36,7 +36,7 @@
|
||||||
"react": "catalog:",
|
"react": "catalog:",
|
||||||
"react-dom": "catalog:",
|
"react-dom": "catalog:",
|
||||||
"react-router": "catalog:",
|
"react-router": "catalog:",
|
||||||
"zod": "^3.25.0"
|
"zod": "^4.0.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@react-router/dev": "catalog:",
|
"@react-router/dev": "catalog:",
|
||||||
|
|
|
||||||
|
|
@ -66,7 +66,7 @@
|
||||||
"react-native-safe-area-context": "~5.6.2",
|
"react-native-safe-area-context": "~5.6.2",
|
||||||
"react-native-screens": "~4.23.0",
|
"react-native-screens": "~4.23.0",
|
||||||
"use-latest-callback": "^0.3.3",
|
"use-latest-callback": "^0.3.3",
|
||||||
"zod": "^3.25.76"
|
"zod": "^4.0.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@testing-library/react-native": "^13.3.3",
|
"@testing-library/react-native": "^13.3.3",
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,6 @@
|
||||||
"typecheck": "tsc"
|
"typecheck": "tsc"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"zod": "^3.25.0"
|
"zod": "^4.0.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,24 +2,24 @@ import { z } from "zod";
|
||||||
|
|
||||||
/** Activity summary for list views */
|
/** Activity summary for list views */
|
||||||
export const ActivitySummarySchema = z.object({
|
export const ActivitySummarySchema = z.object({
|
||||||
id: z.string().uuid(),
|
id: z.uuid(),
|
||||||
name: z.string(),
|
name: z.string(),
|
||||||
description: z.string(),
|
description: z.string(),
|
||||||
routeId: z.string().uuid().nullable(),
|
routeId: z.uuid().nullable(),
|
||||||
routeName: z.string().nullable(),
|
routeName: z.string().nullable(),
|
||||||
distance: z.number().nullable(),
|
distance: z.number().nullable(),
|
||||||
duration: z.number().nullable(),
|
duration: z.number().nullable(),
|
||||||
elevationGain: z.number().nullable(),
|
elevationGain: z.number().nullable(),
|
||||||
elevationLoss: z.number().nullable(),
|
elevationLoss: z.number().nullable(),
|
||||||
startedAt: z.string().datetime().nullable(),
|
startedAt: z.iso.datetime().nullable(),
|
||||||
geojson: z.string().nullable(),
|
geojson: z.string().nullable(),
|
||||||
createdAt: z.string().datetime(),
|
createdAt: z.iso.datetime(),
|
||||||
});
|
});
|
||||||
|
|
||||||
/** Full activity detail */
|
/** Full activity detail */
|
||||||
export const ActivityDetailSchema = ActivitySummarySchema.extend({
|
export const ActivityDetailSchema = ActivitySummarySchema.extend({
|
||||||
gpx: z.string().nullable(),
|
gpx: z.string().nullable(),
|
||||||
photos: z.array(z.string().url()),
|
photos: z.array(z.url()),
|
||||||
});
|
});
|
||||||
|
|
||||||
/** Paginated activity list response */
|
/** Paginated activity list response */
|
||||||
|
|
@ -33,8 +33,8 @@ export const CreateActivityRequestSchema = z.object({
|
||||||
name: z.string().min(1).max(200),
|
name: z.string().min(1).max(200),
|
||||||
description: z.string().max(5000).default(""),
|
description: z.string().max(5000).default(""),
|
||||||
gpx: z.string().optional(),
|
gpx: z.string().optional(),
|
||||||
routeId: z.string().uuid().optional(),
|
routeId: z.uuid().optional(),
|
||||||
startedAt: z.string().datetime().optional(),
|
startedAt: z.iso.datetime().optional(),
|
||||||
duration: z.number().optional(),
|
duration: z.number().optional(),
|
||||||
distance: z.number().optional(),
|
distance: z.number().optional(),
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -20,8 +20,8 @@ export const TokenResponseSchema = z.object({
|
||||||
export const DeviceSchema = z.object({
|
export const DeviceSchema = z.object({
|
||||||
id: z.string(),
|
id: z.string(),
|
||||||
deviceName: z.string().nullable(),
|
deviceName: z.string().nullable(),
|
||||||
lastActiveAt: z.string().datetime(),
|
lastActiveAt: z.iso.datetime(),
|
||||||
createdAt: z.string().datetime(),
|
createdAt: z.iso.datetime(),
|
||||||
isCurrent: z.boolean(),
|
isCurrent: z.boolean(),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -3,8 +3,8 @@ import { z } from "zod";
|
||||||
export const DiscoveryResponseSchema = z.object({
|
export const DiscoveryResponseSchema = z.object({
|
||||||
apiVersion: z.string(),
|
apiVersion: z.string(),
|
||||||
instanceName: z.string(),
|
instanceName: z.string(),
|
||||||
apiBaseUrl: z.string().url(),
|
apiBaseUrl: z.url(),
|
||||||
tileUrl: z.string().url().optional(),
|
tileUrl: z.url().optional(),
|
||||||
});
|
});
|
||||||
|
|
||||||
export type DiscoveryResponse = z.infer<typeof DiscoveryResponseSchema>;
|
export type DiscoveryResponse = z.infer<typeof DiscoveryResponseSchema>;
|
||||||
|
|
|
||||||
|
|
@ -14,6 +14,18 @@ export const ApiErrorResponseSchema = z.object({
|
||||||
export type FieldError = z.infer<typeof FieldErrorSchema>;
|
export type FieldError = z.infer<typeof FieldErrorSchema>;
|
||||||
export type ApiErrorResponse = z.infer<typeof ApiErrorResponseSchema>;
|
export type ApiErrorResponse = z.infer<typeof ApiErrorResponseSchema>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Map a ZodError's issues to the FieldError[] shape used in API
|
||||||
|
* validation responses. Keeps the path-flattening rule in one place so
|
||||||
|
* every route returns validation errors in the same shape.
|
||||||
|
*/
|
||||||
|
export function zodIssuesToFieldErrors(error: z.ZodError): FieldError[] {
|
||||||
|
return error.issues.map((i) => ({
|
||||||
|
field: i.path.join("."),
|
||||||
|
message: i.message,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
/** Standard error codes */
|
/** Standard error codes */
|
||||||
export const ERROR_CODES = {
|
export const ERROR_CODES = {
|
||||||
VALIDATION_ERROR: "VALIDATION_ERROR",
|
VALIDATION_ERROR: "VALIDATION_ERROR",
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@ export { ENDPOINTS } from "./endpoints.ts";
|
||||||
// Error schemas
|
// Error schemas
|
||||||
export {
|
export {
|
||||||
ApiErrorResponseSchema, FieldErrorSchema, ERROR_CODES,
|
ApiErrorResponseSchema, FieldErrorSchema, ERROR_CODES,
|
||||||
|
zodIssuesToFieldErrors,
|
||||||
type ApiErrorResponse, type FieldError,
|
type ApiErrorResponse, type FieldError,
|
||||||
} from "./errors.ts";
|
} from "./errors.ts";
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@ import { z } from "zod";
|
||||||
|
|
||||||
/** Route summary for list views */
|
/** Route summary for list views */
|
||||||
export const RouteSummarySchema = z.object({
|
export const RouteSummarySchema = z.object({
|
||||||
id: z.string().uuid(),
|
id: z.uuid(),
|
||||||
name: z.string(),
|
name: z.string(),
|
||||||
description: z.string(),
|
description: z.string(),
|
||||||
distance: z.number().nullable(),
|
distance: z.number().nullable(),
|
||||||
|
|
@ -11,15 +11,15 @@ export const RouteSummarySchema = z.object({
|
||||||
routingProfile: z.string().nullable(),
|
routingProfile: z.string().nullable(),
|
||||||
dayBreaks: z.array(z.number()),
|
dayBreaks: z.array(z.number()),
|
||||||
geojson: z.string().nullable(),
|
geojson: z.string().nullable(),
|
||||||
createdAt: z.string().datetime(),
|
createdAt: z.iso.datetime(),
|
||||||
updatedAt: z.string().datetime(),
|
updatedAt: z.iso.datetime(),
|
||||||
});
|
});
|
||||||
|
|
||||||
/** Route version info */
|
/** Route version info */
|
||||||
export const RouteVersionSchema = z.object({
|
export const RouteVersionSchema = z.object({
|
||||||
version: z.number(),
|
version: z.number(),
|
||||||
changeDescription: z.string().nullable(),
|
changeDescription: z.string().nullable(),
|
||||||
createdAt: z.string().datetime(),
|
createdAt: z.iso.datetime(),
|
||||||
});
|
});
|
||||||
|
|
||||||
/** Full route detail */
|
/** Full route detail */
|
||||||
|
|
|
||||||
|
|
@ -4,13 +4,13 @@ export const PresignedUploadRequestSchema = z.object({
|
||||||
filename: z.string(),
|
filename: z.string(),
|
||||||
contentType: z.string(),
|
contentType: z.string(),
|
||||||
resourceType: z.enum(["route", "activity"]),
|
resourceType: z.enum(["route", "activity"]),
|
||||||
resourceId: z.string().uuid(),
|
resourceId: z.uuid(),
|
||||||
});
|
});
|
||||||
|
|
||||||
export const PresignedUploadResponseSchema = z.object({
|
export const PresignedUploadResponseSchema = z.object({
|
||||||
uploadUrl: z.string().url(),
|
uploadUrl: z.url(),
|
||||||
storageKey: z.string(),
|
storageKey: z.string(),
|
||||||
expiresAt: z.string().datetime(),
|
expiresAt: z.iso.datetime(),
|
||||||
});
|
});
|
||||||
|
|
||||||
export type PresignedUploadRequest = z.infer<typeof PresignedUploadRequestSchema>;
|
export type PresignedUploadRequest = z.infer<typeof PresignedUploadRequestSchema>;
|
||||||
|
|
|
||||||
17
pnpm-lock.yaml
generated
17
pnpm-lock.yaml
generated
|
|
@ -267,8 +267,8 @@ importers:
|
||||||
specifier: 'catalog:'
|
specifier: 'catalog:'
|
||||||
version: 7.14.1(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
|
version: 7.14.1(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
|
||||||
zod:
|
zod:
|
||||||
specifier: ^3.25.0
|
specifier: ^4.0.0
|
||||||
version: 3.25.76
|
version: 4.3.6
|
||||||
devDependencies:
|
devDependencies:
|
||||||
'@react-router/dev':
|
'@react-router/dev':
|
||||||
specifier: 'catalog:'
|
specifier: 'catalog:'
|
||||||
|
|
@ -418,8 +418,8 @@ importers:
|
||||||
specifier: ^0.3.3
|
specifier: ^0.3.3
|
||||||
version: 0.3.3(react@19.2.5)
|
version: 0.3.3(react@19.2.5)
|
||||||
zod:
|
zod:
|
||||||
specifier: ^3.25.76
|
specifier: ^4.0.0
|
||||||
version: 3.25.76
|
version: 4.3.6
|
||||||
devDependencies:
|
devDependencies:
|
||||||
'@testing-library/react-native':
|
'@testing-library/react-native':
|
||||||
specifier: ^13.3.3
|
specifier: ^13.3.3
|
||||||
|
|
@ -576,8 +576,8 @@ importers:
|
||||||
packages/api:
|
packages/api:
|
||||||
dependencies:
|
dependencies:
|
||||||
zod:
|
zod:
|
||||||
specifier: ^3.25.0
|
specifier: ^4.0.0
|
||||||
version: 3.25.76
|
version: 4.3.6
|
||||||
|
|
||||||
packages/db:
|
packages/db:
|
||||||
dependencies:
|
dependencies:
|
||||||
|
|
@ -7671,6 +7671,9 @@ packages:
|
||||||
zod@3.25.76:
|
zod@3.25.76:
|
||||||
resolution: {integrity: sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==}
|
resolution: {integrity: sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==}
|
||||||
|
|
||||||
|
zod@4.3.6:
|
||||||
|
resolution: {integrity: sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==}
|
||||||
|
|
||||||
snapshots:
|
snapshots:
|
||||||
|
|
||||||
'@adobe/css-tools@4.4.4': {}
|
'@adobe/css-tools@4.4.4': {}
|
||||||
|
|
@ -15661,3 +15664,5 @@ snapshots:
|
||||||
yocto-queue@0.1.0: {}
|
yocto-queue@0.1.0: {}
|
||||||
|
|
||||||
zod@3.25.76: {}
|
zod@3.25.76: {}
|
||||||
|
|
||||||
|
zod@4.3.6: {}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue