Skip to content

Commit d0741f7

Browse files
committed
implements my own safeparse
1 parent 0532720 commit d0741f7

File tree

7 files changed

+112
-84
lines changed

7 files changed

+112
-84
lines changed

package.json

Lines changed: 18 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -12,27 +12,27 @@
1212
"postinstall": "prisma generate"
1313
},
1414
"dependencies": {
15-
"@elysiajs/eden": "^1.1.3",
16-
"@prisma/client": "^6.0.1",
17-
"@tanstack/react-query": "^5.62.3",
18-
"elysia": "^1.1.26",
19-
"jose": "^5.9.6",
20-
"next": "15.0.4",
15+
"@elysiajs/eden": "1.2.0",
16+
"@prisma/client": "6.1.0",
17+
"@tanstack/react-query": "5.62.10",
18+
"elysia": "1.2.6",
19+
"jose": "5.9.6",
20+
"next": "15.1.2",
2121
"react": "19.0.0",
2222
"react-dom": "19.0.0"
2323
},
2424
"devDependencies": {
25-
"@types/bun": "^1.1.14",
26-
"@types/node": "^22.10.1",
27-
"@types/react": "^19",
28-
"@types/react-dom": "^19",
29-
"eslint": "^9.15.0",
30-
"eslint-config-next": "15.0.4",
31-
"postcss": "^8",
32-
"prettier": "^3.4.2",
33-
"prettier-plugin-tailwindcss": "^0.6.9",
34-
"prisma": "^6.0.1",
35-
"tailwindcss": "^3.4.16",
36-
"typescript": "^5"
25+
"@types/bun": "1.1.14",
26+
"@types/node": "22.10.2",
27+
"@types/react": "19.0.2",
28+
"@types/react-dom": "19.0.2",
29+
"eslint": "9.17.0",
30+
"eslint-config-next": "15.1.2",
31+
"postcss": "8.4.49",
32+
"prettier": "3.4.2",
33+
"prettier-plugin-tailwindcss": "0.6.9",
34+
"prisma": "6.1.0",
35+
"tailwindcss": "3.4.17",
36+
"typescript": "5.7.2"
3737
}
3838
}

src/app/(auth)/login/page.tsx

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,11 @@
11
"use client";
22

