From bcdc1bd99161ca436723069900684418aeb51298 Mon Sep 17 00:00:00 2001 From: Nudesuppe42 Date: Fri, 22 May 2026 18:41:06 +0200 Subject: [PATCH 01/31] feat(worker): :boom: Initial Commit, intial entry point, prisma --- .vscode/settings.json | 3 +- apps/worker/.infisical.json | 5 + apps/worker/README.md | 14 + apps/worker/package.json | 30 + apps/worker/src/lib/logger.ts | 103 +++ apps/worker/src/lib/prisma.ts | 27 + apps/worker/src/main.ts | 37 + apps/worker/tsconfig.json | 24 + apps/worker/tsconfig.runtime.json | 8 + packages/db/src/generated/prisma/client.d.ts | 25 + .../generated/prisma/commonInputTypes.d.ts | 447 ++++++++++ packages/db/src/generated/prisma/enums.d.ts | 21 + packages/db/src/generated/prisma/models.d.ts | 18 + packages/db/src/index.d.ts | 1 + yarn.lock | 798 +++++++++++++++++- 15 files changed, 1549 insertions(+), 12 deletions(-) create mode 100644 apps/worker/.infisical.json create mode 100644 apps/worker/README.md create mode 100644 apps/worker/package.json create mode 100644 apps/worker/src/lib/logger.ts create mode 100644 apps/worker/src/lib/prisma.ts create mode 100644 apps/worker/src/main.ts create mode 100644 apps/worker/tsconfig.json create mode 100644 apps/worker/tsconfig.runtime.json create mode 100644 packages/db/src/generated/prisma/client.d.ts create mode 100644 packages/db/src/generated/prisma/commonInputTypes.d.ts create mode 100644 packages/db/src/generated/prisma/enums.d.ts create mode 100644 packages/db/src/generated/prisma/models.d.ts create mode 100644 packages/db/src/index.d.ts diff --git a/.vscode/settings.json b/.vscode/settings.json index 9ed43d15..00227c3a 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -40,7 +40,8 @@ "frontend/map", "frontend/assets", "dash/team", - "frontend/mobile" + "frontend/mobile", + "worker" ], "js/ts.tsdk.path": "node_modules\\typescript\\lib" } diff --git a/apps/worker/.infisical.json b/apps/worker/.infisical.json new file mode 100644 index 00000000..eead4a0f --- /dev/null +++ b/apps/worker/.infisical.json @@ -0,0 +1,5 @@ +{ + "workspaceId": "1609eb2b-2a36-4273-bd12-470f3f3ad35e", + "defaultEnvironment": "dev", + "gitBranchToEnvironmentMapping": null +} diff --git a/apps/worker/README.md b/apps/worker/README.md new file mode 100644 index 00000000..1507f136 --- /dev/null +++ b/apps/worker/README.md @@ -0,0 +1,14 @@ + +
+ + + +# Website Worker + +_Independet worker for handling events._ + +![official](https://go.buildtheearth.net/official-shield) +[![chat](https://img.shields.io/discord/706317564904472627.svg?color=768AD4&label=discord&logo=https%3A%2F%2Fdiscordapp.com%2Fassets%2F8c9701b98ad4372b58f13fd9f65f966e.svg)](https://discord.gg/buildtheearth) + +
+ diff --git a/apps/worker/package.json b/apps/worker/package.json new file mode 100644 index 00000000..28507d40 --- /dev/null +++ b/apps/worker/package.json @@ -0,0 +1,30 @@ +{ + "name": "worker", + "version": "1.0.0", + "private": true, + "packageManager": "yarn@4.9.1+sha512.f95ce356460e05be48d66401c1ae64ef84d163dd689964962c6888a9810865e39097a5e9de748876c2e0bf89b232d583c33982773e9903ae7a76257270986538", + "scripts": { + "dev": "infisical run --path=\"/worker\" -- tsx watch --tsconfig tsconfig.runtime.json src/main.ts", + "build": "tsc", + "start": "infisical run --path=\"/worker\" -- tsx --tsconfig tsconfig.runtime.json dist/main.js", + "prettier": "prettier ./src --write --ignore-unknown" + }, + "devDependencies": { + "@repo/prettier-config": "workspace:^", + "@repo/typescript-config": "workspace:^", + "@types/node": "^22", + "tsx": "^4.22.3", + "typescript": "^5.6.3" + }, + "dependencies": { + "@prisma/adapter-pg": "^7.8.0", + "@repo/db": "*", + "bullmq": "^5.77.0", + "ioredis": "^5.10.1", + "winston": "^3.19.0" + }, + "prettier": "@repo/prettier-config", + "lint-staged": { + "*": "prettier --write --ignore-unknown" + } +} diff --git a/apps/worker/src/lib/logger.ts b/apps/worker/src/lib/logger.ts new file mode 100644 index 00000000..2cbb2b0b --- /dev/null +++ b/apps/worker/src/lib/logger.ts @@ -0,0 +1,103 @@ +import winston from 'winston'; + +type LogMeta = { + timestamp: unknown; + level: string; + requestId?: string; + component?: string; + fields?: Record; +}; + +const formatFieldValue = (value: unknown): string => { + if (typeof value === 'string') { + return value; + } + + if (typeof value === 'number' || typeof value === 'boolean' || typeof value === 'bigint') { + return String(value); + } + + if (value instanceof Error) { + return value.stack ?? `${value.name}: ${value.message}`; + } + + try { + return JSON.stringify(value); + } catch { + return String(value); + } +}; + +const formatPrefix = ({ timestamp, level, requestId, component = 'main', fields = {} }: LogMeta): string => { + return `${timestamp} | ${level} [${requestId || component}] ยป `; +}; + +const formatLine = (content: string | Record, meta: LogMeta): string => { + if (typeof content === 'string') { + return `${formatPrefix(meta)}${content}`; + } + + try { + return formatLine(JSON.stringify(content), meta); + } catch { + return formatLine(String(content), meta); + } +}; + +const formatBlock = (content: string, meta: LogMeta): string => { + return content + .split(/\r?\n/) + .filter((line) => line.length > 0) + .map((line) => formatLine(line, meta)) + .join('\n'); +}; + +const loggerConfig = { + development: { + level: 'debug', + format: winston.format.combine( + winston.format.errors({ stack: true }), + winston.format.timestamp(), + winston.format.colorize(), + winston.format.simple(), + winston.format.printf((info) => { + const { level, message, timestamp, requestId, component, error, stack, label, ...rest } = + info as winston.Logform.TransformableInfo & { + requestId?: string; + component?: string; + error?: unknown; + stack?: string; + }; + + const lines = [message]; + const errorDetails = formatFieldValue(error) ?? stack; + + if (errorDetails) { + lines.push(errorDetails); + } + + return formatBlock(lines.join('\n'), { + timestamp, + level, + requestId, + component, + fields: rest, + }); + }), + ), + transports: [new winston.transports.Console()], + }, + production: { + level: 'info', + format: winston.format.combine( + winston.format.errors({ stack: true }), + winston.format.timestamp(), + winston.format.json(), + ), + transports: [new winston.transports.Console()], + }, +}; + +export const logger = winston.createLogger( + process.env.NODE_ENV === 'production' ? loggerConfig.production : loggerConfig.development, +); diff --git a/apps/worker/src/lib/prisma.ts b/apps/worker/src/lib/prisma.ts new file mode 100644 index 00000000..1b09179e --- /dev/null +++ b/apps/worker/src/lib/prisma.ts @@ -0,0 +1,27 @@ +import { PrismaPg } from '@prisma/adapter-pg'; +import { PrismaClient } from '@repo/db'; + +const prismaClientSingleton = () => { + const adapter = new PrismaPg({ connectionString: process.env.DATABASE_URL }); + return new PrismaClient({ adapter }).$extends({ + name: 'uploadSrc', + result: { + upload: { + src: { + needs: { name: true }, + compute: (upload) => { + return `https://cdn.buildtheearth.net/uploads/${upload.name}`; + }, + }, + }, + }, + }) as unknown as PrismaClient; +}; + +declare const globalThis: { prismaGlobal: ReturnType } & typeof global; + +const prisma = (globalThis.prismaGlobal ?? prismaClientSingleton()) as ReturnType; + +export default prisma; + +if (process.env.NODE_ENV !== 'production') globalThis.prismaGlobal = prisma; diff --git a/apps/worker/src/main.ts b/apps/worker/src/main.ts new file mode 100644 index 00000000..9a168c24 --- /dev/null +++ b/apps/worker/src/main.ts @@ -0,0 +1,37 @@ +import { logger } from './lib/logger'; +import prisma from './lib/prisma'; + +console.clear(); +console.log( + '\n' + + ` /$$ \n` + + ` | $$ \n` + + ` /$$ /$$ /$$ /$$$$$$ /$$$$$$ | $$ /$$ /$$$$$$ /$$$$$$ \n` + + `| $$ | $$ | $$ /$$__ $$ /$$__ $$| $$ /$$/ /$$__ $$ /$$__ $$\n` + + `| $$ | $$ | $$| $$ \\ $$| $$ \\__/| $$$$$$/ | $$$$$$$$| $$ \\__/\n` + + `| $$ | $$ | $$| $$ | $$| $$ | $$_ $$ | $$_____/| $$ \n` + + `| $$$$$/$$$$/| $$$$$$/| $$ | $$ \\ $$| $$$$$$$| $$ \n` + + ` \\_____/\\___/ \\______/ |__/ |__/ \\__/ \\_______/|__/ \n` + + ` \n` + + `A BuildTheEarth Worker Node - Version ${process.env.npm_package_version}\n`, +); + +async function bootstrap() { + logger.info('Initializing Worker Node...'); + + try { + logger.debug('Trying to connect to database'); + await prisma.$connect(); + logger.info('Connected to database successfully'); + } catch (error: any) { + logger.error('Database connection failed', { error }); + process.exit(1); + } + + logger.debug('Bootstrap complete'); +} + +bootstrap().catch((err) => { + logger.error('Fatal error occurred during bootstrap', { error: err }); + process.exit(1); +}); diff --git a/apps/worker/tsconfig.json b/apps/worker/tsconfig.json new file mode 100644 index 00000000..9c704192 --- /dev/null +++ b/apps/worker/tsconfig.json @@ -0,0 +1,24 @@ +{ + "extends": "@repo/typescript-config/base.json", + "include": ["./src/**/*"], + "compilerOptions": { + "module": "NodeNext", + "moduleResolution": "NodeNext", + "declaration": true, + "removeComments": true, + "emitDecoratorMetadata": true, + "experimentalDecorators": true, + "allowSyntheticDefaultImports": true, + "target": "ES2023", + "sourceMap": true, + "outDir": "./dist", + "baseUrl": "./", + "rootDir": "./src", + "incremental": true, + "skipLibCheck": true, + "strictNullChecks": true, + "noImplicitAny": false, + "strictBindCallApply": false, + "noFallthroughCasesInSwitch": false + } +} diff --git a/apps/worker/tsconfig.runtime.json b/apps/worker/tsconfig.runtime.json new file mode 100644 index 00000000..85c20022 --- /dev/null +++ b/apps/worker/tsconfig.runtime.json @@ -0,0 +1,8 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "paths": { + "@repo/*": ["../../packages/*/src"] + } + } +} diff --git a/packages/db/src/generated/prisma/client.d.ts b/packages/db/src/generated/prisma/client.d.ts new file mode 100644 index 00000000..1faa51d1 --- /dev/null +++ b/packages/db/src/generated/prisma/client.d.ts @@ -0,0 +1,25 @@ +import * as runtime from "@prisma/client/runtime/client"; +import * as $Class from "./internal/class.ts"; +import * as Prisma from "./internal/prismaNamespace.ts"; +export * as $Enums from './enums.ts'; +export * from "./enums.ts"; +export declare const PrismaClient: $Class.PrismaClientConstructor; +export type PrismaClient = $Class.PrismaClient; +export { Prisma }; +export type User = Prisma.UserModel; +export type BuildTeam = Prisma.BuildTeamModel; +export type ApplicationQuestion = Prisma.ApplicationQuestionModel; +export type Social = Prisma.SocialModel; +export type Showcase = Prisma.ShowcaseModel; +export type UserPermission = Prisma.UserPermissionModel; +export type MinecraftVerifications = Prisma.MinecraftVerificationsModel; +export type Permisision = Prisma.PermisisionModel; +export type FAQQuestion = Prisma.FAQQuestionModel; +export type Contact = Prisma.ContactModel; +export type Claim = Prisma.ClaimModel; +export type Application = Prisma.ApplicationModel; +export type ApplicationAnswer = Prisma.ApplicationAnswerModel; +export type ApplicationResponseTemplate = Prisma.ApplicationResponseTemplateModel; +export type Upload = Prisma.UploadModel; +export type CalendarEvent = Prisma.CalendarEventModel; +export type JsonStore = Prisma.JsonStoreModel; diff --git a/packages/db/src/generated/prisma/commonInputTypes.d.ts b/packages/db/src/generated/prisma/commonInputTypes.d.ts new file mode 100644 index 00000000..c53ff508 --- /dev/null +++ b/packages/db/src/generated/prisma/commonInputTypes.d.ts @@ -0,0 +1,447 @@ +import type * as runtime from "@prisma/client/runtime/client"; +import * as $Enums from "./enums.ts"; +import type * as Prisma from "./internal/prismaNamespace.ts"; +export type StringFilter<$PrismaModel = never> = { + equals?: string | Prisma.StringFieldRefInput<$PrismaModel>; + in?: string[] | Prisma.ListStringFieldRefInput<$PrismaModel>; + notIn?: string[] | Prisma.ListStringFieldRefInput<$PrismaModel>; + lt?: string | Prisma.StringFieldRefInput<$PrismaModel>; + lte?: string | Prisma.StringFieldRefInput<$PrismaModel>; + gt?: string | Prisma.StringFieldRefInput<$PrismaModel>; + gte?: string | Prisma.StringFieldRefInput<$PrismaModel>; + contains?: string | Prisma.StringFieldRefInput<$PrismaModel>; + startsWith?: string | Prisma.StringFieldRefInput<$PrismaModel>; + endsWith?: string | Prisma.StringFieldRefInput<$PrismaModel>; + mode?: Prisma.QueryMode; + not?: Prisma.NestedStringFilter<$PrismaModel> | string; +}; +export type StringNullableFilter<$PrismaModel = never> = { + equals?: string | Prisma.StringFieldRefInput<$PrismaModel> | null; + in?: string[] | Prisma.ListStringFieldRefInput<$PrismaModel> | null; + notIn?: string[] | Prisma.ListStringFieldRefInput<$PrismaModel> | null; + lt?: string | Prisma.StringFieldRefInput<$PrismaModel>; + lte?: string | Prisma.StringFieldRefInput<$PrismaModel>; + gt?: string | Prisma.StringFieldRefInput<$PrismaModel>; + gte?: string | Prisma.StringFieldRefInput<$PrismaModel>; + contains?: string | Prisma.StringFieldRefInput<$PrismaModel>; + startsWith?: string | Prisma.StringFieldRefInput<$PrismaModel>; + endsWith?: string | Prisma.StringFieldRefInput<$PrismaModel>; + mode?: Prisma.QueryMode; + not?: Prisma.NestedStringNullableFilter<$PrismaModel> | string | null; +}; +export type SortOrderInput = { + sort: Prisma.SortOrder; + nulls?: Prisma.NullsOrder; +}; +export type StringWithAggregatesFilter<$PrismaModel = never> = { + equals?: string | Prisma.StringFieldRefInput<$PrismaModel>; + in?: string[] | Prisma.ListStringFieldRefInput<$PrismaModel>; + notIn?: string[] | Prisma.ListStringFieldRefInput<$PrismaModel>; + lt?: string | Prisma.StringFieldRefInput<$PrismaModel>; + lte?: string | Prisma.StringFieldRefInput<$PrismaModel>; + gt?: string | Prisma.StringFieldRefInput<$PrismaModel>; + gte?: string | Prisma.StringFieldRefInput<$PrismaModel>; + contains?: string | Prisma.StringFieldRefInput<$PrismaModel>; + startsWith?: string | Prisma.StringFieldRefInput<$PrismaModel>; + endsWith?: string | Prisma.StringFieldRefInput<$PrismaModel>; + mode?: Prisma.QueryMode; + not?: Prisma.NestedStringWithAggregatesFilter<$PrismaModel> | string; + _count?: Prisma.NestedIntFilter<$PrismaModel>; + _min?: Prisma.NestedStringFilter<$PrismaModel>; + _max?: Prisma.NestedStringFilter<$PrismaModel>; +}; +export type StringNullableWithAggregatesFilter<$PrismaModel = never> = { + equals?: string | Prisma.StringFieldRefInput<$PrismaModel> | null; + in?: string[] | Prisma.ListStringFieldRefInput<$PrismaModel> | null; + notIn?: string[] | Prisma.ListStringFieldRefInput<$PrismaModel> | null; + lt?: string | Prisma.StringFieldRefInput<$PrismaModel>; + lte?: string | Prisma.StringFieldRefInput<$PrismaModel>; + gt?: string | Prisma.StringFieldRefInput<$PrismaModel>; + gte?: string | Prisma.StringFieldRefInput<$PrismaModel>; + contains?: string | Prisma.StringFieldRefInput<$PrismaModel>; + startsWith?: string | Prisma.StringFieldRefInput<$PrismaModel>; + endsWith?: string | Prisma.StringFieldRefInput<$PrismaModel>; + mode?: Prisma.QueryMode; + not?: Prisma.NestedStringNullableWithAggregatesFilter<$PrismaModel> | string | null; + _count?: Prisma.NestedIntNullableFilter<$PrismaModel>; + _min?: Prisma.NestedStringNullableFilter<$PrismaModel>; + _max?: Prisma.NestedStringNullableFilter<$PrismaModel>; +}; +export type DateTimeFilter<$PrismaModel = never> = { + equals?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>; + in?: Date[] | string[] | Prisma.ListDateTimeFieldRefInput<$PrismaModel>; + notIn?: Date[] | string[] | Prisma.ListDateTimeFieldRefInput<$PrismaModel>; + lt?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>; + lte?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>; + gt?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>; + gte?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>; + not?: Prisma.NestedDateTimeFilter<$PrismaModel> | Date | string; +}; +export type BoolFilter<$PrismaModel = never> = { + equals?: boolean | Prisma.BooleanFieldRefInput<$PrismaModel>; + not?: Prisma.NestedBoolFilter<$PrismaModel> | boolean; +}; +export type BoolNullableFilter<$PrismaModel = never> = { + equals?: boolean | Prisma.BooleanFieldRefInput<$PrismaModel> | null; + not?: Prisma.NestedBoolNullableFilter<$PrismaModel> | boolean | null; +}; +export type DateTimeWithAggregatesFilter<$PrismaModel = never> = { + equals?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>; + in?: Date[] | string[] | Prisma.ListDateTimeFieldRefInput<$PrismaModel>; + notIn?: Date[] | string[] | Prisma.ListDateTimeFieldRefInput<$PrismaModel>; + lt?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>; + lte?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>; + gt?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>; + gte?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>; + not?: Prisma.NestedDateTimeWithAggregatesFilter<$PrismaModel> | Date | string; + _count?: Prisma.NestedIntFilter<$PrismaModel>; + _min?: Prisma.NestedDateTimeFilter<$PrismaModel>; + _max?: Prisma.NestedDateTimeFilter<$PrismaModel>; +}; +export type BoolWithAggregatesFilter<$PrismaModel = never> = { + equals?: boolean | Prisma.BooleanFieldRefInput<$PrismaModel>; + not?: Prisma.NestedBoolWithAggregatesFilter<$PrismaModel> | boolean; + _count?: Prisma.NestedIntFilter<$PrismaModel>; + _min?: Prisma.NestedBoolFilter<$PrismaModel>; + _max?: Prisma.NestedBoolFilter<$PrismaModel>; +}; +export type BoolNullableWithAggregatesFilter<$PrismaModel = never> = { + equals?: boolean | Prisma.BooleanFieldRefInput<$PrismaModel> | null; + not?: Prisma.NestedBoolNullableWithAggregatesFilter<$PrismaModel> | boolean | null; + _count?: Prisma.NestedIntNullableFilter<$PrismaModel>; + _min?: Prisma.NestedBoolNullableFilter<$PrismaModel>; + _max?: Prisma.NestedBoolNullableFilter<$PrismaModel>; +}; +export type EnumApplicationQuestionTypeFilter<$PrismaModel = never> = { + equals?: $Enums.ApplicationQuestionType | Prisma.EnumApplicationQuestionTypeFieldRefInput<$PrismaModel>; + in?: $Enums.ApplicationQuestionType[] | Prisma.ListEnumApplicationQuestionTypeFieldRefInput<$PrismaModel>; + notIn?: $Enums.ApplicationQuestionType[] | Prisma.ListEnumApplicationQuestionTypeFieldRefInput<$PrismaModel>; + not?: Prisma.NestedEnumApplicationQuestionTypeFilter<$PrismaModel> | $Enums.ApplicationQuestionType; +}; +export type JsonFilter<$PrismaModel = never> = Prisma.PatchUndefined>, Exclude>, 'path'>>, Required>> | Prisma.OptionalFlat>, 'path'>>; +export type JsonFilterBase<$PrismaModel = never> = { + equals?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel> | Prisma.JsonNullValueFilter; + path?: string[]; + mode?: Prisma.QueryMode | Prisma.EnumQueryModeFieldRefInput<$PrismaModel>; + string_contains?: string | Prisma.StringFieldRefInput<$PrismaModel>; + string_starts_with?: string | Prisma.StringFieldRefInput<$PrismaModel>; + string_ends_with?: string | Prisma.StringFieldRefInput<$PrismaModel>; + array_starts_with?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel> | null; + array_ends_with?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel> | null; + array_contains?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel> | null; + lt?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel>; + lte?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel>; + gt?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel>; + gte?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel>; + not?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel> | Prisma.JsonNullValueFilter; +}; +export type IntFilter<$PrismaModel = never> = { + equals?: number | Prisma.IntFieldRefInput<$PrismaModel>; + in?: number[] | Prisma.ListIntFieldRefInput<$PrismaModel>; + notIn?: number[] | Prisma.ListIntFieldRefInput<$PrismaModel>; + lt?: number | Prisma.IntFieldRefInput<$PrismaModel>; + lte?: number | Prisma.IntFieldRefInput<$PrismaModel>; + gt?: number | Prisma.IntFieldRefInput<$PrismaModel>; + gte?: number | Prisma.IntFieldRefInput<$PrismaModel>; + not?: Prisma.NestedIntFilter<$PrismaModel> | number; +}; +export type EnumApplicationQuestionTypeWithAggregatesFilter<$PrismaModel = never> = { + equals?: $Enums.ApplicationQuestionType | Prisma.EnumApplicationQuestionTypeFieldRefInput<$PrismaModel>; + in?: $Enums.ApplicationQuestionType[] | Prisma.ListEnumApplicationQuestionTypeFieldRefInput<$PrismaModel>; + notIn?: $Enums.ApplicationQuestionType[] | Prisma.ListEnumApplicationQuestionTypeFieldRefInput<$PrismaModel>; + not?: Prisma.NestedEnumApplicationQuestionTypeWithAggregatesFilter<$PrismaModel> | $Enums.ApplicationQuestionType; + _count?: Prisma.NestedIntFilter<$PrismaModel>; + _min?: Prisma.NestedEnumApplicationQuestionTypeFilter<$PrismaModel>; + _max?: Prisma.NestedEnumApplicationQuestionTypeFilter<$PrismaModel>; +}; +export type JsonWithAggregatesFilter<$PrismaModel = never> = Prisma.PatchUndefined>, Exclude>, 'path'>>, Required>> | Prisma.OptionalFlat>, 'path'>>; +export type JsonWithAggregatesFilterBase<$PrismaModel = never> = { + equals?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel> | Prisma.JsonNullValueFilter; + path?: string[]; + mode?: Prisma.QueryMode | Prisma.EnumQueryModeFieldRefInput<$PrismaModel>; + string_contains?: string | Prisma.StringFieldRefInput<$PrismaModel>; + string_starts_with?: string | Prisma.StringFieldRefInput<$PrismaModel>; + string_ends_with?: string | Prisma.StringFieldRefInput<$PrismaModel>; + array_starts_with?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel> | null; + array_ends_with?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel> | null; + array_contains?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel> | null; + lt?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel>; + lte?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel>; + gt?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel>; + gte?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel>; + not?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel> | Prisma.JsonNullValueFilter; + _count?: Prisma.NestedIntFilter<$PrismaModel>; + _min?: Prisma.NestedJsonFilter<$PrismaModel>; + _max?: Prisma.NestedJsonFilter<$PrismaModel>; +}; +export type IntWithAggregatesFilter<$PrismaModel = never> = { + equals?: number | Prisma.IntFieldRefInput<$PrismaModel>; + in?: number[] | Prisma.ListIntFieldRefInput<$PrismaModel>; + notIn?: number[] | Prisma.ListIntFieldRefInput<$PrismaModel>; + lt?: number | Prisma.IntFieldRefInput<$PrismaModel>; + lte?: number | Prisma.IntFieldRefInput<$PrismaModel>; + gt?: number | Prisma.IntFieldRefInput<$PrismaModel>; + gte?: number | Prisma.IntFieldRefInput<$PrismaModel>; + not?: Prisma.NestedIntWithAggregatesFilter<$PrismaModel> | number; + _count?: Prisma.NestedIntFilter<$PrismaModel>; + _avg?: Prisma.NestedFloatFilter<$PrismaModel>; + _sum?: Prisma.NestedIntFilter<$PrismaModel>; + _min?: Prisma.NestedIntFilter<$PrismaModel>; + _max?: Prisma.NestedIntFilter<$PrismaModel>; +}; +export type EnumApplicationStatusFilter<$PrismaModel = never> = { + equals?: $Enums.ApplicationStatus | Prisma.EnumApplicationStatusFieldRefInput<$PrismaModel>; + in?: $Enums.ApplicationStatus[] | Prisma.ListEnumApplicationStatusFieldRefInput<$PrismaModel>; + notIn?: $Enums.ApplicationStatus[] | Prisma.ListEnumApplicationStatusFieldRefInput<$PrismaModel>; + not?: Prisma.NestedEnumApplicationStatusFilter<$PrismaModel> | $Enums.ApplicationStatus; +}; +export type DateTimeNullableFilter<$PrismaModel = never> = { + equals?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel> | null; + in?: Date[] | string[] | Prisma.ListDateTimeFieldRefInput<$PrismaModel> | null; + notIn?: Date[] | string[] | Prisma.ListDateTimeFieldRefInput<$PrismaModel> | null; + lt?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>; + lte?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>; + gt?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>; + gte?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>; + not?: Prisma.NestedDateTimeNullableFilter<$PrismaModel> | Date | string | null; +}; +export type EnumApplicationStatusWithAggregatesFilter<$PrismaModel = never> = { + equals?: $Enums.ApplicationStatus | Prisma.EnumApplicationStatusFieldRefInput<$PrismaModel>; + in?: $Enums.ApplicationStatus[] | Prisma.ListEnumApplicationStatusFieldRefInput<$PrismaModel>; + notIn?: $Enums.ApplicationStatus[] | Prisma.ListEnumApplicationStatusFieldRefInput<$PrismaModel>; + not?: Prisma.NestedEnumApplicationStatusWithAggregatesFilter<$PrismaModel> | $Enums.ApplicationStatus; + _count?: Prisma.NestedIntFilter<$PrismaModel>; + _min?: Prisma.NestedEnumApplicationStatusFilter<$PrismaModel>; + _max?: Prisma.NestedEnumApplicationStatusFilter<$PrismaModel>; +}; +export type DateTimeNullableWithAggregatesFilter<$PrismaModel = never> = { + equals?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel> | null; + in?: Date[] | string[] | Prisma.ListDateTimeFieldRefInput<$PrismaModel> | null; + notIn?: Date[] | string[] | Prisma.ListDateTimeFieldRefInput<$PrismaModel> | null; + lt?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>; + lte?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>; + gt?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>; + gte?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>; + not?: Prisma.NestedDateTimeNullableWithAggregatesFilter<$PrismaModel> | Date | string | null; + _count?: Prisma.NestedIntNullableFilter<$PrismaModel>; + _min?: Prisma.NestedDateTimeNullableFilter<$PrismaModel>; + _max?: Prisma.NestedDateTimeNullableFilter<$PrismaModel>; +}; +export type NestedStringFilter<$PrismaModel = never> = { + equals?: string | Prisma.StringFieldRefInput<$PrismaModel>; + in?: string[] | Prisma.ListStringFieldRefInput<$PrismaModel>; + notIn?: string[] | Prisma.ListStringFieldRefInput<$PrismaModel>; + lt?: string | Prisma.StringFieldRefInput<$PrismaModel>; + lte?: string | Prisma.StringFieldRefInput<$PrismaModel>; + gt?: string | Prisma.StringFieldRefInput<$PrismaModel>; + gte?: string | Prisma.StringFieldRefInput<$PrismaModel>; + contains?: string | Prisma.StringFieldRefInput<$PrismaModel>; + startsWith?: string | Prisma.StringFieldRefInput<$PrismaModel>; + endsWith?: string | Prisma.StringFieldRefInput<$PrismaModel>; + not?: Prisma.NestedStringFilter<$PrismaModel> | string; +}; +export type NestedStringNullableFilter<$PrismaModel = never> = { + equals?: string | Prisma.StringFieldRefInput<$PrismaModel> | null; + in?: string[] | Prisma.ListStringFieldRefInput<$PrismaModel> | null; + notIn?: string[] | Prisma.ListStringFieldRefInput<$PrismaModel> | null; + lt?: string | Prisma.StringFieldRefInput<$PrismaModel>; + lte?: string | Prisma.StringFieldRefInput<$PrismaModel>; + gt?: string | Prisma.StringFieldRefInput<$PrismaModel>; + gte?: string | Prisma.StringFieldRefInput<$PrismaModel>; + contains?: string | Prisma.StringFieldRefInput<$PrismaModel>; + startsWith?: string | Prisma.StringFieldRefInput<$PrismaModel>; + endsWith?: string | Prisma.StringFieldRefInput<$PrismaModel>; + not?: Prisma.NestedStringNullableFilter<$PrismaModel> | string | null; +}; +export type NestedStringWithAggregatesFilter<$PrismaModel = never> = { + equals?: string | Prisma.StringFieldRefInput<$PrismaModel>; + in?: string[] | Prisma.ListStringFieldRefInput<$PrismaModel>; + notIn?: string[] | Prisma.ListStringFieldRefInput<$PrismaModel>; + lt?: string | Prisma.StringFieldRefInput<$PrismaModel>; + lte?: string | Prisma.StringFieldRefInput<$PrismaModel>; + gt?: string | Prisma.StringFieldRefInput<$PrismaModel>; + gte?: string | Prisma.StringFieldRefInput<$PrismaModel>; + contains?: string | Prisma.StringFieldRefInput<$PrismaModel>; + startsWith?: string | Prisma.StringFieldRefInput<$PrismaModel>; + endsWith?: string | Prisma.StringFieldRefInput<$PrismaModel>; + not?: Prisma.NestedStringWithAggregatesFilter<$PrismaModel> | string; + _count?: Prisma.NestedIntFilter<$PrismaModel>; + _min?: Prisma.NestedStringFilter<$PrismaModel>; + _max?: Prisma.NestedStringFilter<$PrismaModel>; +}; +export type NestedIntFilter<$PrismaModel = never> = { + equals?: number | Prisma.IntFieldRefInput<$PrismaModel>; + in?: number[] | Prisma.ListIntFieldRefInput<$PrismaModel>; + notIn?: number[] | Prisma.ListIntFieldRefInput<$PrismaModel>; + lt?: number | Prisma.IntFieldRefInput<$PrismaModel>; + lte?: number | Prisma.IntFieldRefInput<$PrismaModel>; + gt?: number | Prisma.IntFieldRefInput<$PrismaModel>; + gte?: number | Prisma.IntFieldRefInput<$PrismaModel>; + not?: Prisma.NestedIntFilter<$PrismaModel> | number; +}; +export type NestedStringNullableWithAggregatesFilter<$PrismaModel = never> = { + equals?: string | Prisma.StringFieldRefInput<$PrismaModel> | null; + in?: string[] | Prisma.ListStringFieldRefInput<$PrismaModel> | null; + notIn?: string[] | Prisma.ListStringFieldRefInput<$PrismaModel> | null; + lt?: string | Prisma.StringFieldRefInput<$PrismaModel>; + lte?: string | Prisma.StringFieldRefInput<$PrismaModel>; + gt?: string | Prisma.StringFieldRefInput<$PrismaModel>; + gte?: string | Prisma.StringFieldRefInput<$PrismaModel>; + contains?: string | Prisma.StringFieldRefInput<$PrismaModel>; + startsWith?: string | Prisma.StringFieldRefInput<$PrismaModel>; + endsWith?: string | Prisma.StringFieldRefInput<$PrismaModel>; + not?: Prisma.NestedStringNullableWithAggregatesFilter<$PrismaModel> | string | null; + _count?: Prisma.NestedIntNullableFilter<$PrismaModel>; + _min?: Prisma.NestedStringNullableFilter<$PrismaModel>; + _max?: Prisma.NestedStringNullableFilter<$PrismaModel>; +}; +export type NestedIntNullableFilter<$PrismaModel = never> = { + equals?: number | Prisma.IntFieldRefInput<$PrismaModel> | null; + in?: number[] | Prisma.ListIntFieldRefInput<$PrismaModel> | null; + notIn?: number[] | Prisma.ListIntFieldRefInput<$PrismaModel> | null; + lt?: number | Prisma.IntFieldRefInput<$PrismaModel>; + lte?: number | Prisma.IntFieldRefInput<$PrismaModel>; + gt?: number | Prisma.IntFieldRefInput<$PrismaModel>; + gte?: number | Prisma.IntFieldRefInput<$PrismaModel>; + not?: Prisma.NestedIntNullableFilter<$PrismaModel> | number | null; +}; +export type NestedDateTimeFilter<$PrismaModel = never> = { + equals?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>; + in?: Date[] | string[] | Prisma.ListDateTimeFieldRefInput<$PrismaModel>; + notIn?: Date[] | string[] | Prisma.ListDateTimeFieldRefInput<$PrismaModel>; + lt?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>; + lte?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>; + gt?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>; + gte?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>; + not?: Prisma.NestedDateTimeFilter<$PrismaModel> | Date | string; +}; +export type NestedBoolFilter<$PrismaModel = never> = { + equals?: boolean | Prisma.BooleanFieldRefInput<$PrismaModel>; + not?: Prisma.NestedBoolFilter<$PrismaModel> | boolean; +}; +export type NestedBoolNullableFilter<$PrismaModel = never> = { + equals?: boolean | Prisma.BooleanFieldRefInput<$PrismaModel> | null; + not?: Prisma.NestedBoolNullableFilter<$PrismaModel> | boolean | null; +}; +export type NestedDateTimeWithAggregatesFilter<$PrismaModel = never> = { + equals?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>; + in?: Date[] | string[] | Prisma.ListDateTimeFieldRefInput<$PrismaModel>; + notIn?: Date[] | string[] | Prisma.ListDateTimeFieldRefInput<$PrismaModel>; + lt?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>; + lte?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>; + gt?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>; + gte?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>; + not?: Prisma.NestedDateTimeWithAggregatesFilter<$PrismaModel> | Date | string; + _count?: Prisma.NestedIntFilter<$PrismaModel>; + _min?: Prisma.NestedDateTimeFilter<$PrismaModel>; + _max?: Prisma.NestedDateTimeFilter<$PrismaModel>; +}; +export type NestedBoolWithAggregatesFilter<$PrismaModel = never> = { + equals?: boolean | Prisma.BooleanFieldRefInput<$PrismaModel>; + not?: Prisma.NestedBoolWithAggregatesFilter<$PrismaModel> | boolean; + _count?: Prisma.NestedIntFilter<$PrismaModel>; + _min?: Prisma.NestedBoolFilter<$PrismaModel>; + _max?: Prisma.NestedBoolFilter<$PrismaModel>; +}; +export type NestedBoolNullableWithAggregatesFilter<$PrismaModel = never> = { + equals?: boolean | Prisma.BooleanFieldRefInput<$PrismaModel> | null; + not?: Prisma.NestedBoolNullableWithAggregatesFilter<$PrismaModel> | boolean | null; + _count?: Prisma.NestedIntNullableFilter<$PrismaModel>; + _min?: Prisma.NestedBoolNullableFilter<$PrismaModel>; + _max?: Prisma.NestedBoolNullableFilter<$PrismaModel>; +}; +export type NestedEnumApplicationQuestionTypeFilter<$PrismaModel = never> = { + equals?: $Enums.ApplicationQuestionType | Prisma.EnumApplicationQuestionTypeFieldRefInput<$PrismaModel>; + in?: $Enums.ApplicationQuestionType[] | Prisma.ListEnumApplicationQuestionTypeFieldRefInput<$PrismaModel>; + notIn?: $Enums.ApplicationQuestionType[] | Prisma.ListEnumApplicationQuestionTypeFieldRefInput<$PrismaModel>; + not?: Prisma.NestedEnumApplicationQuestionTypeFilter<$PrismaModel> | $Enums.ApplicationQuestionType; +}; +export type NestedEnumApplicationQuestionTypeWithAggregatesFilter<$PrismaModel = never> = { + equals?: $Enums.ApplicationQuestionType | Prisma.EnumApplicationQuestionTypeFieldRefInput<$PrismaModel>; + in?: $Enums.ApplicationQuestionType[] | Prisma.ListEnumApplicationQuestionTypeFieldRefInput<$PrismaModel>; + notIn?: $Enums.ApplicationQuestionType[] | Prisma.ListEnumApplicationQuestionTypeFieldRefInput<$PrismaModel>; + not?: Prisma.NestedEnumApplicationQuestionTypeWithAggregatesFilter<$PrismaModel> | $Enums.ApplicationQuestionType; + _count?: Prisma.NestedIntFilter<$PrismaModel>; + _min?: Prisma.NestedEnumApplicationQuestionTypeFilter<$PrismaModel>; + _max?: Prisma.NestedEnumApplicationQuestionTypeFilter<$PrismaModel>; +}; +export type NestedJsonFilter<$PrismaModel = never> = Prisma.PatchUndefined>, Exclude>, 'path'>>, Required>> | Prisma.OptionalFlat>, 'path'>>; +export type NestedJsonFilterBase<$PrismaModel = never> = { + equals?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel> | Prisma.JsonNullValueFilter; + path?: string[]; + mode?: Prisma.QueryMode | Prisma.EnumQueryModeFieldRefInput<$PrismaModel>; + string_contains?: string | Prisma.StringFieldRefInput<$PrismaModel>; + string_starts_with?: string | Prisma.StringFieldRefInput<$PrismaModel>; + string_ends_with?: string | Prisma.StringFieldRefInput<$PrismaModel>; + array_starts_with?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel> | null; + array_ends_with?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel> | null; + array_contains?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel> | null; + lt?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel>; + lte?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel>; + gt?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel>; + gte?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel>; + not?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel> | Prisma.JsonNullValueFilter; +}; +export type NestedIntWithAggregatesFilter<$PrismaModel = never> = { + equals?: number | Prisma.IntFieldRefInput<$PrismaModel>; + in?: number[] | Prisma.ListIntFieldRefInput<$PrismaModel>; + notIn?: number[] | Prisma.ListIntFieldRefInput<$PrismaModel>; + lt?: number | Prisma.IntFieldRefInput<$PrismaModel>; + lte?: number | Prisma.IntFieldRefInput<$PrismaModel>; + gt?: number | Prisma.IntFieldRefInput<$PrismaModel>; + gte?: number | Prisma.IntFieldRefInput<$PrismaModel>; + not?: Prisma.NestedIntWithAggregatesFilter<$PrismaModel> | number; + _count?: Prisma.NestedIntFilter<$PrismaModel>; + _avg?: Prisma.NestedFloatFilter<$PrismaModel>; + _sum?: Prisma.NestedIntFilter<$PrismaModel>; + _min?: Prisma.NestedIntFilter<$PrismaModel>; + _max?: Prisma.NestedIntFilter<$PrismaModel>; +}; +export type NestedFloatFilter<$PrismaModel = never> = { + equals?: number | Prisma.FloatFieldRefInput<$PrismaModel>; + in?: number[] | Prisma.ListFloatFieldRefInput<$PrismaModel>; + notIn?: number[] | Prisma.ListFloatFieldRefInput<$PrismaModel>; + lt?: number | Prisma.FloatFieldRefInput<$PrismaModel>; + lte?: number | Prisma.FloatFieldRefInput<$PrismaModel>; + gt?: number | Prisma.FloatFieldRefInput<$PrismaModel>; + gte?: number | Prisma.FloatFieldRefInput<$PrismaModel>; + not?: Prisma.NestedFloatFilter<$PrismaModel> | number; +}; +export type NestedEnumApplicationStatusFilter<$PrismaModel = never> = { + equals?: $Enums.ApplicationStatus | Prisma.EnumApplicationStatusFieldRefInput<$PrismaModel>; + in?: $Enums.ApplicationStatus[] | Prisma.ListEnumApplicationStatusFieldRefInput<$PrismaModel>; + notIn?: $Enums.ApplicationStatus[] | Prisma.ListEnumApplicationStatusFieldRefInput<$PrismaModel>; + not?: Prisma.NestedEnumApplicationStatusFilter<$PrismaModel> | $Enums.ApplicationStatus; +}; +export type NestedDateTimeNullableFilter<$PrismaModel = never> = { + equals?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel> | null; + in?: Date[] | string[] | Prisma.ListDateTimeFieldRefInput<$PrismaModel> | null; + notIn?: Date[] | string[] | Prisma.ListDateTimeFieldRefInput<$PrismaModel> | null; + lt?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>; + lte?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>; + gt?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>; + gte?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>; + not?: Prisma.NestedDateTimeNullableFilter<$PrismaModel> | Date | string | null; +}; +export type NestedEnumApplicationStatusWithAggregatesFilter<$PrismaModel = never> = { + equals?: $Enums.ApplicationStatus | Prisma.EnumApplicationStatusFieldRefInput<$PrismaModel>; + in?: $Enums.ApplicationStatus[] | Prisma.ListEnumApplicationStatusFieldRefInput<$PrismaModel>; + notIn?: $Enums.ApplicationStatus[] | Prisma.ListEnumApplicationStatusFieldRefInput<$PrismaModel>; + not?: Prisma.NestedEnumApplicationStatusWithAggregatesFilter<$PrismaModel> | $Enums.ApplicationStatus; + _count?: Prisma.NestedIntFilter<$PrismaModel>; + _min?: Prisma.NestedEnumApplicationStatusFilter<$PrismaModel>; + _max?: Prisma.NestedEnumApplicationStatusFilter<$PrismaModel>; +}; +export type NestedDateTimeNullableWithAggregatesFilter<$PrismaModel = never> = { + equals?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel> | null; + in?: Date[] | string[] | Prisma.ListDateTimeFieldRefInput<$PrismaModel> | null; + notIn?: Date[] | string[] | Prisma.ListDateTimeFieldRefInput<$PrismaModel> | null; + lt?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>; + lte?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>; + gt?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>; + gte?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>; + not?: Prisma.NestedDateTimeNullableWithAggregatesFilter<$PrismaModel> | Date | string | null; + _count?: Prisma.NestedIntNullableFilter<$PrismaModel>; + _min?: Prisma.NestedDateTimeNullableFilter<$PrismaModel>; + _max?: Prisma.NestedDateTimeNullableFilter<$PrismaModel>; +}; diff --git a/packages/db/src/generated/prisma/enums.d.ts b/packages/db/src/generated/prisma/enums.d.ts new file mode 100644 index 00000000..c38fb18c --- /dev/null +++ b/packages/db/src/generated/prisma/enums.d.ts @@ -0,0 +1,21 @@ +export declare const ApplicationStatus: { + readonly SEND: "SEND"; + readonly REVIEWING: "REVIEWING"; + readonly ACCEPTED: "ACCEPTED"; + readonly DECLINED: "DECLINED"; + readonly TRIAL: "TRIAL"; +}; +export type ApplicationStatus = (typeof ApplicationStatus)[keyof typeof ApplicationStatus]; +export declare const ApplicationQuestionType: { + readonly TEXT: "TEXT"; + readonly SHORT_INPUT: "SHORT_INPUT"; + readonly LONG_INPUT: "LONG_INPUT"; + readonly DROPDOWN: "DROPDOWN"; + readonly CITY: "CITY"; + readonly URL: "URL"; + readonly MINECRAFT: "MINECRAFT"; + readonly SLIDER: "SLIDER"; + readonly IMAGE: "IMAGE"; + readonly CHECKBOX: "CHECKBOX"; +}; +export type ApplicationQuestionType = (typeof ApplicationQuestionType)[keyof typeof ApplicationQuestionType]; diff --git a/packages/db/src/generated/prisma/models.d.ts b/packages/db/src/generated/prisma/models.d.ts new file mode 100644 index 00000000..8a1a32bf --- /dev/null +++ b/packages/db/src/generated/prisma/models.d.ts @@ -0,0 +1,18 @@ +export type * from './models/User.ts'; +export type * from './models/BuildTeam.ts'; +export type * from './models/ApplicationQuestion.ts'; +export type * from './models/Social.ts'; +export type * from './models/Showcase.ts'; +export type * from './models/UserPermission.ts'; +export type * from './models/MinecraftVerifications.ts'; +export type * from './models/Permisision.ts'; +export type * from './models/FAQQuestion.ts'; +export type * from './models/Contact.ts'; +export type * from './models/Claim.ts'; +export type * from './models/Application.ts'; +export type * from './models/ApplicationAnswer.ts'; +export type * from './models/ApplicationResponseTemplate.ts'; +export type * from './models/Upload.ts'; +export type * from './models/CalendarEvent.ts'; +export type * from './models/JsonStore.ts'; +export type * from './commonInputTypes.ts'; diff --git a/packages/db/src/index.d.ts b/packages/db/src/index.d.ts new file mode 100644 index 00000000..a53aeeee --- /dev/null +++ b/packages/db/src/index.d.ts @@ -0,0 +1 @@ +export * from './generated/prisma/client'; diff --git a/yarn.lock b/yarn.lock index 469877e2..bc62f339 100644 --- a/yarn.lock +++ b/yarn.lock @@ -905,6 +905,17 @@ __metadata: languageName: node linkType: hard +"@dabh/diagnostics@npm:^2.0.8": + version: 2.0.8 + resolution: "@dabh/diagnostics@npm:2.0.8" + dependencies: + "@so-ric/colorspace": "npm:^1.1.6" + enabled: "npm:2.0.x" + kuler: "npm:^2.0.0" + checksum: 10c0/64701c272f7de02800039fea99796507670fe5f67d4eb7718599351ec156936efd123fcab7ee18f9d7874939caaacc08e7c7a6bb05ff8cda6d930ad041cc555c + languageName: node + linkType: hard + "@directus/sdk@npm:^21.2.2": version: 21.2.2 resolution: "@directus/sdk@npm:21.2.2" @@ -964,6 +975,188 @@ __metadata: languageName: node linkType: hard +"@esbuild/aix-ppc64@npm:0.28.0": + version: 0.28.0 + resolution: "@esbuild/aix-ppc64@npm:0.28.0" + conditions: os=aix & cpu=ppc64 + languageName: node + linkType: hard + +"@esbuild/android-arm64@npm:0.28.0": + version: 0.28.0 + resolution: "@esbuild/android-arm64@npm:0.28.0" + conditions: os=android & cpu=arm64 + languageName: node + linkType: hard + +"@esbuild/android-arm@npm:0.28.0": + version: 0.28.0 + resolution: "@esbuild/android-arm@npm:0.28.0" + conditions: os=android & cpu=arm + languageName: node + linkType: hard + +"@esbuild/android-x64@npm:0.28.0": + version: 0.28.0 + resolution: "@esbuild/android-x64@npm:0.28.0" + conditions: os=android & cpu=x64 + languageName: node + linkType: hard + +"@esbuild/darwin-arm64@npm:0.28.0": + version: 0.28.0 + resolution: "@esbuild/darwin-arm64@npm:0.28.0" + conditions: os=darwin & cpu=arm64 + languageName: node + linkType: hard + +"@esbuild/darwin-x64@npm:0.28.0": + version: 0.28.0 + resolution: "@esbuild/darwin-x64@npm:0.28.0" + conditions: os=darwin & cpu=x64 + languageName: node + linkType: hard + +"@esbuild/freebsd-arm64@npm:0.28.0": + version: 0.28.0 + resolution: "@esbuild/freebsd-arm64@npm:0.28.0" + conditions: os=freebsd & cpu=arm64 + languageName: node + linkType: hard + +"@esbuild/freebsd-x64@npm:0.28.0": + version: 0.28.0 + resolution: "@esbuild/freebsd-x64@npm:0.28.0" + conditions: os=freebsd & cpu=x64 + languageName: node + linkType: hard + +"@esbuild/linux-arm64@npm:0.28.0": + version: 0.28.0 + resolution: "@esbuild/linux-arm64@npm:0.28.0" + conditions: os=linux & cpu=arm64 + languageName: node + linkType: hard + +"@esbuild/linux-arm@npm:0.28.0": + version: 0.28.0 + resolution: "@esbuild/linux-arm@npm:0.28.0" + conditions: os=linux & cpu=arm + languageName: node + linkType: hard + +"@esbuild/linux-ia32@npm:0.28.0": + version: 0.28.0 + resolution: "@esbuild/linux-ia32@npm:0.28.0" + conditions: os=linux & cpu=ia32 + languageName: node + linkType: hard + +"@esbuild/linux-loong64@npm:0.28.0": + version: 0.28.0 + resolution: "@esbuild/linux-loong64@npm:0.28.0" + conditions: os=linux & cpu=loong64 + languageName: node + linkType: hard + +"@esbuild/linux-mips64el@npm:0.28.0": + version: 0.28.0 + resolution: "@esbuild/linux-mips64el@npm:0.28.0" + conditions: os=linux & cpu=mips64el + languageName: node + linkType: hard + +"@esbuild/linux-ppc64@npm:0.28.0": + version: 0.28.0 + resolution: "@esbuild/linux-ppc64@npm:0.28.0" + conditions: os=linux & cpu=ppc64 + languageName: node + linkType: hard + +"@esbuild/linux-riscv64@npm:0.28.0": + version: 0.28.0 + resolution: "@esbuild/linux-riscv64@npm:0.28.0" + conditions: os=linux & cpu=riscv64 + languageName: node + linkType: hard + +"@esbuild/linux-s390x@npm:0.28.0": + version: 0.28.0 + resolution: "@esbuild/linux-s390x@npm:0.28.0" + conditions: os=linux & cpu=s390x + languageName: node + linkType: hard + +"@esbuild/linux-x64@npm:0.28.0": + version: 0.28.0 + resolution: "@esbuild/linux-x64@npm:0.28.0" + conditions: os=linux & cpu=x64 + languageName: node + linkType: hard + +"@esbuild/netbsd-arm64@npm:0.28.0": + version: 0.28.0 + resolution: "@esbuild/netbsd-arm64@npm:0.28.0" + conditions: os=netbsd & cpu=arm64 + languageName: node + linkType: hard + +"@esbuild/netbsd-x64@npm:0.28.0": + version: 0.28.0 + resolution: "@esbuild/netbsd-x64@npm:0.28.0" + conditions: os=netbsd & cpu=x64 + languageName: node + linkType: hard + +"@esbuild/openbsd-arm64@npm:0.28.0": + version: 0.28.0 + resolution: "@esbuild/openbsd-arm64@npm:0.28.0" + conditions: os=openbsd & cpu=arm64 + languageName: node + linkType: hard + +"@esbuild/openbsd-x64@npm:0.28.0": + version: 0.28.0 + resolution: "@esbuild/openbsd-x64@npm:0.28.0" + conditions: os=openbsd & cpu=x64 + languageName: node + linkType: hard + +"@esbuild/openharmony-arm64@npm:0.28.0": + version: 0.28.0 + resolution: "@esbuild/openharmony-arm64@npm:0.28.0" + conditions: os=openharmony & cpu=arm64 + languageName: node + linkType: hard + +"@esbuild/sunos-x64@npm:0.28.0": + version: 0.28.0 + resolution: "@esbuild/sunos-x64@npm:0.28.0" + conditions: os=sunos & cpu=x64 + languageName: node + linkType: hard + +"@esbuild/win32-arm64@npm:0.28.0": + version: 0.28.0 + resolution: "@esbuild/win32-arm64@npm:0.28.0" + conditions: os=win32 & cpu=arm64 + languageName: node + linkType: hard + +"@esbuild/win32-ia32@npm:0.28.0": + version: 0.28.0 + resolution: "@esbuild/win32-ia32@npm:0.28.0" + conditions: os=win32 & cpu=ia32 + languageName: node + linkType: hard + +"@esbuild/win32-x64@npm:0.28.0": + version: 0.28.0 + resolution: "@esbuild/win32-x64@npm:0.28.0" + conditions: os=win32 & cpu=x64 + languageName: node + linkType: hard + "@eslint-community/eslint-utils@npm:^4.2.0, @eslint-community/eslint-utils@npm:^4.4.0": version: 4.4.0 resolution: "@eslint-community/eslint-utils@npm:4.4.0" @@ -1740,6 +1933,22 @@ __metadata: languageName: node linkType: hard +"@ioredis/commands@npm:1.5.1": + version: 1.5.1 + resolution: "@ioredis/commands@npm:1.5.1" + checksum: 10c0/cb8f6d13cff0753e3e7ef001fb895491985d9a623248192538f13bc2fd9bfdfde3c18cf2ba6f20ec8ceaa681b0771070d3a09b82eed044c798bcfef5e3ae54b3 + languageName: node + linkType: hard + +"@isaacs/fs-minipass@npm:^4.0.0": + version: 4.0.1 + resolution: "@isaacs/fs-minipass@npm:4.0.1" + dependencies: + minipass: "npm:^7.0.4" + checksum: 10c0/c25b6dc1598790d5b55c0947a9b7d111cfa92594db5296c3b907e2f533c033666f692a3939eadac17b1c7c40d362d0b0635dc874cbfe3e70db7c2b07cc97a5d2 + languageName: node + linkType: hard + "@jridgewell/gen-mapping@npm:^0.3.12, @jridgewell/gen-mapping@npm:^0.3.5": version: 0.3.13 resolution: "@jridgewell/gen-mapping@npm:0.3.13" @@ -2306,6 +2515,48 @@ __metadata: languageName: node linkType: hard +"@msgpackr-extract/msgpackr-extract-darwin-arm64@npm:3.0.3": + version: 3.0.3 + resolution: "@msgpackr-extract/msgpackr-extract-darwin-arm64@npm:3.0.3" + conditions: os=darwin & cpu=arm64 + languageName: node + linkType: hard + +"@msgpackr-extract/msgpackr-extract-darwin-x64@npm:3.0.3": + version: 3.0.3 + resolution: "@msgpackr-extract/msgpackr-extract-darwin-x64@npm:3.0.3" + conditions: os=darwin & cpu=x64 + languageName: node + linkType: hard + +"@msgpackr-extract/msgpackr-extract-linux-arm64@npm:3.0.3": + version: 3.0.3 + resolution: "@msgpackr-extract/msgpackr-extract-linux-arm64@npm:3.0.3" + conditions: os=linux & cpu=arm64 + languageName: node + linkType: hard + +"@msgpackr-extract/msgpackr-extract-linux-arm@npm:3.0.3": + version: 3.0.3 + resolution: "@msgpackr-extract/msgpackr-extract-linux-arm@npm:3.0.3" + conditions: os=linux & cpu=arm + languageName: node + linkType: hard + +"@msgpackr-extract/msgpackr-extract-linux-x64@npm:3.0.3": + version: 3.0.3 + resolution: "@msgpackr-extract/msgpackr-extract-linux-x64@npm:3.0.3" + conditions: os=linux & cpu=x64 + languageName: node + linkType: hard + +"@msgpackr-extract/msgpackr-extract-win32-x64@npm:3.0.3": + version: 3.0.3 + resolution: "@msgpackr-extract/msgpackr-extract-win32-x64@npm:3.0.3" + conditions: os=win32 & cpu=x64 + languageName: node + linkType: hard + "@next/bundle-analyzer@npm:16.2.4": version: 16.2.4 resolution: "@next/bundle-analyzer@npm:16.2.4" @@ -2723,7 +2974,7 @@ __metadata: languageName: unknown linkType: soft -"@repo/prettier-config@npm:*, @repo/prettier-config@workspace:packages/prettier-config": +"@repo/prettier-config@npm:*, @repo/prettier-config@workspace:^, @repo/prettier-config@workspace:packages/prettier-config": version: 0.0.0-use.local resolution: "@repo/prettier-config@workspace:packages/prettier-config" dependencies: @@ -2732,7 +2983,7 @@ __metadata: languageName: unknown linkType: soft -"@repo/typescript-config@npm:*, @repo/typescript-config@workspace:packages/typescript-config": +"@repo/typescript-config@npm:*, @repo/typescript-config@workspace:^, @repo/typescript-config@workspace:packages/typescript-config": version: 0.0.0-use.local resolution: "@repo/typescript-config@workspace:packages/typescript-config" languageName: unknown @@ -3364,6 +3615,16 @@ __metadata: languageName: node linkType: hard +"@so-ric/colorspace@npm:^1.1.6": + version: 1.1.6 + resolution: "@so-ric/colorspace@npm:1.1.6" + dependencies: + color: "npm:^5.0.2" + text-hex: "npm:1.0.x" + checksum: 10c0/f3ad26afefbb8d6101ea7c385cd5f402d4291c2ffc9cabe37030d5fdb8bda980ee534a0d7c250f8233fc3a59b99272410177cd98b219f6b3770f91a0fdb6eb3e + languageName: node + linkType: hard + "@standard-schema/spec@npm:^1.0.0": version: 1.0.0 resolution: "@standard-schema/spec@npm:1.0.0" @@ -6576,6 +6837,13 @@ __metadata: languageName: node linkType: hard +"abbrev@npm:^4.0.0": + version: 4.0.0 + resolution: "abbrev@npm:4.0.0" + checksum: 10c0/b4cc16935235e80702fc90192e349e32f8ef0ed151ef506aa78c81a7c455ec18375c4125414b99f84b2e055199d66383e787675f0bcd87da7a4dbd59f9eac1d5 + languageName: node + linkType: hard + "accepts@npm:^2.0.0": version: 2.0.0 resolution: "accepts@npm:2.0.0" @@ -7290,6 +7558,25 @@ __metadata: languageName: node linkType: hard +"bullmq@npm:^5.77.0": + version: 5.77.0 + resolution: "bullmq@npm:5.77.0" + dependencies: + cron-parser: "npm:4.9.0" + ioredis: "npm:5.10.1" + msgpackr: "npm:2.0.1" + node-abort-controller: "npm:3.1.1" + semver: "npm:7.8.0" + tslib: "npm:2.8.1" + peerDependencies: + redis: ">=5.0.0" + peerDependenciesMeta: + redis: + optional: true + checksum: 10c0/5e694efab15603f63aedbd2c7462039f7359238932709b922693359eb3620fcd2f8243867b267ff71dcfdc86fa95934a4d7f61f6d4a7e6eb22fbcc6aa3f893f9 + languageName: node + linkType: hard + "busboy@npm:1.6.0, busboy@npm:^1.0.0": version: 1.6.0 resolution: "busboy@npm:1.6.0" @@ -7477,6 +7764,13 @@ __metadata: languageName: node linkType: hard +"chownr@npm:^3.0.0": + version: 3.0.0 + resolution: "chownr@npm:3.0.0" + checksum: 10c0/43925b87700f7e3893296c8e9c56cc58f926411cce3a6e5898136daaf08f08b9a8eb76d37d3267e707d0dcc17aed2e2ebdf5848c0c3ce95cf910a919935c1b10 + languageName: node + linkType: hard + "chromedriver@npm:latest": version: 135.0.1 resolution: "chromedriver@npm:135.0.1" @@ -7543,6 +7837,13 @@ __metadata: languageName: node linkType: hard +"cluster-key-slot@npm:^1.1.0": + version: 1.1.2 + resolution: "cluster-key-slot@npm:1.1.2" + checksum: 10c0/d7d39ca28a8786e9e801eeb8c770e3c3236a566625d7299a47bb71113fb2298ce1039596acb82590e598c52dbc9b1f088c8f587803e697cb58e1867a95ff94d3 + languageName: node + linkType: hard + "color-convert@npm:^1.9.3": version: 1.9.3 resolution: "color-convert@npm:1.9.3" @@ -7561,6 +7862,15 @@ __metadata: languageName: node linkType: hard +"color-convert@npm:^3.1.3": + version: 3.1.3 + resolution: "color-convert@npm:3.1.3" + dependencies: + color-name: "npm:^2.0.0" + checksum: 10c0/427648b442c6ea6dab5ba03f4962201ee59f128c80b25d5a0f7d9aab0ef52519a9db8a9bb3cf40b73f86eb19b5ca6aeb0ab930665f3d14973ce776d7d0448a15 + languageName: node + linkType: hard + "color-name@npm:1.1.3": version: 1.1.3 resolution: "color-name@npm:1.1.3" @@ -7575,6 +7885,13 @@ __metadata: languageName: node linkType: hard +"color-name@npm:^2.0.0": + version: 2.1.0 + resolution: "color-name@npm:2.1.0" + checksum: 10c0/9c953caba99557fce472232ded438c56b902c569cb15d66fcfbdf6374206126eef52ab66459f3984d4074b4aa8ab95e6f4b31a8e4f228dea57d0afecf94281fa + languageName: node + linkType: hard + "color-string@npm:^1.6.0, color-string@npm:^1.9.0": version: 1.9.1 resolution: "color-string@npm:1.9.1" @@ -7585,6 +7902,15 @@ __metadata: languageName: node linkType: hard +"color-string@npm:^2.1.3": + version: 2.1.4 + resolution: "color-string@npm:2.1.4" + dependencies: + color-name: "npm:^2.0.0" + checksum: 10c0/18a9fefec153d885e0dbfb076f3a65cdcd19f52d96c719f2f261e90e5b7dafd13c51baac399d7099eac290f004d340045ab9467312dcc8afefe6f877ec5c4428 + languageName: node + linkType: hard + "color@npm:^3.1.3": version: 3.2.1 resolution: "color@npm:3.2.1" @@ -7605,6 +7931,16 @@ __metadata: languageName: node linkType: hard +"color@npm:^5.0.2": + version: 5.0.3 + resolution: "color@npm:5.0.3" + dependencies: + color-convert: "npm:^3.1.3" + color-string: "npm:^2.1.3" + checksum: 10c0/f08a03c5113ae4aa36dba9d2438596b194b897e18b961310643cb63872add1da507cd238df264eb434bbdbe3a377ec41f90d877531acca611523cfcd365db1b6 + languageName: node + linkType: hard + "colorette@npm:^2.0.20": version: 2.0.20 resolution: "colorette@npm:2.0.20" @@ -7809,6 +8145,15 @@ __metadata: languageName: node linkType: hard +"cron-parser@npm:4.9.0": + version: 4.9.0 + resolution: "cron-parser@npm:4.9.0" + dependencies: + luxon: "npm:^3.2.1" + checksum: 10c0/348622bdcd1a15695b61fc33af8a60133e5913a85cf99f6344367579e7002896514ba3b0a9d6bb569b02667d6b06836722bf2295fcd101b3de378f71d37bed0b + languageName: node + linkType: hard + "cron@npm:^4.1.4": version: 4.1.4 resolution: "cron@npm:4.1.4" @@ -8320,6 +8665,13 @@ __metadata: languageName: node linkType: hard +"detect-libc@npm:^2.0.1, detect-libc@npm:^2.1.2": + version: 2.1.2 + resolution: "detect-libc@npm:2.1.2" + checksum: 10c0/acc675c29a5649fa1fb6e255f993b8ee829e510b6b56b0910666949c80c364738833417d0edb5f90e4e46be17228b0f2b66a010513984e18b15deeeac49369c4 + languageName: node + linkType: hard + "detect-libc@npm:^2.0.3": version: 2.0.3 resolution: "detect-libc@npm:2.0.3" @@ -8327,13 +8679,6 @@ __metadata: languageName: node linkType: hard -"detect-libc@npm:^2.1.2": - version: 2.1.2 - resolution: "detect-libc@npm:2.1.2" - checksum: 10c0/acc675c29a5649fa1fb6e255f993b8ee829e510b6b56b0910666949c80c364738833417d0edb5f90e4e46be17228b0f2b66a010513984e18b15deeeac49369c4 - languageName: node - linkType: hard - "detect-node-es@npm:^1.1.0": version: 1.1.0 resolution: "detect-node-es@npm:1.1.0" @@ -8582,6 +8927,13 @@ __metadata: languageName: node linkType: hard +"env-paths@npm:^2.2.0": + version: 2.2.1 + resolution: "env-paths@npm:2.2.1" + checksum: 10c0/285325677bf00e30845e330eec32894f5105529db97496ee3f598478e50f008c5352a41a30e5e72ec9de8a542b5a570b85699cd63bd2bc646dbcb9f311d83bc4 + languageName: node + linkType: hard + "environment@npm:^1.0.0": version: 1.1.0 resolution: "environment@npm:1.1.0" @@ -8904,6 +9256,95 @@ __metadata: languageName: node linkType: hard +"esbuild@npm:~0.28.0": + version: 0.28.0 + resolution: "esbuild@npm:0.28.0" + dependencies: + "@esbuild/aix-ppc64": "npm:0.28.0" + "@esbuild/android-arm": "npm:0.28.0" + "@esbuild/android-arm64": "npm:0.28.0" + "@esbuild/android-x64": "npm:0.28.0" + "@esbuild/darwin-arm64": "npm:0.28.0" + "@esbuild/darwin-x64": "npm:0.28.0" + "@esbuild/freebsd-arm64": "npm:0.28.0" + "@esbuild/freebsd-x64": "npm:0.28.0" + "@esbuild/linux-arm": "npm:0.28.0" + "@esbuild/linux-arm64": "npm:0.28.0" + "@esbuild/linux-ia32": "npm:0.28.0" + "@esbuild/linux-loong64": "npm:0.28.0" + "@esbuild/linux-mips64el": "npm:0.28.0" + "@esbuild/linux-ppc64": "npm:0.28.0" + "@esbuild/linux-riscv64": "npm:0.28.0" + "@esbuild/linux-s390x": "npm:0.28.0" + "@esbuild/linux-x64": "npm:0.28.0" + "@esbuild/netbsd-arm64": "npm:0.28.0" + "@esbuild/netbsd-x64": "npm:0.28.0" + "@esbuild/openbsd-arm64": "npm:0.28.0" + "@esbuild/openbsd-x64": "npm:0.28.0" + "@esbuild/openharmony-arm64": "npm:0.28.0" + "@esbuild/sunos-x64": "npm:0.28.0" + "@esbuild/win32-arm64": "npm:0.28.0" + "@esbuild/win32-ia32": "npm:0.28.0" + "@esbuild/win32-x64": "npm:0.28.0" + dependenciesMeta: + "@esbuild/aix-ppc64": + optional: true + "@esbuild/android-arm": + optional: true + "@esbuild/android-arm64": + optional: true + "@esbuild/android-x64": + optional: true + "@esbuild/darwin-arm64": + optional: true + "@esbuild/darwin-x64": + optional: true + "@esbuild/freebsd-arm64": + optional: true + "@esbuild/freebsd-x64": + optional: true + "@esbuild/linux-arm": + optional: true + "@esbuild/linux-arm64": + optional: true + "@esbuild/linux-ia32": + optional: true + "@esbuild/linux-loong64": + optional: true + "@esbuild/linux-mips64el": + optional: true + "@esbuild/linux-ppc64": + optional: true + "@esbuild/linux-riscv64": + optional: true + "@esbuild/linux-s390x": + optional: true + "@esbuild/linux-x64": + optional: true + "@esbuild/netbsd-arm64": + optional: true + "@esbuild/netbsd-x64": + optional: true + "@esbuild/openbsd-arm64": + optional: true + "@esbuild/openbsd-x64": + optional: true + "@esbuild/openharmony-arm64": + optional: true + "@esbuild/sunos-x64": + optional: true + "@esbuild/win32-arm64": + optional: true + "@esbuild/win32-ia32": + optional: true + "@esbuild/win32-x64": + optional: true + bin: + esbuild: bin/esbuild + checksum: 10c0/8acd95c238ec6c4a9d16163277faf228a8994b642d187b3fe667ffbb469008e6748cde144fdc3c175bf8e78ee49e15a0ed9b9f183fdb5fcea1772f87fb1372a4 + languageName: node + linkType: hard + "escalade@npm:^3.2.0": version: 3.2.0 resolution: "escalade@npm:3.2.0" @@ -9564,6 +10005,13 @@ __metadata: languageName: node linkType: hard +"exponential-backoff@npm:^3.1.1": + version: 3.1.3 + resolution: "exponential-backoff@npm:3.1.3" + checksum: 10c0/77e3ae682b7b1f4972f563c6dbcd2b0d54ac679e62d5d32f3e5085feba20483cf28bd505543f520e287a56d4d55a28d7874299941faf637e779a1aa5994d1267 + languageName: node + linkType: hard + "express-session@npm:^1.18.1": version: 1.18.1 resolution: "express-session@npm:1.18.1" @@ -10097,6 +10545,25 @@ __metadata: languageName: node linkType: hard +"fsevents@npm:~2.3.3": + version: 2.3.3 + resolution: "fsevents@npm:2.3.3" + dependencies: + node-gyp: "npm:latest" + checksum: 10c0/a1f0c44595123ed717febbc478aa952e47adfc28e2092be66b8ab1635147254ca6cfe1df792a8997f22716d4cbafc73309899ff7bfac2ac3ad8cf2e4ecc3ec60 + conditions: os=darwin + languageName: node + linkType: hard + +"fsevents@patch:fsevents@npm%3A~2.3.3#optional!builtin": + version: 2.3.3 + resolution: "fsevents@patch:fsevents@npm%3A2.3.3#optional!builtin::version=2.3.3&hash=df0bf1" + dependencies: + node-gyp: "npm:latest" + conditions: os=darwin + languageName: node + linkType: hard + "function-bind@npm:^1.1.2": version: 1.1.2 resolution: "function-bind@npm:1.1.2" @@ -10445,7 +10912,7 @@ __metadata: languageName: node linkType: hard -"graceful-fs@npm:^4.1.6, graceful-fs@npm:^4.2.0, graceful-fs@npm:^4.2.4": +"graceful-fs@npm:^4.1.6, graceful-fs@npm:^4.2.0, graceful-fs@npm:^4.2.4, graceful-fs@npm:^4.2.6": version: 4.2.11 resolution: "graceful-fs@npm:4.2.11" checksum: 10c0/386d011a553e02bc594ac2ca0bd6d9e4c22d7fa8cfbfc448a6d148c59ea881b092db9dbe3547ae4b88e55f1b01f7c4a2ecc53b310c042793e63aa44cf6c257f2 @@ -10797,6 +11264,23 @@ __metadata: languageName: node linkType: hard +"ioredis@npm:5.10.1, ioredis@npm:^5.10.1": + version: 5.10.1 + resolution: "ioredis@npm:5.10.1" + dependencies: + "@ioredis/commands": "npm:1.5.1" + cluster-key-slot: "npm:^1.1.0" + debug: "npm:^4.3.4" + denque: "npm:^2.1.0" + lodash.defaults: "npm:^4.2.0" + lodash.isarguments: "npm:^3.1.0" + redis-errors: "npm:^1.2.0" + redis-parser: "npm:^3.0.0" + standard-as-callback: "npm:^2.1.0" + checksum: 10c0/d0507b52520d3bdd5dacaa33aed9dd3133794d8633b43a6b7fc3199a5e73f92cb77409f6904abe68e3221a95a630d97073b8c1c9e2c0c7613124db67e97c0eb0 + languageName: node + linkType: hard + "ip-address@npm:^9.0.5": version: 9.0.5 resolution: "ip-address@npm:9.0.5" @@ -11304,6 +11788,13 @@ __metadata: languageName: node linkType: hard +"isexe@npm:^4.0.0": + version: 4.0.0 + resolution: "isexe@npm:4.0.0" + checksum: 10c0/5884815115bceac452877659a9c7726382531592f43dc29e5d48b7c4100661aed54018cb90bd36cb2eaeba521092570769167acbb95c18d39afdccbcca06c5ce + languageName: node + linkType: hard + "isobject@npm:^3.0.1": version: 3.0.1 resolution: "isobject@npm:3.0.1" @@ -11661,6 +12152,13 @@ __metadata: languageName: node linkType: hard +"lodash.defaults@npm:^4.2.0": + version: 4.2.0 + resolution: "lodash.defaults@npm:4.2.0" + checksum: 10c0/d5b77aeb702caa69b17be1358faece33a84497bcca814897383c58b28a2f8dfc381b1d9edbec239f8b425126a3bbe4916223da2a576bb0411c2cefd67df80707 + languageName: node + linkType: hard + "lodash.includes@npm:^4.3.0": version: 4.3.0 resolution: "lodash.includes@npm:4.3.0" @@ -11668,6 +12166,13 @@ __metadata: languageName: node linkType: hard +"lodash.isarguments@npm:^3.1.0": + version: 3.1.0 + resolution: "lodash.isarguments@npm:3.1.0" + checksum: 10c0/5e8f95ba10975900a3920fb039a3f89a5a79359a1b5565e4e5b4310ed6ebe64011e31d402e34f577eca983a1fc01ff86c926e3cbe602e1ddfc858fdd353e62d8 + languageName: node + linkType: hard + "lodash.isboolean@npm:^3.0.3": version: 3.0.3 resolution: "lodash.isboolean@npm:3.0.3" @@ -11820,6 +12325,13 @@ __metadata: languageName: node linkType: hard +"luxon@npm:^3.2.1": + version: 3.7.2 + resolution: "luxon@npm:3.7.2" + checksum: 10c0/ed8f0f637826c08c343a29dd478b00628be93bba6f068417b1d8896b61cb61c6deacbe1df1e057dbd9298334044afa150f9aaabbeb3181418ac8520acfdc2ae2 + languageName: node + linkType: hard + "luxon@npm:~3.6.0": version: 3.6.1 resolution: "luxon@npm:3.6.1" @@ -12235,6 +12747,22 @@ __metadata: languageName: node linkType: hard +"minipass@npm:^7.0.4, minipass@npm:^7.1.2": + version: 7.1.3 + resolution: "minipass@npm:7.1.3" + checksum: 10c0/539da88daca16533211ea5a9ee98dc62ff5742f531f54640dd34429e621955e91cc280a91a776026264b7f9f6735947629f920944e9c1558369e8bf22eb33fbb + languageName: node + linkType: hard + +"minizlib@npm:^3.1.0": + version: 3.1.0 + resolution: "minizlib@npm:3.1.0" + dependencies: + minipass: "npm:^7.1.2" + checksum: 10c0/5aad75ab0090b8266069c9aabe582c021ae53eb33c6c691054a13a45db3b4f91a7fb1bd79151e6b4e9e9a86727b522527c0a06ec7d45206b745d54cd3097bcec + languageName: node + linkType: hard + "mkdirp@npm:^0.5.4": version: 0.5.6 resolution: "mkdirp@npm:0.5.6" @@ -12327,6 +12855,49 @@ __metadata: languageName: node linkType: hard +"msgpackr-extract@npm:^3.0.2": + version: 3.0.3 + resolution: "msgpackr-extract@npm:3.0.3" + dependencies: + "@msgpackr-extract/msgpackr-extract-darwin-arm64": "npm:3.0.3" + "@msgpackr-extract/msgpackr-extract-darwin-x64": "npm:3.0.3" + "@msgpackr-extract/msgpackr-extract-linux-arm": "npm:3.0.3" + "@msgpackr-extract/msgpackr-extract-linux-arm64": "npm:3.0.3" + "@msgpackr-extract/msgpackr-extract-linux-x64": "npm:3.0.3" + "@msgpackr-extract/msgpackr-extract-win32-x64": "npm:3.0.3" + node-gyp: "npm:latest" + node-gyp-build-optional-packages: "npm:5.2.2" + dependenciesMeta: + "@msgpackr-extract/msgpackr-extract-darwin-arm64": + optional: true + "@msgpackr-extract/msgpackr-extract-darwin-x64": + optional: true + "@msgpackr-extract/msgpackr-extract-linux-arm": + optional: true + "@msgpackr-extract/msgpackr-extract-linux-arm64": + optional: true + "@msgpackr-extract/msgpackr-extract-linux-x64": + optional: true + "@msgpackr-extract/msgpackr-extract-win32-x64": + optional: true + bin: + download-msgpackr-prebuilds: bin/download-prebuilds.js + checksum: 10c0/e504fd8bf86a29d7527c83776530ee6dc92dcb0273bb3679fd4a85173efead7f0ee32fb82c8410a13c33ef32828c45f81118ffc0fbed5d6842e72299894623b4 + languageName: node + linkType: hard + +"msgpackr@npm:2.0.1": + version: 2.0.1 + resolution: "msgpackr@npm:2.0.1" + dependencies: + msgpackr-extract: "npm:^3.0.2" + dependenciesMeta: + msgpackr-extract: + optional: true + checksum: 10c0/683eaaecf4835acb759e393cf5e76dd097354e1f3b35ed260539b4fd4e0da5523b93bbaf8fc1a956a06d9a60aa2571fe210159c6f2a29fd37a9b0fa54863b6a2 + languageName: node + linkType: hard + "multer@npm:^1.4.5-lts.2": version: 1.4.5-lts.2 resolution: "multer@npm:1.4.5-lts.2" @@ -12618,6 +13189,13 @@ __metadata: languageName: node linkType: hard +"node-abort-controller@npm:3.1.1": + version: 3.1.1 + resolution: "node-abort-controller@npm:3.1.1" + checksum: 10c0/f7ad0e7a8e33809d4f3a0d1d65036a711c39e9d23e0319d80ebe076b9a3b4432b4d6b86a7fab65521de3f6872ffed36fc35d1327487c48eb88c517803403eda3 + languageName: node + linkType: hard + "node-fetch-native@npm:^1.6.6": version: 1.6.7 resolution: "node-fetch-native@npm:1.6.7" @@ -12625,6 +13203,39 @@ __metadata: languageName: node linkType: hard +"node-gyp-build-optional-packages@npm:5.2.2": + version: 5.2.2 + resolution: "node-gyp-build-optional-packages@npm:5.2.2" + dependencies: + detect-libc: "npm:^2.0.1" + bin: + node-gyp-build-optional-packages: bin.js + node-gyp-build-optional-packages-optional: optional.js + node-gyp-build-optional-packages-test: build-test.js + checksum: 10c0/c81128c6f91873381be178c5eddcbdf66a148a6a89a427ce2bcd457593ce69baf2a8662b6d22cac092d24aa9c43c230dec4e69b3a0da604503f4777cd77e282b + languageName: node + linkType: hard + +"node-gyp@npm:latest": + version: 12.3.0 + resolution: "node-gyp@npm:12.3.0" + dependencies: + env-paths: "npm:^2.2.0" + exponential-backoff: "npm:^3.1.1" + graceful-fs: "npm:^4.2.6" + nopt: "npm:^9.0.0" + proc-log: "npm:^6.0.0" + semver: "npm:^7.3.5" + tar: "npm:^7.5.4" + tinyglobby: "npm:^0.2.12" + undici: "npm:^6.25.0" + which: "npm:^6.0.0" + bin: + node-gyp: bin/node-gyp.js + checksum: 10c0/9d9032b405cbe42f72a105259d9eb679376470c102df4a2dbaa51e07d59bf741dcffb85897087ea9d8318b9cabb824a8978af51508ae142f0239ae1e6a3c2329 + languageName: node + linkType: hard + "node-releases@npm:^2.0.27": version: 2.0.27 resolution: "node-releases@npm:2.0.27" @@ -12632,6 +13243,17 @@ __metadata: languageName: node linkType: hard +"nopt@npm:^9.0.0": + version: 9.0.0 + resolution: "nopt@npm:9.0.0" + dependencies: + abbrev: "npm:^4.0.0" + bin: + nopt: bin/nopt.js + checksum: 10c0/1822eb6f9b020ef6f7a7516d7b64a8036e09666ea55ac40416c36e4b2b343122c3cff0e2f085675f53de1d2db99a2a89a60ccea1d120bcd6a5347bf6ceb4a7fd + languageName: node + linkType: hard + "npm-run-path@npm:^5.1.0": version: 5.3.0 resolution: "npm-run-path@npm:5.3.0" @@ -13168,6 +13790,13 @@ __metadata: languageName: node linkType: hard +"picomatch@npm:^4.0.4": + version: 4.0.4 + resolution: "picomatch@npm:4.0.4" + checksum: 10c0/e2c6023372cc7b5764719a5ffb9da0f8e781212fa7ca4bd0562db929df8e117460f00dff3cb7509dacfc06b86de924b247f504d0ce1806a37fac4633081466b0 + languageName: node + linkType: hard + "pidtree@npm:~0.6.0": version: 0.6.0 resolution: "pidtree@npm:0.6.0" @@ -13449,6 +14078,13 @@ __metadata: languageName: node linkType: hard +"proc-log@npm:^6.0.0": + version: 6.1.0 + resolution: "proc-log@npm:6.1.0" + checksum: 10c0/4f178d4062733ead9d71a9b1ab24ebcecdfe2250916a5b1555f04fe2eda972a0ec76fbaa8df1ad9c02707add6749219d118a4fc46dc56bdfe4dde4b47d80bb82 + languageName: node + linkType: hard + "process-nextick-args@npm:~2.0.0": version: 2.0.1 resolution: "process-nextick-args@npm:2.0.1" @@ -14184,6 +14820,22 @@ __metadata: languageName: node linkType: hard +"redis-errors@npm:^1.0.0, redis-errors@npm:^1.2.0": + version: 1.2.0 + resolution: "redis-errors@npm:1.2.0" + checksum: 10c0/5b316736e9f532d91a35bff631335137a4f974927bb2fb42bf8c2f18879173a211787db8ac4c3fde8f75ed6233eb0888e55d52510b5620e30d69d7d719c8b8a7 + languageName: node + linkType: hard + +"redis-parser@npm:^3.0.0": + version: 3.0.0 + resolution: "redis-parser@npm:3.0.0" + dependencies: + redis-errors: "npm:^1.0.0" + checksum: 10c0/ee16ac4c7b2a60b1f42a2cdaee22b005bd4453eb2d0588b8a4939718997ae269da717434da5d570fe0b05030466eeb3f902a58cf2e8e1ca058bf6c9c596f632f + languageName: node + linkType: hard + "redux-thunk@npm:^3.1.0": version: 3.1.0 resolution: "redux-thunk@npm:3.1.0" @@ -14543,6 +15195,15 @@ __metadata: languageName: node linkType: hard +"semver@npm:7.8.0": + version: 7.8.0 + resolution: "semver@npm:7.8.0" + bin: + semver: bin/semver.js + checksum: 10c0/8f096ca9b80ffd47b308d03f9ce8c873e27e2983f36023c559cdc92c51e8433fc23ebbfe57ec9623fc155636a6961ee989501099841ae4bb1babc8d2b3f048cd + languageName: node + linkType: hard + "semver@npm:^6.3.1": version: 6.3.1 resolution: "semver@npm:6.3.1" @@ -14552,6 +15213,15 @@ __metadata: languageName: node linkType: hard +"semver@npm:^7.3.5": + version: 7.8.1 + resolution: "semver@npm:7.8.1" + bin: + semver: bin/semver.js + checksum: 10c0/92d6871d6347e1f99d0ba396a70f2545ccf2a032cda3d378fa0699edf7506b5c6d266aed55c8b88e72bd91a30d2351e4f39db479375374430fcdc4b58f4e3c1a + languageName: node + linkType: hard + "semver@npm:^7.3.7, semver@npm:^7.5.4, semver@npm:^7.6.0, semver@npm:^7.6.3": version: 7.6.3 resolution: "semver@npm:7.6.3" @@ -15159,6 +15829,13 @@ __metadata: languageName: node linkType: hard +"standard-as-callback@npm:^2.1.0": + version: 2.1.0 + resolution: "standard-as-callback@npm:2.1.0" + checksum: 10c0/012677236e3d3fdc5689d29e64ea8a599331c4babe86956bf92fc5e127d53f85411c5536ee0079c52c43beb0026b5ce7aa1d834dd35dd026e82a15d1bcaead1f + languageName: node + linkType: hard + "statuses@npm:2.0.1, statuses@npm:^2.0.1": version: 2.0.1 resolution: "statuses@npm:2.0.1" @@ -15469,6 +16146,19 @@ __metadata: languageName: node linkType: hard +"tar@npm:^7.5.4": + version: 7.5.15 + resolution: "tar@npm:7.5.15" + dependencies: + "@isaacs/fs-minipass": "npm:^4.0.0" + chownr: "npm:^3.0.0" + minipass: "npm:^7.1.2" + minizlib: "npm:^3.1.0" + yallist: "npm:^5.0.0" + checksum: 10c0/8f039edb1d12fdd7df6c6f9877d125afe9f3da3f5f9317df326fdd090d48793d6998cede1506a1471f3e3a250db270a89dace28005eb5e99c5a9132d704ac956 + languageName: node + linkType: hard + "tcp-port-used@npm:^1.0.2": version: 1.0.2 resolution: "tcp-port-used@npm:1.0.2" @@ -15514,6 +16204,16 @@ __metadata: languageName: node linkType: hard +"tinyglobby@npm:^0.2.12": + version: 0.2.16 + resolution: "tinyglobby@npm:0.2.16" + dependencies: + fdir: "npm:^6.5.0" + picomatch: "npm:^4.0.4" + checksum: 10c0/f2e09fd93dd95c41e522113b686ff6f7c13020962f8698a864a257f3d7737599afc47722b7ab726e12f8a813f779906187911ff8ee6701ede65072671a7e934b + languageName: node + linkType: hard + "tinyglobby@npm:^0.2.15": version: 0.2.15 resolution: "tinyglobby@npm:0.2.15" @@ -15704,7 +16404,7 @@ __metadata: languageName: node linkType: hard -"tslib@npm:2, tslib@npm:^2.8.0, tslib@npm:^2.8.1": +"tslib@npm:2, tslib@npm:2.8.1, tslib@npm:^2.8.0, tslib@npm:^2.8.1": version: 2.8.1 resolution: "tslib@npm:2.8.1" checksum: 10c0/9c4759110a19c53f992d9aae23aac5ced636e99887b51b9e61def52611732872ff7668757d4e4c61f19691e36f4da981cd9485e869b4a7408d689f6bf1f14e62 @@ -15736,6 +16436,21 @@ __metadata: languageName: node linkType: hard +"tsx@npm:^4.22.3": + version: 4.22.3 + resolution: "tsx@npm:4.22.3" + dependencies: + esbuild: "npm:~0.28.0" + fsevents: "npm:~2.3.3" + dependenciesMeta: + fsevents: + optional: true + bin: + tsx: dist/cli.mjs + checksum: 10c0/09a5a7d354ff75e5af6285217d709ec80a46c2ee19e0d9b5c5d00eab81047c3ea6b2a5498a2c5255f3960d6e7acd1a1eee34b9433dc56fb32907ca3b38271f41 + languageName: node + linkType: hard + "turbo-darwin-64@npm:2.5.4": version: 2.5.4 resolution: "turbo-darwin-64@npm:2.5.4" @@ -16095,6 +16810,13 @@ __metadata: languageName: node linkType: hard +"undici@npm:^6.25.0": + version: 6.25.0 + resolution: "undici@npm:6.25.0" + checksum: 10c0/2597cc6689bdb02c210c557b1f85febbfda65becae6e6fc1061508e2f33734d25207f81cd8af56ada9956329eb3a7bd7431e87dcfeceba20ee87059b57dcf985 + languageName: node + linkType: hard + "union-value@npm:^1.0.1": version: 1.0.1 resolution: "union-value@npm:1.0.1" @@ -16542,6 +17264,17 @@ __metadata: languageName: node linkType: hard +"which@npm:^6.0.0": + version: 6.0.1 + resolution: "which@npm:6.0.1" + dependencies: + isexe: "npm:^4.0.0" + bin: + node-which: bin/which.js + checksum: 10c0/7e710e54ea36d2d6183bee2f9caa27a3b47b9baf8dee55a199b736fcf85eab3b9df7556fca3d02b50af7f3dfba5ea3a45644189836df06267df457e354da66d5 + languageName: node + linkType: hard + "winston-transport@npm:^4.9.0": version: 4.9.0 resolution: "winston-transport@npm:4.9.0" @@ -16572,6 +17305,25 @@ __metadata: languageName: node linkType: hard +"winston@npm:^3.19.0": + version: 3.19.0 + resolution: "winston@npm:3.19.0" + dependencies: + "@colors/colors": "npm:^1.6.0" + "@dabh/diagnostics": "npm:^2.0.8" + async: "npm:^3.2.3" + is-stream: "npm:^2.0.0" + logform: "npm:^2.7.0" + one-time: "npm:^1.0.0" + readable-stream: "npm:^3.4.0" + safe-stable-stringify: "npm:^2.3.1" + stack-trace: "npm:0.0.x" + triple-beam: "npm:^1.3.0" + winston-transport: "npm:^4.9.0" + checksum: 10c0/341a8ccfb726120209d34e2466040e2ca72cadb1a3402c4fc90425facad002b81275675b4ab9b4432a624311bc47ef7c9fb7652c86fca454d2be2f2ee1882226 + languageName: node + linkType: hard + "word-wrap@npm:^1.2.5": version: 1.2.5 resolution: "word-wrap@npm:1.2.5" @@ -16579,6 +17331,23 @@ __metadata: languageName: node linkType: hard +"worker@workspace:apps/worker": + version: 0.0.0-use.local + resolution: "worker@workspace:apps/worker" + dependencies: + "@prisma/adapter-pg": "npm:^7.8.0" + "@repo/db": "npm:*" + "@repo/prettier-config": "workspace:^" + "@repo/typescript-config": "workspace:^" + "@types/node": "npm:^22" + bullmq: "npm:^5.77.0" + ioredis: "npm:^5.10.1" + tsx: "npm:^4.22.3" + typescript: "npm:^5.6.3" + winston: "npm:^3.19.0" + languageName: unknown + linkType: soft + "wrap-ansi@npm:^9.0.0": version: 9.0.0 resolution: "wrap-ansi@npm:9.0.0" @@ -16633,6 +17402,13 @@ __metadata: languageName: node linkType: hard +"yallist@npm:^5.0.0": + version: 5.0.0 + resolution: "yallist@npm:5.0.0" + checksum: 10c0/a499c81ce6d4a1d260d4ea0f6d49ab4da09681e32c3f0472dee16667ed69d01dae63a3b81745a24bd78476ec4fcf856114cb4896ace738e01da34b2c42235416 + languageName: node + linkType: hard + "yaml@npm:~2.5.0": version: 2.5.1 resolution: "yaml@npm:2.5.1" From 52d761ab0bdb4e327432a7d982996612d2d423a5 Mon Sep 17 00:00:00 2001 From: Nudesuppe42 Date: Fri, 22 May 2026 22:21:03 +0200 Subject: [PATCH 02/31] feat(worker): :sparkles: Basic structure Missing now: real job types and their implementation --- apps/worker/package.json | 3 +- apps/worker/src/lib/config.ts | 26 +++++ apps/worker/src/lib/discordBot.ts | 28 +++++ apps/worker/src/lib/discordWebhook.ts | 47 +++++++++ apps/worker/src/lib/logger.ts | 16 +-- apps/worker/src/lib/redis.ts | 24 +++++ apps/worker/src/main.ts | 75 +++++++++++--- apps/worker/src/queue/base.worker.ts | 100 ++++++++++++++++++ apps/worker/src/queue/cron.manager.ts | 70 +++++++++++++ apps/worker/src/tasks/base.task.ts | 9 ++ apps/worker/src/tasks/debug/console.task.ts | 17 ++++ apps/worker/src/tasks/discord/sendDm.task.ts | 101 +++++++++++++++++++ apps/worker/src/tasks/index.ts | 12 +++ apps/worker/test/index.test.ts | 27 +++++ apps/worker/tsconfig.test.json | 10 ++ 15 files changed, 541 insertions(+), 24 deletions(-) create mode 100644 apps/worker/src/lib/config.ts create mode 100644 apps/worker/src/lib/discordBot.ts create mode 100644 apps/worker/src/lib/discordWebhook.ts create mode 100644 apps/worker/src/lib/redis.ts create mode 100644 apps/worker/src/queue/base.worker.ts create mode 100644 apps/worker/src/queue/cron.manager.ts create mode 100644 apps/worker/src/tasks/base.task.ts create mode 100644 apps/worker/src/tasks/debug/console.task.ts create mode 100644 apps/worker/src/tasks/discord/sendDm.task.ts create mode 100644 apps/worker/src/tasks/index.ts create mode 100644 apps/worker/test/index.test.ts create mode 100644 apps/worker/tsconfig.test.json diff --git a/apps/worker/package.json b/apps/worker/package.json index 28507d40..875f4896 100644 --- a/apps/worker/package.json +++ b/apps/worker/package.json @@ -7,7 +7,8 @@ "dev": "infisical run --path=\"/worker\" -- tsx watch --tsconfig tsconfig.runtime.json src/main.ts", "build": "tsc", "start": "infisical run --path=\"/worker\" -- tsx --tsconfig tsconfig.runtime.json dist/main.js", - "prettier": "prettier ./src --write --ignore-unknown" + "prettier": "prettier ./src --write --ignore-unknown", + "test": "infisical run --path=\"/worker\" -- tsx --tsconfig tsconfig.test.json test/index.test.ts" }, "devDependencies": { "@repo/prettier-config": "workspace:^", diff --git a/apps/worker/src/lib/config.ts b/apps/worker/src/lib/config.ts new file mode 100644 index 00000000..782aaaca --- /dev/null +++ b/apps/worker/src/lib/config.ts @@ -0,0 +1,26 @@ +/* + * Static constant configuration values + */ + +export const config = { + // The number of worker threads to spawn for processing background jobs + workerThreadCount: 5, + eventQueueName: 'EventQueue', + retryOptions: { + attempts: 3, + backoff: { + type: 'exponential', + delay: 1000, // Initial delay of 1 second for retries + }, + }, + removalOptions: { + removeOnComplete: { + age: 3600, // 1h + count: 200, + }, + }, + webhooks: { + errorReporting: process.env.DISCORD_WEBHOOK_ERRORS || '', + logging: process.env.DISCORD_WEBHOOK_LOGGING || '', + }, +}; diff --git a/apps/worker/src/lib/discordBot.ts b/apps/worker/src/lib/discordBot.ts new file mode 100644 index 00000000..499f31ff --- /dev/null +++ b/apps/worker/src/lib/discordBot.ts @@ -0,0 +1,28 @@ +export type DiscordDmResult = { + success: string[]; + failure: string[]; +}; + +export async function sendBotMessage(content: any, users: string[]): Promise { + try { + const res = await fetch(process.env.DISCORD_BOT_API_URL + '/api/v1/website/message/blank', { + method: 'POST', + headers: { + 'Content-type': 'application/json', + authorization: `Bearer ${process.env.DISCORD_BOT_SECRET}`, + }, + body: JSON.stringify({ params: { text: content }, ids: users }), + }); + const json = await res.json(); + return { + success: json.success || [], + failure: json.failed || [], + }; + } catch (e) { + console.error(e); + return { + success: [], + failure: users, + }; + } +} diff --git a/apps/worker/src/lib/discordWebhook.ts b/apps/worker/src/lib/discordWebhook.ts new file mode 100644 index 00000000..b1a5a826 --- /dev/null +++ b/apps/worker/src/lib/discordWebhook.ts @@ -0,0 +1,47 @@ +import { logger } from './logger'; + +type WebhookPayload = { + content?: string; + username?: string; + avatar_url?: string; + embeds?: unknown[]; + [key: string]: unknown; +}; + +export class DiscordWebhook { + async send( + url: string, + payload: WebhookPayload, + ): Promise<{ ok: boolean; status: number; body?: unknown; error?: string }> { + if (!url) throw new Error('Discord webhook URL is required'); + + try { + const res = await fetch(url, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(payload), + }); + + const text = await res.text(); + let body: unknown = text; + try { + body = JSON.parse(text); + } catch { + /* not JSON */ + } + + if (!res.ok) { + logger.error('Discord webhook request failed', { status: res.status, body }); + return { ok: false, status: res.status, body, error: typeof body === 'string' ? body : undefined }; + } + + logger.info('Discord webhook sent', { status: res.status }); + return { ok: true, status: res.status, body }; + } catch (err: any) { + logger.error('Discord webhook error', { error: err?.message }); + return { ok: false, status: 0, error: err?.message }; + } + } +} + +export default new DiscordWebhook(); diff --git a/apps/worker/src/lib/logger.ts b/apps/worker/src/lib/logger.ts index 2cbb2b0b..45170d26 100644 --- a/apps/worker/src/lib/logger.ts +++ b/apps/worker/src/lib/logger.ts @@ -3,7 +3,7 @@ import winston from 'winston'; type LogMeta = { timestamp: unknown; level: string; - requestId?: string; + jobId?: string; component?: string; fields?: Record; }; @@ -28,13 +28,13 @@ const formatFieldValue = (value: unknown): string => { } }; -const formatPrefix = ({ timestamp, level, requestId, component = 'main', fields = {} }: LogMeta): string => { - return `${timestamp} | ${level} [${requestId || component}] ยป `; +const formatPrefix = ({ timestamp, level, jobId, component = 'main', fields = {} }: LogMeta): string => { + return `${timestamp} | ${level} [${jobId ? `j-${jobId}|` : ''}${component}] ยป `; }; const formatLine = (content: string | Record, meta: LogMeta): string => { if (typeof content === 'string') { - return `${formatPrefix(meta)}${content}`; + return `${formatPrefix(meta)}${content} ${Object.keys(meta.fields || {}).length > 0 ? JSON.stringify(meta.fields) : ''}`; } try { @@ -54,16 +54,16 @@ const formatBlock = (content: string, meta: LogMeta): string => { const loggerConfig = { development: { - level: 'debug', + level: 'info', format: winston.format.combine( winston.format.errors({ stack: true }), winston.format.timestamp(), winston.format.colorize(), winston.format.simple(), winston.format.printf((info) => { - const { level, message, timestamp, requestId, component, error, stack, label, ...rest } = + const { level, message, timestamp, jobId, component, error, stack, label, ...rest } = info as winston.Logform.TransformableInfo & { - requestId?: string; + jobId?: string; component?: string; error?: unknown; stack?: string; @@ -79,7 +79,7 @@ const loggerConfig = { return formatBlock(lines.join('\n'), { timestamp, level, - requestId, + jobId, component, fields: rest, }); diff --git a/apps/worker/src/lib/redis.ts b/apps/worker/src/lib/redis.ts new file mode 100644 index 00000000..53d584b5 --- /dev/null +++ b/apps/worker/src/lib/redis.ts @@ -0,0 +1,24 @@ +import IORedis from 'ioredis'; +import { logger } from './logger'; + +const REDIS_URL = process.env.REDIS_URL!; + +export const redisConnectionOptions = { + maxRetriesPerRequest: null, + enableReadyCheck: false, +}; + +export const redis = new IORedis(REDIS_URL, redisConnectionOptions); + +// Attach connection logging hooks for operations visibility +redis.on('connect', () => { + logger.info('Connected to Redis successfully'); +}); + +redis.on('error', (error) => { + logger.error('Error in Redis connection', { error: error.message }); +}); + +redis.on('close', () => { + logger.warn('Disconnected from Redis'); +}); diff --git a/apps/worker/src/main.ts b/apps/worker/src/main.ts index 9a168c24..d45fb92e 100644 --- a/apps/worker/src/main.ts +++ b/apps/worker/src/main.ts @@ -1,20 +1,11 @@ import { logger } from './lib/logger'; import prisma from './lib/prisma'; +import { redis } from './lib/redis'; +import WorkerManager from './queue/base.worker'; +import CronManager from './queue/cron.manager'; -console.clear(); -console.log( - '\n' + - ` /$$ \n` + - ` | $$ \n` + - ` /$$ /$$ /$$ /$$$$$$ /$$$$$$ | $$ /$$ /$$$$$$ /$$$$$$ \n` + - `| $$ | $$ | $$ /$$__ $$ /$$__ $$| $$ /$$/ /$$__ $$ /$$__ $$\n` + - `| $$ | $$ | $$| $$ \\ $$| $$ \\__/| $$$$$$/ | $$$$$$$$| $$ \\__/\n` + - `| $$ | $$ | $$| $$ | $$| $$ | $$_ $$ | $$_____/| $$ \n` + - `| $$$$$/$$$$/| $$$$$$/| $$ | $$ \\ $$| $$$$$$$| $$ \n` + - ` \\_____/\\___/ \\______/ |__/ |__/ \\__/ \\_______/|__/ \n` + - ` \n` + - `A BuildTheEarth Worker Node - Version ${process.env.npm_package_version}\n`, -); +const workers = new Set(); +const cronManagers = new Set(); async function bootstrap() { logger.info('Initializing Worker Node...'); @@ -28,7 +19,61 @@ async function bootstrap() { process.exit(1); } - logger.debug('Bootstrap complete'); + try { + logger.debug('Starting job worker'); + const wm = new WorkerManager().start(); + workers.add(wm); + + logger.debug('Registering cron jobs'); + const cron = new CronManager(); + + cron.register('DEBUG_CONSOLE', { content: 'Hi' }, '*/1 * * * *'); + await cron.start(); + cronManagers.add(cron); + } catch (error: any) { + logger.error('Failed to start job worker or cron jobs', { error }); + process.exit(1); + } + + logger.info('Job and Cron workers started successfully'); + + const signals = ['SIGTERM', 'SIGINT']; + for (const signal of signals) { + process.on(signal, async () => { + logger.warn(`Graceful shutdown initiated due to ${signal}...`); + + for (const active of workers) { + try { + await active.stop(); + } catch (err: any) { + logger.warn('Error while closing job worker', { error: err?.message }); + } + } + + for (const cm of cronManagers) { + try { + await cm.stop(); + } catch (err: any) { + logger.warn('Error while closing cron manager', { error: err?.message }); + } + } + + try { + await prisma.$disconnect(); + } catch (err: any) { + logger.warn('Error disconnecting prisma', { error: err?.message }); + } + + try { + redis.disconnect(); + } catch (err: any) { + logger.warn('Error disconnecting redis', { error: err?.message }); + } + + logger.info('Terminating'); + process.exit(0); + }); + } } bootstrap().catch((err) => { diff --git a/apps/worker/src/queue/base.worker.ts b/apps/worker/src/queue/base.worker.ts new file mode 100644 index 00000000..026a4c22 --- /dev/null +++ b/apps/worker/src/queue/base.worker.ts @@ -0,0 +1,100 @@ +import { Job, Worker } from 'bullmq'; +import { config } from 'src/lib/config'; +import discordWebhook from 'src/lib/discordWebhook'; +import { logger } from 'src/lib/logger'; +import prisma from 'src/lib/prisma'; +import { redis } from 'src/lib/redis'; +import { taskRegistry } from '../tasks'; + +export class WorkerManager { + private worker?: Worker; + + start(): this { + const workerHandler = async (job: Job) => { + const handler = taskRegistry[job.name]; + + if (!handler) { + throw new Error(`Unknown job type: ${job.name}`); + } + + const taskLogger = logger.child({ jobId: job.id, component: job.name }); + + taskLogger.info(`Starting job execution`); + await handler.execute(job.data, { prisma, logger: taskLogger, job }); + }; + + this.worker = new Worker(config.eventQueueName, workerHandler, { + connection: redis, + concurrency: config.workerThreadCount, + ...config.removalOptions, + }); + + this.worker.on('failed', async (job, err) => { + if (!job) { + logger.error(`Job exception occurred without job context`, { error: err?.message, component: 'worker' }); + return; + } + + const currentAttempt = job.attemptsMade; + const maxAttempts = job.opts.attempts ?? 0; + + const context = { + jobId: job.id, + component: job.name, + error: err?.message, + }; + + if (currentAttempt >= maxAttempts) { + // Reached maximum retries; leave the failed job in the failed state for inspection. + logger.error(`Execution failed, maximum retries reached:`, context); + + // Send error reporting webhook if configured + const webhookUrl = config.webhooks?.errorReporting; + if (webhookUrl) { + const payload = { + content: `BuildTheEarth Worker Job Failure`, + embeds: [ + { + title: `Job Failure: ${job.name}`, + fields: [ + { name: 'Job ID', value: String(job.id), inline: true }, + { name: 'Attempts', value: `${currentAttempt}/${maxAttempts}`, inline: true }, + { name: 'Error', value: String(err?.message ?? 'Unknown') }, + { name: 'Data', value: JSON.stringify(job.data).slice(0, 1900) }, + ], + }, + ], + }; + + try { + await discordWebhook.send(webhookUrl, payload as any); + } catch (e: any) { + logger.warn('Failed to send error webhook', { error: e?.message }); + } + } + } else { + logger.warn(`Execution failed, attempt ${currentAttempt}/${maxAttempts}:`, context); + } + }); + + this.worker.on('completed', (job) => { + logger.info(`Job completed successfully`, { jobId: job.id, component: job.name }); + }); + + this.worker.on('error', (err) => { + logger.error(`Worker error:`, { error: err?.message, component: 'worker' }); + }); + + return this; + } + + async stop() { + try { + if (this.worker) await this.worker.close(); + } catch (err: any) { + logger.warn('Error closing worker', { error: err?.message }); + } + } +} + +export default WorkerManager; diff --git a/apps/worker/src/queue/cron.manager.ts b/apps/worker/src/queue/cron.manager.ts new file mode 100644 index 00000000..c2da516a --- /dev/null +++ b/apps/worker/src/queue/cron.manager.ts @@ -0,0 +1,70 @@ +import { Queue } from 'bullmq'; +import { config } from 'src/lib/config'; +import { logger } from 'src/lib/logger'; +import { redis } from 'src/lib/redis'; + +type CronEntry = { + name: string; + data: unknown; + cron: string; + opts?: Record; +}; + +export class CronManager { + private queue: Queue; + private entries: CronEntry[] = []; + + constructor() { + this.queue = new Queue(config.eventQueueName, { connection: redis }); + } + + register(name: string, data: unknown, cron: string, opts?: Record) { + this.entries.push({ name, data, cron, opts }); + } + + async start(clearExisting = true) { + try { + if (clearExisting) { + const repeatables = await this.queue.getRepeatableJobs(); + for (const e of this.entries) { + const matches = repeatables.filter((r) => r.name === e.name); + for (const m of matches) { + try { + await this.queue.removeRepeatableByKey(m.key); + logger.debug('Removed existing repeatable cron job', { name: m.name, key: m.key }); + } catch (err: any) { + logger.warn('Failed to remove existing repeatable', { name: m.name, key: m.key, error: err?.message }); + } + } + } + logger.info('Cleared existing repeatable cron jobs'); + } + + for (const e of this.entries) { + try { + await this.queue.add(e.name, e.data, { + repeat: { cron: e.cron }, + removeOnComplete: true, + ...e.opts, + } as any); + logger.debug('Scheduled cron job', { name: e.name, cron: e.cron }); + } catch (err: any) { + logger.error('Failed to schedule cron job', { name: e.name, error: err?.message }); + } + } + logger.info(`Registered ${this.entries.length} cron job(s) successfully`); + } catch (err: any) { + logger.error('Failed to start cron manager', { error: err?.message }); + } + } + + async stop() { + try { + await this.queue.close(); + } catch (err: any) { + logger.warn('Error closing cron queue', { error: err?.message }); + } + } +} + +export default CronManager; diff --git a/apps/worker/src/tasks/base.task.ts b/apps/worker/src/tasks/base.task.ts new file mode 100644 index 00000000..95e0186e --- /dev/null +++ b/apps/worker/src/tasks/base.task.ts @@ -0,0 +1,9 @@ +import { PrismaClient } from '@repo/db'; +import { Job } from 'bullmq'; +import { Logger } from 'winston'; + +export abstract class BaseTask { + abstract readonly name: string; + + abstract execute(data: T, context: { prisma: PrismaClient; logger: Logger; job: Job }): Promise; +} diff --git a/apps/worker/src/tasks/debug/console.task.ts b/apps/worker/src/tasks/debug/console.task.ts new file mode 100644 index 00000000..597fbcf6 --- /dev/null +++ b/apps/worker/src/tasks/debug/console.task.ts @@ -0,0 +1,17 @@ +import { PrismaClient } from '@repo/db'; +import { Job } from 'bullmq'; +import { sendBotMessage } from 'src/lib/discordBot'; +import { Logger } from 'winston'; +import { BaseTask } from '../base.task'; + +interface ConsolePayload { + content: string; +} + +export class ConsoleTask extends BaseTask { + readonly name = 'DEBUG_CONSOLE'; + + async execute(data: ConsolePayload, { prisma, logger, job }: { prisma: PrismaClient; logger: Logger; job: Job }) { + logger.info(`Console Task: ${data.content}`); + } +} diff --git a/apps/worker/src/tasks/discord/sendDm.task.ts b/apps/worker/src/tasks/discord/sendDm.task.ts new file mode 100644 index 00000000..ec1c3e8f --- /dev/null +++ b/apps/worker/src/tasks/discord/sendDm.task.ts @@ -0,0 +1,101 @@ +import { PrismaClient } from '@repo/db'; +import { Job } from 'bullmq'; +import { sendBotMessage } from 'src/lib/discordBot'; +import { Logger } from 'winston'; +import { BaseTask } from '../base.task'; + +interface DiscordDmPayload { + userId?: string; + userIds?: string[]; + discordId?: string; + discordIds?: string[]; + content: string; +} + +class PartialDiscordDmFailureError extends Error { + readonly failedIds: string[]; + + constructor(failedIds: string[]) { + super(`Failed to send Discord DM to ${failedIds.length} recipient(s)`); + this.name = 'PartialDiscordDmFailureError'; + this.failedIds = failedIds; + } +} + +export class SendDiscordDmTask extends BaseTask { + readonly name = 'SEND_DISCORD_DM'; + + async execute(data: DiscordDmPayload, { prisma, logger, job }: { prisma: PrismaClient; logger: Logger; job: Job }) { + const users = await this.resolveUsers(data, prisma, logger); + if (users.length === 0) { + throw new Error(`Invalid payload: at least one discordId or userId must be provided`); + } + + const result = await sendBotMessage(data.content, users); + const failedIds = result.failure ?? []; + const successIds = result.success ?? []; + + if (failedIds.length > 0) { + logger.warn(`Failed to send Discord DM to some recipients`, { + successCount: successIds.length, + failedCount: failedIds.length, + failedIds, + }); + + // Let BullMQ retry with the failed IDs + await job.updateData({ + ...data, + discordIds: failedIds, + userIds: undefined, + discordId: undefined, + userId: undefined, + }); + throw new PartialDiscordDmFailureError(failedIds); + } + + logger.debug(`Successfully sent Discord DM`, { recipientCount: users.length }); + } + + private async resolveUsers(data: DiscordDmPayload, prisma: PrismaClient, logger: Logger): Promise { + if (Array.isArray(data.discordIds) && data.discordIds.length > 0) { + return data.discordIds; + } + + if (data.discordId) { + return [data.discordId]; + } + + if (Array.isArray(data.userIds) && data.userIds.length > 0) { + logger.debug(`Fetching user profiles`, { userCount: data.userIds.length }); + const users = await prisma.user.findMany({ + where: { OR: [{ id: { in: data.userIds } }, { ssoId: { in: data.userIds } }] }, + }); + const discordIds = users + .map((user) => user.discordId) + .filter((discordId): discordId is string => Boolean(discordId)); + + if (discordIds.length === 0) { + throw new Error(`None of the provided userIds have a linked Discord ID`); + } + + return discordIds; + } + + if (data.userId) { + logger.debug(`Fetching user profile`, { userId: data.userId }); + const user = await prisma.user.findFirst({ where: { OR: [{ id: data.userId }, { ssoId: data.userId }] } }); + + if (!user?.discordId) { + throw new Error(`User ${data.userId} does not have a linked Discord ID`); + } + + return [user.discordId]; + } + + logger.warn(`Invalid payload received for Discord DM task`, { + hasUserId: Boolean(data.userId), + hasDiscordId: Boolean(data.discordId), + }); + return []; + } +} diff --git a/apps/worker/src/tasks/index.ts b/apps/worker/src/tasks/index.ts new file mode 100644 index 00000000..120ba96b --- /dev/null +++ b/apps/worker/src/tasks/index.ts @@ -0,0 +1,12 @@ +import { BaseTask } from './base.task'; +import { ConsoleTask } from './debug/console.task'; +import { SendDiscordDmTask } from './discord/sendDm.task'; + +export const taskRegistry: Record = {}; + +function register(task: BaseTask) { + taskRegistry[task.name] = task; +} + +register(new SendDiscordDmTask()); +register(new ConsoleTask()); diff --git a/apps/worker/test/index.test.ts b/apps/worker/test/index.test.ts new file mode 100644 index 00000000..19b95043 --- /dev/null +++ b/apps/worker/test/index.test.ts @@ -0,0 +1,27 @@ +import { Queue } from 'bullmq'; +import 'dotenv/config'; +import { config } from '../src/lib/config'; +import { redis } from '../src/lib/redis'; + +const testQueue = new Queue(config.eventQueueName, { connection: redis, ...config.retryOptions }); + +async function trigger() { + console.log('๐Ÿš€ Simulating Dashboard action: Queueing a Discord DM request...'); + + await testQueue.add( + 'SEND_DISCORD_DM', + { + discordIds: ['635411595253776385', '455314410198532096'], + content: 'Hello from Redis!', + }, + { + ...config.retryOptions, + }, + ); + + console.log('โœ… Job successfully pushed to Redis. Closing connection.'); + await testQueue.close(); + process.exit(0); +} + +trigger(); diff --git a/apps/worker/tsconfig.test.json b/apps/worker/tsconfig.test.json new file mode 100644 index 00000000..2dba82e5 --- /dev/null +++ b/apps/worker/tsconfig.test.json @@ -0,0 +1,10 @@ +{ + "extends": "./tsconfig.json", + "include": ["./test/**/*"], + "compilerOptions": { + "paths": { + "@repo/*": ["../../packages/*/src"] + }, + "rootDir": "./test" + } +} From fd8e85bfd03c81f1d2a5ef6d7c34b35d8f22c574 Mon Sep 17 00:00:00 2001 From: Nudesuppe42 Date: Fri, 22 May 2026 22:22:14 +0200 Subject: [PATCH 03/31] chore(worker): :test_tube: Remove test cron job [ci skip] --- apps/worker/src/main.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/apps/worker/src/main.ts b/apps/worker/src/main.ts index d45fb92e..1efefc99 100644 --- a/apps/worker/src/main.ts +++ b/apps/worker/src/main.ts @@ -27,7 +27,6 @@ async function bootstrap() { logger.debug('Registering cron jobs'); const cron = new CronManager(); - cron.register('DEBUG_CONSOLE', { content: 'Hi' }, '*/1 * * * *'); await cron.start(); cronManagers.add(cron); } catch (error: any) { From 552abc1364248cec95a7ffd8643aef66838160f0 Mon Sep 17 00:00:00 2001 From: Nudesuppe42 Date: Sat, 23 May 2026 19:29:21 +0200 Subject: [PATCH 04/31] feat(worker/tasks): :sparkles: Add buildteam webhook audit logging task --- .vscode/settings.json | 3 +- apps/worker/src/lib/buildteamWebhook.ts | 62 ++++++++ .../src/tasks/buildteams/auditLogBt.task.ts | 137 ++++++++++++++++++ apps/worker/src/tasks/debug/console.task.ts | 17 --- 4 files changed, 201 insertions(+), 18 deletions(-) create mode 100644 apps/worker/src/lib/buildteamWebhook.ts create mode 100644 apps/worker/src/tasks/buildteams/auditLogBt.task.ts delete mode 100644 apps/worker/src/tasks/debug/console.task.ts diff --git a/.vscode/settings.json b/.vscode/settings.json index 00227c3a..0a6898fb 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -41,7 +41,8 @@ "frontend/assets", "dash/team", "frontend/mobile", - "worker" + "worker", + "worker/tasks" ], "js/ts.tsdk.path": "node_modules\\typescript\\lib" } diff --git a/apps/worker/src/lib/buildteamWebhook.ts b/apps/worker/src/lib/buildteamWebhook.ts new file mode 100644 index 00000000..6d3e13ce --- /dev/null +++ b/apps/worker/src/lib/buildteamWebhook.ts @@ -0,0 +1,62 @@ +import { logger } from './logger'; +import prisma from './prisma'; + +type WebhookPayload = { + type: string; + data: any; +}; +export type WebhookBuildTeam = + | { + url: string; + } + | { id: string } + | { slug: string }; + +export class BuildTeamWebhook { + async send( + destination: WebhookBuildTeam, + type: string, + data: WebhookPayload['data'], + ): Promise<{ ok: boolean; status: number; error?: string }> { + const url = 'url' in destination ? destination.url : await this.resolveUrl(destination); + + if (!url) { + return { ok: true, status: 202 }; + } + + try { + const res = await fetch(url, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'User-Agent': 'BuildTheEarth Worker', + Accept: 'application/json', + }, + body: JSON.stringify({ type, data }), + }); + + const body = await res.text(); + + if (!res.ok) { + logger.error('BuildTeam webhook request failed', { status: res.status }); + return { ok: false, status: res.status, error: typeof body === 'string' ? body : undefined }; + } + + logger.debug('BuildTeam webhook sent', { status: res.status }); + return { ok: true, status: res.status }; + } catch (err: any) { + logger.error('BuildTeam webhook error', { error: err?.message }); + return { ok: false, status: 0, error: err?.message }; + } + } + + private async resolveUrl(destination: WebhookBuildTeam): Promise { + const bt = await prisma.buildTeam.findUnique({ + where: + 'id' in destination ? { id: destination.id } : 'slug' in destination ? { slug: destination.slug } : { id: '' }, + }); + return bt?.webhook || null; + } +} + +export default new BuildTeamWebhook(); diff --git a/apps/worker/src/tasks/buildteams/auditLogBt.task.ts b/apps/worker/src/tasks/buildteams/auditLogBt.task.ts new file mode 100644 index 00000000..c2ff7083 --- /dev/null +++ b/apps/worker/src/tasks/buildteams/auditLogBt.task.ts @@ -0,0 +1,137 @@ +import { PrismaClient } from '@repo/db'; +import { Job } from 'bullmq'; +import { BuildTeamWebhook, WebhookBuildTeam } from 'src/lib/buildteamWebhook'; +import { sendBotMessage } from 'src/lib/discordBot'; +import { Logger } from 'winston'; +import { BaseTask } from '../base.task'; + +enum AuditLogBuildTeamType { + APPLICATION = 'APPLICATION', + APPLICATION_SEND = 'APPLICATION_SEND', + CLAIM_CREATE = 'CLAIM_CREATE', + CLAIM_UPDATE = 'CLAIM_UPDATE', + CLAIM_DELETE = 'CLAIM_DELETE', +} + +type AuditLogBtPayload = { + type: AuditLogBuildTeamType; + data?: any; +} & { destination: WebhookBuildTeam[] }; + +class PartialBuildTeamWebhookFailureError extends Error { + readonly failed: WebhookBuildTeam[]; + + constructor(failed: WebhookBuildTeam[]) { + super(`Failed to send BuildTeam Webhook to ${failed.length} BuildTeam(s)`); + this.name = 'PartialBuildTeamWebhookFailureError'; + this.failed = failed; + } +} + +export class AuditLogBtTask extends BaseTask { + readonly name = 'AUDIT_LOG_BUILDTEAM'; + + async execute(data: AuditLogBtPayload, { prisma, logger, job }: { prisma: PrismaClient; logger: Logger; job: Job }) { + const { type, data: content, destination } = data; + + if (!destination || destination.length === 0) { + return; + } + if (!Object.values(AuditLogBuildTeamType).includes(type)) { + logger.warn(`Unknown AuditLogBuildTeamType: ${type}`); + return; + } + + const failed: WebhookBuildTeam[] = []; + + for (const dest of destination) { + try { + const { ok, status, error } = await new BuildTeamWebhook().send(dest, type, this.transformData(type, content)); + if (!ok) { + failed.push(dest); + } + } catch (err: any) { + failed.push(dest); + } + } + + if (failed.length > 0) { + logger.warn(`Failed to send BuildTeam Webhook to some BuildTeams`, { + successCount: destination.length - failed.length, + failedCount: failed.length, + failed: failed.map((d) => ('url' in d ? d.url : 'id' in d ? d.id : 'slug' in d ? d.slug : 'unknown')), + }); + + // Let BullMQ retry with the failed IDs + await job.updateData({ + ...data, + destination: failed, + }); + throw new PartialBuildTeamWebhookFailureError(failed); + } + } + + private transformData(type: AuditLogBuildTeamType, data: any): any { + switch (type) { + case AuditLogBuildTeamType.APPLICATION: + case AuditLogBuildTeamType.APPLICATION_SEND: + return { + id: data.id, + status: data.status, + createdAt: data.createdAt, + reviewedAt: data.reviewedAt, + reason: data.reason, + trial: data.trial, + buildteamId: data.buildteamId, + buildteam: { + id: data.buildteam.id, + name: data.buildteam.name, + slug: data.buildteam.slug, + acceptionMessage: data.buildteam.acceptionMessage, + rejectionMessage: data.buildteam.rejectionMessage, + trialMessage: data.buildteam.trialMessage, + }, + userId: data.userId, + user: { + id: data.user.id, + username: data.user.username, + discordId: data.user.discordId, + minecraft: data.user.minecraft, + }, + reviewerId: data.reviewerId, + reviewer: data.reviewer?.id + ? { + id: data.reviewer?.id, + username: data.reviewer?.username, + discordId: data.reviewer?.discordId, + minecraft: data.reviewer?.minecraft, + } + : undefined, + ApplicationAnswer: data.ApplicationAnswer, + }; + case AuditLogBuildTeamType.CLAIM_CREATE: + case AuditLogBuildTeamType.CLAIM_UPDATE: + case AuditLogBuildTeamType.CLAIM_DELETE: + return { + id: data.id, + externalId: data.externalId, + ownerId: data.ownerId, + area: data.area, + center: data.center, + size: data.size, + buildings: data.buildings, + active: data.active, + finished: data.finished, + buildTeamId: data.buildTeamId, + name: data.name, + description: data.description, + city: data.city, + osmName: data.osmName, + createdAt: data.createdAt, + }; + default: + // DOES NOT STRIP ANY TOKEN, WEBHOOK LINK etc. + return data; + } + } +} diff --git a/apps/worker/src/tasks/debug/console.task.ts b/apps/worker/src/tasks/debug/console.task.ts deleted file mode 100644 index 597fbcf6..00000000 --- a/apps/worker/src/tasks/debug/console.task.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { PrismaClient } from '@repo/db'; -import { Job } from 'bullmq'; -import { sendBotMessage } from 'src/lib/discordBot'; -import { Logger } from 'winston'; -import { BaseTask } from '../base.task'; - -interface ConsolePayload { - content: string; -} - -export class ConsoleTask extends BaseTask { - readonly name = 'DEBUG_CONSOLE'; - - async execute(data: ConsolePayload, { prisma, logger, job }: { prisma: PrismaClient; logger: Logger; job: Job }) { - logger.info(`Console Task: ${data.content}`); - } -} From 3d219358e68491b08efdd66cbff2adeb3441ac74 Mon Sep 17 00:00:00 2001 From: Nudesuppe42 Date: Sun, 24 May 2026 14:17:25 +0200 Subject: [PATCH 05/31] feat(worker/cron): :sparkles: Remove legacy cron jobs before adding current ones --- apps/worker/src/queue/cron.manager.ts | 33 +++++++++++++++++++-------- 1 file changed, 24 insertions(+), 9 deletions(-) diff --git a/apps/worker/src/queue/cron.manager.ts b/apps/worker/src/queue/cron.manager.ts index c2da516a..959826fd 100644 --- a/apps/worker/src/queue/cron.manager.ts +++ b/apps/worker/src/queue/cron.manager.ts @@ -25,16 +25,31 @@ export class CronManager { async start(clearExisting = true) { try { if (clearExisting) { + const schedulers = await this.queue.getJobSchedulers(); + for (const scheduler of schedulers) { + try { + await this.queue.removeJobScheduler(scheduler.id ?? scheduler.key); + logger.debug('Removed existing job scheduler', { name: scheduler.name, id: scheduler.id ?? scheduler.key }); + } catch (err: any) { + logger.warn('Failed to remove existing job scheduler', { + name: scheduler.name, + id: scheduler.id ?? scheduler.key, + error: err?.message, + }); + } + } + const repeatables = await this.queue.getRepeatableJobs(); - for (const e of this.entries) { - const matches = repeatables.filter((r) => r.name === e.name); - for (const m of matches) { - try { - await this.queue.removeRepeatableByKey(m.key); - logger.debug('Removed existing repeatable cron job', { name: m.name, key: m.key }); - } catch (err: any) { - logger.warn('Failed to remove existing repeatable', { name: m.name, key: m.key, error: err?.message }); - } + for (const repeatable of repeatables) { + try { + await this.queue.removeRepeatableByKey(repeatable.key); + logger.debug('Removed legacy repeatable cron job', { name: repeatable.name, key: repeatable.key }); + } catch (err: any) { + logger.warn('Failed to remove legacy repeatable', { + name: repeatable.name, + key: repeatable.key, + error: err?.message, + }); } } logger.info('Cleared existing repeatable cron jobs'); From 97caa5213b75c95cb215c506bc13d10f440b18b9 Mon Sep 17 00:00:00 2001 From: Nudesuppe42 Date: Sun, 24 May 2026 14:17:55 +0200 Subject: [PATCH 06/31] feat(worker/tasks): :sparkles: Use Zod for input validation --- apps/worker/package.json | 1 + apps/worker/src/queue/base.worker.ts | 3 +- apps/worker/src/tasks/base.task.ts | 10 +++++-- .../src/tasks/buildteams/auditLogBt.task.ts | 29 +++++++++++++++---- apps/worker/src/tasks/discord/sendDm.task.ts | 24 ++++++++++----- apps/worker/src/tasks/index.ts | 2 -- apps/worker/test/index.test.ts | 2 +- 7 files changed, 51 insertions(+), 20 deletions(-) diff --git a/apps/worker/package.json b/apps/worker/package.json index 875f4896..b3e5839d 100644 --- a/apps/worker/package.json +++ b/apps/worker/package.json @@ -22,6 +22,7 @@ "@repo/db": "*", "bullmq": "^5.77.0", "ioredis": "^5.10.1", + "zod": "^4.1.13", "winston": "^3.19.0" }, "prettier": "@repo/prettier-config", diff --git a/apps/worker/src/queue/base.worker.ts b/apps/worker/src/queue/base.worker.ts index 026a4c22..c0c8c6a0 100644 --- a/apps/worker/src/queue/base.worker.ts +++ b/apps/worker/src/queue/base.worker.ts @@ -20,7 +20,8 @@ export class WorkerManager { const taskLogger = logger.child({ jobId: job.id, component: job.name }); taskLogger.info(`Starting job execution`); - await handler.execute(job.data, { prisma, logger: taskLogger, job }); + const data = handler.validate(job.data); + await handler.execute(data, { prisma, logger: taskLogger, job }); }; this.worker = new Worker(config.eventQueueName, workerHandler, { diff --git a/apps/worker/src/tasks/base.task.ts b/apps/worker/src/tasks/base.task.ts index 95e0186e..f206d5cf 100644 --- a/apps/worker/src/tasks/base.task.ts +++ b/apps/worker/src/tasks/base.task.ts @@ -1,9 +1,15 @@ import { PrismaClient } from '@repo/db'; import { Job } from 'bullmq'; import { Logger } from 'winston'; +import { type ZodTypeAny, z } from 'zod'; -export abstract class BaseTask { +export abstract class BaseTask { abstract readonly name: string; + abstract readonly schema: TSchema; - abstract execute(data: T, context: { prisma: PrismaClient; logger: Logger; job: Job }): Promise; + validate(data: unknown): z.infer { + return this.schema.parse(data); + } + + abstract execute(data: z.infer, context: { prisma: PrismaClient; logger: Logger; job: Job }): Promise; } diff --git a/apps/worker/src/tasks/buildteams/auditLogBt.task.ts b/apps/worker/src/tasks/buildteams/auditLogBt.task.ts index c2ff7083..4859c15e 100644 --- a/apps/worker/src/tasks/buildteams/auditLogBt.task.ts +++ b/apps/worker/src/tasks/buildteams/auditLogBt.task.ts @@ -1,8 +1,8 @@ import { PrismaClient } from '@repo/db'; import { Job } from 'bullmq'; import { BuildTeamWebhook, WebhookBuildTeam } from 'src/lib/buildteamWebhook'; -import { sendBotMessage } from 'src/lib/discordBot'; import { Logger } from 'winston'; +import { z } from 'zod'; import { BaseTask } from '../base.task'; enum AuditLogBuildTeamType { @@ -13,10 +13,19 @@ enum AuditLogBuildTeamType { CLAIM_DELETE = 'CLAIM_DELETE', } -type AuditLogBtPayload = { - type: AuditLogBuildTeamType; - data?: any; -} & { destination: WebhookBuildTeam[] }; +const webhookBuildTeamSchema = z.union([ + z.object({ url: z.string().min(1) }), + z.object({ id: z.string().min(1) }), + z.object({ slug: z.string().min(1) }), +]); + +const auditLogBtPayloadSchema = z.object({ + type: z.enum(AuditLogBuildTeamType), + data: z.unknown().optional(), + destination: z.array(webhookBuildTeamSchema), +}); + +type AuditLogBtPayload = z.infer; class PartialBuildTeamWebhookFailureError extends Error { readonly failed: WebhookBuildTeam[]; @@ -28,8 +37,9 @@ class PartialBuildTeamWebhookFailureError extends Error { } } -export class AuditLogBtTask extends BaseTask { +export class AuditLogBtTask extends BaseTask { readonly name = 'AUDIT_LOG_BUILDTEAM'; + readonly schema = auditLogBtPayloadSchema; async execute(data: AuditLogBtPayload, { prisma, logger, job }: { prisma: PrismaClient; logger: Logger; job: Job }) { const { type, data: content, destination } = data; @@ -49,6 +59,13 @@ export class AuditLogBtTask extends BaseTask { const { ok, status, error } = await new BuildTeamWebhook().send(dest, type, this.transformData(type, content)); if (!ok) { failed.push(dest); + if (error) { + logger.warn(`Failed to send BuildTeam Webhook`, { + destination: 'url' in dest ? dest.url : 'id' in dest ? dest.id : 'slug' in dest ? dest.slug : 'unknown', + error, + status, + }); + } } } catch (err: any) { failed.push(dest); diff --git a/apps/worker/src/tasks/discord/sendDm.task.ts b/apps/worker/src/tasks/discord/sendDm.task.ts index ec1c3e8f..9d0d9d47 100644 --- a/apps/worker/src/tasks/discord/sendDm.task.ts +++ b/apps/worker/src/tasks/discord/sendDm.task.ts @@ -2,15 +2,22 @@ import { PrismaClient } from '@repo/db'; import { Job } from 'bullmq'; import { sendBotMessage } from 'src/lib/discordBot'; import { Logger } from 'winston'; +import { z } from 'zod'; import { BaseTask } from '../base.task'; -interface DiscordDmPayload { - userId?: string; - userIds?: string[]; - discordId?: string; - discordIds?: string[]; - content: string; -} +const discordDmPayloadSchema = z + .object({ + userId: z.string().min(1).optional(), + userIds: z.array(z.string().min(1)).optional(), + discordId: z.string().min(1).optional(), + discordIds: z.array(z.string().min(1)).optional(), + content: z.string(), + }) + .refine((data) => Boolean(data.userId || data.userIds || data.discordId || data.discordIds), { + message: 'Invalid payload: at least one discordId or userId must be provided', + }); + +type DiscordDmPayload = z.infer; class PartialDiscordDmFailureError extends Error { readonly failedIds: string[]; @@ -22,8 +29,9 @@ class PartialDiscordDmFailureError extends Error { } } -export class SendDiscordDmTask extends BaseTask { +export class SendDiscordDmTask extends BaseTask { readonly name = 'SEND_DISCORD_DM'; + readonly schema = discordDmPayloadSchema; async execute(data: DiscordDmPayload, { prisma, logger, job }: { prisma: PrismaClient; logger: Logger; job: Job }) { const users = await this.resolveUsers(data, prisma, logger); diff --git a/apps/worker/src/tasks/index.ts b/apps/worker/src/tasks/index.ts index 120ba96b..61147c27 100644 --- a/apps/worker/src/tasks/index.ts +++ b/apps/worker/src/tasks/index.ts @@ -1,5 +1,4 @@ import { BaseTask } from './base.task'; -import { ConsoleTask } from './debug/console.task'; import { SendDiscordDmTask } from './discord/sendDm.task'; export const taskRegistry: Record = {}; @@ -9,4 +8,3 @@ function register(task: BaseTask) { } register(new SendDiscordDmTask()); -register(new ConsoleTask()); diff --git a/apps/worker/test/index.test.ts b/apps/worker/test/index.test.ts index 19b95043..34eecbb4 100644 --- a/apps/worker/test/index.test.ts +++ b/apps/worker/test/index.test.ts @@ -11,7 +11,7 @@ async function trigger() { await testQueue.add( 'SEND_DISCORD_DM', { - discordIds: ['635411595253776385', '455314410198532096'], + discordIds: ['635411595253776385'], content: 'Hello from Redis!', }, { From 04094ba42f814f16adbc77025ee2fb93b596735c Mon Sep 17 00:00:00 2001 From: Nudesuppe42 Date: Sun, 24 May 2026 14:43:24 +0200 Subject: [PATCH 07/31] feat(worker/tasks): :sparkles: Implement Review Activity Check --- apps/worker/src/queue/base.worker.ts | 2 + .../reviewActivityCheck.task.ts | 257 ++++++++++++++++++ apps/worker/src/tasks/base.task.ts | 7 + apps/worker/src/util/reviewActivity.ts | 91 +++++++ 4 files changed, 357 insertions(+) create mode 100644 apps/worker/src/tasks/administrative/reviewActivityCheck.task.ts create mode 100644 apps/worker/src/util/reviewActivity.ts diff --git a/apps/worker/src/queue/base.worker.ts b/apps/worker/src/queue/base.worker.ts index c0c8c6a0..eb1c2ae5 100644 --- a/apps/worker/src/queue/base.worker.ts +++ b/apps/worker/src/queue/base.worker.ts @@ -20,6 +20,8 @@ export class WorkerManager { const taskLogger = logger.child({ jobId: job.id, component: job.name }); taskLogger.info(`Starting job execution`); + + handler.setContext(taskLogger, prisma); const data = handler.validate(job.data); await handler.execute(data, { prisma, logger: taskLogger, job }); }; diff --git a/apps/worker/src/tasks/administrative/reviewActivityCheck.task.ts b/apps/worker/src/tasks/administrative/reviewActivityCheck.task.ts new file mode 100644 index 00000000..bbad20ee --- /dev/null +++ b/apps/worker/src/tasks/administrative/reviewActivityCheck.task.ts @@ -0,0 +1,257 @@ +import { PrismaClient } from '@repo/db'; +import { Job } from 'bullmq'; +import { config } from 'src/lib/config'; +import { sendBotMessage } from 'src/lib/discordBot'; +import discordWebhook from 'src/lib/discordWebhook'; +import { getReviewActivityScore } from 'src/util/reviewActivity'; +import { Logger } from 'winston'; +import { z } from 'zod'; +import { BaseTask } from '../base.task'; + +const reviewActivityCheckPayloadSchema = z.void(); +type reviewActivityCheckPayloadSchema = z.infer; + +type ReviewActivityData = { + date: Date; + current: { id: string; art: number; par: number; ps: number; res: number; ras: number }[]; + compared: { id: string; art: number; par: number; ps: number; res: number; ras: number }[]; +}; + +export class ReviewActivityCheckTask extends BaseTask { + readonly name = 'SEND_DISCORD_DM'; + readonly schema = reviewActivityCheckPayloadSchema; + private readonly CHUNK_SIZE = 9; + + async execute(_data: reviewActivityCheckPayloadSchema) { + const pastData = await this.fetchPastData(); + const buildTeams = await this.fetchBuildTeams(); + + const newData = await this.calculateReviewActivityScores(buildTeams, pastData); + + await this.saveDataToDB(newData); + await this.sendDiscordMessages(newData, buildTeams); + } + + private async fetchPastData() { + this.logger.debug('Fetching past data from DB...'); + + const pastData = ((await this.prisma.jsonStore.findFirst({ where: { id: 'pastReviewActivity' } }))?.data || { + date: new Date(), + current: [], + compared: [], + }) as ReviewActivityData; + + this.logger.debug(`Found data from ${pastData.date.toISOString()} with ${pastData.current.length} BuildTeams.`); + return pastData; + } + + private async fetchBuildTeams() { + this.logger.debug('Fetching BuildTeams...'); + + return this.prisma.buildTeam.findMany({ + select: { id: true, name: true }, + where: { allowApplications: true }, + }); + } + + private async calculateReviewActivityScores( + buildTeams: { id: string; name: string }[], + pastData: ReviewActivityData, + ): Promise { + const newData = { + date: new Date(), + current: [] as { id: string; art: number; par: number; ps: number; res: number; ras: number }[], + compared: [] as { id: string; art: number; par: number; ps: number; res: number; ras: number }[], + }; + + const reviewActivities = await Promise.all( + buildTeams.map(async (buildTeam, i) => { + const reviewActivity = await getReviewActivityScore(buildTeam.id); + const pastReviewActivity = pastData.current.find((team: any) => team.id === buildTeam.id) || { + id: buildTeam.id, + art: 0, + par: 0, + ps: 0, + res: 0, + ras: 0, + }; + + return { buildTeam, reviewActivity, pastReviewActivity }; + }), + ); + + reviewActivities.forEach(({ buildTeam, reviewActivity, pastReviewActivity }) => { + newData.current.push({ id: buildTeam.id, ...reviewActivity }); + newData.compared.push({ + id: buildTeam.id, + art: reviewActivity.art - pastReviewActivity.art, + par: reviewActivity.par - pastReviewActivity.par, + ps: reviewActivity.ps - pastReviewActivity.ps, + res: reviewActivity.res - pastReviewActivity.res, + ras: reviewActivity.ras - pastReviewActivity.ras, + }); + }); + + return newData; + } + + private async saveDataToDB(newData: ReviewActivityData) { + this.logger.debug('Saving new data to DB...'); + await this.prisma.jsonStore.upsert({ + where: { id: 'pastReviewActivity' }, + update: { data: newData }, + create: { id: 'pastReviewActivity', data: newData }, + }); + + this.logger.debug('Data saved successfully.'); + } + + private async sendDiscordMessages(newData: ReviewActivityData, buildTeams: { id: string; name: string }[]) { + this.logger.debug('Sending Discord messages for review activity changes...'); + const filteredData = this.filterSignificantChanges(newData.compared, newData.current); + + if (filteredData.length > 0) { + await this.sendChunkedMessages(filteredData, buildTeams); + } else { + await this.sendNoChangesMessage(); + } + + await this.sendSummaryMessage(newData, buildTeams); + } + + private filterSignificantChanges(comparedData: any[], currentData: any[]) { + const significantIds = new Set( + comparedData + .filter( + (team) => + Math.abs(team.art) > 1 || + Math.abs(team.par) > 1 || + Math.abs(team.ps) > 1 || + Math.abs(team.res) > 1 || + Math.abs(team.ras) > 1, + ) + .map((team) => team.id), + ); + return currentData.filter((team) => significantIds.has(team.id)); + } + + private async sendChunkedMessages(filteredData: any[], buildTeams: { id: string; name: string }[]) { + const messages: any[] = []; + for (let i = 0; i < filteredData.length; i += this.CHUNK_SIZE) { + const chunk = filteredData.slice(i, i + this.CHUNK_SIZE); + messages.push({ + embeds: chunk.map((team) => this.scoreToEmbed(buildTeams.find((t) => t.id === team.id)?.name || team.id, team)), + attachments: [], + components: [], + }); + } + + for (const message of messages) { + const res = await discordWebhook.send(config.webhooks.logging, message as any); + + if (!res.ok) { + this.logger.warn(`Failed to send review activity changes message to Discord`, { + status: res.status, + error: res.error, + }); + } + } + } + + private async sendNoChangesMessage() { + const res = await discordWebhook.send(config.webhooks.logging, { + content: 'No significant changes in review activity score.', + author: 'Daily Review Activity Score Changes', + }); + + if (!res.ok) { + this.logger.warn(`Failed to send 'No changes' message to Discord`, { + status: res.status, + error: res.error, + }); + } + } + + private async sendSummaryMessage(newData: any, buildTeams: { id: string; name: string }[]) { + const summaryMessage = { + embeds: [ + { + title: `Summary - ${new Date().toLocaleDateString()}`, + color: 0x00ff00, + fields: [ + { + name: 'Bad Scores', + value: this.formatScores(newData.current, buildTeams, (ras) => ras < 2), + inline: true, + }, + { + name: 'Medium Scores', + value: this.formatScores(newData.current, buildTeams, (ras) => ras >= 2 && ras < 3.75), + inline: true, + }, + ], + timestamp: new Date().toISOString(), + }, + ], + attachments: [], + components: [], + }; + + const res = await discordWebhook.send(config.webhooks.logging, summaryMessage as any); + + if (!res.ok) { + this.logger.warn(`Failed to send 'Summary' message to Discord`, { + status: res.status, + error: res.error, + }); + } + } + + private scoreToEmbed( + teamName: string, + scores: { art: number; par: number; ps: number; res: number; ras: number }, + ): { title: string; color: number; fields: { name: string; value: string; inline: true }[] } { + return { + title: `Review Activity Score for ${teamName}`, + color: this.getColorForRas(scores.ras), + fields: [ + { name: 'Average Review Time (ART)', value: `${scores.art.toFixed(2)} Days`, inline: true }, + { name: 'Pending Application Ratio (PAR)', value: `${scores.par.toFixed(2)}%`, inline: true }, + { + name: 'Review Efficiency Score (RES)', + value: `${'โญ'.repeat(Math.min(5, Math.max(0, Math.round(scores.res)))).padEnd(5, 'โ˜†')}`, + inline: true, + }, + { + name: 'Processing Speed (PS)', + value: `${'โญ'.repeat(Math.min(5, Math.max(0, Math.round(scores.ps)))).padEnd(5, 'โ˜†')}`, + inline: true, + }, + { + name: 'Review Activity Score (RAS)', + value: `${'โญ'.repeat(Math.min(5, Math.max(0, Math.round(scores.ras)))).padEnd(5, 'โ˜†')}`, + inline: true, + }, + ], + }; + } + + private getColorForRas(ras: number): number { + if (ras < 2) return 0xff0000; // Red + if (ras < 3.75) return 0xffa500; // Orange + return 0x00ff00; // Green + } + + private formatScores( + teams: { id: string; ras: number }[], + buildTeams: { id: string; name: string }[], + filterFn: (ras: number) => boolean, + ) { + return ( + teams + .filter((team) => filterFn(team.ras)) + .map((team) => `${buildTeams.find((t) => t.id === team.id)?.name || team.id}: ${team.ras.toFixed(2)}`) + .join('\n') || 'None' + ); + } +} diff --git a/apps/worker/src/tasks/base.task.ts b/apps/worker/src/tasks/base.task.ts index f206d5cf..a014158f 100644 --- a/apps/worker/src/tasks/base.task.ts +++ b/apps/worker/src/tasks/base.task.ts @@ -6,6 +6,13 @@ import { type ZodTypeAny, z } from 'zod'; export abstract class BaseTask { abstract readonly name: string; abstract readonly schema: TSchema; + logger: Logger; + prisma: PrismaClient; + + setContext(logger: Logger, prisma: PrismaClient) { + this.logger = logger; + this.prisma = prisma; + } validate(data: unknown): z.infer { return this.schema.parse(data); diff --git a/apps/worker/src/util/reviewActivity.ts b/apps/worker/src/util/reviewActivity.ts new file mode 100644 index 00000000..970d96a3 --- /dev/null +++ b/apps/worker/src/util/reviewActivity.ts @@ -0,0 +1,91 @@ +import { ApplicationStatus } from '@repo/db'; +import prisma from '../db'; + +/** + * Calculate Review Activity Score for a Build Team + * @param buildteamId Build Team ID + * @returns PAR: Pending Application Ratio, PS: Pending Score, ART: Average Review Time, RES: Review Efficiency Score, RAS: Review Activity Score + */ +export async function getReviewActivityScore( + buildteamId: string, +): Promise<{ art: number; par: number; ps: number; res: number; ras: number }> { + let art = 0, + par = 0, + ps = 0, + res = 0, + ras = 0; + + // 1. Calculate Average Review Time (ART) + // ART = average(reviewedAt- createdAt for ACCEPTED, DECLINED, TRIAL Applications) in days + + const reviewedApplications = await prisma.application.findMany({ + where: { + buildteamId, + status: { notIn: [ApplicationStatus.SEND, ApplicationStatus.REVIEWING] }, + reviewedAt: { not: null }, + }, + select: { + createdAt: true, + reviewedAt: true, + }, + }); + + if (reviewedApplications.length > 0) { + const artArray = reviewedApplications.map((application) => { + if (application.reviewedAt === null) { + return 0; + } + return application.reviewedAt?.getTime() - application.createdAt.getTime(); + }); + art = + Math.floor((artArray.reduce((acc, curr) => acc + curr, 0) / artArray.length / (1000 * 60 * 60 * 24)) * 100) / 100; + } else { + art = 0; + } + + // 2. Calculate Review Efficiency Score (RES) + // RES = Math.round((5 - (art - getTargetART()) * 0.4) * 100) / 100 + + res = Math.round((5 - (art - getTargetART()) * 0.4) * 100) / 100; + + // 3. Calculate Pending Application Ratio (PAR) + // PAR = (Number of SEND Applications) / (Total Number of Applications) + + const applicationsByType = await prisma.application.groupBy({ + where: { + buildteamId, + }, + by: ['status'], + _count: true, + }); + const sendApplications = applicationsByType.find((a) => a.status === ApplicationStatus.SEND)?._count || 0; + const totalApplications = applicationsByType.reduce((acc, curr) => acc + curr._count, 0); + + par = Math.round((sendApplications / totalApplications) * 10000) / 100; + + // 4. Calculate Pending Score (PS) + // PS = (100 - par) / 10 [0, 10] + + ps = (100 - par) / 10; + + // 5. Calculate Review Activity Score (RAS) + // RAS = 1 + (((RES * 0.7) + (PS * 0.3)) * 4)) + + ras = Math.round((res * 0.7 + (ps / 2) * 0.3) * 100) / 100; + + if (ras < 0) ras = 0; + if (ras > 5) ras = 5; + + return { + art, + res, + par, + ps, + ras, + }; +} + +function getTargetART(): number { + // Example: Target ART of 3 days (in milliseconds) + return 2; +} From 4d51378e716be54f5c8322193bb1eea734b66be0 Mon Sep 17 00:00:00 2001 From: Nudesuppe42 Date: Sun, 24 May 2026 14:43:51 +0200 Subject: [PATCH 08/31] fix(worker): :bug: Change prisma import in review activity util --- apps/worker/src/util/reviewActivity.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/worker/src/util/reviewActivity.ts b/apps/worker/src/util/reviewActivity.ts index 970d96a3..93c90663 100644 --- a/apps/worker/src/util/reviewActivity.ts +++ b/apps/worker/src/util/reviewActivity.ts @@ -1,5 +1,5 @@ import { ApplicationStatus } from '@repo/db'; -import prisma from '../db'; +import prisma from 'src/lib/prisma'; /** * Calculate Review Activity Score for a Build Team From 6936bbb4e7e47d9476146337c363bf15b79984cf Mon Sep 17 00:00:00 2001 From: Nudesuppe42 Date: Sun, 24 May 2026 14:46:24 +0200 Subject: [PATCH 09/31] refactor(worker): :recycle: Simplify execution logic of tasks --- .../reviewActivityCheck.task.ts | 2 +- apps/worker/src/tasks/base.task.ts | 2 +- .../src/tasks/buildteams/auditLogBt.task.ts | 8 ++++---- apps/worker/src/tasks/discord/sendDm.task.ts | 20 +++++++++---------- 4 files changed, 16 insertions(+), 16 deletions(-) diff --git a/apps/worker/src/tasks/administrative/reviewActivityCheck.task.ts b/apps/worker/src/tasks/administrative/reviewActivityCheck.task.ts index bbad20ee..4b80f5a8 100644 --- a/apps/worker/src/tasks/administrative/reviewActivityCheck.task.ts +++ b/apps/worker/src/tasks/administrative/reviewActivityCheck.task.ts @@ -22,7 +22,7 @@ export class ReviewActivityCheckTask extends BaseTask { return this.schema.parse(data); } - abstract execute(data: z.infer, context: { prisma: PrismaClient; logger: Logger; job: Job }): Promise; + abstract execute(data: z.infer, job: Job): Promise; } diff --git a/apps/worker/src/tasks/buildteams/auditLogBt.task.ts b/apps/worker/src/tasks/buildteams/auditLogBt.task.ts index 4859c15e..04be792b 100644 --- a/apps/worker/src/tasks/buildteams/auditLogBt.task.ts +++ b/apps/worker/src/tasks/buildteams/auditLogBt.task.ts @@ -41,14 +41,14 @@ export class AuditLogBtTask extends BaseTask { readonly name = 'AUDIT_LOG_BUILDTEAM'; readonly schema = auditLogBtPayloadSchema; - async execute(data: AuditLogBtPayload, { prisma, logger, job }: { prisma: PrismaClient; logger: Logger; job: Job }) { + async execute(data: AuditLogBtPayload, job: Job) { const { type, data: content, destination } = data; if (!destination || destination.length === 0) { return; } if (!Object.values(AuditLogBuildTeamType).includes(type)) { - logger.warn(`Unknown AuditLogBuildTeamType: ${type}`); + this.logger.warn(`Unknown AuditLogBuildTeamType: ${type}`); return; } @@ -60,7 +60,7 @@ export class AuditLogBtTask extends BaseTask { if (!ok) { failed.push(dest); if (error) { - logger.warn(`Failed to send BuildTeam Webhook`, { + this.logger.warn(`Failed to send BuildTeam Webhook`, { destination: 'url' in dest ? dest.url : 'id' in dest ? dest.id : 'slug' in dest ? dest.slug : 'unknown', error, status, @@ -73,7 +73,7 @@ export class AuditLogBtTask extends BaseTask { } if (failed.length > 0) { - logger.warn(`Failed to send BuildTeam Webhook to some BuildTeams`, { + this.logger.warn(`Failed to send BuildTeam Webhook to some BuildTeams`, { successCount: destination.length - failed.length, failedCount: failed.length, failed: failed.map((d) => ('url' in d ? d.url : 'id' in d ? d.id : 'slug' in d ? d.slug : 'unknown')), diff --git a/apps/worker/src/tasks/discord/sendDm.task.ts b/apps/worker/src/tasks/discord/sendDm.task.ts index 9d0d9d47..2fc54e75 100644 --- a/apps/worker/src/tasks/discord/sendDm.task.ts +++ b/apps/worker/src/tasks/discord/sendDm.task.ts @@ -33,8 +33,8 @@ export class SendDiscordDmTask extends BaseTask { readonly name = 'SEND_DISCORD_DM'; readonly schema = discordDmPayloadSchema; - async execute(data: DiscordDmPayload, { prisma, logger, job }: { prisma: PrismaClient; logger: Logger; job: Job }) { - const users = await this.resolveUsers(data, prisma, logger); + async execute(data: DiscordDmPayload, job: Job) { + const users = await this.resolveUsers(data); if (users.length === 0) { throw new Error(`Invalid payload: at least one discordId or userId must be provided`); } @@ -44,7 +44,7 @@ export class SendDiscordDmTask extends BaseTask { const successIds = result.success ?? []; if (failedIds.length > 0) { - logger.warn(`Failed to send Discord DM to some recipients`, { + this.logger.warn(`Failed to send Discord DM to some recipients`, { successCount: successIds.length, failedCount: failedIds.length, failedIds, @@ -61,10 +61,10 @@ export class SendDiscordDmTask extends BaseTask { throw new PartialDiscordDmFailureError(failedIds); } - logger.debug(`Successfully sent Discord DM`, { recipientCount: users.length }); + this.logger.debug(`Successfully sent Discord DM`, { recipientCount: users.length }); } - private async resolveUsers(data: DiscordDmPayload, prisma: PrismaClient, logger: Logger): Promise { + private async resolveUsers(data: DiscordDmPayload): Promise { if (Array.isArray(data.discordIds) && data.discordIds.length > 0) { return data.discordIds; } @@ -74,8 +74,8 @@ export class SendDiscordDmTask extends BaseTask { } if (Array.isArray(data.userIds) && data.userIds.length > 0) { - logger.debug(`Fetching user profiles`, { userCount: data.userIds.length }); - const users = await prisma.user.findMany({ + this.logger.debug(`Fetching user profiles`, { userCount: data.userIds.length }); + const users = await this.prisma.user.findMany({ where: { OR: [{ id: { in: data.userIds } }, { ssoId: { in: data.userIds } }] }, }); const discordIds = users @@ -90,8 +90,8 @@ export class SendDiscordDmTask extends BaseTask { } if (data.userId) { - logger.debug(`Fetching user profile`, { userId: data.userId }); - const user = await prisma.user.findFirst({ where: { OR: [{ id: data.userId }, { ssoId: data.userId }] } }); + this.logger.debug(`Fetching user profile`, { userId: data.userId }); + const user = await this.prisma.user.findFirst({ where: { OR: [{ id: data.userId }, { ssoId: data.userId }] } }); if (!user?.discordId) { throw new Error(`User ${data.userId} does not have a linked Discord ID`); @@ -100,7 +100,7 @@ export class SendDiscordDmTask extends BaseTask { return [user.discordId]; } - logger.warn(`Invalid payload received for Discord DM task`, { + this.logger.warn(`Invalid payload received for Discord DM task`, { hasUserId: Boolean(data.userId), hasDiscordId: Boolean(data.discordId), }); From b54d2ecc04f1361b834524407eaa0153c568bc1a Mon Sep 17 00:00:00 2001 From: Nudesuppe42 Date: Sun, 24 May 2026 15:17:05 +0200 Subject: [PATCH 10/31] feat(worker/tasks): :sparkles: Add Review Activity Check task and cron job registration --- apps/worker/src/lib/logger.ts | 4 +++- apps/worker/src/main.ts | 3 +++ apps/worker/src/queue/base.worker.ts | 2 +- .../src/tasks/administrative/reviewActivityCheck.task.ts | 2 +- apps/worker/src/tasks/index.ts | 4 ++++ 5 files changed, 12 insertions(+), 3 deletions(-) diff --git a/apps/worker/src/lib/logger.ts b/apps/worker/src/lib/logger.ts index 45170d26..ab669057 100644 --- a/apps/worker/src/lib/logger.ts +++ b/apps/worker/src/lib/logger.ts @@ -34,7 +34,9 @@ const formatPrefix = ({ timestamp, level, jobId, component = 'main', fields = {} const formatLine = (content: string | Record, meta: LogMeta): string => { if (typeof content === 'string') { - return `${formatPrefix(meta)}${content} ${Object.keys(meta.fields || {}).length > 0 ? JSON.stringify(meta.fields) : ''}`; + return `${formatPrefix(meta)}${content} ${ + Object.keys(meta.fields || {}).length > 0 ? JSON.stringify(meta.fields) : '' + }`; } try { diff --git a/apps/worker/src/main.ts b/apps/worker/src/main.ts index 1efefc99..bea57905 100644 --- a/apps/worker/src/main.ts +++ b/apps/worker/src/main.ts @@ -27,6 +27,9 @@ async function bootstrap() { logger.debug('Registering cron jobs'); const cron = new CronManager(); + // Cron jobs + cron.register('REVIEW_ACTIVITY_CHECK', {}, '0 0 * * *'); + await cron.start(); cronManagers.add(cron); } catch (error: any) { diff --git a/apps/worker/src/queue/base.worker.ts b/apps/worker/src/queue/base.worker.ts index eb1c2ae5..fad38613 100644 --- a/apps/worker/src/queue/base.worker.ts +++ b/apps/worker/src/queue/base.worker.ts @@ -23,7 +23,7 @@ export class WorkerManager { handler.setContext(taskLogger, prisma); const data = handler.validate(job.data); - await handler.execute(data, { prisma, logger: taskLogger, job }); + await handler.execute(data, job); }; this.worker = new Worker(config.eventQueueName, workerHandler, { diff --git a/apps/worker/src/tasks/administrative/reviewActivityCheck.task.ts b/apps/worker/src/tasks/administrative/reviewActivityCheck.task.ts index 4b80f5a8..a7aba1e7 100644 --- a/apps/worker/src/tasks/administrative/reviewActivityCheck.task.ts +++ b/apps/worker/src/tasks/administrative/reviewActivityCheck.task.ts @@ -18,7 +18,7 @@ type ReviewActivityData = { }; export class ReviewActivityCheckTask extends BaseTask { - readonly name = 'SEND_DISCORD_DM'; + readonly name = 'REVIEW_ACTIVITY_CHECK'; readonly schema = reviewActivityCheckPayloadSchema; private readonly CHUNK_SIZE = 9; diff --git a/apps/worker/src/tasks/index.ts b/apps/worker/src/tasks/index.ts index 61147c27..fc98ded3 100644 --- a/apps/worker/src/tasks/index.ts +++ b/apps/worker/src/tasks/index.ts @@ -1,4 +1,6 @@ +import { ReviewActivityCheckTask } from './administrative/reviewActivityCheck.task'; import { BaseTask } from './base.task'; +import { AuditLogBtTask } from './buildteams/auditLogBt.task'; import { SendDiscordDmTask } from './discord/sendDm.task'; export const taskRegistry: Record = {}; @@ -8,3 +10,5 @@ function register(task: BaseTask) { } register(new SendDiscordDmTask()); +register(new ReviewActivityCheckTask()); +register(new AuditLogBtTask()); From 2911c94f1885c96c2063542196048e4d1cf78149 Mon Sep 17 00:00:00 2001 From: Nudesuppe42 Date: Sun, 24 May 2026 16:53:27 +0200 Subject: [PATCH 11/31] feat(worker/discord): :sparkles: Support advanced DM template for discord DMs --- apps/worker/src/lib/discordBot.ts | 50 ++++++++++++++++++- .../reviewActivityCheck.task.ts | 2 +- apps/worker/src/tasks/discord/sendDm.task.ts | 8 ++- apps/worker/test/index.test.ts | 7 ++- 4 files changed, 59 insertions(+), 8 deletions(-) diff --git a/apps/worker/src/lib/discordBot.ts b/apps/worker/src/lib/discordBot.ts index 499f31ff..ffcd890a 100644 --- a/apps/worker/src/lib/discordBot.ts +++ b/apps/worker/src/lib/discordBot.ts @@ -1,10 +1,58 @@ +import z from 'zod'; + export type DiscordDmResult = { success: string[]; failure: string[]; }; -export async function sendBotMessage(content: any, users: string[]): Promise { +export enum DiscordBotEmojisRaw { + WARN = '<:warn:1441532241628102686>', + UNMUTE = '<:unmute:1441532234573156433>', + UNBAN = '<:unban:1441532232627130548>', + MUTE = '<:mute:1441532230643224587>', + KICK = '<:kick:1441532228550131725>', + INVALID = '<:invalid:1441532226427813901>', + INFORMATION = '<:information:1441532225119191175>', + INPROGRESS = '<:inprogress:1441532224473268234>', + FORWARDED = '<:forwarded:1441532223298863305>', + DUPLICATE = '<:duplicate:1441532221470146663>', + DENIED = '<:denied:1441532217779294350>', + BAN = '<:ban:1441532215828676790>', + APPROVED = '<:approved:1441532214562128034>', +} +export enum DiscordBotEmojis { + WARN = 'WARN', + UNMUTE = 'UNMUTE', + UNBAN = 'UNBAN', + MUTE = 'MUTE', + KICK = 'KICK', + INVALID = 'INVALID', + INFORMATION = 'INFORMATION', + INPROGRESS = 'INPROGRESS', + FORWARDED = 'FORWARDED', + DUPLICATE = 'DUPLICATE', + DENIED = 'DENIED', + BAN = 'BAN', + APPROVED = 'APPROVED', +} + +export const discordBotMessageMessageSchema = z.object({ + title: z.string(), + emoji: z.enum(DiscordBotEmojis), + body: z.string(), + footer: z.string().optional(), +}); + +export async function sendDiscordDm( + message: string | z.infer, + users: string[], +): Promise { try { + const content = + typeof message === 'string' + ? message + : `## ${DiscordBotEmojisRaw[message.emoji]} ${message.title}\n\n${message.body}${message.footer ? `\n\n-# ${message.footer}` : ''}`; + const res = await fetch(process.env.DISCORD_BOT_API_URL + '/api/v1/website/message/blank', { method: 'POST', headers: { diff --git a/apps/worker/src/tasks/administrative/reviewActivityCheck.task.ts b/apps/worker/src/tasks/administrative/reviewActivityCheck.task.ts index a7aba1e7..7f9d5ebf 100644 --- a/apps/worker/src/tasks/administrative/reviewActivityCheck.task.ts +++ b/apps/worker/src/tasks/administrative/reviewActivityCheck.task.ts @@ -1,7 +1,7 @@ import { PrismaClient } from '@repo/db'; import { Job } from 'bullmq'; import { config } from 'src/lib/config'; -import { sendBotMessage } from 'src/lib/discordBot'; +import { sendDiscordDm } from 'src/lib/discordBot'; import discordWebhook from 'src/lib/discordWebhook'; import { getReviewActivityScore } from 'src/util/reviewActivity'; import { Logger } from 'winston'; diff --git a/apps/worker/src/tasks/discord/sendDm.task.ts b/apps/worker/src/tasks/discord/sendDm.task.ts index 2fc54e75..07a3739a 100644 --- a/apps/worker/src/tasks/discord/sendDm.task.ts +++ b/apps/worker/src/tasks/discord/sendDm.task.ts @@ -1,7 +1,5 @@ -import { PrismaClient } from '@repo/db'; import { Job } from 'bullmq'; -import { sendBotMessage } from 'src/lib/discordBot'; -import { Logger } from 'winston'; +import { discordBotMessageMessageSchema, sendDiscordDm } from 'src/lib/discordBot'; import { z } from 'zod'; import { BaseTask } from '../base.task'; @@ -11,7 +9,7 @@ const discordDmPayloadSchema = z userIds: z.array(z.string().min(1)).optional(), discordId: z.string().min(1).optional(), discordIds: z.array(z.string().min(1)).optional(), - content: z.string(), + content: discordBotMessageMessageSchema.or(z.string()), }) .refine((data) => Boolean(data.userId || data.userIds || data.discordId || data.discordIds), { message: 'Invalid payload: at least one discordId or userId must be provided', @@ -39,7 +37,7 @@ export class SendDiscordDmTask extends BaseTask { throw new Error(`Invalid payload: at least one discordId or userId must be provided`); } - const result = await sendBotMessage(data.content, users); + const result = await sendDiscordDm(data.content, users); const failedIds = result.failure ?? []; const successIds = result.success ?? []; diff --git a/apps/worker/test/index.test.ts b/apps/worker/test/index.test.ts index 34eecbb4..d6003287 100644 --- a/apps/worker/test/index.test.ts +++ b/apps/worker/test/index.test.ts @@ -12,7 +12,12 @@ async function trigger() { 'SEND_DISCORD_DM', { discordIds: ['635411595253776385'], - content: 'Hello from Redis!', + content: { + title: 'Test Message from Worker', + emoji: 'INFORMATION', + body: 'This is a test message sent from the Worker to verify that the Discord DM functionality is working correctly.', + footer: 'This message was generated during testing.', + }, }, { ...config.retryOptions, From 5444c2c73f1b568a13d313a9290b498d407c62ec Mon Sep 17 00:00:00 2001 From: Nudesuppe42 Date: Sun, 24 May 2026 17:02:52 +0200 Subject: [PATCH 12/31] feat(worker/tasks): :sparkles: Internal Logging task, rename buildteam webhook task --- apps/worker/src/lib/discordWebhook.ts | 2 +- ...auditLogBt.task.ts => sendWebhook.task.ts} | 4 ++-- apps/worker/src/tasks/discord/sendLog.task.ts | 22 +++++++++++++++++++ apps/worker/src/tasks/index.ts | 6 +++-- apps/worker/test/index.test.ts | 10 ++------- 5 files changed, 31 insertions(+), 13 deletions(-) rename apps/worker/src/tasks/buildteams/{auditLogBt.task.ts => sendWebhook.task.ts} (97%) create mode 100644 apps/worker/src/tasks/discord/sendLog.task.ts diff --git a/apps/worker/src/lib/discordWebhook.ts b/apps/worker/src/lib/discordWebhook.ts index b1a5a826..a8d93ab3 100644 --- a/apps/worker/src/lib/discordWebhook.ts +++ b/apps/worker/src/lib/discordWebhook.ts @@ -35,7 +35,7 @@ export class DiscordWebhook { return { ok: false, status: res.status, body, error: typeof body === 'string' ? body : undefined }; } - logger.info('Discord webhook sent', { status: res.status }); + logger.debug('Discord webhook sent', { status: res.status }); return { ok: true, status: res.status, body }; } catch (err: any) { logger.error('Discord webhook error', { error: err?.message }); diff --git a/apps/worker/src/tasks/buildteams/auditLogBt.task.ts b/apps/worker/src/tasks/buildteams/sendWebhook.task.ts similarity index 97% rename from apps/worker/src/tasks/buildteams/auditLogBt.task.ts rename to apps/worker/src/tasks/buildteams/sendWebhook.task.ts index 04be792b..342e2f04 100644 --- a/apps/worker/src/tasks/buildteams/auditLogBt.task.ts +++ b/apps/worker/src/tasks/buildteams/sendWebhook.task.ts @@ -37,8 +37,8 @@ class PartialBuildTeamWebhookFailureError extends Error { } } -export class AuditLogBtTask extends BaseTask { - readonly name = 'AUDIT_LOG_BUILDTEAM'; +export class SendBuildTeamWebhookTask extends BaseTask { + readonly name = 'BUILDTEAM_WEBHOOK'; readonly schema = auditLogBtPayloadSchema; async execute(data: AuditLogBtPayload, job: Job) { diff --git a/apps/worker/src/tasks/discord/sendLog.task.ts b/apps/worker/src/tasks/discord/sendLog.task.ts new file mode 100644 index 00000000..5f26f578 --- /dev/null +++ b/apps/worker/src/tasks/discord/sendLog.task.ts @@ -0,0 +1,22 @@ +import { Job } from 'bullmq'; +import { config } from 'src/lib/config'; +import discordWebhook from 'src/lib/discordWebhook'; +import z from 'zod'; +import { BaseTask } from '../base.task'; + +const discordLogPayloadSchema = z.any(); + +type DiscordLogPayload = z.infer; + +export class SendDiscordLogTask extends BaseTask { + readonly name = 'SEND_DISCORD_LOG'; + readonly schema = discordLogPayloadSchema; + + async execute(data: DiscordLogPayload, job: Job) { + const res = await discordWebhook.send(config.webhooks.logging, data); + + if (!res.ok) { + throw new Error(`Failed to send Discord log: ${res.error || 'Unknown error'}`); + } + } +} diff --git a/apps/worker/src/tasks/index.ts b/apps/worker/src/tasks/index.ts index fc98ded3..c3c24606 100644 --- a/apps/worker/src/tasks/index.ts +++ b/apps/worker/src/tasks/index.ts @@ -1,7 +1,8 @@ import { ReviewActivityCheckTask } from './administrative/reviewActivityCheck.task'; import { BaseTask } from './base.task'; -import { AuditLogBtTask } from './buildteams/auditLogBt.task'; +import { SendBuildTeamWebhookTask } from './buildteams/sendWebhook.task'; import { SendDiscordDmTask } from './discord/sendDm.task'; +import { SendDiscordLogTask } from './discord/sendLog.task'; export const taskRegistry: Record = {}; @@ -11,4 +12,5 @@ function register(task: BaseTask) { register(new SendDiscordDmTask()); register(new ReviewActivityCheckTask()); -register(new AuditLogBtTask()); +register(new SendBuildTeamWebhookTask()); +register(new SendDiscordLogTask()); diff --git a/apps/worker/test/index.test.ts b/apps/worker/test/index.test.ts index d6003287..0c3447b9 100644 --- a/apps/worker/test/index.test.ts +++ b/apps/worker/test/index.test.ts @@ -9,15 +9,9 @@ async function trigger() { console.log('๐Ÿš€ Simulating Dashboard action: Queueing a Discord DM request...'); await testQueue.add( - 'SEND_DISCORD_DM', + 'SEND_DISCORD_LOG', { - discordIds: ['635411595253776385'], - content: { - title: 'Test Message from Worker', - emoji: 'INFORMATION', - body: 'This is a test message sent from the Worker to verify that the Discord DM functionality is working correctly.', - footer: 'This message was generated during testing.', - }, + content: 'test', }, { ...config.retryOptions, From 11d8b44b2985deb9ff6a60457cae7f49e5da681c Mon Sep 17 00:00:00 2001 From: Nudesuppe42 Date: Wed, 27 May 2026 16:55:00 +0200 Subject: [PATCH 13/31] feat(worker/tasks): :sparkles: Add purge claims and verifications tasks, and remind applications task --- apps/worker/src/lib/logger.ts | 2 +- apps/worker/src/main.ts | 2 + apps/worker/src/queue/base.worker.ts | 12 ++- .../tasks/administrative/purgeClaims.task.ts | 25 ++++++ .../administrative/purgeVerifications.task.ts | 21 +++++ .../administrative/remindApplications.task.ts | 77 +++++++++++++++++++ apps/worker/src/tasks/base.task.ts | 4 +- apps/worker/src/tasks/index.ts | 6 ++ apps/worker/test/index.test.ts | 13 ++-- 9 files changed, 152 insertions(+), 10 deletions(-) create mode 100644 apps/worker/src/tasks/administrative/purgeClaims.task.ts create mode 100644 apps/worker/src/tasks/administrative/purgeVerifications.task.ts create mode 100644 apps/worker/src/tasks/administrative/remindApplications.task.ts diff --git a/apps/worker/src/lib/logger.ts b/apps/worker/src/lib/logger.ts index ab669057..133df00b 100644 --- a/apps/worker/src/lib/logger.ts +++ b/apps/worker/src/lib/logger.ts @@ -56,7 +56,7 @@ const formatBlock = (content: string, meta: LogMeta): string => { const loggerConfig = { development: { - level: 'info', + level: 'debug', format: winston.format.combine( winston.format.errors({ stack: true }), winston.format.timestamp(), diff --git a/apps/worker/src/main.ts b/apps/worker/src/main.ts index bea57905..96d31094 100644 --- a/apps/worker/src/main.ts +++ b/apps/worker/src/main.ts @@ -29,6 +29,8 @@ async function bootstrap() { // Cron jobs cron.register('REVIEW_ACTIVITY_CHECK', {}, '0 0 * * *'); + cron.register('PURGE_VERIFICATIONS', {}, '0 0 * * *'); + cron.register('PURGE_CLAIMS', {}, '0 0 * * *'); await cron.start(); cronManagers.add(cron); diff --git a/apps/worker/src/queue/base.worker.ts b/apps/worker/src/queue/base.worker.ts index fad38613..1f3ddfc9 100644 --- a/apps/worker/src/queue/base.worker.ts +++ b/apps/worker/src/queue/base.worker.ts @@ -1,4 +1,4 @@ -import { Job, Worker } from 'bullmq'; +import { Job, Queue, Worker } from 'bullmq'; import { config } from 'src/lib/config'; import discordWebhook from 'src/lib/discordWebhook'; import { logger } from 'src/lib/logger'; @@ -9,7 +9,15 @@ import { taskRegistry } from '../tasks'; export class WorkerManager { private worker?: Worker; + private queue: Queue; + start(): this { + this.queue = new Queue(config.eventQueueName, { + connection: redis, + ...config.retryOptions, + ...config.removalOptions, + }); + const workerHandler = async (job: Job) => { const handler = taskRegistry[job.name]; @@ -23,7 +31,7 @@ export class WorkerManager { handler.setContext(taskLogger, prisma); const data = handler.validate(job.data); - await handler.execute(data, job); + await handler.execute(data, job, this.queue); }; this.worker = new Worker(config.eventQueueName, workerHandler, { diff --git a/apps/worker/src/tasks/administrative/purgeClaims.task.ts b/apps/worker/src/tasks/administrative/purgeClaims.task.ts new file mode 100644 index 00000000..c245adf4 --- /dev/null +++ b/apps/worker/src/tasks/administrative/purgeClaims.task.ts @@ -0,0 +1,25 @@ +import { Job } from 'bullmq'; +import { z } from 'zod'; +import { BaseTask } from '../base.task'; + +const purgeClaimsPayloadSchema = z.void(); +type purgeClaimsPayloadSchema = z.infer; + +export class PurgeClaimsTask extends BaseTask { + readonly name = 'PURGE_CLAIMS'; + readonly schema = purgeClaimsPayloadSchema; + + async execute(_data: purgeClaimsPayloadSchema, _job: Job) { + const emptyClaims = await this.prisma.claim.deleteMany({ + where: { center: null }, + }); + + this.logger.debug(`Purged ${emptyClaims.count} emtpy Claims`); + + const noRefClaims = await this.prisma.claim.deleteMany({ + where: { externalId: null, ownerId: null }, + }); + + this.logger.debug(`Purged ${noRefClaims.count} unreferenced Claims`); + } +} diff --git a/apps/worker/src/tasks/administrative/purgeVerifications.task.ts b/apps/worker/src/tasks/administrative/purgeVerifications.task.ts new file mode 100644 index 00000000..634f244a --- /dev/null +++ b/apps/worker/src/tasks/administrative/purgeVerifications.task.ts @@ -0,0 +1,21 @@ +import { Job } from 'bullmq'; +import { z } from 'zod'; +import { BaseTask } from '../base.task'; + +const purgeVerificationsPayloadSchema = z.void(); +type purgeVerificationsPayloadSchema = z.infer; + +export class PurgeVerificationsTask extends BaseTask { + readonly name = 'PURGE_VERIFICATIONS'; + readonly schema = purgeVerificationsPayloadSchema; + + async execute(_data: purgeVerificationsPayloadSchema, _job: Job) { + const codes = await this.prisma.minecraftVerifications.deleteMany({ + where: { + createdAt: { lte: new Date(Date.now() - 24 * 60 * 60 * 1000) }, + }, + }); + + this.logger.debug(`Purged ${codes.count} expired Verification Codes`); + } +} diff --git a/apps/worker/src/tasks/administrative/remindApplications.task.ts b/apps/worker/src/tasks/administrative/remindApplications.task.ts new file mode 100644 index 00000000..c96d2834 --- /dev/null +++ b/apps/worker/src/tasks/administrative/remindApplications.task.ts @@ -0,0 +1,77 @@ +import { ApplicationStatus } from '@repo/db'; +import { Job, Queue } from 'bullmq'; +import { DiscordBotEmojis } from 'src/lib/discordBot'; +import { z } from 'zod'; +import { BaseTask } from '../base.task'; + +const remindApplicationsPayloadSchema = z.void().or(z.object({})); +type remindApplicationsPayloadSchema = z.infer; + +export class RemindApplicationsTask extends BaseTask { + readonly name = 'REMIND_APPLICATIONS'; + readonly schema = remindApplicationsPayloadSchema; + + async execute(_data: remindApplicationsPayloadSchema, _job: Job, queue: Queue) { + const applications = await this.prisma.application.findMany({ + where: { + status: ApplicationStatus.SEND, + createdAt: { lte: new Date(Date.now() - 14 * 24 * 60 * 60 * 1000) }, + buildteam: { + allowApplications: true, + UserPermission: { + some: { permissionId: 'team.application.notify' }, + }, + }, + }, + orderBy: { createdAt: 'asc' }, + select: { + buildteam: { + select: { + name: true, + slug: true, + UserPermission: { + where: { permissionId: 'team.application.notify' }, + select: { user: { select: { discordId: true } } }, + }, + }, + }, + id: true, + createdAt: true, + user: { select: { discordId: true, minecraft: true } }, + trial: true, + }, + }); + const groupedApplications: any = {}; + + for (const application of applications) { + const bt = application.buildteam.slug; + + if (!groupedApplications[bt]) { + groupedApplications[bt] = []; + } + + groupedApplications[bt].push(application); + } + + Object.values(groupedApplications).forEach((apps: any) => { + const content = apps?.map( + (app) => + `- ${new Date(app.createdAt).toLocaleDateString('en-us', { + year: 'numeric', + month: 'numeric', + day: 'numeric', + })}: <@${app.user.discordId}> (${app.user.minecraft})`, + ); + + queue.add('SEND_DISCORD_DM', { + discordIds: apps[0].buildteam.UserPermission.map((u) => u.user.discordId), + content: { + title: `Application reminder for ${apps[0].buildteam.name}`, + emoji: DiscordBotEmojis.INFORMATION, + body: `Here is a list of Applications that are older than two weeks. Please review them:\n${content.join('\n')}`, + footer: `Automatically sent on ${new Date().toISOString().split('T')[0]}`, + }, + }); + }); + } +} diff --git a/apps/worker/src/tasks/base.task.ts b/apps/worker/src/tasks/base.task.ts index 9974efa7..5e221cef 100644 --- a/apps/worker/src/tasks/base.task.ts +++ b/apps/worker/src/tasks/base.task.ts @@ -1,5 +1,5 @@ import { PrismaClient } from '@repo/db'; -import { Job } from 'bullmq'; +import { Job, Queue } from 'bullmq'; import { Logger } from 'winston'; import { type ZodTypeAny, z } from 'zod'; @@ -18,5 +18,5 @@ export abstract class BaseTask { return this.schema.parse(data); } - abstract execute(data: z.infer, job: Job): Promise; + abstract execute(data: z.infer, job: Job, queue: Queue): Promise; } diff --git a/apps/worker/src/tasks/index.ts b/apps/worker/src/tasks/index.ts index c3c24606..ef2b0a67 100644 --- a/apps/worker/src/tasks/index.ts +++ b/apps/worker/src/tasks/index.ts @@ -1,3 +1,6 @@ +import { PurgeClaimsTask } from './administrative/purgeClaims.task'; +import { PurgeVerificationsTask } from './administrative/purgeVerifications.task'; +import { RemindApplicationsTask } from './administrative/remindApplications.task'; import { ReviewActivityCheckTask } from './administrative/reviewActivityCheck.task'; import { BaseTask } from './base.task'; import { SendBuildTeamWebhookTask } from './buildteams/sendWebhook.task'; @@ -14,3 +17,6 @@ register(new SendDiscordDmTask()); register(new ReviewActivityCheckTask()); register(new SendBuildTeamWebhookTask()); register(new SendDiscordLogTask()); +register(new PurgeClaimsTask()); +register(new PurgeVerificationsTask()); +register(new RemindApplicationsTask()); diff --git a/apps/worker/test/index.test.ts b/apps/worker/test/index.test.ts index 0c3447b9..44f6ba5d 100644 --- a/apps/worker/test/index.test.ts +++ b/apps/worker/test/index.test.ts @@ -3,18 +3,21 @@ import 'dotenv/config'; import { config } from '../src/lib/config'; import { redis } from '../src/lib/redis'; -const testQueue = new Queue(config.eventQueueName, { connection: redis, ...config.retryOptions }); +const testQueue = new Queue(config.eventQueueName, { + connection: redis, + ...config.retryOptions, + ...config.removalOptions, +}); async function trigger() { console.log('๐Ÿš€ Simulating Dashboard action: Queueing a Discord DM request...'); await testQueue.add( - 'SEND_DISCORD_LOG', - { - content: 'test', - }, + 'REMIND_APPLICATIONS', + {}, { ...config.retryOptions, + ...config.removalOptions, }, ); From 3fbf1a94130b62f2362c81001d4127fc3915c3f1 Mon Sep 17 00:00:00 2001 From: Nudesuppe42 Date: Wed, 27 May 2026 17:11:08 +0200 Subject: [PATCH 14/31] feat(worker): Add application reminder as weekly cron job --- apps/worker/src/main.ts | 1 + apps/worker/src/tasks/index.ts | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/apps/worker/src/main.ts b/apps/worker/src/main.ts index 96d31094..a24d136d 100644 --- a/apps/worker/src/main.ts +++ b/apps/worker/src/main.ts @@ -31,6 +31,7 @@ async function bootstrap() { cron.register('REVIEW_ACTIVITY_CHECK', {}, '0 0 * * *'); cron.register('PURGE_VERIFICATIONS', {}, '0 0 * * *'); cron.register('PURGE_CLAIMS', {}, '0 0 * * *'); + cron.register('REMIND_APPLICATIONS', {}, '0 0 * * 0'); await cron.start(); cronManagers.add(cron); diff --git a/apps/worker/src/tasks/index.ts b/apps/worker/src/tasks/index.ts index ef2b0a67..ec43d9b1 100644 --- a/apps/worker/src/tasks/index.ts +++ b/apps/worker/src/tasks/index.ts @@ -14,9 +14,9 @@ function register(task: BaseTask) { } register(new SendDiscordDmTask()); -register(new ReviewActivityCheckTask()); -register(new SendBuildTeamWebhookTask()); register(new SendDiscordLogTask()); +register(new SendBuildTeamWebhookTask()); +register(new ReviewActivityCheckTask()); register(new PurgeClaimsTask()); register(new PurgeVerificationsTask()); register(new RemindApplicationsTask()); From 7449f8c9232cb6938eadbdb3d20986ea12fe3ec5 Mon Sep 17 00:00:00 2001 From: Nudesuppe42 Date: Wed, 27 May 2026 17:16:04 +0200 Subject: [PATCH 15/31] feat(worker): Add Dockerfile and GitHub Actions workflow for worker deployment --- .github/workflows/worker.yml | 104 +++++++++++++++++++++++++++++++++++ README.md | 1 + apps/worker/Dockerfile | 35 ++++++++++++ 3 files changed, 140 insertions(+) create mode 100644 .github/workflows/worker.yml create mode 100644 apps/worker/Dockerfile diff --git a/.github/workflows/worker.yml b/.github/workflows/worker.yml new file mode 100644 index 00000000..babc4ec6 --- /dev/null +++ b/.github/workflows/worker.yml @@ -0,0 +1,104 @@ +name: Docker Publish Worker + +permissions: + contents: write + packages: write + +on: + push: + branches: [main] + tags: ['worker-v*.*.*'] + +jobs: + check: + runs-on: ubuntu-latest + outputs: + has_changes: ${{ steps.check_for_changes.outputs.has_changes }} + steps: + - uses: actions/checkout@v3 + with: + fetch-depth: 3 + - uses: actions/setup-node@v3 + with: + node-version: '22.14.0' + - name: Install turbo + run: npm install -g turbo@2.4.4 && npm install -g turbo-ignore + - name: Check for changes + id: check_for_changes + run: | + if [[ "${GITHUB_REF}" == refs/tags/worker-v* ]]; then + echo "has_changes=true" >> "$GITHUB_OUTPUT" + exit 0 + fi + + npx turbo-ignore worker --fallback=HEAD^1 && echo "has_changes=false" >> "$GITHUB_OUTPUT" || echo "has_changes=true" >> "$GITHUB_OUTPUT" + build: + runs-on: ubuntu-latest + needs: check + if: needs.check.outputs.has_changes == 'true' + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 3 + - name: Install turbo + run: npm install -g turbo@^2 + - name: Login to Docker + run: | + echo "${{ github.token }}" | docker login https://ghcr.io -u ${GITHUB_ACTOR} --password-stdin + - name: Setup Docker Buildx + uses: docker/setup-buildx-action@v3 + - name: Resolve release tags + id: release_meta + run: | + if [[ "${GITHUB_REF}" == refs/tags/worker-v* ]]; then + VERSION="${GITHUB_REF_NAME#worker-v}" + if [[ ! "${VERSION}" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then + echo "Invalid release tag format: ${GITHUB_REF_NAME}. Expected worker-vX.Y.Z" + exit 1 + fi + + IFS='.' read -r MAJOR MINOR PATCH <<< "${VERSION}" + echo "is_release=true" >> "$GITHUB_OUTPUT" + echo "version=${VERSION}" >> "$GITHUB_OUTPUT" + echo "major=${MAJOR}" >> "$GITHUB_OUTPUT" + echo "minor=${MINOR}" >> "$GITHUB_OUTPUT" + exit 0 + fi + + echo "is_release=false" >> "$GITHUB_OUTPUT" + - name: Build and push Docker image + env: + DOCKER_BUILDKIT: 1 + IS_RELEASE: ${{ steps.release_meta.outputs.is_release }} + RELEASE_VERSION: ${{ steps.release_meta.outputs.version }} + RELEASE_MAJOR: ${{ steps.release_meta.outputs.major }} + RELEASE_MINOR: ${{ steps.release_meta.outputs.minor }} + run: | + IMAGE="ghcr.io/buildtheearth/website-worker" + TAGS=( + --tag "${IMAGE}:sha-$(git rev-parse --short=12 HEAD)" + ) + + if [[ "${IS_RELEASE}" == "true" ]]; then + TAGS+=( + --tag "${IMAGE}:${RELEASE_VERSION}" + --tag "${IMAGE}:${RELEASE_MAJOR}.${RELEASE_MINOR}" + --tag "${IMAGE}:${RELEASE_MAJOR}" + --tag "${IMAGE}:latest" + ) + fi + + docker buildx build . \ + --file apps/worker/Dockerfile \ + --label org.opencontainers.image.source="${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}" \ + --label org.opencontainers.image.revision="${GITHUB_SHA}" \ + --label org.opencontainers.image.version="${RELEASE_VERSION:-sha-$(git rev-parse --short=12 HEAD)}" \ + "${TAGS[@]}" \ + --push + - name: Publish GitHub release + if: steps.release_meta.outputs.is_release == 'true' + uses: softprops/action-gh-release@v2 + with: + tag_name: ${{ github.ref_name }} + name: worker-v${{ steps.release_meta.outputs.version }} + generate_release_notes: true diff --git a/README.md b/README.md index 537b12dd..a3888c9e 100644 --- a/README.md +++ b/README.md @@ -22,6 +22,7 @@ This repository contains the following apps and other shared packages: | apps/frontend | BuildTheEarth Website frontend | Next.js, TypeScript | https://buildtheearth.net | | apps/api | BuildTheEarth API and Website backend | Express.js, TypeScript | https://api.buildtheearth.net | | apps/dashboard | BuildTheEarth Dashboard | Next.js, TypeScript | https://my.buildtheearth.net | +| apps/worker | Queue Worker for async tasks | BullMQ, TypeScript | -/- | | packages/typescript-config | Shared tsconfig for all apps | TypeScript, JSON | -/- | | packages/prettier-config | Shared Prettier config for all apps | Prettier, JSON | -/- | | packages/db | Shared Prisma client and schema | Prisma.js | -/- | diff --git a/apps/worker/Dockerfile b/apps/worker/Dockerfile new file mode 100644 index 00000000..75a49a20 --- /dev/null +++ b/apps/worker/Dockerfile @@ -0,0 +1,35 @@ +FROM node:22-alpine AS base + +FROM base AS builder +RUN apk update +RUN apk add --no-cache libc6-compat +WORKDIR /app + +# Run turbo +RUN yarn global add turbo@^2.1.1 +COPY . . +RUN turbo prune worker --docker + +# Add lockfile and package.json +FROM base AS installer +RUN apk update +RUN apk add --no-cache libc6-compat +WORKDIR /app + +# Install dependencies +COPY --from=builder /app/out/json/ . +# Enable corepack to use the correct Yarn version +RUN corepack enable +RUN corepack prepare yarn@4.9.1 --activate +RUN yarn install + +# Build the project +COPY --from=builder /app/out/full/ . +RUN yarn turbo run build --filter=worker... + +FROM base AS runner +WORKDIR /app + +COPY --from=installer /app . + +CMD ["node", "apps/worker/dist/main.js"] From 0020cf84391304b65758018f345d6ccb3f5c94d0 Mon Sep 17 00:00:00 2001 From: Nudesuppe42 Date: Wed, 27 May 2026 17:33:19 +0200 Subject: [PATCH 16/31] fix(worker/tasks): :bug: Switch to unknown type from void --- apps/worker/src/tasks/administrative/purgeClaims.task.ts | 2 +- apps/worker/src/tasks/administrative/purgeVerifications.task.ts | 2 +- apps/worker/src/tasks/administrative/remindApplications.task.ts | 2 +- .../worker/src/tasks/administrative/reviewActivityCheck.task.ts | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/apps/worker/src/tasks/administrative/purgeClaims.task.ts b/apps/worker/src/tasks/administrative/purgeClaims.task.ts index c245adf4..2d5576e2 100644 --- a/apps/worker/src/tasks/administrative/purgeClaims.task.ts +++ b/apps/worker/src/tasks/administrative/purgeClaims.task.ts @@ -2,7 +2,7 @@ import { Job } from 'bullmq'; import { z } from 'zod'; import { BaseTask } from '../base.task'; -const purgeClaimsPayloadSchema = z.void(); +const purgeClaimsPayloadSchema = z.unknown(); type purgeClaimsPayloadSchema = z.infer; export class PurgeClaimsTask extends BaseTask { diff --git a/apps/worker/src/tasks/administrative/purgeVerifications.task.ts b/apps/worker/src/tasks/administrative/purgeVerifications.task.ts index 634f244a..8ceb22b6 100644 --- a/apps/worker/src/tasks/administrative/purgeVerifications.task.ts +++ b/apps/worker/src/tasks/administrative/purgeVerifications.task.ts @@ -2,7 +2,7 @@ import { Job } from 'bullmq'; import { z } from 'zod'; import { BaseTask } from '../base.task'; -const purgeVerificationsPayloadSchema = z.void(); +const purgeVerificationsPayloadSchema = z.unknown(); type purgeVerificationsPayloadSchema = z.infer; export class PurgeVerificationsTask extends BaseTask { diff --git a/apps/worker/src/tasks/administrative/remindApplications.task.ts b/apps/worker/src/tasks/administrative/remindApplications.task.ts index c96d2834..54ef7fb1 100644 --- a/apps/worker/src/tasks/administrative/remindApplications.task.ts +++ b/apps/worker/src/tasks/administrative/remindApplications.task.ts @@ -4,7 +4,7 @@ import { DiscordBotEmojis } from 'src/lib/discordBot'; import { z } from 'zod'; import { BaseTask } from '../base.task'; -const remindApplicationsPayloadSchema = z.void().or(z.object({})); +const remindApplicationsPayloadSchema = z.unknown(); type remindApplicationsPayloadSchema = z.infer; export class RemindApplicationsTask extends BaseTask { diff --git a/apps/worker/src/tasks/administrative/reviewActivityCheck.task.ts b/apps/worker/src/tasks/administrative/reviewActivityCheck.task.ts index 7f9d5ebf..2921e75e 100644 --- a/apps/worker/src/tasks/administrative/reviewActivityCheck.task.ts +++ b/apps/worker/src/tasks/administrative/reviewActivityCheck.task.ts @@ -8,7 +8,7 @@ import { Logger } from 'winston'; import { z } from 'zod'; import { BaseTask } from '../base.task'; -const reviewActivityCheckPayloadSchema = z.void(); +const reviewActivityCheckPayloadSchema = z.unknown(); type reviewActivityCheckPayloadSchema = z.infer; type ReviewActivityData = { From 34543ff1e42596d7a6763dc3251ff1a611b0fdab Mon Sep 17 00:00:00 2001 From: Nudesuppe42 Date: Wed, 27 May 2026 17:35:16 +0200 Subject: [PATCH 17/31] fix(worker/tasks): :bug: Use nativeEnum instead of enum for zod --- apps/worker/src/lib/discordBot.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/worker/src/lib/discordBot.ts b/apps/worker/src/lib/discordBot.ts index ffcd890a..fff8325e 100644 --- a/apps/worker/src/lib/discordBot.ts +++ b/apps/worker/src/lib/discordBot.ts @@ -38,7 +38,7 @@ export enum DiscordBotEmojis { export const discordBotMessageMessageSchema = z.object({ title: z.string(), - emoji: z.enum(DiscordBotEmojis), + emoji: z.nativeEnum(DiscordBotEmojis), body: z.string(), footer: z.string().optional(), }); From 4c75e9bdf89eef1041dde9fc86008311e25f8ddf Mon Sep 17 00:00:00 2001 From: Nudesuppe42 Date: Wed, 27 May 2026 17:37:13 +0200 Subject: [PATCH 18/31] fix(worker): :bug: Update zod import to named import for consistency --- apps/worker/src/lib/discordBot.ts | 2 +- apps/worker/src/tasks/buildteams/sendWebhook.task.ts | 2 +- apps/worker/src/tasks/discord/sendLog.task.ts | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/apps/worker/src/lib/discordBot.ts b/apps/worker/src/lib/discordBot.ts index fff8325e..e4efccf8 100644 --- a/apps/worker/src/lib/discordBot.ts +++ b/apps/worker/src/lib/discordBot.ts @@ -1,4 +1,4 @@ -import z from 'zod'; +import { z } from 'zod'; export type DiscordDmResult = { success: string[]; diff --git a/apps/worker/src/tasks/buildteams/sendWebhook.task.ts b/apps/worker/src/tasks/buildteams/sendWebhook.task.ts index 342e2f04..26b2dd76 100644 --- a/apps/worker/src/tasks/buildteams/sendWebhook.task.ts +++ b/apps/worker/src/tasks/buildteams/sendWebhook.task.ts @@ -20,7 +20,7 @@ const webhookBuildTeamSchema = z.union([ ]); const auditLogBtPayloadSchema = z.object({ - type: z.enum(AuditLogBuildTeamType), + type: z.nativeEnum(AuditLogBuildTeamType), data: z.unknown().optional(), destination: z.array(webhookBuildTeamSchema), }); diff --git a/apps/worker/src/tasks/discord/sendLog.task.ts b/apps/worker/src/tasks/discord/sendLog.task.ts index 5f26f578..6cea3f71 100644 --- a/apps/worker/src/tasks/discord/sendLog.task.ts +++ b/apps/worker/src/tasks/discord/sendLog.task.ts @@ -1,7 +1,7 @@ import { Job } from 'bullmq'; import { config } from 'src/lib/config'; import discordWebhook from 'src/lib/discordWebhook'; -import z from 'zod'; +import { z } from 'zod'; import { BaseTask } from '../base.task'; const discordLogPayloadSchema = z.any(); From 5e90876155de8e386b2a5db3f382e3ac4711c443 Mon Sep 17 00:00:00 2001 From: Nudesuppe42 Date: Wed, 27 May 2026 17:38:07 +0200 Subject: [PATCH 19/31] fix(worker/tasks): :bug: Use Promise.all instead of single promises --- .../administrative/remindApplications.task.ts | 40 ++++++++++--------- 1 file changed, 21 insertions(+), 19 deletions(-) diff --git a/apps/worker/src/tasks/administrative/remindApplications.task.ts b/apps/worker/src/tasks/administrative/remindApplications.task.ts index 54ef7fb1..c625b3dd 100644 --- a/apps/worker/src/tasks/administrative/remindApplications.task.ts +++ b/apps/worker/src/tasks/administrative/remindApplications.task.ts @@ -53,25 +53,27 @@ export class RemindApplicationsTask extends BaseTask { - const content = apps?.map( - (app) => - `- ${new Date(app.createdAt).toLocaleDateString('en-us', { - year: 'numeric', - month: 'numeric', - day: 'numeric', - })}: <@${app.user.discordId}> (${app.user.minecraft})`, - ); + await Promise.all( + Object.values(groupedApplications).map((apps: any) => { + const content = apps?.map( + (app) => + `- ${new Date(app.createdAt).toLocaleDateString('en-us', { + year: 'numeric', + month: 'numeric', + day: 'numeric', + })}: <@${app.user.discordId}> (${app.user.minecraft})`, + ); - queue.add('SEND_DISCORD_DM', { - discordIds: apps[0].buildteam.UserPermission.map((u) => u.user.discordId), - content: { - title: `Application reminder for ${apps[0].buildteam.name}`, - emoji: DiscordBotEmojis.INFORMATION, - body: `Here is a list of Applications that are older than two weeks. Please review them:\n${content.join('\n')}`, - footer: `Automatically sent on ${new Date().toISOString().split('T')[0]}`, - }, - }); - }); + return queue.add('SEND_DISCORD_DM', { + discordIds: apps[0].buildteam.UserPermission.map((u) => u.user.discordId), + content: { + title: `Application reminder for ${apps[0].buildteam.name}`, + emoji: DiscordBotEmojis.INFORMATION, + body: `Here is a list of Applications that are older than two weeks. Please review them:\n${content.join('\n')}`, + footer: `Automatically sent on ${new Date().toISOString().split('T')[0]}`, + }, + }); + }), + ); } } From 72ca7452c29dd6c14e63116efa0e51cb8539ccd4 Mon Sep 17 00:00:00 2001 From: Nudesuppe42 Date: Wed, 27 May 2026 17:39:25 +0200 Subject: [PATCH 20/31] fix(worker): :bug: Typo --- apps/worker/README.md | 2 +- apps/worker/src/tasks/administrative/purgeClaims.task.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/worker/README.md b/apps/worker/README.md index 1507f136..ae96221a 100644 --- a/apps/worker/README.md +++ b/apps/worker/README.md @@ -5,7 +5,7 @@ # Website Worker -_Independet worker for handling events._ +_Independent worker for handling events._ ![official](https://go.buildtheearth.net/official-shield) [![chat](https://img.shields.io/discord/706317564904472627.svg?color=768AD4&label=discord&logo=https%3A%2F%2Fdiscordapp.com%2Fassets%2F8c9701b98ad4372b58f13fd9f65f966e.svg)](https://discord.gg/buildtheearth) diff --git a/apps/worker/src/tasks/administrative/purgeClaims.task.ts b/apps/worker/src/tasks/administrative/purgeClaims.task.ts index 2d5576e2..a98db3f1 100644 --- a/apps/worker/src/tasks/administrative/purgeClaims.task.ts +++ b/apps/worker/src/tasks/administrative/purgeClaims.task.ts @@ -14,7 +14,7 @@ export class PurgeClaimsTask extends BaseTask { where: { center: null }, }); - this.logger.debug(`Purged ${emptyClaims.count} emtpy Claims`); + this.logger.debug(`Purged ${emptyClaims.count} empty Claims`); const noRefClaims = await this.prisma.claim.deleteMany({ where: { externalId: null, ownerId: null }, From 7b8ccb2f35605a078ef99d0de83dc5031b9b278f Mon Sep 17 00:00:00 2001 From: Nudesuppe42 Date: Wed, 27 May 2026 17:40:30 +0200 Subject: [PATCH 21/31] fix(worker/Dockerfile): :bug: Use corepack/yarn 4 for turbo prune --- apps/worker/Dockerfile | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/apps/worker/Dockerfile b/apps/worker/Dockerfile index 75a49a20..083fb171 100644 --- a/apps/worker/Dockerfile +++ b/apps/worker/Dockerfile @@ -6,9 +6,10 @@ RUN apk add --no-cache libc6-compat WORKDIR /app # Run turbo -RUN yarn global add turbo@^2.1.1 +RUN corepack enable +RUN corepack prepare yarn@4.9.1 --activate COPY . . -RUN turbo prune worker --docker +RUN yarn dlx turbo@2.1.1 prune worker --docker # Add lockfile and package.json FROM base AS installer From 97afb5de2edc877b2af04483bd578d12e280a73a Mon Sep 17 00:00:00 2001 From: Nudesuppe42 Date: Wed, 27 May 2026 17:42:50 +0200 Subject: [PATCH 22/31] fix(worker): :bug: Use relative imports --- apps/worker/src/queue/base.worker.ts | 10 +++++----- apps/worker/src/queue/cron.manager.ts | 6 +++--- .../tasks/administrative/remindApplications.task.ts | 2 +- .../tasks/administrative/reviewActivityCheck.task.ts | 9 +++------ apps/worker/src/tasks/buildteams/sendWebhook.task.ts | 4 +--- apps/worker/src/tasks/discord/sendDm.task.ts | 2 +- apps/worker/src/tasks/discord/sendLog.task.ts | 4 ++-- apps/worker/src/util/reviewActivity.ts | 2 +- apps/worker/tsconfig.json | 1 - 9 files changed, 17 insertions(+), 23 deletions(-) diff --git a/apps/worker/src/queue/base.worker.ts b/apps/worker/src/queue/base.worker.ts index 1f3ddfc9..dcc41d96 100644 --- a/apps/worker/src/queue/base.worker.ts +++ b/apps/worker/src/queue/base.worker.ts @@ -1,9 +1,9 @@ import { Job, Queue, Worker } from 'bullmq'; -import { config } from 'src/lib/config'; -import discordWebhook from 'src/lib/discordWebhook'; -import { logger } from 'src/lib/logger'; -import prisma from 'src/lib/prisma'; -import { redis } from 'src/lib/redis'; +import { config } from '../../src/lib/config'; +import discordWebhook from '../../src/lib/discordWebhook'; +import { logger } from '../../src/lib/logger'; +import prisma from '../../src/lib/prisma'; +import { redis } from '../../src/lib/redis'; import { taskRegistry } from '../tasks'; export class WorkerManager { diff --git a/apps/worker/src/queue/cron.manager.ts b/apps/worker/src/queue/cron.manager.ts index 959826fd..6e1dcc3e 100644 --- a/apps/worker/src/queue/cron.manager.ts +++ b/apps/worker/src/queue/cron.manager.ts @@ -1,7 +1,7 @@ import { Queue } from 'bullmq'; -import { config } from 'src/lib/config'; -import { logger } from 'src/lib/logger'; -import { redis } from 'src/lib/redis'; +import { config } from '../../src/lib/config'; +import { logger } from '../../src/lib/logger'; +import { redis } from '../../src/lib/redis'; type CronEntry = { name: string; diff --git a/apps/worker/src/tasks/administrative/remindApplications.task.ts b/apps/worker/src/tasks/administrative/remindApplications.task.ts index c625b3dd..7b6c1f75 100644 --- a/apps/worker/src/tasks/administrative/remindApplications.task.ts +++ b/apps/worker/src/tasks/administrative/remindApplications.task.ts @@ -1,7 +1,7 @@ import { ApplicationStatus } from '@repo/db'; import { Job, Queue } from 'bullmq'; -import { DiscordBotEmojis } from 'src/lib/discordBot'; import { z } from 'zod'; +import { DiscordBotEmojis } from '../../../src/lib/discordBot'; import { BaseTask } from '../base.task'; const remindApplicationsPayloadSchema = z.unknown(); diff --git a/apps/worker/src/tasks/administrative/reviewActivityCheck.task.ts b/apps/worker/src/tasks/administrative/reviewActivityCheck.task.ts index 2921e75e..f570a825 100644 --- a/apps/worker/src/tasks/administrative/reviewActivityCheck.task.ts +++ b/apps/worker/src/tasks/administrative/reviewActivityCheck.task.ts @@ -1,11 +1,8 @@ -import { PrismaClient } from '@repo/db'; import { Job } from 'bullmq'; -import { config } from 'src/lib/config'; -import { sendDiscordDm } from 'src/lib/discordBot'; -import discordWebhook from 'src/lib/discordWebhook'; -import { getReviewActivityScore } from 'src/util/reviewActivity'; -import { Logger } from 'winston'; import { z } from 'zod'; +import { config } from '../../../src/lib/config'; +import discordWebhook from '../../../src/lib/discordWebhook'; +import { getReviewActivityScore } from '../../../src/util/reviewActivity'; import { BaseTask } from '../base.task'; const reviewActivityCheckPayloadSchema = z.unknown(); diff --git a/apps/worker/src/tasks/buildteams/sendWebhook.task.ts b/apps/worker/src/tasks/buildteams/sendWebhook.task.ts index 26b2dd76..2ce21882 100644 --- a/apps/worker/src/tasks/buildteams/sendWebhook.task.ts +++ b/apps/worker/src/tasks/buildteams/sendWebhook.task.ts @@ -1,8 +1,6 @@ -import { PrismaClient } from '@repo/db'; import { Job } from 'bullmq'; -import { BuildTeamWebhook, WebhookBuildTeam } from 'src/lib/buildteamWebhook'; -import { Logger } from 'winston'; import { z } from 'zod'; +import { BuildTeamWebhook, WebhookBuildTeam } from '../../../src/lib/buildteamWebhook'; import { BaseTask } from '../base.task'; enum AuditLogBuildTeamType { diff --git a/apps/worker/src/tasks/discord/sendDm.task.ts b/apps/worker/src/tasks/discord/sendDm.task.ts index 07a3739a..d98dc821 100644 --- a/apps/worker/src/tasks/discord/sendDm.task.ts +++ b/apps/worker/src/tasks/discord/sendDm.task.ts @@ -1,6 +1,6 @@ import { Job } from 'bullmq'; -import { discordBotMessageMessageSchema, sendDiscordDm } from 'src/lib/discordBot'; import { z } from 'zod'; +import { discordBotMessageMessageSchema, sendDiscordDm } from '../../../src/lib/discordBot'; import { BaseTask } from '../base.task'; const discordDmPayloadSchema = z diff --git a/apps/worker/src/tasks/discord/sendLog.task.ts b/apps/worker/src/tasks/discord/sendLog.task.ts index 6cea3f71..ca19cb48 100644 --- a/apps/worker/src/tasks/discord/sendLog.task.ts +++ b/apps/worker/src/tasks/discord/sendLog.task.ts @@ -1,7 +1,7 @@ import { Job } from 'bullmq'; -import { config } from 'src/lib/config'; -import discordWebhook from 'src/lib/discordWebhook'; import { z } from 'zod'; +import { config } from '../../../src/lib/config'; +import discordWebhook from '../../../src/lib/discordWebhook'; import { BaseTask } from '../base.task'; const discordLogPayloadSchema = z.any(); diff --git a/apps/worker/src/util/reviewActivity.ts b/apps/worker/src/util/reviewActivity.ts index 93c90663..451c7a13 100644 --- a/apps/worker/src/util/reviewActivity.ts +++ b/apps/worker/src/util/reviewActivity.ts @@ -1,5 +1,5 @@ import { ApplicationStatus } from '@repo/db'; -import prisma from 'src/lib/prisma'; +import prisma from '../../src/lib/prisma'; /** * Calculate Review Activity Score for a Build Team diff --git a/apps/worker/tsconfig.json b/apps/worker/tsconfig.json index 9c704192..cf9a9fb0 100644 --- a/apps/worker/tsconfig.json +++ b/apps/worker/tsconfig.json @@ -12,7 +12,6 @@ "target": "ES2023", "sourceMap": true, "outDir": "./dist", - "baseUrl": "./", "rootDir": "./src", "incremental": true, "skipLibCheck": true, From d7adf894a5a106489b8b8a4bab6550e71288aede Mon Sep 17 00:00:00 2001 From: Nudesuppe42 Date: Wed, 27 May 2026 17:43:07 +0200 Subject: [PATCH 23/31] style(worker): :art: Prettier --- apps/worker/src/lib/discordBot.ts | 4 +++- .../src/tasks/administrative/remindApplications.task.ts | 4 +++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/apps/worker/src/lib/discordBot.ts b/apps/worker/src/lib/discordBot.ts index e4efccf8..a9b477de 100644 --- a/apps/worker/src/lib/discordBot.ts +++ b/apps/worker/src/lib/discordBot.ts @@ -51,7 +51,9 @@ export async function sendDiscordDm( const content = typeof message === 'string' ? message - : `## ${DiscordBotEmojisRaw[message.emoji]} ${message.title}\n\n${message.body}${message.footer ? `\n\n-# ${message.footer}` : ''}`; + : `## ${DiscordBotEmojisRaw[message.emoji]} ${message.title}\n\n${message.body}${ + message.footer ? `\n\n-# ${message.footer}` : '' + }`; const res = await fetch(process.env.DISCORD_BOT_API_URL + '/api/v1/website/message/blank', { method: 'POST', diff --git a/apps/worker/src/tasks/administrative/remindApplications.task.ts b/apps/worker/src/tasks/administrative/remindApplications.task.ts index 7b6c1f75..5db03035 100644 --- a/apps/worker/src/tasks/administrative/remindApplications.task.ts +++ b/apps/worker/src/tasks/administrative/remindApplications.task.ts @@ -69,7 +69,9 @@ export class RemindApplicationsTask extends BaseTask Date: Wed, 27 May 2026 17:57:38 +0200 Subject: [PATCH 24/31] fix(worker/tasks): :bug: Final bug fixes --- .../administrative/remindApplications.task.ts | 51 +++++++++++-------- .../reviewActivityCheck.task.ts | 2 +- 2 files changed, 31 insertions(+), 22 deletions(-) diff --git a/apps/worker/src/tasks/administrative/remindApplications.task.ts b/apps/worker/src/tasks/administrative/remindApplications.task.ts index 5db03035..f471ea9f 100644 --- a/apps/worker/src/tasks/administrative/remindApplications.task.ts +++ b/apps/worker/src/tasks/administrative/remindApplications.task.ts @@ -54,28 +54,37 @@ export class RemindApplicationsTask extends BaseTask { - const content = apps?.map( - (app) => - `- ${new Date(app.createdAt).toLocaleDateString('en-us', { - year: 'numeric', - month: 'numeric', - day: 'numeric', - })}: <@${app.user.discordId}> (${app.user.minecraft})`, - ); + Object.values(groupedApplications) + .map((apps: any) => { + const content = apps?.map( + (app) => + `- ${new Date(app.createdAt).toLocaleDateString('en-us', { + year: 'numeric', + month: 'numeric', + day: 'numeric', + })}: <@${app.user.discordId}> (${app.user.minecraft})`, + ); - return queue.add('SEND_DISCORD_DM', { - discordIds: apps[0].buildteam.UserPermission.map((u) => u.user.discordId), - content: { - title: `Application reminder for ${apps[0].buildteam.name}`, - emoji: DiscordBotEmojis.INFORMATION, - body: `Here is a list of Applications that are older than two weeks. Please review them:\n${content.join( - '\n', - )}`, - footer: `Automatically sent on ${new Date().toISOString().split('T')[0]}`, - }, - }); - }), + const discordIds = apps[0].buildteam.UserPermission.map((u) => u.user.discordId).filter( + (discordId): discordId is string => typeof discordId === 'string' && discordId.trim().length > 0, + ); + if (discordIds.length === 0) { + return null; + } + + return queue.add('SEND_DISCORD_DM', { + discordIds, + content: { + title: `Application reminder for ${apps[0].buildteam.name}`, + emoji: DiscordBotEmojis.INFORMATION, + body: `Here is a list of Applications that are older than two weeks. Please review them:\n${content.join( + '\n', + )}`, + footer: `Automatically sent on ${new Date().toISOString().split('T')[0]}`, + }, + }); + }) + .filter((job): job is NonNullable => job !== null), ); } } diff --git a/apps/worker/src/tasks/administrative/reviewActivityCheck.task.ts b/apps/worker/src/tasks/administrative/reviewActivityCheck.task.ts index f570a825..614d632e 100644 --- a/apps/worker/src/tasks/administrative/reviewActivityCheck.task.ts +++ b/apps/worker/src/tasks/administrative/reviewActivityCheck.task.ts @@ -38,7 +38,7 @@ export class ReviewActivityCheckTask extends BaseTask Date: Wed, 27 May 2026 18:08:12 +0200 Subject: [PATCH 25/31] fix(worker): Imports again Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- apps/worker/src/queue/base.worker.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/apps/worker/src/queue/base.worker.ts b/apps/worker/src/queue/base.worker.ts index dcc41d96..1f99dbae 100644 --- a/apps/worker/src/queue/base.worker.ts +++ b/apps/worker/src/queue/base.worker.ts @@ -1,9 +1,9 @@ import { Job, Queue, Worker } from 'bullmq'; -import { config } from '../../src/lib/config'; -import discordWebhook from '../../src/lib/discordWebhook'; -import { logger } from '../../src/lib/logger'; -import prisma from '../../src/lib/prisma'; -import { redis } from '../../src/lib/redis'; +import { config } from '../lib/config'; +import discordWebhook from '../lib/discordWebhook'; +import { logger } from '../lib/logger'; +import prisma from '../lib/prisma'; +import { redis } from '../lib/redis'; import { taskRegistry } from '../tasks'; export class WorkerManager { From ddbecace910fcb499e992f5e7bbccda12693a7b0 Mon Sep 17 00:00:00 2001 From: Nudesuppe42 <67996941+Nudelsuppe42@users.noreply.github.com> Date: Wed, 27 May 2026 18:08:27 +0200 Subject: [PATCH 26/31] fix(worker): Imports again Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- apps/worker/src/queue/cron.manager.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/apps/worker/src/queue/cron.manager.ts b/apps/worker/src/queue/cron.manager.ts index 6e1dcc3e..e86597ac 100644 --- a/apps/worker/src/queue/cron.manager.ts +++ b/apps/worker/src/queue/cron.manager.ts @@ -1,7 +1,7 @@ import { Queue } from 'bullmq'; -import { config } from '../../src/lib/config'; -import { logger } from '../../src/lib/logger'; -import { redis } from '../../src/lib/redis'; +import { config } from '../lib/config'; +import { logger } from '../lib/logger'; +import { redis } from '../lib/redis'; type CronEntry = { name: string; From 8dbc010f86aa89293a40fcb88f4cd2d7894a0342 Mon Sep 17 00:00:00 2001 From: Nudesuppe42 <67996941+Nudelsuppe42@users.noreply.github.com> Date: Wed, 27 May 2026 18:10:25 +0200 Subject: [PATCH 27/31] fix(worker): Imports again Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- .../src/tasks/administrative/remindApplications.task.ts | 2 +- .../src/tasks/administrative/reviewActivityCheck.task.ts | 6 +++--- apps/worker/src/tasks/buildteams/sendWebhook.task.ts | 2 +- apps/worker/src/tasks/discord/sendDm.task.ts | 2 +- apps/worker/src/tasks/discord/sendLog.task.ts | 4 ++-- apps/worker/src/util/reviewActivity.ts | 2 +- 6 files changed, 9 insertions(+), 9 deletions(-) diff --git a/apps/worker/src/tasks/administrative/remindApplications.task.ts b/apps/worker/src/tasks/administrative/remindApplications.task.ts index f471ea9f..0f64a743 100644 --- a/apps/worker/src/tasks/administrative/remindApplications.task.ts +++ b/apps/worker/src/tasks/administrative/remindApplications.task.ts @@ -1,7 +1,7 @@ import { ApplicationStatus } from '@repo/db'; import { Job, Queue } from 'bullmq'; import { z } from 'zod'; -import { DiscordBotEmojis } from '../../../src/lib/discordBot'; +import { DiscordBotEmojis } from '../../lib/discordBot'; import { BaseTask } from '../base.task'; const remindApplicationsPayloadSchema = z.unknown(); diff --git a/apps/worker/src/tasks/administrative/reviewActivityCheck.task.ts b/apps/worker/src/tasks/administrative/reviewActivityCheck.task.ts index 614d632e..0d327cad 100644 --- a/apps/worker/src/tasks/administrative/reviewActivityCheck.task.ts +++ b/apps/worker/src/tasks/administrative/reviewActivityCheck.task.ts @@ -1,8 +1,8 @@ import { Job } from 'bullmq'; import { z } from 'zod'; -import { config } from '../../../src/lib/config'; -import discordWebhook from '../../../src/lib/discordWebhook'; -import { getReviewActivityScore } from '../../../src/util/reviewActivity'; +import { config } from '../../lib/config'; +import discordWebhook from '../../lib/discordWebhook'; +import { getReviewActivityScore } from '../../util/reviewActivity'; import { BaseTask } from '../base.task'; const reviewActivityCheckPayloadSchema = z.unknown(); diff --git a/apps/worker/src/tasks/buildteams/sendWebhook.task.ts b/apps/worker/src/tasks/buildteams/sendWebhook.task.ts index 2ce21882..17ce426d 100644 --- a/apps/worker/src/tasks/buildteams/sendWebhook.task.ts +++ b/apps/worker/src/tasks/buildteams/sendWebhook.task.ts @@ -1,6 +1,6 @@ import { Job } from 'bullmq'; import { z } from 'zod'; -import { BuildTeamWebhook, WebhookBuildTeam } from '../../../src/lib/buildteamWebhook'; +import { BuildTeamWebhook, WebhookBuildTeam } from '../../lib/buildteamWebhook'; import { BaseTask } from '../base.task'; enum AuditLogBuildTeamType { diff --git a/apps/worker/src/tasks/discord/sendDm.task.ts b/apps/worker/src/tasks/discord/sendDm.task.ts index d98dc821..77963cbf 100644 --- a/apps/worker/src/tasks/discord/sendDm.task.ts +++ b/apps/worker/src/tasks/discord/sendDm.task.ts @@ -1,6 +1,6 @@ import { Job } from 'bullmq'; import { z } from 'zod'; -import { discordBotMessageMessageSchema, sendDiscordDm } from '../../../src/lib/discordBot'; +import { discordBotMessageMessageSchema, sendDiscordDm } from '../../lib/discordBot'; import { BaseTask } from '../base.task'; const discordDmPayloadSchema = z diff --git a/apps/worker/src/tasks/discord/sendLog.task.ts b/apps/worker/src/tasks/discord/sendLog.task.ts index ca19cb48..d6edc7f5 100644 --- a/apps/worker/src/tasks/discord/sendLog.task.ts +++ b/apps/worker/src/tasks/discord/sendLog.task.ts @@ -1,7 +1,7 @@ import { Job } from 'bullmq'; import { z } from 'zod'; -import { config } from '../../../src/lib/config'; -import discordWebhook from '../../../src/lib/discordWebhook'; +import { config } from '../../lib/config'; +import discordWebhook from '../../lib/discordWebhook'; import { BaseTask } from '../base.task'; const discordLogPayloadSchema = z.any(); diff --git a/apps/worker/src/util/reviewActivity.ts b/apps/worker/src/util/reviewActivity.ts index 451c7a13..f5d4c641 100644 --- a/apps/worker/src/util/reviewActivity.ts +++ b/apps/worker/src/util/reviewActivity.ts @@ -1,5 +1,5 @@ import { ApplicationStatus } from '@repo/db'; -import prisma from '../../src/lib/prisma'; +import prisma from '../lib/prisma'; /** * Calculate Review Activity Score for a Build Team From a675a5ed954f3de5aadd51a4ba84d39bc2b403ab Mon Sep 17 00:00:00 2001 From: Nudesuppe42 Date: Wed, 27 May 2026 20:34:22 +0200 Subject: [PATCH 28/31] fix(worker/tasks): Update validation for Discord DM payload schema --- apps/worker/src/tasks/discord/sendDm.task.ts | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/apps/worker/src/tasks/discord/sendDm.task.ts b/apps/worker/src/tasks/discord/sendDm.task.ts index 77963cbf..c39a0984 100644 --- a/apps/worker/src/tasks/discord/sendDm.task.ts +++ b/apps/worker/src/tasks/discord/sendDm.task.ts @@ -11,9 +11,18 @@ const discordDmPayloadSchema = z discordIds: z.array(z.string().min(1)).optional(), content: discordBotMessageMessageSchema.or(z.string()), }) - .refine((data) => Boolean(data.userId || data.userIds || data.discordId || data.discordIds), { - message: 'Invalid payload: at least one discordId or userId must be provided', - }); + .refine( + (data) => + Boolean( + data.userId || + data.discordId || + (Array.isArray(data.userIds) && data.userIds.length > 0) || + (Array.isArray(data.discordIds) && data.discordIds.length > 0), + ), + { + message: 'Invalid payload: at least one discordId or userId must be provided', + }, + ); type DiscordDmPayload = z.infer; From df1bfcbf2ccb928304b974c690606ccb97ecdf00 Mon Sep 17 00:00:00 2001 From: Nudesuppe42 Date: Mon, 1 Jun 2026 15:19:02 +0200 Subject: [PATCH 29/31] feat(worker): Remove errored jobs if count > 200 --- apps/worker/src/lib/config.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/apps/worker/src/lib/config.ts b/apps/worker/src/lib/config.ts index 782aaaca..38d782ab 100644 --- a/apps/worker/src/lib/config.ts +++ b/apps/worker/src/lib/config.ts @@ -2,6 +2,8 @@ * Static constant configuration values */ +import { remove } from 'winston'; + export const config = { // The number of worker threads to spawn for processing background jobs workerThreadCount: 5, @@ -18,6 +20,9 @@ export const config = { age: 3600, // 1h count: 200, }, + removeOnFail: { + count: 200, + }, }, webhooks: { errorReporting: process.env.DISCORD_WEBHOOK_ERRORS || '', From 363d38f2564d3e54c8ba2baa7beb177efc15d0e4 Mon Sep 17 00:00:00 2001 From: Nudesuppe42 Date: Mon, 1 Jun 2026 15:19:45 +0200 Subject: [PATCH 30/31] fix(worker): Set body if body is not json --- apps/worker/src/lib/discordWebhook.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/worker/src/lib/discordWebhook.ts b/apps/worker/src/lib/discordWebhook.ts index a8d93ab3..760bee05 100644 --- a/apps/worker/src/lib/discordWebhook.ts +++ b/apps/worker/src/lib/discordWebhook.ts @@ -27,7 +27,7 @@ export class DiscordWebhook { try { body = JSON.parse(text); } catch { - /* not JSON */ + body = text; } if (!res.ok) { From 5ac8deb7614be971cc539e01b44c82f44a14a6bd Mon Sep 17 00:00:00 2001 From: Nudesuppe42 Date: Mon, 1 Jun 2026 15:30:38 +0200 Subject: [PATCH 31/31] fix(worker/tasks): Add JSDoc --- apps/worker/src/tasks/administrative/purgeClaims.task.ts | 5 ++++- .../src/tasks/administrative/purgeVerifications.task.ts | 4 ++++ .../src/tasks/administrative/remindApplications.task.ts | 4 ++++ .../src/tasks/administrative/reviewActivityCheck.task.ts | 4 ++++ apps/worker/src/tasks/buildteams/sendWebhook.task.ts | 4 ++++ apps/worker/src/tasks/discord/sendDm.task.ts | 4 ++++ apps/worker/src/tasks/discord/sendLog.task.ts | 4 ++++ 7 files changed, 28 insertions(+), 1 deletion(-) diff --git a/apps/worker/src/tasks/administrative/purgeClaims.task.ts b/apps/worker/src/tasks/administrative/purgeClaims.task.ts index a98db3f1..dad3d9c6 100644 --- a/apps/worker/src/tasks/administrative/purgeClaims.task.ts +++ b/apps/worker/src/tasks/administrative/purgeClaims.task.ts @@ -4,7 +4,10 @@ import { BaseTask } from '../base.task'; const purgeClaimsPayloadSchema = z.unknown(); type purgeClaimsPayloadSchema = z.infer; - +/** + * This task deletes claims that are not associated with any center (ghost claims) and claims that have no external reference (unreferenced claims). + * @summary Purge ghost claims + */ export class PurgeClaimsTask extends BaseTask { readonly name = 'PURGE_CLAIMS'; readonly schema = purgeClaimsPayloadSchema; diff --git a/apps/worker/src/tasks/administrative/purgeVerifications.task.ts b/apps/worker/src/tasks/administrative/purgeVerifications.task.ts index 8ceb22b6..7355cc07 100644 --- a/apps/worker/src/tasks/administrative/purgeVerifications.task.ts +++ b/apps/worker/src/tasks/administrative/purgeVerifications.task.ts @@ -5,6 +5,10 @@ import { BaseTask } from '../base.task'; const purgeVerificationsPayloadSchema = z.unknown(); type purgeVerificationsPayloadSchema = z.infer; +/** + * This task deletes Minecraft verification codes that are older than 24 hours. + * @summary Purge expired Minecraft verification codes + */ export class PurgeVerificationsTask extends BaseTask { readonly name = 'PURGE_VERIFICATIONS'; readonly schema = purgeVerificationsPayloadSchema; diff --git a/apps/worker/src/tasks/administrative/remindApplications.task.ts b/apps/worker/src/tasks/administrative/remindApplications.task.ts index 0f64a743..b70b3be9 100644 --- a/apps/worker/src/tasks/administrative/remindApplications.task.ts +++ b/apps/worker/src/tasks/administrative/remindApplications.task.ts @@ -7,6 +7,10 @@ import { BaseTask } from '../base.task'; const remindApplicationsPayloadSchema = z.unknown(); type remindApplicationsPayloadSchema = z.infer; +/** + * This tasks finds all applications that are older than two weeks and havent been reviewed yet and sends a reminder to the BuildTeam staff (team.application.notify) to review them. + * @summary Remind BuildTeams of pending Applications + */ export class RemindApplicationsTask extends BaseTask { readonly name = 'REMIND_APPLICATIONS'; readonly schema = remindApplicationsPayloadSchema; diff --git a/apps/worker/src/tasks/administrative/reviewActivityCheck.task.ts b/apps/worker/src/tasks/administrative/reviewActivityCheck.task.ts index 0d327cad..75995b58 100644 --- a/apps/worker/src/tasks/administrative/reviewActivityCheck.task.ts +++ b/apps/worker/src/tasks/administrative/reviewActivityCheck.task.ts @@ -14,6 +14,10 @@ type ReviewActivityData = { compared: { id: string; art: number; par: number; ps: number; res: number; ras: number }[]; }; +/** + * This task calculated the review activity score of each BuildTeam and sends a message to a Discord channel if there are significant changes compared to the last time the task was run. The review activity score is calculated based on the average review time, pending application ratio, review efficiency score and processing speed of each BuildTeam. + * @summary Check BuildTeam review activity + */ export class ReviewActivityCheckTask extends BaseTask { readonly name = 'REVIEW_ACTIVITY_CHECK'; readonly schema = reviewActivityCheckPayloadSchema; diff --git a/apps/worker/src/tasks/buildteams/sendWebhook.task.ts b/apps/worker/src/tasks/buildteams/sendWebhook.task.ts index 17ce426d..c4b40ec1 100644 --- a/apps/worker/src/tasks/buildteams/sendWebhook.task.ts +++ b/apps/worker/src/tasks/buildteams/sendWebhook.task.ts @@ -35,6 +35,10 @@ class PartialBuildTeamWebhookFailureError extends Error { } } +/** + * This task sends a webhook to the BuildTeam Webhook URL when certain events occur related to BuildTeams, such as application submission, claim creation, etc. If the webhook fails to send to any of the destinations, it will retry with the failed destinations until it succeeds or exhausts the retry attempts. + * @summary Send BuildTeam Webhook on relevant events + */ export class SendBuildTeamWebhookTask extends BaseTask { readonly name = 'BUILDTEAM_WEBHOOK'; readonly schema = auditLogBtPayloadSchema; diff --git a/apps/worker/src/tasks/discord/sendDm.task.ts b/apps/worker/src/tasks/discord/sendDm.task.ts index c39a0984..97640637 100644 --- a/apps/worker/src/tasks/discord/sendDm.task.ts +++ b/apps/worker/src/tasks/discord/sendDm.task.ts @@ -36,6 +36,10 @@ class PartialDiscordDmFailureError extends Error { } } +/** + * This task sends a DM to one or more users on discord. The message follows a standard format. If sending fails for any user, it will retry with the failed users until it succeeds or exhausts the retry attempts. + * @summary Send Discord DM to users + */ export class SendDiscordDmTask extends BaseTask { readonly name = 'SEND_DISCORD_DM'; readonly schema = discordDmPayloadSchema; diff --git a/apps/worker/src/tasks/discord/sendLog.task.ts b/apps/worker/src/tasks/discord/sendLog.task.ts index d6edc7f5..292590c1 100644 --- a/apps/worker/src/tasks/discord/sendLog.task.ts +++ b/apps/worker/src/tasks/discord/sendLog.task.ts @@ -8,6 +8,10 @@ const discordLogPayloadSchema = z.any(); type DiscordLogPayload = z.infer; +/** + * This task sends a message to a staff-only discord channel for logging purposes. The message can be any discord accepted json payload. + * @summary Send log message + */ export class SendDiscordLogTask extends BaseTask { readonly name = 'SEND_DISCORD_LOG'; readonly schema = discordLogPayloadSchema;