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