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