본문 바로가기

[IT/Programming]

DATABASE setup of View My StartUp

728x90
반응형
# DATABASE setup of View My StartUp db error 가 날 땐, ```[.linenums] npx prisma migrate reset --force ```/ 후 다시 ```[.linenums] npm install --production && npx prisma migrate deploy && npx prisma generate && npx prisma db seed ```/ ## PH
  • 2024-09-26 : First posting.
## TOC ## ERD models ### model 1
### model 2
## Schema 모음. User: Company: Watch and Comparison: Investment: ## User
User
```[.linenums] model User { id String @id @default(uuid()) email String @unique // 이메일 nickname String @unique // 서비스 내 표시할 별명 name String // 유저 성명 iter Int @default(10000) // Hash iteration, 매 인증성공시마다 1씩 감소 salt String @db.Char(32) // Random string, length: 32 pwdEncrypted String @db.Char(104) // Encrypted password, length: 104 createdAt DateTime @default(now()) updatedAt DateTime @updatedAt investments Investment[] comparisons Comparison[] watchList Watch[] UserSession UserSession[] } # PostgreSQL CREATE TABLE "User" ( "id" TEXT NOT NULL, "email" TEXT NOT NULL, "nickname" TEXT NOT NULL, "name" TEXT NOT NULL, "iter" INTEGER NOT NULL DEFAULT 10000, "salt" CHAR(32) NOT NULL, "pwdEncrypted" CHAR(104) NOT NULL, "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, "updatedAt" TIMESTAMP(3) NOT NULL, CONSTRAINT "User_pkey" PRIMARY KEY ("id") ); CREATE UNIQUE INDEX "User_email_key" ON "User"("email"); CREATE UNIQUE INDEX "User_nickname_key" ON "User"("nickname"); ```/
UserSession
```[.linenums] model UserSession { user User @relation(fields: [userId], references: [id]) userId String iter Int @default(1000) // Hash iteration. 성공시마다 1씩 감소. // Session 을 검사할때마다 성공시 iter 를 하나씩 줄입니다. hash function 을 1000번 돌린 값으로 sessionEncrypted 와 맞춰보고 999번 돌린 값으로 받고 서버단에서 1번 더 hash 를 돌려서 값이 같은지 판단합니다. 성공하면 이번엔 client 단에서 998번 hash 를 돌린 값을 받고 서버단에서 2번 더 hash 를 돌린 뒤 sessionEncrypted 와 맞춰봅니다. 계속해서 iter 수를 줄여갑니다. // 이렇게 하면 Network 중단에서 데이터를 가로챈 뒤 Session 을 훔쳐서 몰래 로그인 하는걸 방지할 수 있습니다. // Hash 는 정방향으로는 순쉽간에 결과가 나오지만, 이 Hash 의 역함수를 구하는건 굉장히 어렵습니다. Rainbow table 로 모든 암호에 대해 마지막 sessionEncrypted 가 뭐가 나오는지 저장해놓고 역산할수는 있지만, 이 마저도 각 유저별로 랜덤한 다른 salt 를 첨가하는 것으로 막을 수 있습니다. // 이 암호는 양자컴퓨터로도 못 푸는 것으로 알고 있습니다. // 정확히 어떻게 동작하는건지 궁금하신 분은 이강수님 (kipacti@gmail.com) 에게 문의해 주세요. ip String @db.Char(45) // IPv6 port 포함 max 값 sessionSalt String @db.Char(32) sessionEncrypted String @db.Char(104) createdAt DateTime @default(now()) updatedAt DateTime @updatedAt @@id([userId, createdAt]) @@index([userId, createdAt(sort: Desc)]) } # PostgreSQL CREATE TABLE "UserSession" ( "userId" TEXT NOT NULL, "iter" INTEGER NOT NULL DEFAULT 1000, "ip" CHAR(45) NOT NULL, "sessionSalt" CHAR(32) NOT NULL, "sessionEncrypted" CHAR(104) NOT NULL, "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, "updatedAt" TIMESTAMP(3) NOT NULL, CONSTRAINT "UserSession_pkey" PRIMARY KEY ("userId","createdAt") ); CREATE INDEX "UserSession_userId_createdAt_idx" ON "UserSession"("userId", "createdAt" DESC); ALTER TABLE "UserSession" ADD CONSTRAINT "UserSession_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE; ```/
## Company
Company
```[.linenums] model Company { // TODO 가상 투자 금액 총계를 직접 저장할 것인가, 매번 계산할 것인가? // 당장은 FE 단에서 계산하고 있음. id String @id @default(uuid()) name String @unique description String category String // 회사 카테고리. 입력단에서 검증해야함. accumulInvest BigInt // NOTE 누적 투자 금액(실제) revenue BigInt // NOTE 매출액 employees Int // NOTE 고용 인원수 logo String? // NOTE 로고 이미지 createdAt DateTime @default(now()) updatedAt DateTime @updatedAt investments Investment[] comparisons Comparison[] watcherList Watch[] } # PostgreSQL CREATE TABLE "Company" ( "id" TEXT NOT NULL, "name" TEXT NOT NULL, "description" TEXT NOT NULL, "category" TEXT NOT NULL, "accumulInvest" BIGINT NOT NULL, "revenue" BIGINT NOT NULL, "employees" INTEGER NOT NULL, "logo" TEXT, "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, "updatedAt" TIMESTAMP(3) NOT NULL, CONSTRAINT "Company_pkey" PRIMARY KEY ("id") ); CREATE UNIQUE INDEX "Company_name_key" ON "Company"("name"); ```/
## Watch and Comparison
Watch
```[.linenums] model Watch { id String @id @default(uuid()) createdAt DateTime @default(now()) updatedAt DateTime @updatedAt user User? @relation(fields: [userId], references: [id], onDelete: SetNull) userId String? company Company? @relation(fields: [companyId], references: [id], onDelete: SetNull) companyId String? } # PostgreSQL CREATE TABLE "Watch" ( "id" TEXT NOT NULL, "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, "updatedAt" TIMESTAMP(3) NOT NULL, "userId" TEXT, "companyId" TEXT, CONSTRAINT "Watch_pkey" PRIMARY KEY ("id") ); ALTER TABLE "Watch" ADD CONSTRAINT "Watch_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE SET NULL ON UPDATE CASCADE; ALTER TABLE "Watch" ADD CONSTRAINT "Watch_companyId_fkey" FOREIGN KEY ("companyId") REFERENCES "Company"("id") ON DELETE SET NULL ON UPDATE CASCADE; ```/
Comparison
```[.linenums] model Comparison { id String @id @default(uuid()) createdAt DateTime @default(now()) updatedAt DateTime @updatedAt user User? @relation(fields: [userId], references: [id], onDelete: SetNull) userId String? company Company? @relation(fields: [companyId], references: [id], onDelete: SetNull) companyId String? } # PostgreSQL CREATE TABLE "Comparison" ( "id" TEXT NOT NULL, "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, "updatedAt" TIMESTAMP(3) NOT NULL, "userId" TEXT, "companyId" TEXT, CONSTRAINT "Comparison_pkey" PRIMARY KEY ("id") ); ALTER TABLE "Comparison" ADD CONSTRAINT "Comparison_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE SET NULL ON UPDATE CASCADE; ALTER TABLE "Comparison" ADD CONSTRAINT "Comparison_companyId_fkey" FOREIGN KEY ("companyId") REFERENCES "Company"("id") ON DELETE SET NULL ON UPDATE CASCADE; ```/
## Investment
Investment
```[.linenums] model Investment { id String @id @default(uuid()) name String amount BigInt comment String password String createdAt DateTime @default(now()) updatedAt DateTime @updatedAt user User? @relation(fields: [userId], references: [id], onDelete: SetNull) userId String? // NOTE 투자자 company Company? @relation(fields: [companyId], references: [id], onDelete: SetNull) companyId String? // NOTE 투자대상 기업 @@index([userId, createdAt(sort: Desc)]) } # PostgreSQL CREATE TABLE "Investment" ( "id" TEXT NOT NULL, "name" TEXT NOT NULL, "amount" BIGINT NOT NULL, "comment" TEXT NOT NULL, "password" TEXT NOT NULL, "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, "updatedAt" TIMESTAMP(3) NOT NULL, "userId" TEXT, "companyId" TEXT, CONSTRAINT "Investment_pkey" PRIMARY KEY ("id") ); CREATE INDEX "Investment_userId_createdAt_idx" ON "Investment"("userId", "createdAt" DESC); ALTER TABLE "Investment" ADD CONSTRAINT "Investment_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE SET NULL ON UPDATE CASCADE; ALTER TABLE "Investment" ADD CONSTRAINT "Investment_companyId_fkey" FOREIGN KEY ("companyId") REFERENCES "Company"("id") ON DELETE SET NULL ON UPDATE CASCADE; ```/
## API ### /companies #### GET /companies/?skip=0&take=10&sort=recent&keyword= ```[.linenums] query = { keyword = '', skip = 0, take = 10, sort = 'recent', include = '' } return { list: rankedCompanies-with-include, totalCount } ```/
include can be 'investments' or 'watcherAndComparison'.
#### GET /companies/:id ```[.linenums] id: Uuid (company id) return company ```/ ### GET /companies/count ```[.linenums] query = { keyword = '' } return { count: Number } ```/ ### /comparisons #### POST /comparisons/select ```[.linenums] data = { selectedCompanyIds, userId } selectedCompanyIds.map(async companyId => { return await this.data.createComparison(companyId, userId); }); return { list: results-of-comparisons, totalCount: results.length } ```/ #### GET /comparisons/select/:userId ```[.linenums] userId: Uuid prisma.comparison.findMany({ where: { userId }, include: { company: true }, // company 정보 포함 }); return comparisons-with-including-company ```/ ### /watches #### POST /watches/select ```[.linenums] data = { companyId, userId } return watch ```/ #### GET /watches/select/:userId ```[.linenums] userId: Uuid return findFirst-watch-with-including-company ```/ ### /investments #### GET /investments/?page=0&pageSize=10&orderBy=bigger ```[.linenums] params = { page: 0, pageSize: 10, orderBy: 'bigger' } return { list: [investments], totalCount } ```/ #### GET /investments/:companyId?page=0&pageSize=10&orderBy=bigger ```[.linenums] params = { page: 0, pageSize: 10, orderBy: 'bigger' } companyId: Uuid return { list: [investments], totalCount } ```/ #### GET /investments/:companyId/total ```[.linenums] companyId: Uuid return total: Number ```/ #### POST /investments/ (아직 미구현) ```[.linenums] s.object({ id: s.optional(Uuid), name: s.size(s.string(), 1, 10), amount: s.min(s.number(), 0), // 억 단위 comment: s.size(s.string(), 1, 20), // password: s.min(s.union([s.string(), s.number()]), 8), password: s.string(), userId: Uuid, companyId: Uuid, }); return investment ```/ #### PATCH /investments/:id ```[.linenums] id: Uuid (investment id) data: partial(createInvestment) return investment ```/ #### DELETE /investments/:id ```[.linenums] id: Uuid (investment id) { password } return investment or status(204: NO_CONTENT) ```/ ### /account #### POST /account/iter ```[.linenums] data = { email } return { iter: Number, salt: String } ```/ #### POST /account/session-iter ```[.linenums] data = { userId, createdAt } return { iter: Number, sessionSalt: String } ```/ #### POST /account/sessions ```[.linenums] data = { userId, createdAt, sessionEncrypted } return [{ ip, iter, createdAt }, ...] ```/ #### POST /account/log-in ```[.linenums] data = { email, pwdEncrypted } return { userUuid, nickname, sessionPwd, createdAt } ```/ #### POST /account/log-out ```[.linenums] data = { userId, createdAt, sessionEncrypted } return { message } ```/ #### POST /account/log-out-from-all ```[.linenums] data = { userId, createdAt, sessionEncrypted } return { message } ```/ #### POST /account/check ```[.linenums] data = { email, nickname } return { email:true/false, nickname:true/false } ```/ #### POST /account/sign-up ```[.linenums] data = { email, name, nickname, salt, pwdEncrypted, } return { userUuid, nickname, sessionPwd, createdAt } ```/ ## RRA
  1. 코드잇 풀스택 2기 초급 프로젝트 (1팀): View My Startup
  2. kipid's blog :: prisma with PostgreSQL 를 배워봅시다.
728x90
반응형