Skip to content

Commit

Permalink
Add ragger
Browse files Browse the repository at this point in the history
  • Loading branch information
omfj committed Aug 29, 2024
1 parent 388150e commit b220192
Show file tree
Hide file tree
Showing 16 changed files with 624 additions and 2 deletions.
28 changes: 28 additions & 0 deletions apps/ragger/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
# dev
.yarn/
!.yarn/releases
.vscode/*
!.vscode/launch.json
!.vscode/*.code-snippets
.idea/workspace.xml
.idea/usage.statistics.xml
.idea/shelf

# deps
node_modules/

# env
.env
.env.production

# logs
logs/
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*

# misc
.DS_Store
24 changes: 24 additions & 0 deletions apps/ragger/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
{
"name": "@echo-webkom/ragger",
"scripts": {
"dev": "pnpm with-env tsx watch src/index.ts",
"start": "pnpm with-env tsx src/index.ts",
"clean": "rm -rf .turbo node_modules",
"lint": "eslint",
"typecheck": "tsc --noEmit",
"with-env": "dotenv -e ../../.env --"
},
"dependencies": {
"@echo-webkom/db": "workspace:*",
"@hono/node-server": "1.12.2",
"date-fns": "3.6.0",
"drizzle-orm": "0.33.0",
"hono": "4.5.9",
"zod": "3.23.8"
},
"devDependencies": {
"@types/node": "20.11.17",
"dotenv-cli": "7.4.2",
"tsx": "4.7.1"
}
}
13 changes: 13 additions & 0 deletions apps/ragger/src/app.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { Hono } from "hono";

export type Bindings = {
ADMIN_KEY: string;
};

export const createApp = () => {
const app = new Hono<{
Bindings: Bindings;
}>();

return app;
};
17 changes: 17 additions & 0 deletions apps/ragger/src/correct-spot-range.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { SpotRange } from "@echo-webkom/db/schemas";

export const getCorrectSpotrange = (
year: number,
spotRanges: Array<SpotRange>,
canSkipSpotRange: boolean,
) => {
return (
spotRanges.find((spotRange) => {
if (canSkipSpotRange) {
return true;
}

return year >= spotRange.minYear && year <= spotRange.maxYear;
}) ?? null
);
};
11 changes: 11 additions & 0 deletions apps/ragger/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { serve } from "@hono/node-server";

import app from "./routes";

const PORT = 4444;
console.log(`Server is running on port ${PORT}`);

serve({
fetch: app.fetch,
port: PORT,
});
41 changes: 41 additions & 0 deletions apps/ragger/src/is-user-banned.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import type { Happening, User } from "@echo-webkom/db/schemas";

import { getHappeningsFromDateToDate, getStrike } from "./queries";

export const BAN_LENGTH = 3;

export const isUserBanned = async (user: User, bedpres: Happening) => {
if (bedpres.type !== "bedpres") {
return false;
}

if (!bedpres.date) {
return false;
}

if (!user.year) {
return false;
}

if (!user.bannedFromStrike || !user.isBanned) {
return false;
}

const strike = await getStrike(user.bannedFromStrike);
const dateBanned = strike?.strikeInfo.createdAt;
if (!dateBanned) {
return false;
}

const happenings = await getHappeningsFromDateToDate(dateBanned, bedpres.date);

const available = happenings
.filter((happening) =>
happening.spotRanges.some(
(spotRange) => user.year! >= spotRange.minYear && user.year! <= spotRange.maxYear,
),
)
.filter((happening) => happening !== bedpres);

return available.length < BAN_LENGTH;
};
15 changes: 15 additions & 0 deletions apps/ragger/src/is-user-complete.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import type { User } from "@echo-webkom/db/schemas";

type CompleteUser = User & {
degreeId: string;
year: number;
hasReadTerms: boolean;
};

export const isUserComplete = (user: User): user is CompleteUser => {
if (!user.degreeId || !user.year || !user.hasReadTerms) {
return false;
}

return true;
};
26 changes: 26 additions & 0 deletions apps/ragger/src/list.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
/**
* Returns the intersection of two lists.
*
* @example
* ```ts
* intersection([1, 2, 3], [2, 3, 4]); // [2, 3]
* ```
*
* @param a list a
* @param b list b
* @returns the elements that are in both lists
*/
export const intersection = <T>(a: Array<T>, b: Array<T>): Array<T> => {
return a.filter((value) => b.includes(value));
};

