Setup for postgres database & schema for blogposts.
This commit is contained in:
23
api/database/index.js
Normal file
23
api/database/index.js
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
const configuration = require(`${__base}/config/configuration`).getInstance();
|
||||||
|
const PostgresDatabase = require(`${__base}/database/postgres`);
|
||||||
|
|
||||||
|
const user = configuration.get("database", "user");
|
||||||
|
const password = configuration.get("database", "password");
|
||||||
|
const host = configuration.get("database", "host");
|
||||||
|
const dbName = configuration.get("database", "database");
|
||||||
|
|
||||||
|
let postgresDB = new PostgresDatabase(user, password, host, dbName);
|
||||||
|
|
||||||
|
postgresDB.connect();
|
||||||
|
|
||||||
|
class Database {
|
||||||
|
};
|
||||||
|
|
||||||
|
Database.connect = () => postgresDB.connect();
|
||||||
|
|
||||||
|
Database.query = (sql, values) => postgresDB.query(sql, values);
|
||||||
|
Database.update = (sql, values) => postgresDB.update(sql, values);
|
||||||
|
Database.get = (sql, values) => postgresDB.get(sql, values);
|
||||||
|
Database.all = (sql, values) => postgresDB.all(sql, values);
|
||||||
|
|
||||||
|
module.exports = Database;
|
||||||
121
api/database/postgres.js
Normal file
121
api/database/postgres.js
Normal file
@@ -0,0 +1,121 @@
|
|||||||
|
const logger = require(`${__base}/logger`);
|
||||||
|
const fs = require('fs');
|
||||||
|
const path = require('path');
|
||||||
|
const { Pool } = require('pg');
|
||||||
|
|
||||||
|
class PostgresDatabase {
|
||||||
|
constructor(user, password='', host, database) {
|
||||||
|
this.user = user;
|
||||||
|
this.password = password;
|
||||||
|
this.host = host;
|
||||||
|
this.database = database;
|
||||||
|
|
||||||
|
this.pool = new Pool({
|
||||||
|
user,
|
||||||
|
password,
|
||||||
|
host,
|
||||||
|
database,
|
||||||
|
port: 5432
|
||||||
|
});
|
||||||
|
|
||||||
|
// save queries to postgres in local list
|
||||||
|
// should prevent this from overflowing.
|
||||||
|
}
|
||||||
|
|
||||||
|
connect() {
|
||||||
|
return this.pool.connect();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Run a SQL query against the database and retrieve one row.
|
||||||
|
* @param {String} sql SQL query
|
||||||
|
* @param {Array} values in the SQL query
|
||||||
|
* @returns {Promise}
|
||||||
|
*/
|
||||||
|
query(sql, values) {
|
||||||
|
const start = Date.now();
|
||||||
|
|
||||||
|
return this.pool.query(sql, values)
|
||||||
|
.then(res => {
|
||||||
|
const duration = Date.now() - start;
|
||||||
|
logger.debug("Executed query", {
|
||||||
|
sql,
|
||||||
|
values,
|
||||||
|
duration,
|
||||||
|
rows: res.rowCount
|
||||||
|
});
|
||||||
|
|
||||||
|
return res.rowCount;
|
||||||
|
})
|
||||||
|
.catch(err => {
|
||||||
|
console.log('query db error:', err);
|
||||||
|
// TODO log db error
|
||||||
|
throw err;
|
||||||
|
})
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update w/ query and return true or false if update was successful.
|
||||||
|
* @param {String} sql SQL query
|
||||||
|
* @param {Array} values in the SQL query
|
||||||
|
* @returns {Promise}
|
||||||
|
*/
|
||||||
|
update(sql, values) {
|
||||||
|
const start = Date.now();
|
||||||
|
|
||||||
|
return this.pool.query(sql, values)
|
||||||
|
.then(res => {
|
||||||
|
const duration = Date.now() - start;
|
||||||
|
logger.debug("Executed query", {
|
||||||
|
sql,
|
||||||
|
values,
|
||||||
|
duration,
|
||||||
|
rows: res.rowCount
|
||||||
|
});
|
||||||
|
|
||||||
|
if (res.rowCount > 0) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
})
|
||||||
|
.catch(err => {
|
||||||
|
console.log(err)
|
||||||
|
// TODO log db error
|
||||||
|
throw err;
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Run a SQL query against the database and retrieve all the rows.
|
||||||
|
* @param {String} sql SQL query
|
||||||
|
* @param {Array} values in the SQL query
|
||||||
|
* @returns {Promise}
|
||||||
|
*/
|
||||||
|
all(sql, values) {
|
||||||
|
const start = Date.now();
|
||||||
|
|
||||||
|
return this.pool.query(sql, values)
|
||||||
|
.then(res => res.rows)
|
||||||
|
.catch(err => {
|
||||||
|
// TODO log db error
|
||||||
|
throw err;
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Run a SQL query against the database and retrieve one row.
|
||||||
|
* @param {String} sql SQL query
|
||||||
|
* @param {Array} values in the SQL query
|
||||||
|
* @returns {Promise}
|
||||||
|
*/
|
||||||
|
get(sql, values) {
|
||||||
|
return this.pool.query(sql, values)
|
||||||
|
.then(res => res.rows[0])
|
||||||
|
.catch(err => {
|
||||||
|
// TODO log db error
|
||||||
|
throw err;
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = PostgresDatabase;
|
||||||
7
api/database/schemas/posts.sql
Normal file
7
api/database/schemas/posts.sql
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
CREATE TABLE IF NOT EXISTS posts (
|
||||||
|
id serial PRIMARY KEY,
|
||||||
|
title text,
|
||||||
|
created timestamp DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
updated timestamp DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
markdown text
|
||||||
|
)
|
||||||
6
api/database/schemas/seed.sql
Normal file
6
api/database/schemas/seed.sql
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS seed (
|
||||||
|
seed_id serial PRIMARY KEY,
|
||||||
|
filename text,
|
||||||
|
run_date timestamp DEFAULT CURRENT_TIMESTAMP
|
||||||
|
);
|
||||||
90
api/database/scripts/seedDatabase.js
Normal file
90
api/database/scripts/seedDatabase.js
Normal file
@@ -0,0 +1,90 @@
|
|||||||
|
const path = require("path");
|
||||||
|
const fs = require("fs");
|
||||||
|
const fsPromises = require("fs/promises");
|
||||||
|
|
||||||
|
if (global.__base == undefined)
|
||||||
|
global.__base = path.join(__dirname, "../..");
|
||||||
|
|
||||||
|
const db = require("../index.js");
|
||||||
|
|
||||||
|
class SeedStep {
|
||||||
|
constructor(filepath) {
|
||||||
|
this.filepath = filepath;
|
||||||
|
this.filename = filepath.split("/").pop();
|
||||||
|
};
|
||||||
|
|
||||||
|
readData() {
|
||||||
|
this.data = JSON.parse(fs.readFileSync(this.filepath, 'utf-8'));
|
||||||
|
};
|
||||||
|
|
||||||
|
get isApplied() {
|
||||||
|
const query = `SELECT * FROM seed WHERE filename = $1`;
|
||||||
|
return db.query(query, [ this.filename ])
|
||||||
|
.then(resp => resp == 1 ? true : false)
|
||||||
|
}
|
||||||
|
|
||||||
|
commitStepToDb() {
|
||||||
|
const query = `INSERT INTO seed (filename) VALUES ($1)`;
|
||||||
|
return db.query(query, [ this.filename ]);
|
||||||
|
}
|
||||||
|
|
||||||
|
async applySeedData() {
|
||||||
|
if (await this.isApplied) {
|
||||||
|
console.log(`⚠️ Step: ${this.filename}, already applied.`);
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`Seeding ${this.filename}:`);
|
||||||
|
|
||||||
|
const seedSteps = this.data.map(data => {
|
||||||
|
const { model, pk, fields } = data;
|
||||||
|
const columns = Object.keys(fields);
|
||||||
|
const values = Object.values(fields);
|
||||||
|
const parameterKeys = Array.from({length: values.length}, (v, k) => `$${k + 1}`);
|
||||||
|
|
||||||
|
const query = `INSERT INTO ${ model }
|
||||||
|
(${ columns.join(',') })
|
||||||
|
VALUES
|
||||||
|
(${ parameterKeys.join(',') })`;
|
||||||
|
|
||||||
|
return db.query(query, values)
|
||||||
|
});
|
||||||
|
|
||||||
|
const table = this.data[0].model;
|
||||||
|
return Promise.all(seedSteps)
|
||||||
|
.then(objects => console.log(`🌱 ${objects.length} object(s) applied to ${ table }.`))
|
||||||
|
.then(_ => this.commitStepToDb());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* UTILS
|
||||||
|
*/
|
||||||
|
const readSeedFiles = () => {
|
||||||
|
const seedFolder = path.join(__base, "database/seeds/");
|
||||||
|
console.log(`Reading seeds from folder: ${seedFolder}\n`);
|
||||||
|
|
||||||
|
return fsPromises.readdir(seedFolder)
|
||||||
|
.then(files => files.map(filePath => {
|
||||||
|
const seedStep = new SeedStep(path.join(seedFolder, filePath));
|
||||||
|
seedStep.readData();
|
||||||
|
return seedStep;
|
||||||
|
}))
|
||||||
|
.catch(console.log)
|
||||||
|
};
|
||||||
|
|
||||||
|
const runAllSteps = (seedSteps) => {
|
||||||
|
return seedSteps.reduce(async (prevPromise, step) => {
|
||||||
|
await prevPromise;
|
||||||
|
return step.applySeedData();
|
||||||
|
}, Promise.resolve());
|
||||||
|
|
||||||
|
return Promise.all(promises);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Runner
|
||||||
|
*/
|
||||||
|
readSeedFiles()
|
||||||
|
.then(seedSteps => runAllSteps(seedSteps))
|
||||||
|
.finally(_ => process.exit(0));
|
||||||
69
api/database/scripts/setupDatabase.js
Normal file
69
api/database/scripts/setupDatabase.js
Normal file
@@ -0,0 +1,69 @@
|
|||||||
|
const path = require("path");
|
||||||
|
const fs = require("fs");
|
||||||
|
const fsPromises = require("fs/promises");
|
||||||
|
|
||||||
|
if (global.__base == undefined)
|
||||||
|
global.__base = path.join(__dirname, "../..");
|
||||||
|
|
||||||
|
const db = require("../index.js");
|
||||||
|
|
||||||
|
const posts = `posts.sql`;
|
||||||
|
const seed = `seed.sql`;
|
||||||
|
|
||||||
|
// TODO this is not used
|
||||||
|
const schemas = [
|
||||||
|
posts,
|
||||||
|
seed
|
||||||
|
];
|
||||||
|
|
||||||
|
|
||||||
|
const handleExit = (error=undefined) => {
|
||||||
|
if (error != undefined) {
|
||||||
|
console.log(`🚫 Exited with error: ${error}`);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log("✅ Exited setup successfully!");
|
||||||
|
process.exit(0);
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
const readSchemaFiles = () => {
|
||||||
|
const schemaFolder = path.join(__base, "database/schemas");
|
||||||
|
console.log("Reading schemas from folder:", schemaFolder);
|
||||||
|
|
||||||
|
return fsPromises.readdir(schemaFolder)
|
||||||
|
.then(files => files.map(filename => {
|
||||||
|
const filePath = path.join(schemaFolder, filename);
|
||||||
|
return fs.readFileSync(filePath, 'utf-8');
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
const applyAll = schemas => {
|
||||||
|
schemas = schemas.reverse();
|
||||||
|
|
||||||
|
return schemas.reduce(async (prevPromise, schema) => {
|
||||||
|
const tableName = schema.split("CREATE TABLE IF NOT EXISTS ").pop().split(" (")[0];
|
||||||
|
console.log(`✏️ Applying schema: ${tableName}`);
|
||||||
|
|
||||||
|
await prevPromise;
|
||||||
|
return db.query(schema);
|
||||||
|
}, Promise.resolve());
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Runner
|
||||||
|
*/
|
||||||
|
readSchemaFiles()
|
||||||
|
.then(schemas => applyAll(schemas))
|
||||||
|
.catch(err => handleExit(err))
|
||||||
|
.then(_ => process.exit(0))
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
// db.connect()
|
||||||
|
// .then(client => setup(client, schemas))
|
||||||
|
|
||||||
|
module.exports = db;
|
||||||
50
api/database/scripts/teardownDatabase.js
Normal file
50
api/database/scripts/teardownDatabase.js
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
const fs = require("fs");
|
||||||
|
const path = require("path");
|
||||||
|
|
||||||
|
if (global.__base == undefined)
|
||||||
|
global.__base = path.join(__dirname, "../..");
|
||||||
|
|
||||||
|
const db = require("../index.js");
|
||||||
|
|
||||||
|
const allTableNames = () => {
|
||||||
|
const sql = `
|
||||||
|
SELECT tablename
|
||||||
|
FROM pg_catalog.pg_tables
|
||||||
|
WHERE schemaname != 'pg_catalog' AND
|
||||||
|
schemaname != 'information_schema'
|
||||||
|
`;
|
||||||
|
|
||||||
|
return db.all(sql)
|
||||||
|
.then(rows => rows.map(row => row.tablename).reverse())
|
||||||
|
}
|
||||||
|
|
||||||
|
const teardown = (tableNames) => {
|
||||||
|
if (tableNames.length) {
|
||||||
|
console.log(`Tearing down tables:`)
|
||||||
|
console.log(` - ${tableNames.join("\n - ")}`)
|
||||||
|
|
||||||
|
const sql = `DROP TABLE IF EXISTS ${tableNames.join(",")}`;
|
||||||
|
return db.query(sql);
|
||||||
|
} else {
|
||||||
|
console.log("No tables left to drop.");
|
||||||
|
return Promise.resolve();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleExit = (error=undefined) => {
|
||||||
|
if (error != undefined) {
|
||||||
|
console.log(`🚫 Exited with error: ${error}`);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log("✅ Exited teardown successfully!");
|
||||||
|
process.exit(0);
|
||||||
|
};
|
||||||
|
|
||||||
|
db.connect()
|
||||||
|
.then(() => allTableNames())
|
||||||
|
.then(tableNames => teardown(tableNames))
|
||||||
|
.catch(console.log)
|
||||||
|
.finally(handleExit)
|
||||||
|
|
||||||
|
module.exports = db;
|
||||||
8
api/database/seeds/0001_posts.json
Normal file
8
api/database/seeds/0001_posts.json
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
[{
|
||||||
|
"model": "posts",
|
||||||
|
"pk": 12,
|
||||||
|
"fields": {
|
||||||
|
"title": "Making A Raspberry Pi Grafana Monitor",
|
||||||
|
"markdown": "# testost"
|
||||||
|
}
|
||||||
|
}]
|
||||||
Reference in New Issue
Block a user