diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 00000000..d7004a38 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,25 @@ +# Dependencies +node_modules +*.log + +# Git +.git +.gitignore + +# Environment +.env +.env.* + +# OS / Editor +.DS_Store +Thumbs.db +.vscode +.idea + +# Build artifacts +dist +build +coverage + +# Docker +.dockerignore \ No newline at end of file diff --git a/.github/workflows/cd.yml b/.github/workflows/cd.yml new file mode 100644 index 00000000..5ccb2b33 --- /dev/null +++ b/.github/workflows/cd.yml @@ -0,0 +1,28 @@ +# .github/workflows/deploy.yml +name: Deploy to EC2 + +on: + push: + branches: + - main + +jobs: + deploy: + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v5 + + - name: Deploy to EC2 + uses: appleboy/ssh-action@v1.2.2 + with: + host: ${{ secrets.EC2_HOST }} + username: ${{ secrets.EC2_USER }} + key: ${{ secrets.EC2_PRIVATE_KEY }} + port: 22 + script: | + # start.sh에 실행 권한 주기 + chmod +x /home/ec2-user/3-sprint-mission/infra/ec2/start.sh + # 배포 스크립트 실행 + bash /home/ec2-user/3-sprint-mission/infra/ec2/start.sh diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 00000000..87d9a326 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,59 @@ +name: CI + +on: + pull_request: + branches: ["**"] + +jobs: + panda-market-test: + runs-on: ubuntu-latest + + services: + postgres: + image: postgres:15 + env: + POSTGRES_USER: postgres + POSTGRES_PASSWORD: password + POSTGRES_DB: test_db + ports: + - 5432:5432 + options: >- + --health-cmd "pg_isready -U postgres" + --health-interval 10s + --health-timeout 5s + --health-retries 5 + + steps: + - name: Checkout code + uses: actions/checkout@v5 + + - name: Set up Node.js + uses: actions/setup-node@v5 + with: + node-version: "lts/*" + + - name: Cache node modules + uses: actions/cache@v4 + with: + path: ~/.npm + key: ${{ runner.os }}-node-${{ hashFiles('package-lock.json') }} + restore-keys: | + ${{ runner.os }}-node- + + - name: Install dependencies + run: npm install + + - name: Type Check + run: npm run typecheck + + - name: Apply Prisma migrations + run: npx prisma migrate deploy + env: + DATABASE_URL: postgres://postgres:password@localhost:5432/test_db + + - name: Run Tests + run: npm run test + env: + DATABASE_URL: postgres://postgres:password@localhost:5432/test_db + JWT_ACCESS_SECRET: test_jwt_access_secret + JWT_REFRESH_SECRET: test_jwt_refresh_secret diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 00000000..6364d19b --- /dev/null +++ b/Dockerfile @@ -0,0 +1,54 @@ +# ==================================== +# 빌드 스테이지 (build stage) +# ==================================== +ARG NODE_VERSION=22.16.0 +FROM node:${NODE_VERSION} AS my-build-stage + +# 작업 디렉터리 +WORKDIR /docker-compose-app + +# 의존성 모듈 설치 +COPY package*.json ./ +RUN npm ci + +# openssl 설치 +RUN apt-get update -y && apt-get install -y openssl + +# 애플리케이션 소스 복사 +COPY . . + +# Prisma 설정 및 생성 +RUN npx prisma generate + +# 빌드 +RUN npm run build + +# 프로덕션 의존성만 남기기 +RUN npm prune --omit=dev + + +# ==================================== +# 런타임 스테이지 (runtime stage) +# ==================================== +FROM node:${NODE_VERSION}-slim AS runtime + +# openssl 설치 +RUN apt-get update -y && apt-get install -y openssl + +# 보안을 위해 node 사용자 사용 +USER node +WORKDIR /docker-compose-app + +# 필요한 파일만 복사 +COPY --chown=node:node --from=my-build-stage /docker-compose-app/package*.json ./ +COPY --chown=node:node --from=my-build-stage /docker-compose-app/node_modules ./node_modules +COPY --chown=node:node --from=my-build-stage /docker-compose-app/dist ./dist +COPY --chown=node:node --from=my-build-stage /docker-compose-app/prisma ./prisma + +# 환경 +ENV NODE_ENV=production + +EXPOSE 3000 + +# 앱 시작: dist/app.js 기준 +ENTRYPOINT [ "sh", "-c", "npx prisma migrate deploy && npm run start" ] \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 00000000..d00553e2 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,56 @@ +name: youngwook-docker-app + +services: + my-express-app: + image: youngwookcho/docker-app-image:1.0.0 + build: + context: . + dockerfile: ./Dockerfile + args: + - NODE_VERSION=22.16.0 + tags: + - youngwookcho/docker-app-image:1.0.0 + - youngwookcho/docker-app-image + container_name: docker-app-container + env_file: + - .env + environment: + - PORT=3000 + - DATABASE_URL=${POSTGRES_DATABASE_URL} + networks: + - docker-app-network + ports: + - 4000:3000 + depends_on: + my-mypostgres-db: + condition: service_healthy + restart: on-failure + my-mypostgres-db: + image: postgres:17.5 + container_name: mypostgres-db-container + env_file: + - .env + environment: + - POSTGRES_USER=${POSTGRES_USER} + - POSTGRES_PASSWORD=${POSTGRES_PASSWORD} + - POSTGRES_DB=${POSTGRES_DB} + ports: + #로컬환경에서 5432가아닌 5433 사용중이라 컨테이너 포트 5434로 매핑 + - 5434:5432 + healthcheck: + test: ["CMD-SHELL", "pg_isready -U postgres -d ${POSTGRES_DB}"] + interval: 5s + timeout: 5s + retries: 5 + networks: + - docker-app-network + volumes: + - docker-app-volume:/var/lib/postgresql/data + +networks: + docker-app-network: + name: docker-app-network + +volumes: + docker-app-volume: + name: docker-app-volume \ No newline at end of file diff --git a/infra/ec2/start.sh b/infra/ec2/start.sh index dec688c5..4180496d 100644 --- a/infra/ec2/start.sh +++ b/infra/ec2/start.sh @@ -27,13 +27,11 @@ fi echo "PM2로 앱 실행" mkdir -p logs +npx pm2 start infra/ec2/ecosystem.config.js --env production -# ecosystem.config.js 기반 실행 (중복 옵션 제거) -pm2 start infra/ec2/ecosystem.config.js --env production +# PM2 프로세스 저장 & 재부팅 자동 실행 +npx pm2 save +npx pm2 startup -u ec2-user --hp /home/ec2-user -echo "PM2 프로세스 저장 & 재부팅 자동 실행 등록" -pm2 save -pm2 startup -u ec2-user --hp /home/ec2-user - -echo "실행 상태 확인" -pm2 list \ No newline at end of file +# 실행 상태 확인 +npx pm2 list \ No newline at end of file diff --git a/package.json b/package.json index 6cffc68e..ad67d3bb 100644 --- a/package.json +++ b/package.json @@ -10,7 +10,8 @@ "test:coverage": "dotenv -e .env.test -- jest --coverage", "prisma:migrate": "prisma migrate dev", "prisma:reset": "prisma migrate reset --force", - "prisma:seed": "prisma db seed" + "prisma:seed": "prisma db seed", + "typecheck": "tsc --noEmit" }, "prisma": { "seed": "node prisma/seed.js" @@ -18,30 +19,21 @@ "dependencies": { "@aws-sdk/client-s3": "^3.896.0", "@prisma/client": "^6.10.0", - "@types/jest": "^30.0.0", - "@types/supertest": "^6.0.3", "bcrypt": "^6.0.0", "cookie": "^1.0.2", "cookie-parser": "^1.4.7", "cors": "^2.8.5", "dotenv": "^16.6.1", - "dotenv-cli": "^10.0.0", "events": "^3.3.0", "express": "^5.1.0", "express-jwt": "^8.5.1", "is-email": "^1.0.2", "is-uuid": "^1.0.2", - "jest": "^30.1.3", "jsonwebtoken": "^9.0.2", "multer": "^2.0.1", - "pm2": "^6.0.13", - "prisma": "^6.10.0", "socket.io": "^4.8.1", "superstruct": "^2.0.2", - "supertest": "^7.1.4", - "ts-jest": "^29.4.2", "tslib": "^2.8.1", - "tsx": "^4.20.5", "uuid": "^11.1.0", "zod": "^4.0.5" }, @@ -53,12 +45,21 @@ "@types/cookie-parser": "^1.4.9", "@types/cors": "^2.8.19", "@types/express": "^5.0.3", + "@types/jest": "^30.0.0", "@types/jsonwebtoken": "^9.0.10", "@types/multer": "^2.0.0", "@types/node": "^24.1.0", + "@types/supertest": "^6.0.3", "babel-jest": "^30.1.2", + "dotenv-cli": "^10.0.0", + "jest": "^30.1.3", "nodemon": "^3.1.10", + "pm2": "^6.0.13", + "prisma": "^6.10.0", + "supertest": "^7.1.4", + "ts-jest": "^29.4.2", "ts-node": "^10.9.2", + "tsx": "^4.20.5", "typescript": "^5.9.2" } -} +} \ No newline at end of file diff --git a/prisma/schema.prisma b/prisma/schema.prisma index f84b69bb..cd4099d7 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -1,5 +1,6 @@ generator client { provider = "prisma-client-js" + binaryTargets = ["native", "linux-musl-arm64-openssl-3.0.x", "linux-arm64-openssl-1.1.x"] } datasource db { diff --git a/src/middlewares/auth.ts b/src/middlewares/auth.ts index db86a4f7..0e90696f 100644 --- a/src/middlewares/auth.ts +++ b/src/middlewares/auth.ts @@ -1,5 +1,3 @@ -import dotenv from 'dotenv'; -dotenv.config(); import { expressjwt } from 'express-jwt' import { RequestHandler } from 'express'; import { GetResourceFn } from '../types/auth'; @@ -8,10 +6,17 @@ import ProductService from '../services/productService'; import ArticleService from '../services/articleService'; import CommentService from '../services/commentService'; - +const getJWTSecret = () => { + const secret = process.env.JWT_ACCESS_SECRET; + console.log('Auth middleware JWT_ACCESS_SECRET:', secret); + if (!secret) { + throw new Error('JWT_ACCESS_SECRET이 설정되지 않았습니다.'); + } + return secret; +}; const verifyAccessToken = expressjwt({ - secret: (process.env.JWT_ACCESS_SECRET)!, + secret: getJWTSecret(), algorithms: ['HS256'], requestProperty: 'user', }); diff --git a/src/repositories/productRepository.ts b/src/repositories/productRepository.ts index 36920a3d..acdbce2a 100644 --- a/src/repositories/productRepository.ts +++ b/src/repositories/productRepository.ts @@ -1,4 +1,5 @@ -import prisma, { Prisma } from "../config/prisma"; +import prisma from "../config/prisma"; +import { Prisma } from "@prisma/client"; import { HttpError } from "../types/error"; import { PaginatedResponseDto } from "../types/common"; import { diff --git a/src/services/s3Service.ts b/src/services/s3Service.ts index 81904b20..378b21ac 100644 --- a/src/services/s3Service.ts +++ b/src/services/s3Service.ts @@ -1,14 +1,35 @@ import { S3Client, PutObjectCommand } from "@aws-sdk/client-s3"; -const s3Client = new S3Client({ - region: process.env.AWS_S3_REGION ?? "", - credentials: { - accessKeyId: process.env.AWS_S3_ACCESS_KEY_ID ?? "", - secretAccessKey: process.env.AWS_S3_SECRET_KEY_ID ?? "", - }, -}); +let s3Client: S3Client | null = null; + +function getS3Client(): S3Client { + if (!s3Client) { + const region = process.env.AWS_S3_REGION; + const accessKeyId = process.env.AWS_S3_ACCESS_KEY_ID; + const secretAccessKey = process.env.AWS_S3_SECRET_KEY_ID; + + // 테스트 환경이거나 AWS 설정이 없는 경우 mock 클라이언트 반환 + if (!region || !accessKeyId || !secretAccessKey) { + console.warn('AWS S3 환경변수가 설정되지 않았습니다. Mock 클라이언트를 사용합니다.'); + // 테스트 환경에서는 실제 S3 연결 없이 mock 객체 반환 + return { + send: async () => ({ Location: 'mock-s3-url' }) + } as any; + } + + s3Client = new S3Client({ + region, + credentials: { + accessKeyId, + secretAccessKey, + }, + }); + } + return s3Client; +} export async function uploadToS3(file: Express.Multer.File): Promise { + const client = getS3Client(); const key = `sdk/${Date.now()}-${file.originalname}`; const command = new PutObjectCommand({ Bucket: process.env.AWS_S3_BUCKET ?? "", @@ -16,6 +37,12 @@ export async function uploadToS3(file: Express.Multer.File): Promise { Body: file.buffer, ContentType: file.mimetype, }); - await s3Client.send(command); + + // 환경변수가 없는 경우 (테스트 환경) mock URL 반환 + if (!process.env.AWS_S3_REGION || !process.env.AWS_S3_ACCESS_KEY_ID) { + return `mock-s3-url/${key}`; + } + + await client.send(command); return `https://${process.env.AWS_S3_BUCKET}.s3.${process.env.AWS_S3_REGION}.amazonaws.com/${key}`; } \ No newline at end of file diff --git a/test/apiTest/article.private.test.ts b/test/apiTest/article.private.test.ts index 30e208ca..ec115079 100644 --- a/test/apiTest/article.private.test.ts +++ b/test/apiTest/article.private.test.ts @@ -3,6 +3,7 @@ import request from "supertest"; import app from "../../src/app"; import prisma from "../../src/config/prisma"; import { validate } from "uuid"; +import { createTestAccessToken, testUsers, createTestUsers } from "../utils/jwtTestHelper"; describe("인증이 필요한 게시물 API", () => { let agent: any; @@ -11,13 +12,10 @@ describe("인증이 필요한 게시물 API", () => { beforeAll(async () => { agent = request.agent(app); - - const loginResponse = await agent.post("/auth/login").send({ - email: "alice@test.com", - password: "password1", - }); - expect(loginResponse.status).toBe(200); - accessToken = loginResponse.body.accessToken; + // 테스트 유저들을 데이터베이스에 생성 + await createTestUsers(); + // 테스트용 JWT 토큰을 직접 생성 + accessToken = createTestAccessToken(testUsers.alice); }); test("1. 게시글 생성", async () => { @@ -59,15 +57,16 @@ describe("인증이 필요한 게시물 API", () => { }); test("3. 다른 사용자가 작성한 게시글 수정 시도", async () => { - const articleList = await prisma.article.findMany(); - const firstArticleId = articleList[0].id; + // 다른 사용자(Bob)의 토큰 생성 + const bobAccessToken = createTestAccessToken(testUsers.bob); + const updateData = { title: "비정상적인 수정 시도", content: "비정상적인 수정 시도 내용", }; const response = await agent - .patch(`/articles/${firstArticleId}`) - .set("Authorization", `Bearer ${accessToken}`) + .patch(`/articles/${articleId}`) + .set("Authorization", `Bearer ${bobAccessToken}`) .send(updateData); expect(response.status).toBe(403); expect(response.body.message).toBe("게시글을(를) 등록한 유저가 아닙니다"); diff --git a/test/apiTest/auth.test.ts b/test/apiTest/auth.test.ts index 9ea6cb56..f4fec315 100644 --- a/test/apiTest/auth.test.ts +++ b/test/apiTest/auth.test.ts @@ -4,10 +4,11 @@ import app from "../../src/app"; import { validate } from "uuid"; describe("회원가입,로그인 테스트", () => { + const uniqueEmail = `youngwook+${Date.now()}@test.com`; test("1. 회원가입", async () => { const response = await request(app).post("/users").send({ - email: "youngwook@test.com", + email: uniqueEmail, nickname: "영욱", password: "password1234", }); @@ -16,7 +17,7 @@ describe("회원가입,로그인 테스트", () => { expect(response.status).toBe(201); expect(response.body).toHaveProperty("id"); expect(validate(response.body.id)).toBe(true); - expect(response.body.email).toBe("youngwook@test.com"); + expect(response.body.email).toBe(uniqueEmail); expect(response.body.nickname).toBe("영욱"); expect(response.body).not.toHaveProperty("password"); expect(new Date(response.body.createdAt)).toBeInstanceOf(Date); @@ -24,7 +25,7 @@ describe("회원가입,로그인 테스트", () => { test("2. 로그인", async () => { const response = await request(app).post("/auth/login").send({ - email: "youngwook@test.com", + email: uniqueEmail, password: "password1234", }); @@ -36,7 +37,7 @@ describe("회원가입,로그인 테스트", () => { test("3. 잘못된 비밀번호로 로그인 시도", async () => { const response = await request(app).post("/auth/login").send({ - email: "youngwook@test.com", + email: uniqueEmail, password: "wrongpassword", }); expect(response.status).toBe(401); diff --git a/test/apiTest/product.private.test.ts b/test/apiTest/product.private.test.ts index a5974887..78b90328 100644 --- a/test/apiTest/product.private.test.ts +++ b/test/apiTest/product.private.test.ts @@ -2,6 +2,7 @@ import { beforeAll, describe, test, expect } from "@jest/globals"; import request from "supertest"; import app from "../../src/app"; import { validate } from "uuid"; +import { createTestAccessToken, testUsers, createTestUsers } from "../utils/jwtTestHelper"; describe("인증이 필요한 상품 API", () => { let agent: any; @@ -10,14 +11,10 @@ describe("인증이 필요한 상품 API", () => { beforeAll(async () => { agent = request.agent(app); - - const loginResponse = await agent.post("/auth/login").send({ - email: "alice@test.com", - password: "password1", - }); - - expect(loginResponse.status).toBe(200); - accessToken = loginResponse.body.accessToken; + // 테스트 유저들을 데이터베이스에 생성 + await createTestUsers(); + // 테스트용 JWT 토큰을 직접 생성 + accessToken = createTestAccessToken(testUsers.alice); }); test("1. 상품 생성", async () => { diff --git a/test/setup/globalSetup.ts b/test/setup/globalSetup.ts index 2dafa0bc..390db0f1 100644 --- a/test/setup/globalSetup.ts +++ b/test/setup/globalSetup.ts @@ -13,13 +13,17 @@ export default async function globalSetup() { await prisma.$connect(); - await prisma.notification.deleteMany(); + try{ await prisma.articleLike.deleteMany(); await prisma.productLike.deleteMany(); await prisma.comment.deleteMany(); await prisma.article.deleteMany(); await prisma.product.deleteMany(); await prisma.user.deleteMany(); + }catch (error) { + console.log("Global Setup: 데이터베이스 초기화 중 오류 발생", error); + } + await seedDatabase(); diff --git a/test/utils/jwtTestHelper.ts b/test/utils/jwtTestHelper.ts new file mode 100644 index 00000000..74fc7153 --- /dev/null +++ b/test/utils/jwtTestHelper.ts @@ -0,0 +1,114 @@ +import jwt from 'jsonwebtoken'; +import { PrismaClient } from '@prisma/client'; +import bcrypt from 'bcrypt'; + +const prisma = new PrismaClient(); + +export interface TestUser { + id: string; + email: string; + nickname: string; + password: string; +} + +// 테스트 유저들을 데이터베이스에 생성 +export const createTestUsers = async () => { + try { + const saltRounds = 10; + + // 기존 테스트 유저들 먼저 삭제 + const testEmails = Object.values(testUsers).map(user => user.email); + await prisma.user.deleteMany({ + where: { + email: { + in: testEmails + } + } + }); + + for (const user of Object.values(testUsers)) { + const hashedPassword = await bcrypt.hash(user.password, saltRounds); + + const createdUser = await prisma.user.create({ + data: { + id: user.id, + email: user.email, + nickname: user.nickname, + password: hashedPassword + } + }); + + console.log(`유저 생성됨:`, createdUser.id, createdUser.email); + } + + console.log('테스트 유저 생성 완료'); + + } catch (error) { + console.error('테스트 유저 생성 중 오류:', error); + } +}; + +/** + * 테스트용 JWT 액세스 토큰을 생성합니다 + */ +export function createTestAccessToken(user: TestUser): string { + const secret = process.env.JWT_ACCESS_SECRET; + console.log('JWT_ACCESS_SECRET:', secret); + console.log('사용자 ID:', user.id); + + if (!secret) { + throw new Error('JWT_ACCESS_SECRET이 설정되지 않았습니다.'); + } + + // authService.ts와 동일한 페이로드 구조 사용 + const payload = { userId: user.id }; + const token = jwt.sign(payload, secret, { expiresIn: '1h' }); + console.log('생성된 토큰:', token); + + // 토큰이 올바르게 디코드되는지 확인 + try { + const decoded = jwt.verify(token, secret); + console.log('디코드된 토큰:', decoded); + } catch (err) { + console.error('토큰 디코드 오류:', err); + } + + return token; +} + +/** + * 테스트용 JWT 리프레시 토큰을 생성합니다 + */ +export function createTestRefreshToken(user: TestUser): string { + const secret = process.env.JWT_REFRESH_SECRET; + if (!secret) { + throw new Error('JWT_REFRESH_SECRET이 설정되지 않았습니다.'); + } + + const payload = { userId: user.id }; + return jwt.sign(payload, secret, { expiresIn: '2w' }); +} + +/** + * 테스트용 유저 데이터 + */ +export const testUsers = { + alice: { + id: '550e8400-e29b-41d4-a716-446655440001', + email: 'alice@test.com', + nickname: 'Alice', + password: 'testpassword123' + }, + bob: { + id: '550e8400-e29b-41d4-a716-446655440002', + email: 'bob@test.com', + nickname: 'Bob', + password: 'testpassword123' + }, + charlie: { + id: '550e8400-e29b-41d4-a716-446655440003', + email: 'charlie@test.com', + nickname: 'Charlie', + password: 'testpassword123' + } +}; \ No newline at end of file diff --git a/test/utils/seed.ts b/test/utils/seed.ts index 0be05806..80c8625c 100644 --- a/test/utils/seed.ts +++ b/test/utils/seed.ts @@ -1,4 +1,4 @@ -import { PrismaClient, NotificationType } from "@prisma/client"; +import { PrismaClient } from "@prisma/client"; import { hashPassword } from "../../src/utils/passwordUtil"; const prisma = new PrismaClient(); @@ -334,78 +334,6 @@ async function seedDatabase() { // --------------------- // 7. Notifications (더 많은 알림 추가) // --------------------- - await Promise.all([ - prisma.notification.create({ - data: { - userId: alice.id, - type: NotificationType.LIKE, - message: "Bob님이 회원님의 상품을 좋아합니다.", - }, - }), - prisma.notification.create({ - data: { - userId: alice.id, - type: NotificationType.COMMENT, - message: "Carol님이 회원님의 상품에 댓글을 남겼습니다.", - }, - }), - prisma.notification.create({ - data: { - userId: bob.id, - type: NotificationType.COMMENT, - message: "Alice님이 회원님의 상품에 댓글을 남겼습니다.", - }, - }), - prisma.notification.create({ - data: { - userId: bob.id, - type: NotificationType.LIKE, - message: "David님이 회원님의 글을 좋아합니다.", - }, - }), - prisma.notification.create({ - data: { - userId: carol.id, - type: NotificationType.LIKE, - message: "Eve님이 회원님의 상품을 좋아합니다.", - }, - }), - prisma.notification.create({ - data: { - userId: carol.id, - type: NotificationType.COMMENT, - message: "Frank님이 회원님의 글에 댓글을 남겼습니다.", - }, - }), - prisma.notification.create({ - data: { - userId: david.id, - type: NotificationType.LIKE, - message: "Alice님이 회원님의 상품을 좋아합니다.", - }, - }), - prisma.notification.create({ - data: { - userId: eve.id, - type: NotificationType.COMMENT, - message: "Bob님이 회원님의 상품에 댓글을 남겼습니다.", - }, - }), - prisma.notification.create({ - data: { - userId: frank.id, - type: NotificationType.LIKE, - message: "Carol님이 회원님의 상품을 좋아합니다.", - }, - }), - prisma.notification.create({ - data: { - userId: alice.id, - type: NotificationType.LIKE, - message: "Frank님이 회원님의 글을 좋아합니다.", - }, - }), - ]); console.log("Seeding finished!");