Skip to content

Commit f2d5a68

Browse files
authored
refactor: migrate validation to compiled zod 4 (#256)
1 parent ae8c7cd commit f2d5a68

24 files changed

Lines changed: 609 additions & 628 deletions

.agents/skills/vibes-frontend/SKILL.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ Apply these repository-specific rules together with `AGENTS.md`.
3030
- `apps/mobile`: native-only Expo Router app for iOS and Android phones/tablets.
3131
- `apps/tv`: one TV product with a shared session layer, delivered through an Expo Android TV renderer and a Samsung TV DOM renderer.
3232
- `packages/api`: the transport package and the only package that owns `wiretyped`, backend REST calls, typed request capabilities, SSE plumbing, and narrowly scoped reusable SSE hooks.
33-
- `packages/models`: shared Yup schemas and derived domain types.
33+
- `packages/models`: shared compiled Zod 4 schemas and derived domain types.
3434
- `packages/shared`: platform-neutral utilities, hooks, stores, constants, and safe wrappers.
3535
- `packages/ui/web`: DOM components and provider players.
3636
- `packages/ui/native`: React Native primitives and official native provider-player wrappers shared by mobile and Android TV.

AGENTS.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ apps/cast/src/
4949
5050
packages/
5151
├── api/ # API client
52-
├── models/ # Shared types and Yup schemas
52+
├── models/ # Shared types and compiled Zod 4 schemas
5353
├── shared/ # Utilities, hooks, stores
5454
│ ├── src/utils/wrap.ts # safeWrap utilities
5555
│ ├── src/stores/ # Shared Zustand stores (playbackStore)

apps/mobile/app.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
"expo": {
33
"name": "Zoff",
44
"slug": "vibes-mobile",
5-
"version": "0.2.18",
5+
"version": "0.2.19",
66
"orientation": "default",
77
"icon": "./assets/images/icon.png",
88
"scheme": "zoff",

apps/tv/app.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
"expo": {
33
"name": "Zoff TV",
44
"slug": "vibes-tv",
5-
"version": "0.1.11",
5+
"version": "0.1.12",
66
"orientation": "landscape",
77
"icon": "./assets/icon.png",
88
"scheme": "zoff-tv",

packages/api/package.json

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,7 @@
1515
"dependencies": {
1616
"@vibes/models": "workspace:*",
1717
"@vibes/shared": "workspace:*",
18-
"wiretyped": "0.3.4",
19-
"yup": "1.7.1"
18+
"wiretyped": "0.3.4"
2019
},
2120
"peerDependencies": {
2221
"react": "^19.2.7"

packages/api/src/client.ts

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -71,8 +71,6 @@ import {
7171
youTubeVideoSchema,
7272
} from '@vibes/models';
7373

74-
export * as yup from 'yup';
75-
7674
import {
7775
getHttpError,
7876
RequestClient,

packages/api/src/rateLimit.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,7 @@ export async function getAPIErrorMessage(error: Error) {
6565
}
6666

6767
const [validationError, parsedBody] = safeWrap(() =>
68-
errorCodeResponseSchema.validateSync(body),
68+
errorCodeResponseSchema.parse(body),
6969
);
7070
if (validationError || !parsedBody?.propagate) {
7171
return null;

packages/models/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
"lint": "biome check src"
1414
},
1515
"dependencies": {
16-
"yup": "1.7.1"
16+
"zod": "4.5.4"
1717
},
1818
"devDependencies": {
1919
"typescript": "5.9.3"
Lines changed: 101 additions & 94 deletions
Original file line numberDiff line numberDiff line change
@@ -1,115 +1,122 @@
1-
import * as yup from 'yup';
1+
import { z } from 'zod';
22

3-
export const adminRoomSummarySchema = yup.object({
4-
id: yup.string().required(),
5-
name: yup.string().required(),
6-
userCount: yup.number().required(),
7-
songCount: yup.number().required(),
8-
activeSources: yup.array(yup.string().required()).required(),
9-
hasAdminPassword: yup.boolean().required(),
10-
});
11-
export type AdminRoomSummary = yup.InferType<typeof adminRoomSummarySchema>;
3+
export const adminRoomSummarySchema = z.compile(
4+
z.object({
5+
id: z.string(),
6+
name: z.string(),
7+
userCount: z.number(),
8+
songCount: z.number(),
9+
activeSources: z.array(z.string()),
10+
hasAdminPassword: z.boolean(),
11+
}),
12+
);
13+
export type AdminRoomSummary = z.infer<typeof adminRoomSummarySchema>;
1214

13-
export const adminRoomsSchema = yup.array(adminRoomSummarySchema).required();
14-
export type AdminRooms = yup.InferType<typeof adminRoomsSchema>;
15+
export const adminRoomsSchema = z.compile(z.array(adminRoomSummarySchema));
16+
export type AdminRooms = z.infer<typeof adminRoomsSchema>;
1517

16-
export const adminRoomResultSchema = yup.object({
17-
rooms: adminRoomsSchema,
18-
from: yup.number().required(),
19-
to: yup.number().required(),
20-
total: yup.number().required(),
21-
count: yup.number().required(),
22-
});
23-
export type AdminRoomResult = yup.InferType<typeof adminRoomResultSchema>;
18+
export const adminRoomResultSchema = z.compile(
19+
z.object({
20+
rooms: adminRoomsSchema,
21+
from: z.number(),
22+
to: z.number(),
23+
total: z.number(),
24+
count: z.number(),
25+
}),
26+
);
27+
export type AdminRoomResult = z.infer<typeof adminRoomResultSchema>;
2428

25-
export const adminRoomSearchSchema = yup.object({
26-
q: yup.string().optional(),
27-
sortBy: yup.string().oneOf(['listeners', 'songs']).optional(),
28-
order: yup.string().oneOf(['asc', 'desc']).optional(),
29-
from: yup.number().integer().min(0).optional(),
30-
to: yup.number().integer().min(0).optional(),
31-
});
29+
export const adminRoomSearchSchema = z.compile(
30+
z.object({
31+
q: z.string().optional(),
32+
sortBy: z.enum(['listeners', 'songs']).optional(),
33+
order: z.enum(['asc', 'desc']).optional(),
34+
from: z.int().min(0).optional(),
35+
to: z.int().min(0).optional(),
36+
}),
37+
);
3238

33-
export const adminLoginRequestSchema = yup.object({
34-
username: yup.string().required(),
35-
password: yup.string().required(),
36-
});
37-
export type AdminLoginRequest = yup.InferType<typeof adminLoginRequestSchema>;
39+
export const adminLoginRequestSchema = z.compile(
40+
z.object({ username: z.string(), password: z.string() }),
41+
);
42+
export type AdminLoginRequest = z.infer<typeof adminLoginRequestSchema>;
3843

39-
export const adminUserSchema = yup.object({
40-
id: yup.string().required(),
41-
username: yup.string().required(),
42-
createdAt: yup.string().required(),
43-
updatedAt: yup.string().required(),
44-
});
45-
export type AdminUser = yup.InferType<typeof adminUserSchema>;
44+
export const adminUserSchema = z.compile(
45+
z.object({
46+
id: z.string(),
47+
username: z.string(),
48+
createdAt: z.string(),
49+
updatedAt: z.string(),
50+
}),
51+
);
52+
export type AdminUser = z.infer<typeof adminUserSchema>;
4653

47-
export const adminUsersSchema = yup.array(adminUserSchema).required();
48-
export type AdminUsers = yup.InferType<typeof adminUsersSchema>;
54+
export const adminUsersSchema = z.compile(z.array(adminUserSchema));
55+
export type AdminUsers = z.infer<typeof adminUsersSchema>;
4956

50-
export const adminCreateUserRequestSchema = yup.object({
51-
username: yup.string().required(),
52-
password: yup.string().required(),
53-
});
54-
export type AdminCreateUserRequest = yup.InferType<
57+
export const adminCreateUserRequestSchema = z.compile(
58+
z.object({ username: z.string(), password: z.string() }),
59+
);
60+
export type AdminCreateUserRequest = z.infer<
5561
typeof adminCreateUserRequestSchema
5662
>;
5763

58-
export const adminUpdateUserRequestSchema = yup.object({
59-
password: yup.string().required(),
60-
});
61-
export type AdminUpdateUserRequest = yup.InferType<
64+
export const adminUpdateUserRequestSchema = z.compile(
65+
z.object({ password: z.string() }),
66+
);
67+
export type AdminUpdateUserRequest = z.infer<
6268
typeof adminUpdateUserRequestSchema
6369
>;
6470

65-
export const adminUpdateRoomRequestSchema = yup.object({
66-
name: yup.string().optional(),
67-
clearAdminPassword: yup.boolean().optional(),
68-
});
69-
export type AdminUpdateRoomRequest = yup.InferType<
71+
export const adminUpdateRoomRequestSchema = z.compile(
72+
z.object({
73+
name: z.string().optional(),
74+
clearAdminPassword: z.boolean().optional(),
75+
}),
76+
);
77+
export type AdminUpdateRoomRequest = z.infer<
7078
typeof adminUpdateRoomRequestSchema
7179
>;
7280

73-
export const adminSessionResponseSchema = yup.object({
74-
authorized: yup.boolean().required(),
75-
user: adminUserSchema.optional(),
76-
});
77-
export type AdminSessionResponse = yup.InferType<
78-
typeof adminSessionResponseSchema
79-
>;
81+
export const adminSessionResponseSchema = z.compile(
82+
z.object({ authorized: z.boolean(), user: adminUserSchema.optional() }),
83+
);
84+
export type AdminSessionResponse = z.infer<typeof adminSessionResponseSchema>;
8085

81-
export const adminSearchUsagePointSchema = yup.object({
82-
window: yup.string().oneOf(['hour', 'day']).required(),
83-
timestamp: yup.string().required(),
84-
provider: yup.string().required(),
85-
total: yup.number().required(),
86-
unique: yup.number().required(),
87-
cached: yup.number().required(),
88-
live: yup.number().required(),
89-
});
90-
export type AdminSearchUsagePoint = yup.InferType<
91-
typeof adminSearchUsagePointSchema
92-
>;
86+
export const adminSearchUsagePointSchema = z.compile(
87+
z.object({
88+
window: z.enum(['hour', 'day']),
89+
timestamp: z.string(),
90+
provider: z.string(),
91+
total: z.number(),
92+
unique: z.number(),
93+
cached: z.number(),
94+
live: z.number(),
95+
}),
96+
);
97+
export type AdminSearchUsagePoint = z.infer<typeof adminSearchUsagePointSchema>;
9398

94-
export const adminSearchUsageSchema = yup
95-
.object({
96-
points: yup.array(adminSearchUsagePointSchema).required(),
97-
generatedAt: yup.string().required(),
98-
})
99-
.required();
100-
export type AdminSearchUsage = yup.InferType<typeof adminSearchUsageSchema>;
99+
export const adminSearchUsageSchema = z.compile(
100+
z.object({
101+
points: z.array(adminSearchUsagePointSchema),
102+
generatedAt: z.string(),
103+
}),
104+
);
105+
export type AdminSearchUsage = z.infer<typeof adminSearchUsageSchema>;
101106

102-
export const listenerUsagePointSchema = yup.object({
103-
window: yup.string().oneOf(['hour', 'day', 'week', 'month']).required(),
104-
timestamp: yup.string().required(),
105-
listeners: yup.number().required(),
106-
});
107-
export type ListenerUsagePoint = yup.InferType<typeof listenerUsagePointSchema>;
107+
export const listenerUsagePointSchema = z.compile(
108+
z.object({
109+
window: z.enum(['hour', 'day', 'week', 'month']),
110+
timestamp: z.string(),
111+
listeners: z.number(),
112+
}),
113+
);
114+
export type ListenerUsagePoint = z.infer<typeof listenerUsagePointSchema>;
108115

109-
export const adminListenerUsageSchema = yup
110-
.object({
111-
points: yup.array(listenerUsagePointSchema).required(),
112-
generatedAt: yup.string().required(),
113-
})
114-
.required();
115-
export type AdminListenerUsage = yup.InferType<typeof adminListenerUsageSchema>;
116+
export const adminListenerUsageSchema = z.compile(
117+
z.object({
118+
points: z.array(listenerUsagePointSchema),
119+
generatedAt: z.string(),
120+
}),
121+
);
122+
export type AdminListenerUsage = z.infer<typeof adminListenerUsageSchema>;
Lines changed: 7 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,9 @@
1-
import * as yup from 'yup';
1+
import { z } from 'zod';
22

3-
export const authorizationsListSchema = yup
4-
.array(yup.string().required())
5-
.required();
6-
export type AuthorizationsList = yup.InferType<typeof authorizationsListSchema>;
3+
export const authorizationsListSchema = z.compile(z.array(z.string()));
4+
export type AuthorizationsList = z.infer<typeof authorizationsListSchema>;
75

8-
export const providerTokenSchema = yup.object({
9-
accessToken: yup.string().required(),
10-
expiresAt: yup.string().required(),
11-
});
12-
export type ProviderToken = yup.InferType<typeof providerTokenSchema>;
6+
export const providerTokenSchema = z.compile(
7+
z.object({ accessToken: z.string(), expiresAt: z.string() }),
8+
);
9+
export type ProviderToken = z.infer<typeof providerTokenSchema>;

0 commit comments

Comments
 (0)