Skip to content

Commit

Permalink
Merge pull request #160 from lil-lab/connect-rds
Browse files Browse the repository at this point in the history
[recnet-api] Connect environment to rds database and generate DB schema
  • Loading branch information
joannechen1223 authored Mar 10, 2024
2 parents 8440d75 + d32a292 commit 2165f30
Show file tree
Hide file tree
Showing 20 changed files with 471 additions and 137 deletions.
5 changes: 4 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -55,4 +55,7 @@ Thumbs.db

# typescript
*.tsbuildinfo
next-env.d.ts
next-env.d.ts

# recnet-api specific
apps/**/pnpm-lock.yaml
2 changes: 1 addition & 1 deletion Procfile
Original file line number Diff line number Diff line change
@@ -1 +1 @@
web: pnpm nx deploy recnet-api
web: cd dist/apps/recnet-api && node main.js
29 changes: 29 additions & 0 deletions apps/recnet-api/buildspec.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
version: 0.2

phases:
install:
runtime-versions:
nodejs: 18
commands:
- npm i -g pnpm
- pnpm i --frozen-lockfile
pre_build:
commands:
- pnpm nx clean recnet-api
- pnpm nx prisma:generate recnet-api
build:
commands:
- pnpm nx build recnet-api
post_build:
commands:
- rm -rf node_modules
- cp -R apps/recnet-api/prisma/ dist/apps/recnet-api/prisma
- cd dist/apps/recnet-api
- pnpm install --prod --frozen-lockfile
- pnpx prisma generate --schema=prisma/schema.prisma

artifacts:
files:
- "dist/apps/recnet-api/**/*"
- "Procfile"
enable-symlinks: yes
102 changes: 102 additions & 0 deletions apps/recnet-api/prisma/migrations/20240306064817_init/migration.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
-- CreateEnum
CREATE TYPE "Provider" AS ENUM ('FACEBOOK', 'GOOGLE');

-- CreateEnum
CREATE TYPE "Role" AS ENUM ('ADMIN', 'USER');

-- CreateTable
CREATE TABLE "User" (
"id" VARCHAR(64) NOT NULL,
"provider" "Provider" NOT NULL,
"providerId" VARCHAR(128) NOT NULL,
"email" VARCHAR(128) NOT NULL,
"handle" VARCHAR(32) NOT NULL,
"displayName" VARCHAR(32) NOT NULL,
"inviteCode" VARCHAR(64),
"photoUrl" VARCHAR(256) NOT NULL,
"affiliation" VARCHAR(32),
"bio" TEXT,
"lastLoginAt" TIMESTAMP(3) NOT NULL,
"role" "Role" NOT NULL DEFAULT 'USER',
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,

CONSTRAINT "User_pkey" PRIMARY KEY ("id")
);

-- CreateTable
CREATE TABLE "FollowingRecord" (
"userId" VARCHAR(64) NOT NULL,
"followerId" VARCHAR(64) NOT NULL,
"createdAt" TIMESTAMP(3) NOT NULL,

CONSTRAINT "FollowingRecord_pkey" PRIMARY KEY ("userId","followerId")
);

-- CreateTable
CREATE TABLE "Recommendation" (
"id" VARCHAR(64) NOT NULL,
"userId" VARCHAR(64) NOT NULL,
"articleId" VARCHAR(64) NOT NULL,
"description" TEXT NOT NULL,
"cutoff" TIMESTAMP(3) NOT NULL,
"createAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updateAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,

CONSTRAINT "Recommendation_pkey" PRIMARY KEY ("id")
);

-- CreateTable
CREATE TABLE "Article" (
"id" VARCHAR(64) NOT NULL,
"doi" VARCHAR(32),
"title" VARCHAR(256) NOT NULL,
"author" VARCHAR(256) NOT NULL,
"link" VARCHAR(256) NOT NULL,
"year" SMALLINT NOT NULL,
"month" SMALLINT,
"isVerified" BOOLEAN NOT NULL DEFAULT false,
"createAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updateAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,

CONSTRAINT "Article_pkey" PRIMARY KEY ("id")
);

-- CreateTable
CREATE TABLE "InviteCode" (
"id" SERIAL NOT NULL,
"code" VARCHAR(64) NOT NULL,
"ownerId" VARCHAR(64) NOT NULL,
"issuedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"usedById" VARCHAR(64),
"usedAt" TIMESTAMP(3),

CONSTRAINT "InviteCode_pkey" PRIMARY KEY ("id")
);

