Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[recnet-api] Connect environment to rds database and generate DB schema #160

Merged
merged 14 commits into from
Mar 10, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we also add down migration file down.sql for each up migration?

Docs: https://www.prisma.io/docs/orm/prisma-migrate/workflows/generating-down-migrations

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great idea, I will create another PR to add it.

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,
};
Comment on lines +6 to +12
Copy link
Collaborator

@swh00tw swh00tw Mar 8, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[nit]: you can use zod to parse the process.env. For example:

import { z } from "zod"
const PostgreConfigSchema = z.object({
    host: z.string(),
    port: z.number(),
    database: z.string(),
    username: z.string(),
    password: z.string(),
})

const postgresConfig = PostgreConfigSchema.parse(process.env)

The error message would be better since zod would tell you which field exactly is invalid or unexpected, instead of error like this PrismaClientInitializationError: The provided database string is invalid. Error parsing connection string: invalid port number in database URL. Please refer to the documentation in https://www.prisma.io/docs/reference/database-reference/connection-urls for constructing a correct connection string. In some cases, certain characters must be escaped. Please check the string for any illegal characters. It makes it easier to debug if any env var is missing.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same thing apply to process.env.DB_MIGRATE & process.env.PRISMA_SCHEMA. Personally, I always use zod to parse the whole env variables, use EnvSchema or something to prevent me directly accessing the process.env, it's error-prone and easy to have typos.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I tend to use the config service provided by nestJS to deal with these configs. I will create another PR to enhance the configuration management and will consider to take in this advice.


@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
Loading