feat: add question transition constraints and command provider

This commit is contained in:
2026-08-03 17:12:55 +03:30
parent a3b7c70999
commit a3400f66c5
9 changed files with 491 additions and 97 deletions
+2 -1
View File
@@ -39,7 +39,8 @@
"startAlreadyExists": "Start already exists.", "startAlreadyExists": "Start already exists.",
"endAlreadyExists": "End already exists.", "endAlreadyExists": "End already exists.",
"defaultAlreadyExists": "Default already exists.", "defaultAlreadyExists": "Default already exists.",
"duplicateTransition": "Duplicate Transition!" "duplicateTransition": "Duplicate Transition!",
"numberRangeOverlap": "Condition Value's min-max has some overlaps with other transitions."
} }
}, },
"messages": { "messages": {
+2 -1
View File
@@ -39,7 +39,8 @@
"startAlreadyExists": "حالت شروع وجود دارد.", "startAlreadyExists": "حالت شروع وجود دارد.",
"endAlreadyExists": "حالت پایان وجود دارد.", "endAlreadyExists": "حالت پایان وجود دارد.",
"defaultAlreadyExists": "حالت پیشفرض وجود دارد.", "defaultAlreadyExists": "حالت پیشفرض وجود دارد.",
"duplicateTransition": "مسیر تکراری است!" "duplicateTransition": "مسیر تکراری است!",
"numberRangeOverlap": "مقدار بازه شرط با بازه مسیرهای دیگر تداخل دارد."
} }
}, },
"messages": { "messages": {
+149 -52
View File
@@ -1,82 +1,179 @@
import { MigrationInterface, QueryRunner } from "typeorm"; import { MigrationInterface, QueryRunner } from 'typeorm';
export class InitialSchema1783497761523 implements MigrationInterface { export class InitialSchema1783497761523 implements MigrationInterface {
name = 'InitialSchema1783497761523' name = 'InitialSchema1783497761523';
public async up(queryRunner: QueryRunner): Promise<void> { public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`CREATE TYPE "public"."user_role_enum" AS ENUM('superuser', 'admin', 'user')`); await queryRunner.query(
await queryRunner.query(`CREATE TABLE "user" ("id" SERIAL NOT NULL, "firstName" character varying(96), "lastName" character varying(96), "phone" character varying(11) NOT NULL, "password" character varying(96), "role" "public"."user_role_enum" NOT NULL DEFAULT 'user', "createdAt" TIMESTAMP NOT NULL DEFAULT now(), "updatedAt" TIMESTAMP NOT NULL DEFAULT now(), CONSTRAINT "UQ_8e1f623798118e629b46a9e6299" UNIQUE ("phone"), CONSTRAINT "PK_cace4a159ff9f2512dd42373760" PRIMARY KEY ("id"))`); `CREATE TYPE "public"."user_role_enum" AS ENUM('superuser', 'admin', 'user')`,
await queryRunner.query(`CREATE UNIQUE INDEX "IDX_8e1f623798118e629b46a9e629" ON "user" ("phone") `); );
await queryRunner.query(`CREATE TABLE "choice" ("id" SERIAL NOT NULL, "text" character varying(255) NOT NULL, "value" integer NOT NULL, "order" integer NOT NULL, "score" integer NOT NULL, "questionId" integer, CONSTRAINT "PK_5bf2e5939332f46711278a87fcd" PRIMARY KEY ("id"))`); await queryRunner.query(
await queryRunner.query(`CREATE UNIQUE INDEX "UNQ_QUESTION_ORDER_IDX" ON "choice" ("questionId", "order") `); `CREATE TABLE "user" ("id" SERIAL NOT NULL, "firstName" character varying(96), "lastName" character varying(96), "phone" character varying(11) NOT NULL, "password" character varying(96), "role" "public"."user_role_enum" NOT NULL DEFAULT 'user', "createdAt" TIMESTAMP NOT NULL DEFAULT now(), "updatedAt" TIMESTAMP NOT NULL DEFAULT now(), CONSTRAINT "UQ_8e1f623798118e629b46a9e6299" UNIQUE ("phone"), CONSTRAINT "PK_cace4a159ff9f2512dd42373760" PRIMARY KEY ("id"))`,
await queryRunner.query(`CREATE TABLE "question_range_rule" ("id" SERIAL NOT NULL, "min" integer NOT NULL, "max" integer NOT NULL, "score" integer NOT NULL, "questionId" integer, CONSTRAINT "PK_801c56fb79cb878310f4706616e" PRIMARY KEY ("id"))`); );
await queryRunner.query(`CREATE TYPE "public"."question_type_enum" AS ENUM('multi-choice', 'numeric', 'text')`); await queryRunner.query(
await queryRunner.query(`CREATE TABLE "question" ("id" SERIAL NOT NULL, "type" "public"."question_type_enum" NOT NULL DEFAULT 'multi-choice', "content" text NOT NULL, "order" integer NOT NULL, "weight" integer NOT NULL DEFAULT '1', "required" boolean NOT NULL DEFAULT true, "metadata" jsonb, "createdAt" TIMESTAMP NOT NULL DEFAULT now(), "testId" integer, CONSTRAINT "PK_21e5786aa0ea704ae185a79b2d5" PRIMARY KEY ("id"))`); `CREATE UNIQUE INDEX "IDX_8e1f623798118e629b46a9e629" ON "user" ("phone") `,
await queryRunner.query(`CREATE INDEX "IDX_275b917b91392b9a2d6ab7ccee" ON "question" ("order") `); );
await queryRunner.query(`CREATE UNIQUE INDEX "UNQ_TEST_ORDER_IDX" ON "question" ("testId", "order") `); await queryRunner.query(
await queryRunner.query(`CREATE TABLE "result_range" ("id" SERIAL NOT NULL, "minScore" integer NOT NULL, "maxScore" integer NOT NULL, "title" character varying(255) NOT NULL, "description" text NOT NULL, "testId" integer, CONSTRAINT "PK_47825aaf8cbc6dee89983104b2c" PRIMARY KEY ("id"))`); `CREATE TABLE "choice" ("id" SERIAL NOT NULL, "text" character varying(255) NOT NULL, "value" integer NOT NULL, "order" integer NOT NULL, "score" integer NOT NULL, "questionId" integer, CONSTRAINT "PK_5bf2e5939332f46711278a87fcd" PRIMARY KEY ("id"))`,
await queryRunner.query(`CREATE TYPE "public"."test_accesstype_enum" AS ENUM('free', 'login', 'paid')`); );
await queryRunner.query(
`CREATE UNIQUE INDEX "UNQ_QUESTION_ORDER_IDX" ON "choice" ("questionId", "order") `,
);
await queryRunner.query(
`CREATE TABLE "question_range_rule" ("id" SERIAL NOT NULL, "min" integer NOT NULL, "max" integer NOT NULL, "score" integer NOT NULL, "questionId" integer, CONSTRAINT "PK_801c56fb79cb878310f4706616e" PRIMARY KEY ("id"))`,
);
await queryRunner.query(
`CREATE TYPE "public"."question_type_enum" AS ENUM('multi-choice', 'numeric', 'text')`,
);
await queryRunner.query(
`CREATE TABLE "question" ("id" SERIAL NOT NULL, "type" "public"."question_type_enum" NOT NULL DEFAULT 'multi-choice', "content" text NOT NULL, "order" integer NOT NULL, "weight" integer NOT NULL DEFAULT '1', "required" boolean NOT NULL DEFAULT true, "metadata" jsonb, "createdAt" TIMESTAMP NOT NULL DEFAULT now(), "testId" integer, CONSTRAINT "PK_21e5786aa0ea704ae185a79b2d5" PRIMARY KEY ("id"))`,
);
await queryRunner.query(
`CREATE INDEX "IDX_275b917b91392b9a2d6ab7ccee" ON "question" ("order") `,
);
await queryRunner.query(
`CREATE UNIQUE INDEX "UNQ_TEST_ORDER_IDX" ON "question" ("testId", "order") `,
);
await queryRunner.query(
`CREATE TABLE "result_range" ("id" SERIAL NOT NULL, "minScore" integer NOT NULL, "maxScore" integer NOT NULL, "title" character varying(255) NOT NULL, "description" text NOT NULL, "testId" integer, CONSTRAINT "PK_47825aaf8cbc6dee89983104b2c" PRIMARY KEY ("id"))`,
);
await queryRunner.query(
`CREATE TYPE "public"."test_accesstype_enum" AS ENUM('free', 'login', 'paid')`,
);
await queryRunner.query(`CREATE TABLE "test" ("id" SERIAL NOT NULL, "title" character varying(255) NOT NULL, "description" text NOT NULL, "accessType" "public"."test_accesstype_enum" NOT NULL DEFAULT 'login', "price" bigint, "isActive" boolean NOT NULL DEFAULT false, "createdAt" TIMESTAMP NOT NULL DEFAULT now(), CONSTRAINT "UQ_2be572eb4330bf8387466d8e052" UNIQUE ("title"), CONSTRAINT "CHK_a1e32908ffafc3f21493debfdf" CHECK ( await queryRunner.query(`CREATE TABLE "test" ("id" SERIAL NOT NULL, "title" character varying(255) NOT NULL, "description" text NOT NULL, "accessType" "public"."test_accesstype_enum" NOT NULL DEFAULT 'login', "price" bigint, "isActive" boolean NOT NULL DEFAULT false, "createdAt" TIMESTAMP NOT NULL DEFAULT now(), CONSTRAINT "UQ_2be572eb4330bf8387466d8e052" UNIQUE ("title"), CONSTRAINT "CHK_a1e32908ffafc3f21493debfdf" CHECK (
("price" IS NULL AND "accessType" <> 'paid') ("price" IS NULL AND "accessType" <> 'paid')
OR OR
("price" IS NOT NULL AND "accessType" = 'paid') ("price" IS NOT NULL AND "accessType" = 'paid')
), CONSTRAINT "PK_5417af0062cf987495b611b59c7" PRIMARY KEY ("id"))`); ), CONSTRAINT "PK_5417af0062cf987495b611b59c7" PRIMARY KEY ("id"))`);
await queryRunner.query(`CREATE TABLE "submission_answer" ("id" SERIAL NOT NULL, "numericValue" integer, "textValue" character varying(255), "submissionId" integer, "questionId" integer, "choiceId" integer, CONSTRAINT "PK_1692baefb1f7b14f4a2683c66f4" PRIMARY KEY ("id"))`); await queryRunner.query(
await queryRunner.query(`CREATE UNIQUE INDEX "IDX_ea78aac2e76e03f1c2fc720f1d" ON "submission_answer" ("submissionId", "questionId") `); `CREATE TABLE "submission_answer" ("id" SERIAL NOT NULL, "numericValue" integer, "textValue" character varying(255), "submissionId" integer, "questionId" integer, "choiceId" integer, CONSTRAINT "PK_1692baefb1f7b14f4a2683c66f4" PRIMARY KEY ("id"))`,
await queryRunner.query(`CREATE TABLE "submission_result" ("id" SERIAL NOT NULL, "score" integer NOT NULL, "probability" integer NOT NULL, "createdAt" TIMESTAMP NOT NULL DEFAULT now(), "submissionId" integer, "resultRangeId" integer, CONSTRAINT "REL_3ff0186775dd04fd7a209b6dca" UNIQUE ("submissionId"), CONSTRAINT "CHK_f4b6a23dcc9301306827573971" CHECK ("probability" >= 0 AND "probability" <= 100), CONSTRAINT "PK_965bd40b9ba83db22c13f56ce81" PRIMARY KEY ("id"))`); );
await queryRunner.query(`CREATE TYPE "public"."submission_status_enum" AS ENUM('in-progress', 'completed')`); await queryRunner.query(
await queryRunner.query(`CREATE TABLE "submission" ("id" SERIAL NOT NULL, "status" "public"."submission_status_enum" NOT NULL DEFAULT 'in-progress', "createdAt" TIMESTAMP NOT NULL DEFAULT now(), "userId" integer, "testId" integer, CONSTRAINT "PK_7faa571d0e4a7076e85890c9bd0" PRIMARY KEY ("id"))`); `CREATE UNIQUE INDEX "IDX_ea78aac2e76e03f1c2fc720f1d" ON "submission_answer" ("submissionId", "questionId") `,
await queryRunner.query(`CREATE TABLE "otp" ("id" SERIAL NOT NULL, "phone" character varying(11) NOT NULL, "code" character varying NOT NULL, "createdAt" TIMESTAMP NOT NULL DEFAULT now(), "expiresAt" TIMESTAMP WITH TIME ZONE NOT NULL, "used" boolean NOT NULL DEFAULT false, CONSTRAINT "PK_32556d9d7b22031d7d0e1fd6723" PRIMARY KEY ("id"))`); );
await queryRunner.query(`CREATE INDEX "IDX_27c8907d477176af7ed2902f2a" ON "otp" ("phone") `); await queryRunner.query(
await queryRunner.query(`CREATE INDEX "IDX_844706d729b144ed62f482ac2b" ON "otp" ("expiresAt") `); `CREATE TABLE "submission_result" ("id" SERIAL NOT NULL, "score" integer NOT NULL, "probability" integer NOT NULL, "createdAt" TIMESTAMP NOT NULL DEFAULT now(), "submissionId" integer, "resultRangeId" integer, CONSTRAINT "REL_3ff0186775dd04fd7a209b6dca" UNIQUE ("submissionId"), CONSTRAINT "CHK_f4b6a23dcc9301306827573971" CHECK ("probability" >= 0 AND "probability" <= 100), CONSTRAINT "PK_965bd40b9ba83db22c13f56ce81" PRIMARY KEY ("id"))`,
await queryRunner.query(`CREATE INDEX "IDX_2d614f28549e887b905c330dc6" ON "otp" ("phone", "used") `); );
await queryRunner.query(`ALTER TABLE "choice" ADD CONSTRAINT "FK_5cc8f4862acc8b7b956be1be204" FOREIGN KEY ("questionId") REFERENCES "question"("id") ON DELETE CASCADE ON UPDATE NO ACTION`); await queryRunner.query(
await queryRunner.query(`ALTER TABLE "question_range_rule" ADD CONSTRAINT "FK_d7c902bf4e3c214ad80cb9b6e8a" FOREIGN KEY ("questionId") REFERENCES "question"("id") ON DELETE CASCADE ON UPDATE NO ACTION`); `CREATE TYPE "public"."submission_status_enum" AS ENUM('in-progress', 'completed')`,
await queryRunner.query(`ALTER TABLE "question" ADD CONSTRAINT "FK_2c8f911efa2fb5b0fe1abe92020" FOREIGN KEY ("testId") REFERENCES "test"("id") ON DELETE CASCADE ON UPDATE NO ACTION`); );
await queryRunner.query(`ALTER TABLE "result_range" ADD CONSTRAINT "FK_af2d7b64263e469098d234ce3d0" FOREIGN KEY ("testId") REFERENCES "test"("id") ON DELETE CASCADE ON UPDATE NO ACTION`); await queryRunner.query(
await queryRunner.query(`ALTER TABLE "submission_answer" ADD CONSTRAINT "FK_d52d9c3efb0e1aa66d2e27155ac" FOREIGN KEY ("submissionId") REFERENCES "submission"("id") ON DELETE CASCADE ON UPDATE NO ACTION`); `CREATE TABLE "submission" ("id" SERIAL NOT NULL, "status" "public"."submission_status_enum" NOT NULL DEFAULT 'in-progress', "createdAt" TIMESTAMP NOT NULL DEFAULT now(), "userId" integer, "testId" integer, CONSTRAINT "PK_7faa571d0e4a7076e85890c9bd0" PRIMARY KEY ("id"))`,
await queryRunner.query(`ALTER TABLE "submission_answer" ADD CONSTRAINT "FK_1da4a54a983fd01227d9fad225c" FOREIGN KEY ("questionId") REFERENCES "question"("id") ON DELETE CASCADE ON UPDATE NO ACTION`); );
await queryRunner.query(`ALTER TABLE "submission_answer" ADD CONSTRAINT "FK_0e7d9c5bf8fc8952fbf3842fa6c" FOREIGN KEY ("choiceId") REFERENCES "choice"("id") ON DELETE CASCADE ON UPDATE NO ACTION`); await queryRunner.query(
await queryRunner.query(`ALTER TABLE "submission_result" ADD CONSTRAINT "FK_3ff0186775dd04fd7a209b6dca9" FOREIGN KEY ("submissionId") REFERENCES "submission"("id") ON DELETE CASCADE ON UPDATE NO ACTION`); `CREATE TABLE "otp" ("id" SERIAL NOT NULL, "phone" character varying(11) NOT NULL, "code" character varying NOT NULL, "createdAt" TIMESTAMP NOT NULL DEFAULT now(), "expiresAt" TIMESTAMP WITH TIME ZONE NOT NULL, "used" boolean NOT NULL DEFAULT false, CONSTRAINT "PK_32556d9d7b22031d7d0e1fd6723" PRIMARY KEY ("id"))`,
await queryRunner.query(`ALTER TABLE "submission_result" ADD CONSTRAINT "FK_4f342dd432cc10d2c958d99be88" FOREIGN KEY ("resultRangeId") REFERENCES "result_range"("id") ON DELETE CASCADE ON UPDATE NO ACTION`); );
await queryRunner.query(`ALTER TABLE "submission" ADD CONSTRAINT "FK_7bd626272858ef6464aa2579094" FOREIGN KEY ("userId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`); await queryRunner.query(
await queryRunner.query(`ALTER TABLE "submission" ADD CONSTRAINT "FK_53685d02371670b4495ff181aec" FOREIGN KEY ("testId") REFERENCES "test"("id") ON DELETE CASCADE ON UPDATE NO ACTION`); `CREATE INDEX "IDX_27c8907d477176af7ed2902f2a" ON "otp" ("phone") `,
);
await queryRunner.query(
`CREATE INDEX "IDX_844706d729b144ed62f482ac2b" ON "otp" ("expiresAt") `,
);
await queryRunner.query(
`CREATE INDEX "IDX_2d614f28549e887b905c330dc6" ON "otp" ("phone", "used") `,
);
await queryRunner.query(
`ALTER TABLE "choice" ADD CONSTRAINT "FK_5cc8f4862acc8b7b956be1be204" FOREIGN KEY ("questionId") REFERENCES "question"("id") ON DELETE CASCADE ON UPDATE NO ACTION`,
);
await queryRunner.query(
`ALTER TABLE "question_range_rule" ADD CONSTRAINT "FK_d7c902bf4e3c214ad80cb9b6e8a" FOREIGN KEY ("questionId") REFERENCES "question"("id") ON DELETE CASCADE ON UPDATE NO ACTION`,
);
await queryRunner.query(
`ALTER TABLE "question" ADD CONSTRAINT "FK_2c8f911efa2fb5b0fe1abe92020" FOREIGN KEY ("testId") REFERENCES "test"("id") ON DELETE CASCADE ON UPDATE NO ACTION`,
);
await queryRunner.query(
`ALTER TABLE "result_range" ADD CONSTRAINT "FK_af2d7b64263e469098d234ce3d0" FOREIGN KEY ("testId") REFERENCES "test"("id") ON DELETE CASCADE ON UPDATE NO ACTION`,
);
await queryRunner.query(
`ALTER TABLE "submission_answer" ADD CONSTRAINT "FK_d52d9c3efb0e1aa66d2e27155ac" FOREIGN KEY ("submissionId") REFERENCES "submission"("id") ON DELETE CASCADE ON UPDATE NO ACTION`,
);
await queryRunner.query(
`ALTER TABLE "submission_answer" ADD CONSTRAINT "FK_1da4a54a983fd01227d9fad225c" FOREIGN KEY ("questionId") REFERENCES "question"("id") ON DELETE CASCADE ON UPDATE NO ACTION`,
);
await queryRunner.query(
`ALTER TABLE "submission_answer" ADD CONSTRAINT "FK_0e7d9c5bf8fc8952fbf3842fa6c" FOREIGN KEY ("choiceId") REFERENCES "choice"("id") ON DELETE CASCADE ON UPDATE NO ACTION`,
);
await queryRunner.query(
`ALTER TABLE "submission_result" ADD CONSTRAINT "FK_3ff0186775dd04fd7a209b6dca9" FOREIGN KEY ("submissionId") REFERENCES "submission"("id") ON DELETE CASCADE ON UPDATE NO ACTION`,
);
await queryRunner.query(
`ALTER TABLE "submission_result" ADD CONSTRAINT "FK_4f342dd432cc10d2c958d99be88" FOREIGN KEY ("resultRangeId") REFERENCES "result_range"("id") ON DELETE CASCADE ON UPDATE NO ACTION`,
);
await queryRunner.query(
`ALTER TABLE "submission" ADD CONSTRAINT "FK_7bd626272858ef6464aa2579094" FOREIGN KEY ("userId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`,
);
await queryRunner.query(
`ALTER TABLE "submission" ADD CONSTRAINT "FK_53685d02371670b4495ff181aec" FOREIGN KEY ("testId") REFERENCES "test"("id") ON DELETE CASCADE ON UPDATE NO ACTION`,
);
} }
public async down(queryRunner: QueryRunner): Promise<void> { public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`ALTER TABLE "submission" DROP CONSTRAINT "FK_53685d02371670b4495ff181aec"`); await queryRunner.query(
await queryRunner.query(`ALTER TABLE "submission" DROP CONSTRAINT "FK_7bd626272858ef6464aa2579094"`); `ALTER TABLE "submission" DROP CONSTRAINT "FK_53685d02371670b4495ff181aec"`,
await queryRunner.query(`ALTER TABLE "submission_result" DROP CONSTRAINT "FK_4f342dd432cc10d2c958d99be88"`); );
await queryRunner.query(`ALTER TABLE "submission_result" DROP CONSTRAINT "FK_3ff0186775dd04fd7a209b6dca9"`); await queryRunner.query(
await queryRunner.query(`ALTER TABLE "submission_answer" DROP CONSTRAINT "FK_0e7d9c5bf8fc8952fbf3842fa6c"`); `ALTER TABLE "submission" DROP CONSTRAINT "FK_7bd626272858ef6464aa2579094"`,
await queryRunner.query(`ALTER TABLE "submission_answer" DROP CONSTRAINT "FK_1da4a54a983fd01227d9fad225c"`); );
await queryRunner.query(`ALTER TABLE "submission_answer" DROP CONSTRAINT "FK_d52d9c3efb0e1aa66d2e27155ac"`); await queryRunner.query(
await queryRunner.query(`ALTER TABLE "result_range" DROP CONSTRAINT "FK_af2d7b64263e469098d234ce3d0"`); `ALTER TABLE "submission_result" DROP CONSTRAINT "FK_4f342dd432cc10d2c958d99be88"`,
await queryRunner.query(`ALTER TABLE "question" DROP CONSTRAINT "FK_2c8f911efa2fb5b0fe1abe92020"`); );
await queryRunner.query(`ALTER TABLE "question_range_rule" DROP CONSTRAINT "FK_d7c902bf4e3c214ad80cb9b6e8a"`); await queryRunner.query(
await queryRunner.query(`ALTER TABLE "choice" DROP CONSTRAINT "FK_5cc8f4862acc8b7b956be1be204"`); `ALTER TABLE "submission_result" DROP CONSTRAINT "FK_3ff0186775dd04fd7a209b6dca9"`,
await queryRunner.query(`DROP INDEX "public"."IDX_2d614f28549e887b905c330dc6"`); );
await queryRunner.query(`DROP INDEX "public"."IDX_844706d729b144ed62f482ac2b"`); await queryRunner.query(
await queryRunner.query(`DROP INDEX "public"."IDX_27c8907d477176af7ed2902f2a"`); `ALTER TABLE "submission_answer" DROP CONSTRAINT "FK_0e7d9c5bf8fc8952fbf3842fa6c"`,
);
await queryRunner.query(
`ALTER TABLE "submission_answer" DROP CONSTRAINT "FK_1da4a54a983fd01227d9fad225c"`,
);
await queryRunner.query(
`ALTER TABLE "submission_answer" DROP CONSTRAINT "FK_d52d9c3efb0e1aa66d2e27155ac"`,
);
await queryRunner.query(
`ALTER TABLE "result_range" DROP CONSTRAINT "FK_af2d7b64263e469098d234ce3d0"`,
);
await queryRunner.query(
`ALTER TABLE "question" DROP CONSTRAINT "FK_2c8f911efa2fb5b0fe1abe92020"`,
);
await queryRunner.query(
`ALTER TABLE "question_range_rule" DROP CONSTRAINT "FK_d7c902bf4e3c214ad80cb9b6e8a"`,
);
await queryRunner.query(
`ALTER TABLE "choice" DROP CONSTRAINT "FK_5cc8f4862acc8b7b956be1be204"`,
);
await queryRunner.query(
`DROP INDEX "public"."IDX_2d614f28549e887b905c330dc6"`,
);
await queryRunner.query(
`DROP INDEX "public"."IDX_844706d729b144ed62f482ac2b"`,
);
await queryRunner.query(
`DROP INDEX "public"."IDX_27c8907d477176af7ed2902f2a"`,
);
await queryRunner.query(`DROP TABLE "otp"`); await queryRunner.query(`DROP TABLE "otp"`);
await queryRunner.query(`DROP TABLE "submission"`); await queryRunner.query(`DROP TABLE "submission"`);
await queryRunner.query(`DROP TYPE "public"."submission_status_enum"`); await queryRunner.query(`DROP TYPE "public"."submission_status_enum"`);
await queryRunner.query(`DROP TABLE "submission_result"`); await queryRunner.query(`DROP TABLE "submission_result"`);
await queryRunner.query(`DROP INDEX "public"."IDX_ea78aac2e76e03f1c2fc720f1d"`); await queryRunner.query(
`DROP INDEX "public"."IDX_ea78aac2e76e03f1c2fc720f1d"`,
);
await queryRunner.query(`DROP TABLE "submission_answer"`); await queryRunner.query(`DROP TABLE "submission_answer"`);
await queryRunner.query(`DROP TABLE "test"`); await queryRunner.query(`DROP TABLE "test"`);
await queryRunner.query(`DROP TYPE "public"."test_accesstype_enum"`); await queryRunner.query(`DROP TYPE "public"."test_accesstype_enum"`);
await queryRunner.query(`DROP TABLE "result_range"`); await queryRunner.query(`DROP TABLE "result_range"`);
await queryRunner.query(`DROP INDEX "public"."UNQ_TEST_ORDER_IDX"`); await queryRunner.query(`DROP INDEX "public"."UNQ_TEST_ORDER_IDX"`);
await queryRunner.query(`DROP INDEX "public"."IDX_275b917b91392b9a2d6ab7ccee"`); await queryRunner.query(
`DROP INDEX "public"."IDX_275b917b91392b9a2d6ab7ccee"`,
);
await queryRunner.query(`DROP TABLE "question"`); await queryRunner.query(`DROP TABLE "question"`);
await queryRunner.query(`DROP TYPE "public"."question_type_enum"`); await queryRunner.query(`DROP TYPE "public"."question_type_enum"`);
await queryRunner.query(`DROP TABLE "question_range_rule"`); await queryRunner.query(`DROP TABLE "question_range_rule"`);
await queryRunner.query(`DROP INDEX "public"."UNQ_QUESTION_ORDER_IDX"`); await queryRunner.query(`DROP INDEX "public"."UNQ_QUESTION_ORDER_IDX"`);
await queryRunner.query(`DROP TABLE "choice"`); await queryRunner.query(`DROP TABLE "choice"`);
await queryRunner.query(`DROP INDEX "public"."IDX_8e1f623798118e629b46a9e629"`); await queryRunner.query(
`DROP INDEX "public"."IDX_8e1f623798118e629b46a9e629"`,
);
await queryRunner.query(`DROP TABLE "user"`); await queryRunner.query(`DROP TABLE "user"`);
await queryRunner.query(`DROP TYPE "public"."user_role_enum"`); await queryRunner.query(`DROP TYPE "public"."user_role_enum"`);
} }
} }
@@ -1,16 +1,21 @@
import { MigrationInterface, QueryRunner } from "typeorm"; import { MigrationInterface, QueryRunner } from 'typeorm';
export class AnswerSubmissionV21783945681366 implements MigrationInterface { export class AnswerSubmissionV21783945681366 implements MigrationInterface {
name = 'AnswerSubmissionV21783945681366' name = 'AnswerSubmissionV21783945681366';
public async up(queryRunner: QueryRunner): Promise<void> { public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`ALTER TABLE "test" ADD "questionsCount" integer NOT NULL DEFAULT '0'`); await queryRunner.query(
await queryRunner.query(`ALTER TABLE "submission" ADD "currentQuestionOrder" integer NOT NULL DEFAULT '1'`); `ALTER TABLE "test" ADD "questionsCount" integer NOT NULL DEFAULT '0'`,
);
await queryRunner.query(
`ALTER TABLE "submission" ADD "currentQuestionOrder" integer NOT NULL DEFAULT '1'`,
);
} }
public async down(queryRunner: QueryRunner): Promise<void> { public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`ALTER TABLE "submission" DROP COLUMN "currentQuestionOrder"`); await queryRunner.query(
`ALTER TABLE "submission" DROP COLUMN "currentQuestionOrder"`,
);
await queryRunner.query(`ALTER TABLE "test" DROP COLUMN "questionsCount"`); await queryRunner.query(`ALTER TABLE "test" DROP COLUMN "questionsCount"`);
} }
} }
@@ -0,0 +1,48 @@
import { MigrationInterface, QueryRunner } from 'typeorm';
export class QuestionTransition1785750403713 implements MigrationInterface {
name = 'QuestionTransition1785750403713';
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
`CREATE TYPE "public"."question_transition_type_enum" AS ENUM('start', 'default', 'choice', 'number-range', 'end')`,
);
await queryRunner.query(
`CREATE TABLE "question_transition" ("id" SERIAL NOT NULL, "type" "public"."question_transition_type_enum" NOT NULL, "condition" jsonb, "testId" integer, "fromQuestionId" integer, "toQuestionId" integer, CONSTRAINT "PK_fc854577c9dbe770ce91bbc82d7" PRIMARY KEY ("id"))`,
);
await queryRunner.query(
`CREATE TYPE "public"."test_status_enum" AS ENUM('in-progress', 'published')`,
);
await queryRunner.query(
`ALTER TABLE "test" ADD "status" "public"."test_status_enum" NOT NULL DEFAULT 'in-progress'`,
);
await queryRunner.query(
`ALTER TABLE "question_transition" ADD CONSTRAINT "FK_128830a10f0fa9c7c5b502b0993" FOREIGN KEY ("testId") REFERENCES "test"("id") ON DELETE CASCADE ON UPDATE NO ACTION`,
);
await queryRunner.query(
`ALTER TABLE "question_transition" ADD CONSTRAINT "FK_fffa1994c9fce73527344d7cdf0" FOREIGN KEY ("fromQuestionId") REFERENCES "question"("id") ON DELETE CASCADE ON UPDATE NO ACTION`,
);
await queryRunner.query(
`ALTER TABLE "question_transition" ADD CONSTRAINT "FK_002513ef1825a6717d4ed74a6f8" FOREIGN KEY ("toQuestionId") REFERENCES "question"("id") ON DELETE CASCADE ON UPDATE NO ACTION`,
);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
`ALTER TABLE "question_transition" DROP CONSTRAINT "FK_002513ef1825a6717d4ed74a6f8"`,
);
await queryRunner.query(
`ALTER TABLE "question_transition" DROP CONSTRAINT "FK_fffa1994c9fce73527344d7cdf0"`,
);
await queryRunner.query(
`ALTER TABLE "question_transition" DROP CONSTRAINT "FK_128830a10f0fa9c7c5b502b0993"`,
);
await queryRunner.query(`ALTER TABLE "test" DROP COLUMN "status"`);
await queryRunner.query(`DROP TYPE "public"."test_status_enum"`);
await queryRunner.query(`DROP TABLE "question_transition"`);
await queryRunner.query(
`DROP TYPE "public"."question_transition_type_enum"`,
);
}
}
@@ -0,0 +1,79 @@
import {
EX_QUESTION_NUMBER_RANGE_OVERLAP,
UNIQUE_QUESTION_CHOICE,
UNIQUE_QUESTION_DEFAULT,
UNIQUE_QUESTION_END,
UNIQUE_QUESTION_NUMBER_RANGE,
UNIQUE_TEST_START,
} from '@/modules/questions/constants';
import { QuestionTransitionType } from '@/modules/questions/enums/question-transition-type.enum';
import { MigrationInterface, QueryRunner } from 'typeorm';
export class QuestionTransitionConstraints1785755373000 implements MigrationInterface {
name = 'QuestionTransitionConstraints1785755373000';
public async up(queryRunner: QueryRunner): Promise<any> {
await queryRunner.query(`
CREATE UNIQUE INDEX ${UNIQUE_TEST_START}
ON question_transition("testId")
WHERE type = '${QuestionTransitionType.START}';
`);
await queryRunner.query(`
CREATE UNIQUE INDEX ${UNIQUE_QUESTION_END}
ON question_transition("fromQuestionId")
WHERE type = '${QuestionTransitionType.END}';
`);
await queryRunner.query(`
CREATE UNIQUE INDEX ${UNIQUE_QUESTION_DEFAULT}
ON question_transition("fromQuestionId")
WHERE type = '${QuestionTransitionType.DEFAULT}';
`);
await queryRunner.query(`
CREATE UNIQUE INDEX ${UNIQUE_QUESTION_CHOICE}
ON question_transition(
"fromQuestionId",
(condition ->> 'choiceId')
)
WHERE type = '${QuestionTransitionType.CHOICE}';
`);
await queryRunner.query(`
CREATE UNIQUE INDEX ${UNIQUE_QUESTION_NUMBER_RANGE}
ON question_transition(
"fromQuestionId",
(condition ->> 'min'),
(condition ->> 'max')
)
WHERE type = '${QuestionTransitionType.NUMBER_RANGE}';
`);
await queryRunner.query(`
CREATE EXTENSION IF NOT EXISTS btree_gist;
`);
await queryRunner.query(`
ALTER TABLE question_transition
ADD CONSTRAINT ${EX_QUESTION_NUMBER_RANGE_OVERLAP}
EXCLUDE USING gist (
"fromQuestionId" WITH =,
int4range(
((condition ->> 'min')::int),
((condition ->> 'max')::int),
'[]'
) WITH &&
)
WHERE (type = '${QuestionTransitionType.NUMBER_RANGE}');
`);
}
public async down(queryRunner: QueryRunner): Promise<any> {
await queryRunner.query(`
ALTER TABLE question_transition
DROP CONSTRAINT ${EX_QUESTION_NUMBER_RANGE_OVERLAP};
`);
await queryRunner.query(
`DROP INDEX IF EXISTS ${UNIQUE_QUESTION_NUMBER_RANGE};`,
);
await queryRunner.query(`DROP INDEX IF EXISTS ${UNIQUE_QUESTION_CHOICE};`);
await queryRunner.query(`DROP INDEX IF EXISTS ${UNIQUE_QUESTION_DEFAULT};`);
await queryRunner.query(`DROP INDEX IF EXISTS ${UNIQUE_QUESTION_END};`);
await queryRunner.query(`DROP INDEX IF EXISTS ${UNIQUE_TEST_START};`);
}
}
+8
View File
@@ -1,2 +1,10 @@
export const UNIQUE_TEST_ORDER_IDX = 'UNQ_TEST_ORDER_IDX'; export const UNIQUE_TEST_ORDER_IDX = 'UNQ_TEST_ORDER_IDX';
export const UNIQUE_QUESTION_ORDER_IDX = 'UNQ_QUESTION_ORDER_IDX'; export const UNIQUE_QUESTION_ORDER_IDX = 'UNQ_QUESTION_ORDER_IDX';
export const UNIQUE_QUESTION_NUMBER_RANGE = 'UNQ_QUESTION_NUMBER_RANGE';
export const UNIQUE_QUESTION_CHOICE = 'UNQ_QUESTION_CHOICE';
export const UNIQUE_QUESTION_DEFAULT = 'UNQ_QUESTION_DEFAULT';
export const UNIQUE_QUESTION_END = 'UNQ_QUESTION_END';
export const UNIQUE_TEST_START = 'UNQ_TEST_START';
export const EX_QUESTION_NUMBER_RANGE_OVERLAP =
'EX_QUESTION_NUMBER_RANGE_OVERLAP';
@@ -0,0 +1,148 @@
import { ConflictException, HttpStatus, Injectable } from '@nestjs/common';
import { QueryFailedError, Repository } from 'typeorm';
import { QuestionTransition } from '../../entities/question-transition.entity';
import { InjectRepository } from '@nestjs/typeorm';
import { QuestionTransitionValidatorProvider } from './validations/question-transition-validator.provider';
import { CreateQuestionTransitionDTO } from '../../dtos/create-question-transition.dto';
import {
EX_QUESTION_NUMBER_RANGE_OVERLAP,
UNIQUE_QUESTION_CHOICE,
UNIQUE_QUESTION_DEFAULT,
UNIQUE_QUESTION_END,
UNIQUE_QUESTION_NUMBER_RANGE,
UNIQUE_TEST_START,
} from '../../constants';
import { UpdateQuestionTransitionDTO } from '../../dtos/update-question-transition.dto';
import { AppTimeoutException } from '@/common/exceptions/app-timeout.exception';
import { AppException } from '@/common/exceptions/app.exception';
@Injectable()
export class QuestionTransitionCommandProvider {
constructor(
/**
* Inject QuestionTransitions Repository
*/
@InjectRepository(QuestionTransition)
private readonly transitionsRepository: Repository<QuestionTransition>,
/**
* Inject QuestionTransitionValidator Provider
*/
private readonly questionTransitionValidatorProvider: QuestionTransitionValidatorProvider,
) {}
public async createOne(
createQuestionTransitionDto: CreateQuestionTransitionDTO,
) {
const { test, fromQuestion, toQuestion, type, conditionValue } =
await this.questionTransitionValidatorProvider.validate(
createQuestionTransitionDto,
);
const newTransition = this.transitionsRepository.create({
test,
fromQuestion,
toQuestion,
type,
condition: conditionValue,
});
try {
await this.transitionsRepository.save(newTransition);
} catch (err) {
this.handleDBExceptions(err);
}
}
public async updateOne(
id: number,
updateQuestionTransitionDto: UpdateQuestionTransitionDTO,
) {
const { transition, test, fromQuestion, toQuestion, type, conditionValue } =
await this.questionTransitionValidatorProvider.validate(
updateQuestionTransitionDto,
id,
);
if (!transition) {
throw new AppException(
'common.errors.entityNotFound',
HttpStatus.NOT_FOUND,
{ entity: '$t(questions.fields.questionTransition)' },
);
}
transition.test = test;
transition.fromQuestion = fromQuestion;
transition.toQuestion = toQuestion;
transition.type = type;
transition.condition = conditionValue;
try {
await this.transitionsRepository.save(transition);
} catch (err) {
this.handleDBExceptions(err);
}
}
private handleDBExceptions(err: unknown) {
if (err instanceof QueryFailedError) {
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-member-access
const constraint = (err as any).constraint;
switch (constraint) {
case UNIQUE_TEST_START:
throw new ConflictException(
'questions.errors.questionTransition.startAlreadyExists',
);
case UNIQUE_QUESTION_END:
throw new ConflictException(
'questions.errors.questionTransition.endAlreadyExists',
);
case UNIQUE_QUESTION_DEFAULT:
throw new ConflictException(
'questions.errors.questionTransition.defaultAlreadyExists',
);
case UNIQUE_QUESTION_CHOICE:
case UNIQUE_QUESTION_NUMBER_RANGE:
throw new ConflictException(
'questions.errors.questionTransition.duplicateTransition',
);
case EX_QUESTION_NUMBER_RANGE_OVERLAP:
throw new ConflictException(
'questions.errors.questionTransition.numberRangeOverlap',
);
}
}
throw new AppTimeoutException(err);
}
public async deleteOne(id: number) {
let transition: QuestionTransition | null = null;
try {
transition = await this.transitionsRepository.findOneBy({ id });
} catch (err) {
throw new AppTimeoutException(err);
}
if (!transition) {
throw new AppException(
'common.errors.entityNotFound',
HttpStatus.NOT_FOUND,
{ entity: '$t(questions.fields.questionTransition)' },
);
}
try {
await this.transitionsRepository.delete(transition.id);
} catch (err) {
throw new AppTimeoutException(err);
}
}
}
@@ -14,6 +14,7 @@ import { QuestionsQueryProvider } from '../../questions-query.provider';
import { TransitionValidateOptions } from '../../../interfaces/transition-validate-options.interface'; import { TransitionValidateOptions } from '../../../interfaces/transition-validate-options.interface';
import { QuestionTransitionTypeValidator } from '../../../validators/question-transition-type.validator'; import { QuestionTransitionTypeValidator } from '../../../validators/question-transition-type.validator';
import { CheckTransitionDBProvider } from './check-transition-db.provider'; import { CheckTransitionDBProvider } from './check-transition-db.provider';
import { AppTimeoutException } from '@/common/exceptions/app-timeout.exception';
@Injectable() @Injectable()
export class QuestionTransitionValidatorProvider { export class QuestionTransitionValidatorProvider {
@@ -50,8 +51,11 @@ export class QuestionTransitionValidatorProvider {
let toQuestion: Question | null; let toQuestion: Question | null;
let conditionValue: TransitionConditionType | undefined; let conditionValue: TransitionConditionType | undefined;
let transition: QuestionTransition | null = null;
if (transitionId !== undefined) { if (transitionId !== undefined) {
const transition = await this.transitionsRepository.findOne({ try {
transition = await this.transitionsRepository.findOne({
where: { id: transitionId }, where: { id: transitionId },
relations: { relations: {
test: true, test: true,
@@ -59,6 +63,9 @@ export class QuestionTransitionValidatorProvider {
toQuestion: true, toQuestion: true,
}, },
}); });
} catch (err) {
throw new AppTimeoutException(err);
}
if (!transition) { if (!transition) {
throw new AppException( throw new AppException(
@@ -75,7 +82,7 @@ export class QuestionTransitionValidatorProvider {
type = dto.type ?? transition.type; type = dto.type ?? transition.type;
if (dto.fromQuestion) { if (dto.fromQuestion !== undefined && dto.fromQuestion !== null) {
fromQuestion = await this.questionsQueryProvider.findOneByIdForAdmin( fromQuestion = await this.questionsQueryProvider.findOneByIdForAdmin(
dto.fromQuestion, dto.fromQuestion,
); );
@@ -85,7 +92,7 @@ export class QuestionTransitionValidatorProvider {
fromQuestion = transition.fromQuestion; fromQuestion = transition.fromQuestion;
} }
if (dto.toQuestion) { if (dto.toQuestion !== undefined && dto.toQuestion !== null) {
toQuestion = await this.questionsQueryProvider.findOneByIdForAdmin( toQuestion = await this.questionsQueryProvider.findOneByIdForAdmin(
dto.toQuestion, dto.toQuestion,
); );
@@ -138,7 +145,7 @@ export class QuestionTransitionValidatorProvider {
await this.checkTransitionDBProvider.validate(options, transitionId); await this.checkTransitionDBProvider.validate(options, transitionId);
return options; return { ...options, transition };
} }
private validateQuestions( private validateQuestions(