diff --git a/README.md b/README.md index 522a5a8..437116a 100644 --- a/README.md +++ b/README.md @@ -1,11 +1,33 @@ # DeFinMa - API -This is the API to access the database of the digital fingerprint of plastics project. +This is the API to access the database of the digital fingerprint of plastics project. A deep insight into the project +structure can be gained in the +[Bachelor Thesis](https://definma.apps.de1.bosch-iot-cloud.com/assets/docs/Veit-Lukas_Bachelor-Thesis.pdf). ## API documentation -The API is documented using the Open API Specification 3.0.2 and hosted together with the API under the `/api` path +The API is documented using the Open API Specification 3.0.2 in the [api](./api) and hosted together with the API under +the `/api-doc` path. The online version can be found [here](https://definma-api.apps.de1.bosch-iot-cloud.com/api-doc). +The files are automatically bundled and validated when starting the server. ## Testing Testing is done with mocha and can be executed using `npm test`. + +## General structure + +[index.ts](./src/index.ts) is exectued when starting the server. It includes all setup tasks, registers middleware, +routes and error handlers. Setting the `NODE_ENV` environment variable allows starting the server either in +`production`, `development` or `test` mode. + +All route files including the corresponding test files are in the [routes](./src/routes) folder. The +[validate](./src/routes/validate) folder stores Joi validations for data input and output. + +[models](./src/models) keeps all Mongoose database collection models. + +[helpers](./src/helpers) includes functions needed in multiple places like the authorization done for every route, +csv conversion and object flattening as well as a mail service, registering an email address using the mail service on +the BIC and replacing it with a logging statement in development. + +[test](./src/test) holds files for testing like the test database `db.json` loaded freshly for every test case and +a helper utilized for easier testing. `loadDev` loads the `db.json` into the `development` database for a fresh start. diff --git a/api/root.yaml b/api/root.yaml index 019bfb6..749bd27 100644 --- a/api/root.yaml +++ b/api/root.yaml @@ -47,14 +47,9 @@ 500: $ref: 'api.yaml#/components/responses/500' -/changelog/{timestamp}/{page}/{pagesize}: +/changelog/{id}/{page}/{pagesize}: parameters: - - name: timestamp - in: path - required: true - schema: - type: string - example: 1970-01-01T00:00:00.000Z + - $ref: 'api.yaml#/components/parameters/Id' - name: page in: path required: true @@ -69,9 +64,9 @@ example: 30 get: summary: get changelog - description: 'Auth: basic, levels: dev, admin
Displays all logs older than timestamp, sorted by date descending, - page defaults to 0, pagesize defaults to 25
Avoid using high page numbers for older logs, better use an older - timestamp' + description: 'Auth: basic, levels: dev, admin
Displays all logs older than timestamp of the given id, sorted by + date descending, page defaults to 0, pagesize defaults to 25 +
Avoid using high page numbers for older logs, better use an older timestamp' tags: - / responses: @@ -80,6 +75,8 @@ content: application/json: schema: + allOf: + - $ref: 'api.yaml#/components/schemas/_Id' properties: date: type: string diff --git a/src/routes/root.spec.ts b/src/routes/root.spec.ts index a397423..c0a7c26 100644 --- a/src/routes/root.spec.ts +++ b/src/routes/root.spec.ts @@ -25,16 +25,18 @@ describe('/', () => { it('returns the first page', done => { TestHelper.request(server, done, { method: 'get', - url: '/changelog/1979-07-28T06:04:51.000Z/0/2', + url: '/changelog/120000030000000000000000/0/2', auth: {basic: 'admin'}, httpStatus: 200 }).end((err, res) => { if (err) return done(err); + console.log(res.body); should(res.body).have.lengthOf(2); should(res.body[0].date).be.eql('1979-07-28T06:04:51.000Z'); should(res.body[1].date).be.eql('1979-07-28T06:04:50.000Z'); should(res.body).matchEach(log => { - should(log).have.only.keys('date', 'action', 'collection', 'conditions', 'data'); + should(log).have.only.keys('_id', 'date', 'action', 'collection', 'conditions', 'data'); + should(log).have.property('_id').be.type('string'); should(log).have.property('action', 'PUT /sample/400000000000000000000001'); should(log).have.property('collection', 'samples'); should(log).have.property('conditions', {_id: '400000000000000000000001'}); @@ -46,7 +48,7 @@ describe('/', () => { it('returns another page', done => { TestHelper.request(server, done, { method: 'get', - url: '/changelog/1979-07-28T06:04:51.000Z/1/2', + url: '/changelog/120000030000000000000000/1/2', auth: {basic: 'admin'}, httpStatus: 200 }).end((err, res) => { @@ -54,7 +56,8 @@ describe('/', () => { should(res.body).have.lengthOf(1); should(res.body[0].date).be.eql('1979-07-28T06:04:49.000Z'); should(res.body).matchEach(log => { - should(log).have.only.keys('date', 'action', 'collection', 'conditions', 'data'); + should(log).have.only.keys('_id', 'date', 'action', 'collection', 'conditions', 'data'); + should(log).have.property('_id').be.type('string'); should(log).have.property('action', 'PUT /sample/400000000000000000000001'); should(log).have.property('collection', 'samples'); should(log).have.property('conditions', {_id: '400000000000000000000001'}); @@ -66,7 +69,7 @@ describe('/', () => { it('returns an empty array for a page with no results', done => { TestHelper.request(server, done, { method: 'get', - url: '/changelog/1979-07-28T06:04:51.000Z/10/2', + url: '/changelog/120000030000000000000000/10/2', auth: {basic: 'admin'}, httpStatus: 200 }).end((err, res) => { @@ -75,28 +78,19 @@ describe('/', () => { done(); }); }); - it('rejects timestamps pre unix epoch', done => { + it('rejects invalid ids', done => { TestHelper.request(server, done, { method: 'get', - url: '/changelog/1879-07-28T06:04:51.000Z/10/2', + url: '/changelog/12000003000000h000000000/10/2', auth: {basic: 'admin'}, httpStatus: 400, - res: {status: 'Invalid body format', details: '"timestamp" must be greater than or equal to "1970-01-01T00:00:00.000Z"'} - }); - }); - it('rejects invalid timestamps', done => { - TestHelper.request(server, done, { - method: 'get', - url: '/changelog/1979-14-28T06:04:51.000Z/10/2', - auth: {basic: 'admin'}, - httpStatus: 400, - res: {status: 'Invalid body format', details: '"timestamp" must be in ISO 8601 date format'} + res: {status: 'Invalid body format', details: 'Invalid object id'} }); }); it('rejects negative page numbers', done => { TestHelper.request(server, done, { method: 'get', - url: '/changelog/1979-07-28T06:04:51.000Z/-10/2', + url: '/changelog/120000030000000000000000/-10/2', auth: {basic: 'admin'}, httpStatus: 400, res: {status: 'Invalid body format', details: '"page" must be greater than or equal to 0'} @@ -105,7 +99,7 @@ describe('/', () => { it('rejects negative pagesizes', done => { TestHelper.request(server, done, { method: 'get', - url: '/changelog/1979-07-28T06:04:51.000Z/10/-2', + url: '/changelog/120000030000000000000000/10/-2', auth: {basic: 'admin'}, httpStatus: 400, res: {status: 'Invalid body format', details: '"pagesize" must be greater than or equal to 0'} @@ -114,7 +108,7 @@ describe('/', () => { it('rejects request from a write user', done => { TestHelper.request(server, done, { method: 'get', - url: '/changelog/1979-07-28T06:04:51.000Z/10/2', + url: '/changelog/120000030000000000000000/10/2', auth: {basic: 'janedoe'}, httpStatus: 403 }); @@ -122,7 +116,7 @@ describe('/', () => { it('rejects requests from an API key', done => { TestHelper.request(server, done, { method: 'get', - url: '/changelog/1979-07-28T06:04:51.000Z/10/2', + url: '/changelog/120000030000000000000000/10/2', auth: {key: 'admin'}, httpStatus: 401 }); @@ -130,7 +124,7 @@ describe('/', () => { it('rejects unauthorized requests', done => { TestHelper.request(server, done, { method: 'get', - url: '/changelog/1979-07-28T06:04:51.000Z/10/2', + url: '/changelog/120000030000000000000000/10/2', httpStatus: 401 }); }); diff --git a/src/routes/root.ts b/src/routes/root.ts index cee54fe..165da7e 100644 --- a/src/routes/root.ts +++ b/src/routes/root.ts @@ -22,19 +22,18 @@ router.get('/authorized', (req, res) => { }); }); -router.get('/changelog/:timestamp/:page?/:pagesize?', (req, res, next) => { +router.get('/changelog/:id/:page?/:pagesize?', (req, res, next) => { if (!req.auth(res, ['dev', 'admin'], 'basic')) return; const {error, value: options} = RootValidate.changelogParams({ - timestamp: req.params.timestamp, + id: req.params.id, page: req.params.page, pagesize: req.params.pagesize }); if (error) return res400(error, res); - const id = new mongoose.Types - .ObjectId(Math.floor(new Date(options.timestamp).getTime() / 1000).toString(16) + '0000000000000000'); - ChangelogModel.find({_id: {$lte: id}}).sort({_id: -1}).skip(options.page * options.pagesize).limit(options.pagesize) + ChangelogModel.find({_id: {$lte: mongoose.Types.ObjectId(options.id)}}) + .sort({_id: -1}).skip(options.page * options.pagesize).limit(options.pagesize) .lean().exec((err, data) => { if (err) return next(err); diff --git a/src/routes/validate/root.ts b/src/routes/validate/root.ts index a932196..a6dfe33 100644 --- a/src/routes/validate/root.ts +++ b/src/routes/validate/root.ts @@ -28,7 +28,7 @@ export default class RootValidate { // validate input for root methods static changelogParams (data) { return Joi.object({ - timestamp: this.changelog.timestamp.required(), + id: IdValidate.get(), page: this.changelog.page, pagesize: this.changelog.pagesize }).validate(data); @@ -39,6 +39,7 @@ export default class RootValidate { // validate input for root methods data.collection = data.collection_name; data = IdValidate.stringify(data); const {value, error} = Joi.object({ + _id: IdValidate.get(), date: this.changelog.timestamp, action: this.changelog.action, collection: this.changelog.collection,