From dd2980ea38c4c11d0e1aa1c950d72cdc46873ceb Mon Sep 17 00:00:00 2001 From: Wojciech Trocki Date: Tue, 8 Aug 2017 12:43:54 +0100 Subject: [PATCH 01/16] WIP WFM api --- demo/server/src/app.ts | 6 +--- demo/server/src/index.ts | 2 +- .../{sessionOpts.ts => SessionOptions.ts} | 0 demo/server/src/modules/datasync/Connector.ts | 6 +--- .../src/modules/datasync/MongoDataHandler.ts | 4 +-- .../{datasync/data => demo-data}/Workflows.ts | 3 +- .../data => demo-data}/Workorders.ts | 3 +- .../{datasync/data => demo-data}/index.ts | 0 demo/server/src/modules/index.ts | 21 +++++++++-- demo/server/src/modules/keycloak/index.ts | 4 +-- .../server/src/modules/passport-auth/index.ts | 2 +- .../src/modules/wfm-web-api/ApiConfig.ts | 6 ++++ .../src/modules/wfm-web-api/DataController.ts | 35 +++++++++++++++++++ .../src/modules/wfm-web-api/DataService.ts | 35 +++++++++++++++++++ demo/server/src/modules/wfm-web-api/README.md | 2 ++ demo/server/src/modules/wfm-web-api/index.ts | 28 +++++++++++++++ demo/server/src/util/config.ts | 2 +- 17 files changed, 137 insertions(+), 22 deletions(-) rename demo/server/src/modules/{sessionOpts.ts => SessionOptions.ts} (100%) rename demo/server/src/modules/{datasync/data => demo-data}/Workflows.ts (87%) rename demo/server/src/modules/{datasync/data => demo-data}/Workorders.ts (98%) rename demo/server/src/modules/{datasync/data => demo-data}/index.ts (100%) create mode 100644 demo/server/src/modules/wfm-web-api/ApiConfig.ts create mode 100644 demo/server/src/modules/wfm-web-api/DataController.ts create mode 100644 demo/server/src/modules/wfm-web-api/DataService.ts create mode 100644 demo/server/src/modules/wfm-web-api/README.md create mode 100644 demo/server/src/modules/wfm-web-api/index.ts diff --git a/demo/server/src/app.ts b/demo/server/src/app.ts index 72da3fc..6062e4f 100644 --- a/demo/server/src/app.ts +++ b/demo/server/src/app.ts @@ -10,7 +10,7 @@ import * as path from 'path'; import * as favicon from 'serve-favicon'; import { securityMiddleware, setupModules } from './modules'; import index from './user-routes/index'; -import appConfig from './util/config'; +import appConfig from './util/Config'; const app: express.Express = express(); const config = appConfig.getConfig(); @@ -51,9 +51,6 @@ app.set('view engine', 'hbs'); setupModules(app); app.use('/', index); -app.use('/test', securityMiddleware.protect(), index); -app.use('/testAdmin', securityMiddleware.protect('admin'), index); -app.use('/testUser', securityMiddleware.protect('admin'), index); app.use((req: express.Request, res: express.Response, next) => { const err: any = new Error('Not Found'); @@ -73,5 +70,4 @@ errHandler = (err: any, req: express.Request, res: express.Response, next: () => }; app.use(errHandler); - export default app; diff --git a/demo/server/src/index.ts b/demo/server/src/index.ts index 5a27494..6279115 100644 --- a/demo/server/src/index.ts +++ b/demo/server/src/index.ts @@ -3,7 +3,7 @@ * Module dependencies. */ import { BunyanLogger, getLogger, setLogger } from '@raincatcher/logger'; -import appConfig from './util/config'; +import appConfig from './util/Config'; const config = appConfig.getConfig(); setLogger(new BunyanLogger(config.bunyanConfig)); import * as http from 'http'; diff --git a/demo/server/src/modules/sessionOpts.ts b/demo/server/src/modules/SessionOptions.ts similarity index 100% rename from demo/server/src/modules/sessionOpts.ts rename to demo/server/src/modules/SessionOptions.ts diff --git a/demo/server/src/modules/datasync/Connector.ts b/demo/server/src/modules/datasync/Connector.ts index 24861da..7260ba4 100644 --- a/demo/server/src/modules/datasync/Connector.ts +++ b/demo/server/src/modules/datasync/Connector.ts @@ -1,8 +1,7 @@ import SyncServer, { SyncApi, SyncExpressMiddleware, SyncOptions } from '@raincatcher/datasync-cloud'; import { getLogger } from '@raincatcher/logger'; import * as Promise from 'bluebird'; -import appConfig from '../../util/config'; -import initData from './data'; +import appConfig from '../../util/Config'; import { GlobalMongoDataHandler } from './MongoDataHandler'; const sync = SyncServer; @@ -40,9 +39,6 @@ export function connect() { const handler = new GlobalMongoDataHandler(mongo); handler.initGlobalHandlers(); } - if (config.sync.seedDemoData) { - initData(mongo); - } return resolve({ mongo, redis }); }); }); diff --git a/demo/server/src/modules/datasync/MongoDataHandler.ts b/demo/server/src/modules/datasync/MongoDataHandler.ts index 05c4944..866482f 100644 --- a/demo/server/src/modules/datasync/MongoDataHandler.ts +++ b/demo/server/src/modules/datasync/MongoDataHandler.ts @@ -1,5 +1,5 @@ import { sync } from '@raincatcher/datasync-cloud'; -import { Db, ObjectID } from 'mongodb'; +import { Db } from 'mongodb'; /** * Initializes global mongodb data handlers for feedhenry sync @@ -64,7 +64,7 @@ export class GlobalMongoDataHandler { public setupHandleRead() { const self = this; sync.globalHandleRead(function(datasetId, uid, metadata, cb) { - self.db.collection(datasetId).findOne({ 'id': uid}) + self.db.collection(datasetId).findOne({ 'id': uid }) .then(function(result: any) { if (!result) { return cb(new Error('Missing result')); diff --git a/demo/server/src/modules/datasync/data/Workflows.ts b/demo/server/src/modules/demo-data/Workflows.ts similarity index 87% rename from demo/server/src/modules/datasync/data/Workflows.ts rename to demo/server/src/modules/demo-data/Workflows.ts index 7f52ff3..f5aa51d 100644 --- a/demo/server/src/modules/datasync/data/Workflows.ts +++ b/demo/server/src/modules/demo-data/Workflows.ts @@ -1,3 +1,4 @@ +import { getLogger } from '@raincatcher/logger'; import { Db } from 'mongodb'; const workflows = [ @@ -19,7 +20,7 @@ export default function(collectionName: string, database: Db) { if (count !== 0) { return; } - console.info('Saving workflows'); + getLogger().info('Saving workflows'); database.collection(collectionName).insertMany(workflows); }); } diff --git a/demo/server/src/modules/datasync/data/Workorders.ts b/demo/server/src/modules/demo-data/Workorders.ts similarity index 98% rename from demo/server/src/modules/datasync/data/Workorders.ts rename to demo/server/src/modules/demo-data/Workorders.ts index 50cbdf7..69e1c4f 100644 --- a/demo/server/src/modules/datasync/data/Workorders.ts +++ b/demo/server/src/modules/demo-data/Workorders.ts @@ -1,3 +1,4 @@ +import { getLogger } from '@raincatcher/logger'; import { Db } from 'mongodb'; const workorders = [ @@ -189,7 +190,7 @@ export default function(collectionName: string, database: Db) { workorder.startTimestamp = newDate.toDateString(); workorder.finishTimestamp = tomorrow.toDateString(); }); - console.info('Saving workorders'); + getLogger().info('Saving workorders'); database.collection(collectionName).insertMany(workorders); }); } diff --git a/demo/server/src/modules/datasync/data/index.ts b/demo/server/src/modules/demo-data/index.ts similarity index 100% rename from demo/server/src/modules/datasync/data/index.ts rename to demo/server/src/modules/demo-data/index.ts diff --git a/demo/server/src/modules/index.ts b/demo/server/src/modules/index.ts index 3daa88d..b13a1b7 100644 --- a/demo/server/src/modules/index.ts +++ b/demo/server/src/modules/index.ts @@ -1,11 +1,14 @@ import { EndpointSecurity } from '@raincatcher/auth-passport'; import { getLogger } from '@raincatcher/logger'; import * as express from 'express'; -import appConfig from '../util/config'; +import { Db } from 'mongodb'; +import appConfig from '../util/Config'; import { connect as syncConnector } from './datasync/Connector'; import { router as syncRouter } from './datasync/Router'; +import initData from './demo-data'; import { init as initKeycloak } from './keycloak'; import { init as authInit } from './passport-auth'; +import { buildApiRouter } from './wfm-web-api'; const config = appConfig.getConfig(); @@ -13,8 +16,14 @@ export let securityMiddleware: EndpointSecurity; // Setup all modules export function setupModules(app: express.Express) { - syncSetup(app); + const connectionPromise = syncSetup(app); securitySetup(app); + connectionPromise.then(function(mongo: Db) { + if (config.seedDemoData) { + initData(mongo); + } + apiSetup(app, mongo); + }); } function securitySetup(app: express.Express) { @@ -41,9 +50,15 @@ function syncSetup(app: express.Express) { // Mount api app.use('/sync', syncRouter); // Connect sync - syncConnector().then(function() { + return syncConnector().then(function(mongo: Db) { getLogger().info('Sync started'); + return mongo; }).catch(function(err: any) { getLogger().error('Failed to initialize sync', err); }); } + +function apiSetup(app: express.Express, db: Db) { + // Mount api + app.use('/api', buildApiRouter(db)); +} diff --git a/demo/server/src/modules/keycloak/index.ts b/demo/server/src/modules/keycloak/index.ts index 536318f..b9cd6a1 100644 --- a/demo/server/src/modules/keycloak/index.ts +++ b/demo/server/src/modules/keycloak/index.ts @@ -1,8 +1,8 @@ import * as express from 'express'; import * as session from 'express-session'; -import appConfig from '../../util/config'; -import sessionOpts from '../sessionOpts'; +import appConfig from '../../util/Config'; +import sessionOpts from '../SessionOptions'; // tslint:disable-next-line:no-var-requires const Keycloak = require('keycloak-connect'); diff --git a/demo/server/src/modules/passport-auth/index.ts b/demo/server/src/modules/passport-auth/index.ts index 2050244..cdf4ac4 100644 --- a/demo/server/src/modules/passport-auth/index.ts +++ b/demo/server/src/modules/passport-auth/index.ts @@ -1,7 +1,7 @@ import { PassportAuth, UserRepository, UserService } from '@raincatcher/auth-passport'; import { getLogger } from '@raincatcher/logger'; import * as express from 'express'; -import sessionOpts from '../sessionOpts'; +import sessionOpts from '../SessionOptions'; // Implementation for fetching and mapping user data import DemoUserRepository, { SampleUserService } from './DemoUserRepository'; diff --git a/demo/server/src/modules/wfm-web-api/ApiConfig.ts b/demo/server/src/modules/wfm-web-api/ApiConfig.ts new file mode 100644 index 0000000..c83046a --- /dev/null +++ b/demo/server/src/modules/wfm-web-api/ApiConfig.ts @@ -0,0 +1,6 @@ + +export const config = { + workorderApiName: 'workorders', + workflowApiName: 'workflows', + resultApiName: 'results' +}; diff --git a/demo/server/src/modules/wfm-web-api/DataController.ts b/demo/server/src/modules/wfm-web-api/DataController.ts new file mode 100644 index 0000000..566d82e --- /dev/null +++ b/demo/server/src/modules/wfm-web-api/DataController.ts @@ -0,0 +1,35 @@ +import { getLogger } from '@raincatcher/logger'; +import * as express from 'express'; +import { DataService } from './DataService'; + +export class DataController { + constructor(router: express.Router, service: DataService, readonly apiPrefix: string) { + getLogger().info('REST api initialization', apiPrefix); + this.buildRoutes(router, service, apiPrefix); + } + public buildRoutes(router: express.Router, service: DataService, apiPrefix: string) { + const idRoute = router.route(apiPrefix + '/:id'); + const objectRoute = router.route(apiPrefix + '/'); + + objectRoute.get(function(req: express.Request, res: express.Response) { + const objectList = service.list(); + res.json(objectList); + }); + + objectRoute.post(function(req: express.Request, res: express.Response) { + res.json(service.create(req.body)); + }); + + objectRoute.put(function(req: express.Request, res: express.Response) { + res.json(service.update(req.body)); + }); + + idRoute.get(function(req: express.Request, res: express.Response) { + res.json(service.get(req.params.id)); + }); + + idRoute.delete(function(req: express.Request, res: express.Response) { + res.json(service.delete(req.params.id)); + }); + } +} diff --git a/demo/server/src/modules/wfm-web-api/DataService.ts b/demo/server/src/modules/wfm-web-api/DataService.ts new file mode 100644 index 0000000..17b2864 --- /dev/null +++ b/demo/server/src/modules/wfm-web-api/DataService.ts @@ -0,0 +1,35 @@ +import { Db } from 'mongodb'; + +/** + * Service for performing data operations on mongodb database + */ +export class DataService { + + /** + * @param db - mongodb driver connection + * @param collectionName - name of the collection stored in mongodb + */ + constructor(readonly db: Db, readonly collectionName: string) { + } + + public list() { + return this.db.collection(this.collectionName).find({}); + } + + public get(id: string) { + return this.db.collection(this.collectionName).findOne({ id }); + } + + public create(object: any) { + return this.db.collection(this.collectionName).insertOne(object); + } + + public update(object: any) { + const id = object.id; + return this.db.collection(this.collectionName).updateOne({ id }, object); + } + + public delete(id: string) { + return this.db.collection(this.collectionName).deleteOne({ id }); + } +} diff --git a/demo/server/src/modules/wfm-web-api/README.md b/demo/server/src/modules/wfm-web-api/README.md new file mode 100644 index 0000000..fa01594 --- /dev/null +++ b/demo/server/src/modules/wfm-web-api/README.md @@ -0,0 +1,2 @@ +Express.js based api for WFM +Currently in demo (going to be extracted to separate module) diff --git a/demo/server/src/modules/wfm-web-api/index.ts b/demo/server/src/modules/wfm-web-api/index.ts new file mode 100644 index 0000000..04c0dcf --- /dev/null +++ b/demo/server/src/modules/wfm-web-api/index.ts @@ -0,0 +1,28 @@ + +import * as express from 'express'; +import { Db } from 'mongodb'; +import { config } from './ApiConfig'; +import { DataController } from './DataController'; +import { DataService } from './DataService'; + +/** + * Create RESTFULL API for fetching WFM objects from mongo database. + */ +export function buildApiRouter(db: Db) { + const router: express.Router = express.Router(); + // TODO Pagination https://github.com/expressjs/express-paginate + // TODO Wrap controller with security interface + router.route('/test').get(function(req: express.Request, res: express.Response) { + res.json({ test: 'test' }); + }); + const workorderService = new DataService(db, config.workorderApiName); + const workorderController = new DataController(router, workorderService, config.workorderApiName); + + const workflowService = new DataService(db, config.workflowApiName); + const workflowController = new DataController(router, workflowService, config.workflowApiName); + + const resultService = new DataService(db, config.resultApiName); + const resultController = new DataController(router, resultService, config.resultApiName); + + return router; +} diff --git a/demo/server/src/util/config.ts b/demo/server/src/util/config.ts index 009bd30..26a0386 100644 --- a/demo/server/src/util/config.ts +++ b/demo/server/src/util/config.ts @@ -41,9 +41,9 @@ export interface CloudAppConfig { // See bunyan.d.ts/LoggerOptions bunyanConfig: any; keycloakConfig: any; + seedDemoData: boolean; sync: { customDataHandlers: boolean; - seedDemoData: boolean }; } const appConfig: Config = new EnvironmentConfig(); From 218695e20a1435a1fc77270e9f70afcca867391a Mon Sep 17 00:00:00 2001 From: Wojciech Trocki Date: Wed, 9 Aug 2017 14:13:33 +0100 Subject: [PATCH 02/16] Remove db reference --- demo/server/src/modules/index.ts | 14 +++++++++----- .../src/modules/wfm-web-api/DataController.ts | 4 ++-- .../src/modules/wfm-web-api/DataService.ts | 13 ++++++++++--- demo/server/src/modules/wfm-web-api/index.ts | 16 ++++++---------- 4 files changed, 27 insertions(+), 20 deletions(-) diff --git a/demo/server/src/modules/index.ts b/demo/server/src/modules/index.ts index b13a1b7..3b863da 100644 --- a/demo/server/src/modules/index.ts +++ b/demo/server/src/modules/index.ts @@ -1,5 +1,6 @@ import { EndpointSecurity } from '@raincatcher/auth-passport'; import { getLogger } from '@raincatcher/logger'; +import * as Promise from 'bluebird'; import * as express from 'express'; import { Db } from 'mongodb'; import appConfig from '../util/Config'; @@ -18,11 +19,11 @@ export let securityMiddleware: EndpointSecurity; export function setupModules(app: express.Express) { const connectionPromise = syncSetup(app); securitySetup(app); - connectionPromise.then(function(mongo: Db) { + apiSetup(app, connectionPromise); + connectionPromise.then(function(data: any) { if (config.seedDemoData) { - initData(mongo); + initData(data.mongo); } - apiSetup(app, mongo); }); } @@ -58,7 +59,10 @@ function syncSetup(app: express.Express) { }); } -function apiSetup(app: express.Express, db: Db) { +function apiSetup(app: express.Express, connectionPromise: Promise) { + const router: express.Router = express.Router(); + // TODO Pagination https://github.com/expressjs/express-paginate + // TODO Wrap controller with security interface // Mount api - app.use('/api', buildApiRouter(db)); + app.use('/api', buildApiRouter(connectionPromise)); } diff --git a/demo/server/src/modules/wfm-web-api/DataController.ts b/demo/server/src/modules/wfm-web-api/DataController.ts index 566d82e..41b62a6 100644 --- a/demo/server/src/modules/wfm-web-api/DataController.ts +++ b/demo/server/src/modules/wfm-web-api/DataController.ts @@ -8,8 +8,8 @@ export class DataController { this.buildRoutes(router, service, apiPrefix); } public buildRoutes(router: express.Router, service: DataService, apiPrefix: string) { - const idRoute = router.route(apiPrefix + '/:id'); - const objectRoute = router.route(apiPrefix + '/'); + const idRoute = router.route('/' + apiPrefix + '/:id'); + const objectRoute = router.route('/' + apiPrefix + '/'); objectRoute.get(function(req: express.Request, res: express.Response) { const objectList = service.list(); diff --git a/demo/server/src/modules/wfm-web-api/DataService.ts b/demo/server/src/modules/wfm-web-api/DataService.ts index 17b2864..8292ea8 100644 --- a/demo/server/src/modules/wfm-web-api/DataService.ts +++ b/demo/server/src/modules/wfm-web-api/DataService.ts @@ -5,15 +5,22 @@ import { Db } from 'mongodb'; */ export class DataService { + private db: Db; + /** - * @param db - mongodb driver connection + * @param dbPromise - mongodb driver connection promise * @param collectionName - name of the collection stored in mongodb */ - constructor(readonly db: Db, readonly collectionName: string) { + constructor(dbPromise: Promise, readonly collectionName: string) { + const self = this; + dbPromise.then(function(data: any) { + self.db = data.mongo; + }); } public list() { - return this.db.collection(this.collectionName).find({}); + console.info('logging', this.collectionName); + return this.db.collection(this.collectionName).find({}).toArray(); } public get(id: string) { diff --git a/demo/server/src/modules/wfm-web-api/index.ts b/demo/server/src/modules/wfm-web-api/index.ts index 04c0dcf..253f965 100644 --- a/demo/server/src/modules/wfm-web-api/index.ts +++ b/demo/server/src/modules/wfm-web-api/index.ts @@ -1,28 +1,24 @@ +import * as Promise from 'bluebird'; import * as express from 'express'; -import { Db } from 'mongodb'; import { config } from './ApiConfig'; import { DataController } from './DataController'; import { DataService } from './DataService'; /** - * Create RESTFULL API for fetching WFM objects from mongo database. + * Create RESTfull API for fetching WFM objects from mongo database. */ -export function buildApiRouter(db: Db) { +export function buildApiRouter(dbPromise: Promise) { const router: express.Router = express.Router(); // TODO Pagination https://github.com/expressjs/express-paginate // TODO Wrap controller with security interface - router.route('/test').get(function(req: express.Request, res: express.Response) { - res.json({ test: 'test' }); - }); - const workorderService = new DataService(db, config.workorderApiName); + const workorderService = new DataService(dbPromise, config.workorderApiName); const workorderController = new DataController(router, workorderService, config.workorderApiName); - const workflowService = new DataService(db, config.workflowApiName); + const workflowService = new DataService(dbPromise, config.workflowApiName); const workflowController = new DataController(router, workflowService, config.workflowApiName); - const resultService = new DataService(db, config.resultApiName); + const resultService = new DataService(dbPromise, config.resultApiName); const resultController = new DataController(router, resultService, config.resultApiName); - return router; } From a4c72e35d43fd38681156574c27faacf41a11ac9 Mon Sep 17 00:00:00 2001 From: Wojciech Trocki Date: Wed, 9 Aug 2017 23:51:04 +0100 Subject: [PATCH 03/16] Moving code to module --- cloud/wfm-rest-api/README.md | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 cloud/wfm-rest-api/README.md diff --git a/cloud/wfm-rest-api/README.md b/cloud/wfm-rest-api/README.md new file mode 100644 index 0000000..804642f --- /dev/null +++ b/cloud/wfm-rest-api/README.md @@ -0,0 +1,3 @@ +# RainCatcher Api module + +Module used to From 6c4b310e4a35bcb78e6077fbf67a4a9fb9096268 Mon Sep 17 00:00:00 2001 From: Wojciech Trocki Date: Thu, 10 Aug 2017 11:27:58 +0100 Subject: [PATCH 04/16] Migration to separate module --- .travis.yml | 1 + cloud/wfm-rest-api/.gitignore | 4 + cloud/wfm-rest-api/README.md | 13 +- cloud/wfm-rest-api/package.json | 55 +++++++++ cloud/wfm-rest-api/src/ApiConfig.ts | 25 ++++ cloud/wfm-rest-api/src/data-api/ApiError.ts | 8 ++ .../src/data-api/CrudRepository.ts | 51 ++++++++ .../wfm-rest-api/src/data-api/PageRequest.ts | 39 ++++++ .../wfm-rest-api/src/data-api/PageResponse.ts | 19 +++ .../src/data-api/PaginationEngine.ts | 52 ++++++++ cloud/wfm-rest-api/src/impl/ApiController.ts | 112 ++++++++++++++++++ .../src/impl/MongoDbRepository.ts | 63 ++++++++++ cloud/wfm-rest-api/src/index.ts | 54 +++++++++ cloud/wfm-rest-api/test/mocha.opts | 3 + cloud/wfm-rest-api/tsconfig.json | 14 +++ demo/server/package.json | 1 + demo/server/src/modules/index.ts | 10 +- .../src/modules/wfm-web-api/ApiConfig.ts | 6 - .../src/modules/wfm-web-api/DataController.ts | 35 ------ .../src/modules/wfm-web-api/DataService.ts | 42 ------- demo/server/src/modules/wfm-web-api/README.md | 2 - demo/server/src/modules/wfm-web-api/index.ts | 24 ---- 22 files changed, 519 insertions(+), 114 deletions(-) create mode 100644 cloud/wfm-rest-api/.gitignore create mode 100644 cloud/wfm-rest-api/package.json create mode 100644 cloud/wfm-rest-api/src/ApiConfig.ts create mode 100644 cloud/wfm-rest-api/src/data-api/ApiError.ts create mode 100644 cloud/wfm-rest-api/src/data-api/CrudRepository.ts create mode 100644 cloud/wfm-rest-api/src/data-api/PageRequest.ts create mode 100644 cloud/wfm-rest-api/src/data-api/PageResponse.ts create mode 100644 cloud/wfm-rest-api/src/data-api/PaginationEngine.ts create mode 100644 cloud/wfm-rest-api/src/impl/ApiController.ts create mode 100644 cloud/wfm-rest-api/src/impl/MongoDbRepository.ts create mode 100644 cloud/wfm-rest-api/src/index.ts create mode 100644 cloud/wfm-rest-api/test/mocha.opts create mode 100644 cloud/wfm-rest-api/tsconfig.json delete mode 100644 demo/server/src/modules/wfm-web-api/ApiConfig.ts delete mode 100644 demo/server/src/modules/wfm-web-api/DataController.ts delete mode 100644 demo/server/src/modules/wfm-web-api/DataService.ts delete mode 100644 demo/server/src/modules/wfm-web-api/README.md delete mode 100644 demo/server/src/modules/wfm-web-api/index.ts diff --git a/.travis.yml b/.travis.yml index 2c8d99b..b078206 100644 --- a/.travis.yml +++ b/.travis.yml @@ -17,6 +17,7 @@ cache: - cloud/logger/node_modules - cloud/passportauth/node_modules - cloud/datasync/node_modules + - cloud/wfm-rest-api/node_modules - client/logger/node_modules - client/wfm/node_modules - client/datasync-client/node_modules diff --git a/cloud/wfm-rest-api/.gitignore b/cloud/wfm-rest-api/.gitignore new file mode 100644 index 0000000..79125be --- /dev/null +++ b/cloud/wfm-rest-api/.gitignore @@ -0,0 +1,4 @@ +# generated code +/**/*.map +/**/*.js +coverage_report/ diff --git a/cloud/wfm-rest-api/README.md b/cloud/wfm-rest-api/README.md index 804642f..83fed6f 100644 --- a/cloud/wfm-rest-api/README.md +++ b/cloud/wfm-rest-api/README.md @@ -1,3 +1,14 @@ # RainCatcher Api module -Module used to +Module used to expose express based api for WFM objects. + +### WFM speficic implementations + + +## Rest API + +## Framework + +### Pagination + +### Generic data service for simple CRUD operations diff --git a/cloud/wfm-rest-api/package.json b/cloud/wfm-rest-api/package.json new file mode 100644 index 0000000..9387068 --- /dev/null +++ b/cloud/wfm-rest-api/package.json @@ -0,0 +1,55 @@ +{ + "name": "@raincatcher/wfm-rest-api", + "version": "1.0.0", + "description": "Module used for building RESTFULL api on top of the WFM solution", + "types": "src/index.ts", + "author": "feedhenry-raincatcher@redhat.com", + "license": "Apache-2.0", + "main": "src/", + "scripts": { + "clean": "del coverage_report src/**/*.js src/**/*.map test/**/*.js test/**/*.map", + "build": "tsc", + "start": "ts-node src/index.ts", + "test": "npm run clean && nyc mocha" + }, + "nyc": { + "include": [ + "src/**/*.ts" + ], + "extension": [ + ".ts" + ], + "require": [ + "ts-node/register" + ], + "reporter": [ + "lcov", + "text" + ], + "report-dir": "coverage_report", + "check-coverage": true, + "lines": 75, + "functions": 100, + "branches": 80 + }, + "dependencies": { + "@raincatcher/logger": "1.0.0", + "bluebird": "^3.5.0", + "express": "^4.15.3", + "lodash": "^4.17.4" + }, + "devDependencies": { + "@types/bluebird": "^3.5.5", + "@types/express": "^4.0.35", + "@types/lodash": "^4.14.72", + "@types/mocha": "^2.2.41", + "@types/proxyquire": "^1.3.27", + "del-cli": "^1.0.0", + "mocha": "^3.4.2", + "nyc": "^11.0.1", + "proxyquire": "^1.8.0", + "source-map-support": "^0.4.15", + "ts-node": "^3.0.4", + "typescript": "^2.3.4" + } +} diff --git a/cloud/wfm-rest-api/src/ApiConfig.ts b/cloud/wfm-rest-api/src/ApiConfig.ts new file mode 100644 index 0000000..7cf061d --- /dev/null +++ b/cloud/wfm-rest-api/src/ApiConfig.ts @@ -0,0 +1,25 @@ + +/** + * Module configuration interface + * Holds all values that can be changed to modify module behavior + */ +export interface ApiConfig { + workorderApiName?: string; + workflowApiName?: string; + resultApiName?: string; + workorderCollectionName?: string; + workflowCollectionName?: string; + resultCollectionName?: string; +} + +/** + * Default module configuration + */ +export const defaultConfiguration = { + workorderApiName: 'workorders', + workflowApiName: 'workflows', + resultApiName: 'results', + workorderCollectionName: 'workorders', + workflowCollectionName: 'workflows', + resultCollectionName: 'results' +}; diff --git a/cloud/wfm-rest-api/src/data-api/ApiError.ts b/cloud/wfm-rest-api/src/data-api/ApiError.ts new file mode 100644 index 0000000..97e3136 --- /dev/null +++ b/cloud/wfm-rest-api/src/data-api/ApiError.ts @@ -0,0 +1,8 @@ + +/** + * Interface used to construct standarized response for api error handlers. + */ +export interface ApiError { + code: string; + message: string; +} diff --git a/cloud/wfm-rest-api/src/data-api/CrudRepository.ts b/cloud/wfm-rest-api/src/data-api/CrudRepository.ts new file mode 100644 index 0000000..d5db82c --- /dev/null +++ b/cloud/wfm-rest-api/src/data-api/CrudRepository.ts @@ -0,0 +1,51 @@ +import * as Promise from 'bluebird'; +import { Db } from 'mongodb'; +import { PageRequest } from '../data-api/PageRequest'; +import { PageResponse } from '../data-api/PageResponse'; + +/** + * Interface for building CRUD database repository. + * Clients should implement this interface to connect storage of WFM objects + * with different data storage engines like MySQL etc. + * Interface is being used internally to perform database operations for WFM models + * It's not recomended to use it for other application business logic. + */ +export interface CrudRepository { + + /** + * Retrieve list of results from database + * Supports pagination + * + * @param filter - filter for list (for example {id: 'user'}) + * @param request - page request for filter + * + * @see PageRequest + * @see PageResponse + */ + list(filter: any, request: PageRequest): Promise; + + /** + * Get specific item from database + * @param id - object id + */ + get(id: any): Promise; + + /** + * Store object in database + * @param object - object to store + */ + create(object: any): Promise; + + /** + * Update object + * @param object - object to update + * Note: id field of the object will be used to determine what should be updated + */ + update(object: any): Promise; + + /** + * Delete object from database + * @param id - object id + */ + delete(id: any): Promise; +} diff --git a/cloud/wfm-rest-api/src/data-api/PageRequest.ts b/cloud/wfm-rest-api/src/data-api/PageRequest.ts new file mode 100644 index 0000000..87c6863 --- /dev/null +++ b/cloud/wfm-rest-api/src/data-api/PageRequest.ts @@ -0,0 +1,39 @@ +/** + * Interface for pagination information. + * Represents page request (passed from client) + * + * @field page - zero-based page index. + * @field size - the size of the page to be returned. + */ +export interface PageRequest { + /** + * Requested page number + */ + page: number; + /** + * Total page size (numer of elements to return) + */ + size: number; +} + +/** + * Page request that also allows to define sort field and direction + */ +export interface SortedPageRequest extends PageRequest { + /** + * Name of the field to sort + */ + sort?: string; + /** + * Order of the sort direction + */ + order?: DIRECTION; +} + +/** + * Sorting directions + */ +export enum DIRECTION { + ASC = 1, + DESC = -1 +} diff --git a/cloud/wfm-rest-api/src/data-api/PageResponse.ts b/cloud/wfm-rest-api/src/data-api/PageResponse.ts new file mode 100644 index 0000000..e4c26ab --- /dev/null +++ b/cloud/wfm-rest-api/src/data-api/PageResponse.ts @@ -0,0 +1,19 @@ +/** + * Wrapper interface to return ordered and pagged list of items. + */ +export interface PageResponse { + /** + * Number of pages + */ + totalPages: number; + + /** + * Total number of elements + */ + totalCount: number; + + /** + * Represents returned data + */ + data: any[]; +} diff --git a/cloud/wfm-rest-api/src/data-api/PaginationEngine.ts b/cloud/wfm-rest-api/src/data-api/PaginationEngine.ts new file mode 100644 index 0000000..0622c3c --- /dev/null +++ b/cloud/wfm-rest-api/src/data-api/PaginationEngine.ts @@ -0,0 +1,52 @@ + +import { PageRequest } from '../data-api/PageRequest'; +import { PageResponse } from '../data-api/PageResponse'; + +/** + * Pagination procesor + * Clients may override this class to provide custom pagination parameters + * + * Note: pages are counted from number 1. + * Page 0 requests will return empty data. + */ +export class PaginationEngine { + + /** + * @param defaultPageSize Default page size added when parameter is missing + */ + constructor(readonly defaultPageSize: number) { + + } + /** + * @param query - list of arguments passed as http query parameters + */ + public buildRequestFromQuery(query: any): PageRequest { + let page; + let size; + if (query.size) { + size = query.size; + } else { + size = this.defaultPageSize; + } + if (query.page) { + page = query.page; + } else { + page = 1; + } + return { page, size }; + } + /** + * @param totalPages - list of pages available + * @param totalCount - total list of the elements + * @param data + */ + public buildResponse(totalPages: number, totalCount: number, data: any[]): PageResponse { + return { + totalPages, + totalCount, + data + }; + } +} + +export const defaultPaginationEngine = new PaginationEngine(10); diff --git a/cloud/wfm-rest-api/src/impl/ApiController.ts b/cloud/wfm-rest-api/src/impl/ApiController.ts new file mode 100644 index 0000000..a635bf0 --- /dev/null +++ b/cloud/wfm-rest-api/src/impl/ApiController.ts @@ -0,0 +1,112 @@ +import { getLogger } from '@raincatcher/logger'; +import * as express from 'express'; +import { ApiError } from '../data-api/ApiError'; +import { CrudRepository } from '../data-api/CrudRepository'; +import { defaultPaginationEngine } from '../data-api/PaginationEngine'; + +/** + * Generic controller that can be used to create API for specific objects + */ +export class ApiController { + constructor(router: express.Router, service: CrudRepository, readonly apiPrefix: string) { + getLogger().info('REST api initialization', apiPrefix); + this.buildRoutes(router, service, apiPrefix); + } + /** + * Build routes for specific element of api + * + * @param router - router used to attach api + * @param service - service to retrieve data + * @param apiPrefix - prefix to mount api in URI path. For example `/prefix/:id` + */ + public buildRoutes(router: express.Router, service: CrudRepository, apiPrefix: string) { + const idRoute = router.route('/' + apiPrefix + '/:id'); + const objectRoute = router.route('/' + apiPrefix + '/'); + + objectRoute.get(function(req: express.Request, res: express.Response) { + getLogger().debug('Api list method called', + { object: apiPrefix, body: req.query }); + + const page = defaultPaginationEngine.buildRequestFromQuery(req.query); + const objectList = service.list(req.query.filter, page).then(function(data) { + res.send(data); + }).catch(function(err) { + getLogger().error('List error', { err, obj: req.body }); + const error: ApiError = { code: 'DBError', message: 'Failed to list objects' }; + res.status(500).json(error); + }); + }); + + objectRoute.post(function(req: express.Request, res: express.Response) { + getLogger().debug('Api create method called', + { object: apiPrefix, body: req.body }); + + if (!req.body) { + const error: ApiError = { code: 'MissingData', message: 'Missing request body' }; + return res.status(400).json(error); + } + + service.create(req.body).then(function() { + res.send(); + }).catch(function(err) { + getLogger().error('Create error', { err, obj: req.body }); + const error: ApiError = { code: 'DBError', message: 'Failed to save object' }; + res.status(500).json(error); + }); + }); + + objectRoute.put(function(req: express.Request, res: express.Response) { + getLogger().debug('Api update method called', + { object: apiPrefix, body: req.body }); + + if (!req.body) { + const error: ApiError = { code: 'MissingData', message: 'Missing request body' }; + return res.status(400).json(error); + } + + service.update(req.body).then(function(data) { + res.send(); + }).catch(function(err) { + getLogger().error('Update error', { err, obj: req.body }); + const error: ApiError = { code: 'DBError', message: 'Failed to update object' }; + res.status(500).json(error); + }); + }); + + idRoute.get(function(req: express.Request, res: express.Response) { + getLogger().debug('Api get method called', + { object: apiPrefix, params: req.params }); + + if (!req.params.id) { + const error: ApiError = { code: 'MissingId', message: 'Missing id parameter' }; + return res.status(400).json(error); + } + + service.get(req.params.id).then(function(data) { + res.send(data); + }).catch(function(err) { + getLogger().error('Get error', { err, obj: req.body }); + const error: ApiError = { code: 'DBError', message: 'Failed to get object' }; + res.status(500).json(error); + }); + }); + + idRoute.delete(function(req: express.Request, res: express.Response) { + getLogger().debug('Api delete method called', + { object: apiPrefix, params: req.params }); + + if (!req.params.id) { + const error: ApiError = { code: 'MissingId', message: 'Missing id parameter' }; + return res.status(400).json(error); + } + + service.delete(req.params.id).then(function(data) { + res.send(data); + }).catch(function(err) { + getLogger().error('Delete error', { err, obj: req.body }); + const error: ApiError = { code: 'DBError', message: 'Failed to delete object' }; + res.status(500).json(error); + }); + }); + } +} diff --git a/cloud/wfm-rest-api/src/impl/MongoDbRepository.ts b/cloud/wfm-rest-api/src/impl/MongoDbRepository.ts new file mode 100644 index 0000000..5e4f57c --- /dev/null +++ b/cloud/wfm-rest-api/src/impl/MongoDbRepository.ts @@ -0,0 +1,63 @@ +import * as Promise from 'bluebird'; +import { Db } from 'mongodb'; +import { CrudRepository } from '../data-api/CrudRepository'; +import { PageRequest } from '../data-api/PageRequest'; +import { PageResponse } from '../data-api/PageResponse'; + +/** + * Service for performing data operations on mongodb database + */ +export class MongoDbRepository implements CrudRepository { + + private db: Db; + + /** + * @param collectionName - name of the collection stored in mongodb + */ + constructor(readonly collectionName: string) { + const self = this; + } + + public list(filter: any, request: PageRequest): Promise { + if (!this.db) { + Promise.reject('Db not intialized'); + } + return this.db.collection(this.collectionName).find(filter); + } + + public get(id: string) { + if (!this.db) { + Promise.reject('Db not intialized'); + } + return this.db.collection(this.collectionName).findOne({ id }); + } + + public create(object: any) { + if (!this.db) { + Promise.reject('Db not intialized'); + } + return this.db.collection(this.collectionName).insertOne(object); + } + + public update(object: any) { + if (!this.db) { + Promise.reject('Db not intialized'); + } + const id = object.id; + return this.db.collection(this.collectionName).updateOne({ id }, object); + } + + public delete(id: string) { + if (!this.db) { + Promise.reject('Db not intialized'); + } + return this.db.collection(this.collectionName).deleteOne({ id }); + } + + /** + * Inject database connection + */ + public setDb(db: Db) { + this.db = db; + } +} diff --git a/cloud/wfm-rest-api/src/index.ts b/cloud/wfm-rest-api/src/index.ts new file mode 100644 index 0000000..b862e4f --- /dev/null +++ b/cloud/wfm-rest-api/src/index.ts @@ -0,0 +1,54 @@ + +import * as Promise from 'bluebird'; +import * as express from 'express'; +import * as _ from 'lodash'; +import { Db } from 'mongodb'; +import { ApiConfig, defaultConfiguration } from './ApiConfig'; +import { ApiController } from './impl/ApiController'; +import { MongoDbRepository } from './impl/MongoDbRepository'; + +/** + * RESTfull api implementation for Workorders, Workflows and Results (WFM objects) + */ +export class WfmRestApi { + private config; + private workorderService: MongoDbRepository; + private workflowService: MongoDbRepository; + private resultService: MongoDbRepository; + + constructor(userConfig?: ApiConfig) { + this.config = _.defaults(defaultConfiguration, userConfig); + } + + /** + * Create new router for hosting WFM http api. + */ + public createWFMRouter() { + this.createWFMServices(); + const router: express.Router = express.Router(); + const workorderController = new ApiController(router, this.workorderService, this.config.workorderApiName); + const workflowController = new ApiController(router, this.workflowService, this.config.workflowApiName); + const resultController = new ApiController(router, this.resultService, this.config.resultApiName); + return router; + } + + /** + * Inject database connection to services + * + * @param db - mongodb driver + */ + public setDb(db: Db) { + this.workorderService.setDb(db); + this.workflowService.setDb(db); + this.resultService.setDb(db); + } + + protected createWFMServices() { + this.workorderService = new MongoDbRepository(this.config.workorderCollectionName); + this.workflowService = new MongoDbRepository(this.config.workflowApiName); + this.resultService = new MongoDbRepository(this.config.resultApiName); + } +} + +export * from './data-api/PageRequest'; +export * from './data-api/PageResponse'; diff --git a/cloud/wfm-rest-api/test/mocha.opts b/cloud/wfm-rest-api/test/mocha.opts new file mode 100644 index 0000000..b809cdb --- /dev/null +++ b/cloud/wfm-rest-api/test/mocha.opts @@ -0,0 +1,3 @@ +--compilers ts:ts-node/register +--require source-map-support/register +test/**.ts diff --git a/cloud/wfm-rest-api/tsconfig.json b/cloud/wfm-rest-api/tsconfig.json new file mode 100644 index 0000000..4248be2 --- /dev/null +++ b/cloud/wfm-rest-api/tsconfig.json @@ -0,0 +1,14 @@ +{ + "compilerOptions": { + "module": "commonjs", + "target": "es5", + "noImplicitAny": false, + "experimentalDecorators": true, + "strictNullChecks": true, + "sourceMap": true + }, + "include": [ + "src/", + "test/" + ] +} diff --git a/demo/server/package.json b/demo/server/package.json index 365aef0..c44c349 100644 --- a/demo/server/package.json +++ b/demo/server/package.json @@ -58,6 +58,7 @@ "typescript": "^2.3.4" }, "dependencies": { + "@raincatcher/wfm-rest-api": "1.0.0", "@raincatcher/auth-passport": "1.0.0", "@raincatcher/datasync-cloud": "1.0.0", "@raincatcher/logger": "1.0.0", diff --git a/demo/server/src/modules/index.ts b/demo/server/src/modules/index.ts index 3b863da..9a61223 100644 --- a/demo/server/src/modules/index.ts +++ b/demo/server/src/modules/index.ts @@ -1,5 +1,6 @@ import { EndpointSecurity } from '@raincatcher/auth-passport'; import { getLogger } from '@raincatcher/logger'; +import { WfmRestApi } from '@raincatcher/wfm-rest-api'; import * as Promise from 'bluebird'; import * as express from 'express'; import { Db } from 'mongodb'; @@ -9,7 +10,6 @@ import { router as syncRouter } from './datasync/Router'; import initData from './demo-data'; import { init as initKeycloak } from './keycloak'; import { init as authInit } from './passport-auth'; -import { buildApiRouter } from './wfm-web-api'; const config = appConfig.getConfig(); @@ -61,8 +61,10 @@ function syncSetup(app: express.Express) { function apiSetup(app: express.Express, connectionPromise: Promise) { const router: express.Router = express.Router(); - // TODO Pagination https://github.com/expressjs/express-paginate - // TODO Wrap controller with security interface // Mount api - app.use('/api', buildApiRouter(connectionPromise)); + const api = new WfmRestApi(); + app.use('/api', api.createWFMRouter()); + connectionPromise.then(function(db: Db) { + api.setDb(db); + }); } diff --git a/demo/server/src/modules/wfm-web-api/ApiConfig.ts b/demo/server/src/modules/wfm-web-api/ApiConfig.ts deleted file mode 100644 index c83046a..0000000 --- a/demo/server/src/modules/wfm-web-api/ApiConfig.ts +++ /dev/null @@ -1,6 +0,0 @@ - -export const config = { - workorderApiName: 'workorders', - workflowApiName: 'workflows', - resultApiName: 'results' -}; diff --git a/demo/server/src/modules/wfm-web-api/DataController.ts b/demo/server/src/modules/wfm-web-api/DataController.ts deleted file mode 100644 index 41b62a6..0000000 --- a/demo/server/src/modules/wfm-web-api/DataController.ts +++ /dev/null @@ -1,35 +0,0 @@ -import { getLogger } from '@raincatcher/logger'; -import * as express from 'express'; -import { DataService } from './DataService'; - -export class DataController { - constructor(router: express.Router, service: DataService, readonly apiPrefix: string) { - getLogger().info('REST api initialization', apiPrefix); - this.buildRoutes(router, service, apiPrefix); - } - public buildRoutes(router: express.Router, service: DataService, apiPrefix: string) { - const idRoute = router.route('/' + apiPrefix + '/:id'); - const objectRoute = router.route('/' + apiPrefix + '/'); - - objectRoute.get(function(req: express.Request, res: express.Response) { - const objectList = service.list(); - res.json(objectList); - }); - - objectRoute.post(function(req: express.Request, res: express.Response) { - res.json(service.create(req.body)); - }); - - objectRoute.put(function(req: express.Request, res: express.Response) { - res.json(service.update(req.body)); - }); - - idRoute.get(function(req: express.Request, res: express.Response) { - res.json(service.get(req.params.id)); - }); - - idRoute.delete(function(req: express.Request, res: express.Response) { - res.json(service.delete(req.params.id)); - }); - } -} diff --git a/demo/server/src/modules/wfm-web-api/DataService.ts b/demo/server/src/modules/wfm-web-api/DataService.ts deleted file mode 100644 index 8292ea8..0000000 --- a/demo/server/src/modules/wfm-web-api/DataService.ts +++ /dev/null @@ -1,42 +0,0 @@ -import { Db } from 'mongodb'; - -/** - * Service for performing data operations on mongodb database - */ -export class DataService { - - private db: Db; - - /** - * @param dbPromise - mongodb driver connection promise - * @param collectionName - name of the collection stored in mongodb - */ - constructor(dbPromise: Promise, readonly collectionName: string) { - const self = this; - dbPromise.then(function(data: any) { - self.db = data.mongo; - }); - } - - public list() { - console.info('logging', this.collectionName); - return this.db.collection(this.collectionName).find({}).toArray(); - } - - public get(id: string) { - return this.db.collection(this.collectionName).findOne({ id }); - } - - public create(object: any) { - return this.db.collection(this.collectionName).insertOne(object); - } - - public update(object: any) { - const id = object.id; - return this.db.collection(this.collectionName).updateOne({ id }, object); - } - - public delete(id: string) { - return this.db.collection(this.collectionName).deleteOne({ id }); - } -} diff --git a/demo/server/src/modules/wfm-web-api/README.md b/demo/server/src/modules/wfm-web-api/README.md deleted file mode 100644 index fa01594..0000000 --- a/demo/server/src/modules/wfm-web-api/README.md +++ /dev/null @@ -1,2 +0,0 @@ -Express.js based api for WFM -Currently in demo (going to be extracted to separate module) diff --git a/demo/server/src/modules/wfm-web-api/index.ts b/demo/server/src/modules/wfm-web-api/index.ts deleted file mode 100644 index 253f965..0000000 --- a/demo/server/src/modules/wfm-web-api/index.ts +++ /dev/null @@ -1,24 +0,0 @@ - -import * as Promise from 'bluebird'; -import * as express from 'express'; -import { config } from './ApiConfig'; -import { DataController } from './DataController'; -import { DataService } from './DataService'; - -/** - * Create RESTfull API for fetching WFM objects from mongo database. - */ -export function buildApiRouter(dbPromise: Promise) { - const router: express.Router = express.Router(); - // TODO Pagination https://github.com/expressjs/express-paginate - // TODO Wrap controller with security interface - const workorderService = new DataService(dbPromise, config.workorderApiName); - const workorderController = new DataController(router, workorderService, config.workorderApiName); - - const workflowService = new DataService(dbPromise, config.workflowApiName); - const workflowController = new DataController(router, workflowService, config.workflowApiName); - - const resultService = new DataService(dbPromise, config.resultApiName); - const resultController = new DataController(router, resultService, config.resultApiName); - return router; -} From 21219ccc1e2886de1cff6e8597ce4c5b491269f1 Mon Sep 17 00:00:00 2001 From: Wojciech Trocki Date: Thu, 10 Aug 2017 11:35:17 +0100 Subject: [PATCH 05/16] Debugger support --- demo/server/Gruntfile.js | 2 ++ demo/server/README.md | 8 ++++++++ 2 files changed, 10 insertions(+) diff --git a/demo/server/Gruntfile.js b/demo/server/Gruntfile.js index bb4a647..f4d3638 100644 --- a/demo/server/Gruntfile.js +++ b/demo/server/Gruntfile.js @@ -14,6 +14,7 @@ module.exports = function (grunt) { }, exec: { 'run': 'ts-node src/index.ts', + 'runDebug': 'ts-node --inspect src/index.ts', 'test': 'npm run test' }, env: { @@ -32,5 +33,6 @@ module.exports = function (grunt) { grunt.registerTask('serve', ['env:local', 'watch']); grunt.registerTask('test', ['env:local', 'exec:test']); + grunt.registerTask('debug', ['env:local', 'exec:runDebug']); grunt.registerTask('default', ['serve']); }; diff --git a/demo/server/README.md b/demo/server/README.md index 6663990..343ae53 100644 --- a/demo/server/README.md +++ b/demo/server/README.md @@ -10,6 +10,14 @@ Cloud application can be launched using If you wish to run cloud application along with other demo applications please execute this command on top level application.Application was generated from [base application](../../examples/base) template +## Debugging + +Execute + + grunt debug + +When process start you can connect to it using ide of your choice. + ## Prerequisites - mongodb installed and running on port 27017. From df636ba82704faa0276f7d9e0110a9d616dce3b7 Mon Sep 17 00:00:00 2001 From: Wojciech Trocki Date: Thu, 10 Aug 2017 11:57:31 +0100 Subject: [PATCH 06/16] Api security --- demo/server/config-dev.json | 5 ++++- demo/server/config-prod.json | 3 +++ demo/server/src/modules/index.ts | 21 +++++++++++++-------- demo/server/src/util/config.ts | 3 +++ 4 files changed, 23 insertions(+), 9 deletions(-) diff --git a/demo/server/config-dev.json b/demo/server/config-dev.json index 17d5154..086fbbd 100644 --- a/demo/server/config-dev.json +++ b/demo/server/config-dev.json @@ -1,7 +1,10 @@ { "morganOptions": null, "logStackTraces": true, - "sync":{ + "security": { + "apiRole": "ADMIN" + }, + "sync": { "customDataHandlers": true, "seedDemoData": true }, diff --git a/demo/server/config-prod.json b/demo/server/config-prod.json index 8c074f4..a9e85e1 100644 --- a/demo/server/config-prod.json +++ b/demo/server/config-prod.json @@ -5,6 +5,9 @@ "customDataHandlers": true, "seedDemoData": true }, + "security": { + "apiRole": "ADMIN" + }, "keycloakConfig": { "realm": "", "auth-server-url": "", diff --git a/demo/server/src/modules/index.ts b/demo/server/src/modules/index.ts index 9a61223..d059077 100644 --- a/demo/server/src/modules/index.ts +++ b/demo/server/src/modules/index.ts @@ -20,11 +20,7 @@ export function setupModules(app: express.Express) { const connectionPromise = syncSetup(app); securitySetup(app); apiSetup(app, connectionPromise); - connectionPromise.then(function(data: any) { - if (config.seedDemoData) { - initData(data.mongo); - } - }); + demoDataSetup(connectionPromise); } function securitySetup(app: express.Express) { @@ -51,9 +47,9 @@ function syncSetup(app: express.Express) { // Mount api app.use('/sync', syncRouter); // Connect sync - return syncConnector().then(function(mongo: Db) { + return syncConnector().then(function(connections: { mongo: Db, redis: any }) { getLogger().info('Sync started'); - return mongo; + return connections.mongo; }).catch(function(err: any) { getLogger().error('Failed to initialize sync', err); }); @@ -63,8 +59,17 @@ function apiSetup(app: express.Express, connectionPromise: Promise) { const router: express.Router = express.Router(); // Mount api const api = new WfmRestApi(); - app.use('/api', api.createWFMRouter()); + const role = config.security.apiRole; + app.use('/api', securityMiddleware.protect(role), api.createWFMRouter()); connectionPromise.then(function(db: Db) { api.setDb(db); }); } + +function demoDataSetup(connectionPromise: Promise) { + connectionPromise.then(function(mongo: Db) { + if (config.seedDemoData) { + initData(mongo); + } + }); +} diff --git a/demo/server/src/util/config.ts b/demo/server/src/util/config.ts index 26a0386..40741aa 100644 --- a/demo/server/src/util/config.ts +++ b/demo/server/src/util/config.ts @@ -42,6 +42,9 @@ export interface CloudAppConfig { bunyanConfig: any; keycloakConfig: any; seedDemoData: boolean; + security: { + apiRole: string + }; sync: { customDataHandlers: boolean; }; From 32a9915a0d4121ee3bd3794f8cd62ef8f049ee82 Mon Sep 17 00:00:00 2001 From: Wojciech Trocki Date: Thu, 10 Aug 2017 14:14:33 +0100 Subject: [PATCH 07/16] Minor improvements for error handlers --- cloud/wfm-rest-api/src/WfmRestApi.ts | 51 ++++++++++++++++ .../src/data-api/PaginationEngine.ts | 2 +- cloud/wfm-rest-api/src/impl/ApiController.ts | 56 +++++++++--------- cloud/wfm-rest-api/src/impl/ErrorCodes.ts | 3 + .../src/impl/MongoDbRepository.ts | 16 +++-- cloud/wfm-rest-api/src/index.ts | 58 +++---------------- demo/server/src/modules/index.ts | 7 ++- 7 files changed, 105 insertions(+), 88 deletions(-) create mode 100644 cloud/wfm-rest-api/src/WfmRestApi.ts create mode 100644 cloud/wfm-rest-api/src/impl/ErrorCodes.ts diff --git a/cloud/wfm-rest-api/src/WfmRestApi.ts b/cloud/wfm-rest-api/src/WfmRestApi.ts new file mode 100644 index 0000000..002bf47 --- /dev/null +++ b/cloud/wfm-rest-api/src/WfmRestApi.ts @@ -0,0 +1,51 @@ + +import * as Promise from 'bluebird'; +import * as express from 'express'; +import * as _ from 'lodash'; +import { Db } from 'mongodb'; +import { ApiConfig, defaultConfiguration } from './ApiConfig'; +import { ApiController } from './impl/ApiController'; +import { MongoDbRepository } from './impl/MongoDbRepository'; + +/** + * RESTfull api handlers for Workorders, Workflows and Results (WFM objects) + */ +export class WfmRestApi { + private config; + private workorderService: MongoDbRepository; + private workflowService: MongoDbRepository; + private resultService: MongoDbRepository; + + constructor(userConfig?: ApiConfig) { + this.config = _.defaults(defaultConfiguration, userConfig); + } + + /** + * Create new router for hosting WFM http api. + */ + public createWFMRouter() { + this.createWFMServices(); + const router: express.Router = express.Router(); + const workorderController = new ApiController(router, this.workorderService, this.config.workorderApiName); + const workflowController = new ApiController(router, this.workflowService, this.config.workflowApiName); + const resultController = new ApiController(router, this.resultService, this.config.resultApiName); + return router; + } + + /** + * Inject database connection to services + * + * @param db - mongodb driver + */ + public setDb(db: Db) { + this.workorderService.setDb(db); + this.workflowService.setDb(db); + this.resultService.setDb(db); + } + + protected createWFMServices() { + this.workorderService = new MongoDbRepository(this.config.workorderCollectionName); + this.workflowService = new MongoDbRepository(this.config.workflowApiName); + this.resultService = new MongoDbRepository(this.config.resultApiName); + } +} diff --git a/cloud/wfm-rest-api/src/data-api/PaginationEngine.ts b/cloud/wfm-rest-api/src/data-api/PaginationEngine.ts index 0622c3c..571251f 100644 --- a/cloud/wfm-rest-api/src/data-api/PaginationEngine.ts +++ b/cloud/wfm-rest-api/src/data-api/PaginationEngine.ts @@ -15,8 +15,8 @@ export class PaginationEngine { * @param defaultPageSize Default page size added when parameter is missing */ constructor(readonly defaultPageSize: number) { - } + /** * @param query - list of arguments passed as http query parameters */ diff --git a/cloud/wfm-rest-api/src/impl/ApiController.ts b/cloud/wfm-rest-api/src/impl/ApiController.ts index a635bf0..b6dbad8 100644 --- a/cloud/wfm-rest-api/src/impl/ApiController.ts +++ b/cloud/wfm-rest-api/src/impl/ApiController.ts @@ -3,23 +3,25 @@ import * as express from 'express'; import { ApiError } from '../data-api/ApiError'; import { CrudRepository } from '../data-api/CrudRepository'; import { defaultPaginationEngine } from '../data-api/PaginationEngine'; +import * as errorCodes from './ErrorCodes'; /** * Generic controller that can be used to create API for specific objects */ export class ApiController { - constructor(router: express.Router, service: CrudRepository, readonly apiPrefix: string) { + constructor(router: express.Router, repository: CrudRepository, readonly apiPrefix: string) { getLogger().info('REST api initialization', apiPrefix); - this.buildRoutes(router, service, apiPrefix); + this.buildRoutes(router, repository, apiPrefix); } /** * Build routes for specific element of api * * @param router - router used to attach api - * @param service - service to retrieve data + * @param repository - repository to retrieve data * @param apiPrefix - prefix to mount api in URI path. For example `/prefix/:id` */ - public buildRoutes(router: express.Router, service: CrudRepository, apiPrefix: string) { + public buildRoutes(router: express.Router, repository: CrudRepository, apiPrefix: string) { + const self = this; const idRoute = router.route('/' + apiPrefix + '/:id'); const objectRoute = router.route('/' + apiPrefix + '/'); @@ -28,12 +30,10 @@ export class ApiController { { object: apiPrefix, body: req.query }); const page = defaultPaginationEngine.buildRequestFromQuery(req.query); - const objectList = service.list(req.query.filter, page).then(function(data) { + const objectList = repository.list(req.query.filter, page).then(function(data) { res.send(data); - }).catch(function(err) { - getLogger().error('List error', { err, obj: req.body }); - const error: ApiError = { code: 'DBError', message: 'Failed to list objects' }; - res.status(500).json(error); + }).catch(function(err: ApiError) { + self.errorHandler(req, res, err); }); }); @@ -42,16 +42,14 @@ export class ApiController { { object: apiPrefix, body: req.body }); if (!req.body) { - const error: ApiError = { code: 'MissingData', message: 'Missing request body' }; + const error: ApiError = { code: errorCodes.CLIENT_ERROR, message: 'Missing request body' }; return res.status(400).json(error); } - service.create(req.body).then(function() { + repository.create(req.body).then(function() { res.send(); - }).catch(function(err) { - getLogger().error('Create error', { err, obj: req.body }); - const error: ApiError = { code: 'DBError', message: 'Failed to save object' }; - res.status(500).json(error); + }).catch(function(err: ApiError) { + self.errorHandler(req, res, err); }); }); @@ -60,15 +58,15 @@ export class ApiController { { object: apiPrefix, body: req.body }); if (!req.body) { - const error: ApiError = { code: 'MissingData', message: 'Missing request body' }; + const error = { code: errorCodes.CLIENT_ERROR, message: 'Missing request body' }; return res.status(400).json(error); } - service.update(req.body).then(function(data) { + repository.update(req.body).then(function(data) { res.send(); - }).catch(function(err) { - getLogger().error('Update error', { err, obj: req.body }); - const error: ApiError = { code: 'DBError', message: 'Failed to update object' }; + }).catch(function(err: ApiError) { + getLogger().error('Update error', { err: err.message, obj: req.body }); + const error: ApiError = { code: errorCodes.DB_ERROR, message: 'Failed to update object' }; res.status(500).json(error); }); }); @@ -82,12 +80,10 @@ export class ApiController { return res.status(400).json(error); } - service.get(req.params.id).then(function(data) { + repository.get(req.params.id).then(function(data) { res.send(data); - }).catch(function(err) { - getLogger().error('Get error', { err, obj: req.body }); - const error: ApiError = { code: 'DBError', message: 'Failed to get object' }; - res.status(500).json(error); + }).catch(function(err: ApiError) { + self.errorHandler(req, res, err); }); }); @@ -100,13 +96,15 @@ export class ApiController { return res.status(400).json(error); } - service.delete(req.params.id).then(function(data) { + repository.delete(req.params.id).then(function(data) { res.send(data); }).catch(function(err) { - getLogger().error('Delete error', { err, obj: req.body }); - const error: ApiError = { code: 'DBError', message: 'Failed to delete object' }; - res.status(500).json(error); + self.errorHandler(req, res, err); }); }); } + protected errorHandler(req: express.Request, res: express.Response, error: ApiError) { + getLogger().error('Api error', { error, obj: req.body }); + res.status(500).json(error); + } } diff --git a/cloud/wfm-rest-api/src/impl/ErrorCodes.ts b/cloud/wfm-rest-api/src/impl/ErrorCodes.ts new file mode 100644 index 0000000..ce5a748 --- /dev/null +++ b/cloud/wfm-rest-api/src/impl/ErrorCodes.ts @@ -0,0 +1,3 @@ + +export const CLIENT_ERROR = 'CLIENT_ARGUMENT_ERROR'; +export const DB_ERROR = 'DB_ERROR'; diff --git a/cloud/wfm-rest-api/src/impl/MongoDbRepository.ts b/cloud/wfm-rest-api/src/impl/MongoDbRepository.ts index 5e4f57c..fc7facb 100644 --- a/cloud/wfm-rest-api/src/impl/MongoDbRepository.ts +++ b/cloud/wfm-rest-api/src/impl/MongoDbRepository.ts @@ -1,15 +1,19 @@ import * as Promise from 'bluebird'; import { Db } from 'mongodb'; +import { ApiError } from '../data-api/ApiError'; import { CrudRepository } from '../data-api/CrudRepository'; import { PageRequest } from '../data-api/PageRequest'; import { PageResponse } from '../data-api/PageResponse'; +import * as errorCodes from './ErrorCodes'; + +const dbError: ApiError = { code: errorCodes.DB_ERROR, message: 'MongoDbRepository database not intialized' }; /** * Service for performing data operations on mongodb database */ export class MongoDbRepository implements CrudRepository { - private db: Db; + public db: any; /** * @param collectionName - name of the collection stored in mongodb @@ -20,28 +24,28 @@ export class MongoDbRepository implements CrudRepository { public list(filter: any, request: PageRequest): Promise { if (!this.db) { - Promise.reject('Db not intialized'); + return Promise.reject(dbError); } return this.db.collection(this.collectionName).find(filter); } public get(id: string) { if (!this.db) { - Promise.reject('Db not intialized'); + return Promise.reject(dbError); } return this.db.collection(this.collectionName).findOne({ id }); } public create(object: any) { if (!this.db) { - Promise.reject('Db not intialized'); + return Promise.reject(dbError); } return this.db.collection(this.collectionName).insertOne(object); } public update(object: any) { if (!this.db) { - Promise.reject('Db not intialized'); + return Promise.reject(dbError); } const id = object.id; return this.db.collection(this.collectionName).updateOne({ id }, object); @@ -49,7 +53,7 @@ export class MongoDbRepository implements CrudRepository { public delete(id: string) { if (!this.db) { - Promise.reject('Db not intialized'); + return Promise.reject(dbError); } return this.db.collection(this.collectionName).deleteOne({ id }); } diff --git a/cloud/wfm-rest-api/src/index.ts b/cloud/wfm-rest-api/src/index.ts index b862e4f..f40d19f 100644 --- a/cloud/wfm-rest-api/src/index.ts +++ b/cloud/wfm-rest-api/src/index.ts @@ -1,54 +1,14 @@ -import * as Promise from 'bluebird'; -import * as express from 'express'; -import * as _ from 'lodash'; -import { Db } from 'mongodb'; -import { ApiConfig, defaultConfiguration } from './ApiConfig'; -import { ApiController } from './impl/ApiController'; -import { MongoDbRepository } from './impl/MongoDbRepository'; +// WFM implementation +export * from './WfmRestApi'; -/** - * RESTfull api implementation for Workorders, Workflows and Results (WFM objects) - */ -export class WfmRestApi { - private config; - private workorderService: MongoDbRepository; - private workflowService: MongoDbRepository; - private resultService: MongoDbRepository; - - constructor(userConfig?: ApiConfig) { - this.config = _.defaults(defaultConfiguration, userConfig); - } - - /** - * Create new router for hosting WFM http api. - */ - public createWFMRouter() { - this.createWFMServices(); - const router: express.Router = express.Router(); - const workorderController = new ApiController(router, this.workorderService, this.config.workorderApiName); - const workflowController = new ApiController(router, this.workflowService, this.config.workflowApiName); - const resultController = new ApiController(router, this.resultService, this.config.resultApiName); - return router; - } - - /** - * Inject database connection to services - * - * @param db - mongodb driver - */ - public setDb(db: Db) { - this.workorderService.setDb(db); - this.workflowService.setDb(db); - this.resultService.setDb(db); - } - - protected createWFMServices() { - this.workorderService = new MongoDbRepository(this.config.workorderCollectionName); - this.workflowService = new MongoDbRepository(this.config.workflowApiName); - this.resultService = new MongoDbRepository(this.config.resultApiName); - } -} +export * from './impl/ErrorCodes'; +export * from './impl/ApiController'; +export * from './impl/MongoDbRepository'; +// API +export * from './data-api/CrudRepository'; export * from './data-api/PageRequest'; export * from './data-api/PageResponse'; +export * from './data-api/ApiError'; +export * from './data-api/PaginationEngine'; diff --git a/demo/server/src/modules/index.ts b/demo/server/src/modules/index.ts index d059077..b008266 100644 --- a/demo/server/src/modules/index.ts +++ b/demo/server/src/modules/index.ts @@ -60,9 +60,10 @@ function apiSetup(app: express.Express, connectionPromise: Promise) { // Mount api const api = new WfmRestApi(); const role = config.security.apiRole; - app.use('/api', securityMiddleware.protect(role), api.createWFMRouter()); - connectionPromise.then(function(db: Db) { - api.setDb(db); + app.use('/api', api.createWFMRouter()); + app.use('/apiSecure', securityMiddleware.protect(role), api.createWFMRouter()); + connectionPromise.then(function(mongo: Db) { + api.setDb(mongo); }); } From f849b6ebab8bfb244ed46aed397e0efe1b4c1b71 Mon Sep 17 00:00:00 2001 From: Wojciech Trocki Date: Thu, 10 Aug 2017 17:22:52 +0100 Subject: [PATCH 08/16] Documentation --- cloud/wfm-rest-api/example/README.md | 15 +++++++++++++++ 1 file changed, 15 insertions(+) create mode 100644 cloud/wfm-rest-api/example/README.md diff --git a/cloud/wfm-rest-api/example/README.md b/cloud/wfm-rest-api/example/README.md new file mode 100644 index 0000000..293f0cc --- /dev/null +++ b/cloud/wfm-rest-api/example/README.md @@ -0,0 +1,15 @@ +## wfm-web-api example + +### Requirements + +Mongodb running on standard port + +### Runining example + + ts-node example/index.ts + +To test example try + + curl http://localhost:3000/api/workorders + +Note: This example assumes that you have run demo application that populated your database. From 684bfda8c550b9aed2c46076a24cafc917c0d536 Mon Sep 17 00:00:00 2001 From: Wojciech Trocki Date: Thu, 10 Aug 2017 17:23:15 +0100 Subject: [PATCH 09/16] Documentation and example --- cloud/wfm-rest-api/README.md | 82 +++++++++++++++++++++++++++-- cloud/wfm-rest-api/example/index.ts | 33 ++++++++++++ 2 files changed, 112 insertions(+), 3 deletions(-) create mode 100644 cloud/wfm-rest-api/example/index.ts diff --git a/cloud/wfm-rest-api/README.md b/cloud/wfm-rest-api/README.md index 83fed6f..4516139 100644 --- a/cloud/wfm-rest-api/README.md +++ b/cloud/wfm-rest-api/README.md @@ -4,11 +4,87 @@ Module used to expose express based api for WFM objects. ### WFM speficic implementations +Following api is being exposed: + +- `/workorders` +- `/workflows` +- `/results` + +This api is being added to new express router and it can be applied to any existing express based application +as follows: + +```typescript +// Create api +const api = new WfmRestApi(); + +// Mount api into path +app.use('/api', api.createWFMRouter()); +``` + +Api requires mongodb connection that needs to be setup as separate step + +```typescript +api.setDb(db); +``` +See demo application integration or [example application](./example) for more details. + +### Custom database integrations + +Custom database integrations are possible thanks to `CrudRepository` interface. +See Api documentation for more details. ## Rest API -## Framework +Module provides a way to dynamically create API for different business objects. +Created api will use simplied implementations for typical create, read, delete and update operations. +It's not recomended to use it outside WFM framework to store user related objects. Please use mongodb driver directly. + +## Rest API definitions + +Definitions apply to every to object exposed by this API. Placeholder `{object}` can be replaced by `workflow`, `workorder` and `result`. + +### Retrieve list + +> GET {object}/ + +##### Pagination +Supports pagination and sorting by providing additional query parameters: + +- `page` page number +- `size` number of elements to return +- `sortField` sorting field +- `order` -1 for DESC and 1 for ASC +` +Example `/workorders?page=0&size=5&sortField=id&order=-1` + +> **Note** - sorting parameters are optional. When missing default sorting values are applied (10 results) + +##### Filtering + +List can be filtered by providing json as `filter` query parameter or `filter` as request body. + +`filter` - json object with specific field + +For example `filter = { 'reviewer': 'Paolo'}` + +> **Note** - Due to nature of the url filter needs to be encoded to be passed as url + +### Retrieve specific object by id + +> GET {object}/:objectId + +Retrieve specific object by id + +Example `/workorders/B1r71fOBr` + +### Save object + +> POST {object}/ + +### Update object + +> PUT {object}/ -### Pagination +### Delete object -### Generic data service for simple CRUD operations +> DELETE {object}/:objectId diff --git a/cloud/wfm-rest-api/example/index.ts b/cloud/wfm-rest-api/example/index.ts new file mode 100644 index 0000000..f0465b6 --- /dev/null +++ b/cloud/wfm-rest-api/example/index.ts @@ -0,0 +1,33 @@ +import { getLogger } from '@raincatcher/logger'; +import * as express from 'express'; +import { MongoClient } from 'mongodb'; +import * as path from 'path'; +import { WfmRestApi } from '../src/index'; + +const app = express(); + +// Create api +const api = new WfmRestApi(); + +// Mount api into path +app.use('/api', api.createWFMRouter()); + +// Use connect method to connect to the server +const url = 'mongodb://localhost:27017/raincatcher'; +MongoClient.connect(url, function(err, db) { + if (db) { + api.setDb(db); + } else { + console.info('MongoDb not connected', err); + process.exit(); + } +}); + +app.use(function(err: any, req: express.Request, res: express.Response, next: any) { + getLogger().error(err); + res.status(500).send(err); +}); + +app.listen(3000, function() { + getLogger().info('Example auth app listening on port 3000'); +}); From f4431e2317c4ff44b566656e5989be7e0b425f44 Mon Sep 17 00:00:00 2001 From: Wojciech Trocki Date: Thu, 10 Aug 2017 17:23:22 +0100 Subject: [PATCH 10/16] Improvements in filtering --- cloud/wfm-rest-api/package.json | 3 +- cloud/wfm-rest-api/src/ApiConfig.ts | 2 +- cloud/wfm-rest-api/src/WfmRestApi.ts | 6 +-- .../src/data-api/CrudRepository.ts | 4 +- .../wfm-rest-api/src/data-api/PageRequest.ts | 2 +- .../src/data-api/PaginationEngine.ts | 48 ++++++++++++++++--- cloud/wfm-rest-api/src/impl/ApiController.ts | 14 +++++- .../src/impl/MongoDbRepository.ts | 12 +++-- demo/server/src/modules/index.ts | 3 +- 9 files changed, 72 insertions(+), 22 deletions(-) diff --git a/cloud/wfm-rest-api/package.json b/cloud/wfm-rest-api/package.json index 9387068..ee1d5ee 100644 --- a/cloud/wfm-rest-api/package.json +++ b/cloud/wfm-rest-api/package.json @@ -36,7 +36,8 @@ "@raincatcher/logger": "1.0.0", "bluebird": "^3.5.0", "express": "^4.15.3", - "lodash": "^4.17.4" + "lodash": "^4.17.4", + "mongodb": "^2.2.31" }, "devDependencies": { "@types/bluebird": "^3.5.5", diff --git a/cloud/wfm-rest-api/src/ApiConfig.ts b/cloud/wfm-rest-api/src/ApiConfig.ts index 7cf061d..628491a 100644 --- a/cloud/wfm-rest-api/src/ApiConfig.ts +++ b/cloud/wfm-rest-api/src/ApiConfig.ts @@ -21,5 +21,5 @@ export const defaultConfiguration = { resultApiName: 'results', workorderCollectionName: 'workorders', workflowCollectionName: 'workflows', - resultCollectionName: 'results' + resultCollectionName: 'result' }; diff --git a/cloud/wfm-rest-api/src/WfmRestApi.ts b/cloud/wfm-rest-api/src/WfmRestApi.ts index 002bf47..ed20937 100644 --- a/cloud/wfm-rest-api/src/WfmRestApi.ts +++ b/cloud/wfm-rest-api/src/WfmRestApi.ts @@ -18,13 +18,13 @@ export class WfmRestApi { constructor(userConfig?: ApiConfig) { this.config = _.defaults(defaultConfiguration, userConfig); + this.createWFMServices(); } /** * Create new router for hosting WFM http api. */ public createWFMRouter() { - this.createWFMServices(); const router: express.Router = express.Router(); const workorderController = new ApiController(router, this.workorderService, this.config.workorderApiName); const workflowController = new ApiController(router, this.workflowService, this.config.workflowApiName); @@ -45,7 +45,7 @@ export class WfmRestApi { protected createWFMServices() { this.workorderService = new MongoDbRepository(this.config.workorderCollectionName); - this.workflowService = new MongoDbRepository(this.config.workflowApiName); - this.resultService = new MongoDbRepository(this.config.resultApiName); + this.workflowService = new MongoDbRepository(this.config.workflowCollectionName); + this.resultService = new MongoDbRepository(this.config.resultCollectionName); } } diff --git a/cloud/wfm-rest-api/src/data-api/CrudRepository.ts b/cloud/wfm-rest-api/src/data-api/CrudRepository.ts index d5db82c..855aec1 100644 --- a/cloud/wfm-rest-api/src/data-api/CrudRepository.ts +++ b/cloud/wfm-rest-api/src/data-api/CrudRepository.ts @@ -1,6 +1,6 @@ import * as Promise from 'bluebird'; import { Db } from 'mongodb'; -import { PageRequest } from '../data-api/PageRequest'; +import { SortedPageRequest } from '../data-api/PageRequest'; import { PageResponse } from '../data-api/PageResponse'; /** @@ -22,7 +22,7 @@ export interface CrudRepository { * @see PageRequest * @see PageResponse */ - list(filter: any, request: PageRequest): Promise; + list(filter: any, request: SortedPageRequest): Promise; /** * Get specific item from database diff --git a/cloud/wfm-rest-api/src/data-api/PageRequest.ts b/cloud/wfm-rest-api/src/data-api/PageRequest.ts index 87c6863..8de5c89 100644 --- a/cloud/wfm-rest-api/src/data-api/PageRequest.ts +++ b/cloud/wfm-rest-api/src/data-api/PageRequest.ts @@ -23,7 +23,7 @@ export interface SortedPageRequest extends PageRequest { /** * Name of the field to sort */ - sort?: string; + sortField?: string; /** * Order of the sort direction */ diff --git a/cloud/wfm-rest-api/src/data-api/PaginationEngine.ts b/cloud/wfm-rest-api/src/data-api/PaginationEngine.ts index 571251f..498b0ab 100644 --- a/cloud/wfm-rest-api/src/data-api/PaginationEngine.ts +++ b/cloud/wfm-rest-api/src/data-api/PaginationEngine.ts @@ -1,5 +1,6 @@ -import { PageRequest } from '../data-api/PageRequest'; +import { Cursor } from 'mongodb'; +import { DIRECTION, SortedPageRequest } from '../data-api/PageRequest'; import { PageResponse } from '../data-api/PageResponse'; /** @@ -18,23 +19,58 @@ export class PaginationEngine { } /** + * Create page request from query + * * @param query - list of arguments passed as http query parameters + * + * Expected query example: + * { + * page:1, + * size:10, + * sortField: "id", + * order: 1 + * } */ - public buildRequestFromQuery(query: any): PageRequest { + public buildRequestFromQuery(query: any): SortedPageRequest { let page; let size; + let order; if (query.size) { - size = query.size; + size = Number(query.size); } else { size = this.defaultPageSize; } if (query.page) { - page = query.page; + page = Number(query.page); } else { - page = 1; + page = 0; } - return { page, size }; + if (query.order) { + order = Number(query.order); + } + return { page, size, sortField: query.sortField, order }; } + + /** + * Fetch data using PageRequest and return PaggedResponse + * + * @param cursor mongodb cursor + * @param totalCount total number of results + * @param request page request + */ + public buildPageResponse(request: SortedPageRequest, cursor: Cursor, totalCount: number) { + if (request.sortField) { + if (!request.order) { + request.order = DIRECTION.ASC; + } + cursor = cursor.sort(request.sortField, request.order); + } + cursor = cursor.skip(request.size * request.page).limit(request.size); + return cursor.toArray().then(function(data) { + return defaultPaginationEngine.buildResponse(Math.ceil(totalCount / request.size), totalCount, data); + }); + } + /** * @param totalPages - list of pages available * @param totalCount - total list of the elements diff --git a/cloud/wfm-rest-api/src/impl/ApiController.ts b/cloud/wfm-rest-api/src/impl/ApiController.ts index b6dbad8..8534f04 100644 --- a/cloud/wfm-rest-api/src/impl/ApiController.ts +++ b/cloud/wfm-rest-api/src/impl/ApiController.ts @@ -28,9 +28,19 @@ export class ApiController { objectRoute.get(function(req: express.Request, res: express.Response) { getLogger().debug('Api list method called', { object: apiPrefix, body: req.query }); - const page = defaultPaginationEngine.buildRequestFromQuery(req.query); - const objectList = repository.list(req.query.filter, page).then(function(data) { + let filter = {}; + if (req.query.filter) { + try { + filter = JSON.parse(req.query.filter); + } catch (err) { + getLogger().debug('Invalid filter passed'); + } + } + if (req.body.filter) { + filter = req.body.filter; + } + const objectList = repository.list(filter, page).then(function(data) { res.send(data); }).catch(function(err: ApiError) { self.errorHandler(req, res, err); diff --git a/cloud/wfm-rest-api/src/impl/MongoDbRepository.ts b/cloud/wfm-rest-api/src/impl/MongoDbRepository.ts index fc7facb..535704f 100644 --- a/cloud/wfm-rest-api/src/impl/MongoDbRepository.ts +++ b/cloud/wfm-rest-api/src/impl/MongoDbRepository.ts @@ -1,9 +1,10 @@ import * as Promise from 'bluebird'; -import { Db } from 'mongodb'; +import { Cursor, Db } from 'mongodb'; import { ApiError } from '../data-api/ApiError'; import { CrudRepository } from '../data-api/CrudRepository'; -import { PageRequest } from '../data-api/PageRequest'; +import { DIRECTION, SortedPageRequest } from '../data-api/PageRequest'; import { PageResponse } from '../data-api/PageResponse'; +import { defaultPaginationEngine } from '../data-api/PaginationEngine'; import * as errorCodes from './ErrorCodes'; const dbError: ApiError = { code: errorCodes.DB_ERROR, message: 'MongoDbRepository database not intialized' }; @@ -22,11 +23,14 @@ export class MongoDbRepository implements CrudRepository { const self = this; } - public list(filter: any, request: PageRequest): Promise { + public list(filter: any, request: SortedPageRequest): Promise { if (!this.db) { return Promise.reject(dbError); } - return this.db.collection(this.collectionName).find(filter); + const cursor: Cursor = this.db.collection(this.collectionName).find(filter); + return cursor.count(filter).then(function(totalNumber) { + return defaultPaginationEngine.buildPageResponse(request, cursor, totalNumber); + }); } public get(id: string) { diff --git a/demo/server/src/modules/index.ts b/demo/server/src/modules/index.ts index b008266..54aa8c0 100644 --- a/demo/server/src/modules/index.ts +++ b/demo/server/src/modules/index.ts @@ -56,12 +56,11 @@ function syncSetup(app: express.Express) { } function apiSetup(app: express.Express, connectionPromise: Promise) { - const router: express.Router = express.Router(); // Mount api const api = new WfmRestApi(); const role = config.security.apiRole; app.use('/api', api.createWFMRouter()); - app.use('/apiSecure', securityMiddleware.protect(role), api.createWFMRouter()); + // app.use('/apiSecure', securityMiddleware.protect(role), api.createWFMRouter()); connectionPromise.then(function(mongo: Db) { api.setDb(mongo); }); From fb6ca7db57cfb98d2b4ec0eff4fc76f773c6a74c Mon Sep 17 00:00:00 2001 From: Wojciech Trocki Date: Thu, 10 Aug 2017 17:27:14 +0100 Subject: [PATCH 11/16] Revert security --- demo/server/src/modules/index.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/demo/server/src/modules/index.ts b/demo/server/src/modules/index.ts index 54aa8c0..0986f49 100644 --- a/demo/server/src/modules/index.ts +++ b/demo/server/src/modules/index.ts @@ -59,8 +59,7 @@ function apiSetup(app: express.Express, connectionPromise: Promise) { // Mount api const api = new WfmRestApi(); const role = config.security.apiRole; - app.use('/api', api.createWFMRouter()); - // app.use('/apiSecure', securityMiddleware.protect(role), api.createWFMRouter()); + app.use('/api', securityMiddleware.protect(role), api.createWFMRouter()); connectionPromise.then(function(mongo: Db) { api.setDb(mongo); }); From a210bd83f9705dcfcec13a1d1205766a408661ff Mon Sep 17 00:00:00 2001 From: Wojciech Trocki Date: Thu, 10 Aug 2017 22:01:33 +0100 Subject: [PATCH 12/16] Unit tests --- cloud/wfm-rest-api/src/ApiConfig.ts | 8 ++- .../src/data-api/PaginationEngine.ts | 23 +++---- cloud/wfm-rest-api/src/impl/ApiController.ts | 2 +- cloud/wfm-rest-api/test/ApiControllerTest.ts | 14 ++++ .../wfm-rest-api/test/PaginationEngineTest.ts | 68 +++++++++++++++++++ cloud/wfm-rest-api/test/WfmRestApiTest.ts | 19 ++++++ 6 files changed, 117 insertions(+), 17 deletions(-) create mode 100644 cloud/wfm-rest-api/test/ApiControllerTest.ts create mode 100644 cloud/wfm-rest-api/test/PaginationEngineTest.ts create mode 100644 cloud/wfm-rest-api/test/WfmRestApiTest.ts diff --git a/cloud/wfm-rest-api/src/ApiConfig.ts b/cloud/wfm-rest-api/src/ApiConfig.ts index 628491a..9729527 100644 --- a/cloud/wfm-rest-api/src/ApiConfig.ts +++ b/cloud/wfm-rest-api/src/ApiConfig.ts @@ -4,18 +4,24 @@ * Holds all values that can be changed to modify module behavior */ export interface ApiConfig { + /** Used to create workorder route */ workorderApiName?: string; + /** Used to create workflow route */ workflowApiName?: string; + /** Used to create result route */ resultApiName?: string; + /** Used as collection name to make database query for workorder */ workorderCollectionName?: string; + /** Used as collection name to make database query for workflow */ workflowCollectionName?: string; + /** Used as collection name to make database query for result */ resultCollectionName?: string; } /** * Default module configuration */ -export const defaultConfiguration = { +export const defaultConfiguration: ApiConfig = { workorderApiName: 'workorders', workflowApiName: 'workflows', resultApiName: 'results', diff --git a/cloud/wfm-rest-api/src/data-api/PaginationEngine.ts b/cloud/wfm-rest-api/src/data-api/PaginationEngine.ts index 498b0ab..3ea066d 100644 --- a/cloud/wfm-rest-api/src/data-api/PaginationEngine.ts +++ b/cloud/wfm-rest-api/src/data-api/PaginationEngine.ts @@ -1,4 +1,5 @@ +import * as Promise from 'bluebird'; import { Cursor } from 'mongodb'; import { DIRECTION, SortedPageRequest } from '../data-api/PageRequest'; import { PageResponse } from '../data-api/PageResponse'; @@ -58,7 +59,7 @@ export class PaginationEngine { * @param totalCount total number of results * @param request page request */ - public buildPageResponse(request: SortedPageRequest, cursor: Cursor, totalCount: number) { + public buildPageResponse(request: SortedPageRequest, cursor: Cursor, totalCount: number): Promise { if (request.sortField) { if (!request.order) { request.order = DIRECTION.ASC; @@ -67,22 +68,14 @@ export class PaginationEngine { } cursor = cursor.skip(request.size * request.page).limit(request.size); return cursor.toArray().then(function(data) { - return defaultPaginationEngine.buildResponse(Math.ceil(totalCount / request.size), totalCount, data); + const totalPages = Math.ceil(totalCount / request.size); + return { + totalPages, + totalCount, + data + }; }); } - - /** - * @param totalPages - list of pages available - * @param totalCount - total list of the elements - * @param data - */ - public buildResponse(totalPages: number, totalCount: number, data: any[]): PageResponse { - return { - totalPages, - totalCount, - data - }; - } } export const defaultPaginationEngine = new PaginationEngine(10); diff --git a/cloud/wfm-rest-api/src/impl/ApiController.ts b/cloud/wfm-rest-api/src/impl/ApiController.ts index 8534f04..62139d3 100644 --- a/cloud/wfm-rest-api/src/impl/ApiController.ts +++ b/cloud/wfm-rest-api/src/impl/ApiController.ts @@ -20,7 +20,7 @@ export class ApiController { * @param repository - repository to retrieve data * @param apiPrefix - prefix to mount api in URI path. For example `/prefix/:id` */ - public buildRoutes(router: express.Router, repository: CrudRepository, apiPrefix: string) { + protected buildRoutes(router: express.Router, repository: CrudRepository, apiPrefix: string) { const self = this; const idRoute = router.route('/' + apiPrefix + '/:id'); const objectRoute = router.route('/' + apiPrefix + '/'); diff --git a/cloud/wfm-rest-api/test/ApiControllerTest.ts b/cloud/wfm-rest-api/test/ApiControllerTest.ts new file mode 100644 index 0000000..eac4bad --- /dev/null +++ b/cloud/wfm-rest-api/test/ApiControllerTest.ts @@ -0,0 +1,14 @@ +import * as assert from 'assert'; +import * as express from 'express'; +import * as proxyquire from 'proxyquire'; +import { ApiController, ApiError, MongoDbRepository } from '../src/index'; + +describe('FeedHenry ApiController Tests', function() { + describe('ApiController', function() { + it('create router', function() { + const router = express.Router(); + const repository = new MongoDbRepository('test'); + const testSubject = new ApiController(router, repository, 'testApi'); + }); + }); +}); diff --git a/cloud/wfm-rest-api/test/PaginationEngineTest.ts b/cloud/wfm-rest-api/test/PaginationEngineTest.ts new file mode 100644 index 0000000..5ada82f --- /dev/null +++ b/cloud/wfm-rest-api/test/PaginationEngineTest.ts @@ -0,0 +1,68 @@ +import * as assert from 'assert'; +import * as Promise from 'bluebird'; +import * as proxyquire from 'proxyquire'; +import { PaginationEngine } from '../src/index'; + +describe('FeedHenry PaginationEngine Tests', function() { + describe('Test PaginationEngine api', function() { + it('builds request ', function() { + const testSubject = new PaginationEngine(10); + const query = { + page: 1, + size: 10, + sortField: 'id', + order: 1 + }; + const request = testSubject.buildRequestFromQuery(query); + assert.equal(request.order, query.order); + assert.equal(request.page, query.page); + assert.equal(request.size, query.size); + assert.equal(request.sortField, query.sortField); + }); + it('builds request with defaults', function() { + const defaultPageSize = 10; + const testSubject = new PaginationEngine(defaultPageSize); + const query = {}; + const request = testSubject.buildRequestFromQuery(query); + assert.equal(request.page, 0); + assert.equal(request.size, defaultPageSize); + }); + it('builds request ', function(done) { + const testSubject = new PaginationEngine(10); + const result = ['test', 'test2']; + const query = { + page: 1, + size: 10, + sortField: 'id' + }; + const request = testSubject.buildRequestFromQuery(query); + assert.equal(request.page, query.page); + assert.equal(request.size, query.size); + const cursor = { + sort(sortField, order) { + assert.equal(request.sortField, sortField); + assert.equal(request.order, order); + return cursor; + }, + skip(value) { + assert.equal(request.size * request.page, value); + return cursor; + }, + limit(value) { + assert.equal(request.size, value); + return cursor; + }, + toArray() { + return Promise.resolve(result); + } + }; + const totalCount = 10; + testSubject.buildPageResponse(request, cursor, totalCount).then(function(response) { + assert.equal(response.totalPages, Math.ceil(totalCount / request.size)); + assert.equal(response.totalCount, totalCount); + assert.equal(response.data, result); + done(); + }); + }); + }); +}); diff --git a/cloud/wfm-rest-api/test/WfmRestApiTest.ts b/cloud/wfm-rest-api/test/WfmRestApiTest.ts new file mode 100644 index 0000000..21f2ca9 --- /dev/null +++ b/cloud/wfm-rest-api/test/WfmRestApiTest.ts @@ -0,0 +1,19 @@ +import * as assert from 'assert'; +import * as proxyquire from 'proxyquire'; +import { WfmRestApi } from '../src/index'; + +describe('FeedHenry Wfm api Tests', function() { + describe('Test end user api', function() { + it('create router', function() { + const testSubject = new WfmRestApi(); + assert.ok(testSubject.createWFMRouter()); + }); + }); + describe('Test mongo setup', function() { + it('create router', function() { + const testSubject = new WfmRestApi(); + const db: any = {}; + testSubject.setDb(db); + }); + }); +}); From a50970165dec66d7e12aa117fc79d36c3893ef0e Mon Sep 17 00:00:00 2001 From: Wojciech Trocki Date: Thu, 10 Aug 2017 22:11:03 +0100 Subject: [PATCH 13/16] Remove config (trick git) --- demo/server/src/util/config.ts | 54 ---------------------------------- 1 file changed, 54 deletions(-) delete mode 100644 demo/server/src/util/config.ts diff --git a/demo/server/src/util/config.ts b/demo/server/src/util/config.ts deleted file mode 100644 index 40741aa..0000000 --- a/demo/server/src/util/config.ts +++ /dev/null @@ -1,54 +0,0 @@ -/** - * Interface for fetching application configuration. - * - * @type T specifies interface used to wrap configuration - */ -export interface Config { - /** - * Returns application configuration object. - */ - getConfig(): T; -} - -/** - * Default implementation for configuration. - * Reads configuration from different location depending on process.env.NODE_ENV - * - * Required csonfiguration files in application root: - * - config-dev.json - * - config-prod.json - */ -export class EnvironmentConfig implements Config { - private rawConfig: T; - - constructor() { - const prodEnv = process.env.NODE_ENV === 'production'; - if (prodEnv) { - this.rawConfig = require('../../config-prod.json'); - } else { - this.rawConfig = require('../../config-dev.json'); - } - } - - public getConfig() { - return this.rawConfig; - } -} - -export interface CloudAppConfig { - morganOptions: string; - logStackTraces: boolean; - // See bunyan.d.ts/LoggerOptions - bunyanConfig: any; - keycloakConfig: any; - seedDemoData: boolean; - security: { - apiRole: string - }; - sync: { - customDataHandlers: boolean; - }; -} -const appConfig: Config = new EnvironmentConfig(); - -export default appConfig; From b1883776e47e31dd5e028d55ae542af0e47857b1 Mon Sep 17 00:00:00 2001 From: Wojciech Trocki Date: Thu, 10 Aug 2017 22:11:23 +0100 Subject: [PATCH 14/16] Add new config --- demo/server/src/util/Config.ts | 55 ++++++++++++++++++++++++++++++++++ 1 file changed, 55 insertions(+) create mode 100644 demo/server/src/util/Config.ts diff --git a/demo/server/src/util/Config.ts b/demo/server/src/util/Config.ts new file mode 100644 index 0000000..86b1534 --- /dev/null +++ b/demo/server/src/util/Config.ts @@ -0,0 +1,55 @@ +/** + * Interface for fetching application configuration. + * + * @type T specifies interface used to wrap configuration + */ +export interface Config { + /** + * Returns application configuration object. + */ + getConfig(): T; +} + +/** + * Default implementation for configuration. + * Reads configuration from different location depending on process.env.NODE_ENV + * + * Required csonfiguration files in application root: + * - config-dev.json + * - config-prod.json + */ +export class EnvironmentConfig implements Config { + private rawConfig: T; + + constructor() { + const prodEnv = process.env.NODE_ENV === 'production'; + if (prodEnv) { + this.rawConfig = require('../../config-prod.json'); + } else { + this.rawConfig = require('../../config-dev.json'); + } + } + + public getConfig() { + return this.rawConfig; + } +} + +export interface CloudAppConfig { + morganOptions: string; + logStackTraces: boolean; + // See bunyan.d.ts/LoggerOptions + bunyanConfig: any; + keycloakConfig: any; + seedDemoData: boolean; + security: { + apiRole: string + }; + + sync: { + customDataHandlers: boolean; + }; +} +const appConfig: Config = new EnvironmentConfig(); + +export default appConfig; From c7a2b33e304d3fe33c70bf7dbacf60d7d541a79e Mon Sep 17 00:00:00 2001 From: Wojciech Trocki Date: Thu, 10 Aug 2017 22:11:40 +0100 Subject: [PATCH 15/16] Rename to MongoPaginationEngine --- cloud/wfm-rest-api/example/index.ts | 2 +- .../{PaginationEngine.ts => MongoPaginationEngine.ts} | 9 ++++----- cloud/wfm-rest-api/src/impl/ApiController.ts | 2 +- cloud/wfm-rest-api/src/impl/MongoDbRepository.ts | 2 +- cloud/wfm-rest-api/src/index.ts | 2 +- 5 files changed, 8 insertions(+), 9 deletions(-) rename cloud/wfm-rest-api/src/data-api/{PaginationEngine.ts => MongoPaginationEngine.ts} (90%) diff --git a/cloud/wfm-rest-api/example/index.ts b/cloud/wfm-rest-api/example/index.ts index f0465b6..0d53cf3 100644 --- a/cloud/wfm-rest-api/example/index.ts +++ b/cloud/wfm-rest-api/example/index.ts @@ -19,7 +19,7 @@ MongoClient.connect(url, function(err, db) { api.setDb(db); } else { console.info('MongoDb not connected', err); - process.exit(); + process.exit(1); } }); diff --git a/cloud/wfm-rest-api/src/data-api/PaginationEngine.ts b/cloud/wfm-rest-api/src/data-api/MongoPaginationEngine.ts similarity index 90% rename from cloud/wfm-rest-api/src/data-api/PaginationEngine.ts rename to cloud/wfm-rest-api/src/data-api/MongoPaginationEngine.ts index 3ea066d..80c27b2 100644 --- a/cloud/wfm-rest-api/src/data-api/PaginationEngine.ts +++ b/cloud/wfm-rest-api/src/data-api/MongoPaginationEngine.ts @@ -5,13 +5,12 @@ import { DIRECTION, SortedPageRequest } from '../data-api/PageRequest'; import { PageResponse } from '../data-api/PageResponse'; /** - * Pagination procesor + * Mongo pagination procesor * Clients may override this class to provide custom pagination parameters * - * Note: pages are counted from number 1. - * Page 0 requests will return empty data. + * Note: pages are counted starting from 0. */ -export class PaginationEngine { +export class MongoPaginationEngine { /** * @param defaultPageSize Default page size added when parameter is missing @@ -78,4 +77,4 @@ export class PaginationEngine { } } -export const defaultPaginationEngine = new PaginationEngine(10); +export const defaultPaginationEngine = new MongoPaginationEngine(10); diff --git a/cloud/wfm-rest-api/src/impl/ApiController.ts b/cloud/wfm-rest-api/src/impl/ApiController.ts index 62139d3..bf2aba4 100644 --- a/cloud/wfm-rest-api/src/impl/ApiController.ts +++ b/cloud/wfm-rest-api/src/impl/ApiController.ts @@ -2,7 +2,7 @@ import { getLogger } from '@raincatcher/logger'; import * as express from 'express'; import { ApiError } from '../data-api/ApiError'; import { CrudRepository } from '../data-api/CrudRepository'; -import { defaultPaginationEngine } from '../data-api/PaginationEngine'; +import { defaultPaginationEngine } from '../data-api/MongoPaginationEngine'; import * as errorCodes from './ErrorCodes'; /** diff --git a/cloud/wfm-rest-api/src/impl/MongoDbRepository.ts b/cloud/wfm-rest-api/src/impl/MongoDbRepository.ts index 535704f..5d847ad 100644 --- a/cloud/wfm-rest-api/src/impl/MongoDbRepository.ts +++ b/cloud/wfm-rest-api/src/impl/MongoDbRepository.ts @@ -2,9 +2,9 @@ import * as Promise from 'bluebird'; import { Cursor, Db } from 'mongodb'; import { ApiError } from '../data-api/ApiError'; import { CrudRepository } from '../data-api/CrudRepository'; +import { defaultPaginationEngine } from '../data-api/MongoPaginationEngine'; import { DIRECTION, SortedPageRequest } from '../data-api/PageRequest'; import { PageResponse } from '../data-api/PageResponse'; -import { defaultPaginationEngine } from '../data-api/PaginationEngine'; import * as errorCodes from './ErrorCodes'; const dbError: ApiError = { code: errorCodes.DB_ERROR, message: 'MongoDbRepository database not intialized' }; diff --git a/cloud/wfm-rest-api/src/index.ts b/cloud/wfm-rest-api/src/index.ts index f40d19f..c141ee1 100644 --- a/cloud/wfm-rest-api/src/index.ts +++ b/cloud/wfm-rest-api/src/index.ts @@ -11,4 +11,4 @@ export * from './data-api/CrudRepository'; export * from './data-api/PageRequest'; export * from './data-api/PageResponse'; export * from './data-api/ApiError'; -export * from './data-api/PaginationEngine'; +export * from './data-api/MongoPaginationEngine'; From aeb411ac21180e583194d8c19c6b0f388a89c59d Mon Sep 17 00:00:00 2001 From: Wojciech Trocki Date: Fri, 11 Aug 2017 18:09:44 +0100 Subject: [PATCH 16/16] Unit tests madness --- cloud/wfm-rest-api/README.md | 20 +- cloud/wfm-rest-api/package.json | 9 +- cloud/wfm-rest-api/src/WfmRestApi.ts | 3 + .../src/data-api/MongoPaginationEngine.ts | 4 +- ...dRepository.ts => PagingDataRepository.ts} | 2 +- cloud/wfm-rest-api/src/impl/ApiController.ts | 195 ++++++++++-------- cloud/wfm-rest-api/src/impl/ErrorCodes.ts | 1 + .../src/impl/MongoDbRepository.ts | 13 +- cloud/wfm-rest-api/src/index.ts | 3 +- cloud/wfm-rest-api/test/ApiControllerTest.ts | 172 ++++++++++++++- .../test/MongoDbRepositoryTest.ts | 50 +++++ .../wfm-rest-api/test/PaginationEngineTest.ts | 10 +- cloud/wfm-rest-api/test/WfmRestApiTest.ts | 1 + demo/server/config-dev.json | 2 +- demo/server/config-prod.json | 2 +- 15 files changed, 384 insertions(+), 103 deletions(-) rename cloud/wfm-rest-api/src/data-api/{CrudRepository.ts => PagingDataRepository.ts} (97%) create mode 100644 cloud/wfm-rest-api/test/MongoDbRepositoryTest.ts diff --git a/cloud/wfm-rest-api/README.md b/cloud/wfm-rest-api/README.md index 4516139..4a37a07 100644 --- a/cloud/wfm-rest-api/README.md +++ b/cloud/wfm-rest-api/README.md @@ -54,7 +54,7 @@ Supports pagination and sorting by providing additional query parameters: - `size` number of elements to return - `sortField` sorting field - `order` -1 for DESC and 1 for ASC -` + Example `/workorders?page=0&size=5&sortField=id&order=-1` > **Note** - sorting parameters are optional. When missing default sorting values are applied (10 results) @@ -88,3 +88,21 @@ Example `/workorders/B1r71fOBr` ### Delete object > DELETE {object}/:objectId + +### Error handling + +Api returns non 200 status in case of error. + +`400` - For user input error (missing required field etc.) +`500` - For internal server errors + +Additionaly error metadata is being returned: + +```json +{ + "code":"InvalidID", + "message":"Provided id is invalid" +} +``` + +> **Note:** If you apply security middleware additional `401` and `403` statuses may be returned diff --git a/cloud/wfm-rest-api/package.json b/cloud/wfm-rest-api/package.json index ee1d5ee..b45c4f3 100644 --- a/cloud/wfm-rest-api/package.json +++ b/cloud/wfm-rest-api/package.json @@ -29,8 +29,8 @@ "report-dir": "coverage_report", "check-coverage": true, "lines": 75, - "functions": 100, - "branches": 80 + "functions": 80, + "branches": 70 }, "dependencies": { "@raincatcher/logger": "1.0.0", @@ -44,11 +44,16 @@ "@types/express": "^4.0.35", "@types/lodash": "^4.14.72", "@types/mocha": "^2.2.41", + "@types/mongodb": "^2.2.9", "@types/proxyquire": "^1.3.27", + "@types/sinon": "^2.3.3", + "@types/sinon-express-mock": "^1.3.2", "del-cli": "^1.0.0", "mocha": "^3.4.2", "nyc": "^11.0.1", "proxyquire": "^1.8.0", + "sinon": "^3.2.0", + "sinon-express-mock": "^1.3.1", "source-map-support": "^0.4.15", "ts-node": "^3.0.4", "typescript": "^2.3.4" diff --git a/cloud/wfm-rest-api/src/WfmRestApi.ts b/cloud/wfm-rest-api/src/WfmRestApi.ts index ed20937..983dc3a 100644 --- a/cloud/wfm-rest-api/src/WfmRestApi.ts +++ b/cloud/wfm-rest-api/src/WfmRestApi.ts @@ -27,8 +27,11 @@ export class WfmRestApi { public createWFMRouter() { const router: express.Router = express.Router(); const workorderController = new ApiController(router, this.workorderService, this.config.workorderApiName); + workorderController.applyAllRoutes(); const workflowController = new ApiController(router, this.workflowService, this.config.workflowApiName); + workflowController.applyAllRoutes(); const resultController = new ApiController(router, this.resultService, this.config.resultApiName); + resultController.applyAllRoutes(); return router; } diff --git a/cloud/wfm-rest-api/src/data-api/MongoPaginationEngine.ts b/cloud/wfm-rest-api/src/data-api/MongoPaginationEngine.ts index 80c27b2..389b459 100644 --- a/cloud/wfm-rest-api/src/data-api/MongoPaginationEngine.ts +++ b/cloud/wfm-rest-api/src/data-api/MongoPaginationEngine.ts @@ -5,7 +5,7 @@ import { DIRECTION, SortedPageRequest } from '../data-api/PageRequest'; import { PageResponse } from '../data-api/PageResponse'; /** - * Mongo pagination procesor + * Mongo pagination processor * Clients may override this class to provide custom pagination parameters * * Note: pages are counted starting from 0. @@ -66,7 +66,7 @@ export class MongoPaginationEngine { cursor = cursor.sort(request.sortField, request.order); } cursor = cursor.skip(request.size * request.page).limit(request.size); - return cursor.toArray().then(function(data) { + return Promise.resolve(cursor.toArray()).then(function(data) { const totalPages = Math.ceil(totalCount / request.size); return { totalPages, diff --git a/cloud/wfm-rest-api/src/data-api/CrudRepository.ts b/cloud/wfm-rest-api/src/data-api/PagingDataRepository.ts similarity index 97% rename from cloud/wfm-rest-api/src/data-api/CrudRepository.ts rename to cloud/wfm-rest-api/src/data-api/PagingDataRepository.ts index 855aec1..8dc85f5 100644 --- a/cloud/wfm-rest-api/src/data-api/CrudRepository.ts +++ b/cloud/wfm-rest-api/src/data-api/PagingDataRepository.ts @@ -10,7 +10,7 @@ import { PageResponse } from '../data-api/PageResponse'; * Interface is being used internally to perform database operations for WFM models * It's not recomended to use it for other application business logic. */ -export interface CrudRepository { +export interface PagingDataRepository { /** * Retrieve list of results from database diff --git a/cloud/wfm-rest-api/src/impl/ApiController.ts b/cloud/wfm-rest-api/src/impl/ApiController.ts index bf2aba4..b41f15e 100644 --- a/cloud/wfm-rest-api/src/impl/ApiController.ts +++ b/cloud/wfm-rest-api/src/impl/ApiController.ts @@ -1,118 +1,149 @@ import { getLogger } from '@raincatcher/logger'; import * as express from 'express'; import { ApiError } from '../data-api/ApiError'; -import { CrudRepository } from '../data-api/CrudRepository'; import { defaultPaginationEngine } from '../data-api/MongoPaginationEngine'; +import { PagingDataRepository } from '../data-api/PagingDataRepository'; import * as errorCodes from './ErrorCodes'; /** * Generic controller that can be used to create API for specific objects */ export class ApiController { - constructor(router: express.Router, repository: CrudRepository, readonly apiPrefix: string) { - getLogger().info('REST api initialization', apiPrefix); - this.buildRoutes(router, repository, apiPrefix); + constructor(readonly router: express.Router, readonly repository: PagingDataRepository, readonly apiPrefix: string) { } + /** - * Build routes for specific element of api - * - * @param router - router used to attach api - * @param repository - repository to retrieve data - * @param apiPrefix - prefix to mount api in URI path. For example `/prefix/:id` + * Handler for list method + * Can be reused by developers that wish to mount handler directly on router */ - protected buildRoutes(router: express.Router, repository: CrudRepository, apiPrefix: string) { + public listHandler(req: express.Request, res: express.Response) { const self = this; - const idRoute = router.route('/' + apiPrefix + '/:id'); - const objectRoute = router.route('/' + apiPrefix + '/'); - - objectRoute.get(function(req: express.Request, res: express.Response) { - getLogger().debug('Api list method called', - { object: apiPrefix, body: req.query }); - const page = defaultPaginationEngine.buildRequestFromQuery(req.query); - let filter = {}; - if (req.query.filter) { - try { - filter = JSON.parse(req.query.filter); - } catch (err) { - getLogger().debug('Invalid filter passed'); - } - } - if (req.body.filter) { - filter = req.body.filter; + getLogger().debug('Api list method called', + { object: self.apiPrefix, body: req.query }); + const page = defaultPaginationEngine.buildRequestFromQuery(req.query); + let filter = {}; + if (req.query.filter) { + try { + filter = JSON.parse(req.query.filter); + } catch (err) { + getLogger().debug('Invalid filter passed'); + const error: ApiError = { code: errorCodes.CLIENT_ERROR, message: 'Invalid filter query parameter' }; + return res.status(400).json(error); } - const objectList = repository.list(filter, page).then(function(data) { - res.send(data); - }).catch(function(err: ApiError) { - self.errorHandler(req, res, err); - }); + } + if (req.body.filter) { + filter = req.body.filter; + } + const objectList = self.repository.list(filter, page).then(function(data) { + res.json(data); + }).catch(function(err: ApiError) { + self.errorHandler(req, res, err); }); + } - objectRoute.post(function(req: express.Request, res: express.Response) { - getLogger().debug('Api create method called', - { object: apiPrefix, body: req.body }); + /** + * Handler for get method + * Can be reused by developers that wish to mount handler directly on router + */ + public getHandler(req: express.Request, res: express.Response) { + const self = this; + getLogger().debug('Api get method called', + { object: self.apiPrefix, params: req.params }); - if (!req.body) { - const error: ApiError = { code: errorCodes.CLIENT_ERROR, message: 'Missing request body' }; - return res.status(400).json(error); - } + if (!req.params.id) { + const error: ApiError = { code: errorCodes.MISSING_ID, message: 'Missing id parameter' }; + return res.status(400).json(error); + } - repository.create(req.body).then(function() { - res.send(); - }).catch(function(err: ApiError) { - self.errorHandler(req, res, err); - }); + self.repository.get(req.params.id).then(function(data) { + res.json(data); + }).catch(function(err: ApiError) { + self.errorHandler(req, res, err); }); + } - objectRoute.put(function(req: express.Request, res: express.Response) { - getLogger().debug('Api update method called', - { object: apiPrefix, body: req.body }); + /** + * Handler for create method + * Can be reused by developers that wish to mount handler directly on router + */ + public postHandler(req: express.Request, res: express.Response) { + const self = this; + getLogger().debug('Api create method called', + { object: self.apiPrefix, body: req.body }); - if (!req.body) { - const error = { code: errorCodes.CLIENT_ERROR, message: 'Missing request body' }; - return res.status(400).json(error); - } + if (!req.body) { + const error: ApiError = { code: errorCodes.CLIENT_ERROR, message: 'Missing request body' }; + return res.status(400).json(error); + } - repository.update(req.body).then(function(data) { - res.send(); - }).catch(function(err: ApiError) { - getLogger().error('Update error', { err: err.message, obj: req.body }); - const error: ApiError = { code: errorCodes.DB_ERROR, message: 'Failed to update object' }; - res.status(500).json(error); - }); + self.repository.create(req.body).then(function(data) { + res.json(data); + }).catch(function(err: ApiError) { + self.errorHandler(req, res, err); }); + } - idRoute.get(function(req: express.Request, res: express.Response) { - getLogger().debug('Api get method called', - { object: apiPrefix, params: req.params }); + /** + * Delete handler + * Can be reused by developers that wish to mount handler directly on router + */ + public deleteHandler(req: express.Request, res: express.Response) { + const self = this; + getLogger().debug('Api delete method called', + { object: self.apiPrefix, params: req.params }); - if (!req.params.id) { - const error: ApiError = { code: 'MissingId', message: 'Missing id parameter' }; - return res.status(400).json(error); - } + if (!req.params.id) { + const error: ApiError = { code: errorCodes.MISSING_ID, message: 'Missing id parameter' }; + return res.status(400).json(error); + } - repository.get(req.params.id).then(function(data) { - res.send(data); - }).catch(function(err: ApiError) { - self.errorHandler(req, res, err); - }); + self.repository.delete(req.params.id).then(function(data) { + res.json(); + }).catch(function(err) { + self.errorHandler(req, res, err); }); + } - idRoute.delete(function(req: express.Request, res: express.Response) { - getLogger().debug('Api delete method called', - { object: apiPrefix, params: req.params }); + /** + * Update handler + * Can be reused by developers that wish to mount handler directly on router + */ + public putHandler(req: express.Request, res: express.Response) { + const self = this; + getLogger().debug('Api update method called', + { object: self.apiPrefix, body: req.body }); - if (!req.params.id) { - const error: ApiError = { code: 'MissingId', message: 'Missing id parameter' }; - return res.status(400).json(error); - } + if (!req.body) { + const error = { code: errorCodes.CLIENT_ERROR, message: 'Missing request body' }; + return res.status(400).json(error); + } - repository.delete(req.params.id).then(function(data) { - res.send(data); - }).catch(function(err) { - self.errorHandler(req, res, err); - }); + self.repository.update(req.body).then(function(data) { + res.json(data); + }).catch(function(err: ApiError) { + self.errorHandler(req, res, err); }); } + + /** + * Build all CRUD routes for `apiPrefix` + * + * @param router - router used to attach api + * @param repository - repository to retrieve data + * @param apiPrefix - prefix to mount api in URI path. For example `/prefix/:id` + */ + public applyAllRoutes() { + const self = this; + const idRoute = this.router.route('/' + this.apiPrefix + '/:id'); + const objectRoute = this.router.route('/' + this.apiPrefix + '/'); + getLogger().info('REST api initialization', this.apiPrefix); + + objectRoute.get(this.listHandler.bind(this)); + objectRoute.post(this.postHandler.bind(this)); + objectRoute.put(this.putHandler.bind(this)); + idRoute.get(this.getHandler.bind(this)); + idRoute.delete(this.deleteHandler.bind(this)); + } protected errorHandler(req: express.Request, res: express.Response, error: ApiError) { getLogger().error('Api error', { error, obj: req.body }); res.status(500).json(error); diff --git a/cloud/wfm-rest-api/src/impl/ErrorCodes.ts b/cloud/wfm-rest-api/src/impl/ErrorCodes.ts index ce5a748..d0bd809 100644 --- a/cloud/wfm-rest-api/src/impl/ErrorCodes.ts +++ b/cloud/wfm-rest-api/src/impl/ErrorCodes.ts @@ -1,3 +1,4 @@ export const CLIENT_ERROR = 'CLIENT_ARGUMENT_ERROR'; +export const MISSING_ID = 'MISSING_ID'; export const DB_ERROR = 'DB_ERROR'; diff --git a/cloud/wfm-rest-api/src/impl/MongoDbRepository.ts b/cloud/wfm-rest-api/src/impl/MongoDbRepository.ts index 5d847ad..231237c 100644 --- a/cloud/wfm-rest-api/src/impl/MongoDbRepository.ts +++ b/cloud/wfm-rest-api/src/impl/MongoDbRepository.ts @@ -1,10 +1,10 @@ import * as Promise from 'bluebird'; import { Cursor, Db } from 'mongodb'; import { ApiError } from '../data-api/ApiError'; -import { CrudRepository } from '../data-api/CrudRepository'; import { defaultPaginationEngine } from '../data-api/MongoPaginationEngine'; import { DIRECTION, SortedPageRequest } from '../data-api/PageRequest'; import { PageResponse } from '../data-api/PageResponse'; +import { PagingDataRepository } from '../data-api/PagingDataRepository'; import * as errorCodes from './ErrorCodes'; const dbError: ApiError = { code: errorCodes.DB_ERROR, message: 'MongoDbRepository database not intialized' }; @@ -12,7 +12,7 @@ const dbError: ApiError = { code: errorCodes.DB_ERROR, message: 'MongoDbReposito /** * Service for performing data operations on mongodb database */ -export class MongoDbRepository implements CrudRepository { +export class MongoDbRepository implements PagingDataRepository { public db: any; @@ -28,8 +28,13 @@ export class MongoDbRepository implements CrudRepository { return Promise.reject(dbError); } const cursor: Cursor = this.db.collection(this.collectionName).find(filter); - return cursor.count(filter).then(function(totalNumber) { - return defaultPaginationEngine.buildPageResponse(request, cursor, totalNumber); + return new Promise((resolve, reject) => { + cursor.count(filter, function(err, totalNumber) { + if (err) { + return reject(err); + } + return resolve(defaultPaginationEngine.buildPageResponse(request, cursor, totalNumber)); + }); }); } diff --git a/cloud/wfm-rest-api/src/index.ts b/cloud/wfm-rest-api/src/index.ts index c141ee1..7cee2f8 100644 --- a/cloud/wfm-rest-api/src/index.ts +++ b/cloud/wfm-rest-api/src/index.ts @@ -1,4 +1,3 @@ - // WFM implementation export * from './WfmRestApi'; @@ -7,7 +6,7 @@ export * from './impl/ApiController'; export * from './impl/MongoDbRepository'; // API -export * from './data-api/CrudRepository'; +export * from './data-api/PagingDataRepository'; export * from './data-api/PageRequest'; export * from './data-api/PageResponse'; export * from './data-api/ApiError'; diff --git a/cloud/wfm-rest-api/test/ApiControllerTest.ts b/cloud/wfm-rest-api/test/ApiControllerTest.ts index eac4bad..9c49199 100644 --- a/cloud/wfm-rest-api/test/ApiControllerTest.ts +++ b/cloud/wfm-rest-api/test/ApiControllerTest.ts @@ -1,14 +1,182 @@ import * as assert from 'assert'; +import * as Bluebird from 'bluebird'; import * as express from 'express'; import * as proxyquire from 'proxyquire'; -import { ApiController, ApiError, MongoDbRepository } from '../src/index'; +import * as sinon from 'sinon'; +import { MISSING_ID } from '../src/index'; + +import { + ApiController, ApiError, MongoDbRepository, + PageResponse, PagingDataRepository, SortedPageRequest +} from '../src/index'; + +const testObj = { id: 1, name: 'test' }; +const listReponse: PageResponse = { + data: [testObj], + totalCount: 0, + totalPages: 0 +}; + +class MockRepository implements PagingDataRepository { + public list(filter: any, request: SortedPageRequest): Bluebird { + return Bluebird.resolve(listReponse); + } + + public get(id: any): Bluebird { + return Bluebird.resolve(testObj); + } + + public create(object: any): Bluebird { + return Bluebird.resolve(testObj); + } + + public update(object: any): Bluebird { + return Bluebird.resolve(testObj); + } + + public delete(id: any): Bluebird { + return Bluebird.resolve(testObj); + } +} + +const mockApi = { + repository: new MockRepository(), + apiPrefix: 'test' +}; describe('FeedHenry ApiController Tests', function() { - describe('ApiController', function() { + describe('ApiController routes creation', function() { it('create router', function() { const router = express.Router(); const repository = new MongoDbRepository('test'); const testSubject = new ApiController(router, repository, 'testApi'); }); + + it('verify list middleware', function(done) { + const router = express.Router(); + const repository = new MongoDbRepository('test'); + const testSubject = new ApiController(router, repository, 'testApi'); + const request = { + query: { + filter: '{}', + page: 0, + size: 1 + }, + body: { + filter: {} + } + }; + const response = { + json(data) { + assert.equal(data, listReponse); + done(); + }, + status(status) { + return response; + } + }; + testSubject.listHandler.bind(mockApi)(request, response); + }); + + it('verify list middleware fail', function(done) { + const router = express.Router(); + const repository = new MongoDbRepository('test'); + const testSubject = new ApiController(router, repository, 'testApi'); + const request = { + query: { + filter: {}, + page: 0, + size: 1 + }, + body: { + filter: {} + } + }; + const response = { + json(data) { + assert.ok(data); + done(); + }, + status(status) { + assert.ok(status); + return response; + } + }; + testSubject.listHandler.bind(mockApi)(request, response); + }); + + it('verify get middleware error', function(done) { + const router = express.Router(); + const repository = new MongoDbRepository('test'); + const testSubject = new ApiController(router, repository, 'testApi'); + const response = { + json(data) { + assert.equal(MISSING_ID, data.code); + done(); + }, + status(status) { + assert.equal(400, status); + return response; + } + }; + testSubject.getHandler.bind(mockApi)({ params: {} }, response); + }); + + it('verify get middleware success', function(done) { + const router = express.Router(); + const repository = new MongoDbRepository('test'); + const testSubject = new ApiController(router, repository, 'testApi'); + const response = { + json(data) { + assert.equal(testObj, data); + done(); + }, + status(status) { + assert.equal(400, status); + return response; + } + }; + testSubject.getHandler.bind(mockApi)({ params: { id: 1 } }, response); + }); + + it('verify post middleware success', function(done) { + const router = express.Router(); + const repository = new MongoDbRepository('test'); + const testSubject = new ApiController(router, repository, 'testApi'); + const response = { + json(data) { + assert.equal(testObj, data); + done(); + } + }; + testSubject.postHandler.bind(mockApi)({ body: testObj }, response); + }); + + it('verify put middleware success', function(done) { + const router = express.Router(); + const repository = new MongoDbRepository('test'); + const testSubject = new ApiController(router, repository, 'testApi'); + const response = { + json(data) { + assert.equal(testObj, data); + done(); + } + }; + testSubject.putHandler.bind(mockApi)({ body: testObj }, response); + }); + + it('verify delete middleware success', function(done) { + const router = express.Router(); + const repository = new MongoDbRepository('test'); + const testSubject = new ApiController(router, repository, 'testApi'); + const response = { + json(data) { + assert.ok(!data); + done(); + } + }; + testSubject.deleteHandler.bind(mockApi)({ params: { id: 1 } }, response); + }); + }); }); diff --git a/cloud/wfm-rest-api/test/MongoDbRepositoryTest.ts b/cloud/wfm-rest-api/test/MongoDbRepositoryTest.ts new file mode 100644 index 0000000..59386cc --- /dev/null +++ b/cloud/wfm-rest-api/test/MongoDbRepositoryTest.ts @@ -0,0 +1,50 @@ +import * as assert from 'assert'; +import * as proxyquire from 'proxyquire'; +import { MongoDbRepository } from '../src/index'; + +const testObj = { id: 1, name: 'test' }; + +const testSubject = new MongoDbRepository('testCollection'); +const testSubjectNoDb = new MongoDbRepository('testCollection'); +const mock = function() { return db; }; +const db: any = { + collection: mock, + findOne: mock, + insertOne: mock, + find() { + return { + count(filter, cb) { + cb(10); + } + }; + }, + updateOne: mock, + deleteOne: mock +}; +testSubject.setDb(db); + +describe('FeedHenry MongoDbRepository Tests', function() { + describe('Test CRUD operations', function() { + it('should create obj', function() { + assert.ok(testSubjectNoDb.create(testObj)); + assert.ok(testSubject.create(testObj)); + }); + it('should get obj', function() { + assert.ok(testSubjectNoDb.get('test')); + assert.ok(testSubject.get('test')); + }); + it('should list obj', function() { + const page = { order: -1, page: 0, size: 10, sortField: 'id' }; + assert.ok(testSubject.list({}, page)); + assert.ok(testSubjectNoDb.list({}, page)); + }); + it('should update obj', function() { + assert.ok(testSubject.update(testObj)); + assert.ok(testSubjectNoDb.update(testObj)); + }); + it('should delete obj', function() { + assert.ok(testSubject.delete('id')); + assert.ok(testSubjectNoDb.delete('id')); + }); + }); +}); diff --git a/cloud/wfm-rest-api/test/PaginationEngineTest.ts b/cloud/wfm-rest-api/test/PaginationEngineTest.ts index 5ada82f..aaa1e08 100644 --- a/cloud/wfm-rest-api/test/PaginationEngineTest.ts +++ b/cloud/wfm-rest-api/test/PaginationEngineTest.ts @@ -1,12 +1,12 @@ import * as assert from 'assert'; import * as Promise from 'bluebird'; import * as proxyquire from 'proxyquire'; -import { PaginationEngine } from '../src/index'; +import { MongoPaginationEngine } from '../src/index'; describe('FeedHenry PaginationEngine Tests', function() { describe('Test PaginationEngine api', function() { it('builds request ', function() { - const testSubject = new PaginationEngine(10); + const testSubject = new MongoPaginationEngine(10); const query = { page: 1, size: 10, @@ -21,14 +21,14 @@ describe('FeedHenry PaginationEngine Tests', function() { }); it('builds request with defaults', function() { const defaultPageSize = 10; - const testSubject = new PaginationEngine(defaultPageSize); + const testSubject = new MongoPaginationEngine(defaultPageSize); const query = {}; const request = testSubject.buildRequestFromQuery(query); assert.equal(request.page, 0); assert.equal(request.size, defaultPageSize); }); it('builds request ', function(done) { - const testSubject = new PaginationEngine(10); + const testSubject = new MongoPaginationEngine(10); const result = ['test', 'test2']; const query = { page: 1, @@ -38,7 +38,7 @@ describe('FeedHenry PaginationEngine Tests', function() { const request = testSubject.buildRequestFromQuery(query); assert.equal(request.page, query.page); assert.equal(request.size, query.size); - const cursor = { + const cursor: any = { sort(sortField, order) { assert.equal(request.sortField, sortField); assert.equal(request.order, order); diff --git a/cloud/wfm-rest-api/test/WfmRestApiTest.ts b/cloud/wfm-rest-api/test/WfmRestApiTest.ts index 21f2ca9..f6d8093 100644 --- a/cloud/wfm-rest-api/test/WfmRestApiTest.ts +++ b/cloud/wfm-rest-api/test/WfmRestApiTest.ts @@ -9,6 +9,7 @@ describe('FeedHenry Wfm api Tests', function() { assert.ok(testSubject.createWFMRouter()); }); }); + describe('Test mongo setup', function() { it('create router', function() { const testSubject = new WfmRestApi(); diff --git a/demo/server/config-dev.json b/demo/server/config-dev.json index 086fbbd..4d79826 100644 --- a/demo/server/config-dev.json +++ b/demo/server/config-dev.json @@ -2,7 +2,7 @@ "morganOptions": null, "logStackTraces": true, "security": { - "apiRole": "ADMIN" + "apiRole": "admin" }, "sync": { "customDataHandlers": true, diff --git a/demo/server/config-prod.json b/demo/server/config-prod.json index a9e85e1..4432646 100644 --- a/demo/server/config-prod.json +++ b/demo/server/config-prod.json @@ -6,7 +6,7 @@ "seedDemoData": true }, "security": { - "apiRole": "ADMIN" + "apiRole": "admin" }, "keycloakConfig": { "realm": "",