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": "",