-- CreateIndex
CREATE UNIQUE INDEX "User_email_key" ON "User"("email");

-- CreateIndex
CREATE UNIQUE INDEX "User_handle_key" ON "User"("handle");

-- CreateIndex
CREATE UNIQUE INDEX "InviteCode_usedById_key" ON "InviteCode"("usedById");

-- AddForeignKey
ALTER TABLE "FollowingRecord" ADD CONSTRAINT "FollowingRecord_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE;

-- AddForeignKey
ALTER TABLE "FollowingRecord" ADD CONSTRAINT "FollowingRecord_followerId_fkey" FOREIGN KEY ("followerId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE;

-- AddForeignKey
ALTER TABLE "Recommendation" ADD CONSTRAINT "Recommendation_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE;

-- AddForeignKey
ALTER TABLE "Recommendation" ADD CONSTRAINT "Recommendation_articleId_fkey" FOREIGN KEY ("articleId") REFERENCES "Article"("id") ON DELETE RESTRICT ON UPDATE CASCADE;

-- AddForeignKey
ALTER TABLE "InviteCode" ADD CONSTRAINT "InviteCode_ownerId_fkey" FOREIGN KEY ("ownerId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE;

-- AddForeignKey
ALTER TABLE "InviteCode" ADD CONSTRAINT "InviteCode_usedById_fkey" FOREIGN KEY ("usedById") REFERENCES "User"("id") ON DELETE SET NULL ON UPDATE CASCADE;
3 changes: 3 additions & 0 deletions apps/recnet-api/prisma/migrations/migration_lock.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# Please do not edit this file manually
# It should be added in your version-control system (i.e. Git)
provider = "postgresql"
99 changes: 99 additions & 0 deletions apps/recnet-api/prisma/schema.prisma
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
// This is your Prisma schema file,
// learn more about it in the docs: https://pris.ly/d/prisma-schema

// Looking for ways to speed up your queries, or scale easily with your serverless or edge functions?
// Try Prisma Accelerate: https://pris.ly/cli/accelerate-init

generator client {
provider = "prisma-client-js"
}

datasource db {
provider = "postgresql"
url = env("PRISMA_DATABASE_URL")
}

// Define enum types
enum Provider {
FACEBOOK
GOOGLE
// Add other providers here as needed
}

enum Role {
ADMIN
USER
}

model User {
id String @id @db.VarChar(64) @default(uuid()) // Primary key, UUID type
provider Provider // Enum type
providerId String @db.VarChar(128)
email String @db.VarChar(128) @unique
handle String @db.VarChar(32) @unique
displayName String @db.VarChar(32)
inviteCode String? @db.VarChar(64)
photoUrl String @db.VarChar(256)
affiliation String? @db.VarChar(32)
bio String?
lastLoginAt DateTime
role Role @default(USER) // Enum type
createdAt DateTime @default(now())
updatedAt DateTime @default(now()) @updatedAt
following FollowingRecord[] @relation("Following")
followers FollowingRecord[] @relation("Follower")
recommendations Recommendation[]
inviteCodeOwner InviteCode[] @relation("InviteCodeOwner")
inviteCodeUsed InviteCode? @relation("InviteCodeUsedBy")
}

model FollowingRecord {
userId String @db.VarChar(64)
followerId String @db.VarChar(64)
createdAt DateTime
@@id([userId, followerId])
user User @relation("Following", fields: [userId], references: [id])
follower User @relation("Follower", fields: [followerId], references: [id])
}

model Recommendation {
id String @id @db.VarChar(64) @default(uuid())
userId String @db.VarChar(64)
articleId String @db.VarChar(64)
description String
cutoff DateTime
createAt DateTime @default(now())
updateAt DateTime @default(now()) @updatedAt
user User @relation(fields: [userId], references: [id]) // User who made the recommendation
article Article @relation(fields: [articleId], references: [id]) // Article being recommended
}

model Article {
id String @id @db.VarChar(64) @default(uuid())
doi String? @db.VarChar(32)
title String @db.VarChar(256)
author String @db.VarChar(256)
link String @db.VarChar(256)
year Int @db.SmallInt
month Int? @db.SmallInt
isVerified Boolean @default(false)
createAt DateTime @default(now())
updateAt DateTime @default(now()) @updatedAt
recommendations Recommendation[]
}

model InviteCode {
id Int @id @default(autoincrement())
code String @db.VarChar(64)
ownerId String @db.VarChar(64)
issuedAt DateTime @default(now())
usedById String? @db.VarChar(64) @unique
usedAt DateTime?
owner User @relation("InviteCodeOwner", fields: [ownerId], references: [id])
usedBy User? @relation("InviteCodeUsedBy", fields: [usedById], references: [id])
}
23 changes: 22 additions & 1 deletion apps/recnet-api/project.json
Original file line number Diff line number Diff line change
Expand Up @@ -27,21 +27,42 @@
}
}
},
"prebuild": {
"clean": {
"executor": "nx:run-commands",
"options": {
"commands": ["rm -rf dist"],
"cwd": ".",
"forwardAllArgs": false
}
},
"prisma:generate": {
"executor": "nx:run-commands",
"options": {
"commands": [
"pnpx prisma generate --schema=apps/recnet-api/prisma/schema.prisma"
],
"cwd": ".",
"forwardAllArgs": false
}
},
"deploy": {
"executor": "nx:run-commands",
"options": {
"commands": ["node dist/apps/recnet-api/main.js"],
"cwd": ".",
"forwardAllArgs": false
}
},
"sync-package-lock": {
"executor": "nx:run-commands",
"options": {
"commands": [
"# TODO: delete this target once https://github.com/nrwl/nx/issues/21651 is fixed",
"cp pnpm-lock.yaml apps/recnet-api/"
],
"cwd": ".",
"forwardAllArgs": false
}
}
},
"tags": []
Expand Down
3 changes: 2 additions & 1 deletion apps/recnet-api/src/app.module.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import { Module } from "@nestjs/common";

