diff --git a/.idea/dictionaries/VLE2FE.xml b/.idea/dictionaries/VLE2FE.xml
index 5e9cd2d..62d7fb4 100644
--- a/.idea/dictionaries/VLE2FE.xml
+++ b/.idea/dictionaries/VLE2FE.xml
@@ -37,6 +37,9 @@
lati
lyucy
materialnumber
+ modela
+ modelb
+ modelx
nvmrc
oldpass
opblock
diff --git a/api/model.yaml b/api/model.yaml
index f9c3d72..b701df4 100644
--- a/api/model.yaml
+++ b/api/model.yaml
@@ -2,7 +2,7 @@
parameters:
- $ref: 'api.yaml#/components/parameters/Name'
get:
- summary: TODO get model data by name
+ summary: get model data by name
description: 'Auth: all, levels: dev, admin'
tags:
- /model
@@ -22,14 +22,14 @@
$ref: 'api.yaml#/components/responses/404'
500:
$ref: 'api.yaml#/components/responses/500'
- put:
- summary: TODO add/replace model data by name
+ post:
+ summary: add/replace model data by name
description: 'Auth: all, levels: dev, admin'
tags:
- /model
requestBody:
required: true
- description: binary model data
+ description: binary model data, Content-Type header must be set to application/octet-stream
content:
application/json:
schema:
@@ -38,18 +38,14 @@
responses:
200:
$ref: 'api.yaml#/components/responses/Ok'
- 400:
- $ref: 'api.yaml#/components/responses/400'
401:
$ref: 'api.yaml#/components/responses/401'
403:
$ref: 'api.yaml#/components/responses/403'
- 404:
- $ref: 'api.yaml#/components/responses/404'
500:
$ref: 'api.yaml#/components/responses/500'
delete:
- summary: TODO delete model data
+ summary: delete model data
description: 'Auth: basic, levels: dev, admin'
tags:
- /model
diff --git a/src/index.ts b/src/index.ts
index 1e763b7..5b4ed8f 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -111,9 +111,10 @@ if (process.env.NODE_ENV !== 'production') {
app.use('/', require('./routes/root'));
app.use('/', require('./routes/sample'));
app.use('/', require('./routes/material'));
-app.use('/', require('./routes/template'));
-app.use('/', require('./routes/user'));
app.use('/', require('./routes/measurement'));
+app.use('/', require('./routes/template'));
+app.use('/', require('./routes/model'));
+app.use('/', require('./routes/user'));
// static files
app.use('/static', express.static('static'));
diff --git a/src/models/model.ts b/src/models/model.ts
new file mode 100644
index 0000000..925601f
--- /dev/null
+++ b/src/models/model.ts
@@ -0,0 +1,8 @@
+import mongoose from 'mongoose';
+
+const ModelSchema = new mongoose.Schema({
+ name: {type: String, index: {unique: true}},
+ data: Buffer
+});
+
+export default mongoose.model>('model', ModelSchema);
\ No newline at end of file
diff --git a/src/routes/model.spec.ts b/src/routes/model.spec.ts
new file mode 100644
index 0000000..382efd4
--- /dev/null
+++ b/src/routes/model.spec.ts
@@ -0,0 +1,197 @@
+import should from 'should/as-function';
+import ModelModel from '../models/model';
+import TestHelper from "../test/helper";
+
+
+describe('/model', () => {
+ let server;
+ before(done => TestHelper.before(done));
+ beforeEach(done => server = TestHelper.beforeEach(server, done));
+ afterEach(done => TestHelper.afterEach(server, done));
+ after(done => TestHelper.after(done));
+
+ describe('GET /model/{name}', (() => {
+ it('returns the binary data', done => {
+ TestHelper.request(server, done, {
+ method: 'get',
+ url: '/model/modela',
+ auth: {basic: 'admin'},
+ httpStatus: 200,
+ contentType: 'application/octet-stream; charset=utf-8',
+ }).end((err, res) => {
+ if (err) return done (err);
+ should(res.body.toString()).be.eql('binary data');
+ done();
+ });
+ });
+ it('returns the binary data for an API key', done => {
+ TestHelper.request(server, done, {
+ method: 'get',
+ url: '/model/modela',
+ auth: {key: 'admin'},
+ httpStatus: 200,
+ contentType: 'application/octet-stream; charset=utf-8',
+ }).end((err, res) => {
+ if (err) return done (err);
+ should(res.body.toString()).be.eql('binary data');
+ done();
+ });
+ });
+ it('returns 404 for an unknown name', done => {
+ TestHelper.request(server, done, {
+ method: 'get',
+ url: '/model/modelx',
+ auth: {basic: 'admin'},
+ httpStatus: 404
+ })
+ });
+ it('rejects requests from a write user', done => {
+ TestHelper.request(server, done, {
+ method: 'get',
+ url: '/model/modela',
+ auth: {basic: 'janedoe'},
+ httpStatus: 403
+ })
+ });
+ it('rejects unauthorized requests', done => {
+ TestHelper.request(server, done, {
+ method: 'get',
+ url: '/model/modela',
+ httpStatus: 401
+ })
+ });
+ }));
+
+ describe('POST /model/{name}', () => {
+ it('stores the data', done => {
+ TestHelper.request(server, done, {
+ method: 'post',
+ url: '/model/modelb',
+ auth: {basic: 'admin'},
+ httpStatus: 200,
+ reqContentType: 'application/octet-stream',
+ req: 'another binary data'
+ }).end((err, res) => {
+ if (err) return done (err);
+ should(res.body).be.eql({status: 'OK'});
+ ModelModel.find({name: 'modelb'}).lean().exec((err, data) => {
+ if (err) return done (err);
+ should(data).have.lengthOf(1);
+ should(data[0]).have.only.keys('_id', 'name', 'data', '__v');
+ should(data[0]).have.property('name', 'modelb');
+ should(data[0].data.buffer.toString()).be.eql('another binary data');
+ done();
+ });
+ });
+ });
+ it('stores the data with an API key', done => {
+ TestHelper.request(server, done, {
+ method: 'post',
+ url: '/model/modelb',
+ auth: {key: 'admin'},
+ httpStatus: 200,
+ reqContentType: 'application/octet-stream',
+ req: 'another binary data'
+ }).end((err, res) => {
+ if (err) return done (err);
+ should(res.body).be.eql({status: 'OK'});
+ ModelModel.find({name: 'modelb'}).lean().exec((err, data) => {
+ if (err) return done (err);
+ should(data).have.lengthOf(1);
+ should(data[0]).have.only.keys('_id', 'name', 'data', '__v');
+ should(data[0]).have.property('name', 'modelb');
+ should(data[0].data.buffer.toString()).be.eql('another binary data');
+ done();
+ });
+ });
+ });
+ it('overwrites existing data', done => {
+ TestHelper.request(server, done, {
+ method: 'post',
+ url: '/model/modela',
+ auth: {basic: 'admin'},
+ httpStatus: 200,
+ reqContentType: 'application/octet-stream',
+ req: 'another binary data'
+ }).end((err, res) => {
+ if (err) return done (err);
+ should(res.body).be.eql({status: 'OK'});
+ ModelModel.find({name: 'modela'}).lean().exec((err, data) => {
+ if (err) return done (err);
+ should(data).have.lengthOf(1);
+ should(data[0]).have.only.keys('_id', 'name', 'data', '__v');
+ should(data[0]).have.property('name', 'modela');
+ should(data[0].data.buffer.toString()).be.eql('another binary data');
+ done();
+ });
+ });
+ });
+ it('rejects requests from a write user', done => {
+ TestHelper.request(server, done, {
+ method: 'post',
+ url: '/model/modelb',
+ auth: {basic: 'janedoe'},
+ httpStatus: 403,
+ req: 'another binary data'
+ });
+ });
+ it('rejects unauthorized requests', done => {
+ TestHelper.request(server, done, {
+ method: 'post',
+ url: '/model/modelb',
+ httpStatus: 401,
+ req: 'another binary data'
+ });
+ });
+ });
+
+ describe('DELETE /model/{name}', () => {
+ it('deletes the data', done => {
+ TestHelper.request(server, done, {
+ method: 'delete',
+ url: '/model/modela',
+ auth: {basic: 'admin'},
+ httpStatus: 200
+ }).end((err, res) => {
+ if (err) return done(err);
+ should(res.body).be.eql({status: 'OK'});
+ ModelModel.find({name: 'modela'}).lean().exec((err, data) => {
+ if (err) return done(err);
+ should(data).have.lengthOf(0);
+ done();
+ });
+ });
+ });
+ it('returns 404 for an unknown name', done => {
+ TestHelper.request(server, done, {
+ method: 'delete',
+ url: '/model/modelx',
+ auth: {basic: 'admin'},
+ httpStatus: 404
+ });
+ });
+ it('rejects an API key', done => {
+ TestHelper.request(server, done, {
+ method: 'delete',
+ url: '/model/modela',
+ auth: {key: 'admin'},
+ httpStatus: 401
+ });
+ });
+ it('rejects a write user', done => {
+ TestHelper.request(server, done, {
+ method: 'delete',
+ url: '/model/modela',
+ auth: {basic: 'janedoe'},
+ httpStatus: 403
+ });
+ });
+ it('rejects an unauthorized request', done => {
+ TestHelper.request(server, done, {
+ method: 'delete',
+ url: '/model/modela',
+ httpStatus: 401
+ });
+ });
+ });
+});
\ No newline at end of file
diff --git a/src/routes/model.ts b/src/routes/model.ts
new file mode 100644
index 0000000..882de58
--- /dev/null
+++ b/src/routes/model.ts
@@ -0,0 +1,47 @@
+import express from 'express';
+import bodyParser from 'body-parser';
+
+import ModelModel from '../models/model';
+
+const router = express.Router();
+
+router.get('/model/:name', (req, res, next) => {
+ if (!req.auth(res, ['dev', 'admin'], 'all')) return;
+
+ ModelModel.findOne({name: req.params.name}).lean().exec((err, data) => {
+ if (err) return next(err);
+ if (data) {
+ res.set('Content-Type', 'application/octet-stream');
+ res.send(data.data.buffer);
+ }
+ else {
+ res.status(404).json({status: 'Not found'});
+ }
+ });
+});
+
+router.post('/model/:name', bodyParser.raw({limit: '500kb'}), (req, res, next) => {
+ if (!req.auth(res, ['dev', 'admin'], 'all')) return;
+
+ ModelModel.replaceOne({name: req.params.name}, {name: req.params.name, data: req.body}).setOptions({upsert: true})
+ .lean().exec(err => {
+ if (err) return next(err);
+ res.json({status: 'OK'});
+ });
+});
+
+router.delete('/model/:name', (req, res, next) => {
+ if (!req.auth(res, ['dev', 'admin'], 'basic')) return;
+
+ ModelModel.findOneAndDelete({name: req.params.name}).lean().exec((err, data) => {
+ if (err) return next(err);
+ if (data) {
+ res.json({status: 'OK'});
+ }
+ else {
+ res.status(404).json({status: 'Not found'});
+ }
+ });
+});
+
+module.exports = router;
\ No newline at end of file
diff --git a/src/test/db.json b/src/test/db.json
index 83fba42..468c43d 100644
--- a/src/test/db.json
+++ b/src/test/db.json
@@ -678,6 +678,13 @@
"__v": 0
}
],
+ "models": [
+ {
+ "_id": {"$oid":"140000000000000000000001"},
+ "name": "modela",
+ "data": {"buffer": "binary data"}
+ }
+ ],
"users": [
{
"_id": {"$oid":"000000000000000000000001"},
diff --git a/src/test/helper.ts b/src/test/helper.ts
index d4b11e4..ff337a3 100644
--- a/src/test/helper.ts
+++ b/src/test/helper.ts
@@ -69,6 +69,9 @@ export default class TestHelper {
if (options.hasOwnProperty('req')) { // request body
st = st.send(options.req);
}
+ if (options.hasOwnProperty('reqContentType')) { // request body
+ st = st.set('Content-Type', options.reqContentType);
+ }
if (options.hasOwnProperty('auth') && options.auth.hasOwnProperty('basic')) { // resolve basic auth
if (this.auth.hasOwnProperty(options.auth.basic)) {
st = st.auth(options.auth.basic, this.auth[options.auth.basic].pass)