/**
* Checks if two lists have any elements in common.
*
* @param a list a
* @param b list b
* @returns true if the lists have any elements in common, false otherwise
*/
export const doesIntersect = <T>(a: Array<T>, b: Array<T>): boolean => {
return intersection(a, b).length > 0;
};
8 changes: 8 additions & 0 deletions apps/ragger/src/logger.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
export const logger = {
info: (message: string, meta?: Record<string, unknown>) => {
console.log("ℹ️ [INFO]", message, JSON.stringify(meta));
},
error: (message: string, meta?: Record<string, unknown>) => {
console.error("❌ [ERROR]", message, JSON.stringify(meta));
},
};
23 changes: 23 additions & 0 deletions apps/ragger/src/middleware.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import type { MiddlewareHandler } from "hono";

import type { Bindings } from "./app";

/**
* Check if the request authorization header matches the secret
*
* @param c hono context
* @param next next handler
* @returns the next handler
*/
export const auth: MiddlewareHandler<{ Bindings: Bindings }> = async (c, next) => {
if (!c.env.ADMIN_KEY) {
return next();
}

const token = c.req.header("Authorization");
if (token !== `Bearer ${c.env.ADMIN_KEY}`) {
return c.text("Unauthorized", { status: 401 });
}

return next();
};
145 changes: 145 additions & 0 deletions apps/ragger/src/queries.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
import { and, eq, gte, lte, or, sql } from "drizzle-orm";

import { db } from "@echo-webkom/db";
import { AnswerInsert, answers, registrations, SpotRange, users } from "@echo-webkom/db/schemas";

import { RegisterJson } from "./schema";

export const getUser = async (userId: string) => {
return await db.query.users.findFirst({
where: (row, { eq }) => eq(row.id, userId),
with: {
memberships: {
with: {
group: true,
},
},
},
});
};

export const getHappening = async (happeningId: string) => {
return await db.query.happenings.findFirst({
where: (row, { eq }) => eq(row.id, happeningId),
with: {
questions: true,
spotRanges: true,
},
});
};

export const getStrike = async (strikeId: number) => {
return await db.query.strikes.findFirst({
where: (row, { eq }) => eq(row.id, strikeId),
with: {
strikeInfo: true,
},
});
};

export const getHappeningsFromDateToDate = async (fromDate: Date, toDate: Date) => {
return await db.query.happenings.findMany({
where: (happening, { and, eq, gt, lt }) =>
and(eq(happening.type, "bedpres"), gt(happening.date, fromDate), lt(happening.date, toDate)),
with: {
spotRanges: true,
},
orderBy: (happening, { asc }) => [asc(happening.date)],
});
};

export const getExisitingRegistration = async (userId: string, happeningId: string) => {
return await db.query.registrations.findFirst({
where: (registration, { and, eq, or }) =>
and(
eq(registration.userId, userId),
eq(registration.happeningId, happeningId),
or(eq(registration.status, "registered"), eq(registration.status, "waiting")),
),
});
};

export const getHostGroups = async (id: string) => {
return await db.query.happeningsToGroups
.findMany({
where: (happeningToGroup, { eq }) => eq(happeningToGroup.happeningId, id),
})
.then((groups) => groups.map((group) => group.groupId));
};

export const registerUserToHappening = async (
userId: string,
happeningId: string,
spotRange: SpotRange,
) => {
return await db.transaction(
async (tx) => {
await tx.execute(sql`LOCK TABLE ${registrations} IN EXCLUSIVE MODE`);

const regs = await tx
.select()
.from(registrations)
.where(
and(
eq(registrations.happeningId, happeningId),
lte(users.year, spotRange.maxYear),
gte(users.year, spotRange.minYear),
or(eq(registrations.status, "registered"), eq(registrations.status, "waiting")),
),
)
.leftJoin(users, eq(registrations.userId, users.id));

const isInfiniteSpots = spotRange.spots === 0;
const isWaitlisted = !isInfiniteSpots && regs.length >= spotRange.spots;

const registration = await tx
.insert(registrations)
.values({
status: isWaitlisted ? "waiting" : "registered",
happeningId,
userId,
changedBy: null,
})
.returning()
.onConflictDoUpdate({
target: [registrations.happeningId, registrations.userId],
set: {
status: isWaitlisted ? "waiting" : "registered",
},
})
.then((res) => res[0] ?? null);

return {
registration,
isWaitlisted,
};
},
{
isolationLevel: "read committed",
},
);
};

export const insertAnswers = async (
happeningId: string,
userId: string,
questions: RegisterJson["questions"],
) => {
const answersToInsert = questions.map(
(question) =>
({
happeningId: happeningId,
userId: userId,
questionId: question.questionId,
answer: question.answer
? {
answer: question.answer,
}
: null,
}) satisfies AnswerInsert,
);

if (answersToInsert.length > 0) {
await db.insert(answers).values(answersToInsert).onConflictDoNothing();
}
};
Loading

0 comments on commit b220192

Please sign in to comment.