Skip to content
Draft
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
3 changes: 3 additions & 0 deletions .env.local
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,6 @@ POSTGRES_HOST=localhost
POSTGRES_DATABASE=postgres
POSTGRES_USER=postgres
POSTGRES_PASSWORD=postgres
JDBC_URL=jdbc:postgresql://localhost:5432/postgres

LIQUIBASE_UPDATE_ENABLED=true
15 changes: 15 additions & 0 deletions changelog.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
<?xml version="1.0" encoding="UTF-8"?>
<databaseChangeLog
xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:ext="http://www.liquibase.org/xml/ns/dbchangelog-ext"
xmlns:pro="http://www.liquibase.org/xml/ns/pro"
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-latest.xsd
http://www.liquibase.org/xml/ns/dbchangelog-ext http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-ext.xsd
http://www.liquibase.org/xml/ns/pro http://www.liquibase.org/xml/ns/pro/liquibase-pro-latest.xsd">


<changeSet id="1" author="Mike Manley">
<sqlFile path="./scripts/init.sql" />
</changeSet>
</databaseChangeLog>
1 change: 0 additions & 1 deletion docker-compose.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ services:
restart: unless-stopped
volumes:
- db:/var/lib/postgresql/data
- ./scripts/init.sql:/docker-entrypoint-initdb.d/init.sql
environment:
POSTGRES_USER: postgres
POSTGRES_PASSWORD: postgres
Expand Down
12 changes: 12 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
"scripts": {
"start": "node dist/index.js",
"dev": "nodemon src/index.ts",
"build": "npx tsc",
"build": "npx tsc && cp ./changelog.xml ./dist/changelog.xml",
"watch": "src",
"ext": "ts",
"exec": "concurrently \"npx tsc --watch\" \"ts-node src/index.ts\""
Expand All @@ -17,6 +17,7 @@
"cors": "^2.8.5",
"dotenv": "^16.4.5",
"express": "^4.18.2",
"liquibase": "^4.4.0",
"morgan": "^1.10.0",
"pg": "^8.11.3"
},
Expand Down
2 changes: 1 addition & 1 deletion scripts/buildDockerImage.sh
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
#!/bin/sh
docker build -f ../Dockerfile.dev -t mmanle01/dogdb-service ../.
docker build -f ../Dockerfile.dev -t mmanle01/dogdb-service:0.2 -t mmanle01/dogdb-service:latest ../.
256 changes: 248 additions & 8 deletions scripts/init.sql

Large diffs are not rendered by default.

125 changes: 113 additions & 12 deletions src/DB/dogBreedProvider.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,68 @@
import * as db from './postgres';

interface DogBreedRepository {
getDogBreeds(): Promise<DogBreed[]>;
getDogBreedsByBreedIdLike(idLike: string): Promise<DogBreed[]>;
getDogBreeds(): Promise<DogBreedDto[]>;
getDogBreedsByBreedIdLike(idLike: string): Promise<DogBreedDto[]>;
}

