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/dataSource.ts b/dataSource.ts new file mode 100644 index 0000000..c787470 --- /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'], +}); 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/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/connectionBuilder.ts b/src/DAL/connectionBuilder.ts new file mode 100644 index 0000000..59fa504 --- /dev/null +++ b/src/DAL/connectionBuilder.ts @@ -0,0 +1,18 @@ +import { readFileSync } from 'fs'; +import { DataSourceOptions, DataSource } from 'typeorm'; +import { IDbConfig } from '../common/interfaces'; + +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/connectionManager.ts b/src/DAL/connectionManager.ts deleted file mode 100644 index 2a7684d..0000000 --- a/src/DAL/connectionManager.ts +++ /dev/null @@ -1,72 +0,0 @@ -import { readFileSync } from 'fs'; -import { createConnection, Connection, ObjectType, QueryRunner, ConnectionOptions } 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'; - -@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(this.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 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()) { - 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); - } - } -} diff --git a/src/DAL/entity/job.ts b/src/DAL/entity/job.ts index 8d3bd41..9bd3281 100644 --- a/src/DAL/entity/job.ts +++ b/src/DAL/entity/job.ts @@ -1,15 +1,15 @@ -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', + `("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 +19,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 +39,7 @@ export class JobEntity { }) public updateTime: Date; + @Index('jobStatusIndex') @Column({ type: 'enum', enum: OperationStatus, default: OperationStatus.PENDING, nullable: false }) public status: OperationStatus; @@ -47,12 +49,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 +73,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..4e36dca 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', '("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 deleted file mode 100644 index 272dae7..0000000 --- a/src/DAL/repositories/generalRepository.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { container } from 'tsyringe'; -import { Repository } from 'typeorm'; -import { SERVICES } from '../../common/constants'; -import { IConfig, IDbConfig } from '../../common/interfaces'; - -export class GeneralRepository extends Repository { - protected readonly dbConfig: IDbConfig; - private readonly config: IConfig; - - public constructor() { - super(); - this.config = container.resolve(SERVICES.CONFIG); - 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); - } -} diff --git a/src/DAL/repositories/jobRepository.ts b/src/DAL/repositories/jobRepository.ts index 19bd573..26a37f7 100644 --- a/src/DAL/repositories/jobRepository.ts +++ b/src/DAL/repositories/jobRepository.ts @@ -1,9 +1,8 @@ -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 { 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,174 +14,182 @@ import { } from '../../common/dataModels/jobs'; import { JobModelConvertor } from '../convertors/jobModelConverter'; import { OperationStatus } from '../../common/dataModels/enums'; -import { GeneralRepository } from './generalRepository'; - -@EntityRepository(JobEntity) -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); - } +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.appLogger.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' }); - 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.appLogger.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.appLogger.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.findOne(id); - } else { - entity = await this.findOne(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.appLogger.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 }); - 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' }); - } + 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.count({ 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.appLogger.error({ id: id, msg: message }); - throw new NotFoundError(message); - } - try { - await this.delete(id); - this.appLogger.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' }); - throw new DBConstraintError(`job ${id} have tasks`); - } else { - this.appLogger.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 c02ef14..0b9b7c7 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, 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,103 +20,94 @@ import { IUpdateTaskRequest, } from '../../common/dataModels/tasks'; import { OperationStatus } from '../../common/dataModels/enums'; -import { GeneralRepository } from './generalRepository'; +import { IConfig, IDbConfig } from '../../common/interfaces'; declare type SqlRawResponse = [unknown[], number]; -@EntityRepository(TaskEntity) -export class TaskRepository extends GeneralRepository { - private readonly appLogger: Logger; //don't override internal repository logger. - private readonly taskConvertor: TaskModelConvertor; +// 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() { - super(); - //direct injection don't work here due to being initialized by typeOrm - this.appLogger = container.resolve(SERVICES.LOGGER); - this.taskConvertor = container.resolve(TaskModelConvertor); - } + 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.find(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 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' }); - 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.appLogger.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.appLogger.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' }); - return res; - } - } + }, - public async getTask(req: ISpecificTaskParams): Promise { - const entity = await this.findOne({ 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.count({ 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.appLogger.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 }); - 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' }); - } + 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.appLogger.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' }); - } + 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 @@ -130,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 + FROM ${schema}"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.appLogger.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); - 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 39b6d9d..16d8a04 100644 --- a/src/DAL/repositories/transactionActions.ts +++ b/src/DAL/repositories/transactionActions.ts @@ -1,61 +1,28 @@ -import { singleton } from 'tsyringe'; -import { ObjectType, QueryRunner } from 'typeorm'; +import { inject, singleton } from 'tsyringe'; +import { DataSource } 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 { JobRepository, JOB_CUSTOM_REPOSITORY_SYMBOL } from './jobRepository'; +import { TaskRepository, TASK_CUSTOM_REPOSITORY_SYMBOL } from './taskRepository'; @singleton() export class TransactionActions { - public constructor(private readonly connectionManager: ConnectionManager) {} + 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 { - 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); + 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.`); } }); } - - 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.getCustomRepository(repository); - } } 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 b69a469..44083dc 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, TASK_CUSTOM_REPOSITORY_SYMBOL } 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_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 0b68be1..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/connectionManager'; 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 d117262..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/connectionManager'; -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 df51f49..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/connectionManager'; -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); } }