added model templates
This commit is contained in:
parent
656795f02c
commit
e8529c2b23
@ -1,4 +1,79 @@
|
|||||||
/model/{name}:
|
/model/groups:
|
||||||
|
get:
|
||||||
|
summary: list all available groups
|
||||||
|
description: 'Auth: basic, levels: read, write, dev, admin'
|
||||||
|
tags:
|
||||||
|
- /model
|
||||||
|
responses:
|
||||||
|
200:
|
||||||
|
description: all groups
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
type: array
|
||||||
|
items:
|
||||||
|
properties:
|
||||||
|
group:
|
||||||
|
type: string
|
||||||
|
example: VN
|
||||||
|
models:
|
||||||
|
type: array
|
||||||
|
items:
|
||||||
|
$ref: 'api.yaml#/components/schemas/ModelItem'
|
||||||
|
401:
|
||||||
|
$ref: 'api.yaml#/components/responses/401'
|
||||||
|
500:
|
||||||
|
$ref: 'api.yaml#/components/responses/500'
|
||||||
|
|
||||||
|
/model/{group}:
|
||||||
|
parameters:
|
||||||
|
- $ref: 'api.yaml#/components/parameters/Group'
|
||||||
|
post:
|
||||||
|
summary: add/replace model group item
|
||||||
|
description: 'Auth: basic, levels: dev, admin <br> If the given name exists, the item is replaced,
|
||||||
|
otherwise it is newly created'
|
||||||
|
tags:
|
||||||
|
- /model
|
||||||
|
requestBody:
|
||||||
|
required: true
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: 'api.yaml#/components/schemas/ModelItem'
|
||||||
|
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'
|
||||||
|
500:
|
||||||
|
$ref: 'api.yaml#/components/responses/500'
|
||||||
|
|
||||||
|
/model/{group}/{name}:
|
||||||
|
parameters:
|
||||||
|
- $ref: 'api.yaml#/components/parameters/Group'
|
||||||
|
- $ref: 'api.yaml#/components/parameters/Name'
|
||||||
|
delete:
|
||||||
|
summary: remove model group item
|
||||||
|
description: 'Auth: basic, levels: dev, admin'
|
||||||
|
tags:
|
||||||
|
- /model
|
||||||
|
responses:
|
||||||
|
200:
|
||||||
|
$ref: 'api.yaml#/components/responses/Ok'
|
||||||
|
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'
|
||||||
|
|
||||||
|
/model/file/{name}:
|
||||||
parameters:
|
parameters:
|
||||||
- $ref: 'api.yaml#/components/parameters/Name'
|
- $ref: 'api.yaml#/components/parameters/Name'
|
||||||
get:
|
get:
|
||||||
|
@ -39,3 +39,11 @@ Collection:
|
|||||||
schema:
|
schema:
|
||||||
type: string
|
type: string
|
||||||
example: condition
|
example: condition
|
||||||
|
|
||||||
|
Group:
|
||||||
|
name: group
|
||||||
|
in: path
|
||||||
|
required: true
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
example: vn
|
@ -211,3 +211,15 @@ User:
|
|||||||
items:
|
items:
|
||||||
type: string
|
type: string
|
||||||
example: Alpha II
|
example: Alpha II
|
||||||
|
|
||||||
|
ModelItem:
|
||||||
|
properties:
|
||||||
|
name:
|
||||||
|
type: string
|
||||||
|
example: Model 1.1
|
||||||
|
url:
|
||||||
|
type: string
|
||||||
|
example: https://definma-model-test.apps.de1.bosch-iot-cloud.com/predict/model1-1
|
||||||
|
label:
|
||||||
|
type: string
|
||||||
|
example: 'ml/g'
|
@ -1,8 +1,20 @@
|
|||||||
import mongoose from 'mongoose';
|
import mongoose from 'mongoose';
|
||||||
|
import db from '../db';
|
||||||
|
|
||||||
const ModelSchema = new mongoose.Schema({
|
const ModelSchema = new mongoose.Schema({
|
||||||
|
group: {type: String, index: {unique: true}},
|
||||||
|
models: [new mongoose.Schema({
|
||||||
name: {type: String, index: {unique: true}},
|
name: {type: String, index: {unique: true}},
|
||||||
data: Buffer
|
url: String,
|
||||||
|
label: String
|
||||||
|
} ,{ _id : false })]
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// changelog query helper
|
||||||
|
ModelSchema.query.log = function <Q extends mongoose.DocumentQuery<any, any>> (req) {
|
||||||
|
db.log(req, this);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
ModelSchema.index({group: 1});
|
||||||
|
|
||||||
export default mongoose.model<any, mongoose.Model<any, any>>('model', ModelSchema);
|
export default mongoose.model<any, mongoose.Model<any, any>>('model', ModelSchema);
|
8
src/models/model_file.ts
Normal file
8
src/models/model_file.ts
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
import mongoose from 'mongoose';
|
||||||
|
|
||||||
|
const ModelFileSchema = new mongoose.Schema({
|
||||||
|
name: {type: String, index: {unique: true}},
|
||||||
|
data: Buffer
|
||||||
|
});
|
||||||
|
|
||||||
|
export default mongoose.model<any, mongoose.Model<any, any>>('model_file', ModelFileSchema);
|
@ -1,6 +1,8 @@
|
|||||||
import should from 'should/as-function';
|
import should from 'should/as-function';
|
||||||
import ModelModel from '../models/model';
|
import ModelFileModel from '../models/model_file';
|
||||||
import TestHelper from "../test/helper";
|
import TestHelper from "../test/helper";
|
||||||
|
import ModelModel from '../models/model';
|
||||||
|
import _ from 'lodash';
|
||||||
|
|
||||||
|
|
||||||
describe('/model', () => {
|
describe('/model', () => {
|
||||||
@ -10,11 +12,269 @@ describe('/model', () => {
|
|||||||
afterEach(done => TestHelper.afterEach(server, done));
|
afterEach(done => TestHelper.afterEach(server, done));
|
||||||
after(done => TestHelper.after(done));
|
after(done => TestHelper.after(done));
|
||||||
|
|
||||||
describe('GET /model/{name}', (() => {
|
describe('GET /model/groups', () => {
|
||||||
|
it('returns all groups', done => {
|
||||||
|
TestHelper.request(server, done, {
|
||||||
|
method: 'get',
|
||||||
|
url: '/model/groups',
|
||||||
|
auth: {basic: 'janedoe'},
|
||||||
|
httpStatus: 200,
|
||||||
|
}).end((err, res) => {
|
||||||
|
if (err) return done (err);
|
||||||
|
const json = require('../test/db.json');
|
||||||
|
should(res.body).have.lengthOf(json.collections.models.length);
|
||||||
|
should(res.body).matchEach(group => {
|
||||||
|
should(group).have.only.keys('group', 'models');
|
||||||
|
should(group).have.property('group').be.type('string');
|
||||||
|
should(group.models).matchEach(model => {
|
||||||
|
should(model).have.only.keys('name', 'url', 'label');
|
||||||
|
should(model).have.property('name').be.type('string');
|
||||||
|
should(model).have.property('url').be.type('string');
|
||||||
|
should(model).have.property('label').be.type('string');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
it('rejects an API key', done => {
|
||||||
|
TestHelper.request(server, done, {
|
||||||
|
method: 'get',
|
||||||
|
url: '/model/groups',
|
||||||
|
auth: {key: 'janedoe'},
|
||||||
|
httpStatus: 401,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
it('rejects an unauthorized request', done => {
|
||||||
|
TestHelper.request(server, done, {
|
||||||
|
method: 'get',
|
||||||
|
url: '/model/groups',
|
||||||
|
httpStatus: 401,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('POST /model/{group}', () => {
|
||||||
|
it('adds a new model', done => {
|
||||||
|
TestHelper.request(server, done, {
|
||||||
|
method: 'post',
|
||||||
|
url: '/model/VN',
|
||||||
|
auth: {basic: 'admin'},
|
||||||
|
httpStatus: 200,
|
||||||
|
req: {name: 'Model C', url: 'http://model-c.com', label: 'ml/g'}
|
||||||
|
}).end((err, res) => {
|
||||||
|
if (err) return done(err);
|
||||||
|
should(res.body).be.eql({status: 'OK'});
|
||||||
|
ModelModel.findOne({group: 'VN'}).lean().exec((err, res) => {
|
||||||
|
if (err) return done(err);
|
||||||
|
const model = res.models.find(e => e.name === 'Model C');
|
||||||
|
should(model).have.property('url', 'http://model-c.com');
|
||||||
|
should(model).have.property('label', 'ml/g');
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
it('adds a new group', done => {
|
||||||
|
TestHelper.request(server, done, {
|
||||||
|
method: 'post',
|
||||||
|
url: '/model/classification',
|
||||||
|
auth: {basic: 'admin'},
|
||||||
|
httpStatus: 200,
|
||||||
|
req: {name: 'Model 0.1', url: 'http://model-0-1.com', label: 'group'}
|
||||||
|
}).end((err, res) => {
|
||||||
|
if (err) return done(err);
|
||||||
|
should(res.body).be.eql({status: 'OK'});
|
||||||
|
ModelModel.findOne({group: 'classification'}).lean().exec((err, res) => {
|
||||||
|
if (err) return done(err);
|
||||||
|
should(_.omit(res, ['_id', '__v'])).be.eql({group: 'classification', models: [{name: 'Model 0.1', url: 'http://model-0-1.com', label: 'group'}]});
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
it('replaces a model', done => {
|
||||||
|
TestHelper.request(server, done, {
|
||||||
|
method: 'post',
|
||||||
|
url: '/model/VN',
|
||||||
|
auth: {basic: 'admin'},
|
||||||
|
httpStatus: 200,
|
||||||
|
req: {name: 'Model A', url: 'http://model-a-new.com', label: 'ml/cm3'}
|
||||||
|
}).end((err, res) => {
|
||||||
|
if (err) return done(err);
|
||||||
|
should(res.body).be.eql({status: 'OK'});
|
||||||
|
ModelModel.findOne({group: 'VN'}).lean().exec((err, res) => {
|
||||||
|
if (err) return done(err);
|
||||||
|
const model = res.models.find(e => e.name === 'Model A');
|
||||||
|
should(model).have.property('url', 'http://model-a-new.com');
|
||||||
|
should(model).have.property('label', 'ml/cm3');
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
it('accepts an empty label', done => {
|
||||||
|
TestHelper.request(server, done, {
|
||||||
|
method: 'post',
|
||||||
|
url: '/model/VN',
|
||||||
|
auth: {basic: 'admin'},
|
||||||
|
httpStatus: 200,
|
||||||
|
req: {name: 'Model C', url: 'http://model-c.com', label: ''}
|
||||||
|
}).end((err, res) => {
|
||||||
|
if (err) return done(err);
|
||||||
|
should(res.body).be.eql({status: 'OK'});
|
||||||
|
ModelModel.findOne({group: 'VN'}).lean().exec((err, res) => {
|
||||||
|
if (err) return done(err);
|
||||||
|
const model = res.models.find(e => e.name === 'Model C');
|
||||||
|
should(model).have.property('url', 'http://model-c.com');
|
||||||
|
should(model).have.property('label', '');
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
it('rejects an empty name', done => {
|
||||||
|
TestHelper.request(server, done, {
|
||||||
|
method: 'post',
|
||||||
|
url: '/model/VN',
|
||||||
|
auth: {basic: 'admin'},
|
||||||
|
httpStatus: 400,
|
||||||
|
req: {name: '', url: 'http://model-c.com', label: 'ml/g'},
|
||||||
|
res:{status: 'Invalid body format', details: '"name" is not allowed to be empty'}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
it('rejects a missing name', done => {
|
||||||
|
TestHelper.request(server, done, {
|
||||||
|
method: 'post',
|
||||||
|
url: '/model/VN',
|
||||||
|
auth: {basic: 'admin'},
|
||||||
|
httpStatus: 400,
|
||||||
|
req: {url: 'http://model-c.com', label: 'ml/g'},
|
||||||
|
res:{status: 'Invalid body format', details: '"name" is required'}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
it('rejects an invalid URL', done => {
|
||||||
|
TestHelper.request(server, done, {
|
||||||
|
method: 'post',
|
||||||
|
url: '/model/VN',
|
||||||
|
auth: {basic: 'admin'},
|
||||||
|
httpStatus: 400,
|
||||||
|
req: {name: 'Model C', url: 'model-c', label: 'ml/g'},
|
||||||
|
res:{status: 'Invalid body format', details: '"url" must be a valid uri'}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
it('rejects a missing URL', done => {
|
||||||
|
TestHelper.request(server, done, {
|
||||||
|
method: 'post',
|
||||||
|
url: '/model/VN',
|
||||||
|
auth: {basic: 'admin'},
|
||||||
|
httpStatus: 400,
|
||||||
|
req: {name: 'Model C', label: 'ml/g'},
|
||||||
|
res:{status: 'Invalid body format', details: '"url" is required'}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
it('rejects a write user', done => {
|
||||||
|
TestHelper.request(server, done, {
|
||||||
|
method: 'post',
|
||||||
|
url: '/model/VN',
|
||||||
|
auth: {basic: 'janedoe'},
|
||||||
|
httpStatus: 403,
|
||||||
|
req: {name: 'Model C', url: 'http://model-c.com', label: 'ml/g'}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
it('rejects an API key', done => {
|
||||||
|
TestHelper.request(server, done, {
|
||||||
|
method: 'post',
|
||||||
|
url: '/model/VN',
|
||||||
|
auth: {key: 'admin'},
|
||||||
|
httpStatus: 401,
|
||||||
|
req: {name: 'Model C', url: 'http://model-c.com', label: 'ml/g'}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
it('rejects an unauthorized request', done => {
|
||||||
|
TestHelper.request(server, done, {
|
||||||
|
method: 'post',
|
||||||
|
url: '/model/VN',
|
||||||
|
httpStatus: 401,
|
||||||
|
req: {name: 'Model C', url: 'http://model-c.com', label: 'ml/g'}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('DELETE /model/{group}/{name}', () => {
|
||||||
|
it('deletes the model', done => {
|
||||||
|
TestHelper.request(server, done, {
|
||||||
|
method: 'delete',
|
||||||
|
url: '/model/VN/Model%20A',
|
||||||
|
auth: {basic: 'admin'},
|
||||||
|
httpStatus: 200
|
||||||
|
}).end((err, res) => {
|
||||||
|
if (err) return done(err);
|
||||||
|
should(res.body).be.eql({status: 'OK'});
|
||||||
|
ModelModel.findOne({group: 'VN'}).lean().exec((err, res) => {
|
||||||
|
if (err) return done(err);
|
||||||
|
should(_.omit(res, ['_id'])).be.eql({group: 'VN', models: [{name: 'Model B', url: 'http://model-b.com', label: 'ml/g'}]});
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
it('deletes the group, if empty afterwards', done => {
|
||||||
|
TestHelper.request(server, done, {
|
||||||
|
method: 'delete',
|
||||||
|
url: '/model/Moisture/Model%201',
|
||||||
|
auth: {basic: 'admin'},
|
||||||
|
httpStatus: 200
|
||||||
|
}).end((err, res) => {
|
||||||
|
if (err) return done(err);
|
||||||
|
should(res.body).be.eql({status: 'OK'});
|
||||||
|
ModelModel.find({group: 'Moisture'}).lean().exec((err, res) => {
|
||||||
|
if (err) return done(err);
|
||||||
|
should(res).have.lengthOf(0);
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
it('returns 404 for an unknown group', done => {
|
||||||
|
TestHelper.request(server, done, {
|
||||||
|
method: 'delete',
|
||||||
|
url: '/model/xxx/Model%201',
|
||||||
|
auth: {basic: 'admin'},
|
||||||
|
httpStatus: 404
|
||||||
|
});
|
||||||
|
});
|
||||||
|
it('returns 404 for an unknown model', done => {
|
||||||
|
TestHelper.request(server, done, {
|
||||||
|
method: 'delete',
|
||||||
|
url: '/model/VN/xxx',
|
||||||
|
auth: {basic: 'admin'},
|
||||||
|
httpStatus: 404
|
||||||
|
});
|
||||||
|
});
|
||||||
|
it('rejects an API key', done => {
|
||||||
|
TestHelper.request(server, done, {
|
||||||
|
method: 'delete',
|
||||||
|
url: '/model/VN/Model%20A',
|
||||||
|
auth: {key: 'admin'},
|
||||||
|
httpStatus: 401
|
||||||
|
});
|
||||||
|
});
|
||||||
|
it('rejects a write user', done => {
|
||||||
|
TestHelper.request(server, done, {
|
||||||
|
method: 'delete',
|
||||||
|
url: '/model/VN/Model%20A',
|
||||||
|
auth: {basic: 'janedoe'},
|
||||||
|
httpStatus: 403
|
||||||
|
});
|
||||||
|
});
|
||||||
|
it('rejects an unauthorized request', done => {
|
||||||
|
TestHelper.request(server, done, {
|
||||||
|
method: 'delete',
|
||||||
|
url: '/model/VN/Model%20A',
|
||||||
|
httpStatus: 401
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('GET /model/file/{name}', (() => {
|
||||||
it('returns the binary data', done => {
|
it('returns the binary data', done => {
|
||||||
TestHelper.request(server, done, {
|
TestHelper.request(server, done, {
|
||||||
method: 'get',
|
method: 'get',
|
||||||
url: '/model/modela',
|
url: '/model/file/modela',
|
||||||
auth: {basic: 'admin'},
|
auth: {basic: 'admin'},
|
||||||
httpStatus: 200,
|
httpStatus: 200,
|
||||||
contentType: 'application/octet-stream; charset=utf-8',
|
contentType: 'application/octet-stream; charset=utf-8',
|
||||||
@ -27,7 +287,7 @@ describe('/model', () => {
|
|||||||
it('returns the binary data for an API key', done => {
|
it('returns the binary data for an API key', done => {
|
||||||
TestHelper.request(server, done, {
|
TestHelper.request(server, done, {
|
||||||
method: 'get',
|
method: 'get',
|
||||||
url: '/model/modela',
|
url: '/model/file/modela',
|
||||||
auth: {key: 'admin'},
|
auth: {key: 'admin'},
|
||||||
httpStatus: 200,
|
httpStatus: 200,
|
||||||
contentType: 'application/octet-stream; charset=utf-8',
|
contentType: 'application/octet-stream; charset=utf-8',
|
||||||
@ -40,7 +300,7 @@ describe('/model', () => {
|
|||||||
it('returns 404 for an unknown name', done => {
|
it('returns 404 for an unknown name', done => {
|
||||||
TestHelper.request(server, done, {
|
TestHelper.request(server, done, {
|
||||||
method: 'get',
|
method: 'get',
|
||||||
url: '/model/modelx',
|
url: '/model/file/modelx',
|
||||||
auth: {basic: 'admin'},
|
auth: {basic: 'admin'},
|
||||||
httpStatus: 404
|
httpStatus: 404
|
||||||
})
|
})
|
||||||
@ -48,7 +308,7 @@ describe('/model', () => {
|
|||||||
it('rejects requests from a write user', done => {
|
it('rejects requests from a write user', done => {
|
||||||
TestHelper.request(server, done, {
|
TestHelper.request(server, done, {
|
||||||
method: 'get',
|
method: 'get',
|
||||||
url: '/model/modela',
|
url: '/model/file/modela',
|
||||||
auth: {basic: 'janedoe'},
|
auth: {basic: 'janedoe'},
|
||||||
httpStatus: 403
|
httpStatus: 403
|
||||||
})
|
})
|
||||||
@ -56,17 +316,17 @@ describe('/model', () => {
|
|||||||
it('rejects unauthorized requests', done => {
|
it('rejects unauthorized requests', done => {
|
||||||
TestHelper.request(server, done, {
|
TestHelper.request(server, done, {
|
||||||
method: 'get',
|
method: 'get',
|
||||||
url: '/model/modela',
|
url: '/model/file/modela',
|
||||||
httpStatus: 401
|
httpStatus: 401
|
||||||
})
|
})
|
||||||
});
|
});
|
||||||
}));
|
}));
|
||||||
|
|
||||||
describe('POST /model/{name}', () => {
|
describe('POST /model/file/{name}', () => {
|
||||||
it('stores the data', done => {
|
it('stores the data', done => {
|
||||||
TestHelper.request(server, done, {
|
TestHelper.request(server, done, {
|
||||||
method: 'post',
|
method: 'post',
|
||||||
url: '/model/modelb',
|
url: '/model/file/modelb',
|
||||||
auth: {basic: 'admin'},
|
auth: {basic: 'admin'},
|
||||||
httpStatus: 200,
|
httpStatus: 200,
|
||||||
reqContentType: 'application/octet-stream',
|
reqContentType: 'application/octet-stream',
|
||||||
@ -74,7 +334,7 @@ describe('/model', () => {
|
|||||||
}).end((err, res) => {
|
}).end((err, res) => {
|
||||||
if (err) return done (err);
|
if (err) return done (err);
|
||||||
should(res.body).be.eql({status: 'OK'});
|
should(res.body).be.eql({status: 'OK'});
|
||||||
ModelModel.find({name: 'modelb'}).lean().exec((err, data) => {
|
ModelFileModel.find({name: 'modelb'}).lean().exec((err, data) => {
|
||||||
if (err) return done (err);
|
if (err) return done (err);
|
||||||
should(data).have.lengthOf(1);
|
should(data).have.lengthOf(1);
|
||||||
should(data[0]).have.only.keys('_id', 'name', 'data', '__v');
|
should(data[0]).have.only.keys('_id', 'name', 'data', '__v');
|
||||||
@ -87,7 +347,7 @@ describe('/model', () => {
|
|||||||
it('stores the data with an API key', done => {
|
it('stores the data with an API key', done => {
|
||||||
TestHelper.request(server, done, {
|
TestHelper.request(server, done, {
|
||||||
method: 'post',
|
method: 'post',
|
||||||
url: '/model/modelb',
|
url: '/model/file/modelb',
|
||||||
auth: {key: 'admin'},
|
auth: {key: 'admin'},
|
||||||
httpStatus: 200,
|
httpStatus: 200,
|
||||||
reqContentType: 'application/octet-stream',
|
reqContentType: 'application/octet-stream',
|
||||||
@ -95,7 +355,7 @@ describe('/model', () => {
|
|||||||
}).end((err, res) => {
|
}).end((err, res) => {
|
||||||
if (err) return done (err);
|
if (err) return done (err);
|
||||||
should(res.body).be.eql({status: 'OK'});
|
should(res.body).be.eql({status: 'OK'});
|
||||||
ModelModel.find({name: 'modelb'}).lean().exec((err, data) => {
|
ModelFileModel.find({name: 'modelb'}).lean().exec((err, data) => {
|
||||||
if (err) return done (err);
|
if (err) return done (err);
|
||||||
should(data).have.lengthOf(1);
|
should(data).have.lengthOf(1);
|
||||||
should(data[0]).have.only.keys('_id', 'name', 'data', '__v');
|
should(data[0]).have.only.keys('_id', 'name', 'data', '__v');
|
||||||
@ -108,7 +368,7 @@ describe('/model', () => {
|
|||||||
it('overwrites existing data', done => {
|
it('overwrites existing data', done => {
|
||||||
TestHelper.request(server, done, {
|
TestHelper.request(server, done, {
|
||||||
method: 'post',
|
method: 'post',
|
||||||
url: '/model/modela',
|
url: '/model/file/modela',
|
||||||
auth: {basic: 'admin'},
|
auth: {basic: 'admin'},
|
||||||
httpStatus: 200,
|
httpStatus: 200,
|
||||||
reqContentType: 'application/octet-stream',
|
reqContentType: 'application/octet-stream',
|
||||||
@ -116,7 +376,7 @@ describe('/model', () => {
|
|||||||
}).end((err, res) => {
|
}).end((err, res) => {
|
||||||
if (err) return done (err);
|
if (err) return done (err);
|
||||||
should(res.body).be.eql({status: 'OK'});
|
should(res.body).be.eql({status: 'OK'});
|
||||||
ModelModel.find({name: 'modela'}).lean().exec((err, data) => {
|
ModelFileModel.find({name: 'modela'}).lean().exec((err, data) => {
|
||||||
if (err) return done (err);
|
if (err) return done (err);
|
||||||
should(data).have.lengthOf(1);
|
should(data).have.lengthOf(1);
|
||||||
should(data[0]).have.only.keys('_id', 'name', 'data', '__v');
|
should(data[0]).have.only.keys('_id', 'name', 'data', '__v');
|
||||||
@ -129,7 +389,7 @@ describe('/model', () => {
|
|||||||
it('rejects requests from a write user', done => {
|
it('rejects requests from a write user', done => {
|
||||||
TestHelper.request(server, done, {
|
TestHelper.request(server, done, {
|
||||||
method: 'post',
|
method: 'post',
|
||||||
url: '/model/modelb',
|
url: '/model/file/modelb',
|
||||||
auth: {basic: 'janedoe'},
|
auth: {basic: 'janedoe'},
|
||||||
httpStatus: 403,
|
httpStatus: 403,
|
||||||
req: 'another binary data'
|
req: 'another binary data'
|
||||||
@ -138,24 +398,24 @@ describe('/model', () => {
|
|||||||
it('rejects unauthorized requests', done => {
|
it('rejects unauthorized requests', done => {
|
||||||
TestHelper.request(server, done, {
|
TestHelper.request(server, done, {
|
||||||
method: 'post',
|
method: 'post',
|
||||||
url: '/model/modelb',
|
url: '/model/file/modelb',
|
||||||
httpStatus: 401,
|
httpStatus: 401,
|
||||||
req: 'another binary data'
|
req: 'another binary data'
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('DELETE /model/{name}', () => {
|
describe('DELETE /model/file/{name}', () => {
|
||||||
it('deletes the data', done => {
|
it('deletes the data', done => {
|
||||||
TestHelper.request(server, done, {
|
TestHelper.request(server, done, {
|
||||||
method: 'delete',
|
method: 'delete',
|
||||||
url: '/model/modela',
|
url: '/model/file/modela',
|
||||||
auth: {basic: 'admin'},
|
auth: {basic: 'admin'},
|
||||||
httpStatus: 200
|
httpStatus: 200
|
||||||
}).end((err, res) => {
|
}).end((err, res) => {
|
||||||
if (err) return done(err);
|
if (err) return done(err);
|
||||||
should(res.body).be.eql({status: 'OK'});
|
should(res.body).be.eql({status: 'OK'});
|
||||||
ModelModel.find({name: 'modela'}).lean().exec((err, data) => {
|
ModelFileModel.find({name: 'modela'}).lean().exec((err, data) => {
|
||||||
if (err) return done(err);
|
if (err) return done(err);
|
||||||
should(data).have.lengthOf(0);
|
should(data).have.lengthOf(0);
|
||||||
done();
|
done();
|
||||||
@ -165,7 +425,7 @@ describe('/model', () => {
|
|||||||
it('returns 404 for an unknown name', done => {
|
it('returns 404 for an unknown name', done => {
|
||||||
TestHelper.request(server, done, {
|
TestHelper.request(server, done, {
|
||||||
method: 'delete',
|
method: 'delete',
|
||||||
url: '/model/modelx',
|
url: '/model/file/modelx',
|
||||||
auth: {basic: 'admin'},
|
auth: {basic: 'admin'},
|
||||||
httpStatus: 404
|
httpStatus: 404
|
||||||
});
|
});
|
||||||
@ -173,7 +433,7 @@ describe('/model', () => {
|
|||||||
it('rejects an API key', done => {
|
it('rejects an API key', done => {
|
||||||
TestHelper.request(server, done, {
|
TestHelper.request(server, done, {
|
||||||
method: 'delete',
|
method: 'delete',
|
||||||
url: '/model/modela',
|
url: '/model/file/modela',
|
||||||
auth: {key: 'admin'},
|
auth: {key: 'admin'},
|
||||||
httpStatus: 401
|
httpStatus: 401
|
||||||
});
|
});
|
||||||
@ -181,7 +441,7 @@ describe('/model', () => {
|
|||||||
it('rejects a write user', done => {
|
it('rejects a write user', done => {
|
||||||
TestHelper.request(server, done, {
|
TestHelper.request(server, done, {
|
||||||
method: 'delete',
|
method: 'delete',
|
||||||
url: '/model/modela',
|
url: '/model/file/modela',
|
||||||
auth: {basic: 'janedoe'},
|
auth: {basic: 'janedoe'},
|
||||||
httpStatus: 403
|
httpStatus: 403
|
||||||
});
|
});
|
||||||
@ -189,7 +449,7 @@ describe('/model', () => {
|
|||||||
it('rejects an unauthorized request', done => {
|
it('rejects an unauthorized request', done => {
|
||||||
TestHelper.request(server, done, {
|
TestHelper.request(server, done, {
|
||||||
method: 'delete',
|
method: 'delete',
|
||||||
url: '/model/modela',
|
url: '/model/file/modela',
|
||||||
httpStatus: 401
|
httpStatus: 401
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -1,14 +1,97 @@
|
|||||||
import express from 'express';
|
import express from 'express';
|
||||||
import bodyParser from 'body-parser';
|
import bodyParser from 'body-parser';
|
||||||
|
|
||||||
|
import ModelFileModel from '../models/model_file';
|
||||||
import ModelModel from '../models/model';
|
import ModelModel from '../models/model';
|
||||||
|
import _ from 'lodash';
|
||||||
|
import ModelValidate from './validate/model';
|
||||||
|
import res400 from './validate/res400';
|
||||||
|
import db from '../db';
|
||||||
|
|
||||||
const router = express.Router();
|
const router = express.Router();
|
||||||
|
|
||||||
router.get('/model/:name', (req, res, next) => {
|
router.get('/model/groups', (req, res, next) => {
|
||||||
|
if (!req.auth(res, ['read', 'write', 'dev', 'admin'], 'basic')) return;
|
||||||
|
|
||||||
|
ModelModel.find().lean().exec((err, data) => {
|
||||||
|
if (err) return next(err);
|
||||||
|
|
||||||
|
// validate all and filter null values from validation errors
|
||||||
|
res.json(_.compact(data.map(e => ModelValidate.output(e))));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
router.post('/model/:group', (req, res, next) => {
|
||||||
|
if (!req.auth(res, ['dev', 'admin'], 'basic')) return;
|
||||||
|
|
||||||
|
const {error, value: model} = ModelValidate.input(req.body);
|
||||||
|
console.log(error);
|
||||||
|
if (error) return res400(error, res);
|
||||||
|
|
||||||
|
ModelModel.findOne({group: req.params.group}).lean().exec((err, data) => {
|
||||||
|
if (err) return next(err);
|
||||||
|
|
||||||
|
if (data) { // group exists
|
||||||
|
if (data.models.find(e => e.name === model.name)) { // name exists, overwrite
|
||||||
|
ModelModel.findOneAndUpdate(
|
||||||
|
{$and: [{group: req.params.group}, {'models.name': model.name}]},
|
||||||
|
{'models.$': model},
|
||||||
|
{upsert: true}).log(req).lean().exec(err => {
|
||||||
|
if (err) return next(err);
|
||||||
|
res.json({status: 'OK'})
|
||||||
|
});
|
||||||
|
}
|
||||||
|
else { // create new
|
||||||
|
ModelModel.findOneAndUpdate(
|
||||||
|
{group: req.params.group},
|
||||||
|
{$push: {models: model as never}}
|
||||||
|
).log(req).lean().exec(err => {
|
||||||
|
if (err) return next(err);
|
||||||
|
res.json({status: 'OK'})
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else { // create new group
|
||||||
|
new ModelModel({group: req.params.group, models: [model]}).save((err, data) => {
|
||||||
|
if (err) return next(err);
|
||||||
|
db.log(req, 'models', {_id: data._id}, data.toObject());
|
||||||
|
res.json({status: 'OK'})
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
router.delete('/model/:group(((?!file)[^\\/]+?))/:name', (req, res, next) => {
|
||||||
|
if (!req.auth(res, ['dev', 'admin'], 'basic')) return;
|
||||||
|
|
||||||
|
ModelModel.findOne({group: req.params.group}).lean().exec((err, data) => {
|
||||||
|
if (err) return next(err);
|
||||||
|
|
||||||
|
if (!data || !data.models.find(e => e.name === req.params.name)) {
|
||||||
|
return res.status(404).json({status: 'Not found'});
|
||||||
|
}
|
||||||
|
if (data.models.length > 1) { // only remove model
|
||||||
|
ModelModel.findOneAndUpdate(
|
||||||
|
{group: req.params.group},
|
||||||
|
{$pull: {models: data.models.find(e => e.name === req.params.name) as never}}
|
||||||
|
).log(req).lean().exec(err => {
|
||||||
|
if (err) return next(err);
|
||||||
|
res.json({status: 'OK'})
|
||||||
|
});
|
||||||
|
}
|
||||||
|
else { // remove document
|
||||||
|
ModelModel.findOneAndDelete({group: req.params.group}).log(req).lean().exec(err => {
|
||||||
|
if (err) return next(err);
|
||||||
|
res.json({status: 'OK'})
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
router.get('/model/file/:name', (req, res, next) => {
|
||||||
if (!req.auth(res, ['dev', 'admin'], 'all')) return;
|
if (!req.auth(res, ['dev', 'admin'], 'all')) return;
|
||||||
|
|
||||||
ModelModel.findOne({name: req.params.name}).lean().exec((err, data) => {
|
ModelFileModel.findOne({name: req.params.name}).lean().exec((err, data) => {
|
||||||
if (err) return next(err);
|
if (err) return next(err);
|
||||||
if (data) {
|
if (data) {
|
||||||
res.set('Content-Type', 'application/octet-stream');
|
res.set('Content-Type', 'application/octet-stream');
|
||||||
@ -20,20 +103,20 @@ router.get('/model/:name', (req, res, next) => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
router.post('/model/:name', bodyParser.raw({limit: '500kb'}), (req, res, next) => {
|
router.post('/model/file/:name', bodyParser.raw({limit: '5mb'}), (req, res, next) => {
|
||||||
if (!req.auth(res, ['dev', 'admin'], 'all')) return;
|
if (!req.auth(res, ['dev', 'admin'], 'all')) return;
|
||||||
|
|
||||||
ModelModel.replaceOne({name: req.params.name}, {name: req.params.name, data: req.body}).setOptions({upsert: true})
|
ModelFileModel.replaceOne({name: req.params.name}, {name: req.params.name, data: req.body}).setOptions({upsert: true})
|
||||||
.lean().exec(err => {
|
.lean().exec(err => {
|
||||||
if (err) return next(err);
|
if (err) return next(err);
|
||||||
res.json({status: 'OK'});
|
res.json({status: 'OK'});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
router.delete('/model/:name', (req, res, next) => {
|
router.delete('/model/file/:name', (req, res, next) => {
|
||||||
if (!req.auth(res, ['dev', 'admin'], 'basic')) return;
|
if (!req.auth(res, ['dev', 'admin'], 'basic')) return;
|
||||||
|
|
||||||
ModelModel.findOneAndDelete({name: req.params.name}).lean().exec((err, data) => {
|
ModelFileModel.findOneAndDelete({name: req.params.name}).lean().exec((err, data) => {
|
||||||
if (err) return next(err);
|
if (err) return next(err);
|
||||||
if (data) {
|
if (data) {
|
||||||
res.json({status: 'OK'});
|
res.json({status: 'OK'});
|
||||||
|
@ -22,6 +22,8 @@ import globals from '../globals';
|
|||||||
|
|
||||||
const router = express.Router();
|
const router = express.Router();
|
||||||
|
|
||||||
|
// TODO: do not use streaming for spectrum filenames
|
||||||
|
|
||||||
|
|
||||||
router.get('/samples', async (req, res, next) => {
|
router.get('/samples', async (req, res, next) => {
|
||||||
if (!req.auth(res, ['read', 'write', 'dev', 'admin'], 'all')) return;
|
if (!req.auth(res, ['read', 'write', 'dev', 'admin'], 'all')) return;
|
||||||
@ -364,7 +366,7 @@ router.get('/samples', async (req, res, next) => {
|
|||||||
}
|
}
|
||||||
queryPtr.push({$project: projection});
|
queryPtr.push({$project: projection});
|
||||||
// use streaming when including spectrum files
|
// use streaming when including spectrum files
|
||||||
if (!fieldsToAdd.find(e => e.indexOf(globals.spectrum.spectrum + '.') >= 0)) {
|
if (!fieldsToAdd.find(e => e.indexOf(globals.spectrum.spectrum + '.' + globals.spectrum.dpt) >= 0)) {
|
||||||
collection.aggregate(query).allowDiskUse(true).exec((err, data) => {
|
collection.aggregate(query).allowDiskUse(true).exec((err, data) => {
|
||||||
if (err) return next(err);
|
if (err) return next(err);
|
||||||
if (data[0] && data[0].count) {
|
if (data[0] && data[0].count) {
|
||||||
|
38
src/routes/validate/model.ts
Normal file
38
src/routes/validate/model.ts
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
import Joi from 'joi';
|
||||||
|
|
||||||
|
|
||||||
|
export default class ModelValidate { // validate input for model
|
||||||
|
private static model = {
|
||||||
|
group: Joi.string()
|
||||||
|
.disallow('file')
|
||||||
|
.max(128),
|
||||||
|
|
||||||
|
model: Joi.object({
|
||||||
|
name: Joi.string()
|
||||||
|
.max(128)
|
||||||
|
.required(),
|
||||||
|
|
||||||
|
url: Joi.string()
|
||||||
|
.uri()
|
||||||
|
.max(512)
|
||||||
|
.required(),
|
||||||
|
|
||||||
|
label: Joi.string()
|
||||||
|
.allow('')
|
||||||
|
.max(128)
|
||||||
|
.required()
|
||||||
|
})
|
||||||
|
};
|
||||||
|
|
||||||
|
static input (data) { // validate input
|
||||||
|
return this.model.model.required().validate(data);
|
||||||
|
}
|
||||||
|
|
||||||
|
static output (data) { // validate output and strip unwanted properties, returns null if not valid
|
||||||
|
const {value, error} = Joi.object({
|
||||||
|
group: this.model.group,
|
||||||
|
models: Joi.array().items(this.model.model)
|
||||||
|
}).validate(data, {stripUnknown: true});
|
||||||
|
return error !== undefined? null : value;
|
||||||
|
}
|
||||||
|
}
|
@ -678,13 +678,40 @@
|
|||||||
"__v": 0
|
"__v": 0
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"models": [
|
"model_files": [
|
||||||
{
|
{
|
||||||
"_id": {"$oid":"140000000000000000000001"},
|
"_id": {"$oid":"140000000000000000000001"},
|
||||||
"name": "modela",
|
"name": "modela",
|
||||||
"data": {"buffer": "binary data"}
|
"data": {"buffer": "binary data"}
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
"models": [
|
||||||
|
{
|
||||||
|
"group": "VN",
|
||||||
|
"models": [
|
||||||
|
{
|
||||||
|
"name": "Model A",
|
||||||
|
"url": "http://model-a.com",
|
||||||
|
"label": "ml/g"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Model B",
|
||||||
|
"url": "http://model-b.com",
|
||||||
|
"label": "ml/g"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"group": "Moisture",
|
||||||
|
"models": [
|
||||||
|
{
|
||||||
|
"name": "Model 1",
|
||||||
|
"url": "http://model-1.com",
|
||||||
|
"label": "weight %"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
"users": [
|
"users": [
|
||||||
{
|
{
|
||||||
"_id": {"$oid":"000000000000000000000001"},
|
"_id": {"$oid":"000000000000000000000001"},
|
||||||
|
Reference in New Issue
Block a user