From e60bcea014d667f4473be88b39caf62109ffdcee Mon Sep 17 00:00:00 2001 From: Yazeed Loonat Date: Mon, 8 May 2023 09:11:27 -0700 Subject: [PATCH 01/57] fix: adding env template --- backend_new/.env.template | 1 + 1 file changed, 1 insertion(+) create mode 100644 backend_new/.env.template diff --git a/backend_new/.env.template b/backend_new/.env.template new file mode 100644 index 0000000000..19b5164f20 --- /dev/null +++ b/backend_new/.env.template @@ -0,0 +1 @@ +DATABASE_URL="postgres://@localhost:5432/bloom_prisma" \ No newline at end of file From cc0717e1f77ca3a15b6a2395f797ca36debbcd3f Mon Sep 17 00:00:00 2001 From: Yazeed Loonat Date: Mon, 8 May 2023 09:23:30 -0700 Subject: [PATCH 02/57] feat: starter nest app --- backend_new/.eslintrc.js | 24 + backend_new/.gitignore | 35 + backend_new/.prettierrc | 4 + backend_new/README.md | 73 + backend_new/nest-cli.json | 4 + backend_new/package.json | 71 + backend_new/src/app.controller.spec.ts | 22 + backend_new/src/app.controller.ts | 12 + backend_new/src/app.module.ts | 10 + backend_new/src/app.service.ts | 8 + backend_new/src/main.ts | 8 + backend_new/test/app.e2e-spec.ts | 24 + backend_new/test/jest-e2e.json | 9 + backend_new/tsconfig.build.json | 4 + backend_new/tsconfig.json | 21 + backend_new/yarn.lock | 5109 ++++++++++++++++++++++++ 16 files changed, 5438 insertions(+) create mode 100644 backend_new/.eslintrc.js create mode 100644 backend_new/.gitignore create mode 100644 backend_new/.prettierrc create mode 100644 backend_new/README.md create mode 100644 backend_new/nest-cli.json create mode 100644 backend_new/package.json create mode 100644 backend_new/src/app.controller.spec.ts create mode 100644 backend_new/src/app.controller.ts create mode 100644 backend_new/src/app.module.ts create mode 100644 backend_new/src/app.service.ts create mode 100644 backend_new/src/main.ts create mode 100644 backend_new/test/app.e2e-spec.ts create mode 100644 backend_new/test/jest-e2e.json create mode 100644 backend_new/tsconfig.build.json create mode 100644 backend_new/tsconfig.json create mode 100644 backend_new/yarn.lock diff --git a/backend_new/.eslintrc.js b/backend_new/.eslintrc.js new file mode 100644 index 0000000000..f6c62bee27 --- /dev/null +++ b/backend_new/.eslintrc.js @@ -0,0 +1,24 @@ +module.exports = { + parser: '@typescript-eslint/parser', + parserOptions: { + project: 'tsconfig.json', + sourceType: 'module', + }, + plugins: ['@typescript-eslint/eslint-plugin'], + extends: [ + 'plugin:@typescript-eslint/recommended', + 'plugin:prettier/recommended', + ], + root: true, + env: { + node: true, + jest: true, + }, + ignorePatterns: ['.eslintrc.js'], + rules: { + '@typescript-eslint/interface-name-prefix': 'off', + '@typescript-eslint/explicit-function-return-type': 'off', + '@typescript-eslint/explicit-module-boundary-types': 'off', + '@typescript-eslint/no-explicit-any': 'off', + }, +}; diff --git a/backend_new/.gitignore b/backend_new/.gitignore new file mode 100644 index 0000000000..22f55adc56 --- /dev/null +++ b/backend_new/.gitignore @@ -0,0 +1,35 @@ +# compiled output +/dist +/node_modules + +# Logs +logs +*.log +npm-debug.log* +pnpm-debug.log* +yarn-debug.log* +yarn-error.log* +lerna-debug.log* + +# OS +.DS_Store + +# Tests +/coverage +/.nyc_output + +# IDEs and editors +/.idea +.project +.classpath +.c9/ +*.launch +.settings/ +*.sublime-workspace + +# IDE - VSCode +.vscode/* +!.vscode/settings.json +!.vscode/tasks.json +!.vscode/launch.json +!.vscode/extensions.json \ No newline at end of file diff --git a/backend_new/.prettierrc b/backend_new/.prettierrc new file mode 100644 index 0000000000..dcb72794f5 --- /dev/null +++ b/backend_new/.prettierrc @@ -0,0 +1,4 @@ +{ + "singleQuote": true, + "trailingComma": "all" +} \ No newline at end of file diff --git a/backend_new/README.md b/backend_new/README.md new file mode 100644 index 0000000000..9fe8812ff8 --- /dev/null +++ b/backend_new/README.md @@ -0,0 +1,73 @@ +

+ Nest Logo +

+ +[circleci-image]: https://img.shields.io/circleci/build/github/nestjs/nest/master?token=abc123def456 +[circleci-url]: https://circleci.com/gh/nestjs/nest + +

A progressive Node.js framework for building efficient and scalable server-side applications.

+

+NPM Version +Package License +NPM Downloads +CircleCI +Coverage +Discord +Backers on Open Collective +Sponsors on Open Collective + + Support us + +

+ + +## Description + +[Nest](https://github.com/nestjs/nest) framework TypeScript starter repository. + +## Installation + +```bash +$ npm install +``` + +## Running the app + +```bash +# development +$ npm run start + +# watch mode +$ npm run start:dev + +# production mode +$ npm run start:prod +``` + +## Test + +```bash +# unit tests +$ npm run test + +# e2e tests +$ npm run test:e2e + +# test coverage +$ npm run test:cov +``` + +## Support + +Nest is an MIT-licensed open source project. It can grow thanks to the sponsors and support by the amazing backers. If you'd like to join them, please [read more here](https://docs.nestjs.com/support). + +## Stay in touch + +- Author - [Kamil Myƛliwiec](https://kamilmysliwiec.com) +- Website - [https://nestjs.com](https://nestjs.com/) +- Twitter - [@nestframework](https://twitter.com/nestframework) + +## License + +Nest is [MIT licensed](LICENSE). diff --git a/backend_new/nest-cli.json b/backend_new/nest-cli.json new file mode 100644 index 0000000000..56167b36a1 --- /dev/null +++ b/backend_new/nest-cli.json @@ -0,0 +1,4 @@ +{ + "collection": "@nestjs/schematics", + "sourceRoot": "src" +} diff --git a/backend_new/package.json b/backend_new/package.json new file mode 100644 index 0000000000..eb0c067838 --- /dev/null +++ b/backend_new/package.json @@ -0,0 +1,71 @@ +{ + "name": "backend-new", + "version": "0.0.1", + "description": "", + "author": "", + "private": true, + "license": "UNLICENSED", + "scripts": { + "prebuild": "rimraf dist", + "build": "nest build", + "format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"", + "start": "nest start", + "start:dev": "nest start --watch", + "start:debug": "nest start --debug --watch", + "start:prod": "node dist/main", + "lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix", + "test": "jest", + "test:watch": "jest --watch", + "test:cov": "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" + }, + "dependencies": { + "@nestjs/common": "^8.0.0", + "@nestjs/core": "^8.0.0", + "@nestjs/platform-express": "^8.0.0", + "reflect-metadata": "^0.1.13", + "rimraf": "^3.0.2", + "rxjs": "^7.2.0" + }, + "devDependencies": { + "@nestjs/cli": "^8.0.0", + "@nestjs/schematics": "^8.0.0", + "@nestjs/testing": "^8.0.0", + "@types/express": "^4.17.13", + "@types/jest": "27.4.1", + "@types/node": "^16.0.0", + "@types/supertest": "^2.0.11", + "@typescript-eslint/eslint-plugin": "^5.0.0", + "@typescript-eslint/parser": "^5.0.0", + "eslint": "^8.0.1", + "eslint-config-prettier": "^8.3.0", + "eslint-plugin-prettier": "^4.0.0", + "jest": "^27.2.5", + "prettier": "^2.3.2", + "source-map-support": "^0.5.20", + "supertest": "^6.1.3", + "ts-jest": "^27.0.3", + "ts-loader": "^9.2.3", + "ts-node": "^10.0.0", + "tsconfig-paths": "^3.10.1", + "typescript": "^4.3.5" + }, + "jest": { + "moduleFileExtensions": [ + "js", + "json", + "ts" + ], + "rootDir": "src", + "testRegex": ".*\\.spec\\.ts$", + "transform": { + "^.+\\.(t|j)s$": "ts-jest" + }, + "collectCoverageFrom": [ + "**/*.(t|j)s" + ], + "coverageDirectory": "../coverage", + "testEnvironment": "node" + } +} diff --git a/backend_new/src/app.controller.spec.ts b/backend_new/src/app.controller.spec.ts new file mode 100644 index 0000000000..d22f3890a3 --- /dev/null +++ b/backend_new/src/app.controller.spec.ts @@ -0,0 +1,22 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { AppController } from './app.controller'; +import { AppService } from './app.service'; + +describe('AppController', () => { + let appController: AppController; + + beforeEach(async () => { + const app: TestingModule = await Test.createTestingModule({ + controllers: [AppController], + providers: [AppService], + }).compile(); + + appController = app.get(AppController); + }); + + describe('root', () => { + it('should return "Hello World!"', () => { + expect(appController.getHello()).toBe('Hello World!'); + }); + }); +}); diff --git a/backend_new/src/app.controller.ts b/backend_new/src/app.controller.ts new file mode 100644 index 0000000000..cce879ee62 --- /dev/null +++ b/backend_new/src/app.controller.ts @@ -0,0 +1,12 @@ +import { Controller, Get } from '@nestjs/common'; +import { AppService } from './app.service'; + +@Controller() +export class AppController { + constructor(private readonly appService: AppService) {} + + @Get() + getHello(): string { + return this.appService.getHello(); + } +} diff --git a/backend_new/src/app.module.ts b/backend_new/src/app.module.ts new file mode 100644 index 0000000000..86628031ca --- /dev/null +++ b/backend_new/src/app.module.ts @@ -0,0 +1,10 @@ +import { Module } from '@nestjs/common'; +import { AppController } from './app.controller'; +import { AppService } from './app.service'; + +@Module({ + imports: [], + controllers: [AppController], + providers: [AppService], +}) +export class AppModule {} diff --git a/backend_new/src/app.service.ts b/backend_new/src/app.service.ts new file mode 100644 index 0000000000..927d7cca0b --- /dev/null +++ b/backend_new/src/app.service.ts @@ -0,0 +1,8 @@ +import { Injectable } from '@nestjs/common'; + +@Injectable() +export class AppService { + getHello(): string { + return 'Hello World!'; + } +} diff --git a/backend_new/src/main.ts b/backend_new/src/main.ts new file mode 100644 index 0000000000..13cad38cff --- /dev/null +++ b/backend_new/src/main.ts @@ -0,0 +1,8 @@ +import { NestFactory } from '@nestjs/core'; +import { AppModule } from './app.module'; + +async function bootstrap() { + const app = await NestFactory.create(AppModule); + await app.listen(3000); +} +bootstrap(); diff --git a/backend_new/test/app.e2e-spec.ts b/backend_new/test/app.e2e-spec.ts new file mode 100644 index 0000000000..50cda62332 --- /dev/null +++ b/backend_new/test/app.e2e-spec.ts @@ -0,0 +1,24 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { INestApplication } from '@nestjs/common'; +import * as request from 'supertest'; +import { AppModule } from './../src/app.module'; + +describe('AppController (e2e)', () => { + let app: INestApplication; + + beforeEach(async () => { + const moduleFixture: TestingModule = await Test.createTestingModule({ + imports: [AppModule], + }).compile(); + + app = moduleFixture.createNestApplication(); + await app.init(); + }); + + it('/ (GET)', () => { + return request(app.getHttpServer()) + .get('/') + .expect(200) + .expect('Hello World!'); + }); +}); diff --git a/backend_new/test/jest-e2e.json b/backend_new/test/jest-e2e.json new file mode 100644 index 0000000000..e9d912f3e3 --- /dev/null +++ b/backend_new/test/jest-e2e.json @@ -0,0 +1,9 @@ +{ + "moduleFileExtensions": ["js", "json", "ts"], + "rootDir": ".", + "testEnvironment": "node", + "testRegex": ".e2e-spec.ts$", + "transform": { + "^.+\\.(t|j)s$": "ts-jest" + } +} diff --git a/backend_new/tsconfig.build.json b/backend_new/tsconfig.build.json new file mode 100644 index 0000000000..64f86c6bd2 --- /dev/null +++ b/backend_new/tsconfig.build.json @@ -0,0 +1,4 @@ +{ + "extends": "./tsconfig.json", + "exclude": ["node_modules", "test", "dist", "**/*spec.ts"] +} diff --git a/backend_new/tsconfig.json b/backend_new/tsconfig.json new file mode 100644 index 0000000000..adb614cab7 --- /dev/null +++ b/backend_new/tsconfig.json @@ -0,0 +1,21 @@ +{ + "compilerOptions": { + "module": "commonjs", + "declaration": true, + "removeComments": true, + "emitDecoratorMetadata": true, + "experimentalDecorators": true, + "allowSyntheticDefaultImports": true, + "target": "es2017", + "sourceMap": true, + "outDir": "./dist", + "baseUrl": "./", + "incremental": true, + "skipLibCheck": true, + "strictNullChecks": false, + "noImplicitAny": false, + "strictBindCallApply": false, + "forceConsistentCasingInFileNames": false, + "noFallthroughCasesInSwitch": false + } +} diff --git a/backend_new/yarn.lock b/backend_new/yarn.lock new file mode 100644 index 0000000000..bf7fa6a7e2 --- /dev/null +++ b/backend_new/yarn.lock @@ -0,0 +1,5109 @@ +# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. +# yarn lockfile v1 + + +"@ampproject/remapping@^2.2.0": + version "2.2.1" + resolved "https://registry.yarnpkg.com/@ampproject/remapping/-/remapping-2.2.1.tgz#99e8e11851128b8702cd57c33684f1d0f260b630" + integrity sha512-lFMjJTrFL3j7L9yBxwYfCq2k6qqwHyzuUl/XBnif78PWTJYyL/dfowQHWE3sp6U6ZzqWiiIZnpTMO96zhkjwtg== + dependencies: + "@jridgewell/gen-mapping" "^0.3.0" + "@jridgewell/trace-mapping" "^0.3.9" + +"@angular-devkit/core@13.3.5": + version "13.3.5" + resolved "https://registry.yarnpkg.com/@angular-devkit/core/-/core-13.3.5.tgz#c5f32f4f99b5cad8df9cf3cf4da9c4b1335c1155" + integrity sha512-w7vzK4VoYP9rLgxJ2SwEfrkpKybdD+QgQZlsDBzT0C6Ebp7b4gkNcNVFo8EiZvfDl6Yplw2IAP7g7fs3STn0hQ== + dependencies: + ajv "8.9.0" + ajv-formats "2.1.1" + fast-json-stable-stringify "2.1.0" + magic-string "0.25.7" + rxjs "6.6.7" + source-map "0.7.3" + +"@angular-devkit/core@13.3.6": + version "13.3.6" + resolved "https://registry.yarnpkg.com/@angular-devkit/core/-/core-13.3.6.tgz#656284327d6f84a866a8d3cc8625895fe740602d" + integrity sha512-ZmD586B+RnM2CG5+jbXh2NVfIydTc/yKSjppYDDOv4I530YBm6vpfZMwClpiNk6XLbMv7KqX4Tlr4wfxlPYYbA== + dependencies: + ajv "8.9.0" + ajv-formats "2.1.1" + fast-json-stable-stringify "2.1.0" + magic-string "0.25.7" + rxjs "6.6.7" + source-map "0.7.3" + +"@angular-devkit/schematics-cli@13.3.6": + version "13.3.6" + resolved "https://registry.yarnpkg.com/@angular-devkit/schematics-cli/-/schematics-cli-13.3.6.tgz#5246c112b6b837a9d0a348cb6b79a8c4948e90c8" + integrity sha512-5tTuu9gbXM0bMk0sin4phmWA3U1Qz53zT/rpEfzQ/+c/s8CoqZ5N1qOnYtemRct3Jxsz1kn4TBpHeriR4r5hHg== + dependencies: + "@angular-devkit/core" "13.3.6" + "@angular-devkit/schematics" "13.3.6" + ansi-colors "4.1.1" + inquirer "8.2.0" + minimist "1.2.6" + symbol-observable "4.0.0" + +"@angular-devkit/schematics@13.3.5": + version "13.3.5" + resolved "https://registry.yarnpkg.com/@angular-devkit/schematics/-/schematics-13.3.5.tgz#9cb03ac99ee14173a6fa00fd7ca94fa42600c163" + integrity sha512-0N/kL/Vfx0yVAEwa3HYxNx9wYb+G9r1JrLjJQQzDp+z9LtcojNf7j3oey6NXrDUs1WjVZOa/AIdRl3/DuaoG5w== + dependencies: + "@angular-devkit/core" "13.3.5" + jsonc-parser "3.0.0" + magic-string "0.25.7" + ora "5.4.1" + rxjs "6.6.7" + +"@angular-devkit/schematics@13.3.6": + version "13.3.6" + resolved "https://registry.yarnpkg.com/@angular-devkit/schematics/-/schematics-13.3.6.tgz#b02e1eff714c2cf44a54de92410d07cc8cefbb0e" + integrity sha512-yLh5xc92C/FiaAp27coPiKWpSUmwoXF7vMxbJYJTyOXlt0mUITAEAwtrZQNr4yAxW/yvgTdyg7PhXaveQNTUuQ== + dependencies: + "@angular-devkit/core" "13.3.6" + jsonc-parser "3.0.0" + magic-string "0.25.7" + ora "5.4.1" + rxjs "6.6.7" + +"@babel/code-frame@^7.0.0", "@babel/code-frame@^7.12.13", "@babel/code-frame@^7.16.7", "@babel/code-frame@^7.18.6", "@babel/code-frame@^7.21.4": + version "7.21.4" + resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.21.4.tgz#d0fa9e4413aca81f2b23b9442797bda1826edb39" + integrity sha512-LYvhNKfwWSPpocw8GI7gpK2nq3HSDuEPC/uSYaALSJu9xjsalaaYFOq0Pwt5KmVqwEbZlDu81aLXwBOmD/Fv9g== + dependencies: + "@babel/highlight" "^7.18.6" + +"@babel/compat-data@^7.21.5": + version "7.21.7" + resolved "https://registry.yarnpkg.com/@babel/compat-data/-/compat-data-7.21.7.tgz#61caffb60776e49a57ba61a88f02bedd8714f6bc" + integrity sha512-KYMqFYTaenzMK4yUtf4EW9wc4N9ef80FsbMtkwool5zpwl4YrT1SdWYSTRcT94KO4hannogdS+LxY7L+arP3gA== + +"@babel/core@^7.1.0", "@babel/core@^7.12.3", "@babel/core@^7.7.2", "@babel/core@^7.8.0": + version "7.21.8" + resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.21.8.tgz#2a8c7f0f53d60100ba4c32470ba0281c92aa9aa4" + integrity sha512-YeM22Sondbo523Sz0+CirSPnbj9bG3P0CdHcBZdqUuaeOaYEFbOLoGU7lebvGP6P5J/WE9wOn7u7C4J9HvS1xQ== + dependencies: + "@ampproject/remapping" "^2.2.0" + "@babel/code-frame" "^7.21.4" + "@babel/generator" "^7.21.5" + "@babel/helper-compilation-targets" "^7.21.5" + "@babel/helper-module-transforms" "^7.21.5" + "@babel/helpers" "^7.21.5" + "@babel/parser" "^7.21.8" + "@babel/template" "^7.20.7" + "@babel/traverse" "^7.21.5" + "@babel/types" "^7.21.5" + convert-source-map "^1.7.0" + debug "^4.1.0" + gensync "^1.0.0-beta.2" + json5 "^2.2.2" + semver "^6.3.0" + +"@babel/generator@^7.21.5", "@babel/generator@^7.7.2": + version "7.21.5" + resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.21.5.tgz#c0c0e5449504c7b7de8236d99338c3e2a340745f" + integrity sha512-SrKK/sRv8GesIW1bDagf9cCG38IOMYZusoe1dfg0D8aiUe3Amvoj1QtjTPAWcfrZFvIwlleLb0gxzQidL9w14w== + dependencies: + "@babel/types" "^7.21.5" + "@jridgewell/gen-mapping" "^0.3.2" + "@jridgewell/trace-mapping" "^0.3.17" + jsesc "^2.5.1" + +"@babel/helper-compilation-targets@^7.21.5": + version "7.21.5" + resolved "https://registry.yarnpkg.com/@babel/helper-compilation-targets/-/helper-compilation-targets-7.21.5.tgz#631e6cc784c7b660417421349aac304c94115366" + integrity sha512-1RkbFGUKex4lvsB9yhIfWltJM5cZKUftB2eNajaDv3dCMEp49iBG0K14uH8NnX9IPux2+mK7JGEOB0jn48/J6w== + dependencies: + "@babel/compat-data" "^7.21.5" + "@babel/helper-validator-option" "^7.21.0" + browserslist "^4.21.3" + lru-cache "^5.1.1" + semver "^6.3.0" + +"@babel/helper-environment-visitor@^7.21.5": + version "7.21.5" + resolved "https://registry.yarnpkg.com/@babel/helper-environment-visitor/-/helper-environment-visitor-7.21.5.tgz#c769afefd41d171836f7cb63e295bedf689d48ba" + integrity sha512-IYl4gZ3ETsWocUWgsFZLM5i1BYx9SoemminVEXadgLBa9TdeorzgLKm8wWLA6J1N/kT3Kch8XIk1laNzYoHKvQ== + +"@babel/helper-function-name@^7.21.0": + version "7.21.0" + resolved "https://registry.yarnpkg.com/@babel/helper-function-name/-/helper-function-name-7.21.0.tgz#d552829b10ea9f120969304023cd0645fa00b1b4" + integrity sha512-HfK1aMRanKHpxemaY2gqBmL04iAPOPRj7DxtNbiDOrJK+gdwkiNRVpCpUJYbUT+aZyemKN8brqTOxzCaG6ExRg== + dependencies: + "@babel/template" "^7.20.7" + "@babel/types" "^7.21.0" + +"@babel/helper-hoist-variables@^7.18.6": + version "7.18.6" + resolved "https://registry.yarnpkg.com/@babel/helper-hoist-variables/-/helper-hoist-variables-7.18.6.tgz#d4d2c8fb4baeaa5c68b99cc8245c56554f926678" + integrity sha512-UlJQPkFqFULIcyW5sbzgbkxn2FKRgwWiRexcuaR8RNJRy8+LLveqPjwZV/bwrLZCN0eUHD/x8D0heK1ozuoo6Q== + dependencies: + "@babel/types" "^7.18.6" + +"@babel/helper-module-imports@^7.21.4": + version "7.21.4" + resolved "https://registry.yarnpkg.com/@babel/helper-module-imports/-/helper-module-imports-7.21.4.tgz#ac88b2f76093637489e718a90cec6cf8a9b029af" + integrity sha512-orajc5T2PsRYUN3ZryCEFeMDYwyw09c/pZeaQEZPH0MpKzSvn3e0uXsDBu3k03VI+9DBiRo+l22BfKTpKwa/Wg== + dependencies: + "@babel/types" "^7.21.4" + +"@babel/helper-module-transforms@^7.21.5": + version "7.21.5" + resolved "https://registry.yarnpkg.com/@babel/helper-module-transforms/-/helper-module-transforms-7.21.5.tgz#d937c82e9af68d31ab49039136a222b17ac0b420" + integrity sha512-bI2Z9zBGY2q5yMHoBvJ2a9iX3ZOAzJPm7Q8Yz6YeoUjU/Cvhmi2G4QyTNyPBqqXSgTjUxRg3L0xV45HvkNWWBw== + dependencies: + "@babel/helper-environment-visitor" "^7.21.5" + "@babel/helper-module-imports" "^7.21.4" + "@babel/helper-simple-access" "^7.21.5" + "@babel/helper-split-export-declaration" "^7.18.6" + "@babel/helper-validator-identifier" "^7.19.1" + "@babel/template" "^7.20.7" + "@babel/traverse" "^7.21.5" + "@babel/types" "^7.21.5" + +"@babel/helper-plugin-utils@^7.0.0", "@babel/helper-plugin-utils@^7.10.4", "@babel/helper-plugin-utils@^7.12.13", "@babel/helper-plugin-utils@^7.14.5", "@babel/helper-plugin-utils@^7.20.2", "@babel/helper-plugin-utils@^7.8.0": + version "7.21.5" + resolved "https://registry.yarnpkg.com/@babel/helper-plugin-utils/-/helper-plugin-utils-7.21.5.tgz#345f2377d05a720a4e5ecfa39cbf4474a4daed56" + integrity sha512-0WDaIlXKOX/3KfBK/dwP1oQGiPh6rjMkT7HIRv7i5RR2VUMwrx5ZL0dwBkKx7+SW1zwNdgjHd34IMk5ZjTeHVg== + +"@babel/helper-simple-access@^7.21.5": + version "7.21.5" + resolved "https://registry.yarnpkg.com/@babel/helper-simple-access/-/helper-simple-access-7.21.5.tgz#d697a7971a5c39eac32c7e63c0921c06c8a249ee" + integrity sha512-ENPDAMC1wAjR0uaCUwliBdiSl1KBJAVnMTzXqi64c2MG8MPR6ii4qf7bSXDqSFbr4W6W028/rf5ivoHop5/mkg== + dependencies: + "@babel/types" "^7.21.5" + +"@babel/helper-split-export-declaration@^7.18.6": + version "7.18.6" + resolved "https://registry.yarnpkg.com/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.18.6.tgz#7367949bc75b20c6d5a5d4a97bba2824ae8ef075" + integrity sha512-bde1etTx6ZyTmobl9LLMMQsaizFVZrquTEHOqKeQESMKo4PlObf+8+JA25ZsIpZhT/WEd39+vOdLXAFG/nELpA== + dependencies: + "@babel/types" "^7.18.6" + +"@babel/helper-string-parser@^7.21.5": + version "7.21.5" + resolved "https://registry.yarnpkg.com/@babel/helper-string-parser/-/helper-string-parser-7.21.5.tgz#2b3eea65443c6bdc31c22d037c65f6d323b6b2bd" + integrity sha512-5pTUx3hAJaZIdW99sJ6ZUUgWq/Y+Hja7TowEnLNMm1VivRgZQL3vpBY3qUACVsvw+yQU6+YgfBVmcbLaZtrA1w== + +"@babel/helper-validator-identifier@^7.18.6", "@babel/helper-validator-identifier@^7.19.1": + version "7.19.1" + resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.19.1.tgz#7eea834cf32901ffdc1a7ee555e2f9c27e249ca2" + integrity sha512-awrNfaMtnHUr653GgGEs++LlAvW6w+DcPrOliSMXWCKo597CwL5Acf/wWdNkf/tfEQE3mjkeD1YOVZOUV/od1w== + +"@babel/helper-validator-option@^7.21.0": + version "7.21.0" + resolved "https://registry.yarnpkg.com/@babel/helper-validator-option/-/helper-validator-option-7.21.0.tgz#8224c7e13ace4bafdc4004da2cf064ef42673180" + integrity sha512-rmL/B8/f0mKS2baE9ZpyTcTavvEuWhTTW8amjzXNvYG4AwBsqTLikfXsEofsJEfKHf+HQVQbFOHy6o+4cnC/fQ== + +"@babel/helpers@^7.21.5": + version "7.21.5" + resolved "https://registry.yarnpkg.com/@babel/helpers/-/helpers-7.21.5.tgz#5bac66e084d7a4d2d9696bdf0175a93f7fb63c08" + integrity sha512-BSY+JSlHxOmGsPTydUkPf1MdMQ3M81x5xGCOVgWM3G8XH77sJ292Y2oqcp0CbbgxhqBuI46iUz1tT7hqP7EfgA== + dependencies: + "@babel/template" "^7.20.7" + "@babel/traverse" "^7.21.5" + "@babel/types" "^7.21.5" + +"@babel/highlight@^7.18.6": + version "7.18.6" + resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.18.6.tgz#81158601e93e2563795adcbfbdf5d64be3f2ecdf" + integrity sha512-u7stbOuYjaPezCuLj29hNW1v64M2Md2qupEKP1fHc7WdOA3DgLh37suiSrZYY7haUB7iBeQZ9P1uiRF359do3g== + dependencies: + "@babel/helper-validator-identifier" "^7.18.6" + chalk "^2.0.0" + js-tokens "^4.0.0" + +"@babel/parser@^7.1.0", "@babel/parser@^7.14.7", "@babel/parser@^7.20.7", "@babel/parser@^7.21.5", "@babel/parser@^7.21.8": + version "7.21.8" + resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.21.8.tgz#642af7d0333eab9c0ad70b14ac5e76dbde7bfdf8" + integrity sha512-6zavDGdzG3gUqAdWvlLFfk+36RilI+Pwyuuh7HItyeScCWP3k6i8vKclAQ0bM/0y/Kz/xiwvxhMv9MgTJP5gmA== + +"@babel/plugin-syntax-async-generators@^7.8.4": + version "7.8.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-async-generators/-/plugin-syntax-async-generators-7.8.4.tgz#a983fb1aeb2ec3f6ed042a210f640e90e786fe0d" + integrity sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw== + dependencies: + "@babel/helper-plugin-utils" "^7.8.0" + +"@babel/plugin-syntax-bigint@^7.8.3": + version "7.8.3" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-bigint/-/plugin-syntax-bigint-7.8.3.tgz#4c9a6f669f5d0cdf1b90a1671e9a146be5300cea" + integrity sha512-wnTnFlG+YxQm3vDxpGE57Pj0srRU4sHE/mDkt1qv2YJJSeUAec2ma4WLUnUPeKjyrfntVwe/N6dCXpU+zL3Npg== + dependencies: + "@babel/helper-plugin-utils" "^7.8.0" + +"@babel/plugin-syntax-class-properties@^7.8.3": + version "7.12.13" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-class-properties/-/plugin-syntax-class-properties-7.12.13.tgz#b5c987274c4a3a82b89714796931a6b53544ae10" + integrity sha512-fm4idjKla0YahUNgFNLCB0qySdsoPiZP3iQE3rky0mBUtMZ23yDJ9SJdg6dXTSDnulOVqiF3Hgr9nbXvXTQZYA== + dependencies: + "@babel/helper-plugin-utils" "^7.12.13" + +"@babel/plugin-syntax-import-meta@^7.8.3": + version "7.10.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-import-meta/-/plugin-syntax-import-meta-7.10.4.tgz#ee601348c370fa334d2207be158777496521fd51" + integrity sha512-Yqfm+XDx0+Prh3VSeEQCPU81yC+JWZ2pDPFSS4ZdpfZhp4MkFMaDC1UqseovEKwSUpnIL7+vK+Clp7bfh0iD7g== + dependencies: + "@babel/helper-plugin-utils" "^7.10.4" + +"@babel/plugin-syntax-json-strings@^7.8.3": + version "7.8.3" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-json-strings/-/plugin-syntax-json-strings-7.8.3.tgz#01ca21b668cd8218c9e640cb6dd88c5412b2c96a" + integrity sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA== + dependencies: + "@babel/helper-plugin-utils" "^7.8.0" + +"@babel/plugin-syntax-logical-assignment-operators@^7.8.3": + version "7.10.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-logical-assignment-operators/-/plugin-syntax-logical-assignment-operators-7.10.4.tgz#ca91ef46303530448b906652bac2e9fe9941f699" + integrity sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig== + dependencies: + "@babel/helper-plugin-utils" "^7.10.4" + +"@babel/plugin-syntax-nullish-coalescing-operator@^7.8.3": + version "7.8.3" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-nullish-coalescing-operator/-/plugin-syntax-nullish-coalescing-operator-7.8.3.tgz#167ed70368886081f74b5c36c65a88c03b66d1a9" + integrity sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ== + dependencies: + "@babel/helper-plugin-utils" "^7.8.0" + +"@babel/plugin-syntax-numeric-separator@^7.8.3": + version "7.10.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-numeric-separator/-/plugin-syntax-numeric-separator-7.10.4.tgz#b9b070b3e33570cd9fd07ba7fa91c0dd37b9af97" + integrity sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug== + dependencies: + "@babel/helper-plugin-utils" "^7.10.4" + +"@babel/plugin-syntax-object-rest-spread@^7.8.3": + version "7.8.3" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-object-rest-spread/-/plugin-syntax-object-rest-spread-7.8.3.tgz#60e225edcbd98a640332a2e72dd3e66f1af55871" + integrity sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA== + dependencies: + "@babel/helper-plugin-utils" "^7.8.0" + +"@babel/plugin-syntax-optional-catch-binding@^7.8.3": + version "7.8.3" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-optional-catch-binding/-/plugin-syntax-optional-catch-binding-7.8.3.tgz#6111a265bcfb020eb9efd0fdfd7d26402b9ed6c1" + integrity sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q== + dependencies: + "@babel/helper-plugin-utils" "^7.8.0" + +"@babel/plugin-syntax-optional-chaining@^7.8.3": + version "7.8.3" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-optional-chaining/-/plugin-syntax-optional-chaining-7.8.3.tgz#4f69c2ab95167e0180cd5336613f8c5788f7d48a" + integrity sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg== + dependencies: + "@babel/helper-plugin-utils" "^7.8.0" + +"@babel/plugin-syntax-top-level-await@^7.8.3": + version "7.14.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-top-level-await/-/plugin-syntax-top-level-await-7.14.5.tgz#c1cfdadc35a646240001f06138247b741c34d94c" + integrity sha512-hx++upLv5U1rgYfwe1xBQUhRmU41NEvpUvrp8jkrSCdvGSnM5/qdRMtylJ6PG5OFkBaHkbTAKTnd3/YyESRHFw== + dependencies: + "@babel/helper-plugin-utils" "^7.14.5" + +"@babel/plugin-syntax-typescript@^7.7.2": + version "7.21.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.21.4.tgz#2751948e9b7c6d771a8efa59340c15d4a2891ff8" + integrity sha512-xz0D39NvhQn4t4RNsHmDnnsaQizIlUkdtYvLs8La1BlfjQ6JEwxkJGeqJMW2tAXx+q6H+WFuUTXNdYVpEya0YA== + dependencies: + "@babel/helper-plugin-utils" "^7.20.2" + +"@babel/template@^7.20.7", "@babel/template@^7.3.3": + version "7.20.7" + resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.20.7.tgz#a15090c2839a83b02aa996c0b4994005841fd5a8" + integrity sha512-8SegXApWe6VoNw0r9JHpSteLKTpTiLZ4rMlGIm9JQ18KiCtyQiAMEazujAHrUS5flrcqYZa75ukev3P6QmUwUw== + dependencies: + "@babel/code-frame" "^7.18.6" + "@babel/parser" "^7.20.7" + "@babel/types" "^7.20.7" + +"@babel/traverse@^7.21.5", "@babel/traverse@^7.7.2": + version "7.21.5" + resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.21.5.tgz#ad22361d352a5154b498299d523cf72998a4b133" + integrity sha512-AhQoI3YjWi6u/y/ntv7k48mcrCXmus0t79J9qPNlk/lAsFlCiJ047RmbfMOawySTHtywXhbXgpx/8nXMYd+oFw== + dependencies: + "@babel/code-frame" "^7.21.4" + "@babel/generator" "^7.21.5" + "@babel/helper-environment-visitor" "^7.21.5" + "@babel/helper-function-name" "^7.21.0" + "@babel/helper-hoist-variables" "^7.18.6" + "@babel/helper-split-export-declaration" "^7.18.6" + "@babel/parser" "^7.21.5" + "@babel/types" "^7.21.5" + debug "^4.1.0" + globals "^11.1.0" + +"@babel/types@^7.0.0", "@babel/types@^7.18.6", "@babel/types@^7.20.7", "@babel/types@^7.21.0", "@babel/types@^7.21.4", "@babel/types@^7.21.5", "@babel/types@^7.3.0", "@babel/types@^7.3.3": + version "7.21.5" + resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.21.5.tgz#18dfbd47c39d3904d5db3d3dc2cc80bedb60e5b6" + integrity sha512-m4AfNvVF2mVC/F7fDEdH2El3HzUg9It/XsCxZiOTTA3m3qYfcSVSbTfM6Q9xG+hYDniZssYhlXKKUMD5m8tF4Q== + dependencies: + "@babel/helper-string-parser" "^7.21.5" + "@babel/helper-validator-identifier" "^7.19.1" + to-fast-properties "^2.0.0" + +"@bcoe/v8-coverage@^0.2.3": + version "0.2.3" + resolved "https://registry.yarnpkg.com/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz#75a2e8b51cb758a7553d6804a5932d7aace75c39" + integrity sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw== + +"@colors/colors@1.5.0": + version "1.5.0" + resolved "https://registry.yarnpkg.com/@colors/colors/-/colors-1.5.0.tgz#bb504579c1cae923e6576a4f5da43d25f97bdbd9" + integrity sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ== + +"@cspotcode/source-map-support@^0.8.0": + version "0.8.1" + resolved "https://registry.yarnpkg.com/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz#00629c35a688e05a88b1cda684fb9d5e73f000a1" + integrity sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw== + dependencies: + "@jridgewell/trace-mapping" "0.3.9" + +"@eslint-community/eslint-utils@^4.2.0": + version "4.4.0" + resolved "https://registry.yarnpkg.com/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz#a23514e8fb9af1269d5f7788aa556798d61c6b59" + integrity sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA== + dependencies: + eslint-visitor-keys "^3.3.0" + +"@eslint-community/regexpp@^4.4.0": + version "4.5.1" + resolved "https://registry.yarnpkg.com/@eslint-community/regexpp/-/regexpp-4.5.1.tgz#cdd35dce4fa1a89a4fd42b1599eb35b3af408884" + integrity sha512-Z5ba73P98O1KUYCCJTUeVpja9RcGoMdncZ6T49FCUl2lN38JtCJ+3WgIDBv0AuY4WChU5PmtJmOCTlN6FZTFKQ== + +"@eslint/eslintrc@^2.0.3": + version "2.0.3" + resolved "https://registry.yarnpkg.com/@eslint/eslintrc/-/eslintrc-2.0.3.tgz#4910db5505f4d503f27774bf356e3704818a0331" + integrity sha512-+5gy6OQfk+xx3q0d6jGZZC3f3KzAkXc/IanVxd1is/VIIziRqqt3ongQz0FiTUXqTk0c7aDB3OaFuKnuSoJicQ== + dependencies: + ajv "^6.12.4" + debug "^4.3.2" + espree "^9.5.2" + globals "^13.19.0" + ignore "^5.2.0" + import-fresh "^3.2.1" + js-yaml "^4.1.0" + minimatch "^3.1.2" + strip-json-comments "^3.1.1" + +"@eslint/js@8.40.0": + version "8.40.0" + resolved "https://registry.yarnpkg.com/@eslint/js/-/js-8.40.0.tgz#3ba73359e11f5a7bd3e407f70b3528abfae69cec" + integrity sha512-ElyB54bJIhXQYVKjDSvCkPO1iU1tSAeVQJbllWJq1XQSmmA4dgFk8CbiBGpiOPxleE48vDogxCtmMYku4HSVLA== + +"@humanwhocodes/config-array@^0.11.8": + version "0.11.8" + resolved "https://registry.yarnpkg.com/@humanwhocodes/config-array/-/config-array-0.11.8.tgz#03595ac2075a4dc0f191cc2131de14fbd7d410b9" + integrity sha512-UybHIJzJnR5Qc/MsD9Kr+RpO2h+/P1GhOwdiLPXK5TWk5sgTdu88bTD9UP+CKbPPh5Rni1u0GjAdYQLemG8g+g== + dependencies: + "@humanwhocodes/object-schema" "^1.2.1" + debug "^4.1.1" + minimatch "^3.0.5" + +"@humanwhocodes/module-importer@^1.0.1": + version "1.0.1" + resolved "https://registry.yarnpkg.com/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz#af5b2691a22b44be847b0ca81641c5fb6ad0172c" + integrity sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA== + +"@humanwhocodes/object-schema@^1.2.1": + version "1.2.1" + resolved "https://registry.yarnpkg.com/@humanwhocodes/object-schema/-/object-schema-1.2.1.tgz#b520529ec21d8e5945a1851dfd1c32e94e39ff45" + integrity sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA== + +"@istanbuljs/load-nyc-config@^1.0.0": + version "1.1.0" + resolved "https://registry.yarnpkg.com/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz#fd3db1d59ecf7cf121e80650bb86712f9b55eced" + integrity sha512-VjeHSlIzpv/NyD3N0YuHfXOPDIixcA1q2ZV98wsMqcYlPmv2n3Yb2lYP9XMElnaFVXg5A7YLTeLu6V84uQDjmQ== + dependencies: + camelcase "^5.3.1" + find-up "^4.1.0" + get-package-type "^0.1.0" + js-yaml "^3.13.1" + resolve-from "^5.0.0" + +"@istanbuljs/schema@^0.1.2": + version "0.1.3" + resolved "https://registry.yarnpkg.com/@istanbuljs/schema/-/schema-0.1.3.tgz#e45e384e4b8ec16bce2fd903af78450f6bf7ec98" + integrity sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA== + +"@jest/console@^27.5.1": + version "27.5.1" + resolved "https://registry.yarnpkg.com/@jest/console/-/console-27.5.1.tgz#260fe7239602fe5130a94f1aa386eff54b014bba" + integrity sha512-kZ/tNpS3NXn0mlXXXPNuDZnb4c0oZ20r4K5eemM2k30ZC3G0T02nXUvyhf5YdbXWHPEJLc9qGLxEZ216MdL+Zg== + dependencies: + "@jest/types" "^27.5.1" + "@types/node" "*" + chalk "^4.0.0" + jest-message-util "^27.5.1" + jest-util "^27.5.1" + slash "^3.0.0" + +"@jest/core@^27.5.1": + version "27.5.1" + resolved "https://registry.yarnpkg.com/@jest/core/-/core-27.5.1.tgz#267ac5f704e09dc52de2922cbf3af9edcd64b626" + integrity sha512-AK6/UTrvQD0Cd24NSqmIA6rKsu0tKIxfiCducZvqxYdmMisOYAsdItspT+fQDQYARPf8XgjAFZi0ogW2agH5nQ== + dependencies: + "@jest/console" "^27.5.1" + "@jest/reporters" "^27.5.1" + "@jest/test-result" "^27.5.1" + "@jest/transform" "^27.5.1" + "@jest/types" "^27.5.1" + "@types/node" "*" + ansi-escapes "^4.2.1" + chalk "^4.0.0" + emittery "^0.8.1" + exit "^0.1.2" + graceful-fs "^4.2.9" + jest-changed-files "^27.5.1" + jest-config "^27.5.1" + jest-haste-map "^27.5.1" + jest-message-util "^27.5.1" + jest-regex-util "^27.5.1" + jest-resolve "^27.5.1" + jest-resolve-dependencies "^27.5.1" + jest-runner "^27.5.1" + jest-runtime "^27.5.1" + jest-snapshot "^27.5.1" + jest-util "^27.5.1" + jest-validate "^27.5.1" + jest-watcher "^27.5.1" + micromatch "^4.0.4" + rimraf "^3.0.0" + slash "^3.0.0" + strip-ansi "^6.0.0" + +"@jest/environment@^27.5.1": + version "27.5.1" + resolved "https://registry.yarnpkg.com/@jest/environment/-/environment-27.5.1.tgz#d7425820511fe7158abbecc010140c3fd3be9c74" + integrity sha512-/WQjhPJe3/ghaol/4Bq480JKXV/Rfw8nQdN7f41fM8VDHLcxKXou6QyXAh3EFr9/bVG3x74z1NWDkP87EiY8gA== + dependencies: + "@jest/fake-timers" "^27.5.1" + "@jest/types" "^27.5.1" + "@types/node" "*" + jest-mock "^27.5.1" + +"@jest/fake-timers@^27.5.1": + version "27.5.1" + resolved "https://registry.yarnpkg.com/@jest/fake-timers/-/fake-timers-27.5.1.tgz#76979745ce0579c8a94a4678af7a748eda8ada74" + integrity sha512-/aPowoolwa07k7/oM3aASneNeBGCmGQsc3ugN4u6s4C/+s5M64MFo/+djTdiwcbQlRfFElGuDXWzaWj6QgKObQ== + dependencies: + "@jest/types" "^27.5.1" + "@sinonjs/fake-timers" "^8.0.1" + "@types/node" "*" + jest-message-util "^27.5.1" + jest-mock "^27.5.1" + jest-util "^27.5.1" + +"@jest/globals@^27.5.1": + version "27.5.1" + resolved "https://registry.yarnpkg.com/@jest/globals/-/globals-27.5.1.tgz#7ac06ce57ab966566c7963431cef458434601b2b" + integrity sha512-ZEJNB41OBQQgGzgyInAv0UUfDDj3upmHydjieSxFvTRuZElrx7tXg/uVQ5hYVEwiXs3+aMsAeEc9X7xiSKCm4Q== + dependencies: + "@jest/environment" "^27.5.1" + "@jest/types" "^27.5.1" + expect "^27.5.1" + +"@jest/reporters@^27.5.1": + version "27.5.1" + resolved "https://registry.yarnpkg.com/@jest/reporters/-/reporters-27.5.1.tgz#ceda7be96170b03c923c37987b64015812ffec04" + integrity sha512-cPXh9hWIlVJMQkVk84aIvXuBB4uQQmFqZiacloFuGiP3ah1sbCxCosidXFDfqG8+6fO1oR2dTJTlsOy4VFmUfw== + dependencies: + "@bcoe/v8-coverage" "^0.2.3" + "@jest/console" "^27.5.1" + "@jest/test-result" "^27.5.1" + "@jest/transform" "^27.5.1" + "@jest/types" "^27.5.1" + "@types/node" "*" + chalk "^4.0.0" + collect-v8-coverage "^1.0.0" + exit "^0.1.2" + glob "^7.1.2" + graceful-fs "^4.2.9" + istanbul-lib-coverage "^3.0.0" + istanbul-lib-instrument "^5.1.0" + istanbul-lib-report "^3.0.0" + istanbul-lib-source-maps "^4.0.0" + istanbul-reports "^3.1.3" + jest-haste-map "^27.5.1" + jest-resolve "^27.5.1" + jest-util "^27.5.1" + jest-worker "^27.5.1" + slash "^3.0.0" + source-map "^0.6.0" + string-length "^4.0.1" + terminal-link "^2.0.0" + v8-to-istanbul "^8.1.0" + +"@jest/source-map@^27.5.1": + version "27.5.1" + resolved "https://registry.yarnpkg.com/@jest/source-map/-/source-map-27.5.1.tgz#6608391e465add4205eae073b55e7f279e04e8cf" + integrity sha512-y9NIHUYF3PJRlHk98NdC/N1gl88BL08aQQgu4k4ZopQkCw9t9cV8mtl3TV8b/YCB8XaVTFrmUTAJvjsntDireg== + dependencies: + callsites "^3.0.0" + graceful-fs "^4.2.9" + source-map "^0.6.0" + +"@jest/test-result@^27.5.1": + version "27.5.1" + resolved "https://registry.yarnpkg.com/@jest/test-result/-/test-result-27.5.1.tgz#56a6585fa80f7cdab72b8c5fc2e871d03832f5bb" + integrity sha512-EW35l2RYFUcUQxFJz5Cv5MTOxlJIQs4I7gxzi2zVU7PJhOwfYq1MdC5nhSmYjX1gmMmLPvB3sIaC+BkcHRBfag== + dependencies: + "@jest/console" "^27.5.1" + "@jest/types" "^27.5.1" + "@types/istanbul-lib-coverage" "^2.0.0" + collect-v8-coverage "^1.0.0" + +"@jest/test-sequencer@^27.5.1": + version "27.5.1" + resolved "https://registry.yarnpkg.com/@jest/test-sequencer/-/test-sequencer-27.5.1.tgz#4057e0e9cea4439e544c6353c6affe58d095745b" + integrity sha512-LCheJF7WB2+9JuCS7VB/EmGIdQuhtqjRNI9A43idHv3E4KltCTsPsLxvdaubFHSYwY/fNjMWjl6vNRhDiN7vpQ== + dependencies: + "@jest/test-result" "^27.5.1" + graceful-fs "^4.2.9" + jest-haste-map "^27.5.1" + jest-runtime "^27.5.1" + +"@jest/transform@^27.5.1": + version "27.5.1" + resolved "https://registry.yarnpkg.com/@jest/transform/-/transform-27.5.1.tgz#6c3501dcc00c4c08915f292a600ece5ecfe1f409" + integrity sha512-ipON6WtYgl/1329g5AIJVbUuEh0wZVbdpGwC99Jw4LwuoBNS95MVphU6zOeD9pDkon+LLbFL7lOQRapbB8SCHw== + dependencies: + "@babel/core" "^7.1.0" + "@jest/types" "^27.5.1" + babel-plugin-istanbul "^6.1.1" + chalk "^4.0.0" + convert-source-map "^1.4.0" + fast-json-stable-stringify "^2.0.0" + graceful-fs "^4.2.9" + jest-haste-map "^27.5.1" + jest-regex-util "^27.5.1" + jest-util "^27.5.1" + micromatch "^4.0.4" + pirates "^4.0.4" + slash "^3.0.0" + source-map "^0.6.1" + write-file-atomic "^3.0.0" + +"@jest/types@^27.5.1": + version "27.5.1" + resolved "https://registry.yarnpkg.com/@jest/types/-/types-27.5.1.tgz#3c79ec4a8ba61c170bf937bcf9e98a9df175ec80" + integrity sha512-Cx46iJ9QpwQTjIdq5VJu2QTMMs3QlEjI0x1QbBP5W1+nMzyc2XmimiRR/CbX9TO0cPTeUlxWMOu8mslYsJ8DEw== + dependencies: + "@types/istanbul-lib-coverage" "^2.0.0" + "@types/istanbul-reports" "^3.0.0" + "@types/node" "*" + "@types/yargs" "^16.0.0" + chalk "^4.0.0" + +"@jridgewell/gen-mapping@^0.3.0", "@jridgewell/gen-mapping@^0.3.2": + version "0.3.3" + resolved "https://registry.yarnpkg.com/@jridgewell/gen-mapping/-/gen-mapping-0.3.3.tgz#7e02e6eb5df901aaedb08514203b096614024098" + integrity sha512-HLhSWOLRi875zjjMG/r+Nv0oCW8umGb0BgEhyX3dDX3egwZtB8PqLnjz3yedt8R5StBrzcg4aBpnh8UA9D1BoQ== + dependencies: + "@jridgewell/set-array" "^1.0.1" + "@jridgewell/sourcemap-codec" "^1.4.10" + "@jridgewell/trace-mapping" "^0.3.9" + +"@jridgewell/resolve-uri@3.1.0": + version "3.1.0" + resolved "https://registry.yarnpkg.com/@jridgewell/resolve-uri/-/resolve-uri-3.1.0.tgz#2203b118c157721addfe69d47b70465463066d78" + integrity sha512-F2msla3tad+Mfht5cJq7LSXcdudKTWCVYUgw6pLFOOHSTtZlj6SWNYAp+AhuqLmWdBO2X5hPrLcu8cVP8fy28w== + +"@jridgewell/resolve-uri@^3.0.3": + version "3.1.1" + resolved "https://registry.yarnpkg.com/@jridgewell/resolve-uri/-/resolve-uri-3.1.1.tgz#c08679063f279615a3326583ba3a90d1d82cc721" + integrity sha512-dSYZh7HhCDtCKm4QakX0xFpsRDqjjtZf/kjI/v3T3Nwt5r8/qz/M19F9ySyOqU94SXBmeG9ttTul+YnR4LOxFA== + +"@jridgewell/set-array@^1.0.1": + version "1.1.2" + resolved "https://registry.yarnpkg.com/@jridgewell/set-array/-/set-array-1.1.2.tgz#7c6cf998d6d20b914c0a55a91ae928ff25965e72" + integrity sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw== + +"@jridgewell/source-map@^0.3.2": + version "0.3.3" + resolved "https://registry.yarnpkg.com/@jridgewell/source-map/-/source-map-0.3.3.tgz#8108265659d4c33e72ffe14e33d6cc5eb59f2fda" + integrity sha512-b+fsZXeLYi9fEULmfBrhxn4IrPlINf8fiNarzTof004v3lFdntdwa9PF7vFJqm3mg7s+ScJMxXaE3Acp1irZcg== + dependencies: + "@jridgewell/gen-mapping" "^0.3.0" + "@jridgewell/trace-mapping" "^0.3.9" + +"@jridgewell/sourcemap-codec@1.4.14": + version "1.4.14" + resolved "https://registry.yarnpkg.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.14.tgz#add4c98d341472a289190b424efbdb096991bb24" + integrity sha512-XPSJHWmi394fuUuzDnGz1wiKqWfo1yXecHQMRf2l6hztTO+nPru658AyDngaBe7isIxEkRsPR3FZh+s7iVa4Uw== + +"@jridgewell/sourcemap-codec@^1.4.10": + version "1.4.15" + resolved "https://registry.yarnpkg.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz#d7c6e6755c78567a951e04ab52ef0fd26de59f32" + integrity sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg== + +"@jridgewell/trace-mapping@0.3.9": + version "0.3.9" + resolved "https://registry.yarnpkg.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz#6534fd5933a53ba7cbf3a17615e273a0d1273ff9" + integrity sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ== + dependencies: + "@jridgewell/resolve-uri" "^3.0.3" + "@jridgewell/sourcemap-codec" "^1.4.10" + +"@jridgewell/trace-mapping@^0.3.17", "@jridgewell/trace-mapping@^0.3.9": + version "0.3.18" + resolved "https://registry.yarnpkg.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.18.tgz#25783b2086daf6ff1dcb53c9249ae480e4dd4cd6" + integrity sha512-w+niJYzMHdd7USdiH2U6869nqhD2nbfZXND5Yp93qIbEmnDNk7PD48o+YchRVpzMU7M6jVCbenTR7PA1FLQ9pA== + dependencies: + "@jridgewell/resolve-uri" "3.1.0" + "@jridgewell/sourcemap-codec" "1.4.14" + +"@nestjs/cli@^8.0.0": + version "8.2.8" + resolved "https://registry.yarnpkg.com/@nestjs/cli/-/cli-8.2.8.tgz#63e5b477f90e6d0238365dcc6236b95bf4f0c807" + integrity sha512-y5Imcw1EY0OxD3POAM7SLUB1rFdn5FjbfSsyJrokjKmXY+i6KcBdbRrv3Ox7aeJ4W7wXuckIXZEUlK6lC52dnA== + dependencies: + "@angular-devkit/core" "13.3.6" + "@angular-devkit/schematics" "13.3.6" + "@angular-devkit/schematics-cli" "13.3.6" + "@nestjs/schematics" "^8.0.3" + chalk "3.0.0" + chokidar "3.5.3" + cli-table3 "0.6.2" + commander "4.1.1" + fork-ts-checker-webpack-plugin "7.2.11" + inquirer "7.3.3" + node-emoji "1.11.0" + ora "5.4.1" + os-name "4.0.1" + rimraf "3.0.2" + shelljs "0.8.5" + source-map-support "0.5.21" + tree-kill "1.2.2" + tsconfig-paths "3.14.1" + tsconfig-paths-webpack-plugin "3.5.2" + typescript "4.7.4" + webpack "5.73.0" + webpack-node-externals "3.0.0" + +"@nestjs/common@^8.0.0": + version "8.4.7" + resolved "https://registry.yarnpkg.com/@nestjs/common/-/common-8.4.7.tgz#fc4a575b797e230bb5a0bcab6da8b796aa88d605" + integrity sha512-m/YsbcBal+gA5CFrDpqXqsSfylo+DIQrkFY3qhVIltsYRfu8ct8J9pqsTO6OPf3mvqdOpFGrV5sBjoyAzOBvsw== + dependencies: + axios "0.27.2" + iterare "1.2.1" + tslib "2.4.0" + uuid "8.3.2" + +"@nestjs/core@^8.0.0": + version "8.4.7" + resolved "https://registry.yarnpkg.com/@nestjs/core/-/core-8.4.7.tgz#fbec7fa744ac8749a4b966f759a6656c1cf43883" + integrity sha512-XB9uexHqzr2xkPo6QSiQWJJttyYYLmvQ5My64cFvWFi7Wk2NIus0/xUNInwX3kmFWB6pF1ab5Y2ZBvWdPwGBhw== + dependencies: + "@nuxtjs/opencollective" "0.3.2" + fast-safe-stringify "2.1.1" + iterare "1.2.1" + object-hash "3.0.0" + path-to-regexp "3.2.0" + tslib "2.4.0" + uuid "8.3.2" + +"@nestjs/platform-express@^8.0.0": + version "8.4.7" + resolved "https://registry.yarnpkg.com/@nestjs/platform-express/-/platform-express-8.4.7.tgz#402a3d3c47327a164bb3867615f423c29d1a6cd9" + integrity sha512-lPE5Ltg2NbQGRQIwXWY+4cNrXhJdycbxFDQ8mNxSIuv+LbrJBIdEB/NONk+LLn9N/8d2+I2LsIETGQrPvsejBg== + dependencies: + body-parser "1.20.0" + cors "2.8.5" + express "4.18.1" + multer "1.4.4-lts.1" + tslib "2.4.0" + +"@nestjs/schematics@^8.0.0", "@nestjs/schematics@^8.0.3": + version "8.0.11" + resolved "https://registry.yarnpkg.com/@nestjs/schematics/-/schematics-8.0.11.tgz#5d0c56184826660a2c01b1c326dbdbb12880e864" + integrity sha512-W/WzaxgH5aE01AiIErE9QrQJ73VR/M/8p8pq0LZmjmNcjZqU5kQyOWUxZg13WYfSpJdOa62t6TZRtFDmgZPoIg== + dependencies: + "@angular-devkit/core" "13.3.5" + "@angular-devkit/schematics" "13.3.5" + fs-extra "10.1.0" + jsonc-parser "3.0.0" + pluralize "8.0.0" + +"@nestjs/testing@^8.0.0": + version "8.4.7" + resolved "https://registry.yarnpkg.com/@nestjs/testing/-/testing-8.4.7.tgz#fe4f356c0e081e25fe8c899a65e91dd88947fd13" + integrity sha512-aedpeJFicTBeiTCvJWUG45WMMS53f5eu8t2fXsfjsU1t+WdDJqYcZyrlCzA4dL1B7MfbqaTURdvuVVHTmJO8ag== + dependencies: + tslib "2.4.0" + +"@nodelib/fs.scandir@2.1.5": + version "2.1.5" + resolved "https://registry.yarnpkg.com/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz#7619c2eb21b25483f6d167548b4cfd5a7488c3d5" + integrity sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g== + dependencies: + "@nodelib/fs.stat" "2.0.5" + run-parallel "^1.1.9" + +"@nodelib/fs.stat@2.0.5", "@nodelib/fs.stat@^2.0.2": + version "2.0.5" + resolved "https://registry.yarnpkg.com/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz#5bd262af94e9d25bd1e71b05deed44876a222e8b" + integrity sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A== + +"@nodelib/fs.walk@^1.2.3", "@nodelib/fs.walk@^1.2.8": + version "1.2.8" + resolved "https://registry.yarnpkg.com/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz#e95737e8bb6746ddedf69c556953494f196fe69a" + integrity sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg== + dependencies: + "@nodelib/fs.scandir" "2.1.5" + fastq "^1.6.0" + +"@nuxtjs/opencollective@0.3.2": + version "0.3.2" + resolved "https://registry.yarnpkg.com/@nuxtjs/opencollective/-/opencollective-0.3.2.tgz#620ce1044f7ac77185e825e1936115bb38e2681c" + integrity sha512-um0xL3fO7Mf4fDxcqx9KryrB7zgRM5JSlvGN5AGkP6JLM5XEKyjeAiPbNxdXVXQ16isuAhYpvP88NgL2BGd6aA== + dependencies: + chalk "^4.1.0" + consola "^2.15.0" + node-fetch "^2.6.1" + +"@sinonjs/commons@^1.7.0": + version "1.8.6" + resolved "https://registry.yarnpkg.com/@sinonjs/commons/-/commons-1.8.6.tgz#80c516a4dc264c2a69115e7578d62581ff455ed9" + integrity sha512-Ky+XkAkqPZSm3NLBeUng77EBQl3cmeJhITaGHdYH8kjVB+aun3S4XBRti2zt17mtt0mIUDiNxYeoJm6drVvBJQ== + dependencies: + type-detect "4.0.8" + +"@sinonjs/fake-timers@^8.0.1": + version "8.1.0" + resolved "https://registry.yarnpkg.com/@sinonjs/fake-timers/-/fake-timers-8.1.0.tgz#3fdc2b6cb58935b21bfb8d1625eb1300484316e7" + integrity sha512-OAPJUAtgeINhh/TAlUID4QTs53Njm7xzddaVlEs/SXwgtiD1tW22zAB/W1wdqfrpmikgaWQ9Fw6Ws+hsiRm5Vg== + dependencies: + "@sinonjs/commons" "^1.7.0" + +"@tootallnate/once@1": + version "1.1.2" + resolved "https://registry.yarnpkg.com/@tootallnate/once/-/once-1.1.2.tgz#ccb91445360179a04e7fe6aff78c00ffc1eeaf82" + integrity sha512-RbzJvlNzmRq5c3O09UipeuXno4tA1FE6ikOjxZK0tuxVv3412l64l5t1W5pj4+rJq9vpkm/kwiR07aZXnsKPxw== + +"@tsconfig/node10@^1.0.7": + version "1.0.9" + resolved "https://registry.yarnpkg.com/@tsconfig/node10/-/node10-1.0.9.tgz#df4907fc07a886922637b15e02d4cebc4c0021b2" + integrity sha512-jNsYVVxU8v5g43Erja32laIDHXeoNvFEpX33OK4d6hljo3jDhCBDhx5dhCCTMWUojscpAagGiRkBKxpdl9fxqA== + +"@tsconfig/node12@^1.0.7": + version "1.0.11" + resolved "https://registry.yarnpkg.com/@tsconfig/node12/-/node12-1.0.11.tgz#ee3def1f27d9ed66dac6e46a295cffb0152e058d" + integrity sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag== + +"@tsconfig/node14@^1.0.0": + version "1.0.3" + resolved "https://registry.yarnpkg.com/@tsconfig/node14/-/node14-1.0.3.tgz#e4386316284f00b98435bf40f72f75a09dabf6c1" + integrity sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow== + +"@tsconfig/node16@^1.0.2": + version "1.0.3" + resolved "https://registry.yarnpkg.com/@tsconfig/node16/-/node16-1.0.3.tgz#472eaab5f15c1ffdd7f8628bd4c4f753995ec79e" + integrity sha512-yOlFc+7UtL/89t2ZhjPvvB/DeAr3r+Dq58IgzsFkOAvVC6NMJXmCGjbptdXdR9qsX7pKcTL+s87FtYREi2dEEQ== + +"@types/babel__core@^7.0.0", "@types/babel__core@^7.1.14": + version "7.20.0" + resolved "https://registry.yarnpkg.com/@types/babel__core/-/babel__core-7.20.0.tgz#61bc5a4cae505ce98e1e36c5445e4bee060d8891" + integrity sha512-+n8dL/9GWblDO0iU6eZAwEIJVr5DWigtle+Q6HLOrh/pdbXOhOtqzq8VPPE2zvNJzSKY4vH/z3iT3tn0A3ypiQ== + dependencies: + "@babel/parser" "^7.20.7" + "@babel/types" "^7.20.7" + "@types/babel__generator" "*" + "@types/babel__template" "*" + "@types/babel__traverse" "*" + +"@types/babel__generator@*": + version "7.6.4" + resolved "https://registry.yarnpkg.com/@types/babel__generator/-/babel__generator-7.6.4.tgz#1f20ce4c5b1990b37900b63f050182d28c2439b7" + integrity sha512-tFkciB9j2K755yrTALxD44McOrk+gfpIpvC3sxHjRawj6PfnQxrse4Clq5y/Rq+G3mrBurMax/lG8Qn2t9mSsg== + dependencies: + "@babel/types" "^7.0.0" + +"@types/babel__template@*": + version "7.4.1" + resolved "https://registry.yarnpkg.com/@types/babel__template/-/babel__template-7.4.1.tgz#3d1a48fd9d6c0edfd56f2ff578daed48f36c8969" + integrity sha512-azBFKemX6kMg5Io+/rdGT0dkGreboUVR0Cdm3fz9QJWpaQGJRQXl7C+6hOTCZcMll7KFyEQpgbYI2lHdsS4U7g== + dependencies: + "@babel/parser" "^7.1.0" + "@babel/types" "^7.0.0" + +"@types/babel__traverse@*", "@types/babel__traverse@^7.0.4", "@types/babel__traverse@^7.0.6": + version "7.18.5" + resolved "https://registry.yarnpkg.com/@types/babel__traverse/-/babel__traverse-7.18.5.tgz#c107216842905afafd3b6e774f6f935da6f5db80" + integrity sha512-enCvTL8m/EHS/zIvJno9nE+ndYPh1/oNFzRYRmtUqJICG2VnCSBzMLW5VN2KCQU91f23tsNKR8v7VJJQMatl7Q== + dependencies: + "@babel/types" "^7.3.0" + +"@types/body-parser@*": + version "1.19.2" + resolved "https://registry.yarnpkg.com/@types/body-parser/-/body-parser-1.19.2.tgz#aea2059e28b7658639081347ac4fab3de166e6f0" + integrity sha512-ALYone6pm6QmwZoAgeyNksccT9Q4AWZQ6PvfwR37GT6r6FWUPguq6sUmNGSMV2Wr761oQoBxwGGa6DR5o1DC9g== + dependencies: + "@types/connect" "*" + "@types/node" "*" + +"@types/connect@*": + version "3.4.35" + resolved "https://registry.yarnpkg.com/@types/connect/-/connect-3.4.35.tgz#5fcf6ae445e4021d1fc2219a4873cc73a3bb2ad1" + integrity sha512-cdeYyv4KWoEgpBISTxWvqYsVy444DOqehiF3fM3ne10AmJ62RSyNkUnxMJXHQWRQQX2eR94m5y1IZyDwBjV9FQ== + dependencies: + "@types/node" "*" + +"@types/cookiejar@*": + version "2.1.2" + resolved "https://registry.yarnpkg.com/@types/cookiejar/-/cookiejar-2.1.2.tgz#66ad9331f63fe8a3d3d9d8c6e3906dd10f6446e8" + integrity sha512-t73xJJrvdTjXrn4jLS9VSGRbz0nUY3cl2DMGDU48lKl+HR9dbbjW2A9r3g40VA++mQpy6uuHg33gy7du2BKpog== + +"@types/eslint-scope@^3.7.3": + version "3.7.4" + resolved "https://registry.yarnpkg.com/@types/eslint-scope/-/eslint-scope-3.7.4.tgz#37fc1223f0786c39627068a12e94d6e6fc61de16" + integrity sha512-9K4zoImiZc3HlIp6AVUDE4CWYx22a+lhSZMYNpbjW04+YF0KWj4pJXnEMjdnFTiQibFFmElcsasJXDbdI/EPhA== + dependencies: + "@types/eslint" "*" + "@types/estree" "*" + +"@types/eslint@*": + version "8.37.0" + resolved "https://registry.yarnpkg.com/@types/eslint/-/eslint-8.37.0.tgz#29cebc6c2a3ac7fea7113207bf5a828fdf4d7ef1" + integrity sha512-Piet7dG2JBuDIfohBngQ3rCt7MgO9xCO4xIMKxBThCq5PNRB91IjlJ10eJVwfoNtvTErmxLzwBZ7rHZtbOMmFQ== + dependencies: + "@types/estree" "*" + "@types/json-schema" "*" + +"@types/estree@*": + version "1.0.1" + resolved "https://registry.yarnpkg.com/@types/estree/-/estree-1.0.1.tgz#aa22750962f3bf0e79d753d3cc067f010c95f194" + integrity sha512-LG4opVs2ANWZ1TJoKc937iMmNstM/d0ae1vNbnBvBhqCSezgVUOzcLCqbI5elV8Vy6WKwKjaqR+zO9VKirBBCA== + +"@types/estree@^0.0.51": + version "0.0.51" + resolved "https://registry.yarnpkg.com/@types/estree/-/estree-0.0.51.tgz#cfd70924a25a3fd32b218e5e420e6897e1ac4f40" + integrity sha512-CuPgU6f3eT/XgKKPqKd/gLZV1Xmvf1a2R5POBOGQa6uv82xpls89HU5zKeVoyR8XzHd1RGNOlQlvUe3CFkjWNQ== + +"@types/express-serve-static-core@^4.17.33": + version "4.17.34" + resolved "https://registry.yarnpkg.com/@types/express-serve-static-core/-/express-serve-static-core-4.17.34.tgz#c119e85b75215178bc127de588e93100698ab4cc" + integrity sha512-fvr49XlCGoUj2Pp730AItckfjat4WNb0lb3kfrLWffd+RLeoGAMsq7UOy04PAPtoL01uKwcp6u8nhzpgpDYr3w== + dependencies: + "@types/node" "*" + "@types/qs" "*" + "@types/range-parser" "*" + "@types/send" "*" + +"@types/express@^4.17.13": + version "4.17.17" + resolved "https://registry.yarnpkg.com/@types/express/-/express-4.17.17.tgz#01d5437f6ef9cfa8668e616e13c2f2ac9a491ae4" + integrity sha512-Q4FmmuLGBG58btUnfS1c1r/NQdlp3DMfGDGig8WhfpA2YRUtEkxAjkZb0yvplJGYdF1fsQ81iMDcH24sSCNC/Q== + dependencies: + "@types/body-parser" "*" + "@types/express-serve-static-core" "^4.17.33" + "@types/qs" "*" + "@types/serve-static" "*" + +"@types/graceful-fs@^4.1.2": + version "4.1.6" + resolved "https://registry.yarnpkg.com/@types/graceful-fs/-/graceful-fs-4.1.6.tgz#e14b2576a1c25026b7f02ede1de3b84c3a1efeae" + integrity sha512-Sig0SNORX9fdW+bQuTEovKj3uHcUL6LQKbCrrqb1X7J6/ReAbhCXRAhc+SMejhLELFj2QcyuxmUooZ4bt5ReSw== + dependencies: + "@types/node" "*" + +"@types/istanbul-lib-coverage@*", "@types/istanbul-lib-coverage@^2.0.0", "@types/istanbul-lib-coverage@^2.0.1": + version "2.0.4" + resolved "https://registry.yarnpkg.com/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.4.tgz#8467d4b3c087805d63580480890791277ce35c44" + integrity sha512-z/QT1XN4K4KYuslS23k62yDIDLwLFkzxOuMplDtObz0+y7VqJCaO2o+SPwHCvLFZh7xazvvoor2tA/hPz9ee7g== + +"@types/istanbul-lib-report@*": + version "3.0.0" + resolved "https://registry.yarnpkg.com/@types/istanbul-lib-report/-/istanbul-lib-report-3.0.0.tgz#c14c24f18ea8190c118ee7562b7ff99a36552686" + integrity sha512-plGgXAPfVKFoYfa9NpYDAkseG+g6Jr294RqeqcqDixSbU34MZVJRi/P+7Y8GDpzkEwLaGZZOpKIEmeVZNtKsrg== + dependencies: + "@types/istanbul-lib-coverage" "*" + +"@types/istanbul-reports@^3.0.0": + version "3.0.1" + resolved "https://registry.yarnpkg.com/@types/istanbul-reports/-/istanbul-reports-3.0.1.tgz#9153fe98bba2bd565a63add9436d6f0d7f8468ff" + integrity sha512-c3mAZEuK0lvBp8tmuL74XRKn1+y2dcwOUpH7x4WrF6gk1GIgiluDRgMYQtw2OFcBvAJWlt6ASU3tSqxp0Uu0Aw== + dependencies: + "@types/istanbul-lib-report" "*" + +"@types/jest@27.4.1": + version "27.4.1" + resolved "https://registry.yarnpkg.com/@types/jest/-/jest-27.4.1.tgz#185cbe2926eaaf9662d340cc02e548ce9e11ab6d" + integrity sha512-23iPJADSmicDVrWk+HT58LMJtzLAnB2AgIzplQuq/bSrGaxCrlvRFjGbXmamnnk/mAmCdLStiGqggu28ocUyiw== + dependencies: + jest-matcher-utils "^27.0.0" + pretty-format "^27.0.0" + +"@types/json-schema@*", "@types/json-schema@^7.0.8", "@types/json-schema@^7.0.9": + version "7.0.11" + resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.11.tgz#d421b6c527a3037f7c84433fd2c4229e016863d3" + integrity sha512-wOuvG1SN4Us4rez+tylwwwCV1psiNVOkJeM3AUWUNWg/jDQY2+HE/444y5gc+jBmRqASOm2Oeh5c1axHobwRKQ== + +"@types/json5@^0.0.29": + version "0.0.29" + resolved "https://registry.yarnpkg.com/@types/json5/-/json5-0.0.29.tgz#ee28707ae94e11d2b827bcbe5270bcea7f3e71ee" + integrity sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ== + +"@types/mime@*": + version "3.0.1" + resolved "https://registry.yarnpkg.com/@types/mime/-/mime-3.0.1.tgz#5f8f2bca0a5863cb69bc0b0acd88c96cb1d4ae10" + integrity sha512-Y4XFY5VJAuw0FgAqPNd6NNoV44jbq9Bz2L7Rh/J6jLTiHBSBJa9fxqQIvkIld4GsoDOcCbvzOUAbLPsSKKg+uA== + +"@types/mime@^1": + version "1.3.2" + resolved "https://registry.yarnpkg.com/@types/mime/-/mime-1.3.2.tgz#93e25bf9ee75fe0fd80b594bc4feb0e862111b5a" + integrity sha512-YATxVxgRqNH6nHEIsvg6k2Boc1JHI9ZbH5iWFFv/MTkchz3b1ieGDa5T0a9RznNdI0KhVbdbWSN+KWWrQZRxTw== + +"@types/node@*": + version "20.1.0" + resolved "https://registry.yarnpkg.com/@types/node/-/node-20.1.0.tgz#258805edc37c327cf706e64c6957f241ca4c4c20" + integrity sha512-O+z53uwx64xY7D6roOi4+jApDGFg0qn6WHcxe5QeqjMaTezBO/mxdfFXIVAVVyNWKx84OmPB3L8kbVYOTeN34A== + +"@types/node@^16.0.0": + version "16.18.26" + resolved "https://registry.yarnpkg.com/@types/node/-/node-16.18.26.tgz#a18b88726a67bc6a8a5bdac9a40c093ecb03ccd0" + integrity sha512-pCNBzNQqCXE4A6FWDmrn/o1Qu+qBf8tnorBlNoPNSBQJF+jXzvTKNI/aMiE+hGJbK5sDAD65g7OS/YwSHIEJdw== + +"@types/parse-json@^4.0.0": + version "4.0.0" + resolved "https://registry.yarnpkg.com/@types/parse-json/-/parse-json-4.0.0.tgz#2f8bb441434d163b35fb8ffdccd7138927ffb8c0" + integrity sha512-//oorEZjL6sbPcKUaCdIGlIUeH26mgzimjBB77G6XRgnDl/L5wOnpyBGRe/Mmf5CVW3PwEBE1NjiMZ/ssFh4wA== + +"@types/prettier@^2.1.5": + version "2.7.2" + resolved "https://registry.yarnpkg.com/@types/prettier/-/prettier-2.7.2.tgz#6c2324641cc4ba050a8c710b2b251b377581fbf0" + integrity sha512-KufADq8uQqo1pYKVIYzfKbJfBAc0sOeXqGbFaSpv8MRmC/zXgowNZmFcbngndGk922QDmOASEXUZCaY48gs4cg== + +"@types/qs@*": + version "6.9.7" + resolved "https://registry.yarnpkg.com/@types/qs/-/qs-6.9.7.tgz#63bb7d067db107cc1e457c303bc25d511febf6cb" + integrity sha512-FGa1F62FT09qcrueBA6qYTrJPVDzah9a+493+o2PCXsesWHIn27G98TsSMs3WPNbZIEj4+VJf6saSFpvD+3Zsw== + +"@types/range-parser@*": + version "1.2.4" + resolved "https://registry.yarnpkg.com/@types/range-parser/-/range-parser-1.2.4.tgz#cd667bcfdd025213aafb7ca5915a932590acdcdc" + integrity sha512-EEhsLsD6UsDM1yFhAvy0Cjr6VwmpMWqFBCb9w07wVugF7w9nfajxLuVmngTIpgS6svCnm6Vaw+MZhoDCKnOfsw== + +"@types/semver@^7.3.12": + version "7.3.13" + resolved "https://registry.yarnpkg.com/@types/semver/-/semver-7.3.13.tgz#da4bfd73f49bd541d28920ab0e2bf0ee80f71c91" + integrity sha512-21cFJr9z3g5dW8B0CVI9g2O9beqaThGQ6ZFBqHfwhzLDKUxaqTIy3vnfah/UPkfOiF2pLq+tGz+W8RyCskuslw== + +"@types/send@*": + version "0.17.1" + resolved "https://registry.yarnpkg.com/@types/send/-/send-0.17.1.tgz#ed4932b8a2a805f1fe362a70f4e62d0ac994e301" + integrity sha512-Cwo8LE/0rnvX7kIIa3QHCkcuF21c05Ayb0ZfxPiv0W8VRiZiNW/WuRupHKpqqGVGf7SUA44QSOUKaEd9lIrd/Q== + dependencies: + "@types/mime" "^1" + "@types/node" "*" + +"@types/serve-static@*": + version "1.15.1" + resolved "https://registry.yarnpkg.com/@types/serve-static/-/serve-static-1.15.1.tgz#86b1753f0be4f9a1bee68d459fcda5be4ea52b5d" + integrity sha512-NUo5XNiAdULrJENtJXZZ3fHtfMolzZwczzBbnAeBbqBwG+LaG6YaJtuwzwGSQZ2wsCrxjEhNNjAkKigy3n8teQ== + dependencies: + "@types/mime" "*" + "@types/node" "*" + +"@types/stack-utils@^2.0.0": + version "2.0.1" + resolved "https://registry.yarnpkg.com/@types/stack-utils/-/stack-utils-2.0.1.tgz#20f18294f797f2209b5f65c8e3b5c8e8261d127c" + integrity sha512-Hl219/BT5fLAaz6NDkSuhzasy49dwQS/DSdu4MdggFB8zcXv7vflBI3xp7FEmkmdDkBUI2bPUNeMttp2knYdxw== + +"@types/superagent@*": + version "4.1.17" + resolved "https://registry.yarnpkg.com/@types/superagent/-/superagent-4.1.17.tgz#c8f0162b5d8a9c52d38b81398ef0650ef974b452" + integrity sha512-FFK/rRjNy24U6J1BvQkaNWu2ohOIF/kxRQXRsbT141YQODcOcZjzlcc4DGdI2SkTa0rhmF+X14zu6ICjCGIg+w== + dependencies: + "@types/cookiejar" "*" + "@types/node" "*" + +"@types/supertest@^2.0.11": + version "2.0.12" + resolved "https://registry.yarnpkg.com/@types/supertest/-/supertest-2.0.12.tgz#ddb4a0568597c9aadff8dbec5b2e8fddbe8692fc" + integrity sha512-X3HPWTwXRerBZS7Mo1k6vMVR1Z6zmJcDVn5O/31whe0tnjE4te6ZJSJGq1RiqHPjzPdMTfjCFogDJmwng9xHaQ== + dependencies: + "@types/superagent" "*" + +"@types/yargs-parser@*": + version "21.0.0" + resolved "https://registry.yarnpkg.com/@types/yargs-parser/-/yargs-parser-21.0.0.tgz#0c60e537fa790f5f9472ed2776c2b71ec117351b" + integrity sha512-iO9ZQHkZxHn4mSakYV0vFHAVDyEOIJQrV2uZ06HxEPcx+mt8swXoZHIbaaJ2crJYFfErySgktuTZ3BeLz+XmFA== + +"@types/yargs@^16.0.0": + version "16.0.5" + resolved "https://registry.yarnpkg.com/@types/yargs/-/yargs-16.0.5.tgz#12cc86393985735a283e387936398c2f9e5f88e3" + integrity sha512-AxO/ADJOBFJScHbWhq2xAhlWP24rY4aCEG/NFaMvbT3X2MgRsLjhjQwsn0Zi5zn0LG9jUhCCZMeX9Dkuw6k+vQ== + dependencies: + "@types/yargs-parser" "*" + +"@typescript-eslint/eslint-plugin@^5.0.0": + version "5.59.2" + resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.59.2.tgz#684a2ce7182f3b4dac342eef7caa1c2bae476abd" + integrity sha512-yVrXupeHjRxLDcPKL10sGQ/QlVrA8J5IYOEWVqk0lJaSZP7X5DfnP7Ns3cc74/blmbipQ1htFNVGsHX6wsYm0A== + dependencies: + "@eslint-community/regexpp" "^4.4.0" + "@typescript-eslint/scope-manager" "5.59.2" + "@typescript-eslint/type-utils" "5.59.2" + "@typescript-eslint/utils" "5.59.2" + debug "^4.3.4" + grapheme-splitter "^1.0.4" + ignore "^5.2.0" + natural-compare-lite "^1.4.0" + semver "^7.3.7" + tsutils "^3.21.0" + +"@typescript-eslint/parser@^5.0.0": + version "5.59.2" + resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-5.59.2.tgz#c2c443247901d95865b9f77332d9eee7c55655e8" + integrity sha512-uq0sKyw6ao1iFOZZGk9F8Nro/8+gfB5ezl1cA06SrqbgJAt0SRoFhb9pXaHvkrxUpZaoLxt8KlovHNk8Gp6/HQ== + dependencies: + "@typescript-eslint/scope-manager" "5.59.2" + "@typescript-eslint/types" "5.59.2" + "@typescript-eslint/typescript-estree" "5.59.2" + debug "^4.3.4" + +"@typescript-eslint/scope-manager@5.59.2": + version "5.59.2" + resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-5.59.2.tgz#f699fe936ee4e2c996d14f0fdd3a7da5ba7b9a4c" + integrity sha512-dB1v7ROySwQWKqQ8rEWcdbTsFjh2G0vn8KUyvTXdPoyzSL6lLGkiXEV5CvpJsEe9xIdKV+8Zqb7wif2issoOFA== + dependencies: + "@typescript-eslint/types" "5.59.2" + "@typescript-eslint/visitor-keys" "5.59.2" + +"@typescript-eslint/type-utils@5.59.2": + version "5.59.2" + resolved "https://registry.yarnpkg.com/@typescript-eslint/type-utils/-/type-utils-5.59.2.tgz#0729c237503604cd9a7084b5af04c496c9a4cdcf" + integrity sha512-b1LS2phBOsEy/T381bxkkywfQXkV1dWda/z0PhnIy3bC5+rQWQDS7fk9CSpcXBccPY27Z6vBEuaPBCKCgYezyQ== + dependencies: + "@typescript-eslint/typescript-estree" "5.59.2" + "@typescript-eslint/utils" "5.59.2" + debug "^4.3.4" + tsutils "^3.21.0" + +"@typescript-eslint/types@5.59.2": + version "5.59.2" + resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-5.59.2.tgz#b511d2b9847fe277c5cb002a2318bd329ef4f655" + integrity sha512-LbJ/HqoVs2XTGq5shkiKaNTuVv5tTejdHgfdjqRUGdYhjW1crm/M7og2jhVskMt8/4wS3T1+PfFvL1K3wqYj4w== + +"@typescript-eslint/typescript-estree@5.59.2": + version "5.59.2" + resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-5.59.2.tgz#6e2fabd3ba01db5d69df44e0b654c0b051fe9936" + integrity sha512-+j4SmbwVmZsQ9jEyBMgpuBD0rKwi9RxRpjX71Brr73RsYnEr3Lt5QZ624Bxphp8HUkSKfqGnPJp1kA5nl0Sh7Q== + dependencies: + "@typescript-eslint/types" "5.59.2" + "@typescript-eslint/visitor-keys" "5.59.2" + debug "^4.3.4" + globby "^11.1.0" + is-glob "^4.0.3" + semver "^7.3.7" + tsutils "^3.21.0" + +"@typescript-eslint/utils@5.59.2": + version "5.59.2" + resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-5.59.2.tgz#0c45178124d10cc986115885688db6abc37939f4" + integrity sha512-kSuF6/77TZzyGPhGO4uVp+f0SBoYxCDf+lW3GKhtKru/L8k/Hd7NFQxyWUeY7Z/KGB2C6Fe3yf2vVi4V9TsCSQ== + dependencies: + "@eslint-community/eslint-utils" "^4.2.0" + "@types/json-schema" "^7.0.9" + "@types/semver" "^7.3.12" + "@typescript-eslint/scope-manager" "5.59.2" + "@typescript-eslint/types" "5.59.2" + "@typescript-eslint/typescript-estree" "5.59.2" + eslint-scope "^5.1.1" + semver "^7.3.7" + +"@typescript-eslint/visitor-keys@5.59.2": + version "5.59.2" + resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-5.59.2.tgz#37a419dc2723a3eacbf722512b86d6caf7d3b750" + integrity sha512-EEpsO8m3RASrKAHI9jpavNv9NlEUebV4qmF1OWxSTtKSFBpC1NCmWazDQHFivRf0O1DV11BA645yrLEVQ0/Lig== + dependencies: + "@typescript-eslint/types" "5.59.2" + eslint-visitor-keys "^3.3.0" + +"@webassemblyjs/ast@1.11.1": + version "1.11.1" + resolved "https://registry.yarnpkg.com/@webassemblyjs/ast/-/ast-1.11.1.tgz#2bfd767eae1a6996f432ff7e8d7fc75679c0b6a7" + integrity sha512-ukBh14qFLjxTQNTXocdyksN5QdM28S1CxHt2rdskFyL+xFV7VremuBLVbmCePj+URalXBENx/9Lm7lnhihtCSw== + dependencies: + "@webassemblyjs/helper-numbers" "1.11.1" + "@webassemblyjs/helper-wasm-bytecode" "1.11.1" + +"@webassemblyjs/floating-point-hex-parser@1.11.1": + version "1.11.1" + resolved "https://registry.yarnpkg.com/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.11.1.tgz#f6c61a705f0fd7a6aecaa4e8198f23d9dc179e4f" + integrity sha512-iGRfyc5Bq+NnNuX8b5hwBrRjzf0ocrJPI6GWFodBFzmFnyvrQ83SHKhmilCU/8Jv67i4GJZBMhEzltxzcNagtQ== + +"@webassemblyjs/helper-api-error@1.11.1": + version "1.11.1" + resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-api-error/-/helper-api-error-1.11.1.tgz#1a63192d8788e5c012800ba6a7a46c705288fd16" + integrity sha512-RlhS8CBCXfRUR/cwo2ho9bkheSXG0+NwooXcc3PAILALf2QLdFyj7KGsKRbVc95hZnhnERon4kW/D3SZpp6Tcg== + +"@webassemblyjs/helper-buffer@1.11.1": + version "1.11.1" + resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-buffer/-/helper-buffer-1.11.1.tgz#832a900eb444884cde9a7cad467f81500f5e5ab5" + integrity sha512-gwikF65aDNeeXa8JxXa2BAk+REjSyhrNC9ZwdT0f8jc4dQQeDQ7G4m0f2QCLPJiMTTO6wfDmRmj/pW0PsUvIcA== + +"@webassemblyjs/helper-numbers@1.11.1": + version "1.11.1" + resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-numbers/-/helper-numbers-1.11.1.tgz#64d81da219fbbba1e3bd1bfc74f6e8c4e10a62ae" + integrity sha512-vDkbxiB8zfnPdNK9Rajcey5C0w+QJugEglN0of+kmO8l7lDb77AnlKYQF7aarZuCrv+l0UvqL+68gSDr3k9LPQ== + dependencies: + "@webassemblyjs/floating-point-hex-parser" "1.11.1" + "@webassemblyjs/helper-api-error" "1.11.1" + "@xtuc/long" "4.2.2" + +"@webassemblyjs/helper-wasm-bytecode@1.11.1": + version "1.11.1" + resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.11.1.tgz#f328241e41e7b199d0b20c18e88429c4433295e1" + integrity sha512-PvpoOGiJwXeTrSf/qfudJhwlvDQxFgelbMqtq52WWiXC6Xgg1IREdngmPN3bs4RoO83PnL/nFrxucXj1+BX62Q== + +"@webassemblyjs/helper-wasm-section@1.11.1": + version "1.11.1" + resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.11.1.tgz#21ee065a7b635f319e738f0dd73bfbda281c097a" + integrity sha512-10P9No29rYX1j7F3EVPX3JvGPQPae+AomuSTPiF9eBQeChHI6iqjMIwR9JmOJXwpnn/oVGDk7I5IlskuMwU/pg== + dependencies: + "@webassemblyjs/ast" "1.11.1" + "@webassemblyjs/helper-buffer" "1.11.1" + "@webassemblyjs/helper-wasm-bytecode" "1.11.1" + "@webassemblyjs/wasm-gen" "1.11.1" + +"@webassemblyjs/ieee754@1.11.1": + version "1.11.1" + resolved "https://registry.yarnpkg.com/@webassemblyjs/ieee754/-/ieee754-1.11.1.tgz#963929e9bbd05709e7e12243a099180812992614" + integrity sha512-hJ87QIPtAMKbFq6CGTkZYJivEwZDbQUgYd3qKSadTNOhVY7p+gfP6Sr0lLRVTaG1JjFj+r3YchoqRYxNH3M0GQ== + dependencies: + "@xtuc/ieee754" "^1.2.0" + +"@webassemblyjs/leb128@1.11.1": + version "1.11.1" + resolved "https://registry.yarnpkg.com/@webassemblyjs/leb128/-/leb128-1.11.1.tgz#ce814b45574e93d76bae1fb2644ab9cdd9527aa5" + integrity sha512-BJ2P0hNZ0u+Th1YZXJpzW6miwqQUGcIHT1G/sf72gLVD9DZ5AdYTqPNbHZh6K1M5VmKvFXwGSWZADz+qBWxeRw== + dependencies: + "@xtuc/long" "4.2.2" + +"@webassemblyjs/utf8@1.11.1": + version "1.11.1" + resolved "https://registry.yarnpkg.com/@webassemblyjs/utf8/-/utf8-1.11.1.tgz#d1f8b764369e7c6e6bae350e854dec9a59f0a3ff" + integrity sha512-9kqcxAEdMhiwQkHpkNiorZzqpGrodQQ2IGrHHxCy+Ozng0ofyMA0lTqiLkVs1uzTRejX+/O0EOT7KxqVPuXosQ== + +"@webassemblyjs/wasm-edit@1.11.1": + version "1.11.1" + resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-edit/-/wasm-edit-1.11.1.tgz#ad206ebf4bf95a058ce9880a8c092c5dec8193d6" + integrity sha512-g+RsupUC1aTHfR8CDgnsVRVZFJqdkFHpsHMfJuWQzWU3tvnLC07UqHICfP+4XyL2tnr1amvl1Sdp06TnYCmVkA== + dependencies: + "@webassemblyjs/ast" "1.11.1" + "@webassemblyjs/helper-buffer" "1.11.1" + "@webassemblyjs/helper-wasm-bytecode" "1.11.1" + "@webassemblyjs/helper-wasm-section" "1.11.1" + "@webassemblyjs/wasm-gen" "1.11.1" + "@webassemblyjs/wasm-opt" "1.11.1" + "@webassemblyjs/wasm-parser" "1.11.1" + "@webassemblyjs/wast-printer" "1.11.1" + +"@webassemblyjs/wasm-gen@1.11.1": + version "1.11.1" + resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-gen/-/wasm-gen-1.11.1.tgz#86c5ea304849759b7d88c47a32f4f039ae3c8f76" + integrity sha512-F7QqKXwwNlMmsulj6+O7r4mmtAlCWfO/0HdgOxSklZfQcDu0TpLiD1mRt/zF25Bk59FIjEuGAIyn5ei4yMfLhA== + dependencies: + "@webassemblyjs/ast" "1.11.1" + "@webassemblyjs/helper-wasm-bytecode" "1.11.1" + "@webassemblyjs/ieee754" "1.11.1" + "@webassemblyjs/leb128" "1.11.1" + "@webassemblyjs/utf8" "1.11.1" + +"@webassemblyjs/wasm-opt@1.11.1": + version "1.11.1" + resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-opt/-/wasm-opt-1.11.1.tgz#657b4c2202f4cf3b345f8a4c6461c8c2418985f2" + integrity sha512-VqnkNqnZlU5EB64pp1l7hdm3hmQw7Vgqa0KF/KCNO9sIpI6Fk6brDEiX+iCOYrvMuBWDws0NkTOxYEb85XQHHw== + dependencies: + "@webassemblyjs/ast" "1.11.1" + "@webassemblyjs/helper-buffer" "1.11.1" + "@webassemblyjs/wasm-gen" "1.11.1" + "@webassemblyjs/wasm-parser" "1.11.1" + +"@webassemblyjs/wasm-parser@1.11.1": + version "1.11.1" + resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-parser/-/wasm-parser-1.11.1.tgz#86ca734534f417e9bd3c67c7a1c75d8be41fb199" + integrity sha512-rrBujw+dJu32gYB7/Lup6UhdkPx9S9SnobZzRVL7VcBH9Bt9bCBLEuX/YXOOtBsOZ4NQrRykKhffRWHvigQvOA== + dependencies: + "@webassemblyjs/ast" "1.11.1" + "@webassemblyjs/helper-api-error" "1.11.1" + "@webassemblyjs/helper-wasm-bytecode" "1.11.1" + "@webassemblyjs/ieee754" "1.11.1" + "@webassemblyjs/leb128" "1.11.1" + "@webassemblyjs/utf8" "1.11.1" + +"@webassemblyjs/wast-printer@1.11.1": + version "1.11.1" + resolved "https://registry.yarnpkg.com/@webassemblyjs/wast-printer/-/wast-printer-1.11.1.tgz#d0c73beda8eec5426f10ae8ef55cee5e7084c2f0" + integrity sha512-IQboUWM4eKzWW+N/jij2sRatKMh99QEelo3Eb2q0qXkvPRISAj8Qxtmw5itwqK+TTkBuUIE45AxYPToqPtL5gg== + dependencies: + "@webassemblyjs/ast" "1.11.1" + "@xtuc/long" "4.2.2" + +"@xtuc/ieee754@^1.2.0": + version "1.2.0" + resolved "https://registry.yarnpkg.com/@xtuc/ieee754/-/ieee754-1.2.0.tgz#eef014a3145ae477a1cbc00cd1e552336dceb790" + integrity sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA== + +"@xtuc/long@4.2.2": + version "4.2.2" + resolved "https://registry.yarnpkg.com/@xtuc/long/-/long-4.2.2.tgz#d291c6a4e97989b5c61d9acf396ae4fe133a718d" + integrity sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ== + +abab@^2.0.3, abab@^2.0.5: + version "2.0.6" + resolved "https://registry.yarnpkg.com/abab/-/abab-2.0.6.tgz#41b80f2c871d19686216b82309231cfd3cb3d291" + integrity sha512-j2afSsaIENvHZN2B8GOpF566vZ5WVk5opAiMTvWgaQT8DkbOqsTfvNAvHoRGU2zzP8cPoqys+xHTRDWW8L+/BA== + +accepts@~1.3.8: + version "1.3.8" + resolved "https://registry.yarnpkg.com/accepts/-/accepts-1.3.8.tgz#0bf0be125b67014adcb0b0921e62db7bffe16b2e" + integrity sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw== + dependencies: + mime-types "~2.1.34" + negotiator "0.6.3" + +acorn-globals@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/acorn-globals/-/acorn-globals-6.0.0.tgz#46cdd39f0f8ff08a876619b55f5ac8a6dc770b45" + integrity sha512-ZQl7LOWaF5ePqqcX4hLuv/bLXYQNfNWw2c0/yX/TsPRKamzHcTGQnlCjHT3TsmkOUVEPS3crCxiPfdzE/Trlhg== + dependencies: + acorn "^7.1.1" + acorn-walk "^7.1.1" + +acorn-import-assertions@^1.7.6: + version "1.8.0" + resolved "https://registry.yarnpkg.com/acorn-import-assertions/-/acorn-import-assertions-1.8.0.tgz#ba2b5939ce62c238db6d93d81c9b111b29b855e9" + integrity sha512-m7VZ3jwz4eK6A4Vtt8Ew1/mNbP24u0FhdyfA7fSvnJR6LMdfOYnmuIrrJAgrYfYJ10F/otaHTtrtrtmHdMNzEw== + +acorn-jsx@^5.3.2: + version "5.3.2" + resolved "https://registry.yarnpkg.com/acorn-jsx/-/acorn-jsx-5.3.2.tgz#7ed5bb55908b3b2f1bc55c6af1653bada7f07937" + integrity sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ== + +acorn-walk@^7.1.1: + version "7.2.0" + resolved "https://registry.yarnpkg.com/acorn-walk/-/acorn-walk-7.2.0.tgz#0de889a601203909b0fbe07b8938dc21d2e967bc" + integrity sha512-OPdCF6GsMIP+Az+aWfAAOEt2/+iVDKE7oy6lJ098aoe59oAmK76qV6Gw60SbZ8jHuG2wH058GF4pLFbYamYrVA== + +acorn-walk@^8.1.1: + version "8.2.0" + resolved "https://registry.yarnpkg.com/acorn-walk/-/acorn-walk-8.2.0.tgz#741210f2e2426454508853a2f44d0ab83b7f69c1" + integrity sha512-k+iyHEuPgSw6SbuDpGQM+06HQUa04DZ3o+F6CSzXMvvI5KMvnaEqXe+YVe555R9nn6GPt404fos4wcgpw12SDA== + +acorn@^7.1.1: + version "7.4.1" + resolved "https://registry.yarnpkg.com/acorn/-/acorn-7.4.1.tgz#feaed255973d2e77555b83dbc08851a6c63520fa" + integrity sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A== + +acorn@^8.2.4, acorn@^8.4.1, acorn@^8.5.0, acorn@^8.8.0: + version "8.8.2" + resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.8.2.tgz#1b2f25db02af965399b9776b0c2c391276d37c4a" + integrity sha512-xjIYgE8HBrkpd/sJqOGNspf8uHG+NOHGOw6a/Urj8taM2EXfdNAH2oFcPeIFfsv3+kz/mJrS5VuMqbNLjCa2vw== + +agent-base@6: + version "6.0.2" + resolved "https://registry.yarnpkg.com/agent-base/-/agent-base-6.0.2.tgz#49fff58577cfee3f37176feab4c22e00f86d7f77" + integrity sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ== + dependencies: + debug "4" + +ajv-formats@2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/ajv-formats/-/ajv-formats-2.1.1.tgz#6e669400659eb74973bbf2e33327180a0996b520" + integrity sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA== + dependencies: + ajv "^8.0.0" + +ajv-keywords@^3.5.2: + version "3.5.2" + resolved "https://registry.yarnpkg.com/ajv-keywords/-/ajv-keywords-3.5.2.tgz#31f29da5ab6e00d1c2d329acf7b5929614d5014d" + integrity sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ== + +ajv@8.9.0: + version "8.9.0" + resolved "https://registry.yarnpkg.com/ajv/-/ajv-8.9.0.tgz#738019146638824dea25edcf299dcba1b0e7eb18" + integrity sha512-qOKJyNj/h+OWx7s5DePL6Zu1KeM9jPZhwBqs+7DzP6bGOvqzVCSf0xueYmVuaC/oQ/VtS2zLMLHdQFbkka+XDQ== + dependencies: + fast-deep-equal "^3.1.1" + json-schema-traverse "^1.0.0" + require-from-string "^2.0.2" + uri-js "^4.2.2" + +ajv@^6.10.0, ajv@^6.12.4, ajv@^6.12.5: + version "6.12.6" + resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.12.6.tgz#baf5a62e802b07d977034586f8c3baf5adf26df4" + integrity sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g== + dependencies: + fast-deep-equal "^3.1.1" + fast-json-stable-stringify "^2.0.0" + json-schema-traverse "^0.4.1" + uri-js "^4.2.2" + +ajv@^8.0.0: + version "8.12.0" + resolved "https://registry.yarnpkg.com/ajv/-/ajv-8.12.0.tgz#d1a0527323e22f53562c567c00991577dfbe19d1" + integrity sha512-sRu1kpcO9yLtYxBKvqfTeh9KzZEwO3STyX1HT+4CaDzC6HpTGYhIhPIzj9XuKU7KYDwnaeh5hcOwjy1QuJzBPA== + dependencies: + fast-deep-equal "^3.1.1" + json-schema-traverse "^1.0.0" + require-from-string "^2.0.2" + uri-js "^4.2.2" + +ansi-colors@4.1.1: + version "4.1.1" + resolved "https://registry.yarnpkg.com/ansi-colors/-/ansi-colors-4.1.1.tgz#cbb9ae256bf750af1eab344f229aa27fe94ba348" + integrity sha512-JoX0apGbHaUJBNl6yF+p6JAFYZ666/hhCGKN5t9QFjbJQKUU/g8MNbFDbvfrgKXvI1QpZplPOnwIo99lX/AAmA== + +ansi-escapes@^4.2.1: + version "4.3.2" + resolved "https://registry.yarnpkg.com/ansi-escapes/-/ansi-escapes-4.3.2.tgz#6b2291d1db7d98b6521d5f1efa42d0f3a9feb65e" + integrity sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ== + dependencies: + type-fest "^0.21.3" + +ansi-regex@^5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-5.0.1.tgz#082cb2c89c9fe8659a311a53bd6a4dc5301db304" + integrity sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ== + +ansi-styles@^3.2.1: + version "3.2.1" + resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-3.2.1.tgz#41fbb20243e50b12be0f04b8dedbf07520ce841d" + integrity sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA== + dependencies: + color-convert "^1.9.0" + +ansi-styles@^4.0.0, ansi-styles@^4.1.0: + version "4.3.0" + resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-4.3.0.tgz#edd803628ae71c04c85ae7a0906edad34b648937" + integrity sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg== + dependencies: + color-convert "^2.0.1" + +ansi-styles@^5.0.0: + version "5.2.0" + resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-5.2.0.tgz#07449690ad45777d1924ac2abb2fc8895dba836b" + integrity sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA== + +anymatch@^3.0.3, anymatch@~3.1.2: + version "3.1.3" + resolved "https://registry.yarnpkg.com/anymatch/-/anymatch-3.1.3.tgz#790c58b19ba1720a84205b57c618d5ad8524973e" + integrity sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw== + dependencies: + normalize-path "^3.0.0" + picomatch "^2.0.4" + +append-field@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/append-field/-/append-field-1.0.0.tgz#1e3440e915f0b1203d23748e78edd7b9b5b43e56" + integrity sha512-klpgFSWLW1ZEs8svjfb7g4qWY0YS5imI82dTg+QahUvJ8YqAY0P10Uk8tTyh9ZGuYEZEMaeJYCF5BFuX552hsw== + +arg@^4.1.0: + version "4.1.3" + resolved "https://registry.yarnpkg.com/arg/-/arg-4.1.3.tgz#269fc7ad5b8e42cb63c896d5666017261c144089" + integrity sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA== + +argparse@^1.0.7: + version "1.0.10" + resolved "https://registry.yarnpkg.com/argparse/-/argparse-1.0.10.tgz#bcd6791ea5ae09725e17e5ad988134cd40b3d911" + integrity sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg== + dependencies: + sprintf-js "~1.0.2" + +argparse@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/argparse/-/argparse-2.0.1.tgz#246f50f3ca78a3240f6c997e8a9bd1eac49e4b38" + integrity sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q== + +array-flatten@1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/array-flatten/-/array-flatten-1.1.1.tgz#9a5f699051b1e7073328f2a008968b64ea2955d2" + integrity sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg== + +array-union@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/array-union/-/array-union-2.1.0.tgz#b798420adbeb1de828d84acd8a2e23d3efe85e8d" + integrity sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw== + +asap@^2.0.0: + version "2.0.6" + resolved "https://registry.yarnpkg.com/asap/-/asap-2.0.6.tgz#e50347611d7e690943208bbdafebcbc2fb866d46" + integrity sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA== + +asynckit@^0.4.0: + version "0.4.0" + resolved "https://registry.yarnpkg.com/asynckit/-/asynckit-0.4.0.tgz#c79ed97f7f34cb8f2ba1bc9790bcc366474b4b79" + integrity sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q== + +axios@0.27.2: + version "0.27.2" + resolved "https://registry.yarnpkg.com/axios/-/axios-0.27.2.tgz#207658cc8621606e586c85db4b41a750e756d972" + integrity sha512-t+yRIyySRTp/wua5xEr+z1q60QmLq8ABsS5O9Me1AsE5dfKqgnCFzwiCZZ/cGNd1lq4/7akDWMxdhVlucjmnOQ== + dependencies: + follow-redirects "^1.14.9" + form-data "^4.0.0" + +babel-jest@^27.5.1: + version "27.5.1" + resolved "https://registry.yarnpkg.com/babel-jest/-/babel-jest-27.5.1.tgz#a1bf8d61928edfefd21da27eb86a695bfd691444" + integrity sha512-cdQ5dXjGRd0IBRATiQ4mZGlGlRE8kJpjPOixdNRdT+m3UcNqmYWN6rK6nvtXYfY3D76cb8s/O1Ss8ea24PIwcg== + dependencies: + "@jest/transform" "^27.5.1" + "@jest/types" "^27.5.1" + "@types/babel__core" "^7.1.14" + babel-plugin-istanbul "^6.1.1" + babel-preset-jest "^27.5.1" + chalk "^4.0.0" + graceful-fs "^4.2.9" + slash "^3.0.0" + +babel-plugin-istanbul@^6.1.1: + version "6.1.1" + resolved "https://registry.yarnpkg.com/babel-plugin-istanbul/-/babel-plugin-istanbul-6.1.1.tgz#fa88ec59232fd9b4e36dbbc540a8ec9a9b47da73" + integrity sha512-Y1IQok9821cC9onCx5otgFfRm7Lm+I+wwxOx738M/WLPZ9Q42m4IG5W0FNX8WLL2gYMZo3JkuXIH2DOpWM+qwA== + dependencies: + "@babel/helper-plugin-utils" "^7.0.0" + "@istanbuljs/load-nyc-config" "^1.0.0" + "@istanbuljs/schema" "^0.1.2" + istanbul-lib-instrument "^5.0.4" + test-exclude "^6.0.0" + +babel-plugin-jest-hoist@^27.5.1: + version "27.5.1" + resolved "https://registry.yarnpkg.com/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-27.5.1.tgz#9be98ecf28c331eb9f5df9c72d6f89deb8181c2e" + integrity sha512-50wCwD5EMNW4aRpOwtqzyZHIewTYNxLA4nhB+09d8BIssfNfzBRhkBIHiaPv1Si226TQSvp8gxAJm2iY2qs2hQ== + dependencies: + "@babel/template" "^7.3.3" + "@babel/types" "^7.3.3" + "@types/babel__core" "^7.0.0" + "@types/babel__traverse" "^7.0.6" + +babel-preset-current-node-syntax@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/babel-preset-current-node-syntax/-/babel-preset-current-node-syntax-1.0.1.tgz#b4399239b89b2a011f9ddbe3e4f401fc40cff73b" + integrity sha512-M7LQ0bxarkxQoN+vz5aJPsLBn77n8QgTFmo8WK0/44auK2xlCXrYcUxHFxgU7qW5Yzw/CjmLRK2uJzaCd7LvqQ== + dependencies: + "@babel/plugin-syntax-async-generators" "^7.8.4" + "@babel/plugin-syntax-bigint" "^7.8.3" + "@babel/plugin-syntax-class-properties" "^7.8.3" + "@babel/plugin-syntax-import-meta" "^7.8.3" + "@babel/plugin-syntax-json-strings" "^7.8.3" + "@babel/plugin-syntax-logical-assignment-operators" "^7.8.3" + "@babel/plugin-syntax-nullish-coalescing-operator" "^7.8.3" + "@babel/plugin-syntax-numeric-separator" "^7.8.3" + "@babel/plugin-syntax-object-rest-spread" "^7.8.3" + "@babel/plugin-syntax-optional-catch-binding" "^7.8.3" + "@babel/plugin-syntax-optional-chaining" "^7.8.3" + "@babel/plugin-syntax-top-level-await" "^7.8.3" + +babel-preset-jest@^27.5.1: + version "27.5.1" + resolved "https://registry.yarnpkg.com/babel-preset-jest/-/babel-preset-jest-27.5.1.tgz#91f10f58034cb7989cb4f962b69fa6eef6a6bc81" + integrity sha512-Nptf2FzlPCWYuJg41HBqXVT8ym6bXOevuCTbhxlUpjwtysGaIWFvDEjp4y+G7fl13FgOdjs7P/DmErqH7da0Ag== + dependencies: + babel-plugin-jest-hoist "^27.5.1" + babel-preset-current-node-syntax "^1.0.0" + +balanced-match@^1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.2.tgz#e83e3a7e3f300b34cb9d87f615fa0cbf357690ee" + integrity sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw== + +base64-js@^1.3.1: + version "1.5.1" + resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.5.1.tgz#1b1b440160a5bf7ad40b650f095963481903930a" + integrity sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA== + +binary-extensions@^2.0.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-2.2.0.tgz#75f502eeaf9ffde42fc98829645be4ea76bd9e2d" + integrity sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA== + +bl@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/bl/-/bl-4.1.0.tgz#451535264182bec2fbbc83a62ab98cf11d9f7b3a" + integrity sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w== + dependencies: + buffer "^5.5.0" + inherits "^2.0.4" + readable-stream "^3.4.0" + +body-parser@1.20.0: + version "1.20.0" + resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-1.20.0.tgz#3de69bd89011c11573d7bfee6a64f11b6bd27cc5" + integrity sha512-DfJ+q6EPcGKZD1QWUjSpqp+Q7bDQTsQIF4zfUAtZ6qk+H/3/QRhg9CEp39ss+/T2vw0+HaidC0ecJj/DRLIaKg== + dependencies: + bytes "3.1.2" + content-type "~1.0.4" + debug "2.6.9" + depd "2.0.0" + destroy "1.2.0" + http-errors "2.0.0" + iconv-lite "0.4.24" + on-finished "2.4.1" + qs "6.10.3" + raw-body "2.5.1" + type-is "~1.6.18" + unpipe "1.0.0" + +brace-expansion@^1.1.7: + version "1.1.11" + resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.11.tgz#3c7fcbf529d87226f3d2f52b966ff5271eb441dd" + integrity sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA== + dependencies: + balanced-match "^1.0.0" + concat-map "0.0.1" + +braces@^3.0.2, braces@~3.0.2: + version "3.0.2" + resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.2.tgz#3454e1a462ee8d599e236df336cd9ea4f8afe107" + integrity sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A== + dependencies: + fill-range "^7.0.1" + +browser-process-hrtime@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/browser-process-hrtime/-/browser-process-hrtime-1.0.0.tgz#3c9b4b7d782c8121e56f10106d84c0d0ffc94626" + integrity sha512-9o5UecI3GhkpM6DrXr69PblIuWxPKk9Y0jHBRhdocZ2y7YECBFCsHm79Pr3OyR2AvjhDkabFJaDJMYRazHgsow== + +browserslist@^4.14.5, browserslist@^4.21.3: + version "4.21.5" + resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.21.5.tgz#75c5dae60063ee641f977e00edd3cfb2fb7af6a7" + integrity sha512-tUkiguQGW7S3IhB7N+c2MV/HZPSCPAAiYBZXLsBhFB/PCy6ZKKsZrmBayHV9fdGV/ARIfJ14NkxKzRDjvp7L6w== + dependencies: + caniuse-lite "^1.0.30001449" + electron-to-chromium "^1.4.284" + node-releases "^2.0.8" + update-browserslist-db "^1.0.10" + +bs-logger@0.x: + version "0.2.6" + resolved "https://registry.yarnpkg.com/bs-logger/-/bs-logger-0.2.6.tgz#eb7d365307a72cf974cc6cda76b68354ad336bd8" + integrity sha512-pd8DCoxmbgc7hyPKOvxtqNcjYoOsABPQdcCUjGp3d42VR2CX1ORhk2A87oqqu5R1kk+76nsxZupkmyd+MVtCog== + dependencies: + fast-json-stable-stringify "2.x" + +bser@2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/bser/-/bser-2.1.1.tgz#e6787da20ece9d07998533cfd9de6f5c38f4bc05" + integrity sha512-gQxTNE/GAfIIrmHLUE3oJyp5FO6HRBfhjnw4/wMmA63ZGDJnWBmgY/lyQBpnDUkGmAhbSe39tx2d/iTOAfglwQ== + dependencies: + node-int64 "^0.4.0" + +buffer-from@^1.0.0: + version "1.1.2" + resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.1.2.tgz#2b146a6fd72e80b4f55d255f35ed59a3a9a41bd5" + integrity sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ== + +buffer@^5.5.0: + version "5.7.1" + resolved "https://registry.yarnpkg.com/buffer/-/buffer-5.7.1.tgz#ba62e7c13133053582197160851a8f648e99eed0" + integrity sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ== + dependencies: + base64-js "^1.3.1" + ieee754 "^1.1.13" + +busboy@^1.0.0: + version "1.6.0" + resolved "https://registry.yarnpkg.com/busboy/-/busboy-1.6.0.tgz#966ea36a9502e43cdb9146962523b92f531f6893" + integrity sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA== + dependencies: + streamsearch "^1.1.0" + +bytes@3.1.2: + version "3.1.2" + resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.1.2.tgz#8b0beeb98605adf1b128fa4386403c009e0221a5" + integrity sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg== + +call-bind@^1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/call-bind/-/call-bind-1.0.2.tgz#b1d4e89e688119c3c9a903ad30abb2f6a919be3c" + integrity sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA== + dependencies: + function-bind "^1.1.1" + get-intrinsic "^1.0.2" + +callsites@^3.0.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/callsites/-/callsites-3.1.0.tgz#b3630abd8943432f54b3f0519238e33cd7df2f73" + integrity sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ== + +camelcase@^5.3.1: + version "5.3.1" + resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-5.3.1.tgz#e3c9b31569e106811df242f715725a1f4c494320" + integrity sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg== + +camelcase@^6.2.0: + version "6.3.0" + resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-6.3.0.tgz#5685b95eb209ac9c0c177467778c9c84df58ba9a" + integrity sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA== + +caniuse-lite@^1.0.30001449: + version "1.0.30001486" + resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001486.tgz#56a08885228edf62cbe1ac8980f2b5dae159997e" + integrity sha512-uv7/gXuHi10Whlj0pp5q/tsK/32J2QSqVRKQhs2j8VsDCjgyruAh/eEXHF822VqO9yT6iZKw3nRwZRSPBE9OQg== + +chalk@3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/chalk/-/chalk-3.0.0.tgz#3f73c2bf526591f574cc492c51e2456349f844e4" + integrity sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg== + dependencies: + ansi-styles "^4.1.0" + supports-color "^7.1.0" + +chalk@^2.0.0: + version "2.4.2" + resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.4.2.tgz#cd42541677a54333cf541a49108c1432b44c9424" + integrity sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ== + dependencies: + ansi-styles "^3.2.1" + escape-string-regexp "^1.0.5" + supports-color "^5.3.0" + +chalk@^4.0.0, chalk@^4.1.0, chalk@^4.1.1, chalk@^4.1.2: + version "4.1.2" + resolved "https://registry.yarnpkg.com/chalk/-/chalk-4.1.2.tgz#aac4e2b7734a740867aeb16bf02aad556a1e7a01" + integrity sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA== + dependencies: + ansi-styles "^4.1.0" + supports-color "^7.1.0" + +char-regex@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/char-regex/-/char-regex-1.0.2.tgz#d744358226217f981ed58f479b1d6bcc29545dcf" + integrity sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw== + +chardet@^0.7.0: + version "0.7.0" + resolved "https://registry.yarnpkg.com/chardet/-/chardet-0.7.0.tgz#90094849f0937f2eedc2425d0d28a9e5f0cbad9e" + integrity sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA== + +chokidar@3.5.3, chokidar@^3.5.3: + version "3.5.3" + resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.5.3.tgz#1cf37c8707b932bd1af1ae22c0432e2acd1903bd" + integrity sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw== + dependencies: + anymatch "~3.1.2" + braces "~3.0.2" + glob-parent "~5.1.2" + is-binary-path "~2.1.0" + is-glob "~4.0.1" + normalize-path "~3.0.0" + readdirp "~3.6.0" + optionalDependencies: + fsevents "~2.3.2" + +chrome-trace-event@^1.0.2: + version "1.0.3" + resolved "https://registry.yarnpkg.com/chrome-trace-event/-/chrome-trace-event-1.0.3.tgz#1015eced4741e15d06664a957dbbf50d041e26ac" + integrity sha512-p3KULyQg4S7NIHixdwbGX+nFHkoBiA4YQmyWtjb8XngSKV124nJmRysgAeujbUVb15vh+RvFUfCPqU7rXk+hZg== + +ci-info@^3.2.0: + version "3.8.0" + resolved "https://registry.yarnpkg.com/ci-info/-/ci-info-3.8.0.tgz#81408265a5380c929f0bc665d62256628ce9ef91" + integrity sha512-eXTggHWSooYhq49F2opQhuHWgzucfF2YgODK4e1566GQs5BIfP30B0oenwBJHfWxAs2fyPB1s7Mg949zLf61Yw== + +cjs-module-lexer@^1.0.0: + version "1.2.2" + resolved "https://registry.yarnpkg.com/cjs-module-lexer/-/cjs-module-lexer-1.2.2.tgz#9f84ba3244a512f3a54e5277e8eef4c489864e40" + integrity sha512-cOU9usZw8/dXIXKtwa8pM0OTJQuJkxMN6w30csNRUerHfeQ5R6U3kkU/FtJeIf3M202OHfY2U8ccInBG7/xogA== + +cli-cursor@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/cli-cursor/-/cli-cursor-3.1.0.tgz#264305a7ae490d1d03bf0c9ba7c925d1753af307" + integrity sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw== + dependencies: + restore-cursor "^3.1.0" + +cli-spinners@^2.5.0: + version "2.9.0" + resolved "https://registry.yarnpkg.com/cli-spinners/-/cli-spinners-2.9.0.tgz#5881d0ad96381e117bbe07ad91f2008fe6ffd8db" + integrity sha512-4/aL9X3Wh0yiMQlE+eeRhWP6vclO3QRtw1JHKIT0FFUs5FjpFmESqtMvYZ0+lbzBw900b95mS0hohy+qn2VK/g== + +cli-table3@0.6.2: + version "0.6.2" + resolved "https://registry.yarnpkg.com/cli-table3/-/cli-table3-0.6.2.tgz#aaf5df9d8b5bf12634dc8b3040806a0c07120d2a" + integrity sha512-QyavHCaIC80cMivimWu4aWHilIpiDpfm3hGmqAmXVL1UsnbLuBSMd21hTX6VY4ZSDSM73ESLeF8TOYId3rBTbw== + dependencies: + string-width "^4.2.0" + optionalDependencies: + "@colors/colors" "1.5.0" + +cli-width@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/cli-width/-/cli-width-3.0.0.tgz#a2f48437a2caa9a22436e794bf071ec9e61cedf6" + integrity sha512-FxqpkPPwu1HjuN93Omfm4h8uIanXofW0RxVEW3k5RKx+mJJYSthzNhp32Kzxxy3YAEZ/Dc/EWN1vZRY0+kOhbw== + +cliui@^7.0.2: + version "7.0.4" + resolved "https://registry.yarnpkg.com/cliui/-/cliui-7.0.4.tgz#a0265ee655476fc807aea9df3df8df7783808b4f" + integrity sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ== + dependencies: + string-width "^4.2.0" + strip-ansi "^6.0.0" + wrap-ansi "^7.0.0" + +clone@^1.0.2: + version "1.0.4" + resolved "https://registry.yarnpkg.com/clone/-/clone-1.0.4.tgz#da309cc263df15994c688ca902179ca3c7cd7c7e" + integrity sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg== + +co@^4.6.0: + version "4.6.0" + resolved "https://registry.yarnpkg.com/co/-/co-4.6.0.tgz#6ea6bdf3d853ae54ccb8e47bfa0bf3f9031fb184" + integrity sha512-QVb0dM5HvG+uaxitm8wONl7jltx8dqhfU33DcqtOZcLSVIKSDDLDi7+0LbAKiyI8hD9u42m2YxXSkMGWThaecQ== + +collect-v8-coverage@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/collect-v8-coverage/-/collect-v8-coverage-1.0.1.tgz#cc2c8e94fc18bbdffe64d6534570c8a673b27f59" + integrity sha512-iBPtljfCNcTKNAto0KEtDfZ3qzjJvqE3aTGZsbhjSBlorqpXJlaWWtPO35D+ZImoC3KWejX64o+yPGxhWSTzfg== + +color-convert@^1.9.0: + version "1.9.3" + resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-1.9.3.tgz#bb71850690e1f136567de629d2d5471deda4c1e8" + integrity sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg== + dependencies: + color-name "1.1.3" + +color-convert@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-2.0.1.tgz#72d3a68d598c9bdb3af2ad1e84f21d896abd4de3" + integrity sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ== + dependencies: + color-name "~1.1.4" + +color-name@1.1.3: + version "1.1.3" + resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.3.tgz#a7d0558bd89c42f795dd42328f740831ca53bc25" + integrity sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw== + +color-name@~1.1.4: + version "1.1.4" + resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.4.tgz#c2a09a87acbde69543de6f63fa3995c826c536a2" + integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA== + +combined-stream@^1.0.8: + version "1.0.8" + resolved "https://registry.yarnpkg.com/combined-stream/-/combined-stream-1.0.8.tgz#c3d45a8b34fd730631a110a8a2520682b31d5a7f" + integrity sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg== + dependencies: + delayed-stream "~1.0.0" + +commander@4.1.1: + version "4.1.1" + resolved "https://registry.yarnpkg.com/commander/-/commander-4.1.1.tgz#9fd602bd936294e9e9ef46a3f4d6964044b18068" + integrity sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA== + +commander@^2.20.0: + version "2.20.3" + resolved "https://registry.yarnpkg.com/commander/-/commander-2.20.3.tgz#fd485e84c03eb4881c20722ba48035e8531aeb33" + integrity sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ== + +component-emitter@^1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/component-emitter/-/component-emitter-1.3.0.tgz#16e4070fba8ae29b679f2215853ee181ab2eabc0" + integrity sha512-Rd3se6QB+sO1TwqZjscQrurpEPIfO0/yYnSin6Q/rD3mOutHvUrCAhJub3r90uNb+SESBuE0QYoB90YdfatsRg== + +concat-map@0.0.1: + version "0.0.1" + resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" + integrity sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg== + +concat-stream@^1.5.2: + version "1.6.2" + resolved "https://registry.yarnpkg.com/concat-stream/-/concat-stream-1.6.2.tgz#904bdf194cd3122fc675c77fc4ac3d4ff0fd1a34" + integrity sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw== + dependencies: + buffer-from "^1.0.0" + inherits "^2.0.3" + readable-stream "^2.2.2" + typedarray "^0.0.6" + +consola@^2.15.0: + version "2.15.3" + resolved "https://registry.yarnpkg.com/consola/-/consola-2.15.3.tgz#2e11f98d6a4be71ff72e0bdf07bd23e12cb61550" + integrity sha512-9vAdYbHj6x2fLKC4+oPH0kFzY/orMZyG2Aj+kNylHxKGJ/Ed4dpNyAQYwJOdqO4zdM7XpVHmyejQDcQHrnuXbw== + +content-disposition@0.5.4: + version "0.5.4" + resolved "https://registry.yarnpkg.com/content-disposition/-/content-disposition-0.5.4.tgz#8b82b4efac82512a02bb0b1dcec9d2c5e8eb5bfe" + integrity sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ== + dependencies: + safe-buffer "5.2.1" + +content-type@~1.0.4: + version "1.0.5" + resolved "https://registry.yarnpkg.com/content-type/-/content-type-1.0.5.tgz#8b773162656d1d1086784c8f23a54ce6d73d7918" + integrity sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA== + +convert-source-map@^1.4.0, convert-source-map@^1.6.0, convert-source-map@^1.7.0: + version "1.9.0" + resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-1.9.0.tgz#7faae62353fb4213366d0ca98358d22e8368b05f" + integrity sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A== + +cookie-signature@1.0.6: + version "1.0.6" + resolved "https://registry.yarnpkg.com/cookie-signature/-/cookie-signature-1.0.6.tgz#e303a882b342cc3ee8ca513a79999734dab3ae2c" + integrity sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ== + +cookie@0.5.0: + version "0.5.0" + resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.5.0.tgz#d1f5d71adec6558c58f389987c366aa47e994f8b" + integrity sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw== + +cookiejar@^2.1.4: + version "2.1.4" + resolved "https://registry.yarnpkg.com/cookiejar/-/cookiejar-2.1.4.tgz#ee669c1fea2cf42dc31585469d193fef0d65771b" + integrity sha512-LDx6oHrK+PhzLKJU9j5S7/Y3jM/mUHvD/DeI1WQmJn652iPC5Y4TBzC9l+5OMOXlyTTA+SmVUPm0HQUwpD5Jqw== + +core-util-is@~1.0.0: + version "1.0.3" + resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.3.tgz#a6042d3634c2b27e9328f837b965fac83808db85" + integrity sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ== + +cors@2.8.5: + version "2.8.5" + resolved "https://registry.yarnpkg.com/cors/-/cors-2.8.5.tgz#eac11da51592dd86b9f06f6e7ac293b3df875d29" + integrity sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g== + dependencies: + object-assign "^4" + vary "^1" + +cosmiconfig@^7.0.1: + version "7.1.0" + resolved "https://registry.yarnpkg.com/cosmiconfig/-/cosmiconfig-7.1.0.tgz#1443b9afa596b670082ea46cbd8f6a62b84635f6" + integrity sha512-AdmX6xUzdNASswsFtmwSt7Vj8po9IuqXm0UXz7QKPuEUmPB4XyjGfaAr2PSuELMwkRMVH1EpIkX5bTZGRB3eCA== + dependencies: + "@types/parse-json" "^4.0.0" + import-fresh "^3.2.1" + parse-json "^5.0.0" + path-type "^4.0.0" + yaml "^1.10.0" + +create-require@^1.1.0: + version "1.1.1" + resolved "https://registry.yarnpkg.com/create-require/-/create-require-1.1.1.tgz#c1d7e8f1e5f6cfc9ff65f9cd352d37348756c333" + integrity sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ== + +cross-spawn@^7.0.0, cross-spawn@^7.0.2, cross-spawn@^7.0.3: + version "7.0.3" + resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.3.tgz#f73a85b9d5d41d045551c177e2882d4ac85728a6" + integrity sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w== + dependencies: + path-key "^3.1.0" + shebang-command "^2.0.0" + which "^2.0.1" + +cssom@^0.4.4: + version "0.4.4" + resolved "https://registry.yarnpkg.com/cssom/-/cssom-0.4.4.tgz#5a66cf93d2d0b661d80bf6a44fb65f5c2e4e0a10" + integrity sha512-p3pvU7r1MyyqbTk+WbNJIgJjG2VmTIaB10rI93LzVPrmDJKkzKYMtxxyAvQXR/NS6otuzveI7+7BBq3SjBS2mw== + +cssom@~0.3.6: + version "0.3.8" + resolved "https://registry.yarnpkg.com/cssom/-/cssom-0.3.8.tgz#9f1276f5b2b463f2114d3f2c75250af8c1a36f4a" + integrity sha512-b0tGHbfegbhPJpxpiBPU2sCkigAqtM9O121le6bbOlgyV+NyGyCmVfJ6QW9eRjz8CpNfWEOYBIMIGRYkLwsIYg== + +cssstyle@^2.3.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/cssstyle/-/cssstyle-2.3.0.tgz#ff665a0ddbdc31864b09647f34163443d90b0852" + integrity sha512-AZL67abkUzIuvcHqk7c09cezpGNcxUxU4Ioi/05xHk4DQeTkWmGYftIE6ctU6AEt+Gn4n1lDStOtj7FKycP71A== + dependencies: + cssom "~0.3.6" + +data-urls@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/data-urls/-/data-urls-2.0.0.tgz#156485a72963a970f5d5821aaf642bef2bf2db9b" + integrity sha512-X5eWTSXO/BJmpdIKCRuKUgSCgAN0OwliVK3yPKbwIWU1Tdw5BRajxlzMidvh+gwko9AfQ9zIj52pzF91Q3YAvQ== + dependencies: + abab "^2.0.3" + whatwg-mimetype "^2.3.0" + whatwg-url "^8.0.0" + +debug@2.6.9: + version "2.6.9" + resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f" + integrity sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA== + dependencies: + ms "2.0.0" + +debug@4, debug@^4.1.0, debug@^4.1.1, debug@^4.3.2, debug@^4.3.4: + version "4.3.4" + resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.4.tgz#1319f6579357f2338d3337d2cdd4914bb5dcc865" + integrity sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ== + dependencies: + ms "2.1.2" + +decimal.js@^10.2.1: + version "10.4.3" + resolved "https://registry.yarnpkg.com/decimal.js/-/decimal.js-10.4.3.tgz#1044092884d245d1b7f65725fa4ad4c6f781cc23" + integrity sha512-VBBaLc1MgL5XpzgIP7ny5Z6Nx3UrRkIViUkPUdtl9aya5amy3De1gsUUSB1g3+3sExYNjCAsAznmukyxCb1GRA== + +dedent@^0.7.0: + version "0.7.0" + resolved "https://registry.yarnpkg.com/dedent/-/dedent-0.7.0.tgz#2495ddbaf6eb874abb0e1be9df22d2e5a544326c" + integrity sha512-Q6fKUPqnAHAyhiUgFU7BUzLiv0kd8saH9al7tnu5Q/okj6dnupxyTgFIBjVzJATdfIAm9NAsvXNzjaKa+bxVyA== + +deep-is@^0.1.3, deep-is@~0.1.3: + version "0.1.4" + resolved "https://registry.yarnpkg.com/deep-is/-/deep-is-0.1.4.tgz#a6f2dce612fadd2ef1f519b73551f17e85199831" + integrity sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ== + +deepmerge@^4.2.2: + version "4.3.1" + resolved "https://registry.yarnpkg.com/deepmerge/-/deepmerge-4.3.1.tgz#44b5f2147cd3b00d4b56137685966f26fd25dd4a" + integrity sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A== + +defaults@^1.0.3: + version "1.0.4" + resolved "https://registry.yarnpkg.com/defaults/-/defaults-1.0.4.tgz#b0b02062c1e2aa62ff5d9528f0f98baa90978d7a" + integrity sha512-eFuaLoy/Rxalv2kr+lqMlUnrDWV+3j4pljOIJgLIhI058IQfWJ7vXhyEIHu+HtC738klGALYxOKDO0bQP3tg8A== + dependencies: + clone "^1.0.2" + +delayed-stream@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619" + integrity sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ== + +depd@2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/depd/-/depd-2.0.0.tgz#b696163cc757560d09cf22cc8fad1571b79e76df" + integrity sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw== + +destroy@1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/destroy/-/destroy-1.2.0.tgz#4803735509ad8be552934c67df614f94e66fa015" + integrity sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg== + +detect-newline@^3.0.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/detect-newline/-/detect-newline-3.1.0.tgz#576f5dfc63ae1a192ff192d8ad3af6308991b651" + integrity sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA== + +dezalgo@^1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/dezalgo/-/dezalgo-1.0.4.tgz#751235260469084c132157dfa857f386d4c33d81" + integrity sha512-rXSP0bf+5n0Qonsb+SVVfNfIsimO4HEtmnIpPHY8Q1UCzKlQrDMfdobr8nJOOsRgWCyMRqeSBQzmWUMq7zvVig== + dependencies: + asap "^2.0.0" + wrappy "1" + +diff-sequences@^27.5.1: + version "27.5.1" + resolved "https://registry.yarnpkg.com/diff-sequences/-/diff-sequences-27.5.1.tgz#eaecc0d327fd68c8d9672a1e64ab8dccb2ef5327" + integrity sha512-k1gCAXAsNgLwEL+Y8Wvl+M6oEFj5bgazfZULpS5CneoPPXRaCCW7dm+q21Ky2VEE5X+VeRDBVg1Pcvvsr4TtNQ== + +diff@^4.0.1: + version "4.0.2" + resolved "https://registry.yarnpkg.com/diff/-/diff-4.0.2.tgz#60f3aecb89d5fae520c11aa19efc2bb982aade7d" + integrity sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A== + +dir-glob@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/dir-glob/-/dir-glob-3.0.1.tgz#56dbf73d992a4a93ba1584f4534063fd2e41717f" + integrity sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA== + dependencies: + path-type "^4.0.0" + +doctrine@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/doctrine/-/doctrine-3.0.0.tgz#addebead72a6574db783639dc87a121773973961" + integrity sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w== + dependencies: + esutils "^2.0.2" + +domexception@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/domexception/-/domexception-2.0.1.tgz#fb44aefba793e1574b0af6aed2801d057529f304" + integrity sha512-yxJ2mFy/sibVQlu5qHjOkf9J3K6zgmCxgJ94u2EdvDOV09H+32LtRswEcUsmUWN72pVLOEnTSRaIVVzVQgS0dg== + dependencies: + webidl-conversions "^5.0.0" + +ee-first@1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/ee-first/-/ee-first-1.1.1.tgz#590c61156b0ae2f4f0255732a158b266bc56b21d" + integrity sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow== + +electron-to-chromium@^1.4.284: + version "1.4.385" + resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.4.385.tgz#1afd8d6280d510145148777b899ff481c65531ff" + integrity sha512-L9zlje9bIw0h+CwPQumiuVlfMcV4boxRjFIWDcLfFqTZNbkwOExBzfmswytHawObQX4OUhtNv8gIiB21kOurIg== + +emittery@^0.8.1: + version "0.8.1" + resolved "https://registry.yarnpkg.com/emittery/-/emittery-0.8.1.tgz#bb23cc86d03b30aa75a7f734819dee2e1ba70860" + integrity sha512-uDfvUjVrfGJJhymx/kz6prltenw1u7WrCg1oa94zYY8xxVpLLUu045LAT0dhDZdXG58/EpPL/5kA180fQ/qudg== + +emoji-regex@^8.0.0: + version "8.0.0" + resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-8.0.0.tgz#e818fd69ce5ccfcb404594f842963bf53164cc37" + integrity sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A== + +encodeurl@~1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/encodeurl/-/encodeurl-1.0.2.tgz#ad3ff4c86ec2d029322f5a02c3a9a606c95b3f59" + integrity sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w== + +end-of-stream@^1.1.0: + version "1.4.4" + resolved "https://registry.yarnpkg.com/end-of-stream/-/end-of-stream-1.4.4.tgz#5ae64a5f45057baf3626ec14da0ca5e4b2431eb0" + integrity sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q== + dependencies: + once "^1.4.0" + +enhanced-resolve@^5.0.0, enhanced-resolve@^5.7.0, enhanced-resolve@^5.9.3: + version "5.13.0" + resolved "https://registry.yarnpkg.com/enhanced-resolve/-/enhanced-resolve-5.13.0.tgz#26d1ecc448c02de997133217b5c1053f34a0a275" + integrity sha512-eyV8f0y1+bzyfh8xAwW/WTSZpLbjhqc4ne9eGSH4Zo2ejdyiNG9pU6mf9DG8a7+Auk6MFTlNOT4Y2y/9k8GKVg== + dependencies: + graceful-fs "^4.2.4" + tapable "^2.2.0" + +error-ex@^1.3.1: + version "1.3.2" + resolved "https://registry.yarnpkg.com/error-ex/-/error-ex-1.3.2.tgz#b4ac40648107fdcdcfae242f428bea8a14d4f1bf" + integrity sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g== + dependencies: + is-arrayish "^0.2.1" + +es-module-lexer@^0.9.0: + version "0.9.3" + resolved "https://registry.yarnpkg.com/es-module-lexer/-/es-module-lexer-0.9.3.tgz#6f13db00cc38417137daf74366f535c8eb438f19" + integrity sha512-1HQ2M2sPtxwnvOvT1ZClHyQDiggdNjURWpY2we6aMKCQiUVxTmVs2UYPLIrD84sS+kMdUwfBSylbJPwNnBrnHQ== + +escalade@^3.1.1: + version "3.1.1" + resolved "https://registry.yarnpkg.com/escalade/-/escalade-3.1.1.tgz#d8cfdc7000965c5a0174b4a82eaa5c0552742e40" + integrity sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw== + +escape-html@~1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/escape-html/-/escape-html-1.0.3.tgz#0258eae4d3d0c0974de1c169188ef0051d1d1988" + integrity sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow== + +escape-string-regexp@^1.0.5: + version "1.0.5" + resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4" + integrity sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg== + +escape-string-regexp@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz#a30304e99daa32e23b2fd20f51babd07cffca344" + integrity sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w== + +escape-string-regexp@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz#14ba83a5d373e3d311e5afca29cf5bfad965bf34" + integrity sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA== + +escodegen@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/escodegen/-/escodegen-2.0.0.tgz#5e32b12833e8aa8fa35e1bf0befa89380484c7dd" + integrity sha512-mmHKys/C8BFUGI+MAWNcSYoORYLMdPzjrknd2Vc+bUsjN5bXcr8EhrNB+UTqfL1y3I9c4fw2ihgtMPQLBRiQxw== + dependencies: + esprima "^4.0.1" + estraverse "^5.2.0" + esutils "^2.0.2" + optionator "^0.8.1" + optionalDependencies: + source-map "~0.6.1" + +eslint-config-prettier@^8.3.0: + version "8.8.0" + resolved "https://registry.yarnpkg.com/eslint-config-prettier/-/eslint-config-prettier-8.8.0.tgz#bfda738d412adc917fd7b038857110efe98c9348" + integrity sha512-wLbQiFre3tdGgpDv67NQKnJuTlcUVYHas3k+DZCc2U2BadthoEY4B7hLPvAxaqdyOGCzuLfii2fqGph10va7oA== + +eslint-plugin-prettier@^4.0.0: + version "4.2.1" + resolved "https://registry.yarnpkg.com/eslint-plugin-prettier/-/eslint-plugin-prettier-4.2.1.tgz#651cbb88b1dab98bfd42f017a12fa6b2d993f94b" + integrity sha512-f/0rXLXUt0oFYs8ra4w49wYZBG5GKZpAYsJSm6rnYL5uVDjd+zowwMwVZHnAjf4edNrKpCDYfXDgmRE/Ak7QyQ== + dependencies: + prettier-linter-helpers "^1.0.0" + +eslint-scope@5.1.1, eslint-scope@^5.1.1: + version "5.1.1" + resolved "https://registry.yarnpkg.com/eslint-scope/-/eslint-scope-5.1.1.tgz#e786e59a66cb92b3f6c1fb0d508aab174848f48c" + integrity sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw== + dependencies: + esrecurse "^4.3.0" + estraverse "^4.1.1" + +eslint-scope@^7.2.0: + version "7.2.0" + resolved "https://registry.yarnpkg.com/eslint-scope/-/eslint-scope-7.2.0.tgz#f21ebdafda02352f103634b96dd47d9f81ca117b" + integrity sha512-DYj5deGlHBfMt15J7rdtyKNq/Nqlv5KfU4iodrQ019XESsRnwXH9KAE0y3cwtUHDo2ob7CypAnCqefh6vioWRw== + dependencies: + esrecurse "^4.3.0" + estraverse "^5.2.0" + +eslint-visitor-keys@^3.3.0, eslint-visitor-keys@^3.4.1: + version "3.4.1" + resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-3.4.1.tgz#c22c48f48942d08ca824cc526211ae400478a994" + integrity sha512-pZnmmLwYzf+kWaM/Qgrvpen51upAktaaiI01nsJD/Yr3lMOdNtq0cxkrrg16w64VtisN6okbs7Q8AfGqj4c9fA== + +eslint@^8.0.1: + version "8.40.0" + resolved "https://registry.yarnpkg.com/eslint/-/eslint-8.40.0.tgz#a564cd0099f38542c4e9a2f630fa45bf33bc42a4" + integrity sha512-bvR+TsP9EHL3TqNtj9sCNJVAFK3fBN8Q7g5waghxyRsPLIMwL73XSKnZFK0hk/O2ANC+iAoq6PWMQ+IfBAJIiQ== + dependencies: + "@eslint-community/eslint-utils" "^4.2.0" + "@eslint-community/regexpp" "^4.4.0" + "@eslint/eslintrc" "^2.0.3" + "@eslint/js" "8.40.0" + "@humanwhocodes/config-array" "^0.11.8" + "@humanwhocodes/module-importer" "^1.0.1" + "@nodelib/fs.walk" "^1.2.8" + ajv "^6.10.0" + chalk "^4.0.0" + cross-spawn "^7.0.2" + debug "^4.3.2" + doctrine "^3.0.0" + escape-string-regexp "^4.0.0" + eslint-scope "^7.2.0" + eslint-visitor-keys "^3.4.1" + espree "^9.5.2" + esquery "^1.4.2" + esutils "^2.0.2" + fast-deep-equal "^3.1.3" + file-entry-cache "^6.0.1" + find-up "^5.0.0" + glob-parent "^6.0.2" + globals "^13.19.0" + grapheme-splitter "^1.0.4" + ignore "^5.2.0" + import-fresh "^3.0.0" + imurmurhash "^0.1.4" + is-glob "^4.0.0" + is-path-inside "^3.0.3" + js-sdsl "^4.1.4" + js-yaml "^4.1.0" + json-stable-stringify-without-jsonify "^1.0.1" + levn "^0.4.1" + lodash.merge "^4.6.2" + minimatch "^3.1.2" + natural-compare "^1.4.0" + optionator "^0.9.1" + strip-ansi "^6.0.1" + strip-json-comments "^3.1.0" + text-table "^0.2.0" + +espree@^9.5.2: + version "9.5.2" + resolved "https://registry.yarnpkg.com/espree/-/espree-9.5.2.tgz#e994e7dc33a082a7a82dceaf12883a829353215b" + integrity sha512-7OASN1Wma5fum5SrNhFMAMJxOUAbhyfQ8dQ//PJaJbNw0URTPWqIghHWt1MmAANKhHZIYOHruW4Kw4ruUWOdGw== + dependencies: + acorn "^8.8.0" + acorn-jsx "^5.3.2" + eslint-visitor-keys "^3.4.1" + +esprima@^4.0.0, esprima@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/esprima/-/esprima-4.0.1.tgz#13b04cdb3e6c5d19df91ab6987a8695619b0aa71" + integrity sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A== + +esquery@^1.4.2: + version "1.5.0" + resolved "https://registry.yarnpkg.com/esquery/-/esquery-1.5.0.tgz#6ce17738de8577694edd7361c57182ac8cb0db0b" + integrity sha512-YQLXUplAwJgCydQ78IMJywZCceoqk1oH01OERdSAJc/7U2AylwjhSCLDEtqwg811idIS/9fIU5GjG73IgjKMVg== + dependencies: + estraverse "^5.1.0" + +esrecurse@^4.3.0: + version "4.3.0" + resolved "https://registry.yarnpkg.com/esrecurse/-/esrecurse-4.3.0.tgz#7ad7964d679abb28bee72cec63758b1c5d2c9921" + integrity sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag== + dependencies: + estraverse "^5.2.0" + +estraverse@^4.1.1: + version "4.3.0" + resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-4.3.0.tgz#398ad3f3c5a24948be7725e83d11a7de28cdbd1d" + integrity sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw== + +estraverse@^5.1.0, estraverse@^5.2.0: + version "5.3.0" + resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-5.3.0.tgz#2eea5290702f26ab8fe5370370ff86c965d21123" + integrity sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA== + +esutils@^2.0.2: + version "2.0.3" + resolved "https://registry.yarnpkg.com/esutils/-/esutils-2.0.3.tgz#74d2eb4de0b8da1293711910d50775b9b710ef64" + integrity sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g== + +etag@~1.8.1: + version "1.8.1" + resolved "https://registry.yarnpkg.com/etag/-/etag-1.8.1.tgz#41ae2eeb65efa62268aebfea83ac7d79299b0887" + integrity sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg== + +events@^3.2.0: + version "3.3.0" + resolved "https://registry.yarnpkg.com/events/-/events-3.3.0.tgz#31a95ad0a924e2d2c419a813aeb2c4e878ea7400" + integrity sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q== + +execa@^4.0.2: + version "4.1.0" + resolved "https://registry.yarnpkg.com/execa/-/execa-4.1.0.tgz#4e5491ad1572f2f17a77d388c6c857135b22847a" + integrity sha512-j5W0//W7f8UxAn8hXVnwG8tLwdiUy4FJLcSupCg6maBYZDpyBvTApK7KyuI4bKj8KOh1r2YH+6ucuYtJv1bTZA== + dependencies: + cross-spawn "^7.0.0" + get-stream "^5.0.0" + human-signals "^1.1.1" + is-stream "^2.0.0" + merge-stream "^2.0.0" + npm-run-path "^4.0.0" + onetime "^5.1.0" + signal-exit "^3.0.2" + strip-final-newline "^2.0.0" + +execa@^5.0.0: + version "5.1.1" + resolved "https://registry.yarnpkg.com/execa/-/execa-5.1.1.tgz#f80ad9cbf4298f7bd1d4c9555c21e93741c411dd" + integrity sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg== + dependencies: + cross-spawn "^7.0.3" + get-stream "^6.0.0" + human-signals "^2.1.0" + is-stream "^2.0.0" + merge-stream "^2.0.0" + npm-run-path "^4.0.1" + onetime "^5.1.2" + signal-exit "^3.0.3" + strip-final-newline "^2.0.0" + +exit@^0.1.2: + version "0.1.2" + resolved "https://registry.yarnpkg.com/exit/-/exit-0.1.2.tgz#0632638f8d877cc82107d30a0fff1a17cba1cd0c" + integrity sha512-Zk/eNKV2zbjpKzrsQ+n1G6poVbErQxJ0LBOJXaKZ1EViLzH+hrLu9cdXI4zw9dBQJslwBEpbQ2P1oS7nDxs6jQ== + +expect@^27.5.1: + version "27.5.1" + resolved "https://registry.yarnpkg.com/expect/-/expect-27.5.1.tgz#83ce59f1e5bdf5f9d2b94b61d2050db48f3fef74" + integrity sha512-E1q5hSUG2AmYQwQJ041nvgpkODHQvB+RKlB4IYdru6uJsyFTRyZAP463M+1lINorwbqAmUggi6+WwkD8lCS/Dw== + dependencies: + "@jest/types" "^27.5.1" + jest-get-type "^27.5.1" + jest-matcher-utils "^27.5.1" + jest-message-util "^27.5.1" + +express@4.18.1: + version "4.18.1" + resolved "https://registry.yarnpkg.com/express/-/express-4.18.1.tgz#7797de8b9c72c857b9cd0e14a5eea80666267caf" + integrity sha512-zZBcOX9TfehHQhtupq57OF8lFZ3UZi08Y97dwFCkD8p9d/d2Y3M+ykKcwaMDEL+4qyUolgBDX6AblpR3fL212Q== + dependencies: + accepts "~1.3.8" + array-flatten "1.1.1" + body-parser "1.20.0" + content-disposition "0.5.4" + content-type "~1.0.4" + cookie "0.5.0" + cookie-signature "1.0.6" + debug "2.6.9" + depd "2.0.0" + encodeurl "~1.0.2" + escape-html "~1.0.3" + etag "~1.8.1" + finalhandler "1.2.0" + fresh "0.5.2" + http-errors "2.0.0" + merge-descriptors "1.0.1" + methods "~1.1.2" + on-finished "2.4.1" + parseurl "~1.3.3" + path-to-regexp "0.1.7" + proxy-addr "~2.0.7" + qs "6.10.3" + range-parser "~1.2.1" + safe-buffer "5.2.1" + send "0.18.0" + serve-static "1.15.0" + setprototypeof "1.2.0" + statuses "2.0.1" + type-is "~1.6.18" + utils-merge "1.0.1" + vary "~1.1.2" + +external-editor@^3.0.3: + version "3.1.0" + resolved "https://registry.yarnpkg.com/external-editor/-/external-editor-3.1.0.tgz#cb03f740befae03ea4d283caed2741a83f335495" + integrity sha512-hMQ4CX1p1izmuLYyZqLMO/qGNw10wSv9QDCPfzXfyFrOaCSSoRfqE1Kf1s5an66J5JZC62NewG+mK49jOCtQew== + dependencies: + chardet "^0.7.0" + iconv-lite "^0.4.24" + tmp "^0.0.33" + +fast-deep-equal@^3.1.1, fast-deep-equal@^3.1.3: + version "3.1.3" + resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz#3a7d56b559d6cbc3eb512325244e619a65c6c525" + integrity sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q== + +fast-diff@^1.1.2: + version "1.2.0" + resolved "https://registry.yarnpkg.com/fast-diff/-/fast-diff-1.2.0.tgz#73ee11982d86caaf7959828d519cfe927fac5f03" + integrity sha512-xJuoT5+L99XlZ8twedaRf6Ax2TgQVxvgZOYoPKqZufmJib0tL2tegPBOZb1pVNgIhlqDlA0eO0c3wBvQcmzx4w== + +fast-glob@^3.2.9: + version "3.2.12" + resolved "https://registry.yarnpkg.com/fast-glob/-/fast-glob-3.2.12.tgz#7f39ec99c2e6ab030337142da9e0c18f37afae80" + integrity sha512-DVj4CQIYYow0BlaelwK1pHl5n5cRSJfM60UA0zK891sVInoPri2Ekj7+e1CT3/3qxXenpI+nBBmQAcJPJgaj4w== + dependencies: + "@nodelib/fs.stat" "^2.0.2" + "@nodelib/fs.walk" "^1.2.3" + glob-parent "^5.1.2" + merge2 "^1.3.0" + micromatch "^4.0.4" + +fast-json-stable-stringify@2.1.0, fast-json-stable-stringify@2.x, fast-json-stable-stringify@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz#874bf69c6f404c2b5d99c481341399fd55892633" + integrity sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw== + +fast-levenshtein@^2.0.6, fast-levenshtein@~2.0.6: + version "2.0.6" + resolved "https://registry.yarnpkg.com/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz#3d8a5c66883a16a30ca8643e851f19baa7797917" + integrity sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw== + +fast-safe-stringify@2.1.1, fast-safe-stringify@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/fast-safe-stringify/-/fast-safe-stringify-2.1.1.tgz#c406a83b6e70d9e35ce3b30a81141df30aeba884" + integrity sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA== + +fastq@^1.6.0: + version "1.15.0" + resolved "https://registry.yarnpkg.com/fastq/-/fastq-1.15.0.tgz#d04d07c6a2a68fe4599fea8d2e103a937fae6b3a" + integrity sha512-wBrocU2LCXXa+lWBt8RoIRD89Fi8OdABODa/kEnyeyjS5aZO5/GNvI5sEINADqP/h8M29UHTHUb53sUu5Ihqdw== + dependencies: + reusify "^1.0.4" + +fb-watchman@^2.0.0: + version "2.0.2" + resolved "https://registry.yarnpkg.com/fb-watchman/-/fb-watchman-2.0.2.tgz#e9524ee6b5c77e9e5001af0f85f3adbb8623255c" + integrity sha512-p5161BqbuCaSnB8jIbzQHOlpgsPmK5rJVDfDKO91Axs5NC1uu3HRQm6wt9cd9/+GtQQIO53JdGXXoyDpTAsgYA== + dependencies: + bser "2.1.1" + +figures@^3.0.0: + version "3.2.0" + resolved "https://registry.yarnpkg.com/figures/-/figures-3.2.0.tgz#625c18bd293c604dc4a8ddb2febf0c88341746af" + integrity sha512-yaduQFRKLXYOGgEn6AZau90j3ggSOyiqXU0F9JZfeXYhNa+Jk4X+s45A2zg5jns87GAFa34BBm2kXw4XpNcbdg== + dependencies: + escape-string-regexp "^1.0.5" + +file-entry-cache@^6.0.1: + version "6.0.1" + resolved "https://registry.yarnpkg.com/file-entry-cache/-/file-entry-cache-6.0.1.tgz#211b2dd9659cb0394b073e7323ac3c933d522027" + integrity sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg== + dependencies: + flat-cache "^3.0.4" + +fill-range@^7.0.1: + version "7.0.1" + resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-7.0.1.tgz#1919a6a7c75fe38b2c7c77e5198535da9acdda40" + integrity sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ== + dependencies: + to-regex-range "^5.0.1" + +finalhandler@1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/finalhandler/-/finalhandler-1.2.0.tgz#7d23fe5731b207b4640e4fcd00aec1f9207a7b32" + integrity sha512-5uXcUVftlQMFnWC9qu/svkWv3GTd2PfUhK/3PLkYNAe7FbqJMt3515HaxE6eRL74GdsriiwujiawdaB1BpEISg== + dependencies: + debug "2.6.9" + encodeurl "~1.0.2" + escape-html "~1.0.3" + on-finished "2.4.1" + parseurl "~1.3.3" + statuses "2.0.1" + unpipe "~1.0.0" + +find-up@^4.0.0, find-up@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/find-up/-/find-up-4.1.0.tgz#97afe7d6cdc0bc5928584b7c8d7b16e8a9aa5d19" + integrity sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw== + dependencies: + locate-path "^5.0.0" + path-exists "^4.0.0" + +find-up@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/find-up/-/find-up-5.0.0.tgz#4c92819ecb7083561e4f4a240a86be5198f536fc" + integrity sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng== + dependencies: + locate-path "^6.0.0" + path-exists "^4.0.0" + +flat-cache@^3.0.4: + version "3.0.4" + resolved "https://registry.yarnpkg.com/flat-cache/-/flat-cache-3.0.4.tgz#61b0338302b2fe9f957dcc32fc2a87f1c3048b11" + integrity sha512-dm9s5Pw7Jc0GvMYbshN6zchCA9RgQlzzEZX3vylR9IqFfS8XciblUXOKfW6SiuJ0e13eDYZoZV5wdrev7P3Nwg== + dependencies: + flatted "^3.1.0" + rimraf "^3.0.2" + +flatted@^3.1.0: + version "3.2.7" + resolved "https://registry.yarnpkg.com/flatted/-/flatted-3.2.7.tgz#609f39207cb614b89d0765b477cb2d437fbf9787" + integrity sha512-5nqDSxl8nn5BSNxyR3n4I6eDmbolI6WT+QqR547RwxQapgjQBmtktdP+HTBb/a/zLsbzERTONyUB5pefh5TtjQ== + +follow-redirects@^1.14.9: + version "1.15.2" + resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.2.tgz#b460864144ba63f2681096f274c4e57026da2c13" + integrity sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA== + +fork-ts-checker-webpack-plugin@7.2.11: + version "7.2.11" + resolved "https://registry.yarnpkg.com/fork-ts-checker-webpack-plugin/-/fork-ts-checker-webpack-plugin-7.2.11.tgz#aff3febbc11544ba3ad0ae4d5aa4055bd15cd26d" + integrity sha512-2e5+NyTUTE1Xq4fWo7KFEQblCaIvvINQwUX3jRmEGlgCTc1Ecqw/975EfQrQ0GEraxJTnp8KB9d/c8hlCHUMJA== + dependencies: + "@babel/code-frame" "^7.16.7" + chalk "^4.1.2" + chokidar "^3.5.3" + cosmiconfig "^7.0.1" + deepmerge "^4.2.2" + fs-extra "^10.0.0" + memfs "^3.4.1" + minimatch "^3.0.4" + schema-utils "^3.1.1" + semver "^7.3.5" + tapable "^2.2.1" + +form-data@^3.0.0: + version "3.0.1" + resolved "https://registry.yarnpkg.com/form-data/-/form-data-3.0.1.tgz#ebd53791b78356a99af9a300d4282c4d5eb9755f" + integrity sha512-RHkBKtLWUVwd7SqRIvCZMEvAMoGUp0XU+seQiZejj0COz3RI3hWP4sCv3gZWWLjJTd7rGwcsF5eKZGii0r/hbg== + dependencies: + asynckit "^0.4.0" + combined-stream "^1.0.8" + mime-types "^2.1.12" + +form-data@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/form-data/-/form-data-4.0.0.tgz#93919daeaf361ee529584b9b31664dc12c9fa452" + integrity sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww== + dependencies: + asynckit "^0.4.0" + combined-stream "^1.0.8" + mime-types "^2.1.12" + +formidable@^2.1.2: + version "2.1.2" + resolved "https://registry.yarnpkg.com/formidable/-/formidable-2.1.2.tgz#fa973a2bec150e4ce7cac15589d7a25fc30ebd89" + integrity sha512-CM3GuJ57US06mlpQ47YcunuUZ9jpm8Vx+P2CGt2j7HpgkKZO/DJYQ0Bobim8G6PFQmK5lOqOOdUXboU+h73A4g== + dependencies: + dezalgo "^1.0.4" + hexoid "^1.0.0" + once "^1.4.0" + qs "^6.11.0" + +forwarded@0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/forwarded/-/forwarded-0.2.0.tgz#2269936428aad4c15c7ebe9779a84bf0b2a81811" + integrity sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow== + +fresh@0.5.2: + version "0.5.2" + resolved "https://registry.yarnpkg.com/fresh/-/fresh-0.5.2.tgz#3d8cadd90d976569fa835ab1f8e4b23a105605a7" + integrity sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q== + +fs-extra@10.1.0, fs-extra@^10.0.0: + version "10.1.0" + resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-10.1.0.tgz#02873cfbc4084dde127eaa5f9905eef2325d1abf" + integrity sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ== + dependencies: + graceful-fs "^4.2.0" + jsonfile "^6.0.1" + universalify "^2.0.0" + +fs-monkey@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/fs-monkey/-/fs-monkey-1.0.3.tgz#ae3ac92d53bb328efe0e9a1d9541f6ad8d48e2d3" + integrity sha512-cybjIfiiE+pTWicSCLFHSrXZ6EilF30oh91FDP9S2B051prEa7QWfrVTQm10/dDpswBDXZugPa1Ogu8Yh+HV0Q== + +fs.realpath@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f" + integrity sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw== + +fsevents@^2.3.2, fsevents@~2.3.2: + version "2.3.2" + resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.2.tgz#8a526f78b8fdf4623b709e0b975c52c24c02fd1a" + integrity sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA== + +function-bind@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.1.tgz#a56899d3ea3c9bab874bb9773b7c5ede92f4895d" + integrity sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A== + +gensync@^1.0.0-beta.2: + version "1.0.0-beta.2" + resolved "https://registry.yarnpkg.com/gensync/-/gensync-1.0.0-beta.2.tgz#32a6ee76c3d7f52d46b2b1ae5d93fea8580a25e0" + integrity sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg== + +get-caller-file@^2.0.5: + version "2.0.5" + resolved "https://registry.yarnpkg.com/get-caller-file/-/get-caller-file-2.0.5.tgz#4f94412a82db32f36e3b0b9741f8a97feb031f7e" + integrity sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg== + +get-intrinsic@^1.0.2: + version "1.2.0" + resolved "https://registry.yarnpkg.com/get-intrinsic/-/get-intrinsic-1.2.0.tgz#7ad1dc0535f3a2904bba075772763e5051f6d05f" + integrity sha512-L049y6nFOuom5wGyRc3/gdTLO94dySVKRACj1RmJZBQXlbTMhtNIgkWkUHq+jYmZvKf14EW1EoJnnjbmoHij0Q== + dependencies: + function-bind "^1.1.1" + has "^1.0.3" + has-symbols "^1.0.3" + +get-package-type@^0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/get-package-type/-/get-package-type-0.1.0.tgz#8de2d803cff44df3bc6c456e6668b36c3926e11a" + integrity sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q== + +get-stream@^5.0.0: + version "5.2.0" + resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-5.2.0.tgz#4966a1795ee5ace65e706c4b7beb71257d6e22d3" + integrity sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA== + dependencies: + pump "^3.0.0" + +get-stream@^6.0.0: + version "6.0.1" + resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-6.0.1.tgz#a262d8eef67aced57c2852ad6167526a43cbf7b7" + integrity sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg== + +glob-parent@^5.1.2, glob-parent@~5.1.2: + version "5.1.2" + resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-5.1.2.tgz#869832c58034fe68a4093c17dc15e8340d8401c4" + integrity sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow== + dependencies: + is-glob "^4.0.1" + +glob-parent@^6.0.2: + version "6.0.2" + resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-6.0.2.tgz#6d237d99083950c79290f24c7642a3de9a28f9e3" + integrity sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A== + dependencies: + is-glob "^4.0.3" + +glob-to-regexp@^0.4.1: + version "0.4.1" + resolved "https://registry.yarnpkg.com/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz#c75297087c851b9a578bd217dd59a92f59fe546e" + integrity sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw== + +glob@^7.0.0, glob@^7.1.1, glob@^7.1.2, glob@^7.1.3, glob@^7.1.4: + version "7.2.3" + resolved "https://registry.yarnpkg.com/glob/-/glob-7.2.3.tgz#b8df0fb802bbfa8e89bd1d938b4e16578ed44f2b" + integrity sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q== + dependencies: + fs.realpath "^1.0.0" + inflight "^1.0.4" + inherits "2" + minimatch "^3.1.1" + once "^1.3.0" + path-is-absolute "^1.0.0" + +globals@^11.1.0: + version "11.12.0" + resolved "https://registry.yarnpkg.com/globals/-/globals-11.12.0.tgz#ab8795338868a0babd8525758018c2a7eb95c42e" + integrity sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA== + +globals@^13.19.0: + version "13.20.0" + resolved "https://registry.yarnpkg.com/globals/-/globals-13.20.0.tgz#ea276a1e508ffd4f1612888f9d1bad1e2717bf82" + integrity sha512-Qg5QtVkCy/kv3FUSlu4ukeZDVf9ee0iXLAUYX13gbR17bnejFTzr4iS9bY7kwCf1NztRNm1t91fjOiyx4CSwPQ== + dependencies: + type-fest "^0.20.2" + +globby@^11.1.0: + version "11.1.0" + resolved "https://registry.yarnpkg.com/globby/-/globby-11.1.0.tgz#bd4be98bb042f83d796f7e3811991fbe82a0d34b" + integrity sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g== + dependencies: + array-union "^2.1.0" + dir-glob "^3.0.1" + fast-glob "^3.2.9" + ignore "^5.2.0" + merge2 "^1.4.1" + slash "^3.0.0" + +graceful-fs@^4.1.2, graceful-fs@^4.1.6, graceful-fs@^4.2.0, graceful-fs@^4.2.4, graceful-fs@^4.2.9: + version "4.2.11" + resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.11.tgz#4183e4e8bf08bb6e05bbb2f7d2e0c8f712ca40e3" + integrity sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ== + +grapheme-splitter@^1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/grapheme-splitter/-/grapheme-splitter-1.0.4.tgz#9cf3a665c6247479896834af35cf1dbb4400767e" + integrity sha512-bzh50DW9kTPM00T8y4o8vQg89Di9oLJVLW/KaOGIXJWP/iqCN6WKYkbNOF04vFLJhwcpYUh9ydh/+5vpOqV4YQ== + +has-flag@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-3.0.0.tgz#b5d454dc2199ae225699f3467e5a07f3b955bafd" + integrity sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw== + +has-flag@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-4.0.0.tgz#944771fd9c81c81265c4d6941860da06bb59479b" + integrity sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ== + +has-symbols@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/has-symbols/-/has-symbols-1.0.3.tgz#bb7b2c4349251dce87b125f7bdf874aa7c8b39f8" + integrity sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A== + +has@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/has/-/has-1.0.3.tgz#722d7cbfc1f6aa8241f16dd814e011e1f41e8796" + integrity sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw== + dependencies: + function-bind "^1.1.1" + +hexoid@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/hexoid/-/hexoid-1.0.0.tgz#ad10c6573fb907de23d9ec63a711267d9dc9bc18" + integrity sha512-QFLV0taWQOZtvIRIAdBChesmogZrtuXvVWsFHZTk2SU+anspqZ2vMnoLg7IE1+Uk16N19APic1BuF8bC8c2m5g== + +html-encoding-sniffer@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/html-encoding-sniffer/-/html-encoding-sniffer-2.0.1.tgz#42a6dc4fd33f00281176e8b23759ca4e4fa185f3" + integrity sha512-D5JbOMBIR/TVZkubHT+OyT2705QvogUW4IBn6nHd756OwieSF9aDYFj4dv6HHEVGYbHaLETa3WggZYWWMyy3ZQ== + dependencies: + whatwg-encoding "^1.0.5" + +html-escaper@^2.0.0: + version "2.0.2" + resolved "https://registry.yarnpkg.com/html-escaper/-/html-escaper-2.0.2.tgz#dfd60027da36a36dfcbe236262c00a5822681453" + integrity sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg== + +http-errors@2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-2.0.0.tgz#b7774a1486ef73cf7667ac9ae0858c012c57b9d3" + integrity sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ== + dependencies: + depd "2.0.0" + inherits "2.0.4" + setprototypeof "1.2.0" + statuses "2.0.1" + toidentifier "1.0.1" + +http-proxy-agent@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/http-proxy-agent/-/http-proxy-agent-4.0.1.tgz#8a8c8ef7f5932ccf953c296ca8291b95aa74aa3a" + integrity sha512-k0zdNgqWTGA6aeIRVpvfVob4fL52dTfaehylg0Y4UvSySvOq/Y+BOyPrgpUrA7HylqvU8vIZGsRuXmspskV0Tg== + dependencies: + "@tootallnate/once" "1" + agent-base "6" + debug "4" + +https-proxy-agent@^5.0.0: + version "5.0.1" + resolved "https://registry.yarnpkg.com/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz#c59ef224a04fe8b754f3db0063a25ea30d0005d6" + integrity sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA== + dependencies: + agent-base "6" + debug "4" + +human-signals@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/human-signals/-/human-signals-1.1.1.tgz#c5b1cd14f50aeae09ab6c59fe63ba3395fe4dfa3" + integrity sha512-SEQu7vl8KjNL2eoGBLF3+wAjpsNfA9XMlXAYj/3EdaNfAlxKthD1xjEQfGOUhllCGGJVNY34bRr6lPINhNjyZw== + +human-signals@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/human-signals/-/human-signals-2.1.0.tgz#dc91fcba42e4d06e4abaed33b3e7a3c02f514ea0" + integrity sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw== + +iconv-lite@0.4.24, iconv-lite@^0.4.24: + version "0.4.24" + resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.24.tgz#2022b4b25fbddc21d2f524974a474aafe733908b" + integrity sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA== + dependencies: + safer-buffer ">= 2.1.2 < 3" + +ieee754@^1.1.13: + version "1.2.1" + resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.2.1.tgz#8eb7a10a63fff25d15a57b001586d177d1b0d352" + integrity sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA== + +ignore@^5.2.0: + version "5.2.4" + resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.2.4.tgz#a291c0c6178ff1b960befe47fcdec301674a6324" + integrity sha512-MAb38BcSbH0eHNBxn7ql2NH/kX33OkB3lZ1BNdh7ENeRChHTYsTvWrMubiIAMNS2llXEEgZ1MUOBtXChP3kaFQ== + +import-fresh@^3.0.0, import-fresh@^3.2.1: + version "3.3.0" + resolved "https://registry.yarnpkg.com/import-fresh/-/import-fresh-3.3.0.tgz#37162c25fcb9ebaa2e6e53d5b4d88ce17d9e0c2b" + integrity sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw== + dependencies: + parent-module "^1.0.0" + resolve-from "^4.0.0" + +import-local@^3.0.2: + version "3.1.0" + resolved "https://registry.yarnpkg.com/import-local/-/import-local-3.1.0.tgz#b4479df8a5fd44f6cdce24070675676063c95cb4" + integrity sha512-ASB07uLtnDs1o6EHjKpX34BKYDSqnFerfTOJL2HvMqF70LnxpjkzDB8J44oT9pu4AMPkQwf8jl6szgvNd2tRIg== + dependencies: + pkg-dir "^4.2.0" + resolve-cwd "^3.0.0" + +imurmurhash@^0.1.4: + version "0.1.4" + resolved "https://registry.yarnpkg.com/imurmurhash/-/imurmurhash-0.1.4.tgz#9218b9b2b928a238b13dc4fb6b6d576f231453ea" + integrity sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA== + +inflight@^1.0.4: + version "1.0.6" + resolved "https://registry.yarnpkg.com/inflight/-/inflight-1.0.6.tgz#49bd6331d7d02d0c09bc910a1075ba8165b56df9" + integrity sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA== + dependencies: + once "^1.3.0" + wrappy "1" + +inherits@2, inherits@2.0.4, inherits@^2.0.3, inherits@^2.0.4, inherits@~2.0.3: + version "2.0.4" + resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c" + integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== + +inquirer@7.3.3: + version "7.3.3" + resolved "https://registry.yarnpkg.com/inquirer/-/inquirer-7.3.3.tgz#04d176b2af04afc157a83fd7c100e98ee0aad003" + integrity sha512-JG3eIAj5V9CwcGvuOmoo6LB9kbAYT8HXffUl6memuszlwDC/qvFAJw49XJ5NROSFNPxp3iQg1GqkFhaY/CR0IA== + dependencies: + ansi-escapes "^4.2.1" + chalk "^4.1.0" + cli-cursor "^3.1.0" + cli-width "^3.0.0" + external-editor "^3.0.3" + figures "^3.0.0" + lodash "^4.17.19" + mute-stream "0.0.8" + run-async "^2.4.0" + rxjs "^6.6.0" + string-width "^4.1.0" + strip-ansi "^6.0.0" + through "^2.3.6" + +inquirer@8.2.0: + version "8.2.0" + resolved "https://registry.yarnpkg.com/inquirer/-/inquirer-8.2.0.tgz#f44f008dd344bbfc4b30031f45d984e034a3ac3a" + integrity sha512-0crLweprevJ02tTuA6ThpoAERAGyVILC4sS74uib58Xf/zSr1/ZWtmm7D5CI+bSQEaA04f0K7idaHpQbSWgiVQ== + dependencies: + ansi-escapes "^4.2.1" + chalk "^4.1.1" + cli-cursor "^3.1.0" + cli-width "^3.0.0" + external-editor "^3.0.3" + figures "^3.0.0" + lodash "^4.17.21" + mute-stream "0.0.8" + ora "^5.4.1" + run-async "^2.4.0" + rxjs "^7.2.0" + string-width "^4.1.0" + strip-ansi "^6.0.0" + through "^2.3.6" + +interpret@^1.0.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/interpret/-/interpret-1.4.0.tgz#665ab8bc4da27a774a40584e812e3e0fa45b1a1e" + integrity sha512-agE4QfB2Lkp9uICn7BAqoscw4SZP9kTE2hxiFI3jBPmXJfdqiahTbUuKGsMoN2GtqL9AxhYioAcVvgsb1HvRbA== + +ipaddr.js@1.9.1: + version "1.9.1" + resolved "https://registry.yarnpkg.com/ipaddr.js/-/ipaddr.js-1.9.1.tgz#bff38543eeb8984825079ff3a2a8e6cbd46781b3" + integrity sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g== + +is-arrayish@^0.2.1: + version "0.2.1" + resolved "https://registry.yarnpkg.com/is-arrayish/-/is-arrayish-0.2.1.tgz#77c99840527aa8ecb1a8ba697b80645a7a926a9d" + integrity sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg== + +is-binary-path@~2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/is-binary-path/-/is-binary-path-2.1.0.tgz#ea1f7f3b80f064236e83470f86c09c254fb45b09" + integrity sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw== + dependencies: + binary-extensions "^2.0.0" + +is-core-module@^2.11.0: + version "2.12.0" + resolved "https://registry.yarnpkg.com/is-core-module/-/is-core-module-2.12.0.tgz#36ad62f6f73c8253fd6472517a12483cf03e7ec4" + integrity sha512-RECHCBCd/viahWmwj6enj19sKbHfJrddi/6cBDsNTKbNq0f7VeaUkBo60BqzvPqo/W54ChS62Z5qyun7cfOMqQ== + dependencies: + has "^1.0.3" + +is-extglob@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/is-extglob/-/is-extglob-2.1.1.tgz#a88c02535791f02ed37c76a1b9ea9773c833f8c2" + integrity sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ== + +is-fullwidth-code-point@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz#f116f8064fe90b3f7844a38997c0b75051269f1d" + integrity sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg== + +is-generator-fn@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/is-generator-fn/-/is-generator-fn-2.1.0.tgz#7d140adc389aaf3011a8f2a2a4cfa6faadffb118" + integrity sha512-cTIB4yPYL/Grw0EaSzASzg6bBy9gqCofvWN8okThAYIxKJZC+udlRAmGbM0XLeniEJSs8uEgHPGuHSe1XsOLSQ== + +is-glob@^4.0.0, is-glob@^4.0.1, is-glob@^4.0.3, is-glob@~4.0.1: + version "4.0.3" + resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-4.0.3.tgz#64f61e42cbbb2eec2071a9dac0b28ba1e65d5084" + integrity sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg== + dependencies: + is-extglob "^2.1.1" + +is-interactive@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/is-interactive/-/is-interactive-1.0.0.tgz#cea6e6ae5c870a7b0a0004070b7b587e0252912e" + integrity sha512-2HvIEKRoqS62guEC+qBjpvRubdX910WCMuJTZ+I9yvqKU2/12eSL549HMwtabb4oupdj2sMP50k+XJfB/8JE6w== + +is-number@^7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/is-number/-/is-number-7.0.0.tgz#7535345b896734d5f80c4d06c50955527a14f12b" + integrity sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng== + +is-path-inside@^3.0.3: + version "3.0.3" + resolved "https://registry.yarnpkg.com/is-path-inside/-/is-path-inside-3.0.3.tgz#d231362e53a07ff2b0e0ea7fed049161ffd16283" + integrity sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ== + +is-potential-custom-element-name@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz#171ed6f19e3ac554394edf78caa05784a45bebb5" + integrity sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ== + +is-stream@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-2.0.1.tgz#fac1e3d53b97ad5a9d0ae9cef2389f5810a5c077" + integrity sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg== + +is-typedarray@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/is-typedarray/-/is-typedarray-1.0.0.tgz#e479c80858df0c1b11ddda6940f96011fcda4a9a" + integrity sha512-cyA56iCMHAh5CdzjJIa4aohJyeO1YbwLi3Jc35MmRU6poroFjIGZzUzupGiRPOjgHg9TLu43xbpwXk523fMxKA== + +is-unicode-supported@^0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz#3f26c76a809593b52bfa2ecb5710ed2779b522a7" + integrity sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw== + +isarray@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/isarray/-/isarray-1.0.0.tgz#bb935d48582cba168c06834957a54a3e07124f11" + integrity sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ== + +isexe@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10" + integrity sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw== + +istanbul-lib-coverage@^3.0.0, istanbul-lib-coverage@^3.2.0: + version "3.2.0" + resolved "https://registry.yarnpkg.com/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.0.tgz#189e7909d0a39fa5a3dfad5b03f71947770191d3" + integrity sha512-eOeJ5BHCmHYvQK7xt9GkdHuzuCGS1Y6g9Gvnx3Ym33fz/HpLRYxiS0wHNr+m/MBC8B647Xt608vCDEvhl9c6Mw== + +istanbul-lib-instrument@^5.0.4, istanbul-lib-instrument@^5.1.0: + version "5.2.1" + resolved "https://registry.yarnpkg.com/istanbul-lib-instrument/-/istanbul-lib-instrument-5.2.1.tgz#d10c8885c2125574e1c231cacadf955675e1ce3d" + integrity sha512-pzqtp31nLv/XFOzXGuvhCb8qhjmTVo5vjVk19XE4CRlSWz0KoeJ3bw9XsA7nOp9YBf4qHjwBxkDzKcME/J29Yg== + 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" + +istanbul-lib-report@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/istanbul-lib-report/-/istanbul-lib-report-3.0.0.tgz#7518fe52ea44de372f460a76b5ecda9ffb73d8a6" + integrity sha512-wcdi+uAKzfiGT2abPpKZ0hSU1rGQjUQnLvtY5MpQ7QCTahD3VODhcu4wcfY1YtkGaDD5yuydOLINXsfbus9ROw== + dependencies: + istanbul-lib-coverage "^3.0.0" + make-dir "^3.0.0" + supports-color "^7.1.0" + +istanbul-lib-source-maps@^4.0.0: + version "4.0.1" + resolved "https://registry.yarnpkg.com/istanbul-lib-source-maps/-/istanbul-lib-source-maps-4.0.1.tgz#895f3a709fcfba34c6de5a42939022f3e4358551" + integrity sha512-n3s8EwkdFIJCG3BPKBYvskgXGoy88ARzvegkitk60NxRdwltLOTaH7CUiMRXvwYorl0Q712iEjcWB+fK/MrWVw== + dependencies: + debug "^4.1.1" + istanbul-lib-coverage "^3.0.0" + source-map "^0.6.1" + +istanbul-reports@^3.1.3: + version "3.1.5" + resolved "https://registry.yarnpkg.com/istanbul-reports/-/istanbul-reports-3.1.5.tgz#cc9a6ab25cb25659810e4785ed9d9fb742578bae" + integrity sha512-nUsEMa9pBt/NOHqbcbeJEgqIlY/K7rVWUX6Lql2orY5e9roQOthbR3vtY4zzf2orPELg80fnxxk9zUyPlgwD1w== + dependencies: + html-escaper "^2.0.0" + istanbul-lib-report "^3.0.0" + +iterare@1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/iterare/-/iterare-1.2.1.tgz#139c400ff7363690e33abffa33cbba8920f00042" + integrity sha512-RKYVTCjAnRthyJes037NX/IiqeidgN1xc3j1RjFfECFp28A1GVwK9nA+i0rJPaHqSZwygLzRnFlzUuHFoWWy+Q== + +jest-changed-files@^27.5.1: + version "27.5.1" + resolved "https://registry.yarnpkg.com/jest-changed-files/-/jest-changed-files-27.5.1.tgz#a348aed00ec9bf671cc58a66fcbe7c3dfd6a68f5" + integrity sha512-buBLMiByfWGCoMsLLzGUUSpAmIAGnbR2KJoMN10ziLhOLvP4e0SlypHnAel8iqQXTrcbmfEY9sSqae5sgUsTvw== + dependencies: + "@jest/types" "^27.5.1" + execa "^5.0.0" + throat "^6.0.1" + +jest-circus@^27.5.1: + version "27.5.1" + resolved "https://registry.yarnpkg.com/jest-circus/-/jest-circus-27.5.1.tgz#37a5a4459b7bf4406e53d637b49d22c65d125ecc" + integrity sha512-D95R7x5UtlMA5iBYsOHFFbMD/GVA4R/Kdq15f7xYWUfWHBto9NYRsOvnSauTgdF+ogCpJ4tyKOXhUifxS65gdw== + dependencies: + "@jest/environment" "^27.5.1" + "@jest/test-result" "^27.5.1" + "@jest/types" "^27.5.1" + "@types/node" "*" + chalk "^4.0.0" + co "^4.6.0" + dedent "^0.7.0" + expect "^27.5.1" + is-generator-fn "^2.0.0" + jest-each "^27.5.1" + jest-matcher-utils "^27.5.1" + jest-message-util "^27.5.1" + jest-runtime "^27.5.1" + jest-snapshot "^27.5.1" + jest-util "^27.5.1" + pretty-format "^27.5.1" + slash "^3.0.0" + stack-utils "^2.0.3" + throat "^6.0.1" + +jest-cli@^27.5.1: + version "27.5.1" + resolved "https://registry.yarnpkg.com/jest-cli/-/jest-cli-27.5.1.tgz#278794a6e6458ea8029547e6c6cbf673bd30b145" + integrity sha512-Hc6HOOwYq4/74/c62dEE3r5elx8wjYqxY0r0G/nFrLDPMFRu6RA/u8qINOIkvhxG7mMQ5EJsOGfRpI8L6eFUVw== + dependencies: + "@jest/core" "^27.5.1" + "@jest/test-result" "^27.5.1" + "@jest/types" "^27.5.1" + chalk "^4.0.0" + exit "^0.1.2" + graceful-fs "^4.2.9" + import-local "^3.0.2" + jest-config "^27.5.1" + jest-util "^27.5.1" + jest-validate "^27.5.1" + prompts "^2.0.1" + yargs "^16.2.0" + +jest-config@^27.5.1: + version "27.5.1" + resolved "https://registry.yarnpkg.com/jest-config/-/jest-config-27.5.1.tgz#5c387de33dca3f99ad6357ddeccd91bf3a0e4a41" + integrity sha512-5sAsjm6tGdsVbW9ahcChPAFCk4IlkQUknH5AvKjuLTSlcO/wCZKyFdn7Rg0EkC+OGgWODEy2hDpWB1PgzH0JNA== + dependencies: + "@babel/core" "^7.8.0" + "@jest/test-sequencer" "^27.5.1" + "@jest/types" "^27.5.1" + babel-jest "^27.5.1" + chalk "^4.0.0" + ci-info "^3.2.0" + deepmerge "^4.2.2" + glob "^7.1.1" + graceful-fs "^4.2.9" + jest-circus "^27.5.1" + jest-environment-jsdom "^27.5.1" + jest-environment-node "^27.5.1" + jest-get-type "^27.5.1" + jest-jasmine2 "^27.5.1" + jest-regex-util "^27.5.1" + jest-resolve "^27.5.1" + jest-runner "^27.5.1" + jest-util "^27.5.1" + jest-validate "^27.5.1" + micromatch "^4.0.4" + parse-json "^5.2.0" + pretty-format "^27.5.1" + slash "^3.0.0" + strip-json-comments "^3.1.1" + +jest-diff@^27.5.1: + version "27.5.1" + resolved "https://registry.yarnpkg.com/jest-diff/-/jest-diff-27.5.1.tgz#a07f5011ac9e6643cf8a95a462b7b1ecf6680def" + integrity sha512-m0NvkX55LDt9T4mctTEgnZk3fmEg3NRYutvMPWM/0iPnkFj2wIeF45O1718cMSOFO1vINkqmxqD8vE37uTEbqw== + dependencies: + chalk "^4.0.0" + diff-sequences "^27.5.1" + jest-get-type "^27.5.1" + pretty-format "^27.5.1" + +jest-docblock@^27.5.1: + version "27.5.1" + resolved "https://registry.yarnpkg.com/jest-docblock/-/jest-docblock-27.5.1.tgz#14092f364a42c6108d42c33c8cf30e058e25f6c0" + integrity sha512-rl7hlABeTsRYxKiUfpHrQrG4e2obOiTQWfMEH3PxPjOtdsfLQO4ReWSZaQ7DETm4xu07rl4q/h4zcKXyU0/OzQ== + dependencies: + detect-newline "^3.0.0" + +jest-each@^27.5.1: + version "27.5.1" + resolved "https://registry.yarnpkg.com/jest-each/-/jest-each-27.5.1.tgz#5bc87016f45ed9507fed6e4702a5b468a5b2c44e" + integrity sha512-1Ff6p+FbhT/bXQnEouYy00bkNSY7OUpfIcmdl8vZ31A1UUaurOLPA8a8BbJOF2RDUElwJhmeaV7LnagI+5UwNQ== + dependencies: + "@jest/types" "^27.5.1" + chalk "^4.0.0" + jest-get-type "^27.5.1" + jest-util "^27.5.1" + pretty-format "^27.5.1" + +jest-environment-jsdom@^27.5.1: + version "27.5.1" + resolved "https://registry.yarnpkg.com/jest-environment-jsdom/-/jest-environment-jsdom-27.5.1.tgz#ea9ccd1fc610209655a77898f86b2b559516a546" + integrity sha512-TFBvkTC1Hnnnrka/fUb56atfDtJ9VMZ94JkjTbggl1PEpwrYtUBKMezB3inLmWqQsXYLcMwNoDQwoBTAvFfsfw== + dependencies: + "@jest/environment" "^27.5.1" + "@jest/fake-timers" "^27.5.1" + "@jest/types" "^27.5.1" + "@types/node" "*" + jest-mock "^27.5.1" + jest-util "^27.5.1" + jsdom "^16.6.0" + +jest-environment-node@^27.5.1: + version "27.5.1" + resolved "https://registry.yarnpkg.com/jest-environment-node/-/jest-environment-node-27.5.1.tgz#dedc2cfe52fab6b8f5714b4808aefa85357a365e" + integrity sha512-Jt4ZUnxdOsTGwSRAfKEnE6BcwsSPNOijjwifq5sDFSA2kesnXTvNqKHYgM0hDq3549Uf/KzdXNYn4wMZJPlFLw== + dependencies: + "@jest/environment" "^27.5.1" + "@jest/fake-timers" "^27.5.1" + "@jest/types" "^27.5.1" + "@types/node" "*" + jest-mock "^27.5.1" + jest-util "^27.5.1" + +jest-get-type@^27.5.1: + version "27.5.1" + resolved "https://registry.yarnpkg.com/jest-get-type/-/jest-get-type-27.5.1.tgz#3cd613c507b0f7ace013df407a1c1cd578bcb4f1" + integrity sha512-2KY95ksYSaK7DMBWQn6dQz3kqAf3BB64y2udeG+hv4KfSOb9qwcYQstTJc1KCbsix+wLZWZYN8t7nwX3GOBLRw== + +jest-haste-map@^27.5.1: + version "27.5.1" + resolved "https://registry.yarnpkg.com/jest-haste-map/-/jest-haste-map-27.5.1.tgz#9fd8bd7e7b4fa502d9c6164c5640512b4e811e7f" + integrity sha512-7GgkZ4Fw4NFbMSDSpZwXeBiIbx+t/46nJ2QitkOjvwPYyZmqttu2TDSimMHP1EkPOi4xUZAN1doE5Vd25H4Jng== + dependencies: + "@jest/types" "^27.5.1" + "@types/graceful-fs" "^4.1.2" + "@types/node" "*" + anymatch "^3.0.3" + fb-watchman "^2.0.0" + graceful-fs "^4.2.9" + jest-regex-util "^27.5.1" + jest-serializer "^27.5.1" + jest-util "^27.5.1" + jest-worker "^27.5.1" + micromatch "^4.0.4" + walker "^1.0.7" + optionalDependencies: + fsevents "^2.3.2" + +jest-jasmine2@^27.5.1: + version "27.5.1" + resolved "https://registry.yarnpkg.com/jest-jasmine2/-/jest-jasmine2-27.5.1.tgz#a037b0034ef49a9f3d71c4375a796f3b230d1ac4" + integrity sha512-jtq7VVyG8SqAorDpApwiJJImd0V2wv1xzdheGHRGyuT7gZm6gG47QEskOlzsN1PG/6WNaCo5pmwMHDf3AkG2pQ== + dependencies: + "@jest/environment" "^27.5.1" + "@jest/source-map" "^27.5.1" + "@jest/test-result" "^27.5.1" + "@jest/types" "^27.5.1" + "@types/node" "*" + chalk "^4.0.0" + co "^4.6.0" + expect "^27.5.1" + is-generator-fn "^2.0.0" + jest-each "^27.5.1" + jest-matcher-utils "^27.5.1" + jest-message-util "^27.5.1" + jest-runtime "^27.5.1" + jest-snapshot "^27.5.1" + jest-util "^27.5.1" + pretty-format "^27.5.1" + throat "^6.0.1" + +jest-leak-detector@^27.5.1: + version "27.5.1" + resolved "https://registry.yarnpkg.com/jest-leak-detector/-/jest-leak-detector-27.5.1.tgz#6ec9d54c3579dd6e3e66d70e3498adf80fde3fb8" + integrity sha512-POXfWAMvfU6WMUXftV4HolnJfnPOGEu10fscNCA76KBpRRhcMN2c8d3iT2pxQS3HLbA+5X4sOUPzYO2NUyIlHQ== + dependencies: + jest-get-type "^27.5.1" + pretty-format "^27.5.1" + +jest-matcher-utils@^27.0.0, jest-matcher-utils@^27.5.1: + version "27.5.1" + resolved "https://registry.yarnpkg.com/jest-matcher-utils/-/jest-matcher-utils-27.5.1.tgz#9c0cdbda8245bc22d2331729d1091308b40cf8ab" + integrity sha512-z2uTx/T6LBaCoNWNFWwChLBKYxTMcGBRjAt+2SbP929/Fflb9aa5LGma654Rz8z9HLxsrUaYzxE9T/EFIL/PAw== + dependencies: + chalk "^4.0.0" + jest-diff "^27.5.1" + jest-get-type "^27.5.1" + pretty-format "^27.5.1" + +jest-message-util@^27.5.1: + version "27.5.1" + resolved "https://registry.yarnpkg.com/jest-message-util/-/jest-message-util-27.5.1.tgz#bdda72806da10d9ed6425e12afff38cd1458b6cf" + integrity sha512-rMyFe1+jnyAAf+NHwTclDz0eAaLkVDdKVHHBFWsBWHnnh5YeJMNWWsv7AbFYXfK3oTqvL7VTWkhNLu1jX24D+g== + dependencies: + "@babel/code-frame" "^7.12.13" + "@jest/types" "^27.5.1" + "@types/stack-utils" "^2.0.0" + chalk "^4.0.0" + graceful-fs "^4.2.9" + micromatch "^4.0.4" + pretty-format "^27.5.1" + slash "^3.0.0" + stack-utils "^2.0.3" + +jest-mock@^27.5.1: + version "27.5.1" + resolved "https://registry.yarnpkg.com/jest-mock/-/jest-mock-27.5.1.tgz#19948336d49ef4d9c52021d34ac7b5f36ff967d6" + integrity sha512-K4jKbY1d4ENhbrG2zuPWaQBvDly+iZ2yAW+T1fATN78hc0sInwn7wZB8XtlNnvHug5RMwV897Xm4LqmPM4e2Og== + dependencies: + "@jest/types" "^27.5.1" + "@types/node" "*" + +jest-pnp-resolver@^1.2.2: + version "1.2.3" + resolved "https://registry.yarnpkg.com/jest-pnp-resolver/-/jest-pnp-resolver-1.2.3.tgz#930b1546164d4ad5937d5540e711d4d38d4cad2e" + integrity sha512-+3NpwQEnRoIBtx4fyhblQDPgJI0H1IEIkX7ShLUjPGA7TtUTvI1oiKi3SR4oBR0hQhQR80l4WAe5RrXBwWMA8w== + +jest-regex-util@^27.5.1: + version "27.5.1" + resolved "https://registry.yarnpkg.com/jest-regex-util/-/jest-regex-util-27.5.1.tgz#4da143f7e9fd1e542d4aa69617b38e4a78365b95" + integrity sha512-4bfKq2zie+x16okqDXjXn9ql2B0dScQu+vcwe4TvFVhkVyuWLqpZrZtXxLLWoXYgn0E87I6r6GRYHF7wFZBUvg== + +jest-resolve-dependencies@^27.5.1: + version "27.5.1" + resolved "https://registry.yarnpkg.com/jest-resolve-dependencies/-/jest-resolve-dependencies-27.5.1.tgz#d811ecc8305e731cc86dd79741ee98fed06f1da8" + integrity sha512-QQOOdY4PE39iawDn5rzbIePNigfe5B9Z91GDD1ae/xNDlu9kaat8QQ5EKnNmVWPV54hUdxCVwwj6YMgR2O7IOg== + dependencies: + "@jest/types" "^27.5.1" + jest-regex-util "^27.5.1" + jest-snapshot "^27.5.1" + +jest-resolve@^27.5.1: + version "27.5.1" + resolved "https://registry.yarnpkg.com/jest-resolve/-/jest-resolve-27.5.1.tgz#a2f1c5a0796ec18fe9eb1536ac3814c23617b384" + integrity sha512-FFDy8/9E6CV83IMbDpcjOhumAQPDyETnU2KZ1O98DwTnz8AOBsW/Xv3GySr1mOZdItLR+zDZ7I/UdTFbgSOVCw== + dependencies: + "@jest/types" "^27.5.1" + chalk "^4.0.0" + graceful-fs "^4.2.9" + jest-haste-map "^27.5.1" + jest-pnp-resolver "^1.2.2" + jest-util "^27.5.1" + jest-validate "^27.5.1" + resolve "^1.20.0" + resolve.exports "^1.1.0" + slash "^3.0.0" + +jest-runner@^27.5.1: + version "27.5.1" + resolved "https://registry.yarnpkg.com/jest-runner/-/jest-runner-27.5.1.tgz#071b27c1fa30d90540805c5645a0ec167c7b62e5" + integrity sha512-g4NPsM4mFCOwFKXO4p/H/kWGdJp9V8kURY2lX8Me2drgXqG7rrZAx5kv+5H7wtt/cdFIjhqYx1HrlqWHaOvDaQ== + dependencies: + "@jest/console" "^27.5.1" + "@jest/environment" "^27.5.1" + "@jest/test-result" "^27.5.1" + "@jest/transform" "^27.5.1" + "@jest/types" "^27.5.1" + "@types/node" "*" + chalk "^4.0.0" + emittery "^0.8.1" + graceful-fs "^4.2.9" + jest-docblock "^27.5.1" + jest-environment-jsdom "^27.5.1" + jest-environment-node "^27.5.1" + jest-haste-map "^27.5.1" + jest-leak-detector "^27.5.1" + jest-message-util "^27.5.1" + jest-resolve "^27.5.1" + jest-runtime "^27.5.1" + jest-util "^27.5.1" + jest-worker "^27.5.1" + source-map-support "^0.5.6" + throat "^6.0.1" + +jest-runtime@^27.5.1: + version "27.5.1" + resolved "https://registry.yarnpkg.com/jest-runtime/-/jest-runtime-27.5.1.tgz#4896003d7a334f7e8e4a53ba93fb9bcd3db0a1af" + integrity sha512-o7gxw3Gf+H2IGt8fv0RiyE1+r83FJBRruoA+FXrlHw6xEyBsU8ugA6IPfTdVyA0w8HClpbK+DGJxH59UrNMx8A== + dependencies: + "@jest/environment" "^27.5.1" + "@jest/fake-timers" "^27.5.1" + "@jest/globals" "^27.5.1" + "@jest/source-map" "^27.5.1" + "@jest/test-result" "^27.5.1" + "@jest/transform" "^27.5.1" + "@jest/types" "^27.5.1" + 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 "^27.5.1" + jest-message-util "^27.5.1" + jest-mock "^27.5.1" + jest-regex-util "^27.5.1" + jest-resolve "^27.5.1" + jest-snapshot "^27.5.1" + jest-util "^27.5.1" + slash "^3.0.0" + strip-bom "^4.0.0" + +jest-serializer@^27.5.1: + version "27.5.1" + resolved "https://registry.yarnpkg.com/jest-serializer/-/jest-serializer-27.5.1.tgz#81438410a30ea66fd57ff730835123dea1fb1f64" + integrity sha512-jZCyo6iIxO1aqUxpuBlwTDMkzOAJS4a3eYz3YzgxxVQFwLeSA7Jfq5cbqCY+JLvTDrWirgusI/0KwxKMgrdf7w== + dependencies: + "@types/node" "*" + graceful-fs "^4.2.9" + +jest-snapshot@^27.5.1: + version "27.5.1" + resolved "https://registry.yarnpkg.com/jest-snapshot/-/jest-snapshot-27.5.1.tgz#b668d50d23d38054a51b42c4039cab59ae6eb6a1" + integrity sha512-yYykXI5a0I31xX67mgeLw1DZ0bJB+gpq5IpSuCAoyDi0+BhgU/RIrL+RTzDmkNTchvDFWKP8lp+w/42Z3us5sA== + dependencies: + "@babel/core" "^7.7.2" + "@babel/generator" "^7.7.2" + "@babel/plugin-syntax-typescript" "^7.7.2" + "@babel/traverse" "^7.7.2" + "@babel/types" "^7.0.0" + "@jest/transform" "^27.5.1" + "@jest/types" "^27.5.1" + "@types/babel__traverse" "^7.0.4" + "@types/prettier" "^2.1.5" + babel-preset-current-node-syntax "^1.0.0" + chalk "^4.0.0" + expect "^27.5.1" + graceful-fs "^4.2.9" + jest-diff "^27.5.1" + jest-get-type "^27.5.1" + jest-haste-map "^27.5.1" + jest-matcher-utils "^27.5.1" + jest-message-util "^27.5.1" + jest-util "^27.5.1" + natural-compare "^1.4.0" + pretty-format "^27.5.1" + semver "^7.3.2" + +jest-util@^27.0.0, jest-util@^27.5.1: + version "27.5.1" + resolved "https://registry.yarnpkg.com/jest-util/-/jest-util-27.5.1.tgz#3ba9771e8e31a0b85da48fe0b0891fb86c01c2f9" + integrity sha512-Kv2o/8jNvX1MQ0KGtw480E/w4fBCDOnH6+6DmeKi6LZUIlKA5kwY0YNdlzaWTiVgxqAqik11QyxDOKk543aKXw== + dependencies: + "@jest/types" "^27.5.1" + "@types/node" "*" + chalk "^4.0.0" + ci-info "^3.2.0" + graceful-fs "^4.2.9" + picomatch "^2.2.3" + +jest-validate@^27.5.1: + version "27.5.1" + resolved "https://registry.yarnpkg.com/jest-validate/-/jest-validate-27.5.1.tgz#9197d54dc0bdb52260b8db40b46ae668e04df067" + integrity sha512-thkNli0LYTmOI1tDB3FI1S1RTp/Bqyd9pTarJwL87OIBFuqEb5Apv5EaApEudYg4g86e3CT6kM0RowkhtEnCBQ== + dependencies: + "@jest/types" "^27.5.1" + camelcase "^6.2.0" + chalk "^4.0.0" + jest-get-type "^27.5.1" + leven "^3.1.0" + pretty-format "^27.5.1" + +jest-watcher@^27.5.1: + version "27.5.1" + resolved "https://registry.yarnpkg.com/jest-watcher/-/jest-watcher-27.5.1.tgz#71bd85fb9bde3a2c2ec4dc353437971c43c642a2" + integrity sha512-z676SuD6Z8o8qbmEGhoEUFOM1+jfEiL3DXHK/xgEiG2EyNYfFG60jluWcupY6dATjfEsKQuibReS1djInQnoVw== + dependencies: + "@jest/test-result" "^27.5.1" + "@jest/types" "^27.5.1" + "@types/node" "*" + ansi-escapes "^4.2.1" + chalk "^4.0.0" + jest-util "^27.5.1" + string-length "^4.0.1" + +jest-worker@^27.4.5, jest-worker@^27.5.1: + version "27.5.1" + resolved "https://registry.yarnpkg.com/jest-worker/-/jest-worker-27.5.1.tgz#8d146f0900e8973b106b6f73cc1e9a8cb86f8db0" + integrity sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg== + dependencies: + "@types/node" "*" + merge-stream "^2.0.0" + supports-color "^8.0.0" + +jest@^27.2.5: + version "27.5.1" + resolved "https://registry.yarnpkg.com/jest/-/jest-27.5.1.tgz#dadf33ba70a779be7a6fc33015843b51494f63fc" + integrity sha512-Yn0mADZB89zTtjkPJEXwrac3LHudkQMR+Paqa8uxJHCBr9agxztUifWCyiYrjhMPBoUVBjyny0I7XH6ozDr7QQ== + dependencies: + "@jest/core" "^27.5.1" + import-local "^3.0.2" + jest-cli "^27.5.1" + +js-sdsl@^4.1.4: + version "4.4.0" + resolved "https://registry.yarnpkg.com/js-sdsl/-/js-sdsl-4.4.0.tgz#8b437dbe642daa95760400b602378ed8ffea8430" + integrity sha512-FfVSdx6pJ41Oa+CF7RDaFmTnCaFhua+SNYQX74riGOpl96x+2jQCqEfQ2bnXu/5DPCqlRuiqyvTJM0Qjz26IVg== + +js-tokens@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499" + integrity sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ== + +js-yaml@^3.13.1: + version "3.14.1" + resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.14.1.tgz#dae812fdb3825fa306609a8717383c50c36a0537" + integrity sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g== + dependencies: + argparse "^1.0.7" + esprima "^4.0.0" + +js-yaml@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-4.1.0.tgz#c1fb65f8f5017901cdd2c951864ba18458a10602" + integrity sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA== + dependencies: + argparse "^2.0.1" + +jsdom@^16.6.0: + version "16.7.0" + resolved "https://registry.yarnpkg.com/jsdom/-/jsdom-16.7.0.tgz#918ae71965424b197c819f8183a754e18977b710" + integrity sha512-u9Smc2G1USStM+s/x1ru5Sxrl6mPYCbByG1U/hUmqaVsm4tbNyS7CicOSRyuGQYZhTu0h84qkZZQ/I+dzizSVw== + dependencies: + abab "^2.0.5" + acorn "^8.2.4" + acorn-globals "^6.0.0" + cssom "^0.4.4" + cssstyle "^2.3.0" + data-urls "^2.0.0" + decimal.js "^10.2.1" + domexception "^2.0.1" + escodegen "^2.0.0" + form-data "^3.0.0" + html-encoding-sniffer "^2.0.1" + http-proxy-agent "^4.0.1" + https-proxy-agent "^5.0.0" + is-potential-custom-element-name "^1.0.1" + nwsapi "^2.2.0" + parse5 "6.0.1" + saxes "^5.0.1" + symbol-tree "^3.2.4" + tough-cookie "^4.0.0" + w3c-hr-time "^1.0.2" + w3c-xmlserializer "^2.0.0" + webidl-conversions "^6.1.0" + whatwg-encoding "^1.0.5" + whatwg-mimetype "^2.3.0" + whatwg-url "^8.5.0" + ws "^7.4.6" + xml-name-validator "^3.0.0" + +jsesc@^2.5.1: + version "2.5.2" + resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-2.5.2.tgz#80564d2e483dacf6e8ef209650a67df3f0c283a4" + integrity sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA== + +json-parse-even-better-errors@^2.3.0, json-parse-even-better-errors@^2.3.1: + version "2.3.1" + resolved "https://registry.yarnpkg.com/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz#7c47805a94319928e05777405dc12e1f7a4ee02d" + integrity sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w== + +json-schema-traverse@^0.4.1: + version "0.4.1" + resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz#69f6a87d9513ab8bb8fe63bdb0979c448e684660" + integrity sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg== + +json-schema-traverse@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz#ae7bcb3656ab77a73ba5c49bf654f38e6b6860e2" + integrity sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug== + +json-stable-stringify-without-jsonify@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz#9db7b59496ad3f3cfef30a75142d2d930ad72651" + integrity sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw== + +json5@2.x, json5@^2.2.2: + version "2.2.3" + resolved "https://registry.yarnpkg.com/json5/-/json5-2.2.3.tgz#78cd6f1a19bdc12b73db5ad0c61efd66c1e29283" + integrity sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg== + +json5@^1.0.1, json5@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/json5/-/json5-1.0.2.tgz#63d98d60f21b313b77c4d6da18bfa69d80e1d593" + integrity sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA== + dependencies: + minimist "^1.2.0" + +jsonc-parser@3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/jsonc-parser/-/jsonc-parser-3.0.0.tgz#abdd785701c7e7eaca8a9ec8cf070ca51a745a22" + integrity sha512-fQzRfAbIBnR0IQvftw9FJveWiHp72Fg20giDrHz6TdfB12UH/uue0D3hm57UB5KgAVuniLMCaS8P1IMj9NR7cA== + +jsonfile@^6.0.1: + version "6.1.0" + resolved "https://registry.yarnpkg.com/jsonfile/-/jsonfile-6.1.0.tgz#bc55b2634793c679ec6403094eb13698a6ec0aae" + integrity sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ== + dependencies: + universalify "^2.0.0" + optionalDependencies: + graceful-fs "^4.1.6" + +kleur@^3.0.3: + version "3.0.3" + resolved "https://registry.yarnpkg.com/kleur/-/kleur-3.0.3.tgz#a79c9ecc86ee1ce3fa6206d1216c501f147fc07e" + integrity sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w== + +leven@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/leven/-/leven-3.1.0.tgz#77891de834064cccba82ae7842bb6b14a13ed7f2" + integrity sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A== + +levn@^0.4.1: + version "0.4.1" + resolved "https://registry.yarnpkg.com/levn/-/levn-0.4.1.tgz#ae4562c007473b932a6200d403268dd2fffc6ade" + integrity sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ== + dependencies: + prelude-ls "^1.2.1" + type-check "~0.4.0" + +levn@~0.3.0: + version "0.3.0" + resolved "https://registry.yarnpkg.com/levn/-/levn-0.3.0.tgz#3b09924edf9f083c0490fdd4c0bc4421e04764ee" + integrity sha512-0OO4y2iOHix2W6ujICbKIaEQXvFQHue65vUG3pb5EUomzPI90z9hsA1VsO/dbIIpC53J8gxM9Q4Oho0jrCM/yA== + dependencies: + prelude-ls "~1.1.2" + type-check "~0.3.2" + +lines-and-columns@^1.1.6: + version "1.2.4" + resolved "https://registry.yarnpkg.com/lines-and-columns/-/lines-and-columns-1.2.4.tgz#eca284f75d2965079309dc0ad9255abb2ebc1632" + integrity sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg== + +loader-runner@^4.2.0: + version "4.3.0" + resolved "https://registry.yarnpkg.com/loader-runner/-/loader-runner-4.3.0.tgz#c1b4a163b99f614830353b16755e7149ac2314e1" + integrity sha512-3R/1M+yS3j5ou80Me59j7F9IMs4PXs3VqRrm0TU3AbKPxlmpoY1TNscJV/oGJXo8qCatFGTfDbY6W6ipGOYXfg== + +locate-path@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-5.0.0.tgz#1afba396afd676a6d42504d0a67a3a7eb9f62aa0" + integrity sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g== + dependencies: + p-locate "^4.1.0" + +locate-path@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-6.0.0.tgz#55321eb309febbc59c4801d931a72452a681d286" + integrity sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw== + dependencies: + p-locate "^5.0.0" + +lodash.memoize@4.x: + version "4.1.2" + resolved "https://registry.yarnpkg.com/lodash.memoize/-/lodash.memoize-4.1.2.tgz#bcc6c49a42a2840ed997f323eada5ecd182e0bfe" + integrity sha512-t7j+NzmgnQzTAYXcsHYLgimltOV1MXHtlOWf6GjL9Kj8GK5FInw5JotxvbOs+IvV1/Dzo04/fCGfLVs7aXb4Ag== + +lodash.merge@^4.6.2: + version "4.6.2" + resolved "https://registry.yarnpkg.com/lodash.merge/-/lodash.merge-4.6.2.tgz#558aa53b43b661e1925a0afdfa36a9a1085fe57a" + integrity sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ== + +lodash@^4.17.19, lodash@^4.17.21, lodash@^4.7.0: + version "4.17.21" + resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c" + integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg== + +log-symbols@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/log-symbols/-/log-symbols-4.1.0.tgz#3fbdbb95b4683ac9fc785111e792e558d4abd503" + integrity sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg== + dependencies: + chalk "^4.1.0" + is-unicode-supported "^0.1.0" + +lru-cache@^5.1.1: + version "5.1.1" + resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-5.1.1.tgz#1da27e6710271947695daf6848e847f01d84b920" + integrity sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w== + dependencies: + yallist "^3.0.2" + +lru-cache@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-6.0.0.tgz#6d6fe6570ebd96aaf90fcad1dafa3b2566db3a94" + integrity sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA== + dependencies: + yallist "^4.0.0" + +macos-release@^2.5.0: + version "2.5.1" + resolved "https://registry.yarnpkg.com/macos-release/-/macos-release-2.5.1.tgz#bccac4a8f7b93163a8d163b8ebf385b3c5f55bf9" + integrity sha512-DXqXhEM7gW59OjZO8NIjBCz9AQ1BEMrfiOAl4AYByHCtVHRF4KoGNO8mqQeM8lRCtQe/UnJ4imO/d2HdkKsd+A== + +magic-string@0.25.7: + version "0.25.7" + resolved "https://registry.yarnpkg.com/magic-string/-/magic-string-0.25.7.tgz#3f497d6fd34c669c6798dcb821f2ef31f5445051" + integrity sha512-4CrMT5DOHTDk4HYDlzmwu4FVCcIYI8gauveasrdCu2IKIFOJ3f0v/8MDGJCDL9oD2ppz/Av1b0Nj345H9M+XIA== + dependencies: + sourcemap-codec "^1.4.4" + +make-dir@^3.0.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/make-dir/-/make-dir-3.1.0.tgz#415e967046b3a7f1d185277d84aa58203726a13f" + integrity sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw== + dependencies: + semver "^6.0.0" + +make-error@1.x, make-error@^1.1.1: + version "1.3.6" + resolved "https://registry.yarnpkg.com/make-error/-/make-error-1.3.6.tgz#2eb2e37ea9b67c4891f684a1394799af484cf7a2" + integrity sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw== + +makeerror@1.0.12: + version "1.0.12" + resolved "https://registry.yarnpkg.com/makeerror/-/makeerror-1.0.12.tgz#3e5dd2079a82e812e983cc6610c4a2cb0eaa801a" + integrity sha512-JmqCvUhmt43madlpFzG4BQzG2Z3m6tvQDNKdClZnO3VbIudJYmxsT0FNJMeiB2+JTSlTQTSbU8QdesVmwJcmLg== + dependencies: + tmpl "1.0.5" + +media-typer@0.3.0: + version "0.3.0" + resolved "https://registry.yarnpkg.com/media-typer/-/media-typer-0.3.0.tgz#8710d7af0aa626f8fffa1ce00168545263255748" + integrity sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ== + +memfs@^3.4.1: + version "3.5.1" + resolved "https://registry.yarnpkg.com/memfs/-/memfs-3.5.1.tgz#f0cd1e2bfaef58f6fe09bfb9c2288f07fea099ec" + integrity sha512-UWbFJKvj5k+nETdteFndTpYxdeTMox/ULeqX5k/dpaQJCCFmj5EeKv3dBcyO2xmkRAx2vppRu5dVG7SOtsGOzA== + dependencies: + fs-monkey "^1.0.3" + +merge-descriptors@1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/merge-descriptors/-/merge-descriptors-1.0.1.tgz#b00aaa556dd8b44568150ec9d1b953f3f90cbb61" + integrity sha512-cCi6g3/Zr1iqQi6ySbseM1Xvooa98N0w31jzUYrXPX2xqObmFGHJ0tQ5u74H3mVh7wLouTseZyYIq39g8cNp1w== + +merge-stream@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/merge-stream/-/merge-stream-2.0.0.tgz#52823629a14dd00c9770fb6ad47dc6310f2c1f60" + integrity sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w== + +merge2@^1.3.0, merge2@^1.4.1: + version "1.4.1" + resolved "https://registry.yarnpkg.com/merge2/-/merge2-1.4.1.tgz#4368892f885e907455a6fd7dc55c0c9d404990ae" + integrity sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg== + +methods@^1.1.2, methods@~1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/methods/-/methods-1.1.2.tgz#5529a4d67654134edcc5266656835b0f851afcee" + integrity sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w== + +micromatch@^4.0.0, micromatch@^4.0.4: + version "4.0.5" + resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-4.0.5.tgz#bc8999a7cbbf77cdc89f132f6e467051b49090c6" + integrity sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA== + dependencies: + braces "^3.0.2" + picomatch "^2.3.1" + +mime-db@1.52.0: + version "1.52.0" + resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.52.0.tgz#bbabcdc02859f4987301c856e3387ce5ec43bf70" + integrity sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg== + +mime-types@^2.1.12, mime-types@^2.1.27, mime-types@~2.1.24, mime-types@~2.1.34: + version "2.1.35" + resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.35.tgz#381a871b62a734450660ae3deee44813f70d959a" + integrity sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw== + dependencies: + mime-db "1.52.0" + +mime@1.6.0: + version "1.6.0" + resolved "https://registry.yarnpkg.com/mime/-/mime-1.6.0.tgz#32cd9e5c64553bd58d19a568af452acff04981b1" + integrity sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg== + +mime@2.6.0: + version "2.6.0" + resolved "https://registry.yarnpkg.com/mime/-/mime-2.6.0.tgz#a2a682a95cd4d0cb1d6257e28f83da7e35800367" + integrity sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg== + +mimic-fn@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-2.1.0.tgz#7ed2c2ccccaf84d3ffcb7a69b57711fc2083401b" + integrity sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg== + +minimatch@^3.0.4, minimatch@^3.0.5, minimatch@^3.1.1, minimatch@^3.1.2: + version "3.1.2" + resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.1.2.tgz#19cd194bfd3e428f049a70817c038d89ab4be35b" + integrity sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw== + dependencies: + brace-expansion "^1.1.7" + +minimist@1.2.6: + version "1.2.6" + resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.6.tgz#8637a5b759ea0d6e98702cfb3a9283323c93af44" + integrity sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q== + +minimist@^1.2.0, minimist@^1.2.6: + version "1.2.8" + resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.8.tgz#c1a464e7693302e082a075cee0c057741ac4772c" + integrity sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA== + +mkdirp@^0.5.4: + version "0.5.6" + resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.6.tgz#7def03d2432dcae4ba1d611445c48396062255f6" + integrity sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw== + dependencies: + minimist "^1.2.6" + +ms@2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8" + integrity sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A== + +ms@2.1.2: + version "2.1.2" + resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009" + integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w== + +ms@2.1.3: + version "2.1.3" + resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2" + integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA== + +multer@1.4.4-lts.1: + version "1.4.4-lts.1" + resolved "https://registry.yarnpkg.com/multer/-/multer-1.4.4-lts.1.tgz#24100f701a4611211cfae94ae16ea39bb314e04d" + integrity sha512-WeSGziVj6+Z2/MwQo3GvqzgR+9Uc+qt8SwHKh3gvNPiISKfsMfG4SvCOFYlxxgkXt7yIV2i1yczehm0EOKIxIg== + dependencies: + append-field "^1.0.0" + busboy "^1.0.0" + concat-stream "^1.5.2" + mkdirp "^0.5.4" + object-assign "^4.1.1" + type-is "^1.6.4" + xtend "^4.0.0" + +mute-stream@0.0.8: + version "0.0.8" + resolved "https://registry.yarnpkg.com/mute-stream/-/mute-stream-0.0.8.tgz#1630c42b2251ff81e2a283de96a5497ea92e5e0d" + integrity sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA== + +natural-compare-lite@^1.4.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/natural-compare-lite/-/natural-compare-lite-1.4.0.tgz#17b09581988979fddafe0201e931ba933c96cbb4" + integrity sha512-Tj+HTDSJJKaZnfiuw+iaF9skdPpTo2GtEly5JHnWV/hfv2Qj/9RKsGISQtLh2ox3l5EAGw487hnBee0sIJ6v2g== + +natural-compare@^1.4.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/natural-compare/-/natural-compare-1.4.0.tgz#4abebfeed7541f2c27acfb29bdbbd15c8d5ba4f7" + integrity sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw== + +negotiator@0.6.3: + version "0.6.3" + resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-0.6.3.tgz#58e323a72fedc0d6f9cd4d31fe49f51479590ccd" + integrity sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg== + +neo-async@^2.6.2: + version "2.6.2" + resolved "https://registry.yarnpkg.com/neo-async/-/neo-async-2.6.2.tgz#b4aafb93e3aeb2d8174ca53cf163ab7d7308305f" + integrity sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw== + +node-emoji@1.11.0: + version "1.11.0" + resolved "https://registry.yarnpkg.com/node-emoji/-/node-emoji-1.11.0.tgz#69a0150e6946e2f115e9d7ea4df7971e2628301c" + integrity sha512-wo2DpQkQp7Sjm2A0cq+sN7EHKO6Sl0ctXeBdFZrL9T9+UywORbufTcTZxom8YqpLQt/FqNMUkOpkZrJVYSKD3A== + dependencies: + lodash "^4.17.21" + +node-fetch@^2.6.1: + version "2.6.9" + resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.9.tgz#7c7f744b5cc6eb5fd404e0c7a9fec630a55657e6" + integrity sha512-DJm/CJkZkRjKKj4Zi4BsKVZh3ValV5IR5s7LVZnW+6YMh0W1BfNA8XSs6DLMGYlId5F3KnA70uu2qepcR08Qqg== + dependencies: + whatwg-url "^5.0.0" + +node-int64@^0.4.0: + version "0.4.0" + resolved "https://registry.yarnpkg.com/node-int64/-/node-int64-0.4.0.tgz#87a9065cdb355d3182d8f94ce11188b825c68a3b" + integrity sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw== + +node-releases@^2.0.8: + version "2.0.10" + resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-2.0.10.tgz#c311ebae3b6a148c89b1813fd7c4d3c024ef537f" + integrity sha512-5GFldHPXVG/YZmFzJvKK2zDSzPKhEp0+ZR5SVaoSag9fsL5YgHbUHDfnG5494ISANDcK4KwPXAx2xqVEydmd7w== + +normalize-path@^3.0.0, normalize-path@~3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-3.0.0.tgz#0dcd69ff23a1c9b11fd0978316644a0388216a65" + integrity sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA== + +npm-run-path@^4.0.0, npm-run-path@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/npm-run-path/-/npm-run-path-4.0.1.tgz#b7ecd1e5ed53da8e37a55e1c2269e0b97ed748ea" + integrity sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw== + dependencies: + path-key "^3.0.0" + +nwsapi@^2.2.0: + version "2.2.4" + resolved "https://registry.yarnpkg.com/nwsapi/-/nwsapi-2.2.4.tgz#fd59d5e904e8e1f03c25a7d5a15cfa16c714a1e5" + integrity sha512-NHj4rzRo0tQdijE9ZqAx6kYDcoRwYwSYzCA8MY3JzfxlrvEU0jhnhJT9BhqhJs7I/dKcrDm6TyulaRqZPIhN5g== + +object-assign@^4, object-assign@^4.1.1: + version "4.1.1" + resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863" + integrity sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg== + +object-hash@3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/object-hash/-/object-hash-3.0.0.tgz#73f97f753e7baffc0e2cc9d6e079079744ac82e9" + integrity sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw== + +object-inspect@^1.9.0: + version "1.12.3" + resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.12.3.tgz#ba62dffd67ee256c8c086dfae69e016cd1f198b9" + integrity sha512-geUvdk7c+eizMNUDkRpW1wJwgfOiOeHbxBR/hLXK1aT6zmVSO0jsQcs7fj6MGw89jC/cjGfLcNOrtMYtGqm81g== + +on-finished@2.4.1: + version "2.4.1" + resolved "https://registry.yarnpkg.com/on-finished/-/on-finished-2.4.1.tgz#58c8c44116e54845ad57f14ab10b03533184ac3f" + integrity sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg== + dependencies: + ee-first "1.1.1" + +once@^1.3.0, once@^1.3.1, once@^1.4.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1" + integrity sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w== + dependencies: + wrappy "1" + +onetime@^5.1.0, onetime@^5.1.2: + version "5.1.2" + resolved "https://registry.yarnpkg.com/onetime/-/onetime-5.1.2.tgz#d0e96ebb56b07476df1dd9c4806e5237985ca45e" + integrity sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg== + dependencies: + mimic-fn "^2.1.0" + +optionator@^0.8.1: + version "0.8.3" + resolved "https://registry.yarnpkg.com/optionator/-/optionator-0.8.3.tgz#84fa1d036fe9d3c7e21d99884b601167ec8fb495" + integrity sha512-+IW9pACdk3XWmmTXG8m3upGUJst5XRGzxMRjXzAuJ1XnIFNvfhjjIuYkDvysnPQ7qzqVzLt78BCruntqRhWQbA== + dependencies: + deep-is "~0.1.3" + fast-levenshtein "~2.0.6" + levn "~0.3.0" + prelude-ls "~1.1.2" + type-check "~0.3.2" + word-wrap "~1.2.3" + +optionator@^0.9.1: + version "0.9.1" + resolved "https://registry.yarnpkg.com/optionator/-/optionator-0.9.1.tgz#4f236a6373dae0566a6d43e1326674f50c291499" + integrity sha512-74RlY5FCnhq4jRxVUPKDaRwrVNXMqsGsiW6AJw4XK8hmtm10wC0ypZBLw5IIp85NZMr91+qd1RvvENwg7jjRFw== + dependencies: + deep-is "^0.1.3" + fast-levenshtein "^2.0.6" + levn "^0.4.1" + prelude-ls "^1.2.1" + type-check "^0.4.0" + word-wrap "^1.2.3" + +ora@5.4.1, ora@^5.4.1: + version "5.4.1" + resolved "https://registry.yarnpkg.com/ora/-/ora-5.4.1.tgz#1b2678426af4ac4a509008e5e4ac9e9959db9e18" + integrity sha512-5b6Y85tPxZZ7QytO+BQzysW31HJku27cRIlkbAXaNx+BdcVi+LlRFmVXzeF6a7JCwJpyw5c4b+YSVImQIrBpuQ== + dependencies: + bl "^4.1.0" + chalk "^4.1.0" + cli-cursor "^3.1.0" + cli-spinners "^2.5.0" + is-interactive "^1.0.0" + is-unicode-supported "^0.1.0" + log-symbols "^4.1.0" + strip-ansi "^6.0.0" + wcwidth "^1.0.1" + +os-name@4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/os-name/-/os-name-4.0.1.tgz#32cee7823de85a8897647ba4d76db46bf845e555" + integrity sha512-xl9MAoU97MH1Xt5K9ERft2YfCAoaO6msy1OBA0ozxEC0x0TmIoE6K3QvgJMMZA9yKGLmHXNY/YZoDbiGDj4zYw== + dependencies: + macos-release "^2.5.0" + windows-release "^4.0.0" + +os-tmpdir@~1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/os-tmpdir/-/os-tmpdir-1.0.2.tgz#bbe67406c79aa85c5cfec766fe5734555dfa1274" + integrity sha512-D2FR03Vir7FIu45XBY20mTb+/ZSWB00sjU9jdQXt83gDrI4Ztz5Fs7/yy74g2N5SVQY4xY1qDr4rNddwYRVX0g== + +p-limit@^2.2.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-2.3.0.tgz#3dd33c647a214fdfffd835933eb086da0dc21db1" + integrity sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w== + dependencies: + p-try "^2.0.0" + +p-limit@^3.0.2: + version "3.1.0" + resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-3.1.0.tgz#e1daccbe78d0d1388ca18c64fea38e3e57e3706b" + integrity sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ== + dependencies: + yocto-queue "^0.1.0" + +p-locate@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-4.1.0.tgz#a3428bb7088b3a60292f66919278b7c297ad4f07" + integrity sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A== + dependencies: + p-limit "^2.2.0" + +p-locate@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-5.0.0.tgz#83c8315c6785005e3bd021839411c9e110e6d834" + integrity sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw== + dependencies: + p-limit "^3.0.2" + +p-try@^2.0.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/p-try/-/p-try-2.2.0.tgz#cb2868540e313d61de58fafbe35ce9004d5540e6" + integrity sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ== + +parent-module@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/parent-module/-/parent-module-1.0.1.tgz#691d2709e78c79fae3a156622452d00762caaaa2" + integrity sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g== + dependencies: + callsites "^3.0.0" + +parse-json@^5.0.0, parse-json@^5.2.0: + version "5.2.0" + resolved "https://registry.yarnpkg.com/parse-json/-/parse-json-5.2.0.tgz#c76fc66dee54231c962b22bcc8a72cf2f99753cd" + integrity sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg== + dependencies: + "@babel/code-frame" "^7.0.0" + error-ex "^1.3.1" + json-parse-even-better-errors "^2.3.0" + lines-and-columns "^1.1.6" + +parse5@6.0.1: + version "6.0.1" + resolved "https://registry.yarnpkg.com/parse5/-/parse5-6.0.1.tgz#e1a1c085c569b3dc08321184f19a39cc27f7c30b" + integrity sha512-Ofn/CTFzRGTTxwpNEs9PP93gXShHcTq255nzRYSKe8AkVpZY7e1fpmTfOyoIvjP5HG7Z2ZM7VS9PPhQGW2pOpw== + +parseurl@~1.3.3: + version "1.3.3" + resolved "https://registry.yarnpkg.com/parseurl/-/parseurl-1.3.3.tgz#9da19e7bee8d12dff0513ed5b76957793bc2e8d4" + integrity sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ== + +path-exists@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-4.0.0.tgz#513bdbe2d3b95d7762e8c1137efa195c6c61b5b3" + integrity sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w== + +path-is-absolute@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f" + integrity sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg== + +path-key@^3.0.0, path-key@^3.1.0: + version "3.1.1" + resolved "https://registry.yarnpkg.com/path-key/-/path-key-3.1.1.tgz#581f6ade658cbba65a0d3380de7753295054f375" + integrity sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q== + +path-parse@^1.0.7: + version "1.0.7" + resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.7.tgz#fbc114b60ca42b30d9daf5858e4bd68bbedb6735" + integrity sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw== + +path-to-regexp@0.1.7: + version "0.1.7" + resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-0.1.7.tgz#df604178005f522f15eb4490e7247a1bfaa67f8c" + integrity sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ== + +path-to-regexp@3.2.0: + version "3.2.0" + resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-3.2.0.tgz#fa7877ecbc495c601907562222453c43cc204a5f" + integrity sha512-jczvQbCUS7XmS7o+y1aEO9OBVFeZBQ1MDSEqmO7xSoPgOPoowY/SxLpZ6Vh97/8qHZOteiCKb7gkG9gA2ZUxJA== + +path-type@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/path-type/-/path-type-4.0.0.tgz#84ed01c0a7ba380afe09d90a8c180dcd9d03043b" + integrity sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw== + +picocolors@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.0.0.tgz#cb5bdc74ff3f51892236eaf79d68bc44564ab81c" + integrity sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ== + +picomatch@^2.0.4, picomatch@^2.2.1, picomatch@^2.2.3, picomatch@^2.3.1: + version "2.3.1" + resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.3.1.tgz#3ba3833733646d9d3e4995946c1365a67fb07a42" + integrity sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA== + +pirates@^4.0.4: + version "4.0.5" + resolved "https://registry.yarnpkg.com/pirates/-/pirates-4.0.5.tgz#feec352ea5c3268fb23a37c702ab1699f35a5f3b" + integrity sha512-8V9+HQPupnaXMA23c5hvl69zXvTwTzyAYasnkb0Tts4XvO4CliqONMOnvlq26rkhLC3nWDFBJf73LU1e1VZLaQ== + +pkg-dir@^4.2.0: + version "4.2.0" + resolved "https://registry.yarnpkg.com/pkg-dir/-/pkg-dir-4.2.0.tgz#f099133df7ede422e81d1d8448270eeb3e4261f3" + integrity sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ== + dependencies: + find-up "^4.0.0" + +pluralize@8.0.0: + version "8.0.0" + resolved "https://registry.yarnpkg.com/pluralize/-/pluralize-8.0.0.tgz#1a6fa16a38d12a1901e0320fa017051c539ce3b1" + integrity sha512-Nc3IT5yHzflTfbjgqWcCPpo7DaKy4FnpB0l/zCAW0Tc7jxAiuqSxHasntB3D7887LSrA93kDJ9IXovxJYxyLCA== + +prelude-ls@^1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.2.1.tgz#debc6489d7a6e6b0e7611888cec880337d316396" + integrity sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g== + +prelude-ls@~1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.1.2.tgz#21932a549f5e52ffd9a827f570e04be62a97da54" + integrity sha512-ESF23V4SKG6lVSGZgYNpbsiaAkdab6ZgOxe52p7+Kid3W3u3bxR4Vfd/o21dmN7jSt0IwgZ4v5MUd26FEtXE9w== + +prettier-linter-helpers@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/prettier-linter-helpers/-/prettier-linter-helpers-1.0.0.tgz#d23d41fe1375646de2d0104d3454a3008802cf7b" + integrity sha512-GbK2cP9nraSSUF9N2XwUwqfzlAFlMNYYl+ShE/V+H8a9uNl/oUqB1w2EL54Jh0OlyRSd8RfWYJ3coVS4TROP2w== + dependencies: + fast-diff "^1.1.2" + +prettier@^2.3.2: + version "2.8.8" + resolved "https://registry.yarnpkg.com/prettier/-/prettier-2.8.8.tgz#e8c5d7e98a4305ffe3de2e1fc4aca1a71c28b1da" + integrity sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q== + +pretty-format@^27.0.0, pretty-format@^27.5.1: + version "27.5.1" + resolved "https://registry.yarnpkg.com/pretty-format/-/pretty-format-27.5.1.tgz#2181879fdea51a7a5851fb39d920faa63f01d88e" + integrity sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ== + dependencies: + ansi-regex "^5.0.1" + ansi-styles "^5.0.0" + react-is "^17.0.1" + +process-nextick-args@~2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-2.0.1.tgz#7820d9b16120cc55ca9ae7792680ae7dba6d7fe2" + integrity sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag== + +prompts@^2.0.1: + version "2.4.2" + resolved "https://registry.yarnpkg.com/prompts/-/prompts-2.4.2.tgz#7b57e73b3a48029ad10ebd44f74b01722a4cb069" + integrity sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q== + dependencies: + kleur "^3.0.3" + sisteransi "^1.0.5" + +proxy-addr@~2.0.7: + version "2.0.7" + resolved "https://registry.yarnpkg.com/proxy-addr/-/proxy-addr-2.0.7.tgz#f19fe69ceab311eeb94b42e70e8c2070f9ba1025" + integrity sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg== + dependencies: + forwarded "0.2.0" + ipaddr.js "1.9.1" + +psl@^1.1.33: + version "1.9.0" + resolved "https://registry.yarnpkg.com/psl/-/psl-1.9.0.tgz#d0df2a137f00794565fcaf3b2c00cd09f8d5a5a7" + integrity sha512-E/ZsdU4HLs/68gYzgGTkMicWTLPdAftJLfJFlLUAAKZGkStNU72sZjT66SnMDVOfOWY/YAoiD7Jxa9iHvngcag== + +pump@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/pump/-/pump-3.0.0.tgz#b4a2116815bde2f4e1ea602354e8c75565107a64" + integrity sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww== + dependencies: + end-of-stream "^1.1.0" + once "^1.3.1" + +punycode@^2.1.0, punycode@^2.1.1: + version "2.3.0" + resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.3.0.tgz#f67fa67c94da8f4d0cfff981aee4118064199b8f" + integrity sha512-rRV+zQD8tVFys26lAGR9WUuS4iUAngJScM+ZRSKtvl5tKeZ2t5bvdNFdNHBW9FWR4guGHlgmsZ1G7BSm2wTbuA== + +qs@6.10.3: + version "6.10.3" + resolved "https://registry.yarnpkg.com/qs/-/qs-6.10.3.tgz#d6cde1b2ffca87b5aa57889816c5f81535e22e8e" + integrity sha512-wr7M2E0OFRfIfJZjKGieI8lBKb7fRCH4Fv5KNPEs7gJ8jadvotdsS08PzOKR7opXhZ/Xkjtt3WF9g38drmyRqQ== + dependencies: + side-channel "^1.0.4" + +qs@^6.11.0: + version "6.11.1" + resolved "https://registry.yarnpkg.com/qs/-/qs-6.11.1.tgz#6c29dff97f0c0060765911ba65cbc9764186109f" + integrity sha512-0wsrzgTz/kAVIeuxSjnpGC56rzYtr6JT/2BwEvMaPhFIoYa1aGO8LbzuU1R0uUYQkLpWBTOj0l/CLAJB64J6nQ== + dependencies: + side-channel "^1.0.4" + +querystringify@^2.1.1: + version "2.2.0" + resolved "https://registry.yarnpkg.com/querystringify/-/querystringify-2.2.0.tgz#3345941b4153cb9d082d8eee4cda2016a9aef7f6" + integrity sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ== + +queue-microtask@^1.2.2: + version "1.2.3" + resolved "https://registry.yarnpkg.com/queue-microtask/-/queue-microtask-1.2.3.tgz#4929228bbc724dfac43e0efb058caf7b6cfb6243" + integrity sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A== + +randombytes@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/randombytes/-/randombytes-2.1.0.tgz#df6f84372f0270dc65cdf6291349ab7a473d4f2a" + integrity sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ== + dependencies: + safe-buffer "^5.1.0" + +range-parser@~1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/range-parser/-/range-parser-1.2.1.tgz#3cf37023d199e1c24d1a55b84800c2f3e6468031" + integrity sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg== + +raw-body@2.5.1: + version "2.5.1" + resolved "https://registry.yarnpkg.com/raw-body/-/raw-body-2.5.1.tgz#fe1b1628b181b700215e5fd42389f98b71392857" + integrity sha512-qqJBtEyVgS0ZmPGdCFPWJ3FreoqvG4MVQln/kCgF7Olq95IbOp0/BWyMwbdtn4VTvkM8Y7khCQ2Xgk/tcrCXig== + dependencies: + bytes "3.1.2" + http-errors "2.0.0" + iconv-lite "0.4.24" + unpipe "1.0.0" + +react-is@^17.0.1: + version "17.0.2" + resolved "https://registry.yarnpkg.com/react-is/-/react-is-17.0.2.tgz#e691d4a8e9c789365655539ab372762b0efb54f0" + integrity sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w== + +readable-stream@^2.2.2: + version "2.3.8" + resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.8.tgz#91125e8042bba1b9887f49345f6277027ce8be9b" + integrity sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA== + dependencies: + core-util-is "~1.0.0" + inherits "~2.0.3" + isarray "~1.0.0" + process-nextick-args "~2.0.0" + safe-buffer "~5.1.1" + string_decoder "~1.1.1" + util-deprecate "~1.0.1" + +readable-stream@^3.4.0: + version "3.6.2" + resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-3.6.2.tgz#56a9b36ea965c00c5a93ef31eb111a0f11056967" + integrity sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA== + dependencies: + inherits "^2.0.3" + string_decoder "^1.1.1" + util-deprecate "^1.0.1" + +readdirp@~3.6.0: + version "3.6.0" + resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-3.6.0.tgz#74a370bd857116e245b29cc97340cd431a02a6c7" + integrity sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA== + dependencies: + picomatch "^2.2.1" + +rechoir@^0.6.2: + version "0.6.2" + resolved "https://registry.yarnpkg.com/rechoir/-/rechoir-0.6.2.tgz#85204b54dba82d5742e28c96756ef43af50e3384" + integrity sha512-HFM8rkZ+i3zrV+4LQjwQ0W+ez98pApMGM3HUrN04j3CqzPOzl9nmP15Y8YXNm8QHGv/eacOVEjqhmWpkRV0NAw== + dependencies: + resolve "^1.1.6" + +reflect-metadata@^0.1.13: + version "0.1.13" + resolved "https://registry.yarnpkg.com/reflect-metadata/-/reflect-metadata-0.1.13.tgz#67ae3ca57c972a2aa1642b10fe363fe32d49dc08" + integrity sha512-Ts1Y/anZELhSsjMcU605fU9RE4Oi3p5ORujwbIKXfWa+0Zxs510Qrmrce5/Jowq3cHSZSJqBjypxmHarc+vEWg== + +require-directory@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/require-directory/-/require-directory-2.1.1.tgz#8c64ad5fd30dab1c976e2344ffe7f792a6a6df42" + integrity sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q== + +require-from-string@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/require-from-string/-/require-from-string-2.0.2.tgz#89a7fdd938261267318eafe14f9c32e598c36909" + integrity sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw== + +requires-port@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/requires-port/-/requires-port-1.0.0.tgz#925d2601d39ac485e091cf0da5c6e694dc3dcaff" + integrity sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ== + +resolve-cwd@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/resolve-cwd/-/resolve-cwd-3.0.0.tgz#0f0075f1bb2544766cf73ba6a6e2adfebcb13f2d" + integrity sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg== + dependencies: + resolve-from "^5.0.0" + +resolve-from@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-4.0.0.tgz#4abcd852ad32dd7baabfe9b40e00a36db5f392e6" + integrity sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g== + +resolve-from@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-5.0.0.tgz#c35225843df8f776df21c57557bc087e9dfdfc69" + integrity sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw== + +resolve.exports@^1.1.0: + version "1.1.1" + resolved "https://registry.yarnpkg.com/resolve.exports/-/resolve.exports-1.1.1.tgz#05cfd5b3edf641571fd46fa608b610dda9ead999" + integrity sha512-/NtpHNDN7jWhAaQ9BvBUYZ6YTXsRBgfqWFWP7BZBaoMJO/I3G5OFzvTuWNlZC3aPjins1F+TNrLKsGbH4rfsRQ== + +resolve@^1.1.6, resolve@^1.20.0: + version "1.22.2" + resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.22.2.tgz#0ed0943d4e301867955766c9f3e1ae6d01c6845f" + integrity sha512-Sb+mjNHOULsBv818T40qSPeRiuWLyaGMa5ewydRLFimneixmVy2zdivRl+AF6jaYPC8ERxGDmFSiqui6SfPd+g== + dependencies: + is-core-module "^2.11.0" + path-parse "^1.0.7" + supports-preserve-symlinks-flag "^1.0.0" + +restore-cursor@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/restore-cursor/-/restore-cursor-3.1.0.tgz#39f67c54b3a7a58cea5236d95cf0034239631f7e" + integrity sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA== + dependencies: + onetime "^5.1.0" + signal-exit "^3.0.2" + +reusify@^1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/reusify/-/reusify-1.0.4.tgz#90da382b1e126efc02146e90845a88db12925d76" + integrity sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw== + +rimraf@3.0.2, rimraf@^3.0.0, rimraf@^3.0.2: + version "3.0.2" + resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-3.0.2.tgz#f1a5402ba6220ad52cc1282bac1ae3aa49fd061a" + integrity sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA== + dependencies: + glob "^7.1.3" + +run-async@^2.4.0: + version "2.4.1" + resolved "https://registry.yarnpkg.com/run-async/-/run-async-2.4.1.tgz#8440eccf99ea3e70bd409d49aab88e10c189a455" + integrity sha512-tvVnVv01b8c1RrA6Ep7JkStj85Guv/YrMcwqYQnwjsAS2cTmmPGBBjAjpCW7RrSodNSoE2/qg9O4bceNvUuDgQ== + +run-parallel@^1.1.9: + version "1.2.0" + resolved "https://registry.yarnpkg.com/run-parallel/-/run-parallel-1.2.0.tgz#66d1368da7bdf921eb9d95bd1a9229e7f21a43ee" + integrity sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA== + dependencies: + queue-microtask "^1.2.2" + +rxjs@6.6.7, rxjs@^6.6.0: + version "6.6.7" + resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-6.6.7.tgz#90ac018acabf491bf65044235d5863c4dab804c9" + integrity sha512-hTdwr+7yYNIT5n4AMYp85KA6yw2Va0FLa3Rguvbpa4W3I5xynaBZo41cM3XM+4Q6fRMj3sBYIR1VAmZMXYJvRQ== + dependencies: + tslib "^1.9.0" + +rxjs@^7.2.0: + version "7.8.1" + resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-7.8.1.tgz#6f6f3d99ea8044291efd92e7c7fcf562c4057543" + integrity sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg== + dependencies: + tslib "^2.1.0" + +safe-buffer@5.2.1, safe-buffer@^5.1.0, safe-buffer@~5.2.0: + version "5.2.1" + resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6" + integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ== + +safe-buffer@~5.1.0, safe-buffer@~5.1.1: + version "5.1.2" + resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d" + integrity sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g== + +"safer-buffer@>= 2.1.2 < 3": + version "2.1.2" + resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a" + integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg== + +saxes@^5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/saxes/-/saxes-5.0.1.tgz#eebab953fa3b7608dbe94e5dadb15c888fa6696d" + integrity sha512-5LBh1Tls8c9xgGjw3QrMwETmTMVk0oFgvrFSvWx62llR2hcEInrKNZ2GZCCuuy2lvWrdl5jhbpeqc5hRYKFOcw== + dependencies: + xmlchars "^2.2.0" + +schema-utils@^3.1.0, schema-utils@^3.1.1: + version "3.1.2" + resolved "https://registry.yarnpkg.com/schema-utils/-/schema-utils-3.1.2.tgz#36c10abca6f7577aeae136c804b0c741edeadc99" + integrity sha512-pvjEHOgWc9OWA/f/DE3ohBWTD6EleVLf7iFUkoSwAxttdBhB9QUebQgxER2kWueOvRJXPHNnyrvvh9eZINB8Eg== + dependencies: + "@types/json-schema" "^7.0.8" + ajv "^6.12.5" + ajv-keywords "^3.5.2" + +semver@7.x, semver@^7.3.2, semver@^7.3.4, semver@^7.3.5, semver@^7.3.7, semver@^7.3.8: + version "7.5.0" + resolved "https://registry.yarnpkg.com/semver/-/semver-7.5.0.tgz#ed8c5dc8efb6c629c88b23d41dc9bf40c1d96cd0" + integrity sha512-+XC0AD/R7Q2mPSRuy2Id0+CGTZ98+8f+KvwirxOKIEyid+XSx6HbC63p+O4IndTHuX5Z+JxQ0TghCkO5Cg/2HA== + dependencies: + lru-cache "^6.0.0" + +semver@^6.0.0, semver@^6.3.0: + version "6.3.0" + resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.0.tgz#ee0a64c8af5e8ceea67687b133761e1becbd1d3d" + integrity sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw== + +send@0.18.0: + version "0.18.0" + resolved "https://registry.yarnpkg.com/send/-/send-0.18.0.tgz#670167cc654b05f5aa4a767f9113bb371bc706be" + integrity sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg== + dependencies: + debug "2.6.9" + depd "2.0.0" + destroy "1.2.0" + encodeurl "~1.0.2" + escape-html "~1.0.3" + etag "~1.8.1" + fresh "0.5.2" + http-errors "2.0.0" + mime "1.6.0" + ms "2.1.3" + on-finished "2.4.1" + range-parser "~1.2.1" + statuses "2.0.1" + +serialize-javascript@^6.0.1: + version "6.0.1" + resolved "https://registry.yarnpkg.com/serialize-javascript/-/serialize-javascript-6.0.1.tgz#b206efb27c3da0b0ab6b52f48d170b7996458e5c" + integrity sha512-owoXEFjWRllis8/M1Q+Cw5k8ZH40e3zhp/ovX+Xr/vi1qj6QesbyXXViFbpNvWvPNAD62SutwEXavefrLJWj7w== + dependencies: + randombytes "^2.1.0" + +serve-static@1.15.0: + version "1.15.0" + resolved "https://registry.yarnpkg.com/serve-static/-/serve-static-1.15.0.tgz#faaef08cffe0a1a62f60cad0c4e513cff0ac9540" + integrity sha512-XGuRDNjXUijsUL0vl6nSD7cwURuzEgglbOaFuZM9g3kwDXOWVTck0jLzjPzGD+TazWbboZYu52/9/XPdUgne9g== + dependencies: + encodeurl "~1.0.2" + escape-html "~1.0.3" + parseurl "~1.3.3" + send "0.18.0" + +setprototypeof@1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.2.0.tgz#66c9a24a73f9fc28cbe66b09fed3d33dcaf1b424" + integrity sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw== + +shebang-command@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/shebang-command/-/shebang-command-2.0.0.tgz#ccd0af4f8835fbdc265b82461aaf0c36663f34ea" + integrity sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA== + dependencies: + shebang-regex "^3.0.0" + +shebang-regex@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-3.0.0.tgz#ae16f1644d873ecad843b0307b143362d4c42172" + integrity sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A== + +shelljs@0.8.5: + version "0.8.5" + resolved "https://registry.yarnpkg.com/shelljs/-/shelljs-0.8.5.tgz#de055408d8361bed66c669d2f000538ced8ee20c" + integrity sha512-TiwcRcrkhHvbrZbnRcFYMLl30Dfov3HKqzp5tO5b4pt6G/SezKcYhmDg15zXVBswHmctSAQKznqNW2LO5tTDow== + dependencies: + glob "^7.0.0" + interpret "^1.0.0" + rechoir "^0.6.2" + +side-channel@^1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/side-channel/-/side-channel-1.0.4.tgz#efce5c8fdc104ee751b25c58d4290011fa5ea2cf" + integrity sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw== + dependencies: + call-bind "^1.0.0" + get-intrinsic "^1.0.2" + object-inspect "^1.9.0" + +signal-exit@^3.0.2, signal-exit@^3.0.3: + version "3.0.7" + resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.7.tgz#a9a1767f8af84155114eaabd73f99273c8f59ad9" + integrity sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ== + +sisteransi@^1.0.5: + version "1.0.5" + resolved "https://registry.yarnpkg.com/sisteransi/-/sisteransi-1.0.5.tgz#134d681297756437cc05ca01370d3a7a571075ed" + integrity sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg== + +slash@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/slash/-/slash-3.0.0.tgz#6539be870c165adbd5240220dbe361f1bc4d4634" + integrity sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q== + +source-map-support@0.5.21, source-map-support@^0.5.20, source-map-support@^0.5.6, source-map-support@~0.5.20: + version "0.5.21" + resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.5.21.tgz#04fe7c7f9e1ed2d662233c28cb2b35b9f63f6e4f" + integrity sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w== + dependencies: + buffer-from "^1.0.0" + source-map "^0.6.0" + +source-map@0.7.3: + version "0.7.3" + resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.7.3.tgz#5302f8169031735226544092e64981f751750383" + integrity sha512-CkCj6giN3S+n9qrYiBTX5gystlENnRW5jZeNLHpe6aue+SrHcG5VYwujhW9s4dY31mEGsxBDrHR6oI69fTXsaQ== + +source-map@^0.6.0, source-map@^0.6.1, source-map@~0.6.1: + version "0.6.1" + resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.6.1.tgz#74722af32e9614e9c287a8d0bbde48b5e2f1a263" + integrity sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g== + +source-map@^0.7.3: + version "0.7.4" + resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.7.4.tgz#a9bbe705c9d8846f4e08ff6765acf0f1b0898656" + integrity sha512-l3BikUxvPOcn5E74dZiq5BGsTb5yEwhaTSzccU6t4sDOH8NWJCstKO5QT2CvtFoK6F0saL7p9xHAqHOlCPJygA== + +sourcemap-codec@^1.4.4: + version "1.4.8" + resolved "https://registry.yarnpkg.com/sourcemap-codec/-/sourcemap-codec-1.4.8.tgz#ea804bd94857402e6992d05a38ef1ae35a9ab4c4" + integrity sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA== + +sprintf-js@~1.0.2: + version "1.0.3" + resolved "https://registry.yarnpkg.com/sprintf-js/-/sprintf-js-1.0.3.tgz#04e6926f662895354f3dd015203633b857297e2c" + integrity sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g== + +stack-utils@^2.0.3: + version "2.0.6" + resolved "https://registry.yarnpkg.com/stack-utils/-/stack-utils-2.0.6.tgz#aaf0748169c02fc33c8232abccf933f54a1cc34f" + integrity sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ== + dependencies: + escape-string-regexp "^2.0.0" + +statuses@2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/statuses/-/statuses-2.0.1.tgz#55cb000ccf1d48728bd23c685a063998cf1a1b63" + integrity sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ== + +streamsearch@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/streamsearch/-/streamsearch-1.1.0.tgz#404dd1e2247ca94af554e841a8ef0eaa238da764" + integrity sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg== + +string-length@^4.0.1: + version "4.0.2" + resolved "https://registry.yarnpkg.com/string-length/-/string-length-4.0.2.tgz#a8a8dc7bd5c1a82b9b3c8b87e125f66871b6e57a" + integrity sha512-+l6rNN5fYHNhZZy41RXsYptCjA2Igmq4EG7kZAYFQI1E1VTXarr6ZPXBg6eq7Y6eK4FEhY6AJlyuFIb/v/S0VQ== + dependencies: + char-regex "^1.0.2" + strip-ansi "^6.0.0" + +string-width@^4.1.0, string-width@^4.2.0: + version "4.2.3" + resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" + integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== + dependencies: + emoji-regex "^8.0.0" + is-fullwidth-code-point "^3.0.0" + strip-ansi "^6.0.1" + +string_decoder@^1.1.1: + version "1.3.0" + resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.3.0.tgz#42f114594a46cf1a8e30b0a84f56c78c3edac21e" + integrity sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA== + dependencies: + safe-buffer "~5.2.0" + +string_decoder@~1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.1.1.tgz#9cf1611ba62685d7030ae9e4ba34149c3af03fc8" + integrity sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg== + dependencies: + safe-buffer "~5.1.0" + +strip-ansi@^6.0.0, strip-ansi@^6.0.1: + version "6.0.1" + resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" + integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== + dependencies: + ansi-regex "^5.0.1" + +strip-bom@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/strip-bom/-/strip-bom-3.0.0.tgz#2334c18e9c759f7bdd56fdef7e9ae3d588e68ed3" + integrity sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA== + +strip-bom@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/strip-bom/-/strip-bom-4.0.0.tgz#9c3505c1db45bcedca3d9cf7a16f5c5aa3901878" + integrity sha512-3xurFv5tEgii33Zi8Jtp55wEIILR9eh34FAW00PZf+JnSsTmV/ioewSgQl97JHvgjoRGwPShsWm+IdrxB35d0w== + +strip-final-newline@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/strip-final-newline/-/strip-final-newline-2.0.0.tgz#89b852fb2fcbe936f6f4b3187afb0a12c1ab58ad" + integrity sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA== + +strip-json-comments@^3.1.0, strip-json-comments@^3.1.1: + version "3.1.1" + resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-3.1.1.tgz#31f1281b3832630434831c310c01cccda8cbe006" + integrity sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig== + +superagent@^8.0.5: + version "8.0.9" + resolved "https://registry.yarnpkg.com/superagent/-/superagent-8.0.9.tgz#2c6fda6fadb40516515f93e9098c0eb1602e0535" + integrity sha512-4C7Bh5pyHTvU33KpZgwrNKh/VQnvgtCSqPRfJAUdmrtSYePVzVg4E4OzsrbkhJj9O7SO6Bnv75K/F8XVZT8YHA== + dependencies: + component-emitter "^1.3.0" + cookiejar "^2.1.4" + debug "^4.3.4" + fast-safe-stringify "^2.1.1" + form-data "^4.0.0" + formidable "^2.1.2" + methods "^1.1.2" + mime "2.6.0" + qs "^6.11.0" + semver "^7.3.8" + +supertest@^6.1.3: + version "6.3.3" + resolved "https://registry.yarnpkg.com/supertest/-/supertest-6.3.3.tgz#42f4da199fee656106fd422c094cf6c9578141db" + integrity sha512-EMCG6G8gDu5qEqRQ3JjjPs6+FYT1a7Hv5ApHvtSghmOFJYtsU5S+pSb6Y2EUeCEY3CmEL3mmQ8YWlPOzQomabA== + dependencies: + methods "^1.1.2" + superagent "^8.0.5" + +supports-color@^5.3.0: + version "5.5.0" + resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-5.5.0.tgz#e2e69a44ac8772f78a1ec0b35b689df6530efc8f" + integrity sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow== + dependencies: + has-flag "^3.0.0" + +supports-color@^7.0.0, supports-color@^7.1.0: + version "7.2.0" + resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-7.2.0.tgz#1b7dcdcb32b8138801b3e478ba6a51caa89648da" + integrity sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw== + dependencies: + has-flag "^4.0.0" + +supports-color@^8.0.0: + version "8.1.1" + resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-8.1.1.tgz#cd6fc17e28500cff56c1b86c0a7fd4a54a73005c" + integrity sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q== + dependencies: + has-flag "^4.0.0" + +supports-hyperlinks@^2.0.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/supports-hyperlinks/-/supports-hyperlinks-2.3.0.tgz#3943544347c1ff90b15effb03fc14ae45ec10624" + integrity sha512-RpsAZlpWcDwOPQA22aCH4J0t7L8JmAvsCxfOSEwm7cQs3LshN36QaTkwd70DnBOXDWGssw2eUoc8CaRWT0XunA== + dependencies: + has-flag "^4.0.0" + supports-color "^7.0.0" + +supports-preserve-symlinks-flag@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz#6eda4bd344a3c94aea376d4cc31bc77311039e09" + integrity sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w== + +symbol-observable@4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/symbol-observable/-/symbol-observable-4.0.0.tgz#5b425f192279e87f2f9b937ac8540d1984b39205" + integrity sha512-b19dMThMV4HVFynSAM1++gBHAbk2Tc/osgLIBZMKsyqh34jb2e8Os7T6ZW/Bt3pJFdBTd2JwAnAAEQV7rSNvcQ== + +symbol-tree@^3.2.4: + version "3.2.4" + resolved "https://registry.yarnpkg.com/symbol-tree/-/symbol-tree-3.2.4.tgz#430637d248ba77e078883951fb9aa0eed7c63fa2" + integrity sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw== + +tapable@^2.1.1, tapable@^2.2.0, tapable@^2.2.1: + version "2.2.1" + resolved "https://registry.yarnpkg.com/tapable/-/tapable-2.2.1.tgz#1967a73ef4060a82f12ab96af86d52fdb76eeca0" + integrity sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ== + +terminal-link@^2.0.0: + version "2.1.1" + resolved "https://registry.yarnpkg.com/terminal-link/-/terminal-link-2.1.1.tgz#14a64a27ab3c0df933ea546fba55f2d078edc994" + integrity sha512-un0FmiRUQNr5PJqy9kP7c40F5BOfpGlYTrxonDChEZB7pzZxRNp/bt+ymiy9/npwXya9KH99nJ/GXFIiUkYGFQ== + dependencies: + ansi-escapes "^4.2.1" + supports-hyperlinks "^2.0.0" + +terser-webpack-plugin@^5.1.3: + version "5.3.8" + resolved "https://registry.yarnpkg.com/terser-webpack-plugin/-/terser-webpack-plugin-5.3.8.tgz#415e03d2508f7de63d59eca85c5d102838f06610" + integrity sha512-WiHL3ElchZMsK27P8uIUh4604IgJyAW47LVXGbEoB21DbQcZ+OuMpGjVYnEUaqcWM6dO8uS2qUbA7LSCWqvsbg== + dependencies: + "@jridgewell/trace-mapping" "^0.3.17" + jest-worker "^27.4.5" + schema-utils "^3.1.1" + serialize-javascript "^6.0.1" + terser "^5.16.8" + +terser@^5.16.8: + version "5.17.2" + resolved "https://registry.yarnpkg.com/terser/-/terser-5.17.2.tgz#06c9818ae998066234b985abeb57bb7bff29d449" + integrity sha512-1D1aGbOF1Mnayq5PvfMc0amAR1y5Z1nrZaGCvI5xsdEfZEVte8okonk02OiaK5fw5hG1GWuuVsakOnpZW8y25A== + dependencies: + "@jridgewell/source-map" "^0.3.2" + acorn "^8.5.0" + commander "^2.20.0" + source-map-support "~0.5.20" + +test-exclude@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/test-exclude/-/test-exclude-6.0.0.tgz#04a8698661d805ea6fa293b6cb9e63ac044ef15e" + integrity sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w== + dependencies: + "@istanbuljs/schema" "^0.1.2" + glob "^7.1.4" + minimatch "^3.0.4" + +text-table@^0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/text-table/-/text-table-0.2.0.tgz#7f5ee823ae805207c00af2df4a84ec3fcfa570b4" + integrity sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw== + +throat@^6.0.1: + version "6.0.2" + resolved "https://registry.yarnpkg.com/throat/-/throat-6.0.2.tgz#51a3fbb5e11ae72e2cf74861ed5c8020f89f29fe" + integrity sha512-WKexMoJj3vEuK0yFEapj8y64V0A6xcuPuK9Gt1d0R+dzCSJc0lHqQytAbSB4cDAK0dWh4T0E2ETkoLE2WZ41OQ== + +through@^2.3.6: + version "2.3.8" + resolved "https://registry.yarnpkg.com/through/-/through-2.3.8.tgz#0dd4c9ffaabc357960b1b724115d7e0e86a2e1f5" + integrity sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg== + +tmp@^0.0.33: + version "0.0.33" + resolved "https://registry.yarnpkg.com/tmp/-/tmp-0.0.33.tgz#6d34335889768d21b2bcda0aa277ced3b1bfadf9" + integrity sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw== + dependencies: + os-tmpdir "~1.0.2" + +tmpl@1.0.5: + version "1.0.5" + resolved "https://registry.yarnpkg.com/tmpl/-/tmpl-1.0.5.tgz#8683e0b902bb9c20c4f726e3c0b69f36518c07cc" + integrity sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw== + +to-fast-properties@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/to-fast-properties/-/to-fast-properties-2.0.0.tgz#dc5e698cbd079265bc73e0377681a4e4e83f616e" + integrity sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog== + +to-regex-range@^5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/to-regex-range/-/to-regex-range-5.0.1.tgz#1648c44aae7c8d988a326018ed72f5b4dd0392e4" + integrity sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ== + dependencies: + is-number "^7.0.0" + +toidentifier@1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/toidentifier/-/toidentifier-1.0.1.tgz#3be34321a88a820ed1bd80dfaa33e479fbb8dd35" + integrity sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA== + +tough-cookie@^4.0.0: + version "4.1.2" + resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-4.1.2.tgz#e53e84b85f24e0b65dd526f46628db6c85f6b874" + integrity sha512-G9fqXWoYFZgTc2z8Q5zaHy/vJMjm+WV0AkAeHxVCQiEB1b+dGvWzFW6QV07cY5jQ5gRkeid2qIkzkxUnmoQZUQ== + dependencies: + psl "^1.1.33" + punycode "^2.1.1" + universalify "^0.2.0" + url-parse "^1.5.3" + +tr46@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/tr46/-/tr46-2.1.0.tgz#fa87aa81ca5d5941da8cbf1f9b749dc969a4e240" + integrity sha512-15Ih7phfcdP5YxqiB+iDtLoaTz4Nd35+IiAv0kQ5FNKHzXgdWqPoTIqEDDJmXceQt4JZk6lVPT8lnDlPpGDppw== + dependencies: + punycode "^2.1.1" + +tr46@~0.0.3: + version "0.0.3" + resolved "https://registry.yarnpkg.com/tr46/-/tr46-0.0.3.tgz#8184fd347dac9cdc185992f3a6622e14b9d9ab6a" + integrity sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw== + +tree-kill@1.2.2: + version "1.2.2" + resolved "https://registry.yarnpkg.com/tree-kill/-/tree-kill-1.2.2.tgz#4ca09a9092c88b73a7cdc5e8a01b507b0790a0cc" + integrity sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A== + +ts-jest@^27.0.3: + version "27.1.5" + resolved "https://registry.yarnpkg.com/ts-jest/-/ts-jest-27.1.5.tgz#0ddf1b163fbaae3d5b7504a1e65c914a95cff297" + integrity sha512-Xv6jBQPoBEvBq/5i2TeSG9tt/nqkbpcurrEG1b+2yfBrcJelOZF9Ml6dmyMh7bcW9JyFbRYpR5rxROSlBLTZHA== + dependencies: + bs-logger "0.x" + fast-json-stable-stringify "2.x" + jest-util "^27.0.0" + json5 "2.x" + lodash.memoize "4.x" + make-error "1.x" + semver "7.x" + yargs-parser "20.x" + +ts-loader@^9.2.3: + version "9.4.2" + resolved "https://registry.yarnpkg.com/ts-loader/-/ts-loader-9.4.2.tgz#80a45eee92dd5170b900b3d00abcfa14949aeb78" + integrity sha512-OmlC4WVmFv5I0PpaxYb+qGeGOdm5giHU7HwDDUjw59emP2UYMHy9fFSDcYgSNoH8sXcj4hGCSEhlDZ9ULeDraA== + dependencies: + chalk "^4.1.0" + enhanced-resolve "^5.0.0" + micromatch "^4.0.0" + semver "^7.3.4" + +ts-node@^10.0.0: + version "10.9.1" + resolved "https://registry.yarnpkg.com/ts-node/-/ts-node-10.9.1.tgz#e73de9102958af9e1f0b168a6ff320e25adcff4b" + integrity sha512-NtVysVPkxxrwFGUUxGYhfux8k78pQB3JqYBXlLRZgdGUqTO5wU/UyHop5p70iEbGhB7q5KmiZiU0Y3KlJrScEw== + dependencies: + "@cspotcode/source-map-support" "^0.8.0" + "@tsconfig/node10" "^1.0.7" + "@tsconfig/node12" "^1.0.7" + "@tsconfig/node14" "^1.0.0" + "@tsconfig/node16" "^1.0.2" + acorn "^8.4.1" + acorn-walk "^8.1.1" + arg "^4.1.0" + create-require "^1.1.0" + diff "^4.0.1" + make-error "^1.1.1" + v8-compile-cache-lib "^3.0.1" + yn "3.1.1" + +tsconfig-paths-webpack-plugin@3.5.2: + version "3.5.2" + resolved "https://registry.yarnpkg.com/tsconfig-paths-webpack-plugin/-/tsconfig-paths-webpack-plugin-3.5.2.tgz#01aafff59130c04a8c4ebc96a3045c43c376449a" + integrity sha512-EhnfjHbzm5IYI9YPNVIxx1moxMI4bpHD2e0zTXeDNQcwjjRaGepP7IhTHJkyDBG0CAOoxRfe7jCG630Ou+C6Pw== + dependencies: + chalk "^4.1.0" + enhanced-resolve "^5.7.0" + tsconfig-paths "^3.9.0" + +tsconfig-paths@3.14.1: + version "3.14.1" + resolved "https://registry.yarnpkg.com/tsconfig-paths/-/tsconfig-paths-3.14.1.tgz#ba0734599e8ea36c862798e920bcf163277b137a" + integrity sha512-fxDhWnFSLt3VuTwtvJt5fpwxBHg5AdKWMsgcPOOIilyjymcYVZoCQF8fvFRezCNfblEXmi+PcM1eYHeOAgXCOQ== + dependencies: + "@types/json5" "^0.0.29" + json5 "^1.0.1" + minimist "^1.2.6" + strip-bom "^3.0.0" + +tsconfig-paths@^3.10.1, tsconfig-paths@^3.9.0: + version "3.14.2" + resolved "https://registry.yarnpkg.com/tsconfig-paths/-/tsconfig-paths-3.14.2.tgz#6e32f1f79412decd261f92d633a9dc1cfa99f088" + integrity sha512-o/9iXgCYc5L/JxCHPe3Hvh8Q/2xm5Z+p18PESBU6Ff33695QnCHBEjcytY2q19ua7Mbl/DavtBOLq+oG0RCL+g== + dependencies: + "@types/json5" "^0.0.29" + json5 "^1.0.2" + minimist "^1.2.6" + strip-bom "^3.0.0" + +tslib@2.4.0: + version "2.4.0" + resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.4.0.tgz#7cecaa7f073ce680a05847aa77be941098f36dc3" + integrity sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ== + +tslib@^1.8.1, tslib@^1.9.0: + version "1.14.1" + resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.14.1.tgz#cf2d38bdc34a134bcaf1091c41f6619e2f672d00" + integrity sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg== + +tslib@^2.1.0: + version "2.5.0" + resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.5.0.tgz#42bfed86f5787aeb41d031866c8f402429e0fddf" + integrity sha512-336iVw3rtn2BUK7ORdIAHTyxHGRIHVReokCR3XjbckJMK7ms8FysBfhLR8IXnAgy7T0PTPNBWKiH514FOW/WSg== + +tsutils@^3.21.0: + version "3.21.0" + resolved "https://registry.yarnpkg.com/tsutils/-/tsutils-3.21.0.tgz#b48717d394cea6c1e096983eed58e9d61715b623" + integrity sha512-mHKK3iUXL+3UF6xL5k0PEhKRUBKPBCv/+RkEOpjRWxxx27KKRBmmA60A9pgOUvMi8GKhRMPEmjBRPzs2W7O1OA== + dependencies: + tslib "^1.8.1" + +type-check@^0.4.0, type-check@~0.4.0: + version "0.4.0" + resolved "https://registry.yarnpkg.com/type-check/-/type-check-0.4.0.tgz#07b8203bfa7056c0657050e3ccd2c37730bab8f1" + integrity sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew== + dependencies: + prelude-ls "^1.2.1" + +type-check@~0.3.2: + version "0.3.2" + resolved "https://registry.yarnpkg.com/type-check/-/type-check-0.3.2.tgz#5884cab512cf1d355e3fb784f30804b2b520db72" + integrity sha512-ZCmOJdvOWDBYJlzAoFkC+Q0+bUyEOS1ltgp1MGU03fqHG+dbi9tBFU2Rd9QKiDZFAYrhPh2JUf7rZRIuHRKtOg== + dependencies: + prelude-ls "~1.1.2" + +type-detect@4.0.8: + version "4.0.8" + resolved "https://registry.yarnpkg.com/type-detect/-/type-detect-4.0.8.tgz#7646fb5f18871cfbb7749e69bd39a6388eb7450c" + integrity sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g== + +type-fest@^0.20.2: + version "0.20.2" + resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.20.2.tgz#1bf207f4b28f91583666cb5fbd327887301cd5f4" + integrity sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ== + +type-fest@^0.21.3: + version "0.21.3" + resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.21.3.tgz#d260a24b0198436e133fa26a524a6d65fa3b2e37" + integrity sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w== + +type-is@^1.6.4, type-is@~1.6.18: + version "1.6.18" + resolved "https://registry.yarnpkg.com/type-is/-/type-is-1.6.18.tgz#4e552cd05df09467dcbc4ef739de89f2cf37c131" + integrity sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g== + dependencies: + media-typer "0.3.0" + mime-types "~2.1.24" + +typedarray-to-buffer@^3.1.5: + version "3.1.5" + resolved "https://registry.yarnpkg.com/typedarray-to-buffer/-/typedarray-to-buffer-3.1.5.tgz#a97ee7a9ff42691b9f783ff1bc5112fe3fca9080" + integrity sha512-zdu8XMNEDepKKR+XYOXAVPtWui0ly0NtohUscw+UmaHiAWT8hrV1rr//H6V+0DvJ3OQ19S979M0laLfX8rm82Q== + dependencies: + is-typedarray "^1.0.0" + +typedarray@^0.0.6: + version "0.0.6" + resolved "https://registry.yarnpkg.com/typedarray/-/typedarray-0.0.6.tgz#867ac74e3864187b1d3d47d996a78ec5c8830777" + integrity sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA== + +typescript@4.7.4: + version "4.7.4" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.7.4.tgz#1a88596d1cf47d59507a1bcdfb5b9dfe4d488235" + integrity sha512-C0WQT0gezHuw6AdY1M2jxUO83Rjf0HP7Sk1DtXj6j1EwkQNZrHAg2XPWlq62oqEhYvONq5pkC2Y9oPljWToLmQ== + +typescript@^4.3.5: + version "4.9.5" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.9.5.tgz#095979f9bcc0d09da324d58d03ce8f8374cbe65a" + integrity sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g== + +universalify@^0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/universalify/-/universalify-0.2.0.tgz#6451760566fa857534745ab1dde952d1b1761be0" + integrity sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg== + +universalify@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/universalify/-/universalify-2.0.0.tgz#75a4984efedc4b08975c5aeb73f530d02df25717" + integrity sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ== + +unpipe@1.0.0, unpipe@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/unpipe/-/unpipe-1.0.0.tgz#b2bf4ee8514aae6165b4817829d21b2ef49904ec" + integrity sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ== + +update-browserslist-db@^1.0.10: + version "1.0.11" + resolved "https://registry.yarnpkg.com/update-browserslist-db/-/update-browserslist-db-1.0.11.tgz#9a2a641ad2907ae7b3616506f4b977851db5b940" + integrity sha512-dCwEFf0/oT85M1fHBg4F0jtLwJrutGoHSQXCh7u4o2t1drG+c0a9Flnqww6XUKSfQMPpJBRjU8d4RXB09qtvaA== + dependencies: + escalade "^3.1.1" + picocolors "^1.0.0" + +uri-js@^4.2.2: + version "4.4.1" + resolved "https://registry.yarnpkg.com/uri-js/-/uri-js-4.4.1.tgz#9b1a52595225859e55f669d928f88c6c57f2a77e" + integrity sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg== + dependencies: + punycode "^2.1.0" + +url-parse@^1.5.3: + version "1.5.10" + resolved "https://registry.yarnpkg.com/url-parse/-/url-parse-1.5.10.tgz#9d3c2f736c1d75dd3bd2be507dcc111f1e2ea9c1" + integrity sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ== + dependencies: + querystringify "^2.1.1" + requires-port "^1.0.0" + +util-deprecate@^1.0.1, util-deprecate@~1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf" + integrity sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw== + +utils-merge@1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/utils-merge/-/utils-merge-1.0.1.tgz#9f95710f50a267947b2ccc124741c1028427e713" + integrity sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA== + +uuid@8.3.2: + version "8.3.2" + resolved "https://registry.yarnpkg.com/uuid/-/uuid-8.3.2.tgz#80d5b5ced271bb9af6c445f21a1a04c606cefbe2" + integrity sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg== + +v8-compile-cache-lib@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz#6336e8d71965cb3d35a1bbb7868445a7c05264bf" + integrity sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg== + +v8-to-istanbul@^8.1.0: + version "8.1.1" + resolved "https://registry.yarnpkg.com/v8-to-istanbul/-/v8-to-istanbul-8.1.1.tgz#77b752fd3975e31bbcef938f85e9bd1c7a8d60ed" + integrity sha512-FGtKtv3xIpR6BYhvgH8MI/y78oT7d8Au3ww4QIxymrCtZEh5b8gCw2siywE+puhEmuWKDtmfrvF5UlB298ut3w== + dependencies: + "@types/istanbul-lib-coverage" "^2.0.1" + convert-source-map "^1.6.0" + source-map "^0.7.3" + +vary@^1, vary@~1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/vary/-/vary-1.1.2.tgz#2299f02c6ded30d4a5961b0b9f74524a18f634fc" + integrity sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg== + +w3c-hr-time@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/w3c-hr-time/-/w3c-hr-time-1.0.2.tgz#0a89cdf5cc15822df9c360543676963e0cc308cd" + integrity sha512-z8P5DvDNjKDoFIHK7q8r8lackT6l+jo/Ye3HOle7l9nICP9lf1Ci25fy9vHd0JOWewkIFzXIEig3TdKT7JQ5fQ== + dependencies: + browser-process-hrtime "^1.0.0" + +w3c-xmlserializer@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/w3c-xmlserializer/-/w3c-xmlserializer-2.0.0.tgz#3e7104a05b75146cc60f564380b7f683acf1020a" + integrity sha512-4tzD0mF8iSiMiNs30BiLO3EpfGLZUT2MSX/G+o7ZywDzliWQ3OPtTZ0PTC3B3ca1UAf4cJMHB+2Bf56EriJuRA== + dependencies: + xml-name-validator "^3.0.0" + +walker@^1.0.7: + version "1.0.8" + resolved "https://registry.yarnpkg.com/walker/-/walker-1.0.8.tgz#bd498db477afe573dc04185f011d3ab8a8d7653f" + integrity sha512-ts/8E8l5b7kY0vlWLewOkDXMmPdLcVV4GmOQLyxuSswIJsweeFZtAsMF7k1Nszz+TYBQrlYRmzOnr398y1JemQ== + dependencies: + makeerror "1.0.12" + +watchpack@^2.3.1: + version "2.4.0" + resolved "https://registry.yarnpkg.com/watchpack/-/watchpack-2.4.0.tgz#fa33032374962c78113f93c7f2fb4c54c9862a5d" + integrity sha512-Lcvm7MGST/4fup+ifyKi2hjyIAwcdI4HRgtvTpIUxBRhB+RFtUh8XtDOxUfctVCnhVi+QQj49i91OyvzkJl6cg== + dependencies: + glob-to-regexp "^0.4.1" + graceful-fs "^4.1.2" + +wcwidth@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/wcwidth/-/wcwidth-1.0.1.tgz#f0b0dcf915bc5ff1528afadb2c0e17b532da2fe8" + integrity sha512-XHPEwS0q6TaxcvG85+8EYkbiCux2XtWG2mkc47Ng2A77BQu9+DqIOJldST4HgPkuea7dvKSj5VgX3P1d4rW8Tg== + dependencies: + defaults "^1.0.3" + +webidl-conversions@^3.0.0: + version "3.0.1" + resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-3.0.1.tgz#24534275e2a7bc6be7bc86611cc16ae0a5654871" + integrity sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ== + +webidl-conversions@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-5.0.0.tgz#ae59c8a00b121543a2acc65c0434f57b0fc11aff" + integrity sha512-VlZwKPCkYKxQgeSbH5EyngOmRp7Ww7I9rQLERETtf5ofd9pGeswWiOtogpEO850jziPRarreGxn5QIiTqpb2wA== + +webidl-conversions@^6.1.0: + version "6.1.0" + resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-6.1.0.tgz#9111b4d7ea80acd40f5270d666621afa78b69514" + integrity sha512-qBIvFLGiBpLjfwmYAaHPXsn+ho5xZnGvyGvsarywGNc8VyQJUMHJ8OBKGGrPER0okBeMDaan4mNBlgBROxuI8w== + +webpack-node-externals@3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/webpack-node-externals/-/webpack-node-externals-3.0.0.tgz#1a3407c158d547a9feb4229a9e3385b7b60c9917" + integrity sha512-LnL6Z3GGDPht/AigwRh2dvL9PQPFQ8skEpVrWZXLWBYmqcaojHNN0onvHzie6rq7EWKrrBfPYqNEzTJgiwEQDQ== + +webpack-sources@^3.2.3: + version "3.2.3" + resolved "https://registry.yarnpkg.com/webpack-sources/-/webpack-sources-3.2.3.tgz#2d4daab8451fd4b240cc27055ff6a0c2ccea0cde" + integrity sha512-/DyMEOrDgLKKIG0fmvtz+4dUX/3Ghozwgm6iPp8KRhvn+eQf9+Q7GWxVNMk3+uCPWfdXYC4ExGBckIXdFEfH1w== + +webpack@5.73.0: + version "5.73.0" + resolved "https://registry.yarnpkg.com/webpack/-/webpack-5.73.0.tgz#bbd17738f8a53ee5760ea2f59dce7f3431d35d38" + integrity sha512-svjudQRPPa0YiOYa2lM/Gacw0r6PvxptHj4FuEKQ2kX05ZLkjbVc5MnPs6its5j7IZljnIqSVo/OsY2X0IpHGA== + dependencies: + "@types/eslint-scope" "^3.7.3" + "@types/estree" "^0.0.51" + "@webassemblyjs/ast" "1.11.1" + "@webassemblyjs/wasm-edit" "1.11.1" + "@webassemblyjs/wasm-parser" "1.11.1" + acorn "^8.4.1" + acorn-import-assertions "^1.7.6" + browserslist "^4.14.5" + chrome-trace-event "^1.0.2" + enhanced-resolve "^5.9.3" + es-module-lexer "^0.9.0" + eslint-scope "5.1.1" + events "^3.2.0" + glob-to-regexp "^0.4.1" + graceful-fs "^4.2.9" + json-parse-even-better-errors "^2.3.1" + loader-runner "^4.2.0" + mime-types "^2.1.27" + neo-async "^2.6.2" + schema-utils "^3.1.0" + tapable "^2.1.1" + terser-webpack-plugin "^5.1.3" + watchpack "^2.3.1" + webpack-sources "^3.2.3" + +whatwg-encoding@^1.0.5: + version "1.0.5" + resolved "https://registry.yarnpkg.com/whatwg-encoding/-/whatwg-encoding-1.0.5.tgz#5abacf777c32166a51d085d6b4f3e7d27113ddb0" + integrity sha512-b5lim54JOPN9HtzvK9HFXvBma/rnfFeqsic0hSpjtDbVxR3dJKLc+KB4V6GgiGOvl7CY/KNh8rxSo9DKQrnUEw== + dependencies: + iconv-lite "0.4.24" + +whatwg-mimetype@^2.3.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/whatwg-mimetype/-/whatwg-mimetype-2.3.0.tgz#3d4b1e0312d2079879f826aff18dbeeca5960fbf" + integrity sha512-M4yMwr6mAnQz76TbJm914+gPpB/nCwvZbJU28cUD6dR004SAxDLOOSUaB1JDRqLtaOV/vi0IC5lEAGFgrjGv/g== + +whatwg-url@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/whatwg-url/-/whatwg-url-5.0.0.tgz#966454e8765462e37644d3626f6742ce8b70965d" + integrity sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw== + dependencies: + tr46 "~0.0.3" + webidl-conversions "^3.0.0" + +whatwg-url@^8.0.0, whatwg-url@^8.5.0: + version "8.7.0" + resolved "https://registry.yarnpkg.com/whatwg-url/-/whatwg-url-8.7.0.tgz#656a78e510ff8f3937bc0bcbe9f5c0ac35941b77" + integrity sha512-gAojqb/m9Q8a5IV96E3fHJM70AzCkgt4uXYX2O7EmuyOnLrViCQlsEBmF9UQIu3/aeAIp2U17rtbpZWNntQqdg== + dependencies: + lodash "^4.7.0" + tr46 "^2.1.0" + webidl-conversions "^6.1.0" + +which@^2.0.1: + version "2.0.2" + resolved "https://registry.yarnpkg.com/which/-/which-2.0.2.tgz#7c6a8dd0a636a0327e10b59c9286eee93f3f51b1" + integrity sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA== + dependencies: + isexe "^2.0.0" + +windows-release@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/windows-release/-/windows-release-4.0.0.tgz#4725ec70217d1bf6e02c7772413b29cdde9ec377" + integrity sha512-OxmV4wzDKB1x7AZaZgXMVsdJ1qER1ed83ZrTYd5Bwq2HfJVg3DJS8nqlAG4sMoJ7mu8cuRmLEYyU13BKwctRAg== + dependencies: + execa "^4.0.2" + +word-wrap@^1.2.3, word-wrap@~1.2.3: + version "1.2.3" + resolved "https://registry.yarnpkg.com/word-wrap/-/word-wrap-1.2.3.tgz#610636f6b1f703891bd34771ccb17fb93b47079c" + integrity sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ== + +wrap-ansi@^7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" + integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== + dependencies: + ansi-styles "^4.0.0" + string-width "^4.1.0" + strip-ansi "^6.0.0" + +wrappy@1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f" + integrity sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ== + +write-file-atomic@^3.0.0: + version "3.0.3" + resolved "https://registry.yarnpkg.com/write-file-atomic/-/write-file-atomic-3.0.3.tgz#56bd5c5a5c70481cd19c571bd39ab965a5de56e8" + integrity sha512-AvHcyZ5JnSfq3ioSyjrBkH9yW4m7Ayk8/9My/DD9onKeu/94fwrMocemO2QAJFAlnnDN+ZDS+ZjAR5ua1/PV/Q== + dependencies: + imurmurhash "^0.1.4" + is-typedarray "^1.0.0" + signal-exit "^3.0.2" + typedarray-to-buffer "^3.1.5" + +ws@^7.4.6: + version "7.5.9" + resolved "https://registry.yarnpkg.com/ws/-/ws-7.5.9.tgz#54fa7db29f4c7cec68b1ddd3a89de099942bb591" + integrity sha512-F+P9Jil7UiSKSkppIiD94dN07AwvFixvLIj1Og1Rl9GGMuNipJnV9JzjD6XuqmAeiswGvUmNLjr5cFuXwNS77Q== + +xml-name-validator@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/xml-name-validator/-/xml-name-validator-3.0.0.tgz#6ae73e06de4d8c6e47f9fb181f78d648ad457c6a" + integrity sha512-A5CUptxDsvxKJEU3yO6DuWBSJz/qizqzJKOMIfUJHETbBw/sFaDxgd6fxm1ewUaM0jZ444Fc5vC5ROYurg/4Pw== + +xmlchars@^2.2.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/xmlchars/-/xmlchars-2.2.0.tgz#060fe1bcb7f9c76fe2a17db86a9bc3ab894210cb" + integrity sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw== + +xtend@^4.0.0: + version "4.0.2" + resolved "https://registry.yarnpkg.com/xtend/-/xtend-4.0.2.tgz#bb72779f5fa465186b1f438f674fa347fdb5db54" + integrity sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ== + +y18n@^5.0.5: + version "5.0.8" + resolved "https://registry.yarnpkg.com/y18n/-/y18n-5.0.8.tgz#7f4934d0f7ca8c56f95314939ddcd2dd91ce1d55" + integrity sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA== + +yallist@^3.0.2: + version "3.1.1" + resolved "https://registry.yarnpkg.com/yallist/-/yallist-3.1.1.tgz#dbb7daf9bfd8bac9ab45ebf602b8cbad0d5d08fd" + integrity sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g== + +yallist@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/yallist/-/yallist-4.0.0.tgz#9bb92790d9c0effec63be73519e11a35019a3a72" + integrity sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A== + +yaml@^1.10.0: + version "1.10.2" + resolved "https://registry.yarnpkg.com/yaml/-/yaml-1.10.2.tgz#2301c5ffbf12b467de8da2333a459e29e7920e4b" + integrity sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg== + +yargs-parser@20.x, yargs-parser@^20.2.2: + version "20.2.9" + resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-20.2.9.tgz#2eb7dc3b0289718fc295f362753845c41a0c94ee" + integrity sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w== + +yargs@^16.2.0: + version "16.2.0" + resolved "https://registry.yarnpkg.com/yargs/-/yargs-16.2.0.tgz#1c82bf0f6b6a66eafce7ef30e376f49a12477f66" + integrity sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw== + dependencies: + cliui "^7.0.2" + escalade "^3.1.1" + get-caller-file "^2.0.5" + require-directory "^2.1.1" + string-width "^4.2.0" + y18n "^5.0.5" + yargs-parser "^20.2.2" + +yn@3.1.1: + version "3.1.1" + resolved "https://registry.yarnpkg.com/yn/-/yn-3.1.1.tgz#1e87401a09d767c1d5eab26a6e4c185182d2eb50" + integrity sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q== + +yocto-queue@^0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/yocto-queue/-/yocto-queue-0.1.0.tgz#0294eb3dee05028d31ee1a5fa2c556a6aaf10a1b" + integrity sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q== From 63a0a9d338f28b48d3fc697aba606e1f1b7104b3 Mon Sep 17 00:00:00 2001 From: Yazeed Loonat Date: Tue, 23 May 2023 09:05:11 -0700 Subject: [PATCH 03/57] feat: prisma schema generation (#3429) * feat: prisma schema generation * fix: cleaning up removed "models" * fix: updates per emily * fix: caught some schema errors --- backend_new/README.md | 90 +- backend_new/package.json | 5 +- .../20230521220941_init/migration.sql | 1151 +++++++++++++++++ .../prisma/migrations/migration_lock.toml | 3 + backend_new/prisma/schema.prisma | 1030 +++++++++++++++ backend_new/yarn.lock | 24 + 6 files changed, 2235 insertions(+), 68 deletions(-) create mode 100644 backend_new/prisma/migrations/20230521220941_init/migration.sql create mode 100644 backend_new/prisma/migrations/migration_lock.toml create mode 100644 backend_new/prisma/schema.prisma diff --git a/backend_new/README.md b/backend_new/README.md index 9fe8812ff8..68d69cc16a 100644 --- a/backend_new/README.md +++ b/backend_new/README.md @@ -1,73 +1,29 @@ -

- Nest Logo -

- -[circleci-image]: https://img.shields.io/circleci/build/github/nestjs/nest/master?token=abc123def456 -[circleci-url]: https://circleci.com/gh/nestjs/nest - -

A progressive Node.js framework for building efficient and scalable server-side applications.

-

-NPM Version -Package License -NPM Downloads -CircleCI -Coverage -Discord -Backers on Open Collective -Sponsors on Open Collective - - Support us - -

- - -## Description - -[Nest](https://github.com/nestjs/nest) framework TypeScript starter repository. - +# Setup ## Installation +Make sure the .env file's db placement is what works for your set up, Then run the following: ```bash -$ npm install -``` - -## Running the app - -```bash -# development -$ npm run start - -# watch mode -$ npm run start:dev - -# production mode -$ npm run start:prod +$ yarn install +$ yarn db:setup ``` -## Test - -```bash -# unit tests -$ npm run test - -# e2e tests -$ npm run test:e2e - -# test coverage -$ npm run test:cov -``` - -## Support - -Nest is an MIT-licensed open source project. It can grow thanks to the sponsors and support by the amazing backers. If you'd like to join them, please [read more here](https://docs.nestjs.com/support). - -## Stay in touch - -- Author - [Kamil Myƛliwiec](https://kamilmysliwiec.com) -- Website - [https://nestjs.com](https://nestjs.com/) -- Twitter - [@nestframework](https://twitter.com/nestframework) - -## License -Nest is [MIT licensed](LICENSE). +# Modifying the Schema + +We use [Prisma](https://www.prisma.io/) as the ORM. To modify the schema you will need to work with the schema.prisma file. This file controls the following: +
    +
  1. The Structure of each model (entity if you are more familiar with TypeORM)
  2. +
  3. The Relationships between models
  4. +
  5. Enum creation for use in both the API and the database
  6. +
  7. How Prisma connects to the database
  8. +
+ +## Conventions +We use the following conventions: +
    +
  • model and enum names are capitalized camelcased (e.g. HelloWorld)
  • +
  • model and enum names are @@map()ed to lowercase snakecased (e.g. hello_world)
  • +
  • a model's fields are lowercase camelcased (e.g. helloWorld)
  • +
  • a model's fields are @map()ed to lowercase snackcased (e.g. hello_world)
  • +
+This is to make the api easier to work with, and to respect postgres's name space conventions diff --git a/backend_new/package.json b/backend_new/package.json index eb0c067838..8f032b41a1 100644 --- a/backend_new/package.json +++ b/backend_new/package.json @@ -18,12 +18,15 @@ "test:watch": "jest --watch", "test:cov": "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" + "test:e2e": "jest --config ./test/jest-e2e.json", + "db:setup": "psql -c 'DROP DATABASE IF EXISTS bloom_prisma;' && psql -c 'CREATE DATABASE bloom_prisma;' && psql -d bloom_prisma -c 'CREATE EXTENSION IF NOT EXISTS \"uuid-ossp\";' && yarn prisma migrate deploy" }, "dependencies": { "@nestjs/common": "^8.0.0", "@nestjs/core": "^8.0.0", "@nestjs/platform-express": "^8.0.0", + "@prisma/client": "4.13.0", + "prisma": "^4.13.0", "reflect-metadata": "^0.1.13", "rimraf": "^3.0.2", "rxjs": "^7.2.0" diff --git a/backend_new/prisma/migrations/20230521220941_init/migration.sql b/backend_new/prisma/migrations/20230521220941_init/migration.sql new file mode 100644 index 0000000000..73970a579c --- /dev/null +++ b/backend_new/prisma/migrations/20230521220941_init/migration.sql @@ -0,0 +1,1151 @@ +-- CreateEnum +CREATE TYPE "application_methods_type_enum" AS ENUM ('Internal', 'FileDownload', 'ExternalLink', 'PaperPickup', 'POBox', 'LeasingAgent', 'Referral'); + +-- CreateEnum +CREATE TYPE "languages_enum" AS ENUM ('en', 'es', 'vi', 'zh', 'tl'); + +-- CreateEnum +CREATE TYPE "listing_events_type_enum" AS ENUM ('openHouse', 'publicLottery', 'lotteryResults'); + +-- CreateEnum +CREATE TYPE "listings_application_address_type_enum" AS ENUM ('leasingAgent'); + +-- CreateEnum +CREATE TYPE "listings_review_order_type_enum" AS ENUM ('lottery', 'firstComeFirstServe', 'waitlist'); + +-- CreateEnum +CREATE TYPE "listings_status_enum" AS ENUM ('active', 'pending', 'closed'); + +-- CreateEnum +CREATE TYPE "multiselect_questions_application_section_enum" AS ENUM ('programs', 'preferences'); + +-- CreateEnum +CREATE TYPE "yes_no_enum" AS ENUM ('yes', 'no'); + +-- CreateEnum +CREATE TYPE "rule_enum" AS ENUM ('nameAndDOB', 'email'); + +-- CreateEnum +CREATE TYPE "flagged_set_status_enum" AS ENUM ('flagged', 'pending', 'resolved'); + +-- CreateEnum +CREATE TYPE "income_period_enum" AS ENUM ('perMonth', 'perYear'); + +-- CreateEnum +CREATE TYPE "application_status_enum" AS ENUM ('draft', 'submitted', 'removed'); + +-- CreateEnum +CREATE TYPE "application_submission_type_enum" AS ENUM ('paper', 'electronical'); + +-- CreateEnum +CREATE TYPE "application_review_status_enum" AS ENUM ('pending', 'pendingAndValid', 'valid', 'duplicate'); + +-- CreateEnum +CREATE TYPE "units_status_enum" AS ENUM ('unknown', 'available', 'occupied', 'unavailable'); + +-- CreateEnum +CREATE TYPE "listings_home_type_enum" AS ENUM ('apartment', 'duplex', 'house', 'townhome'); + +-- CreateEnum +CREATE TYPE "listings_marketing_season_enum" AS ENUM ('spring', 'summer', 'fall', 'winter'); + +-- CreateEnum +CREATE TYPE "listings_marketing_type_enum" AS ENUM ('marketing', 'comingSoon'); + +-- CreateEnum +CREATE TYPE "property_region_enum" AS ENUM ('Greater_Downtown', 'Eastside', 'Southwest', 'Westside'); + +-- CreateEnum +CREATE TYPE "monthly_rent_determination_type_enum" AS ENUM ('flatRent', 'percentageOfIncome'); + +-- CreateTable +CREATE TABLE "accessibility" ( + "id" UUID NOT NULL DEFAULT uuid_generate_v4(), + "created_at" TIMESTAMP(6) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updated_at" TIMESTAMP(6) NOT NULL, + "mobility" BOOLEAN, + "vision" BOOLEAN, + "hearing" BOOLEAN, + + CONSTRAINT "accessibility_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "activity_log" ( + "id" UUID NOT NULL DEFAULT uuid_generate_v4(), + "created_at" TIMESTAMP(6) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updated_at" TIMESTAMP(6) NOT NULL, + "module" VARCHAR NOT NULL, + "record_id" UUID NOT NULL, + "action" VARCHAR NOT NULL, + "metadata" JSONB, + "user_id" UUID, + + CONSTRAINT "activity_log_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "address" ( + "id" UUID NOT NULL DEFAULT uuid_generate_v4(), + "created_at" TIMESTAMP(6) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updated_at" TIMESTAMP(6) NOT NULL, + "place_name" TEXT, + "city" TEXT, + "county" TEXT, + "state" TEXT, + "street" TEXT, + "street2" TEXT, + "zip_code" TEXT, + "latitude" DECIMAL, + "longitude" DECIMAL, + + CONSTRAINT "address_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "alternate_contact" ( + "id" UUID NOT NULL DEFAULT uuid_generate_v4(), + "created_at" TIMESTAMP(6) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updated_at" TIMESTAMP(6) NOT NULL, + "type" TEXT, + "other_type" TEXT, + "first_name" TEXT, + "last_name" TEXT, + "agency" TEXT, + "phone_number" TEXT, + "email_address" TEXT, + "mailing_address_id" UUID, + + CONSTRAINT "alternate_contact_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "ami_chart" ( + "id" UUID NOT NULL DEFAULT uuid_generate_v4(), + "created_at" TIMESTAMP(6) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updated_at" TIMESTAMP(6) NOT NULL, + "items" JSONB NOT NULL, + "name" VARCHAR NOT NULL, + "jurisdiction_id" UUID NOT NULL, + + CONSTRAINT "ami_chart_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "applicant" ( + "id" UUID NOT NULL DEFAULT uuid_generate_v4(), + "created_at" TIMESTAMP(6) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updated_at" TIMESTAMP(6) NOT NULL, + "first_name" TEXT, + "middle_name" TEXT, + "last_name" TEXT, + "birth_month" TEXT, + "birth_day" TEXT, + "birth_year" TEXT, + "email_address" TEXT, + "no_email" BOOLEAN, + "phone_number" TEXT, + "phone_number_type" TEXT, + "no_phone" BOOLEAN, + "work_in_region" "yes_no_enum", + "work_address_id" UUID, + "address_id" UUID, + + CONSTRAINT "applicant_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "application_flagged_set" ( + "id" UUID NOT NULL DEFAULT uuid_generate_v4(), + "created_at" TIMESTAMP(6) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updated_at" TIMESTAMP(6) NOT NULL, + "rule" "rule_enum" NOT NULL, + "rule_key" VARCHAR NOT NULL, + "resolved_time" TIMESTAMPTZ(6), + "listing_id" UUID NOT NULL, + "show_confirmation_alert" BOOLEAN NOT NULL DEFAULT false, + "status" "flagged_set_status_enum" NOT NULL DEFAULT 'pending', + "resolving_user_id" UUID, + + CONSTRAINT "application_flagged_set_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "application_methods" ( + "id" UUID NOT NULL DEFAULT uuid_generate_v4(), + "created_at" TIMESTAMP(6) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updated_at" TIMESTAMP(6) NOT NULL, + "type" "application_methods_type_enum" NOT NULL, + "label" TEXT, + "external_reference" TEXT, + "accepts_postmarked_applications" BOOLEAN, + "phone_number" TEXT, + "listing_id" UUID, + + CONSTRAINT "application_methods_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "applications" ( + "id" UUID NOT NULL DEFAULT uuid_generate_v4(), + "created_at" TIMESTAMP(6) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updated_at" TIMESTAMP(6) NOT NULL, + "deleted_at" TIMESTAMP(6), + "app_url" TEXT, + "additional_phone" BOOLEAN, + "additional_phone_number" TEXT, + "additional_phone_number_type" TEXT, + "contact_preferences" TEXT[], + "household_size" INTEGER, + "housing_status" TEXT, + "send_mail_to_mailing_address" BOOLEAN, + "household_expecting_changes" BOOLEAN, + "household_student" BOOLEAN, + "income_vouchers" BOOLEAN, + "income" TEXT, + "income_period" "income_period_enum", + "preferences" JSONB NOT NULL, + "programs" JSONB, + "status" "application_status_enum" NOT NULL, + "language" "languages_enum", + "submission_type" "application_submission_type_enum" NOT NULL, + "accepted_terms" BOOLEAN, + "submission_date" TIMESTAMPTZ(6), + "marked_as_duplicate" BOOLEAN NOT NULL DEFAULT false, + "confirmation_code" TEXT NOT NULL, + "review_status" "application_review_status_enum" NOT NULL DEFAULT 'valid', + "user_id" UUID, + "listing_id" UUID, + "applicant_id" UUID, + "mailing_address_id" UUID, + "alternate_address_id" UUID, + "alternate_contact_id" UUID, + "accessibility_id" UUID, + "demographics_id" UUID, + + CONSTRAINT "applications_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "assets" ( + "id" UUID NOT NULL DEFAULT uuid_generate_v4(), + "created_at" TIMESTAMP(6) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updated_at" TIMESTAMP(6) NOT NULL, + "file_id" TEXT NOT NULL, + "label" TEXT NOT NULL, + + CONSTRAINT "assets_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "cron_job" ( + "id" UUID NOT NULL DEFAULT uuid_generate_v4(), + "name" TEXT, + "last_run_date" TIMESTAMPTZ(6), + "created_at" TIMESTAMP(6) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updated_at" TIMESTAMP(6) NOT NULL, + + CONSTRAINT "cron_job_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "demographics" ( + "id" UUID NOT NULL DEFAULT uuid_generate_v4(), + "created_at" TIMESTAMP(6) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updated_at" TIMESTAMP(6) NOT NULL, + "ethnicity" TEXT, + "gender" TEXT, + "sexual_orientation" TEXT, + "how_did_you_hear" TEXT[], + "race" TEXT[], + + CONSTRAINT "demographics_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "generated_listing_translations" ( + "id" UUID NOT NULL DEFAULT uuid_generate_v4(), + "created_at" TIMESTAMP(6) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updated_at" TIMESTAMP(6) NOT NULL, + "listing_id" VARCHAR NOT NULL, + "jurisdiction_id" VARCHAR NOT NULL, + "language" "languages_enum" NOT NULL, + "translations" JSONB NOT NULL, + "timestamp" TIMESTAMP(6) NOT NULL, + + CONSTRAINT "generated_listing_translations_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "household_member" ( + "id" UUID NOT NULL DEFAULT uuid_generate_v4(), + "created_at" TIMESTAMP(6) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updated_at" TIMESTAMP(6) NOT NULL, + "order_id" INTEGER, + "first_name" TEXT, + "middle_name" TEXT, + "last_name" TEXT, + "birth_month" TEXT, + "birth_day" TEXT, + "birth_year" TEXT, + "email_address" TEXT, + "phone_number" TEXT, + "phone_number_type" TEXT, + "no_phone" BOOLEAN, + "same_address" "yes_no_enum" NOT NULL, + "relationship" TEXT, + "work_in_region" "yes_no_enum" NOT NULL, + "address_id" UUID, + "work_address_id" UUID, + "application_id" UUID, + + CONSTRAINT "household_member_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "jurisdictions" ( + "id" UUID NOT NULL DEFAULT uuid_generate_v4(), + "created_at" TIMESTAMP(6) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updated_at" TIMESTAMP(6) NOT NULL, + "name" TEXT NOT NULL, + "notifications_sign_up_url" TEXT, + "languages" "languages_enum"[] DEFAULT ARRAY['en']::"languages_enum"[], + "partner_terms" TEXT, + "public_url" TEXT NOT NULL DEFAULT '', + "email_from_address" TEXT, + "rental_assistance_default" TEXT NOT NULL, + "enable_partner_settings" BOOLEAN NOT NULL DEFAULT false, + "enable_accessibility_features" BOOLEAN NOT NULL DEFAULT false, + "enable_utilities_included" BOOLEAN NOT NULL DEFAULT false, + + CONSTRAINT "jurisdictions_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "listing_events" ( + "id" UUID NOT NULL DEFAULT uuid_generate_v4(), + "created_at" TIMESTAMP(6) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updated_at" TIMESTAMP(6) NOT NULL, + "type" "listing_events_type_enum" NOT NULL, + "start_date" TIMESTAMPTZ(6), + "start_time" TIMESTAMPTZ(6), + "end_time" TIMESTAMPTZ(6), + "url" TEXT, + "note" TEXT, + "label" TEXT, + "listing_id" UUID, + "file_id" UUID, + + CONSTRAINT "listing_events_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "listing_features" ( + "id" UUID NOT NULL DEFAULT uuid_generate_v4(), + "created_at" TIMESTAMP(6) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updated_at" TIMESTAMP(6) NOT NULL, + "elevator" BOOLEAN, + "wheelchair_ramp" BOOLEAN, + "service_animals_allowed" BOOLEAN, + "accessible_parking" BOOLEAN, + "parking_on_site" BOOLEAN, + "in_unit_washer_dryer" BOOLEAN, + "laundry_in_building" BOOLEAN, + "barrier_free_entrance" BOOLEAN, + "roll_in_shower" BOOLEAN, + "grab_bars" BOOLEAN, + "heating_in_unit" BOOLEAN, + "ac_in_unit" BOOLEAN, + "hearing" BOOLEAN, + "visual" BOOLEAN, + "mobility" BOOLEAN, + "barrier_free_unit_entrance" BOOLEAN, + "lowered_light_switch" BOOLEAN, + "barrier_free_bathroom" BOOLEAN, + "wide_doorways" BOOLEAN, + "lowered_cabinets" BOOLEAN, + + CONSTRAINT "listing_features_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "listing_images" ( + "ordinal" INTEGER, + "listing_id" UUID NOT NULL, + "image_id" UUID NOT NULL, + + CONSTRAINT "listing_images_pkey" PRIMARY KEY ("listing_id","image_id") +); + +-- CreateTable +CREATE TABLE "listing_multiselect_questions" ( + "ordinal" INTEGER, + "listing_id" UUID NOT NULL, + "multiselect_question_id" UUID NOT NULL, + + CONSTRAINT "listing_multiselect_questions_pkey" PRIMARY KEY ("listing_id","multiselect_question_id") +); + +-- CreateTable +CREATE TABLE "listing_utilities" ( + "id" UUID NOT NULL DEFAULT uuid_generate_v4(), + "created_at" TIMESTAMP(6) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updated_at" TIMESTAMP(6) NOT NULL, + "water" BOOLEAN, + "gas" BOOLEAN, + "trash" BOOLEAN, + "sewer" BOOLEAN, + "electricity" BOOLEAN, + "cable" BOOLEAN, + "phone" BOOLEAN, + "internet" BOOLEAN, + + CONSTRAINT "listing_utilities_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "listings" ( + "id" UUID NOT NULL DEFAULT uuid_generate_v4(), + "created_at" TIMESTAMP(6) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updated_at" TIMESTAMP(6) NOT NULL, + "additional_application_submission_notes" TEXT, + "digital_application" BOOLEAN, + "common_digital_application" BOOLEAN, + "paper_application" BOOLEAN, + "referral_opportunity" BOOLEAN, + "assets" JSONB NOT NULL, + "accessibility" TEXT, + "amenities" TEXT, + "building_total_units" INTEGER, + "developer" TEXT, + "household_size_max" INTEGER, + "household_size_min" INTEGER, + "neighborhood" TEXT, + "pet_policy" TEXT, + "smoking_policy" TEXT, + "units_available" INTEGER, + "unit_amenities" TEXT, + "services_offered" TEXT, + "year_built" INTEGER, + "application_due_date" TIMESTAMPTZ(6), + "application_open_date" TIMESTAMPTZ(6), + "application_fee" TEXT, + "application_organization" TEXT, + "application_pick_up_address_office_hours" TEXT, + "application_pick_up_address_type" "listings_application_address_type_enum", + "application_drop_off_address_office_hours" TEXT, + "application_drop_off_address_type" "listings_application_address_type_enum", + "application_mailing_address_type" "listings_application_address_type_enum", + "building_selection_criteria" TEXT, + "costs_not_included" TEXT, + "credit_history" TEXT, + "criminal_background" TEXT, + "deposit_min" TEXT, + "deposit_max" TEXT, + "deposit_helper_text" TEXT, + "disable_units_accordion" BOOLEAN, + "leasing_agent_email" TEXT, + "leasing_agent_name" TEXT, + "leasing_agent_office_hours" TEXT, + "leasing_agent_phone" TEXT, + "leasing_agent_title" TEXT, + "name" TEXT NOT NULL, + "postmarked_applications_received_by_date" TIMESTAMPTZ(6), + "program_rules" TEXT, + "rental_assistance" TEXT, + "rental_history" TEXT, + "required_documents" TEXT, + "special_notes" TEXT, + "waitlist_current_size" INTEGER, + "waitlist_max_size" INTEGER, + "what_to_expect" TEXT, + "status" "listings_status_enum" NOT NULL DEFAULT 'pending', + "review_order_type" "listings_review_order_type_enum", + "display_waitlist_size" BOOLEAN NOT NULL, + "reserved_community_description" TEXT, + "reserved_community_min_age" INTEGER, + "result_link" TEXT, + "is_waitlist_open" BOOLEAN, + "waitlist_open_spots" INTEGER, + "custom_map_pin" BOOLEAN, + "published_at" TIMESTAMPTZ(6), + "closed_at" TIMESTAMPTZ(6), + "afs_last_run_at" TIMESTAMPTZ(6) DEFAULT '1970-01-01 00:00:00-07'::timestamp with time zone, + "last_application_update_at" TIMESTAMPTZ(6) DEFAULT '1970-01-01 00:00:00-07'::timestamp with time zone, + "building_address_id" UUID, + "application_pick_up_address_id" UUID, + "application_drop_off_address_id" UUID, + "application_mailing_address_id" UUID, + "building_selection_criteria_file_id" UUID, + "jurisdiction_id" UUID, + "leasing_agent_address_id" UUID, + "reserved_community_type_id" UUID, + "result_id" UUID, + "features_id" UUID, + "utilities_id" UUID, + "hrd_id" TEXT, + "owner_company" TEXT, + "management_company" TEXT, + "management_website" TEXT, + "ami_percentage_min" INTEGER, + "ami_percentage_max" INTEGER, + "phone_number" TEXT, + "temporary_listing_id" INTEGER, + "is_verified" BOOLEAN DEFAULT false, + "marketing_type" "listings_marketing_type_enum" NOT NULL DEFAULT 'marketing', + "marketing_date" TIMESTAMPTZ(6), + "marketing_season" "listings_marketing_season_enum", + "what_to_expect_additional_text" TEXT, + "section8_acceptance" BOOLEAN, + "neighborhood_amenities_id" UUID, + "verified_at" TIMESTAMPTZ(6), + "home_type" "listings_home_type_enum", + "region" "property_region_enum", + + CONSTRAINT "listings_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "migrations" ( + "id" SERIAL NOT NULL, + "timestamp" BIGINT NOT NULL, + "name" VARCHAR NOT NULL, + + CONSTRAINT "migrations_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "multiselect_questions" ( + "id" UUID NOT NULL DEFAULT uuid_generate_v4(), + "created_at" TIMESTAMP(6) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updated_at" TIMESTAMP(6) NOT NULL, + "text" TEXT NOT NULL, + "sub_text" TEXT, + "description" TEXT, + "links" JSONB, + "options" JSONB, + "opt_out_text" TEXT, + "hide_from_listing" BOOLEAN, + "application_section" "multiselect_questions_application_section_enum" NOT NULL, + + CONSTRAINT "multiselect_questions_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "paper_applications" ( + "id" UUID NOT NULL DEFAULT uuid_generate_v4(), + "created_at" TIMESTAMP(6) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updated_at" TIMESTAMP(6) NOT NULL, + "language" "languages_enum" NOT NULL, + "file_id" UUID, + "application_method_id" UUID, + + CONSTRAINT "paper_applications_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "reserved_community_types" ( + "id" UUID NOT NULL DEFAULT uuid_generate_v4(), + "created_at" TIMESTAMP(6) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updated_at" TIMESTAMP(6) NOT NULL, + "name" TEXT NOT NULL, + "description" TEXT, + "jurisdiction_id" UUID NOT NULL, + + CONSTRAINT "reserved_community_types_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "translations" ( + "id" UUID NOT NULL DEFAULT uuid_generate_v4(), + "created_at" TIMESTAMP(6) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updated_at" TIMESTAMP(6) NOT NULL, + "language" "languages_enum" NOT NULL, + "translations" JSONB NOT NULL, + "jurisdiction_id" UUID, + + CONSTRAINT "translations_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "unit_accessibility_priority_types" ( + "id" UUID NOT NULL DEFAULT uuid_generate_v4(), + "created_at" TIMESTAMP(6) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updated_at" TIMESTAMP(6) NOT NULL, + "name" TEXT NOT NULL, + + CONSTRAINT "unit_accessibility_priority_types_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "unit_ami_chart_overrides" ( + "id" UUID NOT NULL DEFAULT uuid_generate_v4(), + "created_at" TIMESTAMP(6) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updated_at" TIMESTAMP(6) NOT NULL, + "items" JSONB NOT NULL, + + CONSTRAINT "unit_ami_chart_overrides_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "unit_rent_types" ( + "id" UUID NOT NULL DEFAULT uuid_generate_v4(), + "created_at" TIMESTAMP(6) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updated_at" TIMESTAMP(6) NOT NULL, + "name" TEXT NOT NULL, + + CONSTRAINT "unit_rent_types_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "unit_types" ( + "id" UUID NOT NULL DEFAULT uuid_generate_v4(), + "created_at" TIMESTAMP(6) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updated_at" TIMESTAMP(6) NOT NULL, + "name" TEXT NOT NULL, + "num_bedrooms" INTEGER NOT NULL, + + CONSTRAINT "unit_types_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "units" ( + "id" UUID NOT NULL DEFAULT uuid_generate_v4(), + "created_at" TIMESTAMP(6) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updated_at" TIMESTAMP(6) NOT NULL, + "ami_percentage" TEXT, + "annual_income_min" TEXT, + "monthly_income_min" TEXT, + "floor" INTEGER, + "annual_income_max" TEXT, + "max_occupancy" INTEGER, + "min_occupancy" INTEGER, + "monthly_rent" TEXT, + "num_bathrooms" INTEGER, + "num_bedrooms" INTEGER, + "number" TEXT, + "sq_feet" DECIMAL(8,2), + "monthly_rent_as_percent_of_income" DECIMAL(8,2), + "bmr_program_chart" BOOLEAN, + "ami_chart_id" UUID, + "listing_id" UUID, + "unit_type_id" UUID, + "unit_rent_type_id" UUID, + "priority_type_id" UUID, + "ami_chart_override_id" UUID, + "status" "units_status_enum" NOT NULL DEFAULT 'unknown', + + CONSTRAINT "units_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "units_summary" ( + "id" UUID NOT NULL DEFAULT uuid_generate_v4(), + "monthly_rent_min" INTEGER, + "monthly_rent_max" INTEGER, + "monthly_rent_as_percent_of_income" DECIMAL(8,2), + "ami_percentage" INTEGER, + "minimum_income_min" TEXT, + "minimum_income_max" TEXT, + "max_occupancy" INTEGER, + "min_occupancy" INTEGER, + "floor_min" INTEGER, + "floor_max" INTEGER, + "sq_feet_min" DECIMAL(8,2), + "sq_feet_max" DECIMAL(8,2), + "total_count" INTEGER, + "total_available" INTEGER, + "unit_type_id" UUID, + "listing_id" UUID, + "priority_type_id" UUID, + + CONSTRAINT "units_summary_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "user_accounts" ( + "id" UUID NOT NULL DEFAULT uuid_generate_v4(), + "password_hash" VARCHAR NOT NULL, + "password_updated_at" TIMESTAMP(6) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "password_valid_for_days" INTEGER NOT NULL DEFAULT 180, + "reset_token" VARCHAR, + "confirmation_token" VARCHAR, + "confirmed_at" TIMESTAMPTZ(6), + "email" VARCHAR NOT NULL, + "first_name" VARCHAR NOT NULL, + "middle_name" VARCHAR, + "last_name" VARCHAR NOT NULL, + "dob" TIMESTAMP(6), + "phone_number" VARCHAR, + "created_at" TIMESTAMP(6) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updated_at" TIMESTAMP(6) NOT NULL, + "language" "languages_enum", + "mfa_enabled" BOOLEAN NOT NULL DEFAULT false, + "mfa_code" VARCHAR, + "mfa_code_updated_at" TIMESTAMPTZ(6), + "last_login_at" TIMESTAMP(6) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "failed_login_attempts_count" INTEGER NOT NULL DEFAULT 0, + "phone_number_verified" BOOLEAN DEFAULT false, + "agreed_to_terms_of_service" BOOLEAN NOT NULL DEFAULT false, + "hit_confirmation_url" TIMESTAMPTZ(6), + "active_access_token" VARCHAR, + "active_refresh_token" VARCHAR, + + CONSTRAINT "user_accounts_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "user_roles" ( + "is_admin" BOOLEAN NOT NULL DEFAULT false, + "is_jurisdictional_admin" BOOLEAN NOT NULL DEFAULT false, + "is_partner" BOOLEAN NOT NULL DEFAULT false, + "user_id" UUID NOT NULL, + + CONSTRAINT "user_roles_pkey" PRIMARY KEY ("user_id") +); + +-- CreateTable +CREATE TABLE "ami_chart_item" ( + "id" UUID NOT NULL DEFAULT uuid_generate_v4(), + "created_at" TIMESTAMP(6) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updated_at" TIMESTAMP(6) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "percent_of_ami" INTEGER NOT NULL, + "household_size" INTEGER NOT NULL, + "income" INTEGER NOT NULL, + "ami_chart_id" UUID, + + CONSTRAINT "ami_chart_item_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "listing_neighborhood_amenities" ( + "id" UUID NOT NULL DEFAULT uuid_generate_v4(), + "created_at" TIMESTAMP(6) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updated_at" TIMESTAMP(6) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "grocery_stores" TEXT, + "pharmacies" TEXT, + "health_care_resources" TEXT, + "parks_and_community_centers" TEXT, + "schools" TEXT, + "public_transportation" TEXT, + + CONSTRAINT "listing_neighborhood_amenities_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "unit_group" ( + "max_occupancy" INTEGER, + "min_occupancy" INTEGER, + "floor_min" INTEGER, + "floor_max" INTEGER, + "total_count" INTEGER, + "total_available" INTEGER, + "priority_type_id" UUID, + "id" UUID NOT NULL DEFAULT uuid_generate_v4(), + "listing_id" UUID, + "bathroom_min" DECIMAL, + "bathroom_max" DECIMAL, + "open_waitlist" BOOLEAN NOT NULL DEFAULT true, + "sq_feet_min" DECIMAL, + "sq_feet_max" DECIMAL, + + CONSTRAINT "unit_group_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "unit_group_ami_levels" ( + "id" UUID NOT NULL DEFAULT uuid_generate_v4(), + "ami_percentage" INTEGER, + "monthly_rent_determination_type" "monthly_rent_determination_type_enum" NOT NULL, + "percentage_of_income_value" DECIMAL, + "ami_chart_id" UUID, + "unit_group_id" UUID, + "flat_rent_value" DECIMAL, + + CONSTRAINT "unit_group_ami_levels_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "user_preferences" ( + "send_email_notifications" BOOLEAN NOT NULL DEFAULT false, + "send_sms_notifications" BOOLEAN NOT NULL DEFAULT false, + "user_id" UUID NOT NULL, + "favorite_ids" TEXT[] DEFAULT ARRAY[]::TEXT[], + + CONSTRAINT "user_preferences_pkey" PRIMARY KEY ("user_id") +); + +-- CreateTable +CREATE TABLE "_ApplicationFlaggedSetToApplications" ( + "A" UUID NOT NULL, + "B" UUID NOT NULL +); + +-- CreateTable +CREATE TABLE "_ApplicationsToUnitTypes" ( + "A" UUID NOT NULL, + "B" UUID NOT NULL +); + +-- CreateTable +CREATE TABLE "_JurisdictionsToMultiselectQuestions" ( + "A" UUID NOT NULL, + "B" UUID NOT NULL +); + +-- CreateTable +CREATE TABLE "_JurisdictionsToUserAccounts" ( + "A" UUID NOT NULL, + "B" UUID NOT NULL +); + +-- CreateTable +CREATE TABLE "_ListingsToUserAccounts" ( + "A" UUID NOT NULL, + "B" UUID NOT NULL +); + +-- CreateTable +CREATE TABLE "_ListingsToUserPreferences" ( + "A" UUID NOT NULL, + "B" UUID NOT NULL +); + +-- CreateTable +CREATE TABLE "_UnitGroupToUnitTypes" ( + "A" UUID NOT NULL, + "B" UUID NOT NULL +); + +-- CreateIndex +CREATE UNIQUE INDEX "alternate_contact_mailing_address_id_key" ON "alternate_contact"("mailing_address_id"); + +-- CreateIndex +CREATE UNIQUE INDEX "applicant_work_address_id_key" ON "applicant"("work_address_id"); + +-- CreateIndex +CREATE UNIQUE INDEX "applicant_address_id_key" ON "applicant"("address_id"); + +-- CreateIndex +CREATE UNIQUE INDEX "application_flagged_set_rule_key_key" ON "application_flagged_set"("rule_key"); + +-- CreateIndex +CREATE INDEX "application_flagged_set_listing_id_idx" ON "application_flagged_set"("listing_id"); + +-- CreateIndex +CREATE UNIQUE INDEX "applications_applicant_id_key" ON "applications"("applicant_id"); + +-- CreateIndex +CREATE UNIQUE INDEX "applications_mailing_address_id_key" ON "applications"("mailing_address_id"); + +-- CreateIndex +CREATE UNIQUE INDEX "applications_alternate_address_id_key" ON "applications"("alternate_address_id"); + +-- CreateIndex +CREATE UNIQUE INDEX "applications_alternate_contact_id_key" ON "applications"("alternate_contact_id"); + +-- CreateIndex +CREATE UNIQUE INDEX "applications_accessibility_id_key" ON "applications"("accessibility_id"); + +-- CreateIndex +CREATE UNIQUE INDEX "applications_demographics_id_key" ON "applications"("demographics_id"); + +-- CreateIndex +CREATE INDEX "applications_listing_id_idx" ON "applications"("listing_id"); + +-- CreateIndex +CREATE UNIQUE INDEX "applications_listing_id_confirmation_code_key" ON "applications"("listing_id", "confirmation_code"); + +-- CreateIndex +CREATE UNIQUE INDEX "household_member_address_id_key" ON "household_member"("address_id"); + +-- CreateIndex +CREATE UNIQUE INDEX "household_member_work_address_id_key" ON "household_member"("work_address_id"); + +-- CreateIndex +CREATE INDEX "household_member_application_id_idx" ON "household_member"("application_id"); + +-- CreateIndex +CREATE UNIQUE INDEX "jurisdictions_name_key" ON "jurisdictions"("name"); + +-- CreateIndex +CREATE INDEX "listing_images_listing_id_idx" ON "listing_images"("listing_id"); + +-- CreateIndex +CREATE UNIQUE INDEX "listings_features_id_key" ON "listings"("features_id"); + +-- CreateIndex +CREATE UNIQUE INDEX "listings_utilities_id_key" ON "listings"("utilities_id"); + +-- CreateIndex +CREATE UNIQUE INDEX "listings_neighborhood_amenities_id_key" ON "listings"("neighborhood_amenities_id"); + +-- CreateIndex +CREATE INDEX "listings_jurisdiction_id_idx" ON "listings"("jurisdiction_id"); + +-- CreateIndex +CREATE UNIQUE INDEX "translations_jurisdiction_id_language_key" ON "translations"("jurisdiction_id", "language"); + +-- CreateIndex +CREATE UNIQUE INDEX "units_ami_chart_override_id_key" ON "units"("ami_chart_override_id"); + +-- CreateIndex +CREATE UNIQUE INDEX "user_accounts_email_key" ON "user_accounts"("email"); + +-- CreateIndex +CREATE UNIQUE INDEX "user_preferences_user_id_key" ON "user_preferences"("user_id"); + +-- CreateIndex +CREATE UNIQUE INDEX "_ApplicationFlaggedSetToApplications_AB_unique" ON "_ApplicationFlaggedSetToApplications"("A", "B"); + +-- CreateIndex +CREATE INDEX "_ApplicationFlaggedSetToApplications_B_index" ON "_ApplicationFlaggedSetToApplications"("B"); + +-- CreateIndex +CREATE UNIQUE INDEX "_ApplicationsToUnitTypes_AB_unique" ON "_ApplicationsToUnitTypes"("A", "B"); + +-- CreateIndex +CREATE INDEX "_ApplicationsToUnitTypes_B_index" ON "_ApplicationsToUnitTypes"("B"); + +-- CreateIndex +CREATE UNIQUE INDEX "_JurisdictionsToMultiselectQuestions_AB_unique" ON "_JurisdictionsToMultiselectQuestions"("A", "B"); + +-- CreateIndex +CREATE INDEX "_JurisdictionsToMultiselectQuestions_B_index" ON "_JurisdictionsToMultiselectQuestions"("B"); + +-- CreateIndex +CREATE UNIQUE INDEX "_JurisdictionsToUserAccounts_AB_unique" ON "_JurisdictionsToUserAccounts"("A", "B"); + +-- CreateIndex +CREATE INDEX "_JurisdictionsToUserAccounts_B_index" ON "_JurisdictionsToUserAccounts"("B"); + +-- CreateIndex +CREATE UNIQUE INDEX "_ListingsToUserAccounts_AB_unique" ON "_ListingsToUserAccounts"("A", "B"); + +-- CreateIndex +CREATE INDEX "_ListingsToUserAccounts_B_index" ON "_ListingsToUserAccounts"("B"); + +-- CreateIndex +CREATE UNIQUE INDEX "_ListingsToUserPreferences_AB_unique" ON "_ListingsToUserPreferences"("A", "B"); + +-- CreateIndex +CREATE INDEX "_ListingsToUserPreferences_B_index" ON "_ListingsToUserPreferences"("B"); + +-- CreateIndex +CREATE UNIQUE INDEX "_UnitGroupToUnitTypes_AB_unique" ON "_UnitGroupToUnitTypes"("A", "B"); + +-- CreateIndex +CREATE INDEX "_UnitGroupToUnitTypes_B_index" ON "_UnitGroupToUnitTypes"("B"); + +-- AddForeignKey +ALTER TABLE "activity_log" ADD CONSTRAINT "activity_log_user_id_fkey" FOREIGN KEY ("user_id") REFERENCES "user_accounts"("id") ON DELETE SET NULL ON UPDATE NO ACTION; + +-- AddForeignKey +ALTER TABLE "alternate_contact" ADD CONSTRAINT "alternate_contact_mailing_address_id_fkey" FOREIGN KEY ("mailing_address_id") REFERENCES "address"("id") ON DELETE NO ACTION ON UPDATE NO ACTION; + +-- AddForeignKey +ALTER TABLE "ami_chart" ADD CONSTRAINT "ami_chart_jurisdiction_id_fkey" FOREIGN KEY ("jurisdiction_id") REFERENCES "jurisdictions"("id") ON DELETE NO ACTION ON UPDATE NO ACTION; + +-- AddForeignKey +ALTER TABLE "applicant" ADD CONSTRAINT "applicant_work_address_id_fkey" FOREIGN KEY ("work_address_id") REFERENCES "address"("id") ON DELETE NO ACTION ON UPDATE NO ACTION; + +-- AddForeignKey +ALTER TABLE "applicant" ADD CONSTRAINT "applicant_address_id_fkey" FOREIGN KEY ("address_id") REFERENCES "address"("id") ON DELETE NO ACTION ON UPDATE NO ACTION; + +-- AddForeignKey +ALTER TABLE "application_flagged_set" ADD CONSTRAINT "application_flagged_set_resolving_user_id_fkey" FOREIGN KEY ("resolving_user_id") REFERENCES "user_accounts"("id") ON DELETE NO ACTION ON UPDATE NO ACTION; + +-- AddForeignKey +ALTER TABLE "application_flagged_set" ADD CONSTRAINT "application_flagged_set_listing_id_fkey" FOREIGN KEY ("listing_id") REFERENCES "listings"("id") ON DELETE NO ACTION ON UPDATE NO ACTION; + +-- AddForeignKey +ALTER TABLE "application_methods" ADD CONSTRAINT "application_methods_listing_id_fkey" FOREIGN KEY ("listing_id") REFERENCES "listings"("id") ON DELETE NO ACTION ON UPDATE NO ACTION; + +-- AddForeignKey +ALTER TABLE "applications" ADD CONSTRAINT "applications_applicant_id_fkey" FOREIGN KEY ("applicant_id") REFERENCES "applicant"("id") ON DELETE NO ACTION ON UPDATE NO ACTION; + +-- AddForeignKey +ALTER TABLE "applications" ADD CONSTRAINT "applications_accessibility_id_fkey" FOREIGN KEY ("accessibility_id") REFERENCES "accessibility"("id") ON DELETE NO ACTION ON UPDATE NO ACTION; + +-- AddForeignKey +ALTER TABLE "applications" ADD CONSTRAINT "applications_alternate_contact_id_fkey" FOREIGN KEY ("alternate_contact_id") REFERENCES "alternate_contact"("id") ON DELETE NO ACTION ON UPDATE NO ACTION; + +-- AddForeignKey +ALTER TABLE "applications" ADD CONSTRAINT "applications_alternate_address_id_fkey" FOREIGN KEY ("alternate_address_id") REFERENCES "address"("id") ON DELETE NO ACTION ON UPDATE NO ACTION; + +-- AddForeignKey +ALTER TABLE "applications" ADD CONSTRAINT "applications_user_id_fkey" FOREIGN KEY ("user_id") REFERENCES "user_accounts"("id") ON DELETE SET NULL ON UPDATE NO ACTION; + +-- AddForeignKey +ALTER TABLE "applications" ADD CONSTRAINT "applications_mailing_address_id_fkey" FOREIGN KEY ("mailing_address_id") REFERENCES "address"("id") ON DELETE NO ACTION ON UPDATE NO ACTION; + +-- AddForeignKey +ALTER TABLE "applications" ADD CONSTRAINT "applications_listing_id_fkey" FOREIGN KEY ("listing_id") REFERENCES "listings"("id") ON DELETE NO ACTION ON UPDATE NO ACTION; + +-- AddForeignKey +ALTER TABLE "applications" ADD CONSTRAINT "applications_demographics_id_fkey" FOREIGN KEY ("demographics_id") REFERENCES "demographics"("id") ON DELETE NO ACTION ON UPDATE NO ACTION; + +-- AddForeignKey +ALTER TABLE "household_member" ADD CONSTRAINT "household_member_application_id_fkey" FOREIGN KEY ("application_id") REFERENCES "applications"("id") ON DELETE NO ACTION ON UPDATE NO ACTION; + +-- AddForeignKey +ALTER TABLE "household_member" ADD CONSTRAINT "household_member_address_id_fkey" FOREIGN KEY ("address_id") REFERENCES "address"("id") ON DELETE NO ACTION ON UPDATE NO ACTION; + +-- AddForeignKey +ALTER TABLE "household_member" ADD CONSTRAINT "household_member_work_address_id_fkey" FOREIGN KEY ("work_address_id") REFERENCES "address"("id") ON DELETE NO ACTION ON UPDATE NO ACTION; + +-- AddForeignKey +ALTER TABLE "listing_events" ADD CONSTRAINT "listing_events_file_id_fkey" FOREIGN KEY ("file_id") REFERENCES "assets"("id") ON DELETE NO ACTION ON UPDATE NO ACTION; + +-- AddForeignKey +ALTER TABLE "listing_events" ADD CONSTRAINT "listing_events_listing_id_fkey" FOREIGN KEY ("listing_id") REFERENCES "listings"("id") ON DELETE NO ACTION ON UPDATE NO ACTION; + +-- AddForeignKey +ALTER TABLE "listing_images" ADD CONSTRAINT "listing_images_image_id_fkey" FOREIGN KEY ("image_id") REFERENCES "assets"("id") ON DELETE NO ACTION ON UPDATE NO ACTION; + +-- AddForeignKey +ALTER TABLE "listing_images" ADD CONSTRAINT "listing_images_listing_id_fkey" FOREIGN KEY ("listing_id") REFERENCES "listings"("id") ON DELETE NO ACTION ON UPDATE NO ACTION; + +-- AddForeignKey +ALTER TABLE "listing_multiselect_questions" ADD CONSTRAINT "listing_multiselect_questions_multiselect_question_id_fkey" FOREIGN KEY ("multiselect_question_id") REFERENCES "multiselect_questions"("id") ON DELETE NO ACTION ON UPDATE NO ACTION; + +-- AddForeignKey +ALTER TABLE "listing_multiselect_questions" ADD CONSTRAINT "listing_multiselect_questions_listing_id_fkey" FOREIGN KEY ("listing_id") REFERENCES "listings"("id") ON DELETE NO ACTION ON UPDATE NO ACTION; + +-- AddForeignKey +ALTER TABLE "listings" ADD CONSTRAINT "listings_neighborhood_amenities_id_fkey" FOREIGN KEY ("neighborhood_amenities_id") REFERENCES "listing_neighborhood_amenities"("id") ON DELETE NO ACTION ON UPDATE NO ACTION; + +-- AddForeignKey +ALTER TABLE "listings" ADD CONSTRAINT "listings_application_drop_off_address_id_fkey" FOREIGN KEY ("application_drop_off_address_id") REFERENCES "address"("id") ON DELETE NO ACTION ON UPDATE NO ACTION; + +-- AddForeignKey +ALTER TABLE "listings" ADD CONSTRAINT "listings_reserved_community_type_id_fkey" FOREIGN KEY ("reserved_community_type_id") REFERENCES "reserved_community_types"("id") ON DELETE NO ACTION ON UPDATE NO ACTION; + +-- AddForeignKey +ALTER TABLE "listings" ADD CONSTRAINT "listings_building_selection_criteria_file_id_fkey" FOREIGN KEY ("building_selection_criteria_file_id") REFERENCES "assets"("id") ON DELETE NO ACTION ON UPDATE NO ACTION; + +-- AddForeignKey +ALTER TABLE "listings" ADD CONSTRAINT "listings_result_id_fkey" FOREIGN KEY ("result_id") REFERENCES "assets"("id") ON DELETE NO ACTION ON UPDATE NO ACTION; + +-- AddForeignKey +ALTER TABLE "listings" ADD CONSTRAINT "listings_utilities_id_fkey" FOREIGN KEY ("utilities_id") REFERENCES "listing_utilities"("id") ON DELETE NO ACTION ON UPDATE NO ACTION; + +-- AddForeignKey +ALTER TABLE "listings" ADD CONSTRAINT "listings_application_mailing_address_id_fkey" FOREIGN KEY ("application_mailing_address_id") REFERENCES "address"("id") ON DELETE NO ACTION ON UPDATE NO ACTION; + +-- AddForeignKey +ALTER TABLE "listings" ADD CONSTRAINT "listings_leasing_agent_address_id_fkey" FOREIGN KEY ("leasing_agent_address_id") REFERENCES "address"("id") ON DELETE NO ACTION ON UPDATE NO ACTION; + +-- AddForeignKey +ALTER TABLE "listings" ADD CONSTRAINT "listings_features_id_fkey" FOREIGN KEY ("features_id") REFERENCES "listing_features"("id") ON DELETE NO ACTION ON UPDATE NO ACTION; + +-- AddForeignKey +ALTER TABLE "listings" ADD CONSTRAINT "listings_jurisdiction_id_fkey" FOREIGN KEY ("jurisdiction_id") REFERENCES "jurisdictions"("id") ON DELETE NO ACTION ON UPDATE NO ACTION; + +-- AddForeignKey +ALTER TABLE "listings" ADD CONSTRAINT "listings_application_pick_up_address_id_fkey" FOREIGN KEY ("application_pick_up_address_id") REFERENCES "address"("id") ON DELETE NO ACTION ON UPDATE NO ACTION; + +-- AddForeignKey +ALTER TABLE "listings" ADD CONSTRAINT "listings_building_address_id_fkey" FOREIGN KEY ("building_address_id") REFERENCES "address"("id") ON DELETE NO ACTION ON UPDATE NO ACTION; + +-- AddForeignKey +ALTER TABLE "paper_applications" ADD CONSTRAINT "paper_applications_file_id_fkey" FOREIGN KEY ("file_id") REFERENCES "assets"("id") ON DELETE NO ACTION ON UPDATE NO ACTION; + +-- AddForeignKey +ALTER TABLE "paper_applications" ADD CONSTRAINT "paper_applications_application_method_id_fkey" FOREIGN KEY ("application_method_id") REFERENCES "application_methods"("id") ON DELETE NO ACTION ON UPDATE NO ACTION; + +-- AddForeignKey +ALTER TABLE "reserved_community_types" ADD CONSTRAINT "reserved_community_types_jurisdiction_id_fkey" FOREIGN KEY ("jurisdiction_id") REFERENCES "jurisdictions"("id") ON DELETE NO ACTION ON UPDATE NO ACTION; + +-- AddForeignKey +ALTER TABLE "translations" ADD CONSTRAINT "translations_jurisdiction_id_fkey" FOREIGN KEY ("jurisdiction_id") REFERENCES "jurisdictions"("id") ON DELETE NO ACTION ON UPDATE NO ACTION; + +-- AddForeignKey +ALTER TABLE "units" ADD CONSTRAINT "units_unit_type_id_fkey" FOREIGN KEY ("unit_type_id") REFERENCES "unit_types"("id") ON DELETE NO ACTION ON UPDATE NO ACTION; + +-- AddForeignKey +ALTER TABLE "units" ADD CONSTRAINT "units_ami_chart_id_fkey" FOREIGN KEY ("ami_chart_id") REFERENCES "ami_chart"("id") ON DELETE NO ACTION ON UPDATE NO ACTION; + +-- AddForeignKey +ALTER TABLE "units" ADD CONSTRAINT "units_ami_chart_override_id_fkey" FOREIGN KEY ("ami_chart_override_id") REFERENCES "unit_ami_chart_overrides"("id") ON DELETE NO ACTION ON UPDATE NO ACTION; + +-- AddForeignKey +ALTER TABLE "units" ADD CONSTRAINT "units_priority_type_id_fkey" FOREIGN KEY ("priority_type_id") REFERENCES "unit_accessibility_priority_types"("id") ON DELETE NO ACTION ON UPDATE NO ACTION; + +-- AddForeignKey +ALTER TABLE "units" ADD CONSTRAINT "units_listing_id_fkey" FOREIGN KEY ("listing_id") REFERENCES "listings"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "units" ADD CONSTRAINT "units_unit_rent_type_id_fkey" FOREIGN KEY ("unit_rent_type_id") REFERENCES "unit_rent_types"("id") ON DELETE NO ACTION ON UPDATE NO ACTION; + +-- AddForeignKey +ALTER TABLE "units_summary" ADD CONSTRAINT "units_summary_unit_type_id_fkey" FOREIGN KEY ("unit_type_id") REFERENCES "unit_types"("id") ON DELETE NO ACTION ON UPDATE NO ACTION; + +-- AddForeignKey +ALTER TABLE "units_summary" ADD CONSTRAINT "units_summary_priority_type_id_fkey" FOREIGN KEY ("priority_type_id") REFERENCES "unit_accessibility_priority_types"("id") ON DELETE NO ACTION ON UPDATE NO ACTION; + +-- AddForeignKey +ALTER TABLE "units_summary" ADD CONSTRAINT "units_summary_listing_id_fkey" FOREIGN KEY ("listing_id") REFERENCES "listings"("id") ON DELETE NO ACTION ON UPDATE NO ACTION; + +-- AddForeignKey +ALTER TABLE "user_roles" ADD CONSTRAINT "user_roles_user_id_fkey" FOREIGN KEY ("user_id") REFERENCES "user_accounts"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "ami_chart_item" ADD CONSTRAINT "ami_chart_item_ami_chart_id_fkey" FOREIGN KEY ("ami_chart_id") REFERENCES "ami_chart"("id") ON DELETE NO ACTION ON UPDATE NO ACTION; + +-- AddForeignKey +ALTER TABLE "unit_group" ADD CONSTRAINT "unit_group_listing_id_fkey" FOREIGN KEY ("listing_id") REFERENCES "listings"("id") ON DELETE NO ACTION ON UPDATE NO ACTION; + +-- AddForeignKey +ALTER TABLE "unit_group" ADD CONSTRAINT "unit_group_priority_type_id_fkey" FOREIGN KEY ("priority_type_id") REFERENCES "unit_accessibility_priority_types"("id") ON DELETE NO ACTION ON UPDATE NO ACTION; + +-- AddForeignKey +ALTER TABLE "unit_group_ami_levels" ADD CONSTRAINT "unit_group_ami_levels_unit_group_id_fkey" FOREIGN KEY ("unit_group_id") REFERENCES "unit_group"("id") ON DELETE NO ACTION ON UPDATE NO ACTION; + +-- AddForeignKey +ALTER TABLE "unit_group_ami_levels" ADD CONSTRAINT "unit_group_ami_levels_ami_chart_id_fkey" FOREIGN KEY ("ami_chart_id") REFERENCES "ami_chart"("id") ON DELETE NO ACTION ON UPDATE NO ACTION; + +-- AddForeignKey +ALTER TABLE "user_preferences" ADD CONSTRAINT "user_preferences_user_id_fkey" FOREIGN KEY ("user_id") REFERENCES "user_accounts"("id") ON DELETE CASCADE ON UPDATE NO ACTION; + +-- AddForeignKey +ALTER TABLE "_ApplicationFlaggedSetToApplications" ADD CONSTRAINT "_ApplicationFlaggedSetToApplications_A_fkey" FOREIGN KEY ("A") REFERENCES "application_flagged_set"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "_ApplicationFlaggedSetToApplications" ADD CONSTRAINT "_ApplicationFlaggedSetToApplications_B_fkey" FOREIGN KEY ("B") REFERENCES "applications"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "_ApplicationsToUnitTypes" ADD CONSTRAINT "_ApplicationsToUnitTypes_A_fkey" FOREIGN KEY ("A") REFERENCES "applications"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "_ApplicationsToUnitTypes" ADD CONSTRAINT "_ApplicationsToUnitTypes_B_fkey" FOREIGN KEY ("B") REFERENCES "unit_types"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "_JurisdictionsToMultiselectQuestions" ADD CONSTRAINT "_JurisdictionsToMultiselectQuestions_A_fkey" FOREIGN KEY ("A") REFERENCES "jurisdictions"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "_JurisdictionsToMultiselectQuestions" ADD CONSTRAINT "_JurisdictionsToMultiselectQuestions_B_fkey" FOREIGN KEY ("B") REFERENCES "multiselect_questions"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "_JurisdictionsToUserAccounts" ADD CONSTRAINT "_JurisdictionsToUserAccounts_A_fkey" FOREIGN KEY ("A") REFERENCES "jurisdictions"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "_JurisdictionsToUserAccounts" ADD CONSTRAINT "_JurisdictionsToUserAccounts_B_fkey" FOREIGN KEY ("B") REFERENCES "user_accounts"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "_ListingsToUserAccounts" ADD CONSTRAINT "_ListingsToUserAccounts_A_fkey" FOREIGN KEY ("A") REFERENCES "listings"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "_ListingsToUserAccounts" ADD CONSTRAINT "_ListingsToUserAccounts_B_fkey" FOREIGN KEY ("B") REFERENCES "user_accounts"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "_ListingsToUserPreferences" ADD CONSTRAINT "_ListingsToUserPreferences_A_fkey" FOREIGN KEY ("A") REFERENCES "listings"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "_ListingsToUserPreferences" ADD CONSTRAINT "_ListingsToUserPreferences_B_fkey" FOREIGN KEY ("B") REFERENCES "user_preferences"("user_id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "_UnitGroupToUnitTypes" ADD CONSTRAINT "_UnitGroupToUnitTypes_A_fkey" FOREIGN KEY ("A") REFERENCES "unit_group"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "_UnitGroupToUnitTypes" ADD CONSTRAINT "_UnitGroupToUnitTypes_B_fkey" FOREIGN KEY ("B") REFERENCES "unit_types"("id") ON DELETE CASCADE ON UPDATE CASCADE; diff --git a/backend_new/prisma/migrations/migration_lock.toml b/backend_new/prisma/migrations/migration_lock.toml new file mode 100644 index 0000000000..fbffa92c2b --- /dev/null +++ b/backend_new/prisma/migrations/migration_lock.toml @@ -0,0 +1,3 @@ +# Please do not edit this file manually +# It should be added in your version-control system (i.e. Git) +provider = "postgresql" \ No newline at end of file diff --git a/backend_new/prisma/schema.prisma b/backend_new/prisma/schema.prisma new file mode 100644 index 0000000000..e05b2795c8 --- /dev/null +++ b/backend_new/prisma/schema.prisma @@ -0,0 +1,1030 @@ +generator client { + provider = "prisma-client-js" +} + +datasource db { + provider = "postgresql" + url = env("DATABASE_URL") +} + +model Accessibility { + id String @id() @default(dbgenerated("uuid_generate_v4()")) @db.Uuid + createdAt DateTime @default(now()) @map("created_at") @db.Timestamp(6) + updatedAt DateTime @updatedAt @map("updated_at") @db.Timestamp(6) + mobility Boolean? + vision Boolean? + hearing Boolean? + applications Applications? + + @@map("accessibility") +} + +model ActivityLog { + id String @id() @default(dbgenerated("uuid_generate_v4()")) @db.Uuid + createdAt DateTime @default(now()) @map("created_at") @db.Timestamp(6) + updatedAt DateTime @updatedAt @map("updated_at") @db.Timestamp(6) + module String @db.VarChar + recordId String @map("record_id") @db.Uuid + action String @db.VarChar + metadata Json? + userId String? @map("user_id") @db.Uuid + userAccounts UserAccounts? @relation(fields: [userId], references: [id], onUpdate: NoAction) + + @@map("activity_log") +} + +// Note: [place_name, city, county, state, street, street2, zip_code] formerly had max length of 64 characters +model Address { + id String @id() @default(dbgenerated("uuid_generate_v4()")) @db.Uuid + createdAt DateTime @default(now()) @map("created_at") @db.Timestamp(6) + updatedAt DateTime @updatedAt @map("updated_at") @db.Timestamp(6) + placeName String? @map("place_name") + city String? + county String? + state String? + street String? + street2 String? + zipCode String? @map("zip_code") + latitude Decimal? @db.Decimal + longitude Decimal? @db.Decimal + alternateContact AlternateContact? + applicantWorkAddress Applicant? @relation("applicant_work_address") + applicantAddress Applicant? @relation("applicant_address") + applicationsAlternateAddress Applications? @relation("applications_alternate_address") + applicationsMailingAddress Applications? @relation("applications_mailing_address") + householdMemberAddress HouseholdMember? @relation("household_member_address") + householdMemberWorkAddress HouseholdMember? @relation("household_member_work_address") + applicationDropOffAddress Listings[] @relation("application_drop_off_address") + applicationMailingAddress Listings[] @relation("application_mailing_address") + leasingAgentAddress Listings[] @relation("leasing_agent_address") + applicationPickUpAddress Listings[] @relation("application_pick_up_address") + buildingAddress Listings[] @relation("building_address") + + @@map("address") +} + +// Note: [type, phone_number] formerly max length 16; [other_type, first_name, last_name] formerly max length 64; [agency] formerly max length 128 +// Note: [email_address] has an isEmail validator attached to it +model AlternateContact { + id String @id() @default(dbgenerated("uuid_generate_v4()")) @db.Uuid + createdAt DateTime @default(now()) @map("created_at") @db.Timestamp(6) + updatedAt DateTime @updatedAt @map("updated_at") @db.Timestamp(6) + type String? + otherType String? @map("other_type") + firstName String? @map("first_name") + lastName String? @map("last_name") + agency String? + phoneNumber String? @map("phone_number") + emailAddress String? @map("email_address") + mailingAddressId String? @unique() @map("mailing_address_id") @db.Uuid + address Address? @relation(fields: [mailingAddressId], references: [id], onDelete: NoAction, onUpdate: NoAction) + applications Applications? + + @@map("alternate_contact") +} + +// Note: [items] was formerly type protected as AmiChartItem +model AmiChart { + id String @id() @default(dbgenerated("uuid_generate_v4()")) @db.Uuid + createdAt DateTime @default(now()) @map("created_at") @db.Timestamp(6) + updatedAt DateTime @updatedAt @map("updated_at") @db.Timestamp(6) + items Json + name String @db.VarChar + jurisdictionId String @map("jurisdiction_id") @db.Uuid + jurisdictions Jurisdictions @relation(fields: [jurisdictionId], references: [id], onDelete: NoAction, onUpdate: NoAction) + amiChartItem AmiChartItem[] + unitGroupAmiLevels UnitGroupAmiLevels[] + units Units[] + + @@map("ami_chart") +} + +// Note: [birth_month, birth_day, birth_year] formerly max length 8; [phone_number, phone_number_type] formerly max length 16; +// [first_name, middle_name, last_name] formerly max length 64 +// Note: [first_name, last_name] formerly min length 1 +// Note: [email_address] needs to have lower case enforcement and needs isEmail validator +model Applicant { + id String @id() @default(dbgenerated("uuid_generate_v4()")) @db.Uuid + createdAt DateTime @default(now()) @map("created_at") @db.Timestamp(6) + updatedAt DateTime @updatedAt @map("updated_at") @db.Timestamp(6) + firstName String? @map("first_name") + middleName String? @map("middle_name") + lastName String? @map("last_name") + birthMonth String? @map("birth_month") + birthDay String? @map("birth_day") + birthYear String? @map("birth_year") + emailAddress String? @map("email_address") + noEmail Boolean? @map("no_email") + phoneNumber String? @map("phone_number") + phoneNumberType String? @map("phone_number_type") + noPhone Boolean? @map("no_phone") + workInRegion YesNoEnum? @map("work_in_region") + workAddressId String? @unique() @map("work_address_id") @db.Uuid + addressId String? @unique() @map("address_id") @db.Uuid + applicantWorkAddress Address? @relation("applicant_work_address", fields: [workAddressId], references: [id], onDelete: NoAction, onUpdate: NoAction) + applicantAddress Address? @relation("applicant_address", fields: [addressId], references: [id], onDelete: NoAction, onUpdate: NoAction) + applications Applications? + + @@map("applicant") +} + +// Note: [rule] used to be a different enum but prisma doesn't support that kind of enum yet. See: https://github.com/prisma/prisma/issues/273 +model ApplicationFlaggedSet { + id String @id() @default(dbgenerated("uuid_generate_v4()")) @db.Uuid + createdAt DateTime @default(now()) @map("created_at") @db.Timestamp(6) + updatedAt DateTime @updatedAt @map("updated_at") @db.Timestamp(6) + rule RuleEnum + ruleKey String @unique() @map("rule_key") @db.VarChar + resolvedTime DateTime? @map("resolved_time") @db.Timestamptz(6) + listingId String @map("listing_id") @db.Uuid + showConfirmationAlert Boolean @default(false) @map("show_confirmation_alert") + status FlaggedSetStatusEnum @default(pending) + resolvingUserId String? @map("resolving_user_id") @db.Uuid + userAccounts UserAccounts? @relation(fields: [resolvingUserId], references: [id], onDelete: NoAction, onUpdate: NoAction) + listings Listings @relation(fields: [listingId], references: [id], onDelete: NoAction, onUpdate: NoAction) + applications Applications[] + + @@index([listingId]) + @@map("application_flagged_set") +} + +// Note: [phone_number] formerly max length 16; [label] formerly max length 256; [external_reference] formerly max length 4096 +model ApplicationMethods { + id String @id() @default(dbgenerated("uuid_generate_v4()")) @db.Uuid + createdAt DateTime @default(now()) @map("created_at") @db.Timestamp(6) + updatedAt DateTime @updatedAt @map("updated_at") @db.Timestamp(6) + type ApplicationMethodsTypeEnum + label String? + externalReference String? @map("external_reference") + acceptsPostmarkedApplications Boolean? @map("accepts_postmarked_applications") + phoneNumber String? @map("phone_number") + listingId String? @map("listing_id") @db.Uuid + listings Listings? @relation(fields: [listingId], references: [id], onDelete: NoAction, onUpdate: NoAction) + paperApplications PaperApplications[] + + @@map("application_methods") +} + +// Note: [additional_phone_number, additional_phone_number_type, household_size] formerly max length 16; +// [contact_preferences, income] formerly max length 64; +// [app_url] formerly max length 256; +// Note: [contact_preferences] formerly max array length of 8 +// Note: [household_member] formerly had max array lenght of 32 +// Note: missing virtual [flagged] field +model Applications { + id String @id() @default(dbgenerated("uuid_generate_v4()")) @db.Uuid + createdAt DateTime @default(now()) @map("created_at") @db.Timestamp(6) + updatedAt DateTime @updatedAt @map("updated_at") @db.Timestamp(6) + deletedAt DateTime? @map("deleted_at") @db.Timestamp(6) + appUrl String? @map("app_url") + additionalPhone Boolean? @map("additional_phone") + additionalPhoneNumber String? @map("additional_phone_number") + additionalPhoneNumberType String? @map("additional_phone_number_type") + contactPreferences String[] @map("contact_preferences") + householdSize Int? @map("household_size") + housingStatus String? @map("housing_status") + sendMailToMailingAddress Boolean? @map("send_mail_to_mailing_address") + householdExpectingChanges Boolean? @map("household_expecting_changes") + householdStudent Boolean? @map("household_student") + incomeVouchers Boolean? @map("income_vouchers") + income String? + incomePeriod IncomePeriodEnum? @map("income_period") + preferences Json + programs Json? + status ApplicationStatusEnum + language LanguagesEnum? + submissionType ApplicationSubmissionTypeEnum @map("submission_type") + acceptedTerms Boolean? @map("accepted_terms") + submissionDate DateTime? @map("submission_date") @db.Timestamptz(6) + // if this field is true then the application is a confirmed duplicate + // meaning that the record in the application flagged set table has a status of duplicate + markedAsDuplicate Boolean @default(false) @map("marked_as_duplicate") + confirmationCode String @map("confirmation_code") + reviewStatus ApplicationReviewStatusEnum @default(valid) @map("review_status") + userId String? @map("user_id") @db.Uuid + listingId String? @map("listing_id") @db.Uuid + applicantId String? @unique() @map("applicant_id") @db.Uuid + mailingAddressId String? @unique() @map("mailing_address_id") @db.Uuid + alternateAddressId String? @unique() @map("alternate_address_id") @db.Uuid + alternateContactId String? @unique() @map("alternate_contact_id") @db.Uuid + accessibilityId String? @unique() @map("accessibility_id") @db.Uuid + demographicsId String? @unique() @map("demographics_id") @db.Uuid + applicationFlaggedSet ApplicationFlaggedSet[] + applicant Applicant? @relation(fields: [applicantId], references: [id], onDelete: NoAction, onUpdate: NoAction) + accessibility Accessibility? @relation(fields: [accessibilityId], references: [id], onDelete: NoAction, onUpdate: NoAction) + alternateContact AlternateContact? @relation(fields: [alternateContactId], references: [id], onDelete: NoAction, onUpdate: NoAction) + applicationsAlternateAddress Address? @relation("applications_alternate_address", fields: [alternateAddressId], references: [id], onDelete: NoAction, onUpdate: NoAction) + userAccounts UserAccounts? @relation(fields: [userId], references: [id], onUpdate: NoAction) + applicationsMailingAddress Address? @relation("applications_mailing_address", fields: [mailingAddressId], references: [id], onDelete: NoAction, onUpdate: NoAction) + listings Listings? @relation(fields: [listingId], references: [id], onDelete: NoAction, onUpdate: NoAction) + demographics Demographics? @relation(fields: [demographicsId], references: [id], onDelete: NoAction, onUpdate: NoAction) + preferredUnitTypes UnitTypes[] + householdMember HouseholdMember[] + + @@unique([listingId, confirmationCode]) + @@index([listingId]) + @@map("applications") +} + +// Note: [file_id, label] formerly max length 128 +model Assets { + id String @id() @default(dbgenerated("uuid_generate_v4()")) @db.Uuid + createdAt DateTime @default(now()) @map("created_at") @db.Timestamp(6) + updatedAt DateTime @updatedAt @map("updated_at") @db.Timestamp(6) + fileId String @map("file_id") + label String + listingEvents ListingEvents[] + listingImages ListingImages[] + buildingSelectionCriteriaFile Listings[] @relation("building_selection_criteria_file") + listingsResult Listings[] @relation("listings_result") + paperApplications PaperApplications[] + + @@map("assets") +} + +// Note: [name] formerly max length 64 +model CronJob { + id String @id() @default(dbgenerated("uuid_generate_v4()")) @db.Uuid + name String? + lastRunDate DateTime? @map("last_run_date") @db.Timestamptz(6) + createdAt DateTime @default(now()) @map("created_at") @db.Timestamp(6) + updatedAt DateTime @updatedAt @map("updated_at") @db.Timestamp(6) + + @@map("cron_job") +} + +// Note: [ethnicity, gender, sexual_orientation, how_did_you_hear] formerly max length 64 +// Note: [how_did_you_hear, race] formerly max array length 64 +model Demographics { + id String @id() @default(dbgenerated("uuid_generate_v4()")) @db.Uuid + createdAt DateTime @default(now()) @map("created_at") @db.Timestamp(6) + updatedAt DateTime @updatedAt @map("updated_at") @db.Timestamp(6) + ethnicity String? + gender String? + sexualOrientation String? @map("sexual_orientation") + howDidYouHear String[] @map("how_did_you_hear") + race String[] + applications Applications? + + @@map("demographics") +} + +model GeneratedListingTranslations { + id String @id() @default(dbgenerated("uuid_generate_v4()")) @db.Uuid + createdAt DateTime @default(now()) @map("created_at") @db.Timestamp(6) + updatedAt DateTime @updatedAt @map("updated_at") @db.Timestamp(6) + listingId String @map("listing_id") @db.VarChar + jurisdictionId String @map("jurisdiction_id") @db.VarChar + language LanguagesEnum + translations Json + timestamp DateTime @db.Timestamp(6) + + @@map("generated_listing_translations") +} + +// Note: [birth_month, birth_day, birth_year] formerly max length 8; +// [phone_number, phone_number_type] formerly max length 16 +// [first_name, middle_name, last_name, relationship] formerly max length 64; +// Note: [email_address] formerly enforced lower case +model HouseholdMember { + id String @id() @default(dbgenerated("uuid_generate_v4()")) @db.Uuid + createdAt DateTime @default(now()) @map("created_at") @db.Timestamp(6) + updatedAt DateTime @updatedAt @map("updated_at") @db.Timestamp(6) + orderId Int? @map("order_id") + firstName String? @map("first_name") + middleName String? @map("middle_name") + lastName String? @map("last_name") + birthMonth String? @map("birth_month") + birthDay String? @map("birth_day") + birthYear String? @map("birth_year") + emailAddress String? @map("email_address") + phoneNumber String? @map("phone_number") + phoneNumberType String? @map("phone_number_type") + noPhone Boolean? @map("no_phone") + sameAddress YesNoEnum @map("same_address") + relationship String? + workInRegion YesNoEnum @map("work_in_region") + addressId String? @unique() @map("address_id") @db.Uuid + workAddressId String? @unique() @map("work_address_id") @db.Uuid + applicationId String? @map("application_id") @db.Uuid + applications Applications? @relation(fields: [applicationId], references: [id], onDelete: NoAction, onUpdate: NoAction) + householdMemberAddress Address? @relation("household_member_address", fields: [addressId], references: [id], onDelete: NoAction, onUpdate: NoAction) + householdMemberWorkAddress Address? @relation("household_member_work_address", fields: [workAddressId], references: [id], onDelete: NoAction, onUpdate: NoAction) + + @@index([applicationId]) + @@map("household_member") +} + +// Note: [name] formerly max length 256 +model Jurisdictions { + id String @id() @default(dbgenerated("uuid_generate_v4()")) @db.Uuid + createdAt DateTime @default(now()) @map("created_at") @db.Timestamp(6) + updatedAt DateTime @updatedAt @map("updated_at") @db.Timestamp(6) + name String @unique() + notificationsSignUpUrl String? @map("notifications_sign_up_url") + languages LanguagesEnum[] @default([en]) + partnerTerms String? @map("partner_terms") + publicUrl String @default("") @map("public_url") + emailFromAddress String? @map("email_from_address") + rentalAssistanceDefault String @map("rental_assistance_default") + enablePartnerSettings Boolean @default(false) @map("enable_partner_settings") + enableAccessibilityFeatures Boolean @default(false) @map("enable_accessibility_features") + enableUtilitiesIncluded Boolean @default(false) @map("enable_utilities_included") + amiChart AmiChart[] + multiselectQuestions MultiselectQuestions[] + listings Listings[] + reservedCommunityTypes ReservedCommunityTypes[] + translations Translations[] + user_accounts UserAccounts[] + + @@map("jurisdictions") +} + +model ListingEvents { + id String @id() @default(dbgenerated("uuid_generate_v4()")) @db.Uuid + createdAt DateTime @default(now()) @map("created_at") @db.Timestamp(6) + updatedAt DateTime @updatedAt @map("updated_at") @db.Timestamp(6) + type ListingEventsTypeEnum + startDate DateTime? @map("start_date") @db.Timestamptz(6) + startTime DateTime? @map("start_time") @db.Timestamptz(6) + endTime DateTime? @map("end_time") @db.Timestamptz(6) + url String? + note String? + label String? + listingId String? @map("listing_id") @db.Uuid + fileId String? @map("file_id") @db.Uuid + assets Assets? @relation(fields: [fileId], references: [id], onDelete: NoAction, onUpdate: NoAction) + listings Listings? @relation(fields: [listingId], references: [id], onDelete: NoAction, onUpdate: NoAction) + + @@map("listing_events") +} + +model ListingFeatures { + id String @id() @default(dbgenerated("uuid_generate_v4()")) @db.Uuid + createdAt DateTime @default(now()) @map("created_at") @db.Timestamp(6) + updatedAt DateTime @updatedAt @map("updated_at") @db.Timestamp(6) + elevator Boolean? + wheelchairRamp Boolean? @map("wheelchair_ramp") + serviceAnimalsAllowed Boolean? @map("service_animals_allowed") + accessibleParking Boolean? @map("accessible_parking") + parkingOnSite Boolean? @map("parking_on_site") + inUnitWasherDryer Boolean? @map("in_unit_washer_dryer") + laundryInBuilding Boolean? @map("laundry_in_building") + barrierFreeEntrance Boolean? @map("barrier_free_entrance") + rollInShower Boolean? @map("roll_in_shower") + grabBars Boolean? @map("grab_bars") + heatingInUnit Boolean? @map("heating_in_unit") + acInUnit Boolean? @map("ac_in_unit") + hearing Boolean? + visual Boolean? + mobility Boolean? + barrierFreeUnitEntrance Boolean? @map("barrier_free_unit_entrance") + loweredLightSwitch Boolean? @map("lowered_light_switch") + barrierFreeBathroom Boolean? @map("barrier_free_bathroom") + wideDoorways Boolean? @map("wide_doorways") + loweredCabinets Boolean? @map("lowered_cabinets") + listings Listings? + + @@map("listing_features") +} + +model ListingImages { + ordinal Int? + listingId String @map("listing_id") @db.Uuid + imageId String @map("image_id") @db.Uuid + assets Assets @relation(fields: [imageId], references: [id], onDelete: NoAction, onUpdate: NoAction) + listings Listings @relation(fields: [listingId], references: [id], onDelete: NoAction, onUpdate: NoAction) + + @@id([listingId, imageId]) + @@index([listingId]) + @@map("listing_images") +} + +model ListingMultiselectQuestions { + ordinal Int? + listingId String @map("listing_id") @db.Uuid + multiselectQuestionId String @map("multiselect_question_id") @db.Uuid + multiselectQuestions MultiselectQuestions @relation(fields: [multiselectQuestionId], references: [id], onDelete: NoAction, onUpdate: NoAction) + listings Listings @relation(fields: [listingId], references: [id], onDelete: NoAction, onUpdate: NoAction) + + @@id([listingId, multiselectQuestionId]) + @@map("listing_multiselect_questions") +} + +model ListingUtilities { + id String @id() @default(dbgenerated("uuid_generate_v4()")) @db.Uuid + createdAt DateTime @default(now()) @map("created_at") @db.Timestamp(6) + updatedAt DateTime @updatedAt @map("updated_at") @db.Timestamp(6) + water Boolean? + gas Boolean? + trash Boolean? + sewer Boolean? + electricity Boolean? + cable Boolean? + phone Boolean? + internet Boolean? + listings Listings? + + @@map("listing_utilities") +} + +// Note: missing [referralApplication, applicationConfig, showWaitlist, unitsSummarized] virtual property +// Note: [reserved_community_description, result_link] formerly max length 4096; +// Note: [leasing_agent_email] formerly had an isEmail validator +model Listings { + id String @id() @default(dbgenerated("uuid_generate_v4()")) @db.Uuid + createdAt DateTime @default(now()) @map("created_at") @db.Timestamp(6) + updatedAt DateTime @updatedAt @map("updated_at") @db.Timestamp(6) + additionalApplicationSubmissionNotes String? @map("additional_application_submission_notes") + digitalApplication Boolean? @map("digital_application") + commonDigitalApplication Boolean? @map("common_digital_application") + paperApplication Boolean? @map("paper_application") + referralOpportunity Boolean? @map("referral_opportunity") + assets Json + accessibility String? + amenities String? + buildingTotalUnits Int? @map("building_total_units") + developer String? + householdSizeMax Int? @map("household_size_max") + householdSizeMin Int? @map("household_size_min") + neighborhood String? + petPolicy String? @map("pet_policy") + smokingPolicy String? @map("smoking_policy") + unitsAvailable Int? @map("units_available") + unitAmenities String? @map("unit_amenities") + servicesOffered String? @map("services_offered") + yearBuilt Int? @map("year_built") + applicationDueDate DateTime? @map("application_due_date") @db.Timestamptz(6) + applicationOpenDate DateTime? @map("application_open_date") @db.Timestamptz(6) + applicationFee String? @map("application_fee") + applicationOrganization String? @map("application_organization") + applicationPickUpAddressOfficeHours String? @map("application_pick_up_address_office_hours") + applicationPickUpAddressType ApplicationAddressTypeEnum? @map("application_pick_up_address_type") + applicationDropOffAddressOfficeHours String? @map("application_drop_off_address_office_hours") + applicationDropOffAddressType ApplicationAddressTypeEnum? @map("application_drop_off_address_type") + applicationMailingAddressType ApplicationAddressTypeEnum? @map("application_mailing_address_type") + buildingSelectionCriteria String? @map("building_selection_criteria") + costsNotIncluded String? @map("costs_not_included") + creditHistory String? @map("credit_history") + criminalBackground String? @map("criminal_background") + depositMin String? @map("deposit_min") + depositMax String? @map("deposit_max") + depositHelperText String? @map("deposit_helper_text") + disableUnitsAccordion Boolean? @map("disable_units_accordion") + leasingAgentEmail String? @map("leasing_agent_email") + leasingAgentName String? @map("leasing_agent_name") + leasingAgentOfficeHours String? @map("leasing_agent_office_hours") + leasingAgentPhone String? @map("leasing_agent_phone") + leasingAgentTitle String? @map("leasing_agent_title") + name String + postmarkedApplicationsReceivedByDate DateTime? @map("postmarked_applications_received_by_date") @db.Timestamptz(6) + programRules String? @map("program_rules") + rentalAssistance String? @map("rental_assistance") + rentalHistory String? @map("rental_history") + requiredDocuments String? @map("required_documents") + specialNotes String? @map("special_notes") + waitlistCurrentSize Int? @map("waitlist_current_size") + waitlistMaxSize Int? @map("waitlist_max_size") + whatToExpect String? @map("what_to_expect") + status ListingsStatusEnum @default(pending) + reviewOrderType ReviewOrderTypeEnum? @map("review_order_type") + displayWaitlistSize Boolean @map("display_waitlist_size") + reservedCommunityDescription String? @map("reserved_community_description") + reservedCommunityMinAge Int? @map("reserved_community_min_age") + resultLink String? @map("result_link") + isWaitlistOpen Boolean? @map("is_waitlist_open") + waitlistOpenSpots Int? @map("waitlist_open_spots") + customMapPin Boolean? @map("custom_map_pin") + publishedAt DateTime? @map("published_at") @db.Timestamptz(6) + closedAt DateTime? @map("closed_at") @db.Timestamptz(6) + afsLastRunAt DateTime? @default(dbgenerated("'1970-01-01 00:00:00-07'::timestamp with time zone")) @map("afs_last_run_at") @db.Timestamptz(6) + lastApplicationUpdateAt DateTime? @default(dbgenerated("'1970-01-01 00:00:00-07'::timestamp with time zone")) @map("last_application_update_at") @db.Timestamptz(6) + buildingAddressId String? @map("building_address_id") @db.Uuid + applicationPickUpAddressId String? @map("application_pick_up_address_id") @db.Uuid + applicationDropOffAddressId String? @map("application_drop_off_address_id") @db.Uuid + applicationMailingAddressId String? @map("application_mailing_address_id") @db.Uuid + buildingSelectionCriteriaFileId String? @map("building_selection_criteria_file_id") @db.Uuid + jurisdictionId String? @map("jurisdiction_id") @db.Uuid + leasingAgentAddressId String? @map("leasing_agent_address_id") @db.Uuid + reservedCommunityTypeId String? @map("reserved_community_type_id") @db.Uuid + resultId String? @map("result_id") @db.Uuid + featuresId String? @unique() @map("features_id") @db.Uuid + utilitiesId String? @unique() @map("utilities_id") @db.Uuid + // START DETROIT SPECIFIC + hrdId String? @map("hrd_id") + ownerCompany String? @map("owner_company") + managementCompany String? @map("management_company") + managementWebsite String? @map("management_website") + amiPercentageMin Int? @map("ami_percentage_min") + amiPercentageMax Int? @map("ami_percentage_max") + phoneNumber String? @map("phone_number") + temporaryListingId Int? @map("temporary_listing_id") + isVerified Boolean? @default(false) @map("is_verified") + marketingType MarketingTypeEnum @default(marketing) @map("marketing_type") + marketingDate DateTime? @map("marketing_date") @db.Timestamptz(6) + marketingSeason MarketingSeasonEnum? @map("marketing_season") + whatToExpectAdditionalText String? @map("what_to_expect_additional_text") + section8Acceptance Boolean? @map("section8_acceptance") + neighborhoodAmenitiesId String? @unique() @map("neighborhood_amenities_id") @db.Uuid + verifiedAt DateTime? @map("verified_at") @db.Timestamptz(6) + homeType HomeTypeEnum? @map("home_type") + listingNeighborhoodAmenities ListingNeighborhoodAmenities? @relation(fields: [neighborhoodAmenitiesId], references: [id], onDelete: NoAction, onUpdate: NoAction) + region RegionEnum? + // END DETROIT SPECIFIC + applicationFlaggedSet ApplicationFlaggedSet[] + applicationMethods ApplicationMethods[] + applications Applications[] + listingEvents ListingEvents[] + listingImages ListingImages[] + listingMultiselectQuestions ListingMultiselectQuestions[] + listingsApplicationDropOffAddress Address? @relation("application_drop_off_address", fields: [applicationDropOffAddressId], references: [id], onDelete: NoAction, onUpdate: NoAction) + reservedCommunityTypes ReservedCommunityTypes? @relation(fields: [reservedCommunityTypeId], references: [id], onDelete: NoAction, onUpdate: NoAction) + listingsBuildingSelectionCriteriaFile Assets? @relation("building_selection_criteria_file", fields: [buildingSelectionCriteriaFileId], references: [id], onDelete: NoAction, onUpdate: NoAction) + listingsResult Assets? @relation("listings_result", fields: [resultId], references: [id], onDelete: NoAction, onUpdate: NoAction) + listingUtilities ListingUtilities? @relation(fields: [utilitiesId], references: [id], onDelete: NoAction, onUpdate: NoAction) + listingsApplicationMailingAddress Address? @relation("application_mailing_address", fields: [applicationMailingAddressId], references: [id], onDelete: NoAction, onUpdate: NoAction) + listingsLeasingAgentAddress Address? @relation("leasing_agent_address", fields: [leasingAgentAddressId], references: [id], onDelete: NoAction, onUpdate: NoAction) + listingFeatures ListingFeatures? @relation(fields: [featuresId], references: [id], onDelete: NoAction, onUpdate: NoAction) + jurisdictions Jurisdictions? @relation(fields: [jurisdictionId], references: [id], onDelete: NoAction, onUpdate: NoAction) + listingsApplicationPickUpAddress Address? @relation("application_pick_up_address", fields: [applicationPickUpAddressId], references: [id], onDelete: NoAction, onUpdate: NoAction) + listingsBuildingAddress Address? @relation("building_address", fields: [buildingAddressId], references: [id], onDelete: NoAction, onUpdate: NoAction) + userAccounts UserAccounts[] + units Units[] + unitGroup UnitGroup[] + userPreferences UserPreferences[] + unitsSummary UnitsSummary[] + + @@index([jurisdictionId]) + @@map("listings") +} + +// Note: hold over from TypeORM +model Migrations { + id Int @id() @default(autoincrement()) + timestamp BigInt + name String @db.VarChar + + @@map("migrations") +} + +// Note: missing [untranslatedText, untranslatedOptOutText] virtual fields +// Note: [options] formerly had array max length 64 +model MultiselectQuestions { + id String @id() @default(dbgenerated("uuid_generate_v4()")) @db.Uuid + createdAt DateTime @default(now()) @map("created_at") @db.Timestamp(6) + updatedAt DateTime @updatedAt @map("updated_at") @db.Timestamp(6) + text String + subText String? @map("sub_text") + description String? + links Json? + options Json? + optOutText String? @map("opt_out_text") + hideFromListing Boolean? @map("hide_from_listing") + applicationSection MultiselectQuestionsApplicationSectionEnum @map("application_section") + jurisdictions Jurisdictions[] + listings ListingMultiselectQuestions[] + + @@map("multiselect_questions") +} + +model PaperApplications { + id String @id() @default(dbgenerated("uuid_generate_v4()")) @db.Uuid + createdAt DateTime @default(now()) @map("created_at") @db.Timestamp(6) + updatedAt DateTime @updatedAt @map("updated_at") @db.Timestamp(6) + language LanguagesEnum + fileId String? @map("file_id") @db.Uuid + applicationMethodId String? @map("application_method_id") @db.Uuid + assets Assets? @relation(fields: [fileId], references: [id], onDelete: NoAction, onUpdate: NoAction) + applicationMethods ApplicationMethods? @relation(fields: [applicationMethodId], references: [id], onDelete: NoAction, onUpdate: NoAction) + + @@map("paper_applications") +} + +// Note: [name] formerly max length 256; [description] formerly max length 2048 +model ReservedCommunityTypes { + id String @id() @default(dbgenerated("uuid_generate_v4()")) @db.Uuid + createdAt DateTime @default(now()) @map("created_at") @db.Timestamp(6) + updatedAt DateTime @updatedAt @map("updated_at") @db.Timestamp(6) + name String + description String? + jurisdictionId String @map("jurisdiction_id") @db.Uuid + listings Listings[] + jurisdictions Jurisdictions @relation(fields: [jurisdictionId], references: [id], onDelete: NoAction, onUpdate: NoAction) + + @@map("reserved_community_types") +} + +model Translations { + id String @id() @default(dbgenerated("uuid_generate_v4()")) @db.Uuid + createdAt DateTime @default(now()) @map("created_at") @db.Timestamp(6) + updatedAt DateTime @updatedAt @map("updated_at") @db.Timestamp(6) + language LanguagesEnum + translations Json + jurisdictionId String? @map("jurisdiction_id") @db.Uuid + jurisdictions Jurisdictions? @relation(fields: [jurisdictionId], references: [id], onDelete: NoAction, onUpdate: NoAction) + + @@unique([jurisdictionId, language]) + @@map("translations") +} + +// Note: [name] formerly max length 256 +model UnitAccessibilityPriorityTypes { + id String @id() @default(dbgenerated("uuid_generate_v4()")) @db.Uuid + createdAt DateTime @default(now()) @map("created_at") @db.Timestamp(6) + updatedAt DateTime @updatedAt @map("updated_at") @db.Timestamp(6) + name String + units Units[] + unitGroup UnitGroup[] + unitsSummary UnitsSummary[] + + @@map("unit_accessibility_priority_types") +} + +model UnitAmiChartOverrides { + id String @id() @default(dbgenerated("uuid_generate_v4()")) @db.Uuid + createdAt DateTime @default(now()) @map("created_at") @db.Timestamp(6) + updatedAt DateTime @updatedAt @map("updated_at") @db.Timestamp(6) + items Json + units Units? + + @@map("unit_ami_chart_overrides") +} + +// Note: [name] formerly max length 256 +model UnitRentTypes { + id String @id() @default(dbgenerated("uuid_generate_v4()")) @db.Uuid + createdAt DateTime @default(now()) @map("created_at") @db.Timestamp(6) + updatedAt DateTime @updatedAt @map("updated_at") @db.Timestamp(6) + name String + units Units[] + + @@map("unit_rent_types") +} + +// Note: [name] formerly max length 256 +model UnitTypes { + id String @id() @default(dbgenerated("uuid_generate_v4()")) @db.Uuid + createdAt DateTime @default(now()) @map("created_at") @db.Timestamp(6) + updatedAt DateTime @updatedAt @map("updated_at") @db.Timestamp(6) + name String + numBedrooms Int @map("num_bedrooms") + applications Applications[] + units Units[] + unitGroups UnitGroup[] + unitsSummary UnitsSummary[] + + @@map("unit_types") +} + +model Units { + id String @id() @default(dbgenerated("uuid_generate_v4()")) @db.Uuid + createdAt DateTime @default(now()) @map("created_at") @db.Timestamp(6) + updatedAt DateTime @updatedAt @map("updated_at") @db.Timestamp(6) + amiPercentage String? @map("ami_percentage") + annualIncomeMin String? @map("annual_income_min") + monthlyIncomeMin String? @map("monthly_income_min") + floor Int? + annualIncomeMax String? @map("annual_income_max") + maxOccupancy Int? @map("max_occupancy") + minOccupancy Int? @map("min_occupancy") + monthlyRent String? @map("monthly_rent") + numBathrooms Int? @map("num_bathrooms") + numBedrooms Int? @map("num_bedrooms") + number String? + sqFeet Decimal? @map("sq_feet") @db.Decimal(8, 2) + monthlyRentAsPercentOfIncome Decimal? @map("monthly_rent_as_percent_of_income") @db.Decimal(8, 2) + bmrProgramChart Boolean? @map("bmr_program_chart") + amiChartId String? @map("ami_chart_id") @db.Uuid + listingId String? @map("listing_id") @db.Uuid + unitTypeId String? @map("unit_type_id") @db.Uuid + unitRentTypeId String? @map("unit_rent_type_id") @db.Uuid + priorityTypeId String? @map("priority_type_id") @db.Uuid + amiChartOverrideId String? @unique() @map("ami_chart_override_id") @db.Uuid + unitTypes UnitTypes? @relation(fields: [unitTypeId], references: [id], onDelete: NoAction, onUpdate: NoAction) + amiChart AmiChart? @relation(fields: [amiChartId], references: [id], onDelete: NoAction, onUpdate: NoAction) + unitAmiChartOverrides UnitAmiChartOverrides? @relation(fields: [amiChartOverrideId], references: [id], onDelete: NoAction, onUpdate: NoAction) + unitAccessibilityPriorityTypes UnitAccessibilityPriorityTypes? @relation(fields: [priorityTypeId], references: [id], onDelete: NoAction, onUpdate: NoAction) + listings Listings? @relation(fields: [listingId], references: [id], onDelete: Cascade) + unitRentTypes UnitRentTypes? @relation(fields: [unitRentTypeId], references: [id], onDelete: NoAction, onUpdate: NoAction) + status UnitsStatusEnum @default(unknown) + + @@map("units") +} + +model UnitsSummary { + id String @id() @default(dbgenerated("uuid_generate_v4()")) @db.Uuid + monthlyRentMin Int? @map("monthly_rent_min") + monthlyRentMax Int? @map("monthly_rent_max") + monthlyRentAsPercentOfIncome Decimal? @map("monthly_rent_as_percent_of_income") @db.Decimal(8, 2) + amiPercentage Int? @map("ami_percentage") + minimumIncomeMin String? @map("minimum_income_min") + minimumIncomeMax String? @map("minimum_income_max") + maxOccupancy Int? @map("max_occupancy") + minOccupancy Int? @map("min_occupancy") + floorMin Int? @map("floor_min") + floorMax Int? @map("floor_max") + sqFeetMin Decimal? @map("sq_feet_min") @db.Decimal(8, 2) + sqFeetMax Decimal? @map("sq_feet_max") @db.Decimal(8, 2) + totalCount Int? @map("total_count") + totalAvailable Int? @map("total_available") + unitTypeId String? @map("unit_type_id") @db.Uuid + listingId String? @map("listing_id") @db.Uuid + priorityTypeId String? @map("priority_type_id") @db.Uuid + unitTypes UnitTypes? @relation(fields: [unitTypeId], references: [id], onDelete: NoAction, onUpdate: NoAction) + unitAccessibilityPriorityTypes UnitAccessibilityPriorityTypes? @relation(fields: [priorityTypeId], references: [id], onDelete: NoAction, onUpdate: NoAction) + listings Listings? @relation(fields: [listingId], references: [id], onDelete: NoAction, onUpdate: NoAction) + + @@map("units_summary") +} + +// Note: [mfa_code] formerly max length 16; [first_name, middle_name, last_name] formerly max length 64 +// Note: [email] formerly lower case enforced formerly had an isEmail validator +// Note: [phone_number] formerly had a phone number validator +model UserAccounts { + id String @id() @default(dbgenerated("uuid_generate_v4()")) @db.Uuid + passwordHash String @map("password_hash") @db.VarChar + passwordUpdatedAt DateTime @default(now()) @map("password_updated_at") @db.Timestamp(6) + passwordValidForDays Int @default(180) @map("password_valid_for_days") + resetToken String? @map("reset_token") @db.VarChar + confirmationToken String? @map("confirmation_token") @db.VarChar + confirmedAt DateTime? @map("confirmed_at") @db.Timestamptz(6) + email String @unique() @db.VarChar + firstName String @map("first_name") @db.VarChar + middleName String? @map("middle_name") @db.VarChar + lastName String @map("last_name") @db.VarChar + dob DateTime? @db.Timestamp(6) + phoneNumber String? @map("phone_number") @db.VarChar + createdAt DateTime @default(now()) @map("created_at") @db.Timestamp(6) + updatedAt DateTime @updatedAt @map("updated_at") @db.Timestamp(6) + language LanguagesEnum? + mfaEnabled Boolean @default(false) @map("mfa_enabled") + mfaCode String? @map("mfa_code") @db.VarChar + mfaCodeUpdatedAt DateTime? @map("mfa_code_updated_at") @db.Timestamptz(6) + lastLoginAt DateTime @default(now()) @map("last_login_at") @db.Timestamp(6) + failedLoginAttemptsCount Int @default(0) @map("failed_login_attempts_count") + phoneNumberVerified Boolean? @default(false) @map("phone_number_verified") + agreedToTermsOfService Boolean @default(false) @map("agreed_to_terms_of_service") + hitConfirmationUrl DateTime? @map("hit_confirmation_url") @db.Timestamptz(6) + activeAccessToken String? @map("active_access_token") @db.VarChar + activeRefreshToken String? @map("active_refresh_token") @db.VarChar + activityLogs ActivityLog[] + applicationFlaggedSet ApplicationFlaggedSet[] + applications Applications[] + listings Listings[] + jurisdictions Jurisdictions[] + userPreferences UserPreferences? + userRoles UserRoles? + + @@map("user_accounts") +} + +model UserRoles { + isAdmin Boolean @default(false) @map("is_admin") + isJurisdictionalAdmin Boolean @default(false) @map("is_jurisdictional_admin") + isPartner Boolean @default(false) @map("is_partner") + userId String @id() @map("user_id") @db.Uuid + userAccounts UserAccounts @relation(fields: [userId], references: [id], onDelete: Cascade) + + @@map("user_roles") +} + +model AmiChartItem { + id String @id() @default(dbgenerated("uuid_generate_v4()")) @db.Uuid + createdAt DateTime @default(now()) @map("created_at") @db.Timestamp(6) + updatedAt DateTime @default(now()) @map("updated_at") @db.Timestamp(6) + percentOfAmi Int @map("percent_of_ami") + householdSize Int @map("household_size") + income Int + amiChartId String? @map("ami_chart_id") @db.Uuid + amiChart AmiChart? @relation(fields: [amiChartId], references: [id], onDelete: NoAction, onUpdate: NoAction) + + @@map("ami_chart_item") +} + +// START DETROIT SPECIFIC +model ListingNeighborhoodAmenities { + id String @id() @default(dbgenerated("uuid_generate_v4()")) @db.Uuid + createdAt DateTime @default(now()) @map("created_at") @db.Timestamp(6) + updatedAt DateTime @default(now()) @map("updated_at") @db.Timestamp(6) + groceryStores String? @map("grocery_stores") + pharmacies String? + healthCareResources String? @map("health_care_resources") + parksAndCommunityCenters String? @map("parks_and_community_centers") + schools String? + publicTransportation String? @map("public_transportation") + listings Listings? + + @@map("listing_neighborhood_amenities") +} + +model UnitGroup { + maxOccupancy Int? @map("max_occupancy") + minOccupancy Int? @map("min_occupancy") + floorMin Int? @map("floor_min") + floorMax Int? @map("floor_max") + totalCount Int? @map("total_count") + totalAvailable Int? @map("total_available") + priorityTypeId String? @map("priority_type_id") @db.Uuid + id String @id() @default(dbgenerated("uuid_generate_v4()")) @db.Uuid + listingId String? @map("listing_id") @db.Uuid + bathroomMin Decimal? @map("bathroom_min") @db.Decimal + bathroomMax Decimal? @map("bathroom_max") @db.Decimal + openWaitlist Boolean @default(true) @map("open_waitlist") + sqFeetMin Decimal? @map("sq_feet_min") @db.Decimal + sqFeetMax Decimal? @map("sq_feet_max") @db.Decimal + listings Listings? @relation(fields: [listingId], references: [id], onDelete: NoAction, onUpdate: NoAction) + unitAccessibilityPriorityTypes UnitAccessibilityPriorityTypes? @relation(fields: [priorityTypeId], references: [id], onDelete: NoAction, onUpdate: NoAction) + unitGroupAmiLevels UnitGroupAmiLevels[] + unitTypes UnitTypes[] + + @@map("unit_group") +} + +model UnitGroupAmiLevels { + id String @id() @default(dbgenerated("uuid_generate_v4()")) @db.Uuid + amiPercentage Int? @map("ami_percentage") + monthlyRentDeterminationType MonthlyRentDeterminationTypeEnum @map("monthly_rent_determination_type") + percentageOfIncomeValue Decimal? @map("percentage_of_income_value") @db.Decimal + amiChartId String? @map("ami_chart_id") @db.Uuid + unitGroupId String? @map("unit_group_id") @db.Uuid + flatRentValue Decimal? @map("flat_rent_value") @db.Decimal + unitGroup UnitGroup? @relation(fields: [unitGroupId], references: [id], onDelete: NoAction, onUpdate: NoAction) + amiChart AmiChart? @relation(fields: [amiChartId], references: [id], onDelete: NoAction, onUpdate: NoAction) + + @@map("unit_group_ami_levels") +} + +model UserPreferences { + sendEmailNotifications Boolean @default(false) @map("send_email_notifications") + sendSmsNotifications Boolean @default(false) @map("send_sms_notifications") + userId String @id() @unique() @map("user_id") @db.Uuid + favoriteIds String[] @default([]) @map("favorite_ids") + userAccounts UserAccounts @relation(fields: [userId], references: [id], onDelete: Cascade, onUpdate: NoAction) + favoritesListings Listings[] + + @@map("user_preferences") +} + +// END DETROIT SPECIFIC + +enum ApplicationMethodsTypeEnum { + Internal + FileDownload + ExternalLink + PaperPickup + POBox + LeasingAgent + Referral + + @@map("application_methods_type_enum") +} + +enum LanguagesEnum { + en + es + vi + zh + tl + + @@map("languages_enum") +} + +enum ListingEventsTypeEnum { + openHouse + publicLottery + lotteryResults + + @@map("listing_events_type_enum") +} + +enum ApplicationAddressTypeEnum { + leasingAgent + + @@map("listings_application_address_type_enum") +} + +enum ReviewOrderTypeEnum { + lottery + firstComeFirstServe + waitlist + + @@map("listings_review_order_type_enum") +} + +enum ListingsStatusEnum { + active + pending + closed + + @@map("listings_status_enum") +} + +enum MultiselectQuestionsApplicationSectionEnum { + programs + preferences + + @@map("multiselect_questions_application_section_enum") +} + +enum YesNoEnum { + yes + no + + @@map("yes_no_enum") +} + +enum RuleEnum { + nameAndDOB + email + + @@map("rule_enum") +} + +enum FlaggedSetStatusEnum { + flagged + pending + resolved + + @@map("flagged_set_status_enum") +} + +enum IncomePeriodEnum { + perMonth + perYear + + @@map("income_period_enum") +} + +enum ApplicationStatusEnum { + draft + submitted + removed + + @@map("application_status_enum") +} + +enum ApplicationSubmissionTypeEnum { + paper + electronical + + @@map("application_submission_type_enum") +} + +enum ApplicationReviewStatusEnum { + pending + pendingAndValid + valid + duplicate + + @@map("application_review_status_enum") +} + +enum UnitsStatusEnum { + unknown + available + occupied + unavailable + + @@map("units_status_enum") +} + +enum HomeTypeEnum { + apartment + duplex + house + townhome + + @@map("listings_home_type_enum") +} + +enum MarketingSeasonEnum { + spring + summer + fall + winter + + @@map("listings_marketing_season_enum") +} + +enum MarketingTypeEnum { + marketing + comingSoon + + @@map("listings_marketing_type_enum") +} + +enum RegionEnum { + Greater_Downtown + Eastside + Southwest + Westside + + @@map("property_region_enum") +} + +enum MonthlyRentDeterminationTypeEnum { + flatRent + percentageOfIncome + + @@map("monthly_rent_determination_type_enum") +} diff --git a/backend_new/yarn.lock b/backend_new/yarn.lock index bf7fa6a7e2..d25731d6b2 100644 --- a/backend_new/yarn.lock +++ b/backend_new/yarn.lock @@ -766,6 +766,23 @@ consola "^2.15.0" node-fetch "^2.6.1" +"@prisma/client@4.13.0": + version "4.13.0" + resolved "https://registry.yarnpkg.com/@prisma/client/-/client-4.13.0.tgz#271d2b9756503ea17bbdb459c7995536cf2a6191" + integrity sha512-YaiiICcRB2hatxsbnfB66uWXjcRw3jsZdlAVxmx0cFcTc/Ad/sKdHCcWSnqyDX47vAewkjRFwiLwrOUjswVvmA== + dependencies: + "@prisma/engines-version" "4.13.0-50.1e7af066ee9cb95cf3a403c78d9aab3e6b04f37a" + +"@prisma/engines-version@4.13.0-50.1e7af066ee9cb95cf3a403c78d9aab3e6b04f37a": + version "4.13.0-50.1e7af066ee9cb95cf3a403c78d9aab3e6b04f37a" + resolved "https://registry.yarnpkg.com/@prisma/engines-version/-/engines-version-4.13.0-50.1e7af066ee9cb95cf3a403c78d9aab3e6b04f37a.tgz#ae338908d11685dee50e7683502d75442b955bf9" + integrity sha512-fsQlbkhPJf08JOzKoyoD9atdUijuGBekwoOPZC3YOygXEml1MTtgXVpnUNchQlRSY82OQ6pSGQ9PxUe4arcSLQ== + +"@prisma/engines@4.13.0": + version "4.13.0" + resolved "https://registry.yarnpkg.com/@prisma/engines/-/engines-4.13.0.tgz#582a6b90b6efeb0f465984f1fe0e72a4afaaa5ae" + integrity sha512-HrniowHRZXHuGT9XRgoXEaP2gJLXM5RMoItaY2PkjvuZ+iHc0Zjbm/302MB8YsPdWozAPHHn+jpFEcEn71OgPw== + "@sinonjs/commons@^1.7.0": version "1.8.6" resolved "https://registry.yarnpkg.com/@sinonjs/commons/-/commons-1.8.6.tgz#80c516a4dc264c2a69115e7578d62581ff455ed9" @@ -4010,6 +4027,13 @@ pretty-format@^27.0.0, pretty-format@^27.5.1: ansi-styles "^5.0.0" react-is "^17.0.1" +prisma@^4.13.0: + version "4.13.0" + resolved "https://registry.yarnpkg.com/prisma/-/prisma-4.13.0.tgz#0b83f40acf50cd47d7463a135c4e9b275713e602" + integrity sha512-L9mqjnSmvWIRCYJ9mQkwCtj4+JDYYTdhoyo8hlsHNDXaZLh/b4hR0IoKIBbTKxZuyHQzLopb/+0Rvb69uGV7uA== + dependencies: + "@prisma/engines" "4.13.0" + process-nextick-args@~2.0.0: version "2.0.1" resolved "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-2.0.1.tgz#7820d9b16120cc55ca9ae7792680ae7dba6d7fe2" From 2bbd67eaf17a740ef332b2d4e74420a5d5142e98 Mon Sep 17 00:00:00 2001 From: Yazeed Loonat Date: Thu, 1 Jun 2023 11:54:45 -0700 Subject: [PATCH 04/57] feat: Prisma listing get endpoints (#3458) * feat: prisma schema generation * fix: cleaning up removed "models" * fix: updates per emily * fix: caught some schema errors * feat: listing get endpoints * feat: adding DTOs * feat: openapi and swagger updates * fix: pass through DTOs * fix: updates per emily * feat: adding ci jobs * fix: round 2 * fix: indentation problem * fix: ci updates * fix: ci test again * fix: adding maybe missing library? * fix: running yarn install * fix: updates per morgan * fix: ci fix round x * fix: test updates * fix: integration tests and ci * fix: update for tests * fix: fixing scheme so migrations can be created * fix: updates per morgan * fix: update backend-new circleci job (#3489) * fix: update backend-new circleci job * fix: add cache * fix: add class validator * fix: update list typing * fix: add class transformer * fix: add listing type * fix: one more attempt at typings * fix: update jest config file * fix: add dbsetup * fix: one more attempt * fix: e2e typing fix * fix: turn off diagnostics for e2e * fix: clear db fields between tests * fix: prettier and linting changes * fix: additional linting fixes --------- Co-authored-by: Yazeed Loonat --------- Co-authored-by: Morgan Ludtke <42942267+ludtkemorgan@users.noreply.github.com> --- .circleci/config.yml | 42 +- .eslintrc.js | 3 +- .../interceptors/activity-log.interceptor.ts | 4 +- .../application-csv-exporter.service.ts | 10 +- .../src/assets/services/upload.service.ts | 6 +- .../src/auth/services/user.service.spec.ts | 4 +- backend/core/src/email/email.service.spec.ts | 6 +- backend/core/src/email/email.service.ts | 46 +- .../listings/tests/listings.service.spec.ts | 38 +- .../seeds/listings/listing-coliseum-seed.ts | 21 +- .../listing-default-lottery-pending.ts | 3 +- .../listing-default-lottery-results.ts | 3 +- .../listings/listing-default-missing-ami.ts | 18 +- .../listings/listing-default-sanjose-seed.ts | 11 +- .../seeds/listings/listing-default-seed.ts | 11 +- .../src/shared/services/abstract-service.ts | 2 +- .../core/src/shared/units-transformations.ts | 18 +- backend/core/src/shared/url-helper.ts | 10 +- backend_new/.env.template | 3 +- backend_new/README.md | 89 +- backend_new/package.json | 25 +- .../migration.sql | 3 + backend_new/prisma/schema.prisma | 8 +- .../seed-helpers/jurisdiction-factory.ts | 16 + .../prisma/seed-helpers/listing-factory.ts | 388 ++++ backend_new/prisma/seed.ts | 24 + backend_new/scripts/generate-axios-client.ts | 19 + backend_new/src/app.module.ts | 4 +- .../src/controllers/listing.controller.ts | 68 + .../enforce-lower-case.decorator.ts | 7 + .../src/dtos/addresses/address-get.dto.ts | 55 + .../application-method-get.dto.ts | 52 + backend_new/src/dtos/assets/asset-get.dto.ts | 18 + .../jurisdictions/jurisdiction-get.dto.ts | 76 + .../src/dtos/listings/listing-event.dto.ts | 56 + .../src/dtos/listings/listing-feature.dto.ts | 66 + .../src/dtos/listings/listing-get.dto.ts | 446 ++++ .../src/dtos/listings/listing-image.dto.ts | 15 + .../listing-multiselect-question.dto.ts | 20 + .../src/dtos/listings/listing-utility.dto.ts | 38 + .../listings/listings-filter-params.dto.ts | 71 + .../listings/listings-query-params.dto.ts | 97 + .../listings/listings-retrieve-params.dto.ts | 19 + .../dtos/listings/paginated-listing.dto.ts | 6 + .../multiselect-link.dto.ts | 18 + .../multiselect-option.dto.ts | 51 + .../multiselect-question.dto.ts | 87 + .../paper-application-get.dto.ts | 21 + .../reserved-community-type-get.dto.ts | 17 + backend_new/src/dtos/shared/abstract.dto.ts | 23 + .../src/dtos/shared/base-filter.dto.ts | 25 + .../src/dtos/shared/min-max-currency.dto.ts | 18 + backend_new/src/dtos/shared/min-max.dto.ts | 18 + backend_new/src/dtos/shared/pagination.dto.ts | 140 ++ .../src/dtos/units/ami-chart-get.dto.ts | 18 + .../src/dtos/units/ami-chart-item-get.dto.ts | 20 + .../dtos/units/ami-chart-override-get.dto.ts | 13 + backend_new/src/dtos/units/hmi-get.dto.ts | 11 + ...nit-accessibility-priority-type-get.dto.ts | 12 + backend_new/src/dtos/units/unit-get.dto.ts | 98 + .../src/dtos/units/unit-rent-type-get.dto.ts | 12 + .../src/dtos/units/unit-summarized.dto.ts | 48 + .../dtos/units/unit-summary-by-ami-get.dto.ts | 20 + .../src/dtos/units/unit-summary-get.dto.ts | 61 + .../src/dtos/units/unit-type-get.dto.ts | 17 + .../src/dtos/units/units-summery-get.dto.ts | 89 + .../src/enums/listings/filter-key-enum.ts | 9 + .../src/enums/listings/order-by-enum.ts | 11 + backend_new/src/enums/listings/view-enum.ts | 6 + backend_new/src/enums/shared/order-by-enum.ts | 4 + .../enums/shared/validation-groups-enum.ts | 5 + .../enums/user_accounts/filter-key-enum.ts | 4 + backend_new/src/main.ts | 11 +- backend_new/src/modules/listing.module.ts | 12 + backend_new/src/services/listing.service.ts | 346 +++ backend_new/src/services/prisma.service.ts | 18 + backend_new/src/utilities/build-filter.ts | 80 + backend_new/src/utilities/build-order-by.ts | 14 + .../default-validation-pipe-options.ts | 16 + backend_new/src/utilities/mapTo.ts | 29 + .../src/utilities/order-by-validator.ts | 30 + .../src/utilities/pagination-helpers.ts | 28 + backend_new/src/utilities/unit-utilities.ts | 530 +++++ .../test/{ => integration}/app.e2e-spec.ts | 2 +- .../test/integration/listing.e2e-spec.ts | 187 ++ backend_new/test/jest-e2e.config.js | 14 + backend_new/test/jest-e2e.json | 9 - backend_new/test/jest.config.js | 14 + .../unit/services/listing.service.spec.ts | 1974 +++++++++++++++++ .../test/unit/utilities/build-filter.spec.ts | 201 ++ .../unit/utilities/build-order-by.spec.ts | 12 + .../unit/utilities/order-by-validator.spec.ts | 87 + .../unit/utilities/pagination-helpers.spec.ts | 40 + .../unit/utilities/unit-utilities.spec.ts | 242 ++ backend_new/types/src/backend-swagger.ts | 413 ++++ backend_new/yarn.lock | 164 +- package.json | 25 +- shared-helpers/src/auth/Timeout.tsx | 6 +- sites/partners/.jest/setup-tests.js | 1 - sites/partners/__tests__/testUtils.tsx | 2 + .../sections/DetailsPrimaryApplicant.tsx | 6 +- .../src/components/flags/flagSetCols.tsx | 7 +- .../sections/AdditionalFees.tsx | 5 +- .../sections/BuildingFeatures.tsx | 5 +- .../components/settings/PreferenceDrawer.tsx | 4 +- .../lib/applications/formatApplicationData.ts | 19 +- sites/partners/src/lib/helpers.ts | 3 +- sites/partners/src/lib/users/signInHelpers.ts | 164 +- sites/partners/src/pages/users/index.tsx | 7 +- sites/partners/tsconfig.json | 18 +- .../ApplicationMultiselectQuestionStep.tsx | 2 +- .../src/components/listing/ListingView.tsx | 6 +- sites/public/src/lib/helpers.tsx | 3 +- sites/public/tsconfig.json | 17 +- sites/public/tsconfig.test.json | 7 +- yarn.lock | 896 +++++--- 116 files changed, 7813 insertions(+), 684 deletions(-) rename backend_new/prisma/migrations/{20230521220941_init => 20230527231022_init}/migration.sql (99%) create mode 100644 backend_new/prisma/seed-helpers/jurisdiction-factory.ts create mode 100644 backend_new/prisma/seed-helpers/listing-factory.ts create mode 100644 backend_new/prisma/seed.ts create mode 100644 backend_new/scripts/generate-axios-client.ts create mode 100644 backend_new/src/controllers/listing.controller.ts create mode 100644 backend_new/src/decorators/enforce-lower-case.decorator.ts create mode 100644 backend_new/src/dtos/addresses/address-get.dto.ts create mode 100644 backend_new/src/dtos/application-methods/application-method-get.dto.ts create mode 100644 backend_new/src/dtos/assets/asset-get.dto.ts create mode 100644 backend_new/src/dtos/jurisdictions/jurisdiction-get.dto.ts create mode 100644 backend_new/src/dtos/listings/listing-event.dto.ts create mode 100644 backend_new/src/dtos/listings/listing-feature.dto.ts create mode 100644 backend_new/src/dtos/listings/listing-get.dto.ts create mode 100644 backend_new/src/dtos/listings/listing-image.dto.ts create mode 100644 backend_new/src/dtos/listings/listing-multiselect-question.dto.ts create mode 100644 backend_new/src/dtos/listings/listing-utility.dto.ts create mode 100644 backend_new/src/dtos/listings/listings-filter-params.dto.ts create mode 100644 backend_new/src/dtos/listings/listings-query-params.dto.ts create mode 100644 backend_new/src/dtos/listings/listings-retrieve-params.dto.ts create mode 100644 backend_new/src/dtos/listings/paginated-listing.dto.ts create mode 100644 backend_new/src/dtos/multiselect-questions/multiselect-link.dto.ts create mode 100644 backend_new/src/dtos/multiselect-questions/multiselect-option.dto.ts create mode 100644 backend_new/src/dtos/multiselect-questions/multiselect-question.dto.ts create mode 100644 backend_new/src/dtos/paper-applications/paper-application-get.dto.ts create mode 100644 backend_new/src/dtos/reserved-community-types/reserved-community-type-get.dto.ts create mode 100644 backend_new/src/dtos/shared/abstract.dto.ts create mode 100644 backend_new/src/dtos/shared/base-filter.dto.ts create mode 100644 backend_new/src/dtos/shared/min-max-currency.dto.ts create mode 100644 backend_new/src/dtos/shared/min-max.dto.ts create mode 100644 backend_new/src/dtos/shared/pagination.dto.ts create mode 100644 backend_new/src/dtos/units/ami-chart-get.dto.ts create mode 100644 backend_new/src/dtos/units/ami-chart-item-get.dto.ts create mode 100644 backend_new/src/dtos/units/ami-chart-override-get.dto.ts create mode 100644 backend_new/src/dtos/units/hmi-get.dto.ts create mode 100644 backend_new/src/dtos/units/unit-accessibility-priority-type-get.dto.ts create mode 100644 backend_new/src/dtos/units/unit-get.dto.ts create mode 100644 backend_new/src/dtos/units/unit-rent-type-get.dto.ts create mode 100644 backend_new/src/dtos/units/unit-summarized.dto.ts create mode 100644 backend_new/src/dtos/units/unit-summary-by-ami-get.dto.ts create mode 100644 backend_new/src/dtos/units/unit-summary-get.dto.ts create mode 100644 backend_new/src/dtos/units/unit-type-get.dto.ts create mode 100644 backend_new/src/dtos/units/units-summery-get.dto.ts create mode 100644 backend_new/src/enums/listings/filter-key-enum.ts create mode 100644 backend_new/src/enums/listings/order-by-enum.ts create mode 100644 backend_new/src/enums/listings/view-enum.ts create mode 100644 backend_new/src/enums/shared/order-by-enum.ts create mode 100644 backend_new/src/enums/shared/validation-groups-enum.ts create mode 100644 backend_new/src/enums/user_accounts/filter-key-enum.ts create mode 100644 backend_new/src/modules/listing.module.ts create mode 100644 backend_new/src/services/listing.service.ts create mode 100644 backend_new/src/services/prisma.service.ts create mode 100644 backend_new/src/utilities/build-filter.ts create mode 100644 backend_new/src/utilities/build-order-by.ts create mode 100644 backend_new/src/utilities/default-validation-pipe-options.ts create mode 100644 backend_new/src/utilities/mapTo.ts create mode 100644 backend_new/src/utilities/order-by-validator.ts create mode 100644 backend_new/src/utilities/pagination-helpers.ts create mode 100644 backend_new/src/utilities/unit-utilities.ts rename backend_new/test/{ => integration}/app.e2e-spec.ts (92%) create mode 100644 backend_new/test/integration/listing.e2e-spec.ts create mode 100644 backend_new/test/jest-e2e.config.js delete mode 100644 backend_new/test/jest-e2e.json create mode 100644 backend_new/test/jest.config.js create mode 100644 backend_new/test/unit/services/listing.service.spec.ts create mode 100644 backend_new/test/unit/utilities/build-filter.spec.ts create mode 100644 backend_new/test/unit/utilities/build-order-by.spec.ts create mode 100644 backend_new/test/unit/utilities/order-by-validator.spec.ts create mode 100644 backend_new/test/unit/utilities/pagination-helpers.spec.ts create mode 100644 backend_new/test/unit/utilities/unit-utilities.spec.ts create mode 100644 backend_new/types/src/backend-swagger.ts diff --git a/.circleci/config.yml b/.circleci/config.yml index 8f80ed3b35..a64c94ebca 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -12,6 +12,15 @@ executors: # Never do this in production or with any sensitive / non-test data: POSTGRES_HOST_AUTH_METHOD: trust POSTGRES_DB: bloom + standard-node-new: + docker: + - image: "cimg/node:18.14.2" + - image: "cimg/postgres:12.10" + environment: + POSTGRES_USER: bloom-ci + # Never do this in production or with any sensitive / non-test data: + POSTGRES_HOST_AUTH_METHOD: trust + POSTGRES_DB: bloom_prisma cypress-node: docker: - image: "cypress/base:18.14.1" @@ -41,12 +50,15 @@ jobs: key: build-cache-{{ .Environment.CIRCLE_SHA1 }} paths: - ~/ - setup-with-db: + setup-with-new-db: executor: standard-node steps: - - restore_cache: - key: build-cache-{{ .Environment.CIRCLE_SHA1 }} - - run: yarn test:backend:core:dbsetup + - checkout + - run: yarn backend:new:install + - save_cache: + key: build-cache-new-{{ .Environment.CIRCLE_SHA1 }} + paths: + - ~/ lint: executor: standard-node steps: @@ -83,6 +95,23 @@ jobs: CLOUDINARY_CLOUD_NAME: "exygy" CLOUDINARY_SECRET: "fake_secret" PARTNERS_PORTAL_URL: "http://localhost:3001" + jest-new-backend: + executor: standard-node-new + steps: + - restore_cache: + key: build-cache-new-{{ .Environment.CIRCLE_SHA1 }} + - run: + name: DB Setup + New Backend Core Tests + command: | + yarn test:backend:new:dbsetup + yarn test:backend:new + yarn test:backend:new:e2e + environment: + PORT: "3100" + EMAIL_API_KEY: "SG.SOME-LONG-SECRET-KEY" + APP_SECRET: "CI-LONG-SECRET-KEY" + # DB URL for migration and seeds: + DATABASE_URL: "postgres://bloom-ci@localhost:5432/bloom_prisma" build-public: executor: standard-node steps: @@ -109,10 +138,10 @@ jobs: - run: yarn test:app:public:unit workflows: - version: 2 build: jobs: - setup + - setup-with-new-db - lint: requires: - setup @@ -122,6 +151,9 @@ workflows: - jest-backend: requires: - setup + - jest-new-backend: + requires: + - setup-with-new-db - build-public: requires: - setup diff --git a/.eslintrc.js b/.eslintrc.js index 0aaa3a44d5..0a57f4599c 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -17,7 +17,6 @@ module.exports = { "plugin:import/typescript", "plugin:react-hooks/recommended", // Make sure we follow https://reactjs.org/docs/hooks-rules.html "plugin:jsx-a11y/recommended", - "prettier/@typescript-eslint", // Uses eslint-config-prettier to disable ESLint rules from @typescript-eslint/eslint-plugin that would conflict with prettier "plugin:prettier/recommended", // Enables eslint-plugin-prettier and displays prettier errors as ESLint errors. Make sure this is always the last configuration in the extends array. ], rules: { @@ -26,6 +25,7 @@ module.exports = { "@typescript-eslint/explicit-function-return-type": "off", "@typescript-eslint/explicit-module-boundary-types": "off", "@typescript-eslint/no-unused-vars": "warn", + "@typescript-eslint/no-var-requires": "off", "react/jsx-uses-vars": "warn", "react/jsx-uses-react": "warn", "@typescript-eslint/restrict-template-expressions": [ @@ -49,6 +49,7 @@ module.exports = { "storybook-static", ".next", "dist", + "backend_new", "migration/", "**/*.stories.tsx", "**/.eslintrc.js", diff --git a/backend/core/src/activity-log/interceptors/activity-log.interceptor.ts b/backend/core/src/activity-log/interceptors/activity-log.interceptor.ts index e5cf4128e5..c55a1c1d58 100644 --- a/backend/core/src/activity-log/interceptors/activity-log.interceptor.ts +++ b/backend/core/src/activity-log/interceptors/activity-log.interceptor.ts @@ -16,9 +16,7 @@ export class ActivityLogInterceptor implements NestInterceptor { protected reflector: Reflector ) {} - getBasicRequestInfo( - context: ExecutionContext - ): { + getBasicRequestInfo(context: ExecutionContext): { module?: string action?: string resourceId?: string diff --git a/backend/core/src/applications/services/application-csv-exporter.service.ts b/backend/core/src/applications/services/application-csv-exporter.service.ts index bf467ecb97..c26dbcf574 100644 --- a/backend/core/src/applications/services/application-csv-exporter.service.ts +++ b/backend/core/src/applications/services/application-csv-exporter.service.ts @@ -217,9 +217,8 @@ export class ApplicationCsvExporterService { } else if ( obj[app.application_id]["Household Members"][app.householdMembers_id] === undefined ) { - obj[app.application_id]["Household Members"][ - app.householdMembers_id - ] = this.mapHouseholdMembers(app) + obj[app.application_id]["Household Members"][app.householdMembers_id] = + this.mapHouseholdMembers(app) extraHeaders["Household Members"] = Math.max( extraHeaders["Household Members"], Object.keys(obj[app.application_id]["Household Members"]).length @@ -227,9 +226,8 @@ export class ApplicationCsvExporterService { } else if ( obj[app.application_id]["Requested Unit Types"][app.preferredUnit_id] === undefined ) { - obj[app.application_id]["Requested Unit Types"][ - app.preferredUnit_id - ] = this.unitTypeToReadable(app.preferredUnit_name) + obj[app.application_id]["Requested Unit Types"][app.preferredUnit_id] = + this.unitTypeToReadable(app.preferredUnit_name) } return obj }, {}) diff --git a/backend/core/src/assets/services/upload.service.ts b/backend/core/src/assets/services/upload.service.ts index ed905077ce..52d66f8e2e 100644 --- a/backend/core/src/assets/services/upload.service.ts +++ b/backend/core/src/assets/services/upload.service.ts @@ -3,9 +3,9 @@ import { ConfigService } from "@nestjs/config" import { v2 as cloudinary } from "cloudinary" export abstract class UploadService { - abstract createPresignedUploadMetadata( - parametersToSign: Record - ): { signature: string } + abstract createPresignedUploadMetadata(parametersToSign: Record): { + signature: string + } } @Injectable() diff --git a/backend/core/src/auth/services/user.service.spec.ts b/backend/core/src/auth/services/user.service.spec.ts index 43ceea2835..78bb196d64 100644 --- a/backend/core/src/auth/services/user.service.spec.ts +++ b/backend/core/src/auth/services/user.service.spec.ts @@ -320,7 +320,7 @@ describe("UserService", () => { it("should return 400 if email is not found", async () => { jest.spyOn(service, "findByResetToken").mockResolvedValueOnce(null) await expect( - service.updatePassword(updateDto, (mockRes as unknown) as Response) + service.updatePassword(updateDto, mockRes as unknown as Response) ).rejects.toThrow( new HttpException(USER_ERRORS.TOKEN_MISSING.message, USER_ERRORS.TOKEN_MISSING.status) ) @@ -336,7 +336,7 @@ describe("UserService", () => { jest.spyOn(service, "findByResetToken").mockResolvedValueOnce(mockedUser as User) // Sets resetToken await service.forgotPassword({ email: "abc@xyz.com" }) - const accessToken = await service.updatePassword(updateDto, (mockRes as unknown) as Response) + const accessToken = await service.updatePassword(updateDto, mockRes as unknown as Response) expect(accessToken).toBeDefined() }) }) diff --git a/backend/core/src/email/email.service.spec.ts b/backend/core/src/email/email.service.spec.ts index 3084f1f808..908bc85241 100644 --- a/backend/core/src/email/email.service.spec.ts +++ b/backend/core/src/email/email.service.spec.ts @@ -62,15 +62,13 @@ const translationServiceMock = { applicationPeriodCloses: "JURISDICTION: Once the application period closes, the property manager will begin processing applications.", eligibleApplicants: { - FCFS: - "Eligible applicants will be placed in order based on first come first serve basis.", + FCFS: "Eligible applicants will be placed in order based on first come first serve basis.", lotteryDate: "The lottery will be held on %{lotteryDate}.", lottery: "Eligible applicants will be placed in order based on preference and lottery rank.", }, eligible: { - fcfs: - "Eligible applicants will be contacted on a first come first serve basis until vacancies are filled.", + fcfs: "Eligible applicants will be contacted on a first come first serve basis until vacancies are filled.", fcfsPreference: "Housing preferences, if applicable, will affect first come first serve order.", lottery: diff --git a/backend/core/src/email/email.service.ts b/backend/core/src/email/email.service.ts index c2f8aed885..b2d3bf1610 100644 --- a/backend/core/src/email/email.service.ts +++ b/backend/core/src/email/email.service.ts @@ -33,12 +33,12 @@ export class EmailService { phrases: {}, }) const polyglot = this.polyglot - Handlebars.registerHelper("t", function ( - phrase: string, - options?: number | Polyglot.InterpolationOptions - ) { - return polyglot.t(phrase, options) - }) + Handlebars.registerHelper( + "t", + function (phrase: string, options?: number | Polyglot.InterpolationOptions) { + return polyglot.t(phrase, options) + } + ) const parts = this.partials() Handlebars.registerPartial(parts) } @@ -220,28 +220,32 @@ export class EmailService { if (language != Language.en) { if (jurisdiction) { - jurisdictionalTranslations = await this.translationService.getTranslationByLanguageAndJurisdictionOrDefaultEn( + jurisdictionalTranslations = + await this.translationService.getTranslationByLanguageAndJurisdictionOrDefaultEn( + language, + jurisdiction.id + ) + } + genericTranslations = + await this.translationService.getTranslationByLanguageAndJurisdictionOrDefaultEn( language, - jurisdiction.id + null ) - } - genericTranslations = await this.translationService.getTranslationByLanguageAndJurisdictionOrDefaultEn( - language, - null - ) } if (jurisdiction) { - jurisdictionalDefaultTranslations = await this.translationService.getTranslationByLanguageAndJurisdictionOrDefaultEn( - Language.en, - jurisdiction.id - ) + jurisdictionalDefaultTranslations = + await this.translationService.getTranslationByLanguageAndJurisdictionOrDefaultEn( + Language.en, + jurisdiction.id + ) } - const genericDefaultTranslations = await this.translationService.getTranslationByLanguageAndJurisdictionOrDefaultEn( - Language.en, - null - ) + const genericDefaultTranslations = + await this.translationService.getTranslationByLanguageAndJurisdictionOrDefaultEn( + Language.en, + null + ) // Deep merge const translations = merge( diff --git a/backend/core/src/listings/tests/listings.service.spec.ts b/backend/core/src/listings/tests/listings.service.spec.ts index e97842ed69..ebd2b9b5c1 100644 --- a/backend/core/src/listings/tests/listings.service.spec.ts +++ b/backend/core/src/listings/tests/listings.service.spec.ts @@ -187,10 +187,10 @@ describe("ListingsService", () => { it("should not add a WHERE clause if no filters are applied", async () => { jest .spyOn(service, "createQueryBuilder") - .mockReturnValueOnce((mockInnerQueryBuilder as unknown) as ListingsQueryBuilder) + .mockReturnValueOnce(mockInnerQueryBuilder as unknown as ListingsQueryBuilder) jest .spyOn(service, "createQueryBuilder") - .mockReturnValueOnce((mockQueryBuilder as unknown) as ListingsQueryBuilder) + .mockReturnValueOnce(mockQueryBuilder as unknown as ListingsQueryBuilder) const listings = await service.list({}) @@ -201,10 +201,10 @@ describe("ListingsService", () => { it("should add a WHERE clause if the neighborhood filter is applied", async () => { jest .spyOn(service, "createQueryBuilder") - .mockReturnValueOnce((mockInnerQueryBuilder as unknown) as ListingsQueryBuilder) + .mockReturnValueOnce(mockInnerQueryBuilder as unknown as ListingsQueryBuilder) jest .spyOn(service, "createQueryBuilder") - .mockReturnValueOnce((mockQueryBuilder as unknown) as ListingsQueryBuilder) + .mockReturnValueOnce(mockQueryBuilder as unknown as ListingsQueryBuilder) const expectedNeighborhood = "Fox Creek" const queryParams: ListingsQueryParams = { @@ -230,10 +230,10 @@ describe("ListingsService", () => { it("should support filters with comma-separated arrays", async () => { jest .spyOn(service, "createQueryBuilder") - .mockReturnValueOnce((mockInnerQueryBuilder as unknown) as ListingsQueryBuilder) + .mockReturnValueOnce(mockInnerQueryBuilder as unknown as ListingsQueryBuilder) jest .spyOn(service, "createQueryBuilder") - .mockReturnValueOnce((mockQueryBuilder as unknown) as ListingsQueryBuilder) + .mockReturnValueOnce(mockQueryBuilder as unknown as ListingsQueryBuilder) const expectedNeighborhoodString = "Fox Creek, , Coliseum," // intentional extra and trailing commas for test // lowercased, trimmed spaces, filtered empty const expectedNeighborhoodArray = ["fox creek", "coliseum"] @@ -261,7 +261,7 @@ describe("ListingsService", () => { it("should throw an exception if an unsupported filter is used", async () => { jest .spyOn(service, "createQueryBuilder") - .mockReturnValueOnce((mockInnerQueryBuilder as unknown) as ListingsQueryBuilder) + .mockReturnValueOnce(mockInnerQueryBuilder as unknown as ListingsQueryBuilder) const queryParams: ListingsQueryParams = { filter: [ @@ -283,7 +283,7 @@ describe("ListingsService", () => { it("should throw an exception if an unsupported comparison is used", async () => { jest .spyOn(service, "createQueryBuilder") - .mockReturnValueOnce((mockInnerQueryBuilder as unknown) as ListingsQueryBuilder) + .mockReturnValueOnce(mockInnerQueryBuilder as unknown as ListingsQueryBuilder) const queryParams: ListingsQueryParams = { filter: [ @@ -305,10 +305,10 @@ describe("ListingsService", () => { it("should not call limit() and offset() if pagination params are not specified", async () => { jest .spyOn(service, "createQueryBuilder") - .mockReturnValueOnce((mockInnerQueryBuilder as unknown) as ListingsQueryBuilder) + .mockReturnValueOnce(mockInnerQueryBuilder as unknown as ListingsQueryBuilder) jest .spyOn(service, "createQueryBuilder") - .mockReturnValueOnce((mockQueryBuilder as unknown) as ListingsQueryBuilder) + .mockReturnValueOnce(mockQueryBuilder as unknown as ListingsQueryBuilder) // Empty params (no pagination) -> no limit/offset const params = {} @@ -322,10 +322,10 @@ describe("ListingsService", () => { it("should not call limit() and offset() if incomplete pagination params are specified", async () => { jest .spyOn(service, "createQueryBuilder") - .mockReturnValueOnce((mockInnerQueryBuilder as unknown) as ListingsQueryBuilder) + .mockReturnValueOnce(mockInnerQueryBuilder as unknown as ListingsQueryBuilder) jest .spyOn(service, "createQueryBuilder") - .mockReturnValueOnce((mockQueryBuilder as unknown) as ListingsQueryBuilder) + .mockReturnValueOnce(mockQueryBuilder as unknown as ListingsQueryBuilder) // Invalid pagination params (page specified, but not limit) -> no limit/offset const params = { page: 3 } @@ -346,12 +346,12 @@ describe("ListingsService", () => { it("should not call limit() and offset() if invalid pagination params are specified", async () => { jest .spyOn(service, "createQueryBuilder") - .mockReturnValueOnce((mockInnerQueryBuilder as unknown) as ListingsQueryBuilder) + .mockReturnValueOnce(mockInnerQueryBuilder as unknown as ListingsQueryBuilder) jest .spyOn(service, "createQueryBuilder") - .mockReturnValueOnce((mockQueryBuilder as unknown) as ListingsQueryBuilder) + .mockReturnValueOnce(mockQueryBuilder as unknown as ListingsQueryBuilder) // Invalid pagination params (page specified, but not limit) -> no limit/offset - const params = { page: ("hello" as unknown) as number } // force the type for testing + const params = { page: "hello" as unknown as number } // force the type for testing const listings = await service.list(params) expect(listings.items).toEqual(mockListings) @@ -370,10 +370,10 @@ describe("ListingsService", () => { mockQueryBuilder.getMany.mockReturnValueOnce(mockFilteredListings) jest .spyOn(service, "createQueryBuilder") - .mockReturnValueOnce((mockInnerQueryBuilder as unknown) as ListingsQueryBuilder) + .mockReturnValueOnce(mockInnerQueryBuilder as unknown as ListingsQueryBuilder) jest .spyOn(service, "createQueryBuilder") - .mockReturnValueOnce((mockQueryBuilder as unknown) as ListingsQueryBuilder) + .mockReturnValueOnce(mockQueryBuilder as unknown as ListingsQueryBuilder) // Valid pagination params -> offset and limit called appropriately const params = { page: 3, limit: 2 } @@ -397,10 +397,10 @@ describe("ListingsService", () => { it("orders by the orderBy param (when set)", async () => { jest .spyOn(service, "createQueryBuilder") - .mockReturnValueOnce((mockInnerQueryBuilder as unknown) as ListingsQueryBuilder) + .mockReturnValueOnce(mockInnerQueryBuilder as unknown as ListingsQueryBuilder) jest .spyOn(service, "createQueryBuilder") - .mockReturnValueOnce((mockQueryBuilder as unknown) as ListingsQueryBuilder) + .mockReturnValueOnce(mockQueryBuilder as unknown as ListingsQueryBuilder) await service.list({ orderBy: [OrderByFieldsEnum.mostRecentlyUpdated], diff --git a/backend/core/src/seeder/seeds/listings/listing-coliseum-seed.ts b/backend/core/src/seeder/seeds/listings/listing-coliseum-seed.ts index 7a1ab0dcc8..51dc036014 100644 --- a/backend/core/src/seeder/seeds/listings/listing-coliseum-seed.ts +++ b/backend/core/src/seeder/seeds/listings/listing-coliseum-seed.ts @@ -149,21 +149,18 @@ const coliseumListing: ListingSeedType = { export class ListingColiseumSeed extends ListingDefaultSeed { async seed() { - const priorityTypeMobilityAndHearingWithVisual = await this.unitAccessibilityPriorityTypeRepository.findOneOrFail( - { + const priorityTypeMobilityAndHearingWithVisual = + await this.unitAccessibilityPriorityTypeRepository.findOneOrFail({ where: { name: PriorityTypes.mobilityHearingVisual }, - } - ) - const priorityTypeMobilityAndMobilityWithHearingAndVisual = await this.unitAccessibilityPriorityTypeRepository.findOneOrFail( - { + }) + const priorityTypeMobilityAndMobilityWithHearingAndVisual = + await this.unitAccessibilityPriorityTypeRepository.findOneOrFail({ where: { name: PriorityTypes.mobilityHearingVisual }, - } - ) - const priorityTypeMobilityAndHearing = await this.unitAccessibilityPriorityTypeRepository.findOneOrFail( - { + }) + const priorityTypeMobilityAndHearing = + await this.unitAccessibilityPriorityTypeRepository.findOneOrFail({ where: { name: PriorityTypes.mobilityHearing }, - } - ) + }) const priorityMobility = await this.unitAccessibilityPriorityTypeRepository.findOneOrFail({ where: { name: PriorityTypes.mobility }, }) diff --git a/backend/core/src/seeder/seeds/listings/listing-default-lottery-pending.ts b/backend/core/src/seeder/seeds/listings/listing-default-lottery-pending.ts index 6d933646b8..1ae0c0d588 100644 --- a/backend/core/src/seeder/seeds/listings/listing-default-lottery-pending.ts +++ b/backend/core/src/seeder/seeds/listings/listing-default-lottery-pending.ts @@ -15,8 +15,7 @@ export class ListingDefaultLotteryPending extends ListingDefaultSeed { startTime: getDate(10), startDate: getDate(10), endTime: getDate(10), - note: - "Custom public lottery event note. This is a long note and should take up more space.", + note: "Custom public lottery event note. This is a long note and should take up more space.", type: ListingEventType.openHouse, url: "https://www.example.com", label: "Custom Event URL Label", diff --git a/backend/core/src/seeder/seeds/listings/listing-default-lottery-results.ts b/backend/core/src/seeder/seeds/listings/listing-default-lottery-results.ts index a0d979ce9d..a75b5923e2 100644 --- a/backend/core/src/seeder/seeds/listings/listing-default-lottery-results.ts +++ b/backend/core/src/seeder/seeds/listings/listing-default-lottery-results.ts @@ -15,8 +15,7 @@ export class ListingDefaultLottery extends ListingDefaultSeed { startTime: getDate(10), startDate: getDate(10), endTime: getDate(10), - note: - "Custom public lottery event note. This is a long note and should take up more space.", + note: "Custom public lottery event note. This is a long note and should take up more space.", type: ListingEventType.openHouse, url: "https://www.example.com", label: "Custom Event URL Label", diff --git a/backend/core/src/seeder/seeds/listings/listing-default-missing-ami.ts b/backend/core/src/seeder/seeds/listings/listing-default-missing-ami.ts index 2bf78e45f9..b9cb74c829 100644 --- a/backend/core/src/seeder/seeds/listings/listing-default-missing-ami.ts +++ b/backend/core/src/seeder/seeds/listings/listing-default-missing-ami.ts @@ -118,16 +118,14 @@ export class ListingDefaultMissingAMI extends ListingDefaultSeed { name: "Test: Default, Missing Household Levels in AMI", }) - const unitsToBeCreated: Array> = missingAmiLevelsUnits.map((unit) => { - return { - ...unit, - amiChart, - listing: { id: newListing.id }, - } - }) + const unitsToBeCreated: Array> = + missingAmiLevelsUnits.map((unit) => { + return { + ...unit, + amiChart, + listing: { id: newListing.id }, + } + }) unitsToBeCreated.forEach((unit) => { unit.unitType = unitTypeOneBdrm diff --git a/backend/core/src/seeder/seeds/listings/listing-default-sanjose-seed.ts b/backend/core/src/seeder/seeds/listings/listing-default-sanjose-seed.ts index 80baa30991..e9f28357ea 100644 --- a/backend/core/src/seeder/seeds/listings/listing-default-sanjose-seed.ts +++ b/backend/core/src/seeder/seeds/listings/listing-default-sanjose-seed.ts @@ -27,9 +27,7 @@ export class ListingDefaultSanJoseSeed { constructor( @InjectRepository(Listing) protected readonly listingRepository: Repository, @InjectRepository(UnitAccessibilityPriorityType) - protected readonly unitAccessibilityPriorityTypeRepository: Repository< - UnitAccessibilityPriorityType - >, + protected readonly unitAccessibilityPriorityTypeRepository: Repository, @InjectRepository(UnitType) protected readonly unitTypeRepository: Repository, @InjectRepository(ReservedCommunityType) protected readonly reservedTypeRepository: Repository, @@ -45,9 +43,10 @@ export class ListingDefaultSanJoseSeed { ) {} async seed() { - const priorityTypeMobilityAndHearing = await this.unitAccessibilityPriorityTypeRepository.findOneOrFail( - { where: { name: PriorityTypes.mobilityHearing } } - ) + const priorityTypeMobilityAndHearing = + await this.unitAccessibilityPriorityTypeRepository.findOneOrFail({ + where: { name: PriorityTypes.mobilityHearing }, + }) const unitTypeOneBdrm = await this.unitTypeRepository.findOneOrFail({ where: { name: "oneBdrm" }, }) diff --git a/backend/core/src/seeder/seeds/listings/listing-default-seed.ts b/backend/core/src/seeder/seeds/listings/listing-default-seed.ts index 7954022efe..483d079226 100644 --- a/backend/core/src/seeder/seeds/listings/listing-default-seed.ts +++ b/backend/core/src/seeder/seeds/listings/listing-default-seed.ts @@ -34,9 +34,7 @@ export class ListingDefaultSeed { constructor( @InjectRepository(Listing) protected readonly listingRepository: Repository, @InjectRepository(UnitAccessibilityPriorityType) - protected readonly unitAccessibilityPriorityTypeRepository: Repository< - UnitAccessibilityPriorityType - >, + protected readonly unitAccessibilityPriorityTypeRepository: Repository, @InjectRepository(UnitType) protected readonly unitTypeRepository: Repository, @InjectRepository(ReservedCommunityType) protected readonly reservedTypeRepository: Repository, @@ -53,9 +51,10 @@ export class ListingDefaultSeed { ) {} async seed() { - const priorityTypeMobilityAndHearing = await this.unitAccessibilityPriorityTypeRepository.findOneOrFail( - { where: { name: PriorityTypes.mobilityHearing } } - ) + const priorityTypeMobilityAndHearing = + await this.unitAccessibilityPriorityTypeRepository.findOneOrFail({ + where: { name: PriorityTypes.mobilityHearing }, + }) const unitTypeOneBdrm = await this.unitTypeRepository.findOneOrFail({ where: { name: "oneBdrm" }, }) diff --git a/backend/core/src/shared/services/abstract-service.ts b/backend/core/src/shared/services/abstract-service.ts index b0de05d18c..0f0b1c6f98 100644 --- a/backend/core/src/shared/services/abstract-service.ts +++ b/backend/core/src/shared/services/abstract-service.ts @@ -28,7 +28,7 @@ export function AbstractServiceFactory { - return await this.repository.save((dto as unknown) as T) + return await this.repository.save(dto as unknown as T) } async findOne(findConditions: FindOneOptions): Promise { diff --git a/backend/core/src/shared/units-transformations.ts b/backend/core/src/shared/units-transformations.ts index ac15ac8799..e7634bbd1e 100644 --- a/backend/core/src/shared/units-transformations.ts +++ b/backend/core/src/shared/units-transformations.ts @@ -371,16 +371,14 @@ export const summarizeUnitsByTypeAndRent = (units: Units, listing: Listing): Uni // One row per unit type export const summarizeUnitsByType = (units: Units, unitTypes: UnitTypeDto[]): UnitSummary[] => { - const summaries = unitTypes.map( - (unitType: UnitTypeDto): UnitSummary => { - const summary = {} as UnitSummary - const unitsByType = units.filter((unit: Unit) => unit.unitType.name == unitType.name) - const finalSummary = Array.from(unitsByType).reduce((summary, unit, index) => { - return getUnitsSummary(unit, index === 0 ? null : summary) - }, summary) - return finalSummary - } - ) + const summaries = unitTypes.map((unitType: UnitTypeDto): UnitSummary => { + const summary = {} as UnitSummary + const unitsByType = units.filter((unit: Unit) => unit.unitType.name == unitType.name) + const finalSummary = Array.from(unitsByType).reduce((summary, unit, index) => { + return getUnitsSummary(unit, index === 0 ? null : summary) + }, summary) + return finalSummary + }) return summaries.sort((a, b) => { return ( UnitTypeSort.indexOf(a.unitType.name) - UnitTypeSort.indexOf(b.unitType.name) || diff --git a/backend/core/src/shared/url-helper.ts b/backend/core/src/shared/url-helper.ts index 23f0842aa3..5740562e1e 100644 --- a/backend/core/src/shared/url-helper.ts +++ b/backend/core/src/shared/url-helper.ts @@ -24,8 +24,14 @@ export const listingUrlSlug = (listing: Listing): string => { const { name } = listing if (listing?.buildingAddress) { - const { city, street, state } = listing?.buildingAddress - return formatUrlSlug([name, street, city, state].join(" ")) + return formatUrlSlug( + [ + name, + listing?.buildingAddress?.street, + listing?.buildingAddress?.city, + listing?.buildingAddress?.state, + ].join(" ") + ) } else { return formatUrlSlug(name) } diff --git a/backend_new/.env.template b/backend_new/.env.template index 19b5164f20..3b8d8e5383 100644 --- a/backend_new/.env.template +++ b/backend_new/.env.template @@ -1 +1,2 @@ -DATABASE_URL="postgres://@localhost:5432/bloom_prisma" \ No newline at end of file +DATABASE_URL="postgres://@localhost:5432/bloom_prisma" +PORT=3101 \ No newline at end of file diff --git a/backend_new/README.md b/backend_new/README.md index 68d69cc16a..7bc19f2313 100644 --- a/backend_new/README.md +++ b/backend_new/README.md @@ -26,4 +26,91 @@ We use the following conventions:
  • a model's fields are lowercase camelcased (e.g. helloWorld)
  • a model's fields are @map()ed to lowercase snackcased (e.g. hello_world)
  • -This is to make the api easier to work with, and to respect postgres's name space conventions +This is to make the api easier to work with, and to respect postgres's name space conventions. +

    + +# Controllers +Controllers are where backend endpoints are housed. They follow the [Nestjs standards](https://docs.nestjs.com/controllers) + +They are housed under `/src/controllers`. + +## Conventions +Controllers are given the extension `.contoller.ts` and the model name (listing, application, etc) is singular. So for example `listing.controller.ts`. + +The exported class should be in capitalized camelcase (e.g. `ListingController`). + +# DTOs +Data Transfer Objects. These are how we flag what fields endpoints will take in, and what the form of the response from the backend will be. + +We use the packages [class-transformer](https://www.npmjs.com/package/class-transformer) & [class-validator](https://www.npmjs.com/package/class-validator) for this. + +They are housed under `src/dtos`, and are broken up by what model they are related too. There are also shared DTOs which are housed under the shared sub-directory. + +## Conventions +DTOs are given the extension `.dto.ts` and the file name is lowercase kebabcase. + +So for example `listings-filter-params.dto.ts`. + +The exported class should be in capitalized camelcase (e.g. `ListingFilterParams`) and does not include the DTO as a suffix. + +# Enums +These are enums used by NestJs primarily for either taking in a request or sending out a response. Database enums (enums from Prisma) are part of the primsa schema and are not housed here. + +They are housed under `src/enums` and the file name is lowercase kebabcase and end with `-enum.ts`. + +So for example `filter-key-enum.ts`. + +## Conventions +The exported enum should be in capitalized camelcase (e.g. `ListingFilterKeys`). + +# Modules +Modules connect the controllers to services and follow [NestJS standards](https://docs.nestjs.com/modules). + +## Conventions +Modules are housed under `src/modules` and are given the extension `.module.ts`. The model name (listing, application, etc) is singular. So for example `listing.module.ts`. + +The exported class should be in capitalized camelcase (e.g. `ListingModule`). + +# Services +Services are where business logic is performed as well as interfacing with the database. + +Controllers should be calling functions in services in order to do their work. + +The follow the [NestJS standards](https://docs.nestjs.com/providers). + +## Conventions +Services are housed under `src/services` and are given the extension `.services.ts`. The model name (listing, application, etc) is singular. So for example `listing.service.ts`. + +The exported class should be in capitalized camelcase (e.g. `ListingService`). + + +# Testing +There are 2 different kinds of tests that the backend supports: Integration tests and Unit tests. + +Integration Tests are tests that DO interface with the database, reading/writing/updating/deleting data from that database. + +Unit Tests are tests that MOCK interaction with the database, or test functionality directly that does not interact with the database. + + +## Integration Testing +Integration Tests are housed under `test/integration`, and files are given the extension `.e2e-spec.ts`. + +These tests will generally test going through the controller's endpoints and will mock as little as possible. When testing the database should start off as empty and should be reset to empty once tests are completed (i.e. data is cleaned up). + +## How to run integration tests +Running the following will run all integration tests: +```bash +$ yarn test:e2e +``` + +## Unit Testing +Unit Tests are housed under `test/unit`, and files are given the extension `.spec.ts`. + +These tests will generally test the functions of a service, or helper functions. +These tests will mock Prisma and therefore will not interface directly with the database. This allows for verifying the correct business logic is performed without having to set up the database. + +## How to run unit tests +Running the following will run all unit tests: +```bash +$ yarn test +``` diff --git a/backend_new/package.json b/backend_new/package.json index 8f032b41a1..fc8c31eb3f 100644 --- a/backend_new/package.json +++ b/backend_new/package.json @@ -14,22 +14,31 @@ "start:debug": "nest start --debug --watch", "start:prod": "node dist/main", "lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix", - "test": "jest", + "test": "jest --config ./test/jest.config.js", "test:watch": "jest --watch", "test:cov": "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", - "db:setup": "psql -c 'DROP DATABASE IF EXISTS bloom_prisma;' && psql -c 'CREATE DATABASE bloom_prisma;' && psql -d bloom_prisma -c 'CREATE EXTENSION IF NOT EXISTS \"uuid-ossp\";' && yarn prisma migrate deploy" + "db:resetup": "psql -c 'DROP DATABASE IF EXISTS bloom_prisma;' && psql -c 'CREATE DATABASE bloom_prisma;' && psql -d bloom_prisma -c 'CREATE EXTENSION IF NOT EXISTS \"uuid-ossp\";'", + "db:migration:run": "yarn prisma migrate deploy", + "db:seed": "yarn prisma db seed", + "generate:client": "ts-node scripts/generate-axios-client.ts && prettier -w types/src/backend-swagger.ts", + "test:e2e": "yarn db:resetup && yarn db:migration:run && jest --config ./test/jest-e2e.json", + "db:setup": "yarn db:resetup && yarn db:migration:run && yarn db:seed" }, "dependencies": { "@nestjs/common": "^8.0.0", "@nestjs/core": "^8.0.0", "@nestjs/platform-express": "^8.0.0", - "@prisma/client": "4.13.0", + "@nestjs/swagger": "^6.3.0", + "@prisma/client": "^4.14.0", + "class-validator": "^0.14.0", + "class-transformer": "^0.5.1", "prisma": "^4.13.0", + "qs": "^6.11.2", "reflect-metadata": "^0.1.13", "rimraf": "^3.0.2", - "rxjs": "^7.2.0" + "rxjs": "^7.2.0", + "swagger-axios-codegen": "^0.15.11" }, "devDependencies": { "@nestjs/cli": "^8.0.0", @@ -52,7 +61,8 @@ "ts-loader": "^9.2.3", "ts-node": "^10.0.0", "tsconfig-paths": "^3.10.1", - "typescript": "^4.3.5" + "typescript": "^4.3.5", + "jest-environment-jsdom": "^27.2.5" }, "jest": { "moduleFileExtensions": [ @@ -70,5 +80,8 @@ ], "coverageDirectory": "../coverage", "testEnvironment": "node" + }, + "prisma": { + "seed": "ts-node prisma/seed.ts" } } diff --git a/backend_new/prisma/migrations/20230521220941_init/migration.sql b/backend_new/prisma/migrations/20230527231022_init/migration.sql similarity index 99% rename from backend_new/prisma/migrations/20230521220941_init/migration.sql rename to backend_new/prisma/migrations/20230527231022_init/migration.sql index 73970a579c..ded4ce382f 100644 --- a/backend_new/prisma/migrations/20230521220941_init/migration.sql +++ b/backend_new/prisma/migrations/20230527231022_init/migration.sql @@ -1,3 +1,6 @@ +-- CreateExtension +CREATE EXTENSION IF NOT EXISTS "uuid-ossp"; + -- CreateEnum CREATE TYPE "application_methods_type_enum" AS ENUM ('Internal', 'FileDownload', 'ExternalLink', 'PaperPickup', 'POBox', 'LeasingAgent', 'Referral'); diff --git a/backend_new/prisma/schema.prisma b/backend_new/prisma/schema.prisma index e05b2795c8..1ad02fbc84 100644 --- a/backend_new/prisma/schema.prisma +++ b/backend_new/prisma/schema.prisma @@ -1,10 +1,12 @@ generator client { - provider = "prisma-client-js" + provider = "prisma-client-js" + previewFeatures = ["postgresqlExtensions"] } datasource db { - provider = "postgresql" - url = env("DATABASE_URL") + provider = "postgresql" + url = env("DATABASE_URL") + extensions = [uuidOssp(map: "uuid-ossp")] } model Accessibility { diff --git a/backend_new/prisma/seed-helpers/jurisdiction-factory.ts b/backend_new/prisma/seed-helpers/jurisdiction-factory.ts new file mode 100644 index 0000000000..7c241a6f9e --- /dev/null +++ b/backend_new/prisma/seed-helpers/jurisdiction-factory.ts @@ -0,0 +1,16 @@ +import { LanguagesEnum, Prisma } from '@prisma/client'; + +export const jurisdictionFactory = ( + i: number, +): Prisma.JurisdictionsCreateInput => ({ + name: `name: ${i}`, + notificationsSignUpUrl: `notificationsSignUpUrl: ${i}`, + languages: [LanguagesEnum.en], + partnerTerms: `partnerTerms: ${i}`, + publicUrl: `publicUrl: ${i}`, + emailFromAddress: `emailFromAddress: ${i}`, + rentalAssistanceDefault: `rentalAssistanceDefault: ${i}`, + enablePartnerSettings: true, + enableAccessibilityFeatures: true, + enableUtilitiesIncluded: true, +}); diff --git a/backend_new/prisma/seed-helpers/listing-factory.ts b/backend_new/prisma/seed-helpers/listing-factory.ts new file mode 100644 index 0000000000..047db1f303 --- /dev/null +++ b/backend_new/prisma/seed-helpers/listing-factory.ts @@ -0,0 +1,388 @@ +import { + Prisma, + ApplicationAddressTypeEnum, + ListingsStatusEnum, + ReviewOrderTypeEnum, + MarketingTypeEnum, + MarketingSeasonEnum, + HomeTypeEnum, + RegionEnum, + ApplicationMethodsTypeEnum, + ListingEventsTypeEnum, + MultiselectQuestionsApplicationSectionEnum, + UnitsStatusEnum, +} from '@prisma/client'; + +export const listingFactory = ( + i: number, + jurisdictionId: string, +): Prisma.ListingsCreateInput => ({ + additionalApplicationSubmissionNotes: `additionalApplicationSubmissionNotes: ${i}`, + digitalApplication: true, + commonDigitalApplication: true, + paperApplication: false, + referralOpportunity: true, + assets: '', + accessibility: `accessibility: ${i}`, + amenities: `amenities: ${i}`, + buildingTotalUnits: i, + developer: `developer: ${i}`, + householdSizeMax: 1, + householdSizeMin: i, + neighborhood: `neighborhood: ${i}`, + petPolicy: `petPolicy: ${i}`, + smokingPolicy: `smokingPolicy: ${i}`, + unitsAvailable: i - 1, + unitAmenities: `unitAmenities: ${i}`, + servicesOffered: `servicesOffered: ${i}`, + yearBuilt: 2000 + i, + applicationDueDate: new Date(), + applicationOpenDate: new Date(), + applicationFee: `applicationFee: ${i}`, + applicationOrganization: `applicationOrganization: ${i}`, + applicationPickUpAddressOfficeHours: `applicationPickUpAddressOfficeHours: ${i}`, + applicationPickUpAddressType: ApplicationAddressTypeEnum.leasingAgent, + applicationDropOffAddressOfficeHours: `applicationDropOffAddressOfficeHours: ${i}`, + applicationDropOffAddressType: ApplicationAddressTypeEnum.leasingAgent, + applicationMailingAddressType: ApplicationAddressTypeEnum.leasingAgent, + buildingSelectionCriteria: `buildingSelectionCriteria: ${i}`, + costsNotIncluded: `costsNotIncluded: ${i}`, + creditHistory: `creditHistory: ${i}`, + criminalBackground: `criminalBackground: ${i}`, + depositMin: `depositMin: ${i}`, + depositMax: `depositMax: ${i}`, + depositHelperText: `depositHelperText: ${i}`, + disableUnitsAccordion: true, + leasingAgentEmail: `leasingAgentEmail: ${i}`, + leasingAgentName: `leasingAgentName: ${i}`, + leasingAgentOfficeHours: `leasingAgentOfficeHours: ${i}`, + leasingAgentPhone: `leasingAgentPhone: ${i}`, + leasingAgentTitle: `leasingAgentTitle: ${i}`, + name: `name: ${i}`, + postmarkedApplicationsReceivedByDate: new Date(), + programRules: `programRules: ${i}`, + rentalAssistance: `rentalAssistance: ${i}`, + rentalHistory: `rentalHistory: ${i}`, + requiredDocuments: `requiredDocuments: ${i}`, + specialNotes: `specialNotes: ${i}`, + waitlistCurrentSize: i, + waitlistMaxSize: i + 1, + whatToExpect: `whatToExpect: ${i}`, + status: ListingsStatusEnum.active, + reviewOrderType: ReviewOrderTypeEnum.firstComeFirstServe, + displayWaitlistSize: true, + reservedCommunityDescription: `reservedCommunityDescription: ${i}`, + reservedCommunityMinAge: i * 10, + resultLink: `resultLink: ${i}`, + isWaitlistOpen: true, + waitlistOpenSpots: i, + customMapPin: false, + publishedAt: new Date(), + + closedAt: new Date(), + afsLastRunAt: null, + lastApplicationUpdateAt: new Date(), + listingsBuildingAddress: { + create: { + placeName: `listingsBuildingAddress: ${i} placeName: ${i}`, + city: `listingsBuildingAddress: ${i} city: ${i}`, + county: `listingsBuildingAddress: ${i} county: ${i}`, + state: `listingsBuildingAddress: ${i} state: ${i}`, + street: `listingsBuildingAddress: ${i} street: ${i}`, + street2: `listingsBuildingAddress: ${i} street2: ${i}`, + zipCode: `listingsBuildingAddress: ${i} zipCode: ${i}`, + latitude: i * 100, + longitude: i * 101, + }, + }, + listingsApplicationDropOffAddress: { + create: { + placeName: `listingsApplicationDropOffAddress: ${i} placeName: ${i}`, + city: `listingsApplicationDropOffAddress: ${i} city: ${i}`, + county: `listingsApplicationDropOffAddress: ${i} county: ${i}`, + state: `listingsApplicationDropOffAddress: ${i} state: ${i}`, + street: `listingsApplicationDropOffAddress: ${i} street: ${i}`, + street2: `listingsApplicationDropOffAddress: ${i} street2: ${i}`, + zipCode: `listingsApplicationDropOffAddress: ${i} zipCode: ${i}`, + latitude: i * 100, + longitude: i * 101, + }, + }, + listingsApplicationMailingAddress: { + create: { + placeName: `listingsApplicationMailingAddress: ${i} placeName: ${i}`, + city: `listingsApplicationMailingAddress: ${i} city: ${i}`, + county: `listingsApplicationMailingAddress: ${i} county: ${i}`, + state: `listingsApplicationMailingAddress: ${i} state: ${i}`, + street: `listingsApplicationMailingAddress: ${i} street: ${i}`, + street2: `listingsApplicationMailingAddress: ${i} street2: ${i}`, + zipCode: `listingsApplicationMailingAddress: ${i} zipCode: ${i}`, + latitude: i * 100, + longitude: i * 101, + }, + }, + listingsLeasingAgentAddress: { + create: { + placeName: `listingsLeasingAgentAddress: ${i} placeName: ${i}`, + city: `listingsLeasingAgentAddress: ${i} city: ${i}`, + county: `listingsLeasingAgentAddress: ${i} county: ${i}`, + state: `listingsLeasingAgentAddress: ${i} state: ${i}`, + street: `listingsLeasingAgentAddress: ${i} street: ${i}`, + street2: `listingsLeasingAgentAddress: ${i} street2: ${i}`, + zipCode: `listingsLeasingAgentAddress: ${i} zipCode: ${i}`, + latitude: i * 100, + longitude: i * 101, + }, + }, + listingsApplicationPickUpAddress: { + create: { + placeName: `listingsApplicationPickUpAddress: ${i} placeName: ${i}`, + city: `listingsApplicationPickUpAddress: ${i} city: ${i}`, + county: `listingsApplicationPickUpAddress: ${i} county: ${i}`, + state: `listingsApplicationPickUpAddress: ${i} state: ${i}`, + street: `listingsApplicationPickUpAddress: ${i} street: ${i}`, + street2: `listingsApplicationPickUpAddress: ${i} street2: ${i}`, + zipCode: `listingsApplicationPickUpAddress: ${i} zipCode: ${i}`, + latitude: i * 100, + longitude: i * 101, + }, + }, + listingsBuildingSelectionCriteriaFile: { + create: { + label: `listingsBuildingSelectionCriteriaFile: ${i} label: ${i}`, + fileId: `listingsBuildingSelectionCriteriaFile: ${i} fileId: ${i}`, + }, + }, + jurisdictions: { + connect: { + id: jurisdictionId, + }, + }, + reservedCommunityTypes: { + create: { + name: `reservedCommunityTypes: ${i} name: ${i}`, + description: `reservedCommunityTypes: ${i} description: ${i}`, + jurisdictions: { + connect: { + id: jurisdictionId, + }, + }, + }, + }, + listingsResult: { + create: { + label: `listingsResult: ${i} label: ${i}`, + fileId: `listingsResult: ${i} fileId: ${i}`, + }, + }, + listingFeatures: { + create: { + elevator: true, + wheelchairRamp: true, + serviceAnimalsAllowed: true, + accessibleParking: true, + parkingOnSite: true, + inUnitWasherDryer: true, + laundryInBuilding: true, + barrierFreeEntrance: true, + rollInShower: true, + grabBars: true, + heatingInUnit: true, + acInUnit: true, + hearing: true, + visual: true, + mobility: true, + barrierFreeUnitEntrance: true, + loweredLightSwitch: true, + barrierFreeBathroom: true, + wideDoorways: true, + loweredCabinets: true, + }, + }, + listingUtilities: { + create: { + water: true, + gas: true, + trash: true, + sewer: true, + electricity: true, + cable: true, + phone: true, + internet: true, + }, + }, + + // detroit specific + hrdId: `hrdId: ${i}`, + ownerCompany: `ownerCompany: ${i}`, + managementCompany: `managementCompany: ${i}`, + managementWebsite: `managementWebsite: ${i}`, + amiPercentageMin: i, + amiPercentageMax: i, + phoneNumber: `phoneNumber: ${i}`, + temporaryListingId: i, + isVerified: true, + marketingType: MarketingTypeEnum.marketing, + marketingDate: new Date(), + marketingSeason: MarketingSeasonEnum.summer, + whatToExpectAdditionalText: `whatToExpectAdditionalText: ${i}`, + section8Acceptance: true, + listingNeighborhoodAmenities: { + create: { + groceryStores: `listingNeighborhoodAmenities: ${i} groceryStores: ${i}`, + pharmacies: `listingNeighborhoodAmenities: ${i} pharmacies: ${i}`, + healthCareResources: `listingNeighborhoodAmenities: ${i} healthCareResources: ${i}`, + parksAndCommunityCenters: `listingNeighborhoodAmenities: ${i} parksAndCommunityCenters: ${i}`, + schools: `listingNeighborhoodAmenities: ${i} schools: ${i}`, + publicTransportation: `listingNeighborhoodAmenities: ${i} publicTransportation: ${i}`, + }, + }, + verifiedAt: new Date(), + homeType: HomeTypeEnum.apartment, + region: RegionEnum.Greater_Downtown, + // end detroit specific + + applicationMethods: { + create: { + type: ApplicationMethodsTypeEnum.Internal, + label: `applicationMethods: ${i} label: ${i}`, + externalReference: `applicationMethods: ${i} externalReference: ${i}`, + acceptsPostmarkedApplications: true, + phoneNumber: `applicationMethods: ${i} phoneNumber: ${i}`, + }, + }, + listingEvents: { + create: { + type: ListingEventsTypeEnum.publicLottery, + startDate: new Date(), + startTime: new Date(), + endTime: new Date(), + url: `listingEvents: ${i} url: ${i}`, + note: `listingEvents: ${i} note: ${i}`, + label: `listingEvents: ${i} label: ${i}`, + assets: { + create: { + label: `listingEvents: ${i} label: ${i}`, + fileId: `listingEvents: ${i} fileId: ${i}`, + }, + }, + }, + }, + listingImages: { + create: { + ordinal: 1, + assets: { + create: { + label: `listingImages: ${i} label: ${i}`, + fileId: `listingImages: ${i} fileId: ${i}`, + }, + }, + }, + }, + listingMultiselectQuestions: { + create: [ + { + ordinal: 1, + multiselectQuestions: { + create: { + text: `multiselectQuestions: ${i} text: ${i}`, + subText: `multiselectQuestions: ${i} subText: ${i}`, + description: `multiselectQuestions: ${i} description: ${i}`, + links: {}, + options: {}, + optOutText: `multiselectQuestions: ${i} optOutText: ${i}`, + hideFromListing: true, + applicationSection: + MultiselectQuestionsApplicationSectionEnum.programs, + jurisdictions: { + connect: { + id: jurisdictionId, + }, + }, + }, + }, + }, + { + ordinal: 2, + multiselectQuestions: { + create: { + text: `multiselectQuestions: ${i} text: ${i}`, + subText: `multiselectQuestions: ${i} subText: ${i}`, + description: `multiselectQuestions: ${i} description: ${i}`, + links: {}, + options: {}, + optOutText: `multiselectQuestions: ${i} optOutText: ${i}`, + hideFromListing: true, + applicationSection: + MultiselectQuestionsApplicationSectionEnum.preferences, + jurisdictions: { + connect: { + id: jurisdictionId, + }, + }, + }, + }, + }, + ], + }, + units: unitFactory(i, i, jurisdictionId), +}); + +const unitFactory = ( + numberToMake: number, + i: number, + jurisdictionId: string, +): Prisma.UnitsCreateNestedManyWithoutListingsInput => { + const createArray: Prisma.UnitsCreateWithoutListingsInput[] = []; + for (let j = 0; j < numberToMake; j++) { + createArray.push({ + amiPercentage: `${i}`, + annualIncomeMin: `${i}`, + monthlyIncomeMin: `${i}`, + floor: i, + annualIncomeMax: `${i}`, + maxOccupancy: i, + minOccupancy: i, + monthlyRent: `${i}`, + numBathrooms: i, + numBedrooms: i, + number: `listing: ${i} unit: ${j}`, + sqFeet: i, + monthlyRentAsPercentOfIncome: i, + bmrProgramChart: true, + status: UnitsStatusEnum.available, + unitTypes: { + create: { + name: `listing: ${i} unit: ${j} unitTypes: ${j}`, + numBedrooms: i, + }, + }, + amiChart: { + create: { + items: {}, + name: `listing: ${i} unit: ${j} amiChart: ${j}`, + jurisdictions: { + connect: { + id: jurisdictionId, + }, + }, + }, + }, + unitAccessibilityPriorityTypes: { + create: { + name: `listing: ${i} unit: ${j} unitAccessibilityPriorityTypes: ${j}`, + }, + }, + unitRentTypes: { + create: { + name: `listing: ${i} unit: ${j} unitRentTypes: ${j}`, + }, + }, + }); + } + const toReturn: Prisma.UnitsCreateNestedManyWithoutListingsInput = { + create: createArray, + }; + + return toReturn; +}; diff --git a/backend_new/prisma/seed.ts b/backend_new/prisma/seed.ts new file mode 100644 index 0000000000..3559d13c9d --- /dev/null +++ b/backend_new/prisma/seed.ts @@ -0,0 +1,24 @@ +import { PrismaClient } from '@prisma/client'; +import { jurisdictionFactory } from './seed-helpers/jurisdiction-factory'; +import { listingFactory } from './seed-helpers/listing-factory'; + +const prisma = new PrismaClient(); +async function main() { + const jurisdiction = await prisma.jurisdictions.create({ + data: jurisdictionFactory(0), + }); + for (let i = 0; i < 5; i++) { + await prisma.listings.create({ + data: listingFactory(i, jurisdiction.id), + }); + } +} +main() + .then(async () => { + await prisma.$disconnect(); + }) + .catch(async (e) => { + console.error(e); + await prisma.$disconnect(); + process.exit(1); + }); diff --git a/backend_new/scripts/generate-axios-client.ts b/backend_new/scripts/generate-axios-client.ts new file mode 100644 index 0000000000..34dbfc32cf --- /dev/null +++ b/backend_new/scripts/generate-axios-client.ts @@ -0,0 +1,19 @@ +import { codegen } from 'swagger-axios-codegen'; +import * as fs from 'fs'; +import 'dotenv/config'; + +async function codeGen() { + await codegen({ + methodNameMode: 'operationId', + remoteUrl: `http://localhost:${process.env.PORT}/api-json`, + outputDir: 'types/src', + useStaticMethod: false, + fileName: 'backend-swagger.ts', + useHeaderParameters: false, + strictNullChecks: true, + }); + let content = fs.readFileSync('./types/src/backend-swagger.ts', 'utf-8'); + content = content.replace(/(\w+)Dto/g, '$1'); + fs.writeFileSync('./types/src/backend-swagger.ts', content); +} +void codeGen(); diff --git a/backend_new/src/app.module.ts b/backend_new/src/app.module.ts index 86628031ca..9ff0f01aab 100644 --- a/backend_new/src/app.module.ts +++ b/backend_new/src/app.module.ts @@ -1,10 +1,12 @@ import { Module } from '@nestjs/common'; import { AppController } from './app.controller'; import { AppService } from './app.service'; +import { ListingModule } from './modules/listing.module'; @Module({ - imports: [], + imports: [ListingModule], controllers: [AppController], providers: [AppService], + exports: [ListingModule], }) export class AppModule {} diff --git a/backend_new/src/controllers/listing.controller.ts b/backend_new/src/controllers/listing.controller.ts new file mode 100644 index 0000000000..e638632f73 --- /dev/null +++ b/backend_new/src/controllers/listing.controller.ts @@ -0,0 +1,68 @@ +import { + ClassSerializerInterceptor, + Controller, + Get, + Headers, + Param, + ParseUUIDPipe, + Query, + UseInterceptors, + UsePipes, + ValidationPipe, +} from '@nestjs/common'; +import { + ApiExtraModels, + ApiOperation, + ApiTags, + ApiOkResponse, +} from '@nestjs/swagger'; +import { ListingService } from '../services/listing.service'; +import { defaultValidationPipeOptions } from '../utilities/default-validation-pipe-options'; +import { ListingsQueryParams } from '../dtos/listings/listings-query-params.dto'; +import { LanguagesEnum } from '@prisma/client'; +import { ListingsRetrieveParams } from '../dtos/listings/listings-retrieve-params.dto'; +import { PaginationAllowsAllQueryParams } from '../dtos/shared/pagination.dto'; +import { ListingFilterParams } from '../dtos/listings/listings-filter-params.dto'; +import { PaginatedListingDto } from '../dtos/listings/paginated-listing.dto'; +import ListingGet from '../dtos/listings/listing-get.dto'; + +@Controller('listings') +@ApiTags('listings') +@ApiExtraModels( + ListingsQueryParams, + ListingFilterParams, + ListingsRetrieveParams, + PaginationAllowsAllQueryParams, +) +export class ListingController { + constructor(private readonly listingService: ListingService) {} + + @Get() + @ApiOperation({ + summary: 'Get a paginated set of listings', + operationId: 'list', + }) + @UsePipes(new ValidationPipe(defaultValidationPipeOptions)) + @UseInterceptors(ClassSerializerInterceptor) + @ApiOkResponse({ type: PaginatedListingDto }) + public async getPaginatedSet(@Query() queryParams: ListingsQueryParams) { + return await this.listingService.list(queryParams); + } + + @Get(`:id`) + @ApiOperation({ summary: 'Get listing by id', operationId: 'retrieve' }) + @UseInterceptors(ClassSerializerInterceptor) + @UsePipes(new ValidationPipe(defaultValidationPipeOptions)) + @ApiOkResponse({ type: ListingGet }) + async retrieve( + @Headers('language') language: LanguagesEnum, + @Param('id', new ParseUUIDPipe({ version: '4' })) listingId: string, + @Query() queryParams: ListingsRetrieveParams, + ) { + return await this.listingService.findOne( + listingId, + language, + queryParams.view, + ); + } +} diff --git a/backend_new/src/decorators/enforce-lower-case.decorator.ts b/backend_new/src/decorators/enforce-lower-case.decorator.ts new file mode 100644 index 0000000000..48b46f4e5c --- /dev/null +++ b/backend_new/src/decorators/enforce-lower-case.decorator.ts @@ -0,0 +1,7 @@ +import { Transform, TransformFnParams } from 'class-transformer'; + +export function EnforceLowerCase() { + return Transform((param: TransformFnParams) => + param?.value ? param.value.toLowerCase() : param.value, + ); +} diff --git a/backend_new/src/dtos/addresses/address-get.dto.ts b/backend_new/src/dtos/addresses/address-get.dto.ts new file mode 100644 index 0000000000..3cba4ae2e9 --- /dev/null +++ b/backend_new/src/dtos/addresses/address-get.dto.ts @@ -0,0 +1,55 @@ +import { Expose, Type } from 'class-transformer'; +import { IsNumber, IsDefined, IsString, MaxLength } from 'class-validator'; +import { ValidationsGroupsEnum } from '../../enums/shared/validation-groups-enum'; +import { AbstractDTO } from '../shared/abstract.dto'; + +export class Address extends AbstractDTO { + @Expose() + @IsString({ groups: [ValidationsGroupsEnum.default] }) + @MaxLength(64, { groups: [ValidationsGroupsEnum.default] }) + placeName?: string; + + @Expose() + @IsString({ groups: [ValidationsGroupsEnum.default] }) + @MaxLength(64, { groups: [ValidationsGroupsEnum.default] }) + @IsDefined({ groups: [ValidationsGroupsEnum.default] }) + city: string; + + @Expose() + @IsString({ groups: [ValidationsGroupsEnum.default] }) + @MaxLength(64, { groups: [ValidationsGroupsEnum.default] }) + county?: string | null; + + @Expose() + @IsString({ groups: [ValidationsGroupsEnum.default] }) + @MaxLength(64, { groups: [ValidationsGroupsEnum.default] }) + @IsDefined({ groups: [ValidationsGroupsEnum.default] }) + state: string; + + @Expose() + @IsString({ groups: [ValidationsGroupsEnum.default] }) + @MaxLength(64, { groups: [ValidationsGroupsEnum.default] }) + @IsDefined({ groups: [ValidationsGroupsEnum.default] }) + street: string; + + @Expose() + @IsString({ groups: [ValidationsGroupsEnum.default] }) + @MaxLength(64, { groups: [ValidationsGroupsEnum.default] }) + street2?: string | null; + + @Expose() + @IsString({ groups: [ValidationsGroupsEnum.default] }) + @MaxLength(10, { groups: [ValidationsGroupsEnum.default] }) + @IsDefined({ groups: [ValidationsGroupsEnum.default] }) + zipCode: string; + + @Expose() + @IsNumber({}, { groups: [ValidationsGroupsEnum.default] }) + @Type(() => Number) + latitude?: number | null; + + @Expose() + @IsNumber({}, { groups: [ValidationsGroupsEnum.default] }) + @Type(() => Number) + longitude?: number | null; +} diff --git a/backend_new/src/dtos/application-methods/application-method-get.dto.ts b/backend_new/src/dtos/application-methods/application-method-get.dto.ts new file mode 100644 index 0000000000..0ab87e14ae --- /dev/null +++ b/backend_new/src/dtos/application-methods/application-method-get.dto.ts @@ -0,0 +1,52 @@ +import { Expose, Type } from 'class-transformer'; +import { + IsBoolean, + IsEnum, + IsDefined, + IsString, + MaxLength, + ValidateNested, +} from 'class-validator'; +import { ValidationsGroupsEnum } from '../../enums/shared/validation-groups-enum'; +import { AbstractDTO } from '../shared/abstract.dto'; +import { ApiProperty } from '@nestjs/swagger'; +import { ApplicationMethodsTypeEnum } from '@prisma/client'; +import { PaperApplication } from '../paper-applications/paper-application-get.dto'; + +export class ApplicationMethod extends AbstractDTO { + @Expose() + @IsString({ groups: [ValidationsGroupsEnum.default] }) + @IsEnum(ApplicationMethodsTypeEnum, { + groups: [ValidationsGroupsEnum.default], + }) + @ApiProperty({ + enum: ApplicationMethodsTypeEnum, + enumName: 'ApplicationMethodsTypeEnum', + }) + @IsDefined({ groups: [ValidationsGroupsEnum.default] }) + type: ApplicationMethodsTypeEnum; + + @Expose() + @IsString({ groups: [ValidationsGroupsEnum.default] }) + @MaxLength(256, { groups: [ValidationsGroupsEnum.default] }) + label?: string | null; + + @Expose() + @IsString({ groups: [ValidationsGroupsEnum.default] }) + @MaxLength(4096, { groups: [ValidationsGroupsEnum.default] }) + externalReference?: string | null; + + @Expose() + @IsBoolean({ groups: [ValidationsGroupsEnum.default] }) + acceptsPostmarkedApplications?: boolean | null; + + @Expose() + @IsString({ groups: [ValidationsGroupsEnum.default] }) + @MaxLength(16, { groups: [ValidationsGroupsEnum.default] }) + phoneNumber?: string | null; + + @Expose() + @ValidateNested({ groups: [ValidationsGroupsEnum.default], each: true }) + @Type(() => PaperApplication) + paperApplications?: PaperApplication[] | null; +} diff --git a/backend_new/src/dtos/assets/asset-get.dto.ts b/backend_new/src/dtos/assets/asset-get.dto.ts new file mode 100644 index 0000000000..b39d3b23f2 --- /dev/null +++ b/backend_new/src/dtos/assets/asset-get.dto.ts @@ -0,0 +1,18 @@ +import { AbstractDTO } from '../shared/abstract.dto'; +import { Expose } from 'class-transformer'; +import { IsString, IsDefined, MaxLength } from 'class-validator'; +import { ValidationsGroupsEnum } from '../../enums/shared/validation-groups-enum'; + +export class Asset extends AbstractDTO { + @Expose() + @IsString({ groups: [ValidationsGroupsEnum.default] }) + @MaxLength(128, { groups: [ValidationsGroupsEnum.default] }) + @IsDefined({ groups: [ValidationsGroupsEnum.default] }) + fileId: string; + + @Expose() + @IsString({ groups: [ValidationsGroupsEnum.default] }) + @MaxLength(128, { groups: [ValidationsGroupsEnum.default] }) + @IsDefined({ groups: [ValidationsGroupsEnum.default] }) + label: string; +} diff --git a/backend_new/src/dtos/jurisdictions/jurisdiction-get.dto.ts b/backend_new/src/dtos/jurisdictions/jurisdiction-get.dto.ts new file mode 100644 index 0000000000..906d12b42a --- /dev/null +++ b/backend_new/src/dtos/jurisdictions/jurisdiction-get.dto.ts @@ -0,0 +1,76 @@ +import { AbstractDTO } from '../shared/abstract.dto'; +import { + IsString, + MaxLength, + IsDefined, + IsEnum, + ArrayMaxSize, + IsArray, + ValidateNested, + IsBoolean, +} from 'class-validator'; +import { ValidationsGroupsEnum } from '../../enums/shared/validation-groups-enum'; +import { LanguagesEnum } from '@prisma/client'; +import { Expose, Type } from 'class-transformer'; +import { MultiselectQuestion } from '../multiselect-questions/multiselect-question.dto'; + +export class Jurisdiction extends AbstractDTO { + @Expose() + @IsString({ groups: [ValidationsGroupsEnum.default] }) + @MaxLength(256, { groups: [ValidationsGroupsEnum.default] }) + @IsDefined({ groups: [ValidationsGroupsEnum.default] }) + name: string; + + @Expose() + @IsString({ groups: [ValidationsGroupsEnum.default] }) + notificationsSignUpURL?: string | null; + + @Expose() + @IsArray({ groups: [ValidationsGroupsEnum.default] }) + @ArrayMaxSize(256, { groups: [ValidationsGroupsEnum.default] }) + @IsEnum(LanguagesEnum, { + groups: [ValidationsGroupsEnum.default], + each: true, + }) + @IsDefined({ groups: [ValidationsGroupsEnum.default] }) + languages: LanguagesEnum[]; + + @Expose() + @ValidateNested({ groups: [ValidationsGroupsEnum.default] }) + @Type(() => MultiselectQuestion) + @IsDefined({ groups: [ValidationsGroupsEnum.default] }) + multiselectQuestions: MultiselectQuestion[]; + + @Expose() + @IsString({ groups: [ValidationsGroupsEnum.default] }) + partnerTerms?: string | null; + + @Expose() + @IsString({ groups: [ValidationsGroupsEnum.default] }) + @IsDefined({ groups: [ValidationsGroupsEnum.default] }) + publicUrl: string; + + @Expose() + @IsString({ groups: [ValidationsGroupsEnum.default] }) + @IsDefined({ groups: [ValidationsGroupsEnum.default] }) + emailFromAddress: string; + + @Expose() + @IsString({ groups: [ValidationsGroupsEnum.default] }) + @IsDefined({ groups: [ValidationsGroupsEnum.default] }) + rentalAssistanceDefault: string; + + @Expose() + @IsBoolean({ groups: [ValidationsGroupsEnum.default] }) + enablePartnerSettings?: boolean | null; + + @Expose() + @IsBoolean({ groups: [ValidationsGroupsEnum.default] }) + @IsDefined({ groups: [ValidationsGroupsEnum.default] }) + enableAccessibilityFeatures: boolean | null; + + @Expose() + @IsBoolean({ groups: [ValidationsGroupsEnum.default] }) + @IsDefined({ groups: [ValidationsGroupsEnum.default] }) + enableUtilitiesIncluded: boolean | null; +} diff --git a/backend_new/src/dtos/listings/listing-event.dto.ts b/backend_new/src/dtos/listings/listing-event.dto.ts new file mode 100644 index 0000000000..1d9a518a45 --- /dev/null +++ b/backend_new/src/dtos/listings/listing-event.dto.ts @@ -0,0 +1,56 @@ +import { Expose, Type } from 'class-transformer'; +import { + IsDate, + IsEnum, + IsDefined, + IsString, + ValidateNested, +} from 'class-validator'; +import { ValidationsGroupsEnum } from '../../enums/shared/validation-groups-enum'; +import { ListingEventsTypeEnum } from '@prisma/client'; +import { ApiProperty } from '@nestjs/swagger'; +import { AbstractDTO } from '../shared/abstract.dto'; +import { Asset } from '../assets/asset-get.dto'; + +export class ListingEvent extends AbstractDTO { + @Expose() + @IsDefined({ groups: [ValidationsGroupsEnum.default] }) + @IsEnum(ListingEventsTypeEnum, { groups: [ValidationsGroupsEnum.default] }) + @ApiProperty({ + enum: ListingEventsTypeEnum, + enumName: 'ListingEventsTypeEnum', + }) + type: ListingEventsTypeEnum; + + @Expose() + @IsDate({ groups: [ValidationsGroupsEnum.default] }) + @Type(() => Date) + startDate?: Date; + + @Expose() + @IsDate({ groups: [ValidationsGroupsEnum.default] }) + @Type(() => Date) + startTime?: Date; + + @Expose() + @IsDate({ groups: [ValidationsGroupsEnum.default] }) + @Type(() => Date) + endTime?: Date; + + @Expose() + @IsString({ groups: [ValidationsGroupsEnum.default] }) + url?: string | null; + + @Expose() + @IsString({ groups: [ValidationsGroupsEnum.default] }) + note?: string | null; + + @Expose() + @IsString({ groups: [ValidationsGroupsEnum.default] }) + label?: string | null; + + @Expose() + @ValidateNested({ groups: [ValidationsGroupsEnum.default] }) + @Type(() => Asset) + assets?: Asset; +} diff --git a/backend_new/src/dtos/listings/listing-feature.dto.ts b/backend_new/src/dtos/listings/listing-feature.dto.ts new file mode 100644 index 0000000000..86808a5bcc --- /dev/null +++ b/backend_new/src/dtos/listings/listing-feature.dto.ts @@ -0,0 +1,66 @@ +import { Expose } from 'class-transformer'; +import { IsBoolean } from 'class-validator'; +import { ValidationsGroupsEnum } from '../../enums/shared/validation-groups-enum'; +import { AbstractDTO } from '../shared/abstract.dto'; + +export class ListingFeatures extends AbstractDTO { + @Expose() + @IsBoolean({ groups: [ValidationsGroupsEnum.default] }) + elevator?: boolean | null; + + @Expose() + @IsBoolean({ groups: [ValidationsGroupsEnum.default] }) + wheelchairRamp?: boolean | null; + + @Expose() + @IsBoolean({ groups: [ValidationsGroupsEnum.default] }) + serviceAnimalsAllowed?: boolean | null; + + @Expose() + @IsBoolean({ groups: [ValidationsGroupsEnum.default] }) + accessibleParking?: boolean | null; + + @Expose() + @IsBoolean({ groups: [ValidationsGroupsEnum.default] }) + parkingOnSite?: boolean | null; + + @Expose() + @IsBoolean({ groups: [ValidationsGroupsEnum.default] }) + inUnitWasherDryer?: boolean | null; + + @Expose() + @IsBoolean({ groups: [ValidationsGroupsEnum.default] }) + laundryInBuilding?: boolean | null; + + @Expose() + @IsBoolean({ groups: [ValidationsGroupsEnum.default] }) + barrierFreeEntrance?: boolean | null; + + @Expose() + @IsBoolean({ groups: [ValidationsGroupsEnum.default] }) + rollInShower?: boolean | null; + + @Expose() + @IsBoolean({ groups: [ValidationsGroupsEnum.default] }) + grabBars?: boolean | null; + + @Expose() + @IsBoolean({ groups: [ValidationsGroupsEnum.default] }) + heatingInUnit?: boolean | null; + + @Expose() + @IsBoolean({ groups: [ValidationsGroupsEnum.default] }) + acInUnit?: boolean | null; + + @Expose() + @IsBoolean({ groups: [ValidationsGroupsEnum.default] }) + hearing?: boolean | null; + + @Expose() + @IsBoolean({ groups: [ValidationsGroupsEnum.default] }) + visual?: boolean | null; + + @Expose() + @IsBoolean({ groups: [ValidationsGroupsEnum.default] }) + mobility?: boolean | null; +} diff --git a/backend_new/src/dtos/listings/listing-get.dto.ts b/backend_new/src/dtos/listings/listing-get.dto.ts new file mode 100644 index 0000000000..a0bdf3363c --- /dev/null +++ b/backend_new/src/dtos/listings/listing-get.dto.ts @@ -0,0 +1,446 @@ +import { Expose, Type } from 'class-transformer'; +import { + IsBoolean, + IsDate, + IsDefined, + IsEmail, + IsEnum, + IsNumber, + IsString, + MaxLength, + ValidateNested, +} from 'class-validator'; +import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; +import { ValidationsGroupsEnum } from '../../enums/shared/validation-groups-enum'; +import { AbstractDTO } from '../shared/abstract.dto'; +import { + ApplicationAddressTypeEnum, + ApplicationMethodsTypeEnum, + ListingsStatusEnum, + ReviewOrderTypeEnum, +} from '@prisma/client'; +import { EnforceLowerCase } from '../../decorators/enforce-lower-case.decorator'; +import { ListingMultiselectQuestion } from './listing-multiselect-question.dto'; +import { ApplicationMethod } from '../application-methods/application-method-get.dto'; +import { Asset } from '../assets/asset-get.dto'; +import { ListingEvent } from './listing-event.dto'; +import { Address } from '../addresses/address-get.dto'; +import { Jurisdiction } from '../jurisdictions/jurisdiction-get.dto'; +import { ReservedCommunityType } from '../reserved-community-types/reserved-community-type-get.dto'; +import { ListingImage } from './listing-image.dto'; +import { ListingFeatures } from './listing-feature.dto'; +import { ListingUtilities } from './listing-utility.dto'; +import { Unit } from '../units/unit-get.dto'; +import { UnitsSummarized } from '../units/unit-summarized.dto'; +import { UnitsSummary } from '../units/units-summery-get.dto'; + +class ListingGet extends AbstractDTO { + @Expose() + @IsString({ groups: [ValidationsGroupsEnum.default] }) + additionalApplicationSubmissionNotes?: string | null; + + @Expose() + @IsBoolean({ groups: [ValidationsGroupsEnum.default] }) + digitalApplication?: boolean; + + @Expose() + @IsBoolean({ groups: [ValidationsGroupsEnum.default] }) + commonDigitalApplication?: boolean; + + @Expose() + @IsBoolean({ groups: [ValidationsGroupsEnum.default] }) + paperApplication?: boolean; + + @Expose() + @IsBoolean({ groups: [ValidationsGroupsEnum.default] }) + referralOpportunity?: boolean; + + @Expose() + @IsString({ groups: [ValidationsGroupsEnum.default] }) + accessibility?: string | null; + + @Expose() + @IsString({ groups: [ValidationsGroupsEnum.default] }) + amenities?: string | null; + + @Expose() + @IsNumber({}, { groups: [ValidationsGroupsEnum.default] }) + buildingTotalUnits?: number | null; + + @Expose() + @IsString({ groups: [ValidationsGroupsEnum.default] }) + developer?: string | null; + + @Expose() + @IsNumber({}, { groups: [ValidationsGroupsEnum.default] }) + householdSizeMax?: number | null; + + @Expose() + @IsNumber({}, { groups: [ValidationsGroupsEnum.default] }) + householdSizeMin?: number | null; + + @Expose() + @IsString({ groups: [ValidationsGroupsEnum.default] }) + neighborhood?: string | null; + + @Expose() + @IsString({ groups: [ValidationsGroupsEnum.default] }) + petPolicy?: string | null; + + @Expose() + @IsString({ groups: [ValidationsGroupsEnum.default] }) + smokingPolicy?: string | null; + + @Expose() + @IsNumber({}, { groups: [ValidationsGroupsEnum.default] }) + unitsAvailable?: number | null; + + @Expose() + @IsString({ groups: [ValidationsGroupsEnum.default] }) + unitAmenities?: string | null; + + @Expose() + @IsString({ groups: [ValidationsGroupsEnum.default] }) + servicesOffered?: string | null; + + @Expose() + @IsNumber({}, { groups: [ValidationsGroupsEnum.default] }) + yearBuilt?: number | null; + + @Expose() + @IsDate({ groups: [ValidationsGroupsEnum.default] }) + @Type(() => Date) + applicationDueDate?: Date | null; + + @Expose() + @IsDate({ groups: [ValidationsGroupsEnum.default] }) + @Type(() => Date) + applicationOpenDate?: Date | null; + + @Expose() + @IsString({ groups: [ValidationsGroupsEnum.default] }) + applicationFee?: string | null; + + @Expose() + @IsString({ groups: [ValidationsGroupsEnum.default] }) + applicationOrganization?: string | null; + + @Expose() + @IsString({ groups: [ValidationsGroupsEnum.default] }) + applicationPickUpAddressOfficeHours?: string | null; + + @Expose() + @IsEnum(ApplicationAddressTypeEnum, { + groups: [ValidationsGroupsEnum.default], + }) + @ApiProperty({ + enum: ApplicationAddressTypeEnum, + enumName: 'ApplicationAddressTypeEnum', + }) + applicationPickUpAddressType?: ApplicationAddressTypeEnum | null; + + @Expose() + @IsString({ groups: [ValidationsGroupsEnum.default] }) + applicationDropOffAddressOfficeHours?: string | null; + + @Expose() + @IsEnum(ApplicationAddressTypeEnum, { + groups: [ValidationsGroupsEnum.default], + }) + @ApiProperty({ + enum: ApplicationAddressTypeEnum, + enumName: 'ApplicationAddressTypeEnum', + }) + applicationDropOffAddressType?: ApplicationAddressTypeEnum | null; + + @Expose() + @IsEnum(ApplicationAddressTypeEnum, { + groups: [ValidationsGroupsEnum.default], + }) + @ApiProperty({ + enum: ApplicationAddressTypeEnum, + enumName: 'ApplicationAddressTypeEnum', + }) + applicationMailingAddressType?: ApplicationAddressTypeEnum | null; + + @Expose() + @IsString({ groups: [ValidationsGroupsEnum.default] }) + buildingSelectionCriteria?: string | null; + + @Expose() + @IsString({ groups: [ValidationsGroupsEnum.default] }) + costsNotIncluded?: string | null; + + @Expose() + @IsString({ groups: [ValidationsGroupsEnum.default] }) + creditHistory?: string | null; + + @Expose() + @IsString({ groups: [ValidationsGroupsEnum.default] }) + criminalBackground?: string | null; + + @Expose() + @IsString({ groups: [ValidationsGroupsEnum.default] }) + depositMin?: string | null; + + @Expose() + @IsString({ groups: [ValidationsGroupsEnum.default] }) + depositMax?: string | null; + + @Expose() + @IsString({ groups: [ValidationsGroupsEnum.default] }) + depositHelperText?: string | null; + + @Expose() + @IsBoolean({ groups: [ValidationsGroupsEnum.default] }) + disableUnitsAccordion?: boolean | null; + + @Expose() + @IsEmail({}, { groups: [ValidationsGroupsEnum.default] }) + @EnforceLowerCase() + leasingAgentEmail?: string | null; + + @Expose() + @IsString({ groups: [ValidationsGroupsEnum.default] }) + leasingAgentName?: string | null; + + @Expose() + @IsString({ groups: [ValidationsGroupsEnum.default] }) + leasingAgentOfficeHours?: string | null; + + @Expose() + @IsString({ groups: [ValidationsGroupsEnum.default] }) + leasingAgentPhone?: string | null; + + @Expose() + @IsString({ groups: [ValidationsGroupsEnum.default] }) + leasingAgentTitle?: string | null; + + @Expose() + @IsDefined({ groups: [ValidationsGroupsEnum.default] }) + @IsString({ groups: [ValidationsGroupsEnum.default] }) + name: string; + + @Expose() + @IsDate({ groups: [ValidationsGroupsEnum.default] }) + @Type(() => Date) + postmarkedApplicationsReceivedByDate?: Date | null; + + @Expose() + @IsString({ groups: [ValidationsGroupsEnum.default] }) + programRules?: string | null; + + @Expose() + @IsString({ groups: [ValidationsGroupsEnum.default] }) + rentalAssistance?: string | null; + + @Expose() + @IsString({ groups: [ValidationsGroupsEnum.default] }) + rentalHistory?: string | null; + + @Expose() + @IsString({ groups: [ValidationsGroupsEnum.default] }) + requiredDocuments?: string | null; + + @Expose() + @IsString({ groups: [ValidationsGroupsEnum.default] }) + specialNotes?: string | null; + + @Expose() + @IsNumber({}, { groups: [ValidationsGroupsEnum.default] }) + waitlistCurrentSize?: number | null; + + @Expose() + @IsNumber({}, { groups: [ValidationsGroupsEnum.default] }) + waitlistMaxSize?: number | null; + + @Expose() + @IsString({ groups: [ValidationsGroupsEnum.default] }) + whatToExpect?: string | null; + + @Expose() + @IsEnum(ListingsStatusEnum, { groups: [ValidationsGroupsEnum.default] }) + @ApiProperty({ enum: ListingsStatusEnum, enumName: 'ListingsStatusEnum' }) + status: ListingsStatusEnum; + + @Expose() + @IsEnum(ReviewOrderTypeEnum, { groups: [ValidationsGroupsEnum.default] }) + @ApiProperty({ + enum: ReviewOrderTypeEnum, + enumName: 'ReviewOrderTypeEnum', + }) + reviewOrderType?: ReviewOrderTypeEnum | null; + + @Expose() + applicationConfig?: Record; + + @Expose() + @IsBoolean({ groups: [ValidationsGroupsEnum.default] }) + @IsDefined({ groups: [ValidationsGroupsEnum.default] }) + displayWaitlistSize: boolean; + + @Expose() + @ApiProperty() + get showWaitlist(): boolean { + return ( + this.waitlistMaxSize !== null && + this.waitlistCurrentSize !== null && + this.waitlistCurrentSize < this.waitlistMaxSize + ); + } + + @Expose() + @IsString({ groups: [ValidationsGroupsEnum.default] }) + @MaxLength(4096, { groups: [ValidationsGroupsEnum.default] }) + reservedCommunityDescription?: string | null; + + @Expose() + @IsNumber({}, { groups: [ValidationsGroupsEnum.default] }) + reservedCommunityMinAge?: number | null; + + @Expose() + @IsString({ groups: [ValidationsGroupsEnum.default] }) + @MaxLength(4096, { groups: [ValidationsGroupsEnum.default] }) + resultLink?: string | null; + + @Expose() + @IsBoolean({ groups: [ValidationsGroupsEnum.default] }) + isWaitlistOpen?: boolean | null; + + @Expose() + @IsNumber({}, { groups: [ValidationsGroupsEnum.default] }) + waitlistOpenSpots?: number | null; + + @Expose() + @IsBoolean({ groups: [ValidationsGroupsEnum.default] }) + customMapPin?: boolean | null; + + @Expose() + @IsDate({ groups: [ValidationsGroupsEnum.default] }) + @Type(() => Date) + publishedAt?: Date | null; + + @Expose() + @IsDate({ groups: [ValidationsGroupsEnum.default] }) + @Type(() => Date) + closedAt?: Date | null; + + @Expose() + @IsDate({ groups: [ValidationsGroupsEnum.default] }) + @Type(() => Date) + afsLastRunAt?: Date | null; + + @Expose() + @IsDate({ groups: [ValidationsGroupsEnum.default] }) + @Type(() => Date) + lastApplicationUpdateAt?: Date | null; + + @Expose() + @ValidateNested({ groups: [ValidationsGroupsEnum.default], each: true }) + @Type(() => ListingMultiselectQuestion) + listingMultiselectQuestions?: ListingMultiselectQuestion[]; + + @Expose() + @ValidateNested({ groups: [ValidationsGroupsEnum.default], each: true }) + @IsDefined({ groups: [ValidationsGroupsEnum.default] }) + @Type(() => ApplicationMethod) + applicationMethods: ApplicationMethod[]; + + @Expose() + @ApiPropertyOptional() + get referralApplication(): ApplicationMethod | undefined { + return this.applicationMethods?.find( + (method) => method.type === ApplicationMethodsTypeEnum.Referral, + ); + } + + @Expose() + @ValidateNested({ groups: [ValidationsGroupsEnum.default], each: true }) + @Type(() => Asset) + @IsDefined({ groups: [ValidationsGroupsEnum.default] }) + assets: Asset[]; + + @Expose() + @ValidateNested({ groups: [ValidationsGroupsEnum.default], each: true }) + @Type(() => ListingEvent) + @IsDefined({ groups: [ValidationsGroupsEnum.default] }) + events: ListingEvent[]; + + @Expose() + @IsDefined({ groups: [ValidationsGroupsEnum.default] }) + @ValidateNested({ groups: [ValidationsGroupsEnum.default] }) + @Type(() => Address) + listingsBuildingAddress: Address; + + @Expose() + @ValidateNested({ groups: [ValidationsGroupsEnum.default] }) + @Type(() => Address) + listingsApplicationPickUpAddress?: Address; + + @Expose() + @ValidateNested({ groups: [ValidationsGroupsEnum.default] }) + @Type(() => Address) + listingsApplicationDropOffAddress?: Address; + + @Expose() + @ValidateNested({ groups: [ValidationsGroupsEnum.default] }) + @Type(() => Address) + listingsApplicationMailingAddress?: Address; + + @Expose() + @ValidateNested({ groups: [ValidationsGroupsEnum.default] }) + @Type(() => Address) + listingsLeasingAgentAddress?: Address; + + @Expose() + @ValidateNested({ groups: [ValidationsGroupsEnum.default] }) + @Type(() => Asset) + listingsBuildingSelectionCriteriaFile?: Asset | null; + + @Expose() + @ValidateNested({ groups: [ValidationsGroupsEnum.default] }) + @IsDefined({ groups: [ValidationsGroupsEnum.default] }) + @Type(() => Jurisdiction) + jurisdictions: Jurisdiction; + + @Expose() + @ValidateNested({ groups: [ValidationsGroupsEnum.default] }) + @Type(() => Asset) + listingsResult?: Asset | null; + + @Expose() + @ValidateNested({ groups: [ValidationsGroupsEnum.default] }) + @Type(() => ReservedCommunityType) + reservedCommunityTypes?: ReservedCommunityType; + + @Expose() + @ValidateNested({ groups: [ValidationsGroupsEnum.default], each: true }) + @Type(() => ListingImage) + listingImages?: ListingImage[] | null; + + @Expose() + @ValidateNested({ groups: [ValidationsGroupsEnum.default], each: true }) + @Type(() => ListingFeatures) + listingFeatures?: ListingFeatures; + + @Expose() + @ValidateNested({ groups: [ValidationsGroupsEnum.default], each: true }) + @Type(() => ListingUtilities) + listingUtilities?: ListingUtilities; + + @Expose() + @ValidateNested({ groups: [ValidationsGroupsEnum.default], each: true }) + @Type(() => Unit) + units: Unit[]; + + @Expose() + @ApiProperty({ type: UnitsSummarized }) + unitsSummarized: UnitsSummarized | undefined; + + @Expose() + @ValidateNested({ groups: [ValidationsGroupsEnum.default], each: true }) + @Type(() => UnitsSummary) + unitsSummary: UnitsSummary[]; +} + +export { ListingGet as default, ListingGet }; diff --git a/backend_new/src/dtos/listings/listing-image.dto.ts b/backend_new/src/dtos/listings/listing-image.dto.ts new file mode 100644 index 0000000000..69a4729304 --- /dev/null +++ b/backend_new/src/dtos/listings/listing-image.dto.ts @@ -0,0 +1,15 @@ +import { Expose, Type } from 'class-transformer'; +import { IsNumber, IsDefined } from 'class-validator'; +import { ValidationsGroupsEnum } from '../../enums/shared/validation-groups-enum'; +import { Asset } from '../assets/asset-get.dto'; + +export class ListingImage { + @Expose() + @Type(() => Asset) + @IsDefined({ groups: [ValidationsGroupsEnum.default] }) + assets: Asset; + + @Expose() + @IsNumber({}, { groups: [ValidationsGroupsEnum.default] }) + ordinal?: number | null; +} diff --git a/backend_new/src/dtos/listings/listing-multiselect-question.dto.ts b/backend_new/src/dtos/listings/listing-multiselect-question.dto.ts new file mode 100644 index 0000000000..2794a4b177 --- /dev/null +++ b/backend_new/src/dtos/listings/listing-multiselect-question.dto.ts @@ -0,0 +1,20 @@ +import { MultiselectQuestion } from '../multiselect-questions/multiselect-question.dto'; +import { Expose, Type } from 'class-transformer'; +import { IsNumber, IsDefined } from 'class-validator'; +import { ValidationsGroupsEnum } from '../../enums/shared/validation-groups-enum'; +import { ListingGet } from './listing-get.dto'; + +export class ListingMultiselectQuestion { + @Type(() => ListingGet) + @IsDefined({ groups: [ValidationsGroupsEnum.default] }) + listings: ListingGet; + + @Expose() + @Type(() => MultiselectQuestion) + @IsDefined({ groups: [ValidationsGroupsEnum.default] }) + multiselectQuestions: MultiselectQuestion; + + @Expose() + @IsNumber({}, { groups: [ValidationsGroupsEnum.default] }) + ordinal?: number | null; +} diff --git a/backend_new/src/dtos/listings/listing-utility.dto.ts b/backend_new/src/dtos/listings/listing-utility.dto.ts new file mode 100644 index 0000000000..aaafc839dd --- /dev/null +++ b/backend_new/src/dtos/listings/listing-utility.dto.ts @@ -0,0 +1,38 @@ +import { Expose } from 'class-transformer'; +import { IsBoolean } from 'class-validator'; +import { ValidationsGroupsEnum } from '../../enums/shared/validation-groups-enum'; +import { AbstractDTO } from '../shared/abstract.dto'; + +export class ListingUtilities extends AbstractDTO { + @Expose() + @IsBoolean({ groups: [ValidationsGroupsEnum.default] }) + water?: boolean | null; + + @Expose() + @IsBoolean({ groups: [ValidationsGroupsEnum.default] }) + gas?: boolean | null; + + @Expose() + @IsBoolean({ groups: [ValidationsGroupsEnum.default] }) + trash?: boolean | null; + + @Expose() + @IsBoolean({ groups: [ValidationsGroupsEnum.default] }) + sewer?: boolean | null; + + @Expose() + @IsBoolean({ groups: [ValidationsGroupsEnum.default] }) + electricity?: boolean | null; + + @Expose() + @IsBoolean({ groups: [ValidationsGroupsEnum.default] }) + cable?: boolean | null; + + @Expose() + @IsBoolean({ groups: [ValidationsGroupsEnum.default] }) + phone?: boolean | null; + + @Expose() + @IsBoolean({ groups: [ValidationsGroupsEnum.default] }) + internet?: boolean | null; +} diff --git a/backend_new/src/dtos/listings/listings-filter-params.dto.ts b/backend_new/src/dtos/listings/listings-filter-params.dto.ts new file mode 100644 index 0000000000..61cb121163 --- /dev/null +++ b/backend_new/src/dtos/listings/listings-filter-params.dto.ts @@ -0,0 +1,71 @@ +import { BaseFilter } from '../shared/base-filter.dto'; +import { Expose } from 'class-transformer'; +import { ApiProperty } from '@nestjs/swagger'; +import { IsEnum, IsNumberString, IsString } from 'class-validator'; +import { ValidationsGroupsEnum } from '../../enums/shared/validation-groups-enum'; +import { ListingFilterKeys } from '../../enums/listings/filter-key-enum'; +import { ListingsStatusEnum } from '@prisma/client'; + +export class ListingFilterParams extends BaseFilter { + @Expose() + @ApiProperty({ + type: String, + example: 'Coliseum', + required: false, + }) + @IsString({ groups: [ValidationsGroupsEnum.default] }) + [ListingFilterKeys.name]?: string; + + @Expose() + @ApiProperty({ + enum: Object.keys(ListingsStatusEnum), + example: 'active', + required: false, + }) + @IsEnum(ListingsStatusEnum, { groups: [ValidationsGroupsEnum.default] }) + [ListingFilterKeys.status]?: ListingsStatusEnum; + + @Expose() + @ApiProperty({ + type: String, + example: 'Fox Creek', + required: false, + }) + @IsString({ groups: [ValidationsGroupsEnum.default] }) + [ListingFilterKeys.neighborhood]?: string; + + @Expose() + @ApiProperty({ + type: Number, + example: '3', + required: false, + }) + @IsNumberString({}, { groups: [ValidationsGroupsEnum.default] }) + [ListingFilterKeys.bedrooms]?: number; + + @Expose() + @ApiProperty({ + type: String, + example: '48211', + required: false, + }) + [ListingFilterKeys.zipcode]?: string; + + @Expose() + @ApiProperty({ + type: String, + example: 'FAB1A3C6-965E-4054-9A48-A282E92E9426', + required: false, + }) + @IsString({ groups: [ValidationsGroupsEnum.default] }) + [ListingFilterKeys.leasingAgents]?: string; + + @Expose() + @ApiProperty({ + type: String, + example: 'bab6cb4f-7a5a-4ee5-b327-0c2508807780', + required: false, + }) + @IsString({ groups: [ValidationsGroupsEnum.default] }) + [ListingFilterKeys.jurisdiction]?: string; +} diff --git a/backend_new/src/dtos/listings/listings-query-params.dto.ts b/backend_new/src/dtos/listings/listings-query-params.dto.ts new file mode 100644 index 0000000000..dfca9471ed --- /dev/null +++ b/backend_new/src/dtos/listings/listings-query-params.dto.ts @@ -0,0 +1,97 @@ +import { PaginationAllowsAllQueryParams } from '../shared/pagination.dto'; +import { Expose, Type } from 'class-transformer'; +import { ApiProperty, getSchemaPath } from '@nestjs/swagger'; +import { ListingFilterParams } from './listings-filter-params.dto'; +import { + ArrayMaxSize, + IsArray, + IsEnum, + IsString, + MinLength, + Validate, + ValidateNested, +} from 'class-validator'; +import { ValidationsGroupsEnum } from '../../enums/shared/validation-groups-enum'; +import { ListingOrderByKeys } from '../../enums/listings/order-by-enum'; +import { ListingViews } from '../../enums/listings/view-enum'; +import { OrderByEnum } from '../../enums/shared/order-by-enum'; +import { OrderQueryParamValidator } from '../../utilities/order-by-validator'; + +export class ListingsQueryParams extends PaginationAllowsAllQueryParams { + @Expose() + @ApiProperty({ + name: 'filter', + required: false, + type: [String], + items: { + $ref: getSchemaPath(ListingFilterParams), + }, + example: { $comparison: '=', status: 'active' }, + }) + @IsArray({ groups: [ValidationsGroupsEnum.default] }) + @ArrayMaxSize(16, { groups: [ValidationsGroupsEnum.default] }) + @Type(() => ListingFilterParams) + @ValidateNested({ groups: [ValidationsGroupsEnum.default], each: true }) + filter?: ListingFilterParams[]; + + @Expose() + @ApiProperty({ + enum: ListingViews, + required: false, + enumName: 'ListingViews', + example: 'full', + }) + @IsEnum(ListingViews, { + groups: [ValidationsGroupsEnum.default], + }) + view?: ListingViews; + + @Expose() + @ApiProperty({ + name: 'orderBy', + required: false, + enumName: 'ListingOrderByKeys', + example: '["updatedAt"]', + isArray: true, + }) + @IsArray({ groups: [ValidationsGroupsEnum.default] }) + @ArrayMaxSize(16, { groups: [ValidationsGroupsEnum.default] }) + @Type(() => ListingFilterParams) + @IsEnum(ListingOrderByKeys, { + groups: [ValidationsGroupsEnum.default], + each: true, + }) + @Validate(OrderQueryParamValidator, { + groups: [ValidationsGroupsEnum.default], + }) + orderBy?: ListingOrderByKeys[]; + + @Expose() + @ApiProperty({ + enum: OrderByEnum, + example: '["desc"]', + default: '["desc"]', + required: false, + isArray: true, + }) + @IsArray({ groups: [ValidationsGroupsEnum.default] }) + @ArrayMaxSize(16, { groups: [ValidationsGroupsEnum.default] }) + @IsEnum(OrderByEnum, { groups: [ValidationsGroupsEnum.default], each: true }) + @Validate(OrderQueryParamValidator, { + groups: [ValidationsGroupsEnum.default], + }) + orderDir?: OrderByEnum[]; + + @Expose() + @ApiProperty({ + type: String, + example: 'search', + required: false, + }) + @IsString({ groups: [ValidationsGroupsEnum.default] }) + @MinLength(3, { + message: 'Search must be at least 3 characters', + groups: [ValidationsGroupsEnum.default], + }) + search?: string; +} diff --git a/backend_new/src/dtos/listings/listings-retrieve-params.dto.ts b/backend_new/src/dtos/listings/listings-retrieve-params.dto.ts new file mode 100644 index 0000000000..e2d91294fc --- /dev/null +++ b/backend_new/src/dtos/listings/listings-retrieve-params.dto.ts @@ -0,0 +1,19 @@ +import { Expose } from 'class-transformer'; +import { ApiProperty } from '@nestjs/swagger'; +import { IsEnum } from 'class-validator'; +import { ValidationsGroupsEnum } from '../../enums/shared/validation-groups-enum'; +import { ListingViews } from '../../enums/listings/view-enum'; + +export class ListingsRetrieveParams { + @Expose() + @ApiProperty({ + enum: ListingViews, + required: false, + enumName: 'ListingViews', + example: 'full', + }) + @IsEnum(ListingViews, { + groups: [ValidationsGroupsEnum.default], + }) + view?: ListingViews; +} diff --git a/backend_new/src/dtos/listings/paginated-listing.dto.ts b/backend_new/src/dtos/listings/paginated-listing.dto.ts new file mode 100644 index 0000000000..6f5a99f9f3 --- /dev/null +++ b/backend_new/src/dtos/listings/paginated-listing.dto.ts @@ -0,0 +1,6 @@ +import { PaginationFactory } from '../shared/pagination.dto'; +import { ListingGet } from './listing-get.dto'; + +export class PaginatedListingDto extends PaginationFactory( + ListingGet, +) {} diff --git a/backend_new/src/dtos/multiselect-questions/multiselect-link.dto.ts b/backend_new/src/dtos/multiselect-questions/multiselect-link.dto.ts new file mode 100644 index 0000000000..9b9da8446a --- /dev/null +++ b/backend_new/src/dtos/multiselect-questions/multiselect-link.dto.ts @@ -0,0 +1,18 @@ +import { Expose } from 'class-transformer'; +import { IsString, IsDefined } from 'class-validator'; +import { ValidationsGroupsEnum } from '../../enums/shared/validation-groups-enum'; +import { ApiProperty } from '@nestjs/swagger'; + +export class MultiselectLink { + @Expose() + @IsString({ groups: [ValidationsGroupsEnum.default] }) + @IsDefined({ groups: [ValidationsGroupsEnum.default] }) + @ApiProperty() + title: string; + + @Expose() + @IsString({ groups: [ValidationsGroupsEnum.default] }) + @IsDefined({ groups: [ValidationsGroupsEnum.default] }) + @ApiProperty() + url: string; +} diff --git a/backend_new/src/dtos/multiselect-questions/multiselect-option.dto.ts b/backend_new/src/dtos/multiselect-questions/multiselect-option.dto.ts new file mode 100644 index 0000000000..afc9981433 --- /dev/null +++ b/backend_new/src/dtos/multiselect-questions/multiselect-option.dto.ts @@ -0,0 +1,51 @@ +import { Expose, Type } from 'class-transformer'; +import { + IsBoolean, + IsNumber, + IsDefined, + IsString, + ValidateNested, +} from 'class-validator'; +import { ValidationsGroupsEnum } from '../../enums/shared/validation-groups-enum'; +import { ApiProperty } from '@nestjs/swagger'; +import { MultiselectLink } from './multiselect-link.dto'; + +export class MultiselectOption { + @Expose() + @IsString({ groups: [ValidationsGroupsEnum.default] }) + @ApiProperty() + @IsDefined({ groups: [ValidationsGroupsEnum.default] }) + text: string; + + @Expose() + @IsString({ groups: [ValidationsGroupsEnum.default] }) + @ApiProperty({ required: false }) + untranslatedText?: string; + + @Expose() + @IsNumber({}, { groups: [ValidationsGroupsEnum.default] }) + @ApiProperty() + @IsDefined({ groups: [ValidationsGroupsEnum.default] }) + ordinal: number; + + @Expose() + @IsString({ groups: [ValidationsGroupsEnum.default] }) + @ApiProperty({ required: false }) + description?: string | null; + + @Expose() + @ValidateNested({ groups: [ValidationsGroupsEnum.default], each: true }) + @Type(() => MultiselectLink) + @ApiProperty({ type: [MultiselectLink], required: false }) + links?: MultiselectLink[] | null; + + @Expose() + @IsBoolean({ groups: [ValidationsGroupsEnum.default] }) + @ApiProperty({ required: false }) + collectAddress?: boolean | null; + + @Expose() + @IsBoolean({ groups: [ValidationsGroupsEnum.default] }) + @ApiProperty({ required: false }) + exclusive?: boolean | null; +} diff --git a/backend_new/src/dtos/multiselect-questions/multiselect-question.dto.ts b/backend_new/src/dtos/multiselect-questions/multiselect-question.dto.ts new file mode 100644 index 0000000000..dc558f0670 --- /dev/null +++ b/backend_new/src/dtos/multiselect-questions/multiselect-question.dto.ts @@ -0,0 +1,87 @@ +import { Expose, Type } from 'class-transformer'; +import { + IsString, + ValidateNested, + ArrayMaxSize, + IsBoolean, + IsEnum, + IsDefined, +} from 'class-validator'; +import { ValidationsGroupsEnum } from '../../enums/shared/validation-groups-enum'; +import { ApiProperty } from '@nestjs/swagger'; +import { AbstractDTO } from '../shared/abstract.dto'; +import { ListingMultiselectQuestion } from '../listings/listing-multiselect-question.dto'; +import { MultiselectQuestionsApplicationSectionEnum } from '@prisma/client'; +import { Jurisdiction } from '../jurisdictions/jurisdiction-get.dto'; +import { MultiselectLink } from './multiselect-link.dto'; +import { MultiselectOption } from './multiselect-option.dto'; + +class MultiselectQuestion extends AbstractDTO { + @Expose() + @IsString({ groups: [ValidationsGroupsEnum.default] }) + @IsDefined({ groups: [ValidationsGroupsEnum.default] }) + text: string; + + @Expose() + @IsString({ groups: [ValidationsGroupsEnum.default] }) + untranslatedText?: string; + + @Expose() + @IsString({ groups: [ValidationsGroupsEnum.default] }) + untranslatedOptOutText?: string; + + @Expose() + @IsString({ groups: [ValidationsGroupsEnum.default] }) + subText?: string | null; + + @Expose() + @IsString({ groups: [ValidationsGroupsEnum.default] }) + description?: string | null; + + @Expose() + @ValidateNested({ groups: [ValidationsGroupsEnum.default], each: true }) + @Type(() => MultiselectLink) + @ApiProperty({ type: [MultiselectLink] }) + links?: MultiselectLink[] | null; + + @Expose() + @ValidateNested({ groups: [ValidationsGroupsEnum.default], each: true }) + @Type(() => ListingMultiselectQuestion) + @IsDefined({ groups: [ValidationsGroupsEnum.default] }) + listings: ListingMultiselectQuestion[]; + + @Expose() + @ValidateNested({ groups: [ValidationsGroupsEnum.default] }) + @IsDefined({ groups: [ValidationsGroupsEnum.default] }) + @Type(() => Jurisdiction) + jurisdictions: Jurisdiction[]; + + @Expose() + @ArrayMaxSize(64, { groups: [ValidationsGroupsEnum.default] }) + @ValidateNested({ groups: [ValidationsGroupsEnum.default], each: true }) + @Type(() => MultiselectOption) + @ApiProperty({ type: [MultiselectOption] }) + options?: MultiselectOption[] | null; + + @Expose() + @IsString({ groups: [ValidationsGroupsEnum.default] }) + optOutText?: string | null; + + @Expose() + @IsBoolean({ groups: [ValidationsGroupsEnum.default] }) + @ApiProperty() + hideFromListing?: boolean; + + @Expose() + @IsEnum(MultiselectQuestionsApplicationSectionEnum, { + groups: [ValidationsGroupsEnum.default], + }) + @ApiProperty({ + enum: MultiselectQuestionsApplicationSectionEnum, + enumName: 'MultiselectQuestionsApplicationSectionEnum', + }) + @IsDefined({ groups: [ValidationsGroupsEnum.default] }) + applicationSection: MultiselectQuestionsApplicationSectionEnum; +} + +export { MultiselectQuestion as default, MultiselectQuestion }; diff --git a/backend_new/src/dtos/paper-applications/paper-application-get.dto.ts b/backend_new/src/dtos/paper-applications/paper-application-get.dto.ts new file mode 100644 index 0000000000..48a6921d73 --- /dev/null +++ b/backend_new/src/dtos/paper-applications/paper-application-get.dto.ts @@ -0,0 +1,21 @@ +import { AbstractDTO } from '../shared/abstract.dto'; +import { LanguagesEnum } from '@prisma/client'; +import { Expose, Type } from 'class-transformer'; +import { IsEnum, IsDefined, ValidateNested } from 'class-validator'; +import { ValidationsGroupsEnum } from '../../enums/shared/validation-groups-enum'; +import { ApiProperty } from '@nestjs/swagger'; +import { Asset } from '../assets/asset-get.dto'; + +export class PaperApplication extends AbstractDTO { + @Expose() + @IsEnum(LanguagesEnum, { groups: [ValidationsGroupsEnum.default] }) + @IsDefined({ groups: [ValidationsGroupsEnum.default] }) + @ApiProperty({ enum: LanguagesEnum, enumName: 'LanguagesEnum' }) + language: LanguagesEnum; + + @Expose() + @ValidateNested({ groups: [ValidationsGroupsEnum.default] }) + @IsDefined({ groups: [ValidationsGroupsEnum.default] }) + @Type(() => Asset) + assets: Asset; +} diff --git a/backend_new/src/dtos/reserved-community-types/reserved-community-type-get.dto.ts b/backend_new/src/dtos/reserved-community-types/reserved-community-type-get.dto.ts new file mode 100644 index 0000000000..d67d6377b0 --- /dev/null +++ b/backend_new/src/dtos/reserved-community-types/reserved-community-type-get.dto.ts @@ -0,0 +1,17 @@ +import { Expose } from 'class-transformer'; +import { IsDefined, IsString, MaxLength } from 'class-validator'; +import { ValidationsGroupsEnum } from '../../enums/shared/validation-groups-enum'; +import { AbstractDTO } from '../shared/abstract.dto'; + +export class ReservedCommunityType extends AbstractDTO { + @Expose() + @IsString({ groups: [ValidationsGroupsEnum.default] }) + @IsDefined({ groups: [ValidationsGroupsEnum.default] }) + @MaxLength(256, { groups: [ValidationsGroupsEnum.default] }) + name: string; + + @Expose() + @IsString({ groups: [ValidationsGroupsEnum.default] }) + @MaxLength(2048, { groups: [ValidationsGroupsEnum.default] }) + description?: string | null; +} diff --git a/backend_new/src/dtos/shared/abstract.dto.ts b/backend_new/src/dtos/shared/abstract.dto.ts new file mode 100644 index 0000000000..0ae68e27e7 --- /dev/null +++ b/backend_new/src/dtos/shared/abstract.dto.ts @@ -0,0 +1,23 @@ +import { Expose, Type } from 'class-transformer'; +import { IsDate, IsDefined, IsString, IsUUID } from 'class-validator'; +import { ValidationsGroupsEnum } from '../../enums/shared/validation-groups-enum'; + +export class AbstractDTO { + @Expose() + @IsString({ groups: [ValidationsGroupsEnum.default] }) + @IsUUID(4, { groups: [ValidationsGroupsEnum.default] }) + @IsDefined({ groups: [ValidationsGroupsEnum.default] }) + id: string; + + @Expose() + @IsDate({ groups: [ValidationsGroupsEnum.default] }) + @IsDefined({ groups: [ValidationsGroupsEnum.default] }) + @Type(() => Date) + createdAt: Date; + + @Expose() + @IsDate({ groups: [ValidationsGroupsEnum.default] }) + @IsDefined({ groups: [ValidationsGroupsEnum.default] }) + @Type(() => Date) + updatedAt: Date; +} diff --git a/backend_new/src/dtos/shared/base-filter.dto.ts b/backend_new/src/dtos/shared/base-filter.dto.ts new file mode 100644 index 0000000000..9b756adec9 --- /dev/null +++ b/backend_new/src/dtos/shared/base-filter.dto.ts @@ -0,0 +1,25 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { Expose } from 'class-transformer'; +import { IsEnum } from 'class-validator'; +import { ValidationsGroupsEnum } from '../../enums/shared/validation-groups-enum'; + +// Add other comparisons as needed (>, <, etc) +export enum Compare { + '=' = '=', + '<>' = '<>', + 'IN' = 'IN', + '>=' = '>=', + '<=' = '<=', + 'NA' = 'NA', // For filters that don't use the comparison param +} + +export class BaseFilter { + @Expose() + @ApiProperty({ + enum: Object.keys(Compare), + example: '=', + default: Compare['='], + }) + @IsEnum(Compare, { groups: [ValidationsGroupsEnum.default] }) + $comparison: Compare; +} diff --git a/backend_new/src/dtos/shared/min-max-currency.dto.ts b/backend_new/src/dtos/shared/min-max-currency.dto.ts new file mode 100644 index 0000000000..05f0bc757d --- /dev/null +++ b/backend_new/src/dtos/shared/min-max-currency.dto.ts @@ -0,0 +1,18 @@ +import { Expose } from 'class-transformer'; +import { IsDefined, IsString } from 'class-validator'; +import { ValidationsGroupsEnum } from '../../enums/shared/validation-groups-enum'; +import { ApiProperty } from '@nestjs/swagger'; + +export class MinMaxCurrency { + @Expose() + @IsDefined({ groups: [ValidationsGroupsEnum.default] }) + @IsString({ groups: [ValidationsGroupsEnum.default] }) + @ApiProperty() + min: string; + + @Expose() + @IsDefined({ groups: [ValidationsGroupsEnum.default] }) + @IsString({ groups: [ValidationsGroupsEnum.default] }) + @ApiProperty() + max: string; +} diff --git a/backend_new/src/dtos/shared/min-max.dto.ts b/backend_new/src/dtos/shared/min-max.dto.ts new file mode 100644 index 0000000000..80b99edeb5 --- /dev/null +++ b/backend_new/src/dtos/shared/min-max.dto.ts @@ -0,0 +1,18 @@ +import { Expose } from 'class-transformer'; +import { IsDefined, IsNumber } from 'class-validator'; +import { ValidationsGroupsEnum } from '../../enums/shared/validation-groups-enum'; +import { ApiProperty } from '@nestjs/swagger'; + +export class MinMax { + @Expose() + @IsDefined({ groups: [ValidationsGroupsEnum.default] }) + @IsNumber({}, { groups: [ValidationsGroupsEnum.default] }) + @ApiProperty() + min: number; + + @Expose() + @IsDefined({ groups: [ValidationsGroupsEnum.default] }) + @IsNumber({}, { groups: [ValidationsGroupsEnum.default] }) + @ApiProperty() + max: number; +} diff --git a/backend_new/src/dtos/shared/pagination.dto.ts b/backend_new/src/dtos/shared/pagination.dto.ts new file mode 100644 index 0000000000..1507e2653f --- /dev/null +++ b/backend_new/src/dtos/shared/pagination.dto.ts @@ -0,0 +1,140 @@ +import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; +import { Expose, Transform, TransformFnParams, Type } from 'class-transformer'; +import { + IsNumber, + registerDecorator, + ValidationOptions, +} from 'class-validator'; +import { ValidationsGroupsEnum } from '../../enums/shared/validation-groups-enum'; +import { ClassType } from 'class-transformer/ClassTransformer'; + +export class PaginationMeta { + @Expose() + currentPage: number; + @Expose() + itemCount: number; + @Expose() + itemsPerPage: number; + @Expose() + totalItems: number; + @Expose() + totalPages: number; +} + +export interface Pagination { + items: T[]; + meta: PaginationMeta; +} + +export function PaginationFactory( + classType: ClassType, +): ClassType> { + class PaginationHost implements Pagination { + @ApiProperty({ type: () => classType, isArray: true }) + @Expose() + @Type(() => classType) + items: T[]; + @Expose() + meta: PaginationMeta; + } + return PaginationHost; +} + +export class PaginationQueryParams { + @Expose() + @ApiPropertyOptional({ + type: Number, + example: 1, + required: false, + default: 1, + }) + @IsNumber({}, { groups: [ValidationsGroupsEnum.default] }) + @Transform( + (value: TransformFnParams) => (value?.value ? parseInt(value.value) : 1), + { + toClassOnly: true, + }, + ) + page?: number; + + @Expose() + @ApiPropertyOptional({ + type: Number, + example: 10, + required: false, + default: 10, + }) + @IsNumber({}, { groups: [ValidationsGroupsEnum.default] }) + @Transform( + (value: TransformFnParams) => (value?.value ? parseInt(value.value) : 10), + { + toClassOnly: true, + }, + ) + limit?: number; +} + +export class PaginationAllowsAllQueryParams { + @Expose() + @ApiPropertyOptional({ + type: Number, + example: 1, + required: false, + default: 1, + }) + @IsNumber({}, { groups: [ValidationsGroupsEnum.default] }) + @Transform( + (value: TransformFnParams) => (value?.value ? parseInt(value.value) : 1), + { + toClassOnly: true, + }, + ) + page?: number; + + @Expose() + @ApiPropertyOptional({ + type: "number | 'all'", + example: 10, + required: false, + default: 10, + }) + @IsNumberOrAll({ + message: 'Limit must be a number or "All"', + groups: [ValidationsGroupsEnum.default], + }) + @Transform( + (value: TransformFnParams) => { + if (value?.value === 'all') { + return value; + } + return value?.value ? parseInt(value.value) : 10; + }, + { + toClassOnly: true, + }, + ) + limit?: number | 'all'; +} + +/* + validates if the value is either a number or the string 'all' +*/ +function IsNumberOrAll(validationOptions?: ValidationOptions) { + // eslint-disable-next-line @typescript-eslint/ban-types + return function (object: Object, propertyName: string) { + registerDecorator({ + name: 'isNumberOrAll', + target: object.constructor, + propertyName: propertyName, + options: validationOptions, + validator: { + validate(value: unknown) { + return ( + (typeof value === 'number' && !isNaN(value)) || + (typeof value === 'string' && value === 'all') + ); + }, + }, + }); + }; +} diff --git a/backend_new/src/dtos/units/ami-chart-get.dto.ts b/backend_new/src/dtos/units/ami-chart-get.dto.ts new file mode 100644 index 0000000000..59f70377c0 --- /dev/null +++ b/backend_new/src/dtos/units/ami-chart-get.dto.ts @@ -0,0 +1,18 @@ +import { IsDefined, IsString, ValidateNested } from 'class-validator'; +import { Expose, Type } from 'class-transformer'; +import { ValidationsGroupsEnum } from '../../enums/shared/validation-groups-enum'; +import { AbstractDTO } from '../shared/abstract.dto'; +import { AmiChartItem } from './ami-chart-item-get.dto'; + +export class AmiChart extends AbstractDTO { + @Expose() + @IsDefined({ groups: [ValidationsGroupsEnum.default] }) + @ValidateNested({ groups: [ValidationsGroupsEnum.default], each: true }) + @Type(() => AmiChartItem) + items: AmiChartItem[]; + + @Expose() + @IsString({ groups: [ValidationsGroupsEnum.default] }) + @IsDefined({ groups: [ValidationsGroupsEnum.default] }) + name: string; +} diff --git a/backend_new/src/dtos/units/ami-chart-item-get.dto.ts b/backend_new/src/dtos/units/ami-chart-item-get.dto.ts new file mode 100644 index 0000000000..ae995f307c --- /dev/null +++ b/backend_new/src/dtos/units/ami-chart-item-get.dto.ts @@ -0,0 +1,20 @@ +import { Expose } from 'class-transformer'; +import { IsNumber, IsDefined } from 'class-validator'; +import { ValidationsGroupsEnum } from '../../enums/shared/validation-groups-enum'; + +export class AmiChartItem { + @Expose() + @IsNumber({}, { groups: [ValidationsGroupsEnum.default] }) + @IsDefined({ groups: [ValidationsGroupsEnum.default] }) + percentOfAmi: number; + + @Expose() + @IsNumber({}, { groups: [ValidationsGroupsEnum.default] }) + @IsDefined({ groups: [ValidationsGroupsEnum.default] }) + householdSize: number; + + @Expose() + @IsNumber({}, { groups: [ValidationsGroupsEnum.default] }) + @IsDefined({ groups: [ValidationsGroupsEnum.default] }) + income: number; +} diff --git a/backend_new/src/dtos/units/ami-chart-override-get.dto.ts b/backend_new/src/dtos/units/ami-chart-override-get.dto.ts new file mode 100644 index 0000000000..1fb257c1e1 --- /dev/null +++ b/backend_new/src/dtos/units/ami-chart-override-get.dto.ts @@ -0,0 +1,13 @@ +import { AbstractDTO } from '../shared/abstract.dto'; +import { Expose, Type } from 'class-transformer'; +import { IsDefined, ValidateNested } from 'class-validator'; +import { ValidationsGroupsEnum } from '../../enums/shared/validation-groups-enum'; +import { AmiChartItem } from './ami-chart-item-get.dto'; + +export class UnitAmiChartOverride extends AbstractDTO { + @Expose() + @IsDefined({ groups: [ValidationsGroupsEnum.default] }) + @ValidateNested({ groups: [ValidationsGroupsEnum.default], each: true }) + @Type(() => AmiChartItem) + items: AmiChartItem[]; +} diff --git a/backend_new/src/dtos/units/hmi-get.dto.ts b/backend_new/src/dtos/units/hmi-get.dto.ts new file mode 100644 index 0000000000..32e336d409 --- /dev/null +++ b/backend_new/src/dtos/units/hmi-get.dto.ts @@ -0,0 +1,11 @@ +import { ApiProperty } from '@nestjs/swagger'; + +type AnyDict = { [key: string]: unknown }; + +export class HMI { + @ApiProperty() + columns: AnyDict; + + @ApiProperty({ type: [Object] }) + rows: AnyDict[]; +} diff --git a/backend_new/src/dtos/units/unit-accessibility-priority-type-get.dto.ts b/backend_new/src/dtos/units/unit-accessibility-priority-type-get.dto.ts new file mode 100644 index 0000000000..f2cc06fe24 --- /dev/null +++ b/backend_new/src/dtos/units/unit-accessibility-priority-type-get.dto.ts @@ -0,0 +1,12 @@ +import { Expose } from 'class-transformer'; +import { IsDefined, IsString, MaxLength } from 'class-validator'; +import { ValidationsGroupsEnum } from '../../enums/shared/validation-groups-enum'; +import { AbstractDTO } from '../shared/abstract.dto'; + +export class UnitAccessibilityPriorityType extends AbstractDTO { + @Expose() + @IsString({ groups: [ValidationsGroupsEnum.default] }) + @IsDefined({ groups: [ValidationsGroupsEnum.default] }) + @MaxLength(256, { groups: [ValidationsGroupsEnum.default] }) + name: string; +} diff --git a/backend_new/src/dtos/units/unit-get.dto.ts b/backend_new/src/dtos/units/unit-get.dto.ts new file mode 100644 index 0000000000..3b2488ab55 --- /dev/null +++ b/backend_new/src/dtos/units/unit-get.dto.ts @@ -0,0 +1,98 @@ +import { + IsBoolean, + IsNumber, + IsNumberString, + IsString, + ValidateNested, +} from 'class-validator'; +import { Expose, Type } from 'class-transformer'; +import { ValidationsGroupsEnum } from '../../enums/shared/validation-groups-enum'; +import { AbstractDTO } from '../shared/abstract.dto'; +import { AmiChart } from './ami-chart-get.dto'; +import { UnitType } from './unit-type-get.dto'; +import { UnitRentType } from './unit-rent-type-get.dto'; +import { UnitAccessibilityPriorityType } from './unit-accessibility-priority-type-get.dto'; +import { UnitAmiChartOverride } from './ami-chart-override-get.dto'; + +class Unit extends AbstractDTO { + @Expose() + amiChart?: AmiChart | null; + + @Expose() + @IsString({ groups: [ValidationsGroupsEnum.default] }) + amiPercentage?: string | null; + + @Expose() + @IsNumberString({}, { groups: [ValidationsGroupsEnum.default] }) + annualIncomeMin?: string | null; + + @Expose() + @IsNumberString({}, { groups: [ValidationsGroupsEnum.default] }) + monthlyIncomeMin?: string | null; + + @Expose() + @IsNumber({}, { groups: [ValidationsGroupsEnum.default] }) + floor?: number | null; + + @Expose() + @IsString({ groups: [ValidationsGroupsEnum.default] }) + annualIncomeMax?: string | null; + + @Expose() + @IsNumber({}, { groups: [ValidationsGroupsEnum.default] }) + maxOccupancy?: number | null; + + @Expose() + @IsNumber({}, { groups: [ValidationsGroupsEnum.default] }) + minOccupancy?: number | null; + + @Expose() + @IsNumberString({}, { groups: [ValidationsGroupsEnum.default] }) + monthlyRent?: string | null; + + @Expose() + @IsNumber({}, { groups: [ValidationsGroupsEnum.default] }) + numBathrooms?: number | null; + + @Expose() + @IsNumber({}, { groups: [ValidationsGroupsEnum.default] }) + numBedrooms?: number | null; + + @Expose() + @IsString({ groups: [ValidationsGroupsEnum.default] }) + number?: string | null; + + @Expose() + @IsString({ groups: [ValidationsGroupsEnum.default] }) + sqFeet?: string | null; + + @Expose() + @IsString({ groups: [ValidationsGroupsEnum.default] }) + monthlyRentAsPercentOfIncome?: string | null; + + @Expose() + @IsBoolean({ groups: [ValidationsGroupsEnum.default] }) + bmrProgramChart?: boolean | null; + + @Expose() + @ValidateNested({ groups: [ValidationsGroupsEnum.default] }) + @Type(() => UnitType) + unitTypes?: UnitType | null; + + @Expose() + @ValidateNested({ groups: [ValidationsGroupsEnum.default] }) + @Type(() => UnitRentType) + unitRentTypes?: UnitRentType | null; + + @Expose() + @ValidateNested({ groups: [ValidationsGroupsEnum.default] }) + @Type(() => UnitAccessibilityPriorityType) + unitAccessibilityPriorityTypes?: UnitAccessibilityPriorityType | null; + + @Expose() + @ValidateNested({ groups: [ValidationsGroupsEnum.default] }) + @Type(() => UnitAmiChartOverride) + unitAmiChartOverrides?: UnitAmiChartOverride; +} + +export { Unit as default, Unit }; diff --git a/backend_new/src/dtos/units/unit-rent-type-get.dto.ts b/backend_new/src/dtos/units/unit-rent-type-get.dto.ts new file mode 100644 index 0000000000..20382325a6 --- /dev/null +++ b/backend_new/src/dtos/units/unit-rent-type-get.dto.ts @@ -0,0 +1,12 @@ +import { Expose } from 'class-transformer'; +import { IsDefined, IsString, MaxLength } from 'class-validator'; +import { ValidationsGroupsEnum } from '../../enums/shared/validation-groups-enum'; +import { AbstractDTO } from '../shared/abstract.dto'; + +export class UnitRentType extends AbstractDTO { + @Expose() + @IsString({ groups: [ValidationsGroupsEnum.default] }) + @IsDefined({ groups: [ValidationsGroupsEnum.default] }) + @MaxLength(256, { groups: [ValidationsGroupsEnum.default] }) + name: string; +} diff --git a/backend_new/src/dtos/units/unit-summarized.dto.ts b/backend_new/src/dtos/units/unit-summarized.dto.ts new file mode 100644 index 0000000000..dd5e658d45 --- /dev/null +++ b/backend_new/src/dtos/units/unit-summarized.dto.ts @@ -0,0 +1,48 @@ +import { Expose, Type } from 'class-transformer'; +import { IsString, ValidateNested } from 'class-validator'; +import { ValidationsGroupsEnum } from '../../enums/shared/validation-groups-enum'; +import { UnitSummary } from './unit-summary-get.dto'; +import { UnitSummaryByAMI } from './unit-summary-by-ami-get.dto'; +import { HMI } from './hmi-get.dto'; +import { ApiProperty } from '@nestjs/swagger'; +import { UnitType } from './unit-type-get.dto'; +import { UnitAccessibilityPriorityType } from './unit-accessibility-priority-type-get.dto'; + +export class UnitsSummarized { + @Expose() + @IsString({ groups: [ValidationsGroupsEnum.default], each: true }) + @ApiProperty({ type: [UnitType] }) + unitTypes?: UnitType[]; + + @Expose() + @IsString({ groups: [ValidationsGroupsEnum.default], each: true }) + @ApiProperty({ type: [UnitAccessibilityPriorityType] }) + priorityTypes?: UnitAccessibilityPriorityType[]; + + @Expose() + @IsString({ groups: [ValidationsGroupsEnum.default], each: true }) + @ApiProperty() + amiPercentages?: string[]; + + @Expose() + @ValidateNested({ groups: [ValidationsGroupsEnum.default], each: true }) + @Type(() => UnitSummary) + @ApiProperty({ type: [UnitSummary] }) + byUnitTypeAndRent?: UnitSummary[]; + + @Expose() + @ValidateNested({ groups: [ValidationsGroupsEnum.default], each: true }) + @Type(() => UnitSummary) + @ApiProperty({ type: [UnitSummary] }) + byUnitType?: UnitSummary[]; + + @Expose() + @ValidateNested({ groups: [ValidationsGroupsEnum.default], each: true }) + @Type(() => UnitSummaryByAMI) + @ApiProperty({ type: [UnitSummaryByAMI] }) + byAMI?: UnitSummaryByAMI[]; + + @Expose() + @ApiProperty({ type: HMI }) + hmi?: HMI; +} diff --git a/backend_new/src/dtos/units/unit-summary-by-ami-get.dto.ts b/backend_new/src/dtos/units/unit-summary-by-ami-get.dto.ts new file mode 100644 index 0000000000..28c24cbcca --- /dev/null +++ b/backend_new/src/dtos/units/unit-summary-by-ami-get.dto.ts @@ -0,0 +1,20 @@ +import { Expose, Type } from 'class-transformer'; +import { IsDefined, IsString, ValidateNested } from 'class-validator'; +import { ValidationsGroupsEnum } from '../../enums/shared/validation-groups-enum'; +import { UnitSummary } from './unit-summary-get.dto'; +import { ApiProperty } from '@nestjs/swagger'; + +export class UnitSummaryByAMI { + @Expose() + @IsDefined({ groups: [ValidationsGroupsEnum.default] }) + @IsString({ groups: [ValidationsGroupsEnum.default] }) + @ApiProperty() + percent: string; + + @Expose() + @IsDefined({ groups: [ValidationsGroupsEnum.default] }) + @ValidateNested({ groups: [ValidationsGroupsEnum.default], each: true }) + @Type(() => UnitSummary) + @ApiProperty({ type: [UnitSummary] }) + byUnitType: UnitSummary[]; +} diff --git a/backend_new/src/dtos/units/unit-summary-get.dto.ts b/backend_new/src/dtos/units/unit-summary-get.dto.ts new file mode 100644 index 0000000000..88fc88d094 --- /dev/null +++ b/backend_new/src/dtos/units/unit-summary-get.dto.ts @@ -0,0 +1,61 @@ +import { Expose, Type } from 'class-transformer'; +import { IsDefined, IsString, ValidateNested } from 'class-validator'; +import { ValidationsGroupsEnum } from '../../enums/shared/validation-groups-enum'; +import { MinMaxCurrency } from '../shared/min-max-currency.dto'; +import { MinMax } from '../shared/min-max.dto'; +import { ApiProperty } from '@nestjs/swagger'; +import { UnitType } from './unit-type-get.dto'; + +export class UnitSummary { + @Expose() + @IsString({ groups: [ValidationsGroupsEnum.default] }) + @ApiProperty() + unitTypes?: UnitType | null; + + @Expose() + @IsDefined({ groups: [ValidationsGroupsEnum.default] }) + @ValidateNested({ groups: [ValidationsGroupsEnum.default] }) + @Type(() => MinMaxCurrency) + @ApiProperty() + minIncomeRange: MinMaxCurrency; + + @Expose() + @IsDefined({ groups: [ValidationsGroupsEnum.default] }) + @ValidateNested({ groups: [ValidationsGroupsEnum.default] }) + @Type(() => MinMax) + @ApiProperty() + occupancyRange: MinMax; + + @Expose() + @IsDefined({ groups: [ValidationsGroupsEnum.default] }) + @ValidateNested({ groups: [ValidationsGroupsEnum.default] }) + @Type(() => MinMax) + @ApiProperty() + rentAsPercentIncomeRange: MinMax; + + @Expose() + @IsDefined({ groups: [ValidationsGroupsEnum.default] }) + @ValidateNested({ groups: [ValidationsGroupsEnum.default] }) + @Type(() => MinMaxCurrency) + @ApiProperty() + rentRange: MinMaxCurrency; + + @Expose() + @IsDefined({ groups: [ValidationsGroupsEnum.default] }) + @IsString({ groups: [ValidationsGroupsEnum.default] }) + @ApiProperty() + totalAvailable: number; + + @Expose() + @IsDefined({ groups: [ValidationsGroupsEnum.default] }) + @ValidateNested({ groups: [ValidationsGroupsEnum.default] }) + @Type(() => MinMax) + @ApiProperty() + areaRange: MinMax; + + @Expose() + @ValidateNested({ groups: [ValidationsGroupsEnum.default] }) + @Type(() => MinMax) + @ApiProperty({ type: MinMax, required: false }) + floorRange?: MinMax; +} diff --git a/backend_new/src/dtos/units/unit-type-get.dto.ts b/backend_new/src/dtos/units/unit-type-get.dto.ts new file mode 100644 index 0000000000..789e202f14 --- /dev/null +++ b/backend_new/src/dtos/units/unit-type-get.dto.ts @@ -0,0 +1,17 @@ +import { Expose } from 'class-transformer'; +import { IsDefined, IsNumber, IsString, MaxLength } from 'class-validator'; +import { ValidationsGroupsEnum } from '../../enums/shared/validation-groups-enum'; +import { AbstractDTO } from '../shared/abstract.dto'; + +export class UnitType extends AbstractDTO { + @Expose() + @IsString({ groups: [ValidationsGroupsEnum.default] }) + @IsDefined({ groups: [ValidationsGroupsEnum.default] }) + @MaxLength(256, { groups: [ValidationsGroupsEnum.default] }) + name: string; + + @Expose() + @IsNumber({}, { groups: [ValidationsGroupsEnum.default] }) + @IsDefined({ groups: [ValidationsGroupsEnum.default] }) + numBedrooms: number; +} diff --git a/backend_new/src/dtos/units/units-summery-get.dto.ts b/backend_new/src/dtos/units/units-summery-get.dto.ts new file mode 100644 index 0000000000..4b9b87005a --- /dev/null +++ b/backend_new/src/dtos/units/units-summery-get.dto.ts @@ -0,0 +1,89 @@ +import { + IsNumber, + IsNumberString, + IsDefined, + IsString, + IsUUID, + ValidateNested, +} from 'class-validator'; +import { Expose, Type } from 'class-transformer'; +import { ValidationsGroupsEnum } from '../../enums/shared/validation-groups-enum'; +import { UnitType } from './unit-type-get.dto'; +import { UnitAccessibilityPriorityType } from './unit-accessibility-priority-type-get.dto'; + +class UnitsSummary { + @Expose() + @IsUUID(4, { groups: [ValidationsGroupsEnum.default] }) + @IsString({ groups: [ValidationsGroupsEnum.default] }) + @IsDefined({ groups: [ValidationsGroupsEnum.default] }) + id: string; + + @Expose() + @ValidateNested({ groups: [ValidationsGroupsEnum.default] }) + @Type(() => UnitType) + @IsDefined({ groups: [ValidationsGroupsEnum.default] }) + unitTypes: UnitType; + + @Expose() + @IsNumberString({}, { groups: [ValidationsGroupsEnum.default] }) + monthlyRentMin?: number | null; + + @Expose() + @IsNumberString({}, { groups: [ValidationsGroupsEnum.default] }) + monthlyRentMax?: number | null; + + @Expose() + @IsString({ groups: [ValidationsGroupsEnum.default] }) + monthlyRentAsPercentOfIncome?: string | null; + + @Expose() + @IsNumber({}, { groups: [ValidationsGroupsEnum.default] }) + amiPercentage?: number | null; + + @Expose() + @IsNumberString({}, { groups: [ValidationsGroupsEnum.default] }) + minimumIncomeMin?: string | null; + + @Expose() + @IsNumberString({}, { groups: [ValidationsGroupsEnum.default] }) + minimumIncomeMax?: string | null; + + @Expose() + @IsNumber({}, { groups: [ValidationsGroupsEnum.default] }) + maxOccupancy?: number | null; + + @Expose() + @IsNumber({}, { groups: [ValidationsGroupsEnum.default] }) + minOccupancy?: number | null; + + @Expose() + @IsNumber({}, { groups: [ValidationsGroupsEnum.default] }) + floorMin?: number | null; + + @Expose() + @IsNumber({}, { groups: [ValidationsGroupsEnum.default] }) + floorMax?: number | null; + + @Expose() + @IsString({ groups: [ValidationsGroupsEnum.default] }) + sqFeetMin?: string | null; + + @Expose() + @IsString({ groups: [ValidationsGroupsEnum.default] }) + sqFeetMax?: string | null; + + @Expose() + @ValidateNested({ groups: [ValidationsGroupsEnum.default] }) + @Type(() => UnitAccessibilityPriorityType) + unitAccessibilityPriorityTypes?: UnitAccessibilityPriorityType | null; + + @Expose() + @IsNumber({}, { groups: [ValidationsGroupsEnum.default] }) + totalCount?: number | null; + + @Expose() + @IsNumber({}, { groups: [ValidationsGroupsEnum.default] }) + totalAvailable?: number | null; +} + +export { UnitsSummary as default, UnitsSummary }; diff --git a/backend_new/src/enums/listings/filter-key-enum.ts b/backend_new/src/enums/listings/filter-key-enum.ts new file mode 100644 index 0000000000..5ff52713ed --- /dev/null +++ b/backend_new/src/enums/listings/filter-key-enum.ts @@ -0,0 +1,9 @@ +export enum ListingFilterKeys { + status = 'status', + name = 'name', + neighborhood = 'neighborhood', + bedrooms = 'bedrooms', + zipcode = 'zipcode', + leasingAgents = 'leasingAgents', + jurisdiction = 'jurisdiction', +} diff --git a/backend_new/src/enums/listings/order-by-enum.ts b/backend_new/src/enums/listings/order-by-enum.ts new file mode 100644 index 0000000000..a2e668e2f4 --- /dev/null +++ b/backend_new/src/enums/listings/order-by-enum.ts @@ -0,0 +1,11 @@ +export enum ListingOrderByKeys { + mostRecentlyUpdated = 'mostRecentlyUpdated', + applicationDates = 'applicationDates', + mostRecentlyClosed = 'mostRecentlyClosed', + mostRecentlyPublished = 'mostRecentlyPublished', + name = 'name', + waitlistOpen = 'waitlistOpen', + status = 'status', + unitsAvailable = 'unitsAvailable', + marketingType = 'marketingType', +} diff --git a/backend_new/src/enums/listings/view-enum.ts b/backend_new/src/enums/listings/view-enum.ts new file mode 100644 index 0000000000..583265c06d --- /dev/null +++ b/backend_new/src/enums/listings/view-enum.ts @@ -0,0 +1,6 @@ +export enum ListingViews { + fundamentals = 'fundamentals', + base = 'base', + full = 'full', + details = 'details', +} diff --git a/backend_new/src/enums/shared/order-by-enum.ts b/backend_new/src/enums/shared/order-by-enum.ts new file mode 100644 index 0000000000..83b1952206 --- /dev/null +++ b/backend_new/src/enums/shared/order-by-enum.ts @@ -0,0 +1,4 @@ +export enum OrderByEnum { + ASC = 'asc', + DESC = 'desc', +} diff --git a/backend_new/src/enums/shared/validation-groups-enum.ts b/backend_new/src/enums/shared/validation-groups-enum.ts new file mode 100644 index 0000000000..6c9c0a4560 --- /dev/null +++ b/backend_new/src/enums/shared/validation-groups-enum.ts @@ -0,0 +1,5 @@ +export enum ValidationsGroupsEnum { + default = 'default', + partners = 'partners', + applicants = 'applicants', +} diff --git a/backend_new/src/enums/user_accounts/filter-key-enum.ts b/backend_new/src/enums/user_accounts/filter-key-enum.ts new file mode 100644 index 0000000000..dfef2f5e6f --- /dev/null +++ b/backend_new/src/enums/user_accounts/filter-key-enum.ts @@ -0,0 +1,4 @@ +export enum UserFilterKeys { + isPartner = 'isPartner', + isPortalUser = 'isPortalUser', +} diff --git a/backend_new/src/main.ts b/backend_new/src/main.ts index 13cad38cff..cfd5f2c0a2 100644 --- a/backend_new/src/main.ts +++ b/backend_new/src/main.ts @@ -1,8 +1,17 @@ import { NestFactory } from '@nestjs/core'; +import { SwaggerModule, DocumentBuilder } from '@nestjs/swagger'; import { AppModule } from './app.module'; async function bootstrap() { const app = await NestFactory.create(AppModule); - await app.listen(3000); + const config = new DocumentBuilder() + .setTitle('Bloom API') + .setDescription('The API for Bloom') + .setVersion('2.0') + .addTag('listings') + .build(); + const document = SwaggerModule.createDocument(app, config); + SwaggerModule.setup('api', app, document); + await app.listen(process.env.PORT); } bootstrap(); diff --git a/backend_new/src/modules/listing.module.ts b/backend_new/src/modules/listing.module.ts new file mode 100644 index 0000000000..a03747f94f --- /dev/null +++ b/backend_new/src/modules/listing.module.ts @@ -0,0 +1,12 @@ +import { Module } from '@nestjs/common'; +import { ListingController } from '../controllers/listing.controller'; +import { ListingService } from '../services/listing.service'; +import { PrismaService } from '../services/prisma.service'; + +@Module({ + imports: [], + controllers: [ListingController], + providers: [ListingService, PrismaService], + exports: [ListingService], +}) +export class ListingModule {} diff --git a/backend_new/src/services/listing.service.ts b/backend_new/src/services/listing.service.ts new file mode 100644 index 0000000000..6cf9ad5436 --- /dev/null +++ b/backend_new/src/services/listing.service.ts @@ -0,0 +1,346 @@ +import { Injectable, NotFoundException } from '@nestjs/common'; +import { PrismaService } from './prisma.service'; +import { LanguagesEnum, Prisma } from '@prisma/client'; +import { ListingsQueryParams } from '../dtos/listings/listings-query-params.dto'; +import { + calculateSkip, + calculateTake, + shouldPaginate, +} from '../utilities/pagination-helpers'; +import { buildOrderBy } from '../utilities/build-order-by'; +import { ListingFilterParams } from '../dtos/listings/listings-filter-params.dto'; +import { ListingFilterKeys } from '../enums/listings/filter-key-enum'; +import { buildFilter } from '../utilities/build-filter'; +import { ListingGet } from '../dtos/listings/listing-get.dto'; +import { mapTo } from '../utilities/mapTo'; +import { + summarizeUnitsByTypeAndRent, + summarizeUnits, +} from '../utilities/unit-utilities'; +import { AmiChart } from '../dtos/units/ami-chart-get.dto'; +import { ListingViews } from '../enums/listings/view-enum'; + +export type getListingsArgs = { + skip: number; + take: number; + orderBy: any; + where: Prisma.ListingsWhereInput; +}; + +const views: Partial> = { + fundamentals: { + jurisdictions: true, + listingsBuildingAddress: true, + reservedCommunityTypes: true, + listingImages: { + include: { + assets: true, + }, + }, + listingMultiselectQuestions: { + include: { + multiselectQuestions: true, + }, + }, + listingFeatures: true, + listingUtilities: true, + }, +}; + +views.base = { + ...views.fundamentals, + units: { + include: { + unitTypes: true, + unitAmiChartOverrides: true, + amiChart: { + include: { + amiChartItem: true, + }, + }, + }, + }, +}; + +views.full = { + ...views.fundamentals, + applicationMethods: { + include: { + paperApplications: { + include: { + assets: true, + }, + }, + }, + }, + listingsBuildingSelectionCriteriaFile: true, + listingEvents: { + include: { + assets: true, + }, + }, + listingsResult: true, + listingsLeasingAgentAddress: true, + listingsApplicationPickUpAddress: true, + listingsApplicationDropOffAddress: true, + units: { + include: { + unitAmiChartOverrides: true, + unitTypes: true, + unitRentTypes: true, + unitAccessibilityPriorityTypes: true, + amiChart: { + include: { + jurisdictions: true, + amiChartItem: true, + unitGroupAmiLevels: true, + }, + }, + }, + }, +}; + +views.details = { + ...views.base, + ...views.full, +}; + +/* + this is the service for listings + it handles all the backend's business logic for reading in listing(s) +*/ +@Injectable() +export class ListingService { + constructor(private prisma: PrismaService) {} + + /* + this will get a set of listings given the params passed in + this set can either be paginated or not depending on the params + it will return both the set of listings, and some meta information to help with pagination + */ + async list(params: ListingsQueryParams): Promise<{ + items: ListingGet[]; + meta: { + currentPage: number; + itemCount: number; + itemsPerPage: number; + totalItems: number; + totalPages: number; + }; + }> { + const whereClause = this.buildWhereClause(params.filter, params.search); + const isPaginated = shouldPaginate(params.limit, params.page); + + const count = await this.prisma.listings.count({ + where: whereClause, + }); + + const listingsRaw = await this.prisma.listings.findMany({ + skip: calculateSkip(params.limit, params.page), + take: calculateTake(params.limit), + orderBy: buildOrderBy(params.orderBy, params.orderDir), + include: views[params.view ?? 'full'], + where: whereClause, + }); + + const listings = mapTo(ListingGet, listingsRaw); + + listings.forEach((listing) => { + if (Array.isArray(listing.units) && listing.units.length > 0) { + listing.unitsSummarized = { + byUnitTypeAndRent: summarizeUnitsByTypeAndRent( + listing.units, + listing, + ), + }; + } + }); + + const itemsPerPage = + isPaginated && params.limit !== 'all' ? params.limit : listings.length; + const totalItems = isPaginated ? count : listings.length; + + const paginationInfo = { + currentPage: isPaginated ? params.page : 1, + itemCount: listings.length, + itemsPerPage: itemsPerPage, + totalItems: totalItems, + totalPages: Math.ceil( + totalItems / (itemsPerPage ? itemsPerPage : totalItems), + ), + }; + + return { + items: listings, + meta: paginationInfo, + }; + } + + /* + this helps build the where clause for the list() + */ + buildWhereClause( + params?: ListingFilterParams[], + search?: string, + ): Prisma.ListingsWhereInput { + const filters: Prisma.ListingsWhereInput[] = []; + + if (params?.length) { + params.forEach((filter) => { + if (ListingFilterKeys.name in filter) { + const builtFilter = buildFilter({ + $comparison: filter.$comparison, + $include_nulls: false, + value: filter[ListingFilterKeys.name], + key: ListingFilterKeys.name, + }); + filters.push({ + OR: builtFilter.map((filt) => ({ [ListingFilterKeys.name]: filt })), + }); + } else if (ListingFilterKeys.status in filter) { + const builtFilter = buildFilter({ + $comparison: filter.$comparison, + $include_nulls: false, + value: filter[ListingFilterKeys.status], + key: ListingFilterKeys.status, + }); + filters.push({ + OR: builtFilter.map((filt) => ({ + [ListingFilterKeys.status]: filt, + })), + }); + } else if (ListingFilterKeys.neighborhood in filter) { + const builtFilter = buildFilter({ + $comparison: filter.$comparison, + $include_nulls: false, + value: filter[ListingFilterKeys.neighborhood], + key: ListingFilterKeys.neighborhood, + }); + filters.push({ + OR: builtFilter.map((filt) => ({ + [ListingFilterKeys.neighborhood]: filt, + })), + }); + } else if (ListingFilterKeys.bedrooms in filter) { + const builtFilter = buildFilter({ + $comparison: filter.$comparison, + $include_nulls: false, + value: filter[ListingFilterKeys.bedrooms], + key: ListingFilterKeys.bedrooms, + }); + filters.push({ + OR: builtFilter.map((filt) => ({ + units: { + some: { + numBedrooms: filt, + }, + }, + })), + }); + } else if (ListingFilterKeys.zipcode in filter) { + const builtFilter = buildFilter({ + $comparison: filter.$comparison, + $include_nulls: false, + value: filter[ListingFilterKeys.zipcode], + key: ListingFilterKeys.zipcode, + }); + filters.push({ + OR: builtFilter.map((filt) => ({ + listingsBuildingAddress: { + zipCode: filt, + }, + })), + }); + } else if (ListingFilterKeys.leasingAgents in filter) { + const builtFilter = buildFilter({ + $comparison: filter.$comparison, + $include_nulls: false, + value: filter[ListingFilterKeys.leasingAgents], + key: ListingFilterKeys.leasingAgents, + }); + filters.push({ + OR: builtFilter.map((filt) => ({ + userAccounts: { + some: { + id: filt, + }, + }, + })), + }); + } else if (ListingFilterKeys.jurisdiction in filter) { + const builtFilter = buildFilter({ + $comparison: filter.$comparison, + $include_nulls: false, + value: filter[ListingFilterKeys.jurisdiction], + key: ListingFilterKeys.jurisdiction, + }); + filters.push({ + OR: builtFilter.map((filt) => ({ + jurisdictionId: filt, + })), + }); + } + }); + } + + if (search) { + filters.push({ + name: { + contains: search, + mode: Prisma.QueryMode.insensitive, + }, + }); + } + + return { + AND: filters, + }; + } + + /* + this will return 1 listing or error + the scope of data it returns is dependent on the view arg passed in + */ + async findOne( + listingId: string, + lang: LanguagesEnum = LanguagesEnum.en, + view: ListingViews = ListingViews.full, + ): Promise { + const listingRaw = await this.prisma.listings.findFirst({ + include: views[view], + where: { + id: { + equals: listingId, + }, + }, + }); + + const result = mapTo(ListingGet, listingRaw); + + if (!result) { + throw new NotFoundException(); + } + + if (lang !== LanguagesEnum.en) { + // TODO: await this.translationService.translateListing(result, lang); + } + + await this.addUnitsSummarized(result); + return result; + } + + addUnitsSummarized = async (listing: ListingGet) => { + if (Array.isArray(listing.units) && listing.units.length > 0) { + const amiChartsRaw = await this.prisma.amiChart.findMany({ + where: { + id: { + in: listing.units.map((unit) => unit.amiChart.id), + }, + }, + }); + const amiCharts = mapTo(AmiChart, amiChartsRaw); + listing.unitsSummarized = summarizeUnits(listing, amiCharts); + } + return listing; + }; +} diff --git a/backend_new/src/services/prisma.service.ts b/backend_new/src/services/prisma.service.ts new file mode 100644 index 0000000000..844f980c44 --- /dev/null +++ b/backend_new/src/services/prisma.service.ts @@ -0,0 +1,18 @@ +import { INestApplication, Injectable, OnModuleInit } from '@nestjs/common'; +import { PrismaClient } from '@prisma/client'; + +/* + This service sets up our database connections +*/ +@Injectable() +export class PrismaService extends PrismaClient implements OnModuleInit { + async onModuleInit() { + await this.$connect(); + } + + async enableShutdownHooks(app: INestApplication) { + this.$on('beforeExit', async () => { + await app.close(); + }); + } +} diff --git a/backend_new/src/utilities/build-filter.ts b/backend_new/src/utilities/build-filter.ts new file mode 100644 index 0000000000..775bbb3330 --- /dev/null +++ b/backend_new/src/utilities/build-filter.ts @@ -0,0 +1,80 @@ +import { HttpException, HttpStatus } from '@nestjs/common'; +import { Prisma, UserAccounts, UserRoles } from '@prisma/client'; +import { Compare } from '../dtos/shared/base-filter.dto'; +import { UserFilterKeys } from '../enums/user_accounts/filter-key-enum'; + +type filter = { + $comparison: Compare; + $include_nulls: boolean; + value: any; + key: string; +}; + +/* + This constructs the "where" part of a prisma query + Because the where clause is specific to each model we are working with this has to be very generic. + It only constructs the actual body of the where statement, how that clause is used must be managed by the service calling this helper function +*/ +export function buildFilter( + filter: filter, + user?: UserAccounts & { roles: UserRoles }, +): any { + const toReturn = []; + const comparison = filter['$comparison']; + const includeNulls = filter['$include_nulls']; + const filterValue = filter.value; + + if (filter.key === UserFilterKeys.isPortalUser) { + // TODO: addIsPortalUserQuery(filter.value, user); + } + + if (comparison === Compare.IN) { + toReturn.push({ + in: String(filterValue) + .split(',') + .map((s) => s.trim().toLowerCase()) + .filter((s) => s.length !== 0), + mode: Prisma.QueryMode.insensitive, + }); + } else if (comparison === Compare['<>']) { + toReturn.push({ + not: { + equals: filterValue, + }, + mode: Prisma.QueryMode.insensitive, + }); + } else if (comparison === Compare['=']) { + toReturn.push({ + equals: filterValue, + mode: Prisma.QueryMode.insensitive, + }); + } else if (comparison === Compare['>=']) { + toReturn.push({ + gte: filterValue, + mode: Prisma.QueryMode.insensitive, + }); + } else if (comparison === Compare['<=']) { + toReturn.push({ + lte: filterValue, + mode: Prisma.QueryMode.insensitive, + }); + } else if (Compare.NA) { + throw new HttpException( + `Filter "${filter.key}" expected to be handled by a custom filter handler, but one was not implemented.`, + HttpStatus.NOT_IMPLEMENTED, + ); + } else { + throw new HttpException( + 'Comparison Not Implemented', + HttpStatus.NOT_IMPLEMENTED, + ); + } + + if (includeNulls) { + toReturn.push({ + equals: null, + }); + } + + return toReturn; +} diff --git a/backend_new/src/utilities/build-order-by.ts b/backend_new/src/utilities/build-order-by.ts new file mode 100644 index 0000000000..ab39102539 --- /dev/null +++ b/backend_new/src/utilities/build-order-by.ts @@ -0,0 +1,14 @@ +import { OrderByEnum } from '../enums/shared/order-by-enum'; + +/* + This constructs the "orderBy" part of a prisma query + We are guaranteed to have the same length for both the orderBy and orderDir arrays +*/ +export const buildOrderBy = (orderBy?: string[], orderDir?: OrderByEnum[]) => { + if (!orderBy?.length) { + return undefined; + } + return orderBy.map((param, index) => ({ + [param]: orderDir[index], + })); +}; diff --git a/backend_new/src/utilities/default-validation-pipe-options.ts b/backend_new/src/utilities/default-validation-pipe-options.ts new file mode 100644 index 0000000000..acb0f6ecb1 --- /dev/null +++ b/backend_new/src/utilities/default-validation-pipe-options.ts @@ -0,0 +1,16 @@ +import { ValidationPipeOptions } from '@nestjs/common'; +import { ValidationsGroupsEnum } from '../enums/shared/validation-groups-enum'; + +/* + This controls the validation pipe that is inherent to NestJs +*/ +export const defaultValidationPipeOptions: ValidationPipeOptions = { + transform: true, + transformOptions: { + excludeExtraneousValues: true, + enableImplicitConversion: false, + }, + groups: [ValidationsGroupsEnum.default], + forbidUnknownValues: true, + skipMissingProperties: true, +}; diff --git a/backend_new/src/utilities/mapTo.ts b/backend_new/src/utilities/mapTo.ts new file mode 100644 index 0000000000..ef4bddf73d --- /dev/null +++ b/backend_new/src/utilities/mapTo.ts @@ -0,0 +1,29 @@ +import { ClassTransformOptions, plainToClass } from 'class-transformer'; +import { ClassType } from 'class-transformer/ClassTransformer'; + +export function mapTo( + cls: ClassType, + plain: V[], + options?: ClassTransformOptions, +): T[]; +export function mapTo( + cls: ClassType, + plain: V, + options?: ClassTransformOptions, +): T; + +/* + This maps a plain object to the class provided + This is mostly used by controllers to map the result of a service to the type returned by the endpoint +*/ +export function mapTo( + cls: ClassType, + plain, + options?: ClassTransformOptions, +) { + return plainToClass(cls, plain, { + ...options, + excludeExtraneousValues: true, + enableImplicitConversion: true, + }); +} diff --git a/backend_new/src/utilities/order-by-validator.ts b/backend_new/src/utilities/order-by-validator.ts new file mode 100644 index 0000000000..014a7d94bf --- /dev/null +++ b/backend_new/src/utilities/order-by-validator.ts @@ -0,0 +1,30 @@ +import { + ValidatorConstraint, + ValidatorConstraintInterface, + ValidationArguments, +} from 'class-validator'; + +/* + This is a custom validator to make sure the orderBy and orderDir arrays have the same length +*/ +@ValidatorConstraint({ name: 'orderDir', async: false }) +export class OrderQueryParamValidator implements ValidatorConstraintInterface { + validate(order: Array | undefined, args: ValidationArguments) { + if (args.property === 'orderDir') { + return Array.isArray(order) + ? (args.object as { orderBy: Array }).orderBy?.length === + order.length + : false; + } else if (args.property === 'orderBy') { + return Array.isArray(order) + ? (args.object as { orderDir: Array }).orderDir?.length === + order.length + : false; + } + return false; + } + + defaultMessage() { + return 'order array length must be equal to orderBy array length'; + } +} diff --git a/backend_new/src/utilities/pagination-helpers.ts b/backend_new/src/utilities/pagination-helpers.ts new file mode 100644 index 0000000000..daca3ff747 --- /dev/null +++ b/backend_new/src/utilities/pagination-helpers.ts @@ -0,0 +1,28 @@ +/* + takes in the params for limit and page + responds true if we should account for pagination + responds false if we don't need to take pagination into account +*/ +export const shouldPaginate = (limit: number | 'all', page: number) => { + return limit !== 'all' && limit > 0 && page > 0; +}; + +/* + takes in the params for limit and page + responds with how many records we should skip over (if we are on page 2 we need to skip over page 1's records) +*/ +export const calculateSkip = (limit?: number | 'all', page?: number) => { + if (shouldPaginate(limit, page)) { + return (page - 1) * (limit as number); + } + return 0; +}; + +/* + takes in the params for limit and page + responds with the # of records per page + e.g. if limit is 10 that means each page should only contain 10 records +*/ +export const calculateTake = (limit?: number | 'all') => { + return limit !== 'all' ? limit : undefined; +}; diff --git a/backend_new/src/utilities/unit-utilities.ts b/backend_new/src/utilities/unit-utilities.ts new file mode 100644 index 0000000000..9a349663b2 --- /dev/null +++ b/backend_new/src/utilities/unit-utilities.ts @@ -0,0 +1,530 @@ +import { ReviewOrderTypeEnum } from '@prisma/client'; +import { UnitSummary } from '../dtos/units/unit-summary-get.dto'; +import Unit from '../dtos/units/unit-get.dto'; +import { AmiChart } from '../dtos/units/ami-chart-get.dto'; +import listingGetDto, { ListingGet } from '../dtos/listings/listing-get.dto'; +import { MinMaxCurrency } from '../dtos/shared/min-max-currency.dto'; +import { MinMax } from '../dtos/shared/min-max.dto'; +import { UnitsSummarized } from '../dtos/units/unit-summarized.dto'; +import { UnitType } from '../dtos/units/unit-type-get.dto'; +import { UnitAccessibilityPriorityType } from '../dtos/units/unit-accessibility-priority-type-get.dto'; +import { AmiChartItem } from '../dtos/units/ami-chart-item-get.dto'; +import { UnitAmiChartOverride } from '../dtos/units/ami-chart-override-get.dto'; + +type AnyDict = { [key: string]: unknown }; +type UnitMap = { + [key: string]: Unit[]; +}; + +export const UnitTypeSort = [ + 'SRO', + 'studio', + 'oneBdrm', + 'twoBdrm', + 'threeBdrm', +]; + +const usd = new Intl.NumberFormat('en-US', { + style: 'currency', + currency: 'USD', + minimumFractionDigits: 0, + maximumFractionDigits: 0, +}); + +export const minMax = (baseValue: MinMax, newValue: number): MinMax => { + return { + min: Math.min(baseValue.min, newValue), + max: Math.max(baseValue.max, newValue), + }; +}; + +export const minMaxCurrency = ( + baseValue: MinMaxCurrency, + newValue: number, +): MinMaxCurrency => { + return { + min: usd.format( + Math.min(parseFloat(baseValue.min.replace(/[^0-9.-]+/g, '')), newValue), + ), + max: usd.format( + Math.max(parseFloat(baseValue.max.replace(/[^0-9.-]+/g, '')), newValue), + ), + }; +}; + +export const yearlyCurrencyStringToMonthly = (currency: string) => { + return usd.format(parseFloat(currency.replace(/[^0-9.-]+/g, '')) / 12); +}; + +export const getAmiChartItemUniqueKey = (amiChartItem: AmiChartItem) => { + return ( + amiChartItem.householdSize.toString() + + '-' + + amiChartItem.percentOfAmi.toString() + ); +}; + +export const mergeAmiChartWithOverrides = ( + amiChart: AmiChart, + override: UnitAmiChartOverride, +) => { + const householdAmiPercentageOverrideMap: Map = + override.items.reduce((acc, amiChartItem) => { + acc.set(getAmiChartItemUniqueKey(amiChartItem), amiChartItem); + return acc; + }, new Map()); + + for (const amiChartItem of amiChart.items) { + const amiChartItemOverride = householdAmiPercentageOverrideMap.get( + getAmiChartItemUniqueKey(amiChartItem), + ); + if (amiChartItemOverride) { + amiChartItem.income = amiChartItemOverride.income; + } + } + return amiChart; +}; + +// Creates data used to display a table of household size/unit size by maximum income per the AMI charts on the units +// Unit sets can have multiple AMI charts used, in which case the table displays ranges +export const generateHmiData = ( + units: Unit[], + minMaxHouseholdSize: MinMax[], + amiCharts: AmiChart[], +) => { + if (!units || units.length === 0) { + return null; + } + // Currently, BMR chart is just toggling whether or not the first column shows Household Size or Unit Type + const showUnitType = units[0].bmrProgramChart; + + type ChartAndPercentage = { + percentage: number; + chart: AmiChart; + }; + + const maxAMIChartHouseholdSize = amiCharts.reduce((maxSize, amiChart) => { + const amiChartMax = amiChart.items.reduce((max, item) => { + return Math.max(max, item.householdSize); + }, 0); + return Math.max(maxSize, amiChartMax); + }, 0); + + // All unique AMI percentages across all units + const allPercentages: number[] = [ + ...new Set( + units + .filter((item) => item != null) + .map((unit) => parseInt(unit.amiPercentage, 10)), + ), + ].sort(function (a, b) { + return a - b; + }); + + const amiChartMap: Record = amiCharts.reduce( + (acc, amiChart) => { + acc[amiChart.id] = amiChart; + return acc; + }, + {}, + ); + + // All unique combinations of an AMI percentage and an AMI chart across all units + const uniquePercentageChartSet: ChartAndPercentage[] = [ + ...new Set( + units + .filter((unit) => amiChartMap[unit.amiChart.id]) + .map((unit) => { + let amiChart = amiChartMap[unit.amiChart.id]; + if (unit.unitAmiChartOverrides) { + amiChart = mergeAmiChartWithOverrides( + amiChart, + unit.unitAmiChartOverrides, + ); + } + return JSON.stringify({ + percentage: parseInt(unit.amiPercentage, 10), + chart: amiChart, + }); + }), + ), + ].map((uniqueSetString) => JSON.parse(uniqueSetString)); + + const hmiHeaders = { + sizeColumn: showUnitType ? 't.unitType' : 'listings.householdSize', + } as AnyDict; + + let bmrHeaders = [ + 'listings.unitTypes.SRO', + 'listings.unitTypes.studio', + 'listings.unitTypes.oneBdrm', + 'listings.unitTypes.twoBdrm', + 'listings.unitTypes.threeBdrm', + 'listings.unitTypes.fourBdrm', + ]; + // this is to map currentHouseholdSize to a units max occupancy + const unitOccupancy = []; + + let validHouseholdSizes = minMaxHouseholdSize.reduce((validSizes, minMax) => { + // Get all numbers between min and max + // If min is more than the largest chart value, make sure we show the largest value + const unitHouseholdSizes = [ + ...Array(Math.min(minMax.max, maxAMIChartHouseholdSize) + 1).keys(), + ].filter( + (value) => value >= Math.min(minMax.min, maxAMIChartHouseholdSize), + ); + return [...new Set([...validSizes, ...unitHouseholdSizes])].sort((a, b) => + a < b ? -1 : 1, + ); + }, []); + + if (showUnitType) { + // the unit types used by the listing + const selectedUnitTypes = units.reduce((obj, unit) => { + if (unit.unitTypes) { + obj[unit.unitTypes.name] = { + rooms: unit.unitTypes.numBedrooms, + maxOccupancy: unit.maxOccupancy, + }; + } + return obj; + }, {}); + const sortedUnitTypeNames = Object.keys(selectedUnitTypes).sort((a, b) => + selectedUnitTypes[a].rooms < selectedUnitTypes[b].rooms + ? -1 + : selectedUnitTypes[a].rooms > selectedUnitTypes[b].rooms + ? 1 + : 0, + ); + // setbmrHeaders based on the actual units + bmrHeaders = sortedUnitTypeNames.map( + (type) => `listings.unitTypes.${type}`, + ); + + // set unitOccupancy based off of a units max occupancy + sortedUnitTypeNames.forEach((name) => { + unitOccupancy.push(selectedUnitTypes[name].maxOccupancy); + }); + + // if showUnitType, we want to set the bedroom sizes to the valid household sizes + validHouseholdSizes = [ + ...new Set(units.map((unit) => unit.unitTypes?.numBedrooms || 0)), + ]; + } + + // 1. If there are multiple AMI levels, show each AMI level (max income per + // year only) for each size (number of cols = the size col + # ami levels) + // 2. If there is only one AMI level, show max income per month and per + // year for each size (number of cols = the size col + 2 for each income style) + if (allPercentages.length > 1) { + allPercentages.forEach((percent) => { + // Pass translation with its respective argument with format `key*argumentName:argumentValue` + hmiHeaders[ + `ami${percent}` + ] = `listings.percentAMIUnit*percent:${percent}`; + }); + } else { + hmiHeaders['maxIncomeMonth'] = 'listings.maxIncomeMonth'; + hmiHeaders['maxIncomeYear'] = 'listings.maxIncomeYear'; + } + + const findAmiValueInChart = ( + amiChart: AmiChartItem[], + householdSize: number, + percentOfAmi: number, + ) => { + return amiChart.find((item) => { + return ( + item.householdSize === householdSize && + item.percentOfAmi === percentOfAmi + ); + })?.income; + }; + + // Build row data by household size + const hmiRows = validHouseholdSizes.reduce( + (hmiRowsData, householdSize: number) => { + const currentHouseholdSize = showUnitType + ? unitOccupancy[householdSize - 1] + : householdSize; + + const rowData = { + sizeColumn: showUnitType + ? bmrHeaders[householdSize - 1] + : currentHouseholdSize, + }; + + let rowHasData = false; // Row is valid if at least one column is filled, otherwise don't push the row + allPercentages.forEach((currentAmiPercent) => { + // Get all the charts that we're using with this percentage and size + const uniquePercentCharts = uniquePercentageChartSet.filter( + (uniqueChartAndPercentage) => { + return uniqueChartAndPercentage.percentage === currentAmiPercent; + }, + ); + // If we don't have data for this AMI percentage and household size, this cell is empty + if (uniquePercentCharts.length === 0) { + if (allPercentages.length === 1) { + rowData['maxIncomeMonth'] = ''; + rowData['maxIncomeYear'] = ''; + } else { + rowData[`ami${currentAmiPercent}`] = ''; + } + } else { + if (!uniquePercentCharts[0].chart) { + return hmiRowsData; + } + // If we have chart data, create a max income range string + const firstChartValue = findAmiValueInChart( + uniquePercentCharts[0].chart.items, + currentHouseholdSize, + currentAmiPercent, + ); + if (!firstChartValue) { + return hmiRowsData; + } + const maxIncomeRange = uniquePercentCharts.reduce( + (incomeRange, uniqueSet) => { + return minMaxCurrency( + incomeRange, + findAmiValueInChart( + uniqueSet.chart.items, + currentHouseholdSize, + currentAmiPercent, + ), + ); + }, + { + min: usd.format(firstChartValue), + max: usd.format(firstChartValue), + } as MinMaxCurrency, + ); + if (allPercentages.length === 1) { + rowData[ + 'maxIncomeMonth' + ] = `listings.monthlyIncome*income:${yearlyCurrencyStringToMonthly( + maxIncomeRange.max, + )}`; + rowData[ + 'maxIncomeYear' + ] = `listings.annualIncome*income:${maxIncomeRange.max}`; + } else { + rowData[ + `ami${currentAmiPercent}` + ] = `listings.annualIncome*income:${maxIncomeRange.max}`; + } + rowHasData = true; + } + }); + if (rowHasData) { + hmiRowsData.push(rowData); + } + return hmiRowsData; + }, + [], + ); + + return { columns: hmiHeaders, rows: hmiRows }; +}; + +export const getCurrencyString = (initialValue: string) => { + const roundedValue = getRoundedNumber(initialValue); + if (Number.isNaN(roundedValue)) return 't.n/a'; + return usd.format(roundedValue); +}; + +export const getRoundedNumber = (initialValue: string) => { + return parseFloat(parseFloat(initialValue).toFixed(2)); +}; + +export const getDefaultSummaryRanges = (unit: Unit) => { + return { + areaRange: { min: parseFloat(unit.sqFeet), max: parseFloat(unit.sqFeet) }, + minIncomeRange: { + min: getCurrencyString(unit.monthlyIncomeMin), + max: getCurrencyString(unit.monthlyIncomeMin), + }, + occupancyRange: { min: unit.minOccupancy, max: unit.maxOccupancy }, + rentRange: { + min: getCurrencyString(unit.monthlyRent), + max: getCurrencyString(unit.monthlyRent), + }, + rentAsPercentIncomeRange: { + min: parseFloat(unit.monthlyRentAsPercentOfIncome), + max: parseFloat(unit.monthlyRentAsPercentOfIncome), + }, + floorRange: { + min: unit.floor, + max: unit.floor, + }, + unitTypes: unit.unitTypes, + totalAvailable: 0, + }; +}; + +export const getUnitsSummary = (unit: Unit, existingSummary?: UnitSummary) => { + if (!existingSummary) { + return getDefaultSummaryRanges(unit); + } + const summary = existingSummary; + + // Income Range + summary.minIncomeRange = minMaxCurrency( + summary.minIncomeRange, + getRoundedNumber(unit.monthlyIncomeMin), + ); + + // Occupancy Range + summary.occupancyRange = minMax(summary.occupancyRange, unit.minOccupancy); + summary.occupancyRange = minMax(summary.occupancyRange, unit.maxOccupancy); + + // Rent Ranges + summary.rentAsPercentIncomeRange = minMax( + summary.rentAsPercentIncomeRange, + parseFloat(unit.monthlyRentAsPercentOfIncome), + ); + summary.rentRange = minMaxCurrency( + summary.rentRange, + getRoundedNumber(unit.monthlyRent), + ); + + // Floor Range + if (unit.floor) { + summary.floorRange = minMax(summary.floorRange, unit.floor); + } + + // Area Range + summary.areaRange = minMax(summary.areaRange, parseFloat(unit.sqFeet)); + + return summary; +}; + +// Allows for multiples rows under one unit type if the rent methods differ +export const summarizeUnitsByTypeAndRent = ( + units: Unit[], + listing: ListingGet, +): UnitSummary[] => { + const summaries: UnitSummary[] = []; + const unitMap: UnitMap = {}; + + units.forEach((unit) => { + const currentUnitType = unit.unitTypes; + const currentUnitRent = unit.monthlyRentAsPercentOfIncome; + const thisKey = currentUnitType?.name.concat(currentUnitRent); + if (!(thisKey in unitMap)) unitMap[thisKey] = []; + unitMap[thisKey].push(unit); + }); + + for (const key in unitMap) { + const finalSummary = unitMap[key].reduce((summary, unit, index) => { + return getUnitsSummary(unit, index === 0 ? null : summary); + }, {} as UnitSummary); + if (listing.reviewOrderType !== ReviewOrderTypeEnum.waitlist) { + finalSummary.totalAvailable = unitMap[key].length; + } + summaries.push(finalSummary); + } + + return summaries.sort((a, b) => { + return ( + UnitTypeSort.indexOf(a.unitTypes.name) - + UnitTypeSort.indexOf(b.unitTypes.name) || + Number(a.minIncomeRange.min) - Number(b.minIncomeRange.min) + ); + }); +}; + +// One row per unit type +export const summarizeUnitsByType = ( + units: Unit[], + unitTypes: UnitType[], +): UnitSummary[] => { + const summaries = unitTypes.map((unitType: UnitType): UnitSummary => { + const summary = {} as UnitSummary; + const unitsByType = units.filter( + (unit: Unit) => unit.unitTypes.name == unitType.name, + ); + const finalSummary = Array.from(unitsByType).reduce( + (summary, unit, index) => { + return getUnitsSummary(unit, index === 0 ? null : summary); + }, + summary, + ); + return finalSummary; + }); + return summaries.sort((a, b) => { + return ( + UnitTypeSort.indexOf(a.unitTypes.name) - + UnitTypeSort.indexOf(b.unitTypes.name) || + Number(a.minIncomeRange.min) - Number(b.minIncomeRange.min) + ); + }); +}; + +export const summarizeByAmi = ( + listing: listingGetDto, + amiPercentages: string[], +) => { + return amiPercentages.map((percent: string) => { + const unitsByAmiPercentage = listing.units.filter( + (unit: Unit) => unit.amiPercentage == percent, + ); + return { + percent: percent, + byUnitType: summarizeUnitsByTypeAndRent(unitsByAmiPercentage, listing), + }; + }); +}; + +export const getUnitTypes = (units: Unit[]): UnitType[] => { + const unitTypes = new Map(); + for (const unitType of units + .map((unit) => unit.unitTypes) + .filter((item) => item != null)) { + unitTypes.set(unitType.id, unitType); + } + + return Array.from(unitTypes.values()); +}; + +export const summarizeUnits = ( + listing: ListingGet, + amiCharts: AmiChart[], +): UnitsSummarized => { + const data = {} as UnitsSummarized; + const units = listing.units; + if (!units || (units && units.length === 0)) { + return data; + } + + const unitTypes = new Map(); + for (const unitType of units + .map((unit) => unit.unitTypes) + .filter((item) => item != null)) { + unitTypes.set(unitType.id, unitType); + } + data.unitTypes = getUnitTypes(units); + + const priorityTypes = new Map(); + for (const priorityType of units + .map((unit) => unit.unitAccessibilityPriorityTypes) + .filter((item) => item != null)) { + priorityTypes.set(priorityType.id, priorityType); + } + data.priorityTypes = Array.from(priorityTypes.values()); + + data.amiPercentages = Array.from( + new Set( + units.map((unit) => unit.amiPercentage).filter((item) => item != null), + ), + ); + data.byUnitTypeAndRent = summarizeUnitsByTypeAndRent(listing.units, listing); + data.byUnitType = summarizeUnitsByType(units, data.unitTypes); + data.byAMI = summarizeByAmi(listing, data.amiPercentages); + data.hmi = generateHmiData( + units, + data.byUnitType.map((byUnitType) => byUnitType.occupancyRange), + amiCharts, + ); + return data; +}; diff --git a/backend_new/test/app.e2e-spec.ts b/backend_new/test/integration/app.e2e-spec.ts similarity index 92% rename from backend_new/test/app.e2e-spec.ts rename to backend_new/test/integration/app.e2e-spec.ts index 50cda62332..a3f86f3817 100644 --- a/backend_new/test/app.e2e-spec.ts +++ b/backend_new/test/integration/app.e2e-spec.ts @@ -1,7 +1,7 @@ import { Test, TestingModule } from '@nestjs/testing'; import { INestApplication } from '@nestjs/common'; import * as request from 'supertest'; -import { AppModule } from './../src/app.module'; +import { AppModule } from '../../src/app.module'; describe('AppController (e2e)', () => { let app: INestApplication; diff --git a/backend_new/test/integration/listing.e2e-spec.ts b/backend_new/test/integration/listing.e2e-spec.ts new file mode 100644 index 0000000000..0c014f68bd --- /dev/null +++ b/backend_new/test/integration/listing.e2e-spec.ts @@ -0,0 +1,187 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { INestApplication } from '@nestjs/common'; +import * as request from 'supertest'; +import { AppModule } from '../../src/app.module'; +import { PrismaService } from '../../src/services/prisma.service'; +import { jurisdictionFactory } from '../../prisma/seed-helpers/jurisdiction-factory'; +import { listingFactory } from '../../prisma/seed-helpers/listing-factory'; +import { stringify } from 'qs'; +import { ListingsQueryParams } from '../../src/dtos/listings/listings-query-params.dto'; +import { Compare } from '../../src/dtos/shared/base-filter.dto'; +import { ListingOrderByKeys } from '../../src/enums/listings/order-by-enum'; +import { OrderByEnum } from '../../src/enums/shared/order-by-enum'; +import { ListingViews } from '../../src/enums/listings/view-enum'; + +describe('Listing Controller Tests', () => { + let app: INestApplication; + let prisma: PrismaService; + + beforeEach(async () => { + const moduleFixture: TestingModule = await Test.createTestingModule({ + imports: [AppModule], + }).compile(); + + app = moduleFixture.createNestApplication(); + prisma = moduleFixture.get(PrismaService); + await app.init(); + await clearAllDb(); + }); + + const clearAllDb = async () => { + await prisma.applicationMethods.deleteMany(); + await prisma.listingEvents.deleteMany(); + await prisma.listingImages.deleteMany(); + await prisma.listingMultiselectQuestions.deleteMany(); + await prisma.units.deleteMany(); + await prisma.amiChart.deleteMany(); + await prisma.listings.deleteMany(); + await prisma.reservedCommunityTypes.deleteMany(); + await prisma.jurisdictions.deleteMany(); + }; + + it('list test no params no data', async () => { + const res = await request(app.getHttpServer()).get('/listings').expect(200); + + expect(res.body).toEqual({ + items: [], + meta: { + currentPage: 1, + itemCount: 0, + itemsPerPage: 10, + totalItems: 0, + totalPages: 0, + }, + }); + }); + + it('list test no params some data', async () => { + const jurisdiction = await prisma.jurisdictions.create({ + data: jurisdictionFactory(100), + }); + + await prisma.listings.create({ + data: listingFactory(10, jurisdiction.id), + }); + + await prisma.listings.create({ + data: listingFactory(50, jurisdiction.id), + }); + + const res = await request(app.getHttpServer()).get('/listings').expect(200); + + expect(res.body.meta).toEqual({ + currentPage: 1, + itemCount: 2, + itemsPerPage: 10, + totalItems: 2, + totalPages: 1, + }); + + const items = res.body.items.sort((a, b) => (a.name < b.name ? -1 : 1)); + + expect(res.body.items.length).toEqual(2); + expect(items[0].name).toEqual('name: 10'); + expect(items[1].name).toEqual('name: 50'); + }); + + it('list test params no data', async () => { + const queryParams: ListingsQueryParams = { + limit: 1, + page: 1, + view: ListingViews.base, + filter: [ + { + $comparison: Compare.IN, + name: 'name: 10,name: 50', + }, + ], + }; + const query = stringify(queryParams as any); + + const res = await request(app.getHttpServer()) + .get(`/listings?${query}'`) + .expect(200); + + expect(res.body).toEqual({ + items: [], + meta: { + currentPage: 1, + itemCount: 0, + itemsPerPage: 1, + totalItems: 0, + totalPages: 0, + }, + }); + }); + + it('list test params some data', async () => { + const jurisdiction = await prisma.jurisdictions.create({ + data: jurisdictionFactory(100), + }); + await prisma.listings.create({ + data: listingFactory(10, jurisdiction.id), + }); + await prisma.listings.create({ + data: listingFactory(50, jurisdiction.id), + }); + + let queryParams: ListingsQueryParams = { + limit: 1, + page: 1, + view: ListingViews.base, + filter: [ + { + $comparison: Compare.IN, + name: 'name: 10,name: 50', + }, + ], + orderBy: [ListingOrderByKeys.name], + orderDir: [OrderByEnum.ASC], + }; + let query = stringify(queryParams as any); + + let res = await request(app.getHttpServer()) + .get(`/listings?${query}`) + .expect(200); + + expect(res.body.meta).toEqual({ + currentPage: 1, + itemCount: 1, + itemsPerPage: 1, + totalItems: 2, + totalPages: 2, + }); + + expect(res.body.items.length).toEqual(1); + expect(res.body.items[0].name).toEqual('name: 10'); + + queryParams = { + limit: 1, + page: 2, + view: ListingViews.base, + filter: [ + { + $comparison: Compare.IN, + name: 'name: 10,name: 50', + }, + ], + orderBy: [ListingOrderByKeys.name], + orderDir: [OrderByEnum.ASC], + }; + query = stringify(queryParams as any); + + res = await request(app.getHttpServer()) + .get(`/listings?${query}`) + .expect(200); + + expect(res.body.meta).toEqual({ + currentPage: 2, + itemCount: 1, + itemsPerPage: 1, + totalItems: 2, + totalPages: 2, + }); + expect(res.body.items.length).toEqual(1); + expect(res.body.items[0].name).toEqual('name: 50'); + }); +}); diff --git a/backend_new/test/jest-e2e.config.js b/backend_new/test/jest-e2e.config.js new file mode 100644 index 0000000000..fdaf2bc53f --- /dev/null +++ b/backend_new/test/jest-e2e.config.js @@ -0,0 +1,14 @@ +module.exports = { + moduleFileExtensions: ['js', 'json', 'ts'], + rootDir: '.', + testEnvironment: 'node', + testRegex: '.e2e-spec.ts$', + transform: { + '^.+\\.(t|j)s$': 'ts-jest', + }, + globals: { + 'ts-jest': { + diagnostics: false, + }, + }, +}; diff --git a/backend_new/test/jest-e2e.json b/backend_new/test/jest-e2e.json deleted file mode 100644 index e9d912f3e3..0000000000 --- a/backend_new/test/jest-e2e.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "moduleFileExtensions": ["js", "json", "ts"], - "rootDir": ".", - "testEnvironment": "node", - "testRegex": ".e2e-spec.ts$", - "transform": { - "^.+\\.(t|j)s$": "ts-jest" - } -} diff --git a/backend_new/test/jest.config.js b/backend_new/test/jest.config.js new file mode 100644 index 0000000000..0b293cef18 --- /dev/null +++ b/backend_new/test/jest.config.js @@ -0,0 +1,14 @@ +module.exports = { + moduleFileExtensions: ['js', 'json', 'ts'], + rootDir: '.', + testEnvironment: 'node', + testRegex: '\\.spec.ts$', + transform: { + '^.+\\.(t|j)s$': 'ts-jest', + }, + globals: { + 'ts-jest': { + diagnostics: false, + }, + }, +}; diff --git a/backend_new/test/unit/services/listing.service.spec.ts b/backend_new/test/unit/services/listing.service.spec.ts new file mode 100644 index 0000000000..965dd7a4e3 --- /dev/null +++ b/backend_new/test/unit/services/listing.service.spec.ts @@ -0,0 +1,1974 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { PrismaService } from '../../../src/services/prisma.service'; +import { ListingService } from '../../../src/services/listing.service'; +import { ListingsQueryParams } from '../../../src/dtos/listings/listings-query-params.dto'; +import { ListingOrderByKeys } from '../../../src/enums/listings/order-by-enum'; +import { OrderByEnum } from '../../../src/enums/shared/order-by-enum'; +import { ListingFilterKeys } from '../../../src/enums/listings/filter-key-enum'; +import { Compare } from '../../../src/dtos/shared/base-filter.dto'; +import { ListingFilterParams } from '../../../src/dtos/listings/listings-filter-params.dto'; +import { LanguagesEnum } from '@prisma/client'; +import { Unit } from '../../../src/dtos/units/unit-get.dto'; +import { UnitTypeSort } from '../../../src/utilities/unit-utilities'; +import { ListingGet } from '../../../src/dtos/listings/listing-get.dto'; +import { ListingViews } from '../../../src/enums/listings/view-enum'; + +/* + generates a super simple mock listing for us to test logic with +*/ +const mockListing = ( + pos: number, + genUnits?: { numberToMake: number; date: Date }, +) => { + const toReturn = { id: pos, name: `listing ${pos + 1}`, units: undefined }; + if (genUnits) { + const units: Unit[] = []; + const { numberToMake, date } = genUnits; + for (let i = 0; i < numberToMake; i++) { + units.push({ + id: `unit ${i}`, + createdAt: date, + updatedAt: date, + amiPercentage: `${i}`, + annualIncomeMin: `${i}`, + monthlyIncomeMin: `${i}`, + floor: i, + annualIncomeMax: `${i}`, + maxOccupancy: i, + minOccupancy: i, + monthlyRent: `${i}`, + numBathrooms: i, + numBedrooms: i, + number: `unit ${i}`, + sqFeet: `${i}`, + monthlyRentAsPercentOfIncome: `${i % UnitTypeSort.length}`, + bmrProgramChart: !(i % 2), + unitTypes: { + id: `unitType ${i}`, + createdAt: date, + updatedAt: date, + name: UnitTypeSort[i % UnitTypeSort.length], + numBedrooms: i, + }, + unitAmiChartOverrides: { + id: `unitAmiChartOverrides ${i}`, + createdAt: date, + updatedAt: date, + items: [ + { + percentOfAmi: i, + householdSize: i, + income: i, + }, + ], + }, + amiChart: { + id: `AMI${i}`, + items: [], + name: `AMI Name ${i}`, + createdAt: date, + updatedAt: date, + }, + }); + } + toReturn.units = units; + } + + return toReturn; +}; + +const mockListingSet = ( + pos: number, + genUnits?: { numberToMake: number; date: Date }, +) => { + const toReturn = []; + for (let i = 0; i < pos; i++) { + toReturn.push(mockListing(i, genUnits)); + } + return toReturn; +}; + +describe('Testing listing service', () => { + let service: ListingService; + let prisma: PrismaService; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [ListingService, PrismaService], + }).compile(); + + service = module.get(ListingService); + prisma = module.get(PrismaService); + }); + + it('testing list() with no params', async () => { + prisma.listings.findMany = jest.fn().mockResolvedValue(mockListingSet(10)); + + prisma.listings.count = jest.fn().mockResolvedValue(10); + + const params: ListingsQueryParams = {}; + + expect(await service.list(params)).toEqual({ + items: [ + { id: '0', name: 'listing 1' }, + { id: '1', name: 'listing 2' }, + { id: '2', name: 'listing 3' }, + { id: '3', name: 'listing 4' }, + { id: '4', name: 'listing 5' }, + { id: '5', name: 'listing 6' }, + { id: '6', name: 'listing 7' }, + { id: '7', name: 'listing 8' }, + { id: '8', name: 'listing 9' }, + { id: '9', name: 'listing 10' }, + ], + meta: { + currentPage: 1, + itemCount: 10, + itemsPerPage: 10, + totalItems: 10, + totalPages: 1, + }, + }); + + expect(prisma.listings.findMany).toHaveBeenCalledWith({ + skip: 0, + take: undefined, + orderBy: undefined, + where: { + AND: [], + }, + include: { + jurisdictions: true, + listingsBuildingAddress: true, + reservedCommunityTypes: true, + listingImages: { + include: { + assets: true, + }, + }, + listingMultiselectQuestions: { + include: { + multiselectQuestions: true, + }, + }, + listingFeatures: true, + listingUtilities: true, + applicationMethods: { + include: { + paperApplications: { + include: { + assets: true, + }, + }, + }, + }, + listingsBuildingSelectionCriteriaFile: true, + listingEvents: { + include: { + assets: true, + }, + }, + listingsResult: true, + listingsLeasingAgentAddress: true, + listingsApplicationPickUpAddress: true, + listingsApplicationDropOffAddress: true, + units: { + include: { + unitAmiChartOverrides: true, + unitTypes: true, + unitRentTypes: true, + unitAccessibilityPriorityTypes: true, + amiChart: { + include: { + jurisdictions: true, + amiChartItem: true, + unitGroupAmiLevels: true, + }, + }, + }, + }, + }, + }); + + expect(prisma.listings.count).toHaveBeenCalledWith({ + where: { + AND: [], + }, + }); + }); + + it('testing list() with params', async () => { + prisma.listings.findMany = jest.fn().mockResolvedValue(mockListingSet(10)); + + prisma.listings.count = jest.fn().mockResolvedValue(20); + + const params: ListingsQueryParams = { + view: ListingViews.base, + page: 2, + limit: 10, + orderBy: [ListingOrderByKeys.name], + orderDir: [OrderByEnum.ASC], + search: 'simple search', + filter: [ + { + [ListingFilterKeys.name]: 'Listing,name', + $comparison: Compare.IN, + }, + { + [ListingFilterKeys.bedrooms]: 2, + $comparison: Compare['>='], + }, + ], + }; + + expect(await service.list(params)).toEqual({ + items: [ + { id: '0', name: 'listing 1' }, + { id: '1', name: 'listing 2' }, + { id: '2', name: 'listing 3' }, + { id: '3', name: 'listing 4' }, + { id: '4', name: 'listing 5' }, + { id: '5', name: 'listing 6' }, + { id: '6', name: 'listing 7' }, + { id: '7', name: 'listing 8' }, + { id: '8', name: 'listing 9' }, + { id: '9', name: 'listing 10' }, + ], + meta: { + currentPage: 2, + itemCount: 10, + itemsPerPage: 10, + totalItems: 20, + totalPages: 2, + }, + }); + + expect(prisma.listings.findMany).toHaveBeenCalledWith({ + skip: 10, + take: 10, + orderBy: [ + { + name: 'asc', + }, + ], + where: { + AND: [ + { + OR: [ + { + name: { + in: ['listing', 'name'], + mode: 'insensitive', + }, + }, + ], + }, + { + OR: [ + { + units: { + some: { + numBedrooms: { + gte: 2, + mode: 'insensitive', + }, + }, + }, + }, + ], + }, + { + name: { + contains: 'simple search', + mode: 'insensitive', + }, + }, + ], + }, + include: { + jurisdictions: true, + listingsBuildingAddress: true, + reservedCommunityTypes: true, + listingImages: { + include: { + assets: true, + }, + }, + listingMultiselectQuestions: { + include: { + multiselectQuestions: true, + }, + }, + listingFeatures: true, + listingUtilities: true, + units: { + include: { + unitTypes: true, + unitAmiChartOverrides: true, + amiChart: { + include: { + amiChartItem: true, + }, + }, + }, + }, + }, + }); + + expect(prisma.listings.count).toHaveBeenCalledWith({ + where: { + AND: [ + { + OR: [ + { + name: { + in: ['listing', 'name'], + mode: 'insensitive', + }, + }, + ], + }, + { + OR: [ + { + units: { + some: { + numBedrooms: { + gte: 2, + mode: 'insensitive', + }, + }, + }, + }, + ], + }, + { + name: { + contains: 'simple search', + mode: 'insensitive', + }, + }, + ], + }, + }); + }); + + it('testing buildWhereClause() with params no search', async () => { + const params: ListingFilterParams[] = [ + { + [ListingFilterKeys.name]: 'Listing,name', + $comparison: Compare.IN, + }, + { + [ListingFilterKeys.bedrooms]: 2, + $comparison: Compare['>='], + }, + ]; + + expect(service.buildWhereClause(params)).toEqual({ + AND: [ + { + OR: [ + { + name: { + in: ['listing', 'name'], + mode: 'insensitive', + }, + }, + ], + }, + { + OR: [ + { + units: { + some: { + numBedrooms: { + gte: 2, + mode: 'insensitive', + }, + }, + }, + }, + ], + }, + ], + }); + }); + + it('testing buildWhereClause() with no params, search present', async () => { + expect(service.buildWhereClause(null, 'simple search')).toEqual({ + AND: [ + { + name: { + contains: 'simple search', + mode: 'insensitive', + }, + }, + ], + }); + }); + + it('testing buildWhereClause() with params, and search present', async () => { + const params: ListingFilterParams[] = [ + { + [ListingFilterKeys.name]: 'Listing,name', + $comparison: Compare.IN, + }, + { + [ListingFilterKeys.bedrooms]: 2, + $comparison: Compare['>='], + }, + ]; + + expect(service.buildWhereClause(params, 'simple search')).toEqual({ + AND: [ + { + OR: [ + { + name: { + in: ['listing', 'name'], + mode: 'insensitive', + }, + }, + ], + }, + { + OR: [ + { + units: { + some: { + numBedrooms: { + gte: 2, + mode: 'insensitive', + }, + }, + }, + }, + ], + }, + { + name: { + contains: 'simple search', + mode: 'insensitive', + }, + }, + ], + }); + }); + + it('testing findOne() base view found record', async () => { + prisma.listings.findFirst = jest.fn().mockResolvedValue(mockListing(0)); + + expect( + await service.findOne('listingId', LanguagesEnum.en, ListingViews.base), + ).toEqual({ id: '0', name: 'listing 1' }); + + expect(prisma.listings.findFirst).toHaveBeenCalledWith({ + where: { + id: { + equals: 'listingId', + }, + }, + include: { + jurisdictions: true, + listingsBuildingAddress: true, + reservedCommunityTypes: true, + listingImages: { + include: { + assets: true, + }, + }, + listingMultiselectQuestions: { + include: { + multiselectQuestions: true, + }, + }, + listingFeatures: true, + listingUtilities: true, + units: { + include: { + unitTypes: true, + unitAmiChartOverrides: true, + amiChart: { + include: { + amiChartItem: true, + }, + }, + }, + }, + }, + }); + }); + + it('testing findOne() base view no record found', async () => { + prisma.listings.findFirst = jest.fn().mockResolvedValue(null); + + await expect( + async () => + await service.findOne( + 'a different listingId', + LanguagesEnum.en, + ListingViews.details, + ), + ).rejects.toThrowError(); + + expect(prisma.listings.findFirst).toHaveBeenCalledWith({ + where: { + id: { + equals: 'a different listingId', + }, + }, + include: { + jurisdictions: true, + listingsBuildingAddress: true, + reservedCommunityTypes: true, + listingImages: { + include: { + assets: true, + }, + }, + listingMultiselectQuestions: { + include: { + multiselectQuestions: true, + }, + }, + listingFeatures: true, + listingUtilities: true, + applicationMethods: { + include: { + paperApplications: { + include: { + assets: true, + }, + }, + }, + }, + listingsBuildingSelectionCriteriaFile: true, + listingEvents: { + include: { + assets: true, + }, + }, + listingsResult: true, + listingsLeasingAgentAddress: true, + listingsApplicationPickUpAddress: true, + listingsApplicationDropOffAddress: true, + units: { + include: { + unitAmiChartOverrides: true, + unitTypes: true, + unitRentTypes: true, + unitAccessibilityPriorityTypes: true, + amiChart: { + include: { + jurisdictions: true, + amiChartItem: true, + unitGroupAmiLevels: true, + }, + }, + }, + }, + }, + }); + }); + + it('testing list() with params and units', async () => { + const date = new Date(); + + prisma.listings.findMany = jest + .fn() + .mockResolvedValue([mockListing(9, { numberToMake: 9, date })]); + + prisma.listings.count = jest.fn().mockResolvedValue(20); + + const params: ListingsQueryParams = { + view: ListingViews.base, + page: 2, + limit: 10, + orderBy: [ListingOrderByKeys.name], + orderDir: [OrderByEnum.ASC], + search: 'simple search', + filter: [ + { + [ListingFilterKeys.name]: 'Listing,name', + $comparison: Compare.IN, + }, + { + [ListingFilterKeys.bedrooms]: 2, + $comparison: Compare['>='], + }, + ], + }; + + const res = await service.list(params); + + expect(res.items[0].name).toEqual(`listing ${10}`); + expect(res.items[0].units).toEqual( + mockListing(9, { numberToMake: 9, date }).units, + ); + expect(res.items[0].unitsSummarized).toEqual({ + byUnitTypeAndRent: [ + { + unitTypes: { + createdAt: date, + updatedAt: date, + id: 'unitType 0', + name: UnitTypeSort[0], + numBedrooms: 0, + }, + minIncomeRange: { + max: '$5', + min: '$0', + }, + occupancyRange: { + max: 5, + min: 0, + }, + rentAsPercentIncomeRange: { + max: 0, + min: 0, + }, + rentRange: { + max: '$5', + min: '$0', + }, + totalAvailable: 2, + areaRange: { + max: 5, + min: 0, + }, + floorRange: { + max: 5, + min: 0, + }, + }, + { + unitTypes: { + createdAt: date, + updatedAt: date, + id: 'unitType 1', + name: UnitTypeSort[1], + numBedrooms: 1, + }, + minIncomeRange: { + max: '$6', + min: '$1', + }, + occupancyRange: { + max: 6, + min: 1, + }, + rentAsPercentIncomeRange: { + max: 1, + min: 1, + }, + rentRange: { + max: '$6', + min: '$1', + }, + totalAvailable: 2, + areaRange: { + max: 6, + min: 1, + }, + floorRange: { + max: 6, + min: 1, + }, + }, + { + unitTypes: { + createdAt: date, + updatedAt: date, + id: 'unitType 2', + name: UnitTypeSort[2], + numBedrooms: 2, + }, + minIncomeRange: { + max: '$7', + min: '$2', + }, + occupancyRange: { + max: 7, + min: 2, + }, + rentAsPercentIncomeRange: { + max: 2, + min: 2, + }, + rentRange: { + max: '$7', + min: '$2', + }, + totalAvailable: 2, + areaRange: { + max: 7, + min: 2, + }, + floorRange: { + max: 7, + min: 2, + }, + }, + { + unitTypes: { + createdAt: date, + updatedAt: date, + id: 'unitType 3', + name: UnitTypeSort[3], + numBedrooms: 3, + }, + minIncomeRange: { + max: '$8', + min: '$3', + }, + occupancyRange: { + max: 8, + min: 3, + }, + rentAsPercentIncomeRange: { + max: 3, + min: 3, + }, + rentRange: { + max: '$8', + min: '$3', + }, + totalAvailable: 2, + areaRange: { + max: 8, + min: 3, + }, + floorRange: { + max: 8, + min: 3, + }, + }, + { + unitTypes: { + createdAt: date, + updatedAt: date, + id: 'unitType 4', + name: UnitTypeSort[4], + numBedrooms: 4, + }, + minIncomeRange: { + max: '$4', + min: '$4', + }, + occupancyRange: { + max: 4, + min: 4, + }, + rentAsPercentIncomeRange: { + max: 4, + min: 4, + }, + rentRange: { + max: '$4', + min: '$4', + }, + totalAvailable: 1, + areaRange: { + max: 4, + min: 4, + }, + floorRange: { + max: 4, + min: 4, + }, + }, + ], + }); + + expect(res.meta).toEqual({ + currentPage: 2, + itemCount: 1, + itemsPerPage: 10, + totalItems: 20, + totalPages: 2, + }); + + expect(prisma.listings.findMany).toHaveBeenCalledWith({ + skip: 10, + take: 10, + orderBy: [ + { + name: 'asc', + }, + ], + where: { + AND: [ + { + OR: [ + { + name: { + in: ['listing', 'name'], + mode: 'insensitive', + }, + }, + ], + }, + { + OR: [ + { + units: { + some: { + numBedrooms: { + gte: 2, + mode: 'insensitive', + }, + }, + }, + }, + ], + }, + { + name: { + contains: 'simple search', + mode: 'insensitive', + }, + }, + ], + }, + include: { + jurisdictions: true, + listingsBuildingAddress: true, + reservedCommunityTypes: true, + listingImages: { + include: { + assets: true, + }, + }, + listingMultiselectQuestions: { + include: { + multiselectQuestions: true, + }, + }, + listingFeatures: true, + listingUtilities: true, + units: { + include: { + unitTypes: true, + unitAmiChartOverrides: true, + amiChart: { + include: { + amiChartItem: true, + }, + }, + }, + }, + }, + }); + + expect(prisma.listings.count).toHaveBeenCalledWith({ + where: { + AND: [ + { + OR: [ + { + name: { + in: ['listing', 'name'], + mode: 'insensitive', + }, + }, + ], + }, + { + OR: [ + { + units: { + some: { + numBedrooms: { + gte: 2, + mode: 'insensitive', + }, + }, + }, + }, + ], + }, + { + name: { + contains: 'simple search', + mode: 'insensitive', + }, + }, + ], + }, + }); + }); + + it('testing findOne() base view found record and units', async () => { + const date = new Date(); + + const mockedListing = mockListing(0, { numberToMake: 10, date }); + + prisma.listings.findFirst = jest.fn().mockResolvedValue(mockedListing); + + prisma.amiChart.findMany = jest.fn().mockResolvedValue([ + { + id: 'AMI0', + items: [], + name: '`AMI Name 0`', + }, + { + id: 'AMI1', + items: [], + name: '`AMI Name 1`', + }, + ]); + + const listing: ListingGet = await service.findOne( + 'listingId', + LanguagesEnum.en, + ListingViews.base, + ); + + expect(listing.id).toEqual('0'); + expect(listing.name).toEqual('listing 1'); + expect(listing.units).toEqual(mockedListing.units); + expect(listing.unitsSummarized).toEqual({ + amiPercentages: ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9'], + priorityTypes: [], + unitTypes: [ + { + createdAt: date, + id: 'unitType 0', + name: 'SRO', + numBedrooms: 0, + updatedAt: date, + }, + { + createdAt: date, + id: 'unitType 1', + name: 'studio', + numBedrooms: 1, + updatedAt: date, + }, + { + createdAt: date, + id: 'unitType 2', + name: 'oneBdrm', + numBedrooms: 2, + updatedAt: date, + }, + { + createdAt: date, + id: 'unitType 3', + name: 'twoBdrm', + numBedrooms: 3, + updatedAt: date, + }, + { + createdAt: date, + id: 'unitType 4', + name: 'threeBdrm', + numBedrooms: 4, + updatedAt: date, + }, + { + createdAt: date, + id: 'unitType 5', + name: 'SRO', + numBedrooms: 5, + updatedAt: date, + }, + { + createdAt: date, + id: 'unitType 6', + name: 'studio', + numBedrooms: 6, + updatedAt: date, + }, + { + createdAt: date, + id: 'unitType 7', + name: 'oneBdrm', + numBedrooms: 7, + updatedAt: date, + }, + { + createdAt: date, + id: 'unitType 8', + name: 'twoBdrm', + numBedrooms: 8, + updatedAt: date, + }, + { + createdAt: date, + id: 'unitType 9', + name: 'threeBdrm', + numBedrooms: 9, + updatedAt: date, + }, + ], + byAMI: [ + { + byUnitType: [ + { + areaRange: { + max: 0, + min: 0, + }, + floorRange: { + max: 0, + min: 0, + }, + minIncomeRange: { + max: '$0', + min: '$0', + }, + occupancyRange: { + max: 0, + min: 0, + }, + rentAsPercentIncomeRange: { + max: 0, + min: 0, + }, + rentRange: { + max: '$0', + min: '$0', + }, + totalAvailable: 1, + unitTypes: { + createdAt: date, + id: 'unitType 0', + name: 'SRO', + numBedrooms: 0, + updatedAt: date, + }, + }, + ], + percent: '0', + }, + { + byUnitType: [ + { + areaRange: { + max: 1, + min: 1, + }, + floorRange: { + max: 1, + min: 1, + }, + minIncomeRange: { + max: '$1', + min: '$1', + }, + occupancyRange: { + max: 1, + min: 1, + }, + rentAsPercentIncomeRange: { + max: 1, + min: 1, + }, + rentRange: { + max: '$1', + min: '$1', + }, + totalAvailable: 1, + unitTypes: { + createdAt: date, + id: 'unitType 1', + name: 'studio', + numBedrooms: 1, + updatedAt: date, + }, + }, + ], + percent: '1', + }, + { + byUnitType: [ + { + areaRange: { + max: 2, + min: 2, + }, + floorRange: { + max: 2, + min: 2, + }, + minIncomeRange: { + max: '$2', + min: '$2', + }, + occupancyRange: { + max: 2, + min: 2, + }, + rentAsPercentIncomeRange: { + max: 2, + min: 2, + }, + rentRange: { + max: '$2', + min: '$2', + }, + totalAvailable: 1, + unitTypes: { + createdAt: date, + id: 'unitType 2', + name: 'oneBdrm', + numBedrooms: 2, + updatedAt: date, + }, + }, + ], + percent: '2', + }, + { + byUnitType: [ + { + areaRange: { + max: 3, + min: 3, + }, + floorRange: { + max: 3, + min: 3, + }, + minIncomeRange: { + max: '$3', + min: '$3', + }, + occupancyRange: { + max: 3, + min: 3, + }, + rentAsPercentIncomeRange: { + max: 3, + min: 3, + }, + rentRange: { + max: '$3', + min: '$3', + }, + totalAvailable: 1, + unitTypes: { + createdAt: date, + id: 'unitType 3', + name: 'twoBdrm', + numBedrooms: 3, + updatedAt: date, + }, + }, + ], + percent: '3', + }, + { + byUnitType: [ + { + areaRange: { + max: 4, + min: 4, + }, + floorRange: { + max: 4, + min: 4, + }, + minIncomeRange: { + max: '$4', + min: '$4', + }, + occupancyRange: { + max: 4, + min: 4, + }, + rentAsPercentIncomeRange: { + max: 4, + min: 4, + }, + rentRange: { + max: '$4', + min: '$4', + }, + totalAvailable: 1, + unitTypes: { + createdAt: date, + id: 'unitType 4', + name: 'threeBdrm', + numBedrooms: 4, + updatedAt: date, + }, + }, + ], + percent: '4', + }, + { + byUnitType: [ + { + areaRange: { + max: 5, + min: 5, + }, + floorRange: { + max: 5, + min: 5, + }, + minIncomeRange: { + max: '$5', + min: '$5', + }, + occupancyRange: { + max: 5, + min: 5, + }, + rentAsPercentIncomeRange: { + max: 0, + min: 0, + }, + rentRange: { + max: '$5', + min: '$5', + }, + totalAvailable: 1, + unitTypes: { + createdAt: date, + id: 'unitType 5', + name: 'SRO', + numBedrooms: 5, + updatedAt: date, + }, + }, + ], + percent: '5', + }, + { + byUnitType: [ + { + areaRange: { + max: 6, + min: 6, + }, + floorRange: { + max: 6, + min: 6, + }, + minIncomeRange: { + max: '$6', + min: '$6', + }, + occupancyRange: { + max: 6, + min: 6, + }, + rentAsPercentIncomeRange: { + max: 1, + min: 1, + }, + rentRange: { + max: '$6', + min: '$6', + }, + totalAvailable: 1, + unitTypes: { + createdAt: date, + id: 'unitType 6', + name: 'studio', + numBedrooms: 6, + updatedAt: date, + }, + }, + ], + percent: '6', + }, + { + byUnitType: [ + { + areaRange: { + max: 7, + min: 7, + }, + floorRange: { + max: 7, + min: 7, + }, + minIncomeRange: { + max: '$7', + min: '$7', + }, + occupancyRange: { + max: 7, + min: 7, + }, + rentAsPercentIncomeRange: { + max: 2, + min: 2, + }, + rentRange: { + max: '$7', + min: '$7', + }, + totalAvailable: 1, + unitTypes: { + createdAt: date, + id: 'unitType 7', + name: 'oneBdrm', + numBedrooms: 7, + updatedAt: date, + }, + }, + ], + percent: '7', + }, + { + byUnitType: [ + { + areaRange: { + max: 8, + min: 8, + }, + floorRange: { + max: 8, + min: 8, + }, + minIncomeRange: { + max: '$8', + min: '$8', + }, + occupancyRange: { + max: 8, + min: 8, + }, + rentAsPercentIncomeRange: { + max: 3, + min: 3, + }, + rentRange: { + max: '$8', + min: '$8', + }, + totalAvailable: 1, + unitTypes: { + createdAt: date, + id: 'unitType 8', + name: 'twoBdrm', + numBedrooms: 8, + updatedAt: date, + }, + }, + ], + percent: '8', + }, + { + byUnitType: [ + { + areaRange: { + max: 9, + min: 9, + }, + floorRange: { + max: 9, + min: 9, + }, + minIncomeRange: { + max: '$9', + min: '$9', + }, + occupancyRange: { + max: 9, + min: 9, + }, + rentAsPercentIncomeRange: { + max: 4, + min: 4, + }, + rentRange: { + max: '$9', + min: '$9', + }, + totalAvailable: 1, + unitTypes: { + createdAt: date, + id: 'unitType 9', + name: 'threeBdrm', + numBedrooms: 9, + updatedAt: date, + }, + }, + ], + percent: '9', + }, + ], + byUnitType: [ + { + areaRange: { + max: 5, + min: 0, + }, + floorRange: { + max: 5, + min: 0, + }, + minIncomeRange: { + max: '$5', + min: '$0', + }, + occupancyRange: { + max: 5, + min: 0, + }, + rentAsPercentIncomeRange: { + max: 0, + min: 0, + }, + rentRange: { + max: '$5', + min: '$0', + }, + totalAvailable: 0, + unitTypes: { + createdAt: date, + id: 'unitType 0', + name: 'SRO', + numBedrooms: 0, + updatedAt: date, + }, + }, + { + areaRange: { + max: 5, + min: 0, + }, + floorRange: { + max: 5, + min: 0, + }, + minIncomeRange: { + max: '$5', + min: '$0', + }, + occupancyRange: { + max: 5, + min: 0, + }, + rentAsPercentIncomeRange: { + max: 0, + min: 0, + }, + rentRange: { + max: '$5', + min: '$0', + }, + totalAvailable: 0, + unitTypes: { + createdAt: date, + id: 'unitType 0', + name: 'SRO', + numBedrooms: 0, + updatedAt: date, + }, + }, + { + areaRange: { + max: 6, + min: 1, + }, + floorRange: { + max: 6, + min: 1, + }, + minIncomeRange: { + max: '$6', + min: '$1', + }, + occupancyRange: { + max: 6, + min: 1, + }, + rentAsPercentIncomeRange: { + max: 1, + min: 1, + }, + rentRange: { + max: '$6', + min: '$1', + }, + totalAvailable: 0, + unitTypes: { + createdAt: date, + id: 'unitType 1', + name: 'studio', + numBedrooms: 1, + updatedAt: date, + }, + }, + { + areaRange: { + max: 6, + min: 1, + }, + floorRange: { + max: 6, + min: 1, + }, + minIncomeRange: { + max: '$6', + min: '$1', + }, + occupancyRange: { + max: 6, + min: 1, + }, + rentAsPercentIncomeRange: { + max: 1, + min: 1, + }, + rentRange: { + max: '$6', + min: '$1', + }, + totalAvailable: 0, + unitTypes: { + createdAt: date, + id: 'unitType 1', + name: 'studio', + numBedrooms: 1, + updatedAt: date, + }, + }, + { + areaRange: { + max: 7, + min: 2, + }, + floorRange: { + max: 7, + min: 2, + }, + minIncomeRange: { + max: '$7', + min: '$2', + }, + occupancyRange: { + max: 7, + min: 2, + }, + rentAsPercentIncomeRange: { + max: 2, + min: 2, + }, + rentRange: { + max: '$7', + min: '$2', + }, + totalAvailable: 0, + unitTypes: { + createdAt: date, + id: 'unitType 2', + name: 'oneBdrm', + numBedrooms: 2, + updatedAt: date, + }, + }, + { + areaRange: { + max: 7, + min: 2, + }, + floorRange: { + max: 7, + min: 2, + }, + minIncomeRange: { + max: '$7', + min: '$2', + }, + occupancyRange: { + max: 7, + min: 2, + }, + rentAsPercentIncomeRange: { + max: 2, + min: 2, + }, + rentRange: { + max: '$7', + min: '$2', + }, + totalAvailable: 0, + unitTypes: { + createdAt: date, + id: 'unitType 2', + name: 'oneBdrm', + numBedrooms: 2, + updatedAt: date, + }, + }, + { + areaRange: { + max: 8, + min: 3, + }, + floorRange: { + max: 8, + min: 3, + }, + minIncomeRange: { + max: '$8', + min: '$3', + }, + occupancyRange: { + max: 8, + min: 3, + }, + rentAsPercentIncomeRange: { + max: 3, + min: 3, + }, + rentRange: { + max: '$8', + min: '$3', + }, + totalAvailable: 0, + unitTypes: { + createdAt: date, + id: 'unitType 3', + name: 'twoBdrm', + numBedrooms: 3, + updatedAt: date, + }, + }, + { + areaRange: { + max: 8, + min: 3, + }, + floorRange: { + max: 8, + min: 3, + }, + minIncomeRange: { + max: '$8', + min: '$3', + }, + occupancyRange: { + max: 8, + min: 3, + }, + rentAsPercentIncomeRange: { + max: 3, + min: 3, + }, + rentRange: { + max: '$8', + min: '$3', + }, + totalAvailable: 0, + unitTypes: { + createdAt: date, + id: 'unitType 3', + name: 'twoBdrm', + numBedrooms: 3, + updatedAt: date, + }, + }, + { + areaRange: { + max: 9, + min: 4, + }, + floorRange: { + max: 9, + min: 4, + }, + minIncomeRange: { + max: '$9', + min: '$4', + }, + occupancyRange: { + max: 9, + min: 4, + }, + rentAsPercentIncomeRange: { + max: 4, + min: 4, + }, + rentRange: { + max: '$9', + min: '$4', + }, + totalAvailable: 0, + unitTypes: { + createdAt: date, + id: 'unitType 4', + name: 'threeBdrm', + numBedrooms: 4, + updatedAt: date, + }, + }, + { + areaRange: { + max: 9, + min: 4, + }, + floorRange: { + max: 9, + min: 4, + }, + minIncomeRange: { + max: '$9', + min: '$4', + }, + occupancyRange: { + max: 9, + min: 4, + }, + rentAsPercentIncomeRange: { + max: 4, + min: 4, + }, + rentRange: { + max: '$9', + min: '$4', + }, + totalAvailable: 0, + unitTypes: { + createdAt: date, + id: 'unitType 4', + name: 'threeBdrm', + numBedrooms: 4, + updatedAt: date, + }, + }, + ], + byUnitTypeAndRent: [ + { + areaRange: { + max: 5, + min: 0, + }, + floorRange: { + max: 5, + min: 0, + }, + minIncomeRange: { + max: '$5', + min: '$0', + }, + occupancyRange: { + max: 5, + min: 0, + }, + rentAsPercentIncomeRange: { + max: 0, + min: 0, + }, + rentRange: { + max: '$5', + min: '$0', + }, + totalAvailable: 2, + unitTypes: { + createdAt: date, + id: 'unitType 0', + name: 'SRO', + numBedrooms: 0, + updatedAt: date, + }, + }, + { + areaRange: { + max: 6, + min: 1, + }, + floorRange: { + max: 6, + min: 1, + }, + minIncomeRange: { + max: '$6', + min: '$1', + }, + occupancyRange: { + max: 6, + min: 1, + }, + rentAsPercentIncomeRange: { + max: 1, + min: 1, + }, + rentRange: { + max: '$6', + min: '$1', + }, + totalAvailable: 2, + unitTypes: { + createdAt: date, + id: 'unitType 1', + name: 'studio', + numBedrooms: 1, + updatedAt: date, + }, + }, + { + areaRange: { + max: 7, + min: 2, + }, + floorRange: { + max: 7, + min: 2, + }, + minIncomeRange: { + max: '$7', + min: '$2', + }, + occupancyRange: { + max: 7, + min: 2, + }, + rentAsPercentIncomeRange: { + max: 2, + min: 2, + }, + rentRange: { + max: '$7', + min: '$2', + }, + totalAvailable: 2, + unitTypes: { + createdAt: date, + id: 'unitType 2', + name: 'oneBdrm', + numBedrooms: 2, + updatedAt: date, + }, + }, + { + areaRange: { + max: 8, + min: 3, + }, + floorRange: { + max: 8, + min: 3, + }, + minIncomeRange: { + max: '$8', + min: '$3', + }, + occupancyRange: { + max: 8, + min: 3, + }, + rentAsPercentIncomeRange: { + max: 3, + min: 3, + }, + rentRange: { + max: '$8', + min: '$3', + }, + totalAvailable: 2, + unitTypes: { + createdAt: date, + id: 'unitType 3', + name: 'twoBdrm', + numBedrooms: 3, + updatedAt: date, + }, + }, + { + areaRange: { + max: 9, + min: 4, + }, + floorRange: { + max: 9, + min: 4, + }, + minIncomeRange: { + max: '$9', + min: '$4', + }, + occupancyRange: { + max: 9, + min: 4, + }, + rentAsPercentIncomeRange: { + max: 4, + min: 4, + }, + rentRange: { + max: '$9', + min: '$4', + }, + totalAvailable: 2, + unitTypes: { + createdAt: date, + id: 'unitType 4', + name: 'threeBdrm', + numBedrooms: 4, + updatedAt: date, + }, + }, + ], + hmi: { + columns: { + ami0: 'listings.percentAMIUnit*percent:0', + ami1: 'listings.percentAMIUnit*percent:1', + ami2: 'listings.percentAMIUnit*percent:2', + ami3: 'listings.percentAMIUnit*percent:3', + ami4: 'listings.percentAMIUnit*percent:4', + ami5: 'listings.percentAMIUnit*percent:5', + ami6: 'listings.percentAMIUnit*percent:6', + ami7: 'listings.percentAMIUnit*percent:7', + ami8: 'listings.percentAMIUnit*percent:8', + ami9: 'listings.percentAMIUnit*percent:9', + sizeColumn: 't.unitType', + }, + rows: [], + }, + }); + + expect(prisma.listings.findFirst).toHaveBeenCalledWith({ + where: { + id: { + equals: 'listingId', + }, + }, + include: { + jurisdictions: true, + listingsBuildingAddress: true, + reservedCommunityTypes: true, + listingImages: { + include: { + assets: true, + }, + }, + listingMultiselectQuestions: { + include: { + multiselectQuestions: true, + }, + }, + listingFeatures: true, + listingUtilities: true, + units: { + include: { + unitTypes: true, + unitAmiChartOverrides: true, + amiChart: { + include: { + amiChartItem: true, + }, + }, + }, + }, + }, + }); + + expect(prisma.amiChart.findMany).toHaveBeenCalledWith({ + where: { + id: { + in: mockedListing.units.map((unit) => unit.amiChart.id), + }, + }, + }); + }); +}); diff --git a/backend_new/test/unit/utilities/build-filter.spec.ts b/backend_new/test/unit/utilities/build-filter.spec.ts new file mode 100644 index 0000000000..fa468ef6d8 --- /dev/null +++ b/backend_new/test/unit/utilities/build-filter.spec.ts @@ -0,0 +1,201 @@ +import { buildFilter } from '../../../src/utilities/build-filter'; +import { Compare } from '../../../src/dtos/shared/base-filter.dto'; +describe('Testing constructFilter', () => { + it('should correctly build IN filter when includes null is false', () => { + expect( + buildFilter({ + $comparison: Compare.IN, + $include_nulls: false, + value: 'Capital, lowercase,', + key: 'a key', + }), + ).toEqual([ + { + in: ['capital', 'lowercase'], + mode: 'insensitive', + }, + ]); + }); + it('should correctly build IN filter when includes null is true', () => { + expect( + buildFilter({ + $comparison: Compare.IN, + $include_nulls: true, + value: 'Capital, lowercase,', + key: 'a key', + }), + ).toEqual([ + { + in: ['capital', 'lowercase'], + mode: 'insensitive', + }, + { + equals: null, + }, + ]); + }); + + it('should correctly build <> filter when includes null is false', () => { + expect( + buildFilter({ + $comparison: Compare['<>'], + $include_nulls: false, + value: 'example', + key: 'a key', + }), + ).toEqual([ + { + not: { + equals: 'example', + }, + mode: 'insensitive', + }, + ]); + }); + it('should correctly build <> filter when includes null is true', () => { + expect( + buildFilter({ + $comparison: Compare['<>'], + $include_nulls: true, + value: 'example', + key: 'a key', + }), + ).toEqual([ + { + not: { + equals: 'example', + }, + mode: 'insensitive', + }, + { + equals: null, + }, + ]); + }); + + it('should correctly build = filter when includes null is false', () => { + expect( + buildFilter({ + $comparison: Compare['='], + $include_nulls: false, + value: 'example', + key: 'a key', + }), + ).toEqual([ + { + equals: 'example', + mode: 'insensitive', + }, + ]); + }); + it('should correctly build = filter when includes null is true', () => { + expect( + buildFilter({ + $comparison: Compare['='], + $include_nulls: true, + value: 'example', + key: 'a key', + }), + ).toEqual([ + { + equals: 'example', + mode: 'insensitive', + }, + { + equals: null, + }, + ]); + }); + + it('should correctly build >= filter when includes null is false', () => { + expect( + buildFilter({ + $comparison: Compare['>='], + $include_nulls: false, + value: 'example', + key: 'a key', + }), + ).toEqual([ + { + gte: 'example', + mode: 'insensitive', + }, + ]); + }); + it('should correctly build >= filter when includes null is true', () => { + expect( + buildFilter({ + $comparison: Compare['>='], + $include_nulls: true, + value: 'example', + key: 'a key', + }), + ).toEqual([ + { + gte: 'example', + mode: 'insensitive', + }, + { + equals: null, + }, + ]); + }); + + it('should correctly build <= filter when includes null is false', () => { + expect( + buildFilter({ + $comparison: Compare['<='], + $include_nulls: false, + value: 'example', + key: 'a key', + }), + ).toEqual([ + { + lte: 'example', + mode: 'insensitive', + }, + ]); + }); + it('should correctly build <= filter when includes null is true', () => { + expect( + buildFilter({ + $comparison: Compare['<='], + $include_nulls: true, + value: 'example', + key: 'a key', + }), + ).toEqual([ + { + lte: 'example', + mode: 'insensitive', + }, + { + equals: null, + }, + ]); + }); + + it('should error if NA filter', () => { + expect(() => + buildFilter({ + $comparison: Compare.NA, + $include_nulls: false, + value: 'example', + key: 'a key', + }), + ).toThrowError( + 'Filter "a key" expected to be handled by a custom filter handler, but one was not implemented.', + ); + }); + + it('should error if unsupport comparison passed in', () => { + expect(() => + buildFilter({ + $comparison: 'aaa' as Compare.NA, + $include_nulls: false, + value: 'example', + key: 'a key', + }), + ).toThrowError(); + }); +}); diff --git a/backend_new/test/unit/utilities/build-order-by.spec.ts b/backend_new/test/unit/utilities/build-order-by.spec.ts new file mode 100644 index 0000000000..eb7ddfb983 --- /dev/null +++ b/backend_new/test/unit/utilities/build-order-by.spec.ts @@ -0,0 +1,12 @@ +import { OrderByEnum } from '../../../src/enums/shared/order-by-enum'; +import { buildOrderBy } from '../../../src/utilities/build-order-by'; +describe('Testing buildOrderBy', () => { + it('should return correctly mapped array when both arrays have length', () => { + expect( + buildOrderBy(['order1', 'order2'], [OrderByEnum.ASC, OrderByEnum.DESC]), + ).toEqual([{ order1: 'asc' }, { order2: 'desc' }]); + }); + it('should return empty array when both arrays are empty', () => { + expect(buildOrderBy([], [])).toEqual(undefined); + }); +}); diff --git a/backend_new/test/unit/utilities/order-by-validator.spec.ts b/backend_new/test/unit/utilities/order-by-validator.spec.ts new file mode 100644 index 0000000000..cbcd63c0b2 --- /dev/null +++ b/backend_new/test/unit/utilities/order-by-validator.spec.ts @@ -0,0 +1,87 @@ +import { OrderQueryParamValidator } from '../../../src/utilities/order-by-validator'; +describe('Testing OrderQueryParamValidator', () => { + it('should return true if order array and orderDir array are the same length', () => { + const orderQueryParamValidator = new OrderQueryParamValidator(); + expect( + orderQueryParamValidator.validate(['order'], { + property: 'orderDir', + object: { + orderBy: ['asc'], + }, + value: undefined, + constraints: [], + targetName: '', + }), + ).toBeTruthy(); + }); + it('should return true if order array and orderBy array are the same length', () => { + const orderQueryParamValidator = new OrderQueryParamValidator(); + expect( + orderQueryParamValidator.validate(['order'], { + property: 'orderBy', + object: { + orderDir: ['asc'], + }, + value: undefined, + constraints: [], + targetName: '', + }), + ).toBeTruthy(); + }); + it('should return false if order array length > orderDir array length', () => { + const orderQueryParamValidator = new OrderQueryParamValidator(); + expect( + orderQueryParamValidator.validate(['order', 'order2'], { + property: 'orderDir', + object: { + orderBy: ['asc'], + }, + value: undefined, + constraints: [], + targetName: '', + }), + ).toBeFalsy(); + }); + it('should return false if order array length < orderDir array length', () => { + const orderQueryParamValidator = new OrderQueryParamValidator(); + expect( + orderQueryParamValidator.validate(['order'], { + property: 'orderDir', + object: { + orderBy: ['asc', 'asc'], + }, + value: undefined, + constraints: [], + targetName: '', + }), + ).toBeFalsy(); + }); + it('should return false if order array length > orderBy array length', () => { + const orderQueryParamValidator = new OrderQueryParamValidator(); + expect( + orderQueryParamValidator.validate(['order', 'order2'], { + property: 'orderBy', + object: { + orderDir: ['asc'], + }, + value: undefined, + constraints: [], + targetName: '', + }), + ).toBeFalsy(); + }); + it('should return false if order array length < orderBy array length', () => { + const orderQueryParamValidator = new OrderQueryParamValidator(); + expect( + orderQueryParamValidator.validate(['order'], { + property: 'orderBy', + object: { + orderDir: ['asc', 'asc'], + }, + value: undefined, + constraints: [], + targetName: '', + }), + ).toBeFalsy(); + }); +}); diff --git a/backend_new/test/unit/utilities/pagination-helpers.spec.ts b/backend_new/test/unit/utilities/pagination-helpers.spec.ts new file mode 100644 index 0000000000..afa3426f00 --- /dev/null +++ b/backend_new/test/unit/utilities/pagination-helpers.spec.ts @@ -0,0 +1,40 @@ +import { + shouldPaginate, + calculateSkip, + calculateTake, +} from '../../../src/utilities/pagination-helpers'; +describe('Testing pagination helpers', () => { + describe('Testing shouldPaginate', () => { + it("should return false for limit of 'all'", () => { + expect(shouldPaginate('all', 0)).toBeFalsy(); + }); + it('should return false for limit of 0', () => { + expect(shouldPaginate(0, 1)).toBeFalsy(); + }); + it('should return false for page of 0', () => { + expect(shouldPaginate(1, 0)).toBeFalsy(); + }); + it('should return true limit > 0, page > 0', () => { + expect(shouldPaginate(1, 1)).toBeTruthy(); + }); + }); + describe('Testing calculateSkip', () => { + it("should return 0 for limit of 'all'", () => { + expect(calculateSkip('all', 0)).toBe(0); + }); + it('should return 0 for page 1', () => { + expect(calculateSkip(1, 1)).toBe(0); + }); + it('should return 10 when on page 2 with limit 10', () => { + expect(calculateSkip(10, 2)).toBe(10); + }); + }); + describe('Testing calculateTake', () => { + it("should return undefined for limit of 'all'", () => { + expect(calculateTake('all')).toBe(undefined); + }); + it('should return limit for numeric limit', () => { + expect(calculateTake(1)).toBe(1); + }); + }); +}); diff --git a/backend_new/test/unit/utilities/unit-utilities.spec.ts b/backend_new/test/unit/utilities/unit-utilities.spec.ts new file mode 100644 index 0000000000..618d0d0735 --- /dev/null +++ b/backend_new/test/unit/utilities/unit-utilities.spec.ts @@ -0,0 +1,242 @@ +import { AmiChart } from '../../../src/dtos/units/ami-chart-get.dto'; +import { UnitAmiChartOverride } from '../../../src/dtos/units/ami-chart-override-get.dto'; +import { + generateHmiData, + mergeAmiChartWithOverrides, +} from '../../../src/utilities/unit-utilities'; +import { Unit } from '../../../src/dtos/units/unit-get.dto'; +import { AmiChartItem } from '../../../src/dtos/units/ami-chart-item-get.dto'; + +const defaultValues = { + createdAt: new Date(), + updatedAt: new Date(), +}; + +const unit: Unit = { + ...defaultValues, + amiPercentage: '30', + maxOccupancy: 2, + amiChart: { + id: 'ami1', + createdAt: new Date(), + updatedAt: new Date(), + items: [], + name: 'ami1', + }, + id: 'example', +}; + +const generateAmiChartItems = ( + maxHousehold: number, + percentage: number, + baseAmount: number, +): AmiChartItem[] => { + return [...Array(maxHousehold).keys()].map((value: number) => { + return { + percentOfAmi: percentage, + householdSize: value + 1, + income: baseAmount + 1000 * value, + }; + }); +}; + +const generateAmiChart = (): AmiChart => { + return { + ...defaultValues, + id: 'ami1', + name: 'ami1', + items: generateAmiChartItems(8, 30, 30_000), + }; +}; + +describe('Unit Transformations', () => { + describe('mergeAmiChartWithOverrides', () => { + it('Ami chart items are correctly overwritten', () => { + const amiChartOverride: UnitAmiChartOverride = { + id: 'id', + createdAt: new Date(), + updatedAt: new Date(), + items: [ + { + percentOfAmi: 30, + householdSize: 2, + income: 20, + }, + ], + }; + const result = mergeAmiChartWithOverrides( + generateAmiChart(), + amiChartOverride, + ); + expect(result.items.length).toBe(8); + expect(result.items[0].income).toBe(30000); + expect(result.items[1].income).toBe(20); + expect(result.items[2].income).toBe(32000); + }); + }); + + describe('generateHmiData', () => { + it('should generate HMI data for one unit and amiChart', () => { + const result = generateHmiData( + [unit], + [{ min: 2, max: 5 }], + [generateAmiChart()], + ); + + expect(result).toEqual({ + columns: { + maxIncomeMonth: 'listings.maxIncomeMonth', + maxIncomeYear: 'listings.maxIncomeYear', + sizeColumn: 'listings.householdSize', + }, + rows: [ + { + maxIncomeMonth: 'listings.monthlyIncome*income:$2,583', + maxIncomeYear: 'listings.annualIncome*income:$31,000', + sizeColumn: 2, + }, + { + maxIncomeMonth: 'listings.monthlyIncome*income:$2,667', + maxIncomeYear: 'listings.annualIncome*income:$32,000', + sizeColumn: 3, + }, + { + maxIncomeMonth: 'listings.monthlyIncome*income:$2,750', + maxIncomeYear: 'listings.annualIncome*income:$33,000', + sizeColumn: 4, + }, + { + maxIncomeMonth: 'listings.monthlyIncome*income:$2,833', + maxIncomeYear: 'listings.annualIncome*income:$34,000', + sizeColumn: 5, + }, + ], + }); + }); + it('should only have the highest chart data if min household is larger', () => { + const result = generateHmiData( + [unit], + [{ min: 9, max: 11 }], + [generateAmiChart()], + ); + + expect(result).toEqual({ + columns: { + maxIncomeMonth: 'listings.maxIncomeMonth', + maxIncomeYear: 'listings.maxIncomeYear', + sizeColumn: 'listings.householdSize', + }, + rows: [ + { + maxIncomeMonth: 'listings.monthlyIncome*income:$3,083', + maxIncomeYear: 'listings.annualIncome*income:$37,000', + sizeColumn: 8, + }, + ], + }); + }); + it('should generate for more than 1 unit and amiChart ', () => { + const result = generateHmiData( + [ + unit, + { + ...unit, + amiPercentage: '40', + amiChart: { + id: 'ami2', + createdAt: new Date(), + updatedAt: new Date(), + items: [], + name: 'ami2', + }, + }, + ], + [ + { min: 1, max: 3 }, + { min: 5, max: 7 }, + ], + [ + generateAmiChart(), + { + ...generateAmiChart(), + id: 'ami2', + items: generateAmiChartItems(8, 40, 40_000), + }, + ], + ); + + expect(result).toEqual({ + columns: { + ami30: 'listings.percentAMIUnit*percent:30', + ami40: 'listings.percentAMIUnit*percent:40', + sizeColumn: 'listings.householdSize', + }, + rows: [ + { + ami30: 'listings.annualIncome*income:$30,000', + ami40: 'listings.annualIncome*income:$40,000', + sizeColumn: 1, + }, + { + ami30: 'listings.annualIncome*income:$31,000', + ami40: 'listings.annualIncome*income:$41,000', + sizeColumn: 2, + }, + { + ami30: 'listings.annualIncome*income:$32,000', + ami40: 'listings.annualIncome*income:$42,000', + sizeColumn: 3, + }, + { + ami30: 'listings.annualIncome*income:$34,000', + ami40: 'listings.annualIncome*income:$44,000', + sizeColumn: 5, + }, + { + ami30: 'listings.annualIncome*income:$35,000', + ami40: 'listings.annualIncome*income:$45,000', + sizeColumn: 6, + }, + { + ami30: 'listings.annualIncome*income:$36,000', + ami40: 'listings.annualIncome*income:$46,000', + sizeColumn: 7, + }, + ], + }); + }); + it('should have bmr values', () => { + const result = generateHmiData( + [ + { + ...unit, + bmrProgramChart: true, + unitTypes: { + ...defaultValues, + id: 'oneBed', + name: 'oneBdrm', + numBedrooms: 1, + }, + }, + ], + [{ min: 1, max: 3 }], + [generateAmiChart()], + ); + + expect(result).toEqual({ + columns: { + maxIncomeMonth: 'listings.maxIncomeMonth', + maxIncomeYear: 'listings.maxIncomeYear', + sizeColumn: 't.unitType', + }, + rows: [ + { + maxIncomeMonth: 'listings.monthlyIncome*income:$2,583', + maxIncomeYear: 'listings.annualIncome*income:$31,000', + sizeColumn: 'listings.unitTypes.oneBdrm', + }, + ], + }); + }); + }); +}); diff --git a/backend_new/types/src/backend-swagger.ts b/backend_new/types/src/backend-swagger.ts new file mode 100644 index 0000000000..d152f57483 --- /dev/null +++ b/backend_new/types/src/backend-swagger.ts @@ -0,0 +1,413 @@ +/** Generate by swagger-axios-codegen */ +// @ts-nocheck +/* eslint-disable */ + +/** Generate by swagger-axios-codegen */ +/* eslint-disable */ +// @ts-nocheck +import axiosStatic, { AxiosInstance, AxiosRequestConfig } from 'axios'; + +export interface IRequestOptions extends AxiosRequestConfig { + /** only in axios interceptor config*/ + loading?: boolean; + showError?: boolean; +} + +export interface IRequestConfig { + method?: any; + headers?: any; + url?: any; + data?: any; + params?: any; +} + +// Add options interface +export interface ServiceOptions { + axios?: AxiosInstance; + /** only in axios interceptor config*/ + loading: boolean; + showError: boolean; +} + +// Add default options +export const serviceOptions: ServiceOptions = {}; + +// Instance selector +export function axios( + configs: IRequestConfig, + resolve: (p: any) => void, + reject: (p: any) => void, +): Promise { + if (serviceOptions.axios) { + return serviceOptions.axios + .request(configs) + .then((res) => { + resolve(res.data); + }) + .catch((err) => { + reject(err); + }); + } else { + throw new Error('please inject yourself instance like axios '); + } +} + +export function getConfigs( + method: string, + contentType: string, + url: string, + options: any, +): IRequestConfig { + const configs: IRequestConfig = { + loading: serviceOptions.loading, + showError: serviceOptions.showError, + ...options, + method, + url, + }; + configs.headers = { + ...options.headers, + 'Content-Type': contentType, + }; + return configs; +} + +export const basePath = ''; + +export interface IList extends Array {} +export interface List extends Array {} +export interface IDictionary { + [key: string]: TValue; +} +export interface Dictionary extends IDictionary {} + +export interface IListResult { + items?: T[]; +} + +export class ListResult implements IListResult { + items?: T[]; +} + +export interface IPagedResult extends IListResult { + totalCount?: number; + items?: T[]; +} + +export class PagedResult implements IPagedResult { + totalCount?: number; + items?: T[]; +} + +// customer definition +// empty + +export class ListingsService { + /** + * Get a paginated set of listings + */ + list( + params: { + /** */ + page?: number; + /** */ + limit?: number | 'all'; + /** */ + filter?: ListingFilterParams[]; + /** */ + view?: ListingViews; + /** */ + orderBy?: any | null[]; + /** */ + orderDir?: any | null[]; + /** */ + search?: string; + } = {} as any, + options: IRequestOptions = {}, + ): Promise { + return new Promise((resolve, reject) => { + let url = basePath + '/listings'; + + const configs: IRequestConfig = getConfigs( + 'get', + 'application/json', + url, + options, + ); + configs.params = { + page: params['page'], + limit: params['limit'], + filter: params['filter'], + view: params['view'], + orderBy: params['orderBy'], + orderDir: params['orderDir'], + search: params['search'], + }; + + /** 适配ios13getèŻ·æ±‚äžć…èźžćžŠbody */ + + axios(configs, resolve, reject); + }); + } + /** + * Get listing by id + */ + retrieve( + params: { + /** */ + id: string; + /** */ + view?: ListingViews; + } = {} as any, + options: IRequestOptions = {}, + ): Promise { + return new Promise((resolve, reject) => { + let url = basePath + '/listings/{id}'; + url = url.replace('{id}', params['id'] + ''); + + const configs: IRequestConfig = getConfigs( + 'get', + 'application/json', + url, + options, + ); + configs.params = { view: params['view'] }; + + /** 适配ios13getèŻ·æ±‚äžć…èźžćžŠbody */ + + axios(configs, resolve, reject); + }); + } +} + +export interface ListingsQueryParams { + /** */ + page?: number; + + /** */ + limit?: number | 'all'; + + /** */ + filter?: string[]; + + /** */ + view?: ListingViews; + + /** */ + orderBy?: []; + + /** */ + orderDir?: EnumListingsQueryParamsOrderDir[]; + + /** */ + search?: string; +} + +export interface ListingFilterParams { + /** */ + $comparison: EnumListingFilterParamsComparison; + + /** */ + name?: string; + + /** */ + status?: EnumListingFilterParamsStatus; + + /** */ + neighborhood?: string; + + /** */ + bedrooms?: number; + + /** */ + zipcode?: string; + + /** */ + leasingAgents?: string; + + /** */ + jurisdiction?: string; +} + +export interface ListingsRetrieveParams { + /** */ + view?: ListingViews; +} + +export interface PaginationAllowsAllQueryParams { + /** */ + page?: number; + + /** */ + limit?: number | 'all'; +} + +export interface ApplicationMethod { + /** */ + type: ApplicationMethodsTypeEnum; +} + +export interface UnitType {} + +export interface UnitAccessibilityPriorityType {} + +export interface MinMaxCurrency { + /** */ + min: string; + + /** */ + max: string; +} + +export interface MinMax { + /** */ + min: number; + + /** */ + max: number; +} + +export interface UnitSummary { + /** */ + unitTypes: UnitType; + + /** */ + minIncomeRange: MinMaxCurrency; + + /** */ + occupancyRange: MinMax; + + /** */ + rentAsPercentIncomeRange: MinMax; + + /** */ + rentRange: MinMaxCurrency; + + /** */ + totalAvailable: number; + + /** */ + areaRange: MinMax; + + /** */ + floorRange?: MinMax; +} + +export interface UnitSummaryByAMI { + /** */ + percent: string; + + /** */ + byUnitType: UnitSummary[]; +} + +export interface HMI { + /** */ + columns: object; + + /** */ + rows: object[]; +} + +export interface UnitsSummarized { + /** */ + unitTypes: UnitType[]; + + /** */ + priorityTypes: UnitAccessibilityPriorityType[]; + + /** */ + amiPercentages: string[]; + + /** */ + byUnitTypeAndRent: UnitSummary[]; + + /** */ + byUnitType: UnitSummary[]; + + /** */ + byAMI: UnitSummaryByAMI[]; + + /** */ + hmi: HMI; +} + +export interface ListingGet { + /** */ + applicationPickUpAddressType: ApplicationAddressTypeEnum; + + /** */ + applicationDropOffAddressType: ApplicationAddressTypeEnum; + + /** */ + applicationMailingAddressType: ApplicationAddressTypeEnum; + + /** */ + status: ListingsStatusEnum; + + /** */ + reviewOrderType: ReviewOrderTypeEnum; + + /** */ + showWaitlist: boolean; + + /** */ + referralApplication?: ApplicationMethod; + + /** */ + unitsSummarized: UnitsSummarized; +} + +export interface PaginatedListing { + /** */ + items: ListingGet[]; +} + +export enum ListingViews { + 'fundamentals' = 'fundamentals', + 'base' = 'base', + 'full' = 'full', + 'details' = 'details', +} +export enum EnumListingsQueryParamsOrderDir { + 'asc' = 'asc', + 'desc' = 'desc', +} +export enum EnumListingFilterParamsComparison { + '=' = '=', + '<>' = '<>', + 'IN' = 'IN', + '>=' = '>=', + '<=' = '<=', + 'NA' = 'NA', +} +export enum EnumListingFilterParamsStatus { + 'active' = 'active', + 'pending' = 'pending', + 'closed' = 'closed', +} +export enum ApplicationAddressTypeEnum { + 'leasingAgent' = 'leasingAgent', +} + +export enum ListingsStatusEnum { + 'active' = 'active', + 'pending' = 'pending', + 'closed' = 'closed', +} + +export enum ReviewOrderTypeEnum { + 'lottery' = 'lottery', + 'firstComeFirstServe' = 'firstComeFirstServe', + 'waitlist' = 'waitlist', +} + +export enum ApplicationMethodsTypeEnum { + 'Internal' = 'Internal', + 'FileDownload' = 'FileDownload', + 'ExternalLink' = 'ExternalLink', + 'PaperPickup' = 'PaperPickup', + 'POBox' = 'POBox', + 'LeasingAgent' = 'LeasingAgent', + 'Referral' = 'Referral', +} diff --git a/backend_new/yarn.lock b/backend_new/yarn.lock index d25731d6b2..1d503955a5 100644 --- a/backend_new/yarn.lock +++ b/backend_new/yarn.lock @@ -707,6 +707,11 @@ tslib "2.4.0" uuid "8.3.2" +"@nestjs/mapped-types@1.2.2": + version "1.2.2" + resolved "https://registry.yarnpkg.com/@nestjs/mapped-types/-/mapped-types-1.2.2.tgz#d9ddb143776e309dbc1a518ac1607fddac1e140e" + integrity sha512-3dHxLXs3M0GPiriAcCFFJQHoDFUuzTD5w6JDhE7TyfT89YKpe6tcCCIqOZWdXmt9AZjjK30RkHRSFF+QEnWFQg== + "@nestjs/platform-express@^8.0.0": version "8.4.7" resolved "https://registry.yarnpkg.com/@nestjs/platform-express/-/platform-express-8.4.7.tgz#402a3d3c47327a164bb3867615f423c29d1a6cd9" @@ -729,6 +734,17 @@ jsonc-parser "3.0.0" pluralize "8.0.0" +"@nestjs/swagger@^6.3.0": + version "6.3.0" + resolved "https://registry.yarnpkg.com/@nestjs/swagger/-/swagger-6.3.0.tgz#2963395a398374c25548a012eb15f03f53ad6e53" + integrity sha512-Gnig189oa1tD+h0BYIfUwhp/wvvmTn6iO3csR2E4rQrDTgCxSxZDlNdfZo3AC+Rmf8u0KX4ZAX1RZN1qXTtC7A== + dependencies: + "@nestjs/mapped-types" "1.2.2" + js-yaml "4.1.0" + lodash "4.17.21" + path-to-regexp "3.2.0" + swagger-ui-dist "4.18.2" + "@nestjs/testing@^8.0.0": version "8.4.7" resolved "https://registry.yarnpkg.com/@nestjs/testing/-/testing-8.4.7.tgz#fe4f356c0e081e25fe8c899a65e91dd88947fd13" @@ -766,17 +782,17 @@ consola "^2.15.0" node-fetch "^2.6.1" -"@prisma/client@4.13.0": - version "4.13.0" - resolved "https://registry.yarnpkg.com/@prisma/client/-/client-4.13.0.tgz#271d2b9756503ea17bbdb459c7995536cf2a6191" - integrity sha512-YaiiICcRB2hatxsbnfB66uWXjcRw3jsZdlAVxmx0cFcTc/Ad/sKdHCcWSnqyDX47vAewkjRFwiLwrOUjswVvmA== +"@prisma/client@^4.14.0": + version "4.14.0" + resolved "https://registry.yarnpkg.com/@prisma/client/-/client-4.14.0.tgz#715b3dd045d094b03cb0a7f2991f088d15ae553e" + integrity sha512-MK/XaA2sFdfaOa7I9MjNKz6dxeIEdeZlnpNRoF2w3JuRLlFJLkpp6cD3yaqw2nUUhbrn3Iqe3ZpVV+VuGGil7Q== dependencies: - "@prisma/engines-version" "4.13.0-50.1e7af066ee9cb95cf3a403c78d9aab3e6b04f37a" + "@prisma/engines-version" "4.14.0-67.d9a4c5988f480fa576d43970d5a23641aa77bc9c" -"@prisma/engines-version@4.13.0-50.1e7af066ee9cb95cf3a403c78d9aab3e6b04f37a": - version "4.13.0-50.1e7af066ee9cb95cf3a403c78d9aab3e6b04f37a" - resolved "https://registry.yarnpkg.com/@prisma/engines-version/-/engines-version-4.13.0-50.1e7af066ee9cb95cf3a403c78d9aab3e6b04f37a.tgz#ae338908d11685dee50e7683502d75442b955bf9" - integrity sha512-fsQlbkhPJf08JOzKoyoD9atdUijuGBekwoOPZC3YOygXEml1MTtgXVpnUNchQlRSY82OQ6pSGQ9PxUe4arcSLQ== +"@prisma/engines-version@4.14.0-67.d9a4c5988f480fa576d43970d5a23641aa77bc9c": + version "4.14.0-67.d9a4c5988f480fa576d43970d5a23641aa77bc9c" + resolved "https://registry.yarnpkg.com/@prisma/engines-version/-/engines-version-4.14.0-67.d9a4c5988f480fa576d43970d5a23641aa77bc9c.tgz#0aeca447c4a5f23c83f68b8033e627b60bc01850" + integrity sha512-3jum8/YSudeSN0zGW5qkpz+wAN2V/NYCQ+BPjvHYDfWatLWlQkqy99toX0GysDeaUoBIJg1vaz2yKqiA3CFcQw== "@prisma/engines@4.13.0": version "4.13.0" @@ -975,6 +991,11 @@ resolved "https://registry.yarnpkg.com/@types/mime/-/mime-1.3.2.tgz#93e25bf9ee75fe0fd80b594bc4feb0e862111b5a" integrity sha512-YATxVxgRqNH6nHEIsvg6k2Boc1JHI9ZbH5iWFFv/MTkchz3b1ieGDa5T0a9RznNdI0KhVbdbWSN+KWWrQZRxTw== +"@types/minimatch@^3.0.3": + version "3.0.5" + resolved "https://registry.yarnpkg.com/@types/minimatch/-/minimatch-3.0.5.tgz#1001cc5e6a3704b83c236027e77f2f58ea010f40" + integrity sha512-Klz949h02Gz2uZCMGwDUSDS1YBlTdDDgbWHi+81l29tQALUtvz4rAYi5uoVhE5Lagoq6DeqAUlbrHvW/mXDgdQ== + "@types/node@*": version "20.1.0" resolved "https://registry.yarnpkg.com/@types/node/-/node-20.1.0.tgz#258805edc37c327cf706e64c6957f241ca4c4c20" @@ -1046,6 +1067,11 @@ dependencies: "@types/superagent" "*" +"@types/validator@^13.7.10": + version "13.7.17" + resolved "https://registry.yarnpkg.com/@types/validator/-/validator-13.7.17.tgz#0a6d1510395065171e3378a4afc587a3aefa7cc1" + integrity sha512-aqayTNmeWrZcvnG2MG9eGYI6b7S5fl+yKgPs6bAjOTwPS316R5SxBGKvtSExfyoJU7pIeHJfsHI0Ji41RVMkvQ== + "@types/yargs-parser@*": version "21.0.0" resolved "https://registry.yarnpkg.com/@types/yargs-parser/-/yargs-parser-21.0.0.tgz#0c60e537fa790f5f9472ed2776c2b71ec117351b" @@ -1439,6 +1465,11 @@ argparse@^2.0.1: resolved "https://registry.yarnpkg.com/argparse/-/argparse-2.0.1.tgz#246f50f3ca78a3240f6c997e8a9bd1eac49e4b38" integrity sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q== +array-differ@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/array-differ/-/array-differ-3.0.0.tgz#3cbb3d0f316810eafcc47624734237d6aee4ae6b" + integrity sha512-THtfYS6KtME/yIAhKjZ2ul7XI96lQGHRputJQHO80LAWQnuGP4iCIN8vdMRboGbIEYBwU33q8Tch1os2+X0kMg== + array-flatten@1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/array-flatten/-/array-flatten-1.1.1.tgz#9a5f699051b1e7073328f2a008968b64ea2955d2" @@ -1449,6 +1480,11 @@ array-union@^2.1.0: resolved "https://registry.yarnpkg.com/array-union/-/array-union-2.1.0.tgz#b798420adbeb1de828d84acd8a2e23d3efe85e8d" integrity sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw== +arrify@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/arrify/-/arrify-2.0.1.tgz#c9655e9331e0abcd588d2a7cad7e9956f66701fa" + integrity sha512-3duEwti880xqi4eAMN8AyR4a0ByT90zoYdLlevfrvU43vb0YZwZVfxOgxWrLXXXpyugL0hNZc9G6BiB5B3nUug== + asap@^2.0.0: version "2.0.6" resolved "https://registry.yarnpkg.com/asap/-/asap-2.0.6.tgz#e50347611d7e690943208bbdafebcbc2fb866d46" @@ -1467,6 +1503,15 @@ axios@0.27.2: follow-redirects "^1.14.9" form-data "^4.0.0" +axios@^1.2.2: + version "1.4.0" + resolved "https://registry.yarnpkg.com/axios/-/axios-1.4.0.tgz#38a7bf1224cd308de271146038b551d725f0be1f" + integrity sha512-S4XCWMEmzvo64T9GfvQDOXgYRDJ/wsSZc7Jvdgx5u1sd0JwsuPLqb3SYmusag+edF6ziyMensPVqLTSc1PiSEA== + dependencies: + follow-redirects "^1.15.0" + form-data "^4.0.0" + proxy-from-env "^1.1.0" + babel-jest@^27.5.1: version "27.5.1" resolved "https://registry.yarnpkg.com/babel-jest/-/babel-jest-27.5.1.tgz#a1bf8d61928edfefd21da27eb86a695bfd691444" @@ -1652,7 +1697,7 @@ callsites@^3.0.0: resolved "https://registry.yarnpkg.com/callsites/-/callsites-3.1.0.tgz#b3630abd8943432f54b3f0519238e33cd7df2f73" integrity sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ== -camelcase@^5.3.1: +camelcase@^5.0.0, camelcase@^5.3.1: version "5.3.1" resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-5.3.1.tgz#e3c9b31569e106811df242f715725a1f4c494320" integrity sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg== @@ -1732,6 +1777,20 @@ cjs-module-lexer@^1.0.0: resolved "https://registry.yarnpkg.com/cjs-module-lexer/-/cjs-module-lexer-1.2.2.tgz#9f84ba3244a512f3a54e5277e8eef4c489864e40" integrity sha512-cOU9usZw8/dXIXKtwa8pM0OTJQuJkxMN6w30csNRUerHfeQ5R6U3kkU/FtJeIf3M202OHfY2U8ccInBG7/xogA== +class-transformer@^0.5.1: + version "0.5.1" + resolved "https://registry.yarnpkg.com/class-transformer/-/class-transformer-0.5.1.tgz#24147d5dffd2a6cea930a3250a677addf96ab336" + integrity sha512-SQa1Ws6hUbfC98vKGxZH3KFY0Y1lm5Zm0SY8XX9zbK7FJCyVEac3ATW0RIpwzW+oOfmHE5PMPufDG9hCfoEOMw== + +class-validator@^0.14.0: + version "0.14.0" + resolved "https://registry.yarnpkg.com/class-validator/-/class-validator-0.14.0.tgz#40ed0ecf3c83b2a8a6a320f4edb607be0f0df159" + integrity sha512-ct3ltplN8I9fOwUd8GrP8UQixwff129BkEtuWDKL5W45cQuLd19xqmTLu5ge78YDm/fdje6FMt0hGOhl0lii3A== + dependencies: + "@types/validator" "^13.7.10" + libphonenumber-js "^1.10.14" + validator "^13.7.0" + cli-cursor@^3.1.0: version "3.1.0" resolved "https://registry.yarnpkg.com/cli-cursor/-/cli-cursor-3.1.0.tgz#264305a7ae490d1d03bf0c9ba7c925d1753af307" @@ -2469,7 +2528,7 @@ flatted@^3.1.0: resolved "https://registry.yarnpkg.com/flatted/-/flatted-3.2.7.tgz#609f39207cb614b89d0765b477cb2d437fbf9787" integrity sha512-5nqDSxl8nn5BSNxyR3n4I6eDmbolI6WT+QqR547RwxQapgjQBmtktdP+HTBb/a/zLsbzERTONyUB5pefh5TtjQ== -follow-redirects@^1.14.9: +follow-redirects@^1.14.9, follow-redirects@^1.15.0: version "1.15.2" resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.2.tgz#b460864144ba63f2681096f274c4e57026da2c13" integrity sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA== @@ -3079,7 +3138,7 @@ jest-each@^27.5.1: jest-util "^27.5.1" pretty-format "^27.5.1" -jest-environment-jsdom@^27.5.1: +jest-environment-jsdom@^27.2.5, jest-environment-jsdom@^27.5.1: version "27.5.1" resolved "https://registry.yarnpkg.com/jest-environment-jsdom/-/jest-environment-jsdom-27.5.1.tgz#ea9ccd1fc610209655a77898f86b2b559516a546" integrity sha512-TFBvkTC1Hnnnrka/fUb56atfDtJ9VMZ94JkjTbggl1PEpwrYtUBKMezB3inLmWqQsXYLcMwNoDQwoBTAvFfsfw== @@ -3384,6 +3443,13 @@ js-tokens@^4.0.0: resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499" integrity sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ== +js-yaml@4.1.0, js-yaml@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-4.1.0.tgz#c1fb65f8f5017901cdd2c951864ba18458a10602" + integrity sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA== + dependencies: + argparse "^2.0.1" + js-yaml@^3.13.1: version "3.14.1" resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.14.1.tgz#dae812fdb3825fa306609a8717383c50c36a0537" @@ -3392,13 +3458,6 @@ js-yaml@^3.13.1: argparse "^1.0.7" esprima "^4.0.0" -js-yaml@^4.1.0: - version "4.1.0" - resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-4.1.0.tgz#c1fb65f8f5017901cdd2c951864ba18458a10602" - integrity sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA== - dependencies: - argparse "^2.0.1" - jsdom@^16.6.0: version "16.7.0" resolved "https://registry.yarnpkg.com/jsdom/-/jsdom-16.7.0.tgz#918ae71965424b197c819f8183a754e18977b710" @@ -3509,6 +3568,11 @@ levn@~0.3.0: prelude-ls "~1.1.2" type-check "~0.3.2" +libphonenumber-js@^1.10.14: + version "1.10.31" + resolved "https://registry.yarnpkg.com/libphonenumber-js/-/libphonenumber-js-1.10.31.tgz#aab580894c263093a3085a02afcda7a742faeff1" + integrity sha512-qYTzElLePmz3X/6I0JPX5n87tu7jVIMtR/yRLi5PGVPvMCMSVTCR+079KmdNK005i4dBjFxY/bMYceI9IBp47w== + lines-and-columns@^1.1.6: version "1.2.4" resolved "https://registry.yarnpkg.com/lines-and-columns/-/lines-and-columns-1.2.4.tgz#eca284f75d2965079309dc0ad9255abb2ebc1632" @@ -3543,7 +3607,7 @@ lodash.merge@^4.6.2: resolved "https://registry.yarnpkg.com/lodash.merge/-/lodash.merge-4.6.2.tgz#558aa53b43b661e1925a0afdfa36a9a1085fe57a" integrity sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ== -lodash@^4.17.19, lodash@^4.17.21, lodash@^4.7.0: +lodash@4.17.21, lodash@^4.17.19, lodash@^4.17.21, lodash@^4.7.0: version "4.17.21" resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c" integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg== @@ -3720,6 +3784,17 @@ multer@1.4.4-lts.1: type-is "^1.6.4" xtend "^4.0.0" +multimatch@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/multimatch/-/multimatch-4.0.0.tgz#8c3c0f6e3e8449ada0af3dd29efb491a375191b3" + integrity sha512-lDmx79y1z6i7RNx0ZGCPq1bzJ6ZoDDKbvh7jxr9SJcWLkShMzXrHbYVpTdnhNM5MXpDUxCQ4DgqVttVXlBgiBQ== + dependencies: + "@types/minimatch" "^3.0.3" + array-differ "^3.0.0" + array-union "^2.1.0" + arrify "^2.0.1" + minimatch "^3.0.4" + mute-stream@0.0.8: version "0.0.8" resolved "https://registry.yarnpkg.com/mute-stream/-/mute-stream-0.0.8.tgz#1630c42b2251ff81e2a283de96a5497ea92e5e0d" @@ -3934,6 +4009,11 @@ parseurl@~1.3.3: resolved "https://registry.yarnpkg.com/parseurl/-/parseurl-1.3.3.tgz#9da19e7bee8d12dff0513ed5b76957793bc2e8d4" integrity sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ== +pascalcase@^0.1.1: + version "0.1.1" + resolved "https://registry.yarnpkg.com/pascalcase/-/pascalcase-0.1.1.tgz#b363e55e8006ca6fe21784d2db22bd15d7917f14" + integrity sha512-XHXfu/yOQRy9vYOtUDVMN60OEJjW013GoObG1o+xwQTpB9eYJX/BjXMsdW13ZDPruFhYYn0AG22w0xgQMwl3Nw== + path-exists@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-4.0.0.tgz#513bdbe2d3b95d7762e8c1137efa195c6c61b5b3" @@ -4013,6 +4093,11 @@ prettier-linter-helpers@^1.0.0: dependencies: fast-diff "^1.1.2" +prettier@^1.15.2: + version "1.19.1" + resolved "https://registry.yarnpkg.com/prettier/-/prettier-1.19.1.tgz#f7d7f5ff8a9cd872a7be4ca142095956a60797cb" + integrity sha512-s7PoyDv/II1ObgQunCbB9PdLmUcBZcnWOcxDh7O0N/UwDEsHyqkW+Qh28jW+mVuCdx7gLB0BotYI1Y6uI9iyew== + prettier@^2.3.2: version "2.8.8" resolved "https://registry.yarnpkg.com/prettier/-/prettier-2.8.8.tgz#e8c5d7e98a4305ffe3de2e1fc4aca1a71c28b1da" @@ -4055,6 +4140,11 @@ proxy-addr@~2.0.7: forwarded "0.2.0" ipaddr.js "1.9.1" +proxy-from-env@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/proxy-from-env/-/proxy-from-env-1.1.0.tgz#e102f16ca355424865755d2c9e8ea4f24d58c3e2" + integrity sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg== + psl@^1.1.33: version "1.9.0" resolved "https://registry.yarnpkg.com/psl/-/psl-1.9.0.tgz#d0df2a137f00794565fcaf3b2c00cd09f8d5a5a7" @@ -4087,6 +4177,13 @@ qs@^6.11.0: dependencies: side-channel "^1.0.4" +qs@^6.11.2: + version "6.11.2" + resolved "https://registry.yarnpkg.com/qs/-/qs-6.11.2.tgz#64bea51f12c1f5da1bc01496f48ffcff7c69d7d9" + integrity sha512-tDNIz22aBzCDxLtVH++VnTfzxlfeK5CbqohpSqpJgj1Wg/cQbStNAz3NuqCs5vV+pjBsK4x4pN9HlVh7rcYRiA== + dependencies: + side-channel "^1.0.4" + querystringify@^2.1.1: version "2.2.0" resolved "https://registry.yarnpkg.com/querystringify/-/querystringify-2.2.0.tgz#3345941b4153cb9d082d8eee4cda2016a9aef7f6" @@ -4494,6 +4591,11 @@ strip-json-comments@^3.1.0, strip-json-comments@^3.1.1: resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-3.1.1.tgz#31f1281b3832630434831c310c01cccda8cbe006" integrity sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig== +structured-log@^0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/structured-log/-/structured-log-0.2.0.tgz#b9be1794c39d6399f265666b84635a3307611c5b" + integrity sha512-W3Tps8PN5Mon37955/wuZZSXwBXiB52AUnd/oPVdmo+O+mLkr2fNajV6821gJ8irrgVQx3gYOqSWPMgj7Dy3Yg== + superagent@^8.0.5: version "8.0.9" resolved "https://registry.yarnpkg.com/superagent/-/superagent-8.0.9.tgz#2c6fda6fadb40516515f93e9098c0eb1602e0535" @@ -4552,6 +4654,23 @@ supports-preserve-symlinks-flag@^1.0.0: resolved "https://registry.yarnpkg.com/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz#6eda4bd344a3c94aea376d4cc31bc77311039e09" integrity sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w== +swagger-axios-codegen@^0.15.11: + version "0.15.11" + resolved "https://registry.yarnpkg.com/swagger-axios-codegen/-/swagger-axios-codegen-0.15.11.tgz#10c4c2314e454a5e28237a2036cd210142466ea0" + integrity sha512-XRDRYLmlydVyFjyf31Vsla40gH/yHg/Gn7nBeH6IKVIsTzqUskRN9ZusDMJPYAipKt+wuF+CduubHqn5RGyR4Q== + dependencies: + axios "^1.2.2" + camelcase "^5.0.0" + multimatch "^4.0.0" + pascalcase "^0.1.1" + prettier "^1.15.2" + structured-log "^0.2.0" + +swagger-ui-dist@4.18.2: + version "4.18.2" + resolved "https://registry.yarnpkg.com/swagger-ui-dist/-/swagger-ui-dist-4.18.2.tgz#323308f1c1d87a7c22ce3e273c31835eb680a71b" + integrity sha512-oVBoBl9Dg+VJw8uRWDxlyUyHoNEDC0c1ysT6+Boy6CTgr2rUcLcfPon4RvxgS2/taNW6O0+US+Z/dlAsWFjOAQ== + symbol-observable@4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/symbol-observable/-/symbol-observable-4.0.0.tgz#5b425f192279e87f2f9b937ac8540d1984b39205" @@ -4896,6 +5015,11 @@ v8-to-istanbul@^8.1.0: convert-source-map "^1.6.0" source-map "^0.7.3" +validator@^13.7.0: + version "13.9.0" + resolved "https://registry.yarnpkg.com/validator/-/validator-13.9.0.tgz#33e7b85b604f3bbce9bb1a05d5c3e22e1c2ff855" + integrity sha512-B+dGG8U3fdtM0/aNK4/X8CXq/EcxU2WPrPEkJGslb47qyHsxmbggTWK0yEA4qnYVNF+nxNlN88o14hIcPmSIEA== + vary@^1, vary@~1.1.2: version "1.1.2" resolved "https://registry.yarnpkg.com/vary/-/vary-1.1.2.tgz#2299f02c6ded30d4a5961b0b9f74524a18f634fc" diff --git a/package.json b/package.json index 1467e8f46b..f98318d5e1 100644 --- a/package.json +++ b/package.json @@ -48,8 +48,13 @@ "test:e2e:backend:core": "cd backend/core && yarn test:e2e", "test:apps": "concurrently \"yarn dev:backend\" \"yarn test:app:public\"", "test:apps:headless": "concurrently \"yarn dev:backend\" \"yarn test:app:public:headless\"", - "lint": "eslint '**/*.ts' '**/*.tsx' '**/*.js'", - "version:all": "lerna version --yes --no-commit-hooks --ignore-scripts --conventional-graduate --include-merged-tags --force-git-tag" + "lint": "eslint '**/*.ts' '**/*.tsx' '**/*.js' && cd backend_new && yarn lint", + "version:all": "lerna version --yes --no-commit-hooks --ignore-scripts --conventional-graduate --include-merged-tags --force-git-tag", + "test:backend:new": "cd backend_new && yarn test", + "test:backend:new:e2e": "cd backend_new && yarn jest --config ./test/jest-e2e.config.js", + "test:backend:new:dbsetup": "cd backend_new && yarn db:migration:run && yarn db:seed", + "backend:new:install": "cd backend_new && yarn install", + "prettier": "prettier --write \"./**/*.{js,jsx,ts,tsx,json}\"" }, "devDependencies": { "@commitlint/cli": "^13.1.0", @@ -60,18 +65,18 @@ "commitizen": "^4.2.4", "concurrently": "^5.3.0", "cz-conventional-changelog": "^3.3.0", - "eslint": "^7.11.0", - "eslint-config-prettier": "^6.11.0", - "eslint-plugin-import": "^2.22.1", + "eslint": "^8.0.1", + "eslint-config-prettier": "^8.3.0", + "eslint-plugin-import": "^2.27.5", "eslint-plugin-jsx-a11y": "6.5.1", - "eslint-plugin-prettier": "^3.1.4", - "eslint-plugin-react": "^7.21.4", - "eslint-plugin-react-hooks": "^4.1.2", + "eslint-plugin-prettier": "^4.0.0", + "eslint-plugin-react": "^7.32.2", + "eslint-plugin-react-hooks": "^4.6.0", "husky": "^4.3.0", "jest": "^26.5.3", "lerna": "^4.0.0", "lint-staged": "^10.4.0", - "prettier": "^2.1.0", + "prettier": "^2.8.8", "react": "18.2.0", "react-test-renderer": "18.2.0", "rimraf": "^3.0.2", @@ -90,7 +95,7 @@ } }, "lint-staged": { - "*.{js,ts,tsx}": "eslint --max-warnings 0" + "*.{js,ts,tsx}": "eslint --max-warnings 100" }, "config": { "commitizen": { diff --git a/shared-helpers/src/auth/Timeout.tsx b/shared-helpers/src/auth/Timeout.tsx index 7e6e98a531..245848a07d 100644 --- a/shared-helpers/src/auth/Timeout.tsx +++ b/shared-helpers/src/auth/Timeout.tsx @@ -22,7 +22,7 @@ function useIdleTimeout(timeoutMs: number, onTimeout: () => void) { if (timer) { clearTimeout(timer) } - timer = (setTimeout(onTimeout, timeoutMs) as unknown) as number + timer = setTimeout(onTimeout, timeoutMs) as unknown as number } // Listen for any activity events & reset the timer when they are found @@ -71,7 +71,7 @@ export const IdleTimeout: FunctionComponent = ({ // Give the user 1 minute to respond to the prompt before the onTimeout action setPromptTimeout( - (setTimeout(() => { + setTimeout(() => { const timeoutAction = async () => { setPromptTimeout(undefined) await onTimeout() @@ -79,7 +79,7 @@ export const IdleTimeout: FunctionComponent = ({ void router.push(redirectPath) } void timeoutAction() - }, PROMPT_TIMEOUT) as unknown) as number + }, PROMPT_TIMEOUT) as unknown as number ) }) diff --git a/sites/partners/.jest/setup-tests.js b/sites/partners/.jest/setup-tests.js index bdac73e61c..56c6efcbab 100644 --- a/sites/partners/.jest/setup-tests.js +++ b/sites/partners/.jest/setup-tests.js @@ -17,7 +17,6 @@ global.beforeEach(() => { }) }) - // Need to set __next on base div to handle the overlay const portalRoot = document.createElement("div") portalRoot.setAttribute("id", "__next") diff --git a/sites/partners/__tests__/testUtils.tsx b/sites/partners/__tests__/testUtils.tsx index e3c7f63663..37e1820e91 100644 --- a/sites/partners/__tests__/testUtils.tsx +++ b/sites/partners/__tests__/testUtils.tsx @@ -17,9 +17,11 @@ const customRender = (ui: ReactElement, options?: Omit render(ui, { wrapper: AllTheProviders, ...options }) // re-export everything +// eslint-disable-next-line import/export export * from "@testing-library/react" // override render method +// eslint-disable-next-line import/export export { customRender as render } export const mockNextRouter = () => { diff --git a/sites/partners/src/components/applications/PaperApplicationDetails/sections/DetailsPrimaryApplicant.tsx b/sites/partners/src/components/applications/PaperApplicationDetails/sections/DetailsPrimaryApplicant.tsx index 16963c8a22..cc1a995635 100644 --- a/sites/partners/src/components/applications/PaperApplicationDetails/sections/DetailsPrimaryApplicant.tsx +++ b/sites/partners/src/components/applications/PaperApplicationDetails/sections/DetailsPrimaryApplicant.tsx @@ -36,10 +36,10 @@ const DetailsPrimaryApplicant = () => { {(() => { - const { birthMonth, birthDay, birthYear } = application?.applicant + const applicant = application?.applicant - if (birthMonth && birthDay && birthYear) { - return `${birthMonth}/${birthDay}/${birthYear}` + if (applicant.birthMonth && applicant.birthDay && applicant.birthYear) { + return `${applicant.birthMonth}/${applicant.birthDay}/${applicant.birthYear}` } return t("t.n/a") diff --git a/sites/partners/src/components/flags/flagSetCols.tsx b/sites/partners/src/components/flags/flagSetCols.tsx index 9dacbf610d..2305c72b54 100644 --- a/sites/partners/src/components/flags/flagSetCols.tsx +++ b/sites/partners/src/components/flags/flagSetCols.tsx @@ -15,7 +15,7 @@ export const getFlagSetCols = () => [ cellRendererFramework: ({ data }) => { if (!data?.applications || !data?.rule || !data?.id) return "" - const { applicant } = data?.applications?.[0] + const applicant = data?.applications?.[0]?.applicant const rule = data?.rule const firstApplicant = `${applicant?.firstName} ${applicant?.lastName}` @@ -41,8 +41,9 @@ export const getFlagSetCols = () => [ const uniqueNames = value .map((item) => [item.applicant?.firstName, item.applicant?.lastName]) .reduce((acc, curr) => { - const includesName = acc.filter((item) => item[0] === curr[0] && item[1] === curr[1]) - .length + const includesName = acc.filter( + (item) => item[0] === curr[0] && item[1] === curr[1] + ).length if (!includesName) { acc.push(curr) diff --git a/sites/partners/src/components/listings/PaperListingForm/sections/AdditionalFees.tsx b/sites/partners/src/components/listings/PaperListingForm/sections/AdditionalFees.tsx index 6ffb7a8f9e..8aa028f2d6 100644 --- a/sites/partners/src/components/listings/PaperListingForm/sections/AdditionalFees.tsx +++ b/sites/partners/src/components/listings/PaperListingForm/sections/AdditionalFees.tsx @@ -34,8 +34,9 @@ const AdditionalFees = (props: AdditionalFeesProps) => { })) }, [props.existingUtilities, register]) - const enableUtilitiesIncluded = profile?.jurisdictions?.find((j) => j.id === jurisdiction) - ?.enableUtilitiesIncluded + const enableUtilitiesIncluded = profile?.jurisdictions?.find( + (j) => j.id === jurisdiction + )?.enableUtilitiesIncluded return (
    diff --git a/sites/partners/src/components/listings/PaperListingForm/sections/BuildingFeatures.tsx b/sites/partners/src/components/listings/PaperListingForm/sections/BuildingFeatures.tsx index 01a2d18179..65d0d7731d 100644 --- a/sites/partners/src/components/listings/PaperListingForm/sections/BuildingFeatures.tsx +++ b/sites/partners/src/components/listings/PaperListingForm/sections/BuildingFeatures.tsx @@ -32,8 +32,9 @@ const BuildingFeatures = (props: BuildingFeaturesProps) => { })) }, [register, props.existingFeatures]) - const enableAccessibilityFeatures = profile?.jurisdictions?.find((j) => j.id === jurisdiction) - ?.enableAccessibilityFeatures + const enableAccessibilityFeatures = profile?.jurisdictions?.find( + (j) => j.id === jurisdiction + )?.enableAccessibilityFeatures return (
    diff --git a/sites/partners/src/components/settings/PreferenceDrawer.tsx b/sites/partners/src/components/settings/PreferenceDrawer.tsx index 434811d71a..2cfe5c3fbe 100644 --- a/sites/partners/src/components/settings/PreferenceDrawer.tsx +++ b/sites/partners/src/components/settings/PreferenceDrawer.tsx @@ -350,10 +350,10 @@ const PreferenceDrawer = ({ controlClassName={"control"} keyPrefix={"jurisdictions"} options={ - profile + profile?.jurisdictions ? [ { label: "", value: "" }, - ...profile?.jurisdictions.map((jurisdiction) => ({ + ...profile.jurisdictions.map((jurisdiction) => ({ label: jurisdiction.name, value: jurisdiction.id, })), diff --git a/sites/partners/src/lib/applications/formatApplicationData.ts b/sites/partners/src/lib/applications/formatApplicationData.ts index 4b43ab41b4..28f26b72e5 100644 --- a/sites/partners/src/lib/applications/formatApplicationData.ts +++ b/sites/partners/src/lib/applications/formatApplicationData.ts @@ -88,8 +88,11 @@ export const mapFormToApi = ({ const TIME_24H_FORMAT = "MM/DD/YYYY HH:mm:ss" // rename default (wrong property names) - const { day: submissionDay, month: submissionMonth, year: submissionYear } = - data.dateSubmitted || {} + const { + day: submissionDay, + month: submissionMonth, + year: submissionYear, + } = data.dateSubmitted || {} const { hours, minutes = 0, seconds = 0, period } = data?.timeSubmitted || {} if (!submissionDay || !submissionMonth || !submissionYear) return null @@ -220,13 +223,11 @@ export const mapFormToApi = ({ } } - const accessibility: Omit< - Accessibility, - "id" | "createdAt" | "updatedAt" - > = adaFeatureKeys.reduce((acc, feature) => { - acc[feature] = data.application.accessibility.includes(feature) - return acc - }, {}) + const accessibility: Omit = + adaFeatureKeys.reduce((acc, feature) => { + acc[feature] = data.application.accessibility.includes(feature) + return acc + }, {}) const result: ApplicationUpdate = { submissionDate, diff --git a/sites/partners/src/lib/helpers.ts b/sites/partners/src/lib/helpers.ts index e48b7a2b24..d793821aaf 100644 --- a/sites/partners/src/lib/helpers.ts +++ b/sites/partners/src/lib/helpers.ts @@ -40,7 +40,8 @@ export interface FormOptions { [key: string]: FormOption[] } -export const emailRegex = /^(([^<>()[\]\\.,;:\s@"]+(\.[^<>()[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/ +export const emailRegex = + /^(([^<>()[\]\\.,;:\s@"]+(\.[^<>()[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/ export const convertDataToPst = (dateObj: Date, type: ApplicationSubmissionType) => { if (!dateObj) { diff --git a/sites/partners/src/lib/users/signInHelpers.ts b/sites/partners/src/lib/users/signInHelpers.ts index 0fccdfb031..6beddab14c 100644 --- a/sites/partners/src/lib/users/signInHelpers.ts +++ b/sites/partners/src/lib/users/signInHelpers.ts @@ -7,98 +7,90 @@ export enum EnumRenderStep { enterCode = "enter mfa code", } -export const onSubmitEmailAndPassword = ( - setEmail, - setPassword, - setRenderStep, - determineNetworkError, - login, - router, - resetNetworkError -) => async (data: { email: string; password: string }) => { - const { email, password } = data - try { - await login(email, password) - await router.push("/") - } catch (error) { - if (error?.response?.data?.name === "mfaCodeIsMissing") { - setEmail(email) - setPassword(password) - resetNetworkError() - setRenderStep(EnumRenderStep.mfaType) - } else { - const { status } = error.response || {} - determineNetworkError(status, error) +export const onSubmitEmailAndPassword = + (setEmail, setPassword, setRenderStep, determineNetworkError, login, router, resetNetworkError) => + async (data: { email: string; password: string }) => { + const { email, password } = data + try { + await login(email, password) + await router.push("/") + } catch (error) { + if (error?.response?.data?.name === "mfaCodeIsMissing") { + setEmail(email) + setPassword(password) + resetNetworkError() + setRenderStep(EnumRenderStep.mfaType) + } else { + const { status } = error.response || {} + determineNetworkError(status, error) + } } } -} -export const onSubmitMfaType = ( - email, - password, - setMfaType, - setRenderStep, - requestMfaCode, - determineNetworkError, - setAllowPhoneNumberEdit, - setPhoneNumber, - resetNetworkError -) => async (data: { mfaType: EnumRequestMfaCodeMfaType }) => { - const { mfaType: incomingMfaType } = data - try { - const res = await requestMfaCode(email, password, incomingMfaType) - if (!res.phoneNumberVerified && incomingMfaType === EnumRequestMfaCodeMfaType.sms) { - setAllowPhoneNumberEdit(true) - setPhoneNumber(res.phoneNumber) - } - setMfaType(incomingMfaType) - resetNetworkError() - setRenderStep(EnumRenderStep.enterCode) - } catch (error) { - if (error?.response?.data?.name === "phoneNumberMissing") { +export const onSubmitMfaType = + ( + email, + password, + setMfaType, + setRenderStep, + requestMfaCode, + determineNetworkError, + setAllowPhoneNumberEdit, + setPhoneNumber, + resetNetworkError + ) => + async (data: { mfaType: EnumRequestMfaCodeMfaType }) => { + const { mfaType: incomingMfaType } = data + try { + const res = await requestMfaCode(email, password, incomingMfaType) + if (!res.phoneNumberVerified && incomingMfaType === EnumRequestMfaCodeMfaType.sms) { + setAllowPhoneNumberEdit(true) + setPhoneNumber(res.phoneNumber) + } setMfaType(incomingMfaType) - setRenderStep(EnumRenderStep.phoneNumber) - } else { - const { status } = error.response || {} - determineNetworkError(status, error) + resetNetworkError() + setRenderStep(EnumRenderStep.enterCode) + } catch (error) { + if (error?.response?.data?.name === "phoneNumberMissing") { + setMfaType(incomingMfaType) + setRenderStep(EnumRenderStep.phoneNumber) + } else { + const { status } = error.response || {} + determineNetworkError(status, error) + } } } -} -export const onSubmitMfaCodeWithPhone = ( - email, - password, - mfaType, - setRenderStep, - requestMfaCode, - setAllowPhoneNumberEdit, - setPhoneNumber, - resetNetworkError -) => async (data: { phoneNumber: string }) => { - const { phoneNumber } = data - await requestMfaCode(email, password, mfaType, phoneNumber) - resetNetworkError() - setRenderStep(EnumRenderStep.enterCode) - setAllowPhoneNumberEdit(true) - setPhoneNumber(phoneNumber) -} - -export const onSubmitMfaCode = ( - email, - password, - determineNetworkError, - login, - router, - mfaType, - resetNetworkError -) => async (data: { mfaCode: string }) => { - const { mfaCode } = data - try { - await login(email, password, mfaCode, mfaType) +export const onSubmitMfaCodeWithPhone = + ( + email, + password, + mfaType, + setRenderStep, + requestMfaCode, + setAllowPhoneNumberEdit, + setPhoneNumber, + resetNetworkError + ) => + async (data: { phoneNumber: string }) => { + const { phoneNumber } = data + await requestMfaCode(email, password, mfaType, phoneNumber) resetNetworkError() - await router.push("/") - } catch (error) { - const { status } = error.response || {} - determineNetworkError(status, error, true) + setRenderStep(EnumRenderStep.enterCode) + setAllowPhoneNumberEdit(true) + setPhoneNumber(phoneNumber) + } + +export const onSubmitMfaCode = + (email, password, determineNetworkError, login, router, mfaType, resetNetworkError) => + async (data: { mfaCode: string }) => { + const { mfaCode } = data + try { + await login(email, password, mfaCode, mfaType) + resetNetworkError() + await router.push("/") + } catch (error) { + const { status } = error.response || {} + determineNetworkError(status, error, true) + } } -} diff --git a/sites/partners/src/pages/users/index.tsx b/sites/partners/src/pages/users/index.tsx index 9c6dbd45b3..56426c9d2b 100644 --- a/sites/partners/src/pages/users/index.tsx +++ b/sites/partners/src/pages/users/index.tsx @@ -115,7 +115,12 @@ const Users = () => { ] }, []) - const { data: userList, loading, error, cacheKey } = useUserList({ + const { + data: userList, + loading, + error, + cacheKey, + } = useUserList({ page: tableOptions.pagination.currentPage, limit: tableOptions.pagination.itemsPerPage, search: tableOptions.filter.filterValue, diff --git a/sites/partners/tsconfig.json b/sites/partners/tsconfig.json index 7cdcbc687d..1c1692c6aa 100644 --- a/sites/partners/tsconfig.json +++ b/sites/partners/tsconfig.json @@ -1,23 +1,11 @@ { "extends": "../../tsconfig.json", "compilerOptions": { - "lib": [ - "dom", - "dom.iterable", - "esnext" - ], + "lib": ["dom", "dom.iterable", "esnext"], "jsx": "preserve", "allowJs": true, "incremental": true }, - "exclude": [ - "node_modules", - "cypress", - "cypress-file-upload" - ], - "include": [ - "next-env.d.ts", - "src/**/*.ts", - "src/**/*.tsx" - ] + "exclude": ["node_modules", "cypress", "cypress-file-upload"], + "include": ["next-env.d.ts", "src/**/*.ts", "src/**/*.tsx"] } diff --git a/sites/public/src/components/applications/ApplicationMultiselectQuestionStep.tsx b/sites/public/src/components/applications/ApplicationMultiselectQuestionStep.tsx index 89307abe6e..fa0644ac71 100644 --- a/sites/public/src/components/applications/ApplicationMultiselectQuestionStep.tsx +++ b/sites/public/src/components/applications/ApplicationMultiselectQuestionStep.tsx @@ -136,7 +136,7 @@ const ApplicationMultiselectQuestionStep = ({ ) } - const allOptions = question ? [...question?.options] : [] + const allOptions = question?.options ? [...question.options] : [] if (question?.optOutText) { allOptions.push({ text: question?.optOutText, diff --git a/sites/public/src/components/listing/ListingView.tsx b/sites/public/src/components/listing/ListingView.tsx index 1c4ad8bf64..87f7ab4fbd 100644 --- a/sites/public/src/components/listing/ListingView.tsx +++ b/sites/public/src/components/listing/ListingView.tsx @@ -69,10 +69,8 @@ interface ListingProps { export const ListingView = (props: ListingProps) => { let buildingSelectionCriteria, preferencesSection const { listing } = props - const { - content: appStatusContent, - subContent: appStatusSubContent, - } = useGetApplicationStatusProps(listing) + const { content: appStatusContent, subContent: appStatusSubContent } = + useGetApplicationStatusProps(listing) const appOpenInFuture = openInFuture(listing) const hasNonReferralMethods = listing?.applicationMethods diff --git a/sites/public/src/lib/helpers.tsx b/sites/public/src/lib/helpers.tsx index df7483b937..859e256199 100644 --- a/sites/public/src/lib/helpers.tsx +++ b/sites/public/src/lib/helpers.tsx @@ -17,7 +17,8 @@ import { } from "@bloom-housing/ui-components" import { imageUrlFromListing, getSummariesTable } from "@bloom-housing/shared-helpers" -export const emailRegex = /^(([^<>()[\]\\.,;:\s@"]+(\.[^<>()[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/ +export const emailRegex = + /^(([^<>()[\]\\.,;:\s@"]+(\.[^<>()[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/ export const getGenericAddress = (bloomAddress: Address) => { return bloomAddress diff --git a/sites/public/tsconfig.json b/sites/public/tsconfig.json index 310e8fefaf..406afd0400 100644 --- a/sites/public/tsconfig.json +++ b/sites/public/tsconfig.json @@ -1,22 +1,11 @@ { "extends": "../../tsconfig.json", "compilerOptions": { - "lib": [ - "dom", - "dom.iterable", - "esnext" - ], + "lib": ["dom", "dom.iterable", "esnext"], "jsx": "preserve", "allowJs": true, "incremental": true }, - "exclude": [ - "node_modules", - "cypress" - ], - "include": [ - "next-env.d.ts", - "**/*.ts", - "**/*.tsx" - ] + "exclude": ["node_modules", "cypress"], + "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"] } diff --git a/sites/public/tsconfig.test.json b/sites/public/tsconfig.test.json index 72688cff76..6727d07ce2 100644 --- a/sites/public/tsconfig.test.json +++ b/sites/public/tsconfig.test.json @@ -3,10 +3,5 @@ "compilerOptions": { "jsx": "react" }, - "include": [ - "next-env.d.ts", - "src/**/*.ts", - "src/**/*.tsx" - ] - + "include": ["next-env.d.ts", "src/**/*.ts", "src/**/*.tsx"] } diff --git a/yarn.lock b/yarn.lock index 6d75264ed2..d2dd2596b5 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1858,22 +1858,38 @@ global-agent "^3.0.0" global-tunnel-ng "^2.7.1" -"@eslint/eslintrc@^0.1.3": - version "0.1.3" - resolved "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-0.1.3.tgz" - integrity sha512-4YVwPkANLeNtRjMekzux1ci8hIaH5eGKktGqR0d3LWsKNn5B2X/1Z6Trxy7jQXl9EBGE6Yj02O+t09FMeRllaA== +"@eslint-community/eslint-utils@^4.2.0": + version "4.4.0" + resolved "https://registry.yarnpkg.com/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz#a23514e8fb9af1269d5f7788aa556798d61c6b59" + integrity sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA== + dependencies: + eslint-visitor-keys "^3.3.0" + +"@eslint-community/regexpp@^4.4.0": + version "4.5.1" + resolved "https://registry.yarnpkg.com/@eslint-community/regexpp/-/regexpp-4.5.1.tgz#cdd35dce4fa1a89a4fd42b1599eb35b3af408884" + integrity sha512-Z5ba73P98O1KUYCCJTUeVpja9RcGoMdncZ6T49FCUl2lN38JtCJ+3WgIDBv0AuY4WChU5PmtJmOCTlN6FZTFKQ== + +"@eslint/eslintrc@^2.0.3": + version "2.0.3" + resolved "https://registry.yarnpkg.com/@eslint/eslintrc/-/eslintrc-2.0.3.tgz#4910db5505f4d503f27774bf356e3704818a0331" + integrity sha512-+5gy6OQfk+xx3q0d6jGZZC3f3KzAkXc/IanVxd1is/VIIziRqqt3ongQz0FiTUXqTk0c7aDB3OaFuKnuSoJicQ== dependencies: ajv "^6.12.4" - debug "^4.1.1" - espree "^7.3.0" - globals "^12.1.0" - ignore "^4.0.6" + debug "^4.3.2" + espree "^9.5.2" + globals "^13.19.0" + ignore "^5.2.0" import-fresh "^3.2.1" - js-yaml "^3.13.1" - lodash "^4.17.19" - minimatch "^3.0.4" + js-yaml "^4.1.0" + minimatch "^3.1.2" strip-json-comments "^3.1.1" +"@eslint/js@8.41.0": + version "8.41.0" + resolved "https://registry.yarnpkg.com/@eslint/js/-/js-8.41.0.tgz#080321c3b68253522f7646b55b577dd99d2950b3" + integrity sha512-LxcyMGxwmTh2lY9FwHPGWOHmYFCZvbrFCBZL4FzSSsxsRPuhrYUg/49/0KDfW8tnIEaEHtfmn6+NPN+1DqaNmA== + "@fortawesome/fontawesome-common-types@6.1.1": version "6.1.1" resolved "https://registry.npmjs.org/@fortawesome/fontawesome-common-types/-/fontawesome-common-types-6.1.1.tgz" @@ -2038,6 +2054,25 @@ dependencies: "@hapi/hoek" "^9.0.0" +"@humanwhocodes/config-array@^0.11.8": + version "0.11.8" + resolved "https://registry.yarnpkg.com/@humanwhocodes/config-array/-/config-array-0.11.8.tgz#03595ac2075a4dc0f191cc2131de14fbd7d410b9" + integrity sha512-UybHIJzJnR5Qc/MsD9Kr+RpO2h+/P1GhOwdiLPXK5TWk5sgTdu88bTD9UP+CKbPPh5Rni1u0GjAdYQLemG8g+g== + dependencies: + "@humanwhocodes/object-schema" "^1.2.1" + debug "^4.1.1" + minimatch "^3.0.5" + +"@humanwhocodes/module-importer@^1.0.1": + version "1.0.1" + resolved "https://registry.yarnpkg.com/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz#af5b2691a22b44be847b0ca81641c5fb6ad0172c" + integrity sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA== + +"@humanwhocodes/object-schema@^1.2.1": + version "1.2.1" + resolved "https://registry.yarnpkg.com/@humanwhocodes/object-schema/-/object-schema-1.2.1.tgz#b520529ec21d8e5945a1851dfd1c32e94e39ff45" + integrity sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA== + "@hutson/parse-repository-url@^3.0.0": version "3.0.2" resolved "https://registry.npmjs.org/@hutson/parse-repository-url/-/parse-repository-url-3.0.2.tgz" @@ -3744,11 +3779,24 @@ "@nodelib/fs.stat" "2.0.3" run-parallel "^1.1.9" +"@nodelib/fs.scandir@2.1.5": + version "2.1.5" + resolved "https://registry.yarnpkg.com/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz#7619c2eb21b25483f6d167548b4cfd5a7488c3d5" + integrity sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g== + dependencies: + "@nodelib/fs.stat" "2.0.5" + run-parallel "^1.1.9" + "@nodelib/fs.stat@2.0.3", "@nodelib/fs.stat@^2.0.2": version "2.0.3" resolved "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.3.tgz" integrity sha512-bQBFruR2TAwoevBEd/NWMoAAtNGzTRgdrqnYCc7dhzfoNvqPzLyqlEQnzZ3kVnNrSp25iyxE00/3h2fqGAGArA== +"@nodelib/fs.stat@2.0.5": + version "2.0.5" + resolved "https://registry.yarnpkg.com/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz#5bd262af94e9d25bd1e71b05deed44876a222e8b" + integrity sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A== + "@nodelib/fs.walk@^1.2.3": version "1.2.4" resolved "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.4.tgz" @@ -3757,6 +3805,14 @@ "@nodelib/fs.scandir" "2.1.3" fastq "^1.6.0" +"@nodelib/fs.walk@^1.2.8": + version "1.2.8" + resolved "https://registry.yarnpkg.com/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz#e95737e8bb6746ddedf69c556953494f196fe69a" + integrity sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg== + dependencies: + "@nodelib/fs.scandir" "2.1.5" + fastq "^1.6.0" + "@npmcli/ci-detect@^1.0.0": version "1.3.0" resolved "https://registry.npmjs.org/@npmcli/ci-detect/-/ci-detect-1.3.0.tgz" @@ -5103,10 +5159,10 @@ acorn-import-assertions@^1.7.6: resolved "https://registry.npmjs.org/acorn-import-assertions/-/acorn-import-assertions-1.8.0.tgz" integrity sha512-m7VZ3jwz4eK6A4Vtt8Ew1/mNbP24u0FhdyfA7fSvnJR6LMdfOYnmuIrrJAgrYfYJ10F/otaHTtrtrtmHdMNzEw== -acorn-jsx@^5.2.0: - version "5.2.0" - resolved "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.2.0.tgz" - integrity sha512-HiUX/+K2YpkpJ+SzBffkM/AQ2YE03S0U1kjTLVpoJdhZMOWy8qvXVN9JdLqv2QsaQ6MPYQIuNmwD8zOiYUofLQ== +acorn-jsx@^5.3.2: + version "5.3.2" + resolved "https://registry.yarnpkg.com/acorn-jsx/-/acorn-jsx-5.3.2.tgz#7ed5bb55908b3b2f1bc55c6af1653bada7f07937" + integrity sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ== acorn-node@^1.6.1: version "1.8.2" @@ -5132,9 +5188,9 @@ acorn-walk@^8.1.1: resolved "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.2.0.tgz" integrity sha512-k+iyHEuPgSw6SbuDpGQM+06HQUa04DZ3o+F6CSzXMvvI5KMvnaEqXe+YVe555R9nn6GPt404fos4wcgpw12SDA== -acorn@^7.0.0, acorn@^7.1.1, acorn@^7.4.0: +acorn@^7.0.0, acorn@^7.1.1: version "7.4.1" - resolved "https://registry.npmjs.org/acorn/-/acorn-7.4.1.tgz" + resolved "https://registry.yarnpkg.com/acorn/-/acorn-7.4.1.tgz#feaed255973d2e77555b83dbc08851a6c63520fa" integrity sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A== acorn@^8.0.4, acorn@^8.2.4, acorn@^8.4.1, acorn@^8.5.0: @@ -5142,6 +5198,11 @@ acorn@^8.0.4, acorn@^8.2.4, acorn@^8.4.1, acorn@^8.5.0: resolved "https://registry.npmjs.org/acorn/-/acorn-8.7.1.tgz" integrity sha512-Xx54uLJQZ19lKygFXOWsscKUbsBZW0CPykPhVQdhIeIwrbPmJzqeASDInc8nKBnp/JT6igTs82qPXz069H8I/A== +acorn@^8.8.0: + version "8.8.2" + resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.8.2.tgz#1b2f25db02af965399b9776b0c2c391276d37c4a" + integrity sha512-xjIYgE8HBrkpd/sJqOGNspf8uHG+NOHGOw6a/Urj8taM2EXfdNAH2oFcPeIFfsv3+kz/mJrS5VuMqbNLjCa2vw== + add-stream@^1.0.0: version "1.0.0" resolved "https://registry.npmjs.org/add-stream/-/add-stream-1.0.0.tgz" @@ -5217,7 +5278,7 @@ ajv@8.9.0: require-from-string "^2.0.2" uri-js "^4.2.2" -ajv@^6.10.0, ajv@^6.10.2, ajv@^6.12.3: +ajv@^6.10.0, ajv@^6.12.3: version "6.12.4" resolved "https://registry.npmjs.org/ajv/-/ajv-6.12.4.tgz" integrity sha512-eienB2c9qVQs2KWexhkrdMLVDoIQCz5KSeLxwg9Lzk4DOfBtIK9PQwwufcsn1jjGuf9WZmqPMbGxOzfcuphJCQ== @@ -5249,7 +5310,7 @@ ajv@^6.12.4, ajv@^6.12.5: ajv@^8.0.0, ajv@^8.8.0: version "8.12.0" - resolved "https://registry.npmjs.org/ajv/-/ajv-8.12.0.tgz" + resolved "https://registry.yarnpkg.com/ajv/-/ajv-8.12.0.tgz#d1a0527323e22f53562c567c00991577dfbe19d1" integrity sha512-sRu1kpcO9yLtYxBKvqfTeh9KzZEwO3STyX1HT+4CaDzC6HpTGYhIhPIzj9XuKU7KYDwnaeh5hcOwjy1QuJzBPA== dependencies: fast-deep-equal "^3.1.1" @@ -5457,6 +5518,14 @@ arr-union@^3.1.0: resolved "https://registry.npmjs.org/arr-union/-/arr-union-3.1.0.tgz" integrity sha1-45sJrqne+Gao8gbiiK9jkZuuOcQ= +array-buffer-byte-length@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/array-buffer-byte-length/-/array-buffer-byte-length-1.0.0.tgz#fabe8bc193fea865f317fe7807085ee0dee5aead" + integrity sha512-LPuwb2P+NrQw3XhxGc36+XSvuBPopovXYTR9Ew++Du9Yb/bx5AzBfrIsBoj0EZUifjQU+sHL21sseZ3jerWO/A== + dependencies: + call-bind "^1.0.2" + is-array-buffer "^3.0.1" + array-differ@^3.0.0: version "3.0.0" resolved "https://registry.npmjs.org/array-differ/-/array-differ-3.0.0.tgz" @@ -5497,6 +5566,17 @@ array-includes@^3.1.4, array-includes@^3.1.5: get-intrinsic "^1.1.1" is-string "^1.0.7" +array-includes@^3.1.6: + version "3.1.6" + resolved "https://registry.yarnpkg.com/array-includes/-/array-includes-3.1.6.tgz#9e9e720e194f198266ba9e18c29e6a9b0e4b225f" + integrity sha512-sgTbLvL6cNnw24FnbaDyjmvddQ2ML8arZsgaJhoABMoplz/4QRhtrYS+alr1BUM1Bwp6dhx8vVCBSLG+StwOFw== + dependencies: + call-bind "^1.0.2" + define-properties "^1.1.4" + es-abstract "^1.20.4" + get-intrinsic "^1.1.3" + is-string "^1.0.7" + array-union@^2.1.0: version "2.1.0" resolved "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz" @@ -5507,22 +5587,36 @@ array-unique@^0.3.2: resolved "https://registry.npmjs.org/array-unique/-/array-unique-0.3.2.tgz" integrity sha1-qJS3XUvE9s1nnvMkSp/Y9Gri1Cg= -array.prototype.flat@^1.2.3: - version "1.2.3" - resolved "https://registry.npmjs.org/array.prototype.flat/-/array.prototype.flat-1.2.3.tgz" - integrity sha512-gBlRZV0VSmfPIeWfuuy56XZMvbVfbEUnOXUvt3F/eUUUSyzlgLxhEX4YAEpxNAogRGehPSnfXyPtYyKAhkzQhQ== +array.prototype.flat@^1.3.1: + version "1.3.1" + resolved "https://registry.yarnpkg.com/array.prototype.flat/-/array.prototype.flat-1.3.1.tgz#ffc6576a7ca3efc2f46a143b9d1dda9b4b3cf5e2" + integrity sha512-roTU0KWIOmJ4DRLmwKd19Otg0/mT3qPNt0Qb3GWW8iObuZXxrjB/pzn0R3hqpRSWg4HCwqx+0vwOnWnvlOyeIA== dependencies: - define-properties "^1.1.3" - es-abstract "^1.17.0-next.1" + call-bind "^1.0.2" + define-properties "^1.1.4" + es-abstract "^1.20.4" + es-shim-unscopables "^1.0.0" -array.prototype.flatmap@^1.2.3: - version "1.2.3" - resolved "https://registry.npmjs.org/array.prototype.flatmap/-/array.prototype.flatmap-1.2.3.tgz" - integrity sha512-OOEk+lkePcg+ODXIpvuU9PAryCikCJyo7GlDG1upleEpQRx6mzL9puEBkozQ5iAx20KV0l3DbyQwqciJtqe5Pg== +array.prototype.flatmap@^1.3.1: + version "1.3.1" + resolved "https://registry.yarnpkg.com/array.prototype.flatmap/-/array.prototype.flatmap-1.3.1.tgz#1aae7903c2100433cb8261cd4ed310aab5c4a183" + integrity sha512-8UGn9O1FDVvMNB0UlLv4voxRMze7+FpHyF5mSMRjWHUMlpoDViniy05870VlxhfgTnLbpuwTzvD76MTtWxB/mQ== dependencies: - define-properties "^1.1.3" - es-abstract "^1.17.0-next.1" - function-bind "^1.1.1" + call-bind "^1.0.2" + define-properties "^1.1.4" + es-abstract "^1.20.4" + es-shim-unscopables "^1.0.0" + +array.prototype.tosorted@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/array.prototype.tosorted/-/array.prototype.tosorted-1.1.1.tgz#ccf44738aa2b5ac56578ffda97c03fd3e23dd532" + integrity sha512-pZYPXPRl2PqWcsUs6LOMn+1f1532nEoPTYowBtqLwAW+W8vSVhkIGnmOX1t/UQjD6YGI0vcD2B1U7ZFGQH9jnQ== + dependencies: + call-bind "^1.0.2" + define-properties "^1.1.4" + es-abstract "^1.20.4" + es-shim-unscopables "^1.0.0" + get-intrinsic "^1.1.3" arrify@^1.0.1: version "1.0.1" @@ -5561,11 +5655,6 @@ ast-types-flow@^0.0.7: resolved "https://registry.npmjs.org/ast-types-flow/-/ast-types-flow-0.0.7.tgz" integrity sha512-eBvWn1lvIApYMhzQMsu9ciLfkBY499mFZlNqG+/9WR7PVlroQw0vG30cOQQbaKz3sCEc44TAOu2ykzqXSNnwag== -astral-regex@^1.0.0: - version "1.0.0" - resolved "https://registry.npmjs.org/astral-regex/-/astral-regex-1.0.0.tgz" - integrity sha512-+Ryf6g3BKoRc7jfp7ad8tM4TtMiaWvbF/1/sQcZPkkS7ag3D5nMBCe2UfOTONtAkaG0tO0ij3C5Lwmf1EiyjHg== - astral-regex@^2.0.0: version "2.0.0" resolved "https://registry.npmjs.org/astral-regex/-/astral-regex-2.0.0.tgz" @@ -6891,11 +6980,6 @@ console-control-strings@^1.0.0, console-control-strings@~1.1.0: resolved "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz" integrity sha1-PXz0Rk22RG6mRL9LOVB/mFEAjo4= -contains-path@^0.1.0: - version "0.1.0" - resolved "https://registry.npmjs.org/contains-path/-/contains-path-0.1.0.tgz" - integrity sha1-/ozxhP9mcLa67wGp1IYaXL7EEgo= - content-disposition@0.5.4: version "0.5.4" resolved "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz" @@ -7369,7 +7453,7 @@ debug@2.6.9, debug@^2.2.0, debug@^2.3.3, debug@^2.6.9: dependencies: ms "2.0.0" -debug@4, debug@4.3.4, debug@^4.0.1, debug@^4.1.0, debug@^4.1.1, debug@^4.3.1, debug@^4.3.2, debug@^4.3.3, debug@^4.3.4: +debug@4, debug@4.3.4, debug@^4.1.0, debug@^4.1.1, debug@^4.3.1, debug@^4.3.2, debug@^4.3.3, debug@^4.3.4: version "4.3.4" resolved "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz" integrity sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ== @@ -7383,9 +7467,9 @@ debug@=3.1.0: dependencies: ms "2.0.0" -debug@^3.1.0: +debug@^3.1.0, debug@^3.2.7: version "3.2.7" - resolved "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz" + resolved "https://registry.yarnpkg.com/debug/-/debug-3.2.7.tgz#72580b7e9145fb39b6676f9c5e5fb100b934179a" integrity sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ== dependencies: ms "^2.1.1" @@ -7695,14 +7779,6 @@ dlv@^1.1.3: resolved "https://registry.npmjs.org/dlv/-/dlv-1.1.3.tgz" integrity sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA== -doctrine@1.5.0: - version "1.5.0" - resolved "https://registry.npmjs.org/doctrine/-/doctrine-1.5.0.tgz" - integrity sha1-N53Ocw9hZvds76TmcHoVmwLFpvo= - dependencies: - esutils "^2.0.2" - isarray "^1.0.0" - doctrine@^2.1.0: version "2.1.0" resolved "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz" @@ -7937,9 +8013,9 @@ enhanced-resolve@^5.8.3: graceful-fs "^4.2.4" tapable "^2.2.0" -enquirer@^2.3.5, enquirer@^2.3.6: +enquirer@^2.3.6: version "2.3.6" - resolved "https://registry.npmjs.org/enquirer/-/enquirer-2.3.6.tgz" + resolved "https://registry.yarnpkg.com/enquirer/-/enquirer-2.3.6.tgz#2a7fe5dd634a1e4125a975ec994ff5456dc3734d" integrity sha512-yjNnPr315/FjS4zIsUxYguYUPP2e1NK4d7E7ZOLiyYCcbFBiTMyID+2wvm2w6+pZ/odMA7cRkjhsPbltwBOrLg== dependencies: ansi-colors "^4.1.1" @@ -7971,7 +8047,7 @@ errno@^0.1.3: dependencies: prr "~1.0.1" -error-ex@^1.2.0, error-ex@^1.3.1: +error-ex@^1.3.1: version "1.3.2" resolved "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz" integrity sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g== @@ -8082,6 +8158,46 @@ es-abstract@^1.19.0, es-abstract@^1.19.5: string.prototype.trimstart "^1.0.5" unbox-primitive "^1.0.2" +es-abstract@^1.20.4: + version "1.21.2" + resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.21.2.tgz#a56b9695322c8a185dc25975aa3b8ec31d0e7eff" + integrity sha512-y/B5POM2iBnIxCiernH1G7rC9qQoM77lLIMQLuob0zhp8C56Po81+2Nj0WFKnd0pNReDTnkYryc+zhOzpEIROg== + dependencies: + array-buffer-byte-length "^1.0.0" + available-typed-arrays "^1.0.5" + call-bind "^1.0.2" + es-set-tostringtag "^2.0.1" + es-to-primitive "^1.2.1" + function.prototype.name "^1.1.5" + get-intrinsic "^1.2.0" + get-symbol-description "^1.0.0" + globalthis "^1.0.3" + gopd "^1.0.1" + has "^1.0.3" + has-property-descriptors "^1.0.0" + has-proto "^1.0.1" + has-symbols "^1.0.3" + internal-slot "^1.0.5" + is-array-buffer "^3.0.2" + is-callable "^1.2.7" + is-negative-zero "^2.0.2" + is-regex "^1.1.4" + is-shared-array-buffer "^1.0.2" + is-string "^1.0.7" + is-typed-array "^1.1.10" + is-weakref "^1.0.2" + object-inspect "^1.12.3" + object-keys "^1.1.1" + object.assign "^4.1.4" + regexp.prototype.flags "^1.4.3" + safe-regex-test "^1.0.0" + string.prototype.trim "^1.2.7" + string.prototype.trimend "^1.0.6" + string.prototype.trimstart "^1.0.6" + typed-array-length "^1.0.4" + unbox-primitive "^1.0.2" + which-typed-array "^1.1.9" + es-get-iterator@^1.1.2: version "1.1.2" resolved "https://registry.npmjs.org/es-get-iterator/-/es-get-iterator-1.1.2.tgz" @@ -8101,6 +8217,22 @@ es-module-lexer@^0.9.0: resolved "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-0.9.3.tgz" integrity sha512-1HQ2M2sPtxwnvOvT1ZClHyQDiggdNjURWpY2we6aMKCQiUVxTmVs2UYPLIrD84sS+kMdUwfBSylbJPwNnBrnHQ== +es-set-tostringtag@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/es-set-tostringtag/-/es-set-tostringtag-2.0.1.tgz#338d502f6f674301d710b80c8592de8a15f09cd8" + integrity sha512-g3OMbtlwY3QewlqAiMLI47KywjWZoEytKr8pf6iTC8uJq5bIAH52Z9pnQ8pVL6whrCto53JZDuUIsifGeLorTg== + dependencies: + get-intrinsic "^1.1.3" + has "^1.0.3" + has-tostringtag "^1.0.0" + +es-shim-unscopables@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/es-shim-unscopables/-/es-shim-unscopables-1.0.0.tgz#702e632193201e3edf8713635d083d378e510241" + integrity sha512-Jm6GPcCdC30eMLbZ2x8z2WuRwAws3zTBBKuusffYVUrNj/GVSUAZ+xKMaUpfNDR5IbyNA5LJbaecoUVbmUcB1w== + dependencies: + has "^1.0.3" + es-to-primitive@^1.2.1: version "1.2.1" resolved "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.2.1.tgz" @@ -8152,47 +8284,47 @@ escodegen@^2.0.0: optionalDependencies: source-map "~0.6.1" -eslint-config-prettier@^6.11.0: - version "6.11.0" - resolved "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-6.11.0.tgz" - integrity sha512-oB8cpLWSAjOVFEJhhyMZh6NOEOtBVziaqdDQ86+qhDHFbZXoRTM7pNSvFRfW/W/L/LrQ38C99J5CGuRBBzBsdA== - dependencies: - get-stdin "^6.0.0" +eslint-config-prettier@^8.3.0: + version "8.8.0" + resolved "https://registry.yarnpkg.com/eslint-config-prettier/-/eslint-config-prettier-8.8.0.tgz#bfda738d412adc917fd7b038857110efe98c9348" + integrity sha512-wLbQiFre3tdGgpDv67NQKnJuTlcUVYHas3k+DZCc2U2BadthoEY4B7hLPvAxaqdyOGCzuLfii2fqGph10va7oA== -eslint-import-resolver-node@^0.3.4: - version "0.3.4" - resolved "https://registry.npmjs.org/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.4.tgz" - integrity sha512-ogtf+5AB/O+nM6DIeBUNr2fuT7ot9Qg/1harBfBtaP13ekEWFQEEMP94BCB7zaNW3gyY+8SHYF00rnqYwXKWOA== +eslint-import-resolver-node@^0.3.7: + version "0.3.7" + resolved "https://registry.yarnpkg.com/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.7.tgz#83b375187d412324a1963d84fa664377a23eb4d7" + integrity sha512-gozW2blMLJCeFpBwugLTGyvVjNoeo1knonXAcatC6bjPBZitotxdWf7Gimr25N4c0AAOo4eOUfaG82IJPDpqCA== dependencies: - debug "^2.6.9" - resolve "^1.13.1" + debug "^3.2.7" + is-core-module "^2.11.0" + resolve "^1.22.1" -eslint-module-utils@^2.6.0: - version "2.6.0" - resolved "https://registry.npmjs.org/eslint-module-utils/-/eslint-module-utils-2.6.0.tgz" - integrity sha512-6j9xxegbqe8/kZY8cYpcp0xhbK0EgJlg3g9mib3/miLaExuuwc3n5UEfSnU6hWMbT0FAYVvDbL9RrRgpUeQIvA== +eslint-module-utils@^2.7.4: + version "2.8.0" + resolved "https://registry.yarnpkg.com/eslint-module-utils/-/eslint-module-utils-2.8.0.tgz#e439fee65fc33f6bba630ff621efc38ec0375c49" + integrity sha512-aWajIYfsqCKRDgUfjEXNN/JlrzauMuSEy5sbd7WXbtW3EH6A6MpwEh42c7qD+MqQo9QMJ6fWLAeIJynx0g6OAw== dependencies: - debug "^2.6.9" - pkg-dir "^2.0.0" + debug "^3.2.7" -eslint-plugin-import@^2.22.1: - version "2.22.1" - resolved "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.22.1.tgz" - integrity sha512-8K7JjINHOpH64ozkAhpT3sd+FswIZTfMZTjdx052pnWrgRCVfp8op9tbjpAk3DdUeI/Ba4C8OjdC0r90erHEOw== +eslint-plugin-import@^2.27.5: + version "2.27.5" + resolved "https://registry.yarnpkg.com/eslint-plugin-import/-/eslint-plugin-import-2.27.5.tgz#876a6d03f52608a3e5bb439c2550588e51dd6c65" + integrity sha512-LmEt3GVofgiGuiE+ORpnvP+kAm3h6MLZJ4Q5HCyHADofsb4VzXFsRiWj3c0OFiV+3DWFh0qg3v9gcPlfc3zRow== dependencies: - array-includes "^3.1.1" - array.prototype.flat "^1.2.3" - contains-path "^0.1.0" - debug "^2.6.9" - doctrine "1.5.0" - eslint-import-resolver-node "^0.3.4" - eslint-module-utils "^2.6.0" + array-includes "^3.1.6" + array.prototype.flat "^1.3.1" + array.prototype.flatmap "^1.3.1" + debug "^3.2.7" + doctrine "^2.1.0" + eslint-import-resolver-node "^0.3.7" + eslint-module-utils "^2.7.4" has "^1.0.3" - minimatch "^3.0.4" - object.values "^1.1.1" - read-pkg-up "^2.0.0" - resolve "^1.17.0" - tsconfig-paths "^3.9.0" + is-core-module "^2.11.0" + is-glob "^4.0.3" + minimatch "^3.1.2" + object.values "^1.1.6" + resolve "^1.22.1" + semver "^6.3.0" + tsconfig-paths "^3.14.1" eslint-plugin-jsx-a11y@6.5.1: version "6.5.1" @@ -8212,34 +8344,38 @@ eslint-plugin-jsx-a11y@6.5.1: language-tags "^1.0.5" minimatch "^3.0.4" -eslint-plugin-prettier@^3.1.4: - version "3.1.4" - resolved "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-3.1.4.tgz" - integrity sha512-jZDa8z76klRqo+TdGDTFJSavwbnWK2ZpqGKNZ+VvweMW516pDUMmQ2koXvxEE4JhzNvTv+radye/bWGBmA6jmg== +eslint-plugin-prettier@^4.0.0: + version "4.2.1" + resolved "https://registry.yarnpkg.com/eslint-plugin-prettier/-/eslint-plugin-prettier-4.2.1.tgz#651cbb88b1dab98bfd42f017a12fa6b2d993f94b" + integrity sha512-f/0rXLXUt0oFYs8ra4w49wYZBG5GKZpAYsJSm6rnYL5uVDjd+zowwMwVZHnAjf4edNrKpCDYfXDgmRE/Ak7QyQ== dependencies: prettier-linter-helpers "^1.0.0" -eslint-plugin-react-hooks@^4.1.2: - version "4.1.2" - resolved "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-4.1.2.tgz" - integrity sha512-ykUeqkGyUGgwTtk78C0o8UG2fzwmgJ0qxBGPp2WqRKsTwcLuVf01kTDRAtOsd4u6whX2XOC8749n2vPydP82fg== +eslint-plugin-react-hooks@^4.6.0: + version "4.6.0" + resolved "https://registry.yarnpkg.com/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-4.6.0.tgz#4c3e697ad95b77e93f8646aaa1630c1ba607edd3" + integrity sha512-oFc7Itz9Qxh2x4gNHStv3BqJq54ExXmfC+a1NjAta66IAN87Wu0R/QArgIS9qKzX3dXKPI9H5crl9QchNMY9+g== -eslint-plugin-react@^7.21.4: - version "7.21.4" - resolved "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.21.4.tgz" - integrity sha512-uHeQ8A0hg0ltNDXFu3qSfFqTNPXm1XithH6/SY318UX76CMj7Q599qWpgmMhVQyvhq36pm7qvoN3pb6/3jsTFg== +eslint-plugin-react@^7.32.2: + version "7.32.2" + resolved "https://registry.yarnpkg.com/eslint-plugin-react/-/eslint-plugin-react-7.32.2.tgz#e71f21c7c265ebce01bcbc9d0955170c55571f10" + integrity sha512-t2fBMa+XzonrrNkyVirzKlvn5RXzzPwRHtMvLAtVZrt8oxgnTQaYbU6SXTOO1mwQgp1y5+toMSKInnzGr0Knqg== dependencies: - array-includes "^3.1.1" - array.prototype.flatmap "^1.2.3" + array-includes "^3.1.6" + array.prototype.flatmap "^1.3.1" + array.prototype.tosorted "^1.1.1" doctrine "^2.1.0" - has "^1.0.3" + estraverse "^5.3.0" jsx-ast-utils "^2.4.1 || ^3.0.0" - object.entries "^1.1.2" - object.fromentries "^2.0.2" - object.values "^1.1.1" - prop-types "^15.7.2" - resolve "^1.17.0" - string.prototype.matchall "^4.0.2" + minimatch "^3.1.2" + object.entries "^1.1.6" + object.fromentries "^2.0.6" + object.hasown "^1.1.2" + object.values "^1.1.6" + prop-types "^15.8.1" + resolve "^2.0.0-next.4" + semver "^6.3.0" + string.prototype.matchall "^4.0.8" eslint-scope@5.1.1, eslint-scope@^5.1.1: version "5.1.1" @@ -8249,12 +8385,13 @@ eslint-scope@5.1.1, eslint-scope@^5.1.1: esrecurse "^4.3.0" estraverse "^4.1.1" -eslint-utils@^2.1.0: - version "2.1.0" - resolved "https://registry.npmjs.org/eslint-utils/-/eslint-utils-2.1.0.tgz" - integrity sha512-w94dQYoauyvlDc43XnGB8lU3Zt713vNChgt4EWwhXAP2XkBvndfxF0AgIqKOOasjPIPzj9JqgwkwbCYD0/V3Zg== +eslint-scope@^7.2.0: + version "7.2.0" + resolved "https://registry.yarnpkg.com/eslint-scope/-/eslint-scope-7.2.0.tgz#f21ebdafda02352f103634b96dd47d9f81ca117b" + integrity sha512-DYj5deGlHBfMt15J7rdtyKNq/Nqlv5KfU4iodrQ019XESsRnwXH9KAE0y3cwtUHDo2ob7CypAnCqefh6vioWRw== dependencies: - eslint-visitor-keys "^1.1.0" + esrecurse "^4.3.0" + estraverse "^5.2.0" eslint-utils@^3.0.0: version "3.0.0" @@ -8263,11 +8400,6 @@ eslint-utils@^3.0.0: dependencies: eslint-visitor-keys "^2.0.0" -eslint-visitor-keys@^1.1.0, eslint-visitor-keys@^1.3.0: - version "1.3.0" - resolved "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-1.3.0.tgz" - integrity sha512-6J72N8UNa462wa/KFODt/PJ3IU60SDpC3QXC1Hjc1BXXpfL2C9R5+AU7jhe0F6GREqVMh4Juu+NY7xn+6dipUQ== - eslint-visitor-keys@^2.0.0: version "2.0.0" resolved "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-2.0.0.tgz" @@ -8278,67 +8410,74 @@ eslint-visitor-keys@^3.0.0: resolved "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.3.0.tgz" integrity sha512-mQ+suqKJVyeuwGYHAdjMFqjCyfl8+Ldnxuyp3ldiMBFKkvytrXUZWaiPCEav8qDHKty44bD+qV1IP4T+w+xXRA== -eslint@^7.11.0: - version "7.11.0" - resolved "https://registry.npmjs.org/eslint/-/eslint-7.11.0.tgz" - integrity sha512-G9+qtYVCHaDi1ZuWzBsOWo2wSwd70TXnU6UHA3cTYHp7gCTXZcpggWFoUVAMRarg68qtPoNfFbzPh+VdOgmwmw== - dependencies: - "@babel/code-frame" "^7.0.0" - "@eslint/eslintrc" "^0.1.3" +eslint-visitor-keys@^3.3.0, eslint-visitor-keys@^3.4.1: + version "3.4.1" + resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-3.4.1.tgz#c22c48f48942d08ca824cc526211ae400478a994" + integrity sha512-pZnmmLwYzf+kWaM/Qgrvpen51upAktaaiI01nsJD/Yr3lMOdNtq0cxkrrg16w64VtisN6okbs7Q8AfGqj4c9fA== + +eslint@^8.0.1: + version "8.41.0" + resolved "https://registry.yarnpkg.com/eslint/-/eslint-8.41.0.tgz#3062ca73363b4714b16dbc1e60f035e6134b6f1c" + integrity sha512-WQDQpzGBOP5IrXPo4Hc0814r4/v2rrIsB0rhT7jtunIalgg6gYXWhRMOejVO8yH21T/FGaxjmFjBMNqcIlmH1Q== + dependencies: + "@eslint-community/eslint-utils" "^4.2.0" + "@eslint-community/regexpp" "^4.4.0" + "@eslint/eslintrc" "^2.0.3" + "@eslint/js" "8.41.0" + "@humanwhocodes/config-array" "^0.11.8" + "@humanwhocodes/module-importer" "^1.0.1" + "@nodelib/fs.walk" "^1.2.8" ajv "^6.10.0" chalk "^4.0.0" cross-spawn "^7.0.2" - debug "^4.0.1" + debug "^4.3.2" doctrine "^3.0.0" - enquirer "^2.3.5" - eslint-scope "^5.1.1" - eslint-utils "^2.1.0" - eslint-visitor-keys "^2.0.0" - espree "^7.3.0" - esquery "^1.2.0" + escape-string-regexp "^4.0.0" + eslint-scope "^7.2.0" + eslint-visitor-keys "^3.4.1" + espree "^9.5.2" + esquery "^1.4.2" esutils "^2.0.2" - file-entry-cache "^5.0.1" - functional-red-black-tree "^1.0.1" - glob-parent "^5.0.0" - globals "^12.1.0" - ignore "^4.0.6" + fast-deep-equal "^3.1.3" + file-entry-cache "^6.0.1" + find-up "^5.0.0" + glob-parent "^6.0.2" + globals "^13.19.0" + graphemer "^1.4.0" + ignore "^5.2.0" import-fresh "^3.0.0" imurmurhash "^0.1.4" is-glob "^4.0.0" - js-yaml "^3.13.1" + is-path-inside "^3.0.3" + js-yaml "^4.1.0" json-stable-stringify-without-jsonify "^1.0.1" levn "^0.4.1" - lodash "^4.17.19" - minimatch "^3.0.4" + lodash.merge "^4.6.2" + minimatch "^3.1.2" natural-compare "^1.4.0" optionator "^0.9.1" - progress "^2.0.0" - regexpp "^3.1.0" - semver "^7.2.1" - strip-ansi "^6.0.0" + strip-ansi "^6.0.1" strip-json-comments "^3.1.0" - table "^5.2.3" text-table "^0.2.0" - v8-compile-cache "^2.0.3" -espree@^7.3.0: - version "7.3.0" - resolved "https://registry.npmjs.org/espree/-/espree-7.3.0.tgz" - integrity sha512-dksIWsvKCixn1yrEXO8UosNSxaDoSYpq9reEjZSbHLpT5hpaCAKTLBwq0RHtLrIr+c0ByiYzWT8KTMRzoRCNlw== +espree@^9.5.2: + version "9.5.2" + resolved "https://registry.yarnpkg.com/espree/-/espree-9.5.2.tgz#e994e7dc33a082a7a82dceaf12883a829353215b" + integrity sha512-7OASN1Wma5fum5SrNhFMAMJxOUAbhyfQ8dQ//PJaJbNw0URTPWqIghHWt1MmAANKhHZIYOHruW4Kw4ruUWOdGw== dependencies: - acorn "^7.4.0" - acorn-jsx "^5.2.0" - eslint-visitor-keys "^1.3.0" + acorn "^8.8.0" + acorn-jsx "^5.3.2" + eslint-visitor-keys "^3.4.1" esprima@^4.0.0, esprima@^4.0.1: version "4.0.1" resolved "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz" integrity sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A== -esquery@^1.2.0: - version "1.3.1" - resolved "https://registry.npmjs.org/esquery/-/esquery-1.3.1.tgz" - integrity sha512-olpvt9QG0vniUBZspVRN6lwB7hOZoTRtT+jzR+tS4ffYx2mzbw+z0XCOk44aaLYKApNX5nMm+E+P6o25ip/DHQ== +esquery@^1.4.2: + version "1.5.0" + resolved "https://registry.yarnpkg.com/esquery/-/esquery-1.5.0.tgz#6ce17738de8577694edd7361c57182ac8cb0db0b" + integrity sha512-YQLXUplAwJgCydQ78IMJywZCceoqk1oH01OERdSAJc/7U2AylwjhSCLDEtqwg811idIS/9fIU5GjG73IgjKMVg== dependencies: estraverse "^5.1.0" @@ -8359,7 +8498,7 @@ estraverse@^5.1.0: resolved "https://registry.npmjs.org/estraverse/-/estraverse-5.2.0.tgz" integrity sha512-BxbNGGNm0RyRYvUdHpIwv9IWzeM9XClbOxwoATuFdOE7ZE6wHL+HQ5T8hoPM+zHvmKzzsEqhgy0GrQ5X13afiQ== -estraverse@^5.2.0: +estraverse@^5.2.0, estraverse@^5.3.0: version "5.3.0" resolved "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz" integrity sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA== @@ -8792,12 +8931,12 @@ figures@^3.0.0, figures@^3.2.0: dependencies: escape-string-regexp "^1.0.5" -file-entry-cache@^5.0.1: - version "5.0.1" - resolved "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-5.0.1.tgz" - integrity sha512-bCg29ictuBaKUwwArK4ouCaqDgLZcysCFLmM/Yn/FDoqndh/9vNuQfXRDvTuXKLxfD/JtZQGKFT8MGcJBK644g== +file-entry-cache@^6.0.1: + version "6.0.1" + resolved "https://registry.yarnpkg.com/file-entry-cache/-/file-entry-cache-6.0.1.tgz#211b2dd9659cb0394b073e7323ac3c933d522027" + integrity sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg== dependencies: - flat-cache "^2.0.1" + flat-cache "^3.0.4" file-selector@^0.2.2: version "0.2.4" @@ -8881,7 +9020,7 @@ find-root@1.1.0: resolved "https://registry.npmjs.org/find-root/-/find-root-1.1.0.tgz" integrity sha512-NKfW6bec6GfKc0SGx1e07QZY9PE99u0Bft/0rzSD5k3sO/vwkVUpDUKVm5Gpp5Ue3YfShPFTX2070tDs5kB9Ng== -find-up@^2.0.0, find-up@^2.1.0: +find-up@^2.0.0: version "2.1.0" resolved "https://registry.npmjs.org/find-up/-/find-up-2.1.0.tgz" integrity sha1-RdG35QbHF93UgndaK3eSCjwMV6c= @@ -8935,19 +9074,18 @@ fishery@^0.3.0: dependencies: lodash.merge "^4.6.2" -flat-cache@^2.0.1: - version "2.0.1" - resolved "https://registry.npmjs.org/flat-cache/-/flat-cache-2.0.1.tgz" - integrity sha512-LoQe6yDuUMDzQAEH8sgmh4Md6oZnc/7PjtwjNFSzveXqSHt6ka9fPBuso7IGf9Rz4uqnSnWiFH2B/zj24a5ReA== +flat-cache@^3.0.4: + version "3.0.4" + resolved "https://registry.yarnpkg.com/flat-cache/-/flat-cache-3.0.4.tgz#61b0338302b2fe9f957dcc32fc2a87f1c3048b11" + integrity sha512-dm9s5Pw7Jc0GvMYbshN6zchCA9RgQlzzEZX3vylR9IqFfS8XciblUXOKfW6SiuJ0e13eDYZoZV5wdrev7P3Nwg== dependencies: - flatted "^2.0.0" - rimraf "2.6.3" - write "1.0.3" + flatted "^3.1.0" + rimraf "^3.0.2" -flatted@^2.0.0: - version "2.0.2" - resolved "https://registry.npmjs.org/flatted/-/flatted-2.0.2.tgz" - integrity sha512-r5wGx7YeOwNWNlCA0wQ86zKyDLMQr+/RB8xy74M4hTphfmjlijTSSXGuH8rnvKZnfT9i+75zmd8jcKdMR4O6jA== +flatted@^3.1.0: + version "3.2.7" + resolved "https://registry.yarnpkg.com/flatted/-/flatted-3.2.7.tgz#609f39207cb614b89d0765b477cb2d437fbf9787" + integrity sha512-5nqDSxl8nn5BSNxyR3n4I6eDmbolI6WT+QqR547RwxQapgjQBmtktdP+HTBb/a/zLsbzERTONyUB5pefh5TtjQ== focus-lock@^0.11.6: version "0.11.6" @@ -9284,6 +9422,16 @@ get-intrinsic@^1.1.3: has "^1.0.3" has-symbols "^1.0.3" +get-intrinsic@^1.2.0: + version "1.2.1" + resolved "https://registry.yarnpkg.com/get-intrinsic/-/get-intrinsic-1.2.1.tgz#d295644fed4505fc9cde952c37ee12b477a83d82" + integrity sha512-2DcsyfABl+gVHEfCOaTrWgyt+tb6MSEGmKq+kI5HwLbIYgjgmMcV8KQ41uaKz1xxUcn9tJtgFbQUEVcEbd0FYw== + dependencies: + function-bind "^1.1.1" + has "^1.0.3" + has-proto "^1.0.1" + has-symbols "^1.0.3" + get-nonce@^1.0.0: version "1.0.1" resolved "https://registry.npmjs.org/get-nonce/-/get-nonce-1.0.1.tgz" @@ -9326,11 +9474,6 @@ get-port@^5.1.1: resolved "https://registry.npmjs.org/get-port/-/get-port-5.1.1.tgz" integrity sha512-g/Q1aTSDOxFpchXC4i8ZWvxA1lnPqx/JHqcpIw0/LX9T8x/GBbi6YnlN5nhaKIFkT8oFsscUKgDJYxfwfS6QsQ== -get-stdin@^6.0.0: - version "6.0.0" - resolved "https://registry.npmjs.org/get-stdin/-/get-stdin-6.0.0.tgz" - integrity sha512-jp4tHawyV7+fkkSKyvjuLZswblUtz+SQKzSWnBbii16BuZksJlU1wuBYXY75r+duh/llF1ur6oNwi+2ZzjKZ7g== - get-stream@3.0.0, get-stream@^3.0.0: version "3.0.0" resolved "https://registry.npmjs.org/get-stream/-/get-stream-3.0.0.tgz" @@ -9441,7 +9584,7 @@ gl-matrix@^3.0.0, gl-matrix@^3.3.0: resolved "https://registry.npmjs.org/gl-matrix/-/gl-matrix-3.3.0.tgz" integrity sha512-COb7LDz+SXaHtl/h4LeaFcNdJdAQSDeVqjiIihSXNrkWObZLhDI4hIkZC11Aeqp7bcE72clzB0BnDXr2SmslRA== -glob-parent@^5.0.0, glob-parent@^5.1.0: +glob-parent@^5.1.0: version "5.1.1" resolved "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.1.tgz" integrity sha512-FnI+VGOpnlGHWZxthPGR+QhR78fuiK0sNLkHQv+bL9fQi57lNNdquIbna/WrfROrolq8GK5Ek6BiMwqL/voRYQ== @@ -9462,6 +9605,13 @@ glob-parent@^6.0.1: dependencies: is-glob "^4.0.1" +glob-parent@^6.0.2: + version "6.0.2" + resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-6.0.2.tgz#6d237d99083950c79290f24c7642a3de9a28f9e3" + integrity sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A== + dependencies: + is-glob "^4.0.3" + glob-to-regexp@^0.4.1: version "0.4.1" resolved "https://registry.npmjs.org/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz" @@ -9563,16 +9713,16 @@ globals@^11.1.0: resolved "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz" integrity sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA== -globals@^12.1.0: - version "12.4.0" - resolved "https://registry.npmjs.org/globals/-/globals-12.4.0.tgz" - integrity sha512-BWICuzzDvDoH54NHKCseDanAhE3CeDorgDL5MT6LMXXj2WCnd9UC2szdk4AWLfjdgNBCXLUanXYcpBBKOSWGwg== +globals@^13.19.0: + version "13.20.0" + resolved "https://registry.yarnpkg.com/globals/-/globals-13.20.0.tgz#ea276a1e508ffd4f1612888f9d1bad1e2717bf82" + integrity sha512-Qg5QtVkCy/kv3FUSlu4ukeZDVf9ee0iXLAUYX13gbR17bnejFTzr4iS9bY7kwCf1NztRNm1t91fjOiyx4CSwPQ== dependencies: - type-fest "^0.8.1" + type-fest "^0.20.2" -globalthis@^1.0.1: +globalthis@^1.0.1, globalthis@^1.0.3: version "1.0.3" - resolved "https://registry.npmjs.org/globalthis/-/globalthis-1.0.3.tgz" + resolved "https://registry.yarnpkg.com/globalthis/-/globalthis-1.0.3.tgz#5852882a52b80dc301b0660273e1ed082f0b6ccf" integrity sha512-sFdI5LyBiNTHjRd7cGPWapiHWMOXKyuBNX/cWJ3NfzrZQVa8GI/8cofCl74AOVqq9W5kNmguTIzJ/1s2gyI9wA== dependencies: define-properties "^1.1.3" @@ -9718,6 +9868,11 @@ graceful-fs@^4.2.9: resolved "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.9.tgz" integrity sha512-NtNxqUcXgpW2iMrfqSfR73Glt39K+BLwWsPs94yR63v45T0Wbej7eRmL5cWfwEgqXnmjQp3zaJTshdRW/qC2ZQ== +graphemer@^1.4.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/graphemer/-/graphemer-1.4.0.tgz#fb2f1d55e0e3a1849aeffc90c4fa0dd53a0e66c6" + integrity sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag== + grid-index@^1.1.0: version "1.1.0" resolved "https://registry.npmjs.org/grid-index/-/grid-index-1.1.0.tgz" @@ -9824,6 +9979,11 @@ has-property-descriptors@^1.0.0: dependencies: get-intrinsic "^1.1.1" +has-proto@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/has-proto/-/has-proto-1.0.1.tgz#1885c1305538958aff469fef37937c22795408e0" + integrity sha512-7qE+iP+O+bgF9clE5+UoBFzE65mlBiVj3tKCrlNQ0Ogwm0BjpT/gK4SlLYDMybDh5I3TCTKnPPa0oMG7JDYrhg== + has-symbol-support-x@^1.4.1: version "1.4.2" resolved "https://registry.npmjs.org/has-symbol-support-x/-/has-symbol-support-x-1.4.2.tgz" @@ -10134,11 +10294,6 @@ ignore-walk@^3.0.3: dependencies: minimatch "^3.0.4" -ignore@^4.0.6: - version "4.0.6" - resolved "https://registry.npmjs.org/ignore/-/ignore-4.0.6.tgz" - integrity sha512-cyFDKrqc/YdcWFniJhzI42+AzS+gNwmUzOSFcRCQYwySuBBBy/KjuxWLZ/FHEH6Moq1NizMOBWyTcv8O4OZIMg== - ignore@^5.1.4: version "5.1.8" resolved "https://registry.npmjs.org/ignore/-/ignore-5.1.8.tgz" @@ -10349,15 +10504,6 @@ inquirer@^8.2.0: through "^2.3.6" wrap-ansi "^7.0.0" -internal-slot@^1.0.2: - version "1.0.2" - resolved "https://registry.npmjs.org/internal-slot/-/internal-slot-1.0.2.tgz" - integrity sha512-2cQNfwhAfJIkU4KZPkDI+Gj5yNNnbqi40W9Gge6dfnk4TocEVm00B3bdiL+JINrbGJil2TeHvM4rETGzk/f/0g== - dependencies: - es-abstract "^1.17.0-next.1" - has "^1.0.3" - side-channel "^1.0.2" - internal-slot@^1.0.3: version "1.0.3" resolved "https://registry.npmjs.org/internal-slot/-/internal-slot-1.0.3.tgz" @@ -10367,6 +10513,15 @@ internal-slot@^1.0.3: has "^1.0.3" side-channel "^1.0.4" +internal-slot@^1.0.5: + version "1.0.5" + resolved "https://registry.yarnpkg.com/internal-slot/-/internal-slot-1.0.5.tgz#f2a2ee21f668f8627a4667f309dc0f4fb6674986" + integrity sha512-Y+R5hJrzs52QCG2laLn4udYVnxsfny9CpOhNhUvk/SSSVyF6T27FzRbF0sroPidSu3X8oEAkOn2K804mjpt6UQ== + dependencies: + get-intrinsic "^1.2.0" + has "^1.0.3" + side-channel "^1.0.4" + interpret@^1.0.0: version "1.4.0" resolved "https://registry.npmjs.org/interpret/-/interpret-1.4.0.tgz" @@ -10477,6 +10632,15 @@ is-array-buffer@^3.0.1: get-intrinsic "^1.1.3" is-typed-array "^1.1.10" +is-array-buffer@^3.0.2: + version "3.0.2" + resolved "https://registry.yarnpkg.com/is-array-buffer/-/is-array-buffer-3.0.2.tgz#f2653ced8412081638ecb0ebbd0c41c6e0aecbbe" + integrity sha512-y+FyyR/w8vfIRq4eQcM1EYgSTnmHXPqaF+IgzgraytCFq5Xh8lllDVmAZolPJiZttZLeFSINPYMaEJ7/vWUa1w== + dependencies: + call-bind "^1.0.2" + get-intrinsic "^1.2.0" + is-typed-array "^1.1.10" + is-arrayish@^0.2.1: version "0.2.1" resolved "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz" @@ -10531,6 +10695,11 @@ is-callable@^1.2.4: resolved "https://registry.npmjs.org/is-callable/-/is-callable-1.2.4.tgz" integrity sha512-nsuwtxZfMX67Oryl9LCQ+upnC0Z0BgpwntpS89m1H/TLF0zNfzfLMV/9Wa/6MZsj0acpEjAO0KF1xT6ZdLl95w== +is-callable@^1.2.7: + version "1.2.7" + resolved "https://registry.yarnpkg.com/is-callable/-/is-callable-1.2.7.tgz#3bc2a85ea742d9e36205dcacdd72ca1fdc51b055" + integrity sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA== + is-ci@^2.0.0: version "2.0.0" resolved "https://registry.npmjs.org/is-ci/-/is-ci-2.0.0.tgz" @@ -10545,6 +10714,13 @@ is-ci@^3.0.0: dependencies: ci-info "^3.2.0" +is-core-module@^2.11.0, is-core-module@^2.9.0: + version "2.12.1" + resolved "https://registry.yarnpkg.com/is-core-module/-/is-core-module-2.12.1.tgz#0c0b6885b6f80011c71541ce15c8d66cf5a4f9fd" + integrity sha512-Q4ZuBAe2FUsKtyQJoQHlvP8OvBERxO3jEmy1I7hcRXcJBGGHFh/aJBswbXuS9sgrDH2QUO8ilkwNPHvHMd8clg== + dependencies: + has "^1.0.3" + is-core-module@^2.2.0: version "2.4.0" resolved "https://registry.npmjs.org/is-core-module/-/is-core-module-2.4.0.tgz" @@ -10753,9 +10929,9 @@ is-object@^1.0.1: resolved "https://registry.npmjs.org/is-object/-/is-object-1.0.2.tgz" integrity sha512-2rRIahhZr2UWb45fIOuvZGpFtz0TyOZLf32KxBbSoUCeZR495zCKlWUKKUByk3geS2eAs7ZAABt0Y/Rx0GiQGA== -is-path-inside@^3.0.2: +is-path-inside@^3.0.2, is-path-inside@^3.0.3: version "3.0.3" - resolved "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz" + resolved "https://registry.yarnpkg.com/is-path-inside/-/is-path-inside-3.0.3.tgz#d231362e53a07ff2b0e0ea7fed049161ffd16283" integrity sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ== is-plain-obj@^1.0.0, is-plain-obj@^1.1.0: @@ -10888,7 +11064,7 @@ is-text-path@^1.0.1: dependencies: text-extensions "^1.0.0" -is-typed-array@^1.1.10: +is-typed-array@^1.1.10, is-typed-array@^1.1.9: version "1.1.10" resolved "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.10.tgz" integrity sha512-PJqgEHiWZvMpaFZ3uTc8kHPM4+4ADTlDniuQL7cU/UDA0Ql7F70yGfHph3cLNe+c9toaigv+DFzTJKhc2CtO6A== @@ -10957,7 +11133,7 @@ is-wsl@^2.2.0: dependencies: is-docker "^2.0.0" -isarray@1.0.0, isarray@^1.0.0, isarray@~1.0.0: +isarray@1.0.0, isarray@~1.0.0: version "1.0.0" resolved "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz" integrity sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE= @@ -12029,9 +12205,9 @@ json5@2.x: resolved "https://registry.npmjs.org/json5/-/json5-2.2.1.tgz" integrity sha512-1hqLFMSrGHRHxav9q9gNjJ5EXznIxGVO09xQRrwplcS8qs28pZ8s8hupZAmqDwZUmVZ2Qb2jnyPOWcDH8m8dlA== -json5@^1.0.1: +json5@^1.0.1, json5@^1.0.2: version "1.0.2" - resolved "https://registry.npmjs.org/json5/-/json5-1.0.2.tgz" + resolved "https://registry.yarnpkg.com/json5/-/json5-1.0.2.tgz#63d98d60f21b313b77c4d6da18bfa69d80e1d593" integrity sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA== dependencies: minimist "^1.2.0" @@ -12417,16 +12593,6 @@ listr2@^3.8.3: through "^2.3.8" wrap-ansi "^7.0.0" -load-json-file@^2.0.0: - version "2.0.0" - resolved "https://registry.npmjs.org/load-json-file/-/load-json-file-2.0.0.tgz" - integrity sha1-eUfkIUmvgNaWy/eXvKq8/h/inKg= - dependencies: - graceful-fs "^4.1.2" - parse-json "^2.2.0" - pify "^2.0.0" - strip-bom "^3.0.0" - load-json-file@^4.0.0: version "4.0.0" resolved "https://registry.npmjs.org/load-json-file/-/load-json-file-4.0.0.tgz" @@ -12605,7 +12771,7 @@ lodash.topath@^4.5.2: resolved "https://registry.npmjs.org/lodash.topath/-/lodash.topath-4.5.2.tgz" integrity sha1-NhY1Hzu6YZlKCTGYlmC9AyVP0Ak= -lodash@4.17.21, lodash@4.x, lodash@^4.17.10, lodash@^4.17.11, lodash@^4.17.12, lodash@^4.17.14, lodash@^4.17.15, lodash@^4.17.19, lodash@^4.17.20, lodash@^4.17.21, lodash@^4.7.0: +lodash@4.17.21, lodash@4.x, lodash@^4.17.10, lodash@^4.17.11, lodash@^4.17.12, lodash@^4.17.15, lodash@^4.17.19, lodash@^4.17.20, lodash@^4.17.21, lodash@^4.7.0: version "4.17.21" resolved "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz" integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg== @@ -13068,6 +13234,13 @@ minimatch@^3.0.4: dependencies: brace-expansion "^1.1.7" +minimatch@^3.0.5, minimatch@^3.1.2: + version "3.1.2" + resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.1.2.tgz#19cd194bfd3e428f049a70817c038d89ab4be35b" + integrity sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw== + dependencies: + brace-expansion "^1.1.7" + minimatch@^5.0.1: version "5.1.6" resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-5.1.6.tgz#1cfcb8cf5522ea69952cd2af95ae09477f122a96" @@ -13919,6 +14092,11 @@ object-inspect@^1.12.0, object-inspect@^1.9.0: resolved "https://registry.npmjs.org/object-inspect/-/object-inspect-1.12.2.tgz" integrity sha512-z+cPxW0QGUp0mcqcsgQyLVRDoXFQbXOwBaqyF7VIgI4TWNQsDHrBpUQslRmIfAoYWdYzs6UlKJtB2XJpTaNSpQ== +object-inspect@^1.12.3: + version "1.12.3" + resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.12.3.tgz#ba62dffd67ee256c8c086dfae69e016cd1f198b9" + integrity sha512-geUvdk7c+eizMNUDkRpW1wJwgfOiOeHbxBR/hLXK1aT6zmVSO0jsQcs7fj6MGw89jC/cjGfLcNOrtMYtGqm81g== + object-inspect@^1.7.0, object-inspect@^1.8.0: version "1.8.0" resolved "https://registry.npmjs.org/object-inspect/-/object-inspect-1.8.0.tgz" @@ -13984,24 +14162,23 @@ object.assign@^4.1.3, object.assign@^4.1.4: has-symbols "^1.0.3" object-keys "^1.1.1" -object.entries@^1.1.2: - version "1.1.2" - resolved "https://registry.npmjs.org/object.entries/-/object.entries-1.1.2.tgz" - integrity sha512-BQdB9qKmb/HyNdMNWVr7O3+z5MUIx3aiegEIJqjMBbBf0YT9RRxTJSim4mzFqtyr7PDAHigq0N9dO0m0tRakQA== +object.entries@^1.1.6: + version "1.1.6" + resolved "https://registry.yarnpkg.com/object.entries/-/object.entries-1.1.6.tgz#9737d0e5b8291edd340a3e3264bb8a3b00d5fa23" + integrity sha512-leTPzo4Zvg3pmbQ3rDK69Rl8GQvIqMWubrkxONG9/ojtFE2rD9fjMKfSI5BxW3osRH1m6VdzmqK8oAY9aT4x5w== dependencies: - define-properties "^1.1.3" - es-abstract "^1.17.5" - has "^1.0.3" + call-bind "^1.0.2" + define-properties "^1.1.4" + es-abstract "^1.20.4" -object.fromentries@^2.0.2: - version "2.0.2" - resolved "https://registry.npmjs.org/object.fromentries/-/object.fromentries-2.0.2.tgz" - integrity sha512-r3ZiBH7MQppDJVLx6fhD618GKNG40CZYH9wgwdhKxBDDbQgjeWGGd4AtkZad84d291YxvWe7bJGuE65Anh0dxQ== +object.fromentries@^2.0.6: + version "2.0.6" + resolved "https://registry.yarnpkg.com/object.fromentries/-/object.fromentries-2.0.6.tgz#cdb04da08c539cffa912dcd368b886e0904bfa73" + integrity sha512-VciD13dswC4j1Xt5394WR4MzmAQmlgN72phd/riNp9vtD7tp4QQWJ0R4wvclXcafgcYK8veHRed2W6XeGBvcfg== dependencies: - define-properties "^1.1.3" - es-abstract "^1.17.0-next.1" - function-bind "^1.1.1" - has "^1.0.3" + call-bind "^1.0.2" + define-properties "^1.1.4" + es-abstract "^1.20.4" object.getownpropertydescriptors@^2.0.3: version "2.1.0" @@ -14011,6 +14188,14 @@ object.getownpropertydescriptors@^2.0.3: define-properties "^1.1.3" es-abstract "^1.17.0-next.1" +object.hasown@^1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/object.hasown/-/object.hasown-1.1.2.tgz#f919e21fad4eb38a57bc6345b3afd496515c3f92" + integrity sha512-B5UIT3J1W+WuWIU55h0mjlwaqxiE5vYENJXIXZ4VFe05pNYrkKuK0U/6aFcb0pKywYJh7IhfoqUfKVmrJJHZHw== + dependencies: + define-properties "^1.1.4" + es-abstract "^1.20.4" + object.pick@^1.3.0: version "1.3.0" resolved "https://registry.npmjs.org/object.pick/-/object.pick-1.3.0.tgz" @@ -14018,15 +14203,14 @@ object.pick@^1.3.0: dependencies: isobject "^3.0.1" -object.values@^1.1.1: - version "1.1.1" - resolved "https://registry.npmjs.org/object.values/-/object.values-1.1.1.tgz" - integrity sha512-WTa54g2K8iu0kmS/us18jEmdv1a4Wi//BZ/DTVYEcH0XhLM5NYdpDHja3gt57VrZLcNAO2WGA+KpWsDBaHt6eA== +object.values@^1.1.6: + version "1.1.6" + resolved "https://registry.yarnpkg.com/object.values/-/object.values-1.1.6.tgz#4abbaa71eba47d63589d402856f908243eea9b1d" + integrity sha512-FVVTkD1vENCsAcwNs9k6jea2uHC/X0+JcjG8YA60FN5CMaJmG95wT9jek/xX9nornqGRrBkKtzuAu2wuHpKqvw== dependencies: - define-properties "^1.1.3" - es-abstract "^1.17.0-next.1" - function-bind "^1.1.1" - has "^1.0.3" + call-bind "^1.0.2" + define-properties "^1.1.4" + es-abstract "^1.20.4" ofetch@^1.0.1: version "1.0.1" @@ -14391,13 +14575,6 @@ parent-module@^1.0.0: dependencies: callsites "^3.0.0" -parse-json@^2.2.0: - version "2.2.0" - resolved "https://registry.npmjs.org/parse-json/-/parse-json-2.2.0.tgz" - integrity sha1-9ID0BDTvgHQfhGkJn43qGPVaTck= - dependencies: - error-ex "^1.2.0" - parse-json@^4.0.0: version "4.0.0" resolved "https://registry.npmjs.org/parse-json/-/parse-json-4.0.0.tgz" @@ -14539,7 +14716,7 @@ path-key@^3.0.0, path-key@^3.1.0: resolved "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz" integrity sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q== -path-parse@^1.0.6: +path-parse@^1.0.6, path-parse@^1.0.7: version "1.0.7" resolved "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz" integrity sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw== @@ -14559,13 +14736,6 @@ path-to-regexp@^6.2.0: resolved "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-6.2.1.tgz" integrity sha512-JLyh7xT1kizaEvcaXOQwOc2/Yhw6KZOvPf1S8401UyLk86CU79LN3vl7ztXGm/pZ+YjoyAJ4rxmHwbkBXJX+yw== -path-type@^2.0.0: - version "2.0.0" - resolved "https://registry.npmjs.org/path-type/-/path-type-2.0.0.tgz" - integrity sha1-8BLMuEFbcJb8LaoQVMPXI4lZTHM= - dependencies: - pify "^2.0.0" - path-type@^3.0.0: version "3.0.0" resolved "https://registry.npmjs.org/path-type/-/path-type-3.0.0.tgz" @@ -14682,7 +14852,7 @@ picomatch@^2.2.3: resolved "https://registry.npmjs.org/picomatch/-/picomatch-2.3.0.tgz" integrity sha512-lY1Q/PiJGC2zOv/z391WOTD+Z02bCgsFfvxoXXf6h7kv9o+WmsmzYqrAwY63sNgOxE4xEdq0WyUnXfKeBrSvYw== -pify@^2.0.0, pify@^2.2.0, pify@^2.3.0: +pify@^2.2.0, pify@^2.3.0: version "2.3.0" resolved "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz" integrity sha1-7RQaasBDqEnqWISY59yosVMw6Qw= @@ -14714,13 +14884,6 @@ pirates@^4.0.4: resolved "https://registry.yarnpkg.com/pirates/-/pirates-4.0.5.tgz#feec352ea5c3268fb23a37c702ab1699f35a5f3b" integrity sha512-8V9+HQPupnaXMA23c5hvl69zXvTwTzyAYasnkb0Tts4XvO4CliqONMOnvlq26rkhLC3nWDFBJf73LU1e1VZLaQ== -pkg-dir@^2.0.0: - version "2.0.0" - resolved "https://registry.npmjs.org/pkg-dir/-/pkg-dir-2.0.0.tgz" - integrity sha1-9tXREJ4Z1j7fQo4L1X4Sd3YVM0s= - dependencies: - find-up "^2.1.0" - pkg-dir@^4.1.0, pkg-dir@^4.2.0: version "4.2.0" resolved "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz" @@ -14898,10 +15061,10 @@ prettier@^1.15.2: resolved "https://registry.npmjs.org/prettier/-/prettier-1.19.1.tgz" integrity sha512-s7PoyDv/II1ObgQunCbB9PdLmUcBZcnWOcxDh7O0N/UwDEsHyqkW+Qh28jW+mVuCdx7gLB0BotYI1Y6uI9iyew== -prettier@^2.1.0: - version "2.1.0" - resolved "https://registry.npmjs.org/prettier/-/prettier-2.1.0.tgz" - integrity sha512-lz28cCbA1cDFHVuY8vvj6QuqOwIpyIfPUYkSl8AZ/vxH8qBXMMjE2knfLHCrZCmUsK/H1bg1P0tOo0dJkTJHvw== +prettier@^2.8.8: + version "2.8.8" + resolved "https://registry.yarnpkg.com/prettier/-/prettier-2.8.8.tgz#e8c5d7e98a4305ffe3de2e1fc4aca1a71c28b1da" + integrity sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q== pretty-bytes@^5.6.0: version "5.6.0" @@ -14983,9 +15146,9 @@ process@^0.11.10: resolved "https://registry.npmjs.org/process/-/process-0.11.10.tgz" integrity sha1-czIwDoQBYb2j5podHZGn1LwW8YI= -progress@^2.0.0, progress@^2.0.3: +progress@^2.0.3: version "2.0.3" - resolved "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz" + resolved "https://registry.yarnpkg.com/progress/-/progress-2.0.3.tgz#7e8cf8d8f5b8f239c1bc68beb4eb78567d572ef8" integrity sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA== promise-inflight@^1.0.1: @@ -15505,14 +15668,6 @@ read-package-tree@^5.3.1: readdir-scoped-modules "^1.0.0" util-promisify "^2.1.0" -read-pkg-up@^2.0.0: - version "2.0.0" - resolved "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-2.0.0.tgz" - integrity sha1-a3KoBImE4MQeeVEP1en6mbO1Sb4= - dependencies: - find-up "^2.0.0" - read-pkg "^2.0.0" - read-pkg-up@^3.0.0: version "3.0.0" resolved "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-3.0.0.tgz" @@ -15530,15 +15685,6 @@ read-pkg-up@^7.0.1: read-pkg "^5.2.0" type-fest "^0.8.1" -read-pkg@^2.0.0: - version "2.0.0" - resolved "https://registry.npmjs.org/read-pkg/-/read-pkg-2.0.0.tgz" - integrity sha1-jvHAYjxqbbDcZxPEv6xGMysjaPg= - dependencies: - load-json-file "^2.0.0" - normalize-package-data "^2.3.2" - path-type "^2.0.0" - read-pkg@^3.0.0: version "3.0.0" resolved "https://registry.npmjs.org/read-pkg/-/read-pkg-3.0.0.tgz" @@ -15709,14 +15855,6 @@ regexp-tree@^0.1.24: resolved "https://registry.npmjs.org/regexp-tree/-/regexp-tree-0.1.24.tgz" integrity sha512-s2aEVuLhvnVJW6s/iPgEGK6R+/xngd2jNQ+xy4bXNDKxZKJH6jpPHY6kVeVv1IeLCHgswRj+Kl3ELaDjG6V1iw== -regexp.prototype.flags@^1.3.0: - version "1.3.0" - resolved "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.3.0.tgz" - integrity sha512-2+Q0C5g951OlYlJz6yu5/M33IcsESLlLfsyIaLJaG4FA2r4yP8MvVMJUUP/fVBkSpbbbZlS5gynbEWLipiiXiQ== - dependencies: - define-properties "^1.1.3" - es-abstract "^1.17.0-next.1" - regexp.prototype.flags@^1.4.3: version "1.4.3" resolved "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.4.3.tgz" @@ -15726,11 +15864,6 @@ regexp.prototype.flags@^1.4.3: define-properties "^1.1.3" functions-have-names "^1.2.2" -regexpp@^3.1.0: - version "3.1.0" - resolved "https://registry.npmjs.org/regexpp/-/regexpp-3.1.0.tgz" - integrity sha512-ZOIzd8yVsQQA7j8GCSlPGXwg5PfmA1mrq0JP4nGhh54LaKN3xdai/vHUDu74pKwV8OxseMS65u2NImosQcSD0Q== - regexpp@^3.2.0: version "3.2.0" resolved "https://registry.npmjs.org/regexpp/-/regexpp-3.2.0.tgz" @@ -15913,7 +16046,7 @@ resolve.exports@^2.0.0: resolved "https://registry.yarnpkg.com/resolve.exports/-/resolve.exports-2.0.2.tgz#f8c934b8e6a13f539e38b7098e2e36134f01e800" integrity sha512-X2UW6Nw3n/aMgDVy+0rSqgHlv39WZAlZrXCdnbyEiKm17DSqHX4MmQMaST3FbeWR5FTuRcUwYAziZajji0Y7mg== -resolve@^1.1.6, resolve@^1.10.0, resolve@^1.13.1, resolve@^1.14.2, resolve@^1.17.0: +resolve@^1.1.6, resolve@^1.10.0, resolve@^1.14.2, resolve@^1.17.0: version "1.17.0" resolved "https://registry.npmjs.org/resolve/-/resolve-1.17.0.tgz" integrity sha512-ic+7JYiV8Vi2yzQGFWOkiZD5Z9z7O2Zhm9XMaTxdJExKasieFCr+yXZ/WmXsckHiKl12ar0y6XiXDx3m4RHn1w== @@ -15928,6 +16061,24 @@ resolve@^1.20.0: is-core-module "^2.2.0" path-parse "^1.0.6" +resolve@^1.22.1: + version "1.22.2" + resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.22.2.tgz#0ed0943d4e301867955766c9f3e1ae6d01c6845f" + integrity sha512-Sb+mjNHOULsBv818T40qSPeRiuWLyaGMa5ewydRLFimneixmVy2zdivRl+AF6jaYPC8ERxGDmFSiqui6SfPd+g== + dependencies: + is-core-module "^2.11.0" + path-parse "^1.0.7" + supports-preserve-symlinks-flag "^1.0.0" + +resolve@^2.0.0-next.4: + version "2.0.0-next.4" + resolved "https://registry.yarnpkg.com/resolve/-/resolve-2.0.0-next.4.tgz#3d37a113d6429f496ec4752d2a2e58efb1fd4660" + integrity sha512-iMDbmAWtfU+MHpxt/I5iWI7cY6YVEZUQ3MBgPQ++XD1PELuJHIl82xBmObyP2KyQmkNB2dsqF7seoQQiAn5yDQ== + dependencies: + is-core-module "^2.9.0" + path-parse "^1.0.7" + supports-preserve-symlinks-flag "^1.0.0" + responselike@1.0.2, responselike@^1.0.2: version "1.0.2" resolved "https://registry.npmjs.org/responselike/-/responselike-1.0.2.tgz" @@ -15979,13 +16130,6 @@ rfdc@^1.3.0: resolved "https://registry.npmjs.org/rfdc/-/rfdc-1.3.0.tgz" integrity sha512-V2hovdzFbOi77/WajaSMXk2OLm+xNIeQdMMuB7icj7bk6zi2F8GGAxigcnDFpJHbNyNcgyJDiP+8nOrY5cZGrA== -rimraf@2.6.3: - version "2.6.3" - resolved "https://registry.npmjs.org/rimraf/-/rimraf-2.6.3.tgz" - integrity sha512-mwqeW5XsA2qAejG46gYdENaxXjx9onRNCfn7L0duuP4hCuTIi/QO7PDK07KJfp1d+izWPrzEJDcSqBa0OZQriA== - dependencies: - glob "^7.1.3" - rimraf@3.0.2, rimraf@^3.0.0, rimraf@^3.0.2: version "3.0.2" resolved "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz" @@ -16082,6 +16226,15 @@ safe-buffer@~5.1.0, safe-buffer@~5.1.1: resolved "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz" integrity sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g== +safe-regex-test@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/safe-regex-test/-/safe-regex-test-1.0.0.tgz#793b874d524eb3640d1873aad03596db2d4f2295" + integrity sha512-JBUUzyOgEwXQY1NuPtvcj/qcBDbDmEvWufhlnXZIm75DEHp+afM1r1ujJpJsV/gSM4t59tpDyPi1sd6ZaPFfsA== + dependencies: + call-bind "^1.0.2" + get-intrinsic "^1.1.3" + is-regex "^1.1.4" + safe-regex@^1.1.0: version "1.1.0" resolved "https://registry.npmjs.org/safe-regex/-/safe-regex-1.1.0.tgz" @@ -16236,7 +16389,7 @@ semver@^6.0.0, semver@^6.1.1, semver@^6.1.2, semver@^6.2.0, semver@^6.3.0: resolved "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz" integrity sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw== -semver@^7.2.1, semver@^7.3.2: +semver@^7.3.2: version "7.3.4" resolved "https://registry.npmjs.org/semver/-/semver-7.3.4.tgz" integrity sha512-tCfb2WLjqFAtXn4KEdxIhalnRtoKFN7nAwj0B3ZXCbQloV2tq5eDbcTmT68JJD3nRJq24/XgxtQKFIpQdtvmVw== @@ -16419,14 +16572,6 @@ shellwords@^0.1.1: resolved "https://registry.npmjs.org/shellwords/-/shellwords-0.1.1.tgz" integrity sha512-vFwSUfQvqybiICwZY5+DAWIPLKsWO31Q91JSKl3UYv+K5c2QRPzn0qzec6QPu1Qc9eHYItiP3NdJqNVqetYAww== -side-channel@^1.0.2: - version "1.0.3" - resolved "https://registry.npmjs.org/side-channel/-/side-channel-1.0.3.tgz" - integrity sha512-A6+ByhlLkksFoUepsGxfj5x1gTSrs+OydsRptUxeNCabQpCFUvcwIczgOigI8vhY/OJCnPnyE9rGiwgvr9cS1g== - dependencies: - es-abstract "^1.18.0-next.0" - object-inspect "^1.8.0" - side-channel@^1.0.4: version "1.0.4" resolved "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz" @@ -16491,15 +16636,6 @@ slash@^3.0.0: resolved "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz" integrity sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q== -slice-ansi@^2.1.0: - version "2.1.0" - resolved "https://registry.npmjs.org/slice-ansi/-/slice-ansi-2.1.0.tgz" - integrity sha512-Qu+VC3EwYLldKa1fCxuuvULvSJOKEgk9pi8dZeCVK7TqBfUNTH4sFkk4joj8afVSfAYgJoSOetjx9QWOJ5mYoQ== - dependencies: - ansi-styles "^3.2.0" - astral-regex "^1.0.0" - is-fullwidth-code-point "^2.0.0" - slice-ansi@^3.0.0: version "3.0.0" resolved "https://registry.npmjs.org/slice-ansi/-/slice-ansi-3.0.0.tgz" @@ -16929,17 +17065,19 @@ string-width@^4.2.3: is-fullwidth-code-point "^3.0.0" strip-ansi "^6.0.1" -string.prototype.matchall@^4.0.2: - version "4.0.2" - resolved "https://registry.npmjs.org/string.prototype.matchall/-/string.prototype.matchall-4.0.2.tgz" - integrity sha512-N/jp6O5fMf9os0JU3E72Qhf590RSRZU/ungsL/qJUYVTNv7hTG0P/dbPjxINVN9jpscu3nzYwKESU3P3RY5tOg== +string.prototype.matchall@^4.0.8: + version "4.0.8" + resolved "https://registry.yarnpkg.com/string.prototype.matchall/-/string.prototype.matchall-4.0.8.tgz#3bf85722021816dcd1bf38bb714915887ca79fd3" + integrity sha512-6zOCOcJ+RJAQshcTvXPHoxoQGONa3e/Lqx90wUA+wEzX78sg5Bo+1tQo4N0pohS0erG9qtCqJDjNCQBjeWVxyg== dependencies: - define-properties "^1.1.3" - es-abstract "^1.17.0" - has-symbols "^1.0.1" - internal-slot "^1.0.2" - regexp.prototype.flags "^1.3.0" - side-channel "^1.0.2" + call-bind "^1.0.2" + define-properties "^1.1.4" + es-abstract "^1.20.4" + get-intrinsic "^1.1.3" + has-symbols "^1.0.3" + internal-slot "^1.0.3" + regexp.prototype.flags "^1.4.3" + side-channel "^1.0.4" string.prototype.trim@^1.1.2: version "1.2.1" @@ -16950,6 +17088,15 @@ string.prototype.trim@^1.1.2: es-abstract "^1.17.0-next.1" function-bind "^1.1.1" +string.prototype.trim@^1.2.7: + version "1.2.7" + resolved "https://registry.yarnpkg.com/string.prototype.trim/-/string.prototype.trim-1.2.7.tgz#a68352740859f6893f14ce3ef1bb3037f7a90533" + integrity sha512-p6TmeT1T3411M8Cgg9wBTMRtY2q9+PNy9EV1i2lIXUN/btt763oIfxwN3RR8VU6wHX8j/1CFy0L+YuThm6bgOg== + dependencies: + call-bind "^1.0.2" + define-properties "^1.1.4" + es-abstract "^1.20.4" + string.prototype.trimend@^1.0.1: version "1.0.1" resolved "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.1.tgz" @@ -16975,6 +17122,15 @@ string.prototype.trimend@^1.0.5: define-properties "^1.1.4" es-abstract "^1.19.5" +string.prototype.trimend@^1.0.6: + version "1.0.6" + resolved "https://registry.yarnpkg.com/string.prototype.trimend/-/string.prototype.trimend-1.0.6.tgz#c4a27fa026d979d79c04f17397f250a462944533" + integrity sha512-JySq+4mrPf9EsDBEDYMOb/lM7XQLulwg5R/m1r0PXEFqrV0qHvl58sdTilSXtKOflCsK2E8jxf+GKC0T07RWwQ== + dependencies: + call-bind "^1.0.2" + define-properties "^1.1.4" + es-abstract "^1.20.4" + string.prototype.trimstart@^1.0.1: version "1.0.1" resolved "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.1.tgz" @@ -17000,6 +17156,15 @@ string.prototype.trimstart@^1.0.5: define-properties "^1.1.4" es-abstract "^1.19.5" +string.prototype.trimstart@^1.0.6: + version "1.0.6" + resolved "https://registry.yarnpkg.com/string.prototype.trimstart/-/string.prototype.trimstart-1.0.6.tgz#e90ab66aa8e4007d92ef591bbf3cd422c56bdcf4" + integrity sha512-omqjMDaY92pbn5HOX7f9IccLA+U1tA9GvtU4JrodiXFfYB7jPzzHpRzpglLAjtUV6bB557zwClJezTqnAiYnQA== + dependencies: + call-bind "^1.0.2" + define-properties "^1.1.4" + es-abstract "^1.20.4" + string_decoder@^1.1.1: version "1.3.0" resolved "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz" @@ -17213,6 +17378,11 @@ supports-hyperlinks@^2.0.0: has-flag "^4.0.0" supports-color "^7.0.0" +supports-preserve-symlinks-flag@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz#6eda4bd344a3c94aea376d4cc31bc77311039e09" + integrity sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w== + swagger-axios-codegen@0.11.16: version "0.11.16" resolved "https://registry.npmjs.org/swagger-axios-codegen/-/swagger-axios-codegen-0.11.16.tgz" @@ -17261,16 +17431,6 @@ symbol-tree@^3.2.4: resolved "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz" integrity sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw== -table@^5.2.3: - version "5.4.6" - resolved "https://registry.npmjs.org/table/-/table-5.4.6.tgz" - integrity sha512-wmEc8m4fjnob4gt5riFRtTu/6+4rSe12TpAELNSqHMfF3IqnA+CH37USM6/YR3qRZv7e56kAEAtd6nKZaxe0Ug== - dependencies: - ajv "^6.10.2" - lodash "^4.17.14" - slice-ansi "^2.1.0" - string-width "^3.0.0" - tailwindcss-rtl@^0.9.0: version "0.9.0" resolved "https://registry.npmjs.org/tailwindcss-rtl/-/tailwindcss-rtl-0.9.0.tgz" @@ -17724,6 +17884,16 @@ tsconfig-paths@3.12.0: minimist "^1.2.0" strip-bom "^3.0.0" +tsconfig-paths@^3.14.1: + version "3.14.2" + resolved "https://registry.yarnpkg.com/tsconfig-paths/-/tsconfig-paths-3.14.2.tgz#6e32f1f79412decd261f92d633a9dc1cfa99f088" + integrity sha512-o/9iXgCYc5L/JxCHPe3Hvh8Q/2xm5Z+p18PESBU6Ff33695QnCHBEjcytY2q19ua7Mbl/DavtBOLq+oG0RCL+g== + dependencies: + "@types/json5" "^0.0.29" + json5 "^1.0.2" + minimist "^1.2.6" + strip-bom "^3.0.0" + tsconfig-paths@^3.9.0: version "3.9.0" resolved "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.9.0.tgz" @@ -17829,6 +17999,11 @@ type-fest@^0.18.0: resolved "https://registry.npmjs.org/type-fest/-/type-fest-0.18.1.tgz" integrity sha512-OIAYXk8+ISY+qTOwkHtKqzAuxchoMiD9Udx+FSGQDuiRR+PJKJHc2NJAXlbhkGwTt/4/nKZxELY1w3ReWOL8mw== +type-fest@^0.20.2: + version "0.20.2" + resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.20.2.tgz#1bf207f4b28f91583666cb5fbd327887301cd5f4" + integrity sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ== + type-fest@^0.4.1: version "0.4.1" resolved "https://registry.npmjs.org/type-fest/-/type-fest-0.4.1.tgz" @@ -17857,6 +18032,15 @@ type-is@^1.6.4, type-is@~1.6.18: media-typer "0.3.0" mime-types "~2.1.24" +typed-array-length@^1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/typed-array-length/-/typed-array-length-1.0.4.tgz#89d83785e5c4098bec72e08b319651f0eac9c1bb" + integrity sha512-KjZypGq+I/H7HI5HlOoGHkWUUGq+Q0TPhQurLbyrVrvnKTBgzLhIJ7j6J/XTQOi0d1RjyZ0wdas8bKs2p0x3Ng== + dependencies: + call-bind "^1.0.2" + for-each "^0.3.3" + is-typed-array "^1.1.9" + typedarray-to-buffer@^3.1.5: version "3.1.5" resolved "https://registry.npmjs.org/typedarray-to-buffer/-/typedarray-to-buffer-3.1.5.tgz" @@ -18253,11 +18437,6 @@ v8-compile-cache-lib@^3.0.1: resolved "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz" integrity sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg== -v8-compile-cache@^2.0.3: - version "2.1.1" - resolved "https://registry.npmjs.org/v8-compile-cache/-/v8-compile-cache-2.1.1.tgz" - integrity sha512-8OQ9CL+VWyt3JStj7HX7/ciTL2V3Rl1Wf5OL+SNTm0yK1KvtReVulksyeRnCANHHuUxHlQig+JJDlUhBt1NQDQ== - v8-to-istanbul@^6.0.1: version "6.0.1" resolved "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-6.0.1.tgz" @@ -18716,13 +18895,6 @@ write-pkg@^4.0.0: type-fest "^0.4.1" write-json-file "^3.2.0" -write@1.0.3: - version "1.0.3" - resolved "https://registry.npmjs.org/write/-/write-1.0.3.tgz" - integrity sha512-/lg70HAjtkUgWPVZhZcm+T4hkL8Zbtp1nFNOn3lRrxnlv50SRBv7cR7RqR+GMsd3hUXy9hWBo4CHTbFTcOYwig== - dependencies: - mkdirp "^0.5.1" - ws@^7.3.1, ws@^7.4.6: version "7.5.8" resolved "https://registry.npmjs.org/ws/-/ws-7.5.8.tgz" From b04c7f533bd4d18b6a479f7531f39fdbc5ac11ce Mon Sep 17 00:00:00 2001 From: Yazeed Loonat Date: Wed, 14 Jun 2023 11:57:15 -0700 Subject: [PATCH 05/57] Prisma ami-charts (#3477) * feat: prisma schema generation * fix: cleaning up removed "models" * fix: updates per emily * fix: caught some schema errors * feat: listing get endpoints * feat: adding DTOs * feat: openapi and swagger updates * fix: pass through DTOs * fix: updates per emily * feat: adding ci jobs * fix: round 2 * fix: indentation problem * fix: ci updates * fix: ci test again * fix: adding maybe missing library? * fix: running yarn install * fix: updates per morgan * fix: ci fix round x * fix: test updates * fix: integration tests and ci * fix: update for tests * fix: fixing scheme so migrations can be created * feat: creating ami-chart endpoints * fix: updates per morgan * fix: update backend-new circleci job (#3489) * fix: update backend-new circleci job * fix: add cache * fix: add class validator * fix: update list typing * fix: add class transformer * fix: add listing type * fix: one more attempt at typings * fix: update jest config file * fix: add dbsetup * fix: one more attempt * fix: e2e typing fix * fix: turn off diagnostics for e2e * fix: clear db fields between tests * fix: prettier and linting changes * fix: additional linting fixes --------- Co-authored-by: Yazeed Loonat * fix: updates from what I learned * fix: forgot swagger file * fix: updates per morgan * fix: updates per sean/morgan and test updates * fix: updates per sean and morgan * fix: maybe resolving test race case * fix: removing jurisdictionName param * fix: upates per sean and emily * fix: updates per emily --------- Co-authored-by: Morgan Ludtke <42942267+ludtkemorgan@users.noreply.github.com> --- backend_new/README.md | 5 + backend_new/package.json | 2 +- .../prisma/seed-helpers/ami-chart-factory.ts | 21 + .../prisma/seed-helpers/listing-factory.ts | 24 +- backend_new/prisma/seed.ts | 7 +- backend_new/src/app.module.ts | 5 +- .../src/controllers/ami-chart.controller.ts | 69 +++ .../dtos/ami-charts/ami-chart-create.dto.ts | 8 + .../ami-charts/ami-chart-query-params.dto.ts | 15 + .../dtos/ami-charts/ami-chart-update.dto.ts | 8 + .../src/dtos/ami-charts/ami-chart.dto.ts | 33 ++ backend_new/src/dtos/shared/abstract.dto.ts | 4 + backend_new/src/dtos/shared/id.dto.ts | 18 + backend_new/src/dtos/shared/success.dto.ts | 12 + .../src/dtos/units/ami-chart-item-get.dto.ts | 4 + backend_new/src/dtos/units/unit-get.dto.ts | 2 +- backend_new/src/modules/ami-chart.module.ts | 12 + backend_new/src/modules/listing.module.ts | 2 +- backend_new/src/services/ami-chart.service.ts | 151 +++++ backend_new/src/services/listing.service.ts | 2 +- backend_new/src/utilities/unit-utilities.ts | 2 +- .../test/integration/ami-chart.e2e-spec.ts | 141 +++++ .../test/integration/listing.e2e-spec.ts | 2 +- .../unit/services/ami-chart.service.spec.ts | 539 ++++++++++++++++++ .../unit/services/listing.service.spec.ts | 3 + .../unit/utilities/unit-utilities.spec.ts | 11 +- backend_new/types/src/backend-swagger.ts | 250 +++++++- backend_new/yarn.lock | 214 +++---- 28 files changed, 1443 insertions(+), 123 deletions(-) create mode 100644 backend_new/prisma/seed-helpers/ami-chart-factory.ts create mode 100644 backend_new/src/controllers/ami-chart.controller.ts create mode 100644 backend_new/src/dtos/ami-charts/ami-chart-create.dto.ts create mode 100644 backend_new/src/dtos/ami-charts/ami-chart-query-params.dto.ts create mode 100644 backend_new/src/dtos/ami-charts/ami-chart-update.dto.ts create mode 100644 backend_new/src/dtos/ami-charts/ami-chart.dto.ts create mode 100644 backend_new/src/dtos/shared/id.dto.ts create mode 100644 backend_new/src/dtos/shared/success.dto.ts create mode 100644 backend_new/src/modules/ami-chart.module.ts create mode 100644 backend_new/src/services/ami-chart.service.ts create mode 100644 backend_new/test/integration/ami-chart.e2e-spec.ts create mode 100644 backend_new/test/unit/services/ami-chart.service.spec.ts diff --git a/backend_new/README.md b/backend_new/README.md index 7bc19f2313..0610ad671c 100644 --- a/backend_new/README.md +++ b/backend_new/README.md @@ -5,6 +5,7 @@ Make sure the .env file's db placement is what works for your set up, Then run t ```bash $ yarn install $ yarn db:setup +$ yarn prisma generate ``` @@ -114,3 +115,7 @@ Running the following will run all unit tests: ```bash $ yarn test ``` + +# Considerations For Detroit +As it stands right now `core` uses the AmiChart items column and `detroit` uses the AmiChartItem table. +As we move through converting detroit over to prisma we will unify those and choose one of the two approaches. \ No newline at end of file diff --git a/backend_new/package.json b/backend_new/package.json index fc8c31eb3f..035b6c9929 100644 --- a/backend_new/package.json +++ b/backend_new/package.json @@ -22,7 +22,7 @@ "db:migration:run": "yarn prisma migrate deploy", "db:seed": "yarn prisma db seed", "generate:client": "ts-node scripts/generate-axios-client.ts && prettier -w types/src/backend-swagger.ts", - "test:e2e": "yarn db:resetup && yarn db:migration:run && jest --config ./test/jest-e2e.json", + "test:e2e": "yarn db:resetup && yarn db:migration:run && jest --config ./test/jest-e2e.config.js", "db:setup": "yarn db:resetup && yarn db:migration:run && yarn db:seed" }, "dependencies": { diff --git a/backend_new/prisma/seed-helpers/ami-chart-factory.ts b/backend_new/prisma/seed-helpers/ami-chart-factory.ts new file mode 100644 index 0000000000..a5a4e51ecf --- /dev/null +++ b/backend_new/prisma/seed-helpers/ami-chart-factory.ts @@ -0,0 +1,21 @@ +import { Prisma } from '@prisma/client'; + +export const amiChartFactory = ( + i: number, + jurisdictionId: string, +): Prisma.AmiChartCreateInput => ({ + name: `name: ${i}`, + items: amiChartItemsFactory(i), + jurisdictions: { + connect: { + id: jurisdictionId, + }, + }, +}); + +const amiChartItemsFactory = (numberToCreate: number): Prisma.JsonArray => + [...Array(numberToCreate)].map((_, index) => ({ + percentOfAmi: index, + householdSize: index, + income: index, + })); diff --git a/backend_new/prisma/seed-helpers/listing-factory.ts b/backend_new/prisma/seed-helpers/listing-factory.ts index 047db1f303..3206a3f8a3 100644 --- a/backend_new/prisma/seed-helpers/listing-factory.ts +++ b/backend_new/prisma/seed-helpers/listing-factory.ts @@ -16,6 +16,7 @@ import { export const listingFactory = ( i: number, jurisdictionId: string, + amiChartId?: string, ): Prisma.ListingsCreateInput => ({ additionalApplicationSubmissionNotes: `additionalApplicationSubmissionNotes: ${i}`, digitalApplication: true, @@ -325,13 +326,14 @@ export const listingFactory = ( }, ], }, - units: unitFactory(i, i, jurisdictionId), + units: unitFactory(i, i, jurisdictionId, amiChartId), }); const unitFactory = ( numberToMake: number, i: number, jurisdictionId: string, + amiChartId?: string, ): Prisma.UnitsCreateNestedManyWithoutListingsInput => { const createArray: Prisma.UnitsCreateWithoutListingsInput[] = []; for (let j = 0; j < numberToMake; j++) { @@ -357,17 +359,19 @@ const unitFactory = ( numBedrooms: i, }, }, - amiChart: { - create: { - items: {}, - name: `listing: ${i} unit: ${j} amiChart: ${j}`, - jurisdictions: { - connect: { - id: jurisdictionId, + amiChart: amiChartId + ? { connect: { id: amiChartId } } + : { + create: { + items: [], + name: `listing: ${i} unit: ${j} amiChart: ${j}`, + jurisdictions: { + connect: { + id: jurisdictionId, + }, + }, }, }, - }, - }, unitAccessibilityPriorityTypes: { create: { name: `listing: ${i} unit: ${j} unitAccessibilityPriorityTypes: ${j}`, diff --git a/backend_new/prisma/seed.ts b/backend_new/prisma/seed.ts index 3559d13c9d..8513b233e4 100644 --- a/backend_new/prisma/seed.ts +++ b/backend_new/prisma/seed.ts @@ -1,4 +1,5 @@ import { PrismaClient } from '@prisma/client'; +import { amiChartFactory } from './seed-helpers/ami-chart-factory'; import { jurisdictionFactory } from './seed-helpers/jurisdiction-factory'; import { listingFactory } from './seed-helpers/listing-factory'; @@ -7,9 +8,13 @@ async function main() { const jurisdiction = await prisma.jurisdictions.create({ data: jurisdictionFactory(0), }); + const amiChart = await prisma.amiChart.create({ + data: amiChartFactory(10, jurisdiction.id), + }); + for (let i = 0; i < 5; i++) { await prisma.listings.create({ - data: listingFactory(i, jurisdiction.id), + data: listingFactory(i, jurisdiction.id, amiChart.id), }); } } diff --git a/backend_new/src/app.module.ts b/backend_new/src/app.module.ts index 9ff0f01aab..c941feb9ed 100644 --- a/backend_new/src/app.module.ts +++ b/backend_new/src/app.module.ts @@ -1,12 +1,13 @@ import { Module } from '@nestjs/common'; import { AppController } from './app.controller'; import { AppService } from './app.service'; +import { AmiChartModule } from './modules/ami-chart.module'; import { ListingModule } from './modules/listing.module'; @Module({ - imports: [ListingModule], + imports: [ListingModule, AmiChartModule], controllers: [AppController], providers: [AppService], - exports: [ListingModule], + exports: [ListingModule, AmiChartModule], }) export class AppModule {} diff --git a/backend_new/src/controllers/ami-chart.controller.ts b/backend_new/src/controllers/ami-chart.controller.ts new file mode 100644 index 0000000000..cc4b99f7c6 --- /dev/null +++ b/backend_new/src/controllers/ami-chart.controller.ts @@ -0,0 +1,69 @@ +import { + Body, + Controller, + Delete, + Get, + Param, + Post, + Put, + Query, + UsePipes, + ValidationPipe, +} from '@nestjs/common'; +import { + ApiExtraModels, + ApiOkResponse, + ApiOperation, + ApiTags, +} from '@nestjs/swagger'; +import { AmiChartService } from '../services/ami-chart.service'; +import { AmiChart } from '../dtos/ami-charts/ami-chart.dto'; +import { AmiChartCreate } from '../dtos/ami-charts/ami-chart-create.dto'; +import { AmiChartUpdate } from '../dtos/ami-charts/ami-chart-update.dto'; +import { defaultValidationPipeOptions } from '../utilities/default-validation-pipe-options'; +import { AmiChartQueryParams } from '../dtos/ami-charts/ami-chart-query-params.dto'; +import { IdDTO } from '../dtos/shared/id.dto'; +import { SuccessDTO } from '../dtos/shared/success.dto'; + +@Controller('/amiCharts') +@ApiTags('amiCharts') +@UsePipes(new ValidationPipe(defaultValidationPipeOptions)) +@ApiExtraModels(AmiChartCreate, AmiChartUpdate, IdDTO, AmiChartQueryParams) +export class AmiChartController { + constructor(private readonly AmiChartService: AmiChartService) {} + + @Get() + @ApiOperation({ summary: 'List amiCharts', operationId: 'list' }) + @ApiOkResponse({ type: AmiChart, isArray: true }) + async list(@Query() queryParams: AmiChartQueryParams): Promise { + return await this.AmiChartService.list(queryParams); + } + + @Get(`:amiChartId`) + @ApiOperation({ summary: 'Get amiChart by id', operationId: 'retrieve' }) + @ApiOkResponse({ type: AmiChart }) + async retrieve(@Param('amiChartId') amiChartId: string): Promise { + return this.AmiChartService.findOne(amiChartId); + } + + @Post() + @ApiOperation({ summary: 'Create amiChart', operationId: 'create' }) + @ApiOkResponse({ type: AmiChart }) + async create(@Body() amiChart: AmiChartCreate): Promise { + return await this.AmiChartService.create(amiChart); + } + + @Put(`:amiChartId`) + @ApiOperation({ summary: 'Update amiChart', operationId: 'update' }) + @ApiOkResponse({ type: AmiChart }) + async update(@Body() amiChart: AmiChartUpdate): Promise { + return await this.AmiChartService.update(amiChart); + } + + @Delete() + @ApiOperation({ summary: 'Delete amiChart by id', operationId: 'delete' }) + @ApiOkResponse({ type: SuccessDTO }) + async delete(@Body() dto: IdDTO): Promise { + return await this.AmiChartService.delete(dto.id); + } +} diff --git a/backend_new/src/dtos/ami-charts/ami-chart-create.dto.ts b/backend_new/src/dtos/ami-charts/ami-chart-create.dto.ts new file mode 100644 index 0000000000..29fe706714 --- /dev/null +++ b/backend_new/src/dtos/ami-charts/ami-chart-create.dto.ts @@ -0,0 +1,8 @@ +import { OmitType } from '@nestjs/swagger'; +import { AmiChart } from './ami-chart.dto'; + +export class AmiChartCreate extends OmitType(AmiChart, [ + 'id', + 'createdAt', + 'updatedAt', +]) {} diff --git a/backend_new/src/dtos/ami-charts/ami-chart-query-params.dto.ts b/backend_new/src/dtos/ami-charts/ami-chart-query-params.dto.ts new file mode 100644 index 0000000000..9621df09c0 --- /dev/null +++ b/backend_new/src/dtos/ami-charts/ami-chart-query-params.dto.ts @@ -0,0 +1,15 @@ +import { Expose } from 'class-transformer'; +import { ApiProperty } from '@nestjs/swagger'; +import { IsString } from 'class-validator'; +import { ValidationsGroupsEnum } from '../../enums/shared/validation-groups-enum'; + +export class AmiChartQueryParams { + @Expose() + @ApiProperty({ + name: 'jurisdictionId', + required: false, + type: String, + }) + @IsString({ groups: [ValidationsGroupsEnum.default] }) + jurisdictionId?: string; +} diff --git a/backend_new/src/dtos/ami-charts/ami-chart-update.dto.ts b/backend_new/src/dtos/ami-charts/ami-chart-update.dto.ts new file mode 100644 index 0000000000..1a00c6f223 --- /dev/null +++ b/backend_new/src/dtos/ami-charts/ami-chart-update.dto.ts @@ -0,0 +1,8 @@ +import { OmitType } from '@nestjs/swagger'; +import { AmiChart } from './ami-chart.dto'; + +export class AmiChartUpdate extends OmitType(AmiChart, [ + 'createdAt', + 'updatedAt', + 'jurisdictions', +]) {} diff --git a/backend_new/src/dtos/ami-charts/ami-chart.dto.ts b/backend_new/src/dtos/ami-charts/ami-chart.dto.ts new file mode 100644 index 0000000000..b0555748d7 --- /dev/null +++ b/backend_new/src/dtos/ami-charts/ami-chart.dto.ts @@ -0,0 +1,33 @@ +import { IsDefined, IsString, ValidateNested } from 'class-validator'; +import { Expose, Type } from 'class-transformer'; +import { ValidationsGroupsEnum } from '../../enums/shared/validation-groups-enum'; +import { AbstractDTO } from '../shared/abstract.dto'; +import { AmiChartItem } from '../units/ami-chart-item-get.dto'; +import { IdDTO } from '../shared/id.dto'; +import { ApiProperty } from '@nestjs/swagger'; + +export class AmiChart extends AbstractDTO { + @Expose() + @IsDefined({ groups: [ValidationsGroupsEnum.default] }) + @ValidateNested({ groups: [ValidationsGroupsEnum.default], each: true }) + @Type(() => AmiChartItem) + @ApiProperty({ + type: AmiChartItem, + isArray: true, + required: true, + }) + items: AmiChartItem[]; + + @Expose() + @IsString({ groups: [ValidationsGroupsEnum.default] }) + @IsDefined({ groups: [ValidationsGroupsEnum.default] }) + @ApiProperty({ required: true }) + name: string; + + @Expose() + @ValidateNested({ groups: [ValidationsGroupsEnum.default] }) + @Type(() => IdDTO) + @IsDefined({ groups: [ValidationsGroupsEnum.default] }) + @ApiProperty({ required: true }) + jurisdictions: IdDTO; +} diff --git a/backend_new/src/dtos/shared/abstract.dto.ts b/backend_new/src/dtos/shared/abstract.dto.ts index 0ae68e27e7..7793ec4e4b 100644 --- a/backend_new/src/dtos/shared/abstract.dto.ts +++ b/backend_new/src/dtos/shared/abstract.dto.ts @@ -1,3 +1,4 @@ +import { ApiProperty } from '@nestjs/swagger'; import { Expose, Type } from 'class-transformer'; import { IsDate, IsDefined, IsString, IsUUID } from 'class-validator'; import { ValidationsGroupsEnum } from '../../enums/shared/validation-groups-enum'; @@ -7,17 +8,20 @@ export class AbstractDTO { @IsString({ groups: [ValidationsGroupsEnum.default] }) @IsUUID(4, { groups: [ValidationsGroupsEnum.default] }) @IsDefined({ groups: [ValidationsGroupsEnum.default] }) + @ApiProperty({ required: true }) id: string; @Expose() @IsDate({ groups: [ValidationsGroupsEnum.default] }) @IsDefined({ groups: [ValidationsGroupsEnum.default] }) @Type(() => Date) + @ApiProperty({ required: true }) createdAt: Date; @Expose() @IsDate({ groups: [ValidationsGroupsEnum.default] }) @IsDefined({ groups: [ValidationsGroupsEnum.default] }) @Type(() => Date) + @ApiProperty({ required: true }) updatedAt: Date; } diff --git a/backend_new/src/dtos/shared/id.dto.ts b/backend_new/src/dtos/shared/id.dto.ts new file mode 100644 index 0000000000..f0629820c5 --- /dev/null +++ b/backend_new/src/dtos/shared/id.dto.ts @@ -0,0 +1,18 @@ +import { IsDefined, IsString, IsUUID } from 'class-validator'; +import { Expose } from 'class-transformer'; +import { ValidationsGroupsEnum } from '../../enums/shared/validation-groups-enum'; +import { ApiProperty } from '@nestjs/swagger'; + +export class IdDTO { + @Expose() + @IsString({ groups: [ValidationsGroupsEnum.default] }) + @IsUUID(4, { groups: [ValidationsGroupsEnum.default] }) + @IsDefined({ groups: [ValidationsGroupsEnum.default] }) + @ApiProperty({ required: true }) + id: string; + + @Expose() + @IsString({ groups: [ValidationsGroupsEnum.default] }) + @ApiProperty({ required: false }) + name?: string; +} diff --git a/backend_new/src/dtos/shared/success.dto.ts b/backend_new/src/dtos/shared/success.dto.ts new file mode 100644 index 0000000000..9058bc3c74 --- /dev/null +++ b/backend_new/src/dtos/shared/success.dto.ts @@ -0,0 +1,12 @@ +import { IsDefined, IsBoolean } from 'class-validator'; +import { Expose } from 'class-transformer'; +import { ValidationsGroupsEnum } from '../../enums/shared/validation-groups-enum'; +import { ApiProperty } from '@nestjs/swagger'; + +export class SuccessDTO { + @Expose() + @IsBoolean({ groups: [ValidationsGroupsEnum.default] }) + @IsDefined({ groups: [ValidationsGroupsEnum.default] }) + @ApiProperty() + success: boolean; +} diff --git a/backend_new/src/dtos/units/ami-chart-item-get.dto.ts b/backend_new/src/dtos/units/ami-chart-item-get.dto.ts index ae995f307c..51c5eeb610 100644 --- a/backend_new/src/dtos/units/ami-chart-item-get.dto.ts +++ b/backend_new/src/dtos/units/ami-chart-item-get.dto.ts @@ -1,3 +1,4 @@ +import { ApiProperty } from '@nestjs/swagger'; import { Expose } from 'class-transformer'; import { IsNumber, IsDefined } from 'class-validator'; import { ValidationsGroupsEnum } from '../../enums/shared/validation-groups-enum'; @@ -6,15 +7,18 @@ export class AmiChartItem { @Expose() @IsNumber({}, { groups: [ValidationsGroupsEnum.default] }) @IsDefined({ groups: [ValidationsGroupsEnum.default] }) + @ApiProperty({ required: true }) percentOfAmi: number; @Expose() @IsNumber({}, { groups: [ValidationsGroupsEnum.default] }) @IsDefined({ groups: [ValidationsGroupsEnum.default] }) + @ApiProperty({ required: true }) householdSize: number; @Expose() @IsNumber({}, { groups: [ValidationsGroupsEnum.default] }) @IsDefined({ groups: [ValidationsGroupsEnum.default] }) + @ApiProperty({ required: true }) income: number; } diff --git a/backend_new/src/dtos/units/unit-get.dto.ts b/backend_new/src/dtos/units/unit-get.dto.ts index 3b2488ab55..cfea6f8189 100644 --- a/backend_new/src/dtos/units/unit-get.dto.ts +++ b/backend_new/src/dtos/units/unit-get.dto.ts @@ -8,7 +8,7 @@ import { import { Expose, Type } from 'class-transformer'; import { ValidationsGroupsEnum } from '../../enums/shared/validation-groups-enum'; import { AbstractDTO } from '../shared/abstract.dto'; -import { AmiChart } from './ami-chart-get.dto'; +import { AmiChart } from '../ami-charts/ami-chart.dto'; import { UnitType } from './unit-type-get.dto'; import { UnitRentType } from './unit-rent-type-get.dto'; import { UnitAccessibilityPriorityType } from './unit-accessibility-priority-type-get.dto'; diff --git a/backend_new/src/modules/ami-chart.module.ts b/backend_new/src/modules/ami-chart.module.ts new file mode 100644 index 0000000000..79ec90c618 --- /dev/null +++ b/backend_new/src/modules/ami-chart.module.ts @@ -0,0 +1,12 @@ +import { Module } from '@nestjs/common'; +import { AmiChartController } from '../controllers/ami-chart.controller'; +import { AmiChartService } from '../services/ami-chart.service'; +import { PrismaService } from '../services/prisma.service'; + +@Module({ + imports: [], + controllers: [AmiChartController], + providers: [AmiChartService, PrismaService], + exports: [AmiChartService, PrismaService], +}) +export class AmiChartModule {} diff --git a/backend_new/src/modules/listing.module.ts b/backend_new/src/modules/listing.module.ts index a03747f94f..0f9fec5bb3 100644 --- a/backend_new/src/modules/listing.module.ts +++ b/backend_new/src/modules/listing.module.ts @@ -7,6 +7,6 @@ import { PrismaService } from '../services/prisma.service'; imports: [], controllers: [ListingController], providers: [ListingService, PrismaService], - exports: [ListingService], + exports: [ListingService, PrismaService], }) export class ListingModule {} diff --git a/backend_new/src/services/ami-chart.service.ts b/backend_new/src/services/ami-chart.service.ts new file mode 100644 index 0000000000..229c9cfedc --- /dev/null +++ b/backend_new/src/services/ami-chart.service.ts @@ -0,0 +1,151 @@ +import { Injectable, NotFoundException } from '@nestjs/common'; +import { PrismaService } from './prisma.service'; +import { Prisma } from '@prisma/client'; +import { AmiChart } from '../dtos/ami-charts/ami-chart.dto'; +import { AmiChartCreate } from '../dtos/ami-charts/ami-chart-create.dto'; +import { AmiChartUpdate } from '../dtos/ami-charts/ami-chart-update.dto'; +import { AmiChartQueryParams } from '../dtos/ami-charts/ami-chart-query-params.dto'; +import { mapTo } from '../utilities/mapTo'; +import { SuccessDTO } from '../dtos/shared/success.dto'; +import { AmiChartItem } from '../dtos/units/ami-chart-item-get.dto'; + +/* + this is the service for ami charts + it handles all the backend's business logic for reading/writing/deleting ami chart data +*/ + +const view: Prisma.AmiChartInclude = { + jurisdictions: true, +}; + +@Injectable() +export class AmiChartService { + constructor(private prisma: PrismaService) {} + + /* + this will get a set of ami charts given the params passed in + */ + async list(params: AmiChartQueryParams): Promise { + const rawAmiCharts = await this.prisma.amiChart.findMany({ + include: view, + where: this.buildWhereClause(params), + }); + return mapTo(AmiChart, rawAmiCharts); + } + + /* + this helps build the where clause for the list() + */ + buildWhereClause(params: AmiChartQueryParams): Prisma.AmiChartWhereInput { + const filters: Prisma.AmiChartWhereInput[] = []; + if (params && 'jurisdictionId' in params && params.jurisdictionId) { + filters.push({ + jurisdictions: { + id: params.jurisdictionId, + }, + }); + } + return { + AND: filters, + }; + } + + /* + this will return 1 ami chart or error + */ + async findOne(amiChartId: string): Promise { + const amiChartRaw = await this.prisma.amiChart.findUnique({ + include: view, + where: { + id: amiChartId, + }, + }); + + if (!amiChartRaw) { + throw new NotFoundException( + `amiChartId ${amiChartId} was requested but not found`, + ); + } + + return mapTo(AmiChart, amiChartRaw); + } + + /* + this will create an ami chart + */ + async create(incomingData: AmiChartCreate): Promise { + const rawResult = await this.prisma.amiChart.create({ + data: { + ...incomingData, + items: this.reconstructItems(incomingData.items), + jurisdictions: { + connect: { + id: incomingData.jurisdictions.id, + }, + }, + }, + include: view, + }); + + return mapTo(AmiChart, rawResult); + } + + /* + this will update an ami chart's name or items field + if no ami chart has the id of the incoming argument an error is thrown + */ + async update(incomingData: AmiChartUpdate): Promise { + await this.findOrThrow(incomingData.id); + const rawResults = await this.prisma.amiChart.update({ + include: view, + data: { + ...incomingData, + items: this.reconstructItems(incomingData.items), + jurisdictions: undefined, + id: undefined, + }, + where: { + id: incomingData.id, + }, + }); + return mapTo(AmiChart, rawResults); + } + + /* + this will delete an ami chart + */ + async delete(amiChartId: string): Promise { + await this.findOrThrow(amiChartId); + await this.prisma.amiChart.delete({ + where: { + id: amiChartId, + }, + }); + return { + success: true, + } as SuccessDTO; + } + + reconstructItems(items: AmiChartItem[]): Prisma.JsonArray { + return items.map((item) => ({ + percentOfAmi: item.percentOfAmi, + householdSize: item.householdSize, + income: item.income, + })) as Prisma.JsonArray; + } + + async findOrThrow(amiChartId: string): Promise { + const amiChart = await this.prisma.amiChart.findUnique({ + where: { + id: amiChartId, + }, + }); + + if (!amiChart) { + throw new NotFoundException( + `amiChartId ${amiChartId} was requested but not found`, + ); + } + return true; + } +} diff --git a/backend_new/src/services/listing.service.ts b/backend_new/src/services/listing.service.ts index 6cf9ad5436..f11781f1d3 100644 --- a/backend_new/src/services/listing.service.ts +++ b/backend_new/src/services/listing.service.ts @@ -17,7 +17,7 @@ import { summarizeUnitsByTypeAndRent, summarizeUnits, } from '../utilities/unit-utilities'; -import { AmiChart } from '../dtos/units/ami-chart-get.dto'; +import { AmiChart } from '../dtos/ami-charts/ami-chart.dto'; import { ListingViews } from '../enums/listings/view-enum'; export type getListingsArgs = { diff --git a/backend_new/src/utilities/unit-utilities.ts b/backend_new/src/utilities/unit-utilities.ts index 9a349663b2..b74b1bf9b9 100644 --- a/backend_new/src/utilities/unit-utilities.ts +++ b/backend_new/src/utilities/unit-utilities.ts @@ -1,7 +1,7 @@ import { ReviewOrderTypeEnum } from '@prisma/client'; import { UnitSummary } from '../dtos/units/unit-summary-get.dto'; import Unit from '../dtos/units/unit-get.dto'; -import { AmiChart } from '../dtos/units/ami-chart-get.dto'; +import { AmiChart } from '../dtos/ami-charts/ami-chart.dto'; import listingGetDto, { ListingGet } from '../dtos/listings/listing-get.dto'; import { MinMaxCurrency } from '../dtos/shared/min-max-currency.dto'; import { MinMax } from '../dtos/shared/min-max.dto'; diff --git a/backend_new/test/integration/ami-chart.e2e-spec.ts b/backend_new/test/integration/ami-chart.e2e-spec.ts new file mode 100644 index 0000000000..fc724be89f --- /dev/null +++ b/backend_new/test/integration/ami-chart.e2e-spec.ts @@ -0,0 +1,141 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { INestApplication } from '@nestjs/common'; +import * as request from 'supertest'; +import { AppModule } from '../../src/app.module'; +import { PrismaService } from '../../src/services/prisma.service'; +import { jurisdictionFactory } from '../../prisma/seed-helpers/jurisdiction-factory'; +import { stringify } from 'qs'; +import { AmiChartQueryParams } from '../../src/dtos/ami-charts/ami-chart-query-params.dto'; +import { amiChartFactory } from '../../prisma/seed-helpers/ami-chart-factory'; +import { AmiChartCreate } from '../../src/dtos/ami-charts/ami-chart-create.dto'; +import { AmiChartUpdate } from '../../src/dtos/ami-charts/ami-chart-update.dto'; +import { IdDTO } from 'src/dtos/shared/id.dto'; + +describe('AmiChart Controller Tests', () => { + let app: INestApplication; + let prisma: PrismaService; + let jurisdictionAId: string; + + beforeAll(async () => { + const moduleFixture: TestingModule = await Test.createTestingModule({ + imports: [AppModule], + }).compile(); + + app = moduleFixture.createNestApplication(); + prisma = moduleFixture.get(PrismaService); + await app.init(); + + const jurisdictionA = await prisma.jurisdictions.create({ + data: jurisdictionFactory(10), + }); + jurisdictionAId = jurisdictionA.id; + }); + + it('testing list endpoint', async () => { + const jurisdictionB = await prisma.jurisdictions.create({ + data: jurisdictionFactory(11), + }); + const amiChartA = await prisma.amiChart.create({ + data: amiChartFactory(10, jurisdictionAId), + }); + await prisma.amiChart.create({ + data: amiChartFactory(15, jurisdictionB.id), + }); + const queryParams: AmiChartQueryParams = { + jurisdictionId: jurisdictionAId, + }; + const query = stringify(queryParams as any); + + const res = await request(app.getHttpServer()) + .get(`/amiCharts?${query}`) + .expect(200); + + expect(res.body.length).toEqual(1); + expect(res.body[0].name).toEqual(amiChartA.name); + }); + + it('testing retrieve endpoint', async () => { + const amiChartA = await prisma.amiChart.create({ + data: amiChartFactory(10, jurisdictionAId), + }); + + const res = await request(app.getHttpServer()) + .get(`/amiCharts/${amiChartA.id}`) + .expect(200); + + expect(res.body.name).toEqual(amiChartA.name); + }); + + it('testing create endpoint', async () => { + const res = await request(app.getHttpServer()) + .post('/amiCharts') + .send({ + name: 'name: 10', + items: [ + { + percentOfAmi: 80, + householdSize: 2, + income: 5000, + }, + ], + jurisdictions: { + id: jurisdictionAId, + }, + } as AmiChartCreate) + .expect(201); + + expect(res.body.name).toEqual('name: 10'); + expect(res.body.items).toEqual([ + { + percentOfAmi: 80, + householdSize: 2, + income: 5000, + }, + ]); + }); + + it('testing update endpoint', async () => { + const amiChartA = await prisma.amiChart.create({ + data: amiChartFactory(10, jurisdictionAId), + }); + + const res = await request(app.getHttpServer()) + .put(`/amiCharts/${amiChartA.id}`) + .send({ + id: amiChartA.id, + name: 'name: 11', + items: [ + { + percentOfAmi: 80, + householdSize: 2, + income: 5000, + }, + ], + } as AmiChartUpdate) + .expect(200); + + expect(res.body.name).toEqual('name: 11'); + expect(res.body.items).toEqual([ + { + percentOfAmi: 80, + householdSize: 2, + income: 5000, + }, + ]); + }); + + it('testing delete endpoint', async () => { + const amiChartA = await prisma.amiChart.create({ + data: amiChartFactory(10, jurisdictionAId), + }); + + const res = await request(app.getHttpServer()) + .delete(`/amiCharts`) + .send({ + id: amiChartA.id, + } as IdDTO) + .expect(200); + + expect(res.body.success).toEqual(true); + }); +}); diff --git a/backend_new/test/integration/listing.e2e-spec.ts b/backend_new/test/integration/listing.e2e-spec.ts index 0c014f68bd..2201cdffad 100644 --- a/backend_new/test/integration/listing.e2e-spec.ts +++ b/backend_new/test/integration/listing.e2e-spec.ts @@ -99,7 +99,7 @@ describe('Listing Controller Tests', () => { const query = stringify(queryParams as any); const res = await request(app.getHttpServer()) - .get(`/listings?${query}'`) + .get(`/listings?${query}`) .expect(200); expect(res.body).toEqual({ diff --git a/backend_new/test/unit/services/ami-chart.service.spec.ts b/backend_new/test/unit/services/ami-chart.service.spec.ts new file mode 100644 index 0000000000..95e58cfac8 --- /dev/null +++ b/backend_new/test/unit/services/ami-chart.service.spec.ts @@ -0,0 +1,539 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { PrismaService } from '../../../src/services/prisma.service'; +import { AmiChartService } from '../../../src/services/ami-chart.service'; +import { AmiChartQueryParams } from '../../../src/dtos/ami-charts/ami-chart-query-params.dto'; +import { AmiChartCreate } from '../../../src/dtos/ami-charts/ami-chart-create.dto'; +import { AmiChartUpdate } from '../../../src/dtos/ami-charts/ami-chart-update.dto'; +import { randomUUID } from 'crypto'; + +describe('Testing ami chart service', () => { + let service: AmiChartService; + let prisma: PrismaService; + + const mockAmiChart = ( + position: number, + date: Date, + jurisdictionData: any, + ) => { + const items = []; + for (let i = 0; i < position; i++) { + items.push({ + percentOfAmi: i, + householdSize: i, + income: i, + }); + } + + return { + id: randomUUID(), + name: `ami ${position}`, + jurisdictions: jurisdictionData, + createdAt: date, + updatedAt: date, + items: items, + }; + }; + + const mockAmiChartSet = ( + numberToCreate: number, + date: Date, + jurisdictionData: any, + ) => { + const toReturn = []; + for (let i = 0; i < numberToCreate; i++) { + toReturn.push(mockAmiChart(i, date, jurisdictionData)); + } + return toReturn; + }; + + beforeAll(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [AmiChartService, PrismaService], + }).compile(); + + service = module.get(AmiChartService); + prisma = module.get(PrismaService); + }); + + it('testing buildWhereClause() no params', () => { + const params: AmiChartQueryParams = {}; + expect(service.buildWhereClause(params)).toEqual({ + AND: [], + }); + }); + + it('testing buildWhereClause() jurisdictionId param present', () => { + const params: AmiChartQueryParams = { + jurisdictionId: 'test id', + }; + expect(service.buildWhereClause(params)).toEqual({ + AND: [ + { + jurisdictions: { + id: 'test id', + }, + }, + ], + }); + }); + + it('testing list() with jurisdictionId param present', async () => { + const date = new Date(); + const jurisdictionData = { + id: 'example Id', + name: 'example name', + }; + const mockedValue = mockAmiChartSet(3, date, jurisdictionData); + + prisma.amiChart.findMany = jest.fn().mockResolvedValue(mockedValue); + + const params: AmiChartQueryParams = { + jurisdictionId: 'test name', + }; + + expect(await service.list(params)).toEqual([ + { + id: mockedValue[0].id, + name: 'ami 0', + jurisdictions: jurisdictionData, + createdAt: date, + updatedAt: date, + items: [], + }, + { + id: mockedValue[1].id, + name: 'ami 1', + jurisdictions: jurisdictionData, + createdAt: date, + updatedAt: date, + items: [ + { + percentOfAmi: 0, + householdSize: 0, + income: 0, + }, + ], + }, + { + id: mockedValue[2].id, + name: 'ami 2', + jurisdictions: jurisdictionData, + createdAt: date, + updatedAt: date, + items: [ + { + percentOfAmi: 0, + householdSize: 0, + income: 0, + }, + { + percentOfAmi: 1, + householdSize: 1, + income: 1, + }, + ], + }, + ]); + + expect(prisma.amiChart.findMany).toHaveBeenCalledWith({ + include: { + jurisdictions: true, + }, + where: { + AND: [ + { + jurisdictions: { + id: 'test name', + }, + }, + ], + }, + }); + }); + + it('testing findOne() with id present', async () => { + const date = new Date(); + const jurisdictionData = { + id: 'example Id', + name: 'example name', + }; + const mockedValue = mockAmiChart(3, date, jurisdictionData); + + prisma.amiChart.findUnique = jest.fn().mockResolvedValue(mockedValue); + + expect(await service.findOne('example Id')).toEqual({ + id: mockedValue.id, + name: 'ami 3', + jurisdictions: jurisdictionData, + createdAt: date, + updatedAt: date, + items: [ + { + percentOfAmi: 0, + householdSize: 0, + income: 0, + }, + { + percentOfAmi: 1, + householdSize: 1, + income: 1, + }, + { + percentOfAmi: 2, + householdSize: 2, + income: 2, + }, + ], + }); + + expect(prisma.amiChart.findUnique).toHaveBeenCalledWith({ + include: { + jurisdictions: true, + }, + where: { + id: 'example Id', + }, + }); + }); + + it('testing findOne() with id not present', async () => { + prisma.amiChart.findUnique = jest.fn().mockResolvedValue(null); + + await expect( + async () => await service.findOne('example Id'), + ).rejects.toThrowError(); + + expect(prisma.amiChart.findUnique).toHaveBeenCalledWith({ + include: { + jurisdictions: true, + }, + where: { + id: 'example Id', + }, + }); + }); + + it('testing create()', async () => { + const date = new Date(); + const jurisdictionData = { + id: 'example Id', + name: 'example name', + }; + const mockedValue = mockAmiChart(3, date, jurisdictionData); + prisma.amiChart.create = jest.fn().mockResolvedValue(mockedValue); + + const params: AmiChartCreate = { + jurisdictions: jurisdictionData, + items: [ + { + percentOfAmi: 0, + householdSize: 0, + income: 0, + }, + { + percentOfAmi: 1, + householdSize: 1, + income: 1, + }, + { + percentOfAmi: 2, + householdSize: 2, + income: 2, + }, + ], + name: 'ami 3', + }; + + expect(await service.create(params)).toEqual({ + id: mockedValue.id, + name: 'ami 3', + jurisdictions: jurisdictionData, + createdAt: date, + updatedAt: date, + items: [ + { + percentOfAmi: 0, + householdSize: 0, + income: 0, + }, + { + percentOfAmi: 1, + householdSize: 1, + income: 1, + }, + { + percentOfAmi: 2, + householdSize: 2, + income: 2, + }, + ], + }); + + expect(prisma.amiChart.create).toHaveBeenCalledWith({ + include: { + jurisdictions: true, + }, + data: { + name: 'ami 3', + jurisdictions: { + connect: { + id: 'example Id', + }, + }, + items: [ + { + percentOfAmi: 0, + householdSize: 0, + income: 0, + }, + { + percentOfAmi: 1, + householdSize: 1, + income: 1, + }, + { + percentOfAmi: 2, + householdSize: 2, + income: 2, + }, + ], + }, + }); + }); + + it('testing update() existing record found', async () => { + const date = new Date(); + const jurisdictionData = { + id: 'example Id', + name: 'example name', + }; + const mockedAmi = mockAmiChart(3, date, jurisdictionData); + + prisma.amiChart.findUnique = jest.fn().mockResolvedValue(mockedAmi); + prisma.amiChart.update = jest.fn().mockResolvedValue({ + ...mockedAmi, + name: 'updated ami 3', + items: [ + { + percentOfAmi: 0, + householdSize: 0, + income: 0, + }, + { + percentOfAmi: 1, + householdSize: 1, + income: 1, + }, + { + percentOfAmi: 2, + householdSize: 2, + income: 2, + }, + { + percentOfAmi: 3, + householdSize: 3, + income: 3, + }, + ], + }); + + const params: AmiChartUpdate = { + items: [ + { + percentOfAmi: 0, + householdSize: 0, + income: 0, + }, + { + percentOfAmi: 1, + householdSize: 1, + income: 1, + }, + { + percentOfAmi: 2, + householdSize: 2, + income: 2, + }, + { + percentOfAmi: 3, + householdSize: 3, + income: 3, + }, + ], + name: 'updated ami 3', + id: mockedAmi.id, + }; + + expect(await service.update(params)).toEqual({ + id: mockedAmi.id, + name: 'updated ami 3', + jurisdictions: jurisdictionData, + createdAt: date, + updatedAt: date, + items: [ + { + percentOfAmi: 0, + householdSize: 0, + income: 0, + }, + { + percentOfAmi: 1, + householdSize: 1, + income: 1, + }, + { + percentOfAmi: 2, + householdSize: 2, + income: 2, + }, + { + percentOfAmi: 3, + householdSize: 3, + income: 3, + }, + ], + }); + + expect(prisma.amiChart.findUnique).toHaveBeenCalledWith({ + where: { + id: mockedAmi.id, + }, + }); + + expect(prisma.amiChart.update).toHaveBeenCalledWith({ + include: { + jurisdictions: true, + }, + data: { + name: 'updated ami 3', + items: [ + { + percentOfAmi: 0, + householdSize: 0, + income: 0, + }, + { + percentOfAmi: 1, + householdSize: 1, + income: 1, + }, + { + percentOfAmi: 2, + householdSize: 2, + income: 2, + }, + { + percentOfAmi: 3, + householdSize: 3, + income: 3, + }, + ], + }, + where: { + id: mockedAmi.id, + }, + }); + }); + + it('testing update() existing record not found', async () => { + prisma.amiChart.findUnique = jest.fn().mockResolvedValue(null); + prisma.amiChart.update = jest.fn().mockResolvedValue(null); + + const params: AmiChartUpdate = { + items: [ + { + percentOfAmi: 0, + householdSize: 0, + income: 0, + }, + { + percentOfAmi: 1, + householdSize: 1, + income: 1, + }, + { + percentOfAmi: 2, + householdSize: 2, + income: 2, + }, + { + percentOfAmi: 3, + householdSize: 3, + income: 3, + }, + ], + name: 'updated ami 3', + id: 'example ami id', + }; + + await expect( + async () => await service.update(params), + ).rejects.toThrowError(); + + expect(prisma.amiChart.findUnique).toHaveBeenCalledWith({ + where: { + id: 'example ami id', + }, + }); + }); + + it('testing delete()', async () => { + const date = new Date(); + const jurisdictionData = undefined; + prisma.amiChart.findUnique = jest + .fn() + .mockResolvedValue(mockAmiChart(3, date, jurisdictionData)); + + prisma.amiChart.delete = jest + .fn() + .mockResolvedValue(mockAmiChart(3, date, jurisdictionData)); + + expect(await service.delete('example Id')).toEqual({ + success: true, + }); + + expect(prisma.amiChart.findUnique).toHaveBeenCalledWith({ + where: { + id: 'example Id', + }, + }); + + expect(prisma.amiChart.delete).toHaveBeenCalledWith({ + where: { + id: 'example Id', + }, + }); + }); + + it('testing findOrThrow() record found', async () => { + prisma.amiChart.findUnique = jest.fn().mockResolvedValue(null); + + await expect( + async () => await service.findOrThrow('example id'), + ).rejects.toThrowError(); + + expect(prisma.amiChart.findUnique).toHaveBeenCalledWith({ + where: { + id: 'example id', + }, + }); + }); + + it('testing findOrThrow() record not found', async () => { + const date = new Date(); + const jurisdictionData = { + id: 'example Id', + name: 'example name', + }; + const mockedAmi = mockAmiChart(3, date, jurisdictionData); + prisma.amiChart.findUnique = jest.fn().mockResolvedValue(mockedAmi); + + expect(await service.findOrThrow('example id')).toEqual(true); + + expect(prisma.amiChart.findUnique).toHaveBeenCalledWith({ + where: { + id: 'example id', + }, + }); + }); +}); diff --git a/backend_new/test/unit/services/listing.service.spec.ts b/backend_new/test/unit/services/listing.service.spec.ts index 965dd7a4e3..10e5988e07 100644 --- a/backend_new/test/unit/services/listing.service.spec.ts +++ b/backend_new/test/unit/services/listing.service.spec.ts @@ -68,6 +68,9 @@ const mockListing = ( name: `AMI Name ${i}`, createdAt: date, updatedAt: date, + jurisdictions: { + id: 'jurisdiction ID', + }, }, }); } diff --git a/backend_new/test/unit/utilities/unit-utilities.spec.ts b/backend_new/test/unit/utilities/unit-utilities.spec.ts index 618d0d0735..14ac4a39c5 100644 --- a/backend_new/test/unit/utilities/unit-utilities.spec.ts +++ b/backend_new/test/unit/utilities/unit-utilities.spec.ts @@ -1,4 +1,4 @@ -import { AmiChart } from '../../../src/dtos/units/ami-chart-get.dto'; +import { AmiChart } from '../../../src/dtos/ami-charts/ami-chart.dto'; import { UnitAmiChartOverride } from '../../../src/dtos/units/ami-chart-override-get.dto'; import { generateHmiData, @@ -22,6 +22,9 @@ const unit: Unit = { updatedAt: new Date(), items: [], name: 'ami1', + jurisdictions: { + id: 'id', + }, }, id: 'example', }; @@ -45,6 +48,9 @@ const generateAmiChart = (): AmiChart => { ...defaultValues, id: 'ami1', name: 'ami1', + jurisdictions: { + id: 'id', + }, items: generateAmiChartItems(8, 30, 30_000), }; }; @@ -148,6 +154,9 @@ describe('Unit Transformations', () => { updatedAt: new Date(), items: [], name: 'ami2', + jurisdictions: { + id: 'id', + }, }, }, ], diff --git a/backend_new/types/src/backend-swagger.ts b/backend_new/types/src/backend-swagger.ts index d152f57483..51c11dcdd7 100644 --- a/backend_new/types/src/backend-swagger.ts +++ b/backend_new/types/src/backend-swagger.ts @@ -180,6 +180,142 @@ export class ListingsService { } } +export class AmiChartsService { + /** + * List amiCharts + */ + list( + params: { + /** */ + jurisdictionId?: string; + } = {} as any, + options: IRequestOptions = {}, + ): Promise { + return new Promise((resolve, reject) => { + let url = basePath + '/amiCharts'; + + const configs: IRequestConfig = getConfigs( + 'get', + 'application/json', + url, + options, + ); + configs.params = { jurisdictionId: params['jurisdictionId'] }; + + /** 适配ios13getèŻ·æ±‚äžć…èźžćžŠbody */ + + axios(configs, resolve, reject); + }); + } + /** + * Create amiChart + */ + create( + params: { + /** requestBody */ + body?: AmiChartCreate; + } = {} as any, + options: IRequestOptions = {}, + ): Promise { + return new Promise((resolve, reject) => { + let url = basePath + '/amiCharts'; + + const configs: IRequestConfig = getConfigs( + 'post', + 'application/json', + url, + options, + ); + + let data = params.body; + + configs.data = data; + + axios(configs, resolve, reject); + }); + } + /** + * Delete amiChart by id + */ + delete( + params: { + /** requestBody */ + body?: IdDTO; + } = {} as any, + options: IRequestOptions = {}, + ): Promise { + return new Promise((resolve, reject) => { + let url = basePath + '/amiCharts'; + + const configs: IRequestConfig = getConfigs( + 'delete', + 'application/json', + url, + options, + ); + + let data = params.body; + + configs.data = data; + + axios(configs, resolve, reject); + }); + } + /** + * Get amiChart by id + */ + retrieve( + params: { + /** */ + amiChartId: string; + } = {} as any, + options: IRequestOptions = {}, + ): Promise { + return new Promise((resolve, reject) => { + let url = basePath + '/amiCharts/{amiChartId}'; + url = url.replace('{amiChartId}', params['amiChartId'] + ''); + + const configs: IRequestConfig = getConfigs( + 'get', + 'application/json', + url, + options, + ); + + /** 适配ios13getèŻ·æ±‚äžć…èźžćžŠbody */ + + axios(configs, resolve, reject); + }); + } + /** + * Update amiChart + */ + update( + params: { + /** requestBody */ + body?: AmiChartUpdate; + } = {} as any, + options: IRequestOptions = {}, + ): Promise { + return new Promise((resolve, reject) => { + let url = basePath + '/amiCharts/{amiChartId}'; + + const configs: IRequestConfig = getConfigs( + 'put', + 'application/json', + url, + options, + ); + + let data = params.body; + + configs.data = data; + + axios(configs, resolve, reject); + }); + } +} + export interface ListingsQueryParams { /** */ page?: number; @@ -243,13 +379,40 @@ export interface PaginationAllowsAllQueryParams { } export interface ApplicationMethod { + /** */ + id: string; + + /** */ + createdAt: Date; + + /** */ + updatedAt: Date; + /** */ type: ApplicationMethodsTypeEnum; } -export interface UnitType {} +export interface UnitType { + /** */ + id: string; -export interface UnitAccessibilityPriorityType {} + /** */ + createdAt: Date; + + /** */ + updatedAt: Date; +} + +export interface UnitAccessibilityPriorityType { + /** */ + id: string; + + /** */ + createdAt: Date; + + /** */ + updatedAt: Date; +} export interface MinMaxCurrency { /** */ @@ -333,6 +496,15 @@ export interface UnitsSummarized { } export interface ListingGet { + /** */ + id: string; + + /** */ + createdAt: Date; + + /** */ + updatedAt: Date; + /** */ applicationPickUpAddressType: ApplicationAddressTypeEnum; @@ -363,6 +535,80 @@ export interface PaginatedListing { items: ListingGet[]; } +export interface AmiChartItem { + /** */ + percentOfAmi: number; + + /** */ + householdSize: number; + + /** */ + income: number; +} + +export interface IdDTO { + /** */ + id: string; + + /** */ + name?: string; +} + +export interface AmiChartCreate { + /** */ + items: AmiChartItem[]; + + /** */ + name: string; + + /** */ + jurisdictions: IdDTO; +} + +export interface AmiChartUpdate { + /** */ + id: string; + + /** */ + items: AmiChartItem[]; + + /** */ + name: string; + + /** */ + jurisdictions: IdDTO; +} + +export interface AmiChartQueryParams { + /** */ + jurisdictionId?: string; +} + +export interface AmiChart { + /** */ + id: string; + + /** */ + createdAt: Date; + + /** */ + updatedAt: Date; + + /** */ + items: AmiChartItem[]; + + /** */ + name: string; + + /** */ + jurisdictions: IdDTO; +} + +export interface SuccessDTO { + /** */ + success: boolean; +} + export enum ListingViews { 'fundamentals' = 'fundamentals', 'base' = 'base', diff --git a/backend_new/yarn.lock b/backend_new/yarn.lock index 1d503955a5..c338a362bb 100644 --- a/backend_new/yarn.lock +++ b/backend_new/yarn.lock @@ -389,10 +389,10 @@ minimatch "^3.1.2" strip-json-comments "^3.1.1" -"@eslint/js@8.40.0": - version "8.40.0" - resolved "https://registry.yarnpkg.com/@eslint/js/-/js-8.40.0.tgz#3ba73359e11f5a7bd3e407f70b3528abfae69cec" - integrity sha512-ElyB54bJIhXQYVKjDSvCkPO1iU1tSAeVQJbllWJq1XQSmmA4dgFk8CbiBGpiOPxleE48vDogxCtmMYku4HSVLA== +"@eslint/js@8.41.0": + version "8.41.0" + resolved "https://registry.yarnpkg.com/@eslint/js/-/js-8.41.0.tgz#080321c3b68253522f7646b55b577dd99d2950b3" + integrity sha512-LxcyMGxwmTh2lY9FwHPGWOHmYFCZvbrFCBZL4FzSSsxsRPuhrYUg/49/0KDfW8tnIEaEHtfmn6+NPN+1DqaNmA== "@humanwhocodes/config-array@^0.11.8": version "0.11.8" @@ -794,10 +794,10 @@ resolved "https://registry.yarnpkg.com/@prisma/engines-version/-/engines-version-4.14.0-67.d9a4c5988f480fa576d43970d5a23641aa77bc9c.tgz#0aeca447c4a5f23c83f68b8033e627b60bc01850" integrity sha512-3jum8/YSudeSN0zGW5qkpz+wAN2V/NYCQ+BPjvHYDfWatLWlQkqy99toX0GysDeaUoBIJg1vaz2yKqiA3CFcQw== -"@prisma/engines@4.13.0": - version "4.13.0" - resolved "https://registry.yarnpkg.com/@prisma/engines/-/engines-4.13.0.tgz#582a6b90b6efeb0f465984f1fe0e72a4afaaa5ae" - integrity sha512-HrniowHRZXHuGT9XRgoXEaP2gJLXM5RMoItaY2PkjvuZ+iHc0Zjbm/302MB8YsPdWozAPHHn+jpFEcEn71OgPw== +"@prisma/engines@4.14.1": + version "4.14.1" + resolved "https://registry.yarnpkg.com/@prisma/engines/-/engines-4.14.1.tgz#dac49f8d1f2d4f14a8ed7e6f96b24cd49bd6cd91" + integrity sha512-APqFddPVHYmWNKqc+5J5SqrLFfOghKOLZxobmguDUacxOwdEutLsbXPVhNnpFDmuQWQFbXmrTTPoRrrF6B1MWA== "@sinonjs/commons@^1.7.0": version "1.8.6" @@ -834,9 +834,9 @@ integrity sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow== "@tsconfig/node16@^1.0.2": - version "1.0.3" - resolved "https://registry.yarnpkg.com/@tsconfig/node16/-/node16-1.0.3.tgz#472eaab5f15c1ffdd7f8628bd4c4f753995ec79e" - integrity sha512-yOlFc+7UtL/89t2ZhjPvvB/DeAr3r+Dq58IgzsFkOAvVC6NMJXmCGjbptdXdR9qsX7pKcTL+s87FtYREi2dEEQ== + version "1.0.4" + resolved "https://registry.yarnpkg.com/@tsconfig/node16/-/node16-1.0.4.tgz#0b92dcc0cc1c81f6f306a381f28e31b1a56536e9" + integrity sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA== "@types/babel__core@^7.0.0", "@types/babel__core@^7.1.14": version "7.20.0" @@ -918,9 +918,9 @@ integrity sha512-CuPgU6f3eT/XgKKPqKd/gLZV1Xmvf1a2R5POBOGQa6uv82xpls89HU5zKeVoyR8XzHd1RGNOlQlvUe3CFkjWNQ== "@types/express-serve-static-core@^4.17.33": - version "4.17.34" - resolved "https://registry.yarnpkg.com/@types/express-serve-static-core/-/express-serve-static-core-4.17.34.tgz#c119e85b75215178bc127de588e93100698ab4cc" - integrity sha512-fvr49XlCGoUj2Pp730AItckfjat4WNb0lb3kfrLWffd+RLeoGAMsq7UOy04PAPtoL01uKwcp6u8nhzpgpDYr3w== + version "4.17.35" + resolved "https://registry.yarnpkg.com/@types/express-serve-static-core/-/express-serve-static-core-4.17.35.tgz#c95dd4424f0d32e525d23812aa8ab8e4d3906c4f" + integrity sha512-wALWQwrgiB2AWTT91CB62b6Yt0sNHpznUXeZEcnPU3DRdlDIz74x8Qg1UUYKSVFi+va5vKOLYRBI1bRKiLLKIg== dependencies: "@types/node" "*" "@types/qs" "*" @@ -971,11 +971,16 @@ jest-matcher-utils "^27.0.0" pretty-format "^27.0.0" -"@types/json-schema@*", "@types/json-schema@^7.0.8", "@types/json-schema@^7.0.9": +"@types/json-schema@*", "@types/json-schema@^7.0.8": version "7.0.11" resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.11.tgz#d421b6c527a3037f7c84433fd2c4229e016863d3" integrity sha512-wOuvG1SN4Us4rez+tylwwwCV1psiNVOkJeM3AUWUNWg/jDQY2+HE/444y5gc+jBmRqASOm2Oeh5c1axHobwRKQ== +"@types/json-schema@^7.0.9": + version "7.0.12" + resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.12.tgz#d70faba7039d5fca54c83c7dbab41051d2b6f6cb" + integrity sha512-Hr5Jfhc9eYOQNPYO5WLDq/n4jqijdHNlDXjuAQkkt+mWdQR+XJToOHrsD4cPaMXpn6KO7y2+wM8AZEs8VpBLVA== + "@types/json5@^0.0.29": version "0.0.29" resolved "https://registry.yarnpkg.com/@types/json5/-/json5-0.0.29.tgz#ee28707ae94e11d2b827bcbe5270bcea7f3e71ee" @@ -1002,9 +1007,9 @@ integrity sha512-O+z53uwx64xY7D6roOi4+jApDGFg0qn6WHcxe5QeqjMaTezBO/mxdfFXIVAVVyNWKx84OmPB3L8kbVYOTeN34A== "@types/node@^16.0.0": - version "16.18.26" - resolved "https://registry.yarnpkg.com/@types/node/-/node-16.18.26.tgz#a18b88726a67bc6a8a5bdac9a40c093ecb03ccd0" - integrity sha512-pCNBzNQqCXE4A6FWDmrn/o1Qu+qBf8tnorBlNoPNSBQJF+jXzvTKNI/aMiE+hGJbK5sDAD65g7OS/YwSHIEJdw== + version "16.18.34" + resolved "https://registry.yarnpkg.com/@types/node/-/node-16.18.34.tgz#62d2099b30339dec4b1b04a14c96266459d7c8b2" + integrity sha512-VmVm7gXwhkUimRfBwVI1CHhwp86jDWR04B5FGebMMyxV90SlCmFujwUHrxTD4oO+SOYU86SoxvhgeRQJY7iXFg== "@types/parse-json@^4.0.0": version "4.0.0" @@ -1027,9 +1032,9 @@ integrity sha512-EEhsLsD6UsDM1yFhAvy0Cjr6VwmpMWqFBCb9w07wVugF7w9nfajxLuVmngTIpgS6svCnm6Vaw+MZhoDCKnOfsw== "@types/semver@^7.3.12": - version "7.3.13" - resolved "https://registry.yarnpkg.com/@types/semver/-/semver-7.3.13.tgz#da4bfd73f49bd541d28920ab0e2bf0ee80f71c91" - integrity sha512-21cFJr9z3g5dW8B0CVI9g2O9beqaThGQ6ZFBqHfwhzLDKUxaqTIy3vnfah/UPkfOiF2pLq+tGz+W8RyCskuslw== + version "7.5.0" + resolved "https://registry.yarnpkg.com/@types/semver/-/semver-7.5.0.tgz#591c1ce3a702c45ee15f47a42ade72c2fd78978a" + integrity sha512-G8hZ6XJiHnuhQKR7ZmysCeJWE08o8T0AXtk5darsCaTVsYZhhgUrq53jizaR2FvsoeCwJhlmwTjkXBY5Pn/ZHw== "@types/send@*": version "0.17.1" @@ -1085,14 +1090,14 @@ "@types/yargs-parser" "*" "@typescript-eslint/eslint-plugin@^5.0.0": - version "5.59.2" - resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.59.2.tgz#684a2ce7182f3b4dac342eef7caa1c2bae476abd" - integrity sha512-yVrXupeHjRxLDcPKL10sGQ/QlVrA8J5IYOEWVqk0lJaSZP7X5DfnP7Ns3cc74/blmbipQ1htFNVGsHX6wsYm0A== + version "5.59.7" + resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.59.7.tgz#e470af414f05ecfdc05a23e9ce6ec8f91db56fe2" + integrity sha512-BL+jYxUFIbuYwy+4fF86k5vdT9lT0CNJ6HtwrIvGh0PhH8s0yy5rjaKH2fDCrz5ITHy07WCzVGNvAmjJh4IJFA== dependencies: "@eslint-community/regexpp" "^4.4.0" - "@typescript-eslint/scope-manager" "5.59.2" - "@typescript-eslint/type-utils" "5.59.2" - "@typescript-eslint/utils" "5.59.2" + "@typescript-eslint/scope-manager" "5.59.7" + "@typescript-eslint/type-utils" "5.59.7" + "@typescript-eslint/utils" "5.59.7" debug "^4.3.4" grapheme-splitter "^1.0.4" ignore "^5.2.0" @@ -1101,71 +1106,71 @@ tsutils "^3.21.0" "@typescript-eslint/parser@^5.0.0": - version "5.59.2" - resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-5.59.2.tgz#c2c443247901d95865b9f77332d9eee7c55655e8" - integrity sha512-uq0sKyw6ao1iFOZZGk9F8Nro/8+gfB5ezl1cA06SrqbgJAt0SRoFhb9pXaHvkrxUpZaoLxt8KlovHNk8Gp6/HQ== + version "5.59.7" + resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-5.59.7.tgz#02682554d7c1028b89aa44a48bf598db33048caa" + integrity sha512-VhpsIEuq/8i5SF+mPg9jSdIwgMBBp0z9XqjiEay+81PYLJuroN+ET1hM5IhkiYMJd9MkTz8iJLt7aaGAgzWUbQ== dependencies: - "@typescript-eslint/scope-manager" "5.59.2" - "@typescript-eslint/types" "5.59.2" - "@typescript-eslint/typescript-estree" "5.59.2" + "@typescript-eslint/scope-manager" "5.59.7" + "@typescript-eslint/types" "5.59.7" + "@typescript-eslint/typescript-estree" "5.59.7" debug "^4.3.4" -"@typescript-eslint/scope-manager@5.59.2": - version "5.59.2" - resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-5.59.2.tgz#f699fe936ee4e2c996d14f0fdd3a7da5ba7b9a4c" - integrity sha512-dB1v7ROySwQWKqQ8rEWcdbTsFjh2G0vn8KUyvTXdPoyzSL6lLGkiXEV5CvpJsEe9xIdKV+8Zqb7wif2issoOFA== +"@typescript-eslint/scope-manager@5.59.7": + version "5.59.7" + resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-5.59.7.tgz#0243f41f9066f3339d2f06d7f72d6c16a16769e2" + integrity sha512-FL6hkYWK9zBGdxT2wWEd2W8ocXMu3K94i3gvMrjXpx+koFYdYV7KprKfirpgY34vTGzEPPuKoERpP8kD5h7vZQ== dependencies: - "@typescript-eslint/types" "5.59.2" - "@typescript-eslint/visitor-keys" "5.59.2" + "@typescript-eslint/types" "5.59.7" + "@typescript-eslint/visitor-keys" "5.59.7" -"@typescript-eslint/type-utils@5.59.2": - version "5.59.2" - resolved "https://registry.yarnpkg.com/@typescript-eslint/type-utils/-/type-utils-5.59.2.tgz#0729c237503604cd9a7084b5af04c496c9a4cdcf" - integrity sha512-b1LS2phBOsEy/T381bxkkywfQXkV1dWda/z0PhnIy3bC5+rQWQDS7fk9CSpcXBccPY27Z6vBEuaPBCKCgYezyQ== +"@typescript-eslint/type-utils@5.59.7": + version "5.59.7" + resolved "https://registry.yarnpkg.com/@typescript-eslint/type-utils/-/type-utils-5.59.7.tgz#89c97291371b59eb18a68039857c829776f1426d" + integrity sha512-ozuz/GILuYG7osdY5O5yg0QxXUAEoI4Go3Do5xeu+ERH9PorHBPSdvD3Tjp2NN2bNLh1NJQSsQu2TPu/Ly+HaQ== dependencies: - "@typescript-eslint/typescript-estree" "5.59.2" - "@typescript-eslint/utils" "5.59.2" + "@typescript-eslint/typescript-estree" "5.59.7" + "@typescript-eslint/utils" "5.59.7" debug "^4.3.4" tsutils "^3.21.0" -"@typescript-eslint/types@5.59.2": - version "5.59.2" - resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-5.59.2.tgz#b511d2b9847fe277c5cb002a2318bd329ef4f655" - integrity sha512-LbJ/HqoVs2XTGq5shkiKaNTuVv5tTejdHgfdjqRUGdYhjW1crm/M7og2jhVskMt8/4wS3T1+PfFvL1K3wqYj4w== +"@typescript-eslint/types@5.59.7": + version "5.59.7" + resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-5.59.7.tgz#6f4857203fceee91d0034ccc30512d2939000742" + integrity sha512-UnVS2MRRg6p7xOSATscWkKjlf/NDKuqo5TdbWck6rIRZbmKpVNTLALzNvcjIfHBE7736kZOFc/4Z3VcZwuOM/A== -"@typescript-eslint/typescript-estree@5.59.2": - version "5.59.2" - resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-5.59.2.tgz#6e2fabd3ba01db5d69df44e0b654c0b051fe9936" - integrity sha512-+j4SmbwVmZsQ9jEyBMgpuBD0rKwi9RxRpjX71Brr73RsYnEr3Lt5QZ624Bxphp8HUkSKfqGnPJp1kA5nl0Sh7Q== +"@typescript-eslint/typescript-estree@5.59.7": + version "5.59.7" + resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-5.59.7.tgz#b887acbd4b58e654829c94860dbff4ac55c5cff8" + integrity sha512-4A1NtZ1I3wMN2UGDkU9HMBL+TIQfbrh4uS0WDMMpf3xMRursDbqEf1ahh6vAAe3mObt8k3ZATnezwG4pdtWuUQ== dependencies: - "@typescript-eslint/types" "5.59.2" - "@typescript-eslint/visitor-keys" "5.59.2" + "@typescript-eslint/types" "5.59.7" + "@typescript-eslint/visitor-keys" "5.59.7" debug "^4.3.4" globby "^11.1.0" is-glob "^4.0.3" semver "^7.3.7" tsutils "^3.21.0" -"@typescript-eslint/utils@5.59.2": - version "5.59.2" - resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-5.59.2.tgz#0c45178124d10cc986115885688db6abc37939f4" - integrity sha512-kSuF6/77TZzyGPhGO4uVp+f0SBoYxCDf+lW3GKhtKru/L8k/Hd7NFQxyWUeY7Z/KGB2C6Fe3yf2vVi4V9TsCSQ== +"@typescript-eslint/utils@5.59.7": + version "5.59.7" + resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-5.59.7.tgz#7adf068b136deae54abd9a66ba5a8780d2d0f898" + integrity sha512-yCX9WpdQKaLufz5luG4aJbOpdXf/fjwGMcLFXZVPUz3QqLirG5QcwwnIHNf8cjLjxK4qtzTO8udUtMQSAToQnQ== dependencies: "@eslint-community/eslint-utils" "^4.2.0" "@types/json-schema" "^7.0.9" "@types/semver" "^7.3.12" - "@typescript-eslint/scope-manager" "5.59.2" - "@typescript-eslint/types" "5.59.2" - "@typescript-eslint/typescript-estree" "5.59.2" + "@typescript-eslint/scope-manager" "5.59.7" + "@typescript-eslint/types" "5.59.7" + "@typescript-eslint/typescript-estree" "5.59.7" eslint-scope "^5.1.1" semver "^7.3.7" -"@typescript-eslint/visitor-keys@5.59.2": - version "5.59.2" - resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-5.59.2.tgz#37a419dc2723a3eacbf722512b86d6caf7d3b750" - integrity sha512-EEpsO8m3RASrKAHI9jpavNv9NlEUebV4qmF1OWxSTtKSFBpC1NCmWazDQHFivRf0O1DV11BA645yrLEVQ0/Lig== +"@typescript-eslint/visitor-keys@5.59.7": + version "5.59.7" + resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-5.59.7.tgz#09c36eaf268086b4fbb5eb9dc5199391b6485fc5" + integrity sha512-tyN+X2jvMslUszIiYbF0ZleP+RqQsFVpGrKI6e0Eet1w8WmhsAtmzaqm8oM8WJQ1ysLwhnsK/4hYHJjOgJVfQQ== dependencies: - "@typescript-eslint/types" "5.59.2" + "@typescript-eslint/types" "5.59.7" eslint-visitor-keys "^3.3.0" "@webassemblyjs/ast@1.11.1": @@ -1321,9 +1326,9 @@ acorn-globals@^6.0.0: acorn-walk "^7.1.1" acorn-import-assertions@^1.7.6: - version "1.8.0" - resolved "https://registry.yarnpkg.com/acorn-import-assertions/-/acorn-import-assertions-1.8.0.tgz#ba2b5939ce62c238db6d93d81c9b111b29b855e9" - integrity sha512-m7VZ3jwz4eK6A4Vtt8Ew1/mNbP24u0FhdyfA7fSvnJR6LMdfOYnmuIrrJAgrYfYJ10F/otaHTtrtrtmHdMNzEw== + version "1.9.0" + resolved "https://registry.yarnpkg.com/acorn-import-assertions/-/acorn-import-assertions-1.9.0.tgz#507276249d684797c84e0734ef84860334cfb1ac" + integrity sha512-cmMwop9x+8KFhxvKrKfPYmN6/pKTYYHBqLa0DfvVZcKMJWNyWLnaqND7dx/qn66R7ewM1UX5XMaDVP5wlVTaVA== acorn-jsx@^5.3.2: version "5.3.2" @@ -2135,7 +2140,7 @@ end-of-stream@^1.1.0: dependencies: once "^1.4.0" -enhanced-resolve@^5.0.0, enhanced-resolve@^5.7.0, enhanced-resolve@^5.9.3: +enhanced-resolve@^5.0.0, enhanced-resolve@^5.7.0: version "5.13.0" resolved "https://registry.yarnpkg.com/enhanced-resolve/-/enhanced-resolve-5.13.0.tgz#26d1ecc448c02de997133217b5c1053f34a0a275" integrity sha512-eyV8f0y1+bzyfh8xAwW/WTSZpLbjhqc4ne9eGSH4Zo2ejdyiNG9pU6mf9DG8a7+Auk6MFTlNOT4Y2y/9k8GKVg== @@ -2143,6 +2148,14 @@ enhanced-resolve@^5.0.0, enhanced-resolve@^5.7.0, enhanced-resolve@^5.9.3: graceful-fs "^4.2.4" tapable "^2.2.0" +enhanced-resolve@^5.9.3: + version "5.14.1" + resolved "https://registry.yarnpkg.com/enhanced-resolve/-/enhanced-resolve-5.14.1.tgz#de684b6803724477a4af5d74ccae5de52c25f6b3" + integrity sha512-Vklwq2vDKtl0y/vtwjSesgJ5MYS7Etuk5txS8VdKL4AOS1aUlD96zqIfsOSLQsdv3xgMRbtkWM8eG9XDfKUPow== + dependencies: + graceful-fs "^4.2.4" + tapable "^2.2.0" + error-ex@^1.3.1: version "1.3.2" resolved "https://registry.yarnpkg.com/error-ex/-/error-ex-1.3.2.tgz#b4ac40648107fdcdcfae242f428bea8a14d4f1bf" @@ -2226,14 +2239,14 @@ eslint-visitor-keys@^3.3.0, eslint-visitor-keys@^3.4.1: integrity sha512-pZnmmLwYzf+kWaM/Qgrvpen51upAktaaiI01nsJD/Yr3lMOdNtq0cxkrrg16w64VtisN6okbs7Q8AfGqj4c9fA== eslint@^8.0.1: - version "8.40.0" - resolved "https://registry.yarnpkg.com/eslint/-/eslint-8.40.0.tgz#a564cd0099f38542c4e9a2f630fa45bf33bc42a4" - integrity sha512-bvR+TsP9EHL3TqNtj9sCNJVAFK3fBN8Q7g5waghxyRsPLIMwL73XSKnZFK0hk/O2ANC+iAoq6PWMQ+IfBAJIiQ== + version "8.41.0" + resolved "https://registry.yarnpkg.com/eslint/-/eslint-8.41.0.tgz#3062ca73363b4714b16dbc1e60f035e6134b6f1c" + integrity sha512-WQDQpzGBOP5IrXPo4Hc0814r4/v2rrIsB0rhT7jtunIalgg6gYXWhRMOejVO8yH21T/FGaxjmFjBMNqcIlmH1Q== dependencies: "@eslint-community/eslint-utils" "^4.2.0" "@eslint-community/regexpp" "^4.4.0" "@eslint/eslintrc" "^2.0.3" - "@eslint/js" "8.40.0" + "@eslint/js" "8.41.0" "@humanwhocodes/config-array" "^0.11.8" "@humanwhocodes/module-importer" "^1.0.1" "@nodelib/fs.walk" "^1.2.8" @@ -2253,13 +2266,12 @@ eslint@^8.0.1: find-up "^5.0.0" glob-parent "^6.0.2" globals "^13.19.0" - grapheme-splitter "^1.0.4" + graphemer "^1.4.0" ignore "^5.2.0" import-fresh "^3.0.0" imurmurhash "^0.1.4" is-glob "^4.0.0" is-path-inside "^3.0.3" - js-sdsl "^4.1.4" js-yaml "^4.1.0" json-stable-stringify-without-jsonify "^1.0.1" levn "^0.4.1" @@ -2718,6 +2730,11 @@ grapheme-splitter@^1.0.4: resolved "https://registry.yarnpkg.com/grapheme-splitter/-/grapheme-splitter-1.0.4.tgz#9cf3a665c6247479896834af35cf1dbb4400767e" integrity sha512-bzh50DW9kTPM00T8y4o8vQg89Di9oLJVLW/KaOGIXJWP/iqCN6WKYkbNOF04vFLJhwcpYUh9ydh/+5vpOqV4YQ== +graphemer@^1.4.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/graphemer/-/graphemer-1.4.0.tgz#fb2f1d55e0e3a1849aeffc90c4fa0dd53a0e66c6" + integrity sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag== + has-flag@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-3.0.0.tgz#b5d454dc2199ae225699f3467e5a07f3b955bafd" @@ -3433,11 +3450,6 @@ jest@^27.2.5: import-local "^3.0.2" jest-cli "^27.5.1" -js-sdsl@^4.1.4: - version "4.4.0" - resolved "https://registry.yarnpkg.com/js-sdsl/-/js-sdsl-4.4.0.tgz#8b437dbe642daa95760400b602378ed8ffea8430" - integrity sha512-FfVSdx6pJ41Oa+CF7RDaFmTnCaFhua+SNYQX74riGOpl96x+2jQCqEfQ2bnXu/5DPCqlRuiqyvTJM0Qjz26IVg== - js-tokens@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499" @@ -4113,11 +4125,11 @@ pretty-format@^27.0.0, pretty-format@^27.5.1: react-is "^17.0.1" prisma@^4.13.0: - version "4.13.0" - resolved "https://registry.yarnpkg.com/prisma/-/prisma-4.13.0.tgz#0b83f40acf50cd47d7463a135c4e9b275713e602" - integrity sha512-L9mqjnSmvWIRCYJ9mQkwCtj4+JDYYTdhoyo8hlsHNDXaZLh/b4hR0IoKIBbTKxZuyHQzLopb/+0Rvb69uGV7uA== + version "4.14.1" + resolved "https://registry.yarnpkg.com/prisma/-/prisma-4.14.1.tgz#7a6bb4ce847a9d08deabb6acdf3116fff15e1316" + integrity sha512-z6hxzTMYqT9SIKlzD08dhzsLUpxjFKKsLpp5/kBDnSqiOjtUyyl/dC5tzxLcOa3jkEHQ8+RpB/fE3w8bgNP51g== dependencies: - "@prisma/engines" "4.13.0" + "@prisma/engines" "4.14.1" process-nextick-args@~2.0.0: version "2.0.1" @@ -4170,14 +4182,7 @@ qs@6.10.3: dependencies: side-channel "^1.0.4" -qs@^6.11.0: - version "6.11.1" - resolved "https://registry.yarnpkg.com/qs/-/qs-6.11.1.tgz#6c29dff97f0c0060765911ba65cbc9764186109f" - integrity sha512-0wsrzgTz/kAVIeuxSjnpGC56rzYtr6JT/2BwEvMaPhFIoYa1aGO8LbzuU1R0uUYQkLpWBTOj0l/CLAJB64J6nQ== - dependencies: - side-channel "^1.0.4" - -qs@^6.11.2: +qs@^6.11.0, qs@^6.11.2: version "6.11.2" resolved "https://registry.yarnpkg.com/qs/-/qs-6.11.2.tgz#64bea51f12c1f5da1bc01496f48ffcff7c69d7d9" integrity sha512-tDNIz22aBzCDxLtVH++VnTfzxlfeK5CbqohpSqpJgj1Wg/cQbStNAz3NuqCs5vV+pjBsK4x4pN9HlVh7rcYRiA== @@ -4385,7 +4390,7 @@ schema-utils@^3.1.0, schema-utils@^3.1.1: ajv "^6.12.5" ajv-keywords "^3.5.2" -semver@7.x, semver@^7.3.2, semver@^7.3.4, semver@^7.3.5, semver@^7.3.7, semver@^7.3.8: +semver@7.x, semver@^7.3.2, semver@^7.3.4, semver@^7.3.5: version "7.5.0" resolved "https://registry.yarnpkg.com/semver/-/semver-7.5.0.tgz#ed8c5dc8efb6c629c88b23d41dc9bf40c1d96cd0" integrity sha512-+XC0AD/R7Q2mPSRuy2Id0+CGTZ98+8f+KvwirxOKIEyid+XSx6HbC63p+O4IndTHuX5Z+JxQ0TghCkO5Cg/2HA== @@ -4397,6 +4402,13 @@ semver@^6.0.0, semver@^6.3.0: resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.0.tgz#ee0a64c8af5e8ceea67687b133761e1becbd1d3d" integrity sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw== +semver@^7.3.7, semver@^7.3.8: + version "7.5.1" + resolved "https://registry.yarnpkg.com/semver/-/semver-7.5.1.tgz#c90c4d631cf74720e46b21c1d37ea07edfab91ec" + integrity sha512-Wvss5ivl8TMRZXXESstBA4uR5iXgEN/VC5/sOcuXdVLzcdkz4HWetIoRfG5gb5X+ij/G9rw9YoGn3QoQ8OCSpw== + dependencies: + lru-cache "^6.0.0" + send@0.18.0: version "0.18.0" resolved "https://registry.yarnpkg.com/send/-/send-0.18.0.tgz#670167cc654b05f5aa4a767f9113bb371bc706be" @@ -4810,9 +4822,9 @@ ts-jest@^27.0.3: yargs-parser "20.x" ts-loader@^9.2.3: - version "9.4.2" - resolved "https://registry.yarnpkg.com/ts-loader/-/ts-loader-9.4.2.tgz#80a45eee92dd5170b900b3d00abcfa14949aeb78" - integrity sha512-OmlC4WVmFv5I0PpaxYb+qGeGOdm5giHU7HwDDUjw59emP2UYMHy9fFSDcYgSNoH8sXcj4hGCSEhlDZ9ULeDraA== + version "9.4.3" + resolved "https://registry.yarnpkg.com/ts-loader/-/ts-loader-9.4.3.tgz#55cfa7c28dd82a2de968ae45c3adb75fb888b27e" + integrity sha512-n3hBnm6ozJYzwiwt5YRiJZkzktftRpMiBApHaJPoWLA+qetQBAXkHqCLM6nwSdRDimqVtA5ocIkcTRLMTt7yzA== dependencies: chalk "^4.1.0" enhanced-resolve "^5.0.0" @@ -4878,9 +4890,9 @@ tslib@^1.8.1, tslib@^1.9.0: integrity sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg== tslib@^2.1.0: - version "2.5.0" - resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.5.0.tgz#42bfed86f5787aeb41d031866c8f402429e0fddf" - integrity sha512-336iVw3rtn2BUK7ORdIAHTyxHGRIHVReokCR3XjbckJMK7ms8FysBfhLR8IXnAgy7T0PTPNBWKiH514FOW/WSg== + version "2.5.2" + resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.5.2.tgz#1b6f07185c881557b0ffa84b111a0106989e8338" + integrity sha512-5svOrSA2w3iGFDs1HibEVBGbDrAY82bFQ3HZ3ixB+88nsbsWQoKqDRb5UBYAUPEzbBn6dAp5gRNXglySbx1MlA== tsutils@^3.21.0: version "3.21.0" From c03069c9d91ce22fe3658417fd40780e1332a428 Mon Sep 17 00:00:00 2001 From: Yazeed Loonat Date: Tue, 20 Jun 2023 10:02:59 -0700 Subject: [PATCH 06/57] feat: reserved community type (#3478) * feat: prisma schema generation * fix: cleaning up removed "models" * fix: updates per emily * fix: caught some schema errors * feat: listing get endpoints * feat: adding DTOs * feat: openapi and swagger updates * fix: pass through DTOs * fix: updates per emily * feat: adding ci jobs * fix: round 2 * fix: indentation problem * fix: ci updates * fix: ci test again * fix: adding maybe missing library? * fix: running yarn install * fix: updates per morgan * fix: ci fix round x * fix: test updates * fix: integration tests and ci * fix: update for tests * fix: fixing scheme so migrations can be created * feat: creating ami-chart endpoints * feat: reserved community type * fix: updates per morgan * fix: update backend-new circleci job (#3489) * fix: update backend-new circleci job * fix: add cache * fix: add class validator * fix: update list typing * fix: add class transformer * fix: add listing type * fix: one more attempt at typings * fix: update jest config file * fix: add dbsetup * fix: one more attempt * fix: e2e typing fix * fix: turn off diagnostics for e2e * fix: clear db fields between tests * fix: prettier and linting changes * fix: additional linting fixes --------- Co-authored-by: Yazeed Loonat * fix: updates from what I learned * fix: forgot swagger file * fix: updates per morgan * fix: updates per sean/morgan and test updates * fix: updates per sean and morgan * fix: maybe resolving test race case * fix: removing jurisdictionName param * fix: upates per sean and emily * fix: updates per emily * fix: updates to listing test * fix: trying to remove seeding for e2e testing * fix: updating test description * fix: updates per morgan * fix: adding some more tests --------- Co-authored-by: Morgan Ludtke <42942267+ludtkemorgan@users.noreply.github.com> --- .../prisma/seed-helpers/listing-factory.ts | 21 +- .../reserved-community-type-factory.ts | 14 + backend_new/prisma/seed.ts | 11 +- backend_new/src/app.module.ts | 5 +- .../reserved-community-type.controller.ts | 100 ++++++ .../src/dtos/listings/listing-get.dto.ts | 2 +- .../reserved-community-type-create.dto.ts | 7 + ...eserved-community-type-query-params.dto.ts | 14 + .../reserved-community-type-update.dto.ts | 7 + ....dto.ts => reserved-community-type.dto.ts} | 20 +- .../modules/reserved-community-type.module.ts | 12 + .../reserved-community-type.service.ts | 162 +++++++++ .../test/integration/listing.e2e-spec.ts | 47 +-- .../reserved-community-type.e2e-spec.ts | 182 ++++++++++ .../reserved-community-type.service.spec.ts | 333 ++++++++++++++++++ backend_new/types/src/backend-swagger.ts | 189 +++++++++- package.json | 2 +- 17 files changed, 1079 insertions(+), 49 deletions(-) create mode 100644 backend_new/prisma/seed-helpers/reserved-community-type-factory.ts create mode 100644 backend_new/src/controllers/reserved-community-type.controller.ts create mode 100644 backend_new/src/dtos/reserved-community-types/reserved-community-type-create.dto.ts create mode 100644 backend_new/src/dtos/reserved-community-types/reserved-community-type-query-params.dto.ts create mode 100644 backend_new/src/dtos/reserved-community-types/reserved-community-type-update.dto.ts rename backend_new/src/dtos/reserved-community-types/{reserved-community-type-get.dto.ts => reserved-community-type.dto.ts} (55%) create mode 100644 backend_new/src/modules/reserved-community-type.module.ts create mode 100644 backend_new/src/services/reserved-community-type.service.ts create mode 100644 backend_new/test/integration/reserved-community-type.e2e-spec.ts create mode 100644 backend_new/test/unit/services/reserved-community-type.service.spec.ts diff --git a/backend_new/prisma/seed-helpers/listing-factory.ts b/backend_new/prisma/seed-helpers/listing-factory.ts index 3206a3f8a3..8720f39f8b 100644 --- a/backend_new/prisma/seed-helpers/listing-factory.ts +++ b/backend_new/prisma/seed-helpers/listing-factory.ts @@ -17,6 +17,7 @@ export const listingFactory = ( i: number, jurisdictionId: string, amiChartId?: string, + reservedCommunityTypeId?: string, ): Prisma.ListingsCreateInput => ({ additionalApplicationSubmissionNotes: `additionalApplicationSubmissionNotes: ${i}`, digitalApplication: true, @@ -159,17 +160,19 @@ export const listingFactory = ( id: jurisdictionId, }, }, - reservedCommunityTypes: { - create: { - name: `reservedCommunityTypes: ${i} name: ${i}`, - description: `reservedCommunityTypes: ${i} description: ${i}`, - jurisdictions: { - connect: { - id: jurisdictionId, + reservedCommunityTypes: reservedCommunityTypeId + ? { connect: { id: reservedCommunityTypeId } } + : { + create: { + name: `reservedCommunityType: ${i}`, + description: `description: ${i}`, + jurisdictions: { + connect: { + id: jurisdictionId, + }, + }, }, }, - }, - }, listingsResult: { create: { label: `listingsResult: ${i} label: ${i}`, diff --git a/backend_new/prisma/seed-helpers/reserved-community-type-factory.ts b/backend_new/prisma/seed-helpers/reserved-community-type-factory.ts new file mode 100644 index 0000000000..1bdd01e013 --- /dev/null +++ b/backend_new/prisma/seed-helpers/reserved-community-type-factory.ts @@ -0,0 +1,14 @@ +import { Prisma } from '@prisma/client'; + +export const reservedCommunityTypeFactory = ( + i: number, + jurisdictionId: string, +): Prisma.ReservedCommunityTypesCreateInput => ({ + name: `name: ${i}`, + description: `description: ${i}`, + jurisdictions: { + connect: { + id: jurisdictionId, + }, + }, +}); diff --git a/backend_new/prisma/seed.ts b/backend_new/prisma/seed.ts index 8513b233e4..e7a3c42f92 100644 --- a/backend_new/prisma/seed.ts +++ b/backend_new/prisma/seed.ts @@ -2,6 +2,7 @@ import { PrismaClient } from '@prisma/client'; import { amiChartFactory } from './seed-helpers/ami-chart-factory'; import { jurisdictionFactory } from './seed-helpers/jurisdiction-factory'; import { listingFactory } from './seed-helpers/listing-factory'; +import { reservedCommunityTypeFactory } from './seed-helpers/reserved-community-type-factory'; const prisma = new PrismaClient(); async function main() { @@ -11,10 +12,18 @@ async function main() { const amiChart = await prisma.amiChart.create({ data: amiChartFactory(10, jurisdiction.id), }); + const reservedCommunityType = await prisma.reservedCommunityTypes.create({ + data: reservedCommunityTypeFactory(6, jurisdiction.id), + }); for (let i = 0; i < 5; i++) { await prisma.listings.create({ - data: listingFactory(i, jurisdiction.id, amiChart.id), + data: listingFactory( + i, + jurisdiction.id, + amiChart.id, + reservedCommunityType.id, + ), }); } } diff --git a/backend_new/src/app.module.ts b/backend_new/src/app.module.ts index c941feb9ed..3320fd1879 100644 --- a/backend_new/src/app.module.ts +++ b/backend_new/src/app.module.ts @@ -3,11 +3,12 @@ import { AppController } from './app.controller'; import { AppService } from './app.service'; import { AmiChartModule } from './modules/ami-chart.module'; import { ListingModule } from './modules/listing.module'; +import { ReservedCommunityTypeModule } from './modules/reserved-community-type.module'; @Module({ - imports: [ListingModule, AmiChartModule], + imports: [ListingModule, AmiChartModule, ReservedCommunityTypeModule], controllers: [AppController], providers: [AppService], - exports: [ListingModule, AmiChartModule], + exports: [ListingModule, AmiChartModule, ReservedCommunityTypeModule], }) export class AppModule {} diff --git a/backend_new/src/controllers/reserved-community-type.controller.ts b/backend_new/src/controllers/reserved-community-type.controller.ts new file mode 100644 index 0000000000..2ea02c70fb --- /dev/null +++ b/backend_new/src/controllers/reserved-community-type.controller.ts @@ -0,0 +1,100 @@ +import { + Body, + Controller, + Delete, + Get, + Param, + Post, + Put, + Query, + UsePipes, + ValidationPipe, +} from '@nestjs/common'; +import { + ApiExtraModels, + ApiOkResponse, + ApiOperation, + ApiTags, +} from '@nestjs/swagger'; +import { ReservedCommunityTypeService } from '../services/reserved-community-type.service'; +import { ReservedCommunityType } from '../dtos/reserved-community-types/reserved-community-type.dto'; +import { ReservedCommunityTypeCreate } from '../dtos/reserved-community-types/reserved-community-type-create.dto'; +import { ReservedCommunityTypeUpdate } from '../dtos/reserved-community-types/reserved-community-type-update.dto'; +import { defaultValidationPipeOptions } from '../utilities/default-validation-pipe-options'; +import { ReservedCommunityTypeQueryParams } from '../dtos/reserved-community-types/reserved-community-type-query-params.dto'; +import { IdDTO } from '../dtos/shared/id.dto'; +import { SuccessDTO } from '../dtos/shared/success.dto'; + +@Controller('reservedCommunityTypes') +@ApiTags('reservedCommunityTypes') +@UsePipes(new ValidationPipe(defaultValidationPipeOptions)) +@ApiExtraModels( + ReservedCommunityTypeCreate, + ReservedCommunityTypeUpdate, + IdDTO, + ReservedCommunityTypeQueryParams, +) +export class ReservedCommunityTypeController { + constructor( + private readonly ReservedCommunityTypeService: ReservedCommunityTypeService, + ) {} + + @Get() + @ApiOperation({ summary: 'List reservedCommunityTypes', operationId: 'list' }) + @ApiOkResponse({ type: ReservedCommunityType, isArray: true }) + async list( + @Query() queryParams: ReservedCommunityTypeQueryParams, + ): Promise { + return await this.ReservedCommunityTypeService.list(queryParams); + } + + @Get(`:reservedCommunityTypeId`) + @ApiOperation({ + summary: 'Get reservedCommunityType by id', + operationId: 'retrieve', + }) + @ApiOkResponse({ type: ReservedCommunityType }) + async retrieve( + @Param('reservedCommunityTypeId') reservedCommunityTypeId: string, + ): Promise { + return this.ReservedCommunityTypeService.findOne(reservedCommunityTypeId); + } + + @Post() + @ApiOperation({ + summary: 'Create reservedCommunityType', + operationId: 'create', + }) + @ApiOkResponse({ type: ReservedCommunityType }) + async create( + @Body() reservedCommunityType: ReservedCommunityTypeCreate, + ): Promise { + return await this.ReservedCommunityTypeService.create( + reservedCommunityType, + ); + } + + @Put(`:reservedCommunityTypeId`) + @ApiOperation({ + summary: 'Update reservedCommunityType', + operationId: 'update', + }) + @ApiOkResponse({ type: ReservedCommunityType }) + async update( + @Body() reservedCommunityType: ReservedCommunityTypeUpdate, + ): Promise { + return await this.ReservedCommunityTypeService.update( + reservedCommunityType, + ); + } + + @Delete() + @ApiOperation({ + summary: 'Delete reservedCommunityType by id', + operationId: 'delete', + }) + @ApiOkResponse({ type: SuccessDTO }) + async delete(@Body() dto: IdDTO): Promise { + return await this.ReservedCommunityTypeService.delete(dto.id); + } +} diff --git a/backend_new/src/dtos/listings/listing-get.dto.ts b/backend_new/src/dtos/listings/listing-get.dto.ts index a0bdf3363c..c38fea37e1 100644 --- a/backend_new/src/dtos/listings/listing-get.dto.ts +++ b/backend_new/src/dtos/listings/listing-get.dto.ts @@ -26,7 +26,7 @@ import { Asset } from '../assets/asset-get.dto'; import { ListingEvent } from './listing-event.dto'; import { Address } from '../addresses/address-get.dto'; import { Jurisdiction } from '../jurisdictions/jurisdiction-get.dto'; -import { ReservedCommunityType } from '../reserved-community-types/reserved-community-type-get.dto'; +import { ReservedCommunityType } from '../reserved-community-types/reserved-community-type.dto'; import { ListingImage } from './listing-image.dto'; import { ListingFeatures } from './listing-feature.dto'; import { ListingUtilities } from './listing-utility.dto'; diff --git a/backend_new/src/dtos/reserved-community-types/reserved-community-type-create.dto.ts b/backend_new/src/dtos/reserved-community-types/reserved-community-type-create.dto.ts new file mode 100644 index 0000000000..fbc8753445 --- /dev/null +++ b/backend_new/src/dtos/reserved-community-types/reserved-community-type-create.dto.ts @@ -0,0 +1,7 @@ +import { OmitType } from '@nestjs/swagger'; +import { ReservedCommunityType } from './reserved-community-type.dto'; + +export class ReservedCommunityTypeCreate extends OmitType( + ReservedCommunityType, + ['id', 'createdAt', 'updatedAt'], +) {} diff --git a/backend_new/src/dtos/reserved-community-types/reserved-community-type-query-params.dto.ts b/backend_new/src/dtos/reserved-community-types/reserved-community-type-query-params.dto.ts new file mode 100644 index 0000000000..98474e01a4 --- /dev/null +++ b/backend_new/src/dtos/reserved-community-types/reserved-community-type-query-params.dto.ts @@ -0,0 +1,14 @@ +import { Expose } from 'class-transformer'; +import { ApiProperty } from '@nestjs/swagger'; +import { IsString } from 'class-validator'; +import { ValidationsGroupsEnum } from '../../enums/shared/validation-groups-enum'; + +export class ReservedCommunityTypeQueryParams { + @Expose() + @ApiProperty({ + required: false, + type: String, + }) + @IsString({ groups: [ValidationsGroupsEnum.default] }) + jurisdictionId?: string; +} diff --git a/backend_new/src/dtos/reserved-community-types/reserved-community-type-update.dto.ts b/backend_new/src/dtos/reserved-community-types/reserved-community-type-update.dto.ts new file mode 100644 index 0000000000..3ba3de01fe --- /dev/null +++ b/backend_new/src/dtos/reserved-community-types/reserved-community-type-update.dto.ts @@ -0,0 +1,7 @@ +import { OmitType } from '@nestjs/swagger'; +import { ReservedCommunityType } from './reserved-community-type.dto'; + +export class ReservedCommunityTypeUpdate extends OmitType( + ReservedCommunityType, + ['createdAt', 'updatedAt', 'jurisdictions'], +) {} diff --git a/backend_new/src/dtos/reserved-community-types/reserved-community-type-get.dto.ts b/backend_new/src/dtos/reserved-community-types/reserved-community-type.dto.ts similarity index 55% rename from backend_new/src/dtos/reserved-community-types/reserved-community-type-get.dto.ts rename to backend_new/src/dtos/reserved-community-types/reserved-community-type.dto.ts index d67d6377b0..9298c0b811 100644 --- a/backend_new/src/dtos/reserved-community-types/reserved-community-type-get.dto.ts +++ b/backend_new/src/dtos/reserved-community-types/reserved-community-type.dto.ts @@ -1,17 +1,33 @@ -import { Expose } from 'class-transformer'; -import { IsDefined, IsString, MaxLength } from 'class-validator'; +import { ApiProperty } from '@nestjs/swagger'; +import { Expose, Type } from 'class-transformer'; +import { + IsDefined, + IsString, + MaxLength, + ValidateNested, +} from 'class-validator'; import { ValidationsGroupsEnum } from '../../enums/shared/validation-groups-enum'; import { AbstractDTO } from '../shared/abstract.dto'; +import { IdDTO } from '../shared/id.dto'; export class ReservedCommunityType extends AbstractDTO { @Expose() @IsString({ groups: [ValidationsGroupsEnum.default] }) @IsDefined({ groups: [ValidationsGroupsEnum.default] }) @MaxLength(256, { groups: [ValidationsGroupsEnum.default] }) + @ApiProperty() name: string; @Expose() @IsString({ groups: [ValidationsGroupsEnum.default] }) @MaxLength(2048, { groups: [ValidationsGroupsEnum.default] }) + @ApiProperty() description?: string | null; + + @Expose() + @ValidateNested({ groups: [ValidationsGroupsEnum.default] }) + @Type(() => IdDTO) + @IsDefined({ groups: [ValidationsGroupsEnum.default] }) + @ApiProperty() + jurisdictions: IdDTO; } diff --git a/backend_new/src/modules/reserved-community-type.module.ts b/backend_new/src/modules/reserved-community-type.module.ts new file mode 100644 index 0000000000..1f36b8894c --- /dev/null +++ b/backend_new/src/modules/reserved-community-type.module.ts @@ -0,0 +1,12 @@ +import { Module } from '@nestjs/common'; +import { ReservedCommunityTypeController } from '../controllers/reserved-community-type.controller'; +import { ReservedCommunityTypeService } from '../services/reserved-community-type.service'; +import { PrismaService } from '../services/prisma.service'; + +@Module({ + imports: [], + controllers: [ReservedCommunityTypeController], + providers: [ReservedCommunityTypeService, PrismaService], + exports: [ReservedCommunityTypeService, PrismaService], +}) +export class ReservedCommunityTypeModule {} diff --git a/backend_new/src/services/reserved-community-type.service.ts b/backend_new/src/services/reserved-community-type.service.ts new file mode 100644 index 0000000000..fa9b69179f --- /dev/null +++ b/backend_new/src/services/reserved-community-type.service.ts @@ -0,0 +1,162 @@ +import { Injectable, NotFoundException } from '@nestjs/common'; +import { PrismaService } from './prisma.service'; +import { Prisma } from '@prisma/client'; +import { ReservedCommunityType } from '../dtos/reserved-community-types/reserved-community-type.dto'; +import { ReservedCommunityTypeCreate } from '../dtos/reserved-community-types/reserved-community-type-create.dto'; +import { ReservedCommunityTypeUpdate } from '../dtos/reserved-community-types/reserved-community-type-update.dto'; +import { mapTo } from '../utilities/mapTo'; +import { SuccessDTO } from '../dtos/shared/success.dto'; +import { ReservedCommunityTypeQueryParams } from '../dtos/reserved-community-types/reserved-community-type-query-params.dto'; + +/* + this is the service for reserved community types + it handles all the backend's business logic for reading/writing/deleting reserved community type data +*/ + +const view: Prisma.ReservedCommunityTypesInclude = { + jurisdictions: true, +}; + +@Injectable() +export class ReservedCommunityTypeService { + constructor(private prisma: PrismaService) {} + + /* + this will get a set of reserved community types given the params passed in + */ + async list( + params: ReservedCommunityTypeQueryParams, + ): Promise { + const rawReservedCommunityTypes = + await this.prisma.reservedCommunityTypes.findMany({ + include: view, + where: this.buildWhereClause(params), + }); + return mapTo(ReservedCommunityType, rawReservedCommunityTypes); + } + + /* + this helps build the where clause for the list() + */ + buildWhereClause( + params: ReservedCommunityTypeQueryParams, + ): Prisma.ReservedCommunityTypesWhereInput { + const filters: Prisma.ReservedCommunityTypesWhereInput[] = []; + + if (params && 'jurisdictionId' in params && params.jurisdictionId) { + filters.push({ + jurisdictions: { + id: params.jurisdictionId, + }, + }); + } + + return { + AND: filters, + }; + } + + /* + this will return 1 reserved community type or error + */ + async findOne( + reservedCommunityTypeId: string, + ): Promise { + const rawReservedCommunityTypes = + await this.prisma.reservedCommunityTypes.findFirst({ + include: view, + where: { + id: { + equals: reservedCommunityTypeId, + }, + }, + }); + + if (!rawReservedCommunityTypes) { + throw new NotFoundException( + `reservedCommunityTypeId ${reservedCommunityTypeId} was requested but not found`, + ); + } + ReservedCommunityTypeCreate; + return mapTo(ReservedCommunityType, rawReservedCommunityTypes); + } + + /* + this will create a reserved community type + */ + async create( + incomingData: ReservedCommunityTypeCreate, + ): Promise { + const rawResult = await this.prisma.reservedCommunityTypes.create({ + data: { + ...incomingData, + jurisdictions: { + connect: { + id: incomingData.jurisdictions.id, + }, + }, + }, + include: view, + }); + + return mapTo(ReservedCommunityType, rawResult); + } + + /* + this will update a reserved community type's name or items field + if no eserved community type has the id of the incoming argument an error is thrown + */ + async update( + incomingData: ReservedCommunityTypeUpdate, + ): Promise { + await this.findOrThrow(incomingData.id); + + const rawResults = await this.prisma.reservedCommunityTypes.update({ + include: view, + data: { + ...incomingData, + jurisdictions: undefined, + id: undefined, + }, + where: { + id: incomingData.id, + }, + }); + return mapTo(ReservedCommunityType, rawResults); + } + + /* + this will delete a reserved community type + */ + async delete(reservedCommunityTypeId: string): Promise { + await this.findOrThrow(reservedCommunityTypeId); + await this.prisma.reservedCommunityTypes.delete({ + where: { + id: reservedCommunityTypeId, + }, + }); + return { + success: true, + } as SuccessDTO; + } + + /* + this will either find a record or throw a customized error + */ + async findOrThrow(reservedCommunityTypeId: string): Promise { + const reservedCommunityType = + await this.prisma.reservedCommunityTypes.findFirst({ + where: { + id: reservedCommunityTypeId, + }, + }); + + if (!reservedCommunityType) { + throw new NotFoundException( + `reservedCommunityTypeId ${reservedCommunityTypeId} was requested but not found`, + ); + } + + return true; + } +} diff --git a/backend_new/test/integration/listing.e2e-spec.ts b/backend_new/test/integration/listing.e2e-spec.ts index 2201cdffad..b928129111 100644 --- a/backend_new/test/integration/listing.e2e-spec.ts +++ b/backend_new/test/integration/listing.e2e-spec.ts @@ -15,8 +15,9 @@ import { ListingViews } from '../../src/enums/listings/view-enum'; describe('Listing Controller Tests', () => { let app: INestApplication; let prisma: PrismaService; + let jurisdictionAId: string; - beforeEach(async () => { + beforeAll(async () => { const moduleFixture: TestingModule = await Test.createTestingModule({ imports: [AppModule], }).compile(); @@ -24,20 +25,13 @@ describe('Listing Controller Tests', () => { app = moduleFixture.createNestApplication(); prisma = moduleFixture.get(PrismaService); await app.init(); - await clearAllDb(); - }); - const clearAllDb = async () => { - await prisma.applicationMethods.deleteMany(); - await prisma.listingEvents.deleteMany(); - await prisma.listingImages.deleteMany(); - await prisma.listingMultiselectQuestions.deleteMany(); - await prisma.units.deleteMany(); - await prisma.amiChart.deleteMany(); - await prisma.listings.deleteMany(); - await prisma.reservedCommunityTypes.deleteMany(); - await prisma.jurisdictions.deleteMany(); - }; + const jurisdiction = await prisma.jurisdictions.create({ + data: jurisdictionFactory(100), + }); + + jurisdictionAId = jurisdiction.id; + }); it('list test no params no data', async () => { const res = await request(app.getHttpServer()).get('/listings').expect(200); @@ -55,16 +49,12 @@ describe('Listing Controller Tests', () => { }); it('list test no params some data', async () => { - const jurisdiction = await prisma.jurisdictions.create({ - data: jurisdictionFactory(100), - }); - await prisma.listings.create({ - data: listingFactory(10, jurisdiction.id), + data: listingFactory(10, jurisdictionAId), }); await prisma.listings.create({ - data: listingFactory(50, jurisdiction.id), + data: listingFactory(50, jurisdictionAId), }); const res = await request(app.getHttpServer()).get('/listings').expect(200); @@ -92,7 +82,7 @@ describe('Listing Controller Tests', () => { filter: [ { $comparison: Compare.IN, - name: 'name: 10,name: 50', + name: 'name: 11,name: 51', }, ], }; @@ -115,14 +105,11 @@ describe('Listing Controller Tests', () => { }); it('list test params some data', async () => { - const jurisdiction = await prisma.jurisdictions.create({ - data: jurisdictionFactory(100), - }); await prisma.listings.create({ - data: listingFactory(10, jurisdiction.id), + data: listingFactory(11, jurisdictionAId), }); await prisma.listings.create({ - data: listingFactory(50, jurisdiction.id), + data: listingFactory(51, jurisdictionAId), }); let queryParams: ListingsQueryParams = { @@ -132,7 +119,7 @@ describe('Listing Controller Tests', () => { filter: [ { $comparison: Compare.IN, - name: 'name: 10,name: 50', + name: 'name: 11,name: 51', }, ], orderBy: [ListingOrderByKeys.name], @@ -153,7 +140,7 @@ describe('Listing Controller Tests', () => { }); expect(res.body.items.length).toEqual(1); - expect(res.body.items[0].name).toEqual('name: 10'); + expect(res.body.items[0].name).toEqual('name: 11'); queryParams = { limit: 1, @@ -162,7 +149,7 @@ describe('Listing Controller Tests', () => { filter: [ { $comparison: Compare.IN, - name: 'name: 10,name: 50', + name: 'name: 11,name: 51', }, ], orderBy: [ListingOrderByKeys.name], @@ -182,6 +169,6 @@ describe('Listing Controller Tests', () => { totalPages: 2, }); expect(res.body.items.length).toEqual(1); - expect(res.body.items[0].name).toEqual('name: 50'); + expect(res.body.items[0].name).toEqual('name: 51'); }); }); diff --git a/backend_new/test/integration/reserved-community-type.e2e-spec.ts b/backend_new/test/integration/reserved-community-type.e2e-spec.ts new file mode 100644 index 0000000000..59c5b2b7be --- /dev/null +++ b/backend_new/test/integration/reserved-community-type.e2e-spec.ts @@ -0,0 +1,182 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { INestApplication } from '@nestjs/common'; +import * as request from 'supertest'; +import { AppModule } from '../../src/app.module'; +import { PrismaService } from '../../src/services/prisma.service'; +import { jurisdictionFactory } from '../../prisma/seed-helpers/jurisdiction-factory'; +import { stringify } from 'qs'; +import { ReservedCommunityTypeQueryParams } from '../../src/dtos/reserved-community-types/reserved-community-type-query-params.dto'; +import { reservedCommunityTypeFactory } from '../../prisma/seed-helpers/reserved-community-type-factory'; +import { ReservedCommunityTypeCreate } from '../../src/dtos/reserved-community-types/reserved-community-type-create.dto'; +import { ReservedCommunityTypeUpdate } from '../../src/dtos/reserved-community-types/reserved-community-type-update.dto'; +import { IdDTO } from 'src/dtos/shared/id.dto'; +import { randomUUID } from 'crypto'; + +describe('ReservedCommunityType Controller Tests', () => { + let app: INestApplication; + let prisma: PrismaService; + let jurisdictionAId: string; + + beforeAll(async () => { + const moduleFixture: TestingModule = await Test.createTestingModule({ + imports: [AppModule], + }).compile(); + + app = moduleFixture.createNestApplication(); + prisma = moduleFixture.get(PrismaService); + await app.init(); + + const jurisdictionA = await prisma.jurisdictions.create({ + data: jurisdictionFactory(12), + }); + jurisdictionAId = jurisdictionA.id; + }); + + it('testing list endpoint without params', async () => { + const jurisdictionA = await prisma.jurisdictions.create({ + data: jurisdictionFactory(13), + }); + + const jurisdictionB = await prisma.jurisdictions.create({ + data: jurisdictionFactory(14), + }); + await prisma.reservedCommunityTypes.create({ + data: reservedCommunityTypeFactory(10, jurisdictionA.id), + }); + await prisma.reservedCommunityTypes.create({ + data: reservedCommunityTypeFactory(10, jurisdictionB.id), + }); + + const res = await request(app.getHttpServer()) + .get(`/reservedCommunityTypes`) + .expect(200); + + expect(res.body.length).toEqual(2); + expect(res.body[0].name).toEqual('name: 10'); + expect(res.body[1].name).toEqual('name: 10'); + expect(res.body[0].jurisdictions.id).not.toBe(res.body[1].jurisdictions.id); + }); + + it('testing list endpoint with params', async () => { + const jurisdictionB = await prisma.jurisdictions.create({ + data: jurisdictionFactory(15), + }); + const reservedCommunityTypeA = await prisma.reservedCommunityTypes.create({ + data: reservedCommunityTypeFactory(10, jurisdictionAId), + }); + await prisma.reservedCommunityTypes.create({ + data: reservedCommunityTypeFactory(10, jurisdictionB.id), + }); + const queryParams: ReservedCommunityTypeQueryParams = { + jurisdictionId: jurisdictionAId, + }; + const query = stringify(queryParams as any); + + // testing with params + const res = await request(app.getHttpServer()) + .get(`/reservedCommunityTypes?${query}`) + .expect(200); + + expect(res.body.length).toEqual(1); + expect(res.body[0].name).toEqual(reservedCommunityTypeA.name); + }); + + it("retrieve endpoint with id that doesn't exist should error", async () => { + const id = randomUUID(); + const res = await request(app.getHttpServer()) + .get(`/reservedCommunityTypes/${id}`) + .expect(404); + expect(res.body.message).toEqual( + `reservedCommunityTypeId ${id} was requested but not found`, + ); + }); + + it('testing retrieve endpoint', async () => { + const reservedCommunityTypeA = await prisma.reservedCommunityTypes.create({ + data: reservedCommunityTypeFactory(10, jurisdictionAId), + }); + + const res = await request(app.getHttpServer()) + .get(`/reservedCommunityTypes/${reservedCommunityTypeA.id}`) + .expect(200); + + expect(res.body.name).toEqual(reservedCommunityTypeA.name); + }); + + it('testing create endpoint', async () => { + const res = await request(app.getHttpServer()) + .post('/reservedCommunityTypes') + .send({ + name: 'name: 10', + description: 'description: 10', + jurisdictions: { + id: jurisdictionAId, + }, + } as ReservedCommunityTypeCreate) + .expect(201); + + expect(res.body.name).toEqual('name: 10'); + expect(res.body.description).toEqual('description: 10'); + }); + + it("update endpoint with id that doesn't exist should error", async () => { + const id = randomUUID(); + const res = await request(app.getHttpServer()) + .put(`/reservedCommunityTypes/${id}`) + .send({ + id: id, + name: 'example name', + description: 'example description', + } as ReservedCommunityTypeUpdate) + .expect(404); + expect(res.body.message).toEqual( + `reservedCommunityTypeId ${id} was requested but not found`, + ); + }); + + it('testing update endpoint', async () => { + const reservedCommunityTypeA = await prisma.reservedCommunityTypes.create({ + data: reservedCommunityTypeFactory(10, jurisdictionAId), + }); + + const res = await request(app.getHttpServer()) + .put(`/reservedCommunityTypes/${reservedCommunityTypeA.id}`) + .send({ + id: reservedCommunityTypeA.id, + name: 'name: 11', + description: 'description: 11', + } as ReservedCommunityTypeUpdate) + .expect(200); + + expect(res.body.name).toEqual('name: 11'); + expect(res.body.description).toEqual('description: 11'); + }); + + it("delete endpoint with id that doesn't exist should error", async () => { + const id = randomUUID(); + const res = await request(app.getHttpServer()) + .delete(`/reservedCommunityTypes`) + .send({ + id: id, + } as IdDTO) + .expect(404); + expect(res.body.message).toEqual( + `reservedCommunityTypeId ${id} was requested but not found`, + ); + }); + + it('testing delete endpoint', async () => { + const reservedCommunityTypeA = await prisma.reservedCommunityTypes.create({ + data: reservedCommunityTypeFactory(16, jurisdictionAId), + }); + + const res = await request(app.getHttpServer()) + .delete(`/reservedCommunityTypes`) + .send({ + id: reservedCommunityTypeA.id, + } as IdDTO) + .expect(200); + + expect(res.body.success).toEqual(true); + }); +}); diff --git a/backend_new/test/unit/services/reserved-community-type.service.spec.ts b/backend_new/test/unit/services/reserved-community-type.service.spec.ts new file mode 100644 index 0000000000..d8bdac1477 --- /dev/null +++ b/backend_new/test/unit/services/reserved-community-type.service.spec.ts @@ -0,0 +1,333 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { PrismaService } from '../../../src/services/prisma.service'; +import { ReservedCommunityTypeService } from '../../../src/services/reserved-community-type.service'; +import { ReservedCommunityTypeQueryParams } from '../../../src/dtos/reserved-community-types/reserved-community-type-query-params.dto'; +import { ReservedCommunityTypeCreate } from '../../../src/dtos/reserved-community-types/reserved-community-type-create.dto'; +import { ReservedCommunityTypeUpdate } from '../../../src/dtos/reserved-community-types/reserved-community-type-update.dto'; +import { randomUUID } from 'crypto'; + +describe('Testing reserved community type service', () => { + let service: ReservedCommunityTypeService; + let prisma: PrismaService; + + const mockReservedCommunityTypeChart = ( + position: number, + date: Date, + jurisdictionData: any, + ) => { + return { + id: randomUUID(), + name: `reserved community type ${position}`, + jurisdictions: jurisdictionData, + createdAt: date, + updatedAt: date, + description: `description ${position}`, + }; + }; + + const mockReservedCommunityTypeSet = ( + numberToCreate: number, + date: Date, + jurisdictionData: any, + ) => { + const toReturn = []; + for (let i = 0; i < numberToCreate; i++) { + toReturn.push(mockReservedCommunityTypeChart(i, date, jurisdictionData)); + } + return toReturn; + }; + + beforeAll(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [ReservedCommunityTypeService, PrismaService], + }).compile(); + + service = module.get( + ReservedCommunityTypeService, + ); + prisma = module.get(PrismaService); + }); + + it('testing buildWhereClause() no params', () => { + const params: ReservedCommunityTypeQueryParams = {}; + expect(service.buildWhereClause(params)).toEqual({ + AND: [], + }); + }); + + it('testing buildWhereClause() jurisdictionId param present', () => { + const params: ReservedCommunityTypeQueryParams = { + jurisdictionId: 'test name', + }; + expect(service.buildWhereClause(params)).toEqual({ + AND: [ + { + jurisdictions: { + id: 'test name', + }, + }, + ], + }); + }); + + it('testing list() with jurisdictionId param present', async () => { + const date = new Date(); + const jurisdictionData = { + id: 'example Id', + name: 'example name', + }; + const mockedValue = mockReservedCommunityTypeSet(3, date, jurisdictionData); + prisma.reservedCommunityTypes.findMany = jest + .fn() + .mockResolvedValue(mockedValue); + + const params: ReservedCommunityTypeQueryParams = { + jurisdictionId: 'test name', + }; + + expect(await service.list(params)).toEqual([ + { + id: mockedValue[0].id, + name: 'reserved community type 0', + jurisdictions: jurisdictionData, + createdAt: date, + updatedAt: date, + description: 'description 0', + }, + { + id: mockedValue[1].id, + name: 'reserved community type 1', + jurisdictions: jurisdictionData, + createdAt: date, + updatedAt: date, + description: 'description 1', + }, + { + id: mockedValue[2].id, + name: 'reserved community type 2', + jurisdictions: jurisdictionData, + createdAt: date, + updatedAt: date, + description: 'description 2', + }, + ]); + + expect(prisma.reservedCommunityTypes.findMany).toHaveBeenCalledWith({ + include: { + jurisdictions: true, + }, + where: { + AND: [ + { + jurisdictions: { + id: 'test name', + }, + }, + ], + }, + }); + }); + + it('testing findOne() with id present', async () => { + const date = new Date(); + const jurisdictionData = { + id: 'example Id', + name: 'example name', + }; + const mockedValue = mockReservedCommunityTypeChart( + 3, + date, + jurisdictionData, + ); + prisma.reservedCommunityTypes.findFirst = jest + .fn() + .mockResolvedValue(mockedValue); + + expect(await service.findOne('example Id')).toEqual({ + id: mockedValue.id, + name: 'reserved community type 3', + jurisdictions: jurisdictionData, + createdAt: date, + updatedAt: date, + description: 'description 3', + }); + + expect(prisma.reservedCommunityTypes.findFirst).toHaveBeenCalledWith({ + include: { + jurisdictions: true, + }, + where: { + id: { + equals: 'example Id', + }, + }, + }); + }); + + it('testing findOne() with id not present', async () => { + prisma.reservedCommunityTypes.findFirst = jest.fn().mockResolvedValue(null); + + await expect( + async () => await service.findOne('example Id'), + ).rejects.toThrowError(); + + expect(prisma.reservedCommunityTypes.findFirst).toHaveBeenCalledWith({ + include: { + jurisdictions: true, + }, + where: { + id: { + equals: 'example Id', + }, + }, + }); + }); + + it('testing create()', async () => { + const date = new Date(); + const jurisdictionData = { + id: 'example Id', + name: 'example name', + }; + const mockedValue = mockReservedCommunityTypeChart( + 3, + date, + jurisdictionData, + ); + prisma.reservedCommunityTypes.create = jest + .fn() + .mockResolvedValue(mockedValue); + + const params: ReservedCommunityTypeCreate = { + jurisdictions: jurisdictionData, + description: 'description 3', + name: 'reserved community type 3', + }; + + expect(await service.create(params)).toEqual({ + id: mockedValue.id, + name: 'reserved community type 3', + jurisdictions: jurisdictionData, + createdAt: date, + updatedAt: date, + description: 'description 3', + }); + + expect(prisma.reservedCommunityTypes.create).toHaveBeenCalledWith({ + include: { + jurisdictions: true, + }, + data: { + name: 'reserved community type 3', + jurisdictions: { + connect: { + id: 'example Id', + }, + }, + description: 'description 3', + }, + }); + }); + + it('testing update() existing record found', async () => { + const date = new Date(); + const jurisdictionData = { + id: 'example Id', + name: 'example name', + }; + const mockedReservedCommunityType = mockReservedCommunityTypeChart( + 3, + date, + jurisdictionData, + ); + + prisma.reservedCommunityTypes.findFirst = jest + .fn() + .mockResolvedValue(mockedReservedCommunityType); + prisma.reservedCommunityTypes.update = jest.fn().mockResolvedValue({ + ...mockedReservedCommunityType, + name: 'updated reserved community type 3', + description: 'updated description 3', + }); + + const params: ReservedCommunityTypeUpdate = { + description: 'updated description 3', + name: 'updated reserved community type 3', + id: mockedReservedCommunityType.id, + }; + + expect(await service.update(params)).toEqual({ + id: mockedReservedCommunityType.id, + name: 'updated reserved community type 3', + jurisdictions: jurisdictionData, + createdAt: date, + updatedAt: date, + description: 'updated description 3', + }); + + expect(prisma.reservedCommunityTypes.findFirst).toHaveBeenCalledWith({ + where: { + id: mockedReservedCommunityType.id, + }, + }); + + expect(prisma.reservedCommunityTypes.update).toHaveBeenCalledWith({ + include: { + jurisdictions: true, + }, + data: { + name: 'updated reserved community type 3', + description: 'updated description 3', + }, + where: { + id: mockedReservedCommunityType.id, + }, + }); + }); + + it('testing update() existing record not found', async () => { + prisma.reservedCommunityTypes.findFirst = jest.fn().mockResolvedValue(null); + prisma.reservedCommunityTypes.update = jest.fn().mockResolvedValue(null); + + const params: ReservedCommunityTypeUpdate = { + description: 'updated description 3', + name: 'updated name 3', + id: 'example id', + }; + + await expect( + async () => await service.update(params), + ).rejects.toThrowError(); + + expect(prisma.reservedCommunityTypes.findFirst).toHaveBeenCalledWith({ + where: { + id: 'example id', + }, + }); + }); + + it('testing delete()', async () => { + const date = new Date(); + const jurisdictionData = undefined; + prisma.reservedCommunityTypes.findFirst = jest + .fn() + .mockResolvedValue( + mockReservedCommunityTypeChart(3, date, jurisdictionData), + ); + prisma.reservedCommunityTypes.delete = jest + .fn() + .mockResolvedValue( + mockReservedCommunityTypeChart(3, date, jurisdictionData), + ); + + expect(await service.delete('example Id')).toEqual({ + success: true, + }); + + expect(prisma.reservedCommunityTypes.delete).toHaveBeenCalledWith({ + where: { + id: 'example Id', + }, + }); + }); +}); diff --git a/backend_new/types/src/backend-swagger.ts b/backend_new/types/src/backend-swagger.ts index 51c11dcdd7..130588f7fa 100644 --- a/backend_new/types/src/backend-swagger.ts +++ b/backend_new/types/src/backend-swagger.ts @@ -316,6 +316,145 @@ export class AmiChartsService { } } +export class ReservedCommunityTypesService { + /** + * List reservedCommunityTypes + */ + list( + params: { + /** */ + jurisdictionId?: string; + } = {} as any, + options: IRequestOptions = {}, + ): Promise { + return new Promise((resolve, reject) => { + let url = basePath + '/reservedCommunityTypes'; + + const configs: IRequestConfig = getConfigs( + 'get', + 'application/json', + url, + options, + ); + configs.params = { jurisdictionId: params['jurisdictionId'] }; + + /** 适配ios13getèŻ·æ±‚äžć…èźžćžŠbody */ + + axios(configs, resolve, reject); + }); + } + /** + * Create reservedCommunityType + */ + create( + params: { + /** requestBody */ + body?: ReservedCommunityTypeCreate; + } = {} as any, + options: IRequestOptions = {}, + ): Promise { + return new Promise((resolve, reject) => { + let url = basePath + '/reservedCommunityTypes'; + + const configs: IRequestConfig = getConfigs( + 'post', + 'application/json', + url, + options, + ); + + let data = params.body; + + configs.data = data; + + axios(configs, resolve, reject); + }); + } + /** + * Delete reservedCommunityType by id + */ + delete( + params: { + /** requestBody */ + body?: IdDTO; + } = {} as any, + options: IRequestOptions = {}, + ): Promise { + return new Promise((resolve, reject) => { + let url = basePath + '/reservedCommunityTypes'; + + const configs: IRequestConfig = getConfigs( + 'delete', + 'application/json', + url, + options, + ); + + let data = params.body; + + configs.data = data; + + axios(configs, resolve, reject); + }); + } + /** + * Get reservedCommunityType by id + */ + retrieve( + params: { + /** */ + reservedCommunityTypeId: string; + } = {} as any, + options: IRequestOptions = {}, + ): Promise { + return new Promise((resolve, reject) => { + let url = basePath + '/reservedCommunityTypes/{reservedCommunityTypeId}'; + url = url.replace( + '{reservedCommunityTypeId}', + params['reservedCommunityTypeId'] + '', + ); + + const configs: IRequestConfig = getConfigs( + 'get', + 'application/json', + url, + options, + ); + + /** 适配ios13getèŻ·æ±‚äžć…èźžćžŠbody */ + + axios(configs, resolve, reject); + }); + } + /** + * Update reservedCommunityType + */ + update( + params: { + /** requestBody */ + body?: ReservedCommunityTypeUpdate; + } = {} as any, + options: IRequestOptions = {}, + ): Promise { + return new Promise((resolve, reject) => { + let url = basePath + '/reservedCommunityTypes/{reservedCommunityTypeId}'; + + const configs: IRequestConfig = getConfigs( + 'put', + 'application/json', + url, + options, + ); + + let data = params.body; + + configs.data = data; + + axios(configs, resolve, reject); + }); + } +} + export interface ListingsQueryParams { /** */ page?: number; @@ -574,9 +713,6 @@ export interface AmiChartUpdate { /** */ name: string; - - /** */ - jurisdictions: IdDTO; } export interface AmiChartQueryParams { @@ -609,6 +745,53 @@ export interface SuccessDTO { success: boolean; } +export interface ReservedCommunityTypeCreate { + /** */ + name: string; + + /** */ + description: string; + + /** */ + jurisdictions: IdDTO; +} + +export interface ReservedCommunityTypeUpdate { + /** */ + id: string; + + /** */ + name: string; + + /** */ + description: string; +} + +export interface ReservedCommunityTypeQueryParams { + /** */ + jurisdictionId?: string; +} + +export interface ReservedCommunityType { + /** */ + id: string; + + /** */ + createdAt: Date; + + /** */ + updatedAt: Date; + + /** */ + name: string; + + /** */ + description: string; + + /** */ + jurisdictions: IdDTO; +} + export enum ListingViews { 'fundamentals' = 'fundamentals', 'base' = 'base', diff --git a/package.json b/package.json index f98318d5e1..6a25a705d9 100644 --- a/package.json +++ b/package.json @@ -52,7 +52,7 @@ "version:all": "lerna version --yes --no-commit-hooks --ignore-scripts --conventional-graduate --include-merged-tags --force-git-tag", "test:backend:new": "cd backend_new && yarn test", "test:backend:new:e2e": "cd backend_new && yarn jest --config ./test/jest-e2e.config.js", - "test:backend:new:dbsetup": "cd backend_new && yarn db:migration:run && yarn db:seed", + "test:backend:new:dbsetup": "cd backend_new && yarn db:migration:run", "backend:new:install": "cd backend_new && yarn install", "prettier": "prettier --write \"./**/*.{js,jsx,ts,tsx,json}\"" }, From aa8335752ebc0d240b0aef5ecba250eb97d7eaa8 Mon Sep 17 00:00:00 2001 From: Yazeed Loonat Date: Fri, 23 Jun 2023 09:04:01 -0700 Subject: [PATCH 07/57] feat: Prisma unit type (#3479) * feat: prisma schema generation * fix: cleaning up removed "models" * fix: updates per emily * fix: caught some schema errors * feat: listing get endpoints * feat: adding DTOs * feat: openapi and swagger updates * fix: pass through DTOs * fix: updates per emily * feat: adding ci jobs * fix: round 2 * fix: indentation problem * fix: ci updates * fix: ci test again * fix: adding maybe missing library? * fix: running yarn install * fix: updates per morgan * fix: ci fix round x * fix: test updates * fix: integration tests and ci * fix: update for tests * fix: fixing scheme so migrations can be created * feat: creating ami-chart endpoints * feat: reserved community type * feat: unit type endpoints * fix: updates per morgan * fix: update backend-new circleci job (#3489) * fix: update backend-new circleci job * fix: add cache * fix: add class validator * fix: update list typing * fix: add class transformer * fix: add listing type * fix: one more attempt at typings * fix: update jest config file * fix: add dbsetup * fix: one more attempt * fix: e2e typing fix * fix: turn off diagnostics for e2e * fix: clear db fields between tests * fix: prettier and linting changes * fix: additional linting fixes --------- Co-authored-by: Yazeed Loonat * fix: updates from what I learned * fix: forgot swagger file * fix: updates per morgan * fix: updates per sean/morgan and test updates * fix: updates per sean and morgan * fix: maybe resolving test race case * fix: removing jurisdictionName param * fix: upates per sean and emily * fix: updates per emily * fix: updates to listing test * fix: updates per ami chart * fix: trying to remove seeding for e2e testing * fix: updating test description * fix: updates from reserved-community-type learning * fix: updates per morgan * fix: converting name to enum --------- Co-authored-by: Morgan Ludtke <42942267+ludtkemorgan@users.noreply.github.com> --- .../migration.sql | 5 +- backend_new/prisma/schema.prisma | 14 +- .../prisma/seed-helpers/listing-factory.ts | 18 +- .../prisma/seed-helpers/unit-type-factory.ts | 15 ++ backend_new/prisma/seed.ts | 10 + backend_new/src/app.module.ts | 15 +- .../src/controllers/unit-type.controller.ts | 79 +++++++ .../dtos/unit-types/unit-type-create.dto.ts | 4 + .../dtos/unit-types/unit-type-update.dto.ts | 7 + .../unit-type.dto.ts} | 16 +- backend_new/src/dtos/units/unit-get.dto.ts | 2 +- .../src/dtos/units/unit-summarized.dto.ts | 2 +- .../src/dtos/units/unit-summary-get.dto.ts | 2 +- .../src/dtos/units/units-summery-get.dto.ts | 2 +- backend_new/src/modules/unit-type.module.ts | 12 + backend_new/src/services/unit-type.service.ts | 112 +++++++++ backend_new/src/utilities/unit-utilities.ts | 2 +- .../test/integration/unit-type.e2e-spec.ts | 140 +++++++++++ .../unit/services/unit-type.service.spec.ts | 218 ++++++++++++++++++ backend_new/types/src/backend-swagger.ts | 164 +++++++++++++ 20 files changed, 819 insertions(+), 20 deletions(-) rename backend_new/prisma/migrations/{20230527231022_init => 20230622231511_init}/migration.sql (99%) create mode 100644 backend_new/prisma/seed-helpers/unit-type-factory.ts create mode 100644 backend_new/src/controllers/unit-type.controller.ts create mode 100644 backend_new/src/dtos/unit-types/unit-type-create.dto.ts create mode 100644 backend_new/src/dtos/unit-types/unit-type-update.dto.ts rename backend_new/src/dtos/{units/unit-type-get.dto.ts => unit-types/unit-type.dto.ts} (56%) create mode 100644 backend_new/src/modules/unit-type.module.ts create mode 100644 backend_new/src/services/unit-type.service.ts create mode 100644 backend_new/test/integration/unit-type.e2e-spec.ts create mode 100644 backend_new/test/unit/services/unit-type.service.spec.ts diff --git a/backend_new/prisma/migrations/20230527231022_init/migration.sql b/backend_new/prisma/migrations/20230622231511_init/migration.sql similarity index 99% rename from backend_new/prisma/migrations/20230527231022_init/migration.sql rename to backend_new/prisma/migrations/20230622231511_init/migration.sql index ded4ce382f..99029e2f01 100644 --- a/backend_new/prisma/migrations/20230527231022_init/migration.sql +++ b/backend_new/prisma/migrations/20230622231511_init/migration.sql @@ -61,6 +61,9 @@ CREATE TYPE "property_region_enum" AS ENUM ('Greater_Downtown', 'Eastside', 'Sou -- CreateEnum CREATE TYPE "monthly_rent_determination_type_enum" AS ENUM ('flatRent', 'percentageOfIncome'); +-- CreateEnum +CREATE TYPE "unit_type_enum" AS ENUM ('studio', 'oneBdrm', 'twoBdrm', 'threeBdrm', 'fourBdrm', 'SRO', 'fiveBdrm'); + -- CreateTable CREATE TABLE "accessibility" ( "id" UUID NOT NULL DEFAULT uuid_generate_v4(), @@ -605,7 +608,7 @@ CREATE TABLE "unit_types" ( "id" UUID NOT NULL DEFAULT uuid_generate_v4(), "created_at" TIMESTAMP(6) NOT NULL DEFAULT CURRENT_TIMESTAMP, "updated_at" TIMESTAMP(6) NOT NULL, - "name" TEXT NOT NULL, + "name" "unit_type_enum" NOT NULL, "num_bedrooms" INTEGER NOT NULL, CONSTRAINT "unit_types_pkey" PRIMARY KEY ("id") diff --git a/backend_new/prisma/schema.prisma b/backend_new/prisma/schema.prisma index 1ad02fbc84..f1e88bbb68 100644 --- a/backend_new/prisma/schema.prisma +++ b/backend_new/prisma/schema.prisma @@ -668,7 +668,7 @@ model UnitTypes { id String @id() @default(dbgenerated("uuid_generate_v4()")) @db.Uuid createdAt DateTime @default(now()) @map("created_at") @db.Timestamp(6) updatedAt DateTime @updatedAt @map("updated_at") @db.Timestamp(6) - name String + name UnitTypeEnum numBedrooms Int @map("num_bedrooms") applications Applications[] units Units[] @@ -1030,3 +1030,15 @@ enum MonthlyRentDeterminationTypeEnum { @@map("monthly_rent_determination_type_enum") } + +enum UnitTypeEnum { + studio + oneBdrm + twoBdrm + threeBdrm + fourBdrm + SRO + fiveBdrm + + @@map("unit_type_enum") +} diff --git a/backend_new/prisma/seed-helpers/listing-factory.ts b/backend_new/prisma/seed-helpers/listing-factory.ts index 8720f39f8b..7909be1a15 100644 --- a/backend_new/prisma/seed-helpers/listing-factory.ts +++ b/backend_new/prisma/seed-helpers/listing-factory.ts @@ -12,12 +12,14 @@ import { MultiselectQuestionsApplicationSectionEnum, UnitsStatusEnum, } from '@prisma/client'; +import { unitTypeFactory } from './unit-type-factory'; export const listingFactory = ( i: number, jurisdictionId: string, amiChartId?: string, reservedCommunityTypeId?: string, + unitTypeId?: string, ): Prisma.ListingsCreateInput => ({ additionalApplicationSubmissionNotes: `additionalApplicationSubmissionNotes: ${i}`, digitalApplication: true, @@ -329,7 +331,7 @@ export const listingFactory = ( }, ], }, - units: unitFactory(i, i, jurisdictionId, amiChartId), + units: unitFactory(i, i, jurisdictionId, amiChartId, unitTypeId), }); const unitFactory = ( @@ -337,6 +339,7 @@ const unitFactory = ( i: number, jurisdictionId: string, amiChartId?: string, + unitTypeId?: string, ): Prisma.UnitsCreateNestedManyWithoutListingsInput => { const createArray: Prisma.UnitsCreateWithoutListingsInput[] = []; for (let j = 0; j < numberToMake; j++) { @@ -356,12 +359,13 @@ const unitFactory = ( monthlyRentAsPercentOfIncome: i, bmrProgramChart: true, status: UnitsStatusEnum.available, - unitTypes: { - create: { - name: `listing: ${i} unit: ${j} unitTypes: ${j}`, - numBedrooms: i, - }, - }, + unitTypes: unitTypeId + ? { + connect: { + id: unitTypeId, + }, + } + : { create: unitTypeFactory(i) }, amiChart: amiChartId ? { connect: { id: amiChartId } } : { diff --git a/backend_new/prisma/seed-helpers/unit-type-factory.ts b/backend_new/prisma/seed-helpers/unit-type-factory.ts new file mode 100644 index 0000000000..377cb7e676 --- /dev/null +++ b/backend_new/prisma/seed-helpers/unit-type-factory.ts @@ -0,0 +1,15 @@ +import { Prisma, UnitTypeEnum } from '@prisma/client'; + +export const unitTypeFactory = (i: number): Prisma.UnitTypesCreateInput => ({ + ...unitTypeArray[i % unitTypeArray.length], +}); + +export const unitTypeArray = [ + { name: UnitTypeEnum.studio, numBedrooms: 0 }, + { name: UnitTypeEnum.oneBdrm, numBedrooms: 1 }, + { name: UnitTypeEnum.twoBdrm, numBedrooms: 2 }, + { name: UnitTypeEnum.threeBdrm, numBedrooms: 3 }, + { name: UnitTypeEnum.fourBdrm, numBedrooms: 4 }, + { name: UnitTypeEnum.SRO, numBedrooms: 0 }, + { name: UnitTypeEnum.fiveBdrm, numBedrooms: 5 }, +]; diff --git a/backend_new/prisma/seed.ts b/backend_new/prisma/seed.ts index e7a3c42f92..8674e5def7 100644 --- a/backend_new/prisma/seed.ts +++ b/backend_new/prisma/seed.ts @@ -3,6 +3,7 @@ import { amiChartFactory } from './seed-helpers/ami-chart-factory'; import { jurisdictionFactory } from './seed-helpers/jurisdiction-factory'; import { listingFactory } from './seed-helpers/listing-factory'; import { reservedCommunityTypeFactory } from './seed-helpers/reserved-community-type-factory'; +import { unitTypeFactory } from './seed-helpers/unit-type-factory'; const prisma = new PrismaClient(); async function main() { @@ -16,6 +17,14 @@ async function main() { data: reservedCommunityTypeFactory(6, jurisdiction.id), }); + const unitTypeIds: string[] = []; + for (let i = 0; i < 7; i++) { + const res = await prisma.unitTypes.create({ + data: unitTypeFactory(i), + }); + unitTypeIds.push(res.id); + } + for (let i = 0; i < 5; i++) { await prisma.listings.create({ data: listingFactory( @@ -23,6 +32,7 @@ async function main() { jurisdiction.id, amiChart.id, reservedCommunityType.id, + unitTypeIds[i], ), }); } diff --git a/backend_new/src/app.module.ts b/backend_new/src/app.module.ts index 3320fd1879..935a585a94 100644 --- a/backend_new/src/app.module.ts +++ b/backend_new/src/app.module.ts @@ -4,11 +4,22 @@ import { AppService } from './app.service'; import { AmiChartModule } from './modules/ami-chart.module'; import { ListingModule } from './modules/listing.module'; import { ReservedCommunityTypeModule } from './modules/reserved-community-type.module'; +import { UnitTypeModule } from './modules/unit-type.module'; @Module({ - imports: [ListingModule, AmiChartModule, ReservedCommunityTypeModule], + imports: [ + ListingModule, + AmiChartModule, + ReservedCommunityTypeModule, + UnitTypeModule, + ], controllers: [AppController], providers: [AppService], - exports: [ListingModule, AmiChartModule, ReservedCommunityTypeModule], + exports: [ + ListingModule, + AmiChartModule, + ReservedCommunityTypeModule, + UnitTypeModule, + ], }) export class AppModule {} diff --git a/backend_new/src/controllers/unit-type.controller.ts b/backend_new/src/controllers/unit-type.controller.ts new file mode 100644 index 0000000000..12af8e5222 --- /dev/null +++ b/backend_new/src/controllers/unit-type.controller.ts @@ -0,0 +1,79 @@ +import { + Body, + Controller, + Delete, + Get, + Param, + Post, + Put, + UsePipes, + ValidationPipe, +} from '@nestjs/common'; +import { + ApiExtraModels, + ApiOkResponse, + ApiOperation, + ApiTags, +} from '@nestjs/swagger'; +import { UnitTypeService } from '../services/unit-type.service'; +import { UnitType } from '../dtos/unit-types/unit-type.dto'; +import { UnitTypeCreate } from '../dtos/unit-types/unit-type-create.dto'; +import { UnitTypeUpdate } from '../dtos/unit-types/unit-type-update.dto'; +import { defaultValidationPipeOptions } from '../utilities/default-validation-pipe-options'; +import { IdDTO } from '../dtos/shared/id.dto'; +import { SuccessDTO } from '../dtos/shared/success.dto'; + +@Controller('unitTypes') +@ApiTags('unitTypes') +@UsePipes(new ValidationPipe(defaultValidationPipeOptions)) +@ApiExtraModels(UnitTypeCreate, UnitTypeUpdate, IdDTO) +export class UnitTypeController { + constructor(private readonly unitTypeService: UnitTypeService) {} + + @Get() + @ApiOperation({ summary: 'List unitTypes', operationId: 'list' }) + @ApiOkResponse({ type: UnitType, isArray: true }) + async list(): Promise { + return await this.unitTypeService.list(); + } + + @Get(`:unitTypeId`) + @ApiOperation({ + summary: 'Get unitType by id', + operationId: 'retrieve', + }) + @ApiOkResponse({ type: UnitType }) + async retrieve(@Param('unitTypeId') unitTypeId: string): Promise { + return this.unitTypeService.findOne(unitTypeId); + } + + @Post() + @ApiOperation({ + summary: 'Create unitType', + operationId: 'create', + }) + @ApiOkResponse({ type: UnitType }) + async create(@Body() unitType: UnitTypeCreate): Promise { + return await this.unitTypeService.create(unitType); + } + + @Put(`:unitTypeId`) + @ApiOperation({ + summary: 'Update unitType', + operationId: 'update', + }) + @ApiOkResponse({ type: UnitType }) + async update(@Body() unitType: UnitTypeUpdate): Promise { + return await this.unitTypeService.update(unitType); + } + + @Delete() + @ApiOperation({ + summary: 'Delete unitType by id', + operationId: 'delete', + }) + @ApiOkResponse({ type: SuccessDTO }) + async delete(@Body() dto: IdDTO): Promise { + return await this.unitTypeService.delete(dto.id); + } +} diff --git a/backend_new/src/dtos/unit-types/unit-type-create.dto.ts b/backend_new/src/dtos/unit-types/unit-type-create.dto.ts new file mode 100644 index 0000000000..c92a54c953 --- /dev/null +++ b/backend_new/src/dtos/unit-types/unit-type-create.dto.ts @@ -0,0 +1,4 @@ +import { OmitType } from '@nestjs/swagger'; +import { UnitTypeUpdate } from './unit-type-update.dto'; + +export class UnitTypeCreate extends OmitType(UnitTypeUpdate, ['id']) {} diff --git a/backend_new/src/dtos/unit-types/unit-type-update.dto.ts b/backend_new/src/dtos/unit-types/unit-type-update.dto.ts new file mode 100644 index 0000000000..08c34a9d87 --- /dev/null +++ b/backend_new/src/dtos/unit-types/unit-type-update.dto.ts @@ -0,0 +1,7 @@ +import { OmitType } from '@nestjs/swagger'; +import { UnitType } from './unit-type.dto'; + +export class UnitTypeUpdate extends OmitType(UnitType, [ + 'createdAt', + 'updatedAt', +]) {} diff --git a/backend_new/src/dtos/units/unit-type-get.dto.ts b/backend_new/src/dtos/unit-types/unit-type.dto.ts similarity index 56% rename from backend_new/src/dtos/units/unit-type-get.dto.ts rename to backend_new/src/dtos/unit-types/unit-type.dto.ts index 789e202f14..a0577205bc 100644 --- a/backend_new/src/dtos/units/unit-type-get.dto.ts +++ b/backend_new/src/dtos/unit-types/unit-type.dto.ts @@ -1,17 +1,25 @@ +import { ApiProperty } from '@nestjs/swagger'; import { Expose } from 'class-transformer'; -import { IsDefined, IsNumber, IsString, MaxLength } from 'class-validator'; +import { IsDefined, IsNumber, IsEnum } from 'class-validator'; import { ValidationsGroupsEnum } from '../../enums/shared/validation-groups-enum'; import { AbstractDTO } from '../shared/abstract.dto'; +import { UnitTypeEnum } from '@prisma/client'; export class UnitType extends AbstractDTO { @Expose() - @IsString({ groups: [ValidationsGroupsEnum.default] }) @IsDefined({ groups: [ValidationsGroupsEnum.default] }) - @MaxLength(256, { groups: [ValidationsGroupsEnum.default] }) - name: string; + @IsEnum(UnitTypeEnum, { + groups: [ValidationsGroupsEnum.default], + }) + @ApiProperty({ + enum: UnitTypeEnum, + enumName: 'UnitTypeEnum', + }) + name: UnitTypeEnum; @Expose() @IsNumber({}, { groups: [ValidationsGroupsEnum.default] }) @IsDefined({ groups: [ValidationsGroupsEnum.default] }) + @ApiProperty() numBedrooms: number; } diff --git a/backend_new/src/dtos/units/unit-get.dto.ts b/backend_new/src/dtos/units/unit-get.dto.ts index cfea6f8189..e7d313c564 100644 --- a/backend_new/src/dtos/units/unit-get.dto.ts +++ b/backend_new/src/dtos/units/unit-get.dto.ts @@ -9,7 +9,7 @@ import { Expose, Type } from 'class-transformer'; import { ValidationsGroupsEnum } from '../../enums/shared/validation-groups-enum'; import { AbstractDTO } from '../shared/abstract.dto'; import { AmiChart } from '../ami-charts/ami-chart.dto'; -import { UnitType } from './unit-type-get.dto'; +import { UnitType } from '../unit-types/unit-type.dto'; import { UnitRentType } from './unit-rent-type-get.dto'; import { UnitAccessibilityPriorityType } from './unit-accessibility-priority-type-get.dto'; import { UnitAmiChartOverride } from './ami-chart-override-get.dto'; diff --git a/backend_new/src/dtos/units/unit-summarized.dto.ts b/backend_new/src/dtos/units/unit-summarized.dto.ts index dd5e658d45..1e777426c1 100644 --- a/backend_new/src/dtos/units/unit-summarized.dto.ts +++ b/backend_new/src/dtos/units/unit-summarized.dto.ts @@ -5,7 +5,7 @@ import { UnitSummary } from './unit-summary-get.dto'; import { UnitSummaryByAMI } from './unit-summary-by-ami-get.dto'; import { HMI } from './hmi-get.dto'; import { ApiProperty } from '@nestjs/swagger'; -import { UnitType } from './unit-type-get.dto'; +import { UnitType } from '../unit-types/unit-type.dto'; import { UnitAccessibilityPriorityType } from './unit-accessibility-priority-type-get.dto'; export class UnitsSummarized { diff --git a/backend_new/src/dtos/units/unit-summary-get.dto.ts b/backend_new/src/dtos/units/unit-summary-get.dto.ts index 88fc88d094..6ee81bd397 100644 --- a/backend_new/src/dtos/units/unit-summary-get.dto.ts +++ b/backend_new/src/dtos/units/unit-summary-get.dto.ts @@ -4,7 +4,7 @@ import { ValidationsGroupsEnum } from '../../enums/shared/validation-groups-enum import { MinMaxCurrency } from '../shared/min-max-currency.dto'; import { MinMax } from '../shared/min-max.dto'; import { ApiProperty } from '@nestjs/swagger'; -import { UnitType } from './unit-type-get.dto'; +import { UnitType } from '../unit-types/unit-type.dto'; export class UnitSummary { @Expose() diff --git a/backend_new/src/dtos/units/units-summery-get.dto.ts b/backend_new/src/dtos/units/units-summery-get.dto.ts index 4b9b87005a..890fd895a2 100644 --- a/backend_new/src/dtos/units/units-summery-get.dto.ts +++ b/backend_new/src/dtos/units/units-summery-get.dto.ts @@ -8,7 +8,7 @@ import { } from 'class-validator'; import { Expose, Type } from 'class-transformer'; import { ValidationsGroupsEnum } from '../../enums/shared/validation-groups-enum'; -import { UnitType } from './unit-type-get.dto'; +import { UnitType } from '../unit-types/unit-type.dto'; import { UnitAccessibilityPriorityType } from './unit-accessibility-priority-type-get.dto'; class UnitsSummary { diff --git a/backend_new/src/modules/unit-type.module.ts b/backend_new/src/modules/unit-type.module.ts new file mode 100644 index 0000000000..9bb10e1521 --- /dev/null +++ b/backend_new/src/modules/unit-type.module.ts @@ -0,0 +1,12 @@ +import { Module } from '@nestjs/common'; +import { UnitTypeController } from '../controllers/unit-type.controller'; +import { UnitTypeService } from '../services/unit-type.service'; +import { PrismaService } from '../services/prisma.service'; + +@Module({ + imports: [], + controllers: [UnitTypeController], + providers: [UnitTypeService, PrismaService], + exports: [UnitTypeService, PrismaService], +}) +export class UnitTypeModule {} diff --git a/backend_new/src/services/unit-type.service.ts b/backend_new/src/services/unit-type.service.ts new file mode 100644 index 0000000000..bc75e2f97d --- /dev/null +++ b/backend_new/src/services/unit-type.service.ts @@ -0,0 +1,112 @@ +import { Injectable, NotFoundException } from '@nestjs/common'; +import { PrismaService } from './prisma.service'; +import { UnitType } from '../dtos/unit-types/unit-type.dto'; +import { UnitTypeCreate } from '../dtos/unit-types/unit-type-create.dto'; +import { mapTo } from '../utilities/mapTo'; +import { SuccessDTO } from '../dtos/shared/success.dto'; +import { UnitTypeUpdate } from '../dtos/unit-types/unit-type-update.dto'; + +/* + this is the service for unit types + it handles all the backend's business logic for reading/writing/deleting unit type data +*/ + +@Injectable() +export class UnitTypeService { + constructor(private prisma: PrismaService) {} + + /* + this will get a set of unit types given the params passed in + */ + async list(): Promise { + const rawUnitTypes = await this.prisma.unitTypes.findMany(); + return mapTo(UnitType, rawUnitTypes); + } + + /* + this will return 1 unit type or error + */ + async findOne(unitTypeId: string): Promise { + const rawUnitType = await this.prisma.unitTypes.findFirst({ + where: { + id: { + equals: unitTypeId, + }, + }, + }); + + if (!rawUnitType) { + throw new NotFoundException( + `unitTypeId ${unitTypeId} was requested but not found`, + ); + } + + return mapTo(UnitType, rawUnitType); + } + + /* + this will create a unit type + */ + async create(incomingData: UnitTypeCreate): Promise { + const rawResult = await this.prisma.unitTypes.create({ + data: { + ...incomingData, + }, + }); + + return mapTo(UnitType, rawResult); + } + + /* + this will update a unit type's name or items field + if no unit type has the id of the incoming argument an error is thrown + */ + async update(incomingData: UnitTypeUpdate): Promise { + await this.findOrThrow(incomingData.id); + + const rawResults = await this.prisma.unitTypes.update({ + data: { + ...incomingData, + id: undefined, + }, + where: { + id: incomingData.id, + }, + }); + return mapTo(UnitType, rawResults); + } + + /* + this will delete a unit type + */ + async delete(unitTypeId: string): Promise { + await this.findOrThrow(unitTypeId); + await this.prisma.unitTypes.delete({ + where: { + id: unitTypeId, + }, + }); + return { + success: true, + } as SuccessDTO; + } + + /* + this will either find a record or throw a customized error + */ + async findOrThrow(unitTypeId: string): Promise { + const unitType = await this.prisma.unitTypes.findFirst({ + where: { + id: unitTypeId, + }, + }); + + if (!unitType) { + throw new NotFoundException( + `unitTypeId ${unitTypeId} was requested but not found`, + ); + } + + return true; + } +} diff --git a/backend_new/src/utilities/unit-utilities.ts b/backend_new/src/utilities/unit-utilities.ts index b74b1bf9b9..7087063892 100644 --- a/backend_new/src/utilities/unit-utilities.ts +++ b/backend_new/src/utilities/unit-utilities.ts @@ -6,7 +6,7 @@ import listingGetDto, { ListingGet } from '../dtos/listings/listing-get.dto'; import { MinMaxCurrency } from '../dtos/shared/min-max-currency.dto'; import { MinMax } from '../dtos/shared/min-max.dto'; import { UnitsSummarized } from '../dtos/units/unit-summarized.dto'; -import { UnitType } from '../dtos/units/unit-type-get.dto'; +import { UnitType } from '../dtos/unit-types/unit-type.dto'; import { UnitAccessibilityPriorityType } from '../dtos/units/unit-accessibility-priority-type-get.dto'; import { AmiChartItem } from '../dtos/units/ami-chart-item-get.dto'; import { UnitAmiChartOverride } from '../dtos/units/ami-chart-override-get.dto'; diff --git a/backend_new/test/integration/unit-type.e2e-spec.ts b/backend_new/test/integration/unit-type.e2e-spec.ts new file mode 100644 index 0000000000..347cd0de6c --- /dev/null +++ b/backend_new/test/integration/unit-type.e2e-spec.ts @@ -0,0 +1,140 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { INestApplication } from '@nestjs/common'; +import * as request from 'supertest'; +import { AppModule } from '../../src/app.module'; +import { PrismaService } from '../../src/services/prisma.service'; +import { unitTypeFactory } from '../../prisma/seed-helpers/unit-type-factory'; +import { UnitTypeCreate } from '../../src/dtos/unit-types/unit-type-create.dto'; +import { UnitTypeUpdate } from '../../src/dtos/unit-types/unit-type-update.dto'; +import { IdDTO } from 'src/dtos/shared/id.dto'; +import { randomUUID } from 'crypto'; + +describe('UnitType Controller Tests', () => { + let app: INestApplication; + let prisma: PrismaService; + + beforeAll(async () => { + const moduleFixture: TestingModule = await Test.createTestingModule({ + imports: [AppModule], + }).compile(); + + app = moduleFixture.createNestApplication(); + prisma = moduleFixture.get(PrismaService); + await app.init(); + }); + + it('testing list endpoint', async () => { + const unitTypeA = await prisma.unitTypes.create({ + data: unitTypeFactory(7), + }); + const unitTypeB = await prisma.unitTypes.create({ + data: unitTypeFactory(8), + }); + + const res = await request(app.getHttpServer()) + .get(`/unitTypes?`) + .expect(200); + + expect(res.body.length).toBeGreaterThanOrEqual(2); + const unitTypeNames = res.body.map((value) => value.name); + expect(unitTypeNames).toContain(unitTypeA.name); + expect(unitTypeNames).toContain(unitTypeB.name); + }); + + it("retrieve endpoint with id that doesn't exist should error", async () => { + const id = randomUUID(); + const res = await request(app.getHttpServer()) + .get(`/unitTypes/${id}`) + .expect(404); + expect(res.body.message).toEqual( + `unitTypeId ${id} was requested but not found`, + ); + }); + + it('testing retrieve endpoint', async () => { + const unitTypeA = await prisma.unitTypes.create({ + data: unitTypeFactory(10), + }); + + const res = await request(app.getHttpServer()) + .get(`/unitTypes/${unitTypeA.id}`) + .expect(200); + + expect(res.body.name).toEqual(unitTypeA.name); + }); + + it('testing create endpoint', async () => { + const name = unitTypeFactory(10).name; + const res = await request(app.getHttpServer()) + .post('/unitTypes') + .send({ + name: name, + numBedrooms: 10, + } as UnitTypeCreate) + .expect(201); + + expect(res.body.name).toEqual(name); + }); + + it("update endpoint with id that doesn't exist should error", async () => { + const id = randomUUID(); + const name = unitTypeFactory(10).name; + const res = await request(app.getHttpServer()) + .put(`/unitTypes/${id}`) + .send({ + id: id, + name: name, + numBedrooms: 11, + } as UnitTypeUpdate) + .expect(404); + expect(res.body.message).toEqual( + `unitTypeId ${id} was requested but not found`, + ); + }); + + it('testing update endpoint', async () => { + const unitTypeA = await prisma.unitTypes.create({ + data: unitTypeFactory(10), + }); + const name = unitTypeFactory(11).name; + const res = await request(app.getHttpServer()) + .put(`/unitTypes/${unitTypeA.id}`) + .send({ + id: unitTypeA.id, + name: name, + numBedrooms: 11, + } as UnitTypeUpdate) + .expect(200); + + expect(res.body.name).toEqual(name); + expect(res.body.numBedrooms).toEqual(11); + }); + + it("delete endpoint with id that doesn't exist should error", async () => { + const id = randomUUID(); + const res = await request(app.getHttpServer()) + .delete(`/unitTypes`) + .send({ + id: id, + } as IdDTO) + .expect(404); + expect(res.body.message).toEqual( + `unitTypeId ${id} was requested but not found`, + ); + }); + + it('testing delete endpoint', async () => { + const unitTypeA = await prisma.unitTypes.create({ + data: unitTypeFactory(16), + }); + + const res = await request(app.getHttpServer()) + .delete(`/unitTypes`) + .send({ + id: unitTypeA.id, + } as IdDTO) + .expect(200); + + expect(res.body.success).toEqual(true); + }); +}); diff --git a/backend_new/test/unit/services/unit-type.service.spec.ts b/backend_new/test/unit/services/unit-type.service.spec.ts new file mode 100644 index 0000000000..18ec902390 --- /dev/null +++ b/backend_new/test/unit/services/unit-type.service.spec.ts @@ -0,0 +1,218 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { PrismaService } from '../../../src/services/prisma.service'; +import { UnitTypeService } from '../../../src/services/unit-type.service'; +import { UnitTypeCreate } from '../../../src/dtos/unit-types/unit-type-create.dto'; +import { UnitTypeUpdate } from '../../../src/dtos/unit-types/unit-type-update.dto'; +import { randomUUID } from 'crypto'; +import { unitTypeArray } from '../../../prisma/seed-helpers/unit-type-factory'; + +describe('Testing unit type service', () => { + let service: UnitTypeService; + let prisma: PrismaService; + + const mockUnitType = (position: number, date: Date) => { + return { + id: randomUUID(), + name: unitTypeArray[position].name, + createdAt: date, + updatedAt: date, + numBedrooms: position, + }; + }; + + const mockUnitTypeSet = (numberToCreate: number, date: Date) => { + const toReturn = []; + for (let i = 0; i < numberToCreate; i++) { + toReturn.push(mockUnitType(i, date)); + } + return toReturn; + }; + + beforeAll(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [UnitTypeService, PrismaService], + }).compile(); + + service = module.get(UnitTypeService); + prisma = module.get(PrismaService); + }); + + it('testing list()', async () => { + const date = new Date(); + const mockedValue = mockUnitTypeSet(3, date); + prisma.unitTypes.findMany = jest.fn().mockResolvedValue(mockedValue); + + expect(await service.list()).toEqual([ + { + id: mockedValue[0].id, + name: unitTypeArray[0].name, + createdAt: date, + updatedAt: date, + numBedrooms: 0, + }, + { + id: mockedValue[1].id, + name: unitTypeArray[1].name, + createdAt: date, + updatedAt: date, + numBedrooms: 1, + }, + { + id: mockedValue[2].id, + name: unitTypeArray[2].name, + createdAt: date, + updatedAt: date, + numBedrooms: 2, + }, + ]); + + expect(prisma.unitTypes.findMany).toHaveBeenCalled(); + }); + + it('testing findOne() with id present', async () => { + const date = new Date(); + const mockedValue = mockUnitType(3, date); + prisma.unitTypes.findFirst = jest.fn().mockResolvedValue(mockedValue); + + expect(await service.findOne('example Id')).toEqual({ + id: mockedValue.id, + name: unitTypeArray[3].name, + createdAt: date, + updatedAt: date, + numBedrooms: 3, + }); + + expect(prisma.unitTypes.findFirst).toHaveBeenCalledWith({ + where: { + id: { + equals: 'example Id', + }, + }, + }); + }); + + it('testing findOne() with id not present', async () => { + prisma.unitTypes.findFirst = jest.fn().mockResolvedValue(null); + + await expect( + async () => await service.findOne('example Id'), + ).rejects.toThrowError(); + + expect(prisma.unitTypes.findFirst).toHaveBeenCalledWith({ + where: { + id: { + equals: 'example Id', + }, + }, + }); + }); + + it('testing create()', async () => { + const date = new Date(); + const mockedValue = mockUnitType(3, date); + prisma.unitTypes.create = jest.fn().mockResolvedValue(mockedValue); + + const params: UnitTypeCreate = { + numBedrooms: 3, + name: unitTypeArray[3].name, + }; + + expect(await service.create(params)).toEqual({ + id: mockedValue.id, + name: unitTypeArray[3].name, + createdAt: date, + updatedAt: date, + numBedrooms: 3, + }); + + expect(prisma.unitTypes.create).toHaveBeenCalledWith({ + data: { + name: unitTypeArray[3].name, + numBedrooms: 3, + }, + }); + }); + + it('testing update() existing record found', async () => { + const date = new Date(); + + const mockedUnitType = mockUnitType(3, date); + + prisma.unitTypes.findFirst = jest.fn().mockResolvedValue(mockedUnitType); + prisma.unitTypes.update = jest.fn().mockResolvedValue({ + ...mockedUnitType, + name: unitTypeArray[4].name, + numBedrooms: 4, + }); + + const params: UnitTypeUpdate = { + numBedrooms: 4, + name: unitTypeArray[4].name, + id: mockedUnitType.id, + }; + + expect(await service.update(params)).toEqual({ + id: mockedUnitType.id, + name: unitTypeArray[4].name, + createdAt: date, + updatedAt: date, + numBedrooms: 4, + }); + + expect(prisma.unitTypes.findFirst).toHaveBeenCalledWith({ + where: { + id: mockedUnitType.id, + }, + }); + + expect(prisma.unitTypes.update).toHaveBeenCalledWith({ + data: { + name: unitTypeArray[4].name, + numBedrooms: 4, + }, + where: { + id: mockedUnitType.id, + }, + }); + }); + + it('testing update() existing record not found', async () => { + prisma.unitTypes.findFirst = jest.fn().mockResolvedValue(null); + prisma.unitTypes.update = jest.fn().mockResolvedValue(null); + + const params: UnitTypeUpdate = { + numBedrooms: 4, + name: unitTypeArray[4].name, + id: 'example id', + }; + + await expect( + async () => await service.update(params), + ).rejects.toThrowError(); + + expect(prisma.unitTypes.findFirst).toHaveBeenCalledWith({ + where: { + id: 'example id', + }, + }); + }); + + it('testing delete()', async () => { + const date = new Date(); + + const mockedUnitType = mockUnitType(3, date); + + prisma.unitTypes.findFirst = jest.fn().mockResolvedValue(mockedUnitType); + prisma.unitTypes.delete = jest.fn().mockResolvedValue(mockedUnitType); + + expect(await service.delete('example Id')).toEqual({ + success: true, + }); + + expect(prisma.unitTypes.delete).toHaveBeenCalledWith({ + where: { + id: 'example Id', + }, + }); + }); +}); diff --git a/backend_new/types/src/backend-swagger.ts b/backend_new/types/src/backend-swagger.ts index 130588f7fa..7592030faf 100644 --- a/backend_new/types/src/backend-swagger.ts +++ b/backend_new/types/src/backend-swagger.ts @@ -455,6 +455,135 @@ export class ReservedCommunityTypesService { } } +export class UnitTypesService { + /** + * List unitTypes + */ + list(options: IRequestOptions = {}): Promise { + return new Promise((resolve, reject) => { + let url = basePath + '/unitTypes'; + + const configs: IRequestConfig = getConfigs( + 'get', + 'application/json', + url, + options, + ); + + /** 适配ios13getèŻ·æ±‚äžć…èźžćžŠbody */ + + axios(configs, resolve, reject); + }); + } + /** + * Create unitType + */ + create( + params: { + /** requestBody */ + body?: UnitTypeCreate; + } = {} as any, + options: IRequestOptions = {}, + ): Promise { + return new Promise((resolve, reject) => { + let url = basePath + '/unitTypes'; + + const configs: IRequestConfig = getConfigs( + 'post', + 'application/json', + url, + options, + ); + + let data = params.body; + + configs.data = data; + + axios(configs, resolve, reject); + }); + } + /** + * Delete unitType by id + */ + delete( + params: { + /** requestBody */ + body?: IdDTO; + } = {} as any, + options: IRequestOptions = {}, + ): Promise { + return new Promise((resolve, reject) => { + let url = basePath + '/unitTypes'; + + const configs: IRequestConfig = getConfigs( + 'delete', + 'application/json', + url, + options, + ); + + let data = params.body; + + configs.data = data; + + axios(configs, resolve, reject); + }); + } + /** + * Get unitType by id + */ + retrieve( + params: { + /** */ + unitTypeId: string; + } = {} as any, + options: IRequestOptions = {}, + ): Promise { + return new Promise((resolve, reject) => { + let url = basePath + '/unitTypes/{unitTypeId}'; + url = url.replace('{unitTypeId}', params['unitTypeId'] + ''); + + const configs: IRequestConfig = getConfigs( + 'get', + 'application/json', + url, + options, + ); + + /** 适配ios13getèŻ·æ±‚äžć…èźžćžŠbody */ + + axios(configs, resolve, reject); + }); + } + /** + * Update unitType + */ + update( + params: { + /** requestBody */ + body?: UnitTypeUpdate; + } = {} as any, + options: IRequestOptions = {}, + ): Promise { + return new Promise((resolve, reject) => { + let url = basePath + '/unitTypes/{unitTypeId}'; + + const configs: IRequestConfig = getConfigs( + 'put', + 'application/json', + url, + options, + ); + + let data = params.body; + + configs.data = data; + + axios(configs, resolve, reject); + }); + } +} + export interface ListingsQueryParams { /** */ page?: number; @@ -540,6 +669,12 @@ export interface UnitType { /** */ updatedAt: Date; + + /** */ + name: UnitTypeEnum; + + /** */ + numBedrooms: number; } export interface UnitAccessibilityPriorityType { @@ -792,6 +927,25 @@ export interface ReservedCommunityType { jurisdictions: IdDTO; } +export interface UnitTypeCreate { + /** */ + name: UnitTypeEnum; + + /** */ + numBedrooms: number; +} + +export interface UnitTypeUpdate { + /** */ + id: string; + + /** */ + name: UnitTypeEnum; + + /** */ + numBedrooms: number; +} + export enum ListingViews { 'fundamentals' = 'fundamentals', 'base' = 'base', @@ -840,3 +994,13 @@ export enum ApplicationMethodsTypeEnum { 'LeasingAgent' = 'LeasingAgent', 'Referral' = 'Referral', } + +export enum UnitTypeEnum { + 'studio' = 'studio', + 'oneBdrm' = 'oneBdrm', + 'twoBdrm' = 'twoBdrm', + 'threeBdrm' = 'threeBdrm', + 'fourBdrm' = 'fourBdrm', + 'SRO' = 'SRO', + 'fiveBdrm' = 'fiveBdrm', +} From 894d51488795abcc69745c402d9cbc0c71d90d50 Mon Sep 17 00:00:00 2001 From: Morgan Ludtke <42942267+ludtkemorgan@users.noreply.github.com> Date: Fri, 23 Jun 2023 14:15:10 -0500 Subject: [PATCH 08/57] Prisma Translation service (#3506) * fix: first part of translation service * fix: remaining translation service changes * fix: add google translate dependency * fix: lint fixes * fix: address review comments * fix: add listing e2e change * fix: update ci dbsetup task --- backend_new/.env.template | 5 +- backend_new/package.json | 2 + .../prisma/seed-helpers/listing-factory.ts | 6 +- backend_new/src/modules/listing.module.ts | 9 +- .../src/services/google-translate.service.ts | 30 + backend_new/src/services/listing.service.ts | 10 +- .../src/services/translation.service.ts | 161 +++++ .../unit/services/listing.service.spec.ts | 16 +- .../unit/services/translation.service.spec.ts | 265 +++++++ backend_new/yarn.lock | 670 +++++++++++++++++- 10 files changed, 1154 insertions(+), 20 deletions(-) create mode 100644 backend_new/src/services/google-translate.service.ts create mode 100644 backend_new/src/services/translation.service.ts create mode 100644 backend_new/test/unit/services/translation.service.spec.ts diff --git a/backend_new/.env.template b/backend_new/.env.template index 3b8d8e5383..8ad3216d57 100644 --- a/backend_new/.env.template +++ b/backend_new/.env.template @@ -1,2 +1,5 @@ DATABASE_URL="postgres://@localhost:5432/bloom_prisma" -PORT=3101 \ No newline at end of file +PORT=3101 +GOOGLE_API_EMAIL= +GOOGLE_API_ID= +GOOGLE_API_KEY= \ No newline at end of file diff --git a/backend_new/package.json b/backend_new/package.json index 035b6c9929..579b87e443 100644 --- a/backend_new/package.json +++ b/backend_new/package.json @@ -26,6 +26,7 @@ "db:setup": "yarn db:resetup && yarn db:migration:run && yarn db:seed" }, "dependencies": { + "@google-cloud/translate": "^7.2.1", "@nestjs/common": "^8.0.0", "@nestjs/core": "^8.0.0", "@nestjs/platform-express": "^8.0.0", @@ -33,6 +34,7 @@ "@prisma/client": "^4.14.0", "class-validator": "^0.14.0", "class-transformer": "^0.5.1", + "lodash": "^4.17.21", "prisma": "^4.13.0", "qs": "^6.11.2", "reflect-metadata": "^0.1.13", diff --git a/backend_new/prisma/seed-helpers/listing-factory.ts b/backend_new/prisma/seed-helpers/listing-factory.ts index 7909be1a15..15c389d6c6 100644 --- a/backend_new/prisma/seed-helpers/listing-factory.ts +++ b/backend_new/prisma/seed-helpers/listing-factory.ts @@ -295,7 +295,9 @@ export const listingFactory = ( subText: `multiselectQuestions: ${i} subText: ${i}`, description: `multiselectQuestions: ${i} description: ${i}`, links: {}, - options: {}, + options: [ + { text: `multiselectQuestions: ${i} option: ${i}`, ordinal: 1 }, + ], optOutText: `multiselectQuestions: ${i} optOutText: ${i}`, hideFromListing: true, applicationSection: @@ -316,7 +318,7 @@ export const listingFactory = ( subText: `multiselectQuestions: ${i} subText: ${i}`, description: `multiselectQuestions: ${i} description: ${i}`, links: {}, - options: {}, + options: [], optOutText: `multiselectQuestions: ${i} optOutText: ${i}`, hideFromListing: true, applicationSection: diff --git a/backend_new/src/modules/listing.module.ts b/backend_new/src/modules/listing.module.ts index 0f9fec5bb3..3e169561a7 100644 --- a/backend_new/src/modules/listing.module.ts +++ b/backend_new/src/modules/listing.module.ts @@ -2,11 +2,18 @@ import { Module } from '@nestjs/common'; import { ListingController } from '../controllers/listing.controller'; import { ListingService } from '../services/listing.service'; import { PrismaService } from '../services/prisma.service'; +import { TranslationService } from '../services/translation.service'; +import { GoogleTranslateService } from '../services/google-translate.service'; @Module({ imports: [], controllers: [ListingController], - providers: [ListingService, PrismaService], + providers: [ + ListingService, + PrismaService, + TranslationService, + GoogleTranslateService, + ], exports: [ListingService, PrismaService], }) export class ListingModule {} diff --git a/backend_new/src/services/google-translate.service.ts b/backend_new/src/services/google-translate.service.ts new file mode 100644 index 0000000000..89edd82faa --- /dev/null +++ b/backend_new/src/services/google-translate.service.ts @@ -0,0 +1,30 @@ +import { Injectable } from '@nestjs/common'; +import { Translate } from '@google-cloud/translate/build/src/v2'; +import { LanguagesEnum } from '@prisma/client'; + +@Injectable() +export class GoogleTranslateService { + public isConfigured(): boolean { + const { GOOGLE_API_ID, GOOGLE_API_EMAIL, GOOGLE_API_KEY } = process.env; + return !!GOOGLE_API_KEY && !!GOOGLE_API_EMAIL && !!GOOGLE_API_ID; + } + public async fetch(values: string[], language: LanguagesEnum) { + return await GoogleTranslateService.makeTranslateService().translate( + values, + { + from: LanguagesEnum.en, + to: language, + }, + ); + } + + private static makeTranslateService() { + return new Translate({ + credentials: { + private_key: process.env.GOOGLE_API_KEY.replace(/\\n/gm, '\n'), + client_email: process.env.GOOGLE_API_EMAIL, + }, + projectId: process.env.GOOGLE_API_ID, + }); + } +} diff --git a/backend_new/src/services/listing.service.ts b/backend_new/src/services/listing.service.ts index f11781f1d3..85d3371ecc 100644 --- a/backend_new/src/services/listing.service.ts +++ b/backend_new/src/services/listing.service.ts @@ -19,6 +19,7 @@ import { } from '../utilities/unit-utilities'; import { AmiChart } from '../dtos/ami-charts/ami-chart.dto'; import { ListingViews } from '../enums/listings/view-enum'; +import { TranslationService } from './translation.service'; export type getListingsArgs = { skip: number; @@ -111,7 +112,10 @@ views.details = { */ @Injectable() export class ListingService { - constructor(private prisma: PrismaService) {} + constructor( + private prisma: PrismaService, + private translationService: TranslationService, + ) {} /* this will get a set of listings given the params passed in @@ -315,14 +319,14 @@ export class ListingService { }, }); - const result = mapTo(ListingGet, listingRaw); + let result = mapTo(ListingGet, listingRaw); if (!result) { throw new NotFoundException(); } if (lang !== LanguagesEnum.en) { - // TODO: await this.translationService.translateListing(result, lang); + result = await this.translationService.translateListing(result, lang); } await this.addUnitsSummarized(result); diff --git a/backend_new/src/services/translation.service.ts b/backend_new/src/services/translation.service.ts new file mode 100644 index 0000000000..672a69f349 --- /dev/null +++ b/backend_new/src/services/translation.service.ts @@ -0,0 +1,161 @@ +import { Injectable } from '@nestjs/common'; +import { PrismaService } from './prisma.service'; +import { LanguagesEnum, Prisma } from '@prisma/client'; +import { ListingGet } from '../dtos/listings/listing-get.dto'; +import { GoogleTranslateService } from './google-translate.service'; +import * as lodash from 'lodash'; + +@Injectable() +export class TranslationService { + constructor( + private prisma: PrismaService, + private readonly googleTranslateService: GoogleTranslateService, + ) {} + + public async getTranslationByLanguageAndJurisdictionOrDefaultEn( + language: LanguagesEnum, + jurisdictionId: string | null, + ) { + let translations = await this.prisma.translations.findUnique({ + where: { + jurisdictionId_language: { language, jurisdictionId }, + }, + }); + + if (translations === null) { + console.warn( + `Fetching translations for ${language} failed on jurisdiction ${jurisdictionId}, defaulting to english.`, + ); + translations = await this.prisma.translations.findUnique({ + where: { + jurisdictionId_language: { + language: LanguagesEnum.en, + jurisdictionId, + }, + }, + }); + } + return translations; + } + + public async translateListing(listing: ListingGet, language: LanguagesEnum) { + if (!this.googleTranslateService.isConfigured()) { + console.warn( + 'listing translation requested, but google translate service is not configured', + ); + return; + } + + const pathsToFilter = { + applicationPickUpAddressOfficeHours: + listing.applicationPickUpAddressOfficeHours, + costsNotIncluded: listing.costsNotIncluded, + creditHistory: listing.creditHistory, + criminalBackground: listing.criminalBackground, + programRules: listing.programRules, + rentalAssistance: listing.rentalAssistance, + rentalHistory: listing.rentalHistory, + requiredDocuments: listing.requiredDocuments, + specialNotes: listing.specialNotes, + whatToExpect: listing.whatToExpect, + accessibility: listing.accessibility, + amenities: listing.amenities, + neighborhood: listing.neighborhood, + petPolicy: listing.petPolicy, + servicesOffered: listing.servicesOffered, + smokingPolicy: listing.smokingPolicy, + unitAmenities: listing.unitAmenities, + }; + + listing.events?.forEach((_, index) => { + pathsToFilter[`events[${index}].note`] = listing.events[index].note; + pathsToFilter[`events[${index}].label`] = listing.events[index].label; + }); + + if (listing.listingMultiselectQuestions) { + listing.listingMultiselectQuestions.map((multiselectQuestion, index) => { + multiselectQuestion.multiselectQuestions.untranslatedText = + multiselectQuestion.multiselectQuestions.text; + multiselectQuestion.multiselectQuestions.untranslatedOptOutText = + multiselectQuestion.multiselectQuestions.optOutText; + pathsToFilter[ + `listingMultiselectQuestions[${index}].multiselectQuestions.text` + ] = multiselectQuestion.multiselectQuestions.text; + pathsToFilter[ + `listingMultiselectQuestions[${index}].multiselectQuestions.description` + ] = multiselectQuestion.multiselectQuestions.description; + pathsToFilter[ + `listingMultiselectQuestions[${index}].multiselectQuestions.subText` + ] = multiselectQuestion.multiselectQuestions.subText; + pathsToFilter[ + `listingMultiselectQuestions[${index}].multiselectQuestions.optOutText` + ] = multiselectQuestion.multiselectQuestions.optOutText; + // TODO: should we translate links? + multiselectQuestion.multiselectQuestions.options?.map( + (multiselectOption, optionIndex) => { + multiselectOption.untranslatedText = multiselectOption.text; + pathsToFilter[ + `listingMultiselectQuestions[${index}].multiselectQuestions.options[${optionIndex}].text` + ] = multiselectOption.text; + pathsToFilter[ + `listingMultiselectQuestions[${index}].multiselectQuestions.options[${optionIndex}].description` + ] = multiselectOption.description; + pathsToFilter[ + `listingMultiselectQuestions[${index}].multiselectQuestions.options[${optionIndex}].description` + ] = multiselectOption.description; + }, + // TODO: should we translate links? + ); + }); + } + + const persistedTranslationsFromDB = await this.getPersistedTranslatedValues( + listing, + language, + ); + let translatedValue; + + if (persistedTranslationsFromDB) { + translatedValue = persistedTranslationsFromDB.translations; + } else { + translatedValue = await this.googleTranslateService.fetch( + Object.values(pathsToFilter), + language, + ); + await this.persistNewTranslatedValues(listing, language, translatedValue); + } + + if (translatedValue) { + [...Object.keys(pathsToFilter).values()].forEach((path, index) => { + lodash.set(listing, path, translatedValue[0][index]); + }); + } + + return listing; + } + + private async getPersistedTranslatedValues( + listing: ListingGet, + language: LanguagesEnum, + ) { + return this.prisma.generatedListingTranslations.findFirst({ + where: { listingId: listing.id, language: language }, + }); + } + + private async persistNewTranslatedValues( + listing: ListingGet, + language: LanguagesEnum, + translatedValues: any, + ) { + return this.prisma.generatedListingTranslations.create({ + data: { + jurisdictionId: listing.jurisdictions.id, + listingId: listing.id, + language: language, + translations: translatedValues, + timestamp: new Date(), + }, + }); + } +} diff --git a/backend_new/test/unit/services/listing.service.spec.ts b/backend_new/test/unit/services/listing.service.spec.ts index 10e5988e07..a1ed62d28c 100644 --- a/backend_new/test/unit/services/listing.service.spec.ts +++ b/backend_new/test/unit/services/listing.service.spec.ts @@ -12,6 +12,8 @@ import { Unit } from '../../../src/dtos/units/unit-get.dto'; import { UnitTypeSort } from '../../../src/utilities/unit-utilities'; import { ListingGet } from '../../../src/dtos/listings/listing-get.dto'; import { ListingViews } from '../../../src/enums/listings/view-enum'; +import { TranslationService } from '../../../src/services/translation.service'; +import { GoogleTranslateService } from '../../../src/services/google-translate.service'; /* generates a super simple mock listing for us to test logic with @@ -95,9 +97,21 @@ describe('Testing listing service', () => { let service: ListingService; let prisma: PrismaService; + const googleTranslateServiceMock = { + isConfigured: () => true, + fetch: jest.fn(), + }; beforeEach(async () => { const module: TestingModule = await Test.createTestingModule({ - providers: [ListingService, PrismaService], + providers: [ + ListingService, + PrismaService, + TranslationService, + { + provide: GoogleTranslateService, + useValue: googleTranslateServiceMock, + }, + ], }).compile(); service = module.get(ListingService); diff --git a/backend_new/test/unit/services/translation.service.spec.ts b/backend_new/test/unit/services/translation.service.spec.ts new file mode 100644 index 0000000000..1fc39dd617 --- /dev/null +++ b/backend_new/test/unit/services/translation.service.spec.ts @@ -0,0 +1,265 @@ +import { + LanguagesEnum, + ListingsStatusEnum, + MultiselectQuestionsApplicationSectionEnum, + Prisma, +} from '@prisma/client'; +import { randomUUID } from 'crypto'; +import { ListingGet } from '../../../src/dtos/listings/listing-get.dto'; +import { TranslationService } from '../../../src/services/translation.service'; +import { PrismaService } from '../../../src/services/prisma.service'; +import { Test, TestingModule } from '@nestjs/testing'; +import { GoogleTranslateService } from '../../../src/services/google-translate.service'; + +const mockListing = (): ListingGet => { + const basicListing = { + id: 'id 1', + createdAt: new Date(), + updatedAt: new Date(), + name: 'listing 1', + status: ListingsStatusEnum.active, + displayWaitlistSize: true, + applicationMethods: [], + assets: [], + events: [], + listingsBuildingAddress: { + id: 'address id 1', + createdAt: new Date(), + updatedAt: new Date(), + city: 'bloom city', + state: 'Bloom', + street: '123 main street', + zipCode: '12345', + }, + jurisdictions: { + id: 'jurisdiction id 1', + createdAt: new Date(), + updatedAt: new Date(), + name: 'jurisdiction', + languages: [], + multiselectQuestions: [], + publicUrl: '', + emailFromAddress: '', + rentalAssistanceDefault: '', + enableAccessibilityFeatures: true, + enableUtilitiesIncluded: true, + }, + units: [], + unitsSummarized: undefined, + unitsSummary: [], + showWaitlist: true, + referralApplication: undefined, + }; + return { + ...basicListing, + unitAmenities: 'untranslated unit amenities', + smokingPolicy: 'untranslated smoking policy', + servicesOffered: 'untranslated services offered', + petPolicy: 'untranslated pet policy', + neighborhood: 'untranslated neighborhood', + amenities: 'untranslated amenities', + accessibility: 'untranslated accessibility', + whatToExpect: 'untranslated what to expect', + specialNotes: 'untranslated special notes', + requiredDocuments: 'untranslated required documents', + rentalHistory: 'untranslated rental history', + rentalAssistance: 'untranslated rental assistance', + programRules: 'untranslated program rules', + criminalBackground: 'untranslated criminal background', + creditHistory: 'untranslated credit history', + costsNotIncluded: 'untranslated costs not included', + applicationPickUpAddressOfficeHours: + 'untranslated application pick up address office hours', + listingMultiselectQuestions: [ + { + multiselectQuestions: { + id: 'multiselectQuestions id 1', + createdAt: new Date(), + updatedAt: new Date(), + text: 'untranslated multiselect text', + description: 'untranslated multiselect description', + subText: 'untranslated multiselect subtext', + optOutText: 'untranslated multiselect opt out text', + listings: [], + jurisdictions: [], + applicationSection: + MultiselectQuestionsApplicationSectionEnum.preferences, + }, + listings: basicListing, + }, + ], + }; +}; + +const translatedStrings = [ + 'translated application pick up address office hours', + 'translated costs not included', + 'translated credit history', + 'translated criminal background', + 'translated program rules', + 'translated rental assistance', + 'translated rental history', + 'translated required documents', + 'translated special notes', + 'translated what to expect', + 'translated accessibility', + 'translated amenities', + 'translated neighborhood', + 'translated pet policy', + 'translated services offered', + 'translated smoking policy', + 'translated unit amenities', + 'translated multiselect text', + 'translated multiselect description', + 'translated multiselect subtext', + 'translated multiselect opt out text', +]; + +describe('Testing translations service', () => { + let service: TranslationService; + let prisma: PrismaService; + let googleTranslateServiceMock; + + beforeEach(async () => { + googleTranslateServiceMock = { + isConfigured: () => true, + fetch: jest.fn(), + }; + const module: TestingModule = await Test.createTestingModule({ + providers: [ + TranslationService, + PrismaService, + { + provide: GoogleTranslateService, + useValue: googleTranslateServiceMock, + }, + ], + }).compile(); + + service = module.get(TranslationService); + prisma = module.get(PrismaService); + }); + + it('Should fall back to english if language does not exist', async () => { + const jurisdictionId = randomUUID(); + const translations = { + id: 'translations id 1', + createdAt: new Date(), + updatedAt: new Date(), + language: LanguagesEnum.en, + jurisdictionId: jurisdictionId, + translations: { + translation1: 'translation 1', + translation2: 'translation 2', + }, + }; + // first call fails to find value so moves to the fallback + prisma.translations.findUnique = jest + .fn() + .mockResolvedValueOnce(null) + .mockResolvedValueOnce(translations); + + const result = + await service.getTranslationByLanguageAndJurisdictionOrDefaultEn( + LanguagesEnum.es, + jurisdictionId, + ); + + expect(result).toEqual(translations); + expect(prisma.translations.findUnique).toHaveBeenCalledTimes(2); + }); + + it('Should get unique translations by language and jurisdiction', async () => { + const jurisdictionId = randomUUID(); + const translations = { + id: 'translations id 1', + createdAt: new Date(), + updatedAt: new Date(), + language: LanguagesEnum.en, + jurisdictionId: jurisdictionId, + translations: { + translation1: 'translation 1', + translation2: 'translation 2', + }, + }; + prisma.translations.findUnique = jest + .fn() + .mockResolvedValueOnce(translations); + + const result = + await service.getTranslationByLanguageAndJurisdictionOrDefaultEn( + LanguagesEnum.es, + jurisdictionId, + ); + + expect(result).toEqual(translations); + expect(prisma.translations.findUnique).toHaveBeenCalledTimes(1); + }); + + it('Should fetch translations and translate listing if not in db', async () => { + googleTranslateServiceMock.fetch.mockResolvedValueOnce([translatedStrings]); + prisma.generatedListingTranslations.findFirst = jest + .fn() + .mockResolvedValue(null); + + const result = await service.translateListing( + mockListing() as ListingGet, + LanguagesEnum.es, + ); + expect(googleTranslateServiceMock.fetch).toHaveBeenCalledTimes(1); + expect(prisma.generatedListingTranslations.findFirst).toHaveBeenCalledTimes( + 1, + ); + validateTranslatedFields(result); + }); + + it('Should fetch translations from db and translate listing', async () => { + prisma.generatedListingTranslations.findFirst = jest + .fn() + .mockResolvedValue({ translations: [translatedStrings] }); + + const result = await service.translateListing( + mockListing() as ListingGet, + LanguagesEnum.es, + ); + expect(googleTranslateServiceMock.fetch).toHaveBeenCalledTimes(0); + expect(prisma.generatedListingTranslations.findFirst).toHaveBeenCalledTimes( + 1, + ); + validateTranslatedFields(result); + }); +}); + +const validateTranslatedFields = (listing: ListingGet) => { + expect(listing.applicationPickUpAddressOfficeHours).toEqual( + 'translated application pick up address office hours', + ); + expect(listing.costsNotIncluded).toEqual('translated costs not included'); + expect(listing.creditHistory).toEqual('translated credit history'); + expect(listing.criminalBackground).toEqual('translated criminal background'); + expect(listing.programRules).toEqual('translated program rules'); + expect(listing.rentalAssistance).toEqual('translated rental assistance'); + expect(listing.rentalHistory).toEqual('translated rental history'); + expect(listing.requiredDocuments).toEqual('translated required documents'); + expect(listing.specialNotes).toEqual('translated special notes'); + expect(listing.whatToExpect).toEqual('translated what to expect'); + expect(listing.accessibility).toEqual('translated accessibility'); + expect(listing.amenities).toEqual('translated amenities'); + expect(listing.neighborhood).toEqual('translated neighborhood'); + expect(listing.petPolicy).toEqual('translated pet policy'); + expect(listing.servicesOffered).toEqual('translated services offered'); + expect(listing.smokingPolicy).toEqual('translated smoking policy'); + expect(listing.unitAmenities).toEqual('translated unit amenities'); + expect( + listing.listingMultiselectQuestions[0].multiselectQuestions.text, + ).toEqual('translated multiselect text'); + expect( + listing.listingMultiselectQuestions[0].multiselectQuestions.description, + ).toEqual('translated multiselect description'); + expect( + listing.listingMultiselectQuestions[0].multiselectQuestions.subText, + ).toEqual('translated multiselect subtext'); + expect( + listing.listingMultiselectQuestions[0].multiselectQuestions.optOutText, + ).toEqual('translated multiselect opt out text'); +}; diff --git a/backend_new/yarn.lock b/backend_new/yarn.lock index c338a362bb..48131685a9 100644 --- a/backend_new/yarn.lock +++ b/backend_new/yarn.lock @@ -220,6 +220,11 @@ resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.21.8.tgz#642af7d0333eab9c0ad70b14ac5e76dbde7bfdf8" integrity sha512-6zavDGdzG3gUqAdWvlLFfk+36RilI+Pwyuuh7HItyeScCWP3k6i8vKclAQ0bM/0y/Kz/xiwvxhMv9MgTJP5gmA== +"@babel/parser@^7.20.15": + version "7.22.5" + resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.22.5.tgz#721fd042f3ce1896238cf1b341c77eb7dee7dbea" + integrity sha512-DFZMC9LJUG9PLOclRC32G63UXwzqS2koQC8dkx+PLdmt1xSePYpbT/NbsrJy8Q/muXz7o/h/d4A7Fuyixm559Q== + "@babel/plugin-syntax-async-generators@^7.8.4": version "7.8.4" resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-async-generators/-/plugin-syntax-async-generators-7.8.4.tgz#a983fb1aeb2ec3f6ed042a210f640e90e786fe0d" @@ -394,6 +399,62 @@ resolved "https://registry.yarnpkg.com/@eslint/js/-/js-8.41.0.tgz#080321c3b68253522f7646b55b577dd99d2950b3" integrity sha512-LxcyMGxwmTh2lY9FwHPGWOHmYFCZvbrFCBZL4FzSSsxsRPuhrYUg/49/0KDfW8tnIEaEHtfmn6+NPN+1DqaNmA== +"@google-cloud/common@^4.0.0": + version "4.0.3" + resolved "https://registry.yarnpkg.com/@google-cloud/common/-/common-4.0.3.tgz#d4324ac83087385d727593f7e1b6d81ee66442cf" + integrity sha512-fUoMo5b8iAKbrYpneIRV3z95AlxVJPrjpevxs4SKoclngWZvTXBSGpNisF5+x5m+oNGve7jfB1e6vNBZBUs7Fw== + dependencies: + "@google-cloud/projectify" "^3.0.0" + "@google-cloud/promisify" "^3.0.0" + arrify "^2.0.1" + duplexify "^4.1.1" + ent "^2.2.0" + extend "^3.0.2" + google-auth-library "^8.0.2" + retry-request "^5.0.0" + teeny-request "^8.0.0" + +"@google-cloud/projectify@^3.0.0": + version "3.0.0" + resolved "https://registry.yarnpkg.com/@google-cloud/projectify/-/projectify-3.0.0.tgz#302b25f55f674854dce65c2532d98919b118a408" + integrity sha512-HRkZsNmjScY6Li8/kb70wjGlDDyLkVk3KvoEo9uIoxSjYLJasGiCch9+PqRVDOCGUFvEIqyogl+BeqILL4OJHA== + +"@google-cloud/promisify@^3.0.0": + version "3.0.1" + resolved "https://registry.yarnpkg.com/@google-cloud/promisify/-/promisify-3.0.1.tgz#8d724fb280f47d1ff99953aee0c1669b25238c2e" + integrity sha512-z1CjRjtQyBOYL+5Qr9DdYIfrdLBe746jRTYfaYU6MeXkqp7UfYs/jX16lFFVzZ7PGEJvqZNqYUEtb1mvDww4pA== + +"@google-cloud/translate@^7.2.1": + version "7.2.1" + resolved "https://registry.yarnpkg.com/@google-cloud/translate/-/translate-7.2.1.tgz#a9bc8e9c249f3a86898d923eb0cbccf0195127bd" + integrity sha512-VXmEvMF8qa4c7p5drjUZ06B8v0DxoUfFPyjhFp/wbgoe0GWaD1y1gGk6rUZxGKD/pB19kFU+0LMMSCgBW+a0iA== + dependencies: + "@google-cloud/common" "^4.0.0" + "@google-cloud/promisify" "^3.0.0" + arrify "^2.0.0" + extend "^3.0.2" + google-gax "^3.5.8" + is-html "^2.0.0" + +"@grpc/grpc-js@~1.8.0": + version "1.8.15" + resolved "https://registry.yarnpkg.com/@grpc/grpc-js/-/grpc-js-1.8.15.tgz#17829cbc9f2bc8b3b0e22a4da59d72db2a34df5c" + integrity sha512-H2Bu/w6+oQ58DsRbQol66ERBk3V5ZIak/z/MDx0T4EgDnJWps807I6BvTjq0v6UvZtOcLO+ur+Q9wvniqu3OJA== + dependencies: + "@grpc/proto-loader" "^0.7.0" + "@types/node" ">=12.12.47" + +"@grpc/proto-loader@^0.7.0": + version "0.7.7" + resolved "https://registry.yarnpkg.com/@grpc/proto-loader/-/proto-loader-0.7.7.tgz#d33677a77eea8407f7c66e2abd97589b60eb4b21" + integrity sha512-1TIeXOi8TuSCQprPItwoMymZXxWT0CPxUhkrkeCUH+D8U7QDwQ6b7SUz2MaLuWM2llT+J/TVFLmQI5KtML3BhQ== + dependencies: + "@types/long" "^4.0.1" + lodash.camelcase "^4.3.0" + long "^4.0.0" + protobufjs "^7.0.0" + yargs "^17.7.2" + "@humanwhocodes/config-array@^0.11.8": version "0.11.8" resolved "https://registry.yarnpkg.com/@humanwhocodes/config-array/-/config-array-0.11.8.tgz#03595ac2075a4dc0f191cc2131de14fbd7d410b9" @@ -656,6 +717,13 @@ "@jridgewell/resolve-uri" "3.1.0" "@jridgewell/sourcemap-codec" "1.4.14" +"@jsdoc/salty@^0.2.1": + version "0.2.5" + resolved "https://registry.yarnpkg.com/@jsdoc/salty/-/salty-0.2.5.tgz#1b2fa5bb8c66485b536d86eee877c263d322f692" + integrity sha512-TfRP53RqunNe2HBobVBJ0VLhK1HbfvBYeTC1ahnN64PWvyYyGebmMiPkuwvD9fpw2ZbkoPb8Q7mwy0aR8Z9rvw== + dependencies: + lodash "^4.17.21" + "@nestjs/cli@^8.0.0": version "8.2.8" resolved "https://registry.yarnpkg.com/@nestjs/cli/-/cli-8.2.8.tgz#63e5b477f90e6d0238365dcc6236b95bf4f0c807" @@ -799,6 +867,59 @@ resolved "https://registry.yarnpkg.com/@prisma/engines/-/engines-4.14.1.tgz#dac49f8d1f2d4f14a8ed7e6f96b24cd49bd6cd91" integrity sha512-APqFddPVHYmWNKqc+5J5SqrLFfOghKOLZxobmguDUacxOwdEutLsbXPVhNnpFDmuQWQFbXmrTTPoRrrF6B1MWA== +"@protobufjs/aspromise@^1.1.1", "@protobufjs/aspromise@^1.1.2": + version "1.1.2" + resolved "https://registry.yarnpkg.com/@protobufjs/aspromise/-/aspromise-1.1.2.tgz#9b8b0cc663d669a7d8f6f5d0893a14d348f30fbf" + integrity sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ== + +"@protobufjs/base64@^1.1.2": + version "1.1.2" + resolved "https://registry.yarnpkg.com/@protobufjs/base64/-/base64-1.1.2.tgz#4c85730e59b9a1f1f349047dbf24296034bb2735" + integrity sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg== + +"@protobufjs/codegen@^2.0.4": + version "2.0.4" + resolved "https://registry.yarnpkg.com/@protobufjs/codegen/-/codegen-2.0.4.tgz#7ef37f0d010fb028ad1ad59722e506d9262815cb" + integrity sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg== + +"@protobufjs/eventemitter@^1.1.0": + version "1.1.0" + resolved "https://registry.yarnpkg.com/@protobufjs/eventemitter/-/eventemitter-1.1.0.tgz#355cbc98bafad5978f9ed095f397621f1d066b70" + integrity sha512-j9ednRT81vYJ9OfVuXG6ERSTdEL1xVsNgqpkxMsbIabzSo3goCjDIveeGv5d03om39ML71RdmrGNjG5SReBP/Q== + +"@protobufjs/fetch@^1.1.0": + version "1.1.0" + resolved "https://registry.yarnpkg.com/@protobufjs/fetch/-/fetch-1.1.0.tgz#ba99fb598614af65700c1619ff06d454b0d84c45" + integrity sha512-lljVXpqXebpsijW71PZaCYeIcE5on1w5DlQy5WH6GLbFryLUrBD4932W/E2BSpfRJWseIL4v/KPgBFxDOIdKpQ== + dependencies: + "@protobufjs/aspromise" "^1.1.1" + "@protobufjs/inquire" "^1.1.0" + +"@protobufjs/float@^1.0.2": + version "1.0.2" + resolved "https://registry.yarnpkg.com/@protobufjs/float/-/float-1.0.2.tgz#5e9e1abdcb73fc0a7cb8b291df78c8cbd97b87d1" + integrity sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ== + +"@protobufjs/inquire@^1.1.0": + version "1.1.0" + resolved "https://registry.yarnpkg.com/@protobufjs/inquire/-/inquire-1.1.0.tgz#ff200e3e7cf2429e2dcafc1140828e8cc638f089" + integrity sha512-kdSefcPdruJiFMVSbn801t4vFK7KB/5gd2fYvrxhuJYg8ILrmn9SKSX2tZdV6V+ksulWqS7aXjBcRXl3wHoD9Q== + +"@protobufjs/path@^1.1.2": + version "1.1.2" + resolved "https://registry.yarnpkg.com/@protobufjs/path/-/path-1.1.2.tgz#6cc2b20c5c9ad6ad0dccfd21ca7673d8d7fbf68d" + integrity sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA== + +"@protobufjs/pool@^1.1.0": + version "1.1.0" + resolved "https://registry.yarnpkg.com/@protobufjs/pool/-/pool-1.1.0.tgz#09fd15f2d6d3abfa9b65bc366506d6ad7846ff54" + integrity sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw== + +"@protobufjs/utf8@^1.1.0": + version "1.1.0" + resolved "https://registry.yarnpkg.com/@protobufjs/utf8/-/utf8-1.1.0.tgz#a777360b5b39a1a2e5106f8e858f2fd2d060c570" + integrity sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw== + "@sinonjs/commons@^1.7.0": version "1.8.6" resolved "https://registry.yarnpkg.com/@sinonjs/commons/-/commons-1.8.6.tgz#80c516a4dc264c2a69115e7578d62581ff455ed9" @@ -818,6 +939,11 @@ resolved "https://registry.yarnpkg.com/@tootallnate/once/-/once-1.1.2.tgz#ccb91445360179a04e7fe6aff78c00ffc1eeaf82" integrity sha512-RbzJvlNzmRq5c3O09UipeuXno4tA1FE6ikOjxZK0tuxVv3412l64l5t1W5pj4+rJq9vpkm/kwiR07aZXnsKPxw== +"@tootallnate/once@2": + version "2.0.0" + resolved "https://registry.yarnpkg.com/@tootallnate/once/-/once-2.0.0.tgz#f544a148d3ab35801c1f633a7441fd87c2e484bf" + integrity sha512-XCuKFP5PS55gnMVu3dty8KPatLqUoy/ZYzDzAGCQ8JNFCkLXzmI7vNHCR+XpbZaMWQK/vQubr7PkYq8g470J/A== + "@tsconfig/node10@^1.0.7": version "1.0.9" resolved "https://registry.yarnpkg.com/@tsconfig/node10/-/node10-1.0.9.tgz#df4907fc07a886922637b15e02d4cebc4c0021b2" @@ -937,6 +1063,14 @@ "@types/qs" "*" "@types/serve-static" "*" +"@types/glob@*": + version "8.1.0" + resolved "https://registry.yarnpkg.com/@types/glob/-/glob-8.1.0.tgz#b63e70155391b0584dce44e7ea25190bbc38f2fc" + integrity sha512-IO+MJPVhoqz+28h1qLAcBEH2+xHMK6MTyHJc7MTnnYb6wsoLR29POVGJ7LycmVXIqyy/4/2ShP5sUwTXuOwb/w== + dependencies: + "@types/minimatch" "^5.1.2" + "@types/node" "*" + "@types/graceful-fs@^4.1.2": version "4.1.6" resolved "https://registry.yarnpkg.com/@types/graceful-fs/-/graceful-fs-4.1.6.tgz#e14b2576a1c25026b7f02ede1de3b84c3a1efeae" @@ -986,6 +1120,29 @@ resolved "https://registry.yarnpkg.com/@types/json5/-/json5-0.0.29.tgz#ee28707ae94e11d2b827bcbe5270bcea7f3e71ee" integrity sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ== +"@types/linkify-it@*": + version "3.0.2" + resolved "https://registry.yarnpkg.com/@types/linkify-it/-/linkify-it-3.0.2.tgz#fd2cd2edbaa7eaac7e7f3c1748b52a19143846c9" + integrity sha512-HZQYqbiFVWufzCwexrvh694SOim8z2d+xJl5UNamcvQFejLY/2YUtzXHYi3cHdI7PMlS8ejH2slRAOJQ32aNbA== + +"@types/long@^4.0.0", "@types/long@^4.0.1": + version "4.0.2" + resolved "https://registry.yarnpkg.com/@types/long/-/long-4.0.2.tgz#b74129719fc8d11c01868010082d483b7545591a" + integrity sha512-MqTGEo5bj5t157U6fA/BiDynNkn0YknVdh48CMPkTSpFTVmvao5UQmm7uEF6xBEo7qIMAlY/JSleYaE6VOdpaA== + +"@types/markdown-it@^12.2.3": + version "12.2.3" + resolved "https://registry.yarnpkg.com/@types/markdown-it/-/markdown-it-12.2.3.tgz#0d6f6e5e413f8daaa26522904597be3d6cd93b51" + integrity sha512-GKMHFfv3458yYy+v/N8gjufHO6MSZKCOXpZc5GXIWWy8uldwfmPn98vp81gZ5f9SVw8YYBctgfJ22a2d7AOMeQ== + dependencies: + "@types/linkify-it" "*" + "@types/mdurl" "*" + +"@types/mdurl@*": + version "1.0.2" + resolved "https://registry.yarnpkg.com/@types/mdurl/-/mdurl-1.0.2.tgz#e2ce9d83a613bacf284c7be7d491945e39e1f8e9" + integrity sha512-eC4U9MlIcu2q0KQmXszyn5Akca/0jrQmwDRgpAMJai7qBWq4amIQhZyNau4VYGtCeALvW1/NtjzJJ567aZxfKA== + "@types/mime@*": version "3.0.1" resolved "https://registry.yarnpkg.com/@types/mime/-/mime-3.0.1.tgz#5f8f2bca0a5863cb69bc0b0acd88c96cb1d4ae10" @@ -1001,11 +1158,21 @@ resolved "https://registry.yarnpkg.com/@types/minimatch/-/minimatch-3.0.5.tgz#1001cc5e6a3704b83c236027e77f2f58ea010f40" integrity sha512-Klz949h02Gz2uZCMGwDUSDS1YBlTdDDgbWHi+81l29tQALUtvz4rAYi5uoVhE5Lagoq6DeqAUlbrHvW/mXDgdQ== +"@types/minimatch@^5.1.2": + version "5.1.2" + resolved "https://registry.yarnpkg.com/@types/minimatch/-/minimatch-5.1.2.tgz#07508b45797cb81ec3f273011b054cd0755eddca" + integrity sha512-K0VQKziLUWkVKiRVrx4a40iPaxTUefQmjtkQofBkYRcoaaL/8rhwDWww9qWbrgicNOgnpIsMxyNIUM4+n6dUIA== + "@types/node@*": version "20.1.0" resolved "https://registry.yarnpkg.com/@types/node/-/node-20.1.0.tgz#258805edc37c327cf706e64c6957f241ca4c4c20" integrity sha512-O+z53uwx64xY7D6roOi4+jApDGFg0qn6WHcxe5QeqjMaTezBO/mxdfFXIVAVVyNWKx84OmPB3L8kbVYOTeN34A== +"@types/node@>=12.12.47", "@types/node@>=13.7.0": + version "20.3.0" + resolved "https://registry.yarnpkg.com/@types/node/-/node-20.3.0.tgz#719498898d5defab83c3560f45d8498f58d11938" + integrity sha512-cumHmIAf6On83X7yP+LrsEyUOf/YlociZelmpRYaGFydoaPdxdt80MAbu6vWerQT2COCp2nPvHdsbD7tHn/YlQ== + "@types/node@^16.0.0": version "16.18.34" resolved "https://registry.yarnpkg.com/@types/node/-/node-16.18.34.tgz#62d2099b30339dec4b1b04a14c96266459d7c8b2" @@ -1031,6 +1198,14 @@ resolved "https://registry.yarnpkg.com/@types/range-parser/-/range-parser-1.2.4.tgz#cd667bcfdd025213aafb7ca5915a932590acdcdc" integrity sha512-EEhsLsD6UsDM1yFhAvy0Cjr6VwmpMWqFBCb9w07wVugF7w9nfajxLuVmngTIpgS6svCnm6Vaw+MZhoDCKnOfsw== +"@types/rimraf@^3.0.2": + version "3.0.2" + resolved "https://registry.yarnpkg.com/@types/rimraf/-/rimraf-3.0.2.tgz#a63d175b331748e5220ad48c901d7bbf1f44eef8" + integrity sha512-F3OznnSLAUxFrCEu/L5PY8+ny8DtcFRjx7fZZ9bycvXRi3KPTRS9HOitGZwvPg0juRhXFWIeKX58cnX5YqLohQ== + dependencies: + "@types/glob" "*" + "@types/node" "*" + "@types/semver@^7.3.12": version "7.5.0" resolved "https://registry.yarnpkg.com/@types/semver/-/semver-7.5.0.tgz#591c1ce3a702c45ee15f47a42ade72c2fd78978a" @@ -1309,6 +1484,13 @@ abab@^2.0.3, abab@^2.0.5: resolved "https://registry.yarnpkg.com/abab/-/abab-2.0.6.tgz#41b80f2c871d19686216b82309231cfd3cb3d291" integrity sha512-j2afSsaIENvHZN2B8GOpF566vZ5WVk5opAiMTvWgaQT8DkbOqsTfvNAvHoRGU2zzP8cPoqys+xHTRDWW8L+/BA== +abort-controller@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/abort-controller/-/abort-controller-3.0.0.tgz#eaf54d53b62bae4138e809ca225c8439a6efb392" + integrity sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg== + dependencies: + event-target-shim "^5.0.0" + accepts@~1.3.8: version "1.3.8" resolved "https://registry.yarnpkg.com/accepts/-/accepts-1.3.8.tgz#0bf0be125b67014adcb0b0921e62db7bffe16b2e" @@ -1485,7 +1667,7 @@ array-union@^2.1.0: resolved "https://registry.yarnpkg.com/array-union/-/array-union-2.1.0.tgz#b798420adbeb1de828d84acd8a2e23d3efe85e8d" integrity sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw== -arrify@^2.0.1: +arrify@^2.0.0, arrify@^2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/arrify/-/arrify-2.0.1.tgz#c9655e9331e0abcd588d2a7cad7e9956f66701fa" integrity sha512-3duEwti880xqi4eAMN8AyR4a0ByT90zoYdLlevfrvU43vb0YZwZVfxOgxWrLXXXpyugL0hNZc9G6BiB5B3nUug== @@ -1583,11 +1765,16 @@ balanced-match@^1.0.0: resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.2.tgz#e83e3a7e3f300b34cb9d87f615fa0cbf357690ee" integrity sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw== -base64-js@^1.3.1: +base64-js@^1.3.0, base64-js@^1.3.1: version "1.5.1" resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.5.1.tgz#1b1b440160a5bf7ad40b650f095963481903930a" integrity sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA== +bignumber.js@^9.0.0: + version "9.1.1" + resolved "https://registry.yarnpkg.com/bignumber.js/-/bignumber.js-9.1.1.tgz#c4df7dc496bd849d4c9464344c1aa74228b4dac6" + integrity sha512-pHm4LsMJ6lzgNGVfZHjMoO8sdoRhOzOH4MLmY65Jg70bpxCKu5iOHNJyfF6OyvYw7t8Fpf35RuzUyqnQsj8Vig== + binary-extensions@^2.0.0: version "2.2.0" resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-2.2.0.tgz#75f502eeaf9ffde42fc98829645be4ea76bd9e2d" @@ -1602,6 +1789,11 @@ bl@^4.1.0: inherits "^2.0.4" readable-stream "^3.4.0" +bluebird@^3.7.2: + version "3.7.2" + resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-3.7.2.tgz#9f229c15be272454ffa973ace0dbee79a1b0c36f" + integrity sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg== + body-parser@1.20.0: version "1.20.0" resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-1.20.0.tgz#3de69bd89011c11573d7bfee6a64f11b6bd27cc5" @@ -1628,6 +1820,13 @@ brace-expansion@^1.1.7: balanced-match "^1.0.0" concat-map "0.0.1" +brace-expansion@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-2.0.1.tgz#1edc459e0f0c548486ecf9fc99f2221364b9a0ae" + integrity sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA== + dependencies: + balanced-match "^1.0.0" + braces@^3.0.2, braces@~3.0.2: version "3.0.2" resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.2.tgz#3454e1a462ee8d599e236df336cd9ea4f8afe107" @@ -1664,6 +1863,11 @@ bser@2.1.1: dependencies: node-int64 "^0.4.0" +buffer-equal-constant-time@1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz#f8e71132f7ffe6e01a5c9697a4c6f3e48d5cc819" + integrity sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA== + buffer-from@^1.0.0: version "1.1.2" resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.1.2.tgz#2b146a6fd72e80b4f55d255f35ed59a3a9a41bd5" @@ -1717,6 +1921,13 @@ caniuse-lite@^1.0.30001449: resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001486.tgz#56a08885228edf62cbe1ac8980f2b5dae159997e" integrity sha512-uv7/gXuHi10Whlj0pp5q/tsK/32J2QSqVRKQhs2j8VsDCjgyruAh/eEXHF822VqO9yT6iZKw3nRwZRSPBE9OQg== +catharsis@^0.9.0: + version "0.9.0" + resolved "https://registry.yarnpkg.com/catharsis/-/catharsis-0.9.0.tgz#40382a168be0e6da308c277d3a2b3eb40c7d2121" + integrity sha512-prMTQVpcns/tzFgFVkVp6ak6RykZyWb3gu8ckUpd6YkTlacOd3DXGJjIpD4Q6zJirizvaiAjSSHlOsA+6sNh2A== + dependencies: + lodash "^4.17.15" + chalk@3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/chalk/-/chalk-3.0.0.tgz#3f73c2bf526591f574cc492c51e2456349f844e4" @@ -1831,6 +2042,15 @@ cliui@^7.0.2: strip-ansi "^6.0.0" wrap-ansi "^7.0.0" +cliui@^8.0.1: + version "8.0.1" + resolved "https://registry.yarnpkg.com/cliui/-/cliui-8.0.1.tgz#0c04b075db02cbfe60dc8e6cf2f5486b1a3608aa" + integrity sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ== + dependencies: + string-width "^4.2.0" + strip-ansi "^6.0.1" + wrap-ansi "^7.0.0" + clone@^1.0.2: version "1.0.4" resolved "https://registry.yarnpkg.com/clone/-/clone-1.0.4.tgz#da309cc263df15994c688ca902179ca3c7cd7c7e" @@ -2108,6 +2328,23 @@ domexception@^2.0.1: dependencies: webidl-conversions "^5.0.0" +duplexify@^4.0.0, duplexify@^4.1.1: + version "4.1.2" + resolved "https://registry.yarnpkg.com/duplexify/-/duplexify-4.1.2.tgz#18b4f8d28289132fa0b9573c898d9f903f81c7b0" + integrity sha512-fz3OjcNCHmRP12MJoZMPglx8m4rrFP8rovnk4vT8Fs+aonZoCwGg10dSsQsfP/E62eZcPTMSMP6686fu9Qlqtw== + dependencies: + end-of-stream "^1.4.1" + inherits "^2.0.3" + readable-stream "^3.1.1" + stream-shift "^1.0.0" + +ecdsa-sig-formatter@1.0.11, ecdsa-sig-formatter@^1.0.11: + version "1.0.11" + resolved "https://registry.yarnpkg.com/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz#ae0f0fa2d85045ef14a817daa3ce9acd0489e5bf" + integrity sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ== + dependencies: + safe-buffer "^5.0.1" + ee-first@1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/ee-first/-/ee-first-1.1.1.tgz#590c61156b0ae2f4f0255732a158b266bc56b21d" @@ -2133,7 +2370,7 @@ encodeurl@~1.0.2: resolved "https://registry.yarnpkg.com/encodeurl/-/encodeurl-1.0.2.tgz#ad3ff4c86ec2d029322f5a02c3a9a606c95b3f59" integrity sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w== -end-of-stream@^1.1.0: +end-of-stream@^1.1.0, end-of-stream@^1.4.1: version "1.4.4" resolved "https://registry.yarnpkg.com/end-of-stream/-/end-of-stream-1.4.4.tgz#5ae64a5f45057baf3626ec14da0ca5e4b2431eb0" integrity sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q== @@ -2156,6 +2393,16 @@ enhanced-resolve@^5.9.3: graceful-fs "^4.2.4" tapable "^2.2.0" +ent@^2.2.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/ent/-/ent-2.2.0.tgz#e964219325a21d05f44466a2f686ed6ce5f5dd1d" + integrity sha512-GHrMyVZQWvTIdDtpiEXdHZnFQKzeO09apj8Cbl4pKWy4i0Oprcq17usfDt5aO63swf0JOeMWjWQE/LzgSRuWpA== + +entities@~2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/entities/-/entities-2.1.0.tgz#992d3129cf7df6870b96c57858c249a120f8b8b5" + integrity sha512-hCx1oky9PFrJ611mf0ifBLBRW8lUUVRlFolb5gWRfIELabBlbp9xZvrqZLZAs+NxFnbfQoeGd8wDkygjg7U85w== + error-ex@^1.3.1: version "1.3.2" resolved "https://registry.yarnpkg.com/error-ex/-/error-ex-1.3.2.tgz#b4ac40648107fdcdcfae242f428bea8a14d4f1bf" @@ -2193,6 +2440,18 @@ escape-string-regexp@^4.0.0: resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz#14ba83a5d373e3d311e5afca29cf5bfad965bf34" integrity sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA== +escodegen@^1.13.0: + version "1.14.3" + resolved "https://registry.yarnpkg.com/escodegen/-/escodegen-1.14.3.tgz#4e7b81fba61581dc97582ed78cab7f0e8d63f503" + integrity sha512-qFcX0XJkdg+PB3xjZZG/wKSuT1PnQWx57+TVSjIMmILd2yC/6ByYElPwJnslDsuWuSAp4AwJGumarAAmJch5Kw== + dependencies: + esprima "^4.0.1" + estraverse "^4.2.0" + esutils "^2.0.2" + optionator "^0.8.1" + optionalDependencies: + source-map "~0.6.1" + escodegen@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/escodegen/-/escodegen-2.0.0.tgz#5e32b12833e8aa8fa35e1bf0befa89380484c7dd" @@ -2283,7 +2542,7 @@ eslint@^8.0.1: strip-json-comments "^3.1.0" text-table "^0.2.0" -espree@^9.5.2: +espree@^9.0.0, espree@^9.5.2: version "9.5.2" resolved "https://registry.yarnpkg.com/espree/-/espree-9.5.2.tgz#e994e7dc33a082a7a82dceaf12883a829353215b" integrity sha512-7OASN1Wma5fum5SrNhFMAMJxOUAbhyfQ8dQ//PJaJbNw0URTPWqIghHWt1MmAANKhHZIYOHruW4Kw4ruUWOdGw== @@ -2311,7 +2570,7 @@ esrecurse@^4.3.0: dependencies: estraverse "^5.2.0" -estraverse@^4.1.1: +estraverse@^4.1.1, estraverse@^4.2.0: version "4.3.0" resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-4.3.0.tgz#398ad3f3c5a24948be7725e83d11a7de28cdbd1d" integrity sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw== @@ -2331,6 +2590,11 @@ etag@~1.8.1: resolved "https://registry.yarnpkg.com/etag/-/etag-1.8.1.tgz#41ae2eeb65efa62268aebfea83ac7d79299b0887" integrity sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg== +event-target-shim@^5.0.0: + version "5.0.1" + resolved "https://registry.yarnpkg.com/event-target-shim/-/event-target-shim-5.0.1.tgz#5d4d3ebdf9583d63a5333ce2deb7480ab2b05789" + integrity sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ== + events@^3.2.0: version "3.3.0" resolved "https://registry.yarnpkg.com/events/-/events-3.3.0.tgz#31a95ad0a924e2d2c419a813aeb2c4e878ea7400" @@ -2418,6 +2682,11 @@ express@4.18.1: utils-merge "1.0.1" vary "~1.1.2" +extend@^3.0.2: + version "3.0.2" + resolved "https://registry.yarnpkg.com/extend/-/extend-3.0.2.tgz#f8b1136b4071fbd8eb140aff858b1019ec2915fa" + integrity sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g== + external-editor@^3.0.3: version "3.1.0" resolved "https://registry.yarnpkg.com/external-editor/-/external-editor-3.1.0.tgz#cb03f740befae03ea4d283caed2741a83f335495" @@ -2463,6 +2732,11 @@ fast-safe-stringify@2.1.1, fast-safe-stringify@^2.1.1: resolved "https://registry.yarnpkg.com/fast-safe-stringify/-/fast-safe-stringify-2.1.1.tgz#c406a83b6e70d9e35ce3b30a81141df30aeba884" integrity sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA== +fast-text-encoding@^1.0.0, fast-text-encoding@^1.0.3: + version "1.0.6" + resolved "https://registry.yarnpkg.com/fast-text-encoding/-/fast-text-encoding-1.0.6.tgz#0aa25f7f638222e3396d72bf936afcf1d42d6867" + integrity sha512-VhXlQgj9ioXCqGstD37E/HBeqEGV/qOD/kmbVG8h5xKBYvM1L3lR1Zn4555cQ8GkYbJa8aJSipLPndE1k6zK2w== + fastq@^1.6.0: version "1.15.0" resolved "https://registry.yarnpkg.com/fastq/-/fastq-1.15.0.tgz#d04d07c6a2a68fe4599fea8d2e103a937fae6b3a" @@ -2629,6 +2903,24 @@ function-bind@^1.1.1: resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.1.tgz#a56899d3ea3c9bab874bb9773b7c5ede92f4895d" integrity sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A== +gaxios@^5.0.0, gaxios@^5.0.1: + version "5.1.0" + resolved "https://registry.yarnpkg.com/gaxios/-/gaxios-5.1.0.tgz#133b77b45532be71eec72012b7e97c2320b6140a" + integrity sha512-aezGIjb+/VfsJtIcHGcBSerNEDdfdHeMros+RbYbGpmonKWQCOVOes0LVZhn1lDtIgq55qq0HaxymIoae3Fl/A== + dependencies: + extend "^3.0.2" + https-proxy-agent "^5.0.0" + is-stream "^2.0.0" + node-fetch "^2.6.7" + +gcp-metadata@^5.2.0: + version "5.2.0" + resolved "https://registry.yarnpkg.com/gcp-metadata/-/gcp-metadata-5.2.0.tgz#b4772e9c5976241f5d3e69c4f446c906d25506ec" + integrity sha512-aFhhvvNycky2QyhG+dcfEdHBF0FRbYcf39s6WNHUDysKSrbJ5vuFbjydxBcmewtXeV248GP8dWT3ByPNxsyHCw== + dependencies: + gaxios "^5.0.0" + json-bigint "^1.0.0" + gensync@^1.0.0-beta.2: version "1.0.0-beta.2" resolved "https://registry.yarnpkg.com/gensync/-/gensync-1.0.0-beta.2.tgz#32a6ee76c3d7f52d46b2b1ae5d93fea8580a25e0" @@ -2696,6 +2988,17 @@ glob@^7.0.0, glob@^7.1.1, glob@^7.1.2, glob@^7.1.3, glob@^7.1.4: once "^1.3.0" path-is-absolute "^1.0.0" +glob@^8.0.0: + version "8.1.0" + resolved "https://registry.yarnpkg.com/glob/-/glob-8.1.0.tgz#d388f656593ef708ee3e34640fdfb99a9fd1c33e" + integrity sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ== + dependencies: + fs.realpath "^1.0.0" + inflight "^1.0.4" + inherits "2" + minimatch "^5.0.1" + once "^1.3.0" + globals@^11.1.0: version "11.12.0" resolved "https://registry.yarnpkg.com/globals/-/globals-11.12.0.tgz#ab8795338868a0babd8525758018c2a7eb95c42e" @@ -2720,7 +3023,50 @@ globby@^11.1.0: merge2 "^1.4.1" slash "^3.0.0" -graceful-fs@^4.1.2, graceful-fs@^4.1.6, graceful-fs@^4.2.0, graceful-fs@^4.2.4, graceful-fs@^4.2.9: +google-auth-library@^8.0.2: + version "8.8.0" + resolved "https://registry.yarnpkg.com/google-auth-library/-/google-auth-library-8.8.0.tgz#2e17494431cef56b571420d483a4debff6c481cd" + integrity sha512-0iJn7IDqObDG5Tu9Tn2WemmJ31ksEa96IyK0J0OZCpTh6CrC6FrattwKX87h3qKVuprCJpdOGKc1Xi8V0kMh8Q== + dependencies: + arrify "^2.0.0" + base64-js "^1.3.0" + ecdsa-sig-formatter "^1.0.11" + fast-text-encoding "^1.0.0" + gaxios "^5.0.0" + gcp-metadata "^5.2.0" + gtoken "^6.1.0" + jws "^4.0.0" + lru-cache "^6.0.0" + +google-gax@^3.5.8: + version "3.6.0" + resolved "https://registry.yarnpkg.com/google-gax/-/google-gax-3.6.0.tgz#0f4ae350159737fe0aa289815c0d92838b19f6af" + integrity sha512-2fyb61vWxUonHiArRNJQmE4tx5oY1ni8VPo08fzII409vDSCWG7apDX4qNOQ2GXXT82gLBn3d3P1Dydh7pWjyw== + dependencies: + "@grpc/grpc-js" "~1.8.0" + "@grpc/proto-loader" "^0.7.0" + "@types/long" "^4.0.0" + "@types/rimraf" "^3.0.2" + abort-controller "^3.0.0" + duplexify "^4.0.0" + fast-text-encoding "^1.0.3" + google-auth-library "^8.0.2" + is-stream-ended "^0.1.4" + node-fetch "^2.6.1" + object-hash "^3.0.0" + proto3-json-serializer "^1.0.0" + protobufjs "7.2.3" + protobufjs-cli "1.1.1" + retry-request "^5.0.0" + +google-p12-pem@^4.0.0: + version "4.0.1" + resolved "https://registry.yarnpkg.com/google-p12-pem/-/google-p12-pem-4.0.1.tgz#82841798253c65b7dc2a4e5fe9df141db670172a" + integrity sha512-WPkN4yGtz05WZ5EhtlxNDWPhC4JIic6G8ePitwUWy4l+XPVYec+a0j0Ts47PDtW59y3RwAhUd9/h9ZZ63px6RQ== + dependencies: + node-forge "^1.3.1" + +graceful-fs@^4.1.2, graceful-fs@^4.1.6, graceful-fs@^4.1.9, graceful-fs@^4.2.0, graceful-fs@^4.2.4, graceful-fs@^4.2.9: version "4.2.11" resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.11.tgz#4183e4e8bf08bb6e05bbb2f7d2e0c8f712ca40e3" integrity sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ== @@ -2735,6 +3081,15 @@ graphemer@^1.4.0: resolved "https://registry.yarnpkg.com/graphemer/-/graphemer-1.4.0.tgz#fb2f1d55e0e3a1849aeffc90c4fa0dd53a0e66c6" integrity sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag== +gtoken@^6.1.0: + version "6.1.2" + resolved "https://registry.yarnpkg.com/gtoken/-/gtoken-6.1.2.tgz#aeb7bdb019ff4c3ba3ac100bbe7b6e74dce0e8bc" + integrity sha512-4ccGpzz7YAr7lxrT2neugmXQ3hP9ho2gcaityLVkiUecAiwiy60Ii8gRbZeOsXV19fYaRjgBSshs8kXw+NKCPQ== + dependencies: + gaxios "^5.0.1" + google-p12-pem "^4.0.0" + jws "^4.0.0" + has-flag@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-3.0.0.tgz#b5d454dc2199ae225699f3467e5a07f3b955bafd" @@ -2774,6 +3129,11 @@ html-escaper@^2.0.0: resolved "https://registry.yarnpkg.com/html-escaper/-/html-escaper-2.0.2.tgz#dfd60027da36a36dfcbe236262c00a5822681453" integrity sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg== +html-tags@^3.0.0: + version "3.3.1" + resolved "https://registry.yarnpkg.com/html-tags/-/html-tags-3.3.1.tgz#a04026a18c882e4bba8a01a3d39cfe465d40b5ce" + integrity sha512-ztqyC3kLto0e9WbNp0aeP+M3kTt+nbaIveGmUxAtZa+8iFgKLUOD4YKM5j+f3QD89bra7UeumolZHKuOXnTmeQ== + http-errors@2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-2.0.0.tgz#b7774a1486ef73cf7667ac9ae0858c012c57b9d3" @@ -2794,6 +3154,15 @@ http-proxy-agent@^4.0.1: agent-base "6" debug "4" +http-proxy-agent@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/http-proxy-agent/-/http-proxy-agent-5.0.0.tgz#5129800203520d434f142bc78ff3c170800f2b43" + integrity sha512-n2hY8YdoRE1i7r6M0w9DIw5GgZN0G25P8zLCRQ8rjXtTU3vsNFBI/vWK/UIeE6g5MUUz6avwAPXmL6Fy9D/90w== + dependencies: + "@tootallnate/once" "2" + agent-base "6" + debug "4" + https-proxy-agent@^5.0.0: version "5.0.1" resolved "https://registry.yarnpkg.com/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz#c59ef224a04fe8b754f3db0063a25ea30d0005d6" @@ -2953,6 +3322,13 @@ is-glob@^4.0.0, is-glob@^4.0.1, is-glob@^4.0.3, is-glob@~4.0.1: dependencies: is-extglob "^2.1.1" +is-html@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/is-html/-/is-html-2.0.0.tgz#b3ab2e27ccb7a12235448f51f115a6690f435fc8" + integrity sha512-S+OpgB5i7wzIue/YSE5hg0e5ZYfG3hhpNh9KGl6ayJ38p7ED6wxQLd1TV91xHpcTvw90KMJ9EwN3F/iNflHBVg== + dependencies: + html-tags "^3.0.0" + is-interactive@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/is-interactive/-/is-interactive-1.0.0.tgz#cea6e6ae5c870a7b0a0004070b7b587e0252912e" @@ -2973,6 +3349,11 @@ is-potential-custom-element-name@^1.0.1: resolved "https://registry.yarnpkg.com/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz#171ed6f19e3ac554394edf78caa05784a45bebb5" integrity sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ== +is-stream-ended@^0.1.4: + version "0.1.4" + resolved "https://registry.yarnpkg.com/is-stream-ended/-/is-stream-ended-0.1.4.tgz#f50224e95e06bce0e356d440a4827cd35b267eda" + integrity sha512-xj0XPvmr7bQFTvirqnFr50o0hQIh6ZItDqloxt5aJrR4NQsYeSsyFQERYGCAzfindAcnKjINnwEEgLx4IqVzQw== + is-stream@^2.0.0: version "2.0.1" resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-2.0.1.tgz#fac1e3d53b97ad5a9d0ae9cef2389f5810a5c077" @@ -3470,6 +3851,34 @@ js-yaml@^3.13.1: argparse "^1.0.7" esprima "^4.0.0" +js2xmlparser@^4.0.2: + version "4.0.2" + resolved "https://registry.yarnpkg.com/js2xmlparser/-/js2xmlparser-4.0.2.tgz#2a1fdf01e90585ef2ae872a01bc169c6a8d5e60a" + integrity sha512-6n4D8gLlLf1n5mNLQPRfViYzu9RATblzPEtm1SthMX1Pjao0r9YI9nw7ZIfRxQMERS87mcswrg+r/OYrPRX6jA== + dependencies: + xmlcreate "^2.0.4" + +jsdoc@^4.0.0: + version "4.0.2" + resolved "https://registry.yarnpkg.com/jsdoc/-/jsdoc-4.0.2.tgz#a1273beba964cf433ddf7a70c23fd02c3c60296e" + integrity sha512-e8cIg2z62InH7azBBi3EsSEqrKx+nUtAS5bBcYTSpZFA+vhNPyhv8PTFZ0WsjOPDj04/dOLlm08EDcQJDqaGQg== + dependencies: + "@babel/parser" "^7.20.15" + "@jsdoc/salty" "^0.2.1" + "@types/markdown-it" "^12.2.3" + bluebird "^3.7.2" + catharsis "^0.9.0" + escape-string-regexp "^2.0.0" + js2xmlparser "^4.0.2" + klaw "^3.0.0" + markdown-it "^12.3.2" + markdown-it-anchor "^8.4.1" + marked "^4.0.10" + mkdirp "^1.0.4" + requizzle "^0.2.3" + strip-json-comments "^3.1.0" + underscore "~1.13.2" + jsdom@^16.6.0: version "16.7.0" resolved "https://registry.yarnpkg.com/jsdom/-/jsdom-16.7.0.tgz#918ae71965424b197c819f8183a754e18977b710" @@ -3508,6 +3917,13 @@ jsesc@^2.5.1: resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-2.5.2.tgz#80564d2e483dacf6e8ef209650a67df3f0c283a4" integrity sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA== +json-bigint@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/json-bigint/-/json-bigint-1.0.0.tgz#ae547823ac0cad8398667f8cd9ef4730f5b01ff1" + integrity sha512-SiPv/8VpZuWbvLSMtTDU8hEfrZWg/mH/nV/b4o0CYbSxu1UIQPLdwKOCIyLQX+VIPO5vrLX3i8qtqFyhdPSUSQ== + dependencies: + bignumber.js "^9.0.0" + json-parse-even-better-errors@^2.3.0, json-parse-even-better-errors@^2.3.1: version "2.3.1" resolved "https://registry.yarnpkg.com/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz#7c47805a94319928e05777405dc12e1f7a4ee02d" @@ -3554,6 +3970,30 @@ jsonfile@^6.0.1: optionalDependencies: graceful-fs "^4.1.6" +jwa@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/jwa/-/jwa-2.0.0.tgz#a7e9c3f29dae94027ebcaf49975c9345593410fc" + integrity sha512-jrZ2Qx916EA+fq9cEAeCROWPTfCwi1IVHqT2tapuqLEVVDKFDENFw1oL+MwrTvH6msKxsd1YTDVw6uKEcsrLEA== + dependencies: + buffer-equal-constant-time "1.0.1" + ecdsa-sig-formatter "1.0.11" + safe-buffer "^5.0.1" + +jws@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/jws/-/jws-4.0.0.tgz#2d4e8cf6a318ffaa12615e9dec7e86e6c97310f4" + integrity sha512-KDncfTmOZoOMTFG4mBlG0qUIOlc03fmzH+ru6RgYVZhPkyiy/92Owlt/8UEN+a4TXR1FQetfIpJE8ApdvdVxTg== + dependencies: + jwa "^2.0.0" + safe-buffer "^5.0.1" + +klaw@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/klaw/-/klaw-3.0.0.tgz#b11bec9cf2492f06756d6e809ab73a2910259146" + integrity sha512-0Fo5oir+O9jnXu5EefYbVK+mHMBeEVEy2cmctR1O1NECcCkPRreJKrS6Qt/j3KC2C148Dfo9i3pCmCMsdqGr0g== + dependencies: + graceful-fs "^4.1.9" + kleur@^3.0.3: version "3.0.3" resolved "https://registry.yarnpkg.com/kleur/-/kleur-3.0.3.tgz#a79c9ecc86ee1ce3fa6206d1216c501f147fc07e" @@ -3590,6 +4030,13 @@ lines-and-columns@^1.1.6: resolved "https://registry.yarnpkg.com/lines-and-columns/-/lines-and-columns-1.2.4.tgz#eca284f75d2965079309dc0ad9255abb2ebc1632" integrity sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg== +linkify-it@^3.0.1: + version "3.0.3" + resolved "https://registry.yarnpkg.com/linkify-it/-/linkify-it-3.0.3.tgz#a98baf44ce45a550efb4d49c769d07524cc2fa2e" + integrity sha512-ynTsyrFSdE5oZ/O9GEf00kPngmOfVwazR5GKDq6EYfhlpFug3J2zybX56a2PRRpc9P+FuSoGNAwjlbDs9jJBPQ== + dependencies: + uc.micro "^1.0.1" + loader-runner@^4.2.0: version "4.3.0" resolved "https://registry.yarnpkg.com/loader-runner/-/loader-runner-4.3.0.tgz#c1b4a163b99f614830353b16755e7149ac2314e1" @@ -3609,6 +4056,11 @@ locate-path@^6.0.0: dependencies: p-locate "^5.0.0" +lodash.camelcase@^4.3.0: + version "4.3.0" + resolved "https://registry.yarnpkg.com/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz#b28aa6288a2b9fc651035c7711f65ab6190331a6" + integrity sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA== + lodash.memoize@4.x: version "4.1.2" resolved "https://registry.yarnpkg.com/lodash.memoize/-/lodash.memoize-4.1.2.tgz#bcc6c49a42a2840ed997f323eada5ecd182e0bfe" @@ -3619,7 +4071,7 @@ lodash.merge@^4.6.2: resolved "https://registry.yarnpkg.com/lodash.merge/-/lodash.merge-4.6.2.tgz#558aa53b43b661e1925a0afdfa36a9a1085fe57a" integrity sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ== -lodash@4.17.21, lodash@^4.17.19, lodash@^4.17.21, lodash@^4.7.0: +lodash@4.17.21, lodash@^4.17.15, lodash@^4.17.19, lodash@^4.17.21, lodash@^4.7.0: version "4.17.21" resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c" integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg== @@ -3632,6 +4084,16 @@ log-symbols@^4.1.0: chalk "^4.1.0" is-unicode-supported "^0.1.0" +long@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/long/-/long-4.0.0.tgz#9a7b71cfb7d361a194ea555241c92f7468d5bf28" + integrity sha512-XsP+KhQif4bjX1kbuSiySJFNAehNxgLb6hPRGJ9QsUr8ajHkuXGdrHmFUTUUXhDwVX2R5bY4JNZEwbUiMhV+MA== + +long@^5.0.0: + version "5.2.3" + resolved "https://registry.yarnpkg.com/long/-/long-5.2.3.tgz#a3ba97f3877cf1d778eccbcb048525ebb77499e1" + integrity sha512-lcHwpNoggQTObv5apGNCTdJrO69eHOZMi4BNC+rTLER8iHAqGrUVeLh/irVIM7zTw2bOXA8T6uNPeujwOLg/2Q== + lru-cache@^5.1.1: version "5.1.1" resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-5.1.1.tgz#1da27e6710271947695daf6848e847f01d84b920" @@ -3677,6 +4139,32 @@ makeerror@1.0.12: dependencies: tmpl "1.0.5" +markdown-it-anchor@^8.4.1: + version "8.6.7" + resolved "https://registry.yarnpkg.com/markdown-it-anchor/-/markdown-it-anchor-8.6.7.tgz#ee6926daf3ad1ed5e4e3968b1740eef1c6399634" + integrity sha512-FlCHFwNnutLgVTflOYHPW2pPcl2AACqVzExlkGQNsi4CJgqOHN7YTgDd4LuhgN1BFO3TS0vLAruV1Td6dwWPJA== + +markdown-it@^12.3.2: + version "12.3.2" + resolved "https://registry.yarnpkg.com/markdown-it/-/markdown-it-12.3.2.tgz#bf92ac92283fe983fe4de8ff8abfb5ad72cd0c90" + integrity sha512-TchMembfxfNVpHkbtriWltGWc+m3xszaRD0CZup7GFFhzIgQqxIfn3eGj1yZpfuflzPvfkt611B2Q/Bsk1YnGg== + dependencies: + argparse "^2.0.1" + entities "~2.1.0" + linkify-it "^3.0.1" + mdurl "^1.0.1" + uc.micro "^1.0.5" + +marked@^4.0.10: + version "4.3.0" + resolved "https://registry.yarnpkg.com/marked/-/marked-4.3.0.tgz#796362821b019f734054582038b116481b456cf3" + integrity sha512-PRsaiG84bK+AMvxziE/lCFss8juXjNaWzVbN5tXAm4XjeaS9NAHhop+PjQxz2A9h8Q4M/xGmzP8vqNwy6JeK0A== + +mdurl@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/mdurl/-/mdurl-1.0.1.tgz#fe85b2ec75a59037f2adfec100fd6c601761152e" + integrity sha512-/sKlQJCBYVY9Ers9hqzKou4H6V5UWc/M59TH2dvkt+84itfnq7uFOMLpOiOS4ujvHP4etln18fmIxA5R5fll0g== + media-typer@0.3.0: version "0.3.0" resolved "https://registry.yarnpkg.com/media-typer/-/media-typer-0.3.0.tgz#8710d7af0aa626f8fffa1ce00168545263255748" @@ -3751,6 +4239,13 @@ minimatch@^3.0.4, minimatch@^3.0.5, minimatch@^3.1.1, minimatch@^3.1.2: dependencies: brace-expansion "^1.1.7" +minimatch@^5.0.1: + version "5.1.6" + resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-5.1.6.tgz#1cfcb8cf5522ea69952cd2af95ae09477f122a96" + integrity sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g== + dependencies: + brace-expansion "^2.0.1" + minimist@1.2.6: version "1.2.6" resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.6.tgz#8637a5b759ea0d6e98702cfb3a9283323c93af44" @@ -3768,6 +4263,11 @@ mkdirp@^0.5.4: dependencies: minimist "^1.2.6" +mkdirp@^1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-1.0.4.tgz#3eb5ed62622756d79a5f0e2a221dfebad75c2f7e" + integrity sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw== + ms@2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8" @@ -3846,6 +4346,18 @@ node-fetch@^2.6.1: dependencies: whatwg-url "^5.0.0" +node-fetch@^2.6.7: + version "2.6.11" + resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.11.tgz#cde7fc71deef3131ef80a738919f999e6edfff25" + integrity sha512-4I6pdBY1EthSqDmJkiNk3JIT8cswwR9nfeW/cPdUagJYEQG7R95WRH74wpz7ma8Gh/9dI9FP+OU+0E4FvtA55w== + dependencies: + whatwg-url "^5.0.0" + +node-forge@^1.3.1: + version "1.3.1" + resolved "https://registry.yarnpkg.com/node-forge/-/node-forge-1.3.1.tgz#be8da2af243b2417d5f646a770663a92b7e9ded3" + integrity sha512-dPEtOeMvF9VMcYV/1Wb8CPoVAXtp6MKMlcbAt4ddqmGqUJ6fQZFXkNZNkNlfevtNkGtaSoXf/vNNNSvgrdXwtA== + node-int64@^0.4.0: version "0.4.0" resolved "https://registry.yarnpkg.com/node-int64/-/node-int64-0.4.0.tgz#87a9065cdb355d3182d8f94ce11188b825c68a3b" @@ -3878,7 +4390,7 @@ object-assign@^4, object-assign@^4.1.1: resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863" integrity sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg== -object-hash@3.0.0: +object-hash@3.0.0, object-hash@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/object-hash/-/object-hash-3.0.0.tgz#73f97f753e7baffc0e2cc9d6e079079744ac82e9" integrity sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw== @@ -4144,6 +4656,47 @@ prompts@^2.0.1: kleur "^3.0.3" sisteransi "^1.0.5" +proto3-json-serializer@^1.0.0: + version "1.1.1" + resolved "https://registry.yarnpkg.com/proto3-json-serializer/-/proto3-json-serializer-1.1.1.tgz#1b5703152b6ce811c5cdcc6468032caf53521331" + integrity sha512-AwAuY4g9nxx0u52DnSMkqqgyLHaW/XaPLtaAo3y/ZCfeaQB/g4YDH4kb8Wc/mWzWvu0YjOznVnfn373MVZZrgw== + dependencies: + protobufjs "^7.0.0" + +protobufjs-cli@1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/protobufjs-cli/-/protobufjs-cli-1.1.1.tgz#f531201b1c8c7772066aa822bf9a08318b24a704" + integrity sha512-VPWMgIcRNyQwWUv8OLPyGQ/0lQY/QTQAVN5fh+XzfDwsVw1FZ2L3DM/bcBf8WPiRz2tNpaov9lPZfNcmNo6LXA== + dependencies: + chalk "^4.0.0" + escodegen "^1.13.0" + espree "^9.0.0" + estraverse "^5.1.0" + glob "^8.0.0" + jsdoc "^4.0.0" + minimist "^1.2.0" + semver "^7.1.2" + tmp "^0.2.1" + uglify-js "^3.7.7" + +protobufjs@7.2.3, protobufjs@^7.0.0: + version "7.2.3" + resolved "https://registry.yarnpkg.com/protobufjs/-/protobufjs-7.2.3.tgz#01af019e40d9c6133c49acbb3ff9e30f4f0f70b2" + integrity sha512-TtpvOqwB5Gdz/PQmOjgsrGH1nHjAQVCN7JG4A6r1sXRWESL5rNMAiRcBQlCAdKxZcAbstExQePYG8xof/JVRgg== + dependencies: + "@protobufjs/aspromise" "^1.1.2" + "@protobufjs/base64" "^1.1.2" + "@protobufjs/codegen" "^2.0.4" + "@protobufjs/eventemitter" "^1.1.0" + "@protobufjs/fetch" "^1.1.0" + "@protobufjs/float" "^1.0.2" + "@protobufjs/inquire" "^1.1.0" + "@protobufjs/path" "^1.1.2" + "@protobufjs/pool" "^1.1.0" + "@protobufjs/utf8" "^1.1.0" + "@types/node" ">=13.7.0" + long "^5.0.0" + proxy-addr@~2.0.7: version "2.0.7" resolved "https://registry.yarnpkg.com/proxy-addr/-/proxy-addr-2.0.7.tgz#f19fe69ceab311eeb94b42e70e8c2070f9ba1025" @@ -4239,7 +4792,7 @@ readable-stream@^2.2.2: string_decoder "~1.1.1" util-deprecate "~1.0.1" -readable-stream@^3.4.0: +readable-stream@^3.1.1, readable-stream@^3.4.0: version "3.6.2" resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-3.6.2.tgz#56a9b36ea965c00c5a93ef31eb111a0f11056967" integrity sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA== @@ -4282,6 +4835,13 @@ requires-port@^1.0.0: resolved "https://registry.yarnpkg.com/requires-port/-/requires-port-1.0.0.tgz#925d2601d39ac485e091cf0da5c6e694dc3dcaff" integrity sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ== +requizzle@^0.2.3: + version "0.2.4" + resolved "https://registry.yarnpkg.com/requizzle/-/requizzle-0.2.4.tgz#319eb658b28c370f0c20f968fa8ceab98c13d27c" + integrity sha512-JRrFk1D4OQ4SqovXOgdav+K8EAhSB/LJZqCz8tbX0KObcdeM15Ss59ozWMBWmmINMagCwmqn4ZNryUGpBsl6Jw== + dependencies: + lodash "^4.17.21" + resolve-cwd@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/resolve-cwd/-/resolve-cwd-3.0.0.tgz#0f0075f1bb2544766cf73ba6a6e2adfebcb13f2d" @@ -4321,6 +4881,14 @@ restore-cursor@^3.1.0: onetime "^5.1.0" signal-exit "^3.0.2" +retry-request@^5.0.0: + version "5.0.2" + resolved "https://registry.yarnpkg.com/retry-request/-/retry-request-5.0.2.tgz#143d85f90c755af407fcc46b7166a4ba520e44da" + integrity sha512-wfI3pk7EE80lCIXprqh7ym48IHYdwmAAzESdbU8Q9l7pnRCk9LEhpbOTNKjz6FARLm/Bl5m+4F0ABxOkYUujSQ== + dependencies: + debug "^4.1.1" + extend "^3.0.2" + reusify@^1.0.4: version "1.0.4" resolved "https://registry.yarnpkg.com/reusify/-/reusify-1.0.4.tgz#90da382b1e126efc02146e90845a88db12925d76" @@ -4359,7 +4927,7 @@ rxjs@^7.2.0: dependencies: tslib "^2.1.0" -safe-buffer@5.2.1, safe-buffer@^5.1.0, safe-buffer@~5.2.0: +safe-buffer@5.2.1, safe-buffer@^5.0.1, safe-buffer@^5.1.0, safe-buffer@~5.2.0: version "5.2.1" resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6" integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ== @@ -4402,7 +4970,7 @@ semver@^6.0.0, semver@^6.3.0: resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.0.tgz#ee0a64c8af5e8ceea67687b133761e1becbd1d3d" integrity sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw== -semver@^7.3.7, semver@^7.3.8: +semver@^7.1.2, semver@^7.3.7, semver@^7.3.8: version "7.5.1" resolved "https://registry.yarnpkg.com/semver/-/semver-7.5.1.tgz#c90c4d631cf74720e46b21c1d37ea07edfab91ec" integrity sha512-Wvss5ivl8TMRZXXESstBA4uR5iXgEN/VC5/sOcuXdVLzcdkz4HWetIoRfG5gb5X+ij/G9rw9YoGn3QoQ8OCSpw== @@ -4540,6 +5108,18 @@ statuses@2.0.1: resolved "https://registry.yarnpkg.com/statuses/-/statuses-2.0.1.tgz#55cb000ccf1d48728bd23c685a063998cf1a1b63" integrity sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ== +stream-events@^1.0.5: + version "1.0.5" + resolved "https://registry.yarnpkg.com/stream-events/-/stream-events-1.0.5.tgz#bbc898ec4df33a4902d892333d47da9bf1c406d5" + integrity sha512-E1GUzBSgvct8Jsb3v2X15pjzN1tYebtbLaMg+eBOUOAxgbLoSbT2NS91ckc5lJD1KfLjId+jXJRgo0qnV5Nerg== + dependencies: + stubs "^3.0.0" + +stream-shift@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/stream-shift/-/stream-shift-1.0.1.tgz#d7088281559ab2778424279b0877da3c392d5a3d" + integrity sha512-AiisoFqQ0vbGcZgQPY1cdP2I76glaVA/RauYR4G4thNFgkTqr90yXTo4LYX60Jl+sIlPNHHdGSwo01AvbKUSVQ== + streamsearch@^1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/streamsearch/-/streamsearch-1.1.0.tgz#404dd1e2247ca94af554e841a8ef0eaa238da764" @@ -4553,7 +5133,7 @@ string-length@^4.0.1: char-regex "^1.0.2" strip-ansi "^6.0.0" -string-width@^4.1.0, string-width@^4.2.0: +string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3: version "4.2.3" resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== @@ -4608,6 +5188,11 @@ structured-log@^0.2.0: resolved "https://registry.yarnpkg.com/structured-log/-/structured-log-0.2.0.tgz#b9be1794c39d6399f265666b84635a3307611c5b" integrity sha512-W3Tps8PN5Mon37955/wuZZSXwBXiB52AUnd/oPVdmo+O+mLkr2fNajV6821gJ8irrgVQx3gYOqSWPMgj7Dy3Yg== +stubs@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/stubs/-/stubs-3.0.0.tgz#e8d2ba1fa9c90570303c030b6900f7d5f89abe5b" + integrity sha512-PdHt7hHUJKxvTCgbKX9C1V/ftOcjJQgz8BZwNfV5c4B6dcGqlpelTbJ999jBGZ2jYiPAwcX5dP6oBwVlBlUbxw== + superagent@^8.0.5: version "8.0.9" resolved "https://registry.yarnpkg.com/superagent/-/superagent-8.0.9.tgz#2c6fda6fadb40516515f93e9098c0eb1602e0535" @@ -4698,6 +5283,17 @@ tapable@^2.1.1, tapable@^2.2.0, tapable@^2.2.1: resolved "https://registry.yarnpkg.com/tapable/-/tapable-2.2.1.tgz#1967a73ef4060a82f12ab96af86d52fdb76eeca0" integrity sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ== +teeny-request@^8.0.0: + version "8.0.3" + resolved "https://registry.yarnpkg.com/teeny-request/-/teeny-request-8.0.3.tgz#5cb9c471ef5e59f2fca8280dc3c5909595e6ca24" + integrity sha512-jJZpA5He2y52yUhA7pyAGZlgQpcB+xLjcN0eUFxr9c8hP/H7uOXbBNVo/O0C/xVfJLJs680jvkFgVJEEvk9+ww== + dependencies: + http-proxy-agent "^5.0.0" + https-proxy-agent "^5.0.0" + node-fetch "^2.6.1" + stream-events "^1.0.5" + uuid "^9.0.0" + terminal-link@^2.0.0: version "2.1.1" resolved "https://registry.yarnpkg.com/terminal-link/-/terminal-link-2.1.1.tgz#14a64a27ab3c0df933ea546fba55f2d078edc994" @@ -4758,6 +5354,13 @@ tmp@^0.0.33: dependencies: os-tmpdir "~1.0.2" +tmp@^0.2.1: + version "0.2.1" + resolved "https://registry.yarnpkg.com/tmp/-/tmp-0.2.1.tgz#8457fc3037dcf4719c251367a1af6500ee1ccf14" + integrity sha512-76SUhtfqR2Ijn+xllcI5P1oyannHNHByD80W1q447gU3mp9G9PSpGdWmjUOHRDPiHYacIk66W7ubDTuPF3BEtQ== + dependencies: + rimraf "^3.0.0" + tmpl@1.0.5: version "1.0.5" resolved "https://registry.yarnpkg.com/tmpl/-/tmpl-1.0.5.tgz#8683e0b902bb9c20c4f726e3c0b69f36518c07cc" @@ -4960,6 +5563,21 @@ typescript@^4.3.5: resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.9.5.tgz#095979f9bcc0d09da324d58d03ce8f8374cbe65a" integrity sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g== +uc.micro@^1.0.1, uc.micro@^1.0.5: + version "1.0.6" + resolved "https://registry.yarnpkg.com/uc.micro/-/uc.micro-1.0.6.tgz#9c411a802a409a91fc6cf74081baba34b24499ac" + integrity sha512-8Y75pvTYkLJW2hWQHXxoqRgV7qb9B+9vFEtidML+7koHUFapnVJAZ6cKs+Qjz5Aw3aZWHMC6u0wJE3At+nSGwA== + +uglify-js@^3.7.7: + version "3.17.4" + resolved "https://registry.yarnpkg.com/uglify-js/-/uglify-js-3.17.4.tgz#61678cf5fa3f5b7eb789bb345df29afb8257c22c" + integrity sha512-T9q82TJI9e/C1TAxYvfb16xO120tMVFZrGA3f9/P4424DNu6ypK103y0GPFVa17yotwSyZW5iYXgjYHkGrJW/g== + +underscore@~1.13.2: + version "1.13.6" + resolved "https://registry.yarnpkg.com/underscore/-/underscore-1.13.6.tgz#04786a1f589dc6c09f761fc5f45b89e935136441" + integrity sha512-+A5Sja4HP1M08MaXya7p5LvjuM7K6q/2EaC0+iovj/wOcMsTzMvDFbasi/oSapiwOlt252IqsKqPjCl7huKS0A== + universalify@^0.2.0: version "0.2.0" resolved "https://registry.yarnpkg.com/universalify/-/universalify-0.2.0.tgz#6451760566fa857534745ab1dde952d1b1761be0" @@ -5013,6 +5631,11 @@ uuid@8.3.2: resolved "https://registry.yarnpkg.com/uuid/-/uuid-8.3.2.tgz#80d5b5ced271bb9af6c445f21a1a04c606cefbe2" integrity sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg== +uuid@^9.0.0: + version "9.0.0" + resolved "https://registry.yarnpkg.com/uuid/-/uuid-9.0.0.tgz#592f550650024a38ceb0c562f2f6aa435761efb5" + integrity sha512-MXcSTerfPa4uqyzStbRoTgt5XIe3x5+42+q1sDuy3R5MDk66URdLMOZe5aPX/SQd+kuYAh0FdP/pO28IkQyTeg== + v8-compile-cache-lib@^3.0.1: version "3.0.1" resolved "https://registry.yarnpkg.com/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz#6336e8d71965cb3d35a1bbb7868445a7c05264bf" @@ -5215,6 +5838,11 @@ xmlchars@^2.2.0: resolved "https://registry.yarnpkg.com/xmlchars/-/xmlchars-2.2.0.tgz#060fe1bcb7f9c76fe2a17db86a9bc3ab894210cb" integrity sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw== +xmlcreate@^2.0.4: + version "2.0.4" + resolved "https://registry.yarnpkg.com/xmlcreate/-/xmlcreate-2.0.4.tgz#0c5ab0f99cdd02a81065fa9cd8f8ae87624889be" + integrity sha512-nquOebG4sngPmGPICTS5EnxqhKbCmz5Ox5hsszI2T6U5qdrJizBc+0ilYSEjTSzU0yZcmvppztXe/5Al5fUwdg== + xtend@^4.0.0: version "4.0.2" resolved "https://registry.yarnpkg.com/xtend/-/xtend-4.0.2.tgz#bb72779f5fa465186b1f438f674fa347fdb5db54" @@ -5245,6 +5873,11 @@ yargs-parser@20.x, yargs-parser@^20.2.2: resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-20.2.9.tgz#2eb7dc3b0289718fc295f362753845c41a0c94ee" integrity sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w== +yargs-parser@^21.1.1: + version "21.1.1" + resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-21.1.1.tgz#9096bceebf990d21bb31fa9516e0ede294a77d35" + integrity sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw== + yargs@^16.2.0: version "16.2.0" resolved "https://registry.yarnpkg.com/yargs/-/yargs-16.2.0.tgz#1c82bf0f6b6a66eafce7ef30e376f49a12477f66" @@ -5258,6 +5891,19 @@ yargs@^16.2.0: y18n "^5.0.5" yargs-parser "^20.2.2" +yargs@^17.7.2: + version "17.7.2" + resolved "https://registry.yarnpkg.com/yargs/-/yargs-17.7.2.tgz#991df39aca675a192b816e1e0363f9d75d2aa269" + integrity sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w== + dependencies: + cliui "^8.0.1" + escalade "^3.1.1" + get-caller-file "^2.0.5" + require-directory "^2.1.1" + string-width "^4.2.3" + y18n "^5.0.5" + yargs-parser "^21.1.1" + yn@3.1.1: version "3.1.1" resolved "https://registry.yarnpkg.com/yn/-/yn-3.1.1.tgz#1e87401a09d767c1d5eab26a6e4c185182d2eb50" From f85fed86860f2098388934e0d5cfc40c330d73b9 Mon Sep 17 00:00:00 2001 From: Yazeed Loonat Date: Mon, 26 Jun 2023 16:06:38 -0700 Subject: [PATCH 09/57] feat: unit accessibility priority type (#3480) * feat: prisma schema generation * fix: cleaning up removed "models" * fix: updates per emily * fix: caught some schema errors * feat: listing get endpoints * feat: adding DTOs * feat: openapi and swagger updates * fix: pass through DTOs * fix: updates per emily * feat: adding ci jobs * fix: round 2 * fix: indentation problem * fix: ci updates * fix: ci test again * fix: adding maybe missing library? * fix: running yarn install * fix: updates per morgan * fix: ci fix round x * fix: test updates * fix: integration tests and ci * fix: update for tests * fix: fixing scheme so migrations can be created * feat: creating ami-chart endpoints * feat: reserved community type * feat: unit type endpoints * feat: unit accessibility priority type * fix: updates per morgan * fix: update backend-new circleci job (#3489) * fix: update backend-new circleci job * fix: add cache * fix: add class validator * fix: update list typing * fix: add class transformer * fix: add listing type * fix: one more attempt at typings * fix: update jest config file * fix: add dbsetup * fix: one more attempt * fix: e2e typing fix * fix: turn off diagnostics for e2e * fix: clear db fields between tests * fix: prettier and linting changes * fix: additional linting fixes --------- Co-authored-by: Yazeed Loonat * fix: updates from what I learned * fix: forgot swagger file * fix: updates per morgan * fix: updates per sean/morgan and test updates * fix: updates per sean and morgan * fix: maybe resolving test race case * fix: removing jurisdictionName param * fix: upates per sean and emily * fix: updates per emily * fix: updates to listing test * fix: updates per ami chart * fix: trying to remove seeding for e2e testing * fix: updating test description * fix: updates from ami chart learnings * fix: updates from reserved-community-type learning * fix: test updates * fix: updates per pr comments * fix: e2e test update * fix: updates for ci * fix: possible ci fix * fix: possible fix for ci errors * fix: maybe fixes to ci --------- Co-authored-by: Morgan Ludtke <42942267+ludtkemorgan@users.noreply.github.com> --- .../migration.sql | 5 +- backend_new/prisma/schema.prisma | 20 +- .../prisma/seed-helpers/listing-factory.ts | 22 +- ...nit-accessibility-priority-type-factory.ts | 17 ++ backend_new/prisma/seed.ts | 10 + backend_new/src/app.module.ts | 3 + ...-accessibility-priority-type.controller.ts | 101 +++++++ ...-accessibility-priority-type-create.dto.ts | 7 + ...-accessibility-priority-type-update.dto.ts | 7 + .../unit-accessibility-priority-type.dto.ts | 19 ++ ...nit-accessibility-priority-type-get.dto.ts | 12 - backend_new/src/dtos/units/unit-get.dto.ts | 2 +- .../src/dtos/units/unit-summarized.dto.ts | 2 +- .../src/dtos/units/units-summery-get.dto.ts | 2 +- ...unit-accessibility-priority-type.module.ts | 12 + ...nit-accessibility-priority-type.service.ts | 119 ++++++++ backend_new/src/utilities/mapTo.ts | 13 +- backend_new/src/utilities/unit-utilities.ts | 28 +- backend_new/test/integration/app.e2e-spec.ts | 4 + .../reserved-community-type.e2e-spec.ts | 11 +- ...it-accessibility-priority-type.e2e-spec.ts | 136 +++++++++ .../unit/services/translation.service.spec.ts | 1 - ...ccessibility-priority-type.service.spec.ts | 273 ++++++++++++++++++ backend_new/types/src/backend-swagger.ts | 154 ++++++++++ package.json | 4 +- 25 files changed, 933 insertions(+), 51 deletions(-) rename backend_new/prisma/migrations/{20230622231511_init => 20230623180942_init}/migration.sql (99%) create mode 100644 backend_new/prisma/seed-helpers/unit-accessibility-priority-type-factory.ts create mode 100644 backend_new/src/controllers/unit-accessibility-priority-type.controller.ts create mode 100644 backend_new/src/dtos/unit-accessibility-priority-types/unit-accessibility-priority-type-create.dto.ts create mode 100644 backend_new/src/dtos/unit-accessibility-priority-types/unit-accessibility-priority-type-update.dto.ts create mode 100644 backend_new/src/dtos/unit-accessibility-priority-types/unit-accessibility-priority-type.dto.ts delete mode 100644 backend_new/src/dtos/units/unit-accessibility-priority-type-get.dto.ts create mode 100644 backend_new/src/modules/unit-accessibility-priority-type.module.ts create mode 100644 backend_new/src/services/unit-accessibility-priority-type.service.ts create mode 100644 backend_new/test/integration/unit-accessibility-priority-type.e2e-spec.ts create mode 100644 backend_new/test/unit/services/unit-accessibility-priority-type.service.spec.ts diff --git a/backend_new/prisma/migrations/20230622231511_init/migration.sql b/backend_new/prisma/migrations/20230623180942_init/migration.sql similarity index 99% rename from backend_new/prisma/migrations/20230622231511_init/migration.sql rename to backend_new/prisma/migrations/20230623180942_init/migration.sql index 99029e2f01..95bd94f9fe 100644 --- a/backend_new/prisma/migrations/20230622231511_init/migration.sql +++ b/backend_new/prisma/migrations/20230623180942_init/migration.sql @@ -64,6 +64,9 @@ CREATE TYPE "monthly_rent_determination_type_enum" AS ENUM ('flatRent', 'percent -- CreateEnum CREATE TYPE "unit_type_enum" AS ENUM ('studio', 'oneBdrm', 'twoBdrm', 'threeBdrm', 'fourBdrm', 'SRO', 'fiveBdrm'); +-- CreateEnum +CREATE TYPE "unit_accessibility_priority_type_enum" AS ENUM ('mobility', 'mobilityAndHearing', 'hearing', 'visual', 'hearingAndVisual', 'mobilityAndVisual', 'mobilityHearingAndVisual'); + -- CreateTable CREATE TABLE "accessibility" ( "id" UUID NOT NULL DEFAULT uuid_generate_v4(), @@ -578,7 +581,7 @@ CREATE TABLE "unit_accessibility_priority_types" ( "id" UUID NOT NULL DEFAULT uuid_generate_v4(), "created_at" TIMESTAMP(6) NOT NULL DEFAULT CURRENT_TIMESTAMP, "updated_at" TIMESTAMP(6) NOT NULL, - "name" TEXT NOT NULL, + "name" "unit_accessibility_priority_type_enum" NOT NULL, CONSTRAINT "unit_accessibility_priority_types_pkey" PRIMARY KEY ("id") ); diff --git a/backend_new/prisma/schema.prisma b/backend_new/prisma/schema.prisma index f1e88bbb68..c7b4303863 100644 --- a/backend_new/prisma/schema.prisma +++ b/backend_new/prisma/schema.prisma @@ -631,10 +631,10 @@ model Translations { // Note: [name] formerly max length 256 model UnitAccessibilityPriorityTypes { - id String @id() @default(dbgenerated("uuid_generate_v4()")) @db.Uuid - createdAt DateTime @default(now()) @map("created_at") @db.Timestamp(6) - updatedAt DateTime @updatedAt @map("updated_at") @db.Timestamp(6) - name String + id String @id() @default(dbgenerated("uuid_generate_v4()")) @db.Uuid + createdAt DateTime @default(now()) @map("created_at") @db.Timestamp(6) + updatedAt DateTime @updatedAt @map("updated_at") @db.Timestamp(6) + name UnitAccessibilityPriorityTypeEnum units Units[] unitGroup UnitGroup[] unitsSummary UnitsSummary[] @@ -1042,3 +1042,15 @@ enum UnitTypeEnum { @@map("unit_type_enum") } + +enum UnitAccessibilityPriorityTypeEnum { + mobility + mobilityAndHearing + hearing + visual + hearingAndVisual + mobilityAndVisual + mobilityHearingAndVisual + + @@map("unit_accessibility_priority_type_enum") +} diff --git a/backend_new/prisma/seed-helpers/listing-factory.ts b/backend_new/prisma/seed-helpers/listing-factory.ts index 15c389d6c6..01bd0b449d 100644 --- a/backend_new/prisma/seed-helpers/listing-factory.ts +++ b/backend_new/prisma/seed-helpers/listing-factory.ts @@ -12,6 +12,7 @@ import { MultiselectQuestionsApplicationSectionEnum, UnitsStatusEnum, } from '@prisma/client'; +import { unitAccessibilityPriorityTypeFactory } from './unit-accessibility-priority-type-factory'; import { unitTypeFactory } from './unit-type-factory'; export const listingFactory = ( @@ -20,6 +21,7 @@ export const listingFactory = ( amiChartId?: string, reservedCommunityTypeId?: string, unitTypeId?: string, + unitAccessibilityPriorityTypeId?: string, ): Prisma.ListingsCreateInput => ({ additionalApplicationSubmissionNotes: `additionalApplicationSubmissionNotes: ${i}`, digitalApplication: true, @@ -333,7 +335,14 @@ export const listingFactory = ( }, ], }, - units: unitFactory(i, i, jurisdictionId, amiChartId, unitTypeId), + units: unitFactory( + i, + i, + jurisdictionId, + amiChartId, + unitTypeId, + unitAccessibilityPriorityTypeId, + ), }); const unitFactory = ( @@ -342,6 +351,7 @@ const unitFactory = ( jurisdictionId: string, amiChartId?: string, unitTypeId?: string, + unitAccessibilityPriorityTypeId?: string, ): Prisma.UnitsCreateNestedManyWithoutListingsInput => { const createArray: Prisma.UnitsCreateWithoutListingsInput[] = []; for (let j = 0; j < numberToMake; j++) { @@ -381,11 +391,11 @@ const unitFactory = ( }, }, }, - unitAccessibilityPriorityTypes: { - create: { - name: `listing: ${i} unit: ${j} unitAccessibilityPriorityTypes: ${j}`, - }, - }, + unitAccessibilityPriorityTypes: unitAccessibilityPriorityTypeId + ? { connect: { id: unitAccessibilityPriorityTypeId } } + : { + create: unitAccessibilityPriorityTypeFactory(i), + }, unitRentTypes: { create: { name: `listing: ${i} unit: ${j} unitRentTypes: ${j}`, diff --git a/backend_new/prisma/seed-helpers/unit-accessibility-priority-type-factory.ts b/backend_new/prisma/seed-helpers/unit-accessibility-priority-type-factory.ts new file mode 100644 index 0000000000..7a50755fe8 --- /dev/null +++ b/backend_new/prisma/seed-helpers/unit-accessibility-priority-type-factory.ts @@ -0,0 +1,17 @@ +import { Prisma, UnitAccessibilityPriorityTypeEnum } from '@prisma/client'; + +export const unitAccessibilityPriorityTypeFactory = ( + i: number, +): Prisma.UnitAccessibilityPriorityTypesCreateInput => ({ + ...unitPriorityTypeArray[i % unitPriorityTypeArray.length], +}); + +export const unitPriorityTypeArray = [ + { name: UnitAccessibilityPriorityTypeEnum.mobility }, + { name: UnitAccessibilityPriorityTypeEnum.mobilityAndHearing }, + { name: UnitAccessibilityPriorityTypeEnum.hearing }, + { name: UnitAccessibilityPriorityTypeEnum.visual }, + { name: UnitAccessibilityPriorityTypeEnum.hearingAndVisual }, + { name: UnitAccessibilityPriorityTypeEnum.mobilityAndVisual }, + { name: UnitAccessibilityPriorityTypeEnum.mobilityHearingAndVisual }, +]; diff --git a/backend_new/prisma/seed.ts b/backend_new/prisma/seed.ts index 8674e5def7..4fd502c0b0 100644 --- a/backend_new/prisma/seed.ts +++ b/backend_new/prisma/seed.ts @@ -3,6 +3,7 @@ import { amiChartFactory } from './seed-helpers/ami-chart-factory'; import { jurisdictionFactory } from './seed-helpers/jurisdiction-factory'; import { listingFactory } from './seed-helpers/listing-factory'; import { reservedCommunityTypeFactory } from './seed-helpers/reserved-community-type-factory'; +import { unitAccessibilityPriorityTypeFactory } from './seed-helpers/unit-accessibility-priority-type-factory'; import { unitTypeFactory } from './seed-helpers/unit-type-factory'; const prisma = new PrismaClient(); @@ -25,6 +26,14 @@ async function main() { unitTypeIds.push(res.id); } + const unitAccessibilityPriorityTypeIds: string[] = []; + for (let i = 0; i < 7; i++) { + const res = await prisma.unitAccessibilityPriorityTypes.create({ + data: unitAccessibilityPriorityTypeFactory(i), + }); + unitAccessibilityPriorityTypeIds.push(res.id); + } + for (let i = 0; i < 5; i++) { await prisma.listings.create({ data: listingFactory( @@ -33,6 +42,7 @@ async function main() { amiChart.id, reservedCommunityType.id, unitTypeIds[i], + unitAccessibilityPriorityTypeIds[i], ), }); } diff --git a/backend_new/src/app.module.ts b/backend_new/src/app.module.ts index 935a585a94..ab3d244d3b 100644 --- a/backend_new/src/app.module.ts +++ b/backend_new/src/app.module.ts @@ -4,6 +4,7 @@ import { AppService } from './app.service'; import { AmiChartModule } from './modules/ami-chart.module'; import { ListingModule } from './modules/listing.module'; import { ReservedCommunityTypeModule } from './modules/reserved-community-type.module'; +import { UnitAccessibilityPriorityTypeServiceModule } from './modules/unit-accessibility-priority-type.module'; import { UnitTypeModule } from './modules/unit-type.module'; @Module({ @@ -12,6 +13,7 @@ import { UnitTypeModule } from './modules/unit-type.module'; AmiChartModule, ReservedCommunityTypeModule, UnitTypeModule, + UnitAccessibilityPriorityTypeServiceModule, ], controllers: [AppController], providers: [AppService], @@ -20,6 +22,7 @@ import { UnitTypeModule } from './modules/unit-type.module'; AmiChartModule, ReservedCommunityTypeModule, UnitTypeModule, + UnitAccessibilityPriorityTypeServiceModule, ], }) export class AppModule {} diff --git a/backend_new/src/controllers/unit-accessibility-priority-type.controller.ts b/backend_new/src/controllers/unit-accessibility-priority-type.controller.ts new file mode 100644 index 0000000000..df771e25c5 --- /dev/null +++ b/backend_new/src/controllers/unit-accessibility-priority-type.controller.ts @@ -0,0 +1,101 @@ +import { + Body, + Controller, + Delete, + Get, + Param, + Post, + Put, + UsePipes, + ValidationPipe, +} from '@nestjs/common'; +import { + ApiExtraModels, + ApiOkResponse, + ApiOperation, + ApiTags, +} from '@nestjs/swagger'; +import { UnitAccessibilityPriorityTypeService } from '../services/unit-accessibility-priority-type.service'; +import { UnitAccessibilityPriorityType } from '../dtos/unit-accessibility-priority-types/unit-accessibility-priority-type.dto'; +import { UnitAccessibilityPriorityTypeCreate } from '../dtos/unit-accessibility-priority-types/unit-accessibility-priority-type-create.dto'; +import { UnitAccessibilityPriorityTypeUpdate } from '../dtos/unit-accessibility-priority-types/unit-accessibility-priority-type-update.dto'; +import { defaultValidationPipeOptions } from '../utilities/default-validation-pipe-options'; +import { IdDTO } from '../dtos/shared/id.dto'; +import { SuccessDTO } from '../dtos/shared/success.dto'; + +@Controller('unitAccessibilityPriorityTypes') +@ApiTags('unitAccessibilityPriorityTypes') +@UsePipes(new ValidationPipe(defaultValidationPipeOptions)) +@ApiExtraModels( + UnitAccessibilityPriorityTypeCreate, + UnitAccessibilityPriorityTypeUpdate, + IdDTO, +) +export class UnitAccessibilityPriorityTypeController { + constructor( + private readonly unitAccessibilityPriorityTypeService: UnitAccessibilityPriorityTypeService, + ) {} + + @Get() + @ApiOperation({ + summary: 'List unitAccessibilityPriorityTypes', + operationId: 'list', + }) + @ApiOkResponse({ type: UnitAccessibilityPriorityType, isArray: true }) + async list(): Promise { + return await this.unitAccessibilityPriorityTypeService.list(); + } + + @Get(`:unitAccessibilityPriorityTypeId`) + @ApiOperation({ + summary: 'Get unitAccessibilityPriorityType by id', + operationId: 'retrieve', + }) + @ApiOkResponse({ type: UnitAccessibilityPriorityType }) + async retrieve( + @Param('unitAccessibilityPriorityTypeId') + unitAccessibilityPriorityTypeId: string, + ): Promise { + return this.unitAccessibilityPriorityTypeService.findOne( + unitAccessibilityPriorityTypeId, + ); + } + + @Post() + @ApiOperation({ + summary: 'Create unitAccessibilityPriorityType', + operationId: 'create', + }) + @ApiOkResponse({ type: UnitAccessibilityPriorityType }) + async create( + @Body() unitAccessibilityPriorityType: UnitAccessibilityPriorityTypeCreate, + ): Promise { + return await this.unitAccessibilityPriorityTypeService.create( + unitAccessibilityPriorityType, + ); + } + + @Put(`:unitAccessibilityPriorityTypeId`) + @ApiOperation({ + summary: 'Update unitAccessibilityPriorityType', + operationId: 'update', + }) + @ApiOkResponse({ type: UnitAccessibilityPriorityType }) + async update( + @Body() unitAccessibilityPriorityType: UnitAccessibilityPriorityTypeUpdate, + ): Promise { + return await this.unitAccessibilityPriorityTypeService.update( + unitAccessibilityPriorityType, + ); + } + + @Delete() + @ApiOperation({ + summary: 'Delete unitAccessibilityPriorityType by id', + operationId: 'delete', + }) + @ApiOkResponse({ type: SuccessDTO }) + async delete(@Body() dto: IdDTO): Promise { + return await this.unitAccessibilityPriorityTypeService.delete(dto.id); + } +} diff --git a/backend_new/src/dtos/unit-accessibility-priority-types/unit-accessibility-priority-type-create.dto.ts b/backend_new/src/dtos/unit-accessibility-priority-types/unit-accessibility-priority-type-create.dto.ts new file mode 100644 index 0000000000..ba3eb4f46c --- /dev/null +++ b/backend_new/src/dtos/unit-accessibility-priority-types/unit-accessibility-priority-type-create.dto.ts @@ -0,0 +1,7 @@ +import { OmitType } from '@nestjs/swagger'; +import { UnitAccessibilityPriorityTypeUpdate } from './unit-accessibility-priority-type-update.dto'; + +export class UnitAccessibilityPriorityTypeCreate extends OmitType( + UnitAccessibilityPriorityTypeUpdate, + ['id'], +) {} diff --git a/backend_new/src/dtos/unit-accessibility-priority-types/unit-accessibility-priority-type-update.dto.ts b/backend_new/src/dtos/unit-accessibility-priority-types/unit-accessibility-priority-type-update.dto.ts new file mode 100644 index 0000000000..9334d62687 --- /dev/null +++ b/backend_new/src/dtos/unit-accessibility-priority-types/unit-accessibility-priority-type-update.dto.ts @@ -0,0 +1,7 @@ +import { OmitType } from '@nestjs/swagger'; +import { UnitAccessibilityPriorityType } from './unit-accessibility-priority-type.dto'; + +export class UnitAccessibilityPriorityTypeUpdate extends OmitType( + UnitAccessibilityPriorityType, + ['createdAt', 'updatedAt'], +) {} diff --git a/backend_new/src/dtos/unit-accessibility-priority-types/unit-accessibility-priority-type.dto.ts b/backend_new/src/dtos/unit-accessibility-priority-types/unit-accessibility-priority-type.dto.ts new file mode 100644 index 0000000000..816a0806a2 --- /dev/null +++ b/backend_new/src/dtos/unit-accessibility-priority-types/unit-accessibility-priority-type.dto.ts @@ -0,0 +1,19 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { Expose } from 'class-transformer'; +import { IsDefined, IsEnum } from 'class-validator'; +import { ValidationsGroupsEnum } from '../../enums/shared/validation-groups-enum'; +import { AbstractDTO } from '../shared/abstract.dto'; +import { UnitAccessibilityPriorityTypeEnum } from '@prisma/client'; + +export class UnitAccessibilityPriorityType extends AbstractDTO { + @Expose() + @IsDefined({ groups: [ValidationsGroupsEnum.default] }) + @IsEnum(UnitAccessibilityPriorityTypeEnum, { + groups: [ValidationsGroupsEnum.default], + }) + @ApiProperty({ + enum: UnitAccessibilityPriorityTypeEnum, + enumName: 'UnitAccessibilityPriorityTypeEnum', + }) + name: UnitAccessibilityPriorityTypeEnum; +} diff --git a/backend_new/src/dtos/units/unit-accessibility-priority-type-get.dto.ts b/backend_new/src/dtos/units/unit-accessibility-priority-type-get.dto.ts deleted file mode 100644 index f2cc06fe24..0000000000 --- a/backend_new/src/dtos/units/unit-accessibility-priority-type-get.dto.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { Expose } from 'class-transformer'; -import { IsDefined, IsString, MaxLength } from 'class-validator'; -import { ValidationsGroupsEnum } from '../../enums/shared/validation-groups-enum'; -import { AbstractDTO } from '../shared/abstract.dto'; - -export class UnitAccessibilityPriorityType extends AbstractDTO { - @Expose() - @IsString({ groups: [ValidationsGroupsEnum.default] }) - @IsDefined({ groups: [ValidationsGroupsEnum.default] }) - @MaxLength(256, { groups: [ValidationsGroupsEnum.default] }) - name: string; -} diff --git a/backend_new/src/dtos/units/unit-get.dto.ts b/backend_new/src/dtos/units/unit-get.dto.ts index e7d313c564..336ab12aa4 100644 --- a/backend_new/src/dtos/units/unit-get.dto.ts +++ b/backend_new/src/dtos/units/unit-get.dto.ts @@ -11,7 +11,7 @@ import { AbstractDTO } from '../shared/abstract.dto'; import { AmiChart } from '../ami-charts/ami-chart.dto'; import { UnitType } from '../unit-types/unit-type.dto'; import { UnitRentType } from './unit-rent-type-get.dto'; -import { UnitAccessibilityPriorityType } from './unit-accessibility-priority-type-get.dto'; +import { UnitAccessibilityPriorityType } from '../unit-accessibility-priority-types/unit-accessibility-priority-type.dto'; import { UnitAmiChartOverride } from './ami-chart-override-get.dto'; class Unit extends AbstractDTO { diff --git a/backend_new/src/dtos/units/unit-summarized.dto.ts b/backend_new/src/dtos/units/unit-summarized.dto.ts index 1e777426c1..31d36b3c6a 100644 --- a/backend_new/src/dtos/units/unit-summarized.dto.ts +++ b/backend_new/src/dtos/units/unit-summarized.dto.ts @@ -6,7 +6,7 @@ import { UnitSummaryByAMI } from './unit-summary-by-ami-get.dto'; import { HMI } from './hmi-get.dto'; import { ApiProperty } from '@nestjs/swagger'; import { UnitType } from '../unit-types/unit-type.dto'; -import { UnitAccessibilityPriorityType } from './unit-accessibility-priority-type-get.dto'; +import { UnitAccessibilityPriorityType } from '../unit-accessibility-priority-types/unit-accessibility-priority-type.dto'; export class UnitsSummarized { @Expose() diff --git a/backend_new/src/dtos/units/units-summery-get.dto.ts b/backend_new/src/dtos/units/units-summery-get.dto.ts index 890fd895a2..7335c23a9e 100644 --- a/backend_new/src/dtos/units/units-summery-get.dto.ts +++ b/backend_new/src/dtos/units/units-summery-get.dto.ts @@ -9,7 +9,7 @@ import { import { Expose, Type } from 'class-transformer'; import { ValidationsGroupsEnum } from '../../enums/shared/validation-groups-enum'; import { UnitType } from '../unit-types/unit-type.dto'; -import { UnitAccessibilityPriorityType } from './unit-accessibility-priority-type-get.dto'; +import { UnitAccessibilityPriorityType } from '../unit-accessibility-priority-types/unit-accessibility-priority-type.dto'; class UnitsSummary { @Expose() diff --git a/backend_new/src/modules/unit-accessibility-priority-type.module.ts b/backend_new/src/modules/unit-accessibility-priority-type.module.ts new file mode 100644 index 0000000000..10392043e7 --- /dev/null +++ b/backend_new/src/modules/unit-accessibility-priority-type.module.ts @@ -0,0 +1,12 @@ +import { Module } from '@nestjs/common'; +import { UnitAccessibilityPriorityTypeController } from '../controllers/unit-accessibility-priority-type.controller'; +import { UnitAccessibilityPriorityTypeService } from '../services/unit-accessibility-priority-type.service'; +import { PrismaService } from '../services/prisma.service'; + +@Module({ + imports: [], + controllers: [UnitAccessibilityPriorityTypeController], + providers: [UnitAccessibilityPriorityTypeService, PrismaService], + exports: [UnitAccessibilityPriorityTypeService, PrismaService], +}) +export class UnitAccessibilityPriorityTypeServiceModule {} diff --git a/backend_new/src/services/unit-accessibility-priority-type.service.ts b/backend_new/src/services/unit-accessibility-priority-type.service.ts new file mode 100644 index 0000000000..a174727e7b --- /dev/null +++ b/backend_new/src/services/unit-accessibility-priority-type.service.ts @@ -0,0 +1,119 @@ +import { Injectable, NotFoundException } from '@nestjs/common'; +import { PrismaService } from './prisma.service'; +import { UnitAccessibilityPriorityType } from '../dtos/unit-accessibility-priority-types/unit-accessibility-priority-type.dto'; +import { UnitAccessibilityPriorityTypeCreate } from '../dtos/unit-accessibility-priority-types/unit-accessibility-priority-type-create.dto'; +import { mapTo } from '../utilities/mapTo'; +import { SuccessDTO } from '../dtos/shared/success.dto'; +import { UnitAccessibilityPriorityTypeUpdate } from '../dtos/unit-accessibility-priority-types/unit-accessibility-priority-type-update.dto'; + +/* + this is the service for unit accessibility priority types + it handles all the backend's business logic for reading/writing/deleting unit type data +*/ + +@Injectable() +export class UnitAccessibilityPriorityTypeService { + constructor(private prisma: PrismaService) {} + + /* + this will get a set of unit accessibility priority types given the params passed in + */ + async list(): Promise { + const rawunitPriortyTypes = + await this.prisma.unitAccessibilityPriorityTypes.findMany(); + return mapTo(UnitAccessibilityPriorityType, rawunitPriortyTypes); + } + + /* + this will return 1 unit accessibility priority type or error + */ + async findOne( + unitAccessibilityPriorityTypeId: string, + ): Promise { + const rawunitPriortyTypes = + await this.prisma.unitAccessibilityPriorityTypes.findUnique({ + where: { + id: unitAccessibilityPriorityTypeId, + }, + }); + + if (!rawunitPriortyTypes) { + throw new NotFoundException( + `unitAccessibilityPriorityTypeId ${unitAccessibilityPriorityTypeId} was requested but not found`, + ); + } + + return mapTo(UnitAccessibilityPriorityType, rawunitPriortyTypes); + } + + /* + this will create a unit accessibility priority type + */ + async create( + incomingData: UnitAccessibilityPriorityTypeCreate, + ): Promise { + const rawResult = await this.prisma.unitAccessibilityPriorityTypes.create({ + data: { + ...incomingData, + }, + }); + + return mapTo(UnitAccessibilityPriorityType, rawResult); + } + + /* + this will update a unit accessibility priority type's name or items field + if no unit accessibility priority type has the id of the incoming argument an error is thrown + */ + async update( + incomingData: UnitAccessibilityPriorityTypeUpdate, + ): Promise { + await this.findOrThrow(incomingData.id); + + const rawResults = await this.prisma.unitAccessibilityPriorityTypes.update({ + data: { + ...incomingData, + id: undefined, + }, + where: { + id: incomingData.id, + }, + }); + return mapTo(UnitAccessibilityPriorityType, rawResults); + } + + /* + this will delete a unit accessibility priority type + */ + async delete(unitAccessibilityPriorityTypeId: string): Promise { + await this.findOrThrow(unitAccessibilityPriorityTypeId); + await this.prisma.unitAccessibilityPriorityTypes.delete({ + where: { + id: unitAccessibilityPriorityTypeId, + }, + }); + return { + success: true, + } as SuccessDTO; + } + + /* + this will either find a record or throw a customized error + */ + async findOrThrow(unitAccessibilityPriorityTypeId: string): Promise { + const unitType = + await this.prisma.unitAccessibilityPriorityTypes.findUnique({ + where: { + id: unitAccessibilityPriorityTypeId, + }, + }); + + if (!unitType) { + throw new NotFoundException( + `unitAccessibilityPriorityTypeId ${unitAccessibilityPriorityTypeId} was requested but not found`, + ); + } + + return true; + } +} diff --git a/backend_new/src/utilities/mapTo.ts b/backend_new/src/utilities/mapTo.ts index ef4bddf73d..3c84cea04b 100644 --- a/backend_new/src/utilities/mapTo.ts +++ b/backend_new/src/utilities/mapTo.ts @@ -1,13 +1,16 @@ -import { ClassTransformOptions, plainToClass } from 'class-transformer'; -import { ClassType } from 'class-transformer/ClassTransformer'; +import { + ClassTransformOptions, + plainToClass, + ClassConstructor, +} from 'class-transformer'; export function mapTo( - cls: ClassType, + cls: ClassConstructor, plain: V[], options?: ClassTransformOptions, ): T[]; export function mapTo( - cls: ClassType, + cls: ClassConstructor, plain: V, options?: ClassTransformOptions, ): T; @@ -17,7 +20,7 @@ export function mapTo( This is mostly used by controllers to map the result of a service to the type returned by the endpoint */ export function mapTo( - cls: ClassType, + cls: ClassConstructor, plain, options?: ClassTransformOptions, ) { diff --git a/backend_new/src/utilities/unit-utilities.ts b/backend_new/src/utilities/unit-utilities.ts index 7087063892..bbe577ec9f 100644 --- a/backend_new/src/utilities/unit-utilities.ts +++ b/backend_new/src/utilities/unit-utilities.ts @@ -1,4 +1,4 @@ -import { ReviewOrderTypeEnum } from '@prisma/client'; +import { ReviewOrderTypeEnum, UnitTypeEnum } from '@prisma/client'; import { UnitSummary } from '../dtos/units/unit-summary-get.dto'; import Unit from '../dtos/units/unit-get.dto'; import { AmiChart } from '../dtos/ami-charts/ami-chart.dto'; @@ -7,7 +7,7 @@ import { MinMaxCurrency } from '../dtos/shared/min-max-currency.dto'; import { MinMax } from '../dtos/shared/min-max.dto'; import { UnitsSummarized } from '../dtos/units/unit-summarized.dto'; import { UnitType } from '../dtos/unit-types/unit-type.dto'; -import { UnitAccessibilityPriorityType } from '../dtos/units/unit-accessibility-priority-type-get.dto'; +import { UnitAccessibilityPriorityType } from '../dtos/unit-accessibility-priority-types/unit-accessibility-priority-type.dto'; import { AmiChartItem } from '../dtos/units/ami-chart-item-get.dto'; import { UnitAmiChartOverride } from '../dtos/units/ami-chart-override-get.dto'; @@ -17,11 +17,11 @@ type UnitMap = { }; export const UnitTypeSort = [ - 'SRO', - 'studio', - 'oneBdrm', - 'twoBdrm', - 'threeBdrm', + UnitTypeEnum.SRO, + UnitTypeEnum.studio, + UnitTypeEnum.oneBdrm, + UnitTypeEnum.twoBdrm, + UnitTypeEnum.threeBdrm, ]; const usd = new Intl.NumberFormat('en-US', { @@ -427,9 +427,10 @@ export const summarizeUnitsByTypeAndRent = ( return summaries.sort((a, b) => { return ( - UnitTypeSort.indexOf(a.unitTypes.name) - - UnitTypeSort.indexOf(b.unitTypes.name) || - Number(a.minIncomeRange.min) - Number(b.minIncomeRange.min) + UnitTypeSort.findIndex((sortedType) => a.unitTypes.name === sortedType) - + UnitTypeSort.findIndex( + (sortedType) => b.unitTypes.name === sortedType, + ) || Number(a.minIncomeRange.min) - Number(b.minIncomeRange.min) ); }); }; @@ -454,9 +455,10 @@ export const summarizeUnitsByType = ( }); return summaries.sort((a, b) => { return ( - UnitTypeSort.indexOf(a.unitTypes.name) - - UnitTypeSort.indexOf(b.unitTypes.name) || - Number(a.minIncomeRange.min) - Number(b.minIncomeRange.min) + UnitTypeSort.findIndex((sortedType) => a.unitTypes.name === sortedType) - + UnitTypeSort.findIndex( + (sortedType) => b.unitTypes.name === sortedType, + ) || Number(a.minIncomeRange.min) - Number(b.minIncomeRange.min) ); }); }; diff --git a/backend_new/test/integration/app.e2e-spec.ts b/backend_new/test/integration/app.e2e-spec.ts index a3f86f3817..262b9396fc 100644 --- a/backend_new/test/integration/app.e2e-spec.ts +++ b/backend_new/test/integration/app.e2e-spec.ts @@ -15,6 +15,10 @@ describe('AppController (e2e)', () => { await app.init(); }); + afterEach(async () => { + await app.close(); + }); + it('/ (GET)', () => { return request(app.getHttpServer()) .get('/') diff --git a/backend_new/test/integration/reserved-community-type.e2e-spec.ts b/backend_new/test/integration/reserved-community-type.e2e-spec.ts index 59c5b2b7be..328964a973 100644 --- a/backend_new/test/integration/reserved-community-type.e2e-spec.ts +++ b/backend_new/test/integration/reserved-community-type.e2e-spec.ts @@ -51,10 +51,13 @@ describe('ReservedCommunityType Controller Tests', () => { .get(`/reservedCommunityTypes`) .expect(200); - expect(res.body.length).toEqual(2); - expect(res.body[0].name).toEqual('name: 10'); - expect(res.body[1].name).toEqual('name: 10'); - expect(res.body[0].jurisdictions.id).not.toBe(res.body[1].jurisdictions.id); + expect(res.body.length).toBeGreaterThanOrEqual(2); + const typeNames = res.body.map((value) => value.name); + const jurisdictions = res.body.map((value) => value.jurisdictions.id); + expect(typeNames).toContain('name: 10'); + expect(typeNames).toContain('name: 10'); + expect(jurisdictions).toContain(jurisdictionA.id); + expect(jurisdictions).toContain(jurisdictionB.id); }); it('testing list endpoint with params', async () => { diff --git a/backend_new/test/integration/unit-accessibility-priority-type.e2e-spec.ts b/backend_new/test/integration/unit-accessibility-priority-type.e2e-spec.ts new file mode 100644 index 0000000000..d345079f64 --- /dev/null +++ b/backend_new/test/integration/unit-accessibility-priority-type.e2e-spec.ts @@ -0,0 +1,136 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { INestApplication } from '@nestjs/common'; +import * as request from 'supertest'; +import { AppModule } from '../../src/app.module'; +import { PrismaService } from '../../src/services/prisma.service'; +import { unitAccessibilityPriorityTypeFactory } from '../../prisma/seed-helpers/unit-accessibility-priority-type-factory'; +import { UnitAccessibilityPriorityTypeCreate } from '../../src/dtos/unit-accessibility-priority-types/unit-accessibility-priority-type-create.dto'; +import { UnitAccessibilityPriorityTypeUpdate } from '../../src/dtos/unit-accessibility-priority-types/unit-accessibility-priority-type-update.dto'; +import { IdDTO } from 'src/dtos/shared/id.dto'; +import { randomUUID } from 'crypto'; + +describe('UnitAccessibilityPriorityType Controller Tests', () => { + let app: INestApplication; + let prisma: PrismaService; + + beforeAll(async () => { + const moduleFixture: TestingModule = await Test.createTestingModule({ + imports: [AppModule], + }).compile(); + + app = moduleFixture.createNestApplication(); + prisma = moduleFixture.get(PrismaService); + await app.init(); + }); + + it('testing list endpoint', async () => { + const unitTypeA = await prisma.unitAccessibilityPriorityTypes.create({ + data: unitAccessibilityPriorityTypeFactory(7), + }); + const unitTypeB = await prisma.unitAccessibilityPriorityTypes.create({ + data: unitAccessibilityPriorityTypeFactory(8), + }); + + const res = await request(app.getHttpServer()) + .get(`/unitAccessibilityPriorityTypes?`) + .expect(200); + + expect(res.body.length).toBeGreaterThanOrEqual(2); + const unitTypeNames = res.body.map((value) => value.name); + expect(unitTypeNames).toContain(unitTypeA.name); + expect(unitTypeNames).toContain(unitTypeB.name); + }); + + it("retrieve endpoint with id that doesn't exist should error", async () => { + const id = randomUUID(); + const res = await request(app.getHttpServer()) + .get(`/unitAccessibilityPriorityTypes/${id}`) + .expect(404); + expect(res.body.message).toEqual( + `unitAccessibilityPriorityTypeId ${id} was requested but not found`, + ); + }); + + it('testing retrieve endpoint', async () => { + const unitTypeA = await prisma.unitAccessibilityPriorityTypes.create({ + data: unitAccessibilityPriorityTypeFactory(10), + }); + + const res = await request(app.getHttpServer()) + .get(`/unitAccessibilityPriorityTypes/${unitTypeA.id}`) + .expect(200); + + expect(res.body.name).toEqual(unitTypeA.name); + }); + + it('testing create endpoint', async () => { + const name = unitAccessibilityPriorityTypeFactory(10).name; + const res = await request(app.getHttpServer()) + .post('/unitAccessibilityPriorityTypes') + .send({ + name: name, + } as UnitAccessibilityPriorityTypeCreate) + .expect(201); + + expect(res.body.name).toEqual(name); + }); + + it("update endpoint with id that doesn't exist should error", async () => { + const name = unitAccessibilityPriorityTypeFactory(10).name; + const id = randomUUID(); + const res = await request(app.getHttpServer()) + .put(`/unitAccessibilityPriorityTypes/${id}`) + .send({ + id: id, + name: name, + } as UnitAccessibilityPriorityTypeUpdate) + .expect(404); + expect(res.body.message).toEqual( + `unitAccessibilityPriorityTypeId ${id} was requested but not found`, + ); + }); + + it('testing update endpoint', async () => { + const unitTypeA = await prisma.unitAccessibilityPriorityTypes.create({ + data: unitAccessibilityPriorityTypeFactory(10), + }); + const name = unitAccessibilityPriorityTypeFactory(11).name; + const res = await request(app.getHttpServer()) + .put(`/unitAccessibilityPriorityTypes/${unitTypeA.id}`) + .send({ + id: unitTypeA.id, + name: name, + } as UnitAccessibilityPriorityTypeUpdate) + .expect(200); + + expect(res.body.name).toEqual(name); + }); + + it("delete endpoint with id that doesn't exist should error", async () => { + const id = randomUUID(); + const res = await request(app.getHttpServer()) + .delete(`/unitAccessibilityPriorityTypes`) + .send({ + id: id, + } as IdDTO) + .expect(404); + expect(res.body.message).toEqual( + `unitAccessibilityPriorityTypeId ${id} was requested but not found`, + ); + }); + + it('testing delete endpoint', async () => { + const unitTypeA = await prisma.unitAccessibilityPriorityTypes.create({ + data: unitAccessibilityPriorityTypeFactory(16), + }); + + const res = await request(app.getHttpServer()) + .delete(`/unitAccessibilityPriorityTypes`) + .send({ + id: unitTypeA.id, + } as IdDTO) + .expect(200); + + expect(res.body.success).toEqual(true); + }); +}); diff --git a/backend_new/test/unit/services/translation.service.spec.ts b/backend_new/test/unit/services/translation.service.spec.ts index 1fc39dd617..b6b8d49291 100644 --- a/backend_new/test/unit/services/translation.service.spec.ts +++ b/backend_new/test/unit/services/translation.service.spec.ts @@ -2,7 +2,6 @@ import { LanguagesEnum, ListingsStatusEnum, MultiselectQuestionsApplicationSectionEnum, - Prisma, } from '@prisma/client'; import { randomUUID } from 'crypto'; import { ListingGet } from '../../../src/dtos/listings/listing-get.dto'; diff --git a/backend_new/test/unit/services/unit-accessibility-priority-type.service.spec.ts b/backend_new/test/unit/services/unit-accessibility-priority-type.service.spec.ts new file mode 100644 index 0000000000..7e33919722 --- /dev/null +++ b/backend_new/test/unit/services/unit-accessibility-priority-type.service.spec.ts @@ -0,0 +1,273 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { PrismaService } from '../../../src/services/prisma.service'; +import { UnitAccessibilityPriorityTypeService } from '../../../src/services/unit-accessibility-priority-type.service'; +import { UnitAccessibilityPriorityTypeCreate } from '../../../src/dtos/unit-accessibility-priority-types/unit-accessibility-priority-type-create.dto'; +import { UnitAccessibilityPriorityTypeUpdate } from '../../../src/dtos/unit-accessibility-priority-types/unit-accessibility-priority-type-update.dto'; +import { UnitAccessibilityPriorityType } from '../../../src/dtos/unit-accessibility-priority-types/unit-accessibility-priority-type.dto'; +import { randomUUID } from 'crypto'; +import { unitPriorityTypeArray } from '../../../prisma/seed-helpers/unit-accessibility-priority-type-factory'; + +describe('Testing unit accessibility priority type service', () => { + let service: UnitAccessibilityPriorityTypeService; + let prisma: PrismaService; + + const mockUnitAccessibilityPriorityType = (position: number, date: Date) => { + return { + id: randomUUID(), + name: unitPriorityTypeArray[position].name, + createdAt: date, + updatedAt: date, + }; + }; + + const mockUnitAccessibilityPriorityTypeSet = ( + numberToCreate: number, + date: Date, + ) => { + const toReturn = []; + for (let i = 0; i < numberToCreate; i++) { + toReturn.push(mockUnitAccessibilityPriorityType(i, date)); + } + return toReturn; + }; + + beforeAll(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [UnitAccessibilityPriorityTypeService, PrismaService], + }).compile(); + + service = module.get( + UnitAccessibilityPriorityTypeService, + ); + prisma = module.get(PrismaService); + }); + + it('testing list()', async () => { + const date = new Date(); + const mockedValue = mockUnitAccessibilityPriorityTypeSet(3, date); + prisma.unitAccessibilityPriorityTypes.findMany = jest + .fn() + .mockResolvedValue(mockedValue); + + expect(await service.list()).toEqual([ + { + id: mockedValue[0].id, + name: unitPriorityTypeArray[0].name, + createdAt: date, + updatedAt: date, + }, + { + id: mockedValue[1].id, + name: unitPriorityTypeArray[1].name, + createdAt: date, + updatedAt: date, + }, + { + id: mockedValue[2].id, + name: unitPriorityTypeArray[2].name, + createdAt: date, + updatedAt: date, + }, + ]); + + expect(prisma.unitAccessibilityPriorityTypes.findMany).toHaveBeenCalled(); + }); + + it('testing findOne() with id present', async () => { + const date = new Date(); + const mockedValue = mockUnitAccessibilityPriorityType(3, date); + prisma.unitAccessibilityPriorityTypes.findUnique = jest + .fn() + .mockResolvedValue(mockedValue); + + expect(await service.findOne('example Id')).toEqual({ + id: mockedValue.id, + name: unitPriorityTypeArray[3].name, + createdAt: date, + updatedAt: date, + }); + + expect( + prisma.unitAccessibilityPriorityTypes.findUnique, + ).toHaveBeenCalledWith({ + where: { + id: 'example Id', + }, + }); + }); + + it('testing findOne() with id not present', async () => { + prisma.unitAccessibilityPriorityTypes.findUnique = jest + .fn() + .mockResolvedValue(null); + + await expect( + async () => await service.findOne('example Id'), + ).rejects.toThrowError(); + + expect( + prisma.unitAccessibilityPriorityTypes.findUnique, + ).toHaveBeenCalledWith({ + where: { + id: 'example Id', + }, + }); + }); + + it('testing create()', async () => { + const date = new Date(); + const mockedValue = mockUnitAccessibilityPriorityType(3, date); + prisma.unitAccessibilityPriorityTypes.create = jest + .fn() + .mockResolvedValue(mockedValue); + + const params: UnitAccessibilityPriorityTypeCreate = { + name: unitPriorityTypeArray[3].name, + }; + + expect(await service.create(params)).toEqual({ + id: mockedValue.id, + name: unitPriorityTypeArray[3].name, + createdAt: date, + updatedAt: date, + }); + + expect(prisma.unitAccessibilityPriorityTypes.create).toHaveBeenCalledWith({ + data: { + name: unitPriorityTypeArray[3].name, + }, + }); + }); + + it('testing update() existing record found', async () => { + const date = new Date(); + + const mockedUnitType = mockUnitAccessibilityPriorityType(3, date); + + prisma.unitAccessibilityPriorityTypes.findUnique = jest + .fn() + .mockResolvedValue(mockedUnitType); + prisma.unitAccessibilityPriorityTypes.update = jest.fn().mockResolvedValue({ + ...mockedUnitType, + name: unitPriorityTypeArray[4].name, + }); + + const params: UnitAccessibilityPriorityTypeUpdate = { + name: unitPriorityTypeArray[4].name, + id: mockedUnitType.id, + }; + + expect(await service.update(params)).toEqual({ + id: mockedUnitType.id, + name: unitPriorityTypeArray[4].name, + createdAt: date, + updatedAt: date, + }); + + expect( + prisma.unitAccessibilityPriorityTypes.findUnique, + ).toHaveBeenCalledWith({ + where: { + id: mockedUnitType.id, + }, + }); + + expect(prisma.unitAccessibilityPriorityTypes.update).toHaveBeenCalledWith({ + data: { + name: unitPriorityTypeArray[4].name, + }, + where: { + id: mockedUnitType.id, + }, + }); + }); + + it('testing update() existing record not found', async () => { + const date = new Date(); + + prisma.unitAccessibilityPriorityTypes.findUnique = jest + .fn() + .mockResolvedValue(null); + prisma.unitAccessibilityPriorityTypes.update = jest + .fn() + .mockResolvedValue(null); + + const params: UnitAccessibilityPriorityType = { + name: unitPriorityTypeArray[4].name, + id: 'example id', + createdAt: date, + updatedAt: date, + }; + + await expect( + async () => await service.update(params), + ).rejects.toThrowError(); + + expect( + prisma.unitAccessibilityPriorityTypes.findUnique, + ).toHaveBeenCalledWith({ + where: { + id: 'example id', + }, + }); + }); + + it('testing delete()', async () => { + const date = new Date(); + + const mockedUnitType = mockUnitAccessibilityPriorityType(3, date); + + prisma.unitAccessibilityPriorityTypes.findUnique = jest + .fn() + .mockResolvedValue(mockedUnitType); + prisma.unitAccessibilityPriorityTypes.delete = jest + .fn() + .mockResolvedValue(mockUnitAccessibilityPriorityType(3, date)); + + expect(await service.delete('example Id')).toEqual({ + success: true, + }); + + expect(prisma.unitAccessibilityPriorityTypes.delete).toHaveBeenCalledWith({ + where: { + id: 'example Id', + }, + }); + }); + + it('testing findOrThrow() record found', async () => { + prisma.unitAccessibilityPriorityTypes.findUnique = jest + .fn() + .mockResolvedValue(null); + + await expect( + async () => await service.findOrThrow('example id'), + ).rejects.toThrowError(); + + expect( + prisma.unitAccessibilityPriorityTypes.findUnique, + ).toHaveBeenCalledWith({ + where: { + id: 'example id', + }, + }); + }); + + it('testing findOrThrow() record not found', async () => { + const date = new Date(); + const mockedAmi = mockUnitAccessibilityPriorityType(3, date); + prisma.unitAccessibilityPriorityTypes.findUnique = jest + .fn() + .mockResolvedValue(mockedAmi); + + expect(await service.findOrThrow('example id')).toEqual(true); + + expect( + prisma.unitAccessibilityPriorityTypes.findUnique, + ).toHaveBeenCalledWith({ + where: { + id: 'example id', + }, + }); + }); +}); diff --git a/backend_new/types/src/backend-swagger.ts b/backend_new/types/src/backend-swagger.ts index 7592030faf..00955508bf 100644 --- a/backend_new/types/src/backend-swagger.ts +++ b/backend_new/types/src/backend-swagger.ts @@ -584,6 +584,144 @@ export class UnitTypesService { } } +export class UnitAccessibilityPriorityTypesService { + /** + * List unitAccessibilityPriorityTypes + */ + list( + options: IRequestOptions = {}, + ): Promise { + return new Promise((resolve, reject) => { + let url = basePath + '/unitAccessibilityPriorityTypes'; + + const configs: IRequestConfig = getConfigs( + 'get', + 'application/json', + url, + options, + ); + + /** 适配ios13getèŻ·æ±‚äžć…èźžćžŠbody */ + + axios(configs, resolve, reject); + }); + } + /** + * Create unitAccessibilityPriorityType + */ + create( + params: { + /** requestBody */ + body?: UnitAccessibilityPriorityTypeCreate; + } = {} as any, + options: IRequestOptions = {}, + ): Promise { + return new Promise((resolve, reject) => { + let url = basePath + '/unitAccessibilityPriorityTypes'; + + const configs: IRequestConfig = getConfigs( + 'post', + 'application/json', + url, + options, + ); + + let data = params.body; + + configs.data = data; + + axios(configs, resolve, reject); + }); + } + /** + * Delete unitAccessibilityPriorityType by id + */ + delete( + params: { + /** requestBody */ + body?: IdDTO; + } = {} as any, + options: IRequestOptions = {}, + ): Promise { + return new Promise((resolve, reject) => { + let url = basePath + '/unitAccessibilityPriorityTypes'; + + const configs: IRequestConfig = getConfigs( + 'delete', + 'application/json', + url, + options, + ); + + let data = params.body; + + configs.data = data; + + axios(configs, resolve, reject); + }); + } + /** + * Get unitAccessibilityPriorityType by id + */ + retrieve( + params: { + /** */ + unitAccessibilityPriorityTypeId: string; + } = {} as any, + options: IRequestOptions = {}, + ): Promise { + return new Promise((resolve, reject) => { + let url = + basePath + + '/unitAccessibilityPriorityTypes/{unitAccessibilityPriorityTypeId}'; + url = url.replace( + '{unitAccessibilityPriorityTypeId}', + params['unitAccessibilityPriorityTypeId'] + '', + ); + + const configs: IRequestConfig = getConfigs( + 'get', + 'application/json', + url, + options, + ); + + /** 适配ios13getèŻ·æ±‚äžć…èźžćžŠbody */ + + axios(configs, resolve, reject); + }); + } + /** + * Update unitAccessibilityPriorityType + */ + update( + params: { + /** requestBody */ + body?: UnitAccessibilityPriorityTypeUpdate; + } = {} as any, + options: IRequestOptions = {}, + ): Promise { + return new Promise((resolve, reject) => { + let url = + basePath + + '/unitAccessibilityPriorityTypes/{unitAccessibilityPriorityTypeId}'; + + const configs: IRequestConfig = getConfigs( + 'put', + 'application/json', + url, + options, + ); + + let data = params.body; + + configs.data = data; + + axios(configs, resolve, reject); + }); + } +} + export interface ListingsQueryParams { /** */ page?: number; @@ -686,6 +824,9 @@ export interface UnitAccessibilityPriorityType { /** */ updatedAt: Date; + + /** */ + name: string; } export interface MinMaxCurrency { @@ -946,6 +1087,19 @@ export interface UnitTypeUpdate { numBedrooms: number; } +export interface UnitAccessibilityPriorityTypeCreate { + /** */ + name: string; +} + +export interface UnitAccessibilityPriorityTypeUpdate { + /** */ + id: string; + + /** */ + name: string; +} + export enum ListingViews { 'fundamentals' = 'fundamentals', 'base' = 'base', diff --git a/package.json b/package.json index 6a25a705d9..2912f62da7 100644 --- a/package.json +++ b/package.json @@ -50,8 +50,8 @@ "test:apps:headless": "concurrently \"yarn dev:backend\" \"yarn test:app:public:headless\"", "lint": "eslint '**/*.ts' '**/*.tsx' '**/*.js' && cd backend_new && yarn lint", "version:all": "lerna version --yes --no-commit-hooks --ignore-scripts --conventional-graduate --include-merged-tags --force-git-tag", - "test:backend:new": "cd backend_new && yarn test", - "test:backend:new:e2e": "cd backend_new && yarn jest --config ./test/jest-e2e.config.js", + "test:backend:new": "cd backend_new && yarn test --detectOpenHandles", + "test:backend:new:e2e": "cd backend_new && yarn jest --config ./test/jest-e2e.config.js --detectOpenHandles", "test:backend:new:dbsetup": "cd backend_new && yarn db:migration:run", "backend:new:install": "cd backend_new && yarn install", "prettier": "prettier --write \"./**/*.{js,jsx,ts,tsx,json}\"" From 53bdb79cd3536445675a0142cb5cc7517c7e57a9 Mon Sep 17 00:00:00 2001 From: Yazeed Loonat Date: Wed, 28 Jun 2023 09:36:37 -0700 Subject: [PATCH 10/57] feat: unit rent type (#3481) * feat: prisma schema generation * fix: cleaning up removed "models" * fix: updates per emily * fix: caught some schema errors * feat: listing get endpoints * feat: adding DTOs * feat: openapi and swagger updates * fix: pass through DTOs * fix: updates per emily * feat: adding ci jobs * fix: round 2 * fix: indentation problem * fix: ci updates * fix: ci test again * fix: adding maybe missing library? * fix: running yarn install * fix: updates per morgan * fix: ci fix round x * fix: test updates * fix: integration tests and ci * fix: update for tests * fix: fixing scheme so migrations can be created * feat: creating ami-chart endpoints * feat: reserved community type * feat: unit type endpoints * feat: unit accessibility priority type * feat: unit rent type * fix: updates per morgan * fix: update backend-new circleci job (#3489) * fix: update backend-new circleci job * fix: add cache * fix: add class validator * fix: update list typing * fix: add class transformer * fix: add listing type * fix: one more attempt at typings * fix: update jest config file * fix: add dbsetup * fix: one more attempt * fix: e2e typing fix * fix: turn off diagnostics for e2e * fix: clear db fields between tests * fix: prettier and linting changes * fix: additional linting fixes --------- Co-authored-by: Yazeed Loonat * fix: updates from what I learned * fix: forgot swagger file * fix: updates per morgan * fix: updates per sean/morgan and test updates * fix: updates per sean and morgan * fix: maybe resolving test race case * fix: removing jurisdictionName param * fix: upates per sean and emily * fix: updates per emily * fix: updates to listing test * fix: updates per ami chart * fix: trying to remove seeding for e2e testing * fix: updating test description * fix: updates from ami chart learnings * fix: updates from ami chart learnings * fix: updating client * fix: updates from reserved-community-type learning * fix: test updates * fix: e2e test updates * fix: updates per morgan * fix: resolving merge conflicts * fix: updates for tests --------- Co-authored-by: Morgan Ludtke <42942267+ludtkemorgan@users.noreply.github.com> --- .../migration.sql | 5 +- backend_new/prisma/schema.prisma | 15 +- .../prisma/seed-helpers/listing-factory.ts | 14 +- .../seed-helpers/unit-rent-type-factory.ts | 12 + backend_new/prisma/seed.ts | 10 + backend_new/src/app.module.ts | 3 + .../controllers/unit-rent-type.controller.ts | 85 +++++++ backend_new/src/dtos/shared/pagination.dto.ts | 13 +- .../unit-rent-type-create.dto.ts | 4 + .../unit-rent-type-update.dto.ts | 7 + .../unit-rent-types/unit-rent-type.dto.ts | 19 ++ backend_new/src/dtos/units/unit-get.dto.ts | 2 +- .../src/dtos/units/unit-rent-type-get.dto.ts | 12 - .../src/modules/unit-rent-type.module.ts | 12 + .../src/services/unit-rent-type.service.ts | 107 ++++++++ .../integration/unit-rent-type.e2e-spec.ts | 135 +++++++++++ .../services/unit-rent-type.service.spec.ts | 228 ++++++++++++++++++ backend_new/types/src/backend-swagger.ts | 177 +++++++++++++- 18 files changed, 830 insertions(+), 30 deletions(-) rename backend_new/prisma/migrations/{20230623180942_init => 20230626231609_init}/migration.sql (99%) create mode 100644 backend_new/prisma/seed-helpers/unit-rent-type-factory.ts create mode 100644 backend_new/src/controllers/unit-rent-type.controller.ts create mode 100644 backend_new/src/dtos/unit-rent-types/unit-rent-type-create.dto.ts create mode 100644 backend_new/src/dtos/unit-rent-types/unit-rent-type-update.dto.ts create mode 100644 backend_new/src/dtos/unit-rent-types/unit-rent-type.dto.ts delete mode 100644 backend_new/src/dtos/units/unit-rent-type-get.dto.ts create mode 100644 backend_new/src/modules/unit-rent-type.module.ts create mode 100644 backend_new/src/services/unit-rent-type.service.ts create mode 100644 backend_new/test/integration/unit-rent-type.e2e-spec.ts create mode 100644 backend_new/test/unit/services/unit-rent-type.service.spec.ts diff --git a/backend_new/prisma/migrations/20230623180942_init/migration.sql b/backend_new/prisma/migrations/20230626231609_init/migration.sql similarity index 99% rename from backend_new/prisma/migrations/20230623180942_init/migration.sql rename to backend_new/prisma/migrations/20230626231609_init/migration.sql index 95bd94f9fe..80dd9cc5ce 100644 --- a/backend_new/prisma/migrations/20230623180942_init/migration.sql +++ b/backend_new/prisma/migrations/20230626231609_init/migration.sql @@ -61,6 +61,9 @@ CREATE TYPE "property_region_enum" AS ENUM ('Greater_Downtown', 'Eastside', 'Sou -- CreateEnum CREATE TYPE "monthly_rent_determination_type_enum" AS ENUM ('flatRent', 'percentageOfIncome'); +-- CreateEnum +CREATE TYPE "unit_rent_type_enum" AS ENUM ('fixed', 'percentageOfIncome'); + -- CreateEnum CREATE TYPE "unit_type_enum" AS ENUM ('studio', 'oneBdrm', 'twoBdrm', 'threeBdrm', 'fourBdrm', 'SRO', 'fiveBdrm'); @@ -601,7 +604,7 @@ CREATE TABLE "unit_rent_types" ( "id" UUID NOT NULL DEFAULT uuid_generate_v4(), "created_at" TIMESTAMP(6) NOT NULL DEFAULT CURRENT_TIMESTAMP, "updated_at" TIMESTAMP(6) NOT NULL, - "name" TEXT NOT NULL, + "name" "unit_rent_type_enum" NOT NULL, CONSTRAINT "unit_rent_types_pkey" PRIMARY KEY ("id") ); diff --git a/backend_new/prisma/schema.prisma b/backend_new/prisma/schema.prisma index c7b4303863..84c21757dd 100644 --- a/backend_new/prisma/schema.prisma +++ b/backend_new/prisma/schema.prisma @@ -654,10 +654,10 @@ model UnitAmiChartOverrides { // Note: [name] formerly max length 256 model UnitRentTypes { - id String @id() @default(dbgenerated("uuid_generate_v4()")) @db.Uuid - createdAt DateTime @default(now()) @map("created_at") @db.Timestamp(6) - updatedAt DateTime @updatedAt @map("updated_at") @db.Timestamp(6) - name String + id String @id() @default(dbgenerated("uuid_generate_v4()")) @db.Uuid + createdAt DateTime @default(now()) @map("created_at") @db.Timestamp(6) + updatedAt DateTime @updatedAt @map("updated_at") @db.Timestamp(6) + name UnitRentTypeEnum units Units[] @@map("unit_rent_types") @@ -1031,6 +1031,13 @@ enum MonthlyRentDeterminationTypeEnum { @@map("monthly_rent_determination_type_enum") } +enum UnitRentTypeEnum { + fixed + percentageOfIncome + + @@map("unit_rent_type_enum") +} + enum UnitTypeEnum { studio oneBdrm diff --git a/backend_new/prisma/seed-helpers/listing-factory.ts b/backend_new/prisma/seed-helpers/listing-factory.ts index 01bd0b449d..e2927d65b5 100644 --- a/backend_new/prisma/seed-helpers/listing-factory.ts +++ b/backend_new/prisma/seed-helpers/listing-factory.ts @@ -14,6 +14,7 @@ import { } from '@prisma/client'; import { unitAccessibilityPriorityTypeFactory } from './unit-accessibility-priority-type-factory'; import { unitTypeFactory } from './unit-type-factory'; +import { unitRentTypeFactory } from './unit-rent-type-factory'; export const listingFactory = ( i: number, @@ -22,6 +23,7 @@ export const listingFactory = ( reservedCommunityTypeId?: string, unitTypeId?: string, unitAccessibilityPriorityTypeId?: string, + unitRentTypeId?: string, ): Prisma.ListingsCreateInput => ({ additionalApplicationSubmissionNotes: `additionalApplicationSubmissionNotes: ${i}`, digitalApplication: true, @@ -342,6 +344,7 @@ export const listingFactory = ( amiChartId, unitTypeId, unitAccessibilityPriorityTypeId, + unitRentTypeId, ), }); @@ -352,6 +355,7 @@ const unitFactory = ( amiChartId?: string, unitTypeId?: string, unitAccessibilityPriorityTypeId?: string, + unitRentTypeId?: string, ): Prisma.UnitsCreateNestedManyWithoutListingsInput => { const createArray: Prisma.UnitsCreateWithoutListingsInput[] = []; for (let j = 0; j < numberToMake; j++) { @@ -396,11 +400,11 @@ const unitFactory = ( : { create: unitAccessibilityPriorityTypeFactory(i), }, - unitRentTypes: { - create: { - name: `listing: ${i} unit: ${j} unitRentTypes: ${j}`, - }, - }, + unitRentTypes: unitRentTypeId + ? { connect: { id: unitRentTypeId } } + : { + create: unitRentTypeFactory(i), + }, }); } const toReturn: Prisma.UnitsCreateNestedManyWithoutListingsInput = { diff --git a/backend_new/prisma/seed-helpers/unit-rent-type-factory.ts b/backend_new/prisma/seed-helpers/unit-rent-type-factory.ts new file mode 100644 index 0000000000..f29e493d09 --- /dev/null +++ b/backend_new/prisma/seed-helpers/unit-rent-type-factory.ts @@ -0,0 +1,12 @@ +import { Prisma, UnitRentTypeEnum } from '@prisma/client'; + +export const unitRentTypeFactory = ( + i: number, +): Prisma.UnitRentTypesCreateInput => ({ + ...unitRentTypeArray[i % unitRentTypeArray.length], +}); + +export const unitRentTypeArray = [ + { name: UnitRentTypeEnum.fixed }, + { name: UnitRentTypeEnum.percentageOfIncome }, +]; diff --git a/backend_new/prisma/seed.ts b/backend_new/prisma/seed.ts index 4fd502c0b0..42e4e6e674 100644 --- a/backend_new/prisma/seed.ts +++ b/backend_new/prisma/seed.ts @@ -4,6 +4,7 @@ import { jurisdictionFactory } from './seed-helpers/jurisdiction-factory'; import { listingFactory } from './seed-helpers/listing-factory'; import { reservedCommunityTypeFactory } from './seed-helpers/reserved-community-type-factory'; import { unitAccessibilityPriorityTypeFactory } from './seed-helpers/unit-accessibility-priority-type-factory'; +import { unitRentTypeFactory } from './seed-helpers/unit-rent-type-factory'; import { unitTypeFactory } from './seed-helpers/unit-type-factory'; const prisma = new PrismaClient(); @@ -34,6 +35,14 @@ async function main() { unitAccessibilityPriorityTypeIds.push(res.id); } + const unitRentTypeIds: string[] = []; + for (let i = 0; i < 2; i++) { + const res = await prisma.unitRentTypes.create({ + data: unitRentTypeFactory(i), + }); + unitRentTypeIds.push(res.id); + } + for (let i = 0; i < 5; i++) { await prisma.listings.create({ data: listingFactory( @@ -43,6 +52,7 @@ async function main() { reservedCommunityType.id, unitTypeIds[i], unitAccessibilityPriorityTypeIds[i], + unitRentTypeIds[i % 2], ), }); } diff --git a/backend_new/src/app.module.ts b/backend_new/src/app.module.ts index ab3d244d3b..55706dab4e 100644 --- a/backend_new/src/app.module.ts +++ b/backend_new/src/app.module.ts @@ -6,6 +6,7 @@ import { ListingModule } from './modules/listing.module'; import { ReservedCommunityTypeModule } from './modules/reserved-community-type.module'; import { UnitAccessibilityPriorityTypeServiceModule } from './modules/unit-accessibility-priority-type.module'; import { UnitTypeModule } from './modules/unit-type.module'; +import { UnitRentTypeModule } from './modules/unit-rent-type.module'; @Module({ imports: [ @@ -14,6 +15,7 @@ import { UnitTypeModule } from './modules/unit-type.module'; ReservedCommunityTypeModule, UnitTypeModule, UnitAccessibilityPriorityTypeServiceModule, + UnitRentTypeModule, ], controllers: [AppController], providers: [AppService], @@ -23,6 +25,7 @@ import { UnitTypeModule } from './modules/unit-type.module'; ReservedCommunityTypeModule, UnitTypeModule, UnitAccessibilityPriorityTypeServiceModule, + UnitRentTypeModule, ], }) export class AppModule {} diff --git a/backend_new/src/controllers/unit-rent-type.controller.ts b/backend_new/src/controllers/unit-rent-type.controller.ts new file mode 100644 index 0000000000..8decc8dc24 --- /dev/null +++ b/backend_new/src/controllers/unit-rent-type.controller.ts @@ -0,0 +1,85 @@ +import { + Body, + Controller, + Delete, + Get, + Param, + Post, + Put, + UsePipes, + ValidationPipe, +} from '@nestjs/common'; +import { + ApiExtraModels, + ApiOkResponse, + ApiOperation, + ApiTags, +} from '@nestjs/swagger'; +import { UnitRentTypeService } from '../services/unit-rent-type.service'; +import { UnitRentType } from '../dtos/unit-rent-types/unit-rent-type.dto'; +import { UnitRentTypeCreate } from '../dtos/unit-rent-types/unit-rent-type-create.dto'; +import { UnitRentTypeUpdate } from '../dtos/unit-rent-types/unit-rent-type-update.dto'; +import { defaultValidationPipeOptions } from '../utilities/default-validation-pipe-options'; +import { IdDTO } from '../dtos/shared/id.dto'; +import { SuccessDTO } from '../dtos/shared/success.dto'; + +@Controller('unitRentTypes') +@ApiTags('unitRentTypes') +@UsePipes(new ValidationPipe(defaultValidationPipeOptions)) +@ApiExtraModels(UnitRentTypeCreate, UnitRentTypeUpdate, IdDTO) +export class UnitRentTypeController { + constructor(private readonly unitRentTypeService: UnitRentTypeService) {} + + @Get() + @ApiOperation({ summary: 'List unitRentTypes', operationId: 'list' }) + @ApiOkResponse({ type: UnitRentType, isArray: true }) + async list(): Promise { + return await this.unitRentTypeService.list(); + } + + @Get(`:unitRentTypeId`) + @ApiOperation({ + summary: 'Get unitRentType by id', + operationId: 'retrieve', + }) + @ApiOkResponse({ type: UnitRentType }) + async retrieve( + @Param('unitRentTypeId') unitRentTypeId: string, + ): Promise { + return this.unitRentTypeService.findOne(unitRentTypeId); + } + + @Post() + @ApiOperation({ + summary: 'Create unitRentType', + operationId: 'create', + }) + @ApiOkResponse({ type: UnitRentType }) + async create( + @Body() unitRentType: UnitRentTypeCreate, + ): Promise { + return await this.unitRentTypeService.create(unitRentType); + } + + @Put(`:unitRentTypeId`) + @ApiOperation({ + summary: 'Update unitRentType', + operationId: 'update', + }) + @ApiOkResponse({ type: UnitRentType }) + async update( + @Body() unitRentType: UnitRentTypeUpdate, + ): Promise { + return await this.unitRentTypeService.update(unitRentType); + } + + @Delete() + @ApiOperation({ + summary: 'Delete unitRentType by id', + operationId: 'delete', + }) + @ApiOkResponse({ type: SuccessDTO }) + async delete(@Body() dto: IdDTO): Promise { + return await this.unitRentTypeService.delete(dto.id); + } +} diff --git a/backend_new/src/dtos/shared/pagination.dto.ts b/backend_new/src/dtos/shared/pagination.dto.ts index 1507e2653f..eb0762083a 100644 --- a/backend_new/src/dtos/shared/pagination.dto.ts +++ b/backend_new/src/dtos/shared/pagination.dto.ts @@ -1,12 +1,17 @@ import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; -import { Expose, Transform, TransformFnParams, Type } from 'class-transformer'; +import { + Expose, + Transform, + TransformFnParams, + Type, + ClassConstructor, +} from 'class-transformer'; import { IsNumber, registerDecorator, ValidationOptions, } from 'class-validator'; import { ValidationsGroupsEnum } from '../../enums/shared/validation-groups-enum'; -import { ClassType } from 'class-transformer/ClassTransformer'; export class PaginationMeta { @Expose() @@ -27,8 +32,8 @@ export interface Pagination { } export function PaginationFactory( - classType: ClassType, -): ClassType> { + classType: ClassConstructor, +): ClassConstructor> { class PaginationHost implements Pagination { @ApiProperty({ type: () => classType, isArray: true }) @Expose() diff --git a/backend_new/src/dtos/unit-rent-types/unit-rent-type-create.dto.ts b/backend_new/src/dtos/unit-rent-types/unit-rent-type-create.dto.ts new file mode 100644 index 0000000000..905df31536 --- /dev/null +++ b/backend_new/src/dtos/unit-rent-types/unit-rent-type-create.dto.ts @@ -0,0 +1,4 @@ +import { OmitType } from '@nestjs/swagger'; +import { UnitRentTypeUpdate } from './unit-rent-type-update.dto'; + +export class UnitRentTypeCreate extends OmitType(UnitRentTypeUpdate, ['id']) {} diff --git a/backend_new/src/dtos/unit-rent-types/unit-rent-type-update.dto.ts b/backend_new/src/dtos/unit-rent-types/unit-rent-type-update.dto.ts new file mode 100644 index 0000000000..8606bab64d --- /dev/null +++ b/backend_new/src/dtos/unit-rent-types/unit-rent-type-update.dto.ts @@ -0,0 +1,7 @@ +import { OmitType } from '@nestjs/swagger'; +import { UnitRentType } from './unit-rent-type.dto'; + +export class UnitRentTypeUpdate extends OmitType(UnitRentType, [ + 'createdAt', + 'updatedAt', +]) {} diff --git a/backend_new/src/dtos/unit-rent-types/unit-rent-type.dto.ts b/backend_new/src/dtos/unit-rent-types/unit-rent-type.dto.ts new file mode 100644 index 0000000000..c164f24f76 --- /dev/null +++ b/backend_new/src/dtos/unit-rent-types/unit-rent-type.dto.ts @@ -0,0 +1,19 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { Expose } from 'class-transformer'; +import { IsDefined, IsEnum } from 'class-validator'; +import { ValidationsGroupsEnum } from '../../enums/shared/validation-groups-enum'; +import { AbstractDTO } from '../shared/abstract.dto'; +import { UnitRentTypeEnum } from '@prisma/client'; + +export class UnitRentType extends AbstractDTO { + @Expose() + @IsDefined({ groups: [ValidationsGroupsEnum.default] }) + @IsEnum(UnitRentTypeEnum, { + groups: [ValidationsGroupsEnum.default], + }) + @ApiProperty({ + enum: UnitRentTypeEnum, + enumName: 'UnitRentTypeEnum', + }) + name: UnitRentTypeEnum; +} diff --git a/backend_new/src/dtos/units/unit-get.dto.ts b/backend_new/src/dtos/units/unit-get.dto.ts index 336ab12aa4..12c9b6e491 100644 --- a/backend_new/src/dtos/units/unit-get.dto.ts +++ b/backend_new/src/dtos/units/unit-get.dto.ts @@ -10,7 +10,7 @@ import { ValidationsGroupsEnum } from '../../enums/shared/validation-groups-enum import { AbstractDTO } from '../shared/abstract.dto'; import { AmiChart } from '../ami-charts/ami-chart.dto'; import { UnitType } from '../unit-types/unit-type.dto'; -import { UnitRentType } from './unit-rent-type-get.dto'; +import { UnitRentType } from '../unit-rent-types/unit-rent-type.dto'; import { UnitAccessibilityPriorityType } from '../unit-accessibility-priority-types/unit-accessibility-priority-type.dto'; import { UnitAmiChartOverride } from './ami-chart-override-get.dto'; diff --git a/backend_new/src/dtos/units/unit-rent-type-get.dto.ts b/backend_new/src/dtos/units/unit-rent-type-get.dto.ts deleted file mode 100644 index 20382325a6..0000000000 --- a/backend_new/src/dtos/units/unit-rent-type-get.dto.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { Expose } from 'class-transformer'; -import { IsDefined, IsString, MaxLength } from 'class-validator'; -import { ValidationsGroupsEnum } from '../../enums/shared/validation-groups-enum'; -import { AbstractDTO } from '../shared/abstract.dto'; - -export class UnitRentType extends AbstractDTO { - @Expose() - @IsString({ groups: [ValidationsGroupsEnum.default] }) - @IsDefined({ groups: [ValidationsGroupsEnum.default] }) - @MaxLength(256, { groups: [ValidationsGroupsEnum.default] }) - name: string; -} diff --git a/backend_new/src/modules/unit-rent-type.module.ts b/backend_new/src/modules/unit-rent-type.module.ts new file mode 100644 index 0000000000..d1173041c1 --- /dev/null +++ b/backend_new/src/modules/unit-rent-type.module.ts @@ -0,0 +1,12 @@ +import { Module } from '@nestjs/common'; +import { UnitRentTypeController } from '../controllers/unit-rent-type.controller'; +import { UnitRentTypeService } from '../services/unit-rent-type.service'; +import { PrismaService } from '../services/prisma.service'; + +@Module({ + imports: [], + controllers: [UnitRentTypeController], + providers: [UnitRentTypeService, PrismaService], + exports: [UnitRentTypeService, PrismaService], +}) +export class UnitRentTypeModule {} diff --git a/backend_new/src/services/unit-rent-type.service.ts b/backend_new/src/services/unit-rent-type.service.ts new file mode 100644 index 0000000000..b7636f3ff4 --- /dev/null +++ b/backend_new/src/services/unit-rent-type.service.ts @@ -0,0 +1,107 @@ +import { Injectable, NotFoundException } from '@nestjs/common'; +import { PrismaService } from './prisma.service'; +import { UnitRentType } from '../dtos/unit-rent-types/unit-rent-type.dto'; +import { UnitRentTypeCreate } from '../dtos/unit-rent-types/unit-rent-type-create.dto'; +import { UnitRentTypeUpdate } from '../dtos/unit-rent-types/unit-rent-type-update.dto'; +import { mapTo } from '../utilities/mapTo'; +import { SuccessDTO } from '../dtos/shared/success.dto'; +import { UnitRentTypes } from '@prisma/client'; + +/* + this is the service for unit rent types + it handles all the backend's business logic for reading/writing/deleting unit rent type data +*/ + +@Injectable() +export class UnitRentTypeService { + constructor(private prisma: PrismaService) {} + + /* + this will get a set of unit rent types given the params passed in + */ + async list(): Promise { + const rawUnitRentTypes = await this.prisma.unitRentTypes.findMany(); + return mapTo(UnitRentType, rawUnitRentTypes); + } + + /* + this will return 1 unit rent type or error + */ + async findOne(unitRentTypeId: string): Promise { + const rawUnitRentType = await this.findOrThrow(unitRentTypeId); + + if (!rawUnitRentType) { + throw new NotFoundException( + `unitRentTypeId ${unitRentTypeId} was requested but not found`, + ); + } + + return mapTo(UnitRentType, rawUnitRentType); + } + + /* + this will create a unit rent type + */ + async create(incomingData: UnitRentTypeCreate): Promise { + const rawResult = await this.prisma.unitRentTypes.create({ + data: { + ...incomingData, + id: undefined, + }, + }); + + return mapTo(UnitRentType, rawResult); + } + + /* + this will update a unit rent type's name or items field + if no unit rent type has the id of the incoming argument an error is thrown + */ + async update(incomingData: UnitRentTypeUpdate): Promise { + await this.findOrThrow(incomingData.id); + + const rawResults = await this.prisma.unitRentTypes.update({ + data: { + name: incomingData.name, + }, + where: { + id: incomingData.id, + }, + }); + return mapTo(UnitRentType, rawResults); + } + + /* + this will delete a unit rent type + */ + async delete(unitRentTypeId: string): Promise { + await this.findOrThrow(unitRentTypeId); + await this.prisma.unitRentTypes.delete({ + where: { + id: unitRentTypeId, + }, + }); + return { + success: true, + } as SuccessDTO; + } + + /* + this will either find a record or throw a customized error + */ + async findOrThrow(unitRentTypeId: string): Promise { + const unitRentType = await this.prisma.unitRentTypes.findUnique({ + where: { + id: unitRentTypeId, + }, + }); + + if (!unitRentType) { + throw new NotFoundException( + `unitRentTypeId ${unitRentTypeId} was requested but not found`, + ); + } + + return unitRentType; + } +} diff --git a/backend_new/test/integration/unit-rent-type.e2e-spec.ts b/backend_new/test/integration/unit-rent-type.e2e-spec.ts new file mode 100644 index 0000000000..ad8e4eee83 --- /dev/null +++ b/backend_new/test/integration/unit-rent-type.e2e-spec.ts @@ -0,0 +1,135 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { INestApplication } from '@nestjs/common'; +import * as request from 'supertest'; +import { AppModule } from '../../src/app.module'; +import { PrismaService } from '../../src/services/prisma.service'; +import { unitRentTypeFactory } from '../../prisma/seed-helpers/unit-rent-type-factory'; +import { UnitRentTypeCreate } from '../../src/dtos/unit-rent-types/unit-rent-type-create.dto'; +import { UnitRentTypeUpdate } from '../../src/dtos/unit-rent-types/unit-rent-type-update.dto'; +import { IdDTO } from 'src/dtos/shared/id.dto'; +import { randomUUID } from 'crypto'; + +describe('UnitRentType Controller Tests', () => { + let app: INestApplication; + let prisma: PrismaService; + + beforeAll(async () => { + const moduleFixture: TestingModule = await Test.createTestingModule({ + imports: [AppModule], + }).compile(); + + app = moduleFixture.createNestApplication(); + prisma = moduleFixture.get(PrismaService); + await app.init(); + }); + + it('testing list endpoint', async () => { + const unitRentTypeA = await prisma.unitRentTypes.create({ + data: unitRentTypeFactory(7), + }); + const unitRentTypeB = await prisma.unitRentTypes.create({ + data: unitRentTypeFactory(8), + }); + + const res = await request(app.getHttpServer()) + .get(`/unitRentTypes?`) + .expect(200); + + expect(res.body.length).toBeGreaterThanOrEqual(2); + const unitTypeNames = res.body.map((value) => value.name); + expect(unitTypeNames).toContain(unitRentTypeA.name); + expect(unitTypeNames).toContain(unitRentTypeB.name); + }); + + it("retrieve endpoint with id that doesn't exist should error", async () => { + const id = randomUUID(); + const res = await request(app.getHttpServer()) + .get(`/unitRentTypes/${id}`) + .expect(404); + expect(res.body.message).toEqual( + `unitRentTypeId ${id} was requested but not found`, + ); + }); + + it('testing retrieve endpoint', async () => { + const unitRentTypeA = await prisma.unitRentTypes.create({ + data: unitRentTypeFactory(10), + }); + + const res = await request(app.getHttpServer()) + .get(`/unitRentTypes/${unitRentTypeA.id}`) + .expect(200); + + expect(res.body.name).toEqual(unitRentTypeA.name); + }); + + it('testing create endpoint', async () => { + const name = unitRentTypeFactory(10).name; + const res = await request(app.getHttpServer()) + .post('/unitRentTypes') + .send({ + name: name, + } as UnitRentTypeCreate) + .expect(201); + + expect(res.body.name).toEqual(name); + }); + + it("update endpoint with id that doesn't exist should error", async () => { + const id = randomUUID(); + const res = await request(app.getHttpServer()) + .put(`/unitRentTypes/${id}`) + .send({ + id: id, + name: unitRentTypeFactory(10).name, + } as UnitRentTypeUpdate) + .expect(404); + expect(res.body.message).toEqual( + `unitRentTypeId ${id} was requested but not found`, + ); + }); + + it('testing update endpoint', async () => { + const unitRentTypeA = await prisma.unitRentTypes.create({ + data: unitRentTypeFactory(10), + }); + const name = unitRentTypeFactory(11).name; + const res = await request(app.getHttpServer()) + .put(`/unitRentTypes/${unitRentTypeA.id}`) + .send({ + id: unitRentTypeA.id, + name: name, + } as UnitRentTypeUpdate) + .expect(200); + + expect(res.body.name).toEqual(name); + }); + + it("delete endpoint with id that doesn't exist should error", async () => { + const id = randomUUID(); + const res = await request(app.getHttpServer()) + .delete(`/unitRentTypes`) + .send({ + id: id, + } as IdDTO) + .expect(404); + expect(res.body.message).toEqual( + `unitRentTypeId ${id} was requested but not found`, + ); + }); + + it('testing delete endpoint', async () => { + const unitRentTypeA = await prisma.unitRentTypes.create({ + data: unitRentTypeFactory(16), + }); + + const res = await request(app.getHttpServer()) + .delete(`/unitRentTypes`) + .send({ + id: unitRentTypeA.id, + } as IdDTO) + .expect(200); + + expect(res.body.success).toEqual(true); + }); +}); diff --git a/backend_new/test/unit/services/unit-rent-type.service.spec.ts b/backend_new/test/unit/services/unit-rent-type.service.spec.ts new file mode 100644 index 0000000000..86925650b8 --- /dev/null +++ b/backend_new/test/unit/services/unit-rent-type.service.spec.ts @@ -0,0 +1,228 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { PrismaService } from '../../../src/services/prisma.service'; +import { UnitRentTypeService } from '../../../src/services/unit-rent-type.service'; +import { UnitRentTypeCreate } from '../../../src/dtos/unit-rent-types/unit-rent-type-create.dto'; +import { UnitRentTypeUpdate } from '../../../src/dtos/unit-rent-types/unit-rent-type-update.dto'; +import { randomUUID } from 'crypto'; +import { unitRentTypeFactory } from '../../../prisma/seed-helpers/unit-rent-type-factory'; + +describe('Testing unit rent type service', () => { + let service: UnitRentTypeService; + let prisma: PrismaService; + + const mockUnitRentType = (position: number, date: Date) => { + return { + id: randomUUID(), + name: unitRentTypeFactory(position).name, + createdAt: date, + updatedAt: date, + }; + }; + + const mockUnitRentTypeSet = (numberToCreate: number, date: Date) => { + const toReturn = []; + for (let i = 0; i < numberToCreate; i++) { + toReturn.push(mockUnitRentType(i, date)); + } + return toReturn; + }; + + beforeAll(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [UnitRentTypeService, PrismaService], + }).compile(); + + service = module.get(UnitRentTypeService); + prisma = module.get(PrismaService); + }); + + it('testing list()', async () => { + const date = new Date(); + const mockedValue = mockUnitRentTypeSet(3, date); + prisma.unitRentTypes.findMany = jest.fn().mockResolvedValue(mockedValue); + + expect(await service.list()).toEqual([ + { + id: mockedValue[0].id, + name: unitRentTypeFactory(0).name, + createdAt: date, + updatedAt: date, + }, + { + id: mockedValue[1].id, + name: unitRentTypeFactory(1).name, + createdAt: date, + updatedAt: date, + }, + { + id: mockedValue[2].id, + name: unitRentTypeFactory(2).name, + createdAt: date, + updatedAt: date, + }, + ]); + + expect(prisma.unitRentTypes.findMany).toHaveBeenCalled(); + }); + + it('testing findOne() with id present', async () => { + const date = new Date(); + const mockedValue = mockUnitRentType(3, date); + prisma.unitRentTypes.findUnique = jest.fn().mockResolvedValue(mockedValue); + + expect(await service.findOne('example Id')).toEqual({ + id: mockedValue.id, + name: unitRentTypeFactory(3).name, + createdAt: date, + updatedAt: date, + }); + + expect(prisma.unitRentTypes.findUnique).toHaveBeenCalledWith({ + where: { + id: 'example Id', + }, + }); + }); + + it('testing findOne() with id not present', async () => { + prisma.unitRentTypes.findUnique = jest.fn().mockResolvedValue(null); + + await expect( + async () => await service.findOne('example Id'), + ).rejects.toThrowError(); + + expect(prisma.unitRentTypes.findUnique).toHaveBeenCalledWith({ + where: { + id: 'example Id', + }, + }); + }); + + it('testing create()', async () => { + const date = new Date(); + const mockedValue = mockUnitRentType(3, date); + prisma.unitRentTypes.create = jest.fn().mockResolvedValue(mockedValue); + + const params: UnitRentTypeCreate = { + name: unitRentTypeFactory(3).name, + }; + + expect(await service.create(params)).toEqual({ + id: mockedValue.id, + name: unitRentTypeFactory(3).name, + createdAt: date, + updatedAt: date, + }); + + expect(prisma.unitRentTypes.create).toHaveBeenCalledWith({ + data: { + name: unitRentTypeFactory(3).name, + }, + }); + }); + + it('testing update() existing record found', async () => { + const date = new Date(); + const mockedUnitRentType = mockUnitRentType(3, date); + + prisma.unitRentTypes.findUnique = jest + .fn() + .mockResolvedValue(mockedUnitRentType); + prisma.unitRentTypes.update = jest.fn().mockResolvedValue({ + ...mockedUnitRentType, + name: unitRentTypeFactory(4).name, + }); + + const params: UnitRentTypeUpdate = { + name: unitRentTypeFactory(4).name, + id: mockedUnitRentType.id, + }; + + expect(await service.update(params)).toEqual({ + id: mockedUnitRentType.id, + name: unitRentTypeFactory(4).name, + createdAt: date, + updatedAt: date, + }); + + expect(prisma.unitRentTypes.findUnique).toHaveBeenCalledWith({ + where: { + id: mockedUnitRentType.id, + }, + }); + + expect(prisma.unitRentTypes.update).toHaveBeenCalledWith({ + data: { + name: unitRentTypeFactory(4).name, + }, + where: { + id: mockedUnitRentType.id, + }, + }); + }); + + it('testing update() existing record not found', async () => { + prisma.unitRentTypes.findUnique = jest.fn().mockResolvedValue(null); + prisma.unitRentTypes.update = jest.fn().mockResolvedValue(null); + + const params: UnitRentTypeUpdate = { + name: unitRentTypeFactory(4).name, + id: 'example id', + }; + + await expect( + async () => await service.update(params), + ).rejects.toThrowError(); + + expect(prisma.unitRentTypes.findUnique).toHaveBeenCalledWith({ + where: { + id: 'example id', + }, + }); + }); + + it('testing delete()', async () => { + const date = new Date(); + const mockedValue = mockUnitRentType(3, date); + prisma.unitRentTypes.findUnique = jest.fn().mockResolvedValue(mockedValue); + prisma.unitRentTypes.delete = jest.fn().mockResolvedValue(mockedValue); + + expect(await service.delete('example Id')).toEqual({ + success: true, + }); + + expect(prisma.unitRentTypes.delete).toHaveBeenCalledWith({ + where: { + id: 'example Id', + }, + }); + }); + + it('testing findOrThrow() record not found', async () => { + prisma.unitRentTypes.findUnique = jest.fn().mockResolvedValue(null); + + await expect( + async () => await service.findOrThrow('example id'), + ).rejects.toThrowError(); + + expect(prisma.unitRentTypes.findUnique).toHaveBeenCalledWith({ + where: { + id: 'example id', + }, + }); + }); + + it('testing findOrThrow() record found', async () => { + const date = new Date(); + const mockedValue = mockUnitRentType(3, date); + prisma.unitRentTypes.findUnique = jest.fn().mockResolvedValue(mockedValue); + + expect(await service.findOrThrow('example id')).toEqual(mockedValue); + + expect(prisma.unitRentTypes.findUnique).toHaveBeenCalledWith({ + where: { + id: 'example id', + }, + }); + }); +}); diff --git a/backend_new/types/src/backend-swagger.ts b/backend_new/types/src/backend-swagger.ts index 00955508bf..80a2a2b3ba 100644 --- a/backend_new/types/src/backend-swagger.ts +++ b/backend_new/types/src/backend-swagger.ts @@ -722,6 +722,135 @@ export class UnitAccessibilityPriorityTypesService { } } +export class UnitRentTypesService { + /** + * List unitRentTypes + */ + list(options: IRequestOptions = {}): Promise { + return new Promise((resolve, reject) => { + let url = basePath + '/unitRentTypes'; + + const configs: IRequestConfig = getConfigs( + 'get', + 'application/json', + url, + options, + ); + + /** 适配ios13getèŻ·æ±‚äžć…èźžćžŠbody */ + + axios(configs, resolve, reject); + }); + } + /** + * Create unitRentType + */ + create( + params: { + /** requestBody */ + body?: UnitRentTypeCreate; + } = {} as any, + options: IRequestOptions = {}, + ): Promise { + return new Promise((resolve, reject) => { + let url = basePath + '/unitRentTypes'; + + const configs: IRequestConfig = getConfigs( + 'post', + 'application/json', + url, + options, + ); + + let data = params.body; + + configs.data = data; + + axios(configs, resolve, reject); + }); + } + /** + * Delete unitRentType by id + */ + delete( + params: { + /** requestBody */ + body?: IdDTO; + } = {} as any, + options: IRequestOptions = {}, + ): Promise { + return new Promise((resolve, reject) => { + let url = basePath + '/unitRentTypes'; + + const configs: IRequestConfig = getConfigs( + 'delete', + 'application/json', + url, + options, + ); + + let data = params.body; + + configs.data = data; + + axios(configs, resolve, reject); + }); + } + /** + * Get unitRentType by id + */ + retrieve( + params: { + /** */ + unitRentTypeId: string; + } = {} as any, + options: IRequestOptions = {}, + ): Promise { + return new Promise((resolve, reject) => { + let url = basePath + '/unitRentTypes/{unitRentTypeId}'; + url = url.replace('{unitRentTypeId}', params['unitRentTypeId'] + ''); + + const configs: IRequestConfig = getConfigs( + 'get', + 'application/json', + url, + options, + ); + + /** 适配ios13getèŻ·æ±‚äžć…èźžćžŠbody */ + + axios(configs, resolve, reject); + }); + } + /** + * Update unitRentType + */ + update( + params: { + /** requestBody */ + body?: UnitRentTypeUpdate; + } = {} as any, + options: IRequestOptions = {}, + ): Promise { + return new Promise((resolve, reject) => { + let url = basePath + '/unitRentTypes/{unitRentTypeId}'; + + const configs: IRequestConfig = getConfigs( + 'put', + 'application/json', + url, + options, + ); + + let data = params.body; + + configs.data = data; + + axios(configs, resolve, reject); + }); + } +} + export interface ListingsQueryParams { /** */ page?: number; @@ -826,7 +955,7 @@ export interface UnitAccessibilityPriorityType { updatedAt: Date; /** */ - name: string; + name: UnitAccessibilityPriorityTypeEnum; } export interface MinMaxCurrency { @@ -1089,7 +1218,7 @@ export interface UnitTypeUpdate { export interface UnitAccessibilityPriorityTypeCreate { /** */ - name: string; + name: UnitAccessibilityPriorityTypeEnum; } export interface UnitAccessibilityPriorityTypeUpdate { @@ -1097,7 +1226,34 @@ export interface UnitAccessibilityPriorityTypeUpdate { id: string; /** */ - name: string; + name: UnitAccessibilityPriorityTypeEnum; +} + +export interface UnitRentTypeCreate { + /** */ + name: UnitRentTypeEnum; +} + +export interface UnitRentTypeUpdate { + /** */ + id: string; + + /** */ + name: UnitRentTypeEnum; +} + +export interface UnitRentType { + /** */ + id: string; + + /** */ + createdAt: Date; + + /** */ + updatedAt: Date; + + /** */ + name: UnitRentTypeEnum; } export enum ListingViews { @@ -1158,3 +1314,18 @@ export enum UnitTypeEnum { 'SRO' = 'SRO', 'fiveBdrm' = 'fiveBdrm', } + +export enum UnitAccessibilityPriorityTypeEnum { + 'mobility' = 'mobility', + 'mobilityAndHearing' = 'mobilityAndHearing', + 'hearing' = 'hearing', + 'visual' = 'visual', + 'hearingAndVisual' = 'hearingAndVisual', + 'mobilityAndVisual' = 'mobilityAndVisual', + 'mobilityHearingAndVisual' = 'mobilityHearingAndVisual', +} + +export enum UnitRentTypeEnum { + 'fixed' = 'fixed', + 'percentageOfIncome' = 'percentageOfIncome', +} From 8ae81cedcec6aa1ae45a565a20b362952f0c3dc2 Mon Sep 17 00:00:00 2001 From: Yazeed Loonat Date: Wed, 5 Jul 2023 14:04:11 -0700 Subject: [PATCH 11/57] feat: jurisdiction (#3483) * feat: prisma schema generation * fix: cleaning up removed "models" * fix: updates per emily * fix: caught some schema errors * feat: listing get endpoints * feat: adding DTOs * feat: openapi and swagger updates * fix: pass through DTOs * fix: updates per emily * feat: adding ci jobs * fix: round 2 * fix: indentation problem * fix: ci updates * fix: ci test again * fix: adding maybe missing library? * fix: running yarn install * fix: updates per morgan * fix: ci fix round x * fix: test updates * fix: integration tests and ci * fix: update for tests * fix: fixing scheme so migrations can be created * feat: creating ami-chart endpoints * feat: reserved community type * feat: unit type endpoints * feat: unit accessibility priority type * feat: unit rent type * feat: jurisdiction * fix: updates per morgan * fix: update backend-new circleci job (#3489) * fix: update backend-new circleci job * fix: add cache * fix: add class validator * fix: update list typing * fix: add class transformer * fix: add listing type * fix: one more attempt at typings * fix: update jest config file * fix: add dbsetup * fix: one more attempt * fix: e2e typing fix * fix: turn off diagnostics for e2e * fix: clear db fields between tests * fix: prettier and linting changes * fix: additional linting fixes --------- Co-authored-by: Yazeed Loonat * fix: updates from what I learned * fix: forgot swagger file * fix: updates per morgan * fix: updates per sean/morgan and test updates * fix: updates per sean and morgan * fix: maybe resolving test race case * fix: removing jurisdictionName param * fix: upates per sean and emily * fix: updates per emily * fix: updates to listing test * fix: updates per ami chart * fix: trying to remove seeding for e2e testing * fix: updating test description * fix: updates from ami chart learnings * fix: updates from ami chart learnings * fix: updating client * fix: updates from reserved-community-type learning * fix: test updates * fix: e2e test updates * fix: updates from previous models * fix: test updates * fix: updates per emly --------- Co-authored-by: Morgan Ludtke <42942267+ludtkemorgan@users.noreply.github.com> --- backend_new/src/app.module.ts | 3 + .../controllers/jurisdiction.controller.ts | 99 ++++++ .../jurisdictions/jurisdiction-create.dto.ts | 4 + .../jurisdictions/jurisdiction-update.dto.ts | 8 + ...diction-get.dto.ts => jurisdiction.dto.ts} | 20 +- .../src/dtos/listings/listing-get.dto.ts | 2 +- .../multiselect-question.dto.ts | 2 +- .../src/modules/jurisdiction.module.ts | 12 + .../src/services/jurisdiction.service.ts | 154 ++++++++++ backend_new/test/integration/app.e2e-spec.ts | 6 +- .../test/integration/jurisdiction.e2e-spec.ts | 185 +++++++++++ .../services/jurisdiction.service.spec.ts | 287 ++++++++++++++++++ backend_new/types/src/backend-swagger.ts | 266 ++++++++++++++++ 13 files changed, 1037 insertions(+), 11 deletions(-) create mode 100644 backend_new/src/controllers/jurisdiction.controller.ts create mode 100644 backend_new/src/dtos/jurisdictions/jurisdiction-create.dto.ts create mode 100644 backend_new/src/dtos/jurisdictions/jurisdiction-update.dto.ts rename backend_new/src/dtos/jurisdictions/{jurisdiction-get.dto.ts => jurisdiction.dto.ts} (85%) create mode 100644 backend_new/src/modules/jurisdiction.module.ts create mode 100644 backend_new/src/services/jurisdiction.service.ts create mode 100644 backend_new/test/integration/jurisdiction.e2e-spec.ts create mode 100644 backend_new/test/unit/services/jurisdiction.service.spec.ts diff --git a/backend_new/src/app.module.ts b/backend_new/src/app.module.ts index 55706dab4e..f1dfea3ac7 100644 --- a/backend_new/src/app.module.ts +++ b/backend_new/src/app.module.ts @@ -7,6 +7,7 @@ import { ReservedCommunityTypeModule } from './modules/reserved-community-type.m import { UnitAccessibilityPriorityTypeServiceModule } from './modules/unit-accessibility-priority-type.module'; import { UnitTypeModule } from './modules/unit-type.module'; import { UnitRentTypeModule } from './modules/unit-rent-type.module'; +import { JurisdictionModule } from './modules/jurisdiction.module'; @Module({ imports: [ @@ -16,6 +17,7 @@ import { UnitRentTypeModule } from './modules/unit-rent-type.module'; UnitTypeModule, UnitAccessibilityPriorityTypeServiceModule, UnitRentTypeModule, + JurisdictionModule, ], controllers: [AppController], providers: [AppService], @@ -26,6 +28,7 @@ import { UnitRentTypeModule } from './modules/unit-rent-type.module'; UnitTypeModule, UnitAccessibilityPriorityTypeServiceModule, UnitRentTypeModule, + JurisdictionModule, ], }) export class AppModule {} diff --git a/backend_new/src/controllers/jurisdiction.controller.ts b/backend_new/src/controllers/jurisdiction.controller.ts new file mode 100644 index 0000000000..d5b1a3e185 --- /dev/null +++ b/backend_new/src/controllers/jurisdiction.controller.ts @@ -0,0 +1,99 @@ +import { + Body, + Controller, + Delete, + Get, + Param, + Post, + Put, + UsePipes, + ValidationPipe, +} from '@nestjs/common'; +import { + ApiExtraModels, + ApiOkResponse, + ApiOperation, + ApiTags, +} from '@nestjs/swagger'; +import { JurisdictionService } from '../services/jurisdiction.service'; +import { Jurisdiction } from '../dtos/jurisdictions/jurisdiction.dto'; +import { JurisdictionCreate } from '../dtos/jurisdictions/jurisdiction-create.dto'; +import { JurisdictionUpdate } from '../dtos/jurisdictions/jurisdiction-update.dto'; +import { defaultValidationPipeOptions } from '../utilities/default-validation-pipe-options'; +import { IdDTO } from '../dtos/shared/id.dto'; +import { SuccessDTO } from '../dtos/shared/success.dto'; + +@Controller('jurisdictions') +@ApiTags('jurisdictions') +@UsePipes(new ValidationPipe(defaultValidationPipeOptions)) +@ApiExtraModels(JurisdictionCreate, JurisdictionUpdate, IdDTO) +export class JurisdictionController { + constructor(private readonly jurisdictionService: JurisdictionService) {} + + @Get() + @ApiOperation({ summary: 'List jurisdictions', operationId: 'list' }) + @ApiOkResponse({ type: Jurisdiction, isArray: true }) + async list(): Promise { + return await this.jurisdictionService.list(); + } + + @Get(`:jurisdictionId`) + @ApiOperation({ + summary: 'Get jurisdiction by id', + operationId: 'retrieve', + }) + @ApiOkResponse({ type: Jurisdiction }) + async retrieve( + @Param('jurisdictionId') jurisdictionId: string, + ): Promise { + return this.jurisdictionService.findOne({ jurisdictionId }); + } + + @Get(`byName/:jurisdictionName`) + @ApiOperation({ + summary: 'Get jurisdiction by name', + operationId: 'retrieveByName', + }) + @ApiOkResponse({ type: Jurisdiction }) + async retrieveByName( + @Param('jurisdictionName') jurisdictionName: string, + ): Promise { + return await this.jurisdictionService.findOne({ + jurisdictionName, + }); + } + + @Post() + @ApiOperation({ + summary: 'Create jurisdiction', + operationId: 'create', + }) + @ApiOkResponse({ type: Jurisdiction }) + async create( + @Body() jurisdiction: JurisdictionCreate, + ): Promise { + return await this.jurisdictionService.create(jurisdiction); + } + + @Put(`:jurisdictionId`) + @ApiOperation({ + summary: 'Update jurisdiction', + operationId: 'update', + }) + @ApiOkResponse({ type: Jurisdiction }) + async update( + @Body() jurisdiction: JurisdictionUpdate, + ): Promise { + return await this.jurisdictionService.update(jurisdiction); + } + + @Delete() + @ApiOperation({ + summary: 'Delete jurisdiction by id', + operationId: 'delete', + }) + @ApiOkResponse({ type: SuccessDTO }) + async delete(@Body() dto: IdDTO): Promise { + return await this.jurisdictionService.delete(dto.id); + } +} diff --git a/backend_new/src/dtos/jurisdictions/jurisdiction-create.dto.ts b/backend_new/src/dtos/jurisdictions/jurisdiction-create.dto.ts new file mode 100644 index 0000000000..3ccde8b9d5 --- /dev/null +++ b/backend_new/src/dtos/jurisdictions/jurisdiction-create.dto.ts @@ -0,0 +1,4 @@ +import { OmitType } from '@nestjs/swagger'; +import { JurisdictionUpdate } from './jurisdiction-update.dto'; + +export class JurisdictionCreate extends OmitType(JurisdictionUpdate, ['id']) {} diff --git a/backend_new/src/dtos/jurisdictions/jurisdiction-update.dto.ts b/backend_new/src/dtos/jurisdictions/jurisdiction-update.dto.ts new file mode 100644 index 0000000000..a2a2f4077e --- /dev/null +++ b/backend_new/src/dtos/jurisdictions/jurisdiction-update.dto.ts @@ -0,0 +1,8 @@ +import { OmitType } from '@nestjs/swagger'; +import { Jurisdiction } from './jurisdiction.dto'; + +export class JurisdictionUpdate extends OmitType(Jurisdiction, [ + 'createdAt', + 'updatedAt', + 'multiselectQuestions', +]) {} diff --git a/backend_new/src/dtos/jurisdictions/jurisdiction-get.dto.ts b/backend_new/src/dtos/jurisdictions/jurisdiction.dto.ts similarity index 85% rename from backend_new/src/dtos/jurisdictions/jurisdiction-get.dto.ts rename to backend_new/src/dtos/jurisdictions/jurisdiction.dto.ts index 906d12b42a..bafe347df9 100644 --- a/backend_new/src/dtos/jurisdictions/jurisdiction-get.dto.ts +++ b/backend_new/src/dtos/jurisdictions/jurisdiction.dto.ts @@ -12,18 +12,21 @@ import { import { ValidationsGroupsEnum } from '../../enums/shared/validation-groups-enum'; import { LanguagesEnum } from '@prisma/client'; import { Expose, Type } from 'class-transformer'; -import { MultiselectQuestion } from '../multiselect-questions/multiselect-question.dto'; +import { ApiProperty } from '@nestjs/swagger'; +import { IdDTO } from '../shared/id.dto'; export class Jurisdiction extends AbstractDTO { @Expose() @IsString({ groups: [ValidationsGroupsEnum.default] }) @MaxLength(256, { groups: [ValidationsGroupsEnum.default] }) @IsDefined({ groups: [ValidationsGroupsEnum.default] }) + @ApiProperty() name: string; @Expose() @IsString({ groups: [ValidationsGroupsEnum.default] }) - notificationsSignUpURL?: string | null; + @ApiProperty() + notificationsSignUpUrl?: string | null; @Expose() @IsArray({ groups: [ValidationsGroupsEnum.default] }) @@ -33,44 +36,53 @@ export class Jurisdiction extends AbstractDTO { each: true, }) @IsDefined({ groups: [ValidationsGroupsEnum.default] }) + @ApiProperty() languages: LanguagesEnum[]; @Expose() @ValidateNested({ groups: [ValidationsGroupsEnum.default] }) - @Type(() => MultiselectQuestion) + @Type(() => IdDTO) @IsDefined({ groups: [ValidationsGroupsEnum.default] }) - multiselectQuestions: MultiselectQuestion[]; + @ApiProperty() + multiselectQuestions: IdDTO[]; @Expose() @IsString({ groups: [ValidationsGroupsEnum.default] }) + @ApiProperty() partnerTerms?: string | null; @Expose() @IsString({ groups: [ValidationsGroupsEnum.default] }) @IsDefined({ groups: [ValidationsGroupsEnum.default] }) + @ApiProperty() publicUrl: string; @Expose() @IsString({ groups: [ValidationsGroupsEnum.default] }) @IsDefined({ groups: [ValidationsGroupsEnum.default] }) + @ApiProperty() emailFromAddress: string; @Expose() @IsString({ groups: [ValidationsGroupsEnum.default] }) @IsDefined({ groups: [ValidationsGroupsEnum.default] }) + @ApiProperty() rentalAssistanceDefault: string; @Expose() @IsBoolean({ groups: [ValidationsGroupsEnum.default] }) + @ApiProperty() enablePartnerSettings?: boolean | null; @Expose() @IsBoolean({ groups: [ValidationsGroupsEnum.default] }) @IsDefined({ groups: [ValidationsGroupsEnum.default] }) + @ApiProperty() enableAccessibilityFeatures: boolean | null; @Expose() @IsBoolean({ groups: [ValidationsGroupsEnum.default] }) @IsDefined({ groups: [ValidationsGroupsEnum.default] }) + @ApiProperty() enableUtilitiesIncluded: boolean | null; } diff --git a/backend_new/src/dtos/listings/listing-get.dto.ts b/backend_new/src/dtos/listings/listing-get.dto.ts index c38fea37e1..94d203c4c6 100644 --- a/backend_new/src/dtos/listings/listing-get.dto.ts +++ b/backend_new/src/dtos/listings/listing-get.dto.ts @@ -25,7 +25,7 @@ import { ApplicationMethod } from '../application-methods/application-method-get import { Asset } from '../assets/asset-get.dto'; import { ListingEvent } from './listing-event.dto'; import { Address } from '../addresses/address-get.dto'; -import { Jurisdiction } from '../jurisdictions/jurisdiction-get.dto'; +import { Jurisdiction } from '../jurisdictions/jurisdiction.dto'; import { ReservedCommunityType } from '../reserved-community-types/reserved-community-type.dto'; import { ListingImage } from './listing-image.dto'; import { ListingFeatures } from './listing-feature.dto'; diff --git a/backend_new/src/dtos/multiselect-questions/multiselect-question.dto.ts b/backend_new/src/dtos/multiselect-questions/multiselect-question.dto.ts index dc558f0670..962ff25c9e 100644 --- a/backend_new/src/dtos/multiselect-questions/multiselect-question.dto.ts +++ b/backend_new/src/dtos/multiselect-questions/multiselect-question.dto.ts @@ -12,7 +12,7 @@ import { ApiProperty } from '@nestjs/swagger'; import { AbstractDTO } from '../shared/abstract.dto'; import { ListingMultiselectQuestion } from '../listings/listing-multiselect-question.dto'; import { MultiselectQuestionsApplicationSectionEnum } from '@prisma/client'; -import { Jurisdiction } from '../jurisdictions/jurisdiction-get.dto'; +import { Jurisdiction } from '../jurisdictions/jurisdiction.dto'; import { MultiselectLink } from './multiselect-link.dto'; import { MultiselectOption } from './multiselect-option.dto'; diff --git a/backend_new/src/modules/jurisdiction.module.ts b/backend_new/src/modules/jurisdiction.module.ts new file mode 100644 index 0000000000..d0d54953bd --- /dev/null +++ b/backend_new/src/modules/jurisdiction.module.ts @@ -0,0 +1,12 @@ +import { Module } from '@nestjs/common'; +import { JurisdictionController } from '../controllers/jurisdiction.controller'; +import { JurisdictionService } from '../services/jurisdiction.service'; +import { PrismaService } from '../services/prisma.service'; + +@Module({ + imports: [], + controllers: [JurisdictionController], + providers: [JurisdictionService, PrismaService], + exports: [JurisdictionService, PrismaService], +}) +export class JurisdictionModule {} diff --git a/backend_new/src/services/jurisdiction.service.ts b/backend_new/src/services/jurisdiction.service.ts new file mode 100644 index 0000000000..9bb9538a4b --- /dev/null +++ b/backend_new/src/services/jurisdiction.service.ts @@ -0,0 +1,154 @@ +import { + Injectable, + BadRequestException, + NotFoundException, +} from '@nestjs/common'; +import { PrismaService } from './prisma.service'; +import { Jurisdiction } from '../dtos/jurisdictions/jurisdiction.dto'; +import { JurisdictionCreate } from '../dtos/jurisdictions/jurisdiction-create.dto'; +import { mapTo } from '../utilities/mapTo'; +import { SuccessDTO } from '../dtos/shared/success.dto'; +import { Prisma } from '@prisma/client'; +import { JurisdictionUpdate } from '../dtos/jurisdictions/jurisdiction-update.dto'; + +const view: Prisma.JurisdictionsInclude = { + multiselectQuestions: true, +}; +/* + this is the service for jurisdictions + it handles all the backend's business logic for reading/writing/deleting jurisdiction data +*/ +@Injectable() +export class JurisdictionService { + constructor(private prisma: PrismaService) {} + + /* + this will get a set of jurisdictions given the params passed in + */ + async list(): Promise { + const rawJurisdictions = await this.prisma.jurisdictions.findMany({ + include: view, + }); + return mapTo(Jurisdiction, rawJurisdictions); + } + + /* + this will build the where clause for findOne() + */ + buildWhere({ + jurisdictionId, + jurisdictionName, + }: { + jurisdictionId?: string; + jurisdictionName?: string; + }): Prisma.JurisdictionsWhereInput { + const toReturn: Prisma.JurisdictionsWhereInput = {}; + if (jurisdictionId) { + toReturn.id = { + equals: jurisdictionId, + }; + } else if (jurisdictionName) { + toReturn.name = { + equals: jurisdictionName, + }; + } + return toReturn; + } + + /* + this will return 1 jurisdiction or error + */ + async findOne(condition: { + jurisdictionId?: string; + jurisdictionName?: string; + }): Promise { + if (!condition.jurisdictionId && !condition.jurisdictionName) { + throw new BadRequestException( + 'a jurisdiction id or jurisdiction name must be provided', + ); + } + + const rawJurisdiction = await this.prisma.jurisdictions.findFirst({ + where: this.buildWhere(condition), + include: view, + }); + + if (!rawJurisdiction) { + throw new NotFoundException( + `jurisdiction ${ + condition.jurisdictionId || condition.jurisdictionName + } was requested but not found`, + ); + } + + return mapTo(Jurisdiction, rawJurisdiction); + } + + /* + this will create a jurisdiction + */ + async create(incomingData: JurisdictionCreate): Promise { + const rawResult = await this.prisma.jurisdictions.create({ + data: { + ...incomingData, + }, + include: view, + }); + + return mapTo(Jurisdiction, rawResult); + } + + /* + this will update a jurisdiction's name or items field + if no jurisdiction has the id of the incoming argument an error is thrown + */ + async update(incomingData: JurisdictionUpdate): Promise { + await this.findOrThrow(incomingData.id); + + const rawResults = await this.prisma.jurisdictions.update({ + data: { + ...incomingData, + id: undefined, + }, + where: { + id: incomingData.id, + }, + include: view, + }); + return mapTo(Jurisdiction, rawResults); + } + + /* + this will delete a jurisdiction + */ + async delete(jurisdictionId: string): Promise { + await this.findOrThrow(jurisdictionId); + await this.prisma.jurisdictions.delete({ + where: { + id: jurisdictionId, + }, + }); + return { + success: true, + } as SuccessDTO; + } + + /* + this will either find a record or throw a customized error + */ + async findOrThrow(jurisdictionId: string): Promise { + const jurisdiction = await this.prisma.jurisdictions.findFirst({ + where: { + id: jurisdictionId, + }, + }); + + if (!jurisdiction) { + throw new NotFoundException( + `jurisdictionId ${jurisdictionId} was requested but not found`, + ); + } + + return true; + } +} diff --git a/backend_new/test/integration/app.e2e-spec.ts b/backend_new/test/integration/app.e2e-spec.ts index 262b9396fc..b8d824ad93 100644 --- a/backend_new/test/integration/app.e2e-spec.ts +++ b/backend_new/test/integration/app.e2e-spec.ts @@ -6,7 +6,7 @@ import { AppModule } from '../../src/app.module'; describe('AppController (e2e)', () => { let app: INestApplication; - beforeEach(async () => { + beforeAll(async () => { const moduleFixture: TestingModule = await Test.createTestingModule({ imports: [AppModule], }).compile(); @@ -15,10 +15,6 @@ describe('AppController (e2e)', () => { await app.init(); }); - afterEach(async () => { - await app.close(); - }); - it('/ (GET)', () => { return request(app.getHttpServer()) .get('/') diff --git a/backend_new/test/integration/jurisdiction.e2e-spec.ts b/backend_new/test/integration/jurisdiction.e2e-spec.ts new file mode 100644 index 0000000000..09aab3fbdc --- /dev/null +++ b/backend_new/test/integration/jurisdiction.e2e-spec.ts @@ -0,0 +1,185 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { INestApplication } from '@nestjs/common'; +import * as request from 'supertest'; +import { AppModule } from '../../src/app.module'; +import { PrismaService } from '../../src/services/prisma.service'; +import { jurisdictionFactory } from '../../prisma/seed-helpers/jurisdiction-factory'; +import { JurisdictionCreate } from '../../src/dtos/jurisdictions/jurisdiction-create.dto'; +import { JurisdictionUpdate } from '../../src/dtos/jurisdictions/jurisdiction-update.dto'; +import { IdDTO } from 'src/dtos/shared/id.dto'; +import { LanguagesEnum } from '@prisma/client'; +import { randomUUID } from 'crypto'; + +describe('Jurisdiction Controller Tests', () => { + let app: INestApplication; + let prisma: PrismaService; + + beforeAll(async () => { + const moduleFixture: TestingModule = await Test.createTestingModule({ + imports: [AppModule], + }).compile(); + + app = moduleFixture.createNestApplication(); + prisma = moduleFixture.get(PrismaService); + await app.init(); + }); + + it('testing list endpoint', async () => { + const jurisdictionA = await prisma.jurisdictions.create({ + data: jurisdictionFactory(70), + }); + const jurisdictionB = await prisma.jurisdictions.create({ + data: jurisdictionFactory(80), + }); + + const res = await request(app.getHttpServer()) + .get(`/jurisdictions?`) + .expect(200); + + expect(res.body.length).toBeGreaterThanOrEqual(2); + const jurisdictions = res.body.map((value) => value.name); + expect(jurisdictions).toContain(jurisdictionA.name); + expect(jurisdictions).toContain(jurisdictionB.name); + }); + + it("retrieve endpoint with id that doesn't exist should error", async () => { + const id = randomUUID(); + const res = await request(app.getHttpServer()) + .get(`/jurisdictions/${id}`) + .expect(404); + expect(res.body.message).toEqual( + `jurisdiction ${id} was requested but not found`, + ); + }); + + it('testing retrieve endpoint', async () => { + const jurisdictionA = await prisma.jurisdictions.create({ + data: jurisdictionFactory(101), + }); + + const res = await request(app.getHttpServer()) + .get(`/jurisdictions/${jurisdictionA.id}`) + .expect(200); + + expect(res.body.name).toEqual(jurisdictionA.name); + }); + + it("retrieve endpoint with name that doesn't exist should error", async () => { + const name = 'a nonexistant name'; + const res = await request(app.getHttpServer()) + .get(`/jurisdictions/byName/${name}`) + .expect(404); + expect(res.body.message).toEqual( + `jurisdiction ${name} was requested but not found`, + ); + }); + + it('testing retrieveByName endpoint', async () => { + const jurisdictionA = await prisma.jurisdictions.create({ + data: jurisdictionFactory(110), + }); + + const res = await request(app.getHttpServer()) + .get(`/jurisdictions/byName/${jurisdictionA.name}`) + .expect(200); + + expect(res.body.name).toEqual(jurisdictionA.name); + }); + + it('testing create endpoint', async () => { + const res = await request(app.getHttpServer()) + .post('/jurisdictions') + .send({ + name: 'new jurisdiction', + notificationsSignUpUrl: `notificationsSignUpUrl: 10`, + languages: [LanguagesEnum.en], + partnerTerms: `partnerTerms: 10`, + publicUrl: `publicUrl: 10`, + emailFromAddress: `emailFromAddress: 10`, + rentalAssistanceDefault: `rentalAssistanceDefault: 10`, + enablePartnerSettings: true, + enableAccessibilityFeatures: true, + enableUtilitiesIncluded: true, + } as JurisdictionCreate) + .expect(201); + + expect(res.body.name).toEqual('new jurisdiction'); + }); + + it("update endpoint with id that doesn't exist should error", async () => { + const id = randomUUID(); + const res = await request(app.getHttpServer()) + .put(`/jurisdictions/${id}`) + .send({ + id: id, + name: 'updated name: 10', + notificationsSignUpUrl: `notificationsSignUpUrl: 10`, + languages: [LanguagesEnum.en], + partnerTerms: `partnerTerms: 10`, + publicUrl: `updated publicUrl: 11`, + emailFromAddress: `emailFromAddress: 10`, + rentalAssistanceDefault: `rentalAssistanceDefault: 10`, + enablePartnerSettings: true, + enableAccessibilityFeatures: true, + enableUtilitiesIncluded: true, + } as JurisdictionUpdate) + .expect(404); + expect(res.body.message).toEqual( + `jurisdictionId ${id} was requested but not found`, + ); + }); + + it('testing update endpoint', async () => { + const jurisdictionA = await prisma.jurisdictions.create({ + data: jurisdictionFactory(120), + }); + + const res = await request(app.getHttpServer()) + .put(`/jurisdictions/${jurisdictionA.id}`) + .send({ + id: jurisdictionA.id, + name: 'updated name: 10', + notificationsSignUpUrl: `notificationsSignUpUrl: 10`, + languages: [LanguagesEnum.en], + partnerTerms: `partnerTerms: 10`, + publicUrl: `updated publicUrl: 10`, + emailFromAddress: `emailFromAddress: 10`, + rentalAssistanceDefault: `rentalAssistanceDefault: 10`, + enablePartnerSettings: true, + enableAccessibilityFeatures: true, + enableUtilitiesIncluded: true, + } as JurisdictionUpdate) + .expect(200); + + expect(res.body.name).toEqual('updated name: 10'); + expect(res.body.publicUrl).toEqual('updated publicUrl: 10'); + }); + + it("delete endpoint with id that doesn't exist should error", async () => { + const id = randomUUID(); + const res = await request(app.getHttpServer()) + .delete(`/jurisdictions`) + .send({ + id: id, + } as IdDTO) + .expect(404); + expect(res.body.message).toEqual( + `jurisdictionId ${id} was requested but not found`, + ); + }); + + it('testing delete endpoint', async () => { + const jurisdictionA = await prisma.jurisdictions.create({ + data: jurisdictionFactory(160), + }); + + const res = await request(app.getHttpServer()) + .delete(`/jurisdictions`) + .send({ + id: jurisdictionA.id, + } as IdDTO) + .expect(200); + + expect(res.body.success).toEqual(true); + }); +}); diff --git a/backend_new/test/unit/services/jurisdiction.service.spec.ts b/backend_new/test/unit/services/jurisdiction.service.spec.ts new file mode 100644 index 0000000000..9331652f81 --- /dev/null +++ b/backend_new/test/unit/services/jurisdiction.service.spec.ts @@ -0,0 +1,287 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { PrismaService } from '../../../src/services/prisma.service'; +import { JurisdictionService } from '../../../src/services/jurisdiction.service'; +import { JurisdictionCreate } from '../../../src/dtos/jurisdictions/jurisdiction-create.dto'; +import { JurisdictionUpdate } from '../../../src/dtos/jurisdictions/jurisdiction-update.dto'; +import { LanguagesEnum } from '@prisma/client'; +import { randomUUID } from 'crypto'; + +describe('Testing jurisdiction service', () => { + let service: JurisdictionService; + let prisma: PrismaService; + + const mockJurisdiction = (position: number, date: Date) => { + return { + id: randomUUID(), + createdAt: date, + updatedAt: date, + name: `jurisdiction ${position}`, + notificationsSignUpUrl: `notificationsSignUpUrl: ${position}`, + languages: [LanguagesEnum.en], + partnerTerms: `partnerTerms: ${position}`, + publicUrl: `publicUrl: ${position}`, + emailFromAddress: `emailFromAddress: ${position}`, + rentalAssistanceDefault: `rentalAssistanceDefault: ${position}`, + enablePartnerSettings: true, + enableAccessibilityFeatures: true, + enableUtilitiesIncluded: true, + }; + }; + + const mockJurisdictionSet = (numberToCreate: number, date: Date) => { + const toReturn = []; + for (let i = 0; i < numberToCreate; i++) { + toReturn.push(mockJurisdiction(i, date)); + } + return toReturn; + }; + + beforeAll(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [JurisdictionService, PrismaService], + }).compile(); + + service = module.get(JurisdictionService); + prisma = module.get(PrismaService); + }); + + it('testing list()', async () => { + const date = new Date(); + const mockedValue = mockJurisdictionSet(3, date); + prisma.jurisdictions.findMany = jest.fn().mockResolvedValue(mockedValue); + + expect(await service.list()).toEqual(mockedValue); + + expect(prisma.jurisdictions.findMany).toHaveBeenCalledWith({ + include: { + multiselectQuestions: true, + }, + }); + }); + + it('testing findOne() with id present', async () => { + const date = new Date(); + const mockedValue = mockJurisdiction(3, date); + prisma.jurisdictions.findFirst = jest.fn().mockResolvedValue(mockedValue); + + expect(await service.findOne({ jurisdictionId: 'example Id' })).toEqual( + mockedValue, + ); + + expect(prisma.jurisdictions.findFirst).toHaveBeenCalledWith({ + where: { + id: { + equals: 'example Id', + }, + }, + include: { + multiselectQuestions: true, + }, + }); + }); + + it('testing findOne() with name present', async () => { + const date = new Date(); + const mockedValue = mockJurisdiction(3, date); + prisma.jurisdictions.findFirst = jest.fn().mockResolvedValue(mockedValue); + + expect(await service.findOne({ jurisdictionName: 'example Id' })).toEqual( + mockedValue, + ); + + expect(prisma.jurisdictions.findFirst).toHaveBeenCalledWith({ + where: { + name: { + equals: 'example Id', + }, + }, + include: { + multiselectQuestions: true, + }, + }); + }); + + it('testing findOne() with id not present', async () => { + prisma.jurisdictions.findFirst = jest.fn().mockResolvedValue(null); + + await expect( + async () => await service.findOne({ jurisdictionId: 'example Id' }), + ).rejects.toThrowError( + 'jurisdiction example Id was requested but not found', + ); + + expect(prisma.jurisdictions.findFirst).toHaveBeenCalledWith({ + where: { + id: { + equals: 'example Id', + }, + }, + include: { + multiselectQuestions: true, + }, + }); + }); + + it('testing create()', async () => { + const date = new Date(); + const mockedValue = mockJurisdiction(3, date); + prisma.jurisdictions.create = jest.fn().mockResolvedValue(mockedValue); + + const params: JurisdictionCreate = { + name: 'jurisdiction 3', + notificationsSignUpUrl: `notificationsSignUpUrl: 3`, + languages: [LanguagesEnum.en], + partnerTerms: `partnerTerms: 3`, + publicUrl: `publicUrl: 3`, + emailFromAddress: `emailFromAddress: 3`, + rentalAssistanceDefault: `rentalAssistanceDefault: 3`, + enablePartnerSettings: true, + enableAccessibilityFeatures: true, + enableUtilitiesIncluded: true, + }; + + expect(await service.create(params)).toEqual(mockedValue); + + expect(prisma.jurisdictions.create).toHaveBeenCalledWith({ + data: { + name: 'jurisdiction 3', + notificationsSignUpUrl: `notificationsSignUpUrl: 3`, + languages: [LanguagesEnum.en], + partnerTerms: `partnerTerms: 3`, + publicUrl: `publicUrl: 3`, + emailFromAddress: `emailFromAddress: 3`, + rentalAssistanceDefault: `rentalAssistanceDefault: 3`, + enablePartnerSettings: true, + enableAccessibilityFeatures: true, + enableUtilitiesIncluded: true, + }, + include: { + multiselectQuestions: true, + }, + }); + }); + + it('testing update() existing record found', async () => { + const date = new Date(); + + const mockedJurisdiction = mockJurisdiction(3, date); + + prisma.jurisdictions.findFirst = jest + .fn() + .mockResolvedValue(mockedJurisdiction); + prisma.jurisdictions.update = jest.fn().mockResolvedValue({ + ...mockedJurisdiction, + name: 'updated jurisdiction 3', + }); + + const params: JurisdictionUpdate = { + name: 'updated jurisdiction 3', + id: mockedJurisdiction.id, + notificationsSignUpUrl: `notificationsSignUpUrl: 3`, + languages: [LanguagesEnum.en], + partnerTerms: `partnerTerms: 3`, + publicUrl: `publicUrl: 3`, + emailFromAddress: `emailFromAddress: 3`, + rentalAssistanceDefault: `rentalAssistanceDefault: 3`, + enablePartnerSettings: true, + enableAccessibilityFeatures: true, + enableUtilitiesIncluded: true, + }; + + expect(await service.update(params)).toEqual({ + id: mockedJurisdiction.id, + name: `updated jurisdiction 3`, + notificationsSignUpUrl: `notificationsSignUpUrl: 3`, + languages: [LanguagesEnum.en], + partnerTerms: `partnerTerms: 3`, + publicUrl: `publicUrl: 3`, + emailFromAddress: `emailFromAddress: 3`, + rentalAssistanceDefault: `rentalAssistanceDefault: 3`, + enablePartnerSettings: true, + enableAccessibilityFeatures: true, + enableUtilitiesIncluded: true, + createdAt: date, + updatedAt: date, + }); + + expect(prisma.jurisdictions.findFirst).toHaveBeenCalledWith({ + where: { + id: mockedJurisdiction.id, + }, + }); + + expect(prisma.jurisdictions.update).toHaveBeenCalledWith({ + data: { + name: 'updated jurisdiction 3', + notificationsSignUpUrl: `notificationsSignUpUrl: 3`, + languages: [LanguagesEnum.en], + partnerTerms: `partnerTerms: 3`, + publicUrl: `publicUrl: 3`, + emailFromAddress: `emailFromAddress: 3`, + rentalAssistanceDefault: `rentalAssistanceDefault: 3`, + enablePartnerSettings: true, + enableAccessibilityFeatures: true, + enableUtilitiesIncluded: true, + }, + where: { + id: mockedJurisdiction.id, + }, + include: { + multiselectQuestions: true, + }, + }); + }); + + it('testing update() existing record not found', async () => { + prisma.jurisdictions.findFirst = jest.fn().mockResolvedValue(null); + prisma.jurisdictions.update = jest.fn().mockResolvedValue(null); + + const params: JurisdictionUpdate = { + name: 'example jurisdiction', + id: 'example id', + notificationsSignUpUrl: `notificationsSignUpUrl: 3`, + languages: [LanguagesEnum.en], + partnerTerms: `partnerTerms: 3`, + publicUrl: `publicUrl: 3`, + emailFromAddress: `emailFromAddress: 3`, + rentalAssistanceDefault: `rentalAssistanceDefault: 3`, + enablePartnerSettings: true, + enableAccessibilityFeatures: true, + enableUtilitiesIncluded: true, + }; + + await expect(async () => await service.update(params)).rejects.toThrowError( + 'jurisdictionId example id was requested but not found', + ); + + expect(prisma.jurisdictions.findFirst).toHaveBeenCalledWith({ + where: { + id: 'example id', + }, + }); + }); + + it('testing delete()', async () => { + const date = new Date(); + const mockedValue = mockJurisdiction(3, date); + + prisma.jurisdictions.findFirst = jest.fn().mockResolvedValue(mockedValue); + prisma.jurisdictions.delete = jest.fn().mockResolvedValue(mockedValue); + + expect(await service.delete('example Id')).toEqual({ + success: true, + }); + + expect(prisma.jurisdictions.delete).toHaveBeenCalledWith({ + where: { + id: 'example Id', + }, + }); + + expect(prisma.jurisdictions.delete).toHaveBeenCalledWith({ + where: { + id: 'example Id', + }, + }); + }); +}); diff --git a/backend_new/types/src/backend-swagger.ts b/backend_new/types/src/backend-swagger.ts index 80a2a2b3ba..f9380f7a27 100644 --- a/backend_new/types/src/backend-swagger.ts +++ b/backend_new/types/src/backend-swagger.ts @@ -851,6 +851,161 @@ export class UnitRentTypesService { } } +export class JurisdictionsService { + /** + * List jurisdictions + */ + list(options: IRequestOptions = {}): Promise { + return new Promise((resolve, reject) => { + let url = basePath + '/jurisdictions'; + + const configs: IRequestConfig = getConfigs( + 'get', + 'application/json', + url, + options, + ); + + /** 适配ios13getèŻ·æ±‚äžć…èźžćžŠbody */ + + axios(configs, resolve, reject); + }); + } + /** + * Create jurisdiction + */ + create( + params: { + /** requestBody */ + body?: JurisdictionCreate; + } = {} as any, + options: IRequestOptions = {}, + ): Promise { + return new Promise((resolve, reject) => { + let url = basePath + '/jurisdictions'; + + const configs: IRequestConfig = getConfigs( + 'post', + 'application/json', + url, + options, + ); + + let data = params.body; + + configs.data = data; + + axios(configs, resolve, reject); + }); + } + /** + * Delete jurisdiction by id + */ + delete( + params: { + /** requestBody */ + body?: IdDTO; + } = {} as any, + options: IRequestOptions = {}, + ): Promise { + return new Promise((resolve, reject) => { + let url = basePath + '/jurisdictions'; + + const configs: IRequestConfig = getConfigs( + 'delete', + 'application/json', + url, + options, + ); + + let data = params.body; + + configs.data = data; + + axios(configs, resolve, reject); + }); + } + /** + * Get jurisdiction by id + */ + retrieve( + params: { + /** */ + jurisdictionId: string; + } = {} as any, + options: IRequestOptions = {}, + ): Promise { + return new Promise((resolve, reject) => { + let url = basePath + '/jurisdictions/{jurisdictionId}'; + url = url.replace('{jurisdictionId}', params['jurisdictionId'] + ''); + + const configs: IRequestConfig = getConfigs( + 'get', + 'application/json', + url, + options, + ); + + /** 适配ios13getèŻ·æ±‚äžć…èźžćžŠbody */ + + axios(configs, resolve, reject); + }); + } + /** + * Update jurisdiction + */ + update( + params: { + /** requestBody */ + body?: JurisdictionUpdate; + } = {} as any, + options: IRequestOptions = {}, + ): Promise { + return new Promise((resolve, reject) => { + let url = basePath + '/jurisdictions/{jurisdictionId}'; + + const configs: IRequestConfig = getConfigs( + 'put', + 'application/json', + url, + options, + ); + + let data = params.body; + + configs.data = data; + + axios(configs, resolve, reject); + }); + } + /** + * Get jurisdiction by name + */ + retrieveByName( + params: { + /** */ + jurisdictionName: string; + } = {} as any, + options: IRequestOptions = {}, + ): Promise { + return new Promise((resolve, reject) => { + let url = basePath + '/jurisdictions/byName/{jurisdictionName}'; + url = url.replace('{jurisdictionName}', params['jurisdictionName'] + ''); + + const configs: IRequestConfig = getConfigs( + 'get', + 'application/json', + url, + options, + ); + + /** 适配ios13getèŻ·æ±‚äžć…èźžćžŠbody */ + + axios(configs, resolve, reject); + }); + } +} + export interface ListingsQueryParams { /** */ page?: number; @@ -1256,6 +1411,117 @@ export interface UnitRentType { name: UnitRentTypeEnum; } +export interface JurisdictionCreate { + /** */ + name: string; + + /** */ + notificationsSignUpUrl: string; + + /** */ + languages: string[]; + + /** */ + partnerTerms: string; + + /** */ + publicUrl: string; + + /** */ + emailFromAddress: string; + + /** */ + rentalAssistanceDefault: string; + + /** */ + enablePartnerSettings: boolean; + + /** */ + enableAccessibilityFeatures: boolean; + + /** */ + enableUtilitiesIncluded: boolean; +} + +export interface JurisdictionUpdate { + /** */ + id: string; + + /** */ + name: string; + + /** */ + notificationsSignUpUrl: string; + + /** */ + languages: string[]; + + /** */ + partnerTerms: string; + + /** */ + publicUrl: string; + + /** */ + emailFromAddress: string; + + /** */ + rentalAssistanceDefault: string; + + /** */ + enablePartnerSettings: boolean; + + /** */ + enableAccessibilityFeatures: boolean; + + /** */ + enableUtilitiesIncluded: boolean; +} + +export interface Jurisdiction { + /** */ + id: string; + + /** */ + createdAt: Date; + + /** */ + updatedAt: Date; + + /** */ + name: string; + + /** */ + notificationsSignUpUrl: string; + + /** */ + languages: string[]; + + /** */ + multiselectQuestions: string[]; + + /** */ + partnerTerms: string; + + /** */ + publicUrl: string; + + /** */ + emailFromAddress: string; + + /** */ + rentalAssistanceDefault: string; + + /** */ + enablePartnerSettings: boolean; + + /** */ + enableAccessibilityFeatures: boolean; + + /** */ + enableUtilitiesIncluded: boolean; +} + export enum ListingViews { 'fundamentals' = 'fundamentals', 'base' = 'base', From 6f1f09df860735f4b6e78088863a0aa7f293f16d Mon Sep 17 00:00:00 2001 From: Morgan Ludtke <42942267+ludtkemorgan@users.noreply.github.com> Date: Wed, 12 Jul 2023 10:02:32 -0500 Subject: [PATCH 12/57] fix: add better seeded data (#3524) * fix: add better seeded data * fix: remove temporary listing and unit factory * fix: update readme * fix: add better addresses --- backend_new/README.md | 1 + backend_new/package.json | 12 +- backend_new/prisma/seed-dev.ts | 68 ++ .../prisma/seed-helpers/address-factory.ts | 72 ++ .../prisma/seed-helpers/ami-chart-factory.ts | 20 +- .../seed-helpers/jurisdiction-factory.ts | 16 +- .../prisma/seed-helpers/listing-factory.ts | 517 ++++--------- .../multiselect-question-factory.ts | 52 ++ .../reserved-community-type-factory.ts | 49 +- ...nit-accessibility-priority-type-factory.ts | 42 +- .../prisma/seed-helpers/unit-factory.ts | 75 ++ .../seed-helpers/unit-rent-type-factory.ts | 10 +- .../prisma/seed-helpers/unit-type-factory.ts | 52 +- .../prisma/seed-helpers/user-factory.ts | 19 + .../prisma/seed-helpers/word-generator.ts | 221 ++++++ backend_new/prisma/seed-staging.ts | 693 ++++++++++++++++++ backend_new/prisma/seed.ts | 87 +-- .../src/services/translation.service.ts | 2 +- .../test/integration/ami-chart.e2e-spec.ts | 8 +- .../test/integration/jurisdiction.e2e-spec.ts | 12 +- .../test/integration/listing.e2e-spec.ts | 45 +- .../reserved-community-type.e2e-spec.ts | 30 +- ...it-accessibility-priority-type.e2e-spec.ts | 18 +- .../integration/unit-rent-type.e2e-spec.ts | 16 +- .../test/integration/unit-type.e2e-spec.ts | 42 +- .../unit/services/listing.service.spec.ts | 4 +- ...ccessibility-priority-type.service.spec.ts | 84 ++- .../services/unit-rent-type.service.spec.ts | 64 +- .../unit/services/unit-type.service.spec.ts | 28 +- backend_new/yarn.lock | 31 +- 30 files changed, 1710 insertions(+), 680 deletions(-) create mode 100644 backend_new/prisma/seed-dev.ts create mode 100644 backend_new/prisma/seed-helpers/address-factory.ts create mode 100644 backend_new/prisma/seed-helpers/multiselect-question-factory.ts create mode 100644 backend_new/prisma/seed-helpers/unit-factory.ts create mode 100644 backend_new/prisma/seed-helpers/user-factory.ts create mode 100644 backend_new/prisma/seed-helpers/word-generator.ts create mode 100644 backend_new/prisma/seed-staging.ts diff --git a/backend_new/README.md b/backend_new/README.md index 0610ad671c..6c3d273b29 100644 --- a/backend_new/README.md +++ b/backend_new/README.md @@ -8,6 +8,7 @@ $ yarn db:setup $ yarn prisma generate ``` +If you would prefer to have it setup with more realistic data you can run `yarn db:setup:staging` instead of `yarn db:setup`. # Modifying the Schema diff --git a/backend_new/package.json b/backend_new/package.json index 579b87e443..c1cc2b1049 100644 --- a/backend_new/package.json +++ b/backend_new/package.json @@ -20,10 +20,13 @@ "test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand", "db:resetup": "psql -c 'DROP DATABASE IF EXISTS bloom_prisma;' && psql -c 'CREATE DATABASE bloom_prisma;' && psql -d bloom_prisma -c 'CREATE EXTENSION IF NOT EXISTS \"uuid-ossp\";'", "db:migration:run": "yarn prisma migrate deploy", - "db:seed": "yarn prisma db seed", + "db:seed:production": "npx prisma db seed -- --environment production", + "db:seed:staging": "npx prisma db seed -- --environment staging", + "db:seed:development": "npx prisma db seed -- --environment development --jurisdictionName Bloomington", "generate:client": "ts-node scripts/generate-axios-client.ts && prettier -w types/src/backend-swagger.ts", "test:e2e": "yarn db:resetup && yarn db:migration:run && jest --config ./test/jest-e2e.config.js", - "db:setup": "yarn db:resetup && yarn db:migration:run && yarn db:seed" + "db:setup": "yarn db:resetup && yarn db:migration:run && yarn db:seed:development", + "db:setup:staging": "yarn db:resetup && yarn db:migration:run && yarn db:seed:staging" }, "dependencies": { "@google-cloud/translate": "^7.2.1", @@ -35,7 +38,7 @@ "class-validator": "^0.14.0", "class-transformer": "^0.5.1", "lodash": "^4.17.21", - "prisma": "^4.13.0", + "prisma": "^4.15.0", "qs": "^6.11.2", "reflect-metadata": "^0.1.13", "rimraf": "^3.0.2", @@ -48,10 +51,11 @@ "@nestjs/testing": "^8.0.0", "@types/express": "^4.17.13", "@types/jest": "27.4.1", - "@types/node": "^16.0.0", + "@types/node": "^18.7.14", "@types/supertest": "^2.0.11", "@typescript-eslint/eslint-plugin": "^5.0.0", "@typescript-eslint/parser": "^5.0.0", + "dayjs": "^1.11.8", "eslint": "^8.0.1", "eslint-config-prettier": "^8.3.0", "eslint-plugin-prettier": "^4.0.0", diff --git a/backend_new/prisma/seed-dev.ts b/backend_new/prisma/seed-dev.ts new file mode 100644 index 0000000000..2003c66893 --- /dev/null +++ b/backend_new/prisma/seed-dev.ts @@ -0,0 +1,68 @@ +import { + ListingsStatusEnum, + MultiselectQuestionsApplicationSectionEnum, + PrismaClient, +} from '@prisma/client'; +import { userFactory } from './seed-helpers/user-factory'; +import { jurisdictionFactory } from './seed-helpers/jurisdiction-factory'; +import { amiChartFactory } from './seed-helpers/ami-chart-factory'; +import { multiselectQuestionFactory } from './seed-helpers/multiselect-question-factory'; +import { listingFactory } from './seed-helpers/listing-factory'; +import { unitTypeFactoryAll } from './seed-helpers/unit-type-factory'; +import { randomName } from './seed-helpers/word-generator'; +import { randomInt } from 'node:crypto'; + +const listingStatusEnumArray = Object.values(ListingsStatusEnum); + +const createMultiselect = async ( + jurisdictionId: string, + prismaClient: PrismaClient, +) => { + const multiSelectQuestions = [...new Array(4)].map(async (_, index) => { + return await prismaClient.multiselectQuestions.create({ + data: multiselectQuestionFactory(jurisdictionId, { + multiselectQuestion: { + text: randomName(), + applicationSection: + index % 2 + ? MultiselectQuestionsApplicationSectionEnum.preferences + : MultiselectQuestionsApplicationSectionEnum.programs, + }, + optOut: index > 1, + numberOfOptions: index, + }), + }); + }); + return multiSelectQuestions; +}; + +export const devSeeding = async (prismaClient: PrismaClient) => { + await prismaClient.userAccounts.create({ + data: userFactory({ isAdmin: true }), + }); + const jurisdiction = await prismaClient.jurisdictions.create({ + data: jurisdictionFactory(), + }); + await unitTypeFactoryAll(prismaClient); + const amiChart = await prismaClient.amiChart.create({ + data: amiChartFactory(10, jurisdiction.id), + }); + const multiselectQuestions = await Promise.all( + await createMultiselect(jurisdiction.id, prismaClient), + ); + + [...new Array(5)].map(async (_, index) => { + const listing = await listingFactory(jurisdiction.id, prismaClient, { + amiChart: amiChart, + numberOfUnits: index, + includeBuildingFeatures: index > 1, + includeEligibilityRules: index > 2, + status: listingStatusEnumArray[randomInt(listingStatusEnumArray.length)], + multiselectQuestions: + index > 0 ? multiselectQuestions.slice(0, index - 1) : [], + }); + await prismaClient.listings.create({ + data: listing, + }); + }); +}; diff --git a/backend_new/prisma/seed-helpers/address-factory.ts b/backend_new/prisma/seed-helpers/address-factory.ts new file mode 100644 index 0000000000..1cafe72e49 --- /dev/null +++ b/backend_new/prisma/seed-helpers/address-factory.ts @@ -0,0 +1,72 @@ +import { Prisma } from '@prisma/client'; +import { randomInt } from 'node:crypto'; + +export const addressFactory = + (): Prisma.AddressCreateWithoutBuildingAddressInput => + [ + whiteHouse, + yellowstone, + goldenGateBridge, + washingtonMonument, + lincolnMemorial, + ][randomInt(5)]; + +export const whiteHouse = { + placeName: 'White House', + city: 'Washington', + county: null, + state: 'DC', + street: '1600 Pennsylvania Avenue', + street2: null, + zipCode: '20500', + latitude: 38.8977, + longitude: -77.0365, +}; + +export const yellowstone = { + placeName: 'Yellowstone National Park', + city: 'Yellowstone National Park', + county: null, + state: 'WY', + street: '3200 Old Faithful Inn Rd', + street2: null, + zipCode: '82190', + latitude: 44.459928576661824, + longitude: -110.83109211487681, +}; + +export const goldenGateBridge = { + placeName: 'Golden Gate Bridge', + city: 'San Francisco', + county: null, + state: 'CA', + street: 'Golden Gate Brg', + street2: null, + zipCode: '94129', + latitude: 37.820589659186425, + longitude: -122.47842676136818, +}; + +export const washingtonMonument = { + placeName: 'Washington Monument', + city: 'Washington', + county: null, + state: 'DC', + street: '2 15th St NW', + street2: null, + zipCode: '20024', + latitude: 38.88983672842871, + longitude: -77.03522750134796, +}; + +export const lincolnMemorial = { + placeName: 'Lincoln Memorial', + city: 'Washington', + county: null, + state: 'DC', + street: '2 Lincoln Memorial Cir NW', + street2: null, + zipCode: '20002', + latitude: 38.88958323798129, + longitude: -77.05024900814298, +}; diff --git a/backend_new/prisma/seed-helpers/ami-chart-factory.ts b/backend_new/prisma/seed-helpers/ami-chart-factory.ts index a5a4e51ecf..10a64b2ebf 100644 --- a/backend_new/prisma/seed-helpers/ami-chart-factory.ts +++ b/backend_new/prisma/seed-helpers/ami-chart-factory.ts @@ -1,11 +1,12 @@ import { Prisma } from '@prisma/client'; +import { randomName } from './word-generator'; export const amiChartFactory = ( - i: number, + numberToCreate: number, jurisdictionId: string, ): Prisma.AmiChartCreateInput => ({ - name: `name: ${i}`, - items: amiChartItemsFactory(i), + name: randomName(), + items: amiChartItemsFactory(numberToCreate), jurisdictions: { connect: { id: jurisdictionId, @@ -14,8 +15,11 @@ export const amiChartFactory = ( }); const amiChartItemsFactory = (numberToCreate: number): Prisma.JsonArray => - [...Array(numberToCreate)].map((_, index) => ({ - percentOfAmi: index, - householdSize: index, - income: index, - })); + [...Array(numberToCreate)].map((_, index) => { + const baseValue = index + 1; + return { + percentOfAmi: baseValue * 10, + householdSize: baseValue, + income: baseValue * 12_000, + }; + }); diff --git a/backend_new/prisma/seed-helpers/jurisdiction-factory.ts b/backend_new/prisma/seed-helpers/jurisdiction-factory.ts index 7c241a6f9e..99b485cf49 100644 --- a/backend_new/prisma/seed-helpers/jurisdiction-factory.ts +++ b/backend_new/prisma/seed-helpers/jurisdiction-factory.ts @@ -1,15 +1,17 @@ import { LanguagesEnum, Prisma } from '@prisma/client'; +import { randomName } from './word-generator'; export const jurisdictionFactory = ( - i: number, + jurisdictionName = randomName(), ): Prisma.JurisdictionsCreateInput => ({ - name: `name: ${i}`, - notificationsSignUpUrl: `notificationsSignUpUrl: ${i}`, + name: jurisdictionName, + notificationsSignUpUrl: null, languages: [LanguagesEnum.en], - partnerTerms: `partnerTerms: ${i}`, - publicUrl: `publicUrl: ${i}`, - emailFromAddress: `emailFromAddress: ${i}`, - rentalAssistanceDefault: `rentalAssistanceDefault: ${i}`, + partnerTerms: 'Example Terms', + publicUrl: 'http://localhost:3000', + emailFromAddress: 'Bloom ', + rentalAssistanceDefault: + 'Housing Choice Vouchers, Section 8 and other valid rental assistance programs will be considered for this property. In the case of a valid rental subsidy, the required minimum income will be based on the portion of the rent that the tenant pays after use of the subsidy.', enablePartnerSettings: true, enableAccessibilityFeatures: true, enableUtilitiesIncluded: true, diff --git a/backend_new/prisma/seed-helpers/listing-factory.ts b/backend_new/prisma/seed-helpers/listing-factory.ts index e2927d65b5..0725591c15 100644 --- a/backend_new/prisma/seed-helpers/listing-factory.ts +++ b/backend_new/prisma/seed-helpers/listing-factory.ts @@ -1,415 +1,150 @@ import { Prisma, - ApplicationAddressTypeEnum, + AmiChart, + MultiselectQuestions, + PrismaClient, ListingsStatusEnum, - ReviewOrderTypeEnum, - MarketingTypeEnum, - MarketingSeasonEnum, - HomeTypeEnum, - RegionEnum, - ApplicationMethodsTypeEnum, - ListingEventsTypeEnum, - MultiselectQuestionsApplicationSectionEnum, - UnitsStatusEnum, } from '@prisma/client'; -import { unitAccessibilityPriorityTypeFactory } from './unit-accessibility-priority-type-factory'; -import { unitTypeFactory } from './unit-type-factory'; -import { unitRentTypeFactory } from './unit-rent-type-factory'; +import { randomName } from './word-generator'; +import { addressFactory } from './address-factory'; +import { unitFactoryMany } from './unit-factory'; +import { reservedCommunityTypeFactory } from './reserved-community-type-factory'; -export const listingFactory = ( - i: number, +export const listingFactory = async ( jurisdictionId: string, - amiChartId?: string, - reservedCommunityTypeId?: string, - unitTypeId?: string, - unitAccessibilityPriorityTypeId?: string, - unitRentTypeId?: string, -): Prisma.ListingsCreateInput => ({ - additionalApplicationSubmissionNotes: `additionalApplicationSubmissionNotes: ${i}`, - digitalApplication: true, - commonDigitalApplication: true, - paperApplication: false, - referralOpportunity: true, - assets: '', - accessibility: `accessibility: ${i}`, - amenities: `amenities: ${i}`, - buildingTotalUnits: i, - developer: `developer: ${i}`, - householdSizeMax: 1, - householdSizeMin: i, - neighborhood: `neighborhood: ${i}`, - petPolicy: `petPolicy: ${i}`, - smokingPolicy: `smokingPolicy: ${i}`, - unitsAvailable: i - 1, - unitAmenities: `unitAmenities: ${i}`, - servicesOffered: `servicesOffered: ${i}`, - yearBuilt: 2000 + i, - applicationDueDate: new Date(), - applicationOpenDate: new Date(), - applicationFee: `applicationFee: ${i}`, - applicationOrganization: `applicationOrganization: ${i}`, - applicationPickUpAddressOfficeHours: `applicationPickUpAddressOfficeHours: ${i}`, - applicationPickUpAddressType: ApplicationAddressTypeEnum.leasingAgent, - applicationDropOffAddressOfficeHours: `applicationDropOffAddressOfficeHours: ${i}`, - applicationDropOffAddressType: ApplicationAddressTypeEnum.leasingAgent, - applicationMailingAddressType: ApplicationAddressTypeEnum.leasingAgent, - buildingSelectionCriteria: `buildingSelectionCriteria: ${i}`, - costsNotIncluded: `costsNotIncluded: ${i}`, - creditHistory: `creditHistory: ${i}`, - criminalBackground: `criminalBackground: ${i}`, - depositMin: `depositMin: ${i}`, - depositMax: `depositMax: ${i}`, - depositHelperText: `depositHelperText: ${i}`, - disableUnitsAccordion: true, - leasingAgentEmail: `leasingAgentEmail: ${i}`, - leasingAgentName: `leasingAgentName: ${i}`, - leasingAgentOfficeHours: `leasingAgentOfficeHours: ${i}`, - leasingAgentPhone: `leasingAgentPhone: ${i}`, - leasingAgentTitle: `leasingAgentTitle: ${i}`, - name: `name: ${i}`, - postmarkedApplicationsReceivedByDate: new Date(), - programRules: `programRules: ${i}`, - rentalAssistance: `rentalAssistance: ${i}`, - rentalHistory: `rentalHistory: ${i}`, - requiredDocuments: `requiredDocuments: ${i}`, - specialNotes: `specialNotes: ${i}`, - waitlistCurrentSize: i, - waitlistMaxSize: i + 1, - whatToExpect: `whatToExpect: ${i}`, - status: ListingsStatusEnum.active, - reviewOrderType: ReviewOrderTypeEnum.firstComeFirstServe, - displayWaitlistSize: true, - reservedCommunityDescription: `reservedCommunityDescription: ${i}`, - reservedCommunityMinAge: i * 10, - resultLink: `resultLink: ${i}`, - isWaitlistOpen: true, - waitlistOpenSpots: i, - customMapPin: false, - publishedAt: new Date(), - - closedAt: new Date(), - afsLastRunAt: null, - lastApplicationUpdateAt: new Date(), - listingsBuildingAddress: { - create: { - placeName: `listingsBuildingAddress: ${i} placeName: ${i}`, - city: `listingsBuildingAddress: ${i} city: ${i}`, - county: `listingsBuildingAddress: ${i} county: ${i}`, - state: `listingsBuildingAddress: ${i} state: ${i}`, - street: `listingsBuildingAddress: ${i} street: ${i}`, - street2: `listingsBuildingAddress: ${i} street2: ${i}`, - zipCode: `listingsBuildingAddress: ${i} zipCode: ${i}`, - latitude: i * 100, - longitude: i * 101, - }, + prismaClient: PrismaClient, + optionalParams?: { + amiChart?: AmiChart; + numberOfUnits?: number; + status?: ListingsStatusEnum; + units?: Prisma.UnitsCreateWithoutListingsInput[]; + listing?: Prisma.ListingsCreateInput; + includeBuildingFeatures?: boolean; + includeEligibilityRules?: boolean; + multiselectQuestions?: MultiselectQuestions[]; }, - listingsApplicationDropOffAddress: { - create: { - placeName: `listingsApplicationDropOffAddress: ${i} placeName: ${i}`, - city: `listingsApplicationDropOffAddress: ${i} city: ${i}`, - county: `listingsApplicationDropOffAddress: ${i} county: ${i}`, - state: `listingsApplicationDropOffAddress: ${i} state: ${i}`, - street: `listingsApplicationDropOffAddress: ${i} street: ${i}`, - street2: `listingsApplicationDropOffAddress: ${i} street2: ${i}`, - zipCode: `listingsApplicationDropOffAddress: ${i} zipCode: ${i}`, - latitude: i * 100, - longitude: i * 101, +): Promise => { + const previousListing = optionalParams?.listing || {}; + let units = optionalParams?.units; + if (!units && optionalParams?.numberOfUnits) { + units = await unitFactoryMany(optionalParams.numberOfUnits, prismaClient, { + randomizePriorityType: true, + amiChart: optionalParams?.amiChart, + }); + } + return { + createdAt: new Date(), + assets: [], + name: randomName(), + status: optionalParams?.status || ListingsStatusEnum.active, + displayWaitlistSize: Math.random() < 0.5, + listingsBuildingAddress: { + create: addressFactory(), }, - }, - listingsApplicationMailingAddress: { - create: { - placeName: `listingsApplicationMailingAddress: ${i} placeName: ${i}`, - city: `listingsApplicationMailingAddress: ${i} city: ${i}`, - county: `listingsApplicationMailingAddress: ${i} county: ${i}`, - state: `listingsApplicationMailingAddress: ${i} state: ${i}`, - street: `listingsApplicationMailingAddress: ${i} street: ${i}`, - street2: `listingsApplicationMailingAddress: ${i} street2: ${i}`, - zipCode: `listingsApplicationMailingAddress: ${i} zipCode: ${i}`, - latitude: i * 100, - longitude: i * 101, + listingsApplicationMailingAddress: { + create: addressFactory(), }, - }, - listingsLeasingAgentAddress: { - create: { - placeName: `listingsLeasingAgentAddress: ${i} placeName: ${i}`, - city: `listingsLeasingAgentAddress: ${i} city: ${i}`, - county: `listingsLeasingAgentAddress: ${i} county: ${i}`, - state: `listingsLeasingAgentAddress: ${i} state: ${i}`, - street: `listingsLeasingAgentAddress: ${i} street: ${i}`, - street2: `listingsLeasingAgentAddress: ${i} street2: ${i}`, - zipCode: `listingsLeasingAgentAddress: ${i} zipCode: ${i}`, - latitude: i * 100, - longitude: i * 101, + listingsApplicationPickUpAddress: { + create: addressFactory(), }, - }, - listingsApplicationPickUpAddress: { - create: { - placeName: `listingsApplicationPickUpAddress: ${i} placeName: ${i}`, - city: `listingsApplicationPickUpAddress: ${i} city: ${i}`, - county: `listingsApplicationPickUpAddress: ${i} county: ${i}`, - state: `listingsApplicationPickUpAddress: ${i} state: ${i}`, - street: `listingsApplicationPickUpAddress: ${i} street: ${i}`, - street2: `listingsApplicationPickUpAddress: ${i} street2: ${i}`, - zipCode: `listingsApplicationPickUpAddress: ${i} zipCode: ${i}`, - latitude: i * 100, - longitude: i * 101, + listingsLeasingAgentAddress: { + create: addressFactory(), }, - }, - listingsBuildingSelectionCriteriaFile: { - create: { - label: `listingsBuildingSelectionCriteriaFile: ${i} label: ${i}`, - fileId: `listingsBuildingSelectionCriteriaFile: ${i} fileId: ${i}`, + listingsApplicationDropOffAddress: { + create: addressFactory(), }, - }, - jurisdictions: { - connect: { - id: jurisdictionId, + reservedCommunityTypes: { + create: reservedCommunityTypeFactory(jurisdictionId), }, - }, - reservedCommunityTypes: reservedCommunityTypeId - ? { connect: { id: reservedCommunityTypeId } } - : { - create: { - name: `reservedCommunityType: ${i}`, - description: `description: ${i}`, - jurisdictions: { - connect: { - id: jurisdictionId, - }, - }, - }, + listingMultiselectQuestions: optionalParams?.multiselectQuestions + ? { + create: optionalParams.multiselectQuestions.map( + (question, index) => ({ + ordinal: index, + multiselectQuestionId: question.id, + }), + ), + } + : undefined, + ...featuresAndUtilites(), + ...buildingFeatures(optionalParams?.includeBuildingFeatures), + ...additionalEligibilityRules(optionalParams?.includeEligibilityRules), + ...previousListing, + jurisdictions: { + connect: { + id: jurisdictionId, }, - listingsResult: { - create: { - label: `listingsResult: ${i} label: ${i}`, - fileId: `listingsResult: ${i} fileId: ${i}`, - }, - }, - listingFeatures: { - create: { - elevator: true, - wheelchairRamp: true, - serviceAnimalsAllowed: true, - accessibleParking: true, - parkingOnSite: true, - inUnitWasherDryer: true, - laundryInBuilding: true, - barrierFreeEntrance: true, - rollInShower: true, - grabBars: true, - heatingInUnit: true, - acInUnit: true, - hearing: true, - visual: true, - mobility: true, - barrierFreeUnitEntrance: true, - loweredLightSwitch: true, - barrierFreeBathroom: true, - wideDoorways: true, - loweredCabinets: true, }, - }, - listingUtilities: { - create: { - water: true, - gas: true, - trash: true, - sewer: true, - electricity: true, - cable: true, - phone: true, - internet: true, - }, - }, + units: units + ? { + create: units, + } + : undefined, + }; +}; - // detroit specific - hrdId: `hrdId: ${i}`, - ownerCompany: `ownerCompany: ${i}`, - managementCompany: `managementCompany: ${i}`, - managementWebsite: `managementWebsite: ${i}`, - amiPercentageMin: i, - amiPercentageMax: i, - phoneNumber: `phoneNumber: ${i}`, - temporaryListingId: i, - isVerified: true, - marketingType: MarketingTypeEnum.marketing, - marketingDate: new Date(), - marketingSeason: MarketingSeasonEnum.summer, - whatToExpectAdditionalText: `whatToExpectAdditionalText: ${i}`, - section8Acceptance: true, - listingNeighborhoodAmenities: { - create: { - groceryStores: `listingNeighborhoodAmenities: ${i} groceryStores: ${i}`, - pharmacies: `listingNeighborhoodAmenities: ${i} pharmacies: ${i}`, - healthCareResources: `listingNeighborhoodAmenities: ${i} healthCareResources: ${i}`, - parksAndCommunityCenters: `listingNeighborhoodAmenities: ${i} parksAndCommunityCenters: ${i}`, - schools: `listingNeighborhoodAmenities: ${i} schools: ${i}`, - publicTransportation: `listingNeighborhoodAmenities: ${i} publicTransportation: ${i}`, - }, - }, - verifiedAt: new Date(), - homeType: HomeTypeEnum.apartment, - region: RegionEnum.Greater_Downtown, - // end detroit specific +const buildingFeatures = (includeBuildingFeatures: boolean) => { + if (!includeBuildingFeatures) return {}; + return { + amenities: + 'Laundry facilities, Elevators, Beautifully landscaped garden, walkways', + unitAmenities: 'All-electric kitchen, Dishwasher', + petPolicy: 'Allow pets with a deposit of $500', + accessibility: 'ADA units available', + smokingPolicy: 'Non-smoking building', + servicesOffered: 'Resident services on-site.', + }; +}; - applicationMethods: { - create: { - type: ApplicationMethodsTypeEnum.Internal, - label: `applicationMethods: ${i} label: ${i}`, - externalReference: `applicationMethods: ${i} externalReference: ${i}`, - acceptsPostmarkedApplications: true, - phoneNumber: `applicationMethods: ${i} phoneNumber: ${i}`, - }, - }, - listingEvents: { +const additionalEligibilityRules = (includeEligibilityRules: boolean) => { + if (!includeEligibilityRules) return {}; + return { + rentalHistory: 'Two years of rental history will be verified', + rentalAssistance: 'additional rental assistance', + creditHistory: + 'A poor credit history may be grounds to deem an applicant ineligible for housing.', + criminalBackground: + 'A criminal background investigation will be obtained on each applicant. As criminal background checks are done county by county and will be ran for all counties in which the applicant lived, Applicants will be disqualified for tenancy if they have been convicted of a felony or misdemeanor. Refer to Tenant Selection Criteria or Qualification Criteria for details related to the qualification process. ', + }; +}; + +// Tables that aren't currently used by bloom but are getting set. +// Setting all fields to false for now +const featuresAndUtilites = () => ({ + listingFeatures: { create: { - type: ListingEventsTypeEnum.publicLottery, - startDate: new Date(), - startTime: new Date(), - endTime: new Date(), - url: `listingEvents: ${i} url: ${i}`, - note: `listingEvents: ${i} note: ${i}`, - label: `listingEvents: ${i} label: ${i}`, - assets: { - create: { - label: `listingEvents: ${i} label: ${i}`, - fileId: `listingEvents: ${i} fileId: ${i}`, - }, - }, + elevator: false, + wheelchairRamp: false, + serviceAnimalsAllowed: false, + accessibleParking: false, + parkingOnSite: false, + inUnitWasherDryer: false, + laundryInBuilding: false, + barrierFreeEntrance: false, + rollInShower: false, + grabBars: false, + heatingInUnit: false, + acInUnit: false, + hearing: false, + visual: false, + mobility: false, + barrierFreeUnitEntrance: false, + loweredLightSwitch: false, + barrierFreeBathroom: false, + wideDoorways: false, + loweredCabinets: false, }, }, - listingImages: { + listingUtilities: { create: { - ordinal: 1, - assets: { - create: { - label: `listingImages: ${i} label: ${i}`, - fileId: `listingImages: ${i} fileId: ${i}`, - }, - }, + water: false, + gas: false, + trash: false, + sewer: false, + electricity: false, + cable: false, + phone: false, + internet: false, }, }, - listingMultiselectQuestions: { - create: [ - { - ordinal: 1, - multiselectQuestions: { - create: { - text: `multiselectQuestions: ${i} text: ${i}`, - subText: `multiselectQuestions: ${i} subText: ${i}`, - description: `multiselectQuestions: ${i} description: ${i}`, - links: {}, - options: [ - { text: `multiselectQuestions: ${i} option: ${i}`, ordinal: 1 }, - ], - optOutText: `multiselectQuestions: ${i} optOutText: ${i}`, - hideFromListing: true, - applicationSection: - MultiselectQuestionsApplicationSectionEnum.programs, - jurisdictions: { - connect: { - id: jurisdictionId, - }, - }, - }, - }, - }, - { - ordinal: 2, - multiselectQuestions: { - create: { - text: `multiselectQuestions: ${i} text: ${i}`, - subText: `multiselectQuestions: ${i} subText: ${i}`, - description: `multiselectQuestions: ${i} description: ${i}`, - links: {}, - options: [], - optOutText: `multiselectQuestions: ${i} optOutText: ${i}`, - hideFromListing: true, - applicationSection: - MultiselectQuestionsApplicationSectionEnum.preferences, - jurisdictions: { - connect: { - id: jurisdictionId, - }, - }, - }, - }, - }, - ], - }, - units: unitFactory( - i, - i, - jurisdictionId, - amiChartId, - unitTypeId, - unitAccessibilityPriorityTypeId, - unitRentTypeId, - ), }); - -const unitFactory = ( - numberToMake: number, - i: number, - jurisdictionId: string, - amiChartId?: string, - unitTypeId?: string, - unitAccessibilityPriorityTypeId?: string, - unitRentTypeId?: string, -): Prisma.UnitsCreateNestedManyWithoutListingsInput => { - const createArray: Prisma.UnitsCreateWithoutListingsInput[] = []; - for (let j = 0; j < numberToMake; j++) { - createArray.push({ - amiPercentage: `${i}`, - annualIncomeMin: `${i}`, - monthlyIncomeMin: `${i}`, - floor: i, - annualIncomeMax: `${i}`, - maxOccupancy: i, - minOccupancy: i, - monthlyRent: `${i}`, - numBathrooms: i, - numBedrooms: i, - number: `listing: ${i} unit: ${j}`, - sqFeet: i, - monthlyRentAsPercentOfIncome: i, - bmrProgramChart: true, - status: UnitsStatusEnum.available, - unitTypes: unitTypeId - ? { - connect: { - id: unitTypeId, - }, - } - : { create: unitTypeFactory(i) }, - amiChart: amiChartId - ? { connect: { id: amiChartId } } - : { - create: { - items: [], - name: `listing: ${i} unit: ${j} amiChart: ${j}`, - jurisdictions: { - connect: { - id: jurisdictionId, - }, - }, - }, - }, - unitAccessibilityPriorityTypes: unitAccessibilityPriorityTypeId - ? { connect: { id: unitAccessibilityPriorityTypeId } } - : { - create: unitAccessibilityPriorityTypeFactory(i), - }, - unitRentTypes: unitRentTypeId - ? { connect: { id: unitRentTypeId } } - : { - create: unitRentTypeFactory(i), - }, - }); - } - const toReturn: Prisma.UnitsCreateNestedManyWithoutListingsInput = { - create: createArray, - }; - - return toReturn; -}; diff --git a/backend_new/prisma/seed-helpers/multiselect-question-factory.ts b/backend_new/prisma/seed-helpers/multiselect-question-factory.ts new file mode 100644 index 0000000000..a5eabdfd3d --- /dev/null +++ b/backend_new/prisma/seed-helpers/multiselect-question-factory.ts @@ -0,0 +1,52 @@ +import { + MultiselectQuestionsApplicationSectionEnum, + Prisma, +} from '@prisma/client'; +import { randomName, randomNoun } from './word-generator'; +import { randomInt } from 'crypto'; + +const multiselectAppSectionAsArray = Object.values( + MultiselectQuestionsApplicationSectionEnum, +); + +export const multiselectQuestionFactory = ( + jurisdictionId: string, + optionalParams?: { + optOut?: boolean; + numberOfOptions?: number; + multiselectQuestion?: Prisma.MultiselectQuestionsCreateInput; + }, +): Prisma.MultiselectQuestionsCreateInput => { + const previousMultiselectQuestion = optionalParams?.multiselectQuestion || {}; + const text = optionalParams?.multiselectQuestion?.text || randomName(); + return { + text: text, + subText: `sub text for ${text}`, + description: `description of ${text}`, + links: {}, + options: multiselectOptionFactory(optionalParams?.numberOfOptions || 0), + optOutText: optionalParams?.optOut ? "I don't want this preference" : null, + hideFromListing: false, + applicationSection: + optionalParams?.multiselectQuestion?.applicationSection || + multiselectAppSectionAsArray[ + randomInt(multiselectAppSectionAsArray.length) + ], + ...previousMultiselectQuestion, + jurisdictions: { + connect: { + id: jurisdictionId, + }, + }, + }; +}; + +const multiselectOptionFactory = ( + numberToMake: number, +): Prisma.InputJsonValue => { + if (!numberToMake) return {}; + return [...new Array(numberToMake)].map((_, index) => ({ + text: randomNoun(), + ordinal: index, + })); +}; diff --git a/backend_new/prisma/seed-helpers/reserved-community-type-factory.ts b/backend_new/prisma/seed-helpers/reserved-community-type-factory.ts index 1bdd01e013..9fcfa8b4b3 100644 --- a/backend_new/prisma/seed-helpers/reserved-community-type-factory.ts +++ b/backend_new/prisma/seed-helpers/reserved-community-type-factory.ts @@ -1,14 +1,43 @@ -import { Prisma } from '@prisma/client'; +import { Prisma, PrismaClient } from '@prisma/client'; +import { randomInt } from 'crypto'; + +const reservedCommunityTypeOptions = [ + 'specialNeeds', + 'senior', + 'senior62', + 'developmentalDisability', + 'veteran', +]; export const reservedCommunityTypeFactory = ( - i: number, jurisdictionId: string, -): Prisma.ReservedCommunityTypesCreateInput => ({ - name: `name: ${i}`, - description: `description: ${i}`, - jurisdictions: { - connect: { - id: jurisdictionId, + name?: string, +): Prisma.ReservedCommunityTypesCreateWithoutListingsInput => { + // if name is not given pick one randomly from the above list + const chosenName = + name || + reservedCommunityTypeOptions[ + randomInt(reservedCommunityTypeOptions.length) + ]; + return { + name: chosenName, + description: `reservedCommunityTypes of ${chosenName}`, + jurisdictions: { + connect: { + id: jurisdictionId, + }, }, - }, -}); + }; +}; + +export const reservedCommunityTypeFactoryAll = async ( + jurisdictionId: string, + prismaClient: PrismaClient, +) => { + await prismaClient.reservedCommunityTypes.createMany({ + data: reservedCommunityTypeOptions.map((value) => ({ + name: value, + jurisdictionId: jurisdictionId, + })), + }); +}; diff --git a/backend_new/prisma/seed-helpers/unit-accessibility-priority-type-factory.ts b/backend_new/prisma/seed-helpers/unit-accessibility-priority-type-factory.ts index 7a50755fe8..df4f5ddeea 100644 --- a/backend_new/prisma/seed-helpers/unit-accessibility-priority-type-factory.ts +++ b/backend_new/prisma/seed-helpers/unit-accessibility-priority-type-factory.ts @@ -1,17 +1,29 @@ -import { Prisma, UnitAccessibilityPriorityTypeEnum } from '@prisma/client'; +import { + Prisma, + PrismaClient, + UnitAccessibilityPriorityTypeEnum, +} from '@prisma/client'; +import { randomInt } from 'crypto'; -export const unitAccessibilityPriorityTypeFactory = ( - i: number, -): Prisma.UnitAccessibilityPriorityTypesCreateInput => ({ - ...unitPriorityTypeArray[i % unitPriorityTypeArray.length], -}); +export const unitAccessibilityPriorityTypeFactorySingle = ( + type?: UnitAccessibilityPriorityTypeEnum, +): Prisma.UnitAccessibilityPriorityTypesCreateInput => { + const chosenType = + type || + unitAccesibilityPriorityTypeAsArray[ + randomInt(unitAccesibilityPriorityTypeAsArray.length) + ]; + return { name: chosenType }; +}; -export const unitPriorityTypeArray = [ - { name: UnitAccessibilityPriorityTypeEnum.mobility }, - { name: UnitAccessibilityPriorityTypeEnum.mobilityAndHearing }, - { name: UnitAccessibilityPriorityTypeEnum.hearing }, - { name: UnitAccessibilityPriorityTypeEnum.visual }, - { name: UnitAccessibilityPriorityTypeEnum.hearingAndVisual }, - { name: UnitAccessibilityPriorityTypeEnum.mobilityAndVisual }, - { name: UnitAccessibilityPriorityTypeEnum.mobilityHearingAndVisual }, -]; +export const unitAccessibilityPriorityTypeFactoryAll = async ( + prismaClient: PrismaClient, +) => { + await prismaClient.unitAccessibilityPriorityTypes.createMany({ + data: unitAccesibilityPriorityTypeAsArray.map((value) => ({ name: value })), + }); +}; + +export const unitAccesibilityPriorityTypeAsArray = Object.values( + UnitAccessibilityPriorityTypeEnum, +); diff --git a/backend_new/prisma/seed-helpers/unit-factory.ts b/backend_new/prisma/seed-helpers/unit-factory.ts new file mode 100644 index 0000000000..dc2461f08a --- /dev/null +++ b/backend_new/prisma/seed-helpers/unit-factory.ts @@ -0,0 +1,75 @@ +import { + AmiChart, + Prisma, + PrismaClient, + UnitTypeEnum, + UnitTypes, +} from '@prisma/client'; +import { unitTypeFactorySingle } from './unit-type-factory'; +import { unitAccessibilityPriorityTypeFactorySingle } from './unit-accessibility-priority-type-factory'; +import { unitRentTypeFactory } from './unit-rent-type-factory'; +import { randomInt } from 'crypto'; + +const unitTypes = Object.values(UnitTypeEnum); + +export const unitFactorySingle = ( + unitType: UnitTypes, + optionalParams?: { + amiChart?: AmiChart; + unitRentTypeId?: string; + otherFields?: Prisma.UnitsCreateWithoutListingsInput; + }, +): Prisma.UnitsCreateWithoutListingsInput => { + return { + amiChart: optionalParams?.amiChart + ? { connect: { id: optionalParams.amiChart.id } } + : undefined, + unitTypes: { + connect: { + id: unitType.id, + }, + }, + amiPercentage: optionalParams?.amiChart + ? (Math.ceil((Math.random() * 100) / 10) * 10).toString() // get an integer divisible by 10 + : undefined, + numBathrooms: randomInt(4), + numBedrooms: unitType.numBedrooms || randomInt(6), + unitRentTypes: optionalParams?.unitRentTypeId + ? { connect: { id: optionalParams?.unitRentTypeId } } + : { + create: unitRentTypeFactory(), + }, + ...optionalParams?.otherFields, + }; +}; + +export const unitFactoryMany = async ( + numberToMake: number, + prismaClient: PrismaClient, + optionalParams?: { + randomizePriorityType?: boolean; + amiChart?: AmiChart; + unitAccessibilityPriorityTypeId?: string; + }, +): Promise => { + const createArray: Promise[] = [ + ...new Array(numberToMake), + ].map(async (_, index) => { + const unitType = await unitTypeFactorySingle( + prismaClient, + unitTypes[randomInt(unitTypes.length)], + ); + + // create a random priority type with roughly half being null + const unitAccessibilityPriorityTypes = + optionalParams?.randomizePriorityType && Math.random() > 0.5 + ? { create: unitAccessibilityPriorityTypeFactorySingle() } + : undefined; + + return unitFactorySingle(unitType, { + ...optionalParams, + otherFields: { unitAccessibilityPriorityTypes, numBathrooms: index }, + }); + }); + return await Promise.all(createArray); +}; diff --git a/backend_new/prisma/seed-helpers/unit-rent-type-factory.ts b/backend_new/prisma/seed-helpers/unit-rent-type-factory.ts index f29e493d09..9b51189fa1 100644 --- a/backend_new/prisma/seed-helpers/unit-rent-type-factory.ts +++ b/backend_new/prisma/seed-helpers/unit-rent-type-factory.ts @@ -1,12 +1,10 @@ import { Prisma, UnitRentTypeEnum } from '@prisma/client'; +import { randomInt } from 'crypto'; export const unitRentTypeFactory = ( - i: number, + type?: UnitRentTypeEnum, ): Prisma.UnitRentTypesCreateInput => ({ - ...unitRentTypeArray[i % unitRentTypeArray.length], + name: type || unitRentTypeArray[randomInt(unitRentTypeArray.length)], }); -export const unitRentTypeArray = [ - { name: UnitRentTypeEnum.fixed }, - { name: UnitRentTypeEnum.percentageOfIncome }, -]; +export const unitRentTypeArray = Object.values(UnitRentTypeEnum); diff --git a/backend_new/prisma/seed-helpers/unit-type-factory.ts b/backend_new/prisma/seed-helpers/unit-type-factory.ts index 377cb7e676..90c898b669 100644 --- a/backend_new/prisma/seed-helpers/unit-type-factory.ts +++ b/backend_new/prisma/seed-helpers/unit-type-factory.ts @@ -1,15 +1,41 @@ -import { Prisma, UnitTypeEnum } from '@prisma/client'; +import { PrismaClient, UnitTypeEnum, UnitTypes } from '@prisma/client'; -export const unitTypeFactory = (i: number): Prisma.UnitTypesCreateInput => ({ - ...unitTypeArray[i % unitTypeArray.length], -}); +export const unitTypeFactorySingle = async ( + prismaClient: PrismaClient, + type: UnitTypeEnum, +): Promise => { + const unitType = await prismaClient.unitTypes.findFirst({ + where: { + name: { + equals: type, + }, + }, + }); + if (!unitType) { + console.warn(`Unit type ${type} was not created, run unitTypeFactoryAll`); + } + return unitType; +}; -export const unitTypeArray = [ - { name: UnitTypeEnum.studio, numBedrooms: 0 }, - { name: UnitTypeEnum.oneBdrm, numBedrooms: 1 }, - { name: UnitTypeEnum.twoBdrm, numBedrooms: 2 }, - { name: UnitTypeEnum.threeBdrm, numBedrooms: 3 }, - { name: UnitTypeEnum.fourBdrm, numBedrooms: 4 }, - { name: UnitTypeEnum.SRO, numBedrooms: 0 }, - { name: UnitTypeEnum.fiveBdrm, numBedrooms: 5 }, -]; +export const unitTypeFactoryAll = async (prismaClient: PrismaClient) => { + return Promise.all( + Object.values(UnitTypeEnum).map(async (value) => { + return await prismaClient.unitTypes.create({ + data: { + name: value, + numBedrooms: unitTypeMapping[value], + }, + }); + }), + ); +}; + +export const unitTypeMapping = { + [UnitTypeEnum.studio]: 0, + [UnitTypeEnum.SRO]: 0, + [UnitTypeEnum.oneBdrm]: 1, + [UnitTypeEnum.twoBdrm]: 2, + [UnitTypeEnum.threeBdrm]: 3, + [UnitTypeEnum.fourBdrm]: 4, + [UnitTypeEnum.fiveBdrm]: 5, +}; diff --git a/backend_new/prisma/seed-helpers/user-factory.ts b/backend_new/prisma/seed-helpers/user-factory.ts new file mode 100644 index 0000000000..f398c27f00 --- /dev/null +++ b/backend_new/prisma/seed-helpers/user-factory.ts @@ -0,0 +1,19 @@ +import { Prisma } from '@prisma/client'; + +export const userFactory = ( + roles?: Prisma.UserRolesUncheckedCreateWithoutUserAccountsInput, +): Prisma.UserAccountsCreateInput => ({ + email: 'admin@example.com', + firstName: 'First', + lastName: 'Last', + // TODO: update with passwordService hashing when that is completed + passwordHash: + 'a921d45de2db97818a124126706a1bf52310d231be04e1764d4eedffaccadcea3af70fa1d806b8527b2ebb98a2dd48ab3f07238bb9d39d4bcd2de4c207b67d4e#c870c8c0dbc08b27f4fc1dab32266cfde4aef8f2c606dab1162f9e71763f1fd11f28b2b81e05e7aeefd08b745d636624b623f505d47a54213fb9822c366bbbfe', + userRoles: { + create: { + isAdmin: roles?.isAdmin || false, + isJurisdictionalAdmin: roles?.isJurisdictionalAdmin || false, + isPartner: roles?.isAdmin || false, + }, + }, +}); diff --git a/backend_new/prisma/seed-helpers/word-generator.ts b/backend_new/prisma/seed-helpers/word-generator.ts new file mode 100644 index 0000000000..a487dc4e97 --- /dev/null +++ b/backend_new/prisma/seed-helpers/word-generator.ts @@ -0,0 +1,221 @@ +import { randomInt } from 'crypto'; + +// Random list of nouns +const nouns = [ + 'Bun', + 'Cable', + 'Impulse', + 'Receipt', + 'Education', + 'Psychology', + 'Event', + 'Recording', + 'Cabbage', + 'Clocks', + 'Territory', + 'Meat', + 'Cactus', + 'Legs', + 'Consequence', + 'Strategy', + 'Minute', + 'Quicksand', + 'Blood', + 'Diamond', + 'Fire', + 'Queen', + 'Lunchroom', + 'Hose', + 'Amusement', + 'Problem', + 'Pet', + 'Stamp', + 'Vehicle', + 'Milk', + 'Library', + 'Yard', + 'Funeral', + 'Solution', + 'Bike', + 'Chemistry', + 'Shame', + 'Investment', + 'Competition', + 'Instance', + 'Magazine', + 'Account', + 'Warning', + 'Example', + 'Weight', + 'Body', + 'Fruit', + 'Question', + 'Dirt', + 'Mood', + 'Series', + 'Hope', + 'Loss', + 'Harbor', + 'Death', + 'Fang', + 'Fairies', + 'Mitten', + 'Truck', + 'Year', + 'Company', + 'Aspect', + 'Guide', + 'Queen', + 'Crow', + 'Situation', + 'Nest', + 'Wilderness', + 'Fishing', + 'Recess', + 'Record', + 'Weakness', + 'Quiver', + 'Crate', + 'Trucks', + 'Price', + 'Nerve', + 'Finger', + 'Station', + 'Curtain', + 'Badge', + 'Homework', + 'Chemistry', + 'Bath', + 'Signature', + 'Zipper', + 'Quiet', + 'Needle', + 'Ability', + 'Piano', + 'Collection', + 'Comb', + 'Height', + 'Election', + 'Disgust', + 'Range', + 'Mother', + 'Artisan', + 'Role', + 'Mode', + 'Lace', + 'Education', + 'Faucet', + 'Food', + 'Bike', + 'Locket', + 'Square', + 'Ink', + 'Heat', + 'Business', + 'Agreement', + 'Sea', + 'Tent', + 'Minute', + 'Territory', + 'Sock', + 'Bag', + 'Revolution', + 'Law', + 'Smell', +]; + +// Random list of adjectives +const adjectives = [ + 'Grandiose', + 'Snobbish', + 'Simple', + 'Lackadaisical', + 'Strange', + 'Psychedelic', + 'Milky', + 'Foregoing', + 'Impolite', + 'Oceanic', + 'Abstracted', + 'Sleepy', + 'Soggy', + 'Sick', + 'Nimble', + 'Likeable', + 'Itchy', + 'Powerful', + 'Flat', + 'Splendid', + 'Abortive', + 'Bloody', + 'Brave', + 'Long-term', + 'Gabby', + 'Polite', + 'Highfalutin', + 'Bite-sized', + 'Discreet', + 'Pricey', + 'Fierce', + 'Drunk', + 'Ready', + 'Kindhearted', + 'Boiling', + 'Extra-small', + 'Jazzy', + 'Laughable', + 'Jealous', + 'Overjoyed', + 'Synonymous', + 'Shocking', + 'Mighty', + 'Aback', + 'Wandering', + 'Grateful', + 'Fuzzy', + 'Four', + 'Abundant', + 'Faint', + 'Absorbed', + 'Absurd', + 'Idiotic', + 'Sparkling', + 'Harmonious', + 'Simple', + 'Silent', + 'Milky', + 'Polite', + 'Evanescent', + 'Dramatic', + 'Thoughtful', + 'Open', + 'Adorable', + 'Shaggy', + 'Salty', + 'Jaded', + 'Spiky', + 'Satisfying', + 'Symptomatic', + 'Broken', + 'Insidious', + 'Broad', + 'Abrupt', + 'Neighborly', + 'Boundless', + 'Ossified', + 'Brown', + 'Honorable', + 'Clammy', +]; + +export const randomNoun = () => { + return nouns[randomInt(nouns.length)]; +}; + +export const randomAdjective = () => { + return adjectives[randomInt(adjectives.length)]; +}; + +export const randomName = () => { + return `${randomAdjective()} ${randomNoun()}`; +}; diff --git a/backend_new/prisma/seed-staging.ts b/backend_new/prisma/seed-staging.ts new file mode 100644 index 0000000000..6cf14ecb4c --- /dev/null +++ b/backend_new/prisma/seed-staging.ts @@ -0,0 +1,693 @@ +import { + ApplicationAddressTypeEnum, + ListingsStatusEnum, + MultiselectQuestionsApplicationSectionEnum, + PrismaClient, + ReviewOrderTypeEnum, +} from '@prisma/client'; +import * as dayjs from 'dayjs'; +import { jurisdictionFactory } from './seed-helpers/jurisdiction-factory'; +import { listingFactory } from './seed-helpers/listing-factory'; +import { amiChartFactory } from './seed-helpers/ami-chart-factory'; +import { userFactory } from './seed-helpers/user-factory'; +import { unitTypeFactoryAll } from './seed-helpers/unit-type-factory'; +import { unitAccessibilityPriorityTypeFactoryAll } from './seed-helpers/unit-accessibility-priority-type-factory'; +import { multiselectQuestionFactory } from './seed-helpers/multiselect-question-factory'; +import { + lincolnMemorial, + washingtonMonument, + whiteHouse, +} from './seed-helpers/address-factory'; + +export const stagingSeed = async ( + prismaClient: PrismaClient, + jurisdictionName: string, +) => { + // create admin user + await prismaClient.userAccounts.create({ + data: userFactory({ isAdmin: true }), + }); + // create single jurisdiction + const jurisdiction = await prismaClient.jurisdictions.create({ + data: jurisdictionFactory(jurisdictionName), + }); + // build ami charts + const amiChart = await prismaClient.amiChart.create({ + data: amiChartFactory(10, jurisdiction.id), + }); + const multiselectQuestion1 = await prismaClient.multiselectQuestions.create({ + data: multiselectQuestionFactory(jurisdiction.id, { + multiselectQuestion: { + text: 'City Employees', + description: 'Employees of the local city.', + applicationSection: + MultiselectQuestionsApplicationSectionEnum.preferences, + options: [ + { + text: 'At least one member of my household is a city employee', + ordinal: 0, + }, + ], + }, + }), + }); + const multiselectQuestion2 = await prismaClient.multiselectQuestions.create({ + data: multiselectQuestionFactory(jurisdiction.id, { + multiselectQuestion: { + text: 'Work in the city', + description: 'At least one member of my household works in the city', + applicationSection: + MultiselectQuestionsApplicationSectionEnum.preferences, + options: [ + { + text: 'At least one member of my household works in the city', + ordinal: 0, + }, + { + text: 'All members of the household work in the city', + ordinal: 1, + }, + ], + }, + }), + }); + // create pre-determined values + const unitTypes = await unitTypeFactoryAll(prismaClient); + await unitAccessibilityPriorityTypeFactoryAll(prismaClient); + // list of predefined listings WARNING: images only work if image setup is cloudinary on exygy account + [ + { + listing: { + additionalApplicationSubmissionNotes: null, + digitalApplication: true, + commonDigitalApplication: true, + paperApplication: false, + referralOpportunity: false, + assets: [], + accessibility: null, + amenities: null, + buildingTotalUnits: 0, + developer: 'Bloom', + householdSizeMax: 0, + householdSizeMin: 0, + neighborhood: 'Hollywood', + petPolicy: null, + smokingPolicy: null, + unitsAvailable: 0, + unitAmenities: null, + servicesOffered: null, + yearBuilt: null, + applicationDueDate: null, + applicationOpenDate: dayjs(new Date()).subtract(70, 'days').toDate(), + applicationFee: null, + applicationOrganization: null, + applicationPickUpAddressOfficeHours: null, + applicationPickUpAddressType: null, + applicationDropOffAddressOfficeHours: null, + applicationDropOffAddressType: null, + applicationMailingAddressType: null, + buildingSelectionCriteria: null, + costsNotIncluded: null, + creditHistory: null, + criminalBackground: null, + depositMin: '0', + depositMax: '0', + depositHelperText: + "or one month's rent may be higher for lower credit scores", + disableUnitsAccordion: false, + leasingAgentEmail: 'bloom@exygy.com', + leasingAgentName: 'Bloom Bloomington', + leasingAgentOfficeHours: null, + leasingAgentPhone: '(555) 555-5555', + leasingAgentTitle: null, + name: 'Hollywood Hills Heights', + postmarkedApplicationsReceivedByDate: null, + programRules: null, + rentalAssistance: + 'Housing Choice Vouchers, Section 8 and other valid rental assistance programs will be considered for this property. In the case of a', + rentalHistory: null, + requiredDocuments: null, + specialNotes: null, + waitlistCurrentSize: null, + waitlistMaxSize: null, + whatToExpect: + 'Applicants will be contacted by the property agent in rank order until vacancies are filled. All of the information that you have provided will be verified and your eligibility confirmed. Your application will be removed from the waitlist if you have made any fraudulent statements. If we cannot verify a housing preference that you have claimed, you will not receive the preference but will not be otherwise penalized. Should your application be chosen, be prepared to fill out a more detailed application and provide required supporting documents.', + status: ListingsStatusEnum.active, + reviewOrderType: ReviewOrderTypeEnum.waitlist, + displayWaitlistSize: false, + reservedCommunityDescription: null, + reservedCommunityMinAge: null, + resultLink: null, + isWaitlistOpen: false, + waitlistOpenSpots: null, + customMapPin: false, + publishedAt: new Date(), + listingsBuildingAddress: { + create: whiteHouse, + }, + listingsApplicationPickUpAddress: undefined, + listingsLeasingAgentAddress: undefined, + listingsApplicationDropOffAddress: undefined, + listingsApplicationMailingAddress: undefined, + reservedCommunityTypes: undefined, + listingImages: { + create: { + ordinal: 0, + assets: { + create: { + label: 'cloudinaryBuilding', + fileId: 'dev/apartment_building_2_b7ujdd', + }, + }, + }, + }, + }, + units: [ + { + amiPercentage: '30', + monthlyIncomeMin: '2000', + floor: 1, + maxOccupancy: 3, + minOccupancy: 1, + monthlyRent: '1200', + numBathrooms: 1, + numBedrooms: 1, + number: '101', + sqFeet: '750.00', + amiChart: { connect: { id: amiChart.id } }, + unitTypes: { + connect: { + id: unitTypes[0].id, + }, + }, + }, + ], + multiselectQuestions: [multiselectQuestion1, multiselectQuestion2], + }, + { + listing: { + additionalApplicationSubmissionNotes: null, + digitalApplication: true, + commonDigitalApplication: true, + paperApplication: false, + referralOpportunity: false, + assets: [], + accessibility: null, + amenities: null, + buildingTotalUnits: 0, + developer: 'ABS Housing', + householdSizeMax: 0, + householdSizeMin: 0, + neighborhood: null, + petPolicy: 'Pets are not permitted on the property. ', + smokingPolicy: null, + unitsAvailable: 0, + unitAmenities: 'Each unit comes with included central AC.', + servicesOffered: null, + yearBuilt: 2021, + applicationDueDate: dayjs(new Date()).add(1, 'hours').toDate(), + applicationOpenDate: dayjs(new Date()).subtract(7, 'days').toDate(), + applicationFee: '35', + applicationOrganization: null, + applicationPickUpAddressOfficeHours: null, + applicationPickUpAddressType: null, + applicationDropOffAddressOfficeHours: null, + applicationDropOffAddressType: null, + applicationMailingAddressType: null, + buildingSelectionCriteria: null, + costsNotIncluded: null, + creditHistory: null, + criminalBackground: null, + depositMin: '500', + depositMax: '0', + depositHelperText: + "or one month's rent may be higher for lower credit scores", + disableUnitsAccordion: false, + leasingAgentEmail: 'sgates@abshousing.com', + leasingAgentName: 'Samuel Gates', + leasingAgentOfficeHours: null, + leasingAgentPhone: '(888) 888-8888', + leasingAgentTitle: 'Property Manager', + name: 'District View Apartments', + postmarkedApplicationsReceivedByDate: null, + programRules: null, + rentalAssistance: + 'Housing Choice Vouchers, Section 8 and other valid rental assistance programs will be considered for this property. In the case of a', + rentalHistory: null, + requiredDocuments: null, + specialNotes: null, + waitlistCurrentSize: null, + waitlistMaxSize: null, + whatToExpect: + 'Applicants will be contacted by the property agent in rank order until vacancies are filled. All of the information that you have provided will be verified and your eligibility confirmed. Your application will be removed from the waitlist if you have made any fraudulent statements. If we cannot verify a housing preference that you have claimed, you will not receive the preference but will not be otherwise penalized. Should your application be chosen, be prepared to fill out a more detailed application and provide required supporting documents.', + status: ListingsStatusEnum.active, + reviewOrderType: ReviewOrderTypeEnum.lottery, + displayWaitlistSize: false, + reservedCommunityDescription: null, + reservedCommunityMinAge: null, + resultLink: null, + isWaitlistOpen: false, + waitlistOpenSpots: null, + customMapPin: false, + publishedAt: new Date(), + listingsApplicationPickUpAddress: undefined, + listingsApplicationDropOffAddress: undefined, + listingsApplicationMailingAddress: undefined, + reservedCommunityTypes: undefined, + }, + units: [ + { + amiPercentage: '30', + annualIncomeMin: null, + monthlyIncomeMin: '1985', + floor: 2, + maxOccupancy: 5, + minOccupancy: 2, + monthlyRent: '800', + numBathrooms: 2, + numBedrooms: 2, + number: '', + amiChart: { connect: { id: amiChart.id } }, + }, + { + amiPercentage: '30', + annualIncomeMin: null, + monthlyIncomeMin: '1985', + floor: 2, + maxOccupancy: 5, + minOccupancy: 2, + monthlyRent: '800', + numBathrooms: 2, + numBedrooms: 2, + number: '', + amiChart: { connect: { id: amiChart.id } }, + }, + { + amiPercentage: '30', + annualIncomeMin: null, + monthlyIncomeMin: '1985', + floor: 2, + maxOccupancy: 5, + minOccupancy: 2, + monthlyRent: '800', + numBathrooms: 2, + numBedrooms: 2, + amiChart: { connect: { id: amiChart.id } }, + }, + ], + multiselectQuestions: [multiselectQuestion1], + }, + { + listing: { + additionalApplicationSubmissionNotes: null, + digitalApplication: true, + commonDigitalApplication: true, + paperApplication: true, + referralOpportunity: false, + assets: [], + accessibility: null, + amenities: null, + buildingTotalUnits: 0, + developer: 'Cielo Housing', + householdSizeMax: 0, + householdSizeMin: 0, + neighborhood: 'North End', + petPolicy: null, + smokingPolicy: null, + unitsAvailable: 1, + unitAmenities: null, + servicesOffered: null, + yearBuilt: 1900, + applicationDueDate: null, + applicationOpenDate: dayjs(new Date()).subtract(1, 'days').toDate(), + applicationFee: '60', + applicationOrganization: null, + applicationPickUpAddressOfficeHours: null, + applicationPickUpAddressType: ApplicationAddressTypeEnum.leasingAgent, + applicationDropOffAddressOfficeHours: null, + applicationDropOffAddressType: ApplicationAddressTypeEnum.leasingAgent, + applicationMailingAddressType: ApplicationAddressTypeEnum.leasingAgent, + buildingSelectionCriteria: null, + costsNotIncluded: null, + creditHistory: null, + criminalBackground: null, + depositMin: '0', + depositMax: '50', + depositHelperText: + "or one month's rent may be higher for lower credit scores", + disableUnitsAccordion: false, + leasingAgentEmail: 'joe@smithrealty.com', + leasingAgentName: 'Joe Smith', + leasingAgentOfficeHours: '9:00am - 5:00pm, Monday-Friday', + leasingAgentPhone: '(773) 580-5897', + leasingAgentTitle: 'Senior Leasing Agent', + name: 'Blue Sky Apartments ', + postmarkedApplicationsReceivedByDate: '2025-06-06T23:00:00.000Z', + programRules: null, + rentalAssistance: + 'Housing Choice Vouchers, Section 8 and other valid rental assistance programs will be considered for this property. ', + rentalHistory: null, + requiredDocuments: null, + specialNotes: null, + waitlistCurrentSize: null, + waitlistMaxSize: null, + whatToExpect: + 'Applicants will be contacted by the property agent in rank order until vacancies are filled. All of the information that you have provided will be verified and your eligibility confirmed. Your application will be removed from the waitlist if you have made any fraudulent statements. If we cannot verify a housing preference that you have claimed, you will not receive the preference but will not be otherwise penalized. Should your application be chosen, be prepared to fill out a more detailed application and provide required supporting documents.', + status: ListingsStatusEnum.active, + reviewOrderType: ReviewOrderTypeEnum.firstComeFirstServe, + displayWaitlistSize: false, + reservedCommunityDescription: + 'Seniors over 55 are eligible for this property ', + reservedCommunityMinAge: null, + resultLink: null, + isWaitlistOpen: false, + waitlistOpenSpots: null, + customMapPin: false, + publishedAt: new Date(), + listingsBuildingAddress: { + create: whiteHouse, + }, + listingsApplicationMailingAddress: { + create: lincolnMemorial, + }, + listingsApplicationPickUpAddress: { + create: washingtonMonument, + }, + listingsLeasingAgentAddress: { + create: lincolnMemorial, + }, + listingsApplicationDropOffAddress: { + create: washingtonMonument, + }, + reservedCommunityTypes: undefined, + listingImages: { + create: [ + { + ordinal: 0, + assets: { + create: { + label: 'cloudinaryBuilding', + fileId: 'dev/trayan-xIOYJSVEZ8c-unsplash_f1axsg', + }, + }, + }, + ], + }, + }, + }, + { + listing: { + additionalApplicationSubmissionNotes: null, + digitalApplication: true, + commonDigitalApplication: true, + paperApplication: false, + referralOpportunity: false, + assets: [], + accessibility: null, + amenities: 'Includes handicap accessible entry and parking spots. ', + buildingTotalUnits: 0, + developer: 'ABS Housing', + householdSizeMax: 0, + householdSizeMin: 0, + neighborhood: null, + petPolicy: null, + smokingPolicy: 'No smoking is allowed on the property.', + unitsAvailable: 0, + unitAmenities: null, + servicesOffered: null, + yearBuilt: 2019, + applicationDueDate: null, + applicationOpenDate: dayjs(new Date()).subtract(100, 'days').toDate(), + applicationFee: '50', + applicationOrganization: null, + applicationPickUpAddressOfficeHours: null, + applicationPickUpAddressType: null, + applicationDropOffAddressOfficeHours: null, + applicationDropOffAddressType: null, + applicationMailingAddressType: null, + buildingSelectionCriteria: null, + costsNotIncluded: 'Residents are responsible for gas and electric. ', + creditHistory: null, + criminalBackground: null, + depositMin: '0', + depositMax: '0', + depositHelperText: + "or one month's rent may be higher for lower credit scores", + disableUnitsAccordion: false, + leasingAgentEmail: 'valleysenior@vpm.com', + leasingAgentName: 'Valley Property Management', + leasingAgentOfficeHours: '10 am - 6 pm Monday through Friday', + leasingAgentPhone: '(919) 999-9999', + leasingAgentTitle: 'Property Manager', + name: 'Valley Heights Senior Community', + postmarkedApplicationsReceivedByDate: null, + programRules: null, + rentalAssistance: + 'Housing Choice Vouchers, Section 8 and other valid rental assistance programs will be considered for this property. In the case of a', + rentalHistory: null, + requiredDocuments: null, + specialNotes: null, + waitlistCurrentSize: null, + waitlistMaxSize: null, + whatToExpect: + 'Applicants will be contacted by the property agent in rank order until vacancies are filled. All of the information that you have provided will be verified and your eligibility confirmed. Your application will be removed from the waitlist if you have made any fraudulent statements. If we cannot verify a housing preference that you have claimed, you will not receive the preference but will not be otherwise penalized. Should your application be chosen, be prepared to fill out a more detailed application and provide required supporting documents.', + status: ListingsStatusEnum.closed, + reviewOrderType: ReviewOrderTypeEnum.waitlist, + displayWaitlistSize: false, + reservedCommunityDescription: + 'Residents must be over the age of 55 at the time of move in.', + reservedCommunityMinAge: null, + resultLink: null, + isWaitlistOpen: false, + waitlistOpenSpots: null, + customMapPin: false, + publishedAt: dayjs(new Date()).subtract(3, 'days').toDate(), + closedAt: dayjs(new Date()).subtract(1, 'days').toDate(), + listingsApplicationPickUpAddress: undefined, + listingsLeasingAgentAddress: undefined, + listingsApplicationDropOffAddress: undefined, + listingsApplicationMailingAddress: undefined, + listingImages: { + create: [ + { + ordinal: 0, + assets: { + create: { + label: 'cloudinaryBuilding', + fileId: 'dev/apartment_ez3yyz', + }, + }, + }, + { + ordinal: 1, + assets: { + create: { + label: 'cloudinaryBuilding', + fileId: 'dev/interior_mc9erd', + }, + }, + }, + { + ordinal: 2, + assets: { + create: { + label: 'cloudinaryBuilding', + fileId: 'dev/inside_qo9wre', + }, + }, + }, + ], + }, + }, + }, + { + listing: { + additionalApplicationSubmissionNotes: null, + digitalApplication: true, + commonDigitalApplication: false, + paperApplication: false, + referralOpportunity: false, + assets: [], + accessibility: null, + amenities: null, + buildingTotalUnits: 0, + developer: 'La Villita Listings', + householdSizeMax: 0, + householdSizeMin: 0, + neighborhood: 'Koreatown', + petPolicy: null, + smokingPolicy: null, + unitsAvailable: 0, + unitAmenities: null, + servicesOffered: null, + yearBuilt: 1996, + applicationDueDate: null, + applicationOpenDate: dayjs(new Date()).subtract(30, 'days').toDate(), + applicationFee: null, + applicationOrganization: null, + applicationPickUpAddressOfficeHours: null, + applicationPickUpAddressType: null, + applicationDropOffAddressOfficeHours: null, + applicationDropOffAddressType: null, + applicationMailingAddressType: null, + buildingSelectionCriteria: null, + costsNotIncluded: null, + creditHistory: null, + criminalBackground: null, + depositMin: '0', + depositMax: '0', + depositHelperText: + "or one month's rent may be higher for lower credit scores", + disableUnitsAccordion: false, + leasingAgentEmail: 'joe@smith.com', + leasingAgentName: 'Joe Smith', + leasingAgentOfficeHours: null, + leasingAgentPhone: '(619) 591-5987', + leasingAgentTitle: null, + name: 'Little Village Apartments', + postmarkedApplicationsReceivedByDate: null, + programRules: null, + rentalAssistance: + 'Housing Choice Vouchers, Section 8 and other valid rental assistance programs will be considered for this property. In the case of a', + rentalHistory: null, + requiredDocuments: null, + specialNotes: null, + waitlistCurrentSize: null, + waitlistMaxSize: null, + whatToExpect: + 'Applicants will be contacted by the property agent in rank order until vacancies are filled. All of the information that you have provided will be verified and your eligibility confirmed. Your application will be removed from the waitlist if you have made any fraudulent statements. If we cannot verify a housing preference that you have claimed, you will not receive the preference but will not be otherwise penalized. Should your application be chosen, be prepared to fill out a more detailed application and provide required supporting documents.', + status: ListingsStatusEnum.pending, + reviewOrderType: ReviewOrderTypeEnum.waitlist, + displayWaitlistSize: false, + reservedCommunityDescription: null, + reservedCommunityMinAge: null, + resultLink: null, + isWaitlistOpen: true, + waitlistOpenSpots: 6, + customMapPin: false, + publishedAt: new Date(), + listingsApplicationPickUpAddress: undefined, + listingsApplicationDropOffAddress: undefined, + listingsApplicationMailingAddress: undefined, + listingImages: { + create: [ + { + ordinal: 0, + assets: { + create: { + label: 'cloudinaryBuilding', + fileId: 'dev/dillon-kydd-2keCPb73aQY-unsplash_lm7krp', + }, + }, + }, + ], + }, + }, + multiselectQuestions: [multiselectQuestion2], + }, + { + listing: { + additionalApplicationSubmissionNotes: null, + digitalApplication: true, + commonDigitalApplication: true, + paperApplication: false, + referralOpportunity: false, + assets: [], + accessibility: null, + amenities: null, + buildingTotalUnits: 0, + developer: 'Johnson Realtors', + householdSizeMax: 0, + householdSizeMin: 0, + neighborhood: 'Hyde Park', + petPolicy: null, + smokingPolicy: null, + unitsAvailable: 2, + unitAmenities: null, + servicesOffered: null, + yearBuilt: 1988, + applicationDueDate: dayjs(new Date()).add(7, 'days').toDate(), + applicationOpenDate: dayjs(new Date()).subtract(1, 'days').toDate(), + applicationFee: null, + applicationOrganization: null, + applicationPickUpAddressOfficeHours: null, + applicationPickUpAddressType: null, + applicationDropOffAddressOfficeHours: null, + applicationDropOffAddressType: null, + applicationMailingAddressType: null, + buildingSelectionCriteria: null, + costsNotIncluded: null, + creditHistory: null, + criminalBackground: null, + depositMin: '0', + depositMax: '0', + depositHelperText: + "or one month's rent may be higher for lower credit scores", + disableUnitsAccordion: true, + leasingAgentEmail: 'jenny@gold.com', + leasingAgentName: 'Jenny Gold', + leasingAgentOfficeHours: null, + leasingAgentPhone: '(208) 772-2856', + leasingAgentTitle: 'Lead Agent', + name: 'Elm Village', + postmarkedApplicationsReceivedByDate: null, + programRules: null, + rentalAssistance: + 'Housing Choice Vouchers, Section 8 and other valid rental assistance programs will be considered for this property. In the case of a', + rentalHistory: null, + requiredDocuments: 'Please bring proof of income and a recent paystub.', + specialNotes: null, + waitlistCurrentSize: null, + waitlistMaxSize: null, + whatToExpect: + 'Applicants will be contacted by the property agent in rank order until vacancies are filled. All of the information that you have provided will be verified and your eligibility confirmed. Your application will be removed from the waitlist if you have made any fraudulent statements. If we cannot verify a housing preference that you have claimed, you will not receive the preference but will not be otherwise penalized. Should your application be chosen, be prepared to fill out a more detailed application and provide required supporting documents.', + status: ListingsStatusEnum.active, + reviewOrderType: ReviewOrderTypeEnum.lottery, + displayWaitlistSize: false, + reservedCommunityDescription: null, + reservedCommunityMinAge: null, + resultLink: null, + isWaitlistOpen: false, + waitlistOpenSpots: null, + customMapPin: false, + publishedAt: new Date(), + listingsApplicationPickUpAddress: undefined, + listingsApplicationDropOffAddress: undefined, + reservedCommunityTypes: undefined, + listingImages: { + create: [ + { + ordinal: 0, + assets: { + create: { + label: 'cloudinaryBuilding', + fileId: 'dev/krzysztof-hepner-V7Q0Oh3Az-c-unsplash_xoj7sr', + }, + }, + }, + { + ordinal: 1, + assets: { + create: { + label: 'cloudinaryBuilding', + fileId: 'dev/blake-wheeler-zBHU08hdzhY-unsplash_swqash ', + }, + }, + }, + ], + }, + }, + }, + ].map(async (value, index) => { + const listing = await listingFactory(jurisdiction.id, prismaClient, { + amiChart: amiChart, + numberOfUnits: index, + listing: value.listing, + units: value.units, + multiselectQuestions: value.multiselectQuestions, + }); + await prismaClient.listings.create({ + data: listing, + }); + }); +}; diff --git a/backend_new/prisma/seed.ts b/backend_new/prisma/seed.ts index 42e4e6e674..d4725111d5 100644 --- a/backend_new/prisma/seed.ts +++ b/backend_new/prisma/seed.ts @@ -1,60 +1,43 @@ import { PrismaClient } from '@prisma/client'; -import { amiChartFactory } from './seed-helpers/ami-chart-factory'; +import { parseArgs } from 'node:util'; import { jurisdictionFactory } from './seed-helpers/jurisdiction-factory'; -import { listingFactory } from './seed-helpers/listing-factory'; -import { reservedCommunityTypeFactory } from './seed-helpers/reserved-community-type-factory'; -import { unitAccessibilityPriorityTypeFactory } from './seed-helpers/unit-accessibility-priority-type-factory'; -import { unitRentTypeFactory } from './seed-helpers/unit-rent-type-factory'; -import { unitTypeFactory } from './seed-helpers/unit-type-factory'; +import { stagingSeed } from './seed-staging'; +import { devSeeding } from './seed-dev'; +import { unitTypeFactoryAll } from './seed-helpers/unit-type-factory'; +import { unitAccessibilityPriorityTypeFactoryAll } from './seed-helpers/unit-accessibility-priority-type-factory'; +import { reservedCommunityTypeFactoryAll } from './seed-helpers/reserved-community-type-factory'; + +const options: { [name: string]: { type: 'string' | 'boolean' } } = { + environment: { type: 'string' }, + jurisdictionName: { type: 'string' }, +}; const prisma = new PrismaClient(); async function main() { - const jurisdiction = await prisma.jurisdictions.create({ - data: jurisdictionFactory(0), - }); - const amiChart = await prisma.amiChart.create({ - data: amiChartFactory(10, jurisdiction.id), - }); - const reservedCommunityType = await prisma.reservedCommunityTypes.create({ - data: reservedCommunityTypeFactory(6, jurisdiction.id), - }); - - const unitTypeIds: string[] = []; - for (let i = 0; i < 7; i++) { - const res = await prisma.unitTypes.create({ - data: unitTypeFactory(i), - }); - unitTypeIds.push(res.id); - } - - const unitAccessibilityPriorityTypeIds: string[] = []; - for (let i = 0; i < 7; i++) { - const res = await prisma.unitAccessibilityPriorityTypes.create({ - data: unitAccessibilityPriorityTypeFactory(i), - }); - unitAccessibilityPriorityTypeIds.push(res.id); - } - - const unitRentTypeIds: string[] = []; - for (let i = 0; i < 2; i++) { - const res = await prisma.unitRentTypes.create({ - data: unitRentTypeFactory(i), - }); - unitRentTypeIds.push(res.id); - } - - for (let i = 0; i < 5; i++) { - await prisma.listings.create({ - data: listingFactory( - i, - jurisdiction.id, - amiChart.id, - reservedCommunityType.id, - unitTypeIds[i], - unitAccessibilityPriorityTypeIds[i], - unitRentTypeIds[i % 2], - ), - }); + const { + values: { environment, jurisdictionName }, + } = parseArgs({ options }); + switch (environment) { + case 'production': + // Setting up a production database we would just need the bare minimum such as jurisdiction + const jurisdictionId = await prisma.jurisdictions.create({ + data: jurisdictionFactory(jurisdictionName as string), + }); + await unitTypeFactoryAll(prisma); + await unitAccessibilityPriorityTypeFactoryAll(prisma); + await reservedCommunityTypeFactoryAll(jurisdictionId.id, prisma); + break; + case 'staging': + // Staging setup should have realistic looking data with a preset list of listings + // along with all of the required tables (ami, users, etc) + stagingSeed(prisma, jurisdictionName as string); + break; + case 'development': + default: + // Development is less realistic data, but can be more experimental and also should + // be partially randomized so we cover all bases + devSeeding(prisma); + break; } } main() diff --git a/backend_new/src/services/translation.service.ts b/backend_new/src/services/translation.service.ts index 672a69f349..9b06c30851 100644 --- a/backend_new/src/services/translation.service.ts +++ b/backend_new/src/services/translation.service.ts @@ -1,6 +1,6 @@ import { Injectable } from '@nestjs/common'; import { PrismaService } from './prisma.service'; -import { LanguagesEnum, Prisma } from '@prisma/client'; +import { LanguagesEnum } from '@prisma/client'; import { ListingGet } from '../dtos/listings/listing-get.dto'; import { GoogleTranslateService } from './google-translate.service'; import * as lodash from 'lodash'; diff --git a/backend_new/test/integration/ami-chart.e2e-spec.ts b/backend_new/test/integration/ami-chart.e2e-spec.ts index fc724be89f..0e2711d041 100644 --- a/backend_new/test/integration/ami-chart.e2e-spec.ts +++ b/backend_new/test/integration/ami-chart.e2e-spec.ts @@ -26,14 +26,14 @@ describe('AmiChart Controller Tests', () => { await app.init(); const jurisdictionA = await prisma.jurisdictions.create({ - data: jurisdictionFactory(10), + data: jurisdictionFactory(), }); jurisdictionAId = jurisdictionA.id; }); it('testing list endpoint', async () => { const jurisdictionB = await prisma.jurisdictions.create({ - data: jurisdictionFactory(11), + data: jurisdictionFactory(), }); const amiChartA = await prisma.amiChart.create({ data: amiChartFactory(10, jurisdictionAId), @@ -103,7 +103,7 @@ describe('AmiChart Controller Tests', () => { .put(`/amiCharts/${amiChartA.id}`) .send({ id: amiChartA.id, - name: 'name: 11', + name: 'updated name', items: [ { percentOfAmi: 80, @@ -114,7 +114,7 @@ describe('AmiChart Controller Tests', () => { } as AmiChartUpdate) .expect(200); - expect(res.body.name).toEqual('name: 11'); + expect(res.body.name).toEqual('updated name'); expect(res.body.items).toEqual([ { percentOfAmi: 80, diff --git a/backend_new/test/integration/jurisdiction.e2e-spec.ts b/backend_new/test/integration/jurisdiction.e2e-spec.ts index 09aab3fbdc..c23fc6d69f 100644 --- a/backend_new/test/integration/jurisdiction.e2e-spec.ts +++ b/backend_new/test/integration/jurisdiction.e2e-spec.ts @@ -26,10 +26,10 @@ describe('Jurisdiction Controller Tests', () => { it('testing list endpoint', async () => { const jurisdictionA = await prisma.jurisdictions.create({ - data: jurisdictionFactory(70), + data: jurisdictionFactory(), }); const jurisdictionB = await prisma.jurisdictions.create({ - data: jurisdictionFactory(80), + data: jurisdictionFactory(), }); const res = await request(app.getHttpServer()) @@ -54,7 +54,7 @@ describe('Jurisdiction Controller Tests', () => { it('testing retrieve endpoint', async () => { const jurisdictionA = await prisma.jurisdictions.create({ - data: jurisdictionFactory(101), + data: jurisdictionFactory(), }); const res = await request(app.getHttpServer()) @@ -76,7 +76,7 @@ describe('Jurisdiction Controller Tests', () => { it('testing retrieveByName endpoint', async () => { const jurisdictionA = await prisma.jurisdictions.create({ - data: jurisdictionFactory(110), + data: jurisdictionFactory(), }); const res = await request(app.getHttpServer()) @@ -131,7 +131,7 @@ describe('Jurisdiction Controller Tests', () => { it('testing update endpoint', async () => { const jurisdictionA = await prisma.jurisdictions.create({ - data: jurisdictionFactory(120), + data: jurisdictionFactory(), }); const res = await request(app.getHttpServer()) @@ -170,7 +170,7 @@ describe('Jurisdiction Controller Tests', () => { it('testing delete endpoint', async () => { const jurisdictionA = await prisma.jurisdictions.create({ - data: jurisdictionFactory(160), + data: jurisdictionFactory(), }); const res = await request(app.getHttpServer()) diff --git a/backend_new/test/integration/listing.e2e-spec.ts b/backend_new/test/integration/listing.e2e-spec.ts index b928129111..abc5b84112 100644 --- a/backend_new/test/integration/listing.e2e-spec.ts +++ b/backend_new/test/integration/listing.e2e-spec.ts @@ -27,7 +27,7 @@ describe('Listing Controller Tests', () => { await app.init(); const jurisdiction = await prisma.jurisdictions.create({ - data: jurisdictionFactory(100), + data: jurisdictionFactory(), }); jurisdictionAId = jurisdiction.id; @@ -49,12 +49,14 @@ describe('Listing Controller Tests', () => { }); it('list test no params some data', async () => { - await prisma.listings.create({ - data: listingFactory(10, jurisdictionAId), + const listing1 = await listingFactory(jurisdictionAId, prisma); + const listing1Created = await prisma.listings.create({ + data: listing1, }); - await prisma.listings.create({ - data: listingFactory(50, jurisdictionAId), + const listing2 = await listingFactory(jurisdictionAId, prisma); + const listing2Created = await prisma.listings.create({ + data: listing2, }); const res = await request(app.getHttpServer()).get('/listings').expect(200); @@ -67,11 +69,11 @@ describe('Listing Controller Tests', () => { totalPages: 1, }); - const items = res.body.items.sort((a, b) => (a.name < b.name ? -1 : 1)); + const items = res.body.items.map((item) => item.name); - expect(res.body.items.length).toEqual(2); - expect(items[0].name).toEqual('name: 10'); - expect(items[1].name).toEqual('name: 50'); + expect(items.length).toBeGreaterThanOrEqual(2); + expect(items).toContain(listing1Created.name); + expect(items).toContain(listing2Created.name); }); it('list test params no data', async () => { @@ -82,7 +84,7 @@ describe('Listing Controller Tests', () => { filter: [ { $comparison: Compare.IN, - name: 'name: 11,name: 51', + name: 'random name', }, ], }; @@ -105,13 +107,20 @@ describe('Listing Controller Tests', () => { }); it('list test params some data', async () => { - await prisma.listings.create({ - data: listingFactory(11, jurisdictionAId), + const listing1 = await listingFactory(jurisdictionAId, prisma); + const listing1Created = await prisma.listings.create({ + data: listing1, }); - await prisma.listings.create({ - data: listingFactory(51, jurisdictionAId), + + const listing2 = await listingFactory(jurisdictionAId, prisma); + const listing2Created = await prisma.listings.create({ + data: listing2, }); + const orderedNames = [listing1Created.name, listing2Created.name].sort( + (a, b) => a.localeCompare(b), + ); + let queryParams: ListingsQueryParams = { limit: 1, page: 1, @@ -119,7 +128,7 @@ describe('Listing Controller Tests', () => { filter: [ { $comparison: Compare.IN, - name: 'name: 11,name: 51', + name: orderedNames.toString(), }, ], orderBy: [ListingOrderByKeys.name], @@ -140,7 +149,7 @@ describe('Listing Controller Tests', () => { }); expect(res.body.items.length).toEqual(1); - expect(res.body.items[0].name).toEqual('name: 11'); + expect(res.body.items[0].name).toEqual(orderedNames[0]); queryParams = { limit: 1, @@ -149,7 +158,7 @@ describe('Listing Controller Tests', () => { filter: [ { $comparison: Compare.IN, - name: 'name: 11,name: 51', + name: orderedNames.toString(), }, ], orderBy: [ListingOrderByKeys.name], @@ -169,6 +178,6 @@ describe('Listing Controller Tests', () => { totalPages: 2, }); expect(res.body.items.length).toEqual(1); - expect(res.body.items[0].name).toEqual('name: 51'); + expect(res.body.items[0].name).toEqual(orderedNames[1]); }); }); diff --git a/backend_new/test/integration/reserved-community-type.e2e-spec.ts b/backend_new/test/integration/reserved-community-type.e2e-spec.ts index 328964a973..a45b4f8481 100644 --- a/backend_new/test/integration/reserved-community-type.e2e-spec.ts +++ b/backend_new/test/integration/reserved-community-type.e2e-spec.ts @@ -27,24 +27,24 @@ describe('ReservedCommunityType Controller Tests', () => { await app.init(); const jurisdictionA = await prisma.jurisdictions.create({ - data: jurisdictionFactory(12), + data: jurisdictionFactory(), }); jurisdictionAId = jurisdictionA.id; }); it('testing list endpoint without params', async () => { const jurisdictionA = await prisma.jurisdictions.create({ - data: jurisdictionFactory(13), + data: jurisdictionFactory(), }); const jurisdictionB = await prisma.jurisdictions.create({ - data: jurisdictionFactory(14), + data: jurisdictionFactory(), }); - await prisma.reservedCommunityTypes.create({ - data: reservedCommunityTypeFactory(10, jurisdictionA.id), + const rctJurisdictionA = await prisma.reservedCommunityTypes.create({ + data: reservedCommunityTypeFactory(jurisdictionA.id), }); - await prisma.reservedCommunityTypes.create({ - data: reservedCommunityTypeFactory(10, jurisdictionB.id), + const rctJurisdictionB = await prisma.reservedCommunityTypes.create({ + data: reservedCommunityTypeFactory(jurisdictionB.id), }); const res = await request(app.getHttpServer()) @@ -54,21 +54,21 @@ describe('ReservedCommunityType Controller Tests', () => { expect(res.body.length).toBeGreaterThanOrEqual(2); const typeNames = res.body.map((value) => value.name); const jurisdictions = res.body.map((value) => value.jurisdictions.id); - expect(typeNames).toContain('name: 10'); - expect(typeNames).toContain('name: 10'); + expect(typeNames).toContain(rctJurisdictionA.name); + expect(typeNames).toContain(rctJurisdictionB.name); expect(jurisdictions).toContain(jurisdictionA.id); expect(jurisdictions).toContain(jurisdictionB.id); }); it('testing list endpoint with params', async () => { const jurisdictionB = await prisma.jurisdictions.create({ - data: jurisdictionFactory(15), + data: jurisdictionFactory(), }); const reservedCommunityTypeA = await prisma.reservedCommunityTypes.create({ - data: reservedCommunityTypeFactory(10, jurisdictionAId), + data: reservedCommunityTypeFactory(jurisdictionAId), }); await prisma.reservedCommunityTypes.create({ - data: reservedCommunityTypeFactory(10, jurisdictionB.id), + data: reservedCommunityTypeFactory(jurisdictionB.id), }); const queryParams: ReservedCommunityTypeQueryParams = { jurisdictionId: jurisdictionAId, @@ -96,7 +96,7 @@ describe('ReservedCommunityType Controller Tests', () => { it('testing retrieve endpoint', async () => { const reservedCommunityTypeA = await prisma.reservedCommunityTypes.create({ - data: reservedCommunityTypeFactory(10, jurisdictionAId), + data: reservedCommunityTypeFactory(jurisdictionAId), }); const res = await request(app.getHttpServer()) @@ -139,7 +139,7 @@ describe('ReservedCommunityType Controller Tests', () => { it('testing update endpoint', async () => { const reservedCommunityTypeA = await prisma.reservedCommunityTypes.create({ - data: reservedCommunityTypeFactory(10, jurisdictionAId), + data: reservedCommunityTypeFactory(jurisdictionAId), }); const res = await request(app.getHttpServer()) @@ -170,7 +170,7 @@ describe('ReservedCommunityType Controller Tests', () => { it('testing delete endpoint', async () => { const reservedCommunityTypeA = await prisma.reservedCommunityTypes.create({ - data: reservedCommunityTypeFactory(16, jurisdictionAId), + data: reservedCommunityTypeFactory(jurisdictionAId), }); const res = await request(app.getHttpServer()) diff --git a/backend_new/test/integration/unit-accessibility-priority-type.e2e-spec.ts b/backend_new/test/integration/unit-accessibility-priority-type.e2e-spec.ts index d345079f64..e1d8f92579 100644 --- a/backend_new/test/integration/unit-accessibility-priority-type.e2e-spec.ts +++ b/backend_new/test/integration/unit-accessibility-priority-type.e2e-spec.ts @@ -3,7 +3,7 @@ import { INestApplication } from '@nestjs/common'; import * as request from 'supertest'; import { AppModule } from '../../src/app.module'; import { PrismaService } from '../../src/services/prisma.service'; -import { unitAccessibilityPriorityTypeFactory } from '../../prisma/seed-helpers/unit-accessibility-priority-type-factory'; +import { unitAccessibilityPriorityTypeFactorySingle } from '../../prisma/seed-helpers/unit-accessibility-priority-type-factory'; import { UnitAccessibilityPriorityTypeCreate } from '../../src/dtos/unit-accessibility-priority-types/unit-accessibility-priority-type-create.dto'; import { UnitAccessibilityPriorityTypeUpdate } from '../../src/dtos/unit-accessibility-priority-types/unit-accessibility-priority-type-update.dto'; import { IdDTO } from 'src/dtos/shared/id.dto'; @@ -25,10 +25,10 @@ describe('UnitAccessibilityPriorityType Controller Tests', () => { it('testing list endpoint', async () => { const unitTypeA = await prisma.unitAccessibilityPriorityTypes.create({ - data: unitAccessibilityPriorityTypeFactory(7), + data: unitAccessibilityPriorityTypeFactorySingle(), }); const unitTypeB = await prisma.unitAccessibilityPriorityTypes.create({ - data: unitAccessibilityPriorityTypeFactory(8), + data: unitAccessibilityPriorityTypeFactorySingle(), }); const res = await request(app.getHttpServer()) @@ -53,7 +53,7 @@ describe('UnitAccessibilityPriorityType Controller Tests', () => { it('testing retrieve endpoint', async () => { const unitTypeA = await prisma.unitAccessibilityPriorityTypes.create({ - data: unitAccessibilityPriorityTypeFactory(10), + data: unitAccessibilityPriorityTypeFactorySingle(), }); const res = await request(app.getHttpServer()) @@ -64,7 +64,7 @@ describe('UnitAccessibilityPriorityType Controller Tests', () => { }); it('testing create endpoint', async () => { - const name = unitAccessibilityPriorityTypeFactory(10).name; + const name = unitAccessibilityPriorityTypeFactorySingle().name; const res = await request(app.getHttpServer()) .post('/unitAccessibilityPriorityTypes') .send({ @@ -76,7 +76,7 @@ describe('UnitAccessibilityPriorityType Controller Tests', () => { }); it("update endpoint with id that doesn't exist should error", async () => { - const name = unitAccessibilityPriorityTypeFactory(10).name; + const name = unitAccessibilityPriorityTypeFactorySingle().name; const id = randomUUID(); const res = await request(app.getHttpServer()) .put(`/unitAccessibilityPriorityTypes/${id}`) @@ -92,9 +92,9 @@ describe('UnitAccessibilityPriorityType Controller Tests', () => { it('testing update endpoint', async () => { const unitTypeA = await prisma.unitAccessibilityPriorityTypes.create({ - data: unitAccessibilityPriorityTypeFactory(10), + data: unitAccessibilityPriorityTypeFactorySingle(), }); - const name = unitAccessibilityPriorityTypeFactory(11).name; + const name = unitAccessibilityPriorityTypeFactorySingle().name; const res = await request(app.getHttpServer()) .put(`/unitAccessibilityPriorityTypes/${unitTypeA.id}`) .send({ @@ -121,7 +121,7 @@ describe('UnitAccessibilityPriorityType Controller Tests', () => { it('testing delete endpoint', async () => { const unitTypeA = await prisma.unitAccessibilityPriorityTypes.create({ - data: unitAccessibilityPriorityTypeFactory(16), + data: unitAccessibilityPriorityTypeFactorySingle(), }); const res = await request(app.getHttpServer()) diff --git a/backend_new/test/integration/unit-rent-type.e2e-spec.ts b/backend_new/test/integration/unit-rent-type.e2e-spec.ts index ad8e4eee83..f84e2cdbdf 100644 --- a/backend_new/test/integration/unit-rent-type.e2e-spec.ts +++ b/backend_new/test/integration/unit-rent-type.e2e-spec.ts @@ -25,10 +25,10 @@ describe('UnitRentType Controller Tests', () => { it('testing list endpoint', async () => { const unitRentTypeA = await prisma.unitRentTypes.create({ - data: unitRentTypeFactory(7), + data: unitRentTypeFactory(), }); const unitRentTypeB = await prisma.unitRentTypes.create({ - data: unitRentTypeFactory(8), + data: unitRentTypeFactory(), }); const res = await request(app.getHttpServer()) @@ -53,7 +53,7 @@ describe('UnitRentType Controller Tests', () => { it('testing retrieve endpoint', async () => { const unitRentTypeA = await prisma.unitRentTypes.create({ - data: unitRentTypeFactory(10), + data: unitRentTypeFactory(), }); const res = await request(app.getHttpServer()) @@ -64,7 +64,7 @@ describe('UnitRentType Controller Tests', () => { }); it('testing create endpoint', async () => { - const name = unitRentTypeFactory(10).name; + const name = unitRentTypeFactory().name; const res = await request(app.getHttpServer()) .post('/unitRentTypes') .send({ @@ -81,7 +81,7 @@ describe('UnitRentType Controller Tests', () => { .put(`/unitRentTypes/${id}`) .send({ id: id, - name: unitRentTypeFactory(10).name, + name: unitRentTypeFactory().name, } as UnitRentTypeUpdate) .expect(404); expect(res.body.message).toEqual( @@ -91,9 +91,9 @@ describe('UnitRentType Controller Tests', () => { it('testing update endpoint', async () => { const unitRentTypeA = await prisma.unitRentTypes.create({ - data: unitRentTypeFactory(10), + data: unitRentTypeFactory(), }); - const name = unitRentTypeFactory(11).name; + const name = unitRentTypeFactory().name; const res = await request(app.getHttpServer()) .put(`/unitRentTypes/${unitRentTypeA.id}`) .send({ @@ -120,7 +120,7 @@ describe('UnitRentType Controller Tests', () => { it('testing delete endpoint', async () => { const unitRentTypeA = await prisma.unitRentTypes.create({ - data: unitRentTypeFactory(16), + data: unitRentTypeFactory(), }); const res = await request(app.getHttpServer()) diff --git a/backend_new/test/integration/unit-type.e2e-spec.ts b/backend_new/test/integration/unit-type.e2e-spec.ts index 347cd0de6c..8685460477 100644 --- a/backend_new/test/integration/unit-type.e2e-spec.ts +++ b/backend_new/test/integration/unit-type.e2e-spec.ts @@ -3,16 +3,19 @@ import { INestApplication } from '@nestjs/common'; import * as request from 'supertest'; import { AppModule } from '../../src/app.module'; import { PrismaService } from '../../src/services/prisma.service'; -import { unitTypeFactory } from '../../prisma/seed-helpers/unit-type-factory'; +import { + unitTypeFactoryAll, + unitTypeFactorySingle, +} from '../../prisma/seed-helpers/unit-type-factory'; import { UnitTypeCreate } from '../../src/dtos/unit-types/unit-type-create.dto'; import { UnitTypeUpdate } from '../../src/dtos/unit-types/unit-type-update.dto'; import { IdDTO } from 'src/dtos/shared/id.dto'; import { randomUUID } from 'crypto'; +import { UnitTypeEnum } from '@prisma/client'; describe('UnitType Controller Tests', () => { let app: INestApplication; let prisma: PrismaService; - beforeAll(async () => { const moduleFixture: TestingModule = await Test.createTestingModule({ imports: [AppModule], @@ -21,24 +24,19 @@ describe('UnitType Controller Tests', () => { app = moduleFixture.createNestApplication(); prisma = moduleFixture.get(PrismaService); await app.init(); + await unitTypeFactoryAll(prisma); }); it('testing list endpoint', async () => { - const unitTypeA = await prisma.unitTypes.create({ - data: unitTypeFactory(7), - }); - const unitTypeB = await prisma.unitTypes.create({ - data: unitTypeFactory(8), - }); - const res = await request(app.getHttpServer()) .get(`/unitTypes?`) .expect(200); - - expect(res.body.length).toBeGreaterThanOrEqual(2); + // all unit types are returned + expect(res.body.length).toEqual(7); + // check for random unit types const unitTypeNames = res.body.map((value) => value.name); - expect(unitTypeNames).toContain(unitTypeA.name); - expect(unitTypeNames).toContain(unitTypeB.name); + expect(unitTypeNames).toContain(UnitTypeEnum.SRO); + expect(unitTypeNames).toContain(UnitTypeEnum.threeBdrm); }); it("retrieve endpoint with id that doesn't exist should error", async () => { @@ -52,9 +50,7 @@ describe('UnitType Controller Tests', () => { }); it('testing retrieve endpoint', async () => { - const unitTypeA = await prisma.unitTypes.create({ - data: unitTypeFactory(10), - }); + const unitTypeA = await unitTypeFactorySingle(prisma, UnitTypeEnum.oneBdrm); const res = await request(app.getHttpServer()) .get(`/unitTypes/${unitTypeA.id}`) @@ -64,7 +60,7 @@ describe('UnitType Controller Tests', () => { }); it('testing create endpoint', async () => { - const name = unitTypeFactory(10).name; + const name = UnitTypeEnum.twoBdrm; const res = await request(app.getHttpServer()) .post('/unitTypes') .send({ @@ -78,7 +74,7 @@ describe('UnitType Controller Tests', () => { it("update endpoint with id that doesn't exist should error", async () => { const id = randomUUID(); - const name = unitTypeFactory(10).name; + const name = UnitTypeEnum.fourBdrm; const res = await request(app.getHttpServer()) .put(`/unitTypes/${id}`) .send({ @@ -93,10 +89,8 @@ describe('UnitType Controller Tests', () => { }); it('testing update endpoint', async () => { - const unitTypeA = await prisma.unitTypes.create({ - data: unitTypeFactory(10), - }); - const name = unitTypeFactory(11).name; + const unitTypeA = await unitTypeFactorySingle(prisma, UnitTypeEnum.SRO); + const name = UnitTypeEnum.SRO; const res = await request(app.getHttpServer()) .put(`/unitTypes/${unitTypeA.id}`) .send({ @@ -124,9 +118,7 @@ describe('UnitType Controller Tests', () => { }); it('testing delete endpoint', async () => { - const unitTypeA = await prisma.unitTypes.create({ - data: unitTypeFactory(16), - }); + const unitTypeA = await unitTypeFactorySingle(prisma, UnitTypeEnum.studio); const res = await request(app.getHttpServer()) .delete(`/unitTypes`) diff --git a/backend_new/test/unit/services/listing.service.spec.ts b/backend_new/test/unit/services/listing.service.spec.ts index a1ed62d28c..e7a751d454 100644 --- a/backend_new/test/unit/services/listing.service.spec.ts +++ b/backend_new/test/unit/services/listing.service.spec.ts @@ -7,7 +7,7 @@ import { OrderByEnum } from '../../../src/enums/shared/order-by-enum'; import { ListingFilterKeys } from '../../../src/enums/listings/filter-key-enum'; import { Compare } from '../../../src/dtos/shared/base-filter.dto'; import { ListingFilterParams } from '../../../src/dtos/listings/listings-filter-params.dto'; -import { LanguagesEnum } from '@prisma/client'; +import { LanguagesEnum, UnitTypeEnum } from '@prisma/client'; import { Unit } from '../../../src/dtos/units/unit-get.dto'; import { UnitTypeSort } from '../../../src/utilities/unit-utilities'; import { ListingGet } from '../../../src/dtos/listings/listing-get.dto'; @@ -49,7 +49,7 @@ const mockListing = ( id: `unitType ${i}`, createdAt: date, updatedAt: date, - name: UnitTypeSort[i % UnitTypeSort.length], + name: UnitTypeSort[i % UnitTypeSort.length] as UnitTypeEnum, numBedrooms: i, }, unitAmiChartOverrides: { diff --git a/backend_new/test/unit/services/unit-accessibility-priority-type.service.spec.ts b/backend_new/test/unit/services/unit-accessibility-priority-type.service.spec.ts index 7e33919722..14b303f57a 100644 --- a/backend_new/test/unit/services/unit-accessibility-priority-type.service.spec.ts +++ b/backend_new/test/unit/services/unit-accessibility-priority-type.service.spec.ts @@ -5,16 +5,23 @@ import { UnitAccessibilityPriorityTypeCreate } from '../../../src/dtos/unit-acce import { UnitAccessibilityPriorityTypeUpdate } from '../../../src/dtos/unit-accessibility-priority-types/unit-accessibility-priority-type-update.dto'; import { UnitAccessibilityPriorityType } from '../../../src/dtos/unit-accessibility-priority-types/unit-accessibility-priority-type.dto'; import { randomUUID } from 'crypto'; -import { unitPriorityTypeArray } from '../../../prisma/seed-helpers/unit-accessibility-priority-type-factory'; +import { + unitAccesibilityPriorityTypeAsArray, + unitAccessibilityPriorityTypeFactorySingle, +} from '../../../prisma/seed-helpers/unit-accessibility-priority-type-factory'; +import { UnitAccessibilityPriorityTypeEnum } from '@prisma/client'; describe('Testing unit accessibility priority type service', () => { let service: UnitAccessibilityPriorityTypeService; let prisma: PrismaService; - const mockUnitAccessibilityPriorityType = (position: number, date: Date) => { + const mockUnitAccessibilityPriorityType = ( + date: Date, + uapType?: UnitAccessibilityPriorityTypeEnum, + ) => { return { id: randomUUID(), - name: unitPriorityTypeArray[position].name, + name: unitAccessibilityPriorityTypeFactorySingle(uapType).name, createdAt: date, updatedAt: date, }; @@ -24,11 +31,12 @@ describe('Testing unit accessibility priority type service', () => { numberToCreate: number, date: Date, ) => { - const toReturn = []; - for (let i = 0; i < numberToCreate; i++) { - toReturn.push(mockUnitAccessibilityPriorityType(i, date)); - } - return toReturn; + return [...new Array(numberToCreate)].map((_, index) => { + return mockUnitAccessibilityPriorityType( + date, + unitAccesibilityPriorityTypeAsArray[index], + ); + }); }; beforeAll(async () => { @@ -52,19 +60,19 @@ describe('Testing unit accessibility priority type service', () => { expect(await service.list()).toEqual([ { id: mockedValue[0].id, - name: unitPriorityTypeArray[0].name, + name: unitAccesibilityPriorityTypeAsArray[0], createdAt: date, updatedAt: date, }, { id: mockedValue[1].id, - name: unitPriorityTypeArray[1].name, + name: unitAccesibilityPriorityTypeAsArray[1], createdAt: date, updatedAt: date, }, { id: mockedValue[2].id, - name: unitPriorityTypeArray[2].name, + name: unitAccesibilityPriorityTypeAsArray[2], createdAt: date, updatedAt: date, }, @@ -75,14 +83,17 @@ describe('Testing unit accessibility priority type service', () => { it('testing findOne() with id present', async () => { const date = new Date(); - const mockedValue = mockUnitAccessibilityPriorityType(3, date); + const mockedValue = mockUnitAccessibilityPriorityType( + date, + UnitAccessibilityPriorityTypeEnum.hearing, + ); prisma.unitAccessibilityPriorityTypes.findUnique = jest .fn() .mockResolvedValue(mockedValue); expect(await service.findOne('example Id')).toEqual({ id: mockedValue.id, - name: unitPriorityTypeArray[3].name, + name: UnitAccessibilityPriorityTypeEnum.hearing, createdAt: date, updatedAt: date, }); @@ -116,25 +127,28 @@ describe('Testing unit accessibility priority type service', () => { it('testing create()', async () => { const date = new Date(); - const mockedValue = mockUnitAccessibilityPriorityType(3, date); + const mockedValue = mockUnitAccessibilityPriorityType( + date, + UnitAccessibilityPriorityTypeEnum.mobilityAndHearing, + ); prisma.unitAccessibilityPriorityTypes.create = jest .fn() .mockResolvedValue(mockedValue); const params: UnitAccessibilityPriorityTypeCreate = { - name: unitPriorityTypeArray[3].name, + name: UnitAccessibilityPriorityTypeEnum.mobilityAndHearing, }; expect(await service.create(params)).toEqual({ id: mockedValue.id, - name: unitPriorityTypeArray[3].name, + name: UnitAccessibilityPriorityTypeEnum.mobilityAndHearing, createdAt: date, updatedAt: date, }); expect(prisma.unitAccessibilityPriorityTypes.create).toHaveBeenCalledWith({ data: { - name: unitPriorityTypeArray[3].name, + name: UnitAccessibilityPriorityTypeEnum.mobilityAndHearing, }, }); }); @@ -142,24 +156,27 @@ describe('Testing unit accessibility priority type service', () => { it('testing update() existing record found', async () => { const date = new Date(); - const mockedUnitType = mockUnitAccessibilityPriorityType(3, date); + const mockedUnitType = mockUnitAccessibilityPriorityType( + date, + UnitAccessibilityPriorityTypeEnum.mobilityAndHearing, + ); prisma.unitAccessibilityPriorityTypes.findUnique = jest .fn() .mockResolvedValue(mockedUnitType); prisma.unitAccessibilityPriorityTypes.update = jest.fn().mockResolvedValue({ ...mockedUnitType, - name: unitPriorityTypeArray[4].name, + name: UnitAccessibilityPriorityTypeEnum.mobilityAndVisual, }); const params: UnitAccessibilityPriorityTypeUpdate = { - name: unitPriorityTypeArray[4].name, + name: UnitAccessibilityPriorityTypeEnum.mobilityAndVisual, id: mockedUnitType.id, }; expect(await service.update(params)).toEqual({ id: mockedUnitType.id, - name: unitPriorityTypeArray[4].name, + name: UnitAccessibilityPriorityTypeEnum.mobilityAndVisual, createdAt: date, updatedAt: date, }); @@ -174,7 +191,7 @@ describe('Testing unit accessibility priority type service', () => { expect(prisma.unitAccessibilityPriorityTypes.update).toHaveBeenCalledWith({ data: { - name: unitPriorityTypeArray[4].name, + name: UnitAccessibilityPriorityTypeEnum.mobilityAndVisual, }, where: { id: mockedUnitType.id, @@ -193,7 +210,7 @@ describe('Testing unit accessibility priority type service', () => { .mockResolvedValue(null); const params: UnitAccessibilityPriorityType = { - name: unitPriorityTypeArray[4].name, + name: UnitAccessibilityPriorityTypeEnum.mobilityAndVisual, id: 'example id', createdAt: date, updatedAt: date, @@ -215,14 +232,22 @@ describe('Testing unit accessibility priority type service', () => { it('testing delete()', async () => { const date = new Date(); - const mockedUnitType = mockUnitAccessibilityPriorityType(3, date); + const mockedValue = mockUnitAccessibilityPriorityType( + date, + UnitAccessibilityPriorityTypeEnum.hearing, + ); prisma.unitAccessibilityPriorityTypes.findUnique = jest .fn() - .mockResolvedValue(mockedUnitType); + .mockResolvedValue(mockedValue); prisma.unitAccessibilityPriorityTypes.delete = jest .fn() - .mockResolvedValue(mockUnitAccessibilityPriorityType(3, date)); + .mockResolvedValue( + mockUnitAccessibilityPriorityType( + date, + UnitAccessibilityPriorityTypeEnum.mobility, + ), + ); expect(await service.delete('example Id')).toEqual({ success: true, @@ -255,10 +280,13 @@ describe('Testing unit accessibility priority type service', () => { it('testing findOrThrow() record not found', async () => { const date = new Date(); - const mockedAmi = mockUnitAccessibilityPriorityType(3, date); + const mockedValue = mockUnitAccessibilityPriorityType( + date, + UnitAccessibilityPriorityTypeEnum.hearing, + ); prisma.unitAccessibilityPriorityTypes.findUnique = jest .fn() - .mockResolvedValue(mockedAmi); + .mockResolvedValue(mockedValue); expect(await service.findOrThrow('example id')).toEqual(true); diff --git a/backend_new/test/unit/services/unit-rent-type.service.spec.ts b/backend_new/test/unit/services/unit-rent-type.service.spec.ts index 86925650b8..ea035392a3 100644 --- a/backend_new/test/unit/services/unit-rent-type.service.spec.ts +++ b/backend_new/test/unit/services/unit-rent-type.service.spec.ts @@ -4,16 +4,20 @@ import { UnitRentTypeService } from '../../../src/services/unit-rent-type.servic import { UnitRentTypeCreate } from '../../../src/dtos/unit-rent-types/unit-rent-type-create.dto'; import { UnitRentTypeUpdate } from '../../../src/dtos/unit-rent-types/unit-rent-type-update.dto'; import { randomUUID } from 'crypto'; -import { unitRentTypeFactory } from '../../../prisma/seed-helpers/unit-rent-type-factory'; +import { + unitRentTypeArray, + unitRentTypeFactory, +} from '../../../prisma/seed-helpers/unit-rent-type-factory'; +import { UnitRentTypeEnum } from '@prisma/client'; describe('Testing unit rent type service', () => { let service: UnitRentTypeService; let prisma: PrismaService; - const mockUnitRentType = (position: number, date: Date) => { + const mockUnitRentType = (type: UnitRentTypeEnum, date: Date) => { return { id: randomUUID(), - name: unitRentTypeFactory(position).name, + name: type, createdAt: date, updatedAt: date, }; @@ -22,7 +26,9 @@ describe('Testing unit rent type service', () => { const mockUnitRentTypeSet = (numberToCreate: number, date: Date) => { const toReturn = []; for (let i = 0; i < numberToCreate; i++) { - toReturn.push(mockUnitRentType(i, date)); + toReturn.push( + mockUnitRentType(unitRentTypeArray[i % unitRentTypeArray.length], date), + ); } return toReturn; }; @@ -44,19 +50,19 @@ describe('Testing unit rent type service', () => { expect(await service.list()).toEqual([ { id: mockedValue[0].id, - name: unitRentTypeFactory(0).name, + name: unitRentTypeArray[0], createdAt: date, updatedAt: date, }, { id: mockedValue[1].id, - name: unitRentTypeFactory(1).name, + name: unitRentTypeArray[1], createdAt: date, updatedAt: date, }, { id: mockedValue[2].id, - name: unitRentTypeFactory(2).name, + name: unitRentTypeArray[0], createdAt: date, updatedAt: date, }, @@ -67,15 +73,10 @@ describe('Testing unit rent type service', () => { it('testing findOne() with id present', async () => { const date = new Date(); - const mockedValue = mockUnitRentType(3, date); + const mockedValue = mockUnitRentType(UnitRentTypeEnum.fixed, date); prisma.unitRentTypes.findUnique = jest.fn().mockResolvedValue(mockedValue); - expect(await service.findOne('example Id')).toEqual({ - id: mockedValue.id, - name: unitRentTypeFactory(3).name, - createdAt: date, - updatedAt: date, - }); + expect(await service.findOne('example Id')).toEqual(mockedValue); expect(prisma.unitRentTypes.findUnique).toHaveBeenCalledWith({ where: { @@ -100,47 +101,45 @@ describe('Testing unit rent type service', () => { it('testing create()', async () => { const date = new Date(); - const mockedValue = mockUnitRentType(3, date); + const mockedValue = mockUnitRentType( + UnitRentTypeEnum.percentageOfIncome, + date, + ); prisma.unitRentTypes.create = jest.fn().mockResolvedValue(mockedValue); const params: UnitRentTypeCreate = { - name: unitRentTypeFactory(3).name, + name: UnitRentTypeEnum.percentageOfIncome, }; - expect(await service.create(params)).toEqual({ - id: mockedValue.id, - name: unitRentTypeFactory(3).name, - createdAt: date, - updatedAt: date, - }); + expect(await service.create(params)).toEqual(mockedValue); expect(prisma.unitRentTypes.create).toHaveBeenCalledWith({ data: { - name: unitRentTypeFactory(3).name, + name: UnitRentTypeEnum.percentageOfIncome, }, }); }); it('testing update() existing record found', async () => { const date = new Date(); - const mockedUnitRentType = mockUnitRentType(3, date); + const mockedUnitRentType = mockUnitRentType(UnitRentTypeEnum.fixed, date); prisma.unitRentTypes.findUnique = jest .fn() .mockResolvedValue(mockedUnitRentType); prisma.unitRentTypes.update = jest.fn().mockResolvedValue({ ...mockedUnitRentType, - name: unitRentTypeFactory(4).name, + name: UnitRentTypeEnum.percentageOfIncome, }); const params: UnitRentTypeUpdate = { - name: unitRentTypeFactory(4).name, + name: UnitRentTypeEnum.percentageOfIncome, id: mockedUnitRentType.id, }; expect(await service.update(params)).toEqual({ id: mockedUnitRentType.id, - name: unitRentTypeFactory(4).name, + name: UnitRentTypeEnum.percentageOfIncome, createdAt: date, updatedAt: date, }); @@ -153,7 +152,7 @@ describe('Testing unit rent type service', () => { expect(prisma.unitRentTypes.update).toHaveBeenCalledWith({ data: { - name: unitRentTypeFactory(4).name, + name: UnitRentTypeEnum.percentageOfIncome, }, where: { id: mockedUnitRentType.id, @@ -166,7 +165,7 @@ describe('Testing unit rent type service', () => { prisma.unitRentTypes.update = jest.fn().mockResolvedValue(null); const params: UnitRentTypeUpdate = { - name: unitRentTypeFactory(4).name, + name: UnitRentTypeEnum.percentageOfIncome, id: 'example id', }; @@ -183,7 +182,7 @@ describe('Testing unit rent type service', () => { it('testing delete()', async () => { const date = new Date(); - const mockedValue = mockUnitRentType(3, date); + const mockedValue = mockUnitRentType(UnitRentTypeEnum.fixed, date); prisma.unitRentTypes.findUnique = jest.fn().mockResolvedValue(mockedValue); prisma.unitRentTypes.delete = jest.fn().mockResolvedValue(mockedValue); @@ -214,7 +213,10 @@ describe('Testing unit rent type service', () => { it('testing findOrThrow() record found', async () => { const date = new Date(); - const mockedValue = mockUnitRentType(3, date); + const mockedValue = mockUnitRentType( + UnitRentTypeEnum.percentageOfIncome, + date, + ); prisma.unitRentTypes.findUnique = jest.fn().mockResolvedValue(mockedValue); expect(await service.findOrThrow('example id')).toEqual(mockedValue); diff --git a/backend_new/test/unit/services/unit-type.service.spec.ts b/backend_new/test/unit/services/unit-type.service.spec.ts index 18ec902390..af4567d5a2 100644 --- a/backend_new/test/unit/services/unit-type.service.spec.ts +++ b/backend_new/test/unit/services/unit-type.service.spec.ts @@ -4,7 +4,7 @@ import { UnitTypeService } from '../../../src/services/unit-type.service'; import { UnitTypeCreate } from '../../../src/dtos/unit-types/unit-type-create.dto'; import { UnitTypeUpdate } from '../../../src/dtos/unit-types/unit-type-update.dto'; import { randomUUID } from 'crypto'; -import { unitTypeArray } from '../../../prisma/seed-helpers/unit-type-factory'; +import { UnitTypeEnum } from '@prisma/client'; describe('Testing unit type service', () => { let service: UnitTypeService; @@ -13,7 +13,7 @@ describe('Testing unit type service', () => { const mockUnitType = (position: number, date: Date) => { return { id: randomUUID(), - name: unitTypeArray[position].name, + name: Object.values(UnitTypeEnum)[position], createdAt: date, updatedAt: date, numBedrooms: position, @@ -45,21 +45,21 @@ describe('Testing unit type service', () => { expect(await service.list()).toEqual([ { id: mockedValue[0].id, - name: unitTypeArray[0].name, + name: Object.values(UnitTypeEnum)[0], createdAt: date, updatedAt: date, numBedrooms: 0, }, { id: mockedValue[1].id, - name: unitTypeArray[1].name, + name: Object.values(UnitTypeEnum)[1], createdAt: date, updatedAt: date, numBedrooms: 1, }, { id: mockedValue[2].id, - name: unitTypeArray[2].name, + name: Object.values(UnitTypeEnum)[2], createdAt: date, updatedAt: date, numBedrooms: 2, @@ -76,7 +76,7 @@ describe('Testing unit type service', () => { expect(await service.findOne('example Id')).toEqual({ id: mockedValue.id, - name: unitTypeArray[3].name, + name: Object.values(UnitTypeEnum)[3], createdAt: date, updatedAt: date, numBedrooms: 3, @@ -114,12 +114,12 @@ describe('Testing unit type service', () => { const params: UnitTypeCreate = { numBedrooms: 3, - name: unitTypeArray[3].name, + name: Object.values(UnitTypeEnum)[3], }; expect(await service.create(params)).toEqual({ id: mockedValue.id, - name: unitTypeArray[3].name, + name: Object.values(UnitTypeEnum)[3], createdAt: date, updatedAt: date, numBedrooms: 3, @@ -127,7 +127,7 @@ describe('Testing unit type service', () => { expect(prisma.unitTypes.create).toHaveBeenCalledWith({ data: { - name: unitTypeArray[3].name, + name: Object.values(UnitTypeEnum)[3], numBedrooms: 3, }, }); @@ -141,19 +141,19 @@ describe('Testing unit type service', () => { prisma.unitTypes.findFirst = jest.fn().mockResolvedValue(mockedUnitType); prisma.unitTypes.update = jest.fn().mockResolvedValue({ ...mockedUnitType, - name: unitTypeArray[4].name, + name: Object.values(UnitTypeEnum)[4], numBedrooms: 4, }); const params: UnitTypeUpdate = { numBedrooms: 4, - name: unitTypeArray[4].name, + name: Object.values(UnitTypeEnum)[4], id: mockedUnitType.id, }; expect(await service.update(params)).toEqual({ id: mockedUnitType.id, - name: unitTypeArray[4].name, + name: Object.values(UnitTypeEnum)[4], createdAt: date, updatedAt: date, numBedrooms: 4, @@ -167,7 +167,7 @@ describe('Testing unit type service', () => { expect(prisma.unitTypes.update).toHaveBeenCalledWith({ data: { - name: unitTypeArray[4].name, + name: Object.values(UnitTypeEnum)[4], numBedrooms: 4, }, where: { @@ -182,7 +182,7 @@ describe('Testing unit type service', () => { const params: UnitTypeUpdate = { numBedrooms: 4, - name: unitTypeArray[4].name, + name: Object.values(UnitTypeEnum)[4], id: 'example id', }; diff --git a/backend_new/yarn.lock b/backend_new/yarn.lock index 48131685a9..914dd714fb 100644 --- a/backend_new/yarn.lock +++ b/backend_new/yarn.lock @@ -862,10 +862,10 @@ resolved "https://registry.yarnpkg.com/@prisma/engines-version/-/engines-version-4.14.0-67.d9a4c5988f480fa576d43970d5a23641aa77bc9c.tgz#0aeca447c4a5f23c83f68b8033e627b60bc01850" integrity sha512-3jum8/YSudeSN0zGW5qkpz+wAN2V/NYCQ+BPjvHYDfWatLWlQkqy99toX0GysDeaUoBIJg1vaz2yKqiA3CFcQw== -"@prisma/engines@4.14.1": - version "4.14.1" - resolved "https://registry.yarnpkg.com/@prisma/engines/-/engines-4.14.1.tgz#dac49f8d1f2d4f14a8ed7e6f96b24cd49bd6cd91" - integrity sha512-APqFddPVHYmWNKqc+5J5SqrLFfOghKOLZxobmguDUacxOwdEutLsbXPVhNnpFDmuQWQFbXmrTTPoRrrF6B1MWA== +"@prisma/engines@4.15.0": + version "4.15.0" + resolved "https://registry.yarnpkg.com/@prisma/engines/-/engines-4.15.0.tgz#d8687a9fda615fab88b75b466931280289de9e26" + integrity sha512-FTaOCGs0LL0OW68juZlGxFtYviZa4xdQj/rQEdat2txw0s3Vu/saAPKjNVXfIgUsGXmQ72HPgNr6935/P8FNAA== "@protobufjs/aspromise@^1.1.1", "@protobufjs/aspromise@^1.1.2": version "1.1.2" @@ -1173,10 +1173,10 @@ resolved "https://registry.yarnpkg.com/@types/node/-/node-20.3.0.tgz#719498898d5defab83c3560f45d8498f58d11938" integrity sha512-cumHmIAf6On83X7yP+LrsEyUOf/YlociZelmpRYaGFydoaPdxdt80MAbu6vWerQT2COCp2nPvHdsbD7tHn/YlQ== -"@types/node@^16.0.0": - version "16.18.34" - resolved "https://registry.yarnpkg.com/@types/node/-/node-16.18.34.tgz#62d2099b30339dec4b1b04a14c96266459d7c8b2" - integrity sha512-VmVm7gXwhkUimRfBwVI1CHhwp86jDWR04B5FGebMMyxV90SlCmFujwUHrxTD4oO+SOYU86SoxvhgeRQJY7iXFg== +"@types/node@^18.7.14": + version "18.16.18" + resolved "https://registry.yarnpkg.com/@types/node/-/node-18.16.18.tgz#85da09bafb66d4bc14f7c899185336d0c1736390" + integrity sha512-/aNaQZD0+iSBAGnvvN2Cx92HqE5sZCPZtx2TsK+4nvV23fFe09jVDvpArXr2j9DnYlzuU9WuoykDDc6wqvpNcw== "@types/parse-json@^4.0.0": version "4.0.0" @@ -2228,6 +2228,11 @@ data-urls@^2.0.0: whatwg-mimetype "^2.3.0" whatwg-url "^8.0.0" +dayjs@^1.11.8: + version "1.11.8" + resolved "https://registry.yarnpkg.com/dayjs/-/dayjs-1.11.8.tgz#4282f139c8c19dd6d0c7bd571e30c2d0ba7698ea" + integrity sha512-LcgxzFoWMEPO7ggRv1Y2N31hUf2R0Vj7fuy/m+Bg1K8rr+KAs1AEy4y9jd5DXe8pbHgX+srkHNS7TH6Q6ZhYeQ== + debug@2.6.9: version "2.6.9" resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f" @@ -4636,12 +4641,12 @@ pretty-format@^27.0.0, pretty-format@^27.5.1: ansi-styles "^5.0.0" react-is "^17.0.1" -prisma@^4.13.0: - version "4.14.1" - resolved "https://registry.yarnpkg.com/prisma/-/prisma-4.14.1.tgz#7a6bb4ce847a9d08deabb6acdf3116fff15e1316" - integrity sha512-z6hxzTMYqT9SIKlzD08dhzsLUpxjFKKsLpp5/kBDnSqiOjtUyyl/dC5tzxLcOa3jkEHQ8+RpB/fE3w8bgNP51g== +prisma@^4.15.0: + version "4.15.0" + resolved "https://registry.yarnpkg.com/prisma/-/prisma-4.15.0.tgz#4faa94f0d584828b68468953ff0bc88f37912c8c" + integrity sha512-iKZZpobPl48gTcSZVawLMQ3lEy6BnXwtoMj7hluoGFYu2kQ6F9LBuBrUyF95zRVnNo8/3KzLXJXJ5TEnLSJFiA== dependencies: - "@prisma/engines" "4.14.1" + "@prisma/engines" "4.15.0" process-nextick-args@~2.0.0: version "2.0.1" From 55bf98a6bedf98ae7c864090a04a24356c04b5a7 Mon Sep 17 00:00:00 2001 From: Morgan Ludtke <42942267+ludtkemorgan@users.noreply.github.com> Date: Mon, 17 Jul 2023 14:44:48 -0500 Subject: [PATCH 13/57] fix: add correct command to run on heroku (#3552) * fix: add procfile * fix: update to prod start command * fix: correct path to main --- backend_new/Procfile | 1 + backend_new/package.json | 2 +- backend_new/tsconfig.json | 3 ++- 3 files changed, 4 insertions(+), 2 deletions(-) create mode 100644 backend_new/Procfile diff --git a/backend_new/Procfile b/backend_new/Procfile new file mode 100644 index 0000000000..36c6b6bdf1 --- /dev/null +++ b/backend_new/Procfile @@ -0,0 +1 @@ +web: yarn start:prod diff --git a/backend_new/package.json b/backend_new/package.json index c1cc2b1049..5911749958 100644 --- a/backend_new/package.json +++ b/backend_new/package.json @@ -12,7 +12,7 @@ "start": "nest start", "start:dev": "nest start --watch", "start:debug": "nest start --debug --watch", - "start:prod": "node dist/main", + "start:prod": "node dist/src/main", "lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix", "test": "jest --config ./test/jest.config.js", "test:watch": "jest --watch", diff --git a/backend_new/tsconfig.json b/backend_new/tsconfig.json index adb614cab7..e43955351b 100644 --- a/backend_new/tsconfig.json +++ b/backend_new/tsconfig.json @@ -17,5 +17,6 @@ "strictBindCallApply": false, "forceConsistentCasingInFileNames": false, "noFallthroughCasesInSwitch": false - } + }, + "exclude": ["node_modules", "dist"] } From 6b790c13123881835f9ff7fba4634aab0a93a015 Mon Sep 17 00:00:00 2001 From: Yazeed Loonat Date: Mon, 24 Jul 2023 10:22:54 -0700 Subject: [PATCH 14/57] feat: multiselect question endpoints (#3484) * feat: prisma schema generation * fix: cleaning up removed "models" * fix: updates per emily * fix: caught some schema errors * feat: listing get endpoints * feat: adding DTOs * feat: openapi and swagger updates * fix: pass through DTOs * fix: updates per emily * feat: adding ci jobs * fix: round 2 * fix: indentation problem * fix: ci updates * fix: ci test again * fix: adding maybe missing library? * fix: running yarn install * fix: updates per morgan * fix: ci fix round x * fix: test updates * fix: integration tests and ci * fix: update for tests * fix: fixing scheme so migrations can be created * feat: creating ami-chart endpoints * feat: reserved community type * feat: unit type endpoints * feat: unit accessibility priority type * feat: unit rent type * feat: jurisdiction * feat: multiselect question endpoints * fix: updates per morgan * fix: update backend-new circleci job (#3489) * fix: update backend-new circleci job * fix: add cache * fix: add class validator * fix: update list typing * fix: add class transformer * fix: add listing type * fix: one more attempt at typings * fix: update jest config file * fix: add dbsetup * fix: one more attempt * fix: e2e typing fix * fix: turn off diagnostics for e2e * fix: clear db fields between tests * fix: prettier and linting changes * fix: additional linting fixes --------- Co-authored-by: Yazeed Loonat * fix: updates from what I learned * fix: forgot swagger file * fix: updates per morgan * fix: updates per sean/morgan and test updates * fix: updates per sean and morgan * fix: maybe resolving test race case * fix: removing jurisdictionName param * fix: upates per sean and emily * fix: updates per emily * fix: updates to listing test * fix: updates per ami chart * fix: trying to remove seeding for e2e testing * fix: updating test description * fix: updates from ami chart learnings * fix: updates from ami chart learnings * fix: updating client * fix: updates from reserved-community-type learning * fix: test updates * fix: e2e test updates * fix: updates from previous models * fix: updates from previous models * fix: updates to tests * fix: swagger update * fix: update per sean * fix: updates per emily * fix: updates to it() and build filters --------- Co-authored-by: Morgan Ludtke <42942267+ludtkemorgan@users.noreply.github.com> --- .../prisma/seed-helpers/listing-factory.ts | 16 +- .../multiselect-question-factory.ts | 2 +- backend_new/src/app.module.ts | 3 + .../src/controllers/listing.controller.ts | 16 + .../multiselect-question.controller.ts | 96 ++ .../src/dtos/listings/listing-get.dto.ts | 77 ++ .../listing-multiselect-question.dto.ts | 10 +- .../multiselect-option.dto.ts | 10 +- .../multiselect-question-create.dto.ts | 7 + .../multiselect-question-filter-params.dto.ts | 28 + ...dto.ts => multiselect-question-get.dto.ts} | 24 +- .../multiselect-question-query-params.dto.ts | 24 + .../multiselect-question-update.dto.ts | 9 + .../multiselect-questions/filter-key-enum.ts | 4 + .../modules/multiselect-question.module.ts | 12 + backend_new/src/services/listing.service.ts | 25 + .../services/multiselect-question.service.ts | 198 ++++ backend_new/src/utilities/build-filter.ts | 14 +- backend_new/test/integration/app.e2e-spec.ts | 4 + .../test/integration/listing.e2e-spec.ts | 39 +- .../multiselect-question.e2e-spec.ts | 309 +++++++ .../unit/services/listing.service.spec.ts | 31 + .../multiselect-question.service.spec.ts | 402 +++++++++ .../unit/services/translation.service.spec.ts | 2 - backend_new/types/src/backend-swagger.ts | 852 +++++++++++++++--- 25 files changed, 2069 insertions(+), 145 deletions(-) create mode 100644 backend_new/src/controllers/multiselect-question.controller.ts create mode 100644 backend_new/src/dtos/multiselect-questions/multiselect-question-create.dto.ts create mode 100644 backend_new/src/dtos/multiselect-questions/multiselect-question-filter-params.dto.ts rename backend_new/src/dtos/multiselect-questions/{multiselect-question.dto.ts => multiselect-question-get.dto.ts} (81%) create mode 100644 backend_new/src/dtos/multiselect-questions/multiselect-question-query-params.dto.ts create mode 100644 backend_new/src/dtos/multiselect-questions/multiselect-question-update.dto.ts create mode 100644 backend_new/src/enums/multiselect-questions/filter-key-enum.ts create mode 100644 backend_new/src/modules/multiselect-question.module.ts create mode 100644 backend_new/src/services/multiselect-question.service.ts create mode 100644 backend_new/test/integration/multiselect-question.e2e-spec.ts create mode 100644 backend_new/test/unit/services/multiselect-question.service.spec.ts diff --git a/backend_new/prisma/seed-helpers/listing-factory.ts b/backend_new/prisma/seed-helpers/listing-factory.ts index 0725591c15..9353814f46 100644 --- a/backend_new/prisma/seed-helpers/listing-factory.ts +++ b/backend_new/prisma/seed-helpers/listing-factory.ts @@ -9,6 +9,7 @@ import { randomName } from './word-generator'; import { addressFactory } from './address-factory'; import { unitFactoryMany } from './unit-factory'; import { reservedCommunityTypeFactory } from './reserved-community-type-factory'; +import { multiselectQuestionFactory } from './multiselect-question-factory'; export const listingFactory = async ( jurisdictionId: string, @@ -21,7 +22,7 @@ export const listingFactory = async ( listing?: Prisma.ListingsCreateInput; includeBuildingFeatures?: boolean; includeEligibilityRules?: boolean; - multiselectQuestions?: MultiselectQuestions[]; + multiselectQuestions?: Partial[]; }, ): Promise => { const previousListing = optionalParams?.listing || {}; @@ -58,12 +59,13 @@ export const listingFactory = async ( }, listingMultiselectQuestions: optionalParams?.multiselectQuestions ? { - create: optionalParams.multiselectQuestions.map( - (question, index) => ({ - ordinal: index, - multiselectQuestionId: question.id, - }), - ), + create: optionalParams.multiselectQuestions.map((question) => ({ + multiselectQuestions: { + create: multiselectQuestionFactory(jurisdictionId, { + multiselectQuestion: { text: question.text }, + }), + }, + })), } : undefined, ...featuresAndUtilites(), diff --git a/backend_new/prisma/seed-helpers/multiselect-question-factory.ts b/backend_new/prisma/seed-helpers/multiselect-question-factory.ts index a5eabdfd3d..a76d985be6 100644 --- a/backend_new/prisma/seed-helpers/multiselect-question-factory.ts +++ b/backend_new/prisma/seed-helpers/multiselect-question-factory.ts @@ -14,7 +14,7 @@ export const multiselectQuestionFactory = ( optionalParams?: { optOut?: boolean; numberOfOptions?: number; - multiselectQuestion?: Prisma.MultiselectQuestionsCreateInput; + multiselectQuestion?: Partial; }, ): Prisma.MultiselectQuestionsCreateInput => { const previousMultiselectQuestion = optionalParams?.multiselectQuestion || {}; diff --git a/backend_new/src/app.module.ts b/backend_new/src/app.module.ts index f1dfea3ac7..fbc54ed9a6 100644 --- a/backend_new/src/app.module.ts +++ b/backend_new/src/app.module.ts @@ -8,6 +8,7 @@ import { UnitAccessibilityPriorityTypeServiceModule } from './modules/unit-acces import { UnitTypeModule } from './modules/unit-type.module'; import { UnitRentTypeModule } from './modules/unit-rent-type.module'; import { JurisdictionModule } from './modules/jurisdiction.module'; +import { MultiselectQuestionModule } from './modules/multiselect-question.module'; @Module({ imports: [ @@ -18,6 +19,7 @@ import { JurisdictionModule } from './modules/jurisdiction.module'; UnitAccessibilityPriorityTypeServiceModule, UnitRentTypeModule, JurisdictionModule, + MultiselectQuestionModule, ], controllers: [AppController], providers: [AppService], @@ -29,6 +31,7 @@ import { JurisdictionModule } from './modules/jurisdiction.module'; UnitAccessibilityPriorityTypeServiceModule, UnitRentTypeModule, JurisdictionModule, + MultiselectQuestionModule, ], }) export class AppModule {} diff --git a/backend_new/src/controllers/listing.controller.ts b/backend_new/src/controllers/listing.controller.ts index e638632f73..6f3fb9fa60 100644 --- a/backend_new/src/controllers/listing.controller.ts +++ b/backend_new/src/controllers/listing.controller.ts @@ -25,6 +25,7 @@ import { PaginationAllowsAllQueryParams } from '../dtos/shared/pagination.dto'; import { ListingFilterParams } from '../dtos/listings/listings-filter-params.dto'; import { PaginatedListingDto } from '../dtos/listings/paginated-listing.dto'; import ListingGet from '../dtos/listings/listing-get.dto'; +import { IdDTO } from '../dtos/shared/id.dto'; @Controller('listings') @ApiTags('listings') @@ -33,6 +34,7 @@ import ListingGet from '../dtos/listings/listing-get.dto'; ListingFilterParams, ListingsRetrieveParams, PaginationAllowsAllQueryParams, + IdDTO, ) export class ListingController { constructor(private readonly listingService: ListingService) {} @@ -65,4 +67,18 @@ export class ListingController { queryParams.view, ); } + + @Get(`byMultiselectQuestion/:multiselectQuestionId`) + @ApiOperation({ + summary: 'Get listings by multiselect question id', + operationId: 'retrieveListings', + }) + @ApiOkResponse({ type: IdDTO, isArray: true }) + async retrieveListings( + @Param('multiselectQuestionId') multiselectQuestionId: string, + ) { + return await this.listingService.findListingsWithMultiSelectQuestion( + multiselectQuestionId, + ); + } } diff --git a/backend_new/src/controllers/multiselect-question.controller.ts b/backend_new/src/controllers/multiselect-question.controller.ts new file mode 100644 index 0000000000..34b7961909 --- /dev/null +++ b/backend_new/src/controllers/multiselect-question.controller.ts @@ -0,0 +1,96 @@ +import { + Body, + Controller, + Delete, + Get, + Param, + Post, + Put, + Query, + UsePipes, + ValidationPipe, +} from '@nestjs/common'; +import { + ApiExtraModels, + ApiOkResponse, + ApiOperation, + ApiTags, +} from '@nestjs/swagger'; +import { MultiselectQuestionService } from '../services/multiselect-question.service'; +import { MultiselectQuestion } from '../dtos/multiselect-questions/multiselect-question-get.dto'; +import { MultiselectQuestionCreate } from '../dtos/multiselect-questions/multiselect-question-create.dto'; +import { MultiselectQuestionUpdate } from '../dtos/multiselect-questions/multiselect-question-update.dto'; +import { MultiselectQuestionQueryParams } from '../dtos/multiselect-questions/multiselect-question-query-params.dto'; +import { defaultValidationPipeOptions } from '../utilities/default-validation-pipe-options'; +import { IdDTO } from '../dtos/shared/id.dto'; +import { SuccessDTO } from '../dtos/shared/success.dto'; + +@Controller('multiselectQuestions') +@ApiTags('multiselectQuestions') +@UsePipes(new ValidationPipe(defaultValidationPipeOptions)) +@ApiExtraModels( + MultiselectQuestionCreate, + MultiselectQuestionUpdate, + MultiselectQuestionQueryParams, + IdDTO, +) +export class MultiselectQuestionController { + constructor( + private readonly multiselectQuestionService: MultiselectQuestionService, + ) {} + + @Get() + @ApiOperation({ summary: 'List multiselect questions', operationId: 'list' }) + @ApiOkResponse({ type: MultiselectQuestion, isArray: true }) + async list( + @Query() queryParams: MultiselectQuestionQueryParams, + ): Promise { + return await this.multiselectQuestionService.list(queryParams); + } + + @Get(`:multiselectQuestionId`) + @ApiOperation({ + summary: 'Get multiselect question by id', + operationId: 'retrieve', + }) + @ApiOkResponse({ type: MultiselectQuestion }) + async retrieve( + @Param('multiselectQuestionId') multiselectQuestionId: string, + ): Promise { + return this.multiselectQuestionService.findOne(multiselectQuestionId); + } + + @Post() + @ApiOperation({ + summary: 'Create multiselect question', + operationId: 'create', + }) + @ApiOkResponse({ type: MultiselectQuestion }) + async create( + @Body() multiselectQuestion: MultiselectQuestionCreate, + ): Promise { + return await this.multiselectQuestionService.create(multiselectQuestion); + } + + @Put(`:multiselectQuestionId`) + @ApiOperation({ + summary: 'Update multiselect question', + operationId: 'update', + }) + @ApiOkResponse({ type: MultiselectQuestion }) + async update( + @Body() multiselectQuestion: MultiselectQuestionUpdate, + ): Promise { + return await this.multiselectQuestionService.update(multiselectQuestion); + } + + @Delete() + @ApiOperation({ + summary: 'Delete multiselect question by id', + operationId: 'delete', + }) + @ApiOkResponse({ type: SuccessDTO }) + async delete(@Body() dto: IdDTO): Promise { + return await this.multiselectQuestionService.delete(dto.id); + } +} diff --git a/backend_new/src/dtos/listings/listing-get.dto.ts b/backend_new/src/dtos/listings/listing-get.dto.ts index 94d203c4c6..2ffa32d081 100644 --- a/backend_new/src/dtos/listings/listing-get.dto.ts +++ b/backend_new/src/dtos/listings/listing-get.dto.ts @@ -37,96 +37,119 @@ import { UnitsSummary } from '../units/units-summery-get.dto'; class ListingGet extends AbstractDTO { @Expose() @IsString({ groups: [ValidationsGroupsEnum.default] }) + @ApiProperty() additionalApplicationSubmissionNotes?: string | null; @Expose() @IsBoolean({ groups: [ValidationsGroupsEnum.default] }) + @ApiProperty() digitalApplication?: boolean; @Expose() @IsBoolean({ groups: [ValidationsGroupsEnum.default] }) + @ApiProperty() commonDigitalApplication?: boolean; @Expose() @IsBoolean({ groups: [ValidationsGroupsEnum.default] }) + @ApiProperty() paperApplication?: boolean; @Expose() @IsBoolean({ groups: [ValidationsGroupsEnum.default] }) + @ApiProperty() referralOpportunity?: boolean; @Expose() @IsString({ groups: [ValidationsGroupsEnum.default] }) + @ApiProperty() accessibility?: string | null; @Expose() @IsString({ groups: [ValidationsGroupsEnum.default] }) + @ApiProperty() amenities?: string | null; @Expose() @IsNumber({}, { groups: [ValidationsGroupsEnum.default] }) + @ApiProperty() buildingTotalUnits?: number | null; @Expose() @IsString({ groups: [ValidationsGroupsEnum.default] }) + @ApiProperty() developer?: string | null; @Expose() @IsNumber({}, { groups: [ValidationsGroupsEnum.default] }) + @ApiProperty() householdSizeMax?: number | null; @Expose() @IsNumber({}, { groups: [ValidationsGroupsEnum.default] }) + @ApiProperty() householdSizeMin?: number | null; @Expose() @IsString({ groups: [ValidationsGroupsEnum.default] }) + @ApiProperty() neighborhood?: string | null; @Expose() @IsString({ groups: [ValidationsGroupsEnum.default] }) + @ApiProperty() petPolicy?: string | null; @Expose() @IsString({ groups: [ValidationsGroupsEnum.default] }) + @ApiProperty() smokingPolicy?: string | null; @Expose() @IsNumber({}, { groups: [ValidationsGroupsEnum.default] }) + @ApiProperty() unitsAvailable?: number | null; @Expose() @IsString({ groups: [ValidationsGroupsEnum.default] }) + @ApiProperty() unitAmenities?: string | null; @Expose() @IsString({ groups: [ValidationsGroupsEnum.default] }) + @ApiProperty() servicesOffered?: string | null; @Expose() @IsNumber({}, { groups: [ValidationsGroupsEnum.default] }) + @ApiProperty() yearBuilt?: number | null; @Expose() @IsDate({ groups: [ValidationsGroupsEnum.default] }) @Type(() => Date) + @ApiProperty() applicationDueDate?: Date | null; @Expose() @IsDate({ groups: [ValidationsGroupsEnum.default] }) @Type(() => Date) + @ApiProperty() applicationOpenDate?: Date | null; @Expose() @IsString({ groups: [ValidationsGroupsEnum.default] }) + @ApiProperty() applicationFee?: string | null; @Expose() @IsString({ groups: [ValidationsGroupsEnum.default] }) + @ApiProperty() applicationOrganization?: string | null; @Expose() @IsString({ groups: [ValidationsGroupsEnum.default] }) + @ApiProperty() applicationPickUpAddressOfficeHours?: string | null; @Expose() @@ -141,6 +164,7 @@ class ListingGet extends AbstractDTO { @Expose() @IsString({ groups: [ValidationsGroupsEnum.default] }) + @ApiProperty() applicationDropOffAddressOfficeHours?: string | null; @Expose() @@ -165,97 +189,120 @@ class ListingGet extends AbstractDTO { @Expose() @IsString({ groups: [ValidationsGroupsEnum.default] }) + @ApiProperty() buildingSelectionCriteria?: string | null; @Expose() @IsString({ groups: [ValidationsGroupsEnum.default] }) + @ApiProperty() costsNotIncluded?: string | null; @Expose() @IsString({ groups: [ValidationsGroupsEnum.default] }) + @ApiProperty() creditHistory?: string | null; @Expose() @IsString({ groups: [ValidationsGroupsEnum.default] }) + @ApiProperty() criminalBackground?: string | null; @Expose() @IsString({ groups: [ValidationsGroupsEnum.default] }) + @ApiProperty() depositMin?: string | null; @Expose() @IsString({ groups: [ValidationsGroupsEnum.default] }) + @ApiProperty() depositMax?: string | null; @Expose() @IsString({ groups: [ValidationsGroupsEnum.default] }) + @ApiProperty() depositHelperText?: string | null; @Expose() @IsBoolean({ groups: [ValidationsGroupsEnum.default] }) + @ApiProperty() disableUnitsAccordion?: boolean | null; @Expose() @IsEmail({}, { groups: [ValidationsGroupsEnum.default] }) @EnforceLowerCase() + @ApiProperty() leasingAgentEmail?: string | null; @Expose() @IsString({ groups: [ValidationsGroupsEnum.default] }) + @ApiProperty() leasingAgentName?: string | null; @Expose() @IsString({ groups: [ValidationsGroupsEnum.default] }) + @ApiProperty() leasingAgentOfficeHours?: string | null; @Expose() @IsString({ groups: [ValidationsGroupsEnum.default] }) + @ApiProperty() leasingAgentPhone?: string | null; @Expose() @IsString({ groups: [ValidationsGroupsEnum.default] }) + @ApiProperty() leasingAgentTitle?: string | null; @Expose() @IsDefined({ groups: [ValidationsGroupsEnum.default] }) @IsString({ groups: [ValidationsGroupsEnum.default] }) + @ApiProperty() name: string; @Expose() @IsDate({ groups: [ValidationsGroupsEnum.default] }) @Type(() => Date) + @ApiProperty() postmarkedApplicationsReceivedByDate?: Date | null; @Expose() @IsString({ groups: [ValidationsGroupsEnum.default] }) + @ApiProperty() programRules?: string | null; @Expose() @IsString({ groups: [ValidationsGroupsEnum.default] }) + @ApiProperty() rentalAssistance?: string | null; @Expose() @IsString({ groups: [ValidationsGroupsEnum.default] }) + @ApiProperty() rentalHistory?: string | null; @Expose() @IsString({ groups: [ValidationsGroupsEnum.default] }) + @ApiProperty() requiredDocuments?: string | null; @Expose() @IsString({ groups: [ValidationsGroupsEnum.default] }) + @ApiProperty() specialNotes?: string | null; @Expose() @IsNumber({}, { groups: [ValidationsGroupsEnum.default] }) + @ApiProperty() waitlistCurrentSize?: number | null; @Expose() @IsNumber({}, { groups: [ValidationsGroupsEnum.default] }) + @ApiProperty() waitlistMaxSize?: number | null; @Expose() @IsString({ groups: [ValidationsGroupsEnum.default] }) + @ApiProperty() whatToExpect?: string | null; @Expose() @@ -272,11 +319,13 @@ class ListingGet extends AbstractDTO { reviewOrderType?: ReviewOrderTypeEnum | null; @Expose() + @ApiProperty() applicationConfig?: Record; @Expose() @IsBoolean({ groups: [ValidationsGroupsEnum.default] }) @IsDefined({ groups: [ValidationsGroupsEnum.default] }) + @ApiProperty() displayWaitlistSize: boolean; @Expose() @@ -292,58 +341,70 @@ class ListingGet extends AbstractDTO { @Expose() @IsString({ groups: [ValidationsGroupsEnum.default] }) @MaxLength(4096, { groups: [ValidationsGroupsEnum.default] }) + @ApiProperty() reservedCommunityDescription?: string | null; @Expose() @IsNumber({}, { groups: [ValidationsGroupsEnum.default] }) + @ApiProperty() reservedCommunityMinAge?: number | null; @Expose() @IsString({ groups: [ValidationsGroupsEnum.default] }) @MaxLength(4096, { groups: [ValidationsGroupsEnum.default] }) + @ApiProperty() resultLink?: string | null; @Expose() @IsBoolean({ groups: [ValidationsGroupsEnum.default] }) + @ApiProperty() isWaitlistOpen?: boolean | null; @Expose() @IsNumber({}, { groups: [ValidationsGroupsEnum.default] }) + @ApiProperty() waitlistOpenSpots?: number | null; @Expose() @IsBoolean({ groups: [ValidationsGroupsEnum.default] }) + @ApiProperty() customMapPin?: boolean | null; @Expose() @IsDate({ groups: [ValidationsGroupsEnum.default] }) @Type(() => Date) + @ApiProperty() publishedAt?: Date | null; @Expose() @IsDate({ groups: [ValidationsGroupsEnum.default] }) @Type(() => Date) + @ApiProperty() closedAt?: Date | null; @Expose() @IsDate({ groups: [ValidationsGroupsEnum.default] }) @Type(() => Date) + @ApiProperty() afsLastRunAt?: Date | null; @Expose() @IsDate({ groups: [ValidationsGroupsEnum.default] }) @Type(() => Date) + @ApiProperty() lastApplicationUpdateAt?: Date | null; @Expose() @ValidateNested({ groups: [ValidationsGroupsEnum.default], each: true }) @Type(() => ListingMultiselectQuestion) + @ApiProperty({ type: ListingMultiselectQuestion, isArray: true }) listingMultiselectQuestions?: ListingMultiselectQuestion[]; @Expose() @ValidateNested({ groups: [ValidationsGroupsEnum.default], each: true }) @IsDefined({ groups: [ValidationsGroupsEnum.default] }) @Type(() => ApplicationMethod) + @ApiProperty({ type: ApplicationMethod, isArray: true }) applicationMethods: ApplicationMethod[]; @Expose() @@ -358,79 +419,94 @@ class ListingGet extends AbstractDTO { @ValidateNested({ groups: [ValidationsGroupsEnum.default], each: true }) @Type(() => Asset) @IsDefined({ groups: [ValidationsGroupsEnum.default] }) + @ApiProperty({ type: Asset, isArray: true }) assets: Asset[]; @Expose() @ValidateNested({ groups: [ValidationsGroupsEnum.default], each: true }) @Type(() => ListingEvent) @IsDefined({ groups: [ValidationsGroupsEnum.default] }) + @ApiProperty({ type: Asset, isArray: true }) events: ListingEvent[]; @Expose() @IsDefined({ groups: [ValidationsGroupsEnum.default] }) @ValidateNested({ groups: [ValidationsGroupsEnum.default] }) @Type(() => Address) + @ApiProperty({ type: Address }) listingsBuildingAddress: Address; @Expose() @ValidateNested({ groups: [ValidationsGroupsEnum.default] }) @Type(() => Address) + @ApiProperty({ type: Address }) listingsApplicationPickUpAddress?: Address; @Expose() @ValidateNested({ groups: [ValidationsGroupsEnum.default] }) @Type(() => Address) + @ApiProperty({ type: Address }) listingsApplicationDropOffAddress?: Address; @Expose() @ValidateNested({ groups: [ValidationsGroupsEnum.default] }) @Type(() => Address) + @ApiProperty({ type: Address }) listingsApplicationMailingAddress?: Address; @Expose() @ValidateNested({ groups: [ValidationsGroupsEnum.default] }) @Type(() => Address) + @ApiProperty({ type: Address }) listingsLeasingAgentAddress?: Address; @Expose() @ValidateNested({ groups: [ValidationsGroupsEnum.default] }) @Type(() => Asset) + @ApiProperty({ type: Asset }) listingsBuildingSelectionCriteriaFile?: Asset | null; @Expose() @ValidateNested({ groups: [ValidationsGroupsEnum.default] }) @IsDefined({ groups: [ValidationsGroupsEnum.default] }) @Type(() => Jurisdiction) + @ApiProperty({ type: Jurisdiction }) jurisdictions: Jurisdiction; @Expose() @ValidateNested({ groups: [ValidationsGroupsEnum.default] }) @Type(() => Asset) + @ApiProperty({ type: Asset }) listingsResult?: Asset | null; @Expose() @ValidateNested({ groups: [ValidationsGroupsEnum.default] }) @Type(() => ReservedCommunityType) + @ApiProperty({ type: ReservedCommunityType }) reservedCommunityTypes?: ReservedCommunityType; @Expose() @ValidateNested({ groups: [ValidationsGroupsEnum.default], each: true }) @Type(() => ListingImage) + @ApiProperty({ type: ListingImage, isArray: true }) listingImages?: ListingImage[] | null; @Expose() @ValidateNested({ groups: [ValidationsGroupsEnum.default], each: true }) @Type(() => ListingFeatures) + @ApiProperty({ type: ListingFeatures }) listingFeatures?: ListingFeatures; @Expose() @ValidateNested({ groups: [ValidationsGroupsEnum.default], each: true }) @Type(() => ListingUtilities) + @ApiProperty({ type: ListingUtilities }) listingUtilities?: ListingUtilities; @Expose() @ValidateNested({ groups: [ValidationsGroupsEnum.default], each: true }) @Type(() => Unit) + @ApiProperty({ type: Unit, isArray: true }) units: Unit[]; @Expose() @@ -439,6 +515,7 @@ class ListingGet extends AbstractDTO { @Expose() @ValidateNested({ groups: [ValidationsGroupsEnum.default], each: true }) + @ApiProperty({ type: UnitsSummary, isArray: true }) @Type(() => UnitsSummary) unitsSummary: UnitsSummary[]; } diff --git a/backend_new/src/dtos/listings/listing-multiselect-question.dto.ts b/backend_new/src/dtos/listings/listing-multiselect-question.dto.ts index 2794a4b177..38b2f1a409 100644 --- a/backend_new/src/dtos/listings/listing-multiselect-question.dto.ts +++ b/backend_new/src/dtos/listings/listing-multiselect-question.dto.ts @@ -1,20 +1,18 @@ -import { MultiselectQuestion } from '../multiselect-questions/multiselect-question.dto'; +import { MultiselectQuestion } from '../multiselect-questions/multiselect-question-get.dto'; import { Expose, Type } from 'class-transformer'; import { IsNumber, IsDefined } from 'class-validator'; import { ValidationsGroupsEnum } from '../../enums/shared/validation-groups-enum'; -import { ListingGet } from './listing-get.dto'; +import { ApiProperty } from '@nestjs/swagger'; export class ListingMultiselectQuestion { - @Type(() => ListingGet) - @IsDefined({ groups: [ValidationsGroupsEnum.default] }) - listings: ListingGet; - @Expose() @Type(() => MultiselectQuestion) @IsDefined({ groups: [ValidationsGroupsEnum.default] }) + @ApiProperty() multiselectQuestions: MultiselectQuestion; @Expose() @IsNumber({}, { groups: [ValidationsGroupsEnum.default] }) + @ApiProperty() ordinal?: number | null; } diff --git a/backend_new/src/dtos/multiselect-questions/multiselect-option.dto.ts b/backend_new/src/dtos/multiselect-questions/multiselect-option.dto.ts index afc9981433..3cc5235536 100644 --- a/backend_new/src/dtos/multiselect-questions/multiselect-option.dto.ts +++ b/backend_new/src/dtos/multiselect-questions/multiselect-option.dto.ts @@ -19,7 +19,7 @@ export class MultiselectOption { @Expose() @IsString({ groups: [ValidationsGroupsEnum.default] }) - @ApiProperty({ required: false }) + @ApiProperty() untranslatedText?: string; @Expose() @@ -30,22 +30,22 @@ export class MultiselectOption { @Expose() @IsString({ groups: [ValidationsGroupsEnum.default] }) - @ApiProperty({ required: false }) + @ApiProperty() description?: string | null; @Expose() @ValidateNested({ groups: [ValidationsGroupsEnum.default], each: true }) @Type(() => MultiselectLink) - @ApiProperty({ type: [MultiselectLink], required: false }) + @ApiProperty({ type: MultiselectLink, isArray: true }) links?: MultiselectLink[] | null; @Expose() @IsBoolean({ groups: [ValidationsGroupsEnum.default] }) - @ApiProperty({ required: false }) + @ApiProperty() collectAddress?: boolean | null; @Expose() @IsBoolean({ groups: [ValidationsGroupsEnum.default] }) - @ApiProperty({ required: false }) + @ApiProperty() exclusive?: boolean | null; } diff --git a/backend_new/src/dtos/multiselect-questions/multiselect-question-create.dto.ts b/backend_new/src/dtos/multiselect-questions/multiselect-question-create.dto.ts new file mode 100644 index 0000000000..32b8c0ebe1 --- /dev/null +++ b/backend_new/src/dtos/multiselect-questions/multiselect-question-create.dto.ts @@ -0,0 +1,7 @@ +import { OmitType } from '@nestjs/swagger'; +import { MultiselectQuestionUpdate } from './multiselect-question-update.dto'; + +export class MultiselectQuestionCreate extends OmitType( + MultiselectQuestionUpdate, + ['id'], +) {} diff --git a/backend_new/src/dtos/multiselect-questions/multiselect-question-filter-params.dto.ts b/backend_new/src/dtos/multiselect-questions/multiselect-question-filter-params.dto.ts new file mode 100644 index 0000000000..8443096af0 --- /dev/null +++ b/backend_new/src/dtos/multiselect-questions/multiselect-question-filter-params.dto.ts @@ -0,0 +1,28 @@ +import { BaseFilter } from '../shared/base-filter.dto'; +import { Expose } from 'class-transformer'; +import { ApiProperty } from '@nestjs/swagger'; +import { IsString, IsUUID } from 'class-validator'; +import { ValidationsGroupsEnum } from '../../enums/shared/validation-groups-enum'; +import { MultiselectQuestionFilterKeys } from '../../enums/multiselect-questions/filter-key-enum'; +import { MultiselectQuestionsApplicationSectionEnum } from '@prisma/client'; + +export class MultiselectQuestionFilterParams extends BaseFilter { + @Expose() + @ApiProperty({ + type: String, + example: 'uuid', + required: false, + }) + @IsString({ groups: [ValidationsGroupsEnum.default] }) + @IsUUID(4, { groups: [ValidationsGroupsEnum.default] }) + [MultiselectQuestionFilterKeys.jurisdiction]?: string; + + @Expose() + @ApiProperty({ + type: String, + example: 'preferences', + required: false, + }) + @IsString({ groups: [ValidationsGroupsEnum.default] }) + [MultiselectQuestionFilterKeys.applicationSection]?: MultiselectQuestionsApplicationSectionEnum; +} diff --git a/backend_new/src/dtos/multiselect-questions/multiselect-question.dto.ts b/backend_new/src/dtos/multiselect-questions/multiselect-question-get.dto.ts similarity index 81% rename from backend_new/src/dtos/multiselect-questions/multiselect-question.dto.ts rename to backend_new/src/dtos/multiselect-questions/multiselect-question-get.dto.ts index 962ff25c9e..ead84cf932 100644 --- a/backend_new/src/dtos/multiselect-questions/multiselect-question.dto.ts +++ b/backend_new/src/dtos/multiselect-questions/multiselect-question-get.dto.ts @@ -10,61 +10,61 @@ import { import { ValidationsGroupsEnum } from '../../enums/shared/validation-groups-enum'; import { ApiProperty } from '@nestjs/swagger'; import { AbstractDTO } from '../shared/abstract.dto'; -import { ListingMultiselectQuestion } from '../listings/listing-multiselect-question.dto'; import { MultiselectQuestionsApplicationSectionEnum } from '@prisma/client'; -import { Jurisdiction } from '../jurisdictions/jurisdiction.dto'; import { MultiselectLink } from './multiselect-link.dto'; import { MultiselectOption } from './multiselect-option.dto'; +import { IdDTO } from '../shared/id.dto'; class MultiselectQuestion extends AbstractDTO { @Expose() @IsString({ groups: [ValidationsGroupsEnum.default] }) @IsDefined({ groups: [ValidationsGroupsEnum.default] }) + @ApiProperty() text: string; @Expose() @IsString({ groups: [ValidationsGroupsEnum.default] }) + @ApiProperty() untranslatedText?: string; @Expose() @IsString({ groups: [ValidationsGroupsEnum.default] }) + @ApiProperty() untranslatedOptOutText?: string; @Expose() @IsString({ groups: [ValidationsGroupsEnum.default] }) + @ApiProperty() subText?: string | null; @Expose() @IsString({ groups: [ValidationsGroupsEnum.default] }) + @ApiProperty() description?: string | null; @Expose() @ValidateNested({ groups: [ValidationsGroupsEnum.default], each: true }) @Type(() => MultiselectLink) - @ApiProperty({ type: [MultiselectLink] }) + @ApiProperty({ type: MultiselectLink, isArray: true }) links?: MultiselectLink[] | null; - @Expose() - @ValidateNested({ groups: [ValidationsGroupsEnum.default], each: true }) - @Type(() => ListingMultiselectQuestion) - @IsDefined({ groups: [ValidationsGroupsEnum.default] }) - listings: ListingMultiselectQuestion[]; - @Expose() @ValidateNested({ groups: [ValidationsGroupsEnum.default] }) @IsDefined({ groups: [ValidationsGroupsEnum.default] }) - @Type(() => Jurisdiction) - jurisdictions: Jurisdiction[]; + @Type(() => IdDTO) + @ApiProperty({ type: IdDTO, isArray: true }) + jurisdictions: IdDTO[]; @Expose() @ArrayMaxSize(64, { groups: [ValidationsGroupsEnum.default] }) @ValidateNested({ groups: [ValidationsGroupsEnum.default], each: true }) @Type(() => MultiselectOption) - @ApiProperty({ type: [MultiselectOption] }) + @ApiProperty({ type: MultiselectOption, isArray: true }) options?: MultiselectOption[] | null; @Expose() @IsString({ groups: [ValidationsGroupsEnum.default] }) + @ApiProperty() optOutText?: string | null; @Expose() diff --git a/backend_new/src/dtos/multiselect-questions/multiselect-question-query-params.dto.ts b/backend_new/src/dtos/multiselect-questions/multiselect-question-query-params.dto.ts new file mode 100644 index 0000000000..337783072b --- /dev/null +++ b/backend_new/src/dtos/multiselect-questions/multiselect-question-query-params.dto.ts @@ -0,0 +1,24 @@ +import { Expose, Type } from 'class-transformer'; +import { ApiProperty, getSchemaPath } from '@nestjs/swagger'; +import { MultiselectQuestionFilterParams } from './multiselect-question-filter-params.dto'; +import { ArrayMaxSize, IsArray, ValidateNested } from 'class-validator'; +import { ValidationsGroupsEnum } from '../../enums/shared/validation-groups-enum'; + +export class MultiselectQuestionQueryParams { + @Expose() + @ApiProperty({ + name: 'filter', + required: false, + type: MultiselectQuestionFilterParams, + isArray: true, + items: { + $ref: getSchemaPath(MultiselectQuestionFilterParams), + }, + example: { $comparison: '=', applicationSection: 'programs' }, + }) + @IsArray({ groups: [ValidationsGroupsEnum.default] }) + @ArrayMaxSize(16, { groups: [ValidationsGroupsEnum.default] }) + @Type(() => MultiselectQuestionFilterParams) + @ValidateNested({ groups: [ValidationsGroupsEnum.default], each: true }) + filter?: MultiselectQuestionFilterParams[]; +} diff --git a/backend_new/src/dtos/multiselect-questions/multiselect-question-update.dto.ts b/backend_new/src/dtos/multiselect-questions/multiselect-question-update.dto.ts new file mode 100644 index 0000000000..4d3e24d185 --- /dev/null +++ b/backend_new/src/dtos/multiselect-questions/multiselect-question-update.dto.ts @@ -0,0 +1,9 @@ +import { OmitType } from '@nestjs/swagger'; +import { MultiselectQuestion } from './multiselect-question-get.dto'; + +export class MultiselectQuestionUpdate extends OmitType(MultiselectQuestion, [ + 'createdAt', + 'updatedAt', + 'untranslatedText', + 'untranslatedText', +]) {} diff --git a/backend_new/src/enums/multiselect-questions/filter-key-enum.ts b/backend_new/src/enums/multiselect-questions/filter-key-enum.ts new file mode 100644 index 0000000000..398269f1eb --- /dev/null +++ b/backend_new/src/enums/multiselect-questions/filter-key-enum.ts @@ -0,0 +1,4 @@ +export enum MultiselectQuestionFilterKeys { + jurisdiction = 'jurisdiction', + applicationSection = 'applicationSection', +} diff --git a/backend_new/src/modules/multiselect-question.module.ts b/backend_new/src/modules/multiselect-question.module.ts new file mode 100644 index 0000000000..832f914463 --- /dev/null +++ b/backend_new/src/modules/multiselect-question.module.ts @@ -0,0 +1,12 @@ +import { Module } from '@nestjs/common'; +import { MultiselectQuestionController } from '../controllers/multiselect-question.controller'; +import { MultiselectQuestionService } from '../services/multiselect-question.service'; +import { PrismaService } from '../services/prisma.service'; + +@Module({ + imports: [], + controllers: [MultiselectQuestionController], + providers: [MultiselectQuestionService, PrismaService], + exports: [MultiselectQuestionService, PrismaService], +}) +export class MultiselectQuestionModule {} diff --git a/backend_new/src/services/listing.service.ts b/backend_new/src/services/listing.service.ts index 85d3371ecc..ac3dc6e6ef 100644 --- a/backend_new/src/services/listing.service.ts +++ b/backend_new/src/services/listing.service.ts @@ -333,6 +333,9 @@ export class ListingService { return result; } + /* + this builds the units summarized for the list() + */ addUnitsSummarized = async (listing: ListingGet) => { if (Array.isArray(listing.units) && listing.units.length > 0) { const amiChartsRaw = await this.prisma.amiChart.findMany({ @@ -347,4 +350,26 @@ export class ListingService { } return listing; }; + + /* + returns id, name of listing given a multiselect question id + */ + findListingsWithMultiSelectQuestion = async ( + multiselectQuestionId: string, + ) => { + const listingsRaw = await this.prisma.listings.findMany({ + select: { + id: true, + name: true, + }, + where: { + listingMultiselectQuestions: { + some: { + multiselectQuestionId: multiselectQuestionId, + }, + }, + }, + }); + return mapTo(ListingGet, listingsRaw); + }; } diff --git a/backend_new/src/services/multiselect-question.service.ts b/backend_new/src/services/multiselect-question.service.ts new file mode 100644 index 0000000000..57df3bb792 --- /dev/null +++ b/backend_new/src/services/multiselect-question.service.ts @@ -0,0 +1,198 @@ +import { Injectable, NotFoundException } from '@nestjs/common'; +import { PrismaService } from './prisma.service'; +import { MultiselectQuestion } from '../dtos/multiselect-questions/multiselect-question-get.dto'; +import { MultiselectQuestionUpdate } from '../dtos/multiselect-questions/multiselect-question-update.dto'; +import { MultiselectQuestionCreate } from '../dtos/multiselect-questions/multiselect-question-create.dto'; +import { mapTo } from '../utilities/mapTo'; +import { SuccessDTO } from '../dtos/shared/success.dto'; +import { Prisma } from '@prisma/client'; +import { buildFilter } from '../utilities/build-filter'; +import { MultiselectQuestionFilterKeys } from '../enums/multiselect-questions/filter-key-enum'; +import { MultiselectQuestionQueryParams } from '../dtos/multiselect-questions/multiselect-question-query-params.dto'; + +const view: Prisma.MultiselectQuestionsInclude = { + jurisdictions: true, +}; + +/* + this is the service for multiselect questions + it handles all the backend's business logic for reading/writing/deleting multiselect questione data +*/ +@Injectable() +export class MultiselectQuestionService { + constructor(private prisma: PrismaService) {} + + /* + this will get a set of multiselect questions given the params passed in + */ + async list( + params: MultiselectQuestionQueryParams, + ): Promise { + const rawMultiselectQuestions = + await this.prisma.multiselectQuestions.findMany({ + include: view, + where: this.buildWhere(params), + }); + return mapTo(MultiselectQuestion, rawMultiselectQuestions); + } + + /* + this will build the where clause for list() + */ + buildWhere( + params: MultiselectQuestionQueryParams, + ): Prisma.MultiselectQuestionsWhereInput { + const filters: Prisma.MultiselectQuestionsWhereInput[] = []; + if (!params?.filter?.length) { + return { + AND: filters, + }; + } + params.filter.forEach((filter) => { + if (MultiselectQuestionFilterKeys.jurisdiction in filter) { + const builtFilter = buildFilter({ + $comparison: filter.$comparison, + $include_nulls: false, + value: filter[MultiselectQuestionFilterKeys.jurisdiction], + key: MultiselectQuestionFilterKeys.jurisdiction, + caseSensitive: true, + }); + filters.push({ + OR: builtFilter.map((filt) => ({ + jurisdictions: { + some: { + id: filt, + }, + }, + })), + }); + } else if (MultiselectQuestionFilterKeys.applicationSection in filter) { + const builtFilter = buildFilter({ + $comparison: filter.$comparison, + $include_nulls: false, + value: filter[MultiselectQuestionFilterKeys.applicationSection], + key: MultiselectQuestionFilterKeys.applicationSection, + caseSensitive: true, + }); + filters.push({ + OR: builtFilter.map((filt) => ({ + applicationSection: filt, + })), + }); + } + }); + return { + AND: filters, + }; + } + + /* + this will return 1 multiselect question or error + */ + async findOne(multiSelectQuestionId: string): Promise { + const rawMultiselectQuestion = + await this.prisma.multiselectQuestions.findFirst({ + where: { + id: { + equals: multiSelectQuestionId, + }, + }, + include: view, + }); + + if (!rawMultiselectQuestion) { + throw new NotFoundException( + `multiselectQuestionId ${multiSelectQuestionId} was requested but not found`, + ); + } + + return mapTo(MultiselectQuestion, rawMultiselectQuestion); + } + + /* + this will create a multiselect question + */ + async create( + incomingData: MultiselectQuestionCreate, + ): Promise { + const rawResult = await this.prisma.multiselectQuestions.create({ + data: { + ...incomingData, + jurisdictions: { + connect: incomingData.jurisdictions.map((juris) => ({ + id: juris.id, + })), + }, + links: JSON.stringify(incomingData.links), + options: JSON.stringify(incomingData.options), + }, + include: view, + }); + + return mapTo(MultiselectQuestion, rawResult); + } + + /* + this will update a multiselect question's name or items field + if no multiselect question has the id of the incoming argument an error is thrown + */ + async update( + incomingData: MultiselectQuestionUpdate, + ): Promise { + await this.findOrThrow(incomingData.id); + + const rawResults = await this.prisma.multiselectQuestions.update({ + data: { + ...incomingData, + jurisdictions: { + connect: incomingData.jurisdictions.map((juris) => ({ + id: juris.id, + })), + }, + links: JSON.stringify(incomingData.links), + options: JSON.stringify(incomingData.options), + id: undefined, + }, + where: { + id: incomingData.id, + }, + include: view, + }); + return mapTo(MultiselectQuestion, rawResults); + } + + /* + this will delete a multiselect question + */ + async delete(multiSelectQuestionId: string): Promise { + await this.findOrThrow(multiSelectQuestionId); + await this.prisma.multiselectQuestions.delete({ + where: { + id: multiSelectQuestionId, + }, + }); + return { + success: true, + } as SuccessDTO; + } + + /* + this will either find a record or throw a customized error + */ + async findOrThrow(multiselectQuestionId: string): Promise { + const multiselectQuestion = + await this.prisma.multiselectQuestions.findFirst({ + where: { + id: multiselectQuestionId, + }, + }); + + if (!multiselectQuestion) { + throw new NotFoundException( + `multiselectQuestionId ${multiselectQuestionId} was requested but not found`, + ); + } + + return true; + } +} diff --git a/backend_new/src/utilities/build-filter.ts b/backend_new/src/utilities/build-filter.ts index 775bbb3330..e0ad3dff62 100644 --- a/backend_new/src/utilities/build-filter.ts +++ b/backend_new/src/utilities/build-filter.ts @@ -8,6 +8,7 @@ type filter = { $include_nulls: boolean; value: any; key: string; + caseSensitive?: boolean; }; /* @@ -23,6 +24,9 @@ export function buildFilter( const comparison = filter['$comparison']; const includeNulls = filter['$include_nulls']; const filterValue = filter.value; + const caseSensitive = filter.caseSensitive + ? Prisma.QueryMode.default + : Prisma.QueryMode.insensitive; if (filter.key === UserFilterKeys.isPortalUser) { // TODO: addIsPortalUserQuery(filter.value, user); @@ -34,29 +38,29 @@ export function buildFilter( .split(',') .map((s) => s.trim().toLowerCase()) .filter((s) => s.length !== 0), - mode: Prisma.QueryMode.insensitive, + mode: caseSensitive, }); } else if (comparison === Compare['<>']) { toReturn.push({ not: { equals: filterValue, }, - mode: Prisma.QueryMode.insensitive, + mode: caseSensitive, }); } else if (comparison === Compare['=']) { toReturn.push({ equals: filterValue, - mode: Prisma.QueryMode.insensitive, + mode: caseSensitive, }); } else if (comparison === Compare['>=']) { toReturn.push({ gte: filterValue, - mode: Prisma.QueryMode.insensitive, + mode: caseSensitive, }); } else if (comparison === Compare['<=']) { toReturn.push({ lte: filterValue, - mode: Prisma.QueryMode.insensitive, + mode: caseSensitive, }); } else if (Compare.NA) { throw new HttpException( diff --git a/backend_new/test/integration/app.e2e-spec.ts b/backend_new/test/integration/app.e2e-spec.ts index b8d824ad93..b87678e76f 100644 --- a/backend_new/test/integration/app.e2e-spec.ts +++ b/backend_new/test/integration/app.e2e-spec.ts @@ -15,6 +15,10 @@ describe('AppController (e2e)', () => { await app.init(); }); + afterAll(async () => { + await app.close(); + }); + it('/ (GET)', () => { return request(app.getHttpServer()) .get('/') diff --git a/backend_new/test/integration/listing.e2e-spec.ts b/backend_new/test/integration/listing.e2e-spec.ts index abc5b84112..dcac50837c 100644 --- a/backend_new/test/integration/listing.e2e-spec.ts +++ b/backend_new/test/integration/listing.e2e-spec.ts @@ -33,7 +33,7 @@ describe('Listing Controller Tests', () => { jurisdictionAId = jurisdiction.id; }); - it('list test no params no data', async () => { + it('should not get listings from list endpoint when no params are sent', async () => { const res = await request(app.getHttpServer()).get('/listings').expect(200); expect(res.body).toEqual({ @@ -48,7 +48,7 @@ describe('Listing Controller Tests', () => { }); }); - it('list test no params some data', async () => { + it('should get listings from list endpoint when no params are sent', async () => { const listing1 = await listingFactory(jurisdictionAId, prisma); const listing1Created = await prisma.listings.create({ data: listing1, @@ -76,7 +76,7 @@ describe('Listing Controller Tests', () => { expect(items).toContain(listing2Created.name); }); - it('list test params no data', async () => { + it('should not get listings from list endpoint when params are sent', async () => { const queryParams: ListingsQueryParams = { limit: 1, page: 1, @@ -106,7 +106,7 @@ describe('Listing Controller Tests', () => { }); }); - it('list test params some data', async () => { + it('should get listings from list endpoint when params are sent', async () => { const listing1 = await listingFactory(jurisdictionAId, prisma); const listing1Created = await prisma.listings.create({ data: listing1, @@ -180,4 +180,35 @@ describe('Listing Controller Tests', () => { expect(res.body.items.length).toEqual(1); expect(res.body.items[0].name).toEqual(orderedNames[1]); }); + + it('should get listings from retrieveListings endpoint', async () => { + const listingA = await listingFactory(jurisdictionAId, prisma, { + multiselectQuestions: [{ text: 'example a' }], + }); + const listingACreated = await prisma.listings.create({ + data: listingA, + include: { + listingMultiselectQuestions: true, + }, + }); + + const listingB = await listingFactory(jurisdictionAId, prisma, { + multiselectQuestions: [{ text: 'example b' }], + }); + await prisma.listings.create({ + data: listingB, + include: { + listingMultiselectQuestions: true, + }, + }); + + const res = await request(app.getHttpServer()) + .get( + `/listings/byMultiselectQuestion/${listingACreated.listingMultiselectQuestions[0].multiselectQuestionId}`, + ) + .expect(200); + + expect(res.body.length).toEqual(1); + expect(res.body[0].name).toEqual(listingA.name); + }); }); diff --git a/backend_new/test/integration/multiselect-question.e2e-spec.ts b/backend_new/test/integration/multiselect-question.e2e-spec.ts new file mode 100644 index 0000000000..a2e2d5b5d2 --- /dev/null +++ b/backend_new/test/integration/multiselect-question.e2e-spec.ts @@ -0,0 +1,309 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { INestApplication } from '@nestjs/common'; +import * as request from 'supertest'; +import { AppModule } from '../../src/app.module'; +import { PrismaService } from '../../src/services/prisma.service'; +import { multiselectQuestionFactory } from '../../prisma/seed-helpers/multiselect-question-factory'; +import { jurisdictionFactory } from '../../prisma/seed-helpers/jurisdiction-factory'; +import { MultiselectQuestionCreate } from '../../src/dtos/multiselect-questions/multiselect-question-create.dto'; +import { MultiselectQuestionUpdate } from '../../src/dtos/multiselect-questions/multiselect-question-update.dto'; +import { IdDTO } from 'src/dtos/shared/id.dto'; +import { MultiselectQuestionsApplicationSectionEnum } from '@prisma/client'; +import { stringify } from 'qs'; +import { MultiselectQuestionQueryParams } from '../../src/dtos/multiselect-questions/multiselect-question-query-params.dto'; +import { Compare } from '../../src/dtos/shared/base-filter.dto'; +import { randomUUID } from 'crypto'; + +describe('MultiselectQuestion Controller Tests', () => { + let app: INestApplication; + let prisma: PrismaService; + let jurisdictionId: string; + + beforeAll(async () => { + const moduleFixture: TestingModule = await Test.createTestingModule({ + imports: [AppModule], + }).compile(); + + app = moduleFixture.createNestApplication(); + prisma = moduleFixture.get(PrismaService); + await app.init(); + + const jurisdiction = await prisma.jurisdictions.create({ + data: jurisdictionFactory(), + }); + jurisdictionId = jurisdiction.id; + }); + + it('should get multiselect questions from list endpoint when no params are sent', async () => { + const jurisdictionB = await prisma.jurisdictions.create({ + data: jurisdictionFactory(), + }); + + const multiselectQuestionA = await prisma.multiselectQuestions.create({ + data: multiselectQuestionFactory(jurisdictionB.id), + }); + const multiselectQuestionB = await prisma.multiselectQuestions.create({ + data: multiselectQuestionFactory(jurisdictionB.id), + }); + + const res = await request(app.getHttpServer()) + .get(`/multiselectQuestions?`) + .expect(200); + + expect(res.body.length).toBeGreaterThanOrEqual(2); + const multiselectQuestions = res.body.map((value) => value.text); + expect(multiselectQuestions).toContain(multiselectQuestionA.text); + expect(multiselectQuestions).toContain(multiselectQuestionB.text); + }); + + it('should get multiselect questions from list endpoint when params are sent', async () => { + const multiselectQuestionA = await prisma.multiselectQuestions.create({ + data: multiselectQuestionFactory(jurisdictionId), + }); + const multiselectQuestionB = await prisma.multiselectQuestions.create({ + data: multiselectQuestionFactory(jurisdictionId), + }); + + const queryParams: MultiselectQuestionQueryParams = { + filter: [ + { + $comparison: Compare['='], + jurisdiction: jurisdictionId, + }, + ], + }; + const query = stringify(queryParams as any); + + const res = await request(app.getHttpServer()) + .get(`/multiselectQuestions?${query}`) + .expect(200); + + expect(res.body.length).toBeGreaterThanOrEqual(2); + const multiselectQuestions = res.body.map((value) => value.text); + expect(multiselectQuestions).toContain(multiselectQuestionA.text); + expect(multiselectQuestions).toContain(multiselectQuestionB.text); + }); + + it('should throw error when retrieve endpoint is hit with nonexistent id', async () => { + const id = randomUUID(); + const res = await request(app.getHttpServer()) + .get(`/multiselectQuestions/${id}`) + .expect(404); + expect(res.body.message).toEqual( + `multiselectQuestionId ${id} was requested but not found`, + ); + }); + + it('should get multiselect question when retrieve endpoint is called and id exists', async () => { + const multiselectQuestionA = await prisma.multiselectQuestions.create({ + data: multiselectQuestionFactory(jurisdictionId), + }); + + const res = await request(app.getHttpServer()) + .get(`/multiselectQuestions/${multiselectQuestionA.id}`) + .expect(200); + + expect(res.body.text).toEqual(multiselectQuestionA.text); + }); + + it('should create a multiselect question', async () => { + const res = await request(app.getHttpServer()) + .post('/multiselectQuestions') + .send({ + text: 'example text', + subText: 'example subText', + description: 'example description', + links: [ + { + title: 'title 1', + url: 'title 1', + }, + { + title: 'title 2', + url: 'title 2', + }, + ], + jurisdictions: [{ id: jurisdictionId }], + options: [ + { + text: 'example option text 1', + ordinal: 1, + description: 'example option description 1', + links: [ + { + title: 'title 3', + url: 'title 3', + }, + ], + collectAddress: true, + exclusive: false, + }, + { + text: 'example option text 2', + ordinal: 2, + description: 'example option description 2', + links: [ + { + title: 'title 4', + url: 'title 4', + }, + ], + collectAddress: true, + exclusive: false, + }, + ], + optOutText: 'example optOutText', + hideFromListing: false, + applicationSection: MultiselectQuestionsApplicationSectionEnum.programs, + } as MultiselectQuestionCreate) + .expect(201); + + expect(res.body.text).toEqual('example text'); + }); + + it('should throw error when update endpoint is hit with nonexistent id', async () => { + const id = randomUUID(); + const res = await request(app.getHttpServer()) + .put(`/multiselectQuestions/${id}`) + .send({ + id: id, + text: 'example text', + subText: 'example subText', + description: 'example description', + links: [ + { + title: 'title 1', + url: 'title 1', + }, + { + title: 'title 2', + url: 'title 2', + }, + ], + jurisdictions: [{ id: jurisdictionId }], + options: [ + { + text: 'example option text 1', + ordinal: 1, + description: 'example option description 1', + links: [ + { + title: 'title 3', + url: 'title 3', + }, + ], + collectAddress: true, + exclusive: false, + }, + { + text: 'example option text 2', + ordinal: 2, + description: 'example option description 2', + links: [ + { + title: 'title 4', + url: 'title 4', + }, + ], + collectAddress: true, + exclusive: false, + }, + ], + optOutText: 'example optOutText', + hideFromListing: false, + applicationSection: MultiselectQuestionsApplicationSectionEnum.programs, + } as MultiselectQuestionUpdate) + .expect(404); + expect(res.body.message).toEqual( + `multiselectQuestionId ${id} was requested but not found`, + ); + }); + + it('should update multiselect question', async () => { + const multiselectQuestionA = await prisma.multiselectQuestions.create({ + data: multiselectQuestionFactory(jurisdictionId), + }); + + const res = await request(app.getHttpServer()) + .put(`/multiselectQuestions/${multiselectQuestionA.id}`) + .send({ + id: multiselectQuestionA.id, + text: 'example text', + subText: 'example subText', + description: 'example description', + links: [ + { + title: 'title 1', + url: 'title 1', + }, + { + title: 'title 2', + url: 'title 2', + }, + ], + jurisdictions: [{ id: jurisdictionId }], + options: [ + { + text: 'example option text 1', + ordinal: 1, + description: 'example option description 1', + links: [ + { + title: 'title 3', + url: 'title 3', + }, + ], + collectAddress: true, + exclusive: false, + }, + { + text: 'example option text 2', + ordinal: 2, + description: 'example option description 2', + links: [ + { + title: 'title 4', + url: 'title 4', + }, + ], + collectAddress: true, + exclusive: false, + }, + ], + optOutText: 'example optOutText', + hideFromListing: false, + applicationSection: MultiselectQuestionsApplicationSectionEnum.programs, + } as MultiselectQuestionUpdate) + .expect(200); + + expect(res.body.text).toEqual('example text'); + }); + + it('should throw error when delete endpoint is hit with nonexistent id', async () => { + const id = randomUUID(); + const res = await request(app.getHttpServer()) + .delete(`/multiselectQuestions`) + .send({ + id: id, + } as IdDTO) + .expect(404); + expect(res.body.message).toEqual( + `multiselectQuestionId ${id} was requested but not found`, + ); + }); + + it('should delete multiselect question', async () => { + const multiselectQuestionA = await prisma.multiselectQuestions.create({ + data: multiselectQuestionFactory(jurisdictionId), + }); + + const res = await request(app.getHttpServer()) + .delete(`/multiselectQuestions`) + .send({ + id: multiselectQuestionA.id, + } as IdDTO) + .expect(200); + + expect(res.body.success).toEqual(true); + }); +}); diff --git a/backend_new/test/unit/services/listing.service.spec.ts b/backend_new/test/unit/services/listing.service.spec.ts index e7a751d454..9308f81d95 100644 --- a/backend_new/test/unit/services/listing.service.spec.ts +++ b/backend_new/test/unit/services/listing.service.spec.ts @@ -1988,4 +1988,35 @@ describe('Testing listing service', () => { }, }); }); + + it('testing findListingsWithMultiSelectQuestion()', async () => { + prisma.listings.findMany = jest.fn().mockResolvedValue([ + { + id: 'example id', + name: 'example name', + }, + ]); + + const listings = await service.findListingsWithMultiSelectQuestion( + 'multiselectQuestionId 1', + ); + + expect(listings.length).toEqual(1); + expect(listings[0].id).toEqual('example id'); + expect(listings[0].name).toEqual('example name'); + + expect(prisma.listings.findMany).toHaveBeenCalledWith({ + select: { + id: true, + name: true, + }, + where: { + listingMultiselectQuestions: { + some: { + multiselectQuestionId: 'multiselectQuestionId 1', + }, + }, + }, + }); + }); }); diff --git a/backend_new/test/unit/services/multiselect-question.service.spec.ts b/backend_new/test/unit/services/multiselect-question.service.spec.ts new file mode 100644 index 0000000000..a5358582fc --- /dev/null +++ b/backend_new/test/unit/services/multiselect-question.service.spec.ts @@ -0,0 +1,402 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { PrismaService } from '../../../src/services/prisma.service'; +import { MultiselectQuestionService } from '../../../src/services/multiselect-question.service'; +import { MultiselectQuestionCreate } from '../../../src/dtos/multiselect-questions/multiselect-question-create.dto'; +import { MultiselectQuestionUpdate } from '../../../src/dtos/multiselect-questions/multiselect-question-update.dto'; +import { MultiselectQuestionsApplicationSectionEnum } from '@prisma/client'; +import { MultiselectQuestionQueryParams } from '../../../src/dtos/multiselect-questions/multiselect-question-query-params.dto'; +import { Compare } from '../../../src/dtos/shared/base-filter.dto'; +import { randomUUID } from 'crypto'; + +describe('Testing multiselect question service', () => { + let service: MultiselectQuestionService; + let prisma: PrismaService; + + const mockMultiselectQuestion = (position: number, date: Date) => { + return { + id: randomUUID(), + createdAt: date, + updatedAt: date, + text: `text ${position}`, + subText: `subText ${position}`, + description: `description ${position}`, + links: `{}`, + options: `{}`, + optOutText: `optOutText ${position}`, + hideFromListing: false, + applicationSection: MultiselectQuestionsApplicationSectionEnum.programs, + }; + }; + + const mockMultiselectQuestionSet = (numberToCreate: number, date: Date) => { + const toReturn = []; + for (let i = 0; i < numberToCreate; i++) { + toReturn.push(mockMultiselectQuestion(i, date)); + } + return toReturn; + }; + + beforeAll(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [MultiselectQuestionService, PrismaService], + }).compile(); + + service = module.get( + MultiselectQuestionService, + ); + prisma = module.get(PrismaService); + }); + + it('should get records with empty param call to list()', async () => { + const date = new Date(); + const mockedValue = mockMultiselectQuestionSet(3, date); + prisma.multiselectQuestions.findMany = jest + .fn() + .mockResolvedValue(mockedValue); + + expect(await service.list({})).toEqual([ + { + id: mockedValue[0].id, + createdAt: date, + updatedAt: date, + text: `text 0`, + subText: `subText 0`, + description: `description 0`, + links: `{}`, + options: `{}`, + optOutText: `optOutText 0`, + hideFromListing: false, + applicationSection: MultiselectQuestionsApplicationSectionEnum.programs, + }, + { + id: mockedValue[1].id, + createdAt: date, + updatedAt: date, + text: `text 1`, + subText: `subText 1`, + description: `description 1`, + links: `{}`, + options: `{}`, + optOutText: `optOutText 1`, + hideFromListing: false, + applicationSection: MultiselectQuestionsApplicationSectionEnum.programs, + }, + { + id: mockedValue[2].id, + createdAt: date, + updatedAt: date, + text: `text 2`, + subText: `subText 2`, + description: `description 2`, + links: `{}`, + options: `{}`, + optOutText: `optOutText 2`, + hideFromListing: false, + applicationSection: MultiselectQuestionsApplicationSectionEnum.programs, + }, + ]); + + expect(prisma.multiselectQuestions.findMany).toHaveBeenCalledWith({ + include: { + jurisdictions: true, + }, + where: { + AND: [], + }, + }); + }); + + it('should get records with paramaterized call to list()', async () => { + const date = new Date(); + const mockedValue = mockMultiselectQuestionSet(3, date); + prisma.multiselectQuestions.findMany = jest + .fn() + .mockResolvedValue(mockedValue); + + const params: MultiselectQuestionQueryParams = { + filter: [ + { + $comparison: Compare['='], + applicationSection: + MultiselectQuestionsApplicationSectionEnum.programs, + }, + ], + }; + + expect(await service.list(params)).toEqual([ + { + id: mockedValue[0].id, + createdAt: date, + updatedAt: date, + text: `text 0`, + subText: `subText 0`, + description: `description 0`, + links: `{}`, + options: `{}`, + optOutText: `optOutText 0`, + hideFromListing: false, + applicationSection: MultiselectQuestionsApplicationSectionEnum.programs, + }, + { + id: mockedValue[1].id, + createdAt: date, + updatedAt: date, + text: `text 1`, + subText: `subText 1`, + description: `description 1`, + links: `{}`, + options: `{}`, + optOutText: `optOutText 1`, + hideFromListing: false, + applicationSection: MultiselectQuestionsApplicationSectionEnum.programs, + }, + { + id: mockedValue[2].id, + createdAt: date, + updatedAt: date, + text: `text 2`, + subText: `subText 2`, + description: `description 2`, + links: `{}`, + options: `{}`, + optOutText: `optOutText 2`, + hideFromListing: false, + applicationSection: MultiselectQuestionsApplicationSectionEnum.programs, + }, + ]); + + expect(prisma.multiselectQuestions.findMany).toHaveBeenCalledWith({ + include: { + jurisdictions: true, + }, + where: { + AND: [ + { + OR: [ + { + applicationSection: { + equals: MultiselectQuestionsApplicationSectionEnum.programs, + mode: 'default', + }, + }, + ], + }, + ], + }, + }); + }); + + it('should get record with call to findOne()', async () => { + const date = new Date(); + const mockedValue = mockMultiselectQuestion(3, date); + prisma.multiselectQuestions.findFirst = jest + .fn() + .mockResolvedValue(mockedValue); + + expect(await service.findOne('example Id')).toEqual({ + id: mockedValue.id, + createdAt: date, + updatedAt: date, + text: `text 3`, + subText: `subText 3`, + description: `description 3`, + links: `{}`, + options: `{}`, + optOutText: `optOutText 3`, + hideFromListing: false, + applicationSection: MultiselectQuestionsApplicationSectionEnum.programs, + }); + + expect(prisma.multiselectQuestions.findFirst).toHaveBeenCalledWith({ + where: { + id: { + equals: 'example Id', + }, + }, + include: { + jurisdictions: true, + }, + }); + }); + + it('should error when nonexistent id is passed to findOne()', async () => { + prisma.multiselectQuestions.findFirst = jest.fn().mockResolvedValue(null); + + await expect( + async () => await service.findOne('example Id'), + ).rejects.toThrowError(); + + expect(prisma.multiselectQuestions.findFirst).toHaveBeenCalledWith({ + where: { + id: { + equals: 'example Id', + }, + }, + include: { + jurisdictions: true, + }, + }); + }); + + it('should create with call to create()', async () => { + const date = new Date(); + const mockedValue = mockMultiselectQuestion(3, date); + prisma.multiselectQuestions.create = jest + .fn() + .mockResolvedValue(mockedValue); + + const params: MultiselectQuestionCreate = { + text: `text 4`, + subText: `subText 4`, + description: `description 4`, + links: [], + options: [], + optOutText: `optOutText 4`, + hideFromListing: false, + applicationSection: MultiselectQuestionsApplicationSectionEnum.programs, + jurisdictions: [{ id: 'jurisdiction id' }], + }; + + expect(await service.create(params)).toEqual({ + id: mockedValue.id, + createdAt: date, + updatedAt: date, + text: `text 3`, + subText: `subText 3`, + description: `description 3`, + links: `{}`, + options: `{}`, + optOutText: `optOutText 3`, + hideFromListing: false, + applicationSection: MultiselectQuestionsApplicationSectionEnum.programs, + }); + + expect(prisma.multiselectQuestions.create).toHaveBeenCalledWith({ + data: { + text: `text 4`, + subText: `subText 4`, + description: `description 4`, + links: '[]', + options: '[]', + optOutText: `optOutText 4`, + hideFromListing: false, + applicationSection: MultiselectQuestionsApplicationSectionEnum.programs, + jurisdictions: { connect: [{ id: 'jurisdiction id' }] }, + }, + include: { + jurisdictions: true, + }, + }); + }); + + it('should update with call to update()', async () => { + const date = new Date(); + + const mockedMultiselectQuestions = mockMultiselectQuestion(3, date); + + prisma.multiselectQuestions.findFirst = jest + .fn() + .mockResolvedValue(mockedMultiselectQuestions); + prisma.multiselectQuestions.update = jest.fn().mockResolvedValue({ + ...mockedMultiselectQuestions, + text: '', + applicationSection: + MultiselectQuestionsApplicationSectionEnum.preferences, + }); + + const params: MultiselectQuestionUpdate = { + id: mockedMultiselectQuestions.id, + jurisdictions: [], + text: '', + applicationSection: + MultiselectQuestionsApplicationSectionEnum.preferences, + }; + + expect(await service.update(params)).toEqual({ + id: mockedMultiselectQuestions.id, + createdAt: date, + updatedAt: date, + text: '', + subText: `subText 3`, + description: `description 3`, + links: `{}`, + options: `{}`, + optOutText: `optOutText 3`, + hideFromListing: false, + applicationSection: + MultiselectQuestionsApplicationSectionEnum.preferences, + }); + + expect(prisma.multiselectQuestions.findFirst).toHaveBeenCalledWith({ + where: { + id: mockedMultiselectQuestions.id, + }, + }); + + expect(prisma.multiselectQuestions.update).toHaveBeenCalledWith({ + data: { + jurisdictions: { + connect: [], + }, + text: '', + applicationSection: + MultiselectQuestionsApplicationSectionEnum.preferences, + }, + where: { + id: mockedMultiselectQuestions.id, + }, + include: { + jurisdictions: true, + }, + }); + }); + + it('should error when nonexistent id is passed to findOne()', async () => { + prisma.multiselectQuestions.findFirst = jest.fn().mockResolvedValue(null); + prisma.multiselectQuestions.update = jest.fn().mockResolvedValue(null); + + const params: MultiselectQuestionUpdate = { + id: 'example id', + text: '', + jurisdictions: [], + applicationSection: MultiselectQuestionsApplicationSectionEnum.programs, + }; + + await expect( + async () => await service.update(params), + ).rejects.toThrowError(); + + expect(prisma.multiselectQuestions.findFirst).toHaveBeenCalledWith({ + where: { + id: 'example id', + }, + }); + }); + + it('should delete with call to delete()', async () => { + const date = new Date(); + const mockedValue = mockMultiselectQuestion(3, date); + prisma.multiselectQuestions.findFirst = jest + .fn() + .mockResolvedValue(mockedValue); + prisma.multiselectQuestions.delete = jest + .fn() + .mockResolvedValue(mockedValue); + + expect(await service.delete('example Id')).toEqual({ + success: true, + }); + + expect(prisma.multiselectQuestions.delete).toHaveBeenCalledWith({ + where: { + id: 'example Id', + }, + }); + + expect(prisma.multiselectQuestions.findFirst).toHaveBeenCalledWith({ + where: { + id: 'example Id', + }, + }); + }); +}); diff --git a/backend_new/test/unit/services/translation.service.spec.ts b/backend_new/test/unit/services/translation.service.spec.ts index b6b8d49291..7ba584afac 100644 --- a/backend_new/test/unit/services/translation.service.spec.ts +++ b/backend_new/test/unit/services/translation.service.spec.ts @@ -79,12 +79,10 @@ const mockListing = (): ListingGet => { description: 'untranslated multiselect description', subText: 'untranslated multiselect subtext', optOutText: 'untranslated multiselect opt out text', - listings: [], jurisdictions: [], applicationSection: MultiselectQuestionsApplicationSectionEnum.preferences, }, - listings: basicListing, }, ], }; diff --git a/backend_new/types/src/backend-swagger.ts b/backend_new/types/src/backend-swagger.ts index f9380f7a27..65f0b8a0d1 100644 --- a/backend_new/types/src/backend-swagger.ts +++ b/backend_new/types/src/backend-swagger.ts @@ -175,6 +175,36 @@ export class ListingsService { /** 适配ios13getèŻ·æ±‚äžć…èźžćžŠbody */ + axios(configs, resolve, reject); + }); + } + /** + * Get listings by multiselect question id + */ + retrieveListings( + params: { + /** */ + multiselectQuestionId: string; + } = {} as any, + options: IRequestOptions = {}, + ): Promise { + return new Promise((resolve, reject) => { + let url = + basePath + '/listings/byMultiselectQuestion/{multiselectQuestionId}'; + url = url.replace( + '{multiselectQuestionId}', + params['multiselectQuestionId'] + '', + ); + + const configs: IRequestConfig = getConfigs( + 'get', + 'application/json', + url, + options, + ); + + /** 适配ios13getèŻ·æ±‚äžć…èźžćžŠbody */ + axios(configs, resolve, reject); }); } @@ -1006,6 +1036,153 @@ export class JurisdictionsService { } } +export class MultiselectQuestionsService { + /** + * List multiselect questions + */ + list( + params: { + /** */ + $comparison: string; + /** */ + jurisdiction?: string; + /** */ + applicationSection?: string; + } = {} as any, + options: IRequestOptions = {}, + ): Promise { + return new Promise((resolve, reject) => { + let url = basePath + '/multiselectQuestions'; + + const configs: IRequestConfig = getConfigs( + 'get', + 'application/json', + url, + options, + ); + configs.params = { + $comparison: params['$comparison'], + jurisdiction: params['jurisdiction'], + applicationSection: params['applicationSection'], + }; + + /** 适配ios13getèŻ·æ±‚äžć…èźžćžŠbody */ + + axios(configs, resolve, reject); + }); + } + /** + * Create multiselect question + */ + create( + params: { + /** requestBody */ + body?: MultiselectQuestionCreate; + } = {} as any, + options: IRequestOptions = {}, + ): Promise { + return new Promise((resolve, reject) => { + let url = basePath + '/multiselectQuestions'; + + const configs: IRequestConfig = getConfigs( + 'post', + 'application/json', + url, + options, + ); + + let data = params.body; + + configs.data = data; + + axios(configs, resolve, reject); + }); + } + /** + * Delete multiselect question by id + */ + delete( + params: { + /** requestBody */ + body?: IdDTO; + } = {} as any, + options: IRequestOptions = {}, + ): Promise { + return new Promise((resolve, reject) => { + let url = basePath + '/multiselectQuestions'; + + const configs: IRequestConfig = getConfigs( + 'delete', + 'application/json', + url, + options, + ); + + let data = params.body; + + configs.data = data; + + axios(configs, resolve, reject); + }); + } + /** + * Get multiselect question by id + */ + retrieve( + params: { + /** */ + multiselectQuestionId: string; + } = {} as any, + options: IRequestOptions = {}, + ): Promise { + return new Promise((resolve, reject) => { + let url = basePath + '/multiselectQuestions/{multiselectQuestionId}'; + url = url.replace( + '{multiselectQuestionId}', + params['multiselectQuestionId'] + '', + ); + + const configs: IRequestConfig = getConfigs( + 'get', + 'application/json', + url, + options, + ); + + /** 适配ios13getèŻ·æ±‚äžć…èźžćžŠbody */ + + axios(configs, resolve, reject); + }); + } + /** + * Update multiselect question + */ + update( + params: { + /** requestBody */ + body?: MultiselectQuestionUpdate; + } = {} as any, + options: IRequestOptions = {}, + ): Promise { + return new Promise((resolve, reject) => { + let url = basePath + '/multiselectQuestions/{multiselectQuestionId}'; + + const configs: IRequestConfig = getConfigs( + 'put', + 'application/json', + url, + options, + ); + + let data = params.body; + + configs.data = data; + + axios(configs, resolve, reject); + }); + } +} + export interface ListingsQueryParams { /** */ page?: number; @@ -1068,38 +1245,46 @@ export interface PaginationAllowsAllQueryParams { limit?: number | 'all'; } -export interface ApplicationMethod { +export interface IdDTO { /** */ id: string; /** */ - createdAt: Date; + name?: string; +} +export interface MultiselectLink { /** */ - updatedAt: Date; + title: string; /** */ - type: ApplicationMethodsTypeEnum; + url: string; } -export interface UnitType { +export interface MultiselectOption { /** */ - id: string; + text: string; /** */ - createdAt: Date; + untranslatedText: string; /** */ - updatedAt: Date; + ordinal: number; /** */ - name: UnitTypeEnum; + description: string; /** */ - numBedrooms: number; + links: MultiselectLink[]; + + /** */ + collectAddress: boolean; + + /** */ + exclusive: boolean; } -export interface UnitAccessibilityPriorityType { +export interface MultiselectQuestion { /** */ id: string; @@ -1110,91 +1295,84 @@ export interface UnitAccessibilityPriorityType { updatedAt: Date; /** */ - name: UnitAccessibilityPriorityTypeEnum; -} + text: string; -export interface MinMaxCurrency { /** */ - min: string; + untranslatedText: string; /** */ - max: string; -} + untranslatedOptOutText: string; -export interface MinMax { /** */ - min: number; + subText: string; /** */ - max: number; -} + description: string; -export interface UnitSummary { /** */ - unitTypes: UnitType; + links: MultiselectLink[]; /** */ - minIncomeRange: MinMaxCurrency; + jurisdictions: IdDTO[]; /** */ - occupancyRange: MinMax; + options: MultiselectOption[]; /** */ - rentAsPercentIncomeRange: MinMax; + optOutText: string; /** */ - rentRange: MinMaxCurrency; + hideFromListing: boolean; /** */ - totalAvailable: number; + applicationSection: MultiselectQuestionsApplicationSectionEnum; +} +export interface ListingMultiselectQuestion { /** */ - areaRange: MinMax; + multiselectQuestions: MultiselectQuestion; /** */ - floorRange?: MinMax; + ordinal: number; } -export interface UnitSummaryByAMI { +export interface ApplicationMethod { /** */ - percent: string; + id: string; /** */ - byUnitType: UnitSummary[]; -} + createdAt: Date; -export interface HMI { /** */ - columns: object; + updatedAt: Date; /** */ - rows: object[]; + type: ApplicationMethodsTypeEnum; } -export interface UnitsSummarized { - /** */ - unitTypes: UnitType[]; - +export interface Asset { /** */ - priorityTypes: UnitAccessibilityPriorityType[]; + id: string; /** */ - amiPercentages: string[]; + createdAt: Date; /** */ - byUnitTypeAndRent: UnitSummary[]; + updatedAt: Date; +} +export interface Address { /** */ - byUnitType: UnitSummary[]; + id: string; /** */ - byAMI: UnitSummaryByAMI[]; + createdAt: Date; /** */ - hmi: HMI; + updatedAt: Date; } -export interface ListingGet { +export interface Jurisdiction { /** */ id: string; @@ -1205,82 +1383,84 @@ export interface ListingGet { updatedAt: Date; /** */ - applicationPickUpAddressType: ApplicationAddressTypeEnum; - - /** */ - applicationDropOffAddressType: ApplicationAddressTypeEnum; + name: string; /** */ - applicationMailingAddressType: ApplicationAddressTypeEnum; + notificationsSignUpUrl: string; /** */ - status: ListingsStatusEnum; + languages: string[]; /** */ - reviewOrderType: ReviewOrderTypeEnum; + multiselectQuestions: string[]; /** */ - showWaitlist: boolean; + partnerTerms: string; /** */ - referralApplication?: ApplicationMethod; + publicUrl: string; /** */ - unitsSummarized: UnitsSummarized; -} + emailFromAddress: string; -export interface PaginatedListing { /** */ - items: ListingGet[]; -} + rentalAssistanceDefault: string; -export interface AmiChartItem { /** */ - percentOfAmi: number; + enablePartnerSettings: boolean; /** */ - householdSize: number; + enableAccessibilityFeatures: boolean; /** */ - income: number; + enableUtilitiesIncluded: boolean; } -export interface IdDTO { +export interface ReservedCommunityType { /** */ id: string; /** */ - name?: string; -} + createdAt: Date; -export interface AmiChartCreate { /** */ - items: AmiChartItem[]; + updatedAt: Date; /** */ name: string; + /** */ + description: string; + /** */ jurisdictions: IdDTO; } -export interface AmiChartUpdate { +export interface ListingImage {} + +export interface ListingFeatures { /** */ id: string; /** */ - items: AmiChartItem[]; + createdAt: Date; /** */ - name: string; + updatedAt: Date; } -export interface AmiChartQueryParams { +export interface ListingUtilities { /** */ - jurisdictionId?: string; + id: string; + + /** */ + createdAt: Date; + + /** */ + updatedAt: Date; } -export interface AmiChart { +export interface Unit { /** */ id: string; @@ -1289,50 +1469,432 @@ export interface AmiChart { /** */ updatedAt: Date; +} +export interface UnitType { /** */ - items: AmiChartItem[]; + id: string; /** */ - name: string; + createdAt: Date; /** */ - jurisdictions: IdDTO; + updatedAt: Date; + + /** */ + name: UnitTypeEnum; + + /** */ + numBedrooms: number; } -export interface SuccessDTO { +export interface UnitAccessibilityPriorityType { /** */ - success: boolean; + id: string; + + /** */ + createdAt: Date; + + /** */ + updatedAt: Date; + + /** */ + name: UnitAccessibilityPriorityTypeEnum; } -export interface ReservedCommunityTypeCreate { +export interface MinMaxCurrency { + /** */ + min: string; + + /** */ + max: string; +} + +export interface MinMax { + /** */ + min: number; + + /** */ + max: number; +} + +export interface UnitSummary { + /** */ + unitTypes: UnitType; + + /** */ + minIncomeRange: MinMaxCurrency; + + /** */ + occupancyRange: MinMax; + + /** */ + rentAsPercentIncomeRange: MinMax; + + /** */ + rentRange: MinMaxCurrency; + + /** */ + totalAvailable: number; + + /** */ + areaRange: MinMax; + + /** */ + floorRange?: MinMax; +} + +export interface UnitSummaryByAMI { + /** */ + percent: string; + + /** */ + byUnitType: UnitSummary[]; +} + +export interface HMI { + /** */ + columns: object; + + /** */ + rows: object[]; +} + +export interface UnitsSummarized { + /** */ + unitTypes: UnitType[]; + + /** */ + priorityTypes: UnitAccessibilityPriorityType[]; + + /** */ + amiPercentages: string[]; + + /** */ + byUnitTypeAndRent: UnitSummary[]; + + /** */ + byUnitType: UnitSummary[]; + + /** */ + byAMI: UnitSummaryByAMI[]; + + /** */ + hmi: HMI; +} + +export interface UnitsSummary {} + +export interface ListingGet { + /** */ + id: string; + + /** */ + createdAt: Date; + + /** */ + updatedAt: Date; + + /** */ + additionalApplicationSubmissionNotes: string; + + /** */ + digitalApplication: boolean; + + /** */ + commonDigitalApplication: boolean; + + /** */ + paperApplication: boolean; + + /** */ + referralOpportunity: boolean; + + /** */ + accessibility: string; + + /** */ + amenities: string; + + /** */ + buildingTotalUnits: number; + + /** */ + developer: string; + + /** */ + householdSizeMax: number; + + /** */ + householdSizeMin: number; + + /** */ + neighborhood: string; + + /** */ + petPolicy: string; + + /** */ + smokingPolicy: string; + + /** */ + unitsAvailable: number; + + /** */ + unitAmenities: string; + + /** */ + servicesOffered: string; + + /** */ + yearBuilt: number; + + /** */ + applicationDueDate: Date; + + /** */ + applicationOpenDate: Date; + + /** */ + applicationFee: string; + + /** */ + applicationOrganization: string; + + /** */ + applicationPickUpAddressOfficeHours: string; + + /** */ + applicationPickUpAddressType: ApplicationAddressTypeEnum; + + /** */ + applicationDropOffAddressOfficeHours: string; + + /** */ + applicationDropOffAddressType: ApplicationAddressTypeEnum; + + /** */ + applicationMailingAddressType: ApplicationAddressTypeEnum; + + /** */ + buildingSelectionCriteria: string; + + /** */ + costsNotIncluded: string; + + /** */ + creditHistory: string; + + /** */ + criminalBackground: string; + + /** */ + depositMin: string; + + /** */ + depositMax: string; + + /** */ + depositHelperText: string; + + /** */ + disableUnitsAccordion: boolean; + + /** */ + leasingAgentEmail: string; + + /** */ + leasingAgentName: string; + + /** */ + leasingAgentOfficeHours: string; + + /** */ + leasingAgentPhone: string; + + /** */ + leasingAgentTitle: string; + /** */ name: string; /** */ - description: string; + postmarkedApplicationsReceivedByDate: Date; + + /** */ + programRules: string; + + /** */ + rentalAssistance: string; + + /** */ + rentalHistory: string; + + /** */ + requiredDocuments: string; + + /** */ + specialNotes: string; + + /** */ + waitlistCurrentSize: number; + + /** */ + waitlistMaxSize: number; + + /** */ + whatToExpect: string; + + /** */ + status: ListingsStatusEnum; + + /** */ + reviewOrderType: ReviewOrderTypeEnum; + + /** */ + applicationConfig: object; + + /** */ + displayWaitlistSize: boolean; + + /** */ + showWaitlist: boolean; + + /** */ + reservedCommunityDescription: string; + + /** */ + reservedCommunityMinAge: number; + + /** */ + resultLink: string; + + /** */ + isWaitlistOpen: boolean; + + /** */ + waitlistOpenSpots: number; + + /** */ + customMapPin: boolean; + + /** */ + publishedAt: Date; + + /** */ + closedAt: Date; + + /** */ + afsLastRunAt: Date; + + /** */ + lastApplicationUpdateAt: Date; + + /** */ + listingMultiselectQuestions: ListingMultiselectQuestion[]; + + /** */ + applicationMethods: ApplicationMethod[]; + + /** */ + referralApplication?: ApplicationMethod; + + /** */ + assets: Asset[]; + + /** */ + events: Asset[]; + + /** */ + listingsBuildingAddress: Address; + + /** */ + listingsApplicationPickUpAddress: Address; + + /** */ + listingsApplicationDropOffAddress: Address; + + /** */ + listingsApplicationMailingAddress: Address; + + /** */ + listingsLeasingAgentAddress: Address; + + /** */ + listingsBuildingSelectionCriteriaFile: Asset; + + /** */ + jurisdictions: Jurisdiction; + + /** */ + listingsResult: Asset; + + /** */ + reservedCommunityTypes: ReservedCommunityType; + + /** */ + listingImages: ListingImage[]; + + /** */ + listingFeatures: ListingFeatures; + + /** */ + listingUtilities: ListingUtilities; + + /** */ + units: Unit[]; + + /** */ + unitsSummarized: UnitsSummarized; + + /** */ + unitsSummary: UnitsSummary[]; +} + +export interface PaginatedListing { + /** */ + items: ListingGet[]; +} + +export interface AmiChartItem { + /** */ + percentOfAmi: number; + + /** */ + householdSize: number; + + /** */ + income: number; +} + +export interface AmiChartCreate { + /** */ + items: AmiChartItem[]; + + /** */ + name: string; /** */ jurisdictions: IdDTO; } -export interface ReservedCommunityTypeUpdate { +export interface AmiChartUpdate { /** */ id: string; /** */ - name: string; + items: AmiChartItem[]; /** */ - description: string; + name: string; } -export interface ReservedCommunityTypeQueryParams { +export interface AmiChartQueryParams { /** */ jurisdictionId?: string; } -export interface ReservedCommunityType { +export interface AmiChart { /** */ id: string; @@ -1342,6 +1904,22 @@ export interface ReservedCommunityType { /** */ updatedAt: Date; + /** */ + items: AmiChartItem[]; + + /** */ + name: string; + + /** */ + jurisdictions: IdDTO; +} + +export interface SuccessDTO { + /** */ + success: boolean; +} + +export interface ReservedCommunityTypeCreate { /** */ name: string; @@ -1352,6 +1930,22 @@ export interface ReservedCommunityType { jurisdictions: IdDTO; } +export interface ReservedCommunityTypeUpdate { + /** */ + id: string; + + /** */ + name: string; + + /** */ + description: string; +} + +export interface ReservedCommunityTypeQueryParams { + /** */ + jurisdictionId?: string; +} + export interface UnitTypeCreate { /** */ name: UnitTypeEnum; @@ -1478,48 +2072,87 @@ export interface JurisdictionUpdate { enableUtilitiesIncluded: boolean; } -export interface Jurisdiction { +export interface MultiselectQuestionCreate { + /** */ + text: string; + + /** */ + untranslatedOptOutText: string; + + /** */ + subText: string; + + /** */ + description: string; + + /** */ + links: MultiselectLink[]; + + /** */ + jurisdictions: IdDTO[]; + + /** */ + options: MultiselectOption[]; + + /** */ + optOutText: string; + + /** */ + hideFromListing: boolean; + + /** */ + applicationSection: MultiselectQuestionsApplicationSectionEnum; +} + +export interface MultiselectQuestionUpdate { /** */ id: string; /** */ - createdAt: Date; + text: string; /** */ - updatedAt: Date; + untranslatedOptOutText: string; /** */ - name: string; + subText: string; /** */ - notificationsSignUpUrl: string; + description: string; /** */ - languages: string[]; + links: MultiselectLink[]; /** */ - multiselectQuestions: string[]; + jurisdictions: IdDTO[]; /** */ - partnerTerms: string; + options: MultiselectOption[]; /** */ - publicUrl: string; + optOutText: string; /** */ - emailFromAddress: string; + hideFromListing: boolean; /** */ - rentalAssistanceDefault: string; + applicationSection: MultiselectQuestionsApplicationSectionEnum; +} +export interface MultiselectQuestionFilterParams { /** */ - enablePartnerSettings: boolean; + $comparison: EnumMultiselectQuestionFilterParamsComparison; /** */ - enableAccessibilityFeatures: boolean; + jurisdiction?: string; /** */ - enableUtilitiesIncluded: boolean; + applicationSection?: string; +} + +export interface MultiselectQuestionQueryParams { + /** */ + filter?: MultiselectQuestionFilterParams[]; } export enum ListingViews { @@ -1561,6 +2194,11 @@ export enum ReviewOrderTypeEnum { 'waitlist' = 'waitlist', } +export enum MultiselectQuestionsApplicationSectionEnum { + 'programs' = 'programs', + 'preferences' = 'preferences', +} + export enum ApplicationMethodsTypeEnum { 'Internal' = 'Internal', 'FileDownload' = 'FileDownload', @@ -1595,3 +2233,11 @@ export enum UnitRentTypeEnum { 'fixed' = 'fixed', 'percentageOfIncome' = 'percentageOfIncome', } +export enum EnumMultiselectQuestionFilterParamsComparison { + '=' = '=', + '<>' = '<>', + 'IN' = 'IN', + '>=' = '>=', + '<=' = '<=', + 'NA' = 'NA', +} From 4b5beeb683f09c92b22940bd70a7eade66d9c44c Mon Sep 17 00:00:00 2001 From: Yazeed Loonat Date: Wed, 2 Aug 2023 08:43:05 -0700 Subject: [PATCH 15/57] feat: Prisma heartbeat (#3569) * feat: heartbeat endpoint * fix: update for ci * fix: updates per morgan * fix: updates per morgan * fix: missed a spot --- .circleci/config.yml | 7 +--- backend_new/src/app.controller.spec.ts | 22 ----------- backend_new/src/app.controller.ts | 12 ------ backend_new/src/app.module.ts | 37 ------------------ backend_new/src/app.service.ts | 8 ---- backend_new/src/controllers/app.controller.ts | 26 +++++++++++++ backend_new/src/main.ts | 2 +- backend_new/src/modules/app.module.ts | 38 +++++++++++++++++++ backend_new/src/services/app.service.ts | 15 ++++++++ .../test/integration/ami-chart.e2e-spec.ts | 2 +- backend_new/test/integration/app.e2e-spec.ts | 18 ++++----- .../test/integration/jurisdiction.e2e-spec.ts | 2 +- .../test/integration/listing.e2e-spec.ts | 2 +- .../multiselect-question.e2e-spec.ts | 2 +- .../reserved-community-type.e2e-spec.ts | 2 +- ...it-accessibility-priority-type.e2e-spec.ts | 2 +- .../integration/unit-rent-type.e2e-spec.ts | 2 +- .../test/integration/unit-type.e2e-spec.ts | 2 +- .../test/unit/services/app.service.spec.ts | 24 ++++++++++++ backend_new/types/src/backend-swagger.ts | 32 +++++++++++++--- 20 files changed, 147 insertions(+), 110 deletions(-) delete mode 100644 backend_new/src/app.controller.spec.ts delete mode 100644 backend_new/src/app.controller.ts delete mode 100644 backend_new/src/app.module.ts delete mode 100644 backend_new/src/app.service.ts create mode 100644 backend_new/src/controllers/app.controller.ts create mode 100644 backend_new/src/modules/app.module.ts create mode 100644 backend_new/src/services/app.service.ts create mode 100644 backend_new/test/unit/services/app.service.spec.ts diff --git a/.circleci/config.yml b/.circleci/config.yml index a64c94ebca..7ae0241025 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -77,11 +77,9 @@ jobs: - restore_cache: key: build-cache-{{ .Environment.CIRCLE_SHA1 }} - run: - name: DB Setup + Backend Core Tests + name: DB Setup command: | yarn test:backend:core:dbsetup - yarn test:backend:core - yarn test:e2e:backend:core environment: PORT: "3100" EMAIL_API_KEY: "SG.SOME-LONG-SECRET-KEY" @@ -148,9 +146,6 @@ workflows: - jest-shared-helpers: requires: - setup - - jest-backend: - requires: - - setup - jest-new-backend: requires: - setup-with-new-db diff --git a/backend_new/src/app.controller.spec.ts b/backend_new/src/app.controller.spec.ts deleted file mode 100644 index d22f3890a3..0000000000 --- a/backend_new/src/app.controller.spec.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { Test, TestingModule } from '@nestjs/testing'; -import { AppController } from './app.controller'; -import { AppService } from './app.service'; - -describe('AppController', () => { - let appController: AppController; - - beforeEach(async () => { - const app: TestingModule = await Test.createTestingModule({ - controllers: [AppController], - providers: [AppService], - }).compile(); - - appController = app.get(AppController); - }); - - describe('root', () => { - it('should return "Hello World!"', () => { - expect(appController.getHello()).toBe('Hello World!'); - }); - }); -}); diff --git a/backend_new/src/app.controller.ts b/backend_new/src/app.controller.ts deleted file mode 100644 index cce879ee62..0000000000 --- a/backend_new/src/app.controller.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { Controller, Get } from '@nestjs/common'; -import { AppService } from './app.service'; - -@Controller() -export class AppController { - constructor(private readonly appService: AppService) {} - - @Get() - getHello(): string { - return this.appService.getHello(); - } -} diff --git a/backend_new/src/app.module.ts b/backend_new/src/app.module.ts deleted file mode 100644 index fbc54ed9a6..0000000000 --- a/backend_new/src/app.module.ts +++ /dev/null @@ -1,37 +0,0 @@ -import { Module } from '@nestjs/common'; -import { AppController } from './app.controller'; -import { AppService } from './app.service'; -import { AmiChartModule } from './modules/ami-chart.module'; -import { ListingModule } from './modules/listing.module'; -import { ReservedCommunityTypeModule } from './modules/reserved-community-type.module'; -import { UnitAccessibilityPriorityTypeServiceModule } from './modules/unit-accessibility-priority-type.module'; -import { UnitTypeModule } from './modules/unit-type.module'; -import { UnitRentTypeModule } from './modules/unit-rent-type.module'; -import { JurisdictionModule } from './modules/jurisdiction.module'; -import { MultiselectQuestionModule } from './modules/multiselect-question.module'; - -@Module({ - imports: [ - ListingModule, - AmiChartModule, - ReservedCommunityTypeModule, - UnitTypeModule, - UnitAccessibilityPriorityTypeServiceModule, - UnitRentTypeModule, - JurisdictionModule, - MultiselectQuestionModule, - ], - controllers: [AppController], - providers: [AppService], - exports: [ - ListingModule, - AmiChartModule, - ReservedCommunityTypeModule, - UnitTypeModule, - UnitAccessibilityPriorityTypeServiceModule, - UnitRentTypeModule, - JurisdictionModule, - MultiselectQuestionModule, - ], -}) -export class AppModule {} diff --git a/backend_new/src/app.service.ts b/backend_new/src/app.service.ts deleted file mode 100644 index 927d7cca0b..0000000000 --- a/backend_new/src/app.service.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { Injectable } from '@nestjs/common'; - -@Injectable() -export class AppService { - getHello(): string { - return 'Hello World!'; - } -} diff --git a/backend_new/src/controllers/app.controller.ts b/backend_new/src/controllers/app.controller.ts new file mode 100644 index 0000000000..8914528c25 --- /dev/null +++ b/backend_new/src/controllers/app.controller.ts @@ -0,0 +1,26 @@ +import { Controller, Get } from '@nestjs/common'; +import { + ApiExtraModels, + ApiOkResponse, + ApiOperation, + ApiTags, +} from '@nestjs/swagger'; +import { SuccessDTO } from '../dtos/shared/success.dto'; +import { AppService } from '../services/app.service'; + +@Controller() +@ApiExtraModels(SuccessDTO) +@ApiTags('root') +export class AppController { + constructor(private readonly appService: AppService) {} + + @Get() + @ApiOperation({ + summary: 'Health check endpoint', + operationId: 'healthCheck', + }) + @ApiOkResponse({ type: SuccessDTO }) + async healthCheck(): Promise { + return await this.appService.healthCheck(); + } +} diff --git a/backend_new/src/main.ts b/backend_new/src/main.ts index cfd5f2c0a2..2f3d4c27a8 100644 --- a/backend_new/src/main.ts +++ b/backend_new/src/main.ts @@ -1,6 +1,6 @@ import { NestFactory } from '@nestjs/core'; import { SwaggerModule, DocumentBuilder } from '@nestjs/swagger'; -import { AppModule } from './app.module'; +import { AppModule } from './modules/app.module'; async function bootstrap() { const app = await NestFactory.create(AppModule); diff --git a/backend_new/src/modules/app.module.ts b/backend_new/src/modules/app.module.ts new file mode 100644 index 0000000000..b64e8ee695 --- /dev/null +++ b/backend_new/src/modules/app.module.ts @@ -0,0 +1,38 @@ +import { Module } from '@nestjs/common'; +import { AppController } from '../controllers/app.controller'; +import { AppService } from '../services/app.service'; +import { PrismaService } from '../services/prisma.service'; +import { AmiChartModule } from './ami-chart.module'; +import { ListingModule } from './listing.module'; +import { ReservedCommunityTypeModule } from './reserved-community-type.module'; +import { UnitAccessibilityPriorityTypeServiceModule } from './unit-accessibility-priority-type.module'; +import { UnitTypeModule } from './unit-type.module'; +import { UnitRentTypeModule } from './unit-rent-type.module'; +import { JurisdictionModule } from './jurisdiction.module'; +import { MultiselectQuestionModule } from './multiselect-question.module'; + +@Module({ + imports: [ + ListingModule, + AmiChartModule, + ReservedCommunityTypeModule, + UnitTypeModule, + UnitAccessibilityPriorityTypeServiceModule, + UnitRentTypeModule, + JurisdictionModule, + MultiselectQuestionModule, + ], + controllers: [AppController], + providers: [AppService, PrismaService], + exports: [ + ListingModule, + AmiChartModule, + ReservedCommunityTypeModule, + UnitTypeModule, + UnitAccessibilityPriorityTypeServiceModule, + UnitRentTypeModule, + JurisdictionModule, + MultiselectQuestionModule, + ], +}) +export class AppModule {} diff --git a/backend_new/src/services/app.service.ts b/backend_new/src/services/app.service.ts new file mode 100644 index 0000000000..a0678a970a --- /dev/null +++ b/backend_new/src/services/app.service.ts @@ -0,0 +1,15 @@ +import { Injectable } from '@nestjs/common'; +import { SuccessDTO } from '../dtos/shared/success.dto'; +import { PrismaService } from './prisma.service'; + +@Injectable() +export class AppService { + constructor(private prisma: PrismaService) {} + + async healthCheck(): Promise { + await this.prisma.$queryRaw`SELECT 1`; + return { + success: true, + } as SuccessDTO; + } +} diff --git a/backend_new/test/integration/ami-chart.e2e-spec.ts b/backend_new/test/integration/ami-chart.e2e-spec.ts index 0e2711d041..ffb675de59 100644 --- a/backend_new/test/integration/ami-chart.e2e-spec.ts +++ b/backend_new/test/integration/ami-chart.e2e-spec.ts @@ -1,7 +1,7 @@ import { Test, TestingModule } from '@nestjs/testing'; import { INestApplication } from '@nestjs/common'; import * as request from 'supertest'; -import { AppModule } from '../../src/app.module'; +import { AppModule } from '../../src/modules/app.module'; import { PrismaService } from '../../src/services/prisma.service'; import { jurisdictionFactory } from '../../prisma/seed-helpers/jurisdiction-factory'; import { stringify } from 'qs'; diff --git a/backend_new/test/integration/app.e2e-spec.ts b/backend_new/test/integration/app.e2e-spec.ts index b87678e76f..d089b62b91 100644 --- a/backend_new/test/integration/app.e2e-spec.ts +++ b/backend_new/test/integration/app.e2e-spec.ts @@ -1,11 +1,10 @@ import { Test, TestingModule } from '@nestjs/testing'; import { INestApplication } from '@nestjs/common'; import * as request from 'supertest'; -import { AppModule } from '../../src/app.module'; +import { AppModule } from '../../src/modules/app.module'; -describe('AppController (e2e)', () => { +describe('App Controller Tests', () => { let app: INestApplication; - beforeAll(async () => { const moduleFixture: TestingModule = await Test.createTestingModule({ imports: [AppModule], @@ -15,14 +14,11 @@ describe('AppController (e2e)', () => { await app.init(); }); - afterAll(async () => { - await app.close(); - }); + it('should return a successDTO', async () => { + const res = await request(app.getHttpServer()).get('/').expect(200); - it('/ (GET)', () => { - return request(app.getHttpServer()) - .get('/') - .expect(200) - .expect('Hello World!'); + expect(res.body).toEqual({ + success: true, + }); }); }); diff --git a/backend_new/test/integration/jurisdiction.e2e-spec.ts b/backend_new/test/integration/jurisdiction.e2e-spec.ts index c23fc6d69f..0eaf238890 100644 --- a/backend_new/test/integration/jurisdiction.e2e-spec.ts +++ b/backend_new/test/integration/jurisdiction.e2e-spec.ts @@ -1,7 +1,7 @@ import { Test, TestingModule } from '@nestjs/testing'; import { INestApplication } from '@nestjs/common'; import * as request from 'supertest'; -import { AppModule } from '../../src/app.module'; +import { AppModule } from '../../src/modules/app.module'; import { PrismaService } from '../../src/services/prisma.service'; import { jurisdictionFactory } from '../../prisma/seed-helpers/jurisdiction-factory'; import { JurisdictionCreate } from '../../src/dtos/jurisdictions/jurisdiction-create.dto'; diff --git a/backend_new/test/integration/listing.e2e-spec.ts b/backend_new/test/integration/listing.e2e-spec.ts index dcac50837c..d2709aea49 100644 --- a/backend_new/test/integration/listing.e2e-spec.ts +++ b/backend_new/test/integration/listing.e2e-spec.ts @@ -1,7 +1,7 @@ import { Test, TestingModule } from '@nestjs/testing'; import { INestApplication } from '@nestjs/common'; import * as request from 'supertest'; -import { AppModule } from '../../src/app.module'; +import { AppModule } from '../../src/modules/app.module'; import { PrismaService } from '../../src/services/prisma.service'; import { jurisdictionFactory } from '../../prisma/seed-helpers/jurisdiction-factory'; import { listingFactory } from '../../prisma/seed-helpers/listing-factory'; diff --git a/backend_new/test/integration/multiselect-question.e2e-spec.ts b/backend_new/test/integration/multiselect-question.e2e-spec.ts index a2e2d5b5d2..13b0c3472a 100644 --- a/backend_new/test/integration/multiselect-question.e2e-spec.ts +++ b/backend_new/test/integration/multiselect-question.e2e-spec.ts @@ -1,7 +1,7 @@ import { Test, TestingModule } from '@nestjs/testing'; import { INestApplication } from '@nestjs/common'; import * as request from 'supertest'; -import { AppModule } from '../../src/app.module'; +import { AppModule } from '../../src/modules/app.module'; import { PrismaService } from '../../src/services/prisma.service'; import { multiselectQuestionFactory } from '../../prisma/seed-helpers/multiselect-question-factory'; import { jurisdictionFactory } from '../../prisma/seed-helpers/jurisdiction-factory'; diff --git a/backend_new/test/integration/reserved-community-type.e2e-spec.ts b/backend_new/test/integration/reserved-community-type.e2e-spec.ts index a45b4f8481..f58becae29 100644 --- a/backend_new/test/integration/reserved-community-type.e2e-spec.ts +++ b/backend_new/test/integration/reserved-community-type.e2e-spec.ts @@ -1,7 +1,7 @@ import { Test, TestingModule } from '@nestjs/testing'; import { INestApplication } from '@nestjs/common'; import * as request from 'supertest'; -import { AppModule } from '../../src/app.module'; +import { AppModule } from '../../src/modules/app.module'; import { PrismaService } from '../../src/services/prisma.service'; import { jurisdictionFactory } from '../../prisma/seed-helpers/jurisdiction-factory'; import { stringify } from 'qs'; diff --git a/backend_new/test/integration/unit-accessibility-priority-type.e2e-spec.ts b/backend_new/test/integration/unit-accessibility-priority-type.e2e-spec.ts index e1d8f92579..f67a5fe688 100644 --- a/backend_new/test/integration/unit-accessibility-priority-type.e2e-spec.ts +++ b/backend_new/test/integration/unit-accessibility-priority-type.e2e-spec.ts @@ -1,7 +1,7 @@ import { Test, TestingModule } from '@nestjs/testing'; import { INestApplication } from '@nestjs/common'; import * as request from 'supertest'; -import { AppModule } from '../../src/app.module'; +import { AppModule } from '../../src/modules/app.module'; import { PrismaService } from '../../src/services/prisma.service'; import { unitAccessibilityPriorityTypeFactorySingle } from '../../prisma/seed-helpers/unit-accessibility-priority-type-factory'; import { UnitAccessibilityPriorityTypeCreate } from '../../src/dtos/unit-accessibility-priority-types/unit-accessibility-priority-type-create.dto'; diff --git a/backend_new/test/integration/unit-rent-type.e2e-spec.ts b/backend_new/test/integration/unit-rent-type.e2e-spec.ts index f84e2cdbdf..759da3830c 100644 --- a/backend_new/test/integration/unit-rent-type.e2e-spec.ts +++ b/backend_new/test/integration/unit-rent-type.e2e-spec.ts @@ -1,7 +1,7 @@ import { Test, TestingModule } from '@nestjs/testing'; import { INestApplication } from '@nestjs/common'; import * as request from 'supertest'; -import { AppModule } from '../../src/app.module'; +import { AppModule } from '../../src/modules/app.module'; import { PrismaService } from '../../src/services/prisma.service'; import { unitRentTypeFactory } from '../../prisma/seed-helpers/unit-rent-type-factory'; import { UnitRentTypeCreate } from '../../src/dtos/unit-rent-types/unit-rent-type-create.dto'; diff --git a/backend_new/test/integration/unit-type.e2e-spec.ts b/backend_new/test/integration/unit-type.e2e-spec.ts index 8685460477..8096d56a33 100644 --- a/backend_new/test/integration/unit-type.e2e-spec.ts +++ b/backend_new/test/integration/unit-type.e2e-spec.ts @@ -1,7 +1,7 @@ import { Test, TestingModule } from '@nestjs/testing'; import { INestApplication } from '@nestjs/common'; import * as request from 'supertest'; -import { AppModule } from '../../src/app.module'; +import { AppModule } from '../../src/modules/app.module'; import { PrismaService } from '../../src/services/prisma.service'; import { unitTypeFactoryAll, diff --git a/backend_new/test/unit/services/app.service.spec.ts b/backend_new/test/unit/services/app.service.spec.ts new file mode 100644 index 0000000000..98197eb3e2 --- /dev/null +++ b/backend_new/test/unit/services/app.service.spec.ts @@ -0,0 +1,24 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { AppService } from '../../../src/services/app.service'; +import { PrismaService } from '../../../src/services/prisma.service'; + +describe('Testing app service', () => { + let service: AppService; + let prisma: PrismaService; + beforeAll(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [AppService, PrismaService], + }).compile(); + + service = module.get(AppService); + prisma = module.get(PrismaService); + }); + + it('should return a successDTO with success true', async () => { + prisma.$queryRaw = jest.fn().mockResolvedValue(1); + expect(await service.healthCheck()).toEqual({ + success: true, + }); + expect(prisma.$queryRaw).toHaveBeenCalled(); + }); +}); diff --git a/backend_new/types/src/backend-swagger.ts b/backend_new/types/src/backend-swagger.ts index 65f0b8a0d1..e766b5a9bd 100644 --- a/backend_new/types/src/backend-swagger.ts +++ b/backend_new/types/src/backend-swagger.ts @@ -102,6 +102,28 @@ export class PagedResult implements IPagedResult { // customer definition // empty +export class RootService { + /** + * Health check endpoint + */ + healthCheck(options: IRequestOptions = {}): Promise { + return new Promise((resolve, reject) => { + let url = basePath + '/'; + + const configs: IRequestConfig = getConfigs( + 'get', + 'application/json', + url, + options, + ); + + /** 适配ios13getèŻ·æ±‚äžć…èźžćžŠbody */ + + axios(configs, resolve, reject); + }); + } +} + export class ListingsService { /** * Get a paginated set of listings @@ -1183,6 +1205,11 @@ export class MultiselectQuestionsService { } } +export interface SuccessDTO { + /** */ + success: boolean; +} + export interface ListingsQueryParams { /** */ page?: number; @@ -1914,11 +1941,6 @@ export interface AmiChart { jurisdictions: IdDTO; } -export interface SuccessDTO { - /** */ - success: boolean; -} - export interface ReservedCommunityTypeCreate { /** */ name: string; From a7732b37bbe36b1fcdc73b6b54f4f0cbceeb30d5 Mon Sep 17 00:00:00 2001 From: Yazeed Loonat Date: Mon, 7 Aug 2023 10:51:58 -0700 Subject: [PATCH 16/57] feat: application get endpoints (#3494) --- backend_new/package.json | 14 +- backend_new/prisma/seed-dev.ts | 5 + .../seed-helpers/application-factory.ts | 69 + .../prisma/seed-helpers/listing-factory.ts | 6 + .../prisma/seed-helpers/unit-type-factory.ts | 27 +- backend_new/prisma/seed-staging.ts | 50 +- .../src/controllers/application.controller.ts | 48 + .../search-string-length-check.decorator.ts | 28 + .../src/dtos/addresses/address-get.dto.ts | 8 +- .../application-method-get.dto.ts | 12 +- .../dtos/applications/accessibility.dto.ts | 22 + .../applications/alternate-contact.dto.ts | 65 + .../src/dtos/applications/applicant.dto.ts | 104 + ...ication-multiselect-question-option.dto.ts | 90 + .../application-multiselect-question.dto.ts | 29 + .../application-query-params.dto.ts | 101 + .../src/dtos/applications/application.dto.ts | 244 +++ .../src/dtos/applications/demographic.dto.ts | 38 + .../dtos/applications/household-member.dto.ts | 85 + .../applications/paginated-application.dto.ts | 6 + .../dtos/jurisdictions/jurisdiction.dto.ts | 10 +- .../src/dtos/listings/listing-event.dto.ts | 6 +- .../src/dtos/listings/listing-feature.dto.ts | 30 +- .../src/dtos/listings/listing-get.dto.ts | 118 +- .../src/dtos/listings/listing-image.dto.ts | 2 +- .../listing-multiselect-question.dto.ts | 2 +- .../src/dtos/listings/listing-utility.dto.ts | 16 +- .../multiselect-option.dto.ts | 8 +- .../multiselect-question-get.dto.ts | 10 +- ...on-get.dto.ts => paper-application.dto.ts} | 0 .../reserved-community-type.dto.ts | 2 +- backend_new/src/dtos/units/unit-get.dto.ts | 36 +- .../src/dtos/units/unit-summary-get.dto.ts | 2 +- .../src/dtos/units/units-summery-get.dto.ts | 30 +- .../src/enums/applications/order-by-enum.ts | 6 + .../src/enums/shared/input-type-enum.ts | 6 + backend_new/src/modules/app.module.ts | 3 + backend_new/src/modules/application.module.ts | 12 + .../src/services/application.service.ts | 262 +++ backend_new/src/services/prisma.service.ts | 8 +- .../src/utilities/applications-utilities.ts | 5 + .../src/utilities/build-pagination-meta.ts | 24 + .../test/integration/ami-chart.e2e-spec.ts | 5 + backend_new/test/integration/app.e2e-spec.ts | 4 + .../test/integration/application.e2e-spec.ts | 161 ++ .../test/integration/jurisdiction.e2e-spec.ts | 5 + .../test/integration/listing.e2e-spec.ts | 5 + .../multiselect-question.e2e-spec.ts | 5 + .../reserved-community-type.e2e-spec.ts | 5 + ...it-accessibility-priority-type.e2e-spec.ts | 5 + .../integration/unit-rent-type.e2e-spec.ts | 5 + .../test/integration/unit-type.e2e-spec.ts | 5 + backend_new/test/jest-e2e.config.js | 12 +- backend_new/test/jest.config.js | 12 +- .../unit/services/application.service.spec.ts | 410 ++++ .../services/unit-rent-type.service.spec.ts | 5 +- backend_new/types/src/backend-swagger.ts | 374 ++++ backend_new/yarn.lock | 1677 +++++++++-------- 58 files changed, 3386 insertions(+), 958 deletions(-) create mode 100644 backend_new/prisma/seed-helpers/application-factory.ts create mode 100644 backend_new/src/controllers/application.controller.ts create mode 100644 backend_new/src/decorators/search-string-length-check.decorator.ts create mode 100644 backend_new/src/dtos/applications/accessibility.dto.ts create mode 100644 backend_new/src/dtos/applications/alternate-contact.dto.ts create mode 100644 backend_new/src/dtos/applications/applicant.dto.ts create mode 100644 backend_new/src/dtos/applications/application-multiselect-question-option.dto.ts create mode 100644 backend_new/src/dtos/applications/application-multiselect-question.dto.ts create mode 100644 backend_new/src/dtos/applications/application-query-params.dto.ts create mode 100644 backend_new/src/dtos/applications/application.dto.ts create mode 100644 backend_new/src/dtos/applications/demographic.dto.ts create mode 100644 backend_new/src/dtos/applications/household-member.dto.ts create mode 100644 backend_new/src/dtos/applications/paginated-application.dto.ts rename backend_new/src/dtos/paper-applications/{paper-application-get.dto.ts => paper-application.dto.ts} (100%) create mode 100644 backend_new/src/enums/applications/order-by-enum.ts create mode 100644 backend_new/src/enums/shared/input-type-enum.ts create mode 100644 backend_new/src/modules/application.module.ts create mode 100644 backend_new/src/services/application.service.ts create mode 100644 backend_new/src/utilities/applications-utilities.ts create mode 100644 backend_new/src/utilities/build-pagination-meta.ts create mode 100644 backend_new/test/integration/application.e2e-spec.ts create mode 100644 backend_new/test/unit/services/application.service.spec.ts diff --git a/backend_new/package.json b/backend_new/package.json index 5911749958..39755f299c 100644 --- a/backend_new/package.json +++ b/backend_new/package.json @@ -34,11 +34,11 @@ "@nestjs/core": "^8.0.0", "@nestjs/platform-express": "^8.0.0", "@nestjs/swagger": "^6.3.0", - "@prisma/client": "^4.14.0", + "@prisma/client": "^5.0.0", "class-validator": "^0.14.0", "class-transformer": "^0.5.1", "lodash": "^4.17.21", - "prisma": "^4.15.0", + "prisma": "^5.0.0", "qs": "^6.11.2", "reflect-metadata": "^0.1.13", "rimraf": "^3.0.2", @@ -50,7 +50,7 @@ "@nestjs/schematics": "^8.0.0", "@nestjs/testing": "^8.0.0", "@types/express": "^4.17.13", - "@types/jest": "27.4.1", + "@types/jest": "^29.5.3", "@types/node": "^18.7.14", "@types/supertest": "^2.0.11", "@typescript-eslint/eslint-plugin": "^5.0.0", @@ -59,16 +59,16 @@ "eslint": "^8.0.1", "eslint-config-prettier": "^8.3.0", "eslint-plugin-prettier": "^4.0.0", - "jest": "^27.2.5", + "jest": "^29.6.2", "prettier": "^2.3.2", "source-map-support": "^0.5.20", "supertest": "^6.1.3", - "ts-jest": "^27.0.3", + "ts-jest": "^29.1.1", "ts-loader": "^9.2.3", "ts-node": "^10.0.0", "tsconfig-paths": "^3.10.1", - "typescript": "^4.3.5", - "jest-environment-jsdom": "^27.2.5" + "typescript": "^5.1.6", + "jest-environment-jsdom": "^29.6.2" }, "jest": { "moduleFileExtensions": [ diff --git a/backend_new/prisma/seed-dev.ts b/backend_new/prisma/seed-dev.ts index 2003c66893..f0789cbd2b 100644 --- a/backend_new/prisma/seed-dev.ts +++ b/backend_new/prisma/seed-dev.ts @@ -11,6 +11,7 @@ import { listingFactory } from './seed-helpers/listing-factory'; import { unitTypeFactoryAll } from './seed-helpers/unit-type-factory'; import { randomName } from './seed-helpers/word-generator'; import { randomInt } from 'node:crypto'; +import { applicationFactory } from './seed-helpers/application-factory'; const listingStatusEnumArray = Object.values(ListingsStatusEnum); @@ -60,6 +61,10 @@ export const devSeeding = async (prismaClient: PrismaClient) => { status: listingStatusEnumArray[randomInt(listingStatusEnumArray.length)], multiselectQuestions: index > 0 ? multiselectQuestions.slice(0, index - 1) : [], + applications: + index > 1 + ? [...new Array(index)].map(() => applicationFactory()) + : undefined, }); await prismaClient.listings.create({ data: listing, diff --git a/backend_new/prisma/seed-helpers/application-factory.ts b/backend_new/prisma/seed-helpers/application-factory.ts new file mode 100644 index 0000000000..4339304cde --- /dev/null +++ b/backend_new/prisma/seed-helpers/application-factory.ts @@ -0,0 +1,69 @@ +import { + Prisma, + IncomePeriodEnum, + ApplicationStatusEnum, + ApplicationSubmissionTypeEnum, + YesNoEnum, +} from '@prisma/client'; +import { randomInt } from 'crypto'; +import { generateConfirmationCode } from '../../src/utilities/applications-utilities'; +import { addressFactory } from './address-factory'; +import { randomNoun } from './word-generator'; + +export const applicationFactory = (optionalParams?: { + househouldSize?: number; + unitTypeId?: string; + applicant?: Prisma.ApplicantCreateWithoutApplicationsInput; + overrides?: Prisma.ApplicationsCreateInput; +}): Prisma.ApplicationsCreateInput => { + let preferredUnitTypes: Prisma.UnitTypesCreateNestedManyWithoutApplicationsInput; + if (optionalParams?.unitTypeId) { + preferredUnitTypes = { + connect: [ + { + id: optionalParams.unitTypeId, + }, + ], + }; + } + return { + confirmationCode: generateConfirmationCode(), + applicant: { create: applicantFactory(optionalParams?.applicant) }, + appUrl: '', + status: ApplicationStatusEnum.submitted, + submissionType: ApplicationSubmissionTypeEnum.electronical, + householdSize: optionalParams?.househouldSize ?? 1, + income: '40000', + incomePeriod: IncomePeriodEnum.perYear, + preferences: {}, + preferredUnitTypes, + ...optionalParams?.overrides, + }; +}; + +export const applicantFactory = ( + overrides?: Prisma.ApplicantCreateWithoutApplicationsInput, +): Prisma.ApplicantCreateWithoutApplicationsInput => { + const firstName = randomNoun(); + const lastName = randomNoun(); + return { + firstName: firstName, + lastName: lastName, + emailAddress: `${firstName}.${lastName}@example.com`, + noEmail: false, + phoneNumber: '(123) 123-1231', + phoneNumberType: 'home', + noPhone: false, + workInRegion: YesNoEnum.no, + birthDay: `${randomInt(31) + 1}`, // no zeros + birthMonth: `${randomInt(12) + 1}`, // no zeros + birthYear: `${randomInt(80) + 1930}`, + applicantAddress: { + create: addressFactory(), + }, + applicantWorkAddress: { + create: addressFactory(), + }, + ...overrides, + }; +}; diff --git a/backend_new/prisma/seed-helpers/listing-factory.ts b/backend_new/prisma/seed-helpers/listing-factory.ts index 9353814f46..f149f7468d 100644 --- a/backend_new/prisma/seed-helpers/listing-factory.ts +++ b/backend_new/prisma/seed-helpers/listing-factory.ts @@ -23,6 +23,7 @@ export const listingFactory = async ( includeBuildingFeatures?: boolean; includeEligibilityRules?: boolean; multiselectQuestions?: Partial[]; + applications?: Prisma.ApplicationsCreateInput[]; }, ): Promise => { const previousListing = optionalParams?.listing || {}; @@ -68,6 +69,11 @@ export const listingFactory = async ( })), } : undefined, + applications: optionalParams?.applications + ? { + create: optionalParams.applications, + } + : undefined, ...featuresAndUtilites(), ...buildingFeatures(optionalParams?.includeBuildingFeatures), ...additionalEligibilityRules(optionalParams?.includeEligibilityRules), diff --git a/backend_new/prisma/seed-helpers/unit-type-factory.ts b/backend_new/prisma/seed-helpers/unit-type-factory.ts index 90c898b669..443ade292d 100644 --- a/backend_new/prisma/seed-helpers/unit-type-factory.ts +++ b/backend_new/prisma/seed-helpers/unit-type-factory.ts @@ -17,17 +17,22 @@ export const unitTypeFactorySingle = async ( return unitType; }; -export const unitTypeFactoryAll = async (prismaClient: PrismaClient) => { - return Promise.all( - Object.values(UnitTypeEnum).map(async (value) => { - return await prismaClient.unitTypes.create({ - data: { - name: value, - numBedrooms: unitTypeMapping[value], - }, - }); - }), - ); +// All unit types should only be created once. This function checks if they have been created +// before putting all types in the database +export const unitTypeFactoryAll = async ( + prismaClient: PrismaClient, +): Promise => { + const all = await prismaClient.unitTypes.findMany({}); + const unitTypes = Object.values(UnitTypeEnum); + if (all.length !== unitTypes.length) { + await prismaClient.unitTypes.createMany({ + data: Object.values(UnitTypeEnum).map((value) => ({ + name: value, + numBedrooms: unitTypeMapping[value], + })), + }); + } + return await prismaClient.unitTypes.findMany({}); }; export const unitTypeMapping = { diff --git a/backend_new/prisma/seed-staging.ts b/backend_new/prisma/seed-staging.ts index 6cf14ecb4c..92db84fbf7 100644 --- a/backend_new/prisma/seed-staging.ts +++ b/backend_new/prisma/seed-staging.ts @@ -1,7 +1,9 @@ import { ApplicationAddressTypeEnum, ListingsStatusEnum, + MultiselectQuestions, MultiselectQuestionsApplicationSectionEnum, + Prisma, PrismaClient, ReviewOrderTypeEnum, } from '@prisma/client'; @@ -18,6 +20,7 @@ import { washingtonMonument, whiteHouse, } from './seed-helpers/address-factory'; +import { applicationFactory } from './seed-helpers/application-factory'; export const stagingSeed = async ( prismaClient: PrismaClient, @@ -183,6 +186,7 @@ export const stagingSeed = async ( }, ], multiselectQuestions: [multiselectQuestion1, multiselectQuestion2], + applications: [applicationFactory(), applicationFactory()], }, { listing: { @@ -296,6 +300,17 @@ export const stagingSeed = async ( }, ], multiselectQuestions: [multiselectQuestion1], + // has applications that are the same email + applications: [ + applicationFactory({ + applicant: { emailAddress: 'user1@example.com' }, + }), + applicationFactory({ + applicant: { emailAddress: 'user1@example.com' }, + }), + applicationFactory(), + applicationFactory(), + ], }, { listing: { @@ -678,16 +693,27 @@ export const stagingSeed = async ( }, }, }, - ].map(async (value, index) => { - const listing = await listingFactory(jurisdiction.id, prismaClient, { - amiChart: amiChart, - numberOfUnits: index, - listing: value.listing, - units: value.units, - multiselectQuestions: value.multiselectQuestions, - }); - await prismaClient.listings.create({ - data: listing, - }); - }); + ].map( + async ( + value: { + listing: Prisma.ListingsCreateInput; + units?: Prisma.UnitsCreateWithoutListingsInput[]; + multiselectQuestions?: Partial[]; + applications?: Prisma.ApplicationsCreateInput[]; + }, + index, + ) => { + const listing = await listingFactory(jurisdiction.id, prismaClient, { + amiChart: amiChart, + numberOfUnits: index, + listing: value.listing, + units: value.units, + multiselectQuestions: value.multiselectQuestions, + applications: value.applications, + }); + await prismaClient.listings.create({ + data: listing, + }); + }, + ); }; diff --git a/backend_new/src/controllers/application.controller.ts b/backend_new/src/controllers/application.controller.ts new file mode 100644 index 0000000000..04b74125be --- /dev/null +++ b/backend_new/src/controllers/application.controller.ts @@ -0,0 +1,48 @@ +import { + Controller, + Get, + Param, + Query, + UsePipes, + ValidationPipe, +} from '@nestjs/common'; +import { + ApiExtraModels, + ApiOkResponse, + ApiOperation, + ApiTags, +} from '@nestjs/swagger'; +import { ApplicationService } from '../services/application.service'; +import { Application } from '../dtos/applications/application.dto'; +import { defaultValidationPipeOptions } from '../utilities/default-validation-pipe-options'; +import { IdDTO } from '../dtos/shared/id.dto'; +import { PaginatedApplicationDto } from '../dtos/applications/paginated-application.dto'; +import { ApplicationQueryParams } from '../dtos/applications/application-query-params.dto'; + +@Controller('applications') +@ApiTags('applications') +@UsePipes(new ValidationPipe(defaultValidationPipeOptions)) +@ApiExtraModels(IdDTO) +export class ApplicationController { + constructor(private readonly applicationService: ApplicationService) {} + + @Get() + @ApiOperation({ + summary: 'Get a paginated set of applications', + operationId: 'list', + }) + @ApiOkResponse({ type: PaginatedApplicationDto }) + async list(@Query() queryParams: ApplicationQueryParams) { + return await this.applicationService.list(queryParams); + } + + @Get(`:applicationId`) + @ApiOperation({ + summary: 'Get application by id', + operationId: 'retrieve', + }) + @ApiOkResponse({ type: Application }) + async retrieve(@Param('applicationId') applicationId: string) { + return this.applicationService.findOne(applicationId); + } +} diff --git a/backend_new/src/decorators/search-string-length-check.decorator.ts b/backend_new/src/decorators/search-string-length-check.decorator.ts new file mode 100644 index 0000000000..66427df87d --- /dev/null +++ b/backend_new/src/decorators/search-string-length-check.decorator.ts @@ -0,0 +1,28 @@ +import { + registerDecorator, + ValidationOptions, + ValidatorConstraint, + ValidatorConstraintInterface, +} from 'class-validator'; + +export function SearchStringLengthCheck( + property: string, + validationOptions?: ValidationOptions, +) { + return (object: unknown, propertyName: string) => { + registerDecorator({ + target: object.constructor, + propertyName, + options: validationOptions, + constraints: [property], + validator: LengthConstraint, + }); + }; +} + +@ValidatorConstraint({ name: 'SearchStringLengthCheck' }) +export class LengthConstraint implements ValidatorConstraintInterface { + validate(value: string) { + return value.length >= 3 || value.length === 0; + } +} diff --git a/backend_new/src/dtos/addresses/address-get.dto.ts b/backend_new/src/dtos/addresses/address-get.dto.ts index 3cba4ae2e9..3a3fbc8862 100644 --- a/backend_new/src/dtos/addresses/address-get.dto.ts +++ b/backend_new/src/dtos/addresses/address-get.dto.ts @@ -18,7 +18,7 @@ export class Address extends AbstractDTO { @Expose() @IsString({ groups: [ValidationsGroupsEnum.default] }) @MaxLength(64, { groups: [ValidationsGroupsEnum.default] }) - county?: string | null; + county?: string; @Expose() @IsString({ groups: [ValidationsGroupsEnum.default] }) @@ -35,7 +35,7 @@ export class Address extends AbstractDTO { @Expose() @IsString({ groups: [ValidationsGroupsEnum.default] }) @MaxLength(64, { groups: [ValidationsGroupsEnum.default] }) - street2?: string | null; + street2?: string; @Expose() @IsString({ groups: [ValidationsGroupsEnum.default] }) @@ -46,10 +46,10 @@ export class Address extends AbstractDTO { @Expose() @IsNumber({}, { groups: [ValidationsGroupsEnum.default] }) @Type(() => Number) - latitude?: number | null; + latitude?: number; @Expose() @IsNumber({}, { groups: [ValidationsGroupsEnum.default] }) @Type(() => Number) - longitude?: number | null; + longitude?: number; } diff --git a/backend_new/src/dtos/application-methods/application-method-get.dto.ts b/backend_new/src/dtos/application-methods/application-method-get.dto.ts index 0ab87e14ae..b9fd8fcdc1 100644 --- a/backend_new/src/dtos/application-methods/application-method-get.dto.ts +++ b/backend_new/src/dtos/application-methods/application-method-get.dto.ts @@ -11,7 +11,7 @@ import { ValidationsGroupsEnum } from '../../enums/shared/validation-groups-enum import { AbstractDTO } from '../shared/abstract.dto'; import { ApiProperty } from '@nestjs/swagger'; import { ApplicationMethodsTypeEnum } from '@prisma/client'; -import { PaperApplication } from '../paper-applications/paper-application-get.dto'; +import { PaperApplication } from '../paper-applications/paper-application.dto'; export class ApplicationMethod extends AbstractDTO { @Expose() @@ -29,24 +29,24 @@ export class ApplicationMethod extends AbstractDTO { @Expose() @IsString({ groups: [ValidationsGroupsEnum.default] }) @MaxLength(256, { groups: [ValidationsGroupsEnum.default] }) - label?: string | null; + label?: string; @Expose() @IsString({ groups: [ValidationsGroupsEnum.default] }) @MaxLength(4096, { groups: [ValidationsGroupsEnum.default] }) - externalReference?: string | null; + externalReference?: string; @Expose() @IsBoolean({ groups: [ValidationsGroupsEnum.default] }) - acceptsPostmarkedApplications?: boolean | null; + acceptsPostmarkedApplications?: boolean; @Expose() @IsString({ groups: [ValidationsGroupsEnum.default] }) @MaxLength(16, { groups: [ValidationsGroupsEnum.default] }) - phoneNumber?: string | null; + phoneNumber?: string; @Expose() @ValidateNested({ groups: [ValidationsGroupsEnum.default], each: true }) @Type(() => PaperApplication) - paperApplications?: PaperApplication[] | null; + paperApplications?: PaperApplication[]; } diff --git a/backend_new/src/dtos/applications/accessibility.dto.ts b/backend_new/src/dtos/applications/accessibility.dto.ts new file mode 100644 index 0000000000..0ecd98b7fe --- /dev/null +++ b/backend_new/src/dtos/applications/accessibility.dto.ts @@ -0,0 +1,22 @@ +import { Expose } from 'class-transformer'; +import { IsBoolean } from 'class-validator'; +import { ValidationsGroupsEnum } from '../../enums/shared/validation-groups-enum'; +import { AbstractDTO } from '../shared/abstract.dto'; +import { ApiProperty } from '@nestjs/swagger'; + +export class Accessibility extends AbstractDTO { + @Expose() + @IsBoolean({ groups: [ValidationsGroupsEnum.default] }) + @ApiProperty() + mobility?: boolean; + + @Expose() + @IsBoolean({ groups: [ValidationsGroupsEnum.default] }) + @ApiProperty() + vision?: boolean; + + @Expose() + @IsBoolean({ groups: [ValidationsGroupsEnum.default] }) + @ApiProperty() + hearing?: boolean; +} diff --git a/backend_new/src/dtos/applications/alternate-contact.dto.ts b/backend_new/src/dtos/applications/alternate-contact.dto.ts new file mode 100644 index 0000000000..b13bcb947d --- /dev/null +++ b/backend_new/src/dtos/applications/alternate-contact.dto.ts @@ -0,0 +1,65 @@ +import { Expose, Type } from 'class-transformer'; +import { + IsDefined, + IsEmail, + IsString, + MaxLength, + ValidateNested, +} from 'class-validator'; +import { ValidationsGroupsEnum } from '../../enums/shared/validation-groups-enum'; +import { AbstractDTO } from '../shared/abstract.dto'; +import { ApiProperty } from '@nestjs/swagger'; +import { Address } from '../addresses/address-get.dto'; +import { EnforceLowerCase } from '../../decorators/enforce-lower-case.decorator'; + +export class AlternateContact extends AbstractDTO { + @Expose() + @IsString({ groups: [ValidationsGroupsEnum.default] }) + @MaxLength(16, { groups: [ValidationsGroupsEnum.default] }) + @ApiProperty() + type?: string; + + @Expose() + @IsString({ groups: [ValidationsGroupsEnum.default] }) + @MaxLength(64, { groups: [ValidationsGroupsEnum.default] }) + @ApiProperty() + otherType?: string; + + @Expose() + @IsString({ groups: [ValidationsGroupsEnum.default] }) + @MaxLength(64, { groups: [ValidationsGroupsEnum.default] }) + @ApiProperty() + firstName?: string; + + @Expose() + @IsString({ groups: [ValidationsGroupsEnum.default] }) + @MaxLength(64, { groups: [ValidationsGroupsEnum.default] }) + @ApiProperty() + lastName?: string; + + @Expose() + @IsString({ groups: [ValidationsGroupsEnum.default] }) + @MaxLength(128, { groups: [ValidationsGroupsEnum.default] }) + @ApiProperty() + agency?: string; + + @Expose() + @IsString({ groups: [ValidationsGroupsEnum.default] }) + @MaxLength(16, { groups: [ValidationsGroupsEnum.default] }) + @ApiProperty() + phoneNumber?: string; + + @Expose() + @IsString({ groups: [ValidationsGroupsEnum.default] }) + @IsEmail({}, { groups: [ValidationsGroupsEnum.default] }) + @EnforceLowerCase() + @ApiProperty() + emailAddress?: string; + + @Expose() + @IsDefined({ groups: [ValidationsGroupsEnum.default] }) + @ValidateNested({ groups: [ValidationsGroupsEnum.default] }) + @Type(() => Address) + @ApiProperty() + address: Address; +} diff --git a/backend_new/src/dtos/applications/applicant.dto.ts b/backend_new/src/dtos/applications/applicant.dto.ts new file mode 100644 index 0000000000..e396a4afae --- /dev/null +++ b/backend_new/src/dtos/applications/applicant.dto.ts @@ -0,0 +1,104 @@ +import { Expose, Type } from 'class-transformer'; +import { + IsBoolean, + IsDefined, + IsEmail, + IsEnum, + IsString, + MaxLength, + MinLength, + ValidateNested, +} from 'class-validator'; +import { ValidationsGroupsEnum } from '../../enums/shared/validation-groups-enum'; +import { AbstractDTO } from '../shared/abstract.dto'; +import { ApiProperty } from '@nestjs/swagger'; +import { YesNoEnum } from '@prisma/client'; +import { Address } from '../addresses/address-get.dto'; +import { EnforceLowerCase } from '../../decorators/enforce-lower-case.decorator'; + +export class Applicant extends AbstractDTO { + @Expose() + @IsString({ groups: [ValidationsGroupsEnum.default] }) + @MaxLength(64, { groups: [ValidationsGroupsEnum.default] }) + @MinLength(1, { groups: [ValidationsGroupsEnum.default] }) + @ApiProperty() + firstName?: string; + + @Expose() + @IsString({ groups: [ValidationsGroupsEnum.default] }) + @MaxLength(64, { groups: [ValidationsGroupsEnum.default] }) + @ApiProperty() + middleName?: string; + + @Expose() + @IsString({ groups: [ValidationsGroupsEnum.default] }) + @MaxLength(64, { groups: [ValidationsGroupsEnum.default] }) + @MinLength(1, { groups: [ValidationsGroupsEnum.default] }) + @ApiProperty() + lastName?: string; + + @Expose() + @IsString({ groups: [ValidationsGroupsEnum.default] }) + @MaxLength(8, { groups: [ValidationsGroupsEnum.default] }) + @ApiProperty() + birthMonth?: string; + + @Expose() + @IsString({ groups: [ValidationsGroupsEnum.default] }) + @MaxLength(8, { groups: [ValidationsGroupsEnum.default] }) + @ApiProperty() + birthDay?: string; + + @Expose() + @IsString({ groups: [ValidationsGroupsEnum.default] }) + @MaxLength(8, { groups: [ValidationsGroupsEnum.default] }) + @ApiProperty() + birthYear?: string; + + @Expose() + @IsEmail({}, { groups: [ValidationsGroupsEnum.default] }) + @EnforceLowerCase() + @ApiProperty() + emailAddress?: string; + + @Expose() + @IsBoolean({ groups: [ValidationsGroupsEnum.default] }) + @ApiProperty() + noEmail?: boolean; + + @Expose() + @IsString({ groups: [ValidationsGroupsEnum.default] }) + @MaxLength(16, { groups: [ValidationsGroupsEnum.default] }) + @ApiProperty() + phoneNumber?: string; + + @Expose() + @IsString({ groups: [ValidationsGroupsEnum.default] }) + @MaxLength(16, { groups: [ValidationsGroupsEnum.default] }) + @ApiProperty() + phoneNumberType?: string; + + @Expose() + @IsBoolean({ groups: [ValidationsGroupsEnum.default] }) + @ApiProperty() + noPhone?: boolean; + + @Expose() + @IsEnum(YesNoEnum, { groups: [ValidationsGroupsEnum.default] }) + @ApiProperty({ enum: YesNoEnum, enumName: 'YesNoEnum' }) + workInRegion?: YesNoEnum; + + @Expose() + @IsDefined({ groups: [ValidationsGroupsEnum.default] }) + @ValidateNested({ groups: [ValidationsGroupsEnum.default] }) + @Type(() => Address) + @ApiProperty() + applicantWorkAddress: Address; + + @Expose() + @IsDefined({ groups: [ValidationsGroupsEnum.default] }) + @ValidateNested({ groups: [ValidationsGroupsEnum.default] }) + @Type(() => Address) + @ApiProperty() + applicantAddress: Address; +} diff --git a/backend_new/src/dtos/applications/application-multiselect-question-option.dto.ts b/backend_new/src/dtos/applications/application-multiselect-question-option.dto.ts new file mode 100644 index 0000000000..63ddbe453f --- /dev/null +++ b/backend_new/src/dtos/applications/application-multiselect-question-option.dto.ts @@ -0,0 +1,90 @@ +import { ApiProperty, getSchemaPath } from '@nestjs/swagger'; +import { Expose, Type } from 'class-transformer'; +import { + ArrayMaxSize, + IsBoolean, + IsDefined, + IsEnum, + IsString, + MaxLength, + ValidateNested, +} from 'class-validator'; +import { InputType } from '../../enums/shared/input-type-enum'; +import { ValidationsGroupsEnum } from '../../enums/shared/validation-groups-enum'; +import { Address } from '../addresses/address-get.dto'; + +class FormMetadataExtraData { + @Expose() + @IsString({ groups: [ValidationsGroupsEnum.default] }) + @IsEnum(InputType, { groups: [ValidationsGroupsEnum.default] }) + @ApiProperty({ enum: InputType, enumName: 'InputType' }) + type: InputType; + + @Expose() + @IsString({ groups: [ValidationsGroupsEnum.default] }) + @ApiProperty() + key: string; +} + +class AddressInput extends FormMetadataExtraData { + @Expose() + @IsDefined({ groups: [ValidationsGroupsEnum.default] }) + @ValidateNested({ groups: [ValidationsGroupsEnum.default] }) + @Type(() => Address) + @ApiProperty() + value: Address; +} + +class BooleanInput extends FormMetadataExtraData { + @Expose() + @IsBoolean({ groups: [ValidationsGroupsEnum.default] }) + @ApiProperty() + value: boolean; +} + +class TextInput extends FormMetadataExtraData { + @Expose() + @IsString({ groups: [ValidationsGroupsEnum.default] }) + @MaxLength(4096, { groups: [ValidationsGroupsEnum.default] }) + @ApiProperty() + value: string; +} + +export class ApplicationMultiselectQuestionOption { + @Expose() + @IsString({ groups: [ValidationsGroupsEnum.default] }) + @ApiProperty() + key: string; + + @Expose() + @IsBoolean({ groups: [ValidationsGroupsEnum.default] }) + @ApiProperty() + checked: boolean; + + @Expose() + @ApiProperty({ + type: 'array', + required: false, + items: { + oneOf: [ + { $ref: getSchemaPath(BooleanInput) }, + { $ref: getSchemaPath(TextInput) }, + { $ref: getSchemaPath(AddressInput) }, + ], + }, + }) + @ArrayMaxSize(64, { groups: [ValidationsGroupsEnum.default] }) + @ValidateNested({ groups: [ValidationsGroupsEnum.default], each: true }) + @Type(() => FormMetadataExtraData, { + keepDiscriminatorProperty: true, + discriminator: { + property: 'type', + subTypes: [ + { value: BooleanInput, name: InputType.boolean }, + { value: TextInput, name: InputType.text }, + { value: AddressInput, name: InputType.address }, + ], + }, + }) + extraData: Array; +} diff --git a/backend_new/src/dtos/applications/application-multiselect-question.dto.ts b/backend_new/src/dtos/applications/application-multiselect-question.dto.ts new file mode 100644 index 0000000000..4e3aaf8f09 --- /dev/null +++ b/backend_new/src/dtos/applications/application-multiselect-question.dto.ts @@ -0,0 +1,29 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { Expose, Type } from 'class-transformer'; +import { + ArrayMaxSize, + IsBoolean, + IsString, + ValidateNested, +} from 'class-validator'; +import { ValidationsGroupsEnum } from '../../enums/shared/validation-groups-enum'; +import { ApplicationMultiselectQuestionOption } from './application-multiselect-question-option.dto'; + +export class ApplicationMultiselectQuestion { + @Expose() + @IsString({ groups: [ValidationsGroupsEnum.default] }) + @ApiProperty() + key: string; + + @Expose() + @IsBoolean({ groups: [ValidationsGroupsEnum.default] }) + @ApiProperty() + claimed: boolean; + + @Expose() + @ArrayMaxSize(64, { groups: [ValidationsGroupsEnum.default] }) + @ValidateNested({ groups: [ValidationsGroupsEnum.default], each: true }) + @Type(() => ApplicationMultiselectQuestionOption) + @ApiProperty() + options: ApplicationMultiselectQuestionOption[]; +} diff --git a/backend_new/src/dtos/applications/application-query-params.dto.ts b/backend_new/src/dtos/applications/application-query-params.dto.ts new file mode 100644 index 0000000000..d9a5910f58 --- /dev/null +++ b/backend_new/src/dtos/applications/application-query-params.dto.ts @@ -0,0 +1,101 @@ +import { PaginationAllowsAllQueryParams } from '../shared/pagination.dto'; +import { Expose, Transform, TransformFnParams } from 'class-transformer'; +import { ApiProperty } from '@nestjs/swagger'; +import { IsBoolean, IsEnum, IsString } from 'class-validator'; +import { ValidationsGroupsEnum } from '../../enums/shared/validation-groups-enum'; +import { ApplicationOrderByKeys } from '../../enums/applications/order-by-enum'; +import { OrderByEnum } from '../../enums/shared/order-by-enum'; +import { SearchStringLengthCheck } from '../../decorators/search-string-length-check.decorator'; + +export class ApplicationQueryParams extends PaginationAllowsAllQueryParams { + @Expose() + @ApiProperty({ + type: String, + example: 'listingId', + required: false, + }) + @IsString({ groups: [ValidationsGroupsEnum.default] }) + listingId?: string; + + @Expose() + @ApiProperty({ + type: String, + example: 'search', + required: false, + }) + @IsString({ groups: [ValidationsGroupsEnum.default] }) + @SearchStringLengthCheck('search', { + message: 'Search must be at least 3 characters', + groups: [ValidationsGroupsEnum.default], + }) + search?: string; + + @Expose() + @ApiProperty({ + type: String, + example: 'userId', + required: false, + }) + @IsString({ groups: [ValidationsGroupsEnum.default] }) + userId?: string; + + @Expose() + @ApiProperty({ + enum: ApplicationOrderByKeys, + enumName: 'ApplicationOrderByKeys', + example: 'createdAt', + default: 'createdAt', + required: false, + }) + @IsString({ groups: [ValidationsGroupsEnum.default] }) + @IsEnum(ApplicationOrderByKeys, { + groups: [ValidationsGroupsEnum.default], + }) + @Transform((value: TransformFnParams) => + value?.value + ? ApplicationOrderByKeys[value.value] + ? ApplicationOrderByKeys[value.value] + : value + : ApplicationOrderByKeys.createdAt, + ) + orderBy?: ApplicationOrderByKeys; + + @Expose() + @ApiProperty({ + enum: OrderByEnum, + enumName: 'OrderByEnum', + example: 'DESC', + default: 'DESC', + required: false, + }) + @IsString({ groups: [ValidationsGroupsEnum.default] }) + @IsEnum(OrderByEnum, { + groups: [ValidationsGroupsEnum.default], + }) + @Transform((value: TransformFnParams) => + value?.value ? value.value : OrderByEnum.DESC, + ) + order?: OrderByEnum; + + @Expose() + @ApiProperty({ + type: Boolean, + example: true, + required: false, + }) + @IsBoolean({ groups: [ValidationsGroupsEnum.default] }) + @Transform( + (value: TransformFnParams) => { + switch (value?.value) { + case 'true': + return true; + case 'false': + return false; + default: + return undefined; + } + }, + { toClassOnly: true }, + ) + markedAsDuplicate?: boolean; +} diff --git a/backend_new/src/dtos/applications/application.dto.ts b/backend_new/src/dtos/applications/application.dto.ts new file mode 100644 index 0000000000..fbe6915bbe --- /dev/null +++ b/backend_new/src/dtos/applications/application.dto.ts @@ -0,0 +1,244 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { + ApplicationReviewStatusEnum, + ApplicationStatusEnum, + ApplicationSubmissionTypeEnum, + IncomePeriodEnum, + LanguagesEnum, +} from '@prisma/client'; +import { Expose, Type } from 'class-transformer'; +import { + ArrayMaxSize, + IsBoolean, + IsDate, + IsDefined, + IsEnum, + IsNumber, + IsString, + MaxLength, + ValidateNested, +} from 'class-validator'; +import { ValidationsGroupsEnum } from '../../enums/shared/validation-groups-enum'; +import { Address } from '../addresses/address-get.dto'; +import { AbstractDTO } from '../shared/abstract.dto'; +import { UnitType } from '../unit-types/unit-type.dto'; +import { Accessibility } from './accessibility.dto'; +import { AlternateContact } from './alternate-contact.dto'; +import { Applicant } from './applicant.dto'; +import { ApplicationMultiselectQuestion } from './application-multiselect-question.dto'; +import { Demographic } from './demographic.dto'; +import { HouseholdMember } from './household-member.dto'; + +export class Application extends AbstractDTO { + @Expose() + @IsDate({ groups: [ValidationsGroupsEnum.default] }) + @Type(() => Date) + @ApiProperty() + deletedAt?: Date; + + @Expose() + @IsString({ groups: [ValidationsGroupsEnum.default] }) + @MaxLength(256, { groups: [ValidationsGroupsEnum.default] }) + @ApiProperty() + appUrl?: string; + + @Expose() + @IsBoolean({ groups: [ValidationsGroupsEnum.default] }) + @ApiProperty() + additionalPhone?: boolean; + + @Expose() + @IsString({ groups: [ValidationsGroupsEnum.default] }) + @MaxLength(16, { groups: [ValidationsGroupsEnum.default] }) + @ApiProperty() + additionalPhoneNumber?: string; + + @Expose() + @IsString({ groups: [ValidationsGroupsEnum.default] }) + @MaxLength(16, { groups: [ValidationsGroupsEnum.default] }) + @ApiProperty() + additionalPhoneNumberType?: string; + + @Expose() + @IsString({ groups: [ValidationsGroupsEnum.default], each: true }) + @ArrayMaxSize(8, { groups: [ValidationsGroupsEnum.default] }) + @MaxLength(64, { groups: [ValidationsGroupsEnum.default], each: true }) + @ApiProperty() + contactPreferences: string[]; + + @Expose() + @IsNumber({}, { groups: [ValidationsGroupsEnum.default] }) + @ApiProperty() + householdSize?: number; + + @Expose() + @IsString({ groups: [ValidationsGroupsEnum.default] }) + @MaxLength(16, { groups: [ValidationsGroupsEnum.default] }) + @ApiProperty() + housingStatus?: string; + + @Expose() + @IsBoolean({ groups: [ValidationsGroupsEnum.default] }) + @ApiProperty() + sendMailToMailingAddress?: boolean; + + @Expose() + @IsBoolean({ groups: [ValidationsGroupsEnum.default] }) + @ApiProperty() + householdExpectingChanges?: boolean; + + @Expose() + @IsBoolean({ groups: [ValidationsGroupsEnum.default] }) + @ApiProperty() + householdStudent?: boolean; + + @Expose() + @IsBoolean({ groups: [ValidationsGroupsEnum.default] }) + @ApiProperty() + incomeVouchers?: boolean; + + @Expose() + @IsString({ groups: [ValidationsGroupsEnum.default] }) + @MaxLength(64, { groups: [ValidationsGroupsEnum.default] }) + @ApiProperty() + income?: string; + + @Expose() + @IsEnum(IncomePeriodEnum, { groups: [ValidationsGroupsEnum.default] }) + @ApiProperty({ enum: IncomePeriodEnum, enumName: 'IncomePeriodEnum' }) + incomePeriod?: IncomePeriodEnum; + + @Expose() + @IsEnum(ApplicationStatusEnum, { groups: [ValidationsGroupsEnum.default] }) + @ApiProperty({ + enum: ApplicationStatusEnum, + enumName: 'ApplicationStatusEnum', + }) + status: ApplicationStatusEnum; + + @Expose() + @IsEnum(LanguagesEnum, { groups: [ValidationsGroupsEnum.default] }) + @ApiProperty({ enum: LanguagesEnum, enumName: 'LanguagesEnum' }) + language?: LanguagesEnum; + + @Expose() + @IsBoolean({ groups: [ValidationsGroupsEnum.default] }) + @ApiProperty() + acceptedTerms?: boolean; + + @Expose() + @IsEnum(ApplicationSubmissionTypeEnum, { + groups: [ValidationsGroupsEnum.default], + }) + @ApiProperty({ + enum: ApplicationSubmissionTypeEnum, + enumName: 'ApplicationSubmissionTypeEnum', + }) + submissionType: ApplicationSubmissionTypeEnum; + + @Expose() + @IsDate({ groups: [ValidationsGroupsEnum.default] }) + @Type(() => Date) + @ApiProperty() + submissionDate?: Date; + + // if this field is true then the application is a confirmed duplicate + // meaning that the record in the applicaiton flagged set table has a status of duplicate + @Expose() + @IsBoolean({ groups: [ValidationsGroupsEnum.default] }) + @ApiProperty() + markedAsDuplicate: boolean; + + // This is a 'virtual field' needed for CSV export + // if this field is true then the application is a possible duplicate + // meaning there exists a record in the application_flagged_set table for this application + @Expose() + @IsBoolean({ groups: [ValidationsGroupsEnum.default] }) + @ApiProperty() + flagged?: boolean; + + @Expose() + @IsString({ groups: [ValidationsGroupsEnum.default] }) + @ApiProperty() + confirmationCode: string; + + @Expose() + @IsEnum(ApplicationReviewStatusEnum, { + groups: [ValidationsGroupsEnum.default], + }) + @ApiProperty({ + enum: ApplicationReviewStatusEnum, + enumName: 'ApplicationReviewStatusEnum', + }) + @ApiProperty() + reviewStatus?: ApplicationReviewStatusEnum; + + @Expose() + @IsDefined({ groups: [ValidationsGroupsEnum.default] }) + @ValidateNested({ groups: [ValidationsGroupsEnum.default] }) + @Type(() => Address) + @ApiProperty() + applicationsMailingAddress: Address; + + @Expose() + @IsDefined({ groups: [ValidationsGroupsEnum.default] }) + @ValidateNested({ groups: [ValidationsGroupsEnum.default] }) + @Type(() => Address) + @ApiProperty() + applicationsAlternateAddress: Address; + + @Expose() + @IsDefined({ groups: [ValidationsGroupsEnum.default] }) + @ValidateNested({ groups: [ValidationsGroupsEnum.default] }) + @Type(() => Accessibility) + @ApiProperty() + accessibility: Accessibility; + + @Expose() + @IsDefined({ groups: [ValidationsGroupsEnum.default] }) + @ValidateNested({ groups: [ValidationsGroupsEnum.default] }) + @Type(() => Demographic) + @ApiProperty() + demographics: Demographic; + + @Expose() + @ValidateNested({ groups: [ValidationsGroupsEnum.default], each: true }) + @ApiProperty() + @Type(() => UnitType) + preferredUnitTypes: UnitType[]; + + @Expose() + @ValidateNested({ groups: [ValidationsGroupsEnum.default] }) + @Type(() => Applicant) + @ApiProperty() + applicant: Applicant; + + @Expose() + @IsDefined({ groups: [ValidationsGroupsEnum.default] }) + @ValidateNested({ groups: [ValidationsGroupsEnum.default] }) + @Type(() => AlternateContact) + @ApiProperty() + alternateContact: AlternateContact; + + @Expose() + @ValidateNested({ groups: [ValidationsGroupsEnum.default], each: true }) + @ArrayMaxSize(32, { groups: [ValidationsGroupsEnum.default] }) + @Type(() => HouseholdMember) + @ApiProperty() + householdMember: HouseholdMember[]; + + @Expose() + @IsDefined({ groups: [ValidationsGroupsEnum.default] }) + @ArrayMaxSize(64, { groups: [ValidationsGroupsEnum.default] }) + @ValidateNested({ groups: [ValidationsGroupsEnum.default], each: true }) + @Type(() => ApplicationMultiselectQuestion) + @ApiProperty() + preferences: ApplicationMultiselectQuestion[]; + + @Expose() + @ArrayMaxSize(64, { groups: [ValidationsGroupsEnum.default] }) + @ValidateNested({ groups: [ValidationsGroupsEnum.default], each: true }) + @Type(() => ApplicationMultiselectQuestion) + @ApiProperty() + programs?: ApplicationMultiselectQuestion[]; +} diff --git a/backend_new/src/dtos/applications/demographic.dto.ts b/backend_new/src/dtos/applications/demographic.dto.ts new file mode 100644 index 0000000000..9fc02edd4f --- /dev/null +++ b/backend_new/src/dtos/applications/demographic.dto.ts @@ -0,0 +1,38 @@ +import { Expose } from 'class-transformer'; +import { ArrayMaxSize, IsString, MaxLength } from 'class-validator'; +import { ValidationsGroupsEnum } from '../../enums/shared/validation-groups-enum'; +import { AbstractDTO } from '../shared/abstract.dto'; +import { ApiProperty } from '@nestjs/swagger'; + +export class Demographic extends AbstractDTO { + @Expose() + @IsString({ groups: [ValidationsGroupsEnum.default] }) + @MaxLength(64, { groups: [ValidationsGroupsEnum.default] }) + @ApiProperty() + ethnicity?: string; + + @Expose() + @IsString({ groups: [ValidationsGroupsEnum.default] }) + @MaxLength(64, { groups: [ValidationsGroupsEnum.default] }) + @ApiProperty() + gender?: string; + + @Expose() + @IsString({ groups: [ValidationsGroupsEnum.default] }) + @MaxLength(64, { groups: [ValidationsGroupsEnum.default] }) + @ApiProperty() + sexualOrientation?: string; + + @Expose() + @IsString({ groups: [ValidationsGroupsEnum.default], each: true }) + @ArrayMaxSize(64, { groups: [ValidationsGroupsEnum.default] }) + @MaxLength(64, { groups: [ValidationsGroupsEnum.default], each: true }) + @ApiProperty() + howDidYouHear: string[]; + + @Expose() + @ArrayMaxSize(64, { groups: [ValidationsGroupsEnum.default] }) + @IsString({ groups: [ValidationsGroupsEnum.default], each: true }) + @ApiProperty() + race?: string[]; +} diff --git a/backend_new/src/dtos/applications/household-member.dto.ts b/backend_new/src/dtos/applications/household-member.dto.ts new file mode 100644 index 0000000000..a6b05d807d --- /dev/null +++ b/backend_new/src/dtos/applications/household-member.dto.ts @@ -0,0 +1,85 @@ +import { Expose, Type } from 'class-transformer'; +import { + IsDefined, + IsEnum, + IsNumber, + IsString, + MaxLength, + ValidateNested, +} from 'class-validator'; +import { ValidationsGroupsEnum } from '../../enums/shared/validation-groups-enum'; +import { AbstractDTO } from '../shared/abstract.dto'; +import { ApiProperty } from '@nestjs/swagger'; +import { YesNoEnum } from '@prisma/client'; +import { Address } from '../addresses/address-get.dto'; + +export class HouseholdMember extends AbstractDTO { + @Expose() + @IsNumber({}, { groups: [ValidationsGroupsEnum.default] }) + @ApiProperty() + orderId?: number; + + @Expose() + @IsString({ groups: [ValidationsGroupsEnum.default] }) + @MaxLength(64, { groups: [ValidationsGroupsEnum.default] }) + @ApiProperty() + firstName?: string; + + @Expose() + @IsString({ groups: [ValidationsGroupsEnum.default] }) + @MaxLength(64, { groups: [ValidationsGroupsEnum.default] }) + @ApiProperty() + middleName?: string; + + @Expose() + @IsString({ groups: [ValidationsGroupsEnum.default] }) + @MaxLength(64, { groups: [ValidationsGroupsEnum.default] }) + @ApiProperty() + lastName?: string; + + @Expose() + @IsString({ groups: [ValidationsGroupsEnum.default] }) + @MaxLength(8, { groups: [ValidationsGroupsEnum.default] }) + birthMonth?: string; + + @Expose() + @IsString({ groups: [ValidationsGroupsEnum.default] }) + @MaxLength(8, { groups: [ValidationsGroupsEnum.default] }) + @ApiProperty() + birthDay?: string; + + @Expose() + @IsString({ groups: [ValidationsGroupsEnum.default] }) + @MaxLength(8, { groups: [ValidationsGroupsEnum.default] }) + @ApiProperty() + birthYear?: string; + + @Expose() + @IsEnum(YesNoEnum, { groups: [ValidationsGroupsEnum.default] }) + @ApiProperty({ enum: YesNoEnum, enumName: 'YesNoEnum' }) + sameAddress?: YesNoEnum; + + @Expose() + @IsString({ groups: [ValidationsGroupsEnum.default] }) + @MaxLength(64, { groups: [ValidationsGroupsEnum.default] }) + @ApiProperty() + relationship?: string; + + @Expose() + @IsEnum(YesNoEnum, { groups: [ValidationsGroupsEnum.default] }) + @ApiProperty({ enum: YesNoEnum, enumName: 'YesNoEnum' }) + workInRegion?: YesNoEnum; + + @Expose() + @ValidateNested({ groups: [ValidationsGroupsEnum.default] }) + @Type(() => Address) + @ApiProperty() + householdMemberWorkAddress?: Address; + + @Expose() + @IsDefined({ groups: [ValidationsGroupsEnum.default] }) + @ValidateNested({ groups: [ValidationsGroupsEnum.default] }) + @Type(() => Address) + @ApiProperty() + householdMemberAddress: Address; +} diff --git a/backend_new/src/dtos/applications/paginated-application.dto.ts b/backend_new/src/dtos/applications/paginated-application.dto.ts new file mode 100644 index 0000000000..50dbe7304c --- /dev/null +++ b/backend_new/src/dtos/applications/paginated-application.dto.ts @@ -0,0 +1,6 @@ +import { PaginationFactory } from '../shared/pagination.dto'; +import { Application } from './application.dto'; + +export class PaginatedApplicationDto extends PaginationFactory( + Application, +) {} diff --git a/backend_new/src/dtos/jurisdictions/jurisdiction.dto.ts b/backend_new/src/dtos/jurisdictions/jurisdiction.dto.ts index bafe347df9..0a2ec04b54 100644 --- a/backend_new/src/dtos/jurisdictions/jurisdiction.dto.ts +++ b/backend_new/src/dtos/jurisdictions/jurisdiction.dto.ts @@ -26,7 +26,7 @@ export class Jurisdiction extends AbstractDTO { @Expose() @IsString({ groups: [ValidationsGroupsEnum.default] }) @ApiProperty() - notificationsSignUpUrl?: string | null; + notificationsSignUpUrl?: string; @Expose() @IsArray({ groups: [ValidationsGroupsEnum.default] }) @@ -49,7 +49,7 @@ export class Jurisdiction extends AbstractDTO { @Expose() @IsString({ groups: [ValidationsGroupsEnum.default] }) @ApiProperty() - partnerTerms?: string | null; + partnerTerms?: string; @Expose() @IsString({ groups: [ValidationsGroupsEnum.default] }) @@ -72,17 +72,17 @@ export class Jurisdiction extends AbstractDTO { @Expose() @IsBoolean({ groups: [ValidationsGroupsEnum.default] }) @ApiProperty() - enablePartnerSettings?: boolean | null; + enablePartnerSettings?: boolean; @Expose() @IsBoolean({ groups: [ValidationsGroupsEnum.default] }) @IsDefined({ groups: [ValidationsGroupsEnum.default] }) @ApiProperty() - enableAccessibilityFeatures: boolean | null; + enableAccessibilityFeatures: boolean; @Expose() @IsBoolean({ groups: [ValidationsGroupsEnum.default] }) @IsDefined({ groups: [ValidationsGroupsEnum.default] }) @ApiProperty() - enableUtilitiesIncluded: boolean | null; + enableUtilitiesIncluded: boolean; } diff --git a/backend_new/src/dtos/listings/listing-event.dto.ts b/backend_new/src/dtos/listings/listing-event.dto.ts index 1d9a518a45..ff480c7a48 100644 --- a/backend_new/src/dtos/listings/listing-event.dto.ts +++ b/backend_new/src/dtos/listings/listing-event.dto.ts @@ -39,15 +39,15 @@ export class ListingEvent extends AbstractDTO { @Expose() @IsString({ groups: [ValidationsGroupsEnum.default] }) - url?: string | null; + url?: string; @Expose() @IsString({ groups: [ValidationsGroupsEnum.default] }) - note?: string | null; + note?: string; @Expose() @IsString({ groups: [ValidationsGroupsEnum.default] }) - label?: string | null; + label?: string; @Expose() @ValidateNested({ groups: [ValidationsGroupsEnum.default] }) diff --git a/backend_new/src/dtos/listings/listing-feature.dto.ts b/backend_new/src/dtos/listings/listing-feature.dto.ts index 86808a5bcc..3f819058f9 100644 --- a/backend_new/src/dtos/listings/listing-feature.dto.ts +++ b/backend_new/src/dtos/listings/listing-feature.dto.ts @@ -6,61 +6,61 @@ import { AbstractDTO } from '../shared/abstract.dto'; export class ListingFeatures extends AbstractDTO { @Expose() @IsBoolean({ groups: [ValidationsGroupsEnum.default] }) - elevator?: boolean | null; + elevator?: boolean; @Expose() @IsBoolean({ groups: [ValidationsGroupsEnum.default] }) - wheelchairRamp?: boolean | null; + wheelchairRamp?: boolean; @Expose() @IsBoolean({ groups: [ValidationsGroupsEnum.default] }) - serviceAnimalsAllowed?: boolean | null; + serviceAnimalsAllowed?: boolean; @Expose() @IsBoolean({ groups: [ValidationsGroupsEnum.default] }) - accessibleParking?: boolean | null; + accessibleParking?: boolean; @Expose() @IsBoolean({ groups: [ValidationsGroupsEnum.default] }) - parkingOnSite?: boolean | null; + parkingOnSite?: boolean; @Expose() @IsBoolean({ groups: [ValidationsGroupsEnum.default] }) - inUnitWasherDryer?: boolean | null; + inUnitWasherDryer?: boolean; @Expose() @IsBoolean({ groups: [ValidationsGroupsEnum.default] }) - laundryInBuilding?: boolean | null; + laundryInBuilding?: boolean; @Expose() @IsBoolean({ groups: [ValidationsGroupsEnum.default] }) - barrierFreeEntrance?: boolean | null; + barrierFreeEntrance?: boolean; @Expose() @IsBoolean({ groups: [ValidationsGroupsEnum.default] }) - rollInShower?: boolean | null; + rollInShower?: boolean; @Expose() @IsBoolean({ groups: [ValidationsGroupsEnum.default] }) - grabBars?: boolean | null; + grabBars?: boolean; @Expose() @IsBoolean({ groups: [ValidationsGroupsEnum.default] }) - heatingInUnit?: boolean | null; + heatingInUnit?: boolean; @Expose() @IsBoolean({ groups: [ValidationsGroupsEnum.default] }) - acInUnit?: boolean | null; + acInUnit?: boolean; @Expose() @IsBoolean({ groups: [ValidationsGroupsEnum.default] }) - hearing?: boolean | null; + hearing?: boolean; @Expose() @IsBoolean({ groups: [ValidationsGroupsEnum.default] }) - visual?: boolean | null; + visual?: boolean; @Expose() @IsBoolean({ groups: [ValidationsGroupsEnum.default] }) - mobility?: boolean | null; + mobility?: boolean; } diff --git a/backend_new/src/dtos/listings/listing-get.dto.ts b/backend_new/src/dtos/listings/listing-get.dto.ts index 2ffa32d081..955f13b13b 100644 --- a/backend_new/src/dtos/listings/listing-get.dto.ts +++ b/backend_new/src/dtos/listings/listing-get.dto.ts @@ -38,7 +38,7 @@ class ListingGet extends AbstractDTO { @Expose() @IsString({ groups: [ValidationsGroupsEnum.default] }) @ApiProperty() - additionalApplicationSubmissionNotes?: string | null; + additionalApplicationSubmissionNotes?: string; @Expose() @IsBoolean({ groups: [ValidationsGroupsEnum.default] }) @@ -63,94 +63,94 @@ class ListingGet extends AbstractDTO { @Expose() @IsString({ groups: [ValidationsGroupsEnum.default] }) @ApiProperty() - accessibility?: string | null; + accessibility?: string; @Expose() @IsString({ groups: [ValidationsGroupsEnum.default] }) @ApiProperty() - amenities?: string | null; + amenities?: string; @Expose() @IsNumber({}, { groups: [ValidationsGroupsEnum.default] }) @ApiProperty() - buildingTotalUnits?: number | null; + buildingTotalUnits?: number; @Expose() @IsString({ groups: [ValidationsGroupsEnum.default] }) @ApiProperty() - developer?: string | null; + developer?: string; @Expose() @IsNumber({}, { groups: [ValidationsGroupsEnum.default] }) @ApiProperty() - householdSizeMax?: number | null; + householdSizeMax?: number; @Expose() @IsNumber({}, { groups: [ValidationsGroupsEnum.default] }) @ApiProperty() - householdSizeMin?: number | null; + householdSizeMin?: number; @Expose() @IsString({ groups: [ValidationsGroupsEnum.default] }) @ApiProperty() - neighborhood?: string | null; + neighborhood?: string; @Expose() @IsString({ groups: [ValidationsGroupsEnum.default] }) @ApiProperty() - petPolicy?: string | null; + petPolicy?: string; @Expose() @IsString({ groups: [ValidationsGroupsEnum.default] }) @ApiProperty() - smokingPolicy?: string | null; + smokingPolicy?: string; @Expose() @IsNumber({}, { groups: [ValidationsGroupsEnum.default] }) @ApiProperty() - unitsAvailable?: number | null; + unitsAvailable?: number; @Expose() @IsString({ groups: [ValidationsGroupsEnum.default] }) @ApiProperty() - unitAmenities?: string | null; + unitAmenities?: string; @Expose() @IsString({ groups: [ValidationsGroupsEnum.default] }) @ApiProperty() - servicesOffered?: string | null; + servicesOffered?: string; @Expose() @IsNumber({}, { groups: [ValidationsGroupsEnum.default] }) @ApiProperty() - yearBuilt?: number | null; + yearBuilt?: number; @Expose() @IsDate({ groups: [ValidationsGroupsEnum.default] }) @Type(() => Date) @ApiProperty() - applicationDueDate?: Date | null; + applicationDueDate?: Date; @Expose() @IsDate({ groups: [ValidationsGroupsEnum.default] }) @Type(() => Date) @ApiProperty() - applicationOpenDate?: Date | null; + applicationOpenDate?: Date; @Expose() @IsString({ groups: [ValidationsGroupsEnum.default] }) @ApiProperty() - applicationFee?: string | null; + applicationFee?: string; @Expose() @IsString({ groups: [ValidationsGroupsEnum.default] }) @ApiProperty() - applicationOrganization?: string | null; + applicationOrganization?: string; @Expose() @IsString({ groups: [ValidationsGroupsEnum.default] }) @ApiProperty() - applicationPickUpAddressOfficeHours?: string | null; + applicationPickUpAddressOfficeHours?: string; @Expose() @IsEnum(ApplicationAddressTypeEnum, { @@ -160,12 +160,12 @@ class ListingGet extends AbstractDTO { enum: ApplicationAddressTypeEnum, enumName: 'ApplicationAddressTypeEnum', }) - applicationPickUpAddressType?: ApplicationAddressTypeEnum | null; + applicationPickUpAddressType?: ApplicationAddressTypeEnum; @Expose() @IsString({ groups: [ValidationsGroupsEnum.default] }) @ApiProperty() - applicationDropOffAddressOfficeHours?: string | null; + applicationDropOffAddressOfficeHours?: string; @Expose() @IsEnum(ApplicationAddressTypeEnum, { @@ -175,7 +175,7 @@ class ListingGet extends AbstractDTO { enum: ApplicationAddressTypeEnum, enumName: 'ApplicationAddressTypeEnum', }) - applicationDropOffAddressType?: ApplicationAddressTypeEnum | null; + applicationDropOffAddressType?: ApplicationAddressTypeEnum; @Expose() @IsEnum(ApplicationAddressTypeEnum, { @@ -185,73 +185,73 @@ class ListingGet extends AbstractDTO { enum: ApplicationAddressTypeEnum, enumName: 'ApplicationAddressTypeEnum', }) - applicationMailingAddressType?: ApplicationAddressTypeEnum | null; + applicationMailingAddressType?: ApplicationAddressTypeEnum; @Expose() @IsString({ groups: [ValidationsGroupsEnum.default] }) @ApiProperty() - buildingSelectionCriteria?: string | null; + buildingSelectionCriteria?: string; @Expose() @IsString({ groups: [ValidationsGroupsEnum.default] }) @ApiProperty() - costsNotIncluded?: string | null; + costsNotIncluded?: string; @Expose() @IsString({ groups: [ValidationsGroupsEnum.default] }) @ApiProperty() - creditHistory?: string | null; + creditHistory?: string; @Expose() @IsString({ groups: [ValidationsGroupsEnum.default] }) @ApiProperty() - criminalBackground?: string | null; + criminalBackground?: string; @Expose() @IsString({ groups: [ValidationsGroupsEnum.default] }) @ApiProperty() - depositMin?: string | null; + depositMin?: string; @Expose() @IsString({ groups: [ValidationsGroupsEnum.default] }) @ApiProperty() - depositMax?: string | null; + depositMax?: string; @Expose() @IsString({ groups: [ValidationsGroupsEnum.default] }) @ApiProperty() - depositHelperText?: string | null; + depositHelperText?: string; @Expose() @IsBoolean({ groups: [ValidationsGroupsEnum.default] }) @ApiProperty() - disableUnitsAccordion?: boolean | null; + disableUnitsAccordion?: boolean; @Expose() @IsEmail({}, { groups: [ValidationsGroupsEnum.default] }) @EnforceLowerCase() @ApiProperty() - leasingAgentEmail?: string | null; + leasingAgentEmail?: string; @Expose() @IsString({ groups: [ValidationsGroupsEnum.default] }) @ApiProperty() - leasingAgentName?: string | null; + leasingAgentName?: string; @Expose() @IsString({ groups: [ValidationsGroupsEnum.default] }) @ApiProperty() - leasingAgentOfficeHours?: string | null; + leasingAgentOfficeHours?: string; @Expose() @IsString({ groups: [ValidationsGroupsEnum.default] }) @ApiProperty() - leasingAgentPhone?: string | null; + leasingAgentPhone?: string; @Expose() @IsString({ groups: [ValidationsGroupsEnum.default] }) @ApiProperty() - leasingAgentTitle?: string | null; + leasingAgentTitle?: string; @Expose() @IsDefined({ groups: [ValidationsGroupsEnum.default] }) @@ -263,47 +263,47 @@ class ListingGet extends AbstractDTO { @IsDate({ groups: [ValidationsGroupsEnum.default] }) @Type(() => Date) @ApiProperty() - postmarkedApplicationsReceivedByDate?: Date | null; + postmarkedApplicationsReceivedByDate?: Date; @Expose() @IsString({ groups: [ValidationsGroupsEnum.default] }) @ApiProperty() - programRules?: string | null; + programRules?: string; @Expose() @IsString({ groups: [ValidationsGroupsEnum.default] }) @ApiProperty() - rentalAssistance?: string | null; + rentalAssistance?: string; @Expose() @IsString({ groups: [ValidationsGroupsEnum.default] }) @ApiProperty() - rentalHistory?: string | null; + rentalHistory?: string; @Expose() @IsString({ groups: [ValidationsGroupsEnum.default] }) @ApiProperty() - requiredDocuments?: string | null; + requiredDocuments?: string; @Expose() @IsString({ groups: [ValidationsGroupsEnum.default] }) @ApiProperty() - specialNotes?: string | null; + specialNotes?: string; @Expose() @IsNumber({}, { groups: [ValidationsGroupsEnum.default] }) @ApiProperty() - waitlistCurrentSize?: number | null; + waitlistCurrentSize?: number; @Expose() @IsNumber({}, { groups: [ValidationsGroupsEnum.default] }) @ApiProperty() - waitlistMaxSize?: number | null; + waitlistMaxSize?: number; @Expose() @IsString({ groups: [ValidationsGroupsEnum.default] }) @ApiProperty() - whatToExpect?: string | null; + whatToExpect?: string; @Expose() @IsEnum(ListingsStatusEnum, { groups: [ValidationsGroupsEnum.default] }) @@ -316,7 +316,7 @@ class ListingGet extends AbstractDTO { enum: ReviewOrderTypeEnum, enumName: 'ReviewOrderTypeEnum', }) - reviewOrderType?: ReviewOrderTypeEnum | null; + reviewOrderType?: ReviewOrderTypeEnum; @Expose() @ApiProperty() @@ -342,57 +342,57 @@ class ListingGet extends AbstractDTO { @IsString({ groups: [ValidationsGroupsEnum.default] }) @MaxLength(4096, { groups: [ValidationsGroupsEnum.default] }) @ApiProperty() - reservedCommunityDescription?: string | null; + reservedCommunityDescription?: string; @Expose() @IsNumber({}, { groups: [ValidationsGroupsEnum.default] }) @ApiProperty() - reservedCommunityMinAge?: number | null; + reservedCommunityMinAge?: number; @Expose() @IsString({ groups: [ValidationsGroupsEnum.default] }) @MaxLength(4096, { groups: [ValidationsGroupsEnum.default] }) @ApiProperty() - resultLink?: string | null; + resultLink?: string; @Expose() @IsBoolean({ groups: [ValidationsGroupsEnum.default] }) @ApiProperty() - isWaitlistOpen?: boolean | null; + isWaitlistOpen?: boolean; @Expose() @IsNumber({}, { groups: [ValidationsGroupsEnum.default] }) @ApiProperty() - waitlistOpenSpots?: number | null; + waitlistOpenSpots?: number; @Expose() @IsBoolean({ groups: [ValidationsGroupsEnum.default] }) @ApiProperty() - customMapPin?: boolean | null; + customMapPin?: boolean; @Expose() @IsDate({ groups: [ValidationsGroupsEnum.default] }) @Type(() => Date) @ApiProperty() - publishedAt?: Date | null; + publishedAt?: Date; @Expose() @IsDate({ groups: [ValidationsGroupsEnum.default] }) @Type(() => Date) @ApiProperty() - closedAt?: Date | null; + closedAt?: Date; @Expose() @IsDate({ groups: [ValidationsGroupsEnum.default] }) @Type(() => Date) @ApiProperty() - afsLastRunAt?: Date | null; + afsLastRunAt?: Date; @Expose() @IsDate({ groups: [ValidationsGroupsEnum.default] }) @Type(() => Date) @ApiProperty() - lastApplicationUpdateAt?: Date | null; + lastApplicationUpdateAt?: Date; @Expose() @ValidateNested({ groups: [ValidationsGroupsEnum.default], each: true }) @@ -464,7 +464,7 @@ class ListingGet extends AbstractDTO { @ValidateNested({ groups: [ValidationsGroupsEnum.default] }) @Type(() => Asset) @ApiProperty({ type: Asset }) - listingsBuildingSelectionCriteriaFile?: Asset | null; + listingsBuildingSelectionCriteriaFile?: Asset; @Expose() @ValidateNested({ groups: [ValidationsGroupsEnum.default] }) @@ -477,7 +477,7 @@ class ListingGet extends AbstractDTO { @ValidateNested({ groups: [ValidationsGroupsEnum.default] }) @Type(() => Asset) @ApiProperty({ type: Asset }) - listingsResult?: Asset | null; + listingsResult?: Asset; @Expose() @ValidateNested({ groups: [ValidationsGroupsEnum.default] }) @@ -489,7 +489,7 @@ class ListingGet extends AbstractDTO { @ValidateNested({ groups: [ValidationsGroupsEnum.default], each: true }) @Type(() => ListingImage) @ApiProperty({ type: ListingImage, isArray: true }) - listingImages?: ListingImage[] | null; + listingImages?: ListingImage[]; @Expose() @ValidateNested({ groups: [ValidationsGroupsEnum.default], each: true }) diff --git a/backend_new/src/dtos/listings/listing-image.dto.ts b/backend_new/src/dtos/listings/listing-image.dto.ts index 69a4729304..f10b70f184 100644 --- a/backend_new/src/dtos/listings/listing-image.dto.ts +++ b/backend_new/src/dtos/listings/listing-image.dto.ts @@ -11,5 +11,5 @@ export class ListingImage { @Expose() @IsNumber({}, { groups: [ValidationsGroupsEnum.default] }) - ordinal?: number | null; + ordinal?: number; } diff --git a/backend_new/src/dtos/listings/listing-multiselect-question.dto.ts b/backend_new/src/dtos/listings/listing-multiselect-question.dto.ts index 38b2f1a409..8eeee3fc54 100644 --- a/backend_new/src/dtos/listings/listing-multiselect-question.dto.ts +++ b/backend_new/src/dtos/listings/listing-multiselect-question.dto.ts @@ -14,5 +14,5 @@ export class ListingMultiselectQuestion { @Expose() @IsNumber({}, { groups: [ValidationsGroupsEnum.default] }) @ApiProperty() - ordinal?: number | null; + ordinal?: number; } diff --git a/backend_new/src/dtos/listings/listing-utility.dto.ts b/backend_new/src/dtos/listings/listing-utility.dto.ts index aaafc839dd..e84324c188 100644 --- a/backend_new/src/dtos/listings/listing-utility.dto.ts +++ b/backend_new/src/dtos/listings/listing-utility.dto.ts @@ -6,33 +6,33 @@ import { AbstractDTO } from '../shared/abstract.dto'; export class ListingUtilities extends AbstractDTO { @Expose() @IsBoolean({ groups: [ValidationsGroupsEnum.default] }) - water?: boolean | null; + water?: boolean; @Expose() @IsBoolean({ groups: [ValidationsGroupsEnum.default] }) - gas?: boolean | null; + gas?: boolean; @Expose() @IsBoolean({ groups: [ValidationsGroupsEnum.default] }) - trash?: boolean | null; + trash?: boolean; @Expose() @IsBoolean({ groups: [ValidationsGroupsEnum.default] }) - sewer?: boolean | null; + sewer?: boolean; @Expose() @IsBoolean({ groups: [ValidationsGroupsEnum.default] }) - electricity?: boolean | null; + electricity?: boolean; @Expose() @IsBoolean({ groups: [ValidationsGroupsEnum.default] }) - cable?: boolean | null; + cable?: boolean; @Expose() @IsBoolean({ groups: [ValidationsGroupsEnum.default] }) - phone?: boolean | null; + phone?: boolean; @Expose() @IsBoolean({ groups: [ValidationsGroupsEnum.default] }) - internet?: boolean | null; + internet?: boolean; } diff --git a/backend_new/src/dtos/multiselect-questions/multiselect-option.dto.ts b/backend_new/src/dtos/multiselect-questions/multiselect-option.dto.ts index 3cc5235536..1e64b93d80 100644 --- a/backend_new/src/dtos/multiselect-questions/multiselect-option.dto.ts +++ b/backend_new/src/dtos/multiselect-questions/multiselect-option.dto.ts @@ -31,21 +31,21 @@ export class MultiselectOption { @Expose() @IsString({ groups: [ValidationsGroupsEnum.default] }) @ApiProperty() - description?: string | null; + description?: string; @Expose() @ValidateNested({ groups: [ValidationsGroupsEnum.default], each: true }) @Type(() => MultiselectLink) @ApiProperty({ type: MultiselectLink, isArray: true }) - links?: MultiselectLink[] | null; + links?: MultiselectLink[]; @Expose() @IsBoolean({ groups: [ValidationsGroupsEnum.default] }) @ApiProperty() - collectAddress?: boolean | null; + collectAddress?: boolean; @Expose() @IsBoolean({ groups: [ValidationsGroupsEnum.default] }) @ApiProperty() - exclusive?: boolean | null; + exclusive?: boolean; } diff --git a/backend_new/src/dtos/multiselect-questions/multiselect-question-get.dto.ts b/backend_new/src/dtos/multiselect-questions/multiselect-question-get.dto.ts index ead84cf932..38fe33818c 100644 --- a/backend_new/src/dtos/multiselect-questions/multiselect-question-get.dto.ts +++ b/backend_new/src/dtos/multiselect-questions/multiselect-question-get.dto.ts @@ -35,18 +35,18 @@ class MultiselectQuestion extends AbstractDTO { @Expose() @IsString({ groups: [ValidationsGroupsEnum.default] }) @ApiProperty() - subText?: string | null; + subText?: string; @Expose() @IsString({ groups: [ValidationsGroupsEnum.default] }) @ApiProperty() - description?: string | null; + description?: string; @Expose() @ValidateNested({ groups: [ValidationsGroupsEnum.default], each: true }) @Type(() => MultiselectLink) @ApiProperty({ type: MultiselectLink, isArray: true }) - links?: MultiselectLink[] | null; + links?: MultiselectLink[]; @Expose() @ValidateNested({ groups: [ValidationsGroupsEnum.default] }) @@ -60,12 +60,12 @@ class MultiselectQuestion extends AbstractDTO { @ValidateNested({ groups: [ValidationsGroupsEnum.default], each: true }) @Type(() => MultiselectOption) @ApiProperty({ type: MultiselectOption, isArray: true }) - options?: MultiselectOption[] | null; + options?: MultiselectOption[]; @Expose() @IsString({ groups: [ValidationsGroupsEnum.default] }) @ApiProperty() - optOutText?: string | null; + optOutText?: string; @Expose() @IsBoolean({ groups: [ValidationsGroupsEnum.default] }) diff --git a/backend_new/src/dtos/paper-applications/paper-application-get.dto.ts b/backend_new/src/dtos/paper-applications/paper-application.dto.ts similarity index 100% rename from backend_new/src/dtos/paper-applications/paper-application-get.dto.ts rename to backend_new/src/dtos/paper-applications/paper-application.dto.ts diff --git a/backend_new/src/dtos/reserved-community-types/reserved-community-type.dto.ts b/backend_new/src/dtos/reserved-community-types/reserved-community-type.dto.ts index 9298c0b811..13201b0dd2 100644 --- a/backend_new/src/dtos/reserved-community-types/reserved-community-type.dto.ts +++ b/backend_new/src/dtos/reserved-community-types/reserved-community-type.dto.ts @@ -22,7 +22,7 @@ export class ReservedCommunityType extends AbstractDTO { @IsString({ groups: [ValidationsGroupsEnum.default] }) @MaxLength(2048, { groups: [ValidationsGroupsEnum.default] }) @ApiProperty() - description?: string | null; + description?: string; @Expose() @ValidateNested({ groups: [ValidationsGroupsEnum.default] }) diff --git a/backend_new/src/dtos/units/unit-get.dto.ts b/backend_new/src/dtos/units/unit-get.dto.ts index 12c9b6e491..3d7ed20511 100644 --- a/backend_new/src/dtos/units/unit-get.dto.ts +++ b/backend_new/src/dtos/units/unit-get.dto.ts @@ -16,78 +16,78 @@ import { UnitAmiChartOverride } from './ami-chart-override-get.dto'; class Unit extends AbstractDTO { @Expose() - amiChart?: AmiChart | null; + amiChart?: AmiChart; @Expose() @IsString({ groups: [ValidationsGroupsEnum.default] }) - amiPercentage?: string | null; + amiPercentage?: string; @Expose() @IsNumberString({}, { groups: [ValidationsGroupsEnum.default] }) - annualIncomeMin?: string | null; + annualIncomeMin?: string; @Expose() @IsNumberString({}, { groups: [ValidationsGroupsEnum.default] }) - monthlyIncomeMin?: string | null; + monthlyIncomeMin?: string; @Expose() @IsNumber({}, { groups: [ValidationsGroupsEnum.default] }) - floor?: number | null; + floor?: number; @Expose() @IsString({ groups: [ValidationsGroupsEnum.default] }) - annualIncomeMax?: string | null; + annualIncomeMax?: string; @Expose() @IsNumber({}, { groups: [ValidationsGroupsEnum.default] }) - maxOccupancy?: number | null; + maxOccupancy?: number; @Expose() @IsNumber({}, { groups: [ValidationsGroupsEnum.default] }) - minOccupancy?: number | null; + minOccupancy?: number; @Expose() @IsNumberString({}, { groups: [ValidationsGroupsEnum.default] }) - monthlyRent?: string | null; + monthlyRent?: string; @Expose() @IsNumber({}, { groups: [ValidationsGroupsEnum.default] }) - numBathrooms?: number | null; + numBathrooms?: number; @Expose() @IsNumber({}, { groups: [ValidationsGroupsEnum.default] }) - numBedrooms?: number | null; + numBedrooms?: number; @Expose() @IsString({ groups: [ValidationsGroupsEnum.default] }) - number?: string | null; + number?: string; @Expose() @IsString({ groups: [ValidationsGroupsEnum.default] }) - sqFeet?: string | null; + sqFeet?: string; @Expose() @IsString({ groups: [ValidationsGroupsEnum.default] }) - monthlyRentAsPercentOfIncome?: string | null; + monthlyRentAsPercentOfIncome?: string; @Expose() @IsBoolean({ groups: [ValidationsGroupsEnum.default] }) - bmrProgramChart?: boolean | null; + bmrProgramChart?: boolean; @Expose() @ValidateNested({ groups: [ValidationsGroupsEnum.default] }) @Type(() => UnitType) - unitTypes?: UnitType | null; + unitTypes?: UnitType; @Expose() @ValidateNested({ groups: [ValidationsGroupsEnum.default] }) @Type(() => UnitRentType) - unitRentTypes?: UnitRentType | null; + unitRentTypes?: UnitRentType; @Expose() @ValidateNested({ groups: [ValidationsGroupsEnum.default] }) @Type(() => UnitAccessibilityPriorityType) - unitAccessibilityPriorityTypes?: UnitAccessibilityPriorityType | null; + unitAccessibilityPriorityTypes?: UnitAccessibilityPriorityType; @Expose() @ValidateNested({ groups: [ValidationsGroupsEnum.default] }) diff --git a/backend_new/src/dtos/units/unit-summary-get.dto.ts b/backend_new/src/dtos/units/unit-summary-get.dto.ts index 6ee81bd397..1bc28381b0 100644 --- a/backend_new/src/dtos/units/unit-summary-get.dto.ts +++ b/backend_new/src/dtos/units/unit-summary-get.dto.ts @@ -10,7 +10,7 @@ export class UnitSummary { @Expose() @IsString({ groups: [ValidationsGroupsEnum.default] }) @ApiProperty() - unitTypes?: UnitType | null; + unitTypes?: UnitType; @Expose() @IsDefined({ groups: [ValidationsGroupsEnum.default] }) diff --git a/backend_new/src/dtos/units/units-summery-get.dto.ts b/backend_new/src/dtos/units/units-summery-get.dto.ts index 7335c23a9e..b5d1db7c41 100644 --- a/backend_new/src/dtos/units/units-summery-get.dto.ts +++ b/backend_new/src/dtos/units/units-summery-get.dto.ts @@ -26,64 +26,64 @@ class UnitsSummary { @Expose() @IsNumberString({}, { groups: [ValidationsGroupsEnum.default] }) - monthlyRentMin?: number | null; + monthlyRentMin?: number; @Expose() @IsNumberString({}, { groups: [ValidationsGroupsEnum.default] }) - monthlyRentMax?: number | null; + monthlyRentMax?: number; @Expose() @IsString({ groups: [ValidationsGroupsEnum.default] }) - monthlyRentAsPercentOfIncome?: string | null; + monthlyRentAsPercentOfIncome?: string; @Expose() @IsNumber({}, { groups: [ValidationsGroupsEnum.default] }) - amiPercentage?: number | null; + amiPercentage?: number; @Expose() @IsNumberString({}, { groups: [ValidationsGroupsEnum.default] }) - minimumIncomeMin?: string | null; + minimumIncomeMin?: string; @Expose() @IsNumberString({}, { groups: [ValidationsGroupsEnum.default] }) - minimumIncomeMax?: string | null; + minimumIncomeMax?: string; @Expose() @IsNumber({}, { groups: [ValidationsGroupsEnum.default] }) - maxOccupancy?: number | null; + maxOccupancy?: number; @Expose() @IsNumber({}, { groups: [ValidationsGroupsEnum.default] }) - minOccupancy?: number | null; + minOccupancy?: number; @Expose() @IsNumber({}, { groups: [ValidationsGroupsEnum.default] }) - floorMin?: number | null; + floorMin?: number; @Expose() @IsNumber({}, { groups: [ValidationsGroupsEnum.default] }) - floorMax?: number | null; + floorMax?: number; @Expose() @IsString({ groups: [ValidationsGroupsEnum.default] }) - sqFeetMin?: string | null; + sqFeetMin?: string; @Expose() @IsString({ groups: [ValidationsGroupsEnum.default] }) - sqFeetMax?: string | null; + sqFeetMax?: string; @Expose() @ValidateNested({ groups: [ValidationsGroupsEnum.default] }) @Type(() => UnitAccessibilityPriorityType) - unitAccessibilityPriorityTypes?: UnitAccessibilityPriorityType | null; + unitAccessibilityPriorityTypes?: UnitAccessibilityPriorityType; @Expose() @IsNumber({}, { groups: [ValidationsGroupsEnum.default] }) - totalCount?: number | null; + totalCount?: number; @Expose() @IsNumber({}, { groups: [ValidationsGroupsEnum.default] }) - totalAvailable?: number | null; + totalAvailable?: number; } export { UnitsSummary as default, UnitsSummary }; diff --git a/backend_new/src/enums/applications/order-by-enum.ts b/backend_new/src/enums/applications/order-by-enum.ts new file mode 100644 index 0000000000..d8c01e2dd2 --- /dev/null +++ b/backend_new/src/enums/applications/order-by-enum.ts @@ -0,0 +1,6 @@ +export enum ApplicationOrderByKeys { + firstName = 'firstName', + lastName = 'lastName', + submissionDate = 'submissionDate', + createdAt = 'createdAt', +} diff --git a/backend_new/src/enums/shared/input-type-enum.ts b/backend_new/src/enums/shared/input-type-enum.ts new file mode 100644 index 0000000000..a6b45a3773 --- /dev/null +++ b/backend_new/src/enums/shared/input-type-enum.ts @@ -0,0 +1,6 @@ +export enum InputType { + boolean = 'boolean', + text = 'text', + address = 'address', + hhMemberSelect = 'hhMemberSelect', +} diff --git a/backend_new/src/modules/app.module.ts b/backend_new/src/modules/app.module.ts index b64e8ee695..7ddb0d2094 100644 --- a/backend_new/src/modules/app.module.ts +++ b/backend_new/src/modules/app.module.ts @@ -10,6 +10,7 @@ import { UnitTypeModule } from './unit-type.module'; import { UnitRentTypeModule } from './unit-rent-type.module'; import { JurisdictionModule } from './jurisdiction.module'; import { MultiselectQuestionModule } from './multiselect-question.module'; +import { ApplicationModule } from './application.module'; @Module({ imports: [ @@ -21,6 +22,7 @@ import { MultiselectQuestionModule } from './multiselect-question.module'; UnitRentTypeModule, JurisdictionModule, MultiselectQuestionModule, + ApplicationModule, ], controllers: [AppController], providers: [AppService, PrismaService], @@ -33,6 +35,7 @@ import { MultiselectQuestionModule } from './multiselect-question.module'; UnitRentTypeModule, JurisdictionModule, MultiselectQuestionModule, + ApplicationModule, ], }) export class AppModule {} diff --git a/backend_new/src/modules/application.module.ts b/backend_new/src/modules/application.module.ts new file mode 100644 index 0000000000..24aeba9a4b --- /dev/null +++ b/backend_new/src/modules/application.module.ts @@ -0,0 +1,12 @@ +import { Module } from '@nestjs/common'; +import { ApplicationController } from '../controllers/application.controller'; +import { ApplicationService } from '../services/application.service'; +import { PrismaService } from '../services/prisma.service'; + +@Module({ + imports: [], + controllers: [ApplicationController], + providers: [ApplicationService, PrismaService], + exports: [ApplicationService, PrismaService], +}) +export class ApplicationModule {} diff --git a/backend_new/src/services/application.service.ts b/backend_new/src/services/application.service.ts new file mode 100644 index 0000000000..e8eec73bdc --- /dev/null +++ b/backend_new/src/services/application.service.ts @@ -0,0 +1,262 @@ +import { Injectable, NotFoundException } from '@nestjs/common'; +import { PrismaService } from './prisma.service'; +import { Application } from '../dtos/applications/application.dto'; +import { mapTo } from '../utilities/mapTo'; +import { ApplicationQueryParams } from '../dtos/applications/application-query-params.dto'; +import { calculateSkip, calculateTake } from '../utilities/pagination-helpers'; +import { Prisma } from '@prisma/client'; +import { buildOrderBy } from '../utilities/build-order-by'; +import { buildPaginationInfo } from '../utilities/build-pagination-meta'; + +const view: Record = { + partnerList: { + applicant: { + include: { + applicantAddress: true, + applicantWorkAddress: true, + }, + }, + householdMember: true, + accessibility: true, + applicationsMailingAddress: true, + applicationsAlternateAddress: true, + alternateContact: { + include: { + address: true, + }, + }, + }, +}; + +view.base = { + ...view.partnerList, + demographics: true, + preferredUnitTypes: true, + householdMember: { + include: { + householdMemberAddress: true, + householdMemberWorkAddress: true, + }, + }, +}; + +/* + this is the service for applicationss + it handles all the backend's business logic for reading/writing/deleting application data +*/ +@Injectable() +export class ApplicationService { + constructor(private prisma: PrismaService) {} + + /* + this will get a set of applications given the params passed in + this set can either be paginated or not depending on the params + it will return both the set of applications, and some meta information to help with pagination + */ + async list(params: ApplicationQueryParams) { + const whereClause = this.buildWhereClause(params); + + const count = await this.prisma.applications.count({ + where: whereClause, + }); + + const rawApplications = await this.prisma.applications.findMany({ + skip: calculateSkip(params.limit, params.page), + take: calculateTake(params.limit), + orderBy: buildOrderBy([params.orderBy], [params.order]), + include: view[params.listingId ? 'partnerList' : 'base'], + where: whereClause, + }); + + const applications = mapTo(Application, rawApplications); + + const promiseArray = applications.map((application) => + this.getDuplicateFlagsForApplication(application.id), + ); + + const flags = await Promise.all(promiseArray); + applications.forEach((application, index) => { + application.flagged = !!flags[index]?.id; + }); + + return { + items: applications, + meta: buildPaginationInfo( + params.limit, + params.page, + count, + applications.length, + ), + }; + } + + /* + this builds the where clause for list() + */ + buildWhereClause( + params: ApplicationQueryParams, + ): Prisma.ApplicationsWhereInput { + const toReturn: Prisma.ApplicationsWhereInput[] = []; + + if (params.userId) { + toReturn.push({ + userAccounts: { + id: params.userId, + }, + }); + } + if (params.listingId) { + toReturn.push({ + listingId: params.listingId, + }); + } + if (params.search) { + const searchFilter: Prisma.StringFilter = { + contains: params.search, + mode: 'insensitive', + }; + toReturn.push({ + OR: [ + { + confirmationCode: searchFilter, + }, + { + applicant: { + firstName: searchFilter, + }, + }, + { + applicant: { + lastName: searchFilter, + }, + }, + { + applicant: { + emailAddress: searchFilter, + }, + }, + { + applicant: { + phoneNumber: searchFilter, + }, + }, + { + alternateContact: { + firstName: searchFilter, + }, + }, + { + alternateContact: { + lastName: searchFilter, + }, + }, + { + alternateContact: { + emailAddress: searchFilter, + }, + }, + { + alternateContact: { + phoneNumber: searchFilter, + }, + }, + ], + }); + } + if (params.markedAsDuplicate !== undefined) { + toReturn.push({ + markedAsDuplicate: params.markedAsDuplicate, + }); + } + return { + AND: toReturn, + }; + } + + /* + this is to calculate the `flagged` property of an application + ideally in the future we save this data on the application so we don't have to keep + recalculating it + */ + async getDuplicateFlagsForApplication(applicationId: string) { + return this.prisma.applications.findFirst({ + select: { + id: true, + }, + where: { + id: applicationId, + applicationFlaggedSet: { + some: {}, + }, + }, + }); + } + + /* + this will return 1 application or error + */ + async findOne(applicationId: string) { + const rawApplication = await this.prisma.applications.findFirst({ + where: { + id: { + equals: applicationId, + }, + }, + include: { + userAccounts: true, + applicant: { + include: { + applicantAddress: true, + applicantWorkAddress: true, + }, + }, + applicationsMailingAddress: true, + applicationsAlternateAddress: true, + alternateContact: { + include: { + address: true, + }, + }, + accessibility: true, + demographics: true, + householdMember: { + include: { + householdMemberAddress: true, + householdMemberWorkAddress: true, + }, + }, + preferredUnitTypes: true, + }, + }); + + if (!rawApplication) { + throw new NotFoundException( + `applicationId ${applicationId} was requested but not found`, + ); + } + + return mapTo(Application, rawApplication); + } + + /* + this will create an application + */ + async create(incomingData: any) { + // TODO + } + + /* + this will update an application + if no application has the id of the incoming argument an error is thrown + */ + async update(incomingData: any) { + // TODO + } + + /* + this will delete an application + */ + async delete(applicationId: string) { + // TODO + } +} diff --git a/backend_new/src/services/prisma.service.ts b/backend_new/src/services/prisma.service.ts index 844f980c44..332639addc 100644 --- a/backend_new/src/services/prisma.service.ts +++ b/backend_new/src/services/prisma.service.ts @@ -1,4 +1,4 @@ -import { INestApplication, Injectable, OnModuleInit } from '@nestjs/common'; +import { Injectable, OnModuleInit } from '@nestjs/common'; import { PrismaClient } from '@prisma/client'; /* @@ -9,10 +9,4 @@ export class PrismaService extends PrismaClient implements OnModuleInit { async onModuleInit() { await this.$connect(); } - - async enableShutdownHooks(app: INestApplication) { - this.$on('beforeExit', async () => { - await app.close(); - }); - } } diff --git a/backend_new/src/utilities/applications-utilities.ts b/backend_new/src/utilities/applications-utilities.ts new file mode 100644 index 0000000000..a3d84a81e9 --- /dev/null +++ b/backend_new/src/utilities/applications-utilities.ts @@ -0,0 +1,5 @@ +import { randomBytes } from 'crypto'; + +export const generateConfirmationCode = (): string => { + return randomBytes(4).toString('hex').toUpperCase(); +}; diff --git a/backend_new/src/utilities/build-pagination-meta.ts b/backend_new/src/utilities/build-pagination-meta.ts new file mode 100644 index 0000000000..899956ea12 --- /dev/null +++ b/backend_new/src/utilities/build-pagination-meta.ts @@ -0,0 +1,24 @@ +import { shouldPaginate } from './pagination-helpers'; + +export const buildPaginationInfo = ( + limit: 'all' | number, + page: number, + count: number, + returnedRecordCount: number, +) => { + const isPaginated = shouldPaginate(limit, page); + + const itemsPerPage = + isPaginated && limit !== 'all' ? limit : returnedRecordCount; + const totalItems = isPaginated ? count : returnedRecordCount; + + return { + currentPage: isPaginated ? page : 1, + itemCount: returnedRecordCount, + itemsPerPage: itemsPerPage, + totalItems: totalItems, + totalPages: Math.ceil( + totalItems / (itemsPerPage ? itemsPerPage : totalItems), + ), + }; +}; diff --git a/backend_new/test/integration/ami-chart.e2e-spec.ts b/backend_new/test/integration/ami-chart.e2e-spec.ts index ffb675de59..c61a8600d6 100644 --- a/backend_new/test/integration/ami-chart.e2e-spec.ts +++ b/backend_new/test/integration/ami-chart.e2e-spec.ts @@ -31,6 +31,11 @@ describe('AmiChart Controller Tests', () => { jurisdictionAId = jurisdictionA.id; }); + afterAll(async () => { + await prisma.$disconnect(); + await app.close(); + }); + it('testing list endpoint', async () => { const jurisdictionB = await prisma.jurisdictions.create({ data: jurisdictionFactory(), diff --git a/backend_new/test/integration/app.e2e-spec.ts b/backend_new/test/integration/app.e2e-spec.ts index d089b62b91..a22f5ec52b 100644 --- a/backend_new/test/integration/app.e2e-spec.ts +++ b/backend_new/test/integration/app.e2e-spec.ts @@ -14,6 +14,10 @@ describe('App Controller Tests', () => { await app.init(); }); + afterAll(async () => { + await app.close(); + }); + it('should return a successDTO', async () => { const res = await request(app.getHttpServer()).get('/').expect(200); diff --git a/backend_new/test/integration/application.e2e-spec.ts b/backend_new/test/integration/application.e2e-spec.ts new file mode 100644 index 0000000000..c7024132a0 --- /dev/null +++ b/backend_new/test/integration/application.e2e-spec.ts @@ -0,0 +1,161 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { INestApplication } from '@nestjs/common'; +import * as request from 'supertest'; +import { AppModule } from '../../src/modules/app.module'; +import { PrismaService } from '../../src/services/prisma.service'; +import { applicationFactory } from '../../prisma/seed-helpers/application-factory'; +import { + unitTypeFactoryAll, + unitTypeFactorySingle, +} from '../../prisma/seed-helpers/unit-type-factory'; +import { ApplicationQueryParams } from '../../src/dtos/applications/application-query-params.dto'; +import { OrderByEnum } from '../../src/enums/shared/order-by-enum'; +import { ApplicationOrderByKeys } from '../../src/enums/applications/order-by-enum'; +import { randomUUID } from 'crypto'; +import { stringify } from 'qs'; +import { UnitTypeEnum } from '@prisma/client'; + +describe('Application Controller Tests', () => { + let app: INestApplication; + let prisma: PrismaService; + + beforeAll(async () => { + const moduleFixture: TestingModule = await Test.createTestingModule({ + imports: [AppModule], + }).compile(); + + app = moduleFixture.createNestApplication(); + prisma = moduleFixture.get(PrismaService); + await app.init(); + await unitTypeFactoryAll(prisma); + }); + + afterAll(async () => { + await prisma.$disconnect(); + await app.close(); + }); + + it('should get no applications when params are sent, and no applications are stored', async () => { + const queryParams: ApplicationQueryParams = { + limit: 2, + page: 1, + order: OrderByEnum.ASC, + orderBy: ApplicationOrderByKeys.createdAt, + }; + const query = stringify(queryParams as any); + + const res = await request(app.getHttpServer()) + .get(`/applications?${query}`) + .expect(200); + expect(res.body.items.length).toBe(0); + }); + + it('should get no applications when no params are sent, and no applications are stored', async () => { + const res = await request(app.getHttpServer()) + .get(`/applications`) + .expect(200); + + expect(res.body.items.length).toBe(0); + }); + + it('should get stored applications when params are sent', async () => { + const unitTypeA = await unitTypeFactorySingle(prisma, UnitTypeEnum.oneBdrm); + + const applicationA = await prisma.applications.create({ + data: applicationFactory({ unitTypeId: unitTypeA.id }), + include: { + applicant: true, + }, + }); + const applicationB = await prisma.applications.create({ + data: applicationFactory({ unitTypeId: unitTypeA.id }), + include: { + applicant: true, + }, + }); + + const queryParams: ApplicationQueryParams = { + limit: 2, + page: 1, + order: OrderByEnum.ASC, + orderBy: ApplicationOrderByKeys.createdAt, + }; + const query = stringify(queryParams as any); + + const res = await request(app.getHttpServer()) + .get(`/applications?${query}`) + .expect(200); + + expect(res.body.items.length).toBeGreaterThanOrEqual(2); + const resApplicationA = res.body.items.find( + (item) => item.applicant.firstName === applicationA.applicant.firstName, + ); + expect(resApplicationA).not.toBeNull(); + res.body.items.find( + (item) => item.applicant.firstName === applicationB.applicant.firstName, + ); + expect(resApplicationA).not.toBeNull(); + }); + + it('should get stored applications when no params sent', async () => { + const unitTypeA = await unitTypeFactorySingle(prisma, UnitTypeEnum.oneBdrm); + + const applicationA = await prisma.applications.create({ + data: applicationFactory({ unitTypeId: unitTypeA.id }), + include: { + applicant: true, + }, + }); + const applicationB = await prisma.applications.create({ + data: applicationFactory({ unitTypeId: unitTypeA.id }), + include: { + applicant: true, + }, + }); + + const res = await request(app.getHttpServer()) + .get(`/applications`) + .expect(200); + + expect(res.body.items.length).toBeGreaterThanOrEqual(2); + const resApplicationA = res.body.items.find( + (item) => item.applicant.firstName === applicationA.applicant.firstName, + ); + expect(resApplicationA).not.toBeNull(); + res.body.items.find( + (item) => item.applicant.firstName === applicationB.applicant.firstName, + ); + expect(resApplicationA).not.toBeNull(); + }); + + it('should retrieve an application when one exists', async () => { + const unitTypeA = await unitTypeFactorySingle(prisma, UnitTypeEnum.oneBdrm); + + const applicationA = await prisma.applications.create({ + data: applicationFactory({ unitTypeId: unitTypeA.id }), + include: { + applicant: true, + }, + }); + + const res = await request(app.getHttpServer()) + .get(`/applications/${applicationA.id}`) + .expect(200); + + expect(res.body.applicant.firstName).toEqual( + applicationA.applicant.firstName, + ); + }); + + it("should throw an error when retrieve is called with an Id that doesn't exist", async () => { + const id = randomUUID(); + + const res = await request(app.getHttpServer()) + .get(`/applications/${id}`) + .expect(404); + + expect(res.body.message).toEqual( + `applicationId ${id} was requested but not found`, + ); + }); +}); diff --git a/backend_new/test/integration/jurisdiction.e2e-spec.ts b/backend_new/test/integration/jurisdiction.e2e-spec.ts index 0eaf238890..c09459068b 100644 --- a/backend_new/test/integration/jurisdiction.e2e-spec.ts +++ b/backend_new/test/integration/jurisdiction.e2e-spec.ts @@ -24,6 +24,11 @@ describe('Jurisdiction Controller Tests', () => { await app.init(); }); + afterAll(async () => { + await prisma.$disconnect(); + await app.close(); + }); + it('testing list endpoint', async () => { const jurisdictionA = await prisma.jurisdictions.create({ data: jurisdictionFactory(), diff --git a/backend_new/test/integration/listing.e2e-spec.ts b/backend_new/test/integration/listing.e2e-spec.ts index d2709aea49..2e6bb5d6ca 100644 --- a/backend_new/test/integration/listing.e2e-spec.ts +++ b/backend_new/test/integration/listing.e2e-spec.ts @@ -33,6 +33,11 @@ describe('Listing Controller Tests', () => { jurisdictionAId = jurisdiction.id; }); + afterAll(async () => { + await prisma.$disconnect(); + await app.close(); + }); + it('should not get listings from list endpoint when no params are sent', async () => { const res = await request(app.getHttpServer()).get('/listings').expect(200); diff --git a/backend_new/test/integration/multiselect-question.e2e-spec.ts b/backend_new/test/integration/multiselect-question.e2e-spec.ts index 13b0c3472a..82eec93508 100644 --- a/backend_new/test/integration/multiselect-question.e2e-spec.ts +++ b/backend_new/test/integration/multiselect-question.e2e-spec.ts @@ -34,6 +34,11 @@ describe('MultiselectQuestion Controller Tests', () => { jurisdictionId = jurisdiction.id; }); + afterAll(async () => { + await prisma.$disconnect(); + await app.close(); + }); + it('should get multiselect questions from list endpoint when no params are sent', async () => { const jurisdictionB = await prisma.jurisdictions.create({ data: jurisdictionFactory(), diff --git a/backend_new/test/integration/reserved-community-type.e2e-spec.ts b/backend_new/test/integration/reserved-community-type.e2e-spec.ts index f58becae29..e925d4b3c4 100644 --- a/backend_new/test/integration/reserved-community-type.e2e-spec.ts +++ b/backend_new/test/integration/reserved-community-type.e2e-spec.ts @@ -32,6 +32,11 @@ describe('ReservedCommunityType Controller Tests', () => { jurisdictionAId = jurisdictionA.id; }); + afterAll(async () => { + await prisma.$disconnect(); + await app.close(); + }); + it('testing list endpoint without params', async () => { const jurisdictionA = await prisma.jurisdictions.create({ data: jurisdictionFactory(), diff --git a/backend_new/test/integration/unit-accessibility-priority-type.e2e-spec.ts b/backend_new/test/integration/unit-accessibility-priority-type.e2e-spec.ts index f67a5fe688..84ef203c97 100644 --- a/backend_new/test/integration/unit-accessibility-priority-type.e2e-spec.ts +++ b/backend_new/test/integration/unit-accessibility-priority-type.e2e-spec.ts @@ -23,6 +23,11 @@ describe('UnitAccessibilityPriorityType Controller Tests', () => { await app.init(); }); + afterAll(async () => { + await prisma.$disconnect(); + await app.close(); + }); + it('testing list endpoint', async () => { const unitTypeA = await prisma.unitAccessibilityPriorityTypes.create({ data: unitAccessibilityPriorityTypeFactorySingle(), diff --git a/backend_new/test/integration/unit-rent-type.e2e-spec.ts b/backend_new/test/integration/unit-rent-type.e2e-spec.ts index 759da3830c..ac66151d79 100644 --- a/backend_new/test/integration/unit-rent-type.e2e-spec.ts +++ b/backend_new/test/integration/unit-rent-type.e2e-spec.ts @@ -23,6 +23,11 @@ describe('UnitRentType Controller Tests', () => { await app.init(); }); + afterAll(async () => { + await prisma.$disconnect(); + await app.close(); + }); + it('testing list endpoint', async () => { const unitRentTypeA = await prisma.unitRentTypes.create({ data: unitRentTypeFactory(), diff --git a/backend_new/test/integration/unit-type.e2e-spec.ts b/backend_new/test/integration/unit-type.e2e-spec.ts index 8096d56a33..d7a5fb442c 100644 --- a/backend_new/test/integration/unit-type.e2e-spec.ts +++ b/backend_new/test/integration/unit-type.e2e-spec.ts @@ -27,6 +27,11 @@ describe('UnitType Controller Tests', () => { await unitTypeFactoryAll(prisma); }); + afterAll(async () => { + await prisma.$disconnect(); + await app.close(); + }); + it('testing list endpoint', async () => { const res = await request(app.getHttpServer()) .get(`/unitTypes?`) diff --git a/backend_new/test/jest-e2e.config.js b/backend_new/test/jest-e2e.config.js index fdaf2bc53f..81905ffe71 100644 --- a/backend_new/test/jest-e2e.config.js +++ b/backend_new/test/jest-e2e.config.js @@ -4,11 +4,11 @@ module.exports = { testEnvironment: 'node', testRegex: '.e2e-spec.ts$', transform: { - '^.+\\.(t|j)s$': 'ts-jest', - }, - globals: { - 'ts-jest': { - diagnostics: false, - }, + '^.+\\.(t|j)s$': [ + 'ts-jest', + { + diagnostics: false, + }, + ], }, }; diff --git a/backend_new/test/jest.config.js b/backend_new/test/jest.config.js index 0b293cef18..41518a1ed3 100644 --- a/backend_new/test/jest.config.js +++ b/backend_new/test/jest.config.js @@ -4,11 +4,11 @@ module.exports = { testEnvironment: 'node', testRegex: '\\.spec.ts$', transform: { - '^.+\\.(t|j)s$': 'ts-jest', - }, - globals: { - 'ts-jest': { - diagnostics: false, - }, + '^.+\\.(t|j)s$': [ + 'ts-jest', + { + diagnostics: false, + }, + ], }, }; diff --git a/backend_new/test/unit/services/application.service.spec.ts b/backend_new/test/unit/services/application.service.spec.ts new file mode 100644 index 0000000000..8b6c4e8441 --- /dev/null +++ b/backend_new/test/unit/services/application.service.spec.ts @@ -0,0 +1,410 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { PrismaService } from '../../../src/services/prisma.service'; +import { ApplicationService } from '../../../src/services/application.service'; +import { + IncomePeriodEnum, + ApplicationStatusEnum, + ApplicationSubmissionTypeEnum, + ApplicationReviewStatusEnum, + YesNoEnum, +} from '@prisma/client'; +import { ApplicationQueryParams } from '../../../src/dtos/applications/application-query-params.dto'; +import { OrderByEnum } from '../../../src/enums/shared/order-by-enum'; +import { ApplicationOrderByKeys } from '../../../src/enums/applications/order-by-enum'; +import { randomUUID } from 'crypto'; + +describe('Testing application service', () => { + let service: ApplicationService; + let prisma: PrismaService; + + const mockApplication = (position: number, date: Date) => { + return { + id: randomUUID(), + appUrl: `appUrl ${position}`, + additionalPhone: true, + additionalPhoneNumber: `additionalPhoneNumber ${position}`, + additionalPhoneNumberType: `additionalPhoneNumberType ${position}`, + householdSize: position, + housingStatus: `housingStatus ${position}`, + sendMailToMailingAddress: true, + householdExpectingChanges: true, + householdStudent: true, + incomeVouchers: true, + income: `income ${position}`, + incomePeriod: IncomePeriodEnum.perMonth, + preferences: { + claimed: true, + key: 'example key', + options: null, + }, + status: ApplicationStatusEnum.submitted, + submissionType: ApplicationSubmissionTypeEnum.electronical, + acceptedTerms: true, + submissionDate: date, + markedAsDuplicate: false, + confirmationCode: `confirmationCode ${position}`, + reviewStatus: ApplicationReviewStatusEnum.valid, + applicant: { + firstName: `application ${position} firstName`, + middleName: `application ${position} middleName`, + lastName: `application ${position} lastName`, + birthMonth: `application ${position} birthMonth`, + birthDay: `application ${position} birthDay`, + birthYear: `application ${position} birthYear`, + emailAddress: `application ${position} emailaddress`, + noEmail: false, + phoneNumber: `application ${position} phoneNumber`, + phoneNumberType: `application ${position} phoneNumberType`, + noPhone: false, + workInRegion: YesNoEnum.yes, + applicantWorkAddress: { + placeName: `application ${position} applicantWorkAddress placeName`, + city: `application ${position} applicantWorkAddress city`, + county: `application ${position} applicantWorkAddress county`, + state: `application ${position} applicantWorkAddress state`, + street: `application ${position} applicantWorkAddress street`, + street2: `application ${position} applicantWorkAddress street2`, + zipCode: `application ${position} applicantWorkAddress zipCode`, + latitude: position, + longitude: position, + }, + applicantAddress: { + placeName: `application ${position} applicantAddress placeName`, + city: `application ${position} applicantAddress city`, + county: `application ${position} applicantAddress county`, + state: `application ${position} applicantAddress state`, + street: `application ${position} applicantAddress street`, + street2: `application ${position} applicantAddress street2`, + zipCode: `application ${position} applicantAddress zipCode`, + latitude: position, + longitude: position, + }, + }, + createdAt: date, + updatedAt: date, + }; + }; + + const mockApplicationSet = (numberToCreate: number, date: Date) => { + const toReturn = []; + for (let i = 0; i < numberToCreate; i++) { + toReturn.push(mockApplication(i, date)); + } + return toReturn; + }; + + beforeAll(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [ApplicationService, PrismaService], + }).compile(); + + service = module.get(ApplicationService); + prisma = module.get(PrismaService); + }); + + it('should get applications from list() when applications are available', async () => { + const date = new Date(); + const mockedValue = mockApplicationSet(3, date); + prisma.applications.findMany = jest.fn().mockResolvedValue(mockedValue); + prisma.applications.count = jest.fn().mockResolvedValue(3); + prisma.applications.findFirst = jest.fn().mockResolvedValue({ + id: 'example id', + }); + + const params: ApplicationQueryParams = { + orderBy: ApplicationOrderByKeys.createdAt, + order: OrderByEnum.ASC, + listingId: 'example listing id', + limit: 3, + page: 1, + }; + + expect(await service.list(params)).toEqual({ + items: mockedValue.map((mock) => ({ ...mock, flagged: true })), + meta: { + currentPage: 1, + itemCount: 3, + itemsPerPage: 3, + totalItems: 3, + totalPages: 1, + }, + }); + + expect(prisma.applications.count).toHaveBeenCalledWith({ + where: { + AND: [ + { + listingId: 'example listing id', + }, + ], + }, + }); + + expect(prisma.applications.findFirst).toHaveBeenNthCalledWith(1, { + select: { + id: true, + }, + where: { + id: mockedValue[0].id, + applicationFlaggedSet: { + some: {}, + }, + }, + }); + expect(prisma.applications.findFirst).toHaveBeenNthCalledWith(2, { + select: { + id: true, + }, + where: { + id: mockedValue[1].id, + applicationFlaggedSet: { + some: {}, + }, + }, + }); + expect(prisma.applications.findFirst).toHaveBeenNthCalledWith(3, { + select: { + id: true, + }, + where: { + id: mockedValue[2].id, + applicationFlaggedSet: { + some: {}, + }, + }, + }); + }); + + it('should get an application when findOne() is called and Id exists', async () => { + const date = new Date(); + const mockedValue = mockApplication(3, date); + prisma.applications.findFirst = jest.fn().mockResolvedValue(mockedValue); + + expect(await service.findOne('example Id')).toEqual(mockedValue); + + expect(prisma.applications.findFirst).toHaveBeenCalledWith({ + where: { + id: { + equals: 'example Id', + }, + }, + include: { + userAccounts: true, + applicant: { + include: { + applicantAddress: true, + applicantWorkAddress: true, + }, + }, + applicationsMailingAddress: true, + applicationsAlternateAddress: true, + alternateContact: { + include: { + address: true, + }, + }, + accessibility: true, + demographics: true, + householdMember: { + include: { + householdMemberAddress: true, + householdMemberWorkAddress: true, + }, + }, + preferredUnitTypes: true, + }, + }); + }); + + it("should throw error when findOne() is called and Id doens't exists", async () => { + prisma.applications.findFirst = jest.fn().mockResolvedValue(null); + + await expect( + async () => await service.findOne('example Id'), + ).rejects.toThrowError( + 'applicationId example Id was requested but not found', + ); + + expect(prisma.applications.findFirst).toHaveBeenCalledWith({ + where: { + id: { + equals: 'example Id', + }, + }, + include: { + userAccounts: true, + applicant: { + include: { + applicantAddress: true, + applicantWorkAddress: true, + }, + }, + applicationsMailingAddress: true, + applicationsAlternateAddress: true, + alternateContact: { + include: { + address: true, + }, + }, + accessibility: true, + demographics: true, + householdMember: { + include: { + householdMemberAddress: true, + householdMemberWorkAddress: true, + }, + }, + preferredUnitTypes: true, + }, + }); + }); + + it('should get record from getDuplicateFlagsForApplication()', async () => { + prisma.applications.findFirst = jest + .fn() + .mockResolvedValue({ id: 'example id' }); + + const res = await service.getDuplicateFlagsForApplication('example id'); + + expect(prisma.applications.findFirst).toHaveBeenCalledWith({ + select: { + id: true, + }, + where: { + id: 'example id', + applicationFlaggedSet: { + some: {}, + }, + }, + }); + + expect(res).toEqual({ id: 'example id' }); + }); + + it('should return no filters when no params passed to buildWhereClause()', () => { + const res = service.buildWhereClause({}); + expect(res).toEqual({ AND: [] }); + }); + + it('should return userId filter when userId param passed to buildWhereClause()', () => { + const res = service.buildWhereClause({ + userId: 'example user id', + }); + expect(res).toEqual({ + AND: [ + { + userAccounts: { + id: 'example user id', + }, + }, + ], + }); + }); + + it('should return listingId filter when listingId param passed to buildWhereClause()', () => { + const res = service.buildWhereClause({ + listingId: 'example listing id', + }); + expect(res).toEqual({ + AND: [ + { + listingId: 'example listing id', + }, + ], + }); + }); + + it('should return markedAsDuplicate filter when markedAsDuplicate param passed to buildWhereClause()', () => { + const res = service.buildWhereClause({ + markedAsDuplicate: false, + }); + expect(res).toEqual({ + AND: [ + { + markedAsDuplicate: false, + }, + ], + }); + }); + + it('should return mixed filters when several params passed to buildWhereClause()', () => { + const res = service.buildWhereClause({ + userId: 'example user id', + listingId: 'example listing id', + markedAsDuplicate: false, + }); + expect(res).toEqual({ + AND: [ + { + userAccounts: { + id: 'example user id', + }, + }, + { + listingId: 'example listing id', + }, + { + markedAsDuplicate: false, + }, + ], + }); + }); + + it('should return search filter when search param passed to buildWhereClause()', () => { + const res = service.buildWhereClause({ + search: 'test', + }); + const searchFilter = { contains: 'test', mode: 'insensitive' }; + expect(res).toEqual({ + AND: [ + { + OR: [ + { + confirmationCode: searchFilter, + }, + { + applicant: { + firstName: searchFilter, + }, + }, + { + applicant: { + lastName: searchFilter, + }, + }, + { + applicant: { + emailAddress: searchFilter, + }, + }, + { + applicant: { + phoneNumber: searchFilter, + }, + }, + { + alternateContact: { + firstName: searchFilter, + }, + }, + { + alternateContact: { + lastName: searchFilter, + }, + }, + { + alternateContact: { + emailAddress: searchFilter, + }, + }, + { + alternateContact: { + phoneNumber: searchFilter, + }, + }, + ], + }, + ], + }); + }); +}); diff --git a/backend_new/test/unit/services/unit-rent-type.service.spec.ts b/backend_new/test/unit/services/unit-rent-type.service.spec.ts index ea035392a3..894b1a2adb 100644 --- a/backend_new/test/unit/services/unit-rent-type.service.spec.ts +++ b/backend_new/test/unit/services/unit-rent-type.service.spec.ts @@ -4,10 +4,7 @@ import { UnitRentTypeService } from '../../../src/services/unit-rent-type.servic import { UnitRentTypeCreate } from '../../../src/dtos/unit-rent-types/unit-rent-type-create.dto'; import { UnitRentTypeUpdate } from '../../../src/dtos/unit-rent-types/unit-rent-type-update.dto'; import { randomUUID } from 'crypto'; -import { - unitRentTypeArray, - unitRentTypeFactory, -} from '../../../prisma/seed-helpers/unit-rent-type-factory'; +import { unitRentTypeArray } from '../../../prisma/seed-helpers/unit-rent-type-factory'; import { UnitRentTypeEnum } from '@prisma/client'; describe('Testing unit rent type service', () => { diff --git a/backend_new/types/src/backend-swagger.ts b/backend_new/types/src/backend-swagger.ts index e766b5a9bd..6a934d8552 100644 --- a/backend_new/types/src/backend-swagger.ts +++ b/backend_new/types/src/backend-swagger.ts @@ -1205,6 +1205,84 @@ export class MultiselectQuestionsService { } } +export class ApplicationsService { + /** + * Get a paginated set of applications + */ + list( + params: { + /** */ + page?: number; + /** */ + limit?: number | 'all'; + /** */ + listingId?: string; + /** */ + search?: string; + /** */ + userId?: string; + /** */ + orderBy?: ApplicationOrderByKeys; + /** */ + order?: OrderByEnum; + /** */ + markedAsDuplicate?: boolean; + } = {} as any, + options: IRequestOptions = {}, + ): Promise { + return new Promise((resolve, reject) => { + let url = basePath + '/applications'; + + const configs: IRequestConfig = getConfigs( + 'get', + 'application/json', + url, + options, + ); + configs.params = { + page: params['page'], + limit: params['limit'], + listingId: params['listingId'], + search: params['search'], + userId: params['userId'], + orderBy: params['orderBy'], + order: params['order'], + markedAsDuplicate: params['markedAsDuplicate'], + }; + + /** 适配ios13getèŻ·æ±‚äžć…èźžćžŠbody */ + + axios(configs, resolve, reject); + }); + } + /** + * Get application by id + */ + retrieve( + params: { + /** */ + applicationId: string; + } = {} as any, + options: IRequestOptions = {}, + ): Promise { + return new Promise((resolve, reject) => { + let url = basePath + '/applications/{applicationId}'; + url = url.replace('{applicationId}', params['applicationId'] + ''); + + const configs: IRequestConfig = getConfigs( + 'get', + 'application/json', + url, + options, + ); + + /** 适配ios13getèŻ·æ±‚äžć…èźžćžŠbody */ + + axios(configs, resolve, reject); + }); + } +} + export interface SuccessDTO { /** */ success: boolean; @@ -2177,6 +2255,255 @@ export interface MultiselectQuestionQueryParams { filter?: MultiselectQuestionFilterParams[]; } +export interface Accessibility { + /** */ + id: string; + + /** */ + createdAt: Date; + + /** */ + updatedAt: Date; + + /** */ + mobility: boolean; + + /** */ + vision: boolean; + + /** */ + hearing: boolean; +} + +export interface Demographic { + /** */ + id: string; + + /** */ + createdAt: Date; + + /** */ + updatedAt: Date; + + /** */ + ethnicity: string; + + /** */ + gender: string; + + /** */ + sexualOrientation: string; + + /** */ + howDidYouHear: string[]; + + /** */ + race: string[]; +} + +export interface Applicant { + /** */ + id: string; + + /** */ + createdAt: Date; + + /** */ + updatedAt: Date; + + /** */ + firstName: string; + + /** */ + middleName: string; + + /** */ + lastName: string; + + /** */ + birthMonth: string; + + /** */ + birthDay: string; + + /** */ + birthYear: string; + + /** */ + emailAddress: string; + + /** */ + noEmail: boolean; + + /** */ + phoneNumber: string; + + /** */ + phoneNumberType: string; + + /** */ + noPhone: boolean; + + /** */ + workInRegion: YesNoEnum; + + /** */ + applicantWorkAddress: Address; + + /** */ + applicantAddress: Address; +} + +export interface AlternateContact { + /** */ + id: string; + + /** */ + createdAt: Date; + + /** */ + updatedAt: Date; + + /** */ + type: string; + + /** */ + otherType: string; + + /** */ + firstName: string; + + /** */ + lastName: string; + + /** */ + agency: string; + + /** */ + phoneNumber: string; + + /** */ + emailAddress: string; + + /** */ + address: Address; +} + +export interface Application { + /** */ + id: string; + + /** */ + createdAt: Date; + + /** */ + updatedAt: Date; + + /** */ + deletedAt: Date; + + /** */ + appUrl: string; + + /** */ + additionalPhone: boolean; + + /** */ + additionalPhoneNumber: string; + + /** */ + additionalPhoneNumberType: string; + + /** */ + contactPreferences: string[]; + + /** */ + householdSize: number; + + /** */ + housingStatus: string; + + /** */ + sendMailToMailingAddress: boolean; + + /** */ + householdExpectingChanges: boolean; + + /** */ + householdStudent: boolean; + + /** */ + incomeVouchers: boolean; + + /** */ + income: string; + + /** */ + incomePeriod: IncomePeriodEnum; + + /** */ + status: ApplicationStatusEnum; + + /** */ + language: LanguagesEnum; + + /** */ + acceptedTerms: boolean; + + /** */ + submissionType: ApplicationSubmissionTypeEnum; + + /** */ + submissionDate: Date; + + /** */ + markedAsDuplicate: boolean; + + /** */ + flagged: boolean; + + /** */ + confirmationCode: string; + + /** */ + reviewStatus: ApplicationReviewStatusEnum; + + /** */ + applicationsMailingAddress: Address; + + /** */ + applicationsAlternateAddress: Address; + + /** */ + accessibility: Accessibility; + + /** */ + demographics: Demographic; + + /** */ + preferredUnitTypes: string[]; + + /** */ + applicant: Applicant; + + /** */ + alternateContact: AlternateContact; + + /** */ + householdMember: string[]; + + /** */ + preferences: string[]; + + /** */ + programs: string[]; +} + +export interface PaginatedApplication { + /** */ + items: Application[]; +} + export enum ListingViews { 'fundamentals' = 'fundamentals', 'base' = 'base', @@ -2263,3 +2590,50 @@ export enum EnumMultiselectQuestionFilterParamsComparison { '<=' = '<=', 'NA' = 'NA', } +export enum ApplicationOrderByKeys { + 'firstName' = 'firstName', + 'lastName' = 'lastName', + 'submissionDate' = 'submissionDate', + 'createdAt' = 'createdAt', +} + +export enum OrderByEnum { + 'asc' = 'asc', + 'desc' = 'desc', +} + +export enum IncomePeriodEnum { + 'perMonth' = 'perMonth', + 'perYear' = 'perYear', +} + +export enum ApplicationStatusEnum { + 'draft' = 'draft', + 'submitted' = 'submitted', + 'removed' = 'removed', +} + +export enum LanguagesEnum { + 'en' = 'en', + 'es' = 'es', + 'vi' = 'vi', + 'zh' = 'zh', + 'tl' = 'tl', +} + +export enum ApplicationSubmissionTypeEnum { + 'paper' = 'paper', + 'electronical' = 'electronical', +} + +export enum ApplicationReviewStatusEnum { + 'pending' = 'pending', + 'pendingAndValid' = 'pendingAndValid', + 'valid' = 'valid', + 'duplicate' = 'duplicate', +} + +export enum YesNoEnum { + 'yes' = 'yes', + 'no' = 'no', +} diff --git a/backend_new/yarn.lock b/backend_new/yarn.lock index 914dd714fb..ee3b58f6a5 100644 --- a/backend_new/yarn.lock +++ b/backend_new/yarn.lock @@ -75,12 +75,45 @@ dependencies: "@babel/highlight" "^7.18.6" +"@babel/code-frame@^7.22.5": + version "7.22.5" + resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.22.5.tgz#234d98e1551960604f1246e6475891a570ad5658" + integrity sha512-Xmwn266vad+6DAqEB2A6V/CcZVp62BbwVmcOJc2RPuwih1kw02TjQvWVWlcKGbBPd+8/0V5DEkOcizRGYsspYQ== + dependencies: + "@babel/highlight" "^7.22.5" + "@babel/compat-data@^7.21.5": version "7.21.7" resolved "https://registry.yarnpkg.com/@babel/compat-data/-/compat-data-7.21.7.tgz#61caffb60776e49a57ba61a88f02bedd8714f6bc" integrity sha512-KYMqFYTaenzMK4yUtf4EW9wc4N9ef80FsbMtkwool5zpwl4YrT1SdWYSTRcT94KO4hannogdS+LxY7L+arP3gA== -"@babel/core@^7.1.0", "@babel/core@^7.12.3", "@babel/core@^7.7.2", "@babel/core@^7.8.0": +"@babel/compat-data@^7.22.9": + version "7.22.9" + resolved "https://registry.yarnpkg.com/@babel/compat-data/-/compat-data-7.22.9.tgz#71cdb00a1ce3a329ce4cbec3a44f9fef35669730" + integrity sha512-5UamI7xkUcJ3i9qVDS+KFDEK8/7oJ55/sJMB1Ge7IEapr7KfdfV/HErR+koZwOfd+SgtFKOKRhRakdg++DcJpQ== + +"@babel/core@^7.11.6": + version "7.22.9" + resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.22.9.tgz#bd96492c68822198f33e8a256061da3cf391f58f" + integrity sha512-G2EgeufBcYw27U4hhoIwFcgc1XU7TlXJ3mv04oOv1WCuo900U/anZSPzEqNjwdjgffkk2Gs0AN0dW1CKVLcG7w== + dependencies: + "@ampproject/remapping" "^2.2.0" + "@babel/code-frame" "^7.22.5" + "@babel/generator" "^7.22.9" + "@babel/helper-compilation-targets" "^7.22.9" + "@babel/helper-module-transforms" "^7.22.9" + "@babel/helpers" "^7.22.6" + "@babel/parser" "^7.22.7" + "@babel/template" "^7.22.5" + "@babel/traverse" "^7.22.8" + "@babel/types" "^7.22.5" + convert-source-map "^1.7.0" + debug "^4.1.0" + gensync "^1.0.0-beta.2" + json5 "^2.2.2" + semver "^6.3.1" + +"@babel/core@^7.12.3": version "7.21.8" resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.21.8.tgz#2a8c7f0f53d60100ba4c32470ba0281c92aa9aa4" integrity sha512-YeM22Sondbo523Sz0+CirSPnbj9bG3P0CdHcBZdqUuaeOaYEFbOLoGU7lebvGP6P5J/WE9wOn7u7C4J9HvS1xQ== @@ -111,6 +144,16 @@ "@jridgewell/trace-mapping" "^0.3.17" jsesc "^2.5.1" +"@babel/generator@^7.22.7", "@babel/generator@^7.22.9": + version "7.22.9" + resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.22.9.tgz#572ecfa7a31002fa1de2a9d91621fd895da8493d" + integrity sha512-KtLMbmicyuK2Ak/FTCJVbDnkN1SlT8/kceFTiuDiiRUUSMnHMidxSCdG4ndkTOHHpoomWe/4xkvHkEOncwjYIw== + dependencies: + "@babel/types" "^7.22.5" + "@jridgewell/gen-mapping" "^0.3.2" + "@jridgewell/trace-mapping" "^0.3.17" + jsesc "^2.5.1" + "@babel/helper-compilation-targets@^7.21.5": version "7.21.5" resolved "https://registry.yarnpkg.com/@babel/helper-compilation-targets/-/helper-compilation-targets-7.21.5.tgz#631e6cc784c7b660417421349aac304c94115366" @@ -122,11 +165,27 @@ lru-cache "^5.1.1" semver "^6.3.0" +"@babel/helper-compilation-targets@^7.22.9": + version "7.22.9" + resolved "https://registry.yarnpkg.com/@babel/helper-compilation-targets/-/helper-compilation-targets-7.22.9.tgz#f9d0a7aaaa7cd32a3f31c9316a69f5a9bcacb892" + integrity sha512-7qYrNM6HjpnPHJbopxmb8hSPoZ0gsX8IvUS32JGVoy+pU9e5N0nLr1VjJoR6kA4d9dmGLxNYOjeB8sUDal2WMw== + dependencies: + "@babel/compat-data" "^7.22.9" + "@babel/helper-validator-option" "^7.22.5" + browserslist "^4.21.9" + lru-cache "^5.1.1" + semver "^6.3.1" + "@babel/helper-environment-visitor@^7.21.5": version "7.21.5" resolved "https://registry.yarnpkg.com/@babel/helper-environment-visitor/-/helper-environment-visitor-7.21.5.tgz#c769afefd41d171836f7cb63e295bedf689d48ba" integrity sha512-IYl4gZ3ETsWocUWgsFZLM5i1BYx9SoemminVEXadgLBa9TdeorzgLKm8wWLA6J1N/kT3Kch8XIk1laNzYoHKvQ== +"@babel/helper-environment-visitor@^7.22.5": + version "7.22.5" + resolved "https://registry.yarnpkg.com/@babel/helper-environment-visitor/-/helper-environment-visitor-7.22.5.tgz#f06dd41b7c1f44e1f8da6c4055b41ab3a09a7e98" + integrity sha512-XGmhECfVA/5sAt+H+xpSg0mfrHq6FzNr9Oxh7PSEBBRUb/mL7Kz3NICXb194rCqAEdxkhPT1a88teizAFyvk8Q== + "@babel/helper-function-name@^7.21.0": version "7.21.0" resolved "https://registry.yarnpkg.com/@babel/helper-function-name/-/helper-function-name-7.21.0.tgz#d552829b10ea9f120969304023cd0645fa00b1b4" @@ -135,6 +194,14 @@ "@babel/template" "^7.20.7" "@babel/types" "^7.21.0" +"@babel/helper-function-name@^7.22.5": + version "7.22.5" + resolved "https://registry.yarnpkg.com/@babel/helper-function-name/-/helper-function-name-7.22.5.tgz#ede300828905bb15e582c037162f99d5183af1be" + integrity sha512-wtHSq6jMRE3uF2otvfuD3DIvVhOsSNshQl0Qrd7qC9oQJzHvOL4qQXlQn2916+CXGywIjpGuIkoyZRRxHPiNQQ== + dependencies: + "@babel/template" "^7.22.5" + "@babel/types" "^7.22.5" + "@babel/helper-hoist-variables@^7.18.6": version "7.18.6" resolved "https://registry.yarnpkg.com/@babel/helper-hoist-variables/-/helper-hoist-variables-7.18.6.tgz#d4d2c8fb4baeaa5c68b99cc8245c56554f926678" @@ -142,6 +209,13 @@ dependencies: "@babel/types" "^7.18.6" +"@babel/helper-hoist-variables@^7.22.5": + version "7.22.5" + resolved "https://registry.yarnpkg.com/@babel/helper-hoist-variables/-/helper-hoist-variables-7.22.5.tgz#c01a007dac05c085914e8fb652b339db50d823bb" + integrity sha512-wGjk9QZVzvknA6yKIUURb8zY3grXCcOZt+/7Wcy8O2uctxhplmUPkOdlgoNhmdVee2c92JXbf1xpMtVNbfoxRw== + dependencies: + "@babel/types" "^7.22.5" + "@babel/helper-module-imports@^7.21.4": version "7.21.4" resolved "https://registry.yarnpkg.com/@babel/helper-module-imports/-/helper-module-imports-7.21.4.tgz#ac88b2f76093637489e718a90cec6cf8a9b029af" @@ -149,6 +223,13 @@ dependencies: "@babel/types" "^7.21.4" +"@babel/helper-module-imports@^7.22.5": + version "7.22.5" + resolved "https://registry.yarnpkg.com/@babel/helper-module-imports/-/helper-module-imports-7.22.5.tgz#1a8f4c9f4027d23f520bd76b364d44434a72660c" + integrity sha512-8Dl6+HD/cKifutF5qGd/8ZJi84QeAKh+CEe1sBzz8UayBBGg1dAIJrdHOcOM5b2MpzWL2yuotJTtGjETq0qjXg== + dependencies: + "@babel/types" "^7.22.5" + "@babel/helper-module-transforms@^7.21.5": version "7.21.5" resolved "https://registry.yarnpkg.com/@babel/helper-module-transforms/-/helper-module-transforms-7.21.5.tgz#d937c82e9af68d31ab49039136a222b17ac0b420" @@ -163,11 +244,27 @@ "@babel/traverse" "^7.21.5" "@babel/types" "^7.21.5" +"@babel/helper-module-transforms@^7.22.9": + version "7.22.9" + resolved "https://registry.yarnpkg.com/@babel/helper-module-transforms/-/helper-module-transforms-7.22.9.tgz#92dfcb1fbbb2bc62529024f72d942a8c97142129" + integrity sha512-t+WA2Xn5K+rTeGtC8jCsdAH52bjggG5TKRuRrAGNM/mjIbO4GxvlLMFOEz9wXY5I2XQ60PMFsAG2WIcG82dQMQ== + dependencies: + "@babel/helper-environment-visitor" "^7.22.5" + "@babel/helper-module-imports" "^7.22.5" + "@babel/helper-simple-access" "^7.22.5" + "@babel/helper-split-export-declaration" "^7.22.6" + "@babel/helper-validator-identifier" "^7.22.5" + "@babel/helper-plugin-utils@^7.0.0", "@babel/helper-plugin-utils@^7.10.4", "@babel/helper-plugin-utils@^7.12.13", "@babel/helper-plugin-utils@^7.14.5", "@babel/helper-plugin-utils@^7.20.2", "@babel/helper-plugin-utils@^7.8.0": version "7.21.5" resolved "https://registry.yarnpkg.com/@babel/helper-plugin-utils/-/helper-plugin-utils-7.21.5.tgz#345f2377d05a720a4e5ecfa39cbf4474a4daed56" integrity sha512-0WDaIlXKOX/3KfBK/dwP1oQGiPh6rjMkT7HIRv7i5RR2VUMwrx5ZL0dwBkKx7+SW1zwNdgjHd34IMk5ZjTeHVg== +"@babel/helper-plugin-utils@^7.22.5": + version "7.22.5" + resolved "https://registry.yarnpkg.com/@babel/helper-plugin-utils/-/helper-plugin-utils-7.22.5.tgz#dd7ee3735e8a313b9f7b05a773d892e88e6d7295" + integrity sha512-uLls06UVKgFG9QD4OeFYLEGteMIAa5kpTPcFL28yuCIIzsf6ZyKZMllKVOCZFhiZ5ptnwX4mtKdWCBE/uT4amg== + "@babel/helper-simple-access@^7.21.5": version "7.21.5" resolved "https://registry.yarnpkg.com/@babel/helper-simple-access/-/helper-simple-access-7.21.5.tgz#d697a7971a5c39eac32c7e63c0921c06c8a249ee" @@ -175,6 +272,13 @@ dependencies: "@babel/types" "^7.21.5" +"@babel/helper-simple-access@^7.22.5": + version "7.22.5" + resolved "https://registry.yarnpkg.com/@babel/helper-simple-access/-/helper-simple-access-7.22.5.tgz#4938357dc7d782b80ed6dbb03a0fba3d22b1d5de" + integrity sha512-n0H99E/K+Bika3++WNL17POvo4rKWZ7lZEp1Q+fStVbUi8nxPQEBOlTmCOxW/0JsS56SKKQ+ojAe2pHKJHN35w== + dependencies: + "@babel/types" "^7.22.5" + "@babel/helper-split-export-declaration@^7.18.6": version "7.18.6" resolved "https://registry.yarnpkg.com/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.18.6.tgz#7367949bc75b20c6d5a5d4a97bba2824ae8ef075" @@ -182,21 +286,43 @@ dependencies: "@babel/types" "^7.18.6" +"@babel/helper-split-export-declaration@^7.22.6": + version "7.22.6" + resolved "https://registry.yarnpkg.com/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.22.6.tgz#322c61b7310c0997fe4c323955667f18fcefb91c" + integrity sha512-AsUnxuLhRYsisFiaJwvp1QF+I3KjD5FOxut14q/GzovUe6orHLesW2C7d754kRm53h5gqrz6sFl6sxc4BVtE/g== + dependencies: + "@babel/types" "^7.22.5" + "@babel/helper-string-parser@^7.21.5": version "7.21.5" resolved "https://registry.yarnpkg.com/@babel/helper-string-parser/-/helper-string-parser-7.21.5.tgz#2b3eea65443c6bdc31c22d037c65f6d323b6b2bd" integrity sha512-5pTUx3hAJaZIdW99sJ6ZUUgWq/Y+Hja7TowEnLNMm1VivRgZQL3vpBY3qUACVsvw+yQU6+YgfBVmcbLaZtrA1w== +"@babel/helper-string-parser@^7.22.5": + version "7.22.5" + resolved "https://registry.yarnpkg.com/@babel/helper-string-parser/-/helper-string-parser-7.22.5.tgz#533f36457a25814cf1df6488523ad547d784a99f" + integrity sha512-mM4COjgZox8U+JcXQwPijIZLElkgEpO5rsERVDJTc2qfCDfERyob6k5WegS14SX18IIjv+XD+GrqNumY5JRCDw== + "@babel/helper-validator-identifier@^7.18.6", "@babel/helper-validator-identifier@^7.19.1": version "7.19.1" resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.19.1.tgz#7eea834cf32901ffdc1a7ee555e2f9c27e249ca2" integrity sha512-awrNfaMtnHUr653GgGEs++LlAvW6w+DcPrOliSMXWCKo597CwL5Acf/wWdNkf/tfEQE3mjkeD1YOVZOUV/od1w== +"@babel/helper-validator-identifier@^7.22.5": + version "7.22.5" + resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.5.tgz#9544ef6a33999343c8740fa51350f30eeaaaf193" + integrity sha512-aJXu+6lErq8ltp+JhkJUfk1MTGyuA4v7f3pA+BJ5HLfNC6nAQ0Cpi9uOquUj8Hehg0aUiHzWQbOVJGao6ztBAQ== + "@babel/helper-validator-option@^7.21.0": version "7.21.0" resolved "https://registry.yarnpkg.com/@babel/helper-validator-option/-/helper-validator-option-7.21.0.tgz#8224c7e13ace4bafdc4004da2cf064ef42673180" integrity sha512-rmL/B8/f0mKS2baE9ZpyTcTavvEuWhTTW8amjzXNvYG4AwBsqTLikfXsEofsJEfKHf+HQVQbFOHy6o+4cnC/fQ== +"@babel/helper-validator-option@^7.22.5": + version "7.22.5" + resolved "https://registry.yarnpkg.com/@babel/helper-validator-option/-/helper-validator-option-7.22.5.tgz#de52000a15a177413c8234fa3a8af4ee8102d0ac" + integrity sha512-R3oB6xlIVKUnxNUxbmgq7pKjxpru24zlimpE8WK47fACIlM0II/Hm1RS8IaOI7NgCr6LNS+jl5l75m20npAziw== + "@babel/helpers@^7.21.5": version "7.21.5" resolved "https://registry.yarnpkg.com/@babel/helpers/-/helpers-7.21.5.tgz#5bac66e084d7a4d2d9696bdf0175a93f7fb63c08" @@ -206,6 +332,15 @@ "@babel/traverse" "^7.21.5" "@babel/types" "^7.21.5" +"@babel/helpers@^7.22.6": + version "7.22.6" + resolved "https://registry.yarnpkg.com/@babel/helpers/-/helpers-7.22.6.tgz#8e61d3395a4f0c5a8060f309fb008200969b5ecd" + integrity sha512-YjDs6y/fVOYFV8hAf1rxd1QvR9wJe1pDBZ2AREKq/SDayfPzgk0PBnVuTCE5X1acEpMMNOVUqoe+OwiZGJ+OaA== + dependencies: + "@babel/template" "^7.22.5" + "@babel/traverse" "^7.22.6" + "@babel/types" "^7.22.5" + "@babel/highlight@^7.18.6": version "7.18.6" resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.18.6.tgz#81158601e93e2563795adcbfbdf5d64be3f2ecdf" @@ -215,6 +350,15 @@ chalk "^2.0.0" js-tokens "^4.0.0" +"@babel/highlight@^7.22.5": + version "7.22.5" + resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.22.5.tgz#aa6c05c5407a67ebce408162b7ede789b4d22031" + integrity sha512-BSKlD1hgnedS5XRnGOljZawtag7H1yPfQp0tdNJCHoH6AZ+Pcm9VvkrK59/Yy593Ypg0zMxH2BxD1VPYUQ7UIw== + dependencies: + "@babel/helper-validator-identifier" "^7.22.5" + chalk "^2.0.0" + js-tokens "^4.0.0" + "@babel/parser@^7.1.0", "@babel/parser@^7.14.7", "@babel/parser@^7.20.7", "@babel/parser@^7.21.5", "@babel/parser@^7.21.8": version "7.21.8" resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.21.8.tgz#642af7d0333eab9c0ad70b14ac5e76dbde7bfdf8" @@ -225,6 +369,11 @@ resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.22.5.tgz#721fd042f3ce1896238cf1b341c77eb7dee7dbea" integrity sha512-DFZMC9LJUG9PLOclRC32G63UXwzqS2koQC8dkx+PLdmt1xSePYpbT/NbsrJy8Q/muXz7o/h/d4A7Fuyixm559Q== +"@babel/parser@^7.22.5", "@babel/parser@^7.22.7": + version "7.22.7" + resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.22.7.tgz#df8cf085ce92ddbdbf668a7f186ce848c9036cae" + integrity sha512-7NF8pOkHP5o2vpmGgNGcfAeCvOYhGLyA3Z4eBQkT1RJlWu47n63bCs93QfJ2hIAFCil7L5P2IWhs1oToVgrL0Q== + "@babel/plugin-syntax-async-generators@^7.8.4": version "7.8.4" resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-async-generators/-/plugin-syntax-async-generators-7.8.4.tgz#a983fb1aeb2ec3f6ed042a210f640e90e786fe0d" @@ -260,6 +409,13 @@ dependencies: "@babel/helper-plugin-utils" "^7.8.0" +"@babel/plugin-syntax-jsx@^7.7.2": + version "7.22.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.22.5.tgz#a6b68e84fb76e759fc3b93e901876ffabbe1d918" + integrity sha512-gvyP4hZrgrs/wWMaocvxZ44Hw0b3W8Pe+cMxc8V1ULQ07oh8VNbIRaoD1LRZVTvD+0nieDKjfgKg89sD7rrKrg== + dependencies: + "@babel/helper-plugin-utils" "^7.22.5" + "@babel/plugin-syntax-logical-assignment-operators@^7.8.3": version "7.10.4" resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-logical-assignment-operators/-/plugin-syntax-logical-assignment-operators-7.10.4.tgz#ca91ef46303530448b906652bac2e9fe9941f699" @@ -325,7 +481,16 @@ "@babel/parser" "^7.20.7" "@babel/types" "^7.20.7" -"@babel/traverse@^7.21.5", "@babel/traverse@^7.7.2": +"@babel/template@^7.22.5": + version "7.22.5" + resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.22.5.tgz#0c8c4d944509875849bd0344ff0050756eefc6ec" + integrity sha512-X7yV7eiwAxdj9k94NEylvbVHLiVG1nvzCV2EAowhxLTwODV1jl9UzZ48leOC0sH7OnuHrIkllaBgneUykIcZaw== + dependencies: + "@babel/code-frame" "^7.22.5" + "@babel/parser" "^7.22.5" + "@babel/types" "^7.22.5" + +"@babel/traverse@^7.21.5": version "7.21.5" resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.21.5.tgz#ad22361d352a5154b498299d523cf72998a4b133" integrity sha512-AhQoI3YjWi6u/y/ntv7k48mcrCXmus0t79J9qPNlk/lAsFlCiJ047RmbfMOawySTHtywXhbXgpx/8nXMYd+oFw== @@ -341,6 +506,22 @@ debug "^4.1.0" globals "^11.1.0" +"@babel/traverse@^7.22.6", "@babel/traverse@^7.22.8": + version "7.22.8" + resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.22.8.tgz#4d4451d31bc34efeae01eac222b514a77aa4000e" + integrity sha512-y6LPR+wpM2I3qJrsheCTwhIinzkETbplIgPBbwvqPKc+uljeA5gP+3nP8irdYt1mjQaDnlIcG+dw8OjAco4GXw== + dependencies: + "@babel/code-frame" "^7.22.5" + "@babel/generator" "^7.22.7" + "@babel/helper-environment-visitor" "^7.22.5" + "@babel/helper-function-name" "^7.22.5" + "@babel/helper-hoist-variables" "^7.22.5" + "@babel/helper-split-export-declaration" "^7.22.6" + "@babel/parser" "^7.22.7" + "@babel/types" "^7.22.5" + debug "^4.1.0" + globals "^11.1.0" + "@babel/types@^7.0.0", "@babel/types@^7.18.6", "@babel/types@^7.20.7", "@babel/types@^7.21.0", "@babel/types@^7.21.4", "@babel/types@^7.21.5", "@babel/types@^7.3.0", "@babel/types@^7.3.3": version "7.21.5" resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.21.5.tgz#18dfbd47c39d3904d5db3d3dc2cc80bedb60e5b6" @@ -350,6 +531,15 @@ "@babel/helper-validator-identifier" "^7.19.1" to-fast-properties "^2.0.0" +"@babel/types@^7.22.5": + version "7.22.5" + resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.22.5.tgz#cd93eeaab025880a3a47ec881f4b096a5b786fbe" + integrity sha512-zo3MIHGOkPOfoRXitsgHLjEXmlDaD/5KU1Uzuc9GNiZPhSqVxVRtxuPaSBZDsYZ9qV88AjtMtWW7ww98loJ9KA== + dependencies: + "@babel/helper-string-parser" "^7.22.5" + "@babel/helper-validator-identifier" "^7.22.5" + to-fast-properties "^2.0.0" + "@bcoe/v8-coverage@^0.2.3": version "0.2.3" resolved "https://registry.yarnpkg.com/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz#75a2e8b51cb758a7553d6804a5932d7aace75c39" @@ -490,173 +680,196 @@ resolved "https://registry.yarnpkg.com/@istanbuljs/schema/-/schema-0.1.3.tgz#e45e384e4b8ec16bce2fd903af78450f6bf7ec98" integrity sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA== -"@jest/console@^27.5.1": - version "27.5.1" - resolved "https://registry.yarnpkg.com/@jest/console/-/console-27.5.1.tgz#260fe7239602fe5130a94f1aa386eff54b014bba" - integrity sha512-kZ/tNpS3NXn0mlXXXPNuDZnb4c0oZ20r4K5eemM2k30ZC3G0T02nXUvyhf5YdbXWHPEJLc9qGLxEZ216MdL+Zg== +"@jest/console@^29.6.2": + version "29.6.2" + resolved "https://registry.yarnpkg.com/@jest/console/-/console-29.6.2.tgz#bf1d4101347c23e07c029a1b1ae07d550f5cc541" + integrity sha512-0N0yZof5hi44HAR2pPS+ikJ3nzKNoZdVu8FffRf3wy47I7Dm7etk/3KetMdRUqzVd16V4O2m2ISpNTbnIuqy1w== dependencies: - "@jest/types" "^27.5.1" + "@jest/types" "^29.6.1" "@types/node" "*" chalk "^4.0.0" - jest-message-util "^27.5.1" - jest-util "^27.5.1" + jest-message-util "^29.6.2" + jest-util "^29.6.2" slash "^3.0.0" -"@jest/core@^27.5.1": - version "27.5.1" - resolved "https://registry.yarnpkg.com/@jest/core/-/core-27.5.1.tgz#267ac5f704e09dc52de2922cbf3af9edcd64b626" - integrity sha512-AK6/UTrvQD0Cd24NSqmIA6rKsu0tKIxfiCducZvqxYdmMisOYAsdItspT+fQDQYARPf8XgjAFZi0ogW2agH5nQ== - dependencies: - "@jest/console" "^27.5.1" - "@jest/reporters" "^27.5.1" - "@jest/test-result" "^27.5.1" - "@jest/transform" "^27.5.1" - "@jest/types" "^27.5.1" +"@jest/core@^29.6.2": + version "29.6.2" + resolved "https://registry.yarnpkg.com/@jest/core/-/core-29.6.2.tgz#6f2d1dbe8aa0265fcd4fb8082ae1952f148209c8" + integrity sha512-Oj+5B+sDMiMWLhPFF+4/DvHOf+U10rgvCLGPHP8Xlsy/7QxS51aU/eBngudHlJXnaWD5EohAgJ4js+T6pa+zOg== + dependencies: + "@jest/console" "^29.6.2" + "@jest/reporters" "^29.6.2" + "@jest/test-result" "^29.6.2" + "@jest/transform" "^29.6.2" + "@jest/types" "^29.6.1" "@types/node" "*" ansi-escapes "^4.2.1" chalk "^4.0.0" - emittery "^0.8.1" + ci-info "^3.2.0" exit "^0.1.2" graceful-fs "^4.2.9" - jest-changed-files "^27.5.1" - jest-config "^27.5.1" - jest-haste-map "^27.5.1" - jest-message-util "^27.5.1" - jest-regex-util "^27.5.1" - jest-resolve "^27.5.1" - jest-resolve-dependencies "^27.5.1" - jest-runner "^27.5.1" - jest-runtime "^27.5.1" - jest-snapshot "^27.5.1" - jest-util "^27.5.1" - jest-validate "^27.5.1" - jest-watcher "^27.5.1" + jest-changed-files "^29.5.0" + jest-config "^29.6.2" + jest-haste-map "^29.6.2" + jest-message-util "^29.6.2" + jest-regex-util "^29.4.3" + jest-resolve "^29.6.2" + jest-resolve-dependencies "^29.6.2" + jest-runner "^29.6.2" + jest-runtime "^29.6.2" + jest-snapshot "^29.6.2" + jest-util "^29.6.2" + jest-validate "^29.6.2" + jest-watcher "^29.6.2" micromatch "^4.0.4" - rimraf "^3.0.0" + pretty-format "^29.6.2" slash "^3.0.0" strip-ansi "^6.0.0" -"@jest/environment@^27.5.1": - version "27.5.1" - resolved "https://registry.yarnpkg.com/@jest/environment/-/environment-27.5.1.tgz#d7425820511fe7158abbecc010140c3fd3be9c74" - integrity sha512-/WQjhPJe3/ghaol/4Bq480JKXV/Rfw8nQdN7f41fM8VDHLcxKXou6QyXAh3EFr9/bVG3x74z1NWDkP87EiY8gA== +"@jest/environment@^29.6.2": + version "29.6.2" + resolved "https://registry.yarnpkg.com/@jest/environment/-/environment-29.6.2.tgz#794c0f769d85e7553439d107d3f43186dc6874a9" + integrity sha512-AEcW43C7huGd/vogTddNNTDRpO6vQ2zaQNrttvWV18ArBx9Z56h7BIsXkNFJVOO4/kblWEQz30ckw0+L3izc+Q== dependencies: - "@jest/fake-timers" "^27.5.1" - "@jest/types" "^27.5.1" + "@jest/fake-timers" "^29.6.2" + "@jest/types" "^29.6.1" "@types/node" "*" - jest-mock "^27.5.1" + jest-mock "^29.6.2" -"@jest/fake-timers@^27.5.1": - version "27.5.1" - resolved "https://registry.yarnpkg.com/@jest/fake-timers/-/fake-timers-27.5.1.tgz#76979745ce0579c8a94a4678af7a748eda8ada74" - integrity sha512-/aPowoolwa07k7/oM3aASneNeBGCmGQsc3ugN4u6s4C/+s5M64MFo/+djTdiwcbQlRfFElGuDXWzaWj6QgKObQ== +"@jest/expect-utils@^29.6.2": + version "29.6.2" + resolved "https://registry.yarnpkg.com/@jest/expect-utils/-/expect-utils-29.6.2.tgz#1b97f290d0185d264dd9fdec7567a14a38a90534" + integrity sha512-6zIhM8go3RV2IG4aIZaZbxwpOzz3ZiM23oxAlkquOIole+G6TrbeXnykxWYlqF7kz2HlBjdKtca20x9atkEQYg== + dependencies: + jest-get-type "^29.4.3" + +"@jest/expect@^29.6.2": + version "29.6.2" + resolved "https://registry.yarnpkg.com/@jest/expect/-/expect-29.6.2.tgz#5a2ad58bb345165d9ce0a1845bbf873c480a4b28" + integrity sha512-m6DrEJxVKjkELTVAztTLyS/7C92Y2b0VYqmDROYKLLALHn8T/04yPs70NADUYPrV3ruI+H3J0iUIuhkjp7vkfg== + dependencies: + expect "^29.6.2" + jest-snapshot "^29.6.2" + +"@jest/fake-timers@^29.6.2": + version "29.6.2" + resolved "https://registry.yarnpkg.com/@jest/fake-timers/-/fake-timers-29.6.2.tgz#fe9d43c5e4b1b901168fe6f46f861b3e652a2df4" + integrity sha512-euZDmIlWjm1Z0lJ1D0f7a0/y5Kh/koLFMUBE5SUYWrmy8oNhJpbTBDAP6CxKnadcMLDoDf4waRYCe35cH6G6PA== dependencies: - "@jest/types" "^27.5.1" - "@sinonjs/fake-timers" "^8.0.1" + "@jest/types" "^29.6.1" + "@sinonjs/fake-timers" "^10.0.2" "@types/node" "*" - jest-message-util "^27.5.1" - jest-mock "^27.5.1" - jest-util "^27.5.1" + jest-message-util "^29.6.2" + jest-mock "^29.6.2" + jest-util "^29.6.2" -"@jest/globals@^27.5.1": - version "27.5.1" - resolved "https://registry.yarnpkg.com/@jest/globals/-/globals-27.5.1.tgz#7ac06ce57ab966566c7963431cef458434601b2b" - integrity sha512-ZEJNB41OBQQgGzgyInAv0UUfDDj3upmHydjieSxFvTRuZElrx7tXg/uVQ5hYVEwiXs3+aMsAeEc9X7xiSKCm4Q== +"@jest/globals@^29.6.2": + version "29.6.2" + resolved "https://registry.yarnpkg.com/@jest/globals/-/globals-29.6.2.tgz#74af81b9249122cc46f1eb25793617eec69bf21a" + integrity sha512-cjuJmNDjs6aMijCmSa1g2TNG4Lby/AeU7/02VtpW+SLcZXzOLK2GpN2nLqcFjmhy3B3AoPeQVx7BnyOf681bAw== dependencies: - "@jest/environment" "^27.5.1" - "@jest/types" "^27.5.1" - expect "^27.5.1" + "@jest/environment" "^29.6.2" + "@jest/expect" "^29.6.2" + "@jest/types" "^29.6.1" + jest-mock "^29.6.2" -"@jest/reporters@^27.5.1": - version "27.5.1" - resolved "https://registry.yarnpkg.com/@jest/reporters/-/reporters-27.5.1.tgz#ceda7be96170b03c923c37987b64015812ffec04" - integrity sha512-cPXh9hWIlVJMQkVk84aIvXuBB4uQQmFqZiacloFuGiP3ah1sbCxCosidXFDfqG8+6fO1oR2dTJTlsOy4VFmUfw== +"@jest/reporters@^29.6.2": + version "29.6.2" + resolved "https://registry.yarnpkg.com/@jest/reporters/-/reporters-29.6.2.tgz#524afe1d76da33d31309c2c4a2c8062d0c48780a" + integrity sha512-sWtijrvIav8LgfJZlrGCdN0nP2EWbakglJY49J1Y5QihcQLfy7ovyxxjJBRXMNltgt4uPtEcFmIMbVshEDfFWw== dependencies: "@bcoe/v8-coverage" "^0.2.3" - "@jest/console" "^27.5.1" - "@jest/test-result" "^27.5.1" - "@jest/transform" "^27.5.1" - "@jest/types" "^27.5.1" + "@jest/console" "^29.6.2" + "@jest/test-result" "^29.6.2" + "@jest/transform" "^29.6.2" + "@jest/types" "^29.6.1" + "@jridgewell/trace-mapping" "^0.3.18" "@types/node" "*" chalk "^4.0.0" collect-v8-coverage "^1.0.0" exit "^0.1.2" - glob "^7.1.2" + glob "^7.1.3" graceful-fs "^4.2.9" istanbul-lib-coverage "^3.0.0" istanbul-lib-instrument "^5.1.0" istanbul-lib-report "^3.0.0" istanbul-lib-source-maps "^4.0.0" istanbul-reports "^3.1.3" - jest-haste-map "^27.5.1" - jest-resolve "^27.5.1" - jest-util "^27.5.1" - jest-worker "^27.5.1" + jest-message-util "^29.6.2" + jest-util "^29.6.2" + jest-worker "^29.6.2" slash "^3.0.0" - source-map "^0.6.0" string-length "^4.0.1" - terminal-link "^2.0.0" - v8-to-istanbul "^8.1.0" + strip-ansi "^6.0.0" + v8-to-istanbul "^9.0.1" -"@jest/source-map@^27.5.1": - version "27.5.1" - resolved "https://registry.yarnpkg.com/@jest/source-map/-/source-map-27.5.1.tgz#6608391e465add4205eae073b55e7f279e04e8cf" - integrity sha512-y9NIHUYF3PJRlHk98NdC/N1gl88BL08aQQgu4k4ZopQkCw9t9cV8mtl3TV8b/YCB8XaVTFrmUTAJvjsntDireg== +"@jest/schemas@^29.6.0": + version "29.6.0" + resolved "https://registry.yarnpkg.com/@jest/schemas/-/schemas-29.6.0.tgz#0f4cb2c8e3dca80c135507ba5635a4fd755b0040" + integrity sha512-rxLjXyJBTL4LQeJW3aKo0M/+GkCOXsO+8i9Iu7eDb6KwtP65ayoDsitrdPBtujxQ88k4wI2FNYfa6TOGwSn6cQ== dependencies: + "@sinclair/typebox" "^0.27.8" + +"@jest/source-map@^29.6.0": + version "29.6.0" + resolved "https://registry.yarnpkg.com/@jest/source-map/-/source-map-29.6.0.tgz#bd34a05b5737cb1a99d43e1957020ac8e5b9ddb1" + integrity sha512-oA+I2SHHQGxDCZpbrsCQSoMLb3Bz547JnM+jUr9qEbuw0vQlWZfpPS7CO9J7XiwKicEz9OFn/IYoLkkiUD7bzA== + dependencies: + "@jridgewell/trace-mapping" "^0.3.18" callsites "^3.0.0" graceful-fs "^4.2.9" - source-map "^0.6.0" -"@jest/test-result@^27.5.1": - version "27.5.1" - resolved "https://registry.yarnpkg.com/@jest/test-result/-/test-result-27.5.1.tgz#56a6585fa80f7cdab72b8c5fc2e871d03832f5bb" - integrity sha512-EW35l2RYFUcUQxFJz5Cv5MTOxlJIQs4I7gxzi2zVU7PJhOwfYq1MdC5nhSmYjX1gmMmLPvB3sIaC+BkcHRBfag== +"@jest/test-result@^29.6.2": + version "29.6.2" + resolved "https://registry.yarnpkg.com/@jest/test-result/-/test-result-29.6.2.tgz#fdd11583cd1608e4db3114e8f0cce277bf7a32ed" + integrity sha512-3VKFXzcV42EYhMCsJQURptSqnyjqCGbtLuX5Xxb6Pm6gUf1wIRIl+mandIRGJyWKgNKYF9cnstti6Ls5ekduqw== dependencies: - "@jest/console" "^27.5.1" - "@jest/types" "^27.5.1" + "@jest/console" "^29.6.2" + "@jest/types" "^29.6.1" "@types/istanbul-lib-coverage" "^2.0.0" collect-v8-coverage "^1.0.0" -"@jest/test-sequencer@^27.5.1": - version "27.5.1" - resolved "https://registry.yarnpkg.com/@jest/test-sequencer/-/test-sequencer-27.5.1.tgz#4057e0e9cea4439e544c6353c6affe58d095745b" - integrity sha512-LCheJF7WB2+9JuCS7VB/EmGIdQuhtqjRNI9A43idHv3E4KltCTsPsLxvdaubFHSYwY/fNjMWjl6vNRhDiN7vpQ== +"@jest/test-sequencer@^29.6.2": + version "29.6.2" + resolved "https://registry.yarnpkg.com/@jest/test-sequencer/-/test-sequencer-29.6.2.tgz#585eff07a68dd75225a7eacf319780cb9f6b9bf4" + integrity sha512-GVYi6PfPwVejO7slw6IDO0qKVum5jtrJ3KoLGbgBWyr2qr4GaxFV6su+ZAjdTX75Sr1DkMFRk09r2ZVa+wtCGw== dependencies: - "@jest/test-result" "^27.5.1" + "@jest/test-result" "^29.6.2" graceful-fs "^4.2.9" - jest-haste-map "^27.5.1" - jest-runtime "^27.5.1" + jest-haste-map "^29.6.2" + slash "^3.0.0" -"@jest/transform@^27.5.1": - version "27.5.1" - resolved "https://registry.yarnpkg.com/@jest/transform/-/transform-27.5.1.tgz#6c3501dcc00c4c08915f292a600ece5ecfe1f409" - integrity sha512-ipON6WtYgl/1329g5AIJVbUuEh0wZVbdpGwC99Jw4LwuoBNS95MVphU6zOeD9pDkon+LLbFL7lOQRapbB8SCHw== +"@jest/transform@^29.6.2": + version "29.6.2" + resolved "https://registry.yarnpkg.com/@jest/transform/-/transform-29.6.2.tgz#522901ebbb211af08835bc3bcdf765ab778094e3" + integrity sha512-ZqCqEISr58Ce3U+buNFJYUktLJZOggfyvR+bZMaiV1e8B1SIvJbwZMrYz3gx/KAPn9EXmOmN+uB08yLCjWkQQg== dependencies: - "@babel/core" "^7.1.0" - "@jest/types" "^27.5.1" + "@babel/core" "^7.11.6" + "@jest/types" "^29.6.1" + "@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 "^27.5.1" - jest-regex-util "^27.5.1" - jest-util "^27.5.1" + jest-haste-map "^29.6.2" + jest-regex-util "^29.4.3" + jest-util "^29.6.2" micromatch "^4.0.4" pirates "^4.0.4" slash "^3.0.0" - source-map "^0.6.1" - write-file-atomic "^3.0.0" + write-file-atomic "^4.0.2" -"@jest/types@^27.5.1": - version "27.5.1" - resolved "https://registry.yarnpkg.com/@jest/types/-/types-27.5.1.tgz#3c79ec4a8ba61c170bf937bcf9e98a9df175ec80" - integrity sha512-Cx46iJ9QpwQTjIdq5VJu2QTMMs3QlEjI0x1QbBP5W1+nMzyc2XmimiRR/CbX9TO0cPTeUlxWMOu8mslYsJ8DEw== +"@jest/types@^29.6.1": + version "29.6.1" + resolved "https://registry.yarnpkg.com/@jest/types/-/types-29.6.1.tgz#ae79080278acff0a6af5eb49d063385aaa897bf2" + integrity sha512-tPKQNMPuXgvdOn2/Lg9HNfUvjYVGolt04Hp03f5hAk878uwOLikN+JzeLY0HcVgKgFl9Hs3EIqpu3WX27XNhnw== dependencies: + "@jest/schemas" "^29.6.0" "@types/istanbul-lib-coverage" "^2.0.0" "@types/istanbul-reports" "^3.0.0" "@types/node" "*" - "@types/yargs" "^16.0.0" + "@types/yargs" "^17.0.8" chalk "^4.0.0" "@jridgewell/gen-mapping@^0.3.0", "@jridgewell/gen-mapping@^0.3.2": @@ -709,7 +922,7 @@ "@jridgewell/resolve-uri" "^3.0.3" "@jridgewell/sourcemap-codec" "^1.4.10" -"@jridgewell/trace-mapping@^0.3.17", "@jridgewell/trace-mapping@^0.3.9": +"@jridgewell/trace-mapping@^0.3.12", "@jridgewell/trace-mapping@^0.3.17", "@jridgewell/trace-mapping@^0.3.18", "@jridgewell/trace-mapping@^0.3.9": version "0.3.18" resolved "https://registry.yarnpkg.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.18.tgz#25783b2086daf6ff1dcb53c9249ae480e4dd4cd6" integrity sha512-w+niJYzMHdd7USdiH2U6869nqhD2nbfZXND5Yp93qIbEmnDNk7PD48o+YchRVpzMU7M6jVCbenTR7PA1FLQ9pA== @@ -850,22 +1063,22 @@ consola "^2.15.0" node-fetch "^2.6.1" -"@prisma/client@^4.14.0": - version "4.14.0" - resolved "https://registry.yarnpkg.com/@prisma/client/-/client-4.14.0.tgz#715b3dd045d094b03cb0a7f2991f088d15ae553e" - integrity sha512-MK/XaA2sFdfaOa7I9MjNKz6dxeIEdeZlnpNRoF2w3JuRLlFJLkpp6cD3yaqw2nUUhbrn3Iqe3ZpVV+VuGGil7Q== +"@prisma/client@^5.0.0": + version "5.0.0" + resolved "https://registry.yarnpkg.com/@prisma/client/-/client-5.0.0.tgz#9f0cd4164f4ffddb28bb1811c27eb7fa1e01a087" + integrity sha512-XlO5ELNAQ7rV4cXIDJUNBEgdLwX3pjtt9Q/RHqDpGf43szpNJx2hJnggfFs7TKNx0cOFsl6KJCSfqr5duEU/bQ== dependencies: - "@prisma/engines-version" "4.14.0-67.d9a4c5988f480fa576d43970d5a23641aa77bc9c" + "@prisma/engines-version" "4.17.0-26.6b0aef69b7cdfc787f822ecd7cdc76d5f1991584" -"@prisma/engines-version@4.14.0-67.d9a4c5988f480fa576d43970d5a23641aa77bc9c": - version "4.14.0-67.d9a4c5988f480fa576d43970d5a23641aa77bc9c" - resolved "https://registry.yarnpkg.com/@prisma/engines-version/-/engines-version-4.14.0-67.d9a4c5988f480fa576d43970d5a23641aa77bc9c.tgz#0aeca447c4a5f23c83f68b8033e627b60bc01850" - integrity sha512-3jum8/YSudeSN0zGW5qkpz+wAN2V/NYCQ+BPjvHYDfWatLWlQkqy99toX0GysDeaUoBIJg1vaz2yKqiA3CFcQw== +"@prisma/engines-version@4.17.0-26.6b0aef69b7cdfc787f822ecd7cdc76d5f1991584": + version "4.17.0-26.6b0aef69b7cdfc787f822ecd7cdc76d5f1991584" + resolved "https://registry.yarnpkg.com/@prisma/engines-version/-/engines-version-4.17.0-26.6b0aef69b7cdfc787f822ecd7cdc76d5f1991584.tgz#b36eda5620872d3fac810c302a7e46cf41daa033" + integrity sha512-HHiUF6NixsldsP3JROq07TYBLEjXFKr6PdH8H4gK/XAoTmIplOJBCgrIUMrsRAnEuGyRoRLXKXWUb943+PFoKQ== -"@prisma/engines@4.15.0": - version "4.15.0" - resolved "https://registry.yarnpkg.com/@prisma/engines/-/engines-4.15.0.tgz#d8687a9fda615fab88b75b466931280289de9e26" - integrity sha512-FTaOCGs0LL0OW68juZlGxFtYviZa4xdQj/rQEdat2txw0s3Vu/saAPKjNVXfIgUsGXmQ72HPgNr6935/P8FNAA== +"@prisma/engines@5.0.0": + version "5.0.0" + resolved "https://registry.yarnpkg.com/@prisma/engines/-/engines-5.0.0.tgz#5249650eabe77c458c90f2be97d8210353c2e22e" + integrity sha512-kyT/8fd0OpWmhAU5YnY7eP31brW1q1YrTGoblWrhQJDiN/1K+Z8S1kylcmtjqx5wsUGcP1HBWutayA/jtyt+sg== "@protobufjs/aspromise@^1.1.1", "@protobufjs/aspromise@^1.1.2": version "1.1.2" @@ -920,24 +1133,24 @@ resolved "https://registry.yarnpkg.com/@protobufjs/utf8/-/utf8-1.1.0.tgz#a777360b5b39a1a2e5106f8e858f2fd2d060c570" integrity sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw== -"@sinonjs/commons@^1.7.0": - version "1.8.6" - resolved "https://registry.yarnpkg.com/@sinonjs/commons/-/commons-1.8.6.tgz#80c516a4dc264c2a69115e7578d62581ff455ed9" - integrity sha512-Ky+XkAkqPZSm3NLBeUng77EBQl3cmeJhITaGHdYH8kjVB+aun3S4XBRti2zt17mtt0mIUDiNxYeoJm6drVvBJQ== +"@sinclair/typebox@^0.27.8": + version "0.27.8" + resolved "https://registry.yarnpkg.com/@sinclair/typebox/-/typebox-0.27.8.tgz#6667fac16c436b5434a387a34dedb013198f6e6e" + integrity sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA== + +"@sinonjs/commons@^3.0.0": + version "3.0.0" + resolved "https://registry.yarnpkg.com/@sinonjs/commons/-/commons-3.0.0.tgz#beb434fe875d965265e04722ccfc21df7f755d72" + integrity sha512-jXBtWAF4vmdNmZgD5FoKsVLv3rPgDnLgPbU84LIJ3otV44vJlDRokVng5v8NFJdCf/da9legHcKaRuZs4L7faA== dependencies: type-detect "4.0.8" -"@sinonjs/fake-timers@^8.0.1": - version "8.1.0" - resolved "https://registry.yarnpkg.com/@sinonjs/fake-timers/-/fake-timers-8.1.0.tgz#3fdc2b6cb58935b21bfb8d1625eb1300484316e7" - integrity sha512-OAPJUAtgeINhh/TAlUID4QTs53Njm7xzddaVlEs/SXwgtiD1tW22zAB/W1wdqfrpmikgaWQ9Fw6Ws+hsiRm5Vg== +"@sinonjs/fake-timers@^10.0.2": + version "10.3.0" + resolved "https://registry.yarnpkg.com/@sinonjs/fake-timers/-/fake-timers-10.3.0.tgz#55fdff1ecab9f354019129daf4df0dd4d923ea66" + integrity sha512-V4BG07kuYSUkTCSBHG8G8TNhM+F19jXFWnQtzj+we8DrkpSBCee9Z3Ms8yiGer/dlmhe35/Xdgyo3/0rQKg7YA== dependencies: - "@sinonjs/commons" "^1.7.0" - -"@tootallnate/once@1": - version "1.1.2" - resolved "https://registry.yarnpkg.com/@tootallnate/once/-/once-1.1.2.tgz#ccb91445360179a04e7fe6aff78c00ffc1eeaf82" - integrity sha512-RbzJvlNzmRq5c3O09UipeuXno4tA1FE6ikOjxZK0tuxVv3412l64l5t1W5pj4+rJq9vpkm/kwiR07aZXnsKPxw== + "@sinonjs/commons" "^3.0.0" "@tootallnate/once@2": version "2.0.0" @@ -964,7 +1177,7 @@ resolved "https://registry.yarnpkg.com/@tsconfig/node16/-/node16-1.0.4.tgz#0b92dcc0cc1c81f6f306a381f28e31b1a56536e9" integrity sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA== -"@types/babel__core@^7.0.0", "@types/babel__core@^7.1.14": +"@types/babel__core@^7.1.14": version "7.20.0" resolved "https://registry.yarnpkg.com/@types/babel__core/-/babel__core-7.20.0.tgz#61bc5a4cae505ce98e1e36c5445e4bee060d8891" integrity sha512-+n8dL/9GWblDO0iU6eZAwEIJVr5DWigtle+Q6HLOrh/pdbXOhOtqzq8VPPE2zvNJzSKY4vH/z3iT3tn0A3ypiQ== @@ -990,7 +1203,7 @@ "@babel/parser" "^7.1.0" "@babel/types" "^7.0.0" -"@types/babel__traverse@*", "@types/babel__traverse@^7.0.4", "@types/babel__traverse@^7.0.6": +"@types/babel__traverse@*", "@types/babel__traverse@^7.0.6": version "7.18.5" resolved "https://registry.yarnpkg.com/@types/babel__traverse/-/babel__traverse-7.18.5.tgz#c107216842905afafd3b6e774f6f935da6f5db80" integrity sha512-enCvTL8m/EHS/zIvJno9nE+ndYPh1/oNFzRYRmtUqJICG2VnCSBzMLW5VN2KCQU91f23tsNKR8v7VJJQMatl7Q== @@ -1071,7 +1284,7 @@ "@types/minimatch" "^5.1.2" "@types/node" "*" -"@types/graceful-fs@^4.1.2": +"@types/graceful-fs@^4.1.3": version "4.1.6" resolved "https://registry.yarnpkg.com/@types/graceful-fs/-/graceful-fs-4.1.6.tgz#e14b2576a1c25026b7f02ede1de3b84c3a1efeae" integrity sha512-Sig0SNORX9fdW+bQuTEovKj3uHcUL6LQKbCrrqb1X7J6/ReAbhCXRAhc+SMejhLELFj2QcyuxmUooZ4bt5ReSw== @@ -1097,13 +1310,22 @@ dependencies: "@types/istanbul-lib-report" "*" -"@types/jest@27.4.1": - version "27.4.1" - resolved "https://registry.yarnpkg.com/@types/jest/-/jest-27.4.1.tgz#185cbe2926eaaf9662d340cc02e548ce9e11ab6d" - integrity sha512-23iPJADSmicDVrWk+HT58LMJtzLAnB2AgIzplQuq/bSrGaxCrlvRFjGbXmamnnk/mAmCdLStiGqggu28ocUyiw== +"@types/jest@^29.5.3": + version "29.5.3" + resolved "https://registry.yarnpkg.com/@types/jest/-/jest-29.5.3.tgz#7a35dc0044ffb8b56325c6802a4781a626b05777" + integrity sha512-1Nq7YrO/vJE/FYnqYyw0FS8LdrjExSgIiHyKg7xPpn+yi8Q4huZryKnkJatN1ZRH89Kw2v33/8ZMB7DuZeSLlA== dependencies: - jest-matcher-utils "^27.0.0" - pretty-format "^27.0.0" + expect "^29.0.0" + pretty-format "^29.0.0" + +"@types/jsdom@^20.0.0": + version "20.0.1" + resolved "https://registry.yarnpkg.com/@types/jsdom/-/jsdom-20.0.1.tgz#07c14bc19bd2f918c1929541cdaacae894744808" + integrity sha512-d0r18sZPmMQr1eG35u12FZfhIXNrnsPU/g5wvRKCUf/tOGilKKwYMYGqh33BNR6ba+2gkHw1EUiHoN3mn7E5IQ== + dependencies: + "@types/node" "*" + "@types/tough-cookie" "*" + parse5 "^7.0.0" "@types/json-schema@*", "@types/json-schema@^7.0.8": version "7.0.11" @@ -1183,11 +1405,6 @@ resolved "https://registry.yarnpkg.com/@types/parse-json/-/parse-json-4.0.0.tgz#2f8bb441434d163b35fb8ffdccd7138927ffb8c0" integrity sha512-//oorEZjL6sbPcKUaCdIGlIUeH26mgzimjBB77G6XRgnDl/L5wOnpyBGRe/Mmf5CVW3PwEBE1NjiMZ/ssFh4wA== -"@types/prettier@^2.1.5": - version "2.7.2" - resolved "https://registry.yarnpkg.com/@types/prettier/-/prettier-2.7.2.tgz#6c2324641cc4ba050a8c710b2b251b377581fbf0" - integrity sha512-KufADq8uQqo1pYKVIYzfKbJfBAc0sOeXqGbFaSpv8MRmC/zXgowNZmFcbngndGk922QDmOASEXUZCaY48gs4cg== - "@types/qs@*": version "6.9.7" resolved "https://registry.yarnpkg.com/@types/qs/-/qs-6.9.7.tgz#63bb7d067db107cc1e457c303bc25d511febf6cb" @@ -1247,6 +1464,11 @@ dependencies: "@types/superagent" "*" +"@types/tough-cookie@*": + version "4.0.2" + resolved "https://registry.yarnpkg.com/@types/tough-cookie/-/tough-cookie-4.0.2.tgz#6286b4c7228d58ab7866d19716f3696e03a09397" + integrity sha512-Q5vtl1W5ue16D+nIaW8JWebSSraJVlK+EthKn7e7UcD4KWsaSJ8BqGPXNaPghgtcn/fhvrN17Tv8ksUsQpiplw== + "@types/validator@^13.7.10": version "13.7.17" resolved "https://registry.yarnpkg.com/@types/validator/-/validator-13.7.17.tgz#0a6d1510395065171e3378a4afc587a3aefa7cc1" @@ -1257,10 +1479,10 @@ resolved "https://registry.yarnpkg.com/@types/yargs-parser/-/yargs-parser-21.0.0.tgz#0c60e537fa790f5f9472ed2776c2b71ec117351b" integrity sha512-iO9ZQHkZxHn4mSakYV0vFHAVDyEOIJQrV2uZ06HxEPcx+mt8swXoZHIbaaJ2crJYFfErySgktuTZ3BeLz+XmFA== -"@types/yargs@^16.0.0": - version "16.0.5" - resolved "https://registry.yarnpkg.com/@types/yargs/-/yargs-16.0.5.tgz#12cc86393985735a283e387936398c2f9e5f88e3" - integrity sha512-AxO/ADJOBFJScHbWhq2xAhlWP24rY4aCEG/NFaMvbT3X2MgRsLjhjQwsn0Zi5zn0LG9jUhCCZMeX9Dkuw6k+vQ== +"@types/yargs@^17.0.8": + version "17.0.24" + resolved "https://registry.yarnpkg.com/@types/yargs/-/yargs-17.0.24.tgz#b3ef8d50ad4aa6aecf6ddc97c580a00f5aa11902" + integrity sha512-6i0aC7jV6QzQB8ne1joVZ0eSFIstHsCrobmOtghM11yGlH0j43FKL2UhWdELkyps0zuf7qVTUVCCR+tgSlyLLw== dependencies: "@types/yargs-parser" "*" @@ -1479,7 +1701,7 @@ resolved "https://registry.yarnpkg.com/@xtuc/long/-/long-4.2.2.tgz#d291c6a4e97989b5c61d9acf396ae4fe133a718d" integrity sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ== -abab@^2.0.3, abab@^2.0.5: +abab@^2.0.6: version "2.0.6" resolved "https://registry.yarnpkg.com/abab/-/abab-2.0.6.tgz#41b80f2c871d19686216b82309231cfd3cb3d291" integrity sha512-j2afSsaIENvHZN2B8GOpF566vZ5WVk5opAiMTvWgaQT8DkbOqsTfvNAvHoRGU2zzP8cPoqys+xHTRDWW8L+/BA== @@ -1499,13 +1721,13 @@ accepts@~1.3.8: mime-types "~2.1.34" negotiator "0.6.3" -acorn-globals@^6.0.0: - version "6.0.0" - resolved "https://registry.yarnpkg.com/acorn-globals/-/acorn-globals-6.0.0.tgz#46cdd39f0f8ff08a876619b55f5ac8a6dc770b45" - integrity sha512-ZQl7LOWaF5ePqqcX4hLuv/bLXYQNfNWw2c0/yX/TsPRKamzHcTGQnlCjHT3TsmkOUVEPS3crCxiPfdzE/Trlhg== +acorn-globals@^7.0.0: + version "7.0.1" + resolved "https://registry.yarnpkg.com/acorn-globals/-/acorn-globals-7.0.1.tgz#0dbf05c44fa7c94332914c02066d5beff62c40c3" + integrity sha512-umOSDSDrfHbTNPuNpC2NSnnA3LUrqpevPb4T9jRx4MagXNS0rs+gwiTcAvqCRmsD6utzsrzNt+ebm00SNWiC3Q== dependencies: - acorn "^7.1.1" - acorn-walk "^7.1.1" + acorn "^8.1.0" + acorn-walk "^8.0.2" acorn-import-assertions@^1.7.6: version "1.9.0" @@ -1517,22 +1739,17 @@ acorn-jsx@^5.3.2: resolved "https://registry.yarnpkg.com/acorn-jsx/-/acorn-jsx-5.3.2.tgz#7ed5bb55908b3b2f1bc55c6af1653bada7f07937" integrity sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ== -acorn-walk@^7.1.1: - version "7.2.0" - resolved "https://registry.yarnpkg.com/acorn-walk/-/acorn-walk-7.2.0.tgz#0de889a601203909b0fbe07b8938dc21d2e967bc" - integrity sha512-OPdCF6GsMIP+Az+aWfAAOEt2/+iVDKE7oy6lJ098aoe59oAmK76qV6Gw60SbZ8jHuG2wH058GF4pLFbYamYrVA== - -acorn-walk@^8.1.1: +acorn-walk@^8.0.2, acorn-walk@^8.1.1: version "8.2.0" resolved "https://registry.yarnpkg.com/acorn-walk/-/acorn-walk-8.2.0.tgz#741210f2e2426454508853a2f44d0ab83b7f69c1" integrity sha512-k+iyHEuPgSw6SbuDpGQM+06HQUa04DZ3o+F6CSzXMvvI5KMvnaEqXe+YVe555R9nn6GPt404fos4wcgpw12SDA== -acorn@^7.1.1: - version "7.4.1" - resolved "https://registry.yarnpkg.com/acorn/-/acorn-7.4.1.tgz#feaed255973d2e77555b83dbc08851a6c63520fa" - integrity sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A== +acorn@^8.1.0, acorn@^8.8.1: + version "8.10.0" + resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.10.0.tgz#8be5b3907a67221a81ab23c7889c4c5526b62ec5" + integrity sha512-F0SAmZ8iUtS//m8DmCTA0jlh6TDKkHQyK6xc6V4KDTyZKA9dnvX9/3sRTVQrWm79glUAZbnmmNcdYwUIHWVybw== -acorn@^8.2.4, acorn@^8.4.1, acorn@^8.5.0, acorn@^8.8.0: +acorn@^8.4.1, acorn@^8.5.0, acorn@^8.8.0: version "8.8.2" resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.8.2.tgz#1b2f25db02af965399b9776b0c2c391276d37c4a" integrity sha512-xjIYgE8HBrkpd/sJqOGNspf8uHG+NOHGOw6a/Urj8taM2EXfdNAH2oFcPeIFfsv3+kz/mJrS5VuMqbNLjCa2vw== @@ -1699,16 +1916,15 @@ axios@^1.2.2: form-data "^4.0.0" proxy-from-env "^1.1.0" -babel-jest@^27.5.1: - version "27.5.1" - resolved "https://registry.yarnpkg.com/babel-jest/-/babel-jest-27.5.1.tgz#a1bf8d61928edfefd21da27eb86a695bfd691444" - integrity sha512-cdQ5dXjGRd0IBRATiQ4mZGlGlRE8kJpjPOixdNRdT+m3UcNqmYWN6rK6nvtXYfY3D76cb8s/O1Ss8ea24PIwcg== +babel-jest@^29.6.2: + version "29.6.2" + resolved "https://registry.yarnpkg.com/babel-jest/-/babel-jest-29.6.2.tgz#cada0a59e07f5acaeb11cbae7e3ba92aec9c1126" + integrity sha512-BYCzImLos6J3BH/+HvUCHG1dTf2MzmAB4jaVxHV+29RZLjR29XuYTmsf2sdDwkrb+FczkGo3kOhE7ga6sI0P4A== dependencies: - "@jest/transform" "^27.5.1" - "@jest/types" "^27.5.1" + "@jest/transform" "^29.6.2" "@types/babel__core" "^7.1.14" babel-plugin-istanbul "^6.1.1" - babel-preset-jest "^27.5.1" + babel-preset-jest "^29.5.0" chalk "^4.0.0" graceful-fs "^4.2.9" slash "^3.0.0" @@ -1724,14 +1940,14 @@ babel-plugin-istanbul@^6.1.1: istanbul-lib-instrument "^5.0.4" test-exclude "^6.0.0" -babel-plugin-jest-hoist@^27.5.1: - version "27.5.1" - resolved "https://registry.yarnpkg.com/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-27.5.1.tgz#9be98ecf28c331eb9f5df9c72d6f89deb8181c2e" - integrity sha512-50wCwD5EMNW4aRpOwtqzyZHIewTYNxLA4nhB+09d8BIssfNfzBRhkBIHiaPv1Si226TQSvp8gxAJm2iY2qs2hQ== +babel-plugin-jest-hoist@^29.5.0: + version "29.5.0" + resolved "https://registry.yarnpkg.com/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-29.5.0.tgz#a97db437936f441ec196990c9738d4b88538618a" + integrity sha512-zSuuuAlTMT4mzLj2nPnUm6fsE6270vdOfnpbJ+RmruU75UhLFvL0N2NgI7xpeS7NaB6hGqmd5pVpGTDYvi4Q3w== dependencies: "@babel/template" "^7.3.3" "@babel/types" "^7.3.3" - "@types/babel__core" "^7.0.0" + "@types/babel__core" "^7.1.14" "@types/babel__traverse" "^7.0.6" babel-preset-current-node-syntax@^1.0.0: @@ -1752,12 +1968,12 @@ babel-preset-current-node-syntax@^1.0.0: "@babel/plugin-syntax-optional-chaining" "^7.8.3" "@babel/plugin-syntax-top-level-await" "^7.8.3" -babel-preset-jest@^27.5.1: - version "27.5.1" - resolved "https://registry.yarnpkg.com/babel-preset-jest/-/babel-preset-jest-27.5.1.tgz#91f10f58034cb7989cb4f962b69fa6eef6a6bc81" - integrity sha512-Nptf2FzlPCWYuJg41HBqXVT8ym6bXOevuCTbhxlUpjwtysGaIWFvDEjp4y+G7fl13FgOdjs7P/DmErqH7da0Ag== +babel-preset-jest@^29.5.0: + version "29.5.0" + resolved "https://registry.yarnpkg.com/babel-preset-jest/-/babel-preset-jest-29.5.0.tgz#57bc8cc88097af7ff6a5ab59d1cd29d52a5916e2" + integrity sha512-JOMloxOqdiBSxMAzjRaH023/vvcaSaec49zvg+2LmNsktC7ei39LTJGw02J+9uUtTZUq6xbLyJ4dxe9sSmIuAg== dependencies: - babel-plugin-jest-hoist "^27.5.1" + babel-plugin-jest-hoist "^29.5.0" babel-preset-current-node-syntax "^1.0.0" balanced-match@^1.0.0: @@ -1834,11 +2050,6 @@ braces@^3.0.2, braces@~3.0.2: dependencies: fill-range "^7.0.1" -browser-process-hrtime@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/browser-process-hrtime/-/browser-process-hrtime-1.0.0.tgz#3c9b4b7d782c8121e56f10106d84c0d0ffc94626" - integrity sha512-9o5UecI3GhkpM6DrXr69PblIuWxPKk9Y0jHBRhdocZ2y7YECBFCsHm79Pr3OyR2AvjhDkabFJaDJMYRazHgsow== - browserslist@^4.14.5, browserslist@^4.21.3: version "4.21.5" resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.21.5.tgz#75c5dae60063ee641f977e00edd3cfb2fb7af6a7" @@ -1849,6 +2060,16 @@ browserslist@^4.14.5, browserslist@^4.21.3: node-releases "^2.0.8" update-browserslist-db "^1.0.10" +browserslist@^4.21.9: + version "4.21.9" + resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.21.9.tgz#e11bdd3c313d7e2a9e87e8b4b0c7872b13897635" + integrity sha512-M0MFoZzbUrRU4KNfCrDLnvyE7gub+peetoTid3TBIqtunaDJyXlwhakT+/VkvSXcfIzFfK/nkCs4nmyTmxdNSg== + dependencies: + caniuse-lite "^1.0.30001503" + electron-to-chromium "^1.4.431" + node-releases "^2.0.12" + update-browserslist-db "^1.0.11" + bs-logger@0.x: version "0.2.6" resolved "https://registry.yarnpkg.com/bs-logger/-/bs-logger-0.2.6.tgz#eb7d365307a72cf974cc6cda76b68354ad336bd8" @@ -1921,6 +2142,11 @@ caniuse-lite@^1.0.30001449: resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001486.tgz#56a08885228edf62cbe1ac8980f2b5dae159997e" integrity sha512-uv7/gXuHi10Whlj0pp5q/tsK/32J2QSqVRKQhs2j8VsDCjgyruAh/eEXHF822VqO9yT6iZKw3nRwZRSPBE9OQg== +caniuse-lite@^1.0.30001503: + version "1.0.30001517" + resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001517.tgz#90fabae294215c3495807eb24fc809e11dc2f0a8" + integrity sha512-Vdhm5S11DaFVLlyiKu4hiUTkpZu+y1KA/rZZqVQfOD5YdDT/eQKlkt7NaE0WGOFgX32diqt9MiP9CAiFeRklaA== + catharsis@^0.9.0: version "0.9.0" resolved "https://registry.yarnpkg.com/catharsis/-/catharsis-0.9.0.tgz#40382a168be0e6da308c277d3a2b3eb40c7d2121" @@ -2033,15 +2259,6 @@ cli-width@^3.0.0: resolved "https://registry.yarnpkg.com/cli-width/-/cli-width-3.0.0.tgz#a2f48437a2caa9a22436e794bf071ec9e61cedf6" integrity sha512-FxqpkPPwu1HjuN93Omfm4h8uIanXofW0RxVEW3k5RKx+mJJYSthzNhp32Kzxxy3YAEZ/Dc/EWN1vZRY0+kOhbw== -cliui@^7.0.2: - version "7.0.4" - resolved "https://registry.yarnpkg.com/cliui/-/cliui-7.0.4.tgz#a0265ee655476fc807aea9df3df8df7783808b4f" - integrity sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ== - dependencies: - string-width "^4.2.0" - strip-ansi "^6.0.0" - wrap-ansi "^7.0.0" - cliui@^8.0.1: version "8.0.1" resolved "https://registry.yarnpkg.com/cliui/-/cliui-8.0.1.tgz#0c04b075db02cbfe60dc8e6cf2f5486b1a3608aa" @@ -2144,11 +2361,16 @@ content-type@~1.0.4: resolved "https://registry.yarnpkg.com/content-type/-/content-type-1.0.5.tgz#8b773162656d1d1086784c8f23a54ce6d73d7918" integrity sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA== -convert-source-map@^1.4.0, convert-source-map@^1.6.0, convert-source-map@^1.7.0: +convert-source-map@^1.6.0, convert-source-map@^1.7.0: version "1.9.0" resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-1.9.0.tgz#7faae62353fb4213366d0ca98358d22e8368b05f" integrity sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A== +convert-source-map@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-2.0.0.tgz#4b560f649fc4e918dd0ab75cf4961e8bc882d82a" + integrity sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg== + cookie-signature@1.0.6: version "1.0.6" resolved "https://registry.yarnpkg.com/cookie-signature/-/cookie-signature-1.0.6.tgz#e303a882b342cc3ee8ca513a79999734dab3ae2c" @@ -2202,10 +2424,10 @@ cross-spawn@^7.0.0, cross-spawn@^7.0.2, cross-spawn@^7.0.3: shebang-command "^2.0.0" which "^2.0.1" -cssom@^0.4.4: - version "0.4.4" - resolved "https://registry.yarnpkg.com/cssom/-/cssom-0.4.4.tgz#5a66cf93d2d0b661d80bf6a44fb65f5c2e4e0a10" - integrity sha512-p3pvU7r1MyyqbTk+WbNJIgJjG2VmTIaB10rI93LzVPrmDJKkzKYMtxxyAvQXR/NS6otuzveI7+7BBq3SjBS2mw== +cssom@^0.5.0: + version "0.5.0" + resolved "https://registry.yarnpkg.com/cssom/-/cssom-0.5.0.tgz#d254fa92cd8b6fbd83811b9fbaed34663cc17c36" + integrity sha512-iKuQcq+NdHqlAcwUY0o/HL69XQrUaQdMjmStJ8JFmUaiiQErlhrmuigkg/CU4E2J0IyUKUrMAgl36TvN67MqTw== cssom@~0.3.6: version "0.3.8" @@ -2219,14 +2441,14 @@ cssstyle@^2.3.0: dependencies: cssom "~0.3.6" -data-urls@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/data-urls/-/data-urls-2.0.0.tgz#156485a72963a970f5d5821aaf642bef2bf2db9b" - integrity sha512-X5eWTSXO/BJmpdIKCRuKUgSCgAN0OwliVK3yPKbwIWU1Tdw5BRajxlzMidvh+gwko9AfQ9zIj52pzF91Q3YAvQ== +data-urls@^3.0.2: + version "3.0.2" + resolved "https://registry.yarnpkg.com/data-urls/-/data-urls-3.0.2.tgz#9cf24a477ae22bcef5cd5f6f0bfbc1d2d3be9143" + integrity sha512-Jy/tj3ldjZJo63sVAvg6LHt2mHvl4V6AgRAmNDtLdm7faqtsx+aJG42rsyCo9JCoRVKwPFzKlIPx3DIibwSIaQ== dependencies: - abab "^2.0.3" - whatwg-mimetype "^2.3.0" - whatwg-url "^8.0.0" + abab "^2.0.6" + whatwg-mimetype "^3.0.0" + whatwg-url "^11.0.0" dayjs@^1.11.8: version "1.11.8" @@ -2247,15 +2469,15 @@ debug@4, debug@^4.1.0, debug@^4.1.1, debug@^4.3.2, debug@^4.3.4: dependencies: ms "2.1.2" -decimal.js@^10.2.1: +decimal.js@^10.4.2: version "10.4.3" resolved "https://registry.yarnpkg.com/decimal.js/-/decimal.js-10.4.3.tgz#1044092884d245d1b7f65725fa4ad4c6f781cc23" integrity sha512-VBBaLc1MgL5XpzgIP7ny5Z6Nx3UrRkIViUkPUdtl9aya5amy3De1gsUUSB1g3+3sExYNjCAsAznmukyxCb1GRA== -dedent@^0.7.0: - version "0.7.0" - resolved "https://registry.yarnpkg.com/dedent/-/dedent-0.7.0.tgz#2495ddbaf6eb874abb0e1be9df22d2e5a544326c" - integrity sha512-Q6fKUPqnAHAyhiUgFU7BUzLiv0kd8saH9al7tnu5Q/okj6dnupxyTgFIBjVzJATdfIAm9NAsvXNzjaKa+bxVyA== +dedent@^1.0.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/dedent/-/dedent-1.3.0.tgz#15d6809eb15b581d5587a2dc208f34118e35bee3" + integrity sha512-7glNLfvdsMzZm3FpRY1CHuI2lbYDR+71YmrhmTZjYFD5pfT0ACgnGRdrrC9Mk2uICnzkcdelCx5at787UDGOvg== deep-is@^0.1.3, deep-is@~0.1.3: version "0.1.4" @@ -2302,10 +2524,10 @@ dezalgo@^1.0.4: asap "^2.0.0" wrappy "1" -diff-sequences@^27.5.1: - version "27.5.1" - resolved "https://registry.yarnpkg.com/diff-sequences/-/diff-sequences-27.5.1.tgz#eaecc0d327fd68c8d9672a1e64ab8dccb2ef5327" - integrity sha512-k1gCAXAsNgLwEL+Y8Wvl+M6oEFj5bgazfZULpS5CneoPPXRaCCW7dm+q21Ky2VEE5X+VeRDBVg1Pcvvsr4TtNQ== +diff-sequences@^29.4.3: + version "29.4.3" + resolved "https://registry.yarnpkg.com/diff-sequences/-/diff-sequences-29.4.3.tgz#9314bc1fabe09267ffeca9cbafc457d8499a13f2" + integrity sha512-ofrBgwpPhCD85kMKtE9RYFFq6OC1A89oW2vvgWZNCwxrUpRUILopY7lsYyMDSjc8g6U6aiO0Qubg6r4Wgt5ZnA== diff@^4.0.1: version "4.0.2" @@ -2326,12 +2548,12 @@ doctrine@^3.0.0: dependencies: esutils "^2.0.2" -domexception@^2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/domexception/-/domexception-2.0.1.tgz#fb44aefba793e1574b0af6aed2801d057529f304" - integrity sha512-yxJ2mFy/sibVQlu5qHjOkf9J3K6zgmCxgJ94u2EdvDOV09H+32LtRswEcUsmUWN72pVLOEnTSRaIVVzVQgS0dg== +domexception@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/domexception/-/domexception-4.0.0.tgz#4ad1be56ccadc86fc76d033353999a8037d03673" + integrity sha512-A2is4PLG+eeSfoTMA95/s4pvAoSo2mKtiM5jlHkAVewmiO8ISFTFKZjH7UAM1Atli/OT/7JHOrJRJiMKUZKYBw== dependencies: - webidl-conversions "^5.0.0" + webidl-conversions "^7.0.0" duplexify@^4.0.0, duplexify@^4.1.1: version "4.1.2" @@ -2360,10 +2582,15 @@ electron-to-chromium@^1.4.284: resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.4.385.tgz#1afd8d6280d510145148777b899ff481c65531ff" integrity sha512-L9zlje9bIw0h+CwPQumiuVlfMcV4boxRjFIWDcLfFqTZNbkwOExBzfmswytHawObQX4OUhtNv8gIiB21kOurIg== -emittery@^0.8.1: - version "0.8.1" - resolved "https://registry.yarnpkg.com/emittery/-/emittery-0.8.1.tgz#bb23cc86d03b30aa75a7f734819dee2e1ba70860" - integrity sha512-uDfvUjVrfGJJhymx/kz6prltenw1u7WrCg1oa94zYY8xxVpLLUu045LAT0dhDZdXG58/EpPL/5kA180fQ/qudg== +electron-to-chromium@^1.4.431: + version "1.4.473" + resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.4.473.tgz#4853de13a335c70fe1f9df8d4029be54068767d1" + integrity sha512-aVfC8+440vGfl06l8HKKn8/PD5jRfSnLkTTD65EFvU46igbpQRri1gxSzW9/+TeUlwYzrXk1sw867T96zlyECA== + +emittery@^0.13.1: + version "0.13.1" + resolved "https://registry.yarnpkg.com/emittery/-/emittery-0.13.1.tgz#c04b8c3457490e0847ae51fced3af52d338e3dad" + integrity sha512-DeWwawk6r5yR9jFgnDKYt4sLS0LmHJJi3ZOnb5/JdbYwj3nW+FxQnHIjhBKz8YLC7oRNPVM9NQ47I3CVx34eqQ== emoji-regex@^8.0.0: version "8.0.0" @@ -2403,6 +2630,11 @@ ent@^2.2.0: resolved "https://registry.yarnpkg.com/ent/-/ent-2.2.0.tgz#e964219325a21d05f44466a2f686ed6ce5f5dd1d" integrity sha512-GHrMyVZQWvTIdDtpiEXdHZnFQKzeO09apj8Cbl4pKWy4i0Oprcq17usfDt5aO63swf0JOeMWjWQE/LzgSRuWpA== +entities@^4.4.0: + version "4.5.0" + resolved "https://registry.yarnpkg.com/entities/-/entities-4.5.0.tgz#5d268ea5e7113ec74c4d033b79ea5a35a488fb48" + integrity sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw== + entities@~2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/entities/-/entities-2.1.0.tgz#992d3129cf7df6870b96c57858c249a120f8b8b5" @@ -2640,15 +2872,17 @@ exit@^0.1.2: resolved "https://registry.yarnpkg.com/exit/-/exit-0.1.2.tgz#0632638f8d877cc82107d30a0fff1a17cba1cd0c" integrity sha512-Zk/eNKV2zbjpKzrsQ+n1G6poVbErQxJ0LBOJXaKZ1EViLzH+hrLu9cdXI4zw9dBQJslwBEpbQ2P1oS7nDxs6jQ== -expect@^27.5.1: - version "27.5.1" - resolved "https://registry.yarnpkg.com/expect/-/expect-27.5.1.tgz#83ce59f1e5bdf5f9d2b94b61d2050db48f3fef74" - integrity sha512-E1q5hSUG2AmYQwQJ041nvgpkODHQvB+RKlB4IYdru6uJsyFTRyZAP463M+1lINorwbqAmUggi6+WwkD8lCS/Dw== +expect@^29.0.0, expect@^29.6.2: + version "29.6.2" + resolved "https://registry.yarnpkg.com/expect/-/expect-29.6.2.tgz#7b08e83eba18ddc4a2cf62b5f2d1918f5cd84521" + integrity sha512-iAErsLxJ8C+S02QbLAwgSGSezLQK+XXRDt8IuFXFpwCNw2ECmzZSmjKcCaFVp5VRMk+WAvz6h6jokzEzBFZEuA== dependencies: - "@jest/types" "^27.5.1" - jest-get-type "^27.5.1" - jest-matcher-utils "^27.5.1" - jest-message-util "^27.5.1" + "@jest/expect-utils" "^29.6.2" + "@types/node" "*" + jest-get-type "^29.4.3" + jest-matcher-utils "^29.6.2" + jest-message-util "^29.6.2" + jest-util "^29.6.2" express@4.18.1: version "4.18.1" @@ -2722,7 +2956,7 @@ fast-glob@^3.2.9: merge2 "^1.3.0" micromatch "^4.0.4" -fast-json-stable-stringify@2.1.0, fast-json-stable-stringify@2.x, fast-json-stable-stringify@^2.0.0: +fast-json-stable-stringify@2.1.0, fast-json-stable-stringify@2.x, fast-json-stable-stringify@^2.0.0, fast-json-stable-stringify@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz#874bf69c6f404c2b5d99c481341399fd55892633" integrity sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw== @@ -2841,15 +3075,6 @@ fork-ts-checker-webpack-plugin@7.2.11: semver "^7.3.5" tapable "^2.2.1" -form-data@^3.0.0: - version "3.0.1" - resolved "https://registry.yarnpkg.com/form-data/-/form-data-3.0.1.tgz#ebd53791b78356a99af9a300d4282c4d5eb9755f" - integrity sha512-RHkBKtLWUVwd7SqRIvCZMEvAMoGUp0XU+seQiZejj0COz3RI3hWP4sCv3gZWWLjJTd7rGwcsF5eKZGii0r/hbg== - dependencies: - asynckit "^0.4.0" - combined-stream "^1.0.8" - mime-types "^2.1.12" - form-data@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/form-data/-/form-data-4.0.0.tgz#93919daeaf361ee529584b9b31664dc12c9fa452" @@ -2981,7 +3206,7 @@ glob-to-regexp@^0.4.1: resolved "https://registry.yarnpkg.com/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz#c75297087c851b9a578bd217dd59a92f59fe546e" integrity sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw== -glob@^7.0.0, glob@^7.1.1, glob@^7.1.2, glob@^7.1.3, glob@^7.1.4: +glob@^7.0.0, glob@^7.1.3, glob@^7.1.4: version "7.2.3" resolved "https://registry.yarnpkg.com/glob/-/glob-7.2.3.tgz#b8df0fb802bbfa8e89bd1d938b4e16578ed44f2b" integrity sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q== @@ -3122,12 +3347,12 @@ hexoid@^1.0.0: resolved "https://registry.yarnpkg.com/hexoid/-/hexoid-1.0.0.tgz#ad10c6573fb907de23d9ec63a711267d9dc9bc18" integrity sha512-QFLV0taWQOZtvIRIAdBChesmogZrtuXvVWsFHZTk2SU+anspqZ2vMnoLg7IE1+Uk16N19APic1BuF8bC8c2m5g== -html-encoding-sniffer@^2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/html-encoding-sniffer/-/html-encoding-sniffer-2.0.1.tgz#42a6dc4fd33f00281176e8b23759ca4e4fa185f3" - integrity sha512-D5JbOMBIR/TVZkubHT+OyT2705QvogUW4IBn6nHd756OwieSF9aDYFj4dv6HHEVGYbHaLETa3WggZYWWMyy3ZQ== +html-encoding-sniffer@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/html-encoding-sniffer/-/html-encoding-sniffer-3.0.0.tgz#2cb1a8cf0db52414776e5b2a7a04d5dd98158de9" + integrity sha512-oWv4T4yJ52iKrufjnyZPkrN0CH3QnrUqdB6In1g5Fe1mia8GmF36gnfNySxoZtxD5+NmYw1EElVXiBk93UeskA== dependencies: - whatwg-encoding "^1.0.5" + whatwg-encoding "^2.0.0" html-escaper@^2.0.0: version "2.0.2" @@ -3150,15 +3375,6 @@ http-errors@2.0.0: statuses "2.0.1" toidentifier "1.0.1" -http-proxy-agent@^4.0.1: - version "4.0.1" - resolved "https://registry.yarnpkg.com/http-proxy-agent/-/http-proxy-agent-4.0.1.tgz#8a8c8ef7f5932ccf953c296ca8291b95aa74aa3a" - integrity sha512-k0zdNgqWTGA6aeIRVpvfVob4fL52dTfaehylg0Y4UvSySvOq/Y+BOyPrgpUrA7HylqvU8vIZGsRuXmspskV0Tg== - dependencies: - "@tootallnate/once" "1" - agent-base "6" - debug "4" - http-proxy-agent@^5.0.0: version "5.0.0" resolved "https://registry.yarnpkg.com/http-proxy-agent/-/http-proxy-agent-5.0.0.tgz#5129800203520d434f142bc78ff3c170800f2b43" @@ -3168,7 +3384,7 @@ http-proxy-agent@^5.0.0: agent-base "6" debug "4" -https-proxy-agent@^5.0.0: +https-proxy-agent@^5.0.0, https-proxy-agent@^5.0.1: version "5.0.1" resolved "https://registry.yarnpkg.com/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz#c59ef224a04fe8b754f3db0063a25ea30d0005d6" integrity sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA== @@ -3193,6 +3409,13 @@ iconv-lite@0.4.24, iconv-lite@^0.4.24: dependencies: safer-buffer ">= 2.1.2 < 3" +iconv-lite@0.6.3: + version "0.6.3" + resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.6.3.tgz#a52f80bf38da1952eb5c681790719871a1a72501" + integrity sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw== + dependencies: + safer-buffer ">= 2.1.2 < 3.0.0" + ieee754@^1.1.13: version "1.2.1" resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.2.1.tgz#8eb7a10a63fff25d15a57b001586d177d1b0d352" @@ -3364,11 +3587,6 @@ is-stream@^2.0.0: resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-2.0.1.tgz#fac1e3d53b97ad5a9d0ae9cef2389f5810a5c077" integrity sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg== -is-typedarray@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/is-typedarray/-/is-typedarray-1.0.0.tgz#e479c80858df0c1b11ddda6940f96011fcda4a9a" - integrity sha512-cyA56iCMHAh5CdzjJIa4aohJyeO1YbwLi3Jc35MmRU6poroFjIGZzUzupGiRPOjgHg9TLu43xbpwXk523fMxKA== - is-unicode-supported@^0.1.0: version "0.1.0" resolved "https://registry.yarnpkg.com/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz#3f26c76a809593b52bfa2ecb5710ed2779b522a7" @@ -3431,394 +3649,359 @@ iterare@1.2.1: resolved "https://registry.yarnpkg.com/iterare/-/iterare-1.2.1.tgz#139c400ff7363690e33abffa33cbba8920f00042" integrity sha512-RKYVTCjAnRthyJes037NX/IiqeidgN1xc3j1RjFfECFp28A1GVwK9nA+i0rJPaHqSZwygLzRnFlzUuHFoWWy+Q== -jest-changed-files@^27.5.1: - version "27.5.1" - resolved "https://registry.yarnpkg.com/jest-changed-files/-/jest-changed-files-27.5.1.tgz#a348aed00ec9bf671cc58a66fcbe7c3dfd6a68f5" - integrity sha512-buBLMiByfWGCoMsLLzGUUSpAmIAGnbR2KJoMN10ziLhOLvP4e0SlypHnAel8iqQXTrcbmfEY9sSqae5sgUsTvw== +jest-changed-files@^29.5.0: + version "29.5.0" + resolved "https://registry.yarnpkg.com/jest-changed-files/-/jest-changed-files-29.5.0.tgz#e88786dca8bf2aa899ec4af7644e16d9dcf9b23e" + integrity sha512-IFG34IUMUaNBIxjQXF/iu7g6EcdMrGRRxaUSw92I/2g2YC6vCdTltl4nHvt7Ci5nSJwXIkCu8Ka1DKF+X7Z1Ag== dependencies: - "@jest/types" "^27.5.1" execa "^5.0.0" - throat "^6.0.1" + p-limit "^3.1.0" -jest-circus@^27.5.1: - version "27.5.1" - resolved "https://registry.yarnpkg.com/jest-circus/-/jest-circus-27.5.1.tgz#37a5a4459b7bf4406e53d637b49d22c65d125ecc" - integrity sha512-D95R7x5UtlMA5iBYsOHFFbMD/GVA4R/Kdq15f7xYWUfWHBto9NYRsOvnSauTgdF+ogCpJ4tyKOXhUifxS65gdw== +jest-circus@^29.6.2: + version "29.6.2" + resolved "https://registry.yarnpkg.com/jest-circus/-/jest-circus-29.6.2.tgz#1e6ffca60151ac66cad63fce34f443f6b5bb4258" + integrity sha512-G9mN+KOYIUe2sB9kpJkO9Bk18J4dTDArNFPwoZ7WKHKel55eKIS/u2bLthxgojwlf9NLCVQfgzM/WsOVvoC6Fw== dependencies: - "@jest/environment" "^27.5.1" - "@jest/test-result" "^27.5.1" - "@jest/types" "^27.5.1" + "@jest/environment" "^29.6.2" + "@jest/expect" "^29.6.2" + "@jest/test-result" "^29.6.2" + "@jest/types" "^29.6.1" "@types/node" "*" chalk "^4.0.0" co "^4.6.0" - dedent "^0.7.0" - expect "^27.5.1" + dedent "^1.0.0" is-generator-fn "^2.0.0" - jest-each "^27.5.1" - jest-matcher-utils "^27.5.1" - jest-message-util "^27.5.1" - jest-runtime "^27.5.1" - jest-snapshot "^27.5.1" - jest-util "^27.5.1" - pretty-format "^27.5.1" + jest-each "^29.6.2" + jest-matcher-utils "^29.6.2" + jest-message-util "^29.6.2" + jest-runtime "^29.6.2" + jest-snapshot "^29.6.2" + jest-util "^29.6.2" + p-limit "^3.1.0" + pretty-format "^29.6.2" + pure-rand "^6.0.0" slash "^3.0.0" stack-utils "^2.0.3" - throat "^6.0.1" -jest-cli@^27.5.1: - version "27.5.1" - resolved "https://registry.yarnpkg.com/jest-cli/-/jest-cli-27.5.1.tgz#278794a6e6458ea8029547e6c6cbf673bd30b145" - integrity sha512-Hc6HOOwYq4/74/c62dEE3r5elx8wjYqxY0r0G/nFrLDPMFRu6RA/u8qINOIkvhxG7mMQ5EJsOGfRpI8L6eFUVw== +jest-cli@^29.6.2: + version "29.6.2" + resolved "https://registry.yarnpkg.com/jest-cli/-/jest-cli-29.6.2.tgz#edb381763398d1a292cd1b636a98bfa5644b8fda" + integrity sha512-TT6O247v6dCEX2UGHGyflMpxhnrL0DNqP2fRTKYm3nJJpCTfXX3GCMQPGFjXDoj0i5/Blp3jriKXFgdfmbYB6Q== dependencies: - "@jest/core" "^27.5.1" - "@jest/test-result" "^27.5.1" - "@jest/types" "^27.5.1" + "@jest/core" "^29.6.2" + "@jest/test-result" "^29.6.2" + "@jest/types" "^29.6.1" chalk "^4.0.0" exit "^0.1.2" graceful-fs "^4.2.9" import-local "^3.0.2" - jest-config "^27.5.1" - jest-util "^27.5.1" - jest-validate "^27.5.1" + jest-config "^29.6.2" + jest-util "^29.6.2" + jest-validate "^29.6.2" prompts "^2.0.1" - yargs "^16.2.0" + yargs "^17.3.1" -jest-config@^27.5.1: - version "27.5.1" - resolved "https://registry.yarnpkg.com/jest-config/-/jest-config-27.5.1.tgz#5c387de33dca3f99ad6357ddeccd91bf3a0e4a41" - integrity sha512-5sAsjm6tGdsVbW9ahcChPAFCk4IlkQUknH5AvKjuLTSlcO/wCZKyFdn7Rg0EkC+OGgWODEy2hDpWB1PgzH0JNA== +jest-config@^29.6.2: + version "29.6.2" + resolved "https://registry.yarnpkg.com/jest-config/-/jest-config-29.6.2.tgz#c68723f06b31ca5e63030686e604727d406cd7c3" + integrity sha512-VxwFOC8gkiJbuodG9CPtMRjBUNZEHxwfQXmIudSTzFWxaci3Qub1ddTRbFNQlD/zUeaifLndh/eDccFX4wCMQw== dependencies: - "@babel/core" "^7.8.0" - "@jest/test-sequencer" "^27.5.1" - "@jest/types" "^27.5.1" - babel-jest "^27.5.1" + "@babel/core" "^7.11.6" + "@jest/test-sequencer" "^29.6.2" + "@jest/types" "^29.6.1" + babel-jest "^29.6.2" chalk "^4.0.0" ci-info "^3.2.0" deepmerge "^4.2.2" - glob "^7.1.1" + glob "^7.1.3" graceful-fs "^4.2.9" - jest-circus "^27.5.1" - jest-environment-jsdom "^27.5.1" - jest-environment-node "^27.5.1" - jest-get-type "^27.5.1" - jest-jasmine2 "^27.5.1" - jest-regex-util "^27.5.1" - jest-resolve "^27.5.1" - jest-runner "^27.5.1" - jest-util "^27.5.1" - jest-validate "^27.5.1" + jest-circus "^29.6.2" + jest-environment-node "^29.6.2" + jest-get-type "^29.4.3" + jest-regex-util "^29.4.3" + jest-resolve "^29.6.2" + jest-runner "^29.6.2" + jest-util "^29.6.2" + jest-validate "^29.6.2" micromatch "^4.0.4" parse-json "^5.2.0" - pretty-format "^27.5.1" + pretty-format "^29.6.2" slash "^3.0.0" strip-json-comments "^3.1.1" -jest-diff@^27.5.1: - version "27.5.1" - resolved "https://registry.yarnpkg.com/jest-diff/-/jest-diff-27.5.1.tgz#a07f5011ac9e6643cf8a95a462b7b1ecf6680def" - integrity sha512-m0NvkX55LDt9T4mctTEgnZk3fmEg3NRYutvMPWM/0iPnkFj2wIeF45O1718cMSOFO1vINkqmxqD8vE37uTEbqw== +jest-diff@^29.6.2: + version "29.6.2" + resolved "https://registry.yarnpkg.com/jest-diff/-/jest-diff-29.6.2.tgz#c36001e5543e82a0805051d3ceac32e6825c1c46" + integrity sha512-t+ST7CB9GX5F2xKwhwCf0TAR17uNDiaPTZnVymP9lw0lssa9vG+AFyDZoeIHStU3WowFFwT+ky+er0WVl2yGhA== dependencies: chalk "^4.0.0" - diff-sequences "^27.5.1" - jest-get-type "^27.5.1" - pretty-format "^27.5.1" + diff-sequences "^29.4.3" + jest-get-type "^29.4.3" + pretty-format "^29.6.2" -jest-docblock@^27.5.1: - version "27.5.1" - resolved "https://registry.yarnpkg.com/jest-docblock/-/jest-docblock-27.5.1.tgz#14092f364a42c6108d42c33c8cf30e058e25f6c0" - integrity sha512-rl7hlABeTsRYxKiUfpHrQrG4e2obOiTQWfMEH3PxPjOtdsfLQO4ReWSZaQ7DETm4xu07rl4q/h4zcKXyU0/OzQ== +jest-docblock@^29.4.3: + version "29.4.3" + resolved "https://registry.yarnpkg.com/jest-docblock/-/jest-docblock-29.4.3.tgz#90505aa89514a1c7dceeac1123df79e414636ea8" + integrity sha512-fzdTftThczeSD9nZ3fzA/4KkHtnmllawWrXO69vtI+L9WjEIuXWs4AmyME7lN5hU7dB0sHhuPfcKofRsUb/2Fg== dependencies: detect-newline "^3.0.0" -jest-each@^27.5.1: - version "27.5.1" - resolved "https://registry.yarnpkg.com/jest-each/-/jest-each-27.5.1.tgz#5bc87016f45ed9507fed6e4702a5b468a5b2c44e" - integrity sha512-1Ff6p+FbhT/bXQnEouYy00bkNSY7OUpfIcmdl8vZ31A1UUaurOLPA8a8BbJOF2RDUElwJhmeaV7LnagI+5UwNQ== +jest-each@^29.6.2: + version "29.6.2" + resolved "https://registry.yarnpkg.com/jest-each/-/jest-each-29.6.2.tgz#c9e4b340bcbe838c73adf46b76817b15712d02ce" + integrity sha512-MsrsqA0Ia99cIpABBc3izS1ZYoYfhIy0NNWqPSE0YXbQjwchyt6B1HD2khzyPe1WiJA7hbxXy77ZoUQxn8UlSw== dependencies: - "@jest/types" "^27.5.1" + "@jest/types" "^29.6.1" chalk "^4.0.0" - jest-get-type "^27.5.1" - jest-util "^27.5.1" - pretty-format "^27.5.1" - -jest-environment-jsdom@^27.2.5, jest-environment-jsdom@^27.5.1: - version "27.5.1" - resolved "https://registry.yarnpkg.com/jest-environment-jsdom/-/jest-environment-jsdom-27.5.1.tgz#ea9ccd1fc610209655a77898f86b2b559516a546" - integrity sha512-TFBvkTC1Hnnnrka/fUb56atfDtJ9VMZ94JkjTbggl1PEpwrYtUBKMezB3inLmWqQsXYLcMwNoDQwoBTAvFfsfw== - dependencies: - "@jest/environment" "^27.5.1" - "@jest/fake-timers" "^27.5.1" - "@jest/types" "^27.5.1" + jest-get-type "^29.4.3" + jest-util "^29.6.2" + pretty-format "^29.6.2" + +jest-environment-jsdom@^29.6.2: + version "29.6.2" + resolved "https://registry.yarnpkg.com/jest-environment-jsdom/-/jest-environment-jsdom-29.6.2.tgz#4fc68836a7774a771819a2f980cb47af3b1629da" + integrity sha512-7oa/+266AAEgkzae8i1awNEfTfjwawWKLpiw2XesZmaoVVj9u9t8JOYx18cG29rbPNtkUlZ8V4b5Jb36y/VxoQ== + dependencies: + "@jest/environment" "^29.6.2" + "@jest/fake-timers" "^29.6.2" + "@jest/types" "^29.6.1" + "@types/jsdom" "^20.0.0" "@types/node" "*" - jest-mock "^27.5.1" - jest-util "^27.5.1" - jsdom "^16.6.0" - -jest-environment-node@^27.5.1: - version "27.5.1" - resolved "https://registry.yarnpkg.com/jest-environment-node/-/jest-environment-node-27.5.1.tgz#dedc2cfe52fab6b8f5714b4808aefa85357a365e" - integrity sha512-Jt4ZUnxdOsTGwSRAfKEnE6BcwsSPNOijjwifq5sDFSA2kesnXTvNqKHYgM0hDq3549Uf/KzdXNYn4wMZJPlFLw== - dependencies: - "@jest/environment" "^27.5.1" - "@jest/fake-timers" "^27.5.1" - "@jest/types" "^27.5.1" + jest-mock "^29.6.2" + jest-util "^29.6.2" + jsdom "^20.0.0" + +jest-environment-node@^29.6.2: + version "29.6.2" + resolved "https://registry.yarnpkg.com/jest-environment-node/-/jest-environment-node-29.6.2.tgz#a9ea2cabff39b08eca14ccb32c8ceb924c8bb1ad" + integrity sha512-YGdFeZ3T9a+/612c5mTQIllvWkddPbYcN2v95ZH24oWMbGA4GGS2XdIF92QMhUhvrjjuQWYgUGW2zawOyH63MQ== + dependencies: + "@jest/environment" "^29.6.2" + "@jest/fake-timers" "^29.6.2" + "@jest/types" "^29.6.1" "@types/node" "*" - jest-mock "^27.5.1" - jest-util "^27.5.1" + jest-mock "^29.6.2" + jest-util "^29.6.2" -jest-get-type@^27.5.1: - version "27.5.1" - resolved "https://registry.yarnpkg.com/jest-get-type/-/jest-get-type-27.5.1.tgz#3cd613c507b0f7ace013df407a1c1cd578bcb4f1" - integrity sha512-2KY95ksYSaK7DMBWQn6dQz3kqAf3BB64y2udeG+hv4KfSOb9qwcYQstTJc1KCbsix+wLZWZYN8t7nwX3GOBLRw== +jest-get-type@^29.4.3: + version "29.4.3" + resolved "https://registry.yarnpkg.com/jest-get-type/-/jest-get-type-29.4.3.tgz#1ab7a5207c995161100b5187159ca82dd48b3dd5" + integrity sha512-J5Xez4nRRMjk8emnTpWrlkyb9pfRQQanDrvWHhsR1+VUfbwxi30eVcZFlcdGInRibU4G5LwHXpI7IRHU0CY+gg== -jest-haste-map@^27.5.1: - version "27.5.1" - resolved "https://registry.yarnpkg.com/jest-haste-map/-/jest-haste-map-27.5.1.tgz#9fd8bd7e7b4fa502d9c6164c5640512b4e811e7f" - integrity sha512-7GgkZ4Fw4NFbMSDSpZwXeBiIbx+t/46nJ2QitkOjvwPYyZmqttu2TDSimMHP1EkPOi4xUZAN1doE5Vd25H4Jng== +jest-haste-map@^29.6.2: + version "29.6.2" + resolved "https://registry.yarnpkg.com/jest-haste-map/-/jest-haste-map-29.6.2.tgz#298c25ea5255cfad8b723179d4295cf3a50a70d1" + integrity sha512-+51XleTDAAysvU8rT6AnS1ZJ+WHVNqhj1k6nTvN2PYP+HjU3kqlaKQ1Lnw3NYW3bm2r8vq82X0Z1nDDHZMzHVA== dependencies: - "@jest/types" "^27.5.1" - "@types/graceful-fs" "^4.1.2" + "@jest/types" "^29.6.1" + "@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 "^27.5.1" - jest-serializer "^27.5.1" - jest-util "^27.5.1" - jest-worker "^27.5.1" + jest-regex-util "^29.4.3" + jest-util "^29.6.2" + jest-worker "^29.6.2" micromatch "^4.0.4" - walker "^1.0.7" + walker "^1.0.8" optionalDependencies: fsevents "^2.3.2" -jest-jasmine2@^27.5.1: - version "27.5.1" - resolved "https://registry.yarnpkg.com/jest-jasmine2/-/jest-jasmine2-27.5.1.tgz#a037b0034ef49a9f3d71c4375a796f3b230d1ac4" - integrity sha512-jtq7VVyG8SqAorDpApwiJJImd0V2wv1xzdheGHRGyuT7gZm6gG47QEskOlzsN1PG/6WNaCo5pmwMHDf3AkG2pQ== - dependencies: - "@jest/environment" "^27.5.1" - "@jest/source-map" "^27.5.1" - "@jest/test-result" "^27.5.1" - "@jest/types" "^27.5.1" - "@types/node" "*" - chalk "^4.0.0" - co "^4.6.0" - expect "^27.5.1" - is-generator-fn "^2.0.0" - jest-each "^27.5.1" - jest-matcher-utils "^27.5.1" - jest-message-util "^27.5.1" - jest-runtime "^27.5.1" - jest-snapshot "^27.5.1" - jest-util "^27.5.1" - pretty-format "^27.5.1" - throat "^6.0.1" - -jest-leak-detector@^27.5.1: - version "27.5.1" - resolved "https://registry.yarnpkg.com/jest-leak-detector/-/jest-leak-detector-27.5.1.tgz#6ec9d54c3579dd6e3e66d70e3498adf80fde3fb8" - integrity sha512-POXfWAMvfU6WMUXftV4HolnJfnPOGEu10fscNCA76KBpRRhcMN2c8d3iT2pxQS3HLbA+5X4sOUPzYO2NUyIlHQ== +jest-leak-detector@^29.6.2: + version "29.6.2" + resolved "https://registry.yarnpkg.com/jest-leak-detector/-/jest-leak-detector-29.6.2.tgz#e2b307fee78cab091c37858a98c7e1d73cdf5b38" + integrity sha512-aNqYhfp5uYEO3tdWMb2bfWv6f0b4I0LOxVRpnRLAeque2uqOVVMLh6khnTcE2qJ5wAKop0HcreM1btoysD6bPQ== dependencies: - jest-get-type "^27.5.1" - pretty-format "^27.5.1" + jest-get-type "^29.4.3" + pretty-format "^29.6.2" -jest-matcher-utils@^27.0.0, jest-matcher-utils@^27.5.1: - version "27.5.1" - resolved "https://registry.yarnpkg.com/jest-matcher-utils/-/jest-matcher-utils-27.5.1.tgz#9c0cdbda8245bc22d2331729d1091308b40cf8ab" - integrity sha512-z2uTx/T6LBaCoNWNFWwChLBKYxTMcGBRjAt+2SbP929/Fflb9aa5LGma654Rz8z9HLxsrUaYzxE9T/EFIL/PAw== +jest-matcher-utils@^29.6.2: + version "29.6.2" + resolved "https://registry.yarnpkg.com/jest-matcher-utils/-/jest-matcher-utils-29.6.2.tgz#39de0be2baca7a64eacb27291f0bd834fea3a535" + integrity sha512-4LiAk3hSSobtomeIAzFTe+N8kL6z0JtF3n6I4fg29iIW7tt99R7ZcIFW34QkX+DuVrf+CUe6wuVOpm7ZKFJzZQ== dependencies: chalk "^4.0.0" - jest-diff "^27.5.1" - jest-get-type "^27.5.1" - pretty-format "^27.5.1" + jest-diff "^29.6.2" + jest-get-type "^29.4.3" + pretty-format "^29.6.2" -jest-message-util@^27.5.1: - version "27.5.1" - resolved "https://registry.yarnpkg.com/jest-message-util/-/jest-message-util-27.5.1.tgz#bdda72806da10d9ed6425e12afff38cd1458b6cf" - integrity sha512-rMyFe1+jnyAAf+NHwTclDz0eAaLkVDdKVHHBFWsBWHnnh5YeJMNWWsv7AbFYXfK3oTqvL7VTWkhNLu1jX24D+g== +jest-message-util@^29.6.2: + version "29.6.2" + resolved "https://registry.yarnpkg.com/jest-message-util/-/jest-message-util-29.6.2.tgz#af7adc2209c552f3f5ae31e77cf0a261f23dc2bb" + integrity sha512-vnIGYEjoPSuRqV8W9t+Wow95SDp6KPX2Uf7EoeG9G99J2OVh7OSwpS4B6J0NfpEIpfkBNHlBZpA2rblEuEFhZQ== dependencies: "@babel/code-frame" "^7.12.13" - "@jest/types" "^27.5.1" + "@jest/types" "^29.6.1" "@types/stack-utils" "^2.0.0" chalk "^4.0.0" graceful-fs "^4.2.9" micromatch "^4.0.4" - pretty-format "^27.5.1" + pretty-format "^29.6.2" slash "^3.0.0" stack-utils "^2.0.3" -jest-mock@^27.5.1: - version "27.5.1" - resolved "https://registry.yarnpkg.com/jest-mock/-/jest-mock-27.5.1.tgz#19948336d49ef4d9c52021d34ac7b5f36ff967d6" - integrity sha512-K4jKbY1d4ENhbrG2zuPWaQBvDly+iZ2yAW+T1fATN78hc0sInwn7wZB8XtlNnvHug5RMwV897Xm4LqmPM4e2Og== +jest-mock@^29.6.2: + version "29.6.2" + resolved "https://registry.yarnpkg.com/jest-mock/-/jest-mock-29.6.2.tgz#ef9c9b4d38c34a2ad61010a021866dad41ce5e00" + integrity sha512-hoSv3lb3byzdKfwqCuT6uTscan471GUECqgNYykg6ob0yiAw3zYc7OrPnI9Qv8Wwoa4lC7AZ9hyS4AiIx5U2zg== dependencies: - "@jest/types" "^27.5.1" + "@jest/types" "^29.6.1" "@types/node" "*" + jest-util "^29.6.2" jest-pnp-resolver@^1.2.2: version "1.2.3" resolved "https://registry.yarnpkg.com/jest-pnp-resolver/-/jest-pnp-resolver-1.2.3.tgz#930b1546164d4ad5937d5540e711d4d38d4cad2e" integrity sha512-+3NpwQEnRoIBtx4fyhblQDPgJI0H1IEIkX7ShLUjPGA7TtUTvI1oiKi3SR4oBR0hQhQR80l4WAe5RrXBwWMA8w== -jest-regex-util@^27.5.1: - version "27.5.1" - resolved "https://registry.yarnpkg.com/jest-regex-util/-/jest-regex-util-27.5.1.tgz#4da143f7e9fd1e542d4aa69617b38e4a78365b95" - integrity sha512-4bfKq2zie+x16okqDXjXn9ql2B0dScQu+vcwe4TvFVhkVyuWLqpZrZtXxLLWoXYgn0E87I6r6GRYHF7wFZBUvg== +jest-regex-util@^29.4.3: + version "29.4.3" + resolved "https://registry.yarnpkg.com/jest-regex-util/-/jest-regex-util-29.4.3.tgz#a42616141e0cae052cfa32c169945d00c0aa0bb8" + integrity sha512-O4FglZaMmWXbGHSQInfXewIsd1LMn9p3ZXB/6r4FOkyhX2/iP/soMG98jGvk/A3HAN78+5VWcBGO0BJAPRh4kg== -jest-resolve-dependencies@^27.5.1: - version "27.5.1" - resolved "https://registry.yarnpkg.com/jest-resolve-dependencies/-/jest-resolve-dependencies-27.5.1.tgz#d811ecc8305e731cc86dd79741ee98fed06f1da8" - integrity sha512-QQOOdY4PE39iawDn5rzbIePNigfe5B9Z91GDD1ae/xNDlu9kaat8QQ5EKnNmVWPV54hUdxCVwwj6YMgR2O7IOg== +jest-resolve-dependencies@^29.6.2: + version "29.6.2" + resolved "https://registry.yarnpkg.com/jest-resolve-dependencies/-/jest-resolve-dependencies-29.6.2.tgz#36435269b6672c256bcc85fb384872c134cc4cf2" + integrity sha512-LGqjDWxg2fuQQm7ypDxduLu/m4+4Lb4gczc13v51VMZbVP5tSBILqVx8qfWcsdP8f0G7aIqByIALDB0R93yL+w== dependencies: - "@jest/types" "^27.5.1" - jest-regex-util "^27.5.1" - jest-snapshot "^27.5.1" + jest-regex-util "^29.4.3" + jest-snapshot "^29.6.2" -jest-resolve@^27.5.1: - version "27.5.1" - resolved "https://registry.yarnpkg.com/jest-resolve/-/jest-resolve-27.5.1.tgz#a2f1c5a0796ec18fe9eb1536ac3814c23617b384" - integrity sha512-FFDy8/9E6CV83IMbDpcjOhumAQPDyETnU2KZ1O98DwTnz8AOBsW/Xv3GySr1mOZdItLR+zDZ7I/UdTFbgSOVCw== +jest-resolve@^29.6.2: + version "29.6.2" + resolved "https://registry.yarnpkg.com/jest-resolve/-/jest-resolve-29.6.2.tgz#f18405fe4b50159b7b6d85e81f6a524d22afb838" + integrity sha512-G/iQUvZWI5e3SMFssc4ug4dH0aZiZpsDq9o1PtXTV1210Ztyb2+w+ZgQkB3iOiC5SmAEzJBOHWz6Hvrd+QnNPw== dependencies: - "@jest/types" "^27.5.1" chalk "^4.0.0" graceful-fs "^4.2.9" - jest-haste-map "^27.5.1" + jest-haste-map "^29.6.2" jest-pnp-resolver "^1.2.2" - jest-util "^27.5.1" - jest-validate "^27.5.1" + jest-util "^29.6.2" + jest-validate "^29.6.2" resolve "^1.20.0" - resolve.exports "^1.1.0" + resolve.exports "^2.0.0" slash "^3.0.0" -jest-runner@^27.5.1: - version "27.5.1" - resolved "https://registry.yarnpkg.com/jest-runner/-/jest-runner-27.5.1.tgz#071b27c1fa30d90540805c5645a0ec167c7b62e5" - integrity sha512-g4NPsM4mFCOwFKXO4p/H/kWGdJp9V8kURY2lX8Me2drgXqG7rrZAx5kv+5H7wtt/cdFIjhqYx1HrlqWHaOvDaQ== - dependencies: - "@jest/console" "^27.5.1" - "@jest/environment" "^27.5.1" - "@jest/test-result" "^27.5.1" - "@jest/transform" "^27.5.1" - "@jest/types" "^27.5.1" +jest-runner@^29.6.2: + version "29.6.2" + resolved "https://registry.yarnpkg.com/jest-runner/-/jest-runner-29.6.2.tgz#89e8e32a8fef24781a7c4c49cd1cb6358ac7fc01" + integrity sha512-wXOT/a0EspYgfMiYHxwGLPCZfC0c38MivAlb2lMEAlwHINKemrttu1uSbcGbfDV31sFaPWnWJPmb2qXM8pqZ4w== + dependencies: + "@jest/console" "^29.6.2" + "@jest/environment" "^29.6.2" + "@jest/test-result" "^29.6.2" + "@jest/transform" "^29.6.2" + "@jest/types" "^29.6.1" "@types/node" "*" chalk "^4.0.0" - emittery "^0.8.1" + emittery "^0.13.1" graceful-fs "^4.2.9" - jest-docblock "^27.5.1" - jest-environment-jsdom "^27.5.1" - jest-environment-node "^27.5.1" - jest-haste-map "^27.5.1" - jest-leak-detector "^27.5.1" - jest-message-util "^27.5.1" - jest-resolve "^27.5.1" - jest-runtime "^27.5.1" - jest-util "^27.5.1" - jest-worker "^27.5.1" - source-map-support "^0.5.6" - throat "^6.0.1" - -jest-runtime@^27.5.1: - version "27.5.1" - resolved "https://registry.yarnpkg.com/jest-runtime/-/jest-runtime-27.5.1.tgz#4896003d7a334f7e8e4a53ba93fb9bcd3db0a1af" - integrity sha512-o7gxw3Gf+H2IGt8fv0RiyE1+r83FJBRruoA+FXrlHw6xEyBsU8ugA6IPfTdVyA0w8HClpbK+DGJxH59UrNMx8A== - dependencies: - "@jest/environment" "^27.5.1" - "@jest/fake-timers" "^27.5.1" - "@jest/globals" "^27.5.1" - "@jest/source-map" "^27.5.1" - "@jest/test-result" "^27.5.1" - "@jest/transform" "^27.5.1" - "@jest/types" "^27.5.1" + jest-docblock "^29.4.3" + jest-environment-node "^29.6.2" + jest-haste-map "^29.6.2" + jest-leak-detector "^29.6.2" + jest-message-util "^29.6.2" + jest-resolve "^29.6.2" + jest-runtime "^29.6.2" + jest-util "^29.6.2" + jest-watcher "^29.6.2" + jest-worker "^29.6.2" + p-limit "^3.1.0" + source-map-support "0.5.13" + +jest-runtime@^29.6.2: + version "29.6.2" + resolved "https://registry.yarnpkg.com/jest-runtime/-/jest-runtime-29.6.2.tgz#692f25e387f982e89ab83270e684a9786248e545" + integrity sha512-2X9dqK768KufGJyIeLmIzToDmsN0m7Iek8QNxRSI/2+iPFYHF0jTwlO3ftn7gdKd98G/VQw9XJCk77rbTGZnJg== + dependencies: + "@jest/environment" "^29.6.2" + "@jest/fake-timers" "^29.6.2" + "@jest/globals" "^29.6.2" + "@jest/source-map" "^29.6.0" + "@jest/test-result" "^29.6.2" + "@jest/transform" "^29.6.2" + "@jest/types" "^29.6.1" + "@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 "^27.5.1" - jest-message-util "^27.5.1" - jest-mock "^27.5.1" - jest-regex-util "^27.5.1" - jest-resolve "^27.5.1" - jest-snapshot "^27.5.1" - jest-util "^27.5.1" + jest-haste-map "^29.6.2" + jest-message-util "^29.6.2" + jest-mock "^29.6.2" + jest-regex-util "^29.4.3" + jest-resolve "^29.6.2" + jest-snapshot "^29.6.2" + jest-util "^29.6.2" slash "^3.0.0" strip-bom "^4.0.0" -jest-serializer@^27.5.1: - version "27.5.1" - resolved "https://registry.yarnpkg.com/jest-serializer/-/jest-serializer-27.5.1.tgz#81438410a30ea66fd57ff730835123dea1fb1f64" - integrity sha512-jZCyo6iIxO1aqUxpuBlwTDMkzOAJS4a3eYz3YzgxxVQFwLeSA7Jfq5cbqCY+JLvTDrWirgusI/0KwxKMgrdf7w== - dependencies: - "@types/node" "*" - graceful-fs "^4.2.9" - -jest-snapshot@^27.5.1: - version "27.5.1" - resolved "https://registry.yarnpkg.com/jest-snapshot/-/jest-snapshot-27.5.1.tgz#b668d50d23d38054a51b42c4039cab59ae6eb6a1" - integrity sha512-yYykXI5a0I31xX67mgeLw1DZ0bJB+gpq5IpSuCAoyDi0+BhgU/RIrL+RTzDmkNTchvDFWKP8lp+w/42Z3us5sA== +jest-snapshot@^29.6.2: + version "29.6.2" + resolved "https://registry.yarnpkg.com/jest-snapshot/-/jest-snapshot-29.6.2.tgz#9b431b561a83f2bdfe041e1cab8a6becdb01af9c" + integrity sha512-1OdjqvqmRdGNvWXr/YZHuyhh5DeaLp1p/F8Tht/MrMw4Kr1Uu/j4lRG+iKl1DAqUJDWxtQBMk41Lnf/JETYBRA== dependencies: - "@babel/core" "^7.7.2" + "@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.0.0" - "@jest/transform" "^27.5.1" - "@jest/types" "^27.5.1" - "@types/babel__traverse" "^7.0.4" - "@types/prettier" "^2.1.5" + "@babel/types" "^7.3.3" + "@jest/expect-utils" "^29.6.2" + "@jest/transform" "^29.6.2" + "@jest/types" "^29.6.1" babel-preset-current-node-syntax "^1.0.0" chalk "^4.0.0" - expect "^27.5.1" + expect "^29.6.2" graceful-fs "^4.2.9" - jest-diff "^27.5.1" - jest-get-type "^27.5.1" - jest-haste-map "^27.5.1" - jest-matcher-utils "^27.5.1" - jest-message-util "^27.5.1" - jest-util "^27.5.1" + jest-diff "^29.6.2" + jest-get-type "^29.4.3" + jest-matcher-utils "^29.6.2" + jest-message-util "^29.6.2" + jest-util "^29.6.2" natural-compare "^1.4.0" - pretty-format "^27.5.1" - semver "^7.3.2" + pretty-format "^29.6.2" + semver "^7.5.3" -jest-util@^27.0.0, jest-util@^27.5.1: - version "27.5.1" - resolved "https://registry.yarnpkg.com/jest-util/-/jest-util-27.5.1.tgz#3ba9771e8e31a0b85da48fe0b0891fb86c01c2f9" - integrity sha512-Kv2o/8jNvX1MQ0KGtw480E/w4fBCDOnH6+6DmeKi6LZUIlKA5kwY0YNdlzaWTiVgxqAqik11QyxDOKk543aKXw== +jest-util@^29.0.0, jest-util@^29.6.2: + version "29.6.2" + resolved "https://registry.yarnpkg.com/jest-util/-/jest-util-29.6.2.tgz#8a052df8fff2eebe446769fd88814521a517664d" + integrity sha512-3eX1qb6L88lJNCFlEADKOkjpXJQyZRiavX1INZ4tRnrBVr2COd3RgcTLyUiEXMNBlDU/cgYq6taUS0fExrWW4w== dependencies: - "@jest/types" "^27.5.1" + "@jest/types" "^29.6.1" "@types/node" "*" chalk "^4.0.0" ci-info "^3.2.0" graceful-fs "^4.2.9" picomatch "^2.2.3" -jest-validate@^27.5.1: - version "27.5.1" - resolved "https://registry.yarnpkg.com/jest-validate/-/jest-validate-27.5.1.tgz#9197d54dc0bdb52260b8db40b46ae668e04df067" - integrity sha512-thkNli0LYTmOI1tDB3FI1S1RTp/Bqyd9pTarJwL87OIBFuqEb5Apv5EaApEudYg4g86e3CT6kM0RowkhtEnCBQ== +jest-validate@^29.6.2: + version "29.6.2" + resolved "https://registry.yarnpkg.com/jest-validate/-/jest-validate-29.6.2.tgz#25d972af35b2415b83b1373baf1a47bb266c1082" + integrity sha512-vGz0yMN5fUFRRbpJDPwxMpgSXW1LDKROHfBopAvDcmD6s+B/s8WJrwi+4bfH4SdInBA5C3P3BI19dBtKzx1Arg== dependencies: - "@jest/types" "^27.5.1" + "@jest/types" "^29.6.1" camelcase "^6.2.0" chalk "^4.0.0" - jest-get-type "^27.5.1" + jest-get-type "^29.4.3" leven "^3.1.0" - pretty-format "^27.5.1" + pretty-format "^29.6.2" -jest-watcher@^27.5.1: - version "27.5.1" - resolved "https://registry.yarnpkg.com/jest-watcher/-/jest-watcher-27.5.1.tgz#71bd85fb9bde3a2c2ec4dc353437971c43c642a2" - integrity sha512-z676SuD6Z8o8qbmEGhoEUFOM1+jfEiL3DXHK/xgEiG2EyNYfFG60jluWcupY6dATjfEsKQuibReS1djInQnoVw== +jest-watcher@^29.6.2: + version "29.6.2" + resolved "https://registry.yarnpkg.com/jest-watcher/-/jest-watcher-29.6.2.tgz#77c224674f0620d9f6643c4cfca186d8893ca088" + integrity sha512-GZitlqkMkhkefjfN/p3SJjrDaxPflqxEAv3/ik10OirZqJGYH5rPiIsgVcfof0Tdqg3shQGdEIxDBx+B4tuLzA== dependencies: - "@jest/test-result" "^27.5.1" - "@jest/types" "^27.5.1" + "@jest/test-result" "^29.6.2" + "@jest/types" "^29.6.1" "@types/node" "*" ansi-escapes "^4.2.1" chalk "^4.0.0" - jest-util "^27.5.1" + emittery "^0.13.1" + jest-util "^29.6.2" string-length "^4.0.1" -jest-worker@^27.4.5, jest-worker@^27.5.1: +jest-worker@^27.4.5: version "27.5.1" resolved "https://registry.yarnpkg.com/jest-worker/-/jest-worker-27.5.1.tgz#8d146f0900e8973b106b6f73cc1e9a8cb86f8db0" integrity sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg== @@ -3827,14 +4010,25 @@ jest-worker@^27.4.5, jest-worker@^27.5.1: merge-stream "^2.0.0" supports-color "^8.0.0" -jest@^27.2.5: - version "27.5.1" - resolved "https://registry.yarnpkg.com/jest/-/jest-27.5.1.tgz#dadf33ba70a779be7a6fc33015843b51494f63fc" - integrity sha512-Yn0mADZB89zTtjkPJEXwrac3LHudkQMR+Paqa8uxJHCBr9agxztUifWCyiYrjhMPBoUVBjyny0I7XH6ozDr7QQ== +jest-worker@^29.6.2: + version "29.6.2" + resolved "https://registry.yarnpkg.com/jest-worker/-/jest-worker-29.6.2.tgz#682fbc4b6856ad0aa122a5403c6d048b83f3fb44" + integrity sha512-l3ccBOabTdkng8I/ORCkADz4eSMKejTYv1vB/Z83UiubqhC1oQ5Li6dWCyqOIvSifGjUBxuvxvlm6KGK2DtuAQ== + dependencies: + "@types/node" "*" + jest-util "^29.6.2" + merge-stream "^2.0.0" + supports-color "^8.0.0" + +jest@^29.6.2: + version "29.6.2" + resolved "https://registry.yarnpkg.com/jest/-/jest-29.6.2.tgz#3bd55b9fd46a161b2edbdf5f1d1bd0d1eab76c42" + integrity sha512-8eQg2mqFbaP7CwfsTpCxQ+sHzw1WuNWL5UUvjnWP4hx2riGz9fPSzYOaU5q8/GqWn1TfgZIVTqYJygbGbWAANg== dependencies: - "@jest/core" "^27.5.1" + "@jest/core" "^29.6.2" + "@jest/types" "^29.6.1" import-local "^3.0.2" - jest-cli "^27.5.1" + jest-cli "^29.6.2" js-tokens@^4.0.0: version "4.0.0" @@ -3884,38 +4078,37 @@ jsdoc@^4.0.0: strip-json-comments "^3.1.0" underscore "~1.13.2" -jsdom@^16.6.0: - version "16.7.0" - resolved "https://registry.yarnpkg.com/jsdom/-/jsdom-16.7.0.tgz#918ae71965424b197c819f8183a754e18977b710" - integrity sha512-u9Smc2G1USStM+s/x1ru5Sxrl6mPYCbByG1U/hUmqaVsm4tbNyS7CicOSRyuGQYZhTu0h84qkZZQ/I+dzizSVw== +jsdom@^20.0.0: + version "20.0.3" + resolved "https://registry.yarnpkg.com/jsdom/-/jsdom-20.0.3.tgz#886a41ba1d4726f67a8858028c99489fed6ad4db" + integrity sha512-SYhBvTh89tTfCD/CRdSOm13mOBa42iTaTyfyEWBdKcGdPxPtLFBXuHR8XHb33YNYaP+lLbmSvBTsnoesCNJEsQ== dependencies: - abab "^2.0.5" - acorn "^8.2.4" - acorn-globals "^6.0.0" - cssom "^0.4.4" + abab "^2.0.6" + acorn "^8.8.1" + acorn-globals "^7.0.0" + cssom "^0.5.0" cssstyle "^2.3.0" - data-urls "^2.0.0" - decimal.js "^10.2.1" - domexception "^2.0.1" + data-urls "^3.0.2" + decimal.js "^10.4.2" + domexception "^4.0.0" escodegen "^2.0.0" - form-data "^3.0.0" - html-encoding-sniffer "^2.0.1" - http-proxy-agent "^4.0.1" - https-proxy-agent "^5.0.0" + form-data "^4.0.0" + html-encoding-sniffer "^3.0.0" + http-proxy-agent "^5.0.0" + https-proxy-agent "^5.0.1" is-potential-custom-element-name "^1.0.1" - nwsapi "^2.2.0" - parse5 "6.0.1" - saxes "^5.0.1" + nwsapi "^2.2.2" + parse5 "^7.1.1" + saxes "^6.0.0" symbol-tree "^3.2.4" - tough-cookie "^4.0.0" - w3c-hr-time "^1.0.2" - w3c-xmlserializer "^2.0.0" - webidl-conversions "^6.1.0" - whatwg-encoding "^1.0.5" - whatwg-mimetype "^2.3.0" - whatwg-url "^8.5.0" - ws "^7.4.6" - xml-name-validator "^3.0.0" + tough-cookie "^4.1.2" + w3c-xmlserializer "^4.0.0" + webidl-conversions "^7.0.0" + whatwg-encoding "^2.0.0" + whatwg-mimetype "^3.0.0" + whatwg-url "^11.0.0" + ws "^8.11.0" + xml-name-validator "^4.0.0" jsesc@^2.5.1: version "2.5.2" @@ -3949,11 +4142,6 @@ json-stable-stringify-without-jsonify@^1.0.1: resolved "https://registry.yarnpkg.com/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz#9db7b59496ad3f3cfef30a75142d2d930ad72651" integrity sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw== -json5@2.x, json5@^2.2.2: - version "2.2.3" - resolved "https://registry.yarnpkg.com/json5/-/json5-2.2.3.tgz#78cd6f1a19bdc12b73db5ad0c61efd66c1e29283" - integrity sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg== - json5@^1.0.1, json5@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/json5/-/json5-1.0.2.tgz#63d98d60f21b313b77c4d6da18bfa69d80e1d593" @@ -3961,6 +4149,11 @@ json5@^1.0.1, json5@^1.0.2: dependencies: minimist "^1.2.0" +json5@^2.2.2, json5@^2.2.3: + version "2.2.3" + resolved "https://registry.yarnpkg.com/json5/-/json5-2.2.3.tgz#78cd6f1a19bdc12b73db5ad0c61efd66c1e29283" + integrity sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg== + jsonc-parser@3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/jsonc-parser/-/jsonc-parser-3.0.0.tgz#abdd785701c7e7eaca8a9ec8cf070ca51a745a22" @@ -4076,7 +4269,7 @@ lodash.merge@^4.6.2: resolved "https://registry.yarnpkg.com/lodash.merge/-/lodash.merge-4.6.2.tgz#558aa53b43b661e1925a0afdfa36a9a1085fe57a" integrity sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ== -lodash@4.17.21, lodash@^4.17.15, lodash@^4.17.19, lodash@^4.17.21, lodash@^4.7.0: +lodash@4.17.21, lodash@^4.17.15, lodash@^4.17.19, lodash@^4.17.21: version "4.17.21" resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c" integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg== @@ -4368,6 +4561,11 @@ node-int64@^0.4.0: resolved "https://registry.yarnpkg.com/node-int64/-/node-int64-0.4.0.tgz#87a9065cdb355d3182d8f94ce11188b825c68a3b" integrity sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw== +node-releases@^2.0.12: + version "2.0.13" + resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-2.0.13.tgz#d5ed1627c23e3461e819b02e57b75e4899b1c81d" + integrity sha512-uYr7J37ae/ORWdZeQ1xxMJe3NtdmqMC/JZK+geofDrkLUApKRHPd18/TxtBOJ4A0/+uUIliorNrfYV6s1b02eQ== + node-releases@^2.0.8: version "2.0.10" resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-2.0.10.tgz#c311ebae3b6a148c89b1813fd7c4d3c024ef537f" @@ -4385,10 +4583,10 @@ npm-run-path@^4.0.0, npm-run-path@^4.0.1: dependencies: path-key "^3.0.0" -nwsapi@^2.2.0: - version "2.2.4" - resolved "https://registry.yarnpkg.com/nwsapi/-/nwsapi-2.2.4.tgz#fd59d5e904e8e1f03c25a7d5a15cfa16c714a1e5" - integrity sha512-NHj4rzRo0tQdijE9ZqAx6kYDcoRwYwSYzCA8MY3JzfxlrvEU0jhnhJT9BhqhJs7I/dKcrDm6TyulaRqZPIhN5g== +nwsapi@^2.2.2: + version "2.2.7" + resolved "https://registry.yarnpkg.com/nwsapi/-/nwsapi-2.2.7.tgz#738e0707d3128cb750dddcfe90e4610482df0f30" + integrity sha512-ub5E4+FBPKwAZx0UwIQOjYWGHTEq5sPqHQNRN8Z9e4A7u3Tj1weLJsL59yH9vmvqEtBHaOmT6cYQKIZOxp35FQ== object-assign@^4, object-assign@^4.1.1: version "4.1.1" @@ -4485,7 +4683,7 @@ p-limit@^2.2.0: dependencies: p-try "^2.0.0" -p-limit@^3.0.2: +p-limit@^3.0.2, p-limit@^3.1.0: version "3.1.0" resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-3.1.0.tgz#e1daccbe78d0d1388ca18c64fea38e3e57e3706b" integrity sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ== @@ -4528,10 +4726,12 @@ parse-json@^5.0.0, parse-json@^5.2.0: json-parse-even-better-errors "^2.3.0" lines-and-columns "^1.1.6" -parse5@6.0.1: - version "6.0.1" - resolved "https://registry.yarnpkg.com/parse5/-/parse5-6.0.1.tgz#e1a1c085c569b3dc08321184f19a39cc27f7c30b" - integrity sha512-Ofn/CTFzRGTTxwpNEs9PP93gXShHcTq255nzRYSKe8AkVpZY7e1fpmTfOyoIvjP5HG7Z2ZM7VS9PPhQGW2pOpw== +parse5@^7.0.0, parse5@^7.1.1: + version "7.1.2" + resolved "https://registry.yarnpkg.com/parse5/-/parse5-7.1.2.tgz#0736bebbfd77793823240a23b7fc5e010b7f8e32" + integrity sha512-Czj1WaSVpaoj0wbhMzLmWD69anp2WH7FXMB9n1Sy8/ZFF9jolSQVMu1Ij5WIyGmcBmhk7EOndpO4mIpihVqAXw== + dependencies: + entities "^4.4.0" parseurl@~1.3.3: version "1.3.3" @@ -4632,21 +4832,21 @@ prettier@^2.3.2: resolved "https://registry.yarnpkg.com/prettier/-/prettier-2.8.8.tgz#e8c5d7e98a4305ffe3de2e1fc4aca1a71c28b1da" integrity sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q== -pretty-format@^27.0.0, pretty-format@^27.5.1: - version "27.5.1" - resolved "https://registry.yarnpkg.com/pretty-format/-/pretty-format-27.5.1.tgz#2181879fdea51a7a5851fb39d920faa63f01d88e" - integrity sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ== +pretty-format@^29.0.0, pretty-format@^29.6.2: + version "29.6.2" + resolved "https://registry.yarnpkg.com/pretty-format/-/pretty-format-29.6.2.tgz#3d5829261a8a4d89d8b9769064b29c50ed486a47" + integrity sha512-1q0oC8eRveTg5nnBEWMXAU2qpv65Gnuf2eCQzSjxpWFkPaPARwqZZDGuNE0zPAZfTCHzIk3A8dIjwlQKKLphyg== dependencies: - ansi-regex "^5.0.1" + "@jest/schemas" "^29.6.0" ansi-styles "^5.0.0" - react-is "^17.0.1" + react-is "^18.0.0" -prisma@^4.15.0: - version "4.15.0" - resolved "https://registry.yarnpkg.com/prisma/-/prisma-4.15.0.tgz#4faa94f0d584828b68468953ff0bc88f37912c8c" - integrity sha512-iKZZpobPl48gTcSZVawLMQ3lEy6BnXwtoMj7hluoGFYu2kQ6F9LBuBrUyF95zRVnNo8/3KzLXJXJ5TEnLSJFiA== +prisma@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/prisma/-/prisma-5.0.0.tgz#f6571c46dc2478172cb7bc1bb62d74026a2c2630" + integrity sha512-KYWk83Fhi1FH59jSpavAYTt2eoMVW9YKgu8ci0kuUnt6Dup5Qy47pcB4/TLmiPAbhGrxxSz7gsSnJcCmkyPANA== dependencies: - "@prisma/engines" "4.15.0" + "@prisma/engines" "5.0.0" process-nextick-args@~2.0.0: version "2.0.1" @@ -4733,6 +4933,11 @@ punycode@^2.1.0, punycode@^2.1.1: resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.3.0.tgz#f67fa67c94da8f4d0cfff981aee4118064199b8f" integrity sha512-rRV+zQD8tVFys26lAGR9WUuS4iUAngJScM+ZRSKtvl5tKeZ2t5bvdNFdNHBW9FWR4guGHlgmsZ1G7BSm2wTbuA== +pure-rand@^6.0.0: + version "6.0.2" + resolved "https://registry.yarnpkg.com/pure-rand/-/pure-rand-6.0.2.tgz#a9c2ddcae9b68d736a8163036f088a2781c8b306" + integrity sha512-6Yg0ekpKICSjPswYOuC5sku/TSWaRYlA0qsXqJgM/d/4pLPHPuTxK7Nbf7jFKzAeedUhR8C7K9Uv63FBsSo8xQ== + qs@6.10.3: version "6.10.3" resolved "https://registry.yarnpkg.com/qs/-/qs-6.10.3.tgz#d6cde1b2ffca87b5aa57889816c5f81535e22e8e" @@ -4779,10 +4984,10 @@ raw-body@2.5.1: iconv-lite "0.4.24" unpipe "1.0.0" -react-is@^17.0.1: - version "17.0.2" - resolved "https://registry.yarnpkg.com/react-is/-/react-is-17.0.2.tgz#e691d4a8e9c789365655539ab372762b0efb54f0" - integrity sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w== +react-is@^18.0.0: + version "18.2.0" + resolved "https://registry.yarnpkg.com/react-is/-/react-is-18.2.0.tgz#199431eeaaa2e09f86427efbb4f1473edb47609b" + integrity sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w== readable-stream@^2.2.2: version "2.3.8" @@ -4864,10 +5069,10 @@ resolve-from@^5.0.0: resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-5.0.0.tgz#c35225843df8f776df21c57557bc087e9dfdfc69" integrity sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw== -resolve.exports@^1.1.0: - version "1.1.1" - resolved "https://registry.yarnpkg.com/resolve.exports/-/resolve.exports-1.1.1.tgz#05cfd5b3edf641571fd46fa608b610dda9ead999" - integrity sha512-/NtpHNDN7jWhAaQ9BvBUYZ6YTXsRBgfqWFWP7BZBaoMJO/I3G5OFzvTuWNlZC3aPjins1F+TNrLKsGbH4rfsRQ== +resolve.exports@^2.0.0: + version "2.0.2" + resolved "https://registry.yarnpkg.com/resolve.exports/-/resolve.exports-2.0.2.tgz#f8c934b8e6a13f539e38b7098e2e36134f01e800" + integrity sha512-X2UW6Nw3n/aMgDVy+0rSqgHlv39WZAlZrXCdnbyEiKm17DSqHX4MmQMaST3FbeWR5FTuRcUwYAziZajji0Y7mg== resolve@^1.1.6, resolve@^1.20.0: version "1.22.2" @@ -4942,15 +5147,15 @@ safe-buffer@~5.1.0, safe-buffer@~5.1.1: resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d" integrity sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g== -"safer-buffer@>= 2.1.2 < 3": +"safer-buffer@>= 2.1.2 < 3", "safer-buffer@>= 2.1.2 < 3.0.0": version "2.1.2" resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a" integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg== -saxes@^5.0.1: - version "5.0.1" - resolved "https://registry.yarnpkg.com/saxes/-/saxes-5.0.1.tgz#eebab953fa3b7608dbe94e5dadb15c888fa6696d" - integrity sha512-5LBh1Tls8c9xgGjw3QrMwETmTMVk0oFgvrFSvWx62llR2hcEInrKNZ2GZCCuuy2lvWrdl5jhbpeqc5hRYKFOcw== +saxes@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/saxes/-/saxes-6.0.0.tgz#fe5b4a4768df4f14a201b1ba6a65c1f3d9988cc5" + integrity sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA== dependencies: xmlchars "^2.2.0" @@ -4963,18 +5168,16 @@ schema-utils@^3.1.0, schema-utils@^3.1.1: ajv "^6.12.5" ajv-keywords "^3.5.2" -semver@7.x, semver@^7.3.2, semver@^7.3.4, semver@^7.3.5: - version "7.5.0" - resolved "https://registry.yarnpkg.com/semver/-/semver-7.5.0.tgz#ed8c5dc8efb6c629c88b23d41dc9bf40c1d96cd0" - integrity sha512-+XC0AD/R7Q2mPSRuy2Id0+CGTZ98+8f+KvwirxOKIEyid+XSx6HbC63p+O4IndTHuX5Z+JxQ0TghCkO5Cg/2HA== - dependencies: - lru-cache "^6.0.0" - semver@^6.0.0, semver@^6.3.0: version "6.3.0" resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.0.tgz#ee0a64c8af5e8ceea67687b133761e1becbd1d3d" integrity sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw== +semver@^6.3.1: + version "6.3.1" + resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.1.tgz#556d2ef8689146e46dcea4bfdd095f3434dffcb4" + integrity sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA== + semver@^7.1.2, semver@^7.3.7, semver@^7.3.8: version "7.5.1" resolved "https://registry.yarnpkg.com/semver/-/semver-7.5.1.tgz#c90c4d631cf74720e46b21c1d37ea07edfab91ec" @@ -4982,6 +5185,20 @@ semver@^7.1.2, semver@^7.3.7, semver@^7.3.8: dependencies: lru-cache "^6.0.0" +semver@^7.3.4, semver@^7.3.5: + version "7.5.0" + resolved "https://registry.yarnpkg.com/semver/-/semver-7.5.0.tgz#ed8c5dc8efb6c629c88b23d41dc9bf40c1d96cd0" + integrity sha512-+XC0AD/R7Q2mPSRuy2Id0+CGTZ98+8f+KvwirxOKIEyid+XSx6HbC63p+O4IndTHuX5Z+JxQ0TghCkO5Cg/2HA== + dependencies: + lru-cache "^6.0.0" + +semver@^7.5.3: + version "7.5.4" + resolved "https://registry.yarnpkg.com/semver/-/semver-7.5.4.tgz#483986ec4ed38e1c6c48c34894a9182dbff68a6e" + integrity sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA== + dependencies: + lru-cache "^6.0.0" + send@0.18.0: version "0.18.0" resolved "https://registry.yarnpkg.com/send/-/send-0.18.0.tgz#670167cc654b05f5aa4a767f9113bb371bc706be" @@ -5053,7 +5270,7 @@ side-channel@^1.0.4: get-intrinsic "^1.0.2" object-inspect "^1.9.0" -signal-exit@^3.0.2, signal-exit@^3.0.3: +signal-exit@^3.0.2, signal-exit@^3.0.3, signal-exit@^3.0.7: version "3.0.7" resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.7.tgz#a9a1767f8af84155114eaabd73f99273c8f59ad9" integrity sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ== @@ -5068,7 +5285,15 @@ slash@^3.0.0: resolved "https://registry.yarnpkg.com/slash/-/slash-3.0.0.tgz#6539be870c165adbd5240220dbe361f1bc4d4634" integrity sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q== -source-map-support@0.5.21, source-map-support@^0.5.20, source-map-support@^0.5.6, source-map-support@~0.5.20: +source-map-support@0.5.13: + version "0.5.13" + resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.5.13.tgz#31b24a9c2e73c2de85066c0feb7d44767ed52932" + integrity sha512-SHSKFHadjVA5oR4PPqhtAVdcBWwRYVd6g6cAXnIbRiIwc2EhPrTuKUBdSLvlEKyIP3GCf89fltvcZiP9MMFA1w== + dependencies: + buffer-from "^1.0.0" + source-map "^0.6.0" + +source-map-support@0.5.21, source-map-support@^0.5.20, source-map-support@~0.5.20: version "0.5.21" resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.5.21.tgz#04fe7c7f9e1ed2d662233c28cb2b35b9f63f6e4f" integrity sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w== @@ -5086,11 +5311,6 @@ source-map@^0.6.0, source-map@^0.6.1, source-map@~0.6.1: resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.6.1.tgz#74722af32e9614e9c287a8d0bbde48b5e2f1a263" integrity sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g== -source-map@^0.7.3: - version "0.7.4" - resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.7.4.tgz#a9bbe705c9d8846f4e08ff6765acf0f1b0898656" - integrity sha512-l3BikUxvPOcn5E74dZiq5BGsTb5yEwhaTSzccU6t4sDOH8NWJCstKO5QT2CvtFoK6F0saL7p9xHAqHOlCPJygA== - sourcemap-codec@^1.4.4: version "1.4.8" resolved "https://registry.yarnpkg.com/sourcemap-codec/-/sourcemap-codec-1.4.8.tgz#ea804bd94857402e6992d05a38ef1ae35a9ab4c4" @@ -5229,7 +5449,7 @@ supports-color@^5.3.0: dependencies: has-flag "^3.0.0" -supports-color@^7.0.0, supports-color@^7.1.0: +supports-color@^7.1.0: version "7.2.0" resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-7.2.0.tgz#1b7dcdcb32b8138801b3e478ba6a51caa89648da" integrity sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw== @@ -5243,14 +5463,6 @@ supports-color@^8.0.0: dependencies: has-flag "^4.0.0" -supports-hyperlinks@^2.0.0: - version "2.3.0" - resolved "https://registry.yarnpkg.com/supports-hyperlinks/-/supports-hyperlinks-2.3.0.tgz#3943544347c1ff90b15effb03fc14ae45ec10624" - integrity sha512-RpsAZlpWcDwOPQA22aCH4J0t7L8JmAvsCxfOSEwm7cQs3LshN36QaTkwd70DnBOXDWGssw2eUoc8CaRWT0XunA== - dependencies: - has-flag "^4.0.0" - supports-color "^7.0.0" - supports-preserve-symlinks-flag@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz#6eda4bd344a3c94aea376d4cc31bc77311039e09" @@ -5299,14 +5511,6 @@ teeny-request@^8.0.0: stream-events "^1.0.5" uuid "^9.0.0" -terminal-link@^2.0.0: - version "2.1.1" - resolved "https://registry.yarnpkg.com/terminal-link/-/terminal-link-2.1.1.tgz#14a64a27ab3c0df933ea546fba55f2d078edc994" - integrity sha512-un0FmiRUQNr5PJqy9kP7c40F5BOfpGlYTrxonDChEZB7pzZxRNp/bt+ymiy9/npwXya9KH99nJ/GXFIiUkYGFQ== - dependencies: - ansi-escapes "^4.2.1" - supports-hyperlinks "^2.0.0" - terser-webpack-plugin@^5.1.3: version "5.3.8" resolved "https://registry.yarnpkg.com/terser-webpack-plugin/-/terser-webpack-plugin-5.3.8.tgz#415e03d2508f7de63d59eca85c5d102838f06610" @@ -5342,11 +5546,6 @@ text-table@^0.2.0: resolved "https://registry.yarnpkg.com/text-table/-/text-table-0.2.0.tgz#7f5ee823ae805207c00af2df4a84ec3fcfa570b4" integrity sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw== -throat@^6.0.1: - version "6.0.2" - resolved "https://registry.yarnpkg.com/throat/-/throat-6.0.2.tgz#51a3fbb5e11ae72e2cf74861ed5c8020f89f29fe" - integrity sha512-WKexMoJj3vEuK0yFEapj8y64V0A6xcuPuK9Gt1d0R+dzCSJc0lHqQytAbSB4cDAK0dWh4T0E2ETkoLE2WZ41OQ== - through@^2.3.6: version "2.3.8" resolved "https://registry.yarnpkg.com/through/-/through-2.3.8.tgz#0dd4c9ffaabc357960b1b724115d7e0e86a2e1f5" @@ -5388,20 +5587,20 @@ toidentifier@1.0.1: resolved "https://registry.yarnpkg.com/toidentifier/-/toidentifier-1.0.1.tgz#3be34321a88a820ed1bd80dfaa33e479fbb8dd35" integrity sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA== -tough-cookie@^4.0.0: - version "4.1.2" - resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-4.1.2.tgz#e53e84b85f24e0b65dd526f46628db6c85f6b874" - integrity sha512-G9fqXWoYFZgTc2z8Q5zaHy/vJMjm+WV0AkAeHxVCQiEB1b+dGvWzFW6QV07cY5jQ5gRkeid2qIkzkxUnmoQZUQ== +tough-cookie@^4.1.2: + version "4.1.3" + resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-4.1.3.tgz#97b9adb0728b42280aa3d814b6b999b2ff0318bf" + integrity sha512-aX/y5pVRkfRnfmuX+OdbSdXvPe6ieKX/G2s7e98f4poJHnqH3281gDPm/metm6E/WRamfx7WC4HUqkWHfQHprw== dependencies: psl "^1.1.33" punycode "^2.1.1" universalify "^0.2.0" url-parse "^1.5.3" -tr46@^2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/tr46/-/tr46-2.1.0.tgz#fa87aa81ca5d5941da8cbf1f9b749dc969a4e240" - integrity sha512-15Ih7phfcdP5YxqiB+iDtLoaTz4Nd35+IiAv0kQ5FNKHzXgdWqPoTIqEDDJmXceQt4JZk6lVPT8lnDlPpGDppw== +tr46@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/tr46/-/tr46-3.0.0.tgz#555c4e297a950617e8eeddef633c87d4d9d6cbf9" + integrity sha512-l7FvfAHlcmulp8kr+flpQZmVwtu7nfRV7NZujtN0OqES8EL4O4e0qqzL0DC5gAvx/ZC/9lk6rhcUwYvkBnBnYA== dependencies: punycode "^2.1.1" @@ -5415,19 +5614,19 @@ tree-kill@1.2.2: resolved "https://registry.yarnpkg.com/tree-kill/-/tree-kill-1.2.2.tgz#4ca09a9092c88b73a7cdc5e8a01b507b0790a0cc" integrity sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A== -ts-jest@^27.0.3: - version "27.1.5" - resolved "https://registry.yarnpkg.com/ts-jest/-/ts-jest-27.1.5.tgz#0ddf1b163fbaae3d5b7504a1e65c914a95cff297" - integrity sha512-Xv6jBQPoBEvBq/5i2TeSG9tt/nqkbpcurrEG1b+2yfBrcJelOZF9Ml6dmyMh7bcW9JyFbRYpR5rxROSlBLTZHA== +ts-jest@^29.1.1: + version "29.1.1" + resolved "https://registry.yarnpkg.com/ts-jest/-/ts-jest-29.1.1.tgz#f58fe62c63caf7bfcc5cc6472082f79180f0815b" + integrity sha512-D6xjnnbP17cC85nliwGiL+tpoKN0StpgE0TeOjXQTU6MVCfsB4v7aW05CgQ/1OywGb0x/oy9hHFnN+sczTiRaA== dependencies: bs-logger "0.x" fast-json-stable-stringify "2.x" - jest-util "^27.0.0" - json5 "2.x" + jest-util "^29.0.0" + json5 "^2.2.3" lodash.memoize "4.x" make-error "1.x" - semver "7.x" - yargs-parser "20.x" + semver "^7.5.3" + yargs-parser "^21.0.1" ts-loader@^9.2.3: version "9.4.3" @@ -5546,13 +5745,6 @@ type-is@^1.6.4, type-is@~1.6.18: media-typer "0.3.0" mime-types "~2.1.24" -typedarray-to-buffer@^3.1.5: - version "3.1.5" - resolved "https://registry.yarnpkg.com/typedarray-to-buffer/-/typedarray-to-buffer-3.1.5.tgz#a97ee7a9ff42691b9f783ff1bc5112fe3fca9080" - integrity sha512-zdu8XMNEDepKKR+XYOXAVPtWui0ly0NtohUscw+UmaHiAWT8hrV1rr//H6V+0DvJ3OQ19S979M0laLfX8rm82Q== - dependencies: - is-typedarray "^1.0.0" - typedarray@^0.0.6: version "0.0.6" resolved "https://registry.yarnpkg.com/typedarray/-/typedarray-0.0.6.tgz#867ac74e3864187b1d3d47d996a78ec5c8830777" @@ -5563,10 +5755,10 @@ typescript@4.7.4: resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.7.4.tgz#1a88596d1cf47d59507a1bcdfb5b9dfe4d488235" integrity sha512-C0WQT0gezHuw6AdY1M2jxUO83Rjf0HP7Sk1DtXj6j1EwkQNZrHAg2XPWlq62oqEhYvONq5pkC2Y9oPljWToLmQ== -typescript@^4.3.5: - version "4.9.5" - resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.9.5.tgz#095979f9bcc0d09da324d58d03ce8f8374cbe65a" - integrity sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g== +typescript@^5.1.6: + version "5.1.6" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.1.6.tgz#02f8ac202b6dad2c0dd5e0913745b47a37998274" + integrity sha512-zaWCozRZ6DLEWAWFrVDz1H6FVXzUSfTy5FUMWsQlU8Ym5JP9eO4xkTIROFCQvhQf61z6O/G6ugw3SgAnvvm+HA== uc.micro@^1.0.1, uc.micro@^1.0.5: version "1.0.6" @@ -5598,7 +5790,7 @@ unpipe@1.0.0, unpipe@~1.0.0: resolved "https://registry.yarnpkg.com/unpipe/-/unpipe-1.0.0.tgz#b2bf4ee8514aae6165b4817829d21b2ef49904ec" integrity sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ== -update-browserslist-db@^1.0.10: +update-browserslist-db@^1.0.10, update-browserslist-db@^1.0.11: version "1.0.11" resolved "https://registry.yarnpkg.com/update-browserslist-db/-/update-browserslist-db-1.0.11.tgz#9a2a641ad2907ae7b3616506f4b977851db5b940" integrity sha512-dCwEFf0/oT85M1fHBg4F0jtLwJrutGoHSQXCh7u4o2t1drG+c0a9Flnqww6XUKSfQMPpJBRjU8d4RXB09qtvaA== @@ -5646,14 +5838,14 @@ v8-compile-cache-lib@^3.0.1: resolved "https://registry.yarnpkg.com/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz#6336e8d71965cb3d35a1bbb7868445a7c05264bf" integrity sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg== -v8-to-istanbul@^8.1.0: - version "8.1.1" - resolved "https://registry.yarnpkg.com/v8-to-istanbul/-/v8-to-istanbul-8.1.1.tgz#77b752fd3975e31bbcef938f85e9bd1c7a8d60ed" - integrity sha512-FGtKtv3xIpR6BYhvgH8MI/y78oT7d8Au3ww4QIxymrCtZEh5b8gCw2siywE+puhEmuWKDtmfrvF5UlB298ut3w== +v8-to-istanbul@^9.0.1: + version "9.1.0" + resolved "https://registry.yarnpkg.com/v8-to-istanbul/-/v8-to-istanbul-9.1.0.tgz#1b83ed4e397f58c85c266a570fc2558b5feb9265" + integrity sha512-6z3GW9x8G1gd+JIIgQQQxXuiJtCXeAjp6RaPEPLv62mH3iPHPxV6W3robxtCzNErRo6ZwTmzWhsbNvjyEBKzKA== dependencies: + "@jridgewell/trace-mapping" "^0.3.12" "@types/istanbul-lib-coverage" "^2.0.1" convert-source-map "^1.6.0" - source-map "^0.7.3" validator@^13.7.0: version "13.9.0" @@ -5665,21 +5857,14 @@ vary@^1, vary@~1.1.2: resolved "https://registry.yarnpkg.com/vary/-/vary-1.1.2.tgz#2299f02c6ded30d4a5961b0b9f74524a18f634fc" integrity sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg== -w3c-hr-time@^1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/w3c-hr-time/-/w3c-hr-time-1.0.2.tgz#0a89cdf5cc15822df9c360543676963e0cc308cd" - integrity sha512-z8P5DvDNjKDoFIHK7q8r8lackT6l+jo/Ye3HOle7l9nICP9lf1Ci25fy9vHd0JOWewkIFzXIEig3TdKT7JQ5fQ== - dependencies: - browser-process-hrtime "^1.0.0" - -w3c-xmlserializer@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/w3c-xmlserializer/-/w3c-xmlserializer-2.0.0.tgz#3e7104a05b75146cc60f564380b7f683acf1020a" - integrity sha512-4tzD0mF8iSiMiNs30BiLO3EpfGLZUT2MSX/G+o7ZywDzliWQ3OPtTZ0PTC3B3ca1UAf4cJMHB+2Bf56EriJuRA== +w3c-xmlserializer@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/w3c-xmlserializer/-/w3c-xmlserializer-4.0.0.tgz#aebdc84920d806222936e3cdce408e32488a3073" + integrity sha512-d+BFHzbiCx6zGfz0HyQ6Rg69w9k19nviJspaj4yNscGjrHu94sVP+aRm75yEbCh+r2/yR+7q6hux9LVtbuTGBw== dependencies: - xml-name-validator "^3.0.0" + xml-name-validator "^4.0.0" -walker@^1.0.7: +walker@^1.0.8: version "1.0.8" resolved "https://registry.yarnpkg.com/walker/-/walker-1.0.8.tgz#bd498db477afe573dc04185f011d3ab8a8d7653f" integrity sha512-ts/8E8l5b7kY0vlWLewOkDXMmPdLcVV4GmOQLyxuSswIJsweeFZtAsMF7k1Nszz+TYBQrlYRmzOnr398y1JemQ== @@ -5706,15 +5891,10 @@ webidl-conversions@^3.0.0: resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-3.0.1.tgz#24534275e2a7bc6be7bc86611cc16ae0a5654871" integrity sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ== -webidl-conversions@^5.0.0: - version "5.0.0" - resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-5.0.0.tgz#ae59c8a00b121543a2acc65c0434f57b0fc11aff" - integrity sha512-VlZwKPCkYKxQgeSbH5EyngOmRp7Ww7I9rQLERETtf5ofd9pGeswWiOtogpEO850jziPRarreGxn5QIiTqpb2wA== - -webidl-conversions@^6.1.0: - version "6.1.0" - resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-6.1.0.tgz#9111b4d7ea80acd40f5270d666621afa78b69514" - integrity sha512-qBIvFLGiBpLjfwmYAaHPXsn+ho5xZnGvyGvsarywGNc8VyQJUMHJ8OBKGGrPER0okBeMDaan4mNBlgBROxuI8w== +webidl-conversions@^7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-7.0.0.tgz#256b4e1882be7debbf01d05f0aa2039778ea080a" + integrity sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g== webpack-node-externals@3.0.0: version "3.0.0" @@ -5756,17 +5936,25 @@ webpack@5.73.0: watchpack "^2.3.1" webpack-sources "^3.2.3" -whatwg-encoding@^1.0.5: - version "1.0.5" - resolved "https://registry.yarnpkg.com/whatwg-encoding/-/whatwg-encoding-1.0.5.tgz#5abacf777c32166a51d085d6b4f3e7d27113ddb0" - integrity sha512-b5lim54JOPN9HtzvK9HFXvBma/rnfFeqsic0hSpjtDbVxR3dJKLc+KB4V6GgiGOvl7CY/KNh8rxSo9DKQrnUEw== +whatwg-encoding@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/whatwg-encoding/-/whatwg-encoding-2.0.0.tgz#e7635f597fd87020858626805a2729fa7698ac53" + integrity sha512-p41ogyeMUrw3jWclHWTQg1k05DSVXPLcVxRTYsXUk+ZooOCZLcoYgPZ/HL/D/N+uQPOtcp1me1WhBEaX02mhWg== dependencies: - iconv-lite "0.4.24" + iconv-lite "0.6.3" -whatwg-mimetype@^2.3.0: - version "2.3.0" - resolved "https://registry.yarnpkg.com/whatwg-mimetype/-/whatwg-mimetype-2.3.0.tgz#3d4b1e0312d2079879f826aff18dbeeca5960fbf" - integrity sha512-M4yMwr6mAnQz76TbJm914+gPpB/nCwvZbJU28cUD6dR004SAxDLOOSUaB1JDRqLtaOV/vi0IC5lEAGFgrjGv/g== +whatwg-mimetype@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/whatwg-mimetype/-/whatwg-mimetype-3.0.0.tgz#5fa1a7623867ff1af6ca3dc72ad6b8a4208beba7" + integrity sha512-nt+N2dzIutVRxARx1nghPKGv1xHikU7HKdfafKkLNLindmPU/ch3U31NOCGGA/dmPcmb1VlofO0vnKAcsm0o/Q== + +whatwg-url@^11.0.0: + version "11.0.0" + resolved "https://registry.yarnpkg.com/whatwg-url/-/whatwg-url-11.0.0.tgz#0a849eebb5faf2119b901bb76fd795c2848d4018" + integrity sha512-RKT8HExMpoYx4igMiVMY83lN6UeITKJlBQ+vR/8ZJ8OCdSiN3RwCq+9gH0+Xzj0+5IrM6i4j/6LuvzbZIQgEcQ== + dependencies: + tr46 "^3.0.0" + webidl-conversions "^7.0.0" whatwg-url@^5.0.0: version "5.0.0" @@ -5776,15 +5964,6 @@ whatwg-url@^5.0.0: tr46 "~0.0.3" webidl-conversions "^3.0.0" -whatwg-url@^8.0.0, whatwg-url@^8.5.0: - version "8.7.0" - resolved "https://registry.yarnpkg.com/whatwg-url/-/whatwg-url-8.7.0.tgz#656a78e510ff8f3937bc0bcbe9f5c0ac35941b77" - integrity sha512-gAojqb/m9Q8a5IV96E3fHJM70AzCkgt4uXYX2O7EmuyOnLrViCQlsEBmF9UQIu3/aeAIp2U17rtbpZWNntQqdg== - dependencies: - lodash "^4.7.0" - tr46 "^2.1.0" - webidl-conversions "^6.1.0" - which@^2.0.1: version "2.0.2" resolved "https://registry.yarnpkg.com/which/-/which-2.0.2.tgz#7c6a8dd0a636a0327e10b59c9286eee93f3f51b1" @@ -5818,25 +5997,23 @@ wrappy@1: resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f" integrity sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ== -write-file-atomic@^3.0.0: - version "3.0.3" - resolved "https://registry.yarnpkg.com/write-file-atomic/-/write-file-atomic-3.0.3.tgz#56bd5c5a5c70481cd19c571bd39ab965a5de56e8" - integrity sha512-AvHcyZ5JnSfq3ioSyjrBkH9yW4m7Ayk8/9My/DD9onKeu/94fwrMocemO2QAJFAlnnDN+ZDS+ZjAR5ua1/PV/Q== +write-file-atomic@^4.0.2: + version "4.0.2" + resolved "https://registry.yarnpkg.com/write-file-atomic/-/write-file-atomic-4.0.2.tgz#a9df01ae5b77858a027fd2e80768ee433555fcfd" + integrity sha512-7KxauUdBmSdWnmpaGFg+ppNjKF8uNLry8LyzjauQDOVONfFLNKrKvQOxZ/VuTIcS/gge/YNahf5RIIQWTSarlg== dependencies: imurmurhash "^0.1.4" - is-typedarray "^1.0.0" - signal-exit "^3.0.2" - typedarray-to-buffer "^3.1.5" + signal-exit "^3.0.7" -ws@^7.4.6: - version "7.5.9" - resolved "https://registry.yarnpkg.com/ws/-/ws-7.5.9.tgz#54fa7db29f4c7cec68b1ddd3a89de099942bb591" - integrity sha512-F+P9Jil7UiSKSkppIiD94dN07AwvFixvLIj1Og1Rl9GGMuNipJnV9JzjD6XuqmAeiswGvUmNLjr5cFuXwNS77Q== +ws@^8.11.0: + version "8.13.0" + resolved "https://registry.yarnpkg.com/ws/-/ws-8.13.0.tgz#9a9fb92f93cf41512a0735c8f4dd09b8a1211cd0" + integrity sha512-x9vcZYTrFPC7aSIbj7sRCYo7L/Xb8Iy+pW0ng0wt2vCJv7M9HOMy0UoN3rr+IFC7hb7vXoqS+P9ktyLLLhO+LA== -xml-name-validator@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/xml-name-validator/-/xml-name-validator-3.0.0.tgz#6ae73e06de4d8c6e47f9fb181f78d648ad457c6a" - integrity sha512-A5CUptxDsvxKJEU3yO6DuWBSJz/qizqzJKOMIfUJHETbBw/sFaDxgd6fxm1ewUaM0jZ444Fc5vC5ROYurg/4Pw== +xml-name-validator@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/xml-name-validator/-/xml-name-validator-4.0.0.tgz#79a006e2e63149a8600f15430f0a4725d1524835" + integrity sha512-ICP2e+jsHvAj2E2lIHxa5tjXRlKDJo4IdvPvCXbXQGdzSfmSpNVyIKMvoZHjDY9DP0zV17iI85o90vRFXNccRw== xmlchars@^2.2.0: version "2.2.0" @@ -5873,30 +6050,12 @@ yaml@^1.10.0: resolved "https://registry.yarnpkg.com/yaml/-/yaml-1.10.2.tgz#2301c5ffbf12b467de8da2333a459e29e7920e4b" integrity sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg== -yargs-parser@20.x, yargs-parser@^20.2.2: - version "20.2.9" - resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-20.2.9.tgz#2eb7dc3b0289718fc295f362753845c41a0c94ee" - integrity sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w== - -yargs-parser@^21.1.1: +yargs-parser@^21.0.1, yargs-parser@^21.1.1: version "21.1.1" resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-21.1.1.tgz#9096bceebf990d21bb31fa9516e0ede294a77d35" integrity sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw== -yargs@^16.2.0: - version "16.2.0" - resolved "https://registry.yarnpkg.com/yargs/-/yargs-16.2.0.tgz#1c82bf0f6b6a66eafce7ef30e376f49a12477f66" - integrity sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw== - dependencies: - cliui "^7.0.2" - escalade "^3.1.1" - get-caller-file "^2.0.5" - require-directory "^2.1.1" - string-width "^4.2.0" - y18n "^5.0.5" - yargs-parser "^20.2.2" - -yargs@^17.7.2: +yargs@^17.3.1, yargs@^17.7.2: version "17.7.2" resolved "https://registry.yarnpkg.com/yargs/-/yargs-17.7.2.tgz#991df39aca675a192b816e1e0363f9d75d2aa269" integrity sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w== From 626f7d142b1e8b1c862d03f71cc54818efa07fa8 Mon Sep 17 00:00:00 2001 From: Yazeed Loonat Date: Mon, 7 Aug 2023 17:18:48 -0700 Subject: [PATCH 17/57] feat: prisma asset endpoint (#3560) --- backend_new/.env.template | 3 +- backend_new/package.json | 3 +- .../src/controllers/asset.controller.ts | 42 +++++++++++++++++ ...create-presign-upload-meta-response.dto.ts | 8 ++++ .../create-presigned-upload-meta.dto.ts | 11 +++++ backend_new/src/modules/app.module.ts | 3 ++ backend_new/src/modules/asset.module.ts | 11 +++++ backend_new/src/services/asset.service.ts | 37 +++++++++++++++ backend_new/test/integration/app.e2e-spec.ts | 5 ++ .../test/integration/asset.e2e-spec.ts | 43 +++++++++++++++++ .../test/unit/services/asset.service.spec.ts | 47 +++++++++++++++++++ backend_new/yarn.lock | 25 ++++++++++ 12 files changed, 236 insertions(+), 2 deletions(-) create mode 100644 backend_new/src/controllers/asset.controller.ts create mode 100644 backend_new/src/dtos/assets/create-presign-upload-meta-response.dto.ts create mode 100644 backend_new/src/dtos/assets/create-presigned-upload-meta.dto.ts create mode 100644 backend_new/src/modules/asset.module.ts create mode 100644 backend_new/src/services/asset.service.ts create mode 100644 backend_new/test/integration/asset.e2e-spec.ts create mode 100644 backend_new/test/unit/services/asset.service.spec.ts diff --git a/backend_new/.env.template b/backend_new/.env.template index 8ad3216d57..8a651d57cb 100644 --- a/backend_new/.env.template +++ b/backend_new/.env.template @@ -2,4 +2,5 @@ DATABASE_URL="postgres://@localhost:5432/bloom_prisma" PORT=3101 GOOGLE_API_EMAIL= GOOGLE_API_ID= -GOOGLE_API_KEY= \ No newline at end of file +GOOGLE_API_KEY= +CLOUDINARY_SECRET= diff --git a/backend_new/package.json b/backend_new/package.json index 39755f299c..47a7844be0 100644 --- a/backend_new/package.json +++ b/backend_new/package.json @@ -24,7 +24,7 @@ "db:seed:staging": "npx prisma db seed -- --environment staging", "db:seed:development": "npx prisma db seed -- --environment development --jurisdictionName Bloomington", "generate:client": "ts-node scripts/generate-axios-client.ts && prettier -w types/src/backend-swagger.ts", - "test:e2e": "yarn db:resetup && yarn db:migration:run && jest --config ./test/jest-e2e.config.js", + "test:e2e": "yarn db:resetup && yarn db:migration:run && jest --config ./test/jest-e2e.config.js --runInBand", "db:setup": "yarn db:resetup && yarn db:migration:run && yarn db:seed:development", "db:setup:staging": "yarn db:resetup && yarn db:migration:run && yarn db:seed:staging" }, @@ -37,6 +37,7 @@ "@prisma/client": "^5.0.0", "class-validator": "^0.14.0", "class-transformer": "^0.5.1", + "cloudinary": "^1.37.3", "lodash": "^4.17.21", "prisma": "^5.0.0", "qs": "^6.11.2", diff --git a/backend_new/src/controllers/asset.controller.ts b/backend_new/src/controllers/asset.controller.ts new file mode 100644 index 0000000000..dba8c40ff1 --- /dev/null +++ b/backend_new/src/controllers/asset.controller.ts @@ -0,0 +1,42 @@ +import { + Body, + Controller, + Post, + UsePipes, + ValidationPipe, +} from '@nestjs/common'; +import { + ApiExtraModels, + ApiOkResponse, + ApiOperation, + ApiTags, +} from '@nestjs/swagger'; +import { CreatePresignedUploadMetadataResponse } from '../dtos/assets/create-presign-upload-meta-response.dto'; +import { CreatePresignedUploadMetadata } from '../dtos/assets/create-presigned-upload-meta.dto'; +import { AssetService } from '../services/asset.service'; +import { defaultValidationPipeOptions } from '../utilities/default-validation-pipe-options'; + +@Controller('assets') +@ApiTags('assets') +@UsePipes(new ValidationPipe(defaultValidationPipeOptions)) +@ApiExtraModels( + CreatePresignedUploadMetadata, + CreatePresignedUploadMetadataResponse, +) +export class AssetController { + constructor(private readonly assetService: AssetService) {} + + @Post('/presigned-upload-metadata') + @ApiOperation({ + summary: 'Create presigned upload metadata', + operationId: 'createPresignedUploadMetadata', + }) + @ApiOkResponse({ type: CreatePresignedUploadMetadataResponse }) + async create( + @Body() createPresignedUploadMetadata: CreatePresignedUploadMetadata, + ): Promise { + return await this.assetService.createPresignedUploadMetadata( + createPresignedUploadMetadata, + ); + } +} diff --git a/backend_new/src/dtos/assets/create-presign-upload-meta-response.dto.ts b/backend_new/src/dtos/assets/create-presign-upload-meta-response.dto.ts new file mode 100644 index 0000000000..81de1abd4f --- /dev/null +++ b/backend_new/src/dtos/assets/create-presign-upload-meta-response.dto.ts @@ -0,0 +1,8 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { Expose } from 'class-transformer'; + +export class CreatePresignedUploadMetadataResponse { + @Expose() + @ApiProperty({ required: true }) + signature: string; +} diff --git a/backend_new/src/dtos/assets/create-presigned-upload-meta.dto.ts b/backend_new/src/dtos/assets/create-presigned-upload-meta.dto.ts new file mode 100644 index 0000000000..a5a40e711b --- /dev/null +++ b/backend_new/src/dtos/assets/create-presigned-upload-meta.dto.ts @@ -0,0 +1,11 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { Expose } from 'class-transformer'; +import { IsDefined } from 'class-validator'; +import { ValidationsGroupsEnum } from '../../enums/shared/validation-groups-enum'; + +export class CreatePresignedUploadMetadata { + @Expose() + @IsDefined({ groups: [ValidationsGroupsEnum.default] }) + @ApiProperty({ required: true }) + parametersToSign: Record; +} diff --git a/backend_new/src/modules/app.module.ts b/backend_new/src/modules/app.module.ts index 7ddb0d2094..0f761498a1 100644 --- a/backend_new/src/modules/app.module.ts +++ b/backend_new/src/modules/app.module.ts @@ -11,6 +11,7 @@ import { UnitRentTypeModule } from './unit-rent-type.module'; import { JurisdictionModule } from './jurisdiction.module'; import { MultiselectQuestionModule } from './multiselect-question.module'; import { ApplicationModule } from './application.module'; +import { AssetModule } from './asset.module'; @Module({ imports: [ @@ -23,6 +24,7 @@ import { ApplicationModule } from './application.module'; JurisdictionModule, MultiselectQuestionModule, ApplicationModule, + AssetModule, ], controllers: [AppController], providers: [AppService, PrismaService], @@ -36,6 +38,7 @@ import { ApplicationModule } from './application.module'; JurisdictionModule, MultiselectQuestionModule, ApplicationModule, + AssetModule, ], }) export class AppModule {} diff --git a/backend_new/src/modules/asset.module.ts b/backend_new/src/modules/asset.module.ts new file mode 100644 index 0000000000..57c89b6f6d --- /dev/null +++ b/backend_new/src/modules/asset.module.ts @@ -0,0 +1,11 @@ +import { Module } from '@nestjs/common'; +import { AssetController } from '../controllers/asset.controller'; +import { AssetService } from '../services/asset.service'; + +@Module({ + imports: [], + controllers: [AssetController], + providers: [AssetService], + exports: [AssetService], +}) +export class AssetModule {} diff --git a/backend_new/src/services/asset.service.ts b/backend_new/src/services/asset.service.ts new file mode 100644 index 0000000000..08125c52d1 --- /dev/null +++ b/backend_new/src/services/asset.service.ts @@ -0,0 +1,37 @@ +import { Injectable } from '@nestjs/common'; +import { v2 as cloudinary } from 'cloudinary'; +import { CreatePresignedUploadMetadata } from '../dtos/assets/create-presigned-upload-meta.dto'; +import { CreatePresignedUploadMetadataResponse } from '../dtos/assets/create-presign-upload-meta-response.dto'; + +/* + this is the service for assets + it handles all the backend's business logic for signing meta data for asset upload +*/ + +@Injectable() +export class AssetService { + /* + this will create a signed signature for upload to cloudinary + */ + async createPresignedUploadMetadata( + createPresignedUploadMetadata: CreatePresignedUploadMetadata, + ): Promise { + // Based on https://cloudinary.com/documentation/upload_images#signed_upload_video_tutorial + + const parametersToSignWithTimestamp = { + ...createPresignedUploadMetadata.parametersToSign, + timestamp: parseInt( + createPresignedUploadMetadata.parametersToSign.timestamp, + ), + }; + + const signature = await cloudinary.utils.api_sign_request( + parametersToSignWithTimestamp, + process.env.CLOUDINARY_SECRET, + ); + + return { + signature, + }; + } +} diff --git a/backend_new/test/integration/app.e2e-spec.ts b/backend_new/test/integration/app.e2e-spec.ts index a22f5ec52b..4f50567336 100644 --- a/backend_new/test/integration/app.e2e-spec.ts +++ b/backend_new/test/integration/app.e2e-spec.ts @@ -2,19 +2,24 @@ import { Test, TestingModule } from '@nestjs/testing'; import { INestApplication } from '@nestjs/common'; import * as request from 'supertest'; import { AppModule } from '../../src/modules/app.module'; +import { PrismaService } from '../../src/services/prisma.service'; describe('App Controller Tests', () => { let app: INestApplication; + let prisma: PrismaService; + beforeAll(async () => { const moduleFixture: TestingModule = await Test.createTestingModule({ imports: [AppModule], }).compile(); app = moduleFixture.createNestApplication(); + prisma = moduleFixture.get(PrismaService); await app.init(); }); afterAll(async () => { + await prisma.$disconnect(); await app.close(); }); diff --git a/backend_new/test/integration/asset.e2e-spec.ts b/backend_new/test/integration/asset.e2e-spec.ts new file mode 100644 index 0000000000..ca9c6c1862 --- /dev/null +++ b/backend_new/test/integration/asset.e2e-spec.ts @@ -0,0 +1,43 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { INestApplication } from '@nestjs/common'; +import * as request from 'supertest'; +import { AppModule } from '../../src//modules/app.module'; +import { randomUUID } from 'crypto'; +import { PrismaService } from '../../src/services/prisma.service'; + +describe('Asset Controller Tests', () => { + let app: INestApplication; + let prisma: PrismaService; + + beforeAll(async () => { + const moduleFixture: TestingModule = await Test.createTestingModule({ + imports: [AppModule], + }).compile(); + + app = moduleFixture.createNestApplication(); + prisma = moduleFixture.get(PrismaService); + await app.init(); + }); + + afterAll(async () => { + await prisma.$disconnect(); + await app.close(); + }); + + it('should create a presigned url for upload', async () => { + const publicId = randomUUID(); + const eager = 'eager'; + const res = await request(app.getHttpServer()) + .post('/assets/presigned-upload-metadata/') + .send({ parametersToSign: { publicId, eager } }) + .expect(201); + + const createPresignedUploadMetadataResponseDto = JSON.parse(res.text); + expect(createPresignedUploadMetadataResponseDto).toHaveProperty( + 'signature', + ); + // we're uploading data to cloudinary so what we get back will depend on cloudinary's response. + // at the minimum we shouldn't get null + expect(createPresignedUploadMetadataResponseDto.signature).not.toBeNull(); + }); +}); diff --git a/backend_new/test/unit/services/asset.service.spec.ts b/backend_new/test/unit/services/asset.service.spec.ts new file mode 100644 index 0000000000..ee7df7a243 --- /dev/null +++ b/backend_new/test/unit/services/asset.service.spec.ts @@ -0,0 +1,47 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { v2 as cloudinary } from 'cloudinary'; +import { AssetService } from '../../../src/services/asset.service'; +import { randomUUID } from 'crypto'; + +describe('Testing asset service', () => { + let service: AssetService; + + beforeAll(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [AssetService], + }).compile(); + + service = module.get(AssetService); + }); + + it('should call the createPresignedUploadMetadata() properly', async () => { + const mockedValue = 'fake'; + cloudinary.utils.api_sign_request = jest + .fn() + .mockResolvedValue(mockedValue); + + const publicId = randomUUID(); + const eager = 'eager'; + const timestamp = '15'; + const params = { + parametersToSign: { + publicId, + eager, + timestamp, + }, + }; + + expect(await service.createPresignedUploadMetadata(params)).toEqual({ + signature: 'fake', + }); + + expect(cloudinary.utils.api_sign_request).toHaveBeenCalledWith( + { + eager: 'eager', + publicId: publicId, + timestamp: 15, + }, + process.env.CLOUDINARY_SECRET, + ); + }); +}); diff --git a/backend_new/yarn.lock b/backend_new/yarn.lock index ee3b58f6a5..bc3c840a08 100644 --- a/backend_new/yarn.lock +++ b/backend_new/yarn.lock @@ -2273,6 +2273,21 @@ clone@^1.0.2: resolved "https://registry.yarnpkg.com/clone/-/clone-1.0.4.tgz#da309cc263df15994c688ca902179ca3c7cd7c7e" integrity sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg== +cloudinary-core@^2.13.0: + version "2.13.0" + resolved "https://registry.yarnpkg.com/cloudinary-core/-/cloudinary-core-2.13.0.tgz#b59f90871b6c708c3d0735b9be47ac08181c57fb" + integrity sha512-Nt0Q5I2FtenmJghtC4YZ3MZZbGg1wLm84SsxcuVwZ83OyJqG9CNIGp86CiI6iDv3QobaqBUpOT7vg+HqY5HxEA== + +cloudinary@^1.37.3: + version "1.37.3" + resolved "https://registry.yarnpkg.com/cloudinary/-/cloudinary-1.37.3.tgz#859ac875c022e84315e6ea092aa3f05e031ceabb" + integrity sha512-XrGb60ZeQhYp9QQjj5DP3cYsAc27OV1B7pezvVxyqgHB5WMeMsofzeIy6+k0o/fCCv744Nf7xsYiTlUi3V0V/Q== + dependencies: + cloudinary-core "^2.13.0" + core-js "^3.30.1" + lodash "^4.17.21" + q "^1.5.1" + co@^4.6.0: version "4.6.0" resolved "https://registry.yarnpkg.com/co/-/co-4.6.0.tgz#6ea6bdf3d853ae54ccb8e47bfa0bf3f9031fb184" @@ -2386,6 +2401,11 @@ cookiejar@^2.1.4: resolved "https://registry.yarnpkg.com/cookiejar/-/cookiejar-2.1.4.tgz#ee669c1fea2cf42dc31585469d193fef0d65771b" integrity sha512-LDx6oHrK+PhzLKJU9j5S7/Y3jM/mUHvD/DeI1WQmJn652iPC5Y4TBzC9l+5OMOXlyTTA+SmVUPm0HQUwpD5Jqw== +core-js@^3.30.1: + version "3.31.1" + resolved "https://registry.yarnpkg.com/core-js/-/core-js-3.31.1.tgz#f2b0eea9be9da0def2c5fece71064a7e5d687653" + integrity sha512-2sKLtfq1eFST7l7v62zaqXacPc7uG8ZAya8ogijLhTtaKNcpzpB4TMoTw2Si+8GYKRwFPMMtUT0263QFWFfqyQ== + core-util-is@~1.0.0: version "1.0.3" resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.3.tgz#a6042d3634c2b27e9328f837b965fac83808db85" @@ -4938,6 +4958,11 @@ pure-rand@^6.0.0: resolved "https://registry.yarnpkg.com/pure-rand/-/pure-rand-6.0.2.tgz#a9c2ddcae9b68d736a8163036f088a2781c8b306" integrity sha512-6Yg0ekpKICSjPswYOuC5sku/TSWaRYlA0qsXqJgM/d/4pLPHPuTxK7Nbf7jFKzAeedUhR8C7K9Uv63FBsSo8xQ== +q@^1.5.1: + version "1.5.1" + resolved "https://registry.yarnpkg.com/q/-/q-1.5.1.tgz#7e32f75b41381291d04611f1bf14109ac00651d7" + integrity sha512-kV/CThkXo6xyFEZUugw/+pIOywXcDbFYgSct5cT3gqlbkBE1SJdwy6UQoZvodiWF/ckQLZyDE/Bu1M6gVu5lVw== + qs@6.10.3: version "6.10.3" resolved "https://registry.yarnpkg.com/qs/-/qs-6.10.3.tgz#d6cde1b2ffca87b5aa57889816c5f81535e22e8e" From 331086b661ce9d22f7f83105fc4d7add8c752b5e Mon Sep 17 00:00:00 2001 From: Yazeed Loonat Date: Tue, 8 Aug 2023 16:15:55 -0700 Subject: [PATCH 18/57] feat: Prisma user get endpoints (#3546) * feat: prisma changeover for users --- backend_new/package.json | 2 +- backend_new/prisma/schema.prisma | 1 + backend_new/prisma/seed-dev.ts | 2 +- .../prisma/seed-helpers/user-factory.ts | 23 +- backend_new/prisma/seed-staging.ts | 2 +- .../src/controllers/user.controller.ts | 66 +++++ .../src/dtos/users/paginated-user.dto.ts | 4 + .../src/dtos/users/user-filter-params.dto.ts | 15 ++ .../src/dtos/users/user-query-param.dto.ts | 40 +++ backend_new/src/dtos/users/user-role.dto.ts | 22 ++ backend_new/src/dtos/users/user.dto.ts | 138 ++++++++++ .../enums/user_accounts/filter-key-enum.ts | 4 - backend_new/src/modules/ami-chart.module.ts | 8 +- backend_new/src/modules/app.module.ts | 9 +- backend_new/src/modules/application.module.ts | 8 +- .../src/modules/jurisdiction.module.ts | 8 +- backend_new/src/modules/listing.module.ts | 13 +- .../modules/multiselect-question.module.ts | 8 +- backend_new/src/modules/prisma.module.ts | 9 + .../modules/reserved-community-type.module.ts | 8 +- ...unit-accessibility-priority-type.module.ts | 8 +- .../src/modules/unit-rent-type.module.ts | 8 +- backend_new/src/modules/unit-type.module.ts | 8 +- backend_new/src/modules/user.module.ts | 12 + backend_new/src/services/listing.service.ts | 21 +- backend_new/src/services/user.service.ts | 235 ++++++++++++++++++ backend_new/src/utilities/build-filter.ts | 12 +- .../src/utilities/pagination-helpers.ts | 34 +++ backend_new/test/integration/user.e2e-spec.ts | 104 ++++++++ .../test/unit/services/user.service.spec.ts | 211 ++++++++++++++++ .../unit/utilities/pagination-helpers.spec.ts | 72 ++++++ backend_new/types/src/backend-swagger.ts | 181 ++++++++++++++ 32 files changed, 1212 insertions(+), 84 deletions(-) create mode 100644 backend_new/src/controllers/user.controller.ts create mode 100644 backend_new/src/dtos/users/paginated-user.dto.ts create mode 100644 backend_new/src/dtos/users/user-filter-params.dto.ts create mode 100644 backend_new/src/dtos/users/user-query-param.dto.ts create mode 100644 backend_new/src/dtos/users/user-role.dto.ts create mode 100644 backend_new/src/dtos/users/user.dto.ts delete mode 100644 backend_new/src/enums/user_accounts/filter-key-enum.ts create mode 100644 backend_new/src/modules/prisma.module.ts create mode 100644 backend_new/src/modules/user.module.ts create mode 100644 backend_new/src/services/user.service.ts create mode 100644 backend_new/test/integration/user.e2e-spec.ts create mode 100644 backend_new/test/unit/services/user.service.spec.ts diff --git a/backend_new/package.json b/backend_new/package.json index 47a7844be0..975cc9ac9c 100644 --- a/backend_new/package.json +++ b/backend_new/package.json @@ -24,7 +24,7 @@ "db:seed:staging": "npx prisma db seed -- --environment staging", "db:seed:development": "npx prisma db seed -- --environment development --jurisdictionName Bloomington", "generate:client": "ts-node scripts/generate-axios-client.ts && prettier -w types/src/backend-swagger.ts", - "test:e2e": "yarn db:resetup && yarn db:migration:run && jest --config ./test/jest-e2e.config.js --runInBand", + "test:e2e": "yarn db:resetup && yarn db:migration:run && jest --config ./test/jest-e2e.config.js", "db:setup": "yarn db:resetup && yarn db:migration:run && yarn db:seed:development", "db:setup:staging": "yarn db:resetup && yarn db:migration:run && yarn db:seed:staging" }, diff --git a/backend_new/prisma/schema.prisma b/backend_new/prisma/schema.prisma index 84c21757dd..8d278e60cc 100644 --- a/backend_new/prisma/schema.prisma +++ b/backend_new/prisma/schema.prisma @@ -6,6 +6,7 @@ generator client { datasource db { provider = "postgresql" url = env("DATABASE_URL") + // directUrl = env("DIRECT_URL") extensions = [uuidOssp(map: "uuid-ossp")] } diff --git a/backend_new/prisma/seed-dev.ts b/backend_new/prisma/seed-dev.ts index f0789cbd2b..adcbf756ed 100644 --- a/backend_new/prisma/seed-dev.ts +++ b/backend_new/prisma/seed-dev.ts @@ -39,7 +39,7 @@ const createMultiselect = async ( export const devSeeding = async (prismaClient: PrismaClient) => { await prismaClient.userAccounts.create({ - data: userFactory({ isAdmin: true }), + data: userFactory({ roles: { isAdmin: true }, email: 'admin@example.com' }), }); const jurisdiction = await prismaClient.jurisdictions.create({ data: jurisdictionFactory(), diff --git a/backend_new/prisma/seed-helpers/user-factory.ts b/backend_new/prisma/seed-helpers/user-factory.ts index f398c27f00..abc69da76f 100644 --- a/backend_new/prisma/seed-helpers/user-factory.ts +++ b/backend_new/prisma/seed-helpers/user-factory.ts @@ -1,19 +1,24 @@ import { Prisma } from '@prisma/client'; +import { randomAdjective, randomNoun } from './word-generator'; -export const userFactory = ( - roles?: Prisma.UserRolesUncheckedCreateWithoutUserAccountsInput, -): Prisma.UserAccountsCreateInput => ({ - email: 'admin@example.com', - firstName: 'First', - lastName: 'Last', +export const userFactory = (optionalParams?: { + roles?: Prisma.UserRolesUncheckedCreateWithoutUserAccountsInput; + firstName?: string; + lastName?: string; + email?: string; +}): Prisma.UserAccountsCreateInput => ({ + email: optionalParams?.email || `${randomNoun()}@${randomAdjective()}.com`, + firstName: optionalParams?.firstName || 'First', + lastName: optionalParams?.lastName || 'Last', // TODO: update with passwordService hashing when that is completed passwordHash: 'a921d45de2db97818a124126706a1bf52310d231be04e1764d4eedffaccadcea3af70fa1d806b8527b2ebb98a2dd48ab3f07238bb9d39d4bcd2de4c207b67d4e#c870c8c0dbc08b27f4fc1dab32266cfde4aef8f2c606dab1162f9e71763f1fd11f28b2b81e05e7aeefd08b745d636624b623f505d47a54213fb9822c366bbbfe', userRoles: { create: { - isAdmin: roles?.isAdmin || false, - isJurisdictionalAdmin: roles?.isJurisdictionalAdmin || false, - isPartner: roles?.isAdmin || false, + isAdmin: optionalParams?.roles?.isAdmin || false, + isJurisdictionalAdmin: + optionalParams?.roles?.isJurisdictionalAdmin || false, + isPartner: optionalParams?.roles?.isAdmin || false, }, }, }); diff --git a/backend_new/prisma/seed-staging.ts b/backend_new/prisma/seed-staging.ts index 92db84fbf7..6ff138caca 100644 --- a/backend_new/prisma/seed-staging.ts +++ b/backend_new/prisma/seed-staging.ts @@ -28,7 +28,7 @@ export const stagingSeed = async ( ) => { // create admin user await prismaClient.userAccounts.create({ - data: userFactory({ isAdmin: true }), + data: userFactory({ roles: { isAdmin: true }, email: 'admin@example.com' }), }); // create single jurisdiction const jurisdiction = await prismaClient.jurisdictions.create({ diff --git a/backend_new/src/controllers/user.controller.ts b/backend_new/src/controllers/user.controller.ts new file mode 100644 index 0000000000..0970b09d76 --- /dev/null +++ b/backend_new/src/controllers/user.controller.ts @@ -0,0 +1,66 @@ +import { + ClassSerializerInterceptor, + Controller, + Get, + Param, + ParseUUIDPipe, + Query, + Request, + UseInterceptors, + UsePipes, + ValidationPipe, +} from '@nestjs/common'; +import { + ApiExtraModels, + ApiOkResponse, + ApiOperation, + ApiTags, +} from '@nestjs/swagger'; +import { UserService } from '../services/user.service'; +import { User } from '../dtos/users/user.dto'; +import { defaultValidationPipeOptions } from '../utilities/default-validation-pipe-options'; +import { IdDTO } from '../dtos/shared/id.dto'; +import { mapTo } from '../utilities/mapTo'; +import { PaginatedUserDto } from '../dtos/users/paginated-user.dto'; +import { UserQueryParams } from '../dtos/users/user-query-param.dto'; +import { Request as ExpressRequest } from 'express'; + +@Controller('user') +@ApiTags('user') +@UsePipes(new ValidationPipe(defaultValidationPipeOptions)) +@ApiExtraModels(IdDTO) +export class UserController { + constructor(private readonly userService: UserService) {} + + @Get() + profile(@Request() req: ExpressRequest): User { + return mapTo(User, req.user); + } + + @Get('/list') + @UsePipes(new ValidationPipe(defaultValidationPipeOptions)) + @UseInterceptors(ClassSerializerInterceptor) + @ApiOkResponse({ type: PaginatedUserDto }) + @ApiOperation({ + summary: 'Get a paginated set of users', + operationId: 'list', + }) + async list( + @Request() req: ExpressRequest, + @Query() queryParams: UserQueryParams, + ): Promise { + return await this.userService.list(queryParams, mapTo(User, req.user)); + } + + @Get(`:id`) + @ApiOperation({ + summary: 'Get user by id', + operationId: 'retrieve', + }) + @ApiOkResponse({ type: User }) + async retrieve( + @Param('id', new ParseUUIDPipe({ version: '4' })) userId: string, + ): Promise { + return this.userService.findOne(userId); + } +} diff --git a/backend_new/src/dtos/users/paginated-user.dto.ts b/backend_new/src/dtos/users/paginated-user.dto.ts new file mode 100644 index 0000000000..72130bc13d --- /dev/null +++ b/backend_new/src/dtos/users/paginated-user.dto.ts @@ -0,0 +1,4 @@ +import { PaginationFactory } from '../shared/pagination.dto'; +import { User } from './user.dto'; + +export class PaginatedUserDto extends PaginationFactory(User) {} diff --git a/backend_new/src/dtos/users/user-filter-params.dto.ts b/backend_new/src/dtos/users/user-filter-params.dto.ts new file mode 100644 index 0000000000..8144cb82ce --- /dev/null +++ b/backend_new/src/dtos/users/user-filter-params.dto.ts @@ -0,0 +1,15 @@ +import { Expose } from 'class-transformer'; +import { ApiProperty } from '@nestjs/swagger'; +import { IsString } from 'class-validator'; +import { ValidationsGroupsEnum } from '../../enums/shared/validation-groups-enum'; + +export class UserFilterParams { + @Expose() + @ApiProperty({ + type: Boolean, + example: true, + required: false, + }) + @IsString({ groups: [ValidationsGroupsEnum.default] }) + isPortalUser?: boolean; +} diff --git a/backend_new/src/dtos/users/user-query-param.dto.ts b/backend_new/src/dtos/users/user-query-param.dto.ts new file mode 100644 index 0000000000..119d7fca2b --- /dev/null +++ b/backend_new/src/dtos/users/user-query-param.dto.ts @@ -0,0 +1,40 @@ +import { PaginationAllowsAllQueryParams } from '../shared/pagination.dto'; +import { Expose, Type } from 'class-transformer'; +import { ApiProperty } from '@nestjs/swagger'; +import { UserFilterParams } from './user-filter-params.dto'; +import { + ArrayMaxSize, + IsArray, + IsString, + MinLength, + ValidateNested, +} from 'class-validator'; +import { ValidationsGroupsEnum } from '../../enums/shared/validation-groups-enum'; + +export class UserQueryParams extends PaginationAllowsAllQueryParams { + @Expose() + @ApiProperty({ + name: 'filter', + required: false, + type: [UserFilterParams], + example: { isPartner: true }, + }) + @IsArray({ groups: [ValidationsGroupsEnum.default] }) + @ArrayMaxSize(16, { groups: [ValidationsGroupsEnum.default] }) + @Type(() => UserFilterParams) + @ValidateNested({ groups: [ValidationsGroupsEnum.default], each: true }) + filter?: UserFilterParams[]; + + @Expose() + @ApiProperty({ + type: String, + example: 'search', + required: false, + }) + @IsString({ groups: [ValidationsGroupsEnum.default] }) + @MinLength(3, { + message: 'Search must be at least 3 characters', + groups: [ValidationsGroupsEnum.default], + }) + search?: string; +} diff --git a/backend_new/src/dtos/users/user-role.dto.ts b/backend_new/src/dtos/users/user-role.dto.ts new file mode 100644 index 0000000000..da9f21f7c8 --- /dev/null +++ b/backend_new/src/dtos/users/user-role.dto.ts @@ -0,0 +1,22 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { Expose } from 'class-transformer'; +import { IsBoolean } from 'class-validator'; +import { ValidationsGroupsEnum } from '../../enums/shared/validation-groups-enum'; +import { AbstractDTO } from '../shared/abstract.dto'; + +export class UserRole extends AbstractDTO { + @Expose() + @IsBoolean({ groups: [ValidationsGroupsEnum.default] }) + @ApiProperty({ required: false }) + isAdmin?: boolean; + + @Expose() + @IsBoolean({ groups: [ValidationsGroupsEnum.default] }) + @ApiProperty({ required: false }) + isJurisdictionalAdmin?: boolean; + + @Expose() + @IsBoolean({ groups: [ValidationsGroupsEnum.default] }) + @ApiProperty({ required: false }) + isPartner?: boolean; +} diff --git a/backend_new/src/dtos/users/user.dto.ts b/backend_new/src/dtos/users/user.dto.ts new file mode 100644 index 0000000000..bfe29e4ab8 --- /dev/null +++ b/backend_new/src/dtos/users/user.dto.ts @@ -0,0 +1,138 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { Expose, Type } from 'class-transformer'; +import { + IsBoolean, + IsDate, + IsEmail, + IsEnum, + IsNumber, + IsPhoneNumber, + IsString, + MaxLength, +} from 'class-validator'; +import { EnforceLowerCase } from '../../decorators/enforce-lower-case.decorator'; +import { ValidationsGroupsEnum } from '../../enums/shared/validation-groups-enum'; +import { AbstractDTO } from '../shared/abstract.dto'; +import { LanguagesEnum } from '@prisma/client'; +import { IdDTO } from '../shared/id.dto'; +import { UserRole } from './user-role.dto'; + +export class User extends AbstractDTO { + @Expose() + @Type(() => Date) + @IsDate({ groups: [ValidationsGroupsEnum.default] }) + @ApiProperty() + passwordUpdatedAt: Date; + + @Expose() + @IsNumber({}, { groups: [ValidationsGroupsEnum.default] }) + @ApiProperty() + passwordValidForDays: number; + + @Expose() + @IsDate({ groups: [ValidationsGroupsEnum.default] }) + @Type(() => Date) + @ApiProperty({ required: false }) + confirmedAt?: Date; + + @Expose() + @IsEmail({}, { groups: [ValidationsGroupsEnum.default] }) + @EnforceLowerCase() + @ApiProperty() + email: string; + + @Expose() + @IsString({ groups: [ValidationsGroupsEnum.default] }) + @MaxLength(64, { groups: [ValidationsGroupsEnum.default] }) + firstName: string; + + @Expose() + @ApiProperty({ required: false }) + @IsString({ groups: [ValidationsGroupsEnum.default] }) + @MaxLength(64, { groups: [ValidationsGroupsEnum.default] }) + middleName?: string; + + @Expose() + @IsString({ groups: [ValidationsGroupsEnum.default] }) + @MaxLength(64, { groups: [ValidationsGroupsEnum.default] }) + @ApiProperty() + lastName: string; + + @Expose() + @ApiProperty({ required: false }) + @IsDate({ groups: [ValidationsGroupsEnum.default] }) + @Type(() => Date) + dob?: Date; + + @Expose() + @ApiProperty({ required: false }) + @IsPhoneNumber('US', { groups: [ValidationsGroupsEnum.default] }) + phoneNumber?: string; + + @Expose() + @Type(() => IdDTO) + @ApiProperty({ type: IdDTO, isArray: true }) + listings: IdDTO[]; + + @Expose() + @Type(() => UserRole) + @ApiProperty({ type: UserRole, required: false }) + userRoles?: UserRole; + + @Expose() + @IsEnum(LanguagesEnum, { groups: [ValidationsGroupsEnum.default] }) + @ApiProperty({ + enum: LanguagesEnum, + enumName: 'LanguagesEnum', + required: false, + }) + language?: LanguagesEnum; + + @Expose() + @Type(() => IdDTO) + @ApiProperty({ type: IdDTO, isArray: true }) + jurisdictions: IdDTO[]; + + @Expose() + @IsBoolean({ groups: [ValidationsGroupsEnum.default] }) + @ApiProperty({ required: false }) + mfaEnabled?: boolean; + + @Expose() + @Type(() => Date) + @ApiProperty({ required: false }) + lastLoginAt?: Date; + + @Expose() + @Type(() => Number) + @ApiProperty({ required: false }) + failedLoginAttemptsCount?: number; + + @Expose() + @ApiProperty({ required: false }) + @IsBoolean({ groups: [ValidationsGroupsEnum.default] }) + phoneNumberVerified?: boolean; + + @Expose() + @IsBoolean({ groups: [ValidationsGroupsEnum.default] }) + @ApiProperty() + agreedToTermsOfService: boolean; + + @Expose() + @IsDate({ groups: [ValidationsGroupsEnum.default] }) + @Type(() => Date) + @ApiProperty({ required: false }) + hitConfirmationURL?: Date; + + // storing the active access token for a user + @Expose() + @IsString({ groups: [ValidationsGroupsEnum.default] }) + @ApiProperty({ required: false }) + activeAccessToken?: string; + + // storing the active refresh token for a user + @Expose() + @IsString({ groups: [ValidationsGroupsEnum.default] }) + @ApiProperty({ required: false }) + activeRefreshToken?: string; +} diff --git a/backend_new/src/enums/user_accounts/filter-key-enum.ts b/backend_new/src/enums/user_accounts/filter-key-enum.ts deleted file mode 100644 index dfef2f5e6f..0000000000 --- a/backend_new/src/enums/user_accounts/filter-key-enum.ts +++ /dev/null @@ -1,4 +0,0 @@ -export enum UserFilterKeys { - isPartner = 'isPartner', - isPortalUser = 'isPortalUser', -} diff --git a/backend_new/src/modules/ami-chart.module.ts b/backend_new/src/modules/ami-chart.module.ts index 79ec90c618..c8b1423fef 100644 --- a/backend_new/src/modules/ami-chart.module.ts +++ b/backend_new/src/modules/ami-chart.module.ts @@ -1,12 +1,12 @@ import { Module } from '@nestjs/common'; import { AmiChartController } from '../controllers/ami-chart.controller'; import { AmiChartService } from '../services/ami-chart.service'; -import { PrismaService } from '../services/prisma.service'; +import { PrismaModule } from './prisma.module'; @Module({ - imports: [], + imports: [PrismaModule], controllers: [AmiChartController], - providers: [AmiChartService, PrismaService], - exports: [AmiChartService, PrismaService], + providers: [AmiChartService], + exports: [AmiChartService], }) export class AmiChartModule {} diff --git a/backend_new/src/modules/app.module.ts b/backend_new/src/modules/app.module.ts index 0f761498a1..559842f519 100644 --- a/backend_new/src/modules/app.module.ts +++ b/backend_new/src/modules/app.module.ts @@ -1,7 +1,7 @@ import { Module } from '@nestjs/common'; import { AppController } from '../controllers/app.controller'; import { AppService } from '../services/app.service'; -import { PrismaService } from '../services/prisma.service'; +import { PrismaModule } from './prisma.module'; import { AmiChartModule } from './ami-chart.module'; import { ListingModule } from './listing.module'; import { ReservedCommunityTypeModule } from './reserved-community-type.module'; @@ -12,6 +12,7 @@ import { JurisdictionModule } from './jurisdiction.module'; import { MultiselectQuestionModule } from './multiselect-question.module'; import { ApplicationModule } from './application.module'; import { AssetModule } from './asset.module'; +import { UserModule } from './user.module'; @Module({ imports: [ @@ -25,9 +26,11 @@ import { AssetModule } from './asset.module'; MultiselectQuestionModule, ApplicationModule, AssetModule, + UserModule, + PrismaModule, ], controllers: [AppController], - providers: [AppService, PrismaService], + providers: [AppService], exports: [ ListingModule, AmiChartModule, @@ -39,6 +42,8 @@ import { AssetModule } from './asset.module'; MultiselectQuestionModule, ApplicationModule, AssetModule, + UserModule, + PrismaModule, ], }) export class AppModule {} diff --git a/backend_new/src/modules/application.module.ts b/backend_new/src/modules/application.module.ts index 24aeba9a4b..323bb4505c 100644 --- a/backend_new/src/modules/application.module.ts +++ b/backend_new/src/modules/application.module.ts @@ -1,12 +1,12 @@ import { Module } from '@nestjs/common'; import { ApplicationController } from '../controllers/application.controller'; import { ApplicationService } from '../services/application.service'; -import { PrismaService } from '../services/prisma.service'; +import { PrismaModule } from './prisma.module'; @Module({ - imports: [], + imports: [PrismaModule], controllers: [ApplicationController], - providers: [ApplicationService, PrismaService], - exports: [ApplicationService, PrismaService], + providers: [ApplicationService], + exports: [ApplicationService], }) export class ApplicationModule {} diff --git a/backend_new/src/modules/jurisdiction.module.ts b/backend_new/src/modules/jurisdiction.module.ts index d0d54953bd..43f800b835 100644 --- a/backend_new/src/modules/jurisdiction.module.ts +++ b/backend_new/src/modules/jurisdiction.module.ts @@ -1,12 +1,12 @@ import { Module } from '@nestjs/common'; import { JurisdictionController } from '../controllers/jurisdiction.controller'; import { JurisdictionService } from '../services/jurisdiction.service'; -import { PrismaService } from '../services/prisma.service'; +import { PrismaModule } from './prisma.module'; @Module({ - imports: [], + imports: [PrismaModule], controllers: [JurisdictionController], - providers: [JurisdictionService, PrismaService], - exports: [JurisdictionService, PrismaService], + providers: [JurisdictionService], + exports: [JurisdictionService], }) export class JurisdictionModule {} diff --git a/backend_new/src/modules/listing.module.ts b/backend_new/src/modules/listing.module.ts index 3e169561a7..6fd7f1f088 100644 --- a/backend_new/src/modules/listing.module.ts +++ b/backend_new/src/modules/listing.module.ts @@ -1,19 +1,14 @@ import { Module } from '@nestjs/common'; import { ListingController } from '../controllers/listing.controller'; import { ListingService } from '../services/listing.service'; -import { PrismaService } from '../services/prisma.service'; +import { PrismaModule } from './prisma.module'; import { TranslationService } from '../services/translation.service'; import { GoogleTranslateService } from '../services/google-translate.service'; @Module({ - imports: [], + imports: [PrismaModule], controllers: [ListingController], - providers: [ - ListingService, - PrismaService, - TranslationService, - GoogleTranslateService, - ], - exports: [ListingService, PrismaService], + providers: [ListingService, TranslationService, GoogleTranslateService], + exports: [ListingService], }) export class ListingModule {} diff --git a/backend_new/src/modules/multiselect-question.module.ts b/backend_new/src/modules/multiselect-question.module.ts index 832f914463..c916e8934a 100644 --- a/backend_new/src/modules/multiselect-question.module.ts +++ b/backend_new/src/modules/multiselect-question.module.ts @@ -1,12 +1,12 @@ import { Module } from '@nestjs/common'; import { MultiselectQuestionController } from '../controllers/multiselect-question.controller'; import { MultiselectQuestionService } from '../services/multiselect-question.service'; -import { PrismaService } from '../services/prisma.service'; +import { PrismaModule } from './prisma.module'; @Module({ - imports: [], + imports: [PrismaModule], controllers: [MultiselectQuestionController], - providers: [MultiselectQuestionService, PrismaService], - exports: [MultiselectQuestionService, PrismaService], + providers: [MultiselectQuestionService], + exports: [MultiselectQuestionService], }) export class MultiselectQuestionModule {} diff --git a/backend_new/src/modules/prisma.module.ts b/backend_new/src/modules/prisma.module.ts new file mode 100644 index 0000000000..bc57b9ad55 --- /dev/null +++ b/backend_new/src/modules/prisma.module.ts @@ -0,0 +1,9 @@ +import { Global, Module } from '@nestjs/common'; +import { PrismaService } from '../services/prisma.service'; + +@Global() +@Module({ + providers: [PrismaService], + exports: [PrismaService], +}) +export class PrismaModule {} diff --git a/backend_new/src/modules/reserved-community-type.module.ts b/backend_new/src/modules/reserved-community-type.module.ts index 1f36b8894c..edab42dbe2 100644 --- a/backend_new/src/modules/reserved-community-type.module.ts +++ b/backend_new/src/modules/reserved-community-type.module.ts @@ -1,12 +1,12 @@ import { Module } from '@nestjs/common'; import { ReservedCommunityTypeController } from '../controllers/reserved-community-type.controller'; import { ReservedCommunityTypeService } from '../services/reserved-community-type.service'; -import { PrismaService } from '../services/prisma.service'; +import { PrismaModule } from './prisma.module'; @Module({ - imports: [], + imports: [PrismaModule], controllers: [ReservedCommunityTypeController], - providers: [ReservedCommunityTypeService, PrismaService], - exports: [ReservedCommunityTypeService, PrismaService], + providers: [ReservedCommunityTypeService], + exports: [ReservedCommunityTypeService], }) export class ReservedCommunityTypeModule {} diff --git a/backend_new/src/modules/unit-accessibility-priority-type.module.ts b/backend_new/src/modules/unit-accessibility-priority-type.module.ts index 10392043e7..9a87a83551 100644 --- a/backend_new/src/modules/unit-accessibility-priority-type.module.ts +++ b/backend_new/src/modules/unit-accessibility-priority-type.module.ts @@ -1,12 +1,12 @@ import { Module } from '@nestjs/common'; import { UnitAccessibilityPriorityTypeController } from '../controllers/unit-accessibility-priority-type.controller'; import { UnitAccessibilityPriorityTypeService } from '../services/unit-accessibility-priority-type.service'; -import { PrismaService } from '../services/prisma.service'; +import { PrismaModule } from './prisma.module'; @Module({ - imports: [], + imports: [PrismaModule], controllers: [UnitAccessibilityPriorityTypeController], - providers: [UnitAccessibilityPriorityTypeService, PrismaService], - exports: [UnitAccessibilityPriorityTypeService, PrismaService], + providers: [UnitAccessibilityPriorityTypeService], + exports: [UnitAccessibilityPriorityTypeService], }) export class UnitAccessibilityPriorityTypeServiceModule {} diff --git a/backend_new/src/modules/unit-rent-type.module.ts b/backend_new/src/modules/unit-rent-type.module.ts index d1173041c1..32d4051fd7 100644 --- a/backend_new/src/modules/unit-rent-type.module.ts +++ b/backend_new/src/modules/unit-rent-type.module.ts @@ -1,12 +1,12 @@ import { Module } from '@nestjs/common'; import { UnitRentTypeController } from '../controllers/unit-rent-type.controller'; import { UnitRentTypeService } from '../services/unit-rent-type.service'; -import { PrismaService } from '../services/prisma.service'; +import { PrismaModule } from './prisma.module'; @Module({ - imports: [], + imports: [PrismaModule], controllers: [UnitRentTypeController], - providers: [UnitRentTypeService, PrismaService], - exports: [UnitRentTypeService, PrismaService], + providers: [UnitRentTypeService], + exports: [UnitRentTypeService], }) export class UnitRentTypeModule {} diff --git a/backend_new/src/modules/unit-type.module.ts b/backend_new/src/modules/unit-type.module.ts index 9bb10e1521..b7899fff4a 100644 --- a/backend_new/src/modules/unit-type.module.ts +++ b/backend_new/src/modules/unit-type.module.ts @@ -1,12 +1,12 @@ import { Module } from '@nestjs/common'; import { UnitTypeController } from '../controllers/unit-type.controller'; import { UnitTypeService } from '../services/unit-type.service'; -import { PrismaService } from '../services/prisma.service'; +import { PrismaModule } from './prisma.module'; @Module({ - imports: [], + imports: [PrismaModule], controllers: [UnitTypeController], - providers: [UnitTypeService, PrismaService], - exports: [UnitTypeService, PrismaService], + providers: [UnitTypeService], + exports: [UnitTypeService], }) export class UnitTypeModule {} diff --git a/backend_new/src/modules/user.module.ts b/backend_new/src/modules/user.module.ts new file mode 100644 index 0000000000..2784de3df3 --- /dev/null +++ b/backend_new/src/modules/user.module.ts @@ -0,0 +1,12 @@ +import { Module } from '@nestjs/common'; +import { UserController } from '../controllers/user.controller'; +import { UserService } from '../services/user.service'; +import { PrismaModule } from './prisma.module'; + +@Module({ + imports: [PrismaModule], + controllers: [UserController], + providers: [UserService], + exports: [UserService], +}) +export class UserModule {} diff --git a/backend_new/src/services/listing.service.ts b/backend_new/src/services/listing.service.ts index ac3dc6e6ef..5da75bd4a9 100644 --- a/backend_new/src/services/listing.service.ts +++ b/backend_new/src/services/listing.service.ts @@ -3,9 +3,9 @@ import { PrismaService } from './prisma.service'; import { LanguagesEnum, Prisma } from '@prisma/client'; import { ListingsQueryParams } from '../dtos/listings/listings-query-params.dto'; import { + buildPaginationMetaInfo, calculateSkip, calculateTake, - shouldPaginate, } from '../utilities/pagination-helpers'; import { buildOrderBy } from '../utilities/build-order-by'; import { ListingFilterParams } from '../dtos/listings/listings-filter-params.dto'; @@ -133,7 +133,6 @@ export class ListingService { }; }> { const whereClause = this.buildWhereClause(params.filter, params.search); - const isPaginated = shouldPaginate(params.limit, params.page); const count = await this.prisma.listings.count({ where: whereClause, @@ -160,19 +159,11 @@ export class ListingService { } }); - const itemsPerPage = - isPaginated && params.limit !== 'all' ? params.limit : listings.length; - const totalItems = isPaginated ? count : listings.length; - - const paginationInfo = { - currentPage: isPaginated ? params.page : 1, - itemCount: listings.length, - itemsPerPage: itemsPerPage, - totalItems: totalItems, - totalPages: Math.ceil( - totalItems / (itemsPerPage ? itemsPerPage : totalItems), - ), - }; + const paginationInfo = buildPaginationMetaInfo( + params, + count, + listings.length, + ); return { items: listings, diff --git a/backend_new/src/services/user.service.ts b/backend_new/src/services/user.service.ts new file mode 100644 index 0000000000..b41576f97d --- /dev/null +++ b/backend_new/src/services/user.service.ts @@ -0,0 +1,235 @@ +import { Injectable, NotFoundException } from '@nestjs/common'; +import { PrismaService } from './prisma.service'; +import { User } from '../dtos/users/user.dto'; +import { mapTo } from '../utilities/mapTo'; +import { + buildPaginationMetaInfo, + calculateSkip, + calculateTake, +} from '../utilities/pagination-helpers'; +import { buildOrderBy } from '../utilities/build-order-by'; +import { Prisma } from '@prisma/client'; +import { UserQueryParams } from '../dtos/users/user-query-param.dto'; +import { PaginatedUserDto } from '../dtos/users/paginated-user.dto'; +import { OrderByEnum } from '../enums/shared/order-by-enum'; + +/* + this is the service for users + it handles all the backend's business logic for reading/writing/deleting user data +*/ + +const view: Prisma.UserAccountsInclude = { + listings: true, + jurisdictions: true, + userRoles: true, +}; + +@Injectable() +export class UserService { + constructor(private prisma: PrismaService) {} + + /* + this will get a set of users given the params passed in + */ + async list(params: UserQueryParams, user: User): Promise { + const whereClause = this.buildWhereClause(params, user); + const countQuery = this.prisma.userAccounts.count({ + where: whereClause, + }); + + const rawUsersQuery = this.prisma.userAccounts.findMany({ + skip: calculateSkip(params.limit, params.page), + take: calculateTake(params.limit), + orderBy: buildOrderBy( + ['firstName', 'lastName'], + [OrderByEnum.ASC, OrderByEnum.ASC], + ), + include: view, + where: whereClause, + }); + + const [count, rawUsers] = await Promise.all([countQuery, rawUsersQuery]); + + const users = mapTo(User, rawUsers); + + const paginationInfo = buildPaginationMetaInfo(params, count, users.length); + + return { + items: users, + meta: paginationInfo, + }; + } + + /* + this helps build the where clause for the list() + */ + buildWhereClause( + params: UserQueryParams, + user: User, + ): Prisma.UserAccountsWhereInput { + const filters: Prisma.UserAccountsWhereInput[] = []; + + if (params.search) { + filters.push({ + OR: [ + { + firstName: { + contains: params.search, + mode: Prisma.QueryMode.insensitive, + }, + }, + { + lastName: { + contains: params.search, + mode: Prisma.QueryMode.insensitive, + }, + }, + { + email: { + contains: params.search, + mode: Prisma.QueryMode.insensitive, + }, + }, + { + listings: { + some: { + name: { + contains: params.search, + mode: Prisma.QueryMode.insensitive, + }, + }, + }, + }, + ], + }); + } + + if (!params.filter?.length) { + return { + AND: filters, + }; + } + + params.filter.forEach((filter) => { + if ('isPortalUser' in filter && filter['isPortalUser']) { + if (user?.userRoles?.isAdmin) { + filters.push({ + OR: [ + { + userRoles: { + isPartner: true, + }, + }, + { + userRoles: { + isAdmin: true, + }, + }, + { + userRoles: { + isJurisdictionalAdmin: true, + }, + }, + ], + }); + } else if (user?.userRoles?.isJurisdictionalAdmin) { + filters.push({ + OR: [ + { + userRoles: { + isPartner: true, + }, + }, + { + userRoles: { + isJurisdictionalAdmin: true, + }, + }, + ], + }); + filters.push({ + jurisdictions: { + some: { + id: { + in: user?.jurisdictions?.map((juris) => juris.id), + }, + }, + }, + }); + } + } else if ('isPortalUser' in filter) { + filters.push({ + AND: [ + { + OR: [ + { + userRoles: { + isPartner: null, + }, + }, + { + userRoles: { + isPartner: false, + }, + }, + ], + }, + { + OR: [ + { + userRoles: { + isJurisdictionalAdmin: null, + }, + }, + { + userRoles: { + isJurisdictionalAdmin: false, + }, + }, + ], + }, + { + OR: [ + { + userRoles: { + isAdmin: null, + }, + }, + { + userRoles: { + isAdmin: false, + }, + }, + ], + }, + ], + }); + } + }); + return { + AND: filters, + }; + } + + /* + this will return 1 user or error + */ + async findOne(userId: string): Promise { + const rawUser = await this.prisma.userAccounts.findFirst({ + include: view, + where: { + id: { + equals: userId, + }, + }, + }); + + if (!rawUser) { + throw new NotFoundException( + `userId ${userId} was requested but not found`, + ); + } + + return mapTo(User, rawUser); + } +} diff --git a/backend_new/src/utilities/build-filter.ts b/backend_new/src/utilities/build-filter.ts index e0ad3dff62..aa35228233 100644 --- a/backend_new/src/utilities/build-filter.ts +++ b/backend_new/src/utilities/build-filter.ts @@ -1,7 +1,6 @@ import { HttpException, HttpStatus } from '@nestjs/common'; -import { Prisma, UserAccounts, UserRoles } from '@prisma/client'; +import { Prisma } from '@prisma/client'; import { Compare } from '../dtos/shared/base-filter.dto'; -import { UserFilterKeys } from '../enums/user_accounts/filter-key-enum'; type filter = { $comparison: Compare; @@ -16,10 +15,7 @@ type filter = { Because the where clause is specific to each model we are working with this has to be very generic. It only constructs the actual body of the where statement, how that clause is used must be managed by the service calling this helper function */ -export function buildFilter( - filter: filter, - user?: UserAccounts & { roles: UserRoles }, -): any { +export function buildFilter(filter: filter): any { const toReturn = []; const comparison = filter['$comparison']; const includeNulls = filter['$include_nulls']; @@ -28,10 +24,6 @@ export function buildFilter( ? Prisma.QueryMode.default : Prisma.QueryMode.insensitive; - if (filter.key === UserFilterKeys.isPortalUser) { - // TODO: addIsPortalUserQuery(filter.value, user); - } - if (comparison === Compare.IN) { toReturn.push({ in: String(filterValue) diff --git a/backend_new/src/utilities/pagination-helpers.ts b/backend_new/src/utilities/pagination-helpers.ts index daca3ff747..cdff57c898 100644 --- a/backend_new/src/utilities/pagination-helpers.ts +++ b/backend_new/src/utilities/pagination-helpers.ts @@ -1,3 +1,5 @@ +import { PaginationMeta } from '../dtos/shared/pagination.dto'; + /* takes in the params for limit and page responds true if we should account for pagination @@ -26,3 +28,35 @@ export const calculateSkip = (limit?: number | 'all', page?: number) => { export const calculateTake = (limit?: number | 'all') => { return limit !== 'all' ? limit : undefined; }; + +interface paginationMetaParams { + limit?: number | 'all'; + page?: number; +} + +/* + takes in params for limit and page, the results from the "count" query (the total number of records that meet whatever criteria) and the current "page" of record's length + responds with the meta info needed for the pagination meta info section +*/ +export const buildPaginationMetaInfo = ( + params: paginationMetaParams, + count: number, + recordArrayLength: number, +): PaginationMeta => { + const isPaginated = shouldPaginate(params.limit, params.page); + const itemsPerPage = + isPaginated && params.limit !== 'all' ? params.limit : recordArrayLength; + const totalItems = isPaginated ? count : recordArrayLength; + + const paginationInfo = { + currentPage: isPaginated ? params.page : 1, + itemCount: recordArrayLength, + itemsPerPage: itemsPerPage, + totalItems: totalItems, + totalPages: Math.ceil( + totalItems / (itemsPerPage ? itemsPerPage : totalItems), + ), + }; + + return paginationInfo; +}; diff --git a/backend_new/test/integration/user.e2e-spec.ts b/backend_new/test/integration/user.e2e-spec.ts new file mode 100644 index 0000000000..08388e9e49 --- /dev/null +++ b/backend_new/test/integration/user.e2e-spec.ts @@ -0,0 +1,104 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { INestApplication } from '@nestjs/common'; +import * as request from 'supertest'; +import { AppModule } from '../../src/modules/app.module'; +import { PrismaService } from '../../src/services/prisma.service'; +import { userFactory } from '../../prisma/seed-helpers/user-factory'; +import { randomUUID } from 'crypto'; +import { stringify } from 'qs'; +import { UserQueryParams } from '../../src/dtos/users/user-query-param.dto'; + +describe('User Controller Tests', () => { + let app: INestApplication; + let prisma: PrismaService; + + beforeAll(async () => { + const moduleFixture: TestingModule = await Test.createTestingModule({ + imports: [AppModule], + }).compile(); + + app = moduleFixture.createNestApplication(); + prisma = moduleFixture.get(PrismaService); + await app.init(); + }); + + afterAll(async () => { + await prisma.$disconnect(); + await app.close(); + }); + + it('should get no users from list() when no params and no data', async () => { + const res = await request(app.getHttpServer()) + .get(`/user/list?`) + .expect(200); + expect(res.body.items.length).toEqual(0); + }); + + it('should get users from list() when no params', async () => { + const userA = await prisma.userAccounts.create({ + data: userFactory(), + }); + const userB = await prisma.userAccounts.create({ + data: userFactory(), + }); + + const res = await request(app.getHttpServer()) + .get(`/user/list?`) + .expect(200); + expect(res.body.items.length).toBeGreaterThanOrEqual(2); + const ids = res.body.items.map((item) => item.id); + expect(ids).toContain(userA.id); + expect(ids).toContain(userB.id); + }); + + it('should get users from list() when params sent', async () => { + const userA = await prisma.userAccounts.create({ + data: userFactory({ roles: { isPartner: true }, firstName: '1110' }), + }); + const userB = await prisma.userAccounts.create({ + data: userFactory({ roles: { isPartner: true }, firstName: '1111' }), + }); + + const queryParams: UserQueryParams = { + limit: 2, + page: 1, + filter: [ + { + isPortalUser: true, + }, + ], + search: '111', + }; + const query = stringify(queryParams as any); + + const res = await request(app.getHttpServer()) + .get(`/user/list?${query}`) + .expect(200); + expect(res.body.items.length).toBeGreaterThanOrEqual(2); + const ids = res.body.items.map((item) => item.id); + expect(ids).toContain(userA.id); + expect(ids).toContain(userB.id); + }); + + it("should error when retrieve() called with id that doesn't exist", async () => { + const id = randomUUID(); + const res = await request(app.getHttpServer()) + .get(`/user/${id}`) + .expect(404); + expect(res.body.message).toEqual( + `userId ${id} was requested but not found`, + ); + }); + + it('should get user from retrieve()', async () => { + const userA = await prisma.userAccounts.create({ + data: userFactory(), + }); + + const res = await request(app.getHttpServer()) + .get(`/user/${userA.id}`) + .expect(200); + + expect(res.body.id).toEqual(userA.id); + }); +}); diff --git a/backend_new/test/unit/services/user.service.spec.ts b/backend_new/test/unit/services/user.service.spec.ts new file mode 100644 index 0000000000..efed9d6f22 --- /dev/null +++ b/backend_new/test/unit/services/user.service.spec.ts @@ -0,0 +1,211 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { PrismaService } from '../../../src/services/prisma.service'; +import { UserService } from '../../../src/services/user.service'; +import { randomUUID } from 'crypto'; +import { LanguagesEnum } from '@prisma/client'; + +describe('Testing user service', () => { + let service: UserService; + let prisma: PrismaService; + + const mockUser = (position: number, date: Date) => { + return { + id: randomUUID(), + createdAt: date, + updatedAt: date, + passwordUpdatedAt: date, + passwordValidForDays: 180, + confirmedAt: date, + email: `exampleemail_${position}@test.com`, + firstName: `first name ${position}`, + middleName: `middle name ${position}`, + lastName: `last name ${position}`, + dob: date, + listings: [], + userRoles: { isPartner: true }, + language: LanguagesEnum.en, + jurisdictions: [ + { + id: randomUUID(), + }, + ], + mfaEnabled: false, + lastLoginAt: date, + failedLoginAttemptsCount: 0, + phoneNumberVerified: true, + agreedToTermsOfService: true, + hitConfirmationURL: date, + activeAccessToken: randomUUID(), + activeRefreshToken: randomUUID(), + }; + }; + + const mockUserSet = (numberToCreate: number, date: Date) => { + const toReturn = []; + for (let i = 0; i < numberToCreate; i++) { + toReturn.push(mockUser(i, date)); + } + return toReturn; + }; + + beforeAll(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [UserService, PrismaService], + }).compile(); + + service = module.get(UserService); + prisma = module.get(PrismaService); + }); + + it('should return users from list() when no params are present', async () => { + const date = new Date(); + const mockedValue = mockUserSet(3, date); + prisma.userAccounts.findMany = jest.fn().mockResolvedValue(mockedValue); + prisma.userAccounts.count = jest.fn().mockResolvedValue(3); + + expect(await service.list({}, null)).toEqual({ + items: mockedValue, + meta: { + currentPage: 1, + itemCount: 3, + itemsPerPage: 3, + totalItems: 3, + totalPages: 1, + }, + }); + + expect(prisma.userAccounts.findMany).toHaveBeenCalledWith({ + include: { + jurisdictions: true, + listings: true, + userRoles: true, + }, + orderBy: [{ firstName: 'asc' }, { lastName: 'asc' }], + skip: 0, + where: { + AND: [], + }, + }); + }); + + it('should return users from list() when params are present', async () => { + const date = new Date(); + const mockedValue = mockUserSet(3, date); + prisma.userAccounts.findMany = jest.fn().mockResolvedValue(mockedValue); + prisma.userAccounts.count = jest.fn().mockResolvedValue(3); + + expect( + await service.list( + { + search: 'search value', + page: 2, + limit: 5, + filter: [ + { + isPortalUser: true, + }, + ], + }, + null, + ), + ).toEqual({ + items: mockedValue, + meta: { + currentPage: 2, + itemCount: 3, + itemsPerPage: 5, + totalItems: 3, + totalPages: 1, + }, + }); + + expect(prisma.userAccounts.findMany).toHaveBeenCalledWith({ + include: { + jurisdictions: true, + listings: true, + userRoles: true, + }, + orderBy: [{ firstName: 'asc' }, { lastName: 'asc' }], + skip: 5, + take: 5, + where: { + AND: [ + { + OR: [ + { + firstName: { + contains: 'search value', + mode: 'insensitive', + }, + }, + { + lastName: { + contains: 'search value', + mode: 'insensitive', + }, + }, + { + email: { + contains: 'search value', + mode: 'insensitive', + }, + }, + { + listings: { + some: { + name: { + contains: 'search value', + mode: 'insensitive', + }, + }, + }, + }, + ], + }, + ], + }, + }); + }); + + it('should return user from findOne() when id present', async () => { + const date = new Date(); + const mockedValue = mockUser(3, date); + prisma.userAccounts.findFirst = jest.fn().mockResolvedValue(mockedValue); + + expect(await service.findOne('example Id')).toEqual(mockedValue); + + expect(prisma.userAccounts.findFirst).toHaveBeenCalledWith({ + include: { + listings: true, + jurisdictions: true, + userRoles: true, + }, + where: { + id: { + equals: 'example Id', + }, + }, + }); + }); + + it('should error when calling findOne() when id not present', async () => { + prisma.userAccounts.findFirst = jest.fn().mockResolvedValue(null); + + await expect( + async () => await service.findOne('example Id'), + ).rejects.toThrowError('userId example Id was requested but not found'); + + expect(prisma.userAccounts.findFirst).toHaveBeenCalledWith({ + include: { + listings: true, + jurisdictions: true, + userRoles: true, + }, + where: { + id: { + equals: 'example Id', + }, + }, + }); + }); +}); diff --git a/backend_new/test/unit/utilities/pagination-helpers.spec.ts b/backend_new/test/unit/utilities/pagination-helpers.spec.ts index afa3426f00..37f61d765e 100644 --- a/backend_new/test/unit/utilities/pagination-helpers.spec.ts +++ b/backend_new/test/unit/utilities/pagination-helpers.spec.ts @@ -2,6 +2,7 @@ import { shouldPaginate, calculateSkip, calculateTake, + buildPaginationMetaInfo, } from '../../../src/utilities/pagination-helpers'; describe('Testing pagination helpers', () => { describe('Testing shouldPaginate', () => { @@ -37,4 +38,75 @@ describe('Testing pagination helpers', () => { expect(calculateTake(1)).toBe(1); }); }); + describe('Testing buildPaginationMetaInfo', () => { + it('should return 1 page of 10 items for 10 items present', () => { + expect(buildPaginationMetaInfo({ limit: 10, page: 1 }, 10, 10)).toEqual({ + currentPage: 1, + itemCount: 10, + itemsPerPage: 10, + totalItems: 10, + totalPages: 1, + }); + }); + it('should return 2 pages of 10 items for 20 items present', () => { + expect(buildPaginationMetaInfo({ limit: 10, page: 1 }, 20, 10)).toEqual({ + currentPage: 1, + itemCount: 10, + itemsPerPage: 10, + totalItems: 20, + totalPages: 2, + }); + }); + it('should return all records for unpaginated', () => { + expect( + buildPaginationMetaInfo({ limit: 'all', page: 1 }, 10, 10), + ).toEqual({ + currentPage: 1, + itemCount: 10, + itemsPerPage: 10, + totalItems: 10, + totalPages: 1, + }); + }); + it('should return 1 page of 5 items for 5 items present when "pagesize" is 10', () => { + expect(buildPaginationMetaInfo({ limit: 10, page: 1 }, 5, 5)).toEqual({ + currentPage: 1, + itemCount: 5, + itemsPerPage: 10, + totalItems: 5, + totalPages: 1, + }); + }); + it('should return 1 page of 0 items for 0 items present when "pagesize" is 20', () => { + expect(buildPaginationMetaInfo({ limit: 10, page: 1 }, 0, 0)).toEqual({ + currentPage: 1, + itemCount: 0, + itemsPerPage: 10, + totalItems: 0, + totalPages: 0, + }); + }); + it('should return 1 page of 100 items for 100 items present when "pagesize" is 100', () => { + expect( + buildPaginationMetaInfo({ limit: 100, page: 1 }, 100, 100), + ).toEqual({ + currentPage: 1, + itemCount: 100, + itemsPerPage: 100, + totalItems: 100, + totalPages: 1, + }); + }); + it('should return page 10 of 200 items present when "pagesize" is 20', () => { + expect(buildPaginationMetaInfo({ limit: 20, page: 10 }, 200, 20)).toEqual( + { + currentPage: 10, + itemCount: 20, + itemsPerPage: 20, + totalItems: 200, + totalPages: 10, + }, + ); + }); + }); }); diff --git a/backend_new/types/src/backend-swagger.ts b/backend_new/types/src/backend-swagger.ts index 6a934d8552..223548e4db 100644 --- a/backend_new/types/src/backend-swagger.ts +++ b/backend_new/types/src/backend-swagger.ts @@ -1283,6 +1283,91 @@ export class ApplicationsService { } } +export class UserService { + /** + * + */ + userControllerProfile(options: IRequestOptions = {}): Promise { + return new Promise((resolve, reject) => { + let url = basePath + '/user'; + + const configs: IRequestConfig = getConfigs( + 'get', + 'application/json', + url, + options, + ); + + /** 适配ios13getèŻ·æ±‚äžć…èźžćžŠbody */ + + axios(configs, resolve, reject); + }); + } + /** + * Get a paginated set of users + */ + list( + params: { + /** */ + page?: number; + /** */ + limit?: number | 'all'; + /** */ + isPortalUser?: boolean; + /** */ + search?: string; + } = {} as any, + options: IRequestOptions = {}, + ): Promise { + return new Promise((resolve, reject) => { + let url = basePath + '/user/list'; + + const configs: IRequestConfig = getConfigs( + 'get', + 'application/json', + url, + options, + ); + configs.params = { + page: params['page'], + limit: params['limit'], + isPortalUser: params['isPortalUser'], + search: params['search'], + }; + + /** 适配ios13getèŻ·æ±‚äžć…èźžćžŠbody */ + + axios(configs, resolve, reject); + }); + } + /** + * Get user by id + */ + retrieve( + params: { + /** */ + id: string; + } = {} as any, + options: IRequestOptions = {}, + ): Promise { + return new Promise((resolve, reject) => { + let url = basePath + '/user/{id}'; + url = url.replace('{id}', params['id'] + ''); + + const configs: IRequestConfig = getConfigs( + 'get', + 'application/json', + url, + options, + ); + + /** 适配ios13getèŻ·æ±‚äžć…èźžćžŠbody */ + + axios(configs, resolve, reject); + }); + } +} + export interface SuccessDTO { /** */ success: boolean; @@ -2504,6 +2589,102 @@ export interface PaginatedApplication { items: Application[]; } +export interface UserRole { + /** */ + id: string; + + /** */ + createdAt: Date; + + /** */ + updatedAt: Date; + + /** */ + isAdmin?: boolean; + + /** */ + isJurisdictionalAdmin?: boolean; + + /** */ + isPartner?: boolean; +} + +export interface User { + /** */ + id: string; + + /** */ + createdAt: Date; + + /** */ + updatedAt: Date; + + /** */ + passwordUpdatedAt: Date; + + /** */ + passwordValidForDays: number; + + /** */ + confirmedAt?: Date; + + /** */ + email: string; + + /** */ + middleName?: string; + + /** */ + lastName: string; + + /** */ + dob?: Date; + + /** */ + phoneNumber?: string; + + /** */ + listings: IdDTO[]; + + /** */ + userRoles?: UserRole; + + /** */ + language?: LanguagesEnum; + + /** */ + jurisdictions: IdDTO[]; + + /** */ + mfaEnabled?: boolean; + + /** */ + lastLoginAt?: Date; + + /** */ + failedLoginAttemptsCount?: number; + + /** */ + phoneNumberVerified?: boolean; + + /** */ + agreedToTermsOfService: boolean; + + /** */ + hitConfirmationURL?: Date; + + /** */ + activeAccessToken?: string; + + /** */ + activeRefreshToken?: string; +} + +export interface PaginatedUser { + /** */ + items: User[]; +} + export enum ListingViews { 'fundamentals' = 'fundamentals', 'base' = 'base', From 4441ed1219110f6eb4219ad3c5ba2a58fd14b0ac Mon Sep 17 00:00:00 2001 From: Yazeed Loonat Date: Thu, 17 Aug 2023 11:07:24 -0700 Subject: [PATCH 19/57] feat: prisma user write (#3591) --- backend_new/.env.template | 1 + backend_new/package.json | 9 +- backend_new/prisma/seed-dev.ts | 5 +- .../prisma/seed-helpers/user-factory.ts | 13 +- backend_new/prisma/seed-staging.ts | 7 +- .../src/controllers/user.controller.ts | 92 +- backend_new/src/decorators/match-decorator.ts | 33 + .../dtos/users/confirmation-request.dto.ts | 12 + .../src/dtos/users/email-and-app-url.dto.ts | 23 + .../src/dtos/users/user-create-params.dto.ts | 18 + backend_new/src/dtos/users/user-create.dto.ts | 62 ++ backend_new/src/dtos/users/user-invite.dto.ts | 20 + backend_new/src/dtos/users/user-role.dto.ts | 3 +- backend_new/src/dtos/users/user-update.dto.ts | 66 ++ backend_new/src/dtos/users/user.dto.ts | 10 +- backend_new/src/services/user.service.ts | 540 +++++++++- backend_new/src/utilities/password-helpers.ts | 37 + backend_new/src/utilities/password-regex.ts | 1 + .../test/integration/ami-chart.e2e-spec.ts | 2 +- backend_new/test/integration/app.e2e-spec.ts | 2 +- .../test/integration/application.e2e-spec.ts | 6 +- .../test/integration/asset.e2e-spec.ts | 2 +- .../test/integration/jurisdiction.e2e-spec.ts | 2 +- .../test/integration/listing.e2e-spec.ts | 2 +- .../multiselect-question.e2e-spec.ts | 2 +- .../reserved-community-type.e2e-spec.ts | 2 +- ...it-accessibility-priority-type.e2e-spec.ts | 2 +- .../integration/unit-rent-type.e2e-spec.ts | 2 +- .../test/integration/unit-type.e2e-spec.ts | 2 +- backend_new/test/integration/user.e2e-spec.ts | 448 +++++++- .../test/unit/services/user.service.spec.ts | 997 +++++++++++++++++- .../unit/utilities/password-helper.spec.ts | 23 + .../unit/utilities/password-regex.spec.ts | 9 + backend_new/tsconfig.json | 3 +- backend_new/types/src/backend-swagger.ts | 391 ++++++- backend_new/yarn.lock | 37 +- 36 files changed, 2819 insertions(+), 67 deletions(-) create mode 100644 backend_new/src/decorators/match-decorator.ts create mode 100644 backend_new/src/dtos/users/confirmation-request.dto.ts create mode 100644 backend_new/src/dtos/users/email-and-app-url.dto.ts create mode 100644 backend_new/src/dtos/users/user-create-params.dto.ts create mode 100644 backend_new/src/dtos/users/user-create.dto.ts create mode 100644 backend_new/src/dtos/users/user-invite.dto.ts create mode 100644 backend_new/src/dtos/users/user-update.dto.ts create mode 100644 backend_new/src/utilities/password-helpers.ts create mode 100644 backend_new/src/utilities/password-regex.ts create mode 100644 backend_new/test/unit/utilities/password-helper.spec.ts create mode 100644 backend_new/test/unit/utilities/password-regex.spec.ts diff --git a/backend_new/.env.template b/backend_new/.env.template index 8a651d57cb..edb7611bf7 100644 --- a/backend_new/.env.template +++ b/backend_new/.env.template @@ -4,3 +4,4 @@ GOOGLE_API_EMAIL= GOOGLE_API_ID= GOOGLE_API_KEY= CLOUDINARY_SECRET= +APP_SECRET= diff --git a/backend_new/package.json b/backend_new/package.json index 975cc9ac9c..f9427c86d4 100644 --- a/backend_new/package.json +++ b/backend_new/package.json @@ -35,9 +35,11 @@ "@nestjs/platform-express": "^8.0.0", "@nestjs/swagger": "^6.3.0", "@prisma/client": "^5.0.0", - "class-validator": "^0.14.0", "class-transformer": "^0.5.1", + "class-validator": "^0.14.0", "cloudinary": "^1.37.3", + "dayjs": "^1.11.9", + "jsonwebtoken": "^9.0.1", "lodash": "^4.17.21", "prisma": "^5.0.0", "qs": "^6.11.2", @@ -56,11 +58,11 @@ "@types/supertest": "^2.0.11", "@typescript-eslint/eslint-plugin": "^5.0.0", "@typescript-eslint/parser": "^5.0.0", - "dayjs": "^1.11.8", "eslint": "^8.0.1", "eslint-config-prettier": "^8.3.0", "eslint-plugin-prettier": "^4.0.0", "jest": "^29.6.2", + "jest-environment-jsdom": "^29.6.2", "prettier": "^2.3.2", "source-map-support": "^0.5.20", "supertest": "^6.1.3", @@ -68,8 +70,7 @@ "ts-loader": "^9.2.3", "ts-node": "^10.0.0", "tsconfig-paths": "^3.10.1", - "typescript": "^5.1.6", - "jest-environment-jsdom": "^29.6.2" + "typescript": "^5.1.6" }, "jest": { "moduleFileExtensions": [ diff --git a/backend_new/prisma/seed-dev.ts b/backend_new/prisma/seed-dev.ts index adcbf756ed..8c68245e8c 100644 --- a/backend_new/prisma/seed-dev.ts +++ b/backend_new/prisma/seed-dev.ts @@ -39,7 +39,10 @@ const createMultiselect = async ( export const devSeeding = async (prismaClient: PrismaClient) => { await prismaClient.userAccounts.create({ - data: userFactory({ roles: { isAdmin: true }, email: 'admin@example.com' }), + data: await userFactory({ + roles: { isAdmin: true }, + email: 'admin@example.com', + }), }); const jurisdiction = await prismaClient.jurisdictions.create({ data: jurisdictionFactory(), diff --git a/backend_new/prisma/seed-helpers/user-factory.ts b/backend_new/prisma/seed-helpers/user-factory.ts index abc69da76f..e6f77883e1 100644 --- a/backend_new/prisma/seed-helpers/user-factory.ts +++ b/backend_new/prisma/seed-helpers/user-factory.ts @@ -1,18 +1,19 @@ import { Prisma } from '@prisma/client'; import { randomAdjective, randomNoun } from './word-generator'; +import { passwordToHash } from '../../src/utilities/password-helpers'; -export const userFactory = (optionalParams?: { +export const userFactory = async (optionalParams?: { roles?: Prisma.UserRolesUncheckedCreateWithoutUserAccountsInput; firstName?: string; lastName?: string; email?: string; -}): Prisma.UserAccountsCreateInput => ({ - email: optionalParams?.email || `${randomNoun()}@${randomAdjective()}.com`, +}): Promise => ({ + email: + optionalParams?.email?.toLocaleLowerCase() || + `${randomNoun().toLowerCase()}@${randomAdjective().toLowerCase()}.com`, firstName: optionalParams?.firstName || 'First', lastName: optionalParams?.lastName || 'Last', - // TODO: update with passwordService hashing when that is completed - passwordHash: - 'a921d45de2db97818a124126706a1bf52310d231be04e1764d4eedffaccadcea3af70fa1d806b8527b2ebb98a2dd48ab3f07238bb9d39d4bcd2de4c207b67d4e#c870c8c0dbc08b27f4fc1dab32266cfde4aef8f2c606dab1162f9e71763f1fd11f28b2b81e05e7aeefd08b745d636624b623f505d47a54213fb9822c366bbbfe', + passwordHash: await passwordToHash('abcdef'), userRoles: { create: { isAdmin: optionalParams?.roles?.isAdmin || false, diff --git a/backend_new/prisma/seed-staging.ts b/backend_new/prisma/seed-staging.ts index 6ff138caca..91adee461c 100644 --- a/backend_new/prisma/seed-staging.ts +++ b/backend_new/prisma/seed-staging.ts @@ -7,7 +7,7 @@ import { PrismaClient, ReviewOrderTypeEnum, } from '@prisma/client'; -import * as dayjs from 'dayjs'; +import dayjs from 'dayjs'; import { jurisdictionFactory } from './seed-helpers/jurisdiction-factory'; import { listingFactory } from './seed-helpers/listing-factory'; import { amiChartFactory } from './seed-helpers/ami-chart-factory'; @@ -28,7 +28,10 @@ export const stagingSeed = async ( ) => { // create admin user await prismaClient.userAccounts.create({ - data: userFactory({ roles: { isAdmin: true }, email: 'admin@example.com' }), + data: await userFactory({ + roles: { isAdmin: true }, + email: 'admin@example.com', + }), }); // create single jurisdiction const jurisdiction = await prismaClient.jurisdictions.create({ diff --git a/backend_new/src/controllers/user.controller.ts b/backend_new/src/controllers/user.controller.ts index 0970b09d76..16c7ef9ee1 100644 --- a/backend_new/src/controllers/user.controller.ts +++ b/backend_new/src/controllers/user.controller.ts @@ -1,9 +1,13 @@ import { + Body, ClassSerializerInterceptor, Controller, + Delete, Get, Param, ParseUUIDPipe, + Post, + Put, Query, Request, UseInterceptors, @@ -24,11 +28,18 @@ import { mapTo } from '../utilities/mapTo'; import { PaginatedUserDto } from '../dtos/users/paginated-user.dto'; import { UserQueryParams } from '../dtos/users/user-query-param.dto'; import { Request as ExpressRequest } from 'express'; +import { UserUpdate } from '../dtos/users/user-update.dto'; +import { SuccessDTO } from '../dtos/shared/success.dto'; +import { UserCreate } from '../dtos/users/user-create.dto'; +import { UserCreateParams } from '../dtos/users/user-create-params.dto'; +import { EmailAndAppUrl } from '../dtos/users/email-and-app-url.dto'; +import { ConfirmationRequest } from '../dtos/users/confirmation-request.dto'; +import { UserInvite } from '../dtos/users/user-invite.dto'; @Controller('user') @ApiTags('user') @UsePipes(new ValidationPipe(defaultValidationPipeOptions)) -@ApiExtraModels(IdDTO) +@ApiExtraModels(IdDTO, EmailAndAppUrl) export class UserController { constructor(private readonly userService: UserService) {} @@ -63,4 +74,83 @@ export class UserController { ): Promise { return this.userService.findOne(userId); } + + @Put('forgot-password') + @ApiOperation({ summary: 'Forgot Password', operationId: 'forgotPassword' }) + @ApiOkResponse({ type: SuccessDTO }) + async forgotPassword(@Body() dto: EmailAndAppUrl): Promise { + return await this.userService.forgotPassword(dto); + } + + @Put(':id') + @ApiOperation({ summary: 'Update user', operationId: 'update' }) + @ApiOkResponse({ type: User }) + async update(@Body() dto: UserUpdate): Promise { + return await this.userService.update(dto); + } + + @Delete() + @ApiOperation({ summary: 'Delete user by id', operationId: 'delete' }) + @ApiOkResponse({ type: SuccessDTO }) + async delete(@Body() dto: IdDTO): Promise { + return await this.userService.delete(dto.id); + } + + @Post() + @ApiOperation({ + summary: 'Creates a public only user', + operationId: 'create', + }) + @ApiOkResponse({ type: User }) + async create( + @Body() dto: UserCreate, + @Query() queryParams: UserCreateParams, + ): Promise { + return await this.userService.create( + dto, + false, + queryParams.noWelcomeEmail !== true, + ); + } + + @Post('/invite') + @ApiOperation({ summary: 'Invite partner user', operationId: 'invite' }) + @ApiOkResponse({ type: User }) + async invite(@Body() dto: UserInvite): Promise { + return await this.userService.create(dto, true); + } + + @Post('resend-confirmation') + @ApiOperation({ + summary: 'Resend public confirmation', + operationId: 'resendConfirmation', + }) + @ApiOkResponse({ type: SuccessDTO }) + async confirmation(@Body() dto: EmailAndAppUrl): Promise { + return await this.userService.resendConfirmation(dto, true); + } + + @Post('resend-partner-confirmation') + @ApiOperation({ + summary: 'Resend partner confirmation', + operationId: 'resendPartnerConfirmation', + }) + @ApiOkResponse({ type: SuccessDTO }) + async requestConfirmationResend( + @Body() dto: EmailAndAppUrl, + ): Promise { + return await this.userService.resendConfirmation(dto, false); + } + + @Post('is-confirmation-token-valid') + @ApiOperation({ + summary: 'Verifies token is valid', + operationId: 'isUserConfirmationTokenValid', + }) + @ApiOkResponse({ type: SuccessDTO }) + async isUserConfirmationTokenValid( + @Body() dto: ConfirmationRequest, + ): Promise { + return await this.userService.isUserConfirmationTokenValid(dto); + } } diff --git a/backend_new/src/decorators/match-decorator.ts b/backend_new/src/decorators/match-decorator.ts new file mode 100644 index 0000000000..385d77c74f --- /dev/null +++ b/backend_new/src/decorators/match-decorator.ts @@ -0,0 +1,33 @@ +import { + registerDecorator, + ValidationArguments, + ValidationOptions, + ValidatorConstraint, + ValidatorConstraintInterface, +} from 'class-validator'; + +/* + This creates a validation decorator. + It requires that the current field's value matches related property supplied to it as an argument + e.g. password's and passwordConfirmation's values should match +*/ +export function Match(property: string, validationOptions?: ValidationOptions) { + return (object: unknown, propertyName: string) => { + registerDecorator({ + target: object.constructor, + propertyName, + options: validationOptions, + constraints: [property], + validator: MatchConstraint, + }); + }; +} + +@ValidatorConstraint({ name: 'Match' }) +export class MatchConstraint implements ValidatorConstraintInterface { + validate(value: unknown, args: ValidationArguments) { + const [relatedPropertyName] = args.constraints; + const relatedValue = (args.object as unknown)[relatedPropertyName]; + return value === relatedValue; + } +} diff --git a/backend_new/src/dtos/users/confirmation-request.dto.ts b/backend_new/src/dtos/users/confirmation-request.dto.ts new file mode 100644 index 0000000000..7ff96f7078 --- /dev/null +++ b/backend_new/src/dtos/users/confirmation-request.dto.ts @@ -0,0 +1,12 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { Expose } from 'class-transformer'; +import { IsString, MaxLength } from 'class-validator'; +import { ValidationsGroupsEnum } from '../../enums/shared/validation-groups-enum'; + +export class ConfirmationRequest { + @Expose() + @IsString({ groups: [ValidationsGroupsEnum.default] }) + @MaxLength(256, { groups: [ValidationsGroupsEnum.default] }) + @ApiProperty({ required: true }) + token: string; +} diff --git a/backend_new/src/dtos/users/email-and-app-url.dto.ts b/backend_new/src/dtos/users/email-and-app-url.dto.ts new file mode 100644 index 0000000000..7126a5b5e0 --- /dev/null +++ b/backend_new/src/dtos/users/email-and-app-url.dto.ts @@ -0,0 +1,23 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { Expose } from 'class-transformer'; +import { IsEmail, IsString, MaxLength } from 'class-validator'; +import { EnforceLowerCase } from '../../decorators/enforce-lower-case.decorator'; +import { ValidationsGroupsEnum } from '../../enums/shared/validation-groups-enum'; + +/* + this DTO is used to take in a user's email address and the url from which the user is sending the api request + the url is option as each endpoint handles a default case for this +*/ +export class EmailAndAppUrl { + @Expose() + @IsEmail({}, { groups: [ValidationsGroupsEnum.default] }) + @ApiProperty({ required: true }) + @EnforceLowerCase() + email: string; + + @Expose() + @IsString({ groups: [ValidationsGroupsEnum.default] }) + @ApiProperty({ required: false }) + @MaxLength(256, { groups: [ValidationsGroupsEnum.default] }) + appUrl?: string; +} diff --git a/backend_new/src/dtos/users/user-create-params.dto.ts b/backend_new/src/dtos/users/user-create-params.dto.ts new file mode 100644 index 0000000000..7a791fd7d0 --- /dev/null +++ b/backend_new/src/dtos/users/user-create-params.dto.ts @@ -0,0 +1,18 @@ +import { Expose, Transform, TransformFnParams } from 'class-transformer'; +import { ApiProperty } from '@nestjs/swagger'; +import { IsBoolean } from 'class-validator'; +import { ValidationsGroupsEnum } from '../../enums/shared/validation-groups-enum'; + +export class UserCreateParams { + @Expose() + @ApiProperty({ + type: Boolean, + example: true, + required: false, + }) + @IsBoolean({ groups: [ValidationsGroupsEnum.default] }) + @Transform((value: TransformFnParams) => value?.value === 'true', { + toClassOnly: true, + }) + noWelcomeEmail?: boolean; +} diff --git a/backend_new/src/dtos/users/user-create.dto.ts b/backend_new/src/dtos/users/user-create.dto.ts new file mode 100644 index 0000000000..c1de34e684 --- /dev/null +++ b/backend_new/src/dtos/users/user-create.dto.ts @@ -0,0 +1,62 @@ +import { ApiProperty, OmitType } from '@nestjs/swagger'; +import { Expose, Type } from 'class-transformer'; +import { + IsArray, + IsEmail, + IsString, + Matches, + MaxLength, + ValidateNested, +} from 'class-validator'; +import { UserUpdate } from './user-update.dto'; + +import { EnforceLowerCase } from '../../decorators/enforce-lower-case.decorator'; +import { ValidationsGroupsEnum } from '../../enums/shared/validation-groups-enum'; +import { passwordRegex } from '../../utilities/password-regex'; +import { Match } from '../../decorators/match-decorator'; +import { IdDTO } from '../shared/id.dto'; + +export class UserCreate extends OmitType(UserUpdate, [ + 'id', + 'userRoles', + 'password', + 'currentPassword', + 'email', + 'jurisdictions', +]) { + @Expose() + @ApiProperty({ required: true }) + @IsString({ groups: [ValidationsGroupsEnum.default] }) + @Matches(passwordRegex, { + message: 'passwordTooWeak', + groups: [ValidationsGroupsEnum.default], + }) + password: string; + + @Expose() + @IsString({ groups: [ValidationsGroupsEnum.default] }) + @MaxLength(64, { groups: [ValidationsGroupsEnum.default] }) + @Match('password', { groups: [ValidationsGroupsEnum.default] }) + @ApiProperty({ required: true }) + passwordConfirmation: string; + + @Expose() + @ApiProperty({ required: true }) + @IsEmail({}, { groups: [ValidationsGroupsEnum.default] }) + @EnforceLowerCase() + email: string; + + @Expose() + @ApiProperty({ required: true }) + @IsEmail({}, { groups: [ValidationsGroupsEnum.default] }) + @Match('email', { groups: [ValidationsGroupsEnum.default] }) + @EnforceLowerCase() + emailConfirmation: string; + + @Expose() + @Type(() => IdDTO) + @IsArray({ groups: [ValidationsGroupsEnum.default] }) + @ValidateNested({ groups: [ValidationsGroupsEnum.default], each: true }) + @ApiProperty({ type: IdDTO, isArray: true, required: false }) + jurisdictions?: IdDTO[]; +} diff --git a/backend_new/src/dtos/users/user-invite.dto.ts b/backend_new/src/dtos/users/user-invite.dto.ts new file mode 100644 index 0000000000..4456eee300 --- /dev/null +++ b/backend_new/src/dtos/users/user-invite.dto.ts @@ -0,0 +1,20 @@ +import { ApiProperty, OmitType } from '@nestjs/swagger'; +import { Expose } from 'class-transformer'; +import { IsEmail } from 'class-validator'; +import { UserUpdate } from './user-update.dto'; + +import { EnforceLowerCase } from '../../decorators/enforce-lower-case.decorator'; +import { ValidationsGroupsEnum } from '../../enums/shared/validation-groups-enum'; + +export class UserInvite extends OmitType(UserUpdate, [ + 'id', + 'password', + 'currentPassword', + 'email', +]) { + @Expose() + @ApiProperty({ required: true }) + @IsEmail({}, { groups: [ValidationsGroupsEnum.default] }) + @EnforceLowerCase() + email: string; +} diff --git a/backend_new/src/dtos/users/user-role.dto.ts b/backend_new/src/dtos/users/user-role.dto.ts index da9f21f7c8..1a9fb023e0 100644 --- a/backend_new/src/dtos/users/user-role.dto.ts +++ b/backend_new/src/dtos/users/user-role.dto.ts @@ -2,9 +2,8 @@ import { ApiProperty } from '@nestjs/swagger'; import { Expose } from 'class-transformer'; import { IsBoolean } from 'class-validator'; import { ValidationsGroupsEnum } from '../../enums/shared/validation-groups-enum'; -import { AbstractDTO } from '../shared/abstract.dto'; -export class UserRole extends AbstractDTO { +export class UserRole { @Expose() @IsBoolean({ groups: [ValidationsGroupsEnum.default] }) @ApiProperty({ required: false }) diff --git a/backend_new/src/dtos/users/user-update.dto.ts b/backend_new/src/dtos/users/user-update.dto.ts new file mode 100644 index 0000000000..023724520d --- /dev/null +++ b/backend_new/src/dtos/users/user-update.dto.ts @@ -0,0 +1,66 @@ +import { ApiProperty, OmitType } from '@nestjs/swagger'; +import { Expose } from 'class-transformer'; +import { + IsEmail, + IsNotEmpty, + IsString, + Matches, + MaxLength, + ValidateIf, +} from 'class-validator'; +import { User } from './user.dto'; + +import { EnforceLowerCase } from '../../decorators/enforce-lower-case.decorator'; +import { ValidationsGroupsEnum } from '../../enums/shared/validation-groups-enum'; +import { passwordRegex } from '../../utilities/password-regex'; + +export class UserUpdate extends OmitType(User, [ + 'createdAt', + 'updatedAt', + 'email', + 'mfaEnabled', + 'passwordUpdatedAt', + 'passwordValidForDays', + 'lastLoginAt', + 'failedLoginAttemptsCount', + 'confirmedAt', + 'lastLoginAt', + 'phoneNumberVerified', + 'agreedToTermsOfService', + 'hitConfirmationURL', + 'activeAccessToken', + 'activeRefreshToken', +]) { + @Expose() + @ApiProperty({ required: false }) + @IsEmail({}, { groups: [ValidationsGroupsEnum.default] }) + @EnforceLowerCase() + email?: string; + + @Expose() + @ApiProperty({ required: false }) + @IsEmail({}, { groups: [ValidationsGroupsEnum.default] }) + @EnforceLowerCase() + newEmail?: string; + + @Expose() + @ApiProperty({ required: false }) + @IsString({ groups: [ValidationsGroupsEnum.default] }) + @Matches(passwordRegex, { + message: 'passwordTooWeak', + groups: [ValidationsGroupsEnum.default], + }) + password?: string; + + @Expose() + @ValidateIf((o) => o.password, { groups: [ValidationsGroupsEnum.default] }) + @IsNotEmpty({ groups: [ValidationsGroupsEnum.default] }) + @ApiProperty({ required: false }) + currentPassword?: string; + + @Expose() + @IsString({ groups: [ValidationsGroupsEnum.default] }) + @MaxLength(256, { groups: [ValidationsGroupsEnum.default] }) + @ApiProperty({ required: false }) + appUrl?: string; +} diff --git a/backend_new/src/dtos/users/user.dto.ts b/backend_new/src/dtos/users/user.dto.ts index bfe29e4ab8..7ba94a683b 100644 --- a/backend_new/src/dtos/users/user.dto.ts +++ b/backend_new/src/dtos/users/user.dto.ts @@ -1,6 +1,8 @@ import { ApiProperty } from '@nestjs/swagger'; import { Expose, Type } from 'class-transformer'; import { + ArrayMinSize, + IsArray, IsBoolean, IsDate, IsEmail, @@ -9,6 +11,7 @@ import { IsPhoneNumber, IsString, MaxLength, + ValidateNested, } from 'class-validator'; import { EnforceLowerCase } from '../../decorators/enforce-lower-case.decorator'; import { ValidationsGroupsEnum } from '../../enums/shared/validation-groups-enum'; @@ -71,8 +74,8 @@ export class User extends AbstractDTO { @Expose() @Type(() => IdDTO) - @ApiProperty({ type: IdDTO, isArray: true }) - listings: IdDTO[]; + @ApiProperty({ type: IdDTO, isArray: true, nullable: true }) + listings?: IdDTO[]; @Expose() @Type(() => UserRole) @@ -90,6 +93,9 @@ export class User extends AbstractDTO { @Expose() @Type(() => IdDTO) + @IsArray({ groups: [ValidationsGroupsEnum.default] }) + @ArrayMinSize(1, { groups: [ValidationsGroupsEnum.default] }) + @ValidateNested({ groups: [ValidationsGroupsEnum.default], each: true }) @ApiProperty({ type: IdDTO, isArray: true }) jurisdictions: IdDTO[]; diff --git a/backend_new/src/services/user.service.ts b/backend_new/src/services/user.service.ts index b41576f97d..48926229c9 100644 --- a/backend_new/src/services/user.service.ts +++ b/backend_new/src/services/user.service.ts @@ -1,4 +1,14 @@ -import { Injectable, NotFoundException } from '@nestjs/common'; +import { + BadRequestException, + Injectable, + NotFoundException, + UnauthorizedException, +} from '@nestjs/common'; +import { Prisma } from '@prisma/client'; +import dayjs from 'dayjs'; +import advancedFormat from 'dayjs/plugin/advancedFormat'; +import crypto from 'crypto'; +import { verify, sign } from 'jsonwebtoken'; import { PrismaService } from './prisma.service'; import { User } from '../dtos/users/user.dto'; import { mapTo } from '../utilities/mapTo'; @@ -8,10 +18,17 @@ import { calculateTake, } from '../utilities/pagination-helpers'; import { buildOrderBy } from '../utilities/build-order-by'; -import { Prisma } from '@prisma/client'; import { UserQueryParams } from '../dtos/users/user-query-param.dto'; import { PaginatedUserDto } from '../dtos/users/paginated-user.dto'; import { OrderByEnum } from '../enums/shared/order-by-enum'; +import { UserUpdate } from '../dtos/users/user-update.dto'; +import { isPasswordValid, passwordToHash } from '../utilities/password-helpers'; +import { SuccessDTO } from '../dtos/shared/success.dto'; +import { EmailAndAppUrl } from '../dtos/users/email-and-app-url.dto'; +import { ConfirmationRequest } from '../dtos/users/confirmation-request.dto'; +import { IdDTO } from '../dtos/shared/id.dto'; +import { UserInvite } from '../dtos/users/user-invite.dto'; +import { UserCreate } from '../dtos/users/user-create.dto'; /* this is the service for users @@ -24,12 +41,21 @@ const view: Prisma.UserAccountsInclude = { userRoles: true, }; +type findByOptions = { + userId?: string; + email?: string; +}; + @Injectable() export class UserService { - constructor(private prisma: PrismaService) {} + constructor(private prisma: PrismaService) { + dayjs.extend(advancedFormat); + } /* this will get a set of users given the params passed in + Only users with a user role of admin or jurisdictional admin can get the list of available users. + This means we don't need to account for a user with only the partner role when it comes to accessing this function */ async list(params: UserQueryParams, user: User): Promise { const whereClause = this.buildWhereClause(params, user); @@ -215,21 +241,519 @@ export class UserService { this will return 1 user or error */ async findOne(userId: string): Promise { - const rawUser = await this.prisma.userAccounts.findFirst({ + const rawUser = await this.findUserOrError({ userId: userId }, true); + return mapTo(User, rawUser); + } + + /* + this will update a user or error if no user is found with the Id + */ + async update(dto: UserUpdate): Promise { + const storedUser = await this.findUserOrError({ userId: dto.id }, false); + + /* + TODO: perm check + */ + + let passwordHash: string; + let passwordUpdatedAt: Date; + if (dto.password) { + if (!dto.currentPassword) { + throw new BadRequestException( + `userID ${dto.id}: request missing currentPassword`, + ); + } + if ( + !(await isPasswordValid(storedUser.passwordHash, dto.currentPassword)) + ) { + throw new UnauthorizedException( + `userID ${dto.id}: incoming current password doesn't match stored password`, + ); + } + + passwordHash = await passwordToHash(dto.password); + passwordUpdatedAt = new Date(); + delete dto.password; + } + + let confirmationToken: string; + if (dto.newEmail && dto.appUrl) { + confirmationToken = this.createConfirmationToken( + storedUser.id, + dto.newEmail, + ); + // TODO: should we be resetting confirmedAt ? + const confirmationUrl = this.getPublicConfirmationUrl( + dto.appUrl, + confirmationToken, + ); + // TODO: email service (https://github.com/bloom-housing/bloom/issues/3503) + } + + const res = this.prisma.userAccounts.update({ include: view, + data: { + passwordHash: passwordHash ?? undefined, + passwordUpdatedAt: passwordUpdatedAt ?? undefined, + confirmationToken: confirmationToken ?? undefined, + firstName: dto.firstName, + middleName: dto.middleName, + lastName: dto.lastName, + dob: dto.dob, + phoneNumber: dto.phoneNumber, + language: dto.language, + listings: dto.listings + ? { + connect: dto.listings.map((listing) => ({ id: listing.id })), + } + : undefined, + jurisdictions: dto.jurisdictions + ? { + connect: dto.jurisdictions.map((jurisdiction) => ({ + id: jurisdiction.id, + })), + } + : undefined, + userRoles: dto.userRoles + ? { + create: { + ...dto.userRoles, + }, + } + : undefined, + }, + where: { + id: dto.id, + }, + }); + + return mapTo(User, res); + } + + /* + this will delete a user or error if no user is found with the Id + */ + async delete(userId: string): Promise { + await this.findUserOrError({ userId: userId }, false); + + // TODO: perms + + await this.prisma.userRoles.delete({ + where: { + userId: userId, + }, + }); + + await this.prisma.userAccounts.delete({ + where: { + id: userId, + }, + }); + + return { + success: true, + } as SuccessDTO; + } + + /* + resends a confirmation email or errors if no user matches the incoming email + if forPublic is true then we resend a confirmation for a public site user + if forPublic is false then we resend a confirmation for a partner site user + */ + async resendConfirmation( + dto: EmailAndAppUrl, + forPublic: boolean, + ): Promise { + const storedUser = await this.findUserOrError({ email: dto.email }, false); + + if (!storedUser.confirmedAt) { + const confirmationToken = this.createConfirmationToken( + storedUser.id, + storedUser.email, + ); + await this.prisma.userAccounts.update({ + data: { + confirmationToken: confirmationToken, + }, + where: { + id: storedUser.id, + }, + }); + + const confirmationUrl = forPublic + ? this.getPublicConfirmationUrl(dto.appUrl, confirmationToken) + : this.getPartnersConfirmationUrl(dto.appUrl, confirmationToken); + // TODO: email service (https://github.com/bloom-housing/bloom/issues/3503) + } + + return { + success: true, + } as SuccessDTO; + } + + /* + sets a reset token so a user can recover their account if they forgot the password + */ + async forgotPassword(dto: EmailAndAppUrl): Promise { + const storedUser = await this.findUserOrError({ email: dto.email }, false); + + const payload = { + id: storedUser.id, + exp: Number.parseInt(dayjs().add(1, 'hour').format('X')), + }; + await this.prisma.userAccounts.update({ + data: { + resetToken: sign(payload, process.env.APP_SECRET), + }, where: { - id: { - equals: userId, + id: storedUser.id, + }, + }); + // TODO: email service (https://github.com/bloom-housing/bloom/issues/3503) + + return { + success: true, + } as SuccessDTO; + } + + /* + checks to see if the confirmation token is valid + sets the hitConfirmationUrl field on the user if the user exists + */ + async isUserConfirmationTokenValid( + dto: ConfirmationRequest, + ): Promise { + try { + const token = verify(dto.token, process.env.APP_SECRET) as IdDTO; + + const storedUser = await this.prisma.userAccounts.findUnique({ + where: { + id: token.id, }, + }); + + await this.setHitConfirmationUrl( + storedUser?.id, + storedUser?.confirmationToken, + dto.token, + ); + + return { + success: true, + } as SuccessDTO; + } catch (_) { + try { + const storedUser = await this.prisma.userAccounts.findFirst({ + where: { + confirmationToken: dto.token, + }, + }); + await this.setHitConfirmationUrl( + storedUser?.id, + storedUser?.confirmationToken, + dto.token, + ); + } catch (e) { + console.error('isUserConfirmationTokenValid error = ', e); + } + } + } + + /* + Updates the hitConfirmationUrl for the user + this is so we can tell if a user attempted to confirm their account + */ + async setHitConfirmationUrl( + userId: string, + confirmationToken: string, + token: string, + ): Promise { + if (!userId) { + throw new NotFoundException( + `user confirmation token ${token} was requested but not found`, + ); + } + if (confirmationToken !== token) { + throw new BadRequestException('tokenMissing'); + } + await this.prisma.userAccounts.update({ + data: { + hitConfirmationUrl: new Date(), + }, + where: { + id: userId, }, }); + } + + /* + creates a new user + takes in either the dto for creating a public user or the dto for creating a partner user + if forPartners is true then we are creating a partner, otherwise we are creating a public user + if sendWelcomeEmail is true then we are sending a public user a welcome email + */ + async create( + dto: UserCreate | UserInvite, + forPartners: boolean, + sendWelcomeEmail = false, + ): Promise { + // TODO: perms + + const existingUser = await this.prisma.userAccounts.findUnique({ + include: view, + where: { + email: dto.email, + }, + }); + + if (existingUser) { + // if attempting to recreate an existing user + if (!existingUser.userRoles && 'userRoles' in dto) { + // existing user && public user && user will get roles -> trying to grant partner access to a public user + const res = await this.prisma.userAccounts.update({ + include: view, + data: { + userRoles: { + create: { + ...dto.userRoles, + }, + }, + listings: { + connect: dto.listings.map((listing) => ({ id: listing.id })), + }, + confirmationToken: + existingUser.confirmationToken || + this.createConfirmationToken(existingUser.id, existingUser.email), + confirmedAt: null, + }, + where: { + id: existingUser.id, + }, + }); + return mapTo(User, res); + } else if ( + existingUser?.userRoles?.isPartner && + 'userRoles' in dto && + dto?.userRoles?.isPartner && + this.jurisdictionMismatch(dto.jurisdictions, existingUser.jurisdictions) + ) { + // recreating a partner with jurisdiction mismatch -> giving partner a new jurisdiction + const jursidictions = existingUser.jurisdictions + .map((juris) => ({ id: juris.id })) + .concat(dto.jurisdictions); + + const listings = existingUser.listings + .map((juris) => ({ id: juris.id })) + .concat(dto.listings); + + const res = this.prisma.userAccounts.update({ + include: view, + data: { + jurisdictions: { + connect: jursidictions.map((juris) => ({ id: juris.id })), + }, + listings: { + connect: listings.map((listing) => ({ id: listing.id })), + }, + userRoles: { + create: { + ...dto.userRoles, + }, + }, + }, + where: { + id: existingUser.id, + }, + }); + + return mapTo(User, res); + } else { + // existing user && ((partner user -> trying to recreate user) || (public user -> trying to recreate a public user)) + throw new BadRequestException('emailInUse'); + } + } + + let passwordHash = ''; + if (forPartners) { + passwordHash = await passwordToHash( + crypto.randomBytes(8).toString('hex'), + ); + } else if (dto instanceof UserCreate) { + passwordHash = await passwordToHash(dto.password); + } + + let newUser = await this.prisma.userAccounts.create({ + data: { + passwordHash: passwordHash, + email: dto.email, + firstName: dto.firstName, + middleName: dto.middleName, + lastName: dto.lastName, + dob: dto.dob, + phoneNumber: dto.phoneNumber, + language: dto.language, + mfaEnabled: forPartners, + jurisdictions: { + connect: dto.jurisdictions.map((juris) => ({ + id: juris.id, + })), + }, + userRoles: + 'userRoles' in dto + ? { + create: { + ...dto.userRoles, + }, + } + : undefined, + listings: dto.listings + ? { + connect: dto.listings.map((listing) => ({ + id: listing.id, + })), + } + : undefined, + }, + }); + + const confirmationToken = this.createConfirmationToken( + newUser.id, + newUser.email, + ); + newUser = await this.prisma.userAccounts.update({ + include: view, + data: { + confirmationToken: confirmationToken, + }, + where: { + id: newUser.id, + }, + }); + + if (!forPartners && sendWelcomeEmail) { + const confirmationUrl = this.getPublicConfirmationUrl( + dto.appUrl, + confirmationToken, + ); + // TODO: email service (https://github.com/bloom-housing/bloom/issues/3503) + } else if ( + forPartners && + existingUser && + 'userRoles' in dto && + existingUser?.userRoles?.isPartner && + dto?.userRoles?.isPartner && + this.jurisdictionMismatch(dto.jurisdictions, existingUser.jurisdictions) + ) { + // TODO: email service (https://github.com/bloom-housing/bloom/issues/3503) + } else if (forPartners) { + // TODO: email service (https://github.com/bloom-housing/bloom/issues/3503) + } + + if (!forPartners) { + await this.connectUserWithExistingApplications(newUser.email, newUser.id); + } + + return mapTo(User, newUser); + } + + /* + connects a newly created public user with any applications they may have already submitted + */ + async connectUserWithExistingApplications( + newUserEmail: string, + newUserId: string, + ): Promise { + const applications = await this.prisma.applications.findMany({ + where: { + applicant: { + emailAddress: newUserEmail, + }, + userAccounts: null, + }, + }); + + for (const app of applications) { + await this.prisma.applications.update({ + data: { + userAccounts: { + connect: { + id: newUserId, + }, + }, + }, + where: { + id: app.id, + }, + }); + } + } + + /* + this will return 1 user or error + takes in a userId or email to find by, and a boolean to indicate if joins should be included + */ + async findUserOrError(findBy: findByOptions, includeJoins: boolean) { + const where: Prisma.UserAccountsWhereUniqueInput = { + id: undefined, + email: undefined, + }; + if (findBy.userId) { + where.id = findBy.userId; + } else if (findBy.email) { + where.email = findBy.email; + } + const rawUser = await this.prisma.userAccounts.findUnique({ + include: includeJoins ? view : undefined, + where, + }); if (!rawUser) { throw new NotFoundException( - `userId ${userId} was requested but not found`, + `user ${findBy.userId || findBy.email} was requested but not found`, ); } - return mapTo(User, rawUser); + return rawUser; + } + + /* + encodes a confirmation token given a userId and email + */ + createConfirmationToken(userId: string, email: string) { + const payload = { + id: userId, + email, + exp: Number.parseInt(dayjs().add(24, 'hours').format('X')), + }; + return sign(payload, process.env.APP_SECRET); + } + + /* + constructs the url to confirm a public site user + */ + getPublicConfirmationUrl(appUrl: string, confirmationToken: string) { + return `${appUrl}?token=${confirmationToken}`; + } + + /* + constructs the url to confirm the partner site user + */ + getPartnersConfirmationUrl(appUrl: string, confirmationToken: string) { + return `${appUrl}/users/confirm?token=${confirmationToken}`; + } + + /* + verify that there is a jurisdictional difference between the incoming user and the existing user + */ + jurisdictionMismatch( + incomingJurisdictions: IdDTO[], + existingJurisdictions: IdDTO[], + ): boolean { + return incomingJurisdictions?.some( + (incomingJuris) => + !existingJurisdictions?.some( + (existingJuris) => existingJuris.id === incomingJuris.id, + ), + ); } } diff --git a/backend_new/src/utilities/password-helpers.ts b/backend_new/src/utilities/password-helpers.ts new file mode 100644 index 0000000000..9e92ac7e22 --- /dev/null +++ b/backend_new/src/utilities/password-helpers.ts @@ -0,0 +1,37 @@ +import { randomBytes, scrypt } from 'crypto'; +const SCRYPT_KEYLEN = 64; +const SALT_SIZE = SCRYPT_KEYLEN; + +export const isPasswordValid = async ( + storedPasswordHash: string, + incomingPassword: string, +): Promise => { + const [salt, savedPasswordHash] = storedPasswordHash.split('#'); + const verifyPasswordHash = await hashPassword( + incomingPassword, + Buffer.from(salt, 'hex'), + ); + return savedPasswordHash === verifyPasswordHash; +}; + +export const hashPassword = async ( + password: string, + salt: Buffer, +): Promise => { + return new Promise((resolve, reject) => + scrypt(password, salt, SCRYPT_KEYLEN, (err, key) => + err ? reject(err) : resolve(key.toString('hex')), + ), + ); +}; + +export const passwordToHash = async (password: string): Promise => { + const salt = generateSalt(); + const hash = await hashPassword(password, salt); + // TODO: redo how we append the salt to the hash + return `${salt.toString('hex')}#${hash}`; +}; + +export const generateSalt = (size = SALT_SIZE) => { + return randomBytes(size); +}; diff --git a/backend_new/src/utilities/password-regex.ts b/backend_new/src/utilities/password-regex.ts new file mode 100644 index 0000000000..f7579d1961 --- /dev/null +++ b/backend_new/src/utilities/password-regex.ts @@ -0,0 +1 @@ +export const passwordRegex = /^(?=.*[0-9])(?=.*[a-zA-Z])([a-zA-Z0-9]+).{7,}$/; diff --git a/backend_new/test/integration/ami-chart.e2e-spec.ts b/backend_new/test/integration/ami-chart.e2e-spec.ts index c61a8600d6..3ed3683b9e 100644 --- a/backend_new/test/integration/ami-chart.e2e-spec.ts +++ b/backend_new/test/integration/ami-chart.e2e-spec.ts @@ -1,6 +1,6 @@ import { Test, TestingModule } from '@nestjs/testing'; import { INestApplication } from '@nestjs/common'; -import * as request from 'supertest'; +import request from 'supertest'; import { AppModule } from '../../src/modules/app.module'; import { PrismaService } from '../../src/services/prisma.service'; import { jurisdictionFactory } from '../../prisma/seed-helpers/jurisdiction-factory'; diff --git a/backend_new/test/integration/app.e2e-spec.ts b/backend_new/test/integration/app.e2e-spec.ts index 4f50567336..67b5943f38 100644 --- a/backend_new/test/integration/app.e2e-spec.ts +++ b/backend_new/test/integration/app.e2e-spec.ts @@ -1,6 +1,6 @@ import { Test, TestingModule } from '@nestjs/testing'; import { INestApplication } from '@nestjs/common'; -import * as request from 'supertest'; +import request from 'supertest'; import { AppModule } from '../../src/modules/app.module'; import { PrismaService } from '../../src/services/prisma.service'; diff --git a/backend_new/test/integration/application.e2e-spec.ts b/backend_new/test/integration/application.e2e-spec.ts index c7024132a0..8c95db7366 100644 --- a/backend_new/test/integration/application.e2e-spec.ts +++ b/backend_new/test/integration/application.e2e-spec.ts @@ -1,6 +1,6 @@ import { Test, TestingModule } from '@nestjs/testing'; import { INestApplication } from '@nestjs/common'; -import * as request from 'supertest'; +import request from 'supertest'; import { AppModule } from '../../src/modules/app.module'; import { PrismaService } from '../../src/services/prisma.service'; import { applicationFactory } from '../../prisma/seed-helpers/application-factory'; @@ -41,6 +41,7 @@ describe('Application Controller Tests', () => { page: 1, order: OrderByEnum.ASC, orderBy: ApplicationOrderByKeys.createdAt, + listingId: randomUUID(), }; const query = stringify(queryParams as any); @@ -50,7 +51,8 @@ describe('Application Controller Tests', () => { expect(res.body.items.length).toBe(0); }); - it('should get no applications when no params are sent, and no applications are stored', async () => { + // without clearing the db between tests or test suites this is flakes because of other e2e tests + it.skip('should get no applications when no params are sent, and no applications are stored', async () => { const res = await request(app.getHttpServer()) .get(`/applications`) .expect(200); diff --git a/backend_new/test/integration/asset.e2e-spec.ts b/backend_new/test/integration/asset.e2e-spec.ts index ca9c6c1862..8b0304daff 100644 --- a/backend_new/test/integration/asset.e2e-spec.ts +++ b/backend_new/test/integration/asset.e2e-spec.ts @@ -1,6 +1,6 @@ import { Test, TestingModule } from '@nestjs/testing'; import { INestApplication } from '@nestjs/common'; -import * as request from 'supertest'; +import request from 'supertest'; import { AppModule } from '../../src//modules/app.module'; import { randomUUID } from 'crypto'; import { PrismaService } from '../../src/services/prisma.service'; diff --git a/backend_new/test/integration/jurisdiction.e2e-spec.ts b/backend_new/test/integration/jurisdiction.e2e-spec.ts index c09459068b..c4b6ec9f66 100644 --- a/backend_new/test/integration/jurisdiction.e2e-spec.ts +++ b/backend_new/test/integration/jurisdiction.e2e-spec.ts @@ -1,6 +1,6 @@ import { Test, TestingModule } from '@nestjs/testing'; import { INestApplication } from '@nestjs/common'; -import * as request from 'supertest'; +import request from 'supertest'; import { AppModule } from '../../src/modules/app.module'; import { PrismaService } from '../../src/services/prisma.service'; import { jurisdictionFactory } from '../../prisma/seed-helpers/jurisdiction-factory'; diff --git a/backend_new/test/integration/listing.e2e-spec.ts b/backend_new/test/integration/listing.e2e-spec.ts index 2e6bb5d6ca..c2dd56d89f 100644 --- a/backend_new/test/integration/listing.e2e-spec.ts +++ b/backend_new/test/integration/listing.e2e-spec.ts @@ -1,6 +1,6 @@ import { Test, TestingModule } from '@nestjs/testing'; import { INestApplication } from '@nestjs/common'; -import * as request from 'supertest'; +import request from 'supertest'; import { AppModule } from '../../src/modules/app.module'; import { PrismaService } from '../../src/services/prisma.service'; import { jurisdictionFactory } from '../../prisma/seed-helpers/jurisdiction-factory'; diff --git a/backend_new/test/integration/multiselect-question.e2e-spec.ts b/backend_new/test/integration/multiselect-question.e2e-spec.ts index 82eec93508..de7b59583e 100644 --- a/backend_new/test/integration/multiselect-question.e2e-spec.ts +++ b/backend_new/test/integration/multiselect-question.e2e-spec.ts @@ -1,6 +1,6 @@ import { Test, TestingModule } from '@nestjs/testing'; import { INestApplication } from '@nestjs/common'; -import * as request from 'supertest'; +import request from 'supertest'; import { AppModule } from '../../src/modules/app.module'; import { PrismaService } from '../../src/services/prisma.service'; import { multiselectQuestionFactory } from '../../prisma/seed-helpers/multiselect-question-factory'; diff --git a/backend_new/test/integration/reserved-community-type.e2e-spec.ts b/backend_new/test/integration/reserved-community-type.e2e-spec.ts index e925d4b3c4..1acf606a87 100644 --- a/backend_new/test/integration/reserved-community-type.e2e-spec.ts +++ b/backend_new/test/integration/reserved-community-type.e2e-spec.ts @@ -1,6 +1,6 @@ import { Test, TestingModule } from '@nestjs/testing'; import { INestApplication } from '@nestjs/common'; -import * as request from 'supertest'; +import request from 'supertest'; import { AppModule } from '../../src/modules/app.module'; import { PrismaService } from '../../src/services/prisma.service'; import { jurisdictionFactory } from '../../prisma/seed-helpers/jurisdiction-factory'; diff --git a/backend_new/test/integration/unit-accessibility-priority-type.e2e-spec.ts b/backend_new/test/integration/unit-accessibility-priority-type.e2e-spec.ts index 84ef203c97..a5ff100921 100644 --- a/backend_new/test/integration/unit-accessibility-priority-type.e2e-spec.ts +++ b/backend_new/test/integration/unit-accessibility-priority-type.e2e-spec.ts @@ -1,6 +1,6 @@ import { Test, TestingModule } from '@nestjs/testing'; import { INestApplication } from '@nestjs/common'; -import * as request from 'supertest'; +import request from 'supertest'; import { AppModule } from '../../src/modules/app.module'; import { PrismaService } from '../../src/services/prisma.service'; import { unitAccessibilityPriorityTypeFactorySingle } from '../../prisma/seed-helpers/unit-accessibility-priority-type-factory'; diff --git a/backend_new/test/integration/unit-rent-type.e2e-spec.ts b/backend_new/test/integration/unit-rent-type.e2e-spec.ts index ac66151d79..66b3452308 100644 --- a/backend_new/test/integration/unit-rent-type.e2e-spec.ts +++ b/backend_new/test/integration/unit-rent-type.e2e-spec.ts @@ -1,6 +1,6 @@ import { Test, TestingModule } from '@nestjs/testing'; import { INestApplication } from '@nestjs/common'; -import * as request from 'supertest'; +import request from 'supertest'; import { AppModule } from '../../src/modules/app.module'; import { PrismaService } from '../../src/services/prisma.service'; import { unitRentTypeFactory } from '../../prisma/seed-helpers/unit-rent-type-factory'; diff --git a/backend_new/test/integration/unit-type.e2e-spec.ts b/backend_new/test/integration/unit-type.e2e-spec.ts index d7a5fb442c..ff76465207 100644 --- a/backend_new/test/integration/unit-type.e2e-spec.ts +++ b/backend_new/test/integration/unit-type.e2e-spec.ts @@ -1,6 +1,6 @@ import { Test, TestingModule } from '@nestjs/testing'; import { INestApplication } from '@nestjs/common'; -import * as request from 'supertest'; +import request from 'supertest'; import { AppModule } from '../../src/modules/app.module'; import { PrismaService } from '../../src/services/prisma.service'; import { diff --git a/backend_new/test/integration/user.e2e-spec.ts b/backend_new/test/integration/user.e2e-spec.ts index 08388e9e49..3ac3a3b690 100644 --- a/backend_new/test/integration/user.e2e-spec.ts +++ b/backend_new/test/integration/user.e2e-spec.ts @@ -1,16 +1,26 @@ import { Test, TestingModule } from '@nestjs/testing'; import { INestApplication } from '@nestjs/common'; -import * as request from 'supertest'; +import request from 'supertest'; import { AppModule } from '../../src/modules/app.module'; import { PrismaService } from '../../src/services/prisma.service'; import { userFactory } from '../../prisma/seed-helpers/user-factory'; import { randomUUID } from 'crypto'; import { stringify } from 'qs'; import { UserQueryParams } from '../../src/dtos/users/user-query-param.dto'; +import { UserUpdate } from '../../src/dtos/users/user-update.dto'; +import { IdDTO } from '../../src/dtos/shared/id.dto'; +import { EmailAndAppUrl } from '../../src/dtos/users/email-and-app-url.dto'; +import { ConfirmationRequest } from '../../src/dtos/users/confirmation-request.dto'; +import { UserService } from '../../src/services/user.service'; +import { UserCreate } from '../../src/dtos/users/user-create.dto'; +import { jurisdictionFactory } from '../../prisma/seed-helpers/jurisdiction-factory'; +import { applicationFactory } from '../../prisma/seed-helpers/application-factory'; +import { UserInvite } from '../../src/dtos/users/user-invite.dto'; describe('User Controller Tests', () => { let app: INestApplication; let prisma: PrismaService; + let userService: UserService; beforeAll(async () => { const moduleFixture: TestingModule = await Test.createTestingModule({ @@ -19,6 +29,7 @@ describe('User Controller Tests', () => { app = moduleFixture.createNestApplication(); prisma = moduleFixture.get(PrismaService); + userService = moduleFixture.get(UserService); await app.init(); }); @@ -27,7 +38,8 @@ describe('User Controller Tests', () => { await app.close(); }); - it('should get no users from list() when no params and no data', async () => { + // without clearing the db between tests or test suites this is flakes because of other e2e tests + it.skip('should get no users from list() when no params and no data', async () => { const res = await request(app.getHttpServer()) .get(`/user/list?`) .expect(200); @@ -36,10 +48,10 @@ describe('User Controller Tests', () => { it('should get users from list() when no params', async () => { const userA = await prisma.userAccounts.create({ - data: userFactory(), + data: await userFactory(), }); const userB = await prisma.userAccounts.create({ - data: userFactory(), + data: await userFactory(), }); const res = await request(app.getHttpServer()) @@ -53,10 +65,16 @@ describe('User Controller Tests', () => { it('should get users from list() when params sent', async () => { const userA = await prisma.userAccounts.create({ - data: userFactory({ roles: { isPartner: true }, firstName: '1110' }), + data: await userFactory({ + roles: { isPartner: true }, + firstName: '1110', + }), }); const userB = await prisma.userAccounts.create({ - data: userFactory({ roles: { isPartner: true }, firstName: '1111' }), + data: await userFactory({ + roles: { isPartner: true }, + firstName: '1111', + }), }); const queryParams: UserQueryParams = { @@ -85,14 +103,12 @@ describe('User Controller Tests', () => { const res = await request(app.getHttpServer()) .get(`/user/${id}`) .expect(404); - expect(res.body.message).toEqual( - `userId ${id} was requested but not found`, - ); + expect(res.body.message).toEqual(`user ${id} was requested but not found`); }); it('should get user from retrieve()', async () => { const userA = await prisma.userAccounts.create({ - data: userFactory(), + data: await userFactory(), }); const res = await request(app.getHttpServer()) @@ -101,4 +117,416 @@ describe('User Controller Tests', () => { expect(res.body.id).toEqual(userA.id); }); + + it('should update user when user exists', async () => { + const userA = await prisma.userAccounts.create({ + data: await userFactory(), + }); + + const res = await request(app.getHttpServer()) + .put(`/user/${userA.id}`) + .send({ + id: userA.id, + firstName: 'New User First Name', + lastName: 'New User Last Name', + } as UserUpdate) + .expect(200); + + expect(res.body.id).toEqual(userA.id); + expect(res.body.firstName).toEqual('New User First Name'); + expect(res.body.lastName).toEqual('New User Last Name'); + }); + + it("should error when updating user that doesn't exist", async () => { + await prisma.userAccounts.create({ + data: await userFactory(), + }); + const randomId = randomUUID(); + const res = await request(app.getHttpServer()) + .put(`/user/${randomId}`) + .send({ + id: randomId, + firstName: 'New User First Name', + lastName: 'New User Last Name', + } as UserUpdate) + .expect(404); + + expect(res.body.message).toEqual( + `user ${randomId} was requested but not found`, + ); + }); + + it('should delete user when user exists', async () => { + const userA = await prisma.userAccounts.create({ + data: await userFactory(), + }); + + const res = await request(app.getHttpServer()) + .delete(`/user/`) + .send({ + id: userA.id, + } as IdDTO) + .expect(200); + + expect(res.body.success).toEqual(true); + }); + + it("should error when deleting user that doesn't exist", async () => { + const randomId = randomUUID(); + const res = await request(app.getHttpServer()) + .delete(`/user/`) + .send({ + id: randomId, + } as IdDTO) + .expect(404); + + expect(res.body.message).toEqual( + `user ${randomId} was requested but not found`, + ); + }); + + it('should resend confirmation when user exists', async () => { + const userA = await prisma.userAccounts.create({ + data: await userFactory(), + }); + + const res = await request(app.getHttpServer()) + .post(`/user/resend-confirmation/`) + .send({ + email: userA.email, + appUrl: 'https://www.google.com', + } as EmailAndAppUrl) + .expect(201); + + expect(res.body.success).toEqual(true); + + const userPostResend = await prisma.userAccounts.findUnique({ + where: { + id: userA.id, + }, + }); + + expect(userPostResend.email).toBe(userA.email); + expect(userPostResend.confirmationToken).not.toBeNull(); + }); + + it('should succeed when trying to resend confirmation but not update record when user is already confirmed', async () => { + const userA = await prisma.userAccounts.create({ + data: { + ...(await userFactory()), + confirmedAt: new Date(), + }, + }); + + const res = await request(app.getHttpServer()) + .post(`/user/resend-confirmation/`) + .send({ + email: userA.email, + appUrl: 'https://www.google.com', + } as EmailAndAppUrl) + .expect(201); + + expect(res.body.success).toEqual(true); + + const userPostResend = await prisma.userAccounts.findUnique({ + where: { + id: userA.id, + }, + }); + + expect(userPostResend.email).toBe(userA.email); + expect(userPostResend.confirmationToken).toBeNull(); + }); + + it('should error trying to resend confirmation but no user exists', async () => { + const email = 'test@nonexistent.com'; + const res = await request(app.getHttpServer()) + .post(`/user/resend-confirmation/`) + .send({ + email: email, + appUrl: 'https://www.google.com', + } as EmailAndAppUrl) + .expect(404); + + expect(res.body.message).toEqual( + `user ${email} was requested but not found`, + ); + }); + + it('should resend partner confirmation when user exists', async () => { + const userA = await prisma.userAccounts.create({ + data: await userFactory(), + }); + + const res = await request(app.getHttpServer()) + .post(`/user/resend-partner-confirmation/`) + .send({ + email: userA.email, + appUrl: 'https://www.google.com', + } as EmailAndAppUrl) + .expect(201); + + expect(res.body.success).toEqual(true); + + const userPostResend = await prisma.userAccounts.findUnique({ + where: { + id: userA.id, + }, + }); + + expect(userPostResend.email).toBe(userA.email); + expect(userPostResend.confirmationToken).not.toBeNull(); + }); + + it('should succeed when trying to resend partner confirmation but not update record when user is already confirmed', async () => { + const userA = await prisma.userAccounts.create({ + data: { + ...(await userFactory()), + confirmedAt: new Date(), + }, + }); + + const res = await request(app.getHttpServer()) + .post(`/user/resend-partner-confirmation/`) + .send({ + email: userA.email, + appUrl: 'https://www.google.com', + } as EmailAndAppUrl) + .expect(201); + + expect(res.body.success).toEqual(true); + + const userPostResend = await prisma.userAccounts.findUnique({ + where: { + id: userA.id, + }, + }); + + expect(userPostResend.email).toBe(userA.email); + expect(userPostResend.confirmationToken).toBeNull(); + }); + + it('should error trying to resend partner confirmation but no user exists', async () => { + const email = 'test@nonexistent.com'; + const res = await request(app.getHttpServer()) + .post(`/user/resend-partner-confirmation/`) + .send({ + email: email, + appUrl: 'https://www.google.com', + } as EmailAndAppUrl) + .expect(404); + + expect(res.body.message).toEqual( + `user ${email} was requested but not found`, + ); + }); + + it('should verify token as valid', async () => { + const userA = await prisma.userAccounts.create({ + data: await userFactory(), + }); + + const confToken = await userService.createConfirmationToken( + userA.id, + userA.email, + ); + await prisma.userAccounts.update({ + where: { + id: userA.id, + }, + data: { + confirmationToken: confToken, + confirmedAt: null, + }, + }); + const res = await request(app.getHttpServer()) + .post(`/user/is-confirmation-token-valid/`) + .send({ + token: confToken, + } as ConfirmationRequest) + .expect(201); + + expect(res.body.success).toEqual(true); + + const userPostResend = await prisma.userAccounts.findUnique({ + where: { + id: userA.id, + }, + }); + + expect(userPostResend.hitConfirmationUrl).not.toBeNull(); + expect(userPostResend.confirmationToken).toEqual(confToken); + }); + + it('should fail to verify token when incorrect user id is provided', async () => { + const userA = await prisma.userAccounts.create({ + data: await userFactory(), + }); + + const storedConfToken = await userService.createConfirmationToken( + userA.id, + userA.email, + ); + await prisma.userAccounts.update({ + where: { + id: userA.id, + }, + data: { + confirmationToken: storedConfToken, + confirmedAt: null, + }, + }); + + const fakeConfToken = await userService.createConfirmationToken( + randomUUID(), + userA.email, + ); + const res = await request(app.getHttpServer()) + .post(`/user/is-confirmation-token-valid/`) + .send({ + token: fakeConfToken, + } as ConfirmationRequest) + .expect(201); + + expect(res.body.success).toBe(undefined); + + const userPostResend = await prisma.userAccounts.findUnique({ + where: { + id: userA.id, + }, + }); + + expect(userPostResend.hitConfirmationUrl).toBeNull(); + }); + + it('should fail to verify token when token mismatch', async () => { + const userA = await prisma.userAccounts.create({ + data: await userFactory(), + }); + + const storedConfToken = await userService.createConfirmationToken( + userA.id, + userA.email, + ); + await prisma.userAccounts.update({ + where: { + id: userA.id, + }, + data: { + confirmationToken: storedConfToken, + confirmedAt: null, + }, + }); + + const fakeConfToken = await userService.createConfirmationToken( + userA.id, + userA.email + 'x', + ); + const res = await request(app.getHttpServer()) + .post(`/user/is-confirmation-token-valid/`) + .send({ + token: fakeConfToken, + } as ConfirmationRequest) + .expect(201); + + expect(res.body.success).toBe(undefined); + + const userPostResend = await prisma.userAccounts.findUnique({ + where: { + id: userA.id, + }, + }); + + expect(userPostResend.hitConfirmationUrl).toBeNull(); + }); + + it('should set resetToken when forgot-password is called', async () => { + const userA = await prisma.userAccounts.create({ + data: await userFactory(), + }); + + const res = await request(app.getHttpServer()) + .put(`/user/forgot-password/`) + .send({ + email: userA.email, + } as EmailAndAppUrl) + .expect(200); + + expect(res.body.success).toBe(true); + + const userPostResend = await prisma.userAccounts.findUnique({ + where: { + id: userA.id, + }, + }); + + expect(userPostResend.resetToken).not.toBeNull(); + }); + + it('should create public user', async () => { + const juris = await prisma.jurisdictions.create({ + data: jurisdictionFactory(), + }); + + const data = applicationFactory(); + data.applicant.create.emailAddress = 'publicuser@email.com'; + const application = await prisma.applications.create({ + data, + }); + + const res = await request(app.getHttpServer()) + .post(`/user/`) + .send({ + firstName: 'Public User firstName', + lastName: 'Public User lastName', + password: 'example password 1', + email: 'publicUser@email.com', + jurisdictions: [{ id: juris.id }], + } as UserCreate) + .expect(201); + + expect(res.body.firstName).toEqual('Public User firstName'); + expect(res.body.jurisdictions).toEqual([ + { id: juris.id, name: juris.name }, + ]); + expect(res.body.email).toEqual('publicuser@email.com'); + + const applicationsOnUser = await prisma.userAccounts.findUnique({ + include: { + applications: true, + }, + where: { + id: res.body.id, + }, + }); + expect(applicationsOnUser.applications.map((app) => app.id)).toContain( + application.id, + ); + }); + + it('should create parter user', async () => { + const juris = await prisma.jurisdictions.create({ + data: jurisdictionFactory(), + }); + + const res = await request(app.getHttpServer()) + .post(`/user/invite`) + .send({ + firstName: 'Partner User firstName', + lastName: 'Partner User lastName', + password: 'example password 1', + email: 'partnerUser@email.com', + jurisdictions: [{ id: juris.id }], + userRoles: { + isAdmin: true, + }, + } as UserInvite) + .expect(201); + + expect(res.body.firstName).toEqual('Partner User firstName'); + expect(res.body.jurisdictions).toEqual([ + { id: juris.id, name: juris.name }, + ]); + expect(res.body.email).toEqual('partneruser@email.com'); + }); }); diff --git a/backend_new/test/unit/services/user.service.spec.ts b/backend_new/test/unit/services/user.service.spec.ts index efed9d6f22..a9c694cdd1 100644 --- a/backend_new/test/unit/services/user.service.spec.ts +++ b/backend_new/test/unit/services/user.service.spec.ts @@ -3,6 +3,9 @@ import { PrismaService } from '../../../src/services/prisma.service'; import { UserService } from '../../../src/services/user.service'; import { randomUUID } from 'crypto'; import { LanguagesEnum } from '@prisma/client'; +import { verify } from 'jsonwebtoken'; +import { passwordToHash } from '../../../src/utilities/password-helpers'; +import { IdDTO } from '../../../src/dtos/shared/id.dto'; describe('Testing user service', () => { let service: UserService; @@ -170,41 +173,1017 @@ describe('Testing user service', () => { it('should return user from findOne() when id present', async () => { const date = new Date(); const mockedValue = mockUser(3, date); - prisma.userAccounts.findFirst = jest.fn().mockResolvedValue(mockedValue); + prisma.userAccounts.findUnique = jest.fn().mockResolvedValue(mockedValue); expect(await service.findOne('example Id')).toEqual(mockedValue); - expect(prisma.userAccounts.findFirst).toHaveBeenCalledWith({ + expect(prisma.userAccounts.findUnique).toHaveBeenCalledWith({ include: { listings: true, jurisdictions: true, userRoles: true, }, where: { - id: { - equals: 'example Id', - }, + id: 'example Id', }, }); }); it('should error when calling findOne() when id not present', async () => { - prisma.userAccounts.findFirst = jest.fn().mockResolvedValue(null); + prisma.userAccounts.findUnique = jest.fn().mockResolvedValue(null); await expect( async () => await service.findOne('example Id'), - ).rejects.toThrowError('userId example Id was requested but not found'); + ).rejects.toThrowError('user example Id was requested but not found'); + + expect(prisma.userAccounts.findUnique).toHaveBeenCalledWith({ + include: { + listings: true, + jurisdictions: true, + userRoles: true, + }, + where: { + id: 'example Id', + }, + }); + }); + + it('should encode a confirmation token correctly', () => { + const id = randomUUID(); + const res = service.createConfirmationToken(id, 'example@email.com'); + expect(res).not.toBeNull(); + const decoded = verify(res, process.env.APP_SECRET) as IdDTO; + expect(decoded.id).toEqual(id); + }); + + it('should build public confirmation url', () => { + const res = service.getPublicConfirmationUrl('url', 'tokenExample'); + expect(res).toEqual('url?token=tokenExample'); + }); + + it('should build partner confirmation url', () => { + const res = service.getPartnersConfirmationUrl('url', 'tokenExample'); + expect(res).toEqual('url/users/confirm?token=tokenExample'); + }); + + it('should verify that there is a jurisdiciton mismatch', () => { + const res = service.jurisdictionMismatch( + [{ id: 'id a' }], + [{ id: 'id 1' }], + ); + expect(res).toEqual(true); + }); + + it('should verify that there is not a jurisdiciton mismatch', () => { + const res = service.jurisdictionMismatch( + [{ id: 'id a' }, { id: 'id b' }], + [{ id: 'id b' }, { id: 'id a' }], + ); + expect(res).toEqual(false); + }); + + it('should find user by id and include joins', async () => { + const id = randomUUID(); + prisma.userAccounts.findUnique = jest.fn().mockResolvedValue({ + id, + }); + const res = await service.findUserOrError({ userId: id }, true); + expect(res).toEqual({ id }); + expect(prisma.userAccounts.findUnique).toHaveBeenCalledWith({ + include: { + jurisdictions: true, + listings: true, + userRoles: true, + }, + where: { + id, + }, + }); + }); + + it('should find user by email and include joins', async () => { + const email = 'example@email.com'; + prisma.userAccounts.findUnique = jest.fn().mockResolvedValue({ + email, + }); + const res = await service.findUserOrError({ email: email }, true); + expect(res).toEqual({ email }); + expect(prisma.userAccounts.findUnique).toHaveBeenCalledWith({ + include: { + jurisdictions: true, + listings: true, + userRoles: true, + }, + where: { + email, + }, + }); + }); + it('should find user by id and no joins', async () => { + const id = randomUUID(); + prisma.userAccounts.findUnique = jest.fn().mockResolvedValue({ + id, + }); + const res = await service.findUserOrError({ userId: id }, false); + expect(res).toEqual({ id }); + expect(prisma.userAccounts.findUnique).toHaveBeenCalledWith({ + where: { + id, + }, + }); + }); + + it('should find user by email and no joins', async () => { + const email = 'example@email.com'; + prisma.userAccounts.findUnique = jest.fn().mockResolvedValue({ + email, + }); + const res = await service.findUserOrError({ email: email }, false); + expect(res).toEqual({ email }); + expect(prisma.userAccounts.findUnique).toHaveBeenCalledWith({ + where: { + email, + }, + }); + }); + + it('should throw when could not find user', async () => { + const email = 'example@email.com'; + prisma.userAccounts.findUnique = jest.fn().mockResolvedValue(null); + await expect( + async () => await service.findUserOrError({ email: email }, false), + ).rejects.toThrowError( + 'user example@email.com was requested but not found', + ); + expect(prisma.userAccounts.findUnique).toHaveBeenCalledWith({ + where: { + email, + }, + }); + }); + + it('should connect user when matching applications exist', async () => { + const id = randomUUID(); + const email = 'example@email.com'; + prisma.applications.findMany = jest + .fn() + .mockReturnValue([{ id: 'app id 1' }, { id: 'app id 2' }]); + + prisma.applications.update = jest.fn().mockReturnValue(null); + await service.connectUserWithExistingApplications(email, id); + expect(prisma.applications.findMany).toHaveBeenCalledWith({ + where: { + applicant: { + emailAddress: email, + }, + userAccounts: null, + }, + }); + expect(prisma.applications.update).toHaveBeenNthCalledWith(1, { + data: { + userAccounts: { + connect: { + id, + }, + }, + }, + where: { + id: 'app id 1', + }, + }); + expect(prisma.applications.update).toHaveBeenNthCalledWith(2, { + data: { + userAccounts: { + connect: { + id, + }, + }, + }, + where: { + id: 'app id 2', + }, + }); + }); + + it('should not connect user when no matching applications exist', async () => { + const id = randomUUID(); + const email = 'example@email.com'; + prisma.applications.findMany = jest.fn().mockReturnValue([]); + + prisma.applications.update = jest.fn().mockReturnValue(null); + await service.connectUserWithExistingApplications(email, id); + expect(prisma.applications.findMany).toHaveBeenCalledWith({ + where: { + applicant: { + emailAddress: email, + }, + userAccounts: null, + }, + }); + expect(prisma.applications.update).not.toHaveBeenCalled(); + }); + + it('should set hitConfirmationUrl', async () => { + const id = randomUUID(); + prisma.userAccounts.update = jest.fn().mockResolvedValue({ + id, + hitConfirmationUrl: new Date(), + }); + await service.setHitConfirmationUrl( + id, + 'confirmation token', + 'confirmation token', + ); + expect(prisma.userAccounts.update).toHaveBeenCalledWith({ + data: { + hitConfirmationUrl: expect.anything(), + }, + where: { + id, + }, + }); + }); + + it('should throw the user missing error when trying to set hitConfirmationUrl', async () => { + const id = null; + await expect( + async () => + await service.setHitConfirmationUrl( + id, + 'confirmation token', + 'confirmation token', + ), + ).rejects.toThrowError( + 'user confirmation token confirmation token was requested but not found', + ); + }); + + it('should throw token mismatch error when trying to set hitConfirmationUrl', async () => { + const id = randomUUID(); + await expect( + async () => + await service.setHitConfirmationUrl( + id, + 'confirmation token', + 'confirmation token different', + ), + ).rejects.toThrowError('tokenMissing'); + }); + + it('should validate confirmationToken', async () => { + const id = randomUUID(); + const token = service.createConfirmationToken(id, 'email@example.com'); + prisma.userAccounts.findUnique = jest.fn().mockResolvedValue({ + id, + confirmationToken: token, + }); + prisma.userAccounts.update = jest.fn().mockResolvedValue({ + id, + hitConfirmationUrl: new Date(), + }); + await service.isUserConfirmationTokenValid({ token }); + expect(prisma.userAccounts.findUnique).toHaveBeenCalledWith({ + where: { + id, + }, + }); + expect(prisma.userAccounts.update).toHaveBeenCalledWith({ + data: { + hitConfirmationUrl: expect.anything(), + }, + where: { + id, + }, + }); + }); + + it('should mark hitConfirmationUrl even though user id was not a match', async () => { + const id = randomUUID(); + const token = service.createConfirmationToken(id, 'email@example.com'); + prisma.userAccounts.findUnique = jest.fn().mockResolvedValue(null); + prisma.userAccounts.findFirst = jest.fn().mockResolvedValue({ + id, + confirmationToken: token, + }); + prisma.userAccounts.update = jest.fn().mockResolvedValue({ + id, + hitConfirmationUrl: new Date(), + }); + await service.isUserConfirmationTokenValid({ token }); + expect(prisma.userAccounts.findUnique).toHaveBeenCalledWith({ + where: { + id, + }, + }); expect(prisma.userAccounts.findFirst).toHaveBeenCalledWith({ + where: { + confirmationToken: token, + }, + }); + expect(prisma.userAccounts.update).toHaveBeenCalledWith({ + data: { + hitConfirmationUrl: expect.anything(), + }, + where: { + id, + }, + }); + }); + + it('should silently fail when confirmation token is not valid', async () => { + const id = randomUUID(); + const token = service.createConfirmationToken(id, 'email@example.com'); + prisma.userAccounts.findUnique = jest.fn().mockResolvedValue(null); + prisma.userAccounts.findFirst = jest.fn().mockResolvedValue(null); + prisma.userAccounts.update = jest.fn().mockResolvedValue(null); + await service.isUserConfirmationTokenValid({ token }); + expect(prisma.userAccounts.findUnique).toHaveBeenCalledWith({ + where: { + id, + }, + }); + expect(prisma.userAccounts.findFirst).toHaveBeenCalledWith({ + where: { + confirmationToken: token, + }, + }); + expect(prisma.userAccounts.update).not.toHaveBeenCalled(); + }); + + it('should set resetToken', async () => { + const id = randomUUID(); + const email = 'email@example.com'; + + prisma.userAccounts.findUnique = jest.fn().mockResolvedValue({ + id, + }); + prisma.userAccounts.update = jest.fn().mockResolvedValue({ + id, + resetToken: 'example reset token', + }); + + await service.forgotPassword({ email }); + expect(prisma.userAccounts.findUnique).toHaveBeenCalledWith({ + where: { + email, + }, + }); + expect(prisma.userAccounts.update).toHaveBeenCalledWith({ + data: { + resetToken: expect.anything(), + }, + where: { + id, + }, + }); + }); + + it('should error when trying to set resetToken on nonexistent user', async () => { + const email = 'email@example.com'; + + prisma.userAccounts.findUnique = jest.fn().mockResolvedValue(null); + prisma.userAccounts.update = jest.fn().mockResolvedValue(null); + + await expect( + async () => await service.forgotPassword({ email }), + ).rejects.toThrowError( + 'user email@example.com was requested but not found', + ); + expect(prisma.userAccounts.findUnique).toHaveBeenCalledWith({ + where: { + email, + }, + }); + + expect(prisma.userAccounts.update).not.toHaveBeenCalled(); + }); + + it('should resend public confirmation', async () => { + const id = randomUUID(); + const email = 'email@example.com'; + + prisma.userAccounts.findUnique = jest.fn().mockResolvedValue({ + id, + email, + }); + prisma.userAccounts.update = jest.fn().mockResolvedValue({ + id, + email, + confirmationToken: 'example confirmation token', + }); + + await service.resendConfirmation({ email }, true); + expect(prisma.userAccounts.findUnique).toHaveBeenCalledWith({ + where: { + email, + }, + }); + expect(prisma.userAccounts.update).toHaveBeenCalledWith({ + data: { + confirmationToken: expect.anything(), + }, + where: { + id, + }, + }); + }); + + it('should resend partner confirmation', async () => { + const id = randomUUID(); + const email = 'email@example.com'; + + prisma.userAccounts.findUnique = jest.fn().mockResolvedValue({ + id, + email, + }); + prisma.userAccounts.update = jest.fn().mockResolvedValue({ + id, + email, + confirmationToken: 'example confirmation token', + }); + + await service.resendConfirmation({ email }, false); + expect(prisma.userAccounts.findUnique).toHaveBeenCalledWith({ + where: { + email, + }, + }); + expect(prisma.userAccounts.update).toHaveBeenCalledWith({ + data: { + confirmationToken: expect.anything(), + }, + where: { + id, + }, + }); + }); + + it('should not update confirmationToken if user is confirmed', async () => { + const id = randomUUID(); + const email = 'email@example.com'; + + prisma.userAccounts.findUnique = jest.fn().mockResolvedValue({ + id, + email, + confirmedAt: new Date(), + }); + prisma.userAccounts.update = jest.fn().mockResolvedValue({ + id, + email, + confirmationToken: 'example confirmation token', + }); + + await service.resendConfirmation({ email }, false); + expect(prisma.userAccounts.findUnique).toHaveBeenCalledWith({ + where: { + email, + }, + }); + expect(prisma.userAccounts.update).not.toHaveBeenCalled(); + }); + + it('should error when trying to resend confirmation on nonexistent user', async () => { + const email = 'email@example.com'; + + prisma.userAccounts.findUnique = jest.fn().mockResolvedValue(null); + prisma.userAccounts.update = jest.fn().mockResolvedValue(null); + + await expect( + async () => await service.resendConfirmation({ email }, true), + ).rejects.toThrowError( + 'user email@example.com was requested but not found', + ); + expect(prisma.userAccounts.findUnique).toHaveBeenCalledWith({ + where: { + email, + }, + }); + + expect(prisma.userAccounts.update).not.toHaveBeenCalled(); + }); + + it('should delete user', async () => { + const id = randomUUID(); + + prisma.userAccounts.findUnique = jest.fn().mockResolvedValue({ + id, + }); + prisma.userAccounts.delete = jest.fn().mockResolvedValue({ + id, + }); + prisma.userRoles.delete = jest.fn().mockResolvedValue({ + id, + }); + + await service.delete(id); + expect(prisma.userAccounts.findUnique).toHaveBeenCalledWith({ + where: { + id, + }, + }); + expect(prisma.userAccounts.delete).toHaveBeenCalledWith({ + where: { + id, + }, + }); + expect(prisma.userRoles.delete).toHaveBeenCalledWith({ + where: { + userId: id, + }, + }); + }); + + it('should error when trying to delete nonexistent user', async () => { + const id = randomUUID(); + + prisma.userAccounts.findUnique = jest.fn().mockResolvedValue(null); + prisma.userAccounts.delete = jest.fn().mockResolvedValue(null); + prisma.userRoles.delete = jest.fn().mockResolvedValue(null); + + await expect(async () => await service.delete(id)).rejects.toThrowError( + `user ${id} was requested but not found`, + ); + expect(prisma.userAccounts.findUnique).toHaveBeenCalledWith({ + where: { + id, + }, + }); + + expect(prisma.userAccounts.delete).not.toHaveBeenCalled(); + expect(prisma.userRoles.delete).not.toHaveBeenCalled(); + }); + + it('should update user without updating password or email', async () => { + const id = randomUUID(); + const jurisId = randomUUID(); + + prisma.userAccounts.findUnique = jest.fn().mockResolvedValue({ + id, + }); + prisma.userAccounts.update = jest.fn().mockResolvedValue({ + id, + }); + + await service.update({ + id, + firstName: 'first name', + lastName: 'last name', + jurisdictions: [{ id: jurisId }], + }); + expect(prisma.userAccounts.findUnique).toHaveBeenCalledWith({ + where: { + id, + }, + }); + expect(prisma.userAccounts.update).toHaveBeenCalledWith({ + data: { + firstName: 'first name', + lastName: 'last name', + jurisdictions: { + connect: [{ id: jurisId }], + }, + }, include: { + jurisdictions: true, listings: true, + userRoles: true, + }, + where: { + id, + }, + }); + }); + + it('should update user and update password', async () => { + const id = randomUUID(); + const jurisId = randomUUID(); + const passwordHashed = await passwordToHash('current password'); + + prisma.userAccounts.findUnique = jest.fn().mockResolvedValue({ + id, + passwordHash: passwordHashed, + }); + prisma.userAccounts.update = jest.fn().mockResolvedValue({ + id, + }); + + await service.update({ + id, + firstName: 'first name', + lastName: 'last name', + jurisdictions: [{ id: jurisId }], + password: 'new password', + currentPassword: 'current password', + }); + expect(prisma.userAccounts.findUnique).toHaveBeenCalledWith({ + where: { + id, + }, + }); + expect(prisma.userAccounts.update).toHaveBeenCalledWith({ + data: { + firstName: 'first name', + lastName: 'last name', + jurisdictions: { + connect: [{ id: jurisId }], + }, + passwordHash: expect.anything(), + passwordUpdatedAt: expect.anything(), + }, + include: { jurisdictions: true, + listings: true, userRoles: true, }, where: { - id: { - equals: 'example Id', + id, + }, + }); + }); + + it('should throw missing currentPassword error', async () => { + const id = randomUUID(); + const jurisId = randomUUID(); + const passwordHashed = await passwordToHash('current password'); + + prisma.userAccounts.findUnique = jest.fn().mockResolvedValue({ + id, + passwordHash: passwordHashed, + }); + prisma.userAccounts.update = jest.fn().mockResolvedValue({ + id, + }); + + await expect( + async () => + await service.update({ + id, + firstName: 'first name', + lastName: 'last name', + jurisdictions: [{ id: jurisId }], + password: 'new password', + }), + ).rejects.toThrowError(`userID ${id}: request missing currentPassword`); + expect(prisma.userAccounts.findUnique).toHaveBeenCalledWith({ + where: { + id, + }, + }); + expect(prisma.userAccounts.update).not.toHaveBeenCalledWith(); + }); + + it('should throw password mismatch error', async () => { + const id = randomUUID(); + const jurisId = randomUUID(); + const passwordHashed = await passwordToHash('password'); + + prisma.userAccounts.findUnique = jest.fn().mockResolvedValue({ + id, + passwordHash: passwordHashed, + }); + prisma.userAccounts.update = jest.fn().mockResolvedValue({ + id, + }); + + await expect( + async () => + await service.update({ + id, + firstName: 'first name', + lastName: 'last name', + jurisdictions: [{ id: jurisId }], + password: 'new password', + currentPassword: 'new password', + }), + ).rejects.toThrowError( + `userID ${id}: incoming current password doesn't match stored password`, + ); + expect(prisma.userAccounts.findUnique).toHaveBeenCalledWith({ + where: { + id, + }, + }); + expect(prisma.userAccounts.update).not.toHaveBeenCalledWith(); + }); + + it('should update user and email', async () => { + const id = randomUUID(); + const jurisId = randomUUID(); + + prisma.userAccounts.findUnique = jest.fn().mockResolvedValue({ + id, + }); + prisma.userAccounts.update = jest.fn().mockResolvedValue({ + id, + }); + + await service.update({ + id, + firstName: 'first name', + lastName: 'last name', + jurisdictions: [{ id: jurisId }], + newEmail: 'new@email.com', + appUrl: 'www.example.com', + }); + expect(prisma.userAccounts.findUnique).toHaveBeenCalledWith({ + where: { + id, + }, + }); + expect(prisma.userAccounts.update).toHaveBeenCalledWith({ + data: { + firstName: 'first name', + lastName: 'last name', + jurisdictions: { + connect: [{ id: jurisId }], }, + confirmationToken: expect.anything(), + }, + include: { + jurisdictions: true, + listings: true, + userRoles: true, + }, + where: { + id, + }, + }); + }); + + it('should error when trying to update nonexistent user', async () => { + const id = randomUUID(); + + prisma.userAccounts.findUnique = jest.fn().mockResolvedValue(null); + prisma.userAccounts.update = jest.fn().mockResolvedValue(null); + + await expect( + async () => + await service.update({ + id, + firstName: 'first name', + lastName: 'last name', + jurisdictions: [{ id: randomUUID() }], + }), + ).rejects.toThrowError(`user ${id} was requested but not found`); + expect(prisma.userAccounts.findUnique).toHaveBeenCalledWith({ + where: { + id, + }, + }); + + expect(prisma.userAccounts.update).not.toHaveBeenCalled(); + }); + + it('should create a partner user with no existing user present', async () => { + const jurisId = randomUUID(); + const id = randomUUID(); + + prisma.userAccounts.findUnique = jest.fn().mockResolvedValue(null); + prisma.userAccounts.create = jest.fn().mockResolvedValue({ + id, + }); + await service.create( + { + firstName: 'Partner User firstName', + lastName: 'Partner User lastName', + password: 'example password 1', + email: 'partnerUser@email.com', + jurisdictions: [{ id: jurisId }], + userRoles: { + isAdmin: true, + }, + }, + true, + ); + expect(prisma.userAccounts.findUnique).toHaveBeenCalledWith({ + include: { + jurisdictions: true, + listings: true, + userRoles: true, + }, + where: { + email: 'partnerUser@email.com', + }, + }); + expect(prisma.userAccounts.create).toHaveBeenCalledWith({ + data: { + passwordHash: expect.anything(), + email: 'partnerUser@email.com', + firstName: 'Partner User firstName', + lastName: 'Partner User lastName', + mfaEnabled: true, + jurisdictions: { + connect: [{ id: jurisId }], + }, + userRoles: { + create: { + isAdmin: true, + }, + }, + }, + }); + }); + + it('should create a partner user with existing public user present', async () => { + const jurisId = randomUUID(); + const id = randomUUID(); + + prisma.userAccounts.findUnique = jest.fn().mockResolvedValue({ + id, + confirmationToken: 'token', + }); + prisma.userAccounts.update = jest.fn().mockResolvedValue({ + id, + }); + await service.create( + { + firstName: 'Partner User firstName', + lastName: 'Partner User lastName', + password: 'example password 1', + email: 'partnerUser@email.com', + jurisdictions: [{ id: jurisId }], + userRoles: { + isPartner: true, + }, + listings: [{ id: 'listing id' }], + }, + true, + ); + expect(prisma.userAccounts.findUnique).toHaveBeenCalledWith({ + include: { + jurisdictions: true, + listings: true, + userRoles: true, + }, + where: { + email: 'partnerUser@email.com', + }, + }); + expect(prisma.userAccounts.update).toHaveBeenCalledWith({ + include: { + jurisdictions: true, + listings: true, + userRoles: true, + }, + data: { + confirmedAt: null, + confirmationToken: 'token', + userRoles: { + create: { + isPartner: true, + }, + }, + listings: { + connect: [{ id: 'listing id' }], + }, + }, + where: { + id, + }, + }); + }); + + it('should error create a partner user with existing partner user present', async () => { + const jurisId = randomUUID(); + const id = randomUUID(); + + prisma.userAccounts.findUnique = jest.fn().mockResolvedValue({ + id, + confirmationToken: 'token', + userRoles: { + isPartner: true, + }, + jurisdictions: [{ id: jurisId }], + }); + prisma.userAccounts.update = jest.fn().mockResolvedValue(null); + prisma.userAccounts.create = jest.fn().mockResolvedValue(null); + await expect( + async () => + await service.create( + { + firstName: 'Partner User firstName', + lastName: 'Partner User lastName', + password: 'example password 1', + email: 'partnerUser@email.com', + jurisdictions: [{ id: jurisId }], + userRoles: { + isPartner: true, + }, + listings: [{ id: 'listing id' }], + }, + true, + ), + ).rejects.toThrowError('emailInUse'); + expect(prisma.userAccounts.findUnique).toHaveBeenCalledWith({ + include: { + jurisdictions: true, + listings: true, + userRoles: true, + }, + where: { + email: 'partnerUser@email.com', + }, + }); + expect(prisma.userAccounts.update).not.toHaveBeenCalled(); + expect(prisma.userAccounts.create).not.toHaveBeenCalled(); + }); + + it('should create a public user', async () => { + const jurisId = randomUUID(); + const id = randomUUID(); + + prisma.userAccounts.findUnique = jest.fn().mockResolvedValue(null); + prisma.applications.findMany = jest + .fn() + .mockResolvedValue([ + { id: 'application id 1' }, + { id: 'application id 2' }, + ]); + prisma.applications.update = jest.fn().mockResolvedValue(null); + prisma.userAccounts.create = jest.fn().mockResolvedValue({ + id, + email: 'publicUser@email.com', + }); + prisma.userAccounts.update = jest.fn().mockResolvedValue({ + id, + email: 'publicUser@email.com', + }); + await service.create( + { + firstName: 'public User firstName', + lastName: 'public User lastName', + password: 'example password 1', + email: 'publicUser@email.com', + jurisdictions: [{ id: jurisId }], + }, + false, + ); + expect(prisma.userAccounts.findUnique).toHaveBeenCalledWith({ + include: { + jurisdictions: true, + listings: true, + userRoles: true, + }, + where: { + email: 'publicUser@email.com', + }, + }); + expect(prisma.userAccounts.create).toHaveBeenCalledWith({ + data: { + passwordHash: expect.anything(), + email: 'publicUser@email.com', + firstName: 'public User firstName', + lastName: 'public User lastName', + mfaEnabled: false, + jurisdictions: { + connect: [{ id: jurisId }], + }, + }, + }); + expect(prisma.userAccounts.update).toHaveBeenCalledWith({ + include: { + jurisdictions: true, + listings: true, + userRoles: true, + }, + data: { + confirmationToken: expect.anything(), + }, + where: { + id: id, + }, + }); + expect(prisma.applications.findMany).toHaveBeenCalledWith({ + where: { + applicant: { + emailAddress: 'publicUser@email.com', + }, + userAccounts: null, + }, + }); + expect(prisma.applications.update).toHaveBeenNthCalledWith(1, { + data: { + userAccounts: { + connect: { + id, + }, + }, + }, + where: { + id: 'application id 1', + }, + }); + expect(prisma.applications.update).toHaveBeenNthCalledWith(2, { + data: { + userAccounts: { + connect: { + id, + }, + }, + }, + where: { + id: 'application id 2', }, }); }); diff --git a/backend_new/test/unit/utilities/password-helper.spec.ts b/backend_new/test/unit/utilities/password-helper.spec.ts new file mode 100644 index 0000000000..4e2dd228d2 --- /dev/null +++ b/backend_new/test/unit/utilities/password-helper.spec.ts @@ -0,0 +1,23 @@ +import { + isPasswordValid, + passwordToHash, + generateSalt, +} from '../../../src/utilities/password-helpers'; +describe('Testing password helpers', () => { + it('should generate salt of the correct length', () => { + expect(generateSalt(15).length).toBe(15); + expect(generateSalt().length).toBe(64); + }); + + it('should create a hash from password and verify that the password is valid', async () => { + const hash = await passwordToHash('abcdef123'); + const isValid = await isPasswordValid(hash, 'abcdef123'); + expect(isValid).toBe(true); + }); + + it('should return false when incorrect password is provided', async () => { + const hash = await passwordToHash('abcdef123'); + const isValid = await isPasswordValid(hash, 'abcdef'); + expect(isValid).toBe(false); + }); +}); diff --git a/backend_new/test/unit/utilities/password-regex.spec.ts b/backend_new/test/unit/utilities/password-regex.spec.ts new file mode 100644 index 0000000000..195f24fb5d --- /dev/null +++ b/backend_new/test/unit/utilities/password-regex.spec.ts @@ -0,0 +1,9 @@ +import { passwordRegex } from '../../../src/utilities/password-regex'; +describe('Testing password regex', () => { + it('should not match for weak password', () => { + expect(passwordRegex.test('abcdef')).toBe(false); + }); + it('should match for strong password', () => { + expect(passwordRegex.test('abcdef123')).toBe(true); + }); +}); diff --git a/backend_new/tsconfig.json b/backend_new/tsconfig.json index e43955351b..ca64985b66 100644 --- a/backend_new/tsconfig.json +++ b/backend_new/tsconfig.json @@ -16,7 +16,8 @@ "noImplicitAny": false, "strictBindCallApply": false, "forceConsistentCasingInFileNames": false, - "noFallthroughCasesInSwitch": false + "noFallthroughCasesInSwitch": false, + "esModuleInterop": true }, "exclude": ["node_modules", "dist"] } diff --git a/backend_new/types/src/backend-swagger.ts b/backend_new/types/src/backend-swagger.ts index 223548e4db..851338db44 100644 --- a/backend_new/types/src/backend-swagger.ts +++ b/backend_new/types/src/backend-swagger.ts @@ -1283,6 +1283,36 @@ export class ApplicationsService { } } +export class AssetsService { + /** + * Create presigned upload metadata + */ + createPresignedUploadMetadata( + params: { + /** requestBody */ + body?: CreatePresignedUploadMetadata; + } = {} as any, + options: IRequestOptions = {}, + ): Promise { + return new Promise((resolve, reject) => { + let url = basePath + '/assets/presigned-upload-metadata'; + + const configs: IRequestConfig = getConfigs( + 'post', + 'application/json', + url, + options, + ); + + let data = params.body; + + configs.data = data; + + axios(configs, resolve, reject); + }); + } +} + export class UserService { /** * @@ -1303,6 +1333,63 @@ export class UserService { axios(configs, resolve, reject); }); } + /** + * Delete user by id + */ + delete( + params: { + /** requestBody */ + body?: IdDTO; + } = {} as any, + options: IRequestOptions = {}, + ): Promise { + return new Promise((resolve, reject) => { + let url = basePath + '/user'; + + const configs: IRequestConfig = getConfigs( + 'delete', + 'application/json', + url, + options, + ); + + let data = params.body; + + configs.data = data; + + axios(configs, resolve, reject); + }); + } + /** + * Creates a public only user + */ + create( + params: { + /** */ + noWelcomeEmail?: boolean; + /** requestBody */ + body?: UserCreate; + } = {} as any, + options: IRequestOptions = {}, + ): Promise { + return new Promise((resolve, reject) => { + let url = basePath + '/user'; + + const configs: IRequestConfig = getConfigs( + 'post', + 'application/json', + url, + options, + ); + configs.params = { noWelcomeEmail: params['noWelcomeEmail'] }; + + let data = params.body; + + configs.data = data; + + axios(configs, resolve, reject); + }); + } /** * Get a paginated set of users */ @@ -1363,6 +1450,168 @@ export class UserService { /** 适配ios13getèŻ·æ±‚äžć…èźžćžŠbody */ + axios(configs, resolve, reject); + }); + } + /** + * Update user + */ + update( + params: { + /** requestBody */ + body?: UserUpdate; + } = {} as any, + options: IRequestOptions = {}, + ): Promise { + return new Promise((resolve, reject) => { + let url = basePath + '/user/{id}'; + + const configs: IRequestConfig = getConfigs( + 'put', + 'application/json', + url, + options, + ); + + let data = params.body; + + configs.data = data; + + axios(configs, resolve, reject); + }); + } + /** + * Forgot Password + */ + forgotPassword( + params: { + /** requestBody */ + body?: EmailAndAppUrl; + } = {} as any, + options: IRequestOptions = {}, + ): Promise { + return new Promise((resolve, reject) => { + let url = basePath + '/user/forgot-password'; + + const configs: IRequestConfig = getConfigs( + 'put', + 'application/json', + url, + options, + ); + + let data = params.body; + + configs.data = data; + + axios(configs, resolve, reject); + }); + } + /** + * Invite partner user + */ + invite( + params: { + /** requestBody */ + body?: UserInvite; + } = {} as any, + options: IRequestOptions = {}, + ): Promise { + return new Promise((resolve, reject) => { + let url = basePath + '/user/invite'; + + const configs: IRequestConfig = getConfigs( + 'post', + 'application/json', + url, + options, + ); + + let data = params.body; + + configs.data = data; + + axios(configs, resolve, reject); + }); + } + /** + * Resend public confirmation + */ + resendConfirmation( + params: { + /** requestBody */ + body?: EmailAndAppUrl; + } = {} as any, + options: IRequestOptions = {}, + ): Promise { + return new Promise((resolve, reject) => { + let url = basePath + '/user/resend-confirmation'; + + const configs: IRequestConfig = getConfigs( + 'post', + 'application/json', + url, + options, + ); + + let data = params.body; + + configs.data = data; + + axios(configs, resolve, reject); + }); + } + /** + * Resend partner confirmation + */ + resendPartnerConfirmation( + params: { + /** requestBody */ + body?: EmailAndAppUrl; + } = {} as any, + options: IRequestOptions = {}, + ): Promise { + return new Promise((resolve, reject) => { + let url = basePath + '/user/resend-partner-confirmation'; + + const configs: IRequestConfig = getConfigs( + 'post', + 'application/json', + url, + options, + ); + + let data = params.body; + + configs.data = data; + + axios(configs, resolve, reject); + }); + } + /** + * Verifies token is valid + */ + isUserConfirmationTokenValid( + params: { + /** requestBody */ + body?: ConfirmationRequest; + } = {} as any, + options: IRequestOptions = {}, + ): Promise { + return new Promise((resolve, reject) => { + let url = basePath + '/user/is-confirmation-token-valid'; + + const configs: IRequestConfig = getConfigs( + 'post', + 'application/json', + url, + options, + ); + + let data = params.body; + + configs.data = data; + axios(configs, resolve, reject); }); } @@ -2589,16 +2838,25 @@ export interface PaginatedApplication { items: Application[]; } -export interface UserRole { +export interface CreatePresignedUploadMetadata { /** */ - id: string; + parametersToSign: object; +} +export interface CreatePresignedUploadMetadataResponse { /** */ - createdAt: Date; + signature: string; +} +export interface EmailAndAppUrl { /** */ - updatedAt: Date; + email: string; + + /** */ + appUrl?: string; +} +export interface UserRole { /** */ isAdmin?: boolean; @@ -2685,6 +2943,131 @@ export interface PaginatedUser { items: User[]; } +export interface UserUpdate { + /** */ + id: string; + + /** */ + middleName?: string; + + /** */ + lastName: string; + + /** */ + dob?: Date; + + /** */ + phoneNumber?: string; + + /** */ + listings: IdDTO[]; + + /** */ + userRoles?: UserRole; + + /** */ + language?: LanguagesEnum; + + /** */ + jurisdictions: IdDTO[]; + + /** */ + email?: string; + + /** */ + newEmail?: string; + + /** */ + password?: string; + + /** */ + currentPassword?: string; + + /** */ + appUrl?: string; +} + +export interface UserCreate { + /** */ + middleName?: string; + + /** */ + lastName: string; + + /** */ + dob?: Date; + + /** */ + phoneNumber?: string; + + /** */ + listings: IdDTO[]; + + /** */ + language?: LanguagesEnum; + + /** */ + newEmail?: string; + + /** */ + appUrl?: string; + + /** */ + password: string; + + /** */ + passwordConfirmation: string; + + /** */ + email: string; + + /** */ + emailConfirmation: string; + + /** */ + jurisdictions?: IdDTO[]; +} + +export interface UserInvite { + /** */ + middleName?: string; + + /** */ + lastName: string; + + /** */ + dob?: Date; + + /** */ + phoneNumber?: string; + + /** */ + listings: IdDTO[]; + + /** */ + userRoles?: UserRole; + + /** */ + language?: LanguagesEnum; + + /** */ + jurisdictions: IdDTO[]; + + /** */ + newEmail?: string; + + /** */ + appUrl?: string; + + /** */ + email: string; +} + +export interface ConfirmationRequest { + /** */ + token: string; +} + export enum ListingViews { 'fundamentals' = 'fundamentals', 'base' = 'base', diff --git a/backend_new/yarn.lock b/backend_new/yarn.lock index bc3c840a08..3f642977cd 100644 --- a/backend_new/yarn.lock +++ b/backend_new/yarn.lock @@ -2470,10 +2470,10 @@ data-urls@^3.0.2: whatwg-mimetype "^3.0.0" whatwg-url "^11.0.0" -dayjs@^1.11.8: - version "1.11.8" - resolved "https://registry.yarnpkg.com/dayjs/-/dayjs-1.11.8.tgz#4282f139c8c19dd6d0c7bd571e30c2d0ba7698ea" - integrity sha512-LcgxzFoWMEPO7ggRv1Y2N31hUf2R0Vj7fuy/m+Bg1K8rr+KAs1AEy4y9jd5DXe8pbHgX+srkHNS7TH6Q6ZhYeQ== +dayjs@^1.11.9: + version "1.11.9" + resolved "https://registry.yarnpkg.com/dayjs/-/dayjs-1.11.9.tgz#9ca491933fadd0a60a2c19f6c237c03517d71d1a" + integrity sha512-QvzAURSbQ0pKdIye2txOzNaHmxtUBXerpY0FJsFXUMKbIZeFm5ht1LS/jFsrncjnmtv8HsG0W2g6c0zUjZWmpA== debug@2.6.9: version "2.6.9" @@ -4188,6 +4188,25 @@ jsonfile@^6.0.1: optionalDependencies: graceful-fs "^4.1.6" +jsonwebtoken@^9.0.1: + version "9.0.1" + resolved "https://registry.yarnpkg.com/jsonwebtoken/-/jsonwebtoken-9.0.1.tgz#81d8c901c112c24e497a55daf6b2be1225b40145" + integrity sha512-K8wx7eJ5TPvEjuiVSkv167EVboBDv9PZdDoF7BgeQnBLVvZWW9clr2PsQHVJDTKaEIH5JBIwHujGcHp7GgI2eg== + dependencies: + jws "^3.2.2" + lodash "^4.17.21" + ms "^2.1.1" + semver "^7.3.8" + +jwa@^1.4.1: + version "1.4.1" + resolved "https://registry.yarnpkg.com/jwa/-/jwa-1.4.1.tgz#743c32985cb9e98655530d53641b66c8645b039a" + integrity sha512-qiLX/xhEEFKUAJ6FiBMbes3w9ATzyk5W7Hvzpa/SLYdxNtng+gcurvrI7TbACjIXlsJyr05/S1oUhZrc63evQA== + dependencies: + buffer-equal-constant-time "1.0.1" + ecdsa-sig-formatter "1.0.11" + safe-buffer "^5.0.1" + jwa@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/jwa/-/jwa-2.0.0.tgz#a7e9c3f29dae94027ebcaf49975c9345593410fc" @@ -4197,6 +4216,14 @@ jwa@^2.0.0: ecdsa-sig-formatter "1.0.11" safe-buffer "^5.0.1" +jws@^3.2.2: + version "3.2.2" + resolved "https://registry.yarnpkg.com/jws/-/jws-3.2.2.tgz#001099f3639468c9414000e99995fa52fb478304" + integrity sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA== + dependencies: + jwa "^1.4.1" + safe-buffer "^5.0.1" + jws@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/jws/-/jws-4.0.0.tgz#2d4e8cf6a318ffaa12615e9dec7e86e6c97310f4" @@ -4496,7 +4523,7 @@ ms@2.1.2: resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009" integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w== -ms@2.1.3: +ms@2.1.3, ms@^2.1.1: version "2.1.3" resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2" integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA== From c027a662b266668b87b7e569ae4d25a0dcefc92b Mon Sep 17 00:00:00 2001 From: Morgan Ludtke <42942267+ludtkemorgan@users.noreply.github.com> Date: Mon, 28 Aug 2023 10:27:20 -0500 Subject: [PATCH 20/57] fix: change how user is retreived from req (#3613) --- backend_new/package.json | 2 +- backend_new/src/controllers/user.controller.ts | 4 ++-- backend_new/yarn.lock | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/backend_new/package.json b/backend_new/package.json index f9427c86d4..607c97e070 100644 --- a/backend_new/package.json +++ b/backend_new/package.json @@ -52,7 +52,7 @@ "@nestjs/cli": "^8.0.0", "@nestjs/schematics": "^8.0.0", "@nestjs/testing": "^8.0.0", - "@types/express": "^4.17.13", + "@types/express": "^4.17.17", "@types/jest": "^29.5.3", "@types/node": "^18.7.14", "@types/supertest": "^2.0.11", diff --git a/backend_new/src/controllers/user.controller.ts b/backend_new/src/controllers/user.controller.ts index 16c7ef9ee1..60651e2e25 100644 --- a/backend_new/src/controllers/user.controller.ts +++ b/backend_new/src/controllers/user.controller.ts @@ -45,7 +45,7 @@ export class UserController { @Get() profile(@Request() req: ExpressRequest): User { - return mapTo(User, req.user); + return mapTo(User, req['user']); } @Get('/list') @@ -60,7 +60,7 @@ export class UserController { @Request() req: ExpressRequest, @Query() queryParams: UserQueryParams, ): Promise { - return await this.userService.list(queryParams, mapTo(User, req.user)); + return await this.userService.list(queryParams, mapTo(User, req['user'])); } @Get(`:id`) diff --git a/backend_new/yarn.lock b/backend_new/yarn.lock index 3f642977cd..16be9cbeca 100644 --- a/backend_new/yarn.lock +++ b/backend_new/yarn.lock @@ -1266,7 +1266,7 @@ "@types/range-parser" "*" "@types/send" "*" -"@types/express@^4.17.13": +"@types/express@^4.17.17": version "4.17.17" resolved "https://registry.yarnpkg.com/@types/express/-/express-4.17.17.tgz#01d5437f6ef9cfa8668e616e13c2f2ac9a491ae4" integrity sha512-Q4FmmuLGBG58btUnfS1c1r/NQdlp3DMfGDGig8WhfpA2YRUtEkxAjkZb0yvplJGYdF1fsQ81iMDcH24sSCNC/Q== From 5087edf742115a4fb3f1375d73e56c48f71dece5 Mon Sep 17 00:00:00 2001 From: Yazeed Loonat Date: Thu, 31 Aug 2023 17:02:00 -0700 Subject: [PATCH 21/57] feat: listing create. update, delete (#3602) --- .circleci/config.yml | 3 +- backend_new/.env.template | 1 + backend_new/package.json | 5 +- .../src/controllers/listing.controller.ts | 38 +- .../multiselect-question.controller.ts | 2 +- .../src/dtos/addresses/address-create.dto.ts | 8 + .../{address-get.dto.ts => address.dto.ts} | 10 + .../ami-charts/ami-chart-query-params.dto.ts | 5 +- .../src/dtos/ami-charts/ami-chart.dto.ts | 7 +- .../application-method-create.dto.ts | 19 + ...d-get.dto.ts => application-method.dto.ts} | 7 +- .../applications/alternate-contact.dto.ts | 2 +- .../src/dtos/applications/applicant.dto.ts | 2 +- ...ication-multiselect-question-option.dto.ts | 11 +- .../application-query-params.dto.ts | 23 +- .../src/dtos/applications/application.dto.ts | 16 +- .../dtos/applications/household-member.dto.ts | 2 +- .../src/dtos/assets/asset-create.dto.ts | 8 + .../assets/{asset-get.dto.ts => asset.dto.ts} | 3 + ...create-presign-upload-meta-response.dto.ts | 2 +- .../create-presigned-upload-meta.dto.ts | 2 +- .../dtos/jurisdictions/jurisdiction.dto.ts | 16 +- .../src/dtos/listings/listing-create.dto.ts | 4 + .../dtos/listings/listing-event-create.dto.ts | 19 + .../src/dtos/listings/listing-event.dto.ts | 11 +- .../listings/listing-feature-create.dto.ts | 8 + .../src/dtos/listings/listing-feature.dto.ts | 16 + .../dtos/listings/listing-image-create.dto.ts | 15 + .../src/dtos/listings/listing-image.dto.ts | 5 +- .../listing-multiselect-question.dto.ts | 8 +- .../listings/listing-published-create.dto.ts | 6 + .../listings/listing-published-update.dto.ts | 141 ++ .../src/dtos/listings/listing-update.dto.ts | 149 ++ .../listings/listing-utility-create.dto.ts | 8 + .../src/dtos/listings/listing-utility.dto.ts | 9 + .../{listing-get.dto.ts => listing.dto.ts} | 200 +- .../listings/listings-filter-params.dto.ts | 32 +- .../listings/listings-query-params.dto.ts | 22 +- .../listings/listings-retrieve-params.dto.ts | 5 +- .../dtos/listings/paginated-listing.dto.ts | 6 +- .../multiselect-option.dto.ts | 12 +- .../multiselect-question-filter-params.dto.ts | 12 +- .../multiselect-question-query-params.dto.ts | 8 +- .../multiselect-question-update.dto.ts | 2 +- ...get.dto.ts => multiselect-question.dto.ts} | 18 +- .../paper-application-create.dto.ts | 19 + .../paper-application.dto.ts | 8 +- ...eserved-community-type-query-params.dto.ts | 10 +- .../reserved-community-type.dto.ts | 7 +- backend_new/src/dtos/shared/abstract.dto.ts | 6 +- backend_new/src/dtos/shared/id.dto.ts | 13 +- backend_new/src/dtos/shared/pagination.dto.ts | 4 - ...-item-get.dto.ts => ami-chart-item.dto.ts} | 6 +- .../units/ami-chart-override-create.dto.ts | 8 + ...e-get.dto.ts => ami-chart-override.dto.ts} | 4 +- ...{ami-chart-get.dto.ts => ami-chart.dto.ts} | 5 +- .../dtos/units/{hmi-get.dto.ts => hmi.dto.ts} | 0 backend_new/src/dtos/units/unit-create.dto.ts | 48 + .../src/dtos/units/unit-summarized.dto.ts | 6 +- ...-get.dto.ts => unit-summary-by-ami.dto.ts} | 2 +- ...summary-get.dto.ts => unit-summary.dto.ts} | 4 +- .../units/{unit-get.dto.ts => unit.dto.ts} | 22 +- .../dtos/units/units-summary-create.dto.ts | 4 + ...ummery-get.dto.ts => units-summary.dto.ts} | 33 +- .../dtos/users/confirmation-request.dto.ts | 2 +- .../src/dtos/users/email-and-app-url.dto.ts | 6 +- .../src/dtos/users/user-create-params.dto.ts | 5 +- backend_new/src/dtos/users/user-create.dto.ts | 12 +- .../src/dtos/users/user-filter-params.dto.ts | 5 +- backend_new/src/dtos/users/user-invite.dto.ts | 2 +- .../src/dtos/users/user-query-param.dto.ts | 8 +- backend_new/src/dtos/users/user-role.dto.ts | 8 +- backend_new/src/dtos/users/user-update.dto.ts | 12 +- backend_new/src/dtos/users/user.dto.ts | 29 +- backend_new/src/modules/listing.module.ts | 3 +- backend_new/src/services/ami-chart.service.ts | 2 +- backend_new/src/services/listing.service.ts | 627 +++++- .../services/multiselect-question.service.ts | 2 +- .../src/services/translation.service.ts | 16 +- backend_new/src/utilities/listing-url-slug.ts | 35 + backend_new/src/utilities/unit-utilities.ts | 19 +- .../listing-create-update-pipe.ts | 39 + .../test/integration/listing.e2e-spec.ts | 349 ++++ backend_new/test/jest-e2e.config.js | 2 +- backend_new/test/jest-with-coverage.config.js | 28 + backend_new/test/jest.config.js | 2 +- .../unit/services/listing.service.spec.ts | 1291 ++++++++++++- .../unit/services/translation.service.spec.ts | 10 +- .../unit/utilities/listing-url-slug.spec.ts | 66 + .../unit/utilities/unit-utilities.spec.ts | 6 +- backend_new/types/src/backend-swagger.ts | 1695 ++++++++++++++--- backend_new/yarn.lock | 7 +- package.json | 1 + 93 files changed, 4690 insertions(+), 716 deletions(-) create mode 100644 backend_new/src/dtos/addresses/address-create.dto.ts rename backend_new/src/dtos/addresses/{address-get.dto.ts => address.dto.ts} (87%) create mode 100644 backend_new/src/dtos/application-methods/application-method-create.dto.ts rename backend_new/src/dtos/application-methods/{application-method-get.dto.ts => application-method.dto.ts} (87%) create mode 100644 backend_new/src/dtos/assets/asset-create.dto.ts rename backend_new/src/dtos/assets/{asset-get.dto.ts => asset.dto.ts} (89%) create mode 100644 backend_new/src/dtos/listings/listing-create.dto.ts create mode 100644 backend_new/src/dtos/listings/listing-event-create.dto.ts create mode 100644 backend_new/src/dtos/listings/listing-feature-create.dto.ts create mode 100644 backend_new/src/dtos/listings/listing-image-create.dto.ts create mode 100644 backend_new/src/dtos/listings/listing-published-create.dto.ts create mode 100644 backend_new/src/dtos/listings/listing-published-update.dto.ts create mode 100644 backend_new/src/dtos/listings/listing-update.dto.ts create mode 100644 backend_new/src/dtos/listings/listing-utility-create.dto.ts rename backend_new/src/dtos/listings/{listing-get.dto.ts => listing.dto.ts} (78%) rename backend_new/src/dtos/multiselect-questions/{multiselect-question-get.dto.ts => multiselect-question.dto.ts} (87%) create mode 100644 backend_new/src/dtos/paper-applications/paper-application-create.dto.ts rename backend_new/src/dtos/units/{ami-chart-item-get.dto.ts => ami-chart-item.dto.ts} (87%) create mode 100644 backend_new/src/dtos/units/ami-chart-override-create.dto.ts rename backend_new/src/dtos/units/{ami-chart-override-get.dto.ts => ami-chart-override.dto.ts} (76%) rename backend_new/src/dtos/units/{ami-chart-get.dto.ts => ami-chart.dto.ts} (79%) rename backend_new/src/dtos/units/{hmi-get.dto.ts => hmi.dto.ts} (100%) create mode 100644 backend_new/src/dtos/units/unit-create.dto.ts rename backend_new/src/dtos/units/{unit-summary-by-ami-get.dto.ts => unit-summary-by-ami.dto.ts} (92%) rename backend_new/src/dtos/units/{unit-summary-get.dto.ts => unit-summary.dto.ts} (94%) rename backend_new/src/dtos/units/{unit-get.dto.ts => unit.dto.ts} (78%) create mode 100644 backend_new/src/dtos/units/units-summary-create.dto.ts rename backend_new/src/dtos/units/{units-summery-get.dto.ts => units-summary.dto.ts} (72%) create mode 100644 backend_new/src/utilities/listing-url-slug.ts create mode 100644 backend_new/src/validation-pipes/listing-create-update-pipe.ts create mode 100644 backend_new/test/jest-with-coverage.config.js create mode 100644 backend_new/test/unit/utilities/listing-url-slug.spec.ts diff --git a/.circleci/config.yml b/.circleci/config.yml index 7ae0241025..0123b75341 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -102,8 +102,7 @@ jobs: name: DB Setup + New Backend Core Tests command: | yarn test:backend:new:dbsetup - yarn test:backend:new - yarn test:backend:new:e2e + yarn test:backend:new:cov environment: PORT: "3100" EMAIL_API_KEY: "SG.SOME-LONG-SECRET-KEY" diff --git a/backend_new/.env.template b/backend_new/.env.template index edb7611bf7..13460d533f 100644 --- a/backend_new/.env.template +++ b/backend_new/.env.template @@ -5,3 +5,4 @@ GOOGLE_API_ID= GOOGLE_API_KEY= CLOUDINARY_SECRET= APP_SECRET= +PROXY_URL= diff --git a/backend_new/package.json b/backend_new/package.json index 607c97e070..24bbb6e9e5 100644 --- a/backend_new/package.json +++ b/backend_new/package.json @@ -16,7 +16,7 @@ "lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix", "test": "jest --config ./test/jest.config.js", "test:watch": "jest --watch", - "test:cov": "jest --coverage", + "test:cov": "yarn db:resetup && yarn db:migration:run && jest --config ./test/jest-with-coverage.config.js", "test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand", "db:resetup": "psql -c 'DROP DATABASE IF EXISTS bloom_prisma;' && psql -c 'CREATE DATABASE bloom_prisma;' && psql -d bloom_prisma -c 'CREATE EXTENSION IF NOT EXISTS \"uuid-ossp\";'", "db:migration:run": "yarn prisma migrate deploy", @@ -30,6 +30,7 @@ }, "dependencies": { "@google-cloud/translate": "^7.2.1", + "@nestjs/axios": "^3.0.0", "@nestjs/common": "^8.0.0", "@nestjs/core": "^8.0.0", "@nestjs/platform-express": "^8.0.0", @@ -45,7 +46,7 @@ "qs": "^6.11.2", "reflect-metadata": "^0.1.13", "rimraf": "^3.0.2", - "rxjs": "^7.2.0", + "rxjs": "^7.8.1", "swagger-axios-codegen": "^0.15.11" }, "devDependencies": { diff --git a/backend_new/src/controllers/listing.controller.ts b/backend_new/src/controllers/listing.controller.ts index 6f3fb9fa60..a8cbb81113 100644 --- a/backend_new/src/controllers/listing.controller.ts +++ b/backend_new/src/controllers/listing.controller.ts @@ -1,10 +1,14 @@ import { + Body, ClassSerializerInterceptor, Controller, + Delete, Get, Headers, Param, ParseUUIDPipe, + Post, + Put, Query, UseInterceptors, UsePipes, @@ -24,8 +28,12 @@ import { ListingsRetrieveParams } from '../dtos/listings/listings-retrieve-param import { PaginationAllowsAllQueryParams } from '../dtos/shared/pagination.dto'; import { ListingFilterParams } from '../dtos/listings/listings-filter-params.dto'; import { PaginatedListingDto } from '../dtos/listings/paginated-listing.dto'; -import ListingGet from '../dtos/listings/listing-get.dto'; +import Listing from '../dtos/listings/listing.dto'; import { IdDTO } from '../dtos/shared/id.dto'; +import { ListingCreate } from '../dtos/listings/listing-create.dto'; +import { SuccessDTO } from '../dtos/shared/success.dto'; +import { ListingUpdate } from '../dtos/listings/listing-update.dto'; +import { ListingCreateUpdateValidationPipe } from '../validation-pipes/listing-create-update-pipe'; @Controller('listings') @ApiTags('listings') @@ -55,7 +63,7 @@ export class ListingController { @ApiOperation({ summary: 'Get listing by id', operationId: 'retrieve' }) @UseInterceptors(ClassSerializerInterceptor) @UsePipes(new ValidationPipe(defaultValidationPipeOptions)) - @ApiOkResponse({ type: ListingGet }) + @ApiOkResponse({ type: Listing }) async retrieve( @Headers('language') language: LanguagesEnum, @Param('id', new ParseUUIDPipe({ version: '4' })) listingId: string, @@ -68,6 +76,32 @@ export class ListingController { ); } + @Post() + @ApiOperation({ summary: 'Create listing', operationId: 'create' }) + @UseInterceptors(ClassSerializerInterceptor) + @UsePipes(new ListingCreateUpdateValidationPipe(defaultValidationPipeOptions)) + @ApiOkResponse({ type: Listing }) + async create(@Body() listingDto: ListingCreate): Promise { + return await this.listingService.create(listingDto); + } + + @Delete() + @ApiOperation({ summary: 'Delete listing by id', operationId: 'delete' }) + @UsePipes(new ValidationPipe(defaultValidationPipeOptions)) + async delete(@Body() dto: IdDTO): Promise { + return await this.listingService.delete(dto.id); + } + + @Put(':id') + @ApiOperation({ summary: 'Update listing by id', operationId: 'update' }) + @UsePipes(new ListingCreateUpdateValidationPipe(defaultValidationPipeOptions)) + async update( + @Param('id') listingId: string, + @Body() dto: ListingUpdate, + ): Promise { + return await this.listingService.update(dto); + } + @Get(`byMultiselectQuestion/:multiselectQuestionId`) @ApiOperation({ summary: 'Get listings by multiselect question id', diff --git a/backend_new/src/controllers/multiselect-question.controller.ts b/backend_new/src/controllers/multiselect-question.controller.ts index 34b7961909..2b15f61c0b 100644 --- a/backend_new/src/controllers/multiselect-question.controller.ts +++ b/backend_new/src/controllers/multiselect-question.controller.ts @@ -17,7 +17,7 @@ import { ApiTags, } from '@nestjs/swagger'; import { MultiselectQuestionService } from '../services/multiselect-question.service'; -import { MultiselectQuestion } from '../dtos/multiselect-questions/multiselect-question-get.dto'; +import { MultiselectQuestion } from '../dtos/multiselect-questions/multiselect-question.dto'; import { MultiselectQuestionCreate } from '../dtos/multiselect-questions/multiselect-question-create.dto'; import { MultiselectQuestionUpdate } from '../dtos/multiselect-questions/multiselect-question-update.dto'; import { MultiselectQuestionQueryParams } from '../dtos/multiselect-questions/multiselect-question-query-params.dto'; diff --git a/backend_new/src/dtos/addresses/address-create.dto.ts b/backend_new/src/dtos/addresses/address-create.dto.ts new file mode 100644 index 0000000000..e040876165 --- /dev/null +++ b/backend_new/src/dtos/addresses/address-create.dto.ts @@ -0,0 +1,8 @@ +import { OmitType } from '@nestjs/swagger'; +import { Address } from './address.dto'; + +export class AddressCreate extends OmitType(Address, [ + 'id', + 'createdAt', + 'updatedAt', +]) {} diff --git a/backend_new/src/dtos/addresses/address-get.dto.ts b/backend_new/src/dtos/addresses/address.dto.ts similarity index 87% rename from backend_new/src/dtos/addresses/address-get.dto.ts rename to backend_new/src/dtos/addresses/address.dto.ts index 3a3fbc8862..aa187f6cad 100644 --- a/backend_new/src/dtos/addresses/address-get.dto.ts +++ b/backend_new/src/dtos/addresses/address.dto.ts @@ -1,3 +1,4 @@ +import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; import { Expose, Type } from 'class-transformer'; import { IsNumber, IsDefined, IsString, MaxLength } from 'class-validator'; import { ValidationsGroupsEnum } from '../../enums/shared/validation-groups-enum'; @@ -7,49 +8,58 @@ export class Address extends AbstractDTO { @Expose() @IsString({ groups: [ValidationsGroupsEnum.default] }) @MaxLength(64, { groups: [ValidationsGroupsEnum.default] }) + @ApiPropertyOptional() placeName?: string; @Expose() @IsString({ groups: [ValidationsGroupsEnum.default] }) @MaxLength(64, { groups: [ValidationsGroupsEnum.default] }) @IsDefined({ groups: [ValidationsGroupsEnum.default] }) + @ApiProperty() city: string; @Expose() @IsString({ groups: [ValidationsGroupsEnum.default] }) @MaxLength(64, { groups: [ValidationsGroupsEnum.default] }) + @ApiPropertyOptional() county?: string; @Expose() @IsString({ groups: [ValidationsGroupsEnum.default] }) @MaxLength(64, { groups: [ValidationsGroupsEnum.default] }) @IsDefined({ groups: [ValidationsGroupsEnum.default] }) + @ApiProperty() state: string; @Expose() @IsString({ groups: [ValidationsGroupsEnum.default] }) @MaxLength(64, { groups: [ValidationsGroupsEnum.default] }) @IsDefined({ groups: [ValidationsGroupsEnum.default] }) + @ApiProperty() street: string; @Expose() @IsString({ groups: [ValidationsGroupsEnum.default] }) @MaxLength(64, { groups: [ValidationsGroupsEnum.default] }) + @ApiPropertyOptional() street2?: string; @Expose() @IsString({ groups: [ValidationsGroupsEnum.default] }) @MaxLength(10, { groups: [ValidationsGroupsEnum.default] }) @IsDefined({ groups: [ValidationsGroupsEnum.default] }) + @ApiProperty() zipCode: string; @Expose() @IsNumber({}, { groups: [ValidationsGroupsEnum.default] }) + @ApiPropertyOptional() @Type(() => Number) latitude?: number; @Expose() @IsNumber({}, { groups: [ValidationsGroupsEnum.default] }) @Type(() => Number) + @ApiPropertyOptional() longitude?: number; } diff --git a/backend_new/src/dtos/ami-charts/ami-chart-query-params.dto.ts b/backend_new/src/dtos/ami-charts/ami-chart-query-params.dto.ts index 9621df09c0..bc6b04db00 100644 --- a/backend_new/src/dtos/ami-charts/ami-chart-query-params.dto.ts +++ b/backend_new/src/dtos/ami-charts/ami-chart-query-params.dto.ts @@ -1,13 +1,12 @@ import { Expose } from 'class-transformer'; -import { ApiProperty } from '@nestjs/swagger'; +import { ApiPropertyOptional } from '@nestjs/swagger'; import { IsString } from 'class-validator'; import { ValidationsGroupsEnum } from '../../enums/shared/validation-groups-enum'; export class AmiChartQueryParams { @Expose() - @ApiProperty({ + @ApiPropertyOptional({ name: 'jurisdictionId', - required: false, type: String, }) @IsString({ groups: [ValidationsGroupsEnum.default] }) diff --git a/backend_new/src/dtos/ami-charts/ami-chart.dto.ts b/backend_new/src/dtos/ami-charts/ami-chart.dto.ts index b0555748d7..71e7f29379 100644 --- a/backend_new/src/dtos/ami-charts/ami-chart.dto.ts +++ b/backend_new/src/dtos/ami-charts/ami-chart.dto.ts @@ -2,7 +2,7 @@ import { IsDefined, IsString, ValidateNested } from 'class-validator'; import { Expose, Type } from 'class-transformer'; import { ValidationsGroupsEnum } from '../../enums/shared/validation-groups-enum'; import { AbstractDTO } from '../shared/abstract.dto'; -import { AmiChartItem } from '../units/ami-chart-item-get.dto'; +import { AmiChartItem } from '../units/ami-chart-item.dto'; import { IdDTO } from '../shared/id.dto'; import { ApiProperty } from '@nestjs/swagger'; @@ -14,20 +14,19 @@ export class AmiChart extends AbstractDTO { @ApiProperty({ type: AmiChartItem, isArray: true, - required: true, }) items: AmiChartItem[]; @Expose() @IsString({ groups: [ValidationsGroupsEnum.default] }) @IsDefined({ groups: [ValidationsGroupsEnum.default] }) - @ApiProperty({ required: true }) + @ApiProperty() name: string; @Expose() @ValidateNested({ groups: [ValidationsGroupsEnum.default] }) @Type(() => IdDTO) @IsDefined({ groups: [ValidationsGroupsEnum.default] }) - @ApiProperty({ required: true }) + @ApiProperty() jurisdictions: IdDTO; } diff --git a/backend_new/src/dtos/application-methods/application-method-create.dto.ts b/backend_new/src/dtos/application-methods/application-method-create.dto.ts new file mode 100644 index 0000000000..05f557059e --- /dev/null +++ b/backend_new/src/dtos/application-methods/application-method-create.dto.ts @@ -0,0 +1,19 @@ +import { OmitType, ApiPropertyOptional } from '@nestjs/swagger'; +import { Expose, Type } from 'class-transformer'; +import { ValidateNested } from 'class-validator'; +import { ValidationsGroupsEnum } from '../../enums/shared/validation-groups-enum'; +import { PaperApplicationCreate } from '../paper-applications/paper-application-create.dto'; +import { ApplicationMethod } from './application-method.dto'; + +export class ApplicationMethodCreate extends OmitType(ApplicationMethod, [ + 'id', + 'createdAt', + 'updatedAt', + 'paperApplications', +]) { + @Expose() + @ValidateNested({ groups: [ValidationsGroupsEnum.default], each: true }) + @Type(() => PaperApplicationCreate) + @ApiPropertyOptional({ type: PaperApplicationCreate, isArray: true }) + paperApplications?: PaperApplicationCreate[]; +} diff --git a/backend_new/src/dtos/application-methods/application-method-get.dto.ts b/backend_new/src/dtos/application-methods/application-method.dto.ts similarity index 87% rename from backend_new/src/dtos/application-methods/application-method-get.dto.ts rename to backend_new/src/dtos/application-methods/application-method.dto.ts index b9fd8fcdc1..4726e6d239 100644 --- a/backend_new/src/dtos/application-methods/application-method-get.dto.ts +++ b/backend_new/src/dtos/application-methods/application-method.dto.ts @@ -9,7 +9,7 @@ import { } from 'class-validator'; import { ValidationsGroupsEnum } from '../../enums/shared/validation-groups-enum'; import { AbstractDTO } from '../shared/abstract.dto'; -import { ApiProperty } from '@nestjs/swagger'; +import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; import { ApplicationMethodsTypeEnum } from '@prisma/client'; import { PaperApplication } from '../paper-applications/paper-application.dto'; @@ -29,24 +29,29 @@ export class ApplicationMethod extends AbstractDTO { @Expose() @IsString({ groups: [ValidationsGroupsEnum.default] }) @MaxLength(256, { groups: [ValidationsGroupsEnum.default] }) + @ApiPropertyOptional() label?: string; @Expose() @IsString({ groups: [ValidationsGroupsEnum.default] }) @MaxLength(4096, { groups: [ValidationsGroupsEnum.default] }) + @ApiPropertyOptional() externalReference?: string; @Expose() @IsBoolean({ groups: [ValidationsGroupsEnum.default] }) + @ApiPropertyOptional() acceptsPostmarkedApplications?: boolean; @Expose() @IsString({ groups: [ValidationsGroupsEnum.default] }) @MaxLength(16, { groups: [ValidationsGroupsEnum.default] }) + @ApiPropertyOptional() phoneNumber?: string; @Expose() @ValidateNested({ groups: [ValidationsGroupsEnum.default], each: true }) @Type(() => PaperApplication) + @ApiPropertyOptional({ type: PaperApplication, isArray: true }) paperApplications?: PaperApplication[]; } diff --git a/backend_new/src/dtos/applications/alternate-contact.dto.ts b/backend_new/src/dtos/applications/alternate-contact.dto.ts index b13bcb947d..3798c4f880 100644 --- a/backend_new/src/dtos/applications/alternate-contact.dto.ts +++ b/backend_new/src/dtos/applications/alternate-contact.dto.ts @@ -9,7 +9,7 @@ import { import { ValidationsGroupsEnum } from '../../enums/shared/validation-groups-enum'; import { AbstractDTO } from '../shared/abstract.dto'; import { ApiProperty } from '@nestjs/swagger'; -import { Address } from '../addresses/address-get.dto'; +import { Address } from '../addresses/address.dto'; import { EnforceLowerCase } from '../../decorators/enforce-lower-case.decorator'; export class AlternateContact extends AbstractDTO { diff --git a/backend_new/src/dtos/applications/applicant.dto.ts b/backend_new/src/dtos/applications/applicant.dto.ts index e396a4afae..c4d552ba78 100644 --- a/backend_new/src/dtos/applications/applicant.dto.ts +++ b/backend_new/src/dtos/applications/applicant.dto.ts @@ -13,7 +13,7 @@ import { ValidationsGroupsEnum } from '../../enums/shared/validation-groups-enum import { AbstractDTO } from '../shared/abstract.dto'; import { ApiProperty } from '@nestjs/swagger'; import { YesNoEnum } from '@prisma/client'; -import { Address } from '../addresses/address-get.dto'; +import { Address } from '../addresses/address.dto'; import { EnforceLowerCase } from '../../decorators/enforce-lower-case.decorator'; export class Applicant extends AbstractDTO { diff --git a/backend_new/src/dtos/applications/application-multiselect-question-option.dto.ts b/backend_new/src/dtos/applications/application-multiselect-question-option.dto.ts index 63ddbe453f..c0446d215a 100644 --- a/backend_new/src/dtos/applications/application-multiselect-question-option.dto.ts +++ b/backend_new/src/dtos/applications/application-multiselect-question-option.dto.ts @@ -1,4 +1,8 @@ -import { ApiProperty, getSchemaPath } from '@nestjs/swagger'; +import { + ApiProperty, + ApiPropertyOptional, + getSchemaPath, +} from '@nestjs/swagger'; import { Expose, Type } from 'class-transformer'; import { ArrayMaxSize, @@ -11,7 +15,7 @@ import { } from 'class-validator'; import { InputType } from '../../enums/shared/input-type-enum'; import { ValidationsGroupsEnum } from '../../enums/shared/validation-groups-enum'; -import { Address } from '../addresses/address-get.dto'; +import { Address } from '../addresses/address.dto'; class FormMetadataExtraData { @Expose() @@ -62,9 +66,8 @@ export class ApplicationMultiselectQuestionOption { checked: boolean; @Expose() - @ApiProperty({ + @ApiPropertyOptional({ type: 'array', - required: false, items: { oneOf: [ { $ref: getSchemaPath(BooleanInput) }, diff --git a/backend_new/src/dtos/applications/application-query-params.dto.ts b/backend_new/src/dtos/applications/application-query-params.dto.ts index d9a5910f58..e77663ac6f 100644 --- a/backend_new/src/dtos/applications/application-query-params.dto.ts +++ b/backend_new/src/dtos/applications/application-query-params.dto.ts @@ -1,27 +1,24 @@ -import { PaginationAllowsAllQueryParams } from '../shared/pagination.dto'; import { Expose, Transform, TransformFnParams } from 'class-transformer'; -import { ApiProperty } from '@nestjs/swagger'; +import { ApiPropertyOptional } from '@nestjs/swagger'; import { IsBoolean, IsEnum, IsString } from 'class-validator'; import { ValidationsGroupsEnum } from '../../enums/shared/validation-groups-enum'; import { ApplicationOrderByKeys } from '../../enums/applications/order-by-enum'; import { OrderByEnum } from '../../enums/shared/order-by-enum'; import { SearchStringLengthCheck } from '../../decorators/search-string-length-check.decorator'; - +import { PaginationAllowsAllQueryParams } from '../shared/pagination.dto'; export class ApplicationQueryParams extends PaginationAllowsAllQueryParams { @Expose() - @ApiProperty({ + @ApiPropertyOptional({ type: String, example: 'listingId', - required: false, }) @IsString({ groups: [ValidationsGroupsEnum.default] }) listingId?: string; @Expose() - @ApiProperty({ + @ApiPropertyOptional({ type: String, example: 'search', - required: false, }) @IsString({ groups: [ValidationsGroupsEnum.default] }) @SearchStringLengthCheck('search', { @@ -31,21 +28,19 @@ export class ApplicationQueryParams extends PaginationAllowsAllQueryParams { search?: string; @Expose() - @ApiProperty({ + @ApiPropertyOptional({ type: String, example: 'userId', - required: false, }) @IsString({ groups: [ValidationsGroupsEnum.default] }) userId?: string; @Expose() - @ApiProperty({ + @ApiPropertyOptional({ enum: ApplicationOrderByKeys, enumName: 'ApplicationOrderByKeys', example: 'createdAt', default: 'createdAt', - required: false, }) @IsString({ groups: [ValidationsGroupsEnum.default] }) @IsEnum(ApplicationOrderByKeys, { @@ -61,12 +56,11 @@ export class ApplicationQueryParams extends PaginationAllowsAllQueryParams { orderBy?: ApplicationOrderByKeys; @Expose() - @ApiProperty({ + @ApiPropertyOptional({ enum: OrderByEnum, enumName: 'OrderByEnum', example: 'DESC', default: 'DESC', - required: false, }) @IsString({ groups: [ValidationsGroupsEnum.default] }) @IsEnum(OrderByEnum, { @@ -78,10 +72,9 @@ export class ApplicationQueryParams extends PaginationAllowsAllQueryParams { order?: OrderByEnum; @Expose() - @ApiProperty({ + @ApiPropertyOptional({ type: Boolean, example: true, - required: false, }) @IsBoolean({ groups: [ValidationsGroupsEnum.default] }) @Transform( diff --git a/backend_new/src/dtos/applications/application.dto.ts b/backend_new/src/dtos/applications/application.dto.ts index fbe6915bbe..09b1eb3b87 100644 --- a/backend_new/src/dtos/applications/application.dto.ts +++ b/backend_new/src/dtos/applications/application.dto.ts @@ -1,4 +1,4 @@ -import { ApiProperty } from '@nestjs/swagger'; +import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; import { ApplicationReviewStatusEnum, ApplicationStatusEnum, @@ -19,7 +19,7 @@ import { ValidateNested, } from 'class-validator'; import { ValidationsGroupsEnum } from '../../enums/shared/validation-groups-enum'; -import { Address } from '../addresses/address-get.dto'; +import { Address } from '../addresses/address.dto'; import { AbstractDTO } from '../shared/abstract.dto'; import { UnitType } from '../unit-types/unit-type.dto'; import { Accessibility } from './accessibility.dto'; @@ -232,13 +232,19 @@ export class Application extends AbstractDTO { @ArrayMaxSize(64, { groups: [ValidationsGroupsEnum.default] }) @ValidateNested({ groups: [ValidationsGroupsEnum.default], each: true }) @Type(() => ApplicationMultiselectQuestion) - @ApiProperty() - preferences: ApplicationMultiselectQuestion[]; + @ApiPropertyOptional({ + type: ApplicationMultiselectQuestion, + isArray: true, + }) + preferences?: ApplicationMultiselectQuestion[]; @Expose() @ArrayMaxSize(64, { groups: [ValidationsGroupsEnum.default] }) @ValidateNested({ groups: [ValidationsGroupsEnum.default], each: true }) @Type(() => ApplicationMultiselectQuestion) - @ApiProperty() + @ApiPropertyOptional({ + type: ApplicationMultiselectQuestion, + isArray: true, + }) programs?: ApplicationMultiselectQuestion[]; } diff --git a/backend_new/src/dtos/applications/household-member.dto.ts b/backend_new/src/dtos/applications/household-member.dto.ts index a6b05d807d..082219e053 100644 --- a/backend_new/src/dtos/applications/household-member.dto.ts +++ b/backend_new/src/dtos/applications/household-member.dto.ts @@ -11,7 +11,7 @@ import { ValidationsGroupsEnum } from '../../enums/shared/validation-groups-enum import { AbstractDTO } from '../shared/abstract.dto'; import { ApiProperty } from '@nestjs/swagger'; import { YesNoEnum } from '@prisma/client'; -import { Address } from '../addresses/address-get.dto'; +import { Address } from '../addresses/address.dto'; export class HouseholdMember extends AbstractDTO { @Expose() diff --git a/backend_new/src/dtos/assets/asset-create.dto.ts b/backend_new/src/dtos/assets/asset-create.dto.ts new file mode 100644 index 0000000000..ff06b9b8dc --- /dev/null +++ b/backend_new/src/dtos/assets/asset-create.dto.ts @@ -0,0 +1,8 @@ +import { OmitType } from '@nestjs/swagger'; +import { Asset } from './asset.dto'; + +export class AssetCreate extends OmitType(Asset, [ + 'id', + 'createdAt', + 'updatedAt', +]) {} diff --git a/backend_new/src/dtos/assets/asset-get.dto.ts b/backend_new/src/dtos/assets/asset.dto.ts similarity index 89% rename from backend_new/src/dtos/assets/asset-get.dto.ts rename to backend_new/src/dtos/assets/asset.dto.ts index b39d3b23f2..1d20bfd363 100644 --- a/backend_new/src/dtos/assets/asset-get.dto.ts +++ b/backend_new/src/dtos/assets/asset.dto.ts @@ -2,17 +2,20 @@ import { AbstractDTO } from '../shared/abstract.dto'; import { Expose } from 'class-transformer'; import { IsString, IsDefined, MaxLength } from 'class-validator'; import { ValidationsGroupsEnum } from '../../enums/shared/validation-groups-enum'; +import { ApiProperty } from '@nestjs/swagger'; export class Asset extends AbstractDTO { @Expose() @IsString({ groups: [ValidationsGroupsEnum.default] }) @MaxLength(128, { groups: [ValidationsGroupsEnum.default] }) @IsDefined({ groups: [ValidationsGroupsEnum.default] }) + @ApiProperty() fileId: string; @Expose() @IsString({ groups: [ValidationsGroupsEnum.default] }) @MaxLength(128, { groups: [ValidationsGroupsEnum.default] }) @IsDefined({ groups: [ValidationsGroupsEnum.default] }) + @ApiProperty() label: string; } diff --git a/backend_new/src/dtos/assets/create-presign-upload-meta-response.dto.ts b/backend_new/src/dtos/assets/create-presign-upload-meta-response.dto.ts index 81de1abd4f..e618414b0c 100644 --- a/backend_new/src/dtos/assets/create-presign-upload-meta-response.dto.ts +++ b/backend_new/src/dtos/assets/create-presign-upload-meta-response.dto.ts @@ -3,6 +3,6 @@ import { Expose } from 'class-transformer'; export class CreatePresignedUploadMetadataResponse { @Expose() - @ApiProperty({ required: true }) + @ApiProperty() signature: string; } diff --git a/backend_new/src/dtos/assets/create-presigned-upload-meta.dto.ts b/backend_new/src/dtos/assets/create-presigned-upload-meta.dto.ts index a5a40e711b..59d6a2b8f7 100644 --- a/backend_new/src/dtos/assets/create-presigned-upload-meta.dto.ts +++ b/backend_new/src/dtos/assets/create-presigned-upload-meta.dto.ts @@ -6,6 +6,6 @@ import { ValidationsGroupsEnum } from '../../enums/shared/validation-groups-enum export class CreatePresignedUploadMetadata { @Expose() @IsDefined({ groups: [ValidationsGroupsEnum.default] }) - @ApiProperty({ required: true }) + @ApiProperty() parametersToSign: Record; } diff --git a/backend_new/src/dtos/jurisdictions/jurisdiction.dto.ts b/backend_new/src/dtos/jurisdictions/jurisdiction.dto.ts index 0a2ec04b54..b282960391 100644 --- a/backend_new/src/dtos/jurisdictions/jurisdiction.dto.ts +++ b/backend_new/src/dtos/jurisdictions/jurisdiction.dto.ts @@ -12,7 +12,7 @@ import { import { ValidationsGroupsEnum } from '../../enums/shared/validation-groups-enum'; import { LanguagesEnum } from '@prisma/client'; import { Expose, Type } from 'class-transformer'; -import { ApiProperty } from '@nestjs/swagger'; +import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; import { IdDTO } from '../shared/id.dto'; export class Jurisdiction extends AbstractDTO { @@ -25,7 +25,7 @@ export class Jurisdiction extends AbstractDTO { @Expose() @IsString({ groups: [ValidationsGroupsEnum.default] }) - @ApiProperty() + @ApiPropertyOptional() notificationsSignUpUrl?: string; @Expose() @@ -36,19 +36,23 @@ export class Jurisdiction extends AbstractDTO { each: true, }) @IsDefined({ groups: [ValidationsGroupsEnum.default] }) - @ApiProperty() + @ApiProperty({ + enum: LanguagesEnum, + enumName: 'LanguagesEnum', + isArray: true, + }) languages: LanguagesEnum[]; @Expose() @ValidateNested({ groups: [ValidationsGroupsEnum.default] }) @Type(() => IdDTO) @IsDefined({ groups: [ValidationsGroupsEnum.default] }) - @ApiProperty() + @ApiProperty({ type: IdDTO, isArray: true }) multiselectQuestions: IdDTO[]; @Expose() @IsString({ groups: [ValidationsGroupsEnum.default] }) - @ApiProperty() + @ApiPropertyOptional() partnerTerms?: string; @Expose() @@ -71,7 +75,7 @@ export class Jurisdiction extends AbstractDTO { @Expose() @IsBoolean({ groups: [ValidationsGroupsEnum.default] }) - @ApiProperty() + @ApiPropertyOptional() enablePartnerSettings?: boolean; @Expose() diff --git a/backend_new/src/dtos/listings/listing-create.dto.ts b/backend_new/src/dtos/listings/listing-create.dto.ts new file mode 100644 index 0000000000..9adbd0ebb2 --- /dev/null +++ b/backend_new/src/dtos/listings/listing-create.dto.ts @@ -0,0 +1,4 @@ +import { OmitType } from '@nestjs/swagger'; +import { ListingUpdate } from './listing-update.dto'; + +export class ListingCreate extends OmitType(ListingUpdate, ['id']) {} diff --git a/backend_new/src/dtos/listings/listing-event-create.dto.ts b/backend_new/src/dtos/listings/listing-event-create.dto.ts new file mode 100644 index 0000000000..76b61e8777 --- /dev/null +++ b/backend_new/src/dtos/listings/listing-event-create.dto.ts @@ -0,0 +1,19 @@ +import { ApiPropertyOptional, OmitType } from '@nestjs/swagger'; +import { Expose, Type } from 'class-transformer'; +import { ValidateNested } from 'class-validator'; +import { ValidationsGroupsEnum } from '../../enums/shared/validation-groups-enum'; +import { AssetCreate } from '../assets/asset-create.dto'; +import { ListingEvent } from './listing-event.dto'; + +export class ListingEventCreate extends OmitType(ListingEvent, [ + 'id', + 'createdAt', + 'updatedAt', + 'assets', +]) { + @Expose() + @ValidateNested({ groups: [ValidationsGroupsEnum.default] }) + @Type(() => AssetCreate) + @ApiPropertyOptional({ type: AssetCreate }) + assets?: AssetCreate; +} diff --git a/backend_new/src/dtos/listings/listing-event.dto.ts b/backend_new/src/dtos/listings/listing-event.dto.ts index ff480c7a48..494b54c76f 100644 --- a/backend_new/src/dtos/listings/listing-event.dto.ts +++ b/backend_new/src/dtos/listings/listing-event.dto.ts @@ -8,9 +8,9 @@ import { } from 'class-validator'; import { ValidationsGroupsEnum } from '../../enums/shared/validation-groups-enum'; import { ListingEventsTypeEnum } from '@prisma/client'; -import { ApiProperty } from '@nestjs/swagger'; +import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; import { AbstractDTO } from '../shared/abstract.dto'; -import { Asset } from '../assets/asset-get.dto'; +import { Asset } from '../assets/asset.dto'; export class ListingEvent extends AbstractDTO { @Expose() @@ -25,32 +25,39 @@ export class ListingEvent extends AbstractDTO { @Expose() @IsDate({ groups: [ValidationsGroupsEnum.default] }) @Type(() => Date) + @ApiPropertyOptional() startDate?: Date; @Expose() @IsDate({ groups: [ValidationsGroupsEnum.default] }) @Type(() => Date) + @ApiPropertyOptional() startTime?: Date; @Expose() @IsDate({ groups: [ValidationsGroupsEnum.default] }) @Type(() => Date) + @ApiPropertyOptional() endTime?: Date; @Expose() @IsString({ groups: [ValidationsGroupsEnum.default] }) + @ApiPropertyOptional() url?: string; @Expose() @IsString({ groups: [ValidationsGroupsEnum.default] }) + @ApiPropertyOptional() note?: string; @Expose() @IsString({ groups: [ValidationsGroupsEnum.default] }) + @ApiPropertyOptional() label?: string; @Expose() @ValidateNested({ groups: [ValidationsGroupsEnum.default] }) @Type(() => Asset) + @ApiPropertyOptional({ type: Asset }) assets?: Asset; } diff --git a/backend_new/src/dtos/listings/listing-feature-create.dto.ts b/backend_new/src/dtos/listings/listing-feature-create.dto.ts new file mode 100644 index 0000000000..b4e939b573 --- /dev/null +++ b/backend_new/src/dtos/listings/listing-feature-create.dto.ts @@ -0,0 +1,8 @@ +import { OmitType } from '@nestjs/swagger'; +import { ListingFeatures } from './listing-feature.dto'; + +export class ListingFeaturesCreate extends OmitType(ListingFeatures, [ + 'id', + 'createdAt', + 'updatedAt', +]) {} diff --git a/backend_new/src/dtos/listings/listing-feature.dto.ts b/backend_new/src/dtos/listings/listing-feature.dto.ts index 3f819058f9..50349b940b 100644 --- a/backend_new/src/dtos/listings/listing-feature.dto.ts +++ b/backend_new/src/dtos/listings/listing-feature.dto.ts @@ -1,3 +1,4 @@ +import { ApiPropertyOptional } from '@nestjs/swagger'; import { Expose } from 'class-transformer'; import { IsBoolean } from 'class-validator'; import { ValidationsGroupsEnum } from '../../enums/shared/validation-groups-enum'; @@ -6,61 +7,76 @@ import { AbstractDTO } from '../shared/abstract.dto'; export class ListingFeatures extends AbstractDTO { @Expose() @IsBoolean({ groups: [ValidationsGroupsEnum.default] }) + @ApiPropertyOptional() elevator?: boolean; @Expose() @IsBoolean({ groups: [ValidationsGroupsEnum.default] }) + @ApiPropertyOptional() wheelchairRamp?: boolean; @Expose() @IsBoolean({ groups: [ValidationsGroupsEnum.default] }) + @ApiPropertyOptional() serviceAnimalsAllowed?: boolean; @Expose() @IsBoolean({ groups: [ValidationsGroupsEnum.default] }) + @ApiPropertyOptional() accessibleParking?: boolean; @Expose() @IsBoolean({ groups: [ValidationsGroupsEnum.default] }) + @ApiPropertyOptional() parkingOnSite?: boolean; @Expose() @IsBoolean({ groups: [ValidationsGroupsEnum.default] }) + @ApiPropertyOptional() inUnitWasherDryer?: boolean; @Expose() @IsBoolean({ groups: [ValidationsGroupsEnum.default] }) + @ApiPropertyOptional() laundryInBuilding?: boolean; @Expose() @IsBoolean({ groups: [ValidationsGroupsEnum.default] }) + @ApiPropertyOptional() barrierFreeEntrance?: boolean; @Expose() @IsBoolean({ groups: [ValidationsGroupsEnum.default] }) + @ApiPropertyOptional() rollInShower?: boolean; @Expose() @IsBoolean({ groups: [ValidationsGroupsEnum.default] }) + @ApiPropertyOptional() grabBars?: boolean; @Expose() @IsBoolean({ groups: [ValidationsGroupsEnum.default] }) + @ApiPropertyOptional() heatingInUnit?: boolean; @Expose() @IsBoolean({ groups: [ValidationsGroupsEnum.default] }) + @ApiPropertyOptional() acInUnit?: boolean; @Expose() @IsBoolean({ groups: [ValidationsGroupsEnum.default] }) + @ApiPropertyOptional() hearing?: boolean; @Expose() @IsBoolean({ groups: [ValidationsGroupsEnum.default] }) + @ApiPropertyOptional() visual?: boolean; @Expose() @IsBoolean({ groups: [ValidationsGroupsEnum.default] }) + @ApiPropertyOptional() mobility?: boolean; } diff --git a/backend_new/src/dtos/listings/listing-image-create.dto.ts b/backend_new/src/dtos/listings/listing-image-create.dto.ts new file mode 100644 index 0000000000..9390ea5979 --- /dev/null +++ b/backend_new/src/dtos/listings/listing-image-create.dto.ts @@ -0,0 +1,15 @@ +import { ApiProperty, OmitType } from '@nestjs/swagger'; +import { Expose, Type } from 'class-transformer'; +import { IsDefined, ValidateNested } from 'class-validator'; +import { ValidationsGroupsEnum } from '../../enums/shared/validation-groups-enum'; +import { AssetCreate } from '../assets/asset-create.dto'; +import { ListingImage } from './listing-image.dto'; + +export class ListingImageCreate extends OmitType(ListingImage, ['assets']) { + @Expose() + @ValidateNested({ groups: [ValidationsGroupsEnum.default], each: true }) + @Type(() => AssetCreate) + @IsDefined({ groups: [ValidationsGroupsEnum.default] }) + @ApiProperty({ type: AssetCreate }) + assets: AssetCreate; +} diff --git a/backend_new/src/dtos/listings/listing-image.dto.ts b/backend_new/src/dtos/listings/listing-image.dto.ts index f10b70f184..73d90e8143 100644 --- a/backend_new/src/dtos/listings/listing-image.dto.ts +++ b/backend_new/src/dtos/listings/listing-image.dto.ts @@ -1,15 +1,18 @@ +import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; import { Expose, Type } from 'class-transformer'; import { IsNumber, IsDefined } from 'class-validator'; import { ValidationsGroupsEnum } from '../../enums/shared/validation-groups-enum'; -import { Asset } from '../assets/asset-get.dto'; +import { Asset } from '../assets/asset.dto'; export class ListingImage { @Expose() @Type(() => Asset) @IsDefined({ groups: [ValidationsGroupsEnum.default] }) + @ApiProperty({ type: Asset }) assets: Asset; @Expose() @IsNumber({}, { groups: [ValidationsGroupsEnum.default] }) + @ApiPropertyOptional() ordinal?: number; } diff --git a/backend_new/src/dtos/listings/listing-multiselect-question.dto.ts b/backend_new/src/dtos/listings/listing-multiselect-question.dto.ts index 8eeee3fc54..44be13d1f7 100644 --- a/backend_new/src/dtos/listings/listing-multiselect-question.dto.ts +++ b/backend_new/src/dtos/listings/listing-multiselect-question.dto.ts @@ -1,18 +1,18 @@ -import { MultiselectQuestion } from '../multiselect-questions/multiselect-question-get.dto'; +import { MultiselectQuestion } from '../multiselect-questions/multiselect-question.dto'; import { Expose, Type } from 'class-transformer'; import { IsNumber, IsDefined } from 'class-validator'; import { ValidationsGroupsEnum } from '../../enums/shared/validation-groups-enum'; -import { ApiProperty } from '@nestjs/swagger'; +import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; export class ListingMultiselectQuestion { @Expose() @Type(() => MultiselectQuestion) @IsDefined({ groups: [ValidationsGroupsEnum.default] }) - @ApiProperty() + @ApiProperty({ type: MultiselectQuestion }) multiselectQuestions: MultiselectQuestion; @Expose() @IsNumber({}, { groups: [ValidationsGroupsEnum.default] }) - @ApiProperty() + @ApiPropertyOptional() ordinal?: number; } diff --git a/backend_new/src/dtos/listings/listing-published-create.dto.ts b/backend_new/src/dtos/listings/listing-published-create.dto.ts new file mode 100644 index 0000000000..6afee389ac --- /dev/null +++ b/backend_new/src/dtos/listings/listing-published-create.dto.ts @@ -0,0 +1,6 @@ +import { OmitType } from '@nestjs/swagger'; +import { ListingPublishedUpdate } from './listing-published-update.dto'; + +export class ListingPublishedCreate extends OmitType(ListingPublishedUpdate, [ + 'id', +]) {} diff --git a/backend_new/src/dtos/listings/listing-published-update.dto.ts b/backend_new/src/dtos/listings/listing-published-update.dto.ts new file mode 100644 index 0000000000..1e312ddf87 --- /dev/null +++ b/backend_new/src/dtos/listings/listing-published-update.dto.ts @@ -0,0 +1,141 @@ +import { OmitType } from '@nestjs/swagger'; +import { Expose, Type } from 'class-transformer'; +import { + ArrayMaxSize, + ArrayMinSize, + IsBoolean, + IsDefined, + IsEmail, + IsEnum, + IsPhoneNumber, + IsString, + MaxLength, + ValidateNested, +} from 'class-validator'; +import { ApiProperty } from '@nestjs/swagger'; +import { ValidationsGroupsEnum } from '../../enums/shared/validation-groups-enum'; +import { ListingUpdate } from './listing-update.dto'; +import { UnitCreate } from '../units/unit-create.dto'; +import { AssetCreate } from '../assets/asset-create.dto'; +import { ListingImageCreate } from './listing-image-create.dto'; +import { AddressCreate } from '../addresses/address-create.dto'; +import { EnforceLowerCase } from '../../decorators/enforce-lower-case.decorator'; +import { ReviewOrderTypeEnum } from '@prisma/client'; + +export class ListingPublishedUpdate extends OmitType(ListingUpdate, [ + 'assets', + 'depositMax', + 'depositMin', + 'developer', + 'digitalApplication', + 'listingImages', + 'leasingAgentEmail', + 'leasingAgentName', + 'leasingAgentPhone', + 'name', + 'paperApplication', + 'referralOpportunity', + 'rentalAssistance', + 'reviewOrderType', + 'units', + 'listingsBuildingAddress', +]) { + @Expose() + @ValidateNested({ groups: [ValidationsGroupsEnum.default], each: true }) + @Type(() => AssetCreate) + @IsDefined({ groups: [ValidationsGroupsEnum.default] }) + @ApiProperty({ type: AssetCreate, isArray: true }) + assets: AssetCreate[]; + + @Expose() + @ValidateNested({ groups: [ValidationsGroupsEnum.default] }) + @Type(() => AddressCreate) + @ApiProperty({ type: AddressCreate }) + listingsBuildingAddress: AddressCreate; + + @Expose() + @IsString({ groups: [ValidationsGroupsEnum.default] }) + @MaxLength(64, { groups: [ValidationsGroupsEnum.default] }) + @ApiProperty() + depositMin: string; + + @Expose() + @IsString({ groups: [ValidationsGroupsEnum.default] }) + @MaxLength(64, { groups: [ValidationsGroupsEnum.default] }) + @ApiProperty() + depositMax: string; + + @Expose() + @IsString({ groups: [ValidationsGroupsEnum.default] }) + @MaxLength(256, { groups: [ValidationsGroupsEnum.default] }) + @ApiProperty() + developer: string; + + @Expose() + @IsBoolean({ groups: [ValidationsGroupsEnum.default] }) + @ApiProperty() + digitalApplication: boolean; + + @Expose() + @ValidateNested({ groups: [ValidationsGroupsEnum.default], each: true }) + @Type(() => ListingImageCreate) + @ApiProperty({ type: ListingImageCreate, isArray: true }) + listingImages: ListingImageCreate[]; + + @Expose() + @IsEmail({}, { groups: [ValidationsGroupsEnum.default] }) + @EnforceLowerCase() + @ApiProperty() + leasingAgentEmail: string; + + @Expose() + @IsString({ groups: [ValidationsGroupsEnum.default] }) + @MaxLength(256, { groups: [ValidationsGroupsEnum.default] }) + @ApiProperty() + leasingAgentName: string; + + @Expose() + @IsString({ groups: [ValidationsGroupsEnum.default] }) + @IsPhoneNumber('US', { groups: [ValidationsGroupsEnum.default] }) + @MaxLength(64, { groups: [ValidationsGroupsEnum.default] }) + @ApiProperty() + leasingAgentPhone: string; + + @Expose() + @IsString({ groups: [ValidationsGroupsEnum.default] }) + @MaxLength(256, { groups: [ValidationsGroupsEnum.default] }) + @ApiProperty() + name: string; + + @Expose() + @IsBoolean({ groups: [ValidationsGroupsEnum.default] }) + @ApiProperty() + paperApplication: boolean; + + @Expose() + @IsBoolean({ groups: [ValidationsGroupsEnum.default] }) + @ApiProperty() + referralOpportunity: boolean; + + @Expose() + @IsString({ groups: [ValidationsGroupsEnum.default] }) + @MaxLength(4096, { groups: [ValidationsGroupsEnum.default] }) + @ApiProperty() + rentalAssistance: string; + + @Expose() + @IsEnum(ReviewOrderTypeEnum, { groups: [ValidationsGroupsEnum.default] }) + @ApiProperty({ + enum: ReviewOrderTypeEnum, + enumName: 'ReviewOrderTypeEnum', + }) + reviewOrderType: ReviewOrderTypeEnum; + + @Expose() + @ApiProperty({ isArray: true, type: UnitCreate }) + @ValidateNested({ groups: [ValidationsGroupsEnum.default], each: true }) + @ArrayMinSize(1, { groups: [ValidationsGroupsEnum.default] }) + @ArrayMaxSize(256, { groups: [ValidationsGroupsEnum.default] }) + @Type(() => UnitCreate) + units: UnitCreate[]; +} diff --git a/backend_new/src/dtos/listings/listing-update.dto.ts b/backend_new/src/dtos/listings/listing-update.dto.ts new file mode 100644 index 0000000000..2390543263 --- /dev/null +++ b/backend_new/src/dtos/listings/listing-update.dto.ts @@ -0,0 +1,149 @@ +import { ApiPropertyOptional, OmitType } from '@nestjs/swagger'; +import { Expose, Type } from 'class-transformer'; +import { IsDefined, ValidateNested } from 'class-validator'; +import { ApiProperty } from '@nestjs/swagger'; +import { ValidationsGroupsEnum } from '../../enums/shared/validation-groups-enum'; +import { IdDTO } from '../shared/id.dto'; +import { Listing } from './listing.dto'; +import { UnitCreate } from '../units/unit-create.dto'; +import { ApplicationMethodCreate } from '../application-methods/application-method-create.dto'; +import { AssetCreate } from '../assets/asset-create.dto'; +import { UnitsSummaryCreate } from '../units/units-summary-create.dto'; +import { ListingImageCreate } from './listing-image-create.dto'; +import { AddressCreate } from '../addresses/address-create.dto'; +import { ListingEventCreate } from './listing-event-create.dto'; +import { ListingFeaturesCreate } from './listing-feature-create.dto'; +import { ListingUtilitiesCreate } from './listing-utility-create.dto'; + +export class ListingUpdate extends OmitType(Listing, [ + // fields get their type changed + 'listingMultiselectQuestions', + 'units', + 'applicationMethods', + 'assets', + 'unitsSummary', + 'listingImages', + 'listingsResult', + 'listingsApplicationPickUpAddress', + 'listingsApplicationMailingAddress', + 'listingsApplicationDropOffAddress', + 'listingsLeasingAgentAddress', + 'listingsBuildingAddress', + 'listingsBuildingSelectionCriteriaFile', + 'listingEvents', + 'listingFeatures', + 'listingUtilities', + + // fields removed entirely + 'createdAt', + 'updatedAt', + 'referralApplication', + 'publishedAt', + 'showWaitlist', + 'unitsSummarized', + 'closedAt', + 'afsLastRunAt', + 'urlSlug', + 'applicationConfig', +]) { + @Expose() + @ValidateNested({ groups: [ValidationsGroupsEnum.default], each: true }) + @Type(() => IdDTO) + @ApiPropertyOptional({ type: IdDTO, isArray: true }) + listingMultiselectQuestions?: IdDTO[]; + + @Expose() + @ValidateNested({ groups: [ValidationsGroupsEnum.default], each: true }) + @Type(() => UnitCreate) + @ApiPropertyOptional({ type: UnitCreate, isArray: true }) + units?: UnitCreate[]; + + @Expose() + @ValidateNested({ groups: [ValidationsGroupsEnum.default], each: true }) + @Type(() => ApplicationMethodCreate) + @ApiPropertyOptional({ + type: ApplicationMethodCreate, + isArray: true, + }) + applicationMethods?: ApplicationMethodCreate[]; + + @Expose() + @ValidateNested({ groups: [ValidationsGroupsEnum.default], each: true }) + @Type(() => AssetCreate) + @IsDefined({ groups: [ValidationsGroupsEnum.default] }) + @ApiProperty({ type: AssetCreate, isArray: true }) + assets: AssetCreate[]; + + @Expose() + @ValidateNested({ groups: [ValidationsGroupsEnum.default], each: true }) + @ApiProperty({ type: UnitsSummaryCreate, isArray: true }) + @Type(() => UnitsSummaryCreate) + unitsSummary: UnitsSummaryCreate[]; + + @Expose() + @ValidateNested({ groups: [ValidationsGroupsEnum.default], each: true }) + @Type(() => ListingImageCreate) + @ApiPropertyOptional({ type: ListingImageCreate, isArray: true }) + listingImages?: ListingImageCreate[]; + + @Expose() + @ValidateNested({ groups: [ValidationsGroupsEnum.default] }) + @Type(() => AddressCreate) + @ApiPropertyOptional({ type: AddressCreate }) + listingsApplicationPickUpAddress?: AddressCreate; + + @Expose() + @ValidateNested({ groups: [ValidationsGroupsEnum.default] }) + @Type(() => AddressCreate) + @ApiPropertyOptional({ type: AddressCreate }) + listingsApplicationMailingAddress?: AddressCreate; + + @Expose() + @ValidateNested({ groups: [ValidationsGroupsEnum.default] }) + @Type(() => AddressCreate) + @ApiPropertyOptional({ type: AddressCreate }) + listingsApplicationDropOffAddress?: AddressCreate; + + @Expose() + @ValidateNested({ groups: [ValidationsGroupsEnum.default] }) + @Type(() => AddressCreate) + @ApiPropertyOptional({ type: AddressCreate }) + listingsLeasingAgentAddress?: AddressCreate; + + @Expose() + @ValidateNested({ groups: [ValidationsGroupsEnum.default] }) + @Type(() => AddressCreate) + @ApiPropertyOptional({ type: AddressCreate }) + listingsBuildingAddress?: AddressCreate; + + @Expose() + @ValidateNested({ groups: [ValidationsGroupsEnum.default] }) + @Type(() => AssetCreate) + @ApiPropertyOptional({ type: AssetCreate }) + listingsBuildingSelectionCriteriaFile?: AssetCreate; + + @Expose() + @ValidateNested({ groups: [ValidationsGroupsEnum.default] }) + @Type(() => AssetCreate) + @ApiPropertyOptional({ type: AssetCreate }) + listingsResult?: AssetCreate; + + @Expose() + @ValidateNested({ groups: [ValidationsGroupsEnum.default], each: true }) + @Type(() => ListingEventCreate) + @IsDefined({ groups: [ValidationsGroupsEnum.default] }) + @ApiProperty({ type: ListingEventCreate, isArray: true }) + listingEvents: ListingEventCreate[]; + + @Expose() + @ValidateNested({ groups: [ValidationsGroupsEnum.default], each: true }) + @Type(() => ListingFeaturesCreate) + @ApiPropertyOptional({ type: ListingFeaturesCreate }) + listingFeatures?: ListingFeaturesCreate; + + @Expose() + @ValidateNested({ groups: [ValidationsGroupsEnum.default], each: true }) + @Type(() => ListingUtilitiesCreate) + @ApiPropertyOptional({ type: ListingUtilitiesCreate }) + listingUtilities?: ListingUtilitiesCreate; +} diff --git a/backend_new/src/dtos/listings/listing-utility-create.dto.ts b/backend_new/src/dtos/listings/listing-utility-create.dto.ts new file mode 100644 index 0000000000..ed2d2e7487 --- /dev/null +++ b/backend_new/src/dtos/listings/listing-utility-create.dto.ts @@ -0,0 +1,8 @@ +import { OmitType } from '@nestjs/swagger'; +import { ListingUtilities } from './listing-utility.dto'; + +export class ListingUtilitiesCreate extends OmitType(ListingUtilities, [ + 'id', + 'createdAt', + 'updatedAt', +]) {} diff --git a/backend_new/src/dtos/listings/listing-utility.dto.ts b/backend_new/src/dtos/listings/listing-utility.dto.ts index e84324c188..b18ce5ec3d 100644 --- a/backend_new/src/dtos/listings/listing-utility.dto.ts +++ b/backend_new/src/dtos/listings/listing-utility.dto.ts @@ -1,3 +1,4 @@ +import { ApiPropertyOptional } from '@nestjs/swagger'; import { Expose } from 'class-transformer'; import { IsBoolean } from 'class-validator'; import { ValidationsGroupsEnum } from '../../enums/shared/validation-groups-enum'; @@ -6,33 +7,41 @@ import { AbstractDTO } from '../shared/abstract.dto'; export class ListingUtilities extends AbstractDTO { @Expose() @IsBoolean({ groups: [ValidationsGroupsEnum.default] }) + @ApiPropertyOptional() water?: boolean; @Expose() @IsBoolean({ groups: [ValidationsGroupsEnum.default] }) + @ApiPropertyOptional() gas?: boolean; @Expose() @IsBoolean({ groups: [ValidationsGroupsEnum.default] }) + @ApiPropertyOptional() trash?: boolean; @Expose() @IsBoolean({ groups: [ValidationsGroupsEnum.default] }) + @ApiPropertyOptional() sewer?: boolean; @Expose() @IsBoolean({ groups: [ValidationsGroupsEnum.default] }) + @ApiPropertyOptional() electricity?: boolean; @Expose() @IsBoolean({ groups: [ValidationsGroupsEnum.default] }) + @ApiPropertyOptional() cable?: boolean; @Expose() @IsBoolean({ groups: [ValidationsGroupsEnum.default] }) + @ApiPropertyOptional() phone?: boolean; @Expose() @IsBoolean({ groups: [ValidationsGroupsEnum.default] }) + @ApiPropertyOptional() internet?: boolean; } diff --git a/backend_new/src/dtos/listings/listing-get.dto.ts b/backend_new/src/dtos/listings/listing.dto.ts similarity index 78% rename from backend_new/src/dtos/listings/listing-get.dto.ts rename to backend_new/src/dtos/listings/listing.dto.ts index 955f13b13b..5f81a6e543 100644 --- a/backend_new/src/dtos/listings/listing-get.dto.ts +++ b/backend_new/src/dtos/listings/listing.dto.ts @@ -1,4 +1,4 @@ -import { Expose, Type } from 'class-transformer'; +import { Expose, Transform, TransformFnParams, Type } from 'class-transformer'; import { IsBoolean, IsDate, @@ -21,142 +21,142 @@ import { } from '@prisma/client'; import { EnforceLowerCase } from '../../decorators/enforce-lower-case.decorator'; import { ListingMultiselectQuestion } from './listing-multiselect-question.dto'; -import { ApplicationMethod } from '../application-methods/application-method-get.dto'; -import { Asset } from '../assets/asset-get.dto'; +import { ApplicationMethod } from '../application-methods/application-method.dto'; +import { Asset } from '../assets/asset.dto'; import { ListingEvent } from './listing-event.dto'; -import { Address } from '../addresses/address-get.dto'; -import { Jurisdiction } from '../jurisdictions/jurisdiction.dto'; -import { ReservedCommunityType } from '../reserved-community-types/reserved-community-type.dto'; +import { Address } from '../addresses/address.dto'; import { ListingImage } from './listing-image.dto'; import { ListingFeatures } from './listing-feature.dto'; import { ListingUtilities } from './listing-utility.dto'; -import { Unit } from '../units/unit-get.dto'; +import { Unit } from '../units/unit.dto'; import { UnitsSummarized } from '../units/unit-summarized.dto'; -import { UnitsSummary } from '../units/units-summery-get.dto'; +import { UnitsSummary } from '../units/units-summary.dto'; +import { IdDTO } from '../shared/id.dto'; +import { listingUrlSlug } from '../../utilities/listing-url-slug'; -class ListingGet extends AbstractDTO { +class Listing extends AbstractDTO { @Expose() @IsString({ groups: [ValidationsGroupsEnum.default] }) - @ApiProperty() + @ApiPropertyOptional() additionalApplicationSubmissionNotes?: string; @Expose() @IsBoolean({ groups: [ValidationsGroupsEnum.default] }) - @ApiProperty() + @ApiPropertyOptional() digitalApplication?: boolean; @Expose() @IsBoolean({ groups: [ValidationsGroupsEnum.default] }) - @ApiProperty() + @ApiPropertyOptional() commonDigitalApplication?: boolean; @Expose() @IsBoolean({ groups: [ValidationsGroupsEnum.default] }) - @ApiProperty() + @ApiPropertyOptional() paperApplication?: boolean; @Expose() @IsBoolean({ groups: [ValidationsGroupsEnum.default] }) - @ApiProperty() + @ApiPropertyOptional() referralOpportunity?: boolean; @Expose() @IsString({ groups: [ValidationsGroupsEnum.default] }) - @ApiProperty() + @ApiPropertyOptional() accessibility?: string; @Expose() @IsString({ groups: [ValidationsGroupsEnum.default] }) - @ApiProperty() + @ApiPropertyOptional() amenities?: string; @Expose() @IsNumber({}, { groups: [ValidationsGroupsEnum.default] }) - @ApiProperty() + @ApiPropertyOptional() buildingTotalUnits?: number; @Expose() @IsString({ groups: [ValidationsGroupsEnum.default] }) - @ApiProperty() + @ApiPropertyOptional() developer?: string; @Expose() @IsNumber({}, { groups: [ValidationsGroupsEnum.default] }) - @ApiProperty() + @ApiPropertyOptional() householdSizeMax?: number; @Expose() @IsNumber({}, { groups: [ValidationsGroupsEnum.default] }) - @ApiProperty() + @ApiPropertyOptional() householdSizeMin?: number; @Expose() @IsString({ groups: [ValidationsGroupsEnum.default] }) - @ApiProperty() + @ApiPropertyOptional() neighborhood?: string; @Expose() @IsString({ groups: [ValidationsGroupsEnum.default] }) - @ApiProperty() + @ApiPropertyOptional() petPolicy?: string; @Expose() @IsString({ groups: [ValidationsGroupsEnum.default] }) - @ApiProperty() + @ApiPropertyOptional() smokingPolicy?: string; @Expose() @IsNumber({}, { groups: [ValidationsGroupsEnum.default] }) - @ApiProperty() + @ApiPropertyOptional() unitsAvailable?: number; @Expose() @IsString({ groups: [ValidationsGroupsEnum.default] }) - @ApiProperty() + @ApiPropertyOptional() unitAmenities?: string; @Expose() @IsString({ groups: [ValidationsGroupsEnum.default] }) - @ApiProperty() + @ApiPropertyOptional() servicesOffered?: string; @Expose() @IsNumber({}, { groups: [ValidationsGroupsEnum.default] }) - @ApiProperty() + @ApiPropertyOptional() yearBuilt?: number; @Expose() @IsDate({ groups: [ValidationsGroupsEnum.default] }) @Type(() => Date) - @ApiProperty() + @ApiPropertyOptional() applicationDueDate?: Date; @Expose() @IsDate({ groups: [ValidationsGroupsEnum.default] }) @Type(() => Date) - @ApiProperty() + @ApiPropertyOptional() applicationOpenDate?: Date; @Expose() @IsString({ groups: [ValidationsGroupsEnum.default] }) - @ApiProperty() + @ApiPropertyOptional() applicationFee?: string; @Expose() @IsString({ groups: [ValidationsGroupsEnum.default] }) - @ApiProperty() + @ApiPropertyOptional() applicationOrganization?: string; @Expose() @IsString({ groups: [ValidationsGroupsEnum.default] }) - @ApiProperty() + @ApiPropertyOptional() applicationPickUpAddressOfficeHours?: string; @Expose() @IsEnum(ApplicationAddressTypeEnum, { groups: [ValidationsGroupsEnum.default], }) - @ApiProperty({ + @ApiPropertyOptional({ enum: ApplicationAddressTypeEnum, enumName: 'ApplicationAddressTypeEnum', }) @@ -164,14 +164,14 @@ class ListingGet extends AbstractDTO { @Expose() @IsString({ groups: [ValidationsGroupsEnum.default] }) - @ApiProperty() + @ApiPropertyOptional() applicationDropOffAddressOfficeHours?: string; @Expose() @IsEnum(ApplicationAddressTypeEnum, { groups: [ValidationsGroupsEnum.default], }) - @ApiProperty({ + @ApiPropertyOptional({ enum: ApplicationAddressTypeEnum, enumName: 'ApplicationAddressTypeEnum', }) @@ -181,7 +181,7 @@ class ListingGet extends AbstractDTO { @IsEnum(ApplicationAddressTypeEnum, { groups: [ValidationsGroupsEnum.default], }) - @ApiProperty({ + @ApiPropertyOptional({ enum: ApplicationAddressTypeEnum, enumName: 'ApplicationAddressTypeEnum', }) @@ -189,68 +189,68 @@ class ListingGet extends AbstractDTO { @Expose() @IsString({ groups: [ValidationsGroupsEnum.default] }) - @ApiProperty() + @ApiPropertyOptional() buildingSelectionCriteria?: string; @Expose() @IsString({ groups: [ValidationsGroupsEnum.default] }) - @ApiProperty() + @ApiPropertyOptional() costsNotIncluded?: string; @Expose() @IsString({ groups: [ValidationsGroupsEnum.default] }) - @ApiProperty() + @ApiPropertyOptional() creditHistory?: string; @Expose() @IsString({ groups: [ValidationsGroupsEnum.default] }) - @ApiProperty() + @ApiPropertyOptional() criminalBackground?: string; @Expose() @IsString({ groups: [ValidationsGroupsEnum.default] }) - @ApiProperty() + @ApiPropertyOptional() depositMin?: string; @Expose() @IsString({ groups: [ValidationsGroupsEnum.default] }) - @ApiProperty() + @ApiPropertyOptional() depositMax?: string; @Expose() @IsString({ groups: [ValidationsGroupsEnum.default] }) - @ApiProperty() + @ApiPropertyOptional() depositHelperText?: string; @Expose() @IsBoolean({ groups: [ValidationsGroupsEnum.default] }) - @ApiProperty() + @ApiPropertyOptional() disableUnitsAccordion?: boolean; @Expose() @IsEmail({}, { groups: [ValidationsGroupsEnum.default] }) @EnforceLowerCase() - @ApiProperty() + @ApiPropertyOptional() leasingAgentEmail?: string; @Expose() @IsString({ groups: [ValidationsGroupsEnum.default] }) - @ApiProperty() + @ApiPropertyOptional() leasingAgentName?: string; @Expose() @IsString({ groups: [ValidationsGroupsEnum.default] }) - @ApiProperty() + @ApiPropertyOptional() leasingAgentOfficeHours?: string; @Expose() @IsString({ groups: [ValidationsGroupsEnum.default] }) - @ApiProperty() + @ApiPropertyOptional() leasingAgentPhone?: string; @Expose() @IsString({ groups: [ValidationsGroupsEnum.default] }) - @ApiProperty() + @ApiPropertyOptional() leasingAgentTitle?: string; @Expose() @@ -262,64 +262,67 @@ class ListingGet extends AbstractDTO { @Expose() @IsDate({ groups: [ValidationsGroupsEnum.default] }) @Type(() => Date) - @ApiProperty() + @ApiPropertyOptional() postmarkedApplicationsReceivedByDate?: Date; @Expose() @IsString({ groups: [ValidationsGroupsEnum.default] }) - @ApiProperty() + @ApiPropertyOptional() programRules?: string; @Expose() @IsString({ groups: [ValidationsGroupsEnum.default] }) - @ApiProperty() + @ApiPropertyOptional() rentalAssistance?: string; @Expose() @IsString({ groups: [ValidationsGroupsEnum.default] }) - @ApiProperty() + @ApiPropertyOptional() rentalHistory?: string; @Expose() @IsString({ groups: [ValidationsGroupsEnum.default] }) - @ApiProperty() + @ApiPropertyOptional() requiredDocuments?: string; @Expose() @IsString({ groups: [ValidationsGroupsEnum.default] }) - @ApiProperty() + @ApiPropertyOptional() specialNotes?: string; @Expose() @IsNumber({}, { groups: [ValidationsGroupsEnum.default] }) - @ApiProperty() + @ApiPropertyOptional() waitlistCurrentSize?: number; @Expose() @IsNumber({}, { groups: [ValidationsGroupsEnum.default] }) - @ApiProperty() + @ApiPropertyOptional() waitlistMaxSize?: number; @Expose() @IsString({ groups: [ValidationsGroupsEnum.default] }) - @ApiProperty() + @ApiPropertyOptional() whatToExpect?: string; @Expose() @IsEnum(ListingsStatusEnum, { groups: [ValidationsGroupsEnum.default] }) - @ApiProperty({ enum: ListingsStatusEnum, enumName: 'ListingsStatusEnum' }) + @ApiProperty({ + enum: ListingsStatusEnum, + enumName: 'ListingsStatusEnum', + }) status: ListingsStatusEnum; @Expose() @IsEnum(ReviewOrderTypeEnum, { groups: [ValidationsGroupsEnum.default] }) - @ApiProperty({ + @ApiPropertyOptional({ enum: ReviewOrderTypeEnum, enumName: 'ReviewOrderTypeEnum', }) reviewOrderType?: ReviewOrderTypeEnum; @Expose() - @ApiProperty() + @ApiPropertyOptional() applicationConfig?: Record; @Expose() @@ -329,7 +332,7 @@ class ListingGet extends AbstractDTO { displayWaitlistSize: boolean; @Expose() - @ApiProperty() + @ApiPropertyOptional() get showWaitlist(): boolean { return ( this.waitlistMaxSize !== null && @@ -341,63 +344,66 @@ class ListingGet extends AbstractDTO { @Expose() @IsString({ groups: [ValidationsGroupsEnum.default] }) @MaxLength(4096, { groups: [ValidationsGroupsEnum.default] }) - @ApiProperty() + @ApiPropertyOptional() reservedCommunityDescription?: string; @Expose() @IsNumber({}, { groups: [ValidationsGroupsEnum.default] }) - @ApiProperty() + @ApiPropertyOptional() reservedCommunityMinAge?: number; @Expose() @IsString({ groups: [ValidationsGroupsEnum.default] }) @MaxLength(4096, { groups: [ValidationsGroupsEnum.default] }) - @ApiProperty() + @ApiPropertyOptional() resultLink?: string; @Expose() @IsBoolean({ groups: [ValidationsGroupsEnum.default] }) - @ApiProperty() + @ApiPropertyOptional() isWaitlistOpen?: boolean; @Expose() @IsNumber({}, { groups: [ValidationsGroupsEnum.default] }) - @ApiProperty() + @ApiPropertyOptional() waitlistOpenSpots?: number; @Expose() @IsBoolean({ groups: [ValidationsGroupsEnum.default] }) - @ApiProperty() + @ApiPropertyOptional() customMapPin?: boolean; @Expose() @IsDate({ groups: [ValidationsGroupsEnum.default] }) @Type(() => Date) - @ApiProperty() + @ApiPropertyOptional() publishedAt?: Date; @Expose() @IsDate({ groups: [ValidationsGroupsEnum.default] }) @Type(() => Date) - @ApiProperty() + @ApiPropertyOptional() closedAt?: Date; @Expose() @IsDate({ groups: [ValidationsGroupsEnum.default] }) @Type(() => Date) - @ApiProperty() + @ApiPropertyOptional() afsLastRunAt?: Date; @Expose() @IsDate({ groups: [ValidationsGroupsEnum.default] }) @Type(() => Date) - @ApiProperty() + @ApiPropertyOptional() lastApplicationUpdateAt?: Date; @Expose() @ValidateNested({ groups: [ValidationsGroupsEnum.default], each: true }) @Type(() => ListingMultiselectQuestion) - @ApiProperty({ type: ListingMultiselectQuestion, isArray: true }) + @ApiPropertyOptional({ + type: ListingMultiselectQuestion, + isArray: true, + }) listingMultiselectQuestions?: ListingMultiselectQuestion[]; @Expose() @@ -427,7 +433,7 @@ class ListingGet extends AbstractDTO { @Type(() => ListingEvent) @IsDefined({ groups: [ValidationsGroupsEnum.default] }) @ApiProperty({ type: Asset, isArray: true }) - events: ListingEvent[]; + listingEvents: ListingEvent[]; @Expose() @IsDefined({ groups: [ValidationsGroupsEnum.default] }) @@ -439,68 +445,68 @@ class ListingGet extends AbstractDTO { @Expose() @ValidateNested({ groups: [ValidationsGroupsEnum.default] }) @Type(() => Address) - @ApiProperty({ type: Address }) + @ApiPropertyOptional({ type: Address }) listingsApplicationPickUpAddress?: Address; @Expose() @ValidateNested({ groups: [ValidationsGroupsEnum.default] }) @Type(() => Address) - @ApiProperty({ type: Address }) + @ApiPropertyOptional({ type: Address }) listingsApplicationDropOffAddress?: Address; @Expose() @ValidateNested({ groups: [ValidationsGroupsEnum.default] }) @Type(() => Address) - @ApiProperty({ type: Address }) + @ApiPropertyOptional({ type: Address }) listingsApplicationMailingAddress?: Address; @Expose() @ValidateNested({ groups: [ValidationsGroupsEnum.default] }) @Type(() => Address) - @ApiProperty({ type: Address }) + @ApiPropertyOptional({ type: Address }) listingsLeasingAgentAddress?: Address; @Expose() @ValidateNested({ groups: [ValidationsGroupsEnum.default] }) @Type(() => Asset) - @ApiProperty({ type: Asset }) + @ApiPropertyOptional({ type: Asset }) listingsBuildingSelectionCriteriaFile?: Asset; @Expose() @ValidateNested({ groups: [ValidationsGroupsEnum.default] }) @IsDefined({ groups: [ValidationsGroupsEnum.default] }) - @Type(() => Jurisdiction) - @ApiProperty({ type: Jurisdiction }) - jurisdictions: Jurisdiction; + @Type(() => IdDTO) + @ApiProperty({ type: IdDTO }) + jurisdictions: IdDTO; @Expose() @ValidateNested({ groups: [ValidationsGroupsEnum.default] }) @Type(() => Asset) - @ApiProperty({ type: Asset }) + @ApiPropertyOptional({ type: Asset }) listingsResult?: Asset; @Expose() @ValidateNested({ groups: [ValidationsGroupsEnum.default] }) - @Type(() => ReservedCommunityType) - @ApiProperty({ type: ReservedCommunityType }) - reservedCommunityTypes?: ReservedCommunityType; + @Type(() => IdDTO) + @ApiPropertyOptional({ type: IdDTO }) + reservedCommunityTypes?: IdDTO; @Expose() @ValidateNested({ groups: [ValidationsGroupsEnum.default], each: true }) @Type(() => ListingImage) - @ApiProperty({ type: ListingImage, isArray: true }) + @ApiPropertyOptional({ type: ListingImage, isArray: true }) listingImages?: ListingImage[]; @Expose() @ValidateNested({ groups: [ValidationsGroupsEnum.default], each: true }) @Type(() => ListingFeatures) - @ApiProperty({ type: ListingFeatures }) + @ApiPropertyOptional({ type: ListingFeatures }) listingFeatures?: ListingFeatures; @Expose() @ValidateNested({ groups: [ValidationsGroupsEnum.default], each: true }) @Type(() => ListingUtilities) - @ApiProperty({ type: ListingUtilities }) + @ApiPropertyOptional({ type: ListingUtilities }) listingUtilities?: ListingUtilities; @Expose() @@ -510,14 +516,20 @@ class ListingGet extends AbstractDTO { units: Unit[]; @Expose() - @ApiProperty({ type: UnitsSummarized }) - unitsSummarized: UnitsSummarized | undefined; + @ApiPropertyOptional({ type: UnitsSummarized }) + unitsSummarized?: UnitsSummarized; @Expose() @ValidateNested({ groups: [ValidationsGroupsEnum.default], each: true }) - @ApiProperty({ type: UnitsSummary, isArray: true }) + @ApiPropertyOptional({ type: UnitsSummary, isArray: true }) @Type(() => UnitsSummary) - unitsSummary: UnitsSummary[]; + unitsSummary?: UnitsSummary[]; + + @Expose() + @IsString({ groups: [ValidationsGroupsEnum.default] }) + @ApiPropertyOptional() + @Transform((value: TransformFnParams) => listingUrlSlug(value.obj as Listing)) + urlSlug?: string; } -export { ListingGet as default, ListingGet }; +export { Listing as default, Listing }; diff --git a/backend_new/src/dtos/listings/listings-filter-params.dto.ts b/backend_new/src/dtos/listings/listings-filter-params.dto.ts index 61cb121163..0c0fcba519 100644 --- a/backend_new/src/dtos/listings/listings-filter-params.dto.ts +++ b/backend_new/src/dtos/listings/listings-filter-params.dto.ts @@ -1,6 +1,6 @@ import { BaseFilter } from '../shared/base-filter.dto'; import { Expose } from 'class-transformer'; -import { ApiProperty } from '@nestjs/swagger'; +import { ApiPropertyOptional } from '@nestjs/swagger'; import { IsEnum, IsNumberString, IsString } from 'class-validator'; import { ValidationsGroupsEnum } from '../../enums/shared/validation-groups-enum'; import { ListingFilterKeys } from '../../enums/listings/filter-key-enum'; @@ -8,63 +8,51 @@ import { ListingsStatusEnum } from '@prisma/client'; export class ListingFilterParams extends BaseFilter { @Expose() - @ApiProperty({ - type: String, + @ApiPropertyOptional({ example: 'Coliseum', - required: false, }) @IsString({ groups: [ValidationsGroupsEnum.default] }) [ListingFilterKeys.name]?: string; @Expose() - @ApiProperty({ - enum: Object.keys(ListingsStatusEnum), + @ApiPropertyOptional({ + enum: ListingsStatusEnum, + enumName: 'ListingStatusEnum', example: 'active', - required: false, }) @IsEnum(ListingsStatusEnum, { groups: [ValidationsGroupsEnum.default] }) [ListingFilterKeys.status]?: ListingsStatusEnum; @Expose() - @ApiProperty({ - type: String, + @ApiPropertyOptional({ example: 'Fox Creek', - required: false, }) @IsString({ groups: [ValidationsGroupsEnum.default] }) [ListingFilterKeys.neighborhood]?: string; @Expose() - @ApiProperty({ - type: Number, + @ApiPropertyOptional({ example: '3', - required: false, }) @IsNumberString({}, { groups: [ValidationsGroupsEnum.default] }) [ListingFilterKeys.bedrooms]?: number; @Expose() - @ApiProperty({ - type: String, + @ApiPropertyOptional({ example: '48211', - required: false, }) [ListingFilterKeys.zipcode]?: string; @Expose() - @ApiProperty({ - type: String, + @ApiPropertyOptional({ example: 'FAB1A3C6-965E-4054-9A48-A282E92E9426', - required: false, }) @IsString({ groups: [ValidationsGroupsEnum.default] }) [ListingFilterKeys.leasingAgents]?: string; @Expose() - @ApiProperty({ - type: String, + @ApiPropertyOptional({ example: 'bab6cb4f-7a5a-4ee5-b327-0c2508807780', - required: false, }) @IsString({ groups: [ValidationsGroupsEnum.default] }) [ListingFilterKeys.jurisdiction]?: string; diff --git a/backend_new/src/dtos/listings/listings-query-params.dto.ts b/backend_new/src/dtos/listings/listings-query-params.dto.ts index dfca9471ed..c0a0884050 100644 --- a/backend_new/src/dtos/listings/listings-query-params.dto.ts +++ b/backend_new/src/dtos/listings/listings-query-params.dto.ts @@ -1,6 +1,6 @@ import { PaginationAllowsAllQueryParams } from '../shared/pagination.dto'; import { Expose, Type } from 'class-transformer'; -import { ApiProperty, getSchemaPath } from '@nestjs/swagger'; +import { ApiPropertyOptional, getSchemaPath } from '@nestjs/swagger'; import { ListingFilterParams } from './listings-filter-params.dto'; import { ArrayMaxSize, @@ -19,9 +19,7 @@ import { OrderQueryParamValidator } from '../../utilities/order-by-validator'; export class ListingsQueryParams extends PaginationAllowsAllQueryParams { @Expose() - @ApiProperty({ - name: 'filter', - required: false, + @ApiPropertyOptional({ type: [String], items: { $ref: getSchemaPath(ListingFilterParams), @@ -35,9 +33,8 @@ export class ListingsQueryParams extends PaginationAllowsAllQueryParams { filter?: ListingFilterParams[]; @Expose() - @ApiProperty({ + @ApiPropertyOptional({ enum: ListingViews, - required: false, enumName: 'ListingViews', example: 'full', }) @@ -47,9 +44,8 @@ export class ListingsQueryParams extends PaginationAllowsAllQueryParams { view?: ListingViews; @Expose() - @ApiProperty({ - name: 'orderBy', - required: false, + @ApiPropertyOptional({ + enum: ListingOrderByKeys, enumName: 'ListingOrderByKeys', example: '["updatedAt"]', isArray: true, @@ -67,11 +63,11 @@ export class ListingsQueryParams extends PaginationAllowsAllQueryParams { orderBy?: ListingOrderByKeys[]; @Expose() - @ApiProperty({ + @ApiPropertyOptional({ enum: OrderByEnum, + enumName: 'OrderByEnum', example: '["desc"]', default: '["desc"]', - required: false, isArray: true, }) @IsArray({ groups: [ValidationsGroupsEnum.default] }) @@ -83,10 +79,8 @@ export class ListingsQueryParams extends PaginationAllowsAllQueryParams { orderDir?: OrderByEnum[]; @Expose() - @ApiProperty({ - type: String, + @ApiPropertyOptional({ example: 'search', - required: false, }) @IsString({ groups: [ValidationsGroupsEnum.default] }) @MinLength(3, { diff --git a/backend_new/src/dtos/listings/listings-retrieve-params.dto.ts b/backend_new/src/dtos/listings/listings-retrieve-params.dto.ts index e2d91294fc..23cbdb00db 100644 --- a/backend_new/src/dtos/listings/listings-retrieve-params.dto.ts +++ b/backend_new/src/dtos/listings/listings-retrieve-params.dto.ts @@ -1,14 +1,13 @@ import { Expose } from 'class-transformer'; -import { ApiProperty } from '@nestjs/swagger'; +import { ApiPropertyOptional } from '@nestjs/swagger'; import { IsEnum } from 'class-validator'; import { ValidationsGroupsEnum } from '../../enums/shared/validation-groups-enum'; import { ListingViews } from '../../enums/listings/view-enum'; export class ListingsRetrieveParams { @Expose() - @ApiProperty({ + @ApiPropertyOptional({ enum: ListingViews, - required: false, enumName: 'ListingViews', example: 'full', }) diff --git a/backend_new/src/dtos/listings/paginated-listing.dto.ts b/backend_new/src/dtos/listings/paginated-listing.dto.ts index 6f5a99f9f3..d7765e9f3b 100644 --- a/backend_new/src/dtos/listings/paginated-listing.dto.ts +++ b/backend_new/src/dtos/listings/paginated-listing.dto.ts @@ -1,6 +1,4 @@ import { PaginationFactory } from '../shared/pagination.dto'; -import { ListingGet } from './listing-get.dto'; +import { Listing } from './listing.dto'; -export class PaginatedListingDto extends PaginationFactory( - ListingGet, -) {} +export class PaginatedListingDto extends PaginationFactory(Listing) {} diff --git a/backend_new/src/dtos/multiselect-questions/multiselect-option.dto.ts b/backend_new/src/dtos/multiselect-questions/multiselect-option.dto.ts index 1e64b93d80..d9cca2ee26 100644 --- a/backend_new/src/dtos/multiselect-questions/multiselect-option.dto.ts +++ b/backend_new/src/dtos/multiselect-questions/multiselect-option.dto.ts @@ -7,7 +7,7 @@ import { ValidateNested, } from 'class-validator'; import { ValidationsGroupsEnum } from '../../enums/shared/validation-groups-enum'; -import { ApiProperty } from '@nestjs/swagger'; +import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; import { MultiselectLink } from './multiselect-link.dto'; export class MultiselectOption { @@ -19,7 +19,7 @@ export class MultiselectOption { @Expose() @IsString({ groups: [ValidationsGroupsEnum.default] }) - @ApiProperty() + @ApiPropertyOptional() untranslatedText?: string; @Expose() @@ -30,22 +30,22 @@ export class MultiselectOption { @Expose() @IsString({ groups: [ValidationsGroupsEnum.default] }) - @ApiProperty() + @ApiPropertyOptional() description?: string; @Expose() @ValidateNested({ groups: [ValidationsGroupsEnum.default], each: true }) @Type(() => MultiselectLink) - @ApiProperty({ type: MultiselectLink, isArray: true }) + @ApiPropertyOptional({ type: MultiselectLink, isArray: true }) links?: MultiselectLink[]; @Expose() @IsBoolean({ groups: [ValidationsGroupsEnum.default] }) - @ApiProperty() + @ApiPropertyOptional() collectAddress?: boolean; @Expose() @IsBoolean({ groups: [ValidationsGroupsEnum.default] }) - @ApiProperty() + @ApiPropertyOptional() exclusive?: boolean; } diff --git a/backend_new/src/dtos/multiselect-questions/multiselect-question-filter-params.dto.ts b/backend_new/src/dtos/multiselect-questions/multiselect-question-filter-params.dto.ts index 8443096af0..d17cfc5190 100644 --- a/backend_new/src/dtos/multiselect-questions/multiselect-question-filter-params.dto.ts +++ b/backend_new/src/dtos/multiselect-questions/multiselect-question-filter-params.dto.ts @@ -1,6 +1,6 @@ import { BaseFilter } from '../shared/base-filter.dto'; import { Expose } from 'class-transformer'; -import { ApiProperty } from '@nestjs/swagger'; +import { ApiPropertyOptional } from '@nestjs/swagger'; import { IsString, IsUUID } from 'class-validator'; import { ValidationsGroupsEnum } from '../../enums/shared/validation-groups-enum'; import { MultiselectQuestionFilterKeys } from '../../enums/multiselect-questions/filter-key-enum'; @@ -8,20 +8,18 @@ import { MultiselectQuestionsApplicationSectionEnum } from '@prisma/client'; export class MultiselectQuestionFilterParams extends BaseFilter { @Expose() - @ApiProperty({ - type: String, + @ApiPropertyOptional({ example: 'uuid', - required: false, }) @IsString({ groups: [ValidationsGroupsEnum.default] }) @IsUUID(4, { groups: [ValidationsGroupsEnum.default] }) [MultiselectQuestionFilterKeys.jurisdiction]?: string; @Expose() - @ApiProperty({ - type: String, + @ApiPropertyOptional({ + enum: MultiselectQuestionsApplicationSectionEnum, + enumName: 'MultiselectQuestionsApplicationSectionEnum', example: 'preferences', - required: false, }) @IsString({ groups: [ValidationsGroupsEnum.default] }) [MultiselectQuestionFilterKeys.applicationSection]?: MultiselectQuestionsApplicationSectionEnum; diff --git a/backend_new/src/dtos/multiselect-questions/multiselect-question-query-params.dto.ts b/backend_new/src/dtos/multiselect-questions/multiselect-question-query-params.dto.ts index 337783072b..0ffe9cc198 100644 --- a/backend_new/src/dtos/multiselect-questions/multiselect-question-query-params.dto.ts +++ b/backend_new/src/dtos/multiselect-questions/multiselect-question-query-params.dto.ts @@ -1,19 +1,15 @@ import { Expose, Type } from 'class-transformer'; -import { ApiProperty, getSchemaPath } from '@nestjs/swagger'; +import { ApiPropertyOptional } from '@nestjs/swagger'; import { MultiselectQuestionFilterParams } from './multiselect-question-filter-params.dto'; import { ArrayMaxSize, IsArray, ValidateNested } from 'class-validator'; import { ValidationsGroupsEnum } from '../../enums/shared/validation-groups-enum'; export class MultiselectQuestionQueryParams { @Expose() - @ApiProperty({ + @ApiPropertyOptional({ name: 'filter', - required: false, type: MultiselectQuestionFilterParams, isArray: true, - items: { - $ref: getSchemaPath(MultiselectQuestionFilterParams), - }, example: { $comparison: '=', applicationSection: 'programs' }, }) @IsArray({ groups: [ValidationsGroupsEnum.default] }) diff --git a/backend_new/src/dtos/multiselect-questions/multiselect-question-update.dto.ts b/backend_new/src/dtos/multiselect-questions/multiselect-question-update.dto.ts index 4d3e24d185..ce497c4cd6 100644 --- a/backend_new/src/dtos/multiselect-questions/multiselect-question-update.dto.ts +++ b/backend_new/src/dtos/multiselect-questions/multiselect-question-update.dto.ts @@ -1,5 +1,5 @@ import { OmitType } from '@nestjs/swagger'; -import { MultiselectQuestion } from './multiselect-question-get.dto'; +import { MultiselectQuestion } from './multiselect-question.dto'; export class MultiselectQuestionUpdate extends OmitType(MultiselectQuestion, [ 'createdAt', diff --git a/backend_new/src/dtos/multiselect-questions/multiselect-question-get.dto.ts b/backend_new/src/dtos/multiselect-questions/multiselect-question.dto.ts similarity index 87% rename from backend_new/src/dtos/multiselect-questions/multiselect-question-get.dto.ts rename to backend_new/src/dtos/multiselect-questions/multiselect-question.dto.ts index 38fe33818c..3fd975fa12 100644 --- a/backend_new/src/dtos/multiselect-questions/multiselect-question-get.dto.ts +++ b/backend_new/src/dtos/multiselect-questions/multiselect-question.dto.ts @@ -8,7 +8,7 @@ import { IsDefined, } from 'class-validator'; import { ValidationsGroupsEnum } from '../../enums/shared/validation-groups-enum'; -import { ApiProperty } from '@nestjs/swagger'; +import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; import { AbstractDTO } from '../shared/abstract.dto'; import { MultiselectQuestionsApplicationSectionEnum } from '@prisma/client'; import { MultiselectLink } from './multiselect-link.dto'; @@ -24,28 +24,28 @@ class MultiselectQuestion extends AbstractDTO { @Expose() @IsString({ groups: [ValidationsGroupsEnum.default] }) - @ApiProperty() + @ApiPropertyOptional() untranslatedText?: string; @Expose() @IsString({ groups: [ValidationsGroupsEnum.default] }) - @ApiProperty() + @ApiPropertyOptional() untranslatedOptOutText?: string; @Expose() @IsString({ groups: [ValidationsGroupsEnum.default] }) - @ApiProperty() + @ApiPropertyOptional() subText?: string; @Expose() @IsString({ groups: [ValidationsGroupsEnum.default] }) - @ApiProperty() + @ApiPropertyOptional() description?: string; @Expose() @ValidateNested({ groups: [ValidationsGroupsEnum.default], each: true }) @Type(() => MultiselectLink) - @ApiProperty({ type: MultiselectLink, isArray: true }) + @ApiPropertyOptional({ type: MultiselectLink, isArray: true }) links?: MultiselectLink[]; @Expose() @@ -59,17 +59,17 @@ class MultiselectQuestion extends AbstractDTO { @ArrayMaxSize(64, { groups: [ValidationsGroupsEnum.default] }) @ValidateNested({ groups: [ValidationsGroupsEnum.default], each: true }) @Type(() => MultiselectOption) - @ApiProperty({ type: MultiselectOption, isArray: true }) + @ApiPropertyOptional({ type: MultiselectOption, isArray: true }) options?: MultiselectOption[]; @Expose() @IsString({ groups: [ValidationsGroupsEnum.default] }) - @ApiProperty() + @ApiPropertyOptional() optOutText?: string; @Expose() @IsBoolean({ groups: [ValidationsGroupsEnum.default] }) - @ApiProperty() + @ApiPropertyOptional() hideFromListing?: boolean; @Expose() diff --git a/backend_new/src/dtos/paper-applications/paper-application-create.dto.ts b/backend_new/src/dtos/paper-applications/paper-application-create.dto.ts new file mode 100644 index 0000000000..babc6ff015 --- /dev/null +++ b/backend_new/src/dtos/paper-applications/paper-application-create.dto.ts @@ -0,0 +1,19 @@ +import { ApiPropertyOptional, OmitType } from '@nestjs/swagger'; +import { Expose, Type } from 'class-transformer'; +import { ValidateNested } from 'class-validator'; +import { ValidationsGroupsEnum } from '../../enums/shared/validation-groups-enum'; +import { PaperApplication } from './paper-application.dto'; +import { AssetCreate } from '../assets/asset-create.dto'; + +export class PaperApplicationCreate extends OmitType(PaperApplication, [ + 'id', + 'createdAt', + 'updatedAt', + 'assets', +]) { + @Expose() + @ValidateNested({ groups: [ValidationsGroupsEnum.default], each: true }) + @Type(() => AssetCreate) + @ApiPropertyOptional({ type: AssetCreate }) + assets?: AssetCreate; +} diff --git a/backend_new/src/dtos/paper-applications/paper-application.dto.ts b/backend_new/src/dtos/paper-applications/paper-application.dto.ts index 48a6921d73..c7aba25653 100644 --- a/backend_new/src/dtos/paper-applications/paper-application.dto.ts +++ b/backend_new/src/dtos/paper-applications/paper-application.dto.ts @@ -4,18 +4,22 @@ import { Expose, Type } from 'class-transformer'; import { IsEnum, IsDefined, ValidateNested } from 'class-validator'; import { ValidationsGroupsEnum } from '../../enums/shared/validation-groups-enum'; import { ApiProperty } from '@nestjs/swagger'; -import { Asset } from '../assets/asset-get.dto'; +import { Asset } from '../assets/asset.dto'; export class PaperApplication extends AbstractDTO { @Expose() @IsEnum(LanguagesEnum, { groups: [ValidationsGroupsEnum.default] }) @IsDefined({ groups: [ValidationsGroupsEnum.default] }) - @ApiProperty({ enum: LanguagesEnum, enumName: 'LanguagesEnum' }) + @ApiProperty({ + enum: LanguagesEnum, + enumName: 'LanguagesEnum', + }) language: LanguagesEnum; @Expose() @ValidateNested({ groups: [ValidationsGroupsEnum.default] }) @IsDefined({ groups: [ValidationsGroupsEnum.default] }) @Type(() => Asset) + @ApiProperty({ type: Asset }) assets: Asset; } diff --git a/backend_new/src/dtos/reserved-community-types/reserved-community-type-query-params.dto.ts b/backend_new/src/dtos/reserved-community-types/reserved-community-type-query-params.dto.ts index 98474e01a4..5b2b6ae221 100644 --- a/backend_new/src/dtos/reserved-community-types/reserved-community-type-query-params.dto.ts +++ b/backend_new/src/dtos/reserved-community-types/reserved-community-type-query-params.dto.ts @@ -1,14 +1,12 @@ import { Expose } from 'class-transformer'; -import { ApiProperty } from '@nestjs/swagger'; -import { IsString } from 'class-validator'; +import { ApiPropertyOptional } from '@nestjs/swagger'; +import { IsString, IsUUID } from 'class-validator'; import { ValidationsGroupsEnum } from '../../enums/shared/validation-groups-enum'; export class ReservedCommunityTypeQueryParams { @Expose() - @ApiProperty({ - required: false, - type: String, - }) + @ApiPropertyOptional() + @IsUUID(4, { groups: [ValidationsGroupsEnum.default] }) @IsString({ groups: [ValidationsGroupsEnum.default] }) jurisdictionId?: string; } diff --git a/backend_new/src/dtos/reserved-community-types/reserved-community-type.dto.ts b/backend_new/src/dtos/reserved-community-types/reserved-community-type.dto.ts index 13201b0dd2..4c7999d1d2 100644 --- a/backend_new/src/dtos/reserved-community-types/reserved-community-type.dto.ts +++ b/backend_new/src/dtos/reserved-community-types/reserved-community-type.dto.ts @@ -1,4 +1,4 @@ -import { ApiProperty } from '@nestjs/swagger'; +import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; import { Expose, Type } from 'class-transformer'; import { IsDefined, @@ -16,18 +16,19 @@ export class ReservedCommunityType extends AbstractDTO { @IsDefined({ groups: [ValidationsGroupsEnum.default] }) @MaxLength(256, { groups: [ValidationsGroupsEnum.default] }) @ApiProperty() + @ApiProperty() name: string; @Expose() @IsString({ groups: [ValidationsGroupsEnum.default] }) @MaxLength(2048, { groups: [ValidationsGroupsEnum.default] }) - @ApiProperty() + @ApiPropertyOptional() description?: string; @Expose() @ValidateNested({ groups: [ValidationsGroupsEnum.default] }) @Type(() => IdDTO) @IsDefined({ groups: [ValidationsGroupsEnum.default] }) - @ApiProperty() + @ApiProperty({ type: IdDTO }) jurisdictions: IdDTO; } diff --git a/backend_new/src/dtos/shared/abstract.dto.ts b/backend_new/src/dtos/shared/abstract.dto.ts index 7793ec4e4b..2656a2a677 100644 --- a/backend_new/src/dtos/shared/abstract.dto.ts +++ b/backend_new/src/dtos/shared/abstract.dto.ts @@ -8,20 +8,20 @@ export class AbstractDTO { @IsString({ groups: [ValidationsGroupsEnum.default] }) @IsUUID(4, { groups: [ValidationsGroupsEnum.default] }) @IsDefined({ groups: [ValidationsGroupsEnum.default] }) - @ApiProperty({ required: true }) + @ApiProperty() id: string; @Expose() @IsDate({ groups: [ValidationsGroupsEnum.default] }) @IsDefined({ groups: [ValidationsGroupsEnum.default] }) @Type(() => Date) - @ApiProperty({ required: true }) + @ApiProperty() createdAt: Date; @Expose() @IsDate({ groups: [ValidationsGroupsEnum.default] }) @IsDefined({ groups: [ValidationsGroupsEnum.default] }) @Type(() => Date) - @ApiProperty({ required: true }) + @ApiProperty() updatedAt: Date; } diff --git a/backend_new/src/dtos/shared/id.dto.ts b/backend_new/src/dtos/shared/id.dto.ts index f0629820c5..bb1c36ee10 100644 --- a/backend_new/src/dtos/shared/id.dto.ts +++ b/backend_new/src/dtos/shared/id.dto.ts @@ -1,18 +1,23 @@ -import { IsDefined, IsString, IsUUID } from 'class-validator'; +import { IsDefined, IsNumber, IsString, IsUUID } from 'class-validator'; import { Expose } from 'class-transformer'; import { ValidationsGroupsEnum } from '../../enums/shared/validation-groups-enum'; -import { ApiProperty } from '@nestjs/swagger'; +import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; export class IdDTO { @Expose() @IsString({ groups: [ValidationsGroupsEnum.default] }) @IsUUID(4, { groups: [ValidationsGroupsEnum.default] }) @IsDefined({ groups: [ValidationsGroupsEnum.default] }) - @ApiProperty({ required: true }) + @ApiProperty() id: string; @Expose() @IsString({ groups: [ValidationsGroupsEnum.default] }) - @ApiProperty({ required: false }) + @ApiPropertyOptional() name?: string; + + @Expose() + @IsNumber({}, { groups: [ValidationsGroupsEnum.default] }) + @ApiPropertyOptional() + ordinal?: number; } diff --git a/backend_new/src/dtos/shared/pagination.dto.ts b/backend_new/src/dtos/shared/pagination.dto.ts index eb0762083a..526f94651c 100644 --- a/backend_new/src/dtos/shared/pagination.dto.ts +++ b/backend_new/src/dtos/shared/pagination.dto.ts @@ -50,7 +50,6 @@ export class PaginationQueryParams { @ApiPropertyOptional({ type: Number, example: 1, - required: false, default: 1, }) @IsNumber({}, { groups: [ValidationsGroupsEnum.default] }) @@ -66,7 +65,6 @@ export class PaginationQueryParams { @ApiPropertyOptional({ type: Number, example: 10, - required: false, default: 10, }) @IsNumber({}, { groups: [ValidationsGroupsEnum.default] }) @@ -84,7 +82,6 @@ export class PaginationAllowsAllQueryParams { @ApiPropertyOptional({ type: Number, example: 1, - required: false, default: 1, }) @IsNumber({}, { groups: [ValidationsGroupsEnum.default] }) @@ -100,7 +97,6 @@ export class PaginationAllowsAllQueryParams { @ApiPropertyOptional({ type: "number | 'all'", example: 10, - required: false, default: 10, }) @IsNumberOrAll({ diff --git a/backend_new/src/dtos/units/ami-chart-item-get.dto.ts b/backend_new/src/dtos/units/ami-chart-item.dto.ts similarity index 87% rename from backend_new/src/dtos/units/ami-chart-item-get.dto.ts rename to backend_new/src/dtos/units/ami-chart-item.dto.ts index 51c5eeb610..b7cc12e00d 100644 --- a/backend_new/src/dtos/units/ami-chart-item-get.dto.ts +++ b/backend_new/src/dtos/units/ami-chart-item.dto.ts @@ -7,18 +7,18 @@ export class AmiChartItem { @Expose() @IsNumber({}, { groups: [ValidationsGroupsEnum.default] }) @IsDefined({ groups: [ValidationsGroupsEnum.default] }) - @ApiProperty({ required: true }) + @ApiProperty() percentOfAmi: number; @Expose() @IsNumber({}, { groups: [ValidationsGroupsEnum.default] }) @IsDefined({ groups: [ValidationsGroupsEnum.default] }) - @ApiProperty({ required: true }) + @ApiProperty() householdSize: number; @Expose() @IsNumber({}, { groups: [ValidationsGroupsEnum.default] }) @IsDefined({ groups: [ValidationsGroupsEnum.default] }) - @ApiProperty({ required: true }) + @ApiProperty() income: number; } diff --git a/backend_new/src/dtos/units/ami-chart-override-create.dto.ts b/backend_new/src/dtos/units/ami-chart-override-create.dto.ts new file mode 100644 index 0000000000..890e883c1d --- /dev/null +++ b/backend_new/src/dtos/units/ami-chart-override-create.dto.ts @@ -0,0 +1,8 @@ +import { OmitType } from '@nestjs/swagger'; +import { UnitAmiChartOverride } from './ami-chart-override.dto'; + +export class UnitAmiChartOverrideCreate extends OmitType(UnitAmiChartOverride, [ + 'id', + 'createdAt', + 'updatedAt', +]) {} diff --git a/backend_new/src/dtos/units/ami-chart-override-get.dto.ts b/backend_new/src/dtos/units/ami-chart-override.dto.ts similarity index 76% rename from backend_new/src/dtos/units/ami-chart-override-get.dto.ts rename to backend_new/src/dtos/units/ami-chart-override.dto.ts index 1fb257c1e1..c197d13b2b 100644 --- a/backend_new/src/dtos/units/ami-chart-override-get.dto.ts +++ b/backend_new/src/dtos/units/ami-chart-override.dto.ts @@ -2,12 +2,14 @@ import { AbstractDTO } from '../shared/abstract.dto'; import { Expose, Type } from 'class-transformer'; import { IsDefined, ValidateNested } from 'class-validator'; import { ValidationsGroupsEnum } from '../../enums/shared/validation-groups-enum'; -import { AmiChartItem } from './ami-chart-item-get.dto'; +import { AmiChartItem } from './ami-chart-item.dto'; +import { ApiProperty } from '@nestjs/swagger'; export class UnitAmiChartOverride extends AbstractDTO { @Expose() @IsDefined({ groups: [ValidationsGroupsEnum.default] }) @ValidateNested({ groups: [ValidationsGroupsEnum.default], each: true }) @Type(() => AmiChartItem) + @ApiProperty({ isArray: true, type: AmiChartItem }) items: AmiChartItem[]; } diff --git a/backend_new/src/dtos/units/ami-chart-get.dto.ts b/backend_new/src/dtos/units/ami-chart.dto.ts similarity index 79% rename from backend_new/src/dtos/units/ami-chart-get.dto.ts rename to backend_new/src/dtos/units/ami-chart.dto.ts index 59f70377c0..e2e7fe2500 100644 --- a/backend_new/src/dtos/units/ami-chart-get.dto.ts +++ b/backend_new/src/dtos/units/ami-chart.dto.ts @@ -2,17 +2,20 @@ import { IsDefined, IsString, ValidateNested } from 'class-validator'; import { Expose, Type } from 'class-transformer'; import { ValidationsGroupsEnum } from '../../enums/shared/validation-groups-enum'; import { AbstractDTO } from '../shared/abstract.dto'; -import { AmiChartItem } from './ami-chart-item-get.dto'; +import { AmiChartItem } from './ami-chart-item.dto'; +import { ApiProperty } from '@nestjs/swagger'; export class AmiChart extends AbstractDTO { @Expose() @IsDefined({ groups: [ValidationsGroupsEnum.default] }) @ValidateNested({ groups: [ValidationsGroupsEnum.default], each: true }) @Type(() => AmiChartItem) + @ApiProperty({ isArray: true, type: AmiChartItem }) items: AmiChartItem[]; @Expose() @IsString({ groups: [ValidationsGroupsEnum.default] }) @IsDefined({ groups: [ValidationsGroupsEnum.default] }) + @ApiProperty() name: string; } diff --git a/backend_new/src/dtos/units/hmi-get.dto.ts b/backend_new/src/dtos/units/hmi.dto.ts similarity index 100% rename from backend_new/src/dtos/units/hmi-get.dto.ts rename to backend_new/src/dtos/units/hmi.dto.ts diff --git a/backend_new/src/dtos/units/unit-create.dto.ts b/backend_new/src/dtos/units/unit-create.dto.ts new file mode 100644 index 0000000000..f8489d15e8 --- /dev/null +++ b/backend_new/src/dtos/units/unit-create.dto.ts @@ -0,0 +1,48 @@ +import { ApiPropertyOptional, OmitType } from '@nestjs/swagger'; +import { Expose, Type } from 'class-transformer'; +import { ValidateNested } from 'class-validator'; +import { ValidationsGroupsEnum } from '../../enums/shared/validation-groups-enum'; +import { IdDTO } from '../shared/id.dto'; +import { Unit } from './unit.dto'; +import { UnitAmiChartOverrideCreate } from './ami-chart-override-create.dto'; + +export class UnitCreate extends OmitType(Unit, [ + 'id', + 'createdAt', + 'updatedAt', + 'amiChart', + 'unitTypes', + 'unitAccessibilityPriorityTypes', + 'unitRentTypes', + 'unitAmiChartOverrides', +]) { + @Expose() + @Type(() => IdDTO) + @ApiPropertyOptional({ type: IdDTO }) + @ValidateNested({ groups: [ValidationsGroupsEnum.default] }) + unitTypes?: IdDTO; + + @Expose() + @Type(() => IdDTO) + @ValidateNested({ groups: [ValidationsGroupsEnum.default] }) + @ApiPropertyOptional({ type: IdDTO }) + amiChart?: IdDTO; + + @Expose() + @Type(() => IdDTO) + @ApiPropertyOptional({ type: IdDTO }) + @ValidateNested({ groups: [ValidationsGroupsEnum.default] }) + unitAccessibilityPriorityTypes?: IdDTO; + + @Expose() + @Type(() => IdDTO) + @ApiPropertyOptional({ type: IdDTO }) + @ValidateNested({ groups: [ValidationsGroupsEnum.default] }) + unitRentTypes?: IdDTO; + + @Expose() + @ValidateNested({ groups: [ValidationsGroupsEnum.default] }) + @Type(() => UnitAmiChartOverrideCreate) + @ApiPropertyOptional({ type: UnitAmiChartOverrideCreate }) + unitAmiChartOverrides?: UnitAmiChartOverrideCreate; +} diff --git a/backend_new/src/dtos/units/unit-summarized.dto.ts b/backend_new/src/dtos/units/unit-summarized.dto.ts index 31d36b3c6a..014e410702 100644 --- a/backend_new/src/dtos/units/unit-summarized.dto.ts +++ b/backend_new/src/dtos/units/unit-summarized.dto.ts @@ -1,9 +1,9 @@ import { Expose, Type } from 'class-transformer'; import { IsString, ValidateNested } from 'class-validator'; import { ValidationsGroupsEnum } from '../../enums/shared/validation-groups-enum'; -import { UnitSummary } from './unit-summary-get.dto'; -import { UnitSummaryByAMI } from './unit-summary-by-ami-get.dto'; -import { HMI } from './hmi-get.dto'; +import { UnitSummary } from './unit-summary.dto'; +import { UnitSummaryByAMI } from './unit-summary-by-ami.dto'; +import { HMI } from './hmi.dto'; import { ApiProperty } from '@nestjs/swagger'; import { UnitType } from '../unit-types/unit-type.dto'; import { UnitAccessibilityPriorityType } from '../unit-accessibility-priority-types/unit-accessibility-priority-type.dto'; diff --git a/backend_new/src/dtos/units/unit-summary-by-ami-get.dto.ts b/backend_new/src/dtos/units/unit-summary-by-ami.dto.ts similarity index 92% rename from backend_new/src/dtos/units/unit-summary-by-ami-get.dto.ts rename to backend_new/src/dtos/units/unit-summary-by-ami.dto.ts index 28c24cbcca..c1477dcc95 100644 --- a/backend_new/src/dtos/units/unit-summary-by-ami-get.dto.ts +++ b/backend_new/src/dtos/units/unit-summary-by-ami.dto.ts @@ -1,7 +1,7 @@ import { Expose, Type } from 'class-transformer'; import { IsDefined, IsString, ValidateNested } from 'class-validator'; import { ValidationsGroupsEnum } from '../../enums/shared/validation-groups-enum'; -import { UnitSummary } from './unit-summary-get.dto'; +import { UnitSummary } from './unit-summary.dto'; import { ApiProperty } from '@nestjs/swagger'; export class UnitSummaryByAMI { diff --git a/backend_new/src/dtos/units/unit-summary-get.dto.ts b/backend_new/src/dtos/units/unit-summary.dto.ts similarity index 94% rename from backend_new/src/dtos/units/unit-summary-get.dto.ts rename to backend_new/src/dtos/units/unit-summary.dto.ts index 1bc28381b0..58918aae9d 100644 --- a/backend_new/src/dtos/units/unit-summary-get.dto.ts +++ b/backend_new/src/dtos/units/unit-summary.dto.ts @@ -3,7 +3,7 @@ import { IsDefined, IsString, ValidateNested } from 'class-validator'; import { ValidationsGroupsEnum } from '../../enums/shared/validation-groups-enum'; import { MinMaxCurrency } from '../shared/min-max-currency.dto'; import { MinMax } from '../shared/min-max.dto'; -import { ApiProperty } from '@nestjs/swagger'; +import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; import { UnitType } from '../unit-types/unit-type.dto'; export class UnitSummary { @@ -56,6 +56,6 @@ export class UnitSummary { @Expose() @ValidateNested({ groups: [ValidationsGroupsEnum.default] }) @Type(() => MinMax) - @ApiProperty({ type: MinMax, required: false }) + @ApiPropertyOptional({ type: MinMax }) floorRange?: MinMax; } diff --git a/backend_new/src/dtos/units/unit-get.dto.ts b/backend_new/src/dtos/units/unit.dto.ts similarity index 78% rename from backend_new/src/dtos/units/unit-get.dto.ts rename to backend_new/src/dtos/units/unit.dto.ts index 3d7ed20511..648244fb54 100644 --- a/backend_new/src/dtos/units/unit-get.dto.ts +++ b/backend_new/src/dtos/units/unit.dto.ts @@ -12,86 +12,106 @@ import { AmiChart } from '../ami-charts/ami-chart.dto'; import { UnitType } from '../unit-types/unit-type.dto'; import { UnitRentType } from '../unit-rent-types/unit-rent-type.dto'; import { UnitAccessibilityPriorityType } from '../unit-accessibility-priority-types/unit-accessibility-priority-type.dto'; -import { UnitAmiChartOverride } from './ami-chart-override-get.dto'; +import { UnitAmiChartOverride } from './ami-chart-override.dto'; +import { ApiPropertyOptional } from '@nestjs/swagger'; class Unit extends AbstractDTO { @Expose() + @ApiPropertyOptional({ type: AmiChart }) amiChart?: AmiChart; @Expose() @IsString({ groups: [ValidationsGroupsEnum.default] }) + @ApiPropertyOptional() amiPercentage?: string; @Expose() @IsNumberString({}, { groups: [ValidationsGroupsEnum.default] }) + @ApiPropertyOptional() annualIncomeMin?: string; @Expose() @IsNumberString({}, { groups: [ValidationsGroupsEnum.default] }) + @ApiPropertyOptional() monthlyIncomeMin?: string; @Expose() @IsNumber({}, { groups: [ValidationsGroupsEnum.default] }) + @ApiPropertyOptional() floor?: number; @Expose() @IsString({ groups: [ValidationsGroupsEnum.default] }) + @ApiPropertyOptional() annualIncomeMax?: string; @Expose() @IsNumber({}, { groups: [ValidationsGroupsEnum.default] }) + @ApiPropertyOptional() maxOccupancy?: number; @Expose() @IsNumber({}, { groups: [ValidationsGroupsEnum.default] }) + @ApiPropertyOptional() minOccupancy?: number; @Expose() @IsNumberString({}, { groups: [ValidationsGroupsEnum.default] }) + @ApiPropertyOptional() monthlyRent?: string; @Expose() @IsNumber({}, { groups: [ValidationsGroupsEnum.default] }) + @ApiPropertyOptional() numBathrooms?: number; @Expose() @IsNumber({}, { groups: [ValidationsGroupsEnum.default] }) + @ApiPropertyOptional() numBedrooms?: number; @Expose() @IsString({ groups: [ValidationsGroupsEnum.default] }) + @ApiPropertyOptional() number?: string; @Expose() @IsString({ groups: [ValidationsGroupsEnum.default] }) + @ApiPropertyOptional() sqFeet?: string; @Expose() @IsString({ groups: [ValidationsGroupsEnum.default] }) + @ApiPropertyOptional() monthlyRentAsPercentOfIncome?: string; @Expose() @IsBoolean({ groups: [ValidationsGroupsEnum.default] }) + @ApiPropertyOptional() bmrProgramChart?: boolean; @Expose() @ValidateNested({ groups: [ValidationsGroupsEnum.default] }) @Type(() => UnitType) + @ApiPropertyOptional({ type: UnitType }) unitTypes?: UnitType; @Expose() @ValidateNested({ groups: [ValidationsGroupsEnum.default] }) @Type(() => UnitRentType) + @ApiPropertyOptional({ type: UnitRentType }) unitRentTypes?: UnitRentType; @Expose() @ValidateNested({ groups: [ValidationsGroupsEnum.default] }) @Type(() => UnitAccessibilityPriorityType) + @ApiPropertyOptional({ type: UnitAccessibilityPriorityType }) unitAccessibilityPriorityTypes?: UnitAccessibilityPriorityType; @Expose() @ValidateNested({ groups: [ValidationsGroupsEnum.default] }) @Type(() => UnitAmiChartOverride) + @ApiPropertyOptional({ type: UnitAmiChartOverride }) unitAmiChartOverrides?: UnitAmiChartOverride; } diff --git a/backend_new/src/dtos/units/units-summary-create.dto.ts b/backend_new/src/dtos/units/units-summary-create.dto.ts new file mode 100644 index 0000000000..c200b0aab1 --- /dev/null +++ b/backend_new/src/dtos/units/units-summary-create.dto.ts @@ -0,0 +1,4 @@ +import { OmitType } from '@nestjs/swagger'; +import { UnitsSummary } from './units-summary.dto'; + +export class UnitsSummaryCreate extends OmitType(UnitsSummary, ['id']) {} diff --git a/backend_new/src/dtos/units/units-summery-get.dto.ts b/backend_new/src/dtos/units/units-summary.dto.ts similarity index 72% rename from backend_new/src/dtos/units/units-summery-get.dto.ts rename to backend_new/src/dtos/units/units-summary.dto.ts index b5d1db7c41..e7a59cc617 100644 --- a/backend_new/src/dtos/units/units-summery-get.dto.ts +++ b/backend_new/src/dtos/units/units-summary.dto.ts @@ -8,81 +8,98 @@ import { } from 'class-validator'; import { Expose, Type } from 'class-transformer'; import { ValidationsGroupsEnum } from '../../enums/shared/validation-groups-enum'; -import { UnitType } from '../unit-types/unit-type.dto'; -import { UnitAccessibilityPriorityType } from '../unit-accessibility-priority-types/unit-accessibility-priority-type.dto'; +import { IdDTO } from '../shared/id.dto'; +import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; class UnitsSummary { @Expose() @IsUUID(4, { groups: [ValidationsGroupsEnum.default] }) @IsString({ groups: [ValidationsGroupsEnum.default] }) @IsDefined({ groups: [ValidationsGroupsEnum.default] }) + @ApiProperty() id: string; @Expose() @ValidateNested({ groups: [ValidationsGroupsEnum.default] }) - @Type(() => UnitType) + @Type(() => IdDTO) @IsDefined({ groups: [ValidationsGroupsEnum.default] }) - unitTypes: UnitType; + @ApiProperty({ type: IdDTO }) + unitTypes: IdDTO; @Expose() - @IsNumberString({}, { groups: [ValidationsGroupsEnum.default] }) + @IsNumber({}, { groups: [ValidationsGroupsEnum.default] }) + @ApiPropertyOptional() monthlyRentMin?: number; @Expose() - @IsNumberString({}, { groups: [ValidationsGroupsEnum.default] }) + @IsNumber({}, { groups: [ValidationsGroupsEnum.default] }) + @ApiPropertyOptional() monthlyRentMax?: number; @Expose() @IsString({ groups: [ValidationsGroupsEnum.default] }) + @ApiPropertyOptional() monthlyRentAsPercentOfIncome?: string; @Expose() @IsNumber({}, { groups: [ValidationsGroupsEnum.default] }) + @ApiPropertyOptional() amiPercentage?: number; @Expose() @IsNumberString({}, { groups: [ValidationsGroupsEnum.default] }) + @ApiPropertyOptional() minimumIncomeMin?: string; @Expose() @IsNumberString({}, { groups: [ValidationsGroupsEnum.default] }) + @ApiPropertyOptional() minimumIncomeMax?: string; @Expose() @IsNumber({}, { groups: [ValidationsGroupsEnum.default] }) + @ApiPropertyOptional() maxOccupancy?: number; @Expose() @IsNumber({}, { groups: [ValidationsGroupsEnum.default] }) + @ApiPropertyOptional() minOccupancy?: number; @Expose() @IsNumber({}, { groups: [ValidationsGroupsEnum.default] }) + @ApiPropertyOptional() floorMin?: number; @Expose() @IsNumber({}, { groups: [ValidationsGroupsEnum.default] }) + @ApiPropertyOptional() floorMax?: number; @Expose() @IsString({ groups: [ValidationsGroupsEnum.default] }) + @ApiPropertyOptional() sqFeetMin?: string; @Expose() @IsString({ groups: [ValidationsGroupsEnum.default] }) + @ApiPropertyOptional() sqFeetMax?: string; @Expose() @ValidateNested({ groups: [ValidationsGroupsEnum.default] }) - @Type(() => UnitAccessibilityPriorityType) - unitAccessibilityPriorityTypes?: UnitAccessibilityPriorityType; + @Type(() => IdDTO) + @ApiPropertyOptional({ type: IdDTO }) + unitAccessibilityPriorityTypes?: IdDTO; @Expose() @IsNumber({}, { groups: [ValidationsGroupsEnum.default] }) + @ApiPropertyOptional() totalCount?: number; @Expose() @IsNumber({}, { groups: [ValidationsGroupsEnum.default] }) + @ApiPropertyOptional() totalAvailable?: number; } diff --git a/backend_new/src/dtos/users/confirmation-request.dto.ts b/backend_new/src/dtos/users/confirmation-request.dto.ts index 7ff96f7078..f507cc6daf 100644 --- a/backend_new/src/dtos/users/confirmation-request.dto.ts +++ b/backend_new/src/dtos/users/confirmation-request.dto.ts @@ -7,6 +7,6 @@ export class ConfirmationRequest { @Expose() @IsString({ groups: [ValidationsGroupsEnum.default] }) @MaxLength(256, { groups: [ValidationsGroupsEnum.default] }) - @ApiProperty({ required: true }) + @ApiProperty() token: string; } diff --git a/backend_new/src/dtos/users/email-and-app-url.dto.ts b/backend_new/src/dtos/users/email-and-app-url.dto.ts index 7126a5b5e0..a61a5e1b88 100644 --- a/backend_new/src/dtos/users/email-and-app-url.dto.ts +++ b/backend_new/src/dtos/users/email-and-app-url.dto.ts @@ -1,4 +1,4 @@ -import { ApiProperty } from '@nestjs/swagger'; +import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; import { Expose } from 'class-transformer'; import { IsEmail, IsString, MaxLength } from 'class-validator'; import { EnforceLowerCase } from '../../decorators/enforce-lower-case.decorator'; @@ -11,13 +11,13 @@ import { ValidationsGroupsEnum } from '../../enums/shared/validation-groups-enum export class EmailAndAppUrl { @Expose() @IsEmail({}, { groups: [ValidationsGroupsEnum.default] }) - @ApiProperty({ required: true }) + @ApiProperty() @EnforceLowerCase() email: string; @Expose() @IsString({ groups: [ValidationsGroupsEnum.default] }) - @ApiProperty({ required: false }) + @ApiPropertyOptional() @MaxLength(256, { groups: [ValidationsGroupsEnum.default] }) appUrl?: string; } diff --git a/backend_new/src/dtos/users/user-create-params.dto.ts b/backend_new/src/dtos/users/user-create-params.dto.ts index 7a791fd7d0..5dc014fcb1 100644 --- a/backend_new/src/dtos/users/user-create-params.dto.ts +++ b/backend_new/src/dtos/users/user-create-params.dto.ts @@ -1,14 +1,13 @@ import { Expose, Transform, TransformFnParams } from 'class-transformer'; -import { ApiProperty } from '@nestjs/swagger'; +import { ApiPropertyOptional } from '@nestjs/swagger'; import { IsBoolean } from 'class-validator'; import { ValidationsGroupsEnum } from '../../enums/shared/validation-groups-enum'; export class UserCreateParams { @Expose() - @ApiProperty({ + @ApiPropertyOptional({ type: Boolean, example: true, - required: false, }) @IsBoolean({ groups: [ValidationsGroupsEnum.default] }) @Transform((value: TransformFnParams) => value?.value === 'true', { diff --git a/backend_new/src/dtos/users/user-create.dto.ts b/backend_new/src/dtos/users/user-create.dto.ts index c1de34e684..c8d726505d 100644 --- a/backend_new/src/dtos/users/user-create.dto.ts +++ b/backend_new/src/dtos/users/user-create.dto.ts @@ -1,4 +1,4 @@ -import { ApiProperty, OmitType } from '@nestjs/swagger'; +import { ApiProperty, ApiPropertyOptional, OmitType } from '@nestjs/swagger'; import { Expose, Type } from 'class-transformer'; import { IsArray, @@ -25,7 +25,7 @@ export class UserCreate extends OmitType(UserUpdate, [ 'jurisdictions', ]) { @Expose() - @ApiProperty({ required: true }) + @ApiProperty() @IsString({ groups: [ValidationsGroupsEnum.default] }) @Matches(passwordRegex, { message: 'passwordTooWeak', @@ -37,17 +37,17 @@ export class UserCreate extends OmitType(UserUpdate, [ @IsString({ groups: [ValidationsGroupsEnum.default] }) @MaxLength(64, { groups: [ValidationsGroupsEnum.default] }) @Match('password', { groups: [ValidationsGroupsEnum.default] }) - @ApiProperty({ required: true }) + @ApiProperty() passwordConfirmation: string; @Expose() - @ApiProperty({ required: true }) + @ApiProperty() @IsEmail({}, { groups: [ValidationsGroupsEnum.default] }) @EnforceLowerCase() email: string; @Expose() - @ApiProperty({ required: true }) + @ApiProperty() @IsEmail({}, { groups: [ValidationsGroupsEnum.default] }) @Match('email', { groups: [ValidationsGroupsEnum.default] }) @EnforceLowerCase() @@ -57,6 +57,6 @@ export class UserCreate extends OmitType(UserUpdate, [ @Type(() => IdDTO) @IsArray({ groups: [ValidationsGroupsEnum.default] }) @ValidateNested({ groups: [ValidationsGroupsEnum.default], each: true }) - @ApiProperty({ type: IdDTO, isArray: true, required: false }) + @ApiPropertyOptional({ type: IdDTO, isArray: true }) jurisdictions?: IdDTO[]; } diff --git a/backend_new/src/dtos/users/user-filter-params.dto.ts b/backend_new/src/dtos/users/user-filter-params.dto.ts index 8144cb82ce..686a551084 100644 --- a/backend_new/src/dtos/users/user-filter-params.dto.ts +++ b/backend_new/src/dtos/users/user-filter-params.dto.ts @@ -1,14 +1,13 @@ import { Expose } from 'class-transformer'; -import { ApiProperty } from '@nestjs/swagger'; +import { ApiPropertyOptional } from '@nestjs/swagger'; import { IsString } from 'class-validator'; import { ValidationsGroupsEnum } from '../../enums/shared/validation-groups-enum'; export class UserFilterParams { @Expose() - @ApiProperty({ + @ApiPropertyOptional({ type: Boolean, example: true, - required: false, }) @IsString({ groups: [ValidationsGroupsEnum.default] }) isPortalUser?: boolean; diff --git a/backend_new/src/dtos/users/user-invite.dto.ts b/backend_new/src/dtos/users/user-invite.dto.ts index 4456eee300..e0a1beef87 100644 --- a/backend_new/src/dtos/users/user-invite.dto.ts +++ b/backend_new/src/dtos/users/user-invite.dto.ts @@ -13,7 +13,7 @@ export class UserInvite extends OmitType(UserUpdate, [ 'email', ]) { @Expose() - @ApiProperty({ required: true }) + @ApiProperty() @IsEmail({}, { groups: [ValidationsGroupsEnum.default] }) @EnforceLowerCase() email: string; diff --git a/backend_new/src/dtos/users/user-query-param.dto.ts b/backend_new/src/dtos/users/user-query-param.dto.ts index 119d7fca2b..f453ed83f7 100644 --- a/backend_new/src/dtos/users/user-query-param.dto.ts +++ b/backend_new/src/dtos/users/user-query-param.dto.ts @@ -1,6 +1,6 @@ import { PaginationAllowsAllQueryParams } from '../shared/pagination.dto'; import { Expose, Type } from 'class-transformer'; -import { ApiProperty } from '@nestjs/swagger'; +import { ApiPropertyOptional } from '@nestjs/swagger'; import { UserFilterParams } from './user-filter-params.dto'; import { ArrayMaxSize, @@ -13,9 +13,8 @@ import { ValidationsGroupsEnum } from '../../enums/shared/validation-groups-enum export class UserQueryParams extends PaginationAllowsAllQueryParams { @Expose() - @ApiProperty({ + @ApiPropertyOptional({ name: 'filter', - required: false, type: [UserFilterParams], example: { isPartner: true }, }) @@ -26,10 +25,9 @@ export class UserQueryParams extends PaginationAllowsAllQueryParams { filter?: UserFilterParams[]; @Expose() - @ApiProperty({ + @ApiPropertyOptional({ type: String, example: 'search', - required: false, }) @IsString({ groups: [ValidationsGroupsEnum.default] }) @MinLength(3, { diff --git a/backend_new/src/dtos/users/user-role.dto.ts b/backend_new/src/dtos/users/user-role.dto.ts index 1a9fb023e0..a15e2015ca 100644 --- a/backend_new/src/dtos/users/user-role.dto.ts +++ b/backend_new/src/dtos/users/user-role.dto.ts @@ -1,4 +1,4 @@ -import { ApiProperty } from '@nestjs/swagger'; +import { ApiPropertyOptional } from '@nestjs/swagger'; import { Expose } from 'class-transformer'; import { IsBoolean } from 'class-validator'; import { ValidationsGroupsEnum } from '../../enums/shared/validation-groups-enum'; @@ -6,16 +6,16 @@ import { ValidationsGroupsEnum } from '../../enums/shared/validation-groups-enum export class UserRole { @Expose() @IsBoolean({ groups: [ValidationsGroupsEnum.default] }) - @ApiProperty({ required: false }) + @ApiPropertyOptional() isAdmin?: boolean; @Expose() @IsBoolean({ groups: [ValidationsGroupsEnum.default] }) - @ApiProperty({ required: false }) + @ApiPropertyOptional() isJurisdictionalAdmin?: boolean; @Expose() @IsBoolean({ groups: [ValidationsGroupsEnum.default] }) - @ApiProperty({ required: false }) + @ApiPropertyOptional() isPartner?: boolean; } diff --git a/backend_new/src/dtos/users/user-update.dto.ts b/backend_new/src/dtos/users/user-update.dto.ts index 023724520d..06c24dbd7b 100644 --- a/backend_new/src/dtos/users/user-update.dto.ts +++ b/backend_new/src/dtos/users/user-update.dto.ts @@ -1,4 +1,4 @@ -import { ApiProperty, OmitType } from '@nestjs/swagger'; +import { ApiPropertyOptional, OmitType } from '@nestjs/swagger'; import { Expose } from 'class-transformer'; import { IsEmail, @@ -32,19 +32,19 @@ export class UserUpdate extends OmitType(User, [ 'activeRefreshToken', ]) { @Expose() - @ApiProperty({ required: false }) + @ApiPropertyOptional() @IsEmail({}, { groups: [ValidationsGroupsEnum.default] }) @EnforceLowerCase() email?: string; @Expose() - @ApiProperty({ required: false }) + @ApiPropertyOptional() @IsEmail({}, { groups: [ValidationsGroupsEnum.default] }) @EnforceLowerCase() newEmail?: string; @Expose() - @ApiProperty({ required: false }) + @ApiPropertyOptional() @IsString({ groups: [ValidationsGroupsEnum.default] }) @Matches(passwordRegex, { message: 'passwordTooWeak', @@ -55,12 +55,12 @@ export class UserUpdate extends OmitType(User, [ @Expose() @ValidateIf((o) => o.password, { groups: [ValidationsGroupsEnum.default] }) @IsNotEmpty({ groups: [ValidationsGroupsEnum.default] }) - @ApiProperty({ required: false }) + @ApiPropertyOptional() currentPassword?: string; @Expose() @IsString({ groups: [ValidationsGroupsEnum.default] }) @MaxLength(256, { groups: [ValidationsGroupsEnum.default] }) - @ApiProperty({ required: false }) + @ApiPropertyOptional() appUrl?: string; } diff --git a/backend_new/src/dtos/users/user.dto.ts b/backend_new/src/dtos/users/user.dto.ts index 7ba94a683b..3c1cdc9f65 100644 --- a/backend_new/src/dtos/users/user.dto.ts +++ b/backend_new/src/dtos/users/user.dto.ts @@ -1,4 +1,4 @@ -import { ApiProperty } from '@nestjs/swagger'; +import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; import { Expose, Type } from 'class-transformer'; import { ArrayMinSize, @@ -35,7 +35,7 @@ export class User extends AbstractDTO { @Expose() @IsDate({ groups: [ValidationsGroupsEnum.default] }) @Type(() => Date) - @ApiProperty({ required: false }) + @ApiPropertyOptional() confirmedAt?: Date; @Expose() @@ -50,7 +50,7 @@ export class User extends AbstractDTO { firstName: string; @Expose() - @ApiProperty({ required: false }) + @ApiPropertyOptional() @IsString({ groups: [ValidationsGroupsEnum.default] }) @MaxLength(64, { groups: [ValidationsGroupsEnum.default] }) middleName?: string; @@ -62,13 +62,13 @@ export class User extends AbstractDTO { lastName: string; @Expose() - @ApiProperty({ required: false }) + @ApiPropertyOptional() @IsDate({ groups: [ValidationsGroupsEnum.default] }) @Type(() => Date) dob?: Date; @Expose() - @ApiProperty({ required: false }) + @ApiPropertyOptional() @IsPhoneNumber('US', { groups: [ValidationsGroupsEnum.default] }) phoneNumber?: string; @@ -79,15 +79,14 @@ export class User extends AbstractDTO { @Expose() @Type(() => UserRole) - @ApiProperty({ type: UserRole, required: false }) + @ApiPropertyOptional({ type: UserRole }) userRoles?: UserRole; @Expose() @IsEnum(LanguagesEnum, { groups: [ValidationsGroupsEnum.default] }) - @ApiProperty({ + @ApiPropertyOptional({ enum: LanguagesEnum, enumName: 'LanguagesEnum', - required: false, }) language?: LanguagesEnum; @@ -101,21 +100,21 @@ export class User extends AbstractDTO { @Expose() @IsBoolean({ groups: [ValidationsGroupsEnum.default] }) - @ApiProperty({ required: false }) + @ApiPropertyOptional() mfaEnabled?: boolean; @Expose() @Type(() => Date) - @ApiProperty({ required: false }) + @ApiPropertyOptional() lastLoginAt?: Date; @Expose() @Type(() => Number) - @ApiProperty({ required: false }) + @ApiPropertyOptional() failedLoginAttemptsCount?: number; @Expose() - @ApiProperty({ required: false }) + @ApiPropertyOptional() @IsBoolean({ groups: [ValidationsGroupsEnum.default] }) phoneNumberVerified?: boolean; @@ -127,18 +126,18 @@ export class User extends AbstractDTO { @Expose() @IsDate({ groups: [ValidationsGroupsEnum.default] }) @Type(() => Date) - @ApiProperty({ required: false }) + @ApiPropertyOptional() hitConfirmationURL?: Date; // storing the active access token for a user @Expose() @IsString({ groups: [ValidationsGroupsEnum.default] }) - @ApiProperty({ required: false }) + @ApiPropertyOptional() activeAccessToken?: string; // storing the active refresh token for a user @Expose() @IsString({ groups: [ValidationsGroupsEnum.default] }) - @ApiProperty({ required: false }) + @ApiPropertyOptional() activeRefreshToken?: string; } diff --git a/backend_new/src/modules/listing.module.ts b/backend_new/src/modules/listing.module.ts index 6fd7f1f088..45d5a43814 100644 --- a/backend_new/src/modules/listing.module.ts +++ b/backend_new/src/modules/listing.module.ts @@ -1,4 +1,5 @@ import { Module } from '@nestjs/common'; +import { HttpModule } from '@nestjs/axios'; import { ListingController } from '../controllers/listing.controller'; import { ListingService } from '../services/listing.service'; import { PrismaModule } from './prisma.module'; @@ -6,7 +7,7 @@ import { TranslationService } from '../services/translation.service'; import { GoogleTranslateService } from '../services/google-translate.service'; @Module({ - imports: [PrismaModule], + imports: [PrismaModule, HttpModule], controllers: [ListingController], providers: [ListingService, TranslationService, GoogleTranslateService], exports: [ListingService], diff --git a/backend_new/src/services/ami-chart.service.ts b/backend_new/src/services/ami-chart.service.ts index 229c9cfedc..48a62c9397 100644 --- a/backend_new/src/services/ami-chart.service.ts +++ b/backend_new/src/services/ami-chart.service.ts @@ -7,7 +7,7 @@ import { AmiChartUpdate } from '../dtos/ami-charts/ami-chart-update.dto'; import { AmiChartQueryParams } from '../dtos/ami-charts/ami-chart-query-params.dto'; import { mapTo } from '../utilities/mapTo'; import { SuccessDTO } from '../dtos/shared/success.dto'; -import { AmiChartItem } from '../dtos/units/ami-chart-item-get.dto'; +import { AmiChartItem } from '../dtos/units/ami-chart-item.dto'; /* this is the service for ami charts diff --git a/backend_new/src/services/listing.service.ts b/backend_new/src/services/listing.service.ts index 5da75bd4a9..629f4b60dd 100644 --- a/backend_new/src/services/listing.service.ts +++ b/backend_new/src/services/listing.service.ts @@ -1,6 +1,13 @@ import { Injectable, NotFoundException } from '@nestjs/common'; +import { HttpService } from '@nestjs/axios'; import { PrismaService } from './prisma.service'; -import { LanguagesEnum, Prisma } from '@prisma/client'; +import { + LanguagesEnum, + ListingsStatusEnum, + Prisma, + ReviewOrderTypeEnum, +} from '@prisma/client'; +import { firstValueFrom } from 'rxjs'; import { ListingsQueryParams } from '../dtos/listings/listings-query-params.dto'; import { buildPaginationMetaInfo, @@ -11,7 +18,7 @@ import { buildOrderBy } from '../utilities/build-order-by'; import { ListingFilterParams } from '../dtos/listings/listings-filter-params.dto'; import { ListingFilterKeys } from '../enums/listings/filter-key-enum'; import { buildFilter } from '../utilities/build-filter'; -import { ListingGet } from '../dtos/listings/listing-get.dto'; +import { Listing } from '../dtos/listings/listing.dto'; import { mapTo } from '../utilities/mapTo'; import { summarizeUnitsByTypeAndRent, @@ -20,6 +27,9 @@ import { import { AmiChart } from '../dtos/ami-charts/ami-chart.dto'; import { ListingViews } from '../enums/listings/view-enum'; import { TranslationService } from './translation.service'; +import { ListingCreate } from '../dtos/listings/listing-create.dto'; +import { SuccessDTO } from '../dtos/shared/success.dto'; +import { ListingUpdate } from '../dtos/listings/listing-update.dto'; export type getListingsArgs = { skip: number; @@ -115,6 +125,7 @@ export class ListingService { constructor( private prisma: PrismaService, private translationService: TranslationService, + private httpService: HttpService, ) {} /* @@ -123,7 +134,7 @@ export class ListingService { it will return both the set of listings, and some meta information to help with pagination */ async list(params: ListingsQueryParams): Promise<{ - items: ListingGet[]; + items: Listing[]; meta: { currentPage: number; itemCount: number; @@ -146,7 +157,7 @@ export class ListingService { where: whereClause, }); - const listings = mapTo(ListingGet, listingsRaw); + const listings = mapTo(Listing, listingsRaw); listings.forEach((listing) => { if (Array.isArray(listing.units) && listing.units.length > 0) { @@ -300,34 +311,612 @@ export class ListingService { listingId: string, lang: LanguagesEnum = LanguagesEnum.en, view: ListingViews = ListingViews.full, - ): Promise { - const listingRaw = await this.prisma.listings.findFirst({ - include: views[view], + ): Promise { + const listingRaw = await this.findOrThrow(listingId, view); + + let result = mapTo(Listing, listingRaw); + + if (lang !== LanguagesEnum.en) { + result = await this.translationService.translateListing(result, lang); + } + + await this.addUnitsSummarized(result); + return result; + } + + /* + creates a listing + */ + async create(dto: ListingCreate): Promise { + // TODO: perms (https://github.com/bloom-housing/bloom/issues/3445) + const rawListing = await this.prisma.listings.create({ + include: views.details, + data: { + ...dto, + assets: dto.assets + ? { + create: dto.assets.map((asset) => ({ + fileId: asset.fileId, + label: asset.label, + })), + } + : undefined, + applicationMethods: dto.applicationMethods + ? { + create: dto.applicationMethods.map((applicationMethod) => ({ + ...applicationMethod, + paperApplications: applicationMethod.paperApplications + ? { + create: applicationMethod.paperApplications.map( + (paperApplication) => ({ + ...paperApplication, + assets: { + create: { + ...paperApplication.assets, + }, + }, + }), + ), + } + : undefined, + })), + } + : undefined, + listingEvents: dto.listingEvents + ? { + create: dto.listingEvents.map((event) => ({ + type: event.type, + startDate: event.startDate, + startTime: event.startTime, + endTime: event.endTime, + url: event.url, + note: event.note, + label: event.label, + assets: { + create: { + ...event.assets, + }, + }, + })), + } + : undefined, + listingImages: dto.listingImages + ? { + create: dto.listingImages.map((image) => ({ + assets: { + create: { + ...image.assets, + }, + }, + ordinal: image.ordinal, + })), + } + : undefined, + listingMultiselectQuestions: dto.listingMultiselectQuestions + ? { + create: dto.listingMultiselectQuestions.map( + (multiselectQuestion) => ({ + ordinal: multiselectQuestion.ordinal, + multiselectQuestions: { + connect: { + id: multiselectQuestion.id, + }, + }, + }), + ), + } + : undefined, + listingsApplicationDropOffAddress: dto.listingsApplicationDropOffAddress + ? { + create: { + ...dto.listingsApplicationDropOffAddress, + }, + } + : undefined, + reservedCommunityTypes: dto.reservedCommunityTypes + ? { + connect: { + id: dto.reservedCommunityTypes.id, + }, + } + : undefined, + listingsBuildingSelectionCriteriaFile: + dto.listingsBuildingSelectionCriteriaFile + ? { + create: { + ...dto.listingsBuildingSelectionCriteriaFile, + }, + } + : undefined, + listingUtilities: dto.listingUtilities + ? { + create: { + ...dto.listingUtilities, + }, + } + : undefined, + listingsApplicationMailingAddress: dto.listingsApplicationMailingAddress + ? { + create: { + ...dto.listingsApplicationMailingAddress, + }, + } + : undefined, + listingsLeasingAgentAddress: dto.listingsLeasingAgentAddress + ? { + create: { + ...dto.listingsLeasingAgentAddress, + }, + } + : undefined, + listingFeatures: dto.listingFeatures + ? { + create: { + ...dto.listingFeatures, + }, + } + : undefined, + jurisdictions: dto.jurisdictions + ? { + connect: { + id: dto.jurisdictions.id, + }, + } + : undefined, + listingsApplicationPickUpAddress: dto.listingsApplicationPickUpAddress + ? { + create: { + ...dto.listingsApplicationPickUpAddress, + }, + } + : undefined, + listingsBuildingAddress: dto.listingsBuildingAddress + ? { + create: { + ...dto.listingsBuildingAddress, + }, + } + : undefined, + units: dto.units + ? { + create: dto.units.map((unit) => ({ + amiPercentage: unit.amiPercentage, + annualIncomeMin: unit.annualIncomeMin, + monthlyIncomeMin: unit.monthlyIncomeMin, + floor: unit.floor, + annualIncomeMax: unit.annualIncomeMax, + maxOccupancy: unit.maxOccupancy, + minOccupancy: unit.minOccupancy, + monthlyRent: unit.monthlyRent, + numBathrooms: unit.numBathrooms, + numBedrooms: unit.numBedrooms, + number: unit.number, + sqFeet: unit.sqFeet, + monthlyRentAsPercentOfIncome: unit.monthlyRentAsPercentOfIncome, + bmrProgramChart: unit.bmrProgramChart, + unitTypes: unit.unitTypes + ? { + connect: { + id: unit.unitTypes.id, + }, + } + : undefined, + amiChart: unit.amiChart + ? { + connect: { + id: unit.amiChart.id, + }, + } + : undefined, + unitAmiChartOverrides: unit.unitAmiChartOverrides + ? { + create: { + items: unit.unitAmiChartOverrides, + }, + } + : undefined, + unitAccessibilityPriorityTypes: + unit.unitAccessibilityPriorityTypes + ? { + connect: { + id: unit.unitAccessibilityPriorityTypes.id, + }, + } + : undefined, + unitRentTypes: unit.unitRentTypes + ? { + connect: { + id: unit.unitRentTypes.id, + }, + } + : undefined, + })), + } + : undefined, + unitsSummary: dto.unitsSummary + ? { + create: dto.unitsSummary.map((unitSummary) => ({ + ...unitSummary, + unitTypes: unitSummary.unitTypes + ? { + connect: { + id: unitSummary.unitTypes.id, + }, + } + : undefined, + unitAccessibilityPriorityTypes: + unitSummary.unitAccessibilityPriorityTypes + ? { + connect: { + id: unitSummary.unitAccessibilityPriorityTypes.id, + }, + } + : undefined, + })), + } + : undefined, + listingsResult: dto.listingsResult + ? { + create: { + ...dto.listingsResult, + }, + } + : undefined, + }, + }); + + return mapTo(Listing, rawListing); + } + + /* + deletes a listing + */ + async delete(id: string): Promise { + const storedListing = await this.findOrThrow(id); + + // TODO: perms (https://github.com/bloom-housing/bloom/issues/3445) + + await this.prisma.listings.delete({ where: { - id: { - equals: listingId, - }, + id, }, }); - let result = mapTo(ListingGet, listingRaw); + return { + success: true, + } as SuccessDTO; + } + + /* + This will either find a listing or throw an error + a listing view can be provided which will add the joins to produce that view correctly + */ + async findOrThrow(id: string, view?: ListingViews) { + const listing = await this.prisma.listings.findUnique({ + include: view ? views[view] : undefined, + where: { + id, + }, + }); - if (!result) { - throw new NotFoundException(); + if (!listing) { + throw new NotFoundException( + `listingId ${id} was requested but not found`, + ); } + return listing; + } - if (lang !== LanguagesEnum.en) { - result = await this.translationService.translateListing(result, lang); + /* + update a listing + */ + async update(dto: ListingUpdate): Promise { + const storedListing = await this.findOrThrow(dto.id, ListingViews.details); + + // TODO: perms (https://github.com/bloom-housing/bloom/issues/3445) + + dto.unitsAvailable = + dto.reviewOrderType !== ReviewOrderTypeEnum.waitlist && dto.units + ? dto.units.length + : 0; + + if ( + storedListing.status === ListingsStatusEnum.active && + dto.status === ListingsStatusEnum.closed + ) { + // TODO: afs process (https://github.com/bloom-housing/bloom/issues/3540) } - await this.addUnitsSummarized(result); - return result; + const rawListing = await this.prisma.listings.update({ + data: { + ...dto, + id: undefined, + createdAt: undefined, + updatedAt: undefined, + assets: dto.assets + ? { + create: dto.assets.map((asset) => ({ + ...asset, + })), + } + : undefined, + applicationMethods: dto.applicationMethods + ? { + create: dto.applicationMethods.map((applicationMethod) => ({ + ...applicationMethod, + paperApplications: applicationMethod.paperApplications + ? { + create: applicationMethod.paperApplications.map( + (paperApplication) => ({ + ...paperApplication, + assets: { + create: { + ...paperApplication.assets, + }, + }, + }), + ), + } + : undefined, + })), + } + : undefined, + listingEvents: dto.listingEvents + ? { + create: dto.listingEvents.map((event) => ({ + type: event.type, + startDate: event.startDate, + startTime: event.startTime, + endTime: event.endTime, + url: event.url, + note: event.note, + label: event.label, + assets: { + create: { + ...event.assets, + }, + }, + })), + } + : undefined, + listingImages: dto.listingImages + ? { + create: dto.listingImages.map((image) => ({ + assets: { + create: { + ...image.assets, + }, + }, + ordinal: image.ordinal, + })), + } + : undefined, + listingMultiselectQuestions: dto.listingMultiselectQuestions + ? { + create: dto.listingMultiselectQuestions.map( + (multiselectQuestion) => ({ + ordinal: multiselectQuestion.ordinal, + multiselectQuestions: { + connect: { + id: multiselectQuestion.id, + }, + }, + }), + ), + } + : undefined, + listingsApplicationDropOffAddress: dto.listingsApplicationDropOffAddress + ? { + create: { + ...dto.listingsApplicationDropOffAddress, + }, + } + : undefined, + reservedCommunityTypes: dto.reservedCommunityTypes + ? { + connect: { + id: dto.reservedCommunityTypes.id, + }, + } + : undefined, + listingsBuildingSelectionCriteriaFile: + dto.listingsBuildingSelectionCriteriaFile + ? { + create: { + ...dto.listingsBuildingSelectionCriteriaFile, + }, + } + : undefined, + listingUtilities: dto.listingUtilities + ? { + create: { + ...dto.listingUtilities, + }, + } + : undefined, + listingsApplicationMailingAddress: dto.listingsApplicationMailingAddress + ? { + create: { + ...dto.listingsApplicationMailingAddress, + }, + } + : undefined, + listingsLeasingAgentAddress: dto.listingsLeasingAgentAddress + ? { + create: { + ...dto.listingsLeasingAgentAddress, + }, + } + : undefined, + listingFeatures: dto.listingFeatures + ? { + create: { + ...dto.listingFeatures, + }, + } + : undefined, + jurisdictions: dto.jurisdictions + ? { + connect: { + id: dto.jurisdictions.id, + }, + } + : undefined, + listingsApplicationPickUpAddress: dto.listingsApplicationPickUpAddress + ? { + create: { + ...dto.listingsApplicationPickUpAddress, + }, + } + : undefined, + listingsBuildingAddress: dto.listingsBuildingAddress + ? { + create: { + ...dto.listingsBuildingAddress, + }, + } + : undefined, + units: dto.units + ? { + create: dto.units.map((unit) => ({ + amiPercentage: unit.amiPercentage, + annualIncomeMin: unit.annualIncomeMin, + monthlyIncomeMin: unit.monthlyIncomeMin, + floor: unit.floor, + annualIncomeMax: unit.annualIncomeMax, + maxOccupancy: unit.maxOccupancy, + minOccupancy: unit.minOccupancy, + monthlyRent: unit.monthlyRent, + numBathrooms: unit.numBathrooms, + numBedrooms: unit.numBedrooms, + number: unit.number, + sqFeet: unit.sqFeet, + monthlyRentAsPercentOfIncome: unit.monthlyRentAsPercentOfIncome, + bmrProgramChart: unit.bmrProgramChart, + unitTypes: unit.unitTypes + ? { + connect: { + id: unit.unitTypes.id, + }, + } + : undefined, + amiChart: unit.amiChart + ? { + connect: { + id: unit.amiChart.id, + }, + } + : undefined, + unitAmiChartOverrides: unit.unitAmiChartOverrides + ? { + create: { + items: unit.unitAmiChartOverrides, + }, + } + : undefined, + unitAccessibilityPriorityTypes: + unit.unitAccessibilityPriorityTypes + ? { + connect: { + id: unit.unitAccessibilityPriorityTypes.id, + }, + } + : undefined, + unitRentTypes: unit.unitRentTypes + ? { + connect: { + id: unit.unitRentTypes.id, + }, + } + : undefined, + })), + } + : undefined, + unitsSummary: dto.unitsSummary + ? { + create: dto.unitsSummary.map((unitSummary) => ({ + ...unitSummary, + unitTypes: unitSummary.unitTypes + ? { + connect: { + id: unitSummary.unitTypes.id, + }, + } + : undefined, + unitAccessibilityPriorityTypes: + unitSummary.unitAccessibilityPriorityTypes + ? { + connect: { + id: unitSummary.unitAccessibilityPriorityTypes.id, + }, + } + : undefined, + })), + } + : undefined, + publishedAt: + storedListing.status !== ListingsStatusEnum.active && + dto.status === ListingsStatusEnum.active + ? new Date() + : storedListing.publishedAt, + closedAt: + storedListing.status !== ListingsStatusEnum.closed && + dto.status === ListingsStatusEnum.closed + ? new Date() + : storedListing.closedAt, + listingsResult: dto.listingsResult + ? { + create: { + ...dto.listingsResult, + }, + } + : undefined, + }, + include: views.details, + where: { + id: dto.id, + }, + }); + + await this.cachePurge(storedListing.status, dto.status, rawListing.id); + + return mapTo(Listing, rawListing); + } + + async cachePurge( + storedListingStatus: ListingsStatusEnum, + incomingListingStatus: ListingsStatusEnum, + savedResponseId: string, + ): Promise { + if (!process.env.PROXY_URL) { + return; + } + const shouldPurgeAllListings = + incomingListingStatus !== ListingsStatusEnum.pending || + storedListingStatus === ListingsStatusEnum.active; + await firstValueFrom( + this.httpService.request({ + baseURL: process.env.PROXY_URL, + method: 'PURGE', + url: shouldPurgeAllListings + ? '/listings?*' + : `/listings/${savedResponseId}*`, + }), + undefined, + ).catch((e) => + console.error( + shouldPurgeAllListings + ? 'purge all listings error = ' + : `purge listing ${savedResponseId} error = `, + e, + ), + ); } /* this builds the units summarized for the list() */ - addUnitsSummarized = async (listing: ListingGet) => { + addUnitsSummarized = async (listing: Listing) => { if (Array.isArray(listing.units) && listing.units.length > 0) { const amiChartsRaw = await this.prisma.amiChart.findMany({ where: { @@ -361,6 +950,6 @@ export class ListingService { }, }, }); - return mapTo(ListingGet, listingsRaw); + return mapTo(Listing, listingsRaw); }; } diff --git a/backend_new/src/services/multiselect-question.service.ts b/backend_new/src/services/multiselect-question.service.ts index 57df3bb792..0d929eb13a 100644 --- a/backend_new/src/services/multiselect-question.service.ts +++ b/backend_new/src/services/multiselect-question.service.ts @@ -1,6 +1,6 @@ import { Injectable, NotFoundException } from '@nestjs/common'; import { PrismaService } from './prisma.service'; -import { MultiselectQuestion } from '../dtos/multiselect-questions/multiselect-question-get.dto'; +import { MultiselectQuestion } from '../dtos/multiselect-questions/multiselect-question.dto'; import { MultiselectQuestionUpdate } from '../dtos/multiselect-questions/multiselect-question-update.dto'; import { MultiselectQuestionCreate } from '../dtos/multiselect-questions/multiselect-question-create.dto'; import { mapTo } from '../utilities/mapTo'; diff --git a/backend_new/src/services/translation.service.ts b/backend_new/src/services/translation.service.ts index 9b06c30851..0f67bea8bf 100644 --- a/backend_new/src/services/translation.service.ts +++ b/backend_new/src/services/translation.service.ts @@ -1,7 +1,7 @@ import { Injectable } from '@nestjs/common'; import { PrismaService } from './prisma.service'; import { LanguagesEnum } from '@prisma/client'; -import { ListingGet } from '../dtos/listings/listing-get.dto'; +import { Listing } from '../dtos/listings/listing.dto'; import { GoogleTranslateService } from './google-translate.service'; import * as lodash from 'lodash'; @@ -38,7 +38,7 @@ export class TranslationService { return translations; } - public async translateListing(listing: ListingGet, language: LanguagesEnum) { + public async translateListing(listing: Listing, language: LanguagesEnum) { if (!this.googleTranslateService.isConfigured()) { console.warn( 'listing translation requested, but google translate service is not configured', @@ -67,9 +67,11 @@ export class TranslationService { unitAmenities: listing.unitAmenities, }; - listing.events?.forEach((_, index) => { - pathsToFilter[`events[${index}].note`] = listing.events[index].note; - pathsToFilter[`events[${index}].label`] = listing.events[index].label; + listing.listingEvents?.forEach((_, index) => { + pathsToFilter[`listingEvents[${index}].note`] = + listing.listingEvents[index].note; + pathsToFilter[`listingEvents[${index}].label`] = + listing.listingEvents[index].label; }); if (listing.listingMultiselectQuestions) { @@ -135,7 +137,7 @@ export class TranslationService { } private async getPersistedTranslatedValues( - listing: ListingGet, + listing: Listing, language: LanguagesEnum, ) { return this.prisma.generatedListingTranslations.findFirst({ @@ -144,7 +146,7 @@ export class TranslationService { } private async persistNewTranslatedValues( - listing: ListingGet, + listing: Listing, language: LanguagesEnum, translatedValues: any, ) { diff --git a/backend_new/src/utilities/listing-url-slug.ts b/backend_new/src/utilities/listing-url-slug.ts new file mode 100644 index 0000000000..77a9de5203 --- /dev/null +++ b/backend_new/src/utilities/listing-url-slug.ts @@ -0,0 +1,35 @@ +import Listing from '../dtos/listings/listing.dto'; + +/* + This maps a listing to its url slug + This is used by the public site front end + */ +export function listingUrlSlug(listing: Listing): string { + const { name, listingsBuildingAddress } = listing; + if (!listingsBuildingAddress) { + return listingUrlSlugHelper(name); + } + return listingUrlSlugHelper( + [ + name, + listingsBuildingAddress.street, + listingsBuildingAddress.city, + listingsBuildingAddress.state, + ].join(' '), + ); +} + +/* + This creates a string "_" separated at every upper case letter then lower cased + This also removes special characters + e.g. "ExampLe namE @ 17 11th Street Phoenix Az" -> "examp_le_nam_e_17_11_th_street_phoenix_az" +*/ +export function listingUrlSlugHelper(input: string): string { + return ( + (input || '').match( + /[A-Z]{2,}(?=[A-Z][a-z]+[0-9]*|\b)|[A-Z]?[a-z]+[0-9]*|[A-Z]+|[0-9]+/g, + ) || [] + ) + .join('_') + .toLowerCase(); +} diff --git a/backend_new/src/utilities/unit-utilities.ts b/backend_new/src/utilities/unit-utilities.ts index bbe577ec9f..702b58e96a 100644 --- a/backend_new/src/utilities/unit-utilities.ts +++ b/backend_new/src/utilities/unit-utilities.ts @@ -1,15 +1,15 @@ import { ReviewOrderTypeEnum, UnitTypeEnum } from '@prisma/client'; -import { UnitSummary } from '../dtos/units/unit-summary-get.dto'; -import Unit from '../dtos/units/unit-get.dto'; +import { UnitSummary } from '../dtos/units/unit-summary.dto'; +import Unit from '../dtos/units/unit.dto'; import { AmiChart } from '../dtos/ami-charts/ami-chart.dto'; -import listingGetDto, { ListingGet } from '../dtos/listings/listing-get.dto'; +import { Listing } from '../dtos/listings/listing.dto'; import { MinMaxCurrency } from '../dtos/shared/min-max-currency.dto'; import { MinMax } from '../dtos/shared/min-max.dto'; import { UnitsSummarized } from '../dtos/units/unit-summarized.dto'; import { UnitType } from '../dtos/unit-types/unit-type.dto'; import { UnitAccessibilityPriorityType } from '../dtos/unit-accessibility-priority-types/unit-accessibility-priority-type.dto'; -import { AmiChartItem } from '../dtos/units/ami-chart-item-get.dto'; -import { UnitAmiChartOverride } from '../dtos/units/ami-chart-override-get.dto'; +import { AmiChartItem } from '../dtos/units/ami-chart-item.dto'; +import { UnitAmiChartOverride } from '../dtos/units/ami-chart-override.dto'; type AnyDict = { [key: string]: unknown }; type UnitMap = { @@ -402,7 +402,7 @@ export const getUnitsSummary = (unit: Unit, existingSummary?: UnitSummary) => { // Allows for multiples rows under one unit type if the rent methods differ export const summarizeUnitsByTypeAndRent = ( units: Unit[], - listing: ListingGet, + listing: Listing, ): UnitSummary[] => { const summaries: UnitSummary[] = []; const unitMap: UnitMap = {}; @@ -463,10 +463,7 @@ export const summarizeUnitsByType = ( }); }; -export const summarizeByAmi = ( - listing: listingGetDto, - amiPercentages: string[], -) => { +export const summarizeByAmi = (listing: Listing, amiPercentages: string[]) => { return amiPercentages.map((percent: string) => { const unitsByAmiPercentage = listing.units.filter( (unit: Unit) => unit.amiPercentage == percent, @@ -490,7 +487,7 @@ export const getUnitTypes = (units: Unit[]): UnitType[] => { }; export const summarizeUnits = ( - listing: ListingGet, + listing: Listing, amiCharts: AmiChart[], ): UnitsSummarized => { const data = {} as UnitsSummarized; diff --git a/backend_new/src/validation-pipes/listing-create-update-pipe.ts b/backend_new/src/validation-pipes/listing-create-update-pipe.ts new file mode 100644 index 0000000000..ecc64d2ba8 --- /dev/null +++ b/backend_new/src/validation-pipes/listing-create-update-pipe.ts @@ -0,0 +1,39 @@ +import { ArgumentMetadata, ValidationPipe } from '@nestjs/common'; +import { ListingsStatusEnum } from '@prisma/client'; +import { ListingUpdate } from '../dtos/listings/listing-update.dto'; +import { ListingPublishedUpdate } from '../dtos/listings/listing-published-update.dto'; +import { ListingCreate } from '../dtos/listings/listing-create.dto'; +import { ListingPublishedCreate } from '../dtos/listings/listing-published-create.dto'; + +export class ListingCreateUpdateValidationPipe extends ValidationPipe { + statusToListingValidationModelMapForUpdate: Record< + ListingsStatusEnum, + typeof ListingUpdate + > = { + [ListingsStatusEnum.closed]: ListingUpdate, + [ListingsStatusEnum.pending]: ListingUpdate, + [ListingsStatusEnum.active]: ListingPublishedUpdate, + }; + + statusToListingValidationModelMapForCreate: Record< + ListingsStatusEnum, + typeof ListingCreate + > = { + [ListingsStatusEnum.closed]: ListingCreate, + [ListingsStatusEnum.pending]: ListingCreate, + [ListingsStatusEnum.active]: ListingPublishedCreate, + }; + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + async transform(value: any, metadata: ArgumentMetadata): Promise { + if (metadata.type === 'body') { + return await super.transform(value, { + ...metadata, + metatype: value.id + ? this.statusToListingValidationModelMapForUpdate[value.status] + : this.statusToListingValidationModelMapForCreate[value.status], + }); + } + return await super.transform(value, metadata); + } +} diff --git a/backend_new/test/integration/listing.e2e-spec.ts b/backend_new/test/integration/listing.e2e-spec.ts index c2dd56d89f..05c1e13869 100644 --- a/backend_new/test/integration/listing.e2e-spec.ts +++ b/backend_new/test/integration/listing.e2e-spec.ts @@ -11,6 +11,30 @@ import { Compare } from '../../src/dtos/shared/base-filter.dto'; import { ListingOrderByKeys } from '../../src/enums/listings/order-by-enum'; import { OrderByEnum } from '../../src/enums/shared/order-by-enum'; import { ListingViews } from '../../src/enums/listings/view-enum'; +import { randomUUID } from 'crypto'; +import { IdDTO } from '../../src/dtos/shared/id.dto'; +import { ListingPublishedUpdate } from '../../src/dtos/listings/listing-published-update.dto'; +import { + ApplicationAddressTypeEnum, + ApplicationMethodsTypeEnum, + LanguagesEnum, + ListingEventsTypeEnum, + ListingsStatusEnum, + ReviewOrderTypeEnum, + UnitTypeEnum, +} from '@prisma/client'; +import { + unitTypeFactoryAll, + unitTypeFactorySingle, +} from '../../prisma/seed-helpers/unit-type-factory'; +import { amiChartFactory } from '../../prisma/seed-helpers/ami-chart-factory'; +import { unitAccessibilityPriorityTypeFactorySingle } from '../../prisma/seed-helpers/unit-accessibility-priority-type-factory'; +import { unitRentTypeFactory } from '../../prisma/seed-helpers/unit-rent-type-factory'; +import { multiselectQuestionFactory } from '../../prisma/seed-helpers/multiselect-question-factory'; +import { reservedCommunityTypeFactory } from '../../prisma/seed-helpers/reserved-community-type-factory'; +import { ListingPublishedCreate } from '../../src/dtos/listings/listing-published-create.dto'; +import { addressFactory } from '../../prisma/seed-helpers/address-factory'; +import { AddressCreate } from '../../src/dtos/addresses/address-create.dto'; describe('Listing Controller Tests', () => { let app: INestApplication; @@ -38,6 +62,253 @@ describe('Listing Controller Tests', () => { await app.close(); }); + const constructFullListingData = async ( + listingId?: string, + jurisdictionId?: string, + ): Promise => { + let jurisdictionA: IdDTO = { id: '' }; + + if (jurisdictionId) { + jurisdictionA.id = jurisdictionId; + } else { + jurisdictionA = await prisma.jurisdictions.create({ + data: jurisdictionFactory(), + }); + } + + await unitTypeFactoryAll(prisma); + const unitType = await unitTypeFactorySingle(prisma, UnitTypeEnum.SRO); + const amiChart = await prisma.amiChart.create({ + data: amiChartFactory(1, jurisdictionA.id), + }); + const unitAccessibilityPriorityType = + await prisma.unitAccessibilityPriorityTypes.create({ + data: unitAccessibilityPriorityTypeFactorySingle(), + }); + const rentType = await prisma.unitRentTypes.create({ + data: unitRentTypeFactory(), + }); + const multiselectQuestion = await prisma.multiselectQuestions.create({ + data: multiselectQuestionFactory(jurisdictionA.id), + }); + const reservedCommunityType = await prisma.reservedCommunityTypes.create({ + data: reservedCommunityTypeFactory(jurisdictionA.id), + }); + + const exampleAddress = addressFactory() as AddressCreate; + + const exampleAsset = { + fileId: randomUUID(), + label: 'example asset label', + }; + + return { + id: listingId ?? undefined, + assets: [exampleAsset], + listingsBuildingAddress: exampleAddress, + depositMin: '1000', + depositMax: '5000', + developer: 'example developer', + digitalApplication: true, + listingImages: [ + { + ordinal: 0, + assets: exampleAsset, + }, + ], + leasingAgentEmail: 'leasingAgent@exygy.com', + leasingAgentName: 'Leasing Agent', + leasingAgentPhone: '520-750-8811', + name: 'example listing', + paperApplication: false, + referralOpportunity: false, + rentalAssistance: 'rental assistance', + reviewOrderType: ReviewOrderTypeEnum.firstComeFirstServe, + units: [ + { + amiPercentage: '1', + annualIncomeMin: '2', + monthlyIncomeMin: '3', + floor: 4, + annualIncomeMax: '5', + maxOccupancy: 6, + minOccupancy: 7, + monthlyRent: '8', + numBathrooms: 9, + numBedrooms: 10, + number: '11', + sqFeet: '12', + monthlyRentAsPercentOfIncome: '13', + bmrProgramChart: true, + unitTypes: { + id: unitType.id, + }, + amiChart: { + id: amiChart.id, + }, + unitAccessibilityPriorityTypes: { + id: unitAccessibilityPriorityType.id, + }, + unitRentTypes: { + id: rentType.id, + }, + unitAmiChartOverrides: { + items: [ + { + percentOfAmi: 10, + householdSize: 20, + income: 30, + }, + ], + }, + }, + ], + listingMultiselectQuestions: [ + { + id: multiselectQuestion.id, + ordinal: 0, + }, + ], + applicationMethods: [ + { + type: ApplicationMethodsTypeEnum.Internal, + label: 'example label', + externalReference: 'example reference', + acceptsPostmarkedApplications: false, + phoneNumber: '520-750-8811', + paperApplications: [ + { + language: LanguagesEnum.en, + assets: exampleAsset, + }, + ], + }, + ], + unitsSummary: [ + { + unitTypes: { + id: unitType.id, + }, + monthlyRentMin: 1, + monthlyRentMax: 2, + monthlyRentAsPercentOfIncome: '3', + amiPercentage: 4, + minimumIncomeMin: '5', + minimumIncomeMax: '6', + maxOccupancy: 7, + minOccupancy: 8, + floorMin: 9, + floorMax: 10, + sqFeetMin: '11', + sqFeetMax: '12', + unitAccessibilityPriorityTypes: { + id: unitAccessibilityPriorityType.id, + }, + totalCount: 13, + totalAvailable: 14, + }, + ], + listingsApplicationPickUpAddress: exampleAddress, + listingsApplicationMailingAddress: exampleAddress, + listingsApplicationDropOffAddress: exampleAddress, + listingsLeasingAgentAddress: exampleAddress, + listingsBuildingSelectionCriteriaFile: exampleAsset, + listingsResult: exampleAsset, + listingEvents: [ + { + type: ListingEventsTypeEnum.openHouse, + startDate: new Date(), + startTime: new Date(), + endTime: new Date(), + url: 'https://www.google.com', + note: 'example note', + label: 'example label', + assets: exampleAsset, + }, + ], + additionalApplicationSubmissionNotes: 'app submission notes', + commonDigitalApplication: true, + accessibility: 'accessibility string', + amenities: 'amenities string', + buildingTotalUnits: 5, + householdSizeMax: 9, + householdSizeMin: 1, + neighborhood: 'neighborhood string', + petPolicy: 'we love pets', + smokingPolicy: 'smokeing policy string', + unitsAvailable: 15, + unitAmenities: 'unit amenity string', + servicesOffered: 'services offered string', + yearBuilt: 2023, + applicationDueDate: new Date(), + applicationOpenDate: new Date(), + applicationFee: 'application fee string', + applicationOrganization: 'app organization string', + applicationPickUpAddressOfficeHours: 'pick up office hours string', + applicationPickUpAddressType: ApplicationAddressTypeEnum.leasingAgent, + applicationDropOffAddressOfficeHours: 'drop off office hours string', + applicationDropOffAddressType: ApplicationAddressTypeEnum.leasingAgent, + applicationMailingAddressType: ApplicationAddressTypeEnum.leasingAgent, + buildingSelectionCriteria: 'selection criteria', + costsNotIncluded: 'all costs included', + creditHistory: 'credit history', + criminalBackground: 'criminal background', + depositHelperText: 'deposit helper text', + disableUnitsAccordion: false, + leasingAgentOfficeHours: 'leasing agent office hours', + leasingAgentTitle: 'leasing agent title', + postmarkedApplicationsReceivedByDate: new Date(), + programRules: 'program rules', + rentalHistory: 'rental history', + requiredDocuments: 'required docs', + specialNotes: 'special notes', + waitlistCurrentSize: 0, + waitlistMaxSize: 100, + whatToExpect: 'what to expect', + status: ListingsStatusEnum.active, + displayWaitlistSize: true, + reservedCommunityDescription: 'reserved community description', + reservedCommunityMinAge: 66, + resultLink: 'result link', + isWaitlistOpen: true, + waitlistOpenSpots: 100, + customMapPin: false, + jurisdictions: { + id: jurisdictionA.id, + }, + reservedCommunityTypes: { + id: reservedCommunityType.id, + }, + listingFeatures: { + elevator: true, + wheelchairRamp: false, + serviceAnimalsAllowed: true, + accessibleParking: false, + parkingOnSite: true, + inUnitWasherDryer: false, + laundryInBuilding: true, + barrierFreeEntrance: false, + rollInShower: true, + grabBars: false, + heatingInUnit: true, + acInUnit: false, + hearing: true, + visual: false, + mobility: true, + }, + listingUtilities: { + water: false, + gas: true, + trash: false, + sewer: true, + electricity: false, + cable: true, + phone: false, + internet: true, + }, + }; + }; + it('should not get listings from list endpoint when no params are sent', async () => { const res = await request(app.getHttpServer()).get('/listings').expect(200); @@ -216,4 +487,82 @@ describe('Listing Controller Tests', () => { expect(res.body.length).toEqual(1); expect(res.body[0].name).toEqual(listingA.name); }); + + it("should error when trying to delete listing that doesn't exist", async () => { + const id = randomUUID(); + const res = await request(app.getHttpServer()) + .delete(`/listings`) + .send({ + id: id, + } as IdDTO) + .expect(404); + expect(res.body.message).toEqual( + `listingId ${id} was requested but not found`, + ); + }); + + it('should delete listing', async () => { + const jurisdictionA = await prisma.jurisdictions.create({ + data: jurisdictionFactory(), + }); + const listingData = await listingFactory(jurisdictionA.id, prisma); + const listing = await prisma.listings.create({ + data: listingData, + }); + + const res = await request(app.getHttpServer()) + .delete(`/listings/`) + .send({ + id: listing.id, + } as IdDTO) + .expect(200); + + const listingAfterDelete = await prisma.listings.findUnique({ + where: { id: listing.id }, + }); + expect(listingAfterDelete).toBeNull(); + expect(res.body.success).toEqual(true); + }); + + it("should error when trying to update listing that doesn't exist", async () => { + const id = randomUUID(); + const res = await request(app.getHttpServer()) + .put(`/listings/${id}`) + .send({ + id: id, + } as IdDTO) + .expect(404); + expect(res.body.message).toEqual( + `listingId ${id} was requested but not found`, + ); + }); + + it('should update listing', async () => { + const jurisdictionA = await prisma.jurisdictions.create({ + data: jurisdictionFactory(), + }); + const listingData = await listingFactory(jurisdictionA.id, prisma); + const listing = await prisma.listings.create({ + data: listingData, + }); + + const val = await constructFullListingData(listing.id, jurisdictionA.id); + + const res = await request(app.getHttpServer()) + .put(`/listings/${listing.id}`) + .send(val) + .expect(200); + expect(res.body.id).toEqual(listing.id); + expect(res.body.name).toEqual(val.name); + }); + + it('should create listing', async () => { + const val = await constructFullListingData(); + + const res = await request(app.getHttpServer()) + .post('/listings') + .send(val) + .expect(201); + expect(res.body.name).toEqual(val.name); + }); }); diff --git a/backend_new/test/jest-e2e.config.js b/backend_new/test/jest-e2e.config.js index 81905ffe71..8cb6fc82b0 100644 --- a/backend_new/test/jest-e2e.config.js +++ b/backend_new/test/jest-e2e.config.js @@ -1,6 +1,6 @@ module.exports = { moduleFileExtensions: ['js', 'json', 'ts'], - rootDir: '.', + rootDir: '../', testEnvironment: 'node', testRegex: '.e2e-spec.ts$', transform: { diff --git a/backend_new/test/jest-with-coverage.config.js b/backend_new/test/jest-with-coverage.config.js new file mode 100644 index 0000000000..1e4fcddb7a --- /dev/null +++ b/backend_new/test/jest-with-coverage.config.js @@ -0,0 +1,28 @@ +module.exports = { + moduleFileExtensions: ['js', 'json', 'ts'], + rootDir: '../', + testEnvironment: 'node', + testRegex: 'spec.ts$', + transform: { + '^.+\\.(t|j)s$': [ + 'ts-jest', + { + diagnostics: false, + }, + ], + }, + collectCoverage: true, + collectCoverageFrom: [ + './src/services/**', + './src/utilities/**', + './src/controllers/**', + './src/modules/**', + ], + coverageThreshold: { + global: { + branches: 75, + functions: 90, + lines: 90, + }, + }, +}; diff --git a/backend_new/test/jest.config.js b/backend_new/test/jest.config.js index 41518a1ed3..c00ca9adc0 100644 --- a/backend_new/test/jest.config.js +++ b/backend_new/test/jest.config.js @@ -1,6 +1,6 @@ module.exports = { moduleFileExtensions: ['js', 'json', 'ts'], - rootDir: '.', + rootDir: '../', testEnvironment: 'node', testRegex: '\\.spec.ts$', transform: { diff --git a/backend_new/test/unit/services/listing.service.spec.ts b/backend_new/test/unit/services/listing.service.spec.ts index 9308f81d95..3c9bbb76da 100644 --- a/backend_new/test/unit/services/listing.service.spec.ts +++ b/backend_new/test/unit/services/listing.service.spec.ts @@ -7,13 +7,28 @@ import { OrderByEnum } from '../../../src/enums/shared/order-by-enum'; import { ListingFilterKeys } from '../../../src/enums/listings/filter-key-enum'; import { Compare } from '../../../src/dtos/shared/base-filter.dto'; import { ListingFilterParams } from '../../../src/dtos/listings/listings-filter-params.dto'; -import { LanguagesEnum, UnitTypeEnum } from '@prisma/client'; -import { Unit } from '../../../src/dtos/units/unit-get.dto'; +import { + ApplicationAddressTypeEnum, + ApplicationMethodsTypeEnum, + LanguagesEnum, + ListingEventsTypeEnum, + ListingsStatusEnum, + ReviewOrderTypeEnum, + UnitTypeEnum, +} from '@prisma/client'; +import { Unit } from '../../../src/dtos/units/unit.dto'; import { UnitTypeSort } from '../../../src/utilities/unit-utilities'; -import { ListingGet } from '../../../src/dtos/listings/listing-get.dto'; +import { Listing } from '../../../src/dtos/listings/listing.dto'; import { ListingViews } from '../../../src/enums/listings/view-enum'; import { TranslationService } from '../../../src/services/translation.service'; import { GoogleTranslateService } from '../../../src/services/google-translate.service'; +import { ListingCreate } from '../../../src/dtos/listings/listing-create.dto'; +import { randomUUID } from 'crypto'; +import { HttpModule, HttpService } from '@nestjs/axios'; +import { of } from 'rxjs'; +import { ListingUpdate } from '../../../src/dtos/listings/listing-update.dto'; +import { ListingPublishedCreate } from '../../../src/dtos/listings/listing-published-create.dto'; +import { ListingPublishedUpdate } from 'src/dtos/listings/listing-published-update.dto'; /* generates a super simple mock listing for us to test logic with @@ -101,7 +116,17 @@ describe('Testing listing service', () => { isConfigured: () => true, fetch: jest.fn(), }; - beforeEach(async () => { + + const httpServiceMock = { + request: jest.fn().mockReturnValue( + of({ + status: 200, + statusText: 'OK', + }), + ), + }; + + beforeAll(async () => { const module: TestingModule = await Test.createTestingModule({ providers: [ ListingService, @@ -111,41 +136,252 @@ describe('Testing listing service', () => { provide: GoogleTranslateService, useValue: googleTranslateServiceMock, }, + { + provide: HttpService, + useValue: httpServiceMock, + }, ], + imports: [HttpModule], }).compile(); service = module.get(ListingService); prisma = module.get(PrismaService); }); - it('testing list() with no params', async () => { + afterAll(() => { + process.env.PROXY_URL = undefined; + }); + + const exampleAddress = { + city: 'Exygy', + state: 'CA', + zipCode: '94104', + street: '548 Market St', + }; + + const exampleAsset = { + fileId: randomUUID(), + label: 'example asset label', + }; + + const constructFullListingData = ( + listingId?: string, + ): ListingPublishedCreate | ListingPublishedUpdate => { + return { + id: listingId ?? undefined, + assets: [exampleAsset], + listingsBuildingAddress: exampleAddress, + depositMin: '1000', + depositMax: '5000', + developer: 'example developer', + digitalApplication: true, + listingImages: [ + { + ordinal: 0, + assets: exampleAsset, + }, + ], + leasingAgentEmail: 'leasingAgent@exygy.com', + leasingAgentName: 'Leasing Agent', + leasingAgentPhone: '520-750-8811', + name: 'example listing', + paperApplication: false, + referralOpportunity: false, + rentalAssistance: 'rental assistance', + reviewOrderType: ReviewOrderTypeEnum.firstComeFirstServe, + units: [ + { + amiPercentage: '1', + annualIncomeMin: '2', + monthlyIncomeMin: '3', + floor: 4, + annualIncomeMax: '5', + maxOccupancy: 6, + minOccupancy: 7, + monthlyRent: '8', + numBathrooms: 9, + numBedrooms: 10, + number: '11', + sqFeet: '12', + monthlyRentAsPercentOfIncome: '13', + bmrProgramChart: true, + unitTypes: { + id: randomUUID(), + }, + amiChart: { + id: randomUUID(), + }, + unitAccessibilityPriorityTypes: { + id: randomUUID(), + }, + unitRentTypes: { + id: randomUUID(), + }, + unitAmiChartOverrides: { + items: [ + { + percentOfAmi: 10, + householdSize: 20, + income: 30, + }, + ], + }, + }, + ], + listingMultiselectQuestions: [ + { + id: randomUUID(), + ordinal: 0, + }, + ], + applicationMethods: [ + { + type: ApplicationMethodsTypeEnum.Internal, + label: 'example label', + externalReference: 'example reference', + acceptsPostmarkedApplications: false, + phoneNumber: '520-750-8811', + paperApplications: [ + { + language: LanguagesEnum.en, + assets: exampleAsset, + }, + ], + }, + ], + unitsSummary: [ + { + unitTypes: { + id: randomUUID(), + }, + monthlyRentMin: 1, + monthlyRentMax: 2, + monthlyRentAsPercentOfIncome: '3', + amiPercentage: 4, + minimumIncomeMin: '5', + minimumIncomeMax: '6', + maxOccupancy: 7, + minOccupancy: 8, + floorMin: 9, + floorMax: 10, + sqFeetMin: '11', + sqFeetMax: '12', + unitAccessibilityPriorityTypes: { + id: randomUUID(), + }, + totalCount: 13, + totalAvailable: 14, + }, + ], + listingsApplicationPickUpAddress: exampleAddress, + listingsApplicationMailingAddress: exampleAddress, + listingsApplicationDropOffAddress: exampleAddress, + listingsLeasingAgentAddress: exampleAddress, + listingsBuildingSelectionCriteriaFile: exampleAsset, + listingsResult: exampleAsset, + listingEvents: [ + { + type: ListingEventsTypeEnum.openHouse, + startDate: new Date(), + startTime: new Date(), + endTime: new Date(), + url: 'https://www.google.com', + note: 'example note', + label: 'example label', + assets: exampleAsset, + }, + ], + additionalApplicationSubmissionNotes: 'app submission notes', + commonDigitalApplication: true, + accessibility: 'accessibility string', + amenities: 'amenities string', + buildingTotalUnits: 5, + householdSizeMax: 9, + householdSizeMin: 1, + neighborhood: 'neighborhood string', + petPolicy: 'we love pets', + smokingPolicy: 'smokeing policy string', + unitsAvailable: 15, + unitAmenities: 'unit amenity string', + servicesOffered: 'services offered string', + yearBuilt: 2023, + applicationDueDate: new Date(), + applicationOpenDate: new Date(), + applicationFee: 'application fee string', + applicationOrganization: 'app organization string', + applicationPickUpAddressOfficeHours: 'pick up office hours string', + applicationPickUpAddressType: ApplicationAddressTypeEnum.leasingAgent, + applicationDropOffAddressOfficeHours: 'drop off office hours string', + applicationDropOffAddressType: ApplicationAddressTypeEnum.leasingAgent, + applicationMailingAddressType: ApplicationAddressTypeEnum.leasingAgent, + buildingSelectionCriteria: 'selection criteria', + costsNotIncluded: 'all costs included', + creditHistory: 'credit history', + criminalBackground: 'criminal background', + depositHelperText: 'deposit helper text', + disableUnitsAccordion: false, + leasingAgentOfficeHours: 'leasing agent office hours', + leasingAgentTitle: 'leasing agent title', + postmarkedApplicationsReceivedByDate: new Date(), + programRules: 'program rules', + rentalHistory: 'rental history', + requiredDocuments: 'required docs', + specialNotes: 'special notes', + waitlistCurrentSize: 0, + waitlistMaxSize: 100, + whatToExpect: 'what to expect', + status: ListingsStatusEnum.active, + displayWaitlistSize: true, + reservedCommunityDescription: 'reserved community description', + reservedCommunityMinAge: 66, + resultLink: 'result link', + isWaitlistOpen: true, + waitlistOpenSpots: 100, + customMapPin: false, + jurisdictions: { + id: randomUUID(), + }, + reservedCommunityTypes: { + id: randomUUID(), + }, + listingFeatures: { + elevator: true, + wheelchairRamp: false, + serviceAnimalsAllowed: true, + accessibleParking: false, + parkingOnSite: true, + inUnitWasherDryer: false, + laundryInBuilding: true, + barrierFreeEntrance: false, + rollInShower: true, + grabBars: false, + heatingInUnit: true, + acInUnit: false, + hearing: true, + visual: false, + mobility: true, + }, + listingUtilities: { + water: false, + gas: true, + trash: false, + sewer: true, + electricity: false, + cable: true, + phone: false, + internet: true, + }, + }; + }; + + it('should handle call to list() with no params sent', async () => { prisma.listings.findMany = jest.fn().mockResolvedValue(mockListingSet(10)); prisma.listings.count = jest.fn().mockResolvedValue(10); const params: ListingsQueryParams = {}; - expect(await service.list(params)).toEqual({ - items: [ - { id: '0', name: 'listing 1' }, - { id: '1', name: 'listing 2' }, - { id: '2', name: 'listing 3' }, - { id: '3', name: 'listing 4' }, - { id: '4', name: 'listing 5' }, - { id: '5', name: 'listing 6' }, - { id: '6', name: 'listing 7' }, - { id: '7', name: 'listing 8' }, - { id: '8', name: 'listing 9' }, - { id: '9', name: 'listing 10' }, - ], - meta: { - currentPage: 1, - itemCount: 10, - itemsPerPage: 10, - totalItems: 10, - totalPages: 1, - }, - }); + await service.list(params); expect(prisma.listings.findMany).toHaveBeenCalledWith({ skip: 0, @@ -214,7 +450,7 @@ describe('Testing listing service', () => { }); }); - it('testing list() with params', async () => { + it('should handle call to list() with params sent', async () => { prisma.listings.findMany = jest.fn().mockResolvedValue(mockListingSet(10)); prisma.listings.count = jest.fn().mockResolvedValue(20); @@ -238,27 +474,7 @@ describe('Testing listing service', () => { ], }; - expect(await service.list(params)).toEqual({ - items: [ - { id: '0', name: 'listing 1' }, - { id: '1', name: 'listing 2' }, - { id: '2', name: 'listing 3' }, - { id: '3', name: 'listing 4' }, - { id: '4', name: 'listing 5' }, - { id: '5', name: 'listing 6' }, - { id: '6', name: 'listing 7' }, - { id: '7', name: 'listing 8' }, - { id: '8', name: 'listing 9' }, - { id: '9', name: 'listing 10' }, - ], - meta: { - currentPage: 2, - itemCount: 10, - itemsPerPage: 10, - totalItems: 20, - totalPages: 2, - }, - }); + await service.list(params); expect(prisma.listings.findMany).toHaveBeenCalledWith({ skip: 10, @@ -370,7 +586,7 @@ describe('Testing listing service', () => { }); }); - it('testing buildWhereClause() with params no search', async () => { + it('should build where clause when only params sent', async () => { const params: ListingFilterParams[] = [ { [ListingFilterKeys.name]: 'Listing,name', @@ -412,7 +628,7 @@ describe('Testing listing service', () => { }); }); - it('testing buildWhereClause() with no params, search present', async () => { + it('should build where clause when only search param sent', async () => { expect(service.buildWhereClause(null, 'simple search')).toEqual({ AND: [ { @@ -425,7 +641,7 @@ describe('Testing listing service', () => { }); }); - it('testing buildWhereClause() with params, and search present', async () => { + it('should build where clause when params, and search param sent', async () => { const params: ListingFilterParams[] = [ { [ListingFilterKeys.name]: 'Listing,name', @@ -473,18 +689,14 @@ describe('Testing listing service', () => { }); }); - it('testing findOne() base view found record', async () => { - prisma.listings.findFirst = jest.fn().mockResolvedValue(mockListing(0)); + it('should return records from findOne() with base view', async () => { + prisma.listings.findUnique = jest.fn().mockResolvedValue(mockListing(0)); - expect( - await service.findOne('listingId', LanguagesEnum.en, ListingViews.base), - ).toEqual({ id: '0', name: 'listing 1' }); + await service.findOne('listingId', LanguagesEnum.en, ListingViews.base); - expect(prisma.listings.findFirst).toHaveBeenCalledWith({ + expect(prisma.listings.findUnique).toHaveBeenCalledWith({ where: { - id: { - equals: 'listingId', - }, + id: 'listingId', }, include: { jurisdictions: true, @@ -517,8 +729,8 @@ describe('Testing listing service', () => { }); }); - it('testing findOne() base view no record found', async () => { - prisma.listings.findFirst = jest.fn().mockResolvedValue(null); + it('should handle no records returned when findOne() is called with base view', async () => { + prisma.listings.findUnique = jest.fn().mockResolvedValue(null); await expect( async () => @@ -529,11 +741,9 @@ describe('Testing listing service', () => { ), ).rejects.toThrowError(); - expect(prisma.listings.findFirst).toHaveBeenCalledWith({ + expect(prisma.listings.findUnique).toHaveBeenCalledWith({ where: { - id: { - equals: 'a different listingId', - }, + id: 'a different listingId', }, include: { jurisdictions: true, @@ -589,7 +799,7 @@ describe('Testing listing service', () => { }); }); - it('testing list() with params and units', async () => { + it('should get records from list() with params and units', async () => { const date = new Date(); prisma.listings.findMany = jest @@ -916,12 +1126,12 @@ describe('Testing listing service', () => { }); }); - it('testing findOne() base view found record and units', async () => { + it('should get records from findOne() with base view found and units', async () => { const date = new Date(); const mockedListing = mockListing(0, { numberToMake: 10, date }); - prisma.listings.findFirst = jest.fn().mockResolvedValue(mockedListing); + prisma.listings.findUnique = jest.fn().mockResolvedValue(mockedListing); prisma.amiChart.findMany = jest.fn().mockResolvedValue([ { @@ -936,7 +1146,7 @@ describe('Testing listing service', () => { }, ]); - const listing: ListingGet = await service.findOne( + const listing: Listing = await service.findOne( 'listingId', LanguagesEnum.en, ListingViews.base, @@ -1944,11 +2154,9 @@ describe('Testing listing service', () => { }, }); - expect(prisma.listings.findFirst).toHaveBeenCalledWith({ + expect(prisma.listings.findUnique).toHaveBeenCalledWith({ where: { - id: { - equals: 'listingId', - }, + id: 'listingId', }, include: { jurisdictions: true, @@ -1989,7 +2197,7 @@ describe('Testing listing service', () => { }); }); - it('testing findListingsWithMultiSelectQuestion()', async () => { + it('should return listings from findListingsWithMultiSelectQuestion()', async () => { prisma.listings.findMany = jest.fn().mockResolvedValue([ { id: 'example id', @@ -2019,4 +2227,937 @@ describe('Testing listing service', () => { }, }); }); + + it('should create a simple listing', async () => { + prisma.listings.create = jest.fn().mockResolvedValue({ + id: 'example id', + name: 'example name', + }); + + await service.create({ + name: 'example listing name', + depositMin: '5', + assets: [ + { + fileId: randomUUID(), + label: 'example asset', + }, + ], + jurisdictions: { + id: randomUUID(), + }, + status: ListingsStatusEnum.pending, + displayWaitlistSize: false, + unitsSummary: null, + listingEvents: [], + } as ListingCreate); + + expect(prisma.listings.create).toHaveBeenCalledWith({ + include: { + applicationMethods: { + include: { + paperApplications: { + include: { + assets: true, + }, + }, + }, + }, + jurisdictions: true, + listingEvents: { + include: { + assets: true, + }, + }, + listingFeatures: true, + listingImages: { + include: { + assets: true, + }, + }, + listingMultiselectQuestions: { + include: { + multiselectQuestions: true, + }, + }, + listingUtilities: true, + listingsApplicationDropOffAddress: true, + listingsApplicationPickUpAddress: true, + listingsBuildingAddress: true, + listingsBuildingSelectionCriteriaFile: true, + listingsLeasingAgentAddress: true, + listingsResult: true, + reservedCommunityTypes: true, + units: { + include: { + amiChart: { + include: { + amiChartItem: true, + jurisdictions: true, + unitGroupAmiLevels: true, + }, + }, + unitAccessibilityPriorityTypes: true, + unitAmiChartOverrides: true, + unitRentTypes: true, + unitTypes: true, + }, + }, + }, + data: { + name: 'example listing name', + depositMin: '5', + assets: { + create: [ + { + fileId: expect.anything(), + label: 'example asset', + }, + ], + }, + jurisdictions: { + connect: { + id: expect.anything(), + }, + }, + status: ListingsStatusEnum.pending, + displayWaitlistSize: false, + unitsSummary: undefined, + listingEvents: { + create: [], + }, + }, + }); + }); + + it('should create a complete listing', async () => { + prisma.listings.create = jest.fn().mockResolvedValue({ + id: 'example id', + name: 'example name', + }); + + const val = constructFullListingData(); + + await service.create(val as ListingCreate); + + expect(prisma.listings.create).toHaveBeenCalledWith({ + include: { + applicationMethods: { + include: { + paperApplications: { + include: { + assets: true, + }, + }, + }, + }, + jurisdictions: true, + listingEvents: { + include: { + assets: true, + }, + }, + listingFeatures: true, + listingImages: { + include: { + assets: true, + }, + }, + listingMultiselectQuestions: { + include: { + multiselectQuestions: true, + }, + }, + listingUtilities: true, + listingsApplicationDropOffAddress: true, + listingsApplicationPickUpAddress: true, + listingsBuildingAddress: true, + listingsBuildingSelectionCriteriaFile: true, + listingsLeasingAgentAddress: true, + listingsResult: true, + reservedCommunityTypes: true, + units: { + include: { + amiChart: { + include: { + amiChartItem: true, + jurisdictions: true, + unitGroupAmiLevels: true, + }, + }, + unitAccessibilityPriorityTypes: true, + unitAmiChartOverrides: true, + unitRentTypes: true, + unitTypes: true, + }, + }, + }, + data: { + ...val, + assets: { + create: [exampleAsset], + }, + applicationMethods: { + create: [ + { + type: ApplicationMethodsTypeEnum.Internal, + label: 'example label', + externalReference: 'example reference', + acceptsPostmarkedApplications: false, + phoneNumber: '520-750-8811', + paperApplications: { + create: [ + { + language: LanguagesEnum.en, + assets: { + create: { + ...exampleAsset, + }, + }, + }, + ], + }, + }, + ], + }, + listingEvents: { + create: [ + { + type: ListingEventsTypeEnum.openHouse, + startDate: expect.anything(), + startTime: expect.anything(), + endTime: expect.anything(), + url: 'https://www.google.com', + note: 'example note', + label: 'example label', + assets: { + create: { + ...exampleAsset, + }, + }, + }, + ], + }, + listingImages: { + create: [ + { + assets: { + create: { + ...exampleAsset, + }, + }, + ordinal: 0, + }, + ], + }, + listingMultiselectQuestions: { + create: [ + { + ordinal: 0, + multiselectQuestions: { + connect: { + id: expect.anything(), + }, + }, + }, + ], + }, + listingsApplicationDropOffAddress: { + create: { + ...exampleAddress, + }, + }, + reservedCommunityTypes: { + connect: { + id: expect.anything(), + }, + }, + listingsBuildingSelectionCriteriaFile: { + create: { + ...exampleAsset, + }, + }, + listingUtilities: { + create: { + water: false, + gas: true, + trash: false, + sewer: true, + electricity: false, + cable: true, + phone: false, + internet: true, + }, + }, + listingsApplicationMailingAddress: { + create: { + ...exampleAddress, + }, + }, + listingsLeasingAgentAddress: { + create: { + ...exampleAddress, + }, + }, + listingFeatures: { + create: { + elevator: true, + wheelchairRamp: false, + serviceAnimalsAllowed: true, + accessibleParking: false, + parkingOnSite: true, + inUnitWasherDryer: false, + laundryInBuilding: true, + barrierFreeEntrance: false, + rollInShower: true, + grabBars: false, + heatingInUnit: true, + acInUnit: false, + hearing: true, + visual: false, + mobility: true, + }, + }, + jurisdictions: { + connect: { + id: expect.anything(), + }, + }, + listingsApplicationPickUpAddress: { + create: { + ...exampleAddress, + }, + }, + listingsBuildingAddress: { + create: { + ...exampleAddress, + }, + }, + units: { + create: [ + { + amiPercentage: '1', + annualIncomeMin: '2', + monthlyIncomeMin: '3', + floor: 4, + annualIncomeMax: '5', + maxOccupancy: 6, + minOccupancy: 7, + monthlyRent: '8', + numBathrooms: 9, + numBedrooms: 10, + number: '11', + sqFeet: '12', + monthlyRentAsPercentOfIncome: '13', + bmrProgramChart: true, + unitTypes: { + connect: { + id: expect.anything(), + }, + }, + amiChart: { + connect: { + id: expect.anything(), + }, + }, + unitAmiChartOverrides: { + create: { + items: { + items: [ + { + percentOfAmi: 10, + householdSize: 20, + income: 30, + }, + ], + }, + }, + }, + unitAccessibilityPriorityTypes: { + connect: { + id: expect.anything(), + }, + }, + unitRentTypes: { + connect: { + id: expect.anything(), + }, + }, + }, + ], + }, + unitsSummary: { + create: [ + { + monthlyRentMin: 1, + monthlyRentMax: 2, + monthlyRentAsPercentOfIncome: '3', + amiPercentage: 4, + minimumIncomeMin: '5', + minimumIncomeMax: '6', + maxOccupancy: 7, + minOccupancy: 8, + floorMin: 9, + floorMax: 10, + sqFeetMin: '11', + sqFeetMax: '12', + totalCount: 13, + totalAvailable: 14, + unitTypes: { + connect: { + id: expect.anything(), + }, + }, + unitAccessibilityPriorityTypes: { + connect: { + id: expect.anything(), + }, + }, + }, + ], + }, + listingsResult: { + create: { + ...exampleAsset, + }, + }, + }, + }); + }); + + it('should delete a listing', async () => { + const id = randomUUID(); + prisma.listings.findUnique = jest.fn().mockResolvedValue({ + id, + }); + prisma.listings.delete = jest.fn().mockResolvedValue({ + id, + }); + + await service.delete(id); + + expect(prisma.listings.findUnique).toHaveBeenCalledWith({ + where: { + id: id, + }, + }); + + expect(prisma.listings.delete).toHaveBeenCalledWith({ + where: { + id: id, + }, + }); + }); + + it('should error when nonexistent id is passed to delete()', async () => { + prisma.listings.findUnique = jest.fn().mockResolvedValue(null); + prisma.listings.delete = jest.fn().mockResolvedValue(null); + + await expect( + async () => await service.delete(randomUUID()), + ).rejects.toThrowError(); + + expect(prisma.listings.findUnique).toHaveBeenCalledWith({ + where: { + id: expect.anything(), + }, + }); + + expect(prisma.listings.delete).not.toHaveBeenCalled(); + }); + + it('should return listing from call to findOrThrow()', async () => { + const id = randomUUID(); + prisma.listings.findUnique = jest.fn().mockResolvedValue({ + id, + }); + + await service.findOrThrow(id, ListingViews.fundamentals); + + expect(prisma.listings.findUnique).toHaveBeenCalledWith({ + include: { + jurisdictions: true, + listingFeatures: true, + listingImages: { + include: { + assets: true, + }, + }, + listingMultiselectQuestions: { + include: { + multiselectQuestions: true, + }, + }, + listingUtilities: true, + listingsBuildingAddress: true, + reservedCommunityTypes: true, + }, + where: { + id: id, + }, + }); + }); + + it('should error when nonexistent id is passed to findOrThrow()', async () => { + prisma.listings.findUnique = jest.fn().mockResolvedValue(null); + + await expect( + async () => await service.findOrThrow(randomUUID()), + ).rejects.toThrowError(); + + expect(prisma.listings.findUnique).toHaveBeenCalledWith({ + where: { + id: expect.anything(), + }, + }); + }); + + it('should update a simple listing', async () => { + prisma.listings.findUnique = jest.fn().mockResolvedValue({ + id: 'example id', + name: 'example name', + }); + prisma.listings.update = jest.fn().mockResolvedValue({ + id: 'example id', + name: 'example name', + }); + + await service.update({ + id: randomUUID(), + name: 'example listing name', + depositMin: '5', + assets: [ + { + fileId: randomUUID(), + label: 'example asset', + }, + ], + jurisdictions: { + id: randomUUID(), + }, + status: ListingsStatusEnum.pending, + displayWaitlistSize: false, + unitsSummary: null, + listingEvents: [], + } as ListingUpdate); + + expect(prisma.listings.update).toHaveBeenCalledWith({ + include: { + applicationMethods: { + include: { + paperApplications: { + include: { + assets: true, + }, + }, + }, + }, + jurisdictions: true, + listingEvents: { + include: { + assets: true, + }, + }, + listingFeatures: true, + listingImages: { + include: { + assets: true, + }, + }, + listingMultiselectQuestions: { + include: { + multiselectQuestions: true, + }, + }, + listingUtilities: true, + listingsApplicationDropOffAddress: true, + listingsApplicationPickUpAddress: true, + listingsBuildingAddress: true, + listingsBuildingSelectionCriteriaFile: true, + listingsLeasingAgentAddress: true, + listingsResult: true, + reservedCommunityTypes: true, + units: { + include: { + amiChart: { + include: { + amiChartItem: true, + jurisdictions: true, + unitGroupAmiLevels: true, + }, + }, + unitAccessibilityPriorityTypes: true, + unitAmiChartOverrides: true, + unitRentTypes: true, + unitTypes: true, + }, + }, + }, + data: { + name: 'example listing name', + depositMin: '5', + assets: { + create: [ + { + fileId: expect.anything(), + label: 'example asset', + }, + ], + }, + jurisdictions: { + connect: { + id: expect.anything(), + }, + }, + status: ListingsStatusEnum.pending, + displayWaitlistSize: false, + unitsSummary: undefined, + listingEvents: { + create: [], + }, + unitsAvailable: 0, + }, + where: { + id: expect.anything(), + }, + }); + }); + + it('should do a complete listing update', async () => { + prisma.listings.findUnique = jest.fn().mockResolvedValue({ + id: 'example id', + name: 'example name', + }); + prisma.listings.update = jest.fn().mockResolvedValue({ + id: 'example id', + name: 'example name', + }); + + const val = constructFullListingData(randomUUID()); + + await service.update(val as ListingUpdate); + + expect(prisma.listings.update).toHaveBeenCalledWith({ + include: { + applicationMethods: { + include: { + paperApplications: { + include: { + assets: true, + }, + }, + }, + }, + jurisdictions: true, + listingEvents: { + include: { + assets: true, + }, + }, + listingFeatures: true, + listingImages: { + include: { + assets: true, + }, + }, + listingMultiselectQuestions: { + include: { + multiselectQuestions: true, + }, + }, + listingUtilities: true, + listingsApplicationDropOffAddress: true, + listingsApplicationPickUpAddress: true, + listingsBuildingAddress: true, + listingsBuildingSelectionCriteriaFile: true, + listingsLeasingAgentAddress: true, + listingsResult: true, + reservedCommunityTypes: true, + units: { + include: { + amiChart: { + include: { + amiChartItem: true, + jurisdictions: true, + unitGroupAmiLevels: true, + }, + }, + unitAccessibilityPriorityTypes: true, + unitAmiChartOverrides: true, + unitRentTypes: true, + unitTypes: true, + }, + }, + }, + data: { + ...val, + id: undefined, + publishedAt: expect.anything(), + assets: { + create: [exampleAsset], + }, + applicationMethods: { + create: [ + { + type: ApplicationMethodsTypeEnum.Internal, + label: 'example label', + externalReference: 'example reference', + acceptsPostmarkedApplications: false, + phoneNumber: '520-750-8811', + paperApplications: { + create: [ + { + language: LanguagesEnum.en, + assets: { + create: { + ...exampleAsset, + }, + }, + }, + ], + }, + }, + ], + }, + listingEvents: { + create: [ + { + type: ListingEventsTypeEnum.openHouse, + startDate: expect.anything(), + startTime: expect.anything(), + endTime: expect.anything(), + url: 'https://www.google.com', + note: 'example note', + label: 'example label', + assets: { + create: { + ...exampleAsset, + }, + }, + }, + ], + }, + listingImages: { + create: [ + { + assets: { + create: { + ...exampleAsset, + }, + }, + ordinal: 0, + }, + ], + }, + listingMultiselectQuestions: { + create: [ + { + ordinal: 0, + multiselectQuestions: { + connect: { + id: expect.anything(), + }, + }, + }, + ], + }, + listingsApplicationDropOffAddress: { + create: { + ...exampleAddress, + }, + }, + reservedCommunityTypes: { + connect: { + id: expect.anything(), + }, + }, + listingsBuildingSelectionCriteriaFile: { + create: { + ...exampleAsset, + }, + }, + listingUtilities: { + create: { + water: false, + gas: true, + trash: false, + sewer: true, + electricity: false, + cable: true, + phone: false, + internet: true, + }, + }, + listingsApplicationMailingAddress: { + create: { + ...exampleAddress, + }, + }, + listingsLeasingAgentAddress: { + create: { + ...exampleAddress, + }, + }, + listingFeatures: { + create: { + elevator: true, + wheelchairRamp: false, + serviceAnimalsAllowed: true, + accessibleParking: false, + parkingOnSite: true, + inUnitWasherDryer: false, + laundryInBuilding: true, + barrierFreeEntrance: false, + rollInShower: true, + grabBars: false, + heatingInUnit: true, + acInUnit: false, + hearing: true, + visual: false, + mobility: true, + }, + }, + jurisdictions: { + connect: { + id: expect.anything(), + }, + }, + listingsApplicationPickUpAddress: { + create: { + ...exampleAddress, + }, + }, + listingsBuildingAddress: { + create: { + ...exampleAddress, + }, + }, + units: { + create: [ + { + amiPercentage: '1', + annualIncomeMin: '2', + monthlyIncomeMin: '3', + floor: 4, + annualIncomeMax: '5', + maxOccupancy: 6, + minOccupancy: 7, + monthlyRent: '8', + numBathrooms: 9, + numBedrooms: 10, + number: '11', + sqFeet: '12', + monthlyRentAsPercentOfIncome: '13', + bmrProgramChart: true, + unitTypes: { + connect: { + id: expect.anything(), + }, + }, + amiChart: { + connect: { + id: expect.anything(), + }, + }, + unitAmiChartOverrides: { + create: { + items: { + items: [ + { + percentOfAmi: 10, + householdSize: 20, + income: 30, + }, + ], + }, + }, + }, + unitAccessibilityPriorityTypes: { + connect: { + id: expect.anything(), + }, + }, + unitRentTypes: { + connect: { + id: expect.anything(), + }, + }, + }, + ], + }, + unitsSummary: { + create: [ + { + monthlyRentMin: 1, + monthlyRentMax: 2, + monthlyRentAsPercentOfIncome: '3', + amiPercentage: 4, + minimumIncomeMin: '5', + minimumIncomeMax: '6', + maxOccupancy: 7, + minOccupancy: 8, + floorMin: 9, + floorMax: 10, + sqFeetMin: '11', + sqFeetMax: '12', + totalCount: 13, + totalAvailable: 14, + unitTypes: { + connect: { + id: expect.anything(), + }, + }, + unitAccessibilityPriorityTypes: { + connect: { + id: expect.anything(), + }, + }, + }, + ], + }, + listingsResult: { + create: { + ...exampleAsset, + }, + }, + }, + where: { + id: expect.anything(), + }, + }); + }); + + it('should purge single listing', async () => { + const id = randomUUID(); + process.env.PROXY_URL = 'https://www.google.com'; + await service.cachePurge( + ListingsStatusEnum.pending, + ListingsStatusEnum.pending, + id, + ); + expect(httpServiceMock.request).toHaveBeenCalledWith({ + baseURL: 'https://www.google.com', + method: 'PURGE', + url: `/listings/${id}*`, + }); + + process.env.PROXY_URL = undefined; + }); + + it('should purge all listings', async () => { + const id = randomUUID(); + process.env.PROXY_URL = 'https://www.google.com'; + await service.cachePurge( + ListingsStatusEnum.active, + ListingsStatusEnum.pending, + id, + ); + expect(httpServiceMock.request).toHaveBeenCalledWith({ + baseURL: 'https://www.google.com', + method: 'PURGE', + url: `/listings?*`, + }); + + process.env.PROXY_URL = undefined; + }); }); diff --git a/backend_new/test/unit/services/translation.service.spec.ts b/backend_new/test/unit/services/translation.service.spec.ts index 7ba584afac..45cd1afc7e 100644 --- a/backend_new/test/unit/services/translation.service.spec.ts +++ b/backend_new/test/unit/services/translation.service.spec.ts @@ -4,13 +4,13 @@ import { MultiselectQuestionsApplicationSectionEnum, } from '@prisma/client'; import { randomUUID } from 'crypto'; -import { ListingGet } from '../../../src/dtos/listings/listing-get.dto'; +import { Listing } from '../../../src/dtos/listings/listing.dto'; import { TranslationService } from '../../../src/services/translation.service'; import { PrismaService } from '../../../src/services/prisma.service'; import { Test, TestingModule } from '@nestjs/testing'; import { GoogleTranslateService } from '../../../src/services/google-translate.service'; -const mockListing = (): ListingGet => { +const mockListing = (): Listing => { const basicListing = { id: 'id 1', createdAt: new Date(), @@ -200,7 +200,7 @@ describe('Testing translations service', () => { .mockResolvedValue(null); const result = await service.translateListing( - mockListing() as ListingGet, + mockListing() as Listing, LanguagesEnum.es, ); expect(googleTranslateServiceMock.fetch).toHaveBeenCalledTimes(1); @@ -216,7 +216,7 @@ describe('Testing translations service', () => { .mockResolvedValue({ translations: [translatedStrings] }); const result = await service.translateListing( - mockListing() as ListingGet, + mockListing() as Listing, LanguagesEnum.es, ); expect(googleTranslateServiceMock.fetch).toHaveBeenCalledTimes(0); @@ -227,7 +227,7 @@ describe('Testing translations service', () => { }); }); -const validateTranslatedFields = (listing: ListingGet) => { +const validateTranslatedFields = (listing: Listing) => { expect(listing.applicationPickUpAddressOfficeHours).toEqual( 'translated application pick up address office hours', ); diff --git a/backend_new/test/unit/utilities/listing-url-slug.spec.ts b/backend_new/test/unit/utilities/listing-url-slug.spec.ts new file mode 100644 index 0000000000..4681cc4df9 --- /dev/null +++ b/backend_new/test/unit/utilities/listing-url-slug.spec.ts @@ -0,0 +1,66 @@ +import { ListingsStatusEnum } from '@prisma/client'; +import { randomUUID } from 'crypto'; +import { + listingUrlSlug, + listingUrlSlugHelper, +} from '../../../src/utilities/listing-url-slug'; + +const baseListingFields = { + status: ListingsStatusEnum.active, + displayWaitlistSize: false, + showWaitlist: false, + applicationMethods: [], + referralApplication: undefined, + assets: [], + listingEvents: [], + listingsBuildingAddress: undefined, + jurisdictions: { + id: randomUUID(), + }, + units: [], + id: randomUUID(), + createdAt: new Date(), + updatedAt: new Date(), + urlSlug: '', +}; + +describe('Testing listing url slug builder', () => { + it('should build a slug that is all lower case and special characters are removed', () => { + expect( + listingUrlSlug({ + name: 'ExampLe namE @ 17', + ...baseListingFields, + }), + ).toEqual('examp_le_nam_e_17'); + }); + + it('should build a slug that includes the address info', () => { + expect( + listingUrlSlug({ + ...baseListingFields, + name: 'ExampLe namE @ 17', + listingsBuildingAddress: { + id: randomUUID(), + createdAt: new Date(), + updatedAt: new Date(), + street: '11th Street', + city: 'Phoenix', + state: 'Az', + zipCode: '87511', + }, + }), + ).toEqual('examp_le_nam_e_17_11_th_street_phoenix_az'); + }); + + it('should build a slug from string', () => { + expect(listingUrlSlugHelper('ExampLe namE @ 17')).toEqual( + 'examp_le_nam_e_17', + ); + }); + + it('should build a slug from array.join(" ")', () => { + expect( + listingUrlSlugHelper('ExampLe namE @ 17 11th street Phoenix Az 85711'), + ).toEqual('examp_le_nam_e_17_11_th_street_phoenix_az_85711'); + }); +}); diff --git a/backend_new/test/unit/utilities/unit-utilities.spec.ts b/backend_new/test/unit/utilities/unit-utilities.spec.ts index 14ac4a39c5..b159324082 100644 --- a/backend_new/test/unit/utilities/unit-utilities.spec.ts +++ b/backend_new/test/unit/utilities/unit-utilities.spec.ts @@ -1,11 +1,11 @@ import { AmiChart } from '../../../src/dtos/ami-charts/ami-chart.dto'; -import { UnitAmiChartOverride } from '../../../src/dtos/units/ami-chart-override-get.dto'; +import { UnitAmiChartOverride } from '../../../src/dtos/units/ami-chart-override.dto'; import { generateHmiData, mergeAmiChartWithOverrides, } from '../../../src/utilities/unit-utilities'; -import { Unit } from '../../../src/dtos/units/unit-get.dto'; -import { AmiChartItem } from '../../../src/dtos/units/ami-chart-item-get.dto'; +import { Unit } from '../../../src/dtos/units/unit.dto'; +import { AmiChartItem } from '../../../src/dtos/units/ami-chart-item.dto'; const defaultValues = { createdAt: new Date(), diff --git a/backend_new/types/src/backend-swagger.ts b/backend_new/types/src/backend-swagger.ts index 851338db44..b286d8ff56 100644 --- a/backend_new/types/src/backend-swagger.ts +++ b/backend_new/types/src/backend-swagger.ts @@ -139,9 +139,9 @@ export class ListingsService { /** */ view?: ListingViews; /** */ - orderBy?: any | null[]; + orderBy?: ListingOrderByKeys[]; /** */ - orderDir?: any | null[]; + orderDir?: OrderByEnum[]; /** */ search?: string; } = {} as any, @@ -171,6 +171,60 @@ export class ListingsService { axios(configs, resolve, reject); }); } + /** + * Create listing + */ + create( + params: { + /** requestBody */ + body?: ListingCreate; + } = {} as any, + options: IRequestOptions = {}, + ): Promise { + return new Promise((resolve, reject) => { + let url = basePath + '/listings'; + + const configs: IRequestConfig = getConfigs( + 'post', + 'application/json', + url, + options, + ); + + let data = params.body; + + configs.data = data; + + axios(configs, resolve, reject); + }); + } + /** + * Delete listing by id + */ + delete( + params: { + /** requestBody */ + body?: IdDTO; + } = {} as any, + options: IRequestOptions = {}, + ): Promise { + return new Promise((resolve, reject) => { + let url = basePath + '/listings'; + + const configs: IRequestConfig = getConfigs( + 'delete', + 'application/json', + url, + options, + ); + + let data = params.body; + + configs.data = data; + + axios(configs, resolve, reject); + }); + } /** * Get listing by id */ @@ -182,7 +236,7 @@ export class ListingsService { view?: ListingViews; } = {} as any, options: IRequestOptions = {}, - ): Promise { + ): Promise { return new Promise((resolve, reject) => { let url = basePath + '/listings/{id}'; url = url.replace('{id}', params['id'] + ''); @@ -200,6 +254,36 @@ export class ListingsService { axios(configs, resolve, reject); }); } + /** + * Update listing by id + */ + update( + params: { + /** */ + id: string; + /** requestBody */ + body?: ListingUpdate; + } = {} as any, + options: IRequestOptions = {}, + ): Promise { + return new Promise((resolve, reject) => { + let url = basePath + '/listings/{id}'; + url = url.replace('{id}', params['id'] + ''); + + const configs: IRequestConfig = getConfigs( + 'put', + 'application/json', + url, + options, + ); + + let data = params.body; + + configs.data = data; + + axios(configs, resolve, reject); + }); + } /** * Get listings by multiselect question id */ @@ -1069,7 +1153,7 @@ export class MultiselectQuestionsService { /** */ jurisdiction?: string; /** */ - applicationSection?: string; + applicationSection?: MultiselectQuestionsApplicationSectionEnum; } = {} as any, options: IRequestOptions = {}, ): Promise { @@ -1636,10 +1720,10 @@ export interface ListingsQueryParams { view?: ListingViews; /** */ - orderBy?: []; + orderBy?: ListingOrderByKeys[]; /** */ - orderDir?: EnumListingsQueryParamsOrderDir[]; + orderDir?: OrderByEnum[]; /** */ search?: string; @@ -1653,7 +1737,7 @@ export interface ListingFilterParams { name?: string; /** */ - status?: EnumListingFilterParamsStatus; + status?: ListingStatusEnum; /** */ neighborhood?: string; @@ -1690,6 +1774,9 @@ export interface IdDTO { /** */ name?: string; + + /** */ + ordinal?: number; } export interface MultiselectLink { @@ -1705,22 +1792,22 @@ export interface MultiselectOption { text: string; /** */ - untranslatedText: string; + untranslatedText?: string; /** */ ordinal: number; /** */ - description: string; + description?: string; /** */ - links: MultiselectLink[]; + links?: MultiselectLink[]; /** */ - collectAddress: boolean; + collectAddress?: boolean; /** */ - exclusive: boolean; + exclusive?: boolean; } export interface MultiselectQuestion { @@ -1737,31 +1824,31 @@ export interface MultiselectQuestion { text: string; /** */ - untranslatedText: string; + untranslatedText?: string; /** */ - untranslatedOptOutText: string; + untranslatedOptOutText?: string; /** */ - subText: string; + subText?: string; /** */ - description: string; + description?: string; /** */ - links: MultiselectLink[]; + links?: MultiselectLink[]; /** */ jurisdictions: IdDTO[]; /** */ - options: MultiselectOption[]; + options?: MultiselectOption[]; /** */ - optOutText: string; + optOutText?: string; /** */ - hideFromListing: boolean; + hideFromListing?: boolean; /** */ applicationSection: MultiselectQuestionsApplicationSectionEnum; @@ -1772,10 +1859,10 @@ export interface ListingMultiselectQuestion { multiselectQuestions: MultiselectQuestion; /** */ - ordinal: number; + ordinal?: number; } -export interface ApplicationMethod { +export interface Asset { /** */ id: string; @@ -1786,10 +1873,13 @@ export interface ApplicationMethod { updatedAt: Date; /** */ - type: ApplicationMethodsTypeEnum; + fileId: string; + + /** */ + label: string; } -export interface Asset { +export interface PaperApplication { /** */ id: string; @@ -1798,20 +1888,15 @@ export interface Asset { /** */ updatedAt: Date; -} - -export interface Address { - /** */ - id: string; /** */ - createdAt: Date; + language: LanguagesEnum; /** */ - updatedAt: Date; + assets: Asset; } -export interface Jurisdiction { +export interface ApplicationMethod { /** */ id: string; @@ -1822,73 +1907,71 @@ export interface Jurisdiction { updatedAt: Date; /** */ - name: string; + type: ApplicationMethodsTypeEnum; /** */ - notificationsSignUpUrl: string; + label?: string; /** */ - languages: string[]; + externalReference?: string; /** */ - multiselectQuestions: string[]; + acceptsPostmarkedApplications?: boolean; /** */ - partnerTerms: string; + phoneNumber?: string; /** */ - publicUrl: string; + paperApplications?: PaperApplication[]; +} +export interface Address { /** */ - emailFromAddress: string; + id: string; /** */ - rentalAssistanceDefault: string; + createdAt: Date; /** */ - enablePartnerSettings: boolean; + updatedAt: Date; /** */ - enableAccessibilityFeatures: boolean; + placeName?: string; /** */ - enableUtilitiesIncluded: boolean; -} + city: string; -export interface ReservedCommunityType { /** */ - id: string; + county?: string; /** */ - createdAt: Date; + state: string; /** */ - updatedAt: Date; + street: string; /** */ - name: string; + street2?: string; /** */ - description: string; + zipCode: string; /** */ - jurisdictions: IdDTO; -} + latitude?: number; -export interface ListingImage {} - -export interface ListingFeatures { /** */ - id: string; + longitude?: number; +} +export interface ListingImage { /** */ - createdAt: Date; + assets: Asset; /** */ - updatedAt: Date; + ordinal?: number; } -export interface ListingUtilities { +export interface ListingFeatures { /** */ id: string; @@ -1897,134 +1980,120 @@ export interface ListingUtilities { /** */ updatedAt: Date; -} -export interface Unit { /** */ - id: string; + elevator?: boolean; /** */ - createdAt: Date; + wheelchairRamp?: boolean; /** */ - updatedAt: Date; -} + serviceAnimalsAllowed?: boolean; -export interface UnitType { /** */ - id: string; + accessibleParking?: boolean; /** */ - createdAt: Date; + parkingOnSite?: boolean; /** */ - updatedAt: Date; + inUnitWasherDryer?: boolean; /** */ - name: UnitTypeEnum; + laundryInBuilding?: boolean; /** */ - numBedrooms: number; -} + barrierFreeEntrance?: boolean; -export interface UnitAccessibilityPriorityType { /** */ - id: string; + rollInShower?: boolean; /** */ - createdAt: Date; + grabBars?: boolean; /** */ - updatedAt: Date; + heatingInUnit?: boolean; /** */ - name: UnitAccessibilityPriorityTypeEnum; -} + acInUnit?: boolean; -export interface MinMaxCurrency { /** */ - min: string; + hearing?: boolean; /** */ - max: string; -} + visual?: boolean; -export interface MinMax { /** */ - min: number; + mobility?: boolean; +} +export interface ListingUtilities { /** */ - max: number; -} + id: string; -export interface UnitSummary { /** */ - unitTypes: UnitType; + createdAt: Date; /** */ - minIncomeRange: MinMaxCurrency; + updatedAt: Date; /** */ - occupancyRange: MinMax; + water?: boolean; /** */ - rentAsPercentIncomeRange: MinMax; + gas?: boolean; /** */ - rentRange: MinMaxCurrency; + trash?: boolean; /** */ - totalAvailable: number; + sewer?: boolean; /** */ - areaRange: MinMax; + electricity?: boolean; /** */ - floorRange?: MinMax; -} + cable?: boolean; -export interface UnitSummaryByAMI { /** */ - percent: string; + phone?: boolean; /** */ - byUnitType: UnitSummary[]; + internet?: boolean; } -export interface HMI { +export interface AmiChartItem { /** */ - columns: object; + percentOfAmi: number; /** */ - rows: object[]; -} + householdSize: number; -export interface UnitsSummarized { /** */ - unitTypes: UnitType[]; + income: number; +} +export interface AmiChart { /** */ - priorityTypes: UnitAccessibilityPriorityType[]; + id: string; /** */ - amiPercentages: string[]; + createdAt: Date; /** */ - byUnitTypeAndRent: UnitSummary[]; + updatedAt: Date; /** */ - byUnitType: UnitSummary[]; + items: AmiChartItem[]; /** */ - byAMI: UnitSummaryByAMI[]; + name: string; /** */ - hmi: HMI; + jurisdictions: IdDTO; } -export interface UnitsSummary {} - -export interface ListingGet { +export interface UnitType { /** */ id: string; @@ -2035,275 +2104,1289 @@ export interface ListingGet { updatedAt: Date; /** */ - additionalApplicationSubmissionNotes: string; + name: UnitTypeEnum; /** */ - digitalApplication: boolean; + numBedrooms: number; +} +export interface UnitRentType { /** */ - commonDigitalApplication: boolean; + id: string; /** */ - paperApplication: boolean; + createdAt: Date; /** */ - referralOpportunity: boolean; + updatedAt: Date; /** */ - accessibility: string; + name: UnitRentTypeEnum; +} +export interface UnitAccessibilityPriorityType { /** */ - amenities: string; + id: string; /** */ - buildingTotalUnits: number; + createdAt: Date; /** */ - developer: string; + updatedAt: Date; /** */ - householdSizeMax: number; + name: UnitAccessibilityPriorityTypeEnum; +} +export interface UnitAmiChartOverride { /** */ - householdSizeMin: number; + id: string; /** */ - neighborhood: string; + createdAt: Date; /** */ - petPolicy: string; + updatedAt: Date; /** */ - smokingPolicy: string; + items: AmiChartItem[]; +} +export interface Unit { /** */ - unitsAvailable: number; + id: string; /** */ - unitAmenities: string; + createdAt: Date; /** */ - servicesOffered: string; + updatedAt: Date; /** */ - yearBuilt: number; + amiChart?: AmiChart; /** */ - applicationDueDate: Date; + amiPercentage?: string; /** */ - applicationOpenDate: Date; + annualIncomeMin?: string; /** */ - applicationFee: string; + monthlyIncomeMin?: string; /** */ - applicationOrganization: string; + floor?: number; /** */ - applicationPickUpAddressOfficeHours: string; + annualIncomeMax?: string; /** */ - applicationPickUpAddressType: ApplicationAddressTypeEnum; + maxOccupancy?: number; /** */ - applicationDropOffAddressOfficeHours: string; + minOccupancy?: number; /** */ - applicationDropOffAddressType: ApplicationAddressTypeEnum; + monthlyRent?: string; /** */ - applicationMailingAddressType: ApplicationAddressTypeEnum; + numBathrooms?: number; /** */ - buildingSelectionCriteria: string; + numBedrooms?: number; /** */ - costsNotIncluded: string; + number?: string; /** */ - creditHistory: string; + sqFeet?: string; /** */ - criminalBackground: string; + monthlyRentAsPercentOfIncome?: string; /** */ - depositMin: string; + bmrProgramChart?: boolean; /** */ - depositMax: string; + unitTypes?: UnitType; /** */ - depositHelperText: string; + unitRentTypes?: UnitRentType; /** */ - disableUnitsAccordion: boolean; + unitAccessibilityPriorityTypes?: UnitAccessibilityPriorityType; /** */ - leasingAgentEmail: string; + unitAmiChartOverrides?: UnitAmiChartOverride; +} +export interface MinMaxCurrency { /** */ - leasingAgentName: string; + min: string; /** */ - leasingAgentOfficeHours: string; + max: string; +} +export interface MinMax { /** */ - leasingAgentPhone: string; + min: number; /** */ - leasingAgentTitle: string; + max: number; +} +export interface UnitSummary { /** */ - name: string; + unitTypes: UnitType; /** */ - postmarkedApplicationsReceivedByDate: Date; + minIncomeRange: MinMaxCurrency; /** */ - programRules: string; + occupancyRange: MinMax; /** */ - rentalAssistance: string; + rentAsPercentIncomeRange: MinMax; /** */ - rentalHistory: string; + rentRange: MinMaxCurrency; /** */ - requiredDocuments: string; + totalAvailable: number; /** */ - specialNotes: string; + areaRange: MinMax; /** */ - waitlistCurrentSize: number; + floorRange?: MinMax; +} +export interface UnitSummaryByAMI { /** */ - waitlistMaxSize: number; + percent: string; /** */ - whatToExpect: string; + byUnitType: UnitSummary[]; +} +export interface HMI { /** */ - status: ListingsStatusEnum; + columns: object; /** */ - reviewOrderType: ReviewOrderTypeEnum; + rows: object[]; +} +export interface UnitsSummarized { /** */ - applicationConfig: object; + unitTypes: UnitType[]; /** */ - displayWaitlistSize: boolean; + priorityTypes: UnitAccessibilityPriorityType[]; /** */ - showWaitlist: boolean; + amiPercentages: string[]; /** */ - reservedCommunityDescription: string; + byUnitTypeAndRent: UnitSummary[]; /** */ - reservedCommunityMinAge: number; + byUnitType: UnitSummary[]; /** */ - resultLink: string; + byAMI: UnitSummaryByAMI[]; /** */ - isWaitlistOpen: boolean; + hmi: HMI; +} + +export interface UnitsSummary { + /** */ + id: string; + + /** */ + unitTypes: IdDTO; + + /** */ + monthlyRentMin?: number; + + /** */ + monthlyRentMax?: number; + + /** */ + monthlyRentAsPercentOfIncome?: string; + + /** */ + amiPercentage?: number; + + /** */ + minimumIncomeMin?: string; + + /** */ + minimumIncomeMax?: string; + + /** */ + maxOccupancy?: number; + + /** */ + minOccupancy?: number; + + /** */ + floorMin?: number; + + /** */ + floorMax?: number; + + /** */ + sqFeetMin?: string; + + /** */ + sqFeetMax?: string; + + /** */ + unitAccessibilityPriorityTypes?: IdDTO; + + /** */ + totalCount?: number; + + /** */ + totalAvailable?: number; +} + +export interface Listing { + /** */ + id: string; + + /** */ + createdAt: Date; + + /** */ + updatedAt: Date; + + /** */ + additionalApplicationSubmissionNotes?: string; + + /** */ + digitalApplication?: boolean; + + /** */ + commonDigitalApplication?: boolean; + + /** */ + paperApplication?: boolean; + + /** */ + referralOpportunity?: boolean; + + /** */ + accessibility?: string; + + /** */ + amenities?: string; + + /** */ + buildingTotalUnits?: number; + + /** */ + developer?: string; + + /** */ + householdSizeMax?: number; + + /** */ + householdSizeMin?: number; + + /** */ + neighborhood?: string; + + /** */ + petPolicy?: string; + + /** */ + smokingPolicy?: string; + + /** */ + unitsAvailable?: number; + + /** */ + unitAmenities?: string; + + /** */ + servicesOffered?: string; + + /** */ + yearBuilt?: number; + + /** */ + applicationDueDate?: Date; + + /** */ + applicationOpenDate?: Date; + + /** */ + applicationFee?: string; + + /** */ + applicationOrganization?: string; + + /** */ + applicationPickUpAddressOfficeHours?: string; + + /** */ + applicationPickUpAddressType?: ApplicationAddressTypeEnum; + + /** */ + applicationDropOffAddressOfficeHours?: string; + + /** */ + applicationDropOffAddressType?: ApplicationAddressTypeEnum; + + /** */ + applicationMailingAddressType?: ApplicationAddressTypeEnum; + + /** */ + buildingSelectionCriteria?: string; + + /** */ + costsNotIncluded?: string; + + /** */ + creditHistory?: string; + + /** */ + criminalBackground?: string; + + /** */ + depositMin?: string; + + /** */ + depositMax?: string; + + /** */ + depositHelperText?: string; + + /** */ + disableUnitsAccordion?: boolean; + + /** */ + leasingAgentEmail?: string; + + /** */ + leasingAgentName?: string; + + /** */ + leasingAgentOfficeHours?: string; + + /** */ + leasingAgentPhone?: string; + + /** */ + leasingAgentTitle?: string; + + /** */ + name: string; + + /** */ + postmarkedApplicationsReceivedByDate?: Date; + + /** */ + programRules?: string; + + /** */ + rentalAssistance?: string; + + /** */ + rentalHistory?: string; + + /** */ + requiredDocuments?: string; + + /** */ + specialNotes?: string; + + /** */ + waitlistCurrentSize?: number; + + /** */ + waitlistMaxSize?: number; + + /** */ + whatToExpect?: string; + + /** */ + status: ListingsStatusEnum; + + /** */ + reviewOrderType?: ReviewOrderTypeEnum; + + /** */ + applicationConfig?: object; + + /** */ + displayWaitlistSize: boolean; + + /** */ + showWaitlist?: boolean; + + /** */ + reservedCommunityDescription?: string; + + /** */ + reservedCommunityMinAge?: number; + + /** */ + resultLink?: string; + + /** */ + isWaitlistOpen?: boolean; + + /** */ + waitlistOpenSpots?: number; + + /** */ + customMapPin?: boolean; + + /** */ + publishedAt?: Date; + + /** */ + closedAt?: Date; + + /** */ + afsLastRunAt?: Date; + + /** */ + lastApplicationUpdateAt?: Date; + + /** */ + listingMultiselectQuestions?: ListingMultiselectQuestion[]; + + /** */ + applicationMethods: ApplicationMethod[]; + + /** */ + referralApplication?: ApplicationMethod; + + /** */ + assets: Asset[]; + + /** */ + listingEvents: Asset[]; + + /** */ + listingsBuildingAddress: Address; + + /** */ + listingsApplicationPickUpAddress?: Address; + + /** */ + listingsApplicationDropOffAddress?: Address; + + /** */ + listingsApplicationMailingAddress?: Address; + + /** */ + listingsLeasingAgentAddress?: Address; + + /** */ + listingsBuildingSelectionCriteriaFile?: Asset; + + /** */ + jurisdictions: IdDTO; + + /** */ + listingsResult?: Asset; + + /** */ + reservedCommunityTypes?: IdDTO; + + /** */ + listingImages?: ListingImage[]; + + /** */ + listingFeatures?: ListingFeatures; + + /** */ + listingUtilities?: ListingUtilities; + + /** */ + units: Unit[]; + + /** */ + unitsSummarized?: UnitsSummarized; + + /** */ + unitsSummary?: UnitsSummary[]; + + /** */ + urlSlug?: string; +} + +export interface PaginatedListing { + /** */ + items: Listing[]; +} + +export interface UnitAmiChartOverrideCreate { + /** */ + items: AmiChartItem[]; +} + +export interface UnitCreate { + /** */ + amiPercentage?: string; + + /** */ + annualIncomeMin?: string; + + /** */ + monthlyIncomeMin?: string; + + /** */ + floor?: number; + + /** */ + annualIncomeMax?: string; + + /** */ + maxOccupancy?: number; + + /** */ + minOccupancy?: number; + + /** */ + monthlyRent?: string; + + /** */ + numBathrooms?: number; + + /** */ + numBedrooms?: number; + + /** */ + number?: string; + + /** */ + sqFeet?: string; + + /** */ + monthlyRentAsPercentOfIncome?: string; + + /** */ + bmrProgramChart?: boolean; + + /** */ + unitTypes?: IdDTO; + + /** */ + amiChart?: IdDTO; + + /** */ + unitAccessibilityPriorityTypes?: IdDTO; + + /** */ + unitRentTypes?: IdDTO; + + /** */ + unitAmiChartOverrides?: UnitAmiChartOverrideCreate; +} + +export interface AssetCreate { + /** */ + fileId: string; + + /** */ + label: string; +} + +export interface PaperApplicationCreate { + /** */ + language: LanguagesEnum; + + /** */ + assets?: AssetCreate; +} + +export interface ApplicationMethodCreate { + /** */ + type: ApplicationMethodsTypeEnum; + + /** */ + label?: string; + + /** */ + externalReference?: string; + + /** */ + acceptsPostmarkedApplications?: boolean; + + /** */ + phoneNumber?: string; + + /** */ + paperApplications?: PaperApplicationCreate[]; +} + +export interface UnitsSummaryCreate { + /** */ + unitTypes: IdDTO; + + /** */ + monthlyRentMin?: number; + + /** */ + monthlyRentMax?: number; + + /** */ + monthlyRentAsPercentOfIncome?: string; + + /** */ + amiPercentage?: number; + + /** */ + minimumIncomeMin?: string; + + /** */ + minimumIncomeMax?: string; + + /** */ + maxOccupancy?: number; + + /** */ + minOccupancy?: number; + + /** */ + floorMin?: number; + + /** */ + floorMax?: number; + + /** */ + sqFeetMin?: string; + + /** */ + sqFeetMax?: string; + + /** */ + unitAccessibilityPriorityTypes?: IdDTO; + + /** */ + totalCount?: number; + + /** */ + totalAvailable?: number; +} + +export interface ListingImageCreate { + /** */ + ordinal?: number; + + /** */ + assets: AssetCreate; +} + +export interface AddressCreate { + /** */ + placeName?: string; + + /** */ + city: string; + + /** */ + county?: string; + + /** */ + state: string; + + /** */ + street: string; + + /** */ + street2?: string; + + /** */ + zipCode: string; + + /** */ + latitude?: number; + + /** */ + longitude?: number; +} + +export interface ListingEventCreate { + /** */ + type: ListingEventsTypeEnum; + + /** */ + startDate?: Date; + + /** */ + startTime?: Date; + + /** */ + endTime?: Date; + + /** */ + url?: string; + + /** */ + note?: string; + + /** */ + label?: string; + + /** */ + assets?: AssetCreate; +} + +export interface ListingFeaturesCreate { + /** */ + elevator?: boolean; + + /** */ + wheelchairRamp?: boolean; + + /** */ + serviceAnimalsAllowed?: boolean; + + /** */ + accessibleParking?: boolean; + + /** */ + parkingOnSite?: boolean; + + /** */ + inUnitWasherDryer?: boolean; + + /** */ + laundryInBuilding?: boolean; + + /** */ + barrierFreeEntrance?: boolean; + + /** */ + rollInShower?: boolean; + + /** */ + grabBars?: boolean; + + /** */ + heatingInUnit?: boolean; + + /** */ + acInUnit?: boolean; + + /** */ + hearing?: boolean; + + /** */ + visual?: boolean; + + /** */ + mobility?: boolean; +} + +export interface ListingUtilitiesCreate { + /** */ + water?: boolean; + + /** */ + gas?: boolean; + + /** */ + trash?: boolean; + + /** */ + sewer?: boolean; + + /** */ + electricity?: boolean; + + /** */ + cable?: boolean; + + /** */ + phone?: boolean; + + /** */ + internet?: boolean; +} + +export interface ListingCreate { + /** */ + additionalApplicationSubmissionNotes?: string; + + /** */ + digitalApplication?: boolean; + + /** */ + commonDigitalApplication?: boolean; + + /** */ + paperApplication?: boolean; + + /** */ + referralOpportunity?: boolean; + + /** */ + accessibility?: string; + + /** */ + amenities?: string; + + /** */ + buildingTotalUnits?: number; + + /** */ + developer?: string; + + /** */ + householdSizeMax?: number; + + /** */ + householdSizeMin?: number; + + /** */ + neighborhood?: string; + + /** */ + petPolicy?: string; + + /** */ + smokingPolicy?: string; + + /** */ + unitsAvailable?: number; + + /** */ + unitAmenities?: string; + + /** */ + servicesOffered?: string; + + /** */ + yearBuilt?: number; + + /** */ + applicationDueDate?: Date; + + /** */ + applicationOpenDate?: Date; + + /** */ + applicationFee?: string; + + /** */ + applicationOrganization?: string; + + /** */ + applicationPickUpAddressOfficeHours?: string; + + /** */ + applicationPickUpAddressType?: ApplicationAddressTypeEnum; + + /** */ + applicationDropOffAddressOfficeHours?: string; + + /** */ + applicationDropOffAddressType?: ApplicationAddressTypeEnum; + + /** */ + applicationMailingAddressType?: ApplicationAddressTypeEnum; + + /** */ + buildingSelectionCriteria?: string; + + /** */ + costsNotIncluded?: string; + + /** */ + creditHistory?: string; + + /** */ + criminalBackground?: string; + + /** */ + depositMin?: string; + + /** */ + depositMax?: string; + + /** */ + depositHelperText?: string; + + /** */ + disableUnitsAccordion?: boolean; + + /** */ + leasingAgentEmail?: string; + + /** */ + leasingAgentName?: string; + + /** */ + leasingAgentOfficeHours?: string; + + /** */ + leasingAgentPhone?: string; + + /** */ + leasingAgentTitle?: string; + + /** */ + name: string; + + /** */ + postmarkedApplicationsReceivedByDate?: Date; + + /** */ + programRules?: string; + + /** */ + rentalAssistance?: string; + + /** */ + rentalHistory?: string; + + /** */ + requiredDocuments?: string; + + /** */ + specialNotes?: string; + + /** */ + waitlistCurrentSize?: number; + + /** */ + waitlistMaxSize?: number; + + /** */ + whatToExpect?: string; + + /** */ + status: ListingsStatusEnum; + + /** */ + reviewOrderType?: ReviewOrderTypeEnum; + + /** */ + displayWaitlistSize: boolean; + + /** */ + reservedCommunityDescription?: string; + + /** */ + reservedCommunityMinAge?: number; + + /** */ + resultLink?: string; + + /** */ + isWaitlistOpen?: boolean; + + /** */ + waitlistOpenSpots?: number; + + /** */ + customMapPin?: boolean; + + /** */ + lastApplicationUpdateAt?: Date; + + /** */ + jurisdictions: IdDTO; + + /** */ + reservedCommunityTypes?: IdDTO; + + /** */ + listingMultiselectQuestions?: IdDTO[]; + + /** */ + units?: UnitCreate[]; + + /** */ + applicationMethods?: ApplicationMethodCreate[]; + + /** */ + assets: AssetCreate[]; + + /** */ + unitsSummary: UnitsSummaryCreate[]; /** */ - waitlistOpenSpots: number; + listingImages?: ListingImageCreate[]; /** */ - customMapPin: boolean; + listingsApplicationPickUpAddress?: AddressCreate; /** */ - publishedAt: Date; + listingsApplicationMailingAddress?: AddressCreate; /** */ - closedAt: Date; + listingsApplicationDropOffAddress?: AddressCreate; /** */ - afsLastRunAt: Date; + listingsLeasingAgentAddress?: AddressCreate; /** */ - lastApplicationUpdateAt: Date; + listingsBuildingAddress?: AddressCreate; /** */ - listingMultiselectQuestions: ListingMultiselectQuestion[]; + listingsBuildingSelectionCriteriaFile?: AssetCreate; /** */ - applicationMethods: ApplicationMethod[]; + listingsResult?: AssetCreate; /** */ - referralApplication?: ApplicationMethod; + listingEvents: ListingEventCreate[]; /** */ - assets: Asset[]; + listingFeatures?: ListingFeaturesCreate; /** */ - events: Asset[]; + listingUtilities?: ListingUtilitiesCreate; +} +export interface ListingUpdate { /** */ - listingsBuildingAddress: Address; + id: string; /** */ - listingsApplicationPickUpAddress: Address; + additionalApplicationSubmissionNotes?: string; /** */ - listingsApplicationDropOffAddress: Address; + digitalApplication?: boolean; /** */ - listingsApplicationMailingAddress: Address; + commonDigitalApplication?: boolean; /** */ - listingsLeasingAgentAddress: Address; + paperApplication?: boolean; /** */ - listingsBuildingSelectionCriteriaFile: Asset; + referralOpportunity?: boolean; /** */ - jurisdictions: Jurisdiction; + accessibility?: string; /** */ - listingsResult: Asset; + amenities?: string; /** */ - reservedCommunityTypes: ReservedCommunityType; + buildingTotalUnits?: number; /** */ - listingImages: ListingImage[]; + developer?: string; /** */ - listingFeatures: ListingFeatures; + householdSizeMax?: number; /** */ - listingUtilities: ListingUtilities; + householdSizeMin?: number; /** */ - units: Unit[]; + neighborhood?: string; /** */ - unitsSummarized: UnitsSummarized; + petPolicy?: string; /** */ - unitsSummary: UnitsSummary[]; -} + smokingPolicy?: string; -export interface PaginatedListing { /** */ - items: ListingGet[]; -} + unitsAvailable?: number; -export interface AmiChartItem { /** */ - percentOfAmi: number; + unitAmenities?: string; /** */ - householdSize: number; + servicesOffered?: string; /** */ - income: number; + yearBuilt?: number; + + /** */ + applicationDueDate?: Date; + + /** */ + applicationOpenDate?: Date; + + /** */ + applicationFee?: string; + + /** */ + applicationOrganization?: string; + + /** */ + applicationPickUpAddressOfficeHours?: string; + + /** */ + applicationPickUpAddressType?: ApplicationAddressTypeEnum; + + /** */ + applicationDropOffAddressOfficeHours?: string; + + /** */ + applicationDropOffAddressType?: ApplicationAddressTypeEnum; + + /** */ + applicationMailingAddressType?: ApplicationAddressTypeEnum; + + /** */ + buildingSelectionCriteria?: string; + + /** */ + costsNotIncluded?: string; + + /** */ + creditHistory?: string; + + /** */ + criminalBackground?: string; + + /** */ + depositMin?: string; + + /** */ + depositMax?: string; + + /** */ + depositHelperText?: string; + + /** */ + disableUnitsAccordion?: boolean; + + /** */ + leasingAgentEmail?: string; + + /** */ + leasingAgentName?: string; + + /** */ + leasingAgentOfficeHours?: string; + + /** */ + leasingAgentPhone?: string; + + /** */ + leasingAgentTitle?: string; + + /** */ + name: string; + + /** */ + postmarkedApplicationsReceivedByDate?: Date; + + /** */ + programRules?: string; + + /** */ + rentalAssistance?: string; + + /** */ + rentalHistory?: string; + + /** */ + requiredDocuments?: string; + + /** */ + specialNotes?: string; + + /** */ + waitlistCurrentSize?: number; + + /** */ + waitlistMaxSize?: number; + + /** */ + whatToExpect?: string; + + /** */ + status: ListingsStatusEnum; + + /** */ + reviewOrderType?: ReviewOrderTypeEnum; + + /** */ + displayWaitlistSize: boolean; + + /** */ + reservedCommunityDescription?: string; + + /** */ + reservedCommunityMinAge?: number; + + /** */ + resultLink?: string; + + /** */ + isWaitlistOpen?: boolean; + + /** */ + waitlistOpenSpots?: number; + + /** */ + customMapPin?: boolean; + + /** */ + lastApplicationUpdateAt?: Date; + + /** */ + jurisdictions: IdDTO; + + /** */ + reservedCommunityTypes?: IdDTO; + + /** */ + listingMultiselectQuestions?: IdDTO[]; + + /** */ + units?: UnitCreate[]; + + /** */ + applicationMethods?: ApplicationMethodCreate[]; + + /** */ + assets: AssetCreate[]; + + /** */ + unitsSummary: UnitsSummaryCreate[]; + + /** */ + listingImages?: ListingImageCreate[]; + + /** */ + listingsApplicationPickUpAddress?: AddressCreate; + + /** */ + listingsApplicationMailingAddress?: AddressCreate; + + /** */ + listingsApplicationDropOffAddress?: AddressCreate; + + /** */ + listingsLeasingAgentAddress?: AddressCreate; + + /** */ + listingsBuildingAddress?: AddressCreate; + + /** */ + listingsBuildingSelectionCriteriaFile?: AssetCreate; + + /** */ + listingsResult?: AssetCreate; + + /** */ + listingEvents: ListingEventCreate[]; + + /** */ + listingFeatures?: ListingFeaturesCreate; + + /** */ + listingUtilities?: ListingUtilitiesCreate; } export interface AmiChartCreate { @@ -2333,51 +3416,51 @@ export interface AmiChartQueryParams { jurisdictionId?: string; } -export interface AmiChart { +export interface ReservedCommunityTypeCreate { /** */ - id: string; + name: string; /** */ - createdAt: Date; + description?: string; /** */ - updatedAt: Date; + jurisdictions: IdDTO; +} +export interface ReservedCommunityTypeUpdate { /** */ - items: AmiChartItem[]; + id: string; /** */ name: string; /** */ - jurisdictions: IdDTO; + description?: string; } -export interface ReservedCommunityTypeCreate { +export interface ReservedCommunityTypeQueryParams { /** */ - name: string; + jurisdictionId?: string; +} +export interface ReservedCommunityType { /** */ - description: string; + id: string; /** */ - jurisdictions: IdDTO; -} + createdAt: Date; -export interface ReservedCommunityTypeUpdate { /** */ - id: string; + updatedAt: Date; /** */ name: string; /** */ - description: string; -} + description?: string; -export interface ReservedCommunityTypeQueryParams { /** */ - jurisdictionId?: string; + jurisdictions: IdDTO; } export interface UnitTypeCreate { @@ -2425,32 +3508,53 @@ export interface UnitRentTypeUpdate { name: UnitRentTypeEnum; } -export interface UnitRentType { +export interface JurisdictionCreate { /** */ - id: string; + name: string; /** */ - createdAt: Date; + notificationsSignUpUrl?: string; /** */ - updatedAt: Date; + languages: LanguagesEnum[]; /** */ - name: UnitRentTypeEnum; + partnerTerms?: string; + + /** */ + publicUrl: string; + + /** */ + emailFromAddress: string; + + /** */ + rentalAssistanceDefault: string; + + /** */ + enablePartnerSettings?: boolean; + + /** */ + enableAccessibilityFeatures: boolean; + + /** */ + enableUtilitiesIncluded: boolean; } -export interface JurisdictionCreate { +export interface JurisdictionUpdate { + /** */ + id: string; + /** */ name: string; /** */ - notificationsSignUpUrl: string; + notificationsSignUpUrl?: string; /** */ - languages: string[]; + languages: LanguagesEnum[]; /** */ - partnerTerms: string; + partnerTerms?: string; /** */ publicUrl: string; @@ -2462,7 +3566,7 @@ export interface JurisdictionCreate { rentalAssistanceDefault: string; /** */ - enablePartnerSettings: boolean; + enablePartnerSettings?: boolean; /** */ enableAccessibilityFeatures: boolean; @@ -2471,21 +3575,30 @@ export interface JurisdictionCreate { enableUtilitiesIncluded: boolean; } -export interface JurisdictionUpdate { +export interface Jurisdiction { /** */ id: string; + /** */ + createdAt: Date; + + /** */ + updatedAt: Date; + /** */ name: string; /** */ - notificationsSignUpUrl: string; + notificationsSignUpUrl?: string; + + /** */ + languages: LanguagesEnum[]; /** */ - languages: string[]; + multiselectQuestions: IdDTO[]; /** */ - partnerTerms: string; + partnerTerms?: string; /** */ publicUrl: string; @@ -2497,7 +3610,7 @@ export interface JurisdictionUpdate { rentalAssistanceDefault: string; /** */ - enablePartnerSettings: boolean; + enablePartnerSettings?: boolean; /** */ enableAccessibilityFeatures: boolean; @@ -2511,28 +3624,28 @@ export interface MultiselectQuestionCreate { text: string; /** */ - untranslatedOptOutText: string; + untranslatedOptOutText?: string; /** */ - subText: string; + subText?: string; /** */ - description: string; + description?: string; /** */ - links: MultiselectLink[]; + links?: MultiselectLink[]; /** */ jurisdictions: IdDTO[]; /** */ - options: MultiselectOption[]; + options?: MultiselectOption[]; /** */ - optOutText: string; + optOutText?: string; /** */ - hideFromListing: boolean; + hideFromListing?: boolean; /** */ applicationSection: MultiselectQuestionsApplicationSectionEnum; @@ -2546,28 +3659,28 @@ export interface MultiselectQuestionUpdate { text: string; /** */ - untranslatedOptOutText: string; + untranslatedOptOutText?: string; /** */ - subText: string; + subText?: string; /** */ - description: string; + description?: string; /** */ - links: MultiselectLink[]; + links?: MultiselectLink[]; /** */ jurisdictions: IdDTO[]; /** */ - options: MultiselectOption[]; + options?: MultiselectOption[]; /** */ - optOutText: string; + optOutText?: string; /** */ - hideFromListing: boolean; + hideFromListing?: boolean; /** */ applicationSection: MultiselectQuestionsApplicationSectionEnum; @@ -2581,7 +3694,7 @@ export interface MultiselectQuestionFilterParams { jurisdiction?: string; /** */ - applicationSection?: string; + applicationSection?: MultiselectQuestionsApplicationSectionEnum; } export interface MultiselectQuestionQueryParams { @@ -2723,6 +3836,17 @@ export interface AlternateContact { address: Address; } +export interface ApplicationMultiselectQuestion { + /** */ + key: string; + + /** */ + claimed: boolean; + + /** */ + options: string[]; +} + export interface Application { /** */ id: string; @@ -2827,10 +3951,10 @@ export interface Application { householdMember: string[]; /** */ - preferences: string[]; + preferences?: ApplicationMultiselectQuestion[]; /** */ - programs: string[]; + programs?: ApplicationMultiselectQuestion[]; } export interface PaginatedApplication { @@ -3074,10 +4198,29 @@ export enum ListingViews { 'full' = 'full', 'details' = 'details', } -export enum EnumListingsQueryParamsOrderDir { + +export enum ListingOrderByKeys { + 'mostRecentlyUpdated' = 'mostRecentlyUpdated', + 'applicationDates' = 'applicationDates', + 'mostRecentlyClosed' = 'mostRecentlyClosed', + 'mostRecentlyPublished' = 'mostRecentlyPublished', + 'name' = 'name', + 'waitlistOpen' = 'waitlistOpen', + 'status' = 'status', + 'unitsAvailable' = 'unitsAvailable', + 'marketingType' = 'marketingType', +} + +export enum OrderByEnum { 'asc' = 'asc', 'desc' = 'desc', } + +export enum ListingStatusEnum { + 'active' = 'active', + 'pending' = 'pending', + 'closed' = 'closed', +} export enum EnumListingFilterParamsComparison { '=' = '=', '<>' = '<>', @@ -3086,11 +4229,6 @@ export enum EnumListingFilterParamsComparison { '<=' = '<=', 'NA' = 'NA', } -export enum EnumListingFilterParamsStatus { - 'active' = 'active', - 'pending' = 'pending', - 'closed' = 'closed', -} export enum ApplicationAddressTypeEnum { 'leasingAgent' = 'leasingAgent', } @@ -3122,6 +4260,14 @@ export enum ApplicationMethodsTypeEnum { 'Referral' = 'Referral', } +export enum LanguagesEnum { + 'en' = 'en', + 'es' = 'es', + 'vi' = 'vi', + 'zh' = 'zh', + 'tl' = 'tl', +} + export enum UnitTypeEnum { 'studio' = 'studio', 'oneBdrm' = 'oneBdrm', @@ -3132,6 +4278,11 @@ export enum UnitTypeEnum { 'fiveBdrm' = 'fiveBdrm', } +export enum UnitRentTypeEnum { + 'fixed' = 'fixed', + 'percentageOfIncome' = 'percentageOfIncome', +} + export enum UnitAccessibilityPriorityTypeEnum { 'mobility' = 'mobility', 'mobilityAndHearing' = 'mobilityAndHearing', @@ -3142,9 +4293,10 @@ export enum UnitAccessibilityPriorityTypeEnum { 'mobilityHearingAndVisual' = 'mobilityHearingAndVisual', } -export enum UnitRentTypeEnum { - 'fixed' = 'fixed', - 'percentageOfIncome' = 'percentageOfIncome', +export enum ListingEventsTypeEnum { + 'openHouse' = 'openHouse', + 'publicLottery' = 'publicLottery', + 'lotteryResults' = 'lotteryResults', } export enum EnumMultiselectQuestionFilterParamsComparison { '=' = '=', @@ -3161,11 +4313,6 @@ export enum ApplicationOrderByKeys { 'createdAt' = 'createdAt', } -export enum OrderByEnum { - 'asc' = 'asc', - 'desc' = 'desc', -} - export enum IncomePeriodEnum { 'perMonth' = 'perMonth', 'perYear' = 'perYear', @@ -3177,14 +4324,6 @@ export enum ApplicationStatusEnum { 'removed' = 'removed', } -export enum LanguagesEnum { - 'en' = 'en', - 'es' = 'es', - 'vi' = 'vi', - 'zh' = 'zh', - 'tl' = 'tl', -} - export enum ApplicationSubmissionTypeEnum { 'paper' = 'paper', 'electronical' = 'electronical', diff --git a/backend_new/yarn.lock b/backend_new/yarn.lock index 16be9cbeca..b333e45181 100644 --- a/backend_new/yarn.lock +++ b/backend_new/yarn.lock @@ -937,6 +937,11 @@ dependencies: lodash "^4.17.21" +"@nestjs/axios@^3.0.0": + version "3.0.0" + resolved "https://registry.yarnpkg.com/@nestjs/axios/-/axios-3.0.0.tgz#a2e70b118e3058f3d4b9c3deacd496ec4e3ee69e" + integrity sha512-ULdH03jDWkS5dy9X69XbUVbhC+0pVnrRcj7bIK/ytTZ76w7CgvTZDJqsIyisg3kNOiljRW/4NIjSf3j6YGvl+g== + "@nestjs/cli@^8.0.0": version "8.2.8" resolved "https://registry.yarnpkg.com/@nestjs/cli/-/cli-8.2.8.tgz#63e5b477f90e6d0238365dcc6236b95bf4f0c807" @@ -5182,7 +5187,7 @@ rxjs@6.6.7, rxjs@^6.6.0: dependencies: tslib "^1.9.0" -rxjs@^7.2.0: +rxjs@^7.2.0, rxjs@^7.8.1: version "7.8.1" resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-7.8.1.tgz#6f6f3d99ea8044291efd92e7c7fcf562c4057543" integrity sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg== diff --git a/package.json b/package.json index 2912f62da7..c7f308ba23 100644 --- a/package.json +++ b/package.json @@ -52,6 +52,7 @@ "version:all": "lerna version --yes --no-commit-hooks --ignore-scripts --conventional-graduate --include-merged-tags --force-git-tag", "test:backend:new": "cd backend_new && yarn test --detectOpenHandles", "test:backend:new:e2e": "cd backend_new && yarn jest --config ./test/jest-e2e.config.js --detectOpenHandles", + "test:backend:new:cov": "cd backend_new && yarn jest --config ./test/jest-with-coverage.config.js --detectOpenHandles", "test:backend:new:dbsetup": "cd backend_new && yarn db:migration:run", "backend:new:install": "cd backend_new && yarn install", "prettier": "prettier --write \"./**/*.{js,jsx,ts,tsx,json}\"" From 4d2748e7b0e68406267f5f3126e3bfc37881bb29 Mon Sep 17 00:00:00 2001 From: Yazeed Loonat Date: Thu, 31 Aug 2023 17:22:23 -0700 Subject: [PATCH 22/57] feat: custom exception filter (#3606) --- backend_new/.env.template | 1 + backend_new/src/main.ts | 12 ++++++++-- .../src/utilities/custom-exception-filter.ts | 24 +++++++++++++++++++ 3 files changed, 35 insertions(+), 2 deletions(-) create mode 100644 backend_new/src/utilities/custom-exception-filter.ts diff --git a/backend_new/.env.template b/backend_new/.env.template index 13460d533f..842f3ba3fa 100644 --- a/backend_new/.env.template +++ b/backend_new/.env.template @@ -6,3 +6,4 @@ GOOGLE_API_KEY= CLOUDINARY_SECRET= APP_SECRET= PROXY_URL= +NODE_ENV=development diff --git a/backend_new/src/main.ts b/backend_new/src/main.ts index 2f3d4c27a8..2f4409e7f5 100644 --- a/backend_new/src/main.ts +++ b/backend_new/src/main.ts @@ -1,9 +1,17 @@ -import { NestFactory } from '@nestjs/core'; +import { NestFactory, HttpAdapterHost } from '@nestjs/core'; import { SwaggerModule, DocumentBuilder } from '@nestjs/swagger'; import { AppModule } from './modules/app.module'; +import { CustomExceptionFilter } from './utilities/custom-exception-filter'; async function bootstrap() { - const app = await NestFactory.create(AppModule); + const app = await NestFactory.create(AppModule, { + logger: + process.env.NODE_ENV === 'development' + ? ['error', 'warn', 'log'] + : ['error', 'warn'], + }); + const { httpAdapter } = app.get(HttpAdapterHost); + app.useGlobalFilters(new CustomExceptionFilter(httpAdapter)); const config = new DocumentBuilder() .setTitle('Bloom API') .setDescription('The API for Bloom') diff --git a/backend_new/src/utilities/custom-exception-filter.ts b/backend_new/src/utilities/custom-exception-filter.ts new file mode 100644 index 0000000000..7a44e03515 --- /dev/null +++ b/backend_new/src/utilities/custom-exception-filter.ts @@ -0,0 +1,24 @@ +import { ArgumentsHost, Catch, Logger } from '@nestjs/common'; +import { AbstractHttpAdapter, BaseExceptionFilter } from '@nestjs/core'; + +/* + This creates a simple custom catch all exception filter for us + As for right now its just a pass through, but as we find more errors that we don't want the + front end to be exposed to this will grow +*/ +@Catch() +export class CustomExceptionFilter extends BaseExceptionFilter { + logger: Logger; + constructor(httpAdapter: AbstractHttpAdapter) { + super(httpAdapter); + this.logger = new Logger('Exception Filter'); + } + catch(exception: any, host: ArgumentsHost) { + this.logger.error({ + message: exception?.response?.message, + stack: exception.stack, + exception, + }); + super.catch(exception, host); + } +} From 5b198facd1f20a031f1d97cef26de1b9429c072c Mon Sep 17 00:00:00 2001 From: Yazeed Loonat Date: Mon, 18 Sep 2023 13:43:01 -0700 Subject: [PATCH 23/57] feat: prisma auth (#3617) --- backend_new/.env.template | 23 + backend_new/README.md | 14 + backend_new/package.json | 41 +- backend_new/prisma/seed-dev.ts | 1 + .../prisma/seed-helpers/user-factory.ts | 11 + backend_new/prisma/seed-staging.ts | 1 + .../src/controllers/auth.controller.ts | 114 +++ backend_new/src/dtos/auth/confirm.dto.ts | 22 + backend_new/src/dtos/auth/login.dto.ts | 30 + .../src/dtos/auth/update-password.dto.ts | 30 + .../dtos/mfa/request-mfa-code-response.dto.ts | 22 + .../src/dtos/mfa/request-mfa-code.dto.ts | 29 + backend_new/src/enums/mfa/mfa-type-enum.ts | 4 + backend_new/src/guards/jwt.guard.ts | 5 + backend_new/src/guards/mfa.guard.ts | 5 + backend_new/src/guards/optional.guard.ts | 11 + backend_new/src/main.ts | 2 + backend_new/src/modules/app.module.ts | 3 + backend_new/src/modules/auth.module.ts | 26 + backend_new/src/modules/sms-module.ts | 8 + backend_new/src/passports/jwt.strategy.ts | 89 ++ backend_new/src/passports/mfa.strategy.ts | 205 ++++ backend_new/src/services/auth.service.ts | 337 +++++++ backend_new/src/services/sms.service.ts | 29 + backend_new/src/services/user.service.ts | 11 +- backend_new/src/utilities/password-helpers.ts | 26 + backend_new/test/integration/auth.e2e-spec.ts | 319 ++++++ backend_new/test/integration/user.e2e-spec.ts | 12 +- backend_new/test/jest-with-coverage.config.js | 1 + .../test/unit/passports/jwt.strategy.spec.ts | 186 ++++ .../test/unit/passports/mfa.strategy.spec.ts | 640 ++++++++++++ .../test/unit/services/auth.service.spec.ts | 794 +++++++++++++++ .../test/unit/services/user.service.spec.ts | 12 +- backend_new/types/src/backend-swagger.ts | 212 ++++ backend_new/yarn.lock | 932 +++++++++++------- 35 files changed, 3847 insertions(+), 360 deletions(-) create mode 100644 backend_new/src/controllers/auth.controller.ts create mode 100644 backend_new/src/dtos/auth/confirm.dto.ts create mode 100644 backend_new/src/dtos/auth/login.dto.ts create mode 100644 backend_new/src/dtos/auth/update-password.dto.ts create mode 100644 backend_new/src/dtos/mfa/request-mfa-code-response.dto.ts create mode 100644 backend_new/src/dtos/mfa/request-mfa-code.dto.ts create mode 100644 backend_new/src/enums/mfa/mfa-type-enum.ts create mode 100644 backend_new/src/guards/jwt.guard.ts create mode 100644 backend_new/src/guards/mfa.guard.ts create mode 100644 backend_new/src/guards/optional.guard.ts create mode 100644 backend_new/src/modules/auth.module.ts create mode 100644 backend_new/src/modules/sms-module.ts create mode 100644 backend_new/src/passports/jwt.strategy.ts create mode 100644 backend_new/src/passports/mfa.strategy.ts create mode 100644 backend_new/src/services/auth.service.ts create mode 100644 backend_new/src/services/sms.service.ts create mode 100644 backend_new/test/integration/auth.e2e-spec.ts create mode 100644 backend_new/test/unit/passports/jwt.strategy.spec.ts create mode 100644 backend_new/test/unit/passports/mfa.strategy.spec.ts create mode 100644 backend_new/test/unit/services/auth.service.spec.ts diff --git a/backend_new/.env.template b/backend_new/.env.template index 842f3ba3fa..3bc2df2f66 100644 --- a/backend_new/.env.template +++ b/backend_new/.env.template @@ -1,9 +1,32 @@ +# url of the db we are trying to connect to DATABASE_URL="postgres://@localhost:5432/bloom_prisma" +# port from which the api is accessible PORT=3101 +# google translate api email GOOGLE_API_EMAIL= +# google translate api id GOOGLE_API_ID= +# google translate api key GOOGLE_API_KEY= +# cloudinary secret CLOUDINARY_SECRET= +# app secret APP_SECRET= +# url for the proxy PROXY_URL= +# the node env the app should be running as NODE_ENV=development +# how long a generated multi-factor authentication code should be +MFA_CODE_LENGTH=5 +# TTL for the mfa code, stored in milliseconds +MFA_CODE_VALID=60000 +# how long logins are locked after too many failed login attempts in milliseconds +AUTH_LOCK_LOGIN_COOLDOWN=1800000 +# how many failed login attempts before a lock occurs +AUTH_LOCK_LOGIN_AFTER_FAILED_ATTEMPTS=5 +# phone number for twilio account +TWILIO_PHONE_NUMBER= +# account sid for twilio +TWILIO_ACCOUNT_SID= +# account auth token for twilio +TWILIO_AUTH_TOKEN= diff --git a/backend_new/README.md b/backend_new/README.md index 6c3d273b29..0fa220f92a 100644 --- a/backend_new/README.md +++ b/backend_new/README.md @@ -85,6 +85,20 @@ Services are housed under `src/services` and are given the extension `.services. The exported class should be in capitalized camelcase (e.g. `ListingService`). +# Guards & Passport Strategies +We currently use guards for 2 purposes. Passport guards and permissioning guards. + +Passport guards (jwt.guard.ts, mfa.guard.ts, and optional.guard.ts) verify that the request is from a legitimate user. JwtAuthGuard does this by verifying the incoming jwt token (off the request's cookies) matches a user. MfaAuthGuard does this by verifying the incoming log in information (email, password, mfaCode) matches a user's information. OptionalAuthGuard is used to allow requests from users not logged in through. It will still verify the user through the JwtAuthGuard if a user was logged in. + +Passport guards are paired with a passport strategy (jwt.strategy.ts, and mfa.strategy.ts), this is where the code to actually verify the requester lives. + +Hopefully that makes sense, if not think of guards as customs agents, and the passport strategy is what the guards look for in a request to allow entry to a requester. Allowing them access the endpoint that the guard protects. + +[NestJS passport docs](https://docs.nestjs.com/recipes/passport) +[NestJS guards docs](https://docs.nestjs.com/guards) + +TODO: add to this document for permissioning guards and strategies [github issue](https://github.com/bloom-housing/bloom/issues/3445) + # Testing There are 2 different kinds of tests that the backend supports: Integration tests and Unit tests. diff --git a/backend_new/package.json b/backend_new/package.json index 24bbb6e9e5..81d1c7d203 100644 --- a/backend_new/package.json +++ b/backend_new/package.json @@ -30,44 +30,51 @@ }, "dependencies": { "@google-cloud/translate": "^7.2.1", - "@nestjs/axios": "^3.0.0", + "@nestjs/axios": "~3.0.0", "@nestjs/common": "^8.0.0", "@nestjs/core": "^8.0.0", + "@nestjs/jwt": "~10.1.0", + "@nestjs/passport": "~10.0.1", "@nestjs/platform-express": "^8.0.0", "@nestjs/swagger": "^6.3.0", "@prisma/client": "^5.0.0", - "class-transformer": "^0.5.1", - "class-validator": "^0.14.0", + "class-transformer": "~0.5.1", + "class-validator": "~0.14.0", "cloudinary": "^1.37.3", - "dayjs": "^1.11.9", - "jsonwebtoken": "^9.0.1", - "lodash": "^4.17.21", + "cookie-parser": "~1.4.6", + "dayjs": "~1.11.9", + "jsonwebtoken": "~9.0.1", + "lodash": "~4.17.21", + "passport": "~0.6.0", + "passport-jwt": "~4.0.1", + "passport-local": "~1.0.0", "prisma": "^5.0.0", - "qs": "^6.11.2", - "reflect-metadata": "^0.1.13", + "qs": "~6.11.2", + "reflect-metadata": "~0.1.13", "rimraf": "^3.0.2", - "rxjs": "^7.8.1", - "swagger-axios-codegen": "^0.15.11" + "rxjs": "~7.8.1", + "swagger-axios-codegen": "~0.15.11", + "twilio": "^4.15.0" }, "devDependencies": { "@nestjs/cli": "^8.0.0", "@nestjs/schematics": "^8.0.0", "@nestjs/testing": "^8.0.0", - "@types/express": "^4.17.17", - "@types/jest": "^29.5.3", + "@types/express": "~4.17.17", + "@types/jest": "~29.5.3", "@types/node": "^18.7.14", - "@types/supertest": "^2.0.11", + "@types/supertest": "~2.0.11", "@typescript-eslint/eslint-plugin": "^5.0.0", "@typescript-eslint/parser": "^5.0.0", "eslint": "^8.0.1", "eslint-config-prettier": "^8.3.0", "eslint-plugin-prettier": "^4.0.0", - "jest": "^29.6.2", - "jest-environment-jsdom": "^29.6.2", + "jest": "~29.6.2", + "jest-environment-jsdom": "~29.6.2", "prettier": "^2.3.2", - "source-map-support": "^0.5.20", + "source-map-support": "~0.5.20", "supertest": "^6.1.3", - "ts-jest": "^29.1.1", + "ts-jest": "~29.1.1", "ts-loader": "^9.2.3", "ts-node": "^10.0.0", "tsconfig-paths": "^3.10.1", diff --git a/backend_new/prisma/seed-dev.ts b/backend_new/prisma/seed-dev.ts index 8c68245e8c..8ed4256204 100644 --- a/backend_new/prisma/seed-dev.ts +++ b/backend_new/prisma/seed-dev.ts @@ -42,6 +42,7 @@ export const devSeeding = async (prismaClient: PrismaClient) => { data: await userFactory({ roles: { isAdmin: true }, email: 'admin@example.com', + confirmedAt: new Date(), }), }); const jurisdiction = await prismaClient.jurisdictions.create({ diff --git a/backend_new/prisma/seed-helpers/user-factory.ts b/backend_new/prisma/seed-helpers/user-factory.ts index e6f77883e1..3bfdd7bbde 100644 --- a/backend_new/prisma/seed-helpers/user-factory.ts +++ b/backend_new/prisma/seed-helpers/user-factory.ts @@ -7,6 +7,11 @@ export const userFactory = async (optionalParams?: { firstName?: string; lastName?: string; email?: string; + mfaCode?: string; + mfaEnabled?: boolean; + confirmedAt?: Date; + phoneNumber?: string; + phoneNumberVerified?: boolean; }): Promise => ({ email: optionalParams?.email?.toLocaleLowerCase() || @@ -22,4 +27,10 @@ export const userFactory = async (optionalParams?: { isPartner: optionalParams?.roles?.isAdmin || false, }, }, + mfaCode: optionalParams?.mfaCode || null, + mfaEnabled: optionalParams?.mfaEnabled || false, + confirmedAt: optionalParams?.confirmedAt || null, + mfaCodeUpdatedAt: optionalParams?.mfaEnabled ? new Date() : undefined, + phoneNumber: optionalParams?.phoneNumber || null, + phoneNumberVerified: optionalParams?.phoneNumberVerified || null, }); diff --git a/backend_new/prisma/seed-staging.ts b/backend_new/prisma/seed-staging.ts index 91adee461c..3457f2c693 100644 --- a/backend_new/prisma/seed-staging.ts +++ b/backend_new/prisma/seed-staging.ts @@ -31,6 +31,7 @@ export const stagingSeed = async ( data: await userFactory({ roles: { isAdmin: true }, email: 'admin@example.com', + confirmedAt: new Date(), }), }); // create single jurisdiction diff --git a/backend_new/src/controllers/auth.controller.ts b/backend_new/src/controllers/auth.controller.ts new file mode 100644 index 0000000000..eafea40c1d --- /dev/null +++ b/backend_new/src/controllers/auth.controller.ts @@ -0,0 +1,114 @@ +import { + Controller, + Get, + Request, + Response, + Post, + UsePipes, + ValidationPipe, + Body, + BadRequestException, + Put, + UseGuards, +} from '@nestjs/common'; +import { ApiBody, ApiOkResponse, ApiOperation, ApiTags } from '@nestjs/swagger'; +import { + Response as ExpressResponse, + Request as ExpressRequest, +} from 'express'; +import { defaultValidationPipeOptions } from '../utilities/default-validation-pipe-options'; +import { SuccessDTO } from '../dtos/shared/success.dto'; +import { AuthService, REFRESH_COOKIE_NAME } from '../services/auth.service'; +import { RequestMfaCode } from '../dtos/mfa/request-mfa-code.dto'; +import { RequestMfaCodeResponse } from '../dtos/mfa/request-mfa-code-response.dto'; +import { Confirm } from '../dtos/auth/confirm.dto'; +import { UpdatePassword } from '../dtos/auth/update-password.dto'; +import { MfaAuthGuard } from '../guards/mfa.guard'; +import { JwtAuthGuard } from '../guards/jwt.guard'; +import { OptionalAuthGuard } from '../guards/optional.guard'; +import { Login } from '../dtos/auth/login.dto'; +import { mapTo } from '../utilities/mapTo'; +import { User } from '../dtos/users/user.dto'; + +@Controller('auth') +@ApiTags('auth') +@UsePipes(new ValidationPipe(defaultValidationPipeOptions)) +export class AuthController { + constructor(private readonly authService: AuthService) {} + + @Post('login') + @ApiOperation({ summary: 'Login', operationId: 'login' }) + @ApiOkResponse({ type: SuccessDTO }) + @ApiBody({ type: Login }) + @UseGuards(MfaAuthGuard) + async login( + @Request() req: ExpressRequest, + @Response({ passthrough: true }) res: ExpressResponse, + ): Promise { + return await this.authService.setCredentials(res, mapTo(User, req['user'])); + } + + @Get('logout') + @ApiOperation({ summary: 'Logout', operationId: 'logout' }) + @ApiOkResponse({ type: SuccessDTO }) + @UseGuards(JwtAuthGuard) + async logout( + @Request() req: ExpressRequest, + @Response({ passthrough: true }) res: ExpressResponse, + ): Promise { + return await this.authService.clearCredentials( + res, + mapTo(User, req['user']), + ); + } + + @Post('request-mfa-code') + @ApiOperation({ summary: 'Request mfa code', operationId: 'requestMfaCode' }) + @ApiOkResponse({ type: RequestMfaCodeResponse }) + async requestMfaCode( + @Body() dto: RequestMfaCode, + ): Promise { + return await this.authService.requestMfaCode(dto); + } + + @Get('requestNewToken') + @ApiOperation({ + summary: 'Requests a new token given a refresh token', + operationId: 'requestNewToken', + }) + @ApiOkResponse({ type: SuccessDTO }) + @UseGuards(OptionalAuthGuard) + async requestNewToken( + @Request() req: ExpressRequest, + @Response({ passthrough: true }) res: ExpressResponse, + ): Promise { + if (!req?.cookies[REFRESH_COOKIE_NAME]) { + throw new BadRequestException('No refresh token sent with request'); + } + return await this.authService.setCredentials( + res, + mapTo(User, req['user']), + req.cookies[REFRESH_COOKIE_NAME], + ); + } + + @Put('update-password') + @ApiOperation({ summary: 'Update Password', operationId: 'update-password' }) + @ApiOkResponse({ type: SuccessDTO }) + async updatePassword( + @Body() dto: UpdatePassword, + @Response({ passthrough: true }) res: ExpressResponse, + ): Promise { + return await this.authService.updatePassword(dto, res); + } + + @Put('confirm') + @ApiOperation({ summary: 'Confirm email', operationId: 'confirm' }) + @ApiOkResponse({ type: SuccessDTO }) + async confirm( + @Body() dto: Confirm, + @Response({ passthrough: true }) res: ExpressResponse, + ): Promise { + return await this.authService.confirmUser(dto, res); + } +} diff --git a/backend_new/src/dtos/auth/confirm.dto.ts b/backend_new/src/dtos/auth/confirm.dto.ts new file mode 100644 index 0000000000..bd35d87da0 --- /dev/null +++ b/backend_new/src/dtos/auth/confirm.dto.ts @@ -0,0 +1,22 @@ +import { IsString, Matches, MaxLength } from 'class-validator'; +import { Expose } from 'class-transformer'; +import { ValidationsGroupsEnum } from '../../enums/shared/validation-groups-enum'; +import { passwordRegex } from '../../utilities/password-regex'; +import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; + +export class Confirm { + @Expose() + @ApiProperty() + @IsString({ groups: [ValidationsGroupsEnum.default] }) + @MaxLength(256, { groups: [ValidationsGroupsEnum.default] }) + token: string; + + @Expose() + @ApiPropertyOptional() + @IsString({ groups: [ValidationsGroupsEnum.default] }) + @Matches(passwordRegex, { + message: 'passwordTooWeak', + groups: [ValidationsGroupsEnum.default], + }) + password?: string; +} diff --git a/backend_new/src/dtos/auth/login.dto.ts b/backend_new/src/dtos/auth/login.dto.ts new file mode 100644 index 0000000000..7765e91614 --- /dev/null +++ b/backend_new/src/dtos/auth/login.dto.ts @@ -0,0 +1,30 @@ +import { IsEmail, IsString, MaxLength, IsEnum } from 'class-validator'; +import { Expose } from 'class-transformer'; +import { EnforceLowerCase } from '../../decorators/enforce-lower-case.decorator'; +import { ValidationsGroupsEnum } from '../../enums/shared/validation-groups-enum'; +import { MfaType } from '../../enums/mfa/mfa-type-enum'; +import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; + +export class Login { + @Expose() + @IsEmail({}, { groups: [ValidationsGroupsEnum.default] }) + @EnforceLowerCase() + @ApiProperty() + email: string; + + @Expose() + @IsString({ groups: [ValidationsGroupsEnum.default] }) + @ApiProperty() + password: string; + + @Expose() + @IsString({ groups: [ValidationsGroupsEnum.default] }) + @MaxLength(16, { groups: [ValidationsGroupsEnum.default] }) + @ApiPropertyOptional() + mfaCode?: string; + + @Expose() + @IsEnum(MfaType, { groups: [ValidationsGroupsEnum.default] }) + @ApiPropertyOptional({ enum: MfaType, enumName: 'MfaType' }) + mfaType?: MfaType; +} diff --git a/backend_new/src/dtos/auth/update-password.dto.ts b/backend_new/src/dtos/auth/update-password.dto.ts new file mode 100644 index 0000000000..20a52ca19f --- /dev/null +++ b/backend_new/src/dtos/auth/update-password.dto.ts @@ -0,0 +1,30 @@ +import { IsString, Matches, MaxLength } from 'class-validator'; +import { Expose } from 'class-transformer'; +import { ValidationsGroupsEnum } from '../../enums/shared/validation-groups-enum'; +import { passwordRegex } from '../../utilities/password-regex'; +import { Match } from '../../decorators/match-decorator'; +import { ApiProperty } from '@nestjs/swagger'; + +export class UpdatePassword { + @Expose() + @ApiProperty() + @IsString({ groups: [ValidationsGroupsEnum.default] }) + @Matches(passwordRegex, { + message: 'passwordTooWeak', + groups: [ValidationsGroupsEnum.default], + }) + password: string; + + @Expose() + @ApiProperty() + @IsString({ groups: [ValidationsGroupsEnum.default] }) + @MaxLength(64, { groups: [ValidationsGroupsEnum.default] }) + @Match('password') + passwordConfirmation: string; + + @Expose() + @ApiProperty() + @IsString({ groups: [ValidationsGroupsEnum.default] }) + @MaxLength(256, { groups: [ValidationsGroupsEnum.default] }) + token: string; +} diff --git a/backend_new/src/dtos/mfa/request-mfa-code-response.dto.ts b/backend_new/src/dtos/mfa/request-mfa-code-response.dto.ts new file mode 100644 index 0000000000..037b61c656 --- /dev/null +++ b/backend_new/src/dtos/mfa/request-mfa-code-response.dto.ts @@ -0,0 +1,22 @@ +import { ApiPropertyOptional } from '@nestjs/swagger'; +import { Expose } from 'class-transformer'; +import { IsEmail, IsPhoneNumber } from 'class-validator'; +import { EnforceLowerCase } from '../../decorators/enforce-lower-case.decorator'; +import { ValidationsGroupsEnum } from '../../enums/shared/validation-groups-enum'; + +export class RequestMfaCodeResponse { + @Expose() + @IsPhoneNumber('US', { groups: [ValidationsGroupsEnum.default] }) + @ApiPropertyOptional() + phoneNumber?: string; + + @Expose() + @IsEmail({}, { groups: [ValidationsGroupsEnum.default] }) + @EnforceLowerCase() + @ApiPropertyOptional() + email?: string; + + @Expose() + @ApiPropertyOptional() + phoneNumberVerified?: boolean; +} diff --git a/backend_new/src/dtos/mfa/request-mfa-code.dto.ts b/backend_new/src/dtos/mfa/request-mfa-code.dto.ts new file mode 100644 index 0000000000..21fea22e4a --- /dev/null +++ b/backend_new/src/dtos/mfa/request-mfa-code.dto.ts @@ -0,0 +1,29 @@ +import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; +import { Expose } from 'class-transformer'; +import { IsString, IsEmail, IsPhoneNumber, IsEnum } from 'class-validator'; +import { MfaType } from '../../enums/mfa/mfa-type-enum'; +import { EnforceLowerCase } from '../../decorators/enforce-lower-case.decorator'; +import { ValidationsGroupsEnum } from '../../enums/shared/validation-groups-enum'; + +export class RequestMfaCode { + @Expose() + @IsEmail({}, { groups: [ValidationsGroupsEnum.default] }) + @EnforceLowerCase() + @ApiProperty() + email: string; + + @Expose() + @IsString({ groups: [ValidationsGroupsEnum.default] }) + @ApiProperty() + password: string; + + @Expose() + @IsEnum(MfaType, { groups: [ValidationsGroupsEnum.default] }) + @ApiProperty({ enum: MfaType, enumName: 'MfaType' }) + mfaType: MfaType; + + @Expose() + @IsPhoneNumber('US', { groups: [ValidationsGroupsEnum.default] }) + @ApiPropertyOptional() + phoneNumber?: string; +} diff --git a/backend_new/src/enums/mfa/mfa-type-enum.ts b/backend_new/src/enums/mfa/mfa-type-enum.ts new file mode 100644 index 0000000000..4b70dda762 --- /dev/null +++ b/backend_new/src/enums/mfa/mfa-type-enum.ts @@ -0,0 +1,4 @@ +export enum MfaType { + sms = 'sms', + email = 'email', +} diff --git a/backend_new/src/guards/jwt.guard.ts b/backend_new/src/guards/jwt.guard.ts new file mode 100644 index 0000000000..2155290ede --- /dev/null +++ b/backend_new/src/guards/jwt.guard.ts @@ -0,0 +1,5 @@ +import { Injectable } from '@nestjs/common'; +import { AuthGuard } from '@nestjs/passport'; + +@Injectable() +export class JwtAuthGuard extends AuthGuard('jwt') {} diff --git a/backend_new/src/guards/mfa.guard.ts b/backend_new/src/guards/mfa.guard.ts new file mode 100644 index 0000000000..4c3ebae5e3 --- /dev/null +++ b/backend_new/src/guards/mfa.guard.ts @@ -0,0 +1,5 @@ +import { Injectable } from '@nestjs/common'; +import { AuthGuard } from '@nestjs/passport'; + +@Injectable() +export class MfaAuthGuard extends AuthGuard('mfa') {} diff --git a/backend_new/src/guards/optional.guard.ts b/backend_new/src/guards/optional.guard.ts new file mode 100644 index 0000000000..fc5d7ad79a --- /dev/null +++ b/backend_new/src/guards/optional.guard.ts @@ -0,0 +1,11 @@ +import { JwtAuthGuard } from './jwt.guard'; +import { Injectable } from '@nestjs/common'; + +@Injectable() +export class OptionalAuthGuard extends JwtAuthGuard { + handleRequest(err, user: User) { + // user is boolean false when not logged in + // return undefined instead + return user || undefined; + } +} diff --git a/backend_new/src/main.ts b/backend_new/src/main.ts index 2f4409e7f5..ae8ae1fce8 100644 --- a/backend_new/src/main.ts +++ b/backend_new/src/main.ts @@ -1,5 +1,6 @@ import { NestFactory, HttpAdapterHost } from '@nestjs/core'; import { SwaggerModule, DocumentBuilder } from '@nestjs/swagger'; +import cookieParser from 'cookie-parser'; import { AppModule } from './modules/app.module'; import { CustomExceptionFilter } from './utilities/custom-exception-filter'; @@ -12,6 +13,7 @@ async function bootstrap() { }); const { httpAdapter } = app.get(HttpAdapterHost); app.useGlobalFilters(new CustomExceptionFilter(httpAdapter)); + app.use(cookieParser()); const config = new DocumentBuilder() .setTitle('Bloom API') .setDescription('The API for Bloom') diff --git a/backend_new/src/modules/app.module.ts b/backend_new/src/modules/app.module.ts index 559842f519..acbd9f1534 100644 --- a/backend_new/src/modules/app.module.ts +++ b/backend_new/src/modules/app.module.ts @@ -13,6 +13,7 @@ import { MultiselectQuestionModule } from './multiselect-question.module'; import { ApplicationModule } from './application.module'; import { AssetModule } from './asset.module'; import { UserModule } from './user.module'; +import { AuthModule } from './auth.module'; @Module({ imports: [ @@ -28,6 +29,7 @@ import { UserModule } from './user.module'; AssetModule, UserModule, PrismaModule, + AuthModule, ], controllers: [AppController], providers: [AppService], @@ -44,6 +46,7 @@ import { UserModule } from './user.module'; AssetModule, UserModule, PrismaModule, + AuthModule, ], }) export class AppModule {} diff --git a/backend_new/src/modules/auth.module.ts b/backend_new/src/modules/auth.module.ts new file mode 100644 index 0000000000..e3cccff6e8 --- /dev/null +++ b/backend_new/src/modules/auth.module.ts @@ -0,0 +1,26 @@ +import { Module } from '@nestjs/common'; +import { PassportModule } from '@nestjs/passport'; +import { JwtModule } from '@nestjs/jwt'; +import { AuthController } from '../controllers/auth.controller'; +import { AuthService } from '../services/auth.service'; +import { PrismaModule } from './prisma.module'; +import { SmsModule } from './sms-module'; +import { UserModule } from './user.module'; +import { MfaStrategy } from '../passports/mfa.strategy'; +import { JwtStrategy } from '../passports/jwt.strategy'; + +@Module({ + imports: [ + PrismaModule, + UserModule, + SmsModule, + PassportModule, + JwtModule.register({ + secret: process.env.APP_SECRET, + }), + ], + controllers: [AuthController], + providers: [AuthService, MfaStrategy, JwtStrategy], + exports: [AuthService], +}) +export class AuthModule {} diff --git a/backend_new/src/modules/sms-module.ts b/backend_new/src/modules/sms-module.ts new file mode 100644 index 0000000000..60412a00d4 --- /dev/null +++ b/backend_new/src/modules/sms-module.ts @@ -0,0 +1,8 @@ +import { Module } from '@nestjs/common'; +import { SmsService } from '../services/sms.service'; + +@Module({ + providers: [SmsService], + exports: [SmsService], +}) +export class SmsModule {} diff --git a/backend_new/src/passports/jwt.strategy.ts b/backend_new/src/passports/jwt.strategy.ts new file mode 100644 index 0000000000..5e210c5ed5 --- /dev/null +++ b/backend_new/src/passports/jwt.strategy.ts @@ -0,0 +1,89 @@ +import { Strategy } from 'passport-jwt'; +import { Request } from 'express'; +import { PassportStrategy } from '@nestjs/passport'; +import { Injectable, UnauthorizedException } from '@nestjs/common'; +import { User } from '../dtos/users/user.dto'; +import { TOKEN_COOKIE_NAME } from '../services/auth.service'; +import { PrismaService } from '../services/prisma.service'; +import { mapTo } from '../utilities/mapTo'; +import { isPasswordOutdated } from '../utilities/password-helpers'; + +type PayloadType = { + sub: string; +}; + +@Injectable() +export class JwtStrategy extends PassportStrategy(Strategy, 'jwt') { + constructor(private prisma: PrismaService) { + super({ + jwtFromRequest: JwtStrategy.extractJwt, + passReqToCallback: true, + ignoreExpiration: false, + secretOrKey: process.env.APP_SECRET, + }); + } + + /* + verifies that the incoming jwt token is valid + returns the verified user + */ + async validate(req: Request, payload: PayloadType): Promise { + const rawToken = JwtStrategy.extractJwt(req); + const userId = payload.sub; + + const rawUser = await this.prisma.userAccounts.findFirst({ + include: { + listings: true, + userRoles: true, + jurisdictions: true, + }, + where: { + id: userId, + }, + }); + + if (!rawUser) { + // if there is no user matching the incoming id + throw new UnauthorizedException(`user ${userId} does not exist`); + } + if ( + isPasswordOutdated( + rawUser.passwordValidForDays, + rawUser.passwordUpdatedAt, + ) + ) { + // if we have a user and the user's password is outdated + throw new UnauthorizedException( + `user ${userId} attempted to log in, but password is outdated`, + ); + } else if (rawUser.activeAccessToken !== rawToken) { + // if the incoming token is not the active token for the user, clear the user's tokens + await this.prisma.userAccounts.update({ + data: { + activeAccessToken: null, + activeRefreshToken: null, + }, + where: { + id: userId, + }, + }); + throw new UnauthorizedException(` + user ${userId} attempted to log in, but token ${rawToken} didn't match their stored token ${rawUser.activeAccessToken} + `); + } + + const user = mapTo(User, rawUser); + return user; + } + + /* + grabs the token out the request's cookies + */ + static extractJwt(req: Request): string | null { + if (req.cookies?.[TOKEN_COOKIE_NAME]) { + return req.cookies[TOKEN_COOKIE_NAME]; + } + + return null; + } +} diff --git a/backend_new/src/passports/mfa.strategy.ts b/backend_new/src/passports/mfa.strategy.ts new file mode 100644 index 0000000000..661a61efcb --- /dev/null +++ b/backend_new/src/passports/mfa.strategy.ts @@ -0,0 +1,205 @@ +import { Strategy } from 'passport-local'; +import { Request } from 'express'; +import { PassportStrategy } from '@nestjs/passport'; +import { + HttpException, + HttpStatus, + Injectable, + UnauthorizedException, + ValidationPipe, +} from '@nestjs/common'; +import { User } from '../dtos/users/user.dto'; +import { PrismaService } from '../services/prisma.service'; +import { mapTo } from '../utilities/mapTo'; +import { + isPasswordOutdated, + isPasswordValid, +} from '../utilities/password-helpers'; +import { defaultValidationPipeOptions } from '../utilities/default-validation-pipe-options'; +import { Login } from '../dtos/auth/login.dto'; +import { MfaType } from '../enums/mfa/mfa-type-enum'; + +@Injectable() +export class MfaStrategy extends PassportStrategy(Strategy, 'mfa') { + constructor(private prisma: PrismaService) { + super({ + usernameField: 'email', + passReqToCallback: true, + }); + } + + /* + verifies that the incoming log in information is valid + returns the verified user + */ + async validate(req: Request): Promise { + const validationPipe = new ValidationPipe(defaultValidationPipeOptions); + const dto: Login = await validationPipe.transform(req.body, { + type: 'body', + metatype: Login, + }); + + const rawUser = await this.prisma.userAccounts.findFirst({ + include: { + userRoles: true, + listings: true, + jurisdictions: true, + }, + where: { + email: dto.email, + }, + }); + if (!rawUser) { + throw new UnauthorizedException( + `user ${dto.email} attempted to log in, but does not exist`, + ); + } else if ( + rawUser.lastLoginAt && + rawUser.failedLoginAttemptsCount >= + Number(process.env.AUTH_LOCK_LOGIN_AFTER_FAILED_ATTEMPTS) + ) { + // if a user has logged in, but has since gone over their max failed login attempts + const retryAfter = new Date( + rawUser.lastLoginAt.getTime() + + Number(process.env.AUTH_LOCK_LOGIN_COOLDOWN), + ); + if (retryAfter <= new Date()) { + // if we have passed the login lock TTL, reset login lock countdown + rawUser.failedLoginAttemptsCount = 0; + } else { + // if the login lock is still a valid lock, error + throw new HttpException( + { + statusCode: HttpStatus.TOO_MANY_REQUESTS, + error: 'Too Many Requests', + message: 'Failed login attempts exceeded.', + retryAfter, + }, + 429, + ); + } + } else if (!rawUser.confirmedAt) { + // if user is not confirmed already + throw new UnauthorizedException( + `user ${rawUser.id} attempted to login, but is not confirmed`, + ); + } else if ( + isPasswordOutdated( + rawUser.passwordValidForDays, + rawUser.passwordUpdatedAt, + ) + ) { + // if password TTL is expired + throw new UnauthorizedException( + `user ${rawUser.id} attempted to login, but password is no longer valid`, + ); + } else if (!(await isPasswordValid(rawUser.passwordHash, dto.password))) { + // if incoming password does not match + await this.updateFailedLoginCount( + rawUser.failedLoginAttemptsCount + 1, + rawUser.id, + ); + throw new UnauthorizedException({ + failureCountRemaining: + Number(process.env.AUTH_LOCK_LOGIN_AFTER_FAILED_ATTEMPTS) + + 1 - + rawUser.failedLoginAttemptsCount, + }); + } + + if (!rawUser.mfaEnabled) { + // if user is not an mfaEnabled user + await this.updateStoredUser(null, null, null, 0, rawUser.id); + return mapTo(User, rawUser); + } + + let authSuccess = true; + if (!dto.mfaCode || !rawUser.mfaCode || !rawUser.mfaCodeUpdatedAt) { + // if an mfaCode was not sent, and an mfaCode wasn't stored in the db for the user + // signal to the front end to request an mfa code + await this.updateFailedLoginCount(0, rawUser.id); + throw new UnauthorizedException({ + name: 'mfaCodeIsMissing', + }); + } else if ( + new Date( + rawUser.mfaCodeUpdatedAt.getTime() + Number(process.env.MFA_CODE_VALID), + ) < new Date() || + rawUser.mfaCode !== dto.mfaCode + ) { + // if mfaCode TTL has expired, or if the mfa code input was incorrect + authSuccess = false; + } else { + // if mfaCode login was a success + rawUser.mfaCode = null; + rawUser.mfaCodeUpdatedAt = new Date(); + } + + if (!authSuccess) { + // if we failed login validation + rawUser.failedLoginAttemptsCount += 1; + await this.updateStoredUser( + rawUser.mfaCode, + rawUser.mfaCodeUpdatedAt, + rawUser.phoneNumberVerified, + rawUser.failedLoginAttemptsCount, + rawUser.id, + ); + throw new UnauthorizedException({ + message: 'mfaUnauthorized', + failureCountRemaining: + Number(process.env.AUTH_LOCK_LOGIN_AFTER_FAILED_ATTEMPTS) + + 1 - + rawUser.failedLoginAttemptsCount, + }); + } + // if the password and mfa code was valid + rawUser.failedLoginAttemptsCount = 0; + if (!rawUser.phoneNumberVerified && dto.mfaType === MfaType.sms) { + // if the phone number was not verfied, but this mfa login was done through sms + // then we should consider the phone number verified + rawUser.phoneNumberVerified = true; + } + + await this.updateStoredUser( + rawUser.mfaCode, + rawUser.mfaCodeUpdatedAt, + rawUser.phoneNumberVerified, + rawUser.failedLoginAttemptsCount, + rawUser.id, + ); + return mapTo(User, rawUser); + } + + async updateFailedLoginCount(count: number, userId: string): Promise { + await this.prisma.userAccounts.update({ + data: { + failedLoginAttemptsCount: count, + }, + where: { + id: userId, + }, + }); + } + + async updateStoredUser( + mfaCode: string, + mfaCodeUpdatedAt: Date, + phoneNumberVerified: boolean, + failedLoginAttemptsCount: number, + userId: string, + ): Promise { + await this.prisma.userAccounts.update({ + data: { + mfaCode, + mfaCodeUpdatedAt, + phoneNumberVerified, + failedLoginAttemptsCount, + lastLoginAt: new Date(), + }, + where: { + id: userId, + }, + }); + } +} diff --git a/backend_new/src/services/auth.service.ts b/backend_new/src/services/auth.service.ts new file mode 100644 index 0000000000..b1a1905a49 --- /dev/null +++ b/backend_new/src/services/auth.service.ts @@ -0,0 +1,337 @@ +import { + BadRequestException, + Injectable, + NotFoundException, + UnauthorizedException, +} from '@nestjs/common'; +import { Response } from 'express'; +import { CookieOptions } from 'express'; +import { sign, verify } from 'jsonwebtoken'; +import { randomInt } from 'crypto'; +import { Prisma } from '@prisma/client'; +import { UpdatePassword } from '../dtos/auth/update-password.dto'; +import { MfaType } from '../enums/mfa/mfa-type-enum'; +import { isPasswordValid, passwordToHash } from '../utilities/password-helpers'; +import { RequestMfaCodeResponse } from '../dtos/mfa/request-mfa-code-response.dto'; +import { RequestMfaCode } from '../dtos/mfa/request-mfa-code.dto'; +import { SuccessDTO } from '../dtos/shared/success.dto'; +import { User } from '../dtos/users/user.dto'; +import { PrismaService } from './prisma.service'; +import { UserService } from './user.service'; +import { IdDTO } from '../dtos/shared/id.dto'; +import { mapTo } from '../utilities/mapTo'; +import { Confirm } from '../dtos/auth/confirm.dto'; +import { SmsService } from './sms.service'; + +// since our local env doesn't have an https cert we can't be secure. Hosted envs should be secure +const secure = process.env.NODE_ENV !== 'development'; +const sameSite = process.env.NODE_ENV === 'development' ? 'strict' : 'none'; + +const TOKEN_COOKIE_MAXAGE = 86400000; // 24 hours +export const TOKEN_COOKIE_NAME = 'access-token'; +export const REFRESH_COOKIE_NAME = 'refresh-token'; +export const ACCESS_TOKEN_AVAILABLE_NAME = 'access-token-available'; +export const AUTH_COOKIE_OPTIONS: CookieOptions = { + httpOnly: true, + secure, + sameSite, + maxAge: TOKEN_COOKIE_MAXAGE / 24, // access token should last 1 hr +}; +export const REFRESH_COOKIE_OPTIONS: CookieOptions = { + ...AUTH_COOKIE_OPTIONS, + maxAge: TOKEN_COOKIE_MAXAGE, +}; +export const ACCESS_TOKEN_AVAILABLE_OPTIONS: CookieOptions = { + ...AUTH_COOKIE_OPTIONS, + httpOnly: false, +}; + +type IdAndEmail = { + id: string; + email: string; +}; + +@Injectable() +export class AuthService { + constructor( + private prisma: PrismaService, + private userService: UserService, + private smsService: SmsService, + ) {} + + /* + generates a signed token for a user + willBeRefreshToken changes the TTL of the token with true being longer and false being shorter + willBeRefreshToken is true when trying to sign a refresh token instead of the standard auth token + */ + generateAccessToken(user: User, willBeRefreshToken?: boolean): string { + const payload = { + sub: user.id, + expiresIn: willBeRefreshToken + ? REFRESH_COOKIE_OPTIONS.maxAge + : AUTH_COOKIE_OPTIONS.maxAge, + }; + return sign(payload, process.env.APP_SECRET); + } + + /* + this sets credentials as part of the response's cookies + handles the storage and creation of these credentials + */ + async setCredentials( + res: Response, + user: User, + incomingRefreshToken?: string, + ): Promise { + if (!user?.id) { + throw new UnauthorizedException('no user found'); + } + + if (incomingRefreshToken) { + // if token is provided, verify that its the correct refresh token + const userCount = await this.prisma.userAccounts.count({ + where: { + id: user.id, + activeRefreshToken: incomingRefreshToken, + }, + }); + + if (!userCount) { + // if the incoming refresh token is not the active refresh token for the user, clear the user's tokens + await this.prisma.userAccounts.update({ + data: { + activeAccessToken: null, + activeRefreshToken: null, + }, + where: { + id: user.id, + }, + }); + res.clearCookie(TOKEN_COOKIE_NAME, AUTH_COOKIE_OPTIONS); + res.clearCookie(REFRESH_COOKIE_NAME, REFRESH_COOKIE_OPTIONS); + res.clearCookie( + ACCESS_TOKEN_AVAILABLE_NAME, + ACCESS_TOKEN_AVAILABLE_OPTIONS, + ); + + throw new UnauthorizedException( + `User ${user.id} was attempting to use outdated token ${incomingRefreshToken} to generate new tokens`, + ); + } + } + + const accessToken = this.generateAccessToken(user); + const newRefreshToken = this.generateAccessToken(user, true); + + // store access and refresh token into db + await this.prisma.userAccounts.update({ + data: { + activeAccessToken: accessToken, + activeRefreshToken: newRefreshToken, + }, + where: { + id: user.id, + }, + }); + + res.cookie(TOKEN_COOKIE_NAME, accessToken, AUTH_COOKIE_OPTIONS); + res.cookie(REFRESH_COOKIE_NAME, newRefreshToken, REFRESH_COOKIE_OPTIONS); + res.cookie( + ACCESS_TOKEN_AVAILABLE_NAME, + true, + ACCESS_TOKEN_AVAILABLE_OPTIONS, + ); + + return { + success: true, + } as SuccessDTO; + } + + /* + this clears credentials from response and the db + */ + async clearCredentials(res: Response, user: User): Promise { + if (!user?.id) { + throw new UnauthorizedException('no user found'); + } + + // clear access and refresh tokens from db + await this.prisma.userAccounts.update({ + data: { + activeAccessToken: null, + activeRefreshToken: null, + }, + where: { + id: user.id, + }, + }); + + res.clearCookie(TOKEN_COOKIE_NAME, AUTH_COOKIE_OPTIONS); + res.clearCookie(REFRESH_COOKIE_NAME, REFRESH_COOKIE_OPTIONS); + res.clearCookie( + ACCESS_TOKEN_AVAILABLE_NAME, + ACCESS_TOKEN_AVAILABLE_OPTIONS, + ); + + return { + success: true, + } as SuccessDTO; + } + + /* + verifies that the requesting user can/should be provided an mfa code + generates then sends an mfa code to a users phone or email + */ + async requestMfaCode(dto: RequestMfaCode): Promise { + const user = await this.userService.findUserOrError( + { email: dto.email }, + false, + ); + + if (!user.mfaEnabled) { + throw new UnauthorizedException( + `user ${dto.email} requested an mfa code, but has mfa disabled`, + ); + } + + if (!(await isPasswordValid(user.passwordHash, dto.password))) { + throw new UnauthorizedException( + `user ${dto.email} requested an mfa code, but provided incorrect password`, + ); + } + + if (dto.mfaType === MfaType.sms) { + if (dto.phoneNumber) { + if (!user.phoneNumberVerified) { + user.phoneNumber = dto.phoneNumber; + } else { + throw new UnauthorizedException( + 'phone number can only be specified the first time using mfa', + ); + } + } else if (!dto.phoneNumber && !user.phoneNumber) { + throw new UnauthorizedException({ + name: 'phoneNumberMissing', + message: 'no valid phone number was found', + }); + } + } + + const mfaCode = this.generateMfaCode(); + await this.prisma.userAccounts.update({ + data: { + mfaCode, + mfaCodeUpdatedAt: new Date(), + phoneNumber: user.phoneNumber, + }, + where: { + id: user.id, + }, + }); + + if (dto.mfaType === MfaType.email) { + // TODO: email service (https://github.com/bloom-housing/bloom/pull/3607) + } else if (dto.mfaType === MfaType.sms) { + await this.smsService.sendMfaCode(user.phoneNumber, mfaCode); + } + + return dto.mfaType === MfaType.email + ? { email: user.email, phoneNumberVerified: user.phoneNumberVerified } + : { + phoneNumber: user.phoneNumber, + phoneNumberVerified: user.phoneNumberVerified, + }; + } + + /* + updates a user's password and logs them in + */ + async updatePassword( + dto: UpdatePassword, + res: Response, + ): Promise { + const user = await this.prisma.userAccounts.findFirst({ + where: { resetToken: dto.token }, + }); + + if (!user) { + throw new NotFoundException( + `user resetToken: ${dto.token} was requested but not found`, + ); + } + + const token: IdDTO = verify(dto.token, process.env.APP_SECRET) as IdDTO; + + if (token.id !== user.id) { + throw new UnauthorizedException( + `resetToken ${dto.token} does not match user ${user.id}'s reset token (${user.resetToken})`, + ); + } + + await this.prisma.userAccounts.update({ + data: { + passwordHash: await passwordToHash(dto.password), + passwordUpdatedAt: new Date(), + resetToken: null, + }, + where: { + id: user.id, + }, + }); + + return await this.setCredentials(res, mapTo(User, user)); + } + + /* + confirms a user and logs them in + */ + async confirmUser(dto: Confirm, res?: Response): Promise { + const token = verify(dto.token, process.env.APP_SECRET) as IdAndEmail; + + let user = await this.userService.findUserOrError( + { userId: token.id }, + false, + ); + + if (user.confirmationToken !== dto.token) { + throw new BadRequestException( + `Confirmation token mismatch for user stored: ${user.confirmationToken}, incoming token: ${dto.token}`, + ); + } + + const data: Prisma.UserAccountsUpdateInput = { + confirmedAt: new Date(), + confirmationToken: null, + }; + + if (dto.password) { + data.passwordHash = await passwordToHash(dto.password); + data.passwordUpdatedAt = new Date(); + } + + if (token.email) { + data.email = token.email; + } + + user = await this.prisma.userAccounts.update({ + data, + where: { + id: user.id, + }, + }); + + return await this.setCredentials(res, mapTo(User, user)); + } + + /* + generates a numeric mfa code + */ + generateMfaCode() { + let out = ''; + const characters = '0123456789'; + for (let i = 0; i < Number(process.env.MFA_CODE_LENGTH); i++) { + out += characters.charAt(randomInt(characters.length)); + } + return out; + } +} diff --git a/backend_new/src/services/sms.service.ts b/backend_new/src/services/sms.service.ts new file mode 100644 index 0000000000..2325a73d73 --- /dev/null +++ b/backend_new/src/services/sms.service.ts @@ -0,0 +1,29 @@ +import { Injectable } from '@nestjs/common'; +import twilio from 'twilio'; +import TwilioClient from 'twilio/lib/rest/Twilio'; + +@Injectable() +export class SmsService { + client: TwilioClient; + public constructor() { + if (process.env.TWILIO_ACCOUNT_SID && process.env.TWILIO_AUTH_TOKEN) { + this.client = twilio( + process.env.TWILIO_ACCOUNT_SID, + process.env.TWILIO_AUTH_TOKEN, + ); + } + } + public async sendMfaCode( + phoneNumber: string, + mfaCode: string, + ): Promise { + if (!this.client) { + return; + } + await this.client.messages.create({ + body: `Your Partners Portal account access token: ${mfaCode}`, + from: process.env.TWILIO_PHONE_NUMBER, + to: phoneNumber, + }); + } +} diff --git a/backend_new/src/services/user.service.ts b/backend_new/src/services/user.service.ts index 48926229c9..0cf94dd569 100644 --- a/backend_new/src/services/user.service.ts +++ b/backend_new/src/services/user.service.ts @@ -44,6 +44,7 @@ const view: Prisma.UserAccountsInclude = { type findByOptions = { userId?: string; email?: string; + resetToken?: string; }; @Injectable() @@ -708,9 +709,13 @@ export class UserService { }); if (!rawUser) { - throw new NotFoundException( - `user ${findBy.userId || findBy.email} was requested but not found`, - ); + let str = ''; + if (findBy.userId) { + str = `id: ${findBy.userId}`; + } else if (findBy.email) { + str = `email: ${findBy.email}`; + } + throw new NotFoundException(`user ${str} was requested but not found`); } return rawUser; diff --git a/backend_new/src/utilities/password-helpers.ts b/backend_new/src/utilities/password-helpers.ts index 9e92ac7e22..17ca43c225 100644 --- a/backend_new/src/utilities/password-helpers.ts +++ b/backend_new/src/utilities/password-helpers.ts @@ -2,6 +2,9 @@ import { randomBytes, scrypt } from 'crypto'; const SCRYPT_KEYLEN = 64; const SALT_SIZE = SCRYPT_KEYLEN; +/* + verifies that the hash of the incoming password matches the stored password hash +*/ export const isPasswordValid = async ( storedPasswordHash: string, incomingPassword: string, @@ -14,6 +17,9 @@ export const isPasswordValid = async ( return savedPasswordHash === verifyPasswordHash; }; +/* + hashes the incoming password with the incoming salt +*/ export const hashPassword = async ( password: string, salt: Buffer, @@ -25,6 +31,9 @@ export const hashPassword = async ( ); }; +/* + hashes and salts the incoming password +*/ export const passwordToHash = async (password: string): Promise => { const salt = generateSalt(); const hash = await hashPassword(password, salt); @@ -32,6 +41,23 @@ export const passwordToHash = async (password: string): Promise => { return `${salt.toString('hex')}#${hash}`; }; +/* + generates a random salt +*/ export const generateSalt = (size = SALT_SIZE) => { return randomBytes(size); }; + +/* + verifies the password's TTL is still valid +*/ +export const isPasswordOutdated = ( + passwordValidForDays: number, + passwordUpdatedAt: Date, +): boolean => { + return ( + new Date( + passwordUpdatedAt.getTime() + passwordValidForDays * 24 * 60 * 60 * 1000, + ) < new Date() + ); +}; diff --git a/backend_new/test/integration/auth.e2e-spec.ts b/backend_new/test/integration/auth.e2e-spec.ts new file mode 100644 index 0000000000..4dea466744 --- /dev/null +++ b/backend_new/test/integration/auth.e2e-spec.ts @@ -0,0 +1,319 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { INestApplication } from '@nestjs/common'; +import request from 'supertest'; +import cookieParser from 'cookie-parser'; +import { sign } from 'jsonwebtoken'; +import { AppModule } from '../../src/modules/app.module'; +import { PrismaService } from '../../src/services/prisma.service'; +import { userFactory } from '../../prisma/seed-helpers/user-factory'; +import { Login } from '../../src/dtos/auth/login.dto'; +import { MfaType } from '../../src/enums/mfa/mfa-type-enum'; +import { + ACCESS_TOKEN_AVAILABLE_NAME, + REFRESH_COOKIE_NAME, + TOKEN_COOKIE_NAME, +} from '../../src/services/auth.service'; +import { SmsService } from '../../src/services/sms.service'; +import { RequestMfaCode } from '../../src/dtos/mfa/request-mfa-code.dto'; +import { UpdatePassword } from '../../src/dtos/auth/update-password.dto'; +import { Confirm } from '../../src/dtos/auth/confirm.dto'; + +describe('Auth Controller Tests', () => { + let app: INestApplication; + let prisma: PrismaService; + let smsService: SmsService; + + beforeAll(async () => { + const moduleFixture: TestingModule = await Test.createTestingModule({ + imports: [AppModule], + }).compile(); + + app = moduleFixture.createNestApplication(); + app.use(cookieParser()); + prisma = moduleFixture.get(PrismaService); + smsService = moduleFixture.get(SmsService); + await app.init(); + }); + + afterAll(async () => { + await prisma.$disconnect(); + await app.close(); + }); + + it('should login successfully as mfaEnabled user', async () => { + const storedUser = await prisma.userAccounts.create({ + data: await userFactory({ + roles: { isAdmin: true }, + mfaCode: 'abcdef', + mfaEnabled: true, + confirmedAt: new Date(), + }), + }); + const res = await request(app.getHttpServer()) + .post('/auth/login') + .send({ + email: storedUser.email, + password: 'abcdef', + mfaCode: storedUser.mfaCode, + mfaType: MfaType.email, + } as Login) + .expect(201); + + expect(res.body).toEqual({ + success: true, + }); + + const cookies = res.headers['set-cookie'].map( + (cookie) => cookie.split('=')[0], + ); + + expect(cookies).toContain(TOKEN_COOKIE_NAME); + expect(cookies).toContain(REFRESH_COOKIE_NAME); + expect(cookies).toContain(ACCESS_TOKEN_AVAILABLE_NAME); + + const loggedInUser = await prisma.userAccounts.findUnique({ + where: { + id: storedUser.id, + }, + }); + + expect(loggedInUser.lastLoginAt).not.toBeNull(); + expect(loggedInUser.mfaCode).toBeNull(); + expect(loggedInUser.activeAccessToken).not.toBeNull(); + expect(loggedInUser.activeRefreshToken).not.toBeNull(); + }); + + it('should login successfully as non mfa user', async () => { + const storedUser = await prisma.userAccounts.create({ + data: await userFactory({ + roles: { isAdmin: true }, + mfaEnabled: false, + confirmedAt: new Date(), + }), + }); + const res = await request(app.getHttpServer()) + .post('/auth/login') + .send({ + email: storedUser.email, + password: 'abcdef', + } as Login) + .expect(201); + + expect(res.body).toEqual({ + success: true, + }); + + const cookies = res.headers['set-cookie'].map( + (cookie) => cookie.split('=')[0], + ); + + expect(cookies).toContain(TOKEN_COOKIE_NAME); + expect(cookies).toContain(REFRESH_COOKIE_NAME); + expect(cookies).toContain(ACCESS_TOKEN_AVAILABLE_NAME); + + const loggedInUser = await prisma.userAccounts.findUnique({ + where: { + id: storedUser.id, + }, + }); + + expect(loggedInUser.lastLoginAt).not.toBeNull(); + expect(loggedInUser.mfaCode).toBeNull(); + expect(loggedInUser.activeAccessToken).not.toBeNull(); + expect(loggedInUser.activeRefreshToken).not.toBeNull(); + }); + + it('should logout successfully', async () => { + const storedUser = await prisma.userAccounts.create({ + data: await userFactory({ + roles: { isAdmin: true }, + mfaEnabled: false, + confirmedAt: new Date(), + }), + }); + const resLogIn = await request(app.getHttpServer()) + .post('/auth/login') + .send({ + email: storedUser.email, + password: 'abcdef', + } as Login) + .expect(201); + + const resLogOut = await request(app.getHttpServer()) + .get('/auth/logout') + .set('Cookie', resLogIn.headers['set-cookie']) + .expect(200); + + expect(resLogOut.body).toEqual({ + success: true, + }); + + const cookies = resLogOut.headers['set-cookie'].map( + (cookie) => cookie.split('=')[0], + ); + + expect(cookies).toContain(TOKEN_COOKIE_NAME); + expect(cookies).toContain(REFRESH_COOKIE_NAME); + expect(cookies).toContain(ACCESS_TOKEN_AVAILABLE_NAME); + + const loggedInUser = await prisma.userAccounts.findUnique({ + where: { + id: storedUser.id, + }, + }); + + expect(loggedInUser.lastLoginAt).not.toBeNull(); + expect(loggedInUser.mfaCode).toBeNull(); + expect(loggedInUser.activeAccessToken).toBeNull(); + expect(loggedInUser.activeRefreshToken).toBeNull(); + }); + + it('should request mfaCode', async () => { + const storedUser = await prisma.userAccounts.create({ + data: await userFactory({ + roles: { isAdmin: true }, + mfaEnabled: true, + confirmedAt: new Date(), + phoneNumber: '111-111-1111', + phoneNumberVerified: true, + }), + }); + smsService.client.messages.create = jest + .fn() + .mockResolvedValue({ success: true }); + + const res = await request(app.getHttpServer()) + .post('/auth/request-mfa-code') + .send({ + email: storedUser.email, + password: 'abcdef', + mfaType: MfaType.sms, + } as RequestMfaCode) + .expect(201); + + expect(res.body).toEqual({ + phoneNumber: '111-111-1111', + phoneNumberVerified: true, + }); + + expect(smsService.client.messages.create).toHaveBeenCalledWith({ + body: expect.anything(), + from: process.env.TWILIO_PHONE_NUMBER, + to: '111-111-1111', + }); + + const user = await prisma.userAccounts.findUnique({ + where: { + id: storedUser.id, + }, + }); + + expect(user.mfaCode).not.toBeNull(); + expect(user.mfaCodeUpdatedAt).not.toBeNull(); + }); + + it('should update password', async () => { + const storedUser = await prisma.userAccounts.create({ + data: await userFactory({ + roles: { isAdmin: true }, + mfaEnabled: true, + phoneNumber: '111-111-1111', + phoneNumberVerified: true, + }), + }); + + const token = sign( + { + id: storedUser.id, + }, + process.env.APP_SECRET, + ); + + await prisma.userAccounts.update({ + data: { + resetToken: token, + }, + where: { + id: storedUser.id, + }, + }); + + const res = await request(app.getHttpServer()) + .put('/auth/update-password') + .send({ + email: storedUser.email, + password: 'abcdef123', + passwordConfirmation: 'abcdef123', + token, + } as UpdatePassword) + .expect(200); + + expect(res.body).toEqual({ + success: true, + }); + + const user = await prisma.userAccounts.findUnique({ + where: { + id: storedUser.id, + }, + }); + + expect(user.resetToken).toBeNull(); + expect(user.passwordHash).not.toEqual(storedUser.passwordHash); + }); + + it('should confirm user', async () => { + const storedUser = await prisma.userAccounts.create({ + data: await userFactory({ + roles: { isAdmin: true }, + mfaEnabled: true, + phoneNumber: '111-111-1111', + phoneNumberVerified: true, + }), + }); + + const token = sign( + { + id: storedUser.id, + }, + process.env.APP_SECRET, + ); + + await prisma.userAccounts.update({ + data: { + confirmationToken: token, + }, + where: { + id: storedUser.id, + }, + }); + + const res = await request(app.getHttpServer()) + .put('/auth/confirm') + .send({ + token, + } as Confirm) + .expect(200); + + expect(res.body).toEqual({ + success: true, + }); + + const user = await prisma.userAccounts.findUnique({ + where: { + id: storedUser.id, + }, + }); + + expect(user.confirmationToken).toBeNull(); + expect(user.confirmedAt).not.toBeNull(); + + const cookies = res.headers['set-cookie'].map( + (cookie) => cookie.split('=')[0], + ); + + expect(cookies).toContain(TOKEN_COOKIE_NAME); + expect(cookies).toContain(REFRESH_COOKIE_NAME); + expect(cookies).toContain(ACCESS_TOKEN_AVAILABLE_NAME); + }); +}); diff --git a/backend_new/test/integration/user.e2e-spec.ts b/backend_new/test/integration/user.e2e-spec.ts index 3ac3a3b690..7485690689 100644 --- a/backend_new/test/integration/user.e2e-spec.ts +++ b/backend_new/test/integration/user.e2e-spec.ts @@ -103,7 +103,9 @@ describe('User Controller Tests', () => { const res = await request(app.getHttpServer()) .get(`/user/${id}`) .expect(404); - expect(res.body.message).toEqual(`user ${id} was requested but not found`); + expect(res.body.message).toEqual( + `user id: ${id} was requested but not found`, + ); }); it('should get user from retrieve()', async () => { @@ -152,7 +154,7 @@ describe('User Controller Tests', () => { .expect(404); expect(res.body.message).toEqual( - `user ${randomId} was requested but not found`, + `user id: ${randomId} was requested but not found`, ); }); @@ -181,7 +183,7 @@ describe('User Controller Tests', () => { .expect(404); expect(res.body.message).toEqual( - `user ${randomId} was requested but not found`, + `user id: ${randomId} was requested but not found`, ); }); @@ -249,7 +251,7 @@ describe('User Controller Tests', () => { .expect(404); expect(res.body.message).toEqual( - `user ${email} was requested but not found`, + `user email: ${email} was requested but not found`, ); }); @@ -317,7 +319,7 @@ describe('User Controller Tests', () => { .expect(404); expect(res.body.message).toEqual( - `user ${email} was requested but not found`, + `user email: ${email} was requested but not found`, ); }); diff --git a/backend_new/test/jest-with-coverage.config.js b/backend_new/test/jest-with-coverage.config.js index 1e4fcddb7a..8ad1fecf6e 100644 --- a/backend_new/test/jest-with-coverage.config.js +++ b/backend_new/test/jest-with-coverage.config.js @@ -17,6 +17,7 @@ module.exports = { './src/utilities/**', './src/controllers/**', './src/modules/**', + './src/passports/**', ], coverageThreshold: { global: { diff --git a/backend_new/test/unit/passports/jwt.strategy.spec.ts b/backend_new/test/unit/passports/jwt.strategy.spec.ts new file mode 100644 index 0000000000..e3bf25a446 --- /dev/null +++ b/backend_new/test/unit/passports/jwt.strategy.spec.ts @@ -0,0 +1,186 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { randomUUID } from 'crypto'; +import { Request } from 'express'; +import { sign } from 'jsonwebtoken'; +import { PrismaService } from '../../../src/services/prisma.service'; +import { JwtStrategy } from '../../../src/passports/jwt.strategy'; +import { TOKEN_COOKIE_NAME } from '../../../src/services/auth.service'; + +describe('Testing jwt strategy', () => { + let strategy: JwtStrategy; + let prisma: PrismaService; + beforeAll(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [JwtStrategy, PrismaService], + }).compile(); + + strategy = module.get(JwtStrategy); + prisma = module.get(PrismaService); + }); + + it('should fail because user does not exist', async () => { + const id = randomUUID(); + const token = sign( + { + sub: id, + }, + process.env.APP_SECRET, + ); + prisma.userAccounts.findFirst = jest.fn().mockResolvedValue(null); + + const request = { + cookies: { + [TOKEN_COOKIE_NAME]: token, + }, + }; + + await expect( + async () => + await strategy.validate(request as unknown as Request, { sub: id }), + ).rejects.toThrowError(`user ${id} does not exist`); + + expect(prisma.userAccounts.findFirst).toHaveBeenCalledWith({ + include: { + userRoles: true, + listings: true, + jurisdictions: true, + }, + where: { + id, + }, + }); + }); + + it('should fail because user password is outdated', async () => { + const id = randomUUID(); + const token = sign( + { + sub: id, + }, + process.env.APP_SECRET, + ); + prisma.userAccounts.findFirst = jest.fn().mockResolvedValue({ + id, + passwordValidForDays: 100, + passwordUpdatedAt: new Date(0), + }); + + const request = { + cookies: { + [TOKEN_COOKIE_NAME]: token, + }, + }; + + await expect( + async () => + await strategy.validate(request as unknown as Request, { sub: id }), + ).rejects.toThrowError( + `user ${id} attempted to log in, but password is outdated`, + ); + + expect(prisma.userAccounts.findFirst).toHaveBeenCalledWith({ + include: { + userRoles: true, + listings: true, + jurisdictions: true, + }, + where: { + id, + }, + }); + }); + + it('should fail because stored token does not match incoming token', async () => { + const id = randomUUID(); + const token = sign( + { + sub: id, + }, + process.env.APP_SECRET, + ); + const activeToken = randomUUID(); + prisma.userAccounts.findFirst = jest.fn().mockResolvedValue({ + id, + passwordValidForDays: 100, + passwordUpdatedAt: new Date(), + activeAccessToken: activeToken, + }); + + prisma.userAccounts.update = jest.fn().mockResolvedValue({ + id, + }); + + const request = { + cookies: { + [TOKEN_COOKIE_NAME]: token, + }, + }; + + await expect( + async () => + await strategy.validate(request as unknown as Request, { sub: id }), + ).rejects.toThrowError(); + + expect(prisma.userAccounts.findFirst).toHaveBeenCalledWith({ + include: { + userRoles: true, + listings: true, + jurisdictions: true, + }, + where: { + id, + }, + }); + + expect(prisma.userAccounts.update).toHaveBeenCalledWith({ + data: { + activeAccessToken: null, + activeRefreshToken: null, + }, + where: { + id, + }, + }); + }); + + it('should succeed user validation', async () => { + const id = randomUUID(); + const token = sign( + { + sub: id, + }, + process.env.APP_SECRET, + ); + prisma.userAccounts.findFirst = jest.fn().mockResolvedValue({ + id, + passwordValidForDays: 100, + passwordUpdatedAt: new Date(), + activeAccessToken: token, + }); + + prisma.userAccounts.update = jest.fn().mockResolvedValue({ + id, + }); + + const request = { + cookies: { + [TOKEN_COOKIE_NAME]: token, + }, + }; + + await strategy.validate(request as unknown as Request, { sub: id }); + + expect(prisma.userAccounts.findFirst).toHaveBeenCalledWith({ + include: { + userRoles: true, + listings: true, + jurisdictions: true, + }, + where: { + id, + }, + }); + + expect(prisma.userAccounts.update).not.toHaveBeenCalled(); + }); +}); diff --git a/backend_new/test/unit/passports/mfa.strategy.spec.ts b/backend_new/test/unit/passports/mfa.strategy.spec.ts new file mode 100644 index 0000000000..36a7c8649a --- /dev/null +++ b/backend_new/test/unit/passports/mfa.strategy.spec.ts @@ -0,0 +1,640 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { randomUUID } from 'crypto'; +import { Request } from 'express'; +import { PrismaService } from '../../../src/services/prisma.service'; +import { MfaStrategy } from '../../../src/passports/mfa.strategy'; +import { MfaType } from '../../../src/enums/mfa/mfa-type-enum'; +import { Login } from '../../../src/dtos/auth/login.dto'; +import { passwordToHash } from '../../../src/utilities/password-helpers'; + +describe('Testing mfa strategy', () => { + let strategy: MfaStrategy; + let prisma: PrismaService; + beforeAll(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [MfaStrategy, PrismaService], + }).compile(); + + strategy = module.get(MfaStrategy); + prisma = module.get(PrismaService); + }); + + it('should fail because user does not exist', async () => { + prisma.userAccounts.findFirst = jest.fn().mockResolvedValue(null); + + const request = { + body: { + email: 'example@exygy.com', + password: 'abcdef', + mfaCode: 'zyxwv', + mfaType: MfaType.sms, + } as Login, + }; + + await expect( + async () => await strategy.validate(request as unknown as Request), + ).rejects.toThrowError( + `user example@exygy.com attempted to log in, but does not exist`, + ); + + expect(prisma.userAccounts.findFirst).toHaveBeenCalledWith({ + include: { + userRoles: true, + listings: true, + jurisdictions: true, + }, + where: { + email: 'example@exygy.com', + }, + }); + }); + + it('should fail because user is locked out', async () => { + prisma.userAccounts.findFirst = jest.fn().mockResolvedValue({ + id: randomUUID(), + lastLoginAt: new Date(), + failedLoginAttemptsCount: 10, + }); + + const request = { + body: { + email: 'example@exygy.com', + password: 'abcdef', + mfaCode: 'zyxwv', + mfaType: MfaType.sms, + } as Login, + }; + + await expect( + async () => await strategy.validate(request as unknown as Request), + ).rejects.toThrowError(`Failed login attempts exceeded.`); + + expect(prisma.userAccounts.findFirst).toHaveBeenCalledWith({ + include: { + userRoles: true, + listings: true, + jurisdictions: true, + }, + where: { + email: 'example@exygy.com', + }, + }); + }); + + it('should fail because user is not confirmed', async () => { + const id = randomUUID(); + prisma.userAccounts.findFirst = jest.fn().mockResolvedValue({ + id: id, + lastLoginAt: new Date(), + failedLoginAttemptsCount: 0, + confirmedAt: null, + }); + + const request = { + body: { + email: 'example@exygy.com', + password: 'abcdef', + mfaCode: 'zyxwv', + mfaType: MfaType.sms, + } as Login, + }; + + await expect( + async () => await strategy.validate(request as unknown as Request), + ).rejects.toThrowError( + `user ${id} attempted to login, but is not confirmed`, + ); + + expect(prisma.userAccounts.findFirst).toHaveBeenCalledWith({ + include: { + userRoles: true, + listings: true, + jurisdictions: true, + }, + where: { + email: 'example@exygy.com', + }, + }); + }); + + it('should fail because password is outdated', async () => { + const id = randomUUID(); + prisma.userAccounts.findFirst = jest.fn().mockResolvedValue({ + id: id, + lastLoginAt: new Date(), + failedLoginAttemptsCount: 0, + confirmedAt: new Date(), + passwordValidForDays: 0, + passwordUpdatedAt: new Date(0), + userRoles: { isAdmin: true }, + }); + + const request = { + body: { + email: 'example@exygy.com', + password: 'abcdef', + mfaCode: 'zyxwv', + mfaType: MfaType.sms, + } as Login, + }; + + await expect( + async () => await strategy.validate(request as unknown as Request), + ).rejects.toThrowError( + `user ${id} attempted to login, but password is no longer valid`, + ); + + expect(prisma.userAccounts.findFirst).toHaveBeenCalledWith({ + include: { + userRoles: true, + listings: true, + jurisdictions: true, + }, + where: { + email: 'example@exygy.com', + }, + }); + }); + + it('should fail because user password is invalid', async () => { + const id = randomUUID(); + prisma.userAccounts.findFirst = jest.fn().mockResolvedValue({ + id: id, + lastLoginAt: new Date(), + failedLoginAttemptsCount: 0, + confirmedAt: new Date(), + passwordValidForDays: 100, + passwordUpdatedAt: new Date(), + userRoles: { isAdmin: false }, + passwordHash: await passwordToHash('abcdef123'), + }); + + prisma.userAccounts.update = jest.fn().mockResolvedValue({ id }); + + const request = { + body: { + email: 'example@exygy.com', + password: 'abcdef', + mfaCode: 'zyxwv', + mfaType: MfaType.sms, + } as Login, + }; + + await expect( + async () => await strategy.validate(request as unknown as Request), + ).rejects.toThrowError(`Unauthorized Exception`); + + expect(prisma.userAccounts.findFirst).toHaveBeenCalledWith({ + include: { + userRoles: true, + listings: true, + jurisdictions: true, + }, + where: { + email: 'example@exygy.com', + }, + }); + + expect(prisma.userAccounts.update).toHaveBeenCalledWith({ + data: { + failedLoginAttemptsCount: 1, + }, + where: { + id, + }, + }); + }); + + it('should succeed if not an mfa user', async () => { + const id = randomUUID(); + prisma.userAccounts.findFirst = jest.fn().mockResolvedValue({ + id: id, + lastLoginAt: new Date(), + failedLoginAttemptsCount: 0, + confirmedAt: new Date(), + passwordValidForDays: 100, + passwordUpdatedAt: new Date(), + userRoles: { isAdmin: false }, + passwordHash: await passwordToHash('abcdef'), + mfaEnabled: false, + phoneNumberVerified: false, + }); + + prisma.userAccounts.update = jest.fn().mockResolvedValue({ id }); + + const request = { + body: { + email: 'example@exygy.com', + password: 'abcdef', + mfaCode: 'zyxwv', + mfaType: MfaType.sms, + } as Login, + }; + + await strategy.validate(request as unknown as Request); + + expect(prisma.userAccounts.findFirst).toHaveBeenCalledWith({ + include: { + userRoles: true, + listings: true, + jurisdictions: true, + }, + where: { + email: 'example@exygy.com', + }, + }); + + expect(prisma.userAccounts.update).toHaveBeenCalledWith({ + data: { + mfaCode: null, + mfaCodeUpdatedAt: null, + phoneNumberVerified: null, + lastLoginAt: expect.anything(), + failedLoginAttemptsCount: 0, + }, + where: { + id, + }, + }); + }); + + it('should fail if no mfaCode is stored', async () => { + const id = randomUUID(); + prisma.userAccounts.findFirst = jest.fn().mockResolvedValue({ + id: id, + lastLoginAt: new Date(), + failedLoginAttemptsCount: 0, + confirmedAt: new Date(), + passwordValidForDays: 100, + passwordUpdatedAt: new Date(), + userRoles: { isAdmin: false }, + passwordHash: await passwordToHash('abcdef'), + mfaEnabled: true, + phoneNumberVerified: false, + mfaCodeUpdatedAt: new Date(), + }); + + prisma.userAccounts.update = jest.fn().mockResolvedValue({ id }); + + const request = { + body: { + email: 'example@exygy.com', + password: 'abcdef', + mfaCode: 'zyxwv', + mfaType: MfaType.sms, + } as Login, + }; + + await expect( + async () => await strategy.validate(request as unknown as Request), + ).rejects.toThrowError(`Unauthorized Exception`); + + expect(prisma.userAccounts.findFirst).toHaveBeenCalledWith({ + include: { + userRoles: true, + listings: true, + jurisdictions: true, + }, + where: { + email: 'example@exygy.com', + }, + }); + + expect(prisma.userAccounts.update).toHaveBeenCalledWith({ + data: { + failedLoginAttemptsCount: 0, + }, + where: { + id, + }, + }); + }); + + it('should fail if no mfaCodeUpdatedAt is stored', async () => { + const id = randomUUID(); + prisma.userAccounts.findFirst = jest.fn().mockResolvedValue({ + id: id, + lastLoginAt: new Date(), + failedLoginAttemptsCount: 0, + confirmedAt: new Date(), + passwordValidForDays: 100, + passwordUpdatedAt: new Date(), + userRoles: { isAdmin: false }, + passwordHash: await passwordToHash('abcdef'), + mfaEnabled: true, + phoneNumberVerified: false, + mfaCode: 'zyxwv', + }); + + prisma.userAccounts.update = jest.fn().mockResolvedValue({ id }); + + const request = { + body: { + email: 'example@exygy.com', + password: 'abcdef', + mfaCode: 'zyxwv', + mfaType: MfaType.sms, + } as Login, + }; + + await expect( + async () => await strategy.validate(request as unknown as Request), + ).rejects.toThrowError(`Unauthorized Exception`); + + expect(prisma.userAccounts.findFirst).toHaveBeenCalledWith({ + include: { + userRoles: true, + listings: true, + jurisdictions: true, + }, + where: { + email: 'example@exygy.com', + }, + }); + + expect(prisma.userAccounts.update).toHaveBeenCalledWith({ + data: { + failedLoginAttemptsCount: 0, + }, + where: { + id, + }, + }); + }); + + it('should fail if no mfaCode is sent', async () => { + const id = randomUUID(); + prisma.userAccounts.findFirst = jest.fn().mockResolvedValue({ + id: id, + lastLoginAt: new Date(), + failedLoginAttemptsCount: 0, + confirmedAt: new Date(), + passwordValidForDays: 100, + passwordUpdatedAt: new Date(), + userRoles: { isAdmin: false }, + passwordHash: await passwordToHash('abcdef'), + mfaEnabled: true, + phoneNumberVerified: false, + mfaCode: 'zyxwv', + mfaCodeUpdatedAt: new Date(), + }); + + prisma.userAccounts.update = jest.fn().mockResolvedValue({ id }); + + const request = { + body: { + email: 'example@exygy.com', + password: 'abcdef', + mfaType: MfaType.sms, + } as Login, + }; + + await expect( + async () => await strategy.validate(request as unknown as Request), + ).rejects.toThrowError(`Unauthorized Exception`); + + expect(prisma.userAccounts.findFirst).toHaveBeenCalledWith({ + include: { + userRoles: true, + listings: true, + jurisdictions: true, + }, + where: { + email: 'example@exygy.com', + }, + }); + + expect(prisma.userAccounts.update).toHaveBeenCalledWith({ + data: { + failedLoginAttemptsCount: 0, + }, + where: { + id, + }, + }); + }); + + it('should fail if no mfaCode is incorrect', async () => { + const id = randomUUID(); + prisma.userAccounts.findFirst = jest.fn().mockResolvedValue({ + id: id, + lastLoginAt: new Date(), + failedLoginAttemptsCount: 0, + confirmedAt: new Date(), + passwordValidForDays: 100, + passwordUpdatedAt: new Date(), + userRoles: { isAdmin: false }, + passwordHash: await passwordToHash('abcdef'), + mfaEnabled: true, + phoneNumberVerified: false, + mfaCode: 'zyxwv', + mfaCodeUpdatedAt: new Date(), + }); + + prisma.userAccounts.update = jest.fn().mockResolvedValue({ id }); + + const request = { + body: { + email: 'example@exygy.com', + password: 'abcdef', + mfaCode: 'zyxwv1', + mfaType: MfaType.sms, + } as Login, + }; + + await expect( + async () => await strategy.validate(request as unknown as Request), + ).rejects.toThrowError(`mfaUnauthorized`); + + expect(prisma.userAccounts.findFirst).toHaveBeenCalledWith({ + include: { + userRoles: true, + listings: true, + jurisdictions: true, + }, + where: { + email: 'example@exygy.com', + }, + }); + + expect(prisma.userAccounts.update).toHaveBeenCalledWith({ + data: { + mfaCode: 'zyxwv', + mfaCodeUpdatedAt: expect.anything(), + phoneNumberVerified: false, + lastLoginAt: expect.anything(), + failedLoginAttemptsCount: 1, + }, + where: { + id, + }, + }); + }); + + it('should fail if no mfaCode is expired', async () => { + const id = randomUUID(); + prisma.userAccounts.findFirst = jest.fn().mockResolvedValue({ + id: id, + lastLoginAt: new Date(), + failedLoginAttemptsCount: 0, + confirmedAt: new Date(), + passwordValidForDays: 100, + passwordUpdatedAt: new Date(), + userRoles: { isAdmin: false }, + passwordHash: await passwordToHash('abcdef'), + mfaEnabled: true, + phoneNumberVerified: false, + mfaCode: 'zyxwv', + mfaCodeUpdatedAt: new Date(0), + }); + + prisma.userAccounts.update = jest.fn().mockResolvedValue({ id }); + + const request = { + body: { + email: 'example@exygy.com', + password: 'abcdef', + mfaCode: 'zyxwv', + mfaType: MfaType.sms, + } as Login, + }; + + await expect( + async () => await strategy.validate(request as unknown as Request), + ).rejects.toThrowError(`mfaUnauthorized`); + + expect(prisma.userAccounts.findFirst).toHaveBeenCalledWith({ + include: { + userRoles: true, + listings: true, + jurisdictions: true, + }, + where: { + email: 'example@exygy.com', + }, + }); + + expect(prisma.userAccounts.update).toHaveBeenCalledWith({ + data: { + mfaCode: 'zyxwv', + mfaCodeUpdatedAt: expect.anything(), + phoneNumberVerified: false, + lastLoginAt: expect.anything(), + failedLoginAttemptsCount: 1, + }, + where: { + id, + }, + }); + }); + + it('should succeed and set phoneNumberVerified', async () => { + const id = randomUUID(); + prisma.userAccounts.findFirst = jest.fn().mockResolvedValue({ + id: id, + lastLoginAt: new Date(), + failedLoginAttemptsCount: 0, + confirmedAt: new Date(), + passwordValidForDays: 100, + passwordUpdatedAt: new Date(), + userRoles: { isAdmin: false }, + passwordHash: await passwordToHash('abcdef'), + mfaEnabled: true, + phoneNumberVerified: false, + mfaCode: 'zyxwv', + mfaCodeUpdatedAt: new Date(), + }); + + prisma.userAccounts.update = jest.fn().mockResolvedValue({ id }); + + const request = { + body: { + email: 'example@exygy.com', + password: 'abcdef', + mfaCode: 'zyxwv', + mfaType: MfaType.sms, + } as Login, + }; + + await strategy.validate(request as unknown as Request); + + expect(prisma.userAccounts.findFirst).toHaveBeenCalledWith({ + include: { + userRoles: true, + listings: true, + jurisdictions: true, + }, + where: { + email: 'example@exygy.com', + }, + }); + + expect(prisma.userAccounts.update).toHaveBeenCalledWith({ + data: { + mfaCode: null, + mfaCodeUpdatedAt: expect.anything(), + phoneNumberVerified: true, + lastLoginAt: expect.anything(), + failedLoginAttemptsCount: 0, + }, + where: { + id, + }, + }); + }); + + it('should succeed and leave phoneNumberVerified false', async () => { + const id = randomUUID(); + prisma.userAccounts.findFirst = jest.fn().mockResolvedValue({ + id: id, + lastLoginAt: new Date(), + failedLoginAttemptsCount: 0, + confirmedAt: new Date(), + passwordValidForDays: 100, + passwordUpdatedAt: new Date(), + userRoles: { isAdmin: false }, + passwordHash: await passwordToHash('abcdef'), + mfaEnabled: true, + phoneNumberVerified: false, + mfaCode: 'zyxwv', + mfaCodeUpdatedAt: new Date(), + }); + + prisma.userAccounts.update = jest.fn().mockResolvedValue({ id }); + + const request = { + body: { + email: 'example@exygy.com', + password: 'abcdef', + mfaCode: 'zyxwv', + mfaType: MfaType.email, + } as Login, + }; + + await strategy.validate(request as unknown as Request); + + expect(prisma.userAccounts.findFirst).toHaveBeenCalledWith({ + include: { + userRoles: true, + listings: true, + jurisdictions: true, + }, + where: { + email: 'example@exygy.com', + }, + }); + + expect(prisma.userAccounts.update).toHaveBeenCalledWith({ + data: { + mfaCode: null, + mfaCodeUpdatedAt: expect.anything(), + phoneNumberVerified: false, + lastLoginAt: expect.anything(), + failedLoginAttemptsCount: 0, + }, + where: { + id, + }, + }); + }); +}); diff --git a/backend_new/test/unit/services/auth.service.spec.ts b/backend_new/test/unit/services/auth.service.spec.ts new file mode 100644 index 0000000000..6a25af592c --- /dev/null +++ b/backend_new/test/unit/services/auth.service.spec.ts @@ -0,0 +1,794 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { randomUUID } from 'crypto'; +import { sign } from 'jsonwebtoken'; +import { Response } from 'express'; +import { + ACCESS_TOKEN_AVAILABLE_NAME, + ACCESS_TOKEN_AVAILABLE_OPTIONS, + AuthService, + AUTH_COOKIE_OPTIONS, + REFRESH_COOKIE_NAME, + REFRESH_COOKIE_OPTIONS, + TOKEN_COOKIE_NAME, +} from '../../../src/services/auth.service'; +import { UserService } from '../../../src/services/user.service'; +import { PrismaService } from '../../../src/services/prisma.service'; +import { SmsService } from '../../../src/services/sms.service'; +import { + generateSalt, + hashPassword, + passwordToHash, +} from '../../../src/utilities/password-helpers'; +import { MfaType } from '../../../src/enums/mfa/mfa-type-enum'; + +describe('Testing auth service', () => { + let authService: AuthService; + let smsService: SmsService; + let prisma: PrismaService; + beforeAll(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [AuthService, UserService, PrismaService, SmsService], + }).compile(); + + authService = module.get(AuthService); + smsService = module.get(SmsService); + prisma = module.get(PrismaService); + }); + + it('should return a signed string when generating a new accessToken', () => { + const id = randomUUID(); + const token = authService.generateAccessToken( + { + passwordUpdatedAt: new Date(), + passwordValidForDays: 100, + email: 'example@exygy.com', + firstName: 'Exygy', + lastName: 'User', + jurisdictions: [ + { + id: randomUUID(), + }, + ], + agreedToTermsOfService: false, + id, + createdAt: new Date(), + updatedAt: new Date(), + }, + false, + ); + expect(token).toEqual( + sign( + { + sub: id, + expiresIn: 86400000 / 24, + }, + process.env.APP_SECRET, + ), + ); + }); + + it('should return a signed string when generating a new refreshToken', () => { + const id = randomUUID(); + const token = authService.generateAccessToken( + { + passwordUpdatedAt: new Date(), + passwordValidForDays: 100, + email: 'example@exygy.com', + firstName: 'Exygy', + lastName: 'User', + jurisdictions: [ + { + id: randomUUID(), + }, + ], + agreedToTermsOfService: false, + id, + createdAt: new Date(), + updatedAt: new Date(), + }, + true, + ); + expect(token).toEqual( + sign( + { + sub: id, + expiresIn: 86400000, + }, + process.env.APP_SECRET, + ), + ); + }); + + it('should set credentials when no incoming refresh token', async () => { + const id = randomUUID(); + const response = { + cookie: jest.fn(), + }; + prisma.userAccounts.update = jest.fn().mockResolvedValue({ id }); + + await authService.setCredentials(response as unknown as Response, { + passwordUpdatedAt: new Date(), + passwordValidForDays: 100, + email: 'example@exygy.com', + firstName: 'Exygy', + lastName: 'User', + jurisdictions: [ + { + id: randomUUID(), + }, + ], + agreedToTermsOfService: false, + id, + createdAt: new Date(), + updatedAt: new Date(), + }); + + expect(prisma.userAccounts.update).toHaveBeenCalledWith({ + data: { + activeAccessToken: expect.anything(), + activeRefreshToken: expect.anything(), + }, + where: { + id, + }, + }); + + expect(response.cookie).toHaveBeenCalledWith( + TOKEN_COOKIE_NAME, + expect.anything(), + AUTH_COOKIE_OPTIONS, + ); + + expect(response.cookie).toHaveBeenCalledWith( + REFRESH_COOKIE_NAME, + expect.anything(), + REFRESH_COOKIE_OPTIONS, + ); + + expect(response.cookie).toHaveBeenCalledWith( + ACCESS_TOKEN_AVAILABLE_NAME, + true, + ACCESS_TOKEN_AVAILABLE_OPTIONS, + ); + }); + + it('should set credentials with incoming refresh token and user exists', async () => { + const id = randomUUID(); + const response = { + cookie: jest.fn(), + }; + prisma.userAccounts.update = jest.fn().mockResolvedValue({ id }); + prisma.userAccounts.count = jest.fn().mockResolvedValue(1); + + await authService.setCredentials( + response as unknown as Response, + { + passwordUpdatedAt: new Date(), + passwordValidForDays: 100, + email: 'example@exygy.com', + firstName: 'Exygy', + lastName: 'User', + jurisdictions: [ + { + id: randomUUID(), + }, + ], + agreedToTermsOfService: false, + id, + createdAt: new Date(), + updatedAt: new Date(), + }, + 'refreshToken', + ); + + expect(prisma.userAccounts.update).toHaveBeenCalledWith({ + data: { + activeAccessToken: expect.anything(), + activeRefreshToken: expect.anything(), + }, + where: { + id, + }, + }); + + expect(prisma.userAccounts.count).toHaveBeenCalledWith({ + where: { + id, + activeRefreshToken: 'refreshToken', + }, + }); + + expect(response.cookie).toHaveBeenCalledWith( + TOKEN_COOKIE_NAME, + expect.anything(), + AUTH_COOKIE_OPTIONS, + ); + + expect(response.cookie).toHaveBeenCalledWith( + REFRESH_COOKIE_NAME, + expect.anything(), + REFRESH_COOKIE_OPTIONS, + ); + + expect(response.cookie).toHaveBeenCalledWith( + ACCESS_TOKEN_AVAILABLE_NAME, + true, + ACCESS_TOKEN_AVAILABLE_OPTIONS, + ); + }); + + it('should error when trying to set credentials with incoming refresh token and user does not exist', async () => { + const id = randomUUID(); + const response = { + cookie: jest.fn(), + clearCookie: jest.fn(), + }; + prisma.userAccounts.update = jest.fn().mockResolvedValue({ id }); + prisma.userAccounts.count = jest.fn().mockResolvedValue(0); + + await expect( + async () => + await authService.setCredentials( + response as unknown as Response, + { + passwordUpdatedAt: new Date(), + passwordValidForDays: 100, + email: 'example@exygy.com', + firstName: 'Exygy', + lastName: 'User', + jurisdictions: [ + { + id: randomUUID(), + }, + ], + agreedToTermsOfService: false, + id, + createdAt: new Date(), + updatedAt: new Date(), + }, + 'refreshToken', + ), + ).rejects.toThrowError( + `User ${id} was attempting to use outdated token refreshToken to generate new tokens`, + ); + + expect(prisma.userAccounts.update).toHaveBeenCalledWith({ + data: { + activeAccessToken: null, + activeRefreshToken: null, + }, + where: { + id, + }, + }); + + expect(prisma.userAccounts.count).toHaveBeenCalledWith({ + where: { + id, + activeRefreshToken: 'refreshToken', + }, + }); + + expect(response.clearCookie).toHaveBeenCalledWith( + TOKEN_COOKIE_NAME, + AUTH_COOKIE_OPTIONS, + ); + + expect(response.clearCookie).toHaveBeenCalledWith( + REFRESH_COOKIE_NAME, + REFRESH_COOKIE_OPTIONS, + ); + + expect(response.clearCookie).toHaveBeenCalledWith( + ACCESS_TOKEN_AVAILABLE_NAME, + ACCESS_TOKEN_AVAILABLE_OPTIONS, + ); + }); + + it('should error when trying to set credentials,but user id not passed in', async () => { + const id = randomUUID(); + const response = { + cookie: jest.fn(), + clearCookie: jest.fn(), + }; + prisma.userAccounts.update = jest.fn().mockResolvedValue({ id }); + prisma.userAccounts.count = jest.fn().mockResolvedValue(0); + + await expect( + async () => + await authService.setCredentials( + response as unknown as Response, + { + passwordUpdatedAt: new Date(), + passwordValidForDays: 100, + email: 'example@exygy.com', + firstName: 'Exygy', + lastName: 'User', + jurisdictions: [ + { + id: randomUUID(), + }, + ], + agreedToTermsOfService: false, + id: null, + createdAt: new Date(), + updatedAt: new Date(), + }, + 'refreshToken', + ), + ).rejects.toThrowError(`no user found`); + + expect(prisma.userAccounts.update).not.toHaveBeenCalled(); + + expect(prisma.userAccounts.count).not.toHaveBeenCalled(); + + expect(response.clearCookie).not.toHaveBeenCalled(); + + expect(response.clearCookie).not.toHaveBeenCalled(); + + expect(response.clearCookie).not.toHaveBeenCalled(); + }); + + it('should error when trying to clear credentials,but user id not passed in', async () => { + const id = randomUUID(); + const response = { + cookie: jest.fn(), + clearCookie: jest.fn(), + }; + prisma.userAccounts.update = jest.fn().mockResolvedValue({ id }); + + await expect( + async () => + await authService.clearCredentials(response as unknown as Response, { + passwordUpdatedAt: new Date(), + passwordValidForDays: 100, + email: 'example@exygy.com', + firstName: 'Exygy', + lastName: 'User', + jurisdictions: [ + { + id: randomUUID(), + }, + ], + agreedToTermsOfService: false, + id: null, + createdAt: new Date(), + updatedAt: new Date(), + }), + ).rejects.toThrowError(`no user found`); + + expect(prisma.userAccounts.update).not.toHaveBeenCalled(); + + expect(response.clearCookie).not.toHaveBeenCalled(); + + expect(response.clearCookie).not.toHaveBeenCalled(); + + expect(response.clearCookie).not.toHaveBeenCalled(); + }); + + it('should clear credentials when user exists', async () => { + const id = randomUUID(); + const response = { + cookie: jest.fn(), + clearCookie: jest.fn(), + }; + prisma.userAccounts.update = jest.fn().mockResolvedValue({ id }); + + await authService.clearCredentials(response as unknown as Response, { + passwordUpdatedAt: new Date(), + passwordValidForDays: 100, + email: 'example@exygy.com', + firstName: 'Exygy', + lastName: 'User', + jurisdictions: [ + { + id: randomUUID(), + }, + ], + agreedToTermsOfService: false, + id, + createdAt: new Date(), + updatedAt: new Date(), + }); + + expect(prisma.userAccounts.update).toHaveBeenCalledWith({ + data: { + activeAccessToken: null, + activeRefreshToken: null, + }, + where: { + id, + }, + }); + + expect(response.clearCookie).toHaveBeenCalledWith( + TOKEN_COOKIE_NAME, + AUTH_COOKIE_OPTIONS, + ); + + expect(response.clearCookie).toHaveBeenCalledWith( + REFRESH_COOKIE_NAME, + REFRESH_COOKIE_OPTIONS, + ); + + expect(response.clearCookie).toHaveBeenCalledWith( + ACCESS_TOKEN_AVAILABLE_NAME, + ACCESS_TOKEN_AVAILABLE_OPTIONS, + ); + }); + + it('should request mfa code through email', async () => { + const id = randomUUID(); + prisma.userAccounts.findUnique = jest.fn().mockResolvedValue({ + id: id, + mfaEnabled: true, + passwordHash: await passwordToHash('abcdef'), + email: 'example@exygy.com', + phoneNumberVerified: false, + }); + prisma.userAccounts.update = jest.fn().mockResolvedValue({ + id, + }); + + const res = await authService.requestMfaCode({ + email: 'example@exygy.com', + password: 'abcdef', + mfaType: MfaType.email, + }); + + expect(prisma.userAccounts.findUnique).toHaveBeenCalledWith({ + where: { + email: 'example@exygy.com', + }, + }); + expect(prisma.userAccounts.update).toHaveBeenCalledWith({ + data: { + mfaCode: expect.anything(), + mfaCodeUpdatedAt: expect.anything(), + }, + where: { + id, + }, + }); + expect(res).toEqual({ + email: 'example@exygy.com', + phoneNumberVerified: false, + }); + }); + + it('should request mfa code through sms', async () => { + const id = randomUUID(); + prisma.userAccounts.findUnique = jest.fn().mockResolvedValue({ + id: id, + mfaEnabled: true, + passwordHash: await passwordToHash('abcdef'), + email: 'example@exygy.com', + phoneNumberVerified: false, + phoneNumber: '520-781-8711', + }); + prisma.userAccounts.update = jest.fn().mockResolvedValue({ + id, + }); + smsService.client.messages.create = jest + .fn() + .mockResolvedValue({ success: true }); + + const res = await authService.requestMfaCode({ + email: 'example@exygy.com', + password: 'abcdef', + mfaType: MfaType.sms, + }); + + expect(prisma.userAccounts.findUnique).toHaveBeenCalledWith({ + where: { + email: 'example@exygy.com', + }, + }); + expect(prisma.userAccounts.update).toHaveBeenCalledWith({ + data: { + mfaCode: expect.anything(), + mfaCodeUpdatedAt: expect.anything(), + phoneNumber: '520-781-8711', + }, + where: { + id, + }, + }); + expect(smsService.client.messages.create).toHaveBeenCalledWith({ + body: expect.anything(), + from: expect.anything(), + to: '520-781-8711', + }); + expect(res).toEqual({ + phoneNumber: '520-781-8711', + phoneNumberVerified: false, + }); + }); + + it('should error when trying to request mfa code, but mfa disabled', async () => { + const id = randomUUID(); + prisma.userAccounts.findUnique = jest.fn().mockResolvedValue({ + id: id, + mfaEnabled: false, + passwordHash: await hashPassword('abcdef', generateSalt()), + email: 'example@exygy.com', + phoneNumberVerified: false, + phoneNumber: '520-781-8711', + }); + prisma.userAccounts.update = jest.fn().mockResolvedValue({ + id: id, + }); + + await expect( + async () => + await await authService.requestMfaCode({ + email: 'example@exygy.com', + password: 'abcdef', + mfaType: MfaType.sms, + }), + ).rejects.toThrowError( + 'user example@exygy.com requested an mfa code, but has mfa disabled', + ); + + expect(prisma.userAccounts.update).not.toHaveBeenCalled(); + }); + + it('should error when trying to request mfa code, but incorrect password', async () => { + const id = randomUUID(); + prisma.userAccounts.findUnique = jest.fn().mockResolvedValue({ + id: id, + mfaEnabled: true, + passwordHash: await hashPassword('abcdef', generateSalt()), + email: 'example@exygy.com', + phoneNumberVerified: false, + phoneNumber: '520-781-8711', + }); + prisma.userAccounts.update = jest.fn().mockResolvedValue({ + id: id, + }); + + await expect( + async () => + await await authService.requestMfaCode({ + email: 'example@exygy.com', + password: 'abcdef123', + mfaType: MfaType.sms, + }), + ).rejects.toThrowError( + 'user example@exygy.com requested an mfa code, but provided incorrect password', + ); + + expect(prisma.userAccounts.update).not.toHaveBeenCalled(); + }); + + it('should generate mfa code', () => { + expect(authService.generateMfaCode().length).toEqual( + Number(process.env.MFA_CODE_LENGTH), + ); + }); + + it('should update password when correct token passed in', async () => { + const id = randomUUID(); + const token = sign( + { + id, + }, + process.env.APP_SECRET, + ); + const response = { + cookie: jest.fn(), + }; + prisma.userAccounts.update = jest.fn().mockResolvedValue({ id }); + prisma.userAccounts.findFirst = jest.fn().mockResolvedValue({ id }); + + await authService.updatePassword( + { + password: 'abcdef', + passwordConfirmation: 'abcdef', + token, + }, + response as unknown as Response, + ); + + expect(prisma.userAccounts.findFirst).toHaveBeenCalledWith({ + where: { + resetToken: token, + }, + }); + + expect(prisma.userAccounts.update).toHaveBeenCalledWith({ + data: { + passwordHash: expect.anything(), + passwordUpdatedAt: expect.anything(), + resetToken: null, + }, + where: { + id, + }, + }); + + expect(response.cookie).toHaveBeenCalledWith( + TOKEN_COOKIE_NAME, + expect.anything(), + AUTH_COOKIE_OPTIONS, + ); + + expect(response.cookie).toHaveBeenCalledWith( + REFRESH_COOKIE_NAME, + expect.anything(), + REFRESH_COOKIE_OPTIONS, + ); + + expect(response.cookie).toHaveBeenCalledWith( + ACCESS_TOKEN_AVAILABLE_NAME, + true, + ACCESS_TOKEN_AVAILABLE_OPTIONS, + ); + }); + + it('should error when trying to update password, but there is an id mismatch', async () => { + const id = randomUUID(); + const token = sign( + { + id, + }, + process.env.APP_SECRET, + ); + const secondId = randomUUID(); + const secondToken = sign( + { + id: secondId, + }, + process.env.APP_SECRET, + ); + + const response = { + cookie: jest.fn(), + }; + prisma.userAccounts.update = jest.fn().mockResolvedValue({ id: secondId }); + prisma.userAccounts.findFirst = jest + .fn() + .mockResolvedValue({ id: secondId, resetToken: secondToken }); + + await expect( + async () => + await authService.updatePassword( + { + password: 'abcdef', + passwordConfirmation: 'abcdef', + token, + }, + response as unknown as Response, + ), + ).rejects.toThrowError( + `resetToken ${token} does not match user ${secondId}'s reset token (${secondToken})`, + ); + + expect(prisma.userAccounts.update).not.toHaveBeenCalled(); + }); + + it('should confirm user no email no password', async () => { + const id = randomUUID(); + const token = sign( + { + id, + }, + process.env.APP_SECRET, + ); + prisma.userAccounts.findUnique = jest + .fn() + .mockResolvedValue({ id, confirmationToken: token }); + + prisma.userAccounts.update = jest.fn().mockResolvedValue({ id }); + + const response = { + cookie: jest.fn(), + } as unknown as Response; + + await authService.confirmUser( + { + token, + }, + response, + ); + + expect(prisma.userAccounts.findUnique).toHaveBeenCalledWith({ + where: { + id, + }, + }); + + expect(prisma.userAccounts.update).toHaveBeenCalledWith({ + data: { + confirmedAt: expect.anything(), + confirmationToken: null, + }, + where: { + id, + }, + }); + + expect(response.cookie).toHaveBeenCalledWith( + TOKEN_COOKIE_NAME, + expect.anything(), + AUTH_COOKIE_OPTIONS, + ); + + expect(response.cookie).toHaveBeenCalledWith( + REFRESH_COOKIE_NAME, + expect.anything(), + REFRESH_COOKIE_OPTIONS, + ); + + expect(response.cookie).toHaveBeenCalledWith( + ACCESS_TOKEN_AVAILABLE_NAME, + true, + ACCESS_TOKEN_AVAILABLE_OPTIONS, + ); + }); + + it('should confirm user with email and password', async () => { + const id = randomUUID(); + const token = sign( + { + id, + email: 'example@exygy.com', + }, + process.env.APP_SECRET, + ); + prisma.userAccounts.findUnique = jest + .fn() + .mockResolvedValue({ id, confirmationToken: token }); + + prisma.userAccounts.update = jest.fn().mockResolvedValue({ id }); + + const response = { + cookie: jest.fn(), + } as unknown as Response; + + await authService.confirmUser( + { + token, + password: 'abcdef', + }, + response, + ); + + expect(prisma.userAccounts.findUnique).toHaveBeenCalledWith({ + where: { + id, + }, + }); + + expect(prisma.userAccounts.update).toHaveBeenCalledWith({ + data: { + confirmedAt: expect.anything(), + confirmationToken: null, + email: 'example@exygy.com', + passwordHash: expect.anything(), + passwordUpdatedAt: expect.anything(), + }, + where: { + id, + }, + }); + + expect(response.cookie).toHaveBeenCalledWith( + TOKEN_COOKIE_NAME, + expect.anything(), + AUTH_COOKIE_OPTIONS, + ); + + expect(response.cookie).toHaveBeenCalledWith( + REFRESH_COOKIE_NAME, + expect.anything(), + REFRESH_COOKIE_OPTIONS, + ); + + expect(response.cookie).toHaveBeenCalledWith( + ACCESS_TOKEN_AVAILABLE_NAME, + true, + ACCESS_TOKEN_AVAILABLE_OPTIONS, + ); + }); +}); diff --git a/backend_new/test/unit/services/user.service.spec.ts b/backend_new/test/unit/services/user.service.spec.ts index a9c694cdd1..85aec1f2ac 100644 --- a/backend_new/test/unit/services/user.service.spec.ts +++ b/backend_new/test/unit/services/user.service.spec.ts @@ -194,7 +194,7 @@ describe('Testing user service', () => { await expect( async () => await service.findOne('example Id'), - ).rejects.toThrowError('user example Id was requested but not found'); + ).rejects.toThrowError('user id: example Id was requested but not found'); expect(prisma.userAccounts.findUnique).toHaveBeenCalledWith({ include: { @@ -314,7 +314,7 @@ describe('Testing user service', () => { await expect( async () => await service.findUserOrError({ email: email }, false), ).rejects.toThrowError( - 'user example@email.com was requested but not found', + 'user email: example@email.com was requested but not found', ); expect(prisma.userAccounts.findUnique).toHaveBeenCalledWith({ where: { @@ -548,7 +548,7 @@ describe('Testing user service', () => { await expect( async () => await service.forgotPassword({ email }), ).rejects.toThrowError( - 'user email@example.com was requested but not found', + 'user email: email@example.com was requested but not found', ); expect(prisma.userAccounts.findUnique).toHaveBeenCalledWith({ where: { @@ -652,7 +652,7 @@ describe('Testing user service', () => { await expect( async () => await service.resendConfirmation({ email }, true), ).rejects.toThrowError( - 'user email@example.com was requested but not found', + 'user email: email@example.com was requested but not found', ); expect(prisma.userAccounts.findUnique).toHaveBeenCalledWith({ where: { @@ -702,7 +702,7 @@ describe('Testing user service', () => { prisma.userRoles.delete = jest.fn().mockResolvedValue(null); await expect(async () => await service.delete(id)).rejects.toThrowError( - `user ${id} was requested but not found`, + `user id: ${id} was requested but not found`, ); expect(prisma.userAccounts.findUnique).toHaveBeenCalledWith({ where: { @@ -925,7 +925,7 @@ describe('Testing user service', () => { lastName: 'last name', jurisdictions: [{ id: randomUUID() }], }), - ).rejects.toThrowError(`user ${id} was requested but not found`); + ).rejects.toThrowError(`user id: ${id} was requested but not found`); expect(prisma.userAccounts.findUnique).toHaveBeenCalledWith({ where: { id, diff --git a/backend_new/types/src/backend-swagger.ts b/backend_new/types/src/backend-swagger.ts index b286d8ff56..fcae82bf08 100644 --- a/backend_new/types/src/backend-swagger.ts +++ b/backend_new/types/src/backend-swagger.ts @@ -1701,6 +1701,155 @@ export class UserService { } } +export class AuthService { + /** + * Login + */ + login( + params: { + /** requestBody */ + body?: Login; + } = {} as any, + options: IRequestOptions = {}, + ): Promise { + return new Promise((resolve, reject) => { + let url = basePath + '/auth/login'; + + const configs: IRequestConfig = getConfigs( + 'post', + 'application/json', + url, + options, + ); + + let data = params.body; + + configs.data = data; + + axios(configs, resolve, reject); + }); + } + /** + * Logout + */ + logout(options: IRequestOptions = {}): Promise { + return new Promise((resolve, reject) => { + let url = basePath + '/auth/logout'; + + const configs: IRequestConfig = getConfigs( + 'get', + 'application/json', + url, + options, + ); + + /** 适配ios13getèŻ·æ±‚äžć…èźžćžŠbody */ + + axios(configs, resolve, reject); + }); + } + /** + * Request mfa code + */ + requestMfaCode( + params: { + /** requestBody */ + body?: RequestMfaCode; + } = {} as any, + options: IRequestOptions = {}, + ): Promise { + return new Promise((resolve, reject) => { + let url = basePath + '/auth/request-mfa-code'; + + const configs: IRequestConfig = getConfigs( + 'post', + 'application/json', + url, + options, + ); + + let data = params.body; + + configs.data = data; + + axios(configs, resolve, reject); + }); + } + /** + * Requests a new token given a refresh token + */ + requestNewToken(options: IRequestOptions = {}): Promise { + return new Promise((resolve, reject) => { + let url = basePath + '/auth/requestNewToken'; + + const configs: IRequestConfig = getConfigs( + 'get', + 'application/json', + url, + options, + ); + + /** 适配ios13getèŻ·æ±‚äžć…èźžćžŠbody */ + + axios(configs, resolve, reject); + }); + } + /** + * Update Password + */ + updatePassword( + params: { + /** requestBody */ + body?: UpdatePassword; + } = {} as any, + options: IRequestOptions = {}, + ): Promise { + return new Promise((resolve, reject) => { + let url = basePath + '/auth/update-password'; + + const configs: IRequestConfig = getConfigs( + 'put', + 'application/json', + url, + options, + ); + + let data = params.body; + + configs.data = data; + + axios(configs, resolve, reject); + }); + } + /** + * Confirm email + */ + confirm( + params: { + /** requestBody */ + body?: Confirm; + } = {} as any, + options: IRequestOptions = {}, + ): Promise { + return new Promise((resolve, reject) => { + let url = basePath + '/auth/confirm'; + + const configs: IRequestConfig = getConfigs( + 'put', + 'application/json', + url, + options, + ); + + let data = params.body; + + configs.data = data; + + axios(configs, resolve, reject); + }); + } +} + export interface SuccessDTO { /** */ success: boolean; @@ -4192,6 +4341,64 @@ export interface ConfirmationRequest { token: string; } +export interface Login { + /** */ + email: string; + + /** */ + password: string; + + /** */ + mfaCode?: string; + + /** */ + mfaType?: MfaType; +} + +export interface RequestMfaCode { + /** */ + email: string; + + /** */ + password: string; + + /** */ + mfaType: MfaType; + + /** */ + phoneNumber?: string; +} + +export interface RequestMfaCodeResponse { + /** */ + phoneNumber?: string; + + /** */ + email?: string; + + /** */ + phoneNumberVerified?: boolean; +} + +export interface UpdatePassword { + /** */ + password: string; + + /** */ + passwordConfirmation: string; + + /** */ + token: string; +} + +export interface Confirm { + /** */ + token: string; + + /** */ + password?: string; +} + export enum ListingViews { 'fundamentals' = 'fundamentals', 'base' = 'base', @@ -4340,3 +4547,8 @@ export enum YesNoEnum { 'yes' = 'yes', 'no' = 'no', } + +export enum MfaType { + 'sms' = 'sms', + 'email' = 'email', +} diff --git a/backend_new/yarn.lock b/backend_new/yarn.lock index b333e45181..a11caf4dc4 100644 --- a/backend_new/yarn.lock +++ b/backend_new/yarn.lock @@ -680,61 +680,61 @@ resolved "https://registry.yarnpkg.com/@istanbuljs/schema/-/schema-0.1.3.tgz#e45e384e4b8ec16bce2fd903af78450f6bf7ec98" integrity sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA== -"@jest/console@^29.6.2": - version "29.6.2" - resolved "https://registry.yarnpkg.com/@jest/console/-/console-29.6.2.tgz#bf1d4101347c23e07c029a1b1ae07d550f5cc541" - integrity sha512-0N0yZof5hi44HAR2pPS+ikJ3nzKNoZdVu8FffRf3wy47I7Dm7etk/3KetMdRUqzVd16V4O2m2ISpNTbnIuqy1w== +"@jest/console@^29.6.4": + version "29.6.4" + resolved "https://registry.yarnpkg.com/@jest/console/-/console-29.6.4.tgz#a7e2d84516301f986bba0dd55af9d5fe37f46527" + integrity sha512-wNK6gC0Ha9QeEPSkeJedQuTQqxZYnDPuDcDhVuVatRvMkL4D0VTvFVZj+Yuh6caG2aOfzkUZ36KtCmLNtR02hw== dependencies: - "@jest/types" "^29.6.1" + "@jest/types" "^29.6.3" "@types/node" "*" chalk "^4.0.0" - jest-message-util "^29.6.2" - jest-util "^29.6.2" + jest-message-util "^29.6.3" + jest-util "^29.6.3" slash "^3.0.0" -"@jest/core@^29.6.2": - version "29.6.2" - resolved "https://registry.yarnpkg.com/@jest/core/-/core-29.6.2.tgz#6f2d1dbe8aa0265fcd4fb8082ae1952f148209c8" - integrity sha512-Oj+5B+sDMiMWLhPFF+4/DvHOf+U10rgvCLGPHP8Xlsy/7QxS51aU/eBngudHlJXnaWD5EohAgJ4js+T6pa+zOg== +"@jest/core@^29.6.4": + version "29.6.4" + resolved "https://registry.yarnpkg.com/@jest/core/-/core-29.6.4.tgz#265ebee05ec1ff3567757e7a327155c8d6bdb126" + integrity sha512-U/vq5ccNTSVgYH7mHnodHmCffGWHJnz/E1BEWlLuK5pM4FZmGfBn/nrJGLjUsSmyx3otCeqc1T31F4y08AMDLg== dependencies: - "@jest/console" "^29.6.2" - "@jest/reporters" "^29.6.2" - "@jest/test-result" "^29.6.2" - "@jest/transform" "^29.6.2" - "@jest/types" "^29.6.1" + "@jest/console" "^29.6.4" + "@jest/reporters" "^29.6.4" + "@jest/test-result" "^29.6.4" + "@jest/transform" "^29.6.4" + "@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 "^29.5.0" - jest-config "^29.6.2" - jest-haste-map "^29.6.2" - jest-message-util "^29.6.2" - jest-regex-util "^29.4.3" - jest-resolve "^29.6.2" - jest-resolve-dependencies "^29.6.2" - jest-runner "^29.6.2" - jest-runtime "^29.6.2" - jest-snapshot "^29.6.2" - jest-util "^29.6.2" - jest-validate "^29.6.2" - jest-watcher "^29.6.2" + jest-changed-files "^29.6.3" + jest-config "^29.6.4" + jest-haste-map "^29.6.4" + jest-message-util "^29.6.3" + jest-regex-util "^29.6.3" + jest-resolve "^29.6.4" + jest-resolve-dependencies "^29.6.4" + jest-runner "^29.6.4" + jest-runtime "^29.6.4" + jest-snapshot "^29.6.4" + jest-util "^29.6.3" + jest-validate "^29.6.3" + jest-watcher "^29.6.4" micromatch "^4.0.4" - pretty-format "^29.6.2" + pretty-format "^29.6.3" slash "^3.0.0" strip-ansi "^6.0.0" -"@jest/environment@^29.6.2": - version "29.6.2" - resolved "https://registry.yarnpkg.com/@jest/environment/-/environment-29.6.2.tgz#794c0f769d85e7553439d107d3f43186dc6874a9" - integrity sha512-AEcW43C7huGd/vogTddNNTDRpO6vQ2zaQNrttvWV18ArBx9Z56h7BIsXkNFJVOO4/kblWEQz30ckw0+L3izc+Q== +"@jest/environment@^29.6.4": + version "29.6.4" + resolved "https://registry.yarnpkg.com/@jest/environment/-/environment-29.6.4.tgz#78ec2c9f8c8829a37616934ff4fea0c028c79f4f" + integrity sha512-sQ0SULEjA1XUTHmkBRl7A1dyITM9yb1yb3ZNKPX3KlTd6IG7mWUe3e2yfExtC2Zz1Q+mMckOLHmL/qLiuQJrBQ== dependencies: - "@jest/fake-timers" "^29.6.2" - "@jest/types" "^29.6.1" + "@jest/fake-timers" "^29.6.4" + "@jest/types" "^29.6.3" "@types/node" "*" - jest-mock "^29.6.2" + jest-mock "^29.6.3" "@jest/expect-utils@^29.6.2": version "29.6.2" @@ -743,46 +743,53 @@ dependencies: jest-get-type "^29.4.3" -"@jest/expect@^29.6.2": - version "29.6.2" - resolved "https://registry.yarnpkg.com/@jest/expect/-/expect-29.6.2.tgz#5a2ad58bb345165d9ce0a1845bbf873c480a4b28" - integrity sha512-m6DrEJxVKjkELTVAztTLyS/7C92Y2b0VYqmDROYKLLALHn8T/04yPs70NADUYPrV3ruI+H3J0iUIuhkjp7vkfg== +"@jest/expect-utils@^29.6.4": + version "29.6.4" + resolved "https://registry.yarnpkg.com/@jest/expect-utils/-/expect-utils-29.6.4.tgz#17c7dfe6cec106441f218b0aff4b295f98346679" + integrity sha512-FEhkJhqtvBwgSpiTrocquJCdXPsyvNKcl/n7A3u7X4pVoF4bswm11c9d4AV+kfq2Gpv/mM8x7E7DsRvH+djkrg== dependencies: - expect "^29.6.2" - jest-snapshot "^29.6.2" + jest-get-type "^29.6.3" -"@jest/fake-timers@^29.6.2": - version "29.6.2" - resolved "https://registry.yarnpkg.com/@jest/fake-timers/-/fake-timers-29.6.2.tgz#fe9d43c5e4b1b901168fe6f46f861b3e652a2df4" - integrity sha512-euZDmIlWjm1Z0lJ1D0f7a0/y5Kh/koLFMUBE5SUYWrmy8oNhJpbTBDAP6CxKnadcMLDoDf4waRYCe35cH6G6PA== +"@jest/expect@^29.6.4": + version "29.6.4" + resolved "https://registry.yarnpkg.com/@jest/expect/-/expect-29.6.4.tgz#1d6ae17dc68d906776198389427ab7ce6179dba6" + integrity sha512-Warhsa7d23+3X5bLbrbYvaehcgX5TLYhI03JKoedTiI8uJU4IhqYBWF7OSSgUyz4IgLpUYPkK0AehA5/fRclAA== dependencies: - "@jest/types" "^29.6.1" + expect "^29.6.4" + jest-snapshot "^29.6.4" + +"@jest/fake-timers@^29.6.4": + version "29.6.4" + resolved "https://registry.yarnpkg.com/@jest/fake-timers/-/fake-timers-29.6.4.tgz#45a27f093c43d5d989362a3e7a8c70c83188b4f6" + integrity sha512-6UkCwzoBK60edXIIWb0/KWkuj7R7Qq91vVInOe3De6DSpaEiqjKcJw4F7XUet24Wupahj9J6PlR09JqJ5ySDHw== + dependencies: + "@jest/types" "^29.6.3" "@sinonjs/fake-timers" "^10.0.2" "@types/node" "*" - jest-message-util "^29.6.2" - jest-mock "^29.6.2" - jest-util "^29.6.2" + jest-message-util "^29.6.3" + jest-mock "^29.6.3" + jest-util "^29.6.3" -"@jest/globals@^29.6.2": - version "29.6.2" - resolved "https://registry.yarnpkg.com/@jest/globals/-/globals-29.6.2.tgz#74af81b9249122cc46f1eb25793617eec69bf21a" - integrity sha512-cjuJmNDjs6aMijCmSa1g2TNG4Lby/AeU7/02VtpW+SLcZXzOLK2GpN2nLqcFjmhy3B3AoPeQVx7BnyOf681bAw== +"@jest/globals@^29.6.4": + version "29.6.4" + resolved "https://registry.yarnpkg.com/@jest/globals/-/globals-29.6.4.tgz#4f04f58731b062b44ef23036b79bdb31f40c7f63" + integrity sha512-wVIn5bdtjlChhXAzVXavcY/3PEjf4VqM174BM3eGL5kMxLiZD5CLnbmkEyA1Dwh9q8XjP6E8RwjBsY/iCWrWsA== dependencies: - "@jest/environment" "^29.6.2" - "@jest/expect" "^29.6.2" - "@jest/types" "^29.6.1" - jest-mock "^29.6.2" + "@jest/environment" "^29.6.4" + "@jest/expect" "^29.6.4" + "@jest/types" "^29.6.3" + jest-mock "^29.6.3" -"@jest/reporters@^29.6.2": - version "29.6.2" - resolved "https://registry.yarnpkg.com/@jest/reporters/-/reporters-29.6.2.tgz#524afe1d76da33d31309c2c4a2c8062d0c48780a" - integrity sha512-sWtijrvIav8LgfJZlrGCdN0nP2EWbakglJY49J1Y5QihcQLfy7ovyxxjJBRXMNltgt4uPtEcFmIMbVshEDfFWw== +"@jest/reporters@^29.6.4": + version "29.6.4" + resolved "https://registry.yarnpkg.com/@jest/reporters/-/reporters-29.6.4.tgz#9d6350c8a2761ece91f7946e97ab0dabc06deab7" + integrity sha512-sxUjWxm7QdchdrD3NfWKrL8FBsortZeibSJv4XLjESOOjSUOkjQcb0ZHJwfhEGIvBvTluTzfG2yZWZhkrXJu8g== dependencies: "@bcoe/v8-coverage" "^0.2.3" - "@jest/console" "^29.6.2" - "@jest/test-result" "^29.6.2" - "@jest/transform" "^29.6.2" - "@jest/types" "^29.6.1" + "@jest/console" "^29.6.4" + "@jest/test-result" "^29.6.4" + "@jest/transform" "^29.6.4" + "@jest/types" "^29.6.3" "@jridgewell/trace-mapping" "^0.3.18" "@types/node" "*" chalk "^4.0.0" @@ -791,13 +798,13 @@ 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 "^29.6.2" - jest-util "^29.6.2" - jest-worker "^29.6.2" + jest-message-util "^29.6.3" + jest-util "^29.6.3" + jest-worker "^29.6.4" slash "^3.0.0" string-length "^4.0.1" strip-ansi "^6.0.0" @@ -810,51 +817,58 @@ dependencies: "@sinclair/typebox" "^0.27.8" -"@jest/source-map@^29.6.0": - version "29.6.0" - resolved "https://registry.yarnpkg.com/@jest/source-map/-/source-map-29.6.0.tgz#bd34a05b5737cb1a99d43e1957020ac8e5b9ddb1" - integrity sha512-oA+I2SHHQGxDCZpbrsCQSoMLb3Bz547JnM+jUr9qEbuw0vQlWZfpPS7CO9J7XiwKicEz9OFn/IYoLkkiUD7bzA== +"@jest/schemas@^29.6.3": + version "29.6.3" + resolved "https://registry.yarnpkg.com/@jest/schemas/-/schemas-29.6.3.tgz#430b5ce8a4e0044a7e3819663305a7b3091c8e03" + integrity sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA== + dependencies: + "@sinclair/typebox" "^0.27.8" + +"@jest/source-map@^29.6.3": + version "29.6.3" + resolved "https://registry.yarnpkg.com/@jest/source-map/-/source-map-29.6.3.tgz#d90ba772095cf37a34a5eb9413f1b562a08554c4" + integrity sha512-MHjT95QuipcPrpLM+8JMSzFx6eHp5Bm+4XeFDJlwsvVBjmKNiIAvasGK2fxz2WbGRlnvqehFbh07MMa7n3YJnw== dependencies: "@jridgewell/trace-mapping" "^0.3.18" callsites "^3.0.0" graceful-fs "^4.2.9" -"@jest/test-result@^29.6.2": - version "29.6.2" - resolved "https://registry.yarnpkg.com/@jest/test-result/-/test-result-29.6.2.tgz#fdd11583cd1608e4db3114e8f0cce277bf7a32ed" - integrity sha512-3VKFXzcV42EYhMCsJQURptSqnyjqCGbtLuX5Xxb6Pm6gUf1wIRIl+mandIRGJyWKgNKYF9cnstti6Ls5ekduqw== +"@jest/test-result@^29.6.4": + version "29.6.4" + resolved "https://registry.yarnpkg.com/@jest/test-result/-/test-result-29.6.4.tgz#adf5c79f6e1fb7405ad13d67d9e2b6ff54b54c6b" + integrity sha512-uQ1C0AUEN90/dsyEirgMLlouROgSY+Wc/JanVVk0OiUKa5UFh7sJpMEM3aoUBAz2BRNvUJ8j3d294WFuRxSyOQ== dependencies: - "@jest/console" "^29.6.2" - "@jest/types" "^29.6.1" + "@jest/console" "^29.6.4" + "@jest/types" "^29.6.3" "@types/istanbul-lib-coverage" "^2.0.0" collect-v8-coverage "^1.0.0" -"@jest/test-sequencer@^29.6.2": - version "29.6.2" - resolved "https://registry.yarnpkg.com/@jest/test-sequencer/-/test-sequencer-29.6.2.tgz#585eff07a68dd75225a7eacf319780cb9f6b9bf4" - integrity sha512-GVYi6PfPwVejO7slw6IDO0qKVum5jtrJ3KoLGbgBWyr2qr4GaxFV6su+ZAjdTX75Sr1DkMFRk09r2ZVa+wtCGw== +"@jest/test-sequencer@^29.6.4": + version "29.6.4" + resolved "https://registry.yarnpkg.com/@jest/test-sequencer/-/test-sequencer-29.6.4.tgz#86aef66aaa22b181307ed06c26c82802fb836d7b" + integrity sha512-E84M6LbpcRq3fT4ckfKs9ryVanwkaIB0Ws9bw3/yP4seRLg/VaCZ/LgW0MCq5wwk4/iP/qnilD41aj2fsw2RMg== dependencies: - "@jest/test-result" "^29.6.2" + "@jest/test-result" "^29.6.4" graceful-fs "^4.2.9" - jest-haste-map "^29.6.2" + jest-haste-map "^29.6.4" slash "^3.0.0" -"@jest/transform@^29.6.2": - version "29.6.2" - resolved "https://registry.yarnpkg.com/@jest/transform/-/transform-29.6.2.tgz#522901ebbb211af08835bc3bcdf765ab778094e3" - integrity sha512-ZqCqEISr58Ce3U+buNFJYUktLJZOggfyvR+bZMaiV1e8B1SIvJbwZMrYz3gx/KAPn9EXmOmN+uB08yLCjWkQQg== +"@jest/transform@^29.6.4": + version "29.6.4" + resolved "https://registry.yarnpkg.com/@jest/transform/-/transform-29.6.4.tgz#a6bc799ef597c5d85b2e65a11fd96b6b239bab5a" + integrity sha512-8thgRSiXUqtr/pPGY/OsyHuMjGyhVnWrFAwoxmIemlBuiMyU1WFs0tXoNxzcr4A4uErs/ABre76SGmrr5ab/AA== dependencies: "@babel/core" "^7.11.6" - "@jest/types" "^29.6.1" + "@jest/types" "^29.6.3" "@jridgewell/trace-mapping" "^0.3.18" babel-plugin-istanbul "^6.1.1" chalk "^4.0.0" convert-source-map "^2.0.0" fast-json-stable-stringify "^2.1.0" graceful-fs "^4.2.9" - jest-haste-map "^29.6.2" - jest-regex-util "^29.4.3" - jest-util "^29.6.2" + jest-haste-map "^29.6.4" + jest-regex-util "^29.6.3" + jest-util "^29.6.3" micromatch "^4.0.4" pirates "^4.0.4" slash "^3.0.0" @@ -872,6 +886,18 @@ "@types/yargs" "^17.0.8" chalk "^4.0.0" +"@jest/types@^29.6.3": + version "29.6.3" + resolved "https://registry.yarnpkg.com/@jest/types/-/types-29.6.3.tgz#1131f8cf634e7e84c5e77bab12f052af585fba59" + integrity sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw== + dependencies: + "@jest/schemas" "^29.6.3" + "@types/istanbul-lib-coverage" "^2.0.0" + "@types/istanbul-reports" "^3.0.0" + "@types/node" "*" + "@types/yargs" "^17.0.8" + chalk "^4.0.0" + "@jridgewell/gen-mapping@^0.3.0", "@jridgewell/gen-mapping@^0.3.2": version "0.3.3" resolved "https://registry.yarnpkg.com/@jridgewell/gen-mapping/-/gen-mapping-0.3.3.tgz#7e02e6eb5df901aaedb08514203b096614024098" @@ -937,7 +963,7 @@ dependencies: lodash "^4.17.21" -"@nestjs/axios@^3.0.0": +"@nestjs/axios@~3.0.0": version "3.0.0" resolved "https://registry.yarnpkg.com/@nestjs/axios/-/axios-3.0.0.tgz#a2e70b118e3058f3d4b9c3deacd496ec4e3ee69e" integrity sha512-ULdH03jDWkS5dy9X69XbUVbhC+0pVnrRcj7bIK/ytTZ76w7CgvTZDJqsIyisg3kNOiljRW/4NIjSf3j6YGvl+g== @@ -993,11 +1019,24 @@ tslib "2.4.0" uuid "8.3.2" +"@nestjs/jwt@~10.1.0": + version "10.1.1" + resolved "https://registry.yarnpkg.com/@nestjs/jwt/-/jwt-10.1.1.tgz#6a564ef3b5a5e7d8d34d78f1ceb23c1523cdb9e7" + integrity sha512-sISYylg8y1Mb7saxPx5Zh11i7v9JOh70CEC/rN6g43MrbFlJ57c1eYFrffxip1YAx3DmV4K67yXob3syKZMOew== + dependencies: + "@types/jsonwebtoken" "9.0.2" + jsonwebtoken "9.0.0" + "@nestjs/mapped-types@1.2.2": version "1.2.2" resolved "https://registry.yarnpkg.com/@nestjs/mapped-types/-/mapped-types-1.2.2.tgz#d9ddb143776e309dbc1a518ac1607fddac1e140e" integrity sha512-3dHxLXs3M0GPiriAcCFFJQHoDFUuzTD5w6JDhE7TyfT89YKpe6tcCCIqOZWdXmt9AZjjK30RkHRSFF+QEnWFQg== +"@nestjs/passport@~10.0.1": + version "10.0.1" + resolved "https://registry.yarnpkg.com/@nestjs/passport/-/passport-10.0.1.tgz#4a745cb4acf01ef8fd56b9ec1349ac74165b098f" + integrity sha512-hS22LeNj0LByS9toBPkpKyZhyKAXoHACLS1EQrjbAJJEQjhocOskVGwcMwvMlz+ohN+VU804/nMF1Zlya4+TiQ== + "@nestjs/platform-express@^8.0.0": version "8.4.7" resolved "https://registry.yarnpkg.com/@nestjs/platform-express/-/platform-express-8.4.7.tgz#402a3d3c47327a164bb3867615f423c29d1a6cd9" @@ -1271,7 +1310,7 @@ "@types/range-parser" "*" "@types/send" "*" -"@types/express@^4.17.17": +"@types/express@~4.17.17": version "4.17.17" resolved "https://registry.yarnpkg.com/@types/express/-/express-4.17.17.tgz#01d5437f6ef9cfa8668e616e13c2f2ac9a491ae4" integrity sha512-Q4FmmuLGBG58btUnfS1c1r/NQdlp3DMfGDGig8WhfpA2YRUtEkxAjkZb0yvplJGYdF1fsQ81iMDcH24sSCNC/Q== @@ -1315,10 +1354,10 @@ dependencies: "@types/istanbul-lib-report" "*" -"@types/jest@^29.5.3": - version "29.5.3" - resolved "https://registry.yarnpkg.com/@types/jest/-/jest-29.5.3.tgz#7a35dc0044ffb8b56325c6802a4781a626b05777" - integrity sha512-1Nq7YrO/vJE/FYnqYyw0FS8LdrjExSgIiHyKg7xPpn+yi8Q4huZryKnkJatN1ZRH89Kw2v33/8ZMB7DuZeSLlA== +"@types/jest@~29.5.3": + version "29.5.4" + resolved "https://registry.yarnpkg.com/@types/jest/-/jest-29.5.4.tgz#9d0a16edaa009a71e6a71a999acd582514dab566" + integrity sha512-PhglGmhWeD46FYOVLt3X7TiWjzwuVGW9wG/4qocPevXMjCmrIc5b6db9WjeGE4QYVpUAWMDv3v0IiBwObY289A== dependencies: expect "^29.0.0" pretty-format "^29.0.0" @@ -1347,6 +1386,13 @@ resolved "https://registry.yarnpkg.com/@types/json5/-/json5-0.0.29.tgz#ee28707ae94e11d2b827bcbe5270bcea7f3e71ee" integrity sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ== +"@types/jsonwebtoken@9.0.2": + version "9.0.2" + resolved "https://registry.yarnpkg.com/@types/jsonwebtoken/-/jsonwebtoken-9.0.2.tgz#9eeb56c76dd555039be2a3972218de5bd3b8d83e" + integrity sha512-drE6uz7QBKq1fYqqoFKTDRdFCPHd5TCub75BM+D+cMx7NU9hUz7SESLfC2fSCXVFMO5Yj8sOWHuGqPgjc+fz0Q== + dependencies: + "@types/node" "*" + "@types/linkify-it@*": version "3.0.2" resolved "https://registry.yarnpkg.com/@types/linkify-it/-/linkify-it-3.0.2.tgz#fd2cd2edbaa7eaac7e7f3c1748b52a19143846c9" @@ -1462,7 +1508,7 @@ "@types/cookiejar" "*" "@types/node" "*" -"@types/supertest@^2.0.11": +"@types/supertest@~2.0.11": version "2.0.12" resolved "https://registry.yarnpkg.com/@types/supertest/-/supertest-2.0.12.tgz#ddb4a0568597c9aadff8dbec5b2e8fddbe8692fc" integrity sha512-X3HPWTwXRerBZS7Mo1k6vMVR1Z6zmJcDVn5O/31whe0tnjE4te6ZJSJGq1RiqHPjzPdMTfjCFogDJmwng9xHaQ== @@ -1912,6 +1958,13 @@ axios@0.27.2: follow-redirects "^1.14.9" form-data "^4.0.0" +axios@^0.26.1: + version "0.26.1" + resolved "https://registry.yarnpkg.com/axios/-/axios-0.26.1.tgz#1ede41c51fcf51bbbd6fd43669caaa4f0495aaa9" + integrity sha512-fPwcX4EvnSHuInCMItEhAGnaSEXRBjtzh9fOtsE6E1G6p7vl7edEeZe11QHf18+6+9gR5PbKV/sGKNaD8YaMeA== + dependencies: + follow-redirects "^1.14.8" + axios@^1.2.2: version "1.4.0" resolved "https://registry.yarnpkg.com/axios/-/axios-1.4.0.tgz#38a7bf1224cd308de271146038b551d725f0be1f" @@ -1921,15 +1974,15 @@ axios@^1.2.2: form-data "^4.0.0" proxy-from-env "^1.1.0" -babel-jest@^29.6.2: - version "29.6.2" - resolved "https://registry.yarnpkg.com/babel-jest/-/babel-jest-29.6.2.tgz#cada0a59e07f5acaeb11cbae7e3ba92aec9c1126" - integrity sha512-BYCzImLos6J3BH/+HvUCHG1dTf2MzmAB4jaVxHV+29RZLjR29XuYTmsf2sdDwkrb+FczkGo3kOhE7ga6sI0P4A== +babel-jest@^29.6.4: + version "29.6.4" + resolved "https://registry.yarnpkg.com/babel-jest/-/babel-jest-29.6.4.tgz#98dbc45d1c93319c82a8ab4a478b670655dd2585" + integrity sha512-meLj23UlSLddj6PC+YTOFRgDAtjnZom8w/ACsrx0gtPtv5cJZk0A5Unk5bV4wixD7XaPCN1fQvpww8czkZURmw== dependencies: - "@jest/transform" "^29.6.2" + "@jest/transform" "^29.6.4" "@types/babel__core" "^7.1.14" babel-plugin-istanbul "^6.1.1" - babel-preset-jest "^29.5.0" + babel-preset-jest "^29.6.3" chalk "^4.0.0" graceful-fs "^4.2.9" slash "^3.0.0" @@ -1945,10 +1998,10 @@ babel-plugin-istanbul@^6.1.1: istanbul-lib-instrument "^5.0.4" test-exclude "^6.0.0" -babel-plugin-jest-hoist@^29.5.0: - version "29.5.0" - resolved "https://registry.yarnpkg.com/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-29.5.0.tgz#a97db437936f441ec196990c9738d4b88538618a" - integrity sha512-zSuuuAlTMT4mzLj2nPnUm6fsE6270vdOfnpbJ+RmruU75UhLFvL0N2NgI7xpeS7NaB6hGqmd5pVpGTDYvi4Q3w== +babel-plugin-jest-hoist@^29.6.3: + version "29.6.3" + resolved "https://registry.yarnpkg.com/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-29.6.3.tgz#aadbe943464182a8922c3c927c3067ff40d24626" + integrity sha512-ESAc/RJvGTFEzRwOTT4+lNDk/GNHMkKbNzsvT0qKRfDyyYTskxB5rnU2njIDYVxXCBHHEI1c0YwHob3WaYujOg== dependencies: "@babel/template" "^7.3.3" "@babel/types" "^7.3.3" @@ -1973,12 +2026,12 @@ babel-preset-current-node-syntax@^1.0.0: "@babel/plugin-syntax-optional-chaining" "^7.8.3" "@babel/plugin-syntax-top-level-await" "^7.8.3" -babel-preset-jest@^29.5.0: - version "29.5.0" - resolved "https://registry.yarnpkg.com/babel-preset-jest/-/babel-preset-jest-29.5.0.tgz#57bc8cc88097af7ff6a5ab59d1cd29d52a5916e2" - integrity sha512-JOMloxOqdiBSxMAzjRaH023/vvcaSaec49zvg+2LmNsktC7ei39LTJGw02J+9uUtTZUq6xbLyJ4dxe9sSmIuAg== +babel-preset-jest@^29.6.3: + version "29.6.3" + resolved "https://registry.yarnpkg.com/babel-preset-jest/-/babel-preset-jest-29.6.3.tgz#fa05fa510e7d493896d7b0dd2033601c840f171c" + integrity sha512-0B3bhxR6snWXJZtR/RliHTDPRgn1sNHOR0yVtq/IiQFyuOVjFS+wuio/R4gSNkyYmKmJB4wGZv2NZanmKmTnNA== dependencies: - babel-plugin-jest-hoist "^29.5.0" + babel-plugin-jest-hoist "^29.6.3" babel-preset-current-node-syntax "^1.0.0" balanced-match@^1.0.0: @@ -2224,12 +2277,12 @@ cjs-module-lexer@^1.0.0: resolved "https://registry.yarnpkg.com/cjs-module-lexer/-/cjs-module-lexer-1.2.2.tgz#9f84ba3244a512f3a54e5277e8eef4c489864e40" integrity sha512-cOU9usZw8/dXIXKtwa8pM0OTJQuJkxMN6w30csNRUerHfeQ5R6U3kkU/FtJeIf3M202OHfY2U8ccInBG7/xogA== -class-transformer@^0.5.1: +class-transformer@~0.5.1: version "0.5.1" resolved "https://registry.yarnpkg.com/class-transformer/-/class-transformer-0.5.1.tgz#24147d5dffd2a6cea930a3250a677addf96ab336" integrity sha512-SQa1Ws6hUbfC98vKGxZH3KFY0Y1lm5Zm0SY8XX9zbK7FJCyVEac3ATW0RIpwzW+oOfmHE5PMPufDG9hCfoEOMw== -class-validator@^0.14.0: +class-validator@~0.14.0: version "0.14.0" resolved "https://registry.yarnpkg.com/class-validator/-/class-validator-0.14.0.tgz#40ed0ecf3c83b2a8a6a320f4edb607be0f0df159" integrity sha512-ct3ltplN8I9fOwUd8GrP8UQixwff129BkEtuWDKL5W45cQuLd19xqmTLu5ge78YDm/fdje6FMt0hGOhl0lii3A== @@ -2391,11 +2444,24 @@ convert-source-map@^2.0.0: resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-2.0.0.tgz#4b560f649fc4e918dd0ab75cf4961e8bc882d82a" integrity sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg== +cookie-parser@~1.4.6: + version "1.4.6" + resolved "https://registry.yarnpkg.com/cookie-parser/-/cookie-parser-1.4.6.tgz#3ac3a7d35a7a03bbc7e365073a26074824214594" + integrity sha512-z3IzaNjdwUC2olLIB5/ITd0/setiaFMLYiZJle7xg5Fe9KWAceil7xszYfHHBtDFYLSgJduS2Ty0P1uJdPDJeA== + dependencies: + cookie "0.4.1" + cookie-signature "1.0.6" + cookie-signature@1.0.6: version "1.0.6" resolved "https://registry.yarnpkg.com/cookie-signature/-/cookie-signature-1.0.6.tgz#e303a882b342cc3ee8ca513a79999734dab3ae2c" integrity sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ== +cookie@0.4.1: + version "0.4.1" + resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.4.1.tgz#afd713fe26ebd21ba95ceb61f9a8116e50a537d1" + integrity sha512-ZwrFkGJxUR3EIoXtO+yVE69Eb7KlixbaeAWfBQB9vVsNn/o+Yw69gBWSSDK825hQNdN+wF8zELf3dFNl/kxkUA== + cookie@0.5.0: version "0.5.0" resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.5.0.tgz#d1f5d71adec6558c58f389987c366aa47e994f8b" @@ -2475,7 +2541,7 @@ data-urls@^3.0.2: whatwg-mimetype "^3.0.0" whatwg-url "^11.0.0" -dayjs@^1.11.9: +dayjs@^1.11.9, dayjs@~1.11.9: version "1.11.9" resolved "https://registry.yarnpkg.com/dayjs/-/dayjs-1.11.9.tgz#9ca491933fadd0a60a2c19f6c237c03517d71d1a" integrity sha512-QvzAURSbQ0pKdIye2txOzNaHmxtUBXerpY0FJsFXUMKbIZeFm5ht1LS/jFsrncjnmtv8HsG0W2g6c0zUjZWmpA== @@ -2554,6 +2620,11 @@ diff-sequences@^29.4.3: resolved "https://registry.yarnpkg.com/diff-sequences/-/diff-sequences-29.4.3.tgz#9314bc1fabe09267ffeca9cbafc457d8499a13f2" integrity sha512-ofrBgwpPhCD85kMKtE9RYFFq6OC1A89oW2vvgWZNCwxrUpRUILopY7lsYyMDSjc8g6U6aiO0Qubg6r4Wgt5ZnA== +diff-sequences@^29.6.3: + version "29.6.3" + resolved "https://registry.yarnpkg.com/diff-sequences/-/diff-sequences-29.6.3.tgz#4deaf894d11407c51efc8418012f9e70b84ea921" + integrity sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q== + diff@^4.0.1: version "4.0.2" resolved "https://registry.yarnpkg.com/diff/-/diff-4.0.2.tgz#60f3aecb89d5fae520c11aa19efc2bb982aade7d" @@ -2897,7 +2968,7 @@ exit@^0.1.2: resolved "https://registry.yarnpkg.com/exit/-/exit-0.1.2.tgz#0632638f8d877cc82107d30a0fff1a17cba1cd0c" integrity sha512-Zk/eNKV2zbjpKzrsQ+n1G6poVbErQxJ0LBOJXaKZ1EViLzH+hrLu9cdXI4zw9dBQJslwBEpbQ2P1oS7nDxs6jQ== -expect@^29.0.0, expect@^29.6.2: +expect@^29.0.0: version "29.6.2" resolved "https://registry.yarnpkg.com/expect/-/expect-29.6.2.tgz#7b08e83eba18ddc4a2cf62b5f2d1918f5cd84521" integrity sha512-iAErsLxJ8C+S02QbLAwgSGSezLQK+XXRDt8IuFXFpwCNw2ECmzZSmjKcCaFVp5VRMk+WAvz6h6jokzEzBFZEuA== @@ -2909,6 +2980,17 @@ expect@^29.0.0, expect@^29.6.2: jest-message-util "^29.6.2" jest-util "^29.6.2" +expect@^29.6.4: + version "29.6.4" + resolved "https://registry.yarnpkg.com/expect/-/expect-29.6.4.tgz#a6e6f66d4613717859b2fe3da98a739437b6f4b8" + integrity sha512-F2W2UyQ8XYyftHT57dtfg8Ue3X5qLgm2sSug0ivvLRH/VKNRL/pDxg/TH7zVzbQB0tu80clNFy6LU7OS/VSEKA== + dependencies: + "@jest/expect-utils" "^29.6.4" + jest-get-type "^29.6.3" + jest-matcher-utils "^29.6.4" + jest-message-util "^29.6.3" + jest-util "^29.6.3" + express@4.18.1: version "4.18.1" resolved "https://registry.yarnpkg.com/express/-/express-4.18.1.tgz#7797de8b9c72c857b9cd0e14a5eea80666267caf" @@ -3078,7 +3160,7 @@ flatted@^3.1.0: resolved "https://registry.yarnpkg.com/flatted/-/flatted-3.2.7.tgz#609f39207cb614b89d0765b477cb2d437fbf9787" integrity sha512-5nqDSxl8nn5BSNxyR3n4I6eDmbolI6WT+QqR547RwxQapgjQBmtktdP+HTBb/a/zLsbzERTONyUB5pefh5TtjQ== -follow-redirects@^1.14.9, follow-redirects@^1.15.0: +follow-redirects@^1.14.8, follow-redirects@^1.14.9, follow-redirects@^1.15.0: version "1.15.2" resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.2.tgz#b460864144ba63f2681096f274c4e57026da2c13" integrity sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA== @@ -3632,7 +3714,7 @@ istanbul-lib-coverage@^3.0.0, istanbul-lib-coverage@^3.2.0: resolved "https://registry.yarnpkg.com/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.0.tgz#189e7909d0a39fa5a3dfad5b03f71947770191d3" integrity sha512-eOeJ5BHCmHYvQK7xt9GkdHuzuCGS1Y6g9Gvnx3Ym33fz/HpLRYxiS0wHNr+m/MBC8B647Xt608vCDEvhl9c6Mw== -istanbul-lib-instrument@^5.0.4, istanbul-lib-instrument@^5.1.0: +istanbul-lib-instrument@^5.0.4: version "5.2.1" resolved "https://registry.yarnpkg.com/istanbul-lib-instrument/-/istanbul-lib-instrument-5.2.1.tgz#d10c8885c2125574e1c231cacadf955675e1ce3d" integrity sha512-pzqtp31nLv/XFOzXGuvhCb8qhjmTVo5vjVk19XE4CRlSWz0KoeJ3bw9XsA7nOp9YBf4qHjwBxkDzKcME/J29Yg== @@ -3643,6 +3725,17 @@ istanbul-lib-instrument@^5.0.4, istanbul-lib-instrument@^5.1.0: istanbul-lib-coverage "^3.2.0" semver "^6.3.0" +istanbul-lib-instrument@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/istanbul-lib-instrument/-/istanbul-lib-instrument-6.0.0.tgz#7a8af094cbfff1d5bb280f62ce043695ae8dd5b8" + integrity sha512-x58orMzEVfzPUKqlbLd1hXCnySCxKdDKa6Rjg97CwuLLRI4g3FHTdnExu1OqffVFay6zeMW+T6/DowFLndWnIw== + dependencies: + "@babel/core" "^7.12.3" + "@babel/parser" "^7.14.7" + "@istanbuljs/schema" "^0.1.2" + istanbul-lib-coverage "^3.2.0" + semver "^7.5.4" + istanbul-lib-report@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/istanbul-lib-report/-/istanbul-lib-report-3.0.0.tgz#7518fe52ea44de372f460a76b5ecda9ffb73d8a6" @@ -3674,83 +3767,84 @@ iterare@1.2.1: resolved "https://registry.yarnpkg.com/iterare/-/iterare-1.2.1.tgz#139c400ff7363690e33abffa33cbba8920f00042" integrity sha512-RKYVTCjAnRthyJes037NX/IiqeidgN1xc3j1RjFfECFp28A1GVwK9nA+i0rJPaHqSZwygLzRnFlzUuHFoWWy+Q== -jest-changed-files@^29.5.0: - version "29.5.0" - resolved "https://registry.yarnpkg.com/jest-changed-files/-/jest-changed-files-29.5.0.tgz#e88786dca8bf2aa899ec4af7644e16d9dcf9b23e" - integrity sha512-IFG34IUMUaNBIxjQXF/iu7g6EcdMrGRRxaUSw92I/2g2YC6vCdTltl4nHvt7Ci5nSJwXIkCu8Ka1DKF+X7Z1Ag== +jest-changed-files@^29.6.3: + version "29.6.3" + resolved "https://registry.yarnpkg.com/jest-changed-files/-/jest-changed-files-29.6.3.tgz#97cfdc93f74fb8af2a1acb0b78f836f1fb40c449" + integrity sha512-G5wDnElqLa4/c66ma5PG9eRjE342lIbF6SUnTJi26C3J28Fv2TVY2rOyKB9YGbSA5ogwevgmxc4j4aVjrEK6Yg== dependencies: execa "^5.0.0" + jest-util "^29.6.3" p-limit "^3.1.0" -jest-circus@^29.6.2: - version "29.6.2" - resolved "https://registry.yarnpkg.com/jest-circus/-/jest-circus-29.6.2.tgz#1e6ffca60151ac66cad63fce34f443f6b5bb4258" - integrity sha512-G9mN+KOYIUe2sB9kpJkO9Bk18J4dTDArNFPwoZ7WKHKel55eKIS/u2bLthxgojwlf9NLCVQfgzM/WsOVvoC6Fw== +jest-circus@^29.6.4: + version "29.6.4" + resolved "https://registry.yarnpkg.com/jest-circus/-/jest-circus-29.6.4.tgz#f074c8d795e0cc0f2ebf0705086b1be6a9a8722f" + integrity sha512-YXNrRyntVUgDfZbjXWBMPslX1mQ8MrSG0oM/Y06j9EYubODIyHWP8hMUbjbZ19M3M+zamqEur7O80HODwACoJw== dependencies: - "@jest/environment" "^29.6.2" - "@jest/expect" "^29.6.2" - "@jest/test-result" "^29.6.2" - "@jest/types" "^29.6.1" + "@jest/environment" "^29.6.4" + "@jest/expect" "^29.6.4" + "@jest/test-result" "^29.6.4" + "@jest/types" "^29.6.3" "@types/node" "*" chalk "^4.0.0" co "^4.6.0" dedent "^1.0.0" is-generator-fn "^2.0.0" - jest-each "^29.6.2" - jest-matcher-utils "^29.6.2" - jest-message-util "^29.6.2" - jest-runtime "^29.6.2" - jest-snapshot "^29.6.2" - jest-util "^29.6.2" + jest-each "^29.6.3" + jest-matcher-utils "^29.6.4" + jest-message-util "^29.6.3" + jest-runtime "^29.6.4" + jest-snapshot "^29.6.4" + jest-util "^29.6.3" p-limit "^3.1.0" - pretty-format "^29.6.2" + pretty-format "^29.6.3" pure-rand "^6.0.0" slash "^3.0.0" stack-utils "^2.0.3" -jest-cli@^29.6.2: - version "29.6.2" - resolved "https://registry.yarnpkg.com/jest-cli/-/jest-cli-29.6.2.tgz#edb381763398d1a292cd1b636a98bfa5644b8fda" - integrity sha512-TT6O247v6dCEX2UGHGyflMpxhnrL0DNqP2fRTKYm3nJJpCTfXX3GCMQPGFjXDoj0i5/Blp3jriKXFgdfmbYB6Q== +jest-cli@^29.6.4: + version "29.6.4" + resolved "https://registry.yarnpkg.com/jest-cli/-/jest-cli-29.6.4.tgz#ad52f2dfa1b0291de7ec7f8d7c81ac435521ede0" + integrity sha512-+uMCQ7oizMmh8ZwRfZzKIEszFY9ksjjEQnTEMTaL7fYiL3Kw4XhqT9bYh+A4DQKUb67hZn2KbtEnDuHvcgK4pQ== dependencies: - "@jest/core" "^29.6.2" - "@jest/test-result" "^29.6.2" - "@jest/types" "^29.6.1" + "@jest/core" "^29.6.4" + "@jest/test-result" "^29.6.4" + "@jest/types" "^29.6.3" chalk "^4.0.0" exit "^0.1.2" graceful-fs "^4.2.9" import-local "^3.0.2" - jest-config "^29.6.2" - jest-util "^29.6.2" - jest-validate "^29.6.2" + jest-config "^29.6.4" + jest-util "^29.6.3" + jest-validate "^29.6.3" prompts "^2.0.1" yargs "^17.3.1" -jest-config@^29.6.2: - version "29.6.2" - resolved "https://registry.yarnpkg.com/jest-config/-/jest-config-29.6.2.tgz#c68723f06b31ca5e63030686e604727d406cd7c3" - integrity sha512-VxwFOC8gkiJbuodG9CPtMRjBUNZEHxwfQXmIudSTzFWxaci3Qub1ddTRbFNQlD/zUeaifLndh/eDccFX4wCMQw== +jest-config@^29.6.4: + version "29.6.4" + resolved "https://registry.yarnpkg.com/jest-config/-/jest-config-29.6.4.tgz#eff958ee41d4e1ee7a6106d02b74ad9fc427d79e" + integrity sha512-JWohr3i9m2cVpBumQFv2akMEnFEPVOh+9L2xIBJhJ0zOaci2ZXuKJj0tgMKQCBZAKA09H049IR4HVS/43Qb19A== dependencies: "@babel/core" "^7.11.6" - "@jest/test-sequencer" "^29.6.2" - "@jest/types" "^29.6.1" - babel-jest "^29.6.2" + "@jest/test-sequencer" "^29.6.4" + "@jest/types" "^29.6.3" + babel-jest "^29.6.4" chalk "^4.0.0" ci-info "^3.2.0" deepmerge "^4.2.2" glob "^7.1.3" graceful-fs "^4.2.9" - jest-circus "^29.6.2" - jest-environment-node "^29.6.2" - jest-get-type "^29.4.3" - jest-regex-util "^29.4.3" - jest-resolve "^29.6.2" - jest-runner "^29.6.2" - jest-util "^29.6.2" - jest-validate "^29.6.2" + jest-circus "^29.6.4" + jest-environment-node "^29.6.4" + jest-get-type "^29.6.3" + jest-regex-util "^29.6.3" + jest-resolve "^29.6.4" + jest-runner "^29.6.4" + jest-util "^29.6.3" + jest-validate "^29.6.3" micromatch "^4.0.4" parse-json "^5.2.0" - pretty-format "^29.6.2" + pretty-format "^29.6.3" slash "^3.0.0" strip-json-comments "^3.1.1" @@ -3764,81 +3858,96 @@ jest-diff@^29.6.2: jest-get-type "^29.4.3" pretty-format "^29.6.2" -jest-docblock@^29.4.3: - version "29.4.3" - resolved "https://registry.yarnpkg.com/jest-docblock/-/jest-docblock-29.4.3.tgz#90505aa89514a1c7dceeac1123df79e414636ea8" - integrity sha512-fzdTftThczeSD9nZ3fzA/4KkHtnmllawWrXO69vtI+L9WjEIuXWs4AmyME7lN5hU7dB0sHhuPfcKofRsUb/2Fg== +jest-diff@^29.6.4: + version "29.6.4" + resolved "https://registry.yarnpkg.com/jest-diff/-/jest-diff-29.6.4.tgz#85aaa6c92a79ae8cd9a54ebae8d5b6d9a513314a" + integrity sha512-9F48UxR9e4XOEZvoUXEHSWY4qC4zERJaOfrbBg9JpbJOO43R1vN76REt/aMGZoY6GD5g84nnJiBIVlscegefpw== dependencies: - detect-newline "^3.0.0" + chalk "^4.0.0" + diff-sequences "^29.6.3" + jest-get-type "^29.6.3" + pretty-format "^29.6.3" -jest-each@^29.6.2: - version "29.6.2" - resolved "https://registry.yarnpkg.com/jest-each/-/jest-each-29.6.2.tgz#c9e4b340bcbe838c73adf46b76817b15712d02ce" - integrity sha512-MsrsqA0Ia99cIpABBc3izS1ZYoYfhIy0NNWqPSE0YXbQjwchyt6B1HD2khzyPe1WiJA7hbxXy77ZoUQxn8UlSw== +jest-docblock@^29.6.3: + version "29.6.3" + resolved "https://registry.yarnpkg.com/jest-docblock/-/jest-docblock-29.6.3.tgz#293dca5188846c9f7c0c2b1bb33e5b11f21645f2" + integrity sha512-2+H+GOTQBEm2+qFSQ7Ma+BvyV+waiIFxmZF5LdpBsAEjWX8QYjSCa4FrkIYtbfXUJJJnFCYrOtt6TZ+IAiTjBQ== dependencies: - "@jest/types" "^29.6.1" - chalk "^4.0.0" - jest-get-type "^29.4.3" - jest-util "^29.6.2" - pretty-format "^29.6.2" + detect-newline "^3.0.0" -jest-environment-jsdom@^29.6.2: - version "29.6.2" - resolved "https://registry.yarnpkg.com/jest-environment-jsdom/-/jest-environment-jsdom-29.6.2.tgz#4fc68836a7774a771819a2f980cb47af3b1629da" - integrity sha512-7oa/+266AAEgkzae8i1awNEfTfjwawWKLpiw2XesZmaoVVj9u9t8JOYx18cG29rbPNtkUlZ8V4b5Jb36y/VxoQ== +jest-each@^29.6.3: + version "29.6.3" + resolved "https://registry.yarnpkg.com/jest-each/-/jest-each-29.6.3.tgz#1956f14f5f0cb8ae0b2e7cabc10bb03ec817c142" + integrity sha512-KoXfJ42k8cqbkfshW7sSHcdfnv5agDdHCPA87ZBdmHP+zJstTJc0ttQaJ/x7zK6noAL76hOuTIJ6ZkQRS5dcyg== dependencies: - "@jest/environment" "^29.6.2" - "@jest/fake-timers" "^29.6.2" - "@jest/types" "^29.6.1" + "@jest/types" "^29.6.3" + chalk "^4.0.0" + jest-get-type "^29.6.3" + jest-util "^29.6.3" + pretty-format "^29.6.3" + +jest-environment-jsdom@~29.6.2: + version "29.6.4" + resolved "https://registry.yarnpkg.com/jest-environment-jsdom/-/jest-environment-jsdom-29.6.4.tgz#0daf44454041f9e1ef7fa82eb1bd43426a82eb1c" + integrity sha512-K6wfgUJ16DoMs02JYFid9lOsqfpoVtyJxpRlnTxUHzvZWBnnh2VNGRB9EC1Cro96TQdq5TtSjb3qUjNaJP9IyA== + dependencies: + "@jest/environment" "^29.6.4" + "@jest/fake-timers" "^29.6.4" + "@jest/types" "^29.6.3" "@types/jsdom" "^20.0.0" "@types/node" "*" - jest-mock "^29.6.2" - jest-util "^29.6.2" + jest-mock "^29.6.3" + jest-util "^29.6.3" jsdom "^20.0.0" -jest-environment-node@^29.6.2: - version "29.6.2" - resolved "https://registry.yarnpkg.com/jest-environment-node/-/jest-environment-node-29.6.2.tgz#a9ea2cabff39b08eca14ccb32c8ceb924c8bb1ad" - integrity sha512-YGdFeZ3T9a+/612c5mTQIllvWkddPbYcN2v95ZH24oWMbGA4GGS2XdIF92QMhUhvrjjuQWYgUGW2zawOyH63MQ== +jest-environment-node@^29.6.4: + version "29.6.4" + resolved "https://registry.yarnpkg.com/jest-environment-node/-/jest-environment-node-29.6.4.tgz#4ce311549afd815d3cafb49e60a1e4b25f06d29f" + integrity sha512-i7SbpH2dEIFGNmxGCpSc2w9cA4qVD+wfvg2ZnfQ7XVrKL0NA5uDVBIiGH8SR4F0dKEv/0qI5r+aDomDf04DpEQ== dependencies: - "@jest/environment" "^29.6.2" - "@jest/fake-timers" "^29.6.2" - "@jest/types" "^29.6.1" + "@jest/environment" "^29.6.4" + "@jest/fake-timers" "^29.6.4" + "@jest/types" "^29.6.3" "@types/node" "*" - jest-mock "^29.6.2" - jest-util "^29.6.2" + jest-mock "^29.6.3" + jest-util "^29.6.3" jest-get-type@^29.4.3: version "29.4.3" resolved "https://registry.yarnpkg.com/jest-get-type/-/jest-get-type-29.4.3.tgz#1ab7a5207c995161100b5187159ca82dd48b3dd5" integrity sha512-J5Xez4nRRMjk8emnTpWrlkyb9pfRQQanDrvWHhsR1+VUfbwxi30eVcZFlcdGInRibU4G5LwHXpI7IRHU0CY+gg== -jest-haste-map@^29.6.2: - version "29.6.2" - resolved "https://registry.yarnpkg.com/jest-haste-map/-/jest-haste-map-29.6.2.tgz#298c25ea5255cfad8b723179d4295cf3a50a70d1" - integrity sha512-+51XleTDAAysvU8rT6AnS1ZJ+WHVNqhj1k6nTvN2PYP+HjU3kqlaKQ1Lnw3NYW3bm2r8vq82X0Z1nDDHZMzHVA== +jest-get-type@^29.6.3: + version "29.6.3" + resolved "https://registry.yarnpkg.com/jest-get-type/-/jest-get-type-29.6.3.tgz#36f499fdcea197c1045a127319c0481723908fd1" + integrity sha512-zrteXnqYxfQh7l5FHyL38jL39di8H8rHoecLH3JNxH3BwOrBsNeabdap5e0I23lD4HHI8W5VFBZqG4Eaq5LNcw== + +jest-haste-map@^29.6.4: + version "29.6.4" + resolved "https://registry.yarnpkg.com/jest-haste-map/-/jest-haste-map-29.6.4.tgz#97143ce833829157ea7025204b08f9ace609b96a" + integrity sha512-12Ad+VNTDHxKf7k+M65sviyynRoZYuL1/GTuhEVb8RYsNSNln71nANRb/faSyWvx0j+gHcivChXHIoMJrGYjog== dependencies: - "@jest/types" "^29.6.1" + "@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 "^29.4.3" - jest-util "^29.6.2" - jest-worker "^29.6.2" + jest-regex-util "^29.6.3" + jest-util "^29.6.3" + jest-worker "^29.6.4" micromatch "^4.0.4" walker "^1.0.8" optionalDependencies: fsevents "^2.3.2" -jest-leak-detector@^29.6.2: - version "29.6.2" - resolved "https://registry.yarnpkg.com/jest-leak-detector/-/jest-leak-detector-29.6.2.tgz#e2b307fee78cab091c37858a98c7e1d73cdf5b38" - integrity sha512-aNqYhfp5uYEO3tdWMb2bfWv6f0b4I0LOxVRpnRLAeque2uqOVVMLh6khnTcE2qJ5wAKop0HcreM1btoysD6bPQ== +jest-leak-detector@^29.6.3: + version "29.6.3" + resolved "https://registry.yarnpkg.com/jest-leak-detector/-/jest-leak-detector-29.6.3.tgz#b9661bc3aec8874e59aff361fa0c6d7cd507ea01" + integrity sha512-0kfbESIHXYdhAdpLsW7xdwmYhLf1BRu4AA118/OxFm0Ho1b2RcTmO4oF6aAMaxpxdxnJ3zve2rgwzNBD4Zbm7Q== dependencies: - jest-get-type "^29.4.3" - pretty-format "^29.6.2" + jest-get-type "^29.6.3" + pretty-format "^29.6.3" jest-matcher-utils@^29.6.2: version "29.6.2" @@ -3850,6 +3959,16 @@ jest-matcher-utils@^29.6.2: jest-get-type "^29.4.3" pretty-format "^29.6.2" +jest-matcher-utils@^29.6.4: + version "29.6.4" + resolved "https://registry.yarnpkg.com/jest-matcher-utils/-/jest-matcher-utils-29.6.4.tgz#327db7ababea49455df3b23e5d6109fe0c709d24" + integrity sha512-KSzwyzGvK4HcfnserYqJHYi7sZVqdREJ9DMPAKVbS98JsIAvumihaNUbjrWw0St7p9IY7A9UskCW5MYlGmBQFQ== + dependencies: + chalk "^4.0.0" + jest-diff "^29.6.4" + jest-get-type "^29.6.3" + pretty-format "^29.6.3" + jest-message-util@^29.6.2: version "29.6.2" resolved "https://registry.yarnpkg.com/jest-message-util/-/jest-message-util-29.6.2.tgz#af7adc2209c552f3f5ae31e77cf0a261f23dc2bb" @@ -3865,127 +3984,142 @@ jest-message-util@^29.6.2: slash "^3.0.0" stack-utils "^2.0.3" -jest-mock@^29.6.2: - version "29.6.2" - resolved "https://registry.yarnpkg.com/jest-mock/-/jest-mock-29.6.2.tgz#ef9c9b4d38c34a2ad61010a021866dad41ce5e00" - integrity sha512-hoSv3lb3byzdKfwqCuT6uTscan471GUECqgNYykg6ob0yiAw3zYc7OrPnI9Qv8Wwoa4lC7AZ9hyS4AiIx5U2zg== +jest-message-util@^29.6.3: + version "29.6.3" + resolved "https://registry.yarnpkg.com/jest-message-util/-/jest-message-util-29.6.3.tgz#bce16050d86801b165f20cfde34dc01d3cf85fbf" + integrity sha512-FtzaEEHzjDpQp51HX4UMkPZjy46ati4T5pEMyM6Ik48ztu4T9LQplZ6OsimHx7EuM9dfEh5HJa6D3trEftu3dA== dependencies: - "@jest/types" "^29.6.1" + "@babel/code-frame" "^7.12.13" + "@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 "^29.6.3" + slash "^3.0.0" + stack-utils "^2.0.3" + +jest-mock@^29.6.3: + version "29.6.3" + resolved "https://registry.yarnpkg.com/jest-mock/-/jest-mock-29.6.3.tgz#433f3fd528c8ec5a76860177484940628bdf5e0a" + integrity sha512-Z7Gs/mOyTSR4yPsaZ72a/MtuK6RnC3JYqWONe48oLaoEcYwEDxqvbXz85G4SJrm2Z5Ar9zp6MiHF4AlFlRM4Pg== + dependencies: + "@jest/types" "^29.6.3" "@types/node" "*" - jest-util "^29.6.2" + jest-util "^29.6.3" jest-pnp-resolver@^1.2.2: version "1.2.3" resolved "https://registry.yarnpkg.com/jest-pnp-resolver/-/jest-pnp-resolver-1.2.3.tgz#930b1546164d4ad5937d5540e711d4d38d4cad2e" integrity sha512-+3NpwQEnRoIBtx4fyhblQDPgJI0H1IEIkX7ShLUjPGA7TtUTvI1oiKi3SR4oBR0hQhQR80l4WAe5RrXBwWMA8w== -jest-regex-util@^29.4.3: - version "29.4.3" - resolved "https://registry.yarnpkg.com/jest-regex-util/-/jest-regex-util-29.4.3.tgz#a42616141e0cae052cfa32c169945d00c0aa0bb8" - integrity sha512-O4FglZaMmWXbGHSQInfXewIsd1LMn9p3ZXB/6r4FOkyhX2/iP/soMG98jGvk/A3HAN78+5VWcBGO0BJAPRh4kg== +jest-regex-util@^29.6.3: + version "29.6.3" + resolved "https://registry.yarnpkg.com/jest-regex-util/-/jest-regex-util-29.6.3.tgz#4a556d9c776af68e1c5f48194f4d0327d24e8a52" + integrity sha512-KJJBsRCyyLNWCNBOvZyRDnAIfUiRJ8v+hOBQYGn8gDyF3UegwiP4gwRR3/SDa42g1YbVycTidUF3rKjyLFDWbg== -jest-resolve-dependencies@^29.6.2: - version "29.6.2" - resolved "https://registry.yarnpkg.com/jest-resolve-dependencies/-/jest-resolve-dependencies-29.6.2.tgz#36435269b6672c256bcc85fb384872c134cc4cf2" - integrity sha512-LGqjDWxg2fuQQm7ypDxduLu/m4+4Lb4gczc13v51VMZbVP5tSBILqVx8qfWcsdP8f0G7aIqByIALDB0R93yL+w== +jest-resolve-dependencies@^29.6.4: + version "29.6.4" + resolved "https://registry.yarnpkg.com/jest-resolve-dependencies/-/jest-resolve-dependencies-29.6.4.tgz#20156b33c7eacbb6bb77aeba4bed0eab4a3f8734" + integrity sha512-7+6eAmr1ZBF3vOAJVsfLj1QdqeXG+WYhidfLHBRZqGN24MFRIiKG20ItpLw2qRAsW/D2ZUUmCNf6irUr/v6KHA== dependencies: - jest-regex-util "^29.4.3" - jest-snapshot "^29.6.2" + jest-regex-util "^29.6.3" + jest-snapshot "^29.6.4" -jest-resolve@^29.6.2: - version "29.6.2" - resolved "https://registry.yarnpkg.com/jest-resolve/-/jest-resolve-29.6.2.tgz#f18405fe4b50159b7b6d85e81f6a524d22afb838" - integrity sha512-G/iQUvZWI5e3SMFssc4ug4dH0aZiZpsDq9o1PtXTV1210Ztyb2+w+ZgQkB3iOiC5SmAEzJBOHWz6Hvrd+QnNPw== +jest-resolve@^29.6.4: + version "29.6.4" + resolved "https://registry.yarnpkg.com/jest-resolve/-/jest-resolve-29.6.4.tgz#e34cb06f2178b429c38455d98d1a07572ac9faa3" + integrity sha512-fPRq+0vcxsuGlG0O3gyoqGTAxasagOxEuyoxHeyxaZbc9QNek0AmJWSkhjlMG+mTsj+8knc/mWb3fXlRNVih7Q== dependencies: chalk "^4.0.0" graceful-fs "^4.2.9" - jest-haste-map "^29.6.2" + jest-haste-map "^29.6.4" jest-pnp-resolver "^1.2.2" - jest-util "^29.6.2" - jest-validate "^29.6.2" + jest-util "^29.6.3" + jest-validate "^29.6.3" resolve "^1.20.0" resolve.exports "^2.0.0" slash "^3.0.0" -jest-runner@^29.6.2: - version "29.6.2" - resolved "https://registry.yarnpkg.com/jest-runner/-/jest-runner-29.6.2.tgz#89e8e32a8fef24781a7c4c49cd1cb6358ac7fc01" - integrity sha512-wXOT/a0EspYgfMiYHxwGLPCZfC0c38MivAlb2lMEAlwHINKemrttu1uSbcGbfDV31sFaPWnWJPmb2qXM8pqZ4w== +jest-runner@^29.6.4: + version "29.6.4" + resolved "https://registry.yarnpkg.com/jest-runner/-/jest-runner-29.6.4.tgz#b3b8ccb85970fde0fae40c73ee11eb75adccfacf" + integrity sha512-SDaLrMmtVlQYDuG0iSPYLycG8P9jLI+fRm8AF/xPKhYDB2g6xDWjXBrR5M8gEWsK6KVFlebpZ4QsrxdyIX1Jaw== dependencies: - "@jest/console" "^29.6.2" - "@jest/environment" "^29.6.2" - "@jest/test-result" "^29.6.2" - "@jest/transform" "^29.6.2" - "@jest/types" "^29.6.1" + "@jest/console" "^29.6.4" + "@jest/environment" "^29.6.4" + "@jest/test-result" "^29.6.4" + "@jest/transform" "^29.6.4" + "@jest/types" "^29.6.3" "@types/node" "*" chalk "^4.0.0" emittery "^0.13.1" graceful-fs "^4.2.9" - jest-docblock "^29.4.3" - jest-environment-node "^29.6.2" - jest-haste-map "^29.6.2" - jest-leak-detector "^29.6.2" - jest-message-util "^29.6.2" - jest-resolve "^29.6.2" - jest-runtime "^29.6.2" - jest-util "^29.6.2" - jest-watcher "^29.6.2" - jest-worker "^29.6.2" + jest-docblock "^29.6.3" + jest-environment-node "^29.6.4" + jest-haste-map "^29.6.4" + jest-leak-detector "^29.6.3" + jest-message-util "^29.6.3" + jest-resolve "^29.6.4" + jest-runtime "^29.6.4" + jest-util "^29.6.3" + jest-watcher "^29.6.4" + jest-worker "^29.6.4" p-limit "^3.1.0" source-map-support "0.5.13" -jest-runtime@^29.6.2: - version "29.6.2" - resolved "https://registry.yarnpkg.com/jest-runtime/-/jest-runtime-29.6.2.tgz#692f25e387f982e89ab83270e684a9786248e545" - integrity sha512-2X9dqK768KufGJyIeLmIzToDmsN0m7Iek8QNxRSI/2+iPFYHF0jTwlO3ftn7gdKd98G/VQw9XJCk77rbTGZnJg== - dependencies: - "@jest/environment" "^29.6.2" - "@jest/fake-timers" "^29.6.2" - "@jest/globals" "^29.6.2" - "@jest/source-map" "^29.6.0" - "@jest/test-result" "^29.6.2" - "@jest/transform" "^29.6.2" - "@jest/types" "^29.6.1" +jest-runtime@^29.6.4: + version "29.6.4" + resolved "https://registry.yarnpkg.com/jest-runtime/-/jest-runtime-29.6.4.tgz#b0bc495c9b6b12a0a7042ac34ca9bb85f8cd0ded" + integrity sha512-s/QxMBLvmwLdchKEjcLfwzP7h+jsHvNEtxGP5P+Fl1FMaJX2jMiIqe4rJw4tFprzCwuSvVUo9bn0uj4gNRXsbA== + dependencies: + "@jest/environment" "^29.6.4" + "@jest/fake-timers" "^29.6.4" + "@jest/globals" "^29.6.4" + "@jest/source-map" "^29.6.3" + "@jest/test-result" "^29.6.4" + "@jest/transform" "^29.6.4" + "@jest/types" "^29.6.3" "@types/node" "*" chalk "^4.0.0" cjs-module-lexer "^1.0.0" collect-v8-coverage "^1.0.0" glob "^7.1.3" graceful-fs "^4.2.9" - jest-haste-map "^29.6.2" - jest-message-util "^29.6.2" - jest-mock "^29.6.2" - jest-regex-util "^29.4.3" - jest-resolve "^29.6.2" - jest-snapshot "^29.6.2" - jest-util "^29.6.2" + jest-haste-map "^29.6.4" + jest-message-util "^29.6.3" + jest-mock "^29.6.3" + jest-regex-util "^29.6.3" + jest-resolve "^29.6.4" + jest-snapshot "^29.6.4" + jest-util "^29.6.3" slash "^3.0.0" strip-bom "^4.0.0" -jest-snapshot@^29.6.2: - version "29.6.2" - resolved "https://registry.yarnpkg.com/jest-snapshot/-/jest-snapshot-29.6.2.tgz#9b431b561a83f2bdfe041e1cab8a6becdb01af9c" - integrity sha512-1OdjqvqmRdGNvWXr/YZHuyhh5DeaLp1p/F8Tht/MrMw4Kr1Uu/j4lRG+iKl1DAqUJDWxtQBMk41Lnf/JETYBRA== +jest-snapshot@^29.6.4: + version "29.6.4" + resolved "https://registry.yarnpkg.com/jest-snapshot/-/jest-snapshot-29.6.4.tgz#9833eb6b66ff1541c7fd8ceaa42d541f407b4876" + integrity sha512-VC1N8ED7+4uboUKGIDsbvNAZb6LakgIPgAF4RSpF13dN6YaMokfRqO+BaqK4zIh6X3JffgwbzuGqDEjHm/MrvA== 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/types" "^7.3.3" - "@jest/expect-utils" "^29.6.2" - "@jest/transform" "^29.6.2" - "@jest/types" "^29.6.1" + "@jest/expect-utils" "^29.6.4" + "@jest/transform" "^29.6.4" + "@jest/types" "^29.6.3" babel-preset-current-node-syntax "^1.0.0" chalk "^4.0.0" - expect "^29.6.2" + expect "^29.6.4" graceful-fs "^4.2.9" - jest-diff "^29.6.2" - jest-get-type "^29.4.3" - jest-matcher-utils "^29.6.2" - jest-message-util "^29.6.2" - jest-util "^29.6.2" + jest-diff "^29.6.4" + jest-get-type "^29.6.3" + jest-matcher-utils "^29.6.4" + jest-message-util "^29.6.3" + jest-util "^29.6.3" natural-compare "^1.4.0" - pretty-format "^29.6.2" + pretty-format "^29.6.3" semver "^7.5.3" jest-util@^29.0.0, jest-util@^29.6.2: @@ -4000,30 +4134,42 @@ jest-util@^29.0.0, jest-util@^29.6.2: graceful-fs "^4.2.9" picomatch "^2.2.3" -jest-validate@^29.6.2: - version "29.6.2" - resolved "https://registry.yarnpkg.com/jest-validate/-/jest-validate-29.6.2.tgz#25d972af35b2415b83b1373baf1a47bb266c1082" - integrity sha512-vGz0yMN5fUFRRbpJDPwxMpgSXW1LDKROHfBopAvDcmD6s+B/s8WJrwi+4bfH4SdInBA5C3P3BI19dBtKzx1Arg== +jest-util@^29.6.3: + version "29.6.3" + resolved "https://registry.yarnpkg.com/jest-util/-/jest-util-29.6.3.tgz#e15c3eac8716440d1ed076f09bc63ace1aebca63" + integrity sha512-QUjna/xSy4B32fzcKTSz1w7YYzgiHrjjJjevdRf61HYk998R5vVMMNmrHESYZVDS5DSWs+1srPLPKxXPkeSDOA== dependencies: - "@jest/types" "^29.6.1" + "@jest/types" "^29.6.3" + "@types/node" "*" + chalk "^4.0.0" + ci-info "^3.2.0" + graceful-fs "^4.2.9" + picomatch "^2.2.3" + +jest-validate@^29.6.3: + version "29.6.3" + resolved "https://registry.yarnpkg.com/jest-validate/-/jest-validate-29.6.3.tgz#a75fca774cfb1c5758c70d035d30a1f9c2784b4d" + integrity sha512-e7KWZcAIX+2W1o3cHfnqpGajdCs1jSM3DkXjGeLSNmCazv1EeI1ggTeK5wdZhF+7N+g44JI2Od3veojoaumlfg== + dependencies: + "@jest/types" "^29.6.3" camelcase "^6.2.0" chalk "^4.0.0" - jest-get-type "^29.4.3" + jest-get-type "^29.6.3" leven "^3.1.0" - pretty-format "^29.6.2" + pretty-format "^29.6.3" -jest-watcher@^29.6.2: - version "29.6.2" - resolved "https://registry.yarnpkg.com/jest-watcher/-/jest-watcher-29.6.2.tgz#77c224674f0620d9f6643c4cfca186d8893ca088" - integrity sha512-GZitlqkMkhkefjfN/p3SJjrDaxPflqxEAv3/ik10OirZqJGYH5rPiIsgVcfof0Tdqg3shQGdEIxDBx+B4tuLzA== +jest-watcher@^29.6.4: + version "29.6.4" + resolved "https://registry.yarnpkg.com/jest-watcher/-/jest-watcher-29.6.4.tgz#633eb515ae284aa67fd6831f1c9d1b534cf0e0ba" + integrity sha512-oqUWvx6+On04ShsT00Ir9T4/FvBeEh2M9PTubgITPxDa739p4hoQweWPRGyYeaojgT0xTpZKF0Y/rSY1UgMxvQ== dependencies: - "@jest/test-result" "^29.6.2" - "@jest/types" "^29.6.1" + "@jest/test-result" "^29.6.4" + "@jest/types" "^29.6.3" "@types/node" "*" ansi-escapes "^4.2.1" chalk "^4.0.0" emittery "^0.13.1" - jest-util "^29.6.2" + jest-util "^29.6.3" string-length "^4.0.1" jest-worker@^27.4.5: @@ -4035,25 +4181,25 @@ jest-worker@^27.4.5: merge-stream "^2.0.0" supports-color "^8.0.0" -jest-worker@^29.6.2: - version "29.6.2" - resolved "https://registry.yarnpkg.com/jest-worker/-/jest-worker-29.6.2.tgz#682fbc4b6856ad0aa122a5403c6d048b83f3fb44" - integrity sha512-l3ccBOabTdkng8I/ORCkADz4eSMKejTYv1vB/Z83UiubqhC1oQ5Li6dWCyqOIvSifGjUBxuvxvlm6KGK2DtuAQ== +jest-worker@^29.6.4: + version "29.6.4" + resolved "https://registry.yarnpkg.com/jest-worker/-/jest-worker-29.6.4.tgz#f34279f4afc33c872b470d4af21b281ac616abd3" + integrity sha512-6dpvFV4WjcWbDVGgHTWo/aupl8/LbBx2NSKfiwqf79xC/yeJjKHT1+StcKy/2KTmW16hE68ccKVOtXf+WZGz7Q== dependencies: "@types/node" "*" - jest-util "^29.6.2" + jest-util "^29.6.3" merge-stream "^2.0.0" supports-color "^8.0.0" -jest@^29.6.2: - version "29.6.2" - resolved "https://registry.yarnpkg.com/jest/-/jest-29.6.2.tgz#3bd55b9fd46a161b2edbdf5f1d1bd0d1eab76c42" - integrity sha512-8eQg2mqFbaP7CwfsTpCxQ+sHzw1WuNWL5UUvjnWP4hx2riGz9fPSzYOaU5q8/GqWn1TfgZIVTqYJygbGbWAANg== +jest@~29.6.2: + version "29.6.4" + resolved "https://registry.yarnpkg.com/jest/-/jest-29.6.4.tgz#7c48e67a445ba264b778253b5d78d4ebc9d0a622" + integrity sha512-tEFhVQFF/bzoYV1YuGyzLPZ6vlPrdfvDmmAxudA1dLEuiztqg2Rkx20vkKY32xiDROcD2KXlgZ7Cu8RPeEHRKw== dependencies: - "@jest/core" "^29.6.2" - "@jest/types" "^29.6.1" + "@jest/core" "^29.6.4" + "@jest/types" "^29.6.3" import-local "^3.0.2" - jest-cli "^29.6.2" + jest-cli "^29.6.4" js-tokens@^4.0.0: version "4.0.0" @@ -4193,7 +4339,17 @@ jsonfile@^6.0.1: optionalDependencies: graceful-fs "^4.1.6" -jsonwebtoken@^9.0.1: +jsonwebtoken@9.0.0: + version "9.0.0" + resolved "https://registry.yarnpkg.com/jsonwebtoken/-/jsonwebtoken-9.0.0.tgz#d0faf9ba1cc3a56255fe49c0961a67e520c1926d" + integrity sha512-tuGfYXxkQGDPnLJ7SibiQgVgeDgfbPq2k2ICcbgqW8WxWLBAxKQM/ZCu/IT8SOSwmaYl4dpTFCW5xZv7YbbWUw== + dependencies: + jws "^3.2.2" + lodash "^4.17.21" + ms "^2.1.1" + semver "^7.3.8" + +jsonwebtoken@^9.0.0: version "9.0.1" resolved "https://registry.yarnpkg.com/jsonwebtoken/-/jsonwebtoken-9.0.1.tgz#81d8c901c112c24e497a55daf6b2be1225b40145" integrity sha512-K8wx7eJ5TPvEjuiVSkv167EVboBDv9PZdDoF7BgeQnBLVvZWW9clr2PsQHVJDTKaEIH5JBIwHujGcHp7GgI2eg== @@ -4203,6 +4359,22 @@ jsonwebtoken@^9.0.1: ms "^2.1.1" semver "^7.3.8" +jsonwebtoken@~9.0.1: + version "9.0.2" + resolved "https://registry.yarnpkg.com/jsonwebtoken/-/jsonwebtoken-9.0.2.tgz#65ff91f4abef1784697d40952bb1998c504caaf3" + integrity sha512-PRp66vJ865SSqOlgqS8hujT5U4AOgMfhrwYIuIhfKaoSCZcirrmASQr8CX7cUg+RMih+hgznrjp99o+W4pJLHQ== + dependencies: + jws "^3.2.2" + lodash.includes "^4.3.0" + lodash.isboolean "^3.0.3" + lodash.isinteger "^4.0.4" + lodash.isnumber "^3.0.3" + lodash.isplainobject "^4.0.6" + lodash.isstring "^4.0.1" + lodash.once "^4.0.0" + ms "^2.1.1" + semver "^7.5.4" + jwa@^1.4.1: version "1.4.1" resolved "https://registry.yarnpkg.com/jwa/-/jwa-1.4.1.tgz#743c32985cb9e98655530d53641b66c8645b039a" @@ -4311,6 +4483,36 @@ lodash.camelcase@^4.3.0: resolved "https://registry.yarnpkg.com/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz#b28aa6288a2b9fc651035c7711f65ab6190331a6" integrity sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA== +lodash.includes@^4.3.0: + version "4.3.0" + resolved "https://registry.yarnpkg.com/lodash.includes/-/lodash.includes-4.3.0.tgz#60bb98a87cb923c68ca1e51325483314849f553f" + integrity sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w== + +lodash.isboolean@^3.0.3: + version "3.0.3" + resolved "https://registry.yarnpkg.com/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz#6c2e171db2a257cd96802fd43b01b20d5f5870f6" + integrity sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg== + +lodash.isinteger@^4.0.4: + version "4.0.4" + resolved "https://registry.yarnpkg.com/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz#619c0af3d03f8b04c31f5882840b77b11cd68343" + integrity sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA== + +lodash.isnumber@^3.0.3: + version "3.0.3" + resolved "https://registry.yarnpkg.com/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz#3ce76810c5928d03352301ac287317f11c0b1ffc" + integrity sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw== + +lodash.isplainobject@^4.0.6: + version "4.0.6" + resolved "https://registry.yarnpkg.com/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz#7c526a52d89b45c45cc690b88163be0497f550cb" + integrity sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA== + +lodash.isstring@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/lodash.isstring/-/lodash.isstring-4.0.1.tgz#d527dfb5456eca7cc9bb95d5daeaf88ba54a5451" + integrity sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw== + lodash.memoize@4.x: version "4.1.2" resolved "https://registry.yarnpkg.com/lodash.memoize/-/lodash.memoize-4.1.2.tgz#bcc6c49a42a2840ed997f323eada5ecd182e0bfe" @@ -4321,7 +4523,12 @@ lodash.merge@^4.6.2: resolved "https://registry.yarnpkg.com/lodash.merge/-/lodash.merge-4.6.2.tgz#558aa53b43b661e1925a0afdfa36a9a1085fe57a" integrity sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ== -lodash@4.17.21, lodash@^4.17.15, lodash@^4.17.19, lodash@^4.17.21: +lodash.once@^4.0.0: + version "4.1.1" + resolved "https://registry.yarnpkg.com/lodash.once/-/lodash.once-4.1.1.tgz#0dd3971213c7c56df880977d504c88fb471a97ac" + integrity sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg== + +lodash@4.17.21, lodash@^4.17.15, lodash@^4.17.19, lodash@^4.17.21, lodash@~4.17.21: version "4.17.21" resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c" integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg== @@ -4795,6 +5002,35 @@ pascalcase@^0.1.1: resolved "https://registry.yarnpkg.com/pascalcase/-/pascalcase-0.1.1.tgz#b363e55e8006ca6fe21784d2db22bd15d7917f14" integrity sha512-XHXfu/yOQRy9vYOtUDVMN60OEJjW013GoObG1o+xwQTpB9eYJX/BjXMsdW13ZDPruFhYYn0AG22w0xgQMwl3Nw== +passport-jwt@~4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/passport-jwt/-/passport-jwt-4.0.1.tgz#c443795eff322c38d173faa0a3c481479646ec3d" + integrity sha512-UCKMDYhNuGOBE9/9Ycuoyh7vP6jpeTp/+sfMJl7nLff/t6dps+iaeE0hhNkKN8/HZHcJ7lCdOyDxHdDoxoSvdQ== + dependencies: + jsonwebtoken "^9.0.0" + passport-strategy "^1.0.0" + +passport-local@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/passport-local/-/passport-local-1.0.0.tgz#1fe63268c92e75606626437e3b906662c15ba6ee" + integrity sha512-9wCE6qKznvf9mQYYbgJ3sVOHmCWoUNMVFoZzNoznmISbhnNNPhN9xfY3sLmScHMetEJeoY7CXwfhCe7argfQow== + dependencies: + passport-strategy "1.x.x" + +passport-strategy@1.x.x, passport-strategy@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/passport-strategy/-/passport-strategy-1.0.0.tgz#b5539aa8fc225a3d1ad179476ddf236b440f52e4" + integrity sha512-CB97UUvDKJde2V0KDWWB3lyf6PC3FaZP7YxZ2G8OAtn9p4HI9j9JLP9qjOGZFvyl8uwNT8qM+hGnz/n16NI7oA== + +passport@~0.6.0: + version "0.6.0" + resolved "https://registry.yarnpkg.com/passport/-/passport-0.6.0.tgz#e869579fab465b5c0b291e841e6cc95c005fac9d" + integrity sha512-0fe+p3ZnrWRW74fe8+SvCyf4a3Pb2/h7gFkQ8yTJpAO50gDzlfjZUZTO1k5Eg9kUct22OxHLqDZoKUWRHOh9ug== + dependencies: + passport-strategy "1.x.x" + pause "0.0.1" + utils-merge "^1.0.1" + path-exists@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-4.0.0.tgz#513bdbe2d3b95d7762e8c1137efa195c6c61b5b3" @@ -4830,6 +5066,11 @@ path-type@^4.0.0: resolved "https://registry.yarnpkg.com/path-type/-/path-type-4.0.0.tgz#84ed01c0a7ba380afe09d90a8c180dcd9d03043b" integrity sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw== +pause@0.0.1: + version "0.0.1" + resolved "https://registry.yarnpkg.com/pause/-/pause-0.0.1.tgz#1d408b3fdb76923b9543d96fb4c9dfd535d9cb5d" + integrity sha512-KG8UEiEVkR3wGEb4m5yZkVCzigAD+cVEJck2CzYZO37ZGJfctvVptVO192MwrtPhzONn6go8ylnOdMhKqi4nfg== + picocolors@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.0.0.tgz#cb5bdc74ff3f51892236eaf79d68bc44564ab81c" @@ -4893,6 +5134,15 @@ pretty-format@^29.0.0, pretty-format@^29.6.2: ansi-styles "^5.0.0" react-is "^18.0.0" +pretty-format@^29.6.3: + version "29.6.3" + resolved "https://registry.yarnpkg.com/pretty-format/-/pretty-format-29.6.3.tgz#d432bb4f1ca6f9463410c3fb25a0ba88e594ace7" + integrity sha512-ZsBgjVhFAj5KeK+nHfF1305/By3lechHQSMWCTl8iHSbfOm2TN5nHEtFc/+W7fAyUeCs2n5iow72gld4gW0xDw== + dependencies: + "@jest/schemas" "^29.6.3" + ansi-styles "^5.0.0" + react-is "^18.0.0" + prisma@^5.0.0: version "5.0.0" resolved "https://registry.yarnpkg.com/prisma/-/prisma-5.0.0.tgz#f6571c46dc2478172cb7bc1bb62d74026a2c2630" @@ -5002,7 +5252,7 @@ qs@6.10.3: dependencies: side-channel "^1.0.4" -qs@^6.11.0, qs@^6.11.2: +qs@^6.11.0, qs@^6.9.4, qs@~6.11.2: version "6.11.2" resolved "https://registry.yarnpkg.com/qs/-/qs-6.11.2.tgz#64bea51f12c1f5da1bc01496f48ffcff7c69d7d9" integrity sha512-tDNIz22aBzCDxLtVH++VnTfzxlfeK5CbqohpSqpJgj1Wg/cQbStNAz3NuqCs5vV+pjBsK4x4pN9HlVh7rcYRiA== @@ -5082,7 +5332,7 @@ rechoir@^0.6.2: dependencies: resolve "^1.1.6" -reflect-metadata@^0.1.13: +reflect-metadata@~0.1.13: version "0.1.13" resolved "https://registry.yarnpkg.com/reflect-metadata/-/reflect-metadata-0.1.13.tgz#67ae3ca57c972a2aa1642b10fe363fe32d49dc08" integrity sha512-Ts1Y/anZELhSsjMcU605fU9RE4Oi3p5ORujwbIKXfWa+0Zxs510Qrmrce5/Jowq3cHSZSJqBjypxmHarc+vEWg== @@ -5187,7 +5437,7 @@ rxjs@6.6.7, rxjs@^6.6.0: dependencies: tslib "^1.9.0" -rxjs@^7.2.0, rxjs@^7.8.1: +rxjs@^7.2.0, rxjs@~7.8.1: version "7.8.1" resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-7.8.1.tgz#6f6f3d99ea8044291efd92e7c7fcf562c4057543" integrity sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg== @@ -5225,6 +5475,11 @@ schema-utils@^3.1.0, schema-utils@^3.1.1: ajv "^6.12.5" ajv-keywords "^3.5.2" +scmp@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/scmp/-/scmp-2.1.0.tgz#37b8e197c425bdeb570ab91cc356b311a11f9c9a" + integrity sha512-o/mRQGk9Rcer/jEEw/yw4mwo3EU/NvYvp577/Btqrym9Qy5/MdWGBqipbALgd2lrdWTJ5/gqDusxfnQBxOxT2Q== + semver@^6.0.0, semver@^6.3.0: version "6.3.0" resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.0.tgz#ee0a64c8af5e8ceea67687b133761e1becbd1d3d" @@ -5249,7 +5504,7 @@ semver@^7.3.4, semver@^7.3.5: dependencies: lru-cache "^6.0.0" -semver@^7.5.3: +semver@^7.5.3, semver@^7.5.4: version "7.5.4" resolved "https://registry.yarnpkg.com/semver/-/semver-7.5.4.tgz#483986ec4ed38e1c6c48c34894a9182dbff68a6e" integrity sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA== @@ -5350,7 +5605,7 @@ source-map-support@0.5.13: buffer-from "^1.0.0" source-map "^0.6.0" -source-map-support@0.5.21, source-map-support@^0.5.20, source-map-support@~0.5.20: +source-map-support@0.5.21, source-map-support@~0.5.20: version "0.5.21" resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.5.21.tgz#04fe7c7f9e1ed2d662233c28cb2b35b9f63f6e4f" integrity sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w== @@ -5525,10 +5780,10 @@ supports-preserve-symlinks-flag@^1.0.0: resolved "https://registry.yarnpkg.com/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz#6eda4bd344a3c94aea376d4cc31bc77311039e09" integrity sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w== -swagger-axios-codegen@^0.15.11: - version "0.15.11" - resolved "https://registry.yarnpkg.com/swagger-axios-codegen/-/swagger-axios-codegen-0.15.11.tgz#10c4c2314e454a5e28237a2036cd210142466ea0" - integrity sha512-XRDRYLmlydVyFjyf31Vsla40gH/yHg/Gn7nBeH6IKVIsTzqUskRN9ZusDMJPYAipKt+wuF+CduubHqn5RGyR4Q== +swagger-axios-codegen@~0.15.11: + version "0.15.12" + resolved "https://registry.yarnpkg.com/swagger-axios-codegen/-/swagger-axios-codegen-0.15.12.tgz#ee7d516ca8923aa949c03685453d6dcc3e4edd9a" + integrity sha512-VIm9ek12L4BuOcHNcJUYNP2W87E2Du5kMzlTHDIfFcnJVEdf2eZiTO6lQUJei2N3zubrH7jCEdkhXodl/OaC0g== dependencies: axios "^1.2.2" camelcase "^5.0.0" @@ -5671,7 +5926,7 @@ tree-kill@1.2.2: resolved "https://registry.yarnpkg.com/tree-kill/-/tree-kill-1.2.2.tgz#4ca09a9092c88b73a7cdc5e8a01b507b0790a0cc" integrity sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A== -ts-jest@^29.1.1: +ts-jest@~29.1.1: version "29.1.1" resolved "https://registry.yarnpkg.com/ts-jest/-/ts-jest-29.1.1.tgz#f58fe62c63caf7bfcc5cc6472082f79180f0815b" integrity sha512-D6xjnnbP17cC85nliwGiL+tpoKN0StpgE0TeOjXQTU6MVCfsB4v7aW05CgQ/1OywGb0x/oy9hHFnN+sczTiRaA== @@ -5765,6 +6020,20 @@ tsutils@^3.21.0: dependencies: tslib "^1.8.1" +twilio@^4.15.0: + version "4.15.0" + resolved "https://registry.yarnpkg.com/twilio/-/twilio-4.15.0.tgz#e41ee8e98d3c62c90f729b8872341fb410294d4c" + integrity sha512-wkHfIpAr2oOMfJ6A/Z6WD0jzLZZwMdbzys3XbslxNkmu4gGgRCre7o3IgaXRR5HIELEilbRhZjsmYHpG3fL8HA== + dependencies: + axios "^0.26.1" + dayjs "^1.11.9" + https-proxy-agent "^5.0.0" + jsonwebtoken "^9.0.0" + qs "^6.9.4" + scmp "^2.1.0" + url-parse "^1.5.9" + xmlbuilder "^13.0.2" + type-check@^0.4.0, type-check@~0.4.0: version "0.4.0" resolved "https://registry.yarnpkg.com/type-check/-/type-check-0.4.0.tgz#07b8203bfa7056c0657050e3ccd2c37730bab8f1" @@ -5862,7 +6131,7 @@ uri-js@^4.2.2: dependencies: punycode "^2.1.0" -url-parse@^1.5.3: +url-parse@^1.5.3, url-parse@^1.5.9: version "1.5.10" resolved "https://registry.yarnpkg.com/url-parse/-/url-parse-1.5.10.tgz#9d3c2f736c1d75dd3bd2be507dcc111f1e2ea9c1" integrity sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ== @@ -5875,7 +6144,7 @@ util-deprecate@^1.0.1, util-deprecate@~1.0.1: resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf" integrity sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw== -utils-merge@1.0.1: +utils-merge@1.0.1, utils-merge@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/utils-merge/-/utils-merge-1.0.1.tgz#9f95710f50a267947b2ccc124741c1028427e713" integrity sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA== @@ -6072,6 +6341,11 @@ xml-name-validator@^4.0.0: resolved "https://registry.yarnpkg.com/xml-name-validator/-/xml-name-validator-4.0.0.tgz#79a006e2e63149a8600f15430f0a4725d1524835" integrity sha512-ICP2e+jsHvAj2E2lIHxa5tjXRlKDJo4IdvPvCXbXQGdzSfmSpNVyIKMvoZHjDY9DP0zV17iI85o90vRFXNccRw== +xmlbuilder@^13.0.2: + version "13.0.2" + resolved "https://registry.yarnpkg.com/xmlbuilder/-/xmlbuilder-13.0.2.tgz#02ae33614b6a047d1c32b5389c1fdacb2bce47a7" + integrity sha512-Eux0i2QdDYKbdbA6AM6xE4m6ZTZr4G4xF9kahI2ukSEMCzwce2eX9WlTI5J3s+NU7hpasFsr8hWIONae7LluAQ== + xmlchars@^2.2.0: version "2.2.0" resolved "https://registry.yarnpkg.com/xmlchars/-/xmlchars-2.2.0.tgz#060fe1bcb7f9c76fe2a17db86a9bc3ab894210cb" From 80aa3eee2a900741b4fe72cf366d5fb8d777e63c Mon Sep 17 00:00:00 2001 From: Morgan Ludtke <42942267+ludtkemorgan@users.noreply.github.com> Date: Mon, 18 Sep 2023 19:21:57 -0500 Subject: [PATCH 24/57] fix: add email-service (#3607) * fix: add email-service * fix: add todo comment --- backend_new/.env.template | 5 + backend_new/nest-cli.json | 5 +- backend_new/package.json | 4 + backend_new/prisma/seed-dev.ts | 8 + .../seed-helpers/translation-factory.ts | 130 +++++ backend_new/prisma/seed-staging.ts | 8 + backend_new/src/modules/email.module.ts | 24 + backend_new/src/modules/user.module.ts | 6 +- backend_new/src/services/email.service.ts | 343 +++++++++++ backend_new/src/services/sendgrid.service.ts | 29 + .../src/services/translation.service.ts | 72 ++- backend_new/src/services/user.service.ts | 115 +++- backend_new/src/views/change-email.hbs | 11 + backend_new/src/views/confirmation.hbs | 86 +++ backend_new/src/views/forgot-password.hbs | 15 + backend_new/src/views/invite.hbs | 44 ++ backend_new/src/views/layouts/default.hbs | 9 + backend_new/src/views/mfa-code.hbs | 8 + backend_new/src/views/partials/feedback.hbs | 3 + backend_new/src/views/partials/footer.hbs | 29 + backend_new/src/views/partials/head.hbs | 417 +++++++++++++ backend_new/src/views/partials/header.hbs | 23 + .../src/views/partials/leasing-agent.hbs | 12 + .../src/views/partials/simple-footer.hbs | 4 + backend_new/src/views/partials/user-name.hbs | 4 + .../src/views/portal-account-update.hbs | 11 + backend_new/src/views/register-email.hbs | 11 + backend_new/test/integration/user.e2e-spec.ts | 42 +- .../test/unit/services/auth.service.spec.ts | 21 +- .../test/unit/services/email.service.spec.ts | 99 ++++ .../unit/services/translation.service.spec.ts | 71 ++- .../test/unit/services/user.service.spec.ts | 83 ++- backend_new/yarn.lock | 547 +++++++++++++++++- 33 files changed, 2245 insertions(+), 54 deletions(-) create mode 100644 backend_new/prisma/seed-helpers/translation-factory.ts create mode 100644 backend_new/src/modules/email.module.ts create mode 100644 backend_new/src/services/email.service.ts create mode 100644 backend_new/src/services/sendgrid.service.ts create mode 100644 backend_new/src/views/change-email.hbs create mode 100644 backend_new/src/views/confirmation.hbs create mode 100644 backend_new/src/views/forgot-password.hbs create mode 100644 backend_new/src/views/invite.hbs create mode 100644 backend_new/src/views/layouts/default.hbs create mode 100644 backend_new/src/views/mfa-code.hbs create mode 100644 backend_new/src/views/partials/feedback.hbs create mode 100644 backend_new/src/views/partials/footer.hbs create mode 100644 backend_new/src/views/partials/head.hbs create mode 100644 backend_new/src/views/partials/header.hbs create mode 100644 backend_new/src/views/partials/leasing-agent.hbs create mode 100644 backend_new/src/views/partials/simple-footer.hbs create mode 100644 backend_new/src/views/partials/user-name.hbs create mode 100644 backend_new/src/views/portal-account-update.hbs create mode 100644 backend_new/src/views/register-email.hbs create mode 100644 backend_new/test/unit/services/email.service.spec.ts diff --git a/backend_new/.env.template b/backend_new/.env.template index 3bc2df2f66..55a8405742 100644 --- a/backend_new/.env.template +++ b/backend_new/.env.template @@ -30,3 +30,8 @@ TWILIO_PHONE_NUMBER= TWILIO_ACCOUNT_SID= # account auth token for twilio TWILIO_AUTH_TOKEN= +# url for the partner front end +PARTNERS_PORTAL_URL=http://localhost:3001/ +# sendgrid email api key +EMAIL_API_KEY=SG.ExampleApiKey + diff --git a/backend_new/nest-cli.json b/backend_new/nest-cli.json index 56167b36a1..0f687d8c66 100644 --- a/backend_new/nest-cli.json +++ b/backend_new/nest-cli.json @@ -1,4 +1,7 @@ { "collection": "@nestjs/schematics", - "sourceRoot": "src" + "sourceRoot": "src", + "compilerOptions": { + "assets": [{ "include": "views/**/*.hbs", "outDir": "dist/src" }] + } } diff --git a/backend_new/package.json b/backend_new/package.json index 81d1c7d203..8eaa90dff9 100644 --- a/backend_new/package.json +++ b/backend_new/package.json @@ -32,19 +32,23 @@ "@google-cloud/translate": "^7.2.1", "@nestjs/axios": "~3.0.0", "@nestjs/common": "^8.0.0", + "@nestjs/config": "~3.0.0", "@nestjs/core": "^8.0.0", "@nestjs/jwt": "~10.1.0", "@nestjs/passport": "~10.0.1", "@nestjs/platform-express": "^8.0.0", "@nestjs/swagger": "^6.3.0", "@prisma/client": "^5.0.0", + "@sendgrid/mail": "7.7.0", "class-transformer": "~0.5.1", "class-validator": "~0.14.0", "cloudinary": "^1.37.3", "cookie-parser": "~1.4.6", "dayjs": "~1.11.9", + "handlebars": "~4.7.8", "jsonwebtoken": "~9.0.1", "lodash": "~4.17.21", + "node-polyglot": "~2.5.0", "passport": "~0.6.0", "passport-jwt": "~4.0.1", "passport-local": "~1.0.0", diff --git a/backend_new/prisma/seed-dev.ts b/backend_new/prisma/seed-dev.ts index 8ed4256204..e9288928a1 100644 --- a/backend_new/prisma/seed-dev.ts +++ b/backend_new/prisma/seed-dev.ts @@ -12,6 +12,7 @@ import { unitTypeFactoryAll } from './seed-helpers/unit-type-factory'; import { randomName } from './seed-helpers/word-generator'; import { randomInt } from 'node:crypto'; import { applicationFactory } from './seed-helpers/application-factory'; +import { translationFactory } from './seed-helpers/translation-factory'; const listingStatusEnumArray = Object.values(ListingsStatusEnum); @@ -48,6 +49,13 @@ export const devSeeding = async (prismaClient: PrismaClient) => { const jurisdiction = await prismaClient.jurisdictions.create({ data: jurisdictionFactory(), }); + // add jurisdiction specific translations and default ones + await prismaClient.translations.create({ + data: translationFactory(jurisdiction.id, jurisdiction.name), + }); + await prismaClient.translations.create({ + data: translationFactory(), + }); await unitTypeFactoryAll(prismaClient); const amiChart = await prismaClient.amiChart.create({ data: amiChartFactory(10, jurisdiction.id), diff --git a/backend_new/prisma/seed-helpers/translation-factory.ts b/backend_new/prisma/seed-helpers/translation-factory.ts new file mode 100644 index 0000000000..7253c592cc --- /dev/null +++ b/backend_new/prisma/seed-helpers/translation-factory.ts @@ -0,0 +1,130 @@ +import { LanguagesEnum, Prisma } from '@prisma/client'; + +const translations = (jurisdictionName?: string) => ({ + t: { hello: 'Hello', seeListing: 'See Listing' }, + footer: { + line1: `${jurisdictionName || 'Bloom'}`, + line2: '', + thankYou: 'Thank you', + footer: `${jurisdictionName || 'Bloom Housing'}`, + }, + header: { + logoUrl: + 'https://res.cloudinary.com/exygy/image/upload/v1692118607/core/bloom_housing_logo.png', + logoTitle: 'Bloom Housing Portal', + }, + invite: { + hello: 'Welcome to the Partners Portal', + confirmMyAccount: 'Confirm my account', + inviteManageListings: + 'You will now be able to manage listings and applications that you are a part of from one centralized location.', + inviteWelcomeMessage: 'Welcome to the Partners Portal at %{appUrl}.', + toCompleteAccountCreation: + 'To complete your account creation, please click the link below:', + }, + register: { + welcome: 'Welcome', + welcomeMessage: + 'Thank you for setting up your account on %{appUrl}. It will now be easier for you to start, save, and submit online applications for listings that appear on the site.', + confirmMyAccount: 'Confirm my account', + toConfirmAccountMessage: + 'To complete your account creation, please click the link below:', + }, + changeEmail: { + message: 'An email address change has been requested for your account.', + changeMyEmail: 'Confirm email change', + onChangeEmailMessage: + 'To confirm the change to your email address, please click the link below:', + }, + confirmation: { + subject: 'Your Application Confirmation', + eligible: { + fcfs: 'Eligible applicants will be contacted on a first come first serve basis until vacancies are filled.', + lottery: + 'Once the application period closes, eligible applicants will be placed in order based on lottery rank order.', + waitlist: + 'Eligible applicants will be placed on the waitlist on a first come first serve basis until waitlist spots are filled.', + fcfsPreference: + 'Housing preferences, if applicable, will affect first come first serve order.', + waitlistContact: + 'You may be contacted while on the waitlist to confirm that you wish to remain on the waitlist.', + lotteryPreference: + 'Housing preferences, if applicable, will affect lottery rank order.', + waitlistPreference: + 'Housing preferences, if applicable, will affect waitlist order.', + }, + interview: + 'If you are contacted for an interview, you will be asked to fill out a more detailed application and provide supporting documents.', + whatToExpect: { + FCFS: 'Applicants will be contacted by the property agent on a first come first serve basis until vacancies are filled.', + lottery: + 'Applicants will be contacted by the agent in lottery rank order until vacancies are filled.', + noLottery: + 'Applicants will be contacted by the agent in waitlist order until vacancies are filled.', + }, + whileYouWait: + 'While you wait, there are things you can do to prepare for potential next steps and future opportunities.', + shouldBeChosen: + 'Should your application be chosen, be prepared to fill out a more detailed application and provide required supporting documents.', + whatHappensNext: 'What happens next?', + whatToExpectNext: 'What to expect next:', + needToMakeUpdates: 'Need to make updates?', + applicationsClosed: 'Application
    closed', + applicationsRanked: 'Application
    ranked', + eligibleApplicants: { + FCFS: 'Eligible applicants will be placed in order based on first come first serve basis.', + lottery: + 'Eligible applicants will be placed in order based on preference and lottery rank.', + lotteryDate: 'The lottery will be held on %{lotteryDate}.', + }, + applicationReceived: 'Application
    received', + prepareForNextSteps: 'Prepare for next steps', + thankYouForApplying: + 'Thanks for applying. We have received your application for', + readHowYouCanPrepare: 'Read about how you can prepare for next steps', + yourConfirmationNumber: 'Your Confirmation Number', + applicationPeriodCloses: + 'Once the application period closes, the property manager will begin processing applications.', + contactedForAnInterview: + 'If you are contacted for an interview, you will need to fill out a more detailed application and provide supporting documents.', + gotYourConfirmationNumber: 'We got your application for', + }, + leasingAgent: { + officeHours: 'Office Hours:', + propertyManager: 'Property Manager', + contactAgentToUpdateInfo: + 'If you need to update information on your application, do not apply again. Instead, contact the agent for this listing.', + }, + mfaCodeEmail: { + message: 'Access code for your account has been requested.', + mfaCode: 'Your access code is: %{mfaCode}', + }, + forgotPassword: { + subject: 'Forgot your password?', + callToAction: + 'If you did make this request, please click on the link below to reset your password:', + passwordInfo: + "Your password won't change until you access the link above and create a new one.", + resetRequest: + 'A request to reset your Bloom Housing Portal website password for %{appUrl} has recently been made.', + ignoreRequest: "If you didn't request this, please ignore this email.", + changePassword: 'Change my password', + }, +}); + +export const translationFactory = ( + jurisdictionId?: string, + jurisdictionName?: string, +): Prisma.TranslationsCreateInput => { + return { + language: LanguagesEnum.en, + translations: translations(jurisdictionName), + jurisdictions: jurisdictionId + ? { + connect: { + id: jurisdictionId, + }, + } + : undefined, + }; +}; diff --git a/backend_new/prisma/seed-staging.ts b/backend_new/prisma/seed-staging.ts index 3457f2c693..4c84cf017f 100644 --- a/backend_new/prisma/seed-staging.ts +++ b/backend_new/prisma/seed-staging.ts @@ -21,6 +21,7 @@ import { whiteHouse, } from './seed-helpers/address-factory'; import { applicationFactory } from './seed-helpers/application-factory'; +import { translationFactory } from './seed-helpers/translation-factory'; export const stagingSeed = async ( prismaClient: PrismaClient, @@ -38,6 +39,13 @@ export const stagingSeed = async ( const jurisdiction = await prismaClient.jurisdictions.create({ data: jurisdictionFactory(jurisdictionName), }); + // add jurisdiction specific translations and default ones + await prismaClient.translations.create({ + data: translationFactory(jurisdiction.id, jurisdiction.name), + }); + await prismaClient.translations.create({ + data: translationFactory(), + }); // build ami charts const amiChart = await prismaClient.amiChart.create({ data: amiChartFactory(10, jurisdiction.id), diff --git a/backend_new/src/modules/email.module.ts b/backend_new/src/modules/email.module.ts new file mode 100644 index 0000000000..d1af8100d5 --- /dev/null +++ b/backend_new/src/modules/email.module.ts @@ -0,0 +1,24 @@ +import { Module } from '@nestjs/common'; +import { ConfigService } from '@nestjs/config'; +import { MailService } from '@sendgrid/mail'; +import { EmailService } from '../services/email.service'; +import { JurisdictionService } from '../services/jurisdiction.service'; +import { TranslationService } from '../services/translation.service'; +import { GoogleTranslateService } from '../services/google-translate.service'; +import { SendGridService } from '../services/sendgrid.service'; + +@Module({ + imports: [], + controllers: [], + providers: [ + EmailService, + JurisdictionService, + TranslationService, + ConfigService, + GoogleTranslateService, + SendGridService, + MailService, + ], + exports: [EmailService], +}) +export class EmailModule {} diff --git a/backend_new/src/modules/user.module.ts b/backend_new/src/modules/user.module.ts index 2784de3df3..c4a51a65bb 100644 --- a/backend_new/src/modules/user.module.ts +++ b/backend_new/src/modules/user.module.ts @@ -1,12 +1,14 @@ import { Module } from '@nestjs/common'; +import { ConfigService } from '@nestjs/config'; import { UserController } from '../controllers/user.controller'; import { UserService } from '../services/user.service'; import { PrismaModule } from './prisma.module'; +import { EmailModule } from './email.module'; @Module({ - imports: [PrismaModule], + imports: [PrismaModule, EmailModule], controllers: [UserController], - providers: [UserService], + providers: [UserService, ConfigService], exports: [UserService], }) export class UserModule {} diff --git a/backend_new/src/services/email.service.ts b/backend_new/src/services/email.service.ts new file mode 100644 index 0000000000..884526456f --- /dev/null +++ b/backend_new/src/services/email.service.ts @@ -0,0 +1,343 @@ +import { Injectable, Logger } from '@nestjs/common'; +import { ConfigService } from '@nestjs/config'; +import { ResponseError } from '@sendgrid/helpers/classes'; +import fs from 'fs'; +import Handlebars from 'handlebars'; +import Polyglot from 'node-polyglot'; +import path from 'path'; +import { TranslationService } from './translation.service'; +import { JurisdictionService } from './jurisdiction.service'; +import { Jurisdiction } from '../dtos/jurisdictions/jurisdiction.dto'; +import { LanguagesEnum, ReviewOrderTypeEnum } from '@prisma/client'; +import { IdDTO } from '../dtos/shared/id.dto'; +import { Listing } from '../dtos/listings/listing.dto'; +import { Application } from '../dtos/applications/application.dto'; +import { SendGridService } from './sendgrid.service'; + +type User = { + firstName: string; + middleName?: string; + lastName: string; + email: string; + language: LanguagesEnum; +}; +@Injectable() +export class EmailService { + polyglot: Polyglot; + + constructor( + private readonly sendGrid: SendGridService, + private readonly configService: ConfigService, + private readonly translationService: TranslationService, + private readonly jurisdictionService: JurisdictionService, + ) { + this.polyglot = new Polyglot({ + phrases: {}, + }); + const polyglot = this.polyglot; + Handlebars.registerHelper( + 't', + function ( + phrase: string, + options?: number | Polyglot.InterpolationOptions, + ) { + return polyglot.t(phrase, options); + }, + ); + const parts = this.partials(); + Handlebars.registerPartial(parts); + } + + private template(view: string) { + return Handlebars.compile( + fs.readFileSync( + path.join(path.resolve(__dirname, '..', 'views'), `/${view}.hbs`), + 'utf8', + ), + ); + } + + private partial(view: string) { + return fs.readFileSync( + path.join(path.resolve(__dirname, '..', 'views'), `/${view}`), + 'utf8', + ); + } + + private partials() { + const partials = {}; + const dirName = path.resolve(__dirname, '..', 'views/partials'); + + fs.readdirSync(dirName).forEach((filename) => { + partials[filename.slice(0, -4)] = this.partial('partials/' + filename); + }); + + const layoutsDirName = path.resolve(__dirname, '..', 'views/layouts'); + + fs.readdirSync(layoutsDirName).forEach((filename) => { + partials[`layout_${filename.slice(0, -4)}`] = this.partial( + 'layouts/' + filename, + ); + }); + + return partials; + } + + private async send( + to: string, + from: string, + subject: string, + body: string, + retry = 3, + ) { + await this.sendGrid.send( + { + to: to, + from, + subject: subject, + html: body, + }, + false, + (error) => { + if (error instanceof ResponseError) { + const { response } = error; + const { body: errBody } = response; + console.error( + `Error sending email to: ${to}! Error body: ${errBody}`, + ); + if (retry > 0) { + void this.send(to, from, subject, body, retry - 1); + } + } + }, + ); + } + + // TODO: update this to be memoized based on jurisdiction and language + // https://github.com/bloom-housing/bloom/issues/3648 + private async loadTranslations( + jurisdiction: Jurisdiction | null, + language?: LanguagesEnum, + ) { + const translations = await this.translationService.getMergedTranslations( + jurisdiction?.id, + language, + ); + this.polyglot.replace(translations); + } + + private async getJurisdiction( + jurisdictionIds: IdDTO[] | null, + ): Promise { + // Only return the jurisdiction if there is one jurisdiction passed in. + // For example if the user is tied to more than one jurisdiction the user should received the generic translations + if (jurisdictionIds?.length === 1) { + return await this.jurisdictionService.findOne({ + jurisdictionId: jurisdictionIds[0]?.id, + }); + } + return null; + } + + /* Send welcome email to new public users */ + public async welcome( + jurisdictionIds: IdDTO[], + user: User, + appUrl: string, + confirmationUrl: string, + ) { + const jurisdiction = await this.getJurisdiction(jurisdictionIds); + await this.loadTranslations(jurisdiction, user.language); + if (this.configService.get('NODE_ENV') === 'production') { + Logger.log( + `Preparing to send a welcome email to ${user.email} from ${jurisdiction.emailFromAddress}...`, + ); + } + await this.send( + user.email, + jurisdiction.emailFromAddress, + this.polyglot.t('register.welcome'), + this.template('register-email')({ + user: user, + confirmationUrl: confirmationUrl, + appOptions: { appUrl: appUrl }, + }), + ); + } + + /* Send invite email to partner users */ + async invitePartnerUser( + jurisdictionIds: IdDTO[], + user: User, + appUrl: string, + confirmationUrl: string, + ) { + const jurisdiction = await this.getJurisdiction(jurisdictionIds); + void (await this.loadTranslations(jurisdiction, user.language)); + await this.send( + user.email, + jurisdiction.emailFromAddress, + this.polyglot.t('invite.hello'), + this.template('invite')({ + user: user, + confirmationUrl: confirmationUrl, + appOptions: { appUrl }, + }), + ); + } + + /* send account update email */ + async portalAccountUpdate( + jurisdictionIds: IdDTO[], + user: User, + appUrl: string, + ) { + const jurisdiction = await this.getJurisdiction(jurisdictionIds); + void (await this.loadTranslations(jurisdiction, user.language)); + await this.send( + user.email, + jurisdiction.emailFromAddress, + this.polyglot.t('invite.portalAccountUpdate'), + this.template('portal-account-update')({ + user, + appUrl, + }), + ); + } + + /* send change of email email */ + public async changeEmail( + jurisdictionIds: IdDTO[], + user: User, + appUrl: string, + confirmationUrl: string, + newEmail: string, + ) { + const jurisdiction = await this.getJurisdiction(jurisdictionIds); + await this.loadTranslations(jurisdiction, user.language); + await this.send( + newEmail, + jurisdiction.emailFromAddress, + 'Bloom email change request', + this.template('change-email')({ + user: user, + confirmationUrl: confirmationUrl, + appOptions: { appUrl: appUrl }, + }), + ); + } + + /* Send forgot password email */ + public async forgotPassword( + jurisdictionIds: IdDTO[], + user: User, + appUrl: string, + resetToken: string, + ) { + const jurisdiction = await this.getJurisdiction(jurisdictionIds); + void (await this.loadTranslations(jurisdiction, user.language)); + const compiledTemplate = this.template('forgot-password'); + const resetUrl = `${appUrl}/reset-password?token=${resetToken}`; + + await this.send( + user.email, + jurisdiction.emailFromAddress, + this.polyglot.t('forgotPassword.subject'), + compiledTemplate({ + resetUrl: resetUrl, + resetOptions: { appUrl: appUrl }, + user: user, + }), + ); + } + + // TODO: connect to auth controller when it is implemented + public async sendMfaCode( + jurisdictionIds: IdDTO[], + user: User, + email: string, + mfaCode: string, + ) { + const jurisdiction = await this.getJurisdiction(jurisdictionIds); + void (await this.loadTranslations(jurisdiction, user.language)); + await this.send( + email, + jurisdiction.emailFromAddress, + 'Partners Portal account access token', + this.template('mfa-code')({ + user: user, + mfaCodeOptions: { mfaCode }, + }), + ); + } + + // TODO: connect to application controller when it is implemented + public async applicationConfirmation( + listing: Listing, + application: Application, + appUrl: string, + ) { + const jurisdiction = await this.getJurisdiction([listing.jurisdictions]); + void (await this.loadTranslations(jurisdiction, application.language)); + const listingUrl = `${appUrl}/listing/${listing.id}`; + const compiledTemplate = this.template('confirmation'); + + if (this.configService.get('NODE_ENV') == 'production') { + Logger.log( + `Preparing to send a confirmation email to ${application.applicant.emailAddress} from ${jurisdiction.emailFromAddress}...`, + ); + } + + let eligibleText: string; + let preferenceText: string; + let contactText = null; + if (listing.reviewOrderType === ReviewOrderTypeEnum.firstComeFirstServe) { + eligibleText = this.polyglot.t('confirmation.eligible.fcfs'); + preferenceText = this.polyglot.t('confirmation.eligible.fcfsPreference'); + } + if (listing.reviewOrderType === ReviewOrderTypeEnum.lottery) { + eligibleText = this.polyglot.t('confirmation.eligible.lottery'); + preferenceText = this.polyglot.t( + 'confirmation.eligible.lotteryPreference', + ); + } + if (listing.reviewOrderType === ReviewOrderTypeEnum.waitlist) { + eligibleText = this.polyglot.t('confirmation.eligible.waitlist'); + contactText = this.polyglot.t('confirmation.eligible.waitlistContact'); + preferenceText = this.polyglot.t( + 'confirmation.eligible.waitlistPreference', + ); + } + + const user = { + firstName: application.applicant.firstName, + middleName: application.applicant.middleName, + lastName: application.applicant.lastName, + }; + + const nextStepsUrl = this.polyglot.t('confirmation.nextStepsUrl'); + + await this.send( + application.applicant.emailAddress, + jurisdiction.emailFromAddress, + this.polyglot.t('confirmation.subject'), + compiledTemplate({ + subject: this.polyglot.t('confirmation.subject'), + header: { + logoTitle: this.polyglot.t('header.logoTitle'), + logoUrl: this.polyglot.t('header.logoUrl'), + }, + listing, + listingUrl, + application, + preferenceText, + interviewText: this.polyglot.t('confirmation.interview'), + eligibleText, + contactText, + nextStepsUrl: + nextStepsUrl != 'confirmation.nextStepsUrl' ? nextStepsUrl : null, + user, + }), + ); + } +} diff --git a/backend_new/src/services/sendgrid.service.ts b/backend_new/src/services/sendgrid.service.ts new file mode 100644 index 0000000000..b6875ac57e --- /dev/null +++ b/backend_new/src/services/sendgrid.service.ts @@ -0,0 +1,29 @@ +import { Injectable } from '@nestjs/common'; +import { ConfigService } from '@nestjs/config'; +import { + ClientResponse, + MailDataRequired, + MailService, + ResponseError, +} from '@sendgrid/mail'; + +@Injectable() +export class SendGridService { + constructor( + private readonly mailService: MailService, + private readonly configService: ConfigService, + ) { + this.mailService.setApiKey(configService.get('EMAIL_API_KEY')); + } + + public async send( + data: MailDataRequired, + isMultiple?: boolean, + cb?: ( + err: Error | ResponseError, + result: [ClientResponse, unknown], + ) => void, + ): Promise<[ClientResponse, unknown]> { + return this.mailService.send(data, isMultiple, cb); + } +} diff --git a/backend_new/src/services/translation.service.ts b/backend_new/src/services/translation.service.ts index 0f67bea8bf..71937bf84e 100644 --- a/backend_new/src/services/translation.service.ts +++ b/backend_new/src/services/translation.service.ts @@ -1,9 +1,10 @@ import { Injectable } from '@nestjs/common'; +import { LanguagesEnum, Translations } from '@prisma/client'; import { PrismaService } from './prisma.service'; -import { LanguagesEnum } from '@prisma/client'; import { Listing } from '../dtos/listings/listing.dto'; import { GoogleTranslateService } from './google-translate.service'; import * as lodash from 'lodash'; +import { Jurisdiction } from '../dtos/jurisdictions/jurisdiction.dto'; @Injectable() export class TranslationService { @@ -12,27 +13,72 @@ export class TranslationService { private readonly googleTranslateService: GoogleTranslateService, ) {} + public async getMergedTranslations( + jurisdictionId: string | null, + language?: LanguagesEnum, + ) { + let jurisdictionalTranslations: Promise, + genericTranslations: Promise, + jurisdictionalDefaultTranslations: Promise; + + if (language && language !== LanguagesEnum.en) { + if (jurisdictionId) { + jurisdictionalTranslations = + this.getTranslationByLanguageAndJurisdictionOrDefaultEn( + language, + jurisdictionId, + ); + } + genericTranslations = + this.getTranslationByLanguageAndJurisdictionOrDefaultEn(language, null); + } + + if (jurisdictionId) { + jurisdictionalDefaultTranslations = + this.getTranslationByLanguageAndJurisdictionOrDefaultEn( + LanguagesEnum.en, + jurisdictionId, + ); + } + + const genericDefaultTranslations = + this.getTranslationByLanguageAndJurisdictionOrDefaultEn( + LanguagesEnum.en, + null, + ); + + const [genericDefault, generic, jurisdictionalDefault, jurisdictional] = + await Promise.all([ + genericDefaultTranslations, + genericTranslations, + jurisdictionalDefaultTranslations, + jurisdictionalTranslations, + ]); + // Deep merge + const translations = lodash.merge( + genericDefault?.translations, + generic?.translations, + jurisdictionalDefault?.translations, + jurisdictional?.translations, + ); + + return translations; + } + public async getTranslationByLanguageAndJurisdictionOrDefaultEn( language: LanguagesEnum, jurisdictionId: string | null, ) { - let translations = await this.prisma.translations.findUnique({ - where: { - jurisdictionId_language: { language, jurisdictionId }, - }, + let translations = await this.prisma.translations.findFirst({ + where: { AND: [{ language: language }, { jurisdictionId }] }, }); - if (translations === null) { + if (translations === null && language !== LanguagesEnum.en) { console.warn( `Fetching translations for ${language} failed on jurisdiction ${jurisdictionId}, defaulting to english.`, ); - translations = await this.prisma.translations.findUnique({ - where: { - jurisdictionId_language: { - language: LanguagesEnum.en, - jurisdictionId, - }, - }, + translations = await this.prisma.translations.findFirst({ + where: { AND: [{ language: LanguagesEnum.en }, { jurisdictionId }] }, }); } return translations; diff --git a/backend_new/src/services/user.service.ts b/backend_new/src/services/user.service.ts index 0cf94dd569..1e9d713964 100644 --- a/backend_new/src/services/user.service.ts +++ b/backend_new/src/services/user.service.ts @@ -4,6 +4,7 @@ import { NotFoundException, UnauthorizedException, } from '@nestjs/common'; +import { ConfigService } from '@nestjs/config'; import { Prisma } from '@prisma/client'; import dayjs from 'dayjs'; import advancedFormat from 'dayjs/plugin/advancedFormat'; @@ -29,6 +30,7 @@ import { ConfirmationRequest } from '../dtos/users/confirmation-request.dto'; import { IdDTO } from '../dtos/shared/id.dto'; import { UserInvite } from '../dtos/users/user-invite.dto'; import { UserCreate } from '../dtos/users/user-create.dto'; +import { EmailService } from './email.service'; /* this is the service for users @@ -49,7 +51,11 @@ type findByOptions = { @Injectable() export class UserService { - constructor(private prisma: PrismaService) { + constructor( + private prisma: PrismaService, + private emailService: EmailService, + private readonly configService: ConfigService, + ) { dayjs.extend(advancedFormat); } @@ -288,7 +294,14 @@ export class UserService { dto.appUrl, confirmationToken, ); - // TODO: email service (https://github.com/bloom-housing/bloom/issues/3503) + + this.emailService.changeEmail( + dto.jurisdictions, + storedUser, + dto.appUrl, + confirmationUrl, + dto.newEmail, + ); } const res = this.prisma.userAccounts.update({ @@ -365,7 +378,7 @@ export class UserService { dto: EmailAndAppUrl, forPublic: boolean, ): Promise { - const storedUser = await this.findUserOrError({ email: dto.email }, false); + const storedUser = await this.findUserOrError({ email: dto.email }, true); if (!storedUser.confirmedAt) { const confirmationToken = this.createConfirmationToken( @@ -381,10 +394,29 @@ export class UserService { }, }); - const confirmationUrl = forPublic - ? this.getPublicConfirmationUrl(dto.appUrl, confirmationToken) - : this.getPartnersConfirmationUrl(dto.appUrl, confirmationToken); - // TODO: email service (https://github.com/bloom-housing/bloom/issues/3503) + if (forPublic) { + const confirmationUrl = this.getPublicConfirmationUrl( + dto.appUrl, + confirmationToken, + ); + this.emailService.welcome( + storedUser.jurisdictions, + storedUser, + dto.appUrl, + confirmationUrl, + ); + } else { + const confirmationUrl = this.getPartnersConfirmationUrl( + dto.appUrl, + confirmationToken, + ); + this.emailService.invitePartnerUser( + storedUser.jurisdictions, + storedUser, + dto.appUrl, + confirmationUrl, + ); + } } return { @@ -396,22 +428,27 @@ export class UserService { sets a reset token so a user can recover their account if they forgot the password */ async forgotPassword(dto: EmailAndAppUrl): Promise { - const storedUser = await this.findUserOrError({ email: dto.email }, false); + const storedUser = await this.findUserOrError({ email: dto.email }, true); const payload = { id: storedUser.id, exp: Number.parseInt(dayjs().add(1, 'hour').format('X')), }; + const resetToken = sign(payload, process.env.APP_SECRET); await this.prisma.userAccounts.update({ data: { - resetToken: sign(payload, process.env.APP_SECRET), + resetToken: resetToken, }, where: { id: storedUser.id, }, }); - // TODO: email service (https://github.com/bloom-housing/bloom/issues/3503) - + this.emailService.forgotPassword( + storedUser.jurisdictions, + storedUser, + dto.appUrl, + resetToken, + ); return { success: true, } as SuccessDTO; @@ -631,12 +668,19 @@ export class UserService { }, }); + // Public user that needs email if (!forPartners && sendWelcomeEmail) { const confirmationUrl = this.getPublicConfirmationUrl( dto.appUrl, confirmationToken, ); - // TODO: email service (https://github.com/bloom-housing/bloom/issues/3503) + this.emailService.welcome( + dto.jurisdictions, + newUser, + dto.appUrl, + confirmationUrl, + ); + // Partner user that is given access to an additional jurisdiction } else if ( forPartners && existingUser && @@ -645,9 +689,26 @@ export class UserService { dto?.userRoles?.isPartner && this.jurisdictionMismatch(dto.jurisdictions, existingUser.jurisdictions) ) { - // TODO: email service (https://github.com/bloom-housing/bloom/issues/3503) + const newJurisdictions = this.getMismatchedJurisdictions( + dto.jurisdictions, + existingUser.jurisdictions, + ); + this.emailService.portalAccountUpdate( + newJurisdictions, + newUser, + dto.appUrl, + ); } else if (forPartners) { - // TODO: email service (https://github.com/bloom-housing/bloom/issues/3503) + const confirmationUrl = this.getPublicConfirmationUrl( + this.configService.get('PARTNERS_PORTAL_URL'), + confirmationToken, + ); + this.emailService.invitePartnerUser( + dto.jurisdictions, + newUser, + this.configService.get('PARTNERS_PORTAL_URL'), + confirmationUrl, + ); } if (!forPartners) { @@ -754,11 +815,27 @@ export class UserService { incomingJurisdictions: IdDTO[], existingJurisdictions: IdDTO[], ): boolean { - return incomingJurisdictions?.some( - (incomingJuris) => - !existingJurisdictions?.some( - (existingJuris) => existingJuris.id === incomingJuris.id, - ), + return ( + this.getMismatchedJurisdictions( + incomingJurisdictions, + existingJurisdictions, + ).length > 0 ); } + + getMismatchedJurisdictions( + incomingJurisdictions: IdDTO[], + existingJurisdictions: IdDTO[], + ) { + return incomingJurisdictions.reduce((misMatched, jurisdiction) => { + if ( + !existingJurisdictions?.some( + (existingJuris) => existingJuris.id === jurisdiction.id, + ) + ) { + misMatched.push(jurisdiction.id); + } + return misMatched; + }, []); + } } diff --git a/backend_new/src/views/change-email.hbs b/backend_new/src/views/change-email.hbs new file mode 100644 index 0000000000..13493e26c2 --- /dev/null +++ b/backend_new/src/views/change-email.hbs @@ -0,0 +1,11 @@ +

    {{t "t.hello"}} {{> user-name }},

    +

    + {{t "changeEmail.message" appOptions}} +

    +

    + {{t "changeEmail.onChangeEmailMessage"}} +

    +

    + {{t "changeEmail.changeMyEmail"}} +

    +{{> simple-footer }} diff --git a/backend_new/src/views/confirmation.hbs b/backend_new/src/views/confirmation.hbs new file mode 100644 index 0000000000..fb779769de --- /dev/null +++ b/backend_new/src/views/confirmation.hbs @@ -0,0 +1,86 @@ +{{#> layout_default }} +

    {{t "confirmation.gotYourConfirmationNumber"}}
    {{listing.name}}

    + + + + + + + + + + + + + + + +
    + {{t "t.seeListing"}} + +
    +{{/layout_default }} diff --git a/backend_new/src/views/forgot-password.hbs b/backend_new/src/views/forgot-password.hbs new file mode 100644 index 0000000000..0209837abe --- /dev/null +++ b/backend_new/src/views/forgot-password.hbs @@ -0,0 +1,15 @@ +

    {{t "t.hello"}} {{> user-name }},

    +

    + {{t "forgotPassword.resetRequest" resetOptions}} +

    +

    + {{t "forgotPassword.ignoreRequest"}} +

    +

    + {{t "forgotPassword.callToAction"}} + {{t "forgotPassword.changePassword"}} +

    +

    + {{t "forgotPassword.passwordInfo"}} +

    +{{> simple-footer }} diff --git a/backend_new/src/views/invite.hbs b/backend_new/src/views/invite.hbs new file mode 100644 index 0000000000..23a02715e5 --- /dev/null +++ b/backend_new/src/views/invite.hbs @@ -0,0 +1,44 @@ +{{#> layout_default }} +

    + + {{t "invite.hello"}} + +
    + + {{> user-name }} +

    + + + + + + + +
    +

    + {{t "invite.inviteWelcomeMessage" appOptions}} +

    + {{t "invite.inviteManageListings"}} +

    + {{t "invite.toCompleteAccountCreation"}} +

    +
    + + + + + + + +
    + + {{t "invite.confirmMyAccount"}} + +
    +{{/layout_default }} diff --git a/backend_new/src/views/layouts/default.hbs b/backend_new/src/views/layouts/default.hbs new file mode 100644 index 0000000000..2960258da4 --- /dev/null +++ b/backend_new/src/views/layouts/default.hbs @@ -0,0 +1,9 @@ + + + {{> head }} + + {{> header }} + {{> @partial-block }} + {{> footer }} + + diff --git a/backend_new/src/views/mfa-code.hbs b/backend_new/src/views/mfa-code.hbs new file mode 100644 index 0000000000..ac650a65c0 --- /dev/null +++ b/backend_new/src/views/mfa-code.hbs @@ -0,0 +1,8 @@ +

    {{t "t.hello"}} {{> user-name }}

    +

    + {{t "mfaCodeEmail.message" }} +

    +

    + {{t "mfaCodeEmail.mfaCode" mfaCodeOptions}} +

    +{{> simple-footer }} diff --git a/backend_new/src/views/partials/feedback.hbs b/backend_new/src/views/partials/feedback.hbs new file mode 100644 index 0000000000..367e6f64b7 --- /dev/null +++ b/backend_new/src/views/partials/feedback.hbs @@ -0,0 +1,3 @@ +

    + {{t "footer.callToAction"}} {{t "footer.feedback"}}. +

    diff --git a/backend_new/src/views/partials/footer.hbs b/backend_new/src/views/partials/footer.hbs new file mode 100644 index 0000000000..ac448bffe5 --- /dev/null +++ b/backend_new/src/views/partials/footer.hbs @@ -0,0 +1,29 @@ + + + + + + + + + + + + + +
    + +   + + diff --git a/backend_new/src/views/partials/head.hbs b/backend_new/src/views/partials/head.hbs new file mode 100644 index 0000000000..e813416c57 --- /dev/null +++ b/backend_new/src/views/partials/head.hbs @@ -0,0 +1,417 @@ + + + + {{ subject }} + + + + + diff --git a/backend_new/src/views/partials/header.hbs b/backend_new/src/views/partials/header.hbs new file mode 100644 index 0000000000..419b66e25c --- /dev/null +++ b/backend_new/src/views/partials/header.hbs @@ -0,0 +1,23 @@ + + +