Skip to content

Commit

Permalink
Feat(envited.ascs.digital): add db connection (#15)
Browse files Browse the repository at this point in the history
  • Loading branch information
royscheeren authored Jan 8, 2024
2 parents 17b2b83 + 0920c6f commit 2aaea8a
Show file tree
Hide file tree
Showing 26 changed files with 1,841 additions and 429 deletions.
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -43,8 +43,9 @@ Thumbs.db
# Next.js
.next
.env
.env.dev
.env.development
.env.staging
.env.prod
.env.production

.nx
Expand Down
63 changes: 62 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,10 +1,32 @@
# ENVITED Marketplace

## Resources
* [Miro Board](https://miro.com/app/board/uXjVNeZRbEw=/)

- [Miro Board](https://miro.com/app/board/uXjVNeZRbEw=/)

### Prerequisites

- Node.js >= 18.18, [Installation instructions](https://github.com/nvm-sh/nvm)
- Postgres DB, for example using [Docker](https://hub.docker.com/_/postgres) automatically downloaded later

#### Installing dependencies

### Installation

#### Clone the repository

```bash
git clone [email protected]:ASCS-eV/envited-marketplace
```

#### Update repository

```bash
git fetch origin # get new branches
git status # check branch
git pull # update current branch
```

From the root directory run:

```bash
Expand All @@ -14,6 +36,45 @@ node --version
npm install
```

#### Database Connection

> The local database schema may change so you have to make sure to clean up by removing existing container and images just to be sure:
```bash
docker container ls --all
docker container remove envited
docker image ls
docker image remove postgres
```

At first you need to create a docker container. The only thing we need is the individual connection string of a running postgres db.
For example by using Docker, when running the Docker instance with the following example values:

```bash
docker run --name envited -p 5436:5432 \
-e POSTGRES_DB=envited \
-e POSTGRES_USER=admin \
-e POSTGRES_PASSWORD=123456 \
postgres
```

The connection string will in this case look like this:

```text
postgres://admin:123456@localhost:5436/envited
```

If you have completed the setup of the docker container before you can start it and check if the envited container is running:

```bash
docker start envited
docker container ls
```

#### Setting up the environment

In `apps/envited.ascs.digital` rename `.env.example` to `.env.development` and fill out the required values.

## Start the app

To start the development server run `npx nx serve envited.ascs.digital`. Open your browser and navigate to http://localhost:4200/. Happy coding!
14 changes: 12 additions & 2 deletions apps/envited.ascs.digital/.env.example
Original file line number Diff line number Diff line change
@@ -1,2 +1,12 @@
PLATFORM='' # Database platform thats being used. Choices are 'node', 'RDS' or 'vercel'
ROLE_TO_ASSUME=''
ENV='local'
REGION='eu-central-1'

# .env.development only
POSTGRES_HOST='localhost'
POSTGRES_PORT='5436'
POSTGRES_DATABASE_NAME='envited'
POSTGRES_DATABASE_USER='postgres'
POSTGRES_DATABASE_PASSWORD=''

# AWS deployments only
RDS_SECRET_ARN=''
7 changes: 5 additions & 2 deletions apps/envited.ascs.digital/app/api/hello/route.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
export async function GET(request: Request) {
import { db } from '../../../common/database/queries'

export async function GET(request: Request) {
try {
return Response.json('hello dynamic world')
const connection = await db()
const tables = await connection.fetchTables()
return Response.json(tables)
} catch (error) {
console.log('error', error)
return Response.json(error)
Expand Down
3 changes: 3 additions & 0 deletions apps/envited.ascs.digital/common/constants/errors.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export const ERRORS = {
CANNOT_CONNECT_TO_DATABASE: 'Cannot connect to database',
}
1 change: 1 addition & 0 deletions apps/envited.ascs.digital/common/constants/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { ERRORS } from './errors'
67 changes: 67 additions & 0 deletions apps/envited.ascs.digital/common/database/database.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import { initDb } from './database'

describe('common/database', () => {
describe('database', () => {
it('should should setup a connection with a local database', async () => {
// when ... we want to make a connection with a local db
// then ... we should get a connection as expected
process.env.ENV = 'local'
process.env.POSTGRES_DATABASE_NAME = 'DB_NAME'
process.env.POSTGRES_DATABASE_USER = 'DB_USER'
process.env.POSTGRES_DATABASE_PASSWORD = 'DB_PASSWORD'

const drizzle = jest.fn().mockReturnValue('DB_CONNECTION')
const postgres = jest.fn().mockReturnValue(jest.fn())
const getSecret = jest.fn().mockResolvedValue({})

const config = {
host: 'localhost',
port: 5432,
database: 'DB_NAME',
username: 'DB_USER',
password: 'DB_PASSWORD',
max: 1,
}

const db = await initDb({ drizzle, postgres, getSecret })()

expect(postgres).toHaveBeenCalledWith(config)
expect(drizzle).toHaveBeenCalledWith(postgres(config))
expect(getSecret).not.toHaveBeenCalled()
expect(db).toEqual('DB_CONNECTION')
})

it('should should setup a connection with a local remote database', async () => {
// when ... we want to make a connection with a remote db
// then ... we should get a connection as expected while using the secrets manager to get the credentials
process.env.ENV = 'staging'

const drizzle = jest.fn().mockReturnValue('DB_CONNECTION')
const postgres = jest.fn().mockReturnValue(jest.fn())
const getSecret = jest.fn().mockResolvedValue({
host: 'localhost',
port: 5432,
dbname: 'DB_NAME',
username: 'DB_USER',
password: 'DB_PASSWORD',
max: 1,
})

const config = {
host: 'localhost',
port: 5432,
database: 'DB_NAME',
username: 'DB_USER',
password: 'DB_PASSWORD',
max: 1,
}

const db = await initDb({ drizzle, postgres, getSecret })()

expect(postgres).toHaveBeenCalledWith(config)
expect(drizzle).toHaveBeenCalledWith(postgres(config))
expect(getSecret).toHaveBeenCalled()
expect(db).toEqual('DB_CONNECTION')
})
})
})
47 changes: 47 additions & 0 deletions apps/envited.ascs.digital/common/database/database.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import { PostgresJsDatabase, drizzle } from 'drizzle-orm/postgres-js'
import postgres from 'postgres'
import { equals } from 'ramda'

import { ERRORS } from '../constants'
import { getSecret } from '../secretsManager'

export const initDb =
({
drizzle,
postgres,
getSecret,
}: {
drizzle: (client: postgres.Sql) => PostgresJsDatabase
postgres: (options: postgres.Options<any>) => postgres.Sql
getSecret: (secretId: string) => Promise<Record<string, any>>
}): (() => Promise<PostgresJsDatabase>) =>
async () => {
let config = {
host: process.env.POSTGRES_HOST || 'localhost', // Postgres ip address[s] or domain name[s]
port: parseInt(process.env.POSTGRES_PORT || '5432', 10), // Postgres server port[s]
database: process.env.POSTGRES_DATABASE_NAME!, // Name of database to connect to
username: process.env.POSTGRES_DATABASE_USER!, // Username of database user
password: process.env.POSTGRES_DATABASE_PASSWORD!, // Password of database user
max: 1,
}

if (!equals(process.env.ENV, 'local')) {
try {
const { password, dbname, port, host, username } = await getSecret(process.env.RDS_SECRET_ARN!)
config = {
host,
port,
database: dbname,
username,
password,
max: 1,
}
} catch (error) {
console.log(ERRORS.CANNOT_CONNECT_TO_DATABASE, error)
}
}

return drizzle(postgres(config))
}

export const connectDb = initDb({ drizzle, postgres, getSecret })
1 change: 1 addition & 0 deletions apps/envited.ascs.digital/common/database/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { connectDb } from './database'
17 changes: 17 additions & 0 deletions apps/envited.ascs.digital/common/database/migrate.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { PostgresJsDatabase } from 'drizzle-orm/postgres-js'
import { migrate as PGMigrate } from 'drizzle-orm/postgres-js/migrator'

import { connectDb } from './database'

const runMigration = async () => {
const db = await connectDb()
try {
await PGMigrate(db as PostgresJsDatabase, { migrationsFolder: `../../drizzle/${process.env.ENV}` })
} catch (error) {
console.error(error)
} finally {
process.exit(0)
}
}

runMigration()
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { sql } from 'drizzle-orm'
import { PostgresJsDatabase } from 'drizzle-orm/postgres-js'

export const fetchTables = (db: PostgresJsDatabase) => async () =>
db.execute(sql`select * from information_schema.tables;`)
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { fetchTables } from './common'
1 change: 1 addition & 0 deletions apps/envited.ascs.digital/common/database/queries/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { db } from './queries'
18 changes: 18 additions & 0 deletions apps/envited.ascs.digital/common/database/queries/queries.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { init } from './queries'

describe('common/database/queries', () => {
describe('queries', () => {
it('should load the queries with an active db connection', async () => {
// when ... we want to load the queries
// then ... we should get the queries as expected
const connectDb = jest.fn().mockResolvedValue('DB_CONNECTION')
const testQuery = jest.fn().mockReturnValue('TEST_QUERY')

const db = await init(connectDb)({ testQuery: testQuery })()

expect(connectDb).toHaveBeenCalled()
expect(testQuery).toHaveBeenCalledWith('DB_CONNECTION')
expect(db).toEqual({ testQuery: 'TEST_QUERY' })
})
})
})
28 changes: 28 additions & 0 deletions apps/envited.ascs.digital/common/database/queries/queries.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { PostgresJsDatabase } from 'drizzle-orm/postgres-js'
import postgres from 'postgres'
import { fromPairs, map, pipe, toPairs } from 'ramda'

import { connectDb } from '../database'
import { fetchTables } from './common'

const queries = {
fetchTables,
}

export const init =
(connectDb: () => Promise<PostgresJsDatabase>) =>
(queries: Record<string, (db: PostgresJsDatabase) => () => Promise<postgres.RowList<Record<string, unknown>[]>>>) =>
async () => {
const connection = await connectDb()

return pipe(
toPairs,
map(([key, value]: [key: string, value: (connection: PostgresJsDatabase) => any]): [any, any] => [
key,
value(connection),
]),
fromPairs,
)(queries)
}

export const db = init(connectDb)(queries)
11 changes: 11 additions & 0 deletions apps/envited.ascs.digital/common/database/seed.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
const seed = async () => {
try {
// Insert seeding requirements here
} catch (error) {
console.error(error)
} finally {
process.exit(0)
}
}

seed()
1 change: 1 addition & 0 deletions apps/envited.ascs.digital/common/secretsManager/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { getSecret } from './secretsManager'
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { GetSecretValueCommand, SecretsManagerClient } from '@aws-sdk/client-secrets-manager'

export const getSecret = (secretId: string) =>
new SecretsManagerClient()
.send(new GetSecretValueCommand({ SecretId: secretId }))
.then(({ SecretString }) => (SecretString ? JSON.parse(SecretString) : {}))
.catch(error => console.log('error', error))
6 changes: 6 additions & 0 deletions apps/envited.ascs.digital/drizzle.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import type { Config } from 'drizzle-kit'

export default {
schema: './common/database/schema.ts',
out: `./drizzle/${process.env.ENV}`,
} satisfies Config
Loading

0 comments on commit 2aaea8a

Please sign in to comment.