From b6bc0b81fc55e1f5269425f0f27e2c6385cb7905 Mon Sep 17 00:00:00 2001 From: Vegard Kyrkjedelen Hovland Date: Mon, 10 Nov 2025 17:47:02 +0100 Subject: [PATCH 01/13] g v-327 Add jsonschema parsing Added utilityfunctions for parsing jsonschemas --- lib/json-schema.ts | 55 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 55 insertions(+) create mode 100644 lib/json-schema.ts diff --git a/lib/json-schema.ts b/lib/json-schema.ts new file mode 100644 index 0000000..31fbc0d --- /dev/null +++ b/lib/json-schema.ts @@ -0,0 +1,55 @@ +import Ajv, { type JSONSchemaType, type ErrorObject } from "ajv"; +import z from "zod"; + +type JsonSchemaResult = + | { + success: true; + } + | { + success: false; + error: ErrorObject[]; + }; + +const ajv = new Ajv(); + +export function validateJsonSchema( + schema: JSONSchemaType, + data: Json, +): JsonSchemaResult { + const validator = ajv.compile(schema); + const isValid = validator(data); + if (isValid) { + return { success: true }; + } + return { + success: false, + error: validator.errors === undefined || + validator.errors === null ? [] : validator.errors, + }; +} + + +export function turnJsonIntoZodSchema( + schema: JSONSchemaType +) { + return z.object({}).passthrough().superRefine((data, ctx) => { + let validationResult = validateJsonSchema(schema, data); + if (!validationResult.success) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: "The interview schema is not valid", + params: validationResult.error, + }); + } + + return validationResult.success; + }); + +} + +// from: https://www.reddit.com/r/typescript/comments/13mssvc/types_for_json_and_writing_json +type JsonPrimative = string | number | boolean | null; +type JsonArray = Json[]; +type JsonObject = { [key: string]: Json }; +type JsonComposite = JsonArray | JsonObject; +export type Json = JsonPrimative | JsonComposite; From 71d3048e1737d7ff4ce2739344375c1f8de013f6 Mon Sep 17 00:00:00 2001 From: Vegard Kyrkjedelen Hovland Date: Mon, 10 Nov 2025 18:07:42 +0100 Subject: [PATCH 02/13] vf-327 Add tables to database for interviews Added drizzle schemas for the two tables interviews and interviewSchemas --- db/tables/interview-schemas.ts | 3 ++- db/tables/interviews.ts | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/db/tables/interview-schemas.ts b/db/tables/interview-schemas.ts index 9e97344..ce61f60 100644 --- a/db/tables/interview-schemas.ts +++ b/db/tables/interview-schemas.ts @@ -1,11 +1,12 @@ import { interviewsTable } from "@/db/tables/interviews"; import { mainSchema } from "@/db/tables/schema"; +import type { JSONSchemaType } from "ajv"; import { relations } from "drizzle-orm"; import { json, serial } from "drizzle-orm/pg-core"; export const interviewSchemasTable = mainSchema.table("interviewSchemas", { id: serial("id").primaryKey(), - jsonSchema: json("jsonSchema").notNull(), // used to validate corresponding interviews interviewAnswers + jsonSchema: json("jsonSchema").$type>().notNull(), // used to validate corresponding interviews interviewAnswers }); export const interviewScemasRelations = relations( diff --git a/db/tables/interviews.ts b/db/tables/interviews.ts index 3cdc0ab..cd74628 100644 --- a/db/tables/interviews.ts +++ b/db/tables/interviews.ts @@ -2,6 +2,7 @@ import { assistantApplicationsTable } from "@/db/tables/applications"; import { interviewSchemasTable } from "@/db/tables/interview-schemas"; import { mainSchema } from "@/db/tables/schema"; import { teamUsersTable } from "@/db/tables/users"; +import { Json } from "@/lib/json-schema"; import { relations } from "drizzle-orm"; import { primaryKey } from "drizzle-orm/pg-core"; import { boolean, integer, json, serial, timestamp } from "drizzle-orm/pg-core"; @@ -14,7 +15,7 @@ export const interviewsTable = mainSchema.table("interviews", { interviewSchemaId: integer("interviewSchemaId") .notNull() .references(() => interviewSchemasTable.id), - interviewAnswers: json("interviewAnswers"), + interviewAnswers: json("interviewAnswers").$type(), isCancelled: boolean("isCancelled").notNull(), plannedTime: timestamp("plannedTime").notNull(), finishedTime: timestamp("timeFinished"), From 43cc674604b74214ebf19560b26e14fbd4a7c3f1 Mon Sep 17 00:00:00 2001 From: Vegard Kyrkjedelen Hovland Date: Mon, 10 Nov 2025 18:10:44 +0100 Subject: [PATCH 03/13] vf-327 Add dependency to ajv This package is used to parse json schemas --- package.json | 3 ++- pnpm-lock.yaml | 34 ++++++++++++++++++++++++++++++++++ 2 files changed, 36 insertions(+), 1 deletion(-) diff --git a/package.json b/package.json index f5249fe..efd64d6 100644 --- a/package.json +++ b/package.json @@ -22,6 +22,7 @@ }, "license": "ISC", "dependencies": { + "ajv": "^8.17.1", "cors": "^2.8.5", "dotenv": "^16.4.7", "drizzle-orm": "^0.33.0", @@ -52,5 +53,5 @@ "typescript": "^5.8.2", "yaml": "^2.7.1" }, - "packageManager": "pnpm@10.15.0+sha512.486ebc259d3e999a4e8691ce03b5cac4a71cbeca39372a9b762cb500cfdf0873e2cb16abe3d951b1ee2cf012503f027b98b6584e4df22524e0c7450d9ec7aa7b" + "packageManager": "pnpm@10.18.3+sha512.bbd16e6d7286fd7e01f6b3c0b3c932cda2965c06a908328f74663f10a9aea51f1129eea615134bf992831b009eabe167ecb7008b597f40ff9bc75946aadfb08d" } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index cce3045..f26c070 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -8,6 +8,9 @@ importers: .: dependencies: + ajv: + specifier: ^8.17.1 + version: 8.17.1 cors: specifier: ^2.8.5 version: 2.8.5 @@ -671,6 +674,9 @@ packages: resolution: {integrity: sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==} engines: {node: '>= 0.6'} + ajv@8.17.1: + resolution: {integrity: sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==} + anymatch@3.1.3: resolution: {integrity: sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==} engines: {node: '>= 8'} @@ -977,6 +983,9 @@ packages: resolution: {integrity: sha512-DT9ck5YIRU+8GYzzU5kT3eHGA5iL+1Zd0EutOmTE9Dtk+Tvuzd23VBU+ec7HPNSTxXYO55gPV/hq4pSBJDjFpA==} engines: {node: '>= 18'} + fast-deep-equal@3.1.3: + resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} + fast-glob@3.3.3: resolution: {integrity: sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==} engines: {node: '>=8.6.0'} @@ -984,6 +993,9 @@ packages: fast-safe-stringify@2.1.1: resolution: {integrity: sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA==} + fast-uri@3.1.0: + resolution: {integrity: sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==} + fastq@1.19.1: resolution: {integrity: sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==} @@ -1110,6 +1122,9 @@ packages: resolution: {integrity: sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==} hasBin: true + json-schema-traverse@1.0.0: + resolution: {integrity: sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==} + lodash.get@4.4.2: resolution: {integrity: sha512-z+Uw/vLuy6gQe8cfaFWD7p0wVv8fJl3mbzXh33RS+0oW2wvUqiRXiQ69gLWSLpgB5/6sU+r6BlQR0MBILadqTQ==} deprecated: This package is deprecated. Use the optional chaining (?.) operator instead. @@ -1336,6 +1351,10 @@ packages: resolution: {integrity: sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==} engines: {node: '>=8.10.0'} + require-from-string@2.0.2: + resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==} + engines: {node: '>=0.10.0'} + resolve-pkg-maps@1.0.0: resolution: {integrity: sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==} @@ -1874,6 +1893,13 @@ snapshots: mime-types: 3.0.1 negotiator: 1.0.0 + ajv@8.17.1: + dependencies: + fast-deep-equal: 3.1.3 + fast-uri: 3.1.0 + json-schema-traverse: 1.0.0 + require-from-string: 2.0.2 + anymatch@3.1.3: dependencies: normalize-path: 3.0.0 @@ -2174,6 +2200,8 @@ snapshots: transitivePeerDependencies: - supports-color + fast-deep-equal@3.1.3: {} + fast-glob@3.3.3: dependencies: '@nodelib/fs.stat': 2.0.5 @@ -2184,6 +2212,8 @@ snapshots: fast-safe-stringify@2.1.1: {} + fast-uri@3.1.0: {} + fastq@1.19.1: dependencies: reusify: 1.1.0 @@ -2326,6 +2356,8 @@ snapshots: dependencies: argparse: 2.0.1 + json-schema-traverse@1.0.0: {} + lodash.get@4.4.2: {} lodash.isequal@4.5.0: {} @@ -2500,6 +2532,8 @@ snapshots: dependencies: picomatch: 2.3.1 + require-from-string@2.0.2: {} + resolve-pkg-maps@1.0.0: {} reusify@1.1.0: {} From 5e13a47d1ddad40d900557baa314e909a262aa99 Mon Sep 17 00:00:00 2001 From: Vegard Kyrkjedelen Hovland Date: Mon, 10 Nov 2025 18:12:58 +0100 Subject: [PATCH 04/13] vf-327 Update time parsers Updated future and past time parsers. Also added some new configuring to the time string parser. --- lib/time-parsers.ts | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/lib/time-parsers.ts b/lib/time-parsers.ts index 5858adc..c6bd612 100644 --- a/lib/time-parsers.ts +++ b/lib/time-parsers.ts @@ -1,7 +1,8 @@ import { z } from "zod"; -export const timeStringParser = z.union([z.string().date(), z.string().time()]); +export const timeStringParser = z.union([z.string().date(), z.string().time(), z.string().datetime()]); +// Date here refers to the JS object date, so it allows more specific times than dates export const dateParser = z.date(); export const toDateParser = z .union([timeStringParser, z.date()]) @@ -24,7 +25,7 @@ export const toDatePeriodParser = z }) .pipe(datePeriodParser); -export const pastDateParser = z.date().max(new Date()); -export const futureDateParser = z.date().min(new Date()); +export const pastDateParser = dateParser.max(new Date()); +export const futureDateParser = dateParser.min(new Date()); export type DatePeriod = z.infer; From 4d2317592168c10a65473138fbcb1a624f42ad38 Mon Sep 17 00:00:00 2001 From: Vegard Kyrkjedelen Hovland Date: Mon, 10 Nov 2025 18:46:47 +0100 Subject: [PATCH 05/13] =?UTF-8?q?v-327=20Oppdaterte=20jsonschema=20parsere?= =?UTF-8?q?n=20til=20=C3=A5=20ta=20inn=20unknown?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Dette gjør det mulig å parse alt, ikke bare det man vet er av Json typen. --- lib/json-schema.ts | 2 +- src/db-access/interviews.ts | 19 +++++++++++++++++++ 2 files changed, 20 insertions(+), 1 deletion(-) create mode 100644 src/db-access/interviews.ts diff --git a/lib/json-schema.ts b/lib/json-schema.ts index 31fbc0d..b3ccbd0 100644 --- a/lib/json-schema.ts +++ b/lib/json-schema.ts @@ -14,7 +14,7 @@ const ajv = new Ajv(); export function validateJsonSchema( schema: JSONSchemaType, - data: Json, + data: unknown, ): JsonSchemaResult { const validator = ajv.compile(schema); const isValid = validator(data); diff --git a/src/db-access/interviews.ts b/src/db-access/interviews.ts new file mode 100644 index 0000000..f2ce709 --- /dev/null +++ b/src/db-access/interviews.ts @@ -0,0 +1,19 @@ +import { database } from "@/db/setup/query-postgres"; +import { newDatabaseTransaction } from "./common"; +import { interviewSchemasTable } from "@/db/tables/interview-schemas"; +import { inArray } from "drizzle-orm"; +import { ormError, OrmResult } from "../error/orm-error"; +import type { InterviewSchema, InterviewSchemaKey } from "@/src/response-handling/interviews"; + +export async function selectInterviewSchemaWithId(id: InterviewSchemaKey[]): Promise> { + return await newDatabaseTransaction(database, async (tx) => { + const result = await tx.select() + .from(interviewSchemasTable) + .where(inArray(interviewSchemasTable.id, id)); + if(result.length !== id.length) { + throw ormError("Couln't find all entries"); + } + + return result; + }); +} From 7905feeaed610ba95749a9d89446f174c0906f09 Mon Sep 17 00:00:00 2001 From: Vegard Kyrkjedelen Hovland Date: Mon, 19 Jan 2026 17:40:24 +0100 Subject: [PATCH 06/13] Change to AnySchema Changed to AnySchema instead of unknown JSON schema --- db/tables/interview-schemas.ts | 4 ++-- lib/json-schema.ts | 6 +++--- src/db-access/interviews.ts | 13 +++++++++++++ 3 files changed, 18 insertions(+), 5 deletions(-) diff --git a/db/tables/interview-schemas.ts b/db/tables/interview-schemas.ts index ce61f60..a22c0bf 100644 --- a/db/tables/interview-schemas.ts +++ b/db/tables/interview-schemas.ts @@ -1,12 +1,12 @@ import { interviewsTable } from "@/db/tables/interviews"; import { mainSchema } from "@/db/tables/schema"; -import type { JSONSchemaType } from "ajv"; +import type { AnySchema } from "ajv"; import { relations } from "drizzle-orm"; import { json, serial } from "drizzle-orm/pg-core"; export const interviewSchemasTable = mainSchema.table("interviewSchemas", { id: serial("id").primaryKey(), - jsonSchema: json("jsonSchema").$type>().notNull(), // used to validate corresponding interviews interviewAnswers + jsonSchema: json("jsonSchema").$type().notNull(), // used to validate corresponding interviews interviewAnswers }); export const interviewScemasRelations = relations( diff --git a/lib/json-schema.ts b/lib/json-schema.ts index b3ccbd0..fa67cea 100644 --- a/lib/json-schema.ts +++ b/lib/json-schema.ts @@ -1,4 +1,4 @@ -import Ajv, { type JSONSchemaType, type ErrorObject } from "ajv"; +import Ajv, { type ErrorObject, type AnySchema } from "ajv"; import z from "zod"; type JsonSchemaResult = @@ -13,7 +13,7 @@ type JsonSchemaResult = const ajv = new Ajv(); export function validateJsonSchema( - schema: JSONSchemaType, + schema: AnySchema, data: unknown, ): JsonSchemaResult { const validator = ajv.compile(schema); @@ -30,7 +30,7 @@ export function validateJsonSchema( export function turnJsonIntoZodSchema( - schema: JSONSchemaType + schema: AnySchema ) { return z.object({}).passthrough().superRefine((data, ctx) => { let validationResult = validateJsonSchema(schema, data); diff --git a/src/db-access/interviews.ts b/src/db-access/interviews.ts index f2ce709..1b193ac 100644 --- a/src/db-access/interviews.ts +++ b/src/db-access/interviews.ts @@ -4,6 +4,7 @@ import { interviewSchemasTable } from "@/db/tables/interview-schemas"; import { inArray } from "drizzle-orm"; import { ormError, OrmResult } from "../error/orm-error"; import type { InterviewSchema, InterviewSchemaKey } from "@/src/response-handling/interviews"; +import type { NewInterviewSchema } from "@/src/request-handling/interviews"; export async function selectInterviewSchemaWithId(id: InterviewSchemaKey[]): Promise> { return await newDatabaseTransaction(database, async (tx) => { @@ -17,3 +18,15 @@ export async function selectInterviewSchemaWithId(id: InterviewSchemaKey[]): Pro return result; }); } + +export async function insertInterviewSchema(interviewSchemaRequests: NewInterviewSchema[]): Promise> { + return await newDatabaseTransaction(database, async (tx) => { + const result = await tx.insert(interviewSchemasTable).values(interviewSchemaRequests).returning(); + + if(result.length !== interviewSchemaRequests.length) { + throw ormError("Failed to insert all entries"); + } + + return result; + }); +} From 6837fb85e412a941bd27b474ed2c0d73c43e2c14 Mon Sep 17 00:00:00 2001 From: Vegard Kyrkjedelen Hovland Date: Mon, 19 Jan 2026 17:50:08 +0100 Subject: [PATCH 07/13] Add zod schemas for request, response This will be used to parse interviewSchema requests to the database --- src/request-handling/interviews.ts | 33 +++++++++++++++++++++++++++++ src/response-handling/interviews.ts | 10 +++++++++ 2 files changed, 43 insertions(+) create mode 100644 src/request-handling/interviews.ts create mode 100644 src/response-handling/interviews.ts diff --git a/src/request-handling/interviews.ts b/src/request-handling/interviews.ts new file mode 100644 index 0000000..00be9d3 --- /dev/null +++ b/src/request-handling/interviews.ts @@ -0,0 +1,33 @@ +import { serialIdParser } from "@/src/request-handling/common"; +import { z } from "zod"; +import { futureDateParser, timeStringParser, toDateParser } from "@/lib/time-parsers"; +import { createInsertSchema } from "drizzle-zod"; +import { interviewsTable } from "@/db/tables/interviews"; +import { turnJsonIntoZodSchema } from "@/lib/json-schema"; +import metaSchema from "ajv/dist/refs/json-schema-draft-07.json"; +import { AnySchema } from "ajv"; + +export const newInterviewSchemaSchema = z.object({ + jsonSchema: turnJsonIntoZodSchema(metaSchema).transform(v => v), // If the object passed this json parser we know it is a validJsonSchema +}); + +export const newInterviewSchema = z.object({ + applicationId: serialIdParser, + interviewSchemaId: serialIdParser, + interviewAnswers: z.object({}).passthrough(), // This will be further checked after schema is gotten from database + isCancelled: z.boolean(), + plannedTime: timeStringParser, +}); + + +export const newInterviewToInsertSchema = newInterviewSchema.extend({ + plannedTime: newInterviewSchema.shape.plannedTime + .pipe(toDateParser) + .pipe(futureDateParser), +}).pipe(createInsertSchema(interviewsTable)); + +export const newInterviewSchemaToInsertSchema = newInterviewSchemaSchema.extend({}) // because of the way drizzle-zod works this pipe does wrong type inference and shouldn't be used + + +export type NewInterview= z.infer; +export type NewInterviewSchema= z.infer; diff --git a/src/response-handling/interviews.ts b/src/response-handling/interviews.ts new file mode 100644 index 0000000..fd37d25 --- /dev/null +++ b/src/response-handling/interviews.ts @@ -0,0 +1,10 @@ +import { createSelectSchema } from "drizzle-zod"; +import { interviewSchemasTable } from "@/db/tables/interview-schemas"; +import { z } from "zod"; + +const interviewSchemaSchema = createSelectSchema(interviewSchemasTable) + .strict() + .readonly(); + +export type InterviewSchema = z.infer; +export type InterviewSchemaKey = InterviewSchema["id"]; From 5f09662eaba8469783a5cf0081bd7f21ec26e9f1 Mon Sep 17 00:00:00 2001 From: Vegard Kyrkjedelen Hovland Date: Mon, 19 Jan 2026 18:24:32 +0100 Subject: [PATCH 08/13] Add routes for adding interviewSchema and started adding interviews The only things left in the interviews-route is sending to database and returning. This will be done after an insert-query is made. --- src/routers/interviews.ts | 55 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 55 insertions(+) create mode 100644 src/routers/interviews.ts diff --git a/src/routers/interviews.ts b/src/routers/interviews.ts new file mode 100644 index 0000000..c395e5d --- /dev/null +++ b/src/routers/interviews.ts @@ -0,0 +1,55 @@ +import { Router } from "express"; +import { newInterviewSchemaToInsertSchema, newInterviewToInsertSchema } from "../request-handling/interviews"; +import { clientError } from "../error/http-errors"; +import { insertInterviewSchema, selectInterviewSchemaWithId } from "../db-access/interviews"; +import { validateJsonSchema } from "@/lib/json-schema"; +import { JSONSchemaType } from "ajv"; + + +const interviewsRouter = Router(); + +interviewsRouter.post("/", async (req, res, next) => { + const bodyResult = newInterviewToInsertSchema.safeParse(req.body); + if (!bodyResult.success) { + next(clientError(400, "Invalid input data", bodyResult.error)); + return; + } + const body = bodyResult.data; + if(body.interviewAnswers !== undefined) { + const interviewSchemaResult = await selectInterviewSchemaWithId([body.interviewSchemaId]); + + if(!interviewSchemaResult.success) { + next(clientError(404, "Resource not available", bodyResult.error)); + return; + } + + // We assume that jsonschemas already in the database are valid. + const interviewJsonSchema = interviewSchemaResult.data[0].jsonSchema as JSONSchemaType; + + const jsonSchemaValidationResult = validateJsonSchema(interviewJsonSchema, body.interviewAnswers); + + if(!jsonSchemaValidationResult.success) { + next(clientError(422, "Invalid request format", jsonSchemaValidationResult.error)); + return; + } + } + + // TODO: insert interview + // TODO: return +}); + +interviewsRouter.post("/schema", async (req, res, next) => { + const bodyResult = newInterviewSchemaToInsertSchema.safeParse(req.body); + if(!bodyResult.success) { + next(clientError(400, "Invalid input data", bodyResult.error)); + return; + } + const body = bodyResult.data; + const databaseResult = await insertInterviewSchema([body]); + + if(!databaseResult.success) { + next(clientError(400, "Database error", databaseResult.error)); + return; + } + res.json(databaseResult.data); +}); From 54edad5e50f74e02d33d013f81277031e9a78a55 Mon Sep 17 00:00:00 2001 From: Vegard Kyrkjedelen Hovland Date: Mon, 19 Jan 2026 18:39:30 +0100 Subject: [PATCH 09/13] Add insert interview query Added a query to insert interviews. --- src/db-access/interviews.ts | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/src/db-access/interviews.ts b/src/db-access/interviews.ts index 1b193ac..fa1e54b 100644 --- a/src/db-access/interviews.ts +++ b/src/db-access/interviews.ts @@ -4,7 +4,8 @@ import { interviewSchemasTable } from "@/db/tables/interview-schemas"; import { inArray } from "drizzle-orm"; import { ormError, OrmResult } from "../error/orm-error"; import type { InterviewSchema, InterviewSchemaKey } from "@/src/response-handling/interviews"; -import type { NewInterviewSchema } from "@/src/request-handling/interviews"; +import type { NewInterview, NewInterviewSchema } from "@/src/request-handling/interviews"; +import { interviewsTable } from "@/db/tables/interviews"; export async function selectInterviewSchemaWithId(id: InterviewSchemaKey[]): Promise> { return await newDatabaseTransaction(database, async (tx) => { @@ -30,3 +31,14 @@ export async function insertInterviewSchema(interviewSchemaRequests: NewIntervie return result; }); } + +export async function insertInterview(interviewRequests: NewInterview[]) { + return await newDatabaseTransaction(database, async (tx) => { + const result = await tx.insert(interviewsTable).values(interviewRequests).returning(); + + if(result.length !== interviewRequests.length) { + throw ormError("Failed to insert all entries"); + } + return result; + }) +} From b37b4e7aa6ab0564c15ac3fd1089df3c1138dcf6 Mon Sep 17 00:00:00 2001 From: Vegard Kyrkjedelen Hovland Date: Mon, 19 Jan 2026 19:01:30 +0100 Subject: [PATCH 10/13] Finish insert route for interview schemas Added databasecall, errorhandling and return call --- src/routers/interviews.ts | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/src/routers/interviews.ts b/src/routers/interviews.ts index c395e5d..b8c0f0a 100644 --- a/src/routers/interviews.ts +++ b/src/routers/interviews.ts @@ -1,7 +1,7 @@ import { Router } from "express"; import { newInterviewSchemaToInsertSchema, newInterviewToInsertSchema } from "../request-handling/interviews"; import { clientError } from "../error/http-errors"; -import { insertInterviewSchema, selectInterviewSchemaWithId } from "../db-access/interviews"; +import { insertInterview, insertInterviewSchema, selectInterviewSchemaWithId } from "../db-access/interviews"; import { validateJsonSchema } from "@/lib/json-schema"; import { JSONSchemaType } from "ajv"; @@ -34,8 +34,13 @@ interviewsRouter.post("/", async (req, res, next) => { } } - // TODO: insert interview - // TODO: return + const databaseResult = await insertInterview([body]); + if (!databaseResult.success) { + next(clientError(400, "Database error", databaseResult.error)); + return; + } + + res.json(databaseResult.data); }); interviewsRouter.post("/schema", async (req, res, next) => { From 9158a4b53be843556ef8103c0e97a0ec2d1aa8a4 Mon Sep 17 00:00:00 2001 From: Vegard Kyrkjedelen Hovland Date: Mon, 2 Feb 2026 17:19:20 +0100 Subject: [PATCH 11/13] Add interviewschema API calls Add calls to the API to get schemas from ID and all of them. --- db/tables/applications.ts | 2 +- nim | 110 ++++++++++++++++++++++++++++++++++++ nvim | 110 ++++++++++++++++++++++++++++++++++++ src/db-access/interviews.ts | 9 +++ src/routers/interviews.ts | 34 ++++++++++- 5 files changed, 262 insertions(+), 3 deletions(-) create mode 100644 nim create mode 100644 nvim diff --git a/db/tables/applications.ts b/db/tables/applications.ts index 264868c..9eb316b 100644 --- a/db/tables/applications.ts +++ b/db/tables/applications.ts @@ -45,7 +45,7 @@ export const applicationsRelations = relations( references: [assistantApplicationsTable.id], }), teamApplication: many(teamApplicationsTable), - interview: many(interviewsTable), + //interview: many(interviewsTable), }), ); diff --git a/nim b/nim new file mode 100644 index 0000000..6b51ae8 --- /dev/null +++ b/nim @@ -0,0 +1,110 @@ +diff --git a/db/tables/applications.ts b/db/tables/applications.ts +index 264868c..9eb316b 100644 +--- a/db/tables/applications.ts ++++ b/db/tables/applications.ts +@@ -45,7 +45,7 @@ export const applicationsRelations = relations( + references: [assistantApplicationsTable.id], + }), + teamApplication: many(teamApplicationsTable), +- interview: many(interviewsTable), ++ //interview: many(interviewsTable), + }), + ); + +diff --git a/src/db-access/interviews.ts b/src/db-access/interviews.ts +index fa1e54b..f382056 100644 +--- a/src/db-access/interviews.ts ++++ b/src/db-access/interviews.ts +@@ -6,6 +6,7 @@ import { ormError, OrmResult } from "../error/orm-error"; + import type { InterviewSchema, InterviewSchemaKey } from "@/src/response-handling/interviews"; + import type { NewInterview, NewInterviewSchema } from "@/src/request-handling/interviews"; + import { interviewsTable } from "@/db/tables/interviews"; ++import { QueryParameters } from "../request-handling/common"; + + export async function selectInterviewSchemaWithId(id: InterviewSchemaKey[]): Promise> { + return await newDatabaseTransaction(database, async (tx) => { +@@ -20,6 +21,14 @@ export async function selectInterviewSchemaWithId(id: InterviewSchemaKey[]): Pro + }); + } + ++export async function selectInterviewSchemas(list_queries: QueryParameters) { ++ return await newDatabaseTransaction(database, async (tx) => { ++ const result = await tx.select().from(interviewSchemasTable).limit(list_queries.limit).offset(list_queries.offset); ++ ++ return result; ++ }) ++} ++ + export async function insertInterviewSchema(interviewSchemaRequests: NewInterviewSchema[]): Promise> { + return await newDatabaseTransaction(database, async (tx) => { + const result = await tx.insert(interviewSchemasTable).values(interviewSchemaRequests).returning(); +diff --git a/src/routers/interviews.ts b/src/routers/interviews.ts +index b8c0f0a..eaf6bb9 100644 +--- a/src/routers/interviews.ts ++++ b/src/routers/interviews.ts +@@ -1,9 +1,11 @@ + import { Router } from "express"; + import { newInterviewSchemaToInsertSchema, newInterviewToInsertSchema } from "../request-handling/interviews"; +-import { clientError } from "../error/http-errors"; +-import { insertInterview, insertInterviewSchema, selectInterviewSchemaWithId } from "../db-access/interviews"; ++import { clientError, serverError } from "../error/http-errors"; ++import { insertInterview, insertInterviewSchema, selectInterviewSchemas, selectInterviewSchemaWithId } from "../db-access/interviews"; + import { validateJsonSchema } from "@/lib/json-schema"; + import { JSONSchemaType } from "ajv"; ++import { serialIdParser, toListQueryParser, toSerialIdParser } from "../request-handling/common"; ++import { param } from "drizzle-orm"; + + + const interviewsRouter = Router(); +@@ -58,3 +60,51 @@ interviewsRouter.post("/schema", async (req, res, next) => { + } + res.json(databaseResult.data); + }); ++ ++interviewsRouter.get("/schema/:id", async (req, res, next) => { ++ const idParameterResult = toSerialIdParser.safeParse(req.params.id); ++ if(!idParameterResult.success) { ++ next(clientError(400, "Invalid input data", idParameterResult.error)); ++ return; ++ } ++ const schemaResult = await selectInterviewSchemaWithId([idParameterResult.data]); ++ if(!schemaResult.success) { ++ next(clientError(400, "Database error", schemaResult.error)); ++ return; ++ } ++ res.json(schemaResult.data); ++}); ++ ++interviewsRouter.get("/schema", async (req, res, next) => { ++ const queryResult = toListQueryParser.safeParse(req.query); ++ if(!queryResult.success) { ++ next(clientError(400, "Invalid input data", queryResult.error)); ++ return; ++ } ++ const dbResult = await selectInterviewSchemas(queryResult.data); ++ if(!dbResult.success) { ++ next(serverError(500, "Data processing error", dbResult.error)); ++ return; ++ } ++ res.json(dbResult.data); ++}); ++ ++interviewsRouter.get("/:id", async (req, res, next) => { ++ const parameterResult = serialIdParser.safeParse(req.params.id); ++ if(!parameterResult.success) { ++ next(clientError(400, "Invalid input data", parameterResult.error)); ++ } ++}); ++ ++ ++function queryParseCall(parser, databaseCall) { ++ return (req, res, next) => { ++ const parseResult = parser.safeParse(req.query); ++ if(!parseResult.success) { ++ next(clientError(400, "Invalid input data", parseResult.error)); ++ return; ++ } ++ const db ++ ++ } ++} diff --git a/nvim b/nvim new file mode 100644 index 0000000..6b51ae8 --- /dev/null +++ b/nvim @@ -0,0 +1,110 @@ +diff --git a/db/tables/applications.ts b/db/tables/applications.ts +index 264868c..9eb316b 100644 +--- a/db/tables/applications.ts ++++ b/db/tables/applications.ts +@@ -45,7 +45,7 @@ export const applicationsRelations = relations( + references: [assistantApplicationsTable.id], + }), + teamApplication: many(teamApplicationsTable), +- interview: many(interviewsTable), ++ //interview: many(interviewsTable), + }), + ); + +diff --git a/src/db-access/interviews.ts b/src/db-access/interviews.ts +index fa1e54b..f382056 100644 +--- a/src/db-access/interviews.ts ++++ b/src/db-access/interviews.ts +@@ -6,6 +6,7 @@ import { ormError, OrmResult } from "../error/orm-error"; + import type { InterviewSchema, InterviewSchemaKey } from "@/src/response-handling/interviews"; + import type { NewInterview, NewInterviewSchema } from "@/src/request-handling/interviews"; + import { interviewsTable } from "@/db/tables/interviews"; ++import { QueryParameters } from "../request-handling/common"; + + export async function selectInterviewSchemaWithId(id: InterviewSchemaKey[]): Promise> { + return await newDatabaseTransaction(database, async (tx) => { +@@ -20,6 +21,14 @@ export async function selectInterviewSchemaWithId(id: InterviewSchemaKey[]): Pro + }); + } + ++export async function selectInterviewSchemas(list_queries: QueryParameters) { ++ return await newDatabaseTransaction(database, async (tx) => { ++ const result = await tx.select().from(interviewSchemasTable).limit(list_queries.limit).offset(list_queries.offset); ++ ++ return result; ++ }) ++} ++ + export async function insertInterviewSchema(interviewSchemaRequests: NewInterviewSchema[]): Promise> { + return await newDatabaseTransaction(database, async (tx) => { + const result = await tx.insert(interviewSchemasTable).values(interviewSchemaRequests).returning(); +diff --git a/src/routers/interviews.ts b/src/routers/interviews.ts +index b8c0f0a..eaf6bb9 100644 +--- a/src/routers/interviews.ts ++++ b/src/routers/interviews.ts +@@ -1,9 +1,11 @@ + import { Router } from "express"; + import { newInterviewSchemaToInsertSchema, newInterviewToInsertSchema } from "../request-handling/interviews"; +-import { clientError } from "../error/http-errors"; +-import { insertInterview, insertInterviewSchema, selectInterviewSchemaWithId } from "../db-access/interviews"; ++import { clientError, serverError } from "../error/http-errors"; ++import { insertInterview, insertInterviewSchema, selectInterviewSchemas, selectInterviewSchemaWithId } from "../db-access/interviews"; + import { validateJsonSchema } from "@/lib/json-schema"; + import { JSONSchemaType } from "ajv"; ++import { serialIdParser, toListQueryParser, toSerialIdParser } from "../request-handling/common"; ++import { param } from "drizzle-orm"; + + + const interviewsRouter = Router(); +@@ -58,3 +60,51 @@ interviewsRouter.post("/schema", async (req, res, next) => { + } + res.json(databaseResult.data); + }); ++ ++interviewsRouter.get("/schema/:id", async (req, res, next) => { ++ const idParameterResult = toSerialIdParser.safeParse(req.params.id); ++ if(!idParameterResult.success) { ++ next(clientError(400, "Invalid input data", idParameterResult.error)); ++ return; ++ } ++ const schemaResult = await selectInterviewSchemaWithId([idParameterResult.data]); ++ if(!schemaResult.success) { ++ next(clientError(400, "Database error", schemaResult.error)); ++ return; ++ } ++ res.json(schemaResult.data); ++}); ++ ++interviewsRouter.get("/schema", async (req, res, next) => { ++ const queryResult = toListQueryParser.safeParse(req.query); ++ if(!queryResult.success) { ++ next(clientError(400, "Invalid input data", queryResult.error)); ++ return; ++ } ++ const dbResult = await selectInterviewSchemas(queryResult.data); ++ if(!dbResult.success) { ++ next(serverError(500, "Data processing error", dbResult.error)); ++ return; ++ } ++ res.json(dbResult.data); ++}); ++ ++interviewsRouter.get("/:id", async (req, res, next) => { ++ const parameterResult = serialIdParser.safeParse(req.params.id); ++ if(!parameterResult.success) { ++ next(clientError(400, "Invalid input data", parameterResult.error)); ++ } ++}); ++ ++ ++function queryParseCall(parser, databaseCall) { ++ return (req, res, next) => { ++ const parseResult = parser.safeParse(req.query); ++ if(!parseResult.success) { ++ next(clientError(400, "Invalid input data", parseResult.error)); ++ return; ++ } ++ const db ++ ++ } ++} diff --git a/src/db-access/interviews.ts b/src/db-access/interviews.ts index fa1e54b..f382056 100644 --- a/src/db-access/interviews.ts +++ b/src/db-access/interviews.ts @@ -6,6 +6,7 @@ import { ormError, OrmResult } from "../error/orm-error"; import type { InterviewSchema, InterviewSchemaKey } from "@/src/response-handling/interviews"; import type { NewInterview, NewInterviewSchema } from "@/src/request-handling/interviews"; import { interviewsTable } from "@/db/tables/interviews"; +import { QueryParameters } from "../request-handling/common"; export async function selectInterviewSchemaWithId(id: InterviewSchemaKey[]): Promise> { return await newDatabaseTransaction(database, async (tx) => { @@ -20,6 +21,14 @@ export async function selectInterviewSchemaWithId(id: InterviewSchemaKey[]): Pro }); } +export async function selectInterviewSchemas(list_queries: QueryParameters) { + return await newDatabaseTransaction(database, async (tx) => { + const result = await tx.select().from(interviewSchemasTable).limit(list_queries.limit).offset(list_queries.offset); + + return result; + }) +} + export async function insertInterviewSchema(interviewSchemaRequests: NewInterviewSchema[]): Promise> { return await newDatabaseTransaction(database, async (tx) => { const result = await tx.insert(interviewSchemasTable).values(interviewSchemaRequests).returning(); diff --git a/src/routers/interviews.ts b/src/routers/interviews.ts index b8c0f0a..74f6bc0 100644 --- a/src/routers/interviews.ts +++ b/src/routers/interviews.ts @@ -1,9 +1,11 @@ import { Router } from "express"; import { newInterviewSchemaToInsertSchema, newInterviewToInsertSchema } from "../request-handling/interviews"; -import { clientError } from "../error/http-errors"; -import { insertInterview, insertInterviewSchema, selectInterviewSchemaWithId } from "../db-access/interviews"; +import { clientError, serverError } from "../error/http-errors"; +import { insertInterview, insertInterviewSchema, selectInterviewSchemas, selectInterviewSchemaWithId } from "../db-access/interviews"; import { validateJsonSchema } from "@/lib/json-schema"; import { JSONSchemaType } from "ajv"; +import { serialIdParser, toListQueryParser, toSerialIdParser } from "../request-handling/common"; +import { param } from "drizzle-orm"; const interviewsRouter = Router(); @@ -58,3 +60,31 @@ interviewsRouter.post("/schema", async (req, res, next) => { } res.json(databaseResult.data); }); + +interviewsRouter.get("/schema/:id", async (req, res, next) => { + const idParameterResult = toSerialIdParser.safeParse(req.params.id); + if(!idParameterResult.success) { + next(clientError(400, "Invalid input data", idParameterResult.error)); + return; + } + const schemaResult = await selectInterviewSchemaWithId([idParameterResult.data]); + if(!schemaResult.success) { + next(clientError(400, "Database error", schemaResult.error)); + return; + } + res.json(schemaResult.data); +}); + +interviewsRouter.get("/schema", async (req, res, next) => { + const queryResult = toListQueryParser.safeParse(req.query); + if(!queryResult.success) { + next(clientError(400, "Invalid input data", queryResult.error)); + return; + } + const dbResult = await selectInterviewSchemas(queryResult.data); + if(!dbResult.success) { + next(serverError(500, "Data processing error", dbResult.error)); + return; + } + res.json(dbResult.data); +}); From ab6e55f738509014b15704b5113abc7dc7f58f2d Mon Sep 17 00:00:00 2001 From: Vegard Kyrkjedelen Hovland Date: Mon, 2 Feb 2026 17:27:36 +0100 Subject: [PATCH 12/13] Fix styling via pnpm check --- db/tables/interviews.ts | 2 +- lib/json-schema.ts | 39 +++++++-------- lib/time-parsers.ts | 6 ++- src/db-access/interviews.ts | 59 +++++++++++++++-------- src/request-handling/interviews.ts | 40 +++++++++------ src/response-handling/interviews.ts | 4 +- src/routers/interviews.ts | 75 +++++++++++++++++++---------- 7 files changed, 141 insertions(+), 84 deletions(-) diff --git a/db/tables/interviews.ts b/db/tables/interviews.ts index cd74628..f9dfc0a 100644 --- a/db/tables/interviews.ts +++ b/db/tables/interviews.ts @@ -2,7 +2,7 @@ import { assistantApplicationsTable } from "@/db/tables/applications"; import { interviewSchemasTable } from "@/db/tables/interview-schemas"; import { mainSchema } from "@/db/tables/schema"; import { teamUsersTable } from "@/db/tables/users"; -import { Json } from "@/lib/json-schema"; +import type { Json } from "@/lib/json-schema"; import { relations } from "drizzle-orm"; import { primaryKey } from "drizzle-orm/pg-core"; import { boolean, integer, json, serial, timestamp } from "drizzle-orm/pg-core"; diff --git a/lib/json-schema.ts b/lib/json-schema.ts index fa67cea..253a616 100644 --- a/lib/json-schema.ts +++ b/lib/json-schema.ts @@ -23,28 +23,29 @@ export function validateJsonSchema( } return { success: false, - error: validator.errors === undefined || - validator.errors === null ? [] : validator.errors, + error: + validator.errors === undefined || validator.errors === null + ? [] + : validator.errors, }; } - -export function turnJsonIntoZodSchema( - schema: AnySchema -) { - return z.object({}).passthrough().superRefine((data, ctx) => { - let validationResult = validateJsonSchema(schema, data); - if (!validationResult.success) { - ctx.addIssue({ - code: z.ZodIssueCode.custom, - message: "The interview schema is not valid", - params: validationResult.error, - }); - } - - return validationResult.success; - }); - +export function turnJsonIntoZodSchema(schema: AnySchema) { + return z + .object({}) + .passthrough() + .superRefine((data, ctx) => { + const validationResult = validateJsonSchema(schema, data); + if (!validationResult.success) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: "The interview schema is not valid", + params: validationResult.error, + }); + } + + return validationResult.success; + }); } // from: https://www.reddit.com/r/typescript/comments/13mssvc/types_for_json_and_writing_json diff --git a/lib/time-parsers.ts b/lib/time-parsers.ts index c6bd612..6f9751b 100644 --- a/lib/time-parsers.ts +++ b/lib/time-parsers.ts @@ -1,6 +1,10 @@ import { z } from "zod"; -export const timeStringParser = z.union([z.string().date(), z.string().time(), z.string().datetime()]); +export const timeStringParser = z.union([ + z.string().date(), + z.string().time(), + z.string().datetime(), +]); // Date here refers to the JS object date, so it allows more specific times than dates export const dateParser = z.date(); diff --git a/src/db-access/interviews.ts b/src/db-access/interviews.ts index f382056..ff54c7c 100644 --- a/src/db-access/interviews.ts +++ b/src/db-access/interviews.ts @@ -1,19 +1,28 @@ import { database } from "@/db/setup/query-postgres"; -import { newDatabaseTransaction } from "./common"; import { interviewSchemasTable } from "@/db/tables/interview-schemas"; -import { inArray } from "drizzle-orm"; -import { ormError, OrmResult } from "../error/orm-error"; -import type { InterviewSchema, InterviewSchemaKey } from "@/src/response-handling/interviews"; -import type { NewInterview, NewInterviewSchema } from "@/src/request-handling/interviews"; import { interviewsTable } from "@/db/tables/interviews"; -import { QueryParameters } from "../request-handling/common"; +import type { + NewInterview, + NewInterviewSchema, +} from "@/src/request-handling/interviews"; +import type { + InterviewSchema, + InterviewSchemaKey, +} from "@/src/response-handling/interviews"; +import { inArray } from "drizzle-orm"; +import { type OrmResult, ormError } from "../error/orm-error"; +import type { QueryParameters } from "../request-handling/common"; +import { newDatabaseTransaction } from "./common"; -export async function selectInterviewSchemaWithId(id: InterviewSchemaKey[]): Promise> { +export async function selectInterviewSchemaWithId( + id: InterviewSchemaKey[], +): Promise> { return await newDatabaseTransaction(database, async (tx) => { - const result = await tx.select() + const result = await tx + .select() .from(interviewSchemasTable) .where(inArray(interviewSchemasTable.id, id)); - if(result.length !== id.length) { + if (result.length !== id.length) { throw ormError("Couln't find all entries"); } @@ -21,19 +30,28 @@ export async function selectInterviewSchemaWithId(id: InterviewSchemaKey[]): Pro }); } -export async function selectInterviewSchemas(list_queries: QueryParameters) { +export async function selectInterviewSchemas(listQueries: QueryParameters) { return await newDatabaseTransaction(database, async (tx) => { - const result = await tx.select().from(interviewSchemasTable).limit(list_queries.limit).offset(list_queries.offset); - + const result = await tx + .select() + .from(interviewSchemasTable) + .limit(listQueries.limit) + .offset(listQueries.offset); + return result; - }) + }); } -export async function insertInterviewSchema(interviewSchemaRequests: NewInterviewSchema[]): Promise> { +export async function insertInterviewSchema( + interviewSchemaRequests: NewInterviewSchema[], +): Promise> { return await newDatabaseTransaction(database, async (tx) => { - const result = await tx.insert(interviewSchemasTable).values(interviewSchemaRequests).returning(); + const result = await tx + .insert(interviewSchemasTable) + .values(interviewSchemaRequests) + .returning(); - if(result.length !== interviewSchemaRequests.length) { + if (result.length !== interviewSchemaRequests.length) { throw ormError("Failed to insert all entries"); } @@ -43,11 +61,14 @@ export async function insertInterviewSchema(interviewSchemaRequests: NewIntervie export async function insertInterview(interviewRequests: NewInterview[]) { return await newDatabaseTransaction(database, async (tx) => { - const result = await tx.insert(interviewsTable).values(interviewRequests).returning(); + const result = await tx + .insert(interviewsTable) + .values(interviewRequests) + .returning(); - if(result.length !== interviewRequests.length) { + if (result.length !== interviewRequests.length) { throw ormError("Failed to insert all entries"); } return result; - }) + }); } diff --git a/src/request-handling/interviews.ts b/src/request-handling/interviews.ts index 00be9d3..55cff8d 100644 --- a/src/request-handling/interviews.ts +++ b/src/request-handling/interviews.ts @@ -1,14 +1,18 @@ -import { serialIdParser } from "@/src/request-handling/common"; -import { z } from "zod"; -import { futureDateParser, timeStringParser, toDateParser } from "@/lib/time-parsers"; -import { createInsertSchema } from "drizzle-zod"; import { interviewsTable } from "@/db/tables/interviews"; import { turnJsonIntoZodSchema } from "@/lib/json-schema"; +import { + futureDateParser, + timeStringParser, + toDateParser, +} from "@/lib/time-parsers"; +import { serialIdParser } from "@/src/request-handling/common"; +import type { AnySchema } from "ajv"; import metaSchema from "ajv/dist/refs/json-schema-draft-07.json"; -import { AnySchema } from "ajv"; +import { createInsertSchema } from "drizzle-zod"; +import { z } from "zod"; export const newInterviewSchemaSchema = z.object({ - jsonSchema: turnJsonIntoZodSchema(metaSchema).transform(v => v), // If the object passed this json parser we know it is a validJsonSchema + jsonSchema: turnJsonIntoZodSchema(metaSchema).transform((v) => v), // If the object passed this json parser we know it is a validJsonSchema }); export const newInterviewSchema = z.object({ @@ -19,15 +23,19 @@ export const newInterviewSchema = z.object({ plannedTime: timeStringParser, }); +export const newInterviewToInsertSchema = newInterviewSchema + .extend({ + plannedTime: newInterviewSchema.shape.plannedTime + .pipe(toDateParser) + .pipe(futureDateParser), + }) + .pipe(createInsertSchema(interviewsTable)); -export const newInterviewToInsertSchema = newInterviewSchema.extend({ - plannedTime: newInterviewSchema.shape.plannedTime - .pipe(toDateParser) - .pipe(futureDateParser), -}).pipe(createInsertSchema(interviewsTable)); - -export const newInterviewSchemaToInsertSchema = newInterviewSchemaSchema.extend({}) // because of the way drizzle-zod works this pipe does wrong type inference and shouldn't be used - +export const newInterviewSchemaToInsertSchema = newInterviewSchemaSchema.extend( + {}, +); // because of the way drizzle-zod works this pipe does wrong type inference and shouldn't be used -export type NewInterview= z.infer; -export type NewInterviewSchema= z.infer; +export type NewInterview = z.infer; +export type NewInterviewSchema = z.infer< + typeof newInterviewSchemaToInsertSchema +>; diff --git a/src/response-handling/interviews.ts b/src/response-handling/interviews.ts index fd37d25..358ea4b 100644 --- a/src/response-handling/interviews.ts +++ b/src/response-handling/interviews.ts @@ -1,6 +1,6 @@ -import { createSelectSchema } from "drizzle-zod"; import { interviewSchemasTable } from "@/db/tables/interview-schemas"; -import { z } from "zod"; +import { createSelectSchema } from "drizzle-zod"; +import type { z } from "zod"; const interviewSchemaSchema = createSelectSchema(interviewSchemasTable) .strict() diff --git a/src/routers/interviews.ts b/src/routers/interviews.ts index 74f6bc0..e6a6b2c 100644 --- a/src/routers/interviews.ts +++ b/src/routers/interviews.ts @@ -1,12 +1,21 @@ +import { validateJsonSchema } from "@/lib/json-schema"; +import type { JSONSchemaType } from "ajv"; import { Router } from "express"; -import { newInterviewSchemaToInsertSchema, newInterviewToInsertSchema } from "../request-handling/interviews"; +import { + insertInterview, + insertInterviewSchema, + selectInterviewSchemaWithId, + selectInterviewSchemas, +} from "../db-access/interviews"; import { clientError, serverError } from "../error/http-errors"; -import { insertInterview, insertInterviewSchema, selectInterviewSchemas, selectInterviewSchemaWithId } from "../db-access/interviews"; -import { validateJsonSchema } from "@/lib/json-schema"; -import { JSONSchemaType } from "ajv"; -import { serialIdParser, toListQueryParser, toSerialIdParser } from "../request-handling/common"; -import { param } from "drizzle-orm"; - +import { + toListQueryParser, + toSerialIdParser, +} from "../request-handling/common"; +import { + newInterviewSchemaToInsertSchema, + newInterviewToInsertSchema, +} from "../request-handling/interviews"; const interviewsRouter = Router(); @@ -17,21 +26,33 @@ interviewsRouter.post("/", async (req, res, next) => { return; } const body = bodyResult.data; - if(body.interviewAnswers !== undefined) { - const interviewSchemaResult = await selectInterviewSchemaWithId([body.interviewSchemaId]); - - if(!interviewSchemaResult.success) { + if (body.interviewAnswers !== undefined) { + const interviewSchemaResult = await selectInterviewSchemaWithId([ + body.interviewSchemaId, + ]); + + if (!interviewSchemaResult.success) { next(clientError(404, "Resource not available", bodyResult.error)); return; } - + // We assume that jsonschemas already in the database are valid. - const interviewJsonSchema = interviewSchemaResult.data[0].jsonSchema as JSONSchemaType; - - const jsonSchemaValidationResult = validateJsonSchema(interviewJsonSchema, body.interviewAnswers); - - if(!jsonSchemaValidationResult.success) { - next(clientError(422, "Invalid request format", jsonSchemaValidationResult.error)); + const interviewJsonSchema = interviewSchemaResult.data[0] + .jsonSchema as JSONSchemaType; + + const jsonSchemaValidationResult = validateJsonSchema( + interviewJsonSchema, + body.interviewAnswers, + ); + + if (!jsonSchemaValidationResult.success) { + next( + clientError( + 422, + "Invalid request format", + jsonSchemaValidationResult.error, + ), + ); return; } } @@ -47,14 +68,14 @@ interviewsRouter.post("/", async (req, res, next) => { interviewsRouter.post("/schema", async (req, res, next) => { const bodyResult = newInterviewSchemaToInsertSchema.safeParse(req.body); - if(!bodyResult.success) { + if (!bodyResult.success) { next(clientError(400, "Invalid input data", bodyResult.error)); return; } const body = bodyResult.data; const databaseResult = await insertInterviewSchema([body]); - - if(!databaseResult.success) { + + if (!databaseResult.success) { next(clientError(400, "Database error", databaseResult.error)); return; } @@ -63,12 +84,14 @@ interviewsRouter.post("/schema", async (req, res, next) => { interviewsRouter.get("/schema/:id", async (req, res, next) => { const idParameterResult = toSerialIdParser.safeParse(req.params.id); - if(!idParameterResult.success) { + if (!idParameterResult.success) { next(clientError(400, "Invalid input data", idParameterResult.error)); return; } - const schemaResult = await selectInterviewSchemaWithId([idParameterResult.data]); - if(!schemaResult.success) { + const schemaResult = await selectInterviewSchemaWithId([ + idParameterResult.data, + ]); + if (!schemaResult.success) { next(clientError(400, "Database error", schemaResult.error)); return; } @@ -77,12 +100,12 @@ interviewsRouter.get("/schema/:id", async (req, res, next) => { interviewsRouter.get("/schema", async (req, res, next) => { const queryResult = toListQueryParser.safeParse(req.query); - if(!queryResult.success) { + if (!queryResult.success) { next(clientError(400, "Invalid input data", queryResult.error)); return; } const dbResult = await selectInterviewSchemas(queryResult.data); - if(!dbResult.success) { + if (!dbResult.success) { next(serverError(500, "Data processing error", dbResult.error)); return; } From 7187e68dade4c838c6d20d01814666e34f64b7d4 Mon Sep 17 00:00:00 2001 From: Vegard Kyrkjedelen Hovland Date: Mon, 2 Feb 2026 17:56:33 +0100 Subject: [PATCH 13/13] Delete files that popped up at some point during development These files should not be there. --- nim | 110 ----------------------------------------------------------- nvim | 110 ----------------------------------------------------------- 2 files changed, 220 deletions(-) delete mode 100644 nim delete mode 100644 nvim diff --git a/nim b/nim deleted file mode 100644 index 6b51ae8..0000000 --- a/nim +++ /dev/null @@ -1,110 +0,0 @@ -diff --git a/db/tables/applications.ts b/db/tables/applications.ts -index 264868c..9eb316b 100644 ---- a/db/tables/applications.ts -+++ b/db/tables/applications.ts -@@ -45,7 +45,7 @@ export const applicationsRelations = relations( - references: [assistantApplicationsTable.id], - }), - teamApplication: many(teamApplicationsTable), -- interview: many(interviewsTable), -+ //interview: many(interviewsTable), - }), - ); - -diff --git a/src/db-access/interviews.ts b/src/db-access/interviews.ts -index fa1e54b..f382056 100644 ---- a/src/db-access/interviews.ts -+++ b/src/db-access/interviews.ts -@@ -6,6 +6,7 @@ import { ormError, OrmResult } from "../error/orm-error"; - import type { InterviewSchema, InterviewSchemaKey } from "@/src/response-handling/interviews"; - import type { NewInterview, NewInterviewSchema } from "@/src/request-handling/interviews"; - import { interviewsTable } from "@/db/tables/interviews"; -+import { QueryParameters } from "../request-handling/common"; - - export async function selectInterviewSchemaWithId(id: InterviewSchemaKey[]): Promise> { - return await newDatabaseTransaction(database, async (tx) => { -@@ -20,6 +21,14 @@ export async function selectInterviewSchemaWithId(id: InterviewSchemaKey[]): Pro - }); - } - -+export async function selectInterviewSchemas(list_queries: QueryParameters) { -+ return await newDatabaseTransaction(database, async (tx) => { -+ const result = await tx.select().from(interviewSchemasTable).limit(list_queries.limit).offset(list_queries.offset); -+ -+ return result; -+ }) -+} -+ - export async function insertInterviewSchema(interviewSchemaRequests: NewInterviewSchema[]): Promise> { - return await newDatabaseTransaction(database, async (tx) => { - const result = await tx.insert(interviewSchemasTable).values(interviewSchemaRequests).returning(); -diff --git a/src/routers/interviews.ts b/src/routers/interviews.ts -index b8c0f0a..eaf6bb9 100644 ---- a/src/routers/interviews.ts -+++ b/src/routers/interviews.ts -@@ -1,9 +1,11 @@ - import { Router } from "express"; - import { newInterviewSchemaToInsertSchema, newInterviewToInsertSchema } from "../request-handling/interviews"; --import { clientError } from "../error/http-errors"; --import { insertInterview, insertInterviewSchema, selectInterviewSchemaWithId } from "../db-access/interviews"; -+import { clientError, serverError } from "../error/http-errors"; -+import { insertInterview, insertInterviewSchema, selectInterviewSchemas, selectInterviewSchemaWithId } from "../db-access/interviews"; - import { validateJsonSchema } from "@/lib/json-schema"; - import { JSONSchemaType } from "ajv"; -+import { serialIdParser, toListQueryParser, toSerialIdParser } from "../request-handling/common"; -+import { param } from "drizzle-orm"; - - - const interviewsRouter = Router(); -@@ -58,3 +60,51 @@ interviewsRouter.post("/schema", async (req, res, next) => { - } - res.json(databaseResult.data); - }); -+ -+interviewsRouter.get("/schema/:id", async (req, res, next) => { -+ const idParameterResult = toSerialIdParser.safeParse(req.params.id); -+ if(!idParameterResult.success) { -+ next(clientError(400, "Invalid input data", idParameterResult.error)); -+ return; -+ } -+ const schemaResult = await selectInterviewSchemaWithId([idParameterResult.data]); -+ if(!schemaResult.success) { -+ next(clientError(400, "Database error", schemaResult.error)); -+ return; -+ } -+ res.json(schemaResult.data); -+}); -+ -+interviewsRouter.get("/schema", async (req, res, next) => { -+ const queryResult = toListQueryParser.safeParse(req.query); -+ if(!queryResult.success) { -+ next(clientError(400, "Invalid input data", queryResult.error)); -+ return; -+ } -+ const dbResult = await selectInterviewSchemas(queryResult.data); -+ if(!dbResult.success) { -+ next(serverError(500, "Data processing error", dbResult.error)); -+ return; -+ } -+ res.json(dbResult.data); -+}); -+ -+interviewsRouter.get("/:id", async (req, res, next) => { -+ const parameterResult = serialIdParser.safeParse(req.params.id); -+ if(!parameterResult.success) { -+ next(clientError(400, "Invalid input data", parameterResult.error)); -+ } -+}); -+ -+ -+function queryParseCall(parser, databaseCall) { -+ return (req, res, next) => { -+ const parseResult = parser.safeParse(req.query); -+ if(!parseResult.success) { -+ next(clientError(400, "Invalid input data", parseResult.error)); -+ return; -+ } -+ const db -+ -+ } -+} diff --git a/nvim b/nvim deleted file mode 100644 index 6b51ae8..0000000 --- a/nvim +++ /dev/null @@ -1,110 +0,0 @@ -diff --git a/db/tables/applications.ts b/db/tables/applications.ts -index 264868c..9eb316b 100644 ---- a/db/tables/applications.ts -+++ b/db/tables/applications.ts -@@ -45,7 +45,7 @@ export const applicationsRelations = relations( - references: [assistantApplicationsTable.id], - }), - teamApplication: many(teamApplicationsTable), -- interview: many(interviewsTable), -+ //interview: many(interviewsTable), - }), - ); - -diff --git a/src/db-access/interviews.ts b/src/db-access/interviews.ts -index fa1e54b..f382056 100644 ---- a/src/db-access/interviews.ts -+++ b/src/db-access/interviews.ts -@@ -6,6 +6,7 @@ import { ormError, OrmResult } from "../error/orm-error"; - import type { InterviewSchema, InterviewSchemaKey } from "@/src/response-handling/interviews"; - import type { NewInterview, NewInterviewSchema } from "@/src/request-handling/interviews"; - import { interviewsTable } from "@/db/tables/interviews"; -+import { QueryParameters } from "../request-handling/common"; - - export async function selectInterviewSchemaWithId(id: InterviewSchemaKey[]): Promise> { - return await newDatabaseTransaction(database, async (tx) => { -@@ -20,6 +21,14 @@ export async function selectInterviewSchemaWithId(id: InterviewSchemaKey[]): Pro - }); - } - -+export async function selectInterviewSchemas(list_queries: QueryParameters) { -+ return await newDatabaseTransaction(database, async (tx) => { -+ const result = await tx.select().from(interviewSchemasTable).limit(list_queries.limit).offset(list_queries.offset); -+ -+ return result; -+ }) -+} -+ - export async function insertInterviewSchema(interviewSchemaRequests: NewInterviewSchema[]): Promise> { - return await newDatabaseTransaction(database, async (tx) => { - const result = await tx.insert(interviewSchemasTable).values(interviewSchemaRequests).returning(); -diff --git a/src/routers/interviews.ts b/src/routers/interviews.ts -index b8c0f0a..eaf6bb9 100644 ---- a/src/routers/interviews.ts -+++ b/src/routers/interviews.ts -@@ -1,9 +1,11 @@ - import { Router } from "express"; - import { newInterviewSchemaToInsertSchema, newInterviewToInsertSchema } from "../request-handling/interviews"; --import { clientError } from "../error/http-errors"; --import { insertInterview, insertInterviewSchema, selectInterviewSchemaWithId } from "../db-access/interviews"; -+import { clientError, serverError } from "../error/http-errors"; -+import { insertInterview, insertInterviewSchema, selectInterviewSchemas, selectInterviewSchemaWithId } from "../db-access/interviews"; - import { validateJsonSchema } from "@/lib/json-schema"; - import { JSONSchemaType } from "ajv"; -+import { serialIdParser, toListQueryParser, toSerialIdParser } from "../request-handling/common"; -+import { param } from "drizzle-orm"; - - - const interviewsRouter = Router(); -@@ -58,3 +60,51 @@ interviewsRouter.post("/schema", async (req, res, next) => { - } - res.json(databaseResult.data); - }); -+ -+interviewsRouter.get("/schema/:id", async (req, res, next) => { -+ const idParameterResult = toSerialIdParser.safeParse(req.params.id); -+ if(!idParameterResult.success) { -+ next(clientError(400, "Invalid input data", idParameterResult.error)); -+ return; -+ } -+ const schemaResult = await selectInterviewSchemaWithId([idParameterResult.data]); -+ if(!schemaResult.success) { -+ next(clientError(400, "Database error", schemaResult.error)); -+ return; -+ } -+ res.json(schemaResult.data); -+}); -+ -+interviewsRouter.get("/schema", async (req, res, next) => { -+ const queryResult = toListQueryParser.safeParse(req.query); -+ if(!queryResult.success) { -+ next(clientError(400, "Invalid input data", queryResult.error)); -+ return; -+ } -+ const dbResult = await selectInterviewSchemas(queryResult.data); -+ if(!dbResult.success) { -+ next(serverError(500, "Data processing error", dbResult.error)); -+ return; -+ } -+ res.json(dbResult.data); -+}); -+ -+interviewsRouter.get("/:id", async (req, res, next) => { -+ const parameterResult = serialIdParser.safeParse(req.params.id); -+ if(!parameterResult.success) { -+ next(clientError(400, "Invalid input data", parameterResult.error)); -+ } -+}); -+ -+ -+function queryParseCall(parser, databaseCall) { -+ return (req, res, next) => { -+ const parseResult = parser.safeParse(req.query); -+ if(!parseResult.success) { -+ next(clientError(400, "Invalid input data", parseResult.error)); -+ return; -+ } -+ const db -+ -+ } -+}