import { HealthModule } from "./modules/health/health.module";
import { UserModule } from "./modules/user/user.module";

@Module({
imports: [HealthModule],
imports: [HealthModule, UserModule],
controllers: [],
providers: [],
})
Expand Down
53 changes: 53 additions & 0 deletions apps/recnet-api/src/database/prisma/prisma.connection.provider.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import { execSync } from "child_process";

import { Injectable, OnModuleInit } from "@nestjs/common";
import { PrismaClient } from "@prisma/client";

const postgresConfig = {
host: process.env.RDS_HOSTNAME,
port: parseInt(process.env.RDS_PORT, 10),
database: process.env.RDS_DB_NAME,
username: process.env.RDS_USERNAME,
password: process.env.RDS_PASSWORD,
};

@Injectable()
export class PrismaConnectionProvider
extends PrismaClient
implements OnModuleInit
{
private prismaUrl: string;

constructor() {
const connectionUrl = `postgresql://${postgresConfig.username}:${encodeURIComponent(
postgresConfig.password
)}@${postgresConfig.host}:${postgresConfig.port}/${
postgresConfig.database
}?schema=recnet`;

super({
datasources: {
db: {
url: `${connectionUrl}&pool_timeout=60`,
},
},
});

this.prismaUrl = connectionUrl;
}

async onModuleInit() {
await this.$connect();

const prismaSchema = process.env.PRISMA_SCHEMA || "prisma/schema.prisma";

if (process.env.DB_MIGRATE === "true") {
execSync(
`export PRISMA_DATABASE_URL=${this.prismaUrl} && pnpx prisma migrate deploy --schema=${prismaSchema}`,
{ stdio: "inherit" }
);
}
}
}

export default PrismaConnectionProvider;
9 changes: 9 additions & 0 deletions apps/recnet-api/src/database/prisma/prisma.module.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { Module } from "@nestjs/common";

import PrismaConnectionProvider from "./prisma.connection.provider";

@Module({
providers: [PrismaConnectionProvider],
exports: [PrismaConnectionProvider],
})
export class PrismaModule {}
11 changes: 11 additions & 0 deletions apps/recnet-api/src/database/repository/db.repository.module.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { Module } from "@nestjs/common";

import UserRepository from "./user.repository";
import { PrismaModule } from "../prisma/prisma.module";

@Module({
imports: [PrismaModule],
providers: [UserRepository],
exports: [UserRepository],
})
export class DbRepositoryModule {}
15 changes: 15 additions & 0 deletions apps/recnet-api/src/database/repository/user.repository.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { Injectable } from "@nestjs/common";
import { User } from "@prisma/client";

import PrismaConnectionProvider from "src/database/prisma/prisma.connection.provider";

@Injectable()
export default class UserRepository {
constructor(private readonly prisma: PrismaConnectionProvider) {}

public async findByHandle(handle: string): Promise<User> {
return this.prisma.user.findFirst({
where: { handle },
});
}
}
Loading

0 comments on commit 2165f30

Please sign in to comment.