Skip to content

Commit a0af0e7

Browse files
nyj001012scarf005JeongJiHwanweg901127kylee001
authored
ci: 2023-01-21 비정기 배포 (#815)
* ci: 타입오류 메시지 상대경로 명시적으로 해소 (#643) 참고: actions/runner#659 * feat: v2 book api (#746) * refactor: v1 api와 다른 부분 수정 * refactor: books API schema default sort value 추가 및 callSign 오타 수정 * refactor: status: zod.enum -> zod.nativeEnum 변경 z.enum으로 했을때 number 반환값에 대한 처리가 되지 않아 enumStatus 생성 후 nativeEnum으로 변경하였습니다. * feat: books//:id v2 구현 * feat: books/search v2 구현 * feat: book/update v2 구현 * feat: books/info/:id v2 구현 * refactor: BookNotFoundError import 오타 수정 * feat: books/info/sorted v2 구현 * feat: books/info/tag v2 구현 book_info.id 로 distinct가 되지 않는 오류가 있어요 * refactor: 의미에 맞는 변수명으로 수정 및 코드 간략화 Co-authored-by: scarf <greenscarf005@gmail.com> * refactor: book_info 중복 select 부분 처리 Co-authored-by: scarf <greenscarf005@gmail.com> * feat: books/donator v2 구현 v1 구현 시 얘기했었던 기부자가 유저가 아니더라도 수정될 수 있도록 수정 & email은 더이상 관리하지 않기 때문에 변수명에서 제거 * refactor: 대출 가능 여부 boolean 반환값으로 수정 kysely 사용중 select가 없을 경우 sql syntax 에러로 해당 부분 수정 및 boolean 값 반환되도록 수정 * feat: [get] books/create v2 구현 axios.get 동작 중 발생하는 에러(catch 영역)에 대한 처리를 어떻게 해야할지 모르겠어요 * fix: 임시로 타입 오류 무시 --------- Co-authored-by: scarf <greenscarf005@gmail.com> * style: prettier 적용 (#761) * build: prettier 설정 * style: prettier 적용 --------- Co-authored-by: nocontribute <> * [fix] backend dockerfile error (#764) Co-authored-by: kylee <kylee@fitfuns.com> * fix: `positiveInt` -> `nonNegativeInt` (#766) * refactor: v2 라우트 정리 적용 (#771) * refactor: `/stock` 제거 #767 (comment) * refactor: contracts에서 500번대 오류 제거 백엔드에서 복구 불가능한 오류일 시 반환하기 때문에, 프론트엔드에서 따로 처리하는 것이 좋을 것 같습니다. * refactor: `/history` -> `/lendings` #767 (comment) Co-authored-by: jwoo <74581396+Jiwon-Woo@users.noreply.github.com> * refactor: `/users ` 정리 #767 (comment) Co-authored-by: honeyl3ee <ddanhopark@gmail.com> * refactor: `/tag` 서비스 임시 제거 고도화를 하기 위해서는 내부 구현을 바꾸어야 하는 문제가 있어 우선순위를 낮추었습니다. 주석처리를 할까 고민했으나 제거 이전 커밋(1565441)으로 체크아웃시 전체 코드를 확인 가능하기 때문에 복잡도 감소를 위해 코드를 제거하였습니다. * refactor: `/books` 경로 정리 #767 (comment) #767 (comment) Co-authored-by: Jeong Jihwan <47599349+JeongJiHwan@users.noreply.github.com> Co-authored-by: jwoo <74581396+Jiwon-Woo@users.noreply.github.com> * feat: swagger에서 1줄 요약 표시 --------- Co-authored-by: jwoo <74581396+Jiwon-Woo@users.noreply.github.com> Co-authored-by: honeyl3ee <ddanhopark@gmail.com> Co-authored-by: Jeong Jihwan <47599349+JeongJiHwan@users.noreply.github.com> * feat: add mydata service 토큰에서 id 정보 찾아서 유저 정보 반환하는 controller * feat: 유저 search 할 때 id 가 undefined 인 경우 핸들링 * feat: add swagger && /me endpoint && apply authValidate * fix: searchUsersById 타입을 이전과 같이 리턴하도록 변경 searchUsersById 서비스 함수의 종속성이 생각보다 많음.controller 에서 items 의 length 를 확인하도록 * fix: add librarian validate in search endpoint * feat: 로그인한 유저만 본인 정보를 찾을 수 있도록 middleware 에서 권한 체크 * chore: console.log 제거 * User API 경로 정리 (#777) * refactor: 400번대 에러 반환 제거 * refactor: overDueDay 반환 값에서 제거 * fix(cursus): Access-Control-Allow-Origin 설정 (#790) * chore: dependencies 업데이트 (#796) * chore(deps): contracts의 pnpm-lock 업데이트 * chore(deps): @mapbox/npm-pre-gyp 설치 * chore(deps): npm-pre-gyp 설치 * Revert "chore(deps): npm-pre-gyp 설치" This reverts commit 8922c38 * chore(deps): package.json과 pnpm-lock.yaml 동기화 * fix: users/me 유저권한 all 로 변경 * fix: 반납 3일 전 알림이 여러 번 전송됨 (#801) * fix(notification): 3일 전 반납 알림을 중복 사용한 부분 삭제 - 3일 전 반납 알림을 보내는 함수가 notifyReturningReminder(), notifyOverdueManager() 인데, 후자가 유연한 동작을 지원하므로 전자 함수의 동작을 제거함 * fix(notification): 슬랙 연체 알림 보내는 함수 스케줄러에 추가 * Update backend/src/v1/notifications/notifications.service.ts Co-authored-by: Ji-Hyuck, Min <45284810+jimin52@users.noreply.github.com> * refactor: console.log 제거 --------- Co-authored-by: Ji-Hyuck, Min <45284810+jimin52@users.noreply.github.com> * fix: `dev/v2` 경로 복구 (#808) * security: 보안 취약점 해결 (#818) * feat(utils): rate limit 모듈 추가 - R, CUD에 해당하는 rate limit 모듈 추가 * refactor(cursus): rate limit 모듈 import해서 사용하도록 변경 * feat: getRateLimiter 적용 * feat(books): 유효한 ISBN인지 검사하는 로직 추가 * feat(tags): tags router에 rate limit 추가 * feat(routes): router에서 authValidate를 미들웨어로 쓰는 곳에 rate limiter 추가 * feat(auth): /get/me에 rate limiter 추가 * feat(users): 유저 생성 후 created 문장 출력 시, db에 저장된 email 값을 사용 * build: csrf 방지를 위한 lusca 패키지 추가 * feat(app): csrf 방지 로직 추가 * feat(app): csrf 방지 옵션 수정 * build: express-session 패키지 추가 * feat(app): lusca 상태 유지를 위한 세션 추가 * feat(app): cookie에도 secret 추가 * feat(app): session에서 cookie 설정 및 lusca에서 부가적인 설정 제거 * fix(auth): /get/me시, id가 null이면 400 status code 반환 (#816) - /get/me 시, id가 null이면 400 status code, errorCode.NO_USER 반환 - catch 로직 수정 --------- Co-authored-by: scarf <greenscarf005@gmail.com> Co-authored-by: Jeong Jihwan <47599349+JeongJiHwan@users.noreply.github.com> Co-authored-by: gilee <gilee@student.42seoul.kr> Co-authored-by: kylee <kylee@fitfuns.com> Co-authored-by: jwoo <74581396+Jiwon-Woo@users.noreply.github.com> Co-authored-by: honeyl3ee <ddanhopark@gmail.com> Co-authored-by: jimin <tesla52@naver.com> Co-authored-by: Ji-Hyuck, Min <45284810+jimin52@users.noreply.github.com>
1 parent d72f526 commit a0af0e7

File tree

163 files changed

+4947
-4726
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

163 files changed

+4947
-4726
lines changed

.eslintrc.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
"jest": true
66
},
77
"root": true,
8-
"extends": ["airbnb-base", "plugin:@typescript-eslint/recommended"],
8+
"extends": ["airbnb-base", "plugin:@typescript-eslint/recommended", "prettier"],
99
"parser": "@typescript-eslint/parser",
1010
"plugins": ["@typescript-eslint", "import"],
1111
"parserOptions": {

.github/workflows/test.yml

+10-9
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,19 @@ name: Test
33
on:
44
pull_request:
55

6+
defaults:
7+
run:
8+
shell: bash
9+
610
jobs:
711
test:
812
name: Test PR
913
runs-on: ubuntu-latest
1014
environment: development
1115

1216
steps:
17+
- uses: reviewdog/action-setup@v1
18+
1319
- name: Checkout
1420
uses: actions/checkout@v3
1521

@@ -40,12 +46,7 @@ jobs:
4046
pnpm install --frozen-lockfile
4147
pnpm --filter='@jiphyeonjeon-42/contracts' build
4248
43-
- if: always()
44-
name: check types (backend)
45-
working-directory: backend
46-
run: pnpm check
47-
48-
- if: always()
49-
name: check types (contracts)
50-
working-directory: contracts
51-
run: pnpm check
49+
- name: check types
50+
if: always()
51+
run: |
52+
pnpm -r --no-bail --parallel run check | sed -r 's|(.*)( check: )(.*)|\1/\3|'

.prettierrc.yml

+7
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
semi: true
2+
singleQuote: true
3+
useTabs: false
4+
tabWidth: 2
5+
trailingComma: all
6+
printWidth: 100
7+
arrowParens: always

Dockerfile

+6
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,12 @@ FROM node:18-alpine as pnpm-installed
33
# https://github.com/pnpm/pnpm/issues/4495#issuecomment-1317831712
44
ENV PNPM_HOME="/root/.local/share/pnpm"
55
ENV PATH="${PATH}:${PNPM_HOME}"
6+
ENV PYTHONUNBUFFERED=1
7+
RUN apk add --update --no-cache python3 && ln -sf python3 /usr/bin/python
8+
RUN python3 -m ensurepip
9+
RUN pip3 install --no-cache --upgrade pip setuptools
10+
RUN apk add --no-cache make
11+
RUN apk add build-base
612
RUN npm install --global pnpm
713
RUN pnpm config set store-dir .pnpm-store
814
RUN pnpm install --global node-pre-gyp

backend/package.json

+5
Original file line numberDiff line numberDiff line change
@@ -24,9 +24,11 @@
2424
"@types/cookie-parser": "^1.4.3",
2525
"@types/cors": "^2.8.13",
2626
"@types/express": "^4.17.17",
27+
"@types/express-session": "^1.17.10",
2728
"@types/http-errors": "^2.0.1",
2829
"@types/jest": "^29.5.2",
2930
"@types/jsonwebtoken": "^9.0.2",
31+
"@types/lusca": "^1.7.4",
3032
"@types/morgan": "^1.9.4",
3133
"@types/node-schedule": "^2.1.0",
3234
"@types/passport": "^1.0.12",
@@ -49,6 +51,7 @@
4951
},
5052
"dependencies": {
5153
"@jiphyeonjeon-42/contracts": "workspace:*",
54+
"@mapbox/node-pre-gyp": "^1.0.11",
5255
"@slack/web-api": "^6.7.1",
5356
"@ts-rest/express": "^3.28.0",
5457
"@ts-rest/open-api": "^3.28.0",
@@ -60,13 +63,15 @@
6063
"dotenv": "^16.0.0",
6164
"express": "^4.17.2",
6265
"express-rate-limit": "^6.9.0",
66+
"express-session": "^1.17.3",
6367
"hangul-js": "^0.2.6",
6468
"http-errors": "^2.0.0",
6569
"http-status": "^1.5.0",
6670
"http-terminator": "^3.2.0",
6771
"jsonwebtoken": "^8.5.1",
6872
"kysely": "^0.26.1",
6973
"kysely-paginate": "^0.2.0",
74+
"lusca": "^1.7.0",
7075
"morgan": "^1.10.0",
7176
"mysql2": "^2.3.3",
7277
"node-schedule": "^2.1.0",

backend/src/app.ts

+18-1
Original file line numberDiff line numberDiff line change
@@ -18,12 +18,29 @@ import { createExpressEndpoints } from '@ts-rest/express';
1818

1919
import router from '~/v1/routes';
2020
import routerV2 from '~/v2/routes';
21+
import lusca from "lusca";
22+
import session from 'express-session';
23+
import * as crypto from "crypto";
2124
import { morganMiddleware } from './logger';
2225

2326
const app: express.Application = express();
27+
const secret = crypto.randomBytes(42).toString('hex');
2428

29+
app.use(session({
30+
secret,
31+
resave: false,
32+
saveUninitialized: true,
33+
cookie: {
34+
httpOnly: true,
35+
sameSite: 'strict',
36+
secure: true,
37+
}
38+
}));
2539
app.use(morganMiddleware);
26-
app.use(cookieParser());
40+
app.use(cookieParser(
41+
secret,
42+
));
43+
app.use(lusca.csrf());
2744
app.use(passport.initialize());
2845
app.use(express.urlencoded({ extended: true }));
2946
app.use(express.json());

backend/src/config/JwtOption.ts

+13-11
Original file line numberDiff line numberDiff line change
@@ -5,19 +5,21 @@ import { Mode } from './modeOption';
55
import { match } from 'ts-pattern';
66

77
type getJwtOption = (mode: Mode) => (option: OauthUrlOption) => JwtOption;
8-
export const getJwtOption: getJwtOption = (mode) => ({ redirectURL, clientURL }) => {
9-
const redirectDomain = new URL(redirectURL).hostname;
10-
const clientDomain = new URL(clientURL).hostname;
11-
const secure = mode === 'prod' || mode === 'https';
8+
export const getJwtOption: getJwtOption =
9+
(mode) =>
10+
({ redirectURL, clientURL }) => {
11+
const redirectDomain = new URL(redirectURL).hostname;
12+
const clientDomain = new URL(clientURL).hostname;
13+
const secure = mode === 'prod' || mode === 'https';
1214

13-
const issuer = secure ? redirectDomain : 'localhost';
14-
const domain = match(mode)
15-
.with('prod', () => clientDomain)
16-
.with('https', () => undefined)
17-
.otherwise(() => 'localhost');
15+
const issuer = secure ? redirectDomain : 'localhost';
16+
const domain = match(mode)
17+
.with('prod', () => clientDomain)
18+
.with('https', () => undefined)
19+
.otherwise(() => 'localhost');
1820

19-
return { issuer, domain, secure };
20-
};
21+
return { issuer, domain, secure };
22+
};
2123

2224
export const jwtSecretSchema = z.object({ JWT_SECRET: nonempty }).transform((v) => v.JWT_SECRET);
2325

backend/src/config/config.type.ts

+5-5
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ export type NaverBookApiOption = {
1919

2020
/** 네이버 도서 검색 API 시크릿 */
2121
secret: string;
22-
}
22+
};
2323

2424
/** DB 연결 옵션 */
2525
export type ConnectOption = {
@@ -34,7 +34,7 @@ export type ConnectOption = {
3434

3535
/** DB 이름 */
3636
database: string;
37-
}
37+
};
3838

3939
/** OAuth URL 옵션 */
4040
export type OauthUrlOption = {
@@ -43,7 +43,7 @@ export type OauthUrlOption = {
4343

4444
/** 집현전 프론트엔드 URL */
4545
clientURL: string;
46-
}
46+
};
4747

4848
/** 42 API OAuth 클라이언트 인증 정보 */
4949
export type Oauth42ApiOption = {
@@ -52,7 +52,7 @@ export type Oauth42ApiOption = {
5252

5353
/** 42 API OAuth 클라이언트 시크릿 */
5454
secret: string;
55-
}
55+
};
5656

5757
/** npm 로깅 레벨 */
5858
export type LogLevel = keyof typeof levels;
@@ -64,4 +64,4 @@ export type LogLevelOption = {
6464

6565
/** 콘솔 로깅 레벨 */
6666
readonly consoleLogLevel: 'error' | 'debug';
67-
}
67+
};

