Skip to content

Commit 449473f

Browse files
committed
Setup database, zod schema
1 parent f217eaf commit 449473f

11 files changed

Lines changed: 199 additions & 3 deletions

File tree

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,3 +25,4 @@ vite.config.ts.timestamp-*
2525
# Paraglide
2626
src/lib/paraglide
2727
project.inlang/cache/
28+
docker-compose.yml

bun.lock

Lines changed: 6 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
"clsx": "^2.1.1",
1717
"embla-carousel-svelte": "^8.6.0",
1818
"mode-watcher": "^1.1.0",
19+
"postgres": "^3.4.8",
1920
"svelte": "^5.45.6",
2021
"svelte-check": "^4.3.4",
2122
"svelte-sonner": "^1.0.7",
@@ -24,7 +25,8 @@
2425
"tailwindcss": "^4.1.17",
2526
"tw-animate-css": "^1.4.0",
2627
"typescript": "^5.9.3",
27-
"vite": "^7.2.6"
28+
"vite": "^7.2.6",
29+
"zod": "^4.3.5"
2830
},
2931
"private": true,
3032
"scripts": {

src/lib/env/private.ts

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
import { env } from "$env/dynamic/private";
2+
import z from "zod";
3+
4+
export const privateEnv = z
5+
.object({
6+
/** Postgres connection string. */
7+
DATABASE_URL: z.url(),
8+
/** JWT access token secrets. */
9+
JWT_ACCESS_SECRET: z.string(),
10+
/** Runtime environment. Currently only changes whether secure cookies are used. */
11+
NODE_ENV: z.enum(["development", "production", "test"]),
12+
/** If the specified user does not exist on startup, it will be created with the specified email and `INIT_PASSWORD`. */
13+
INIT_EMAIL: z.string().optional(),
14+
/** If the specified user does not exist on startup, it will be created with the specified password and `NIT_USERNAME`. */
15+
INIT_PASSWORD: z.string().optional(),
16+
})
17+
.transform(env => {
18+
const {
19+
JWT_ACCESS_SECRET,
20+
INIT_EMAIL,
21+
INIT_PASSWORD,
22+
...rest
23+
} = env;
24+
25+
return {
26+
...rest,
27+
...{
28+
jwtSecrets: {
29+
access: JWT_ACCESS_SECRET,
30+
},
31+
},
32+
};
33+
})
34+
.parse(env);

src/lib/env/public.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
import { env } from "$env/dynamic/public";
2+
import z from "zod";
3+
4+
export const publicEnv = z
5+
.object({
6+
/** Origin used in outbound emails. */
7+
PUBLIC_BASE_URL: z.url().default("http://localhost:5173"),
8+
})
9+
.transform(env => {
10+
const { PUBLIC_BASE_URL, ...rest } = env;
11+
12+
return {
13+
BASE_URL: PUBLIC_BASE_URL,
14+
...rest,
15+
};
16+
})
17+
.parse(env);

src/lib/models.ts

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
import { z } from "zod";
2+
3+
export const Password = z
4+
.string()
5+
.min(8, "Password must be at least 8 characters.")
6+
.max(70, "Password must be at most 70 characters.")
7+
.refine(val => /[A-Z]/.test(val), {
8+
error: "Password must contain at least one uppercase letter.",
9+
})
10+
.refine(val => /[0-9]/.test(val), {
11+
error: "Password must contain at least one number.",
12+
})
13+
.refine(val => /[^A-Za-z0-9].*[^A-Za-z0-9]/.test(val), {
14+
error: "Password must contain at least two special characters.",
15+
});
16+
export type Password = z.infer<typeof Password>;
17+
18+
export const User = z.object({
19+
id: z.uuid(),
20+
email: z.email(),
21+
name: z.string().max(100),
22+
role: z.enum(["student", "faculty", "admin"])
23+
});
24+
export type User = z.infer<typeof User>;
25+
26+
export const Term = z.object({
27+
id: z.uuid(),
28+
code: z.string().max(3),
29+
starts_at: z.iso.datetime(),
30+
ends_at: z.iso.datetime(),
31+
});
32+
export type Term = z.infer<typeof Term>;
33+
34+
export const Course = z.object({
35+
id: z.uuid(),
36+
course_code: z.string().max(20),
37+
course_name: z.string().max(100),
38+
description: z.string().max(500).nullable(),
39+
});
40+
export type Course = z.infer<typeof Course>;
41+
42+
export const Section = z.object({
43+
id: z.uuid(),
44+
code: z.string().length(5),
45+
instructor_id: z.uuid().nullable(),
46+
course_id: z.uuid(),
47+
term_id: z.uuid(),
48+
});
49+
export type Section = z.infer<typeof Section>;

src/lib/server/postgres.ts

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import { privateEnv } from "$lib/env/private";
2+
import { error } from "@sveltejs/kit";
3+
import postgres from "postgres";
4+
5+
export const db = postgres(privateEnv.DATABASE_URL, {
6+
//ssl: "require",
7+
connect_timeout: 60,
8+
});
9+
10+
export const sql = async (...args: Parameters<typeof db>) => {
11+
try {
12+
return await db(...args);
13+
} catch (err: unknown) {
14+
const e = err as any;
15+
const isConnRefused =
16+
err instanceof AggregateError
17+
? Array.isArray(err.errors) && err.errors.some((inner: any) => inner?.code === "ECONNREFUSED")
18+
: e?.code === "ECONNREFUSED" || String(e?.message).includes("ECONNREFUSED");
19+
20+
if (isConnRefused) error(503, { message: "Backend unavailable" });
21+
throw err;
22+
}
23+
};

src/lib/server/utils.ts

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
import { Term, User } from "$lib/models";
2+
import { sql } from "./postgres";
3+
4+
export async function getActiveTerm(getNextIfNone = false): Promise<Term | undefined> {
5+
return undefined;
6+
}
7+
8+
export async function getNextTerm(): Promise<Term | undefined> {
9+
return undefined;
10+
}
11+
12+
export async function getLastTerm(): Promise<Term | undefined> {
13+
// Get the most recent term that has ended
14+
return undefined;
15+
}
16+
17+
export async function createUser(
18+
email: string,
19+
name: string,
20+
role: "student" | "faculty" | "admin",
21+
password_hash?: string,
22+
): Promise<User | undefined> {
23+
return undefined;
24+
}
25+
26+
export async function updateUserPassword(email: string, password_hash: string) {
27+
await sql`
28+
UPDATE users
29+
SET password_hash = ${password_hash}
30+
WHERE email = ${email}
31+
`;
32+
}
33+
34+
export async function userExists(student_id: string, email: string): Promise<boolean> {
35+
const result = await sql`
36+
SELECT student_id
37+
FROM users
38+
WHERE student_id = ${student_id}
39+
OR email = ${email}
40+
LIMIT 1
41+
`;
42+
43+
return result.count === 1;
44+
}

src/routes/+page.server.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
import type { PageServerLoad } from './$types';
2+
3+
export const load: PageServerLoad = async ({ params }) => {
4+
const msg = "hi"
5+
return {
6+
message: msg
7+
}
8+
};

src/routes/+page.svelte

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
11
<script lang="ts">
2-
2+
import type { PageProps } from "./$types";
3+
const { data }: PageProps = $props();
4+
const { message } = data;
35
</script>
46

57
<h1>Welcome to SvelteKit</h1>
6-
<p>Visit <a href="https://svelte.dev/docs/kit">svelte.dev/docs/kit</a> to read the documentation</p>
8+
<p>Visit <a href="https://svelte.dev/docs/kit">svelte.dev/docs/kit</a> to read the documentation</p>
9+
10+
<h1 class="text-9xl text-black">this message says: {message}</h1>

0 commit comments

Comments
 (0)