From 3aef0862ec966b587c4fc4dad6a1760590f31674 Mon Sep 17 00:00:00 2001 From: KevinMidboe Date: Sun, 3 Jan 2021 18:16:52 +0100 Subject: [PATCH] Setup for postgres database & schema for blogposts. --- api/database/index.js | 23 +++++ api/database/postgres.js | 121 +++++++++++++++++++++++ api/database/schemas/posts.sql | 7 ++ api/database/schemas/seed.sql | 6 ++ api/database/scripts/seedDatabase.js | 90 +++++++++++++++++ api/database/scripts/setupDatabase.js | 69 +++++++++++++ api/database/scripts/teardownDatabase.js | 50 ++++++++++ api/database/seeds/0001_posts.json | 8 ++ 8 files changed, 374 insertions(+) create mode 100644 api/database/index.js create mode 100644 api/database/postgres.js create mode 100644 api/database/schemas/posts.sql create mode 100644 api/database/schemas/seed.sql create mode 100644 api/database/scripts/seedDatabase.js create mode 100644 api/database/scripts/setupDatabase.js create mode 100644 api/database/scripts/teardownDatabase.js create mode 100644 api/database/seeds/0001_posts.json diff --git a/api/database/index.js b/api/database/index.js new file mode 100644 index 0000000..a4eab90 --- /dev/null +++ b/api/database/index.js @@ -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; diff --git a/api/database/postgres.js b/api/database/postgres.js new file mode 100644 index 0000000..dd61633 --- /dev/null +++ b/api/database/postgres.js @@ -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; diff --git a/api/database/schemas/posts.sql b/api/database/schemas/posts.sql new file mode 100644 index 0000000..9d4117e --- /dev/null +++ b/api/database/schemas/posts.sql @@ -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 +) diff --git a/api/database/schemas/seed.sql b/api/database/schemas/seed.sql new file mode 100644 index 0000000..43579bd --- /dev/null +++ b/api/database/schemas/seed.sql @@ -0,0 +1,6 @@ + +CREATE TABLE IF NOT EXISTS seed ( + seed_id serial PRIMARY KEY, + filename text, + run_date timestamp DEFAULT CURRENT_TIMESTAMP +); \ No newline at end of file diff --git a/api/database/scripts/seedDatabase.js b/api/database/scripts/seedDatabase.js new file mode 100644 index 0000000..e0320f9 --- /dev/null +++ b/api/database/scripts/seedDatabase.js @@ -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)); diff --git a/api/database/scripts/setupDatabase.js b/api/database/scripts/setupDatabase.js new file mode 100644 index 0000000..f60a9ff --- /dev/null +++ b/api/database/scripts/setupDatabase.js @@ -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; diff --git a/api/database/scripts/teardownDatabase.js b/api/database/scripts/teardownDatabase.js new file mode 100644 index 0000000..b6d8dea --- /dev/null +++ b/api/database/scripts/teardownDatabase.js @@ -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; diff --git a/api/database/seeds/0001_posts.json b/api/database/seeds/0001_posts.json new file mode 100644 index 0000000..4855abc --- /dev/null +++ b/api/database/seeds/0001_posts.json @@ -0,0 +1,8 @@ +[{ + "model": "posts", + "pk": 12, + "fields": { + "title": "Making A Raspberry Pi Grafana Monitor", + "markdown": "# testost" + } +}]