backend/src/config/dbSchema.ts

+11-7
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,17 @@
11
import { envObject, nonempty } from './envObject';
22

33
/** RDS 연결 옵션 파싱을 위한 스키마 */
4-
export const rdsSchema = envObject('RDS_HOSTNAME', 'RDS_USERNAME', 'RDS_PASSWORD', 'RDS_DB_NAME')
5-
.transform((v) => ({
6-
host: v.RDS_HOSTNAME,
7-
username: v.RDS_USERNAME,
8-
password: v.RDS_PASSWORD,
9-
database: v.RDS_DB_NAME,
10-
}));
4+
export const rdsSchema = envObject(
5+
'RDS_HOSTNAME',
6+
'RDS_USERNAME',
7+
'RDS_PASSWORD',
8+
'RDS_DB_NAME',
9+
).transform((v) => ({
10+
host: v.RDS_HOSTNAME,
11+
username: v.RDS_USERNAME,
12+
password: v.RDS_PASSWORD,
13+
database: v.RDS_DB_NAME,
14+
}));
1115

1216
/** MYSQL 연결 옵션 파싱을 위한 스키마 */
1317
const mysqlSchema = envObject('MYSQL_USER', 'MYSQL_PASSWORD', 'MYSQL_DATABASE')

backend/src/config/envObject.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ export const url = z.string().trim().url();
1515
* @param keys 환경변수 키 목록
1616
*/
1717
export const envObject = <T extends readonly string[]>(...keys: T) => {
18-
type Keys = T[ number ];
18+
type Keys = T[number];
1919
const env = Object.fromEntries(keys.map((key) => [key, nonempty]));
2020

2121
return z.object(env as Record<Keys, typeof nonempty>);

backend/src/config/getConnectOption.ts

+6-4
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,10 @@ const getConnectOptionSchema = (mode: Mode) => {
1313
/**
1414
* 환경변수에서 DB 연결 옵션을 파싱하는 함수
1515
*/
16-
export const getConnectOption = (mode: Mode) => (processEnv: NodeJS.ProcessEnv): ConnectOption => {
17-
const connectOptionSchema = getConnectOptionSchema(mode);
16+
export const getConnectOption =
17+
(mode: Mode) =>
18+
(processEnv: NodeJS.ProcessEnv): ConnectOption => {
19+
const connectOptionSchema = getConnectOptionSchema(mode);
1820

19-
return connectOptionSchema.parse(processEnv);
20-
};
21+
return connectOptionSchema.parse(processEnv);
22+
};

backend/src/config/logOption.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,8 @@ export const colors: Record<LogLevel, string> = {
1818
} as const;
1919

2020
export const getLogLevelOption = (mode: RuntimeMode): LogLevelOption => {
21-
const logLevel = (mode === 'production' ? 'http' : 'debug');
22-
const consoleLogLevel = (mode === 'production' ? 'error' : 'debug');
21+
const logLevel = mode === 'production' ? 'http' : 'debug';
22+
const consoleLogLevel = mode === 'production' ? 'error' : 'debug';
2323

2424
return { logLevel, consoleLogLevel } as const;
2525
};

backend/src/config/naverBookApiOption.ts

+7-5
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,13 @@
22
import { NaverBookApiOption } from './config.type';
33
import { envObject } from './envObject';
44

5-
const naverBookApiSchema = envObject('NAVER_BOOK_SEARCH_CLIENT_ID', 'NAVER_BOOK_SEARCH_SECRET')
6-
.transform((v) => ({
7-
client: v.NAVER_BOOK_SEARCH_CLIENT_ID,
8-
secret: v.NAVER_BOOK_SEARCH_SECRET,
9-
}));
5+
const naverBookApiSchema = envObject(
6+
'NAVER_BOOK_SEARCH_CLIENT_ID',
7+
'NAVER_BOOK_SEARCH_SECRET',
8+
).transform((v) => ({
9+
client: v.NAVER_BOOK_SEARCH_CLIENT_ID,
10+
secret: v.NAVER_BOOK_SEARCH_SECRET,
11+
}));
1012

1113
export const getNaverBookApiOption = (processEnv: NodeJS.ProcessEnv): NaverBookApiOption => {
1214
const option = naverBookApiSchema.parse(processEnv);

backend/src/config/oauthOption.ts

+14-10
Original file line numberDiff line numberDiff line change
@@ -12,15 +12,19 @@ export const oauth42Schema = z.object({
1212
CLIENT_SECRET: nonempty,
1313
});
1414

15-
export const getOauthUrlOption = (processEnv: NodeJS.ProcessEnv): OauthUrlOption => oauthUrlSchema
16-
.transform((v) => ({
17-
redirectURL: v.REDIRECT_URL,
18-
clientURL: v.CLIENT_URL,
19-
})).parse(processEnv);
15+
export const getOauthUrlOption = (processEnv: NodeJS.ProcessEnv): OauthUrlOption =>
16+
oauthUrlSchema
17+
.transform((v) => ({
18+
redirectURL: v.REDIRECT_URL,
19+
clientURL: v.CLIENT_URL,
20+
}))
21+
.parse(processEnv);
2022

2123
// eslint-disable-next-line max-len
22-
export const getOauth42ApiOption = (processEnv: NodeJS.ProcessEnv): Oauth42ApiOption => oauth42Schema
23-
.transform((v) => ({
24-
id: v.CLIENT_ID,
25-
secret: v.CLIENT_SECRET,
26-
})).parse(processEnv);
24+
export const getOauth42ApiOption = (processEnv: NodeJS.ProcessEnv): Oauth42ApiOption =>
25+
oauth42Schema
26+
.transform((v) => ({
27+
id: v.CLIENT_ID,
28+
secret: v.CLIENT_SECRET,
29+
}))
30+
.parse(processEnv);

backend/src/entity/entities/Book.ts

+19-14
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,11 @@
11
import {
2-
Column, Entity, Index, JoinColumn, ManyToOne, OneToMany, PrimaryGeneratedColumn,
2+
Column,
3+
Entity,
4+
Index,
5+
JoinColumn,
6+
ManyToOne,
7+
OneToMany,
8+
PrimaryGeneratedColumn,
39
} from 'typeorm';
410
import { BookInfo } from './BookInfo';
511
import { User } from './User';
@@ -8,55 +14,54 @@ import { Reservation } from './Reservation';
814

915
@Index('FK_donator_id_from_user', ['donatorId'], {})
1016
@Entity('book')
11-
1217
export class Book {
1318
@PrimaryGeneratedColumn({ type: 'int', name: 'id' })
14-
id?: number;
19+
id?: number;
1520

1621
@Column('varchar', { name: 'donator', nullable: true, length: 255 })
17-
donator: string | null;
22+
donator: string | null;
1823

1924
@Column('varchar', { name: 'callSign', length: 255 })
20-
callSign: string;
25+
callSign: string;
2126

2227
@Column('int', { name: 'status' })
23-
status: number;
28+
status: number;
2429

2530
@Column('datetime', {
2631
name: 'createdAt',
2732
default: () => "'CURRENT_TIMESTAMP(6)'",
2833
})
29-
createdAt?: Date;
34+
createdAt?: Date;
3035

3136
@Column('int')
32-
infoId: number;
37+
infoId: number;
3338

3439
@Column('datetime', {
3540
name: 'updatedAt',
3641
default: () => "'CURRENT_TIMESTAMP(6)'",
3742
})
38-
updatedAt?: Date;
43+
updatedAt?: Date;
3944

4045
@Column('int', { name: 'donatorId', nullable: true })
41-
donatorId: number | null;
46+
donatorId: number | null;
4247

4348
@ManyToOne(() => BookInfo, (bookInfo) => bookInfo.books, {
4449
onDelete: 'NO ACTION',
4550
onUpdate: 'NO ACTION',
4651
})
4752
@JoinColumn([{ name: 'infoId', referencedColumnName: 'id' }])
48-
info?: BookInfo;
53+
info?: BookInfo;
4954

5055
@ManyToOne(() => User, (user) => user.books, {
5156
onDelete: 'NO ACTION',
5257
onUpdate: 'NO ACTION',
5358
})
5459
@JoinColumn([{ name: 'donatorId', referencedColumnName: 'id' }])
55-
donator2?: User;
60+
donator2?: User;
5661

5762
@OneToMany(() => Lending, (lending) => lending.book)
58-
lendings?: Lending[];
63+
lendings?: Lending[];
5964

6065
@OneToMany(() => Reservation, (reservation) => reservation.book)
61-
reservations?: Reservation[];
66+
reservations?: Reservation[];
6267
}

0 commit comments

Comments
 (0)