Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
86 changes: 49 additions & 37 deletions db/config/parameters.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,51 +16,63 @@ const parametersResult = z
DATABASE_PASSWORD: z.string().nonempty(),
DATABASE_PORT: toPortParser,
DATABASE_SSL_OPTION: z
.union([
z.literal("prod").transform(() => {
return {
requestCert: true,
rejectUnauthorized: true,
} as ConnectionOptions;
}),
z.literal("prod-provide_ca_cert").transform((_, ctx) => {
const caCert = getCaCert();
if (caCert === undefined) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: "Could not find ca certificate",
});
return z.NEVER;
}
return {
requestCert: true,
rejectUnauthorized: true,
ca: caCert,
} as ConnectionOptions;
}),
z.literal("dev").transform(() => {
return {
requestCert: true,
rejectUnauthorized: false,
} as ConnectionOptions;
}),
z.literal("true").transform(() => {
return true;
}),
z.literal("false").transform(() => {
return false;
}),
])
.enum(["prod", "prod-provide_ca_cert", "dev", "true", "false"])
.default("prod"),
})
.transform((schema) => {
.transform((schema, ctx) => {
let ssl: boolean | ConnectionOptions;
switch (schema.DATABASE_SSL_OPTION) {
case "prod":
ssl = {
requestCert: true,
rejectUnauthorized: true,
};
break;
case "prod-provide_ca_cert": {
const caCert = getCaCert();
if (caCert === undefined) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: "Could not find ca certificate",
path: ["DATABASE_SSL_OPTION"],
});
return z.NEVER;
}
ssl = {
requestCert: true,
rejectUnauthorized: true,
ca: caCert,
};
break;
}
case "dev":
ssl = {
requestCert: true,
rejectUnauthorized: false,
};
break;
case "true":
ssl = true;
break;
case "false":
ssl = false;
break;
default: {
ctx.addIssue({
code: "custom",
message: "No valid ssl option",
path: ["DATABASE_SSL_OPTION"],
});
return z.NEVER;
}
}
return {
host: schema.DATABASE_HOST.trim(),
database: schema.DATABASE_NAME.trim(),
user: schema.DATABASE_USER.trim(),
password: schema.DATABASE_PASSWORD.trim(),
port: schema.DATABASE_PORT,
ssl: schema.DATABASE_SSL_OPTION,
ssl,
};
})
.safeParse(env);
Expand Down
2 changes: 1 addition & 1 deletion db/errors/postgres-error-constants-parsers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import {
POSTGRES_NOTICE_SEVERITIES,
PUBLIC_POSTGRES_ERROR_CLASSES,
} from "@/db/errors/postgres-error-constants";
import { zodEnumFromObjKeys } from "@/lib/lib";
import { zodEnumFromObjKeys } from "@/lib/zod";
import { z } from "zod";

