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/.vscode/settings.json b/.vscode/settings.json index 9ed43d15..0a6898fb 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -40,7 +40,9 @@ "frontend/map", "frontend/assets", "dash/team", - "frontend/mobile" + "frontend/mobile", + "worker", + "worker/tasks" ], "js/ts.tsdk.path": "node_modules\\typescript\\lib" } 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/.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/Dockerfile b/apps/worker/Dockerfile new file mode 100644 index 00000000..083fb171 --- /dev/null +++ b/apps/worker/Dockerfile @@ -0,0 +1,36 @@ +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 corepack enable +RUN corepack prepare yarn@4.9.1 --activate +COPY . . +RUN yarn dlx turbo@2.1.1 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"] diff --git a/apps/worker/README.md b/apps/worker/README.md new file mode 100644 index 00000000..ae96221a --- /dev/null +++ b/apps/worker/README.md @@ -0,0 +1,14 @@ + +
+ + + +# Website Worker + +_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/package.json b/apps/worker/package.json new file mode 100644 index 00000000..b3e5839d --- /dev/null +++ b/apps/worker/package.json @@ -0,0 +1,32 @@ +{ + "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", + "test": "infisical run --path=\"/worker\" -- tsx --tsconfig tsconfig.test.json test/index.test.ts" + }, + "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", + "zod": "^4.1.13", + "winston": "^3.19.0" + }, + "prettier": "@repo/prettier-config", + "lint-staged": { + "*": "prettier --write --ignore-unknown" + } +} 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/lib/config.ts b/apps/worker/src/lib/config.ts new file mode 100644 index 00000000..38d782ab --- /dev/null +++ b/apps/worker/src/lib/config.ts @@ -0,0 +1,31 @@ +/* + * Static constant configuration values + */ + +import { remove } from 'winston'; + +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, + }, + removeOnFail: { + 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..a9b477de --- /dev/null +++ b/apps/worker/src/lib/discordBot.ts @@ -0,0 +1,78 @@ +import { z } from 'zod'; + +export type DiscordDmResult = { + success: string[]; + failure: string[]; +}; + +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.nativeEnum(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: { + '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..760bee05 --- /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 { + body = text; + } + + 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.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 }); + 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 new file mode 100644 index 00000000..133df00b --- /dev/null +++ b/apps/worker/src/lib/logger.ts @@ -0,0 +1,105 @@ +import winston from 'winston'; + +type LogMeta = { + timestamp: unknown; + level: string; + jobId?: 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, 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} ${ + Object.keys(meta.fields || {}).length > 0 ? JSON.stringify(meta.fields) : '' + }`; + } + + 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, jobId, component, error, stack, label, ...rest } = + info as winston.Logform.TransformableInfo & { + jobId?: 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, + jobId, + 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/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 new file mode 100644 index 00000000..a24d136d --- /dev/null +++ b/apps/worker/src/main.ts @@ -0,0 +1,87 @@ +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'; + +const workers = new Set(); +const cronManagers = new Set(); + +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); + } + + try { + logger.debug('Starting job worker'); + const wm = new WorkerManager().start(); + workers.add(wm); + + logger.debug('Registering cron jobs'); + const cron = new CronManager(); + + // Cron jobs + 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); + } 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) => { + logger.error('Fatal error occurred during bootstrap', { error: err }); + process.exit(1); +}); diff --git a/apps/worker/src/queue/base.worker.ts b/apps/worker/src/queue/base.worker.ts new file mode 100644 index 00000000..1f99dbae --- /dev/null +++ b/apps/worker/src/queue/base.worker.ts @@ -0,0 +1,111 @@ +import { Job, Queue, Worker } from 'bullmq'; +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 { + 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]; + + 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`); + + handler.setContext(taskLogger, prisma); + const data = handler.validate(job.data); + await handler.execute(data, job, this.queue); + }; + + 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..e86597ac --- /dev/null +++ b/apps/worker/src/queue/cron.manager.ts @@ -0,0 +1,85 @@ +import { Queue } from 'bullmq'; +import { config } from '../lib/config'; +import { logger } from '../lib/logger'; +import { redis } from '../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 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 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'); + } + + 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/administrative/purgeClaims.task.ts b/apps/worker/src/tasks/administrative/purgeClaims.task.ts new file mode 100644 index 00000000..dad3d9c6 --- /dev/null +++ b/apps/worker/src/tasks/administrative/purgeClaims.task.ts @@ -0,0 +1,28 @@ +import { Job } from 'bullmq'; +import { z } from 'zod'; +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; + + async execute(_data: purgeClaimsPayloadSchema, _job: Job) { + const emptyClaims = await this.prisma.claim.deleteMany({ + where: { center: null }, + }); + + this.logger.debug(`Purged ${emptyClaims.count} empty 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..7355cc07 --- /dev/null +++ b/apps/worker/src/tasks/administrative/purgeVerifications.task.ts @@ -0,0 +1,25 @@ +import { Job } from 'bullmq'; +import { z } from 'zod'; +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; + + 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..b70b3be9 --- /dev/null +++ b/apps/worker/src/tasks/administrative/remindApplications.task.ts @@ -0,0 +1,94 @@ +import { ApplicationStatus } from '@repo/db'; +import { Job, Queue } from 'bullmq'; +import { z } from 'zod'; +import { DiscordBotEmojis } from '../../lib/discordBot'; +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; + + 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); + } + + 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})`, + ); + + 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 new file mode 100644 index 00000000..75995b58 --- /dev/null +++ b/apps/worker/src/tasks/administrative/reviewActivityCheck.task.ts @@ -0,0 +1,258 @@ +import { Job } from 'bullmq'; +import { z } from 'zod'; +import { config } from '../../lib/config'; +import discordWebhook from '../../lib/discordWebhook'; +import { getReviewActivityScore } from '../../util/reviewActivity'; +import { BaseTask } from '../base.task'; + +const reviewActivityCheckPayloadSchema = z.unknown(); +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 }[]; +}; + +/** + * 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; + private readonly CHUNK_SIZE = 9; + + async execute(_data: reviewActivityCheckPayloadSchema, _job: Job) { + 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.toString()} 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 new file mode 100644 index 00000000..5e221cef --- /dev/null +++ b/apps/worker/src/tasks/base.task.ts @@ -0,0 +1,22 @@ +import { PrismaClient } from '@repo/db'; +import { Job, Queue } from 'bullmq'; +import { Logger } from 'winston'; +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); + } + + abstract execute(data: z.infer, job: Job, queue: Queue): Promise; +} diff --git a/apps/worker/src/tasks/buildteams/sendWebhook.task.ts b/apps/worker/src/tasks/buildteams/sendWebhook.task.ts new file mode 100644 index 00000000..c4b40ec1 --- /dev/null +++ b/apps/worker/src/tasks/buildteams/sendWebhook.task.ts @@ -0,0 +1,156 @@ +import { Job } from 'bullmq'; +import { z } from 'zod'; +import { BuildTeamWebhook, WebhookBuildTeam } from '../../lib/buildteamWebhook'; +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', +} + +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.nativeEnum(AuditLogBuildTeamType), + data: z.unknown().optional(), + destination: z.array(webhookBuildTeamSchema), +}); + +type AuditLogBtPayload = z.infer; + +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; + } +} + +/** + * 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; + + async execute(data: AuditLogBtPayload, job: Job) { + const { type, data: content, destination } = data; + + if (!destination || destination.length === 0) { + return; + } + if (!Object.values(AuditLogBuildTeamType).includes(type)) { + this.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); + if (error) { + 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, + }); + } + } + } catch (err: any) { + failed.push(dest); + } + } + + if (failed.length > 0) { + 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')), + }); + + // 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/discord/sendDm.task.ts b/apps/worker/src/tasks/discord/sendDm.task.ts new file mode 100644 index 00000000..97640637 --- /dev/null +++ b/apps/worker/src/tasks/discord/sendDm.task.ts @@ -0,0 +1,120 @@ +import { Job } from 'bullmq'; +import { z } from 'zod'; +import { discordBotMessageMessageSchema, sendDiscordDm } from '../../lib/discordBot'; +import { BaseTask } from '../base.task'; + +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: discordBotMessageMessageSchema.or(z.string()), + }) + .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; + +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; + } +} + +/** + * 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; + + 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`); + } + + const result = await sendDiscordDm(data.content, users); + const failedIds = result.failure ?? []; + const successIds = result.success ?? []; + + if (failedIds.length > 0) { + this.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); + } + + this.logger.debug(`Successfully sent Discord DM`, { recipientCount: users.length }); + } + + private async resolveUsers(data: DiscordDmPayload): 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) { + 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 + .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) { + 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`); + } + + return [user.discordId]; + } + + this.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/discord/sendLog.task.ts b/apps/worker/src/tasks/discord/sendLog.task.ts new file mode 100644 index 00000000..292590c1 --- /dev/null +++ b/apps/worker/src/tasks/discord/sendLog.task.ts @@ -0,0 +1,26 @@ +import { Job } from 'bullmq'; +import { z } from 'zod'; +import { config } from '../../lib/config'; +import discordWebhook from '../../lib/discordWebhook'; +import { BaseTask } from '../base.task'; + +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; + + 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 new file mode 100644 index 00000000..ec43d9b1 --- /dev/null +++ b/apps/worker/src/tasks/index.ts @@ -0,0 +1,22 @@ +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'; +import { SendDiscordDmTask } from './discord/sendDm.task'; +import { SendDiscordLogTask } from './discord/sendLog.task'; + +export const taskRegistry: Record = {}; + +function register(task: BaseTask) { + taskRegistry[task.name] = task; +} + +register(new SendDiscordDmTask()); +register(new SendDiscordLogTask()); +register(new SendBuildTeamWebhookTask()); +register(new ReviewActivityCheckTask()); +register(new PurgeClaimsTask()); +register(new PurgeVerificationsTask()); +register(new RemindApplicationsTask()); diff --git a/apps/worker/src/util/reviewActivity.ts b/apps/worker/src/util/reviewActivity.ts new file mode 100644 index 00000000..f5d4c641 --- /dev/null +++ b/apps/worker/src/util/reviewActivity.ts @@ -0,0 +1,91 @@ +import { ApplicationStatus } from '@repo/db'; +import prisma from '../lib/prisma'; + +/** + * 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; +} diff --git a/apps/worker/test/index.test.ts b/apps/worker/test/index.test.ts new file mode 100644 index 00000000..44f6ba5d --- /dev/null +++ b/apps/worker/test/index.test.ts @@ -0,0 +1,29 @@ +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, + ...config.removalOptions, +}); + +async function trigger() { + console.log('🚀 Simulating Dashboard action: Queueing a Discord DM request...'); + + await testQueue.add( + 'REMIND_APPLICATIONS', + {}, + { + ...config.retryOptions, + ...config.removalOptions, + }, + ); + + console.log('✅ Job successfully pushed to Redis. Closing connection.'); + await testQueue.close(); + process.exit(0); +} + +trigger(); diff --git a/apps/worker/tsconfig.json b/apps/worker/tsconfig.json new file mode 100644 index 00000000..cf9a9fb0 --- /dev/null +++ b/apps/worker/tsconfig.json @@ -0,0 +1,23 @@ +{ + "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", + "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/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" + } +} 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"