Node API attempt #1

Merged
danieljsummers merged 4 commits from node-api-attempt into master 2017-09-21 23:47:59 +00:00
8 changed files with 130 additions and 126 deletions
Showing only changes of commit 270ac45e8c - Show all commits

View File

@ -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<AddColumnOperation>
EnteredOn : OperationBuilder<AddColumnOperation>
UserId : OperationBuilder<AddColumnOperation>
}
type HistoryTable = {
RequestId : OperationBuilder<AddColumnOperation>
AsOf : OperationBuilder<AddColumnOperation>
Status : OperationBuilder<AddColumnOperation>
Text : OperationBuilder<AddColumnOperation>
}
[<DbContext (typeof<DataContext>)>]
[<Migration "20170104023341_InitialDb">]
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<Guid>(nullable = false)
EnteredOn = table.Column<int64>(nullable = false)
UserId = table.Column<string>(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<Guid>(nullable = false)
AsOf = table.Column<int64>(nullable = false)
Status = table.Column<string>(nullable = true)
Text = table.Column<string>(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

70
src/api/db/ddl.js Normal file
View File

@ -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, [])
}
}
}
}

View File

@ -1,9 +1,9 @@
'use strict' 'use strict'
const { Pool } = require('pg') 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 * Run a SQL query
@ -14,5 +14,6 @@ const query = (text, params) => pool.query(text, params)
module.exports = { module.exports = {
query: query, query: query,
request: require('./request')(query) request: require('./request')(query),
verify: require('./ddl')(query).ensureDatabase
} }

View File

@ -10,6 +10,6 @@ module.exports = query => {
* @return The requests that make up the current journal * @return The requests that make up the current journal
*/ */
journal: async userId => 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
} }
} }

View File

@ -3,34 +3,11 @@
const express = require('express') const express = require('express')
/** Configuration for the application */ /** Configuration for the application */
const config = require('./appsettings.json') const appConfig = require('./appsettings.json')
/** Express app */ /** Express app */
const app = express() 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 // Serve the Vue files from /public
app.use(express.static('public')) app.use(express.static('public'))
@ -38,23 +15,21 @@ app.use(express.static('public'))
app.use(require('morgan')('dev')) app.use(require('morgan')('dev'))
// Tie in all the API routes // 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 // Send the index.html file for what would normally get a 404
app.use(async (req, res, next) => { app.use(async (req, res, next) => {
const options = {
root: __dirname + '/public/',
dotfiles: 'deny'
}
try { try {
await res.sendFile('index.html', options) await res.sendFile('index.html', { root: __dirname + '/public/', dotfiles: 'deny' })
} }
catch (err) { catch (err) {
return next(err) return next(err)
} }
}) })
// Start it up! // Ensure the database exists...
app.listen(config.port, () => { require('./db').verify().then(() =>
console.log(`Listening on port ${config.port}`) // ...and start it up!
}) app.listen(appConfig.port, () => {
console.log(`Listening on port ${appConfig.port}`)
}))

View File

@ -7,6 +7,7 @@
"author": "Daniel J. Summers <daniel@djs-consulting.com>", "author": "Daniel J. Summers <daniel@djs-consulting.com>",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"cuid": "^1.3.8",
"express": "^4.15.4", "express": "^4.15.4",
"express-jwt": "^5.3.0", "express-jwt": "^5.3.0",
"express-promise-router": "^2.0.0", "express-promise-router": "^2.0.0",

View File

@ -1,7 +1,31 @@
'use strict' '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 = { module.exports = {
mount: (app, checkJwt) => { mount: app => {
app.use('/api/journal', require('./journal')(checkJwt)) app.use('/api/journal', require('./journal')(checkJwt))
} }
} }

View File

@ -109,6 +109,10 @@ boom@2.x.x:
dependencies: dependencies:
hoek "2.x.x" 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: buffer-equal-constant-time@1.0.1:
version "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" 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" version "0.3.1"
resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.3.1.tgz#e7e0a1f9ef43b4c8ba925c5c5a96e806d16873bb" 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: core-util-is@1.0.2:
version "1.0.2" version "1.0.2"
resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.2.tgz#b5fd54220aa2bc5ab57aab7140c940754503c1a7" 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: dependencies:
boom "2.x.x" 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: dashdash@^1.12.0:
version "1.14.1" version "1.14.1"
resolved "https://registry.yarnpkg.com/dashdash/-/dashdash-1.14.1.tgz#853cfa0f7cbe2fed5de20326b8dd581035f6e2f0" resolved "https://registry.yarnpkg.com/dashdash/-/dashdash-1.14.1.tgz#853cfa0f7cbe2fed5de20326b8dd581035f6e2f0"
@ -545,6 +561,10 @@ negotiator@0.6.1:
version "0.6.1" version "0.6.1"
resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-0.6.1.tgz#2b327184e8992101177b28563fb5e7102acd0ca9" 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: oauth-sign@~0.8.1:
version "0.8.2" version "0.8.2"
resolved "https://registry.yarnpkg.com/oauth-sign/-/oauth-sign-0.8.2.tgz#46a6ab7f0aead8deae9ec0565780b7d4efeb9d43" resolved "https://registry.yarnpkg.com/oauth-sign/-/oauth-sign-0.8.2.tgz#46a6ab7f0aead8deae9ec0565780b7d4efeb9d43"