33
import { AuthHook } from "@/components/hooks/auth-hook";
4-
import { authenticationSchema, authSchemas } from "@/lib/typebox/auth";
4+
import {
5+
authenticationChecker,
6+
authenticationSchema,
7+
} from "@/lib/typebox/auth";
8+
import { safeParse } from "@/utils/base";
59
import Link from "next/link";
610
import { useRouter } from "next/navigation";
711
import { FormEvent, useState } from "react";
@@ -60,7 +64,7 @@ export default function LoginPage(props: LoginPageProps) {
6064
type="submit"
6165
disabled={
6266
loginMutation.isPending ||
63-
!authSchemas.authUser.safeParse({
67+
!safeParse(authenticationChecker, {
6468
username,
6569
password,
6670
}).success

src/app/(auth)/register/page.tsx

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,11 @@
11
"use client";
22

33
import { AuthHook } from "@/components/hooks/auth-hook";
4-
import { authenticationSchema, authSchemas } from "@/lib/typebox/auth";
4+
import {
5+
authenticationChecker,
6+
authenticationSchema,
7+
} from "@/lib/typebox/auth";
8+
import { safeParse } from "@/utils/base";
59
import Link from "next/link";
610
import { useRouter } from "next/navigation";
711
import { FormEvent, useState } from "react";
@@ -58,7 +62,7 @@ export default function RegisterPage() {
5862
type="submit"
5963
disabled={
6064
registerMutation.isPending ||
61-
!authSchemas.authUser.safeParse({
65+
!safeParse(authenticationChecker, {
6266
username,
6367
password,
6468
}).success

src/lib/typebox/auth.ts

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
1+
import { TypeCompiler } from "@sinclair/typebox/compiler";
12
import { Type as t, type Static } from "@sinclair/typebox/type";
2-
import Elysia from "elysia";
3-
43
/**
54
* TypeBox schema Can be used for elysia body params or query schema and in frontend form validation.
65
*/
@@ -10,10 +9,7 @@ export const authenticationSchema = t.Object({
109
test: t.Optional(t.String({ minLength: 1, maxLength: 128 })),
1110
});
1211

13-
export const { models: authSchemas } = new Elysia().model({
14-
authUser: authenticationSchema,
15-
});
16-
12+
export const authenticationChecker = TypeCompiler.Compile(authenticationSchema);
1713
/**
1814
* TypeScript type derived from the authUser schema.
1915
*/

src/utils/base.ts

Lines changed: 42 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,16 @@
11
import type { EdenFetchError } from "@elysiajs/eden/dist/errors";
2-
import { Errors } from "@sinclair/typebox/errors";
2+
import { TypeCompiler } from "@sinclair/typebox/compiler";
33
import type { Static, TSchema } from "@sinclair/typebox/type";
4-
import { Check } from "@sinclair/typebox/value";
54

65
/**
7-
* Parses a value against a TypeBox schema and throws an error if invalid.
8-
* @param schema The TypeBox schema to validate against.
9-
* @param value The value to validate.
10-
* @returns The validated value.
11-
* @throws Error if the value is invalid according to the schema.
12-
*/
13-
export const parse = <T extends TSchema>(
14-
schema: T,
15-
value: unknown,
16-
): Static<T> => {
17-
const check = Check(schema, value);
18-
if (!check) throw new Error(Errors(schema, value).First()?.message);
19-
return value;
20-
};
21-
22-
/**
23-
* Simplifies the response from Elysia tRP for `tanstack-query`, extracting the DATA or throwing an error.
6+
* Helper to handle Eden.js API responses for use with TanStack Query.
7+
* Takes an Eden response object and either:
8+
* - Returns the data if successful
9+
* - Throws the error if unsuccessful
10+
*
11+
* @param response The Eden response object containing data or error
12+
* @returns The response data of type T
13+
* @throws EdenFetchError if the response contains an error
2414
*/
2515
export function handleEden<T>(
2616
response: (
@@ -41,3 +31,36 @@ export function handleEden<T>(
4131
if (response.error) throw response.error;
4232
return response.data;
4333
}
34+
35+
/**
36+
* Safe parsing utility for TypeBox schemas that returns a discriminated union result
37+
* rather than throwing errors. Similar to Zod's safeParse pattern.
38+
*
39+
* @param checker A compiled TypeBox schema checker
40+
* @param value The value to validate
41+
* @returns An object with either:
42+
* - {success: true, data: validatedValue} if validation succeeds
43+
* - {success: false, errors: [{message: string}]} if validation fails
44+
*/
45+
export function safeParse<T extends TSchema>(
46+
checker: ReturnType<typeof TypeCompiler.Compile<T>>,
47+
value: unknown,
48+
):
49+
| { success: true; data: Static<T> }
50+
| { success: false; errors: { message: string }[] } {
51+
const isValid = checker.Check(value);
52+
53+
if (isValid) {
54+
return {
55+
success: true,
56+
data: value as Static<T>,
57+
};
58+
}
59+
60+
return {
61+
success: false,
62+
errors: Array.from(checker.Errors(value)).map((error) => ({
63+
message: error.message,
64+
})),
65+
};
66+
}

src/utils/env/client/index.ts

Lines changed: 15 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,28 +1,28 @@
1-
import { Elysia, t } from "elysia";
1+
import { safeParse } from "@/utils/base";
2+
import { Type as t } from "@sinclair/typebox";
3+
import { TypeCompiler } from "@sinclair/typebox/compiler";
24

3-
/** Schema for client-side environment variables in typebox */
4-
const {
5-
models: { clientSchema },
6-
} = new Elysia().model({
7-
clientSchema: t.Object({
8-
URL: t.String({
9-
minLength: 1,
10-
error: "URL client environment variable is not set!",
11-
}),
5+
const clientSchema = t.Object({
6+
URL: t.String({
7+
minLength: 1,
8+
error: "URL client environment variable is not set!",
129
}),
1310
});
1411

15-
const clientEnvResult = clientSchema.safeParse({
12+
const clientSchemaChecker = TypeCompiler.Compile(clientSchema);
13+
14+
const clientEnvResult = safeParse(clientSchemaChecker, {
1615
URL: process.env.NEXT_PUBLIC_URL,
1716
});
1817

19-
if (!clientEnvResult.data) {
18+
if (!clientEnvResult.success) {
2019
const firstError = clientEnvResult.errors[0];
21-
if (firstError)
20+
if (firstError) {
2221
throw new Error(
23-
`Invalid client environment variable ${firstError.path.slice(1)}: ${firstError.summary.replaceAll(" ", " ")}`,
22+
`Invalid client environment variable: ${firstError.message}`,
2423
);
25-
else throw new Error(`Invalid client environment ${clientEnvResult.error}`);
24+
}
25+
throw new Error(`Invalid client environment validation failed`);
2626
}
2727

2828
export const clientEnv = clientEnvResult.data;

src/utils/env/server/index.ts

Lines changed: 23 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,25 @@
1+
import { safeParse } from "@/utils/base";
12
import { SERVER_URL_KEY } from "@/utils/constants";
2-
import { Elysia, t } from "elysia";
3+
import { Type as t } from "@sinclair/typebox";
4+
import { TypeCompiler } from "@sinclair/typebox/compiler";
35

4-
const {
5-
models: { serverSchema },
6-
} = new Elysia().model({
7-
serverSchema: t.Object({
8-
DATABASE_URL: t.String({ minLength: 1, error: "DATABASE_URL not set!" }),
9-
SECRET: t.String({ minLength: 1, error: "SECRET not set!" }),
10-
NODE_ENV: t.Union(
11-
[t.Literal("development"), t.Literal("test"), t.Literal("production")],
12-
{
13-
error: "NODE_ENV not set!",
14-
},
15-
),
16-
AUTH_COOKIE: t.Literal("auth", { error: "AUTH_COOKIE not set!" }),
17-
SERVER_URL_KEY: t.Literal(SERVER_URL_KEY, { error: "SERVER_URL not set!" }),
18-
SEVEN_DAYS: t.Integer({ minimum: 1, error: "SEVEN_DAYS not set!" }),
19-
}),
6+
const serverSchema = t.Object({
7+
DATABASE_URL: t.String({ minLength: 1, error: "DATABASE_URL not set!" }),
8+
SECRET: t.String({ minLength: 1, error: "SECRET not set!" }),
9+
NODE_ENV: t.Union(
10+
[t.Literal("development"), t.Literal("test"), t.Literal("production")],
11+
{
12+
error: "NODE_ENV not set!",
13+
},
14+
),
15+
AUTH_COOKIE: t.Literal("auth", { error: "AUTH_COOKIE not set!" }),
16+
SERVER_URL_KEY: t.Literal(SERVER_URL_KEY, { error: "SERVER_URL not set!" }),
17+
SEVEN_DAYS: t.Integer({ minimum: 1, error: "SEVEN_DAYS not set!" }),
2018
});
2119

22-
const serverEnvResult = serverSchema.safeParse({
20+
const serverSchemaChecker = TypeCompiler.Compile(serverSchema);
21+
22+
const serverEnvResult = safeParse(serverSchemaChecker, {
2323
DATABASE_URL: process.env.DATABASE_URL,
2424
SECRET: process.env.SECRET,
2525
NODE_ENV: process.env.NODE_ENV,
@@ -28,13 +28,14 @@ const serverEnvResult = serverSchema.safeParse({
2828
SEVEN_DAYS: 60 * 60 * 24 * 7, // 7 days in seconds
2929
});
3030

31-
if (!serverEnvResult.data) {
31+
if (!serverEnvResult.success) {
3232
const firstError = serverEnvResult.errors[0];
33-
if (firstError)
33+
if (firstError) {
3434
throw new Error(
35-
`Invalid server environment variable ${firstError.path.slice(1)}: ${firstError.summary.replaceAll(" ", " ")}`,
35+
`Invalid server environment variable: ${firstError.message}`,
3636
);
37-
else throw new Error(`Invalid server environment ${serverEnvResult.error}`);
37+
}
38+
throw new Error(`Invalid server environment validation failed`);
3839
}
3940

4041
export const serverEnv = serverEnvResult.data;

0 commit comments

Comments
 (0)