export const postgresErrorCodeParser = zodEnumFromObjKeys(
Expand Down
22 changes: 10 additions & 12 deletions db/errors/postgres-error.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,19 @@ import {
postgresSeverityParser,
publicPostgresErrorClassParser,
} from "@/db/errors/postgres-error-constants-parsers";
import { parseWithSchema } from "@/lib/zod";
import { z } from "zod";

// Inspiration from
// https://www.postgresql.org/docs/9.3/protocol-error-fields.html and
// https://github.com/brianc/node-postgres/blob/master/packages/pg-protocol/src/messages.ts

const nonnegativeIntFromStringParser = z
.string()
.transform(
parseWithSchema(z.coerce.number().finite().safe().int().nonnegative()),
);

export const postgresErrorParser = z
.object({
length: z.number().finite().safe().int().nonnegative(),
Expand All @@ -19,14 +26,8 @@ export const postgresErrorParser = z
severity: postgresSeverityParser,
detail: z.string().optional(),
hint: z.string().optional(),
position: z
.string()
.pipe(z.coerce.number().finite().safe().int().nonnegative())
.optional(),
internalPosition: z
.string()
.pipe(z.coerce.number().finite().safe().int().nonnegative())
.optional(),
position: nonnegativeIntFromStringParser.optional(),
internalPosition: nonnegativeIntFromStringParser.optional(),
internalQuery: z.string().optional(),
where: z.string().optional(),
schema: z.string().optional(),
Expand All @@ -35,10 +36,7 @@ export const postgresErrorParser = z
dataType: z.string().optional(),
constaint: z.string().optional(),
file: z.string().optional(),
line: z
.string()
.pipe(z.coerce.number().finite().safe().int().nonnegative())
.optional(),
line: nonnegativeIntFromStringParser.optional(),
routine: z.string().optional(),
})
.readonly()
Expand Down
3 changes: 2 additions & 1 deletion lib/finance-parsers.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { parseWithSchema } from "@/lib/zod";
import validator from "validator";

Check warning on line 2 in lib/finance-parsers.ts

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Import validator methods from subpaths such as "validator/es/lib/isEmail" to avoid importing more code than needed.

See more on https://sonarcloud.io/project/issues?id=vektorprogrammet_api&issues=AaA-9Q7zRwvl8ETcAbet&open=AaA-9Q7zRwvl8ETcAbet&pullRequest=145
import { z } from "zod";

export const currencyParser = z.string().refine((input) => {
Expand Down Expand Up @@ -27,4 +28,4 @@
.string()
.length(11)
.transform((string) => `NO93${string}`)
.pipe(norwegianIbanParser);
.transform(parseWithSchema(norwegianIbanParser));
25 changes: 10 additions & 15 deletions lib/json-schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,21 +31,16 @@ export function validateJsonSchema(
}

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;
});
return z.looseObject({}).superRefine((data, ctx) => {
const validationResult = validateJsonSchema(schema, data);
if (!validationResult.success) {
ctx.addIssue({
code: "custom",
message: "The interview schema is not valid",
params: validationResult.error,
});
}
});
}

// from: https://www.reddit.com/r/typescript/comments/13mssvc/types_for_json_and_writing_json
Expand Down
13 changes: 0 additions & 13 deletions lib/lib.ts

This file was deleted.

19 changes: 9 additions & 10 deletions lib/network-parsers.ts
Original file line number Diff line number Diff line change
@@ -1,24 +1,23 @@
import { isIP } from "node:net";
import { z } from "zod";

export const portParser = z
.number({
invalid_type_error: "not a valid port. must be a number",
})
.number({ error: "not a valid port. must be a number" })
.nonnegative("ports cannot be negative numbers")
.max(65535, "ports cannot be higher than 65535(2^16 - 1)")
.finite()
.safe()
.int("ports must have integer values");

export const toPortParser = z
.union([z.number(), z.string()])
.pipe(z.coerce.number())
.pipe(portParser);
export const toPortParser = z.coerce.number().pipe(portParser);

export const hostingStringParser = z.union(
[z.literal("localhost"), z.string().url(), z.string().ip()],
[
z.literal("localhost"),
z.string().url(),
z.string().refine((value) => isIP(value) !== 0, "not a valid IP address"),
],
{
invalid_type_error:
"not a valid host string, must be localhost, a url or an IP-adress",
error: "not a valid host string, must be localhost, a url or an IP-adress",
},
);
21 changes: 10 additions & 11 deletions lib/time-parsers.ts
Original file line number Diff line number Diff line change
@@ -1,22 +1,21 @@
import { parseWithSchema } from "@/lib/zod";
import { z } from "zod";

export const timeStringParser = z.union([
z.string().date(),
z.string().time(),
z.string().datetime(),
z.iso.date(),
z.iso.time(),
z.iso.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()])
.pipe(z.coerce.date())
.pipe(dateParser);
.transform(parseWithSchema(z.coerce.date()));

