diff --git a/.github/workflows/branch--lint-unit-and-smoke-test.yml b/.github/workflows/branch--lint-unit-and-smoke-test.yml index cf5cff02cf..af3fdb5b91 100644 --- a/.github/workflows/branch--lint-unit-and-smoke-test.yml +++ b/.github/workflows/branch--lint-unit-and-smoke-test.yml @@ -19,7 +19,7 @@ jobs: name: Workspace strategy: matrix: - workspace: [model, designer, runner, submitter] + workspace: [model, designer, runner] uses: ./.github/workflows/lint-and-test.yml with: workspace: ${{ matrix.workspace }} @@ -31,13 +31,6 @@ jobs: app: designer secrets: inherit - build-submitter: - name: Submitter - uses: ./.github/workflows/build.yml - with: - app: submitter - secrets: inherit - build-runner: name: Runner uses: ./.github/workflows/build.yml diff --git a/.gitignore b/.gitignore index accb873911..b859a04de8 100644 --- a/.gitignore +++ b/.gitignore @@ -29,8 +29,4 @@ tsconfig.tsbuildinfo .yarn/install-state.gz docs/**/typedoc -/e2e/cypress/screenshots/ -.env_mysql -/queue-model/dist -/queue-model/module -/queue-model/src/prisma/generated \ No newline at end of file +/e2e/cypress/screenshots/ \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml index 976f3c2a16..da4c5e30f9 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -40,53 +40,19 @@ services: - PREVIEW_MODE=true - LAST_COMMIT - LAST_TAG - # - ENABLE_QUEUE_SERVICE=true - # - QUEUE_DATABASE_URL=mysql://root:root@mysql:3306/queue # or postgres://user:root@postgres:5432/queue - # - DEBUG="prisma*" - # - QUEUE_TYPE="MYSQL" +# - ENABLE_QUEUE_SERVICE=true +# - QUEUE_DATABASE_URL=postgres://user:root@postgres:5432/queue +# - QUEUE_TYPE="PGBOSS" command: yarn runner start depends_on: redis: condition: service_started - # mysql: - # condition: service_healthy redis: image: "redis:alpine" command: redis-server --requirepass 123abc ports: - "6379:6379" -# if using MYSQL, uncomment submitter -# submitter: -# image: digital-form-builder-submitter -# build: -# context: . -# dockerfile: ./submitter/Dockerfile -# ports: -# - "9000:9000" -# environment: -# - PORT=9000 -# - QUEUE_DATABASE_URL=mysql://root:root@mysql:3306/queue -# - QUEUE_POLLING_INTERVAL=5000 -# - DEBUG="prisma*" -# command: yarn submitter start -# depends_on: -# mysql: -# condition: service_healthy -# mysql: -# container_name: mysql -# image: "mysql:latest" -# command: --default-authentication-plugin=mysql_native_password -# ports: -# - "3306:3306" -# environment: -# MYSQL_ROOT_PASSWORD: root -# MYSQL_DATABASE: queue -# healthcheck: -# test: ["CMD", "mysqladmin", "ping", "-h", "localhost"] -# timeout: 20s -# retries: 10 - -# use psql if you want a PostgreSQL based queue (recommended) +## use psql if you want a PostgreSQL based queue # postgres: # container_name: postgres # image: "postgres:16" @@ -95,4 +61,13 @@ services: # environment: # POSTGRES_DB: queue # POSTGRES_PASSWORD: root -# POSTGRES_USER: user \ No newline at end of file +# POSTGRES_USER: user +# +## uncomment worker if using PGBOSS queue +# worker: +# depends_on: [postgres] +# platform: linux/amd64 +# container_name: worker +# image: ghcr.io/xgovformbuilder/forms-worker:latest +# environment: +# QUEUE_URL: "postgres://user:root@postgres:5432/queue" diff --git a/docs/runner/submission-queue.md b/docs/runner/submission-queue.md index a82ed057f1..c486f9ba77 100644 --- a/docs/runner/submission-queue.md +++ b/docs/runner/submission-queue.md @@ -3,12 +3,7 @@ The runner can be configured to add new submissions to a queue and, if using the MYSQL queue type, for this queue to be processed by the submitter module. -Two queue types are currently allowed, MYSQL and PGBOSS. - -For `MYSQL`, enabling the queue service this will change the webhook process, so that the runner will push the -submission to a -specified database, and will await a response from the submitter for a few seconds before returning the success screen -to the user. +`PGBOSS` is now the only allowed queue type to reduce maintenance burden. See the migration guide below for switching to PGBOSS. For `PGBOSS`, which handles events and queues as expected from event based architecture. The `PGBOSS` queue type uses [pg-boss](https://www.npmjs.com/package/pg-boss). @@ -26,69 +21,23 @@ capability to support it in your organisation. You may need queuing if your service expects high volume of submissions, but your webhook endpoints or further downstream endpoints change frequently or have slow response times. -You will need to set up a MySQL or PostgreSQL database. +You will need to set up a PostgreSQL database. Use `PGBOSS` and PostgreSQL for higher availability and features like exponential backoff. -It is highly recommended you use `PGBOSS` and PostgreSQL. MYSQL may be deprecated due to the additional overhead and -support that is required. +It is highly recommended you use `PGBOSS` and PostgreSQL. #### PGBOSS Prerequisites - PostgreSQL database >=v11 -- A worker process which can connect to the PostgreSQL database, via PgBoss. Your implementation should look something like this - -```ts -export async function setupWorker() { - const pgboss = new PgBoss(config.get("Queue.url")); - await consumer.work( - "submission", - { newJobCheckInterval: 500 }, - submitHandler - ); -} +- A worker process which can connect to the PostgreSQL database, via PgBoss. + - You may use our [forms-worker](https://github.com/XGovFormBuilder/forms-worker), or implement your own. -setupWorker(); - -/** - * When a "submission" event is detected, this worker POSTs the data to `job.data.data.webhook_url` - * The source of this event is the runner, after a user has submitted a form. - */ -export async function submitHandler(job: Job) { - const { data } = job; - const requestBody = data.data; - const url = data.webhook_url; - try { - const res = await axios.post(url, requestBody); - const reference = res.data.reference; - if (reference) { - return { reference }; - } - } catch (e: any) { - throw e; - } -} -``` When using pgboss, it is important that successful work returns `{ reference }` so that the runner can retrieve the successful response. Thrown errors will be recorded in the database for you to investigate later. Logging has been omitted for brevity, but you should include it! - The `jobId` is generated when a users' submission is successfully inserted into the queue - The webhook endpoint should respond with application/json `{ "reference": "FCDO-3252" }` -#### MYSQL Prerequisites - -- MySQL database - -### Environment variables - -| Variable name | Definition | Default | Example | -| ------------------------------ | ---------------------------------------------------------------------------------------- | ------- | ------------------------------------------- | -| ENABLE_QUEUE_SERVICE | Whether the queue service is enabled or not | `false` | | -| QUEUE_DATABASE_TYPE | PGBOSS or MYSQL | | | -| QUEUE_DATABASE_URL | Used for configuring the endpoint of the database instance | | mysql://username:password@endpoint/database | -| QUEUE_DATABASE_USERNAME | Used for configuring the user being used to access the database | | root | -| QUEUE_DATABASE_PASSWORD | Used for configuring the password used for accessing the database | | password | -| QUEUE_SERVICE_POLLING_INTERVAL | The amount of time, in milliseconds, between poll requests for updates from the database | 500 | | -| QUEUE_SERVICE_POLLING_TIMEOUT | The total amount of time, in milliseconds, to poll requests for from the database | 2000 | | Webhooks can be configured so that the submitter only attempts to post to the webhook URL once. @@ -112,65 +61,53 @@ Webhooks can be configured so that the submitter only attempts to post to the we To use the submission queue locally, you will need to have a running instance of a database, the runner, and the submitter. The easiest way to do this is by using the provided `docker-compose.yml` file. -In that file, you will see the following lines commented out: +In that file, you will see the following lines commented out under the runner service: + ```yaml + runner: + container_name: runner + image: digital-form-builder-runner + environment: +# ... # - ENABLE_QUEUE_SERVICE=true -# - QUEUE_DATABASE_URL=mysql://root:root@mysql:3306/queue -# - DEBUG="prisma*" +# - QUEUE_DATABASE_URL=postgres://user:root@postgres:5432/queue +# - QUEUE_TYPE="PGBOSS" ``` +and two services commented out, `postgres` and `worker`. + ```yaml -# if using MYSQL, uncomment submitter -# submitter: -# image: digital-form-builder-submitter -# build: -# context: . -# dockerfile: ./submitter/Dockerfile -# ports: -# - "9000:9000" -# environment: -# - PORT=9000 -# - QUEUE_DATABASE_URL=mysql://root:root@mysql:3306/queue -# - QUEUE_POLLING_INTERVAL=5000 -# - DEBUG="prisma*" -# command: yarn submitter start -# depends_on: -# mysql: -# condition: service_healthy -# mysql: -# container_name: mysql -# image: "mysql:latest" -# command: --default-authentication-plugin=mysql_native_password -# ports: -# - "3306:3306" -# environment: -# MYSQL_ROOT_PASSWORD: root -# MYSQL_DATABASE: queue -# healthcheck: -# test: ["CMD", "mysqladmin", "ping", "-h", "localhost"] -# timeout: 20s -# retries: 10 - -# use psql if you want a PostgreSQL based queue (recommended) -# postgres: -# container_name: postgres -# image: "postgres:16" -# ports: -# - "5432:5432" -# environment: -# POSTGRES_DB: queue -# POSTGRES_PASSWORD: root -# POSTGRES_USER: user +## use psql if you want a PostgreSQL based queue +#postgres: +# container_name: postgres +# image: "postgres:16" +# ports: +# - "5432:5432" +# environment: +# POSTGRES_DB: queue +# POSTGRES_PASSWORD: root +# POSTGRES_USER: user +# +## uncomment worker if using PGBOSS queue +#worker: +# depends_on: [postgres] +# platform: linux/amd64 +# container_name: worker +# image: ghcr.io/xgovformbuilder/forms-worker:latest +# environment: +# QUEUE_URL: "postgres://user:root@postgres:5432/queue" + ``` -Uncommenting the environment variables under the runner configuration will enable the queue service, set the database -url to the url of your mysql container, and turn on debug messages for prisma (the ORM used to communicate with the -database). -Uncommenting the mysql dependency will make sure the mysql server is started before prisma starts trying to connect to -it. -Uncommenting the submitter configuration will trigger the submitter to be created, exposed on port 9000, connecting to -the mysql container, with a polling interval of 5 seconds. +Uncommenting the environment variables under the runner configuration will enable the queue service and set the database +url to the url of your postgres container. + +Uncommenting the postgres dependency will make sure the postgres server is started before worker connects to it. + +Uncommenting the worker configuration start up the [forms-worker](https://github.com/XGovFormBuilder/forms-worker). The +worker will poll the database every 2 seconds by default, but you may increase that by adding a new environment variable `NEW_JOB_CHECK_INTERVAL`, which is in ms. +Other environment variables can be found in the [forms-worker README](https://github.com/XGovFormBuilder/forms-worker?tab=readme-ov-file#environment-variables). Once your docker-compose file is ready, start all of your containers by using the command `docker compose up` or `docker compose up -d` to run the containers in detached mode. diff --git a/model/package.json b/model/package.json index 25063cd596..ddfa0fbe0f 100644 --- a/model/package.json +++ b/model/package.json @@ -55,6 +55,7 @@ "expr-eval": "2.0.2", "hmpo-components": "5.2.1", "jest": "^29.2.0", + "jest-cli": "^29.7.0", "nanoid": "^3.3.4", "nunjucks": "^3.2.3", "path": "0.12.7", diff --git a/package.json b/package.json index 5ea7f5b1e6..e917a42aa3 100644 --- a/package.json +++ b/package.json @@ -14,13 +14,12 @@ "runner", "designer", "e2e", - "queue-model", - "submitter" + "queue-model" ], "scripts": { "setup": "yarn && yarn build", "build": "yarn workspaces foreach run build", - "build:dependencies": "yarn model build && yarn queue-model build", + "build:dependencies": "yarn model build", "lint": "yarn workspaces foreach run lint", "test": "yarn workspaces foreach run test", "fix-lint": "yarn workspaces foreach run fix-lint", @@ -29,8 +28,6 @@ "designer": "yarn workspace @xgovformbuilder/designer", "model": "yarn workspace @xgovformbuilder/model", "e2e": "yarn workspace e2e", - "queue-model": "yarn workspace @xgovformbuilder/queue-model", - "submitter": "yarn workspace @xgovformbuilder/submitter", "test-cov": "yarn workspaces foreach run test-cov", "runner:start": "yarn workspace @xgovformbuilder/runner start", "type-check": "yarn workspaces foreach run tsc --noEmit", diff --git a/queue-model/babel.config.json b/queue-model/babel.config.json deleted file mode 100644 index a5248bb333..0000000000 --- a/queue-model/babel.config.json +++ /dev/null @@ -1,18 +0,0 @@ -{ - "env": { - "node": { - "presets": [ - "@babel/typescript", - [ - "@babel/preset-env", - { - "targets": { - "node": "16" - } - } - ] - ], - "sourceMaps": true - } - } -} diff --git a/queue-model/migrations/20230913152003_init/migration.sql b/queue-model/migrations/20230913152003_init/migration.sql deleted file mode 100644 index 44c4b74854..0000000000 --- a/queue-model/migrations/20230913152003_init/migration.sql +++ /dev/null @@ -1,14 +0,0 @@ --- CreateTable -CREATE TABLE `Submission` ( - `id` INTEGER NOT NULL AUTO_INCREMENT, - `webhook_url` VARCHAR(191) NULL, - `created_at` DATETIME(3) NOT NULL, - `updated_at` DATETIME(3) NOT NULL, - `data` VARCHAR(8192) NOT NULL, - `error` VARCHAR(191) NULL, - `return_reference` VARCHAR(191) NULL, - `complete` BOOLEAN NOT NULL, - `retry_counter` INTEGER NOT NULL, - - PRIMARY KEY (`id`) -) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; diff --git a/queue-model/migrations/20230915145048_add_defaults/migration.sql b/queue-model/migrations/20230915145048_add_defaults/migration.sql deleted file mode 100644 index 578f37b1f5..0000000000 --- a/queue-model/migrations/20230915145048_add_defaults/migration.sql +++ /dev/null @@ -1,4 +0,0 @@ --- AlterTable -ALTER TABLE `Submission` MODIFY `created_at` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), - MODIFY `updated_at` DATETIME(3) NULL, - MODIFY `complete` BOOLEAN NOT NULL DEFAULT false; diff --git a/queue-model/migrations/20230919102214_use_db_text/migration.sql b/queue-model/migrations/20230919102214_use_db_text/migration.sql deleted file mode 100644 index a604757502..0000000000 --- a/queue-model/migrations/20230919102214_use_db_text/migration.sql +++ /dev/null @@ -1,2 +0,0 @@ --- AlterTable -ALTER TABLE `Submission` MODIFY `data` TEXT NULL; diff --git a/queue-model/migrations/20231107195736_add_allow_retry/migration.sql b/queue-model/migrations/20231107195736_add_allow_retry/migration.sql deleted file mode 100644 index ace728b233..0000000000 --- a/queue-model/migrations/20231107195736_add_allow_retry/migration.sql +++ /dev/null @@ -1,2 +0,0 @@ --- AlterTable -ALTER TABLE `Submission` ADD COLUMN `allow_retry` BOOLEAN NOT NULL DEFAULT true; diff --git a/queue-model/migrations/20231108100812_webhook_url_required/migration.sql b/queue-model/migrations/20231108100812_webhook_url_required/migration.sql deleted file mode 100644 index be5aafbc23..0000000000 --- a/queue-model/migrations/20231108100812_webhook_url_required/migration.sql +++ /dev/null @@ -1,5 +0,0 @@ --- UpdateColumn -UPDATE `Submission` SET `webhook_url`='' WHERE `webhook_url` IS NULL; - --- AlterTable -ALTER TABLE `Submission` MODIFY `webhook_url` VARCHAR(191) NOT NULL; diff --git a/queue-model/migrations/migration_lock.toml b/queue-model/migrations/migration_lock.toml deleted file mode 100644 index e5a788a7af..0000000000 --- a/queue-model/migrations/migration_lock.toml +++ /dev/null @@ -1,3 +0,0 @@ -# Please do not edit this file manually -# It should be added in your version-control system (i.e. Git) -provider = "mysql" \ No newline at end of file diff --git a/queue-model/package.json b/queue-model/package.json deleted file mode 100644 index 0d5c98ef64..0000000000 --- a/queue-model/package.json +++ /dev/null @@ -1,41 +0,0 @@ -{ - "name": "@xgovformbuilder/queue-model", - "version": "1.0.0", - "description": "A hapi plugin to provide the queue model for Xgov digital form builder based applications using the queue service", - "main": "dist/module/index.js", - "engines": { - "node": ">=16" - }, - "repository": { - "type": "git", - "url": "https://github.com/XGovFormBuilder/digital-form-builder/tree/feat/failure-queue/queue-model" - }, - "scripts": { - "lint": "yarn run eslint .", - "fix-lint": "yarn run eslint . --fix", - "build": "yarn run build:prisma && yarn run build:types && yarn run build:node", - "build:prisma": "prisma generate", - "migrate": "prisma migrate dev", - "build:node": "BABEL_ENV=node babel --extensions '.ts' src --out-dir dist/module --copy-files", - "build:types": "tsc --emitDeclarationOnly", - "type-check": "tsc --noEmit" - }, - "devDependencies": { - "@babel/cli": "^7.23.3", - "@babel/core": "^7.23.3", - "@babel/eslint-parser": "^7.23.3", - "@babel/eslint-plugin": "^7.22.10", - "@babel/preset-env": "^7.23.3", - "@babel/preset-typescript": "^7.23.3", - "@types/node": "^20.4.6", - "babel-eslint": "^10.1.0", - "eslint": "^8.10.0", - "eslint-plugin-import": "^2.25.4", - "eslint-plugin-tsdoc": "^0.2.14", - "prisma": "^5.1.1", - "typescript": "4.9.5" - }, - "dependencies": { - "@prisma/client": "5.0.0" - } -} diff --git a/queue-model/schema.prisma b/queue-model/schema.prisma deleted file mode 100644 index 78fe3a8424..0000000000 --- a/queue-model/schema.prisma +++ /dev/null @@ -1,21 +0,0 @@ -datasource db { - provider = "mysql" - url = env("QUEUE_DATABASE_URL") -} - -generator client { - provider = "prisma-client-js" -} - -model Submission { - id Int @id @default(autoincrement()) - webhook_url String @default("") - created_at DateTime @default(now()) - updated_at DateTime? - data String? @db.Text - error String? - return_reference String? - complete Boolean @default(false) - retry_counter Int - allow_retry Boolean @default(true) -} \ No newline at end of file diff --git a/queue-model/src/index.ts b/queue-model/src/index.ts deleted file mode 100644 index 9c39c71b34..0000000000 --- a/queue-model/src/index.ts +++ /dev/null @@ -1,4 +0,0 @@ -import * as path from "path"; - -export { PrismaClient, Prisma, Submission } from "@prisma/client"; -export const SCHEMA_LOCATION = path.resolve(__dirname, "schema.prisma"); diff --git a/queue-model/tsconfig.json b/queue-model/tsconfig.json deleted file mode 100644 index b0931dd737..0000000000 --- a/queue-model/tsconfig.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "extends": "../tsconfig.json", - "baseUrl": "./src", - "compilerOptions": { - "outDir": "dist/module", - "rootDir": "src", - "composite": true, - "declaration": true, - "skipLibCheck": true - }, - "include": ["./src"], - "exclude": ["../node_modules", "node_modules", "./src/prisma"] -} diff --git a/runner/Dockerfile b/runner/Dockerfile index 8f9de48220..c31df74753 100644 --- a/runner/Dockerfile +++ b/runner/Dockerfile @@ -41,26 +41,11 @@ RUN yarn model build # ---------------------------- # Stage 4 -# Base with queue model stage -# In this layer we build the queue-model workspace. -# It will re-run only if anything inside /queue-model changes, otherwise this stage is cached. -# rsync is used to merge folders instead of individually copying files -FROM model AS queue-model -WORKDIR /usr/src/app -COPY --chown=appuser:appuser ./queue-model/package.json ./queue-model/tsconfig.json ./queue-model/babel.config.json ./queue-model/schema.prisma ./queue-model/ -COPY --chown=appuser:appuser ./queue-model/src ./queue-model/src/ -COPY --chown=appuser:appuser ./queue-model/migrations ./queue-model/migrations/ -RUN --mount=type=cache,target=.yarn/cache,uid=1001,mode=0755,id=queue-model \ - --mount=type=cache,target=.yarn/cache,uid=1001,mode=0755,id=queue-model yarn workspaces focus @xgovformbuilder/queue-model -RUN yarn queue-model build - -# ---------------------------- -# Stage 5 # Build stage # In this layer we build the runner workspace # It will re-run only if anything inside ./runner changes, otherwise this stage is cached. # rsync is used to merge folders instead of individually copying files -FROM queue-model AS build-runner +FROM model AS build-runner WORKDIR /usr/src/app ARG LAST_COMMIT="" ARG LAST_TAG="" diff --git a/runner/package.json b/runner/package.json index 850464002b..f000a44192 100644 --- a/runner/package.json +++ b/runner/package.json @@ -105,7 +105,6 @@ "@types/wreck": "^14.0.0", "@xgovformbuilder/lab-babel": "2.1.2", "@xgovformbuilder/model": "workspace:model", - "@xgovformbuilder/queue-model": "workspace:queue-model", "acorn": "^8.7.0", "babel-eslint": "^10.1.0", "babel-plugin-module-name-mapper": "^1.2.0", diff --git a/runner/src/server/index.ts b/runner/src/server/index.ts index f27f03fca8..80fe40deb1 100644 --- a/runner/src/server/index.ts +++ b/runner/src/server/index.ts @@ -35,9 +35,7 @@ import { } from "./services"; import { HapiRequest, HapiResponseToolkit, RouteConfig } from "./types"; import getRequestInfo from "./utils/getRequestInfo"; -import { pluginQueue } from "server/plugins/queue"; import { QueueStatusService } from "server/services/queueStatusService"; -import { MySqlQueueService } from "server/services/mySqlQueueService"; import { PgBossQueueService } from "server/services/pgBossQueueService"; const serverOptions = (): ServerOptions => { @@ -122,11 +120,8 @@ async function createServer(routeConfig: RouteConfig) { } if (config.enableQueueService) { - const queueType = config.queueType; - const queueService = - queueType === "PGBOSS" ? PgBossQueueService : MySqlQueueService; server.registerService([ - Schmervice.withName("queueService", queueService), + Schmervice.withName("queueService", PgBossQueueService), Schmervice.withName("statusService", QueueStatusService), ]); } else { @@ -183,8 +178,6 @@ async function createServer(routeConfig: RouteConfig) { encoding: "base64json", }); - await server.register(pluginQueue); - return server; } diff --git a/runner/src/server/plugins/queue.ts b/runner/src/server/plugins/queue.ts deleted file mode 100644 index 1ed63d621b..0000000000 --- a/runner/src/server/plugins/queue.ts +++ /dev/null @@ -1,41 +0,0 @@ -import config from "server/config"; -import { spawnSync } from "child_process"; -const DEFAULT_OPTIONS = { - enableQueueService: config.enableQueueService, -}; -export const pluginQueue = { - name: "queue", - register: async function (server, options) { - if (DEFAULT_OPTIONS.enableQueueService) { - const schemaLocation = require.resolve( - "@xgovformbuilder/queue-model/schema.prisma" - ); - - const child = spawnSync( - "prisma", - ["migrate", "deploy", "--schema", schemaLocation], - { - encoding: "utf-8", - stdio: "inherit", - } - ); - - if (child.error) { - server.log(["error", "queue initialisation"], child.error); - process.exit(1); - } - if (child.stdout) { - server.log(["queue initialisation", "child process"], child.stdout); - server.log( - ["queue initialisation"], - "Database migration was successful, continuing" - ); - } - } else { - server.log( - ["queue initialisation"], - "Queue service not enabled, skipping initialisation" - ); - } - }, -}; diff --git a/runner/src/server/prismaClient.ts b/runner/src/server/prismaClient.ts deleted file mode 100644 index b52d782457..0000000000 --- a/runner/src/server/prismaClient.ts +++ /dev/null @@ -1,72 +0,0 @@ -import type { Prisma } from "@xgovformbuilder/queue-model"; -import { PrismaClient } from "@xgovformbuilder/queue-model"; -import config from "./config"; -import logger from "pino"; - -const prismaLogger = logger(); - -const logLevel: Prisma.LogDefinition[] = [ - { - emit: "event", - level: "error", - }, - { - emit: "event", - level: "warn", - }, -]; - -if (config.isDev) { - logLevel.push( - { - emit: "event", - level: "query", - }, - { - emit: "event", - level: "info", - } - ); -} - -export const prisma: PrismaClient = new PrismaClient({ - log: logLevel, -}); - -if (config.enableQueueService && config.queueType === "MYSQL") { - prismaLogger.info( - "ENABLE_QUEUE_SERVICE is true, and queueType is set to MYSQL connecting to Prisma" - ); - prisma.$connect().catch((error) => { - prismaLogger.fatal( - `ENABLE_QUEUE_SERVICE is set to true, and queueType is set to MYSQL but Prisma failed to connect ${error}, exiting with status 1` - ); - process.exit(1); - }); - - process.on("query", (e: Prisma.QueryEvent) => { - if (!config.isTest) { - prismaLogger.info(` - Prisma Query: ${e.query} \r\n - Duration: ${e.duration}ms \r\n - Params: ${e.params} - `); - } - }); - - process.on("warn", (e) => { - prismaLogger.warn(e); - }); - - process.on("info", (e) => { - prismaLogger.info(e); - }); - - process.on("error", (e) => { - prismaLogger.error(e); - }); - - process.on("beforeExit", () => { - prismaLogger.info("Prisma is exiting"); - }); -} diff --git a/runner/src/server/services/mySqlQueueService.ts b/runner/src/server/services/mySqlQueueService.ts deleted file mode 100644 index a7943ec8ce..0000000000 --- a/runner/src/server/services/mySqlQueueService.ts +++ /dev/null @@ -1,98 +0,0 @@ -import { HapiServer } from "server/types"; -import { PrismaClient } from "@xgovformbuilder/queue-model"; -import { prisma } from "../prismaClient"; -import config from "../config"; -import { QueueService } from "server/services/QueueService"; - -type QueueResponse = [number, string | undefined]; -export class MySqlQueueService extends QueueService { - prisma: PrismaClient; - logger: HapiServer["logger"]; - interval: number; - - constructor(server: HapiServer) { - super(server); - this.prisma = prisma; - this.interval = parseInt(config.queueServicePollingInterval); - } - - /** - * Send data from form submission to submission queue - * @param data - * @param url - * @returns The ID of the newly added row, or undefined in the event of an error - */ - async sendToQueue( - data: object, - url: string, - allowRetry = true - ): Promise { - const rowData = { - data: JSON.stringify(data), - created_at: new Date(), - updated_at: new Date(), - webhook_url: url, - complete: false, - retry_counter: 0, - allow_retry: allowRetry, - }; - const row = await this.prisma.submission.create({ - data: rowData, - }); - this.logger.info(["queueService", "sendToQueue", "success"], row); - try { - const newRowRef = (await this.pollForRef(row.id)) ?? "UNKNOWN"; - this.logger.info( - ["queueService", "sendToQueue", `Row ref: ${row.id}`], - `Return ref: ${newRowRef}` - ); - return [row.id, newRowRef]; - } catch (err) { - this.logger.error( - ["QueueService", "sendToQueue", `Row ref: ${row.id}`], - "Polling for return reference failed." - ); - return [row.id, undefined]; - } - } - - async pollForRef(rowId: number): Promise { - let timeElapsed = 0; - return new Promise((resolve, reject) => { - const pollInterval = setInterval(async () => { - try { - const newRef = await this.getReturnRef(rowId); - if (newRef) { - resolve(newRef); - clearInterval(pollInterval); - } - if (timeElapsed >= 2000) { - resolve(); - clearInterval(pollInterval); - } - timeElapsed += parseInt(config.queueServicePollingInterval); - } catch (err) { - this.logger.error( - ["QueueService", "pollForRef", `Row ref: ${rowId}`], - err - ); - reject(); - } - }, config.queueServicePollingInterval); - }); - } - async getReturnRef(rowId: number) { - const row = await this.prisma.submission.findUnique({ - select: { - return_reference: true, - }, - where: { - id: rowId, - }, - }); - if (!row) { - throw new Error("Submission row not found"); - } - return row.return_reference; - } -} diff --git a/runner/src/server/services/pgBossQueueService.ts b/runner/src/server/services/pgBossQueueService.ts index 1b6823a07e..1f5d590a8f 100644 --- a/runner/src/server/services/pgBossQueueService.ts +++ b/runner/src/server/services/pgBossQueueService.ts @@ -1,6 +1,6 @@ import { QueueService } from "server/services/QueueService"; type QueueResponse = [number | string, string | undefined]; -import PgBoss, { Job, JobWithMetadata } from "pg-boss"; +import PgBoss, { JobWithMetadata } from "pg-boss"; import config from "server/config"; type QueueReferenceApiResponse = { @@ -24,7 +24,6 @@ export class PgBossQueueService extends QueueService { this.queueReferenceApiUrl = config.queueReferenceApiUrl; this.pollingInterval = parseInt(config.queueServicePollingInterval); this.pollingTimeout = parseInt(config.queueServicePollingTimeout); - const boss = new PgBoss(config.queueDatabaseUrl); this.queue = boss; boss.on("error", this.logger.error); diff --git a/runner/src/server/services/queueStatusService.ts b/runner/src/server/services/queueStatusService.ts index 26e27eee58..116a185140 100644 --- a/runner/src/server/services/queueStatusService.ts +++ b/runner/src/server/services/queueStatusService.ts @@ -1,11 +1,10 @@ import { StatusService } from "server/services/statusService"; import { HapiRequest, HapiServer } from "server/types"; import Boom from "boom"; -import { MySqlQueueService } from "server/services/mySqlQueueService"; import { PgBossQueueService } from "server/services/pgBossQueueService"; export class QueueStatusService extends StatusService { - queueService: MySqlQueueService | PgBossQueueService; + queueService: PgBossQueueService; constructor(server: HapiServer) { super(server); const { queueService } = server.services([]); @@ -71,18 +70,6 @@ export class QueueStatusService extends StatusService { }); } - if (!queueReference) { - const queueResults = await this.queueService?.sendToQueue(formData, ""); - if (!queueResults) { - this.logQueueServiceError(); - } - [queueReference, newReference] = queueResults; - this.logger.info( - ["QueueStatusService", "outputRequests"], - `Queue reference: ${queueReference}` - ); - } - const { notify = [], webhook = [] } = this.outputArgs( otherOutputs, formData, @@ -93,6 +80,7 @@ export class QueueStatusService extends StatusService { const requests = [ ...notify.map((args) => this.notifyService.sendNotification(args)), ...webhook.map(({ url, sendAdditionalPayMetadata, formData }) => + // TODO: run these ase queue inserts instead this.webhookService.postRequest( url, formData, diff --git a/submitter/.babelrc b/submitter/.babelrc deleted file mode 100644 index 4aaf53be3a..0000000000 --- a/submitter/.babelrc +++ /dev/null @@ -1,18 +0,0 @@ -{ - "presets": [ - "@babel/typescript", - [ - "@babel/preset-env", - { - "targets": { - "node": "16" - } - } - ] - ], - "exclude": ["node_modules/**"], - "plugins": [ - "@babel/plugin-transform-runtime" - ] -} - diff --git a/submitter/Dockerfile b/submitter/Dockerfile deleted file mode 100644 index bfddaf5901..0000000000 --- a/submitter/Dockerfile +++ /dev/null @@ -1,64 +0,0 @@ -# ---------------------------- -# Stage 1 -# Base image contains the updated OS and -# It also configures the non-root user that will be given permission to copied files/folders in every subsequent stages -FROM node:18-alpine AS base -RUN mkdir -p /usr/src/app && \ - addgroup -g 1001 appuser && \ - adduser -S -u 1001 -G appuser appuser && \ - chown -R appuser:appuser /usr/src/app && \ - chmod -R +x /usr/src/app && \ - apk update && \ - apk add --no-cache bash git - -# ---------------------------- -# Stage 2 -# Cache layer contains npm packages for all workspaces -# It will re-run only if one of the copied files change, otherwise this stage is cached -FROM base AS dependencies -WORKDIR /usr/src/app -COPY --chown=appuser:appuser .yarn ./.yarn/ -COPY --chown=appuser:appuser package.json yarn.lock .yarnrc.yml tsconfig.json ./ -USER 1001 -RUN --mount=type=cache,target=./.yarn/cache,id=base,uid=1001,mode=0755 yarn - -# ---------------------------- -# Stage 3 -# Base with queue model stage -# In this layer we build the queue-model workspace. -# It will re-run only if anything inside /queue-model changes, otherwise this stage is cached. -# rsync is used to merge folders instead of individually copying files -FROM dependencies AS queue-model -WORKDIR /usr/src/app -COPY --chown=appuser:appuser ./queue-model/package.json ./queue-model/tsconfig.json ./queue-model/babel.config.json ./queue-model/schema.prisma ./queue-model/ -COPY --chown=appuser:appuser ./queue-model/src ./queue-model/src/ -COPY --chown=appuser:appuser ./queue-model/migrations ./queue-model/migrations/ -RUN --mount=type=cache,target=.yarn/cache,uid=1001,mode=0755,id=queue-model \ - --mount=type=cache,target=.yarn/cache,uid=1001,mode=0755,id=queue-model yarn workspaces focus @xgovformbuilder/queue-model -RUN yarn queue-model build - -# ---------------------------- -# Stage 4 -# Build stage -# In this layer we build the submitter workspace -# It will re-run only if anything inside ./submitter changes, otherwise this stage is cached. -# rsync is used to merge folders instead of individually copying files -FROM queue-model AS build-submitter -WORKDIR /usr/src/app -ARG LAST_COMMIT="" -ARG LAST_TAG="" -ENV LAST_COMMIT=$LAST_COMMIT -ENV LAST_TAG=$LAST_TAG -COPY --chown=appuser:appuser ./submitter/package.json ./submitter/tsconfig.json ./submitter/.babelrc ./submitter/nodemon.json ./submitter/ -COPY --chown=appuser:appuser ./submitter/config ./submitter/config -RUN --mount=type=cache,target=.yarn/cache,uid=1001,mode=0755,id=runner \ - yarn workspaces focus @xgovformbuilder/submitter -COPY --chown=appuser:appuser ./submitter/src ./submitter/src/ -RUN touch submitter/.env && \ - echo "LAST_TAG_GH=$LAST_TAG" >> submitter/.env && \ - echo "LAST_COMMIT=$LAST_COMMIT" >> submitter/.env - -RUN yarn submitter build -USER 1001 -EXPOSE 9000 -CMD [ "yarn", "submitter", "start" ] diff --git a/submitter/README.md b/submitter/README.md deleted file mode 100644 index 3a678899d7..0000000000 --- a/submitter/README.md +++ /dev/null @@ -1,53 +0,0 @@ -# digital-form-builder-submitter - -This is an optional additional module for the digital-form-builder project. - -The submitter acts as one half of the form builder queue system, used for saving your form submissions to a database prior to running webhook inputs. - -## Setup - -The Submitter needs a running database instance to connect to in order to work properly. - -To connect a database instance, configure the following environment variables: - -| Variable name | Definition | Example | -| ----------------------- | ----------------------------------------------------------------- | ----------------------------------------- | -| QUEUE_DATABASE_URL | Used for configuring the endpoint of the database instance | mysql://username:password@dbhost/database | -| QUEUE_DATABASE_USERNAME | Used for configuring the user being used to access the database | root | -| QUEUE_DATABASE_PASSWORD | Used for configuring the password used for accessing the database | password | - -Once these variables are set, the submitter will run a series of migrations that will set up your chosen database. - -## Other settings - -The submitter will poll the database for new submissions at regular intervals. The submitter can also delete rows after a specified amount of time, in line with you organisation's retention policy. - -These intervals can be set using the following environment variables: - -| Variable name | Definition | Default | -| ---------------------- | ----------------------------------------------------------------------------------------------- | ------- | -| QUEUE_POLLING_INTERVAL | The length of time, in milliseconds, between poll requests | 5000 | -| QUEUE_RETENTION_PERIOD | The length of time, in days, that a successful submission will be kept for before being deleted | 365 | -| MAX_RETRIES | The maximum number of times to retry a failed request | 1000 | - -If a submission should only be tried once, configure your webhook output with the option `"allowRetry": false`. [Check docs/runner/submission-queue.md](./../docs/runner/submission-queue.md) for a full example. - -## Error codes - -The submitter may fail to submit a form for a variety of reasons. In each, case one of the following error codes will be thrown: - -| Code | Example | Definition | -| ---- | ------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| Q001 | Q001 - Prisma (ORM) could not find submission {details} | There was an issue with the ORM reading from the database. See PXX code, and their [supporting documentation](https://www.prisma.io/docs/reference/api-reference/error-reference) | -| Q002 | Post to webhook failed {details} {rowId: X} | This can be an issue with the URL being sent to, the data being posted, or that the webhook responded with an error. See the details of the error for more information. | -| Q003 | Updating DB failed | This means there was an issue with either setting the record to `complete: true`, or an issue with incrementing `retry_counter` | - -### Retention errors - -In some circumstances, the submitter may fail to run the scheduled redaction task. In those cases the submitter will return one of the following error codes: - -| Code | Example | Definition | -| ---- | ----------------------------------------- | ---------------------------------------------------------------------------------------------------- | -| R001 | Could not set retention period | environment variable QUEUE_RETENTION_PERIOD_DAYS could not be parsed into int. This defaults to 365. | -| R002 | Could not delete records < \${date} | The date printed is the date used to query the database. | -| R003 | Could not run, caught exception {details} | There was an issue running the deletion cron. | diff --git a/submitter/config/custom-environment-variables.json b/submitter/config/custom-environment-variables.json deleted file mode 100644 index e6c9799caf..0000000000 --- a/submitter/config/custom-environment-variables.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "env": "NODE_ENV", - "port": "PORT", - - "logPrettyPrint": "LOG_PRETTY_PRINT", - "logLevel": "LOG_LEVEL", - - "queueDatabaseUrl": "QUEUE_DATABASE_URL", - "queueDatabaseUsername": "QUEUE_DATABASE_USERNAME", - "queueDatabasePassword": "QUEUE_DATABASE_PASSWORD", - "pollingInterval": "QUEUE_POLLING_INTERVAL", - "retentionPeriod": "QUEUE_RETENTION_PERIOD_DAYS" -} diff --git a/submitter/config/default.js b/submitter/config/default.js deleted file mode 100644 index 7ae98f437f..0000000000 --- a/submitter/config/default.js +++ /dev/null @@ -1,38 +0,0 @@ -const { deferConfig } = require("config/defer"); -const dotEnv = require("dotenv"); -if (process.env.NODE_ENV !== "test") { - dotEnv.config({ path: ".env" }); -} - -module.exports = { - env: "development", - port: "9000", - - /** - * logging config - */ - logPrettyPrint: false, - logLevel: "info", - - /** - * Helper flags - */ - isDev: deferConfig(function () { - return this.env !== "production"; - }), - isProd: deferConfig(function () { - return this.env === "production"; - }), - isTest: deferConfig(function () { - return this.env === "test"; - }), - - /** - * Queue service config - */ - queueDatabaseUrl: "", - queueDatabaseUsername: "", - queueDatabasePassword: "", - pollingInterval: "2000", - retentionPeriod: "365", -}; diff --git a/submitter/config/test.json b/submitter/config/test.json deleted file mode 100644 index 67854b9cd7..0000000000 --- a/submitter/config/test.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "env": "test", - "isTest": true -} diff --git a/submitter/jest.config.js b/submitter/jest.config.js deleted file mode 100644 index de42ed08a3..0000000000 --- a/submitter/jest.config.js +++ /dev/null @@ -1,8 +0,0 @@ -/* - * For a detailed explanation regarding each configuration property, visit: - * https://jestjs.io/docs/en/configuration.html - */ - -module.exports = { - testMatch: ["**/__tests__/**.test.ts"], -}; diff --git a/submitter/nodemon.json b/submitter/nodemon.json deleted file mode 100644 index 51690cd8ae..0000000000 --- a/submitter/nodemon.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "delay": "500", - "watch": ["dist"], - "legacyWatch": true -} diff --git a/submitter/package.json b/submitter/package.json deleted file mode 100644 index e3eb2e1122..0000000000 --- a/submitter/package.json +++ /dev/null @@ -1,64 +0,0 @@ -{ - "name": "@xgovformbuilder/submitter", - "version": "1.0.0", - "description": "Digital forms webhook queue submitter", - "main": "dist/index.js", - "private": true, - "scripts": { - "watch": "babel --watch --extensions '.ts' src -d dist", - "start": "node dist/submission/index.js", - "start:submission": "node dist/submission/index.js", - "start:retention": "node dist/retention/index.js", - "start:submission:local": "NODE_ENV=development nodemon dist/submission/index.js", - "start:retention:local": "NODE_ENV=development nodemon dist/retention/index.js", - "dev:submission": "concurrently 'yarn watch' 'yarn start:submission:local'", - "dev:retention": "concurrently 'yarn watch' 'yarn start:retention:local'", - "build": "babel --extensions '.ts' src -d dist", - "lint": "yarn eslint .", - "fix-lint": "yarn bin/run eslint . --fix", - "test": "jest", - "test-cov": "jest --coverage" - }, - "engines": { - "node": ">=16" - }, - "dependencies": { - "@hapi/hapi": "^21.3.2", - "config": "^3.3.7", - "dotenv": "8.2.0", - "hapi-cron": "^1.1.0", - "hapi-pino": "8.0.0", - "pino": "8.15.1", - "schmervice": "^1.6.0" - }, - "devDependencies": { - "@babel/cli": "^7.23.3", - "@babel/core": "^7.23.3", - "@babel/eslint-parser": "^7.23.3", - "@babel/eslint-plugin": "^7.22.10", - "@babel/plugin-proposal-export-default-from": "^7.23.3", - "@babel/plugin-transform-classes": "^7.23.3", - "@babel/plugin-transform-modules-commonjs": "^7.23.3", - "@babel/plugin-transform-runtime": "^7.23.3", - "@babel/preset-env": "^7.23.3", - "@babel/preset-typescript": "^7.23.3", - "@babel/register": "^7.22.15", - "@types/config": "^3.3.0", - "@types/hapi": "^18.0.7", - "@xgovformbuilder/queue-model": "workspace:queue-model", - "babel-jest": "^29.7.0", - "eslint": "^8.11.0", - "eslint-plugin-import": "^2.25.4", - "eslint-plugin-tsdoc": "^0.2.14", - "jest": "^26.6.3", - "jest-mock-extended": "^3.0.5", - "nodemon": "^3.0.1", - "prisma": "^5.1.1", - "typescript": "4.9.5" - }, - "pkg": { - "assets": [ - "../node_modules/.prisma/client/*.node" - ] - } -} diff --git a/submitter/src/__mocks__/prismaClient.ts b/submitter/src/__mocks__/prismaClient.ts deleted file mode 100644 index 4f1545c913..0000000000 --- a/submitter/src/__mocks__/prismaClient.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { mockDeep, mockReset, MockProxy } from "jest-mock-extended"; -import { prisma as prismaClient } from "../prismaClient"; -import { PrismaClient } from "@xgovformbuilder/queue-model"; - -jest.mock("../prismaClient", () => ({ - __esModule: true, - prisma: mockDeep(), -})); - -beforeEach(() => { - mockReset(prismaClient); -}); - -export const prisma = (prismaClient as unknown) as MockProxy; diff --git a/submitter/src/config.ts b/submitter/src/config.ts deleted file mode 100644 index 241a6a786d..0000000000 --- a/submitter/src/config.ts +++ /dev/null @@ -1,2 +0,0 @@ -import config from "config"; -export default config; diff --git a/submitter/src/prismaClient.ts b/submitter/src/prismaClient.ts deleted file mode 100644 index ec8b830a4b..0000000000 --- a/submitter/src/prismaClient.ts +++ /dev/null @@ -1,63 +0,0 @@ -import { Prisma, PrismaClient } from "@xgovformbuilder/queue-model"; -import config from "./config"; -import logger from "pino"; - -const prismaLogger = logger(); - -const logLevel: Prisma.LogDefinition[] = [ - { - emit: "event", - level: "error", - }, - { - emit: "event", - level: "warn", - }, -]; - -if (config.isDev) { - logLevel.push( - { - emit: "event", - level: "query", - }, - { - emit: "event", - level: "info", - } - ); -} - -export const prisma: PrismaClient = new PrismaClient({ - log: logLevel, -}); - -prisma.$connect().catch((error) => { - prismaLogger.error(`Prisma Connect Error ${error.message}`); -}); - -process.on("query", (e: Prisma.QueryEvent) => { - if (!config.isTest) { - prismaLogger.info(` - Prisma Query: ${e.query} \r\n - Duration: ${e.duration}ms \r\n - Params: ${e.params} - `); - } -}); - -process.on("warn", (e) => { - prismaLogger.warn(e); -}); - -process.on("info", (e) => { - prismaLogger.info(e); -}); - -process.on("error", (e) => { - prismaLogger.error(e); -}); - -process.on("beforeExit", () => { - prismaLogger.info("Prisma is exiting"); -}); diff --git a/submitter/src/submission/createServer.ts b/submitter/src/submission/createServer.ts deleted file mode 100644 index d75182b092..0000000000 --- a/submitter/src/submission/createServer.ts +++ /dev/null @@ -1,48 +0,0 @@ -import hapi, { ServerOptions } from "@hapi/hapi"; -import { pluginLogging } from "./plugins/logging"; -import config from "../config"; -import { QueueService, WebhookService } from "./services"; -import Schmervice from "schmervice"; -import { pluginPoll } from "./plugins/poll"; -import { pluginRetention } from "./plugins/retention"; -import { pluginRetentionCron } from "./plugins/retentionCron"; - -const serverOptions: ServerOptions = { - debug: { request: [`${config.isDev}`] }, - port: config.port, - router: { - stripTrailingSlash: true, - }, - routes: { - validate: { - options: { - abortEarly: false, - }, - }, - security: { - hsts: { - maxAge: 31536000, - includeSubDomains: true, - preload: false, - }, - xss: true, - noSniff: true, - xframe: true, - }, - }, -}; - -export async function createServer(): Promise { - const server = hapi.server(serverOptions); - - await server.register(pluginLogging); - await server.register(Schmervice); - - server.registerService([WebhookService, QueueService]); - - await server.register(pluginPoll); - await server.register(pluginRetention); - await server.register(pluginRetentionCron); - - return server; -} diff --git a/submitter/src/submission/index.ts b/submitter/src/submission/index.ts deleted file mode 100644 index c720718d4f..0000000000 --- a/submitter/src/submission/index.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { createServer } from "./createServer"; -import { setupDatabase } from "./setupDatabase"; - -async function initApp() { - await setupDatabase(); - - const server = await createServer(); - try { - await server.start(); - process?.send?.("online"); - } catch (e) { - throw e; - } -} - -initApp().catch((err) => { - console.error(err.message); -}); diff --git a/submitter/src/submission/plugins/logging.ts b/submitter/src/submission/plugins/logging.ts deleted file mode 100644 index 17a636c43d..0000000000 --- a/submitter/src/submission/plugins/logging.ts +++ /dev/null @@ -1,22 +0,0 @@ -import pino from "hapi-pino"; -import config from "../../config"; - -export const pluginLogging = { - plugin: pino, - options: { - prettyPrint: false, - level: config.logLevel, - formatters: { - level: (label) => { - return { level: label }; - }, - }, - debug: config.isDev, - logRequestStart: config.isDev, - logRequestComplete: config.isDev, - redact: { - paths: ["req.headers['x-forwarded-for']"], - censor: "REDACTED", - }, - }, -}; diff --git a/submitter/src/submission/plugins/poll.ts b/submitter/src/submission/plugins/poll.ts deleted file mode 100644 index 6961d0dc93..0000000000 --- a/submitter/src/submission/plugins/poll.ts +++ /dev/null @@ -1,28 +0,0 @@ -import config from "../../config"; -import { QueueService } from "../services"; -import { Logger } from "pino"; - -export const pluginPoll = { - name: "poll", - register: async function (server, _options) { - const { queueService } = server.services([]); - await poll(queueService, server.logger); - }, -}; - -async function poll(queueService: QueueService, logger: Logger) { - const submission = await queueService.getSubmissions(); - if (!submission) { - logger.info(["poll"], "No unprocessed submissions found. Continuing"); - setTimeout(async () => { - await poll(queueService, logger); - }, config.pollingInterval); - } else { - logger.info( - ["poll"], - `Unprocessed submission found. Row ref: ${submission.id}` - ); - await queueService.submit(submission); - await poll(queueService, logger); - } -} diff --git a/submitter/src/submission/plugins/retention.ts b/submitter/src/submission/plugins/retention.ts deleted file mode 100644 index a85904ee9b..0000000000 --- a/submitter/src/submission/plugins/retention.ts +++ /dev/null @@ -1,25 +0,0 @@ -import config from "../../config"; -import { redactSubmissions } from "../retention/redactSubmissions"; -import { R_ERRORS } from "../retention/errors"; -export const pluginRetention = { - name: "retention", - register: async function (server, _options) { - server.route({ - method: "GET", - path: "/retention", - handler: async function (_req, h) { - server.logger.info( - `Deleting records older than ${config.retentionPeriod} days` - ); - - try { - await redactSubmissions(); - return h.response().code(204); - } catch (e) { - server.error(R_ERRORS.RUN_ERROR); - return h.response().code(400); - } - }, - }); - }, -}; diff --git a/submitter/src/submission/plugins/retentionCron.ts b/submitter/src/submission/plugins/retentionCron.ts deleted file mode 100644 index 28df7216b3..0000000000 --- a/submitter/src/submission/plugins/retentionCron.ts +++ /dev/null @@ -1,22 +0,0 @@ -import HapiCron from "hapi-cron"; -import pino from "pino"; -const logger = pino(); -export const pluginRetentionCron = { - plugin: HapiCron, - options: { - jobs: [ - { - name: "retention-cron", - time: "*/1 * * * *", - timezone: "Europe/London", - request: { - method: "GET", - url: "/retention", - }, - onComplete: () => { - logger.info("retention-cron complete"); - }, - }, - ], - }, -}; diff --git a/submitter/src/submission/retention/errors.ts b/submitter/src/submission/retention/errors.ts deleted file mode 100644 index 77275aa94e..0000000000 --- a/submitter/src/submission/retention/errors.ts +++ /dev/null @@ -1,5 +0,0 @@ -export const R_ERRORS = { - CONFIG: `R001 - Could not set retention period`, - DELETION_FAILED: `R002 - Could not delete records`, - RUN_ERROR: `R003 - Could not run, caught exception`, -}; diff --git a/submitter/src/submission/retention/redactSubmissions.ts b/submitter/src/submission/retention/redactSubmissions.ts deleted file mode 100644 index 5dffb297c8..0000000000 --- a/submitter/src/submission/retention/redactSubmissions.ts +++ /dev/null @@ -1,44 +0,0 @@ -import config from "../../config"; -import { prisma } from "../../prismaClient"; -import { pino } from "pino"; -import { R_ERRORS } from "./errors"; -const logger = pino().child({ process: "removeExpired" }); - -let RETENTION_PERIOD = 365; - -try { - RETENTION_PERIOD = parseInt(config.retentionPeriod); - logger.info( - `config.retentionPeriod set to ${config.rentionPeriod}, defaulting to ${RETENTION_PERIOD} instead` - ); -} catch (e) { - logger.error(R_ERRORS.CONFIG); -} - -export async function redactSubmissions() { - const today = new Date(); - const retentionLimit = new Date( - today.getFullYear(), - today.getMonth(), - today.getDate() - RETENTION_PERIOD - ); - - try { - logger.info( - `Attempting to delete completed records updated < ${retentionLimit.toISOString()}. Retention period is set to ${RETENTION_PERIOD} days` - ); - const del = await prisma.submission.deleteMany({ - where: { - updated_at: { - not: undefined, - lt: retentionLimit, - }, - complete: true, - }, - }); - - logger.info(`deleted ${del.count} records`); - } catch (e) { - logger.error(e, `${R_ERRORS.DELETION_FAILED} < ${retentionLimit}`); - } -} diff --git a/submitter/src/submission/services/__tests__/queueService.test.ts b/submitter/src/submission/services/__tests__/queueService.test.ts deleted file mode 100644 index 6a49e0fbd8..0000000000 --- a/submitter/src/submission/services/__tests__/queueService.test.ts +++ /dev/null @@ -1,77 +0,0 @@ -import { QueueService } from "../index"; -import { prisma } from "../../../__mocks__/prismaClient"; -jest.mock("../../../prismaClient"); - -const webhookService = { - postRequest: jest.fn(), -}; - -const server = { - logger: { - info: jest.fn(), - warn: jest.fn(), - error: jest.fn(), - }, - services: () => ({ - webhookService, - }), -}; - -const queueService = new QueueService(server); - -test("Queue service does not process any submissions if none found", async () => { - (prisma.submission.findMany as jest.Mock).mockResolvedValueOnce([]); - - const row = await queueService.getSubmissions(); - expect(row).toBe(undefined); -}); - -test("Queue service updates a submission entry with an error if the webhook fails", async () => { - (prisma.submission.findMany as jest.Mock).mockResolvedValueOnce([ - { - id: 1, - webhook_url: "https://some-url.com", - data: `{"some": "data"}`, - complete: false, - retry_count: 0, - }, - ]); - webhookService.postRequest.mockResolvedValueOnce({ - payload: { - error: { - statusCode: 400, - message: ":(", - }, - }, - }); - const updateWithError = jest.spyOn(queueService, "updateWithError"); - - const row = await queueService.getSubmissions(); - await queueService.submit(row); - expect(updateWithError).toBeCalled(); -}); -test("Queue service updates a submission entry with a successful response if the webhook was successful", async () => { - (prisma.submission.findMany as jest.Mock).mockResolvedValueOnce([ - { - id: 1, - created_at: new Date(), - updated_at: new Date(), - webhook_url: "https://some-url.com", - data: `{"some": "data"}`, - complete: false, - retry_count: 0, - }, - ]); - - webhookService.postRequest.mockResolvedValueOnce({ - payload: { - reference: "REF-0042", - }, - }); - - const updateFunc = jest.spyOn(queueService, "updateWithSuccess"); - const row = await queueService.getSubmissions(); - await queueService.submit(row); - - expect(updateFunc).toBeCalled(); -}); diff --git a/submitter/src/submission/services/__tests__/webhookService.test.ts b/submitter/src/submission/services/__tests__/webhookService.test.ts deleted file mode 100644 index aff6eaa6e4..0000000000 --- a/submitter/src/submission/services/__tests__/webhookService.test.ts +++ /dev/null @@ -1,30 +0,0 @@ -import logger from "pino"; -import { WebhookService } from "../webhookService"; -import * as httpService from "../httpService"; - -test.each` - name | payload | returnRef - ${"reference if request is successful"} | ${`{"reference": "REF1234"}`} | ${"REF1234"} - ${"UNKNOWN if the reference isn't returned"} | ${`{}`} | ${"UNKNOWN"} - ${"an error if the response cannot be parsed"} | ${"not json"} | ${undefined} -`("postRequest returns $name", async ({ payload, returnRef }) => { - jest.spyOn(httpService, "post").mockResolvedValue({ res: {}, payload }); - - const webhookService = new WebhookService({ logger: logger() }); - - const ref = await webhookService.postRequest( - "https://a-url.com", - "{}", - "POST" - ); - - if (returnRef) { - expect(ref).toEqual({ - payload: { - reference: returnRef, - }, - }); - } else { - expect(ref.payload.error).toBeDefined(); - } -}); diff --git a/submitter/src/submission/services/httpService.ts b/submitter/src/submission/services/httpService.ts deleted file mode 100644 index bc9cadf47f..0000000000 --- a/submitter/src/submission/services/httpService.ts +++ /dev/null @@ -1,38 +0,0 @@ -import http from "http"; -import wreck from "@hapi/wreck"; - -type Method = "get" | "post" | "path" | "put" | "delete"; - -export interface Response { - res: http.IncomingMessage; - payload?: T | any; - error?: Error; -} - -export type Request = ( - method: Method, - url: string, - options?: object -) => Promise>; - -export const request: Request = async (method, url, options = {}) => { - const { res, payload } = await wreck[method](url, options); - - try { - return { res, payload } as Response; - } catch (error) { - return { res, error } as Response; - } -}; - -export const get = (url: string, options?: object) => { - return request("get", url, options); -}; - -export const post = (url: string, options: object) => { - return request("post", url, options); -}; - -export const put = (url: string, options: object) => { - return request("put", url, options); -}; diff --git a/submitter/src/submission/services/index.ts b/submitter/src/submission/services/index.ts deleted file mode 100644 index 014e065ce6..0000000000 --- a/submitter/src/submission/services/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export { QueueService } from "./queueService"; -export { WebhookService } from "./webhookService"; diff --git a/submitter/src/submission/services/queueService.ts b/submitter/src/submission/services/queueService.ts deleted file mode 100644 index 2647cb6946..0000000000 --- a/submitter/src/submission/services/queueService.ts +++ /dev/null @@ -1,130 +0,0 @@ -import { PrismaClient, Submission } from "@xgovformbuilder/queue-model"; -import { Server } from "@hapi/hapi"; -import { HapiServer } from "../types"; -import { prisma } from "../../prismaClient"; -import { WebhookService } from "./webhookService"; - -const ERRORS = { - DB_FIND_ERROR: `Q001 - Prisma (ORM) could not find submissions`, - SUBMISSION: `Q002 - Post to webhook failed`, - UPDATE: `Q003 - Updating DB failed`, -}; - -export class QueueService { - prisma: PrismaClient; - logger: Server["logger"]; - webhookService: WebhookService; - MAX_RETRIES = 1000; - - constructor(server: HapiServer) { - this.prisma = prisma; - this.logger = server.logger; - const { webhookService } = server.services([]); - this.webhookService = webhookService; - - if (process.env.MAX_RETRIES) { - try { - this.MAX_RETRIES = parseInt(process.env.MAX_RETRIES); - } catch (e) { - this.logger.warn( - `MAX_RETRIES was set to ${process.env.MAX_RETRIES} but could not be parsed. Using ${this.MAX_RETRIES} instead` - ); - } - } - } - - async getSubmissions() { - try { - const submissionRes = await this.prisma.submission.findMany({ - where: { - complete: false, - OR: [ - { - allow_retry: true, - retry_counter: { - lt: this.MAX_RETRIES, - }, - }, - { - allow_retry: false, - error: null, - }, - ], - }, - orderBy: [ - { - created_at: "desc", - }, - { - error: { sort: "asc", nulls: "first" }, - }, - ], - take: 1, - }); - return submissionRes.at(0); - } catch (e) { - this.logger.error( - ["queueService", "processSubmissions"], - `${ERRORS.DB_FIND_ERROR}: ${e?.message ?? e}` - ); - return; - } - } - - async updateWithSuccess(row: Submission, reference: string = "UNKNOWN") { - const update = await this.prisma.submission.update({ - data: { - return_reference: reference, - complete: true, - }, - where: { - id: row.id, - complete: false, - }, - }); - - this.logger.info(update, `${row.id} succeeded: ${reference}`); - } - - async updateWithError(row: Submission, error) { - this.logger.error( - { - rowId: row.id, - error, - }, - ERRORS.SUBMISSION - ); - - await this.prisma.submission.update({ - data: { - error: JSON.stringify(error), - retry_counter: { - increment: 1, - }, - }, - where: { - id: row.id, - }, - }); - return; - } - - async submit(row) { - try { - const { payload } = await this.webhookService.postRequest( - row.webhook_url, - row.data - ); - - if (payload.error) { - await this.updateWithError(row, payload); - } - - if (payload.reference) { - await this.updateWithSuccess(row, payload.reference); - } - } catch (err) { - this.logger.error(ERRORS.UPDATE); - } - } -} diff --git a/submitter/src/submission/services/webhookService.ts b/submitter/src/submission/services/webhookService.ts deleted file mode 100644 index 907f113af7..0000000000 --- a/submitter/src/submission/services/webhookService.ts +++ /dev/null @@ -1,54 +0,0 @@ -import { post, put } from "./httpService"; -import { HapiServer } from "../types"; - -const DEFAULT_OPTIONS = { - headers: { - accept: "application/json", - "content-type": "application/json", - }, - timeout: 60000, -}; - -export class WebhookService { - logger: any; - - constructor(server: HapiServer) { - this.logger = server.logger; - } - - async postRequest( - url: string, - data: string, - method: "POST" | "PUT" = "POST" - ) { - const request = method === "POST" ? post : put; - - let parsed; - - try { - parsed = JSON.parse(data); - } catch (e) { - this.logger.error(`Not submitting ${data}, ${e}`); - return { payload: { error: e.message } }; - } - - this.logger.info({ data: parsed }, `${method} to ${url}`); - - try { - const { payload } = await request(url, { - ...DEFAULT_OPTIONS, - payload: parsed, - }); - - const { reference } = JSON.parse(payload); - - return { payload: { reference: reference ?? "UNKNOWN" } }; - } catch (e) { - if (e.isBoom) { - return e.output; - } - this.logger.error({ data }, e); - return { payload: { error: e.message } }; - } - } -} diff --git a/submitter/src/submission/setupDatabase.ts b/submitter/src/submission/setupDatabase.ts deleted file mode 100644 index 898c2760f1..0000000000 --- a/submitter/src/submission/setupDatabase.ts +++ /dev/null @@ -1,29 +0,0 @@ -import { spawnSync } from "child_process"; -import pino from "pino"; -const logger = pino(); - -export function setupDatabase() { - const schemaLocation = require.resolve( - "@xgovformbuilder/queue-model/schema.prisma" - ); - - const child = spawnSync( - "prisma", - ["migrate", "deploy", "--schema", schemaLocation], - { - encoding: "utf-8", - stdio: "inherit", - } - ); - - if (child.status === 1) { - logger.error("Could not connect to database, exiting"); - logger.error(child.error); - process.exit(1); - } - - if (child.stdout) { - logger.info(child.stdout); - logger.info("Database migration was successful, continuing"); - } -} diff --git a/submitter/src/submission/types.ts b/submitter/src/submission/types.ts deleted file mode 100644 index d991a0cd39..0000000000 --- a/submitter/src/submission/types.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { Server } from "@hapi/hapi"; -import { Logger } from "pino"; -import { WebhookService, QueueService } from "./services"; - -type Services = ( - services: string[] -) => { - webhookService: WebhookService; - queueService: QueueService; -}; - -declare module "@hapi/hapi" { - // Here we are decorating Hapi interface types with - // props from plugins which doesn't export @types - - interface Server { - logger: Logger; - services: Services; // plugin schmervice - registerService: (services: any[]) => void; // plugin schmervice - } -} - -export type HapiServer = Server; diff --git a/submitter/tsconfig.json b/submitter/tsconfig.json deleted file mode 100644 index 0ea4038cfd..0000000000 --- a/submitter/tsconfig.json +++ /dev/null @@ -1,25 +0,0 @@ -{ - "extends": "../tsconfig.json", - "compilerOptions": { - "baseUrl": "./src", - "outDir": "dist", - "allowSyntheticDefaultImports": true, - "noImplicitAny": false, - "esModuleInterop": true, - "paths": { - "src/*": ["src/*"] - }, - "resolveJsonModule": true, - "lib": [ - "dom", - "ES2020.Promise", - "ES2019.Object", - "ES2019.Array" - ], - "skipLibCheck": true - }, - "include": [ - "src", - "package.json" - ] -} diff --git a/yarn.lock b/yarn.lock index 2a1445fbe3..4728e1e8c5 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2368,13 +2368,6 @@ __metadata: languageName: node linkType: hard -"@hapi/hoek@npm:^6.1.2": - version: 6.2.4 - resolution: "@hapi/hoek@npm:6.2.4" - checksum: 17e6e687509c20d3730dfb14b05536024d50253e96fcd0c7a4df8247e3a30568a8028b59208173f3e63dbab3eb6b71f07eff06b234dd5c2bd773d254af769e37 - languageName: node - linkType: hard - "@hapi/inert@npm:^6.0.1": version: 6.0.5 resolution: "@hapi/inert@npm:6.0.5" @@ -3667,20 +3660,6 @@ __metadata: languageName: node linkType: hard -"@prisma/client@npm:5.0.0": - version: 5.0.0 - resolution: "@prisma/client@npm:5.0.0" - dependencies: - "@prisma/engines-version": 4.17.0-26.6b0aef69b7cdfc787f822ecd7cdc76d5f1991584 - peerDependencies: - prisma: "*" - peerDependenciesMeta: - prisma: - optional: true - checksum: 332c2af44e880ffc9dd1223992bf6f45910094c7a3a540cbbfdda45d6caf3e82998376338abdf85e34a12f1082ae932f2385d6396c62fe4bba7ec6b84de54466 - languageName: node - linkType: hard - "@prisma/debug@npm:5.8.0": version: 5.8.0 resolution: "@prisma/debug@npm:5.8.0" @@ -3688,13 +3667,6 @@ __metadata: languageName: node linkType: hard -"@prisma/engines-version@npm:4.17.0-26.6b0aef69b7cdfc787f822ecd7cdc76d5f1991584": - version: 4.17.0-26.6b0aef69b7cdfc787f822ecd7cdc76d5f1991584 - resolution: "@prisma/engines-version@npm:4.17.0-26.6b0aef69b7cdfc787f822ecd7cdc76d5f1991584" - checksum: 8fcbceef3b554ee7fa404bead50be5286412a097b21723272aff298b289caf2802b01b84bb85c4c9f3b592eeac114c8d6e79db083f271dc8c54f453b4a515233 - languageName: node - linkType: hard - "@prisma/engines-version@npm:5.8.0-37.0a83d8541752d7582de2ebc1ece46519ce72a848": version: 5.8.0-37.0a83d8541752d7582de2ebc1ece46519ce72a848 resolution: "@prisma/engines-version@npm:5.8.0-37.0a83d8541752d7582de2ebc1ece46519ce72a848" @@ -3991,13 +3963,6 @@ __metadata: languageName: node linkType: hard -"@types/config@npm:^3.3.0": - version: 3.3.3 - resolution: "@types/config@npm:3.3.3" - checksum: 738c616fc385fa93e61b2236b43fbcfd93b1b53fc222fa14f2ebaef17ec7eaf03bf3d768fa8cb00df1b5173574893c020dc73eab11ddd6ea769dbf9ea113e8da - languageName: node - linkType: hard - "@types/cookie@npm:^0.4.1": version: 0.4.1 resolution: "@types/cookie@npm:0.4.1" @@ -4217,7 +4182,7 @@ __metadata: languageName: node linkType: hard -"@types/node@npm:*, @types/node@npm:^20.4.6": +"@types/node@npm:*": version: 20.10.8 resolution: "@types/node@npm:20.10.8" dependencies: @@ -5070,6 +5035,7 @@ __metadata: govuk-frontend: ^4.0.1 hmpo-components: 5.2.1 jest: ^29.2.0 + jest-cli: ^29.7.0 joi: 17.2.1 nanoid: ^3.3.4 nunjucks: ^3.2.3 @@ -5080,27 +5046,6 @@ __metadata: languageName: unknown linkType: soft -"@xgovformbuilder/queue-model@workspace:queue-model": - version: 0.0.0-use.local - resolution: "@xgovformbuilder/queue-model@workspace:queue-model" - dependencies: - "@babel/cli": ^7.23.3 - "@babel/core": ^7.23.3 - "@babel/eslint-parser": ^7.23.3 - "@babel/eslint-plugin": ^7.22.10 - "@babel/preset-env": ^7.23.3 - "@babel/preset-typescript": ^7.23.3 - "@prisma/client": 5.0.0 - "@types/node": ^20.4.6 - babel-eslint: ^10.1.0 - eslint: ^8.10.0 - eslint-plugin-import: ^2.25.4 - eslint-plugin-tsdoc: ^0.2.14 - prisma: ^5.1.1 - typescript: 4.9.5 - languageName: unknown - linkType: soft - "@xgovformbuilder/runner@workspace:runner": version: 0.0.0-use.local resolution: "@xgovformbuilder/runner@workspace:runner" @@ -5144,7 +5089,6 @@ __metadata: "@types/wreck": ^14.0.0 "@xgovformbuilder/lab-babel": 2.1.2 "@xgovformbuilder/model": "workspace:model" - "@xgovformbuilder/queue-model": "workspace:queue-model" accept-language-parser: 1.5.0 accessible-autocomplete: ^2.0.2 acorn: ^8.7.0 @@ -5198,43 +5142,6 @@ __metadata: languageName: unknown linkType: soft -"@xgovformbuilder/submitter@workspace:submitter": - version: 0.0.0-use.local - resolution: "@xgovformbuilder/submitter@workspace:submitter" - dependencies: - "@babel/cli": ^7.23.3 - "@babel/core": ^7.23.3 - "@babel/eslint-parser": ^7.23.3 - "@babel/eslint-plugin": ^7.22.10 - "@babel/plugin-proposal-export-default-from": ^7.23.3 - "@babel/plugin-transform-classes": ^7.23.3 - "@babel/plugin-transform-modules-commonjs": ^7.23.3 - "@babel/plugin-transform-runtime": ^7.23.3 - "@babel/preset-env": ^7.23.3 - "@babel/preset-typescript": ^7.23.3 - "@babel/register": ^7.22.15 - "@hapi/hapi": ^21.3.2 - "@types/config": ^3.3.0 - "@types/hapi": ^18.0.7 - "@xgovformbuilder/queue-model": "workspace:queue-model" - babel-jest: ^29.7.0 - config: ^3.3.7 - dotenv: 8.2.0 - eslint: ^8.11.0 - eslint-plugin-import: ^2.25.4 - eslint-plugin-tsdoc: ^0.2.14 - hapi-cron: ^1.1.0 - hapi-pino: 8.0.0 - jest: ^26.6.3 - jest-mock-extended: ^3.0.5 - nodemon: ^3.0.1 - pino: 8.15.1 - prisma: ^5.1.1 - schmervice: ^1.6.0 - typescript: 4.9.5 - languageName: unknown - linkType: soft - "@xmldom/xmldom@npm:0.8.6": version: 0.8.6 resolution: "@xmldom/xmldom@npm:0.8.6" @@ -7927,15 +7834,6 @@ __metadata: languageName: node linkType: hard -"cron@npm:^1.7.1": - version: 1.8.2 - resolution: "cron@npm:1.8.2" - dependencies: - moment-timezone: ^0.5.x - checksum: 9df2d2e24684e1ebce37e3e29c183b1dfecf7adb5fb16f84435014e7205926fc9868e9bfacfa5f7b93c556c9f4a6fbcce5fde9bee1306d20bc848568bb587791 - languageName: node - linkType: hard - "cross-env@npm:^7.0.3": version: 7.0.3 resolution: "cross-env@npm:7.0.3" @@ -11525,16 +11423,6 @@ __metadata: languageName: node linkType: hard -"hapi-cron@npm:^1.1.0": - version: 1.1.0 - resolution: "hapi-cron@npm:1.1.0" - dependencies: - "@hapi/hoek": ^6.1.2 - cron: ^1.7.1 - checksum: 8e24bf17d9af49aa5f544d0464109f4c5b1d8c9a2cae3034d161e8a532d024d3d646c2be77e9dc2a16311205b4b75b8daecf29516f8750ac2c66367e6936bee3 - languageName: node - linkType: hard - "hapi-pino@npm:8.0.0": version: 8.0.0 resolution: "hapi-pino@npm:8.0.0" @@ -13771,18 +13659,6 @@ __metadata: languageName: node linkType: hard -"jest-mock-extended@npm:^3.0.5": - version: 3.0.5 - resolution: "jest-mock-extended@npm:3.0.5" - dependencies: - ts-essentials: ^7.0.3 - peerDependencies: - jest: ^24.0.0 || ^25.0.0 || ^26.0.0 || ^27.0.0 || ^28.0.0 || ^29.0.0 - typescript: ^3.0.0 || ^4.0.0 || ^5.0.0 - checksum: 440c52f743af588493c2cd02fa7e4e42177748ac3f7ae720f414bd58a4a72fad4271878457bf8796b62abcf9cf32cde4dc5151caad0805037bd965cc9ef07ca8 - languageName: node - linkType: hard - "jest-mock@npm:^26.6.2": version: 26.6.2 resolution: "jest-mock@npm:26.6.2" @@ -15654,15 +15530,6 @@ __metadata: languageName: node linkType: hard -"moment-timezone@npm:^0.5.x": - version: 0.5.44 - resolution: "moment-timezone@npm:0.5.44" - dependencies: - moment: ^2.29.4 - checksum: 2f1de58f145bb87896ca06afaebaea0904f24542a900208d6a56a54f2fb50b38d9d2b61c46039ed36d0ec575140ff9ba6a5824877551763dbf3db7bda0333781 - languageName: node - linkType: hard - "moment@npm:^2.29.4": version: 2.30.1 resolution: "moment@npm:2.30.1" @@ -16056,7 +15923,7 @@ __metadata: languageName: node linkType: hard -"nodemon@npm:^3.0.1, nodemon@npm:^3.0.2": +"nodemon@npm:^3.0.2": version: 3.0.2 resolution: "nodemon@npm:3.0.2" dependencies: @@ -20483,15 +20350,6 @@ __metadata: languageName: node linkType: hard -"ts-essentials@npm:^7.0.3": - version: 7.0.3 - resolution: "ts-essentials@npm:7.0.3" - peerDependencies: - typescript: ">=3.7.0" - checksum: 74d75868acf7f8b95e447d8b3b7442ca21738c6894e576df9917a352423fde5eb43c5651da5f78997da6061458160ae1f6b279150b42f47ccc58b73e55acaa2f - languageName: node - linkType: hard - "ts-jest@npm:^29.1.1": version: 29.1.1 resolution: "ts-jest@npm:29.1.1"