From 270ac45e8c7c389103b2e38dc2e99de71fccd6fa Mon Sep 17 00:00:00 2001 From: "Daniel J. Summers" Date: Sat, 16 Sep 2017 13:16:02 -0500 Subject: [PATCH] Create tables/indexes on startup Ready to create a request --- src/20170104023341_InitialDb.fs | 87 --------------------------------- src/api/db/ddl.js | 70 ++++++++++++++++++++++++++ src/api/db/index.js | 7 +-- src/api/db/request.js | 2 +- src/api/index.js | 43 ++++------------ src/api/package.json | 1 + src/api/routes/index.js | 26 +++++++++- src/api/yarn.lock | 20 ++++++++ 8 files changed, 130 insertions(+), 126 deletions(-) delete mode 100644 src/20170104023341_InitialDb.fs create mode 100644 src/api/db/ddl.js diff --git a/src/20170104023341_InitialDb.fs b/src/20170104023341_InitialDb.fs deleted file mode 100644 index 575f0a7..0000000 --- a/src/20170104023341_InitialDb.fs +++ /dev/null @@ -1,87 +0,0 @@ -namespace MyPrayerJournal.Migrations - -open System -open System.Collections.Generic -open Microsoft.EntityFrameworkCore -open Microsoft.EntityFrameworkCore.Infrastructure -open Microsoft.EntityFrameworkCore.Metadata -open Microsoft.EntityFrameworkCore.Migrations -open Microsoft.EntityFrameworkCore.Migrations.Operations -open Microsoft.EntityFrameworkCore.Migrations.Operations.Builders -open MyPrayerJournal - -type RequestTable = { - RequestId : OperationBuilder - EnteredOn : OperationBuilder - UserId : OperationBuilder - } - -type HistoryTable = { - RequestId : OperationBuilder - AsOf : OperationBuilder - Status : OperationBuilder - Text : OperationBuilder - } - -[)>] -[] -type InitialDb () = - inherit Migration () - - override this.Up migrationBuilder = - migrationBuilder.EnsureSchema( - name = "mpj") - |> ignore - - migrationBuilder.CreateTable( - name = "Request", - schema = "mpj", - columns = - (fun table -> - { RequestId = table.Column(nullable = false) - EnteredOn = table.Column(nullable = false) - UserId = table.Column(nullable = false) - } - ), - constraints = - fun table -> - table.PrimaryKey("PK_Request", fun x -> x.RequestId :> obj) |> ignore - ) - |> ignore - - migrationBuilder.CreateTable( - name = "History", - schema = "mpj", - columns = - (fun table -> - { RequestId = table.Column(nullable = false) - AsOf = table.Column(nullable = false) - Status = table.Column(nullable = true) - Text = table.Column(nullable = true) - } - ), - constraints = - fun table -> - table.PrimaryKey("PK_History", fun x -> (x.RequestId, x.AsOf) :> obj) - |> ignore - table.ForeignKey( - name = "FK_History_Request_RequestId", - column = (fun x -> x.RequestId :> obj), - principalSchema = "mpj", - principalTable = "Request", - principalColumn = "RequestId", - onDelete = ReferentialAction.Cascade) - |> ignore - ) - |> ignore - - override this.Down migrationBuilder = - migrationBuilder.DropTable( - name = "History", - schema = "mpj") - |> ignore - - migrationBuilder.DropTable( - name = "Request", - schema = "mpj") - |> ignore diff --git a/src/api/db/ddl.js b/src/api/db/ddl.js new file mode 100644 index 0000000..5b6416e --- /dev/null +++ b/src/api/db/ddl.js @@ -0,0 +1,70 @@ +'use strict' + +const { Pool } = require('pg') + +/** + * SQL to check the existence of a table in the mpj schema + * @param {string} table The name of the table whose existence should be checked + */ +const tableSql = table => `SELECT 1 FROM pg_tables WHERE schemaname='mpj' AND tablename='${table}'` + +/** + * SQL to determine if an index exists + * @param {string} table The name of the table which the given index indexes + * @param {string} index The name of the index + */ +const indexSql = (table, index) => +`SELECT 1 FROM pg_indexes WHERE schemaname='mpj' AND tablename='${table}' AND indexname='${index}'` + +const ddl = [ + { + name: 'myPrayerJournal Schema', + check: `SELECT 1 FROM pg_namespace WHERE nspname='mpj'`, + fix: ` + CREATE SCHEMA mpj; + COMMENT ON SCHEMA mpj IS 'myPrayerJournal data'` + }, + { + name: 'request Table', + check: tableSql('request'), + fix: ` + CREATE TABLE mpj.request ( + "requestId" varchar(25) PRIMARY KEY, + "enteredOn" bigint NOT NULL, + "userId" varchar(100) NOT NULL); + COMMENT ON TABLE mpj.request IS 'Requests'` + }, + { + name: 'history Table', + check: tableSql('history'), + fix: ` + CREATE TABLE mpj.history ( + "requestId" varchar(25) NOT NULL REFERENCES mpj.request, + "asOf" bigint NOT NULL, + "status" varchar(25), + "text" text, + PRIMARY KEY ("requestId", "asOf")); + COMMENT ON TABLE mpj.history IS 'Request update history'` + }, + { + name: 'request.userId Index', + check: indexSql('request', 'idx_request_userId'), + fix: ` + CREATE INDEX "idx_request_userId" ON mpj.request ("userId"); + COMMENT ON INDEX "idx_request_userId" IS 'Requests are retrieved by user'` + } +] + +module.exports = query => { + return { + /** + * Ensure that the database schema, tables, and indexes exist + */ + ensureDatabase: async () => { + for (let item of ddl) { + const result = await query(item.check, []) + if (1 > result.rowCount) await query(item.fix, []) + } + } + } +} diff --git a/src/api/db/index.js b/src/api/db/index.js index 8c3116e..07f7f2c 100644 --- a/src/api/db/index.js +++ b/src/api/db/index.js @@ -1,9 +1,9 @@ 'use strict' const { Pool } = require('pg') -const config = require('../appsettings.json') -const pool = new Pool(config.pgPool) +/** Pooled PostgreSQL instance */ +const pool = new Pool(require('../appsettings.json').pgPool) /** * Run a SQL query @@ -14,5 +14,6 @@ const query = (text, params) => pool.query(text, params) module.exports = { query: query, - request: require('./request')(query) + request: require('./request')(query), + verify: require('./ddl')(query).ensureDatabase } diff --git a/src/api/db/request.js b/src/api/db/request.js index 0a664b1..0b3ba9e 100644 --- a/src/api/db/request.js +++ b/src/api/db/request.js @@ -10,6 +10,6 @@ module.exports = query => { * @return The requests that make up the current journal */ journal: async userId => - (await query('SELECT "RequestId" FROM "Request" WHERE "UserId" = $1', [userId])).rows + (await query('SELECT "requestId" FROM request WHERE "userId" = $1', [userId])).rows } } diff --git a/src/api/index.js b/src/api/index.js index 5831078..8d5fc99 100644 --- a/src/api/index.js +++ b/src/api/index.js @@ -3,34 +3,11 @@ const express = require('express') /** Configuration for the application */ -const config = require('./appsettings.json') +const appConfig = require('./appsettings.json') /** Express app */ const app = express() -const jwt = require('express-jwt') -const jwksRsa = require('jwks-rsa') - -// Authentication middleware. When used, the -// access token must exist and be verified against -// the Auth0 JSON Web Key Set -const checkJwt = jwt({ - // Dynamically provide a signing key - // based on the kid in the header and - // the singing keys provided by the JWKS endpoint. - secret: jwksRsa.expressJwtSecret({ - cache: true, - rateLimit: true, - jwksRequestsPerMinute: 5, - jwksUri: `https://${config.auth0.domain}/.well-known/jwks.json` - }), - - // Validate the audience and the issuer. - audience: config.auth0.clientId, - issuer: `https://${config.auth0.domain}/`, - algorithms: ['RS256'] -}) - // Serve the Vue files from /public app.use(express.static('public')) @@ -38,23 +15,21 @@ app.use(express.static('public')) app.use(require('morgan')('dev')) // Tie in all the API routes -require('./routes').mount(app, checkJwt) +require('./routes').mount(app) // Send the index.html file for what would normally get a 404 app.use(async (req, res, next) => { - const options = { - root: __dirname + '/public/', - dotfiles: 'deny' - } try { - await res.sendFile('index.html', options) + await res.sendFile('index.html', { root: __dirname + '/public/', dotfiles: 'deny' }) } catch (err) { return next(err) } }) -// Start it up! -app.listen(config.port, () => { - console.log(`Listening on port ${config.port}`) -}) +// Ensure the database exists... +require('./db').verify().then(() => + // ...and start it up! + app.listen(appConfig.port, () => { + console.log(`Listening on port ${appConfig.port}`) + })) diff --git a/src/api/package.json b/src/api/package.json index 6f5fd9d..66fe3a7 100644 --- a/src/api/package.json +++ b/src/api/package.json @@ -7,6 +7,7 @@ "author": "Daniel J. Summers ", "license": "MIT", "dependencies": { + "cuid": "^1.3.8", "express": "^4.15.4", "express-jwt": "^5.3.0", "express-promise-router": "^2.0.0", diff --git a/src/api/routes/index.js b/src/api/routes/index.js index 28c5b62..188f179 100644 --- a/src/api/routes/index.js +++ b/src/api/routes/index.js @@ -1,7 +1,31 @@ 'use strict' +const jwt = require('express-jwt') +const jwksRsa = require('jwks-rsa') +const config = require('../appsettings.json') + +// Authentication middleware. When used, the +// access token must exist and be verified against +// the Auth0 JSON Web Key Set +const checkJwt = jwt({ + // Dynamically provide a signing key + // based on the kid in the header and + // the singing keys provided by the JWKS endpoint. + secret: jwksRsa.expressJwtSecret({ + cache: true, + rateLimit: true, + jwksRequestsPerMinute: 5, + jwksUri: `https://${config.auth0.domain}/.well-known/jwks.json` + }), + + // Validate the audience and the issuer. + audience: config.auth0.clientId, + issuer: `https://${config.auth0.domain}/`, + algorithms: ['RS256'] +}) + module.exports = { - mount: (app, checkJwt) => { + mount: app => { app.use('/api/journal', require('./journal')(checkJwt)) } } diff --git a/src/api/yarn.lock b/src/api/yarn.lock index 4399873..96cf70f 100644 --- a/src/api/yarn.lock +++ b/src/api/yarn.lock @@ -109,6 +109,10 @@ boom@2.x.x: dependencies: hoek "2.x.x" +browser-fingerprint@0.0.1: + version "0.0.1" + resolved "https://registry.yarnpkg.com/browser-fingerprint/-/browser-fingerprint-0.0.1.tgz#8df3cdca25bf7d5b3542d61545d730053fce604a" + buffer-equal-constant-time@1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz#f8e71132f7ffe6e01a5c9697a4c6f3e48d5cc819" @@ -147,6 +151,10 @@ cookie@0.3.1: version "0.3.1" resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.3.1.tgz#e7e0a1f9ef43b4c8ba925c5c5a96e806d16873bb" +core-js@^1.1.1: + version "1.2.7" + resolved "https://registry.yarnpkg.com/core-js/-/core-js-1.2.7.tgz#652294c14651db28fa93bd2d5ff2983a4f08c636" + core-util-is@1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.2.tgz#b5fd54220aa2bc5ab57aab7140c940754503c1a7" @@ -157,6 +165,14 @@ cryptiles@2.x.x: dependencies: boom "2.x.x" +cuid@^1.3.8: + version "1.3.8" + resolved "https://registry.yarnpkg.com/cuid/-/cuid-1.3.8.tgz#4b875e0969bad764f7ec0706cf44f5fb0831f6b7" + dependencies: + browser-fingerprint "0.0.1" + core-js "^1.1.1" + node-fingerprint "0.0.2" + dashdash@^1.12.0: version "1.14.1" resolved "https://registry.yarnpkg.com/dashdash/-/dashdash-1.14.1.tgz#853cfa0f7cbe2fed5de20326b8dd581035f6e2f0" @@ -545,6 +561,10 @@ negotiator@0.6.1: version "0.6.1" resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-0.6.1.tgz#2b327184e8992101177b28563fb5e7102acd0ca9" +node-fingerprint@0.0.2: + version "0.0.2" + resolved "https://registry.yarnpkg.com/node-fingerprint/-/node-fingerprint-0.0.2.tgz#31cbabeb71a67ae7dd5a7dc042e51c3c75868501" + oauth-sign@~0.8.1: version "0.8.2" resolved "https://registry.yarnpkg.com/oauth-sign/-/oauth-sign-0.8.2.tgz#46a6ab7f0aead8deae9ec0565780b7d4efeb9d43"