Skip to content

Commit e1306c5

Browse files
authored
feat(server): add programs and courses schemas and functions (#10)
1 parent f5e8830 commit e1306c5

File tree

11 files changed

+350
-18
lines changed

11 files changed

+350
-18
lines changed

bun.lock

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,7 @@
8282
"name": "@dev-team-fall-25/server",
8383
"dependencies": {
8484
"convex": "^1.27.3",
85+
"convex-helpers": "^0.1.104",
8586
},
8687
"devDependencies": {
8788
"@biomejs/biome": "2.2.4",
@@ -487,6 +488,8 @@
487488

488489
"convex": ["[email protected]", "", { "dependencies": { "esbuild": "0.25.4", "jwt-decode": "^4.0.0", "prettier": "^3.0.0" }, "peerDependencies": { "@auth0/auth0-react": "^2.0.1", "@clerk/clerk-react": "^4.12.8 || ^5.0.0", "react": "^18.0.0 || ^19.0.0-0 || ^19.0.0" }, "optionalPeers": ["@auth0/auth0-react", "@clerk/clerk-react", "react"], "bin": { "convex": "bin/main.js" } }, "sha512-Ebr9lPgXkL7JO5IFr3bG+gYvHskyJjc96Fx0BBNkJUDXrR/bd9/uI4q8QszbglK75XfDu068vR0d/HK2T7tB9Q=="],
489490

491+
"convex-helpers": ["[email protected]", "", { "peerDependencies": { "@standard-schema/spec": "^1.0.0", "convex": "^1.24.0", "hono": "^4.0.5", "react": "^17.0.2 || ^18.0.0 || ^19.0.0", "typescript": "^5.5", "zod": "^3.22.4 || ^4.0.15" }, "optionalPeers": ["@standard-schema/spec", "hono", "react", "typescript", "zod"], "bin": { "convex-helpers": "bin.cjs" } }, "sha512-7CYvx7T3K6n+McDTK4ZQaQNNGBzq5aWezpjzsKbOxPXx7oNcTP9wrpef3JxeXWFzkByJv5hRCjseh9B7eNJ7Ig=="],
492+
490493
"cookie": ["[email protected]", "", {}, "sha512-9Kr/j4O16ISv8zBBhJoi4bXOYNTkFLOqSL3UDB0njXxCXNezjeyVrJyGOWtgfs/q2km1gwBcfH8q1yEGoMYunA=="],
491494

492495
"core-util-is": ["[email protected]", "", {}, "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ=="],
@@ -805,6 +808,8 @@
805808

806809
"@cspotcode/source-map-support/@jridgewell/trace-mapping": ["@jridgewell/[email protected]", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.0.3", "@jridgewell/sourcemap-codec": "^1.4.10" } }, "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ=="],
807810

811+
"@dev-team-fall-25/server/@types/bun": ["@types/[email protected]", "", { "dependencies": { "bun-types": "1.2.23" } }, "sha512-le8ueOY5b6VKYf19xT3McVbXqLqmxzPXHsQT/q9JHgikJ2X22wyTW3g3ohz2ZMnp7dod6aduIiq8A14Xyimm0A=="],
812+
808813
"@rollup/pluginutils/picomatch": ["[email protected]", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="],
809814

810815
"@tailwindcss/oxide-wasm32-wasi/@emnapi/core": ["@emnapi/[email protected]", "", { "dependencies": { "@emnapi/wasi-threads": "1.1.0", "tslib": "^2.4.0" }, "bundled": true }, "sha512-sbP8GzB1WDzacS8fgNPpHlp6C9VZe+SJP3F90W9rLemaQj2PzIuTEl1qDOYQf58YIpyjViI24y9aPWCjEzY2cg=="],
@@ -849,6 +854,8 @@
849854

850855
"web/@types/node": ["@types/[email protected]", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-gfehUI8N1z92kygssiuWvLiwcbOB3IRktR6hTDgJlXMYh5OvkPSRmgfoBUmfZt+vhwJtX7v1Yw4KvvAf7c5QKQ=="],
851856

857+
"@dev-team-fall-25/server/@types/bun/bun-types": ["[email protected]", "", { "dependencies": { "@types/node": "*" }, "peerDependencies": { "@types/react": "^19" } }, "sha512-R9f0hKAZXgFU3mlrA0YpE/fiDvwV0FT9rORApt2aQVWSuJDzZOyB5QLc0N/4HF57CS8IXJ6+L5E4W1bW6NS2Aw=="],
858+
852859
"bun-types/@types/node/undici-types": ["[email protected]", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="],
853860

854861
"miniflare/sharp/@img/sharp-darwin-arm64": ["@img/[email protected]", "", { "optionalDependencies": { "@img/sharp-libvips-darwin-arm64": "1.0.4" }, "os": "darwin", "cpu": "arm64" }, "sha512-UT4p+iz/2H4twwAoLCqfA9UH5pI6DggwKEGuaPy7nCVQ8ZsiY5PIcrRvD1DzuY3qYL07NtIQcWnBSY/heikIFQ=="],

packages/server/convex/_generated/api.d.ts

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,12 @@ import type {
1313
FilterApi,
1414
FunctionReference,
1515
} from "convex/server";
16+
import type * as courses from "../courses.js";
17+
import type * as prerequisites from "../prerequisites.js";
18+
import type * as programs from "../programs.js";
19+
import type * as requirements from "../requirements.js";
20+
import type * as schemas_courses from "../schemas/courses.js";
21+
import type * as schemas_programs from "../schemas/programs.js";
1622

1723
/**
1824
* A utility for referencing Convex functions in your app's API.
@@ -22,7 +28,14 @@ import type {
2228
* const myFunctionReference = api.myModule.myFunction;
2329
* ```
2430
*/
25-
declare const fullApi: ApiFromModules<{}>;
31+
declare const fullApi: ApiFromModules<{
32+
courses: typeof courses;
33+
prerequisites: typeof prerequisites;
34+
programs: typeof programs;
35+
requirements: typeof requirements;
36+
"schemas/courses": typeof schemas_courses;
37+
"schemas/programs": typeof schemas_programs;
38+
}>;
2639
export declare const api: FilterApi<
2740
typeof fullApi,
2841
FunctionReference<any, "public">

packages/server/convex/_generated/dataModel.d.ts

Lines changed: 18 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -8,29 +8,29 @@
88
* @module
99
*/
1010

11-
import { AnyDataModel } from "convex/server";
11+
import type {
12+
DataModelFromSchemaDefinition,
13+
DocumentByName,
14+
TableNamesInDataModel,
15+
SystemTableNames,
16+
} from "convex/server";
1217
import type { GenericId } from "convex/values";
13-
14-
/**
15-
* No `schema.ts` file found!
16-
*
17-
* This generated code has permissive types like `Doc = any` because
18-
* Convex doesn't know your schema. If you'd like more type safety, see
19-
* https://docs.convex.dev/using/schemas for instructions on how to add a
20-
* schema file.
21-
*
22-
* After you change a schema, rerun codegen with `npx convex dev`.
23-
*/
18+
import schema from "../schema.js";
2419

2520
/**
2621
* The names of all of your Convex tables.
2722
*/
28-
export type TableNames = string;
23+
export type TableNames = TableNamesInDataModel<DataModel>;
2924

3025
/**
3126
* The type of a document stored in Convex.
27+
*
28+
* @typeParam TableName - A string literal type of the table name (like "users").
3229
*/
33-
export type Doc = any;
30+
export type Doc<TableName extends TableNames> = DocumentByName<
31+
DataModel,
32+
TableName
33+
>;
3434

3535
/**
3636
* An identifier for a document in Convex.
@@ -42,8 +42,10 @@ export type Doc = any;
4242
*
4343
* IDs are just strings at runtime, but this type can be used to distinguish them from other
4444
* strings when type checking.
45+
*
46+
* @typeParam TableName - A string literal type of the table name (like "users").
4547
*/
46-
export type Id<TableName extends TableNames = TableNames> =
48+
export type Id<TableName extends TableNames | SystemTableNames> =
4749
GenericId<TableName>;
4850

4951
/**
@@ -55,4 +57,4 @@ export type Id<TableName extends TableNames = TableNames> =
5557
* This type is used to parameterize methods like `queryGeneric` and
5658
* `mutationGeneric` to make them type-safe.
5759
*/
58-
export type DataModel = AnyDataModel;
60+
export type DataModel = DataModelFromSchemaDefinition<typeof schema>;

packages/server/convex/courses.ts

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
import { v } from "convex/values";
2+
import { partial } from "convex-helpers/validators";
3+
import { mutation, query } from "./_generated/server";
4+
import { courses } from "./schemas/courses";
5+
6+
export const getCourseById = query({
7+
args: { id: v.id("courses") },
8+
handler: async (ctx, args) => {
9+
return await ctx.db.get(args.id);
10+
},
11+
});
12+
13+
export const getCourseByCode = query({
14+
args: { code: v.string() },
15+
handler: async (ctx, args) => {
16+
return await ctx.db
17+
.query("courses")
18+
.withIndex("by_course_code", (q) => q.eq("code", args.code))
19+
.unique();
20+
},
21+
});
22+
23+
export const deleteCourse = mutation({
24+
args: { id: v.id("courses") },
25+
handler: async (ctx, args) => {
26+
await ctx.db.delete(args.id);
27+
},
28+
});
29+
30+
export const upsertCourse = mutation({
31+
args: courses,
32+
handler: async (ctx, args) => {
33+
const existing = await ctx.db
34+
.query("courses")
35+
.withIndex("by_course_code", (q) => q.eq("code", args.code))
36+
.unique();
37+
38+
if (existing) {
39+
return await ctx.db.patch(existing._id, args);
40+
} else {
41+
return await ctx.db.insert("courses", args);
42+
}
43+
},
44+
});
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
import { v } from "convex/values";
2+
import { mutation, query } from "./_generated/server";
3+
import { prerequisites } from "./schemas/courses";
4+
5+
export const getPrerequisite = query({
6+
args: { id: v.id("prerequisites") },
7+
handler: async (ctx, args) => {
8+
return await ctx.db.get(args.id);
9+
},
10+
});
11+
12+
export const getPrerequisitesByCourse = query({
13+
args: { courseId: v.id("courses") },
14+
handler: async (ctx, args) => {
15+
return await ctx.db
16+
.query("prerequisites")
17+
.withIndex("by_course", (q) => q.eq("courseId", args.courseId))
18+
.collect();
19+
},
20+
});
21+
22+
export const createPrerequisite = mutation({
23+
args: {
24+
courseId: v.id("courses"),
25+
type: v.union(
26+
v.literal("required"),
27+
v.literal("alternative"),
28+
v.literal("options"),
29+
),
30+
courses: v.array(v.string()),
31+
creditsRequired: v.optional(v.int64()),
32+
},
33+
handler: async (ctx, args) => {
34+
if (args.type === "options") {
35+
if (args.creditsRequired === undefined) {
36+
throw new Error("creditsRequired is required for options type");
37+
}
38+
return await ctx.db.insert("prerequisites", {
39+
courseId: args.courseId,
40+
type: args.type,
41+
courses: args.courses,
42+
creditsRequired: args.creditsRequired,
43+
});
44+
} else {
45+
return await ctx.db.insert("prerequisites", {
46+
courseId: args.courseId,
47+
type: args.type,
48+
courses: args.courses,
49+
});
50+
}
51+
},
52+
});
53+
54+
export const deletePrerequisite = mutation({
55+
args: { id: v.id("prerequisites") },
56+
handler: async (ctx, args) => {
57+
await ctx.db.delete(args.id);
58+
},
59+
});
60+
61+
export const deletePrerequisitesByCourse = mutation({
62+
args: { courseId: v.id("courses") },
63+
handler: async (ctx, args) => {
64+
const prerequisitesToDelete = await ctx.db
65+
.query("prerequisites")
66+
.withIndex("by_course", (q) => q.eq("courseId", args.courseId))
67+
.collect();
68+
69+
for (const prerequisite of prerequisitesToDelete) {
70+
await ctx.db.delete(prerequisite._id);
71+
}
72+
},
73+
});

packages/server/convex/programs.ts

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
import { v } from "convex/values";
2+
import { mutation, query } from "./_generated/server";
3+
import { programs } from "./schemas/programs";
4+
5+
export const getProgram = query({
6+
args: { id: v.id("programs") },
7+
handler: async (ctx, args) => {
8+
return await ctx.db.get(args.id);
9+
},
10+
});
11+
12+
export const getProgramByName = query({
13+
args: { name: v.string() },
14+
handler: async (ctx, args) => {
15+
return await ctx.db
16+
.query("programs")
17+
.withIndex("by_program_name", (q) => q.eq("name", args.name))
18+
.unique();
19+
},
20+
});
21+
22+
export const deleteProgram = mutation({
23+
args: { id: v.id("programs") },
24+
handler: async (ctx, args) => {
25+
await ctx.db.delete(args.id);
26+
},
27+
});
28+
29+
export const upsertProgram = mutation({
30+
args: programs,
31+
handler: async (ctx, args) => {
32+
const existing = await ctx.db
33+
.query("programs")
34+
.withIndex("by_program_name", (q) => q.eq("name", args.name))
35+
.unique();
36+
37+
if (existing) {
38+
return await ctx.db.patch(existing._id, args);
39+
} else {
40+
return await ctx.db.insert("programs", args);
41+
}
42+
},
43+
});
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
import { v } from "convex/values";
2+
import { mutation, query } from "./_generated/server";
3+
4+
export const getRequirement = query({
5+
args: { id: v.id("requirements") },
6+
handler: async (ctx, args) => {
7+
return await ctx.db.get(args.id);
8+
},
9+
});
10+
11+
export const getRequirementsByProgram = query({
12+
args: { programId: v.id("programs") },
13+
handler: async (ctx, args) => {
14+
return await ctx.db
15+
.query("requirements")
16+
.withIndex("by_program", (q) => q.eq("programId", args.programId))
17+
.collect();
18+
},
19+
});
20+
21+
export const createRequirement = mutation({
22+
args: {
23+
programId: v.id("programs"),
24+
isMajor: v.boolean(),
25+
type: v.union(
26+
v.literal("required"),
27+
v.literal("alternative"),
28+
v.literal("options"),
29+
),
30+
courses: v.array(v.string()),
31+
creditsRequired: v.optional(v.int64()),
32+
},
33+
handler: async (ctx, args) => {
34+
if (args.type === "options") {
35+
if (args.creditsRequired === undefined) {
36+
throw new Error("creditsRequired is required for options type");
37+
}
38+
return await ctx.db.insert("requirements", {
39+
programId: args.programId,
40+
isMajor: args.isMajor,
41+
type: args.type,
42+
courses: args.courses,
43+
creditsRequired: args.creditsRequired,
44+
});
45+
} else {
46+
return await ctx.db.insert("requirements", {
47+
programId: args.programId,
48+
isMajor: args.isMajor,
49+
type: args.type,
50+
courses: args.courses,
51+
});
52+
}
53+
},
54+
});
55+
56+
export const deleteRequirement = mutation({
57+
args: { id: v.id("requirements") },
58+
handler: async (ctx, args) => {
59+
await ctx.db.delete(args.id);
60+
},
61+
});
62+
63+
export const deleteRequirementsByProgram = mutation({
64+
args: { programId: v.id("programs") },
65+
handler: async (ctx, args) => {
66+
const requirementsToDelete = await ctx.db
67+
.query("requirements")
68+
.withIndex("by_program", (q) => q.eq("programId", args.programId))
69+
.collect();
70+
71+
for (const requirement of requirementsToDelete) {
72+
await ctx.db.delete(requirement._id);
73+
}
74+
},
75+
});

packages/server/convex/schema.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
import { defineSchema, defineTable } from "convex/server";
2+
import { courses, prerequisites } from "./schemas/courses";
3+
import { programs, requirements } from "./schemas/programs";
4+
5+
export default defineSchema({
6+
programs: defineTable(programs).index("by_program_name", ["name"]),
7+
requirements: defineTable(requirements)
8+
.index("by_program", ["programId"])
9+
.index("by_program_and_type", ["programId", "isMajor"]),
10+
courses: defineTable(courses).index("by_course_code", ["code"]),
11+
prerequisites: defineTable(prerequisites).index("by_course", ["courseId"]),
12+
});

0 commit comments

Comments
 (0)