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