interface DogBreed {
/**
* Represents a row of the dog_breed database table
*/
interface DogBreedRow {
id: string;
short_description: string;
long_description: string;
breed_characteristics: DogBreedCharacteristicRow[];
breed_images: BreedImageRow[];
}

/**
* Represents a row of the breed_characteristic database table
*/
interface DogBreedCharacteristicRow {
characteristic_pk: string
}

/**
* Represents a row of the breed_image database table
*/
interface BreedImageRow {
image_path: string;
alt_text: string;
}

/**
* This class is the Data Transfer Object (DTO) representation of a dog breed. This is intended to separate the database
* logic (such as Table or Class naming conventions and additional/extra columns) from the JSON reponse to be sent to and
* from the client.
*/
class DogBreedDto {
id: string;
shortDescription: string;
longDescription: string;
breedCharacteristics: string[];
breedImages: BreedImageDto[];

constructor(dogBreed: DogBreedRow) {
this.id = dogBreed.id
this.shortDescription = dogBreed.short_description
this.longDescription = dogBreed.long_description
this.breedCharacteristics = dogBreed.breed_characteristics?.map(c => c.characteristic_pk)
this.breedImages = dogBreed.breed_images?.map(img => { return new BreedImageDto(img.image_path, img.alt_text) })
if (this.breedImages == undefined) {
this.breedImages = []
}
}
}

class BreedImageDto {
imagePath: string;
altText: string;

constructor(imagePath: string, altText: string = "") {
this.imagePath = imagePath
this.altText = altText
}
}

export interface NewDogBreed {
Expand All @@ -20,37 +73,85 @@ export interface NewDogBreed {
}

class PostgresDogBreedProvider implements DogBreedRepository {
async getDogBreeds(): Promise<DogBreed[]> {
async getDogBreeds(): Promise<DogBreedDto[]> {
try {
return (await db.query('SELECT * FROM dog_breed', [])).rows;
const dogBreeds: DogBreedRow[] = (await db.query('SELECT * FROM dog_breed', [])).rows
await this.mapDogBreedRelations(dogBreeds);
return dogBreeds.map(d => new DogBreedDto(d))
} catch (error: any) {
throw new Error('Error fetching dog breeds: ' + error.message);
}
}

async getDogBreedsByBreedIdLike(idLike: string): Promise<DogBreed[]> {
/**
* This method takes a list of {@link DogBreedRow}s and executes the queries needed to fill out the relationships of this
* DogBreedRow entity. Currently queries for dog breed characteristics and breed images.
* @param dogBreeds The array of DogBreedRows
*/
private async mapDogBreedRelations(dogBreeds: DogBreedRow[]) {
await Promise.all(dogBreeds.map(async (breed) => {
breed.breed_characteristics = await this.getBreedCharacteristicsByBreedId(breed.id);
console.log(breed.breed_characteristics);
breed.breed_images = await this.getBreedImagesByBreedId(breed.id);
console.log(breed.breed_images);
}));
}

async getBreedCharacteristicsByBreedId(id: string): Promise<DogBreedCharacteristicRow[]> {
return (await db.query('SELECT characteristic_pk FROM breed_characteristic WHERE breed_id = $1', [id])).rows
}

async getBreedImagesByBreedId(id: string): Promise<BreedImageRow[]> {
return (await db.query('SELECT image_path FROM breed_image WHERE breed_id = $1', [id])).rows
}

async getDogBreedsByBreedIdLike(idLike: string): Promise<DogBreedDto[]> {
try {
return (
const dogBreeds: DogBreedRow[] = (
await db.query("select * from dog_breed db where db.id like ('%$1%')", [
idLike,
])
).rows;
await this.mapDogBreedRelations(dogBreeds);
return dogBreeds.map(d => new DogBreedDto(d))
} catch (error: any) {
throw new Error('Error fetching dog breeds: ' + error.message);
}
}

async createDogBreed(newDogBreed: NewDogBreed): Promise<void> {
/**
* Creates a dog breed. This method takes the DTO representation and inserts the appropriate database table rows
* within a transaction. If an exception occurs, the transaction will be rolled back.
* @param newDogBreed The DTO representation of the dog breed to save to the database.
* @returns Returns the {@link NewDogBreed} DTO representation of the created dog breed.
*/
async createDogBreed(newDogBreed: NewDogBreed): Promise<DogBreedDto> {
const client = await db.pool.connect();
try {
client.query("BEGIN")
const { id, shortDescription, longDescription, imagePath } = newDogBreed;
return (
await db.query(
'INSERT INTO dog_breed (id, short_description, long_description, image_path) VALUES ($1, $2, $3, $4) RETURNING *',
[id, shortDescription, longDescription, imagePath]
const dogBreed: DogBreedRow = (
await client.query(
'INSERT INTO dog_breed (id, short_description, long_description) VALUES ($1, $2, $3) RETURNING *',
[id, shortDescription, longDescription]
)
).rows[0];

const img: BreedImageRow = (
await client.query(
'INSERT INTO breed_image (image_path, breed_id) VALUES ($1, $2) RETURNING *',
[imagePath, id]
)
).rows[0];
const dogBreedDto = new DogBreedDto(dogBreed)
dogBreedDto.breedImages.push(new BreedImageDto(img.image_path))
client.query("COMMIT")
return dogBreedDto
} catch (error: any) {
client.query("ROLLBACK")
throw new Error('Error creating dog breed: ' + error.message);
} finally {
client.release()
}
}
}
Expand Down
56 changes: 56 additions & 0 deletions src/DB/liquibase.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import { Liquibase, LiquibaseConfig, LiquibaseLogLevels, POSTGRESQL_DEFAULT_CONFIG } from 'liquibase';
import { pool } from './postgres';
import dotenv from 'dotenv';
dotenv.config({ path: ['.env.local', '.env'] })

const myConfig: LiquibaseConfig = {
...POSTGRESQL_DEFAULT_CONFIG,
changeLogFile: './changelog.xml',
url: process.env.JDBC_URL || "",
username: process.env.POSTGRES_USER || "",
password: process.env.POSTGRES_PASSWORD || "",
liquibaseSchemaName: 'dogpedia',
logLevel: LiquibaseLogLevels.Warning,
}
const inst = new Liquibase(myConfig);

/**
* This function ensures that the schema exists on the database before then migrating
* the database schema with liquibase, if configured to migrate(update).
*/
export async function ensureDatabaseSchemaIsCorrect() {
const updatedSchemaResults = await updateSchemaIfNotExists();
console.log(updatedSchemaResults);

await runLiquibaseMigrationCommand();
}

/**
* This function runs liquibase update, if configured to do so, or liquibase "status" if not.
* Update function will run any change sets that haven't been applied to the database yet.
*
* The liquibase status function will indicate if the database is on the latest changeset or not.
*/
export async function runLiquibaseMigrationCommand() {
const shouldUpdate = process.env.LIQUIBASE_UPDATE_ENABLED || 'true'
if (shouldUpdate === 'true') {
console.log("Updating schema via liquibase")
await inst.update({});
} else {
inst.status()
}
}

/**
* This function adds the dogpedia schema if it does not already exist. This needs to happen
* before liquibase runs, as liquibase is expecting the schema to already exist on the database.
*
* @returns The results of the create schema command sent to the database.
*/
export const updateSchemaIfNotExists = async () => {
const start = Date.now()
console.log('attempt to update schema: ')
const res = await pool.query('CREATE SCHEMA IF NOT EXISTS dogpedia;')
console.log('Results: ', { rows: res.rowCount })
return res
}
4 changes: 2 additions & 2 deletions src/DB/postgres.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,13 @@ dotenv.config({ path: ['.env.local', '.env'] })
import { Client, Pool } from 'pg'


const pool = new Pool({
export const pool = new Pool({
host: process.env.POSTGRES_HOST,
user: process.env.POSTGRES_USER,
password: process.env.POSTGRES_PASSWORD,
database: process.env.POSTGRES_DATABASE,
})

export const query = async (text: string, params: any) => {
const start = Date.now()
const res = await pool.query(text, params)
Expand Down
20 changes: 16 additions & 4 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import morgan from 'morgan';
import cors from 'cors';
import bodyParser from 'body-parser';
import dogBreedProvider, { NewDogBreed } from './DB/dogBreedProvider';
import * as liquibase from './DB/liquibase';

const app: Express = express();
const port = process.env.PORT || 5000;
Expand All @@ -16,19 +17,30 @@ app.use(express.json());
app.use(bodyParser.urlencoded({ extended: true }));
app.use(bodyParser.json());

liquibase.ensureDatabaseSchemaIsCorrect();

app.get('/', (req: Request, res: Response) => {
res.send('Hello, World!');
});

app.get('/dog-breed', async (req: Request, res: Response) => {
const data = await dogBreedProvider.getDogBreeds();
res.status(200).send(data);
try {
const data = await dogBreedProvider.getDogBreeds();
res.status(200).send(data);
} catch (error: any) {
res.status(500).send("Error occurred! " + error.message)
}

});

app.post('/dog-breed', async (req: Request, res: Response) => {
const newDogBreed = req.body as NewDogBreed;
const createdDogBreed = await dogBreedProvider.createDogBreed(newDogBreed);
res.status(201).send(createdDogBreed);
try {
const createdDogBreed = await dogBreedProvider.createDogBreed(newDogBreed);
res.status(201).send(createdDogBreed);
} catch (error: any) {
res.status(400).send(error)
}
});

app.get('*', (req: Request, res: Response) => {
Expand Down
Loading