From 9f9119c5107238115acf07864dca90e49112ac18 Mon Sep 17 00:00:00 2001 From: doxylee <68041124+doxylee@users.noreply.github.com> Date: Tue, 10 Dec 2024 16:55:27 +0900 Subject: [PATCH] Feat/scholar sync (#142) * initialize scholar module * wip: create service * base structure * wip: repository and interface * wip * feat: SyncApiKeyAuth * wip: working on scholardb sync * feat: department update * wip * feat: scholarDB update API * feat: Add sync slack logs * refactor: APIKeyAuth set authentication/authorization instead of isPublic * refactor: Remove redundant secret in body * fix: Add missing SkackNotiService * feat: sync examtime * feat: sync classtimes * refactor: syncTime method to handle both examtime and classtime syncing * refactor: Divide sync services * feat: Sync taken lectures * feat: Create schema for saving taken_lecture raw data * fix: sync baseline with prod database * feat: Save raw taken lectures * feat: repopulate taken lectures of new user from rawTakenLecture data * chore: Add needed env vars to .env.example * fix: Allow bigger request for sync requests * fix: Remove Error being logged when jwt auth fails * fix: Nested object becomes empty, english_lec can be "" * chore: console.log related * fix: Fix or skip tests * chore: update jest * fix: Maark lectures not in API as deleted * enhance: syncScholarDB log and result * test: syncScholarDB * test: classtime sync * fix: Time not saved correctly * test: examtime sync * fix: professor id wrongly connected * test: enhance syncScholarDB test * test: syncTakenLecture * fix sync bugs * fix: enlarge type, name_en fields length in schema * fix: handle multiple users with same student_id * fix: timezone related issue --- env/.env.example | 4 +- package-lock.json | 1737 ++++++++++------- package.json | 13 +- src/app.controller.spec.ts | 5 +- src/app.module.ts | 16 +- src/bootstrap/bootstrap.ts | 4 + .../decorators/sync-api-key-auth.decorator.ts | 4 + src/common/entities/EProfessor.ts | 8 + src/common/entities/ETakenLecture.ts | 12 + src/common/entities/EUserProfile.ts | 13 + src/common/interfaces/ISync.ts | 357 ++++ src/common/interfaces/constants/professor.ts | 1 + src/modules/auth/auth.config.ts | 20 +- src/modules/auth/auth.module.ts | 10 +- src/modules/auth/auth.service.ts | 16 +- src/modules/auth/command/jwt.command.ts | 10 +- .../auth/command/syncApiKey.command.ts | 30 + src/modules/sync/slackNoti.service.ts | 22 + src/modules/sync/sync.controller.ts | 53 + src/modules/sync/sync.module.ts | 14 + .../sync/syncScholarDB.service.spec.ts | 843 ++++++++ src/modules/sync/syncScholarDB.service.ts | 650 ++++++ .../sync/syncTakenLecture.service.spec.ts | 233 +++ src/modules/sync/syncTakenLecture.service.ts | 178 ++ src/modules/sync/types.ts | 62 + .../middleware/prisma.lectureprofessors.ts | 5 +- .../migration.sql | 14 + .../migration.sql | 2 + .../migration.sql | 2 + .../migration.sql | 3 + .../migration.sql | 3 + .../migration.sql | 3 + .../migration.sql | 2 + src/prisma/prisma.module.ts | 3 + src/prisma/repositories/sync.repository.ts | 325 +++ src/prisma/schema.prisma | 25 +- src/settings.ts | 8 + test/prisma.spec.ts | 9 +- test/session/session.spec.ts | 3 +- test/transaction.spec.ts | 10 +- 40 files changed, 3938 insertions(+), 794 deletions(-) create mode 100644 src/common/decorators/sync-api-key-auth.decorator.ts create mode 100644 src/common/entities/EProfessor.ts create mode 100644 src/common/entities/ETakenLecture.ts create mode 100644 src/common/entities/EUserProfile.ts create mode 100644 src/common/interfaces/ISync.ts create mode 100644 src/common/interfaces/constants/professor.ts create mode 100644 src/modules/auth/command/syncApiKey.command.ts create mode 100644 src/modules/sync/slackNoti.service.ts create mode 100644 src/modules/sync/sync.controller.ts create mode 100644 src/modules/sync/sync.module.ts create mode 100644 src/modules/sync/syncScholarDB.service.spec.ts create mode 100644 src/modules/sync/syncScholarDB.service.ts create mode 100644 src/modules/sync/syncTakenLecture.service.spec.ts create mode 100644 src/modules/sync/syncTakenLecture.service.ts create mode 100644 src/modules/sync/types.ts create mode 100644 src/prisma/migrations/20241025174157_sync_taken_lectures/migration.sql create mode 100644 src/prisma/migrations/20241025180646_sync_taken_lectures_year_semester_index/migration.sql create mode 100644 src/prisma/migrations/20241105080413_department_name_en_length/migration.sql create mode 100644 src/prisma/migrations/20241105081301_course_type_type_en_length/migration.sql create mode 100644 src/prisma/migrations/20241105082001_course_type_type_en_length_2/migration.sql create mode 100644 src/prisma/migrations/20241105082806_lecture_type_type_en_length/migration.sql create mode 100644 src/prisma/migrations/20241110204737_classtime_room_name_length_increase/migration.sql create mode 100644 src/prisma/repositories/sync.repository.ts diff --git a/env/.env.example b/env/.env.example index 2c111e45..3e1e7916 100644 --- a/env/.env.example +++ b/env/.env.example @@ -7,4 +7,6 @@ EXPIRES_IN=3600 REFRESH_EXPIRES_IN=2592000 SSO_IS_BETA = false SSO_CLIENT_ID = "" -SSO_SECRET_KEY = "" \ No newline at end of file +SSO_SECRET_KEY = "" +SYNC_SECRET = "" +SLACK_KEY = "" \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index fde61e90..c88eaaaf 100644 --- a/package-lock.json +++ b/package-lock.json @@ -18,6 +18,7 @@ "@nestjs/platform-express": "^9.0.0", "@nestjs/swagger": "^7.1.1", "@prisma/client": "^5.18.0", + "@slack/web-api": "^7.7.0", "@types/cookie-parser": "^1.4.3", "@types/express-session": "^1.17.7", "@types/morgan": "^1.9.4", @@ -54,7 +55,7 @@ "@types/csurf": "^1.11.5", "@types/express": "^4.17.13", "@types/inquirer": "^9.0.7", - "@types/jest": "28.1.4", + "@types/jest": "^29.5.14", "@types/node": "^16.18.23", "@types/supertest": "^6.0.2", "@typescript-eslint/eslint-plugin": "^5.0.0", @@ -63,13 +64,13 @@ "eslint": "^8.0.1", "eslint-config-prettier": "^8.3.0", "husky": "^8.0.0", - "jest": "28.1.2", + "jest": "^29.7.0", "lint-staged": "^13.2.3", "prettier": "^2.3.2", "prisma": "^5.18.0", "source-map-support": "^0.5.20", "supertest": "^7.0.0", - "ts-jest": "^28.0.5", + "ts-jest": "^29.2.5", "ts-loader": "^9.2.3", "ts-node": "^10.9.1", "tsconfig-paths": "4.0.0", @@ -186,12 +187,13 @@ } }, "node_modules/@babel/code-frame": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.24.7.tgz", - "integrity": "sha512-BcYH1CVJBO9tvyIZ2jVeXgSIMvGZ2FDRvDdOIVQyuklNKSsx+eppDEBq/g47Ayw+RqNFE+URvOShmf+f/qwAlA==", + "version": "7.26.0", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.26.0.tgz", + "integrity": "sha512-INCKxTtbXtcNbUZ3YXutwMpEleqttcswhAdee7dhuoVrD2cnuc3PqtERBtxkX5nziX9vnBL8WXmSGwv8CuPV6g==", "dev": true, "dependencies": { - "@babel/highlight": "^7.24.7", + "@babel/helper-validator-identifier": "^7.25.9", + "js-tokens": "^4.0.0", "picocolors": "^1.0.0" }, "engines": { @@ -199,30 +201,30 @@ } }, "node_modules/@babel/compat-data": { - "version": "7.25.2", - "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.25.2.tgz", - "integrity": "sha512-bYcppcpKBvX4znYaPEeFau03bp89ShqNMLs+rmdptMw+heSZh9+z84d2YG+K7cYLbWwzdjtDoW/uqZmPjulClQ==", + "version": "7.26.0", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.26.0.tgz", + "integrity": "sha512-qETICbZSLe7uXv9VE8T/RWOdIE5qqyTucOt4zLYMafj2MRO271VGgLd4RACJMeBO37UPWhXiKMBk7YlJ0fOzQA==", "dev": true, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/core": { - "version": "7.25.2", - "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.25.2.tgz", - "integrity": "sha512-BBt3opiCOxUr9euZ5/ro/Xv8/V7yJ5bjYMqG/C1YAo8MIKAnumZalCN+msbci3Pigy4lIQfPUpfMM27HMGaYEA==", + "version": "7.26.0", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.26.0.tgz", + "integrity": "sha512-i1SLeK+DzNnQ3LL/CswPCa/E5u4lh1k6IAEphON8F+cXt0t9euTshDru0q7/IqMa1PMPz5RnHuHscF8/ZJsStg==", "dev": true, "dependencies": { "@ampproject/remapping": "^2.2.0", - "@babel/code-frame": "^7.24.7", - "@babel/generator": "^7.25.0", - "@babel/helper-compilation-targets": "^7.25.2", - "@babel/helper-module-transforms": "^7.25.2", - "@babel/helpers": "^7.25.0", - "@babel/parser": "^7.25.0", - "@babel/template": "^7.25.0", - "@babel/traverse": "^7.25.2", - "@babel/types": "^7.25.2", + "@babel/code-frame": "^7.26.0", + "@babel/generator": "^7.26.0", + "@babel/helper-compilation-targets": "^7.25.9", + "@babel/helper-module-transforms": "^7.26.0", + "@babel/helpers": "^7.26.0", + "@babel/parser": "^7.26.0", + "@babel/template": "^7.25.9", + "@babel/traverse": "^7.25.9", + "@babel/types": "^7.26.0", "convert-source-map": "^2.0.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", @@ -237,12 +239,6 @@ "url": "https://opencollective.com/babel" } }, - "node_modules/@babel/core/node_modules/convert-source-map": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", - "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", - "dev": true - }, "node_modules/@babel/core/node_modules/semver": { "version": "6.3.1", "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", @@ -253,29 +249,30 @@ } }, "node_modules/@babel/generator": { - "version": "7.25.0", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.25.0.tgz", - "integrity": "sha512-3LEEcj3PVW8pW2R1SR1M89g/qrYk/m/mB/tLqn7dn4sbBUQyTqnlod+II2U4dqiGtUmkcnAmkMDralTFZttRiw==", + "version": "7.26.0", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.26.0.tgz", + "integrity": "sha512-/AIkAmInnWwgEAJGQr9vY0c66Mj6kjkE2ZPB1PurTRaRAh3U+J45sAQMjQDJdh4WbR3l0x5xkimXBKyBXXAu2w==", "dev": true, "dependencies": { - "@babel/types": "^7.25.0", + "@babel/parser": "^7.26.0", + "@babel/types": "^7.26.0", "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.25", - "jsesc": "^2.5.1" + "jsesc": "^3.0.2" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-compilation-targets": { - "version": "7.25.2", - "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.25.2.tgz", - "integrity": "sha512-U2U5LsSaZ7TAt3cfaymQ8WHh0pxvdHoEk6HVpaexxixjyEquMh0L0YNJNM6CTGKMXV1iksi0iZkGw4AcFkPaaw==", + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.25.9.tgz", + "integrity": "sha512-j9Db8Suy6yV/VHa4qzrj9yZfZxhLWQdVnRlXxmKLYlhWUVB1sB2G5sxuWYXk/whHD9iW76PmNzxZ4UCnTQTVEQ==", "dev": true, "dependencies": { - "@babel/compat-data": "^7.25.2", - "@babel/helper-validator-option": "^7.24.8", - "browserslist": "^4.23.1", + "@babel/compat-data": "^7.25.9", + "@babel/helper-validator-option": "^7.25.9", + "browserslist": "^4.24.0", "lru-cache": "^5.1.1", "semver": "^6.3.1" }, @@ -293,28 +290,27 @@ } }, "node_modules/@babel/helper-module-imports": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.24.7.tgz", - "integrity": "sha512-8AyH3C+74cgCVVXow/myrynrAGv+nTVg5vKu2nZph9x7RcRwzmh0VFallJuFTZ9mx6u4eSdXZfcOzSqTUm0HCA==", + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.25.9.tgz", + "integrity": "sha512-tnUA4RsrmflIM6W6RFTLFSXITtl0wKjgpnLgXyowocVPrbYrLUXSBXDgTs8BlbmIzIdlBySRQjINYs2BAkiLtw==", "dev": true, "dependencies": { - "@babel/traverse": "^7.24.7", - "@babel/types": "^7.24.7" + "@babel/traverse": "^7.25.9", + "@babel/types": "^7.25.9" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-module-transforms": { - "version": "7.25.2", - "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.25.2.tgz", - "integrity": "sha512-BjyRAbix6j/wv83ftcVJmBt72QtHI56C7JXZoG2xATiLpmoC7dpd8WnkikExHDVPpi/3qCmO6WY1EaXOluiecQ==", + "version": "7.26.0", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.26.0.tgz", + "integrity": "sha512-xO+xu6B5K2czEnQye6BHA7DolFFmS3LB7stHZFaOLb1pAwO1HWLS8fXA+eh0A2yIvltPVmx3eNNDBJA2SLHXFw==", "dev": true, "dependencies": { - "@babel/helper-module-imports": "^7.24.7", - "@babel/helper-simple-access": "^7.24.7", - "@babel/helper-validator-identifier": "^7.24.7", - "@babel/traverse": "^7.25.2" + "@babel/helper-module-imports": "^7.25.9", + "@babel/helper-validator-identifier": "^7.25.9", + "@babel/traverse": "^7.25.9" }, "engines": { "node": ">=6.9.0" @@ -324,160 +320,61 @@ } }, "node_modules/@babel/helper-plugin-utils": { - "version": "7.24.8", - "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.24.8.tgz", - "integrity": "sha512-FFWx5142D8h2Mgr/iPVGH5G7w6jDn4jUSpZTyDnQO0Yn7Ks2Kuz6Pci8H6MPCoUJegd/UZQ3tAvfLCxQSnWWwg==", + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.25.9.tgz", + "integrity": "sha512-kSMlyUVdWe25rEsRGviIgOWnoT/nfABVWlqt9N19/dIPWViAOW2s9wznP5tURbs/IDuNk4gPy3YdYRgH3uxhBw==", "dev": true, "engines": { "node": ">=6.9.0" } }, - "node_modules/@babel/helper-simple-access": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.24.7.tgz", - "integrity": "sha512-zBAIvbCMh5Ts+b86r/CjU+4XGYIs+R1j951gxI3KmmxBMhCg4oQMsv6ZXQ64XOm/cvzfU1FmoCyt6+owc5QMYg==", - "dev": true, - "dependencies": { - "@babel/traverse": "^7.24.7", - "@babel/types": "^7.24.7" - }, - "engines": { - "node": ">=6.9.0" - } - }, "node_modules/@babel/helper-string-parser": { - "version": "7.24.8", - "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.24.8.tgz", - "integrity": "sha512-pO9KhhRcuUyGnJWwyEgnRJTSIZHiT+vMD0kPeD+so0l7mxkMT19g3pjY9GTnHySck/hDzq+dtW/4VgnMkippsQ==", + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.25.9.tgz", + "integrity": "sha512-4A/SCr/2KLd5jrtOMFzaKjVtAei3+2r/NChoBNoZ3EyP/+GlhoaEGoWOZUmFmoITP7zOJyHIMm+DYRd8o3PvHA==", "dev": true, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-validator-identifier": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.24.7.tgz", - "integrity": "sha512-rR+PBcQ1SMQDDyF6X0wxtG8QyLCgUB0eRAGguqRLfkCA87l7yAP7ehq8SNj96OOGTO8OBV70KhuFYcIkHXOg0w==", + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.25.9.tgz", + "integrity": "sha512-Ed61U6XJc3CVRfkERJWDz4dJwKe7iLmmJsbOGu9wSloNSFttHV0I8g6UAgb7qnK5ly5bGLPd4oXZlxCdANBOWQ==", "dev": true, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-validator-option": { - "version": "7.24.8", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.24.8.tgz", - "integrity": "sha512-xb8t9tD1MHLungh/AIoWYN+gVHaB9kwlu8gffXGSt3FFEIT7RjS+xWbc2vUD1UTZdIpKj/ab3rdqJ7ufngyi2Q==", + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.25.9.tgz", + "integrity": "sha512-e/zv1co8pp55dNdEcCynfj9X7nyUKUXoUEwfXqaZt0omVOmDe9oOTdKStH4GmAw6zxMFs50ZayuMfHDKlO7Tfw==", "dev": true, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helpers": { - "version": "7.25.0", - "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.25.0.tgz", - "integrity": "sha512-MjgLZ42aCm0oGjJj8CtSM3DB8NOOf8h2l7DCTePJs29u+v7yO/RBX9nShlKMgFnRks/Q4tBAe7Hxnov9VkGwLw==", + "version": "7.26.0", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.26.0.tgz", + "integrity": "sha512-tbhNuIxNcVb21pInl3ZSjksLCvgdZy9KwJ8brv993QtIVKJBBkYXz4q4ZbAv31GdnC+R90np23L5FbEBlthAEw==", "dev": true, "dependencies": { - "@babel/template": "^7.25.0", - "@babel/types": "^7.25.0" + "@babel/template": "^7.25.9", + "@babel/types": "^7.26.0" }, "engines": { "node": ">=6.9.0" } }, - "node_modules/@babel/highlight": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.24.7.tgz", - "integrity": "sha512-EStJpq4OuY8xYfhGVXngigBJRWxftKX9ksiGDnmlY3o7B/V7KIAc9X4oiK87uPJSc/vs5L869bem5fhZa8caZw==", - "dev": true, - "dependencies": { - "@babel/helper-validator-identifier": "^7.24.7", - "chalk": "^2.4.2", - "js-tokens": "^4.0.0", - "picocolors": "^1.0.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/highlight/node_modules/ansi-styles": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", - "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", - "dev": true, - "dependencies": { - "color-convert": "^1.9.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/@babel/highlight/node_modules/chalk": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", - "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", - "dev": true, - "dependencies": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/@babel/highlight/node_modules/color-convert": { - "version": "1.9.3", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", - "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", - "dev": true, - "dependencies": { - "color-name": "1.1.3" - } - }, - "node_modules/@babel/highlight/node_modules/color-name": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", - "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", - "dev": true - }, - "node_modules/@babel/highlight/node_modules/escape-string-regexp": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", - "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", - "dev": true, - "engines": { - "node": ">=0.8.0" - } - }, - "node_modules/@babel/highlight/node_modules/has-flag": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", - "dev": true, - "engines": { - "node": ">=4" - } - }, - "node_modules/@babel/highlight/node_modules/supports-color": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", - "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", - "dev": true, - "dependencies": { - "has-flag": "^3.0.0" - }, - "engines": { - "node": ">=4" - } - }, "node_modules/@babel/parser": { - "version": "7.25.3", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.25.3.tgz", - "integrity": "sha512-iLTJKDbJ4hMvFPgQwwsVoxtHyWpKKPBrxkANrSYewDPaPpT5py5yeVkgPIJ7XYXhndxJpaA3PyALSXQ7u8e/Dw==", + "version": "7.26.1", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.26.1.tgz", + "integrity": "sha512-reoQYNiAJreZNsJzyrDNzFQ+IQ5JFiIzAHJg9bn94S3l+4++J7RsIhNMoB+lgP/9tpmiAQqspv+xfdxTSzREOw==", "dev": true, "dependencies": { - "@babel/types": "^7.25.2" + "@babel/types": "^7.26.0" }, "bin": { "parser": "bin/babel-parser.js" @@ -538,12 +435,12 @@ } }, "node_modules/@babel/plugin-syntax-import-attributes": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.24.7.tgz", - "integrity": "sha512-hbX+lKKeUMGihnK8nvKqmXBInriT3GVjzXKFriV3YC6APGxMbP8RZNFwy91+hocLXq90Mta+HshoB31802bb8A==", + "version": "7.26.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.26.0.tgz", + "integrity": "sha512-e2dttdsJ1ZTpi3B9UYGLw41hifAubg19AtCu/2I/F1QNVclOBr1dYpTdmdyZ84Xiz43BS/tCUkMAZNLv12Pi+A==", "dev": true, "dependencies": { - "@babel/helper-plugin-utils": "^7.24.7" + "@babel/helper-plugin-utils": "^7.25.9" }, "engines": { "node": ">=6.9.0" @@ -576,6 +473,21 @@ "@babel/core": "^7.0.0-0" } }, + "node_modules/@babel/plugin-syntax-jsx": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.25.9.tgz", + "integrity": "sha512-ld6oezHQMZsZfp6pWtbjaNDF2tiiCYYDqQszHt5VV437lewP9aSi2Of99CK0D0XB21k7FLgnLcmQKyKzynfeAA==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, "node_modules/@babel/plugin-syntax-logical-assignment-operators": { "version": "7.10.4", "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-logical-assignment-operators/-/plugin-syntax-logical-assignment-operators-7.10.4.tgz", @@ -679,12 +591,12 @@ } }, "node_modules/@babel/plugin-syntax-typescript": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.24.7.tgz", - "integrity": "sha512-c/+fVeJBB0FeKsFvwytYiUD+LBvhHjGSI0g446PRGdSVGZLRNArBUno2PETbAly3tpiNAQR5XaZ+JslxkotsbA==", + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.25.9.tgz", + "integrity": "sha512-hjMgRy5hb8uJJjUcdWunWVcoi9bGpJp8p5Ol1229PoN6aytsLwNMgmdftO23wnCLMfVmTwZDWMPNq/D1SY60JQ==", "dev": true, "dependencies": { - "@babel/helper-plugin-utils": "^7.24.7" + "@babel/helper-plugin-utils": "^7.25.9" }, "engines": { "node": ">=6.9.0" @@ -694,30 +606,30 @@ } }, "node_modules/@babel/template": { - "version": "7.25.0", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.25.0.tgz", - "integrity": "sha512-aOOgh1/5XzKvg1jvVz7AVrx2piJ2XBi227DHmbY6y+bM9H2FlN+IfecYu4Xl0cNiiVejlsCri89LUsbj8vJD9Q==", + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.25.9.tgz", + "integrity": "sha512-9DGttpmPvIxBb/2uwpVo3dqJ+O6RooAFOS+lB+xDqoE2PVCE8nfoHMdZLpfCQRLwvohzXISPZcgxt80xLfsuwg==", "dev": true, "dependencies": { - "@babel/code-frame": "^7.24.7", - "@babel/parser": "^7.25.0", - "@babel/types": "^7.25.0" + "@babel/code-frame": "^7.25.9", + "@babel/parser": "^7.25.9", + "@babel/types": "^7.25.9" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/traverse": { - "version": "7.25.3", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.25.3.tgz", - "integrity": "sha512-HefgyP1x754oGCsKmV5reSmtV7IXj/kpaE1XYY+D9G5PvKKoFfSbiS4M77MdjuwlZKDIKFCffq9rPU+H/s3ZdQ==", + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.25.9.tgz", + "integrity": "sha512-ZCuvfwOwlz/bawvAuvcj8rrithP2/N55Tzz342AkTvq4qaWbGfmCk/tKhNaV2cthijKrPAA8SRJV5WWe7IBMJw==", "dev": true, "dependencies": { - "@babel/code-frame": "^7.24.7", - "@babel/generator": "^7.25.0", - "@babel/parser": "^7.25.3", - "@babel/template": "^7.25.0", - "@babel/types": "^7.25.2", + "@babel/code-frame": "^7.25.9", + "@babel/generator": "^7.25.9", + "@babel/parser": "^7.25.9", + "@babel/template": "^7.25.9", + "@babel/types": "^7.25.9", "debug": "^4.3.1", "globals": "^11.1.0" }, @@ -735,14 +647,13 @@ } }, "node_modules/@babel/types": { - "version": "7.25.2", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.25.2.tgz", - "integrity": "sha512-YTnYtra7W9e6/oAZEHj0bJehPRUlLH9/fbpT5LfB0NhQXyALCRkRs3zH9v07IYhkgpqX6Z78FnuccZr/l4Fs4Q==", + "version": "7.26.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.26.0.tgz", + "integrity": "sha512-Z/yiTPj+lDVnF7lWeKCIJzaIkI0vYO87dMpZ4bg4TDrFe4XXLFWL1TbXU27gBP3QccxV9mZICCrnjnYlJjXHOA==", "dev": true, "dependencies": { - "@babel/helper-string-parser": "^7.24.8", - "@babel/helper-validator-identifier": "^7.24.7", - "to-fast-properties": "^2.0.0" + "@babel/helper-string-parser": "^7.25.9", + "@babel/helper-validator-identifier": "^7.25.9" }, "engines": { "node": ">=6.9.0" @@ -1044,60 +955,59 @@ } }, "node_modules/@jest/console": { - "version": "28.1.3", - "resolved": "https://registry.npmjs.org/@jest/console/-/console-28.1.3.tgz", - "integrity": "sha512-QPAkP5EwKdK/bxIr6C1I4Vs0rm2nHiANzj/Z5X2JQkrZo6IqvC4ldZ9K95tF0HdidhA8Bo6egxSzUFPYKcEXLw==", + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/console/-/console-29.7.0.tgz", + "integrity": "sha512-5Ni4CU7XHQi32IJ398EEP4RrB8eV09sXP2ROqD4bksHrnTree52PsxvX8tpL8LvTZ3pFzXyPbNQReSN41CAhOg==", "dev": true, "dependencies": { - "@jest/types": "^28.1.3", + "@jest/types": "^29.6.3", "@types/node": "*", "chalk": "^4.0.0", - "jest-message-util": "^28.1.3", - "jest-util": "^28.1.3", + "jest-message-util": "^29.7.0", + "jest-util": "^29.7.0", "slash": "^3.0.0" }, "engines": { - "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0" + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, "node_modules/@jest/core": { - "version": "28.1.3", - "resolved": "https://registry.npmjs.org/@jest/core/-/core-28.1.3.tgz", - "integrity": "sha512-CIKBrlaKOzA7YG19BEqCw3SLIsEwjZkeJzf5bdooVnW4bH5cktqe3JX+G2YV1aK5vP8N9na1IGWFzYaTp6k6NA==", + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/core/-/core-29.7.0.tgz", + "integrity": "sha512-n7aeXWKMnGtDA48y8TLWJPJmLmmZ642Ceo78cYWEpiD7FzDgmNDV/GCVRorPABdXLJZ/9wzzgZAlHjXjxDHGsg==", "dev": true, "dependencies": { - "@jest/console": "^28.1.3", - "@jest/reporters": "^28.1.3", - "@jest/test-result": "^28.1.3", - "@jest/transform": "^28.1.3", - "@jest/types": "^28.1.3", + "@jest/console": "^29.7.0", + "@jest/reporters": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", "@types/node": "*", "ansi-escapes": "^4.2.1", "chalk": "^4.0.0", "ci-info": "^3.2.0", "exit": "^0.1.2", "graceful-fs": "^4.2.9", - "jest-changed-files": "^28.1.3", - "jest-config": "^28.1.3", - "jest-haste-map": "^28.1.3", - "jest-message-util": "^28.1.3", - "jest-regex-util": "^28.0.2", - "jest-resolve": "^28.1.3", - "jest-resolve-dependencies": "^28.1.3", - "jest-runner": "^28.1.3", - "jest-runtime": "^28.1.3", - "jest-snapshot": "^28.1.3", - "jest-util": "^28.1.3", - "jest-validate": "^28.1.3", - "jest-watcher": "^28.1.3", + "jest-changed-files": "^29.7.0", + "jest-config": "^29.7.0", + "jest-haste-map": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-regex-util": "^29.6.3", + "jest-resolve": "^29.7.0", + "jest-resolve-dependencies": "^29.7.0", + "jest-runner": "^29.7.0", + "jest-runtime": "^29.7.0", + "jest-snapshot": "^29.7.0", + "jest-util": "^29.7.0", + "jest-validate": "^29.7.0", + "jest-watcher": "^29.7.0", "micromatch": "^4.0.4", - "pretty-format": "^28.1.3", - "rimraf": "^3.0.0", + "pretty-format": "^29.7.0", "slash": "^3.0.0", "strip-ansi": "^6.0.0" }, "engines": { - "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0" + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" }, "peerDependencies": { "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" @@ -1109,88 +1019,98 @@ } }, "node_modules/@jest/environment": { - "version": "28.1.3", - "resolved": "https://registry.npmjs.org/@jest/environment/-/environment-28.1.3.tgz", - "integrity": "sha512-1bf40cMFTEkKyEf585R9Iz1WayDjHoHqvts0XFYEqyKM3cFWDpeMoqKKTAF9LSYQModPUlh8FKptoM2YcMWAXA==", + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/environment/-/environment-29.7.0.tgz", + "integrity": "sha512-aQIfHDq33ExsN4jP1NWGXhxgQ/wixs60gDiKO+XVMd8Mn0NWPWgc34ZQDTb2jKaUWQ7MuwoitXAsN2XVXNMpAw==", "dev": true, "dependencies": { - "@jest/fake-timers": "^28.1.3", - "@jest/types": "^28.1.3", + "@jest/fake-timers": "^29.7.0", + "@jest/types": "^29.6.3", "@types/node": "*", - "jest-mock": "^28.1.3" + "jest-mock": "^29.7.0" }, "engines": { - "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0" + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, "node_modules/@jest/expect": { - "version": "28.1.3", - "resolved": "https://registry.npmjs.org/@jest/expect/-/expect-28.1.3.tgz", - "integrity": "sha512-lzc8CpUbSoE4dqT0U+g1qODQjBRHPpCPXissXD4mS9+sWQdmmpeJ9zSH1rS1HEkrsMN0fb7nKrJ9giAR1d3wBw==", + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/expect/-/expect-29.7.0.tgz", + "integrity": "sha512-8uMeAMycttpva3P1lBHB8VciS9V0XAr3GymPpipdyQXbBcuhkLQOSe8E/p92RyAdToS6ZD1tFkX+CkhoECE0dQ==", "dev": true, "dependencies": { - "expect": "^28.1.3", - "jest-snapshot": "^28.1.3" + "expect": "^29.7.0", + "jest-snapshot": "^29.7.0" }, "engines": { - "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0" + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, "node_modules/@jest/expect-utils": { - "version": "28.1.3", - "resolved": "https://registry.npmjs.org/@jest/expect-utils/-/expect-utils-28.1.3.tgz", - "integrity": "sha512-wvbi9LUrHJLn3NlDW6wF2hvIMtd4JUl2QNVrjq+IBSHirgfrR3o9RnVtxzdEGO2n9JyIWwHnLfby5KzqBGg2YA==", + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/expect-utils/-/expect-utils-29.7.0.tgz", + "integrity": "sha512-GlsNBWiFQFCVi9QVSx7f5AgMeLxe9YCCs5PuP2O2LdjDAA8Jh9eX7lA1Jq/xdXw3Wb3hyvlFNfZIfcRetSzYcA==", "dev": true, "dependencies": { - "jest-get-type": "^28.0.2" + "jest-get-type": "^29.6.3" }, "engines": { - "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0" + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/expect-utils/node_modules/jest-get-type": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-29.6.3.tgz", + "integrity": "sha512-zrteXnqYxfQh7l5FHyL38jL39di8H8rHoecLH3JNxH3BwOrBsNeabdap5e0I23lD4HHI8W5VFBZqG4Eaq5LNcw==", + "dev": true, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, "node_modules/@jest/fake-timers": { - "version": "28.1.3", - "resolved": "https://registry.npmjs.org/@jest/fake-timers/-/fake-timers-28.1.3.tgz", - "integrity": "sha512-D/wOkL2POHv52h+ok5Oj/1gOG9HSywdoPtFsRCUmlCILXNn5eIWmcnd3DIiWlJnpGvQtmajqBP95Ei0EimxfLw==", + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/fake-timers/-/fake-timers-29.7.0.tgz", + "integrity": "sha512-q4DH1Ha4TTFPdxLsqDXK1d3+ioSL7yL5oCMJZgDYm6i+6CygW5E5xVr/D1HdsGxjt1ZWSfUAs9OxSB/BNelWrQ==", "dev": true, "dependencies": { - "@jest/types": "^28.1.3", - "@sinonjs/fake-timers": "^9.1.2", + "@jest/types": "^29.6.3", + "@sinonjs/fake-timers": "^10.0.2", "@types/node": "*", - "jest-message-util": "^28.1.3", - "jest-mock": "^28.1.3", - "jest-util": "^28.1.3" + "jest-message-util": "^29.7.0", + "jest-mock": "^29.7.0", + "jest-util": "^29.7.0" }, "engines": { - "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0" + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, "node_modules/@jest/globals": { - "version": "28.1.3", - "resolved": "https://registry.npmjs.org/@jest/globals/-/globals-28.1.3.tgz", - "integrity": "sha512-XFU4P4phyryCXu1pbcqMO0GSQcYe1IsalYCDzRNyhetyeyxMcIxa11qPNDpVNLeretItNqEmYYQn1UYz/5x1NA==", + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/globals/-/globals-29.7.0.tgz", + "integrity": "sha512-mpiz3dutLbkW2MNFubUGUEVLkTGiqW6yLVTA+JbP6fI6J5iL9Y0Nlg8k95pcF8ctKwCS7WVxteBs29hhfAotzQ==", "dev": true, "dependencies": { - "@jest/environment": "^28.1.3", - "@jest/expect": "^28.1.3", - "@jest/types": "^28.1.3" + "@jest/environment": "^29.7.0", + "@jest/expect": "^29.7.0", + "@jest/types": "^29.6.3", + "jest-mock": "^29.7.0" }, "engines": { - "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0" + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, "node_modules/@jest/reporters": { - "version": "28.1.3", - "resolved": "https://registry.npmjs.org/@jest/reporters/-/reporters-28.1.3.tgz", - "integrity": "sha512-JuAy7wkxQZVNU/V6g9xKzCGC5LVXx9FDcABKsSXp5MiKPEE2144a/vXTEDoyzjUpZKfVwp08Wqg5A4WfTMAzjg==", + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/reporters/-/reporters-29.7.0.tgz", + "integrity": "sha512-DApq0KJbJOEzAFYjHADNNxAE3KbhxQB1y5Kplb5Waqw6zVbuWatSnMjE5gs8FUgEPmNsnZA3NCWl9NG0ia04Pg==", "dev": true, "dependencies": { "@bcoe/v8-coverage": "^0.2.3", - "@jest/console": "^28.1.3", - "@jest/test-result": "^28.1.3", - "@jest/transform": "^28.1.3", - "@jest/types": "^28.1.3", - "@jridgewell/trace-mapping": "^0.3.13", + "@jest/console": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "@jridgewell/trace-mapping": "^0.3.18", "@types/node": "*", "chalk": "^4.0.0", "collect-v8-coverage": "^1.0.0", @@ -1198,21 +1118,20 @@ "glob": "^7.1.3", "graceful-fs": "^4.2.9", "istanbul-lib-coverage": "^3.0.0", - "istanbul-lib-instrument": "^5.1.0", + "istanbul-lib-instrument": "^6.0.0", "istanbul-lib-report": "^3.0.0", "istanbul-lib-source-maps": "^4.0.0", "istanbul-reports": "^3.1.3", - "jest-message-util": "^28.1.3", - "jest-util": "^28.1.3", - "jest-worker": "^28.1.3", + "jest-message-util": "^29.7.0", + "jest-util": "^29.7.0", + "jest-worker": "^29.7.0", "slash": "^3.0.0", "string-length": "^4.0.1", "strip-ansi": "^6.0.0", - "terminal-link": "^2.0.0", "v8-to-istanbul": "^9.0.1" }, "engines": { - "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0" + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" }, "peerDependencies": { "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" @@ -1224,94 +1143,94 @@ } }, "node_modules/@jest/schemas": { - "version": "28.1.3", - "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-28.1.3.tgz", - "integrity": "sha512-/l/VWsdt/aBXgjshLWOFyFt3IVdYypu5y2Wn2rOO1un6nkqIn8SLXzgIMYXFyYsRWDyF5EthmKJMIdJvk08grg==", + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-29.6.3.tgz", + "integrity": "sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==", "dev": true, "dependencies": { - "@sinclair/typebox": "^0.24.1" + "@sinclair/typebox": "^0.27.8" }, "engines": { - "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0" + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, "node_modules/@jest/source-map": { - "version": "28.1.2", - "resolved": "https://registry.npmjs.org/@jest/source-map/-/source-map-28.1.2.tgz", - "integrity": "sha512-cV8Lx3BeStJb8ipPHnqVw/IM2VCMWO3crWZzYodSIkxXnRcXJipCdx1JCK0K5MsJJouZQTH73mzf4vgxRaH9ww==", + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/source-map/-/source-map-29.6.3.tgz", + "integrity": "sha512-MHjT95QuipcPrpLM+8JMSzFx6eHp5Bm+4XeFDJlwsvVBjmKNiIAvasGK2fxz2WbGRlnvqehFbh07MMa7n3YJnw==", "dev": true, "dependencies": { - "@jridgewell/trace-mapping": "^0.3.13", + "@jridgewell/trace-mapping": "^0.3.18", "callsites": "^3.0.0", "graceful-fs": "^4.2.9" }, "engines": { - "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0" + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, "node_modules/@jest/test-result": { - "version": "28.1.3", - "resolved": "https://registry.npmjs.org/@jest/test-result/-/test-result-28.1.3.tgz", - "integrity": "sha512-kZAkxnSE+FqE8YjW8gNuoVkkC9I7S1qmenl8sGcDOLropASP+BkcGKwhXoyqQuGOGeYY0y/ixjrd/iERpEXHNg==", + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/test-result/-/test-result-29.7.0.tgz", + "integrity": "sha512-Fdx+tv6x1zlkJPcWXmMDAG2HBnaR9XPSd5aDWQVsfrZmLVT3lU1cwyxLgRmXR9yrq4NBoEm9BMsfgFzTQAbJYA==", "dev": true, "dependencies": { - "@jest/console": "^28.1.3", - "@jest/types": "^28.1.3", + "@jest/console": "^29.7.0", + "@jest/types": "^29.6.3", "@types/istanbul-lib-coverage": "^2.0.0", "collect-v8-coverage": "^1.0.0" }, "engines": { - "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0" + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, "node_modules/@jest/test-sequencer": { - "version": "28.1.3", - "resolved": "https://registry.npmjs.org/@jest/test-sequencer/-/test-sequencer-28.1.3.tgz", - "integrity": "sha512-NIMPEqqa59MWnDi1kvXXpYbqsfQmSJsIbnd85mdVGkiDfQ9WQQTXOLsvISUfonmnBT+w85WEgneCigEEdHDFxw==", + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/test-sequencer/-/test-sequencer-29.7.0.tgz", + "integrity": "sha512-GQwJ5WZVrKnOJuiYiAF52UNUJXgTZx1NHjFSEB0qEMmSZKAkdMoIzw/Cj6x6NF4AvV23AUqDpFzQkN/eYCYTxw==", "dev": true, "dependencies": { - "@jest/test-result": "^28.1.3", + "@jest/test-result": "^29.7.0", "graceful-fs": "^4.2.9", - "jest-haste-map": "^28.1.3", + "jest-haste-map": "^29.7.0", "slash": "^3.0.0" }, "engines": { - "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0" + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, "node_modules/@jest/transform": { - "version": "28.1.3", - "resolved": "https://registry.npmjs.org/@jest/transform/-/transform-28.1.3.tgz", - "integrity": "sha512-u5dT5di+oFI6hfcLOHGTAfmUxFRrjK+vnaP0kkVow9Md/M7V/MxqQMOz/VV25UZO8pzeA9PjfTpOu6BDuwSPQA==", + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/transform/-/transform-29.7.0.tgz", + "integrity": "sha512-ok/BTPFzFKVMwO5eOHRrvnBVHdRy9IrsrW1GpMaQ9MCnilNLXQKmAX8s1YXDFaai9xJpac2ySzV0YeRRECr2Vw==", "dev": true, "dependencies": { "@babel/core": "^7.11.6", - "@jest/types": "^28.1.3", - "@jridgewell/trace-mapping": "^0.3.13", + "@jest/types": "^29.6.3", + "@jridgewell/trace-mapping": "^0.3.18", "babel-plugin-istanbul": "^6.1.1", "chalk": "^4.0.0", - "convert-source-map": "^1.4.0", - "fast-json-stable-stringify": "^2.0.0", + "convert-source-map": "^2.0.0", + "fast-json-stable-stringify": "^2.1.0", "graceful-fs": "^4.2.9", - "jest-haste-map": "^28.1.3", - "jest-regex-util": "^28.0.2", - "jest-util": "^28.1.3", + "jest-haste-map": "^29.7.0", + "jest-regex-util": "^29.6.3", + "jest-util": "^29.7.0", "micromatch": "^4.0.4", "pirates": "^4.0.4", "slash": "^3.0.0", - "write-file-atomic": "^4.0.1" + "write-file-atomic": "^4.0.2" }, "engines": { - "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0" + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, "node_modules/@jest/types": { - "version": "28.1.3", - "resolved": "https://registry.npmjs.org/@jest/types/-/types-28.1.3.tgz", - "integrity": "sha512-RyjiyMUZrKz/c+zlMFO1pm70DcIlST8AeWTkoUdZevew44wcNZQHsEVOiCVtgVnlFFD82FPaXycys58cf2muVQ==", + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-29.6.3.tgz", + "integrity": "sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw==", "dev": true, "dependencies": { - "@jest/schemas": "^28.1.3", + "@jest/schemas": "^29.6.3", "@types/istanbul-lib-coverage": "^2.0.0", "@types/istanbul-reports": "^3.0.0", "@types/node": "*", @@ -1319,7 +1238,7 @@ "chalk": "^4.0.0" }, "engines": { - "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0" + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, "node_modules/@jridgewell/gen-mapping": { @@ -1942,27 +1861,87 @@ } }, "node_modules/@sinclair/typebox": { - "version": "0.24.51", - "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.24.51.tgz", - "integrity": "sha512-1P1OROm/rdubP5aFDSZQILU0vrLCJ4fvHt6EoqHEM+2D/G5MK3bIaymUKLit8Js9gbns5UyJnkP/TZROLw4tUA==", + "version": "0.27.8", + "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.8.tgz", + "integrity": "sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==", "dev": true }, "node_modules/@sinonjs/commons": { - "version": "1.8.6", - "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-1.8.6.tgz", - "integrity": "sha512-Ky+XkAkqPZSm3NLBeUng77EBQl3cmeJhITaGHdYH8kjVB+aun3S4XBRti2zt17mtt0mIUDiNxYeoJm6drVvBJQ==", + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-3.0.1.tgz", + "integrity": "sha512-K3mCHKQ9sVh8o1C9cxkwxaOmXoAMlDxC1mYyHrjqOWEcBjYr76t96zL2zlj5dUGZ3HSw240X1qgH3Mjf1yJWpQ==", "dev": true, "dependencies": { "type-detect": "4.0.8" } }, "node_modules/@sinonjs/fake-timers": { - "version": "9.1.2", - "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-9.1.2.tgz", - "integrity": "sha512-BPS4ynJW/o92PUR4wgriz2Ud5gpST5vz6GQfMixEDK0Z8ZCUv2M7SkBLykH56T++Xs+8ln9zTGbOvNGIe02/jw==", + "version": "10.3.0", + "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-10.3.0.tgz", + "integrity": "sha512-V4BG07kuYSUkTCSBHG8G8TNhM+F19jXFWnQtzj+we8DrkpSBCee9Z3Ms8yiGer/dlmhe35/Xdgyo3/0rQKg7YA==", "dev": true, "dependencies": { - "@sinonjs/commons": "^1.7.0" + "@sinonjs/commons": "^3.0.0" + } + }, + "node_modules/@slack/logger": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@slack/logger/-/logger-4.0.0.tgz", + "integrity": "sha512-Wz7QYfPAlG/DR+DfABddUZeNgoeY7d1J39OCR2jR+v7VBsB8ezulDK5szTnDDPDwLH5IWhLvXIHlCFZV7MSKgA==", + "dependencies": { + "@types/node": ">=18.0.0" + }, + "engines": { + "node": ">= 18", + "npm": ">= 8.6.0" + } + }, + "node_modules/@slack/logger/node_modules/@types/node": { + "version": "22.7.9", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.7.9.tgz", + "integrity": "sha512-jrTfRC7FM6nChvU7X2KqcrgquofrWLFDeYC1hKfwNWomVvrn7JIksqf344WN2X/y8xrgqBd2dJATZV4GbatBfg==", + "dependencies": { + "undici-types": "~6.19.2" + } + }, + "node_modules/@slack/types": { + "version": "2.14.0", + "resolved": "https://registry.npmjs.org/@slack/types/-/types-2.14.0.tgz", + "integrity": "sha512-n0EGm7ENQRxlXbgKSrQZL69grzg1gHLAVd+GlRVQJ1NSORo0FrApR7wql/gaKdu2n4TO83Sq/AmeUOqD60aXUA==", + "engines": { + "node": ">= 12.13.0", + "npm": ">= 6.12.0" + } + }, + "node_modules/@slack/web-api": { + "version": "7.7.0", + "resolved": "https://registry.npmjs.org/@slack/web-api/-/web-api-7.7.0.tgz", + "integrity": "sha512-DtRyjgQi0mObA2uC6H8nL2OhAISKDhvtOXgRjGRBnBhiaWb6df5vPmKHsOHjpweYALBMHtiqE5ajZFkDW/ag8Q==", + "dependencies": { + "@slack/logger": "^4.0.0", + "@slack/types": "^2.9.0", + "@types/node": ">=18.0.0", + "@types/retry": "0.12.0", + "axios": "^1.7.4", + "eventemitter3": "^5.0.1", + "form-data": "^4.0.0", + "is-electron": "2.2.2", + "is-stream": "^2", + "p-queue": "^6", + "p-retry": "^4", + "retry": "^0.13.1" + }, + "engines": { + "node": ">= 18", + "npm": ">= 8.6.0" + } + }, + "node_modules/@slack/web-api/node_modules/@types/node": { + "version": "22.7.9", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.7.9.tgz", + "integrity": "sha512-jrTfRC7FM6nChvU7X2KqcrgquofrWLFDeYC1hKfwNWomVvrn7JIksqf344WN2X/y8xrgqBd2dJATZV4GbatBfg==", + "dependencies": { + "undici-types": "~6.19.2" } }, "node_modules/@tsconfig/node10": { @@ -2185,13 +2164,13 @@ } }, "node_modules/@types/jest": { - "version": "28.1.4", - "resolved": "https://registry.npmjs.org/@types/jest/-/jest-28.1.4.tgz", - "integrity": "sha512-telv6G5N7zRJiLcI3Rs3o+ipZ28EnE+7EvF0pSrt2pZOMnAVI/f+6/LucDxOvcBcTeTL3JMF744BbVQAVBUQRA==", + "version": "29.5.14", + "resolved": "https://registry.npmjs.org/@types/jest/-/jest-29.5.14.tgz", + "integrity": "sha512-ZN+4sdnLUbo8EVvVc2ao0GFW6oVrQRPn4K2lglySj7APvSrgzxHiNNK99us4WDMi57xxA2yggblIAMNhXOotLQ==", "dev": true, "dependencies": { - "jest-matcher-utils": "^28.0.0", - "pretty-format": "^28.0.0" + "expect": "^29.0.0", + "pretty-format": "^29.0.0" } }, "node_modules/@types/json-schema": { @@ -2275,12 +2254,6 @@ "@types/passport": "*" } }, - "node_modules/@types/prettier": { - "version": "2.7.3", - "resolved": "https://registry.npmjs.org/@types/prettier/-/prettier-2.7.3.tgz", - "integrity": "sha512-+68kP9yzs4LMp7VNh8gdzMSPZFL44MLGqiHWvttYJe+6qnuVr4Ek9wSBQoveqY/r+LwjCcU29kNVkidwim+kYA==", - "dev": true - }, "node_modules/@types/qs": { "version": "6.9.15", "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.15.tgz", @@ -2291,6 +2264,11 @@ "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.7.tgz", "integrity": "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==" }, + "node_modules/@types/retry": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/@types/retry/-/retry-0.12.0.tgz", + "integrity": "sha512-wWKOClTTiizcZhXnPY4wikVAwmdYHp8q6DmC+EJUzAMsycb7HB32Kh9RN4+0gExjmPmZSAQjgURXIGATPegAvA==" + }, "node_modules/@types/semver": { "version": "7.5.8", "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.5.8.tgz", @@ -2984,6 +2962,12 @@ "integrity": "sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA==", "dev": true }, + "node_modules/async": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz", + "integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==", + "dev": true + }, "node_modules/asynckit": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", @@ -3000,21 +2984,21 @@ } }, "node_modules/babel-jest": { - "version": "28.1.3", - "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-28.1.3.tgz", - "integrity": "sha512-epUaPOEWMk3cWX0M/sPvCHHCe9fMFAa/9hXEgKP8nFfNl/jlGkE9ucq9NqkZGXLDduCJYS0UvSlPUwC0S+rH6Q==", + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-29.7.0.tgz", + "integrity": "sha512-BrvGY3xZSwEcCzKvKsCi2GgHqDqsYkOP4/by5xCgIwGXQxIEh+8ew3gmrE1y7XRR6LHZIj6yLYnUi/mm2KXKBg==", "dev": true, "dependencies": { - "@jest/transform": "^28.1.3", + "@jest/transform": "^29.7.0", "@types/babel__core": "^7.1.14", "babel-plugin-istanbul": "^6.1.1", - "babel-preset-jest": "^28.1.3", + "babel-preset-jest": "^29.6.3", "chalk": "^4.0.0", "graceful-fs": "^4.2.9", "slash": "^3.0.0" }, "engines": { - "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0" + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" }, "peerDependencies": { "@babel/core": "^7.8.0" @@ -3036,10 +3020,35 @@ "node": ">=8" } }, + "node_modules/babel-plugin-istanbul/node_modules/istanbul-lib-instrument": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-5.2.1.tgz", + "integrity": "sha512-pzqtp31nLv/XFOzXGuvhCb8qhjmTVo5vjVk19XE4CRlSWz0KoeJ3bw9XsA7nOp9YBf4qHjwBxkDzKcME/J29Yg==", + "dev": true, + "dependencies": { + "@babel/core": "^7.12.3", + "@babel/parser": "^7.14.7", + "@istanbuljs/schema": "^0.1.2", + "istanbul-lib-coverage": "^3.2.0", + "semver": "^6.3.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/babel-plugin-istanbul/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + } + }, "node_modules/babel-plugin-jest-hoist": { - "version": "28.1.3", - "resolved": "https://registry.npmjs.org/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-28.1.3.tgz", - "integrity": "sha512-Ys3tUKAmfnkRUpPdpa98eYrAR0nV+sSFUZZEGuQ2EbFd1y4SOLtD5QDNHAq+bb9a+bbXvYQC4b+ID/THIMcU6Q==", + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-29.6.3.tgz", + "integrity": "sha512-ESAc/RJvGTFEzRwOTT4+lNDk/GNHMkKbNzsvT0qKRfDyyYTskxB5rnU2njIDYVxXCBHHEI1c0YwHob3WaYujOg==", "dev": true, "dependencies": { "@babel/template": "^7.3.3", @@ -3048,7 +3057,7 @@ "@types/babel__traverse": "^7.0.6" }, "engines": { - "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0" + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, "node_modules/babel-preset-current-node-syntax": { @@ -3078,16 +3087,16 @@ } }, "node_modules/babel-preset-jest": { - "version": "28.1.3", - "resolved": "https://registry.npmjs.org/babel-preset-jest/-/babel-preset-jest-28.1.3.tgz", - "integrity": "sha512-L+fupJvlWAHbQfn74coNX3zf60LXMJsezNvvx8eIh7iOR1luJ1poxYgQk1F8PYtNq/6QODDHCqsSnTFSWC491A==", + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/babel-preset-jest/-/babel-preset-jest-29.6.3.tgz", + "integrity": "sha512-0B3bhxR6snWXJZtR/RliHTDPRgn1sNHOR0yVtq/IiQFyuOVjFS+wuio/R4gSNkyYmKmJB4wGZv2NZanmKmTnNA==", "dev": true, "dependencies": { - "babel-plugin-jest-hoist": "^28.1.3", + "babel-plugin-jest-hoist": "^29.6.3", "babel-preset-current-node-syntax": "^1.0.0" }, "engines": { - "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0" + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" }, "peerDependencies": { "@babel/core": "^7.0.0" @@ -3242,9 +3251,9 @@ } }, "node_modules/browserslist": { - "version": "4.23.3", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.23.3.tgz", - "integrity": "sha512-btwCFJVjI4YWDNfau8RhZ+B1Q/VLoUITrm3RlP6y1tYGWIOa+InuYiRGXUBXo8nA1qKmHMyLB/iVQg5TT4eFoA==", + "version": "4.24.2", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.24.2.tgz", + "integrity": "sha512-ZIc+Q62revdMcqC6aChtW4jz3My3klmCO1fEmINZY/8J3EpBg5/A/D0AKmBveUh6pgoeycoMkVMko84tuYS+Gg==", "dev": true, "funding": [ { @@ -3261,10 +3270,10 @@ } ], "dependencies": { - "caniuse-lite": "^1.0.30001646", - "electron-to-chromium": "^1.5.4", + "caniuse-lite": "^1.0.30001669", + "electron-to-chromium": "^1.5.41", "node-releases": "^2.0.18", - "update-browserslist-db": "^1.1.0" + "update-browserslist-db": "^1.1.1" }, "bin": { "browserslist": "cli.js" @@ -3384,9 +3393,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001651", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001651.tgz", - "integrity": "sha512-9Cf+Xv1jJNe1xPZLGuUXLNkE1BoDkqRqYyFJ9TDYSqhduqA4hu4oR9HluGoWYQC/aj8WHjsGVV+bwkh0+tegRg==", + "version": "1.0.30001671", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001671.tgz", + "integrity": "sha512-jocyVaSSfXg2faluE6hrWkMgDOiULBMca4QLtDT39hw1YxaIPHWc1CcTCKkPmHgGH6tKji6ZNbMSmUAvENf2/A==", "dev": true, "funding": [ { @@ -3507,9 +3516,9 @@ } }, "node_modules/cjs-module-lexer": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/cjs-module-lexer/-/cjs-module-lexer-1.3.1.tgz", - "integrity": "sha512-a3KdPAANPbNE4ZUv9h6LckSl9zLsYOP4MBmhIPkRaeyybt+r4UghLvq+xw/YwUcC1gqylCkL4rdVs3Lwupjm4Q==", + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/cjs-module-lexer/-/cjs-module-lexer-1.4.1.tgz", + "integrity": "sha512-cuSVIHi9/9E/+821Qjdvngor+xpnlwnuwIyZOaLmHBVdXL+gP+I6QQB9VkO7RI77YIcTV+S1W9AreJ5eN63JBA==", "dev": true }, "node_modules/class-transformer": { @@ -3809,9 +3818,9 @@ } }, "node_modules/convert-source-map": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.9.0.tgz", - "integrity": "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", "dev": true }, "node_modules/cookie": { @@ -3878,6 +3887,27 @@ "node": ">=10" } }, + "node_modules/create-jest": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/create-jest/-/create-jest-29.7.0.tgz", + "integrity": "sha512-Adz2bdH0Vq3F53KEMJOoftQFutWCukm6J24wbPWRO4k1kMY7gS7ds/uoJkNuV8wDCtWWnuwGcJwpWcih+zEW1Q==", + "dev": true, + "dependencies": { + "@jest/types": "^29.6.3", + "chalk": "^4.0.0", + "exit": "^0.1.2", + "graceful-fs": "^4.2.9", + "jest-config": "^29.7.0", + "jest-util": "^29.7.0", + "prompts": "^2.0.1" + }, + "bin": { + "create-jest": "bin/create-jest.js" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, "node_modules/create-require": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", @@ -4039,10 +4069,18 @@ } }, "node_modules/dedent": { - "version": "0.7.0", - "resolved": "https://registry.npmjs.org/dedent/-/dedent-0.7.0.tgz", - "integrity": "sha512-Q6fKUPqnAHAyhiUgFU7BUzLiv0kd8saH9al7tnu5Q/okj6dnupxyTgFIBjVzJATdfIAm9NAsvXNzjaKa+bxVyA==", - "dev": true + "version": "1.5.3", + "resolved": "https://registry.npmjs.org/dedent/-/dedent-1.5.3.tgz", + "integrity": "sha512-NHQtfOOW68WD8lgypbLA5oT+Bt0xXJhiYvoR6SmmNXZfpzOGXwdKWmcwG8N7PwVVWV3eF/68nmD9BaJSsTBhyQ==", + "dev": true, + "peerDependencies": { + "babel-plugin-macros": "^3.1.0" + }, + "peerDependenciesMeta": { + "babel-plugin-macros": { + "optional": true + } + } }, "node_modules/deep-is": { "version": "0.1.4", @@ -4153,15 +4191,6 @@ "node": ">=0.3.1" } }, - "node_modules/diff-sequences": { - "version": "28.1.1", - "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-28.1.1.tgz", - "integrity": "sha512-FU0iFaH/E23a+a718l8Qa/19bF9p06kgE0KipMOMadwa3SjnaElKzPaUC0vnibs6/B/9ni97s61mcejk8W1fQw==", - "dev": true, - "engines": { - "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0" - } - }, "node_modules/dir-glob": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", @@ -4239,16 +4268,31 @@ "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==" }, + "node_modules/ejs": { + "version": "3.1.10", + "resolved": "https://registry.npmjs.org/ejs/-/ejs-3.1.10.tgz", + "integrity": "sha512-UeJmFfOrAQS8OJWPZ4qtgHyWExa088/MtK5UEyoJGFH67cDEXkZSviOiKRCZ4Xij0zxI3JECgYs3oKx+AizQBA==", + "dev": true, + "dependencies": { + "jake": "^10.8.5" + }, + "bin": { + "ejs": "bin/cli.js" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/electron-to-chromium": { - "version": "1.5.12", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.12.tgz", - "integrity": "sha512-tIhPkdlEoCL1Y+PToq3zRNehUaKp3wBX/sr7aclAWdIWjvqAe/Im/H0SiCM4c1Q8BLPHCdoJTol+ZblflydehA==", + "version": "1.5.47", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.47.tgz", + "integrity": "sha512-zS5Yer0MOYw4rtK2iq43cJagHZ8sXN0jDHDKzB+86gSBSAI4v07S97mcq+Gs2vclAxSh1j7vOAHxSVgduiiuVQ==", "dev": true }, "node_modules/emittery": { - "version": "0.10.2", - "resolved": "https://registry.npmjs.org/emittery/-/emittery-0.10.2.tgz", - "integrity": "sha512-aITqOwnLanpHLNXZJENbOgjUBeHocD+xsSJmNrjovKBW5HbSpW3d1pEls7GFQPUWXiwG9+0P4GtHfEqC/4M0Iw==", + "version": "0.13.1", + "resolved": "https://registry.npmjs.org/emittery/-/emittery-0.13.1.tgz", + "integrity": "sha512-DeWwawk6r5yR9jFgnDKYt4sLS0LmHJJi3ZOnb5/JdbYwj3nW+FxQnHIjhBKz8YLC7oRNPVM9NQ47I3CVx34eqQ==", "dev": true, "engines": { "node": ">=12" @@ -4339,9 +4383,9 @@ "dev": true }, "node_modules/escalade": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.2.tgz", - "integrity": "sha512-ErCHMCae19vR8vQGe50xIsVomy19rg6gFu3+r3jkEO46suLMWBksvVyoGgQV+jOfl84ZSOSlmv6Gxa89PmTGmA==", + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", "dev": true, "engines": { "node": ">=6" @@ -4616,8 +4660,7 @@ "node_modules/eventemitter3": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.1.tgz", - "integrity": "sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==", - "dev": true + "integrity": "sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==" }, "node_modules/events": { "version": "3.3.0", @@ -4661,19 +4704,67 @@ } }, "node_modules/expect": { - "version": "28.1.3", - "resolved": "https://registry.npmjs.org/expect/-/expect-28.1.3.tgz", - "integrity": "sha512-eEh0xn8HlsuOBxFgIss+2mX85VAS4Qy3OSkjV7rlBWljtA4oWH37glVGyOZSZvErDT/yBywZdPGwCXuTvSG85g==", + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/expect/-/expect-29.7.0.tgz", + "integrity": "sha512-2Zks0hf1VLFYI1kbh0I5jP3KHHyCHpkfyHBzsSXRFgl/Bg9mWYfMW8oD+PdMPlEwy5HNsR9JutYy6pMeOh61nw==", "dev": true, "dependencies": { - "@jest/expect-utils": "^28.1.3", - "jest-get-type": "^28.0.2", - "jest-matcher-utils": "^28.1.3", - "jest-message-util": "^28.1.3", - "jest-util": "^28.1.3" + "@jest/expect-utils": "^29.7.0", + "jest-get-type": "^29.6.3", + "jest-matcher-utils": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-util": "^29.7.0" }, "engines": { - "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0" + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/expect/node_modules/diff-sequences": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-29.6.3.tgz", + "integrity": "sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q==", + "dev": true, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/expect/node_modules/jest-diff": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-29.7.0.tgz", + "integrity": "sha512-LMIgiIrhigmPrs03JHpxUh2yISK3vLFPkAodPeo0+BuF7wA2FoQbkEg1u8gBYBThncu7e1oEDUfIXVuTqLRUjw==", + "dev": true, + "dependencies": { + "chalk": "^4.0.0", + "diff-sequences": "^29.6.3", + "jest-get-type": "^29.6.3", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/expect/node_modules/jest-get-type": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-29.6.3.tgz", + "integrity": "sha512-zrteXnqYxfQh7l5FHyL38jL39di8H8rHoecLH3JNxH3BwOrBsNeabdap5e0I23lD4HHI8W5VFBZqG4Eaq5LNcw==", + "dev": true, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/expect/node_modules/jest-matcher-utils": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-29.7.0.tgz", + "integrity": "sha512-sBkD+Xi9DtcChsI3L3u0+N0opgPYnCRPtGcQYrgXmR+hmt/fYfWAL0xRXYU8eWOdfuLgBe0YCW3AFtnRLagq/g==", + "dev": true, + "dependencies": { + "chalk": "^4.0.0", + "jest-diff": "^29.7.0", + "jest-get-type": "^29.6.3", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, "node_modules/express": { @@ -4931,6 +5022,36 @@ "node": "^10.12.0 || >=12.0.0" } }, + "node_modules/filelist": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/filelist/-/filelist-1.0.4.tgz", + "integrity": "sha512-w1cEuf3S+DrLCQL7ET6kz+gmlJdbq9J7yXCSjK/OZCPA+qEN1WyF4ZAf0YYJa4/shHJra2t/d/r8SV4Ji+x+8Q==", + "dev": true, + "dependencies": { + "minimatch": "^5.0.1" + } + }, + "node_modules/filelist/node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/filelist/node_modules/minimatch": { + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", + "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", + "dev": true, + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/fill-range": { "version": "7.1.1", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", @@ -5694,6 +5815,11 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-electron": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/is-electron/-/is-electron-2.2.2.tgz", + "integrity": "sha512-FO/Rhvz5tuw4MCWkpMzHFKWD2LsfHzIb7i6MdPYZ/KW7AlxawyLkqdy+jPZP1WubqEADE3O4FUENlJHDfQASRg==" + }, "node_modules/is-extglob": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", @@ -5767,7 +5893,6 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", - "dev": true, "engines": { "node": ">=8" }, @@ -5807,28 +5932,19 @@ } }, "node_modules/istanbul-lib-instrument": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-5.2.1.tgz", - "integrity": "sha512-pzqtp31nLv/XFOzXGuvhCb8qhjmTVo5vjVk19XE4CRlSWz0KoeJ3bw9XsA7nOp9YBf4qHjwBxkDzKcME/J29Yg==", + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-6.0.3.tgz", + "integrity": "sha512-Vtgk7L/R2JHyyGW07spoFlB8/lpjiOLTjMdms6AFMraYt3BaJauod/NGrfnVG/y4Ix1JEuMRPDPEj2ua+zz1/Q==", "dev": true, "dependencies": { - "@babel/core": "^7.12.3", - "@babel/parser": "^7.14.7", - "@istanbuljs/schema": "^0.1.2", + "@babel/core": "^7.23.9", + "@babel/parser": "^7.23.9", + "@istanbuljs/schema": "^0.1.3", "istanbul-lib-coverage": "^3.2.0", - "semver": "^6.3.0" + "semver": "^7.5.4" }, "engines": { - "node": ">=8" - } - }, - "node_modules/istanbul-lib-instrument/node_modules/semver": { - "version": "6.3.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", - "dev": true, - "bin": { - "semver": "bin/semver.js" + "node": ">=10" } }, "node_modules/istanbul-lib-report": { @@ -5904,22 +6020,40 @@ "node": ">=6" } }, + "node_modules/jake": { + "version": "10.9.2", + "resolved": "https://registry.npmjs.org/jake/-/jake-10.9.2.tgz", + "integrity": "sha512-2P4SQ0HrLQ+fw6llpLnOaGAvN2Zu6778SJMrCUwns4fOoG9ayrTiZk3VV8sCPkVZF8ab0zksVpS8FDY5pRCNBA==", + "dev": true, + "dependencies": { + "async": "^3.2.3", + "chalk": "^4.0.2", + "filelist": "^1.0.4", + "minimatch": "^3.1.2" + }, + "bin": { + "jake": "bin/cli.js" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/jest": { - "version": "28.1.2", - "resolved": "https://registry.npmjs.org/jest/-/jest-28.1.2.tgz", - "integrity": "sha512-Tuf05DwLeCh2cfWCQbcz9UxldoDyiR1E9Igaei5khjonKncYdc6LDfynKCEWozK0oLE3GD+xKAo2u8x/0s6GOg==", + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest/-/jest-29.7.0.tgz", + "integrity": "sha512-NIy3oAFp9shda19hy4HK0HRTWKtPJmGdnvywu01nOqNC2vZg+Z+fvJDxpMQA88eb2I9EcafcdjYgsDthnYTvGw==", "dev": true, "dependencies": { - "@jest/core": "^28.1.2", - "@jest/types": "^28.1.1", + "@jest/core": "^29.7.0", + "@jest/types": "^29.6.3", "import-local": "^3.0.2", - "jest-cli": "^28.1.2" + "jest-cli": "^29.7.0" }, "bin": { "jest": "bin/jest.js" }, "engines": { - "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0" + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" }, "peerDependencies": { "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" @@ -5931,72 +6065,121 @@ } }, "node_modules/jest-changed-files": { - "version": "28.1.3", - "resolved": "https://registry.npmjs.org/jest-changed-files/-/jest-changed-files-28.1.3.tgz", - "integrity": "sha512-esaOfUWJXk2nfZt9SPyC8gA1kNfdKLkQWyzsMlqq8msYSlNKfmZxfRgZn4Cd4MGVUF+7v6dBs0d5TOAKa7iIiA==", + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-changed-files/-/jest-changed-files-29.7.0.tgz", + "integrity": "sha512-fEArFiwf1BpQ+4bXSprcDc3/x4HSzL4al2tozwVpDFpsxALjLYdyiIK4e5Vz66GQJIbXJ82+35PtysofptNX2w==", "dev": true, "dependencies": { "execa": "^5.0.0", + "jest-util": "^29.7.0", "p-limit": "^3.1.0" }, "engines": { - "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0" + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, "node_modules/jest-circus": { - "version": "28.1.3", - "resolved": "https://registry.npmjs.org/jest-circus/-/jest-circus-28.1.3.tgz", - "integrity": "sha512-cZ+eS5zc79MBwt+IhQhiEp0OeBddpc1n8MBo1nMB8A7oPMKEO+Sre+wHaLJexQUj9Ya/8NOBY0RESUgYjB6fow==", + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-circus/-/jest-circus-29.7.0.tgz", + "integrity": "sha512-3E1nCMgipcTkCocFwM90XXQab9bS+GMsjdpmPrlelaxwD93Ad8iVEjX/vvHPdLPnFf+L40u+5+iutRdA1N9myw==", "dev": true, "dependencies": { - "@jest/environment": "^28.1.3", - "@jest/expect": "^28.1.3", - "@jest/test-result": "^28.1.3", - "@jest/types": "^28.1.3", + "@jest/environment": "^29.7.0", + "@jest/expect": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/types": "^29.6.3", "@types/node": "*", "chalk": "^4.0.0", "co": "^4.6.0", - "dedent": "^0.7.0", + "dedent": "^1.0.0", "is-generator-fn": "^2.0.0", - "jest-each": "^28.1.3", - "jest-matcher-utils": "^28.1.3", - "jest-message-util": "^28.1.3", - "jest-runtime": "^28.1.3", - "jest-snapshot": "^28.1.3", - "jest-util": "^28.1.3", + "jest-each": "^29.7.0", + "jest-matcher-utils": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-runtime": "^29.7.0", + "jest-snapshot": "^29.7.0", + "jest-util": "^29.7.0", "p-limit": "^3.1.0", - "pretty-format": "^28.1.3", + "pretty-format": "^29.7.0", + "pure-rand": "^6.0.0", "slash": "^3.0.0", "stack-utils": "^2.0.3" }, "engines": { - "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0" + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-circus/node_modules/diff-sequences": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-29.6.3.tgz", + "integrity": "sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q==", + "dev": true, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-circus/node_modules/jest-diff": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-29.7.0.tgz", + "integrity": "sha512-LMIgiIrhigmPrs03JHpxUh2yISK3vLFPkAodPeo0+BuF7wA2FoQbkEg1u8gBYBThncu7e1oEDUfIXVuTqLRUjw==", + "dev": true, + "dependencies": { + "chalk": "^4.0.0", + "diff-sequences": "^29.6.3", + "jest-get-type": "^29.6.3", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-circus/node_modules/jest-get-type": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-29.6.3.tgz", + "integrity": "sha512-zrteXnqYxfQh7l5FHyL38jL39di8H8rHoecLH3JNxH3BwOrBsNeabdap5e0I23lD4HHI8W5VFBZqG4Eaq5LNcw==", + "dev": true, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-circus/node_modules/jest-matcher-utils": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-29.7.0.tgz", + "integrity": "sha512-sBkD+Xi9DtcChsI3L3u0+N0opgPYnCRPtGcQYrgXmR+hmt/fYfWAL0xRXYU8eWOdfuLgBe0YCW3AFtnRLagq/g==", + "dev": true, + "dependencies": { + "chalk": "^4.0.0", + "jest-diff": "^29.7.0", + "jest-get-type": "^29.6.3", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, "node_modules/jest-cli": { - "version": "28.1.3", - "resolved": "https://registry.npmjs.org/jest-cli/-/jest-cli-28.1.3.tgz", - "integrity": "sha512-roY3kvrv57Azn1yPgdTebPAXvdR2xfezaKKYzVxZ6It/5NCxzJym6tUI5P1zkdWhfUYkxEI9uZWcQdaFLo8mJQ==", + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-cli/-/jest-cli-29.7.0.tgz", + "integrity": "sha512-OVVobw2IubN/GSYsxETi+gOe7Ka59EFMR/twOU3Jb2GnKKeMGJB5SGUUrEz3SFVmJASUdZUzy83sLNNQ2gZslg==", "dev": true, "dependencies": { - "@jest/core": "^28.1.3", - "@jest/test-result": "^28.1.3", - "@jest/types": "^28.1.3", + "@jest/core": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/types": "^29.6.3", "chalk": "^4.0.0", + "create-jest": "^29.7.0", "exit": "^0.1.2", - "graceful-fs": "^4.2.9", "import-local": "^3.0.2", - "jest-config": "^28.1.3", - "jest-util": "^28.1.3", - "jest-validate": "^28.1.3", - "prompts": "^2.0.1", + "jest-config": "^29.7.0", + "jest-util": "^29.7.0", + "jest-validate": "^29.7.0", "yargs": "^17.3.1" }, "bin": { "jest": "bin/jest.js" }, "engines": { - "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0" + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" }, "peerDependencies": { "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" @@ -6008,36 +6191,36 @@ } }, "node_modules/jest-config": { - "version": "28.1.3", - "resolved": "https://registry.npmjs.org/jest-config/-/jest-config-28.1.3.tgz", - "integrity": "sha512-MG3INjByJ0J4AsNBm7T3hsuxKQqFIiRo/AUqb1q9LRKI5UU6Aar9JHbr9Ivn1TVwfUD9KirRoM/T6u8XlcQPHQ==", + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-config/-/jest-config-29.7.0.tgz", + "integrity": "sha512-uXbpfeQ7R6TZBqI3/TxCU4q4ttk3u0PJeC+E0zbfSoSjq6bJ7buBPxzQPL0ifrkY4DNu4JUdk0ImlBUYi840eQ==", "dev": true, "dependencies": { "@babel/core": "^7.11.6", - "@jest/test-sequencer": "^28.1.3", - "@jest/types": "^28.1.3", - "babel-jest": "^28.1.3", + "@jest/test-sequencer": "^29.7.0", + "@jest/types": "^29.6.3", + "babel-jest": "^29.7.0", "chalk": "^4.0.0", "ci-info": "^3.2.0", "deepmerge": "^4.2.2", "glob": "^7.1.3", "graceful-fs": "^4.2.9", - "jest-circus": "^28.1.3", - "jest-environment-node": "^28.1.3", - "jest-get-type": "^28.0.2", - "jest-regex-util": "^28.0.2", - "jest-resolve": "^28.1.3", - "jest-runner": "^28.1.3", - "jest-util": "^28.1.3", - "jest-validate": "^28.1.3", + "jest-circus": "^29.7.0", + "jest-environment-node": "^29.7.0", + "jest-get-type": "^29.6.3", + "jest-regex-util": "^29.6.3", + "jest-resolve": "^29.7.0", + "jest-runner": "^29.7.0", + "jest-util": "^29.7.0", + "jest-validate": "^29.7.0", "micromatch": "^4.0.4", "parse-json": "^5.2.0", - "pretty-format": "^28.1.3", + "pretty-format": "^29.7.0", "slash": "^3.0.0", "strip-json-comments": "^3.1.1" }, "engines": { - "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0" + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" }, "peerDependencies": { "@types/node": "*", @@ -6052,159 +6235,148 @@ } } }, - "node_modules/jest-diff": { - "version": "28.1.3", - "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-28.1.3.tgz", - "integrity": "sha512-8RqP1B/OXzjjTWkqMX67iqgwBVJRgCyKD3L9nq+6ZqJMdvjE8RgHktqZ6jNrkdMT+dJuYNI3rhQpxaz7drJHfw==", + "node_modules/jest-config/node_modules/jest-get-type": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-29.6.3.tgz", + "integrity": "sha512-zrteXnqYxfQh7l5FHyL38jL39di8H8rHoecLH3JNxH3BwOrBsNeabdap5e0I23lD4HHI8W5VFBZqG4Eaq5LNcw==", "dev": true, - "dependencies": { - "chalk": "^4.0.0", - "diff-sequences": "^28.1.1", - "jest-get-type": "^28.0.2", - "pretty-format": "^28.1.3" - }, "engines": { - "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0" + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, "node_modules/jest-docblock": { - "version": "28.1.1", - "resolved": "https://registry.npmjs.org/jest-docblock/-/jest-docblock-28.1.1.tgz", - "integrity": "sha512-3wayBVNiOYx0cwAbl9rwm5kKFP8yHH3d/fkEaL02NPTkDojPtheGB7HZSFY4wzX+DxyrvhXz0KSCVksmCknCuA==", + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-docblock/-/jest-docblock-29.7.0.tgz", + "integrity": "sha512-q617Auw3A612guyaFgsbFeYpNP5t2aoUNLwBUbc/0kD1R4t9ixDbyFTHd1nok4epoVFpr7PmeWHrhvuV3XaJ4g==", "dev": true, "dependencies": { "detect-newline": "^3.0.0" }, "engines": { - "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0" + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, "node_modules/jest-each": { - "version": "28.1.3", - "resolved": "https://registry.npmjs.org/jest-each/-/jest-each-28.1.3.tgz", - "integrity": "sha512-arT1z4sg2yABU5uogObVPvSlSMQlDA48owx07BDPAiasW0yYpYHYOo4HHLz9q0BVzDVU4hILFjzJw0So9aCL/g==", + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-each/-/jest-each-29.7.0.tgz", + "integrity": "sha512-gns+Er14+ZrEoC5fhOfYCY1LOHHr0TI+rQUHZS8Ttw2l7gl+80eHc/gFf2Ktkw0+SIACDTeWvpFcv3B04VembQ==", "dev": true, "dependencies": { - "@jest/types": "^28.1.3", + "@jest/types": "^29.6.3", "chalk": "^4.0.0", - "jest-get-type": "^28.0.2", - "jest-util": "^28.1.3", - "pretty-format": "^28.1.3" + "jest-get-type": "^29.6.3", + "jest-util": "^29.7.0", + "pretty-format": "^29.7.0" }, "engines": { - "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0" + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, - "node_modules/jest-environment-node": { - "version": "28.1.3", - "resolved": "https://registry.npmjs.org/jest-environment-node/-/jest-environment-node-28.1.3.tgz", - "integrity": "sha512-ugP6XOhEpjAEhGYvp5Xj989ns5cB1K6ZdjBYuS30umT4CQEETaxSiPcZ/E1kFktX4GkrcM4qu07IIlDYX1gp+A==", + "node_modules/jest-each/node_modules/jest-get-type": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-29.6.3.tgz", + "integrity": "sha512-zrteXnqYxfQh7l5FHyL38jL39di8H8rHoecLH3JNxH3BwOrBsNeabdap5e0I23lD4HHI8W5VFBZqG4Eaq5LNcw==", "dev": true, - "dependencies": { - "@jest/environment": "^28.1.3", - "@jest/fake-timers": "^28.1.3", - "@jest/types": "^28.1.3", - "@types/node": "*", - "jest-mock": "^28.1.3", - "jest-util": "^28.1.3" - }, "engines": { - "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0" + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, - "node_modules/jest-get-type": { - "version": "28.0.2", - "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-28.0.2.tgz", - "integrity": "sha512-ioj2w9/DxSYHfOm5lJKCdcAmPJzQXmbM/Url3rhlghrPvT3tt+7a/+oXc9azkKmLvoiXjtV83bEWqi+vs5nlPA==", + "node_modules/jest-environment-node": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-environment-node/-/jest-environment-node-29.7.0.tgz", + "integrity": "sha512-DOSwCRqXirTOyheM+4d5YZOrWcdu0LNZ87ewUoywbcb2XR4wKgqiG8vNeYwhjFMbEkfju7wx2GYH0P2gevGvFw==", "dev": true, + "dependencies": { + "@jest/environment": "^29.7.0", + "@jest/fake-timers": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "jest-mock": "^29.7.0", + "jest-util": "^29.7.0" + }, "engines": { - "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0" + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, "node_modules/jest-haste-map": { - "version": "28.1.3", - "resolved": "https://registry.npmjs.org/jest-haste-map/-/jest-haste-map-28.1.3.tgz", - "integrity": "sha512-3S+RQWDXccXDKSWnkHa/dPwt+2qwA8CJzR61w3FoYCvoo3Pn8tvGcysmMF0Bj0EX5RYvAI2EIvC57OmotfdtKA==", + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-haste-map/-/jest-haste-map-29.7.0.tgz", + "integrity": "sha512-fP8u2pyfqx0K1rGn1R9pyE0/KTn+G7PxktWidOBTqFPLYX0b9ksaMFkhK5vrS3DVun09pckLdlx90QthlW7AmA==", "dev": true, "dependencies": { - "@jest/types": "^28.1.3", + "@jest/types": "^29.6.3", "@types/graceful-fs": "^4.1.3", "@types/node": "*", "anymatch": "^3.0.3", "fb-watchman": "^2.0.0", "graceful-fs": "^4.2.9", - "jest-regex-util": "^28.0.2", - "jest-util": "^28.1.3", - "jest-worker": "^28.1.3", + "jest-regex-util": "^29.6.3", + "jest-util": "^29.7.0", + "jest-worker": "^29.7.0", "micromatch": "^4.0.4", "walker": "^1.0.8" }, "engines": { - "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0" + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" }, "optionalDependencies": { "fsevents": "^2.3.2" } }, "node_modules/jest-leak-detector": { - "version": "28.1.3", - "resolved": "https://registry.npmjs.org/jest-leak-detector/-/jest-leak-detector-28.1.3.tgz", - "integrity": "sha512-WFVJhnQsiKtDEo5lG2mM0v40QWnBM+zMdHHyJs8AWZ7J0QZJS59MsyKeJHWhpBZBH32S48FOVvGyOFT1h0DlqA==", + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-leak-detector/-/jest-leak-detector-29.7.0.tgz", + "integrity": "sha512-kYA8IJcSYtST2BY9I+SMC32nDpBT3J2NvWJx8+JCuCdl/CR1I4EKUJROiP8XtCcxqgTTBGJNdbB1A8XRKbTetw==", "dev": true, "dependencies": { - "jest-get-type": "^28.0.2", - "pretty-format": "^28.1.3" + "jest-get-type": "^29.6.3", + "pretty-format": "^29.7.0" }, "engines": { - "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0" + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, - "node_modules/jest-matcher-utils": { - "version": "28.1.3", - "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-28.1.3.tgz", - "integrity": "sha512-kQeJ7qHemKfbzKoGjHHrRKH6atgxMk8Enkk2iPQ3XwO6oE/KYD8lMYOziCkeSB9G4adPM4nR1DE8Tf5JeWH6Bw==", + "node_modules/jest-leak-detector/node_modules/jest-get-type": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-29.6.3.tgz", + "integrity": "sha512-zrteXnqYxfQh7l5FHyL38jL39di8H8rHoecLH3JNxH3BwOrBsNeabdap5e0I23lD4HHI8W5VFBZqG4Eaq5LNcw==", "dev": true, - "dependencies": { - "chalk": "^4.0.0", - "jest-diff": "^28.1.3", - "jest-get-type": "^28.0.2", - "pretty-format": "^28.1.3" - }, "engines": { - "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0" + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, "node_modules/jest-message-util": { - "version": "28.1.3", - "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-28.1.3.tgz", - "integrity": "sha512-PFdn9Iewbt575zKPf1286Ht9EPoJmYT7P0kY+RibeYZ2XtOr53pDLEFoTWXbd1h4JiGiWpTBC84fc8xMXQMb7g==", + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-29.7.0.tgz", + "integrity": "sha512-GBEV4GRADeP+qtB2+6u61stea8mGcOT4mCtrYISZwfu9/ISHFJ/5zOMXYbpBE9RsS5+Gb63DW4FgmnKJ79Kf6w==", "dev": true, "dependencies": { "@babel/code-frame": "^7.12.13", - "@jest/types": "^28.1.3", + "@jest/types": "^29.6.3", "@types/stack-utils": "^2.0.0", "chalk": "^4.0.0", "graceful-fs": "^4.2.9", "micromatch": "^4.0.4", - "pretty-format": "^28.1.3", + "pretty-format": "^29.7.0", "slash": "^3.0.0", "stack-utils": "^2.0.3" }, "engines": { - "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0" + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, "node_modules/jest-mock": { - "version": "28.1.3", - "resolved": "https://registry.npmjs.org/jest-mock/-/jest-mock-28.1.3.tgz", - "integrity": "sha512-o3J2jr6dMMWYVH4Lh/NKmDXdosrsJgi4AviS8oXLujcjpCMBb1FMsblDnOXKZKfSiHLxYub1eS0IHuRXsio9eA==", + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-mock/-/jest-mock-29.7.0.tgz", + "integrity": "sha512-ITOMZn+UkYS4ZFh83xYAOzWStloNzJFO2s8DWrE4lhtGD+AorgnbkiKERe4wQVBydIGPx059g6riW5Btp6Llnw==", "dev": true, "dependencies": { - "@jest/types": "^28.1.3", - "@types/node": "*" + "@jest/types": "^29.6.3", + "@types/node": "*", + "jest-util": "^29.7.0" }, "engines": { - "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0" + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, "node_modules/jest-pnp-resolver": { @@ -6225,77 +6397,77 @@ } }, "node_modules/jest-regex-util": { - "version": "28.0.2", - "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-28.0.2.tgz", - "integrity": "sha512-4s0IgyNIy0y9FK+cjoVYoxamT7Zeo7MhzqRGx7YDYmaQn1wucY9rotiGkBzzcMXTtjrCAP/f7f+E0F7+fxPNdw==", + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-29.6.3.tgz", + "integrity": "sha512-KJJBsRCyyLNWCNBOvZyRDnAIfUiRJ8v+hOBQYGn8gDyF3UegwiP4gwRR3/SDa42g1YbVycTidUF3rKjyLFDWbg==", "dev": true, "engines": { - "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0" + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, "node_modules/jest-resolve": { - "version": "28.1.3", - "resolved": "https://registry.npmjs.org/jest-resolve/-/jest-resolve-28.1.3.tgz", - "integrity": "sha512-Z1W3tTjE6QaNI90qo/BJpfnvpxtaFTFw5CDgwpyE/Kz8U/06N1Hjf4ia9quUhCh39qIGWF1ZuxFiBiJQwSEYKQ==", + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-resolve/-/jest-resolve-29.7.0.tgz", + "integrity": "sha512-IOVhZSrg+UvVAshDSDtHyFCCBUl/Q3AAJv8iZ6ZjnZ74xzvwuzLXid9IIIPgTnY62SJjfuupMKZsZQRsCvxEgA==", "dev": true, "dependencies": { "chalk": "^4.0.0", "graceful-fs": "^4.2.9", - "jest-haste-map": "^28.1.3", + "jest-haste-map": "^29.7.0", "jest-pnp-resolver": "^1.2.2", - "jest-util": "^28.1.3", - "jest-validate": "^28.1.3", + "jest-util": "^29.7.0", + "jest-validate": "^29.7.0", "resolve": "^1.20.0", - "resolve.exports": "^1.1.0", + "resolve.exports": "^2.0.0", "slash": "^3.0.0" }, "engines": { - "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0" + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, "node_modules/jest-resolve-dependencies": { - "version": "28.1.3", - "resolved": "https://registry.npmjs.org/jest-resolve-dependencies/-/jest-resolve-dependencies-28.1.3.tgz", - "integrity": "sha512-qa0QO2Q0XzQoNPouMbCc7Bvtsem8eQgVPNkwn9LnS+R2n8DaVDPL/U1gngC0LTl1RYXJU0uJa2BMC2DbTfFrHA==", + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-resolve-dependencies/-/jest-resolve-dependencies-29.7.0.tgz", + "integrity": "sha512-un0zD/6qxJ+S0et7WxeI3H5XSe9lTBBR7bOHCHXkKR6luG5mwDDlIzVQ0V5cZCuoTgEdcdwzTghYkTWfubi+nA==", "dev": true, "dependencies": { - "jest-regex-util": "^28.0.2", - "jest-snapshot": "^28.1.3" + "jest-regex-util": "^29.6.3", + "jest-snapshot": "^29.7.0" }, "engines": { - "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0" + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, "node_modules/jest-runner": { - "version": "28.1.3", - "resolved": "https://registry.npmjs.org/jest-runner/-/jest-runner-28.1.3.tgz", - "integrity": "sha512-GkMw4D/0USd62OVO0oEgjn23TM+YJa2U2Wu5zz9xsQB1MxWKDOlrnykPxnMsN0tnJllfLPinHTka61u0QhaxBA==", + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-runner/-/jest-runner-29.7.0.tgz", + "integrity": "sha512-fsc4N6cPCAahybGBfTRcq5wFR6fpLznMg47sY5aDpsoejOcVYFb07AHuSnR0liMcPTgBsA3ZJL6kFOjPdoNipQ==", "dev": true, "dependencies": { - "@jest/console": "^28.1.3", - "@jest/environment": "^28.1.3", - "@jest/test-result": "^28.1.3", - "@jest/transform": "^28.1.3", - "@jest/types": "^28.1.3", + "@jest/console": "^29.7.0", + "@jest/environment": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", "@types/node": "*", "chalk": "^4.0.0", - "emittery": "^0.10.2", + "emittery": "^0.13.1", "graceful-fs": "^4.2.9", - "jest-docblock": "^28.1.1", - "jest-environment-node": "^28.1.3", - "jest-haste-map": "^28.1.3", - "jest-leak-detector": "^28.1.3", - "jest-message-util": "^28.1.3", - "jest-resolve": "^28.1.3", - "jest-runtime": "^28.1.3", - "jest-util": "^28.1.3", - "jest-watcher": "^28.1.3", - "jest-worker": "^28.1.3", + "jest-docblock": "^29.7.0", + "jest-environment-node": "^29.7.0", + "jest-haste-map": "^29.7.0", + "jest-leak-detector": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-resolve": "^29.7.0", + "jest-runtime": "^29.7.0", + "jest-util": "^29.7.0", + "jest-watcher": "^29.7.0", + "jest-worker": "^29.7.0", "p-limit": "^3.1.0", "source-map-support": "0.5.13" }, "engines": { - "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0" + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, "node_modules/jest-runner/node_modules/source-map": { @@ -6318,79 +6490,124 @@ } }, "node_modules/jest-runtime": { - "version": "28.1.3", - "resolved": "https://registry.npmjs.org/jest-runtime/-/jest-runtime-28.1.3.tgz", - "integrity": "sha512-NU+881ScBQQLc1JHG5eJGU7Ui3kLKrmwCPPtYsJtBykixrM2OhVQlpMmFWJjMyDfdkGgBMNjXCGB/ebzsgNGQw==", - "dev": true, - "dependencies": { - "@jest/environment": "^28.1.3", - "@jest/fake-timers": "^28.1.3", - "@jest/globals": "^28.1.3", - "@jest/source-map": "^28.1.2", - "@jest/test-result": "^28.1.3", - "@jest/transform": "^28.1.3", - "@jest/types": "^28.1.3", + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-runtime/-/jest-runtime-29.7.0.tgz", + "integrity": "sha512-gUnLjgwdGqW7B4LvOIkbKs9WGbn+QLqRQQ9juC6HndeDiezIwhDP+mhMwHWCEcfQ5RUXa6OPnFF8BJh5xegwwQ==", + "dev": true, + "dependencies": { + "@jest/environment": "^29.7.0", + "@jest/fake-timers": "^29.7.0", + "@jest/globals": "^29.7.0", + "@jest/source-map": "^29.6.3", + "@jest/test-result": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", "chalk": "^4.0.0", "cjs-module-lexer": "^1.0.0", "collect-v8-coverage": "^1.0.0", - "execa": "^5.0.0", "glob": "^7.1.3", "graceful-fs": "^4.2.9", - "jest-haste-map": "^28.1.3", - "jest-message-util": "^28.1.3", - "jest-mock": "^28.1.3", - "jest-regex-util": "^28.0.2", - "jest-resolve": "^28.1.3", - "jest-snapshot": "^28.1.3", - "jest-util": "^28.1.3", + "jest-haste-map": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-mock": "^29.7.0", + "jest-regex-util": "^29.6.3", + "jest-resolve": "^29.7.0", + "jest-snapshot": "^29.7.0", + "jest-util": "^29.7.0", "slash": "^3.0.0", "strip-bom": "^4.0.0" }, "engines": { - "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0" + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, "node_modules/jest-snapshot": { - "version": "28.1.3", - "resolved": "https://registry.npmjs.org/jest-snapshot/-/jest-snapshot-28.1.3.tgz", - "integrity": "sha512-4lzMgtiNlc3DU/8lZfmqxN3AYD6GGLbl+72rdBpXvcV+whX7mDrREzkPdp2RnmfIiWBg1YbuFSkXduF2JcafJg==", + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-snapshot/-/jest-snapshot-29.7.0.tgz", + "integrity": "sha512-Rm0BMWtxBcioHr1/OX5YCP8Uov4riHvKPknOGs804Zg9JGZgmIBkbtlxJC/7Z4msKYVbIJtfU+tKb8xlYNfdkw==", "dev": true, "dependencies": { "@babel/core": "^7.11.6", "@babel/generator": "^7.7.2", + "@babel/plugin-syntax-jsx": "^7.7.2", "@babel/plugin-syntax-typescript": "^7.7.2", - "@babel/traverse": "^7.7.2", "@babel/types": "^7.3.3", - "@jest/expect-utils": "^28.1.3", - "@jest/transform": "^28.1.3", - "@jest/types": "^28.1.3", - "@types/babel__traverse": "^7.0.6", - "@types/prettier": "^2.1.5", + "@jest/expect-utils": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", "babel-preset-current-node-syntax": "^1.0.0", "chalk": "^4.0.0", - "expect": "^28.1.3", + "expect": "^29.7.0", "graceful-fs": "^4.2.9", - "jest-diff": "^28.1.3", - "jest-get-type": "^28.0.2", - "jest-haste-map": "^28.1.3", - "jest-matcher-utils": "^28.1.3", - "jest-message-util": "^28.1.3", - "jest-util": "^28.1.3", + "jest-diff": "^29.7.0", + "jest-get-type": "^29.6.3", + "jest-matcher-utils": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-util": "^29.7.0", "natural-compare": "^1.4.0", - "pretty-format": "^28.1.3", - "semver": "^7.3.5" + "pretty-format": "^29.7.0", + "semver": "^7.5.3" }, "engines": { - "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0" + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-snapshot/node_modules/diff-sequences": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-29.6.3.tgz", + "integrity": "sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q==", + "dev": true, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-snapshot/node_modules/jest-diff": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-29.7.0.tgz", + "integrity": "sha512-LMIgiIrhigmPrs03JHpxUh2yISK3vLFPkAodPeo0+BuF7wA2FoQbkEg1u8gBYBThncu7e1oEDUfIXVuTqLRUjw==", + "dev": true, + "dependencies": { + "chalk": "^4.0.0", + "diff-sequences": "^29.6.3", + "jest-get-type": "^29.6.3", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-snapshot/node_modules/jest-get-type": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-29.6.3.tgz", + "integrity": "sha512-zrteXnqYxfQh7l5FHyL38jL39di8H8rHoecLH3JNxH3BwOrBsNeabdap5e0I23lD4HHI8W5VFBZqG4Eaq5LNcw==", + "dev": true, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-snapshot/node_modules/jest-matcher-utils": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-29.7.0.tgz", + "integrity": "sha512-sBkD+Xi9DtcChsI3L3u0+N0opgPYnCRPtGcQYrgXmR+hmt/fYfWAL0xRXYU8eWOdfuLgBe0YCW3AFtnRLagq/g==", + "dev": true, + "dependencies": { + "chalk": "^4.0.0", + "jest-diff": "^29.7.0", + "jest-get-type": "^29.6.3", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, "node_modules/jest-util": { - "version": "28.1.3", - "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-28.1.3.tgz", - "integrity": "sha512-XdqfpHwpcSRko/C35uLYFM2emRAltIIKZiJ9eAmhjsj0CqZMa0p1ib0R5fWIqGhn1a103DebTbpqIaP1qCQ6tQ==", + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-29.7.0.tgz", + "integrity": "sha512-z6EbKajIpqGKU56y5KBUgy1dt1ihhQJgWzUlZHArA/+X2ad7Cb5iF+AK1EWVL/Bo7Rz9uurpqw6SiBCefUbCGA==", "dev": true, "dependencies": { - "@jest/types": "^28.1.3", + "@jest/types": "^29.6.3", "@types/node": "*", "chalk": "^4.0.0", "ci-info": "^3.2.0", @@ -6398,24 +6615,24 @@ "picomatch": "^2.2.3" }, "engines": { - "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0" + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, "node_modules/jest-validate": { - "version": "28.1.3", - "resolved": "https://registry.npmjs.org/jest-validate/-/jest-validate-28.1.3.tgz", - "integrity": "sha512-SZbOGBWEsaTxBGCOpsRWlXlvNkvTkY0XxRfh7zYmvd8uL5Qzyg0CHAXiXKROflh801quA6+/DsT4ODDthOC/OA==", + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-validate/-/jest-validate-29.7.0.tgz", + "integrity": "sha512-ZB7wHqaRGVw/9hST/OuFUReG7M8vKeq0/J2egIGLdvjHCmYqGARhzXmtgi+gVeZ5uXFF219aOc3Ls2yLg27tkw==", "dev": true, "dependencies": { - "@jest/types": "^28.1.3", + "@jest/types": "^29.6.3", "camelcase": "^6.2.0", "chalk": "^4.0.0", - "jest-get-type": "^28.0.2", + "jest-get-type": "^29.6.3", "leven": "^3.1.0", - "pretty-format": "^28.1.3" + "pretty-format": "^29.7.0" }, "engines": { - "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0" + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, "node_modules/jest-validate/node_modules/camelcase": { @@ -6430,37 +6647,47 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/jest-validate/node_modules/jest-get-type": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-29.6.3.tgz", + "integrity": "sha512-zrteXnqYxfQh7l5FHyL38jL39di8H8rHoecLH3JNxH3BwOrBsNeabdap5e0I23lD4HHI8W5VFBZqG4Eaq5LNcw==", + "dev": true, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, "node_modules/jest-watcher": { - "version": "28.1.3", - "resolved": "https://registry.npmjs.org/jest-watcher/-/jest-watcher-28.1.3.tgz", - "integrity": "sha512-t4qcqj9hze+jviFPUN3YAtAEeFnr/azITXQEMARf5cMwKY2SMBRnCQTXLixTl20OR6mLh9KLMrgVJgJISym+1g==", + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-watcher/-/jest-watcher-29.7.0.tgz", + "integrity": "sha512-49Fg7WXkU3Vl2h6LbLtMQ/HyB6rXSIX7SqvBLQmssRBGN9I0PNvPmAmCWSOY6SOvrjhI/F7/bGAv9RtnsPA03g==", "dev": true, "dependencies": { - "@jest/test-result": "^28.1.3", - "@jest/types": "^28.1.3", + "@jest/test-result": "^29.7.0", + "@jest/types": "^29.6.3", "@types/node": "*", "ansi-escapes": "^4.2.1", "chalk": "^4.0.0", - "emittery": "^0.10.2", - "jest-util": "^28.1.3", + "emittery": "^0.13.1", + "jest-util": "^29.7.0", "string-length": "^4.0.1" }, "engines": { - "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0" + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, "node_modules/jest-worker": { - "version": "28.1.3", - "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-28.1.3.tgz", - "integrity": "sha512-CqRA220YV/6jCo8VWvAt1KKx6eek1VIHMPeLEbpcfSfkEeWyBNppynM/o6q+Wmw+sOhos2ml34wZbSX3G13//g==", + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-29.7.0.tgz", + "integrity": "sha512-eIz2msL/EzL9UFTFFx7jBTkeZfku0yUAyZZZmJ93H2TYEiroIx2PQjEXcwYtYl8zXCxb+PAmA2hLIt/6ZEkPHw==", "dev": true, "dependencies": { "@types/node": "*", + "jest-util": "^29.7.0", "merge-stream": "^2.0.0", "supports-color": "^8.0.0" }, "engines": { - "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0" + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, "node_modules/jest-worker/node_modules/supports-color": { @@ -6496,15 +6723,15 @@ } }, "node_modules/jsesc": { - "version": "2.5.2", - "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz", - "integrity": "sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==", + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.0.2.tgz", + "integrity": "sha512-xKqzzWXDttJuOcawBt4KnKHHIf5oQ/Cxax+0PWFG+DFDgHNAdi+TXECADI+RYiFUMmx8792xsMbbgXj4CwnP4g==", "dev": true, "bin": { "jsesc": "bin/jsesc" }, "engines": { - "node": ">=4" + "node": ">=6" } }, "node_modules/json-buffer": { @@ -7812,6 +8039,14 @@ "node": ">=0.10.0" } }, + "node_modules/p-finally": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/p-finally/-/p-finally-1.0.0.tgz", + "integrity": "sha512-LICb2p9CB7FS+0eR1oqWnHhp0FljGLZCWBE9aix0Uye9W8LTQPwMTYVGWQWIw9RdQiDg4+epXQODwIYJtSJaow==", + "engines": { + "node": ">=4" + } + }, "node_modules/p-limit": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", @@ -7842,6 +8077,49 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/p-queue": { + "version": "6.6.2", + "resolved": "https://registry.npmjs.org/p-queue/-/p-queue-6.6.2.tgz", + "integrity": "sha512-RwFpb72c/BhQLEXIZ5K2e+AhgNVmIejGlTgiB9MzZ0e93GRvqZ7uSi0dvRF7/XIXDeNkra2fNHBxTyPDGySpjQ==", + "dependencies": { + "eventemitter3": "^4.0.4", + "p-timeout": "^3.2.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-queue/node_modules/eventemitter3": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz", + "integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==" + }, + "node_modules/p-retry": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/p-retry/-/p-retry-4.6.2.tgz", + "integrity": "sha512-312Id396EbJdvRONlngUx0NydfrIQ5lsYu0znKVUzVvArzEIt08V1qhtyESbGVd1FGX7UKtiFp5uwKZdM8wIuQ==", + "dependencies": { + "@types/retry": "0.12.0", + "retry": "^0.13.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/p-timeout": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/p-timeout/-/p-timeout-3.2.0.tgz", + "integrity": "sha512-rhIwUycgwwKcP9yTOOFK/AKsAopjjCakVqLHePO3CC6Mir1Z99xT+R63jZxAT5lFZLa2inS5h+ZS2GvR99/FBg==", + "dependencies": { + "p-finally": "^1.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/p-try": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", @@ -7996,9 +8274,9 @@ "integrity": "sha512-KG8UEiEVkR3wGEb4m5yZkVCzigAD+cVEJck2CzYZO37ZGJfctvVptVO192MwrtPhzONn6go8ylnOdMhKqi4nfg==" }, "node_modules/picocolors": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.1.tgz", - "integrity": "sha512-anP1Z8qwhkbmu7MFP5iTt+wQKXgwzf7zTyGlcdzabySa9vd0Xt392U0rVmz9poOaBj0uHJKyyo9/upk0HrEQew==", + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", "dev": true }, "node_modules/picomatch": { @@ -8132,18 +8410,17 @@ } }, "node_modules/pretty-format": { - "version": "28.1.3", - "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-28.1.3.tgz", - "integrity": "sha512-8gFb/To0OmxHR9+ZTb14Df2vNxdGCX8g1xWGUTqUw5TiZvcQf5sHKObd5UcPyLLyowNwDAMTF3XWOG1B6mxl1Q==", + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", + "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", "dev": true, "dependencies": { - "@jest/schemas": "^28.1.3", - "ansi-regex": "^5.0.1", + "@jest/schemas": "^29.6.3", "ansi-styles": "^5.0.0", "react-is": "^18.0.0" }, "engines": { - "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0" + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, "node_modules/pretty-format/node_modules/ansi-styles": { @@ -8227,6 +8504,22 @@ "node": ">=6" } }, + "node_modules/pure-rand": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/pure-rand/-/pure-rand-6.1.0.tgz", + "integrity": "sha512-bVWawvoZoBYpp6yIoQtQXHZjmz35RSVHnUOTefl8Vcjr8snTPY1wnpSPMWekcFwbxI6gtmT7rSYPFvz71ldiOA==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/dubzzz" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fast-check" + } + ] + }, "node_modules/qs": { "version": "6.11.0", "resolved": "https://registry.npmjs.org/qs/-/qs-6.11.0.tgz", @@ -8420,9 +8713,9 @@ } }, "node_modules/resolve.exports": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/resolve.exports/-/resolve.exports-1.1.1.tgz", - "integrity": "sha512-/NtpHNDN7jWhAaQ9BvBUYZ6YTXsRBgfqWFWP7BZBaoMJO/I3G5OFzvTuWNlZC3aPjins1F+TNrLKsGbH4rfsRQ==", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/resolve.exports/-/resolve.exports-2.0.2.tgz", + "integrity": "sha512-X2UW6Nw3n/aMgDVy+0rSqgHlv39WZAlZrXCdnbyEiKm17DSqHX4MmQMaST3FbeWR5FTuRcUwYAziZajji0Y7mg==", "dev": true, "engines": { "node": ">=10" @@ -8441,6 +8734,14 @@ "node": ">=8" } }, + "node_modules/retry": { + "version": "0.13.1", + "resolved": "https://registry.npmjs.org/retry/-/retry-0.13.1.tgz", + "integrity": "sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg==", + "engines": { + "node": ">= 4" + } + }, "node_modules/reusify": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", @@ -9097,19 +9398,6 @@ "node": ">=8" } }, - "node_modules/supports-hyperlinks": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/supports-hyperlinks/-/supports-hyperlinks-2.3.0.tgz", - "integrity": "sha512-RpsAZlpWcDwOPQA22aCH4J0t7L8JmAvsCxfOSEwm7cQs3LshN36QaTkwd70DnBOXDWGssw2eUoc8CaRWT0XunA==", - "dev": true, - "dependencies": { - "has-flag": "^4.0.0", - "supports-color": "^7.0.0" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/supports-preserve-symlinks-flag": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", @@ -9177,22 +9465,6 @@ "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" }, - "node_modules/terminal-link": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/terminal-link/-/terminal-link-2.1.1.tgz", - "integrity": "sha512-un0FmiRUQNr5PJqy9kP7c40F5BOfpGlYTrxonDChEZB7pzZxRNp/bt+ymiy9/npwXya9KH99nJ/GXFIiUkYGFQ==", - "dev": true, - "dependencies": { - "ansi-escapes": "^4.2.1", - "supports-hyperlinks": "^2.0.0" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/terser": { "version": "5.31.6", "resolved": "https://registry.npmjs.org/terser/-/terser-5.31.6.tgz", @@ -9324,15 +9596,6 @@ "integrity": "sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw==", "dev": true }, - "node_modules/to-fast-properties": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", - "integrity": "sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==", - "dev": true, - "engines": { - "node": ">=4" - } - }, "node_modules/to-regex-range": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", @@ -9368,37 +9631,42 @@ } }, "node_modules/ts-jest": { - "version": "28.0.8", - "resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-28.0.8.tgz", - "integrity": "sha512-5FaG0lXmRPzApix8oFG8RKjAz4ehtm8yMKOTy5HX3fY6W8kmvOrmcY0hKDElW52FJov+clhUbrKAqofnj4mXTg==", + "version": "29.2.5", + "resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-29.2.5.tgz", + "integrity": "sha512-KD8zB2aAZrcKIdGk4OwpJggeLcH1FgrICqDSROWqlnJXGCXK4Mn6FcdK2B6670Xr73lHMG1kHw8R87A0ecZ+vA==", "dev": true, "dependencies": { - "bs-logger": "0.x", - "fast-json-stable-stringify": "2.x", - "jest-util": "^28.0.0", - "json5": "^2.2.1", - "lodash.memoize": "4.x", - "make-error": "1.x", - "semver": "7.x", - "yargs-parser": "^21.0.1" + "bs-logger": "^0.2.6", + "ejs": "^3.1.10", + "fast-json-stable-stringify": "^2.1.0", + "jest-util": "^29.0.0", + "json5": "^2.2.3", + "lodash.memoize": "^4.1.2", + "make-error": "^1.3.6", + "semver": "^7.6.3", + "yargs-parser": "^21.1.1" }, "bin": { "ts-jest": "cli.js" }, "engines": { - "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0" + "node": "^14.15.0 || ^16.10.0 || ^18.0.0 || >=20.0.0" }, "peerDependencies": { "@babel/core": ">=7.0.0-beta.0 <8", - "@jest/types": "^28.0.0", - "babel-jest": "^28.0.0", - "jest": "^28.0.0", - "typescript": ">=4.3" + "@jest/transform": "^29.0.0", + "@jest/types": "^29.0.0", + "babel-jest": "^29.0.0", + "jest": "^29.0.0", + "typescript": ">=4.3 <6" }, "peerDependenciesMeta": { "@babel/core": { "optional": true }, + "@jest/transform": { + "optional": true + }, "@jest/types": { "optional": true }, @@ -9650,6 +9918,11 @@ "node": ">= 0.8" } }, + "node_modules/undici-types": { + "version": "6.19.8", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz", + "integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==" + }, "node_modules/universalify": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", @@ -9668,9 +9941,9 @@ } }, "node_modules/update-browserslist-db": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.0.tgz", - "integrity": "sha512-EdRAaAyk2cUE1wOf2DkEhzxqOQvFOoRJFNS6NeyJ01Gp2beMRpBAINjM2iDXE3KCuKhwnvHIQCJm6ThL2Z+HzQ==", + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.1.tgz", + "integrity": "sha512-R8UzCaa9Az+38REPiJ1tXlImTJXlVfgHZsglwBD/k6nj76ctsH1E3q4doGrukiLQd3sGQYu56r5+lo5r94l29A==", "dev": true, "funding": [ { @@ -9687,8 +9960,8 @@ } ], "dependencies": { - "escalade": "^3.1.2", - "picocolors": "^1.0.1" + "escalade": "^3.2.0", + "picocolors": "^1.1.0" }, "bin": { "update-browserslist-db": "cli.js" @@ -9744,12 +10017,6 @@ "node": ">=10.12.0" } }, - "node_modules/v8-to-istanbul/node_modules/convert-source-map": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", - "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", - "dev": true - }, "node_modules/validator": { "version": "13.12.0", "resolved": "https://registry.npmjs.org/validator/-/validator-13.12.0.tgz", diff --git a/package.json b/package.json index e137f2ef..64adf34c 100644 --- a/package.json +++ b/package.json @@ -30,9 +30,9 @@ "client:generate": "npx prisma generate --schema src/prisma/schema.prisma", "lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix", "lint:check": "eslint .", - "test": "jest", - "test:watch": "jest --watch", - "test:cov": "jest --coverage", + "test": "dotenv -e env/.env.test -- jest --maxWorkers 1", + "test:watch": "dotenv -e env/.env.test -- jest --watch", + "test:cov": "dotenv -e env/.env.test -- jest --coverage", "test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand", "test:e2e": "jest --config ./test/jest-e2e.json", "prepare": "husky install", @@ -51,6 +51,7 @@ "@nestjs/platform-express": "^9.0.0", "@nestjs/swagger": "^7.1.1", "@prisma/client": "^5.18.0", + "@slack/web-api": "^7.7.0", "@types/cookie-parser": "^1.4.3", "@types/express-session": "^1.17.7", "@types/morgan": "^1.9.4", @@ -87,7 +88,7 @@ "@types/csurf": "^1.11.5", "@types/express": "^4.17.13", "@types/inquirer": "^9.0.7", - "@types/jest": "28.1.4", + "@types/jest": "^29.5.14", "@types/node": "^16.18.23", "@types/supertest": "^6.0.2", "@typescript-eslint/eslint-plugin": "^5.0.0", @@ -96,13 +97,13 @@ "eslint": "^8.0.1", "eslint-config-prettier": "^8.3.0", "husky": "^8.0.0", - "jest": "28.1.2", + "jest": "^29.7.0", "lint-staged": "^13.2.3", "prettier": "^2.3.2", "prisma": "^5.18.0", "source-map-support": "^0.5.20", "supertest": "^7.0.0", - "ts-jest": "^28.0.5", + "ts-jest": "^29.2.5", "ts-loader": "^9.2.3", "ts-node": "^10.9.1", "tsconfig-paths": "4.0.0", diff --git a/src/app.controller.spec.ts b/src/app.controller.spec.ts index d22f3890..baa8733c 100644 --- a/src/app.controller.spec.ts +++ b/src/app.controller.spec.ts @@ -1,14 +1,13 @@ import { Test, TestingModule } from '@nestjs/testing'; import { AppController } from './app.controller'; -import { AppService } from './app.service'; +import { AppModule } from './app.module'; describe('AppController', () => { let appController: AppController; beforeEach(async () => { const app: TestingModule = await Test.createTestingModule({ - controllers: [AppController], - providers: [AppService], + imports: [AppModule], }).compile(); appController = app.get(AppController); diff --git a/src/app.module.ts b/src/app.module.ts index 7b916df9..a77d0a08 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -1,9 +1,15 @@ +import { ClsPluginTransactional } from '@nestjs-cls/transactional'; +import { TransactionalAdapterPrisma } from '@nestjs-cls/transactional-adapter-prisma'; import { Module } from '@nestjs/common'; import { APP_GUARD } from '@nestjs/core'; import { JwtService } from '@nestjs/jwt'; +import { PrismaService } from '@src/prisma/prisma.service'; +import { ClsModule } from 'nestjs-cls'; import { AppController } from './app.controller'; import { AppService } from './app.service'; +import { AuthConfig } from './modules/auth/auth.config'; import { AuthModule } from './modules/auth/auth.module'; +import { AuthGuard } from './modules/auth/guard/auth.guard'; import { JwtCookieGuard } from './modules/auth/guard/jwt-cookie.guard'; import { MockAuthGuard } from './modules/auth/guard/mock-auth-guard'; import { CoursesModule } from './modules/courses/courses.module'; @@ -16,19 +22,14 @@ import { RatesModule } from './modules/rates/rates.module'; import { ReviewsModule } from './modules/reviews/reviews.module'; import { SemestersModule } from './modules/semesters/semesters.module'; import { SessionModule } from './modules/session/session.module'; +import { ShareModule } from './modules/share/share.module'; import { StatusModule } from './modules/status/status.module'; +import { SyncModule } from './modules/sync/sync.module'; import { TimetablesModule } from './modules/timetables/timetables.module'; import { TracksModule } from './modules/tracks/tracks.module'; import { UserModule } from './modules/user/user.module'; import { WishlistModule } from './modules/wishlist/wishlist.module'; import { PrismaModule } from './prisma/prisma.module'; -import { ShareModule } from './modules/share/share.module'; -import { AuthConfig } from './modules/auth/auth.config'; -import { AuthGuard } from './modules/auth/guard/auth.guard'; -import { ClsModule } from 'nestjs-cls'; -import { ClsPluginTransactional } from '@nestjs-cls/transactional'; -import { PrismaService } from '@src/prisma/prisma.service'; -import { TransactionalAdapterPrisma } from '@nestjs-cls/transactional-adapter-prisma'; @Module({ imports: [ @@ -50,6 +51,7 @@ import { TransactionalAdapterPrisma } from '@nestjs-cls/transactional-adapter-pr PlannersModule, TracksModule, ShareModule, + SyncModule, ClsModule.forRoot({ global: true, middleware: { mount: true }, diff --git a/src/bootstrap/bootstrap.ts b/src/bootstrap/bootstrap.ts index 9d30e05d..b5e4e8e3 100644 --- a/src/bootstrap/bootstrap.ts +++ b/src/bootstrap/bootstrap.ts @@ -2,6 +2,7 @@ import { ValidationPipe, VersioningType } from '@nestjs/common'; import { NestFactory } from '@nestjs/core'; import cookieParser from 'cookie-parser'; import csrf from 'csurf'; +import { json } from 'express'; import session from 'express-session'; import { AppModule } from '../app.module'; import settings from '../settings'; @@ -69,6 +70,9 @@ async function bootstrap() { }), ); + app.use('/sync', json({ limit: '50mb' })); + app.use(json({ limit: '100kb' })); + app.enableShutdownHooks(); return app.listen(8000); } diff --git a/src/common/decorators/sync-api-key-auth.decorator.ts b/src/common/decorators/sync-api-key-auth.decorator.ts new file mode 100644 index 00000000..b0aec1b1 --- /dev/null +++ b/src/common/decorators/sync-api-key-auth.decorator.ts @@ -0,0 +1,4 @@ +import { SetMetadata } from '@nestjs/common'; + +export const USE_SYNC_API_KEY = 'useSyncAPIKey'; +export const SyncApiKeyAuth = () => SetMetadata(USE_SYNC_API_KEY, true); diff --git a/src/common/entities/EProfessor.ts b/src/common/entities/EProfessor.ts new file mode 100644 index 00000000..86009400 --- /dev/null +++ b/src/common/entities/EProfessor.ts @@ -0,0 +1,8 @@ +import { Prisma } from '@prisma/client'; + +export namespace EProfessor { + export const Basic = Prisma.validator()( + {}, + ); + export type Basic = Prisma.subject_professorGetPayload; +} diff --git a/src/common/entities/ETakenLecture.ts b/src/common/entities/ETakenLecture.ts new file mode 100644 index 00000000..79aba50b --- /dev/null +++ b/src/common/entities/ETakenLecture.ts @@ -0,0 +1,12 @@ +import { Prisma } from '@prisma/client'; + +export namespace ETakenLecture { + export const Basic = + Prisma.validator()( + {}, + ); + + export type Basic = Prisma.session_userprofile_taken_lecturesGetPayload< + typeof Basic + >; +} diff --git a/src/common/entities/EUserProfile.ts b/src/common/entities/EUserProfile.ts new file mode 100644 index 00000000..8f46c216 --- /dev/null +++ b/src/common/entities/EUserProfile.ts @@ -0,0 +1,13 @@ +import { Prisma } from '@prisma/client'; +import { ETakenLecture } from './ETakenLecture'; + +export namespace EUserProfile { + export const WithTakenLectures = + Prisma.validator()({ + include: { taken_lectures: ETakenLecture.Basic }, + }); + + export type WithTakenLectures = Prisma.session_userprofileGetPayload< + typeof WithTakenLectures + >; +} diff --git a/src/common/interfaces/ISync.ts b/src/common/interfaces/ISync.ts new file mode 100644 index 00000000..20d47ec1 --- /dev/null +++ b/src/common/interfaces/ISync.ts @@ -0,0 +1,357 @@ +import { Type } from 'class-transformer'; +import { + IsIn, + IsInt, + IsNumber, + IsOptional, + IsString, + ValidateNested, +} from 'class-validator'; + +export namespace ISync { + export class ScholarDBBody { + /** 동기화 대상 연도 */ + @IsInt() + year!: number; + /** 동기화 대상 학기 */ + @IsIn([1, 2, 3, 4]) + semester!: number; + /** 동기화 대상 강의 정보 */ + @Type(() => ScholarLectureType) + @ValidateNested() + lectures!: ScholarLectureType[]; + /** 강의 강사 정보 */ + @Type(() => ScholarChargeType) + @ValidateNested() + charges!: ScholarChargeType[]; + } + + /** 동기화 대상 강의 정보 + example: + { + "lecture_year": 2024, + "lecture_term": 3, + "subject_no": "36.492", + "lecture_class": "F ", + "dept_id": 4421, + "dept_name": "전산학부", + "e_dept_name": "School of Computing", + "sub_title": "전산학특강", + "e_sub_title": "Special Topics in Computer Science", + "subject_id": 34, + "subject_type": "전공선택", + "e_subject_type": "Major Elective", + "course_sect": 7, + "act_unit": 0, + "lecture": 3, + "lab": 0, + "credit": 3, + "limit": 25, + "prof_names": "안드리아", + "notice": "", + "old_no": "CS492", + "english_lec": "Y", + "e_prof_names": "Bianchi Andrea" + }, + */ + export class ScholarLectureType { + /** 개설 연도 */ + @IsInt() + lecture_year!: number; + /** 개설 학기. 1: 봄학기, 2:여름학기, 3: 가을학기, 4: 겨울학기 */ + @IsIn([1, 2, 3, 4]) + lecture_term!: number; + /** 36.492 등의 과목코드 */ + @IsString() + subject_no!: string; + /** 분반 */ + @IsString() + lecture_class!: string; + /** 학과 숫자 id */ + @IsInt() + dept_id!: number; + /** 학과 이름 */ + @IsString() + dept_name!: string; + /** 학과 영어 이름 */ + @IsString() + e_dept_name!: string; + /** 강의 이름, 전산학특강 처럼 꺾쇠도 사용. */ + @IsString() + sub_title!: string; + /** 강의 영어 이름 */ + @IsString() + e_sub_title!: string; + /** 과목 종류 id로 추정. 아래 subject_type과 대응. */ + @IsInt() + subject_id!: number; + /** 과목 종류 한글 명칭. 전공필수, 전공선택 등 */ + @IsString() + subject_type!: string; + /** 과목 종류 영어 명칭. Major Elective 등 */ + @IsString() + e_subject_type!: string; + /** ? 학년 구분이라고 보임. */ + @IsInt() + course_sect!: number; + /** 부여 AU */ + @IsInt() + act_unit!: number; + /** 강의시간 */ + @IsNumber() + lecture!: number; + /** 실습시간 */ + @IsNumber() + lab!: number; + /** 학점 */ + @IsInt() + credit!: number; + /** 수강 제한 인원 */ + @IsInt() + limit!: number; + /** 교수(들) 이름 */ + @IsString() + prof_names!: string; + /** 공지사항 */ + @IsString() + notice!: string; + /** CS492 형의 과목 코드 */ + @IsString() + old_no!: string; + /** 영어 강의 여부 */ + @IsIn(['Y', 'N', ''], { message: (args) => JSON.stringify(args) }) + english_lec!: 'Y' | 'N' | ''; + /** 교수(들) 영어 이름 */ + @IsString() + e_prof_names!: string; + } + + /** 동기화 대상 교수 정보 + example: + { + "lecture_year": 2024, + "lecture_term": 1, + "subject_no": "21.960", + "lecture_class": "AS", + "dept_id": 132, + "prof_id": 1849, + "prof_name": "전상용", + "portion": 3, + "e_prof_name": " Sangyong Jon" + }, + */ + export class ScholarChargeType { + /** 개설 연도 */ + @IsInt() + lecture_year!: number; + /** 개설 학기. 1: 봄학기, 2:여름학기, 3: 가을학기, 4: 겨울학기 */ + @IsIn([1, 2, 3, 4]) + lecture_term!: number; + /** 36.492 등의 과목코드 */ + @IsString() + subject_no!: string; + /** 분반 */ + @IsString() + lecture_class!: string; + /** 학과 숫자 id */ + @IsInt() + dept_id!: number; + /** 교수 숫자 id */ + @IsInt() + prof_id!: number; + /** 교수 이름 */ + @IsString() + prof_name!: string; + /** ? */ + @IsNumber() + portion!: number; + // TODO: 이전 코드에 따르면 e_prof_name은 null이 가능하다고 되어 있음. 이게 맞는지 확인 필요. + /** 교수 영어 이름 */ + @IsOptional() + @IsString() + e_prof_name?: string | null; + } + + export class ExamtimeBody { + /** 동기화 대상 연도 */ + @IsInt() + year!: number; + /** 동기화 대상 학기 */ + @IsIn([1, 2, 3, 4]) + semester!: number; + /** 동기화 대상 시험시간 정보 */ + @Type(() => ExamtimeType) + @ValidateNested({ each: true }) + examtimes!: ExamtimeType[]; + } + + /** 동기화 대상 시험시간 정보 + example: + { + "lecture_year": 2024, + "lecture_term": 1, + "subject_no": "31.343", + "lecture_class": " ", + "dept_id": 331, + "exam_day": 4, + "exam_begin": "1899-12-31 13:00:00.0", + "exam_end": "1899-12-31 15:45:00.0", + "notice": "" + } + */ + export class ExamtimeType { + /** 개설 연도 */ + @IsInt() + lecture_year!: number; + /** 개설 학기. 1: 봄학기, 2:여름학기, 3: 가을학기, 4: 겨울학기 */ + @IsIn([1, 2, 3, 4]) + lecture_term!: number; + /** 36.492 등의 과목코드 */ + @IsString() + subject_no!: string; + /** 분반 */ + @IsString() + lecture_class!: string; + /** 학과 숫자 id */ + @IsInt() + dept_id!: number; + /** 시험 요일. 1~6 범위의 값 확인. 월부터 시작. */ + @IsInt() + exam_day!: number; + /** 시험 시작 시간 */ + @IsString() + exam_begin!: string; + /** 시험 종료 시간 */ + @IsString() + exam_end!: string; + /** 공지사항 */ + @IsString() + notice!: string; + } + + export class ClasstimeBody { + /** 동기화 대상 연도 */ + @IsInt() + year!: number; + /** 동기화 대상 학기 */ + @IsIn([1, 2, 3, 4]) + semester!: number; + /** 동기화 대상 수업시간 정보 */ + @Type(() => ClasstimeType) + @ValidateNested({ each: true }) + classtimes!: ClasstimeType[]; + } + + /** 동기화 대상 수업시간 정보 + example: + { + "lecture_year": 2024, + "lecture_term": 1, + "subject_no": "36.204", + "lecture_class": "A ", + "dept_id": 4421, + "lecture_day": 2, + "lecture_begin": "1900-01-01 13:00:00.0", + "lecture_end": "1900-01-01 14:30:00.0", + "lecture_type": "l", + "building": 304, + "room_no": "터만홀", + "room_k_name": "(E11)창의학습관", + "room_e_name": "(E11)Creative Learning Bldg.", + "teaching": 3 + } + */ + export class ClasstimeType { + /** 개설 연도 */ + @IsInt() + lecture_year!: number; + /** 개설 학기. 1: 봄학기, 2:여름학기, 3: 가을학기, 4: 겨울학기 */ + @IsIn([1, 2, 3, 4]) + lecture_term!: number; + /** 36.492 등의 과목코드 */ + @IsString() + subject_no!: string; + /** 분반 */ + @IsString() + lecture_class!: string; + /** 학과 숫자 id */ + @IsInt() + dept_id!: number; + /** 수업 요일. 1~7 범위의 값 확인. 월부터 시작. */ + @IsInt() + lecture_day!: number; + /** 수업 시작 시간 */ + @IsString() + lecture_begin!: string; + /** 수업 종료 시간 */ + @IsString() + lecture_end!: string; + /** 수업 종류. l: lecture, e: experiment */ + @IsIn(['l', 'e']) + lecture_type!: 'l' | 'e'; + /** 건물 번호 */ + @IsInt() + building!: number; + /** 강의실 번호 */ + @IsString() + room_no!: string; + /** 강의실 한글 이름 */ + @IsString() + room_k_name!: string; + /** 강의실 영어 이름 */ + @IsString() + room_e_name!: string; + /** 수업 교시 */ + @IsInt() + teaching!: number; + } + + export class TakenLectureBody { + /** 동기화 대상 연도 */ + @IsInt() + year!: number; + /** 동기화 대상 학기 */ + @IsIn([1, 2, 3, 4]) + semester!: number; + /** 동기화 대상 수강 강의 정보 */ + @Type(() => AttendType) + @ValidateNested({ each: true }) + attend!: AttendType[]; + } + + /** 동기화 대상 들은 수업 정보 + example: + { + "lecture_year": 2024, + "lecture_term": 1, + "subject_no": "25.101", + "lecture_class": "G ", + "dept_id": 151, + "student_no": 20240111, + "process_type": "I" + } + */ + export class AttendType { + /** 개설 연도 */ + @IsInt() + lecture_year!: number; + /** 개설 학기. 1: 봄학기, 2:여름학기, 3: 가을학기, 4: 겨울학기 */ + @IsIn([1, 2, 3, 4]) + lecture_term!: number; + /** 36.492 등의 과목코드 */ + @IsString() + subject_no!: string; + /** 분반 */ + @IsString() + lecture_class!: string; + /** 학과 숫자 id */ + @IsInt() + dept_id!: number; + /** 학번 */ + @IsInt() + student_no!: number; + /** 신청 기간 구분으로 유추. I: 수강신청기간 내 신청, C: 수강변경기간 내 신청 */ + @IsIn(['I', 'C']) + process_type!: 'I' | 'C'; + } +} diff --git a/src/common/interfaces/constants/professor.ts b/src/common/interfaces/constants/professor.ts new file mode 100644 index 00000000..2fcb68e2 --- /dev/null +++ b/src/common/interfaces/constants/professor.ts @@ -0,0 +1 @@ +export const STAFF_ID = 830; diff --git a/src/modules/auth/auth.config.ts b/src/modules/auth/auth.config.ts index ee8dcb21..b5030370 100644 --- a/src/modules/auth/auth.config.ts +++ b/src/modules/auth/auth.config.ts @@ -1,11 +1,9 @@ -import { ExecutionContext, Injectable } from '@nestjs/common'; -import { Reflector } from '@nestjs/core'; -import { AuthService } from './auth.service'; -import { JwtService } from '@nestjs/jwt'; +import { Injectable } from '@nestjs/common'; +import { AuthChain } from './auth.chain'; +import { IsPublicCommand } from './command/isPublic.command'; import { JwtCommand } from './command/jwt.command'; import { SidCommand } from './command/sid.command'; -import { IsPublicCommand } from './command/isPublic.command'; -import { AuthChain } from './auth.chain'; +import { SyncApiKeyCommand } from './command/syncApiKey.command'; @Injectable() export class AuthConfig { @@ -14,6 +12,7 @@ export class AuthConfig { private readonly jwtCommand: JwtCommand, private readonly sidCommand: SidCommand, private readonly isPublicCommand: IsPublicCommand, + private readonly syncApiKeyCommand: SyncApiKeyCommand, ) {} public async config(env: string) { @@ -27,19 +26,22 @@ export class AuthConfig { return this.authChain .register(this.isPublicCommand) .register(this.sidCommand) - .register(this.jwtCommand); + .register(this.jwtCommand) + .register(this.syncApiKeyCommand); }; private getDevGuardConfig = () => { return this.authChain .register(this.isPublicCommand) .register(this.sidCommand) - .register(this.jwtCommand); + .register(this.jwtCommand) + .register(this.syncApiKeyCommand); }; private getProdGuardConfig = () => { return this.authChain .register(this.jwtCommand) - .register(this.isPublicCommand); + .register(this.isPublicCommand) + .register(this.syncApiKeyCommand); }; } diff --git a/src/modules/auth/auth.module.ts b/src/modules/auth/auth.module.ts index e65e0f3a..2922bc83 100644 --- a/src/modules/auth/auth.module.ts +++ b/src/modules/auth/auth.module.ts @@ -3,21 +3,24 @@ import { JwtModule } from '@nestjs/jwt'; import { PassportModule } from '@nestjs/passport'; import { PrismaModule } from '../../prisma/prisma.module'; import { UserRepository } from '../../prisma/repositories/user.repository'; +import { SyncModule } from '../sync/sync.module'; import { UserService } from '../user/user.service'; +import { AuthChain } from './auth.chain'; +import { AuthConfig } from './auth.config'; import { AuthController } from './auth.controller'; import { AuthService } from './auth.service'; -import { JwtCookieStrategy } from './strategy/jwt-cookie.strategy'; -import { AuthChain } from './auth.chain'; import { IsPublicCommand } from './command/isPublic.command'; import { JwtCommand } from './command/jwt.command'; import { SidCommand } from './command/sid.command'; -import { AuthConfig } from './auth.config'; +import { SyncApiKeyCommand } from './command/syncApiKey.command'; +import { JwtCookieStrategy } from './strategy/jwt-cookie.strategy'; @Module({ imports: [ PrismaModule, PassportModule.register({ defaultStrategy: 'jwt-cookie' }), JwtModule.register({}), + SyncModule, ], controllers: [AuthController], providers: [ @@ -29,6 +32,7 @@ import { AuthConfig } from './auth.config'; IsPublicCommand, JwtCommand, SidCommand, + SyncApiKeyCommand, AuthConfig, ], exports: [AuthService, AuthConfig, AuthChain], diff --git a/src/modules/auth/auth.service.ts b/src/modules/auth/auth.service.ts index a6849510..963eb0f4 100644 --- a/src/modules/auth/auth.service.ts +++ b/src/modules/auth/auth.service.ts @@ -3,15 +3,16 @@ import { JwtService } from '@nestjs/jwt'; import { Prisma, session_userprofile } from '@prisma/client'; import * as bcrypt from 'bcrypt'; import { ESSOUser } from 'src/common/entities/ESSOUser'; -import { import_student_lectures } from '../../common/scholarDB/scripts'; import { UserRepository } from '../../prisma/repositories/user.repository'; import settings from '../../settings'; +import { SyncTakenLectureService } from '../sync/syncTakenLecture.service'; @Injectable() export class AuthService { constructor( private readonly userRepository: UserRepository, private readonly jwtService: JwtService, + private readonly syncTakenLecturesService: SyncTakenLectureService, ) {} public async findBySid(sid: string) { @@ -42,11 +43,11 @@ export class AuthService { ssoProfile['last_name'], encryptedRefreshToken, ); + await this.syncTakenLecturesService.repopulateTakenLectureForStudent( + user.id, + ); } else { - if (user.student_id != studentId) { - await import_student_lectures(studentId); - } - + const prev_student_id = user.student_id; const updateData = { first_name: ssoProfile['first_name'], last_name: ssoProfile['last_name'], @@ -54,6 +55,11 @@ export class AuthService { refresh_token: encryptedRefreshToken, }; user = await this.updateUser(user.id, updateData); + if (prev_student_id !== studentId) { + await this.syncTakenLecturesService.repopulateTakenLectureForStudent( + user.id, + ); + } } return { diff --git a/src/modules/auth/command/jwt.command.ts b/src/modules/auth/command/jwt.command.ts index 92b48e1c..3e8bd31d 100644 --- a/src/modules/auth/command/jwt.command.ts +++ b/src/modules/auth/command/jwt.command.ts @@ -1,18 +1,16 @@ -import { AuthChain } from '../auth.chain'; import { ExecutionContext, Injectable, InternalServerErrorException, NotFoundException, - UnauthorizedException, } from '@nestjs/common'; import { Reflector } from '@nestjs/core'; +import { JwtService } from '@nestjs/jwt'; +import * as bcrypt from 'bcrypt'; import { Request } from 'express'; import settings from '../../../settings'; -import * as bcrypt from 'bcrypt'; -import { AuthService } from '../auth.service'; -import { JwtService } from '@nestjs/jwt'; import { AuthCommand, AuthResult } from '../auth.command'; +import { AuthService } from '../auth.service'; @Injectable() export class JwtCommand implements AuthCommand { @@ -48,7 +46,7 @@ export class JwtCommand implements AuthCommand { request, 'refreshToken', ); - if (!refreshToken) throw new UnauthorizedException(); + if (!refreshToken) return prevResult; const payload = await this.jwtService.verify(refreshToken, { secret: settings().getJwtConfig().secret, ignoreExpiration: false, diff --git a/src/modules/auth/command/syncApiKey.command.ts b/src/modules/auth/command/syncApiKey.command.ts new file mode 100644 index 00000000..840bcd40 --- /dev/null +++ b/src/modules/auth/command/syncApiKey.command.ts @@ -0,0 +1,30 @@ +import { ExecutionContext, Injectable } from '@nestjs/common'; +import { Reflector } from '@nestjs/core'; +import { USE_SYNC_API_KEY } from '@src/common/decorators/sync-api-key-auth.decorator'; +import settings from '@src/settings'; +import { AuthCommand, AuthResult } from '../auth.command'; + +@Injectable() +export class SyncApiKeyCommand implements AuthCommand { + constructor(private reflector: Reflector) {} + + public next( + context: ExecutionContext, + prevResult: AuthResult, + ): Promise { + const useSyncApiKey = this.reflector.getAllAndOverride( + USE_SYNC_API_KEY, + [context.getHandler(), context.getClass()], + ); + + const apiKey = context.switchToHttp().getRequest().headers['x-api-key']; + const realApiKey = settings().syncConfig().apiKey; + + if (useSyncApiKey && realApiKey && apiKey === realApiKey) { + prevResult.authentication = true; + prevResult.authorization = true; + return Promise.resolve(prevResult); + } + return Promise.resolve(prevResult); + } +} diff --git a/src/modules/sync/slackNoti.service.ts b/src/modules/sync/slackNoti.service.ts new file mode 100644 index 00000000..2bade92c --- /dev/null +++ b/src/modules/sync/slackNoti.service.ts @@ -0,0 +1,22 @@ +import { Injectable } from '@nestjs/common'; +import { WebClient } from '@slack/web-api'; +import settings from '@src/settings'; + +@Injectable() +export class SlackNotiService { + private client?: WebClient; + constructor() { + const key = settings().syncConfig().slackKey; + if (key) this.client = new WebClient(key); + else console.info('No slack key, logging to console.'); + } + + async sendSyncNoti(text: string) { + if (this.client) + await this.client.chat.postMessage({ + channel: '#otl-db-sync', + text, + }); + else console.info(text); + } +} diff --git a/src/modules/sync/sync.controller.ts b/src/modules/sync/sync.controller.ts new file mode 100644 index 00000000..d7b8193c --- /dev/null +++ b/src/modules/sync/sync.controller.ts @@ -0,0 +1,53 @@ +import { + Body, + Controller, + Get, + InternalServerErrorException, + Post, +} from '@nestjs/common'; +import { SyncApiKeyAuth } from '@src/common/decorators/sync-api-key-auth.decorator'; +import { ISync } from 'src/common/interfaces/ISync'; +import { toJsonSemester } from 'src/common/interfaces/serializer/semester.serializer'; +import { SyncScholarDBService } from './syncScholarDB.service'; +import { SyncTakenLectureService } from './syncTakenLecture.service'; + +@Controller('sync') +export class SyncController { + constructor( + private readonly syncScholarDBService: SyncScholarDBService, + private readonly syncTakenLectureService: SyncTakenLectureService, + ) {} + + @Get('defaultSemester') + @SyncApiKeyAuth() + async getDefaultSemester() { + const semester = await this.syncScholarDBService.getDefaultSemester(); + if (!semester) + throw new InternalServerErrorException('No default semester in DB'); + return toJsonSemester(semester); + } + + @Post('scholarDB') + @SyncApiKeyAuth() + async syncScholarDB(@Body() body: ISync.ScholarDBBody) { + return await this.syncScholarDBService.syncScholarDB(body); + } + + @Post('examtime') + @SyncApiKeyAuth() + async syncExamtime(@Body() body: ISync.ExamtimeBody) { + return await this.syncScholarDBService.syncExamtime(body); + } + + @Post('classtime') + @SyncApiKeyAuth() + async syncClasstime(@Body() body: ISync.ClasstimeBody) { + return await this.syncScholarDBService.syncClassTime(body); + } + + @Post('takenLecture') + @SyncApiKeyAuth() + async syncTakenLecture(@Body() body: ISync.TakenLectureBody) { + return await this.syncTakenLectureService.syncTakenLecture(body); + } +} diff --git a/src/modules/sync/sync.module.ts b/src/modules/sync/sync.module.ts new file mode 100644 index 00000000..e5d94fac --- /dev/null +++ b/src/modules/sync/sync.module.ts @@ -0,0 +1,14 @@ +import { Module } from '@nestjs/common'; +import { PrismaModule } from 'src/prisma/prisma.module'; +import { SlackNotiService } from './slackNoti.service'; +import { SyncController } from './sync.controller'; +import { SyncScholarDBService } from './syncScholarDB.service'; +import { SyncTakenLectureService } from './syncTakenLecture.service'; + +@Module({ + controllers: [SyncController], + providers: [SyncScholarDBService, SyncTakenLectureService, SlackNotiService], + imports: [PrismaModule], + exports: [SyncTakenLectureService], +}) +export class SyncModule {} diff --git a/src/modules/sync/syncScholarDB.service.spec.ts b/src/modules/sync/syncScholarDB.service.spec.ts new file mode 100644 index 00000000..0cd374cd --- /dev/null +++ b/src/modules/sync/syncScholarDB.service.spec.ts @@ -0,0 +1,843 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { session_userprofile } from '@prisma/client'; +import { AppModule } from '@src/app.module'; +import { PrismaService } from '@src/prisma/prisma.service'; +import { SyncScholarDBService } from './syncScholarDB.service'; + +// This tests on test database only. Add `DATABASE_URL` with `otlplus_test` database to run this test. + +const maybe = process.env.DATABASE_URL?.includes('/otlplus_test?') + ? describe + : describe.skip; + +const departmentData = [...Array(2).keys()].map((i) => ({ + id: i + 1, + num_id: (10 + i).toString(), + code: String.fromCharCode(65 + i), + name: `학과${i}`, + name_en: `department${i}`, + visible: true, +})); +const courseData = [...Array(2).keys()].map((i) => ({ + id: i + 1, + old_code: `${departmentData[i].code}10${i}`, + department_id: departmentData[i].id, + type: '전공', + type_en: 'major', + title: `과목 ${i}`, + title_en: `subject ${i}`, + summury: '', + grade_sum: 0, + load_sum: 0, + speech_sum: 0, + review_total_weight: 0, + grade: 0, + load: 0, + speech: 0, + title_no_space: `과목${i}`, + title_en_no_space: `subject${i}`, +})); +const professorData = [...Array(2).keys()].map((i) => ({ + id: i + 1, + professor_id: i + 1, + professor_name: `교수${i}`, + professor_name_en: `professor${i}`, + major: departmentData[i].id.toString(), + grade_sum: 0, + load_sum: 0, + speech_sum: 0, + review_total_weight: 0, + grade: 0, + load: 0, + speech: 0, +})); +const lectureData = [...Array(2).keys()].map((i) => ({ + id: i + 1, + code: `${departmentData[i].num_id}:10${i}`, + year: 3000, + semester: 1, + class_no: String.fromCharCode(65 + i), + department_id: departmentData[i].id, + old_code: courseData[i].old_code, + title: courseData[i].title, + title_en: courseData[i].title_en, + type: courseData[i].type, + type_en: courseData[i].type_en, + audience: 0, + limit: 0, + credit: 3, + credit_au: 0, + num_classes: 3, + num_labs: 0, + is_english: false, + course_id: courseData[i].id, + deleted: false, + grade_sum: 0, + load_sum: 0, + speech_sum: 0, + review_total_weight: 0, + grade: 0, + load: 0, + speech: 0, + title_no_space: courseData[i].title_no_space, + title_en_no_space: courseData[i].title_en_no_space, +})); + +const lectureBase = { + lecture_year: 3000, + lecture_term: 1, + subject_no: lectureData[0].code, + lecture_class: lectureData[0].class_no + ' ', + dept_id: departmentData[0].id, + dept_name: departmentData[0].name, + e_dept_name: departmentData[0].name_en, + sub_title: lectureData[0].title, + e_sub_title: lectureData[0].title_en, + subject_id: 0, + subject_type: '전공', + e_subject_type: 'major', + course_sect: 0, + act_unit: 0, + lecture: 3, + lab: 0, + credit: 3, + limit: 0, + prof_names: '교수0', + notice: '', + old_no: lectureData[0].old_code, + english_lec: 'N' as const, + e_prof_names: 'professor0', +}; + +const classtimeData = [...Array(1).keys()] + .map((i) => [ + { + id: 2 * i + 1, + lecture_id: lectureData[i].id, + day: 0, + begin: new Date('1970-01-01T09:00:00Z'), + end: new Date('1970-01-01T10:30:00Z'), + type: 'l', + building_id: '301', + building_full_name: '(E11)창의학습관', + building_full_name_en: '(E11)Creative Learning Bldg.', + room_name: '304', + unit_time: 1, + }, + { + id: 2 * i + 2, + lecture_id: lectureData[i].id, + day: 2, + begin: new Date('1970-01-01T09:00:00Z'), + end: new Date('1970-01-01T10:30:00Z'), + type: 'l', + building_id: '301', + building_full_name: '(E11)창의학습관', + building_full_name_en: '(E11)Creative Learning Bldg.', + room_name: '304', + unit_time: 1, + }, + ]) + .flat(); + +const classtimeBase = { + lecture_year: 3000, + lecture_term: 1, + subject_no: lectureData[0].code, + lecture_class: lectureData[0].class_no + ' ', + dept_id: lectureData[0].department_id, + lecture_day: 1, + lecture_begin: '1900-01-01 09:00:00.0', + lecture_end: '1900-01-01 10:30:00.0', + lecture_type: 'l', + building: 301, + room_no: '304', + room_k_name: '(E11)창의학습관', + room_e_name: '(E11)Creative Learning Bldg.', + teaching: 1, +} as const; + +const examtimeData = [...Array(1).keys()].map((i) => ({ + id: i + 1, + lecture_id: lectureData[i].id, + day: 0, + begin: new Date('1970-01-01T09:00:00Z'), + end: new Date('1970-01-01T12:00:00Z'), +})); + +const examtimeBase = { + lecture_year: 3000, + lecture_term: 1, + subject_no: lectureData[0].code, + lecture_class: lectureData[0].class_no + ' ', + dept_id: lectureData[0].department_id, + exam_day: 1, + exam_begin: '1900-01-01 09:00:00.0', + exam_end: '1900-01-01 12:00:00.0', + notice: '', +} as const; + +maybe('SyncScholarDBService', () => { + let service: SyncScholarDBService; + let prisma: PrismaService; + let users: session_userprofile[]; + beforeAll(async () => { + const module: TestingModule = await Test.createTestingModule({ + imports: [AppModule], + providers: [], + }).compile(); + + service = module.get(SyncScholarDBService); + prisma = module.get(PrismaService); + + const userData = [...Array(5).keys()].map((i) => ({ + student_id: `3000000${i}`, + sid: i.toString(), + date_joined: new Date(), + first_name: 'test', + last_name: 'test', + })); + + await prisma.session_userprofile.deleteMany(); + await prisma.session_userprofile.createMany({ data: userData }); + users = await prisma.session_userprofile.findMany(); + }); + + afterAll(async () => { + await prisma.session_userprofile.deleteMany(); + }); + + it('should be defined', () => { + expect(service).toBeDefined(); + }); + + function checkNoError(result: any) { + if (result.departments.errors.length > 0) + console.error(result.departments.errors); + if (result.courses.errors.length > 0) console.error(result.courses.errors); + if (result.lectures.errors.length > 0) + console.error(result.lectures.errors); + if (result.professors.errors.length > 0) + console.error(result.professors.errors); + expect(result.departments.errors.length).toBe(0); + expect(result.courses.errors.length).toBe(0); + expect(result.lectures.errors.length).toBe(0); + expect(result.professors.errors.length).toBe(0); + } + + describe('syncScholarDB', () => { + beforeEach(async () => { + await prisma.subject_lecture_professors.deleteMany(); + await prisma.subject_lecture.deleteMany(); + await prisma.subject_course.deleteMany(); + await prisma.subject_professor.deleteMany(); + await prisma.subject_department.deleteMany(); + + await prisma.subject_department.createMany({ data: departmentData }); + await prisma.subject_course.createMany({ data: courseData }); + await prisma.subject_professor.createMany({ data: professorData }); + await prisma.subject_lecture.createMany({ data: lectureData }); + }); + + afterEach(async () => { + await prisma.subject_lecture_professors.deleteMany(); + await prisma.subject_lecture.deleteMany(); + await prisma.subject_course.deleteMany(); + await prisma.subject_professor.deleteMany(); + await prisma.subject_department.deleteMany(); + }); + + it('should create a new department if not exists', async () => { + const result = await service.syncScholarDB({ + year: 3000, + semester: 1, + lectures: [ + { + ...lectureBase, + dept_id: 10, + dept_name: '학과10', + old_no: 'K100', + e_dept_name: 'department10', + }, + ], + charges: [], + }); + + expect(result.departments.created.length).toBe(1); + checkNoError(result); + + const department = await prisma.subject_department.findFirst({ + where: { id: 10 }, + }); + expect(department).toMatchObject({ + id: 10, + code: 'K', + name: '학과10', + name_en: 'department10', + visible: true, + }); + }); + + it('should update an existing department if changes are detected', async () => { + const result = await service.syncScholarDB({ + year: 3000, + semester: 1, + lectures: [ + { + ...lectureBase, + dept_id: departmentData[0].id, + dept_name: '학과0_Updated', + old_no: 'AA100', + e_dept_name: 'department0_Updated', + }, + ], + charges: [], + }); + + expect(result.departments.updated.length).toBe(1); + checkNoError(result); + + const department = await prisma.subject_department.findFirst({ + where: { id: departmentData[0].id }, + }); + + expect(department).toMatchObject({ + id: departmentData[0].id, + code: 'AA', + name: '학과0_Updated', + name_en: 'department0_Updated', + visible: true, + }); + }); + + it('should handle course creation', async () => { + const result = await service.syncScholarDB({ + year: 3000, + semester: 1, + lectures: [ + { + ...lectureBase, + subject_no: `${departmentData[0].num_id}.200`, + old_no: `${departmentData[0].code}200`, + dept_id: departmentData[0].id, + sub_title: '새 과목', + e_sub_title: 'New Course EN', + }, + ], + charges: [], + }); + + expect(result.courses.created.length).toBe(1); + checkNoError(result); + + const course = await prisma.subject_course.findFirst({ + where: { old_code: `${departmentData[0].code}200` }, + }); + + expect(course).toMatchObject({ + department_id: departmentData[0].id, + title: '새 과목', + title_en: 'New Course EN', + }); + }); + + it('should handle course update', async () => { + const existingLecture = lectureData[0]; + + const result = await service.syncScholarDB({ + year: 3000, + semester: 1, + lectures: [ + { + ...lectureBase, + subject_no: existingLecture.code, + old_no: existingLecture.old_code, + dept_id: existingLecture.department_id, + sub_title: '수정된 과목', + e_sub_title: 'Updated Course EN', + }, + ], + charges: [], + }); + + expect(result.courses.updated.length).toBe(1); + checkNoError(result); + + const updatedCourse = await prisma.subject_course.findFirst({ + where: { old_code: existingLecture.old_code }, + }); + + expect(updatedCourse).toMatchObject({ + department_id: existingLecture.department_id, + title: '수정된 과목', + title_en: 'Updated Course EN', + }); + }); + + it('should handle lecture creation', async () => { + const existingCourse = courseData[0]; // Use the first course from courseData + + const result = await service.syncScholarDB({ + year: 3000, + semester: 1, + lectures: [ + { + ...lectureBase, + subject_no: departmentData[0].code + '.200', // Use the same department code as the existing course + old_no: existingCourse.old_code, + dept_id: existingCourse.department_id, + sub_title: `${existingCourse.title}`, + e_sub_title: `${existingCourse.title_en}`, + lecture_class: 'Z', // Set a new class number + }, + ], + charges: [], + }); + + expect(result.lectures.updated.length).toBe(0); + expect(result.lectures.created.length).toBe(1); + checkNoError(result); + + const lecture = await prisma.subject_lecture.findFirst({ + where: { + code: `${existingCourse.old_code.split('10')[0]}.200`, + class_no: 'Z', + }, + }); + + expect(lecture).toBeDefined(); + expect(lecture).toMatchObject({ + department_id: existingCourse.department_id, + title: `${existingCourse.title}`, + title_en: `${existingCourse.title_en}`, + class_no: 'Z', + }); + }); + + it('should handle lecture update', async () => { + const existingLecture = lectureData[0]; // Use the first lecture from lectureData + + const result = await service.syncScholarDB({ + year: 3000, + semester: 1, + lectures: [ + { + ...lectureBase, + subject_no: existingLecture.code, + old_no: existingLecture.old_code, + dept_id: existingLecture.department_id, + sub_title: '수정된 강의', + e_sub_title: 'Updated Lecture EN', + lecture_class: existingLecture.class_no, // Use the existing class number + }, + ], + charges: [], + }); + + expect(result.lectures.updated.length).toBe(1); + expect(result.lectures.updated.length).toBe(1); + checkNoError(result); + + const updatedLecture = await prisma.subject_lecture.findFirst({ + where: { + code: existingLecture.code, + class_no: existingLecture.class_no, + }, + }); + + expect(updatedLecture).toMatchObject({ + department_id: existingLecture.department_id, + title: '수정된 강의', + title_en: 'Updated Lecture EN', + class_no: existingLecture.class_no, + }); + }); + + it('should handle lecture removal', async () => { + const existingLecture = lectureData[0]; // Use the first lecture from lectureData + + const result = await service.syncScholarDB({ + year: 3000, + semester: 1, + lectures: [], + charges: [], + }); + + expect(result.lectures.deleted.length).toBe(lectureData.length); + checkNoError(result); + + const deletedLectureCount = await prisma.subject_lecture.count({ + where: { deleted: true }, + }); + expect(deletedLectureCount).toBe(lectureData.length); + }); + + it('should handle professor creation', async () => { + const existingLecture = lectureData[0]; // Use the first lecture from lectureData + + const result = await service.syncScholarDB({ + year: 3000, + semester: 1, + lectures: [lectureBase], + charges: [ + { + lecture_year: existingLecture.year, + lecture_term: existingLecture.semester, + subject_no: existingLecture.code, + lecture_class: existingLecture.class_no, + dept_id: existingLecture.department_id, + prof_id: 999, // New professor ID + prof_name: 'New Professor', + portion: 3, + e_prof_name: 'New Professor EN', + }, + ], + }); + + expect(result.professors.created.length).toBe(1); + checkNoError(result); + + const professor = await prisma.subject_professor.findFirst({ + where: { professor_id: 999 }, + }); + + expect(professor).toMatchObject({ + professor_name: 'New Professor', + professor_name_en: 'New Professor EN', + major: existingLecture.department_id.toString(), + }); + }); + + it('should handle professor update', async () => { + const existingLecture = lectureData[0]; // Use the first lecture from lectureData + const existingProfessor = professorData[0]; // Use the first professor from professorData + + const result = await service.syncScholarDB({ + year: 3000, + semester: 1, + lectures: [lectureBase], + charges: [ + { + lecture_year: existingLecture.year, + lecture_term: existingLecture.semester, + subject_no: existingLecture.code, + lecture_class: existingLecture.class_no, + dept_id: existingLecture.department_id, + prof_id: existingProfessor.professor_id, // Existing professor ID + prof_name: 'Updated Professor', + portion: 3, + e_prof_name: 'Updated Professor EN', + }, + ], + }); + + expect(result.professors.updated.length).toBe(1); + checkNoError(result); + + const updatedProfessor = await prisma.subject_professor.findFirst({ + where: { professor_id: existingProfessor.professor_id }, + }); + + expect(updatedProfessor).toMatchObject({ + professor_name: 'Updated Professor', + professor_name_en: 'Updated Professor EN', + major: existingLecture.department_id.toString(), + }); + }); + + it('should handle lecture professor change', async () => { + const existingLecture = lectureData[0]; // Use the first lecture from lectureData + const existingProfessor = professorData[1]; // Use the first professor from professorData + + await prisma.subject_lecture_professors.create({ + data: { + lecture_id: existingLecture.id, + professor_id: professorData[0].id, + }, + }); + + const result = await service.syncScholarDB({ + year: 3000, + semester: 1, + lectures: [lectureBase], + charges: [ + { + lecture_year: existingLecture.year, + lecture_term: existingLecture.semester, + subject_no: existingLecture.code, + lecture_class: existingLecture.class_no, + dept_id: parseInt(existingProfessor.major), + prof_id: existingProfessor.professor_id, // Existing professor ID + prof_name: existingProfessor.professor_name, + portion: 3, + e_prof_name: existingProfessor.professor_name_en, + }, + ], + }); + + expect(result.professors.updated.length).toBe(0); + expect(result.professors.created.length).toBe(0); + expect(result.lectures.updated.length).toBe(0); + expect(result.lectures.chargeUpdated.length).toBe(1); + checkNoError(result); + + expect(result.lectures.chargeUpdated[0]).toMatchObject({ + added: [{ id: professorData[1].id }], + removed: [{ id: professorData[0].id }], + }); + }); + }); + + describe('sync classtime', () => { + beforeAll(async () => { + await prisma.subject_lecture_professors.deleteMany(); + await prisma.subject_lecture.deleteMany(); + await prisma.subject_course.deleteMany(); + await prisma.subject_professor.deleteMany(); + await prisma.subject_department.deleteMany(); + + await prisma.subject_department.createMany({ data: departmentData }); + await prisma.subject_course.createMany({ data: courseData }); + await prisma.subject_professor.createMany({ data: professorData }); + await prisma.subject_lecture.createMany({ data: lectureData }); + }); + + afterAll(async () => { + await prisma.subject_lecture_professors.deleteMany(); + await prisma.subject_lecture.deleteMany(); + await prisma.subject_course.deleteMany(); + await prisma.subject_professor.deleteMany(); + await prisma.subject_department.deleteMany(); + }); + + beforeEach(async () => { + await prisma.subject_classtime.deleteMany(); + + await prisma.subject_classtime.createMany({ data: classtimeData }); + }); + + afterEach(async () => { + await prisma.subject_classtime.deleteMany(); + }); + + it('should handle classtime creation & deletion', async () => { + const existingLecture = lectureData[1]; // Use the second lecture from lectureData + + const result = await service.syncClassTime({ + year: 3000, + semester: 1, + classtimes: [ + { + ...classtimeBase, + subject_no: existingLecture.code, + lecture_class: existingLecture.class_no, + dept_id: existingLecture.department_id, + lecture_day: 2, + lecture_begin: '1900-01-01 13:00:00.0', + lecture_end: '1900-01-01 14:30:00.0', + }, + ], + }); + + expect(result.updated.length).toBe(2); + expect( + result.updated.filter((l: any) => l.lecture === existingLecture.code)[0] + .added.length, + ).toBe(1); + expect( + result.updated.filter((l: any) => l.lecture === lectureData[0].code)[0] + .removed.length, + ).toBe(2); + + const classtime = await prisma.subject_classtime.findFirst({ + where: { lecture_id: existingLecture.id }, + }); + + expect(classtime).toMatchObject({ + day: 1, + begin: new Date('1970-01-01T13:00:00Z'), + end: new Date('1970-01-01T14:30:00Z'), + building_id: '301', + }); + }); + + it('should handle classtime update', async () => { + const existingLecture = lectureData[0]; // Use the first lecture + + const result = await service.syncClassTime({ + year: 3000, + semester: 1, + classtimes: [ + { + ...classtimeBase, + subject_no: existingLecture.code, + lecture_class: existingLecture.class_no, + dept_id: existingLecture.department_id, + lecture_day: 2, + lecture_begin: '1900-01-01 14:00:00.0', + lecture_end: '1900-01-01 15:30:00.0', + }, + ], + }); + + expect(result.updated.length).toBe(1); + expect(result.updated[0].added.length).toBe(1); + expect(result.updated[0].removed.length).toBe(2); + + const updatedClasstime = await prisma.subject_classtime.findFirst({}); + + expect(updatedClasstime).toMatchObject({ + day: 1, + begin: new Date('1970-01-01T14:00:00Z'), + end: new Date('1970-01-01T15:30:00Z'), + }); + }); + + it('should not update classtime if no changes are detected', async () => { + const existingLecture = lectureData[0]; // Use the first lecture + + const result = await service.syncClassTime({ + year: 3000, + semester: 1, + classtimes: [ + { + ...classtimeBase, + lecture_day: 1, + lecture_begin: '1900-01-01 09:00:00.0', + lecture_end: '1900-01-01 10:30:00.0', + }, + { + ...classtimeBase, + lecture_day: 3, + lecture_begin: '1900-01-01 09:00:00.0', + lecture_end: '1900-01-01 10:30:00.0', + }, + ], + }); + + expect(result.updated.length).toBe(0); + }); + }); + + describe('sync examtime', () => { + beforeAll(async () => { + await prisma.subject_lecture_professors.deleteMany(); + await prisma.subject_lecture.deleteMany(); + await prisma.subject_course.deleteMany(); + await prisma.subject_professor.deleteMany(); + await prisma.subject_department.deleteMany(); + + await prisma.subject_department.createMany({ data: departmentData }); + await prisma.subject_course.createMany({ data: courseData }); + await prisma.subject_professor.createMany({ data: professorData }); + await prisma.subject_lecture.createMany({ data: lectureData }); + }); + + afterAll(async () => { + await prisma.subject_lecture_professors.deleteMany(); + await prisma.subject_lecture.deleteMany(); + await prisma.subject_course.deleteMany(); + await prisma.subject_professor.deleteMany(); + await prisma.subject_department.deleteMany(); + }); + + beforeEach(async () => { + await prisma.subject_examtime.deleteMany(); + + await prisma.subject_examtime.createMany({ data: examtimeData }); + }); + + afterEach(async () => { + await prisma.subject_examtime.deleteMany(); + }); + + it('should handle examtime creation & deletion', async () => { + const existingLecture = lectureData[1]; // Use the second lecture from lectureData + + const result = await service.syncExamtime({ + year: 3000, + semester: 1, + examtimes: [ + { + ...examtimeBase, + subject_no: existingLecture.code, + lecture_class: existingLecture.class_no, + dept_id: existingLecture.department_id, + exam_day: 2, + exam_begin: '1900-01-01 13:00:00.0', + exam_end: '1900-01-01 14:30:00.0', + }, + ], + }); + + expect(result.updated.length).toBe(2); + expect( + result.updated.filter((l: any) => l.lecture === existingLecture.code)[0] + .added.length, + ).toBe(1); + expect( + result.updated.filter((l: any) => l.lecture === lectureData[0].code)[0] + .removed.length, + ).toBe(1); + + const examtime = await prisma.subject_examtime.findFirst({ + where: { lecture_id: existingLecture.id }, + }); + + expect(examtime).toMatchObject({ + day: 1, + begin: new Date('1970-01-01T13:00:00Z'), + end: new Date('1970-01-01T14:30:00Z'), + }); + }); + + it('should handle examtime update', async () => { + const existingLecture = lectureData[0]; // Use the first lecture + + const result = await service.syncExamtime({ + year: 3000, + semester: 1, + examtimes: [ + { + ...examtimeBase, + subject_no: existingLecture.code, + lecture_class: existingLecture.class_no, + dept_id: existingLecture.department_id, + exam_day: 2, + exam_begin: '1900-01-01 14:00:00.0', + exam_end: '1900-01-01 15:30:00.0', + }, + ], + }); + + expect(result.updated.length).toBe(1); + expect(result.updated[0].added.length).toBe(1); + expect(result.updated[0].removed.length).toBe(1); + + const updatedExamtime = await prisma.subject_examtime.findFirst({}); + + expect(updatedExamtime).toMatchObject({ + day: 1, + begin: new Date('1970-01-01T14:00:00Z'), + end: new Date('1970-01-01T15:30:00Z'), + }); + }); + + it('should not update examtime if no changes are detected', async () => { + const result = await service.syncExamtime({ + year: 3000, + semester: 1, + examtimes: [ + { + ...examtimeBase, + exam_day: 1, + exam_begin: '1900-01-01 09:00:00.0', + exam_end: '1900-01-01 12:00:00.0', + }, + ], + }); + + expect(result.updated.length).toBe(0); + }); + }); +}); diff --git a/src/modules/sync/syncScholarDB.service.ts b/src/modules/sync/syncScholarDB.service.ts new file mode 100644 index 00000000..b48cc03e --- /dev/null +++ b/src/modules/sync/syncScholarDB.service.ts @@ -0,0 +1,650 @@ +import { Injectable } from '@nestjs/common'; +import { ECourse } from '@src/common/entities/ECourse'; +import { EDepartment } from '@src/common/entities/EDepartment'; +import { ELecture } from '@src/common/entities/ELecture'; +import { EProfessor } from '@src/common/entities/EProfessor'; +import { ISync } from 'src/common/interfaces/ISync'; +import { SyncRepository } from 'src/prisma/repositories/sync.repository'; +import { SlackNotiService } from './slackNoti.service'; +import { + ChargeDerivedProfessorInfo, + DerivedClasstimeInfo, + DerivedExamtimeInfo, + DerivedLectureInfo, + LectureDerivedCourseInfo, + LectureDerivedDepartmentInfo, +} from './types'; + +@Injectable() +export class SyncScholarDBService { + constructor( + private readonly syncRepository: SyncRepository, + private readonly slackNoti: SlackNotiService, + ) {} + + async getDefaultSemester() { + return await this.syncRepository.getDefaultSemester(); + } + + async syncScholarDB(data: ISync.ScholarDBBody) { + this.slackNoti.sendSyncNoti( + `syncScholarDB ${data.year}-${data.semester}: ${data.lectures.length} lectures, ${data.charges.length} charges`, + ); + const result: any = { + time: new Date().toISOString(), + departments: { + created: [], + updated: [], + errors: [], + }, + courses: { + created: [], + updated: [], + errors: [], + }, + professors: { + created: [], + updated: [], + errors: [], + }, + lectures: { + created: [], + updated: [], + chargeUpdated: [], + deleted: [], + errors: [], + }, + }; + + const staffProfessor = + await this.syncRepository.getOrCreateStaffProfessor(); + + /// Department update + const existingDepartments = + await this.syncRepository.getExistingDepartments(); + const departmentMap: Record = Object.fromEntries( + existingDepartments.map((dept) => [dept.id, dept]), + ); + this.slackNoti.sendSyncNoti( + `Found ${existingDepartments.length} existing departments, updating...`, + ); + const processedDepartmentIds = new Set(); + for (const lecture of data.lectures) { + try { + if (processedDepartmentIds.has(lecture.dept_id)) continue; // skip if already processed + processedDepartmentIds.add(lecture.dept_id); + + const departmentInfo = this.deriveDepartmentInfo(lecture); + const foundDepartment = departmentMap[lecture.dept_id]; + + // No department found, create new department + if (!foundDepartment) { + const newDept = await this.syncRepository.createDepartment( + departmentInfo, + ); + departmentMap[newDept.id] = newDept; + result.departments.created.push(newDept); + + const deptsToMakeInvisible = existingDepartments.filter( + (l) => l.code === newDept.code && l.visible, + ); + await Promise.all( + deptsToMakeInvisible.map((l) => + this.syncRepository.updateDepartment(l.id, { visible: false }), + ), + ); + } else if (this.departmentChanged(foundDepartment, departmentInfo)) { + const updated = await this.syncRepository.updateDepartment( + foundDepartment.id, + { + num_id: departmentInfo.num_id, + code: departmentInfo.code, + name: departmentInfo.name, + name_en: departmentInfo.name_en, + }, + ); + departmentMap[foundDepartment.id] = updated; + result.departments.updated.push([foundDepartment, updated]); + } + } catch (e: any) { + result.departments.errors.push({ + dept_id: lecture.dept_id, + error: e.message || 'Unknown error', + }); + } + } + this.slackNoti.sendSyncNoti( + `Department created: ${result.departments.created.length}, updated: ${result.departments.updated.length}, errors: ${result.departments.errors.length}`, + ); + + /// Course update + const lectureByCode = new Map( + data.lectures.map((l) => [l.old_no, l] as const), + ); + const existingCourses = + await this.syncRepository.getExistingCoursesByOldCodes( + Array.from(lectureByCode.keys()), + ); + this.slackNoti.sendSyncNoti( + `Found ${existingCourses.length} existing related courses, updating...`, + ); + const courseMap = new Map( + existingCourses.map((l) => [l.old_code, l] as const), + ); + for (const [old_code, lecture] of lectureByCode.entries()) { + try { + const foundCourse = courseMap.get(old_code); + const derivedCourse = this.deriveCourseInfo(lecture); + if (!foundCourse) { + const newCourse = await this.syncRepository.createCourse( + derivedCourse, + ); + result.courses.created.push(newCourse); + courseMap.set(old_code, newCourse); + } else { + if (this.courseChanged(foundCourse, derivedCourse)) { + const updatedCourse = await this.syncRepository.updateCourse( + foundCourse.id, + derivedCourse, + ); + result.courses.updated.push([foundCourse, updatedCourse]); + courseMap.set(old_code, updatedCourse); + } + } + } catch (e: any) { + result.courses.errors.push({ + old_code, + error: e.message || 'Unknown error', + }); + } + } + this.slackNoti.sendSyncNoti( + `Course created: ${result.courses.created.length}, updated: ${result.courses.updated.length}, errors: ${result.courses.errors.length}`, + ); + + // Professor update + const existingProfessors = + await this.syncRepository.getExistingProfessorsById( + data.charges.map((c) => c.prof_id), + ); + const professorMap = new Map( + existingProfessors.map((p) => [p.professor_id, p]), + ); + this.slackNoti.sendSyncNoti( + `Found ${existingProfessors.length} existing related professors, updating...`, + ); + const processedProfessorIds = new Set(); + for (const charge of data.charges) { + try { + // TODO: 아래 로직 변경 필요할 수 있음? id는 staff id인데 실제 강사 이름이 들어있음. 데이터에서 staff id 830으로 확인 바람. + // 기존 코드에서도 이렇게 처리하고 있었음. + // staff id인 경우 이름이 각자 다를 수 있다는 것임. + if (charge.prof_id === staffProfessor.professor_id) continue; + if (processedProfessorIds.has(charge.prof_id)) continue; + processedProfessorIds.add(charge.prof_id); + + const professor = professorMap.get(charge.prof_id); + const derivedProfessor = this.deriveProfessorInfo(charge); + + if (!professor) { + const newProfessor = await this.syncRepository.createProfessor( + derivedProfessor, + ); + professorMap.set(charge.prof_id, newProfessor); + result.professors.created.push(newProfessor); + } else if (this.professorChanged(professor, derivedProfessor)) { + const updatedProfessor = await this.syncRepository.updateProfessor( + professor.id, + derivedProfessor, + ); + professorMap.set(charge.prof_id, updatedProfessor); + result.professors.updated.push([professor, updatedProfessor]); + } + } catch (e: any) { + result.professors.errors.push({ + prof_id: charge.prof_id, + error: e.message || 'Unknown error', + }); + } + } + this.slackNoti.sendSyncNoti( + `Professor created: ${result.professors.created.length}, updated: ${result.professors.updated.length}, errors: ${result.professors.errors.length}`, + ); + + /// Lecture update + const existingLectures = + await this.syncRepository.getExistingDetailedLectures({ + year: data.year, + semester: data.semester, + }); + this.slackNoti.sendSyncNoti( + `Found ${existingLectures.length} existing lectures, updating...`, + ); + const notExistingLectures = new Set(existingLectures.map((l) => l.id)); + for (const lecture of data.lectures) { + try { + const foundLecture = existingLectures.find( + (l) => + l.code === lecture.subject_no && + l.class_no === lecture.lecture_class.trim(), + ); + const course_id = courseMap.get(lecture.old_no)?.id; + if (!course_id) + throw new Error(`Course not found for lecture ${lecture.subject_no}`); + const derivedLecture = this.deriveLectureInfo(lecture, course_id); + const professorCharges = data.charges.filter( + (c) => + c.lecture_year === lecture.lecture_year && + c.lecture_term === lecture.lecture_term && + c.subject_no === lecture.subject_no && + c.lecture_class.trim() === lecture.lecture_class.trim(), + ); + + if (foundLecture) { + notExistingLectures.delete(foundLecture.id); + if (this.lectureChanged(foundLecture, derivedLecture)) { + const updatedLecture = await this.syncRepository.updateLecture( + foundLecture.id, + derivedLecture, + ); + result.lectures.updated.push([foundLecture, updatedLecture]); + } + const { addedIds, removedIds } = this.lectureProfessorsChanges( + foundLecture, + professorCharges, + professorMap, + ); + + if (addedIds.length || removedIds.length) { + await this.syncRepository.updateLectureProfessors(foundLecture.id, { + added: addedIds, + removed: removedIds, + }); + result.lectures.chargeUpdated.push({ + lecture: foundLecture, + added: addedIds.map((id) => professorMap.get(id)), + removed: removedIds.map((id) => professorMap.get(id) || { id }), + }); + } + } else { + const newLecture = await this.syncRepository.createLecture( + derivedLecture, + ); + const addedIds = professorCharges.map( + (charge) => professorMap.get(charge.prof_id)!.id, + ); + + await this.syncRepository.updateLectureProfessors(newLecture.id, { + added: addedIds, + removed: [], + }); + result.lectures.created.push({ ...newLecture, professors: addedIds }); + } + } catch (e: any) { + result.lectures.errors.push({ + lecture: { + code: lecture.subject_no, + class_no: lecture.lecture_class, + }, + error: e.message || 'Unknown error', + }); + } + } + + // Remove not existing lectures + try { + await this.syncRepository.markLecturesDeleted( + Array.from(notExistingLectures), + ); + result.lectures.deleted = Array.from(notExistingLectures); + } catch (e: any) { + result.lectures.errors.push({ + lecturesToDelete: Array.from(notExistingLectures), + error: e.message || 'Unknown error', + }); + } + + this.slackNoti.sendSyncNoti( + `Lecture created: ${result.lectures.created.length}, updated: ${result.lectures.updated.length}, errors: ${result.lectures.errors.length}`, + ); + + return result; + } + + deriveDepartmentInfo( + lecture: ISync.ScholarLectureType, + ): LectureDerivedDepartmentInfo { + return { + id: lecture.dept_id, + num_id: lecture.subject_no.slice(0, 2), // TODO: This will be changed in new API + code: this.extract_dept_code(lecture.old_no), + name: lecture.dept_name, + name_en: lecture.e_dept_name, + }; + } + + extract_dept_code(lectureCode: string) { + const code = lectureCode.match(/([a-zA-Z]+)(\d+)/)?.[1]; + if (!code) + throw new Error(`Failed to extract department code from ${lectureCode}`); + return code; + } + + departmentChanged( + dept: EDepartment.Basic, + newData: LectureDerivedDepartmentInfo, + ) { + return ( + dept.num_id !== newData.num_id || + dept.code !== newData.code || + dept.name !== newData.name || + dept.name_en !== newData.name_en + ); + } + + deriveCourseInfo( + lecture: ISync.ScholarLectureType, + ): LectureDerivedCourseInfo { + return { + old_code: lecture.old_no, + department_id: lecture.dept_id, + type: lecture.subject_type, + type_en: lecture.e_subject_type, + title: lecture.sub_title.split('<')[0].split('[')[0].trim(), + title_en: lecture.e_sub_title.split('<')[0].split('[')[0].trim(), + }; + } + + courseChanged(course: ECourse.Basic, newData: LectureDerivedCourseInfo) { + return ( + course.department_id !== newData.department_id || + course.type !== newData.type || + course.type_en !== newData.type_en || + course.title !== newData.title || + course.title_en !== newData.title_en + ); + } + + deriveProfessorInfo( + charge: ISync.ScholarChargeType, + ): ChargeDerivedProfessorInfo { + return { + professor_id: charge.prof_id, + professor_name: charge.prof_name.trim(), + professor_name_en: charge.e_prof_name?.trim() || '', + major: charge.dept_id.toString(), + }; + } + + professorChanged( + professor: EProfessor.Basic, + newData: ChargeDerivedProfessorInfo, + ) { + return ( + professor.professor_name !== newData.professor_name || + professor.professor_name_en !== newData.professor_name_en || + professor.major !== newData.major + ); + } + + deriveLectureInfo( + lecture: ISync.ScholarLectureType, + course_id: number, + ): DerivedLectureInfo { + return { + code: lecture.subject_no, + year: lecture.lecture_year, + semester: lecture.lecture_term, + class_no: lecture.lecture_class.trim(), + department_id: lecture.dept_id, + old_code: lecture.old_no, + title: lecture.sub_title, + title_en: lecture.e_sub_title, + type: lecture.subject_type, + type_en: lecture.e_subject_type, + audience: lecture.course_sect, + limit: lecture.limit, + credit: lecture.credit, + credit_au: lecture.act_unit, + num_classes: lecture.lecture, + num_labs: lecture.lab, + is_english: lecture.english_lec === 'Y', + course_id, + }; + } + + lectureChanged(lecture: ELecture.Details, newData: DerivedLectureInfo) { + return ( + lecture.code !== newData.code || + lecture.year !== newData.year || + lecture.semester !== newData.semester || + lecture.class_no !== newData.class_no || + lecture.department_id !== newData.department_id || + lecture.old_code !== newData.old_code || + lecture.title !== newData.title || + lecture.title_en !== newData.title_en || + lecture.type !== newData.type || + lecture.type_en !== newData.type_en || + lecture.audience !== newData.audience || + lecture.limit !== newData.limit || + lecture.credit !== newData.credit || + lecture.credit_au !== newData.credit_au || + lecture.num_classes !== newData.num_classes || + lecture.num_labs !== newData.num_labs || + lecture.is_english !== newData.is_english || + lecture.course_id !== newData.course_id + ); + } + + lectureProfessorsChanges( + lecture: ELecture.Details, + charges: ISync.ScholarChargeType[], + professorMap: Map, + ): { addedIds: number[]; removedIds: number[] } { + const addedIds = charges + .filter( + (charge) => + !lecture.subject_lecture_professors.find( + (p) => p.professor.professor_id === charge.prof_id, + ), + ) + .map((charge) => professorMap.get(charge.prof_id)!.id); + const removedIds = lecture.subject_lecture_professors + .filter( + (p) => + !charges.find( + (charge) => charge.prof_id === p.professor.professor_id, + ), + ) + .map((p) => p.professor.id); + return { + addedIds: Array.from(new Set(addedIds)), + removedIds: Array.from(new Set(removedIds)), + }; + } + + async syncExamtime(data: ISync.ExamtimeBody) { + return this.syncTime( + data.year, + data.semester, + data.examtimes, + 'examtime', + this.deriveExamtimeInfo, + this.examtimeMatches, + ); + } + + async syncClassTime(data: ISync.ClasstimeBody) { + return this.syncTime( + data.year, + data.semester, + data.classtimes, + 'classtime', + this.deriveClasstimeInfo, + this.classtimeMatches, + ); + } + + async syncTime< + TYPE extends 'examtime' | 'classtime', + T extends TYPE extends 'examtime' + ? ISync.ExamtimeType + : ISync.ClasstimeType, + D extends TYPE extends 'examtime' + ? DerivedExamtimeInfo + : DerivedClasstimeInfo, + >( + year: number, + semester: number, + data: T[], + type: TYPE, + deriveInfo: (time: T) => D, + matches: (derivedTime: D, existingTime: any) => boolean, + ) { + this.slackNoti.sendSyncNoti( + `sync ${type} ${year}-${semester}: ${data.length} ${type}s`, + ); + const result: any = { + time: new Date().toISOString(), + updated: [], + skipped: [], + errors: [], + }; + + const existingLectures = + await this.syncRepository.getExistingDetailedLectures({ + year: year, + semester: semester, + }); + + this.slackNoti.sendSyncNoti( + `Found ${existingLectures.length} existing lectures, updating ${type}s...`, + ); + + const lecturePairMap = new Map( + existingLectures.map((l) => [l.id, [l, []]]), + ); + + for (const time of data) { + const lecture = existingLectures.find( + (l) => + l.code === time.subject_no && + l.class_no === time.lecture_class.trim(), + ); + if (!lecture) { + result.skipped.push({ + subject_no: time.subject_no, + lecture_class: time.lecture_class, + error: 'Lecture not found', + }); + continue; + } + const [, times] = lecturePairMap.get(lecture.id)!; + times.push(time); + } + + for (const [lecture, times] of lecturePairMap.values()) { + try { + const derivedTimes = times.map(deriveInfo); + const existingTimes = + type === 'examtime' + ? lecture.subject_examtime + : lecture.subject_classtime; + const timesToRemove = []; + + for (const existing of existingTimes) { + const idx = derivedTimes.findIndex((t) => matches(t, existing)); + if (idx === -1) timesToRemove.push(existing.id); + else derivedTimes.splice(idx, 1); // remove matched time + } + const timesToAdd = derivedTimes; + if (type === 'examtime') + await this.syncRepository.updateLectureExamtimes(lecture.id, { + added: timesToAdd, + removed: timesToRemove, + }); + else + await this.syncRepository.updateLectureClasstimes(lecture.id, { + added: timesToAdd as any, + removed: timesToRemove, + }); + + if (timesToAdd.length > 0 || timesToRemove.length > 0) { + result.updated.push({ + lecture: lecture.code, + class_no: lecture.class_no, + previous: existingTimes, + added: timesToAdd, + removed: timesToRemove, + }); + } + } catch (e: any) { + result.errors.push({ + lecture: { + code: lecture.code, + class_no: lecture.class_no, + }, + error: e.message || 'Unknown error', + }); + } + } + + this.slackNoti.sendSyncNoti( + `${type.charAt(0).toUpperCase() + type.slice(1)} updated: ${ + result.updated.length + }, skipped: ${result.skipped.length}, errors: ${result.errors.length}`, + ); + + return result; + } + + deriveExamtimeInfo(examtime: ISync.ExamtimeType): DerivedExamtimeInfo { + return { + day: examtime.exam_day - 1, + begin: new Date('1970-01-01T' + examtime.exam_begin.slice(11) + 'Z'), + end: new Date('1970-01-01T' + examtime.exam_end.slice(11) + 'Z'), + }; + } + + examtimeMatches( + examtime: DerivedExamtimeInfo, + existing: ELecture.Details['subject_examtime'][number], + ) { + return ( + examtime.day === existing.day && + examtime.begin.getHours() === existing.begin.getHours() && + examtime.begin.getMinutes() === existing.begin.getMinutes() + ); + } + + deriveClasstimeInfo(classTime: ISync.ClasstimeType): DerivedClasstimeInfo { + return { + day: classTime.lecture_day - 1, + begin: new Date('1970-01-01T' + classTime.lecture_begin.slice(11) + 'Z'), + end: new Date('1970-01-01T' + classTime.lecture_end.slice(11) + 'Z'), + type: classTime.lecture_type, + building_id: classTime.building.toString(), + room_name: classTime.room_no, + building_full_name: classTime.room_k_name, + building_full_name_en: classTime.room_e_name, + unit_time: classTime.teaching, + }; + } + + classtimeMatches( + classtime: DerivedClasstimeInfo, + existing: ELecture.Details['subject_classtime'][number], + ) { + return ( + classtime.day === existing.day && + classtime.begin.getHours() === existing.begin.getHours() && + classtime.begin.getMinutes() === existing.begin.getMinutes() && + classtime.type === existing.type && + classtime.building_id === existing.building_id && + classtime.room_name === existing.room_name && + classtime.building_full_name === existing.building_full_name && + classtime.building_full_name_en === existing.building_full_name_en && + classtime.unit_time === existing.unit_time + ); + } +} diff --git a/src/modules/sync/syncTakenLecture.service.spec.ts b/src/modules/sync/syncTakenLecture.service.spec.ts new file mode 100644 index 00000000..c28dab67 --- /dev/null +++ b/src/modules/sync/syncTakenLecture.service.spec.ts @@ -0,0 +1,233 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { AppModule } from '@src/app.module'; +import { PrismaService } from '@src/prisma/prisma.service'; +import { SyncTakenLectureService } from './syncTakenLecture.service'; + +// This tests on test database only. Add `DATABASE_URL` with `otlplus_test` database to run this test. + +const maybe = process.env.DATABASE_URL?.includes('/otlplus_test?') + ? describe + : describe.skip; + +const userData = [...Array(5).keys()].map((i) => ({ + id: i + 1, + student_id: `3000000${i}`, + sid: i.toString(), + date_joined: new Date(), + first_name: 'test', + last_name: 'test', +})); + +const departmentData = [...Array(2).keys()].map((i) => ({ + id: i + 1, + num_id: (10 + i).toString(), + code: String.fromCharCode(65 + i), + name: `학과${i}`, + name_en: `department${i}`, + visible: true, +})); +const courseData = [...Array(2).keys()].map((i) => ({ + id: i + 1, + old_code: `${departmentData[i].code}10${i}`, + department_id: departmentData[i].id, + type: '전공', + type_en: 'major', + title: `과목 ${i}`, + title_en: `subject ${i}`, + summury: '', + grade_sum: 0, + load_sum: 0, + speech_sum: 0, + review_total_weight: 0, + grade: 0, + load: 0, + speech: 0, + title_no_space: `과목${i}`, + title_en_no_space: `subject${i}`, +})); + +const lectureData = [...Array(2).keys()].map((i) => ({ + id: i + 1, + code: `${departmentData[i].num_id}:10${i}`, + year: 3000, + semester: 1, + class_no: String.fromCharCode(65 + i), + department_id: departmentData[i].id, + old_code: courseData[i].old_code, + title: courseData[i].title, + title_en: courseData[i].title_en, + type: courseData[i].type, + type_en: courseData[i].type_en, + audience: 0, + limit: 0, + credit: 3, + credit_au: 0, + num_classes: 3, + num_labs: 0, + is_english: false, + course_id: courseData[i].id, + deleted: false, + grade_sum: 0, + load_sum: 0, + speech_sum: 0, + review_total_weight: 0, + grade: 0, + load: 0, + speech: 0, + title_no_space: courseData[i].title_no_space, + title_en_no_space: courseData[i].title_en_no_space, +})); + +const takenLectureData = [...Array(2).keys()].map((i) => ({ + id: i + 1, + userprofile_id: userData[i].id, + lecture_id: lectureData[i].id, +})); + +const attendBase = { + lecture_year: 3000, + lecture_term: 1, + subject_no: lectureData[0].code, + lecture_class: lectureData[0].class_no, + dept_id: departmentData[0].id, + student_no: parseInt(userData[0].student_id), + process_type: 'I', +} as const; + +maybe('SyncTakenLectureService', () => { + let service: SyncTakenLectureService; + let prisma: PrismaService; + beforeAll(async () => { + const module: TestingModule = await Test.createTestingModule({ + imports: [AppModule], + providers: [], + }).compile(); + + service = module.get(SyncTakenLectureService); + prisma = module.get(PrismaService); + + await prisma.session_userprofile_taken_lectures.deleteMany(); + await prisma.session_userprofile.deleteMany(); + await prisma.subject_lecture_professors.deleteMany(); + await prisma.subject_lecture.deleteMany(); + await prisma.subject_course.deleteMany(); + await prisma.subject_department.deleteMany(); + + await prisma.session_userprofile.createMany({ data: userData }); + await prisma.subject_department.createMany({ data: departmentData }); + await prisma.subject_course.createMany({ data: courseData }); + await prisma.subject_lecture.createMany({ data: lectureData }); + }); + + afterAll(async () => { + await prisma.session_userprofile_taken_lectures.deleteMany(); + await prisma.session_userprofile.deleteMany(); + await prisma.subject_lecture_professors.deleteMany(); + await prisma.subject_lecture.deleteMany(); + await prisma.subject_course.deleteMany(); + await prisma.subject_department.deleteMany(); + }); + + beforeEach(async () => { + await prisma.session_userprofile_taken_lectures.createMany({ + data: takenLectureData, + }); + }); + + afterEach(async () => { + await prisma.session_userprofile_taken_lectures.deleteMany(); + }); + + it('should be defined', () => { + expect(service).toBeDefined(); + }); + + it('should update taken lectures', async () => { + const result = await service.syncTakenLecture({ + year: 3000, + semester: 1, + attend: [ + { + ...attendBase, + subject_no: lectureData[1].code, + lecture_class: lectureData[1].class_no, + dept_id: departmentData[1].id, + }, + ], + }); + + expect(result.updated).toHaveLength(2); + expect(result.errors).toHaveLength(0); + expect( + result.updated.filter( + (u: any) => u.studentId.toString() === userData[0].student_id, + )[0], + ).toMatchObject({ + studentId: parseInt(userData[0].student_id), + add: [lectureData[1].id], + remove: [takenLectureData[0].id], + }); + expect( + result.updated.filter( + (u: any) => u.studentId.toString() === userData[1].student_id, + )[0], + ).toMatchObject({ + studentId: parseInt(userData[1].student_id), + add: [], + remove: [takenLectureData[1].id], + }); + + const takenLectures = + await prisma.session_userprofile_taken_lectures.findMany(); + expect(takenLectures).toHaveLength(1); + expect(takenLectures[0]).toMatchObject({ + userprofile_id: 1, + lecture_id: 2, + }); + }); + + it('should handle error', async () => { + const result = await service.syncTakenLecture({ + year: 3000, + semester: 1, + attend: [ + { + ...attendBase, + subject_no: 'invalid', + }, + ], + }); + + expect(result.errors).toHaveLength(1); + expect(result.errors[0]).toMatchObject({ + student_no: parseInt(userData[0].student_id), + attend: { + ...attendBase, + subject_no: 'invalid', + }, + error: 'lecture not found', + }); + }); + + it('should not update if no change', async () => { + const result = await service.syncTakenLecture({ + year: 3000, + semester: 1, + attend: [ + { + ...attendBase, + }, + { + ...attendBase, + student_no: parseInt(userData[1].student_id), + subject_no: lectureData[1].code, + lecture_class: lectureData[1].class_no, + dept_id: departmentData[1].id, + }, + ], + }); + + expect(result.updated).toHaveLength(0); + expect(result.errors).toHaveLength(0); + }); +}); diff --git a/src/modules/sync/syncTakenLecture.service.ts b/src/modules/sync/syncTakenLecture.service.ts new file mode 100644 index 00000000..fb3b1691 --- /dev/null +++ b/src/modules/sync/syncTakenLecture.service.ts @@ -0,0 +1,178 @@ +import { Injectable } from '@nestjs/common'; +import { ELecture } from '@src/common/entities/ELecture'; +import { ETakenLecture } from '@src/common/entities/ETakenLecture'; +import { ISync } from '@src/common/interfaces/ISync'; +import { SyncRepository } from 'src/prisma/repositories/sync.repository'; +import { SlackNotiService } from './slackNoti.service'; + +@Injectable() +export class SyncTakenLectureService { + constructor( + private readonly syncRepository: SyncRepository, + private readonly slackNoti: SlackNotiService, + ) {} + + async syncTakenLecture(data: ISync.TakenLectureBody) { + this.slackNoti.sendSyncNoti( + `syncTakenLecture: ${data.year}-${data.semester}: ${data.attend.length} attend records`, + ); + + const result: any = { + time: new Date().toISOString(), + updated: [], + errors: [], + }; + + const existingLectures = + await this.syncRepository.getExistingDetailedLectures({ + year: data.year, + semester: data.semester, + }); + const existingUserTakenLectures = ( + await this.syncRepository.getUserExistingTakenLectures({ + year: data.year, + semester: data.semester, + }) + ).filter((u) => !Number.isNaN(parseInt(u.student_id))); + this.slackNoti.sendSyncNoti( + `Found ${existingLectures.length} existing lectures, ${existingUserTakenLectures.length} existing user with taken records`, + ); + const studentIds = Array.from( + new Set([ + ...data.attend.map((a) => a.student_no), + ...existingUserTakenLectures.map((u) => parseInt(u.student_id)), + ]), + ); + + /** 여러 user가 동일한 student_id를 가질 수 있음 (실제로 존재) + * 따라서 student_id를 key로 하여 [taken_lectures, attend_lecture_ids, userprofile_id]를 저장 + * [0]]: 이미 DB에 저장된 수강 강의 목록. 동일한 student_id 여러 user들이 합쳐져있다. + * [1]: attend에서 찾은 새로운 lecture_id 목록 + * [2]: student_id에 해당하는 userprofile id 목록 + */ + const studentPairMap = new Map< + number, + [ETakenLecture.Basic[], number[], number[]] + >(); + + for (const studentId of studentIds) { + studentPairMap.set(studentId, [[], [], []]); + } + + for (const user of existingUserTakenLectures) { + const student_id = parseInt(user.student_id); + const pair = studentPairMap.get(student_id)!; + pair[0].push(...user.taken_lectures); + } + + for (const attend of data.attend) { + const lectureId = this.getLectureIdOfAttendRecord( + existingLectures, + attend, + ); + if (lectureId) { + const pair = studentPairMap.get(attend.student_no)!; + pair[1].push(lectureId); + } else + result.errors.push({ + student_no: attend.student_no, + attend, + error: 'lecture not found', + }); + } + + const userprofiles = + await this.syncRepository.getUserProfileIdsFromStudentIds(studentIds); + for (const user of userprofiles) { + const student_id = parseInt(user.student_id); + const pair = studentPairMap.get(student_id)!; + pair[2].push(user.id); + } + + const saveToDB = []; + let skipCount = 0; + for (const [ + studentId, + [existingTakenLectures, attendRecords, userprofileIds], + ] of studentPairMap) { + try { + if (attendRecords.length) + saveToDB.push( + ...attendRecords.map((lectureId) => ({ + studentId, + lectureId, + })), + ); + + if (userprofileIds.length === 0) { + skipCount++; + continue; + } + for (const userprofileId of userprofileIds) { + const recordIdsToRemove = []; + const recordsToAdd = [...attendRecords]; + for (const existing of existingTakenLectures.filter( + (e) => e.userprofile_id === userprofileId, + )) { + const idx = recordsToAdd.indexOf(existing.lecture_id); + if (idx === -1) recordIdsToRemove.push(existing.id); + else recordsToAdd.splice(idx, 1); + } + + if (recordIdsToRemove.length || recordsToAdd.length) { + await this.syncRepository.updateTakenLectures(userprofileId, { + remove: recordIdsToRemove, + add: recordsToAdd, + }); + result.updated.push({ + studentId, + remove: recordIdsToRemove.map( + (id) => + existingTakenLectures.find((e) => e.id === id)?.lecture_id, + ), + add: recordsToAdd, + }); + } + } + } catch (e: any) { + result.errors.push({ studentId, error: e.message || 'Unknown error' }); + } + } + + await this.syncRepository.replaceRawTakenLectures(saveToDB, { + year: data.year, + semester: data.semester, + }); + + this.slackNoti.sendSyncNoti( + `syncTakenLecture: ${result.updated.length} updated, ${skipCount} skipped, ${result.errors.length} errors`, + ); + + return result; + } + + getLectureIdOfAttendRecord( + lectures: ELecture.Basic[], + attend: ISync.AttendType, + ) { + const lecture = lectures.find( + (l) => + l.code === attend.subject_no && + l.class_no === attend.lecture_class.trim(), + ); + return lecture?.id; + } + + async repopulateTakenLectureForStudent(userId: number) { + const user = await this.syncRepository.getUserWithId(userId); + if (!user) throw new Error('User not found'); + const studentId = parseInt(user.student_id); + if (Number.isNaN(studentId)) return; // Skip if student_id is not a number + const rawTakenLectures = + await this.syncRepository.getRawTakenLecturesOfStudent(studentId); + await this.syncRepository.repopulateTakenLecturesOfUser( + studentId, + rawTakenLectures, + ); + } +} diff --git a/src/modules/sync/types.ts b/src/modules/sync/types.ts new file mode 100644 index 00000000..f5852902 --- /dev/null +++ b/src/modules/sync/types.ts @@ -0,0 +1,62 @@ +export type LectureDerivedDepartmentInfo = { + id: number; + num_id: string; + code: string; + name: string; + name_en: string; +}; + +export type LectureDerivedCourseInfo = { + old_code: string; + department_id: number; + type: string; + type_en: string; + title: string; + title_en: string; +}; + +export type ChargeDerivedProfessorInfo = { + professor_id: number; + professor_name: string; + professor_name_en: string; + major: string; +}; + +export type DerivedLectureInfo = { + code: string; + year: number; + semester: number; + class_no: string; + department_id: number; + old_code: string; + title: string; + title_en: string; + type: string; + type_en: string; + audience: number; + limit: number; + credit: number; + credit_au: number; + num_classes: number; + num_labs: number; + is_english: boolean; + course_id: number; +}; + +export type DerivedExamtimeInfo = { + day: number; + begin: Date; + end: Date; +}; + +export type DerivedClasstimeInfo = { + day: number; + begin: Date; + end: Date; + type: 'l' | 'e'; + building_id: string; + room_name: string; + building_full_name: string; + building_full_name_en: string; + unit_time: number; +}; diff --git a/src/prisma/middleware/prisma.lectureprofessors.ts b/src/prisma/middleware/prisma.lectureprofessors.ts index 1c2fbc1e..f47fc022 100644 --- a/src/prisma/middleware/prisma.lectureprofessors.ts +++ b/src/prisma/middleware/prisma.lectureprofessors.ts @@ -30,7 +30,10 @@ export class LectureProfessorsMiddleware operations === 'delete' || operations === 'deleteMany' ) { - const lectureId = result.lecture_id; + const lectureId = result.lecture_id || args?.where?.lecture_id; + if (!lectureId) { + console.warn("lecture_id not found. Can't recalculate lecture score."); + } const lecture = await this.prisma.subject_lecture.findUniqueOrThrow({ where: { id: lectureId }, }); diff --git a/src/prisma/migrations/20241025174157_sync_taken_lectures/migration.sql b/src/prisma/migrations/20241025174157_sync_taken_lectures/migration.sql new file mode 100644 index 00000000..c69fb51b --- /dev/null +++ b/src/prisma/migrations/20241025174157_sync_taken_lectures/migration.sql @@ -0,0 +1,14 @@ +-- CreateTable +CREATE TABLE `sync_taken_lectures` ( + `id` INTEGER NOT NULL AUTO_INCREMENT, + `year` INTEGER NOT NULL, + `semester` INTEGER NOT NULL, + `student_id` INTEGER NOT NULL, + `lecture_id` INTEGER NOT NULL, + + UNIQUE INDEX `sync_taken_lectures_student_id_lecture_id_key`(`student_id`, `lecture_id`), + PRIMARY KEY (`id`) +) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; + +-- AddForeignKey +ALTER TABLE `sync_taken_lectures` ADD CONSTRAINT `sync_taken_lectures_lecture_id_fkey` FOREIGN KEY (`lecture_id`) REFERENCES `subject_lecture`(`id`) ON DELETE CASCADE ON UPDATE CASCADE; diff --git a/src/prisma/migrations/20241025180646_sync_taken_lectures_year_semester_index/migration.sql b/src/prisma/migrations/20241025180646_sync_taken_lectures_year_semester_index/migration.sql new file mode 100644 index 00000000..95a90535 --- /dev/null +++ b/src/prisma/migrations/20241025180646_sync_taken_lectures_year_semester_index/migration.sql @@ -0,0 +1,2 @@ +-- CreateIndex +CREATE INDEX `sync_taken_lectures_year_semester_idx` ON `sync_taken_lectures`(`year`, `semester`); diff --git a/src/prisma/migrations/20241105080413_department_name_en_length/migration.sql b/src/prisma/migrations/20241105080413_department_name_en_length/migration.sql new file mode 100644 index 00000000..f3ba3bd2 --- /dev/null +++ b/src/prisma/migrations/20241105080413_department_name_en_length/migration.sql @@ -0,0 +1,2 @@ +-- AlterTable +ALTER TABLE `subject_department` MODIFY `name_en` VARCHAR(100) NULL; diff --git a/src/prisma/migrations/20241105081301_course_type_type_en_length/migration.sql b/src/prisma/migrations/20241105081301_course_type_type_en_length/migration.sql new file mode 100644 index 00000000..7bdf073f --- /dev/null +++ b/src/prisma/migrations/20241105081301_course_type_type_en_length/migration.sql @@ -0,0 +1,3 @@ +-- AlterTable +ALTER TABLE `subject_course` MODIFY `type` VARCHAR(20) NOT NULL, + MODIFY `type_en` VARCHAR(50) NOT NULL; diff --git a/src/prisma/migrations/20241105082001_course_type_type_en_length_2/migration.sql b/src/prisma/migrations/20241105082001_course_type_type_en_length_2/migration.sql new file mode 100644 index 00000000..462ff5c5 --- /dev/null +++ b/src/prisma/migrations/20241105082001_course_type_type_en_length_2/migration.sql @@ -0,0 +1,3 @@ +-- AlterTable +ALTER TABLE `subject_course` MODIFY `type` VARCHAR(30) NOT NULL, + MODIFY `type_en` VARCHAR(60) NOT NULL; diff --git a/src/prisma/migrations/20241105082806_lecture_type_type_en_length/migration.sql b/src/prisma/migrations/20241105082806_lecture_type_type_en_length/migration.sql new file mode 100644 index 00000000..b8f2db7a --- /dev/null +++ b/src/prisma/migrations/20241105082806_lecture_type_type_en_length/migration.sql @@ -0,0 +1,3 @@ +-- AlterTable +ALTER TABLE `subject_lecture` MODIFY `type` VARCHAR(30) NOT NULL, + MODIFY `type_en` VARCHAR(60) NOT NULL; diff --git a/src/prisma/migrations/20241110204737_classtime_room_name_length_increase/migration.sql b/src/prisma/migrations/20241110204737_classtime_room_name_length_increase/migration.sql new file mode 100644 index 00000000..2f08cef2 --- /dev/null +++ b/src/prisma/migrations/20241110204737_classtime_room_name_length_increase/migration.sql @@ -0,0 +1,2 @@ +-- AlterTable +ALTER TABLE `subject_classtime` MODIFY `room_name` VARCHAR(40) NULL; diff --git a/src/prisma/prisma.module.ts b/src/prisma/prisma.module.ts index 43a7c2df..a90ceeab 100644 --- a/src/prisma/prisma.module.ts +++ b/src/prisma/prisma.module.ts @@ -16,6 +16,7 @@ import { NoticesRepository } from './repositories/notices.repository'; import { PlannerRepository } from './repositories/planner.repository'; import { ReviewsRepository } from './repositories/review.repository'; import { SemesterRepository } from './repositories/semester.repository'; +import { SyncRepository } from './repositories/sync.repository'; import { TimetableRepository } from './repositories/timetable.repository'; import { TracksRepository } from './repositories/track.repository'; import { UserRepository } from './repositories/user.repository'; @@ -45,6 +46,7 @@ import { TranManager } from './transactionManager'; PlannerRepository, TracksRepository, NoticesRepository, + SyncRepository, ReviewMiddleware, TranManager, ], @@ -61,6 +63,7 @@ import { TranManager } from './transactionManager'; PlannerRepository, TracksRepository, NoticesRepository, + SyncRepository, ], }) export class PrismaModule implements OnModuleInit { diff --git a/src/prisma/repositories/sync.repository.ts b/src/prisma/repositories/sync.repository.ts new file mode 100644 index 00000000..c80a8e6b --- /dev/null +++ b/src/prisma/repositories/sync.repository.ts @@ -0,0 +1,325 @@ +import { Injectable } from '@nestjs/common'; +import { EDepartment } from '@src/common/entities/EDepartment'; +import { ELecture } from '@src/common/entities/ELecture'; +import { EProfessor } from '@src/common/entities/EProfessor'; +import { EUserProfile } from '@src/common/entities/EUserProfile'; +import { STAFF_ID } from '@src/common/interfaces/constants/professor'; +import { + ChargeDerivedProfessorInfo, + DerivedClasstimeInfo, + DerivedExamtimeInfo, + DerivedLectureInfo, + LectureDerivedCourseInfo, + LectureDerivedDepartmentInfo, +} from '@src/modules/sync/types'; +import { ESemester } from 'src/common/entities/ESemester'; +import { PrismaService } from '../prisma.service'; + +@Injectable() +export class SyncRepository { + constructor(private readonly prisma: PrismaService) {} + + async getDefaultSemester(): Promise { + const now = new Date(); + return await this.prisma.subject_semester.findFirst({ + where: { courseDesciptionSubmission: { lt: now } }, + orderBy: { courseDesciptionSubmission: 'desc' }, + }); + } + + async getExistingDetailedLectures({ + year, + semester, + }: { + year: number; + semester: number; + }): Promise { + return await this.prisma.subject_lecture.findMany({ + where: { year, semester, deleted: false }, // 기존 코드에서 한 번 삭제된 강의는 복구되지 않고 새로 생성하던 것으로 보임. + include: ELecture.Details.include, + }); + } + + async getOrCreateStaffProfessor(): Promise { + const staffProfessor = await this.prisma.subject_professor.findFirst({ + where: { professor_id: STAFF_ID }, + }); + if (staffProfessor) return staffProfessor; + return await this.prisma.subject_professor.create({ + data: { + professor_name: 'Staff', + professor_name_en: 'Staff', + professor_id: STAFF_ID, + major: '', + grade_sum: 0, + load_sum: 0, + speech_sum: 0, + review_total_weight: 0, + grade: 0, + load: 0, + speech: 0, + }, + }); + } + + async getExistingDepartments(): Promise { + return await this.prisma.subject_department.findMany(); + } + + async createDepartment(data: LectureDerivedDepartmentInfo) { + return await this.prisma.subject_department.create({ + data: { + id: data.id, + num_id: data.num_id, + code: data.code, + name: data.name, + name_en: data.name_en, + visible: true, + }, + }); + } + + async updateDepartment(id: number, data: Partial) { + return await this.prisma.subject_department.update({ + where: { id }, + data, + }); + } + + async getExistingCoursesByOldCodes(oldCodes: string[]) { + return await this.prisma.subject_course.findMany({ + where: { old_code: { in: oldCodes } }, + }); + } + + async createCourse(data: LectureDerivedCourseInfo) { + return await this.prisma.subject_course.create({ + data: { + old_code: data.old_code, + department_id: data.department_id, + type: data.type, + type_en: data.type_en, + title: data.title, + title_en: data.title_en, + title_en_no_space: data.title_en.replace(/\s/g, ''), + title_no_space: data.title.replace(/\s/g, ''), + summury: '', + grade_sum: 0, + load_sum: 0, + speech_sum: 0, + review_total_weight: 0, + grade: 0, + load: 0, + speech: 0, + }, + }); + } + + async updateCourse(id: number, data: Partial) { + return await this.prisma.subject_course.update({ + where: { id }, + data: { + ...data, + title_no_space: data.title && data.title.replace(/\s/g, ''), + title_en_no_space: data.title_en && data.title_en.replace(/\s/g, ''), + }, + }); + } + + async getExistingProfessorsById(professorIds: number[]) { + return await this.prisma.subject_professor.findMany({ + where: { professor_id: { in: professorIds } }, + }); + } + + async createProfessor(data: ChargeDerivedProfessorInfo) { + return await this.prisma.subject_professor.create({ + data: { + ...data, + grade_sum: 0, + load_sum: 0, + speech_sum: 0, + review_total_weight: 0, + grade: 0, + load: 0, + speech: 0, + }, + }); + } + + async updateProfessor(id: number, data: Partial) { + return await this.prisma.subject_professor.update({ + where: { id }, + data, + }); + } + + async createLecture(data: DerivedLectureInfo) { + return await this.prisma.subject_lecture.create({ + data: { + ...data, + deleted: false, + title_no_space: data.title.replace(/\s/g, ''), + title_en_no_space: data.title_en.replace(/\s/g, ''), + grade_sum: 0, + load_sum: 0, + speech_sum: 0, + grade: 0, + load: 0, + speech: 0, + review_total_weight: 0, + }, + }); + } + + async updateLecture(id: number, data: Partial) { + return await this.prisma.subject_lecture.update({ + where: { id }, + data, + }); + } + + async updateLectureProfessors( + id: number, + { added, removed }: { added: number[]; removed: number[] }, + ) { + if (removed.length) + await this.prisma.subject_lecture_professors.deleteMany({ + where: { + lecture_id: id, + professor_id: { in: removed }, + }, + }); + if (added.length) + await this.prisma.subject_lecture_professors.createMany({ + data: added.map((professor_id) => ({ + lecture_id: id, + professor_id, + })), + }); + } + + async markLecturesDeleted(ids: number[]) { + await this.prisma.subject_lecture.updateMany({ + where: { id: { in: ids } }, + data: { deleted: true }, + }); + } + + async updateLectureExamtimes( + id: number, + { added, removed }: { added: DerivedExamtimeInfo[]; removed: number[] }, + ) { + if (removed.length) + await this.prisma.subject_examtime.deleteMany({ + where: { id: { in: removed } }, + }); + if (added.length) + await this.prisma.subject_examtime.createMany({ + data: added.map((examtime) => ({ + lecture_id: id, + ...examtime, + })), + }); + } + + async updateLectureClasstimes( + id: number, + { added, removed }: { added: DerivedClasstimeInfo[]; removed: number[] }, + ) { + if (removed.length) + await this.prisma.subject_classtime.deleteMany({ + where: { id: { in: removed } }, + }); + if (added.length) + await this.prisma.subject_classtime.createMany({ + data: added.map((classtime) => ({ + lecture_id: id, + ...classtime, + })), + }); + } + + async getUserExistingTakenLectures({ + year, + semester, + }: { + year: number; + semester: number; + }): Promise { + return await this.prisma.session_userprofile.findMany({ + where: { taken_lectures: { some: { lecture: { year, semester } } } }, + include: { taken_lectures: { where: { lecture: { year, semester } } } }, + }); + } + + async getUserProfileIdsFromStudentIds(studentIds: number[]) { + return await this.prisma.session_userprofile.findMany({ + where: { student_id: { in: studentIds.map((id) => id.toString()) } }, + select: { id: true, student_id: true }, + }); + } + + async updateTakenLectures( + userprofile_id: number, + data: { + remove: number[]; + add: number[]; + }, + ) { + if (data.remove.length) + await this.prisma.session_userprofile_taken_lectures.deleteMany({ + where: { id: { in: data.remove } }, + }); + if (data.add.length) { + await this.prisma.session_userprofile_taken_lectures.createMany({ + data: data.add.map((lecture_id) => ({ userprofile_id, lecture_id })), + }); + } + } + async replaceRawTakenLectures( + data: { + studentId: number; + lectureId: number; + }[], + { year, semester }: { year: number; semester: number }, + ) { + await this.prisma.sync_taken_lectures.deleteMany({ + where: { year, semester }, + }); + await this.prisma.sync_taken_lectures.createMany({ + data: data.map(({ studentId, lectureId }) => ({ + year, + semester, + student_id: studentId, + lecture_id: lectureId, + })), + }); + } + + async getUserWithId(userId: number) { + return await this.prisma.session_userprofile.findUnique({ + where: { id: userId }, + }); + } + + async getRawTakenLecturesOfStudent(student_id: number) { + return ( + await this.prisma.sync_taken_lectures.findMany({ + where: { student_id }, + }) + ).map((l) => l.lecture_id); + } + + async repopulateTakenLecturesOfUser(userId: number, takenLectures: number[]) { + await this.prisma.session_userprofile_taken_lectures.deleteMany({ + where: { userprofile_id: userId }, + }); + await this.prisma.session_userprofile_taken_lectures.createMany({ + data: takenLectures.map((lecture_id) => ({ + userprofile_id: userId, + lecture_id, + })), + }); + } +} diff --git a/src/prisma/schema.prisma b/src/prisma/schema.prisma index d6a4952f..e6aa8ebb 100644 --- a/src/prisma/schema.prisma +++ b/src/prisma/schema.prisma @@ -358,7 +358,7 @@ model subject_classtime { building_id String? @db.VarChar(10) building_full_name String? @db.VarChar(60) building_full_name_en String? @db.VarChar(60) - room_name String? @db.VarChar(20) + room_name String? @db.VarChar(40) unit_time Int? @db.SmallInt lecture_id Int? subject_lecture subject_lecture? @relation(fields: [lecture_id], references: [id], onDelete: Restrict, onUpdate: Restrict, map: "subject_classtime_lecture_id_bf773e65_fk_subject_lecture_id") @@ -370,8 +370,8 @@ model subject_course { id Int @id @default(autoincrement()) old_code String @db.VarChar(10) department_id Int - type String @db.VarChar(12) - type_en String @db.VarChar(36) + type String @db.VarChar(30) + type_en String @db.VarChar(60) title String @db.VarChar(100) title_en String @db.VarChar(200) summury String @db.VarChar(400) @@ -454,7 +454,7 @@ model subject_department { num_id String @db.VarChar(4) code String @db.VarChar(5) name String @db.VarChar(60) - name_en String? @db.VarChar(60) + name_en String? @db.VarChar(100) visible Boolean graduation_additionaltrack graduation_additionaltrack[] graduation_majortrack graduation_majortrack[] @@ -490,8 +490,8 @@ model subject_lecture { class_no String @db.VarChar(4) title String @db.VarChar(100) title_en String @db.VarChar(200) - type String @db.VarChar(12) - type_en String @db.VarChar(36) + type String @db.VarChar(30) + type_en String @db.VarChar(60) audience Int credit Int num_classes Int @@ -527,6 +527,7 @@ model subject_lecture { timetable_oldtimetable_lectures timetable_oldtimetable_lectures[] timetable_timetable_lectures timetable_timetable_lectures[] timetable_wishlist_lectures timetable_wishlist_lectures[] + sync_taken_lectures sync_taken_lectures[] @@index([deleted], map: "subject_lecture_deleted_bedc6156_uniq") @@index([type_en], map: "subject_lecture_type_en_45ee2d3a_uniq") @@ -822,3 +823,15 @@ model subject_professor_course_list { @@unique([professor_id, course_id], map: "professor_id") } + +model sync_taken_lectures { + id Int @id @default(autoincrement()) + year Int + semester Int + student_id Int + lecture_id Int + subject_lecture subject_lecture @relation(fields: [lecture_id], references: [id], onDelete: Cascade, onUpdate: Cascade) + + @@unique([student_id, lecture_id]) + @@index([year, semester]) +} diff --git a/src/settings.ts b/src/settings.ts index 9408c031..722f19ba 100644 --- a/src/settings.ts +++ b/src/settings.ts @@ -13,6 +13,7 @@ export default () => { getJwtConfig: () => getJwtConfig(), getSsoConfig: () => getSsoConfig(), getCorsConfig: () => getCorsConfig(), + syncConfig: () => getSyncConfig(), getVersion: () => getVersion(), getStaticConfig: () => staticConfig(), }; @@ -107,6 +108,13 @@ const getSsoConfig = (): any => { }; }; +const getSyncConfig = () => { + return { + apiKey: process.env.SYNC_SECRET, + slackKey: process.env.SLACK_KEY, + }; +}; + const getVersion = () => { return String(process.env.npm_package_version); }; diff --git a/test/prisma.spec.ts b/test/prisma.spec.ts index 1e7a2d7e..6494cd1e 100644 --- a/test/prisma.spec.ts +++ b/test/prisma.spec.ts @@ -1,12 +1,11 @@ import { INestApplication } from '@nestjs/common'; import { Test, TestingModule } from '@nestjs/testing'; -import { AppModule } from '../src/app.module'; -import { PrismaService } from '../src/prisma/prisma.service'; -import { CourseRepository } from '../src/prisma/repositories/course.repository'; import { ELecture } from '@src/common/entities/ELecture'; import { applyOffset, applyOrder } from '@src/common/utils/search.utils'; import { LectureRepository } from '@src/prisma/repositories/lecture.repository'; -import { set } from 'date-fns'; +import { AppModule } from '../src/app.module'; +import { PrismaService } from '../src/prisma/prisma.service'; +import { CourseRepository } from '../src/prisma/repositories/course.repository'; describe('AppController (e2e)', () => { let app: INestApplication; @@ -126,7 +125,6 @@ describe('AppController (e2e)', () => { }, take: DEFAULT_LIMIT, }; - console.log(JSON.stringify(options, null, 2)); const queryResult = await prisma.subject_lecture.findMany(options); const levelFilteredResult = courseRepo.levelFilter( queryResult, @@ -138,7 +136,6 @@ describe('AppController (e2e)', () => { (['old_code', 'class_no'] ?? DEFAULT_ORDER) as (keyof ELecture.Details)[], ); const result = applyOffset(orderedQuery, 0); - console.log(JSON.stringify(result, null, 2)); }); // it("select classtime", async () => { diff --git a/test/session/session.spec.ts b/test/session/session.spec.ts index 4a1cbea9..00049d21 100644 --- a/test/session/session.spec.ts +++ b/test/session/session.spec.ts @@ -5,7 +5,7 @@ import { AppModule } from '../../src/app.module'; import { UserService } from '../../src/modules/user/user.service'; import { PrismaService } from '../../src/prisma/prisma.service'; -describe('AppController (e2e)', () => { +describe.skip('AppController (e2e)', () => { let app: INestApplication; beforeEach(async () => { @@ -38,6 +38,7 @@ describe('AppController (e2e)', () => { }) .then((user) => { try { + if (!user) return null; return userService.getProfile(user); } catch (e) { console.log('error with sid: ', sid); diff --git a/test/transaction.spec.ts b/test/transaction.spec.ts index 7ea82faf..f9e97212 100644 --- a/test/transaction.spec.ts +++ b/test/transaction.spec.ts @@ -1,14 +1,14 @@ import { INestApplication } from '@nestjs/common'; import { Test, TestingModule } from '@nestjs/testing'; +import { session_userprofile } from '@prisma/client'; +import { ECourse } from '@src/common/entities/ECourse'; +import { CoursesService } from '@src/modules/courses/courses.service'; import { AppModule } from '../src/app.module'; import { PrismaService } from '../src/prisma/prisma.service'; -import { TranManager } from '../src/prisma/transactionManager'; import { CourseRepository } from '../src/prisma/repositories/course.repository'; -import { ECourse } from '@src/common/entities/ECourse'; -import { CoursesService } from '@src/modules/courses/courses.service'; -import { session_userprofile } from '@prisma/client'; +import { TranManager } from '../src/prisma/transactionManager'; -describe('AppController (e2e)', () => { +describe.skip('AppController (e2e)', () => { let app: INestApplication; beforeEach(async () => {