diff --git a/.gitea/workflows/builder.yaml b/.gitea/workflows/builder.yaml index 0b4f35f..c5019f2 100644 --- a/.gitea/workflows/builder.yaml +++ b/.gitea/workflows/builder.yaml @@ -13,6 +13,16 @@ jobs: - uses: actions/checkout@v3 name: Check out code + - uses: mr-smithers-excellent/docker-build-push@v6 + name: Build futureporn/bot + with: + image: futureporn/bot + tags: latest + registry: gitea.futureporn.net + dockerfile: d.bot.dockerfile + username: ${{ secrets.DOCKER_USERNAME }} + password: ${{ secrets.DOCKER_PASSWORD }} + - uses: mr-smithers-excellent/docker-build-push@v6 name: Build futureporn/migrations with: diff --git a/Tiltfile b/Tiltfile index 008d8c7..870d51c 100644 --- a/Tiltfile +++ b/Tiltfile @@ -136,29 +136,22 @@ docker_build( pull=False, ) -# docker_build( -# 'fp/bot', -# '.', -# only=[ -# './.npmrc', -# './package.json', -# './pnpm-lock.yaml', -# './pnpm-workspace.yaml', -# './packages/bot', -# './packages/image', -# './packages/scout', -# './packages/storage', -# './packages/workflows', -# './packages/types', -# './packages/utils', -# ], -# dockerfile='./d.bot.dockerfile', -# target='dev', -# live_update=[ -# sync('./packages/bot', '/app'), -# run('cd /app && pnpm i', trigger=['./packages/bot/package.json', './packages/bot/pnpm-lock.yaml']) -# ] -# ) +docker_build( + 'fp/bot', + '.', + only=[ + './.npmrc', + './package.json', + './pnpm-lock.yaml', + './pnpm-workspace.yaml', + './services/bot', + ], + dockerfile='./d.bot.dockerfile', + target='dev', + live_update=[ + sync('./services/bot', '/app') + ] +) @@ -192,13 +185,6 @@ cmd_button('capture-api:create', text='Start Recording' ) -cmd_button('postgrest:restore', - argv=['./scripts/postgrest.sh'], - resource='postgrest', - icon_name='start', - text='initialize', -) - cmd_button('postgrest:migrate', argv=['./scripts/postgrest-migrations.sh'], resource='postgrest', @@ -295,20 +281,19 @@ docker_build( 'fp/capture', '.', dockerfile='d.capture.dockerfile', - target='capture', + target='dev', only=[ './.npmrc', './package.json', './pnpm-lock.yaml', './pnpm-workspace.yaml', - './packages/capture', './packages/scout', './packages/types', './packages/utils', './services/capture', ], live_update=[ - sync('./packages/capture/dist', '/app/dist'), + sync('./services/capture/dist', '/app/dist'), ], pull=False, ) @@ -441,11 +426,11 @@ k8s_resource( # ) -# k8s_resource( -# workload='bot', -# labels=['backend'], -# # resource_deps=['strapi'], -# ) +k8s_resource( + workload='bot', + labels=['backend'], + resource_deps=['postgrest'], +) k8s_resource( workload='capture-api', port_forwards=['5003'], diff --git a/charts/fp/templates-staging/redis.yaml b/charts/fp/templates-staging/redis.yaml new file mode 100644 index 0000000..e451268 --- /dev/null +++ b/charts/fp/templates-staging/redis.yaml @@ -0,0 +1,49 @@ +--- +apiVersion: v1 +kind: Service +metadata: + name: redis + namespace: futureporn +spec: + type: ClusterIP + selector: + app: redis + ports: + - name: web + port: {{ .Values.redis.port }} + targetPort: http + protocol: TCP + + +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: redis + namespace: futureporn + labels: + app: redis +spec: + replicas: {{ .Values.redis.replicas }} + selector: + matchLabels: + app: redis + template: + metadata: + labels: + app: redis + spec: + containers: + - name: redis + image: "{{ .Values.redis.image }}" + ports: + - name: http + containerPort: {{ .Values.redis.port }} + env: + - name: PGRST_DB_ANON_ROLE + value: anonymous + - name: PGRST_JWT_SECRET + valueFrom: + secretKeyRef: + name: redis + key: jwtSecret \ No newline at end of file diff --git a/charts/fp/templates/bot.yaml b/charts/fp/templates/bot.yaml new file mode 100644 index 0000000..8cc4881 --- /dev/null +++ b/charts/fp/templates/bot.yaml @@ -0,0 +1,58 @@ +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: bot + namespace: futureporn + labels: + app.kubernetes.io/name: bot +spec: + replicas: {{ .Values.bot.replicas }} + selector: + matchLabels: + app: bot + template: + metadata: + labels: + app: bot + spec: + containers: + - name: bot + image: "{{ .Values.bot.imageName }}" + env: + - name: AUTOMATION_USER_JWT + valueFrom: + secretKeyRef: + name: bot + key: automationUserJwt + - name: DISCORD_TOKEN + valueFrom: + secretKeyRef: + name: bot + key: discordToken + - name: DISCORD_APPLICATION_ID + valueFrom: + secretKeyRef: + name: bot + key: discordApplicationId + - name: DISCORD_CHANNEL_ID + valueFrom: + secretKeyRef: + name: bot + key: discordChannelId + - name: DISCORD_GUILD_ID + valueFrom: + secretKeyRef: + name: bot + key: discordGuildId + - name: WORKER_CONNECTION_STRING + valueFrom: + secretKeyRef: + name: bot + key: workerConnectionString + resources: + limits: + cpu: 150m + memory: 256Mi + restartPolicy: Always + diff --git a/charts/fp/templates/capture.yaml b/charts/fp/templates/capture.yaml index 18c34fc..b9c71ea 100644 --- a/charts/fp/templates/capture.yaml +++ b/charts/fp/templates/capture.yaml @@ -38,11 +38,18 @@ spec: env: - name: FUNCTION value: worker - - name: PGBOSS_URL + - name: WORKER_CONNECTION_STRING valueFrom: secretKeyRef: name: capture - key: pgbossUrl + key: workerConnectionString + - name: AUTOMATION_USER_JWT + valueFrom: + secretKeyRef: + name: postgrest + key: jwtSecret + - name: POSTGREST_URL + value: http://postgrest.futureporn.svc.cluster.local:9000 - name: PORT value: "{{ .Values.capture.api.port }}" - name: S3_ENDPOINT @@ -95,11 +102,11 @@ spec: env: - name: FUNCTION value: api - - name: PGBOSS_URL + - name: WORKER_CONNECTION_STRING valueFrom: secretKeyRef: name: capture - key: pgbossUrl + key: workerConnectionString - name: PORT value: "{{ .Values.capture.api.port }}" resources: diff --git a/charts/fp/templates/postgrest.yaml b/charts/fp/templates/postgrest.yaml index d830406..a8fccf2 100644 --- a/charts/fp/templates/postgrest.yaml +++ b/charts/fp/templates/postgrest.yaml @@ -1,4 +1,18 @@ - +--- +apiVersion: v1 +kind: Service +metadata: + name: postgrest + namespace: futureporn +spec: + type: ClusterIP + selector: + app: postgrest + ports: + - name: web + port: {{ .Values.postgrest.port }} + targetPort: http + protocol: TCP --- diff --git a/charts/fp/values.yaml b/charts/fp/values.yaml index fee7810..d74ad09 100644 --- a/charts/fp/values.yaml +++ b/charts/fp/values.yaml @@ -22,7 +22,7 @@ next: capture: imageName: fp/capture worker: - replicas: 1 + replicas: 3 api: port: 5003 replicas: 1 diff --git a/d.bot.dockerfile b/d.bot.dockerfile index 627c2f7..1419e13 100644 --- a/d.bot.dockerfile +++ b/d.bot.dockerfile @@ -1,36 +1,25 @@ -FROM node:20.15 as base +FROM node:20 AS base ENV PNPM_HOME="/pnpm" ENV PATH="$PNPM_HOME:$PATH" WORKDIR /app -RUN corepack enable && corepack prepare pnpm@9.5.0 --activate +RUN corepack enable && corepack prepare pnpm@9.6.0 --activate ENTRYPOINT ["pnpm"] FROM base AS install COPY pnpm-lock.yaml .npmrc package.json . -COPY ./packages/bot/ ./packages/bot/ -COPY ./packages/types/ ./packages/types/ -COPY ./packages/storage/ ./packages/storage/ -COPY ./packages/scout/ ./packages/scout/ -COPY ./packages/image/ ./packages/image/ -COPY ./packages/utils/ ./packages/utils/ +COPY ./services/bot/ ./services/bot/ - -# RUN ls -lash . -# RUN ls -lash ./packages/ -# RUN ls -lash ./packages/bot/ +RUN --mount=type=cache,id=pnpm,target=/pnpm/store pnpm fetch RUN --mount=type=cache,id=pnpm,target=/pnpm/store pnpm install --recursive --frozen-lockfile --prefer-offline -# RUN ls -lash . -# RUN ls -la ./packages FROM install AS build RUN pnpm -r build -RUN pnpm deploy --filter=bot /prod/bot-dev RUN pnpm deploy --filter=bot --prod /prod/bot -FROM base AS dev -COPY --from=build /prod/bot-dev . +FROM install AS dev +WORKDIR /app/services/bot CMD ["run", "dev"] diff --git a/d.capture.dockerfile b/d.capture.dockerfile index abce579..dd49dc8 100644 --- a/d.capture.dockerfile +++ b/d.capture.dockerfile @@ -42,6 +42,12 @@ RUN mkdir -p /prod/capture RUN --mount=type=cache,id=pnpm,target=/pnpm/store pnpm --filter=@futureporn/capture deploy --prod /prod/capture + +FROM build AS dev +WORKDIR /app/services/capture +ENTRYPOINT ["pnpm", "run", "dev"] + + ## start the app with dumb init to spawn the Node.js runtime process ## with signal support ## The mode @futureporn/capture uses when starting is determined by FUNCTION environment variable. (worker|api) diff --git a/scripts/k8s-secrets.sh b/scripts/k8s-secrets.sh index d6384ae..3bca02e 100755 --- a/scripts/k8s-secrets.sh +++ b/scripts/k8s-secrets.sh @@ -33,6 +33,15 @@ EOF # --from-literal=b2Key=${UPPY_B2_KEY} \ # --from-literal=b2Secret=${UPPY_B2_SECRET}\ +kubectl --namespace futureporn delete secret bot --ignore-not-found +kubectl --namespace futureporn create secret generic bot \ +--from-literal=automationUserJwt=${AUTOMATION_USER_JWT} \ +--from-literal=discordToken=${DISCORD_TOKEN} \ +--from-literal=discordChannelId=${DISCORD_CHANNEL_ID} \ +--from-literal=discordGuildId=${DISCORD_GUILD_ID} \ +--from-literal=discordApplicationId=${DISCORD_APPLICATION_ID} \ +--from-literal=workerConnectionString=${WORKER_CONNECTION_STRING} + kubectl --namespace futureporn delete secret pgadmin4 --ignore-not-found kubectl --namespace futureporn create secret generic pgadmin4 \ --from-literal=email=${PGADMIN_DEFAULT_EMAIL} \ @@ -45,7 +54,7 @@ kubectl --namespace futureporn create secret generic postgrest \ kubectl --namespace futureporn delete secret capture --ignore-not-found kubectl --namespace futureporn create secret generic capture \ ---from-literal=pgbossUrl=${PGBOSS_URL} \ +--from-literal=workerConnectionString=${WORKER_CONNECTION_STRING} \ --from-literal=s3AccessKeyId=${S3_USC_BUCKET_KEY_ID} \ --from-literal=s3SecretAccessKey=${S3_USC_BUCKET_APPLICATION_KEY} @@ -58,22 +67,11 @@ kubectl --namespace futureporn create secret generic mailbox \ --from-literal=imapPassword=${IMAP_PASSWORD} \ --from-literal=imapAccessToken=${IMAP_ACCESS_TOKEN} -kubectl --namespace futureporn delete secret trigger --ignore-not-found -kubectl --namespace futureporn create secret generic trigger \ ---from-literal=redisUrl=${TRIGGER_REDIS_URL} \ ---from-literal=encryptionKey=${TRIGGER_ENCRYPTION_KEY} \ ---from-literal=providerSecret=${TRIGGER_PROVIDER_SECRET} \ ---from-literal=coordinatorSecret=${TRIGGER_COORDINATOR_SECRET} \ ---from-literal=magicLinkSecret=${TRIGGER_MAGIC_LINK_SECRET} \ ---from-literal=sessionSecret=${TRIGGER_SESSION_SECRET} \ ---from-literal=databaseUrl=${TRIGGER_DATABASE_URL} \ ---from-literal=loginUrl=${TRIGGER_LOGIN_ORIGIN} \ ---from-literal=appOrigin=${TRIGGER_APP_ORIGIN} -kubectl --namespace futureporn delete secret discord --ignore-not-found -kubectl --namespace futureporn create secret generic discord \ ---from-literal=token=${DISCORD_TOKEN} \ ---from-literal=applicationId=${DISCORD_APPLICATION_ID} +# kubectl --namespace futureporn delete secret discord --ignore-not-found +# kubectl --namespace futureporn create secret generic discord \ +# --from-literal=token=${DISCORD_TOKEN} \ +# --from-literal=applicationId=${DISCORD_APPLICATION_ID} kubectl --namespace futureporn delete secret redis --ignore-not-found kubectl --namespace futureporn create secret generic redis \ @@ -113,22 +111,9 @@ kubectl --namespace futureporn create secret generic grafana \ --from-literal=admin-password=${GRAFANA_PASSWORD} -kubectl --namespace futureporn delete secret scout --ignore-not-found -kubectl --namespace futureporn create secret generic scout \ ---from-literal=recentsToken=${SCOUT_RECENTS_TOKEN} \ ---from-literal=strapiApiKey=${SCOUT_STRAPI_API_KEY} \ ---from-literal=imapServer=${SCOUT_IMAP_SERVER} \ ---from-literal=imapPort=${SCOUT_IMAP_PORT} \ ---from-literal=imapUsername=${SCOUT_IMAP_USERNAME} \ ---from-literal=imapPassword=${SCOUT_IMAP_PASSWORD} \ ---from-literal=imapAccessToken=${SCOUT_IMAP_ACCESS_TOKEN} \ ---from-literal=nitterAccessKey=${SCOUT_NITTER_ACCESS_KEY} \ ---from-literal=s3BucketKeyId=${S3_BUCKET_KEY_ID} \ ---from-literal=s3BucketApplicationKey=${S3_BUCKET_APPLICATION_KEY} - -kubectl --namespace futureporn delete secret link2cid --ignore-not-found -kubectl --namespace futureporn create secret generic link2cid \ ---from-literal=apiKey=${LINK2CID_API_KEY} +# kubectl --namespace futureporn delete secret link2cid --ignore-not-found +# kubectl --namespace futureporn create secret generic link2cid \ +# --from-literal=apiKey=${LINK2CID_API_KEY} kubectl --namespace cert-manager delete secret vultr --ignore-not-found kubectl --namespace cert-manager create secret generic vultr \ diff --git a/services/capture/graphile.config.ts b/services/bot/graphile.config.ts similarity index 64% rename from services/capture/graphile.config.ts rename to services/bot/graphile.config.ts index 5e6d03f..6501bf3 100644 --- a/services/capture/graphile.config.ts +++ b/services/bot/graphile.config.ts @@ -3,8 +3,8 @@ import type {} from "graphile-worker"; const preset: GraphileConfig.Preset = { worker: { - connectionString: process.env.DATABASE_URL, - concurrentJobs: 3, + connectionString: process.env.WORKER_CONNECTION_STRING, + concurrentJobs: 5, fileExtensions: [".js", ".ts"], }, }; diff --git a/services/bot/package.json b/services/bot/package.json index 82ba218..a62f59b 100644 --- a/services/bot/package.json +++ b/services/bot/package.json @@ -1,29 +1,32 @@ { "name": "@futureporn/bot", "type": "module", - "version": "1.0.0", - "description": "", + "version": "1.0.4", + "description": "Futureporn Discord bot", "main": "dist/index.js", "scripts": { "test": "echo \"Warn: no test specified\" && exit 0", "start": "node ./dist/index.js", - "dev": "nodemon --ext js,ts,json,yaml --exec \"node --loader ts-node/esm --disable-warning=ExperimentalWarning ./src/index.ts\"", + "dev.nodemon": "nodemon --ext js,ts,json,yaml --exec \"node --loader ts-node/esm --disable-warning=ExperimentalWarning ./src/index.ts\"", + "dev": "tsx --watch ./src/index.ts", "build": "tsc --build", "clean": "rm -rf dist", "superclean": "rm -rf node_modules && rm -rf pnpm-lock.yaml && rm -rf dist" }, - "packageManager": "pnpm@9.5.0", + "packageManager": "pnpm@9.6.0", "keywords": [], - "author": "", + "author": "@CJ_Clippy", "license": "Unlicense", "dependencies": { "discord.js": "^14.15.3", "dotenv": "^16.4.5", - "pg-boss": "^9.0.3" + "graphile-config": "0.0.1-beta.9", + "graphile-worker": "^0.16.6" }, "devDependencies": { "nodemon": "^3.1.4", "ts-node": "^10.9.2", + "tsx": "^4.16.2", "typescript": "^5.5.3" } } diff --git a/services/bot/pnpm-lock.yaml b/services/bot/pnpm-lock.yaml index 9ed42ed..ef8fb6b 100644 --- a/services/bot/pnpm-lock.yaml +++ b/services/bot/pnpm-lock.yaml @@ -14,9 +14,12 @@ importers: dotenv: specifier: ^16.4.5 version: 16.4.5 - pg-boss: - specifier: ^9.0.3 - version: 9.0.3 + graphile-config: + specifier: 0.0.1-beta.9 + version: 0.0.1-beta.9 + graphile-worker: + specifier: ^0.16.6 + version: 0.16.6(typescript@5.5.4) devDependencies: nodemon: specifier: ^3.1.4 @@ -24,12 +27,63 @@ importers: ts-node: specifier: ^10.9.2 version: 10.9.2(@types/node@22.0.0)(typescript@5.5.4) + tsx: + specifier: ^4.16.2 + version: 4.16.2 typescript: specifier: ^5.5.3 version: 5.5.4 + ../..: {} + + ../../packages/image: {} + + ../../packages/infra: {} + + ../../packages/meal: {} + + ../../packages/old: {} + + ../../packages/scout: {} + + ../../packages/storage: {} + + ../../packages/taco: {} + + ../../packages/types: {} + + ../../packages/utils: {} + + ../../packages/video: {} + + ../../packages/worker: {} + + ../capture: {} + + ../mailbox: {} + + ../migrations: {} + + ../next: {} + + ../strapi: {} + + ../uppy: {} + packages: + '@babel/code-frame@7.24.7': + resolution: {integrity: sha512-BcYH1CVJBO9tvyIZ2jVeXgSIMvGZ2FDRvDdOIVQyuklNKSsx+eppDEBq/g47Ayw+RqNFE+URvOShmf+f/qwAlA==} + engines: {node: '>=6.9.0'} + + '@babel/helper-validator-identifier@7.24.7': + resolution: {integrity: sha512-rR+PBcQ1SMQDDyF6X0wxtG8QyLCgUB0eRAGguqRLfkCA87l7yAP7ehq8SNj96OOGTO8OBV70KhuFYcIkHXOg0w==} + engines: {node: '>=6.9.0'} + + '@babel/highlight@7.24.7': + resolution: {integrity: sha512-EStJpq4OuY8xYfhGVXngigBJRWxftKX9ksiGDnmlY3o7B/V7KIAc9X4oiK87uPJSc/vs5L869bem5fhZa8caZw==} + engines: {node: '>=6.9.0'} + '@cspotcode/source-map-support@0.8.1': resolution: {integrity: sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==} engines: {node: '>=12'} @@ -62,6 +116,147 @@ packages: resolution: {integrity: sha512-PZ+vLpxGCRtmr2RMkqh8Zp+BenUaJqlS6xhgWKEZcgC/vfHLEzpHtKkB0sl3nZWpwtcKk6YWy+pU3okL2I97FA==} engines: {node: '>=16.11.0'} + '@esbuild/aix-ppc64@0.21.5': + resolution: {integrity: sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==} + engines: {node: '>=12'} + cpu: [ppc64] + os: [aix] + + '@esbuild/android-arm64@0.21.5': + resolution: {integrity: sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==} + engines: {node: '>=12'} + cpu: [arm64] + os: [android] + + '@esbuild/android-arm@0.21.5': + resolution: {integrity: sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==} + engines: {node: '>=12'} + cpu: [arm] + os: [android] + + '@esbuild/android-x64@0.21.5': + resolution: {integrity: sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==} + engines: {node: '>=12'} + cpu: [x64] + os: [android] + + '@esbuild/darwin-arm64@0.21.5': + resolution: {integrity: sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==} + engines: {node: '>=12'} + cpu: [arm64] + os: [darwin] + + '@esbuild/darwin-x64@0.21.5': + resolution: {integrity: sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==} + engines: {node: '>=12'} + cpu: [x64] + os: [darwin] + + '@esbuild/freebsd-arm64@0.21.5': + resolution: {integrity: sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==} + engines: {node: '>=12'} + cpu: [arm64] + os: [freebsd] + + '@esbuild/freebsd-x64@0.21.5': + resolution: {integrity: sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==} + engines: {node: '>=12'} + cpu: [x64] + os: [freebsd] + + '@esbuild/linux-arm64@0.21.5': + resolution: {integrity: sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==} + engines: {node: '>=12'} + cpu: [arm64] + os: [linux] + + '@esbuild/linux-arm@0.21.5': + resolution: {integrity: sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==} + engines: {node: '>=12'} + cpu: [arm] + os: [linux] + + '@esbuild/linux-ia32@0.21.5': + resolution: {integrity: sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==} + engines: {node: '>=12'} + cpu: [ia32] + os: [linux] + + '@esbuild/linux-loong64@0.21.5': + resolution: {integrity: sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==} + engines: {node: '>=12'} + cpu: [loong64] + os: [linux] + + '@esbuild/linux-mips64el@0.21.5': + resolution: {integrity: sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==} + engines: {node: '>=12'} + cpu: [mips64el] + os: [linux] + + '@esbuild/linux-ppc64@0.21.5': + resolution: {integrity: sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==} + engines: {node: '>=12'} + cpu: [ppc64] + os: [linux] + + '@esbuild/linux-riscv64@0.21.5': + resolution: {integrity: sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==} + engines: {node: '>=12'} + cpu: [riscv64] + os: [linux] + + '@esbuild/linux-s390x@0.21.5': + resolution: {integrity: sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==} + engines: {node: '>=12'} + cpu: [s390x] + os: [linux] + + '@esbuild/linux-x64@0.21.5': + resolution: {integrity: sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==} + engines: {node: '>=12'} + cpu: [x64] + os: [linux] + + '@esbuild/netbsd-x64@0.21.5': + resolution: {integrity: sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==} + engines: {node: '>=12'} + cpu: [x64] + os: [netbsd] + + '@esbuild/openbsd-x64@0.21.5': + resolution: {integrity: sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==} + engines: {node: '>=12'} + cpu: [x64] + os: [openbsd] + + '@esbuild/sunos-x64@0.21.5': + resolution: {integrity: sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==} + engines: {node: '>=12'} + cpu: [x64] + os: [sunos] + + '@esbuild/win32-arm64@0.21.5': + resolution: {integrity: sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==} + engines: {node: '>=12'} + cpu: [arm64] + os: [win32] + + '@esbuild/win32-ia32@0.21.5': + resolution: {integrity: sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==} + engines: {node: '>=12'} + cpu: [ia32] + os: [win32] + + '@esbuild/win32-x64@0.21.5': + resolution: {integrity: sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==} + engines: {node: '>=12'} + cpu: [x64] + os: [win32] + + '@graphile/logger@0.2.0': + resolution: {integrity: sha512-jjcWBokl9eb1gVJ85QmoaQ73CQ52xAaOCF29ukRbYNl6lY+ts0ErTaDYOBlejcbUs2OpaiqYLO5uDhyLFzWw4w==} + '@jridgewell/resolve-uri@3.1.2': resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==} engines: {node: '>=6.0.0'} @@ -96,9 +291,27 @@ packages: '@tsconfig/node16@1.0.4': resolution: {integrity: sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==} + '@types/debug@4.1.12': + resolution: {integrity: sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ==} + + '@types/interpret@1.1.3': + resolution: {integrity: sha512-uBaBhj/BhilG58r64mtDb/BEdH51HIQLgP5bmWzc5qCtFMja8dCk/IOJmk36j0lbi9QHwI6sbtUNGuqXdKCAtQ==} + + '@types/ms@0.7.34': + resolution: {integrity: sha512-nG96G3Wp6acyAgJqGasjODb+acrI7KltPiRxzHPXnP3NgI28bpQDRv53olbqGXbfcgF5aiiHmO3xpwEpS5Ld9g==} + + '@types/node@20.14.13': + resolution: {integrity: sha512-+bHoGiZb8UiQ0+WEtmph2IWQCjIqg8MDZMAV+ppRRhUZnquF5mQkP/9vpSwJClEiSM/C7fZZExPzfU0vJTyp8w==} + '@types/node@22.0.0': resolution: {integrity: sha512-VT7KSYudcPOzP5Q0wfbowyNLaVR8QWUdw+088uFWwfvpY6uCWaXpqV6ieLAu9WBcnTa7H4Z5RLK8I5t2FuOcqw==} + '@types/pg@8.11.6': + resolution: {integrity: sha512-/2WmmBXHLsfRqzfHW7BNZ8SbYzE8OSk7i3WjFYvfgRHj7S1xj+16Je5fUKv3lVdVzk/zn9TXOqf+avFCFIE0yQ==} + + '@types/semver@7.5.8': + resolution: {integrity: sha512-I8EUhyrgfLrcTkzV3TSsGyl1tSuPrEDzr0yd5m90UgNxQkyDXULk3b6MlQqTCpZpNtWe1K0hzclnZkTcLBe2UQ==} + '@types/ws@8.5.11': resolution: {integrity: sha512-4+q7P5h3SpJxaBft0Dzpbr6lmMaqh0Jr2tbhJZ/luAwvD7ohSCniYkwz/pLxuT2h0EOa6QADgJj1Ko+TzRfZ+w==} @@ -115,8 +328,16 @@ packages: engines: {node: '>=0.4.0'} hasBin: true - aggregate-error@3.1.0: - resolution: {integrity: sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA==} + ansi-regex@5.0.1: + resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} + engines: {node: '>=8'} + + ansi-styles@3.2.1: + resolution: {integrity: sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==} + engines: {node: '>=4'} + + ansi-styles@4.3.0: + resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} engines: {node: '>=8'} anymatch@3.1.3: @@ -126,6 +347,9 @@ packages: arg@4.1.3: resolution: {integrity: sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==} + argparse@2.0.1: + resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} + balanced-match@1.0.2: resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} @@ -140,24 +364,54 @@ packages: resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==} engines: {node: '>=8'} + callsites@3.1.0: + resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==} + engines: {node: '>=6'} + + chalk@2.4.2: + resolution: {integrity: sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==} + engines: {node: '>=4'} + + chalk@4.1.2: + resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} + engines: {node: '>=10'} + chokidar@3.6.0: resolution: {integrity: sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==} engines: {node: '>= 8.10.0'} - clean-stack@2.2.0: - resolution: {integrity: sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A==} - engines: {node: '>=6'} + cliui@8.0.1: + resolution: {integrity: sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==} + engines: {node: '>=12'} + + color-convert@1.9.3: + resolution: {integrity: sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==} + + color-convert@2.0.1: + resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} + engines: {node: '>=7.0.0'} + + color-name@1.1.3: + resolution: {integrity: sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==} + + color-name@1.1.4: + resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} concat-map@0.0.1: resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} + cosmiconfig@8.3.6: + resolution: {integrity: sha512-kcZ6+W5QzcJ3P1Mt+83OUv/oHFqZHIx8DuxG6eZ5RGMERoLqp4BuGjhHLYGK+Kf5XVkQvqBSmAy/nGWN3qDgEA==} + engines: {node: '>=14'} + peerDependencies: + typescript: '>=4.9.5' + peerDependenciesMeta: + typescript: + optional: true + create-require@1.1.1: resolution: {integrity: sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==} - cron-parser@4.9.0: - resolution: {integrity: sha512-p0SaNjrHOnQeR8/VnfGbmg9te2kfyYSQ7Sc/j/6DtPL3JQvKxmjO9TSjNFpujqV3vEYYBvNNvXSxzyksBWAx1Q==} - engines: {node: '>=12.0.0'} - debug@4.3.6: resolution: {integrity: sha512-O/09Bd4Z1fBrU4VzkhFqVgpPzaGbw6Sm9FEkBT1A/YBXQFGuuSxa1dN2nxgxS34JmKXqYx8CZAwEVoJFImUXIg==} engines: {node: '>=6.0'} @@ -167,10 +421,6 @@ packages: supports-color: optional: true - delay@5.0.0: - resolution: {integrity: sha512-ReEBKkIfe4ya47wlPYf/gu5ib6yUG0/Aez0JQZQz94kiWtRQvZIQbTiehsnwHvLSWJnQdhVeqYue7Id1dKr0qw==} - engines: {node: '>=10'} - diff@4.0.2: resolution: {integrity: sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==} engines: {node: '>=0.3.1'} @@ -186,6 +436,25 @@ packages: resolution: {integrity: sha512-ZmdL2rui+eB2YwhsWzjInR8LldtZHGDoQ1ugH85ppHKwpUHL7j7rN0Ti9NCnGiQbhaZ11FpR+7ao1dNsmduNUg==} engines: {node: '>=12'} + emoji-regex@8.0.0: + resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} + + error-ex@1.3.2: + resolution: {integrity: sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==} + + esbuild@0.21.5: + resolution: {integrity: sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==} + engines: {node: '>=12'} + hasBin: true + + escalade@3.1.2: + resolution: {integrity: sha512-ErCHMCae19vR8vQGe50xIsVomy19rg6gFu3+r3jkEO46suLMWBksvVyoGgQV+jOfl84ZSOSlmv6Gxa89PmTGmA==} + engines: {node: '>=6'} + + escape-string-regexp@1.0.5: + resolution: {integrity: sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==} + engines: {node: '>=0.8.0'} + fast-deep-equal@3.1.3: resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} @@ -198,20 +467,47 @@ packages: engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} os: [darwin] + get-caller-file@2.0.5: + resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==} + engines: {node: 6.* || 8.* || >= 10.*} + + get-tsconfig@4.7.6: + resolution: {integrity: sha512-ZAqrLlu18NbDdRaHq+AKXzAmqIUPswPWKUchfytdAjiRFnCe5ojG2bstg6mRiZabkKfCoL/e98pbBELIV/YCeA==} + glob-parent@5.1.2: resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==} engines: {node: '>= 6'} + graphile-config@0.0.1-beta.9: + resolution: {integrity: sha512-7vNxXZ24OAgXxDKXYi9JtgWPMuNbBL3057Yf32Ux+/rVP4+EePgySCc+NNnn0tORi8qwqVreN8bdWqGIcSwNXg==} + engines: {node: '>=16'} + + graphile-worker@0.16.6: + resolution: {integrity: sha512-e7gGYDmGqzju2l83MpzX8vNG/lOtVJiSzI3eZpAFubSxh/cxs7sRrRGBGjzBP1kNG0H+c95etPpNRNlH65PYhw==} + engines: {node: '>=14.0.0'} + hasBin: true + has-flag@3.0.0: resolution: {integrity: sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==} engines: {node: '>=4'} + has-flag@4.0.0: + resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} + engines: {node: '>=8'} + ignore-by-default@1.0.1: resolution: {integrity: sha512-Ius2VYcGNk7T90CppJqcIkS5ooHUZyIQK+ClZfMfMNFEF9VSE73Fq+906u/CWu92x4gzZMWOwfFYckPObzdEbA==} - indent-string@4.0.0: - resolution: {integrity: sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==} - engines: {node: '>=8'} + import-fresh@3.3.0: + resolution: {integrity: sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==} + engines: {node: '>=6'} + + interpret@3.1.1: + resolution: {integrity: sha512-6xwYfHbajpoF0xLW+iwLkhwgvLoZDfjYfoFNu8ftMoXINzwuymNLd9u/KmwtdT2GbR+/Cz66otEGEVVUHX9QLQ==} + engines: {node: '>=10.13.0'} + + is-arrayish@0.2.1: + resolution: {integrity: sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==} is-binary-path@2.1.0: resolution: {integrity: sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==} @@ -221,6 +517,10 @@ packages: resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} engines: {node: '>=0.10.0'} + is-fullwidth-code-point@3.0.0: + resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==} + engines: {node: '>=8'} + is-glob@4.0.3: resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} engines: {node: '>=0.10.0'} @@ -229,8 +529,23 @@ packages: resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} engines: {node: '>=0.12.0'} - lodash.debounce@4.0.8: - resolution: {integrity: sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==} + js-tokens@4.0.0: + resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} + + js-yaml@4.1.0: + resolution: {integrity: sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==} + hasBin: true + + json-parse-even-better-errors@2.3.1: + resolution: {integrity: sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==} + + json5@2.2.3: + resolution: {integrity: sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==} + engines: {node: '>=6'} + hasBin: true + + lines-and-columns@1.2.4: + resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==} lodash.snakecase@4.1.1: resolution: {integrity: sha512-QZ1d4xoBHYUeuouhEq3lk3Uq7ldgyFXGBhg04+oRLnIz8o9T65Eh+8YdroUwn846zchkA9yDsDl5CVVaV2nqYw==} @@ -238,10 +553,6 @@ packages: lodash@4.17.21: resolution: {integrity: sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==} - luxon@3.4.4: - resolution: {integrity: sha512-zobTr7akeGHnv7eBOXcRgMeCP6+uyYsczwmeRCauvpvaAltgNyTbLH/+VaEAPUeWBT+1GuNmz4wC/6jtQzbbVA==} - engines: {node: '>=12'} - magic-bytes.js@1.10.0: resolution: {integrity: sha512-/k20Lg2q8LE5xiaaSkMXk4sfvI+9EGEykFS4b0CHHGWqDYU0bGUFSwchNOMA56D7TCs9GwVTkqe9als1/ns8UQ==} @@ -263,13 +574,20 @@ packages: resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==} engines: {node: '>=0.10.0'} - p-map@4.0.0: - resolution: {integrity: sha512-/bjOqmgETBYB5BoEeGVea8dmvHb2m9GLy1E9W43yeyfP6QQCZGFNa+XRceJEuDB6zqr+gKpIAmlLebMpykw/MQ==} - engines: {node: '>=10'} + obuf@1.1.2: + resolution: {integrity: sha512-PX1wu0AmAdPqOL1mWhqmlOd8kOIZQwGZw6rh7uby9fTc5lhaOWFLX3I6R1hrF9k3zUY40e6igsLGkDXK92LJNg==} - pg-boss@9.0.3: - resolution: {integrity: sha512-cUWUiv3sr563yNy0nCZ25Tv5U0m59Y9MhX/flm0vTR012yeVCrqpfboaZP4xFOQPdWipMJpuu4g94HR0SncTgw==} - engines: {node: '>=16'} + parent-module@1.0.1: + resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==} + engines: {node: '>=6'} + + parse-json@5.2.0: + resolution: {integrity: sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==} + engines: {node: '>=8'} + + path-type@4.0.0: + resolution: {integrity: sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==} + engines: {node: '>=8'} pg-cloudflare@1.1.1: resolution: {integrity: sha512-xWPagP/4B6BgFO+EKz3JONXv3YDgvkbVrGw2mTo3D6tVDQRh1e7cqVGvyR3BE+eQgAvx1XhW/iEASj4/jCWl3Q==} @@ -281,6 +599,10 @@ packages: resolution: {integrity: sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw==} engines: {node: '>=4.0.0'} + pg-numeric@1.0.2: + resolution: {integrity: sha512-BM/Thnrw5jm2kKLE5uJkXqqExRUY/toLHda65XgFTBTFYZyopbKjBe29Ii3RbkvlsMoFwD+tHeGaCjjv0gHlyw==} + engines: {node: '>=4'} + pg-pool@3.6.2: resolution: {integrity: sha512-Htjbg8BlwXqSBQ9V8Vjtc+vzf/6fVUuak/3/XXKA9oxZprwW3IMDQTGHP+KDmVL7rtd+R1QjbnCFPuTHm3G4hg==} peerDependencies: @@ -293,6 +615,10 @@ packages: resolution: {integrity: sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA==} engines: {node: '>=4'} + pg-types@4.0.2: + resolution: {integrity: sha512-cRL3JpS3lKMGsKaWndugWQoLOCoP+Cic8oseVcbr0qhPzYD5DWXK+RZ9LY9wxRf7RQia4SCwQlXk0q6FCPrVng==} + engines: {node: '>=10'} + pg@8.12.0: resolution: {integrity: sha512-A+LHUSnwnxrnL/tZ+OLfqR1SxLN3c/pgDztZ47Rpbsd4jUytsTtwQo/TLPRzPJMp/1pbhYVhH9cuSZLAajNfjQ==} engines: {node: '>= 8.0.0'} @@ -305,6 +631,9 @@ packages: pgpass@1.0.5: resolution: {integrity: sha512-FdW9r/jQZhSeohs1Z3sI1yxFQNFvMcnmfuj4WBMUTxOrAyLMaTcE1aAMBiTlbMNaXvBCQuVi0R7hd8udDSP7ug==} + picocolors@1.0.1: + resolution: {integrity: sha512-anP1Z8qwhkbmu7MFP5iTt+wQKXgwzf7zTyGlcdzabySa9vd0Xt392U0rVmz9poOaBj0uHJKyyo9/upk0HrEQew==} + picomatch@2.3.1: resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==} engines: {node: '>=8.6'} @@ -313,18 +642,37 @@ packages: resolution: {integrity: sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA==} engines: {node: '>=4'} + postgres-array@3.0.2: + resolution: {integrity: sha512-6faShkdFugNQCLwucjPcY5ARoW1SlbnrZjmGl0IrrqewpvxvhSLHimCVzqeuULCbG0fQv7Dtk1yDbG3xv7Veog==} + engines: {node: '>=12'} + postgres-bytea@1.0.0: resolution: {integrity: sha512-xy3pmLuQqRBZBXDULy7KbaitYqLcmxigw14Q5sj8QBVLqEwXfeybIKVWiqAXTlcvdvb0+xkOtDbfQMOf4lST1w==} engines: {node: '>=0.10.0'} + postgres-bytea@3.0.0: + resolution: {integrity: sha512-CNd4jim9RFPkObHSjVHlVrxoVQXz7quwNFpz7RY1okNNme49+sVyiTvTRobiLV548Hx/hb1BG+iE7h9493WzFw==} + engines: {node: '>= 6'} + postgres-date@1.0.7: resolution: {integrity: sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q==} engines: {node: '>=0.10.0'} + postgres-date@2.1.0: + resolution: {integrity: sha512-K7Juri8gtgXVcDfZttFKVmhglp7epKb1K4pgrkLxehjqkrgPhfG6OO8LHLkfaqkbpjNRnra018XwAr1yQFWGcA==} + engines: {node: '>=12'} + postgres-interval@1.2.0: resolution: {integrity: sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ==} engines: {node: '>=0.10.0'} + postgres-interval@3.0.0: + resolution: {integrity: sha512-BSNDnbyZCXSxgA+1f5UU2GmwhoI0aU5yMxRGO8CdFEcY2BQF9xm/7MqKnYoM1nJDk8nONNWDk9WeSmePFhQdlw==} + engines: {node: '>=12'} + + postgres-range@1.1.4: + resolution: {integrity: sha512-i/hbxIE9803Alj/6ytL7UHQxRvZkI9O4Sy+J3HGc4F4oo/2eQAjTSNJ0bfxyse3bH0nuVesCk+3IRLaMtG3H6w==} + pstree.remy@1.1.8: resolution: {integrity: sha512-77DZwxQmxKnu3aR542U+X8FypNzbfJ+C5XQDk3uWjWxn6151aIMGthWYRXTqT1E5oJvg+ljaa2OJi+VfvCOQ8w==} @@ -332,15 +680,22 @@ packages: resolution: {integrity: sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==} engines: {node: '>=8.10.0'} + require-directory@2.1.1: + resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==} + engines: {node: '>=0.10.0'} + + resolve-from@4.0.0: + resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==} + engines: {node: '>=4'} + + resolve-pkg-maps@1.0.0: + resolution: {integrity: sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==} + semver@7.6.3: resolution: {integrity: sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==} engines: {node: '>=10'} hasBin: true - serialize-error@8.1.0: - resolution: {integrity: sha512-3NnuWfM6vBYoy5gZFvHiYsVbafvI9vZv/+jlIigFn4oP4zjNPK3LhcY0xSCgeb1a5L8jO71Mit9LlNoi2UfDDQ==} - engines: {node: '>=10'} - simple-update-notifier@2.0.0: resolution: {integrity: sha512-a2B9Y0KlNXl9u/vsW6sTIu9vGEpfKu2wRV6l1H3XEas/0gUIzGzBoP/IouTcUQbm9JWZLH3COxyn03TYlFax6w==} engines: {node: '>=10'} @@ -349,10 +704,22 @@ packages: resolution: {integrity: sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==} engines: {node: '>= 10.x'} + string-width@4.2.3: + resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} + engines: {node: '>=8'} + + strip-ansi@6.0.1: + resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} + engines: {node: '>=8'} + supports-color@5.5.0: resolution: {integrity: sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==} engines: {node: '>=4'} + supports-color@7.2.0: + resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} + engines: {node: '>=8'} + to-regex-range@5.0.1: resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} engines: {node: '>=8.0'} @@ -381,9 +748,10 @@ packages: tslib@2.6.2: resolution: {integrity: sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==} - type-fest@0.20.2: - resolution: {integrity: sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==} - engines: {node: '>=10'} + tsx@4.16.2: + resolution: {integrity: sha512-C1uWweJDgdtX2x600HjaFaucXTilT7tgUZHbOE4+ypskZ1OP8CRCSDkCxG6Vya9EwaFIVagWwpaVAn5wzypaqQ==} + engines: {node: '>=18.0.0'} + hasBin: true typescript@5.5.4: resolution: {integrity: sha512-Mtq29sKDAEYP7aljRgtPOpTvOfbwRWlS6dPRzwjdE+C0R4brX/GUyhHSecbHMFLNBLcJIPt9nl9yG5TZ1weH+Q==} @@ -393,6 +761,9 @@ packages: undefsafe@2.0.5: resolution: {integrity: sha512-WxONCrssBM8TSPRqN5EmsjVrsv4A8X12J4ArBiiayv3DyyG3ZlIg6yysuuSYdZsVz3TKcTg2fd//Ujd4CHV1iA==} + undici-types@5.26.5: + resolution: {integrity: sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==} + undici-types@6.11.1: resolution: {integrity: sha512-mIDEX2ek50x0OlRgxryxsenE5XaQD4on5U2inY7RApK3SOJpofyw7uW2AyfMKkhAxXIceo2DeWGVGwyvng1GNQ==} @@ -400,13 +771,13 @@ packages: resolution: {integrity: sha512-Q2rtqmZWrbP8nePMq7mOJIN98M0fYvSgV89vwl/BQRT4mDOeY2GXZngfGpcBBhtky3woM7G24wZV3Q304Bv6cw==} engines: {node: '>=18.0'} - uuid@9.0.1: - resolution: {integrity: sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==} - hasBin: true - v8-compile-cache-lib@3.0.1: resolution: {integrity: sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==} + wrap-ansi@7.0.0: + resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==} + engines: {node: '>=10'} + ws@8.18.0: resolution: {integrity: sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==} engines: {node: '>=10.0.0'} @@ -423,12 +794,38 @@ packages: resolution: {integrity: sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==} engines: {node: '>=0.4'} + y18n@5.0.8: + resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==} + engines: {node: '>=10'} + + yargs-parser@21.1.1: + resolution: {integrity: sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==} + engines: {node: '>=12'} + + yargs@17.7.2: + resolution: {integrity: sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==} + engines: {node: '>=12'} + yn@3.1.1: resolution: {integrity: sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==} engines: {node: '>=6'} snapshots: + '@babel/code-frame@7.24.7': + dependencies: + '@babel/highlight': 7.24.7 + picocolors: 1.0.1 + + '@babel/helper-validator-identifier@7.24.7': {} + + '@babel/highlight@7.24.7': + dependencies: + '@babel/helper-validator-identifier': 7.24.7 + chalk: 2.4.2 + js-tokens: 4.0.0 + picocolors: 1.0.1 + '@cspotcode/source-map-support@0.8.1': dependencies: '@jridgewell/trace-mapping': 0.3.9 @@ -480,6 +877,77 @@ snapshots: - bufferutil - utf-8-validate + '@esbuild/aix-ppc64@0.21.5': + optional: true + + '@esbuild/android-arm64@0.21.5': + optional: true + + '@esbuild/android-arm@0.21.5': + optional: true + + '@esbuild/android-x64@0.21.5': + optional: true + + '@esbuild/darwin-arm64@0.21.5': + optional: true + + '@esbuild/darwin-x64@0.21.5': + optional: true + + '@esbuild/freebsd-arm64@0.21.5': + optional: true + + '@esbuild/freebsd-x64@0.21.5': + optional: true + + '@esbuild/linux-arm64@0.21.5': + optional: true + + '@esbuild/linux-arm@0.21.5': + optional: true + + '@esbuild/linux-ia32@0.21.5': + optional: true + + '@esbuild/linux-loong64@0.21.5': + optional: true + + '@esbuild/linux-mips64el@0.21.5': + optional: true + + '@esbuild/linux-ppc64@0.21.5': + optional: true + + '@esbuild/linux-riscv64@0.21.5': + optional: true + + '@esbuild/linux-s390x@0.21.5': + optional: true + + '@esbuild/linux-x64@0.21.5': + optional: true + + '@esbuild/netbsd-x64@0.21.5': + optional: true + + '@esbuild/openbsd-x64@0.21.5': + optional: true + + '@esbuild/sunos-x64@0.21.5': + optional: true + + '@esbuild/win32-arm64@0.21.5': + optional: true + + '@esbuild/win32-ia32@0.21.5': + optional: true + + '@esbuild/win32-x64@0.21.5': + optional: true + + '@graphile/logger@0.2.0': {} + '@jridgewell/resolve-uri@3.1.2': {} '@jridgewell/sourcemap-codec@1.5.0': {} @@ -506,10 +974,32 @@ snapshots: '@tsconfig/node16@1.0.4': {} + '@types/debug@4.1.12': + dependencies: + '@types/ms': 0.7.34 + + '@types/interpret@1.1.3': + dependencies: + '@types/node': 22.0.0 + + '@types/ms@0.7.34': {} + + '@types/node@20.14.13': + dependencies: + undici-types: 5.26.5 + '@types/node@22.0.0': dependencies: undici-types: 6.11.1 + '@types/pg@8.11.6': + dependencies: + '@types/node': 22.0.0 + pg-protocol: 1.6.1 + pg-types: 4.0.2 + + '@types/semver@7.5.8': {} + '@types/ws@8.5.11': dependencies: '@types/node': 22.0.0 @@ -522,10 +1012,15 @@ snapshots: acorn@8.12.1: {} - aggregate-error@3.1.0: + ansi-regex@5.0.1: {} + + ansi-styles@3.2.1: dependencies: - clean-stack: 2.2.0 - indent-string: 4.0.0 + color-convert: 1.9.3 + + ansi-styles@4.3.0: + dependencies: + color-convert: 2.0.1 anymatch@3.1.3: dependencies: @@ -534,6 +1029,8 @@ snapshots: arg@4.1.3: {} + argparse@2.0.1: {} + balanced-match@1.0.2: {} binary-extensions@2.3.0: {} @@ -547,6 +1044,19 @@ snapshots: dependencies: fill-range: 7.1.1 + callsites@3.1.0: {} + + chalk@2.4.2: + dependencies: + ansi-styles: 3.2.1 + escape-string-regexp: 1.0.5 + supports-color: 5.5.0 + + chalk@4.1.2: + dependencies: + ansi-styles: 4.3.0 + supports-color: 7.2.0 + chokidar@3.6.0: dependencies: anymatch: 3.1.3 @@ -559,15 +1069,36 @@ snapshots: optionalDependencies: fsevents: 2.3.3 - clean-stack@2.2.0: {} + cliui@8.0.1: + dependencies: + string-width: 4.2.3 + strip-ansi: 6.0.1 + wrap-ansi: 7.0.0 + + color-convert@1.9.3: + dependencies: + color-name: 1.1.3 + + color-convert@2.0.1: + dependencies: + color-name: 1.1.4 + + color-name@1.1.3: {} + + color-name@1.1.4: {} concat-map@0.0.1: {} - create-require@1.1.1: {} - - cron-parser@4.9.0: + cosmiconfig@8.3.6(typescript@5.5.4): dependencies: - luxon: 3.4.4 + import-fresh: 3.3.0 + js-yaml: 4.1.0 + parse-json: 5.2.0 + path-type: 4.0.0 + optionalDependencies: + typescript: 5.5.4 + + create-require@1.1.1: {} debug@4.3.6(supports-color@5.5.0): dependencies: @@ -575,8 +1106,6 @@ snapshots: optionalDependencies: supports-color: 5.5.0 - delay@5.0.0: {} - diff@4.0.2: {} discord-api-types@0.37.83: {} @@ -601,6 +1130,42 @@ snapshots: dotenv@16.4.5: {} + emoji-regex@8.0.0: {} + + error-ex@1.3.2: + dependencies: + is-arrayish: 0.2.1 + + esbuild@0.21.5: + optionalDependencies: + '@esbuild/aix-ppc64': 0.21.5 + '@esbuild/android-arm': 0.21.5 + '@esbuild/android-arm64': 0.21.5 + '@esbuild/android-x64': 0.21.5 + '@esbuild/darwin-arm64': 0.21.5 + '@esbuild/darwin-x64': 0.21.5 + '@esbuild/freebsd-arm64': 0.21.5 + '@esbuild/freebsd-x64': 0.21.5 + '@esbuild/linux-arm': 0.21.5 + '@esbuild/linux-arm64': 0.21.5 + '@esbuild/linux-ia32': 0.21.5 + '@esbuild/linux-loong64': 0.21.5 + '@esbuild/linux-mips64el': 0.21.5 + '@esbuild/linux-ppc64': 0.21.5 + '@esbuild/linux-riscv64': 0.21.5 + '@esbuild/linux-s390x': 0.21.5 + '@esbuild/linux-x64': 0.21.5 + '@esbuild/netbsd-x64': 0.21.5 + '@esbuild/openbsd-x64': 0.21.5 + '@esbuild/sunos-x64': 0.21.5 + '@esbuild/win32-arm64': 0.21.5 + '@esbuild/win32-ia32': 0.21.5 + '@esbuild/win32-x64': 0.21.5 + + escalade@3.1.2: {} + + escape-string-regexp@1.0.5: {} + fast-deep-equal@3.1.3: {} fill-range@7.1.1: @@ -610,15 +1175,60 @@ snapshots: fsevents@2.3.3: optional: true + get-caller-file@2.0.5: {} + + get-tsconfig@4.7.6: + dependencies: + resolve-pkg-maps: 1.0.0 + glob-parent@5.1.2: dependencies: is-glob: 4.0.3 + graphile-config@0.0.1-beta.9: + dependencies: + '@types/interpret': 1.1.3 + '@types/node': 20.14.13 + '@types/semver': 7.5.8 + chalk: 4.1.2 + debug: 4.3.6(supports-color@5.5.0) + interpret: 3.1.1 + semver: 7.6.3 + tslib: 2.6.2 + yargs: 17.7.2 + transitivePeerDependencies: + - supports-color + + graphile-worker@0.16.6(typescript@5.5.4): + dependencies: + '@graphile/logger': 0.2.0 + '@types/debug': 4.1.12 + '@types/pg': 8.11.6 + cosmiconfig: 8.3.6(typescript@5.5.4) + graphile-config: 0.0.1-beta.9 + json5: 2.2.3 + pg: 8.12.0 + tslib: 2.6.2 + yargs: 17.7.2 + transitivePeerDependencies: + - pg-native + - supports-color + - typescript + has-flag@3.0.0: {} + has-flag@4.0.0: {} + ignore-by-default@1.0.1: {} - indent-string@4.0.0: {} + import-fresh@3.3.0: + dependencies: + parent-module: 1.0.1 + resolve-from: 4.0.0 + + interpret@3.1.1: {} + + is-arrayish@0.2.1: {} is-binary-path@2.1.0: dependencies: @@ -626,20 +1236,30 @@ snapshots: is-extglob@2.1.1: {} + is-fullwidth-code-point@3.0.0: {} + is-glob@4.0.3: dependencies: is-extglob: 2.1.1 is-number@7.0.0: {} - lodash.debounce@4.0.8: {} + js-tokens@4.0.0: {} + + js-yaml@4.1.0: + dependencies: + argparse: 2.0.1 + + json-parse-even-better-errors@2.3.1: {} + + json5@2.2.3: {} + + lines-and-columns@1.2.4: {} lodash.snakecase@4.1.1: {} lodash@4.17.21: {} - luxon@3.4.4: {} - magic-bytes.js@1.10.0: {} make-error@1.3.6: {} @@ -665,21 +1285,20 @@ snapshots: normalize-path@3.0.0: {} - p-map@4.0.0: - dependencies: - aggregate-error: 3.1.0 + obuf@1.1.2: {} - pg-boss@9.0.3: + parent-module@1.0.1: dependencies: - cron-parser: 4.9.0 - delay: 5.0.0 - lodash.debounce: 4.0.8 - p-map: 4.0.0 - pg: 8.12.0 - serialize-error: 8.1.0 - uuid: 9.0.1 - transitivePeerDependencies: - - pg-native + callsites: 3.1.0 + + parse-json@5.2.0: + dependencies: + '@babel/code-frame': 7.24.7 + error-ex: 1.3.2 + json-parse-even-better-errors: 2.3.1 + lines-and-columns: 1.2.4 + + path-type@4.0.0: {} pg-cloudflare@1.1.1: optional: true @@ -688,6 +1307,8 @@ snapshots: pg-int8@1.0.1: {} + pg-numeric@1.0.2: {} + pg-pool@3.6.2(pg@8.12.0): dependencies: pg: 8.12.0 @@ -702,6 +1323,16 @@ snapshots: postgres-date: 1.0.7 postgres-interval: 1.2.0 + pg-types@4.0.2: + dependencies: + pg-int8: 1.0.1 + pg-numeric: 1.0.2 + postgres-array: 3.0.2 + postgres-bytea: 3.0.0 + postgres-date: 2.1.0 + postgres-interval: 3.0.0 + postgres-range: 1.1.4 + pg@8.12.0: dependencies: pg-connection-string: 2.6.4 @@ -716,29 +1347,45 @@ snapshots: dependencies: split2: 4.2.0 + picocolors@1.0.1: {} + picomatch@2.3.1: {} postgres-array@2.0.0: {} + postgres-array@3.0.2: {} + postgres-bytea@1.0.0: {} + postgres-bytea@3.0.0: + dependencies: + obuf: 1.1.2 + postgres-date@1.0.7: {} + postgres-date@2.1.0: {} + postgres-interval@1.2.0: dependencies: xtend: 4.0.2 + postgres-interval@3.0.0: {} + + postgres-range@1.1.4: {} + pstree.remy@1.1.8: {} readdirp@3.6.0: dependencies: picomatch: 2.3.1 - semver@7.6.3: {} + require-directory@2.1.1: {} - serialize-error@8.1.0: - dependencies: - type-fest: 0.20.2 + resolve-from@4.0.0: {} + + resolve-pkg-maps@1.0.0: {} + + semver@7.6.3: {} simple-update-notifier@2.0.0: dependencies: @@ -746,10 +1393,24 @@ snapshots: split2@4.2.0: {} + string-width@4.2.3: + dependencies: + emoji-regex: 8.0.0 + is-fullwidth-code-point: 3.0.0 + strip-ansi: 6.0.1 + + strip-ansi@6.0.1: + dependencies: + ansi-regex: 5.0.1 + supports-color@5.5.0: dependencies: has-flag: 3.0.0 + supports-color@7.2.0: + dependencies: + has-flag: 4.0.0 + to-regex-range@5.0.1: dependencies: is-number: 7.0.0 @@ -778,22 +1439,47 @@ snapshots: tslib@2.6.2: {} - type-fest@0.20.2: {} + tsx@4.16.2: + dependencies: + esbuild: 0.21.5 + get-tsconfig: 4.7.6 + optionalDependencies: + fsevents: 2.3.3 typescript@5.5.4: {} undefsafe@2.0.5: {} + undici-types@5.26.5: {} + undici-types@6.11.1: {} undici@6.13.0: {} - uuid@9.0.1: {} - v8-compile-cache-lib@3.0.1: {} + wrap-ansi@7.0.0: + dependencies: + ansi-styles: 4.3.0 + string-width: 4.2.3 + strip-ansi: 6.0.1 + ws@8.18.0: {} xtend@4.0.2: {} + y18n@5.0.8: {} + + yargs-parser@21.1.1: {} + + yargs@17.7.2: + dependencies: + cliui: 8.0.1 + escalade: 3.1.2 + get-caller-file: 2.0.5 + require-directory: 2.1.1 + string-width: 4.2.3 + y18n: 5.0.8 + yargs-parser: 21.1.1 + yn@3.1.1: {} diff --git a/services/bot/src/commands/utilities/donger.ts b/services/bot/src/commands/utilities/donger.ts index 4c64f1b..e86a524 100644 --- a/services/bot/src/commands/utilities/donger.ts +++ b/services/bot/src/commands/utilities/donger.ts @@ -37,7 +37,7 @@ export default { data: new SlashCommandBuilder() .setName('donger') .setDescription('Replies with a free donger!'), - async execute(interaction: ChatInputCommandInteraction): Promise { + async execute({ interaction }: { interaction: ChatInputCommandInteraction}): Promise { await interaction.reply({ content: dongers[Math.floor(Math.random()*dongers.length)] }); diff --git a/services/bot/src/commands/utilities/record.ts b/services/bot/src/commands/utilities/record.ts index 2909bf3..123589c 100644 --- a/services/bot/src/commands/utilities/record.ts +++ b/services/bot/src/commands/utilities/record.ts @@ -9,6 +9,7 @@ import { import type { ExecuteArguments } from '../../index.js'; +if (!process.env.AUTOMATION_USER_JWT) throw new Error(`AUTOMATION_USER_JWT was missing from env`); export default { data: new SlashCommandBuilder() @@ -20,9 +21,8 @@ export default { .setDescription('The channel URL to record') .setRequired(true) ), - async execute({ interaction, boss }: ExecuteArguments): Promise { + async execute({ interaction, workerUtils }: ExecuteArguments): Promise { const url = interaction.options.getString('url') - const jobId = await boss.send('record', { url }) // const row = new ActionRowBuilder() // .addComponents(component); @@ -59,7 +59,7 @@ export default { const idk = await interaction.reply({ - content: `Recording queued. jobId=${jobId}`, + content: `Recording ${url}`, embeds: [ statusEmbed ], @@ -71,19 +71,10 @@ export default { // console.log('the following is idk, the return value from interaction.reply') // console.log(idk) - const message = await idk.fetch(); - console.log('the following is the message, retrieved by awaiting idk.fetch()') - console.log(message) + const message = await idk.fetch() + const discordMessageId = message.id + await workerUtils.addJob('startRecording', { url, discordMessageId }) - // create a Discord Interaction in the db which relates the discord message to the record Job. - // we can later use the record to update the status displayed in the discord message - - await fetch(`${STRAPI_URL}/api/discord-interaction`) - // 1267182888403861608 - // await interaction.reply({ components: [row] }); - // await interaction.reply({ - // content: `Recording queued. ID ${jobId}` - // }); }, }; diff --git a/services/bot/src/index.ts b/services/bot/src/index.ts index e4e6d08..a78babc 100644 --- a/services/bot/src/index.ts +++ b/services/bot/src/index.ts @@ -2,30 +2,34 @@ import 'dotenv/config' import { type ChatInputCommandInteraction, Client, Events, GatewayIntentBits, Partials } from 'discord.js' import loadCommands from './loadCommands.js' import deployCommands from './deployCommands.js' -import PgBoss from 'pg-boss' +import discordMessageUpdate from './tasks/discordMessageUpdate.js' +import { makeWorkerUtils, type WorkerUtils } from 'graphile-worker' export interface ExecuteArguments { interaction: ChatInputCommandInteraction; - boss: PgBoss; + workerUtils: WorkerUtils } +if (!process.env.AUTOMATION_USER_JWT) throw new Error(`AUTOMATION_USER_JWT was missing from env`); if (!process.env.DISCORD_TOKEN) throw new Error("DISCORD_TOKEN was missing from env"); if (!process.env.DISCORD_CHANNEL_ID) throw new Error("DISCORD_CHANNEL_ID was missing from env"); -if (!process.env.PGBOSS_URL) throw new Error("PGBOSS_URL was missing from env"); -const connectionString = process.env.PGBOSS_URL! +if (!process.env.WORKER_CONNECTION_STRING) throw new Error("WORKER_CONNECTION_STRING was missing from env"); +async function setupGraphileWorker() { + const workerUtils = await makeWorkerUtils({ + connectionString: process.env.WORKER_CONNECTION_STRING!, + }); + await workerUtils.migrate() + return workerUtils +} -async function setup(commands: any[]) { +async function setupDiscordBot(commands: any[], workerUtils: WorkerUtils) { + console.log(`setup()`) if (!commands) throw new Error('commands passed to setup() was missing'); - const channelId = '' + process.env.DISCORD_CHANNEL_ID + - // setup pg-boss - const boss = new PgBoss({ connectionString }) - boss.on('error', (err: any) => console.error(err)) - await boss.start() - - // Create a new client instance + console.log(`Create a new client instance`) const client = new Client({ intents: [ GatewayIntentBits.Guilds, @@ -44,13 +48,18 @@ async function setup(commands: any[]) { if (!interaction.isChatInputCommand()) return; const { commandName } = interaction; console.log(`Received interaction with commandName=${commandName}`) - commands.find((c) => c.data.name === commandName).execute({ interaction, boss }) + const cmd = commands.find((c) => c.data.name === commandName) + if (!cmd) { + console.log(`no command handler matches commandName=${commandName}`) + return; + } + cmd.execute({ interaction, workerUtils }) }); // When the client is ready, run this code (only once). // The distinction between `client: Client` and `readyClient: Client` is important for TypeScript developers. // It makes some properties non-nullable. client.once(Events.ClientReady, readyClient => { - console.log(`Ready! Logged in as ${readyClient.user.tag} coño!`); + console.log(`Ready! Logged in as ${readyClient.user.tag}`); // client.channels.cache.get(process.env.DISCORD_CHANNEL_ID).send('testing 123'); // readyClient.channels.fetch(channelId).then(channel => { // channel.send('generic welcome message!') @@ -98,11 +107,17 @@ async function setup(commands: any[]) { } async function main() { + console.log(`main()`) const commands = await loadCommands() if (!commands) throw new Error('there were no commands available to be loaded.'); await deployCommands(commands.map((c) => c.data.toJSON())) console.log(`${commands.length} commands deployed: ${commands.map((c) => c.data.name).join(', ')}`) - setup(commands) + const workerUtils = await setupGraphileWorker() + setupDiscordBot(commands, workerUtils) } -main() +main().catch((e) => { + console.error("error during main() function") + console.error(e) + process.exit(3) +}) diff --git a/services/bot/src/loadCommands.ts b/services/bot/src/loadCommands.ts index 97d1357..32fdce3 100644 --- a/services/bot/src/loadCommands.ts +++ b/services/bot/src/loadCommands.ts @@ -13,8 +13,8 @@ export default async function loadCommands(): Promise { for (const folder of commandFolders) { const commandsPath = path.join(foldersPath, folder); - const commandFiles = fs.readdirSync(commandsPath).filter(file => file.endsWith('.ts')); - // console.log(`commandFiles=${commandFiles}`) + const commandFiles = fs.readdirSync(commandsPath).filter(file => file.endsWith('.ts') || file.endsWith('.js')); + console.log(`commandFiles=${commandFiles}`) // console.log(`Grab the SlashCommandBuilder#toJSON() output of each command's data for deployment`) for (const file of commandFiles) { const filePath = path.join(commandsPath, file); diff --git a/services/bot/src/tasks/discordMessageUpdate.ts b/services/bot/src/tasks/discordMessageUpdate.ts new file mode 100644 index 0000000..446ea34 --- /dev/null +++ b/services/bot/src/tasks/discordMessageUpdate.ts @@ -0,0 +1,106 @@ +import 'dotenv/config' +import { type Task, type WorkerUtils } from 'graphile-worker'; +import { type ChatInputCommandInteraction, Client, Events, GatewayIntentBits, Partials, Message, TextChannel } from 'discord.js' + + +// export interface DiscordMessageUpdateJob extends Job { +// data: { +// captureJobId: string; +// } +// } + +interface Payload { + discord_message_id: string; + capture_job_id: string; +} + +function assertPayload(payload: any): asserts payload is Payload { + if (typeof payload !== "object" || !payload) throw new Error("invalid payload"); + if (typeof payload.discord_message_id !== "string") throw new Error("invalid discord_message_id"); + if (typeof payload.capture_job_id.to !== "string") throw new Error("invalid capture_job_id"); +} + + +if (!process.env.AUTOMATION_USER_JWT) throw new Error(`AUTOMATION_USER_JWT was missing from env`); +if (!process.env.DISCORD_TOKEN) throw new Error("DISCORD_TOKEN was missing from env"); +if (!process.env.DISCORD_CHANNEL_ID) throw new Error("DISCORD_CHANNEL_ID was missing from env"); +if (!process.env.DISCORD_GUILD_ID) throw new Error("DISCORD_GUILD_ID was missing from env"); + +/** + * discordMessageUpdate is the task where we edit a previously sent discord message to display new status information sent to us from @futureporn/capture + * + * Sometimes the update is changing the state, one of Pending|Recording|Aborted|Ended. + * Sometimes the update is updating the Filesize of the recording in-progress + * Sometimes the update is adding a thumbnail image to the message + */ +export default async function discordMessageUpdate (payload: Payload) { + assertPayload(payload) + // const { captureJobId } = job.data + console.log(`discordMessageUpdate job has begun with captureJobId=${payload.capture_job_id}`) + + + // // find the discord_interactions record containing the captureJobId + // const res = await fetch(`http://postgrest.futureporn.svc.cluster.local:9000/discord_interactions?capture_job_id=eq.${captureJobId}`) + // if (!res.ok) throw new Error('failed to fetch the discord_interactions'); + // const body = await res.json() as DiscordInteraction + // console.log('discord_interactions as follows') + // console.log(body) + + // // create a discord.js client + // const client = new Client({ + // intents: [ + // GatewayIntentBits.Guilds, + // GatewayIntentBits.GuildMessages + // ], + // partials: [ + // Partials.Message, + // Partials.Channel, + // ] + // }); + + // // const messageManager = client. + // // const guild = client.guilds.cache.get(process.env.DISCORD_GUILD_ID!); + // // if (!guild) throw new Error('guild was undefined') + + + // const channel = await client.channels.fetch(process.env.DISCORD_CHANNEL_ID!) as TextChannel + // if (!channel) throw new Error(`discord channel was undefined`); + + // // console.log('we got the following channel') + // // console.log(channel) + + // const message = await channel.messages.fetch(body.discord_message_id) + + // console.log('we got the following message') + // console.log(message) + + // get the message + // client.channel.messages.get() + // client.rest. + + + + // const message: Message = { + // id: body.discord_message_id + // }; + // await message.fetchReference() + // message.edit('test message update payload thingy') + + + // client.rest.updateMessage(message, "My new content"); + // TextChannel.fetchMessage(msgId).then(console.log); + + + + // TextChannel + + // channel.messages.fetch(`Your Message ID`).then(message => { + // message.edit("New message Text"); + // }).catch(err => { + // console.error(err); + // }); + + // using the discord_interaction's discord_message_id, use discord.js to update the discord message. + + +} diff --git a/services/capture/src/Record.spec.ts b/services/capture/src/Record.spec.ts index 0ce4ae7..434f661 100644 --- a/services/capture/src/Record.spec.ts +++ b/services/capture/src/Record.spec.ts @@ -45,12 +45,26 @@ describe('Record', function () { s3ClientMock.on(CreateMultipartUploadCommand).resolves({UploadId: '1'}); s3ClientMock.on(UploadPartCommand).resolves({ETag: '1'}); const s3Client = new S3Client({ region: 'us-west-000' }) - const record = new Record({ inputStream, s3Client, channel: 'coolguy_69', bucket: 'test' }) + const jobId = 'test-job-1234' + const record = new Record({ inputStream, s3Client, bucket: 'test', jobId }) await record.start() expect(record).to.have.property('counter', 192627) expect(record).to.have.property('bucket', 'test') }) + it('should be abortable', async function () { + const inputStream = createReadStream(join(__dirname, './fixtures/mock-stream0.mp4')) // 192627 bytes + const s3ClientMock = mockClient(S3Client) + const s3Client = new S3Client({ region: 'us-west-000' }) + s3ClientMock.on(CreateMultipartUploadCommand).resolves({UploadId: '1'}); + s3ClientMock.on(UploadPartCommand).resolves({ETag: '1'}); + const jobId = 'test-job-3456' + const record = new Record({ inputStream, s3Client, jobId, bucket: 'test' }) + await record.start() + expect(record).to.have.property('abortController') + await record.abort() + }) + xit('should restart if a EPIPE is encountered', async function () { // @todo IDK how to implement this. const inputStream = createReadStream(join(__dirname, './fixtures/mock-stream0.mp4')) diff --git a/services/capture/src/Record.ts b/services/capture/src/Record.ts index c64a00f..08ed581 100644 --- a/services/capture/src/Record.ts +++ b/services/capture/src/Record.ts @@ -1,10 +1,9 @@ import { spawn } from 'child_process'; -import { PassThrough, pipeline, Readable, Writable } from 'stream'; +import { PassThrough, pipeline, Readable } from 'stream'; import prettyBytes from 'pretty-bytes'; import { Upload } from "@aws-sdk/lib-storage"; import { S3Client } from "@aws-sdk/client-s3"; import 'dotenv/config' -import { createWriteStream } from 'fs'; const ua0 = 'Mozilla/5.0 (X11; Linux x86_64; rv:105.0) Gecko/20100101 Firefox/105.0' @@ -128,9 +127,9 @@ export default class Record { } }); - console.log('awaiting parallelUploads3.done()...') + console.log('Waiting for parallelUploads3 to finish...') await parallelUploads3.done(); - console.log('parallelUploads3.done() is complete.') + console.log('parallelUploads3 is complete.') } catch (e) { if (e instanceof Error) { diff --git a/services/capture/src/app.ts b/services/capture/src/app.ts index 919dfb9..7531f0c 100644 --- a/services/capture/src/app.ts +++ b/services/capture/src/app.ts @@ -2,8 +2,7 @@ import fastify, { type FastifyRequest } from 'fastify' import { getPackageVersion } from '@futureporn/utils' -import pgbossPlugin, { type ExtendedFastifyInstance } from './fastify-pgboss-plugin.ts' -import PgBoss from 'pg-boss' +import fastifyGraphileWorkerPlugin, { type ExtendedFastifyInstance } from './fastify-graphile-worker-plugin.ts' import { join, dirname } from 'node:path' import { fileURLToPath } from 'node:url' @@ -12,28 +11,28 @@ const __dirname = dirname(fileURLToPath(import.meta.url)); const version = getPackageVersion(join(__dirname, '../package.json')) interface RecordBodyType { url: string; - channel: string; + discordMessageId: string; } -const build = function (opts: Record={}, boss: PgBoss) { +const build = function (opts: Record={}, connectionString: string) { const app: ExtendedFastifyInstance = fastify(opts) - app.register(pgbossPlugin, { boss }) + app.register(fastifyGraphileWorkerPlugin, { connectionString }) app.get('/', async function (request, reply) { return { app: '@futureporn/capture', version } }) app.post('/api/record', async function (request: FastifyRequest<{ Body: RecordBodyType }>, reply) { - const { url, channel } = request.body + const { url, discordMessageId } = request.body console.log(`POST /api/record with url=${url}`) - if (app?.boss) { - const jobId = await app.boss.send('record', { + if (app?.graphile) { + const jobId = await app.graphile.addJob('startRecording', { url, - channel + discordMessageId }) return { jobId } } else { - console.error(`app.boss was missing! Is the pgboss plugin registered to the fastify instance?`) + console.error(`app.graphile was missing! Is the graphile worker plugin registered to the fastify instance?`) } return { 'idk': true } }) diff --git a/services/capture/src/index.ts b/services/capture/src/index.ts index 90298ef..e59345b 100644 --- a/services/capture/src/index.ts +++ b/services/capture/src/index.ts @@ -4,21 +4,33 @@ import { build } from './app.ts' import 'dotenv/config' -import PgBoss, { Job } from 'pg-boss' -import { dirname } from 'node:path'; +import { makeWorkerUtils, type WorkerUtils, Runner, RunnerOptions, run as graphileRun } from 'graphile-worker' +import { join, dirname } from 'node:path'; import { fileURLToPath } from 'url'; -import record, { type RecordJob } from './tasks/record.ts' +import { getPackageVersion } from '@futureporn/utils'; +import type { GraphileConfig } from "graphile-config"; +import type {} from "graphile-worker"; +import startRecording from './tasks/startRecording.ts'; +import { stopRecording } from './tasks/stopRecording.ts'; +import record from './tasks/record.ts' const __dirname = dirname(fileURLToPath(import.meta.url)); +const version = getPackageVersion(join(__dirname, '../package.json')) -if (!process.env.PGBOSS_URL) throw new Error('PGBOSS_URL is missing in env'); if (!process.env.FUNCTION) throw new Error(`FUNCTION env var was missing. FUNCTION env var must be either 'api' or 'worker'.`); -const connectionString = process.env.PGBOSS_URL! +if (!process.env.WORKER_CONNECTION_STRING) throw new Error(`WORKER_CONNECTION_STRING env var was missing`); +const connectionString = process.env.WORKER_CONNECTION_STRING! const concurrency = (process.env?.WORKER_CONCURRENCY) ? parseInt(process.env.WORKER_CONCURRENCY) : 1 +const preset: GraphileConfig.Preset = { + worker: { + connectionString: process.env.WORKER_CONNECTION_STRING, + concurrentJobs: concurrency, + fileExtensions: [".js", ".ts"], + }, +}; - -async function api(boss: PgBoss) { +async function api() { if (!process.env.PORT) throw new Error('PORT is missing in env'); console.log(`api FUNCTION listening on PORT ${process.env.PORT}`) const PORT = parseInt(process.env.PORT!) @@ -32,7 +44,7 @@ async function api(boss: PgBoss) { } } - const server = build(fastifyOpts, boss) + const server = build(fastifyOpts, connectionString) server.listen({ port: PORT }, (err) => { if (err) { @@ -42,42 +54,34 @@ async function api(boss: PgBoss) { }) } -async function worker(boss: PgBoss) { - const queue = 'record' - const batchSize = 20 - const options = { - teamSize: 1, - teamConcurrency: concurrency, - batchSize +async function worker(workerUtils: WorkerUtils) { + const runnerOptions: RunnerOptions = { + preset, + concurrency, + // taskDirectory: join(__dirname, 'tasks'), + taskList: { + 'record': record, + 'startRecording': startRecording, + 'stopRecording': stopRecording + } } - await boss.work(queue, options, (job: RecordJob[]) => record(job)) + + const runner = await graphileRun(runnerOptions) + if (!runner) throw new Error('failed to initialize graphile worker'); + await runner.promise } async function main() { - const boss = new PgBoss({ - connectionString - }) - boss.on('error', (err: any) => console.error(err)) - boss.on('wip', (wip: any) => { - console.log('wip event was received.') - console.log(wip) - }) - boss.on('stopped', () => { - console.log('stopped event was received.') - }) - boss.on('monitor-states', (data: any) => { - console.log('monitor-states event was received.') - console.log(data) - }) - - await boss.start() + const workerUtils = await makeWorkerUtils({ connectionString }) + await workerUtils.migrate() + console.log(`@futureporn/capture version ${version} (FUNCTION=${process.env.FUNCTION})`) if (process.env.FUNCTION === 'api') { - api(boss) + api() } else if (process.env.FUNCTION === 'worker') { - worker(boss) + worker(workerUtils) } else { throw new Error('process.env.FUNCTION must be either api or worker. got '+process.env.FUNCTION) } diff --git a/services/capture/src/tasks/record.ts b/services/capture/src/tasks/record.ts index 950dd04..6f47b15 100644 --- a/services/capture/src/tasks/record.ts +++ b/services/capture/src/tasks/record.ts @@ -1,76 +1,99 @@ + +import { Helpers, type Task } from 'graphile-worker' import Record from '../Record.ts' import { getPlaylistUrl } from '@futureporn/scout/ytdlp.ts' -import 'dotenv/config' -import { type Job } from 'pg-boss' -import { backOff } from "exponential-backoff" -export interface RecordJob extends Job { - data: { - url: string; - } + +/** + * url is the URL to be recorded. Ex: chaturbate.com/projektmelody + * recordId is the ID of the record record in postgres + * we use the ID to poll the db to see if the job is aborted by the user + */ +interface Payload { + url: string, + recordId: number } -async function _record (job: RecordJob, retries?: number): Promise { - +interface RecordingRecord { + id: number; + isAborted: boolean; +} - if (!process.env.S3_BUCKET_NAME) throw new Error('S3_BUCKET_NAME was undefined in env'); - if (!process.env.S3_ENDPOINT) throw new Error('S3_ENDPOINT was undefined in env'); - if (!process.env.S3_REGION) throw new Error('S3_REGION was undefined in env'); - if (!process.env.S3_ACCESS_KEY_ID) throw new Error('S3_ACCESS_KEY_ID was undefined in env'); - if (!process.env.S3_SECRET_ACCESS_KEY) throw new Error('S3_SECRET_ACCESS_KEY was undefined in env'); +function assertPayload(payload: any): asserts payload is Payload { + if (typeof payload !== "object" || !payload) throw new Error("invalid payload"); + if (typeof payload.url !== "string") throw new Error("invalid url"); + if (typeof payload.recordId !== "number") throw new Error("invalid recordId"); +} - if (!job) throw new Error('Job sent to job worker execution callback was empty!!!'); - const { url } = job.data; - console.log(`'record' job ${job!.id} begin with url=${url}`) +function assertEnv() { + if (!process.env.S3_ACCESS_KEY_ID) throw new Error('S3_ACCESS_KEY_ID was missing in env'); + if (!process.env.S3_SECRET_ACCESS_KEY) throw new Error('S3_SECRET_ACCESS_KEY was missing in env'); + if (!process.env.S3_REGION) throw new Error('S3_REGION was missing in env'); + if (!process.env.S3_ENDPOINT) throw new Error('S3_ENDPOINT was missing in env'); + if (!process.env.S3_BUCKET) throw new Error('S3_BUCKET was missing in env'); + if (!process.env.POSTGREST_URL) throw new Error('POSTGREST_URL was missing in env'); +} - - const bucket = process.env.S3_BUCKET_NAME! - const endpoint = process.env.S3_ENDPOINT! - const region = process.env.S3_REGION! - const accessKeyId = process.env.S3_ACCESS_KEY_ID! - const secretAccessKey = process.env.S3_SECRET_ACCESS_KEY! - - let playlistUrl - try { - playlistUrl = await getPlaylistUrl(url) - console.log(`playlistUrl=${playlistUrl}`) - } catch (e) { - console.error('error during getPlaylistUrl()') - console.error(e) - throw e - } - - const jobId = job.id +async function getRecording(url: string, recordId: number, abortSignal: AbortSignal) { + const accessKeyId = process.env.S3_ACCESS_KEY_ID!; + const secretAccessKey = process.env.S3_SECRET_ACCESS_KEY!; + const region = process.env.S3_REGION!; + const endpoint = process.env.S3_ENDPOINT!; + const bucket = process.env.S3_BUCKET!; + const playlistUrl = await getPlaylistUrl(url) const s3Client = Record.makeS3Client({ accessKeyId, secretAccessKey, region, endpoint }) const inputStream = Record.getFFmpegStream({ url: playlistUrl }) - const record = new Record({ inputStream, bucket, s3Client, jobId }) - await record.start() - - console.log(`record job ${job.id} complete`) - - return job.id - + const record = new Record({ inputStream, bucket, s3Client, jobId: ''+recordId, abortSignal }) + record.start() + return record } -export default async function main (jobs: RecordJob[]): Promise { - // @todo why are we passed multiple jobs? I'm expecting only one. - const backOffOptions = { - numOfAttempts: 5, - startingDelay: 5000, - retry: (e: any, attemptNumber: number) => { - console.log(`Record Job is retrying. Attempt number ${attemptNumber}. e=${JSON.stringify(e, null, 2)}`) - return true +async function checkIfAborted(recordId: number): Promise { + const res = await fetch(`${process.env.POSTGREST_URL}/records?id.eq=${recordId}`, { + headers: { + 'Content-Type': 'application/json', + 'Accepts': 'application/json' } + }) + if (!res.ok) { + throw new Error(`failed to checkIfAborted. status=${res.status}, statusText=${res.statusText}`); } - for (const j of jobs) { - console.log(`record job ${j.id} GO GO GO`) - try { - await backOff(() => _record(j), backOffOptions) - } catch (e) { - console.warn(`record job ${j.id} encountered the following error.`) - console.error(e) - } - console.log(`record job ${j.id} is finished.`) + const body = await res.json() as RecordingRecord[]; + if (!body[0]) throw new Error(`failed to get a record that matched recordId=${recordId}`) + return body[0].isAborted +} + + +export const record: Task = async function (payload, helpers) { + assertPayload(payload) + assertEnv() + const { url, recordId } = payload + const abortController = new AbortController() + let interval + try { + const record = await getRecording(url, recordId, abortController.signal) + // every 30s, poll db to see if our job has been aborted by the user + interval = setInterval(async () => { + const isAborted = await checkIfAborted(recordId) + if (isAborted) { + abortController.abort() + } + }, 30000) + } finally { + clearInterval(interval) } -}; \ No newline at end of file + + // const recordId = await createRecordingRecord(payload, helpers) + // const { url } = payload; + // console.log(`@todo simulated start_recording with url=${url}, recordId=${recordId}`) + // await helpers.addJob('record', { url, recordId }) +} + +// // export default record +// export default function (payload: Payload, helpers: Helpers) { +// helpers.logger.info('WHEEEEEEEEEEEEEEEE (record.ts task executor)') +// } + + +export default record \ No newline at end of file diff --git a/services/capture/src/tasks/recording_start.ts.old b/services/capture/src/tasks/recording_start.ts.old new file mode 100644 index 0000000..4278bba --- /dev/null +++ b/services/capture/src/tasks/recording_start.ts.old @@ -0,0 +1,77 @@ +import Record from '../Record.ts' +import { getPlaylistUrl } from '@futureporn/scout/ytdlp.ts' +import 'dotenv/config' +import { type Job } from 'pg-boss' +import { backOff } from 'exponential-backoff' + +export interface RecordJob extends Job { + data: { + url: string; + } +} + +async function _record (job: RecordJob, retries?: number): Promise { + + + if (!process.env.S3_BUCKET_NAME) throw new Error('S3_BUCKET_NAME was undefined in env'); + if (!process.env.S3_ENDPOINT) throw new Error('S3_ENDPOINT was undefined in env'); + if (!process.env.S3_REGION) throw new Error('S3_REGION was undefined in env'); + if (!process.env.S3_ACCESS_KEY_ID) throw new Error('S3_ACCESS_KEY_ID was undefined in env'); + if (!process.env.S3_SECRET_ACCESS_KEY) throw new Error('S3_SECRET_ACCESS_KEY was undefined in env'); + + if (!job) throw new Error('Job sent to job worker execution callback was empty!!!'); + const { url } = job.data; + console.log(`'record' job ${job!.id} begin with url=${url}`) + + + const bucket = process.env.S3_BUCKET_NAME! + const endpoint = process.env.S3_ENDPOINT! + const region = process.env.S3_REGION! + const accessKeyId = process.env.S3_ACCESS_KEY_ID! + const secretAccessKey = process.env.S3_SECRET_ACCESS_KEY! + + let playlistUrl + try { + playlistUrl = await getPlaylistUrl(url) + console.log(`playlistUrl=${playlistUrl}`) + } catch (e) { + console.error('error during getPlaylistUrl()') + console.error(e) + throw e + } + + const jobId = job.id + const s3Client = Record.makeS3Client({ accessKeyId, secretAccessKey, region, endpoint }) + const inputStream = Record.getFFmpegStream({ url: playlistUrl }) + const record = new Record({ inputStream, bucket, s3Client, jobId }) + + await record.start() + + console.log(`record job ${job.id} complete`) + + return job.id + +} + + +export default async function main (jobs: RecordJob[]): Promise { + // @todo why are we passed multiple jobs? I'm expecting only one. + const backOffOptions = { + numOfAttempts: 5, + startingDelay: 5000, + retry: (e: any, attemptNumber: number) => { + console.log(`Record Job is retrying. Attempt number ${attemptNumber}. e=${JSON.stringify(e, null, 2)}`) + return true + } + } + for (const j of jobs) { + console.log(`record job ${j.id} GO GO GO`) + try { + await backOff(() => _record(j), backOffOptions) + } catch (e) { + console.warn(`record job ${j.id} encountered the following error.`) + console.error(e) + } + console.log(`record job ${j.id} is finished.`) + } +}; \ No newline at end of file diff --git a/services/capture/src/tasks/startRecording.ts b/services/capture/src/tasks/startRecording.ts new file mode 100644 index 0000000..c95cdb0 --- /dev/null +++ b/services/capture/src/tasks/startRecording.ts @@ -0,0 +1,60 @@ + +import { Helpers, type Task } from 'graphile-worker' + +/** + * url is the URL to be recorded. Ex: chaturbate.com/projektmelody + * discordMessageId is the ID of the discord messate which displays recording status. + * we use the ID to update the message later, and/or relate button press events to this record task + */ +interface Payload { + url: string; + discordMessageId: string; + isAborted: boolean; +} + +function assertPayload(payload: any): asserts payload is Payload { + if (typeof payload !== "object" || !payload) throw new Error("invalid payload"); + if (typeof payload.url !== "string") throw new Error("invalid url"); + if (typeof payload.discordMessageId !== "string") throw new Error("invalid discordMessageId"); +} + +function assertEnv() { + if (!process.env.AUTOMATION_USER_JWT) throw new Error('AUTOMATION_USER_JWT was missing in env'); +} + +async function createRecordingRecord(payload: Payload, helpers: Helpers): Promise { + const { url, discordMessageId } = payload + const record = { + url, + discordMessageId, + isAborted: false + } + const res = await fetch('http://postgrest.futureporn.svc.cluster.local:9000/records', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'Authorization': `Bearer ${process.env.AUTOMATION_USER_JWT}`, + 'Prefer': 'return=headers-only' + }, + body: JSON.stringify(record) + }) + if (!res.ok) { + const status = res.status + const statusText = res.statusText + throw new Error(`fetch failed to create recording record in database. status=${status}, statusText=${statusText}`) + } + helpers.logger.info('res.headers as follows.') + helpers.logger.info(res.headers) + return res.headers.Location.split('.').at(-1) +} + +export const startRecording: Task = async function (payload, helpers) { + assertPayload(payload) + assertEnv() + const recordId = await createRecordingRecord(payload, helpers) + const { url } = payload; + console.log(`@todo simulated start_recording with url=${url}, recordId=${recordId}`) + await helpers.addJob('record', { url, recordId }) +} + +export default startRecording \ No newline at end of file diff --git a/services/capture/src/tasks/stopRecording.ts b/services/capture/src/tasks/stopRecording.ts new file mode 100644 index 0000000..5df888e --- /dev/null +++ b/services/capture/src/tasks/stopRecording.ts @@ -0,0 +1,18 @@ + +import { type Task } from 'graphile-worker' + +interface Payload { + id: string +} + +function assertPayload(payload: any): asserts payload is Payload { + if (typeof payload !== "object" || !payload) throw new Error("invalid payload"); + if (typeof payload.id !== "string") throw new Error("invalid id"); +} + + +export const stopRecording: Task = async function (payload) { + assertPayload(payload) + const { id } = payload; + console.log(`@todo simulated stop_recording with id=${id}`) +} \ No newline at end of file diff --git a/services/migrations/migrations/00001_create.sql b/services/migrations/migrations/00001_create.sql index 62d6c80..0aa3a35 100644 --- a/services/migrations/migrations/00001_create.sql +++ b/services/migrations/migrations/00001_create.sql @@ -2,20 +2,26 @@ -- example: api.discord_interactions becomes accessible at localhost:9000/discord_interactions CREATE schema api; --- schema for @futureporn/capture and @futureporn/bot -CREATE TABLE api.discord_interactions ( - id int PRIMARY KEY, - discord_message_id text NOT NULL, - capture_job_id text NOT NULL -); - -- authenticator is the role which can "impersonate" other users. CREATE ROLE authenticator LOGIN NOINHERIT NOCREATEDB NOCREATEROLE NOSUPERUSER; -- anonymous is the role assigned to anonymous web requests CREATE ROLE anonymous NOLOGIN; --- roles & users for our @futureporn/capture user -CREATE ROLE capture_user NOLOGIN; -GRANT capture_user TO authenticator; -GRANT usage ON SCHEMA api TO capture_user; -GRANT ALL ON api.discord_interactions TO capture_user; +-- schema for @futureporn/capture and @futureporn/bot +CREATE TABLE api.discord_interactions ( + id int PRIMARY KEY GENERATED ALWAYS AS IDENTITY, + discord_message_id text NOT NULL, + capture_job_id text NOT NULL +); + + +-- roles & permissions for our backend automation user +CREATE ROLE automation_user NOLOGIN; +GRANT automation_user TO authenticator; +GRANT usage ON SCHEMA api TO automation_user; +GRANT all ON api.discord_interactions TO automation_user; + +-- role & permissions for anonymous web user +CREATE ROLE anonymous_user NOLOGIN; +GRANT usage on schema api TO anonymous_user; +GRANT SELECT ON api.discord_interactions TO anonymous_user; diff --git a/services/migrations/migrations/00002_add-records-table.sql b/services/migrations/migrations/00002_add-records-table.sql new file mode 100644 index 0000000..60eff7c --- /dev/null +++ b/services/migrations/migrations/00002_add-records-table.sql @@ -0,0 +1,10 @@ +-- schema for @futureporn/capture and @futureporn/bot +CREATE TABLE api.records ( + id INT PRIMARY KEY GENERATED ALWAYS AS IDENTITY, + url TEXT NOT NULL, + discordMessageId TEXT NOT NULL, + isAborted BOOLEAN DEFAULT FALSE +); + +-- roles & permissions for our backend automation user +GRANT all ON api.records TO automation_user; diff --git a/services/migrations/package.json b/services/migrations/package.json index 8ec1cb4..29de4bc 100644 --- a/services/migrations/package.json +++ b/services/migrations/package.json @@ -1,7 +1,7 @@ { "name": "@futureporn/migrations", "type": "module", - "version": "0.0.1", + "version": "0.0.2", "description": "", "main": "index.js", "scripts": {