export const datePeriodParser = z
.object({
startDate: dateParser,
endDate: dateParser,
startDate: z.date(),
endDate: z.date(),
})
.refine((datePeriod) => {
return datePeriod.startDate.getTime() <= datePeriod.endDate.getTime();
Expand All @@ -27,9 +26,9 @@ export const toDatePeriodParser = z
startDate: toDateParser,
endDate: toDateParser,
})
.pipe(datePeriodParser);
.transform(parseWithSchema(datePeriodParser));

export const pastDateParser = dateParser.max(new Date());
export const futureDateParser = dateParser.min(new Date());
export const pastDateParser = z.date().max(new Date());
export const futureDateParser = z.date().min(new Date());

export type DatePeriod = z.infer<typeof datePeriodParser>;
26 changes: 26 additions & 0 deletions lib/zod.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { z } from "zod";

export function parseWithSchema<T extends z.ZodType>(schema: T) {
return (value: unknown, ctx: z.RefinementCtx): z.output<T> => {
const result = schema.safeParse(value);
if (!result.success) {
for (const issue of result.error.issues) {
ctx.addIssue({ ...issue });
}
return z.NEVER;
}
return result.data;
};
}

// modified from https://github.com/colinhacks/zod/discussions/839#discussioncomment-4335236
export function zodEnumFromObjKeys<K extends string>(
obj: Record<K, unknown>,
): z.ZodEnum<Record<K, K>> {
const keys = Object.keys(obj) as K[];
return z.enum(keys);
}
const phoneNumberRegex = /^\d{8}$/;
export const phoneNumberParser = z
.string()
.regex(phoneNumberRegex, "Phone number must be 8 digits");
9 changes: 4 additions & 5 deletions openapi/config.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import "zod-openapi/extend";
import { datePeriodParser } from "@/lib/time-parsers";
import { hostOptions } from "@/src/enviroment";
import { teamApplicationParser } from "@/src/request-handling/applications";
Expand Down Expand Up @@ -88,10 +87,10 @@ const openapiDocument = createDocument({
teams: teamsRequestParser,
},
parameters: {
id: serialIdParser.openapi({ param: { in: "path", name: "id" } }),
limit: limitParser.openapi({ param: { in: "query", name: "limit" } }),
sort: sortParser.openapi({ param: { in: "query", name: "sort" } }),
offset: offsetParser.openapi({
id: serialIdParser.meta({ param: { in: "path", name: "id" } }),
limit: limitParser.meta({ param: { in: "query", name: "limit" } }),
sort: sortParser.meta({ param: { in: "query", name: "sort" } }),
offset: offsetParser.meta({
param: { in: "query", name: "offset" },
}),
},
Expand Down
14 changes: 7 additions & 7 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -25,17 +25,17 @@
"ajv": "^8.17.1",
"cors": "^2.8.5",
"dotenv": "^16.4.7",
"drizzle-orm": "^0.33.0",
"drizzle-orm": "^0.45.2",
"drizzle-seed": "^0.3.1",
"drizzle-zod": "^0.5.1",
"drizzle-zod": "^0.8.3",
"express": "^5.1.0",
"express-zod-safe": "^1.3.3",
"express-zod-safe": "^3.2.1",
"pg": "^8.14.1",
"swagger-jsdoc": "^6.2.8",
"validator": "^13.15.0",
"zod": "^3.24.2",
"zod-openapi": "^3.3.0",
"zod-validation-error": "^3.4.0"
"zod": "^4.4.3",
"zod-openapi": "^6.0.1",
"zod-validation-error": "^5.0.0"
},
"devDependencies": {
"@biomejs/biome": "1.9.3",
Expand All @@ -46,7 +46,7 @@
"@types/supertest": "^6.0.3",
"@types/swagger-jsdoc": "^6.0.4",
"@types/validator": "^13.12.3",
"drizzle-kit": "^0.24.2",
"drizzle-kit": "^0.31.10",
"supertest": "^7.1.0",
"tsc-alias": "^1.8.13",
"tsx": "^4.19.3",
Expand Down
Loading
Loading