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