From 614765d480f320c715dd57a043b63ec735ae2e95 Mon Sep 17 00:00:00 2001 From: Shahar s Date: Thu, 22 Sep 2022 14:59:30 +0300 Subject: [PATCH 1/4] feat: upgrade type orm WIP --- 1663841935533-initaialMigration.ts | 36 +++ dataSource.ts | 13 + .../1663845942790-initaialMigration.ts | 37 +++ package-lock.json | 240 +++++++++--------- package.json | 10 +- ...nectionManager.ts => connectionBuilder.ts} | 24 +- src/DAL/entity/job.ts | 15 +- src/DAL/entity/task.ts | 12 +- src/DAL/repositories/generalRepository.ts | 54 +++- src/DAL/repositories/jobRepository.ts | 45 ++-- src/DAL/repositories/taskRepository.ts | 49 ++-- src/DAL/repositories/transactionActions.ts | 108 ++++---- src/jobs/models/jobManager.ts | 2 +- src/jobs/models/taskManager.ts | 2 +- .../models/taskManagementManger.ts | 2 +- 15 files changed, 379 insertions(+), 270 deletions(-) create mode 100644 1663841935533-initaialMigration.ts create mode 100644 dataSource.ts create mode 100644 db/migrations/1663845942790-initaialMigration.ts rename src/DAL/{connectionManager.ts => connectionBuilder.ts} (80%) diff --git a/1663841935533-initaialMigration.ts b/1663841935533-initaialMigration.ts new file mode 100644 index 0000000..017bbcf --- /dev/null +++ b/1663841935533-initaialMigration.ts @@ -0,0 +1,36 @@ +import { MigrationInterface, QueryRunner } from "typeorm"; + +export class initaialMigration1663841935533 implements MigrationInterface { + name = 'initaialMigration1663841935533' + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query(`CREATE TYPE "JobManager"."Task_status_enum" AS ENUM('Pending', 'In-Progress', 'Completed', 'Failed', 'Expired', 'Aborted')`); + await queryRunner.query(`CREATE TABLE "JobManager"."Task" ("id" uuid NOT NULL DEFAULT uuid_generate_v4(), "jobId" uuid NOT NULL, "type" character varying(255) NOT NULL, "description" character varying(2000) NOT NULL DEFAULT '', "parameters" jsonb NOT NULL, "creationTime" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), "updateTime" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), "status" "JobManager"."Task_status_enum" NOT NULL DEFAULT 'Pending', "percentage" smallint, "reason" character varying NOT NULL DEFAULT '', "attempts" integer NOT NULL DEFAULT '0', "resettable" boolean NOT NULL DEFAULT true, "block_duplication" boolean NOT NULL DEFAULT false, CONSTRAINT "UQ_uniqueness_on_job_and_type" EXCLUDE EXCLUDE ("type" with =, "jobId" with =) WHERE ("block_duplication" = true, CONSTRAINT "PK_task_id" PRIMARY KEY ("id"))`); + await queryRunner.query(`CREATE INDEX "taskResettableIndex" ON "JobManager"."Task" ("resettable") WHERE "resettable" = FALSE`); + await queryRunner.query(`CREATE TYPE "JobManager"."Job_status_enum" AS ENUM('Pending', 'In-Progress', 'Completed', 'Failed', 'Expired', 'Aborted')`); + await queryRunner.query(`CREATE TABLE "JobManager"."Job" ("id" uuid NOT NULL DEFAULT uuid_generate_v4(), "resourceId" character varying(300) NOT NULL, "version" character varying(30) NOT NULL, "type" character varying(255) NOT NULL, "description" character varying(2000) NOT NULL DEFAULT '', "parameters" jsonb NOT NULL, "creationTime" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), "updateTime" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), "status" "JobManager"."Job_status_enum" NOT NULL DEFAULT 'Pending', "percentage" smallint, "reason" character varying NOT NULL DEFAULT '', "isCleaned" boolean NOT NULL DEFAULT false, "priority" integer NOT NULL DEFAULT '1000', "expirationDate" TIMESTAMP WITH TIME ZONE, "internalId" uuid, "producerName" text, "productName" text, "productType" text, "additionalIdentifiers" text, "taskCount" integer NOT NULL DEFAULT '0', "completedTasks" integer NOT NULL DEFAULT '0', "failedTasks" integer NOT NULL DEFAULT '0', "expiredTasks" integer NOT NULL DEFAULT '0', "pendingTasks" integer NOT NULL DEFAULT '0', "inProgressTasks" integer NOT NULL DEFAULT '0', "abortedTasks" integer NOT NULL DEFAULT '0', CONSTRAINT "UQ_uniqueness_on_active_tasks" EXCLUDE EXCLUDE ("resourceId" with =, "version" with =, "type" with =, "additionalIdentifiers" with =) WHERE (status = 'Pending' OR status = 'In-Progress'), CONSTRAINT "PK_job_id" PRIMARY KEY ("id"))`); + await queryRunner.query(`CREATE INDEX "jobTypeIndex" ON "JobManager"."Job" ("type") `); + await queryRunner.query(`CREATE INDEX "jobStatusIndex" ON "JobManager"."Job" ("status") `); + await queryRunner.query(`CREATE INDEX "jobCleanedIndex" ON "JobManager"."Job" ("isCleaned") `); + await queryRunner.query(`CREATE INDEX "jobPriorityIndex" ON "JobManager"."Job" ("priority") `); + await queryRunner.query(`CREATE INDEX "jobExpirationDateIndex" ON "JobManager"."Job" ("expirationDate") `); + await queryRunner.query(`CREATE INDEX "jobResourceIndex" ON "JobManager"."Job" ("resourceId", "version") `); + await queryRunner.query(`ALTER TABLE "JobManager"."Task" ADD CONSTRAINT "FK_task_job_id" FOREIGN KEY ("jobId") REFERENCES "JobManager"."Job"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE "JobManager"."Task" DROP CONSTRAINT "FK_task_job_id"`); + await queryRunner.query(`DROP INDEX "JobManager"."jobResourceIndex"`); + await queryRunner.query(`DROP INDEX "JobManager"."jobExpirationDateIndex"`); + await queryRunner.query(`DROP INDEX "JobManager"."jobPriorityIndex"`); + await queryRunner.query(`DROP INDEX "JobManager"."jobCleanedIndex"`); + await queryRunner.query(`DROP INDEX "JobManager"."jobStatusIndex"`); + await queryRunner.query(`DROP INDEX "JobManager"."jobTypeIndex"`); + await queryRunner.query(`DROP TABLE "JobManager"."Job"`); + await queryRunner.query(`DROP TYPE "JobManager"."Job_status_enum"`); + await queryRunner.query(`DROP INDEX "JobManager"."taskResettableIndex"`); + await queryRunner.query(`DROP TABLE "JobManager"."Task"`); + await queryRunner.query(`DROP TYPE "JobManager"."Task_status_enum"`); + } + +} diff --git a/dataSource.ts b/dataSource.ts new file mode 100644 index 0000000..79491fb --- /dev/null +++ b/dataSource.ts @@ -0,0 +1,13 @@ +import config from 'config'; +import { DataSource } from 'typeorm'; +import { createConnectionOptions } from './src/DAL/connectionBuilder'; +import { IDbConfig } from './src/common/interfaces'; + +const connectionOptions = config.get('typeOrm');; +config.get('typeOrm'); +export const appDataSource = new DataSource({ + ...createConnectionOptions(connectionOptions), + entities: ['src/DAL/**/*.ts'], + migrationsTableName: 'migrations_table', + migrations: ['db/migrations/*.ts'], +}); \ No newline at end of file diff --git a/db/migrations/1663845942790-initaialMigration.ts b/db/migrations/1663845942790-initaialMigration.ts new file mode 100644 index 0000000..0966942 --- /dev/null +++ b/db/migrations/1663845942790-initaialMigration.ts @@ -0,0 +1,37 @@ +import { MigrationInterface, QueryRunner } from "typeorm"; + +export class initaialMigration1663845942790 implements MigrationInterface { + name = 'initaialMigration1663845942790' + + public async up(queryRunner: QueryRunner): Promise { + //TODO: fix task type name and remove its duplication + await queryRunner.query(`CREATE TYPE "JobManager"."Task_status_enum" AS ENUM('Pending', 'In-Progress', 'Completed', 'Failed', 'Expired', 'Aborted')`); + await queryRunner.query(`CREATE TABLE "JobManager"."Task" ("id" uuid NOT NULL DEFAULT uuid_generate_v4(), "jobId" uuid NOT NULL, "type" character varying(255) NOT NULL, "description" character varying(2000) NOT NULL DEFAULT '', "parameters" jsonb NOT NULL, "creationTime" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), "updateTime" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), "status" "JobManager"."Task_status_enum" NOT NULL DEFAULT 'Pending', "percentage" smallint, "reason" character varying NOT NULL DEFAULT '', "attempts" integer NOT NULL DEFAULT '0', "resettable" boolean NOT NULL DEFAULT true, "block_duplication" boolean NOT NULL DEFAULT false, CONSTRAINT "UQ_uniqueness_on_job_and_type" EXCLUDE EXCLUDE ("type" with =, "jobId" with =) WHERE ("block_duplication" = true, CONSTRAINT "PK_task_id" PRIMARY KEY ("id"))`); + await queryRunner.query(`CREATE INDEX "taskResettableIndex" ON "JobManager"."Task" ("resettable") WHERE "resettable" = FALSE`); + await queryRunner.query(`CREATE TYPE "JobManager"."Job_status_enum" AS ENUM('Pending', 'In-Progress', 'Completed', 'Failed', 'Expired', 'Aborted')`); + await queryRunner.query(`CREATE TABLE "JobManager"."Job" ("id" uuid NOT NULL DEFAULT uuid_generate_v4(), "resourceId" character varying(300) NOT NULL, "version" character varying(30) NOT NULL, "type" character varying(255) NOT NULL, "description" character varying(2000) NOT NULL DEFAULT '', "parameters" jsonb NOT NULL, "creationTime" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), "updateTime" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), "status" "JobManager"."Job_status_enum" NOT NULL DEFAULT 'Pending', "percentage" smallint, "reason" character varying NOT NULL DEFAULT '', "isCleaned" boolean NOT NULL DEFAULT false, "priority" integer NOT NULL DEFAULT '1000', "expirationDate" TIMESTAMP WITH TIME ZONE, "internalId" uuid, "producerName" text, "productName" text, "productType" text, "additionalIdentifiers" text, "taskCount" integer NOT NULL DEFAULT '0', "completedTasks" integer NOT NULL DEFAULT '0', "failedTasks" integer NOT NULL DEFAULT '0', "expiredTasks" integer NOT NULL DEFAULT '0', "pendingTasks" integer NOT NULL DEFAULT '0', "inProgressTasks" integer NOT NULL DEFAULT '0', "abortedTasks" integer NOT NULL DEFAULT '0', CONSTRAINT "UQ_uniqueness_on_active_tasks" EXCLUDE EXCLUDE ("resourceId" with =, "version" with =, "type" with =, "additionalIdentifiers" with =) WHERE (status = 'Pending' OR status = 'In-Progress'), CONSTRAINT "PK_job_id" PRIMARY KEY ("id"))`); + await queryRunner.query(`CREATE INDEX "jobTypeIndex" ON "JobManager"."Job" ("type") `); + await queryRunner.query(`CREATE INDEX "jobStatusIndex" ON "JobManager"."Job" ("status") `); + await queryRunner.query(`CREATE INDEX "jobCleanedIndex" ON "JobManager"."Job" ("isCleaned") `); + await queryRunner.query(`CREATE INDEX "jobPriorityIndex" ON "JobManager"."Job" ("priority") `); + await queryRunner.query(`CREATE INDEX "jobExpirationDateIndex" ON "JobManager"."Job" ("expirationDate") `); + await queryRunner.query(`CREATE INDEX "jobResourceIndex" ON "JobManager"."Job" ("resourceId", "version") `); + await queryRunner.query(`ALTER TABLE "JobManager"."Task" ADD CONSTRAINT "FK_task_job_id" FOREIGN KEY ("jobId") REFERENCES "JobManager"."Job"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE "JobManager"."Task" DROP CONSTRAINT "FK_task_job_id"`); + await queryRunner.query(`DROP INDEX "JobManager"."jobResourceIndex"`); + await queryRunner.query(`DROP INDEX "JobManager"."jobExpirationDateIndex"`); + await queryRunner.query(`DROP INDEX "JobManager"."jobPriorityIndex"`); + await queryRunner.query(`DROP INDEX "JobManager"."jobCleanedIndex"`); + await queryRunner.query(`DROP INDEX "JobManager"."jobStatusIndex"`); + await queryRunner.query(`DROP INDEX "JobManager"."jobTypeIndex"`); + await queryRunner.query(`DROP TABLE "JobManager"."Job"`); + await queryRunner.query(`DROP TYPE "JobManager"."Job_status_enum"`); + await queryRunner.query(`DROP INDEX "JobManager"."taskResettableIndex"`); + await queryRunner.query(`DROP TABLE "JobManager"."Task"`); + await queryRunner.query(`DROP TYPE "JobManager"."Task_status_enum"`); + } + +} diff --git a/package-lock.json b/package-lock.json index a6d0982..a9f4a51 100644 --- a/package-lock.json +++ b/package-lock.json @@ -27,10 +27,10 @@ "express": "^4.18.1", "express-openapi-validator": "^4.13.8", "http-status-codes": "^2.2.0", - "pg": "^8.5.1", + "pg": "^8.8.0", "reflect-metadata": "^0.1.13", "tsyringe": "^4.7.0", - "typeorm": "^0.2.30" + "typeorm": "^0.3.9" }, "devDependencies": { "@commitlint/cli": "^17.0.1", @@ -2366,7 +2366,7 @@ "version": "0.8.1", "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", "integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==", - "dev": true, + "devOptional": true, "dependencies": { "@jridgewell/trace-mapping": "0.3.9" }, @@ -2378,7 +2378,7 @@ "version": "0.3.9", "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz", "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==", - "dev": true, + "devOptional": true, "dependencies": { "@jridgewell/resolve-uri": "^3.0.3", "@jridgewell/sourcemap-codec": "^1.4.10" @@ -2901,7 +2901,7 @@ "version": "3.1.0", "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.0.tgz", "integrity": "sha512-F2msla3tad+Mfht5cJq7LSXcdudKTWCVYUgw6pLFOOHSTtZlj6SWNYAp+AhuqLmWdBO2X5hPrLcu8cVP8fy28w==", - "dev": true, + "devOptional": true, "engines": { "node": ">=6.0.0" } @@ -2919,7 +2919,7 @@ "version": "1.4.14", "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.14.tgz", "integrity": "sha512-XPSJHWmi394fuUuzDnGz1wiKqWfo1yXecHQMRf2l6hztTO+nPru658AyDngaBe7isIxEkRsPR3FZh+s7iVa4Uw==", - "dev": true + "devOptional": true }, "node_modules/@jridgewell/trace-mapping": { "version": "0.3.15", @@ -3599,25 +3599,25 @@ "version": "1.0.9", "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.9.tgz", "integrity": "sha512-jNsYVVxU8v5g43Erja32laIDHXeoNvFEpX33OK4d6hljo3jDhCBDhx5dhCCTMWUojscpAagGiRkBKxpdl9fxqA==", - "dev": true + "devOptional": true }, "node_modules/@tsconfig/node12": { "version": "1.0.11", "resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.11.tgz", "integrity": "sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==", - "dev": true + "devOptional": true }, "node_modules/@tsconfig/node14": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.3.tgz", "integrity": "sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==", - "dev": true + "devOptional": true }, "node_modules/@tsconfig/node16": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.3.tgz", "integrity": "sha512-yOlFc+7UtL/89t2ZhjPvvB/DeAr3r+Dq58IgzsFkOAvVC6NMJXmCGjbptdXdR9qsX7pKcTL+s87FtYREi2dEEQ==", - "dev": true + "devOptional": true }, "node_modules/@types/babel__core": { "version": "7.1.19", @@ -3964,11 +3964,6 @@ "integrity": "sha512-iO9ZQHkZxHn4mSakYV0vFHAVDyEOIJQrV2uZ06HxEPcx+mt8swXoZHIbaaJ2crJYFfErySgktuTZ3BeLz+XmFA==", "dev": true }, - "node_modules/@types/zen-observable": { - "version": "0.8.3", - "resolved": "https://registry.npmjs.org/@types/zen-observable/-/zen-observable-0.8.3.tgz", - "integrity": "sha512-fbF6oTd4sGGy0xjHPKAt+eS2CrxJ3+6gQ3FGcBoIJR2TLAyCkCyI8JqZNy+FeON0AhVgNJoUumVoZQjBFUqHkw==" - }, "node_modules/@typescript-eslint/eslint-plugin": { "version": "5.24.0", "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.24.0.tgz", @@ -4286,7 +4281,7 @@ "version": "8.8.0", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.8.0.tgz", "integrity": "sha512-QOxyigPVrpZ2GXT+PFyZTl6TtOFc5egxHIP9IlQ+RbupQuX4RkT/Bee4/kQuC02Xkzg84JcT7oLYtDIQxp+v7w==", - "dev": true, + "devOptional": true, "bin": { "acorn": "bin/acorn" }, @@ -4307,7 +4302,7 @@ "version": "8.2.0", "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.2.0.tgz", "integrity": "sha512-k+iyHEuPgSw6SbuDpGQM+06HQUa04DZ3o+F6CSzXMvvI5KMvnaEqXe+YVe555R9nn6GPt404fos4wcgpw12SDA==", - "dev": true, + "devOptional": true, "engines": { "node": ">=0.4.0" } @@ -4435,7 +4430,7 @@ "version": "4.1.3", "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==", - "dev": true + "devOptional": true }, "node_modules/argparse": { "version": "2.0.1", @@ -6259,7 +6254,7 @@ "version": "1.1.1", "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==", - "dev": true + "devOptional": true }, "node_modules/cross-spawn": { "version": "7.0.3", @@ -6381,6 +6376,18 @@ "node": ">=8" } }, + "node_modules/date-fns": { + "version": "2.29.3", + "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-2.29.3.tgz", + "integrity": "sha512-dDCnyH2WnnKusqvZZ6+jA1O51Ibt8ZMRNkDZdyAyK4YfbDwa/cEmuztzG5pk6hqlp9aSBPYcjOlktquahGwGeA==", + "engines": { + "node": ">=0.11" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/date-fns" + } + }, "node_modules/dateformat": { "version": "4.6.3", "resolved": "https://registry.npmjs.org/dateformat/-/dateformat-4.6.3.tgz", @@ -6558,7 +6565,7 @@ "version": "4.0.2", "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==", - "dev": true, + "devOptional": true, "engines": { "node": ">=0.3.1" } @@ -6609,11 +6616,11 @@ } }, "node_modules/dotenv": { - "version": "8.6.0", - "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-8.6.0.tgz", - "integrity": "sha512-IrPdXQsk2BbzvCBGBOTmmSH5SodmqZNt4ERAZDmW4CT+tL8VtvinqywuANaFu4bOMWki16nqf0e4oC0QIaDr/g==", + "version": "16.0.2", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.0.2.tgz", + "integrity": "sha512-JvpYKUmzQhYoIFgK2MOnF3bciIZoItIIoryihy0rIA+H4Jy0FmgyKYAHCTN98P5ybGSJcIFbh6QKeJdtZd1qhA==", "engines": { - "node": ">=10" + "node": ">=12" } }, "node_modules/dotgitignore": { @@ -10606,7 +10613,7 @@ "version": "1.3.6", "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", - "dev": true + "devOptional": true }, "node_modules/makeerror": { "version": "1.0.12", @@ -11479,14 +11486,14 @@ } }, "node_modules/pg": { - "version": "8.7.3", - "resolved": "https://registry.npmjs.org/pg/-/pg-8.7.3.tgz", - "integrity": "sha512-HPmH4GH4H3AOprDJOazoIcpI49XFsHCe8xlrjHkWiapdbHK+HLtbm/GQzXYAZwmPju/kzKhjaSfMACG+8cgJcw==", + "version": "8.8.0", + "resolved": "https://registry.npmjs.org/pg/-/pg-8.8.0.tgz", + "integrity": "sha512-UXYN0ziKj+AeNNP7VDMwrehpACThH7LUl/p8TDFpEUuSejCUIwGSfxpHsPvtM6/WXFy6SU4E5RG4IJV/TZAGjw==", "dependencies": { "buffer-writer": "2.0.0", "packet-reader": "1.0.0", "pg-connection-string": "^2.5.0", - "pg-pool": "^3.5.1", + "pg-pool": "^3.5.2", "pg-protocol": "^1.5.0", "pg-types": "^2.1.0", "pgpass": "1.x" @@ -11495,7 +11502,7 @@ "node": ">= 8.0.0" }, "peerDependencies": { - "pg-native": ">=2.0.0" + "pg-native": ">=3.0.1" }, "peerDependenciesMeta": { "pg-native": { @@ -11517,9 +11524,9 @@ } }, "node_modules/pg-pool": { - "version": "3.5.1", - "resolved": "https://registry.npmjs.org/pg-pool/-/pg-pool-3.5.1.tgz", - "integrity": "sha512-6iCR0wVrro6OOHFsyavV+i6KYL4lVNyYAB9RD18w66xSzN+d8b66HiwuP30Gp1SH5O9T82fckkzsRjlrhD0ioQ==", + "version": "3.5.2", + "resolved": "https://registry.npmjs.org/pg-pool/-/pg-pool-3.5.2.tgz", + "integrity": "sha512-His3Fh17Z4eg7oANLob6ZvH8xIVen3phEZh2QuyrIl4dQSDVEabNducv6ysROKpDNPSD+12tONZVWfSgMvDD9w==", "peerDependencies": { "pg": ">=8.0" } @@ -13687,7 +13694,7 @@ "version": "10.9.1", "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.1.tgz", "integrity": "sha512-NtVysVPkxxrwFGUUxGYhfux8k78pQB3JqYBXlLRZgdGUqTO5wU/UyHop5p70iEbGhB7q5KmiZiU0Y3KlJrScEw==", - "dev": true, + "devOptional": true, "dependencies": { "@cspotcode/source-map-support": "^0.8.0", "@tsconfig/node10": "^1.0.7", @@ -13854,52 +13861,62 @@ "dev": true }, "node_modules/typeorm": { - "version": "0.2.45", - "resolved": "https://registry.npmjs.org/typeorm/-/typeorm-0.2.45.tgz", - "integrity": "sha512-c0rCO8VMJ3ER7JQ73xfk0zDnVv0WDjpsP6Q1m6CVKul7DB9iVdWLRjPzc8v2eaeBuomsbZ2+gTaYr8k1gm3bYA==", + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/typeorm/-/typeorm-0.3.9.tgz", + "integrity": "sha512-xNcE44D4hn74n7pjuMog9hRgep+BiO3IBpjEaQZ8fb56zsDz7xHT1GAeWwmGuuU+4nDEELp2mIqgSCR+zxR7Jw==", "dependencies": { "@sqltools/formatter": "^1.2.2", "app-root-path": "^3.0.0", "buffer": "^6.0.3", "chalk": "^4.1.0", "cli-highlight": "^2.1.11", - "debug": "^4.3.1", - "dotenv": "^8.2.0", - "glob": "^7.1.6", - "js-yaml": "^4.0.0", + "date-fns": "^2.28.0", + "debug": "^4.3.3", + "dotenv": "^16.0.0", + "glob": "^7.2.0", + "js-yaml": "^4.1.0", "mkdirp": "^1.0.4", "reflect-metadata": "^0.1.13", "sha.js": "^2.4.11", - "tslib": "^2.1.0", + "tslib": "^2.3.1", "uuid": "^8.3.2", "xml2js": "^0.4.23", - "yargs": "^17.0.1", - "zen-observable-ts": "^1.0.0" + "yargs": "^17.3.1" }, "bin": { - "typeorm": "cli.js" + "typeorm": "cli.js", + "typeorm-ts-node-commonjs": "cli-ts-node-commonjs.js", + "typeorm-ts-node-esm": "cli-ts-node-esm.js" + }, + "engines": { + "node": ">= 12.9.0" }, "funding": { "url": "https://opencollective.com/typeorm" }, "peerDependencies": { - "@sap/hana-client": "^2.11.14", + "@google-cloud/spanner": "^5.18.0", + "@sap/hana-client": "^2.12.25", "better-sqlite3": "^7.1.2", "hdb-pool": "^0.1.6", - "ioredis": "^4.28.3", + "ioredis": "^5.0.4", "mongodb": "^3.6.0", - "mssql": "^6.3.1", + "mssql": "^7.3.0", "mysql2": "^2.2.5", "oracledb": "^5.1.0", "pg": "^8.5.1", "pg-native": "^3.0.0", "pg-query-stream": "^4.0.0", - "redis": "^3.1.1", + "redis": "^3.1.1 || ^4.0.0", "sql.js": "^1.4.0", - "sqlite3": "^5.0.2", + "sqlite3": "^5.0.3", + "ts-node": "^10.7.0", "typeorm-aurora-data-api-driver": "^2.0.0" }, "peerDependenciesMeta": { + "@google-cloud/spanner": { + "optional": true + }, "@sap/hana-client": { "optional": true }, @@ -13942,6 +13959,9 @@ "sqlite3": { "optional": true }, + "ts-node": { + "optional": true + }, "typeorm-aurora-data-api-driver": { "optional": true } @@ -13979,7 +13999,7 @@ "version": "4.7.4", "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.7.4.tgz", "integrity": "sha512-C0WQT0gezHuw6AdY1M2jxUO83Rjf0HP7Sk1DtXj6j1EwkQNZrHAg2XPWlq62oqEhYvONq5pkC2Y9oPljWToLmQ==", - "dev": true, + "devOptional": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -14155,7 +14175,7 @@ "version": "3.0.1", "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz", "integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==", - "dev": true + "devOptional": true }, "node_modules/v8-to-istanbul": { "version": "9.0.1", @@ -14417,7 +14437,7 @@ "version": "3.1.1", "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==", - "dev": true, + "devOptional": true, "engines": { "node": ">=6" } @@ -14433,20 +14453,6 @@ "funding": { "url": "https://github.com/sponsors/sindresorhus" } - }, - "node_modules/zen-observable": { - "version": "0.8.15", - "resolved": "https://registry.npmjs.org/zen-observable/-/zen-observable-0.8.15.tgz", - "integrity": "sha512-PQ2PC7R9rslx84ndNBZB/Dkv8V8fZEpk83RLgXtYd0fwUgEjseMn1Dgajh2x6S8QbZAFa9p2qVCEuYZNgve0dQ==" - }, - "node_modules/zen-observable-ts": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/zen-observable-ts/-/zen-observable-ts-1.1.0.tgz", - "integrity": "sha512-1h4zlLSqI2cRLPJUHJFL8bCWHhkpuXkF+dbGkRaWjgDIG26DmzyshUMrdV/rL3UnR+mhaX4fRq8LPouq0MYYIA==", - "dependencies": { - "@types/zen-observable": "0.8.3", - "zen-observable": "0.8.15" - } } }, "dependencies": { @@ -16082,7 +16088,7 @@ "version": "0.8.1", "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", "integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==", - "dev": true, + "devOptional": true, "requires": { "@jridgewell/trace-mapping": "0.3.9" }, @@ -16091,7 +16097,7 @@ "version": "0.3.9", "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz", "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==", - "dev": true, + "devOptional": true, "requires": { "@jridgewell/resolve-uri": "^3.0.3", "@jridgewell/sourcemap-codec": "^1.4.10" @@ -16511,7 +16517,7 @@ "version": "3.1.0", "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.0.tgz", "integrity": "sha512-F2msla3tad+Mfht5cJq7LSXcdudKTWCVYUgw6pLFOOHSTtZlj6SWNYAp+AhuqLmWdBO2X5hPrLcu8cVP8fy28w==", - "dev": true + "devOptional": true }, "@jridgewell/set-array": { "version": "1.1.2", @@ -16523,7 +16529,7 @@ "version": "1.4.14", "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.14.tgz", "integrity": "sha512-XPSJHWmi394fuUuzDnGz1wiKqWfo1yXecHQMRf2l6hztTO+nPru658AyDngaBe7isIxEkRsPR3FZh+s7iVa4Uw==", - "dev": true + "devOptional": true }, "@jridgewell/trace-mapping": { "version": "0.3.15", @@ -17044,25 +17050,25 @@ "version": "1.0.9", "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.9.tgz", "integrity": "sha512-jNsYVVxU8v5g43Erja32laIDHXeoNvFEpX33OK4d6hljo3jDhCBDhx5dhCCTMWUojscpAagGiRkBKxpdl9fxqA==", - "dev": true + "devOptional": true }, "@tsconfig/node12": { "version": "1.0.11", "resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.11.tgz", "integrity": "sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==", - "dev": true + "devOptional": true }, "@tsconfig/node14": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.3.tgz", "integrity": "sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==", - "dev": true + "devOptional": true }, "@tsconfig/node16": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.3.tgz", "integrity": "sha512-yOlFc+7UtL/89t2ZhjPvvB/DeAr3r+Dq58IgzsFkOAvVC6NMJXmCGjbptdXdR9qsX7pKcTL+s87FtYREi2dEEQ==", - "dev": true + "devOptional": true }, "@types/babel__core": { "version": "7.1.19", @@ -17409,11 +17415,6 @@ "integrity": "sha512-iO9ZQHkZxHn4mSakYV0vFHAVDyEOIJQrV2uZ06HxEPcx+mt8swXoZHIbaaJ2crJYFfErySgktuTZ3BeLz+XmFA==", "dev": true }, - "@types/zen-observable": { - "version": "0.8.3", - "resolved": "https://registry.npmjs.org/@types/zen-observable/-/zen-observable-0.8.3.tgz", - "integrity": "sha512-fbF6oTd4sGGy0xjHPKAt+eS2CrxJ3+6gQ3FGcBoIJR2TLAyCkCyI8JqZNy+FeON0AhVgNJoUumVoZQjBFUqHkw==" - }, "@typescript-eslint/eslint-plugin": { "version": "5.24.0", "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.24.0.tgz", @@ -17588,7 +17589,7 @@ "version": "8.8.0", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.8.0.tgz", "integrity": "sha512-QOxyigPVrpZ2GXT+PFyZTl6TtOFc5egxHIP9IlQ+RbupQuX4RkT/Bee4/kQuC02Xkzg84JcT7oLYtDIQxp+v7w==", - "dev": true + "devOptional": true }, "acorn-jsx": { "version": "5.3.2", @@ -17601,7 +17602,7 @@ "version": "8.2.0", "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.2.0.tgz", "integrity": "sha512-k+iyHEuPgSw6SbuDpGQM+06HQUa04DZ3o+F6CSzXMvvI5KMvnaEqXe+YVe555R9nn6GPt404fos4wcgpw12SDA==", - "dev": true + "devOptional": true }, "add-stream": { "version": "1.0.0", @@ -17689,7 +17690,7 @@ "version": "4.1.3", "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==", - "dev": true + "devOptional": true }, "argparse": { "version": "2.0.1", @@ -19093,7 +19094,7 @@ "version": "1.1.1", "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==", - "dev": true + "devOptional": true }, "cross-spawn": { "version": "7.0.3", @@ -19191,6 +19192,11 @@ "integrity": "sha512-2iy1EkLdlBzQGvbweYRFxmFath8+K7+AKB0TlhHWkNuH+TmovaMH/Wp7V7R4u7f4SnX3OgLsU9t1NI9ioDnUpg==", "dev": true }, + "date-fns": { + "version": "2.29.3", + "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-2.29.3.tgz", + "integrity": "sha512-dDCnyH2WnnKusqvZZ6+jA1O51Ibt8ZMRNkDZdyAyK4YfbDwa/cEmuztzG5pk6hqlp9aSBPYcjOlktquahGwGeA==" + }, "dateformat": { "version": "4.6.3", "resolved": "https://registry.npmjs.org/dateformat/-/dateformat-4.6.3.tgz", @@ -19319,7 +19325,7 @@ "version": "4.0.2", "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==", - "dev": true + "devOptional": true }, "diff-sequences": { "version": "27.5.1", @@ -19355,9 +19361,9 @@ } }, "dotenv": { - "version": "8.6.0", - "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-8.6.0.tgz", - "integrity": "sha512-IrPdXQsk2BbzvCBGBOTmmSH5SodmqZNt4ERAZDmW4CT+tL8VtvinqywuANaFu4bOMWki16nqf0e4oC0QIaDr/g==" + "version": "16.0.2", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.0.2.tgz", + "integrity": "sha512-JvpYKUmzQhYoIFgK2MOnF3bciIZoItIIoryihy0rIA+H4Jy0FmgyKYAHCTN98P5ybGSJcIFbh6QKeJdtZd1qhA==" }, "dotgitignore": { "version": "2.1.0", @@ -22367,7 +22373,7 @@ "version": "1.3.6", "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", - "dev": true + "devOptional": true }, "makeerror": { "version": "1.0.12", @@ -23043,14 +23049,14 @@ "dev": true }, "pg": { - "version": "8.7.3", - "resolved": "https://registry.npmjs.org/pg/-/pg-8.7.3.tgz", - "integrity": "sha512-HPmH4GH4H3AOprDJOazoIcpI49XFsHCe8xlrjHkWiapdbHK+HLtbm/GQzXYAZwmPju/kzKhjaSfMACG+8cgJcw==", + "version": "8.8.0", + "resolved": "https://registry.npmjs.org/pg/-/pg-8.8.0.tgz", + "integrity": "sha512-UXYN0ziKj+AeNNP7VDMwrehpACThH7LUl/p8TDFpEUuSejCUIwGSfxpHsPvtM6/WXFy6SU4E5RG4IJV/TZAGjw==", "requires": { "buffer-writer": "2.0.0", "packet-reader": "1.0.0", "pg-connection-string": "^2.5.0", - "pg-pool": "^3.5.1", + "pg-pool": "^3.5.2", "pg-protocol": "^1.5.0", "pg-types": "^2.1.0", "pgpass": "1.x" @@ -23067,9 +23073,9 @@ "integrity": "sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw==" }, "pg-pool": { - "version": "3.5.1", - "resolved": "https://registry.npmjs.org/pg-pool/-/pg-pool-3.5.1.tgz", - "integrity": "sha512-6iCR0wVrro6OOHFsyavV+i6KYL4lVNyYAB9RD18w66xSzN+d8b66HiwuP30Gp1SH5O9T82fckkzsRjlrhD0ioQ==", + "version": "3.5.2", + "resolved": "https://registry.npmjs.org/pg-pool/-/pg-pool-3.5.2.tgz", + "integrity": "sha512-His3Fh17Z4eg7oANLob6ZvH8xIVen3phEZh2QuyrIl4dQSDVEabNducv6ysROKpDNPSD+12tONZVWfSgMvDD9w==", "requires": {} }, "pg-protocol": { @@ -24749,7 +24755,7 @@ "version": "10.9.1", "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.1.tgz", "integrity": "sha512-NtVysVPkxxrwFGUUxGYhfux8k78pQB3JqYBXlLRZgdGUqTO5wU/UyHop5p70iEbGhB7q5KmiZiU0Y3KlJrScEw==", - "dev": true, + "devOptional": true, "requires": { "@cspotcode/source-map-support": "^0.8.0", "@tsconfig/node10": "^1.0.7", @@ -24865,27 +24871,27 @@ "dev": true }, "typeorm": { - "version": "0.2.45", - "resolved": "https://registry.npmjs.org/typeorm/-/typeorm-0.2.45.tgz", - "integrity": "sha512-c0rCO8VMJ3ER7JQ73xfk0zDnVv0WDjpsP6Q1m6CVKul7DB9iVdWLRjPzc8v2eaeBuomsbZ2+gTaYr8k1gm3bYA==", + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/typeorm/-/typeorm-0.3.9.tgz", + "integrity": "sha512-xNcE44D4hn74n7pjuMog9hRgep+BiO3IBpjEaQZ8fb56zsDz7xHT1GAeWwmGuuU+4nDEELp2mIqgSCR+zxR7Jw==", "requires": { "@sqltools/formatter": "^1.2.2", "app-root-path": "^3.0.0", "buffer": "^6.0.3", "chalk": "^4.1.0", "cli-highlight": "^2.1.11", - "debug": "^4.3.1", - "dotenv": "^8.2.0", - "glob": "^7.1.6", - "js-yaml": "^4.0.0", + "date-fns": "^2.28.0", + "debug": "^4.3.3", + "dotenv": "^16.0.0", + "glob": "^7.2.0", + "js-yaml": "^4.1.0", "mkdirp": "^1.0.4", "reflect-metadata": "^0.1.13", "sha.js": "^2.4.11", - "tslib": "^2.1.0", + "tslib": "^2.3.1", "uuid": "^8.3.2", "xml2js": "^0.4.23", - "yargs": "^17.0.1", - "zen-observable-ts": "^1.0.0" + "yargs": "^17.3.1" }, "dependencies": { "buffer": { @@ -24908,7 +24914,7 @@ "version": "4.7.4", "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.7.4.tgz", "integrity": "sha512-C0WQT0gezHuw6AdY1M2jxUO83Rjf0HP7Sk1DtXj6j1EwkQNZrHAg2XPWlq62oqEhYvONq5pkC2Y9oPljWToLmQ==", - "dev": true + "devOptional": true }, "uglify-js": { "version": "3.17.0", @@ -25024,7 +25030,7 @@ "version": "3.0.1", "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz", "integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==", - "dev": true + "devOptional": true }, "v8-to-istanbul": { "version": "9.0.1", @@ -25223,27 +25229,13 @@ "version": "3.1.1", "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==", - "dev": true + "devOptional": true }, "yocto-queue": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", "dev": true - }, - "zen-observable": { - "version": "0.8.15", - "resolved": "https://registry.npmjs.org/zen-observable/-/zen-observable-0.8.15.tgz", - "integrity": "sha512-PQ2PC7R9rslx84ndNBZB/Dkv8V8fZEpk83RLgXtYd0fwUgEjseMn1Dgajh2x6S8QbZAFa9p2qVCEuYZNgve0dQ==" - }, - "zen-observable-ts": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/zen-observable-ts/-/zen-observable-ts-1.1.0.tgz", - "integrity": "sha512-1h4zlLSqI2cRLPJUHJFL8bCWHhkpuXkF+dbGkRaWjgDIG26DmzyshUMrdV/rL3UnR+mhaX4fRq8LPouq0MYYIA==", - "requires": { - "@types/zen-observable": "0.8.3", - "zen-observable": "0.8.15" - } } } } diff --git a/package.json b/package.json index d14794e..2a5a175 100644 --- a/package.json +++ b/package.json @@ -20,7 +20,11 @@ "start": "npm run build && cd dist && node ./index.js", "assets:copy": "copyfiles -f ./config/* ./dist/config && copyfiles -f ./openapi3.yaml ./dist/ && copyfiles ./package.json dist", "clean": "rimraf dist", - "install": "npx husky install" + "install": "npx husky install", + "typeorm": "node --require ts-node/register ./node_modules/typeorm/cli.js -d dataSource.ts", + "migration:create": "npm run typeorm migration:generate", + "migration:run": "npm run typeorm migration:run", + "migration:revert": "npm run typeorm migration:revert" }, "directories": { "test": "tests" @@ -56,10 +60,10 @@ "express": "^4.18.1", "express-openapi-validator": "^4.13.8", "http-status-codes": "^2.2.0", - "pg": "^8.5.1", + "pg": "^8.8.0", "reflect-metadata": "^0.1.13", "tsyringe": "^4.7.0", - "typeorm": "^0.2.30" + "typeorm": "^0.3.9" }, "devDependencies": { "@commitlint/cli": "^17.0.1", diff --git a/src/DAL/connectionManager.ts b/src/DAL/connectionBuilder.ts similarity index 80% rename from src/DAL/connectionManager.ts rename to src/DAL/connectionBuilder.ts index 2a7684d..2e6262b 100644 --- a/src/DAL/connectionManager.ts +++ b/src/DAL/connectionBuilder.ts @@ -1,5 +1,5 @@ import { readFileSync } from 'fs'; -import { createConnection, Connection, ObjectType, QueryRunner, ConnectionOptions } from 'typeorm'; +import { createConnection, Connection, ObjectType, QueryRunner, DataSourceOptions } from 'typeorm'; import { inject, singleton } from 'tsyringe'; import { Logger } from '@map-colonies/js-logger'; import { DBConnectionError } from '../common/errors'; @@ -20,7 +20,7 @@ export class ConnectionManager { this.logger.info(`connection to database ${connectionConfig.database as string} on ${connectionConfig.host as string}`); try { if (this.connectionStatusPromise === undefined) { - this.connectionStatusPromise = createConnection(this.createConnectionOptions(connectionConfig)); + this.connectionStatusPromise = createConnection(createConnectionOptions(connectionConfig)); } this.connection = await this.connectionStatusPromise; } catch (err) { @@ -48,16 +48,7 @@ export class ConnectionManager { } const connection = this.connection as Connection; return connection.createQueryRunner(); - } - - private createConnectionOptions(dbConfig: IDbConfig): ConnectionOptions { - const { enableSslAuth, sslPaths, ...connectionOptions } = dbConfig; - if (enableSslAuth) { - connectionOptions.password = undefined; - connectionOptions.ssl = { key: readFileSync(sslPaths.key), cert: readFileSync(sslPaths.cert), ca: readFileSync(sslPaths.ca) }; - } - return connectionOptions; - } + } private getRepository(repository: ObjectType): T { if (!this.isConnected()) { @@ -70,3 +61,12 @@ export class ConnectionManager { } } } + +export const createConnectionOptions = (dbConfig: IDbConfig):DataSourceOptions => { + const { enableSslAuth, sslPaths, ...connectionOptions } = dbConfig; + if (enableSslAuth) { + connectionOptions.password = undefined; + connectionOptions.ssl = { key: readFileSync(sslPaths.key), cert: readFileSync(sslPaths.cert), ca: readFileSync(sslPaths.ca) }; + } + return connectionOptions; +} diff --git a/src/DAL/entity/job.ts b/src/DAL/entity/job.ts index 8d3bd41..b805b72 100644 --- a/src/DAL/entity/job.ts +++ b/src/DAL/entity/job.ts @@ -1,15 +1,12 @@ -import { Entity, Column, PrimaryColumn, Index, UpdateDateColumn, Generated, CreateDateColumn, OneToMany } from 'typeorm'; +import { Entity, Column, PrimaryColumn, Index, UpdateDateColumn, Generated, CreateDateColumn, OneToMany, Exclusion } from 'typeorm'; import { OperationStatus } from '../../common/dataModels/enums'; import { TaskEntity } from './task'; @Entity('Job') @Index('jobResourceIndex', ['resourceId', 'version'], { unique: false }) -@Index('jobStatusIndex', ['status'], { unique: false }) -@Index('jobTypeIndex', ['type'], { unique: false }) -@Index('jobCleanedIndex', ['isCleaned'], { unique: false }) -@Index('additionalIdentifiersIndex', ['additionalIdentifiers'], { unique: false }) +@Exclusion('UQ_uniqueness_on_active_tasks','EXCLUDE ("resourceId" with =, "version" with =, "type" with =, "additionalIdentifiers" with =) WHERE (status = \'Pending\' OR status = \'In-Progress\')') export class JobEntity { - @PrimaryColumn({ type: 'uuid' }) + @PrimaryColumn({ type: 'uuid', primaryKeyConstraintName: 'PK_job_id' }) @Generated('uuid') public id: string; @@ -19,6 +16,7 @@ export class JobEntity { @Column('varchar', { length: 30, nullable: false }) public version: string; + @Index('jobTypeIndex') @Column('varchar', { length: 255, nullable: false }) public type: string; @@ -38,6 +36,7 @@ export class JobEntity { }) public updateTime: Date; + @Index('jobStatusIndex') @Column({ type: 'enum', enum: OperationStatus, default: OperationStatus.PENDING, nullable: false }) public status: OperationStatus; @@ -47,12 +46,15 @@ export class JobEntity { @Column('varchar', { default: '', nullable: false }) public reason: string; + @Index('jobCleanedIndex') @Column('boolean', { default: false, nullable: false }) public isCleaned: boolean; + @Index('jobPriorityIndex') @Column('int', { default: 1000, nullable: false }) public priority: number; + @Index('jobExpirationDateIndex') @Column('timestamp with time zone', { nullable: true }) public expirationDate?: Date; @@ -68,6 +70,7 @@ export class JobEntity { @Column('text', { nullable: true }) public productType: string; + @Column('additionalIdentifiersIndex') @Column('text', { nullable: true }) public additionalIdentifiers: string | undefined; diff --git a/src/DAL/entity/task.ts b/src/DAL/entity/task.ts index 5652887..e3e683d 100644 --- a/src/DAL/entity/task.ts +++ b/src/DAL/entity/task.ts @@ -1,10 +1,11 @@ -import { Entity, Column, PrimaryColumn, UpdateDateColumn, Generated, CreateDateColumn, ManyToOne, JoinColumn } from 'typeorm'; +import { Entity, Column, PrimaryColumn, UpdateDateColumn, Generated, CreateDateColumn, ManyToOne, JoinColumn, Exclusion, Index } from 'typeorm'; import { OperationStatus } from '../../common/dataModels/enums'; import { JobEntity } from './job'; @Entity('Task') +@Exclusion('UQ_uniqueness_on_job_and_type','EXCLUDE ("type" with =, "jobId" with =) WHERE ("block_duplication" = true)') export class TaskEntity { - @PrimaryColumn({ type: 'uuid' }) + @PrimaryColumn({ type: 'uuid', primaryKeyConstraintName: 'PK_task_id' }) @Generated('uuid') public id: string; @@ -12,8 +13,8 @@ export class TaskEntity { @Column({ name: 'jobId' }) public jobId: string; - @ManyToOne(() => JobEntity, (job) => job.tasks, { nullable: false }) - @JoinColumn({ name: 'jobId' }) + @ManyToOne(() => JobEntity, (job) => job.tasks, { nullable: false,cascade: false }) + @JoinColumn({ name: 'jobId', foreignKeyConstraintName:'FK_task_job_id', }) public job: JobEntity; @Column('varchar', { length: 255 }) @@ -41,12 +42,13 @@ export class TaskEntity { @Column('smallint', { nullable: true }) public percentage: number; - @Column('varchar', { default: '', nullable: false }) + @Column('text', { default: '', nullable: false }) public reason: string; @Column('integer', { nullable: false, default: 0 }) public attempts: number; + @Index('taskResettableIndex', {where:'"resettable" = FALSE'}) @Column('boolean', { nullable: false, default: true }) public resettable: boolean; diff --git a/src/DAL/repositories/generalRepository.ts b/src/DAL/repositories/generalRepository.ts index 272dae7..4fba74e 100644 --- a/src/DAL/repositories/generalRepository.ts +++ b/src/DAL/repositories/generalRepository.ts @@ -1,21 +1,51 @@ -import { container } from 'tsyringe'; -import { Repository } from 'typeorm'; -import { SERVICES } from '../../common/constants'; +import { ObjectLiteral, Repository } from 'typeorm'; import { IConfig, IDbConfig } from '../../common/interfaces'; -export class GeneralRepository extends Repository { +export abstract class GeneralRepository { protected readonly dbConfig: IDbConfig; - private readonly config: IConfig; - public constructor() { - super(); - this.config = container.resolve(SERVICES.CONFIG); + protected metadata = this.repository.metadata; + protected manager = this.repository.manager; + protected queryRunner = this.repository.queryRunner; + + protected constructor(private readonly repository : Repository, protected readonly config: IConfig) { this.dbConfig = this.config.get('typeOrm'); } - + /* eslint-disable-next-line @typescript-eslint/no-explicit-any */ - public async query(query: string, parameters?: any[] | undefined): Promise { - await super.query(`SET search_path TO "${this.dbConfig.schema as string}", public`); - return super.query(query, parameters); + protected async query(query: string, parameters?: any[] | undefined): Promise { + await this.repository.query(`SET search_path TO "${this.dbConfig.schema as string}", public`); + return this.repository.query(query, parameters); } + + /* eslint-disable @typescript-eslint/member-ordering */ + protected createQueryBuilder = this.repository.createQueryBuilder.bind(this.repository); + protected count = this.repository.count.bind(this.repository); + protected countBy = this.repository.countBy.bind(this.repository); + protected create = this.repository.create.bind(this.repository); + protected decrement = this.repository.decrement.bind(this.repository); + protected delete = this.repository.delete.bind(this.repository); + protected find = this.repository.find.bind(this.repository); + protected findAndCount = this.repository.findAndCount.bind(this.repository); + protected findAndCountBy = this.repository.findAndCountBy.bind(this.repository); + protected findBy = this.repository.findBy.bind(this.repository); + protected findOne = this.repository.findOne.bind(this.repository); + protected findOneBy = this.repository.findOneBy.bind(this.repository); + protected findOneByOrFail = this.repository.findOneByOrFail.bind(this.repository); + protected findOneOrFail = this.repository.findOneOrFail.bind(this.repository); + protected getId = this.repository.getId.bind(this.repository); + protected hasId = this.repository.hasId.bind(this.repository); + protected increment = this.repository.increment.bind(this.repository); + protected insert = this.repository.insert.bind(this.repository); + protected merge = this.repository.merge.bind(this.repository); + protected preload = this.repository.preload.bind(this.repository); + protected recover = this.repository.recover.bind(this.repository); + protected remove = this.repository.remove.bind(this.repository); + protected restore = this.repository.restore.bind(this.repository); + protected save = this.repository.save.bind(this.repository); + protected softDelete = this.repository.softDelete.bind(this.repository); + protected softRemove = this.repository.softRemove.bind(this.repository); + protected update = this.repository.update.bind(this.repository); + protected upsert = this.repository.upsert.bind(this.repository); + } diff --git a/src/DAL/repositories/jobRepository.ts b/src/DAL/repositories/jobRepository.ts index 19bd573..e4c7864 100644 --- a/src/DAL/repositories/jobRepository.ts +++ b/src/DAL/repositories/jobRepository.ts @@ -1,5 +1,5 @@ -import { EntityRepository, FindManyOptions, LessThan, Brackets, Between, LessThanOrEqual, MoreThanOrEqual } from 'typeorm'; -import { container } from 'tsyringe'; +import { FindManyOptions, LessThan, Brackets, Between, LessThanOrEqual, MoreThanOrEqual, DataSource } from 'typeorm'; +import { inject, singleton } from 'tsyringe'; import { Logger } from '@map-colonies/js-logger'; import { ConflictError, NotFoundError } from '@map-colonies/error-types'; import { DBConstraintError } from '../../common/errors'; @@ -15,18 +15,15 @@ import { } from '../../common/dataModels/jobs'; import { JobModelConvertor } from '../convertors/jobModelConverter'; import { OperationStatus } from '../../common/dataModels/enums'; +import { IConfig } from '../../common/interfaces'; import { GeneralRepository } from './generalRepository'; -@EntityRepository(JobEntity) +@singleton() export class JobRepository extends GeneralRepository { - private readonly appLogger: Logger; //don't override internal repository logger. - private readonly jobConvertor: JobModelConvertor; - public constructor() { - super(); - //direct injection don't work here due to being initialized by typeOrm - this.appLogger = container.resolve(SERVICES.LOGGER); - this.jobConvertor = container.resolve(JobModelConvertor); + public constructor(db: DataSource, private readonly jobConvertor: JobModelConvertor, + @inject(SERVICES.CONFIG) config: IConfig, @inject(SERVICES.LOGGER)private readonly logger: Logger) { + super(db.getRepository(JobEntity), config); } public async findJobs(req: IFindJobsRequest): Promise { @@ -65,11 +62,11 @@ export class JobRepository extends GeneralRepository { } public async createJob(req: ICreateJobBody): Promise { - this.appLogger.info({ resourceId: req.resourceId, version: req.version, type: req.type, msg: 'Start job creation ' }); + this.logger.info({ resourceId: req.resourceId, version: req.version, type: req.type, msg: 'Start job creation ' }); try { let entity = this.jobConvertor.createModelToEntity(req); entity = await this.save(entity); - this.appLogger.info({ resourceId: entity.resourceId, version: entity.version, type: entity.type, msg: 'Job was created successfully' }); + this.logger.info({ resourceId: entity.resourceId, version: entity.version, type: entity.type, msg: 'Job was created successfully' }); return { id: entity.id, taskIds: entity.tasks ? entity.tasks.map((task) => task.id) : [], @@ -80,7 +77,7 @@ export class JobRepository extends GeneralRepository { if (error.code === pgExclusionViolationErrorCode) { if (error.message.includes('UQ_uniqueness_on_active_tasks')) { const message = 'failed to create job because another active job exists for provided resource, version and identifiers.'; - this.appLogger.error({ + this.logger.error({ resourceId: req.resourceId, version: req.version, type: req.type, @@ -91,7 +88,7 @@ export class JobRepository extends GeneralRepository { } if (error.message.includes('UQ_uniqness_on_job_and_type')) { const message = 'failed to create job, for provided resource:, version and identifiers, because it contains duplicate tasks.'; - this.appLogger.error({ + this.logger.error({ resourceId: req.resourceId, version: req.version, type: req.type, @@ -108,48 +105,48 @@ export class JobRepository extends GeneralRepository { public async getJob(id: string, shouldReturnTasks = true): Promise { let entity; if (!shouldReturnTasks) { - entity = await this.findOne(id); + entity = await this.findOneBy({id}); } else { - entity = await this.findOne(id, { relations: ['tasks'] }); + entity = await this.findOne({where: {id}, relations: ['tasks'] }); } const model = entity ? this.jobConvertor.entityToModel(entity) : undefined; return model; } public async updateJob(req: IUpdateJobRequest): Promise { - this.appLogger.info({ jobId: req.jobId, msg: 'start job update' }); + this.logger.info({ jobId: req.jobId, msg: 'start job update' }); if (!(await this.exists(req.jobId))) { const message = 'job was not found for provided update request'; - this.appLogger.error({ jobId: req.jobId, msg: message }); + this.logger.error({ jobId: req.jobId, msg: message }); throw new NotFoundError(message); } const entity = this.jobConvertor.updateModelToEntity(req); await this.save(entity); - this.appLogger.info({ jobId: req.jobId, msg: 'Job was updated successfully' }); + this.logger.info({ jobId: req.jobId, msg: 'Job was updated successfully' }); } public async exists(id: string): Promise { - const jobCount = await this.count({ id: id }); + const jobCount = await this.countBy({ id: id }); return jobCount === 1; } public async deleteJob(id: string): Promise { if (!(await this.exists(id))) { const message = 'job id was not found for delete request'; - this.appLogger.error({ id: id, msg: message }); + this.logger.error({ id: id, msg: message }); throw new NotFoundError(message); } try { await this.delete(id); - this.appLogger.info({ id: id, msg: 'Finish job deletion successfully' }); + this.logger.info({ id: id, msg: 'Finish job deletion successfully' }); } catch (err) { const pgForeignKeyConstraintViolationErrorCode = '23503'; const error = err as Error & { code: string }; if (error.code === pgForeignKeyConstraintViolationErrorCode) { - this.appLogger.error({ jobId: id, errorMessage: error.message, errorCode: error.code, msg: 'failed job deletion because it have tasks' }); + this.logger.error({ jobId: id, errorMessage: error.message, errorCode: error.code, msg: 'failed job deletion because it have tasks' }); throw new DBConstraintError(`job ${id} have tasks`); } else { - this.appLogger.error({ jobId: id, errorMessage: error.message, errorCode: error.code, msg: 'failed job deletion' }); + this.logger.error({ jobId: id, errorMessage: error.message, errorCode: error.code, msg: 'failed job deletion' }); throw err; } } diff --git a/src/DAL/repositories/taskRepository.ts b/src/DAL/repositories/taskRepository.ts index c02ef14..822c3c7 100644 --- a/src/DAL/repositories/taskRepository.ts +++ b/src/DAL/repositories/taskRepository.ts @@ -1,5 +1,5 @@ -import { EntityRepository, In, LessThan, Brackets, UpdateResult } from 'typeorm'; -import { container } from 'tsyringe'; +import { DataSource, In, LessThan, Brackets, UpdateResult, FindOptionsWhere } from 'typeorm'; +import { inject, singleton } from 'tsyringe'; import { Logger } from '@map-colonies/js-logger'; import { ConflictError, NotFoundError } from '@map-colonies/error-types'; import { SERVICES } from '../../common/constants'; @@ -20,39 +20,34 @@ import { IUpdateTaskRequest, } from '../../common/dataModels/tasks'; import { OperationStatus } from '../../common/dataModels/enums'; +import { IConfig } from '../../common/interfaces'; import { GeneralRepository } from './generalRepository'; declare type SqlRawResponse = [unknown[], number]; -@EntityRepository(TaskEntity) +@singleton() export class TaskRepository extends GeneralRepository { - private readonly appLogger: Logger; //don't override internal repository logger. - private readonly taskConvertor: TaskModelConvertor; - public constructor() { - super(); - //direct injection don't work here due to being initialized by typeOrm - this.appLogger = container.resolve(SERVICES.LOGGER); - this.taskConvertor = container.resolve(TaskModelConvertor); + public constructor(db: DataSource, private readonly taskConvertor: TaskModelConvertor, + @inject(SERVICES.CONFIG) config: IConfig, @inject(SERVICES.LOGGER)private readonly logger: Logger) { + super(db.getRepository(TaskEntity),config); } public async getTasks(req: IAllTasksParams): Promise { - const entities = await this.find(req); + const entities = await this.findBy(req); const models = entities.map((entity) => this.taskConvertor.entityToModel(entity)); return models; } public async findTasks(req: IFindTasksRequest): Promise { - const entities = await this.find({ - where: req, - }); + const entities = await this.findBy(req as FindOptionsWhere); const models = entities.map((entity) => this.taskConvertor.entityToModel(entity)); return models; } public async createTask(req: CreateTasksRequest): Promise { const jobId = Array.isArray(req) ? req[0].jobId : req.jobId; - this.appLogger.info({ jobId: jobId, msg: 'Start task(s) creation' }); + this.logger.info({ jobId: jobId, msg: 'Start task(s) creation' }); if (Array.isArray(req)) { const ids: string[] = []; const errors: string[] = []; @@ -67,51 +62,51 @@ export class TaskRepository extends GeneralRepository { } else { const error = err as Error; const message = 'failed create task'; - this.appLogger.error({ jobId: request.jobId, taskType: request.type, err: error, msg: message }); + this.logger.error({ jobId: request.jobId, taskType: request.type, err: error, msg: message }); throw err; } } } - this.appLogger.info({ jobId: jobId, ids: ids, errors: errors, msg: 'Finished tasks creation successfully' }); + this.logger.info({ jobId: jobId, ids: ids, errors: errors, msg: 'Finished tasks creation successfully' }); return errors.length != 0 ? { ids, errors } : { ids }; } else { const res = await this.createSingleTask(req); - this.appLogger.info({ jobId: jobId, id: res.id, msg: 'Finished task creation successfully' }); + this.logger.info({ jobId: jobId, id: res.id, msg: 'Finished task creation successfully' }); return res; } } public async getTask(req: ISpecificTaskParams): Promise { - const entity = await this.findOne({ id: req.taskId, jobId: req.jobId }); + const entity = await this.findOneBy({ id: req.taskId, jobId: req.jobId }); const model = entity ? this.taskConvertor.entityToModel(entity) : undefined; return model; } public async exists(taskIdentifier: ISpecificTaskParams): Promise { - const taskCount = await this.count({ id: taskIdentifier.taskId, jobId: taskIdentifier.jobId }); + const taskCount = await this.countBy({ id: taskIdentifier.taskId, jobId: taskIdentifier.jobId }); return taskCount === 1; } public async updateTask(req: IUpdateTaskRequest): Promise { - this.appLogger.info({ jobId: req.jobId, taskId: req.taskId, msg: 'Start task update successfully' }); + this.logger.info({ jobId: req.jobId, taskId: req.taskId, msg: 'Start task update successfully' }); if (!(await this.exists(req))) { const message = 'task for update not found with provided jobId and taskId'; - this.appLogger.error({ jobId: req.jobId, taskId: req.taskId, msg: message }); + this.logger.error({ jobId: req.jobId, taskId: req.taskId, msg: message }); throw new NotFoundError(message); } const entity = this.taskConvertor.updateModelToEntity(req); await this.save(entity); - this.appLogger.info({ jobId: req.jobId, taskId: req.taskId, msg: 'Finish task update successfully' }); + this.logger.info({ jobId: req.jobId, taskId: req.taskId, msg: 'Finish task update successfully' }); } public async deleteTask(taskIdentifier: ISpecificTaskParams): Promise { if (!(await this.exists(taskIdentifier))) { const message = 'provided task not found for delete'; - this.appLogger.error({ id: taskIdentifier.taskId, jobId: taskIdentifier.jobId, msg: message }); + this.logger.error({ id: taskIdentifier.taskId, jobId: taskIdentifier.jobId, msg: message }); throw new NotFoundError(message); } await this.delete({ id: taskIdentifier.taskId, jobId: taskIdentifier.jobId }); - this.appLogger.info({ id: taskIdentifier.taskId, jobId: taskIdentifier.jobId, msg: 'Finish task deletion successfully' }); + this.logger.info({ id: taskIdentifier.taskId, jobId: taskIdentifier.jobId, msg: 'Finish task deletion successfully' }); } public async retrieveAndUpdate(jobType: string, taskType: string): Promise { @@ -261,12 +256,12 @@ export class TaskRepository extends GeneralRepository { const error = err as Error & { code: string }; if (error.code === pgForeignKeyViolationErrorCode && error.message.includes('FK_task_job_id')) { const message = `failed to create task for job: ${req.jobId}, job id was not found.`; - this.appLogger.error(message); + this.logger.error(message); throw new NotFoundError(message); } if (error.code === pgExclusionViolationErrorCode && error.message.includes('UQ_uniqueness_on_job_and_type')) { const message = `failed to create ${req.type as string} task for job ${req.jobId} because it already exists.`; - this.appLogger.warn(message); + this.logger.warn(message); throw new ConflictError(message); } throw err; diff --git a/src/DAL/repositories/transactionActions.ts b/src/DAL/repositories/transactionActions.ts index 39b6d9d..ed3d782 100644 --- a/src/DAL/repositories/transactionActions.ts +++ b/src/DAL/repositories/transactionActions.ts @@ -1,61 +1,61 @@ -import { singleton } from 'tsyringe'; -import { ObjectType, QueryRunner } from 'typeorm'; -import { BadRequestError } from '@map-colonies/error-types'; -import { OperationStatus } from '../../common/dataModels/enums'; -import { ConnectionManager } from '../connectionManager'; -import { JobRepository } from './jobRepository'; -import { TaskRepository } from './taskRepository'; +// import { singleton } from 'tsyringe'; +// import { ObjectType, QueryRunner } from 'typeorm'; +// import { BadRequestError } from '@map-colonies/error-types'; +// import { OperationStatus } from '../../common/dataModels/enums'; +// import { ConnectionManager } from '../connectionBuilder'; +// import { JobRepository } from './jobRepository'; +// import { TaskRepository } from './taskRepository'; -@singleton() -export class TransactionActions { - public constructor(private readonly connectionManager: ConnectionManager) {} +// @singleton() +// export class TransactionActions { +// public constructor(private readonly connectionManager: ConnectionManager) {} - public async resetJob(jobId: string, expirationDate?: Date): Promise { - return this.handleTransaction(async (runner: QueryRunner) => { - const jobRepo = this.getJobRepository(runner); - if (await jobRepo.isJobResettable(jobId)) { - await jobRepo.updateJob({ jobId, expirationDate, status: OperationStatus.IN_PROGRESS }); - const taskRepo = this.getTaskRepository(runner); - await taskRepo.resetJobTasks(jobId); - } else { - throw new BadRequestError(`job ${jobId} is not resettable.`); - } - }); - } +// public async resetJob(jobId: string, expirationDate?: Date): Promise { +// return this.handleTransaction(async (runner: QueryRunner) => { +// const jobRepo = this.getJobRepository(runner); +// if (await jobRepo.isJobResettable(jobId)) { +// await jobRepo.updateJob({ jobId, expirationDate, status: OperationStatus.IN_PROGRESS }); +// const taskRepo = this.getTaskRepository(runner); +// await taskRepo.resetJobTasks(jobId); +// } else { +// throw new BadRequestError(`job ${jobId} is not resettable.`); +// } +// }); +// } - private async handleTransaction(logic: (runner: QueryRunner) => Promise): Promise { - const runner = await this.getTransactionRunner(); - try { - const res = await logic(runner); - await runner.commitTransaction(); - return res; - } catch (err) { - await runner.rollbackTransaction(); - throw err; - } finally { - await runner.release(); - } - } +// private async handleTransaction(logic: (runner: QueryRunner) => Promise): Promise { +// const runner = await this.getTransactionRunner(); +// try { +// const res = await logic(runner); +// await runner.commitTransaction(); +// return res; +// } catch (err) { +// await runner.rollbackTransaction(); +// throw err; +// } finally { +// await runner.release(); +// } +// } - private async getTransactionRunner(): Promise { - if (!this.connectionManager.isConnected()) { - await this.connectionManager.init(); - } - const runner = await this.connectionManager.createQueryRunner(); - await runner.connect(); - await runner.startTransaction(); - return runner; - } +// private async getTransactionRunner(): Promise { +// if (!this.connectionManager.isConnected()) { +// await this.connectionManager.init(); +// } +// const runner = await this.connectionManager.createQueryRunner(); +// await runner.connect(); +// await runner.startTransaction(); +// return runner; +// } - private getJobRepository(queryRunner: QueryRunner): JobRepository { - return this.getRepository(JobRepository, queryRunner); - } +// private getJobRepository(queryRunner: QueryRunner): JobRepository { +// return this.getRepository(JobRepository, queryRunner); +// } - private getTaskRepository(queryRunner: QueryRunner): TaskRepository { - return this.getRepository(TaskRepository, queryRunner); - } +// private getTaskRepository(queryRunner: QueryRunner): TaskRepository { +// return this.getRepository(TaskRepository, queryRunner); +// } - private getRepository(repository: ObjectType, queryRunner: QueryRunner): T { - return queryRunner.manager.getCustomRepository(repository); - } -} +// private getRepository(repository: ObjectType, queryRunner: QueryRunner): T { +// return queryRunner.manager.withRepository(repository); +// } +// } diff --git a/src/jobs/models/jobManager.ts b/src/jobs/models/jobManager.ts index 0b68be1..362b7a5 100644 --- a/src/jobs/models/jobManager.ts +++ b/src/jobs/models/jobManager.ts @@ -3,7 +3,7 @@ import { NotFoundError } from '@map-colonies/error-types'; import { inject, injectable } from 'tsyringe'; import { SERVICES } from '../../common/constants'; -import { ConnectionManager } from '../../DAL/connectionManager'; +import { ConnectionManager } from '../../DAL/connectionBuilder'; import { FindJobsResponse, ICreateJobBody, diff --git a/src/jobs/models/taskManager.ts b/src/jobs/models/taskManager.ts index d117262..8e20a84 100644 --- a/src/jobs/models/taskManager.ts +++ b/src/jobs/models/taskManager.ts @@ -3,7 +3,7 @@ import { NotFoundError } from '@map-colonies/error-types'; import { inject, injectable } from 'tsyringe'; import { SERVICES } from '../../common/constants'; -import { ConnectionManager } from '../../DAL/connectionManager'; +import { ConnectionManager } from '../../DAL/connectionBuilder'; import { TaskRepository } from '../../DAL/repositories/taskRepository'; import { CreateTasksRequest, diff --git a/src/taskManagement/models/taskManagementManger.ts b/src/taskManagement/models/taskManagementManger.ts index df51f49..c62e6d9 100644 --- a/src/taskManagement/models/taskManagementManger.ts +++ b/src/taskManagement/models/taskManagementManger.ts @@ -2,7 +2,7 @@ import { Logger } from '@map-colonies/js-logger'; import { NotFoundError } from '@map-colonies/error-types'; import { inject, injectable } from 'tsyringe'; import { SERVICES } from '../../common/constants'; -import { ConnectionManager } from '../../DAL/connectionManager'; +import { ConnectionManager } from '../../DAL/connectionBuilder'; import { TaskRepository } from '../../DAL/repositories/taskRepository'; import { IFindInactiveTasksRequest, IGetTaskResponse, IRetrieveAndStartRequest } from '../../common/dataModels/tasks'; import { JobRepository } from '../../DAL/repositories/jobRepository'; From 9929d72393d768bb817d9a3db68d8e898ba211c7 Mon Sep 17 00:00:00 2001 From: Shahar s Date: Wed, 28 Sep 2022 12:13:53 +0300 Subject: [PATCH 2/4] feat: converted repositories to new typeorm api --- 1663841935533-initaialMigration.ts | 71 +-- dataSource.ts | 4 +- .../1663845942790-initaialMigration.ts | 74 +-- src/DAL/connectionBuilder.ts | 74 +-- src/DAL/entity/job.ts | 7 +- src/DAL/entity/task.ts | 8 +- src/DAL/repositories/generalRepository.ts | 11 +- src/DAL/repositories/jobRepository.ts | 310 +++++++------ src/DAL/repositories/taskRepository.ts | 428 +++++++++--------- src/DAL/repositories/transactionActions.ts | 81 +--- src/containerConfig.ts | 12 +- 11 files changed, 520 insertions(+), 560 deletions(-) diff --git a/1663841935533-initaialMigration.ts b/1663841935533-initaialMigration.ts index 017bbcf..8825422 100644 --- a/1663841935533-initaialMigration.ts +++ b/1663841935533-initaialMigration.ts @@ -1,36 +1,45 @@ -import { MigrationInterface, QueryRunner } from "typeorm"; +import { MigrationInterface, QueryRunner } from 'typeorm'; export class initaialMigration1663841935533 implements MigrationInterface { - name = 'initaialMigration1663841935533' + name = 'initaialMigration1663841935533'; - public async up(queryRunner: QueryRunner): Promise { - await queryRunner.query(`CREATE TYPE "JobManager"."Task_status_enum" AS ENUM('Pending', 'In-Progress', 'Completed', 'Failed', 'Expired', 'Aborted')`); - await queryRunner.query(`CREATE TABLE "JobManager"."Task" ("id" uuid NOT NULL DEFAULT uuid_generate_v4(), "jobId" uuid NOT NULL, "type" character varying(255) NOT NULL, "description" character varying(2000) NOT NULL DEFAULT '', "parameters" jsonb NOT NULL, "creationTime" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), "updateTime" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), "status" "JobManager"."Task_status_enum" NOT NULL DEFAULT 'Pending', "percentage" smallint, "reason" character varying NOT NULL DEFAULT '', "attempts" integer NOT NULL DEFAULT '0', "resettable" boolean NOT NULL DEFAULT true, "block_duplication" boolean NOT NULL DEFAULT false, CONSTRAINT "UQ_uniqueness_on_job_and_type" EXCLUDE EXCLUDE ("type" with =, "jobId" with =) WHERE ("block_duplication" = true, CONSTRAINT "PK_task_id" PRIMARY KEY ("id"))`); - await queryRunner.query(`CREATE INDEX "taskResettableIndex" ON "JobManager"."Task" ("resettable") WHERE "resettable" = FALSE`); - await queryRunner.query(`CREATE TYPE "JobManager"."Job_status_enum" AS ENUM('Pending', 'In-Progress', 'Completed', 'Failed', 'Expired', 'Aborted')`); - await queryRunner.query(`CREATE TABLE "JobManager"."Job" ("id" uuid NOT NULL DEFAULT uuid_generate_v4(), "resourceId" character varying(300) NOT NULL, "version" character varying(30) NOT NULL, "type" character varying(255) NOT NULL, "description" character varying(2000) NOT NULL DEFAULT '', "parameters" jsonb NOT NULL, "creationTime" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), "updateTime" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), "status" "JobManager"."Job_status_enum" NOT NULL DEFAULT 'Pending', "percentage" smallint, "reason" character varying NOT NULL DEFAULT '', "isCleaned" boolean NOT NULL DEFAULT false, "priority" integer NOT NULL DEFAULT '1000', "expirationDate" TIMESTAMP WITH TIME ZONE, "internalId" uuid, "producerName" text, "productName" text, "productType" text, "additionalIdentifiers" text, "taskCount" integer NOT NULL DEFAULT '0', "completedTasks" integer NOT NULL DEFAULT '0', "failedTasks" integer NOT NULL DEFAULT '0', "expiredTasks" integer NOT NULL DEFAULT '0', "pendingTasks" integer NOT NULL DEFAULT '0', "inProgressTasks" integer NOT NULL DEFAULT '0', "abortedTasks" integer NOT NULL DEFAULT '0', CONSTRAINT "UQ_uniqueness_on_active_tasks" EXCLUDE EXCLUDE ("resourceId" with =, "version" with =, "type" with =, "additionalIdentifiers" with =) WHERE (status = 'Pending' OR status = 'In-Progress'), CONSTRAINT "PK_job_id" PRIMARY KEY ("id"))`); - await queryRunner.query(`CREATE INDEX "jobTypeIndex" ON "JobManager"."Job" ("type") `); - await queryRunner.query(`CREATE INDEX "jobStatusIndex" ON "JobManager"."Job" ("status") `); - await queryRunner.query(`CREATE INDEX "jobCleanedIndex" ON "JobManager"."Job" ("isCleaned") `); - await queryRunner.query(`CREATE INDEX "jobPriorityIndex" ON "JobManager"."Job" ("priority") `); - await queryRunner.query(`CREATE INDEX "jobExpirationDateIndex" ON "JobManager"."Job" ("expirationDate") `); - await queryRunner.query(`CREATE INDEX "jobResourceIndex" ON "JobManager"."Job" ("resourceId", "version") `); - await queryRunner.query(`ALTER TABLE "JobManager"."Task" ADD CONSTRAINT "FK_task_job_id" FOREIGN KEY ("jobId") REFERENCES "JobManager"."Job"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`); - } - - public async down(queryRunner: QueryRunner): Promise { - await queryRunner.query(`ALTER TABLE "JobManager"."Task" DROP CONSTRAINT "FK_task_job_id"`); - await queryRunner.query(`DROP INDEX "JobManager"."jobResourceIndex"`); - await queryRunner.query(`DROP INDEX "JobManager"."jobExpirationDateIndex"`); - await queryRunner.query(`DROP INDEX "JobManager"."jobPriorityIndex"`); - await queryRunner.query(`DROP INDEX "JobManager"."jobCleanedIndex"`); - await queryRunner.query(`DROP INDEX "JobManager"."jobStatusIndex"`); - await queryRunner.query(`DROP INDEX "JobManager"."jobTypeIndex"`); - await queryRunner.query(`DROP TABLE "JobManager"."Job"`); - await queryRunner.query(`DROP TYPE "JobManager"."Job_status_enum"`); - await queryRunner.query(`DROP INDEX "JobManager"."taskResettableIndex"`); - await queryRunner.query(`DROP TABLE "JobManager"."Task"`); - await queryRunner.query(`DROP TYPE "JobManager"."Task_status_enum"`); - } + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `CREATE TYPE "JobManager"."Task_status_enum" AS ENUM('Pending', 'In-Progress', 'Completed', 'Failed', 'Expired', 'Aborted')` + ); + await queryRunner.query( + `CREATE TABLE "JobManager"."Task" ("id" uuid NOT NULL DEFAULT uuid_generate_v4(), "jobId" uuid NOT NULL, "type" character varying(255) NOT NULL, "description" character varying(2000) NOT NULL DEFAULT '', "parameters" jsonb NOT NULL, "creationTime" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), "updateTime" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), "status" "JobManager"."Task_status_enum" NOT NULL DEFAULT 'Pending', "percentage" smallint, "reason" character varying NOT NULL DEFAULT '', "attempts" integer NOT NULL DEFAULT '0', "resettable" boolean NOT NULL DEFAULT true, "block_duplication" boolean NOT NULL DEFAULT false, CONSTRAINT "UQ_uniqueness_on_job_and_type" EXCLUDE EXCLUDE ("type" with =, "jobId" with =) WHERE ("block_duplication" = true, CONSTRAINT "PK_task_id" PRIMARY KEY ("id"))` + ); + await queryRunner.query(`CREATE INDEX "taskResettableIndex" ON "JobManager"."Task" ("resettable") WHERE "resettable" = FALSE`); + await queryRunner.query( + `CREATE TYPE "JobManager"."Job_status_enum" AS ENUM('Pending', 'In-Progress', 'Completed', 'Failed', 'Expired', 'Aborted')` + ); + await queryRunner.query( + `CREATE TABLE "JobManager"."Job" ("id" uuid NOT NULL DEFAULT uuid_generate_v4(), "resourceId" character varying(300) NOT NULL, "version" character varying(30) NOT NULL, "type" character varying(255) NOT NULL, "description" character varying(2000) NOT NULL DEFAULT '', "parameters" jsonb NOT NULL, "creationTime" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), "updateTime" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), "status" "JobManager"."Job_status_enum" NOT NULL DEFAULT 'Pending', "percentage" smallint, "reason" character varying NOT NULL DEFAULT '', "isCleaned" boolean NOT NULL DEFAULT false, "priority" integer NOT NULL DEFAULT '1000', "expirationDate" TIMESTAMP WITH TIME ZONE, "internalId" uuid, "producerName" text, "productName" text, "productType" text, "additionalIdentifiers" text, "taskCount" integer NOT NULL DEFAULT '0', "completedTasks" integer NOT NULL DEFAULT '0', "failedTasks" integer NOT NULL DEFAULT '0', "expiredTasks" integer NOT NULL DEFAULT '0', "pendingTasks" integer NOT NULL DEFAULT '0', "inProgressTasks" integer NOT NULL DEFAULT '0', "abortedTasks" integer NOT NULL DEFAULT '0', CONSTRAINT "UQ_uniqueness_on_active_tasks" EXCLUDE EXCLUDE ("resourceId" with =, "version" with =, "type" with =, "additionalIdentifiers" with =) WHERE (status = 'Pending' OR status = 'In-Progress'), CONSTRAINT "PK_job_id" PRIMARY KEY ("id"))` + ); + await queryRunner.query(`CREATE INDEX "jobTypeIndex" ON "JobManager"."Job" ("type") `); + await queryRunner.query(`CREATE INDEX "jobStatusIndex" ON "JobManager"."Job" ("status") `); + await queryRunner.query(`CREATE INDEX "jobCleanedIndex" ON "JobManager"."Job" ("isCleaned") `); + await queryRunner.query(`CREATE INDEX "jobPriorityIndex" ON "JobManager"."Job" ("priority") `); + await queryRunner.query(`CREATE INDEX "jobExpirationDateIndex" ON "JobManager"."Job" ("expirationDate") `); + await queryRunner.query(`CREATE INDEX "jobResourceIndex" ON "JobManager"."Job" ("resourceId", "version") `); + await queryRunner.query( + `ALTER TABLE "JobManager"."Task" ADD CONSTRAINT "FK_task_job_id" FOREIGN KEY ("jobId") REFERENCES "JobManager"."Job"("id") ON DELETE NO ACTION ON UPDATE NO ACTION` + ); + } + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE "JobManager"."Task" DROP CONSTRAINT "FK_task_job_id"`); + await queryRunner.query(`DROP INDEX "JobManager"."jobResourceIndex"`); + await queryRunner.query(`DROP INDEX "JobManager"."jobExpirationDateIndex"`); + await queryRunner.query(`DROP INDEX "JobManager"."jobPriorityIndex"`); + await queryRunner.query(`DROP INDEX "JobManager"."jobCleanedIndex"`); + await queryRunner.query(`DROP INDEX "JobManager"."jobStatusIndex"`); + await queryRunner.query(`DROP INDEX "JobManager"."jobTypeIndex"`); + await queryRunner.query(`DROP TABLE "JobManager"."Job"`); + await queryRunner.query(`DROP TYPE "JobManager"."Job_status_enum"`); + await queryRunner.query(`DROP INDEX "JobManager"."taskResettableIndex"`); + await queryRunner.query(`DROP TABLE "JobManager"."Task"`); + await queryRunner.query(`DROP TYPE "JobManager"."Task_status_enum"`); + } } diff --git a/dataSource.ts b/dataSource.ts index 79491fb..c787470 100644 --- a/dataSource.ts +++ b/dataSource.ts @@ -3,11 +3,11 @@ import { DataSource } from 'typeorm'; import { createConnectionOptions } from './src/DAL/connectionBuilder'; import { IDbConfig } from './src/common/interfaces'; -const connectionOptions = config.get('typeOrm');; +const connectionOptions = config.get('typeOrm'); config.get('typeOrm'); export const appDataSource = new DataSource({ ...createConnectionOptions(connectionOptions), entities: ['src/DAL/**/*.ts'], migrationsTableName: 'migrations_table', migrations: ['db/migrations/*.ts'], -}); \ No newline at end of file +}); diff --git a/db/migrations/1663845942790-initaialMigration.ts b/db/migrations/1663845942790-initaialMigration.ts index 0966942..2ce8c5d 100644 --- a/db/migrations/1663845942790-initaialMigration.ts +++ b/db/migrations/1663845942790-initaialMigration.ts @@ -1,37 +1,47 @@ -import { MigrationInterface, QueryRunner } from "typeorm"; +import { MigrationInterface, QueryRunner } from 'typeorm'; export class initaialMigration1663845942790 implements MigrationInterface { - name = 'initaialMigration1663845942790' + name = 'initaialMigration1663845942790'; - public async up(queryRunner: QueryRunner): Promise { - //TODO: fix task type name and remove its duplication - await queryRunner.query(`CREATE TYPE "JobManager"."Task_status_enum" AS ENUM('Pending', 'In-Progress', 'Completed', 'Failed', 'Expired', 'Aborted')`); - await queryRunner.query(`CREATE TABLE "JobManager"."Task" ("id" uuid NOT NULL DEFAULT uuid_generate_v4(), "jobId" uuid NOT NULL, "type" character varying(255) NOT NULL, "description" character varying(2000) NOT NULL DEFAULT '', "parameters" jsonb NOT NULL, "creationTime" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), "updateTime" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), "status" "JobManager"."Task_status_enum" NOT NULL DEFAULT 'Pending', "percentage" smallint, "reason" character varying NOT NULL DEFAULT '', "attempts" integer NOT NULL DEFAULT '0', "resettable" boolean NOT NULL DEFAULT true, "block_duplication" boolean NOT NULL DEFAULT false, CONSTRAINT "UQ_uniqueness_on_job_and_type" EXCLUDE EXCLUDE ("type" with =, "jobId" with =) WHERE ("block_duplication" = true, CONSTRAINT "PK_task_id" PRIMARY KEY ("id"))`); - await queryRunner.query(`CREATE INDEX "taskResettableIndex" ON "JobManager"."Task" ("resettable") WHERE "resettable" = FALSE`); - await queryRunner.query(`CREATE TYPE "JobManager"."Job_status_enum" AS ENUM('Pending', 'In-Progress', 'Completed', 'Failed', 'Expired', 'Aborted')`); - await queryRunner.query(`CREATE TABLE "JobManager"."Job" ("id" uuid NOT NULL DEFAULT uuid_generate_v4(), "resourceId" character varying(300) NOT NULL, "version" character varying(30) NOT NULL, "type" character varying(255) NOT NULL, "description" character varying(2000) NOT NULL DEFAULT '', "parameters" jsonb NOT NULL, "creationTime" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), "updateTime" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), "status" "JobManager"."Job_status_enum" NOT NULL DEFAULT 'Pending', "percentage" smallint, "reason" character varying NOT NULL DEFAULT '', "isCleaned" boolean NOT NULL DEFAULT false, "priority" integer NOT NULL DEFAULT '1000', "expirationDate" TIMESTAMP WITH TIME ZONE, "internalId" uuid, "producerName" text, "productName" text, "productType" text, "additionalIdentifiers" text, "taskCount" integer NOT NULL DEFAULT '0', "completedTasks" integer NOT NULL DEFAULT '0', "failedTasks" integer NOT NULL DEFAULT '0', "expiredTasks" integer NOT NULL DEFAULT '0', "pendingTasks" integer NOT NULL DEFAULT '0', "inProgressTasks" integer NOT NULL DEFAULT '0', "abortedTasks" integer NOT NULL DEFAULT '0', CONSTRAINT "UQ_uniqueness_on_active_tasks" EXCLUDE EXCLUDE ("resourceId" with =, "version" with =, "type" with =, "additionalIdentifiers" with =) WHERE (status = 'Pending' OR status = 'In-Progress'), CONSTRAINT "PK_job_id" PRIMARY KEY ("id"))`); - await queryRunner.query(`CREATE INDEX "jobTypeIndex" ON "JobManager"."Job" ("type") `); - await queryRunner.query(`CREATE INDEX "jobStatusIndex" ON "JobManager"."Job" ("status") `); - await queryRunner.query(`CREATE INDEX "jobCleanedIndex" ON "JobManager"."Job" ("isCleaned") `); - await queryRunner.query(`CREATE INDEX "jobPriorityIndex" ON "JobManager"."Job" ("priority") `); - await queryRunner.query(`CREATE INDEX "jobExpirationDateIndex" ON "JobManager"."Job" ("expirationDate") `); - await queryRunner.query(`CREATE INDEX "jobResourceIndex" ON "JobManager"."Job" ("resourceId", "version") `); - await queryRunner.query(`ALTER TABLE "JobManager"."Task" ADD CONSTRAINT "FK_task_job_id" FOREIGN KEY ("jobId") REFERENCES "JobManager"."Job"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`); - } - - public async down(queryRunner: QueryRunner): Promise { - await queryRunner.query(`ALTER TABLE "JobManager"."Task" DROP CONSTRAINT "FK_task_job_id"`); - await queryRunner.query(`DROP INDEX "JobManager"."jobResourceIndex"`); - await queryRunner.query(`DROP INDEX "JobManager"."jobExpirationDateIndex"`); - await queryRunner.query(`DROP INDEX "JobManager"."jobPriorityIndex"`); - await queryRunner.query(`DROP INDEX "JobManager"."jobCleanedIndex"`); - await queryRunner.query(`DROP INDEX "JobManager"."jobStatusIndex"`); - await queryRunner.query(`DROP INDEX "JobManager"."jobTypeIndex"`); - await queryRunner.query(`DROP TABLE "JobManager"."Job"`); - await queryRunner.query(`DROP TYPE "JobManager"."Job_status_enum"`); - await queryRunner.query(`DROP INDEX "JobManager"."taskResettableIndex"`); - await queryRunner.query(`DROP TABLE "JobManager"."Task"`); - await queryRunner.query(`DROP TYPE "JobManager"."Task_status_enum"`); - } + public async up(queryRunner: QueryRunner): Promise { + //status type duplication is typeorm bug https://github.com/typeorm/typeorm/issues/8136 + await queryRunner.query( + `CREATE TYPE "JobManager"."Task_status_enum" AS ENUM('Pending', 'In-Progress', 'Completed', 'Failed', 'Expired', 'Aborted')` + ); + await queryRunner.query( + `CREATE TABLE "JobManager"."Task" ("id" uuid NOT NULL DEFAULT uuid_generate_v4(), "jobId" uuid NOT NULL, "type" character varying(255) NOT NULL, "description" character varying(2000) NOT NULL DEFAULT '', "parameters" jsonb NOT NULL, "creationTime" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), "updateTime" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), "status" "JobManager"."Task_status_enum" NOT NULL DEFAULT 'Pending', "percentage" smallint, "reason" character varying NOT NULL DEFAULT '', "attempts" integer NOT NULL DEFAULT '0', "resettable" boolean NOT NULL DEFAULT true, "block_duplication" boolean NOT NULL DEFAULT false, CONSTRAINT "UQ_uniqueness_on_job_and_type" EXCLUDE EXCLUDE ("type" with =, "jobId" with =) WHERE ("block_duplication" = true, CONSTRAINT "PK_task_id" PRIMARY KEY ("id"))` + ); + await queryRunner.query(`CREATE INDEX "taskResettableIndex" ON "JobManager"."Task" ("resettable") WHERE "resettable" = FALSE`); + await queryRunner.query( + `CREATE TYPE "JobManager"."Job_status_enum" AS ENUM('Pending', 'In-Progress', 'Completed', 'Failed', 'Expired', 'Aborted')` + ); + await queryRunner.query( + `CREATE TABLE "JobManager"."Job" ("id" uuid NOT NULL DEFAULT uuid_generate_v4(), "resourceId" character varying(300) NOT NULL, "version" character varying(30) NOT NULL, "type" character varying(255) NOT NULL, "description" character varying(2000) NOT NULL DEFAULT '', "parameters" jsonb NOT NULL, "creationTime" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), "updateTime" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), "status" "JobManager"."Job_status_enum" NOT NULL DEFAULT 'Pending', "percentage" smallint, "reason" character varying NOT NULL DEFAULT '', "isCleaned" boolean NOT NULL DEFAULT false, "priority" integer NOT NULL DEFAULT '1000', "expirationDate" TIMESTAMP WITH TIME ZONE, "internalId" uuid, "producerName" text, "productName" text, "productType" text, "additionalIdentifiers" text, "taskCount" integer NOT NULL DEFAULT '0', "completedTasks" integer NOT NULL DEFAULT '0', "failedTasks" integer NOT NULL DEFAULT '0', "expiredTasks" integer NOT NULL DEFAULT '0', "pendingTasks" integer NOT NULL DEFAULT '0', "inProgressTasks" integer NOT NULL DEFAULT '0', "abortedTasks" integer NOT NULL DEFAULT '0', CONSTRAINT "UQ_uniqueness_on_active_tasks" EXCLUDE EXCLUDE ("resourceId" with =, "version" with =, "type" with =, "additionalIdentifiers" with =) WHERE (status = 'Pending' OR status = 'In-Progress'), CONSTRAINT "PK_job_id" PRIMARY KEY ("id"))` + ); + await queryRunner.query(`CREATE INDEX "jobTypeIndex" ON "JobManager"."Job" ("type") `); + await queryRunner.query(`CREATE INDEX "jobStatusIndex" ON "JobManager"."Job" ("status") `); + await queryRunner.query(`CREATE INDEX "jobCleanedIndex" ON "JobManager"."Job" ("isCleaned") `); + await queryRunner.query(`CREATE INDEX "jobPriorityIndex" ON "JobManager"."Job" ("priority") `); + await queryRunner.query(`CREATE INDEX "jobExpirationDateIndex" ON "JobManager"."Job" ("expirationDate") `); + await queryRunner.query(`CREATE INDEX "jobResourceIndex" ON "JobManager"."Job" ("resourceId", "version") `); + await queryRunner.query( + `ALTER TABLE "JobManager"."Task" ADD CONSTRAINT "FK_task_job_id" FOREIGN KEY ("jobId") REFERENCES "JobManager"."Job"("id") ON DELETE NO ACTION ON UPDATE NO ACTION` + ); + //TODO: add functions and triggers + } + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE "JobManager"."Task" DROP CONSTRAINT "FK_task_job_id"`); + await queryRunner.query(`DROP INDEX "JobManager"."jobResourceIndex"`); + await queryRunner.query(`DROP INDEX "JobManager"."jobExpirationDateIndex"`); + await queryRunner.query(`DROP INDEX "JobManager"."jobPriorityIndex"`); + await queryRunner.query(`DROP INDEX "JobManager"."jobCleanedIndex"`); + await queryRunner.query(`DROP INDEX "JobManager"."jobStatusIndex"`); + await queryRunner.query(`DROP INDEX "JobManager"."jobTypeIndex"`); + await queryRunner.query(`DROP TABLE "JobManager"."Job"`); + await queryRunner.query(`DROP TYPE "JobManager"."Job_status_enum"`); + await queryRunner.query(`DROP INDEX "JobManager"."taskResettableIndex"`); + await queryRunner.query(`DROP TABLE "JobManager"."Task"`); + await queryRunner.query(`DROP TYPE "JobManager"."Task_status_enum"`); + } } diff --git a/src/DAL/connectionBuilder.ts b/src/DAL/connectionBuilder.ts index 2e6262b..59fa504 100644 --- a/src/DAL/connectionBuilder.ts +++ b/src/DAL/connectionBuilder.ts @@ -1,72 +1,18 @@ import { readFileSync } from 'fs'; -import { createConnection, Connection, ObjectType, QueryRunner, DataSourceOptions } from 'typeorm'; -import { inject, singleton } from 'tsyringe'; -import { Logger } from '@map-colonies/js-logger'; -import { DBConnectionError } from '../common/errors'; -import { SERVICES } from '../common/constants'; -import { IConfig, IDbConfig } from '../common/interfaces'; -import { JobRepository } from './repositories/jobRepository'; -import { TaskRepository } from './repositories/taskRepository'; +import { DataSourceOptions, DataSource } from 'typeorm'; +import { IDbConfig } from '../common/interfaces'; -@singleton() -export class ConnectionManager { - private connection?: Connection; - private connectionStatusPromise?: Promise; - - public constructor(@inject(SERVICES.LOGGER) private readonly logger: Logger, @inject(SERVICES.CONFIG) private readonly config: IConfig) {} - - public async init(): Promise { - const connectionConfig = this.config.get('typeOrm'); - this.logger.info(`connection to database ${connectionConfig.database as string} on ${connectionConfig.host as string}`); - try { - if (this.connectionStatusPromise === undefined) { - this.connectionStatusPromise = createConnection(createConnectionOptions(connectionConfig)); - } - this.connection = await this.connectionStatusPromise; - } catch (err) { - const errString = JSON.stringify(err, Object.getOwnPropertyNames(err)); - this.logger.error(`failed to connect to database: ${errString}`); - throw new DBConnectionError(); - } - } - - public isConnected(): boolean { - return this.connection !== undefined; - } - - public getJobRepository(): JobRepository { - return this.getRepository(JobRepository); - } - - public getTaskRepository(): TaskRepository { - return this.getRepository(TaskRepository); - } - - public async createQueryRunner(): Promise { - if (!this.isConnected()) { - await this.init(); - } - const connection = this.connection as Connection; - return connection.createQueryRunner(); - } - - private getRepository(repository: ObjectType): T { - if (!this.isConnected()) { - const msg = 'failed to send request to database: no open connection'; - this.logger.error(msg); - throw new DBConnectionError(); - } else { - const connection = this.connection as Connection; - return connection.getCustomRepository(repository); - } - } -} - -export const createConnectionOptions = (dbConfig: IDbConfig):DataSourceOptions => { +export const createConnectionOptions = (dbConfig: IDbConfig): DataSourceOptions => { const { enableSslAuth, sslPaths, ...connectionOptions } = dbConfig; if (enableSslAuth) { connectionOptions.password = undefined; connectionOptions.ssl = { key: readFileSync(sslPaths.key), cert: readFileSync(sslPaths.cert), ca: readFileSync(sslPaths.ca) }; } return connectionOptions; -} +}; + +export const initDataSource = async (dbConfig: IDbConfig): Promise => { + const connection = new DataSource(createConnectionOptions(dbConfig)); + await connection.initialize(); + return connection; +}; diff --git a/src/DAL/entity/job.ts b/src/DAL/entity/job.ts index b805b72..057a2c5 100644 --- a/src/DAL/entity/job.ts +++ b/src/DAL/entity/job.ts @@ -4,7 +4,10 @@ import { TaskEntity } from './task'; @Entity('Job') @Index('jobResourceIndex', ['resourceId', 'version'], { unique: false }) -@Exclusion('UQ_uniqueness_on_active_tasks','EXCLUDE ("resourceId" with =, "version" with =, "type" with =, "additionalIdentifiers" with =) WHERE (status = \'Pending\' OR status = \'In-Progress\')') +@Exclusion( + 'UQ_uniqueness_on_active_tasks', + 'EXCLUDE ("resourceId" with =, "version" with =, "type" with =, "additionalIdentifiers" with =) WHERE (status = \'Pending\' OR status = \'In-Progress\')' +) export class JobEntity { @PrimaryColumn({ type: 'uuid', primaryKeyConstraintName: 'PK_job_id' }) @Generated('uuid') @@ -50,7 +53,7 @@ export class JobEntity { @Column('boolean', { default: false, nullable: false }) public isCleaned: boolean; - @Index('jobPriorityIndex') + @Index('jobPriorityIndex', {}) @Column('int', { default: 1000, nullable: false }) public priority: number; diff --git a/src/DAL/entity/task.ts b/src/DAL/entity/task.ts index e3e683d..69de221 100644 --- a/src/DAL/entity/task.ts +++ b/src/DAL/entity/task.ts @@ -3,7 +3,7 @@ import { OperationStatus } from '../../common/dataModels/enums'; import { JobEntity } from './job'; @Entity('Task') -@Exclusion('UQ_uniqueness_on_job_and_type','EXCLUDE ("type" with =, "jobId" with =) WHERE ("block_duplication" = true)') +@Exclusion('UQ_uniqueness_on_job_and_type', 'EXCLUDE ("type" with =, "jobId" with =) WHERE ("block_duplication" = true)') export class TaskEntity { @PrimaryColumn({ type: 'uuid', primaryKeyConstraintName: 'PK_task_id' }) @Generated('uuid') @@ -13,8 +13,8 @@ export class TaskEntity { @Column({ name: 'jobId' }) public jobId: string; - @ManyToOne(() => JobEntity, (job) => job.tasks, { nullable: false,cascade: false }) - @JoinColumn({ name: 'jobId', foreignKeyConstraintName:'FK_task_job_id', }) + @ManyToOne(() => JobEntity, (job) => job.tasks, { nullable: false, cascade: false }) + @JoinColumn({ name: 'jobId', foreignKeyConstraintName: 'FK_task_job_id' }) public job: JobEntity; @Column('varchar', { length: 255 }) @@ -48,7 +48,7 @@ export class TaskEntity { @Column('integer', { nullable: false, default: 0 }) public attempts: number; - @Index('taskResettableIndex', {where:'"resettable" = FALSE'}) + @Index('taskResettableIndex', { where: '"resettable" = FALSE' }) @Column('boolean', { nullable: false, default: true }) public resettable: boolean; diff --git a/src/DAL/repositories/generalRepository.ts b/src/DAL/repositories/generalRepository.ts index 4fba74e..7ab8371 100644 --- a/src/DAL/repositories/generalRepository.ts +++ b/src/DAL/repositories/generalRepository.ts @@ -1,23 +1,23 @@ import { ObjectLiteral, Repository } from 'typeorm'; import { IConfig, IDbConfig } from '../../common/interfaces'; -export abstract class GeneralRepository { +export abstract class GeneralRepository { protected readonly dbConfig: IDbConfig; protected metadata = this.repository.metadata; protected manager = this.repository.manager; protected queryRunner = this.repository.queryRunner; - - protected constructor(private readonly repository : Repository, protected readonly config: IConfig) { + + protected constructor(private readonly repository: Repository, protected readonly config: IConfig) { this.dbConfig = this.config.get('typeOrm'); } - + /* eslint-disable-next-line @typescript-eslint/no-explicit-any */ protected async query(query: string, parameters?: any[] | undefined): Promise { await this.repository.query(`SET search_path TO "${this.dbConfig.schema as string}", public`); return this.repository.query(query, parameters); } - + /* eslint-disable @typescript-eslint/member-ordering */ protected createQueryBuilder = this.repository.createQueryBuilder.bind(this.repository); protected count = this.repository.count.bind(this.repository); @@ -47,5 +47,4 @@ export abstract class GeneralRepository { protected softRemove = this.repository.softRemove.bind(this.repository); protected update = this.repository.update.bind(this.repository); protected upsert = this.repository.upsert.bind(this.repository); - } diff --git a/src/DAL/repositories/jobRepository.ts b/src/DAL/repositories/jobRepository.ts index e4c7864..92b1351 100644 --- a/src/DAL/repositories/jobRepository.ts +++ b/src/DAL/repositories/jobRepository.ts @@ -1,9 +1,8 @@ -import { FindManyOptions, LessThan, Brackets, Between, LessThanOrEqual, MoreThanOrEqual, DataSource } from 'typeorm'; -import { inject, singleton } from 'tsyringe'; +import { FindManyOptions, LessThan, Brackets, Between, LessThanOrEqual, MoreThanOrEqual, DataSource, Repository } from 'typeorm'; +import { FactoryFunction } from 'tsyringe'; import { Logger } from '@map-colonies/js-logger'; import { ConflictError, NotFoundError } from '@map-colonies/error-types'; import { DBConstraintError } from '../../common/errors'; -import { SERVICES } from '../../common/constants'; import { JobEntity } from '../entity/job'; import { FindJobsResponse, @@ -15,171 +14,182 @@ import { } from '../../common/dataModels/jobs'; import { JobModelConvertor } from '../convertors/jobModelConverter'; import { OperationStatus } from '../../common/dataModels/enums'; -import { IConfig } from '../../common/interfaces'; -import { GeneralRepository } from './generalRepository'; - -@singleton() -export class JobRepository extends GeneralRepository { - - public constructor(db: DataSource, private readonly jobConvertor: JobModelConvertor, - @inject(SERVICES.CONFIG) config: IConfig, @inject(SERVICES.LOGGER)private readonly logger: Logger) { - super(db.getRepository(JobEntity), config); - } +import { IConfig, IDbConfig } from '../../common/interfaces'; +import { SERVICES } from '../../common/constants'; - public async findJobs(req: IFindJobsRequest): Promise { - const filter: Record = { - resourceId: req.resourceId, - version: req.version, - isCleaned: req.isCleaned, - status: req.status, - type: req.type, - productType: req.productType, - internalId: req.internalId, - }; +// eslint-disable-next-line @typescript-eslint/explicit-function-return-type +const getJobRepository = (db: DataSource, config: IConfig, logger: Logger, jobConvertor: JobModelConvertor) => { + const schemaConf = config.get('typeorm').schema; + const schema = schemaConf == undefined || schemaConf == '' ? '' : `"${schemaConf}".`; + return db.getRepository(JobEntity).extend({ + async findJobs(req: IFindJobsRequest): Promise { + const filter: Record = { + resourceId: req.resourceId, + version: req.version, + isCleaned: req.isCleaned, + status: req.status, + type: req.type, + productType: req.productType, + internalId: req.internalId, + }; - if (req.fromDate != undefined && req.tillDate != undefined) { - filter.updateTime = Between(req.fromDate, req.tillDate); - } else if (req.tillDate != undefined) { - filter.updateTime = LessThanOrEqual(req.tillDate); - } else if (req.fromDate != undefined) { - filter.updateTime = MoreThanOrEqual(req.fromDate); - } + if (req.fromDate != undefined && req.tillDate != undefined) { + filter.updateTime = Between(req.fromDate, req.tillDate); + } else if (req.tillDate != undefined) { + filter.updateTime = LessThanOrEqual(req.tillDate); + } else if (req.fromDate != undefined) { + filter.updateTime = MoreThanOrEqual(req.fromDate); + } - for (const key of Object.keys(filter)) { - if (filter[key] == undefined) { - delete filter[key]; + for (const key of Object.keys(filter)) { + if (filter[key] == undefined) { + delete filter[key]; + } } - } - const options: FindManyOptions = { where: filter }; - if (req.shouldReturnTasks !== false) { - options.relations = ['tasks']; - } + const options: FindManyOptions = { where: filter }; + if (req.shouldReturnTasks !== false) { + options.relations = ['tasks']; + } - const entities = await this.find(options); - const models = entities.map((entity) => this.jobConvertor.entityToModel(entity)); - return models; - } + const entities = await this.find(options); + const models = entities.map((entity) => jobConvertor.entityToModel(entity)); + return models; + }, - public async createJob(req: ICreateJobBody): Promise { - this.logger.info({ resourceId: req.resourceId, version: req.version, type: req.type, msg: 'Start job creation ' }); - try { - let entity = this.jobConvertor.createModelToEntity(req); - entity = await this.save(entity); - this.logger.info({ resourceId: entity.resourceId, version: entity.version, type: entity.type, msg: 'Job was created successfully' }); - return { - id: entity.id, - taskIds: entity.tasks ? entity.tasks.map((task) => task.id) : [], - }; - } catch (err) { - const pgExclusionViolationErrorCode = '23P01'; - const error = err as Error & { code: string }; - if (error.code === pgExclusionViolationErrorCode) { - if (error.message.includes('UQ_uniqueness_on_active_tasks')) { - const message = 'failed to create job because another active job exists for provided resource, version and identifiers.'; - this.logger.error({ - resourceId: req.resourceId, - version: req.version, - type: req.type, - identifiers: req.additionalIdentifiers as string, - msg: message, - }); - throw new ConflictError(message); - } - if (error.message.includes('UQ_uniqness_on_job_and_type')) { - const message = 'failed to create job, for provided resource:, version and identifiers, because it contains duplicate tasks.'; - this.logger.error({ - resourceId: req.resourceId, - version: req.version, - type: req.type, - identifiers: req.additionalIdentifiers as string, - msg: message, - }); - throw new DBConstraintError('request contains duplicate tasks.'); + async createJob(req: ICreateJobBody): Promise { + logger.info({ resourceId: req.resourceId, version: req.version, type: req.type, msg: 'Start job creation ' }); + try { + let entity = jobConvertor.createModelToEntity(req); + entity = await this.save(entity); + logger.info({ resourceId: entity.resourceId, version: entity.version, type: entity.type, msg: 'Job was created successfully' }); + return { + id: entity.id, + taskIds: entity.tasks ? entity.tasks.map((task) => task.id) : [], + }; + } catch (err) { + const pgExclusionViolationErrorCode = '23P01'; + const error = err as Error & { code: string }; + if (error.code === pgExclusionViolationErrorCode) { + if (error.message.includes('UQ_uniqueness_on_active_tasks')) { + const message = 'failed to create job because another active job exists for provided resource, version and identifiers.'; + logger.error({ + resourceId: req.resourceId, + version: req.version, + type: req.type, + identifiers: req.additionalIdentifiers as string, + msg: message, + }); + throw new ConflictError(message); + } + if (error.message.includes('UQ_uniqness_on_job_and_type')) { + const message = 'failed to create job, for provided resource:, version and identifiers, because it contains duplicate tasks.'; + logger.error({ + resourceId: req.resourceId, + version: req.version, + type: req.type, + identifiers: req.additionalIdentifiers as string, + msg: message, + }); + throw new DBConstraintError('request contains duplicate tasks.'); + } } + throw err; } - throw err; - } - } + }, - public async getJob(id: string, shouldReturnTasks = true): Promise { - let entity; - if (!shouldReturnTasks) { - entity = await this.findOneBy({id}); - } else { - entity = await this.findOne({where: {id}, relations: ['tasks'] }); - } - const model = entity ? this.jobConvertor.entityToModel(entity) : undefined; - return model; - } + async getJob(id: string, shouldReturnTasks = true): Promise { + let entity; + if (!shouldReturnTasks) { + entity = await this.findOneBy({ id }); + } else { + entity = await this.findOne({ where: { id }, relations: ['tasks'] }); + } + const model = entity ? jobConvertor.entityToModel(entity) : undefined; + return model; + }, - public async updateJob(req: IUpdateJobRequest): Promise { - this.logger.info({ jobId: req.jobId, msg: 'start job update' }); - if (!(await this.exists(req.jobId))) { - const message = 'job was not found for provided update request'; - this.logger.error({ jobId: req.jobId, msg: message }); - throw new NotFoundError(message); - } - const entity = this.jobConvertor.updateModelToEntity(req); - await this.save(entity); - this.logger.info({ jobId: req.jobId, msg: 'Job was updated successfully' }); - } + async updateJob(req: IUpdateJobRequest): Promise { + logger.info({ jobId: req.jobId, msg: 'start job update' }); + if (!(await this.exists(req.jobId))) { + const message = 'job was not found for provided update request'; + logger.error({ jobId: req.jobId, msg: message }); + throw new NotFoundError(message); + } + const entity = jobConvertor.updateModelToEntity(req); + await this.save(entity); + logger.info({ jobId: req.jobId, msg: 'Job was updated successfully' }); + }, - public async exists(id: string): Promise { - const jobCount = await this.countBy({ id: id }); - return jobCount === 1; - } + async exists(id: string): Promise { + const jobCount = await this.countBy({ id: id }); + return jobCount === 1; + }, - public async deleteJob(id: string): Promise { - if (!(await this.exists(id))) { - const message = 'job id was not found for delete request'; - this.logger.error({ id: id, msg: message }); - throw new NotFoundError(message); - } - try { - await this.delete(id); - this.logger.info({ id: id, msg: 'Finish job deletion successfully' }); - } catch (err) { - const pgForeignKeyConstraintViolationErrorCode = '23503'; - const error = err as Error & { code: string }; - if (error.code === pgForeignKeyConstraintViolationErrorCode) { - this.logger.error({ jobId: id, errorMessage: error.message, errorCode: error.code, msg: 'failed job deletion because it have tasks' }); - throw new DBConstraintError(`job ${id} have tasks`); - } else { - this.logger.error({ jobId: id, errorMessage: error.message, errorCode: error.code, msg: 'failed job deletion' }); - throw err; + async deleteJob(id: string): Promise { + if (!(await this.exists(id))) { + const message = 'job id was not found for delete request'; + logger.error({ id: id, msg: message }); + throw new NotFoundError(message); } - } - } + try { + await this.delete(id); + logger.info({ id: id, msg: 'Finish job deletion successfully' }); + } catch (err) { + const pgForeignKeyConstraintViolationErrorCode = '23503'; + const error = err as Error & { code: string }; + if (error.code === pgForeignKeyConstraintViolationErrorCode) { + logger.error({ jobId: id, errorMessage: error.message, errorCode: error.code, msg: 'failed job deletion because it have tasks' }); + throw new DBConstraintError(`job ${id} have tasks`); + } else { + logger.error({ jobId: id, errorMessage: error.message, errorCode: error.code, msg: 'failed job deletion' }); + throw err; + } + } + }, - public async updateExpiredJobs(): Promise { - const now = new Date(); - const query = this.createQueryBuilder('jb') - .update() - .set({ status: OperationStatus.EXPIRED }) - .where({ expirationDate: LessThan(now) }) - .andWhere( - new Brackets((qb) => { - qb.where([{ status: OperationStatus.IN_PROGRESS }, { status: OperationStatus.PENDING }]); - }) - ); - await query.execute(); - } + async updateExpiredJobs(): Promise { + const now = new Date(); + const query = this.createQueryBuilder('jb') + .update() + .set({ status: OperationStatus.EXPIRED }) + .where({ expirationDate: LessThan(now) }) + .andWhere( + new Brackets((qb) => { + qb.where([{ status: OperationStatus.IN_PROGRESS }, { status: OperationStatus.PENDING }]); + }) + ); + await query.execute(); + }, - public async isJobResettable(jobId: string): Promise { - const query = `SELECT count(*) FILTER (WHERE tk."resettable" = FALSE) as "unResettableTasks", count(*) AS "failedTasks" - FROM "Job" AS jb + async isJobResettable(jobId: string): Promise { + const query = `SELECT count(*) FILTER (WHERE tk."resettable" = FALSE) as "unResettableTasks", count(*) AS "failedTasks" + FROM ${schema}"Job" AS jb INNER JOIN "Task" as tk ON tk."jobId" = jb.id WHERE jb.id = $1 AND (jb.status = '${OperationStatus.EXPIRED}' OR jb.status = '${OperationStatus.FAILED}' OR jb.status = '${OperationStatus.ABORTED}') AND (tk.status = '${OperationStatus.EXPIRED}' OR tk.status = '${OperationStatus.FAILED}' OR tk.status = '${OperationStatus.ABORTED}') AND jb."isCleaned" = FALSE`; - const sqlRes = (await this.query(query, [jobId])) as { unResettableTasks: string; failedTasks: string }[]; - if (sqlRes.length === 0) { - //no matching job found. it might not exist, not have task, be cleaned or not be in failed status - return false; - } - const res = sqlRes[0]; - return parseInt(res.unResettableTasks) === 0 && parseInt(res.failedTasks) > 0; - } -} + const sqlRes = (await this.query(query, [jobId])) as { unResettableTasks: string; failedTasks: string }[]; + if (sqlRes.length === 0) { + //no matching job found. it might not exist, not have task, be cleaned or not be in failed status + return false; + } + const res = sqlRes[0]; + return parseInt(res.unResettableTasks) === 0 && parseInt(res.failedTasks) > 0; + }, + }); +}; + +export type JobRepository = ReturnType; + +export const JOB_CUSTOM_REPOSITORY_SYMBOL = Symbol('JOB_CUSTOM_REPOSITORY_SYMBOL'); + +export const jobRepositoryFactory: FactoryFunction = (depContainer) => { + return getJobRepository( + depContainer.resolve(DataSource), + depContainer.resolve(SERVICES.CONFIG), + depContainer.resolve(SERVICES.LOGGER), + depContainer.resolve(JobModelConvertor) + ); +}; diff --git a/src/DAL/repositories/taskRepository.ts b/src/DAL/repositories/taskRepository.ts index 822c3c7..12e6b67 100644 --- a/src/DAL/repositories/taskRepository.ts +++ b/src/DAL/repositories/taskRepository.ts @@ -1,5 +1,5 @@ -import { DataSource, In, LessThan, Brackets, UpdateResult, FindOptionsWhere } from 'typeorm'; -import { inject, singleton } from 'tsyringe'; +import { DataSource, In, LessThan, Brackets, UpdateResult, FindOptionsWhere, Repository } from 'typeorm'; +import { FactoryFunction } from 'tsyringe'; import { Logger } from '@map-colonies/js-logger'; import { ConflictError, NotFoundError } from '@map-colonies/error-types'; import { SERVICES } from '../../common/constants'; @@ -20,98 +20,94 @@ import { IUpdateTaskRequest, } from '../../common/dataModels/tasks'; import { OperationStatus } from '../../common/dataModels/enums'; -import { IConfig } from '../../common/interfaces'; -import { GeneralRepository } from './generalRepository'; +import { IConfig, IDbConfig } from '../../common/interfaces'; declare type SqlRawResponse = [unknown[], number]; -@singleton() -export class TaskRepository extends GeneralRepository { +// eslint-disable-next-line @typescript-eslint/explicit-function-return-type +const getJobRepository = (db: DataSource, config: IConfig, logger: Logger, taskConvertor: TaskModelConvertor) => { + const schemaConf = config.get('typeorm').schema; + const schema = schemaConf == undefined || schemaConf == '' ? '' : `"${schemaConf}".`; + return db.getRepository(TaskEntity).extend({ + async getTasks(req: IAllTasksParams): Promise { + const entities = await this.findBy(req); + const models = entities.map((entity) => taskConvertor.entityToModel(entity)); + return models; + }, - public constructor(db: DataSource, private readonly taskConvertor: TaskModelConvertor, - @inject(SERVICES.CONFIG) config: IConfig, @inject(SERVICES.LOGGER)private readonly logger: Logger) { - super(db.getRepository(TaskEntity),config); - } + async findTasks(req: IFindTasksRequest): Promise { + const entities = await this.findBy(req as FindOptionsWhere); + const models = entities.map((entity) => taskConvertor.entityToModel(entity)); + return models; + }, - public async getTasks(req: IAllTasksParams): Promise { - const entities = await this.findBy(req); - const models = entities.map((entity) => this.taskConvertor.entityToModel(entity)); - return models; - } - - public async findTasks(req: IFindTasksRequest): Promise { - const entities = await this.findBy(req as FindOptionsWhere); - const models = entities.map((entity) => this.taskConvertor.entityToModel(entity)); - return models; - } - - public async createTask(req: CreateTasksRequest): Promise { - const jobId = Array.isArray(req) ? req[0].jobId : req.jobId; - this.logger.info({ jobId: jobId, msg: 'Start task(s) creation' }); - if (Array.isArray(req)) { - const ids: string[] = []; - const errors: string[] = []; - for (const request of req) { - try { - const res = await this.createSingleTask(request); - ids.push(res.id); - } catch (err) { - if (err instanceof ConflictError) { - const error = err as Error; - errors.push(error.message); - } else { - const error = err as Error; - const message = 'failed create task'; - this.logger.error({ jobId: request.jobId, taskType: request.type, err: error, msg: message }); - throw err; + async createTask(req: CreateTasksRequest): Promise { + const jobId = Array.isArray(req) ? req[0].jobId : req.jobId; + logger.info({ jobId: jobId, msg: 'Start task(s) creation' }); + if (Array.isArray(req)) { + const ids: string[] = []; + const errors: string[] = []; + for (const request of req) { + try { + const res = await this.createSingleTask(request); + ids.push(res.id); + } catch (err) { + if (err instanceof ConflictError) { + const error = err as Error; + errors.push(error.message); + } else { + const error = err as Error; + const message = 'failed create task'; + logger.error({ jobId: request.jobId, taskType: request.type, err: error, msg: message }); + throw err; + } } } + logger.info({ jobId: jobId, ids: ids, errors: errors, msg: 'Finished tasks creation successfully' }); + return errors.length != 0 ? { ids, errors } : { ids }; + } else { + const res = await this.createSingleTask(req); + logger.info({ jobId: jobId, id: res.id, msg: 'Finished task creation successfully' }); + return res; } - this.logger.info({ jobId: jobId, ids: ids, errors: errors, msg: 'Finished tasks creation successfully' }); - return errors.length != 0 ? { ids, errors } : { ids }; - } else { - const res = await this.createSingleTask(req); - this.logger.info({ jobId: jobId, id: res.id, msg: 'Finished task creation successfully' }); - return res; - } - } + }, - public async getTask(req: ISpecificTaskParams): Promise { - const entity = await this.findOneBy({ id: req.taskId, jobId: req.jobId }); - const model = entity ? this.taskConvertor.entityToModel(entity) : undefined; - return model; - } + async getTask(req: ISpecificTaskParams): Promise { + const entity = await this.findOneBy({ id: req.taskId, jobId: req.jobId }); + const model = entity ? taskConvertor.entityToModel(entity) : undefined; + return model; + }, - public async exists(taskIdentifier: ISpecificTaskParams): Promise { - const taskCount = await this.countBy({ id: taskIdentifier.taskId, jobId: taskIdentifier.jobId }); - return taskCount === 1; - } + async exists(taskIdentifier: ISpecificTaskParams): Promise { + const taskCount = await this.countBy({ id: taskIdentifier.taskId, jobId: taskIdentifier.jobId }); + return taskCount === 1; + }, - public async updateTask(req: IUpdateTaskRequest): Promise { - this.logger.info({ jobId: req.jobId, taskId: req.taskId, msg: 'Start task update successfully' }); - if (!(await this.exists(req))) { - const message = 'task for update not found with provided jobId and taskId'; - this.logger.error({ jobId: req.jobId, taskId: req.taskId, msg: message }); - throw new NotFoundError(message); - } - const entity = this.taskConvertor.updateModelToEntity(req); - await this.save(entity); - this.logger.info({ jobId: req.jobId, taskId: req.taskId, msg: 'Finish task update successfully' }); - } + async updateTask(req: IUpdateTaskRequest): Promise { + logger.info({ jobId: req.jobId, taskId: req.taskId, msg: 'Start task update successfully' }); + if (!(await this.exists(req))) { + const message = 'task for update not found with provided jobId and taskId'; + logger.error({ jobId: req.jobId, taskId: req.taskId, msg: message }); + throw new NotFoundError(message); + } + const entity = taskConvertor.updateModelToEntity(req); + await this.save(entity); + logger.info({ jobId: req.jobId, taskId: req.taskId, msg: 'Finish task update successfully' }); + }, - public async deleteTask(taskIdentifier: ISpecificTaskParams): Promise { - if (!(await this.exists(taskIdentifier))) { - const message = 'provided task not found for delete'; - this.logger.error({ id: taskIdentifier.taskId, jobId: taskIdentifier.jobId, msg: message }); - throw new NotFoundError(message); - } - await this.delete({ id: taskIdentifier.taskId, jobId: taskIdentifier.jobId }); - this.logger.info({ id: taskIdentifier.taskId, jobId: taskIdentifier.jobId, msg: 'Finish task deletion successfully' }); - } + async deleteTask(taskIdentifier: ISpecificTaskParams): Promise { + if (!(await this.exists(taskIdentifier))) { + const message = 'provided task not found for delete'; + logger.error({ id: taskIdentifier.taskId, jobId: taskIdentifier.jobId, msg: message }); + throw new NotFoundError(message); + } + await this.delete({ id: taskIdentifier.taskId, jobId: taskIdentifier.jobId }); + logger.info({ id: taskIdentifier.taskId, jobId: taskIdentifier.jobId, msg: 'Finish task deletion successfully' }); + }, - public async retrieveAndUpdate(jobType: string, taskType: string): Promise { - const retrieveAndUpdateQuery = ` - UPDATE "Task" + async retrieveAndUpdate(jobType: string, taskType: string): Promise { + const retrieveAndUpdateQuery = ` + UPDATE ${schema}"Task" SET status = 'In-Progress'::"operation_status_enum", "updateTime" = now() WHERE id = ( SELECT tk.id @@ -125,146 +121,160 @@ export class TaskRepository extends GeneralRepository { FOR UPDATE SKIP LOCKED ) RETURNING *;`; - const res = (await this.query(retrieveAndUpdateQuery, [taskType, jobType])) as SqlRawResponse; + const res = (await this.query(retrieveAndUpdateQuery, [taskType, jobType])) as SqlRawResponse; - if (res[1] === 0) { - return undefined; - } - const entity = res[0][0] as TaskEntity; - return this.taskConvertor.entityToModel(entity); - } - - public async releaseInactiveTask(taskIds: string[]): Promise { - const res = await this.createQueryBuilder() - .update() - .set({ status: OperationStatus.PENDING, attempts: () => 'attempts + 1' }) - .where({ id: In(taskIds), status: OperationStatus.IN_PROGRESS }) - .returning('id') - .updateEntity(true) - .execute(); - const raw = res.raw as { id: string }[]; - const updatedIds = raw.map((value) => { - return (value as TaskEntity).id; - }); - return updatedIds; - } + if (res[1] === 0) { + return undefined; + } + const entity = res[0][0] as TaskEntity; + return taskConvertor.entityToModel(entity); + }, - public async findInactiveTasks(req: IFindInactiveTasksRequest): Promise { - //find timed out "In-Progress" tasks (of given types if requested) - const secToMsConversionRate = 1000; - const olderThen = new Date(Date.now() - req.inactiveTimeSec * secToMsConversionRate); - let query = this.createQueryBuilder('tk') - .select('tk.id AS id') - .where({ - status: OperationStatus.IN_PROGRESS, - updateTime: LessThan(olderThen), + async releaseInactiveTask(taskIds: string[]): Promise { + const res = await this.createQueryBuilder() + .update() + .set({ status: OperationStatus.PENDING, attempts: () => 'attempts + 1' }) + .where({ id: In(taskIds), status: OperationStatus.IN_PROGRESS }) + .returning('id') + .updateEntity(true) + .execute(); + const raw = res.raw as { id: string }[]; + const updatedIds = raw.map((value) => { + return (value as TaskEntity).id; }); - const hasTypes = req.types != undefined && req.types.length > 0; - const hasIgnoredTypes = req.ignoreTypes != undefined && req.ignoreTypes.length > 0; - if (hasTypes || hasIgnoredTypes) { - query = query.innerJoin('tk.jobId', 'jb'); - if (hasTypes) { - const types = req.types as ITaskType[]; - query = query.andWhere( - new Brackets((qb) => { - qb.where('tk.type = :taskType AND jb.type = :jobType', types[0]); - for (let i = 1; i < types.length; i++) { - qb.orWhere('tk.type = :taskType AND jb.type = :jobType', types[i]); - } - }) - ); - } - if (hasIgnoredTypes) { - const ignoredTypes = req.ignoreTypes as ITaskType[]; - query = query.andWhere( - new Brackets((qb) => { - qb.where('NOT (tk.type = :taskType AND jb.type = :jobType)', ignoredTypes[0]); - for (let i = 1; i < ignoredTypes.length; i++) { - qb.andWhere('NOT (tk.type = :taskType AND jb.type = :jobType)', ignoredTypes[i]); - } - }) - ); + return updatedIds; + }, + + async findInactiveTasks(req: IFindInactiveTasksRequest): Promise { + //find timed out "In-Progress" tasks (of given types if requested) + const secToMsConversionRate = 1000; + const olderThen = new Date(Date.now() - req.inactiveTimeSec * secToMsConversionRate); + let query = this.createQueryBuilder('tk') + .select('tk.id AS id') + .where({ + status: OperationStatus.IN_PROGRESS, + updateTime: LessThan(olderThen), + }); + const hasTypes = req.types != undefined && req.types.length > 0; + const hasIgnoredTypes = req.ignoreTypes != undefined && req.ignoreTypes.length > 0; + if (hasTypes || hasIgnoredTypes) { + query = query.innerJoin('tk.jobId', 'jb'); + if (hasTypes) { + const types = req.types as ITaskType[]; + query = query.andWhere( + new Brackets((qb) => { + qb.where('tk.type = :taskType AND jb.type = :jobType', types[0]); + for (let i = 1; i < types.length; i++) { + qb.orWhere('tk.type = :taskType AND jb.type = :jobType', types[i]); + } + }) + ); + } + if (hasIgnoredTypes) { + const ignoredTypes = req.ignoreTypes as ITaskType[]; + query = query.andWhere( + new Brackets((qb) => { + qb.where('NOT (tk.type = :taskType AND jb.type = :jobType)', ignoredTypes[0]); + for (let i = 1; i < ignoredTypes.length; i++) { + qb.andWhere('NOT (tk.type = :taskType AND jb.type = :jobType)', ignoredTypes[i]); + } + }) + ); + } } - } - const res = (await query.execute()) as { id: string }[]; - return res.map((value) => value.id); - } + const res = (await query.execute()) as { id: string }[]; + return res.map((value) => value.id); + }, - public async checkIfAllCompleted(jobId: string): Promise { - const count = await this.count({ where: { jobId } }); - const allCompleted = await this.getTasksCountByStatus(OperationStatus.COMPLETED, jobId); - return count === allCompleted; - } + async checkIfAllCompleted(jobId: string): Promise { + const count = await this.count({ where: { jobId } }); + const allCompleted = await this.getTasksCountByStatus(OperationStatus.COMPLETED, jobId); + return count === allCompleted; + }, - public async getTasksCountByStatus(status: OperationStatus, jobId: string): Promise { - const count = await this.count({ - where: { - status, - jobId, - }, - }); - return count; - } + async getTasksCountByStatus(status: OperationStatus, jobId: string): Promise { + const count = await this.count({ + where: { + status, + jobId, + }, + }); + return count; + }, - public async updateTasksOfExpiredJobs(): Promise { - const query = this.createQueryBuilder() - .update() - .set({ status: OperationStatus.EXPIRED }) - .where( - `"jobId" IN ( + async updateTasksOfExpiredJobs(): Promise { + const query = this.createQueryBuilder() + .update() + .set({ status: OperationStatus.EXPIRED }) + .where( + `"jobId" IN ( SELECT id FROM "${this.dbConfig.schema as string}"."Job" as jb WHERE jb.status = :status)`, - { status: OperationStatus.EXPIRED } - ) - .andWhere( - new Brackets((qb) => { - qb.where([{ status: OperationStatus.IN_PROGRESS }, { status: OperationStatus.PENDING }]); - }) - ); - await query.execute(); - } + { status: OperationStatus.EXPIRED } + ) + .andWhere( + new Brackets((qb) => { + qb.where([{ status: OperationStatus.IN_PROGRESS }, { status: OperationStatus.PENDING }]); + }) + ); + await query.execute(); + }, - public async resetJobTasks(jobId: string): Promise { - await this.createQueryBuilder() - .update() - .set({ status: OperationStatus.PENDING, reason: '', attempts: 0, percentage: 0 }) - .where('"jobId" = :jobId', { jobId }) - .andWhere( - new Brackets((qb) => { - qb.where([{ status: OperationStatus.FAILED }, { status: OperationStatus.EXPIRED }, { status: OperationStatus.ABORTED }]); - }) - ) - .execute(); - } + async resetJobTasks(jobId: string): Promise { + await this.createQueryBuilder() + .update() + .set({ status: OperationStatus.PENDING, reason: '', attempts: 0, percentage: 0 }) + .where('"jobId" = :jobId', { jobId }) + .andWhere( + new Brackets((qb) => { + qb.where([{ status: OperationStatus.FAILED }, { status: OperationStatus.EXPIRED }, { status: OperationStatus.ABORTED }]); + }) + ) + .execute(); + }, - public async abortJobTasks(jobId: string): Promise { - return this.update({ jobId, status: OperationStatus.PENDING }, { status: OperationStatus.ABORTED }); - } + async abortJobTasks(jobId: string): Promise { + return this.update({ jobId, status: OperationStatus.PENDING }, { status: OperationStatus.ABORTED }); + }, - private async createSingleTask(req: ICreateTaskRequest): Promise { - try { - let entity = this.taskConvertor.createModelToEntity(req); - entity = await this.save(entity); - return { - id: entity.id, - }; - } catch (err) { - const pgForeignKeyViolationErrorCode = '23503'; - const pgExclusionViolationErrorCode = '23P01'; - const error = err as Error & { code: string }; - if (error.code === pgForeignKeyViolationErrorCode && error.message.includes('FK_task_job_id')) { - const message = `failed to create task for job: ${req.jobId}, job id was not found.`; - this.logger.error(message); - throw new NotFoundError(message); - } - if (error.code === pgExclusionViolationErrorCode && error.message.includes('UQ_uniqueness_on_job_and_type')) { - const message = `failed to create ${req.type as string} task for job ${req.jobId} because it already exists.`; - this.logger.warn(message); - throw new ConflictError(message); + async createSingleTask(req: ICreateTaskRequest): Promise { + try { + let entity = taskConvertor.createModelToEntity(req); + entity = await this.save(entity); + return { + id: entity.id, + }; + } catch (err) { + const pgForeignKeyViolationErrorCode = '23503'; + const pgExclusionViolationErrorCode = '23P01'; + const error = err as Error & { code: string }; + if (error.code === pgForeignKeyViolationErrorCode && error.message.includes('FK_task_job_id')) { + const message = `failed to create task for job: ${req.jobId}, job id was not found.`; + logger.error(message); + throw new NotFoundError(message); + } + if (error.code === pgExclusionViolationErrorCode && error.message.includes('UQ_uniqueness_on_job_and_type')) { + const message = `failed to create ${req.type as string} task for job ${req.jobId} because it already exists.`; + logger.warn(message); + throw new ConflictError(message); + } + throw err; } - throw err; - } - } -} + }, + }); +}; + +export type TaskRepository = ReturnType; + +export const TASK_CUSTOM_REPOSITORY_SYMBOL = Symbol('TASK_CUSTOM_REPOSITORY_SYMBOL'); + +export const taskRepositoryFactory: FactoryFunction> = (depContainer) => { + return getJobRepository( + depContainer.resolve(DataSource), + depContainer.resolve(SERVICES.CONFIG), + depContainer.resolve(SERVICES.LOGGER), + depContainer.resolve(TaskModelConvertor) + ); +}; diff --git a/src/DAL/repositories/transactionActions.ts b/src/DAL/repositories/transactionActions.ts index ed3d782..00cd048 100644 --- a/src/DAL/repositories/transactionActions.ts +++ b/src/DAL/repositories/transactionActions.ts @@ -1,61 +1,24 @@ -// import { singleton } from 'tsyringe'; -// import { ObjectType, QueryRunner } from 'typeorm'; -// import { BadRequestError } from '@map-colonies/error-types'; -// import { OperationStatus } from '../../common/dataModels/enums'; -// import { ConnectionManager } from '../connectionBuilder'; -// import { JobRepository } from './jobRepository'; -// import { TaskRepository } from './taskRepository'; +import { singleton } from 'tsyringe'; +import { DataSource } from 'typeorm'; +import { BadRequestError } from '@map-colonies/error-types'; +import { OperationStatus } from '../../common/dataModels/enums'; +import { JobRepository } from './jobRepository'; +import { TaskRepository } from './taskRepository'; -// @singleton() -// export class TransactionActions { -// public constructor(private readonly connectionManager: ConnectionManager) {} +@singleton() +export class TransactionActions { + public constructor(private readonly db: DataSource, private readonly jobRepo: JobRepository, private readonly taskRepo: TaskRepository) {} -// public async resetJob(jobId: string, expirationDate?: Date): Promise { -// return this.handleTransaction(async (runner: QueryRunner) => { -// const jobRepo = this.getJobRepository(runner); -// if (await jobRepo.isJobResettable(jobId)) { -// await jobRepo.updateJob({ jobId, expirationDate, status: OperationStatus.IN_PROGRESS }); -// const taskRepo = this.getTaskRepository(runner); -// await taskRepo.resetJobTasks(jobId); -// } else { -// throw new BadRequestError(`job ${jobId} is not resettable.`); -// } -// }); -// } - -// private async handleTransaction(logic: (runner: QueryRunner) => Promise): Promise { -// const runner = await this.getTransactionRunner(); -// try { -// const res = await logic(runner); -// await runner.commitTransaction(); -// return res; -// } catch (err) { -// await runner.rollbackTransaction(); -// throw err; -// } finally { -// await runner.release(); -// } -// } - -// private async getTransactionRunner(): Promise { -// if (!this.connectionManager.isConnected()) { -// await this.connectionManager.init(); -// } -// const runner = await this.connectionManager.createQueryRunner(); -// await runner.connect(); -// await runner.startTransaction(); -// return runner; -// } - -// private getJobRepository(queryRunner: QueryRunner): JobRepository { -// return this.getRepository(JobRepository, queryRunner); -// } - -// private getTaskRepository(queryRunner: QueryRunner): TaskRepository { -// return this.getRepository(TaskRepository, queryRunner); -// } - -// private getRepository(repository: ObjectType, queryRunner: QueryRunner): T { -// return queryRunner.manager.withRepository(repository); -// } -// } + public async resetJob(jobId: string, expirationDate?: Date): Promise { + await this.db.transaction(async (manager) => { + const transactionJobRepo = manager.withRepository(this.jobRepo); + if (await transactionJobRepo.isJobResettable(jobId)) { + await transactionJobRepo.updateJob({ jobId, expirationDate, status: OperationStatus.IN_PROGRESS }); + const transactionTaskRepo = manager.withRepository(this.taskRepo); + await transactionTaskRepo.resetJobTasks(jobId); + } else { + throw new BadRequestError(`job ${jobId} is not resettable.`); + } + }); + } +} diff --git a/src/containerConfig.ts b/src/containerConfig.ts index b69a469..53dab70 100644 --- a/src/containerConfig.ts +++ b/src/containerConfig.ts @@ -4,18 +4,23 @@ import { trace } from '@opentelemetry/api'; import { DependencyContainer } from 'tsyringe/dist/typings/types'; import jsLogger, { LoggerOptions } from '@map-colonies/js-logger'; import { Metrics } from '@map-colonies/telemetry'; +import { DataSource } from 'typeorm'; import { SERVICES, SERVICE_NAME } from './common/constants'; import { tracing } from './common/tracing'; import { InjectionObject, registerDependencies } from './common/dependencyRegistration'; import { jobRouterFactory, JOB_ROUTER_SYMBOL } from './jobs/routes/jobRouter'; import { taskManagerRouterFactory, TASK_MANAGER_ROUTER_SYMBOL } from './taskManagement/routes/taskManagerRouter'; +import { initDataSource } from './DAL/connectionBuilder'; +import { IDbConfig } from './common/interfaces'; +import { jobRepositoryFactory, JOB_CUSTOM_REPOSITORY_SYMBOL } from './DAL/repositories/jobRepository'; +import { taskRepositoryFactory } from './DAL/repositories/taskRepository'; export interface RegisterOptions { override?: InjectionObject[]; useChild?: boolean; } -export const registerExternalValues = (options?: RegisterOptions): DependencyContainer => { +export const registerExternalValues = async (options?: RegisterOptions): Promise => { const loggerConfig = config.get('telemetry.logger'); // @ts-expect-error the signature is wrong const logger = jsLogger({ ...loggerConfig, prettyPrint: loggerConfig.prettyPrint, hooks: { logMethod } }); @@ -26,6 +31,8 @@ export const registerExternalValues = (options?: RegisterOptions): DependencyCon tracing.start(); const tracer = trace.getTracer(SERVICE_NAME); + const db = await initDataSource(config.get('typeOrm')); + const dependencies: InjectionObject[] = [ { token: SERVICES.CONFIG, provider: { useValue: config } }, { token: SERVICES.LOGGER, provider: { useValue: logger } }, @@ -43,6 +50,9 @@ export const registerExternalValues = (options?: RegisterOptions): DependencyCon }, }, }, + { token: DataSource, provider: { useValue: db } }, + { token: JOB_CUSTOM_REPOSITORY_SYMBOL, provider: { useFactory: jobRepositoryFactory } }, + { token: TASK_MANAGER_ROUTER_SYMBOL, provider: { useFactory: taskRepositoryFactory } }, ]; return registerDependencies(dependencies, options?.override, options?.useChild); From 691a8457fde18d8edb6b15f99cb5882fd89bfbc7 Mon Sep 17 00:00:00 2001 From: Shahar s Date: Wed, 28 Sep 2022 16:30:57 +0300 Subject: [PATCH 3/4] chore: cleanup --- src/DAL/repositories/generalRepository.ts | 50 ----------------------- 1 file changed, 50 deletions(-) delete mode 100644 src/DAL/repositories/generalRepository.ts diff --git a/src/DAL/repositories/generalRepository.ts b/src/DAL/repositories/generalRepository.ts deleted file mode 100644 index 7ab8371..0000000 --- a/src/DAL/repositories/generalRepository.ts +++ /dev/null @@ -1,50 +0,0 @@ -import { ObjectLiteral, Repository } from 'typeorm'; -import { IConfig, IDbConfig } from '../../common/interfaces'; - -export abstract class GeneralRepository { - protected readonly dbConfig: IDbConfig; - - protected metadata = this.repository.metadata; - protected manager = this.repository.manager; - protected queryRunner = this.repository.queryRunner; - - protected constructor(private readonly repository: Repository, protected readonly config: IConfig) { - this.dbConfig = this.config.get('typeOrm'); - } - - /* eslint-disable-next-line @typescript-eslint/no-explicit-any */ - protected async query(query: string, parameters?: any[] | undefined): Promise { - await this.repository.query(`SET search_path TO "${this.dbConfig.schema as string}", public`); - return this.repository.query(query, parameters); - } - - /* eslint-disable @typescript-eslint/member-ordering */ - protected createQueryBuilder = this.repository.createQueryBuilder.bind(this.repository); - protected count = this.repository.count.bind(this.repository); - protected countBy = this.repository.countBy.bind(this.repository); - protected create = this.repository.create.bind(this.repository); - protected decrement = this.repository.decrement.bind(this.repository); - protected delete = this.repository.delete.bind(this.repository); - protected find = this.repository.find.bind(this.repository); - protected findAndCount = this.repository.findAndCount.bind(this.repository); - protected findAndCountBy = this.repository.findAndCountBy.bind(this.repository); - protected findBy = this.repository.findBy.bind(this.repository); - protected findOne = this.repository.findOne.bind(this.repository); - protected findOneBy = this.repository.findOneBy.bind(this.repository); - protected findOneByOrFail = this.repository.findOneByOrFail.bind(this.repository); - protected findOneOrFail = this.repository.findOneOrFail.bind(this.repository); - protected getId = this.repository.getId.bind(this.repository); - protected hasId = this.repository.hasId.bind(this.repository); - protected increment = this.repository.increment.bind(this.repository); - protected insert = this.repository.insert.bind(this.repository); - protected merge = this.repository.merge.bind(this.repository); - protected preload = this.repository.preload.bind(this.repository); - protected recover = this.repository.recover.bind(this.repository); - protected remove = this.repository.remove.bind(this.repository); - protected restore = this.repository.restore.bind(this.repository); - protected save = this.repository.save.bind(this.repository); - protected softDelete = this.repository.softDelete.bind(this.repository); - protected softRemove = this.repository.softRemove.bind(this.repository); - protected update = this.repository.update.bind(this.repository); - protected upsert = this.repository.upsert.bind(this.repository); -} From 5a31042cba56cfe40f20451c79ac551c2046230e Mon Sep 17 00:00:00 2001 From: Shahar s Date: Thu, 29 Sep 2022 13:41:41 +0300 Subject: [PATCH 4/4] feat: update usage and migration --- .eslintrc.json | 2 +- 1663841935533-initaialMigration.ts | 45 ------ .../1663845942790-initaialMigration.ts | 47 ------ .../1664441289062-initial_migration.ts | 135 ++++++++++++++++++ src/DAL/entity/job.ts | 2 +- src/DAL/entity/task.ts | 2 +- src/DAL/repositories/jobRepository.ts | 4 +- src/DAL/repositories/taskRepository.ts | 4 +- src/DAL/repositories/transactionActions.ts | 12 +- src/app.ts | 4 +- src/containerConfig.ts | 4 +- src/index.ts | 16 +-- src/jobs/models/jobManager.ts | 37 ++--- src/jobs/models/taskManager.ts | 45 ++---- .../models/taskManagementManger.ts | 55 ++----- 15 files changed, 197 insertions(+), 217 deletions(-) delete mode 100644 1663841935533-initaialMigration.ts delete mode 100644 db/migrations/1663845942790-initaialMigration.ts create mode 100644 db/migrations/1664441289062-initial_migration.ts diff --git a/.eslintrc.json b/.eslintrc.json index b41d3c7..7841890 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -1,5 +1,5 @@ { - "ignorePatterns": ["**/*.js", "dist", "helm"], + "ignorePatterns": ["**/*.js", "dist", "helm", "db/migrations"], "extends": ["@map-colonies/eslint-config/jest", "@map-colonies/eslint-config/ts-base"], "parserOptions": { "project": "./tsconfig.lint.json" diff --git a/1663841935533-initaialMigration.ts b/1663841935533-initaialMigration.ts deleted file mode 100644 index 8825422..0000000 --- a/1663841935533-initaialMigration.ts +++ /dev/null @@ -1,45 +0,0 @@ -import { MigrationInterface, QueryRunner } from 'typeorm'; - -export class initaialMigration1663841935533 implements MigrationInterface { - name = 'initaialMigration1663841935533'; - - public async up(queryRunner: QueryRunner): Promise { - await queryRunner.query( - `CREATE TYPE "JobManager"."Task_status_enum" AS ENUM('Pending', 'In-Progress', 'Completed', 'Failed', 'Expired', 'Aborted')` - ); - await queryRunner.query( - `CREATE TABLE "JobManager"."Task" ("id" uuid NOT NULL DEFAULT uuid_generate_v4(), "jobId" uuid NOT NULL, "type" character varying(255) NOT NULL, "description" character varying(2000) NOT NULL DEFAULT '', "parameters" jsonb NOT NULL, "creationTime" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), "updateTime" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), "status" "JobManager"."Task_status_enum" NOT NULL DEFAULT 'Pending', "percentage" smallint, "reason" character varying NOT NULL DEFAULT '', "attempts" integer NOT NULL DEFAULT '0', "resettable" boolean NOT NULL DEFAULT true, "block_duplication" boolean NOT NULL DEFAULT false, CONSTRAINT "UQ_uniqueness_on_job_and_type" EXCLUDE EXCLUDE ("type" with =, "jobId" with =) WHERE ("block_duplication" = true, CONSTRAINT "PK_task_id" PRIMARY KEY ("id"))` - ); - await queryRunner.query(`CREATE INDEX "taskResettableIndex" ON "JobManager"."Task" ("resettable") WHERE "resettable" = FALSE`); - await queryRunner.query( - `CREATE TYPE "JobManager"."Job_status_enum" AS ENUM('Pending', 'In-Progress', 'Completed', 'Failed', 'Expired', 'Aborted')` - ); - await queryRunner.query( - `CREATE TABLE "JobManager"."Job" ("id" uuid NOT NULL DEFAULT uuid_generate_v4(), "resourceId" character varying(300) NOT NULL, "version" character varying(30) NOT NULL, "type" character varying(255) NOT NULL, "description" character varying(2000) NOT NULL DEFAULT '', "parameters" jsonb NOT NULL, "creationTime" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), "updateTime" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), "status" "JobManager"."Job_status_enum" NOT NULL DEFAULT 'Pending', "percentage" smallint, "reason" character varying NOT NULL DEFAULT '', "isCleaned" boolean NOT NULL DEFAULT false, "priority" integer NOT NULL DEFAULT '1000', "expirationDate" TIMESTAMP WITH TIME ZONE, "internalId" uuid, "producerName" text, "productName" text, "productType" text, "additionalIdentifiers" text, "taskCount" integer NOT NULL DEFAULT '0', "completedTasks" integer NOT NULL DEFAULT '0', "failedTasks" integer NOT NULL DEFAULT '0', "expiredTasks" integer NOT NULL DEFAULT '0', "pendingTasks" integer NOT NULL DEFAULT '0', "inProgressTasks" integer NOT NULL DEFAULT '0', "abortedTasks" integer NOT NULL DEFAULT '0', CONSTRAINT "UQ_uniqueness_on_active_tasks" EXCLUDE EXCLUDE ("resourceId" with =, "version" with =, "type" with =, "additionalIdentifiers" with =) WHERE (status = 'Pending' OR status = 'In-Progress'), CONSTRAINT "PK_job_id" PRIMARY KEY ("id"))` - ); - await queryRunner.query(`CREATE INDEX "jobTypeIndex" ON "JobManager"."Job" ("type") `); - await queryRunner.query(`CREATE INDEX "jobStatusIndex" ON "JobManager"."Job" ("status") `); - await queryRunner.query(`CREATE INDEX "jobCleanedIndex" ON "JobManager"."Job" ("isCleaned") `); - await queryRunner.query(`CREATE INDEX "jobPriorityIndex" ON "JobManager"."Job" ("priority") `); - await queryRunner.query(`CREATE INDEX "jobExpirationDateIndex" ON "JobManager"."Job" ("expirationDate") `); - await queryRunner.query(`CREATE INDEX "jobResourceIndex" ON "JobManager"."Job" ("resourceId", "version") `); - await queryRunner.query( - `ALTER TABLE "JobManager"."Task" ADD CONSTRAINT "FK_task_job_id" FOREIGN KEY ("jobId") REFERENCES "JobManager"."Job"("id") ON DELETE NO ACTION ON UPDATE NO ACTION` - ); - } - - public async down(queryRunner: QueryRunner): Promise { - await queryRunner.query(`ALTER TABLE "JobManager"."Task" DROP CONSTRAINT "FK_task_job_id"`); - await queryRunner.query(`DROP INDEX "JobManager"."jobResourceIndex"`); - await queryRunner.query(`DROP INDEX "JobManager"."jobExpirationDateIndex"`); - await queryRunner.query(`DROP INDEX "JobManager"."jobPriorityIndex"`); - await queryRunner.query(`DROP INDEX "JobManager"."jobCleanedIndex"`); - await queryRunner.query(`DROP INDEX "JobManager"."jobStatusIndex"`); - await queryRunner.query(`DROP INDEX "JobManager"."jobTypeIndex"`); - await queryRunner.query(`DROP TABLE "JobManager"."Job"`); - await queryRunner.query(`DROP TYPE "JobManager"."Job_status_enum"`); - await queryRunner.query(`DROP INDEX "JobManager"."taskResettableIndex"`); - await queryRunner.query(`DROP TABLE "JobManager"."Task"`); - await queryRunner.query(`DROP TYPE "JobManager"."Task_status_enum"`); - } -} diff --git a/db/migrations/1663845942790-initaialMigration.ts b/db/migrations/1663845942790-initaialMigration.ts deleted file mode 100644 index 2ce8c5d..0000000 --- a/db/migrations/1663845942790-initaialMigration.ts +++ /dev/null @@ -1,47 +0,0 @@ -import { MigrationInterface, QueryRunner } from 'typeorm'; - -export class initaialMigration1663845942790 implements MigrationInterface { - name = 'initaialMigration1663845942790'; - - public async up(queryRunner: QueryRunner): Promise { - //status type duplication is typeorm bug https://github.com/typeorm/typeorm/issues/8136 - await queryRunner.query( - `CREATE TYPE "JobManager"."Task_status_enum" AS ENUM('Pending', 'In-Progress', 'Completed', 'Failed', 'Expired', 'Aborted')` - ); - await queryRunner.query( - `CREATE TABLE "JobManager"."Task" ("id" uuid NOT NULL DEFAULT uuid_generate_v4(), "jobId" uuid NOT NULL, "type" character varying(255) NOT NULL, "description" character varying(2000) NOT NULL DEFAULT '', "parameters" jsonb NOT NULL, "creationTime" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), "updateTime" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), "status" "JobManager"."Task_status_enum" NOT NULL DEFAULT 'Pending', "percentage" smallint, "reason" character varying NOT NULL DEFAULT '', "attempts" integer NOT NULL DEFAULT '0', "resettable" boolean NOT NULL DEFAULT true, "block_duplication" boolean NOT NULL DEFAULT false, CONSTRAINT "UQ_uniqueness_on_job_and_type" EXCLUDE EXCLUDE ("type" with =, "jobId" with =) WHERE ("block_duplication" = true, CONSTRAINT "PK_task_id" PRIMARY KEY ("id"))` - ); - await queryRunner.query(`CREATE INDEX "taskResettableIndex" ON "JobManager"."Task" ("resettable") WHERE "resettable" = FALSE`); - await queryRunner.query( - `CREATE TYPE "JobManager"."Job_status_enum" AS ENUM('Pending', 'In-Progress', 'Completed', 'Failed', 'Expired', 'Aborted')` - ); - await queryRunner.query( - `CREATE TABLE "JobManager"."Job" ("id" uuid NOT NULL DEFAULT uuid_generate_v4(), "resourceId" character varying(300) NOT NULL, "version" character varying(30) NOT NULL, "type" character varying(255) NOT NULL, "description" character varying(2000) NOT NULL DEFAULT '', "parameters" jsonb NOT NULL, "creationTime" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), "updateTime" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), "status" "JobManager"."Job_status_enum" NOT NULL DEFAULT 'Pending', "percentage" smallint, "reason" character varying NOT NULL DEFAULT '', "isCleaned" boolean NOT NULL DEFAULT false, "priority" integer NOT NULL DEFAULT '1000', "expirationDate" TIMESTAMP WITH TIME ZONE, "internalId" uuid, "producerName" text, "productName" text, "productType" text, "additionalIdentifiers" text, "taskCount" integer NOT NULL DEFAULT '0', "completedTasks" integer NOT NULL DEFAULT '0', "failedTasks" integer NOT NULL DEFAULT '0', "expiredTasks" integer NOT NULL DEFAULT '0', "pendingTasks" integer NOT NULL DEFAULT '0', "inProgressTasks" integer NOT NULL DEFAULT '0', "abortedTasks" integer NOT NULL DEFAULT '0', CONSTRAINT "UQ_uniqueness_on_active_tasks" EXCLUDE EXCLUDE ("resourceId" with =, "version" with =, "type" with =, "additionalIdentifiers" with =) WHERE (status = 'Pending' OR status = 'In-Progress'), CONSTRAINT "PK_job_id" PRIMARY KEY ("id"))` - ); - await queryRunner.query(`CREATE INDEX "jobTypeIndex" ON "JobManager"."Job" ("type") `); - await queryRunner.query(`CREATE INDEX "jobStatusIndex" ON "JobManager"."Job" ("status") `); - await queryRunner.query(`CREATE INDEX "jobCleanedIndex" ON "JobManager"."Job" ("isCleaned") `); - await queryRunner.query(`CREATE INDEX "jobPriorityIndex" ON "JobManager"."Job" ("priority") `); - await queryRunner.query(`CREATE INDEX "jobExpirationDateIndex" ON "JobManager"."Job" ("expirationDate") `); - await queryRunner.query(`CREATE INDEX "jobResourceIndex" ON "JobManager"."Job" ("resourceId", "version") `); - await queryRunner.query( - `ALTER TABLE "JobManager"."Task" ADD CONSTRAINT "FK_task_job_id" FOREIGN KEY ("jobId") REFERENCES "JobManager"."Job"("id") ON DELETE NO ACTION ON UPDATE NO ACTION` - ); - //TODO: add functions and triggers - } - - public async down(queryRunner: QueryRunner): Promise { - await queryRunner.query(`ALTER TABLE "JobManager"."Task" DROP CONSTRAINT "FK_task_job_id"`); - await queryRunner.query(`DROP INDEX "JobManager"."jobResourceIndex"`); - await queryRunner.query(`DROP INDEX "JobManager"."jobExpirationDateIndex"`); - await queryRunner.query(`DROP INDEX "JobManager"."jobPriorityIndex"`); - await queryRunner.query(`DROP INDEX "JobManager"."jobCleanedIndex"`); - await queryRunner.query(`DROP INDEX "JobManager"."jobStatusIndex"`); - await queryRunner.query(`DROP INDEX "JobManager"."jobTypeIndex"`); - await queryRunner.query(`DROP TABLE "JobManager"."Job"`); - await queryRunner.query(`DROP TYPE "JobManager"."Job_status_enum"`); - await queryRunner.query(`DROP INDEX "JobManager"."taskResettableIndex"`); - await queryRunner.query(`DROP TABLE "JobManager"."Task"`); - await queryRunner.query(`DROP TYPE "JobManager"."Task_status_enum"`); - } -} diff --git a/db/migrations/1664441289062-initial_migration.ts b/db/migrations/1664441289062-initial_migration.ts new file mode 100644 index 0000000..3f090c2 --- /dev/null +++ b/db/migrations/1664441289062-initial_migration.ts @@ -0,0 +1,135 @@ +import { MigrationInterface, QueryRunner } from "typeorm"; + +export class initialMigration1664441289062 implements MigrationInterface { + name = 'initialMigration1664441289062' + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query(`CREATE EXTENSION IF NOT EXISTS "uuid-ossp";`); + //status type duplication is typeorm bug https://github.com/typeorm/typeorm/issues/8136 + await queryRunner.query(`CREATE TYPE "JobManager"."Task_status_enum" AS ENUM('Pending', 'In-Progress', 'Completed', 'Failed', 'Expired', 'Aborted')`); + await queryRunner.query(`CREATE TABLE "JobManager"."Task" ("id" uuid NOT NULL DEFAULT uuid_generate_v4(), "jobId" uuid NOT NULL, "type" character varying(255) NOT NULL, "description" character varying(2000) NOT NULL DEFAULT '', "parameters" jsonb NOT NULL, "creationTime" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), "updateTime" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), "status" "JobManager"."Task_status_enum" NOT NULL DEFAULT 'Pending', "percentage" smallint, "reason" text NOT NULL DEFAULT '', "attempts" integer NOT NULL DEFAULT '0', "resettable" boolean NOT NULL DEFAULT true, "block_duplication" boolean NOT NULL DEFAULT false, CONSTRAINT "UQ_uniqueness_on_job_and_type" EXCLUDE ("type" with =, "jobId" with =) WHERE ("block_duplication" = true), CONSTRAINT "PK_task_id" PRIMARY KEY ("id"))`); + await queryRunner.query(`CREATE INDEX "taskResettableIndex" ON "JobManager"."Task" ("resettable") WHERE "resettable" = FALSE`); + await queryRunner.query(`CREATE TYPE "JobManager"."Job_status_enum" AS ENUM('Pending', 'In-Progress', 'Completed', 'Failed', 'Expired', 'Aborted')`); + await queryRunner.query(`CREATE TABLE "JobManager"."Job" ("id" uuid NOT NULL DEFAULT uuid_generate_v4(), "resourceId" character varying(300) NOT NULL, "version" character varying(30) NOT NULL, "type" character varying(255) NOT NULL, "description" character varying(2000) NOT NULL DEFAULT '', "parameters" jsonb NOT NULL, "creationTime" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), "updateTime" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), "status" "JobManager"."Job_status_enum" NOT NULL DEFAULT 'Pending', "percentage" smallint, "reason" character varying NOT NULL DEFAULT '', "isCleaned" boolean NOT NULL DEFAULT false, "priority" integer NOT NULL DEFAULT '1000', "expirationDate" TIMESTAMP WITH TIME ZONE, "internalId" uuid, "producerName" text, "productName" text, "productType" text, "additionalIdentifiers" text, "taskCount" integer NOT NULL DEFAULT '0', "completedTasks" integer NOT NULL DEFAULT '0', "failedTasks" integer NOT NULL DEFAULT '0', "expiredTasks" integer NOT NULL DEFAULT '0', "pendingTasks" integer NOT NULL DEFAULT '0', "inProgressTasks" integer NOT NULL DEFAULT '0', "abortedTasks" integer NOT NULL DEFAULT '0', CONSTRAINT "UQ_uniqueness_on_active_tasks" EXCLUDE ("resourceId" with =, "version" with =, "type" with =, "additionalIdentifiers" with =) WHERE (status = 'Pending' OR status = 'In-Progress'), CONSTRAINT "PK_job_id" PRIMARY KEY ("id"))`); + await queryRunner.query(`CREATE INDEX "jobTypeIndex" ON "JobManager"."Job" ("type") `); + await queryRunner.query(`CREATE INDEX "jobStatusIndex" ON "JobManager"."Job" ("status") `); + await queryRunner.query(`CREATE INDEX "jobCleanedIndex" ON "JobManager"."Job" ("isCleaned") `); + await queryRunner.query(`CREATE INDEX "jobPriorityIndex" ON "JobManager"."Job" ("priority") `); + await queryRunner.query(`CREATE INDEX "jobExpirationDateIndex" ON "JobManager"."Job" ("expirationDate") `); + await queryRunner.query(`CREATE INDEX "jobResourceIndex" ON "JobManager"."Job" ("resourceId", "version") `); + await queryRunner.query(`ALTER TABLE "JobManager"."Task" ADD CONSTRAINT "FK_task_job_id" FOREIGN KEY ("jobId") REFERENCES "JobManager"."Job"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`); + await queryRunner.query(`SET search_path TO "JobManager", public; + CREATE FUNCTION update_tasks_counters_insert() RETURNS trigger + SET search_path FROM CURRENT + LANGUAGE plpgsql + AS $$ + BEGIN + UPDATE "Job" + SET "taskCount" = "taskCount" + 1, + "completedTasks" = "completedTasks" + CASE WHEN NEW."status" = 'Completed' THEN 1 ELSE 0 END, + "failedTasks" = "failedTasks" + CASE WHEN NEW."status" = 'Failed' THEN 1 ELSE 0 END, + "expiredTasks" = "expiredTasks" + CASE WHEN NEW."status" = 'Expired' THEN 1 ELSE 0 END, + "pendingTasks" = "pendingTasks" + CASE WHEN NEW."status" = 'Pending' THEN 1 ELSE 0 END, + "inProgressTasks" = "inProgressTasks" + CASE WHEN NEW."status" = 'In-Progress' THEN 1 ELSE 0 END, + "abortedTasks" = "abortedTasks" + CASE WHEN NEW."status" = 'Aborted' THEN 1 ELSE 0 END + WHERE id = NEW."jobId"; + RETURN NULL; + END; + $$;`); + await queryRunner.query(`SET search_path TO "JobManager", public; + CREATE TRIGGER update_tasks_counters_insert + AFTER INSERT + ON "Task" + FOR EACH ROW + EXECUTE PROCEDURE update_tasks_counters_insert();`); + await queryRunner.query(`SET search_path TO "JobManager", public; + CREATE FUNCTION update_tasks_counters_delete() RETURNS trigger + SET search_path FROM CURRENT + LANGUAGE plpgsql + AS $$ + BEGIN + UPDATE "Job" + SET "taskCount" = "taskCount" - 1, + "completedTasks" = "completedTasks" - CASE WHEN OLD."status" = 'Completed' THEN 1 ELSE 0 END, + "failedTasks" = "failedTasks" - CASE WHEN OLD."status" = 'Failed' THEN 1 ELSE 0 END, + "expiredTasks" = "expiredTasks" - CASE WHEN OLD."status" = 'Expired' THEN 1 ELSE 0 END, + "pendingTasks" = "pendingTasks" - CASE WHEN OLD."status" = 'Pending' THEN 1 ELSE 0 END, + "inProgressTasks" = "inProgressTasks" - CASE WHEN OLD."status" = 'In-Progress' THEN 1 ELSE 0 END, + "abortedTasks" = "abortedTasks" - CASE WHEN OLD."status" = 'Aborted' THEN 1 ELSE 0 END + WHERE id = OLD."jobId"; + RETURN NULL; + END; + $$;`); + await queryRunner.query(`SET search_path TO "JobManager", public; + CREATE TRIGGER update_tasks_counters_delete + AFTER DELETE + ON "Task" + FOR EACH ROW + EXECUTE PROCEDURE update_tasks_counters_delete();`); + await queryRunner.query(`SET search_path TO "JobManager", public; + CREATE FUNCTION update_tasks_counters_update() RETURNS trigger + SET search_path FROM CURRENT + LANGUAGE plpgsql + AS $$ + BEGIN + IF NEW."status" != OLD."status" THEN + UPDATE "Job" + SET + "completedTasks" = "completedTasks" + CASE WHEN NEW."status" = 'Completed' THEN 1 WHEN OLD."status" = 'Completed' THEN -1 ELSE 0 END, + "failedTasks" = "failedTasks" + CASE WHEN NEW."status" = 'Failed' THEN 1 WHEN OLD."status" = 'Failed' THEN -1 ELSE 0 END, + "expiredTasks" = "expiredTasks" + CASE WHEN NEW."status" = 'Expired' THEN 1 WHEN OLD."status" = 'Expired' THEN -1 ELSE 0 END, + "pendingTasks" = "pendingTasks" + CASE WHEN NEW."status" = 'Pending' THEN 1 WHEN OLD."status" = 'Pending' THEN -1 ELSE 0 END, + "inProgressTasks" = "inProgressTasks" + CASE WHEN NEW."status" = 'In-Progress' THEN 1 WHEN OLD."status" = 'In-Progress' THEN -1 ELSE 0 END, + "abortedTasks" = "abortedTasks" + CASE WHEN NEW."status" = 'Aborted' THEN 1 WHEN OLD."status" = 'Aborted' THEN -1 ELSE 0 END + WHERE id = NEW."jobId"; + END IF; + RETURN NULL; + END; + $$;`); + await queryRunner.query(`SET search_path TO "JobManager", public; + CREATE TRIGGER update_tasks_counters_update + AFTER UPDATE + ON "Task" + FOR EACH ROW + WHEN (NEW."status" IS NOT NULL) + EXECUTE PROCEDURE update_tasks_counters_update();`); + await queryRunner.query(`CREATE OR REPLACE FUNCTION deleteTaskAndJobsByJobId(jobId text) RETURNS bool AS $func$ + BEGIN + delete from "JobManager"."Task" where "jobId" = jobId::uuid; + delete from "JobManager"."Job" where "id" = jobId::uuid; + RETURN true; + END + $func$ LANGUAGE plpgsql;`); + await queryRunner.query(`CREATE OR REPLACE FUNCTION deleteTaskAndJobsByJobType(jobType text) RETURNS bool AS $func$ + BEGIN + delete from "JobManager"."Task" where "jobId" in (select id from "Job" where "type" = jobType); + delete from "JobManager"."Job" where "type" = jobType; + RETURN true; + END + $func$ LANGUAGE plpgsql;`); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE "JobManager"."Task" DROP CONSTRAINT "FK_task_job_id"`); + await queryRunner.query(`DROP INDEX "JobManager"."jobResourceIndex"`); + await queryRunner.query(`DROP INDEX "JobManager"."jobExpirationDateIndex"`); + await queryRunner.query(`DROP INDEX "JobManager"."jobPriorityIndex"`); + await queryRunner.query(`DROP INDEX "JobManager"."jobCleanedIndex"`); + await queryRunner.query(`DROP INDEX "JobManager"."jobStatusIndex"`); + await queryRunner.query(`DROP INDEX "JobManager"."jobTypeIndex"`); + await queryRunner.query(`DROP TABLE "JobManager"."Job"`); + await queryRunner.query(`DROP TYPE "JobManager"."Job_status_enum"`); + await queryRunner.query(`DROP INDEX "JobManager"."taskResettableIndex"`); + await queryRunner.query(`DROP TABLE "JobManager"."Task"`); + await queryRunner.query(`DROP TYPE "JobManager"."Task_status_enum"`); + await queryRunner.query(`DROP TRIGGER "update_tasks_counters_insert" ON "JobManager"."Task"`); + await queryRunner.query('DROP FUNCTION "JobManager".update_tasks_counters_insert()'); + await queryRunner.query(`DROP TRIGGER "update_tasks_counters_delete" ON "JobManager"."Task"`); + await queryRunner.query('DROP FUNCTION "JobManager".update_tasks_counters_delete()'); + await queryRunner.query(`DROP TRIGGER "update_tasks_counters_update" ON "JobManager"."Task"`); + await queryRunner.query('DROP FUNCTION "JobManager".update_tasks_counters_update()'); + await queryRunner.query('DROP FUNCTION "JobManager".deleteTaskAndJobsByJobId(text)'); + await queryRunner.query('DROP FUNCTION "JobManager".deleteTaskAndJobsByJobType(text)'); + } + +} diff --git a/src/DAL/entity/job.ts b/src/DAL/entity/job.ts index 057a2c5..9bd3281 100644 --- a/src/DAL/entity/job.ts +++ b/src/DAL/entity/job.ts @@ -6,7 +6,7 @@ import { TaskEntity } from './task'; @Index('jobResourceIndex', ['resourceId', 'version'], { unique: false }) @Exclusion( 'UQ_uniqueness_on_active_tasks', - 'EXCLUDE ("resourceId" with =, "version" with =, "type" with =, "additionalIdentifiers" with =) WHERE (status = \'Pending\' OR status = \'In-Progress\')' + `("resourceId" with =, "version" with =, "type" with =, "additionalIdentifiers" with =) WHERE (status = 'Pending' OR status = 'In-Progress')` ) export class JobEntity { @PrimaryColumn({ type: 'uuid', primaryKeyConstraintName: 'PK_job_id' }) diff --git a/src/DAL/entity/task.ts b/src/DAL/entity/task.ts index 69de221..4e36dca 100644 --- a/src/DAL/entity/task.ts +++ b/src/DAL/entity/task.ts @@ -3,7 +3,7 @@ import { OperationStatus } from '../../common/dataModels/enums'; import { JobEntity } from './job'; @Entity('Task') -@Exclusion('UQ_uniqueness_on_job_and_type', 'EXCLUDE ("type" with =, "jobId" with =) WHERE ("block_duplication" = true)') +@Exclusion('UQ_uniqueness_on_job_and_type', '("type" with =, "jobId" with =) WHERE ("block_duplication" = true)') export class TaskEntity { @PrimaryColumn({ type: 'uuid', primaryKeyConstraintName: 'PK_task_id' }) @Generated('uuid') diff --git a/src/DAL/repositories/jobRepository.ts b/src/DAL/repositories/jobRepository.ts index 92b1351..26a37f7 100644 --- a/src/DAL/repositories/jobRepository.ts +++ b/src/DAL/repositories/jobRepository.ts @@ -1,4 +1,4 @@ -import { FindManyOptions, LessThan, Brackets, Between, LessThanOrEqual, MoreThanOrEqual, DataSource, Repository } from 'typeorm'; +import { FindManyOptions, LessThan, Brackets, Between, LessThanOrEqual, MoreThanOrEqual, DataSource } from 'typeorm'; import { FactoryFunction } from 'tsyringe'; import { Logger } from '@map-colonies/js-logger'; import { ConflictError, NotFoundError } from '@map-colonies/error-types'; @@ -19,7 +19,7 @@ import { SERVICES } from '../../common/constants'; // eslint-disable-next-line @typescript-eslint/explicit-function-return-type const getJobRepository = (db: DataSource, config: IConfig, logger: Logger, jobConvertor: JobModelConvertor) => { - const schemaConf = config.get('typeorm').schema; + const schemaConf = config.get('typeOrm').schema; const schema = schemaConf == undefined || schemaConf == '' ? '' : `"${schemaConf}".`; return db.getRepository(JobEntity).extend({ async findJobs(req: IFindJobsRequest): Promise { diff --git a/src/DAL/repositories/taskRepository.ts b/src/DAL/repositories/taskRepository.ts index 12e6b67..0b9b7c7 100644 --- a/src/DAL/repositories/taskRepository.ts +++ b/src/DAL/repositories/taskRepository.ts @@ -26,7 +26,7 @@ declare type SqlRawResponse = [unknown[], number]; // eslint-disable-next-line @typescript-eslint/explicit-function-return-type const getJobRepository = (db: DataSource, config: IConfig, logger: Logger, taskConvertor: TaskModelConvertor) => { - const schemaConf = config.get('typeorm').schema; + const schemaConf = config.get('typeOrm').schema; const schema = schemaConf == undefined || schemaConf == '' ? '' : `"${schemaConf}".`; return db.getRepository(TaskEntity).extend({ async getTasks(req: IAllTasksParams): Promise { @@ -210,7 +210,7 @@ const getJobRepository = (db: DataSource, config: IConfig, logger: Logger, taskC .where( `"jobId" IN ( SELECT id - FROM "${this.dbConfig.schema as string}"."Job" as jb + FROM ${schema}"Job" as jb WHERE jb.status = :status)`, { status: OperationStatus.EXPIRED } ) diff --git a/src/DAL/repositories/transactionActions.ts b/src/DAL/repositories/transactionActions.ts index 00cd048..16d8a04 100644 --- a/src/DAL/repositories/transactionActions.ts +++ b/src/DAL/repositories/transactionActions.ts @@ -1,13 +1,17 @@ -import { singleton } from 'tsyringe'; +import { inject, singleton } from 'tsyringe'; import { DataSource } from 'typeorm'; import { BadRequestError } from '@map-colonies/error-types'; import { OperationStatus } from '../../common/dataModels/enums'; -import { JobRepository } from './jobRepository'; -import { TaskRepository } from './taskRepository'; +import { JobRepository, JOB_CUSTOM_REPOSITORY_SYMBOL } from './jobRepository'; +import { TaskRepository, TASK_CUSTOM_REPOSITORY_SYMBOL } from './taskRepository'; @singleton() export class TransactionActions { - public constructor(private readonly db: DataSource, private readonly jobRepo: JobRepository, private readonly taskRepo: TaskRepository) {} + public constructor( + private readonly db: DataSource, + @inject(JOB_CUSTOM_REPOSITORY_SYMBOL) private readonly jobRepo: JobRepository, + @inject(TASK_CUSTOM_REPOSITORY_SYMBOL) private readonly taskRepo: TaskRepository + ) {} public async resetJob(jobId: string, expirationDate?: Date): Promise { await this.db.transaction(async (manager) => { diff --git a/src/app.ts b/src/app.ts index 9a0fa84..87fb62a 100644 --- a/src/app.ts +++ b/src/app.ts @@ -2,8 +2,8 @@ import { Application } from 'express'; import { registerExternalValues, RegisterOptions } from './containerConfig'; import { ServerBuilder } from './serverBuilder'; -function getApp(registerOptions?: RegisterOptions): Application { - const container = registerExternalValues(registerOptions); +async function getApp(registerOptions?: RegisterOptions): Promise { + const container = await registerExternalValues(registerOptions); const app = container.resolve(ServerBuilder).build(); return app; } diff --git a/src/containerConfig.ts b/src/containerConfig.ts index 53dab70..44083dc 100644 --- a/src/containerConfig.ts +++ b/src/containerConfig.ts @@ -13,7 +13,7 @@ import { taskManagerRouterFactory, TASK_MANAGER_ROUTER_SYMBOL } from './taskMana import { initDataSource } from './DAL/connectionBuilder'; import { IDbConfig } from './common/interfaces'; import { jobRepositoryFactory, JOB_CUSTOM_REPOSITORY_SYMBOL } from './DAL/repositories/jobRepository'; -import { taskRepositoryFactory } from './DAL/repositories/taskRepository'; +import { taskRepositoryFactory, TASK_CUSTOM_REPOSITORY_SYMBOL } from './DAL/repositories/taskRepository'; export interface RegisterOptions { override?: InjectionObject[]; @@ -52,7 +52,7 @@ export const registerExternalValues = async (options?: RegisterOptions): Promise }, { token: DataSource, provider: { useValue: db } }, { token: JOB_CUSTOM_REPOSITORY_SYMBOL, provider: { useFactory: jobRepositoryFactory } }, - { token: TASK_MANAGER_ROUTER_SYMBOL, provider: { useFactory: taskRepositoryFactory } }, + { token: TASK_CUSTOM_REPOSITORY_SYMBOL, provider: { useFactory: taskRepositoryFactory } }, ]; return registerDependencies(dependencies, options?.override, options?.useChild); diff --git a/src/index.ts b/src/index.ts index 232806c..3726e53 100644 --- a/src/index.ts +++ b/src/index.ts @@ -12,13 +12,13 @@ import { getApp } from './app'; const port: number = config.get('server.port') || DEFAULT_SERVER_PORT; -const app = getApp(); +void getApp().then((app) => { + const logger = container.resolve(SERVICES.LOGGER); + const stubHealthcheck = async (): Promise => Promise.resolve(); + // eslint-disable-next-line @typescript-eslint/naming-convention + const server = createTerminus(createServer(app), { healthChecks: { '/liveness': stubHealthcheck, onSignal: container.resolve('onSignal') } }); -const logger = container.resolve(SERVICES.LOGGER); -const stubHealthcheck = async (): Promise => Promise.resolve(); -// eslint-disable-next-line @typescript-eslint/naming-convention -const server = createTerminus(createServer(app), { healthChecks: { '/liveness': stubHealthcheck, onSignal: container.resolve('onSignal') } }); - -server.listen(port, () => { - logger.info(`app started on port ${port}`); + server.listen(port, () => { + logger.info(`app started on port ${port}`); + }); }); diff --git a/src/jobs/models/jobManager.ts b/src/jobs/models/jobManager.ts index 362b7a5..4d030ea 100644 --- a/src/jobs/models/jobManager.ts +++ b/src/jobs/models/jobManager.ts @@ -2,8 +2,6 @@ import { Logger } from '@map-colonies/js-logger'; import { NotFoundError } from '@map-colonies/error-types'; import { inject, injectable } from 'tsyringe'; import { SERVICES } from '../../common/constants'; - -import { ConnectionManager } from '../../DAL/connectionBuilder'; import { FindJobsResponse, ICreateJobBody, @@ -16,34 +14,30 @@ import { IIsResettableResponse, IResetJobRequest, } from '../../common/dataModels/jobs'; -import { JobRepository } from '../../DAL/repositories/jobRepository'; +import { JobRepository, JOB_CUSTOM_REPOSITORY_SYMBOL } from '../../DAL/repositories/jobRepository'; import { TransactionActions } from '../../DAL/repositories/transactionActions'; @injectable() export class JobManager { - private repository?: JobRepository; public constructor( @inject(SERVICES.LOGGER) private readonly logger: Logger, - private readonly connectionManager: ConnectionManager, - private readonly transactionManager: TransactionActions + private readonly transactionManager: TransactionActions, + @inject(JOB_CUSTOM_REPOSITORY_SYMBOL) private readonly repository: JobRepository ) {} public async findJobs(req: IFindJobsRequest): Promise { - const repo = await this.getRepository(); - const res = await repo.findJobs(req); + const res = await this.repository.findJobs(req); return res; } public async createJob(req: ICreateJobBody): Promise { this.logger.debug(req, 'Create-job parameters'); - const repo = await this.getRepository(); - const res = await repo.createJob(req); + const res = await this.repository.createJob(req); return res; } public async getJob(req: IJobsParams, query: IJobsQuery): Promise { - const repo = await this.getRepository(); - const res = await repo.getJob(req.jobId, query.shouldReturnTasks); + const res = await this.repository.getJob(req.jobId, query.shouldReturnTasks); if (res === undefined) { throw new NotFoundError('Job not found'); } @@ -52,21 +46,18 @@ export class JobManager { public async updateJob(req: IUpdateJobRequest): Promise { this.logger.debug(req, 'Update-job parameters'); - const repo = await this.getRepository(); - await repo.updateJob(req); + await this.repository.updateJob(req); } public async deleteJob(req: IJobsParams): Promise { this.logger.info({ jobId: req.jobId, msg: 'deleting job' }); - const repo = await this.getRepository(); - const res = await repo.deleteJob(req.jobId); + const res = await this.repository.deleteJob(req.jobId); return res; } public async isResettable(req: IJobsParams): Promise { const jobId = req.jobId; - const repo = await this.getRepository(); - const isResettable = await repo.isJobResettable(jobId); + const isResettable = await this.repository.isJobResettable(jobId); return { jobId, isResettable }; } @@ -76,14 +67,4 @@ export class JobManager { this.logger.info(`reset job ${req.jobId}, newExpirationDate ${(newExpirationDate ?? 'undefiend') as string}`); await this.transactionManager.resetJob(jobId, newExpirationDate); } - - private async getRepository(): Promise { - if (!this.repository) { - if (!this.connectionManager.isConnected()) { - await this.connectionManager.init(); - } - this.repository = this.connectionManager.getJobRepository(); - } - return this.repository; - } } diff --git a/src/jobs/models/taskManager.ts b/src/jobs/models/taskManager.ts index 8e20a84..0cce891 100644 --- a/src/jobs/models/taskManager.ts +++ b/src/jobs/models/taskManager.ts @@ -2,9 +2,7 @@ import { Logger } from '@map-colonies/js-logger'; import { NotFoundError } from '@map-colonies/error-types'; import { inject, injectable } from 'tsyringe'; import { SERVICES } from '../../common/constants'; - -import { ConnectionManager } from '../../DAL/connectionBuilder'; -import { TaskRepository } from '../../DAL/repositories/taskRepository'; +import { TaskRepository, TASK_CUSTOM_REPOSITORY_SYMBOL } from '../../DAL/repositories/taskRepository'; import { CreateTasksRequest, CreateTasksResponse, @@ -21,30 +19,25 @@ import { JobManager } from './jobManager'; @injectable() export class TaskManager { - private repository?: TaskRepository; - public constructor( @inject(SERVICES.LOGGER) private readonly logger: Logger, - private readonly connectionManager: ConnectionManager, - private readonly jobManager: JobManager + private readonly jobManager: JobManager, + @inject(TASK_CUSTOM_REPOSITORY_SYMBOL) private readonly repository: TaskRepository ) {} public async getAllTasks(req: IAllTasksParams): Promise { - const repo = await this.getRepository(); - const res = await repo.getTasks(req); + const res = await this.repository.getTasks(req); return res; } public async createTask(req: CreateTasksRequest): Promise { this.logger.debug(req, 'Create-task request parameters'); - const repo = await this.getRepository(); - const res = await repo.createTask(req); + const res = await this.repository.createTask(req); return res; } public async getTask(req: ISpecificTaskParams): Promise { - const repo = await this.getRepository(); - const res = await repo.getTask(req); + const res = await this.repository.getTask(req); if (res === undefined) { throw new NotFoundError('Task not found'); } @@ -52,8 +45,7 @@ export class TaskManager { } public async findTasks(req: IFindTasksRequest): Promise { - const repo = await this.getRepository(); - const res = await repo.findTasks(req); + const res = await this.repository.findTasks(req); if (res.length === 0) { throw new NotFoundError('Tasks not found'); } @@ -62,25 +54,22 @@ export class TaskManager { public async updateTask(req: IUpdateTaskRequest): Promise { this.logger.debug(req, 'Update-Task request parameters'); - const repo = await this.getRepository(); - await repo.updateTask(req); + await this.repository.updateTask(req); } public async deleteTask(req: ISpecificTaskParams): Promise { this.logger.info(`deleting task ${req.taskId} from job ${req.jobId}`); - const repo = await this.getRepository(); - const res = await repo.deleteTask(req); + const res = await this.repository.deleteTask(req); return res; } public async getTaskStatus(req: IAllTasksParams): Promise { const { version: resourceVersion, resourceId } = await this.jobManager.getJob(req, { shouldReturnTasks: false }); - const repo = await this.getRepository(); this.logger.info(`Getting tasks statuses for jobId ${req.jobId}`); - const completedTasksCount = await repo.getTasksCountByStatus(OperationStatus.COMPLETED, req.jobId); - const failedTasksCount = await repo.getTasksCountByStatus(OperationStatus.FAILED, req.jobId); - const allTasksCompleted = await repo.checkIfAllCompleted(req.jobId); + const completedTasksCount = await this.repository.getTasksCountByStatus(OperationStatus.COMPLETED, req.jobId); + const failedTasksCount = await this.repository.getTasksCountByStatus(OperationStatus.FAILED, req.jobId); + const allTasksCompleted = await this.repository.checkIfAllCompleted(req.jobId); const tasksStatus: IGetTasksStatus = { allTasksCompleted, @@ -92,14 +81,4 @@ export class TaskManager { return tasksStatus; } - - private async getRepository(): Promise { - if (!this.repository) { - if (!this.connectionManager.isConnected()) { - await this.connectionManager.init(); - } - this.repository = this.connectionManager.getTaskRepository(); - } - return this.repository; - } } diff --git a/src/taskManagement/models/taskManagementManger.ts b/src/taskManagement/models/taskManagementManger.ts index c62e6d9..97e0294 100644 --- a/src/taskManagement/models/taskManagementManger.ts +++ b/src/taskManagement/models/taskManagementManger.ts @@ -2,24 +2,23 @@ import { Logger } from '@map-colonies/js-logger'; import { NotFoundError } from '@map-colonies/error-types'; import { inject, injectable } from 'tsyringe'; import { SERVICES } from '../../common/constants'; -import { ConnectionManager } from '../../DAL/connectionBuilder'; -import { TaskRepository } from '../../DAL/repositories/taskRepository'; +import { TaskRepository, TASK_CUSTOM_REPOSITORY_SYMBOL } from '../../DAL/repositories/taskRepository'; import { IFindInactiveTasksRequest, IGetTaskResponse, IRetrieveAndStartRequest } from '../../common/dataModels/tasks'; -import { JobRepository } from '../../DAL/repositories/jobRepository'; +import { JobRepository, JOB_CUSTOM_REPOSITORY_SYMBOL } from '../../DAL/repositories/jobRepository'; import { OperationStatus } from '../../common/dataModels/enums'; import { IJobsParams } from '../../common/dataModels/jobs'; @injectable() export class TaskManagementManager { - private jobRepository?: JobRepository; - private taskRepository?: TaskRepository; - - public constructor(@inject(SERVICES.LOGGER) private readonly logger: Logger, private readonly connectionManager: ConnectionManager) {} + public constructor( + @inject(SERVICES.LOGGER) private readonly logger: Logger, + @inject(JOB_CUSTOM_REPOSITORY_SYMBOL) private readonly jobRepository: JobRepository, + @inject(TASK_CUSTOM_REPOSITORY_SYMBOL) private readonly taskRepository: TaskRepository + ) {} public async retrieveAndStart(req: IRetrieveAndStartRequest): Promise { - const repo = await this.getTaskRepository(); this.logger.debug(`try to start task by retrieving and updating to "In-Progress" for job type: ${req.jobType}, task type: ${req.taskType}`); - const res = await repo.retrieveAndUpdate(req.jobType, req.taskType); + const res = await this.taskRepository.retrieveAndUpdate(req.jobType, req.taskType); if (res === undefined) { this.logger.debug(`Pending task was not found for job type: ${req.jobType}, task type: ${req.taskType}`); throw new NotFoundError('Pending task was not found'); @@ -29,51 +28,25 @@ export class TaskManagementManager { } public async releaseInactive(tasks: string[]): Promise { - const repo = await this.getTaskRepository(); this.logger.info(`trying to release dead tasks: ${tasks.join(',')}`); - const releasedTasks = await repo.releaseInactiveTask(tasks); + const releasedTasks = await this.taskRepository.releaseInactiveTask(tasks); this.logger.info(`released dead tasks: ${releasedTasks.join(',')}`); return releasedTasks; } public async getInactiveTasks(req: IFindInactiveTasksRequest): Promise { - const repo = await this.getTaskRepository(); this.logger.info(`finding tasks inactive for longer then ${req.inactiveTimeSec} seconds, with types: ${req.types ? req.types.join() : 'any'}`); - const res = await repo.findInactiveTasks(req); + const res = await this.taskRepository.findInactiveTasks(req); return res; } public async updateExpiredJobsAndTasks(): Promise { - const jobsRepo = await this.getJobRepository(); - await jobsRepo.updateExpiredJobs(); - const taskRepo = await this.getTaskRepository(); - await taskRepo.updateTasksOfExpiredJobs(); + await this.jobRepository.updateExpiredJobs(); + await this.taskRepository.updateTasksOfExpiredJobs(); } public async abortJobAndTasks(req: IJobsParams): Promise { - const jobRepo = await this.getJobRepository(); - await jobRepo.updateJob({ jobId: req.jobId, status: OperationStatus.ABORTED }); - const taskRepo = await this.getTaskRepository(); - await taskRepo.abortJobTasks(req.jobId); - } - - private async getTaskRepository(): Promise { - if (!this.taskRepository) { - if (!this.connectionManager.isConnected()) { - await this.connectionManager.init(); - } - this.taskRepository = this.connectionManager.getTaskRepository(); - } - return this.taskRepository; - } - - private async getJobRepository(): Promise { - if (!this.jobRepository) { - if (!this.connectionManager.isConnected()) { - await this.connectionManager.init(); - } - this.jobRepository = this.connectionManager.getJobRepository(); - } - return this.jobRepository; + await this.jobRepository.updateJob({ jobId: req.jobId, status: OperationStatus.ABORTED }); + await this.taskRepository.abortJobTasks(req.jobId); } }