diff --git a/.eslintrc.json b/.eslintrc.json index e83e38ff..e15ccc99 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -5,7 +5,7 @@ "jest": true }, "root": true, - "extends": ["airbnb-base", "plugin:@typescript-eslint/recommended"], + "extends": ["airbnb-base", "plugin:@typescript-eslint/recommended", "prettier"], "parser": "@typescript-eslint/parser", "plugins": ["@typescript-eslint", "import"], "parserOptions": { diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 906c4daf..cdac6b0e 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -3,6 +3,10 @@ name: Test on: pull_request: +defaults: + run: + shell: bash + jobs: test: name: Test PR @@ -10,6 +14,8 @@ jobs: environment: development steps: + - uses: reviewdog/action-setup@v1 + - name: Checkout uses: actions/checkout@v3 @@ -40,12 +46,7 @@ jobs: pnpm install --frozen-lockfile pnpm --filter='@jiphyeonjeon-42/contracts' build - - if: always() - name: check types (backend) - working-directory: backend - run: pnpm check - - - if: always() - name: check types (contracts) - working-directory: contracts - run: pnpm check + - name: check types + if: always() + run: | + pnpm -r --no-bail --parallel run check | sed -r 's|(.*)( check: )(.*)|\1/\3|' diff --git a/.prettierrc.yml b/.prettierrc.yml new file mode 100644 index 00000000..6589e8e5 --- /dev/null +++ b/.prettierrc.yml @@ -0,0 +1,7 @@ +semi: true +singleQuote: true +useTabs: false +tabWidth: 2 +trailingComma: all +printWidth: 100 +arrowParens: always diff --git a/Dockerfile b/Dockerfile index 016bd4a0..56dbef19 100644 --- a/Dockerfile +++ b/Dockerfile @@ -3,6 +3,12 @@ FROM node:18-alpine as pnpm-installed # https://github.com/pnpm/pnpm/issues/4495#issuecomment-1317831712 ENV PNPM_HOME="/root/.local/share/pnpm" ENV PATH="${PATH}:${PNPM_HOME}" +ENV PYTHONUNBUFFERED=1 +RUN apk add --update --no-cache python3 && ln -sf python3 /usr/bin/python +RUN python3 -m ensurepip +RUN pip3 install --no-cache --upgrade pip setuptools +RUN apk add --no-cache make +RUN apk add build-base RUN npm install --global pnpm RUN pnpm config set store-dir .pnpm-store RUN pnpm install --global node-pre-gyp diff --git a/backend/package.json b/backend/package.json index aa731272..f6e6bfac 100644 --- a/backend/package.json +++ b/backend/package.json @@ -20,70 +20,72 @@ "schema": ". ./.env && DATABASE_URL=mysql://$RDS_USERNAME:$RDS_PASSWORD@$RDS_HOSTNAME:3306/$RDS_DB_NAME kysely-codegen --dialect=mysql --out-file=src/kysely/generated.ts" }, "devDependencies": { - "@types/bcrypt": "^5.0.0", - "@types/cookie-parser": "^1.4.3", - "@types/cors": "^2.8.13", - "@types/express": "^4.17.17", - "@types/http-errors": "^2.0.1", - "@types/jest": "^29.5.2", - "@types/jsonwebtoken": "^9.0.2", - "@types/morgan": "^1.9.4", - "@types/node-schedule": "^2.1.0", - "@types/passport": "^1.0.12", - "@types/passport-jwt": "^3.0.8", - "@types/swagger-jsdoc": "^6.0.1", - "@types/swagger-ui-express": "^4.1.3", - "@typescript-eslint/eslint-plugin": "^6.1.0", - "@typescript-eslint/parser": "^6.1.0", - "eslint": "^8.45.0", + "@types/bcrypt": "^5.0.1", + "@types/cookie-parser": "^1.4.5", + "@types/cors": "^2.8.15", + "@types/express": "^4.17.20", + "@types/http-errors": "^2.0.3", + "@types/jest": "^29.5.6", + "@types/jsonwebtoken": "^9.0.4", + "@types/morgan": "^1.9.7", + "@types/node-schedule": "^2.1.2", + "@types/passport": "^1.0.14", + "@types/passport-jwt": "^3.0.12", + "@types/swagger-jsdoc": "^6.0.2", + "@types/swagger-ui-express": "^4.1.5", + "@typescript-eslint/eslint-plugin": "^6.9.0", + "@typescript-eslint/parser": "^6.9.0", + "eslint": "^8.52.0", "eslint-config-airbnb-base": "^15.0.0", - "eslint-import-resolver-typescript": "^3.5.5", - "eslint-plugin-import": "^2.27.5", - "jest": "^29.5.0", - "jest-mock-extended": "^3.0.4", + "eslint-import-resolver-typescript": "^3.6.1", + "eslint-plugin-import": "^2.29.0", + "jest": "^29.7.0", + "jest-mock-extended": "^3.0.5", "kysely-codegen": "^0.10.1", "nodemon": "^3.0.1", "prettier": "^2.8.8", - "ts-jest": "^29.1.0", + "ts-jest": "^29.1.1", "typeorm-model-generator": "^0.4.6" }, "dependencies": { "@jiphyeonjeon-42/contracts": "workspace:*", + "@mapbox/node-pre-gyp": "^1.0.11", "@slack/web-api": "^6.7.1", "@ts-rest/express": "^3.28.0", "@ts-rest/open-api": "^3.28.0", "axios": "^0.27.2", - "bcrypt": "^5.0.1", + "bcrypt": "^5.1.1", "cookie-parser": "^1.4.6", "cors": "^2.8.5", - "date-fns": "^2.29.3", - "dotenv": "^16.0.0", - "express": "^4.17.2", - "express-rate-limit": "^6.9.0", + "date-fns": "^2.30.0", + "dotenv": "^16.3.1", + "express": "^4.18.2", + "express-rate-limit": "^6.11.2", "hangul-js": "^0.2.6", "http-errors": "^2.0.0", - "http-status": "^1.5.0", + "http-status": "^1.7.3", "http-terminator": "^3.2.0", "jsonwebtoken": "^8.5.1", - "kysely": "^0.26.1", + "kysely": "^0.26.3", "kysely-paginate": "^0.2.0", "morgan": "^1.10.0", "mysql2": "^2.3.3", - "node-schedule": "^2.1.0", - "passport": "^0.5.2", + "node-pre-gyp": "^0.17.0", + "node-schedule": "^2.1.1", + "passport": "^0.5.3", "passport-42": "^1.2.6", - "passport-jwt": "^4.0.0", + "passport-jwt": "^4.0.1", "passport-strategy": "^1.0.0", "password-validator": "^5.3.0", "reflect-metadata": "^0.1.13", - "swagger-jsdoc": "^6.1.0", - "swagger-ui-express": "^4.3.0", - "ts-pattern": "^5.0.1", - "typeorm": "^0.3.11", - "vite": "^4.4.7", - "vite-node": "^0.34.1", - "vite-tsconfig-paths": "^4.2.0", - "winston": "^3.6.0", - "winston-daily-rotate-file": "^4.6.1" + "swagger-jsdoc": "^6.2.8", + "swagger-ui-express": "^4.6.3", + "ts-pattern": "^5.0.5", + "typeorm": "^0.3.17", + "vite": "^4.5.0", + "vite-node": "^0.34.6", + "vite-tsconfig-paths": "^4.2.1", + "winston": "^3.11.0", + "winston-daily-rotate-file": "^4.7.1" } } diff --git a/backend/src/app.ts b/backend/src/app.ts index ed62e1b4..c471143f 100644 --- a/backend/src/app.ts +++ b/backend/src/app.ts @@ -78,12 +78,12 @@ app.use( // dev route app.use('/api', router); -// dev/v2 route -createExpressEndpoints(contract, routerV2, app, { - logInitialization: true, - responseValidation: true, - jsonQuery: true, -}); +// // dev/v2 route +// createExpressEndpoints(contract, routerV2, app, { +// logInitialization: true, +// responseValidation: true, +// jsonQuery: true, +// }); // 에러 핸들러 app.use(errorConverter); diff --git a/backend/src/config/JwtOption.ts b/backend/src/config/JwtOption.ts index ffd0a186..0bd71179 100644 --- a/backend/src/config/JwtOption.ts +++ b/backend/src/config/JwtOption.ts @@ -5,19 +5,21 @@ import { Mode } from './modeOption'; import { match } from 'ts-pattern'; type getJwtOption = (mode: Mode) => (option: OauthUrlOption) => JwtOption; -export const getJwtOption: getJwtOption = (mode) => ({ redirectURL, clientURL }) => { - const redirectDomain = new URL(redirectURL).hostname; - const clientDomain = new URL(clientURL).hostname; - const secure = mode === 'prod' || mode === 'https'; +export const getJwtOption: getJwtOption = + (mode) => + ({ redirectURL, clientURL }) => { + const redirectDomain = new URL(redirectURL).hostname; + const clientDomain = new URL(clientURL).hostname; + const secure = mode === 'prod' || mode === 'https'; - const issuer = secure ? redirectDomain : 'localhost'; - const domain = match(mode) - .with('prod', () => clientDomain) - .with('https', () => undefined) - .otherwise(() => 'localhost'); + const issuer = secure ? redirectDomain : 'localhost'; + const domain = match(mode) + .with('prod', () => clientDomain) + .with('https', () => undefined) + .otherwise(() => 'localhost'); - return { issuer, domain, secure }; -}; + return { issuer, domain, secure }; + }; export const jwtSecretSchema = z.object({ JWT_SECRET: nonempty }).transform((v) => v.JWT_SECRET); diff --git a/backend/src/config/config.type.ts b/backend/src/config/config.type.ts index 69d1a86e..11bb943d 100644 --- a/backend/src/config/config.type.ts +++ b/backend/src/config/config.type.ts @@ -19,7 +19,7 @@ export type NaverBookApiOption = { /** 네이버 도서 검색 API 시크릿 */ secret: string; -} +}; /** DB 연결 옵션 */ export type ConnectOption = { @@ -34,7 +34,7 @@ export type ConnectOption = { /** DB 이름 */ database: string; -} +}; /** OAuth URL 옵션 */ export type OauthUrlOption = { @@ -43,7 +43,7 @@ export type OauthUrlOption = { /** 집현전 프론트엔드 URL */ clientURL: string; -} +}; /** 42 API OAuth 클라이언트 인증 정보 */ export type Oauth42ApiOption = { @@ -52,7 +52,7 @@ export type Oauth42ApiOption = { /** 42 API OAuth 클라이언트 시크릿 */ secret: string; -} +}; /** npm 로깅 레벨 */ export type LogLevel = keyof typeof levels; @@ -64,4 +64,4 @@ export type LogLevelOption = { /** 콘솔 로깅 레벨 */ readonly consoleLogLevel: 'error' | 'debug'; -} +}; diff --git a/backend/src/config/dbSchema.ts b/backend/src/config/dbSchema.ts index 7c072271..cf5f7073 100644 --- a/backend/src/config/dbSchema.ts +++ b/backend/src/config/dbSchema.ts @@ -1,13 +1,17 @@ import { envObject, nonempty } from './envObject'; /** RDS 연결 옵션 파싱을 위한 스키마 */ -export const rdsSchema = envObject('RDS_HOSTNAME', 'RDS_USERNAME', 'RDS_PASSWORD', 'RDS_DB_NAME') - .transform((v) => ({ - host: v.RDS_HOSTNAME, - username: v.RDS_USERNAME, - password: v.RDS_PASSWORD, - database: v.RDS_DB_NAME, - })); +export const rdsSchema = envObject( + 'RDS_HOSTNAME', + 'RDS_USERNAME', + 'RDS_PASSWORD', + 'RDS_DB_NAME', +).transform((v) => ({ + host: v.RDS_HOSTNAME, + username: v.RDS_USERNAME, + password: v.RDS_PASSWORD, + database: v.RDS_DB_NAME, +})); /** MYSQL 연결 옵션 파싱을 위한 스키마 */ const mysqlSchema = envObject('MYSQL_USER', 'MYSQL_PASSWORD', 'MYSQL_DATABASE') diff --git a/backend/src/config/envObject.ts b/backend/src/config/envObject.ts index ace38068..b690733b 100644 --- a/backend/src/config/envObject.ts +++ b/backend/src/config/envObject.ts @@ -15,7 +15,7 @@ export const url = z.string().trim().url(); * @param keys 환경변수 키 목록 */ export const envObject = (...keys: T) => { - type Keys = T[ number ]; + type Keys = T[number]; const env = Object.fromEntries(keys.map((key) => [key, nonempty])); return z.object(env as Record); diff --git a/backend/src/config/getConnectOption.ts b/backend/src/config/getConnectOption.ts index 80eee87e..a08e857e 100644 --- a/backend/src/config/getConnectOption.ts +++ b/backend/src/config/getConnectOption.ts @@ -13,8 +13,10 @@ const getConnectOptionSchema = (mode: Mode) => { /** * 환경변수에서 DB 연결 옵션을 파싱하는 함수 */ -export const getConnectOption = (mode: Mode) => (processEnv: NodeJS.ProcessEnv): ConnectOption => { - const connectOptionSchema = getConnectOptionSchema(mode); +export const getConnectOption = + (mode: Mode) => + (processEnv: NodeJS.ProcessEnv): ConnectOption => { + const connectOptionSchema = getConnectOptionSchema(mode); - return connectOptionSchema.parse(processEnv); -}; + return connectOptionSchema.parse(processEnv); + }; diff --git a/backend/src/config/logOption.ts b/backend/src/config/logOption.ts index 1477cdbf..e30af6d2 100644 --- a/backend/src/config/logOption.ts +++ b/backend/src/config/logOption.ts @@ -18,8 +18,8 @@ export const colors: Record = { } as const; export const getLogLevelOption = (mode: RuntimeMode): LogLevelOption => { - const logLevel = (mode === 'production' ? 'http' : 'debug'); - const consoleLogLevel = (mode === 'production' ? 'error' : 'debug'); + const logLevel = mode === 'production' ? 'http' : 'debug'; + const consoleLogLevel = mode === 'production' ? 'error' : 'debug'; return { logLevel, consoleLogLevel } as const; }; diff --git a/backend/src/config/naverBookApiOption.ts b/backend/src/config/naverBookApiOption.ts index 73b3f7eb..282d4671 100644 --- a/backend/src/config/naverBookApiOption.ts +++ b/backend/src/config/naverBookApiOption.ts @@ -2,11 +2,13 @@ import { NaverBookApiOption } from './config.type'; import { envObject } from './envObject'; -const naverBookApiSchema = envObject('NAVER_BOOK_SEARCH_CLIENT_ID', 'NAVER_BOOK_SEARCH_SECRET') - .transform((v) => ({ - client: v.NAVER_BOOK_SEARCH_CLIENT_ID, - secret: v.NAVER_BOOK_SEARCH_SECRET, - })); +const naverBookApiSchema = envObject( + 'NAVER_BOOK_SEARCH_CLIENT_ID', + 'NAVER_BOOK_SEARCH_SECRET', +).transform((v) => ({ + client: v.NAVER_BOOK_SEARCH_CLIENT_ID, + secret: v.NAVER_BOOK_SEARCH_SECRET, +})); export const getNaverBookApiOption = (processEnv: NodeJS.ProcessEnv): NaverBookApiOption => { const option = naverBookApiSchema.parse(processEnv); diff --git a/backend/src/config/oauthOption.ts b/backend/src/config/oauthOption.ts index 30b9d0c5..fa811c58 100644 --- a/backend/src/config/oauthOption.ts +++ b/backend/src/config/oauthOption.ts @@ -12,15 +12,19 @@ export const oauth42Schema = z.object({ CLIENT_SECRET: nonempty, }); -export const getOauthUrlOption = (processEnv: NodeJS.ProcessEnv): OauthUrlOption => oauthUrlSchema - .transform((v) => ({ - redirectURL: v.REDIRECT_URL, - clientURL: v.CLIENT_URL, - })).parse(processEnv); +export const getOauthUrlOption = (processEnv: NodeJS.ProcessEnv): OauthUrlOption => + oauthUrlSchema + .transform((v) => ({ + redirectURL: v.REDIRECT_URL, + clientURL: v.CLIENT_URL, + })) + .parse(processEnv); // eslint-disable-next-line max-len -export const getOauth42ApiOption = (processEnv: NodeJS.ProcessEnv): Oauth42ApiOption => oauth42Schema - .transform((v) => ({ - id: v.CLIENT_ID, - secret: v.CLIENT_SECRET, - })).parse(processEnv); +export const getOauth42ApiOption = (processEnv: NodeJS.ProcessEnv): Oauth42ApiOption => + oauth42Schema + .transform((v) => ({ + id: v.CLIENT_ID, + secret: v.CLIENT_SECRET, + })) + .parse(processEnv); diff --git a/backend/src/entity/entities/Book.ts b/backend/src/entity/entities/Book.ts index 5435d1fa..38a7a48e 100644 --- a/backend/src/entity/entities/Book.ts +++ b/backend/src/entity/entities/Book.ts @@ -1,5 +1,11 @@ import { - Column, Entity, Index, JoinColumn, ManyToOne, OneToMany, PrimaryGeneratedColumn, + Column, + Entity, + Index, + JoinColumn, + ManyToOne, + OneToMany, + PrimaryGeneratedColumn, } from 'typeorm'; import { BookInfo } from './BookInfo'; import { User } from './User'; @@ -8,55 +14,54 @@ import { Reservation } from './Reservation'; @Index('FK_donator_id_from_user', ['donatorId'], {}) @Entity('book') - export class Book { @PrimaryGeneratedColumn({ type: 'int', name: 'id' }) - id?: number; + id?: number; @Column('varchar', { name: 'donator', nullable: true, length: 255 }) - donator: string | null; + donator: string | null; @Column('varchar', { name: 'callSign', length: 255 }) - callSign: string; + callSign: string; @Column('int', { name: 'status' }) - status: number; + status: number; @Column('datetime', { name: 'createdAt', default: () => "'CURRENT_TIMESTAMP(6)'", }) - createdAt?: Date; + createdAt?: Date; @Column('int') - infoId: number; + infoId: number; @Column('datetime', { name: 'updatedAt', default: () => "'CURRENT_TIMESTAMP(6)'", }) - updatedAt?: Date; + updatedAt?: Date; @Column('int', { name: 'donatorId', nullable: true }) - donatorId: number | null; + donatorId: number | null; @ManyToOne(() => BookInfo, (bookInfo) => bookInfo.books, { onDelete: 'NO ACTION', onUpdate: 'NO ACTION', }) @JoinColumn([{ name: 'infoId', referencedColumnName: 'id' }]) - info?: BookInfo; + info?: BookInfo; @ManyToOne(() => User, (user) => user.books, { onDelete: 'NO ACTION', onUpdate: 'NO ACTION', }) @JoinColumn([{ name: 'donatorId', referencedColumnName: 'id' }]) - donator2?: User; + donator2?: User; @OneToMany(() => Lending, (lending) => lending.book) - lendings?: Lending[]; + lendings?: Lending[]; @OneToMany(() => Reservation, (reservation) => reservation.book) - reservations?: Reservation[]; + reservations?: Reservation[]; } diff --git a/backend/src/entity/entities/BookInfo.ts b/backend/src/entity/entities/BookInfo.ts index 9ef8b3b0..a0c19bc4 100644 --- a/backend/src/entity/entities/BookInfo.ts +++ b/backend/src/entity/entities/BookInfo.ts @@ -20,66 +20,63 @@ import { BookInfoSearchKeywords } from './BookInfoSearchKeywords'; @Entity('book_info') export class BookInfo { @PrimaryGeneratedColumn({ type: 'int', name: 'id' }) - id?: number; + id?: number; @Column('varchar', { name: 'title', length: 255 }) - title?: string; + title?: string; @Column('varchar', { name: 'author', length: 255 }) - author?: string; + author?: string; @Column('varchar', { name: 'publisher', length: 255 }) - publisher?: string; + publisher?: string; @Column('varchar', { name: 'isbn', nullable: true, length: 255 }) - isbn?: string | null; + isbn?: string | null; @Column('varchar', { name: 'image', nullable: true, length: 255 }) - image?: string | null; + image?: string | null; @Column('date', { name: 'publishedAt', nullable: true }) - publishedAt?: string | null; + publishedAt?: string | null; @Column('datetime', { name: 'createdAt', default: () => "'CURRENT_TIMESTAMP(6)'", }) - createdAt?: Date; + createdAt?: Date; @Column('datetime', { name: 'updatedAt', default: () => "'CURRENT_TIMESTAMP(6)'", }) - updatedAt?: Date; + updatedAt?: Date; @Column('int', { name: 'categoryId' }) - categoryId?: number; + categoryId?: number; @OneToMany(() => Book, (book) => book.info) - books?: Book[]; + books?: Book[]; @ManyToOne(() => Category, (category) => category.bookInfos, { onDelete: 'NO ACTION', onUpdate: 'NO ACTION', }) @JoinColumn([{ name: 'categoryId', referencedColumnName: 'id' }]) - category?: Category; + category?: Category; @OneToMany(() => Likes, (likes) => likes.bookInfo) - likes?: Likes[]; + likes?: Likes[]; @OneToMany(() => Reservation, (reservation) => reservation.bookInfo) - reservations?: Reservation[]; + reservations?: Reservation[]; @OneToMany(() => Reviews, (reviews) => reviews.bookInfo) - reviews?: Reviews[]; + reviews?: Reviews[]; @OneToMany(() => SuperTag, (superTags) => superTags.userId) - superTags?: SuperTag[]; + superTags?: SuperTag[]; - @OneToOne( - () => BookInfoSearchKeywords, - (bookInfoSearchKeyword) => bookInfoSearchKeyword.bookInfo, - ) - bookInfoSearchKeyword?: BookInfoSearchKeywords; + @OneToOne(() => BookInfoSearchKeywords, (bookInfoSearchKeyword) => bookInfoSearchKeyword.bookInfo) + bookInfoSearchKeyword?: BookInfoSearchKeywords; } diff --git a/backend/src/entity/entities/BookInfoSearchKeywords.ts b/backend/src/entity/entities/BookInfoSearchKeywords.ts index 83d556c9..a51b03ee 100644 --- a/backend/src/entity/entities/BookInfoSearchKeywords.ts +++ b/backend/src/entity/entities/BookInfoSearchKeywords.ts @@ -1,36 +1,34 @@ -import { - Column, Entity, Index, JoinColumn, OneToOne, PrimaryGeneratedColumn, -} from 'typeorm'; +import { Column, Entity, Index, JoinColumn, OneToOne, PrimaryGeneratedColumn } from 'typeorm'; import { BookInfo } from './BookInfo'; @Index('FK_bookInfoId', ['bookInfoId'], {}) @Entity('book_info_search_keywords') export class BookInfoSearchKeywords { @PrimaryGeneratedColumn({ type: 'int', name: 'id' }) - id?: number; + id?: number; @Column('varchar', { name: 'disassembled_title', length: 255 }) - disassembledTitle?: string; + disassembledTitle?: string; @Column('varchar', { name: 'disassembled_author', length: 255 }) - disassembledAuthor?: string; + disassembledAuthor?: string; @Column('varchar', { name: 'disassembled_publisher', length: 255 }) - disassembledPublisher?: string; + disassembledPublisher?: string; @Column('varchar', { name: 'title_initials', length: 255 }) - titleInitials?: string; + titleInitials?: string; @Column('varchar', { name: 'author_initials', length: 255 }) - authorInitials?: string; + authorInitials?: string; @Column('varchar', { name: 'publisher_initials', length: 255 }) - publisherInitials?: string; + publisherInitials?: string; @Column('int', { name: 'book_info_id' }) - bookInfoId?: number; + bookInfoId?: number; @OneToOne(() => BookInfo, (bookInfo) => bookInfo.id) @JoinColumn([{ name: 'book_info_id', referencedColumnName: 'id' }]) - bookInfo?: BookInfo; + bookInfo?: BookInfo; } diff --git a/backend/src/entity/entities/Category.ts b/backend/src/entity/entities/Category.ts index 27c77fec..cd6cca5c 100644 --- a/backend/src/entity/entities/Category.ts +++ b/backend/src/entity/entities/Category.ts @@ -1,10 +1,4 @@ -import { - Column, - Entity, - Index, - OneToMany, - PrimaryGeneratedColumn, -} from 'typeorm'; +import { Column, Entity, Index, OneToMany, PrimaryGeneratedColumn } from 'typeorm'; import { BookInfo } from './BookInfo'; @Index('id', ['id'], { unique: true }) @@ -12,11 +6,11 @@ import { BookInfo } from './BookInfo'; @Entity('category', { schema: '42library' }) export class Category { @PrimaryGeneratedColumn({ type: 'int', name: 'id' }) - id: number; + id: number; @Column('varchar', { name: 'name', unique: true, length: 255 }) - name: string; + name: string; @OneToMany(() => BookInfo, (bookInfo) => bookInfo.category) - bookInfos: BookInfo[]; + bookInfos: BookInfo[]; } diff --git a/backend/src/entity/entities/Lending.ts b/backend/src/entity/entities/Lending.ts index bd77d783..4a321f4f 100644 --- a/backend/src/entity/entities/Lending.ts +++ b/backend/src/entity/entities/Lending.ts @@ -1,84 +1,76 @@ -import { - Column, - Entity, - Index, - JoinColumn, - ManyToOne, - PrimaryGeneratedColumn, -} from 'typeorm'; +import { Column, Entity, Index, JoinColumn, ManyToOne, PrimaryGeneratedColumn } from 'typeorm'; import { Book } from './Book'; import { User } from './User'; - @Index('FK_f2adde8c7d298210c39c500d966', ['lendingLibrarianId'], {}) - @Index('FK_returningLibrarianId', ['returningLibrarianId'], {}) +@Index('FK_f2adde8c7d298210c39c500d966', ['lendingLibrarianId'], {}) +@Index('FK_returningLibrarianId', ['returningLibrarianId'], {}) @Entity('lending', { schema: '42library' }) - export class Lending { @PrimaryGeneratedColumn({ type: 'int', name: 'id' }) - id: number; + id: number; @Column('int', { name: 'lendingLibrarianId' }) - lendingLibrarianId: number; + lendingLibrarianId: number; @Column('varchar', { name: 'lendingCondition', length: 255 }) - lendingCondition: string; + lendingCondition: string; @Column('int', { name: 'returningLibrarianId', nullable: true }) - returningLibrarianId: number | null; + returningLibrarianId: number | null; @Column('varchar', { name: 'returningCondition', nullable: true, length: 255, }) - returningCondition: string | null; + returningCondition: string | null; @Column('datetime', { name: 'returnedAt', nullable: true }) - returnedAt: Date | null; + returnedAt: Date | null; @Column('timestamp', { name: 'createdAt', default: () => "'CURRENT_TIMESTAMP(6)'", }) - createdAt: Date; + createdAt: Date; @Column('timestamp', { name: 'updatedAt', default: () => "'CURRENT_TIMESTAMP(6)'", }) - updatedAt: Date; + updatedAt: Date; @ManyToOne(() => Book, (book) => book.lendings, { onDelete: 'NO ACTION', onUpdate: 'NO ACTION', }) @JoinColumn([{ name: 'bookId', referencedColumnName: 'id' }]) - book: Book; + book: Book; @Column({ name: 'bookId', type: 'int' }) - bookId: number; + bookId: number; @ManyToOne(() => User, (user) => user.lendings, { onDelete: 'NO ACTION', onUpdate: 'NO ACTION', }) @JoinColumn([{ name: 'userId', referencedColumnName: 'id' }]) - user: User; + user: User; @Column({ name: 'userId', type: 'int' }) - userId: number; + userId: number; @ManyToOne(() => User, (user) => user.lendings2, { onDelete: 'NO ACTION', onUpdate: 'NO ACTION', }) @JoinColumn([{ name: 'lendingLibrarianId', referencedColumnName: 'id' }]) - lendingLibrarian: User; + lendingLibrarian: User; @ManyToOne(() => User, (user) => user.lendings3, { onDelete: 'NO ACTION', onUpdate: 'NO ACTION', }) @JoinColumn([{ name: 'returningLibrarianId', referencedColumnName: 'id' }]) - returningLibrarian: User; + returningLibrarian: User; } diff --git a/backend/src/entity/entities/Likes.ts b/backend/src/entity/entities/Likes.ts index b173470c..86a147c5 100644 --- a/backend/src/entity/entities/Likes.ts +++ b/backend/src/entity/entities/Likes.ts @@ -1,42 +1,34 @@ -import { - Column, - Entity, - Index, - JoinColumn, - ManyToOne, - PrimaryGeneratedColumn, -} from 'typeorm'; +import { Column, Entity, Index, JoinColumn, ManyToOne, PrimaryGeneratedColumn } from 'typeorm'; import { User } from './User'; import { BookInfo } from './BookInfo'; - @Index('FK_529dceb01ef681127fef04d755d4', ['userId'], {}) - @Index('FK_bookInfo3', ['bookInfoId'], {}) +@Index('FK_529dceb01ef681127fef04d755d4', ['userId'], {}) +@Index('FK_bookInfo3', ['bookInfoId'], {}) @Entity('likes', { schema: '42library' }) - export class Likes { @PrimaryGeneratedColumn({ type: 'int', name: 'id' }) - id: number; + id: number; @Column('int', { name: 'userId' }) - userId: number; + userId: number; @Column('int', { name: 'bookInfoId' }) - bookInfoId: number; + bookInfoId: number; @Column('tinyint', { name: 'isDeleted', width: 1, default: () => "'0'" }) - isDeleted: boolean; + isDeleted: boolean; @ManyToOne(() => User, (user) => user.likes, { onDelete: 'NO ACTION', onUpdate: 'NO ACTION', }) @JoinColumn([{ name: 'userId', referencedColumnName: 'id' }]) - user: User; + user: User; @ManyToOne(() => BookInfo, (bookInfo) => bookInfo.likes, { onDelete: 'NO ACTION', onUpdate: 'NO ACTION', }) @JoinColumn([{ name: 'bookInfoId', referencedColumnName: 'id' }]) - bookInfo: BookInfo; + bookInfo: BookInfo; } diff --git a/backend/src/entity/entities/Reservation.ts b/backend/src/entity/entities/Reservation.ts index ea652c1d..d8292147 100644 --- a/backend/src/entity/entities/Reservation.ts +++ b/backend/src/entity/entities/Reservation.ts @@ -1,66 +1,59 @@ -import { - Column, - Entity, - Index, - JoinColumn, - ManyToOne, - PrimaryGeneratedColumn, -} from 'typeorm'; +import { Column, Entity, Index, JoinColumn, ManyToOne, PrimaryGeneratedColumn } from 'typeorm'; import { User } from './User'; import { BookInfo } from './BookInfo'; import { Book } from './Book'; - @Index('FK_bookInfo', ['bookInfoId'], {}) +@Index('FK_bookInfo', ['bookInfoId'], {}) @Entity('reservation') export class Reservation { @PrimaryGeneratedColumn({ type: 'int', name: 'id' }) - id: number; + id: number; @Column('datetime', { name: 'endAt', nullable: true }) - endAt: Date | null; + endAt: Date | null; @Column('datetime', { name: 'createdAt', default: () => 'CURRENT_TIMESTAMP(6)', }) - createdAt: Date; + createdAt: Date; @Column('datetime', { name: 'updatedAt', default: () => 'CURRENT_TIMESTAMP(6)', }) - updatedAt: Date; + updatedAt: Date; @Column('int', { name: 'status', default: () => '0' }) - status: number; + status: number; @Column('int', { name: 'bookInfoId' }) - bookInfoId: number; + bookInfoId: number; - @Column('int', { name: 'userId' }) - userId: number; + @Column('int', { name: 'userId' }) + userId: number; @ManyToOne(() => User, (user) => user.reservations, { onDelete: 'NO ACTION', onUpdate: 'NO ACTION', }) @JoinColumn([{ name: 'userId', referencedColumnName: 'id' }]) - user: User; + user: User; @ManyToOne(() => BookInfo, (bookInfo) => bookInfo.reservations, { onDelete: 'NO ACTION', onUpdate: 'NO ACTION', }) @JoinColumn([{ name: 'bookInfoId', referencedColumnName: 'id' }]) - bookInfo: BookInfo; + bookInfo: BookInfo; @ManyToOne(() => Book, (book) => book.reservations, { onDelete: 'NO ACTION', onUpdate: 'NO ACTION', }) @JoinColumn([{ name: 'bookId', referencedColumnName: 'id' }]) - book: Book; + book: Book; @Column('int', { name: 'bookId', nullable: true }) - bookId: number | null; + bookId: number | null; } diff --git a/backend/src/entity/entities/Reviews.ts b/backend/src/entity/entities/Reviews.ts index 611e4e67..62b1e75d 100644 --- a/backend/src/entity/entities/Reviews.ts +++ b/backend/src/entity/entities/Reviews.ts @@ -1,69 +1,61 @@ -import { - Column, - Entity, - Index, - JoinColumn, - ManyToOne, - PrimaryGeneratedColumn, -} from 'typeorm'; +import { Column, Entity, Index, JoinColumn, ManyToOne, PrimaryGeneratedColumn } from 'typeorm'; import { User } from './User'; import { BookInfo } from './BookInfo'; - @Index('FK_529dceb01ef681127fef04d755d3', ['userId'], {}) - @Index('FK_bookInfo2', ['bookInfoId'], {}) +@Index('FK_529dceb01ef681127fef04d755d3', ['userId'], {}) +@Index('FK_bookInfo2', ['bookInfoId'], {}) @Entity('reviews') - export class Reviews { @PrimaryGeneratedColumn({ type: 'int', name: 'id' }) - id: number; + id: number; @Column('int', { name: 'userId' }) - userId: number; + userId: number; @Column('int', { name: 'bookInfoId' }) - bookInfoId: number; + bookInfoId: number; @Column('datetime', { name: 'createdAt', default: () => "'CURRENT_TIMESTAMP(6)'", }) - createdAt: Date; + createdAt: Date; @Column('datetime', { name: 'updatedAt', default: () => "'CURRENT_TIMESTAMP(6)'", }) - updatedAt: Date; + updatedAt: Date; @Column('int', { name: 'updateUserId' }) - updateUserId: number; + updateUserId: number; @Column('tinyint', { name: 'isDeleted', width: 1, default: () => "'0'" }) - isDeleted: boolean; + isDeleted: boolean; @Column('int', { name: 'deleteUserId', nullable: true }) - deleteUserId: number | null; + deleteUserId: number | null; @Column('text', { name: 'content' }) - content: string; + content: string; @Column('tinyint', { name: 'disabled', width: 1, default: () => "'0'" }) - disabled: boolean; + disabled: boolean; @Column('int', { name: 'disabledUserId', nullable: true }) - disabledUserId: number | null; + disabledUserId: number | null; @ManyToOne(() => User, (user) => user.reviews, { onDelete: 'NO ACTION', onUpdate: 'NO ACTION', }) @JoinColumn([{ name: 'userId', referencedColumnName: 'id' }]) - user: User; + user: User; @ManyToOne(() => BookInfo, (bookInfo) => bookInfo.reviews, { onDelete: 'NO ACTION', onUpdate: 'NO ACTION', }) @JoinColumn([{ name: 'bookInfoId', referencedColumnName: 'id' }]) - bookInfo: BookInfo; + bookInfo: BookInfo; } diff --git a/backend/src/entity/entities/SearchKeywords.ts b/backend/src/entity/entities/SearchKeywords.ts index aca0cfb6..b7407de6 100644 --- a/backend/src/entity/entities/SearchKeywords.ts +++ b/backend/src/entity/entities/SearchKeywords.ts @@ -1,22 +1,20 @@ -import { - Column, Entity, OneToMany, PrimaryGeneratedColumn, -} from 'typeorm'; +import { Column, Entity, OneToMany, PrimaryGeneratedColumn } from 'typeorm'; import { SearchLogs } from './SearchLogs'; @Entity('search_keywords') export class SearchKeywords { @PrimaryGeneratedColumn({ type: 'int', name: 'id' }) - id?: number; + id?: number; @Column('varchar', { name: 'keyword', length: 255 }) - keyword?: string; + keyword?: string; @Column('varchar', { name: 'disassembled_keyword', length: 255 }) - disassembledKeyword?: string; + disassembledKeyword?: string; @Column('varchar', { name: 'initial_consonants', length: 255 }) - initialConsonants?: string; + initialConsonants?: string; @OneToMany(() => SearchLogs, (searchLogs) => searchLogs.searchKeyword) - searchLogs?: SearchLogs[]; + searchLogs?: SearchLogs[]; } diff --git a/backend/src/entity/entities/SearchLogs.ts b/backend/src/entity/entities/SearchLogs.ts index 6e1b1d1b..35249e8e 100644 --- a/backend/src/entity/entities/SearchLogs.ts +++ b/backend/src/entity/entities/SearchLogs.ts @@ -1,29 +1,22 @@ -import { - Column, - Entity, - Index, - JoinColumn, - ManyToOne, - PrimaryGeneratedColumn, -} from 'typeorm'; +import { Column, Entity, Index, JoinColumn, ManyToOne, PrimaryGeneratedColumn } from 'typeorm'; import { SearchKeywords } from './SearchKeywords'; @Index('FK_searchKeywordId', ['searchKeywordId'], {}) @Entity('search_logs') export class SearchLogs { @PrimaryGeneratedColumn({ type: 'int', name: 'id' }) - id?: number; + id?: number; @Column('int', { name: 'search_keyword_id' }) - searchKeywordId?: number; + searchKeywordId?: number; @Column('varchar', { name: 'timestamp', length: 255 }) - timestamp?: string; + timestamp?: string; @ManyToOne(() => SearchKeywords, (SearchKeyword) => SearchKeyword.id, { onDelete: 'NO ACTION', onUpdate: 'NO ACTION', }) @JoinColumn([{ name: 'search_keyword_id', referencedColumnName: 'id' }]) - searchKeyword?: SearchKeywords; + searchKeyword?: SearchKeywords; } diff --git a/backend/src/entity/entities/SubTag.ts b/backend/src/entity/entities/SubTag.ts index 99a4c8f6..2124b394 100644 --- a/backend/src/entity/entities/SubTag.ts +++ b/backend/src/entity/entities/SubTag.ts @@ -1,11 +1,4 @@ -import { - Column, - Entity, - Index, - JoinColumn, - ManyToOne, - PrimaryGeneratedColumn, -} from 'typeorm'; +import { Column, Entity, Index, JoinColumn, ManyToOne, PrimaryGeneratedColumn } from 'typeorm'; import { User } from './User'; import { SuperTag } from './SuperTag'; @@ -14,52 +7,52 @@ import { SuperTag } from './SuperTag'; @Entity('sub_tag', { schema: 'jip_dev' }) export class SubTag { @PrimaryGeneratedColumn({ type: 'int', name: 'id' }) - id: number; + id: number; @Column('int', { name: 'userId' }) - userId: number; + userId: number; @Column('int', { name: 'superTagId' }) - superTagId: number; + superTagId: number; @Column('datetime', { name: 'createdAt', default: () => 'current_timestamp(6)', }) - createdAt: Date; + createdAt: Date; @Column('datetime', { name: 'updatedAt', default: () => 'current_timestamp(6)', }) - updatedAt: Date; + updatedAt: Date; @Column('tinyint', { name: 'isDeleted', default: () => '0' }) - isDeleted: number; + isDeleted: number; @Column('int', { name: 'updateUserId' }) - updateUserId: number; + updateUserId: number; @Column('varchar', { name: 'content', length: 42 }) - content: string; + content: string; @Column('tinyint', { name: 'isPublic' }) - isPublic: number; + isPublic: number; @ManyToOne(() => User, (user) => user.subTag, { onDelete: 'NO ACTION', onUpdate: 'NO ACTION', }) @JoinColumn([{ name: 'userId', referencedColumnName: 'id' }]) - user: User; + user: User; @ManyToOne(() => SuperTag, (superTag) => superTag.subTags, { onDelete: 'NO ACTION', onUpdate: 'NO ACTION', }) @JoinColumn([{ name: 'superTagId', referencedColumnName: 'id' }]) - superTag: SuperTag; + superTag: SuperTag; @JoinColumn([{ name: 'bookInfoId', referencedColumnName: 'id' }]) - bookInfoId: number; + bookInfoId: number; } diff --git a/backend/src/entity/entities/SuperTag.ts b/backend/src/entity/entities/SuperTag.ts index 25a6fd98..72a579e6 100644 --- a/backend/src/entity/entities/SuperTag.ts +++ b/backend/src/entity/entities/SuperTag.ts @@ -16,49 +16,49 @@ import { BookInfo } from './BookInfo'; @Entity('super_tag', { schema: 'jip_dev' }) export class SuperTag { @PrimaryGeneratedColumn({ type: 'int', name: 'id' }) - id: number; + id: number; @Column('int', { name: 'userId' }) - userId: number; + userId: number; @Column('int', { name: 'bookInfoId' }) - bookInfoId: number; + bookInfoId: number; @Column('datetime', { name: 'createdAt', default: () => 'current_timestamp(6)', }) - createdAt: Date; + createdAt: Date; @Column('datetime', { name: 'updatedAt', default: () => 'current_timestamp(6)', }) - updatedAt: Date; + updatedAt: Date; @Column('tinyint', { name: 'isDeleted', default: () => '0' }) - isDeleted: number; + isDeleted: number; @Column('int', { name: 'updateUserId' }) - updateUserId: number; + updateUserId: number; @Column('varchar', { name: 'content', length: 42 }) - content: string; + content: string; @OneToMany(() => SubTag, (subTag) => subTag.superTag) - subTags: SubTag[]; + subTags: SubTag[]; @ManyToOne(() => User, (user) => user.superTags, { onDelete: 'NO ACTION', onUpdate: 'NO ACTION', }) @JoinColumn([{ name: 'userId', referencedColumnName: 'id' }]) - user: User; + user: User; @ManyToOne(() => BookInfo, (bookInfo) => bookInfo.superTags, { onDelete: 'NO ACTION', onUpdate: 'NO ACTION', }) @JoinColumn([{ name: 'bookInfoId', referencedColumnName: 'id' }]) - bookInfo: BookInfo; + bookInfo: BookInfo; } diff --git a/backend/src/entity/entities/User.ts b/backend/src/entity/entities/User.ts index 1820c90f..6d7bf39e 100644 --- a/backend/src/entity/entities/User.ts +++ b/backend/src/entity/entities/User.ts @@ -1,10 +1,4 @@ -import { - Column, - Entity, - Index, - OneToMany, - PrimaryGeneratedColumn, -} from 'typeorm'; +import { Column, Entity, Index, OneToMany, PrimaryGeneratedColumn } from 'typeorm'; import { Book } from './Book'; import { Lending } from './Lending'; import { Likes } from './Likes'; @@ -19,19 +13,19 @@ import { SuperTag } from './SuperTag'; @Entity('user') export class User { @PrimaryGeneratedColumn({ type: 'int', name: 'id' }) - id: number; + id: number; @Column('varchar', { name: 'email', unique: true, length: 255 }) - email: string; + email: string; @Column('varchar', { name: 'password', length: 255, select: false }) - password: string; + password: string; @Column('varchar', { name: 'nickname', nullable: true, length: 255 }) - nickname: string | null; + nickname: string | null; @Column('int', { name: 'intraId', nullable: true, unique: true }) - intraId: number | null; + intraId: number | null; @Column('varchar', { name: 'slack', @@ -39,53 +33,53 @@ export class User { unique: true, length: 255, }) - slack: string | null; + slack: string | null; @Column('datetime', { name: 'penaltyEndDate', default: () => 'CURRENT_TIMESTAMP', }) - penaltyEndDate: Date; + penaltyEndDate: Date; @Column('tinyint', { name: 'role', default: () => '0' }) - role: number; + role: number; @Column('datetime', { name: 'createdAt', default: () => 'CURRENT_TIMESTAMP(6)', }) - createdAt: Date; + createdAt: Date; @Column('datetime', { name: 'updatedAt', default: () => 'CURRENT_TIMESTAMP(6)', }) - updatedAt: Date; + updatedAt: Date; @OneToMany(() => Book, (book) => book.donator2) - books: Book[]; + books: Book[]; @OneToMany(() => Lending, (lending) => lending.user) - lendings: Lending[]; + lendings: Lending[]; @OneToMany(() => Lending, (lending) => lending.lendingLibrarian) - lendings2: Lending[]; + lendings2: Lending[]; @OneToMany(() => Lending, (lending) => lending.returningLibrarian) - lendings3: Lending[]; + lendings3: Lending[]; @OneToMany(() => Likes, (likes) => likes.user) - likes: Likes[]; + likes: Likes[]; @OneToMany(() => Reservation, (reservation) => reservation.user) - reservations: Reservation[]; + reservations: Reservation[]; @OneToMany(() => Reviews, (reviews) => reviews.user) - reviews: Reviews[]; + reviews: Reviews[]; @OneToMany(() => SubTag, (subtag) => subtag.userId) - subTag: SubTag[]; + subTag: SubTag[]; @OneToMany(() => SuperTag, (superTags) => superTags.userId) - superTags: SuperTag[]; + superTags: SuperTag[]; } diff --git a/backend/src/entity/entities/UserReservation.ts b/backend/src/entity/entities/UserReservation.ts index 56e6e75d..64e99055 100644 --- a/backend/src/entity/entities/UserReservation.ts +++ b/backend/src/entity/entities/UserReservation.ts @@ -3,53 +3,53 @@ import { BookInfo } from './BookInfo'; import { Reservation } from './Reservation'; @ViewEntity({ - expression: (Data: DataSource) => Data - .createQueryBuilder() - .select('r.id', 'reservationId') - .addSelect('r.bookInfoId', 'reservedBookInfoId') - .addSelect('r.createdAt', 'reservationDate') - .addSelect('r.endAt', 'endAt') - .addSelect( - `(SELECT COUNT(*) + expression: (Data: DataSource) => + Data.createQueryBuilder() + .select('r.id', 'reservationId') + .addSelect('r.bookInfoId', 'reservedBookInfoId') + .addSelect('r.createdAt', 'reservationDate') + .addSelect('r.endAt', 'endAt') + .addSelect( + `(SELECT COUNT(*) FROM reservation WHERE (status = 0) AND (bookInfoId = reservedBookInfoId) AND (createdAt <= reservationDate))`, - 'ranking', - ) - .addSelect('bi.title', 'title') - .addSelect('bi.author', 'author') - .addSelect('bi.image', 'image') - .addSelect('r.userId', 'userId') - .from(Reservation, 'r') - .leftJoin(BookInfo, 'bi', 'r.bookInfoId = bi.id') - .where('r.status = 0'), + 'ranking', + ) + .addSelect('bi.title', 'title') + .addSelect('bi.author', 'author') + .addSelect('bi.image', 'image') + .addSelect('r.userId', 'userId') + .from(Reservation, 'r') + .leftJoin(BookInfo, 'bi', 'r.bookInfoId = bi.id') + .where('r.status = 0'), }) export class UserReservation { @ViewColumn() - reservationId: number; + reservationId: number; @ViewColumn() - reservedBookInfoId: number; + reservedBookInfoId: number; @ViewColumn() - reservationDate: Date; + reservationDate: Date; @ViewColumn() - endAt: Date; + endAt: Date; @ViewColumn() - ranking: number; + ranking: number; @ViewColumn() - title: string; + title: string; @ViewColumn() - author: string; + author: string; @ViewColumn() - image: string; + image: string; @ViewColumn() - userId: number; + userId: number; } diff --git a/backend/src/entity/entities/VHistories.ts b/backend/src/entity/entities/VHistories.ts index dae4c939..a7c5e4e5 100644 --- a/backend/src/entity/entities/VHistories.ts +++ b/backend/src/entity/entities/VHistories.ts @@ -2,74 +2,83 @@ import { DataSource, ViewColumn, ViewEntity } from 'typeorm'; // TODO: 대출자 id로 검색 가능하게 @ViewEntity({ - expression: (Data: DataSource) => Data - .createQueryBuilder() - .select('l.id', 'id') - .addSelect('lendingCondition', 'lendingCondition') - .addSelect('u.nickname', 'login') - .addSelect('l.returningCondition', 'returningCondition') - .addSelect(` + expression: (Data: DataSource) => + Data.createQueryBuilder() + .select('l.id', 'id') + .addSelect('lendingCondition', 'lendingCondition') + .addSelect('u.nickname', 'login') + .addSelect('l.returningCondition', 'returningCondition') + .addSelect( + ` CASE WHEN NOW() > u.penaltyEndDate THEN 0 ELSE DATEDIFF(u.penaltyEndDate, NOW()) END - `, 'penaltyDays') - .addSelect('b.callSign', 'callSign') - .addSelect('bi.title', 'title') - .addSelect('bi.id', 'bookInfoId') - .addSelect('bi.image', 'image') - .addSelect('DATE_FORMAT(l.createdAt, "%Y-%m-%d")', 'createdAt') - .addSelect('DATE_FORMAT(l.returnedAt, "%Y-%m-%d")', 'returnedAt') - .addSelect('DATE_FORMAT(l.updatedAt, "%Y-%m-%d")', 'updatedAt') - .addSelect("DATE_FORMAT(DATE_ADD(l.createdAt, interval 14 day), '%Y-%m-%d')", 'dueDate') - .addSelect('(SELECT nickname FROM user WHERE user.id = lendingLibrarianId)', 'lendingLibrarianNickName') - .addSelect('(SELECT nickname FROM user WHERE user.id = returningLibrarianId)', 'returningLibrarianNickname') - .from('lending', 'l') - .innerJoin('user', 'u', 'l.userId = u.id') - .innerJoin('book', 'b', 'l.bookId = b.id') - .leftJoin('book_info', 'bi', 'b.infoId = bi.id'), + `, + 'penaltyDays', + ) + .addSelect('b.callSign', 'callSign') + .addSelect('bi.title', 'title') + .addSelect('bi.id', 'bookInfoId') + .addSelect('bi.image', 'image') + .addSelect('DATE_FORMAT(l.createdAt, "%Y-%m-%d")', 'createdAt') + .addSelect('DATE_FORMAT(l.returnedAt, "%Y-%m-%d")', 'returnedAt') + .addSelect('DATE_FORMAT(l.updatedAt, "%Y-%m-%d")', 'updatedAt') + .addSelect("DATE_FORMAT(DATE_ADD(l.createdAt, interval 14 day), '%Y-%m-%d')", 'dueDate') + .addSelect( + '(SELECT nickname FROM user WHERE user.id = lendingLibrarianId)', + 'lendingLibrarianNickName', + ) + .addSelect( + '(SELECT nickname FROM user WHERE user.id = returningLibrarianId)', + 'returningLibrarianNickname', + ) + .from('lending', 'l') + .innerJoin('user', 'u', 'l.userId = u.id') + .innerJoin('book', 'b', 'l.bookId = b.id') + .leftJoin('book_info', 'bi', 'b.infoId = bi.id'), }) export class VHistories { @ViewColumn() - id: number; + id: number; @ViewColumn() - lendingCondition: string; + lendingCondition: string; @ViewColumn() - login: string; + login: string; @ViewColumn() - returningCondition: string; + returningCondition: string; @ViewColumn() - penaltyDays: number; + penaltyDays: number; @ViewColumn() - callSign: string; + callSign: string; @ViewColumn() - title: string; + title: string; @ViewColumn() - bookInfoId: number; + bookInfoId: number; @ViewColumn() - image: string; + image: string; @ViewColumn() - createdAt: Date; + createdAt: Date; @ViewColumn() - returnedAt: Date; + returnedAt: Date; @ViewColumn() - updatedAt: Date; + updatedAt: Date; @ViewColumn() - dueDate: Date; + dueDate: Date; @ViewColumn() - lendingLibrarianNickName: string; + lendingLibrarianNickName: string; @ViewColumn() - returningLibrarianNickname: string; + returningLibrarianNickname: string; } diff --git a/backend/src/entity/entities/VLending.ts b/backend/src/entity/entities/VLending.ts index 4e7fdb9f..71f086ca 100644 --- a/backend/src/entity/entities/VLending.ts +++ b/backend/src/entity/entities/VLending.ts @@ -1,55 +1,58 @@ import { DataSource, ViewColumn, ViewEntity } from 'typeorm'; @ViewEntity({ - expression: (Data: DataSource) => Data - .createQueryBuilder() - .select('l.id', 'id') - .addSelect('l.lendingCondition', 'lendingCondition') - .addSelect('u.nickname', 'login') - .addSelect('CASE WHEN NOW() > u.penaltyEndDate THEN 0 ELSE DATEDIFF(u.penaltyEndDate, now()) END', 'penaltyDays') - .addSelect('b.id', 'bookId') - .addSelect('b.callSign', 'callSign') - .addSelect('bi.title', 'title') - .addSelect('bi.image', 'image') - .addSelect('date_format(l.createdAt, \'%Y-%m-%d\')', 'createdAt') - .addSelect('date_format(l.returnedAt, \'%Y-%m-%d\')', 'returnedAt') - .addSelect('date_format(DATE_ADD(l.createdAt, INTERVAL 14 DAY), \'%Y-%m-%d\')', 'dueDate') - .from('lending', 'l') - .innerJoin('user', 'u', 'l.userId = u.id') - .leftJoin('book', 'b', 'l.bookId = b.id') - .leftJoin('book_info', 'bi', 'b.infoid = bi.id'), + expression: (Data: DataSource) => + Data.createQueryBuilder() + .select('l.id', 'id') + .addSelect('l.lendingCondition', 'lendingCondition') + .addSelect('u.nickname', 'login') + .addSelect( + 'CASE WHEN NOW() > u.penaltyEndDate THEN 0 ELSE DATEDIFF(u.penaltyEndDate, now()) END', + 'penaltyDays', + ) + .addSelect('b.id', 'bookId') + .addSelect('b.callSign', 'callSign') + .addSelect('bi.title', 'title') + .addSelect('bi.image', 'image') + .addSelect("date_format(l.createdAt, '%Y-%m-%d')", 'createdAt') + .addSelect("date_format(l.returnedAt, '%Y-%m-%d')", 'returnedAt') + .addSelect("date_format(DATE_ADD(l.createdAt, INTERVAL 14 DAY), '%Y-%m-%d')", 'dueDate') + .from('lending', 'l') + .innerJoin('user', 'u', 'l.userId = u.id') + .leftJoin('book', 'b', 'l.bookId = b.id') + .leftJoin('book_info', 'bi', 'b.infoid = bi.id'), }) export class VLending { @ViewColumn() - id: number; + id: number; @ViewColumn() - lendingCondition: string; + lendingCondition: string; @ViewColumn() - login: string; + login: string; @ViewColumn() - penaltyDays: number; + penaltyDays: number; @ViewColumn() - bookId: number; + bookId: number; @ViewColumn() - callSign: string; + callSign: string; @ViewColumn() - title: string; + title: string; @ViewColumn() - image: string; + image: string; @ViewColumn() - createdAt: Date; + createdAt: Date; @ViewColumn() - returnedAt: Date; + returnedAt: Date; @ViewColumn() - dueDate: Date; + dueDate: Date; } diff --git a/backend/src/entity/entities/VLendingForSearchUser.ts b/backend/src/entity/entities/VLendingForSearchUser.ts index cb5c3c6c..6690c251 100644 --- a/backend/src/entity/entities/VLendingForSearchUser.ts +++ b/backend/src/entity/entities/VLendingForSearchUser.ts @@ -1,52 +1,58 @@ import { DataSource, ViewColumn, ViewEntity } from 'typeorm'; @ViewEntity('v_lending_for_search_user', { - expression: (Data: DataSource) => Data - .createQueryBuilder() - .addSelect('u.id', 'userId') - .addSelect('bi.id', 'bookInfoId') - .addSelect('l.createdAt', 'lendDate') - .addSelect('l.lendingCondition', 'lendingCondition') - .addSelect('bi.image', 'image') - .addSelect('bi.author', 'author') - .addSelect('bi.title', 'title') - .addSelect('DATE_ADD(l.createdAt, INTERVAL 14 DAY)', 'duedate') - .addSelect('CASE WHEN DATEDIFF(now(), DATE_ADD(l.createdAt, INTERVAL 14 DAY)) < 0 THEN 0 ELSE DATEDIFF(now(), DATE_ADD(l.createdAt, INTERVAL 14 DAY)) END', 'overDueDay') - .addSelect('(SELECT COUNT(r.id) FROM reservation r WHERE r.bookInfoId = bi.id AND r.status = 0)', 'reservedNum') - .from('lending', 'l') - .where('l.returnedAt is NULL') - .innerJoin('user', 'u', 'l.userId = u.id') - .leftJoin('book', 'b', 'l.bookId = b.id') - .leftJoin('book_info', 'bi', 'b.infoid = bi.id'), + expression: (Data: DataSource) => + Data.createQueryBuilder() + .addSelect('u.id', 'userId') + .addSelect('bi.id', 'bookInfoId') + .addSelect('l.createdAt', 'lendDate') + .addSelect('l.lendingCondition', 'lendingCondition') + .addSelect('bi.image', 'image') + .addSelect('bi.author', 'author') + .addSelect('bi.title', 'title') + .addSelect('DATE_ADD(l.createdAt, INTERVAL 14 DAY)', 'duedate') + .addSelect( + 'CASE WHEN DATEDIFF(now(), DATE_ADD(l.createdAt, INTERVAL 14 DAY)) < 0 THEN 0 ELSE DATEDIFF(now(), DATE_ADD(l.createdAt, INTERVAL 14 DAY)) END', + 'overDueDay', + ) + .addSelect( + '(SELECT COUNT(r.id) FROM reservation r WHERE r.bookInfoId = bi.id AND r.status = 0)', + 'reservedNum', + ) + .from('lending', 'l') + .where('l.returnedAt is NULL') + .innerJoin('user', 'u', 'l.userId = u.id') + .leftJoin('book', 'b', 'l.bookId = b.id') + .leftJoin('book_info', 'bi', 'b.infoid = bi.id'), }) export class VLendingForSearchUser { @ViewColumn() - userId: number; + userId: number; @ViewColumn() - bookInfoId: number; + bookInfoId: number; @ViewColumn() - lendDate: Date; + lendDate: Date; @ViewColumn() - lendingCondition: string; + lendingCondition: string; @ViewColumn() - image: string; + image: string; @ViewColumn() - author: string; + author: string; @ViewColumn() - title: string; + title: string; @ViewColumn() - duedate: Date; + duedate: Date; @ViewColumn() - overDueDay: Date; + overDueDay: Date; @ViewColumn() - reservedNum: number; + reservedNum: number; } diff --git a/backend/src/entity/entities/VSearchBook.ts b/backend/src/entity/entities/VSearchBook.ts index 6986aa84..f1d82ddd 100644 --- a/backend/src/entity/entities/VSearchBook.ts +++ b/backend/src/entity/entities/VSearchBook.ts @@ -4,74 +4,75 @@ import { Book } from './Book'; import { Category } from './Category'; @ViewEntity('v_search_book', { - expression: (Data: DataSource) => Data.createQueryBuilder() - .select('book.infoId', 'bookInfoId') - .addSelect('book_info.title', 'title') - .addSelect('book_info.author', 'author') - .addSelect('book_info.publisher', 'publisher') - .addSelect("DATE_FORMAT(book_info.publishedAt, '%Y%m%d')", 'publishedAt') - .addSelect('book_info.isbn', 'isbn') - .addSelect('book_info.image', 'image') - .addSelect('book.callSign', 'callSign') - .addSelect('book.id', 'bookId') - .addSelect('book.status', 'status') - .addSelect('book.donator', 'donator') - .addSelect('book_info.categoryId', 'categoryId') - .addSelect('category.name', 'category') - .addSelect( - ' IF((\n' - + ' IF((select COUNT(*) from lending as l where l.bookId = book.id and l.returnedAt is NULL) = 0, TRUE, FALSE)\n' - + ' AND\n' - + ' IF((select COUNT(*) from book as b where (b.id = book.id and b.status = 0)) = 1, TRUE, FALSE)\n' - + ' AND\n' - + ' IF((select COUNT(*) from reservation as r where (r.bookId = book.id and status = 0)) = 0, TRUE, FALSE)\n' - + ' ), TRUE, FALSE)', - 'isLendable', - ) - .from(Book, 'book') - .leftJoin(BookInfo, 'book_info', 'book_info.id = book.infoId') - .leftJoin(Category, 'category', 'book_info.categoryId = category.id'), + expression: (Data: DataSource) => + Data.createQueryBuilder() + .select('book.infoId', 'bookInfoId') + .addSelect('book_info.title', 'title') + .addSelect('book_info.author', 'author') + .addSelect('book_info.publisher', 'publisher') + .addSelect("DATE_FORMAT(book_info.publishedAt, '%Y%m%d')", 'publishedAt') + .addSelect('book_info.isbn', 'isbn') + .addSelect('book_info.image', 'image') + .addSelect('book.callSign', 'callSign') + .addSelect('book.id', 'bookId') + .addSelect('book.status', 'status') + .addSelect('book.donator', 'donator') + .addSelect('book_info.categoryId', 'categoryId') + .addSelect('category.name', 'category') + .addSelect( + ' IF((\n' + + ' IF((select COUNT(*) from lending as l where l.bookId = book.id and l.returnedAt is NULL) = 0, TRUE, FALSE)\n' + + ' AND\n' + + ' IF((select COUNT(*) from book as b where (b.id = book.id and b.status = 0)) = 1, TRUE, FALSE)\n' + + ' AND\n' + + ' IF((select COUNT(*) from reservation as r where (r.bookId = book.id and status = 0)) = 0, TRUE, FALSE)\n' + + ' ), TRUE, FALSE)', + 'isLendable', + ) + .from(Book, 'book') + .leftJoin(BookInfo, 'book_info', 'book_info.id = book.infoId') + .leftJoin(Category, 'category', 'book_info.categoryId = category.id'), }) export class VSearchBook { @ViewColumn() - bookId: number; + bookId: number; @ViewColumn() - bookInfoId: number; + bookInfoId: number; @ViewColumn() - title: string; + title: string; @ViewColumn() - author: string; + author: string; @ViewColumn() - donator: string; + donator: string; @ViewColumn() - publisher: string; + publisher: string; @ViewColumn() - publishedAt: string; + publishedAt: string; @ViewColumn() - isbn: string; + isbn: string; @ViewColumn() - image: string; + image: string; @ViewColumn() - status: number; + status: number; @ViewColumn() - categoryId: string; + categoryId: string; @ViewColumn() - callSign: string; + callSign: string; @ViewColumn() - category: string; + category: string; @ViewColumn() - isLendable: boolean; + isLendable: boolean; } diff --git a/backend/src/entity/entities/VSearchBookByTag.ts b/backend/src/entity/entities/VSearchBookByTag.ts index 69834260..5d74c981 100644 --- a/backend/src/entity/entities/VSearchBookByTag.ts +++ b/backend/src/entity/entities/VSearchBookByTag.ts @@ -5,63 +5,71 @@ import { SubTag } from './SubTag'; import { SuperTag } from './SuperTag'; @ViewEntity('v_search_book_by_tag', { - expression: (Data: DataSource) => Data.createQueryBuilder() - .distinctOn(['bi.id']) - .select('bi.id', 'id') - .addSelect('bi.title', 'title') - .addSelect('bi.author', 'author') - .addSelect('bi.isbn', 'isbn') - .addSelect('bi.image', 'image') - .addSelect('bi.publishedAt', 'publishedAt') - .addSelect('bi.createdAt', 'createdAt') - .addSelect('bi.updatedAt', 'updatedAt') - .addSelect('c.name', 'category') - .addSelect('sp.content', 'superTagContent') - .addSelect('sb.content', 'subTagContent') - .addSelect((subQuery) => subQuery - .select('COUNT(l.id)') - .from('book', 'b') - .leftJoin('lending', 'l', 'l.bookId = b.id') - .innerJoin('book_info', 'bi2', 'bi2.id = b.infoId') - .where('bi.id = bi.id'), 'lendingCnt') - .from(BookInfo, 'bi') - .innerJoin(Category, 'c', 'c.id = bi.categoryId') - .innerJoin(SuperTag, 'sp', 'sp.bookInfoId = bi.id') - .leftJoin(SubTag, 'sb', 'sb.superTagId = sp.id'), + expression: (Data: DataSource) => + Data.createQueryBuilder() + .distinctOn(['bi.id']) + .select('bi.id', 'id') + .addSelect('bi.title', 'title') + .addSelect('bi.author', 'author') + .addSelect('bi.isbn', 'isbn') + .addSelect('bi.image', 'image') + .addSelect('bi.publishedAt', 'publishedAt') + .addSelect('bi.createdAt', 'createdAt') + .addSelect('bi.updatedAt', 'updatedAt') + .addSelect('c.name', 'category') + .addSelect('sp.content', 'superTagContent') + .addSelect('sb.content', 'subTagContent') + .addSelect( + (subQuery) => + subQuery + .select('COUNT(l.id)') + .from('book', 'b') + .leftJoin('lending', 'l', 'l.bookId = b.id') + .innerJoin('book_info', 'bi2', 'bi2.id = b.infoId') + .where('bi.id = bi.id'), + 'lendingCnt', + ) + .from(BookInfo, 'bi') + .innerJoin(Category, 'c', 'c.id = bi.categoryId') + .innerJoin(SuperTag, 'sp', 'sp.bookInfoId = bi.id') + .leftJoin(SubTag, 'sb', 'sb.superTagId = sp.id'), }) export class VSearchBookByTag { @ViewColumn() - id: number; + id: number; @ViewColumn() - title: string; + title: string; @ViewColumn() - isbn: number; + author: string; @ViewColumn() - image: string; + isbn: number; @ViewColumn() - publishedAt: string; + image: string; @ViewColumn() - createdAt: string; + publishedAt: string; @ViewColumn() - updatedAt: string; + createdAt: string; @ViewColumn() - category: string; + updatedAt: string; @ViewColumn() - superTagContent: string; + category: string; @ViewColumn() - subTagContent: string; + superTagContent: string; @ViewColumn() - lendingCnt: number; + subTagContent: string; + + @ViewColumn() + lendingCnt: number; } export default VSearchBookByTag; diff --git a/backend/src/entity/entities/VStock.ts b/backend/src/entity/entities/VStock.ts index 18113f66..4b81cd10 100644 --- a/backend/src/entity/entities/VStock.ts +++ b/backend/src/entity/entities/VStock.ts @@ -6,72 +6,71 @@ import { Lending } from './Lending'; import { Reservation } from './Reservation'; @ViewEntity('v_stock', { - expression: (Data: DataSource) => Data.createQueryBuilder() - .select('book.infoId', 'bookInfoId') - .addSelect('book_info.title', 'title') - .addSelect('book_info.author', 'author') - .addSelect('book_info.publisher', 'publisher') - .addSelect("DATE_FORMAT(book_info.publishedAt, '%Y%m%d')", 'publishedAt') - .addSelect('book_info.isbn', 'isbn') - .addSelect('book_info.image', 'image') - .addSelect('book.callSign', 'callSign') - .addSelect('book.id', 'bookId') - .addSelect('book.status', 'status') - .addSelect('book.donator', 'donator') - .addSelect("date_format(book.updatedAt, '%Y-%m-%d %T')", 'updatedAt') - .addSelect('book_info.categoryId', 'categoryId') - .addSelect('category.name', 'category') - .from(Book, 'book') - .leftJoin(BookInfo, 'book_info', 'book_info.id = book.infoId') - .leftJoin(Category, 'category', 'book_info.categoryId = category.id') - .leftJoin(Lending, 'l', 'book.id = l.bookId') - .leftJoin(Reservation, 'r', 'r.bookId = book.id AND r.status = 0') - .groupBy('book.id') - .having('COUNT(l.id) = COUNT(l.returnedAt) AND COUNT(r.id) = 0') - .where('book.status = 0'), + expression: (Data: DataSource) => + Data.createQueryBuilder() + .select('book.infoId', 'bookInfoId') + .addSelect('book_info.title', 'title') + .addSelect('book_info.author', 'author') + .addSelect('book_info.publisher', 'publisher') + .addSelect("DATE_FORMAT(book_info.publishedAt, '%Y%m%d')", 'publishedAt') + .addSelect('book_info.isbn', 'isbn') + .addSelect('book_info.image', 'image') + .addSelect('book.callSign', 'callSign') + .addSelect('book.id', 'bookId') + .addSelect('book.status', 'status') + .addSelect('book.donator', 'donator') + .addSelect("date_format(book.updatedAt, '%Y-%m-%d %T')", 'updatedAt') + .addSelect('book_info.categoryId', 'categoryId') + .addSelect('category.name', 'category') + .from(Book, 'book') + .leftJoin(BookInfo, 'book_info', 'book_info.id = book.infoId') + .leftJoin(Category, 'category', 'book_info.categoryId = category.id') + .leftJoin(Lending, 'l', 'book.id = l.bookId') + .leftJoin(Reservation, 'r', 'r.bookId = book.id AND r.status = 0') + .groupBy('book.id') + .having('COUNT(l.id) = COUNT(l.returnedAt) AND COUNT(r.id) = 0') + .where('book.status = 0'), }) export class VStock { @ViewColumn() - bookId: number; + bookId: number; @ViewColumn() - bookInfoId: number; + bookInfoId: number; @ViewColumn() - title: string; + title: string; @ViewColumn() - author: string; + author: string; @ViewColumn() - donator: string; + donator: string; @ViewColumn() - publisher: string; + publisher: string; @ViewColumn() - publishedAt: string; + publishedAt: string; @ViewColumn() - isbn: string; + isbn: string; @ViewColumn() - image: string; + image: string; @ViewColumn() - status: number; + status: number; @ViewColumn() - categoryId: number; + categoryId: number; @ViewColumn() - callSign: string; + callSign: string; @ViewColumn() - category: string; + category: string; @ViewColumn() - updatedAt: Date; + updatedAt: Date; } - - diff --git a/backend/src/entity/entities/VTagsSubDefault.ts b/backend/src/entity/entities/VTagsSubDefault.ts index c933ac9c..ca3ad9d0 100644 --- a/backend/src/entity/entities/VTagsSubDefault.ts +++ b/backend/src/entity/entities/VTagsSubDefault.ts @@ -5,56 +5,55 @@ import { SubTag } from './SubTag'; import { User } from './User'; @ViewEntity('v_tags_sub_default', { - expression: (Data: DataSource) => Data.createQueryBuilder() - .select('sp.bookInfoId', 'bookInfoId') - .addSelect('bi.title', 'title') - .addSelect('sb.id', 'id') - .addSelect('DATE_FORMAT(sb.createdAt, "%Y-%m-%d")', 'createdAt') - .addSelect('u.nickname', 'login') - .addSelect('sb.content', 'content') - .addSelect('sp.id', 'superTagId') - .addSelect('sp.content', 'superContent') - .addSelect('sb.isPublic', 'isPublic') - .addSelect('sb.isDeleted', 'isDeleted') - .addSelect('CASE WHEN sb.isPublic = 1 THEN \'public\' ELSE 1 \'private\' END', 'visibility') - .from(SuperTag, 'sp') - .innerJoin(SubTag, 'sb', 'sb.superTagId = sp.id') - .innerJoin(BookInfo, 'bi', 'bi.id = sp.bookInfoId') - .innerJoin(User, 'u', 'u.id = sb.userId'), + expression: (Data: DataSource) => + Data.createQueryBuilder() + .select('sp.bookInfoId', 'bookInfoId') + .addSelect('bi.title', 'title') + .addSelect('sb.id', 'id') + .addSelect('DATE_FORMAT(sb.createdAt, "%Y-%m-%d")', 'createdAt') + .addSelect('u.nickname', 'login') + .addSelect('sb.content', 'content') + .addSelect('sp.id', 'superTagId') + .addSelect('sp.content', 'superContent') + .addSelect('sb.isPublic', 'isPublic') + .addSelect('sb.isDeleted', 'isDeleted') + .addSelect("CASE WHEN sb.isPublic = 1 THEN 'public' ELSE 1 'private' END", 'visibility') + .from(SuperTag, 'sp') + .innerJoin(SubTag, 'sb', 'sb.superTagId = sp.id') + .innerJoin(BookInfo, 'bi', 'bi.id = sp.bookInfoId') + .innerJoin(User, 'u', 'u.id = sb.userId'), }) export class VTagsSubDefault { @ViewColumn() - bookInfoId: number; + bookInfoId: number; @ViewColumn() - title: string; + title: string; @ViewColumn() - id: number; + id: number; @ViewColumn() - createdAt: string; + createdAt: string; @ViewColumn() - login: string; + login: string; @ViewColumn() - content: string; + content: string; @ViewColumn() - superTagId: number; + superTagId: number; @ViewColumn() - superContent: string; + superContent: string; @ViewColumn() - isPublic: boolean; + isPublic: boolean; @ViewColumn() - isDeleted: boolean; + isDeleted: boolean; @ViewColumn() - visibility: string; + visibility: string; } - - diff --git a/backend/src/entity/entities/VTagsSuperDefault.ts b/backend/src/entity/entities/VTagsSuperDefault.ts index beb8ee26..d5f5255b 100644 --- a/backend/src/entity/entities/VTagsSuperDefault.ts +++ b/backend/src/entity/entities/VTagsSuperDefault.ts @@ -50,14 +50,14 @@ import { ObjectLiteral, SelectQueryBuilder } from 'typeorm/browser'; }) export class VTagsSuperDefault { @ViewColumn() - content: string; + content: string; @ViewColumn() - count: number; + count: number; @ViewColumn() - type: string; + type: string; @ViewColumn() - createdAt: string; + createdAt: string; } diff --git a/backend/src/entity/entities/VUserLending.ts b/backend/src/entity/entities/VUserLending.ts index 4adfb01b..39f60460 100644 --- a/backend/src/entity/entities/VUserLending.ts +++ b/backend/src/entity/entities/VUserLending.ts @@ -1,45 +1,46 @@ import { DataSource, ViewColumn, ViewEntity } from 'typeorm'; @ViewEntity({ - expression: (Data: DataSource) => Data - .createQueryBuilder() - .select('l.userId', 'userId') - .addSelect('date_format(l.createdAt, \'%Y-%m-%d\')', 'lendDate') - .addSelect('l.lendingCondition', 'lendingCondition') - .addSelect('bi.id', 'bookInfoId') - .addSelect('bi.title', 'title') - .addSelect('date_format(DATE_ADD(l.createdAt, INTERVAL 14 DAY), \'%Y-%m-%d\')', 'duedate') - .addSelect('bi.image', 'image') - .addSelect('CASE WHEN DATEDIFF(now(), DATE_ADD(l.createdAt, INTERVAL 14 DAY)) < 0 THEN 0 ELSE DATEDIFF(now(), DATE_ADD(l.createdAt, INTERVAL 14 DAY)) END', 'overDueDay') - .from('lending', 'l') - .leftJoin('book', 'b', 'l.bookId = b.id') - .leftJoin('book_info', 'bi', 'b.infoid = bi.id') - .where('l.returnedAt IS NULL'), + expression: (Data: DataSource) => + Data.createQueryBuilder() + .select('l.userId', 'userId') + .addSelect("date_format(l.createdAt, '%Y-%m-%d')", 'lendDate') + .addSelect('l.lendingCondition', 'lendingCondition') + .addSelect('bi.id', 'bookInfoId') + .addSelect('bi.title', 'title') + .addSelect("date_format(DATE_ADD(l.createdAt, INTERVAL 14 DAY), '%Y-%m-%d')", 'duedate') + .addSelect('bi.image', 'image') + .addSelect( + 'CASE WHEN DATEDIFF(now(), DATE_ADD(l.createdAt, INTERVAL 14 DAY)) < 0 THEN 0 ELSE DATEDIFF(now(), DATE_ADD(l.createdAt, INTERVAL 14 DAY)) END', + 'overDueDay', + ) + .from('lending', 'l') + .leftJoin('book', 'b', 'l.bookId = b.id') + .leftJoin('book_info', 'bi', 'b.infoid = bi.id') + .where('l.returnedAt IS NULL'), }) export class VUserLending { @ViewColumn() - userId: number; + userId: number; @ViewColumn() - lendDate: Date; + lendDate: Date; @ViewColumn() - lendingCondition: string; + lendingCondition: string; @ViewColumn() - bookInfoId: number; + bookInfoId: number; @ViewColumn() - title: string; + title: string; @ViewColumn() - duedate: Date; + duedate: Date; @ViewColumn() - image: string; + image: string; @ViewColumn() - overDueDay: number; + overDueDay: number; } - - diff --git a/backend/src/kysely/generated.ts b/backend/src/kysely/generated.ts index 047ccd5d..7a1de228 100644 --- a/backend/src/kysely/generated.ts +++ b/backend/src/kysely/generated.ts @@ -1,4 +1,4 @@ -import type { ColumnType, SqlBool } from "kysely"; +import type { ColumnType, SqlBool } from 'kysely'; export type Generated = T extends ColumnType ? ColumnType @@ -22,7 +22,7 @@ export interface BookInfo { publisher: string; isbn: Generated; image: Generated; - publishedAt: Generated; + publishedAt: Generated; createdAt: Generated; updatedAt: Generated; categoryId: number; diff --git a/backend/src/kysely/mod.ts b/backend/src/kysely/mod.ts index b0e24ea3..fcd06fe5 100644 --- a/backend/src/kysely/mod.ts +++ b/backend/src/kysely/mod.ts @@ -18,7 +18,7 @@ const dialect = new MysqlDialect({ export const db = new Kysely({ dialect, - log: event => console.log('kysely:', event.query.sql, event.query.parameters), + log: (event) => console.log('kysely:', event.query.sql, event.query.parameters), }); export type Database = typeof db; diff --git a/backend/src/kysely/paginated.ts b/backend/src/kysely/paginated.ts index 414e1138..30e582ff 100644 --- a/backend/src/kysely/paginated.ts +++ b/backend/src/kysely/paginated.ts @@ -1,12 +1,12 @@ import { SelectQueryBuilder } from 'kysely'; type Paginated = { - items: O[], + items: O[]; meta: { totalItems: number; totalPages: number; }; -} +}; export const metaPaginated = async ( qb: SelectQueryBuilder, diff --git a/backend/src/kysely/shared.ts b/backend/src/kysely/shared.ts index 58fe8de1..1eb4f97e 100644 --- a/backend/src/kysely/shared.ts +++ b/backend/src/kysely/shared.ts @@ -8,15 +8,12 @@ const throwIf = (value: T, ok: (v: T) => boolean) => { throw new Error(`값이 예상과 달리 ${value}입니다`); }; -export type Visibility = 'public' | 'hidden' | 'all' +export type Visibility = 'public' | 'hidden' | 'all'; const roles = ['user', 'cadet', 'librarian', 'staff'] as const; -export type Role = typeof roles[number] +export type Role = (typeof roles)[number]; -const fromEnum = (role: number): Role => - throwIf(roles[role], (v) => v === undefined); +const fromEnum = (role: number): Role => throwIf(roles[role], (v) => v === undefined); -export const toRole = (role: Role): number => - throwIf(roles.indexOf(role), (v) => v === -1); +export const toRole = (role: Role): number => throwIf(roles.indexOf(role), (v) => v === -1); -export const roleSchema = z.number().int().min(0).max(3) - .transform(fromEnum); +export const roleSchema = z.number().int().min(0).max(3).transform(fromEnum); diff --git a/backend/src/kysely/sqlDates.ts b/backend/src/kysely/sqlDates.ts index 5d0af7b7..60ca8e7e 100644 --- a/backend/src/kysely/sqlDates.ts +++ b/backend/src/kysely/sqlDates.ts @@ -12,10 +12,7 @@ export const dateNow = () => sql`NOW()`; * * @todo(@scarf005): 임의의 날짜 형식을 타입 안전하게 사용 */ -export function dateFormat( - expr: Expression, - format: '%Y-%m-%d', -): RawBuilder; +export function dateFormat(expr: Expression, format: '%Y-%m-%d'): RawBuilder; export function dateFormat( expr: Expression, @@ -30,33 +27,28 @@ export function dateFormat(expr: Expression, format: '%Y-%m-%d') { * * @see {@link https://dev.mysql.com/doc/refman/8.0/en/date-and-time-functions.html#function_date-add | DATE_ADD} */ -export function dateAddDays( - expr: Expression, - days: number, -): RawBuilder; +export function dateAddDays(expr: Expression, days: number): RawBuilder; export function dateAddDays(expr: Expression, days: number) { return sql`DATE_ADD(${expr}, INTERVAL ${days} DAY)`; } +export function dateSubDays(expr: Expression, days: number) { + return sql`DATE_SUB(${expr}, INTERVAL ${days} DAY)`; +} + /** * {@link left} - {@link right}일 수를 반환합니다. * * @see {@link https://dev.mysql.com/doc/refman/8.0/en/date-and-time-functions.html#function_datediff | DATEDIFF} */ -export function dateDiff( - left: Expression, - right: Expression, -): RawBuilder; +export function dateDiff(left: Expression, right: Expression): RawBuilder; export function dateDiff( left: Expression, right: Expression, ): RawBuilder; -export function dateDiff( - left: Expression, - right: Expression, -) { +export function dateDiff(left: Expression, right: Expression) { return sql`DATEDIFF(${left}, ${right})`; } diff --git a/backend/src/logger.ts b/backend/src/logger.ts index ebd9295c..84c9d0a9 100644 --- a/backend/src/logger.ts +++ b/backend/src/logger.ts @@ -1,19 +1,12 @@ import morgan from 'morgan'; import path from 'path'; -import { - addColors, - createLogger, - format, - transports, -} from 'winston'; +import { addColors, createLogger, format, transports } from 'winston'; import WinstonDaily from 'winston-daily-rotate-file'; import { logFormatOption, logLevelOption } from '~/config'; const { colors, levels } = logFormatOption; -const { - combine, timestamp, printf, colorize, errors, -} = format; +const { combine, timestamp, printf, colorize, errors } = format; addColors(colors); @@ -34,10 +27,7 @@ const logFormat = combine( const consoleOpts = { handleExceptions: true, level: logLevelOption.consoleLogLevel, - format: combine( - colorize({ all: true }), - timestamp({ format: logTimestampFormat }), - ), + format: combine(colorize({ all: true }), timestamp({ format: logTimestampFormat })), }; const logger = createLogger({ @@ -65,14 +55,11 @@ const logger = createLogger({ ], }); -const morganMiddleware = morgan( - ':method :url :status :res[content-length] - :response-time ms', - { - stream: { - // Use the http severity - write: (message: string) => logger.http(message), - }, +const morganMiddleware = morgan(':method :url :status :res[content-length] - :response-time ms', { + stream: { + // Use the http severity + write: (message: string) => logger.http(message), }, -); +}); export { logger, morganMiddleware }; diff --git a/backend/src/mysql.ts b/backend/src/mysql.ts index 4940f01f..e8a8de61 100644 --- a/backend/src/mysql.ts +++ b/backend/src/mysql.ts @@ -18,10 +18,7 @@ export const executeQuery = async (queryText: string, values: any[] = []): Promi logger.debug(`Executing query: ${queryText} (${values})`); let result; try { - const queryResult: [ - any, - FieldPacket[] - ] = await connection.query(queryText, values); + const queryResult: [any, FieldPacket[]] = await connection.query(queryText, values); [result] = queryResult; } catch (e) { if (e instanceof Error) { @@ -35,61 +32,62 @@ export const executeQuery = async (queryText: string, values: any[] = []): Promi return result; }; -export const makeExecuteQuery = (connection: mysql.PoolConnection) => async ( - queryText: string, - values: any[] = [], -): Promise => { - logger.debug(`Executing query: ${queryText} (${values})`); - let result; - try { - const queryResult: [ - any, - FieldPacket[] - ] = await connection.query(queryText, values); - [result] = queryResult; - } catch (e) { - if (e instanceof Error) { - logger.error(e); - throw new Error(DBError); +export const makeExecuteQuery = + (connection: mysql.PoolConnection) => + async (queryText: string, values: any[] = []): Promise => { + logger.debug(`Executing query: ${queryText} (${values})`); + let result; + try { + const queryResult: [any, FieldPacket[]] = await connection.query(queryText, values); + [result] = queryResult; + } catch (e) { + if (e instanceof Error) { + logger.error(e); + throw new Error(DBError); + } + throw e; } - throw e; - } - return result; -}; + return result; + }; type lending = { lendingId: number; -} +}; type UserRow = { userId: number; lending: lending[]; -} +}; export const queryTest = async () => { const connection = await pool.getConnection(); - const rows = await connection.query(` + const rows = (await connection.query(` SELECT id AS userId FROM user LIMIT 50; - `) as unknown as UserRow[][]; - const newRows = Promise.all(rows[0].map(async (row) => { - const lendings = await connection.query(` + `)) as unknown as UserRow[][]; + const newRows = Promise.all( + rows[0].map(async (row) => { + const lendings = (await connection.query( + ` SELECT id AS lendingId FROM lending WHERE userId = ? - `, [row.userId]) as unknown as lending[][]; - const newRow = row; - [newRow.lending] = lendings; - // eslint-disable-next-line no-console - console.log(lendings[0]); - return newRow; - })); + `, + [row.userId], + )) as unknown as lending[][]; + const newRow = row; + [newRow.lending] = lendings; + // eslint-disable-next-line no-console + console.log(lendings[0]); + return newRow; + }), + ); // eslint-disable-next-line no-console console.log(newRows); diff --git a/backend/src/v1/DTO/common.interface.ts b/backend/src/v1/DTO/common.interface.ts index 1f2ba1c3..3ccc8bc4 100644 --- a/backend/src/v1/DTO/common.interface.ts +++ b/backend/src/v1/DTO/common.interface.ts @@ -1,23 +1,23 @@ export type Meta = { - totalItems: number, - itemCount: number, - itemsPerPage: number, - totalPages: number, - currentPage: number -} + totalItems: number; + itemCount: number; + itemsPerPage: number; + totalPages: number; + currentPage: number; +}; export type searchQuery = { - nickname: string, - page?: string, - limit?: string, -} + nickname: string; + page?: string; + limit?: string; +}; export type createQuery = { - email: string, - password: string, -} + email: string; + password: string; +}; export type categoryWithBookCount = { - name: string, - bookCount: number, -} + name: string; + bookCount: number; +}; diff --git a/backend/src/v1/DTO/cursus.model.ts b/backend/src/v1/DTO/cursus.model.ts index 73eb8d21..5e828882 100644 --- a/backend/src/v1/DTO/cursus.model.ts +++ b/backend/src/v1/DTO/cursus.model.ts @@ -45,7 +45,7 @@ export type UserProjectFrom42 = { 'active?': boolean; }; teams: object[]; -} +}; export type UserProject = { id: UserProjectFrom42['id']; @@ -56,7 +56,7 @@ export type UserProject = { marked: UserProjectFrom42['marked']; marked_at: UserProjectFrom42['marked_at']; updated_at: UserProjectFrom42['updated_at']; -} +}; export type ProjectFrom42 = { id: number; @@ -73,9 +73,9 @@ export type ProjectFrom42 = { repository: string; cursus: Cursus[]; campus: Campus[]; - videos: [], + videos: []; project_sessions: object[]; -} +}; export type ProjectInfo = { id: number; @@ -87,7 +87,7 @@ export type ProjectInfo = { name: string; slug: string; }[]; -} +}; export type Campus = { id: number; @@ -113,7 +113,7 @@ export type Campus = { public: boolean; email_extension: string; default_hidden_phone: boolean; -} +}; export type Cursus = { id: number; @@ -121,13 +121,13 @@ export type Cursus = { name: string; slug: string; kind: string; -} +}; export type ProjectWithCircle = { [key: string]: { project_ids: number[]; - } -} + }; +}; export type BooksWithProjectInfo = { book_info_id: number; @@ -135,7 +135,7 @@ export type BooksWithProjectInfo = { id: number; circle: number; }[]; -} +}; export type RecommendedBook = { id: number; @@ -145,4 +145,4 @@ export type RecommendedBook = { image: string; publishedAt: string; subjects: string[]; -} +}; diff --git a/backend/src/v1/DTO/tags.model.ts b/backend/src/v1/DTO/tags.model.ts index 0f9c6e31..afcc3ab9 100644 --- a/backend/src/v1/DTO/tags.model.ts +++ b/backend/src/v1/DTO/tags.model.ts @@ -7,7 +7,7 @@ export type subDefaultTag = { content: string; superContent: string; visibility: 'public' | 'private'; -} +}; export type superDefaultTag = { id: number; @@ -15,4 +15,4 @@ export type superDefaultTag = { login: string; count: number; type: 'super' | 'default'; -} +}; diff --git a/backend/src/v1/DTO/users.model.ts b/backend/src/v1/DTO/users.model.ts index 7b145ca7..587e78fa 100644 --- a/backend/src/v1/DTO/users.model.ts +++ b/backend/src/v1/DTO/users.model.ts @@ -1,28 +1,28 @@ export type Lending = { - userId: number, - bookInfoId: number, - lendDate: Date, - lendingCondition: string, - image: string, - author: string, - title: string, - duedate: Date, - overDueDay: number, - reservedNum: number, -} + userId: number; + bookInfoId: number; + lendDate: Date; + lendingCondition: string; + image: string; + author: string; + title: string; + duedate: Date; + overDueDay: number; + reservedNum: number; +}; export type User = { - id: number, - email: string, - nickname: string, - intraId: number, - slack?: string, - penaltyEndDate?: Date, - overDueDay: number, - role: number, - reservations?: [], - lendings?: Lending[], -} + id: number; + email: string; + nickname: string; + intraId: number; + slack?: string; + penaltyEndDate?: Date; + overDueDay: number; + role: number; + reservations?: []; + lendings?: Lending[]; +}; export type PrivateUser = User & { - password: string, -} + password: string; +}; diff --git a/backend/src/v1/auth/auth.controller.ts b/backend/src/v1/auth/auth.controller.ts index 52aadbea..d8f340e4 100644 --- a/backend/src/v1/auth/auth.controller.ts +++ b/backend/src/v1/auth/auth.controller.ts @@ -39,13 +39,23 @@ export const getToken = async (req: Request, res: Response, next: NextFunction): } catch (error: any) { const errorNumber = parseInt(error.message ? error.message : error.errorCode, 10); if (errorNumber === 203) { - res.status(status.BAD_REQUEST).send(``); + res + .status(status.BAD_REQUEST) + .send( + ``, + ); return; } - res.status(status.SERVICE_UNAVAILABLE).send(``); + res + .status(status.SERVICE_UNAVAILABLE) + .send( + ``, + ); return; } - } else { await authJwt.saveJwt(req, res, user[0]); } + } else { + await authJwt.saveJwt(req, res, user[0]); + } res.status(302).redirect(`${oauthUrlOption.clientURL}/auth`); } catch (error: any) { const errorNumber = parseInt(error.message ? error.message : error.errorCode, 10); @@ -99,7 +109,9 @@ export const login = async (req: Request, res: Response, next: NextFunction): Pr throw new ErrorResponse(errorCode.NO_INPUT, 400); } /* 여기에 id, password의 유효성 검증 한번 더 할 수도 있음 */ - const user: { items: models.PrivateUser[] } = await usersService.searchUserWithPasswordByEmail(id); + const user: { items: models.PrivateUser[] } = await usersService.searchUserWithPasswordByEmail( + id, + ); if (user.items.length === 0) { return next(new ErrorResponse(errorCode.NO_ID, 401)); } @@ -146,40 +158,55 @@ export const intraAuthentication = async ( req: Request, res: Response, next: NextFunction, -) : Promise => { +): Promise => { try { const usersService = new UsersService(); const { intraProfile, id } = req.user as any; const { intraId, nickName } = intraProfile; const user: { items: models.User[] } = await usersService.searchUserById(id); if (user.items.length === 0) { - res.status(status.BAD_REQUEST) - .send(``); + res + .status(status.BAD_REQUEST) + .send( + ``, + ); return; // return next(new ErrorResponse(errorCode.NO_USER, 410)); } if (user.items[0].role !== role.user) { - res.status(status.BAD_REQUEST) - .send(``); + res + .status(status.BAD_REQUEST) + .send( + ``, + ); // return next(new ErrorResponse(errorCode.ALREADY_AUTHENTICATED, 401)); } const intraList: models.User[] = await usersService.searchUserByIntraId(intraId); if (intraList.length !== 0) { - res.status(status.BAD_REQUEST) - .send(``); + res + .status(status.BAD_REQUEST) + .send( + ``, + ); return; // return next(new ErrorResponse(errorCode.ALREADY_AUTHENTICATED, 401)); } const affectedRow = await authService.updateAuthenticationUser(id, intraId, nickName); if (affectedRow === 0) { - res.status(status.BAD_REQUEST) - .send(``); + res + .status(status.BAD_REQUEST) + .send( + ``, + ); // return next(new ErrorResponse(errorCode.NON_AFFECTED, 401)); } await updateSlackIdByUserId(user.items[0].id); await authJwt.saveJwt(req, res, user.items[0]); - res.status(status.OK) - .send(``); + res + .status(status.OK) + .send( + ``, + ); } catch (error: any) { const errorNumber = parseInt(error.message, 10); if (errorNumber >= 100 && errorNumber < 200) { diff --git a/backend/src/v1/auth/auth.jwt.ts b/backend/src/v1/auth/auth.jwt.ts index b9903ea3..fa48fac1 100644 --- a/backend/src/v1/auth/auth.jwt.ts +++ b/backend/src/v1/auth/auth.jwt.ts @@ -28,7 +28,7 @@ export const issueJwt = (user: User) => { * 설정값 설명 * expires: 밀리세컨드 값으로 설정해야하고, 1000 * 60 * 480 = 8시간으로 설정 */ -export const saveJwt = async (req: Request, res: Response, user: User) : Promise => { +export const saveJwt = async (req: Request, res: Response, user: User): Promise => { const token = issueJwt(user); res.cookie('access_token', token, { ...cookieOptions, diff --git a/backend/src/v1/auth/auth.service.ts b/backend/src/v1/auth/auth.service.ts index 273beb05..a0b1275e 100644 --- a/backend/src/v1/auth/auth.service.ts +++ b/backend/src/v1/auth/auth.service.ts @@ -10,12 +10,15 @@ export const updateAuthenticationUser = async ( id: number, intraId: number, nickname: string, -) : Promise => { - const result : ResultSetHeader = await executeQuery(` +): Promise => { + const result: ResultSetHeader = await executeQuery( + ` UPDATE user SET intraId = ?, nickname = ?, role = ? WHERE id = ? - `, [intraId, nickname, role.cadet, id]); + `, + [intraId, nickname, role.cadet, id], + ); return result.affectedRows; }; @@ -34,10 +37,16 @@ export const getAccessToken = async (): Promise => { 'Content-Type': 'application/json', }, data: queryString, - }).then((response) => { - accessToken = response.data.access_token; - }).catch((error) => { - throw new ErrorResponse(httpStatus[500], httpStatus.INTERNAL_SERVER_ERROR, '42 API로부터 토큰을 받아오는데 실패했습니다.'); - }); + }) + .then((response) => { + accessToken = response.data.access_token; + }) + .catch((error) => { + throw new ErrorResponse( + httpStatus[500], + httpStatus.INTERNAL_SERVER_ERROR, + '42 API로부터 토큰을 받아오는데 실패했습니다.', + ); + }); return accessToken; }; diff --git a/backend/src/v1/auth/auth.strategy.ts b/backend/src/v1/auth/auth.strategy.ts index cb7d81ae..3fc79a79 100644 --- a/backend/src/v1/auth/auth.strategy.ts +++ b/backend/src/v1/auth/auth.strategy.ts @@ -40,9 +40,7 @@ export const FtAuthentication = new FortyTwoStrategy( export const JwtStrategy = new JWTStrategy( { - jwtFromRequest: ExtractJwt.fromExtractors([ - (req: Request) => req?.cookies?.access_token, - ]), + jwtFromRequest: ExtractJwt.fromExtractors([(req: Request) => req?.cookies?.access_token]), secretOrKey: jwtOption.secret, ignoreExpiration: false, issuer: jwtOption.issuer, diff --git a/backend/src/v1/auth/auth.type.ts b/backend/src/v1/auth/auth.type.ts index cf4691b3..24f9cf57 100644 --- a/backend/src/v1/auth/auth.type.ts +++ b/backend/src/v1/auth/auth.type.ts @@ -2,10 +2,10 @@ /* eslint-disable no-shadow */ export const enum role { - user = 0, - cadet, - librarian, - staff, + user = 0, + cadet, + librarian, + staff, } export const roleSet = { diff --git a/backend/src/v1/auth/auth.validate.ts b/backend/src/v1/auth/auth.validate.ts index e70d59a5..4ecff381 100644 --- a/backend/src/v1/auth/auth.validate.ts +++ b/backend/src/v1/auth/auth.validate.ts @@ -11,62 +11,66 @@ import { role } from './auth.type'; const usersService = new UsersService(); -const authValidate = (roles: role[]) => async ( - req: Request, - res: Response, - next: Function, -) : Promise => { - try { - if (!req.cookies.access_token) { - if (roles.includes(role.user)) { - req.user = { - intraProfile: null, id: null, role: role.user, nickname: null, - }; - return next(); +const authValidate = + (roles: role[]) => + async (req: Request, res: Response, next: Function): Promise => { + try { + if (!req.cookies.access_token) { + if (roles.includes(role.user)) { + req.user = { + intraProfile: null, + id: null, + role: role.user, + nickname: null, + }; + return next(); + } + throw new ErrorResponse(errorCode.NO_TOKEN, 401); + } + // 토큰 복호화 + const verifyCheck = verify(req.cookies.access_token, jwtOption.secret); + const { id } = verifyCheck as any; + const user: { items: User[] } = await usersService.searchUserById(id); + // User가 없는 경우 + if (user.items.length === 0) { + throw new ErrorResponse(errorCode.NO_USER, 410); + } + // 권한이 있지 않은 경우 + if (!roles.includes(user.items[0].role)) { + throw new ErrorResponse(errorCode.NO_AUTHORIZATION, 403); + } + req.user = { + intraProfile: req.user, + id, + role: user.items[0].role, + nickname: user.items[0].nickname, + }; + next(); + } catch (error: any) { + switch (error.message) { + // 토큰에 대한 오류를 판단합니다. + case 'INVALID_TOKEN': + case 'TOKEN_IS_ARRAY': + case 'NO_USER': + return next(new ErrorResponse(errorCode.TOKEN_NOT_VALID, status.UNAUTHORIZED)); + case 'EXPIRED_TOKEN': + return next(new ErrorResponse(errorCode.EXPIRATION_TOKEN, status.GONE)); + default: + break; + } + if (error instanceof ErrorResponse) { + next(error); + } + const errorNumber = parseInt(error.message, 10); + if (errorNumber >= 100 && errorNumber < 200) { + next(new ErrorResponse(error.message, status.BAD_REQUEST)); + } else if (error.message === 'DB error') { + next(new ErrorResponse(errorCode.QUERY_EXECUTION_FAILED, status.INTERNAL_SERVER_ERROR)); + } else { + logger.error(error); + next(new ErrorResponse(errorCode.UNKNOWN_ERROR, status.INTERNAL_SERVER_ERROR)); } - throw new ErrorResponse(errorCode.NO_TOKEN, 401); - } - // 토큰 복호화 - const verifyCheck = verify(req.cookies.access_token, jwtOption.secret); - const { id } = verifyCheck as any; - const user: { items: User[] } = await usersService.searchUserById(id); - // User가 없는 경우 - if (user.items.length === 0) { - throw new ErrorResponse(errorCode.NO_USER, 410); - } - // 권한이 있지 않은 경우 - if (!roles.includes(user.items[0].role)) { - throw new ErrorResponse(errorCode.NO_AUTHORIZATION, 403); - } - req.user = { - intraProfile: req.user, id, role: user.items[0].role, nickname: user.items[0].nickname, - }; - next(); - } catch (error: any) { - switch (error.message) { - // 토큰에 대한 오류를 판단합니다. - case 'INVALID_TOKEN': - case 'TOKEN_IS_ARRAY': - case 'NO_USER': - return next(new ErrorResponse(errorCode.TOKEN_NOT_VALID, status.UNAUTHORIZED)); - case 'EXPIRED_TOKEN': - return next(new ErrorResponse(errorCode.EXPIRATION_TOKEN, status.GONE)); - default: - break; - } - if (error instanceof ErrorResponse) { - next(error); - } - const errorNumber = parseInt(error.message, 10); - if (errorNumber >= 100 && errorNumber < 200) { - next(new ErrorResponse(error.message, status.BAD_REQUEST)); - } else if (error.message === 'DB error') { - next(new ErrorResponse(errorCode.QUERY_EXECUTION_FAILED, status.INTERNAL_SERVER_ERROR)); - } else { - logger.error(error); - next(new ErrorResponse(errorCode.UNKNOWN_ERROR, status.INTERNAL_SERVER_ERROR)); } - } -}; + }; export default authValidate; diff --git a/backend/src/v1/auth/auth.validateDefaultNullUser.ts b/backend/src/v1/auth/auth.validateDefaultNullUser.ts index 17f60864..272234b0 100644 --- a/backend/src/v1/auth/auth.validateDefaultNullUser.ts +++ b/backend/src/v1/auth/auth.validateDefaultNullUser.ts @@ -11,56 +11,54 @@ import { role } from './auth.type'; const usersService = new UsersService(); -const authValidateDefaultNullUser = (roles: role[]) => async ( - req: Request, - res: Response, - next: Function, -) : Promise => { - if (!req.cookies.access_token) { - req.user = { intraProfile: null, id: null, role: null }; - next(); - } else { - try { - // 토큰 복호화 - const verifyCheck = verify(req.cookies.access_token, jwtOption.secret); - const { id } = verifyCheck as any; - const user: { items: User[] } = await usersService.searchUserById(id); - // User가 없는 경우 - if (user.items.length === 0) { - throw new ErrorResponse(errorCode.NO_USER, 410); - } - // 권한이 있지 않은 경우 - if (!roles.includes(user.items[0].role)) { - throw new ErrorResponse(errorCode.NO_AUTHORIZATION, 403); - } - req.user = { intraProfile: req.user, id, role: user.items[0].role }; +const authValidateDefaultNullUser = + (roles: role[]) => + async (req: Request, res: Response, next: Function): Promise => { + if (!req.cookies.access_token) { + req.user = { intraProfile: null, id: null, role: null }; next(); - } catch (error: any) { - switch (error.message) { - // 토큰에 대한 오류를 판단합니다. - case 'INVALID_TOKEN': - case 'TOKEN_IS_ARRAY': - case 'NO_USER': - return next(new ErrorResponse(errorCode.TOKEN_NOT_VALID, status.UNAUTHORIZED)); - case 'EXPIRED_TOKEN': - return next(new ErrorResponse(errorCode.EXPIRATION_TOKEN, status.GONE)); - default: - break; - } - if (error instanceof ErrorResponse) { - next(error); - } - const errorNumber = parseInt(error.message, 10); - if (errorNumber >= 100 && errorNumber < 200) { - next(new ErrorResponse(error.message, status.BAD_REQUEST)); - } else if (error.message === 'DB error') { - next(new ErrorResponse(errorCode.QUERY_EXECUTION_FAILED, status.INTERNAL_SERVER_ERROR)); - } else { - logger.error(error); - next(new ErrorResponse(errorCode.UNKNOWN_ERROR, status.INTERNAL_SERVER_ERROR)); + } else { + try { + // 토큰 복호화 + const verifyCheck = verify(req.cookies.access_token, jwtOption.secret); + const { id } = verifyCheck as any; + const user: { items: User[] } = await usersService.searchUserById(id); + // User가 없는 경우 + if (user.items.length === 0) { + throw new ErrorResponse(errorCode.NO_USER, 410); + } + // 권한이 있지 않은 경우 + if (!roles.includes(user.items[0].role)) { + throw new ErrorResponse(errorCode.NO_AUTHORIZATION, 403); + } + req.user = { intraProfile: req.user, id, role: user.items[0].role }; + next(); + } catch (error: any) { + switch (error.message) { + // 토큰에 대한 오류를 판단합니다. + case 'INVALID_TOKEN': + case 'TOKEN_IS_ARRAY': + case 'NO_USER': + return next(new ErrorResponse(errorCode.TOKEN_NOT_VALID, status.UNAUTHORIZED)); + case 'EXPIRED_TOKEN': + return next(new ErrorResponse(errorCode.EXPIRATION_TOKEN, status.GONE)); + default: + break; + } + if (error instanceof ErrorResponse) { + next(error); + } + const errorNumber = parseInt(error.message, 10); + if (errorNumber >= 100 && errorNumber < 200) { + next(new ErrorResponse(error.message, status.BAD_REQUEST)); + } else if (error.message === 'DB error') { + next(new ErrorResponse(errorCode.QUERY_EXECUTION_FAILED, status.INTERNAL_SERVER_ERROR)); + } else { + logger.error(error); + next(new ErrorResponse(errorCode.UNKNOWN_ERROR, status.INTERNAL_SERVER_ERROR)); + } } } - } -}; + }; export default authValidateDefaultNullUser; diff --git a/backend/src/v1/book-info-reviews/controller/bookInfoReviews.controller.ts b/backend/src/v1/book-info-reviews/controller/bookInfoReviews.controller.ts index af7255c5..3c40be90 100644 --- a/backend/src/v1/book-info-reviews/controller/bookInfoReviews.controller.ts +++ b/backend/src/v1/book-info-reviews/controller/bookInfoReviews.controller.ts @@ -1,19 +1,13 @@ -import { - NextFunction, Request, Response, -} from 'express'; +import { NextFunction, Request, Response } from 'express'; import * as status from 'http-status'; import * as parseCheck from './utils/parseCheck'; import * as bookInfoReviewsService from '../service/bookInfoReviews.service'; import * as errorCheck from './utils/errorCheck'; -export const getBookInfoReviewsPage = async ( - req: Request, - res: Response, - next: NextFunction, -) => { +export const getBookInfoReviewsPage = async (req: Request, res: Response, next: NextFunction) => { const bookInfoId = errorCheck.bookInfoParseCheck(req?.params?.bookInfoId); const reviewsId = parseCheck.reviewsIdParse(req?.query?.reviewsId); - const sort : 'asc' | 'desc' = parseCheck.sortParse(req?.query?.sort); + const sort: 'asc' | 'desc' = parseCheck.sortParse(req?.query?.sort); const limit = parseInt(String(req?.query?.limit), 10); return res .status(status.OK) diff --git a/backend/src/v1/book-info-reviews/controller/utils/errorCheck.ts b/backend/src/v1/book-info-reviews/controller/utils/errorCheck.ts index 45419aeb..44c7d49d 100644 --- a/backend/src/v1/book-info-reviews/controller/utils/errorCheck.ts +++ b/backend/src/v1/book-info-reviews/controller/utils/errorCheck.ts @@ -1,16 +1,14 @@ import * as errorCode from '~/v1/utils/error/errorCode'; import ErrorResponse from '~/v1/utils/error/errorResponse'; -export const bookInfoParseCheck = ( - bookInfoId : string, -) => { - let result : number; +export const bookInfoParseCheck = (bookInfoId: string) => { + let result: number; if (bookInfoId.trim() === '') { throw new ErrorResponse(errorCode.INVALID_INPUT, 400); } try { result = parseInt(bookInfoId, 10); - } catch (error : any) { + } catch (error: any) { throw new ErrorResponse(errorCode.INVALID_INPUT, 400); } return result; diff --git a/backend/src/v1/book-info-reviews/controller/utils/parseCheck.ts b/backend/src/v1/book-info-reviews/controller/utils/parseCheck.ts index c0452725..aee3b957 100644 --- a/backend/src/v1/book-info-reviews/controller/utils/parseCheck.ts +++ b/backend/src/v1/book-info-reviews/controller/utils/parseCheck.ts @@ -1,18 +1,14 @@ -export const reviewsIdParse = ( - reviewsId : any, -) => { - let result : number; +export const reviewsIdParse = (reviewsId: any) => { + let result: number; try { result = parseInt(reviewsId, 10); - } catch (error : any) { + } catch (error: any) { result = NaN; } return result; }; -export const sortParse = ( - sort : any, -) : 'asc' | 'desc' => { +export const sortParse = (sort: any): 'asc' | 'desc' => { if (sort === 'asc' || sort === 'desc') { return sort; } diff --git a/backend/src/v1/book-info-reviews/repository/bookInfoReviews.repository.ts b/backend/src/v1/book-info-reviews/repository/bookInfoReviews.repository.ts index 3d233509..97e42a1a 100644 --- a/backend/src/v1/book-info-reviews/repository/bookInfoReviews.repository.ts +++ b/backend/src/v1/book-info-reviews/repository/bookInfoReviews.repository.ts @@ -1,12 +1,19 @@ import { executeQuery } from '~/mysql'; -export const getBookinfoReviewsPageNoOffset = async (bookInfoId: number, reviewsId: number, sort: 'asc' | 'desc', limit: number) => { - const bookInfoIdQuery = (Number.isNaN(bookInfoId)) ? '' : `AND reviews.bookInfoId = ${bookInfoId}`; +export const getBookinfoReviewsPageNoOffset = async ( + bookInfoId: number, + reviewsId: number, + sort: 'asc' | 'desc', + limit: number, +) => { + const bookInfoIdQuery = Number.isNaN(bookInfoId) ? '' : `AND reviews.bookInfoId = ${bookInfoId}`; const sign = sort === 'asc' ? '>' : '<'; - const reviewIdQuery = (Number.isNaN(reviewsId)) ? '' : `AND reviews.id ${sign} ${reviewsId}`; + const reviewIdQuery = Number.isNaN(reviewsId) ? '' : `AND reviews.id ${sign} ${reviewsId}`; const sortQuery = `ORDER BY reviews.id ${sort}`; - if (bookInfoIdQuery === '') { return []; } - const limitQuery = (Number.isNaN(limit)) ? 'LIMIT 10' : `LIMIT ${limit}`; + if (bookInfoIdQuery === '') { + return []; + } + const limitQuery = Number.isNaN(limit) ? 'LIMIT 10' : `LIMIT ${limit}`; const reviews = await executeQuery(` SELECT @@ -27,13 +34,17 @@ export const getBookinfoReviewsPageNoOffset = async (bookInfoId: number, reviews ${sortQuery} ${limitQuery} `); - return (reviews); + return reviews; }; -export const getBookInfoReviewsCounts = async (bookInfoId: number, reviewsId: number, sort: 'asc' | 'desc') => { - const bookInfoIdQuery = (Number.isNaN(bookInfoId)) ? '' : `AND reviews.bookInfoId = ${bookInfoId}`; +export const getBookInfoReviewsCounts = async ( + bookInfoId: number, + reviewsId: number, + sort: 'asc' | 'desc', +) => { + const bookInfoIdQuery = Number.isNaN(bookInfoId) ? '' : `AND reviews.bookInfoId = ${bookInfoId}`; const sign = sort === 'asc' ? '>' : '<'; - const reviewIdQuery = (Number.isNaN(reviewsId)) ? '' : `AND reviews.id ${sign} ${reviewsId}`; + const reviewIdQuery = Number.isNaN(reviewsId) ? '' : `AND reviews.id ${sign} ${reviewsId}`; const counts = await executeQuery(` SELECT COUNT(*) as counts @@ -43,5 +54,5 @@ export const getBookInfoReviewsCounts = async (bookInfoId: number, reviewsId: nu ${bookInfoIdQuery} ${reviewIdQuery} `); - return (counts[0].counts); + return counts[0].counts; }; diff --git a/backend/src/v1/book-info-reviews/service/bookInfoReviews.service.ts b/backend/src/v1/book-info-reviews/service/bookInfoReviews.service.ts index f54eea16..3c62bcd8 100644 --- a/backend/src/v1/book-info-reviews/service/bookInfoReviews.service.ts +++ b/backend/src/v1/book-info-reviews/service/bookInfoReviews.service.ts @@ -1,11 +1,23 @@ import * as bookInfoReviewsRepository from '../repository/bookInfoReviews.repository'; -export const getPageNoOffset = async (bookInfoId: number, reviewsId: number, sort: 'asc' | 'desc', limit: number) => { - const items = await bookInfoReviewsRepository - .getBookinfoReviewsPageNoOffset(bookInfoId, reviewsId, sort, limit); - const counts = await bookInfoReviewsRepository - .getBookInfoReviewsCounts(bookInfoId, reviewsId, sort); - const itemsPerPage = (Number.isNaN(limit)) ? 10 : limit; +export const getPageNoOffset = async ( + bookInfoId: number, + reviewsId: number, + sort: 'asc' | 'desc', + limit: number, +) => { + const items = await bookInfoReviewsRepository.getBookinfoReviewsPageNoOffset( + bookInfoId, + reviewsId, + sort, + limit, + ); + const counts = await bookInfoReviewsRepository.getBookInfoReviewsCounts( + bookInfoId, + reviewsId, + sort, + ); + const itemsPerPage = Number.isNaN(limit) ? 10 : limit; const finalReviewsId = items[items.length - 1]?.reviewsId; const meta = { totalLeftItems: counts, diff --git a/backend/src/v1/books/Likes.repository.ts b/backend/src/v1/books/Likes.repository.ts index 0921ebdf..bd6a316f 100644 --- a/backend/src/v1/books/Likes.repository.ts +++ b/backend/src/v1/books/Likes.repository.ts @@ -8,7 +8,7 @@ class LikesRepository extends Repository { super(Likes, entityManager); } - async getLikesByBookInfoId(bookInfoId: number) : Promise { + async getLikesByBookInfoId(bookInfoId: number): Promise { const likes = this.find({ where: { bookInfoId, @@ -17,7 +17,7 @@ class LikesRepository extends Repository { return likes; } - async getLikesByUserId(userId: number) : Promise { + async getLikesByUserId(userId: number): Promise { const likes = await this.find({ where: { userId, diff --git a/backend/src/v1/books/books.controller.ts b/backend/src/v1/books/books.controller.ts index 765fcea0..fe4f80d2 100644 --- a/backend/src/v1/books/books.controller.ts +++ b/backend/src/v1/books/books.controller.ts @@ -1,7 +1,5 @@ /* eslint-disable import/no-unresolved */ -import { - NextFunction, Request, RequestHandler, Response, -} from 'express'; +import { NextFunction, Request, RequestHandler, Response } from 'express'; import * as status from 'http-status'; import { logger } from '~/logger'; import * as errorCode from '~/v1/utils/error/errorCode'; @@ -26,21 +24,15 @@ const pubdateFormatValidator = (pubdate: string | Date) => { return true; }; -const bookStatusFormatValidator = (bookStatus : number) => { +const bookStatusFormatValidator = (bookStatus: number) => { if (bookStatus < 0 || bookStatus > 3) { return false; } return true; }; -export const createBook = async ( - req: Request, - res: Response, - next: NextFunction, -) => { - const { - title, author, categoryId, pubdate, - } = req.body; +export const createBook = async (req: Request, res: Response, next: NextFunction) => { + const { title, author, categoryId, pubdate } = req.body; if (!(title && author && categoryId && pubdate)) { return next(new ErrorResponse(errorCode.INVALID_INPUT, status.BAD_REQUEST)); } @@ -48,9 +40,7 @@ export const createBook = async ( return next(new ErrorResponse(errorCode.INVALID_PUBDATE_FORNAT, status.BAD_REQUEST)); } try { - return res - .status(status.OK) - .send(await BooksService.createBook(req.body)); + return res.status(status.OK).send(await BooksService.createBook(req.body)); } catch (error: any) { const errorNumber = parseInt(error.message, 10); if (errorNumber >= 300 && errorNumber < 400) { @@ -64,19 +54,13 @@ export const createBook = async ( return 0; }; -export const createBookInfo = async ( - req: Request, - res: Response, - next: NextFunction, -) => { - const isbn = req.query.isbnQuery ? req.query.isbnQuery as string : ''; +export const createBookInfo = async (req: Request, res: Response, next: NextFunction) => { + const isbn = req.query.isbnQuery ? (req.query.isbnQuery as string) : ''; if (isbn === '') { return next(new ErrorResponse(errorCode.INVALID_INPUT, status.BAD_REQUEST)); } try { - return res - .status(status.OK) - .send(await BooksService.createBookInfo(isbn)); + return res.status(status.OK).send(await BooksService.createBookInfo(isbn)); } catch (error: any) { const errorNumber = parseInt(error.message, 10); if (errorNumber >= 300 && errorNumber < 400) { @@ -98,9 +82,7 @@ export const searchBookInfo = async ( // URI에 있는 파라미터/쿼리 변수에 저장 let query = req.query?.query ?? ''; query = query.trim(); - const { - page, limit, sort, category, - } = req.query; + const { page, limit, sort, category } = req.query; // 유효한 인자인지 파악 if (Number.isNaN(page) || Number.isNaN(limit)) { @@ -117,9 +99,7 @@ export const searchBookInfo = async ( category, ); logger.info(`[ES_S] : ${JSON.stringify(searchBookInfoResult.items)}`); - return res - .status(status.OK) - .json(searchBookInfoResult); + return res.status(status.OK).json(searchBookInfoResult); } catch (error: any) { const errorNumber = parseInt(error.message, 10); if (errorNumber >= 300 && errorNumber < 400) { @@ -146,9 +126,9 @@ export const searchBookInfoByTag = async ( const category = parseCheck.stringQueryParse(rawData.category); try { - return res.status(status.OK).json( - await BooksService.searchInfoByTag(query, page, limit, sort, category), - ); + return res + .status(status.OK) + .json(await BooksService.searchInfoByTag(query, page, limit, sort, category)); } catch (error: any) { return next(new ErrorResponse(errorCode.UNKNOWN_ERROR, status.INTERNAL_SERVER_ERROR)); } @@ -165,9 +145,7 @@ export const getBookById: RequestHandler = async ( } try { const bookInfo = await BooksService.getBookById(req.params.id); - return res - .status(status.OK) - .json(bookInfo); + return res.status(status.OK).json(bookInfo); } catch (error: any) { const errorNumber = parseInt(error.message, 10); if (errorNumber >= 300 && errorNumber < 400) { @@ -193,9 +171,7 @@ export const getInfoId: RequestHandler = async ( try { const bookInfo = await BooksService.getInfo(req.params.id); logger.info(`[ES_C] : ${JSON.stringify(bookInfo)}`); - return res - .status(status.OK) - .json(bookInfo); + return res.status(status.OK).json(bookInfo); } catch (error: any) { const errorNumber = parseInt(error.message, 10); if (errorNumber >= 300 && errorNumber < 400) { @@ -220,9 +196,7 @@ export const sortInfo = async ( return next(new ErrorResponse(errorCode.INVALID_INPUT, status.BAD_REQUEST)); } try { - return res - .status(status.OK) - .json(await BooksService.sortInfo(limit, sort)); + return res.status(status.OK).json(await BooksService.sortInfo(limit, sort)); } catch (error: any) { const errorNumber = parseInt(error.message, 10); if (errorNumber >= 300 && errorNumber < 400) { @@ -236,11 +210,7 @@ export const sortInfo = async ( return 0; }; -export const search = async ( - req: Request, - res: Response, - next: NextFunction, -) => { +export const search = async (req: Request, res: Response, next: NextFunction) => { const query = String(req.query.query) === 'undefined' ? ' ' : String(req.query.query); const page = parseInt(String(req.query.page), 10); const limit = parseInt(String(req.query.limit), 10); @@ -249,9 +219,7 @@ export const search = async ( return next(new ErrorResponse(errorCode.INVALID_INPUT, status.BAD_REQUEST)); } try { - return res - .status(status.OK) - .json(await BooksService.search(query, page, limit)); + return res.status(status.OK).json(await BooksService.search(query, page, limit)); } catch (error: any) { const errorNumber = parseInt(error.message, 10); if (errorNumber >= 300 && errorNumber < 400) { @@ -265,11 +233,7 @@ export const search = async ( return 0; }; -export const createLike = async ( - req: Request, - res: Response, - next: NextFunction, -) => { +export const createLike = async (req: Request, res: Response, next: NextFunction) => { // parameters const bookInfoId = parseInt(String(req?.params?.bookInfoId), 10); const { id } = req.user as any; @@ -295,17 +259,15 @@ export const createLike = async ( return 0; }; -export const deleteLike = async ( - req: Request, - res: Response, - next: NextFunction, -) => { +export const deleteLike = async (req: Request, res: Response, next: NextFunction) => { const { id } = req.user as any; const parameter = String(req?.params); const bookInfoId = parseInt(String(req?.params?.bookInfoId), 10); // parameter 검증 - if (parameter === 'undefined' || Number.isNaN(bookInfoId)) { return next(new ErrorResponse(errorCode.INVALID_INPUT, status.BAD_REQUEST)); } + if (parameter === 'undefined' || Number.isNaN(bookInfoId)) { + return next(new ErrorResponse(errorCode.INVALID_INPUT, status.BAD_REQUEST)); + } // 로직수행 및 에러처리 try { @@ -324,18 +286,16 @@ export const deleteLike = async ( return 0; }; -export const getLikeInfo = async ( - req: Request, - res: Response, - next: NextFunction, -) => { +export const getLikeInfo = async (req: Request, res: Response, next: NextFunction) => { // parameters const { id } = req.user as any; const parameter = String(req?.params); const bookInfoId = parseInt(String(req?.params?.bookInfoId), 10); // parameter 검증 - if (parameter === 'undefined' || Number.isNaN(bookInfoId)) { return next(new ErrorResponse(errorCode.INVALID_INPUT, status.BAD_REQUEST)); } + if (parameter === 'undefined' || Number.isNaN(bookInfoId)) { + return next(new ErrorResponse(errorCode.INVALID_INPUT, status.BAD_REQUEST)); + } // 로직수행 및 에러처리 try { @@ -353,11 +313,7 @@ export const getLikeInfo = async ( return 0; }; -export const updateBookInfo = async ( - req: Request, - res: Response, - next: NextFunction, -) => { +export const updateBookInfo = async (req: Request, res: Response, next: NextFunction) => { const bookInfo: types.UpdateBookInfo = { id: req.body.bookInfoId, title: req.body.title, @@ -376,24 +332,51 @@ export const updateBookInfo = async ( if (book.id <= 0 || Number.isNaN(book.id) || bookInfo.id <= 0 || Number.isNaN(bookInfo.id)) { return next(new ErrorResponse(errorCode.INVALID_INPUT, status.BAD_REQUEST)); } - if (!(bookInfo.title || bookInfo.author || bookInfo.publisher || bookInfo.image - || bookInfo.categoryId || bookInfo.publishedAt || book.callSign || book.status !== undefined)) { return next(new ErrorResponse(errorCode.NO_BOOK_INFO_DATA, status.BAD_REQUEST)); } + if ( + !( + bookInfo.title || + bookInfo.author || + bookInfo.publisher || + bookInfo.image || + bookInfo.categoryId || + bookInfo.publishedAt || + book.callSign || + book.status !== undefined + ) + ) { + return next(new ErrorResponse(errorCode.NO_BOOK_INFO_DATA, status.BAD_REQUEST)); + } - if (!isNullish(bookInfo.title)) { bookInfo.title.trim(); } - if (!isNullish(bookInfo.author)) { bookInfo.author.trim(); } - if (!isNullish(bookInfo.publisher)) { bookInfo.publisher.trim(); } - if (!isNullish(bookInfo.image)) { bookInfo.image.trim(); } + if (!isNullish(bookInfo.title)) { + bookInfo.title.trim(); + } + if (!isNullish(bookInfo.author)) { + bookInfo.author.trim(); + } + if (!isNullish(bookInfo.publisher)) { + bookInfo.publisher.trim(); + } + if (!isNullish(bookInfo.image)) { + bookInfo.image.trim(); + } if (!isNullish(bookInfo.publishedAt) && pubdateFormatValidator(bookInfo.publishedAt)) { String(bookInfo.publishedAt).trim(); - } else if (!isNullish(bookInfo.publishedAt) && pubdateFormatValidator(bookInfo.publishedAt) === false) { + } else if ( + !isNullish(bookInfo.publishedAt) && + pubdateFormatValidator(bookInfo.publishedAt) === false + ) { return next(new ErrorResponse(errorCode.INVALID_PUBDATE_FORNAT, status.BAD_REQUEST)); } - if (isNullish(book.callSign) === false) { book.callSign.trim(); } + if (isNullish(book.callSign) === false) { + book.callSign.trim(); + } if (bookStatusFormatValidator(book.status) === false) { return next(new ErrorResponse(errorCode.INVALID_INPUT, status.BAD_REQUEST)); } try { - if (book.id) { await BooksService.updateBook(book, bookInfo); } + if (book.id) { + await BooksService.updateBook(book, bookInfo); + } return res.status(status.NO_CONTENT).send(); } catch (error: any) { const errorNumber = parseInt(error.message, 10); @@ -408,30 +391,28 @@ export const updateBookInfo = async ( return 0; }; -export const updateBookDonator = async ( - req: Request, - res: Response, - next: NextFunction, -) => { +export const updateBookDonator = async (req: Request, res: Response, next: NextFunction) => { const parsed = searchSchema.safeParse(req.body); if (!parsed.success) { return next(new ErrorResponse(errorCode.INVALID_INPUT, status.BAD_REQUEST)); } - const { - nicknameOrEmail, page, limit, - } = parsed.data; + const { nicknameOrEmail, page, limit } = parsed.data; let items; let user; try { if (nicknameOrEmail) { - items = JSON.parse(JSON.stringify( - await usersService.searchUserBynicknameOrEmail(nicknameOrEmail, limit, page), - )); + items = JSON.parse( + JSON.stringify( + await usersService.searchUserBynicknameOrEmail(nicknameOrEmail, limit, page), + ), + ); } if (items) { - items.items = await Promise.all(items.items.map(async (data: User) => ({ - ...data, - }))); + items.items = await Promise.all( + items.items.map(async (data: User) => ({ + ...data, + })), + ); } if (items.items[0]) { user = items.items[0]; @@ -447,7 +428,9 @@ export const updateBookDonator = async ( if (bookDonator.id <= 0 || Number.isNaN(bookDonator.id)) { return next(new ErrorResponse(errorCode.INVALID_INPUT, status.BAD_REQUEST)); } - if (bookDonator.id) { await BooksService.updateBookDonator(bookDonator); } + if (bookDonator.id) { + await BooksService.updateBookDonator(bookDonator); + } return res.status(status.NO_CONTENT).send(); } catch (error: any) { diff --git a/backend/src/v1/books/books.model.ts b/backend/src/v1/books/books.model.ts index bcf01d6f..8b8c8e69 100644 --- a/backend/src/v1/books/books.model.ts +++ b/backend/src/v1/books/books.model.ts @@ -11,38 +11,38 @@ export type BookInfo = RowDataPacket & { publishedAt?: string | Date; createdAt: Date; updatedAt: Date; -} +}; export type BookEach = RowDataPacket & { - id?: number; - donator: string; - donatorId?: number; - callSign: string; - status: number; - createdAt: Date; - updatedAt: Date; - infoId: number; -} + id?: number; + donator: string; + donatorId?: number; + callSign: string; + status: number; + createdAt: Date; + updatedAt: Date; + infoId: number; +}; export type Book = { - title: string; - author: string; - publisher: string; - isbn: string; - image?: string; - category: string; - publishedAt?: Date; - donator?: string; - callSign: string; - status: number; -} + title: string; + author: string; + publisher: string; + isbn: string; + image?: string; + category: string; + publishedAt?: Date; + donator?: string; + callSign: string; + status: number; +}; export type categoryCount = RowDataPacket & { - name: string; - count: number; -} + name: string; + count: number; +}; export type lending = RowDataPacket & { - lendingCreatedAt: Date; - returningCreatedAt: Date; -} + lendingCreatedAt: Date; + returningCreatedAt: Date; +}; diff --git a/backend/src/v1/books/books.repository.ts b/backend/src/v1/books/books.repository.ts index fdd3933d..a16a2d25 100644 --- a/backend/src/v1/books/books.repository.ts +++ b/backend/src/v1/books/books.repository.ts @@ -4,12 +4,14 @@ import * as errorCode from '~/v1/utils/error/errorCode'; import ErrorResponse from '~/v1/utils/error/errorResponse'; import jipDataSource from '~/app-data-source'; import { VSearchBookByTag } from '~/entity/entities/VSearchBookByTag'; -import { - Book, BookInfo, User, Lending, Category, VSearchBook, -} from '~/entity/entities'; +import { Book, BookInfo, User, Lending, Category, VSearchBook } from '~/entity/entities'; import { number } from 'zod'; import { - CreateBookInfo, LendingBookList, UpdateBook, UpdateBookInfo, UpdateBookDonator, + CreateBookInfo, + LendingBookList, + UpdateBook, + UpdateBookInfo, + UpdateBookDonator, } from './books.type'; class BooksRepository extends Repository { @@ -47,11 +49,7 @@ class BooksRepository extends Repository { return this.users.count({ where: { nickname } }); } - async getBookList( - condition: string, - limit: number, - page: number, - ): Promise { + async getBookList(condition: string, limit: number, page: number): Promise { const searchBook = await this.searchBook.find({ where: [ { title: Like(`%${condition}%`) }, @@ -116,14 +114,10 @@ class BooksRepository extends Repository { } // TODO: refactact sort type - async getLendingBookList( - sort: string, - limit: number, - ): Promise { + async getLendingBookList(sort: string, limit: number): Promise { const order = sort === 'popular' ? 'lendingCnt' : 'createdAt'; - const lendingCondition: string = sort === 'popular' - ? 'and lending.createdAt >= date_sub(now(), interval 42 day)' - : ''; + const lendingCondition: string = + sort === 'popular' ? 'and lending.createdAt >= date_sub(now(), interval 42 day)' : ''; const lendingBookList = this.bookInfo .createQueryBuilder('book_info') @@ -139,11 +133,7 @@ class BooksRepository extends Repository { .addSelect('book_info.updatedAt', 'updatedAt') .addSelect('COUNT(lending.id)', 'lendingCnt') .leftJoin(Book, 'book', 'book.infoId = book_info.id') - .leftJoin( - Lending, - 'lending', - `lending.bookId = book.id ${lendingCondition}`, - ) + .leftJoin(Lending, 'lending', `lending.bookId = book.id ${lendingCondition}`) .leftJoin(Category, 'category', 'category.id = book_info.categoryId') .limit(limit) .groupBy('book_info.id') @@ -154,22 +144,14 @@ class BooksRepository extends Repository { } async getNewCallsignPrimaryNum(categoryId: string | undefined): Promise { - return ( - (await this.bookInfo.countBy({ categoryId: Number(categoryId) })) + 1 - ); + return (await this.bookInfo.countBy({ categoryId: Number(categoryId) })) + 1; } async getOldCallsignNums(categoryAlphabet: string) { return this.books .createQueryBuilder() - .select( - "substring(SUBSTRING_INDEX(callSign, '.', 1),2)", - 'recommendPrimaryNum', - ) - .addSelect( - "substring(SUBSTRING_INDEX(callSign, '.', -1),2)", - 'recommendCopyNum', - ) + .select("substring(SUBSTRING_INDEX(callSign, '.', 1),2)", 'recommendPrimaryNum') + .addSelect("substring(SUBSTRING_INDEX(callSign, '.', -1),2)", 'recommendCopyNum') .where('callsign like :categoryAlphabet', { categoryAlphabet: `${categoryAlphabet}%`, }) @@ -191,9 +173,7 @@ class BooksRepository extends Repository { await this.books.update(bookDonator.id, bookDonator as Book); } - async createBookInfo( - target: CreateBookInfo, - ): Promise { + async createBookInfo(target: CreateBookInfo): Promise { const bookInfo: BookInfo = { title: target.title, author: target.author, @@ -206,9 +186,7 @@ class BooksRepository extends Repository { return this.bookInfo.save(bookInfo); } - async createBook( - target: CreateBookInfo, - ): Promise { + async createBook(target: CreateBookInfo): Promise { const book: Book = { donator: target.donator, donatorId: target.donatorId, @@ -220,7 +198,8 @@ class BooksRepository extends Repository { } async findBooksByIds(idList: number[]) { - const bookList = await this.bookInfo.createQueryBuilder('bi') + const bookList = await this.bookInfo + .createQueryBuilder('bi') .select('bi.id', 'id') .addSelect('bi.title', 'title') .addSelect('bi.author', 'author') diff --git a/backend/src/v1/books/books.service.spec.ts b/backend/src/v1/books/books.service.spec.ts index 4317cc80..df96baf6 100644 --- a/backend/src/v1/books/books.service.spec.ts +++ b/backend/src/v1/books/books.service.spec.ts @@ -4,7 +4,10 @@ import { CreateBookInfo } from './books.type'; describe('BooksService', () => { beforeAll(async () => { - await jipDataSource.initialize().then(() => console.log('good!')).catch((err) => console.log(err)); + await jipDataSource + .initialize() + .then(() => console.log('good!')) + .catch((err) => console.log(err)); }); afterAll(() => { jipDataSource.destroy(); diff --git a/backend/src/v1/books/books.service.ts b/backend/src/v1/books/books.service.ts index f330b844..1438b150 100644 --- a/backend/src/v1/books/books.service.ts +++ b/backend/src/v1/books/books.service.ts @@ -16,8 +16,12 @@ import { import * as models from './books.model'; import BooksRepository from './books.repository'; import { - CreateBookInfo, LendingBookList, UpdateBook, UpdateBookInfo, - categoryIds, UpdateBookDonator, + CreateBookInfo, + LendingBookList, + UpdateBook, + UpdateBookInfo, + categoryIds, + UpdateBookDonator, } from './books.type'; import { categoryWithBookCount } from '../DTO/common.interface'; import * as searchKeywordsService from '../search-keywords/searchKeywords.service'; @@ -27,21 +31,33 @@ const getInfoInNationalLibrary = async (isbn: string) => { let book; let searchResult; await axios - .get(`https://www.nl.go.kr/seoji/SearchApi.do?cert_key=${nationalIsbnApiKey}&result_style=json&page_no=1&page_size=10&isbn=${isbn}`) + .get( + `https://www.nl.go.kr/seoji/SearchApi.do?cert_key=${nationalIsbnApiKey}&result_style=json&page_no=1&page_size=10&isbn=${isbn}`, + ) .then((res) => { searchResult = res.data.docs[0]; const { - TITLE: title, SUBJECT: category, PUBLISHER: publisher, PUBLISH_PREDATE: pubdate, + TITLE: title, + SUBJECT: category, + PUBLISHER: publisher, + PUBLISH_PREDATE: pubdate, } = searchResult; - const image = `https://image.kyobobook.co.kr/images/book/xlarge/${isbn.slice(-3)}/x${isbn}.jpg`; + const image = `https://image.kyobobook.co.kr/images/book/xlarge/${isbn.slice( + -3, + )}/x${isbn}.jpg`; book = { - title, image, category, isbn, publisher, pubdate, + title, + image, + category, + isbn, + publisher, + pubdate, }; }) .catch(() => { throw new Error(errorCode.ISBN_SEARCH_FAILED); }); - return (book); + return book; }; const getAuthorInNaver = async (isbn: string) => { @@ -64,10 +80,10 @@ const getAuthorInNaver = async (isbn: string) => { .catch(() => { throw new Error(errorCode.ISBN_SEARCH_FAILED_IN_NAVER); }); - return (author); + return author; }; -const getCategoryAlphabet = (categoryId : number): string => { +const getCategoryAlphabet = (categoryId: number): string => { try { const category = Object.values(categoryIds) as string[]; return category[categoryId - 1]; @@ -76,11 +92,7 @@ const getCategoryAlphabet = (categoryId : number): string => { } }; -export const search = async ( - query: string, - page: number, - limit: number, -) => { +export const search = async (query: string, page: number, limit: number) => { const booksRepository = new BooksRepository(); const bookList = await booksRepository.getBookList(query, limit, page); const totalItems = await booksRepository.getTotalItems(query); @@ -110,7 +122,9 @@ export const createBook = async (book: CreateBookInfo) => { let recommendPrimaryNum; if (checkNickName > 1) { - logger.warn(`${errorCode.SLACKID_OVERLAP}: nickname이 중복입니다. 최근에 가입한 user의 ID로 기부가 기록됩니다.`); + logger.warn( + `${errorCode.SLACKID_OVERLAP}: nickname이 중복입니다. 최근에 가입한 user의 ID로 기부가 기록됩니다.`, + ); } if (isbnInBookInfo === 0) { @@ -128,10 +142,12 @@ export const createBook = async (book: CreateBookInfo) => { recommendPrimaryNum = nums.recommendPrimaryNum; recommendCopyNum = nums.recommendCopyNum * 1 + 1; } - const recommendCallSign = `${categoryAlphabet}${recommendPrimaryNum}.${String(book.pubdate).slice(2, 4)}.v1.c${recommendCopyNum}`; + const recommendCallSign = `${categoryAlphabet}${recommendPrimaryNum}.${String( + book.pubdate, + ).slice(2, 4)}.v1.c${recommendCopyNum}`; await booksRepository.createBook({ ...book, callSign: recommendCallSign }); await transactionQueryRunner.commitTransaction(); - return ({ callsign: recommendCallSign }); + return { callsign: recommendCallSign }; } catch (error) { await transactionQueryRunner.rollbackTransaction(); if (error instanceof Error) { @@ -140,7 +156,7 @@ export const createBook = async (book: CreateBookInfo) => { } finally { await transactionQueryRunner.release(); } - return (new Error(errorCode.FAIL_CREATE_BOOK_BY_UNEXPECTED)); + return new Error(errorCode.FAIL_CREATE_BOOK_BY_UNEXPECTED); }; export const createBookInfo = async (isbn: string) => { @@ -149,10 +165,7 @@ export const createBookInfo = async (isbn: string) => { return { bookInfo }; }; -export const sortInfo = async ( - limit: number, - sort: string, -) => { +export const sortInfo = async (limit: number, sort: string) => { const booksRepository = new BooksRepository(); const bookList: LendingBookList[] = await booksRepository.getLendingBookList(sort, limit); return { items: bookList }; @@ -336,10 +349,7 @@ export const searchInfoByTag = async ( default: sortQuery = { createdAt: 'DESC' }; } - const whereQuery: Array = [ - { superTagContent: query }, - { subTagContent: query }, - ]; + const whereQuery: Array = [{ superTagContent: query }, { subTagContent: query }]; if (category) { whereQuery.push({ category }); } @@ -466,7 +476,10 @@ export const getInfo = async (id: string) => { } const { ...rest } = eachBook; return { - ...rest, dueDate, isLendable, isReserved, + ...rest, + dueDate, + isLendable, + isReserved, }; }), ); @@ -490,9 +503,9 @@ export const updateBook = async (book: UpdateBook, bookInfo: UpdateBookInfo) => await booksRepository.updateBook(book); if (bookInfo.id) { await booksRepository.updateBookInfo(bookInfo); - const keyword = await bookInfoSearchKeywordRepository.getBookInfoSearchKeyword( - { bookInfoId: bookInfo.id }, - ); + const keyword = await bookInfoSearchKeywordRepository.getBookInfoSearchKeyword({ + bookInfoId: bookInfo.id, + }); if (keyword?.id) { await bookInfoSearchKeywordRepository.updateBookInfoSearchKeyword(keyword.id, bookInfo); } diff --git a/backend/src/v1/books/books.type.ts b/backend/src/v1/books/books.type.ts index baa40af4..e746a807 100644 --- a/backend/src/v1/books/books.type.ts +++ b/backend/src/v1/books/books.type.ts @@ -1,41 +1,41 @@ export type SearchBookInfoQuery = { - query: string; - sort: string; - page: string; - limit: string; - category: string; -} + query: string; + sort: string; + page: string; + limit: string; + category: string; +}; export type SortInfoType = { - sort: string; - limit: string; -} + sort: string; + limit: string; +}; export type LendingBookList = { - id: number; - title: string; - author: string; - publisher: string; - isbn: string; - image: string; - publishedAt: Date | string; - updatedAt: Date | string; - lendingCnt: number; -} + id: number; + title: string; + author: string; + publisher: string; + isbn: string; + image: string; + publishedAt: Date | string; + updatedAt: Date | string; + lendingCnt: number; +}; export type CreateBookInfo = { - infoId: number; - callSign: string; - title: string; - author: string; - publisher: string; - isbn?: string; - image?: string; - categoryId?: string; - pubdate?: string | null; - donator: string; - donatorId: number | null; -} + infoId: number; + callSign: string; + title: string; + author: string; + publisher: string; + isbn?: string; + image?: string; + categoryId?: string; + pubdate?: string | null; + donator: string; + donatorId: number | null; +}; export type UpdateBookInfo = { id: number; @@ -45,47 +45,47 @@ export type UpdateBookInfo = { publishedAt: string | Date; image: string; categoryId?: string; -} +}; export type UpdateBook = { id: number; callSign: string; status: number; -} +}; export type UpdateBookDonator = { - id: number; - donator: string; - donatorId: number; -} + id: number; + donator: string; + donatorId: number; +}; -export enum categoryIds{ - 'K' = 1, - 'C', - 'O', - 'A', - 'I', - 'G', - 'J', - 'c', - 'F', - 'E', - 'h', - 'H', - 'd', - 'D', - 'k', - 'g', - 'B', - 'e', - 'n', - 'N', - 'j', - 'a', - 'f', - 'L', - 'b', - 'M', - 'i', - 'l', +export enum categoryIds { + 'K' = 1, + 'C', + 'O', + 'A', + 'I', + 'G', + 'J', + 'c', + 'F', + 'E', + 'h', + 'H', + 'd', + 'D', + 'k', + 'g', + 'B', + 'e', + 'n', + 'N', + 'j', + 'a', + 'f', + 'L', + 'b', + 'M', + 'i', + 'l', } diff --git a/backend/src/v1/books/likes.service.ts b/backend/src/v1/books/likes.service.ts index 6abf729e..8ed5d79c 100644 --- a/backend/src/v1/books/likes.service.ts +++ b/backend/src/v1/books/likes.service.ts @@ -3,7 +3,7 @@ import jipDataSource from '~/app-data-source'; import LikesRepository from './Likes.repository'; export default class LikesService { - private readonly likesRepository : LikesRepository; + private readonly likesRepository: LikesRepository; constructor() { this.likesRepository = new LikesRepository(); @@ -22,7 +22,9 @@ export default class LikesService { const LikeArray = await this.likesRepository.find({ where: { userId, bookInfoId, isDeleted: false }, }); - if (LikeArray.length === 0) { throw new Error(errorCode.NONEXISTENT_LIKES); } + if (LikeArray.length === 0) { + throw new Error(errorCode.NONEXISTENT_LIKES); + } } async createLike(userId: number, bookInfoId: number) { @@ -33,9 +35,12 @@ export default class LikesService { await likesRepo.manager.queryRunner?.connect(); await likesRepo.manager.queryRunner?.startTransaction(); try { - const ret = await likesRepo.update({ userId, bookInfoId }, { - isDeleted: false, - }); + const ret = await likesRepo.update( + { userId, bookInfoId }, + { + isDeleted: false, + }, + ); if (ret.affected === 0) { const like = likesRepo.create({ userId, bookInfoId }); await likesRepo.save(like); @@ -54,18 +59,25 @@ export default class LikesService { async deleteLike(userId: number, bookInfoId: number) { // update를 할때 이미 해당 데이터가 존재하는지 검사하지 말라는 이유는?? // UpdateResult { generatedMaps: [], raw: [], affected: 0 } - const { affected } = await this.likesRepository.update({ userId, bookInfoId }, { - isDeleted: true, - }); - if (affected === 0) { throw new Error(errorCode.NONEXISTENT_LIKES); } + const { affected } = await this.likesRepository.update( + { userId, bookInfoId }, + { + isDeleted: true, + }, + ); + if (affected === 0) { + throw new Error(errorCode.NONEXISTENT_LIKES); + } } async getLikeInfo(userId: number, bookInfoId: number) { const LikeArray = await this.likesRepository.find({ where: { bookInfoId, isDeleted: false } }); let isLiked = false; LikeArray.forEach((like: any) => { - if (like.userId === userId && like.isDeleted === 0) { isLiked = true; } + if (like.userId === userId && like.isDeleted === 0) { + isLiked = true; + } }); - return ({ bookInfoId, isLiked, likeNum: LikeArray.length }); + return { bookInfoId, isLiked, likeNum: LikeArray.length }; } } diff --git a/backend/src/v1/cursus/cursus.controller.ts b/backend/src/v1/cursus/cursus.controller.ts index f154c37b..0c70b580 100644 --- a/backend/src/v1/cursus/cursus.controller.ts +++ b/backend/src/v1/cursus/cursus.controller.ts @@ -1,16 +1,11 @@ -import { - NextFunction, Request, Response, -} from 'express'; +import { NextFunction, Request, Response } from 'express'; import * as status from 'http-status'; import { getAccessToken } from '~/v1/auth/auth.service'; import { RecommendedBook, UserProject, ProjectInfo } from '~/v1/DTO/cursus.model'; import { logger } from '~/logger'; import * as CursusService from './cursus.service'; -export const recommendBook = async ( - req: Request, - res: Response, -) => { +export const recommendBook = async (req: Request, res: Response) => { const { nickname: login } = req.user as any; const limit = req.query.limit ? Number(req.query.limit) : 4; const project = req.query.project as string; @@ -46,21 +41,19 @@ export const recommendBook = async ( return res.status(status.OK).json({ items: bookList, meta }); }; -export const getProjects = async ( - req: Request, - res: Response, - next: NextFunction, -) => { +export const getProjects = async (req: Request, res: Response, next: NextFunction) => { const page = req.query.page as string; const mode = req.query.mode as string; - const accessToken:string = await getAccessToken(); + const accessToken: string = await getAccessToken(); let projects: ProjectInfo[] = []; try { projects = await CursusService.getProjectsInfo(accessToken, page); } catch (error) { return next(error); } - if (projects.length !== 0) { CursusService.saveProjects(projects, mode); } + if (projects.length !== 0) { + CursusService.saveProjects(projects, mode); + } return res.status(200).send({ projects }); }; diff --git a/backend/src/v1/cursus/cursus.service.ts b/backend/src/v1/cursus/cursus.service.ts index e2848713..69ecf51e 100644 --- a/backend/src/v1/cursus/cursus.service.ts +++ b/backend/src/v1/cursus/cursus.service.ts @@ -36,9 +36,7 @@ export const readFiles = async () => { * @param login 사용자의 닉네임 * @returns 사용자의 intra id */ -export const getIntraId = async ( - login: string, -): Promise => { +export const getIntraId = async (login: string): Promise => { const usersRepo = new UsersRepository(); const user = (await usersRepo.searchUserBy({ nickname: login }, 1, 0))[0]; return user[0].intraId.toString(); @@ -61,28 +59,36 @@ export const getUserProjectFrom42API = async ( headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${accessToken}`, + AccessControlAllowOrigin: 'https://42library.kr', + AccessControllAllowCredentials: 'true', }, - }).then((response) => { - const rawData: UserProjectFrom42[] = response.data; - rawData.forEach((data: UserProjectFrom42) => { - userProject.push({ - id: data.id, - status: data.status, - validated: data['validated?'], - project: data.project, - cursus_ids: data.cursus_ids, - marked: data.marked, - marked_at: data.marked_at, - updated_at: data.updated_at, + }) + .then((response) => { + const rawData: UserProjectFrom42[] = response.data; + rawData.forEach((data: UserProjectFrom42) => { + userProject.push({ + id: data.id, + status: data.status, + validated: data['validated?'], + project: data.project, + cursus_ids: data.cursus_ids, + marked: data.marked, + marked_at: data.marked_at, + updated_at: data.updated_at, + }); }); + }) + .catch((error) => { + if (error.response.status === 401) { + throw new ErrorResponse('401', 401, '권한이 없습니다.'); + } else { + throw new ErrorResponse( + '500', + 500, + '42 API로부터 프로젝트 정보를 받아오는데 실패했습니다.', + ); + } }); - }).catch((error) => { - if (error.response.status === 401) { - throw new ErrorResponse('401', 401, '권한이 없습니다.'); - } else { - throw new ErrorResponse('500', 500, '42 API로부터 프로젝트 정보를 받아오는데 실패했습니다.'); - } - }); return userProject; }; @@ -93,10 +99,7 @@ export const getUserProjectFrom42API = async ( * @param projectId 프로젝트 id * @returns projectId가 포함된 서클 번호 문자열 */ -const findCircle = ( - cursus: ProjectWithCircle, - projectId: number, -) => { +const findCircle = (cursus: ProjectWithCircle, projectId: number) => { let circle: string | null = null; Object.keys(cursus).forEach((key) => { const projectIds = cursus[key].project_ids; @@ -116,10 +119,7 @@ const findCircle = ( * @param projectList 사용자가 진행한 프로젝트 목록 * @returns 아우터 서클에 있는 프로젝트 id 배열 */ -const getOuterProjectIds = ( - cursus: ProjectWithCircle, - projectList: UserProject[] | null, -) => { +const getOuterProjectIds = (cursus: ProjectWithCircle, projectList: UserProject[] | null) => { let outerProjectIds: number[] = []; for (let i = 0; i < projectsInfo.length; i += 1) { const projectId = projectsInfo[i].id; @@ -141,10 +141,7 @@ const getOuterProjectIds = ( * @param circle 서클 번호 * @returns 추천할 프로젝트 id 배열 */ -const getNextProjectIds = ( - cursus: ProjectWithCircle, - circle: string, -) => { +const getNextProjectIds = (cursus: ProjectWithCircle, circle: string) => { const projectIds = cursus[circle].project_ids; let innerProjectIds = projectIds.filter((id) => id !== 0); if (innerProjectIds.length === 0) { @@ -162,14 +159,11 @@ const getNextProjectIds = ( * @param userProject 사용자의 프로젝트 정보 * @returns 사용자에게 추천할 프로젝트 */ -export const getRecommendedProject = async ( - userProject: UserProject[], -) => { - const projectList = userProject.sort((prev, post) => - new Date(post.updated_at).getTime() - new Date(prev.updated_at).getTime()) +export const getRecommendedProject = async (userProject: UserProject[]) => { + const projectList = userProject + .sort((prev, post) => new Date(post.updated_at).getTime() - new Date(prev.updated_at).getTime()) .filter((item: UserProject) => !item.project.name.includes('Exam Rank')); - const recommendedProject = projectList.filter((project) => - project.status === 'in_progress'); + const recommendedProject = projectList.filter((project) => project.status === 'in_progress'); if (recommendedProject.length > 0) { return recommendedProject.map((project) => project.project.id); } @@ -177,9 +171,11 @@ export const getRecommendedProject = async ( const userProjectId = userProject[0].project.id; const circle: string | null = findCircle(cursusInfo, userProjectId); let nextProjectIds: number[] = []; - if (circle) { // Inner Circle + if (circle) { + // Inner Circle nextProjectIds = getNextProjectIds(cursusInfo, circle); - } else { // Outer Circle + } else { + // Outer Circle nextProjectIds = getOuterProjectIds(cursusInfo, projectList); } return nextProjectIds; @@ -191,11 +187,9 @@ export const getRecommendedProject = async ( * @param projectIds 추천할 프로젝트 id 배열 * @returns 추천할 책 id 배열 */ -export const getRecommendedBookInfoIds = async ( - userProjectIds: number[], -) => { +export const getRecommendedBookInfoIds = async (userProjectIds: number[]) => { if (userProjectIds.length === 0) { - return (booksWithProjectInfo.map((book) => book.book_info_id)); + return booksWithProjectInfo.map((book) => book.book_info_id); } const recommendedBookIds: number[] = []; for (let i = 0; i < booksWithProjectInfo.length; i += 1) { @@ -208,7 +202,7 @@ export const getRecommendedBookInfoIds = async ( } } if (recommendedBookIds.length === 0) { - return (booksWithProjectInfo.map((book) => book.book_info_id)); + return booksWithProjectInfo.map((book) => book.book_info_id); } return [...new Set(recommendedBookIds)]; }; @@ -218,9 +212,7 @@ export const getRecommendedBookInfoIds = async ( * @param bookInfoId 추천 도서의 book_info_id * @returns 추천 도서의 프로젝트 이름 배열 */ -const findProjectNamesWithBookInfoId = ( - bookInfoId: number, -) => { +const findProjectNamesWithBookInfoId = (bookInfoId: number) => { const bookWithProjectInfo = booksWithProjectInfo.find((book) => book.book_info_id === bookInfoId); const recommendedProjects: ProjectInfo[] = projectsInfo.filter((info) => { if (bookWithProjectInfo) { @@ -276,7 +268,9 @@ export const getRecommendMeta = async () => { projectName = '기타'; } let circle = projects[j].circle.toString(); - if (circle === '-1') { circle = '아우터 '; } + if (circle === '-1') { + circle = '아우터 '; + } meta.push(`${circle}서클 | ${projectName}`); } } @@ -290,20 +284,18 @@ export const getRecommendMeta = async () => { * @param data 42 API에서 받아온 프로젝트 정보 * @returns */ -const processData = async ( - data: ProjectFrom42[], -) => { +const processData = async (data: ProjectFrom42[]) => { const ftSeoulData = data.filter((project) => { for (let i = 0; i < project.campus.length; i += 1) { if (project.campus[i].id === 29) { for (let j = 0; j < project.cursus.length; j += 1) { if (project.cursus[j].id === 21) { - return (true); + return true; } } } } - return (false); + return false; }); const processedData: ProjectInfo[] = ftSeoulData.map((project) => ({ id: project.id, @@ -316,7 +308,7 @@ const processData = async ( slug: cursus.slug, })), })); - return (processedData); + return processedData; }; /** @@ -324,22 +316,28 @@ const processData = async ( * @param accessToken 42 API에 접근하기 위한 access token * @param pageNumber 프로젝트 정보를 가져올 페이 */ -export const getProjectsInfo = async ( - accessToken: string, - pageNumber: string, -) => { +export const getProjectsInfo = async (accessToken: string, pageNumber: string) => { const uri: string = 'https://api.intra.42.fr/v2/projects'; - const queryString: string = 'sort=id&filter[exam]=false&filter[visible]=true&filter[has_mark]=true&page[size]=100'; + const queryString: string = + 'sort=id&filter[exam]=false&filter[visible]=true&filter[has_mark]=true&page[size]=100'; const pageQuery: string = `&page[number]=${pageNumber}`; - const response = await axios.get(`${uri}?${queryString}${pageQuery}`, { - headers: { - Authorization: `Bearer ${accessToken}`, - }, - }).catch((error) => { - if (error.status === 401) { throw new ErrorResponse(status[401], 401, 'Unauthorized'); } else { throw new ErrorResponse('500', 500, 'Internal Server Error'); } - }); + const response = await axios + .get(`${uri}?${queryString}${pageQuery}`, { + headers: { + Authorization: `Bearer ${accessToken}`, + AccessControlAllowOrigin: 'https://42library.kr', + AccessControllAllowCredentials: 'true', + }, + }) + .catch((error) => { + if (error.status === 401) { + throw new ErrorResponse(status[401], 401, 'Unauthorized'); + } else { + throw new ErrorResponse('500', 500, 'Internal Server Error'); + } + }); const processedData = await processData(response.data); - return (processedData); + return processedData; }; /** @@ -347,10 +345,7 @@ export const getProjectsInfo = async ( * @param projects 저장할 프로젝트 정보 배열 * @param mode 저장할 모드. append면 기존에 저장된 정보에 추가로 저장하고, overwrite면 기존에 저장된 정보를 덮어쓴다. */ -export const saveProjects = async ( - projects: ProjectInfo[], - mode: string, -) => { +export const saveProjects = async (projects: ProjectInfo[], mode: string) => { const filePath: string = path.join(__dirname, '../../assets', 'projects_info.json'); const jsonString = JSON.stringify(projects, null, 2); if (mode === 'overwrite') { diff --git a/backend/src/v1/histories/histories.controller.ts b/backend/src/v1/histories/histories.controller.ts index 01de6a3a..31abee94 100644 --- a/backend/src/v1/histories/histories.controller.ts +++ b/backend/src/v1/histories/histories.controller.ts @@ -1,6 +1,4 @@ -import { - NextFunction, Request, Response, -} from 'express'; +import { NextFunction, Request, Response } from 'express'; import * as status from 'http-status'; import { logger } from '~/logger'; import * as errorCode from '~/v1/utils/error/errorCode'; @@ -8,16 +6,14 @@ import ErrorResponse from '~/v1/utils/error/errorResponse'; import * as historiesService from './histories.service'; // eslint-disable-next-line import/prefer-default-export -export const histories = async ( - req: Request, - res: Response, - next: NextFunction, -) => { +export const histories = async (req: Request, res: Response, next: NextFunction) => { const query = String(req.query.query) !== 'undefined' ? String(req.query.query) : ''; const who = String(req.query.who) !== 'undefined' ? String(req.query.who) : ''; const page = parseInt(req.query.page as string, 10) ? parseInt(req.query.page as string, 10) : 0; // eslint-disable-next-line max-len - const limit = parseInt(req.query.limit as string, 10) ? parseInt(req.query.limit as string, 10) : 5; + const limit = parseInt(req.query.limit as string, 10) + ? parseInt(req.query.limit as string, 10) + : 5; const type = String(req.query.type) !== 'undefined' ? String(req.query.type) : 'all'; const { id: userId, role: userRole } = req.user as any; diff --git a/backend/src/v1/histories/histories.repository.ts b/backend/src/v1/histories/histories.repository.ts index 1d6d65f9..19e2ed9c 100644 --- a/backend/src/v1/histories/histories.repository.ts +++ b/backend/src/v1/histories/histories.repository.ts @@ -9,8 +9,11 @@ class HistoriesRepository extends Repository { super(VHistories, entityManager); } - async getHistoriesItems(conditions: {}, limit: number, page: number) - : Promise<[VHistories[], number]> { + async getHistoriesItems( + conditions: {}, + limit: number, + page: number, + ): Promise<[VHistories[], number]> { const [histories, count] = await this.findAndCount({ where: conditions, take: limit, diff --git a/backend/src/v1/histories/histories.service.ts b/backend/src/v1/histories/histories.service.ts index 6cdaffc8..67ba1061 100644 --- a/backend/src/v1/histories/histories.service.ts +++ b/backend/src/v1/histories/histories.service.ts @@ -22,10 +22,7 @@ export const getHistories = async ( } else if (type === 'title') { filterQuery.title = Like(`%${query}%`); } else { - filterQuery = [ - { login: Like(`%${query}%`) }, - { title: Like(`%${query}%`) }, - ]; + filterQuery = [{ login: Like(`%${query}%`) }, { title: Like(`%${query}%`) }]; } const historiesRepo = new HistoriesRepository(); const [items, count] = await historiesRepo.getHistoriesItems(filterQuery, limit, page); diff --git a/backend/src/v1/lendings/lendings.controller.ts b/backend/src/v1/lendings/lendings.controller.ts index e47ca818..43eb543f 100644 --- a/backend/src/v1/lendings/lendings.controller.ts +++ b/backend/src/v1/lendings/lendings.controller.ts @@ -1,17 +1,11 @@ -import { - NextFunction, Request, RequestHandler, Response, -} from 'express'; +import { NextFunction, Request, RequestHandler, Response } from 'express'; import * as status from 'http-status'; import { logger } from '~/logger'; import * as errorCode from '~/v1/utils/error/errorCode'; import ErrorResponse from '~/v1/utils/error/errorResponse'; import * as lendingsService from './lendings.service'; -export const create: RequestHandler = async ( - req: Request, - res: Response, - next: NextFunction, -) => { +export const create: RequestHandler = async (req: Request, res: Response, next: NextFunction) => { const { id } = req.user as any; if (!req.body.userId || !req.body.bookId) { next(new ErrorResponse(errorCode.INVALID_INPUT, status.BAD_REQUEST)); @@ -37,22 +31,26 @@ export const create: RequestHandler = async ( } }; -const argumentCheck = (sort:string, type:string) => { - if (type !== 'user' && type !== 'title' && type !== 'callSign' && type !== 'all' && type !== 'bookId') { return 0; } +const argumentCheck = (sort: string, type: string) => { + if ( + type !== 'user' && + type !== 'title' && + type !== 'callSign' && + type !== 'all' && + type !== 'bookId' + ) { + return 0; + } return 1; }; -export const search: RequestHandler = async ( - req: Request, - res: Response, - next: NextFunction, -) => { +export const search: RequestHandler = async (req: Request, res: Response, next: NextFunction) => { const info = req.query; const query = String(info.query) !== 'undefined' ? String(info.query) : ''; const page = parseInt(info.page as string, 10) ? parseInt(info.page as string, 10) : 0; const limit = parseInt(info.limit as string, 10) ? parseInt(info.limit as string, 10) : 5; const sort = info.sort as string; - const type = info.type as string ? info.type as string : 'all'; + const type = (info.type as string) ? (info.type as string) : 'all'; if (!argumentCheck(sort, type)) { return next(new ErrorResponse(errorCode.INVALID_INPUT, status.BAD_REQUEST)); } @@ -109,11 +107,7 @@ export const returnBook: RequestHandler = async ( return next(new ErrorResponse(errorCode.INVALID_INPUT, status.BAD_REQUEST)); } try { - const result = await lendingsService.returnBook( - id, - req.body.lendingId, - req.body.condition, - ); + const result = await lendingsService.returnBook(id, req.body.lendingId, req.body.condition); res.status(status.OK).json(result); } catch (error: any) { const errorNumber = parseInt(error.message, 10); diff --git a/backend/src/v1/lendings/lendings.repository.ts b/backend/src/v1/lendings/lendings.repository.ts index f265677d..ffc03b37 100644 --- a/backend/src/v1/lendings/lendings.repository.ts +++ b/backend/src/v1/lendings/lendings.repository.ts @@ -1,11 +1,7 @@ -import { - IsNull, MoreThan, QueryRunner, Repository, UpdateResult, -} from 'typeorm'; +import { IsNull, MoreThan, QueryRunner, Repository, UpdateResult } from 'typeorm'; import { QueryDeepPartialEntity } from 'typeorm/query-builder/QueryPartialEntity.js'; import jipDataSource from '~/app-data-source'; -import { - VUserLending, Reservation, VLending, Lending, User, Book, -} from '~/entity/entities'; +import { VUserLending, Reservation, VLending, Lending, User, Book } from '~/entity/entities'; import { formatDate } from '~/v1/utils/dateFormat'; class LendingRepository extends Repository { @@ -24,30 +20,15 @@ class LendingRepository extends Repository { const entityManager = jipDataSource.createEntityManager(queryRunner); super(Lending, entityManager); - this.userRepo = new Repository( - User, - entityManager, - ); - - this.userLendingRepo = new Repository( - VUserLending, - entityManager, - ); - - this.bookRepo = new Repository( - Book, - entityManager, - ); - - this.reserveRepo = new Repository( - Reservation, - entityManager, - ); - - this.vlendingRepo = new Repository( - VLending, - entityManager, - ); + this.userRepo = new Repository(User, entityManager); + + this.userLendingRepo = new Repository(VUserLending, entityManager); + + this.bookRepo = new Repository(Book, entityManager); + + this.reserveRepo = new Repository(Reservation, entityManager); + + this.vlendingRepo = new Repository(VLending, entityManager); } async searchLendingCount(conditions: {}, limit: number, page: number) { @@ -59,8 +40,12 @@ class LendingRepository extends Repository { return count; } - async searchLending(conditions: {}, limit: number, page: number, order: {}) - : Promise<[VLending[], number]> { + async searchLending( + conditions: {}, + limit: number, + page: number, + order: {}, + ): Promise<[VLending[], number]> { const [lending, count] = await this.vlendingRepo.findAndCount({ select: [ 'id', @@ -165,25 +150,20 @@ class LendingRepository extends Repository { const updateObject: QueryDeepPartialEntity = { returningLibrarianId, returningCondition, - returnedAt: (new Date()), - updatedAt: (new Date()), + returnedAt: new Date(), + updatedAt: new Date(), }; await this.update(lendingId, updateObject); } - async updateUserPenaltyEndDate( - penaltyEndDate: string, - id: number, - ): Promise { + async updateUserPenaltyEndDate(penaltyEndDate: string, id: number): Promise { const updateObject: QueryDeepPartialEntity = { penaltyEndDate, }; await this.userRepo.update(id, updateObject); } - async searchReservedBook( - bookInfoId: number, - ): Promise { + async searchReservedBook(bookInfoId: number): Promise { const reservation = await this.reserveRepo.findOne({ relations: ['book', 'user', 'bookInfo'], where: { @@ -208,9 +188,7 @@ class LendingRepository extends Repository { return this.reserveRepo.update(reservationId, updateObject); } - async updateReservationToLended( - reservationId: number, - ): Promise { + async updateReservationToLended(reservationId: number): Promise { await this.reserveRepo.update(reservationId, { status: 1 }); } } diff --git a/backend/src/v1/lendings/lendings.service.spec.ts b/backend/src/v1/lendings/lendings.service.spec.ts index ce9e9672..0271a0ec 100644 --- a/backend/src/v1/lendings/lendings.service.spec.ts +++ b/backend/src/v1/lendings/lendings.service.spec.ts @@ -12,44 +12,52 @@ describe('LendingsService', () => { const condition = '이상없음'; it('lend a book (success)', async () => { - expect(await lendingsService.create(userId, bookId, librarianId, condition)) - .toBe(lendingsService.NO_USER_ID); + expect(await lendingsService.create(userId, bookId, librarianId, condition)).toBe( + lendingsService.NO_USER_ID, + ); }); it('lend a book (noPermission)', async () => { userId = 1392; - expect(await lendingsService.create(userId, bookId, librarianId, condition)) - .toBe(lendingsService.noPermission); + expect(await lendingsService.create(userId, bookId, librarianId, condition)).toBe( + lendingsService.noPermission, + ); }); it('lend a book (lendingOverload)', async () => { userId = 1408; - expect(await lendingsService.create(userId, bookId, librarianId, condition)) - .toBe(lendingsService.lendingOverload); + expect(await lendingsService.create(userId, bookId, librarianId, condition)).toBe( + lendingsService.lendingOverload, + ); }); it('lend a book (LENDING_OVERDUE)', async () => { userId = 1418; - expect(await lendingsService.create(userId, bookId, librarianId, condition)) - .toBe(lendingsService.LENDING_OVERDUE); + expect(await lendingsService.create(userId, bookId, librarianId, condition)).toBe( + lendingsService.LENDING_OVERDUE, + ); }); it('lend a book (ON_LENDING)', async () => { userId = 1444; bookId = 1; - expect(await lendingsService.create(userId, bookId, librarianId, condition)) - .toBe(lendingsService.ON_LENDING); + expect(await lendingsService.create(userId, bookId, librarianId, condition)).toBe( + lendingsService.ON_LENDING, + ); }); it('lend a book (ON_RESERVATION)', async () => { bookId = 82; - expect(await lendingsService.create(userId, bookId, librarianId, condition)) - .toBe(lendingsService.ON_RESERVATION); + expect(await lendingsService.create(userId, bookId, librarianId, condition)).toBe( + lendingsService.ON_RESERVATION, + ); }); it('lend a book (LOST_BOOK)', async () => { bookId = 859; - expect(await lendingsService.create(userId, bookId, librarianId, condition)) - .toBe(lendingsService.LOST_BOOK); + expect(await lendingsService.create(userId, bookId, librarianId, condition)).toBe( + lendingsService.LOST_BOOK, + ); }); it('lend a book (DAMAGED_BOOK)', async () => { bookId = 858; - expect(await lendingsService.create(userId, bookId, librarianId, condition)) - .toBe(lendingsService.DAMAGED_BOOK); + expect(await lendingsService.create(userId, bookId, librarianId, condition)).toBe( + lendingsService.DAMAGED_BOOK, + ); }); it('search lending record (success)', async () => { @@ -152,18 +160,21 @@ describe('LendingsService', () => { let lendingId = 135; it('return a book (ok)', async () => { - expect(await lendingsService.returnBook(librarianId, lendingId, condition)) - .toBe(lendingsService.ok); + expect(await lendingsService.returnBook(librarianId, lendingId, condition)).toBe( + lendingsService.ok, + ); }); it('return a book (ALREADY_RETURNED)', async () => { - expect(await lendingsService.returnBook(librarianId, lendingId, condition)) - .toBe(lendingsService.ALREADY_RETURNED); + expect(await lendingsService.returnBook(librarianId, lendingId, condition)).toBe( + lendingsService.ALREADY_RETURNED, + ); }); it('return a book (NONEXISTENT_LENDING)', async () => { lendingId = 1000; - expect(await lendingsService.returnBook(librarianId, lendingId, condition)) - .toBe(lendingsService.NONEXISTENT_LENDING); + expect(await lendingsService.returnBook(librarianId, lendingId, condition)).toBe( + lendingsService.NONEXISTENT_LENDING, + ); }); }); diff --git a/backend/src/v1/lendings/lendings.service.ts b/backend/src/v1/lendings/lendings.service.ts index a310456c..ba87c44a 100644 --- a/backend/src/v1/lendings/lendings.service.ts +++ b/backend/src/v1/lendings/lendings.service.ts @@ -22,21 +22,34 @@ export const create = async ( try { await transaction.startTransaction(); const [users, count] = await usersRepository.searchUserBy({ id: userId }, 0, 0); - if (!count) { throw new Error(errorCode.NO_USER_ID); } - if (users[0].role === 0) { throw new Error(errorCode.NO_PERMISSION); } + if (!count) { + throw new Error(errorCode.NO_USER_ID); + } + if (users[0].role === 0) { + throw new Error(errorCode.NO_PERMISSION); + } // user conditions - const numberOfLendings = await lendingRepo.searchLendingCount({ - userId, - }, 0, 0); - if (numberOfLendings >= 2) { throw new Error(errorCode.LENDING_OVERLOAD); } + const numberOfLendings = await lendingRepo.searchLendingCount( + { + userId, + }, + 0, + 0, + ); + if (numberOfLendings >= 2) { + throw new Error(errorCode.LENDING_OVERLOAD); + } const penaltyEndDate = await lendingRepo.getUsersPenalty(userId); const overDueDay = await lendingRepo.getUsersOverDueDay(userId); - if (penaltyEndDate >= new Date() - || overDueDay !== undefined) { throw new Error(errorCode.LENDING_OVERDUE); } + if (penaltyEndDate >= new Date() || overDueDay !== undefined) { + throw new Error(errorCode.LENDING_OVERDUE); + } // book conditions const countOfBookInLending = await lendingRepo.getLendingCountByBookId(bookId); - if (countOfBookInLending !== 0) { throw new Error(errorCode.ON_LENDING); } + if (countOfBookInLending !== 0) { + throw new Error(errorCode.ON_LENDING); + } // 책이 분실, 파손이 아닌지 const book = await lendingRepo.searchBookForLending(bookId); @@ -54,10 +67,17 @@ export const create = async ( // 책 대출 정보 insert await lendingRepo.createLending(userId, bookId, librarianId, condition); // 예약 대출 시 상태값 reservation status 0 -> 1 변경 - if (reservationOfBook) { await lendingRepo.updateReservationToLended(reservationOfBook.id); } + if (reservationOfBook) { + await lendingRepo.updateReservationToLended(reservationOfBook.id); + } await transaction.commitTransaction(); if (users[0].slack) { - await publishMessage(users[0].slack, `:jiphyeonjeon: 대출 알림 :jiphyeonjeon: \n대출 하신 \`${book?.info?.title}\`은(는) ${formatDate(dueDate)}까지 반납해주세요.`); + await publishMessage( + users[0].slack, + `:jiphyeonjeon: 대출 알림 :jiphyeonjeon: \n대출 하신 \`${ + book?.info?.title + }\`은(는) ${formatDate(dueDate)}까지 반납해주세요.`, + ); } } catch (e) { await transaction.rollbackTransaction(); @@ -67,14 +87,10 @@ export const create = async ( } finally { await transaction.release(); } - return ({ dueDate: formatDate(dueDate) }); + return { dueDate: formatDate(dueDate) }; }; -export const returnBook = async ( - librarianId: number, - lendingId: number, - condition: string, -) => { +export const returnBook = async (librarianId: number, lendingId: number, condition: string) => { const transaction = jipDataSource.createQueryRunner(); const lendingRepo = new LendingRepository(transaction); try { @@ -91,7 +107,12 @@ export const returnBook = async ( const today = new Date().setHours(0, 0, 0, 0); const createdDate = new Date(lendingInfo.createdAt); // eslint-disable-next-line max-len - const expecetReturnDate = new Date(createdDate.setDate(createdDate.getDate() + 14)).setHours(0, 0, 0, 0); + const expecetReturnDate = new Date(createdDate.setDate(createdDate.getDate() + 14)).setHours( + 0, + 0, + 0, + 0, + ); if (today > expecetReturnDate) { const todayDate = new Date(); const overDueDays = (today - expecetReturnDate) / 1000 / 60 / 60 / 24; @@ -100,9 +121,15 @@ export const returnBook = async ( // eslint-disable-next-line max-len const originPenaltyEndDate = new Date(penaltyEndDateInDB); if (today < originPenaltyEndDate.setHours(0, 0, 0, 0)) { - confirmedPenaltyEndDate = new Date(originPenaltyEndDate.setDate(originPenaltyEndDate.getDate() + overDueDays)).toISOString().split('T')[0]; + confirmedPenaltyEndDate = new Date( + originPenaltyEndDate.setDate(originPenaltyEndDate.getDate() + overDueDays), + ) + .toISOString() + .split('T')[0]; } else { - confirmedPenaltyEndDate = new Date(todayDate.setDate(todayDate.getDate() + overDueDays)).toISOString().split('T')[0]; + confirmedPenaltyEndDate = new Date(todayDate.setDate(todayDate.getDate() + overDueDays)) + .toISOString() + .split('T')[0]; } await lendingRepo.updateUserPenaltyEndDate(confirmedPenaltyEndDate, lendingInfo.userId); } @@ -117,14 +144,19 @@ export const returnBook = async ( if (updateResult && slackIdReservedUser) { // 예약자에게 슬랙메시지 보내기 const bookTitle = reservationInfo.bookInfo.title; - if (slackIdReservedUser) { await publishMessage(slackIdReservedUser, `:jiphyeonjeon: 예약 알림 :jiphyeonjeon:\n예약하신 도서 \`${bookTitle}\`(이)가 대출 가능합니다. 3일 내로 집현전에 방문해 대출해주세요.`); } + if (slackIdReservedUser) { + await publishMessage( + slackIdReservedUser, + `:jiphyeonjeon: 예약 알림 :jiphyeonjeon:\n예약하신 도서 \`${bookTitle}\`(이)가 대출 가능합니다. 3일 내로 집현전에 방문해 대출해주세요.`, + ); + } } } await transaction.commitTransaction(); if (reservationInfo) { - return ({ reservedBook: true }); + return { reservedBook: true }; } - return ({ reservedBook: false }); + return { reservedBook: false }; } catch (error) { await transaction.rollbackTransaction(); if (error instanceof Error) { @@ -139,7 +171,7 @@ export const search = async ( query: string, page: number, limit: number, - sort:string, + sort: string, type: string, ) => { const lendingRepo = new LendingRepository(); @@ -165,12 +197,7 @@ export const search = async ( ]); } const orderQuery = sort === 'new' ? { createdAt: 'DESC' } : { createdAt: 'ASC' }; - const [items, count] = await lendingRepo.searchLending( - filterQuery, - limit, - page, - orderQuery, - ); + const [items, count] = await lendingRepo.searchLending(filterQuery, limit, page, orderQuery); const meta: Meta = { totalItems: count, itemCount: items.length, @@ -181,7 +208,7 @@ export const search = async ( return { items, meta }; }; -export const lendingId = async (id:number) => { +export const lendingId = async (id: number) => { const lendingRepo = new LendingRepository(); const data = (await lendingRepo.searchLending({ id }, 0, 0, {}))[0]; return data[0]; diff --git a/backend/src/v1/middlewares/wrapAsyncController.ts b/backend/src/v1/middlewares/wrapAsyncController.ts index 64563f55..fd835025 100644 --- a/backend/src/v1/middlewares/wrapAsyncController.ts +++ b/backend/src/v1/middlewares/wrapAsyncController.ts @@ -1,8 +1,5 @@ /* eslint-disable import/prefer-default-export */ -import { - NextFunction, - Request, Response, -} from 'express'; +import { NextFunction, Request, Response } from 'express'; import ErrorResponse from '~/v1/utils/error/errorResponse'; import * as errorCode from '~/v1/utils/error/errorCode'; diff --git a/backend/src/v1/notifications/notifications.service.ts b/backend/src/v1/notifications/notifications.service.ts index 1be21ee6..6cefcab5 100644 --- a/backend/src/v1/notifications/notifications.service.ts +++ b/backend/src/v1/notifications/notifications.service.ts @@ -1,18 +1,16 @@ import { executeQuery, makeExecuteQuery, pool } from '~/mysql'; import { publishMessage } from '../slack/slack.service'; -const succeedReservation = async (reservation: { - bookId: number, - bookInfoId: number, -}) => { +const succeedReservation = async (reservation: { bookId: number; bookInfoId: number }) => { const conn = await pool.getConnection(); const transactionExecuteQuery = makeExecuteQuery(conn); try { const candidates: { - id: number - slack: string, - title: string, - }[] = await transactionExecuteQuery(` + id: number; + slack: string; + title: string; + }[] = await transactionExecuteQuery( + ` SELECT reservation.id AS id, user.slack AS slack, @@ -29,9 +27,12 @@ const succeedReservation = async (reservation: { ORDER BY reservation.createdAt DESC LIMIT 1 - `, [reservation.bookInfoId]); + `, + [reservation.bookInfoId], + ); if (candidates.length !== 0) { - await transactionExecuteQuery(` + await transactionExecuteQuery( + ` UPDATE reservation SET @@ -39,8 +40,13 @@ const succeedReservation = async (reservation: { endAt = DATE_ADD(NOW(), INTERVAL 3 DAY) WHERE reservation.id = ? - `, [reservation.bookId, candidates[0].id]); - publishMessage(candidates[0].slack, `:jiphyeonjeon: 예약 알림 :jiphyeonjeon:\n예약하신 도서 \`${candidates[0].title}\`(이)가 대출 가능합니다. 3일 내로 집현전에 방문해 대출해주세요. (방문하시기 전에 비치 여부를 확인해주세요)`); + `, + [reservation.bookId, candidates[0].id], + ); + publishMessage( + candidates[0].slack, + `:jiphyeonjeon: 예약 알림 :jiphyeonjeon:\n예약하신 도서 \`${candidates[0].title}\`(이)가 대출 가능합니다. 3일 내로 집현전에 방문해 대출해주세요. (방문하시기 전에 비치 여부를 확인해주세요)`, + ); } } catch (e) { await conn.rollback(); @@ -53,10 +59,12 @@ const succeedReservation = async (reservation: { }; export const notifyReservation = async () => { - const reservations: [{ - bookId: number, - bookInfoId: number, - }] = await executeQuery(` + const reservations: [ + { + bookId: number; + bookInfoId: number; + }, + ] = await executeQuery(` SELECT reservation.bookId AS bookId, reservation.bookInfoId AS bookInfoId @@ -75,10 +83,10 @@ export const notifyReservation = async () => { export const notifyReservationOverdue = async () => { const reservations: { - slack: string, - title: string, - bookId: number, - bookInfoId: number, + slack: string; + title: string; + bookId: number; + bookInfoId: number; }[] = await executeQuery(` SELECT user.slack AS slack, @@ -96,8 +104,12 @@ export const notifyReservationOverdue = async () => { DATEDIFF(CURDATE(), DATE(reservation.endAt)) = 1 `); reservations.forEach(async (reservation) => { - publishMessage(reservation.slack, `:jiphyeonjeon: 예약 만료 알림 :jiphyeonjeon:\n예약하신 도서 \`${reservation.title}\`의 예약이 만료되었습니다.`); - const ranks: [{id: number, createdAt: Date}] = await executeQuery(` + publishMessage( + reservation.slack, + `:jiphyeonjeon: 예약 만료 알림 :jiphyeonjeon:\n예약하신 도서 \`${reservation.title}\`의 예약이 만료되었습니다.`, + ); + const ranks: [{ id: number; createdAt: Date }] = await executeQuery( + ` SELECT id, createdAt @@ -106,20 +118,25 @@ export const notifyReservationOverdue = async () => { WHERE bookInfoId = ? AND status = 0 ORDER BY createdAt ASC - `, [reservation.bookInfoId]); - await executeQuery(` + `, + [reservation.bookInfoId], + ); + await executeQuery( + ` UPDATE reservation SET bookId = ?, endAt = ADDDATE(CURDATE(),1) WHERE id = ? - `, [reservation.bookId, ranks[0].id]); + `, + [reservation.bookId, ranks[0].id], + ); }); }; export const notifyReturningReminder = async () => { - const lendings: [{title: string, slack: string}] = await executeQuery(` + const lendings: [{ title: string; slack: string }] = await executeQuery(` SELECT book_info.title, user.slack @@ -136,15 +153,18 @@ export const notifyReturningReminder = async () => { lending.returnedAt IS NULL `); lendings.forEach(async (lending) => { - publishMessage(lending.slack, `:jiphyeonjeon: 반납 알림 :jiphyeonjeon:\n 대출하신 도서 \`${lending.title}\`의 반납 기한이 다가왔습니다. 3일 내로 반납해주시기 바랍니다.`); + publishMessage( + lending.slack, + `:jiphyeonjeon: 반납 알림 :jiphyeonjeon:\n 대출하신 도서 \`${lending.title}\`의 반납 기한이 다가왔습니다. 3일 내로 반납해주시기 바랍니다.`, + ); }); }; -type Lender = {title: string, slack: string, daysLeft: number}; +type Lender = { title: string; slack: string; daysLeft: number }; // day : 반납까지 남은 기한. // 반납기한이 N일 남은 유저의 목록을 가져옵니다. -export const GetUserFromNDaysLeft = async (day : number) : Promise => { +export const GetUserFromNDaysLeft = async (day: number): Promise => { const LOAN_PERIOD = 14; const daysLeft = LOAN_PERIOD - day; const lendings: Lender[] = await executeQuery(` @@ -166,9 +186,13 @@ export const GetUserFromNDaysLeft = async (day : number) : Promise => return lendings.map(({ ...args }) => ({ ...args, daysLeft: day })); }; -const notifyUser = ({ slack, title, daysLeft }: Lender) => publishMessage(slack, `:jiphyeonjeon: 반납 알림 :jiphyeonjeon:\n 대출하신 도서 \`${title}\`의 반납 기한이 다가왔습니다. ${daysLeft}일 내로 반납해주시기 바랍니다.`); +const notifyUser = ({ slack, title, daysLeft }: Lender) => + publishMessage( + slack, + `:jiphyeonjeon: 반납 알림 :jiphyeonjeon:\n 대출하신 도서 \`${title}\`의 반납 기한이 다가왔습니다. ${daysLeft}일 내로 반납해주시기 바랍니다.`, + ); -export const notifyUsers = async (userList : Lender[], notifyFn: (_: Lender) => Promise) => { +export const notifyUsers = async (userList: Lender[], notifyFn: (_: Lender) => Promise) => { await Promise.all(userList.map(notifyFn)); }; @@ -180,7 +204,7 @@ export const notifyOverdueManager = async () => { }; export const notifyOverdue = async () => { - const lendings: [{title: string, slack: string}] = await executeQuery(` + const lendings: [{ title: string; slack: string }] = await executeQuery(` SELECT book_info.title, user.slack @@ -197,6 +221,9 @@ export const notifyOverdue = async () => { lending.returnedAt IS NULL `); lendings.forEach(async (lending) => { - publishMessage(lending.slack, `:jiphyeonjeon: 연체 알림 :jiphyeonjeon:\n 대출하신 도서 \`${lending.title}\`가 연체되었습니다. 빠른 시일 내에 반납해주시기 바랍니다.`); + publishMessage( + lending.slack, + `:jiphyeonjeon: 연체 알림 :jiphyeonjeon:\n 대출하신 도서 \`${lending.title}\`가 연체되었습니다. 빠른 시일 내에 반납해주시기 바랍니다.`, + ); }); }; diff --git a/backend/src/v1/reservations/reservations.controller.ts b/backend/src/v1/reservations/reservations.controller.ts index 5daed9b6..4ec347ba 100644 --- a/backend/src/v1/reservations/reservations.controller.ts +++ b/backend/src/v1/reservations/reservations.controller.ts @@ -1,6 +1,4 @@ -import { - NextFunction, Request, RequestHandler, Response, -} from 'express'; +import { NextFunction, Request, RequestHandler, Response } from 'express'; import * as status from 'http-status'; import { logger } from '~/logger'; import * as userUtils from '~/v1/users/users.utils'; @@ -8,11 +6,7 @@ import * as errorCode from '~/v1/utils/error/errorCode'; import ErrorResponse from '~/v1/utils/error/errorResponse'; import * as reservationsService from './reservations.service'; -export const create: RequestHandler = async ( - req: Request, - res: Response, - next: NextFunction, -) => { +export const create: RequestHandler = async (req: Request, res: Response, next: NextFunction) => { const { id } = req.user as any; const bookInfoId = Number.parseInt(req.body.bookInfoId, 10); if (Number.isNaN(bookInfoId)) { @@ -21,9 +15,7 @@ export const create: RequestHandler = async ( try { const createdReservation = await reservationsService.create(id, req.body.bookInfoId); logger.info(`[ES_R] : userId: ${id} bookInfoId: ${bookInfoId}`); - return res - .status(status.OK) - .json(createdReservation); + return res.status(status.OK).json(createdReservation); } catch (error: any) { const errorNumber = parseInt(error.message, 10); if (errorNumber >= 500 && errorNumber < 600) { @@ -52,7 +44,7 @@ const filterCheck = (argument: string) => { export const search: RequestHandler = async (req: Request, res: Response, next: NextFunction) => { const info = req.query; - const query = info.query as string ? info.query as string : ''; + const query = (info.query as string) ? (info.query as string) : ''; const page = parseInt(info.page as string, 10) ? parseInt(info.page as string, 10) : 0; const limit = parseInt(info.limit as string, 10) ? parseInt(info.limit as string, 10) : 5; const filter = info.filter as string; @@ -61,9 +53,7 @@ export const search: RequestHandler = async (req: Request, res: Response, next: } try { const searchResult = await reservationsService.search(query, page, limit, filter); - return res - .status(status.OK) - .json(searchResult); + return res.status(status.OK).json(searchResult); } catch (error: any) { const errorNumber = parseInt(error.message, 10); if (errorNumber >= 500 && errorNumber < 600) { @@ -74,7 +64,8 @@ export const search: RequestHandler = async (req: Request, res: Response, next: logger.error(error); next(new ErrorResponse(errorCode.UNKNOWN_ERROR, status.INTERNAL_SERVER_ERROR)); } - } return 0; + } + return 0; }; export const cancel: RequestHandler = async (req: Request, res: Response, next: NextFunction) => { @@ -98,7 +89,8 @@ export const cancel: RequestHandler = async (req: Request, res: Response, next: logger.error(error); next(new ErrorResponse(errorCode.UNKNOWN_ERROR, status.INTERNAL_SERVER_ERROR)); } - } return 0; + } + return 0; }; export const count: RequestHandler = async (req: Request, res: Response, next: NextFunction) => { @@ -119,7 +111,8 @@ export const count: RequestHandler = async (req: Request, res: Response, next: N logger.error(error); next(new ErrorResponse(errorCode.UNKNOWN_ERROR, status.INTERNAL_SERVER_ERROR)); } - } return 0; + } + return 0; }; export const userReservations: RequestHandler = async ( @@ -133,9 +126,7 @@ export const userReservations: RequestHandler = async ( return next(new ErrorResponse(errorCode.INVALID_INPUT, status.BAD_REQUEST)); } try { - return res - .status(status.OK) - .json(await reservationsService.userReservations(userId)); + return res.status(status.OK).json(await reservationsService.userReservations(userId)); } catch (error: any) { const errorNumber = parseInt(error.message, 10); if (errorNumber >= 500 && errorNumber < 600) { diff --git a/backend/src/v1/reservations/reservations.repository.ts b/backend/src/v1/reservations/reservations.repository.ts index ddbc3c15..e3a1d590 100644 --- a/backend/src/v1/reservations/reservations.repository.ts +++ b/backend/src/v1/reservations/reservations.repository.ts @@ -1,15 +1,6 @@ -import { - Brackets, - IsNull, MoreThan, Not, QueryRunner, Repository, -} from 'typeorm'; - -import { - BookInfo, - User, - Lending, - Book, - Reservation, -} from '~/entity/entities'; +import { Brackets, IsNull, MoreThan, Not, QueryRunner, Repository } from 'typeorm'; + +import { BookInfo, User, Lending, Book, Reservation } from '~/entity/entities'; import jipDataSource from '~/app-data-source'; import { Meta } from '../DTO/common.interface'; @@ -27,18 +18,9 @@ class ReservationsRepository extends Repository { const entityManager = jipDataSource.createEntityManager(queryRunner); super(Reservation, entityManager); - this.bookInfo = new Repository( - BookInfo, - entityManager, - ); - this.user = new Repository( - User, - entityManager, - ); - this.lending = new Repository( - Lending, - entityManager, - ); + this.bookInfo = new Repository(BookInfo, entityManager); + this.user = new Repository(User, entityManager); + this.lending = new Repository(Lending, entityManager); } // 유저가 대출 패널티 중인지 확인 @@ -59,7 +41,11 @@ class ReservationsRepository extends Repository { .createQueryBuilder('u') .select('u.id') .addSelect('count(u.id)', 'overdueLendingCnt') - .innerJoin('lending', 'l', 'l.userId = u.id AND l.returnedAt IS NULL AND DATEDIFF(now(), DATE_ADD(l.createdAt, INTERVAL 14 DAY)) > 0') + .innerJoin( + 'lending', + 'l', + 'l.userId = u.id AND l.returnedAt IS NULL AND DATEDIFF(now(), DATE_ADD(l.createdAt, INTERVAL 14 DAY)) > 0', + ) .where('u.id = :userId', { userId }) .groupBy('u.id') .getExists(); @@ -67,15 +53,19 @@ class ReservationsRepository extends Repository { // 유저가 2권 이상 예약 중인지 확인 async isAllRenderUser(userId: number): Promise { - const [rendUser] = await Promise.all([this.user - .createQueryBuilder('u') - .select('u.id', 'id') - .addSelect('COUNT(r.id)', 'count') - .innerJoin('reservation', 'r', 'r.userId = u.id AND r.status = 0') - .where(`u.id = ${userId}`) - .groupBy('u.id') - .getRawOne()]); - if (rendUser?.count >= 2) { return true; } + const [rendUser] = await Promise.all([ + this.user + .createQueryBuilder('u') + .select('u.id', 'id') + .addSelect('COUNT(r.id)', 'count') + .innerJoin('reservation', 'r', 'r.userId = u.id AND r.status = 0') + .where(`u.id = ${userId}`) + .groupBy('u.id') + .getRawOne(), + ]); + if (rendUser?.count >= 2) { + return true; + } return false; } @@ -85,7 +75,11 @@ class ReservationsRepository extends Repository { .createQueryBuilder('u') .select('u.id') .addSelect('u.nickname') - .leftJoin('lending', 'l', 'l.userId = u.id AND l.returnedAt IS NULL AND DATEDIFF(now(), DATE_ADD(l.createdAt, INTERVAL 14 DAY) > 0') + .leftJoin( + 'lending', + 'l', + 'l.userId = u.id AND l.returnedAt IS NULL AND DATEDIFF(now(), DATE_ADD(l.createdAt, INTERVAL 14 DAY) > 0', + ) .leftJoin('reservation', 'r', 'r.userId = u.id AND r.status = 0') .groupBy('u.id') .having('count(l.id) = 0 AND count(DISTINCT r.id) < 2') @@ -123,8 +117,7 @@ class ReservationsRepository extends Repository { // Todo: return 값 수정할 것 async getReservedBooks(userId: number, bookInfoId: number) { - const reservedBooks = this - .createQueryBuilder('r') + const reservedBooks = this.createQueryBuilder('r') .select('r.id', 'id') .where('r.bookInfoId = :bookInfoId', { bookInfoId }) .andWhere('r.userId = :userId', { userId }) @@ -132,7 +125,7 @@ class ReservationsRepository extends Repository { return reservedBooks; } - async createReservation(userId: number, bookInfoId:number): Promise { + async createReservation(userId: number, bookInfoId: number): Promise { await this.createQueryBuilder() .insert() .into(Reservation) @@ -140,10 +133,13 @@ class ReservationsRepository extends Repository { .execute(); } - async searchReservations(query: string, filter: string, page: number, limit: number): - Promise<{ meta: Meta; items: Reservation[] }> { - const searchAll = this - .createQueryBuilder('r') + async searchReservations( + query: string, + filter: string, + page: number, + limit: number, + ): Promise<{ meta: Meta; items: Reservation[] }> { + const searchAll = this.createQueryBuilder('r') .select('r.id', 'reservationsId') .addSelect('r.endAt', 'endAt') .addSelect('r.createdAt', 'createdAt') @@ -151,7 +147,10 @@ class ReservationsRepository extends Repository { .addSelect('r.userId', 'userId') .addSelect('r.bookId', 'bookId') .addSelect('u.nickname', 'login') - .addSelect('CASE WHEN NOW() > u.penaltyEndDate THEN 0 ELSE DATEDIFF(u.penaltyEndDate, NOW()) END', 'penaltyDays') + .addSelect( + 'CASE WHEN NOW() > u.penaltyEndDate THEN 0 ELSE DATEDIFF(u.penaltyEndDate, NOW()) END', + 'penaltyDays', + ) .addSelect('bi.title', 'title') .addSelect('bi.image', 'image') .addSelect('(SELECT COUNT(*) FROM reservation)', 'count') @@ -159,11 +158,13 @@ class ReservationsRepository extends Repository { .leftJoin('user', 'u', 'r.userId = u.id') .leftJoin('book_info', 'bi', 'r.bookInfoId = bi.id') .leftJoin('book', 'b', 'r.bookId = b.id') - .where(new Brackets((qb) => { - qb.where('bi.title like :query', { query: `%${query}%` }) - .orWhere('u.nickname like :query', { query: `%${query}%` }) - .orWhere('b.callSign like :query', { query: `%${query}%` }); - })); + .where( + new Brackets((qb) => { + qb.where('bi.title like :query', { query: `%${query}%` }) + .orWhere('u.nickname like :query', { query: `%${query}%` }) + .orWhere('b.callSign like :query', { query: `%${query}%` }); + }), + ); switch (filter) { case 'waiting': searchAll.andWhere({ status: 0, bookId: IsNull() }); @@ -179,9 +180,12 @@ class ReservationsRepository extends Repository { default: searchAll.andWhere({ status: 0, bookId: IsNull() }); } - const items = await searchAll.offset(limit * page).limit(limit).getRawMany(); + const items = await searchAll + .offset(limit * page) + .limit(limit) + .getRawMany(); const totalItems = await searchAll.getCount(); - const meta : Meta = { + const meta: Meta = { totalItems, itemCount: items.length, itemsPerPage: limit, diff --git a/backend/src/v1/reservations/reservations.service.spec.ts b/backend/src/v1/reservations/reservations.service.spec.ts index 7887a354..05c3abb9 100644 --- a/backend/src/v1/reservations/reservations.service.spec.ts +++ b/backend/src/v1/reservations/reservations.service.spec.ts @@ -128,14 +128,12 @@ describe('ReservationsServices', () => { it('reservation count (INVALID_INFO_ID)', async () => { bookInfoId = 4242; - expect(await reservationsService.count(bookInfoId)) - .toBe(reservationsService.INVALID_INFO_ID); + expect(await reservationsService.count(bookInfoId)).toBe(reservationsService.INVALID_INFO_ID); }); it('reservation count (NOT_LENDED)', async () => { bookInfoId = 1; - expect(await reservationsService.count(bookInfoId)) - .toBe(reservationsService.NOT_LENDED); + expect(await reservationsService.count(bookInfoId)).toBe(reservationsService.NOT_LENDED); }); it('get user reservation', async () => { diff --git a/backend/src/v1/reservations/reservations.service.ts b/backend/src/v1/reservations/reservations.service.ts index 1cd9063d..110c0ffa 100644 --- a/backend/src/v1/reservations/reservations.service.ts +++ b/backend/src/v1/reservations/reservations.service.ts @@ -7,16 +7,20 @@ import { publishMessage } from '../slack/slack.service'; import ReservationsRepository from './reservations.repository'; export const count = async (bookInfoId: number) => { - const numberOfBookInfo = await executeQuery(` + const numberOfBookInfo = await executeQuery( + ` SELECT COUNT(*) as count FROM book WHERE infoId = ? AND status = 0; - `, [bookInfoId]); + `, + [bookInfoId], + ); if (numberOfBookInfo[0].count === 0) { throw new Error(errorCode.INVALID_INFO_ID); } // bookInfoId가 모두 대출 중이거나 예약 중인지 확인 - const cantReservBookInfo = await executeQuery(` + const cantReservBookInfo = await executeQuery( + ` SELECT COUNT(*) as count FROM book LEFT JOIN lending ON lending.bookId = book.id @@ -24,15 +28,20 @@ export const count = async (bookInfoId: number) => { WHERE book.infoId = ? AND book.status = 0 AND (lending.returnedAt IS NULL OR reservation.status = 0); -`, [bookInfoId]); +`, + [bookInfoId], + ); if (numberOfBookInfo[0].count > cantReservBookInfo[0].count) { throw new Error(errorCode.NOT_LENDED); } - const numberOfReservations = await executeQuery(` + const numberOfReservations = await executeQuery( + ` SELECT COUNT(*) as count FROM reservation WHERE bookInfoId = ? AND status = 0; - `, [bookInfoId]); + `, + [bookInfoId], + ); return numberOfReservations[0]; }; @@ -87,7 +96,7 @@ export const create = async (userId: number, bookInfoId: number) => { } }; -export const search = async (query:string, page: number, limit: number, filter: string) => { +export const search = async (query: string, page: number, limit: number, filter: string) => { const reservationRepo = new ReservationsRepository(); const reservationList = await reservationRepo.searchReservations(query, filter, page, limit); return reservationList; @@ -100,10 +109,11 @@ export const cancel = async (reservationId: number): Promise => { try { // 올바른 예약인지 확인. const reservations: { - status: number, - bookId: string, - title: string, - }[] = await transactionExecuteQuery(` + status: number; + bookId: string; + title: string; + }[] = await transactionExecuteQuery( + ` SELECT reservation.status AS status, reservation.bookId AS bookId, @@ -112,7 +122,9 @@ export const cancel = async (reservationId: number): Promise => { LEFT JOIN book_info ON book_info.id = reservation.bookInfoId WHERE reservation.id = ? - `, [reservationId]); + `, + [reservationId], + ); if (!reservations.length) { throw new Error(errorCode.RESERVATION_NOT_EXIST); } @@ -120,15 +132,19 @@ export const cancel = async (reservationId: number): Promise => { throw new Error(errorCode.NOT_RESERVED); } // 예약 취소 ( 2번 ) 으로 status 변경 - await transactionExecuteQuery(` + await transactionExecuteQuery( + ` UPDATE reservation SET status = 2 WHERE id = ?; - `, [reservationId]); + `, + [reservationId], + ); // bookId 가 있는 사람이 취소했으면 ( 0순위 예약자 ) if (reservations[0].bookId) { // 예약자 (취소된 bookInfoId 로 예약한 사람) 중에 가장 빨리 예약한 사람 찾아서 반환 - const candidates: {id: number, slack: string}[] = await transactionExecuteQuery(` + const candidates: { id: number; slack: string }[] = await transactionExecuteQuery( + ` SELECT reservation.id AS id, user.slack AS slack @@ -143,15 +159,23 @@ export const cancel = async (reservationId: number): Promise => { ) AND reservation.status = 0 ORDER BY reservation.createdAt ASC; - `, [reservations[0].bookId]); + `, + [reservations[0].bookId], + ); // 그 사람이 존재한다면 예약 update 하고 예약 알림 보내기 if (candidates.length) { - await transactionExecuteQuery(` + await transactionExecuteQuery( + ` UPDATE reservation SET bookId = ?, endAt = DATE_ADD(NOW(), INTERVAL 3 DAY) WHERE id = ? - `, [reservations[0].bookId, candidates[0].id]); - publishMessage(candidates[0].slack, `:jiphyeonjeon: 예약 알림 :jiphyeonjeon:\n예약하신 도서 \`${reservations[0].title}\`(이)가 대출 가능합니다. 3일 내로 집현전에 방문해 대출해주세요. (방문하시기 전에 비치 여부를 확인해주세요)`); + `, + [reservations[0].bookId, candidates[0].id], + ); + publishMessage( + candidates[0].slack, + `:jiphyeonjeon: 예약 알림 :jiphyeonjeon:\n예약하신 도서 \`${reservations[0].title}\`(이)가 대출 가능합니다. 3일 내로 집현전에 방문해 대출해주세요. (방문하시기 전에 비치 여부를 확인해주세요)`, + ); } } conn.commit(); @@ -164,11 +188,14 @@ export const cancel = async (reservationId: number): Promise => { }; export const userCancel = async (userId: number, reservationId: number): Promise => { - const reservations = await executeQuery(` + const reservations = await executeQuery( + ` SELECT userId FROM reservation WHERE id = ? - `, [reservationId]); + `, + [reservationId], + ); if (!reservations.length) { throw new Error(errorCode.RESERVATION_NOT_EXIST); } @@ -192,7 +219,8 @@ export const reservationKeySubstitution = (obj: queriedReservationInfo): reserva }; export const userReservations = async (userId: number) => { - const reservationList = await executeQuery(` + const reservationList = (await executeQuery( + ` SELECT reservation.id as reservationId, reservation.bookInfoId as reservedBookInfoId, reservation.createdAt as reservationDate, @@ -208,7 +236,9 @@ export const userReservations = async (userId: number) => { LEFT JOIN book_info ON reservation.bookInfoId = book_info.id WHERE reservation.userId = ? AND reservation.status = 0; - `, [userId]) as [queriedReservationInfo]; + `, + [userId], + )) as [queriedReservationInfo]; reservationList.forEach((obj) => reservationKeySubstitution(obj)); return reservationList; }; diff --git a/backend/src/v1/reservations/reservations.type.ts b/backend/src/v1/reservations/reservations.type.ts index 3802a8be..0c9a1182 100644 --- a/backend/src/v1/reservations/reservations.type.ts +++ b/backend/src/v1/reservations/reservations.type.ts @@ -1,19 +1,19 @@ export type queriedReservationInfo = { - reservationId: number, - reservedBookInfoId: number, - reservationDate: Date, - endAt: Date, - orderOfReservation: number, - title: string, - image: string, -} + reservationId: number; + reservedBookInfoId: number; + reservationDate: Date; + endAt: Date; + orderOfReservation: number; + title: string; + image: string; +}; export type reservationInfo = { - reservationId: number, - bookInfoId: number, - createdAt: Date, - endAt: Date, - orderOfReservation: number, - title: string, - image: string, -} + reservationId: number; + bookInfoId: number; + createdAt: Date; + endAt: Date; + orderOfReservation: number; + title: string; + image: string; +}; diff --git a/backend/src/v1/reviews/controller/reviews.controller.ts b/backend/src/v1/reviews/controller/reviews.controller.ts index 11097d6a..cc8ec0f0 100644 --- a/backend/src/v1/reviews/controller/reviews.controller.ts +++ b/backend/src/v1/reviews/controller/reviews.controller.ts @@ -1,6 +1,4 @@ -import { - Request, RequestHandler, Response, -} from 'express'; +import { Request, RequestHandler, Response } from 'express'; import * as status from 'http-status'; import ErrorResponse from '~/v1/utils/error/errorResponse'; import * as errorCode from '~/v1/utils/error/errorCode'; @@ -47,21 +45,21 @@ export const getReviews: RequestHandler = async (req, res, next) => { } const { id } = parsedId.data; - const { - isMyReview, titleOrNickname, disabled, page, sort, limit, - } = parsedQuery.data; + const { isMyReview, titleOrNickname, disabled, page, sort, limit } = parsedQuery.data; return res .status(status.OK) - .json(await reviewsService.getReviewsPage( - id, - isMyReview, - titleOrNickname ?? '', - disabled, - page, - sort, - limit, - )); + .json( + await reviewsService.getReviewsPage( + id, + isMyReview, + titleOrNickname ?? '', + disabled, + page, + sort, + limit, + ), + ); }; export const updateReviews: RequestHandler = async (req, res, next) => { diff --git a/backend/src/v1/reviews/controller/reviews.type.ts b/backend/src/v1/reviews/controller/reviews.type.ts index 16ba1924..bfd91b3c 100644 --- a/backend/src/v1/reviews/controller/reviews.type.ts +++ b/backend/src/v1/reviews/controller/reviews.type.ts @@ -12,16 +12,22 @@ export const reviewsIdSchema = positiveInt; export const contentSchema = z.string().min(10).max(420); export type Sort = 'ASC' | 'DESC'; -export const sortSchema = z.string().toUpperCase() +export const sortSchema = z + .string() + .toUpperCase() .refine((s): s is Sort => s === 'ASC' || s === 'DESC') .default('DESC' as const); /** 0: 공개, 1: 비공개, -1: 전체 리뷰 */ type Disabled = 0 | 1 | -1; -const disabledSchema = z.coerce.number().int().refine( - (n): n is Disabled => [-1, 0, 1].includes(n), - (n) => ({ message: `0: 공개, 1: 비공개, -1: 전체 리뷰, 입력값: ${n}` }), -).default(-1); +const disabledSchema = z.coerce + .number() + .int() + .refine( + (n): n is Disabled => [-1, 0, 1].includes(n), + (n) => ({ message: `0: 공개, 1: 비공개, -1: 전체 리뷰, 입력값: ${n}` }), + ) + .default(-1); export const queryOptionSchema = z.object({ page: positiveInt.default(0), @@ -34,11 +40,13 @@ export const booleanLikeSchema = z.union([ z.enum(['true', 'false']).transform((v) => v === 'true'), ]); -export const getReviewsSchema = z.object({ - isMyReview: booleanLikeSchema.catch(false), - titleOrNickname: z.string().optional(), - disabled: disabledSchema, -}).merge(queryOptionSchema); +export const getReviewsSchema = z + .object({ + isMyReview: booleanLikeSchema.catch(false), + titleOrNickname: z.string().optional(), + disabled: disabledSchema, + }) + .merge(queryOptionSchema); export const createReviewsSchema = z.object({ bookInfoId: bookInfoIdSchema, diff --git a/backend/src/v1/reviews/controller/utils/errorCheck.ts b/backend/src/v1/reviews/controller/utils/errorCheck.ts index bd9c2338..0f69814e 100644 --- a/backend/src/v1/reviews/controller/utils/errorCheck.ts +++ b/backend/src/v1/reviews/controller/utils/errorCheck.ts @@ -4,9 +4,7 @@ import ReviewsService from '../../service/reviews.service'; const reviewsService = new ReviewsService(); -export const contentParseCheck = ( - content : string, -) => { +export const contentParseCheck = (content: string) => { const result = content.trim(); if (result === '' || result.length < 10 || result.length > 420) { throw new ErrorResponse(errorCode.INVALID_INPUT_REVIEWS_CONTENT, 400); @@ -14,35 +12,28 @@ export const contentParseCheck = ( return result; }; -export const reviewsIdParseCheck = ( - reviewsId : string, -) => { +export const reviewsIdParseCheck = (reviewsId: string) => { if (reviewsId.trim() === '') { throw new ErrorResponse(errorCode.INVALID_INPUT_REVIEWS_ID, 400); } try { return parseInt(reviewsId, 10); - } catch (error : any) { + } catch (error: any) { throw new ErrorResponse(errorCode.INVALID_INPUT_REVIEWS, 400); } }; -export const reviewsIdExistCheck = async ( - reviewsId : number, -) => { - let result : number; +export const reviewsIdExistCheck = async (reviewsId: number) => { + let result: number; try { result = await reviewsService.getReviewsUserId(reviewsId); - } catch (error : any) { + } catch (error: any) { throw new ErrorResponse(errorCode.NOT_FOUND_REVIEWS, 404); } return result; }; -export const idAndTokenIdSameCheck = ( - id : number, - tokenId : number, -) => { +export const idAndTokenIdSameCheck = (id: number, tokenId: number) => { if (id !== tokenId) { throw new ErrorResponse(errorCode.UNAUTHORIZED_REVIEWS, 401); } diff --git a/backend/src/v1/reviews/controller/utils/parseCheck.ts b/backend/src/v1/reviews/controller/utils/parseCheck.ts index f6da9b63..75358c12 100644 --- a/backend/src/v1/reviews/controller/utils/parseCheck.ts +++ b/backend/src/v1/reviews/controller/utils/parseCheck.ts @@ -1,28 +1,17 @@ -export const sortParse = ( - sort : any, -) : 'ASC' | 'DESC' => { +export const sortParse = (sort: any): 'ASC' | 'DESC' => { if (sort === 'asc' || sort === 'desc' || sort === 'ASC' || sort === 'DESC') { return sort.toUpperCase(); } return 'DESC'; }; -export const pageParse = ( - page : number, -) : number => (Number.isNaN(page) ? 0 : page); +export const pageParse = (page: number): number => (Number.isNaN(page) ? 0 : page); -export const limitParse = ( - limit : number, -) : number => (Number.isNaN(limit) ? 10 : limit); +export const limitParse = (limit: number): number => (Number.isNaN(limit) ? 10 : limit); -export const stringQueryParse = ( - stringQuery : any, -) : string => ((stringQuery === undefined || null) ? '' : stringQuery.trim()); +export const stringQueryParse = (stringQuery: any): string => + stringQuery === undefined || null ? '' : stringQuery.trim(); -export const booleanQueryParse = ( - booleanQuery : any, -) : boolean => (booleanQuery === 'true'); +export const booleanQueryParse = (booleanQuery: any): boolean => booleanQuery === 'true'; -export const disabledParse = ( - disabled : number, -) : number => (Number.isNaN(disabled) ? -1 : disabled); +export const disabledParse = (disabled: number): number => (Number.isNaN(disabled) ? -1 : disabled); diff --git a/backend/src/v1/reviews/repository/reviews.repository.ts b/backend/src/v1/reviews/repository/reviews.repository.ts index b113f92b..1eff0433 100644 --- a/backend/src/v1/reviews/repository/reviews.repository.ts +++ b/backend/src/v1/reviews/repository/reviews.repository.ts @@ -11,13 +11,10 @@ export default class ReviewsRepository extends Repository { const queryRunner: QueryRunner | undefined = transactionQueryRunner; const entityManager = jipDataSource.createEntityManager(queryRunner); super(Reviews, entityManager); - this.bookInfoRepo = new Repository( - BookInfo, - entityManager, - ); + this.bookInfoRepo = new Repository(BookInfo, entityManager); } - async validateBookInfo(bookInfoId: number) : Promise { + async validateBookInfo(bookInfoId: number): Promise { const bookInfoCount = await this.bookInfoRepo.count({ where: { id: bookInfoId }, }); @@ -28,14 +25,17 @@ export default class ReviewsRepository extends Repository { async createReviews(userId: number, bookInfoId: number, content: string): Promise { await this.insert({ - userId, bookInfoId, content, updateUserId: userId, + userId, + bookInfoId, + content, + updateUserId: userId, }); } async getReviewsPage( reviewerId: number, isMyReview: boolean, - titleOrNickname :string, + titleOrNickname: string, disabled: number, page: number, sort: 'ASC' | 'DESC' | undefined, @@ -58,12 +58,15 @@ export default class ReviewsRepository extends Repository { if (isMyReview === true) { reviews.andWhere({ userId: reviewerId }); } else if (!isMyReview && titleOrNickname !== '') { - reviews.andWhere(`(title LIKE '%${titleOrNickname}%' OR nickname LIKE '%${titleOrNickname}%')`); + reviews.andWhere( + `(title LIKE '%${titleOrNickname}%' OR nickname LIKE '%${titleOrNickname}%')`, + ); } if (disabled !== -1) { reviews.andWhere({ disabled }); } - const ret = await reviews.offset(page * limit) + const ret = await reviews + .offset(page * limit) .limit(limit) .getRawMany(); return ret; @@ -74,7 +77,7 @@ export default class ReviewsRepository extends Repository { isMyReview: boolean, titleOrNickname: string, disabled: number, - ) : Promise { + ): Promise { const reviews = this.createQueryBuilder('reviews') .select('COUNT(*)', 'counts') .leftJoin(User, 'user', 'user.id = reviews.userId') @@ -83,7 +86,9 @@ export default class ReviewsRepository extends Repository { if (isMyReview === true) { reviews.andWhere({ userId: reviewerId }); } else if (!isMyReview && titleOrNickname !== '') { - reviews.andWhere(`(title LIKE '%${titleOrNickname}%' OR nickname LIKE '%${titleOrNickname}%')`); + reviews.andWhere( + `(title LIKE '%${titleOrNickname}%' OR nickname LIKE '%${titleOrNickname}%')`, + ); } if (disabled !== -1) { reviews.andWhere({ disabled }); @@ -92,7 +97,7 @@ export default class ReviewsRepository extends Repository { return ret.counts; } - async getReviewsUserId(reviewsId : number): Promise { + async getReviewsUserId(reviewsId: number): Promise { const ret = await this.findOneOrFail({ select: { userId: true, @@ -105,7 +110,7 @@ export default class ReviewsRepository extends Repository { return ret.userId; } - async getReviews(reviewsId : number): Promise { + async getReviews(reviewsId: number): Promise { const ret = await this.find({ select: { userId: true, @@ -119,7 +124,7 @@ export default class ReviewsRepository extends Repository { return ret; } - async updateReviews(reviewsId : number, userId : number, content : string): Promise { + async updateReviews(reviewsId: number, userId: number, content: string): Promise { await this.update(reviewsId, { content, updateUserId: userId }); } @@ -127,13 +132,10 @@ export default class ReviewsRepository extends Repository { await this.update(reviewId, { isDeleted: true, deleteUserId: deleteUser }); } - async patchReviews(reviewsId : number, userId : number): Promise { - await this.update( - reviewsId, - { - disabled: () => 'IF(disabled=TRUE, FALSE, TRUE)', - disabledUserId: () => `IF(disabled=FALSE, NULL, ${userId})`, - }, - ); + async patchReviews(reviewsId: number, userId: number): Promise { + await this.update(reviewsId, { + disabled: () => 'IF(disabled=TRUE, FALSE, TRUE)', + disabledUserId: () => `IF(disabled=FALSE, NULL, ${userId})`, + }); } } diff --git a/backend/src/v1/reviews/service/reviews.service.ts b/backend/src/v1/reviews/service/reviews.service.ts index a914d580..b2637b9b 100644 --- a/backend/src/v1/reviews/service/reviews.service.ts +++ b/backend/src/v1/reviews/service/reviews.service.ts @@ -2,7 +2,7 @@ import * as errorCheck from './utils/errorCheck'; import ReviewsRepository from '../repository/reviews.repository'; export default class ReviewsService { - private readonly reviewsRepository : ReviewsRepository; + private readonly reviewsRepository: ReviewsRepository; constructor() { this.reviewsRepository = new ReviewsRepository(); @@ -37,12 +37,14 @@ export default class ReviewsService { titleOrNickname, disabled, ); - const itemsPerPage = (Number.isNaN(limit)) ? 10 : limit; + const itemsPerPage = Number.isNaN(limit) ? 10 : limit; const meta = { totalItems: counts, itemsPerPage, - totalPages: parseInt(String(counts / itemsPerPage - + Number((counts % itemsPerPage !== 0) || !counts)), 10), + totalPages: parseInt( + String(counts / itemsPerPage + Number(counts % itemsPerPage !== 0 || !counts)), + 10, + ), firstPage: page === 0, finalPage: page === parseInt(String(counts / itemsPerPage), 10), currentPage: page, @@ -50,18 +52,12 @@ export default class ReviewsService { return { items, meta }; } - async getReviewsUserId( - reviewsId: number, - ) { + async getReviewsUserId(reviewsId: number) { const reviewsUserId = await this.reviewsRepository.getReviewsUserId(reviewsId); return reviewsUserId; } - async updateReviews( - reviewsId: number, - userId: number, - content: string, - ) { + async updateReviews(reviewsId: number, userId: number, content: string) { const reviewsUserId = await errorCheck.updatePossibleCheck(reviewsId); errorCheck.idAndTokenIdSameCheck(reviewsUserId, userId); await this.reviewsRepository.updateReviews(reviewsId, userId, content); @@ -71,10 +67,7 @@ export default class ReviewsService { await this.reviewsRepository.deleteReviews(reviewId, deleteUser); } - async patchReviews( - reviewsId: number, - userId: number, - ) { + async patchReviews(reviewsId: number, userId: number) { await this.reviewsRepository.patchReviews(reviewsId, userId); } } diff --git a/backend/src/v1/reviews/service/utils/errorCheck.ts b/backend/src/v1/reviews/service/utils/errorCheck.ts index ab3af1f0..ad7e5e88 100644 --- a/backend/src/v1/reviews/service/utils/errorCheck.ts +++ b/backend/src/v1/reviews/service/utils/errorCheck.ts @@ -4,15 +4,13 @@ import ReviewsRepository from '../../repository/reviews.repository'; const reviewsRepository = new ReviewsRepository(); -export const updatePossibleCheck = async ( - reviewsId : number, -) => { - let result : any; - let resultId : number; +export const updatePossibleCheck = async (reviewsId: number) => { + let result: any; + let resultId: number; try { result = await reviewsRepository.getReviews(reviewsId); resultId = result[0].userId; - } catch (error : any) { + } catch (error: any) { throw new ErrorResponse(errorCode.NOT_FOUND_REVIEWS, 404); } if (result[0].disabled === 1) { @@ -21,10 +19,7 @@ export const updatePossibleCheck = async ( return resultId; }; -export const idAndTokenIdSameCheck = ( - id : number, - tokenId : number, -) => { +export const idAndTokenIdSameCheck = (id: number, tokenId: number) => { if (id !== tokenId) { throw new ErrorResponse(errorCode.UNAUTHORIZED_REVIEWS, 401); } diff --git a/backend/src/v1/routes/auth.routes.ts b/backend/src/v1/routes/auth.routes.ts index d1b81915..6fe39443 100644 --- a/backend/src/v1/routes/auth.routes.ts +++ b/backend/src/v1/routes/auth.routes.ts @@ -7,7 +7,12 @@ import { oauthUrlOption } from '~/config'; import * as errorCode from '~/v1/utils/error/errorCode'; import { getIntraAuthentication, - getMe, getOAuth, getToken, intraAuthentication, login, logout, + getMe, + getOAuth, + getToken, + intraAuthentication, + login, + logout, } from '~/v1/auth/auth.controller'; export const path = '/auth'; @@ -71,7 +76,14 @@ router.get('/oauth', getOAuth); * message: * type: string */ -router.get('/token', passport.authenticate('42', { session: false, failureRedirect: `${oauthUrlOption.clientURL}/login?errorCode=${errorCode.ACCESS_DENIED}` }), getToken); +router.get( + '/token', + passport.authenticate('42', { + session: false, + failureRedirect: `${oauthUrlOption.clientURL}/login?errorCode=${errorCode.ACCESS_DENIED}`, + }), + getToken, +); /** * @openapi @@ -312,4 +324,15 @@ router.get('/getIntraAuthentication', getIntraAuthentication); * message: * type: string */ -router.get('/intraAuthentication', passport.authenticate('42Auth', { session: false, failureRedirect: `${oauthUrlOption.clientURL}/mypage?errorCode=${errorCode.ACCESS_DENIED}` }), passport.authenticate('jwt', { session: false, failureRedirect: `${oauthUrlOption.clientURL}/logout` }), intraAuthentication); +router.get( + '/intraAuthentication', + passport.authenticate('42Auth', { + session: false, + failureRedirect: `${oauthUrlOption.clientURL}/mypage?errorCode=${errorCode.ACCESS_DENIED}`, + }), + passport.authenticate('jwt', { + session: false, + failureRedirect: `${oauthUrlOption.clientURL}/logout`, + }), + intraAuthentication, +); diff --git a/backend/src/v1/routes/bookInfoReviews.routes.ts b/backend/src/v1/routes/bookInfoReviews.routes.ts index 219d70a0..7c64b366 100644 --- a/backend/src/v1/routes/bookInfoReviews.routes.ts +++ b/backend/src/v1/routes/bookInfoReviews.routes.ts @@ -1,115 +1,113 @@ import { Router } from 'express'; import wrapAsyncController from '~/v1/middlewares/wrapAsyncController'; -import { - getBookInfoReviewsPage, -} from '~/v1/book-info-reviews/controller/bookInfoReviews.controller'; +import { getBookInfoReviewsPage } from '~/v1/book-info-reviews/controller/bookInfoReviews.controller'; export const path = '/book-info'; export const router = Router(); router -/** - * @openapi - * /api/book-info/{bookInfoId}/reviews: - * get: - * description: 책 리뷰 10개를 반환한다. 최종 페이지의 경우 1 <= n <= 10 개의 값이 반환될 수 있다. content에는 리뷰에 대한 정보를, finalPage 에는 해당 페이지가 마지막인지에 대한 여부를 boolean 값으로 반환한다. finalReviewsId는 마지막 리뷰의 Id를 반환하며, 반환할 아이디가 존재하지 않는 경우에는 해당 인자를 반환하지 않는다. - * tags: - * - bookInfo/reviews - * parameters: - * - name: bookInfoId - * required: true - * in: path - * schema: - * type: number - * description: bookInfoId에 해당 하는 리뷰 페이지를 반환한다. - * - name: reviewsId - * in: query - * schema: - * type: number - * required: false - * description: 해당 reviewsId를 조건으로 asc 기준 이후, desc 기준 이전의 페이지를 반환한다. 기본값은 첫 페이지를 반환한다. - * - name: sort - * in: query - * schema: - * type: string - * required: false - * description: asc, desc 값을 통해 시간순으로 정렬된 페이지를 반환한다. 기본값은 asd으로 한다. - * - name: limit - * in: query - * schema: - * type: number - * description: 한 페이지에서 몇 개의 게시글을 가져올 지 결정한다. [default = 10] - * responses: - * '200': - * content: - * application/json: - * schema: - * type: object - * examples: - * default(bookInfoId = 1) : - * value: - * items : [ - * { - * reviewsId : 1, - * reviewerId : 100, - * bookInfoId: 1, - * title: 클린코드, - * nickname : sechung1, - * content : hello, - * }, - * { - * reviewsId : 2, - * reviewerId : 101, - * bookInfoId: 1, - * title: 클린코드, - * nickname : sechung2, - * content : hello, - * }, - * { - * reviewsId : 3, - * reviewerId : 102, - * bookInfoId: 1, - * title: 클린코드, - * nickname : sechung3, - * content : hello, - * }, - * { - * reviewsId : 4, - * reviewerId : 103, - * bookInfoId: 1, - * title: 클린코드, - * nickname : sechung4, - * content : hello, - * }, - * { - * reviewsId : 5, - * reviewerId : 104, - * bookInfoId: 1, - * title: 클린코드, - * nickname : sechung5, - * content : hello, - * } - * ] - * "meta": { - * totalItems: 100, - * itemsPerPage : 5, - * totalPages : 20, - * finalPage : False, - * finalReviewsId : 104 - * } - * '400': - * content: - * application/json: - * schema: - * type: object - * examples: - * 적절하지 않는 reviewsId 값: - * value: - * errorCode: 800 - * 적절하지 않는 bookInfoId 값: - * value: - * errorCode: 2 - * 적절하지 않는 sort 값: - * value: - * errorCode: 2 - */ + /** + * @openapi + * /api/book-info/{bookInfoId}/reviews: + * get: + * description: 책 리뷰 10개를 반환한다. 최종 페이지의 경우 1 <= n <= 10 개의 값이 반환될 수 있다. content에는 리뷰에 대한 정보를, finalPage 에는 해당 페이지가 마지막인지에 대한 여부를 boolean 값으로 반환한다. finalReviewsId는 마지막 리뷰의 Id를 반환하며, 반환할 아이디가 존재하지 않는 경우에는 해당 인자를 반환하지 않는다. + * tags: + * - bookInfo/reviews + * parameters: + * - name: bookInfoId + * required: true + * in: path + * schema: + * type: number + * description: bookInfoId에 해당 하는 리뷰 페이지를 반환한다. + * - name: reviewsId + * in: query + * schema: + * type: number + * required: false + * description: 해당 reviewsId를 조건으로 asc 기준 이후, desc 기준 이전의 페이지를 반환한다. 기본값은 첫 페이지를 반환한다. + * - name: sort + * in: query + * schema: + * type: string + * required: false + * description: asc, desc 값을 통해 시간순으로 정렬된 페이지를 반환한다. 기본값은 asd으로 한다. + * - name: limit + * in: query + * schema: + * type: number + * description: 한 페이지에서 몇 개의 게시글을 가져올 지 결정한다. [default = 10] + * responses: + * '200': + * content: + * application/json: + * schema: + * type: object + * examples: + * default(bookInfoId = 1) : + * value: + * items : [ + * { + * reviewsId : 1, + * reviewerId : 100, + * bookInfoId: 1, + * title: 클린코드, + * nickname : sechung1, + * content : hello, + * }, + * { + * reviewsId : 2, + * reviewerId : 101, + * bookInfoId: 1, + * title: 클린코드, + * nickname : sechung2, + * content : hello, + * }, + * { + * reviewsId : 3, + * reviewerId : 102, + * bookInfoId: 1, + * title: 클린코드, + * nickname : sechung3, + * content : hello, + * }, + * { + * reviewsId : 4, + * reviewerId : 103, + * bookInfoId: 1, + * title: 클린코드, + * nickname : sechung4, + * content : hello, + * }, + * { + * reviewsId : 5, + * reviewerId : 104, + * bookInfoId: 1, + * title: 클린코드, + * nickname : sechung5, + * content : hello, + * } + * ] + * "meta": { + * totalItems: 100, + * itemsPerPage : 5, + * totalPages : 20, + * finalPage : False, + * finalReviewsId : 104 + * } + * '400': + * content: + * application/json: + * schema: + * type: object + * examples: + * 적절하지 않는 reviewsId 값: + * value: + * errorCode: 800 + * 적절하지 않는 bookInfoId 값: + * value: + * errorCode: 2 + * 적절하지 않는 sort 값: + * value: + * errorCode: 2 + */ .get('/:bookInfoId/reviews', wrapAsyncController(getBookInfoReviewsPage)); diff --git a/backend/src/v1/routes/books.routes.ts b/backend/src/v1/routes/books.routes.ts index aee0c720..ff70baaf 100644 --- a/backend/src/v1/routes/books.routes.ts +++ b/backend/src/v1/routes/books.routes.ts @@ -798,7 +798,7 @@ router .get('/create', authValidate(roleSet.librarian), createBookInfo); router -/** + /** * @openapi * /api/books/{id}: * get: @@ -871,7 +871,7 @@ router .get('/:id', getBookById); router -/** + /** * @openapi * /api/books/info/{bookInfoId}/like: * post: @@ -920,7 +920,7 @@ router .post('/info/:bookInfoId/like', authValidate(roleSet.service), createLike); router -/** + /** * @openapi * /api/books/info/{bookInfoId}/like: * delete: @@ -961,7 +961,7 @@ router .delete('/info/:bookInfoId/like', authValidate(roleSet.service), deleteLike); router -/** + /** * @openapi * /api/books/info/{bookInfoId}/like: * get: @@ -1007,98 +1007,98 @@ router .get('/info/:bookInfoId/like', authValidateDefaultNullUser(roleSet.all), getLikeInfo); router -/** - * @openapi - * /api/books/update: - * patch: - * description: 책 정보를 수정합니다. book_info table or book table - * tags: - * - books - * requestBody: - * content: - * application/json: - * schema: - * type: object - * properties: - * bookInfoId: - * description: bookInfoId - * type: integer - * nullable: false - * example: 1 - * categoryId: - * description: categoryId - * type: integer - * nullable: false - * example: 1 - * title: - * description: 제목 - * type: string - * nullable: true - * example: "작별인사 (김영하 장편소설)" - * author: - * description: 저자 - * type: string - * nullable: true - * example: "김영하" - * publisher: - * description: 출판사 - * type: string - * nullable: true - * example: "복복서가" - * publishedAt: - * description: 출판연월 - * type: string - * nullable: true - * example: "20200505" - * image: - * description: 표지이미지 - * type: string - * nullable: true - * example: "https://bookthumb-phinf.pstatic.net/cover/223/538/22353804.jpg?type=m1&udate=20220608" - * bookId: - * description: bookId - * type: integer - * nullable: false - * example: 1 - * callSign: - * description: 청구기호 - * type: string - * nullable: true - * example: h1.18.v1.c1 - * status: - * description: 도서 상태 - * type: integer - * nullable: false - * example: 0 - * responses: - * '204': - * description: 성공했을 때 http 상태코드 204(NO_CONTENT) 값을 반환. - * content: - * application: - * schema: - * type: - * description: 성공했을 때 http 상태코드 204 값을 반환. - * '실패 케이스 1': - * description: 예상치 못한 에러로 책 정보 patch에 실패. - * content: - * application/json: - * schema: - * type: json - * example : { errorCode: 312 } - * '실패 케이스 2': - * description: 수정할 DATA가 적어도 한 개는 필요. 수정할 DATA가 없음" - * content: - * application/json: - * schema: - * type: json - * example : { errorCode: 313 } - * '실패 케이스 3': - * description: 입력한 publishedAt filed가 알맞은 형식이 아님. 기대하는 형식 "20220807" - * content: - * application/json: - * schema: - * type: json - * example : { errorCode: 311 } - */ + /** + * @openapi + * /api/books/update: + * patch: + * description: 책 정보를 수정합니다. book_info table or book table + * tags: + * - books + * requestBody: + * content: + * application/json: + * schema: + * type: object + * properties: + * bookInfoId: + * description: bookInfoId + * type: integer + * nullable: false + * example: 1 + * categoryId: + * description: categoryId + * type: integer + * nullable: false + * example: 1 + * title: + * description: 제목 + * type: string + * nullable: true + * example: "작별인사 (김영하 장편소설)" + * author: + * description: 저자 + * type: string + * nullable: true + * example: "김영하" + * publisher: + * description: 출판사 + * type: string + * nullable: true + * example: "복복서가" + * publishedAt: + * description: 출판연월 + * type: string + * nullable: true + * example: "20200505" + * image: + * description: 표지이미지 + * type: string + * nullable: true + * example: "https://bookthumb-phinf.pstatic.net/cover/223/538/22353804.jpg?type=m1&udate=20220608" + * bookId: + * description: bookId + * type: integer + * nullable: false + * example: 1 + * callSign: + * description: 청구기호 + * type: string + * nullable: true + * example: h1.18.v1.c1 + * status: + * description: 도서 상태 + * type: integer + * nullable: false + * example: 0 + * responses: + * '204': + * description: 성공했을 때 http 상태코드 204(NO_CONTENT) 값을 반환. + * content: + * application: + * schema: + * type: + * description: 성공했을 때 http 상태코드 204 값을 반환. + * '실패 케이스 1': + * description: 예상치 못한 에러로 책 정보 patch에 실패. + * content: + * application/json: + * schema: + * type: json + * example : { errorCode: 312 } + * '실패 케이스 2': + * description: 수정할 DATA가 적어도 한 개는 필요. 수정할 DATA가 없음" + * content: + * application/json: + * schema: + * type: json + * example : { errorCode: 313 } + * '실패 케이스 3': + * description: 입력한 publishedAt filed가 알맞은 형식이 아님. 기대하는 형식 "20220807" + * content: + * application/json: + * schema: + * type: json + * example : { errorCode: 311 } + */ .patch('/update', authValidate(roleSet.librarian), updateBookInfo) .patch('/donator', authValidate(roleSet.librarian), updateBookDonator); diff --git a/backend/src/v1/routes/cursus.routes.ts b/backend/src/v1/routes/cursus.routes.ts index 45e524de..b0d0c737 100644 --- a/backend/src/v1/routes/cursus.routes.ts +++ b/backend/src/v1/routes/cursus.routes.ts @@ -97,106 +97,106 @@ router .get('/recommend/books', limiter, authValidate(roleSet.all), recommendBook); router -/** - * @openapi - * /api/cursus/projects: - * get: - * summary: 42 API를 통해 cursus의 프로젝트 정보를 가져온다. - * description: 42 API를 통해 cursus의 프로젝트를 정보를 가져와서 json으로 저장한다. - * tags: - * - cursus - * parameters: - * - name: page - * in: query - * description: 프로젝트 정보를 가져올 페이지 번호 - * required: true - * schema: - * type: integer - * example: 1 - * default: 1 - * - name: mode - * in: query - * description: 프로젝트 정보를 가져올 모드. append면 기존에 저장된 정보에 추가로 저장하고, overwrite면 기존에 저장된 정보를 덮어쓴다. - * required: true - * schema: - * type: string - * enum: [append, overwrite] - * example: overwrite - * responses: - * '200': - * description: 프로젝트 정보를 성공적으로 가져옴. - * content: - * application/json: - * schema: - * type: object - * example: { - * projects: [ - * { - * id: 1, - * name: "Libft", - * slug: "libft", - * parent: null, - * cursus: [ - * { - * id: 1, - * name: "42", - * slug: "42" - * }, - * { - * id: 8, - * name: "WeThinkCode_", - * slug: "wethinkcode_" - * }, - * { - * id: 10, - * name: "Formation Pole Emploi", - * slug: "formation-pole-emploi" - * } - * ] - * }, - * { - * id: 2, - * name: "GET_Next_Line", - * slug: "get_next_line", - * parent: null, - * cursus: [ - * { - * id: 1, - * name: "42", - * slug: "42" - * }, - * { - * id: 8, - * name: "WeThinkCode_", - * slug: "wethinkcode_" - * }, - * { - * id: 10, - * name: "Formation Pole Emploi", - * slug: "formation-pole-emploi" - * }, - * { - * id: 18, - * name: "Starfleet", - * slug: "starfleet" - * } - * ] - * } - * ] - * } - * '400': - * description: 잘못된 요청 URL입니다. - * content: - * application/json: - * schema: - * type: json - * example: {errorCode: 400} - * '401': - * description: 토큰이 유효하지 않습니다. - * content: - * application/json: - * schema: - * type: json - * example: {errorCode: 401} - */ + /** + * @openapi + * /api/cursus/projects: + * get: + * summary: 42 API를 통해 cursus의 프로젝트 정보를 가져온다. + * description: 42 API를 통해 cursus의 프로젝트를 정보를 가져와서 json으로 저장한다. + * tags: + * - cursus + * parameters: + * - name: page + * in: query + * description: 프로젝트 정보를 가져올 페이지 번호 + * required: true + * schema: + * type: integer + * example: 1 + * default: 1 + * - name: mode + * in: query + * description: 프로젝트 정보를 가져올 모드. append면 기존에 저장된 정보에 추가로 저장하고, overwrite면 기존에 저장된 정보를 덮어쓴다. + * required: true + * schema: + * type: string + * enum: [append, overwrite] + * example: overwrite + * responses: + * '200': + * description: 프로젝트 정보를 성공적으로 가져옴. + * content: + * application/json: + * schema: + * type: object + * example: { + * projects: [ + * { + * id: 1, + * name: "Libft", + * slug: "libft", + * parent: null, + * cursus: [ + * { + * id: 1, + * name: "42", + * slug: "42" + * }, + * { + * id: 8, + * name: "WeThinkCode_", + * slug: "wethinkcode_" + * }, + * { + * id: 10, + * name: "Formation Pole Emploi", + * slug: "formation-pole-emploi" + * } + * ] + * }, + * { + * id: 2, + * name: "GET_Next_Line", + * slug: "get_next_line", + * parent: null, + * cursus: [ + * { + * id: 1, + * name: "42", + * slug: "42" + * }, + * { + * id: 8, + * name: "WeThinkCode_", + * slug: "wethinkcode_" + * }, + * { + * id: 10, + * name: "Formation Pole Emploi", + * slug: "formation-pole-emploi" + * }, + * { + * id: 18, + * name: "Starfleet", + * slug: "starfleet" + * } + * ] + * } + * ] + * } + * '400': + * description: 잘못된 요청 URL입니다. + * content: + * application/json: + * schema: + * type: json + * example: {errorCode: 400} + * '401': + * description: 토큰이 유효하지 않습니다. + * content: + * application/json: + * schema: + * type: json + * example: {errorCode: 401} + */ .get('/projects', getProjects); diff --git a/backend/src/v1/routes/histories.routes.ts b/backend/src/v1/routes/histories.routes.ts index cc3b8e8c..8a58ba7c 100644 --- a/backend/src/v1/routes/histories.routes.ts +++ b/backend/src/v1/routes/histories.routes.ts @@ -1,7 +1,5 @@ import { Router } from 'express'; -import { - histories, -} from '~/v1/histories/histories.controller'; +import { histories } from '~/v1/histories/histories.controller'; import authValidate from '~/v1/auth/auth.validate'; import { roleSet } from '~/v1/auth/auth.type'; diff --git a/backend/src/v1/routes/lendings.routes.ts b/backend/src/v1/routes/lendings.routes.ts index 3ad119c9..1e15965b 100644 --- a/backend/src/v1/routes/lendings.routes.ts +++ b/backend/src/v1/routes/lendings.routes.ts @@ -1,7 +1,5 @@ import { Router } from 'express'; -import { - create, search, lendingId, returnBook, -} from '~/v1/lendings/lendings.controller'; +import { create, search, lendingId, returnBook } from '~/v1/lendings/lendings.controller'; import authValidate from '~/v1/auth/auth.validate'; import { roleSet } from '~/v1/auth/auth.type'; @@ -9,313 +7,313 @@ export const path = '/lendings'; export const router = Router(); router -/** - * @openapi - * /api/lendings: - * post: - * tags: - * - lendings - * summary: 대출 기록 생성 - * description: 대출 기록을 생성한다. - * requestBody: - * description: bookId와 userId는 각각 대출할 도서와 대출할 회원의 pk, condition은 대출 당시 책 상태를 의미한다. - * content: - * application/json: - * schema: - * type: object - * properties: - * bookId: - * type: integer - * example: 33 - * userId: - * type: integer - * example: 45 - * condition: - * type: string - * example: "이상 없음" - * required: - * - bookId - * - userId - * - condition - * responses: - * '200': - * description: 생성된 대출기록의 반납일자를 반환. - * content: - * application/json: - * schema: - * type: object - * properties: - * dueDate: - * type: date | string - * example: 2022-12-12 - * '400': - * description: 잘못된 요청. 잘못 입력된 json key, 유효하지 않은 value 등 - * content: - * application/json: - * schema: - * type: object - * properties: - * errorCode: - * type: integer - * example: 2 - * '401': - * description: 대출을 생성할 권한이 없는 사용자 - * '500': - * description: db 에러 - * */ + /** + * @openapi + * /api/lendings: + * post: + * tags: + * - lendings + * summary: 대출 기록 생성 + * description: 대출 기록을 생성한다. + * requestBody: + * description: bookId와 userId는 각각 대출할 도서와 대출할 회원의 pk, condition은 대출 당시 책 상태를 의미한다. + * content: + * application/json: + * schema: + * type: object + * properties: + * bookId: + * type: integer + * example: 33 + * userId: + * type: integer + * example: 45 + * condition: + * type: string + * example: "이상 없음" + * required: + * - bookId + * - userId + * - condition + * responses: + * '200': + * description: 생성된 대출기록의 반납일자를 반환. + * content: + * application/json: + * schema: + * type: object + * properties: + * dueDate: + * type: date | string + * example: 2022-12-12 + * '400': + * description: 잘못된 요청. 잘못 입력된 json key, 유효하지 않은 value 등 + * content: + * application/json: + * schema: + * type: object + * properties: + * errorCode: + * type: integer + * example: 2 + * '401': + * description: 대출을 생성할 권한이 없는 사용자 + * '500': + * description: db 에러 + * */ .post('/', authValidate(roleSet.librarian), create) -/** - * @openapi - * /api/lendings/search: - * get: - * tags: - * - lendings - * summary: 대출 기록 정보 조회 - * description: 대출 기록의 정보를 검색하여 보여준다. - * parameters: - * - name: page - * in: query - * description: 검색 결과의 페이지 - * schema: - * type: integer - * default: 1 - * example: 3 - * - name: limit - * in: query - * description: 검색 결과 한 페이지당 보여줄 결과물의 개수 - * schema: - * type: integer - * default: 5 - * example: 3 - * - name: sort - * in: query - * description: 검색 결과를 정렬할 기준 - * schema: - * type: string - * enum: [new, old] - * default: new - * - name: query - * in: query - * description: 대출 기록에서 검색할 단어, 검색 가능한 필드 [user, title, callSign, bookId] - * schema: - * type: string - * example: 파이썬 - * - name: type - * in: query - * description: query를 조회할 항목 - * schema: - * type: string - * enum: [user, title, callSign, bookId] - * responses: - * '200': - * description: 대출 기록을 반환한다. - * content: - * application/json: - * schema: - * type: object - * properties: - * items: - * description: 검색된 책들의 목록 - * type: array - * items: - * type: object - * properties: - * id: - * description: 대출 고유 id - * type: integer - * condition: - * description: 대출 당시 책 상태 - * type: string - * login: - * description: 대출한 카뎃의 인트라 id - * type: string - * penaltyDays: - * description: 현재 대출 기록의 연체 일수 - * type: integer - * callSign: - * description: 대출된 책의 청구기호 - * type: string - * title: - * description: 대출된 책의 제목 - * type: string - * createdAt: - * type: string - * format: date - * dueDate: - * description: 반납기한 - * type: string - * format: date - * example: - * - id: 2 - * condition: 양호 - * login: minkykim - * penaltyDays: 0 - * callSign: O40.15.v1.c1 - * title: "소프트웨어 장인(로버트 C. 마틴 시리즈)" - * dueDate: 2021.09.20 - * - id: 42 - * condition: 이상없음 - * login: jwoo - * penaltyDays: 2 - * callSign: H19.19.v1.c1 - * title: "클린 아키텍처: 소프트웨어 구조와 설계의 원칙" - * dueDate: 2022.06.07 - * meta: - * description: 대출 조회 결과에 대한 요약 정보 - * type: object - * properties: - * totalItems: - * description: 전체 대출 검색 결과 건수 - * type: integer - * example: 2 - * itemCount: - * description: 현재 페이지 검색 결과 수 - * type: integer - * example: 2 - * itemsPerPage: - * description: 페이지 당 검색 결과 수 - * type: integer - * example: 2 - * totalPages: - * description: 전체 결과 페이지 수 - * type: integer - * example: 1 - * currentPage: - * description: 현재 페이지 - * type: integer - * example: 1 - * '400': - * description: 잘못된 요청. 잘못 입력된 json key, 유효하지 않은 value 등 - * '401': - * description: 대출을 조회할 권한이 없는 사용자 - * '500': - * description: db 에러 - */ + /** + * @openapi + * /api/lendings/search: + * get: + * tags: + * - lendings + * summary: 대출 기록 정보 조회 + * description: 대출 기록의 정보를 검색하여 보여준다. + * parameters: + * - name: page + * in: query + * description: 검색 결과의 페이지 + * schema: + * type: integer + * default: 1 + * example: 3 + * - name: limit + * in: query + * description: 검색 결과 한 페이지당 보여줄 결과물의 개수 + * schema: + * type: integer + * default: 5 + * example: 3 + * - name: sort + * in: query + * description: 검색 결과를 정렬할 기준 + * schema: + * type: string + * enum: [new, old] + * default: new + * - name: query + * in: query + * description: 대출 기록에서 검색할 단어, 검색 가능한 필드 [user, title, callSign, bookId] + * schema: + * type: string + * example: 파이썬 + * - name: type + * in: query + * description: query를 조회할 항목 + * schema: + * type: string + * enum: [user, title, callSign, bookId] + * responses: + * '200': + * description: 대출 기록을 반환한다. + * content: + * application/json: + * schema: + * type: object + * properties: + * items: + * description: 검색된 책들의 목록 + * type: array + * items: + * type: object + * properties: + * id: + * description: 대출 고유 id + * type: integer + * condition: + * description: 대출 당시 책 상태 + * type: string + * login: + * description: 대출한 카뎃의 인트라 id + * type: string + * penaltyDays: + * description: 현재 대출 기록의 연체 일수 + * type: integer + * callSign: + * description: 대출된 책의 청구기호 + * type: string + * title: + * description: 대출된 책의 제목 + * type: string + * createdAt: + * type: string + * format: date + * dueDate: + * description: 반납기한 + * type: string + * format: date + * example: + * - id: 2 + * condition: 양호 + * login: minkykim + * penaltyDays: 0 + * callSign: O40.15.v1.c1 + * title: "소프트웨어 장인(로버트 C. 마틴 시리즈)" + * dueDate: 2021.09.20 + * - id: 42 + * condition: 이상없음 + * login: jwoo + * penaltyDays: 2 + * callSign: H19.19.v1.c1 + * title: "클린 아키텍처: 소프트웨어 구조와 설계의 원칙" + * dueDate: 2022.06.07 + * meta: + * description: 대출 조회 결과에 대한 요약 정보 + * type: object + * properties: + * totalItems: + * description: 전체 대출 검색 결과 건수 + * type: integer + * example: 2 + * itemCount: + * description: 현재 페이지 검색 결과 수 + * type: integer + * example: 2 + * itemsPerPage: + * description: 페이지 당 검색 결과 수 + * type: integer + * example: 2 + * totalPages: + * description: 전체 결과 페이지 수 + * type: integer + * example: 1 + * currentPage: + * description: 현재 페이지 + * type: integer + * example: 1 + * '400': + * description: 잘못된 요청. 잘못 입력된 json key, 유효하지 않은 value 등 + * '401': + * description: 대출을 조회할 권한이 없는 사용자 + * '500': + * description: db 에러 + */ .get('/search', authValidate(roleSet.librarian), search) -/** - * @openapi - * /api/lendings/{lendingId}: - * get: - * tags: - * - lendings - * summary: 특정 대출 기록 조회 - * description: 특정 대출 기록의 상세 정보를 보여준다. - * parameters: - * - name: lendingId - * in: path - * description: 대출 기록의 고유 아이디 - * required: true - * schema: - * type: integer - * responses: - * '200': - * description: 대출 기록을 반환한다. - * content: - * application/json: - * schema: - * type: object - * properties: - * id: - * description: 대출 고유 id - * type: integer - * example: 2 - * condition: - * description: 대출 당시 책 상태 - * type: string - * example: 양호 - * createdAt: - * description: 대출 일자(대출 레코드 생성 일자) - * type: string - * format: date - * example: 2021.09.06. - * login: - * description: 대출한 카뎃의 인트라 id - * type: string - * example: minkykim - * penaltyDays: - * description: 현재 대출 기록의 연체 일수 - * type: integer - * example: 2 - * callSign: - * description: 대출된 책의 청구기호 - * type: string - * example: H1.13.v1.c1 - * title: - * description: 대출된 책의 제목 - * type: string - * example: 소프트웨어 장인(로버트 C. 마틴 시리즈) - * image: - * description: 대출된 책의 표지 - * type: string - * example: https://search1.kakaocdn.net/thumb/R120x174.q85/?fname=http%3A%2F%2Ft1.daumcdn.net%2Flbook%2Fimage%2F1633934%3Ftimestamp%3D20210706193409 - * dueDate: - * description: 반납기한 - * type: string - * format: date - * example: 2021.09.20 - * '400': - * description: 잘못된 요청. 잘못 입력된 json key, 유효하지 않은 lendingId 등 - * '401': - * description: 대출을 조회할 권한이 없는 사용자 - * '500': - * description: db 에러 - */ + /** + * @openapi + * /api/lendings/{lendingId}: + * get: + * tags: + * - lendings + * summary: 특정 대출 기록 조회 + * description: 특정 대출 기록의 상세 정보를 보여준다. + * parameters: + * - name: lendingId + * in: path + * description: 대출 기록의 고유 아이디 + * required: true + * schema: + * type: integer + * responses: + * '200': + * description: 대출 기록을 반환한다. + * content: + * application/json: + * schema: + * type: object + * properties: + * id: + * description: 대출 고유 id + * type: integer + * example: 2 + * condition: + * description: 대출 당시 책 상태 + * type: string + * example: 양호 + * createdAt: + * description: 대출 일자(대출 레코드 생성 일자) + * type: string + * format: date + * example: 2021.09.06. + * login: + * description: 대출한 카뎃의 인트라 id + * type: string + * example: minkykim + * penaltyDays: + * description: 현재 대출 기록의 연체 일수 + * type: integer + * example: 2 + * callSign: + * description: 대출된 책의 청구기호 + * type: string + * example: H1.13.v1.c1 + * title: + * description: 대출된 책의 제목 + * type: string + * example: 소프트웨어 장인(로버트 C. 마틴 시리즈) + * image: + * description: 대출된 책의 표지 + * type: string + * example: https://search1.kakaocdn.net/thumb/R120x174.q85/?fname=http%3A%2F%2Ft1.daumcdn.net%2Flbook%2Fimage%2F1633934%3Ftimestamp%3D20210706193409 + * dueDate: + * description: 반납기한 + * type: string + * format: date + * example: 2021.09.20 + * '400': + * description: 잘못된 요청. 잘못 입력된 json key, 유효하지 않은 lendingId 등 + * '401': + * description: 대출을 조회할 권한이 없는 사용자 + * '500': + * description: db 에러 + */ .get('/:id', authValidate(roleSet.librarian), lendingId) -/** - * @openapi - * /api/lendings/return: - * patch: - * tags: - * - lendings - * summary: 반납 처리 - * description: 대출 레코드에 반납 처리를 한다. - * requestBody: - * description: lendingId는 대출 고유 아이디, condition은 반납 당시 책 상태 - * content: - * application/json: - * schema: - * type: object - * properties: - * lendingId: - * type: integer - * condition: - * type: string - * required: - * - lendingId - * - condition - * responses: - * '200': - * description: 반납처리 완료, 반납된 책이 예약이 되어있는지 알려줌 - * content: - * application/json: - * schema: - * type: object - * properties: - * reservedBook: - * description: 반납된 책이 예약이 되어있는지 알려줌 - * type: boolean - * example: true - * '400': - * description: 에러코드 0 dto에러 잘못된 json key, 1 db 에러 알 수 없는 lending id 등 - * content: - * application/json: - * schema: - * type: object - * properties: - * errorCode: - * type: integer - * '401': - * description: 알 수 없는 사용자 0 로그인 안 된 유저 1 사서권한없음 - * content: - * application/json: - * schema: - * type: object - * properties: - * errorCode: - * type: integer - * */ + /** + * @openapi + * /api/lendings/return: + * patch: + * tags: + * - lendings + * summary: 반납 처리 + * description: 대출 레코드에 반납 처리를 한다. + * requestBody: + * description: lendingId는 대출 고유 아이디, condition은 반납 당시 책 상태 + * content: + * application/json: + * schema: + * type: object + * properties: + * lendingId: + * type: integer + * condition: + * type: string + * required: + * - lendingId + * - condition + * responses: + * '200': + * description: 반납처리 완료, 반납된 책이 예약이 되어있는지 알려줌 + * content: + * application/json: + * schema: + * type: object + * properties: + * reservedBook: + * description: 반납된 책이 예약이 되어있는지 알려줌 + * type: boolean + * example: true + * '400': + * description: 에러코드 0 dto에러 잘못된 json key, 1 db 에러 알 수 없는 lending id 등 + * content: + * application/json: + * schema: + * type: object + * properties: + * errorCode: + * type: integer + * '401': + * description: 알 수 없는 사용자 0 로그인 안 된 유저 1 사서권한없음 + * content: + * application/json: + * schema: + * type: object + * properties: + * errorCode: + * type: integer + * */ .patch('/return', authValidate(roleSet.librarian), returnBook); diff --git a/backend/src/v1/routes/reservations.routes.ts b/backend/src/v1/routes/reservations.routes.ts index 8d83ebd5..e26d259a 100644 --- a/backend/src/v1/routes/reservations.routes.ts +++ b/backend/src/v1/routes/reservations.routes.ts @@ -1,6 +1,10 @@ import { Router } from 'express'; import { - cancel, create, search, count, userReservations, + cancel, + create, + search, + count, + userReservations, } from '~/v1/reservations/reservations.controller'; import authValidate from '~/v1/auth/auth.validate'; import { roleSet } from '~/v1/auth/auth.type'; @@ -88,7 +92,7 @@ export const router = Router(); * description: * type: integer * example: 2 -* '400_case2': + * '400_case2': * description: 예약에 실패한 경우 * content: * application/json: diff --git a/backend/src/v1/routes/reviews.routes.ts b/backend/src/v1/routes/reviews.routes.ts index e4ef1030..1c06f306 100644 --- a/backend/src/v1/routes/reviews.routes.ts +++ b/backend/src/v1/routes/reviews.routes.ts @@ -1,6 +1,10 @@ import { Router } from 'express'; import { - createReviews, updateReviews, getReviews, deleteReviews, patchReviews, + createReviews, + updateReviews, + getReviews, + deleteReviews, + patchReviews, } from '~/v1/reviews/controller/reviews.controller'; import authValidate from '~/v1/auth/auth.validate'; import { roleSet } from '~/v1/auth/auth.type'; @@ -11,610 +15,610 @@ export const router = Router(); router /** - * @openapi - * /api/reviews: - * post: - * description: 책 리뷰를 작성한다. content 길이는 10글자 이상 420글자 이하로 입력하여야 한다. - * tags: - * - reviews - * requestBody: - * required: true - * content: - * application/json: - * schema: - * type: object - * properties: - * bookInfoId: - * type: number - * nullable: false - * required: true - * example: 42 - * content: - * type: string - * nullable: false - * required: true - * example: "책이 좋네요 열글자." - * responses: - * '201': - * description: 리뷰가 DB에 정상적으로 insert됨. - * '400': - * description: 잘못된 요청. - * content: - * application/json: - * schema: - * type: object - * examples: - * 유효하지 않은 content 길이 : - * value: - * errorCode: 801 - * '401': - * description: 권한 없음. - * content: - * application/json: - * schema: - * type: object - * examples: - * 토큰 누락 : - * value: - * errorCode: 100 - * 토큰 유저 존재하지 않음 : - * value : - * errorCode: 101 - * 토큰 만료 : - * value : - * errorCode: 108 - * 토큰 유효하지 않음 : - * value : - * errorCode: 109 - */ + * @openapi + * /api/reviews: + * post: + * description: 책 리뷰를 작성한다. content 길이는 10글자 이상 420글자 이하로 입력하여야 한다. + * tags: + * - reviews + * requestBody: + * required: true + * content: + * application/json: + * schema: + * type: object + * properties: + * bookInfoId: + * type: number + * nullable: false + * required: true + * example: 42 + * content: + * type: string + * nullable: false + * required: true + * example: "책이 좋네요 열글자." + * responses: + * '201': + * description: 리뷰가 DB에 정상적으로 insert됨. + * '400': + * description: 잘못된 요청. + * content: + * application/json: + * schema: + * type: object + * examples: + * 유효하지 않은 content 길이 : + * value: + * errorCode: 801 + * '401': + * description: 권한 없음. + * content: + * application/json: + * schema: + * type: object + * examples: + * 토큰 누락 : + * value: + * errorCode: 100 + * 토큰 유저 존재하지 않음 : + * value : + * errorCode: 101 + * 토큰 만료 : + * value : + * errorCode: 108 + * 토큰 유효하지 않음 : + * value : + * errorCode: 109 + */ .post('/', authValidate(roleSet.all), wrapAsyncController(createReviews)); router -/** - * @openapi - * /api/reviews: - * get: - * description: 책 리뷰 10개를 반환한다. 최종 페이지의 경우 1 <= n <= 10 개의 값이 반환될 수 있다. content에는 리뷰에 대한 정보를, - * finalPage 에는 해당 페이지가 마지막인지에 대한 여부를 boolean 값으로 반환한다. - * tags: - * - reviews - * parameters: - * - name: titleOrNickname - * in: query - * description: 책 제목 또는 닉네임을 검색어로 받는다. - * schema: - * type: string - * - name: page - * in: query - * schema: - * type: number - * description: 해당하는 페이지를 보여준다. - * required: false - * - name: disabled - * in: query - * description: 0이라면 공개 리뷰를, 1이라면 비공개 리뷰를, -1이라면 모든 리뷰를 가져온다. - * required: true - * schema: - * type: number - * - name: limit - * in: query - * description: 한 페이지에서 몇 개의 게시글을 가져올 지 결정한다. [default = 10] - * required: false - * schema: - * type: number - * - name: sort - * in: query - * description: asc, desc 값을 통해 시간순으로 정렬된 페이지를 반환한다. - * required: false - * schema: - * type: string - * responses: - * '200': - * content: - * application/json: - * schema: - * type: object - * examples: - * bookInfo 기준 : - * value: - * items : [ - * { - * reviewsId : 1, - * reviewerId : 100, - * bookInfoId: 1, - * content : "hello", - * createdAt: "2022-11-09T06:56:15.640Z", - * title: "클린코드", - * nickname : "sechung", - * intraId: "default@student.42seoul.kr", - * }, - * { - * reviewsId : 2, - * reviewerId : 101, - * bookInfoId: 1, - * content : "hello", - * createdAt: "2022-11-09T06:56:15.640Z", - * title: "클린코드", - * nickname : "chanheki", - * intraId: "default@student.42seoul.kr", - * }, - * { - * reviewsId : 3, - * reviewerId : 102, - * bookInfoId: 1, - * content : "hello", - * createdAt: "2022-11-09T06:56:15.640Z", - * title: "클린코드", - * nickname : "chanheki", - * intraId: "default@student.42seoul.kr", - * }, - * { - * reviewsId : 4, - * reviewerId : 103, - * bookInfoId: 1, - * content : "hello", - * createdAt: "2022-11-09T06:56:15.640Z", - * title: "클린코드", - * nickname : "chanheki", - * intraId: "default@student.42seoul.kr", - * }, - * { - * reviewsId : 5, - * reviewerId : 104, - * bookInfoId: 1, - * content : "hello", - * createdAt: "2022-11-09T06:56:15.640Z", - * title: "클린코드", - * nickname : "chanheki", - * intraId: "default@student.42seoul.kr", - * }, - * { - * reviewsId : 6, - * reviewerId : 105, - * bookInfoId: 1, - * content : "hello", - * createdAt: "2022-11-09T06:56:15.640Z", - * title: "클린코드", - * nickname : "chanheki", - * intraId: "default@student.42seoul.kr", - * }, - * { - * reviewsId : 7, - * reviewerId : 106, - * bookInfoId: 1, - * content : "hello", - * createdAt: "2022-11-09T06:56:15.640Z", - * title: "클린코드", - * nickname : "chanheki", - * intraId: "default@student.42seoul.kr", - * }, - * { - * reviewsId : 8, - * reviewerId : 107, - * bookInfoId: 1, - * content : "hello", - * createdAt: "2022-11-09T06:56:15.640Z", - * title: "클린코드", - * nickname : "chanheki", - * intraId: "default@student.42seoul.kr", - * }, - * { - * reviewsId : 9, - * reviewerId : 108, - * bookInfoId: 1, - * content : "hello", - * createdAt: "2022-11-09T06:56:15.640Z", - * title: "클린코드", - * nickname : "chanheki", - * intraId: "default@student.42seoul.kr", - * }, - * { - * reviewsId : 10, - * reviewerId : 109, - * bookInfoId: 1, - * content : "hello", - * createdAt: "2022-11-09T06:56:15.640Z", - * title: "클린코드", - * nickname : "chanheki", - * intraId: "default@student.42seoul.kr", - * }, - * ] - * "meta": { - * totalItems: 100, - * itemCount : 10, - * itemsPerPage : 10, - * totalPages : 20, - * currentPage : 1, - * finalPage : False - * } - * '400': - * content: - * application/json: - * schema: - * type: object - * examples: - * 적절하지 않는 bookInfoId 값: - * value: - * errorCode: 2 - * 적절하지 않는 userId 값: - * value: - * errorCode: 2 - * 적절하지 않는 page 값: - * value: - * errorCode: 2 - * 적절하지 않는 sort 값: - * value: - * errorCode: 2 - * '401': - * description: 권한 없음. - * content: - * application/json: - * schema: - * type: object - * examples: - * 토큰 누락 : - * value: - * errorCode: 100 - * 사서 권한 없음 : - * value: - * errorCode: 100 - * 토큰 유저 존재하지 않음 : - * value : - * errorCode: 101 - * 토큰 만료 : - * value : - * errorCode: 108 - * 토큰 유효하지 않음 : - * value : - * errorCode: 109 - */ + /** + * @openapi + * /api/reviews: + * get: + * description: 책 리뷰 10개를 반환한다. 최종 페이지의 경우 1 <= n <= 10 개의 값이 반환될 수 있다. content에는 리뷰에 대한 정보를, + * finalPage 에는 해당 페이지가 마지막인지에 대한 여부를 boolean 값으로 반환한다. + * tags: + * - reviews + * parameters: + * - name: titleOrNickname + * in: query + * description: 책 제목 또는 닉네임을 검색어로 받는다. + * schema: + * type: string + * - name: page + * in: query + * schema: + * type: number + * description: 해당하는 페이지를 보여준다. + * required: false + * - name: disabled + * in: query + * description: 0이라면 공개 리뷰를, 1이라면 비공개 리뷰를, -1이라면 모든 리뷰를 가져온다. + * required: true + * schema: + * type: number + * - name: limit + * in: query + * description: 한 페이지에서 몇 개의 게시글을 가져올 지 결정한다. [default = 10] + * required: false + * schema: + * type: number + * - name: sort + * in: query + * description: asc, desc 값을 통해 시간순으로 정렬된 페이지를 반환한다. + * required: false + * schema: + * type: string + * responses: + * '200': + * content: + * application/json: + * schema: + * type: object + * examples: + * bookInfo 기준 : + * value: + * items : [ + * { + * reviewsId : 1, + * reviewerId : 100, + * bookInfoId: 1, + * content : "hello", + * createdAt: "2022-11-09T06:56:15.640Z", + * title: "클린코드", + * nickname : "sechung", + * intraId: "default@student.42seoul.kr", + * }, + * { + * reviewsId : 2, + * reviewerId : 101, + * bookInfoId: 1, + * content : "hello", + * createdAt: "2022-11-09T06:56:15.640Z", + * title: "클린코드", + * nickname : "chanheki", + * intraId: "default@student.42seoul.kr", + * }, + * { + * reviewsId : 3, + * reviewerId : 102, + * bookInfoId: 1, + * content : "hello", + * createdAt: "2022-11-09T06:56:15.640Z", + * title: "클린코드", + * nickname : "chanheki", + * intraId: "default@student.42seoul.kr", + * }, + * { + * reviewsId : 4, + * reviewerId : 103, + * bookInfoId: 1, + * content : "hello", + * createdAt: "2022-11-09T06:56:15.640Z", + * title: "클린코드", + * nickname : "chanheki", + * intraId: "default@student.42seoul.kr", + * }, + * { + * reviewsId : 5, + * reviewerId : 104, + * bookInfoId: 1, + * content : "hello", + * createdAt: "2022-11-09T06:56:15.640Z", + * title: "클린코드", + * nickname : "chanheki", + * intraId: "default@student.42seoul.kr", + * }, + * { + * reviewsId : 6, + * reviewerId : 105, + * bookInfoId: 1, + * content : "hello", + * createdAt: "2022-11-09T06:56:15.640Z", + * title: "클린코드", + * nickname : "chanheki", + * intraId: "default@student.42seoul.kr", + * }, + * { + * reviewsId : 7, + * reviewerId : 106, + * bookInfoId: 1, + * content : "hello", + * createdAt: "2022-11-09T06:56:15.640Z", + * title: "클린코드", + * nickname : "chanheki", + * intraId: "default@student.42seoul.kr", + * }, + * { + * reviewsId : 8, + * reviewerId : 107, + * bookInfoId: 1, + * content : "hello", + * createdAt: "2022-11-09T06:56:15.640Z", + * title: "클린코드", + * nickname : "chanheki", + * intraId: "default@student.42seoul.kr", + * }, + * { + * reviewsId : 9, + * reviewerId : 108, + * bookInfoId: 1, + * content : "hello", + * createdAt: "2022-11-09T06:56:15.640Z", + * title: "클린코드", + * nickname : "chanheki", + * intraId: "default@student.42seoul.kr", + * }, + * { + * reviewsId : 10, + * reviewerId : 109, + * bookInfoId: 1, + * content : "hello", + * createdAt: "2022-11-09T06:56:15.640Z", + * title: "클린코드", + * nickname : "chanheki", + * intraId: "default@student.42seoul.kr", + * }, + * ] + * "meta": { + * totalItems: 100, + * itemCount : 10, + * itemsPerPage : 10, + * totalPages : 20, + * currentPage : 1, + * finalPage : False + * } + * '400': + * content: + * application/json: + * schema: + * type: object + * examples: + * 적절하지 않는 bookInfoId 값: + * value: + * errorCode: 2 + * 적절하지 않는 userId 값: + * value: + * errorCode: 2 + * 적절하지 않는 page 값: + * value: + * errorCode: 2 + * 적절하지 않는 sort 값: + * value: + * errorCode: 2 + * '401': + * description: 권한 없음. + * content: + * application/json: + * schema: + * type: object + * examples: + * 토큰 누락 : + * value: + * errorCode: 100 + * 사서 권한 없음 : + * value: + * errorCode: 100 + * 토큰 유저 존재하지 않음 : + * value : + * errorCode: 101 + * 토큰 만료 : + * value : + * errorCode: 108 + * 토큰 유효하지 않음 : + * value : + * errorCode: 109 + */ .get('/', authValidate(roleSet.librarian), wrapAsyncController(getReviews)); router -/** - * @openapi - * /api/reviews/my-reviews: - * get: - * description: 자기자신에 대한 모든 Review 데이터를 가져온다. 내부적으로 getReview와 같은 함수를 사용한다. - * tags: - * - reviews - * parameters: - * - name: titleOrNickname - * in: query - * description: 책 제목 또는 닉네임을 검색어로 받는다. - * schema: - * type: string - * - name: limit - * in: query - * schema: - * type: number - * description: 한 페이지에서 몇 개의 게시글을 가져올 지 결정한다. [default = 10] - * required: false - * - name: page - * in: query - * schema: - * type: number - * description: 해당하는 페이지를 보여준다. - * required: false - * - name: sort - * in: query - * schema: - * type: string - * description: asd, desc 값을 통해 시간순으로 정렬된 페이지를 반환한다. - * required: false - * - name: isMyReview - * in: query - * default: false - * schema: - * type: boolean - * description: true 라면 마이페이지 용도의 리뷰를, false 라면 모든 리뷰를 가져온다. - * responses: - * '200': - * content: - * application/json: - * schema: - * type: object - * examples: - * bookInfo 기준 : - * value: - * items : [ - * { - * reviewsId : 1, - * reviewerId : 100, - * bookInfoId: 1, - * content : "hello", - * createdAt: "2022-11-09T06:56:15.640Z", - * title: "클린코드", - * nickname : "sechung", - * intraId: "default@student.42seoul.kr", - * }, - * { - * reviewsId : 2, - * reviewerId : 101, - * bookInfoId: 1, - * content : "hello", - * createdAt: "2022-11-09T06:56:15.640Z", - * title: "클린코드", - * nickname : "chanheki", - * intraId: "default@student.42seoul.kr", - * }, - * { - * reviewsId : 3, - * reviewerId : 102, - * bookInfoId: 1, - * content : "hello", - * createdAt: "2022-11-09T06:56:15.640Z", - * title: "클린코드", - * nickname : "chanheki", - * intraId: "default@student.42seoul.kr", - * }, - * { - * reviewsId : 4, - * reviewerId : 103, - * bookInfoId: 1, - * content : "hello", - * createdAt: "2022-11-09T06:56:15.640Z", - * title: "클린코드", - * nickname : "chanheki", - * intraId: "default@student.42seoul.kr", - * }, - * { - * reviewsId : 5, - * reviewerId : 104, - * bookInfoId: 1, - * content : "hello", - * createdAt: "2022-11-09T06:56:15.640Z", - * title: "클린코드", - * nickname : "chanheki", - * intraId: "default@student.42seoul.kr", - * }, - * { - * reviewsId : 6, - * reviewerId : 105, - * bookInfoId: 1, - * content : "hello", - * createdAt: "2022-11-09T06:56:15.640Z", - * title: "클린코드", - * nickname : "chanheki", - * intraId: "default@student.42seoul.kr", - * }, - * { - * reviewsId : 7, - * reviewerId : 106, - * bookInfoId: 1, - * content : "hello", - * createdAt: "2022-11-09T06:56:15.640Z", - * title: "클린코드", - * nickname : "chanheki", - * intraId: "default@student.42seoul.kr", - * }, - * { - * reviewsId : 8, - * reviewerId : 107, - * bookInfoId: 1, - * content : "hello", - * createdAt: "2022-11-09T06:56:15.640Z", - * title: "클린코드", - * nickname : "chanheki", - * intraId: "default@student.42seoul.kr", - * }, - * { - * reviewsId : 9, - * reviewerId : 108, - * bookInfoId: 1, - * content : "hello", - * createdAt: "2022-11-09T06:56:15.640Z", - * title: "클린코드", - * nickname : "chanheki", - * intraId: "default@student.42seoul.kr", - * }, - * { - * reviewsId : 10, - * reviewerId : 109, - * bookInfoId: 1, - * content : "hello", - * createdAt: "2022-11-09T06:56:15.640Z", - * title: "클린코드", - * nickname : "chanheki", - * intraId: "default@student.42seoul.kr", - * }, - * ] - * "meta": { - * totalItems: 100, - * itemCount : 10, - * itemsPerPage : 10, - * totalPages : 20, - * currentPage : 1, - * finalPage : False - * } - * '400': - * content: - * application/json: - * schema: - * type: object - * examples: - * 적절하지 않는 page 값: - * value: - * errorCode: 2 - * 적절하지 않는 sort 값: - * value: - * errorCode: 2 - * '401': - * description: 권한 없음. - * content: - * application/json: - * schema: - * type: object - * examples: - * 토큰 누락 : - * value: - * errorCode: 100 - * 토큰 유저 존재하지 않음 : - * value : - * errorCode: 101 - * 토큰 만료 : - * value : - * errorCode: 108 - * 토큰 유효하지 않음 : - * value : - * errorCode: 109 - */ + /** + * @openapi + * /api/reviews/my-reviews: + * get: + * description: 자기자신에 대한 모든 Review 데이터를 가져온다. 내부적으로 getReview와 같은 함수를 사용한다. + * tags: + * - reviews + * parameters: + * - name: titleOrNickname + * in: query + * description: 책 제목 또는 닉네임을 검색어로 받는다. + * schema: + * type: string + * - name: limit + * in: query + * schema: + * type: number + * description: 한 페이지에서 몇 개의 게시글을 가져올 지 결정한다. [default = 10] + * required: false + * - name: page + * in: query + * schema: + * type: number + * description: 해당하는 페이지를 보여준다. + * required: false + * - name: sort + * in: query + * schema: + * type: string + * description: asd, desc 값을 통해 시간순으로 정렬된 페이지를 반환한다. + * required: false + * - name: isMyReview + * in: query + * default: false + * schema: + * type: boolean + * description: true 라면 마이페이지 용도의 리뷰를, false 라면 모든 리뷰를 가져온다. + * responses: + * '200': + * content: + * application/json: + * schema: + * type: object + * examples: + * bookInfo 기준 : + * value: + * items : [ + * { + * reviewsId : 1, + * reviewerId : 100, + * bookInfoId: 1, + * content : "hello", + * createdAt: "2022-11-09T06:56:15.640Z", + * title: "클린코드", + * nickname : "sechung", + * intraId: "default@student.42seoul.kr", + * }, + * { + * reviewsId : 2, + * reviewerId : 101, + * bookInfoId: 1, + * content : "hello", + * createdAt: "2022-11-09T06:56:15.640Z", + * title: "클린코드", + * nickname : "chanheki", + * intraId: "default@student.42seoul.kr", + * }, + * { + * reviewsId : 3, + * reviewerId : 102, + * bookInfoId: 1, + * content : "hello", + * createdAt: "2022-11-09T06:56:15.640Z", + * title: "클린코드", + * nickname : "chanheki", + * intraId: "default@student.42seoul.kr", + * }, + * { + * reviewsId : 4, + * reviewerId : 103, + * bookInfoId: 1, + * content : "hello", + * createdAt: "2022-11-09T06:56:15.640Z", + * title: "클린코드", + * nickname : "chanheki", + * intraId: "default@student.42seoul.kr", + * }, + * { + * reviewsId : 5, + * reviewerId : 104, + * bookInfoId: 1, + * content : "hello", + * createdAt: "2022-11-09T06:56:15.640Z", + * title: "클린코드", + * nickname : "chanheki", + * intraId: "default@student.42seoul.kr", + * }, + * { + * reviewsId : 6, + * reviewerId : 105, + * bookInfoId: 1, + * content : "hello", + * createdAt: "2022-11-09T06:56:15.640Z", + * title: "클린코드", + * nickname : "chanheki", + * intraId: "default@student.42seoul.kr", + * }, + * { + * reviewsId : 7, + * reviewerId : 106, + * bookInfoId: 1, + * content : "hello", + * createdAt: "2022-11-09T06:56:15.640Z", + * title: "클린코드", + * nickname : "chanheki", + * intraId: "default@student.42seoul.kr", + * }, + * { + * reviewsId : 8, + * reviewerId : 107, + * bookInfoId: 1, + * content : "hello", + * createdAt: "2022-11-09T06:56:15.640Z", + * title: "클린코드", + * nickname : "chanheki", + * intraId: "default@student.42seoul.kr", + * }, + * { + * reviewsId : 9, + * reviewerId : 108, + * bookInfoId: 1, + * content : "hello", + * createdAt: "2022-11-09T06:56:15.640Z", + * title: "클린코드", + * nickname : "chanheki", + * intraId: "default@student.42seoul.kr", + * }, + * { + * reviewsId : 10, + * reviewerId : 109, + * bookInfoId: 1, + * content : "hello", + * createdAt: "2022-11-09T06:56:15.640Z", + * title: "클린코드", + * nickname : "chanheki", + * intraId: "default@student.42seoul.kr", + * }, + * ] + * "meta": { + * totalItems: 100, + * itemCount : 10, + * itemsPerPage : 10, + * totalPages : 20, + * currentPage : 1, + * finalPage : False + * } + * '400': + * content: + * application/json: + * schema: + * type: object + * examples: + * 적절하지 않는 page 값: + * value: + * errorCode: 2 + * 적절하지 않는 sort 값: + * value: + * errorCode: 2 + * '401': + * description: 권한 없음. + * content: + * application/json: + * schema: + * type: object + * examples: + * 토큰 누락 : + * value: + * errorCode: 100 + * 토큰 유저 존재하지 않음 : + * value : + * errorCode: 101 + * 토큰 만료 : + * value : + * errorCode: 108 + * 토큰 유효하지 않음 : + * value : + * errorCode: 109 + */ .get('/my-reviews', authValidate(roleSet.all), wrapAsyncController(getReviews)); router -/** - * @openapi - * /api/reviews/{reviewsId}: - * put: - * description: 책 리뷰를 수정한다. 작성자만 수정할 수 있다. content 길이는 10글자 이상 100글자 이하로 입력하여야 한다. - * tags: - * - reviews - * parameters: - * - name: reviewsId - * in: path - * description: 수정할 reviews ID - * required: true - * requestBody: - * required: true - * content: - * application/json: - * schema: - * type: object - * properties: - * content: - * type: string - * nullable: false경 - * example: "책이 좋네요 열글자." - * responses: - * '200': - * description: 리뷰가 DB에 정상적으로 update됨. - * '400': - * content: - * application/json: - * schema: - * type: object - * examples: - * 적절하지 않는 reviewsId 값: - * value: - * errorCode: 800 - * 유효하지 않은 content 길이 : - * value: - * errorCode: 801 - * '401': - * description: 권한 없음. - * content: - * application/json: - * schema: - * type: object - * examples: - * 토큰 누락 : - * value: - * errorCode: 100 - * 토큰 유저 존재하지 않음 : - * value : - * errorCode: 101 - * 토큰 만료 : - * value : - * errorCode: 108 - * 토큰 유효하지 않음 : - * value : - * errorCode: 109 - * 토큰 userId와 리뷰 userID 불일치 && 사서 권한 없음 : - * value : - * errorCode: 801 - * 토큰 Disabled Reviews는 수정할 수 없음. : - * value : - * errorCode: 805 - * '404': - * description: 존재하지 않는 reviewsId. - * content: - * application/json: - * schema: - * type: object - * examples: - * 존재하지 않는 reviewsId : - * value: - * errorCode: 804 - */ + /** + * @openapi + * /api/reviews/{reviewsId}: + * put: + * description: 책 리뷰를 수정한다. 작성자만 수정할 수 있다. content 길이는 10글자 이상 100글자 이하로 입력하여야 한다. + * tags: + * - reviews + * parameters: + * - name: reviewsId + * in: path + * description: 수정할 reviews ID + * required: true + * requestBody: + * required: true + * content: + * application/json: + * schema: + * type: object + * properties: + * content: + * type: string + * nullable: false경 + * example: "책이 좋네요 열글자." + * responses: + * '200': + * description: 리뷰가 DB에 정상적으로 update됨. + * '400': + * content: + * application/json: + * schema: + * type: object + * examples: + * 적절하지 않는 reviewsId 값: + * value: + * errorCode: 800 + * 유효하지 않은 content 길이 : + * value: + * errorCode: 801 + * '401': + * description: 권한 없음. + * content: + * application/json: + * schema: + * type: object + * examples: + * 토큰 누락 : + * value: + * errorCode: 100 + * 토큰 유저 존재하지 않음 : + * value : + * errorCode: 101 + * 토큰 만료 : + * value : + * errorCode: 108 + * 토큰 유효하지 않음 : + * value : + * errorCode: 109 + * 토큰 userId와 리뷰 userID 불일치 && 사서 권한 없음 : + * value : + * errorCode: 801 + * 토큰 Disabled Reviews는 수정할 수 없음. : + * value : + * errorCode: 805 + * '404': + * description: 존재하지 않는 reviewsId. + * content: + * application/json: + * schema: + * type: object + * examples: + * 존재하지 않는 reviewsId : + * value: + * errorCode: 804 + */ .put('/:reviewsId', authValidate(roleSet.all), wrapAsyncController(updateReviews)); router -/** - * @openapi - * /api/reviews/{reviewsId}: - * patch: - * description: 책 리뷰의 비활성화 여부를 토글 방식으로 변환 - * tags: - * - reviews - * parameters: - * - name: reviewsId - * in: path - * description: 수정할 reviews ID - * required: true - * requestBody: - * required: false - * responses: - * '200': - * description: 리뷰가 DB에 정상적으로 fetch됨. - */ + /** + * @openapi + * /api/reviews/{reviewsId}: + * patch: + * description: 책 리뷰의 비활성화 여부를 토글 방식으로 변환 + * tags: + * - reviews + * parameters: + * - name: reviewsId + * in: path + * description: 수정할 reviews ID + * required: true + * requestBody: + * required: false + * responses: + * '200': + * description: 리뷰가 DB에 정상적으로 fetch됨. + */ .patch('/:reviewsId', authValidate(roleSet.librarian), wrapAsyncController(patchReviews)); router -/** - * @openapi - * /api/reviews/{reviewsId}: - * delete: - * description: 책 리뷰를 삭제한다. 작성자와 사서 권한이 있는 사용자만 삭제할 수 있다. - * tags: - * - reviews - * parameters: - * - name: reviewsId - * required: true - * in: path - * description: 들어온 reviewsId에 해당하는 리뷰를 삭제한다. - * responses: - * '200': - * description: 리뷰가 DB에서 정상적으로 delete됨. - * '400': - * content: - * application/json: - * schema: - * type: object - * examples: - * 적절하지 않는 reviewsId 값: - * value: - * errorCode: 800 - * '401': - * description: 권한 없음. - * content: - * application/json: - * schema: - * type: object - * examples: - * 토큰 누락 : - * value: - * errorCode: 100 - * 토큰 유저 존재하지 않음 : - * value : - * errorCode: 101 - * 토큰 만료 : - * value : - * errorCode: 108 - * 토큰 유효하지 않음 : - * value : - * errorCode: 109 - * 토큰 userId와 리뷰 userID 불일치 && 사서 권한 없음 : - * value : - * errorCode: 801 - * '404': - * description: 존재하지 않는 reviewsId. - * content: - * application/json: - * schema: - * type: object - * examples: - * 존재하지 않는 reviewsId : - * value: - * errorCode: 804 - */ + /** + * @openapi + * /api/reviews/{reviewsId}: + * delete: + * description: 책 리뷰를 삭제한다. 작성자와 사서 권한이 있는 사용자만 삭제할 수 있다. + * tags: + * - reviews + * parameters: + * - name: reviewsId + * required: true + * in: path + * description: 들어온 reviewsId에 해당하는 리뷰를 삭제한다. + * responses: + * '200': + * description: 리뷰가 DB에서 정상적으로 delete됨. + * '400': + * content: + * application/json: + * schema: + * type: object + * examples: + * 적절하지 않는 reviewsId 값: + * value: + * errorCode: 800 + * '401': + * description: 권한 없음. + * content: + * application/json: + * schema: + * type: object + * examples: + * 토큰 누락 : + * value: + * errorCode: 100 + * 토큰 유저 존재하지 않음 : + * value : + * errorCode: 101 + * 토큰 만료 : + * value : + * errorCode: 108 + * 토큰 유효하지 않음 : + * value : + * errorCode: 109 + * 토큰 userId와 리뷰 userID 불일치 && 사서 권한 없음 : + * value : + * errorCode: 801 + * '404': + * description: 존재하지 않는 reviewsId. + * content: + * application/json: + * schema: + * type: object + * examples: + * 존재하지 않는 reviewsId : + * value: + * errorCode: 804 + */ .delete('/:reviewsId', authValidate(roleSet.all), wrapAsyncController(deleteReviews)); diff --git a/backend/src/v1/routes/searchKeywords.routes.ts b/backend/src/v1/routes/searchKeywords.routes.ts index 457e12e1..83740735 100644 --- a/backend/src/v1/routes/searchKeywords.routes.ts +++ b/backend/src/v1/routes/searchKeywords.routes.ts @@ -1,5 +1,8 @@ import { Router } from 'express'; -import { searchKeywordsAutocomplete, getPopularSearchKeywords } from '../search-keywords/searchKeywords.controller'; +import { + searchKeywordsAutocomplete, + getPopularSearchKeywords, +} from '../search-keywords/searchKeywords.controller'; export const path = '/search-keywords'; export const router = Router(); diff --git a/backend/src/v1/routes/stock.routes.ts b/backend/src/v1/routes/stock.routes.ts index 652ab929..ab3c29da 100644 --- a/backend/src/v1/routes/stock.routes.ts +++ b/backend/src/v1/routes/stock.routes.ts @@ -6,136 +6,136 @@ export const path = '/stock'; export const router = Router(); router -/** - * @openapi - * /api/stock/search: - * get: - * description: 책 재고 정보를 검색해 온다. - * tags: - * - stock - * parameters: - * - in: query - * name: page - * description: 페이지 - * schema: - * type: integer - * - in: query - * name: limit - * description: 한 페이지에 들어올 검색결과 수 - * schema: - * type: integer - * responses: - * '200': - * description: 검색 결과를 반환한다. - * content: - * application/json: - * schema: - * type: object - * properties: - * items: - * description: 재고 정보 목록 - * type: array - * items: - * type: object - * properties: - * bookId: - * description: 도서 번호 - * type: integer - * example: 3 - * bookInfoId: - * description: 도서 정보 번호 - * type: integer - * example: 2 - * title: - * description: 책 제목 - * type: string - * example: "TCP IP 윈도우 소켓 프로그래밍" - * author: - * description: 저자 - * type: string - * example: "김선우" - * donator: - * description: 기부자 닉네임 - * type: string - * example: "" - * publisher: - * description: 출판사 - * type: string - * example: "한빛아카데미" - * pubishedAt: - * description: 출판일 - * type: string - * format: date - * example: 20220522 - * isbn: - * description: isbn - * type: string - * format: number - * example: "9788998756444" - * image: - * description: 이미지 주소 - * type: string - * format: uri - * example: "https://image.kyobobook.co.kr/images/book/xlarge/444/x9788998756444.jpg" - * status: - * description: 책의 상태 정보 - * type: number - * example: 0 - * categoryId: - * description: 책의 캬테고리 번호 - * type: number - * example: 2 - * callSign: - * description: 책의 고유 호출 번호 - * type: string - * example: "C5.13.v1.c2" - * category: - * description: 책의 카테고리 정보 - * type: string - * example: "네트워크" - * updatedAt: - * description: 책 정보의 마지막 변경 날짜 - * type: string - * format: date - * example: "2022-07-09-22:49:33" - * meta: - * description: 재고 수와 관련된 정보 - * type: object - * properties: - * totalItems: - * description: 전체 검색 결과 수 - * type: integer - * example: 1 - * itemCount: - * description: 현재 페이지 검색 결과 수 - * type: integer - * example: 1 - * itemsPerPage: - * description: 페이지 당 검색 결과 수 - * type: integer - * example: 1 - * totalPages: - * description: 전체 결과 페이지 수 - * type: integer - * example: 1 - * currentPage: - * description: 현재 페이지 - * type: integer - * example: 1 - * '500': - * description: Server Error - * content: - * application/json: - * schema: - * type: object - * description: error decription - * properties: - * errorCode: - * type: number - * description: 에러코드 - * example: 1 - * - */ + /** + * @openapi + * /api/stock/search: + * get: + * description: 책 재고 정보를 검색해 온다. + * tags: + * - stock + * parameters: + * - in: query + * name: page + * description: 페이지 + * schema: + * type: integer + * - in: query + * name: limit + * description: 한 페이지에 들어올 검색결과 수 + * schema: + * type: integer + * responses: + * '200': + * description: 검색 결과를 반환한다. + * content: + * application/json: + * schema: + * type: object + * properties: + * items: + * description: 재고 정보 목록 + * type: array + * items: + * type: object + * properties: + * bookId: + * description: 도서 번호 + * type: integer + * example: 3 + * bookInfoId: + * description: 도서 정보 번호 + * type: integer + * example: 2 + * title: + * description: 책 제목 + * type: string + * example: "TCP IP 윈도우 소켓 프로그래밍" + * author: + * description: 저자 + * type: string + * example: "김선우" + * donator: + * description: 기부자 닉네임 + * type: string + * example: "" + * publisher: + * description: 출판사 + * type: string + * example: "한빛아카데미" + * pubishedAt: + * description: 출판일 + * type: string + * format: date + * example: 20220522 + * isbn: + * description: isbn + * type: string + * format: number + * example: "9788998756444" + * image: + * description: 이미지 주소 + * type: string + * format: uri + * example: "https://image.kyobobook.co.kr/images/book/xlarge/444/x9788998756444.jpg" + * status: + * description: 책의 상태 정보 + * type: number + * example: 0 + * categoryId: + * description: 책의 캬테고리 번호 + * type: number + * example: 2 + * callSign: + * description: 책의 고유 호출 번호 + * type: string + * example: "C5.13.v1.c2" + * category: + * description: 책의 카테고리 정보 + * type: string + * example: "네트워크" + * updatedAt: + * description: 책 정보의 마지막 변경 날짜 + * type: string + * format: date + * example: "2022-07-09-22:49:33" + * meta: + * description: 재고 수와 관련된 정보 + * type: object + * properties: + * totalItems: + * description: 전체 검색 결과 수 + * type: integer + * example: 1 + * itemCount: + * description: 현재 페이지 검색 결과 수 + * type: integer + * example: 1 + * itemsPerPage: + * description: 페이지 당 검색 결과 수 + * type: integer + * example: 1 + * totalPages: + * description: 전체 결과 페이지 수 + * type: integer + * example: 1 + * currentPage: + * description: 현재 페이지 + * type: integer + * example: 1 + * '500': + * description: Server Error + * content: + * application/json: + * schema: + * type: object + * description: error decription + * properties: + * errorCode: + * type: number + * description: 에러코드 + * example: 1 + * + */ .get('/search', stockSearch) /** diff --git a/backend/src/v1/routes/tags.routes.ts b/backend/src/v1/routes/tags.routes.ts index cfd7b8e9..4c3b2650 100644 --- a/backend/src/v1/routes/tags.routes.ts +++ b/backend/src/v1/routes/tags.routes.ts @@ -19,247 +19,247 @@ export const path = '/tags'; export const router = Router(); router -/** - * @openapi - * /api/tags/super: - * patch: - * description: 슈퍼 태그를 수정한다. - * tags: - * - tags - * requestBody: - * required: true - * content: - * application/json: - * schema: - * type: object - * properties: - * id: - * description: 수정할 태그의 id - * type: integer - * example: 1 - * required: true - * content: - * description: 슈퍼 태그 내용 - * type: string - * example: "수정할_내용_적기" - * responses: - * '200': - * description: 슈퍼 태그 수정 성공. - * content: - * application/json: - * schema: - * type: object - * properties: - * id: - * description: 수정된 슈퍼 태그의 id - * type: integer - * example: 1 - * '900': - * description: 태그의 양식이 올바르지 않습니다. - * content: - * application/json: - * schema: - * type: object - * properties: - * errorCode: - * type: integer - * example: 900 - * '902': - * description: 이미 존재하는 태그입니다. - * content: - * application/json: - * schema: - * type: object - * properties: - * errorCode: - * type: integer - * example: 902 - * '906': - * description: 디폴트 태그입니다. - * content: - * application/json: - * schema: - * type: object - * properties: - * errorCode: - * type: integer - * example: 906 - * '905': - * description: DB 에러로 인한 업데이트 실패 - * content: - * application/json: - * schema: - * type: object - * properties: - * errorCode: - * type: number - * example: 500 - */ + /** + * @openapi + * /api/tags/super: + * patch: + * description: 슈퍼 태그를 수정한다. + * tags: + * - tags + * requestBody: + * required: true + * content: + * application/json: + * schema: + * type: object + * properties: + * id: + * description: 수정할 태그의 id + * type: integer + * example: 1 + * required: true + * content: + * description: 슈퍼 태그 내용 + * type: string + * example: "수정할_내용_적기" + * responses: + * '200': + * description: 슈퍼 태그 수정 성공. + * content: + * application/json: + * schema: + * type: object + * properties: + * id: + * description: 수정된 슈퍼 태그의 id + * type: integer + * example: 1 + * '900': + * description: 태그의 양식이 올바르지 않습니다. + * content: + * application/json: + * schema: + * type: object + * properties: + * errorCode: + * type: integer + * example: 900 + * '902': + * description: 이미 존재하는 태그입니다. + * content: + * application/json: + * schema: + * type: object + * properties: + * errorCode: + * type: integer + * example: 902 + * '906': + * description: 디폴트 태그입니다. + * content: + * application/json: + * schema: + * type: object + * properties: + * errorCode: + * type: integer + * example: 906 + * '905': + * description: DB 에러로 인한 업데이트 실패 + * content: + * application/json: + * schema: + * type: object + * properties: + * errorCode: + * type: number + * example: 500 + */ .patch('/super', authValidate(roleSet.librarian), updateSuperTags); router -/** - * @openapi - * /api/tags/sub: - * patch: - * description: 서브 태그를 수정한다. - * tags: - * - tags - * requestBody: - * required: true - * content: - * application/json: - * schema: - * type: object - * properties: - * id: - * description: 수정할 태그의 id - * type: integer - * example: 1 - * required: true - * visibility: - * description: 서브 태그의 공개 여부 - * type: string - * example: public, private - * responses: - * '200': - * description: 서브 태그 수정 성공. - * content: - * application/json: - * schema: - * type: object - * properties: - * id: - * description: 수정된 서브 태그의 id - * type: integer - * example: 1 - * '900': - * description: 태그의 양식이 올바르지 않습니다. - * content: - * application/json: - * schema: - * type: object - * properties: - * errorCode: - * type: integer - * example: 900 - * '901': - * description: 권한이 없습니다. - * content: - * application/json: - * schema: - * type: object - * properties: - * errorCode: - * type: integer - * example: 902 - * '905': - * description: DB 에러로 인한 업데이트 실패 - * content: - * application/json: - * schema: - * type: object - * properties: - * errorCode: - * type: number - * example: 500 - */ + /** + * @openapi + * /api/tags/sub: + * patch: + * description: 서브 태그를 수정한다. + * tags: + * - tags + * requestBody: + * required: true + * content: + * application/json: + * schema: + * type: object + * properties: + * id: + * description: 수정할 태그의 id + * type: integer + * example: 1 + * required: true + * visibility: + * description: 서브 태그의 공개 여부 + * type: string + * example: public, private + * responses: + * '200': + * description: 서브 태그 수정 성공. + * content: + * application/json: + * schema: + * type: object + * properties: + * id: + * description: 수정된 서브 태그의 id + * type: integer + * example: 1 + * '900': + * description: 태그의 양식이 올바르지 않습니다. + * content: + * application/json: + * schema: + * type: object + * properties: + * errorCode: + * type: integer + * example: 900 + * '901': + * description: 권한이 없습니다. + * content: + * application/json: + * schema: + * type: object + * properties: + * errorCode: + * type: integer + * example: 902 + * '905': + * description: DB 에러로 인한 업데이트 실패 + * content: + * application/json: + * schema: + * type: object + * properties: + * errorCode: + * type: number + * example: 500 + */ .patch('/sub', authValidate(roleSet.librarian), updateSubTags); router -/** - * @openapi - * /api/tags/{bookInfoId}/merge: - * patch: - * description: 태그를 병합한다. - * tags: - * - tags - * parameters: - * - in: path - * name: bookInfoId - * description: 병합할 책 정보의 id - * required: true - * type: integer - * requestBody: - * required: true - * content: - * application/json: - * schema: - * type: object - * properties: - * subTagIds: - * description: 병합될 서브 태그의 id 리스트 - * type: list - * required: true - * example: [1, 2, 3, 5, 10] - * superTagId: - * description: 슈퍼 태그의 id. null일 경우, 디폴트 태그로 병합됨을 의미한다. - * type: integer - * required: true - * example: 2 - * responses: - * '200': - * description: 슈퍼 태그 수정 성공. - * content: - * application/json: - * schema: - * type: object - * properties: - * id: - * description: 슈퍼 태그의 id - * type: integer - * example: 1 - * '900': - * description: 태그의 양식이 올바르지 않습니다. - * content: - * application/json: - * schema: - * type: object - * properties: - * errorCode: - * type: integer - * example: 900 - * '902': - * description: 이미 존재하는 태그입니다. - * content: - * application/json: - * schema: - * type: object - * properties: - * errorCode: - * type: integer - * example: 902 - * '906': - * description: 디폴트 태그에는 병합할 수 없습니다. - * content: - * application/json: - * schema: - * type: object - * properties: - * errorCode: - * type: integer - * example: 906 - * '910': - * description: 유효하지 않은 태그 id입니다. - * content: - * application/json: - * schema: - * type: object - * properties: - * errorCode: - * type: integer - * example: 910 - * '905': - * description: DB 에러로 인한 업데이트 실패 - * content: - * application/json: - * schema: - * type: object - * properties: - * errorCode: - * type: number - * example: 500 - */ + /** + * @openapi + * /api/tags/{bookInfoId}/merge: + * patch: + * description: 태그를 병합한다. + * tags: + * - tags + * parameters: + * - in: path + * name: bookInfoId + * description: 병합할 책 정보의 id + * required: true + * type: integer + * requestBody: + * required: true + * content: + * application/json: + * schema: + * type: object + * properties: + * subTagIds: + * description: 병합될 서브 태그의 id 리스트 + * type: list + * required: true + * example: [1, 2, 3, 5, 10] + * superTagId: + * description: 슈퍼 태그의 id. null일 경우, 디폴트 태그로 병합됨을 의미한다. + * type: integer + * required: true + * example: 2 + * responses: + * '200': + * description: 슈퍼 태그 수정 성공. + * content: + * application/json: + * schema: + * type: object + * properties: + * id: + * description: 슈퍼 태그의 id + * type: integer + * example: 1 + * '900': + * description: 태그의 양식이 올바르지 않습니다. + * content: + * application/json: + * schema: + * type: object + * properties: + * errorCode: + * type: integer + * example: 900 + * '902': + * description: 이미 존재하는 태그입니다. + * content: + * application/json: + * schema: + * type: object + * properties: + * errorCode: + * type: integer + * example: 902 + * '906': + * description: 디폴트 태그에는 병합할 수 없습니다. + * content: + * application/json: + * schema: + * type: object + * properties: + * errorCode: + * type: integer + * example: 906 + * '910': + * description: 유효하지 않은 태그 id입니다. + * content: + * application/json: + * schema: + * type: object + * properties: + * errorCode: + * type: integer + * example: 910 + * '905': + * description: DB 에러로 인한 업데이트 실패 + * content: + * application/json: + * schema: + * type: object + * properties: + * errorCode: + * type: number + * example: 500 + */ .patch('/:bookInfoId/merge', authValidate(roleSet.librarian), mergeTags); router diff --git a/backend/src/v1/routes/users.routes.ts b/backend/src/v1/routes/users.routes.ts index b5e4efa2..1ab9c82a 100644 --- a/backend/src/v1/routes/users.routes.ts +++ b/backend/src/v1/routes/users.routes.ts @@ -1,9 +1,8 @@ import { Router } from 'express'; import { roleSet } from '~/v1/auth/auth.type'; import authValidate from '~/v1/auth/auth.validate'; -import { - create, getVersion, myupdate, search, update, -} from '~/v1/users/users.controller'; +import { create, getVersion, myupdate, search, update, mydata } from '~/v1/users/users.controller'; + export const path = '/users'; export const router = Router(); @@ -31,7 +30,7 @@ export const router = Router(); * description: 한 페이지에 들어올 검색결과 수 * schema: * type: integer - * - in: query + * - in: query * name: id * description: 검색할 유저의 id * schema: @@ -358,10 +357,60 @@ export const router = Router(); * type: string * example: gshim.v1 */ -router.get('/search', search) + /** + * @openapi + * /api/users/me: + * get: + * description: 내 정보를 가져온다. + * tags: + * - users + * responses: + * '200': + * description: 내 정보를 반환한다. + * content: + * application/json: + * schema: + * properties: + * nickname: + * description: 에러코드 + * type: string + * example: jimin + * intraId: + * description: 인트라 ID + * type: string + * example: 10035 + * slack: + * description: slack 맴버 변수 + * type: string + * example: "U02LNNDRC9F" + * role: + * description: 유저의 권한 + * type: string + * example: 2 + * penaltyEbdDate: + * description: 패널티가 끝나는 날 + * type: string + * example: 2022-06-18 + * overDueDay: + * description: 현재 연체된 날수 + * type: string + * format: number + * example: 0 + * reservations: + * description: 해당 유저의 예약 정보 + * type: array + * example: [] + * lendings: + * description: 해당 유저의 대출 정보 + * type: array + * example: [] + */ +router + .get('/search', authValidate(roleSet.librarian), search) .post('/create', create) .patch('/update/:id', authValidate(roleSet.librarian), update) .patch('/myupdate', authValidate(roleSet.all), myupdate) + .get('/me', authValidate(roleSet.service), mydata) .get('/EasterEgg', getVersion); -// .delete('/delete/:id', authValidate(roleSet.librarian), deleteUser); \ No newline at end of file +// .delete('/delete/:id', authValidate(roleSet.librarian), deleteUser); diff --git a/backend/src/v1/search-keywords/booksInfoSearchKeywords.repository.ts b/backend/src/v1/search-keywords/booksInfoSearchKeywords.repository.ts index a31bdad7..4dbe7150 100644 --- a/backend/src/v1/search-keywords/booksInfoSearchKeywords.repository.ts +++ b/backend/src/v1/search-keywords/booksInfoSearchKeywords.repository.ts @@ -1,10 +1,7 @@ import { QueryRunner, Repository } from 'typeorm'; import jipDataSource from '~/app-data-source'; import { BookInfo, BookInfoSearchKeywords } from '~/entity/entities'; -import { - disassembleHangul, - extractHangulInitials, -} from '../utils/processKeywords'; +import { disassembleHangul, extractHangulInitials } from '../utils/processKeywords'; import { UpdateBookInfo } from '../books/books.type'; import { FindBookInfoSearchKeyword } from './searchKeywords.type'; @@ -21,9 +18,7 @@ class BookInfoSearchKeywordRepository extends Repository } async createBookInfoSearchKeyword(bookInfo: BookInfo) { - const { - id, title, author, publisher, - } = bookInfo; + const { id, title, author, publisher } = bookInfo; const disassembledTitle = disassembleHangul(title); const titleInitials = extractHangulInitials(title); @@ -45,9 +40,7 @@ class BookInfoSearchKeywordRepository extends Repository } async updateBookInfoSearchKeyword(targetId: number, bookInfo: UpdateBookInfo) { - const { - id, title, author, publisher, - } = bookInfo; + const { id, title, author, publisher } = bookInfo; const disassembledTitle = disassembleHangul(title); const titleInitials = extractHangulInitials(title); diff --git a/backend/src/v1/search-keywords/searchKeywords.controller.ts b/backend/src/v1/search-keywords/searchKeywords.controller.ts index 2f231ce3..5684af04 100644 --- a/backend/src/v1/search-keywords/searchKeywords.controller.ts +++ b/backend/src/v1/search-keywords/searchKeywords.controller.ts @@ -21,16 +21,11 @@ export const getPopularSearchKeywords = async ( } if (error.message === 'DB error') { return next( - new ErrorResponse( - errorCode.QUERY_EXECUTION_FAILED, - status.INTERNAL_SERVER_ERROR, - ), + new ErrorResponse(errorCode.QUERY_EXECUTION_FAILED, status.INTERNAL_SERVER_ERROR), ); } logger.error(error); - return next( - new ErrorResponse(errorCode.UNKNOWN_ERROR, status.INTERNAL_SERVER_ERROR), - ); + return next(new ErrorResponse(errorCode.UNKNOWN_ERROR, status.INTERNAL_SERVER_ERROR)); } }; @@ -38,10 +33,8 @@ export const searchKeywordsAutocomplete = async ( req: Request, res: Response, next: NextFunction, -) : Promise => { - let { - keyword, - } = req.query; +): Promise => { + let { keyword } = req.query; if (typeof keyword === 'string') { keyword = keyword.trim(); } diff --git a/backend/src/v1/search-keywords/searchKeywords.service.ts b/backend/src/v1/search-keywords/searchKeywords.service.ts index 7426f7d9..e933da84 100644 --- a/backend/src/v1/search-keywords/searchKeywords.service.ts +++ b/backend/src/v1/search-keywords/searchKeywords.service.ts @@ -7,11 +7,7 @@ import { disassembleHangul, removeSpecialCharacters, } from '~/v1/utils/processKeywords'; -import { - AutocompleteKeyword, - PopularSearchKeyword, - SearchKeyword, -} from './searchKeywords.type'; +import { AutocompleteKeyword, PopularSearchKeyword, SearchKeyword } from './searchKeywords.type'; import SearchKeywordsRepository from './searchKeywords.repository'; import SearchLogsRepository from './searchLogs.repository'; @@ -30,7 +26,7 @@ export const getPopularSearches = async ( SELECT keyword FROM search_logs LEFT JOIN search_keywords ON search_logs.search_keyword_id = search_keywords.id - WHERE search_logs.timestamp BETWEEN NOW() - INTERVAL 1 DAY - INTERVAL ? DAY AND NOW() - INTERVAL ? DAY + WHERE search_logs.timestamp BETWEEN NOW() - INTERVAL 1 DAY - INTERVAL ? DAY AND NOW() - INTERVAL ? DAY GROUP BY search_keywords.keyword HAVING COUNT(search_keywords.keyword) >= ? ORDER BY COUNT(search_keywords.keyword) DESC, MAX(search_logs.timestamp) DESC @@ -41,7 +37,7 @@ export const getPopularSearches = async ( SELECT keyword FROM search_logs LEFT JOIN search_keywords ON search_logs.search_keyword_id = search_keywords.id - WHERE search_logs.timestamp BETWEEN NOW() - INTERVAL 1 MONTH - INTERVAL ? DAY AND NOW() - INTERVAL ? DAY + WHERE search_logs.timestamp BETWEEN NOW() - INTERVAL 1 MONTH - INTERVAL ? DAY AND NOW() - INTERVAL ? DAY GROUP BY search_keywords.keyword HAVING COUNT(search_keywords.keyword) >= ? ORDER BY COUNT(search_keywords.keyword) DESC, MAX(search_logs.timestamp) DESC @@ -78,10 +74,7 @@ export const getPopularSearches = async ( const updateLastPopular = (items: string[]) => { lastPopular = [...items]; - logger.debug( - `(${new Date().toLocaleString()}) Popular Search Keywords `, - lastPopular, - ); + logger.debug(`(${new Date().toLocaleString()}) Popular Search Keywords `, lastPopular); }; export const renewLastPopular = async () => { @@ -103,15 +96,13 @@ export const getPopularSearchKeywords = async () => { if (!lastPopular || lastPopular.length === 0) { updateLastPopular(popularSearchKeywords.map((item) => item.keyword)); } - const items: PopularSearchKeyword[] = popularSearchKeywords.map( - (item, index: number) => { - const preRanking = lastPopular.indexOf(item.keyword); - return { - searchKeyword: item.keyword, - rankingChange: preRanking === -1 ? null : preRanking - index, - }; - }, - ); + const items: PopularSearchKeyword[] = popularSearchKeywords.map((item, index: number) => { + const preRanking = lastPopular.indexOf(item.keyword); + return { + searchKeyword: item.keyword, + rankingChange: preRanking === -1 ? null : preRanking - index, + }; + }); return items; }; @@ -124,9 +115,7 @@ export const createSearchKeywordLog = async ( if (!keyword) return; const transactionQueryRunner = jipDataSource.createQueryRunner(); - const searchKeywordsRepository = new SearchKeywordsRepository( - transactionQueryRunner, - ); + const searchKeywordsRepository = new SearchKeywordsRepository(transactionQueryRunner); const searchLogsRepository = new SearchLogsRepository(transactionQueryRunner); try { diff --git a/backend/src/v1/slack/slack.controller.ts b/backend/src/v1/slack/slack.controller.ts index 84de2df6..a30f24c3 100644 --- a/backend/src/v1/slack/slack.controller.ts +++ b/backend/src/v1/slack/slack.controller.ts @@ -9,7 +9,7 @@ export const updateSlackList = async ( req: Request, res: Response, next: NextFunction, -) : Promise => { +): Promise => { try { await slack.updateSlackId(); res.status(204).send(); diff --git a/backend/src/v1/slack/slack.service.ts b/backend/src/v1/slack/slack.service.ts index 9936aa44..0430155a 100644 --- a/backend/src/v1/slack/slack.service.ts +++ b/backend/src/v1/slack/slack.service.ts @@ -8,17 +8,20 @@ import * as models from '../DTO/users.model'; const usersService = new UsersService(); -export const updateSlackIdUser = async (id: number, slackId: string) : Promise => { - const result : ResultSetHeader = await executeQuery(` +export const updateSlackIdUser = async (id: number, slackId: string): Promise => { + const result: ResultSetHeader = await executeQuery( + ` UPDATE user SET slack = ? WHERE id = ? - `, [slackId, id]); + `, + [slackId, id], + ); return result.affectedRows; }; -export const searchAuthenticatedUser = async () : Promise => { - const result : models.User[] = await executeQuery(` +export const searchAuthenticatedUser = async (): Promise => { + const result: models.User[] = await executeQuery(` SELECT * FROM user WHERE intraId IS NOT NULL AND (slack IS NULL OR slack = '') @@ -33,10 +36,10 @@ const userMap = new Map(); export const updateSlackId = async (): Promise => { let searchUsers: any[] = []; let cursor; - const authenticatedUser : models.User[] = await searchAuthenticatedUser(); + const authenticatedUser: models.User[] = await searchAuthenticatedUser(); if (authenticatedUser.length === 0) return; while (cursor === undefined || cursor !== '') { - const response = await web.users.list({ cursor, limit: 1000 }) as any; + const response = (await web.users.list({ cursor, limit: 1000 })) as any; searchUsers = searchUsers.concat(response.members); cursor = response.response_metadata.next_cursor; } @@ -59,14 +62,16 @@ export const updateSlackIdByUserId = async (userId: number): Promise => { } }; -export const findUser = (intraName: any) => (userMap.get(intraName)); +export const findUser = (intraName: any) => userMap.get(intraName); export const publishMessage = async (slackId: string, msg: string) => { - await web.chat.postMessage({ - token, - channel: slackId, - text: msg, - }).catch((e) => { - logger.error(e); - }); + await web.chat + .postMessage({ + token, + channel: slackId, + text: msg, + }) + .catch((e) => { + logger.error(e); + }); }; diff --git a/backend/src/v1/stocks/stocks.controller.ts b/backend/src/v1/stocks/stocks.controller.ts index 8cafd3dd..2f4a6502 100644 --- a/backend/src/v1/stocks/stocks.controller.ts +++ b/backend/src/v1/stocks/stocks.controller.ts @@ -1,6 +1,4 @@ -import { - NextFunction, Request, RequestHandler, Response, -} from 'express'; +import { NextFunction, Request, RequestHandler, Response } from 'express'; import * as status from 'http-status'; import * as errorCode from '~/v1/utils/error/errorCode'; import ErrorResponse from '~/v1/utils/error/errorResponse'; diff --git a/backend/src/v1/stocks/stocks.repository.ts b/backend/src/v1/stocks/stocks.repository.ts index ec9fcbe6..5974de07 100644 --- a/backend/src/v1/stocks/stocks.repository.ts +++ b/backend/src/v1/stocks/stocks.repository.ts @@ -1,7 +1,4 @@ -import { - LessThan, - QueryRunner, Repository, -} from 'typeorm'; +import { LessThan, QueryRunner, Repository } from 'typeorm'; import { startOfDay, addDays } from 'date-fns'; import { Book, VStock } from '~/entity/entities'; import jipDataSource from '~/app-data-source'; @@ -14,36 +11,31 @@ class StocksRepository extends Repository { const entityManager = jipDataSource.createEntityManager(queryRunner); super(Book, entityManager); - this.vStock = new Repository( - VStock, - entityManager, - ); + this.vStock = new Repository(VStock, entityManager); } - async getAllStocksAndCount(limit:number, page:number) - : Promise<[VStock[], number]> { + async getAllStocksAndCount(limit: number, page: number): Promise<[VStock[], number]> { const today = startOfDay(new Date()); - const [items, totalItems] = await this.vStock - .findAndCount({ - where: { - updatedAt: LessThan(addDays(today, -15)), - }, - take: limit, - skip: limit * page, - }); + const [items, totalItems] = await this.vStock.findAndCount({ + where: { + updatedAt: LessThan(addDays(today, -15)), + }, + take: limit, + skip: limit * page, + }); return [items, totalItems]; } async getStockById(bookId: number) { - const stock = await this.vStock - .findOneBy({ bookId }); - if (stock === null) { throw new Error('701'); } + const stock = await this.vStock.findOneBy({ bookId }); + if (stock === null) { + throw new Error('701'); + } return stock; } async updateBook(bookId: number) { - await this - .update(bookId, { updatedAt: new Date() }); + await this.update(bookId, { updatedAt: new Date() }); } } export default StocksRepository; diff --git a/backend/src/v1/stocks/stocks.service.ts b/backend/src/v1/stocks/stocks.service.ts index 1b7e675f..36a00561 100644 --- a/backend/src/v1/stocks/stocks.service.ts +++ b/backend/src/v1/stocks/stocks.service.ts @@ -2,13 +2,10 @@ import jipDataSource from '~/app-data-source'; import { Meta } from '../DTO/common.interface'; import StocksRepository from './stocks.repository'; -export const getAllStocks = async ( - page: number, - limit: number, -) => { +export const getAllStocks = async (page: number, limit: number) => { const stocksRepo = new StocksRepository(); const [items, totalItems] = await stocksRepo.getAllStocksAndCount(limit, page); - const meta:Meta = { + const meta: Meta = { totalItems, itemCount: items.length, itemsPerPage: limit, @@ -18,9 +15,7 @@ export const getAllStocks = async ( return { items, meta }; }; -export const updateBook = async ( - bookId: number, -) => { +export const updateBook = async (bookId: number) => { const transaction = jipDataSource.createQueryRunner(); const stocksRepo = new StocksRepository(transaction); try { @@ -31,7 +26,7 @@ export const updateBook = async ( return stock; } catch (error: any) { await transaction.rollbackTransaction(); - throw (error); + throw error; } finally { await transaction.release(); } diff --git a/backend/src/v1/swagger/swagger.ts b/backend/src/v1/swagger/swagger.ts index e34ba4f2..7e2bbb0d 100644 --- a/backend/src/v1/swagger/swagger.ts +++ b/backend/src/v1/swagger/swagger.ts @@ -5,7 +5,7 @@ const swaggerOptions = { title: '42-jiphyoenjeon web service API', version: '0.1.0', description: - "42-jiphyeonjeon web service, that is, 42library's APIs with Express and documented with Swagger", + "42-jiphyeonjeon web service, that is, 42library's APIs with Express and documented with Swagger", license: { name: 'MIT', url: 'https://spdx.org/licenses/MIT.html', diff --git a/backend/src/v1/tags/tags.controller.ts b/backend/src/v1/tags/tags.controller.ts index e6594624..425cebc9 100644 --- a/backend/src/v1/tags/tags.controller.ts +++ b/backend/src/v1/tags/tags.controller.ts @@ -1,17 +1,11 @@ -import { - NextFunction, Request, Response, -} from 'express'; +import { NextFunction, Request, Response } from 'express'; import * as status from 'http-status'; import ErrorResponse from '~/v1/utils/error/errorResponse'; import * as parseCheck from '~/v1/utils/parseCheck'; import * as errorCode from '~/v1/utils/error/errorCode'; import TagsService from './tags.service'; -export const createDefaultTags = async ( - req: Request, - res: Response, - next: NextFunction, -) => { +export const createDefaultTags = async (req: Request, res: Response, next: NextFunction) => { const { id: tokenId } = req.user as any; const bookInfoId = req?.body?.bookInfoId; const content = req?.body?.content.trim(); @@ -21,7 +15,7 @@ export const createDefaultTags = async ( await tagsService.releaseConnection(); return next(new ErrorResponse(errorCode.INVALID_INPUT_TAGS, 400)); } - if (await tagsService.isValidBookInfoId(parseInt(bookInfoId, 10)) === false) { + if ((await tagsService.isValidBookInfoId(parseInt(bookInfoId, 10))) === false) { await tagsService.releaseConnection(); return next(new ErrorResponse(errorCode.INVALID_BOOKINFO_ID, 400)); } @@ -29,30 +23,27 @@ export const createDefaultTags = async ( await tagsService.releaseConnection(); return next(new ErrorResponse(errorCode.DUPLICATED_SUB_DEFAULT_TAGS, 400)); } - const defaultTagInsertion = await tagsService.createDefaultTags( - tokenId, - bookInfoId, - content, - ); + const defaultTagInsertion = await tagsService.createDefaultTags(tokenId, bookInfoId, content); await tagsService.releaseConnection(); return res.status(status.CREATED).send(defaultTagInsertion); }; -export const createSuperTags = async ( - req: Request, - res: Response, - next: NextFunction, -) => { +export const createSuperTags = async (req: Request, res: Response, next: NextFunction) => { const { id: tokenId } = req.user as any; const bookInfoId = req?.body?.bookInfoId; const content = req?.body?.content.trim(); const tagsService = new TagsService(); const regex = /[^가-힣a-zA-Z0-9_]/g; - if (content === '' || content === 'default' || content.length > 42 || regex.test(content) === true) { + if ( + content === '' || + content === 'default' || + content.length > 42 || + regex.test(content) === true + ) { await tagsService.releaseConnection(); return next(new ErrorResponse(errorCode.INVALID_INPUT_TAGS, 400)); } - if (await tagsService.isValidBookInfoId(parseInt(bookInfoId, 10)) === false) { + if ((await tagsService.isValidBookInfoId(parseInt(bookInfoId, 10))) === false) { await tagsService.releaseConnection(); return next(new ErrorResponse(errorCode.INVALID_BOOKINFO_ID, 400)); } @@ -65,11 +56,7 @@ export const createSuperTags = async ( return res.status(status.CREATED).send(superTagInsertion); }; -export const deleteSuperTags = async ( - req: Request, - res: Response, - next: NextFunction, -) => { +export const deleteSuperTags = async (req: Request, res: Response, next: NextFunction) => { const { id: tokenId } = req.user as any; const superTagId = Number(req?.params?.tagId); const tagsService = new TagsService(); @@ -82,11 +69,7 @@ export const deleteSuperTags = async ( return res.status(status.OK).send(); }; -export const deleteSubTags = async ( - req: Request, - res: Response, - next: NextFunction, -) => { +export const deleteSubTags = async (req: Request, res: Response, next: NextFunction) => { const { id: tokenId } = req.user as any; const subTagId = Number(req?.params?.tagId); const tagsService = new TagsService(); @@ -99,10 +82,7 @@ export const deleteSubTags = async ( return res.status(status.OK).send(); }; -export const searchSubDefaultTags = async ( - req: Request, - res: Response, -) => { +export const searchSubDefaultTags = async (req: Request, res: Response) => { const page: number = parseCheck.pageParse(parseInt(String(req?.query?.page), 10)); const limit: number = parseCheck.limitParse(parseInt(String(req?.query?.limit), 10)); const visibility: string = parseCheck.stringQueryParse(req?.query?.visibility); @@ -118,10 +98,7 @@ export const searchSubDefaultTags = async ( return res.status(status.OK).json(subDefaultTags); }; -export const searchSubTags = async ( - req: Request, - res: Response, -) => { +export const searchSubTags = async (req: Request, res: Response) => { const superTagId: number = parseInt(req.params.superTagId, 10); const tagsService = new TagsService(); const subTags = await tagsService.searchSubTags(superTagId); @@ -129,10 +106,7 @@ export const searchSubTags = async ( return res.status(status.OK).json(subTags); }; -export const searchSuperDefaultTags = async ( - req: Request, - res: Response, -) => { +export const searchSuperDefaultTags = async (req: Request, res: Response) => { const bookInfoId: number = parseInt(req.params.bookInfoId, 10); const tagsService = new TagsService(); const superDefaultTags = await tagsService.searchSuperDefaultTags(bookInfoId); @@ -140,11 +114,7 @@ export const searchSuperDefaultTags = async ( return res.status(status.OK).json(superDefaultTags); }; -export const mergeTags = async ( - req: Request, - res: Response, - next: NextFunction, -) => { +export const mergeTags = async (req: Request, res: Response, next: NextFunction) => { const { id: tokenId } = req.user as any; const bookInfoId = Number(req?.params?.bookInfoId); const superTagId = Number(req?.body?.superTagId); @@ -152,16 +122,15 @@ export const mergeTags = async ( const tagsService = new TagsService(); let returnSuperTagId = 0; - if (await tagsService.isValidBookInfoId(bookInfoId) === false) { + if ((await tagsService.isValidBookInfoId(bookInfoId)) === false) { await tagsService.releaseConnection(); return next(new ErrorResponse(errorCode.INVALID_BOOKINFO_ID, 400)); } - if (superTagId !== 0 - && await tagsService.isValidSuperTagId(superTagId, bookInfoId) === false) { + if (superTagId !== 0 && (await tagsService.isValidSuperTagId(superTagId, bookInfoId)) === false) { await tagsService.releaseConnection(); return next(new ErrorResponse(errorCode.INVALID_TAG_ID, 400)); } - if (await tagsService.isValidSubTagId(subTagIds) === false) { + if ((await tagsService.isValidSubTagId(subTagIds)) === false) { await tagsService.releaseConnection(); return next(new ErrorResponse(errorCode.INVALID_TAG_ID, 400)); } @@ -180,25 +149,26 @@ export const mergeTags = async ( return res.status(status.OK).send({ id: returnSuperTagId }); }; -export const updateSuperTags = async ( - req: Request, - res: Response, - next: NextFunction, -) => { +export const updateSuperTags = async (req: Request, res: Response, next: NextFunction) => { const { id: tokenId } = req.user as any; const superTagId = parseInt(req?.body?.id, 10); const content = req?.body?.content; const tagsService = new TagsService(); const regex = /[^가-힣a-zA-Z0-9_]/g; - if (content === '' || content === 'default' || content.length > 42 || regex.test(content) === true) { + if ( + content === '' || + content === 'default' || + content.length > 42 || + regex.test(content) === true + ) { await tagsService.releaseConnection(); return next(new ErrorResponse(errorCode.INVALID_INPUT_TAGS, 400)); } - if (await tagsService.isExistingSuperTag(superTagId, content) === true) { + if ((await tagsService.isExistingSuperTag(superTagId, content)) === true) { await tagsService.releaseConnection(); return next(new ErrorResponse(errorCode.ALREADY_EXISTING_TAGS, 400)); } - if (await tagsService.isDefaultTag(superTagId) === true) { + if ((await tagsService.isDefaultTag(superTagId)) === true) { await tagsService.releaseConnection(); return next(new ErrorResponse(errorCode.DEFAULT_TAG_ID, 400)); } @@ -212,11 +182,7 @@ export const updateSuperTags = async ( return res.status(status.OK).send({ id: superTagId }); }; -export const updateSubTags = async ( - req: Request, - res: Response, - next: NextFunction, -) => { +export const updateSubTags = async (req: Request, res: Response, next: NextFunction) => { const { id: tokenId } = req.user as any; const subTagId = parseInt(req?.body?.id, 10); const visibility = req?.body?.visibility; @@ -225,7 +191,7 @@ export const updateSubTags = async ( await tagsService.releaseConnection(); return next(new ErrorResponse(errorCode.INVALID_INPUT_TAGS, 400)); } - if (await tagsService.isExistingSubTag(subTagId) === false) { + if ((await tagsService.isExistingSubTag(subTagId)) === false) { await tagsService.releaseConnection(); return next(new ErrorResponse(errorCode.INVALID_TAG_ID, 400)); } @@ -239,10 +205,7 @@ export const updateSubTags = async ( return res.status(status.OK).send({ id: subTagId }); }; -export const searchMainTags = async ( - req: Request, - res: Response, -) => { +export const searchMainTags = async (req: Request, res: Response) => { const limit: number = req.query.limit === undefined || null ? 100 : Number(req.query.limit); const tagsService = new TagsService(); const mainTags = await tagsService.searchMainTags(limit); diff --git a/backend/src/v1/tags/tags.repository.ts b/backend/src/v1/tags/tags.repository.ts index 39004f83..1ca59c02 100644 --- a/backend/src/v1/tags/tags.repository.ts +++ b/backend/src/v1/tags/tags.repository.ts @@ -3,7 +3,12 @@ import { In, QueryRunner, Repository } from 'typeorm'; import { QueryDeepPartialEntity } from 'typeorm/query-builder/QueryPartialEntity.js'; import jipDataSource from '~/app-data-source'; import { - BookInfo, SubTag, SuperTag, User, VTagsSubDefault, VTagsSuperDefault, + BookInfo, + SubTag, + SuperTag, + User, + VTagsSubDefault, + VTagsSuperDefault, } from '~/entity/entities'; import { subDefaultTag, superDefaultTag } from '../DTO/tags.model'; @@ -17,14 +22,15 @@ export class SubTagRepository extends Repository { const entityManager = jipDataSource.createEntityManager(queryRunner); super(SubTag, entityManager); this.entityManager = entityManager; - this.vSubDefaultRepo = new Repository( - VTagsSubDefault, - entityManager, - ); + this.vSubDefaultRepo = new Repository(VTagsSubDefault, entityManager); } - async createDefaultTags(userId: number, bookInfoId: number, content: string, superTagId: number) - : Promise { + async createDefaultTags( + userId: number, + bookInfoId: number, + content: string, + superTagId: number, + ): Promise { const insertObject: QueryDeepPartialEntity = { superTagId, userId, @@ -42,11 +48,7 @@ export class SubTagRepository extends Repository { async getSubTags(conditions: object) { const subTags = await this.vSubDefaultRepo.find({ - select: [ - 'id', - 'content', - 'login', - ], + select: ['id', 'content', 'login'], where: conditions, }); return subTags; @@ -66,8 +68,7 @@ export class SubTagRepository extends Repository { ); } - async countSubTag(conditions: object) - : Promise { + async countSubTag(conditions: object): Promise { const count = await this.count({ where: conditions, }); @@ -75,10 +76,7 @@ export class SubTagRepository extends Repository { } async updateSubTags(userId: number, subTagId: number, isPublic: number) { - await this.update( - { id: subTagId }, - { isPublic, updateUserId: userId, updatedAt: new Date() }, - ); + await this.update({ id: subTagId }, { isPublic, updateUserId: userId, updatedAt: new Date() }); } } @@ -98,18 +96,9 @@ export class SuperTagRepository extends Repository { const entityManager = jipDataSource.createEntityManager(queryRunner); super(SuperTag, entityManager); this.entityManager = entityManager; - this.vSubDefaultRepo = new Repository( - VTagsSubDefault, - this.entityManager, - ); - this.userRepo = new Repository( - User, - this.entityManager, - ); - this.bookInfoRepo = new Repository( - BookInfo, - this.entityManager, - ); + this.vSubDefaultRepo = new Repository(VTagsSubDefault, this.entityManager); + this.userRepo = new Repository(User, this.entityManager); + this.bookInfoRepo = new Repository(BookInfo, this.entityManager); this.vSuperDefaultRepo = new Repository( VTagsSuperDefault, this.entityManager, @@ -126,22 +115,15 @@ export class SuperTagRepository extends Repository { async getSuperTags(conditions: object) { const superTags = await this.find({ - select: [ - 'id', - 'content', - 'bookInfoId', - ], + select: ['id', 'content', 'bookInfoId'], where: conditions, }); return superTags; } - async getDefaultTag(bookInfoId: number) - : Promise { + async getDefaultTag(bookInfoId: number): Promise { const defaultTag = await this.findOne({ - select: [ - 'id', - ], + select: ['id'], where: { bookInfoId, content: 'default', @@ -150,8 +132,7 @@ export class SuperTagRepository extends Repository { return defaultTag; } - async createSuperTag(content: string, bookInfoId: number, userId: number) - : Promise { + async createSuperTag(content: string, bookInfoId: number, userId: number): Promise { const insertObject: QueryDeepPartialEntity = { userId, bookInfoId, @@ -166,8 +147,11 @@ export class SuperTagRepository extends Repository { await this.update(superTagsId, { isDeleted: 1, updateUserId: deleteUser }); } - async getSubAndSuperTags(page: number, limit: number, conditions: Object) - : Promise<[subDefaultTag[], number]> { + async getSubAndSuperTags( + page: number, + limit: number, + conditions: Object, + ): Promise<[subDefaultTag[], number]> { const [items, count] = await this.vSubDefaultRepo.findAndCount({ select: [ 'bookInfoId', @@ -188,35 +172,35 @@ export class SuperTagRepository extends Repository { return [convertedItems, count]; } - async getSuperTagsWithSubCount(bookInfoId: number) - : Promise { + async getSuperTagsWithSubCount(bookInfoId: number): Promise { const superTags = await this.createQueryBuilder('sp') .select('sp.id', 'id') .addSelect('sp.content', 'content') .addSelect('NULL', 'login') - .addSelect((subQuery) => subQuery - .select('COUNT(sb.id)', 'count') - .from(SubTag, 'sb') - .where('sb.superTagId = sp.id AND sb.isDeleted IS FALSE AND sb.isPublic IS TRUE'), 'count') - .where('sp.bookInfoId = :bookInfoId AND sp.content != \'default\' AND sp.isDeleted IS FALSE', { bookInfoId }) + .addSelect( + (subQuery) => + subQuery + .select('COUNT(sb.id)', 'count') + .from(SubTag, 'sb') + .where('sb.superTagId = sp.id AND sb.isDeleted IS FALSE AND sb.isPublic IS TRUE'), + 'count', + ) + .where("sp.bookInfoId = :bookInfoId AND sp.content != 'default' AND sp.isDeleted IS FALSE", { + bookInfoId, + }) .getRawMany(); return superTags as superDefaultTag[]; } - async countSuperTag(conditions: object) - : Promise { + async countSuperTag(conditions: object): Promise { const count = await this.count({ where: conditions, }); return count; } - async updateSuperTags(updateUserId: number, superTagId: number, content: string) - : Promise { - await this.update( - { id: superTagId }, - { content, updateUserId, updatedAt: new Date() }, - ); + async updateSuperTags(updateUserId: number, superTagId: number, content: string): Promise { + await this.update({ id: superTagId }, { content, updateUserId, updatedAt: new Date() }); } async countBookInfoId(bookInfoId: number): Promise { diff --git a/backend/src/v1/tags/tags.service.ts b/backend/src/v1/tags/tags.service.ts index 928750e2..5b293d3f 100644 --- a/backend/src/v1/tags/tags.service.ts +++ b/backend/src/v1/tags/tags.service.ts @@ -7,11 +7,11 @@ import { SubTagRepository, SuperTagRepository } from './tags.repository'; import { superDefaultTag } from '../DTO/tags.model'; export class TagsService { - private readonly subTagRepository : SubTagRepository; + private readonly subTagRepository: SubTagRepository; - private readonly superTagRepository : SuperTagRepository; + private readonly superTagRepository: SuperTagRepository; - private readonly queryRunner : QueryRunner; + private readonly queryRunner: QueryRunner; constructor() { this.queryRunner = jipDataSource.createQueryRunner(); @@ -58,8 +58,12 @@ export class TagsService { return defaultTagsInsertion; } - async searchSubDefaultTags(page: number, limit: number, visibility: string, query: string) - : Promise { + async searchSubDefaultTags( + page: number, + limit: number, + visibility: string, + query: string, + ): Promise { const conditions: Array = []; const deleteAndVisibility: any = { isDeleted: 0, isPublic: null }; @@ -81,12 +85,14 @@ export class TagsService { limit, conditions, ); - const itemPerPage = (Number.isNaN(limit)) ? 10 : limit; + const itemPerPage = Number.isNaN(limit) ? 10 : limit; const meta = { totalItems: count, itemPerPage, - totalPages: parseInt(String(count / itemPerPage - + Number((count % itemPerPage !== 0) || !count)), 10), + totalPages: parseInt( + String(count / itemPerPage + Number(count % itemPerPage !== 0 || !count)), + 10, + ), firstPage: page === 0, finalPage: page === parseInt(String(count / itemPerPage), 10), currentPage: page, @@ -113,13 +119,11 @@ export class TagsService { })); const defaultTag = await this.superTagRepository.getDefaultTag(bookInfoId); if (defaultTag) { - const defaultTags = await this.subTagRepository.getSubTags( - { - superTagId: defaultTag.id, - isPublic: 1, - isDeleted: 0, - }, - ); + const defaultTags = await this.subTagRepository.getSubTags({ + superTagId: defaultTag.id, + isPublic: 1, + isDeleted: 0, + }); defaultTags.forEach((dt) => { superDefaultTags.push({ id: dt.id, @@ -186,12 +190,7 @@ export class TagsService { return subTagCount > 0; } - async mergeTags( - bookInfoId: number, - subTagIds: number[], - rawSuperTagId: number, - userId: number, - ) { + async mergeTags(bookInfoId: number, subTagIds: number[], rawSuperTagId: number, userId: number) { let superTagId = 0; try { @@ -200,8 +199,12 @@ export class TagsService { const defaultTag = await this.superTagRepository.getDefaultTag(bookInfoId); if (defaultTag === null) { superTagId = await this.superTagRepository.createSuperTag('default', bookInfoId, userId); - } else { superTagId = defaultTag.id; } - } else { superTagId = rawSuperTagId; } + } else { + superTagId = defaultTag.id; + } + } else { + superTagId = rawSuperTagId; + } await this.subTagRepository.mergeTags(subTagIds, superTagId, userId); await this.queryRunner.commitTransaction(); } catch (e) { @@ -216,9 +219,7 @@ export class TagsService { async isExistingSuperTag(superTagId: number, content: string): Promise { const superTag: SuperTag[] = await this.superTagRepository.getSuperTags({ id: superTagId }); const { bookInfoId } = superTag[0]; - const duplicates: number = await this.superTagRepository.countSuperTag( - { content, bookInfoId }, - ); + const duplicates: number = await this.superTagRepository.countSuperTag({ content, bookInfoId }); if (duplicates === 0) { return false; } @@ -265,7 +266,7 @@ export class TagsService { } async updateSubTags(userId: number, subTagId: number, visibility: string): Promise { - const isPublic = (visibility === 'public') ? 1 : 0; + const isPublic = visibility === 'public' ? 1 : 0; try { await this.queryRunner.startTransaction(); await this.subTagRepository.updateSubTags(userId, subTagId, isPublic); diff --git a/backend/src/v1/users/users.controller.spec.ts b/backend/src/v1/users/users.controller.spec.ts index bc984af8..3f964d57 100644 --- a/backend/src/v1/users/users.controller.spec.ts +++ b/backend/src/v1/users/users.controller.spec.ts @@ -3,18 +3,23 @@ import { searchSchema } from './users.types'; describe('searchSchema query', () => { test('regular query', () => { const data = { - id: 1, nicknameOrEmail: 'test', page: 1, limit: 1, + id: 1, + nicknameOrEmail: 'test', + page: 1, + limit: 1, }; expect(searchSchema.safeParse(data)).toEqual({ success: true, data }); }); test('default value for empty query', () => { - expect(searchSchema.safeParse({})) - .toEqual({ success: true, data: { page: 0, limit: 5 } }); + expect(searchSchema.safeParse({})).toEqual({ success: true, data: { page: 0, limit: 5 } }); }); test('id should be parseable to number', () => { const error = { - id: 'abcd', nicknameOrEmail: 'test', page: 1, limit: 1, + id: 'abcd', + nicknameOrEmail: 'test', + page: 1, + limit: 1, }; const parseResult = searchSchema.safeParse(error); diff --git a/backend/src/v1/users/users.controller.ts b/backend/src/v1/users/users.controller.ts index 0b5ffda0..b7aec194 100644 --- a/backend/src/v1/users/users.controller.ts +++ b/backend/src/v1/users/users.controller.ts @@ -11,39 +11,33 @@ import { searchSchema } from './users.types'; const usersService = new UsersService(); -export const search = async ( - req: Request, - res: Response, - next: NextFunction, -) => { +export const search = async (req: Request, res: Response, next: NextFunction) => { const parsed = searchSchema.safeParse(req.query); if (!parsed.success) { return next(new ErrorResponse(errorCode.INVALID_INPUT, status.BAD_REQUEST)); } - const { - id, nicknameOrEmail, page, limit, - } = parsed.data; + const { id, nicknameOrEmail, page, limit } = parsed.data; let items; try { if (!nicknameOrEmail && !id) { items = await usersService.searchAllUsers(limit, page); } else if (nicknameOrEmail && !id) { - items = JSON.parse(JSON.stringify( - await usersService.searchUserBynicknameOrEmail(nicknameOrEmail, limit, page), - )); + items = JSON.parse( + JSON.stringify( + await usersService.searchUserBynicknameOrEmail(nicknameOrEmail, limit, page), + ), + ); } else if (!nicknameOrEmail && id) { - items = JSON.parse(JSON.stringify( - await usersService.searchUserById(id), - )); + items = JSON.parse(JSON.stringify(await usersService.searchUserById(id))); } if (items) { - items.items = await Promise.all(items.items.map(async (data: User) => ({ - ...data, - lendings: - await usersService.userLendings(data.id), - reservations: - await usersService.userReservations(data.id), - }))); + items.items = await Promise.all( + items.items.map(async (data: User) => ({ + ...data, + lendings: await usersService.userLendings(data.id), + reservations: await usersService.userReservations(data.id), + })), + ); } return res.json(items); } catch (error: any) { @@ -72,9 +66,12 @@ export const create = async (req: Request, res: Response, next: NextFunction) => } try { pwSchema - .is().min(10) - .is().max(42) /* eslint-disable-next-line newline-per-chained-call */ - .has().digits(1) /* eslint-disable-next-line newline-per-chained-call */ + .is() + .min(10) + .is() + .max(42) /* eslint-disable-next-line newline-per-chained-call */ + .has() + .digits(1) /* eslint-disable-next-line newline-per-chained-call */ .symbols(1); if (!pwSchema.validate(String(password))) { return next(new ErrorResponse(errorCode.INVALIDATE_PASSWORD, status.BAD_REQUEST)); @@ -97,16 +94,13 @@ export const create = async (req: Request, res: Response, next: NextFunction) => return 0; }; -export const update = async ( - req: Request, - res: Response, - next: NextFunction, -) => { +export const update = async (req: Request, res: Response, next: NextFunction) => { const { id } = req.params; - const { - nickname = '', intraId = 0, slack = '', role = -1, penaltyEndDate = '', - } = req.body; - if (!id || !(nickname !== '' || intraId !== 0 || slack !== '' || role !== -1 || penaltyEndDate !== '')) { + const { nickname = '', intraId = 0, slack = '', role = -1, penaltyEndDate = '' } = req.body; + if ( + !id || + !(nickname !== '' || intraId !== 0 || slack !== '' || role !== -1 || penaltyEndDate !== '') + ) { return next(new ErrorResponse(errorCode.INVALID_INPUT, status.BAD_REQUEST)); } try { @@ -136,15 +130,9 @@ export const update = async ( return 0; }; -export const myupdate = async ( - req: Request, - res: Response, - next: NextFunction, -) => { +export const myupdate = async (req: Request, res: Response, next: NextFunction) => { const { id: tokenId } = req.user as any; - const { - email = '', password = '0', - } = req.body; + const { email = '', password = '0' } = req.body; if (email === '' && password === '0') { return next(new ErrorResponse(errorCode.INVALID_INPUT, status.BAD_REQUEST)); } @@ -154,16 +142,21 @@ export const myupdate = async ( } else if (email === '' && password !== '0') { const pwSchema = new PasswordValidator(); pwSchema - .is().min(10) - .is().max(42) /* eslint-disable-next-line newline-per-chained-call */ - .has().lowercase() /* eslint-disable-next-line newline-per-chained-call */ - .has().digits(1) /* eslint-disable-next-line newline-per-chained-call */ + .is() + .min(10) + .is() + .max(42) /* eslint-disable-next-line newline-per-chained-call */ + .has() + .lowercase() /* eslint-disable-next-line newline-per-chained-call */ + .has() + .digits(1) /* eslint-disable-next-line newline-per-chained-call */ .symbols(1); if (!pwSchema.validate(password)) { return next(new ErrorResponse(errorCode.INVALIDATE_PASSWORD, status.BAD_REQUEST)); } await usersService.updateUserPassword(parseInt(tokenId, 10), bcrypt.hashSync(password, 10)); - } res.status(200).send('success'); + } + res.status(200).send('success'); } catch (error: any) { const errorNumber = parseInt(error.message, 10); if (errorNumber >= 200 && errorNumber < 300) { @@ -184,10 +177,23 @@ export const myupdate = async ( return 0; }; -export const getVersion = async ( +export const mydata = async ( req: Request, res: Response, ) => { + const { id: tokenId } = req.user as any; + try { + const user = await usersService.searchUserById(parseInt(tokenId, 10)); + if (user.items.length === 0) return res.status(404).send('Not Found'); + return res.status(200).json(user.items[0]); + } catch (error: any) { + logger.error(error); + return res.status(500).send('Internal Server Error'); + } +}; + +export const getVersion = async (req: Request, res: Response) => { + res.status(200).send({ version: 'gshim.v1' }); return 0; }; diff --git a/backend/src/v1/users/users.repository.ts b/backend/src/v1/users/users.repository.ts index 310c330f..a210982d 100644 --- a/backend/src/v1/users/users.repository.ts +++ b/backend/src/v1/users/users.repository.ts @@ -3,7 +3,11 @@ import { Repository } from 'typeorm'; import { formatDate } from '~/v1/utils/dateFormat'; import jipDataSource from '~/app-data-source'; import { - VUserLending, VLendingForSearchUser, Reservation, UserReservation, User, + VUserLending, + VLendingForSearchUser, + Reservation, + UserReservation, + User, } from '~/entity/entities'; import * as models from '../DTO/users.model'; @@ -16,42 +20,26 @@ export default class UsersRepository extends Repository { private readonly userReservRepo: Repository; - constructor( - queryRunner?: QueryRunner, - ) { + constructor(queryRunner?: QueryRunner) { const qr = queryRunner; const manager = jipDataSource.createEntityManager(qr); super(User, manager); - this.userLendingRepo = new Repository( - VUserLending, - manager, - ); + this.userLendingRepo = new Repository(VUserLending, manager); this.lendingForSearchUserRepo = new Repository( VLendingForSearchUser, manager, ); - this.reservationsRepo = new Repository( - Reservation, - manager, - ); - this.userReservRepo = new Repository( - UserReservation, - manager, - ); + this.reservationsRepo = new Repository(Reservation, manager); + this.userReservRepo = new Repository(UserReservation, manager); } - async searchUserBy(conditions: {}, limit: number, page: number) - : Promise<[models.User[], number]> { + async searchUserBy( + conditions: {}, + limit: number, + page: number, + ): Promise<[models.User[], number]> { const [users, count] = await this.findAndCount({ - select: [ - 'id', - 'email', - 'nickname', - 'intraId', - 'slack', - 'penaltyEndDate', - 'role', - ], + select: ['id', 'email', 'nickname', 'intraId', 'slack', 'penaltyEndDate', 'role'], where: conditions, take: limit, skip: page * limit, @@ -68,19 +56,13 @@ export default class UsersRepository extends Repository { /** * @warning : use only password needed */ - async searchUserWithPasswordBy(conditions: {}, limit: number, page: number) - : Promise<[models.PrivateUser[], number]> { + async searchUserWithPasswordBy( + conditions: {}, + limit: number, + page: number, + ): Promise<[models.PrivateUser[], number]> { const [users, count] = await this.findAndCount({ - select: [ - 'id', - 'email', - 'nickname', - 'intraId', - 'slack', - 'penaltyEndDate', - 'role', - 'password', - ], + select: ['id', 'email', 'nickname', 'intraId', 'slack', 'penaltyEndDate', 'role', 'password'], where: conditions, take: limit, skip: page * limit, @@ -94,7 +76,7 @@ export default class UsersRepository extends Repository { return [customUsers, count]; } - async getLending(users: { userId: number; }[]) { + async getLending(users: { userId: number }[]) { if (users.length !== 0) return this.userLendingRepo.find({ where: users }); return this.userLendingRepo.find(); } @@ -109,11 +91,11 @@ export default class UsersRepository extends Repository { } async getUserLendings(userId: number) { - const userLendingList = await this.lendingForSearchUserRepo.find({ + const userLendingList = (await this.lendingForSearchUserRepo.find({ where: { userId, }, - }) as unknown as models.Lending[]; + })) as unknown as models.Lending[]; return userLendingList; } @@ -136,12 +118,8 @@ export default class UsersRepository extends Repository { }); } - async updateUser(id: number, values: {}) - : Promise { - const updatedUser = await this.update( - id, - values, - ) as unknown as models.User; + async updateUser(id: number, values: {}): Promise { + const updatedUser = (await this.update(id, values)) as unknown as models.User; return updatedUser; } } diff --git a/backend/src/v1/users/users.service.spec.ts b/backend/src/v1/users/users.service.spec.ts index 30b07530..5ad4c826 100644 --- a/backend/src/v1/users/users.service.spec.ts +++ b/backend/src/v1/users/users.service.spec.ts @@ -12,16 +12,15 @@ describe('UsersService', () => { jest.setTimeout(10 * 1000); let queryRunner: QueryRunner; beforeAll(async () => { - await jipDataSource.initialize().then( - () => { + await jipDataSource + .initialize() + .then(() => { logger.info('typeORM INIT SUCCESS'); logger.info(connectMode); - }, - ).catch( - (e) => { + }) + .catch((e) => { logger.error(`typeORM INIT FAILED : ${e.message}`); - }, - ); + }); // 트랜잭션 사전작업 queryRunner = jipDataSource.createQueryRunner(); await queryRunner.connect(); @@ -64,30 +63,8 @@ describe('UsersService', () => { // searchUserById it('searchUserById()', async () => { - expect(await usersService.searchUserById(1414)).toStrictEqual( - { - items: [ - { - id: 1414, - email: 'example_role1_7@gmail.com', - password: '4444', - nickname: 'hihi', - intraId: 44, - slack: 'dasdwqwe1132', - penaltyEndDay: new Date(Date.parse('2022-05-20 07:02:34')), - role: 1, - createdAt: new Date(Date.parse('2022-05-20 07:02:34.973193')), - updatedAt: new Date(Date.parse('2022-05-20 16:13:39.314069')), - }, - ], - }, - ); - }); - - // searchUserByIntraId - it('searchUserByIntraId()', async () => { - expect(await usersService.searchUserByIntraId(44)).toStrictEqual( - [ + expect(await usersService.searchUserById(1414)).toStrictEqual({ + items: [ { id: 1414, email: 'example_role1_7@gmail.com', @@ -101,7 +78,25 @@ describe('UsersService', () => { updatedAt: new Date(Date.parse('2022-05-20 16:13:39.314069')), }, ], - ); + }); + }); + + // searchUserByIntraId + it('searchUserByIntraId()', async () => { + expect(await usersService.searchUserByIntraId(44)).toStrictEqual([ + { + id: 1414, + email: 'example_role1_7@gmail.com', + password: '4444', + nickname: 'hihi', + intraId: 44, + slack: 'dasdwqwe1132', + penaltyEndDay: new Date(Date.parse('2022-05-20 07:02:34')), + role: 1, + createdAt: new Date(Date.parse('2022-05-20 07:02:34.973193')), + updatedAt: new Date(Date.parse('2022-05-20 16:13:39.314069')), + }, + ]); }); // searchAllUsers diff --git a/backend/src/v1/users/users.service.ts b/backend/src/v1/users/users.service.ts index b641e6a9..ee4bdba6 100644 --- a/backend/src/v1/users/users.service.ts +++ b/backend/src/v1/users/users.service.ts @@ -5,7 +5,7 @@ import * as types from '../DTO/common.interface'; import UsersRepository from './users.repository'; export default class UsersService { - private readonly usersRepository : UsersRepository; + private readonly usersRepository: UsersRepository; constructor() { this.usersRepository = new UsersRepository(); @@ -19,8 +19,9 @@ export default class UsersService { */ async withLendingInfo(users: models.User[]): Promise { const usersIdList = users.map((user) => ({ userId: user.id })); - const lending = await this.usersRepository - .getLending(usersIdList) as unknown as models.Lending[]; + const lending = (await this.usersRepository.getLending( + usersIdList, + )) as unknown as models.Lending[]; return users.map((user) => { const lendings = lending.filter((lend) => lend.userId === user.id); @@ -40,10 +41,11 @@ export default class UsersService { } async searchUserBynicknameOrEmail(nicknameOrEmail: string, limit: number, page: number) { - const [items, count] = await this.usersRepository.searchUserBy([ - { nickname: Like(`%${nicknameOrEmail}%`) }, - { email: Like(`%${nicknameOrEmail}`) }, - ], limit, page); + const [items, count] = await this.usersRepository.searchUserBy( + [{ nickname: Like(`%${nicknameOrEmail}%`) }, { email: Like(`%${nicknameOrEmail}`) }], + limit, + page, + ); const setItems = await this.withLendingInfo(items); const meta: types.Meta = { totalItems: count, @@ -67,7 +69,9 @@ export default class UsersService { } async searchUserWithPasswordByEmail(email: string) { - const items = (await this.usersRepository.searchUserWithPasswordBy({ email: Like(`%${email}%`) }, 0, 0))[0]; + const items = ( + await this.usersRepository.searchUserWithPasswordBy({ email: Like(`%${email}%`) }, 0, 0) + )[0]; return { items }; } @@ -98,7 +102,7 @@ export default class UsersService { return null; } - async updateUserEmail(id: number, email:string) { + async updateUserEmail(id: number, email: string) { const emailCount = (await this.usersRepository.searchUserBy({ email }, 0, 0))[1]; if (emailCount > 0) { throw new Error(errorCode.EMAIL_OVERLAP); @@ -118,16 +122,18 @@ export default class UsersService { role: number, penaltyEndDate: string, ) { - const nicknameCount = (await this.usersRepository - .searchUserBy({ nickname, id: Not(id) }, 0, 0))[1]; + const nicknameCount = ( + await this.usersRepository.searchUserBy({ nickname, id: Not(id) }, 0, 0) + )[1]; if (nicknameCount > 0) { throw new Error(errorCode.NICKNAME_OVERLAP); } if (!(role >= 0 && role <= 3)) { throw new Error(errorCode.INVALID_ROLE); } - const slackCount = (await this.usersRepository - .searchUserBy({ nickname, slack: Not(slack) }, 0, 0))[1]; + const slackCount = ( + await this.usersRepository.searchUserBy({ nickname, slack: Not(slack) }, 0, 0) + )[1]; if (slackCount > 0) { throw new Error(errorCode.SLACK_OVERLAP); } @@ -141,6 +147,6 @@ export default class UsersService { updateParam.penaltyEndDate = penaltyEndDate; } const updatedUser = this.usersRepository.updateUser(id, updateParam); - return (updatedUser); + return updatedUser; } } diff --git a/backend/src/v1/users/users.types.ts b/backend/src/v1/users/users.types.ts index 2c7b86f9..1ad3d274 100644 --- a/backend/src/v1/users/users.types.ts +++ b/backend/src/v1/users/users.types.ts @@ -7,6 +7,4 @@ export const searchSchema = z.object({ limit: z.coerce.number().min(1).default(5), }); -export const createSchema = z.object({ - -}); +export const createSchema = z.object({}); diff --git a/backend/src/v1/users/users.utils.ts b/backend/src/v1/users/users.utils.ts index 36d27fe8..79507cbb 100644 --- a/backend/src/v1/users/users.utils.ts +++ b/backend/src/v1/users/users.utils.ts @@ -1,6 +1,6 @@ /* eslint-disable import/prefer-default-export */ /* 추후 여러 utils 함수들이 추가될 거 생각해서, default export를 안 넣어둠 */ -export const isLibrian = (role : number):boolean => { +export const isLibrian = (role: number): boolean => { if (role === 2) return true; return false; }; diff --git a/backend/src/v1/utils/dateFormat.ts b/backend/src/v1/utils/dateFormat.ts index 5875c82a..b187e900 100644 --- a/backend/src/v1/utils/dateFormat.ts +++ b/backend/src/v1/utils/dateFormat.ts @@ -6,6 +6,8 @@ function leftPad(value: number) { } export const formatDate = (date: Date) => { - const formatted_date = `${date.getFullYear()}-${leftPad(date.getMonth() + 1)}-${leftPad(date.getDate())}`; + const formatted_date = `${date.getFullYear()}-${leftPad(date.getMonth() + 1)}-${leftPad( + date.getDate(), + )}`; return formatted_date; }; diff --git a/backend/src/v1/utils/error/errorCode.ts b/backend/src/v1/utils/error/errorCode.ts index 9cb9adce..773fdc9d 100644 --- a/backend/src/v1/utils/error/errorCode.ts +++ b/backend/src/v1/utils/error/errorCode.ts @@ -86,4 +86,5 @@ export const DUPLICATED_SUPER_TAGS = '908'; export const DUPLICATED_SUB_DEFAULT_TAGS = '909'; export const INVALID_TAG_ID = '910'; -export const CLIENT_AUTH_FAILED_ERROR_MESSAGE = 'Client authentication failed due to unknown client, no client authentication included, or unsupported authentication method.'; +export const CLIENT_AUTH_FAILED_ERROR_MESSAGE = + 'Client authentication failed due to unknown client, no client authentication included, or unsupported authentication method.'; diff --git a/backend/src/v1/utils/error/errorHandler.ts b/backend/src/v1/utils/error/errorHandler.ts index e9aca049..7becd989 100644 --- a/backend/src/v1/utils/error/errorHandler.ts +++ b/backend/src/v1/utils/error/errorHandler.ts @@ -21,7 +21,10 @@ export default function errorHandler( ); } else error = err as ErrorResponse; if (parseInt(error.errorCode, 10) === 42) { - res.status(error.status).json({ errorCode: parseInt(error.errorCode, 10), message: '42키값 업데이트가 필요합니다. 키값 업데이트까지는 일반 로그인을 이용해주세요.' }); + res.status(error.status).json({ + errorCode: parseInt(error.errorCode, 10), + message: '42키값 업데이트가 필요합니다. 키값 업데이트까지는 일반 로그인을 이용해주세요.', + }); } res.status(error.status).json({ errorCode: parseInt(error.errorCode, 10) }); } diff --git a/backend/src/v1/utils/isNullish.ts b/backend/src/v1/utils/isNullish.ts index f6ed0d4d..405e21c9 100644 --- a/backend/src/v1/utils/isNullish.ts +++ b/backend/src/v1/utils/isNullish.ts @@ -1,3 +1,3 @@ export default function isNullish(value: unknown) { - return (value === null || value === undefined); + return value === null || value === undefined; } diff --git a/backend/src/v1/utils/parseCheck.ts b/backend/src/v1/utils/parseCheck.ts index 596d08b3..23514262 100644 --- a/backend/src/v1/utils/parseCheck.ts +++ b/backend/src/v1/utils/parseCheck.ts @@ -1,32 +1,20 @@ -export const sortParse = ( - sort : any, -) : 'ASC' | 'DESC' => { +export const sortParse = (sort: any): 'ASC' | 'DESC' => { if (sort === 'asc' || sort === 'desc' || sort === 'ASC' || sort === 'DESC') { return sort.toUpperCase(); } return 'DESC'; }; -export const pageParse = ( - page : number, -) : number => (Number.isNaN(page) ? 0 : page); +export const pageParse = (page: number): number => (Number.isNaN(page) ? 0 : page); -export const limitParse = ( - limit : number, -) : number => (Number.isNaN(limit) ? 10 : limit); +export const limitParse = (limit: number): number => (Number.isNaN(limit) ? 10 : limit); -export const stringQueryParse = ( - stringQuery : any, -) : string => ((stringQuery === undefined || null) ? '' : stringQuery.trim()); +export const stringQueryParse = (stringQuery: any): string => + stringQuery === undefined || null ? '' : stringQuery.trim(); -export const booleanQueryParse = ( - booleanQuery : any, -) : boolean => (booleanQuery === 'true'); +export const booleanQueryParse = (booleanQuery: any): boolean => booleanQuery === 'true'; -export const disabledParse = ( - disabled : number, -) : number => (Number.isNaN(disabled) ? -1 : disabled); +export const disabledParse = (disabled: number): number => (Number.isNaN(disabled) ? -1 : disabled); -export const visibilityParse = ( - visibility : string, -) : string => ((visibility === undefined || null) ? '' : visibility); +export const visibilityParse = (visibility: string): string => + visibility === undefined || null ? '' : visibility; diff --git a/backend/src/v1/utils/types.ts b/backend/src/v1/utils/types.ts index 19157d71..fea013d4 100644 --- a/backend/src/v1/utils/types.ts +++ b/backend/src/v1/utils/types.ts @@ -1,5 +1,5 @@ import { RowDataPacket } from 'mysql2'; export type StringRows = RowDataPacket & { - str: string -} + str: string; +}; diff --git a/backend/src/v2/books/errors.ts b/backend/src/v2/books/errors.ts new file mode 100644 index 00000000..f307a577 --- /dev/null +++ b/backend/src/v2/books/errors.ts @@ -0,0 +1,23 @@ +export class PubdateFormatError extends Error { + declare readonly _tag: 'FormatError'; + + constructor(exp: string) { + super(`${exp}가 지정된 포맷과 일치하지 않습니다.`); + } +} + +export class IsbnNotFoundError extends Error { + declare readonly _tag: 'ISBN_NOT_FOUND'; + + constructor(exp: string) { + super(`국립중앙도서관 API에서 ISBN(${exp}) 검색이 실패하였습니다.`); + } +} + +export class NaverBookNotFound extends Error { + declare readonly _tag: 'NAVER_BOOK_NOT_FOUND'; + + constructor(exp: string) { + super(`네이버 책검색 API에서 ISBN(${exp}) 검색이 실패하였습니다.`); + } +} diff --git a/backend/src/v2/books/mod.ts b/backend/src/v2/books/mod.ts new file mode 100644 index 00000000..a7cccbd3 --- /dev/null +++ b/backend/src/v2/books/mod.ts @@ -0,0 +1,106 @@ +// import { contract } from '@jiphyeonjeon-42/contracts'; +// import { initServer } from '@ts-rest/express'; +// import { +// searchAllBooks, +// searchBookById, +// searchBookInfoById, +// searchBookInfoForCreate, +// searchBookInfosByTag, +// searchBookInfosSorted, +// updateBookDonator, +// updateBookOrBookInfo, +// } from './service'; +// import { +// BookInfoNotFoundError, +// BookNotFoundError, +// bookInfoNotFound, +// bookNotFound, +// isbnNotFound, +// naverBookNotFound, +// pubdateFormatError, +// } from '../shared'; +// import { IsbnNotFoundError, NaverBookNotFound, PubdateFormatError } from './errors'; +// import authValidate from '~/v1/auth/auth.validate'; +// import { roleSet } from '~/v1/auth/auth.type'; + +// const s = initServer(); +// export const books = s.router(contract.books, { +// // searchAllBookInfos: async ({ query }) => { +// // const result = await searchAllBookInfos(query); + +// // return { status: 200, body: result } as const; +// // }, +// searchBookInfosByTag: async ({ query }) => { +// const result = await searchBookInfosByTag(query); + +// return { status: 200, body: result } as const; +// }, +// searchBookInfosSorted: async ({ query }) => { +// const result = await searchBookInfosSorted(query); + +// return { status: 200, body: result } as const; +// }, +// searchBookInfoById: async ({ params: { id } }) => { +// const result = await searchBookInfoById(id); + +// if (result instanceof BookInfoNotFoundError) return bookInfoNotFound; + +// return { status: 200, body: result } as const; +// }, +// searchAllBooks: async ({ query }) => { +// const result = await searchAllBooks(query); + +// return { status: 200, body: result } as const; +// }, +// searchBookInfoForCreate: { +// // middleware: [authValidate(roleSet.librarian)], +// handler: async ({ query: { isbnQuery } }) => { +// const result = await searchBookInfoForCreate(isbnQuery); + +// if (result instanceof IsbnNotFoundError) return isbnNotFound; + +// if (result instanceof NaverBookNotFound) return naverBookNotFound; + +// return { status: 200, body: result } as const; +// }, +// }, +// searchBookById: async ({ params: { id } }) => { +// const result = await searchBookById({ id }); + +// if (result instanceof BookNotFoundError) { +// return bookNotFound; +// } + +// return { +// status: 200, +// body: result, +// } as const; +// }, +// // createBook: { +// // middleware: [authValidate(roleSet.librarian)], +// // handler: async ({ body }) => { + +// // } +// // }, +// updateBook: { +// // middleware: [authValidate(roleSet.librarian)], +// // @ts-expect-error +// handler: async ({ body }) => { +// const result = await updateBookOrBookInfo(body); + +// if (result instanceof PubdateFormatError) { +// return pubdateFormatError; +// } +// return { status: 200, body: '책 정보가 수정되었습니다.' } as const; +// }, +// }, +// updateDonator: { +// // middleware: [authValidate(roleSet.librarian)], +// // @ts-expect-error +// handler: async ({ body }) => { +// const result = await updateBookDonator(body); + +// return { status: 200, body: '기부자 정보가 수정되었습니다.' } as const; +// }, +// }, +// }); diff --git a/backend/src/v2/books/repository.ts b/backend/src/v2/books/repository.ts new file mode 100644 index 00000000..b615f0ee --- /dev/null +++ b/backend/src/v2/books/repository.ts @@ -0,0 +1,186 @@ +import { db } from '~/kysely/mod.ts'; +import { sql } from 'kysely'; + +import jipDataSource from '~/app-data-source'; +import { VSearchBook, Book, BookInfo, VSearchBookByTag, User } from '~/entity/entities'; +import { Like } from 'typeorm'; +import { dateAddDays, dateFormat } from '~/kysely/sqlDates'; + +export const vSearchBookRepo = jipDataSource.getRepository(VSearchBook); +export const bookRepo = jipDataSource.getRepository(Book); +export const bookInfoRepo = jipDataSource.getRepository(BookInfo); +export const vSearchBookByTagRepo = jipDataSource.getRepository(VSearchBookByTag); +export const userRepo = jipDataSource.getRepository(User); + +export const getBookInfosByTag = async ( + whereQuery: object, + sortQuery: object, + page: number, + limit: number, +) => { + return await vSearchBookByTagRepo.findAndCount({ + select: [ + 'id', + 'title', + 'author', + 'isbn', + 'image', + 'publishedAt', + 'createdAt', + 'updatedAt', + 'category', + 'superTagContent', + 'subTagContent', + 'lendingCnt', + ], + where: whereQuery, + take: limit, + skip: page * limit, + order: sortQuery, + }); +}; + +const bookInfoBy = () => + db + .selectFrom('book_info') + .select([ + 'book_info.id', + 'book_info.title', + 'book_info.author', + 'book_info.publisher', + 'book_info.isbn', + 'book_info.image', + 'book_info.publishedAt', + 'book_info.createdAt', + 'book_info.updatedAt', + ]); + +export const getBookInfosSorted = (limit: number) => + bookInfoBy() + .leftJoin('book', 'book_info.id', 'book.infoId') + .leftJoin('category', 'book_info.categoryId', 'category.id') + .leftJoin('lending', 'book.id', 'lending.bookId') + .select('category.name as category') + .select(({ eb }) => eb.fn.count('lending.id').as('lendingCnt')) + .limit(limit) + .groupBy('id'); + +export const searchBookInfoSpecById = async (id: number) => + bookInfoBy() + .select(({ selectFrom }) => [ + selectFrom('category') + .select('name') + .whereRef('category.id', '=', 'book_info.categoryId') + .as('category'), + ]) + .where('id', '=', id) + .executeTakeFirst(); + +export const searchBooksByInfoId = async (id: number) => + db + .selectFrom('book') + .select(['id', 'callSign', 'donator', 'status']) + .where('infoId', '=', id) + .execute(); + +export const getIsLendable = async (id: number) => { + const isLended = await db + .selectFrom('lending') + .where('bookId', '=', id) + .where('returnedAt', 'is', null) + .select('id') + .executeTakeFirst(); + + const book = await db + .selectFrom('book') + .where('id', '=', id) + .where('status', '=', 0) + .select('id') + .executeTakeFirst(); + + const isReserved = await db + .selectFrom('reservation') + .where('bookId', '=', id) + .where('status', '=', 0) + .select('id') + .executeTakeFirst(); + + return book !== undefined && isLended === undefined && isReserved !== undefined; +}; + +export const getIsReserved = async (id: number) => { + const count = await db + .selectFrom('reservation') + .where('bookId', '=', id) + .where('status', '=', 0) + .select(({ eb }) => eb.fn.countAll().as('count')) + .executeTakeFirst(); + + if (Number(count?.count) > 0) return true; + else return false; +}; + +export const getDuedate = async (id: number, interval = 14) => + db + .selectFrom('lending') + .where('bookId', '=', id) + .orderBy('createdAt', 'desc') + .limit(1) + .select(({ ref }) => { + const createdAt = ref('lending.createdAt'); + + return dateAddDays(createdAt, interval).as('dueDate'); + }) + .executeTakeFirst(); + +type SearchBookListArgs = { query: string; page: number; limit: number }; +export const searchBookListAndCount = async ({ query, page, limit }: SearchBookListArgs) => { + return await vSearchBookRepo.findAndCount({ + where: [ + { title: Like(`%${query}%`) }, + { author: Like(`%${query}%`) }, + { isbn: Like(`%${query}%`) }, + ], + take: limit, + skip: page * limit, + }); +}; + +type UpdateBookArgs = { + id: number; + callSign?: string | undefined; + status?: number | undefined; +}; +export const updateBookById = async ({ id, callSign, status }: UpdateBookArgs) => { + await bookRepo.update(id, { callSign, status }); +}; + +type UpdateBookInfoArgs = { + id: number; + title?: string | undefined; + author?: string | undefined; + publisher?: string | undefined; + publishedAt?: string | undefined; + image?: string | undefined; + categoryId?: number | undefined; +}; +export const updateBookInfoById = async ({ + id, + title, + author, + publisher, + publishedAt, + image, + categoryId, +}: UpdateBookInfoArgs) => { + await bookInfoRepo.update(id, { title, author, publisher, publishedAt, image, categoryId }); +}; + +type UpdateBookDonatorNameArgs = { bookId: number; donator: string; donatorId?: number | null }; +export const updateBookDonatorName = async ({ + bookId, + donator, + donatorId, +}: UpdateBookDonatorNameArgs) => { + await bookRepo.update(bookId, { donator, donatorId }); +}; diff --git a/backend/src/v2/books/service.ts b/backend/src/v2/books/service.ts new file mode 100644 index 00000000..5ee2f71c --- /dev/null +++ b/backend/src/v2/books/service.ts @@ -0,0 +1,307 @@ +import { match } from 'ts-pattern'; +import { + searchBookListAndCount, + vSearchBookRepo, + updateBookById, + updateBookInfoById, + searchBookInfoSpecById, + searchBooksByInfoId, + getIsLendable, + getIsReserved, + getDuedate, + getBookInfosSorted, + getBookInfosByTag, + userRepo, + updateBookDonatorName, +} from './repository'; +import { BookInfoNotFoundError, Meta, BookNotFoundError } from '../shared'; +import { IsbnNotFoundError, NaverBookNotFound, PubdateFormatError } from './errors'; +import { dateNow, dateSubDays } from '~/kysely/sqlDates'; +import axios from 'axios'; +import { nationalIsbnApiKey, naverBookApiOption } from '~/config'; + +type CategoryList = { name: string; count: number }; +type SearchBookInfosByTag = { + query: string; + sort: string; + page: number; + limit: number; + category?: string | undefined; +}; +export const searchBookInfosByTag = async ({ + query, + sort, + page, + limit, + category, +}: SearchBookInfosByTag) => { + let sortQuery = {}; + switch (sort) { + case 'title': + sortQuery = { title: 'ASC' }; + break; + case 'popular': + sortQuery = { lendingCnt: 'DESC' }; + break; + default: + sortQuery = { createdAt: 'DESC' }; + } + + let whereQuery: Array = [{ superTagContent: query }, { subTagContent: query }]; + + if (category) { + whereQuery.push({ category }); + } + + const [bookInfoList, totalItems] = await getBookInfosByTag(whereQuery, sortQuery, page, limit); + let categoryList = new Array(); + bookInfoList.forEach((bookInfo) => { + const index = categoryList.findIndex((item) => bookInfo.category === item.name); + if (index === -1) categoryList.push({ name: bookInfo.category, count: 1 }); + else categoryList[index].count += 1; + }); + const meta = { + totalItems, + itemCount: bookInfoList.length, + itemsPerPage: limit, + totalPages: Math.ceil(bookInfoList.length / limit), + currentPage: page + 1, + }; + + return { + items: bookInfoList, + categories: categoryList, + meta, + }; +}; + +type SearchBookInfosSortedArgs = { sort: string; limit: number }; +export const searchBookInfosSorted = async ({ sort, limit }: SearchBookInfosSortedArgs) => { + let items; + if (sort === 'popular') { + items = await getBookInfosSorted(limit) + .where('lending.createdAt', '>=', dateSubDays(dateNow(), 42)) + .orderBy('lendingCnt', 'desc') + .orderBy('title', 'asc') + .execute(); + } else { + items = await getBookInfosSorted(limit) + .orderBy('createdAt', 'desc') + .orderBy('title', 'asc') + .execute(); + } + + return { items } as const; +}; + +export const searchBookInfoById = async (id: number) => { + let bookSpec = await searchBookInfoSpecById(id); + + if (bookSpec === undefined) return new BookInfoNotFoundError(id); + + if (bookSpec.publishedAt) { + const date = new Date(bookSpec.publishedAt); + bookSpec.publishedAt = `${date.getFullYear()}년 ${date.getMonth() + 1}월`; + } + + const eachbooks = await searchBooksByInfoId(id); + + const books = await Promise.all( + eachbooks.map(async (eachBook) => { + const isLendable = await getIsLendable(eachBook.id); + const isReserved = await getIsReserved(eachBook.id); + let dueDate; + + if (eachBook.status === 0 && isLendable === false) { + dueDate = await getDuedate(eachBook.id); + dueDate = dueDate?.dueDate; + } else dueDate = '-'; + + return { + ...eachBook, + dueDate, + isLendable, + isReserved, + }; + }), + ); + + return { + ...bookSpec, + books, + }; +}; + +type SearchAllBooksArgs = { query?: string | undefined; page: number; limit: number }; +export const searchAllBooks = async ({ query, page, limit }: SearchAllBooksArgs) => { + const [BookList, totalItems] = await searchBookListAndCount({ + query: query ? query : '', + page, + limit, + }); + + const meta: Meta = { + totalItems, + itemCount: BookList.length, + itemsPerPage: limit, + totalPages: Math.ceil(totalItems / limit), + currentPage: page + 1, + }; + return { items: BookList, meta }; +}; + +type BookInfoForCreate = { + title: string; + author?: string | undefined; + isbn: string; + category: string; + publisher: string; + pubdate: string; + image: string; +}; +const getInfoInNationalLibrary = async (isbn: string) => { + let bookInfo: BookInfoForCreate | undefined; + let searchResult; + + await axios + .get( + `https://www.nl.go.kr/seoji/SearchApi.do?cert_key=${nationalIsbnApiKey}&result_style=json&page_no=1&page_size=10&isbn=${isbn}`, + ) + .then((res) => { + searchResult = res.data.docs[0]; + const { + TITLE: title, + SUBJECT: category, + PUBLISHER: publisher, + PUBLISH_PREDATE: pubdate, + } = searchResult; + const image = `https://image.kyobobook.co.kr/images/book/xlarge/${isbn.slice( + -3, + )}/x${isbn}.jpg`; + bookInfo = { + title, + image, + category, + isbn, + publisher, + pubdate, + }; + }) + .catch(() => { + console.log('Error'); + }); + return bookInfo; +}; + +const getAuthorInNaver = async (isbn: string) => { + let author; + + await axios + .get(`https://openapi.naver.com/v1/search/book_adv?d_isbn=${isbn}`, { + headers: { + 'X-Naver-Client-Id': `${naverBookApiOption.client}`, + 'X-Naver-Client-Secret': `${naverBookApiOption.secret}`, + }, + }) + .then((res) => { + author = res.data.items[0].author; + }) + .catch(() => { + console.log('ERROR'); + }); + return author; +}; + +export const searchBookInfoForCreate = async (isbn: string) => { + let bookInfo = await getInfoInNationalLibrary(isbn); + if (bookInfo === undefined) return new IsbnNotFoundError(isbn); + + bookInfo.author = await getAuthorInNaver(isbn); + if (bookInfo.author === undefined) return new NaverBookNotFound(isbn); + + return { bookInfo }; +}; + +type SearchBookByIdArgs = { id: number }; +export const searchBookById = async ({ id }: SearchBookByIdArgs) => { + const book = await vSearchBookRepo.findOneBy({ bookId: id }); + + return match(book) + .with(null, () => new BookNotFoundError(id)) + .otherwise(() => { + return { + id: book?.bookId, + ...book, + }; + }); +}; + +type UpdateBookArgs = { + bookId: number; + callSign?: string | undefined; + status?: number | undefined; +}; +const updateBook = async (book: UpdateBookArgs) => { + return await updateBookById({ id: book.bookId, callSign: book.callSign, status: book.status }); +}; + +type UpdateBookInfoArgs = { + bookInfoId: number; + title?: string | undefined; + author?: string | undefined; + publisher?: string | undefined; + publishedAt?: string | undefined; + image?: string | undefined; + categoryId?: number | undefined; +}; +const pubdateFormatValidator = (pubdate: string) => { + const regexCondition = /^[0-9]{8}$/; + return regexCondition.test(pubdate); +}; +const updateBookInfo = async (book: UpdateBookInfoArgs) => { + if (book.publishedAt && !pubdateFormatValidator(book.publishedAt)) + return new PubdateFormatError(book.publishedAt); + return await updateBookInfoById({ + id: book.bookInfoId, + title: book.title, + author: book.author, + publisher: book.publisher, + publishedAt: book.publishedAt, + image: book.image, + categoryId: book.categoryId, + }); +}; + +type UpdateBookOrBookInfoArgs = Omit & + Omit & { + bookId?: number | undefined; + bookInfoId?: number | undefined; + }; +export const updateBookOrBookInfo = async (book: UpdateBookOrBookInfoArgs) => { + if (book.bookId) + await updateBook({ + bookId: book.bookId, + callSign: book.callSign, + status: book.status, + }); + if (book.bookInfoId) + return await updateBookInfo({ + bookInfoId: book.bookInfoId, + title: book.title, + author: book.author, + publisher: book.publisher, + publishedAt: book.publishedAt, + image: book.image, + categoryId: book.categoryId, + }); +}; + +type UpdateBookDonatorArgs = { bookId: number; nickname: string }; +export const updateBookDonator = async ({ bookId, nickname }: UpdateBookDonatorArgs) => { + const user = await userRepo.findOneBy({ nickname }); + return await updateBookDonatorName({ + bookId, + donator: nickname, + donatorId: user ? user.id : null, + }); +}; diff --git a/backend/src/v2/histories/repository.ts b/backend/src/v2/histories/repository.ts deleted file mode 100644 index 3a8f039f..00000000 --- a/backend/src/v2/histories/repository.ts +++ /dev/null @@ -1,68 +0,0 @@ -import { Like } from 'typeorm'; -import { match } from 'ts-pattern'; -import jipDataSource from '~/app-data-source'; -import { VHistories } from '~/entity/entities'; - -const historiesRepo = jipDataSource.getRepository(VHistories); - -const queriesLike = (query: string) => { - const like = Like(`%${query}%`); - const login = { login: like }; - const title = { title: like }; - const callSign = { callSign: like }; - - return { login, title, callSign }; -}; - -const getSearchCondition = ({ query, type }: Args) => { - if (!query) { - return undefined; - } - - const { login, title, callSign } = queriesLike(query); - return match(type) - .with(undefined, () => [login, title, callSign]) - .with('user', () => login) - .with('title', () => title) - .with('callsign', () => callSign) - .exhaustive(); -}; - -type Offset = { - page: number; - limit: number; -}; - -type Args = { - query?: string | undefined; - type?: 'title' | 'user' | 'callsign' | undefined; -}; - -export const getHistoriesByQuery = ({ - query, - type, - page, - limit, -}: Args & Offset) => - historiesRepo.findAndCount({ - where: getSearchCondition({ query, type }), - take: limit, - skip: limit * page, - }); - -type MyPageArgs = { - login: string; - query?: string | undefined; - type?: 'title' | 'callsign' | undefined; -}; - -export const getHistoriesByUser = ({ - login, - page, - limit, -}: MyPageArgs & Offset) => - historiesRepo.findAndCount({ - where: { login }, - take: limit, - skip: limit * page, - }); diff --git a/backend/src/v2/histories/mod.ts b/backend/src/v2/lendings/mod.ts similarity index 76% rename from backend/src/v2/histories/mod.ts rename to backend/src/v2/lendings/mod.ts index 7d24dd45..205eaf10 100644 --- a/backend/src/v2/histories/mod.ts +++ b/backend/src/v2/lendings/mod.ts @@ -1,20 +1,17 @@ import { contract } from '@jiphyeonjeon-42/contracts'; import { initServer } from '@ts-rest/express'; -import jipDataSource from '~/app-data-source'; import { roleSet } from '~/v1/auth/auth.type'; import authValidate from '~/v1/auth/auth.validate'; -import { VHistories } from '~/entity/entities/VHistories'; import { getHistoriesByQuery, getHistoriesByUser } from './repository'; import { getUser } from '../shared'; const s = initServer(); -export const histories = s.router(contract.histories, { - getMyHistories: { +export const lendings = s.router(contract.lendings, { + getMine: { middleware: [authValidate(roleSet.all)], handler: async ({ query, req: { user } }) => { const { nickname: login } = getUser.parse(user); - const [items, count] = await getHistoriesByUser({ ...query, login }); - + const { items, count } = await getHistoriesByUser({ ...query, login }); const meta = { totalItems: count, itemCount: items.length, @@ -27,10 +24,10 @@ export const histories = s.router(contract.histories, { }, }, - getAllHistories: { + get: { middleware: [authValidate(roleSet.librarian)], handler: async ({ query }) => { - const [items, count] = await getHistoriesByQuery(query); + const { items, count } = await getHistoriesByQuery(query); const meta = { totalItems: count, diff --git a/backend/src/v2/lendings/repository.ts b/backend/src/v2/lendings/repository.ts new file mode 100644 index 00000000..dd412a9e --- /dev/null +++ b/backend/src/v2/lendings/repository.ts @@ -0,0 +1,77 @@ +import { match } from 'ts-pattern'; +import { DB } from '~/kysely/generated.ts'; +import { db } from '~/kysely/mod.ts'; +import { ExpressionBuilder, SelectQueryBuilder } from "kysely"; +import { AllSelection } from 'kysely/dist/cjs/parser/select-parser'; + +const queriesLike = (sql: SelectQueryBuilder>, query: string) => { + const all = sql.where((eb: ExpressionBuilder) => + eb('login', 'like', `%${query}%`) + .or('title', 'like', `%${query}%`) + .or('callSign', 'like', `%${query}%`)); + const login = sql.where((eb: ExpressionBuilder) => + eb('login', 'like', `%${query}%`)); + const title = sql.where((eb: ExpressionBuilder) => + eb('title', 'like', `%${query}%`)); + const callSign = sql.where((eb: ExpressionBuilder) => + eb('callSign', 'like', `%${query}%`)); + + return { all, login, title, callSign }; +}; + +const getSearchCondition = (sql: SelectQueryBuilder>, { query, type }: Args) => { + if (!query) { + return sql; + } + const { all, login, title, callSign } = queriesLike(sql, query); + return match(type) + .with(undefined, () => all) + .with('user', () => login) + .with('title', () => title) + .with('callsign', () => callSign) + .exhaustive(); +}; + +type Offset = { + page: number; + limit: number; +}; + +type Args = { + query?: string | undefined; + type?: 'title' | 'user' | 'callsign' | undefined; +}; + +export const getHistoriesByQuery = async ({ query, type, page, limit }: Args & Offset) => { + let sql = db.selectFrom('v_histories') + .selectAll(); + sql = getSearchCondition(sql, {query, type}); + const items = await sql.limit(limit) + .offset(limit * page) + .execute(); + let countSql = db.selectFrom('v_histories') + .select(({fn}) => [ fn.count('id').as('count') ]); + countSql = getSearchCondition(countSql, {query, type}); + const [{count}] = await countSql.execute(); + return { items, count }; +} + +type MyPageArgs = { + login: string; + query?: string | undefined; + type?: 'title' | 'callsign' | undefined; +}; + +export const getHistoriesByUser = async ({login, page, limit}: MyPageArgs & Offset) => { + const items = await db.selectFrom('v_histories') + .where('login', '=', login) + .selectAll() + .limit(limit) + .offset(limit * page) + .execute(); + const [{count}] = await db.selectFrom('v_histories') + .where('login', '=', login) + .select(({fn}) => [ fn.count('id').as('count') ]) + .execute(); + return { items, count }; +} diff --git a/backend/src/v2/reviews/mod.ts b/backend/src/v2/reviews/mod.ts index 15f295f7..5639f7ff 100644 --- a/backend/src/v2/reviews/mod.ts +++ b/backend/src/v2/reviews/mod.ts @@ -9,14 +9,9 @@ import { getUser, reviewNotFound, } from '../shared/index.ts'; -import { - createReview, - removeReview, - toggleReviewVisibility, - updateReview, -} from './service.ts'; +import { createReview, removeReview, toggleReviewVisibility, updateReview } from './service.ts'; import { ReviewNotFoundError } from './errors.js'; -import { searchReviews } from './repository.ts' +import { searchReviews } from './repository.ts'; const s = initServer(); export const reviews = s.router(contract.reviews, { @@ -26,7 +21,7 @@ export const reviews = s.router(contract.reviews, { const body = await searchReviews(query); return { status: 200, body }; - } + }, }, post: { middleware: [authValidate(roleSet.all)], diff --git a/backend/src/v2/reviews/repository.ts b/backend/src/v2/reviews/repository.ts index 328995b2..90776403 100644 --- a/backend/src/v2/reviews/repository.ts +++ b/backend/src/v2/reviews/repository.ts @@ -40,16 +40,10 @@ const queryReviews = () => 'user.nickname', ]); -export const searchReviews = async ({ - search, - sort, - visibility, - page, - perPage, -}: SearchOption) => { +export const searchReviews = async ({ search, sort, visibility, page, perPage }: SearchOption) => { const searchQuery = queryReviews() - .$if(search !== undefined, qb => - qb.where(eb => + .$if(search !== undefined, (qb) => + qb.where((eb) => eb.or([ eb('user.nickname', 'like', `%${search}%`), eb('book_info.title', 'like', `%${search}%`), @@ -102,11 +96,7 @@ type ToggleVisibilityOption = { userId: number; disabled: SqlBool; }; -export const toggleVisibilityById = ({ - reviewsId, - userId, - disabled, -}: ToggleVisibilityOption) => +export const toggleVisibilityById = ({ reviewsId, userId, disabled }: ToggleVisibilityOption) => db .updateTable('reviews') .where('id', '=', reviewsId) @@ -119,11 +109,7 @@ type UpdateOption = { content: string; }; -export const updateReviewById = ({ - reviewsId, - userId, - content, -}: UpdateOption) => +export const updateReviewById = ({ reviewsId, userId, content }: UpdateOption) => db .updateTable('reviews') .where('id', '=', reviewsId) diff --git a/backend/src/v2/reviews/service.ts b/backend/src/v2/reviews/service.ts index 34cbb514..d7278a77 100644 --- a/backend/src/v2/reviews/service.ts +++ b/backend/src/v2/reviews/service.ts @@ -1,11 +1,7 @@ import { match } from 'ts-pattern'; import { BookInfoNotFoundError } from '~/v2/shared/errors'; -import { - ReviewDisabledError, - ReviewForbiddenAccessError, - ReviewNotFoundError, -} from './errors'; +import { ReviewDisabledError, ReviewForbiddenAccessError, ReviewNotFoundError } from './errors'; import { ParsedUser } from '~/v2/shared'; import { bookInfoExistsById, @@ -28,29 +24,18 @@ export const createReview = async (args: CreateArgs) => { type RemoveArgs = { reviewsId: number; deleter: ParsedUser }; export const removeReview = async ({ reviewsId, deleter }: RemoveArgs) => { const isAdmin = () => deleter.role === 'librarian'; - const doRemoveReview = () => - deleteReviewById({ reviewsId, deleteUserId: deleter.id }); + const doRemoveReview = () => deleteReviewById({ reviewsId, deleteUserId: deleter.id }); const review = await getReviewById(reviewsId); return match(review) - .with( - undefined, - { isDeleted: true }, - () => new ReviewNotFoundError(reviewsId), - ) + .with(undefined, { isDeleted: true }, () => new ReviewNotFoundError(reviewsId)) .when(isAdmin, doRemoveReview) .with({ userId: deleter.id }, doRemoveReview) - .otherwise( - () => new ReviewForbiddenAccessError({ userId: deleter.id, reviewsId }), - ); + .otherwise(() => new ReviewForbiddenAccessError({ userId: deleter.id, reviewsId })); }; type UpdateArgs = { reviewsId: number; userId: number; content: string }; -export const updateReview = async ({ - reviewsId, - userId, - content, -}: UpdateArgs) => { +export const updateReview = async ({ reviewsId, userId, content }: UpdateArgs) => { const review = await getReviewById(reviewsId); return await match(review) @@ -61,15 +46,10 @@ export const updateReview = async ({ }; type ToggleReviewArgs = { reviewsId: number; userId: number }; -export const toggleReviewVisibility = async ({ - reviewsId, - userId, -}: ToggleReviewArgs) => { +export const toggleReviewVisibility = async ({ reviewsId, userId }: ToggleReviewArgs) => { const review = await getReviewById(reviewsId); return await match(review) .with(undefined, () => new ReviewNotFoundError(reviewsId)) - .otherwise(({ disabled }) => - toggleVisibilityById({ reviewsId, userId, disabled }), - ); + .otherwise(({ disabled }) => toggleVisibilityById({ reviewsId, userId, disabled })); }; diff --git a/backend/src/v2/routes.ts b/backend/src/v2/routes.ts index 1bff3eb7..32f3d39c 100644 --- a/backend/src/v2/routes.ts +++ b/backend/src/v2/routes.ts @@ -3,12 +3,10 @@ import { contract } from '@jiphyeonjeon-42/contracts'; import { initServer } from '@ts-rest/express'; import { reviews } from './reviews/mod.ts'; -import { histories } from './histories/mod.ts'; -import { stock } from './stock/mod.ts'; +import { lendings } from './lendings/mod.ts'; const s = initServer(); export default s.router(contract, { reviews, - histories, - stock, + lendings, }); diff --git a/backend/src/v2/shared/responses.ts b/backend/src/v2/shared/responses.ts index f915bb0f..8122b3d4 100644 --- a/backend/src/v2/shared/responses.ts +++ b/backend/src/v2/shared/responses.ts @@ -1,4 +1,9 @@ -import { bookInfoNotFoundSchema, reviewNotFoundSchema, unauthorizedSchema, bookNotFoundSchema } from '@jiphyeonjeon-42/contracts'; +import { + bookInfoNotFoundSchema, + reviewNotFoundSchema, + unauthorizedSchema, + bookNotFoundSchema, +} from '@jiphyeonjeon-42/contracts'; import { z } from 'zod'; export const reviewNotFound = { @@ -32,3 +37,27 @@ export const bookNotFound = { description: '검색한 책이 존재하지 않습니다.', } as z.infer, } as const; + +export const pubdateFormatError = { + status: 311, + body: { + code: 'PUBDATE_FORMAT_ERROR', + description: '입력한 pubdate가 알맞은 형식이 아님.', + }, +} as const; + +export const isbnNotFound = { + status: 303, + body: { + code: 'ISBN_NOT_FOUND', + description: '국립중앙도서관 API에서 ISBN 검색이 실패하였습니다.', + }, +} as const; + +export const naverBookNotFound = { + status: 310, + body: { + code: 'NAVER_BOOK_NOT_FOUND', + description: '네이버 책검색 API에서 ISBN 검색이 실패', + }, +} as const; diff --git a/backend/src/v2/stock/mod.ts b/backend/src/v2/stock/mod.ts deleted file mode 100644 index 800a9180..00000000 --- a/backend/src/v2/stock/mod.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { contract } from '@jiphyeonjeon-42/contracts'; -import { initServer } from '@ts-rest/express'; -import { searchStock, updateStock } from './service'; -import { BookNotFoundError, bookNotFound } from '../shared'; - -const s = initServer(); -export const stock = s.router(contract.stock, { - get: async ({ query }) => { - const result = await searchStock(query); - - return { status: 200, body: result } as const; - }, - - patch: async ({ body }) => { - const result = await updateStock(body); - - if (result instanceof BookNotFoundError) { - return bookNotFound; - } - return { status: 200, body: '재고 상태가 업데이트되었습니다.' } as const; - } -}); diff --git a/backend/src/v2/stock/repository.ts b/backend/src/v2/stock/repository.ts deleted file mode 100644 index bca83a14..00000000 --- a/backend/src/v2/stock/repository.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { LessThan } from 'typeorm'; -import { startOfDay, addDays } from 'date-fns'; -import { Book, VStock } from '~/entity/entities'; -import jipDataSource from '~/app-data-source'; - -export const stockRepo = jipDataSource.getRepository(VStock); -export const bookRepo = jipDataSource.getRepository(Book); - -type SearchStockArgs = { page: number; limit: number; days: number }; - -export const searchStockByUpdatedOlderThan = ({ - limit, - page, - days, -}: SearchStockArgs) => { - const today = startOfDay(new Date()); - return stockRepo.findAndCount({ - where: { updatedAt: LessThan(addDays(today, days * -1)) }, - take: limit, - skip: limit * page, - }); -}; diff --git a/backend/src/v2/stock/service.ts b/backend/src/v2/stock/service.ts deleted file mode 100644 index 048c5715..00000000 --- a/backend/src/v2/stock/service.ts +++ /dev/null @@ -1,41 +0,0 @@ -import { match } from 'ts-pattern'; - -import { VStock } from '~/entity/entities'; -import { type Repository } from 'typeorm'; - -import { Meta } from '~/v2/shared'; -import { BookNotFoundError } from '~/v2/shared/errors'; -import { searchStockByUpdatedOlderThan } from './repository'; -import { stockRepo } from './repository'; -import { bookRepo } from './repository'; - -type SearchArgs = { page: number; limit: number }; -export const searchStock = async ({ - limit, - page, -}: SearchArgs): Promise<{ items: VStock[]; meta: Meta }> => { - const [items, totalItems] = await searchStockByUpdatedOlderThan({ - limit, - page, - days: 15, - }); - - const meta: Meta = { - totalItems, - itemCount: items.length, - itemsPerPage: limit, - totalPages: Math.ceil(totalItems / limit), - currentPage: page + 1, - }; - - return { items, meta }; -}; - -type UpdateArgs = { id: number }; -export const updateStock = async ({ id }: UpdateArgs) => { - const stock = await stockRepo.findOneBy({ bookId: id }); - - return match(stock) - .with(null, () => new BookNotFoundError(id)) - .otherwise(() => bookRepo.update({ id }, { updatedAt: new Date() })); -}; diff --git a/contracts/package.json b/contracts/package.json index dbd62adf..b747a4b3 100644 --- a/contracts/package.json +++ b/contracts/package.json @@ -13,13 +13,15 @@ }, "dependencies": { "@anatine/zod-openapi": "^2.2.0", - "openapi3-ts": "^4.1.2" + "@ts-rest/core": "^3.30.4", + "openapi3-ts": "^4.1.2", + "zod": "^3.22.4" }, "devDependencies": { + "@apidevtools/swagger-cli": "^4.0.4", "@typescript-eslint/eslint-plugin": "^6.1.0", "@typescript-eslint/parser": "^6.1.0", "eslint": "^8.45.0", - "@apidevtools/swagger-cli": "^4.0.4", "openapi-endpoint-trimmer": "^2.0.0" } } diff --git a/contracts/src/books/index.ts b/contracts/src/books/index.ts index 455572b9..712fec07 100644 --- a/contracts/src/books/index.ts +++ b/contracts/src/books/index.ts @@ -1,150 +1,71 @@ -import { initContract } from "@ts-rest/core"; -import { - searchAllBooksQuerySchema, - searchAllBooksResponseSchema, - searchBookByIdResponseSchema, - searchBookInfoCreateQuerySchema, - searchBookInfoCreateResponseSchema, - createBookBodySchema, - createBookResponseSchema, - categoryNotFoundSchema, - formatErrorSchema, - insertionFailureSchema, - isbnNotFoundSchema, - naverBookNotFoundSchema, - updateBookBodySchema, - updateBookResponseSchema, - unknownPatchErrorSchema, - nonDataErrorSchema, - searchBookInfosQuerySchema, - searchBookInfosResponseSchema, - searchBookInfosSortedQuerySchema, - searchBookInfosSortedResponseSchema, - searchBookInfoByIdQuerySchema, - searchBookInfoByIdResponseSchema, - updateDonatorBodySchema, - updateDonatorResponseSchema -} from "./schema"; -import { badRequestSchema, bookInfoNotFoundSchema, bookNotFoundSchema, serverErrorSchema } from "../shared"; +import { initContract } from '@ts-rest/core'; +import { + searchAllBooksQuerySchema, + searchAllBooksResponseSchema, + searchBookByIdResponseSchema, + pubdateFormatErrorSchema, + updateBookBodySchema, + updateBookResponseSchema, + unknownPatchErrorSchema, + nonDataErrorSchema, + searchBookInfoByIdResponseSchema, + searchBookInfoByIdPathSchema, + searchBookByIdParamSchema, +} from './schema'; +import { badRequestSchema, bookInfoNotFoundSchema, bookNotFoundSchema } from '../shared'; const c = initContract(); +export const bookMetasContracts = c.router( + { + getById: { + method: 'GET', + path: '/info/:id', + pathParams: searchBookInfoByIdPathSchema, + summary: '도서 정보를 조회합니다.', + responses: { + 200: searchBookInfoByIdResponseSchema, + 404: bookInfoNotFoundSchema, + }, + }, + }, + { pathPrefix: '/bookmetas' }, +); + export const booksContract = c.router( - { - searchAllBookInfos: { - method: 'GET', - path: '/info/search', - description: '책 정보(book_info)를 검색하여 가져온다.', - query: searchBookInfosQuerySchema, - responses: { - 200: searchBookInfosResponseSchema, - 400: badRequestSchema, - 500: serverErrorSchema, - }, - }, - searchBookInfosByTag: { - method: 'GET', - path: '/info/tag', - description: '똑같은 내용의 태그가 달린 책의 정보를 검색하여 가져온다.', - query: searchBookInfosQuerySchema, - responses: { - 200: searchBookInfosResponseSchema, - 400: badRequestSchema, - 500: serverErrorSchema, - }, - }, - searchBookInfosSorted: { - method: 'GET', - path: '/info/sorted', - description: '책 정보를 기준에 따라 정렬한다. 정렬기준이 popular일 경우 당일으로부터 42일간 인기순으로 한다.', - query: searchBookInfosSortedQuerySchema, - responses: { - 200: searchBookInfosSortedResponseSchema, - 400: badRequestSchema, - 500: serverErrorSchema, - }, - }, - searchBookInfoById: { - method: 'GET', - path: '/info/:id', - description: 'book_info테이블의 ID기준으로 책 한 종류의 정보를 가져온다.', - query: searchBookInfoByIdQuerySchema, - responses: { - 200: searchBookInfoByIdResponseSchema, - 404: bookInfoNotFoundSchema, - 500: serverErrorSchema - } - }, - searchAllBooks: { - method: 'GET', - path: '/search', - description: '개별 책 정보(book)를 검색하여 가져온다. 책이 대출할 수 있는지 확인 할 수 있음', - query: searchAllBooksQuerySchema, - responses: { - 200: searchAllBooksResponseSchema, - 400: badRequestSchema, - 500: serverErrorSchema, - }, - }, - searchBookInfoForCreate: { - method: 'GET', - path: '/create', - description: '책 생성을 위해 국립중앙도서관에서 ISBN으로 검색한 뒤에 책정보를 반환', - query: searchBookInfoCreateQuerySchema, - responses: { - 200: searchBookInfoCreateResponseSchema, - 303: isbnNotFoundSchema, - 310: naverBookNotFoundSchema, - 500: serverErrorSchema, - } - }, - searchBookById: { - method: 'GET', - path: '/:bookId', - description: 'book테이블의 ID기준으로 책 한 종류의 정보를 가져온다.', - responses: { - 200: searchBookByIdResponseSchema, - 404: bookNotFoundSchema, - 500: serverErrorSchema, - } - }, - createBook: { - method: 'POST', - path: '/create', - description: '책 정보를 생성한다. bookInfo가 있으면 book에만 insert한다.', - body: createBookBodySchema, - responses: { - 200: createBookResponseSchema, - 308: insertionFailureSchema, - 309: categoryNotFoundSchema, - 311: formatErrorSchema, - 500: serverErrorSchema, - }, - }, - updateBook: { - method: 'PATCH', - path: '/update', - description: '책 정보를 수정합니다. book_info table or book table', - body: updateBookBodySchema, - responses: { - 204: updateBookResponseSchema, - 312: unknownPatchErrorSchema, - 313: nonDataErrorSchema, - 311: formatErrorSchema, - 500: serverErrorSchema, - }, - }, - updateDonator: { - method: 'PATCH', - path: '/donator', - description: '기부자 정보를 수정합니다.', - body: updateDonatorBodySchema, - responses: { - 204: updateDonatorResponseSchema, - 404: bookNotFoundSchema, - 500: serverErrorSchema, - }, - }, - }, - { pathPrefix: '/books' }, -) \ No newline at end of file + { + getById: { + method: 'GET', + path: '/:id', + summary: 'book테이블의 ID기준으로 책 한 종류의 정보를 가져온다.', + pathParams: searchBookByIdParamSchema, + responses: { + 200: searchBookByIdResponseSchema, + 404: bookNotFoundSchema, + }, + }, + get: { + method: 'GET', + path: '/', + summary: '개별 책 정보(book)를 검색하여 가져온다. 책이 대출할 수 있는지 확인 할 수 있음', + query: searchAllBooksQuerySchema, + responses: { + 200: searchAllBooksResponseSchema, + 400: badRequestSchema, + }, + }, + patch: { + method: 'PATCH', + path: '/update', + summary: '책 정보 하나를 수정합니다.', + body: updateBookBodySchema, + responses: { + 204: updateBookResponseSchema, + 312: unknownPatchErrorSchema, + 313: nonDataErrorSchema, + 311: pubdateFormatErrorSchema, + }, + }, + }, + { pathPrefix: '/books' }, +); diff --git a/contracts/src/books/mod.ts b/contracts/src/books/mod.ts new file mode 100644 index 00000000..712fec07 --- /dev/null +++ b/contracts/src/books/mod.ts @@ -0,0 +1,71 @@ +import { initContract } from '@ts-rest/core'; +import { + searchAllBooksQuerySchema, + searchAllBooksResponseSchema, + searchBookByIdResponseSchema, + pubdateFormatErrorSchema, + updateBookBodySchema, + updateBookResponseSchema, + unknownPatchErrorSchema, + nonDataErrorSchema, + searchBookInfoByIdResponseSchema, + searchBookInfoByIdPathSchema, + searchBookByIdParamSchema, +} from './schema'; +import { badRequestSchema, bookInfoNotFoundSchema, bookNotFoundSchema } from '../shared'; + +const c = initContract(); + +export const bookMetasContracts = c.router( + { + getById: { + method: 'GET', + path: '/info/:id', + pathParams: searchBookInfoByIdPathSchema, + summary: '도서 정보를 조회합니다.', + responses: { + 200: searchBookInfoByIdResponseSchema, + 404: bookInfoNotFoundSchema, + }, + }, + }, + { pathPrefix: '/bookmetas' }, +); + +export const booksContract = c.router( + { + getById: { + method: 'GET', + path: '/:id', + summary: 'book테이블의 ID기준으로 책 한 종류의 정보를 가져온다.', + pathParams: searchBookByIdParamSchema, + responses: { + 200: searchBookByIdResponseSchema, + 404: bookNotFoundSchema, + }, + }, + get: { + method: 'GET', + path: '/', + summary: '개별 책 정보(book)를 검색하여 가져온다. 책이 대출할 수 있는지 확인 할 수 있음', + query: searchAllBooksQuerySchema, + responses: { + 200: searchAllBooksResponseSchema, + 400: badRequestSchema, + }, + }, + patch: { + method: 'PATCH', + path: '/update', + summary: '책 정보 하나를 수정합니다.', + body: updateBookBodySchema, + responses: { + 204: updateBookResponseSchema, + 312: unknownPatchErrorSchema, + 313: nonDataErrorSchema, + 311: pubdateFormatErrorSchema, + }, + }, + }, + { pathPrefix: '/books' }, +); diff --git a/contracts/src/books/schema.ts b/contracts/src/books/schema.ts index 056073e1..1916aec6 100644 --- a/contracts/src/books/schema.ts +++ b/contracts/src/books/schema.ts @@ -1,174 +1,150 @@ -import { metaSchema, positiveInt, mkErrorMessageSchema, statusSchema, metaPaginatedSchema } from "../shared"; -import { z } from "../zodWithOpenapi"; +import { + metaSchema, + nonNegativeInt, + mkErrorMessageSchema, + statusSchema, + metaPaginatedSchema, + dateLike, +} from '../shared'; +import { z } from '../zodWithOpenapi'; export const commonQuerySchema = z.object({ - query: z.string().optional(), - page: positiveInt.default(0), - limit: positiveInt.default(10), + query: z.string().optional(), + page: nonNegativeInt.default(0).openapi({ example: 0 }), + limit: nonNegativeInt.default(10).openapi({ example: 10 }), }); -export const searchBookInfosQuerySchema = commonQuerySchema.extend({ - sort: z.string(), - category: z.string(), +export const searchAllBookInfosQuerySchema = commonQuerySchema.extend({ + sort: z.enum(['new', 'popular', 'title']).default('new'), + category: z.string().optional(), }); -export const searchBookInfosSortedQuerySchema = z.object({ - sort: z.string(), - limit: positiveInt.default(10), -}); - -export const searchBookInfoByIdQuerySchema = z.object({ - id: positiveInt, +export const searchBookInfoByIdPathSchema = z.object({ + id: nonNegativeInt, }); export const searchAllBooksQuerySchema = commonQuerySchema; -export const searchBookInfoCreateQuerySchema = z.object({ - isbnQuery: z.string(), +export const createBookBodySchema = z.object({ + title: z.string(), + isbn: z.string(), + author: z.string(), + publisher: z.string(), + image: z.string(), + categoryId: z.string(), + pubdate: z.string(), + donator: z.string(), }); -export const createBookBodySchema = z.object({ - title: z.string(), - isbn: z.string(), - author: z.string(), - publisher: z.string(), - image: z.string(), - categoryId: z.string(), - pubdate: z.string(), - donator: z.string(), +export const searchBookByIdParamSchema = z.object({ + id: nonNegativeInt, }); export const updateBookBodySchema = z.object({ - bookInfoId: positiveInt, - categoryId: positiveInt, - title: z.string(), - author: z.string(), - publisher: z.string(), - publishedAt: z.string(), - image: z.string(), - bookId: positiveInt, - callSign: z.string(), - status: statusSchema, + bookInfoId: nonNegativeInt.optional(), + title: z.string().optional(), + author: z.string().optional(), + publisher: z.string().optional(), + publishedAt: z.string().optional(), + image: z.string().optional(), + categoryId: nonNegativeInt.optional(), + bookId: nonNegativeInt.optional(), + callSign: z.string().optional(), + status: statusSchema.optional(), }); -export const updateDonatorBodySchema = z.object({ - bookId: positiveInt, - nickname: z.string(), -}); export const bookInfoSchema = z.object({ - id: positiveInt, - title: z.string(), - author: z.string(), - publisher: z.string(), - isbn: z.string(), - image: z.string(), - category: z.string(), - publishedAt: z.string(), - createdAt: z.string(), - updatedAt: z.string(), - lendingCnt: positiveInt, -}); - -export const searchBookInfosResponseSchema = metaPaginatedSchema(bookInfoSchema) - .extend({ - categories: z.array( - z.object({ - name: z.string(), - count: positiveInt, - }), - ), + id: nonNegativeInt, + title: z.string(), + author: z.string(), + publisher: z.string(), + isbn: z.string(), + image: z.string(), + category: z.string(), + publishedAt: z.string(), + createdAt: dateLike, + updatedAt: dateLike, }); -export const searchBookInfosSortedResponseSchema = z.object({ - items: z.array( - bookInfoSchema, - ) -}); -export const searchBookInfoByIdResponseSchema = z.object({ - bookInfoSchema, - books: z.array( - z.object({ - id: positiveInt, - callSign: z.string(), - donator: z.string(), - status: statusSchema, - dueDate: z.string(), - isLendable: positiveInt, - isReserved: positiveInt, - }), - ), +export const searchBookInfoByIdResponseSchema = bookInfoSchema.extend({ + books: z.array( + z.object({ + id: nonNegativeInt, + callSign: z.string(), + donator: z.string(), + status: statusSchema, + dueDate: dateLike, + isLendable: nonNegativeInt, + isReserved: nonNegativeInt, + }), + ), }); -export const searchAllBooksResponseSchema = - metaPaginatedSchema( - z.object({ - bookId: positiveInt.openapi({ example: 1 }), - bookInfoId: positiveInt.openapi({ example: 1 }), - title: z.string().openapi({ example: '모두의 데이터 과학 with 파이썬' }), - author: z.string().openapi({ example: '드미트리 지노비에프' }), - donator: z.string().openapi({ example: 'mingkang' }), - publisher: z.string().openapi({ example: '길벗' }), - publishedAt: z.string().openapi({ example: '20170714' }), - isbn: z.string().openapi({ example: '9791160502152' }), - image: z.string().openapi({ example: 'https://image.kyobobook.co.kr/images/book/xlarge/152/x9791160502152.jpg' }), - status: statusSchema.openapi({ example: 3 }), - categoryId: positiveInt.openapi({ example: 8 }), - callSign: z.string().openapi({ example: 'K23.17.v1.c1' }), - category: z.string().openapi({ example: '데이터 분석/AI/ML' }), - isLendable: positiveInt.openapi({ example: 0 }), - }) +export const searchAllBooksResponseSchema = metaPaginatedSchema( + z.object({ + bookId: nonNegativeInt.openapi({ example: 1 }), + bookInfoId: nonNegativeInt.openapi({ example: 1 }), + title: z.string().openapi({ example: '모두의 데이터 과학 with 파이썬' }), + author: z.string().openapi({ example: '드미트리 지노비에프' }), + donator: z.string().openapi({ example: 'mingkang' }), + publisher: z.string().openapi({ example: '길벗' }), + publishedAt: z.string().openapi({ example: '20170714' }), + isbn: z.string().openapi({ example: '9791160502152' }), + image: z.string().openapi({ + example: 'https://image.kyobobook.co.kr/images/book/xlarge/152/x9791160502152.jpg', + }), + status: statusSchema.openapi({ example: 3 }), + categoryId: nonNegativeInt.openapi({ example: 8 }), + callSign: z.string().openapi({ example: 'K23.17.v1.c1' }), + category: z.string().openapi({ example: '데이터 분석/AI/ML' }), + isLendable: nonNegativeInt.openapi({ example: 0 }), + }), ); -export const searchBookInfoCreateResponseSchema = z.object({ - bookInfo: z.object({ - title: z.string().openapi({ example: '작별인사' }), - image: z.string().openapi({ example: 'http://image.kyobobook.co.kr/images/book/xlarge/225/x9791191114225.jpg' }), - author: z.string().openapi({ example: '지은이: 김영하' }), - category: z.string().openapi({ example: '8' }), - isbn: z.string().openapi({ example: '9791191114225' }), - publisher: z.string().openapi({ example: '복복서가' }), - pubdate: z.string().openapi({ example: '20220502' }), - }), -}) - export const searchBookByIdResponseSchema = z.object({ - id: positiveInt.openapi({ example: 3 }), - bookId: positiveInt.openapi({ example: 3 }), - bookInfoId: positiveInt.openapi({ example: 2}), - title: z.string().openapi({ example: 'TCP IP 윈도우 소켓 프로그래밍(IT Cookbook 한빛 교재 시리즈 124)' }), - author: z.string().openapi({ example: '김선우' }), - donator: z.string().openapi({ example: 'mingkang' }), - publisher: z.string().openapi({ example: '한빛아카데미' }), - publishedAt: z.string().openapi({ example: '20130730' }), - isbn: z.string().openapi({ example: '9788998756444' }), - image: z.string().openapi({ example: 'https://image.kyobobook.co.kr/images/book/xlarge/444/x9788998756444.jpg' }), - status: statusSchema.openapi({ example: 0 }), - categoryId: positiveInt.openapi({ example: 2}), - callsign: z.string().openapi({ example: 'C5.13.v1.c2' }), - category: z.string().openapi({ example: '네트워크' }), - isLendable: positiveInt.openapi({ example: 1 }), + id: nonNegativeInt.openapi({ example: 3 }), + bookId: nonNegativeInt.openapi({ example: 3 }), + bookInfoId: nonNegativeInt.openapi({ example: 2 }), + title: z + .string() + .openapi({ example: 'TCP IP 윈도우 소켓 프로그래밍(IT Cookbook 한빛 교재 시리즈 124)' }), + author: z.string().openapi({ example: '김선우' }), + donator: z.string().openapi({ example: 'mingkang' }), + publisher: z.string().openapi({ example: '한빛아카데미' }), + publishedAt: z.string().openapi({ example: '20130730' }), + isbn: z.string().openapi({ example: '9788998756444' }), + image: z.string().openapi({ + example: 'https://image.kyobobook.co.kr/images/book/xlarge/444/x9788998756444.jpg', + }), + status: statusSchema.openapi({ example: 0 }), + categoryId: nonNegativeInt.openapi({ example: 2 }), + callSign: z.string().openapi({ example: 'C5.13.v1.c2' }), + category: z.string().openapi({ example: '네트워크' }), + isLendable: nonNegativeInt.openapi({ example: 1 }), }); export const updateBookResponseSchema = z.literal('책 정보가 수정되었습니다.'); -export const updateDonatorResponseSchema = z.literal('기부자 정보가 수정되었습니다.'); - export const createBookResponseSchema = z.object({ - callSign: z.string().openapi({ example: 'K23.17.v1.c1' }), + callSign: z.string().openapi({ example: 'K23.17.v1.c1' }), }); -export const isbnNotFoundSchema = mkErrorMessageSchema('ISBN_NOT_FOUND').describe('국립중앙도서관 API에서 ISBN 검색이 실패하였습니다.'); - -export const naverBookNotFoundSchema = mkErrorMessageSchema('NAVER_BOOK_NOT_FOUND').describe('네이버 책검색 API에서 ISBN 검색이 실패'); - -export const insertionFailureSchema = mkErrorMessageSchema('INSERT_FAILURE').describe('예상치 못한 에러로 책 정보 insert에 실패함.'); +export const insertionFailureSchema = mkErrorMessageSchema('INSERT_FAILURE').describe( + '예상치 못한 에러로 책 정보 insert에 실패함.', +); -export const categoryNotFoundSchema = mkErrorMessageSchema('CATEGORY_NOT_FOUND').describe('보내준 카테고리 ID에 해당하는 callsign을 찾을 수 없음'); +export const categoryNotFoundSchema = mkErrorMessageSchema('CATEGORY_NOT_FOUND').describe( + '보내준 카테고리 ID에 해당하는 callsign을 찾을 수 없음', +); -export const formatErrorSchema = mkErrorMessageSchema('FORMAT_ERROR').describe('입력한 pubdate가 알맞은 형식이 아님. 기대하는 형식 "20220807"'); +export const pubdateFormatErrorSchema = mkErrorMessageSchema('PUBDATE_FORMAT_ERROR').describe( + '입력한 pubdate가 알맞은 형식이 아님. 기대하는 형식 "20220807"', +); -export const unknownPatchErrorSchema = mkErrorMessageSchema('PATCH_ERROR').describe('예상치 못한 에러로 patch에 실패.'); +export const unknownPatchErrorSchema = + mkErrorMessageSchema('PATCH_ERROR').describe('예상치 못한 에러로 patch에 실패.'); -export const nonDataErrorSchema = mkErrorMessageSchema('NO_DATA_ERROR').describe('DATA가 적어도 한 개는 필요.'); +export const nonDataErrorSchema = + mkErrorMessageSchema('NO_DATA_ERROR').describe('DATA가 적어도 한 개는 필요.'); diff --git a/contracts/src/histories/schema.ts b/contracts/src/histories/schema.ts deleted file mode 100644 index a671cc38..00000000 --- a/contracts/src/histories/schema.ts +++ /dev/null @@ -1,38 +0,0 @@ -import { dateLike, metaSchema, positiveInt } from '../shared'; -import { z } from '../zodWithOpenapi'; - -export const historiesGetMyQuerySchema = z.object({ - query: z.string().optional(), - page: z.number().int().nonnegative().default(0), - limit: z.number().int().nonnegative().default(10), -}); - -export const historiesGetQuerySchema = z.object({ - query: z.string().optional(), - type: z.enum(['user', 'title', 'callsign']).optional(), - page: z.number().int().nonnegative().default(0), - limit: z.number().int().nonnegative().default(10), -}); - -export const historiesGetResponseSchema = z.object({ - items: z.array( - z.object({ - id: positiveInt, - lendingCondition: z.string(), - login: z.string(), - returningCondition: z.string(), - penaltyDays: z.number().int().nonnegative(), - callSign: z.string(), - title: z.string(), - bookInfoId: positiveInt, - image: z.string(), - createdAt: dateLike, - returnedAt: dateLike, - updatedAt: dateLike, - dueDate: dateLike, - lendingLibrarianNickName: z.string(), - returningLibrarianNickname: z.string(), - }), - ), - meta: metaSchema, -}); diff --git a/contracts/src/index.ts b/contracts/src/index.ts index 88dc7add..61c8b649 100644 --- a/contracts/src/index.ts +++ b/contracts/src/index.ts @@ -1,10 +1,9 @@ import { initContract } from '@ts-rest/core'; import { reviewsContract } from './reviews'; -import { historiesContract } from './histories'; +import { lendingsContract } from './lendings/mod'; import { usersContract } from './users'; import { likesContract } from './likes'; -import { stockContract } from './stock'; -import { tagContract } from './tags'; +import { booksContract } from './books'; export * from './reviews'; export * from './shared'; @@ -16,13 +15,10 @@ export const contract = c.router( { // likes: likesContract, reviews: reviewsContract, - histories: historiesContract, - - stock: stockContract, - // TODO(@nyj001012): 태그 서비스 작성 - // tags: tagContract, + lendings: lendingsContract, + // books: booksContract, // TODO(@scarf005): 유저 서비스 작성 -// users: usersContract, + // users: usersContract, }, { pathPrefix: '/api/v2', diff --git a/contracts/src/histories/index.ts b/contracts/src/lendings/mod.ts similarity index 69% rename from contracts/src/histories/index.ts rename to contracts/src/lendings/mod.ts index 3726ef0f..c9fde889 100644 --- a/contracts/src/histories/index.ts +++ b/contracts/src/lendings/mod.ts @@ -11,21 +11,21 @@ export * from './schema'; // contract 를 생성할 때, router 함수를 사용하여 api 를 생성 const c = initContract(); -export const historiesContract = c.router({ - getMyHistories: { +export const lendingsContract = c.router({ + getMine: { method: 'GET', - path: '/mypage/histories', - description: '마이페이지에서 본인의 대출 기록을 가져온다.', + path: '/mypage/lendings', + summary: '내 대출 기록을 가져옵니다.', query: historiesGetMyQuerySchema, responses: { 200: historiesGetResponseSchema, 401: unauthorizedSchema, }, }, - getAllHistories: { + get: { method: 'GET', - path: '/histories', - description: '사서가 전체 대출 기록을 가져온다.', + path: '/lendings', + summary: '사서가 전체 대출 기록을 가져옵니다.', query: historiesGetQuerySchema, responses: { 200: historiesGetResponseSchema, diff --git a/contracts/src/lendings/schema.ts b/contracts/src/lendings/schema.ts new file mode 100644 index 00000000..be0898f9 --- /dev/null +++ b/contracts/src/lendings/schema.ts @@ -0,0 +1,38 @@ +import { dateLike, metaSchema, nonNegativeInt } from '../shared'; +import { z } from '../zodWithOpenapi'; + +export const historiesGetMyQuerySchema = z.object({ + query: z.string().optional(), + page: nonNegativeInt.default(0), + limit: nonNegativeInt.default(10), +}); + +export const historiesGetQuerySchema = z.object({ + query: z.string().optional(), + type: z.enum(['user', 'title', 'callsign']).optional(), + page: nonNegativeInt.default(0), + limit: nonNegativeInt.default(10), +}); + +export const historiesGetResponseSchema = z.object({ + items: z.array( + z.object({ + id: nonNegativeInt, + lendingCondition: z.string().nullable(), + login: z.string().nullable(), + returningCondition: z.string().nullable(), + penaltyDays: nonNegativeInt.nullable(), + callSign: z.string(), + title: z.string().nullable(), + bookInfoId: nonNegativeInt.nullable(), + image: z.string().nullable(), + createdAt: dateLike.nullable(), + returnedAt: dateLike.nullable(), + updatedAt: dateLike, + dueDate: dateLike.nullable(), + lendingLibrarianNickName: z.string().nullable(), + returningLibrarianNickname: z.string().nullable(), + }), + ), + meta: metaSchema, +}); diff --git a/contracts/src/likes/index.ts b/contracts/src/likes/index.ts index 5df56fa0..3335b466 100644 --- a/contracts/src/likes/index.ts +++ b/contracts/src/likes/index.ts @@ -1,5 +1,5 @@ import { initContract } from '@ts-rest/core'; -import { bookInfoIdSchema, bookInfoNotFoundSchema, positiveInt } from '../shared'; +import { bookInfoIdSchema, bookInfoNotFoundSchema, nonNegativeInt } from '../shared'; import { z } from '../zodWithOpenapi'; import { likeNotFoundSchema, likeResponseSchema } from './schema'; @@ -10,12 +10,12 @@ export const likesContract = c.router( post: { method: 'POST', path: '/:bookInfoId/like', - description: '책에 좋아요를 누릅니다.', + summary: '책에 좋아요를 누릅니다.', pathParams: z.object({ bookInfoId: bookInfoIdSchema }), body: null, responses: { 200: z.object({ - userId: positiveInt, + userId: nonNegativeInt, bookInfoId: bookInfoIdSchema, }), }, @@ -23,8 +23,7 @@ export const likesContract = c.router( get: { method: 'GET', path: '/:bookInfoId/like', - summary: 'Like 정보를 가져온다.', - description: '사용자가 좋아요 버튼을 누르면 좋아요 개수를 가져온다.', + summary: '좋아요 개수를 가져옵니다.', pathParams: z.object({ bookInfoId: bookInfoIdSchema }), responses: { 200: likeResponseSchema, @@ -34,7 +33,7 @@ export const likesContract = c.router( delete: { method: 'DELETE', path: '/:bookInfoId/like', - description: 'delete a like', + summary: '좋아요를 취소합니다', pathParams: z.object({ bookInfoId: bookInfoIdSchema }), body: null, responses: { diff --git a/contracts/src/likes/schema.ts b/contracts/src/likes/schema.ts index c5338101..e611f9a9 100644 --- a/contracts/src/likes/schema.ts +++ b/contracts/src/likes/schema.ts @@ -1,5 +1,5 @@ import z from 'zod'; -import { bookInfoIdSchema, positiveInt } from '../shared'; +import { bookInfoIdSchema, nonNegativeInt } from '../shared'; export const likeNotFoundSchema = z.object({ code: z.literal('LIKE_NOT_FOUND'), @@ -10,7 +10,7 @@ export const likeResponseSchema = z .object({ bookInfoId: bookInfoIdSchema, isLiked: z.boolean(), - likeNum: positiveInt, + likeNum: nonNegativeInt, }) .openapi({ examples: [ diff --git a/contracts/src/reviews/index.ts b/contracts/src/reviews/index.ts index 18830248..83a3206e 100644 --- a/contracts/src/reviews/index.ts +++ b/contracts/src/reviews/index.ts @@ -1,13 +1,19 @@ import { initContract } from '@ts-rest/core'; import { z } from 'zod'; -import { bookInfoIdSchema, bookInfoNotFoundSchema, metaPaginatedSchema, offsetPaginatedSchema, paginatedSearchSchema, visibility } from '../shared'; +import { + bookInfoIdSchema, + bookInfoNotFoundSchema, + metaPaginatedSchema, + paginatedSearchSchema, + visibility, +} from '../shared'; import { contentSchema, mutationDescription, reviewIdPathSchema, reviewNotFoundSchema, + reviewSchema } from './schema'; -import { reviewSchema } from './schema' export * from './schema'; @@ -23,16 +29,16 @@ export const reviewsContract = c.router( search: z.string().optional().describe('도서 제목 또는 리뷰 작성자 닉네임'), visibility, }), - description: '전체 도서 리뷰 목록을 조회합니다.', + summary: '전체 도서 리뷰 목록을 조회합니다.', responses: { - 200: metaPaginatedSchema(reviewSchema) + 200: metaPaginatedSchema(reviewSchema), }, }, post: { method: 'POST', path: '/', query: z.object({ bookInfoId: bookInfoIdSchema.openapi({ description: '도서 ID' }) }), - description: '책 리뷰를 작성합니다.', + summary: '책 리뷰를 작성합니다.', body: contentSchema, responses: { 201: z.literal('리뷰가 작성되었습니다.'), @@ -43,7 +49,7 @@ export const reviewsContract = c.router( method: 'PATCH', path: '/:reviewsId', pathParams: reviewIdPathSchema, - description: '책 리뷰의 비활성화 여부를 토글 방식으로 변환합니다.', + summary: '책 리뷰의 비활성화 여부를 토글 방식으로 변환합니다.', body: null, responses: { 200: z.literal('리뷰 공개 여부가 업데이트되었습니다.'), diff --git a/contracts/src/reviews/schema.ts b/contracts/src/reviews/schema.ts index 24d006f4..fcbdc82b 100644 --- a/contracts/src/reviews/schema.ts +++ b/contracts/src/reviews/schema.ts @@ -1,7 +1,7 @@ -import { mkErrorMessageSchema, positiveInt } from '../shared'; +import { mkErrorMessageSchema, nonNegativeInt } from '../shared'; import { z } from '../zodWithOpenapi'; -export const reviewsIdSchema = positiveInt.describe('도서 리뷰 ID'); +export const reviewsIdSchema = nonNegativeInt.describe('도서 리뷰 ID'); export const contentSchema = z.object({ content: z.string().min(10).max(420).openapi({ example: '책 정말 재미있어요 10글자 넘었다' }), @@ -16,15 +16,21 @@ export const reviewNotFoundSchema = export const mutationDescription = (action: '수정' | '삭제') => `리뷰를 ${action}합니다. 작성자 또는 관리자만 ${action} 가능합니다.`; -export const sqlBool = z.number().int().gte(0).lte(1).transform(x => Boolean(x)).or(z.boolean()); +export const sqlBool = z + .number() + .int() + .gte(0) + .lte(1) + .transform((x) => Boolean(x)) + .or(z.boolean()); export const reviewSchema = z.object({ id: z.number().int(), userId: z.number().int(), nickname: z.string().nullable(), bookInfoId: z.number().int(), - createdAt: z.date().transform(x => x.toISOString()), + createdAt: z.date().transform((x) => x.toISOString()), title: z.string().nullable(), content: z.string(), disabled: sqlBool, -}) +}); diff --git a/contracts/src/shared.ts b/contracts/src/shared.ts index 47e08dc9..edf2a796 100644 --- a/contracts/src/shared.ts +++ b/contracts/src/shared.ts @@ -1,13 +1,18 @@ import { z } from './zodWithOpenapi'; -export const positiveInt = z.coerce.number().int().nonnegative(); +export const nonNegativeInt = z.coerce.number().int().nonnegative(); -export const dateLike = z.union([z.date(), z.string()]).transform(String) +export const dateLike = z.union([z.date(), z.string()]).transform(String); -export const bookInfoIdSchema = positiveInt.describe('개별 도서 ID'); - -export const statusSchema = z.enum(["ok", "lost", "damaged"]); +export const bookInfoIdSchema = nonNegativeInt.describe('개별 도서 ID'); +export enum enumStatus { + 'ok', + 'lost', + 'damaged', + 'designate', +} +export const statusSchema = z.nativeEnum(enumStatus); /** * 오류 메시지를 통일된 형식으로 보여주는 zod 스키마를 생성합니다. @@ -25,24 +30,21 @@ export const statusSchema = z.enum(["ok", "lost", "damaged"]); export const mkErrorMessageSchema = (code: T) => z.object({ code: z.literal(code) as z.ZodLiteral }); -export const unauthorizedSchema = mkErrorMessageSchema('UNAUTHORIZED').describe( - '권한이 없습니다.', -); +export const unauthorizedSchema = mkErrorMessageSchema('UNAUTHORIZED').describe('권한이 없습니다.'); export const bookNotFoundSchema = mkErrorMessageSchema('BOOK_NOT_FOUND').describe('해당 도서가 존재하지 않습니다'); -export const bookInfoNotFoundSchema = mkErrorMessageSchema('BOOK_INFO_NOT_FOUND').describe('해당 도서 연관 정보가 존재하지 않습니다'); - -export const serverErrorSchema = mkErrorMessageSchema('SERVER_ERROR').describe('서버에서 오류가 발생했습니다.'); +export const bookInfoNotFoundSchema = + mkErrorMessageSchema('BOOK_INFO_NOT_FOUND').describe('해당 도서 연관 정보가 존재하지 않습니다'); export const badRequestSchema = mkErrorMessageSchema('BAD_REQUEST').describe('잘못된 요청입니다.'); export const forbiddenSchema = mkErrorMessageSchema('FORBIDDEN').describe('권한이 없습니다.'); export const metaSchema = z.object({ - totalItems: positiveInt.describe('전체 검색 결과 수 ').openapi({ example: 42 }), - totalPages: positiveInt.describe('전체 결과 페이지 수').openapi({ example: 5 }), + totalItems: nonNegativeInt.describe('전체 검색 결과 수 ').openapi({ example: 42 }), + totalPages: nonNegativeInt.describe('전체 결과 페이지 수').openapi({ example: 5 }), // itemCount: positiveInt.describe('현재 페이지의 검색 결과 수').openapi({ example: 3 }), // itemsPerPage: positiveInt.describe('한 페이지당 검색 결과 수').openapi({ example: 10 }), // currentPage: positiveInt.describe('현재 페이지').openapi({ example: 1 }), @@ -69,10 +71,12 @@ export const offsetPaginatedSchema = >(itemSchema: T) = hasPrevPage: z.boolean().optional().describe('이전 페이지가 존재하는지 여부'), }); -export const visibility = z.enum([ 'all', 'public', 'hidden' ]).default('public').describe('공개 상태'); - +export const visibility = z + .enum(['all', 'public', 'hidden']) + .default('public') + .describe('공개 상태'); export const paginationQuerySchema = z.object({ - page: positiveInt.default(1).optional().openapi({ example: 1 }), - limit: positiveInt.default(10).optional().openapi({ example: 10 }), + page: nonNegativeInt.default(1).optional().openapi({ example: 1 }), + limit: nonNegativeInt.default(10).optional().openapi({ example: 10 }), }); diff --git a/contracts/src/stock/index.ts b/contracts/src/stock/index.ts deleted file mode 100644 index 981c5d60..00000000 --- a/contracts/src/stock/index.ts +++ /dev/null @@ -1,36 +0,0 @@ -import { initContract } from '@ts-rest/core'; -import { - stockGetQuerySchema, - stockGetResponseSchema, - stockPatchBodySchema, - stockPatchResponseSchema -} from './schema'; -import { bookNotFoundSchema } from '../shared'; - -const c = initContract(); - -export const stockContract = c.router( - { - get: { - method: 'GET', - path: '/search', - description: '책 재고 정보를 검색해 온다.', - query: stockGetQuerySchema, - responses: { - 200: stockGetResponseSchema, - // 특정한 에러케이스가 생각나지 않습니다. - }, - }, - patch: { - method: 'PATCH', - path: '/update', - description: '책 재고를 확인하고 수정일시를 업데이트한다.', - body: stockPatchBodySchema, - responses: { - 200: stockPatchResponseSchema, - 404: bookNotFoundSchema, - }, - }, - }, - { pathPrefix: '/stock'}, -); \ No newline at end of file diff --git a/contracts/src/stock/schema.ts b/contracts/src/stock/schema.ts deleted file mode 100644 index 5aa14ce7..00000000 --- a/contracts/src/stock/schema.ts +++ /dev/null @@ -1,37 +0,0 @@ -import { dateLike, metaSchema, positiveInt } from '../shared'; -import { z } from '../zodWithOpenapi'; - -export const bookIdSchema = positiveInt.describe('업데이트 할 도서 ID'); - -export const stockPatchBodySchema = z.object({ - id: bookIdSchema.openapi({ example: 0 }), -}); - -export const stockPatchResponseSchema = z.literal('재고 상태가 업데이트되었습니다.'); - -export const stockGetQuerySchema = z.object({ - page: positiveInt.default(0), - limit: positiveInt.default(10), -}); - -export const stockGetResponseSchema = z.object({ - items: z.array( - z.object({ - bookId: positiveInt, - bookInfoId: positiveInt, - title: z.string(), - author: z.string(), - donator: z.string(), - publisher: z.string(), - publishedAt: dateLike, - isbn: z.string(), - image: z.string(), - status: positiveInt, - categoryId: positiveInt, - callSign: z.string(), - category: z.string(), - updatedAt: dateLike, - }), - ), - meta: metaSchema, -}); diff --git a/contracts/src/tags/index.ts b/contracts/src/tags/index.ts deleted file mode 100644 index 76c218f3..00000000 --- a/contracts/src/tags/index.ts +++ /dev/null @@ -1,166 +0,0 @@ -import { initContract } from '@ts-rest/core'; -import { z } from 'zod'; -import { - subDefaultTagQuerySchema, - subDefaultTagResponseSchema, - superDefaultTagResponseSchema, - superTagIdQuerySchema, - subTagResponseSchema, - tagsOfBookResponseSchema, - modifySuperTagBodySchema, - modifyTagResponseSchema, - incorrectTagFormatSchema, - alreadyExistTagSchema, - defaultTagCannotBeModifiedSchema, - modifySubTagBodySchema, - NoAuthorityToModifyTagSchema, - mergeTagsBodySchema, - invalidTagIdSchema, - createTagBodySchema, - duplicateTagSchema, - tagIdSchema, -} from './schema'; -import { - bookInfoIdSchema, - bookInfoNotFoundSchema, - paginationQuerySchema, -} from '../shared'; - -const c = initContract(); - -export const tagContract = c.router( - { - getSubDefault: { - method: 'GET', - path: '', - summary: '서브/디폴트 태그 정보를 검색한다.', - description: '서브/디폴트 태그 정보를 검색한다. 이는 태그 관리 페이지에서 사용한다.', - query: subDefaultTagQuerySchema, - responses: { - 200: subDefaultTagResponseSchema, - }, - }, - getSuperDefaultForMain: { - method: 'GET', - path: '/main', - summary: '메인 페이지에서 사용할 태그 목록을 가져온다.', - description: '슈퍼 태그(노출되는 태그), 디폴트 태그(노출되지 않고 분류되지 않은 태그)를 랜덤한 순서로 가져온다. 이는 메인 페이지에서 사용된다.', - query: paginationQuerySchema.omit({ page: true }), - responses: { - 200: superDefaultTagResponseSchema, - }, - }, - getSubOfSuperTag: { - method: 'GET', - path: '/{superTagId}/sub', - summary: '슈퍼 태그에 속한 서브 태그 목록을 가져온다.', - description: 'superTagId에 해당하는 슈퍼 태그에 속한 서브 태그 목록을 가져온다. 태그 병합 페이지에서 슈퍼 태그의 서브 태그를 가져올 때 사용한다.', - pathParams: superTagIdQuerySchema, - responses: { - 200: subTagResponseSchema, - }, - }, - getSubOfSuperTagForAdmin: { - method: 'GET', - path: '/manage/{superTagId}/sub', - summary: '슈퍼 태그에 속한 서브 태그 목록을 가져온다.', - description: 'superTagId에 해당하는 슈퍼 태그에 속한 서브 태그 목록을 가져온다. 태그 관리 페이지에서 슈퍼 태그의 서브 태그를 가져올 때 사용한다.', - pathParams: superTagIdQuerySchema, - responses: { - 200: subTagResponseSchema, - }, - }, - getTagsOfBook: { - method: 'GET', - path: '/{bookInfoId}', - summary: '도서에 등록된 슈퍼 태그, 디폴트 태그 목록을 가져온다.', - description: '슈퍼 태그(노출되는 태그), 디폴트 태그(노출되지 않고 분류되지 않은 태그)를 가져온다. 이는 도서 상세 페이지 및 태그 병합 페이지에서 사용된다.', - pathParams: bookInfoIdSchema, - responses: { - 200: tagsOfBookResponseSchema, - }, - }, - modifySuperTag: { - method: 'PATCH', - path: '/super', - description: '슈퍼 태그를 수정한다.', - body: modifySuperTagBodySchema, - responses: { - 204: z.null(), - 400: z.union([alreadyExistTagSchema, defaultTagCannotBeModifiedSchema]), - }, - }, - modifySubTag: { - method: 'PATCH', - path: '/sub', - description: '서브 태그를 수정한다.', - body: modifySubTagBodySchema, - responses: { - 200: modifyTagResponseSchema, - 900: incorrectTagFormatSchema, - 901: NoAuthorityToModifyTagSchema, - }, - }, - mergeTags: { - method: 'PATCH', - path: '/{bookInfoId}/merge', - description: '태그를 병합한다.', - pathParams: bookInfoIdSchema, - body: mergeTagsBodySchema, - responses: { - 200: modifyTagResponseSchema, - 900: incorrectTagFormatSchema, - 902: alreadyExistTagSchema, - 906: defaultTagCannotBeModifiedSchema, - 910: invalidTagIdSchema, - }, - }, - createDefaultTag: { - method: 'POST', - path: '/default', - description: '디폴트 태그를 생성한다. 태그 길이는 42자 이하여야 한다.', - body: createTagBodySchema, - responses: { - 201: modifyTagResponseSchema, - 900: incorrectTagFormatSchema, - 907: bookInfoNotFoundSchema, - 909: duplicateTagSchema, - }, - }, - createSuperTag: { - method: 'POST', - path: '/super', - description: '슈퍼 태그를 생성한다. 태그 길이는 42자 이하여야 한다.', - body: createTagBodySchema, - responses: { - 201: modifyTagResponseSchema, - 900: incorrectTagFormatSchema, - 907: bookInfoNotFoundSchema, - 909: duplicateTagSchema, - }, - }, - deleteSubDefaultTag: { - method: 'DELETE', - path: '/sub/{tagId}', - description: '서브/디폴트 태그를 삭제한다.', - pathParams: tagIdSchema, - body: null, - responses: { - 200: modifyTagResponseSchema, - 910: invalidTagIdSchema, - }, - }, - deleteSuperTag: { - method: 'DELETE', - path: '/super/{tagId}', - description: '슈퍼 태그를 삭제한다.', - pathParams: tagIdSchema, - body: null, - responses: { - 200: modifyTagResponseSchema, - 910: invalidTagIdSchema, - }, - }, - }, - { pathPrefix: '/tags' }, -); diff --git a/contracts/src/tags/schema.ts b/contracts/src/tags/schema.ts deleted file mode 100644 index 5ed6db43..00000000 --- a/contracts/src/tags/schema.ts +++ /dev/null @@ -1,198 +0,0 @@ -import { - dateLike, metaSchema, mkErrorMessageSchema, positiveInt, -} from '../shared'; -import { z } from '../zodWithOpenapi'; - -export const subDefaultTagQuerySchema = z.object({ - page: positiveInt.optional().default(0), - limit: positiveInt.optional().default(10), - visibility: z.enum(['public', 'private']).optional(), - query: z.string().optional().openapi({ example: '개발자의 코드' }), -}); - -export const subDefaultTagResponseSchema = z.object({ - items: z.array( - z.object({ - bookInfoId: positiveInt.openapi({ - description: '태그가 등록된 도서의 info id', - example: 1, - }), - title: z.string().openapi({ - description: '태그가 등록된 도서의 제목', - example: '개발자의 코드', - }), - id: positiveInt.openapi({ - description: '태그 고유 id', - example: 1, - }), - createdAt: dateLike.openapi({ - description: '태그가 등록된 시간', - example: '2023-04-12', - }), - login: z.string().openapi({ - description: '태그를 작성한 카뎃의 닉네임', - example: 'yena', - }), - content: z.string().openapi({ - description: '서브/디폴트 태그의 내용', - example: 'yena가_추천하는', - }), - superContent: z.string().openapi({ - description: '슈퍼 태그의 내용', - example: '1서클_추천_책', - }), - visibility: z.enum(['public', 'private']).openapi({ - description: '태그의 공개 여부. 공개는 public, 비공개는 private', - example: 'private', - }), - meta: metaSchema, - }), - ), -}); - -export const superDefaultTagResponseSchema = z.object({ - items: z.array( - z.object({ - createdAt: dateLike.openapi({ - description: '태그 생성일', - example: '2023-04-12', - }), - content: z.string().openapi({ - description: '태그 내용', - example: '1서클_추천_책', - }), - count: positiveInt.openapi({ - description: '슈퍼 태그에 속한 서브 태그의 개수. 디폴트 태그는 0', - example: 1, - }), - type: z.enum(['super', 'default']).openapi({ - description: '태그의 타입. 슈퍼 태그는 super, 디폴트 태그는 default', - example: 'super', - }), - }), - ), -}); - -export const superTagIdQuerySchema = z.object({ - superTagId: positiveInt.openapi({ - description: '슈퍼 태그의 id', - example: 1, - }), -}); - -export const subTagResponseSchema = z.object({ - id: positiveInt.openapi({ - description: '태그 고유 id', - example: 1, - }), - login: z.string().openapi({ - description: '태그를 작성한 카뎃의 닉네임', - example: 'yena', - }), - content: z.string().openapi({ - description: '서브/디폴트 태그의 내용', - example: 'yena가_추천하는', - }), -}); - -export const tagsOfBookResponseSchema = z.object({ - items: z.array( - z.object({ - id: positiveInt.openapi({ - description: '태그 고유 id', - example: 1, - }), - login: z.string().openapi({ - description: '태그를 작성한 카뎃의 닉네임', - example: 'yena', - }), - content: z.string().openapi({ - description: '슈퍼/디폴트 태그의 내용', - example: 'yena가_추천하는', - }), - type: z.enum(['super', 'default']).openapi({ - description: '태그의 타입. 슈퍼 태그는 super, 디폴트 태그는 default', - example: 'super', - }), - count: positiveInt.openapi({ - description: '슈퍼 태그에 속한 서브 태그의 개수. 디폴트 태그는 0', - example: 1, - }), - }), - ), -}); - -export const modifySuperTagBodySchema = z.object({ - id: positiveInt.openapi({ - description: '수정할 슈퍼 태그의 id', - example: 1, - }), - content: z.string().openapi({ - description: '수정할 슈퍼 태그의 내용', - example: '1서클_추천_책', - }), -}); - -export const modifyTagResponseSchema = z.literal('success'); - -export const incorrectTagFormatSchema = mkErrorMessageSchema('INCORRECT_TAG_FORMAT') - .describe('태그 형식이 올바르지 않습니다.'); - -export const alreadyExistTagSchema = mkErrorMessageSchema('ALREADY_EXIST_TAG') - .describe('이미 존재하는 태그입니다.'); - -export const defaultTagCannotBeModifiedSchema = mkErrorMessageSchema('DEFAULT_TAG_CANNOT_BE_MODIFIED') - .describe('디폴트 태그는 수정할 수 없습니다.'); - -export const modifySubTagBodySchema = z.object({ - id: positiveInt.openapi({ - description: '수정할 서브 태그의 id', - example: 1, - }), - content: z.string().openapi({ - description: '수정할 서브 태그의 내용', - example: 'yena가_추천하는', - }), - visibility: z.enum(['public', 'private']).openapi({ - description: '태그의 공개 여부. 공개는 public, 비공개는 private', - example: 'private', - }), -}); - -export const NoAuthorityToModifyTagSchema = mkErrorMessageSchema('NO_AUTHORITY_TO_MODIFY_TAG') - .describe('태그를 수정할 권한이 없습니다.'); - -export const mergeTagsBodySchema = z.object({ - superTagId: positiveInt.nullable().openapi({ - description: '병합할 슈퍼 태그의 id. null이면 디폴트 태그로 병합됨을 의미한다.', - example: 1, - }), - subTagIds: z.array(positiveInt).openapi({ - description: '병합할 서브 태그의 id 목록', - example: [1, 2, 3], - }), -}); - -export const invalidTagIdSchema = mkErrorMessageSchema('INVALID_TAG_ID') - .describe('태그 id가 올바르지 않습니다.'); - -export const createTagBodySchema = z.object({ - bookInfoId: positiveInt.openapi({ - description: '태그를 등록할 도서의 info id', - example: 1, - }), - content: z.string().openapi({ - description: '태그 내용', - example: 'yena가_추천하는', - }), -}); - -export const duplicateTagSchema = mkErrorMessageSchema('DUPLICATE_TAG') - .describe('이미 존재하는 태그입니다.'); - -export const tagIdSchema = z.object({ - tagId: positiveInt.openapi({ - description: '태그의 id', - example: 1, - }), -}); diff --git a/contracts/src/users/index.ts b/contracts/src/users/index.ts index 58a9379c..25287096 100644 --- a/contracts/src/users/index.ts +++ b/contracts/src/users/index.ts @@ -1,5 +1,5 @@ import { initContract } from '@ts-rest/core'; -import { badRequestSchema, serverErrorSchema, forbiddenSchema } from '../shared'; +import { badRequestSchema } from '../shared'; import { searchUserSchema, searchUserResponseSchema, @@ -7,8 +7,6 @@ import { createUserResponseSchema, userIdSchema, updateUserSchema, - updatePrivateInfoSchema, - updateUserResponseSchema, } from './schema'; export * from './schema'; @@ -17,50 +15,35 @@ const c = initContract(); export const usersContract = c.router( { - searchUser: { + get: { method: 'GET', - path: '/search', - description: '유저 정보를 검색해 온다. query가 null이면 모든 유저를 검색한다.', + path: '/', + summary: '유저 정보를 검색해 온다. query가 null이면 모든 유저를 검색한다.', query: searchUserSchema, responses: { 200: searchUserResponseSchema, 400: badRequestSchema, - 500: serverErrorSchema, }, }, - createUser: { + post: { method: 'POST', - path: '/create', - description: '유저를 생성한다.', + path: '/', + summary: '유저를 생성한다.', body: createUserSchema, responses: { 201: createUserResponseSchema, 400: badRequestSchema, - 500: serverErrorSchema, }, }, - updateUser: { + patch: { method: 'PATCH', - path: '/update/:id', - description: '유저 정보를 변경한다.', + path: '/:id', + summary: '유저 정보를 변경한다.', pathParams: userIdSchema, body: updateUserSchema, responses: { 200: updateUserSchema, 400: badRequestSchema, - 500: serverErrorSchema, - }, - }, - updatePrivateInfo: { - method: 'PATCH', - path: '/myupdate', - description: '유저의 정보를 변경한다.', - body: updatePrivateInfoSchema, - responses: { - 200: updateUserResponseSchema, - 400: badRequestSchema, - 403: forbiddenSchema, - 500: serverErrorSchema, }, }, }, diff --git a/contracts/src/users/schema.ts b/contracts/src/users/schema.ts index 2702de25..9400fbf7 100644 --- a/contracts/src/users/schema.ts +++ b/contracts/src/users/schema.ts @@ -1,46 +1,72 @@ -import { metaSchema, mkErrorMessageSchema, positiveInt } from '../shared'; +import { metaSchema, mkErrorMessageSchema, nonNegativeInt } from '../shared'; import { z } from '../zodWithOpenapi'; export const searchUserSchema = z.object({ nicknameOrEmail: z.string().optional().describe('검색할 유저의 nickname or email'), - page: positiveInt.optional().default(0).describe('페이지'), - limit: positiveInt.optional().default(10).describe('한 페이지에 들어올 검색결과 수'), - id: positiveInt.optional().describe('검색할 유저의 id'), + page: nonNegativeInt.optional().default(0).describe('페이지'), + limit: nonNegativeInt.optional().default(10).describe('한 페이지에 들어올 검색결과 수'), + id: nonNegativeInt.optional().describe('검색할 유저의 id'), }); -const reservationSchema = z.object({ - reservationId: positiveInt.describe('예약 번호').openapi({ example: 17 }), - reservedBookInfoId: positiveInt.describe('예약된 도서 번호').openapi({ example: 34 }), - endAt: z.coerce.string().nullable().describe('예약 만료 날짜').openapi({ example: '2023-08-16' }), - ranking: z.coerce.string().nullable().describe('예약 순위').openapi({ example: '1' }), - title: z.string().describe('예약된 도서 제목').openapi({ example: '생활코딩! Node.js 노드제이에스 프로그래밍(위키북스 러닝스쿨 시리즈)' }), - author: z.string().describe('예약된 도서 저자').openapi({ example: '이고잉' }), - image: z.string().describe('예약된 도서 이미지').openapi({ example: 'https://image.kyobobook.co.kr/images/book/xlarge/383/x9791158392383.jpg' }), - userId: positiveInt.describe('예약한 유저 번호').openapi({ example: 1547 }), -}).optional(); +const reservationSchema = z + .object({ + reservationId: nonNegativeInt.describe('예약 번호').openapi({ example: 17 }), + reservedBookInfoId: nonNegativeInt.describe('예약된 도서 번호').openapi({ example: 34 }), + endAt: z.coerce + .string() + .nullable() + .describe('예약 만료 날짜') + .openapi({ example: '2023-08-16' }), + ranking: z.coerce.string().nullable().describe('예약 순위').openapi({ example: '1' }), + title: z + .string() + .describe('예약된 도서 제목') + .openapi({ example: '생활코딩! Node.js 노드제이에스 프로그래밍(위키북스 러닝스쿨 시리즈)' }), + author: z.string().describe('예약된 도서 저자').openapi({ example: '이고잉' }), + image: z.string().describe('예약된 도서 이미지').openapi({ + example: 'https://image.kyobobook.co.kr/images/book/xlarge/383/x9791158392383.jpg', + }), + userId: nonNegativeInt.describe('예약한 유저 번호').openapi({ example: 1547 }), + }) + .optional(); -const lendingSchema = z.object({ - userId: positiveInt.describe('대출한 유저 번호').openapi({ example: 1547 }), - bookInfoId: positiveInt.describe('대출한 도서 info id').openapi({ example: 20 }), - lendDate: z.coerce.string().describe('대출 날짜').openapi({ example: '2023-08-08T20:20:55.000Z' }), - lendingCondition: z.string().describe('대출 상태').openapi({ example: '이상 없음' }), - image: z.string().describe('대출한 도서 이미지').openapi({ example: 'https://image.kyobobook.co.kr/images/book/xlarge/642/x9791185585642.jpg' }), - author: z.string().describe('대출한 도서 저자').openapi({ example: '어제이 애그러월, 조슈아 갠스, 아비 골드파브' }), - title: z.string().describe('대출한 도서 제목').openapi({ example: '예측 기계' }), - duedate: z.coerce.string().describe('반납 예정 날짜').openapi({ example: '2023-08-22T20:20:55.000Z' }), - overDueDay: positiveInt.describe('연체된 날 수').openapi({ example: 0 }), - reservedNum: z.string().describe('예약된 수').openapi({ example: '0' }), -}).optional(); +const lendingSchema = z + .object({ + userId: nonNegativeInt.describe('대출한 유저 번호').openapi({ example: 1547 }), + bookInfoId: nonNegativeInt.describe('대출한 도서 info id').openapi({ example: 20 }), + lendDate: z.coerce + .string() + .describe('대출 날짜') + .openapi({ example: '2023-08-08T20:20:55.000Z' }), + lendingCondition: z.string().describe('대출 상태').openapi({ example: '이상 없음' }), + image: z.string().describe('대출한 도서 이미지').openapi({ + example: 'https://image.kyobobook.co.kr/images/book/xlarge/642/x9791185585642.jpg', + }), + author: z + .string() + .describe('대출한 도서 저자') + .openapi({ example: '어제이 애그러월, 조슈아 갠스, 아비 골드파브' }), + title: z.string().describe('대출한 도서 제목').openapi({ example: '예측 기계' }), + duedate: z.coerce + .string() + .describe('반납 예정 날짜') + .openapi({ example: '2023-08-22T20:20:55.000Z' }), + reservedNum: z.string().describe('예약된 수').openapi({ example: '0' }), + }) + .optional(); const searchUserResponseItemSchema = z.object({ - id: positiveInt.describe('유저 번호').openapi({ example: 1 }), + id: nonNegativeInt.describe('유저 번호').openapi({ example: 1 }), email: z.string().email().describe('이메일').openapi({ example: 'kyungsle@gmail.com' }), nickname: z.string().describe('닉네임').openapi({ example: 'kyungsle' }), - intraId: positiveInt.describe('인트라 고유 번호').openapi({ example: '10068' }), + intraId: nonNegativeInt.describe('인트라 고유 번호').openapi({ example: '10068' }), slack: z.string().describe('slack 멤버 Id').openapi({ example: 'U035MUEUGKW' }), - penaltyEndDate: z.coerce.string().optional().describe('연체 패널티 끝나는 날짜').openapi({ example: '2022-05-22' }), - overDueDay: z.coerce.string().default('0').describe('현재 연체된 날 수').openapi({ example: '0' }), - role: positiveInt.describe('유저 권한').openapi({ example: 2 }), + penaltyEndDate: z.coerce + .string() + .optional() + .describe('연체 패널티 끝나는 날짜') + .openapi({ example: '2022-05-22' }), + role: nonNegativeInt.describe('유저 권한').openapi({ example: 2 }), reservations: z.array(reservationSchema).describe('해당 유저의 예약 정보'), lendings: z.array(lendingSchema).describe('해당 유저의 대출 정보'), }); @@ -58,20 +84,17 @@ export const createUserSchema = z.object({ export const createUserResponseSchema = z.literal('유저 생성 성공!'); export const userIdSchema = z.object({ - id: positiveInt.describe('유저 id 값').openapi({ example: 1 }), + id: nonNegativeInt.describe('유저 id 값').openapi({ example: 1 }), }); export const updateUserSchema = z.object({ nickname: z.string().optional().describe('닉네임').openapi({ example: 'kyungsle' }), - intraId: positiveInt.optional().describe('인트라 고유 번호').openapi({ example: '10068' }), + intraId: nonNegativeInt.optional().describe('인트라 고유 번호').openapi({ example: '10068' }), slack: z.string().optional().describe('slack 멤버 Id').openapi({ example: 'U035MUEUGKW' }), - role: positiveInt.optional().describe('유저 권한').openapi({ example: 2 }), - penaltyEndDate: z.coerce.string().optional().describe('연체 패널티 끝나는 날짜').openapi({ example: '2022-05-22' }), + role: nonNegativeInt.optional().describe('유저 권한').openapi({ example: 2 }), + penaltyEndDate: z.coerce + .string() + .optional() + .describe('연체 패널티 끝나는 날짜') + .openapi({ example: '2022-05-22' }), }); - -export const updatePrivateInfoSchema = z.object({ - email: z.string().email().optional().describe('이메일').openapi({ example: 'yena@student.42seoul.kr' }), - password: z.string().optional().describe('패스워드').openapi({ example: 'KingGodMajesty42' }), -}); - -export const updateUserResponseSchema = z.literal('유저 정보 변경 성공!'); diff --git a/package.json b/package.json index d47d4b24..0f5774cd 100644 --- a/package.json +++ b/package.json @@ -4,11 +4,13 @@ }, "dependencies": { "typescript": "5.1.6", - "zod": "^3.22.2", - "@ts-rest/core": "^3.28.0" + "@ts-rest/core": "^3.28.0", + "zod": "^3.22.2" }, "devDependencies": { "@types/node": "18.16.1", - "rome": "^12.1.3" + "eslint-config-prettier": "^9.0.0", + "rome": "^12.1.3", + "typescript": "5.1.6" } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 3abb761b..a3f59d47 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1,4 +1,4 @@ -lockfileVersion: '6.0' +lockfileVersion: '6.1' settings: autoInstallPeers: true @@ -21,6 +21,9 @@ importers: '@types/node': specifier: 18.16.1 version: 18.16.1 + eslint-config-prettier: + specifier: ^9.0.0 + version: 9.0.0(eslint@8.52.0) rome: specifier: ^12.1.3 version: 12.1.3 @@ -30,21 +33,24 @@ importers: '@jiphyeonjeon-42/contracts': specifier: workspace:* version: link:../contracts + '@mapbox/node-pre-gyp': + specifier: ^1.0.11 + version: 1.0.11 '@slack/web-api': specifier: ^6.7.1 version: 6.7.1 '@ts-rest/express': specifier: ^3.28.0 - version: 3.28.0(@ts-rest/core@3.28.0)(express@4.17.2)(zod@3.22.2) + version: 3.28.0(@ts-rest/core@3.28.0)(express@4.18.2)(zod@3.22.4) '@ts-rest/open-api': specifier: ^3.28.0 - version: 3.28.0(@ts-rest/core@3.28.0)(zod@3.22.2) + version: 3.28.0(@ts-rest/core@3.28.0)(zod@3.22.4) axios: specifier: ^0.27.2 version: 0.27.2 bcrypt: - specifier: ^5.0.1 - version: 5.0.1 + specifier: ^5.1.1 + version: 5.1.1 cookie-parser: specifier: ^1.4.6 version: 1.4.6 @@ -52,17 +58,17 @@ importers: specifier: ^2.8.5 version: 2.8.5 date-fns: - specifier: ^2.29.3 - version: 2.29.3 + specifier: ^2.30.0 + version: 2.30.0 dotenv: - specifier: ^16.0.0 - version: 16.0.0 + specifier: ^16.3.1 + version: 16.3.1 express: - specifier: ^4.17.2 - version: 4.17.2 + specifier: ^4.18.2 + version: 4.18.2 express-rate-limit: - specifier: ^6.9.0 - version: 6.9.0(express@4.17.2) + specifier: ^6.11.2 + version: 6.11.2(express@4.18.2) hangul-js: specifier: ^0.2.6 version: 0.2.6 @@ -70,8 +76,8 @@ importers: specifier: ^2.0.0 version: 2.0.0 http-status: - specifier: ^1.5.0 - version: 1.5.0 + specifier: ^1.7.3 + version: 1.7.3 http-terminator: specifier: ^3.2.0 version: 3.2.0 @@ -79,29 +85,32 @@ importers: specifier: ^8.5.1 version: 8.5.1 kysely: - specifier: ^0.26.1 - version: 0.26.1 + specifier: ^0.26.3 + version: 0.26.3 kysely-paginate: specifier: ^0.2.0 - version: 0.2.0(kysely@0.26.1) + version: 0.2.0(kysely@0.26.3) morgan: specifier: ^1.10.0 version: 1.10.0 mysql2: specifier: ^2.3.3 version: 2.3.3 + node-pre-gyp: + specifier: ^0.17.0 + version: 0.17.0 node-schedule: - specifier: ^2.1.0 - version: 2.1.0 + specifier: ^2.1.1 + version: 2.1.1 passport: - specifier: ^0.5.2 - version: 0.5.2 + specifier: ^0.5.3 + version: 0.5.3 passport-42: specifier: ^1.2.6 version: 1.2.6 passport-jwt: - specifier: ^4.0.0 - version: 4.0.0 + specifier: ^4.0.1 + version: 4.0.1 passport-strategy: specifier: ^1.0.0 version: 1.0.0 @@ -112,99 +121,99 @@ importers: specifier: ^0.1.13 version: 0.1.13 swagger-jsdoc: - specifier: ^6.1.0 - version: 6.1.0(openapi-types@12.1.3) + specifier: ^6.2.8 + version: 6.2.8(openapi-types@12.1.3) swagger-ui-express: - specifier: ^4.3.0 - version: 4.3.0(express@4.17.2) + specifier: ^4.6.3 + version: 4.6.3(express@4.18.2) ts-pattern: - specifier: ^5.0.1 - version: 5.0.1 + specifier: ^5.0.5 + version: 5.0.5 typeorm: - specifier: ^0.3.11 - version: 0.3.11(mysql2@2.3.3)(pg@8.11.1) + specifier: ^0.3.17 + version: 0.3.17(mysql2@2.3.3)(pg@8.11.1) vite: - specifier: ^4.4.7 - version: 4.4.7(@types/node@18.16.1) + specifier: ^4.5.0 + version: 4.5.0(@types/node@18.16.1) vite-node: - specifier: ^0.34.1 - version: 0.34.1(@types/node@18.16.1) + specifier: ^0.34.6 + version: 0.34.6(@types/node@18.16.1) vite-tsconfig-paths: - specifier: ^4.2.0 - version: 4.2.0(typescript@5.1.6)(vite@4.4.7) + specifier: ^4.2.1 + version: 4.2.1(typescript@5.1.6)(vite@4.5.0) winston: - specifier: ^3.6.0 - version: 3.6.0 + specifier: ^3.11.0 + version: 3.11.0 winston-daily-rotate-file: - specifier: ^4.6.1 - version: 4.6.1(winston@3.6.0) + specifier: ^4.7.1 + version: 4.7.1(winston@3.11.0) devDependencies: '@types/bcrypt': - specifier: ^5.0.0 - version: 5.0.0 + specifier: ^5.0.1 + version: 5.0.1 '@types/cookie-parser': - specifier: ^1.4.3 - version: 1.4.3 + specifier: ^1.4.5 + version: 1.4.5 '@types/cors': - specifier: ^2.8.13 - version: 2.8.13 + specifier: ^2.8.15 + version: 2.8.15 '@types/express': - specifier: ^4.17.17 - version: 4.17.17 + specifier: ^4.17.20 + version: 4.17.20 '@types/http-errors': - specifier: ^2.0.1 - version: 2.0.1 + specifier: ^2.0.3 + version: 2.0.3 '@types/jest': - specifier: ^29.5.2 - version: 29.5.2 + specifier: ^29.5.6 + version: 29.5.6 '@types/jsonwebtoken': - specifier: ^9.0.2 - version: 9.0.2 + specifier: ^9.0.4 + version: 9.0.4 '@types/morgan': - specifier: ^1.9.4 - version: 1.9.4 + specifier: ^1.9.7 + version: 1.9.7 '@types/node-schedule': - specifier: ^2.1.0 - version: 2.1.0 + specifier: ^2.1.2 + version: 2.1.2 '@types/passport': - specifier: ^1.0.12 - version: 1.0.12 + specifier: ^1.0.14 + version: 1.0.14 '@types/passport-jwt': - specifier: ^3.0.8 - version: 3.0.8 + specifier: ^3.0.12 + version: 3.0.12 '@types/swagger-jsdoc': - specifier: ^6.0.1 - version: 6.0.1 + specifier: ^6.0.2 + version: 6.0.2 '@types/swagger-ui-express': - specifier: ^4.1.3 - version: 4.1.3 + specifier: ^4.1.5 + version: 4.1.5 '@typescript-eslint/eslint-plugin': - specifier: ^6.1.0 - version: 6.1.0(@typescript-eslint/parser@6.1.0)(eslint@8.45.0)(typescript@5.1.6) + specifier: ^6.9.0 + version: 6.9.0(@typescript-eslint/parser@6.9.0)(eslint@8.52.0)(typescript@5.1.6) '@typescript-eslint/parser': - specifier: ^6.1.0 - version: 6.1.0(eslint@8.45.0)(typescript@5.1.6) + specifier: ^6.9.0 + version: 6.9.0(eslint@8.52.0)(typescript@5.1.6) eslint: - specifier: ^8.45.0 - version: 8.45.0 + specifier: ^8.52.0 + version: 8.52.0 eslint-config-airbnb-base: specifier: ^15.0.0 - version: 15.0.0(eslint-plugin-import@2.27.5)(eslint@8.45.0) + version: 15.0.0(eslint-plugin-import@2.29.0)(eslint@8.52.0) eslint-import-resolver-typescript: - specifier: ^3.5.5 - version: 3.5.5(@typescript-eslint/parser@6.1.0)(eslint-plugin-import@2.27.5)(eslint@8.45.0) + specifier: ^3.6.1 + version: 3.6.1(@typescript-eslint/parser@6.9.0)(eslint-plugin-import@2.29.0)(eslint@8.52.0) eslint-plugin-import: - specifier: ^2.27.5 - version: 2.27.5(@typescript-eslint/parser@6.1.0)(eslint-import-resolver-typescript@3.5.5)(eslint@8.45.0) + specifier: ^2.29.0 + version: 2.29.0(@typescript-eslint/parser@6.9.0)(eslint-import-resolver-typescript@3.6.1)(eslint@8.52.0) jest: - specifier: ^29.5.0 - version: 29.5.0(@types/node@18.16.1) + specifier: ^29.7.0 + version: 29.7.0(@types/node@18.16.1) jest-mock-extended: - specifier: ^3.0.4 - version: 3.0.4(jest@29.5.0)(typescript@5.1.6) + specifier: ^3.0.5 + version: 3.0.5(jest@29.7.0)(typescript@5.1.6) kysely-codegen: specifier: ^0.10.1 - version: 0.10.1(kysely@0.26.1)(mysql2@2.3.3)(pg@8.11.1) + version: 0.10.1(kysely@0.26.3)(mysql2@2.3.3)(pg@8.11.1) nodemon: specifier: ^3.0.1 version: 3.0.1 @@ -212,8 +221,8 @@ importers: specifier: ^2.8.8 version: 2.8.8 ts-jest: - specifier: ^29.1.0 - version: 29.1.0(@babel/core@7.22.8)(jest@29.5.0)(typescript@5.1.6) + specifier: ^29.1.1 + version: 29.1.1(@babel/core@7.22.8)(jest@29.7.0)(typescript@5.1.6) typeorm-model-generator: specifier: ^0.4.6 version: 0.4.6 @@ -222,10 +231,16 @@ importers: dependencies: '@anatine/zod-openapi': specifier: ^2.2.0 - version: 2.2.0(openapi3-ts@4.1.2)(zod@3.22.2) + version: 2.2.0(openapi3-ts@4.1.2)(zod@3.22.4) + '@ts-rest/core': + specifier: ^3.30.4 + version: 3.30.4(zod@3.22.4) openapi3-ts: specifier: ^4.1.2 version: 4.1.2 + zod: + specifier: ^3.22.4 + version: 3.22.4 devDependencies: '@apidevtools/swagger-cli': specifier: ^4.0.4 @@ -258,7 +273,7 @@ packages: '@jridgewell/trace-mapping': 0.3.18 dev: true - /@anatine/zod-openapi@1.14.2(openapi3-ts@2.0.2)(zod@3.22.2): + /@anatine/zod-openapi@1.14.2(openapi3-ts@2.0.2)(zod@3.22.4): resolution: {integrity: sha512-q0qHfnuNYVKu0Swrnnvfj9971AEyW7c8v9jCOZGCl5ZbyGMNG4RPyJkRcMi/JC8CRfdOe0IDfNm1nNsi2avprg==} peerDependencies: openapi3-ts: ^2.0.0 || ^3.0.0 @@ -266,10 +281,10 @@ packages: dependencies: openapi3-ts: 2.0.2 ts-deepmerge: 6.2.0 - zod: 3.22.2 + zod: 3.22.4 dev: false - /@anatine/zod-openapi@2.2.0(openapi3-ts@4.1.2)(zod@3.22.2): + /@anatine/zod-openapi@2.2.0(openapi3-ts@4.1.2)(zod@3.22.4): resolution: {integrity: sha512-Ponwenf2NMIVbs9IuB3YoW7CQPkuRWxhYV/qxBu48DQhmRyipEw/YROVTWnvku7+lwhv1EyP4+h3J5SwrvmfHg==} peerDependencies: openapi3-ts: ^4.1.2 @@ -277,7 +292,7 @@ packages: dependencies: openapi3-ts: 4.1.2 ts-deepmerge: 6.2.0 - zod: 3.22.2 + zod: 3.22.4 dev: false /@apidevtools/json-schema-ref-parser@9.1.2: @@ -320,6 +335,21 @@ packages: call-me-maybe: 1.0.2 openapi-types: 12.1.3 z-schema: 4.2.4 + dev: true + + /@apidevtools/swagger-parser@10.0.3(openapi-types@12.1.3): + resolution: {integrity: sha512-sNiLY51vZOmSPFZA5TF35KZ2HbgYklQnTSDnkghamzLb3EkNtcQnrBQEj5AOCxHpTtXpqMCRM1CrmV2rG6nw4g==} + peerDependencies: + openapi-types: '>=7' + dependencies: + '@apidevtools/json-schema-ref-parser': 9.1.2 + '@apidevtools/openapi-schemas': 2.1.0 + '@apidevtools/swagger-methods': 3.0.2 + '@jsdevtools/ono': 7.1.3 + call-me-maybe: 1.0.2 + openapi-types: 12.1.3 + z-schema: 5.0.5 + dev: false /@azure/abort-controller@1.1.0: resolution: {integrity: sha512-TrRLIoSQVzfAJX9H1JeFjzAoDGcoK1IYX1UImfceTZpsyYfWr09Ss1aHW1y5TrrR3iq6RZLBwJ3E24uwPhwahw==} @@ -525,6 +555,7 @@ packages: /@babel/parser@7.22.7: resolution: {integrity: sha512-7NF8pOkHP5o2vpmGgNGcfAeCvOYhGLyA3Z4eBQkT1RJlWu47n63bCs93QfJ2hIAFCil7L5P2IWhs1oToVgrL0Q==} engines: {node: '>=6.0.0'} + hasBin: true dependencies: '@babel/types': 7.22.5 dev: true @@ -658,6 +689,13 @@ packages: '@babel/helper-plugin-utils': 7.22.5 dev: true + /@babel/runtime@7.23.2: + resolution: {integrity: sha512-mM8eg4yl5D6i3lu2QKPuPH4FArvJ8KhTofbE7jwMUv9KX5mBvwPAqnV3MlyBNqdp9RyRKP6Yck8TrfYrPvX3bg==} + engines: {node: '>=6.9.0'} + dependencies: + regenerator-runtime: 0.14.0 + dev: false + /@babel/template@7.22.5: resolution: {integrity: sha512-X7yV7eiwAxdj9k94NEylvbVHLiVG1nvzCV2EAowhxLTwODV1jl9UzZ48leOC0sH7OnuHrIkllaBgneUykIcZaw==} engines: {node: '>=6.9.0'} @@ -703,6 +741,11 @@ packages: engines: {node: '>=0.1.90'} dev: false + /@colors/colors@1.6.0: + resolution: {integrity: sha512-Ir+AOibqzrIsL6ajt3Rz3LskB7OiMVHqltZmspbW/TJuTVuyOMirVqAkjfY6JISiLHgyNqicAC8AyHHGzNd/dA==} + engines: {node: '>=0.1.90'} + dev: false + /@dabh/diagnostics@2.0.3: resolution: {integrity: sha512-hrlQOIi7hAfzsMqlGSFyVucrx38O+j6wiGOf//H2ecvIEqYN4ADBSS2iLMh5UFyDunCNniUIPk/q3riFv45xRA==} dependencies: @@ -916,7 +959,22 @@ packages: eslint: ^6.0.0 || ^7.0.0 || >=8.0.0 dependencies: eslint: 8.45.0 - eslint-visitor-keys: 3.4.1 + eslint-visitor-keys: 3.4.3 + dev: true + + /@eslint-community/eslint-utils@4.4.0(eslint@8.52.0): + resolution: {integrity: sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + peerDependencies: + eslint: ^6.0.0 || ^7.0.0 || >=8.0.0 + dependencies: + eslint: 8.52.0 + eslint-visitor-keys: 3.4.3 + dev: true + + /@eslint-community/regexpp@4.10.0: + resolution: {integrity: sha512-Cu96Sd2By9mCNTx2iyKOmq10v22jUVQv0lQnlGNy16oE9589yE+QADPbrMGCkA51cKZSg3Pu/aTJVTGfL/qjUA==} + engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0} dev: true /@eslint-community/regexpp@4.5.1: @@ -941,11 +999,33 @@ packages: - supports-color dev: true + /@eslint/eslintrc@2.1.3: + resolution: {integrity: sha512-yZzuIG+jnVu6hNSzFEN07e8BxF3uAzYtQb6uDkaYZLo6oYZDCq454c5kB8zxnzfCYyP4MIuyBn10L0DqwujTmA==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + dependencies: + ajv: 6.12.6 + debug: 4.3.4 + espree: 9.6.1 + globals: 13.20.0 + ignore: 5.2.4 + import-fresh: 3.3.0 + js-yaml: 4.1.0 + minimatch: 3.1.2 + strip-json-comments: 3.1.1 + transitivePeerDependencies: + - supports-color + dev: true + /@eslint/js@8.44.0: resolution: {integrity: sha512-Ag+9YM4ocKQx9AarydN0KY2j0ErMHNIocPDrVo8zAE44xLTjEtz81OdR68/cydGtk6m6jDb5Za3r2useMzYmSw==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} dev: true + /@eslint/js@8.52.0: + resolution: {integrity: sha512-mjZVbpaeMZludF2fsWLD0Z9gCref1Tk4i9+wddjRvpUNqqcndPkBD09N/Mapey0b3jaXbLm2kICwFv2E64QinA==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + dev: true + /@gar/promisify@1.1.3: resolution: {integrity: sha512-k2Ty1JcVojjJFwrg/ThKi2ujJ7XNLYaFGNB/bWT9wGR+oSMJHMa5w+CUq6p/pVrKeNNgA7pCqEcjSnHVoqJQFw==} requiresBuild: true @@ -963,6 +1043,17 @@ packages: - supports-color dev: true + /@humanwhocodes/config-array@0.11.13: + resolution: {integrity: sha512-JSBDMiDKSzQVngfRjOdFXgFfklaXI4K9nLF49Auh21lmBWRLIK3+xTErTWD4KU54pb6coM6ESE7Awz/FNU3zgQ==} + engines: {node: '>=10.10.0'} + dependencies: + '@humanwhocodes/object-schema': 2.0.1 + debug: 4.3.4 + minimatch: 3.1.2 + transitivePeerDependencies: + - supports-color + dev: true + /@humanwhocodes/module-importer@1.0.1: resolution: {integrity: sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==} engines: {node: '>=12.22'} @@ -972,6 +1063,10 @@ packages: resolution: {integrity: sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA==} dev: true + /@humanwhocodes/object-schema@2.0.1: + resolution: {integrity: sha512-dvuCeX5fC9dXgJn9t+X5atfmgQAzUOWqS1254Gh0m6i8wKd10ebXkfNKiRK+1GWi/yTvvLDHpoxLr0xxxeslWw==} + dev: true + /@istanbuljs/load-nyc-config@1.1.0: resolution: {integrity: sha512-VjeHSlIzpv/NyD3N0YuHfXOPDIixcA1q2ZV98wsMqcYlPmv2n3Yb2lYP9XMElnaFVXg5A7YLTeLu6V84uQDjmQ==} engines: {node: '>=8'} @@ -988,20 +1083,20 @@ packages: engines: {node: '>=8'} dev: true - /@jest/console@29.6.1: - resolution: {integrity: sha512-Aj772AYgwTSr5w8qnyoJ0eDYvN6bMsH3ORH1ivMotrInHLKdUz6BDlaEXHdM6kODaBIkNIyQGzsMvRdOv7VG7Q==} + /@jest/console@29.7.0: + resolution: {integrity: sha512-5Ni4CU7XHQi32IJ398EEP4RrB8eV09sXP2ROqD4bksHrnTree52PsxvX8tpL8LvTZ3pFzXyPbNQReSN41CAhOg==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} dependencies: - '@jest/types': 29.6.1 + '@jest/types': 29.6.3 '@types/node': 18.16.1 chalk: 4.1.2 - jest-message-util: 29.6.1 - jest-util: 29.6.1 + jest-message-util: 29.7.0 + jest-util: 29.7.0 slash: 3.0.0 dev: true - /@jest/core@29.6.1: - resolution: {integrity: sha512-CcowHypRSm5oYQ1obz1wfvkjZZ2qoQlrKKvlfPwh5jUXVU12TWr2qMeH8chLMuTFzHh5a1g2yaqlqDICbr+ukQ==} + /@jest/core@29.7.0: + resolution: {integrity: sha512-n7aeXWKMnGtDA48y8TLWJPJmLmmZ642Ceo78cYWEpiD7FzDgmNDV/GCVRorPABdXLJZ/9wzzgZAlHjXjxDHGsg==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} peerDependencies: node-notifier: ^8.0.1 || ^9.0.0 || ^10.0.0 @@ -1009,47 +1104,48 @@ packages: node-notifier: optional: true dependencies: - '@jest/console': 29.6.1 - '@jest/reporters': 29.6.1 - '@jest/test-result': 29.6.1 - '@jest/transform': 29.6.1 - '@jest/types': 29.6.1 + '@jest/console': 29.7.0 + '@jest/reporters': 29.7.0 + '@jest/test-result': 29.7.0 + '@jest/transform': 29.7.0 + '@jest/types': 29.6.3 '@types/node': 18.16.1 ansi-escapes: 4.3.2 chalk: 4.1.2 ci-info: 3.8.0 exit: 0.1.2 graceful-fs: 4.2.11 - jest-changed-files: 29.5.0 - jest-config: 29.6.1(@types/node@18.16.1) - jest-haste-map: 29.6.1 - jest-message-util: 29.6.1 - jest-regex-util: 29.4.3 - jest-resolve: 29.6.1 - jest-resolve-dependencies: 29.6.1 - jest-runner: 29.6.1 - jest-runtime: 29.6.1 - jest-snapshot: 29.6.1 - jest-util: 29.6.1 - jest-validate: 29.6.1 - jest-watcher: 29.6.1 + jest-changed-files: 29.7.0 + jest-config: 29.7.0(@types/node@18.16.1) + jest-haste-map: 29.7.0 + jest-message-util: 29.7.0 + jest-regex-util: 29.6.3 + jest-resolve: 29.7.0 + jest-resolve-dependencies: 29.7.0 + jest-runner: 29.7.0 + jest-runtime: 29.7.0 + jest-snapshot: 29.7.0 + jest-util: 29.7.0 + jest-validate: 29.7.0 + jest-watcher: 29.7.0 micromatch: 4.0.5 - pretty-format: 29.6.1 + pretty-format: 29.7.0 slash: 3.0.0 strip-ansi: 6.0.1 transitivePeerDependencies: + - babel-plugin-macros - supports-color - ts-node dev: true - /@jest/environment@29.6.1: - resolution: {integrity: sha512-RMMXx4ws+Gbvw3DfLSuo2cfQlK7IwGbpuEWXCqyYDcqYTI+9Ju3a5hDnXaxjNsa6uKh9PQF2v+qg+RLe63tz5A==} + /@jest/environment@29.7.0: + resolution: {integrity: sha512-aQIfHDq33ExsN4jP1NWGXhxgQ/wixs60gDiKO+XVMd8Mn0NWPWgc34ZQDTb2jKaUWQ7MuwoitXAsN2XVXNMpAw==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} dependencies: - '@jest/fake-timers': 29.6.1 - '@jest/types': 29.6.1 + '@jest/fake-timers': 29.7.0 + '@jest/types': 29.6.3 '@types/node': 18.16.1 - jest-mock: 29.6.1 + jest-mock: 29.7.0 dev: true /@jest/expect-utils@29.6.1: @@ -1059,42 +1155,49 @@ packages: jest-get-type: 29.4.3 dev: true - /@jest/expect@29.6.1: - resolution: {integrity: sha512-N5xlPrAYaRNyFgVf2s9Uyyvr795jnB6rObuPx4QFvNJz8aAjpZUDfO4bh5G/xuplMID8PrnuF1+SfSyDxhsgYg==} + /@jest/expect-utils@29.7.0: + resolution: {integrity: sha512-GlsNBWiFQFCVi9QVSx7f5AgMeLxe9YCCs5PuP2O2LdjDAA8Jh9eX7lA1Jq/xdXw3Wb3hyvlFNfZIfcRetSzYcA==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} dependencies: - expect: 29.6.1 - jest-snapshot: 29.6.1 + jest-get-type: 29.6.3 + dev: true + + /@jest/expect@29.7.0: + resolution: {integrity: sha512-8uMeAMycttpva3P1lBHB8VciS9V0XAr3GymPpipdyQXbBcuhkLQOSe8E/p92RyAdToS6ZD1tFkX+CkhoECE0dQ==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + expect: 29.7.0 + jest-snapshot: 29.7.0 transitivePeerDependencies: - supports-color dev: true - /@jest/fake-timers@29.6.1: - resolution: {integrity: sha512-RdgHgbXyosCDMVYmj7lLpUwXA4c69vcNzhrt69dJJdf8azUrpRh3ckFCaTPNjsEeRi27Cig0oKDGxy5j7hOgHg==} + /@jest/fake-timers@29.7.0: + resolution: {integrity: sha512-q4DH1Ha4TTFPdxLsqDXK1d3+ioSL7yL5oCMJZgDYm6i+6CygW5E5xVr/D1HdsGxjt1ZWSfUAs9OxSB/BNelWrQ==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} dependencies: - '@jest/types': 29.6.1 + '@jest/types': 29.6.3 '@sinonjs/fake-timers': 10.3.0 '@types/node': 18.16.1 - jest-message-util: 29.6.1 - jest-mock: 29.6.1 - jest-util: 29.6.1 + jest-message-util: 29.7.0 + jest-mock: 29.7.0 + jest-util: 29.7.0 dev: true - /@jest/globals@29.6.1: - resolution: {integrity: sha512-2VjpaGy78JY9n9370H8zGRCFbYVWwjY6RdDMhoJHa1sYfwe6XM/azGN0SjY8kk7BOZApIejQ1BFPyH7FPG0w3A==} + /@jest/globals@29.7.0: + resolution: {integrity: sha512-mpiz3dutLbkW2MNFubUGUEVLkTGiqW6yLVTA+JbP6fI6J5iL9Y0Nlg8k95pcF8ctKwCS7WVxteBs29hhfAotzQ==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} dependencies: - '@jest/environment': 29.6.1 - '@jest/expect': 29.6.1 - '@jest/types': 29.6.1 - jest-mock: 29.6.1 + '@jest/environment': 29.7.0 + '@jest/expect': 29.7.0 + '@jest/types': 29.6.3 + jest-mock: 29.7.0 transitivePeerDependencies: - supports-color dev: true - /@jest/reporters@29.6.1: - resolution: {integrity: sha512-9zuaI9QKr9JnoZtFQlw4GREQbxgmNYXU6QuWtmuODvk5nvPUeBYapVR/VYMyi2WSx3jXTLJTJji8rN6+Cm4+FA==} + /@jest/reporters@29.7.0: + resolution: {integrity: sha512-DApq0KJbJOEzAFYjHADNNxAE3KbhxQB1y5Kplb5Waqw6zVbuWatSnMjE5gs8FUgEPmNsnZA3NCWl9NG0ia04Pg==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} peerDependencies: node-notifier: ^8.0.1 || ^9.0.0 || ^10.0.0 @@ -1103,10 +1206,10 @@ packages: optional: true dependencies: '@bcoe/v8-coverage': 0.2.3 - '@jest/console': 29.6.1 - '@jest/test-result': 29.6.1 - '@jest/transform': 29.6.1 - '@jest/types': 29.6.1 + '@jest/console': 29.7.0 + '@jest/test-result': 29.7.0 + '@jest/transform': 29.7.0 + '@jest/types': 29.6.3 '@jridgewell/trace-mapping': 0.3.18 '@types/node': 18.16.1 chalk: 4.1.2 @@ -1115,13 +1218,13 @@ packages: glob: 7.2.3 graceful-fs: 4.2.11 istanbul-lib-coverage: 3.2.0 - istanbul-lib-instrument: 5.2.1 + istanbul-lib-instrument: 6.0.1 istanbul-lib-report: 3.0.0 istanbul-lib-source-maps: 4.0.1 istanbul-reports: 3.1.5 - jest-message-util: 29.6.1 - jest-util: 29.6.1 - jest-worker: 29.6.1 + jest-message-util: 29.7.0 + jest-util: 29.7.0 + jest-worker: 29.7.0 slash: 3.0.0 string-length: 4.0.2 strip-ansi: 6.0.1 @@ -1137,8 +1240,15 @@ packages: '@sinclair/typebox': 0.27.8 dev: true - /@jest/source-map@29.6.0: - resolution: {integrity: sha512-oA+I2SHHQGxDCZpbrsCQSoMLb3Bz547JnM+jUr9qEbuw0vQlWZfpPS7CO9J7XiwKicEz9OFn/IYoLkkiUD7bzA==} + /@jest/schemas@29.6.3: + resolution: {integrity: sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + '@sinclair/typebox': 0.27.8 + dev: true + + /@jest/source-map@29.6.3: + resolution: {integrity: sha512-MHjT95QuipcPrpLM+8JMSzFx6eHp5Bm+4XeFDJlwsvVBjmKNiIAvasGK2fxz2WbGRlnvqehFbh07MMa7n3YJnw==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} dependencies: '@jridgewell/trace-mapping': 0.3.18 @@ -1146,41 +1256,41 @@ packages: graceful-fs: 4.2.11 dev: true - /@jest/test-result@29.6.1: - resolution: {integrity: sha512-Ynr13ZRcpX6INak0TPUukU8GWRfm/vAytE3JbJNGAvINySWYdfE7dGZMbk36oVuK4CigpbhMn8eg1dixZ7ZJOw==} + /@jest/test-result@29.7.0: + resolution: {integrity: sha512-Fdx+tv6x1zlkJPcWXmMDAG2HBnaR9XPSd5aDWQVsfrZmLVT3lU1cwyxLgRmXR9yrq4NBoEm9BMsfgFzTQAbJYA==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} dependencies: - '@jest/console': 29.6.1 - '@jest/types': 29.6.1 + '@jest/console': 29.7.0 + '@jest/types': 29.6.3 '@types/istanbul-lib-coverage': 2.0.4 collect-v8-coverage: 1.0.2 dev: true - /@jest/test-sequencer@29.6.1: - resolution: {integrity: sha512-oBkC36PCDf/wb6dWeQIhaviU0l5u6VCsXa119yqdUosYAt7/FbQU2M2UoziO3igj/HBDEgp57ONQ3fm0v9uyyg==} + /@jest/test-sequencer@29.7.0: + resolution: {integrity: sha512-GQwJ5WZVrKnOJuiYiAF52UNUJXgTZx1NHjFSEB0qEMmSZKAkdMoIzw/Cj6x6NF4AvV23AUqDpFzQkN/eYCYTxw==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} dependencies: - '@jest/test-result': 29.6.1 + '@jest/test-result': 29.7.0 graceful-fs: 4.2.11 - jest-haste-map: 29.6.1 + jest-haste-map: 29.7.0 slash: 3.0.0 dev: true - /@jest/transform@29.6.1: - resolution: {integrity: sha512-URnTneIU3ZjRSaf906cvf6Hpox3hIeJXRnz3VDSw5/X93gR8ycdfSIEy19FlVx8NFmpN7fe3Gb1xF+NjXaQLWg==} + /@jest/transform@29.7.0: + resolution: {integrity: sha512-ok/BTPFzFKVMwO5eOHRrvnBVHdRy9IrsrW1GpMaQ9MCnilNLXQKmAX8s1YXDFaai9xJpac2ySzV0YeRRECr2Vw==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} dependencies: '@babel/core': 7.22.8 - '@jest/types': 29.6.1 + '@jest/types': 29.6.3 '@jridgewell/trace-mapping': 0.3.18 babel-plugin-istanbul: 6.1.1 chalk: 4.1.2 convert-source-map: 2.0.0 fast-json-stable-stringify: 2.1.0 graceful-fs: 4.2.11 - jest-haste-map: 29.6.1 - jest-regex-util: 29.4.3 - jest-util: 29.6.1 + jest-haste-map: 29.7.0 + jest-regex-util: 29.6.3 + jest-util: 29.7.0 micromatch: 4.0.5 pirates: 4.0.6 slash: 3.0.0 @@ -1189,11 +1299,11 @@ packages: - supports-color dev: true - /@jest/types@29.6.1: - resolution: {integrity: sha512-tPKQNMPuXgvdOn2/Lg9HNfUvjYVGolt04Hp03f5hAk878uwOLikN+JzeLY0HcVgKgFl9Hs3EIqpu3WX27XNhnw==} + /@jest/types@29.6.3: + resolution: {integrity: sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} dependencies: - '@jest/schemas': 29.6.0 + '@jest/schemas': 29.6.3 '@types/istanbul-lib-coverage': 2.0.4 '@types/istanbul-reports': 3.0.1 '@types/node': 18.16.1 @@ -1238,8 +1348,9 @@ packages: /@jsdevtools/ono@7.1.3: resolution: {integrity: sha512-4JQNk+3mVzK3xh2rqd6RB4J46qUR19azEHBneZyTZM+c456qOrbbM/5xcR8huNCCcbVt7+UmizG6GuUvPvKUYg==} - /@mapbox/node-pre-gyp@1.0.10: - resolution: {integrity: sha512-4ySo4CjzStuprMwk35H5pPbkymjv1SF3jGLj6rAHp/xT/RF7TL7bd9CTm1xDY49K2qF7jmR/g7k+SkLETP6opA==} + /@mapbox/node-pre-gyp@1.0.11: + resolution: {integrity: sha512-Yhlar6v9WQgUp/He7BdgzOz8lqMQ8sU+jkCq7Wx8Myc5YFJLbEe7lgui/V7G1qB1DJykHSGwreceSaD60Y0PUQ==} + hasBin: true dependencies: detect-libc: 2.0.1 https-proxy-agent: 5.0.1 @@ -1256,6 +1367,7 @@ packages: /@nicolo-ribaudo/semver-v6@6.3.3: resolution: {integrity: sha512-3Yc1fUTs69MG/uZbJlLSI3JISMn2UV2rg+1D/vROUqZyh3l6iYHCs7GMp+M40ZD7yOdDbYjJcU1oTJhrc+dGKg==} + hasBin: true dev: true /@nodelib/fs.scandir@2.1.5: @@ -1299,18 +1411,6 @@ packages: dev: true optional: true - /@pkgr/utils@2.4.2: - resolution: {integrity: sha512-POgTXhjrTfbTV63DiFXav4lBHiICLKKwDeaKn9Nphwj7WH6m0hMMCaJkMyRWjgtPFyRKRVoMXXjczsTQRDEhYw==} - engines: {node: ^12.20.0 || ^14.18.0 || >=16.0.0} - dependencies: - cross-spawn: 7.0.3 - fast-glob: 3.3.0 - is-glob: 4.0.3 - open: 9.1.0 - picocolors: 1.0.0 - tslib: 2.6.0 - dev: true - /@rometools/cli-darwin-arm64@12.1.3: resolution: {integrity: sha512-AmFTUDYjBuEGQp/Wwps+2cqUr+qhR7gyXAUnkL5psCuNCz3807TrUq/ecOoct5MIavGJTH6R4aaSL6+f+VlBEg==} cpu: [arm64] @@ -1427,7 +1527,29 @@ packages: zod: 3.22.2 dev: false - /@ts-rest/express@3.28.0(@ts-rest/core@3.28.0)(express@4.17.2)(zod@3.22.2): + /@ts-rest/core@3.28.0(zod@3.22.4): + resolution: {integrity: sha512-m0Wn2DgO3O57dsGT5fqxvF/WNFPx/8/dxbqLMV9TYXwLBgaJG4vDgR7qXVIYss/c5vlHTQ0oB9X+PwxhI3ksMg==} + peerDependencies: + zod: ^3.21.0 + peerDependenciesMeta: + zod: + optional: true + dependencies: + zod: 3.22.4 + dev: false + + /@ts-rest/core@3.30.4(zod@3.22.4): + resolution: {integrity: sha512-wHI3OHnqpak8jmz2drjzMtgvFVLBrTMvxkkWIUIsGDkEpFwuF/9592RpawkCLIvlxbrxTzp0MzqqwwbzlG6BkA==} + peerDependencies: + zod: ^3.22.3 + peerDependenciesMeta: + zod: + optional: true + dependencies: + zod: 3.22.4 + dev: false + + /@ts-rest/express@3.28.0(@ts-rest/core@3.28.0)(express@4.18.2)(zod@3.22.4): resolution: {integrity: sha512-kkFdgBCtSDGIRcnh+uJGK5BMiYo/bHMH0/zVhAA4T3Wb6PPixf80GtsT9KiOOciVrG4JtLWQbsdTh69KxjPe5g==} peerDependencies: '@ts-rest/core': 3.28.0 @@ -1437,21 +1559,21 @@ packages: zod: optional: true dependencies: - '@ts-rest/core': 3.28.0(zod@3.22.2) - express: 4.17.2 - zod: 3.22.2 + '@ts-rest/core': 3.28.0(zod@3.22.4) + express: 4.18.2 + zod: 3.22.4 dev: false - /@ts-rest/open-api@3.28.0(@ts-rest/core@3.28.0)(zod@3.22.2): + /@ts-rest/open-api@3.28.0(@ts-rest/core@3.28.0)(zod@3.22.4): resolution: {integrity: sha512-9EG1mWxzcTriWAQGLNLXvpRT5VLKIQGPcBfTndRDsHUQe9jor+GtjEP8ttuoTfo3GlhkND9V6Qrikda1/kM0tg==} peerDependencies: '@ts-rest/core': 3.28.0 zod: ^3.21.0 dependencies: - '@anatine/zod-openapi': 1.14.2(openapi3-ts@2.0.2)(zod@3.22.2) - '@ts-rest/core': 3.28.0(zod@3.22.2) + '@anatine/zod-openapi': 1.14.2(openapi3-ts@2.0.2)(zod@3.22.4) + '@ts-rest/core': 3.28.0(zod@3.22.4) openapi3-ts: 2.0.2 - zod: 3.22.2 + zod: 3.22.4 dev: false /@types/babel__core@7.20.1: @@ -1483,8 +1605,8 @@ packages: '@babel/types': 7.22.5 dev: true - /@types/bcrypt@5.0.0: - resolution: {integrity: sha512-agtcFKaruL8TmcvqbndlqHPSJgsolhf/qPWchFlgnW1gECTN/nKbFcoFnvKAQRFfKbh+BO6A3SWdJu9t+xF3Lw==} + /@types/bcrypt@5.0.1: + resolution: {integrity: sha512-dIIrEsLV1/v0AUNI8oHMaRRTSeVjoy5ID8oclJavtPj8CwPJoD1eFoNXEypuu6k091brEzBeOo3LlxeAH9zRZg==} dependencies: '@types/node': 18.16.1 dev: true @@ -1502,14 +1624,14 @@ packages: '@types/node': 18.16.1 dev: true - /@types/cookie-parser@1.4.3: - resolution: {integrity: sha512-CqSKwFwefj4PzZ5n/iwad/bow2hTCh0FlNAeWLtQM3JA/NX/iYagIpWG2cf1bQKQ2c9gU2log5VUCrn7LDOs0w==} + /@types/cookie-parser@1.4.5: + resolution: {integrity: sha512-cbpH1NldYslPt7WRHXZFm+G7DTfUg57dQSCf1qrHwT8wtGX41JHLYf3Cieiqg7waPWjorVgcSSllZov+A1PJbg==} dependencies: - '@types/express': 4.17.17 + '@types/express': 4.17.20 dev: true - /@types/cors@2.8.13: - resolution: {integrity: sha512-RG8AStHlUiV5ysZQKq97copd2UmVYw3/pRMLefISZ3S1hK104Cwm7iLQ3fTKx+lsUH2CE8FlLaYeEA2LSeqYUA==} + /@types/cors@2.8.15: + resolution: {integrity: sha512-n91JxbNLD8eQIuXDIChAN1tCKNWCEgpceU9b7ZMbFA+P+Q4yIeh80jizFLEvolRPc1ES0VdwFlGv+kJTSirogw==} dependencies: '@types/node': 18.16.1 dev: true @@ -1523,8 +1645,8 @@ packages: '@types/send': 0.17.1 dev: true - /@types/express@4.17.17: - resolution: {integrity: sha512-Q4FmmuLGBG58btUnfS1c1r/NQdlp3DMfGDGig8WhfpA2YRUtEkxAjkZb0yvplJGYdF1fsQ81iMDcH24sSCNC/Q==} + /@types/express@4.17.20: + resolution: {integrity: sha512-rOaqlkgEvOW495xErXMsmyX3WKBInbhG5eqojXYi3cGUaLoRDlXa5d52fkfWZT963AZ3v2eZ4MbKE6WpDAGVsw==} dependencies: '@types/body-parser': 1.19.2 '@types/express-serve-static-core': 4.17.35 @@ -1538,8 +1660,8 @@ packages: '@types/node': 18.16.1 dev: true - /@types/http-errors@2.0.1: - resolution: {integrity: sha512-/K3ds8TRAfBvi5vfjuz8y6+GiAYBZ0x4tXv1Av6CWBWn0IlADc+ZX9pMq7oU0fNQPnBwIZl3rmeLp6SBApbxSQ==} + /@types/http-errors@2.0.3: + resolution: {integrity: sha512-pP0P/9BnCj1OVvQR2lF41EkDG/lWWnDyA203b/4Fmi2eTyORnBtcDoKDwjWQthELrBvWkMOrvSOnZ8OVlW6tXA==} dev: true /@types/is-stream@1.1.0: @@ -1564,8 +1686,8 @@ packages: '@types/istanbul-lib-report': 3.0.0 dev: true - /@types/jest@29.5.2: - resolution: {integrity: sha512-mSoZVJF5YzGVCk+FsDxzDuH7s+SCkzrgKZzf0Z0T2WudhBUPoF6ktoTPC4R0ZoCPCV5xUvuU6ias5NvxcBcMMg==} + /@types/jest@29.5.6: + resolution: {integrity: sha512-/t9NnzkOpXb4Nfvg17ieHE6EeSjDS2SGSpNYfoLbUAeL/EOueU/RSdOWFpfQTXBEM7BguYW1XQ0EbM+6RlIh6w==} dependencies: expect: 29.6.1 pretty-format: 29.6.1 @@ -1578,8 +1700,8 @@ packages: resolution: {integrity: sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==} dev: true - /@types/jsonwebtoken@9.0.2: - resolution: {integrity: sha512-drE6uz7QBKq1fYqqoFKTDRdFCPHd5TCub75BM+D+cMx7NU9hUz7SESLfC2fSCXVFMO5Yj8sOWHuGqPgjc+fz0Q==} + /@types/jsonwebtoken@9.0.4: + resolution: {integrity: sha512-8UYapdmR0QlxgvJmyE8lP7guxD0UGVMfknsdtCFZh4ovShdBl3iOI4zdvqBHrB/IS+xUj3PSx73Qkey1fhWz+g==} dependencies: '@types/node': 18.16.1 dev: true @@ -1592,14 +1714,14 @@ packages: resolution: {integrity: sha512-Y4XFY5VJAuw0FgAqPNd6NNoV44jbq9Bz2L7Rh/J6jLTiHBSBJa9fxqQIvkIld4GsoDOcCbvzOUAbLPsSKKg+uA==} dev: true - /@types/morgan@1.9.4: - resolution: {integrity: sha512-cXoc4k+6+YAllH3ZHmx4hf7La1dzUk6keTR4bF4b4Sc0mZxU/zK4wO7l+ZzezXm/jkYj/qC+uYGZrarZdIVvyQ==} + /@types/morgan@1.9.7: + resolution: {integrity: sha512-4sJFBUBrIZkP5EvMm1L6VCXp3SQe8dnXqlVpe1jsmTjS1JQVmSjnpMNs8DosQd6omBi/K7BSKJ6z/Mc3ki0K9g==} dependencies: '@types/node': 18.16.1 dev: true - /@types/node-schedule@2.1.0: - resolution: {integrity: sha512-NiTwl8YN3v/1YCKrDFSmCTkVxFDylueEqsOFdgF+vPsm+AlyJKGAo5yzX1FiOxPsZiN6/r8gJitYx2EaSuBmmg==} + /@types/node-schedule@2.1.2: + resolution: {integrity: sha512-pNf6vCw14EYbqo0Y1eLGhkyv9RhgvphrxpPk4bd1CqwsWbHCrLSVYpO+9NmKOCUSYwxG6eRaWDR3Y6C+4gtzow==} dependencies: '@types/node': 18.16.1 dev: true @@ -1611,29 +1733,25 @@ packages: /@types/node@18.16.1: resolution: {integrity: sha512-DZxSZWXxFfOlx7k7Rv4LAyiMroaxa3Ly/7OOzZO8cBNho0YzAi4qlbrx8W27JGqG57IgR/6J7r+nOJWw6kcvZA==} - /@types/passport-jwt@3.0.8: - resolution: {integrity: sha512-VKJZDJUAHFhPHHYvxdqFcc5vlDht8Q2pL1/ePvKAgqRThDaCc84lSYOTQmnx3+JIkDlN+2KfhFhXIzlcVT+Pcw==} + /@types/passport-jwt@3.0.12: + resolution: {integrity: sha512-nXCd1lu20rw//nZ5AnK1FnlVZdSC4R5xksquev9oAJlXwJw0irMdZ7dRAE4KDlalptKObiaoam6BQ8lpujeZog==} dependencies: - '@types/express': 4.17.17 - '@types/jsonwebtoken': 9.0.2 + '@types/express': 4.17.20 + '@types/jsonwebtoken': 9.0.4 '@types/passport-strategy': 0.2.35 dev: true /@types/passport-strategy@0.2.35: resolution: {integrity: sha512-o5D19Jy2XPFoX2rKApykY15et3Apgax00RRLf0RUotPDUsYrQa7x4howLYr9El2mlUApHmCMv5CZ1IXqKFQ2+g==} dependencies: - '@types/express': 4.17.17 - '@types/passport': 1.0.12 + '@types/express': 4.17.20 + '@types/passport': 1.0.14 dev: true - /@types/passport@1.0.12: - resolution: {integrity: sha512-QFdJ2TiAEoXfEQSNDISJR1Tm51I78CymqcBa8imbjo6dNNu+l2huDxxbDEIoFIwOSKMkOfHEikyDuZ38WwWsmw==} + /@types/passport@1.0.14: + resolution: {integrity: sha512-D6p2ygR2S7Cq5PO7iUaEIQu/5WrM0tONu6Lxgk0C9r3lafQIlVpWCo3V/KI9To3OqHBxcfQaOeK+8AvwW5RYmw==} dependencies: - '@types/express': 4.17.17 - dev: true - - /@types/prettier@2.7.3: - resolution: {integrity: sha512-+68kP9yzs4LMp7VNh8gdzMSPZFL44MLGqiHWvttYJe+6qnuVr4Ek9wSBQoveqY/r+LwjCcU29kNVkidwim+kYA==} + '@types/express': 4.17.20 dev: true /@types/qs@6.9.7: @@ -1669,7 +1787,7 @@ packages: /@types/serve-static@1.15.2: resolution: {integrity: sha512-J2LqtvFYCzaj8pVYKw8klQXrLLk7TBZmQ4ShlcdkELFKGwGMfevMLneMMRkMgZxotOD9wg497LpC7O8PcvAmfw==} dependencies: - '@types/http-errors': 2.0.1 + '@types/http-errors': 2.0.3 '@types/mime': 3.0.1 '@types/node': 18.16.1 dev: true @@ -1678,14 +1796,14 @@ packages: resolution: {integrity: sha512-Hl219/BT5fLAaz6NDkSuhzasy49dwQS/DSdu4MdggFB8zcXv7vflBI3xp7FEmkmdDkBUI2bPUNeMttp2knYdxw==} dev: true - /@types/swagger-jsdoc@6.0.1: - resolution: {integrity: sha512-+MUpcbyxD528dECUBCEVm6abNuORdbuGjbrUdHDeAQ+rkPuo2a+L4N02WJHF3bonSSE6SJ3dUJwF2V6+cHnf0w==} + /@types/swagger-jsdoc@6.0.2: + resolution: {integrity: sha512-Qor3ObrH9Q27VWEiyw2zfE/nNgFCrA3D6Rm8V8fwYeLikfVw4au9Je0F+88QBgeCgMKSoCx12akTEaBqacIHjw==} dev: true - /@types/swagger-ui-express@4.1.3: - resolution: {integrity: sha512-jqCjGU/tGEaqIplPy3WyQg+Nrp6y80DCFnDEAvVKWkJyv0VivSSDCChkppHRHAablvInZe6pijDFMnavtN0vqA==} + /@types/swagger-ui-express@4.1.5: + resolution: {integrity: sha512-MRvm1OCzIR321glc/4tP34wRVmsupgLzs6XIq50CFp0CJUzxbpDsrhJxEBMQfoO46ixrlCiw3QXxEs5HHxYI8Q==} dependencies: - '@types/express': 4.17.17 + '@types/express': 4.17.20 '@types/serve-static': 1.15.2 dev: true @@ -1737,6 +1855,35 @@ packages: - supports-color dev: true + /@typescript-eslint/eslint-plugin@6.9.0(@typescript-eslint/parser@6.9.0)(eslint@8.52.0)(typescript@5.1.6): + resolution: {integrity: sha512-lgX7F0azQwRPB7t7WAyeHWVfW1YJ9NIgd9mvGhfQpRY56X6AVf8mwM8Wol+0z4liE7XX3QOt8MN1rUKCfSjRIA==} + engines: {node: ^16.0.0 || >=18.0.0} + peerDependencies: + '@typescript-eslint/parser': ^6.0.0 || ^6.0.0-alpha + eslint: ^7.0.0 || ^8.0.0 + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + dependencies: + '@eslint-community/regexpp': 4.5.1 + '@typescript-eslint/parser': 6.9.0(eslint@8.52.0)(typescript@5.1.6) + '@typescript-eslint/scope-manager': 6.9.0 + '@typescript-eslint/type-utils': 6.9.0(eslint@8.52.0)(typescript@5.1.6) + '@typescript-eslint/utils': 6.9.0(eslint@8.52.0)(typescript@5.1.6) + '@typescript-eslint/visitor-keys': 6.9.0 + debug: 4.3.4 + eslint: 8.52.0 + graphemer: 1.4.0 + ignore: 5.2.4 + natural-compare: 1.4.0 + semver: 7.5.4 + ts-api-utils: 1.0.1(typescript@5.1.6) + typescript: 5.1.6 + transitivePeerDependencies: + - supports-color + dev: true + /@typescript-eslint/parser@6.1.0(eslint@8.45.0)(typescript@5.1.6): resolution: {integrity: sha512-hIzCPvX4vDs4qL07SYzyomamcs2/tQYXg5DtdAfj35AyJ5PIUqhsLf4YrEIFzZcND7R2E8tpQIZKayxg8/6Wbw==} engines: {node: ^16.0.0 || >=18.0.0} @@ -1758,6 +1905,27 @@ packages: - supports-color dev: true + /@typescript-eslint/parser@6.9.0(eslint@8.52.0)(typescript@5.1.6): + resolution: {integrity: sha512-GZmjMh4AJ/5gaH4XF2eXA8tMnHWP+Pm1mjQR2QN4Iz+j/zO04b9TOvJYOX2sCNIQHtRStKTxRY1FX7LhpJT4Gw==} + engines: {node: ^16.0.0 || >=18.0.0} + peerDependencies: + eslint: ^7.0.0 || ^8.0.0 + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + dependencies: + '@typescript-eslint/scope-manager': 6.9.0 + '@typescript-eslint/types': 6.9.0 + '@typescript-eslint/typescript-estree': 6.9.0(typescript@5.1.6) + '@typescript-eslint/visitor-keys': 6.9.0 + debug: 4.3.4 + eslint: 8.52.0 + typescript: 5.1.6 + transitivePeerDependencies: + - supports-color + dev: true + /@typescript-eslint/scope-manager@6.1.0: resolution: {integrity: sha512-AxjgxDn27hgPpe2rQe19k0tXw84YCOsjDJ2r61cIebq1t+AIxbgiXKvD4999Wk49GVaAcdJ/d49FYel+Pp3jjw==} engines: {node: ^16.0.0 || >=18.0.0} @@ -1766,6 +1934,14 @@ packages: '@typescript-eslint/visitor-keys': 6.1.0 dev: true + /@typescript-eslint/scope-manager@6.9.0: + resolution: {integrity: sha512-1R8A9Mc39n4pCCz9o79qRO31HGNDvC7UhPhv26TovDsWPBDx+Sg3rOZdCELIA3ZmNoWAuxaMOT7aWtGRSYkQxw==} + engines: {node: ^16.0.0 || >=18.0.0} + dependencies: + '@typescript-eslint/types': 6.9.0 + '@typescript-eslint/visitor-keys': 6.9.0 + dev: true + /@typescript-eslint/type-utils@6.1.0(eslint@8.45.0)(typescript@5.1.6): resolution: {integrity: sha512-kFXBx6QWS1ZZ5Ni89TyT1X9Ag6RXVIVhqDs0vZE/jUeWlBv/ixq2diua6G7ece6+fXw3TvNRxP77/5mOMusx2w==} engines: {node: ^16.0.0 || >=18.0.0} @@ -1786,11 +1962,36 @@ packages: - supports-color dev: true + /@typescript-eslint/type-utils@6.9.0(eslint@8.52.0)(typescript@5.1.6): + resolution: {integrity: sha512-XXeahmfbpuhVbhSOROIzJ+b13krFmgtc4GlEuu1WBT+RpyGPIA4Y/eGnXzjbDj5gZLzpAXO/sj+IF/x2GtTMjQ==} + engines: {node: ^16.0.0 || >=18.0.0} + peerDependencies: + eslint: ^7.0.0 || ^8.0.0 + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + dependencies: + '@typescript-eslint/typescript-estree': 6.9.0(typescript@5.1.6) + '@typescript-eslint/utils': 6.9.0(eslint@8.52.0)(typescript@5.1.6) + debug: 4.3.4 + eslint: 8.52.0 + ts-api-utils: 1.0.1(typescript@5.1.6) + typescript: 5.1.6 + transitivePeerDependencies: + - supports-color + dev: true + /@typescript-eslint/types@6.1.0: resolution: {integrity: sha512-+Gfd5NHCpDoHDOaU/yIF3WWRI2PcBRKKpP91ZcVbL0t5tQpqYWBs3z/GGhvU+EV1D0262g9XCnyqQh19prU0JQ==} engines: {node: ^16.0.0 || >=18.0.0} dev: true + /@typescript-eslint/types@6.9.0: + resolution: {integrity: sha512-+KB0lbkpxBkBSiVCuQvduqMJy+I1FyDbdwSpM3IoBS7APl4Bu15lStPjgBIdykdRqQNYqYNMa8Kuidax6phaEw==} + engines: {node: ^16.0.0 || >=18.0.0} + dev: true + /@typescript-eslint/typescript-estree@6.1.0(typescript@5.1.6): resolution: {integrity: sha512-nUKAPWOaP/tQjU1IQw9sOPCDavs/iU5iYLiY/6u7gxS7oKQoi4aUxXS1nrrVGTyBBaGesjkcwwHkbkiD5eBvcg==} engines: {node: ^16.0.0 || >=18.0.0} @@ -1812,6 +2013,27 @@ packages: - supports-color dev: true + /@typescript-eslint/typescript-estree@6.9.0(typescript@5.1.6): + resolution: {integrity: sha512-NJM2BnJFZBEAbCfBP00zONKXvMqihZCrmwCaik0UhLr0vAgb6oguXxLX1k00oQyD+vZZ+CJn3kocvv2yxm4awQ==} + engines: {node: ^16.0.0 || >=18.0.0} + peerDependencies: + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + dependencies: + '@typescript-eslint/types': 6.9.0 + '@typescript-eslint/visitor-keys': 6.9.0 + debug: 4.3.4 + globby: 11.1.0 + is-glob: 4.0.3 + semver: 7.5.4 + ts-api-utils: 1.0.1(typescript@5.1.6) + typescript: 5.1.6 + transitivePeerDependencies: + - supports-color + dev: true + /@typescript-eslint/utils@6.1.0(eslint@8.45.0)(typescript@5.1.6): resolution: {integrity: sha512-wp652EogZlKmQoMS5hAvWqRKplXvkuOnNzZSE0PVvsKjpexd/XznRVHAtrfHFYmqaJz0DFkjlDsGYC9OXw+OhQ==} engines: {node: ^16.0.0 || >=18.0.0} @@ -1831,6 +2053,25 @@ packages: - typescript dev: true + /@typescript-eslint/utils@6.9.0(eslint@8.52.0)(typescript@5.1.6): + resolution: {integrity: sha512-5Wf+Jsqya7WcCO8me504FBigeQKVLAMPmUzYgDbWchINNh1KJbxCgVya3EQ2MjvJMVeXl3pofRmprqX6mfQkjQ==} + engines: {node: ^16.0.0 || >=18.0.0} + peerDependencies: + eslint: ^7.0.0 || ^8.0.0 + dependencies: + '@eslint-community/eslint-utils': 4.4.0(eslint@8.52.0) + '@types/json-schema': 7.0.12 + '@types/semver': 7.5.0 + '@typescript-eslint/scope-manager': 6.9.0 + '@typescript-eslint/types': 6.9.0 + '@typescript-eslint/typescript-estree': 6.9.0(typescript@5.1.6) + eslint: 8.52.0 + semver: 7.5.4 + transitivePeerDependencies: + - supports-color + - typescript + dev: true + /@typescript-eslint/visitor-keys@6.1.0: resolution: {integrity: sha512-yQeh+EXhquh119Eis4k0kYhj9vmFzNpbhM3LftWQVwqVjipCkwHBQOZutcYW+JVkjtTG9k8nrZU1UoNedPDd1A==} engines: {node: ^16.0.0 || >=18.0.0} @@ -1839,6 +2080,18 @@ packages: eslint-visitor-keys: 3.4.1 dev: true + /@typescript-eslint/visitor-keys@6.9.0: + resolution: {integrity: sha512-dGtAfqjV6RFOtIP8I0B4ZTBRrlTT8NHHlZZSchQx3qReaoDeXhYM++M4So2AgFK9ZB0emRPA6JI1HkafzA2Ibg==} + engines: {node: ^16.0.0 || >=18.0.0} + dependencies: + '@typescript-eslint/types': 6.9.0 + eslint-visitor-keys: 3.4.3 + dev: true + + /@ungap/structured-clone@1.2.0: + resolution: {integrity: sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==} + dev: true + /@xmldom/xmldom@0.8.10: resolution: {integrity: sha512-2WALfTl4xo2SkGCYRt6rDTFfk9R1czmBvUQy12gK2KuRKIpWEhcbbzy8EZXtz/jkRqHX8bFEc6FC1HjX4TUWYw==} engines: {node: '>=10.0.0'} @@ -1937,6 +2190,11 @@ packages: type-fest: 0.21.3 dev: true + /ansi-regex@2.1.1: + resolution: {integrity: sha512-TIGnTpdo+E3+pCyAluZvtED5p5wCqLdezCyhPZzKPcxvFplEt4i+W7OONCKgeZFT3+y5NZZfOOS/Bdcanm1MYA==} + engines: {node: '>=0.10.0'} + dev: false + /ansi-regex@5.0.1: resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} engines: {node: '>=8'} @@ -1974,9 +2232,20 @@ packages: resolution: {integrity: sha512-biN3PwB2gUtjaYy/isrU3aNWI5w+fAfvHkSvCKeQGxhmYpwKFUxudR3Yya+KqVRHBmEDYh+/lTozYCFbmzX4nA==} engines: {node: '>= 6.0.0'} + /aproba@1.2.0: + resolution: {integrity: sha512-Y9J6ZjXtoYh8RnXVCMOU/ttDmk1aBjunq9vO0ta5x85WDQiQfUF9sIPBITdbiiIVcBo03Hi3jMxigBtsddlXRw==} + dev: false + /aproba@2.0.0: resolution: {integrity: sha512-lYe4Gx7QT+MKGbDsA+Z+he/Wtef0BiwDOlK/XkBrdfsh9J/jPPXbX0tE9x9cl27Tmu5gg3QUbUrQYa/y+KOHPQ==} + /are-we-there-yet@1.1.7: + resolution: {integrity: sha512-nxwy40TuMiUGqMyRHgCSWZ9FM4VAoRP4xUYSTv5ImRog+h9yISPbVH7H8fASCIzYn9wlEv4zvFL7uKDMCFQm3g==} + dependencies: + delegates: 1.0.0 + readable-stream: 2.3.8 + dev: false + /are-we-there-yet@2.0.0: resolution: {integrity: sha512-Ci/qENmwHnsYo9xKIcUJN5LeDKdJ6R1Z1j9V/J5wyq8nh/mYPEpIKJbBZXtZjG04HiK7zV/p6Vs9952MrMeUIw==} engines: {node: '>=10'} @@ -2014,13 +2283,13 @@ packages: resolution: {integrity: sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==} dev: false - /array-includes@3.1.6: - resolution: {integrity: sha512-sgTbLvL6cNnw24FnbaDyjmvddQ2ML8arZsgaJhoABMoplz/4QRhtrYS+alr1BUM1Bwp6dhx8vVCBSLG+StwOFw==} + /array-includes@3.1.7: + resolution: {integrity: sha512-dlcsNBIiWhPkHdOEEKnehA+RNUWDc4UqFtnIXU4uuYDPtA4LDkr7qip2p0VvFAEXNDr0yWZ9PJyIRiGjRLQzwQ==} engines: {node: '>= 0.4'} dependencies: call-bind: 1.0.2 define-properties: 1.2.0 - es-abstract: 1.21.2 + es-abstract: 1.22.3 get-intrinsic: 1.2.1 is-string: 1.0.7 dev: true @@ -2030,26 +2299,50 @@ packages: engines: {node: '>=8'} dev: true - /array.prototype.flat@1.3.1: - resolution: {integrity: sha512-roTU0KWIOmJ4DRLmwKd19Otg0/mT3qPNt0Qb3GWW8iObuZXxrjB/pzn0R3hqpRSWg4HCwqx+0vwOnWnvlOyeIA==} + /array.prototype.findlastindex@1.2.3: + resolution: {integrity: sha512-LzLoiOMAxvy+Gd3BAq3B7VeIgPdo+Q8hthvKtXybMvRV0jrXfJM/t8mw7nNlpEcVlVUnCnM2KSX4XU5HmpodOA==} engines: {node: '>= 0.4'} dependencies: call-bind: 1.0.2 define-properties: 1.2.0 - es-abstract: 1.21.2 + es-abstract: 1.22.3 es-shim-unscopables: 1.0.0 + get-intrinsic: 1.2.1 dev: true - /array.prototype.flatmap@1.3.1: - resolution: {integrity: sha512-8UGn9O1FDVvMNB0UlLv4voxRMze7+FpHyF5mSMRjWHUMlpoDViniy05870VlxhfgTnLbpuwTzvD76MTtWxB/mQ==} + /array.prototype.flat@1.3.2: + resolution: {integrity: sha512-djYB+Zx2vLewY8RWlNCUdHjDXs2XOgm602S9E7P/UpHgfeHL00cRiIF+IN/G/aUJ7kGPb6yO/ErDI5V2s8iycA==} engines: {node: '>= 0.4'} dependencies: call-bind: 1.0.2 define-properties: 1.2.0 - es-abstract: 1.21.2 + es-abstract: 1.22.3 + es-shim-unscopables: 1.0.0 + dev: true + + /array.prototype.flatmap@1.3.2: + resolution: {integrity: sha512-Ewyx0c9PmpcsByhSW4r+9zDU7sGjFc86qf/kKtuSCRdhfbk0SNLLkaT5qvcHnRGgc5NP/ly/y+qkXkqONX54CQ==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.2 + define-properties: 1.2.0 + es-abstract: 1.22.3 es-shim-unscopables: 1.0.0 dev: true + /arraybuffer.prototype.slice@1.0.2: + resolution: {integrity: sha512-yMBKppFur/fbHu9/6USUe03bZ4knMYiwFBcyiaXB8Go0qNehwX6inYPzK9U0NeQvGxKthcmHcaR8P5MStSRBAw==} + engines: {node: '>= 0.4'} + dependencies: + array-buffer-byte-length: 1.0.0 + call-bind: 1.0.5 + define-properties: 1.2.0 + es-abstract: 1.22.3 + get-intrinsic: 1.2.2 + is-array-buffer: 3.0.2 + is-shared-array-buffer: 1.0.2 + dev: true + /async@2.6.4: resolution: {integrity: sha512-mzo5dfJYwAn29PeiJ0zvwTo04zj8HDJj0Mn8TD7sno7q12prdbnasKJHhkm2c1LgrhlJ0teaea8860oxi51mGA==} dependencies: @@ -2098,17 +2391,17 @@ packages: - debug dev: false - /babel-jest@29.6.1(@babel/core@7.22.8): - resolution: {integrity: sha512-qu+3bdPEQC6KZSPz+4Fyjbga5OODNcp49j6GKzG1EKbkfyJBxEYGVUmVGpwCSeGouG52R4EgYMLb6p9YeEEQ4A==} + /babel-jest@29.7.0(@babel/core@7.22.8): + resolution: {integrity: sha512-BrvGY3xZSwEcCzKvKsCi2GgHqDqsYkOP4/by5xCgIwGXQxIEh+8ew3gmrE1y7XRR6LHZIj6yLYnUi/mm2KXKBg==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} peerDependencies: '@babel/core': ^7.8.0 dependencies: '@babel/core': 7.22.8 - '@jest/transform': 29.6.1 + '@jest/transform': 29.7.0 '@types/babel__core': 7.20.1 babel-plugin-istanbul: 6.1.1 - babel-preset-jest: 29.5.0(@babel/core@7.22.8) + babel-preset-jest: 29.6.3(@babel/core@7.22.8) chalk: 4.1.2 graceful-fs: 4.2.11 slash: 3.0.0 @@ -2129,8 +2422,8 @@ packages: - supports-color dev: true - /babel-plugin-jest-hoist@29.5.0: - resolution: {integrity: sha512-zSuuuAlTMT4mzLj2nPnUm6fsE6270vdOfnpbJ+RmruU75UhLFvL0N2NgI7xpeS7NaB6hGqmd5pVpGTDYvi4Q3w==} + /babel-plugin-jest-hoist@29.6.3: + resolution: {integrity: sha512-ESAc/RJvGTFEzRwOTT4+lNDk/GNHMkKbNzsvT0qKRfDyyYTskxB5rnU2njIDYVxXCBHHEI1c0YwHob3WaYujOg==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} dependencies: '@babel/template': 7.22.5 @@ -2159,14 +2452,14 @@ packages: '@babel/plugin-syntax-top-level-await': 7.14.5(@babel/core@7.22.8) dev: true - /babel-preset-jest@29.5.0(@babel/core@7.22.8): - resolution: {integrity: sha512-JOMloxOqdiBSxMAzjRaH023/vvcaSaec49zvg+2LmNsktC7ei39LTJGw02J+9uUtTZUq6xbLyJ4dxe9sSmIuAg==} + /babel-preset-jest@29.6.3(@babel/core@7.22.8): + resolution: {integrity: sha512-0B3bhxR6snWXJZtR/RliHTDPRgn1sNHOR0yVtq/IiQFyuOVjFS+wuio/R4gSNkyYmKmJB4wGZv2NZanmKmTnNA==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} peerDependencies: '@babel/core': ^7.0.0 dependencies: '@babel/core': 7.22.8 - babel-plugin-jest-hoist: 29.5.0 + babel-plugin-jest-hoist: 29.6.3 babel-preset-current-node-syntax: 1.0.1(@babel/core@7.22.8) dev: true @@ -2188,23 +2481,18 @@ packages: safe-buffer: 5.1.2 dev: false - /bcrypt@5.0.1: - resolution: {integrity: sha512-9BTgmrhZM2t1bNuDtrtIMVSmmxZBrJ71n8Wg+YgdjHuIWYF7SjjmCPZFB+/5i/o/PIeRpwVJR3P+NrpIItUjqw==} + /bcrypt@5.1.1: + resolution: {integrity: sha512-AGBHOG5hPYZ5Xl9KXzU5iKq9516yEmvCKDg3ecP5kX2aB6UqTeXZxk2ELnDgDm6BQSMlLt9rDB4LoSMx0rYwww==} engines: {node: '>= 10.0.0'} requiresBuild: true dependencies: - '@mapbox/node-pre-gyp': 1.0.10 - node-addon-api: 3.2.1 + '@mapbox/node-pre-gyp': 1.0.11 + node-addon-api: 5.1.0 transitivePeerDependencies: - encoding - supports-color dev: false - /big-integer@1.6.51: - resolution: {integrity: sha512-GPEid2Y9QU1Exl1rpO9B2IPJGHPSupF5GnVIP0blYvNOMer2bTvSWs1jGOUg04hTmu67nmLsQ9TBo1puaotBHg==} - engines: {node: '>=0.6'} - dev: true - /binary-extensions@2.2.0: resolution: {integrity: sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==} engines: {node: '>=8'} @@ -2216,20 +2504,22 @@ packages: readable-stream: 3.6.2 dev: true - /body-parser@1.19.1: - resolution: {integrity: sha512-8ljfQi5eBk8EJfECMrgqNGWPEY5jWP+1IzkzkGdFFEwFQZZyaZ21UqdaHktgiMlH0xLHqIFtE/u2OYE5dOtViA==} - engines: {node: '>= 0.8'} + /body-parser@1.20.1: + resolution: {integrity: sha512-jWi7abTbYwajOytWCQc37VulmWiRae5RyTpaCyDcS5/lMdtwSz5lOpDE67srw/HYe35f1z3fDQw+3txg7gNtWw==} + engines: {node: '>= 0.8', npm: 1.2.8000 || >= 1.4.16} dependencies: - bytes: 3.1.1 + bytes: 3.1.2 content-type: 1.0.5 debug: 2.6.9 - depd: 1.1.2 - http-errors: 1.8.1 + depd: 2.0.0 + destroy: 1.2.0 + http-errors: 2.0.0 iconv-lite: 0.4.24 - on-finished: 2.3.0 - qs: 6.9.6 - raw-body: 2.4.2 + on-finished: 2.4.1 + qs: 6.11.0 + raw-body: 2.5.1 type-is: 1.6.18 + unpipe: 1.0.0 transitivePeerDependencies: - supports-color dev: false @@ -2238,19 +2528,18 @@ packages: resolution: {integrity: sha512-d0II/GO9uf9lfUHH2BQsjxzRJZBdsjgsBiW4BvhWk/3qoKwQFjIDVN19PfX8F2D/r9PCMTtLWjYVCFrpeYUzsw==} dev: false - /bplist-parser@0.2.0: - resolution: {integrity: sha512-z0M+byMThzQmD9NILRniCUXYsYpjwnlO8N5uCFaCqIOpqRsJCrQL9NK3JsD67CN5a08nF5oIL2bD6loTdHOuKw==} - engines: {node: '>= 5.10.0'} - dependencies: - big-integer: 1.6.51 - dev: true - /brace-expansion@1.1.11: resolution: {integrity: sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==} dependencies: balanced-match: 1.0.2 concat-map: 0.0.1 + /brace-expansion@2.0.1: + resolution: {integrity: sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==} + dependencies: + balanced-match: 1.0.2 + dev: false + /braces@3.0.2: resolution: {integrity: sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==} engines: {node: '>=8'} @@ -2261,6 +2550,7 @@ packages: /browserslist@4.21.9: resolution: {integrity: sha512-M0MFoZzbUrRU4KNfCrDLnvyE7gub+peetoTid3TBIqtunaDJyXlwhakT+/VkvSXcfIzFfK/nkCs4nmyTmxdNSg==} engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} + hasBin: true dependencies: caniuse-lite: 1.0.30001513 electron-to-chromium: 1.4.454 @@ -2298,13 +2588,6 @@ packages: base64-js: 1.5.1 ieee754: 1.2.1 - /bundle-name@3.0.0: - resolution: {integrity: sha512-PKA4BeSvBpQKQ8iPOGCSiell+N8P+Tf1DlwqmYhpe2gAhKPHn8EYOxVT+ShuGmhg8lN8XiSlS80yiExKXrURlw==} - engines: {node: '>=12'} - dependencies: - run-applescript: 5.0.0 - dev: true - /busboy@1.6.0: resolution: {integrity: sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==} engines: {node: '>=10.16.0'} @@ -2312,8 +2595,8 @@ packages: streamsearch: 1.1.0 dev: true - /bytes@3.1.1: - resolution: {integrity: sha512-dWe4nWO/ruEOY7HkUJ5gFt1DCFV9zPRoJr8pV0/ASQermOZjtq8jMjOprC0Kd10GLN+l7xaUPvxzJFWtxGu8Fg==} + /bytes@3.1.2: + resolution: {integrity: sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==} engines: {node: '>= 0.8'} dev: false @@ -2356,6 +2639,14 @@ packages: function-bind: 1.1.1 get-intrinsic: 1.2.1 + /call-bind@1.0.5: + resolution: {integrity: sha512-C3nQxfFZxFRVoJoGKKI8y3MOEo129NQ+FgQ08iye+Mk4zNZZGdjfs06bVTr+DBSlA66Q2VEcMki/cUCP4SercQ==} + dependencies: + function-bind: 1.1.2 + get-intrinsic: 1.2.2 + set-function-length: 1.1.1 + dev: true + /call-me-maybe@1.0.2: resolution: {integrity: sha512-HpX65o1Hnr9HH25ojC1YGs7HCQLq0GCOibSaWER0eNpgJ/Z1MZv2mTc7+xh6WOPxbRVcmgbv4hGU+uSQ/2xFZQ==} @@ -2455,6 +2746,10 @@ packages: fsevents: 2.3.2 dev: true + /chownr@1.1.4: + resolution: {integrity: sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==} + dev: false + /chownr@2.0.0: resolution: {integrity: sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==} engines: {node: '>=10'} @@ -2485,6 +2780,7 @@ packages: /cli-highlight@2.1.11: resolution: {integrity: sha512-9KDcoEVwyUXrjcJNvHD0NFc/hiwe/WPVYIleQh2O1N2Zro5gWJZ/K+3DGn8w8P/F6FxOgzyC5bxDyHIgCSPhGg==} engines: {node: '>=8.0.0', npm: '>=5.0.0'} + hasBin: true dependencies: chalk: 4.1.2 highlight.js: 10.7.3 @@ -2526,6 +2822,11 @@ packages: engines: {iojs: '>= 1.0.0', node: '>= 0.12.0'} dev: true + /code-point-at@1.1.0: + resolution: {integrity: sha512-RpAVKQA5T63xEj6/giIbUEtZwJ4UFIc3ZtvEkiaUERylqe8xb5IvqcgOurZLahv93CLKfxcw5YI+DZcUBRyLXA==} + engines: {node: '>=0.10.0'} + dev: false + /collect-v8-coverage@1.0.2: resolution: {integrity: sha512-lHl4d5/ONEbLlJvaJNtsF/Lz+WvB07u2ycqTYbdrq7UypDXailES4valYb2eWiJFxZlVmpGekfqoxQhzyFdT4Q==} dev: true @@ -2556,6 +2857,7 @@ packages: /color-support@1.1.3: resolution: {integrity: sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg==} + hasBin: true /color@3.2.1: resolution: {integrity: sha512-aBl7dZI9ENN6fUGC7mWpMTPNHmWUSNan9tuWN6ahh5ZLNk9baLJOnSMlrQkHcrfFgz2/RigjUVAjdx36VcemKA==} @@ -2585,6 +2887,7 @@ packages: /commander@2.20.3: resolution: {integrity: sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==} requiresBuild: true + dev: true optional: true /commander@6.2.0: @@ -2592,6 +2895,13 @@ packages: engines: {node: '>= 6'} dev: false + /commander@9.5.0: + resolution: {integrity: sha512-KRs7WVDKg86PWiuAqhDrAQnTXZKraVcCc6vFdL14qrZ/DcWwuRo7VoiYXalXO7S5GKpqYiVEwCbgFDfxNHKJBQ==} + engines: {node: ^12.20.0 || >=14} + requiresBuild: true + dev: false + optional: true + /concat-map@0.0.1: resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} @@ -2647,6 +2957,15 @@ packages: engines: {node: '>= 0.6'} dev: false + /cookie@0.5.0: + resolution: {integrity: sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw==} + engines: {node: '>= 0.6'} + dev: false + + /core-util-is@1.0.3: + resolution: {integrity: sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==} + dev: false + /cors@2.8.5: resolution: {integrity: sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==} engines: {node: '>= 0.10'} @@ -2655,12 +2974,30 @@ packages: vary: 1.1.2 dev: false - /cron-parser@3.5.0: - resolution: {integrity: sha512-wyVZtbRs6qDfFd8ap457w3XVntdvqcwBGxBoTvJQH9KGVKL/fB+h2k3C8AqiVxvUQKN1Ps/Ns46CNViOpVDhfQ==} - engines: {node: '>=0.8'} + /create-jest@29.7.0(@types/node@18.16.1): + resolution: {integrity: sha512-Adz2bdH0Vq3F53KEMJOoftQFutWCukm6J24wbPWRO4k1kMY7gS7ds/uoJkNuV8wDCtWWnuwGcJwpWcih+zEW1Q==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + hasBin: true dependencies: - is-nan: 1.3.2 - luxon: 1.28.1 + '@jest/types': 29.6.3 + chalk: 4.1.2 + exit: 0.1.2 + graceful-fs: 4.2.11 + jest-config: 29.7.0(@types/node@18.16.1) + jest-util: 29.7.0 + prompts: 2.4.2 + transitivePeerDependencies: + - '@types/node' + - babel-plugin-macros + - supports-color + - ts-node + dev: true + + /cron-parser@4.9.0: + resolution: {integrity: sha512-p0SaNjrHOnQeR8/VnfGbmg9te2kfyYSQ7Sc/j/6DtPL3JQvKxmjO9TSjNFpujqV3vEYYBvNNvXSxzyksBWAx1Q==} + engines: {node: '>=12.0.0'} + dependencies: + luxon: 3.4.3 dev: false /cross-spawn@7.0.3: @@ -2672,9 +3009,11 @@ packages: which: 2.0.2 dev: true - /date-fns@2.29.3: - resolution: {integrity: sha512-dDCnyH2WnnKusqvZZ6+jA1O51Ibt8ZMRNkDZdyAyK4YfbDwa/cEmuztzG5pk6hqlp9aSBPYcjOlktquahGwGeA==} + /date-fns@2.30.0: + resolution: {integrity: sha512-fnULvOpxnC5/Vg3NCiWelDsLiUc9bRwAPs/+LfTLNvetFCtCTN+yQz15C/fs4AwX1R9K5GLtLfn8QW+dWisaAw==} engines: {node: '>=0.11'} + dependencies: + '@babel/runtime': 7.23.2 dev: false /date-utils@1.2.21: @@ -2703,7 +3042,6 @@ packages: dependencies: ms: 2.1.3 supports-color: 5.5.0 - dev: true /debug@4.3.4: resolution: {integrity: sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==} @@ -2721,10 +3059,20 @@ packages: engines: {node: '>=0.10.0'} dev: true - /dedent@0.7.0: - resolution: {integrity: sha512-Q6fKUPqnAHAyhiUgFU7BUzLiv0kd8saH9al7tnu5Q/okj6dnupxyTgFIBjVzJATdfIAm9NAsvXNzjaKa+bxVyA==} + /dedent@1.5.1: + resolution: {integrity: sha512-+LxW+KLWxu3HW3M2w2ympwtqPrqYRzU8fqi6Fhd18fBALe15blJPI/I4+UHveMVG6lJqB4JNd4UG0S5cnVHwIg==} + peerDependencies: + babel-plugin-macros: ^3.1.0 + peerDependenciesMeta: + babel-plugin-macros: + optional: true dev: true + /deep-extend@0.6.0: + resolution: {integrity: sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==} + engines: {node: '>=4.0.0'} + dev: false + /deep-is@0.1.4: resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==} dev: true @@ -2733,27 +3081,13 @@ packages: resolution: {integrity: sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==} engines: {node: '>=0.10.0'} - /default-browser-id@3.0.0: - resolution: {integrity: sha512-OZ1y3y0SqSICtE8DE4S8YOE9UZOJ8wO16fKWVP5J1Qz42kV9jcnMVFrEE/noXb/ss3Q4pZIH79kxofzyNNtUNA==} - engines: {node: '>=12'} - dependencies: - bplist-parser: 0.2.0 - untildify: 4.0.0 - dev: true - - /default-browser@4.0.0: - resolution: {integrity: sha512-wX5pXO1+BrhMkSbROFsyxUm0i/cJEScyNhA4PPxc41ICuv05ZZB/MX28s8aZx6xjmatvebIapF6hLEKEcpneUA==} - engines: {node: '>=14.16'} + /define-data-property@1.1.1: + resolution: {integrity: sha512-E7uGkTzkk1d0ByLeSc6ZsFS79Axg+m1P/VsgYsxHgiuc3tFSj+MjMIwe90FC4lOAZzNBdY7kkO2P2wKdsQ1vgQ==} + engines: {node: '>= 0.4'} dependencies: - bundle-name: 3.0.0 - default-browser-id: 3.0.0 - execa: 7.1.1 - titleize: 3.0.0 - dev: true - - /define-lazy-prop@3.0.0: - resolution: {integrity: sha512-N+MeXYoqr3pOgn8xfyRPREN7gHakLYjhsHhWGT3fWAiL4IkAt0iDw14QiiEm2bE30c5XX5q0FtAA3CK5f9/BUg==} - engines: {node: '>=12'} + get-intrinsic: 1.2.2 + gopd: 1.0.1 + has-property-descriptors: 1.0.0 dev: true /define-properties@1.2.0: @@ -2779,17 +3113,19 @@ packages: resolution: {integrity: sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw==} engines: {node: '>=0.10'} - /depd@1.1.2: - resolution: {integrity: sha512-7emPTl6Dpo6JRXOXjLRxck+FlLRX5847cLKEn00PLAgc3g2hTZZgr+e4c2v6QpSmLeFP3n5yUo7ft6avBK/5jQ==} - engines: {node: '>= 0.6'} - dev: false - /depd@2.0.0: resolution: {integrity: sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==} engines: {node: '>= 0.8'} - /destroy@1.0.4: - resolution: {integrity: sha512-3NdhDuEXnfun/z7x9GOElY49LoqVHoGScmOKwmxhsS8N5Y+Z8KyPPDnaSzqWgYt/ji4mqwfTS34Htrk0zPIXVg==} + /destroy@1.2.0: + resolution: {integrity: sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==} + engines: {node: '>= 0.8', npm: 1.2.8000 || >= 1.4.16} + dev: false + + /detect-libc@1.0.3: + resolution: {integrity: sha512-pGjwhsmsp4kL2RTz08wcOlGN83otlqHeD/Z5T8GXZB+/YcpQ/dgo+lbU8ZsGxV0HIvqqxo9l7mqYwyYMD9bKDg==} + engines: {node: '>=0.10'} + hasBin: true dev: false /detect-libc@2.0.1: @@ -2806,6 +3142,11 @@ packages: engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} dev: true + /diff-sequences@29.6.3: + resolution: {integrity: sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dev: true + /dir-glob@3.0.1: resolution: {integrity: sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==} engines: {node: '>=8'} @@ -2833,15 +3174,9 @@ packages: tslib: 2.6.0 dev: true - /dotenv@16.0.0: - resolution: {integrity: sha512-qD9WU0MPM4SWLPJy/r2Be+2WgQj8plChsyrCNQzW/0WjvcJQiKQJ9mH3ZgB3fxbUUxgc/11ZJ0Fi5KiimWGz2Q==} - engines: {node: '>=12'} - dev: false - /dotenv@16.3.1: resolution: {integrity: sha512-IPzF4w4/Rd94bA9imS68tZBaYyBWSCE47V1RGuMrB94iyTOIEwRmVL2x/4An+6mETpLrKJ5hQkB8W4kFAadeIQ==} engines: {node: '>=12'} - dev: true /dotenv@8.6.0: resolution: {integrity: sha512-IrPdXQsk2BbzvCBGBOTmmSH5SodmqZNt4ERAZDmW4CT+tL8VtvinqywuANaFu4bOMWki16nqf0e4oC0QIaDr/g==} @@ -2953,6 +3288,51 @@ packages: which-typed-array: 1.1.9 dev: true + /es-abstract@1.22.3: + resolution: {integrity: sha512-eiiY8HQeYfYH2Con2berK+To6GrK2RxbPawDkGq4UiCQQfZHb6wX9qQqkbpPqaxQFcl8d9QzZqo0tGE0VcrdwA==} + engines: {node: '>= 0.4'} + dependencies: + array-buffer-byte-length: 1.0.0 + arraybuffer.prototype.slice: 1.0.2 + available-typed-arrays: 1.0.5 + call-bind: 1.0.5 + es-set-tostringtag: 2.0.1 + es-to-primitive: 1.2.1 + function.prototype.name: 1.1.6 + get-intrinsic: 1.2.2 + get-symbol-description: 1.0.0 + globalthis: 1.0.3 + gopd: 1.0.1 + has-property-descriptors: 1.0.0 + has-proto: 1.0.1 + has-symbols: 1.0.3 + hasown: 2.0.0 + 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.12 + is-weakref: 1.0.2 + object-inspect: 1.13.1 + object-keys: 1.1.1 + object.assign: 4.1.4 + regexp.prototype.flags: 1.5.1 + safe-array-concat: 1.0.1 + safe-regex-test: 1.0.0 + string.prototype.trim: 1.2.8 + string.prototype.trimend: 1.0.7 + string.prototype.trimstart: 1.0.7 + typed-array-buffer: 1.0.0 + typed-array-byte-length: 1.0.0 + typed-array-byte-offset: 1.0.0 + typed-array-length: 1.0.4 + unbox-primitive: 1.0.2 + which-typed-array: 1.1.13 + dev: true + /es-set-tostringtag@2.0.1: resolution: {integrity: sha512-g3OMbtlwY3QewlqAiMLI47KywjWZoEytKr8pf6iTC8uJq5bIAH52Z9pnQ8pVL6whrCto53JZDuUIsifGeLorTg==} engines: {node: '>= 0.4'} @@ -3030,7 +3410,7 @@ packages: engines: {node: '>=10'} dev: true - /eslint-config-airbnb-base@15.0.0(eslint-plugin-import@2.27.5)(eslint@8.45.0): + /eslint-config-airbnb-base@15.0.0(eslint-plugin-import@2.29.0)(eslint@8.52.0): resolution: {integrity: sha512-xaX3z4ZZIcFLvh2oUNvcX5oEofXda7giYmuplVxoOg5A7EXJMrUyqRgR+mhDhPK8LZ4PttFOBvCYDbX3sUoUig==} engines: {node: ^10.12.0 || >=12.0.0} peerDependencies: @@ -3038,25 +3418,34 @@ packages: eslint-plugin-import: ^2.25.2 dependencies: confusing-browser-globals: 1.0.11 - eslint: 8.45.0 - eslint-plugin-import: 2.27.5(@typescript-eslint/parser@6.1.0)(eslint-import-resolver-typescript@3.5.5)(eslint@8.45.0) + eslint: 8.52.0 + eslint-plugin-import: 2.29.0(@typescript-eslint/parser@6.9.0)(eslint-import-resolver-typescript@3.6.1)(eslint@8.52.0) object.assign: 4.1.4 object.entries: 1.1.6 semver: 6.3.0 dev: true - /eslint-import-resolver-node@0.3.7: - resolution: {integrity: sha512-gozW2blMLJCeFpBwugLTGyvVjNoeo1knonXAcatC6bjPBZitotxdWf7Gimr25N4c0AAOo4eOUfaG82IJPDpqCA==} + /eslint-config-prettier@9.0.0(eslint@8.52.0): + resolution: {integrity: sha512-IcJsTkJae2S35pRsRAwoCE+925rJJStOdkKnLVgtE+tEpqU0EVVM7OqrwxqgptKdX29NUwC82I5pXsGFIgSevw==} + hasBin: true + peerDependencies: + eslint: '>=7.0.0' + dependencies: + eslint: 8.52.0 + dev: true + + /eslint-import-resolver-node@0.3.9: + resolution: {integrity: sha512-WFj2isz22JahUv+B788TlO3N6zL3nNJGU8CcZbPZvVEkBPaJdCV4vy5wyghty5ROFbCRnm132v8BScu5/1BQ8g==} dependencies: debug: 3.2.7(supports-color@5.5.0) - is-core-module: 2.12.1 - resolve: 1.22.2 + is-core-module: 2.13.1 + resolve: 1.22.8 transitivePeerDependencies: - supports-color dev: true - /eslint-import-resolver-typescript@3.5.5(@typescript-eslint/parser@6.1.0)(eslint-plugin-import@2.27.5)(eslint@8.45.0): - resolution: {integrity: sha512-TdJqPHs2lW5J9Zpe17DZNQuDnox4xo2o+0tE7Pggain9Rbc19ik8kFtXdxZ250FVx2kF4vlt2RSf4qlUpG7bhw==} + /eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@6.9.0)(eslint-plugin-import@2.29.0)(eslint@8.52.0): + resolution: {integrity: sha512-xgdptdoi5W3niYeuQxKmzVDTATvLYqhpwmykwsh7f6HIOStGWEIL9iqZgQDF9u9OEzrRwR8no5q2VT+bjAujTg==} engines: {node: ^14.18.0 || >=16.0.0} peerDependencies: eslint: '*' @@ -3064,14 +3453,13 @@ packages: dependencies: debug: 4.3.4 enhanced-resolve: 5.15.0 - eslint: 8.45.0 - eslint-module-utils: 2.8.0(@typescript-eslint/parser@6.1.0)(eslint-import-resolver-node@0.3.7)(eslint-import-resolver-typescript@3.5.5)(eslint@8.45.0) - eslint-plugin-import: 2.27.5(@typescript-eslint/parser@6.1.0)(eslint-import-resolver-typescript@3.5.5)(eslint@8.45.0) + eslint: 8.52.0 + eslint-module-utils: 2.8.0(@typescript-eslint/parser@6.9.0)(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.1)(eslint@8.52.0) + eslint-plugin-import: 2.29.0(@typescript-eslint/parser@6.9.0)(eslint-import-resolver-typescript@3.6.1)(eslint@8.52.0) + fast-glob: 3.3.1 get-tsconfig: 4.6.2 - globby: 13.2.2 is-core-module: 2.12.1 is-glob: 4.0.3 - synckit: 0.8.5 transitivePeerDependencies: - '@typescript-eslint/parser' - eslint-import-resolver-node @@ -3079,7 +3467,7 @@ packages: - supports-color dev: true - /eslint-module-utils@2.8.0(@typescript-eslint/parser@6.1.0)(eslint-import-resolver-node@0.3.7)(eslint-import-resolver-typescript@3.5.5)(eslint@8.45.0): + /eslint-module-utils@2.8.0(@typescript-eslint/parser@6.9.0)(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.1)(eslint@8.52.0): resolution: {integrity: sha512-aWajIYfsqCKRDgUfjEXNN/JlrzauMuSEy5sbd7WXbtW3EH6A6MpwEh42c7qD+MqQo9QMJ6fWLAeIJynx0g6OAw==} engines: {node: '>=4'} peerDependencies: @@ -3100,17 +3488,17 @@ packages: eslint-import-resolver-webpack: optional: true dependencies: - '@typescript-eslint/parser': 6.1.0(eslint@8.45.0)(typescript@5.1.6) + '@typescript-eslint/parser': 6.9.0(eslint@8.52.0)(typescript@5.1.6) debug: 3.2.7(supports-color@5.5.0) - eslint: 8.45.0 - eslint-import-resolver-node: 0.3.7 - eslint-import-resolver-typescript: 3.5.5(@typescript-eslint/parser@6.1.0)(eslint-plugin-import@2.27.5)(eslint@8.45.0) + eslint: 8.52.0 + eslint-import-resolver-node: 0.3.9 + eslint-import-resolver-typescript: 3.6.1(@typescript-eslint/parser@6.9.0)(eslint-plugin-import@2.29.0)(eslint@8.52.0) transitivePeerDependencies: - supports-color dev: true - /eslint-plugin-import@2.27.5(@typescript-eslint/parser@6.1.0)(eslint-import-resolver-typescript@3.5.5)(eslint@8.45.0): - resolution: {integrity: sha512-LmEt3GVofgiGuiE+ORpnvP+kAm3h6MLZJ4Q5HCyHADofsb4VzXFsRiWj3c0OFiV+3DWFh0qg3v9gcPlfc3zRow==} + /eslint-plugin-import@2.29.0(@typescript-eslint/parser@6.9.0)(eslint-import-resolver-typescript@3.6.1)(eslint@8.52.0): + resolution: {integrity: sha512-QPOO5NO6Odv5lpoTkddtutccQjysJuFxoPS7fAHO+9m9udNHvTCPSAMW9zGAYj8lAIdr40I8yPCdUYrncXtrwg==} engines: {node: '>=4'} peerDependencies: '@typescript-eslint/parser': '*' @@ -3119,22 +3507,24 @@ packages: '@typescript-eslint/parser': optional: true dependencies: - '@typescript-eslint/parser': 6.1.0(eslint@8.45.0)(typescript@5.1.6) - array-includes: 3.1.6 - array.prototype.flat: 1.3.1 - array.prototype.flatmap: 1.3.1 + '@typescript-eslint/parser': 6.9.0(eslint@8.52.0)(typescript@5.1.6) + array-includes: 3.1.7 + array.prototype.findlastindex: 1.2.3 + array.prototype.flat: 1.3.2 + array.prototype.flatmap: 1.3.2 debug: 3.2.7(supports-color@5.5.0) doctrine: 2.1.0 - eslint: 8.45.0 - eslint-import-resolver-node: 0.3.7 - eslint-module-utils: 2.8.0(@typescript-eslint/parser@6.1.0)(eslint-import-resolver-node@0.3.7)(eslint-import-resolver-typescript@3.5.5)(eslint@8.45.0) - has: 1.0.3 - is-core-module: 2.12.1 + eslint: 8.52.0 + eslint-import-resolver-node: 0.3.9 + eslint-module-utils: 2.8.0(@typescript-eslint/parser@6.9.0)(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.1)(eslint@8.52.0) + hasown: 2.0.0 + is-core-module: 2.13.1 is-glob: 4.0.3 minimatch: 3.1.2 - object.values: 1.1.6 - resolve: 1.22.2 - semver: 6.3.0 + object.fromentries: 2.0.7 + object.groupby: 1.0.1 + object.values: 1.1.7 + semver: 6.3.1 tsconfig-paths: 3.14.2 transitivePeerDependencies: - eslint-import-resolver-typescript @@ -3150,11 +3540,24 @@ packages: estraverse: 5.3.0 dev: true + /eslint-scope@7.2.2: + resolution: {integrity: sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + dependencies: + esrecurse: 4.3.0 + estraverse: 5.3.0 + dev: true + /eslint-visitor-keys@3.4.1: resolution: {integrity: sha512-pZnmmLwYzf+kWaM/Qgrvpen51upAktaaiI01nsJD/Yr3lMOdNtq0cxkrrg16w64VtisN6okbs7Q8AfGqj4c9fA==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} dev: true + /eslint-visitor-keys@3.4.3: + resolution: {integrity: sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + dev: true + /eslint@8.45.0: resolution: {integrity: sha512-pd8KSxiQpdYRfYa9Wufvdoct3ZPQQuVuU5O6scNgMuOMYuxvH0IGaYK0wUFjo4UYYQQCUndlXiMbnxopwvvTiw==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} @@ -3200,6 +3603,53 @@ packages: - supports-color dev: true + /eslint@8.52.0: + resolution: {integrity: sha512-zh/JHnaixqHZsolRB/w9/02akBk9EPrOs9JwcTP2ek7yL5bVvXuRariiaAjjoJ5DvuwQ1WAE/HsMz+w17YgBCg==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + hasBin: true + dependencies: + '@eslint-community/eslint-utils': 4.4.0(eslint@8.52.0) + '@eslint-community/regexpp': 4.10.0 + '@eslint/eslintrc': 2.1.3 + '@eslint/js': 8.52.0 + '@humanwhocodes/config-array': 0.11.13 + '@humanwhocodes/module-importer': 1.0.1 + '@nodelib/fs.walk': 1.2.8 + '@ungap/structured-clone': 1.2.0 + ajv: 6.12.6 + chalk: 4.1.2 + cross-spawn: 7.0.3 + debug: 4.3.4 + doctrine: 3.0.0 + escape-string-regexp: 4.0.0 + eslint-scope: 7.2.2 + eslint-visitor-keys: 3.4.3 + espree: 9.6.1 + esquery: 1.5.0 + esutils: 2.0.3 + fast-deep-equal: 3.1.3 + file-entry-cache: 6.0.1 + find-up: 5.0.0 + glob-parent: 6.0.2 + globals: 13.20.0 + graphemer: 1.4.0 + ignore: 5.2.4 + imurmurhash: 0.1.4 + is-glob: 4.0.3 + is-path-inside: 3.0.3 + 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.3 + strip-ansi: 6.0.1 + text-table: 0.2.0 + transitivePeerDependencies: + - supports-color + dev: true + /espree@9.6.0: resolution: {integrity: sha512-1FH/IiruXZ84tpUlm0aCUEwMl2Ho5ilqVh0VvQXw+byAz/4SAciyHLlfmL5WYqsvD38oymdUwBss0LtK8m4s/A==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} @@ -3209,6 +3659,15 @@ packages: eslint-visitor-keys: 3.4.1 dev: true + /espree@9.6.1: + resolution: {integrity: sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + dependencies: + acorn: 8.10.0 + acorn-jsx: 5.3.2(acorn@8.10.0) + eslint-visitor-keys: 3.4.3 + dev: true + /esprima@4.0.1: resolution: {integrity: sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==} engines: {node: '>=4'} @@ -3270,21 +3729,6 @@ packages: strip-final-newline: 2.0.0 dev: true - /execa@7.1.1: - resolution: {integrity: sha512-wH0eMf/UXckdUYnO21+HDztteVv05rq2GXksxT4fCGeHkBhw1DROXh40wcjMcRqDOWE7iPJ4n3M7e2+YFP+76Q==} - engines: {node: ^14.18.0 || ^16.14.0 || >=18.0.0} - dependencies: - cross-spawn: 7.0.3 - get-stream: 6.0.1 - human-signals: 4.3.1 - is-stream: 3.0.0 - merge-stream: 2.0.0 - npm-run-path: 5.1.0 - onetime: 6.0.0 - signal-exit: 3.0.7 - strip-final-newline: 3.0.0 - dev: true - /exit@0.1.2: resolution: {integrity: sha512-Zk/eNKV2zbjpKzrsQ+n1G6poVbErQxJ0LBOJXaKZ1EViLzH+hrLu9cdXI4zw9dBQJslwBEpbQ2P1oS7nDxs6jQ==} engines: {node: '>= 0.8.0'} @@ -3302,46 +3746,58 @@ packages: jest-util: 29.6.1 dev: true - /express-rate-limit@6.9.0(express@4.17.2): - resolution: {integrity: sha512-AnISR3V8qy4gpKM62/TzYdoFO9NV84fBx0POXzTryHU/qGUJBWuVGd+JhbvtVmKBv37t8/afmqdnv16xWoQxag==} - engines: {node: '>= 14.0.0'} + /expect@29.7.0: + resolution: {integrity: sha512-2Zks0hf1VLFYI1kbh0I5jP3KHHyCHpkfyHBzsSXRFgl/Bg9mWYfMW8oD+PdMPlEwy5HNsR9JutYy6pMeOh61nw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + '@jest/expect-utils': 29.7.0 + jest-get-type: 29.6.3 + jest-matcher-utils: 29.7.0 + jest-message-util: 29.7.0 + jest-util: 29.7.0 + dev: true + + /express-rate-limit@6.11.2(express@4.18.2): + resolution: {integrity: sha512-a7uwwfNTh1U60ssiIkuLFWHt4hAC5yxlLGU2VP0X4YNlyEDZAqF4tK3GD3NSitVBrCQmQ0++0uOyFOgC2y4DDw==} + engines: {node: '>= 14'} peerDependencies: express: ^4 || ^5 dependencies: - express: 4.17.2 + express: 4.18.2 dev: false - /express@4.17.2: - resolution: {integrity: sha512-oxlxJxcQlYwqPWKVJJtvQiwHgosH/LrLSPA+H4UxpyvSS6jC5aH+5MoHFM+KABgTOt0APue4w66Ha8jCUo9QGg==} + /express@4.18.2: + resolution: {integrity: sha512-5/PsL6iGPdfQ/lKM1UuielYgv3BUoJfz1aUwU9vHZ+J7gyvwdQXFEBIEIaxeGf0GIcreATNyBExtalisDbuMqQ==} engines: {node: '>= 0.10.0'} dependencies: accepts: 1.3.8 array-flatten: 1.1.1 - body-parser: 1.19.1 + body-parser: 1.20.1 content-disposition: 0.5.4 content-type: 1.0.5 - cookie: 0.4.1 + cookie: 0.5.0 cookie-signature: 1.0.6 debug: 2.6.9 - depd: 1.1.2 + depd: 2.0.0 encodeurl: 1.0.2 escape-html: 1.0.3 etag: 1.8.1 - finalhandler: 1.1.2 + 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.3.0 + on-finished: 2.4.1 parseurl: 1.3.3 path-to-regexp: 0.1.7 proxy-addr: 2.0.7 - qs: 6.9.6 + qs: 6.11.0 range-parser: 1.2.1 safe-buffer: 5.2.1 - send: 0.17.2 - serve-static: 1.14.2 + send: 0.18.0 + serve-static: 1.15.0 setprototypeof: 1.2.0 - statuses: 1.5.0 + statuses: 2.0.1 type-is: 1.6.18 utils-merge: 1.0.1 vary: 1.1.2 @@ -3361,8 +3817,8 @@ packages: /fast-deep-equal@3.1.3: resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} - /fast-glob@3.3.0: - resolution: {integrity: sha512-ChDuvbOypPuNjO8yIDf36x7BlZX1smcUMTTcyoIjycexOxd6DFsKsg21qVBzEmr3G7fUKIRy2/psii+CIUt7FA==} + /fast-glob@3.3.1: + resolution: {integrity: sha512-kNFPyjhh5cKjrUltxs+wFx+ZkbRaxxmZ+X0ZU31SOsxCEtP9VPgtq2teZw1DebupL5GmDaNQ6yKMMVcM41iqDg==} engines: {node: '>=8.6.0'} dependencies: '@nodelib/fs.stat': 2.0.5 @@ -3439,16 +3895,16 @@ packages: to-regex-range: 5.0.1 dev: true - /finalhandler@1.1.2: - resolution: {integrity: sha512-aAWcW57uxVNrQZqFXjITpW3sIUQmHGG3qSb9mUah9MgMC4NeWhNOlNjXEYq3HjRAvL6arUviZGGJsBg6z0zsWA==} + /finalhandler@1.2.0: + resolution: {integrity: sha512-5uXcUVftlQMFnWC9qu/svkWv3GTd2PfUhK/3PLkYNAe7FbqJMt3515HaxE6eRL74GdsriiwujiawdaB1BpEISg==} engines: {node: '>= 0.8'} dependencies: debug: 2.6.9 encodeurl: 1.0.2 escape-html: 1.0.3 - on-finished: 2.3.0 + on-finished: 2.4.1 parseurl: 1.3.3 - statuses: 1.5.0 + statuses: 2.0.1 unpipe: 1.0.0 transitivePeerDependencies: - supports-color @@ -3540,6 +3996,12 @@ packages: universalify: 2.0.0 dev: true + /fs-minipass@1.2.7: + resolution: {integrity: sha512-GWSSJGFy4e9GUeCcbIkED+bgAoFyj7XF1mV8rma3QW4NIqX9Kyx79N/PF61H5udOV3aY1IaMLs6pGbH71nlCTA==} + dependencies: + minipass: 2.9.0 + dev: false + /fs-minipass@2.1.0: resolution: {integrity: sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==} engines: {node: '>= 8'} @@ -3559,6 +4021,10 @@ packages: /function-bind@1.1.1: resolution: {integrity: sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==} + /function-bind@1.1.2: + resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} + dev: true + /function.prototype.name@1.1.5: resolution: {integrity: sha512-uN7m/BzVKQnCUF/iW8jYea67v++2u7m5UgENbHRtdDVclOUP+FMPlCNdmk0h/ysGyo2tavMJEDqJAkJdRa1vMA==} engines: {node: '>= 0.4'} @@ -3569,10 +4035,33 @@ packages: functions-have-names: 1.2.3 dev: true + /function.prototype.name@1.1.6: + resolution: {integrity: sha512-Z5kx79swU5P27WEayXM1tBi5Ze/lbIyiNgU3qyXUOf9b2rgXYyF9Dy9Cx+IQv/Lc8WCG6L82zwUPpSS9hGehIg==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.5 + define-properties: 1.2.0 + es-abstract: 1.22.3 + functions-have-names: 1.2.3 + dev: true + /functions-have-names@1.2.3: resolution: {integrity: sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==} dev: true + /gauge@2.7.4: + resolution: {integrity: sha512-14x4kjc6lkD3ltw589k0NrPD6cCNTD6CWoVUNpB85+DrtONoZn+Rug6xZU5RvSC4+TZPxA5AnBibQYAvZn41Hg==} + dependencies: + aproba: 1.2.0 + console-control-strings: 1.1.0 + has-unicode: 2.0.1 + object-assign: 4.1.1 + signal-exit: 3.0.7 + string-width: 1.0.2 + strip-ansi: 3.0.1 + wide-align: 1.1.5 + dev: false + /gauge@3.0.2: resolution: {integrity: sha512-+5J6MS/5XksCuXq++uFRsnUd7Ovu1XenbeuIuNRJxYWjgQbPuFhT14lAvsWfqfAmnwluf1OwMjz39HjfLPci0Q==} engines: {node: '>=10'} @@ -3625,6 +4114,15 @@ packages: has-proto: 1.0.1 has-symbols: 1.0.3 + /get-intrinsic@1.2.2: + resolution: {integrity: sha512-0gSo4ml/0j98Y3lngkFEot/zhiCeWsbYIlZ+uZOVgzLyLaUw7wxUL+nCTP0XJvJg1AXulJRI3UJi8GsbDuxdGA==} + dependencies: + function-bind: 1.1.2 + has-proto: 1.0.1 + has-symbols: 1.0.3 + hasown: 2.0.0 + dev: true + /get-package-type@0.1.0: resolution: {integrity: sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q==} engines: {node: '>=8.0.0'} @@ -3684,6 +4182,17 @@ packages: once: 1.4.0 path-is-absolute: 1.0.1 + /glob@8.1.0: + resolution: {integrity: sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ==} + engines: {node: '>=12'} + dependencies: + fs.realpath: 1.0.0 + inflight: 1.0.6 + inherits: 2.0.4 + minimatch: 5.1.6 + once: 1.4.0 + dev: false + /globals@11.12.0: resolution: {integrity: sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==} engines: {node: '>=4'} @@ -3708,23 +4217,12 @@ packages: dependencies: array-union: 2.1.0 dir-glob: 3.0.1 - fast-glob: 3.3.0 + fast-glob: 3.3.1 ignore: 5.2.4 merge2: 1.4.1 slash: 3.0.0 dev: true - /globby@13.2.2: - resolution: {integrity: sha512-Y1zNGV+pzQdh7H39l9zgB4PJqjRNqydvdYCDG4HFXM4XuvSaQQlEc91IU1yALL8gUTDomgBAfz3XJdmUS+oo0w==} - engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} - dependencies: - dir-glob: 3.0.1 - fast-glob: 3.3.0 - ignore: 5.2.4 - merge2: 1.4.1 - slash: 4.0.0 - dev: true - /globrex@0.1.2: resolution: {integrity: sha512-uHJgbwAMwNFf5mLst7IWLNg14x1CkeqglJb/K3doi4dw6q2IvAAmM/Y81kevy83wP+Sst+nutFTYOGg3d1lsxg==} dev: false @@ -3767,7 +4265,6 @@ packages: /has-flag@3.0.0: resolution: {integrity: sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==} engines: {node: '>=4'} - dev: true /has-flag@4.0.0: resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} @@ -3802,6 +4299,13 @@ packages: dependencies: function-bind: 1.1.1 + /hasown@2.0.0: + resolution: {integrity: sha512-vUptKVTpIJhcczKBbgnS+RtcuYMB8+oNzPK2/Hp3hanz8JmpATdmmgLgSaadVREkDm+e2giHwY3ZRkyjSIDDFA==} + engines: {node: '>= 0.4'} + dependencies: + function-bind: 1.1.2 + dev: true + /header-case@2.0.4: resolution: {integrity: sha512-H/vuk5TEEVZwrR0lp2zed9OCo1uAILMlx0JEMgC26rzyJJ3N1v6XkwHHXJQdR2doSjcGPM6OKPYoJgf0plJ11Q==} dependencies: @@ -3822,17 +4326,6 @@ packages: dev: true optional: true - /http-errors@1.8.1: - resolution: {integrity: sha512-Kpk9Sm7NmI+RHhnj6OIWDI1d6fIoFAtFt9RLaTMRlg/8w49juAStsrBgp0Dp4OdxdVbRIeKhtCUvoi/RuAhO4g==} - engines: {node: '>= 0.6'} - dependencies: - depd: 1.1.2 - inherits: 2.0.4 - setprototypeof: 1.2.0 - statuses: 1.5.0 - toidentifier: 1.0.1 - dev: false - /http-errors@2.0.0: resolution: {integrity: sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==} engines: {node: '>= 0.8'} @@ -3857,8 +4350,8 @@ packages: dev: true optional: true - /http-status@1.5.0: - resolution: {integrity: sha512-wcGvY31MpFNHIkUcXHHnvrE4IKYlpvitJw5P/1u892gMBAM46muQ+RH7UN1d+Ntnfx5apnOnVY6vcLmrWHOLwg==} + /http-status@1.7.3: + resolution: {integrity: sha512-GS8tL1qHT2nBCMJDYMHGkkkKQLNkIAHz37vgO68XKvzv+XyqB4oh/DfmMHdtRzfqSJPj1xKG2TaELZtlCz6BEQ==} engines: {node: '>= 0.4.0'} dev: false @@ -3886,11 +4379,6 @@ packages: engines: {node: '>=10.17.0'} dev: true - /human-signals@4.3.1: - resolution: {integrity: sha512-nZXjEF2nbo7lIw3mgYjItAfgQXog3OjJogSbKa2CQIIvSGWcKgeJnQlNXip6NglNzYH45nSRiEVimMvYL8DDqQ==} - engines: {node: '>=14.18.0'} - dev: true - /humanize-ms@1.2.1: resolution: {integrity: sha512-Fl70vYtsAFb/C06PTS9dZBo7ihau+Tu/DNCk/OyHhea07S+aeMWpFFkUaXRa8fI+ScZbEI8dfSxwY7gxZ9SAVQ==} requiresBuild: true @@ -3925,6 +4413,12 @@ packages: resolution: {integrity: sha512-Ius2VYcGNk7T90CppJqcIkS5ooHUZyIQK+ClZfMfMNFEF9VSE73Fq+906u/CWu92x4gzZMWOwfFYckPObzdEbA==} dev: true + /ignore-walk@3.0.4: + resolution: {integrity: sha512-PY6Ii8o1jMRA1z4F2hRkH/xN59ox43DavKvD3oDpfurRlOJyAHpifIwpbdv1n4jt4ov0jSpw3kQ4GhJnpBL6WQ==} + dependencies: + minimatch: 3.1.2 + dev: false + /ignore@5.2.4: resolution: {integrity: sha512-MAb38BcSbH0eHNBxn7ql2NH/kX33OkB3lZ1BNdh7ENeRChHTYsTvWrMubiIAMNS2llXEEgZ1MUOBtXChP3kaFQ==} engines: {node: '>= 4'} @@ -3941,6 +4435,7 @@ packages: /import-local@3.1.0: resolution: {integrity: sha512-ASB07uLtnDs1o6EHjKpX34BKYDSqnFerfTOJL2HvMqF70LnxpjkzDB8J44oT9pu4AMPkQwf8jl6szgvNd2tRIg==} engines: {node: '>=8'} + hasBin: true dependencies: pkg-dir: 4.2.0 resolve-cwd: 3.0.0 @@ -3973,6 +4468,10 @@ packages: /inherits@2.0.4: resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} + /ini@1.3.8: + resolution: {integrity: sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==} + dev: false + /inquirer@7.3.3: resolution: {integrity: sha512-JG3eIAj5V9CwcGvuOmoo6LB9kbAYT8HXffUl6memuszlwDC/qvFAJw49XJ5NROSFNPxp3iQg1GqkFhaY/CR0IA==} engines: {node: '>=8.0.0'} @@ -4060,6 +4559,12 @@ packages: has: 1.0.3 dev: true + /is-core-module@2.13.1: + resolution: {integrity: sha512-hHrIjvZsftOsvKSn2TRYl63zvxsgE0K+0mYMoH6gD4omR5IWB2KynivBQczo3+wF1cCkjzvptnI9Q0sPU66ilw==} + dependencies: + hasown: 2.0.0 + dev: true + /is-date-object@1.0.5: resolution: {integrity: sha512-9YQaSxsAiSwcvS33MBk3wTCVnWK+HhF8VZR2jRxehM16QcVOdHqPn4VPHmRK4lSr38n9JriurInLcP90xsYNfQ==} engines: {node: '>= 0.4'} @@ -4067,16 +4572,6 @@ packages: has-tostringtag: 1.0.0 dev: true - /is-docker@2.2.1: - resolution: {integrity: sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==} - engines: {node: '>=8'} - dev: true - - /is-docker@3.0.0: - resolution: {integrity: sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ==} - engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} - dev: true - /is-electron@2.2.0: resolution: {integrity: sha512-SpMppC2XR3YdxSzczXReBjqs2zGscWQpBIKqwXYBFic0ERaxNVgwLCHwOLZeESfdJQjX0RDvrJ1lBXX2ij+G1Q==} dev: false @@ -4086,6 +4581,13 @@ packages: engines: {node: '>=0.10.0'} dev: true + /is-fullwidth-code-point@1.0.0: + resolution: {integrity: sha512-1pqUqRjkhPJ9miNq9SwMfdvi6lBJcd6eFxvfaivQhaH3SgisfiuudvFntdKOmxuee/77l+FPjKrQjWvmPjWrRw==} + engines: {node: '>=0.10.0'} + dependencies: + number-is-nan: 1.0.1 + dev: false + /is-fullwidth-code-point@3.0.0: resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==} engines: {node: '>=8'} @@ -4102,27 +4604,12 @@ packages: is-extglob: 2.1.1 dev: true - /is-inside-container@1.0.0: - resolution: {integrity: sha512-KIYLCCJghfHZxqjYBE7rEy0OBuTd5xCHS7tHVgvCLkx7StIoaxwNW3hCALgEUjFfeRk+MG/Qxmp/vtETEF3tRA==} - engines: {node: '>=14.16'} - dependencies: - is-docker: 3.0.0 - dev: true - /is-lambda@1.0.1: resolution: {integrity: sha512-z7CMFGNrENq5iFB9Bqo64Xk6Y9sg+epq1myIcdHaGnbMTYOxvzsEtdYqQUylB7LxfkvgrrjP32T6Ywciio9UIQ==} requiresBuild: true dev: true optional: true - /is-nan@1.3.2: - resolution: {integrity: sha512-E+zBKpQ2t6MEo1VsonYmluk9NxGrbzpeeLC2xIViuO2EjU2xsXsBPwTr3Ykv9l08UYEVEdWeRZNouaZqF6RN0w==} - engines: {node: '>= 0.4'} - dependencies: - call-bind: 1.0.2 - define-properties: 1.2.0 - dev: false - /is-negative-zero@2.0.2: resolution: {integrity: sha512-dqJvarLawXsFbNDeJW7zAz8ItJ9cd28YufuuFzh0G8pNHjJMnY08Dv7sYX2uF5UpQOwieAeOExEYAWWfu7ZZUA==} engines: {node: '>= 0.4'} @@ -4171,11 +4658,6 @@ packages: resolution: {integrity: sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==} engines: {node: '>=8'} - /is-stream@3.0.0: - resolution: {integrity: sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA==} - engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} - dev: true - /is-string@1.0.7: resolution: {integrity: sha512-tE2UXzivje6ofPW7l23cjDOMa09gb7xlAqG6jG5ej6uPV32TlWP3NKPigtaGeHNu9fohccRYvIiZMfOOnOYUtg==} engines: {node: '>= 0.4'} @@ -4201,17 +4683,25 @@ packages: has-tostringtag: 1.0.0 dev: true + /is-typed-array@1.1.12: + resolution: {integrity: sha512-Z14TF2JNG8Lss5/HMqt0//T9JeHXttXy5pH/DBU4vi98ozO2btxzq9MwYDZYnKwU8nRsz/+GVFVRDq3DkVuSPg==} + engines: {node: '>= 0.4'} + dependencies: + which-typed-array: 1.1.13 + dev: true + /is-weakref@1.0.2: resolution: {integrity: sha512-qctsuLZmIQ0+vSSMfoVvyFe2+GSEvnmZ2ezTup1SBse9+twCCeial6EEi3Nc2KFcf6+qz2FBPnjXsk8xhKSaPQ==} dependencies: call-bind: 1.0.2 dev: true - /is-wsl@2.2.0: - resolution: {integrity: sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==} - engines: {node: '>=8'} - dependencies: - is-docker: 2.2.1 + /isarray@1.0.0: + resolution: {integrity: sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==} + dev: false + + /isarray@2.0.5: + resolution: {integrity: sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==} dev: true /isexe@2.0.0: @@ -4231,7 +4721,20 @@ packages: '@babel/parser': 7.22.7 '@istanbuljs/schema': 0.1.3 istanbul-lib-coverage: 3.2.0 - semver: 6.3.0 + semver: 6.3.1 + transitivePeerDependencies: + - supports-color + dev: true + + /istanbul-lib-instrument@6.0.1: + resolution: {integrity: sha512-EAMEJBsYuyyztxMxW3g7ugGPkrZsV57v0Hmv3mm1uQsmB+QnZuepg731CRaIgeUVSdmsTngOkSnauNF8p7FIhA==} + engines: {node: '>=10'} + dependencies: + '@babel/core': 7.22.8 + '@babel/parser': 7.22.7 + '@istanbuljs/schema': 0.1.3 + istanbul-lib-coverage: 3.2.0 + semver: 7.5.4 transitivePeerDependencies: - supports-color dev: true @@ -4264,44 +4767,46 @@ packages: istanbul-lib-report: 3.0.0 dev: true - /jest-changed-files@29.5.0: - resolution: {integrity: sha512-IFG34IUMUaNBIxjQXF/iu7g6EcdMrGRRxaUSw92I/2g2YC6vCdTltl4nHvt7Ci5nSJwXIkCu8Ka1DKF+X7Z1Ag==} + /jest-changed-files@29.7.0: + resolution: {integrity: sha512-fEArFiwf1BpQ+4bXSprcDc3/x4HSzL4al2tozwVpDFpsxALjLYdyiIK4e5Vz66GQJIbXJ82+35PtysofptNX2w==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} dependencies: execa: 5.1.1 + jest-util: 29.7.0 p-limit: 3.1.0 dev: true - /jest-circus@29.6.1: - resolution: {integrity: sha512-tPbYLEiBU4MYAL2XoZme/bgfUeotpDBd81lgHLCbDZZFaGmECk0b+/xejPFtmiBP87GgP/y4jplcRpbH+fgCzQ==} + /jest-circus@29.7.0: + resolution: {integrity: sha512-3E1nCMgipcTkCocFwM90XXQab9bS+GMsjdpmPrlelaxwD93Ad8iVEjX/vvHPdLPnFf+L40u+5+iutRdA1N9myw==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} dependencies: - '@jest/environment': 29.6.1 - '@jest/expect': 29.6.1 - '@jest/test-result': 29.6.1 - '@jest/types': 29.6.1 + '@jest/environment': 29.7.0 + '@jest/expect': 29.7.0 + '@jest/test-result': 29.7.0 + '@jest/types': 29.6.3 '@types/node': 18.16.1 chalk: 4.1.2 co: 4.6.0 - dedent: 0.7.0 + dedent: 1.5.1 is-generator-fn: 2.1.0 - jest-each: 29.6.1 - jest-matcher-utils: 29.6.1 - jest-message-util: 29.6.1 - jest-runtime: 29.6.1 - jest-snapshot: 29.6.1 - jest-util: 29.6.1 + jest-each: 29.7.0 + jest-matcher-utils: 29.7.0 + jest-message-util: 29.7.0 + jest-runtime: 29.7.0 + jest-snapshot: 29.7.0 + jest-util: 29.7.0 p-limit: 3.1.0 - pretty-format: 29.6.1 + pretty-format: 29.7.0 pure-rand: 6.0.2 slash: 3.0.0 stack-utils: 2.0.6 transitivePeerDependencies: + - babel-plugin-macros - supports-color dev: true - /jest-cli@29.6.1(@types/node@18.16.1): - resolution: {integrity: sha512-607dSgTA4ODIN6go9w6xY3EYkyPFGicx51a69H7yfvt7lN53xNswEVLovq+E77VsTRi5fWprLH0yl4DJgE8Ing==} + /jest-cli@29.7.0(@types/node@18.16.1): + resolution: {integrity: sha512-OVVobw2IubN/GSYsxETi+gOe7Ka59EFMR/twOU3Jb2GnKKeMGJB5SGUUrEz3SFVmJASUdZUzy83sLNNQ2gZslg==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} hasBin: true peerDependencies: @@ -4310,26 +4815,26 @@ packages: node-notifier: optional: true dependencies: - '@jest/core': 29.6.1 - '@jest/test-result': 29.6.1 - '@jest/types': 29.6.1 + '@jest/core': 29.7.0 + '@jest/test-result': 29.7.0 + '@jest/types': 29.6.3 chalk: 4.1.2 + create-jest: 29.7.0(@types/node@18.16.1) exit: 0.1.2 - graceful-fs: 4.2.11 import-local: 3.1.0 - jest-config: 29.6.1(@types/node@18.16.1) - jest-util: 29.6.1 - jest-validate: 29.6.1 - prompts: 2.4.2 + jest-config: 29.7.0(@types/node@18.16.1) + jest-util: 29.7.0 + jest-validate: 29.7.0 yargs: 17.7.2 transitivePeerDependencies: - '@types/node' + - babel-plugin-macros - supports-color - ts-node dev: true - /jest-config@29.6.1(@types/node@18.16.1): - resolution: {integrity: sha512-XdjYV2fy2xYixUiV2Wc54t3Z4oxYPAELUzWnV6+mcbq0rh742X2p52pii5A3oeRzYjLnQxCsZmp0qpI6klE2cQ==} + /jest-config@29.7.0(@types/node@18.16.1): + resolution: {integrity: sha512-uXbpfeQ7R6TZBqI3/TxCU4q4ttk3u0PJeC+E0zbfSoSjq6bJ7buBPxzQPL0ifrkY4DNu4JUdk0ImlBUYi840eQ==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} peerDependencies: '@types/node': '*' @@ -4341,29 +4846,30 @@ packages: optional: true dependencies: '@babel/core': 7.22.8 - '@jest/test-sequencer': 29.6.1 - '@jest/types': 29.6.1 + '@jest/test-sequencer': 29.7.0 + '@jest/types': 29.6.3 '@types/node': 18.16.1 - babel-jest: 29.6.1(@babel/core@7.22.8) + babel-jest: 29.7.0(@babel/core@7.22.8) chalk: 4.1.2 ci-info: 3.8.0 deepmerge: 4.3.1 glob: 7.2.3 graceful-fs: 4.2.11 - jest-circus: 29.6.1 - jest-environment-node: 29.6.1 - jest-get-type: 29.4.3 - jest-regex-util: 29.4.3 - jest-resolve: 29.6.1 - jest-runner: 29.6.1 - jest-util: 29.6.1 - jest-validate: 29.6.1 + jest-circus: 29.7.0 + jest-environment-node: 29.7.0 + jest-get-type: 29.6.3 + jest-regex-util: 29.6.3 + jest-resolve: 29.7.0 + jest-runner: 29.7.0 + jest-util: 29.7.0 + jest-validate: 29.7.0 micromatch: 4.0.5 parse-json: 5.2.0 - pretty-format: 29.6.1 + pretty-format: 29.7.0 slash: 3.0.0 strip-json-comments: 3.1.1 transitivePeerDependencies: + - babel-plugin-macros - supports-color dev: true @@ -4373,38 +4879,48 @@ packages: dependencies: chalk: 4.1.2 diff-sequences: 29.4.3 - jest-get-type: 29.4.3 - pretty-format: 29.6.1 + jest-get-type: 29.6.3 + pretty-format: 29.7.0 dev: true - /jest-docblock@29.4.3: - resolution: {integrity: sha512-fzdTftThczeSD9nZ3fzA/4KkHtnmllawWrXO69vtI+L9WjEIuXWs4AmyME7lN5hU7dB0sHhuPfcKofRsUb/2Fg==} + /jest-diff@29.7.0: + resolution: {integrity: sha512-LMIgiIrhigmPrs03JHpxUh2yISK3vLFPkAodPeo0+BuF7wA2FoQbkEg1u8gBYBThncu7e1oEDUfIXVuTqLRUjw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + chalk: 4.1.2 + diff-sequences: 29.6.3 + jest-get-type: 29.6.3 + pretty-format: 29.7.0 + dev: true + + /jest-docblock@29.7.0: + resolution: {integrity: sha512-q617Auw3A612guyaFgsbFeYpNP5t2aoUNLwBUbc/0kD1R4t9ixDbyFTHd1nok4epoVFpr7PmeWHrhvuV3XaJ4g==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} dependencies: detect-newline: 3.1.0 dev: true - /jest-each@29.6.1: - resolution: {integrity: sha512-n5eoj5eiTHpKQCAVcNTT7DRqeUmJ01hsAL0Q1SMiBHcBcvTKDELixQOGMCpqhbIuTcfC4kMfSnpmDqRgRJcLNQ==} + /jest-each@29.7.0: + resolution: {integrity: sha512-gns+Er14+ZrEoC5fhOfYCY1LOHHr0TI+rQUHZS8Ttw2l7gl+80eHc/gFf2Ktkw0+SIACDTeWvpFcv3B04VembQ==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} dependencies: - '@jest/types': 29.6.1 + '@jest/types': 29.6.3 chalk: 4.1.2 - jest-get-type: 29.4.3 - jest-util: 29.6.1 - pretty-format: 29.6.1 + jest-get-type: 29.6.3 + jest-util: 29.7.0 + pretty-format: 29.7.0 dev: true - /jest-environment-node@29.6.1: - resolution: {integrity: sha512-ZNIfAiE+foBog24W+2caIldl4Irh8Lx1PUhg/GZ0odM1d/h2qORAsejiFc7zb+SEmYPn1yDZzEDSU5PmDkmVLQ==} + /jest-environment-node@29.7.0: + resolution: {integrity: sha512-DOSwCRqXirTOyheM+4d5YZOrWcdu0LNZ87ewUoywbcb2XR4wKgqiG8vNeYwhjFMbEkfju7wx2GYH0P2gevGvFw==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} dependencies: - '@jest/environment': 29.6.1 - '@jest/fake-timers': 29.6.1 - '@jest/types': 29.6.1 + '@jest/environment': 29.7.0 + '@jest/fake-timers': 29.7.0 + '@jest/types': 29.6.3 '@types/node': 18.16.1 - jest-mock: 29.6.1 - jest-util: 29.6.1 + jest-mock: 29.7.0 + jest-util: 29.7.0 dev: true /jest-get-type@29.4.3: @@ -4412,31 +4928,36 @@ packages: engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} dev: true - /jest-haste-map@29.6.1: - resolution: {integrity: sha512-0m7f9PZXxOCk1gRACiVgX85knUKPKLPg4oRCjLoqIm9brTHXaorMA0JpmtmVkQiT8nmXyIVoZd/nnH1cfC33ig==} + /jest-get-type@29.6.3: + resolution: {integrity: sha512-zrteXnqYxfQh7l5FHyL38jL39di8H8rHoecLH3JNxH3BwOrBsNeabdap5e0I23lD4HHI8W5VFBZqG4Eaq5LNcw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dev: true + + /jest-haste-map@29.7.0: + resolution: {integrity: sha512-fP8u2pyfqx0K1rGn1R9pyE0/KTn+G7PxktWidOBTqFPLYX0b9ksaMFkhK5vrS3DVun09pckLdlx90QthlW7AmA==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} dependencies: - '@jest/types': 29.6.1 + '@jest/types': 29.6.3 '@types/graceful-fs': 4.1.6 '@types/node': 18.16.1 anymatch: 3.1.3 fb-watchman: 2.0.2 graceful-fs: 4.2.11 - jest-regex-util: 29.4.3 - jest-util: 29.6.1 - jest-worker: 29.6.1 + jest-regex-util: 29.6.3 + jest-util: 29.7.0 + jest-worker: 29.7.0 micromatch: 4.0.5 walker: 1.0.8 optionalDependencies: fsevents: 2.3.2 dev: true - /jest-leak-detector@29.6.1: - resolution: {integrity: sha512-OrxMNyZirpOEwkF3UHnIkAiZbtkBWiye+hhBweCHkVbCgyEy71Mwbb5zgeTNYWJBi1qgDVfPC1IwO9dVEeTLwQ==} + /jest-leak-detector@29.7.0: + resolution: {integrity: sha512-kYA8IJcSYtST2BY9I+SMC32nDpBT3J2NvWJx8+JCuCdl/CR1I4EKUJROiP8XtCcxqgTTBGJNdbB1A8XRKbTetw==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} dependencies: - jest-get-type: 29.4.3 - pretty-format: 29.6.1 + jest-get-type: 29.6.3 + pretty-format: 29.7.0 dev: true /jest-matcher-utils@29.6.1: @@ -4446,7 +4967,17 @@ packages: chalk: 4.1.2 jest-diff: 29.6.1 jest-get-type: 29.4.3 - pretty-format: 29.6.1 + pretty-format: 29.7.0 + dev: true + + /jest-matcher-utils@29.7.0: + resolution: {integrity: sha512-sBkD+Xi9DtcChsI3L3u0+N0opgPYnCRPtGcQYrgXmR+hmt/fYfWAL0xRXYU8eWOdfuLgBe0YCW3AFtnRLagq/g==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + chalk: 4.1.2 + jest-diff: 29.7.0 + jest-get-type: 29.6.3 + pretty-format: 29.7.0 dev: true /jest-message-util@29.6.1: @@ -4454,37 +4985,52 @@ packages: engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} dependencies: '@babel/code-frame': 7.22.5 - '@jest/types': 29.6.1 + '@jest/types': 29.6.3 '@types/stack-utils': 2.0.1 chalk: 4.1.2 graceful-fs: 4.2.11 micromatch: 4.0.5 - pretty-format: 29.6.1 + pretty-format: 29.7.0 slash: 3.0.0 stack-utils: 2.0.6 dev: true - /jest-mock-extended@3.0.4(jest@29.5.0)(typescript@5.1.6): - resolution: {integrity: sha512-2ynEZ7IEJNrhrgshklDMhrOdnmW4Nt+PhkyRqZxRgpwMo7JjmFWMzyp0+eSyk+H9KK1QjXI5xTZIw6x7cVDcRg==} + /jest-message-util@29.7.0: + resolution: {integrity: sha512-GBEV4GRADeP+qtB2+6u61stea8mGcOT4mCtrYISZwfu9/ISHFJ/5zOMXYbpBE9RsS5+Gb63DW4FgmnKJ79Kf6w==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + '@babel/code-frame': 7.22.5 + '@jest/types': 29.6.3 + '@types/stack-utils': 2.0.1 + chalk: 4.1.2 + graceful-fs: 4.2.11 + micromatch: 4.0.5 + pretty-format: 29.7.0 + slash: 3.0.0 + stack-utils: 2.0.6 + dev: true + + /jest-mock-extended@3.0.5(jest@29.7.0)(typescript@5.1.6): + resolution: {integrity: sha512-/eHdaNPUAXe7f65gHH5urc8SbRVWjYxBqmCgax2uqOBJy8UUcCBMN1upj1eZ8y/i+IqpyEm4Kq0VKss/GCCTdw==} peerDependencies: jest: ^24.0.0 || ^25.0.0 || ^26.0.0 || ^27.0.0 || ^28.0.0 || ^29.0.0 typescript: ^3.0.0 || ^4.0.0 || ^5.0.0 dependencies: - jest: 29.5.0(@types/node@18.16.1) + jest: 29.7.0(@types/node@18.16.1) ts-essentials: 7.0.3(typescript@5.1.6) typescript: 5.1.6 dev: true - /jest-mock@29.6.1: - resolution: {integrity: sha512-brovyV9HBkjXAEdRooaTQK42n8usKoSRR3gihzUpYeV/vwqgSoNfrksO7UfSACnPmxasO/8TmHM3w9Hp3G1dgw==} + /jest-mock@29.7.0: + resolution: {integrity: sha512-ITOMZn+UkYS4ZFh83xYAOzWStloNzJFO2s8DWrE4lhtGD+AorgnbkiKERe4wQVBydIGPx059g6riW5Btp6Llnw==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} dependencies: - '@jest/types': 29.6.1 + '@jest/types': 29.6.3 '@types/node': 18.16.1 - jest-util: 29.6.1 + jest-util: 29.7.0 dev: true - /jest-pnp-resolver@1.2.3(jest-resolve@29.6.1): + /jest-pnp-resolver@1.2.3(jest-resolve@29.7.0): resolution: {integrity: sha512-+3NpwQEnRoIBtx4fyhblQDPgJI0H1IEIkX7ShLUjPGA7TtUTvI1oiKi3SR4oBR0hQhQR80l4WAe5RrXBwWMA8w==} engines: {node: '>=6'} peerDependencies: @@ -4493,100 +5039,100 @@ packages: jest-resolve: optional: true dependencies: - jest-resolve: 29.6.1 + jest-resolve: 29.7.0 dev: true - /jest-regex-util@29.4.3: - resolution: {integrity: sha512-O4FglZaMmWXbGHSQInfXewIsd1LMn9p3ZXB/6r4FOkyhX2/iP/soMG98jGvk/A3HAN78+5VWcBGO0BJAPRh4kg==} + /jest-regex-util@29.6.3: + resolution: {integrity: sha512-KJJBsRCyyLNWCNBOvZyRDnAIfUiRJ8v+hOBQYGn8gDyF3UegwiP4gwRR3/SDa42g1YbVycTidUF3rKjyLFDWbg==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} dev: true - /jest-resolve-dependencies@29.6.1: - resolution: {integrity: sha512-BbFvxLXtcldaFOhNMXmHRWx1nXQO5LoXiKSGQcA1LxxirYceZT6ch8KTE1bK3X31TNG/JbkI7OkS/ABexVahiw==} + /jest-resolve-dependencies@29.7.0: + resolution: {integrity: sha512-un0zD/6qxJ+S0et7WxeI3H5XSe9lTBBR7bOHCHXkKR6luG5mwDDlIzVQ0V5cZCuoTgEdcdwzTghYkTWfubi+nA==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} dependencies: - jest-regex-util: 29.4.3 - jest-snapshot: 29.6.1 + jest-regex-util: 29.6.3 + jest-snapshot: 29.7.0 transitivePeerDependencies: - supports-color dev: true - /jest-resolve@29.6.1: - resolution: {integrity: sha512-AeRkyS8g37UyJiP9w3mmI/VXU/q8l/IH52vj/cDAyScDcemRbSBhfX/NMYIGilQgSVwsjxrCHf3XJu4f+lxCMg==} + /jest-resolve@29.7.0: + resolution: {integrity: sha512-IOVhZSrg+UvVAshDSDtHyFCCBUl/Q3AAJv8iZ6ZjnZ74xzvwuzLXid9IIIPgTnY62SJjfuupMKZsZQRsCvxEgA==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} dependencies: chalk: 4.1.2 graceful-fs: 4.2.11 - jest-haste-map: 29.6.1 - jest-pnp-resolver: 1.2.3(jest-resolve@29.6.1) - jest-util: 29.6.1 - jest-validate: 29.6.1 - resolve: 1.22.2 + jest-haste-map: 29.7.0 + jest-pnp-resolver: 1.2.3(jest-resolve@29.7.0) + jest-util: 29.7.0 + jest-validate: 29.7.0 + resolve: 1.22.8 resolve.exports: 2.0.2 slash: 3.0.0 dev: true - /jest-runner@29.6.1: - resolution: {integrity: sha512-tw0wb2Q9yhjAQ2w8rHRDxteryyIck7gIzQE4Reu3JuOBpGp96xWgF0nY8MDdejzrLCZKDcp8JlZrBN/EtkQvPQ==} + /jest-runner@29.7.0: + resolution: {integrity: sha512-fsc4N6cPCAahybGBfTRcq5wFR6fpLznMg47sY5aDpsoejOcVYFb07AHuSnR0liMcPTgBsA3ZJL6kFOjPdoNipQ==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} dependencies: - '@jest/console': 29.6.1 - '@jest/environment': 29.6.1 - '@jest/test-result': 29.6.1 - '@jest/transform': 29.6.1 - '@jest/types': 29.6.1 + '@jest/console': 29.7.0 + '@jest/environment': 29.7.0 + '@jest/test-result': 29.7.0 + '@jest/transform': 29.7.0 + '@jest/types': 29.6.3 '@types/node': 18.16.1 chalk: 4.1.2 emittery: 0.13.1 graceful-fs: 4.2.11 - jest-docblock: 29.4.3 - jest-environment-node: 29.6.1 - jest-haste-map: 29.6.1 - jest-leak-detector: 29.6.1 - jest-message-util: 29.6.1 - jest-resolve: 29.6.1 - jest-runtime: 29.6.1 - jest-util: 29.6.1 - jest-watcher: 29.6.1 - jest-worker: 29.6.1 + jest-docblock: 29.7.0 + jest-environment-node: 29.7.0 + jest-haste-map: 29.7.0 + jest-leak-detector: 29.7.0 + jest-message-util: 29.7.0 + jest-resolve: 29.7.0 + jest-runtime: 29.7.0 + jest-util: 29.7.0 + jest-watcher: 29.7.0 + jest-worker: 29.7.0 p-limit: 3.1.0 source-map-support: 0.5.13 transitivePeerDependencies: - supports-color dev: true - /jest-runtime@29.6.1: - resolution: {integrity: sha512-D6/AYOA+Lhs5e5il8+5pSLemjtJezUr+8zx+Sn8xlmOux3XOqx4d8l/2udBea8CRPqqrzhsKUsN/gBDE/IcaPQ==} + /jest-runtime@29.7.0: + resolution: {integrity: sha512-gUnLjgwdGqW7B4LvOIkbKs9WGbn+QLqRQQ9juC6HndeDiezIwhDP+mhMwHWCEcfQ5RUXa6OPnFF8BJh5xegwwQ==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} dependencies: - '@jest/environment': 29.6.1 - '@jest/fake-timers': 29.6.1 - '@jest/globals': 29.6.1 - '@jest/source-map': 29.6.0 - '@jest/test-result': 29.6.1 - '@jest/transform': 29.6.1 - '@jest/types': 29.6.1 + '@jest/environment': 29.7.0 + '@jest/fake-timers': 29.7.0 + '@jest/globals': 29.7.0 + '@jest/source-map': 29.6.3 + '@jest/test-result': 29.7.0 + '@jest/transform': 29.7.0 + '@jest/types': 29.6.3 '@types/node': 18.16.1 chalk: 4.1.2 cjs-module-lexer: 1.2.3 collect-v8-coverage: 1.0.2 glob: 7.2.3 graceful-fs: 4.2.11 - jest-haste-map: 29.6.1 - jest-message-util: 29.6.1 - jest-mock: 29.6.1 - jest-regex-util: 29.4.3 - jest-resolve: 29.6.1 - jest-snapshot: 29.6.1 - jest-util: 29.6.1 + jest-haste-map: 29.7.0 + jest-message-util: 29.7.0 + jest-mock: 29.7.0 + jest-regex-util: 29.6.3 + jest-resolve: 29.7.0 + jest-snapshot: 29.7.0 + jest-util: 29.7.0 slash: 3.0.0 strip-bom: 4.0.0 transitivePeerDependencies: - supports-color dev: true - /jest-snapshot@29.6.1: - resolution: {integrity: sha512-G4UQE1QQ6OaCgfY+A0uR1W2AY0tGXUPQpoUClhWHq1Xdnx1H6JOrC2nH5lqnOEqaDgbHFgIwZ7bNq24HpB180A==} + /jest-snapshot@29.7.0: + resolution: {integrity: sha512-Rm0BMWtxBcioHr1/OX5YCP8Uov4riHvKPknOGs804Zg9JGZgmIBkbtlxJC/7Z4msKYVbIJtfU+tKb8xlYNfdkw==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} dependencies: '@babel/core': 7.22.8 @@ -4594,21 +5140,20 @@ packages: '@babel/plugin-syntax-jsx': 7.22.5(@babel/core@7.22.8) '@babel/plugin-syntax-typescript': 7.22.5(@babel/core@7.22.8) '@babel/types': 7.22.5 - '@jest/expect-utils': 29.6.1 - '@jest/transform': 29.6.1 - '@jest/types': 29.6.1 - '@types/prettier': 2.7.3 + '@jest/expect-utils': 29.7.0 + '@jest/transform': 29.7.0 + '@jest/types': 29.6.3 babel-preset-current-node-syntax: 1.0.1(@babel/core@7.22.8) chalk: 4.1.2 - expect: 29.6.1 + expect: 29.7.0 graceful-fs: 4.2.11 - jest-diff: 29.6.1 - jest-get-type: 29.4.3 - jest-matcher-utils: 29.6.1 - jest-message-util: 29.6.1 - jest-util: 29.6.1 + jest-diff: 29.7.0 + jest-get-type: 29.6.3 + jest-matcher-utils: 29.7.0 + jest-message-util: 29.7.0 + jest-util: 29.7.0 natural-compare: 1.4.0 - pretty-format: 29.6.1 + pretty-format: 29.7.0 semver: 7.5.4 transitivePeerDependencies: - supports-color @@ -4618,7 +5163,7 @@ packages: resolution: {integrity: sha512-NRFCcjc+/uO3ijUVyNOQJluf8PtGCe/W6cix36+M3cTFgiYqFOOW5MgN4JOOcvbUhcKTYVd1CvHz/LWi8d16Mg==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} dependencies: - '@jest/types': 29.6.1 + '@jest/types': 29.6.3 '@types/node': 18.16.1 chalk: 4.1.2 ci-info: 3.8.0 @@ -4626,44 +5171,56 @@ packages: picomatch: 2.3.1 dev: true - /jest-validate@29.6.1: - resolution: {integrity: sha512-r3Ds69/0KCN4vx4sYAbGL1EVpZ7MSS0vLmd3gV78O+NAx3PDQQukRU5hNHPXlyqCgFY8XUk7EuTMLugh0KzahA==} + /jest-util@29.7.0: + resolution: {integrity: sha512-z6EbKajIpqGKU56y5KBUgy1dt1ihhQJgWzUlZHArA/+X2ad7Cb5iF+AK1EWVL/Bo7Rz9uurpqw6SiBCefUbCGA==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} dependencies: - '@jest/types': 29.6.1 + '@jest/types': 29.6.3 + '@types/node': 18.16.1 + chalk: 4.1.2 + ci-info: 3.8.0 + graceful-fs: 4.2.11 + picomatch: 2.3.1 + dev: true + + /jest-validate@29.7.0: + resolution: {integrity: sha512-ZB7wHqaRGVw/9hST/OuFUReG7M8vKeq0/J2egIGLdvjHCmYqGARhzXmtgi+gVeZ5uXFF219aOc3Ls2yLg27tkw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + '@jest/types': 29.6.3 camelcase: 6.3.0 chalk: 4.1.2 - jest-get-type: 29.4.3 + jest-get-type: 29.6.3 leven: 3.1.0 - pretty-format: 29.6.1 + pretty-format: 29.7.0 dev: true - /jest-watcher@29.6.1: - resolution: {integrity: sha512-d4wpjWTS7HEZPaaj8m36QiaP856JthRZkrgcIY/7ISoUWPIillrXM23WPboZVLbiwZBt4/qn2Jke84Sla6JhFA==} + /jest-watcher@29.7.0: + resolution: {integrity: sha512-49Fg7WXkU3Vl2h6LbLtMQ/HyB6rXSIX7SqvBLQmssRBGN9I0PNvPmAmCWSOY6SOvrjhI/F7/bGAv9RtnsPA03g==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} dependencies: - '@jest/test-result': 29.6.1 - '@jest/types': 29.6.1 + '@jest/test-result': 29.7.0 + '@jest/types': 29.6.3 '@types/node': 18.16.1 ansi-escapes: 4.3.2 chalk: 4.1.2 emittery: 0.13.1 - jest-util: 29.6.1 + jest-util: 29.7.0 string-length: 4.0.2 dev: true - /jest-worker@29.6.1: - resolution: {integrity: sha512-U+Wrbca7S8ZAxAe9L6nb6g8kPdia5hj32Puu5iOqBCMTMWFHXuK6dOV2IFrpedbTV8fjMFLdWNttQTBL6u2MRA==} + /jest-worker@29.7.0: + resolution: {integrity: sha512-eIz2msL/EzL9UFTFFx7jBTkeZfku0yUAyZZZmJ93H2TYEiroIx2PQjEXcwYtYl8zXCxb+PAmA2hLIt/6ZEkPHw==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} dependencies: '@types/node': 18.16.1 - jest-util: 29.6.1 + jest-util: 29.7.0 merge-stream: 2.0.0 supports-color: 8.1.1 dev: true - /jest@29.5.0(@types/node@18.16.1): - resolution: {integrity: sha512-juMg3he2uru1QoXX078zTa7pO85QyB9xajZc6bU+d9yEGwrKX6+vGmJQ3UdVZsvTEUARIdObzH68QItim6OSSQ==} + /jest@29.7.0(@types/node@18.16.1): + resolution: {integrity: sha512-NIy3oAFp9shda19hy4HK0HRTWKtPJmGdnvywu01nOqNC2vZg+Z+fvJDxpMQA88eb2I9EcafcdjYgsDthnYTvGw==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} hasBin: true peerDependencies: @@ -4672,12 +5229,13 @@ packages: node-notifier: optional: true dependencies: - '@jest/core': 29.6.1 - '@jest/types': 29.6.1 + '@jest/core': 29.7.0 + '@jest/types': 29.6.3 import-local: 3.1.0 - jest-cli: 29.6.1(@types/node@18.16.1) + jest-cli: 29.7.0(@types/node@18.16.1) transitivePeerDependencies: - '@types/node' + - babel-plugin-macros - supports-color - ts-node dev: true @@ -4695,6 +5253,7 @@ packages: /js-yaml@4.1.0: resolution: {integrity: sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==} + hasBin: true dependencies: argparse: 2.0.1 @@ -4705,6 +5264,7 @@ packages: /jsesc@2.5.2: resolution: {integrity: sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==} engines: {node: '>=4'} + hasBin: true dev: true /json-parse-even-better-errors@2.3.1: @@ -4728,6 +5288,7 @@ packages: /json5@2.2.3: resolution: {integrity: sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==} engines: {node: '>=6'} + hasBin: true dev: true /jsonc-parser@3.2.0: @@ -4758,6 +5319,22 @@ packages: semver: 5.7.1 dev: false + /jsonwebtoken@9.0.2: + resolution: {integrity: sha512-PRp66vJ865SSqOlgqS8hujT5U4AOgMfhrwYIuIhfKaoSCZcirrmASQr8CX7cUg+RMih+hgznrjp99o+W4pJLHQ==} + engines: {node: '>=12', npm: '>=6'} + 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.1.1 + ms: 2.1.3 + semver: 7.5.4 + dev: false + /jwa@1.4.1: resolution: {integrity: sha512-qiLX/xhEEFKUAJ6FiBMbes3w9ATzyk5W7Hvzpa/SLYdxNtng+gcurvrI7TbACjIXlsJyr05/S1oUhZrc63evQA==} dependencies: @@ -4780,7 +5357,7 @@ packages: resolution: {integrity: sha512-Xq9nH7KlWZmXAtodXDDRE7vs6DU1gTU8zYDHDiWLSip45Egwq3plLHzPn27NgvzL2r1LMPC1vdqh98sQxtqj4A==} dev: false - /kysely-codegen@0.10.1(kysely@0.26.1)(mysql2@2.3.3)(pg@8.11.1): + /kysely-codegen@0.10.1(kysely@0.26.3)(mysql2@2.3.3)(pg@8.11.1): resolution: {integrity: sha512-8Bslh952gN5gtucRv4jTZDFD18RBioS6M50zHfe5kwb5iSyEAunU4ZYMdHzkHraa4zxjg5/183XlOryBCXLRIw==} hasBin: true peerDependencies: @@ -4798,24 +5375,24 @@ packages: dependencies: chalk: 4.1.2 dotenv: 16.3.1 - kysely: 0.26.1 + kysely: 0.26.3 micromatch: 4.0.5 minimist: 1.2.8 mysql2: 2.3.3 pg: 8.11.1 dev: true - /kysely-paginate@0.2.0(kysely@0.26.1): + /kysely-paginate@0.2.0(kysely@0.26.3): resolution: {integrity: sha512-LsnhnfmXJH+bjFVGy67dXkg21QYBFffLFI+R1FDr/wSyq3yMXGKaxwlbl9r7kXe1QZPXLSIB3oT+nmC1waDTqg==} engines: {node: '>= 16.14.0'} peerDependencies: kysely: ^0.24.2 dependencies: - kysely: 0.26.1 + kysely: 0.26.3 dev: false - /kysely@0.26.1: - resolution: {integrity: sha512-FVRomkdZofBu3O8SiwAOXrwbhPZZr8mBN5ZeUWyprH29jzvy6Inzqbd0IMmGxpd4rcOCL9HyyBNWBa8FBqDAdg==} + /kysely@0.26.3: + resolution: {integrity: sha512-yWSgGi9bY13b/W06DD2OCDDHQmq1kwTGYlQ4wpZkMOJqMGCstVCFIvxCCVG4KfY1/3G0MhDAcZsip/Lw8/vJWw==} engines: {node: '>=14.0.0'} /leven@3.1.0: @@ -4939,8 +5516,9 @@ packages: resolution: {integrity: sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==} engines: {node: '>=12'} - /luxon@1.28.1: - resolution: {integrity: sha512-gYHAa180mKrNIUJCbwpmD0aTu9kV0dREDrwNnuyFAsO1Wt0EVYSZelPnJlbj9HplzXX/YWXHFTL45kvZ53M0pw==} + /luxon@3.4.3: + resolution: {integrity: sha512-tFWBiv3h7z+T/tDaoxA8rqTxy1CHV6gHS//QdaH4pulbq/JuBSGgQspQQqcgnwdAx6pNI7cmvz5Sv/addzHmUg==} + engines: {node: '>=12'} dev: false /make-dir@3.1.0: @@ -5030,6 +5608,7 @@ packages: /mime@1.6.0: resolution: {integrity: sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==} engines: {node: '>=4'} + hasBin: true dev: false /mimic-fn@2.1.0: @@ -5037,19 +5616,20 @@ packages: engines: {node: '>=6'} dev: true - /mimic-fn@4.0.0: - resolution: {integrity: sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw==} - engines: {node: '>=12'} - dev: true - /minimatch@3.1.2: resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==} dependencies: brace-expansion: 1.1.11 + /minimatch@5.1.6: + resolution: {integrity: sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==} + engines: {node: '>=10'} + dependencies: + brace-expansion: 2.0.1 + dev: false + /minimist@1.2.8: resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==} - dev: true /minipass-collect@1.0.2: resolution: {integrity: sha512-6T6lH0H8OG9kITm/Jm6tdooIbogG9e0tLgpY6mphXSm/A9u8Nq1ryBG+Qspiub9LjWlBPsPS3tWQ/Botq4FdxA==} @@ -5100,6 +5680,13 @@ packages: dev: true optional: true + /minipass@2.9.0: + resolution: {integrity: sha512-wxfUjg9WebH+CUDX/CdbRlh5SmfZiy/hpkxaRI16Y9W56Pa75sWgd/rvFilSgrauD9NyFymP/+JFV3KwzIsJeg==} + dependencies: + safe-buffer: 5.2.1 + yallist: 3.1.1 + dev: false + /minipass@3.3.6: resolution: {integrity: sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==} engines: {node: '>=8'} @@ -5110,6 +5697,12 @@ packages: resolution: {integrity: sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==} engines: {node: '>=8'} + /minizlib@1.3.3: + resolution: {integrity: sha512-6ZYMOEnmVsdCeTJVE0W9ZD+pVnE8h9Hma/iOwwRDsdQoePpoX56/8B6z3P9VNwppJuBKNRuFDRNRqRWexT9G9Q==} + dependencies: + minipass: 2.9.0 + dev: false + /minizlib@2.1.2: resolution: {integrity: sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==} engines: {node: '>= 8'} @@ -5117,9 +5710,23 @@ packages: minipass: 3.3.6 yallist: 4.0.0 + /mkdirp@0.5.6: + resolution: {integrity: sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==} + hasBin: true + dependencies: + minimist: 1.2.8 + dev: false + /mkdirp@1.0.4: resolution: {integrity: sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==} engines: {node: '>=10'} + hasBin: true + + /mkdirp@2.1.6: + resolution: {integrity: sha512-+hEnITedc8LAtIP9u3HJDFIdcLV2vXP33sqLLIzkv1Db1zO/1OxbvYf0Y1OC/S/Qo5dxHXepofhmxL02PsKe+A==} + engines: {node: '>=10'} + hasBin: true + dev: false /mlly@1.4.0: resolution: {integrity: sha512-ua8PAThnTwpprIaU47EPeZ/bPUVp2QYBbWMphUQpVdBI3Lgqzm5KZQ45Agm3YJedHXaIHl6pBGabaLSUPPSptg==} @@ -5218,6 +5825,18 @@ packages: resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==} dev: true + /needle@2.9.1: + resolution: {integrity: sha512-6R9fqJ5Zcmf+uYaFgdIHmLwNldn5HbK8L5ybn7Uz+ylX/rnOsSp1AHcvQSrCaFN+qNM1wpymHqD7mVasEOlHGQ==} + engines: {node: '>= 4.4.x'} + hasBin: true + dependencies: + debug: 3.2.7(supports-color@5.5.0) + iconv-lite: 0.4.24 + sax: 1.2.4 + transitivePeerDependencies: + - supports-color + dev: false + /negotiator@0.6.3: resolution: {integrity: sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==} engines: {node: '>= 0.6'} @@ -5233,14 +5852,14 @@ packages: tslib: 2.6.0 dev: true - /node-addon-api@3.2.1: - resolution: {integrity: sha512-mmcei9JghVNDYydghQmeDX8KoAm0FAiYyIcUt/N4nhyAipB17pllZQDOJD2fotxABnt4Mdz+dKTO7eftLg4d0A==} - dev: false - /node-addon-api@4.3.0: resolution: {integrity: sha512-73sE9+3UaLYYFmDsFZnqCInzPyh3MqIwZO9cw58yIqAZhONrrabrYyYe3TuIqtIiOuTXVhsGau8hcrhhwSsDIQ==} dev: true + /node-addon-api@5.1.0: + resolution: {integrity: sha512-eh0GgfEkpnoWDq+VY8OyvYhFEzBk6jIYbRKdIlyTiAXIVJ8PyBaKb0rp7oDtoddbdoHWhq8wwr+XZ81F1rpNdA==} + dev: false + /node-fetch@2.6.12: resolution: {integrity: sha512-C/fGU2E8ToujUivIO0H+tpQ6HWo4eEmchoPIoXtxCrVghxdKq+QOHqEZW7tuP3KlV3bC8FRMO5nMCC7Zm1VP6g==} engines: {node: 4.x || >=6.0.0} @@ -5278,15 +5897,34 @@ packages: resolution: {integrity: sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==} dev: true + /node-pre-gyp@0.17.0: + resolution: {integrity: sha512-abzZt1hmOjkZez29ppg+5gGqdPLUuJeAEwVPtHYEJgx0qzttCbcKFpxrCQn2HYbwCv2c+7JwH4BgEzFkUGpn4A==} + deprecated: 'Please upgrade to @mapbox/node-pre-gyp: the non-scoped node-pre-gyp package is deprecated and only the @mapbox scoped package will recieve updates in the future' + hasBin: true + dependencies: + detect-libc: 1.0.3 + mkdirp: 0.5.6 + needle: 2.9.1 + nopt: 4.0.3 + npm-packlist: 1.4.8 + npmlog: 4.1.2 + rc: 1.2.8 + rimraf: 2.7.1 + semver: 5.7.1 + tar: 4.4.19 + transitivePeerDependencies: + - supports-color + dev: false + /node-releases@2.0.13: resolution: {integrity: sha512-uYr7J37ae/ORWdZeQ1xxMJe3NtdmqMC/JZK+geofDrkLUApKRHPd18/TxtBOJ4A0/+uUIliorNrfYV6s1b02eQ==} dev: true - /node-schedule@2.1.0: - resolution: {integrity: sha512-nl4JTiZ7ZQDc97MmpTq9BQjYhq7gOtoh7SiPH069gBFBj0PzD8HI7zyFs6rzqL8Y5tTiEEYLxgtbx034YPrbyQ==} + /node-schedule@2.1.1: + resolution: {integrity: sha512-OXdegQq03OmXEjt2hZP33W2YPs/E5BcFQks46+G2gAxs4gHOIVD1u7EqlYLYSKsaIpyKCK9Gbk0ta1/gjRSMRQ==} engines: {node: '>=6'} dependencies: - cron-parser: 3.5.0 + cron-parser: 4.9.0 long-timeout: 0.1.1 sorted-array-functions: 1.3.0 dev: false @@ -5315,9 +5953,18 @@ packages: abbrev: 1.1.1 dev: true + /nopt@4.0.3: + resolution: {integrity: sha512-CvaGwVMztSMJLOeXPrez7fyfObdZqNUK1cPAEzLHrTybIua9pMdmmPR5YwtfNftIOMv3DPUhFaxsZMNTQO20Kg==} + hasBin: true + dependencies: + abbrev: 1.1.1 + osenv: 0.1.5 + dev: false + /nopt@5.0.0: resolution: {integrity: sha512-Tbj67rffqceeLpcRXrT7vKAN8CwfPeIBgM7E6iBkmKLV7bEMwpGgYLGv0jACUsECaa/vuxP0IjEont6umdMgtQ==} engines: {node: '>=6'} + hasBin: true dependencies: abbrev: 1.1.1 @@ -5326,6 +5973,24 @@ packages: engines: {node: '>=0.10.0'} dev: true + /npm-bundled@1.1.2: + resolution: {integrity: sha512-x5DHup0SuyQcmL3s7Rx/YQ8sbw/Hzg0rj48eN0dV7hf5cmQq5PXIeioroH3raV1QC1yh3uTYuMThvEQF3iKgGQ==} + dependencies: + npm-normalize-package-bin: 1.0.1 + dev: false + + /npm-normalize-package-bin@1.0.1: + resolution: {integrity: sha512-EPfafl6JL5/rU+ot6P3gRSCpPDW5VmIzX959Ob1+ySFUuuYHWHekXpwdUZcKP5C+DS4GEtdJluwBjnsNDl+fSA==} + dev: false + + /npm-packlist@1.4.8: + resolution: {integrity: sha512-5+AZgwru5IevF5ZdnFglB5wNlHG1AOOuw28WhUq8/8emhBmLv6jX5by4WJCh7lW0uSYZYS6DXqIsyZVIXRZU9A==} + dependencies: + ignore-walk: 3.0.4 + npm-bundled: 1.1.2 + npm-normalize-package-bin: 1.0.1 + dev: false + /npm-run-path@4.0.1: resolution: {integrity: sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==} engines: {node: '>=8'} @@ -5333,12 +5998,14 @@ packages: path-key: 3.1.1 dev: true - /npm-run-path@5.1.0: - resolution: {integrity: sha512-sJOdmRGrY2sjNTRMbSvluQqg+8X7ZK61yvzBEIDhz4f8z1TZFYABsqjjCBd/0PUNE9M6QDgHJXQkGUEm7Q+l9Q==} - engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + /npmlog@4.1.2: + resolution: {integrity: sha512-2uUqazuKlTaSI/dC8AzicUck7+IrEaOnN/e0jd3Xtt1KcGpwx30v50mL7oPyr/h9bL3E4aZccVwpwP+5W9Vjkg==} dependencies: - path-key: 4.0.0 - dev: true + are-we-there-yet: 1.1.7 + console-control-strings: 1.1.0 + gauge: 2.7.4 + set-blocking: 2.0.0 + dev: false /npmlog@5.0.1: resolution: {integrity: sha512-AqZtDUWOMKs1G/8lwylVjrdYgqA4d9nu8hc+0gzRxlDb1I10+FHBGMXs6aiQHFdCUUlqH99MUMuLfzWDNDtfxw==} @@ -5360,6 +6027,11 @@ packages: dev: true optional: true + /number-is-nan@1.0.1: + resolution: {integrity: sha512-4jbtZXNAsfZbAHiiqjLPBiCl16dES1zI4Hpzzxw61Tk+loF+sBDBKx1ICKKKwIqQ7M0mFn1TmkN7euSncWgHiQ==} + engines: {node: '>=0.10.0'} + dev: false + /oauth@0.9.15: resolution: {integrity: sha512-a5ERWK1kh38ExDEfoO6qUHJb32rd7aYmPHuyCu3Fta/cnICvYmgd2uhuKXvPD+PXB+gCEYYEaQdIRAjCOwAKNA==} dev: false @@ -5375,6 +6047,9 @@ packages: /object-inspect@1.12.3: resolution: {integrity: sha512-geUvdk7c+eizMNUDkRpW1wJwgfOiOeHbxBR/hLXK1aT6zmVSO0jsQcs7fj6MGw89jC/cjGfLcNOrtMYtGqm81g==} + + /object-inspect@1.13.1: + resolution: {integrity: sha512-5qoj1RUiKOMsCCNLV1CBiPYE10sziTsnmNxkAI/rZhiD63CF7IqdFGC/XzjWjpSgLf0LxXX3bDFIh0E18f6UhQ==} dev: true /object-keys@1.1.1: @@ -5400,13 +6075,31 @@ packages: es-abstract: 1.21.2 dev: true - /object.values@1.1.6: - resolution: {integrity: sha512-FVVTkD1vENCsAcwNs9k6jea2uHC/X0+JcjG8YA60FN5CMaJmG95wT9jek/xX9nornqGRrBkKtzuAu2wuHpKqvw==} + /object.fromentries@2.0.7: + resolution: {integrity: sha512-UPbPHML6sL8PI/mOqPwsH4G6iyXcCGzLin8KvEPenOZN5lpCNBZZQ+V62vdjB1mQHrmqGQt5/OJzemUA+KJmEA==} engines: {node: '>= 0.4'} dependencies: call-bind: 1.0.2 define-properties: 1.2.0 - es-abstract: 1.21.2 + es-abstract: 1.22.3 + dev: true + + /object.groupby@1.0.1: + resolution: {integrity: sha512-HqaQtqLnp/8Bn4GL16cj+CUYbnpe1bh0TtEaWvybszDG4tgxCJuRpV8VGuvNaI1fAnI4lUJzDG55MXcOH4JZcQ==} + dependencies: + call-bind: 1.0.2 + define-properties: 1.2.0 + es-abstract: 1.22.3 + get-intrinsic: 1.2.1 + dev: true + + /object.values@1.1.7: + resolution: {integrity: sha512-aU6xnDFYT3x17e/f0IiiwlGPTy2jzMySGfUB4fq6z7CV8l85CWHDk5ErhyhpfDHhrOMwGFhSQkhMGHaIotA6Ng==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.2 + define-properties: 1.2.0 + es-abstract: 1.22.3 dev: true /on-finished@2.3.0: @@ -5416,6 +6109,13 @@ packages: ee-first: 1.1.1 dev: false + /on-finished@2.4.1: + resolution: {integrity: sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==} + engines: {node: '>= 0.8'} + dependencies: + ee-first: 1.1.1 + dev: false + /on-headers@1.0.2: resolution: {integrity: sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA==} engines: {node: '>= 0.8'} @@ -5439,23 +6139,6 @@ packages: mimic-fn: 2.1.0 dev: true - /onetime@6.0.0: - resolution: {integrity: sha512-1FlR+gjXK7X+AsAHso35MnyN5KqGwJRi/31ft6x0M194ht7S+rWAvd7PHss9xSKMzE0asv1pyIHaJYq+BbacAQ==} - engines: {node: '>=12'} - dependencies: - mimic-fn: 4.0.0 - dev: true - - /open@9.1.0: - resolution: {integrity: sha512-OS+QTnw1/4vrf+9hh1jc1jnYjzSG4ttTBB8UxOwAnInG3Uo4ssetzC1ihqaIHjLJnA5GGlRl6QlZXOTQhRBUvg==} - engines: {node: '>=14.16'} - dependencies: - default-browser: 4.0.0 - define-lazy-prop: 3.0.0 - is-inside-container: 1.0.0 - is-wsl: 2.2.0 - dev: true - /openapi-endpoint-trimmer@2.0.0: resolution: {integrity: sha512-0eHcIbHkItgk9S8mHPMn8Zn+jsdBztc2KHEaUmJYdq70vzgDr6WF9k4npisb7LhUeH3GN2d6YmKy30Xzs7GXAQ==} hasBin: true @@ -5464,7 +6147,7 @@ packages: commander: 10.0.1 js-yaml: 4.1.0 undici: 5.22.1 - zod: 3.22.2 + zod: 3.22.4 dev: true /openapi-types@12.1.3: @@ -5494,10 +6177,21 @@ packages: type-check: 0.4.0 dev: true + /os-homedir@1.0.2: + resolution: {integrity: sha512-B5JU3cabzk8c67mRRd3ECmROafjYMXbuzlwtqdM8IbS8ktlTix8aFGb2bAGKrSRIlnfKwovGUUr72JUPyOb6kQ==} + engines: {node: '>=0.10.0'} + dev: false + /os-tmpdir@1.0.2: resolution: {integrity: sha512-D2FR03Vir7FIu45XBY20mTb+/ZSWB00sjU9jdQXt83gDrI4Ztz5Fs7/yy74g2N5SVQY4xY1qDr4rNddwYRVX0g==} engines: {node: '>=0.10.0'} - dev: true + + /osenv@0.1.5: + resolution: {integrity: sha512-0CWcCECdMVc2Rw3U5w9ZjqX6ga6ubk1xDVKxtBQPK7wis/0F2r9T6k4ydGYhecl7YUBxBVxhL5oisPsNxAPe2g==} + dependencies: + os-homedir: 1.0.2 + os-tmpdir: 1.0.2 + dev: false /p-finally@1.0.0: resolution: {integrity: sha512-LICb2p9CB7FS+0eR1oqWnHhp0FljGLZCWBE9aix0Uye9W8LTQPwMTYVGWQWIw9RdQiDg4+epXQODwIYJtSJaow==} @@ -5633,10 +6327,10 @@ packages: passport-oauth2: 1.7.0 dev: false - /passport-jwt@4.0.0: - resolution: {integrity: sha512-BwC0n2GP/1hMVjR4QpnvqA61TxenUMlmfNjYNgK0ZAs0HK4SOQkHcSv4L328blNTLtHq7DbmvyNJiH+bn6C5Mg==} + /passport-jwt@4.0.1: + resolution: {integrity: sha512-UCKMDYhNuGOBE9/9Ycuoyh7vP6jpeTp/+sfMJl7nLff/t6dps+iaeE0hhNkKN8/HZHcJ7lCdOyDxHdDoxoSvdQ==} dependencies: - jsonwebtoken: 8.5.1 + jsonwebtoken: 9.0.2 passport-strategy: 1.0.0 dev: false @@ -5656,8 +6350,8 @@ packages: engines: {node: '>= 0.4.0'} dev: false - /passport@0.5.2: - resolution: {integrity: sha512-w9n/Ot5I7orGD4y+7V3EFJCQEznE5RxHamUxcqLT2QoJY0f2JdN8GyHonYFvN0Vz+L6lUJfVhrk2aZz2LbuREw==} + /passport@0.5.3: + resolution: {integrity: sha512-gGc+70h4gGdBWNsR3FuV3byLDY6KBTJAIExGFXTpQaYfbbcHCBlRRKx7RBQSpqEqc5Hh2qVzRs7ssvSfOpkUEA==} engines: {node: '>= 0.4.0'} dependencies: passport-strategy: 1.0.0 @@ -5690,11 +6384,6 @@ packages: engines: {node: '>=8'} dev: true - /path-key@4.0.0: - resolution: {integrity: sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ==} - engines: {node: '>=12'} - dev: true - /path-parse@1.0.7: resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==} dev: true @@ -5851,6 +6540,19 @@ packages: react-is: 18.2.0 dev: true + /pretty-format@29.7.0: + resolution: {integrity: sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + '@jest/schemas': 29.6.3 + ansi-styles: 5.2.0 + react-is: 18.2.0 + dev: true + + /process-nextick-args@2.0.1: + resolution: {integrity: sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==} + dev: false + /promise-inflight@1.0.1: resolution: {integrity: sha512-6zWPyEOFaQBJYcGMHBKTKJ3u6TBsnMFOIZSa6ce1e/ZrrsOlnHRHbabMjLiBYKp+n44X9eUI6VUPaukCXHuG4g==} requiresBuild: true @@ -5900,9 +6602,11 @@ packages: resolution: {integrity: sha512-6Yg0ekpKICSjPswYOuC5sku/TSWaRYlA0qsXqJgM/d/4pLPHPuTxK7Nbf7jFKzAeedUhR8C7K9Uv63FBsSo8xQ==} dev: true - /qs@6.9.6: - resolution: {integrity: sha512-TIRk4aqYLNoJUbd+g2lEdz5kLWIuTMRagAXxl78Q0RiVjAOugHmeKNGdd3cwo/ktpf9aL9epCfFqWDEKysUlLQ==} + /qs@6.11.0: + resolution: {integrity: sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==} engines: {node: '>=0.6'} + dependencies: + side-channel: 1.0.4 dev: false /queue-microtask@1.2.3: @@ -5914,20 +6618,42 @@ packages: engines: {node: '>= 0.6'} dev: false - /raw-body@2.4.2: - resolution: {integrity: sha512-RPMAFUJP19WIet/99ngh6Iv8fzAbqum4Li7AD6DtGaW2RpMB/11xDoalPiJMTbu6I3hkbMVkATvZrqb9EEqeeQ==} + /raw-body@2.5.1: + resolution: {integrity: sha512-qqJBtEyVgS0ZmPGdCFPWJ3FreoqvG4MVQln/kCgF7Olq95IbOp0/BWyMwbdtn4VTvkM8Y7khCQ2Xgk/tcrCXig==} engines: {node: '>= 0.8'} dependencies: - bytes: 3.1.1 - http-errors: 1.8.1 + bytes: 3.1.2 + http-errors: 2.0.0 iconv-lite: 0.4.24 unpipe: 1.0.0 dev: false + /rc@1.2.8: + resolution: {integrity: sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==} + hasBin: true + dependencies: + deep-extend: 0.6.0 + ini: 1.3.8 + minimist: 1.2.8 + strip-json-comments: 2.0.1 + dev: false + /react-is@18.2.0: resolution: {integrity: sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==} dev: true + /readable-stream@2.3.8: + resolution: {integrity: sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==} + dependencies: + core-util-is: 1.0.3 + inherits: 2.0.4 + isarray: 1.0.0 + process-nextick-args: 2.0.1 + safe-buffer: 5.1.2 + string_decoder: 1.1.1 + util-deprecate: 1.0.2 + dev: false + /readable-stream@3.6.2: resolution: {integrity: sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==} engines: {node: '>= 6'} @@ -5946,6 +6672,10 @@ packages: /reflect-metadata@0.1.13: resolution: {integrity: sha512-Ts1Y/anZELhSsjMcU605fU9RE4Oi3p5ORujwbIKXfWa+0Zxs510Qrmrce5/Jowq3cHSZSJqBjypxmHarc+vEWg==} + /regenerator-runtime@0.14.0: + resolution: {integrity: sha512-srw17NI0TUWHuGa5CFGGmhfNIeja30WMBfbslPNhf6JrqQlLN5gcrvig1oqPxiVaXb0oW0XRKtH6Nngs5lKCIA==} + dev: false + /regexp.prototype.flags@1.5.0: resolution: {integrity: sha512-0SutC3pNudRKgquxGoRGIz946MZVHqbNfPjBdxeOhBrdgDKlRoXmYLQN9xRbrR09ZXWeGAdPuif7egofn6v5LA==} engines: {node: '>= 0.4'} @@ -5955,6 +6685,15 @@ packages: functions-have-names: 1.2.3 dev: true + /regexp.prototype.flags@1.5.1: + resolution: {integrity: sha512-sy6TXMN+hnP/wMy+ISxg3krXx7BAtWVO4UouuCN/ziM9UEne0euamVNafDfvC83bRNr95y0V5iijeDQFUNpvrg==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.5 + define-properties: 1.2.0 + set-function-name: 2.0.1 + dev: true + /require-directory@2.1.1: resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==} engines: {node: '>=0.10.0'} @@ -5989,10 +6728,11 @@ packages: engines: {node: '>=10'} dev: true - /resolve@1.22.2: - resolution: {integrity: sha512-Sb+mjNHOULsBv818T40qSPeRiuWLyaGMa5ewydRLFimneixmVy2zdivRl+AF6jaYPC8ERxGDmFSiqui6SfPd+g==} + /resolve@1.22.8: + resolution: {integrity: sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==} + hasBin: true dependencies: - is-core-module: 2.12.1 + is-core-module: 2.13.1 path-parse: 1.0.7 supports-preserve-symlinks-flag: 1.0.0 dev: true @@ -6026,8 +6766,16 @@ packages: resolution: {integrity: sha512-V2hovdzFbOi77/WajaSMXk2OLm+xNIeQdMMuB7icj7bk6zi2F8GGAxigcnDFpJHbNyNcgyJDiP+8nOrY5cZGrA==} dev: false + /rimraf@2.7.1: + resolution: {integrity: sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==} + hasBin: true + dependencies: + glob: 7.2.3 + dev: false + /rimraf@3.0.2: resolution: {integrity: sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==} + hasBin: true dependencies: glob: 7.2.3 @@ -6043,8 +6791,8 @@ packages: semver-compare: 1.0.0 dev: false - /rollup@3.27.0: - resolution: {integrity: sha512-aOltLCrYZ0FhJDm7fCqwTjIUEVjWjcydKBV/Zeid6Mn8BWgDCUBBWT5beM5ieForYNo/1ZHuGJdka26kvQ3Gzg==} + /rollup@3.29.4: + resolution: {integrity: sha512-oWzmBZwvYrU0iJHtDmhsm662rC15FRXmcjCk1xD771dFDx5jJ02ufAQQTn0etB2emNk4J9EZg/yWKpsn9BWGRw==} engines: {node: '>=14.18.0', npm: '>=8.0.0'} hasBin: true optionalDependencies: @@ -6065,13 +6813,6 @@ packages: '@rometools/cli-win32-x64': 12.1.3 dev: true - /run-applescript@5.0.0: - resolution: {integrity: sha512-XcT5rBksx1QdIhlFOCtgZkB99ZEouFZ1E2Kc2LHqNW13U3/74YGdkQRmThTwxy4QIyookibDKYZOPqX//6BlAg==} - engines: {node: '>=12'} - dependencies: - execa: 5.1.1 - dev: true - /run-async@2.4.1: resolution: {integrity: sha512-tvVnVv01b8c1RrA6Ep7JkStj85Guv/YrMcwqYQnwjsAS2cTmmPGBBjAjpCW7RrSodNSoE2/qg9O4bceNvUuDgQ==} engines: {node: '>=0.12.0'} @@ -6090,6 +6831,16 @@ packages: tslib: 1.14.1 dev: true + /safe-array-concat@1.0.1: + resolution: {integrity: sha512-6XbUAseYE2KtOuGueyeobCySj9L4+66Tn6KQMOPQJrAJEowYKW/YR/MGJZl7FdydUdaFu4LYyDZjxf4/Nmo23Q==} + engines: {node: '>=0.4'} + dependencies: + call-bind: 1.0.5 + get-intrinsic: 1.2.2 + has-symbols: 1.0.3 + isarray: 2.0.5 + dev: true + /safe-buffer@5.1.2: resolution: {integrity: sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==} @@ -6125,30 +6876,37 @@ packages: /semver@6.3.0: resolution: {integrity: sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==} + hasBin: true + + /semver@6.3.1: + resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==} + hasBin: true + dev: true /semver@7.5.4: resolution: {integrity: sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==} engines: {node: '>=10'} + hasBin: true dependencies: lru-cache: 6.0.0 - /send@0.17.2: - resolution: {integrity: sha512-UJYB6wFSJE3G00nEivR5rgWp8c2xXvJ3OPWPhmuteU0IKj8nKbG3DrjiOmLwpnHGYWAVwA69zmTm++YG0Hmwww==} + /send@0.18.0: + resolution: {integrity: sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg==} engines: {node: '>= 0.8.0'} dependencies: debug: 2.6.9 - depd: 1.1.2 - destroy: 1.0.4 + 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: 1.8.1 + http-errors: 2.0.0 mime: 1.6.0 ms: 2.1.3 - on-finished: 2.3.0 + on-finished: 2.4.1 range-parser: 1.2.1 - statuses: 1.5.0 + statuses: 2.0.1 transitivePeerDependencies: - supports-color dev: false @@ -6164,14 +6922,14 @@ packages: /seq-queue@0.0.5: resolution: {integrity: sha512-hr3Wtp/GZIc/6DAGPDcV4/9WoZhjrkXsi5B/07QgX8tsdc6ilr7BFM6PM6rbdAX1kFSDYeZGLipIZZKyQP0O5Q==} - /serve-static@1.14.2: - resolution: {integrity: sha512-+TMNA9AFxUEGuC0z2mevogSnn9MXKb4fa7ngeRMJaaGv8vTwnIEkKi+QGvPt33HSnf8pRS+WGM0EbMtCJLKMBQ==} + /serve-static@1.15.0: + resolution: {integrity: sha512-XGuRDNjXUijsUL0vl6nSD7cwURuzEgglbOaFuZM9g3kwDXOWVTck0jLzjPzGD+TazWbboZYu52/9/XPdUgne9g==} engines: {node: '>= 0.8.0'} dependencies: encodeurl: 1.0.2 escape-html: 1.0.3 parseurl: 1.3.3 - send: 0.17.2 + send: 0.18.0 transitivePeerDependencies: - supports-color dev: false @@ -6179,12 +6937,32 @@ packages: /set-blocking@2.0.0: resolution: {integrity: sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==} + /set-function-length@1.1.1: + resolution: {integrity: sha512-VoaqjbBJKiWtg4yRcKBQ7g7wnGnLV3M8oLvVWwOk2PdYY6PEFegR1vezXR0tw6fZGF9csVakIRjrJiy2veSBFQ==} + engines: {node: '>= 0.4'} + dependencies: + define-data-property: 1.1.1 + get-intrinsic: 1.2.2 + gopd: 1.0.1 + has-property-descriptors: 1.0.0 + dev: true + + /set-function-name@2.0.1: + resolution: {integrity: sha512-tMNCiqYVkXIZgc2Hnoy2IvC/f8ezc5koaRFkCjrpWzGpCd3qbZXPzVy9MAZzK1ch/X0jvSkojys3oqJN0qCmdA==} + engines: {node: '>= 0.4'} + dependencies: + define-data-property: 1.1.1 + functions-have-names: 1.2.3 + has-property-descriptors: 1.0.0 + dev: true + /setprototypeof@1.2.0: resolution: {integrity: sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==} dev: false /sha.js@2.4.11: resolution: {integrity: sha512-QMEp5B7cftE7APOjk5Y6xgrbWu+WkLVQwk8JNjZ8nKRciZaByEW6MubieAiToS7+dwvrjGhH8jRXz3MVd0AYqQ==} + hasBin: true dependencies: inherits: 2.0.4 safe-buffer: 5.2.1 @@ -6207,7 +6985,6 @@ packages: call-bind: 1.0.2 get-intrinsic: 1.2.1 object-inspect: 1.12.3 - dev: true /signal-exit@3.0.7: resolution: {integrity: sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==} @@ -6234,11 +7011,6 @@ packages: engines: {node: '>=8'} dev: true - /slash@4.0.0: - resolution: {integrity: sha512-3dOsAHXXUkQTpOYcoAxLIorMTp4gIQr5IW3iVb7A7lFIp0VHhnynm9izx6TssdrIcVIESAlVjtnO2K8bg+Coew==} - engines: {node: '>=12'} - dev: true - /smart-buffer@4.2.0: resolution: {integrity: sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==} engines: {node: '>= 6.0.0', npm: '>= 3.0.0'} @@ -6316,7 +7088,7 @@ packages: node-gyp: optional: true dependencies: - '@mapbox/node-pre-gyp': 1.0.10 + '@mapbox/node-pre-gyp': 1.0.11 node-addon-api: 4.3.0 tar: 6.1.15 optionalDependencies: @@ -6351,11 +7123,6 @@ packages: escape-string-regexp: 2.0.0 dev: true - /statuses@1.5.0: - resolution: {integrity: sha512-OpZ3zP+jT1PI7I8nemJX4AKmAX070ZkYPVWV/AaKTJl+tXCTGyVdC1a4SL8RUQYEwk/f34ZX8UTykN68FwrqAA==} - engines: {node: '>= 0.6'} - dev: false - /statuses@2.0.1: resolution: {integrity: sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==} engines: {node: '>= 0.8'} @@ -6378,6 +7145,15 @@ packages: resolution: {integrity: sha512-/q/8Q4Bl4ZKAPjj8WerIBJWALKkaPRfrvhfF8k/B23i4nzrlRj2/go1m90In7nG/3XDSbOo0+pu6RvCTM9RGMQ==} dev: false + /string-width@1.0.2: + resolution: {integrity: sha512-0XsVpQLnVCXHJfyEs8tC0zpTVIr5PKKsQtkT29IwupnPTjtPmQ3xT/4yCREF9hYkV/3M3kzcUTSAZT6a6h81tw==} + engines: {node: '>=0.10.0'} + dependencies: + code-point-at: 1.1.0 + is-fullwidth-code-point: 1.0.0 + strip-ansi: 3.0.1 + dev: false + /string-width@4.2.3: resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} engines: {node: '>=8'} @@ -6395,6 +7171,15 @@ packages: es-abstract: 1.21.2 dev: true + /string.prototype.trim@1.2.8: + resolution: {integrity: sha512-lfjY4HcixfQXOfaqCvcBuOIapyaroTXhbkfJN3gcB1OtyupngWK4sEET9Knd0cXd28kTUqu/kHoV4HKSJdnjiQ==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.5 + define-properties: 1.2.0 + es-abstract: 1.22.3 + dev: true + /string.prototype.trimend@1.0.6: resolution: {integrity: sha512-JySq+4mrPf9EsDBEDYMOb/lM7XQLulwg5R/m1r0PXEFqrV0qHvl58sdTilSXtKOflCsK2E8jxf+GKC0T07RWwQ==} dependencies: @@ -6403,6 +7188,14 @@ packages: es-abstract: 1.21.2 dev: true + /string.prototype.trimend@1.0.7: + resolution: {integrity: sha512-Ni79DqeB72ZFq1uH/L6zJ+DKZTkOtPIHovb3YZHQViE+HDouuU4mBrLOLDn5Dde3RF8qw5qVETEjhu9locMLvA==} + dependencies: + call-bind: 1.0.5 + define-properties: 1.2.0 + es-abstract: 1.22.3 + dev: true + /string.prototype.trimstart@1.0.6: resolution: {integrity: sha512-omqjMDaY92pbn5HOX7f9IccLA+U1tA9GvtU4JrodiXFfYB7jPzzHpRzpglLAjtUV6bB557zwClJezTqnAiYnQA==} dependencies: @@ -6411,11 +7204,32 @@ packages: es-abstract: 1.21.2 dev: true + /string.prototype.trimstart@1.0.7: + resolution: {integrity: sha512-NGhtDFu3jCEm7B4Fy0DpLewdJQOZcQ0rGbwQ/+stjnrp2i+rlKeCvos9hOIeCmqwratM47OBxY7uFZzjxHXmrg==} + dependencies: + call-bind: 1.0.5 + define-properties: 1.2.0 + es-abstract: 1.22.3 + dev: true + + /string_decoder@1.1.1: + resolution: {integrity: sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==} + dependencies: + safe-buffer: 5.1.2 + dev: false + /string_decoder@1.3.0: resolution: {integrity: sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==} dependencies: safe-buffer: 5.2.1 + /strip-ansi@3.0.1: + resolution: {integrity: sha512-VhumSSbBqDTP8p2ZLKj40UjBCV4+v8bUSEpUb4KjRgWk9pbqGF4REFj6KEagidb2f/M6AzC0EmFyDNGaw9OCzg==} + engines: {node: '>=0.10.0'} + dependencies: + ansi-regex: 2.1.1 + dev: false + /strip-ansi@6.0.1: resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} engines: {node: '>=8'} @@ -6437,10 +7251,10 @@ packages: engines: {node: '>=6'} dev: true - /strip-final-newline@3.0.0: - resolution: {integrity: sha512-dOESqjYr96iWYylGObzd39EuNTa5VJxyvVAEm5Jnh7KGo75V43Hk1odPQkNDyXNmUR6k+gEiDVXnjB8HJ3crXw==} - engines: {node: '>=12'} - dev: true + /strip-json-comments@2.0.1: + resolution: {integrity: sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==} + engines: {node: '>=0.10.0'} + dev: false /strip-json-comments@3.1.1: resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==} @@ -6452,7 +7266,6 @@ packages: engines: {node: '>=4'} dependencies: has-flag: 3.0.0 - dev: true /supports-color@7.2.0: resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} @@ -6472,25 +7285,26 @@ packages: engines: {node: '>= 0.4'} dev: true - /swagger-jsdoc@6.1.0(openapi-types@12.1.3): - resolution: {integrity: sha512-xgep5M8Gq31MxpCbQLvJZpNqHfGPfI+sILCzujZbEXIQp2COtkZgoGASs0gacRs4xHmLDH+GuMGdorPITSG4tA==} + /swagger-jsdoc@6.2.8(openapi-types@12.1.3): + resolution: {integrity: sha512-VPvil1+JRpmJ55CgAtn8DIcpBs0bL5L3q5bVQvF4tAW/k/9JYSj7dCpaYCAv5rufe0vcCbBRQXGvzpkWjvLklQ==} engines: {node: '>=12.0.0'} + hasBin: true dependencies: commander: 6.2.0 doctrine: 3.0.0 glob: 7.1.6 lodash.mergewith: 4.6.2 - swagger-parser: 10.0.2(openapi-types@12.1.3) + swagger-parser: 10.0.3(openapi-types@12.1.3) yaml: 2.0.0-1 transitivePeerDependencies: - openapi-types dev: false - /swagger-parser@10.0.2(openapi-types@12.1.3): - resolution: {integrity: sha512-9jHkHM+QXyLGFLk1DkXBwV+4HyNm0Za3b8/zk/+mjr8jgOSiqm3FOTHBSDsBjtn9scdL+8eWcHdupp2NLM8tDw==} + /swagger-parser@10.0.3(openapi-types@12.1.3): + resolution: {integrity: sha512-nF7oMeL4KypldrQhac8RyHerJeGPD1p2xDh900GPvc+Nk7nWP6jX2FcC7WmkinMoAmoO774+AFXcWsW8gMWEIg==} engines: {node: '>=10'} dependencies: - '@apidevtools/swagger-parser': 10.0.2(openapi-types@12.1.3) + '@apidevtools/swagger-parser': 10.0.3(openapi-types@12.1.3) transitivePeerDependencies: - openapi-types dev: false @@ -6499,29 +7313,34 @@ packages: resolution: {integrity: sha512-c1KmAjuVODxw+vwkNLALQZrgdlBAuBbr2xSPfYrJgseEi7gFKcTvShysPmyuDI4kcUa1+5rFpjWvXdusKY74mg==} dev: false - /swagger-ui-express@4.3.0(express@4.17.2): - resolution: {integrity: sha512-jN46SEEe9EoXa3ZgZoKgnSF6z0w3tnM1yqhO4Y+Q4iZVc8JOQB960EZpIAz6rNROrDApVDwcMHR0mhlnc/5Omw==} + /swagger-ui-express@4.6.3(express@4.18.2): + resolution: {integrity: sha512-CDje4PndhTD2HkgyKH3pab+LKspDeB/NhPN2OF1j+piYIamQqBYwAXWESOT1Yju2xFg51bRW9sUng2WxDjzArw==} engines: {node: '>= v0.10.32'} peerDependencies: - express: '>=4.0.0' + express: '>=4.0.0 || >=5.0.0-beta' dependencies: - express: 4.17.2 + express: 4.18.2 swagger-ui-dist: 5.1.0 dev: false - /synckit@0.8.5: - resolution: {integrity: sha512-L1dapNV6vu2s/4Sputv8xGsCdAVlb5nRDMFU/E27D44l5U6cw1g0dGd45uLc+OXjNMmF4ntiMdCimzcjFKQI8Q==} - engines: {node: ^14.18.0 || >=16.0.0} - dependencies: - '@pkgr/utils': 2.4.2 - tslib: 2.6.0 - dev: true - /tapable@2.2.1: resolution: {integrity: sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ==} engines: {node: '>=6'} dev: true + /tar@4.4.19: + resolution: {integrity: sha512-a20gEsvHnWe0ygBY8JbxoM4w3SJdhc7ZAuxkLqh+nvNQN2IOt0B5lLgM490X5Hl8FF0dl0tOf2ewFYAlIFgzVA==} + engines: {node: '>=4.5'} + dependencies: + chownr: 1.1.4 + fs-minipass: 1.2.7 + minipass: 2.9.0 + minizlib: 1.3.3 + mkdirp: 0.5.6 + safe-buffer: 5.2.1 + yallist: 3.1.1 + dev: false + /tar@6.1.15: resolution: {integrity: sha512-/zKt9UyngnxIT/EAGYuxaMYgOIJiP81ab9ZfkILq4oNLPFX50qyYmu7jRj9qeXoxmJHjGlbH0+cm2uy1WCs10A==} engines: {node: '>=10'} @@ -6590,11 +7409,6 @@ packages: resolution: {integrity: sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==} dev: true - /titleize@3.0.0: - resolution: {integrity: sha512-KxVu8EYHDPBdUYdKZdKtU2aj2XfEx9AfjXxE/Aj0vT06w2icA09Vus1rh6eSu1y01akYg6BjIK/hxyLJINoMLQ==} - engines: {node: '>=12'} - dev: true - /tmp@0.0.33: resolution: {integrity: sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==} engines: {node: '>=0.6.0'} @@ -6659,8 +7473,8 @@ packages: typescript: 5.1.6 dev: true - /ts-jest@29.1.0(@babel/core@7.22.8)(jest@29.5.0)(typescript@5.1.6): - resolution: {integrity: sha512-ZhNr7Z4PcYa+JjMl62ir+zPiNJfXJN6E8hSLnaUKhOgqcn8vb3e537cpkd0FuAfRK3sR1LSqM1MOhliXNgOFPA==} + /ts-jest@29.1.1(@babel/core@7.22.8)(jest@29.7.0)(typescript@5.1.6): + resolution: {integrity: sha512-D6xjnnbP17cC85nliwGiL+tpoKN0StpgE0TeOjXQTU6MVCfsB4v7aW05CgQ/1OywGb0x/oy9hHFnN+sczTiRaA==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} hasBin: true peerDependencies: @@ -6683,7 +7497,7 @@ packages: '@babel/core': 7.22.8 bs-logger: 0.2.6 fast-json-stable-stringify: 2.1.0 - jest: 29.5.0(@types/node@18.16.1) + jest: 29.7.0(@types/node@18.16.1) jest-util: 29.6.1 json5: 2.2.3 lodash.memoize: 4.1.2 @@ -6693,8 +7507,8 @@ packages: yargs-parser: 21.1.1 dev: true - /ts-pattern@5.0.1: - resolution: {integrity: sha512-ZyNm28Lsg34Co5DS3e9DVyjlX2Y+2exkI4jqTKyU+9/OL6Y2fKOOuL8i+7no71o74C6mVS+UFoP3ekM3iCT1HQ==} + /ts-pattern@5.0.5: + resolution: {integrity: sha512-tL0w8U/pgaacOmkb9fRlYzWEUDCfVjjv9dD4wHTgZ61MjhuMt46VNWTG747NqW6vRzoWIKABVhFSOJ82FvXrfA==} dev: false /tsconfck@2.1.2(typescript@5.1.6): @@ -6766,6 +7580,36 @@ packages: mime-types: 2.1.35 dev: false + /typed-array-buffer@1.0.0: + resolution: {integrity: sha512-Y8KTSIglk9OZEr8zywiIHG/kmQ7KWyjseXs1CbSo8vC42w7hg2HgYTxSWwP0+is7bWDc1H+Fo026CpHFwm8tkw==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.5 + get-intrinsic: 1.2.2 + is-typed-array: 1.1.12 + dev: true + + /typed-array-byte-length@1.0.0: + resolution: {integrity: sha512-Or/+kvLxNpeQ9DtSydonMxCx+9ZXOswtwJn17SNLvhptaXYDJvkFFP5zbfU/uLmvnBJlI4yrnXRxpdWH/M5tNA==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.5 + for-each: 0.3.3 + has-proto: 1.0.1 + is-typed-array: 1.1.12 + dev: true + + /typed-array-byte-offset@1.0.0: + resolution: {integrity: sha512-RD97prjEt9EL8YgAgpOkf3O4IF9lhJFr9g0htQkm0rchFp/Vx7LW5Q8fSXXub7BXAODyUQohRMyOc3faCPd0hg==} + engines: {node: '>= 0.4'} + dependencies: + available-typed-arrays: 1.0.5 + call-bind: 1.0.5 + for-each: 0.3.3 + has-proto: 1.0.1 + is-typed-array: 1.1.12 + dev: true + /typed-array-length@1.0.4: resolution: {integrity: sha512-KjZypGq+I/H7HI5HlOoGHkWUUGq+Q0TPhQurLbyrVrvnKTBgzLhIJ7j6J/XTQOi0d1RjyZ0wdas8bKs2p0x3Ng==} dependencies: @@ -6884,8 +7728,8 @@ packages: - supports-color dev: true - /typeorm@0.3.11(mysql2@2.3.3)(pg@8.11.1): - resolution: {integrity: sha512-pzdOyWbVuz/z8Ww6gqvBW4nylsM0KLdUCDExr2gR20/x1khGSVxQkjNV/3YqliG90jrWzrknYbYscpk8yxFJVg==} + /typeorm@0.3.17(mysql2@2.3.3)(pg@8.11.1): + resolution: {integrity: sha512-UDjUEwIQalO9tWw9O2A4GU+sT3oyoUXheHJy4ft+RFdnRdQctdQ34L9SqE2p7LdwzafHx1maxT+bqXON+Qnmig==} engines: {node: '>= 12.9.0'} hasBin: true peerDependencies: @@ -6894,9 +7738,9 @@ packages: better-sqlite3: ^7.1.2 || ^8.0.0 hdb-pool: ^0.1.6 ioredis: ^5.0.4 - mongodb: ^3.6.0 - mssql: ^7.3.0 - mysql2: ^2.2.5 + mongodb: ^5.2.0 + mssql: ^9.1.1 + mysql2: ^2.2.5 || ^3.0.1 oracledb: ^5.1.0 pg: ^8.5.1 pg-native: ^3.0.0 @@ -6947,19 +7791,17 @@ packages: buffer: 6.0.3 chalk: 4.1.2 cli-highlight: 2.1.11 - date-fns: 2.29.3 + date-fns: 2.30.0 debug: 4.3.4 - dotenv: 16.0.0 - glob: 7.2.3 - js-yaml: 4.1.0 - mkdirp: 1.0.4 + dotenv: 16.3.1 + glob: 8.1.0 + mkdirp: 2.1.6 mysql2: 2.3.3 pg: 8.11.1 reflect-metadata: 0.1.13 sha.js: 2.4.11 tslib: 2.6.0 - uuid: 8.3.2 - xml2js: 0.4.23 + uuid: 9.0.1 yargs: 17.7.2 transitivePeerDependencies: - supports-color @@ -7036,11 +7878,6 @@ packages: engines: {node: '>= 0.8'} dev: false - /untildify@4.0.0: - resolution: {integrity: sha512-KK8xQ1mkzZeg9inewmFVDNkg3l5LUhoq9kN6iWYB/CC9YMG8HA+c1Q8HwDe6dEX7kErrEVNVBO3fWsVq5iDgtw==} - engines: {node: '>=8'} - dev: true - /update-browserslist-db@1.0.11(browserslist@4.21.9): resolution: {integrity: sha512-dCwEFf0/oT85M1fHBg4F0jtLwJrutGoHSQXCh7u4o2t1drG+c0a9Flnqww6XUKSfQMPpJBRjU8d4RXB09qtvaA==} hasBin: true @@ -7085,6 +7922,12 @@ packages: /uuid@8.3.2: resolution: {integrity: sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==} + dev: true + + /uuid@9.0.1: + resolution: {integrity: sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==} + hasBin: true + dev: false /v8-to-istanbul@9.1.0: resolution: {integrity: sha512-6z3GW9x8G1gd+JIIgQQQxXuiJtCXeAjp6RaPEPLv62mH3iPHPxV6W3robxtCzNErRo6ZwTmzWhsbNvjyEBKzKA==} @@ -7104,8 +7947,8 @@ packages: engines: {node: '>= 0.8'} dev: false - /vite-node@0.34.1(@types/node@18.16.1): - resolution: {integrity: sha512-odAZAL9xFMuAg8aWd7nSPT+hU8u2r9gU3LRm9QKjxBEF2rRdWpMuqkrkjvyVQEdNFiBctqr2Gg4uJYizm5Le6w==} + /vite-node@0.34.6(@types/node@18.16.1): + resolution: {integrity: sha512-nlBMJ9x6n7/Amaz6F3zJ97EBwR2FkzhBRxF5e+jE6LA3yi6Wtc2lyTij1OnDMIr34v5g/tVQtsVAzhT0jc5ygA==} engines: {node: '>=v14.18.0'} hasBin: true dependencies: @@ -7114,7 +7957,7 @@ packages: mlly: 1.4.0 pathe: 1.1.1 picocolors: 1.0.0 - vite: 4.4.7(@types/node@18.16.1) + vite: 4.5.0(@types/node@18.16.1) transitivePeerDependencies: - '@types/node' - less @@ -7126,8 +7969,8 @@ packages: - terser dev: false - /vite-tsconfig-paths@4.2.0(typescript@5.1.6)(vite@4.4.7): - resolution: {integrity: sha512-jGpus0eUy5qbbMVGiTxCL1iB9ZGN6Bd37VGLJU39kTDD6ZfULTTb1bcc5IeTWqWJKiWV5YihCaibeASPiGi8kw==} + /vite-tsconfig-paths@4.2.1(typescript@5.1.6)(vite@4.5.0): + resolution: {integrity: sha512-GNUI6ZgPqT3oervkvzU+qtys83+75N/OuDaQl7HmOqFTb0pjZsuARrRipsyJhJ3enqV8beI1xhGbToR4o78nSQ==} peerDependencies: vite: '*' peerDependenciesMeta: @@ -7137,14 +7980,14 @@ packages: debug: 4.3.4 globrex: 0.1.2 tsconfck: 2.1.2(typescript@5.1.6) - vite: 4.4.7(@types/node@18.16.1) + vite: 4.5.0(@types/node@18.16.1) transitivePeerDependencies: - supports-color - typescript dev: false - /vite@4.4.7(@types/node@18.16.1): - resolution: {integrity: sha512-6pYf9QJ1mHylfVh39HpuSfMPojPSKVxZvnclX1K1FyZ1PXDOcLBibdq5t1qxJSnL63ca8Wf4zts6mD8u8oc9Fw==} + /vite@4.5.0(@types/node@18.16.1): + resolution: {integrity: sha512-ulr8rNLA6rkyFAlVWw2q5YJ91v098AFQ2R0PRFwPzREXOUJQPtFUG0t+/ZikhaOCDqFoDhN6/v8Sq0o4araFAw==} engines: {node: ^14.18.0 || >=16.0.0} hasBin: true peerDependencies: @@ -7174,7 +8017,7 @@ packages: '@types/node': 18.16.1 esbuild: 0.18.17 postcss: 8.4.27 - rollup: 3.27.0 + rollup: 3.29.4 optionalDependencies: fsevents: 2.3.2 dev: false @@ -7208,6 +8051,17 @@ packages: resolution: {integrity: sha512-iBdZ57RDvnOR9AGBhML2vFZf7h8vmBjhoaZqODJBFWHVtKkDmKuHai3cx5PgVMrX5YDNp27AofYbAwctSS+vhQ==} dev: true + /which-typed-array@1.1.13: + resolution: {integrity: sha512-P5Nra0qjSncduVPEAr7xhoF5guty49ArDTwzJ/yNuPIbZppyRxFQsRCWrocxIY+CnMVG+qfbU2FmDKyvSGClow==} + engines: {node: '>= 0.4'} + dependencies: + available-typed-arrays: 1.0.5 + call-bind: 1.0.5 + for-each: 0.3.3 + gopd: 1.0.1 + has-tostringtag: 1.0.0 + dev: true + /which-typed-array@1.1.9: resolution: {integrity: sha512-w9c4xkx6mPidwp7180ckYWfMmvxpjlZuIudNtDf4N/tTAUB8VJbX25qZoAsrtGuYNnGw3pa0AXgbGKRB8/EceA==} engines: {node: '>= 0.4'} @@ -7232,8 +8086,8 @@ packages: dependencies: string-width: 4.2.3 - /winston-daily-rotate-file@4.6.1(winston@3.6.0): - resolution: {integrity: sha512-Ycch4LZmTycbhgiI2eQXBKI1pKcEQgAqmBjyq7/dC6Dk77nasdxvhLKraqTdCw7wNDSs8/M0jXaLATHquG7xYg==} + /winston-daily-rotate-file@4.7.1(winston@3.11.0): + resolution: {integrity: sha512-7LGPiYGBPNyGHLn9z33i96zx/bd71pjBn9tqQzO3I4Tayv94WPmBNwKC7CO1wPHdP9uvu+Md/1nr6VSH9h0iaA==} engines: {node: '>=8'} peerDependencies: winston: ^3 @@ -7241,7 +8095,7 @@ packages: file-stream-rotator: 0.6.1 object-hash: 2.2.0 triple-beam: 1.3.0 - winston: 3.6.0 + winston: 3.11.0 winston-transport: 4.5.0 dev: false @@ -7254,10 +8108,11 @@ packages: triple-beam: 1.3.0 dev: false - /winston@3.6.0: - resolution: {integrity: sha512-9j8T75p+bcN6D00sF/zjFVmPp+t8KMPB1MzbbzYjeN9VWxdsYnTB40TkbNUEXAmILEfChMvAMgidlX64OG3p6w==} + /winston@3.11.0: + resolution: {integrity: sha512-L3yR6/MzZAOl0DsysUXHVjOwv8mKZ71TrA/41EIduGpOOV5LQVodqN+QdQ6BS6PJ/RdIshZhq84P/fStEZkk7g==} engines: {node: '>= 12.0.0'} dependencies: + '@colors/colors': 1.6.0 '@dabh/diagnostics': 2.0.3 async: 3.2.4 is-stream: 2.0.1 @@ -7308,6 +8163,7 @@ packages: dependencies: sax: 1.2.4 xmlbuilder: 11.0.1 + dev: true /xml2js@0.5.0: resolution: {integrity: sha512-drPFnkQJik/O+uPKpqSgr22mpuFHqKdbS835iAQrUC73L2F5WkboIRd63ai/2Yg6I1jzifPFKH2NTK+cfglkIA==} @@ -7320,6 +8176,7 @@ packages: /xmlbuilder@11.0.1: resolution: {integrity: sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA==} engines: {node: '>=4.0'} + dev: true /xpath.js@1.1.0: resolution: {integrity: sha512-jg+qkfS4K8E7965sqaUl8mRngXiKb3WZGfONgE18pr03FUQiuSV6G+Ej4tS55B+rIQSFEIw3phdVAQ4pPqNWfQ==} @@ -7340,7 +8197,6 @@ packages: /yallist@3.1.1: resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==} - dev: true /yallist@4.0.0: resolution: {integrity: sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==} @@ -7431,6 +8287,19 @@ packages: validator: 13.9.0 optionalDependencies: commander: 2.20.3 + dev: true + + /z-schema@5.0.5: + resolution: {integrity: sha512-D7eujBWkLa3p2sIpJA0d1pr7es+a7m0vFAnZLlCEKq/Ij2k0MLi9Br2UPxoxdYystm5K1yeBGzub0FlYUEWj2Q==} + engines: {node: '>=8.0.0'} + hasBin: true + dependencies: + lodash.get: 4.4.2 + lodash.isequal: 4.5.0 + validator: 13.9.0 + optionalDependencies: + commander: 9.5.0 + dev: false /zen-observable-ts@1.1.0: resolution: {integrity: sha512-1h4zlLSqI2cRLPJUHJFL8bCWHhkpuXkF+dbGkRaWjgDIG26DmzyshUMrdV/rL3UnR+mhaX4fRq8LPouq0MYYIA==} @@ -7445,3 +8314,7 @@ packages: /zod@3.22.2: resolution: {integrity: sha512-wvWkphh5WQsJbVk1tbx1l1Ly4yg+XecD+Mq280uBGt9wa5BKSWf4Mhp6GmrkPixhMxmabYY7RbzlwVP32pbGCg==} + dev: false + + /zod@3.22.4: + resolution: {integrity: sha512-iC+8Io04lddc+mVqQ9AZ7OQ2MrUKGN+oIQyq1vemgt46jwCwLfhq7/pwnBnNXXXZb8VTVLKwp9EDkx+ryxIWmg==}