From 70070dd465414dad8513ee5f7927f53be6a42cbf Mon Sep 17 00:00:00 2001 From: Richard Martens Date: Wed, 12 Oct 2022 21:34:23 +0200 Subject: [PATCH 01/64] upgrade bug fixed --- src/db/db.js | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/src/db/db.js b/src/db/db.js index 7978f57..1d9db00 100644 --- a/src/db/db.js +++ b/src/db/db.js @@ -9,12 +9,7 @@ export default class { this._models = {}; } - createConnection(connection, optionsIn, onError) { - const options = { - ...optionsIn, - server: { reconnectTries: Number.MAX_VALUE }, - }; - + createConnection(connection, options, onError) { this._connection = mongoose.createConnection(connection, options, onError); return this._connection; From 2f2701319ae5da53a8e8a3bd81add48750a2d3d5 Mon Sep 17 00:00:00 2001 From: Richard Martens Date: Wed, 12 Oct 2022 22:41:11 +0200 Subject: [PATCH 02/64] parsing of accept header optimized. closes #100 --- src/pipes.js | 23 ++++++++++++++++++++--- src/server.js | 21 +++++++++++++++------ test/metadata.format.js | 18 +++++++++++++++++- 3 files changed, 52 insertions(+), 10 deletions(-) diff --git a/src/pipes.js b/src/pipes.js index dbbf1dd..583a2c4 100644 --- a/src/pipes.js +++ b/src/pipes.js @@ -10,10 +10,27 @@ function writeJson(res, data, status, resolve) { } function getMediaType(accept) { - if (accept.match(/(application\/)?json/)) { - return 'application/json'; - } if (accept.match(/(application\/)?xml/)) { + // reduce multi mimetypes to most weigth mimetype + // e.g. Accept: text/html, application/xhtml+xml, application/xml;q=0.9, image/webp, */*;q=0.8 + const mimeStructs = accept.split(/[ ,]+/g); + const mostWeightMimetype = mimeStructs.reduce((previous, current) => { + const [mimetype, qualityParam] = current.split(/[ ;]+/); + const [, qualityValue] = qualityParam ? qualityParam.split(/=/) : ['q', 1]; + const result = { + ...previous, + }; + + if (!previous.mimetype || previous.qualityValue < qualityValue) { + result.mimetype = mimetype; + result.qualityValue = qualityValue; + } + return result; + }, {}); + + if (mostWeightMimetype.mimetype.match(/((application|\*)\/(xml|\*)|^xml$)/)) { return 'application/xml'; + } if (mostWeightMimetype.mimetype.match(/((application|\*)\/(json|\*)|^json$)/)) { + return 'application/json'; } const error406 = new Error('Not acceptable'); diff --git a/src/server.js b/src/server.js index 2f499e6..0a12845 100644 --- a/src/server.js +++ b/src/server.js @@ -99,26 +99,35 @@ class Server { }); } - listen(...args) { - const router = this._metadata._router(); + _getRouter() { + const result = []; - this._app.use(this.get('prefix'), router); + result.push(this._metadata._router()); Object.keys(this.resources).forEach((resourceKey) => { const resource = this.resources[resourceKey]; - const resourceRouter = resource._router(this.getSettings()); - this.use(this.get('prefix'), resourceRouter); + result.push(resource._router(this.getSettings())); if (resource.actions) { Object.keys(resource.actions).forEach((actionKey) => { const action = resource.actions[actionKey]; - this.use(action.router); + result.push(action.router); }); } }); + return result; + } + + listen(...args) { + const router = this._getRouter(); + + router.forEach((item) => { + this._app.use(this.get('prefix'), item); + }); + return this._app.listen(...args); } diff --git a/test/metadata.format.js b/test/metadata.format.js index f6010e8..edd7eac 100644 --- a/test/metadata.format.js +++ b/test/metadata.format.js @@ -103,13 +103,29 @@ describe('metadata.format', () => { res.body.should.deepEqual(jsonDocument); }); - it('should return xml if $format overrides accept header', async function() { + it('should return json if $format overrides accept header', async function() { httpServer = server.listen(port); const res = await request(host).get('/$metadata?$format=json').set('accept', 'application/xml'); res.statusCode.should.equal(200); checkContentType(res, 'application/json'); res.body.should.deepEqual(jsonDocument); }); + + it('should return xml if xml has highest quality value', async function() { + httpServer = server.listen(port); + const res = await request(host).get('/$metadata').set('accept', 'application/json;q=0.9, application/xml'); + res.statusCode.should.equal(200); + checkContentType(res, 'application/xml'); + res.text.should.equal(xmlDocument); + }); + + it('should return xml if xml and json matched with asterix', async function() { + httpServer = server.listen(port); + const res = await request(host).get('/$metadata').set('accept', '*/*'); + res.statusCode.should.equal(200); + checkContentType(res, 'application/xml'); + res.text.should.equal(xmlDocument); + }); }); From cf29fa092b38305eb9e9f3b71692ddb7eafa8714 Mon Sep 17 00:00:00 2001 From: Richard Martens Date: Thu, 13 Oct 2022 21:55:52 +0200 Subject: [PATCH 03/64] Tests for service document ready --- src/metadata/ODataServiceDocument.js | 228 +++++++++++++++++++++++++++ test/service.document.js | 48 ++++++ 2 files changed, 276 insertions(+) create mode 100644 src/metadata/ODataServiceDocument.js create mode 100644 test/service.document.js diff --git a/src/metadata/ODataServiceDocument.js b/src/metadata/ODataServiceDocument.js new file mode 100644 index 0000000..2760ea7 --- /dev/null +++ b/src/metadata/ODataServiceDocument.js @@ -0,0 +1,228 @@ +import { Router } from 'express'; +import pipes from '../pipes'; +import Resource from '../ODataResource'; + +export default class Metadata { + constructor(server) { + this._server = server; + this._hooks = { + }; + this._count = 0; + } + + get() { + return this; + } + + before(fn) { + this._hooks.before = fn; + return this; + } + + after(fn) { + this._hooks.after = fn; + return this; + } + + auth(fn) { + this._hooks.auth = fn; + return this; + } + + _router() { + /*eslint-disable */ + const router = Router(); + /* eslint-enable */ + router.get('/\\$metadata', (req, res) => { + pipes.authorizePipe(req, res, this._hooks.auth) + .then(() => pipes.beforePipe(req, res, this._hooks.before)) + .then(() => this.ctrl(req)) + .then((result) => pipes.respondPipe(req, res, result || {})) + .then((data) => pipes.afterPipe(req, res, this._hooks.after, data)) + .catch((err) => pipes.errorPipe(req, res, err)); + }); + + return router; + } + + visitProperty(node, root) { + const result = {}; + + switch (node.instance) { + case 'ObjectId': + result.$Type = 'self.ObjectId'; + break; + + case 'Number': + result.$Type = 'Edm.Double'; + break; + + case 'Date': + result.$Type = 'Edm.DateTimeOffset'; + break; + + case 'String': + result.$Type = 'Edm.String'; + break; + + case 'Array': // node.path = p1; node.schema.paths + result.$Collection = true; + if (node.schema && node.schema.paths) { + this._count += 1; + const notClassifiedName = `${node.path}Child${this._count}`; + // Array of complex type + result.$Type = `self.${notClassifiedName}`; + root(notClassifiedName, this.visitor('ComplexType', node.schema.paths, root)); + } else { + const arrayItemType = this.visitor('Property', { instance: node.options.type[0].name }, root); + + result.$Type = arrayItemType.$Type; + } + break; + + default: + return null; + } + + return result; + } + + visitEntityType(node, root) { + const properties = Object.keys(node) + .filter((path) => path !== '_id') + .reduce((previousProperty, curentProperty) => { + const result = { + ...previousProperty, + [curentProperty]: this.visitor('Property', node[curentProperty], root), + }; + + return result; + }, {}); + + return { + $Kind: 'EntityType', + $Key: ['id'], + id: { + $Type: 'self.ObjectId', + $Nullable: false, + }, + ...properties, + }; + } + + visitComplexType(node, root) { + const properties = Object.keys(node) + .filter((item) => item !== '_id') + .reduce((previousProperty, curentProperty) => { + const result = { + ...previousProperty, + [curentProperty]: this.visitor('Property', node[curentProperty], root), + }; + + return result; + }, {}); + + return { + $Kind: 'ComplexType', + ...properties, + }; + } + + static visitAction(node) { + return { + $Kind: 'Action', + $IsBound: true, + $Parameter: [{ + $Name: node.resource, + $Type: `self.${node.resource}`, + $Collection: node.binding === 'collection' ? true : undefined, + }], + }; + } + + static visitFunction(node) { + return { + $Kind: 'Function', + ...node.params, + }; + } + + visitor(type, node, root) { + switch (type) { + case 'Property': + return this.visitProperty(node, root); + + case 'ComplexType': + return this.visitComplexType(node, root); + + case 'Action': + return Metadata.visitAction(node); + + case 'Function': + return Metadata.visitFunction(node, root); + + default: + return this.visitEntityType(node, root); + } + } + + ctrl() { + const entityTypeNames = Object.keys(this._server.resources); + const entityTypes = entityTypeNames.reduce((previousResource, currentResource) => { + const resource = this._server.resources[currentResource]; + const result = { ...previousResource }; + const attachToRoot = (name, value) => { result[name] = value; }; + + if (resource instanceof Resource) { + const { paths } = resource.model.model.schema; + + result[currentResource] = this.visitor('EntityType', paths, attachToRoot); + const actions = Object.keys(resource.actions); + if (actions && actions.length) { + actions.forEach((action) => { + result[action] = this.visitor('Action', resource.actions[action], attachToRoot); + }); + } + } else { + result[currentResource] = this.visitor('Function', resource, attachToRoot); + } + + return result; + }, {}); + + const entitySetNames = Object.keys(this._server.resources); + const entitySets = entitySetNames.reduce((previousResource, currentResource) => { + const result = { ...previousResource }; + result[currentResource] = this._server.resources[currentResource] instanceof Resource ? { + $Collection: true, + $Type: `self.${currentResource}`, + } : { + $Function: `self.${currentResource}`, + }; + + return result; + }, {}); + + const document = { + $Version: '4.0', + ObjectId: { + $Kind: 'TypeDefinition', + $UnderlyingType: 'Edm.String', + $MaxLength: 24, + }, + ...entityTypes, + $EntityContainer: 'org.example.DemoService', + ['org.example.DemoService']: { // eslint-disable-line no-useless-computed-key + $Kind: 'EntityContainer', + ...entitySets, + }, + }; + + return new Promise((resolve) => { + resolve({ + status: 200, + metadata: document, + }); + }); + } +} diff --git a/test/service.document.js b/test/service.document.js new file mode 100644 index 0000000..a7b6dd2 --- /dev/null +++ b/test/service.document.js @@ -0,0 +1,48 @@ +import 'should'; +import request from 'supertest'; +import { host, port, bookSchema, odata, assertSuccess } from './support/setup'; +import FakeDb from './support/fake-db'; + +describe('metadata.format', () => { + let httpServer, server, db; + + const jsonDocument = { + '@context': 'http://localhost:8080/', + value: [{ + kind: 'EntitySet', + name: 'books', + url: 'books' + }] + }; + beforeEach(async function() { + db = new FakeDb(); + server = odata(db); + server.resource('book', bookSchema); + + }); + + afterEach(() => { + httpServer.close(); + }); + + it('should return json if no format given', async function() { + httpServer = server.listen(port); + const res = await request(host).get('/'); + assertSuccess(res); + checkContentType(res, 'application/json'); + res.body.should.deepEqual(jsonDocument); + }); + + it('should return 406 if other than json format requested', async function() { + httpServer = server.listen(port); + const res = await request(host).get('/').set('accept', 'application/xml'); + res.status.should.be.equal(406); + }); + +}); + + +function checkContentType(res, value) { + res.header.should.have.property('content-type'); + res.header['content-type'].should.containEql(value); +} \ No newline at end of file From beeff9e36c1c1b290ce9c288c11bddb765cbe8d8 Mon Sep 17 00:00:00 2001 From: Richard Martens Date: Thu, 13 Oct 2022 22:04:33 +0200 Subject: [PATCH 04/64] Tests for default mimetypes. closes #99 --- test/metadata.format.js | 8 -------- test/mimetype.defaults.js | 39 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 39 insertions(+), 8 deletions(-) create mode 100644 test/mimetype.defaults.js diff --git a/test/metadata.format.js b/test/metadata.format.js index edd7eac..71b50e1 100644 --- a/test/metadata.format.js +++ b/test/metadata.format.js @@ -87,14 +87,6 @@ describe('metadata.format', () => { httpServer.close(); }); - it('should return xml if no format given', async function() { - httpServer = server.listen(port); - const res = await request(host).get('/$metadata'); - assertSuccess(res); - checkContentType(res, 'application/xml'); - res.text.should.equal(xmlDocument); - }); - it('should return json according accept header', async function() { httpServer = server.listen(port); const res = await request(host).get('/$metadata').set('accept', 'application/json'); diff --git a/test/mimetype.defaults.js b/test/mimetype.defaults.js new file mode 100644 index 0000000..e979f87 --- /dev/null +++ b/test/mimetype.defaults.js @@ -0,0 +1,39 @@ +import 'should'; +import request from 'supertest'; +import { host, conn, port, bookSchema, odata, assertSuccess } from './support/setup'; +import FakeDb from './support/fake-db'; + +describe('metadata.format', () => { + let httpServer, server, db; + + beforeEach(async function() { + db = new FakeDb(); + server = odata(db); + server.resource('book', bookSchema); + + }); + + afterEach(() => { + httpServer.close(); + }); + + it('should return xml if no format given for metadata request', async function() { + httpServer = server.listen(port); + const res = await request(host).get('/$metadata'); + assertSuccess(res); + checkContentType(res, 'application/xml'); + }); + + it('should return json if no format given for data request', async function() { + httpServer = server.listen(port); + const res = await request(host).get('/book'); + assertSuccess(res); + checkContentType(res, 'application/json'); + }); +}); + + +function checkContentType(res, value) { + res.header.should.have.property('content-type'); + res.header['content-type'].should.containEql(value); +} \ No newline at end of file From b099741d9ac333a93233741cfe58ecb6c595d4dc Mon Sep 17 00:00:00 2001 From: Richard Martens Date: Thu, 13 Oct 2022 22:11:32 +0200 Subject: [PATCH 05/64] name of new tests fixed --- test/mimetype.defaults.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/mimetype.defaults.js b/test/mimetype.defaults.js index e979f87..184a6c1 100644 --- a/test/mimetype.defaults.js +++ b/test/mimetype.defaults.js @@ -1,9 +1,9 @@ import 'should'; import request from 'supertest'; -import { host, conn, port, bookSchema, odata, assertSuccess } from './support/setup'; +import { host, port, bookSchema, odata, assertSuccess } from './support/setup'; import FakeDb from './support/fake-db'; -describe('metadata.format', () => { +describe('mimetype.defaults', () => { let httpServer, server, db; beforeEach(async function() { From 418aee3a558b7266cf87237a14c2660646031bae Mon Sep 17 00:00:00 2001 From: Richard Martens Date: Thu, 13 Oct 2022 23:25:51 +0200 Subject: [PATCH 06/64] service document request implemented. closes #101 --- src/metadata/ODataMetadata.js | 21 ++-- src/metadata/ODataServiceDocument.js | 182 ++------------------------- src/pipes.js | 8 +- src/server.js | 9 +- test/service.document.js | 16 ++- 5 files changed, 48 insertions(+), 188 deletions(-) diff --git a/src/metadata/ODataMetadata.js b/src/metadata/ODataMetadata.js index 2760ea7..f9b95c1 100644 --- a/src/metadata/ODataMetadata.js +++ b/src/metadata/ODataMetadata.js @@ -1,6 +1,7 @@ import { Router } from 'express'; import pipes from '../pipes'; import Resource from '../ODataResource'; +import Function from '../ODataFunction'; export default class Metadata { constructor(server) { @@ -183,7 +184,7 @@ export default class Metadata { result[action] = this.visitor('Action', resource.actions[action], attachToRoot); }); } - } else { + } else if (resource instanceof Function) { result[currentResource] = this.visitor('Function', resource, attachToRoot); } @@ -193,12 +194,18 @@ export default class Metadata { const entitySetNames = Object.keys(this._server.resources); const entitySets = entitySetNames.reduce((previousResource, currentResource) => { const result = { ...previousResource }; - result[currentResource] = this._server.resources[currentResource] instanceof Resource ? { - $Collection: true, - $Type: `self.${currentResource}`, - } : { - $Function: `self.${currentResource}`, - }; + const resource = this._server.resources[currentResource]; + + if (resource instanceof Resource) { + result[currentResource] = { + $Collection: true, + $Type: `self.${currentResource}`, + }; + } else if (resource instanceof Function) { + result[currentResource] = { + $Function: `self.${currentResource}`, + }; + } return result; }, {}); diff --git a/src/metadata/ODataServiceDocument.js b/src/metadata/ODataServiceDocument.js index 2760ea7..fda01ed 100644 --- a/src/metadata/ODataServiceDocument.js +++ b/src/metadata/ODataServiceDocument.js @@ -33,7 +33,7 @@ export default class Metadata { /*eslint-disable */ const router = Router(); /* eslint-enable */ - router.get('/\\$metadata', (req, res) => { + router.get('/', (req, res) => { pipes.authorizePipe(req, res, this._hooks.auth) .then(() => pipes.beforePipe(req, res, this._hooks.before)) .then(() => this.ctrl(req)) @@ -45,183 +45,25 @@ export default class Metadata { return router; } - visitProperty(node, root) { - const result = {}; - - switch (node.instance) { - case 'ObjectId': - result.$Type = 'self.ObjectId'; - break; - - case 'Number': - result.$Type = 'Edm.Double'; - break; - - case 'Date': - result.$Type = 'Edm.DateTimeOffset'; - break; - - case 'String': - result.$Type = 'Edm.String'; - break; - - case 'Array': // node.path = p1; node.schema.paths - result.$Collection = true; - if (node.schema && node.schema.paths) { - this._count += 1; - const notClassifiedName = `${node.path}Child${this._count}`; - // Array of complex type - result.$Type = `self.${notClassifiedName}`; - root(notClassifiedName, this.visitor('ComplexType', node.schema.paths, root)); - } else { - const arrayItemType = this.visitor('Property', { instance: node.options.type[0].name }, root); - - result.$Type = arrayItemType.$Type; - } - break; - - default: - return null; - } - - return result; - } - - visitEntityType(node, root) { - const properties = Object.keys(node) - .filter((path) => path !== '_id') - .reduce((previousProperty, curentProperty) => { - const result = { - ...previousProperty, - [curentProperty]: this.visitor('Property', node[curentProperty], root), - }; - - return result; - }, {}); - - return { - $Kind: 'EntityType', - $Key: ['id'], - id: { - $Type: 'self.ObjectId', - $Nullable: false, - }, - ...properties, - }; - } - - visitComplexType(node, root) { - const properties = Object.keys(node) - .filter((item) => item !== '_id') - .reduce((previousProperty, curentProperty) => { - const result = { - ...previousProperty, - [curentProperty]: this.visitor('Property', node[curentProperty], root), - }; - - return result; - }, {}); - - return { - $Kind: 'ComplexType', - ...properties, - }; - } - - static visitAction(node) { - return { - $Kind: 'Action', - $IsBound: true, - $Parameter: [{ - $Name: node.resource, - $Type: `self.${node.resource}`, - $Collection: node.binding === 'collection' ? true : undefined, - }], - }; - } - - static visitFunction(node) { - return { - $Kind: 'Function', - ...node.params, - }; - } - - visitor(type, node, root) { - switch (type) { - case 'Property': - return this.visitProperty(node, root); - - case 'ComplexType': - return this.visitComplexType(node, root); - - case 'Action': - return Metadata.visitAction(node); - - case 'Function': - return Metadata.visitFunction(node, root); - - default: - return this.visitEntityType(node, root); - } - } - - ctrl() { + ctrl(req) { const entityTypeNames = Object.keys(this._server.resources); - const entityTypes = entityTypeNames.reduce((previousResource, currentResource) => { - const resource = this._server.resources[currentResource]; - const result = { ...previousResource }; - const attachToRoot = (name, value) => { result[name] = value; }; - - if (resource instanceof Resource) { - const { paths } = resource.model.model.schema; - - result[currentResource] = this.visitor('EntityType', paths, attachToRoot); - const actions = Object.keys(resource.actions); - if (actions && actions.length) { - actions.forEach((action) => { - result[action] = this.visitor('Action', resource.actions[action], attachToRoot); - }); - } - } else { - result[currentResource] = this.visitor('Function', resource, attachToRoot); - } - - return result; - }, {}); - - const entitySetNames = Object.keys(this._server.resources); - const entitySets = entitySetNames.reduce((previousResource, currentResource) => { - const result = { ...previousResource }; - result[currentResource] = this._server.resources[currentResource] instanceof Resource ? { - $Collection: true, - $Type: `self.${currentResource}`, - } : { - $Function: `self.${currentResource}`, - }; - - return result; - }, {}); + const entitySets = entityTypeNames + .filter((item) => this._server.resources[item] instanceof Resource) + .map((currentResource) => ({ + name: currentResource, + kind: 'EntitySet', + url: currentResource, + })); const document = { - $Version: '4.0', - ObjectId: { - $Kind: 'TypeDefinition', - $UnderlyingType: 'Edm.String', - $MaxLength: 24, - }, - ...entityTypes, - $EntityContainer: 'org.example.DemoService', - ['org.example.DemoService']: { // eslint-disable-line no-useless-computed-key - $Kind: 'EntityContainer', - ...entitySets, - }, + '@context': `${req.protocol}://${req.get('host')}${this._server.get('prefix')}/$metadata`, + value: entitySets, }; return new Promise((resolve) => { resolve({ status: 200, - metadata: document, + entity: document, }); }); } diff --git a/src/pipes.js b/src/pipes.js index 583a2c4..e8acd1c 100644 --- a/src/pipes.js +++ b/src/pipes.js @@ -9,7 +9,7 @@ function writeJson(res, data, status, resolve) { resolve(data); } -function getMediaType(accept) { +function getMediaType(accept, data) { // reduce multi mimetypes to most weigth mimetype // e.g. Accept: text/html, application/xhtml+xml, application/xml;q=0.9, image/webp, */*;q=0.8 const mimeStructs = accept.split(/[ ,]+/g); @@ -27,7 +27,7 @@ function getMediaType(accept) { return result; }, {}); - if (mostWeightMimetype.mimetype.match(/((application|\*)\/(xml|\*)|^xml$)/)) { + if (!data.entity && mostWeightMimetype.mimetype.match(/((application|\*)\/(xml|\*)|^xml$)/)) { return 'application/xml'; } if (mostWeightMimetype.mimetype.match(/((application|\*)\/(json|\*)|^json$)/)) { return 'application/json'; @@ -44,10 +44,10 @@ function getWriter(req, result) { if (req.query.$format) { // get requested media type from $format query - mediaType = getMediaType(req.query.$format); + mediaType = getMediaType(req.query.$format, result); } else if (req.headers.accept) { // get requested media type from accept header - mediaType = getMediaType(req.headers.accept); + mediaType = getMediaType(req.headers.accept, result); } // xml representation of metadata diff --git a/src/server.js b/src/server.js index 0a12845..ac795a0 100644 --- a/src/server.js +++ b/src/server.js @@ -2,6 +2,7 @@ import createExpress from './express'; import Resource from './ODataResource'; import Func from './ODataFunction'; import Metadata from './metadata/ODataMetadata'; +import ServiceDocument from './metadata/ODataServiceDocument'; import Db from './db/db'; function checkAuth(auth, req) { @@ -22,8 +23,10 @@ class Server { // Should mix _resources object and resources object: _resources + resource = resources. // Encapsulation to a object, separate mognoose, try to use *repository pattern*. // 这里也许应该让 resources 支持 odata 查询的, 以方便直接在代码中使用 OData 查询方式来进行数据筛选, 达到隔离 mongo 的效果. - this.resources = {}; - this._metadata = new Metadata(this); + this.resources = { + $metadata: new Metadata(this), + }; + this._serviceDocument = new ServiceDocument(this); } function(url, middleware, params) { @@ -102,7 +105,7 @@ class Server { _getRouter() { const result = []; - result.push(this._metadata._router()); + result.push(this._serviceDocument._router()); Object.keys(this.resources).forEach((resourceKey) => { const resource = this.resources[resourceKey]; diff --git a/test/service.document.js b/test/service.document.js index a7b6dd2..8843c14 100644 --- a/test/service.document.js +++ b/test/service.document.js @@ -3,15 +3,15 @@ import request from 'supertest'; import { host, port, bookSchema, odata, assertSuccess } from './support/setup'; import FakeDb from './support/fake-db'; -describe('metadata.format', () => { +describe('service.document', () => { let httpServer, server, db; const jsonDocument = { - '@context': 'http://localhost:8080/', + '@context': 'http://localhost:3000/$metadata', value: [{ kind: 'EntitySet', - name: 'books', - url: 'books' + name: 'book', + url: 'book' }] }; beforeEach(async function() { @@ -33,6 +33,14 @@ describe('metadata.format', () => { res.body.should.deepEqual(jsonDocument); }); + it('should return json if asterix pattern match', async function() { + httpServer = server.listen(port); + const res = await request(host).get('/').set('accept', '*/*'); + assertSuccess(res); + checkContentType(res, 'application/json'); + res.body.should.deepEqual(jsonDocument); + }); + it('should return 406 if other than json format requested', async function() { httpServer = server.listen(port); const res = await request(host).get('/').set('accept', 'application/xml'); From dd4f72f730c6ba4c2f6d662d14f39d87398ae8ce Mon Sep 17 00:00:00 2001 From: Richard Martens Date: Sun, 23 Oct 2022 21:53:08 +0200 Subject: [PATCH 07/64] Support for 'default Value', Maxlength and Boolean implemented. Also support for multiple mongo attributes for a property implemented --- src/metadata/ODataMetadata.js | 66 ++++-- src/metadata/xmlWriter.js | 3 + test/metadata.action..js | 36 ++-- test/metadata.format.js | 14 +- test/metadata.function.js | 10 +- test/metadata.js | 338 ++++++++++++++++++++++++++++++ test/metadata.resource.complex.js | 46 ++-- test/support/fake-db-model.js | 59 +++--- 8 files changed, 474 insertions(+), 98 deletions(-) create mode 100644 test/metadata.js diff --git a/src/metadata/ODataMetadata.js b/src/metadata/ODataMetadata.js index f9b95c1..e720b44 100644 --- a/src/metadata/ODataMetadata.js +++ b/src/metadata/ODataMetadata.js @@ -46,12 +46,20 @@ export default class Metadata { return router; } - visitProperty(node, root) { + visitProperty(node, model, root) { const result = {}; + if (model.default) { + result.$DefaultValue = model.default; + } + switch (node.instance) { case 'ObjectId': - result.$Type = 'self.ObjectId'; + result.$Type = 'node.odata.ObjectId'; + break; + + case 'Boolean': + result.$Type = 'Edm.Boolean'; break; case 'Number': @@ -64,18 +72,21 @@ export default class Metadata { case 'String': result.$Type = 'Edm.String'; + if (model.maxLength) { + result.$MaxLength = model.maxLength; + } break; - case 'Array': // node.path = p1; node.schema.paths + case 'Array': result.$Collection = true; if (node.schema && node.schema.paths) { this._count += 1; const notClassifiedName = `${node.path}Child${this._count}`; // Array of complex type - result.$Type = `self.${notClassifiedName}`; - root(notClassifiedName, this.visitor('ComplexType', node.schema.paths, root)); + result.$Type = `node.odata.${notClassifiedName}`; + root(notClassifiedName, this.visitor('ComplexType', node.schema.paths, model[0], root)); } else { - const arrayItemType = this.visitor('Property', { instance: node.options.type[0].name }, root); + const arrayItemType = this.visitor('Property', { instance: node.options.type[0].name }, model[0], root); result.$Type = arrayItemType.$Type; } @@ -88,13 +99,26 @@ export default class Metadata { return result; } - visitEntityType(node, root) { + resolveModelproperty(model, property) { + const props = property.split('.'); + + if (props.length > 1) { + const index = property.indexOf('.') + 1; + + return this.resolveModelproperty(model[props[0]], property.substr(index)); + } + + return model[property]; + } + + visitEntityType(node, model, root) { const properties = Object.keys(node) .filter((path) => path !== '_id') .reduce((previousProperty, curentProperty) => { + const modelProperty = this.resolveModelproperty(model, curentProperty); const result = { ...previousProperty, - [curentProperty]: this.visitor('Property', node[curentProperty], root), + [curentProperty]: this.visitor('Property', node[curentProperty], modelProperty, root), }; return result; @@ -104,20 +128,20 @@ export default class Metadata { $Kind: 'EntityType', $Key: ['id'], id: { - $Type: 'self.ObjectId', + $Type: 'node.odata.ObjectId', $Nullable: false, }, ...properties, }; } - visitComplexType(node, root) { + visitComplexType(node, model, root) { const properties = Object.keys(node) .filter((item) => item !== '_id') .reduce((previousProperty, curentProperty) => { const result = { ...previousProperty, - [curentProperty]: this.visitor('Property', node[curentProperty], root), + [curentProperty]: this.visitor('Property', node[curentProperty], model[curentProperty], root), }; return result; @@ -135,7 +159,7 @@ export default class Metadata { $IsBound: true, $Parameter: [{ $Name: node.resource, - $Type: `self.${node.resource}`, + $Type: `node.odata.${node.resource}`, $Collection: node.binding === 'collection' ? true : undefined, }], }; @@ -148,13 +172,13 @@ export default class Metadata { }; } - visitor(type, node, root) { + visitor(type, node, model, root) { switch (type) { case 'Property': - return this.visitProperty(node, root); + return this.visitProperty(node, model, root); case 'ComplexType': - return this.visitComplexType(node, root); + return this.visitComplexType(node, model, root); case 'Action': return Metadata.visitAction(node); @@ -163,7 +187,7 @@ export default class Metadata { return Metadata.visitFunction(node, root); default: - return this.visitEntityType(node, root); + return this.visitEntityType(node, model, root); } } @@ -177,7 +201,7 @@ export default class Metadata { if (resource instanceof Resource) { const { paths } = resource.model.model.schema; - result[currentResource] = this.visitor('EntityType', paths, attachToRoot); + result[currentResource] = this.visitor('EntityType', paths, resource._model, attachToRoot); const actions = Object.keys(resource.actions); if (actions && actions.length) { actions.forEach((action) => { @@ -199,11 +223,11 @@ export default class Metadata { if (resource instanceof Resource) { result[currentResource] = { $Collection: true, - $Type: `self.${currentResource}`, + $Type: `node.odata.${currentResource}`, }; } else if (resource instanceof Function) { result[currentResource] = { - $Function: `self.${currentResource}`, + $Function: `node.odata.${currentResource}`, }; } @@ -218,8 +242,8 @@ export default class Metadata { $MaxLength: 24, }, ...entityTypes, - $EntityContainer: 'org.example.DemoService', - ['org.example.DemoService']: { // eslint-disable-line no-useless-computed-key + $EntityContainer: 'node.odata', + ['node.odata']: { // eslint-disable-line no-useless-computed-key $Kind: 'EntityContainer', ...entitySets, }, diff --git a/src/metadata/xmlWriter.js b/src/metadata/xmlWriter.js index 34a109b..dcf1d18 100644 --- a/src/metadata/xmlWriter.js +++ b/src/metadata/xmlWriter.js @@ -90,6 +90,9 @@ export default class XmlWriter { if (node.$Collection) { attributes += ' Collection="true"'; } + if (node.$DefaultValue) { + attributes += ` DefaultValue="${node.$DefaultValue}"`; + } return ``; } diff --git a/test/metadata.action..js b/test/metadata.action..js index ba1e38d..4da1a2b 100644 --- a/test/metadata.action..js +++ b/test/metadata.action..js @@ -32,26 +32,26 @@ describe('metadata.action', () => { $IsBound: true, $Parameter: [{ $Name: 'book', - $Type: 'self.book' + $Type: 'node.odata.book' }] }, 'book': { $Kind: "EntityType", $Key: ["id"], id: { - $Type: "self.ObjectId", + $Type: "node.odata.ObjectId", $Nullable: false, }, author: { $Type: 'Edm.String' } }, - $EntityContainer: 'org.example.DemoService', - ['org.example.DemoService']: { + $EntityContainer: 'node.odata', + ['node.odata']: { $Kind: 'EntityContainer', 'book': { $Collection: true, - $Type: `self.book`, + $Type: `node.odata.book`, } }, }; @@ -70,21 +70,21 @@ describe('metadata.action', () => { const xmlDocument = ` - + - + - + - + @@ -113,7 +113,7 @@ describe('metadata.action', () => { $IsBound: true, $Parameter: [{ $Name: 'book', - $Type: 'self.book', + $Type: 'node.odata.book', $Collection: true }] }, @@ -121,19 +121,19 @@ describe('metadata.action', () => { $Kind: "EntityType", $Key: ["id"], id: { - $Type: "self.ObjectId", + $Type: "node.odata.ObjectId", $Nullable: false, }, author: { $Type: 'Edm.String' } }, - $EntityContainer: 'org.example.DemoService', - ['org.example.DemoService']: { + $EntityContainer: 'node.odata', + ['node.odata']: { $Kind: 'EntityContainer', 'book': { $Collection: true, - $Type: `self.book`, + $Type: `node.odata.book`, } }, }; @@ -152,21 +152,21 @@ describe('metadata.action', () => { const xmlDocument = ` - + - + - + - + diff --git a/test/metadata.format.js b/test/metadata.format.js index 71b50e1..b256d6d 100644 --- a/test/metadata.format.js +++ b/test/metadata.format.js @@ -20,7 +20,7 @@ describe('metadata.format', () => { $Kind: "EntityType", $Key: ["id"], id: { - $Type: "self.ObjectId", + $Type: "node.odata.ObjectId", $Nullable: false, }, author: { @@ -42,26 +42,26 @@ describe('metadata.format', () => { $Type: 'Edm.String' } }, - $EntityContainer: 'org.example.DemoService', - ['org.example.DemoService']: { + $EntityContainer: 'node.odata', + ['node.odata']: { $Kind: 'EntityContainer', book: { $Collection: true, - $Type: `self.book`, + $Type: `node.odata.book`, } }, }; const xmlDocument = ` - + - + @@ -70,7 +70,7 @@ describe('metadata.format', () => { - + diff --git a/test/metadata.function.js b/test/metadata.function.js index 5a81520..bc93f26 100644 --- a/test/metadata.function.js +++ b/test/metadata.function.js @@ -33,11 +33,11 @@ describe('metadata.function', () => { $Type: 'Edm.String' } }, - $EntityContainer: 'org.example.DemoService', - ['org.example.DemoService']: { + $EntityContainer: 'node.odata', + ['node.odata']: { $Kind: 'EntityContainer', 'odata-function': { - $Function: 'self.odata-function' + $Function: 'node.odata.odata-function' } }, }; @@ -57,14 +57,14 @@ describe('metadata.function', () => { const xmlDocument = ` - + - + diff --git a/test/metadata.js b/test/metadata.js new file mode 100644 index 0000000..a327767 --- /dev/null +++ b/test/metadata.js @@ -0,0 +1,338 @@ +// For issue: https://github.com/TossShinHwa/node-odata/issues/96 +// For issue: https://github.com/TossShinHwa/node-odata/issues/25 + +import 'should'; +import request from 'supertest'; +import { host, conn, port, odata, assertSuccess } from './support/setup'; +import FakeDb from './support/fake-db'; + +describe('metadata', () => { + let httpServer, server, db; + + beforeEach(async function() { + db = new FakeDb(); + server = odata(db); + + }); + + afterEach(() => { + httpServer.close(); + }); + + it('should return json metadata and ignore unknown attributes', async function() { + const jsonDocument = { + $Version: '4.0', + ObjectId: { + $Kind: "TypeDefinition", + $UnderlyingType: "Edm.String", + $MaxLength: 24 + }, + book: { + $Kind: "EntityType", + $Key: ["id"], + id: { + $Type: "node.odata.ObjectId", + $Nullable: false, + }, + price: { + $Type: 'Edm.Double' + }, + author: { + $Type: 'Edm.String' + } + }, + $EntityContainer: 'node.odata', + ['node.odata']: { + $Kind: 'EntityContainer', + book: { + $Collection: true, + $Type: `node.odata.book`, + } + }, + }; + server.resource('book', { + price: { + type: Number, + min: 1, + max: 300 + }, + author: { + type: String, + required: true, + trim:true, + unique: true, + minLength: 2, + match: [/[a-z]+/, 'It must containatleast one lowercase letter'], + validate: [{ + validator: (value) => value.match(/[A-Z]+/), + msg: 'It must contain at least one capital letter' + }] + } + }); + httpServer = server.listen(port); + const res = await request(host).get('/$metadata?$format=json'); + assertSuccess(res); + res.body.should.deepEqual(jsonDocument); + }); + + it('should return xml metadata and ignore unknown attributes', async function() { + const xmlDocument = + ` + + + + + + + + + + + + + + + + + + `.replace(/\s*\s*/g, '>'); + server.resource('book', { + price: { + type: Number, + min: 1, + max: 300 + }, + author: { + type: String, + required: true, + minLength: 2, + match: [/[a-z]+/, 'It must containatleast one lowercase letter'], + validate: [{ + validator: (value) => value.match(/[A-Z]+/), + msg: 'It must contain at least one capital letter' + }] + } + }); + httpServer = server.listen(port); + const res = await request(host).get('/$metadata'); + assertSuccess(res); + res.text.should.equal(xmlDocument); + }); + + it('should return json metadata with maxLength attribute', async function() { + const jsonDocument = { + $Version: '4.0', + ObjectId: { + $Kind: "TypeDefinition", + $UnderlyingType: "Edm.String", + $MaxLength: 24 + }, + book: { + $Kind: "EntityType", + $Key: ["id"], + id: { + $Type: "node.odata.ObjectId", + $Nullable: false, + }, + author: { + $Type: 'Edm.String', + $MaxLength: 25 + } + }, + $EntityContainer: 'node.odata', + ['node.odata']: { + $Kind: 'EntityContainer', + book: { + $Collection: true, + $Type: `node.odata.book`, + } + }, + }; + server.resource('book', { + author: { + type: String, + maxLength: 25 + } + }); + httpServer = server.listen(port); + const res = await request(host).get('/$metadata?$format=json'); + assertSuccess(res); + res.body.should.deepEqual(jsonDocument); + }); + + it('should return xml metadata with maxLength attribute', async function() { + const xmlDocument = + ` + + + + + + + + + + + + + + + + + `.replace(/\s*\s*/g, '>'); + server.resource('book', { + author: { + type: String, + maxLength: 25 + } + }); + httpServer = server.listen(port); + const res = await request(host).get('/$metadata'); + assertSuccess(res); + res.text.should.equal(xmlDocument); + }); + + it('should return json metadata with default value attribute', async function() { + const jsonDocument = { + $Version: '4.0', + ObjectId: { + $Kind: "TypeDefinition", + $UnderlyingType: "Edm.String", + $MaxLength: 24 + }, + book: { + $Kind: "EntityType", + $Key: ["id"], + id: { + $Type: "node.odata.ObjectId", + $Nullable: false, + }, + author: { + $Type: 'Edm.String', + $DefaultValue: "William Shakespeare" + } + }, + $EntityContainer: 'node.odata', + ['node.odata']: { + $Kind: 'EntityContainer', + book: { + $Collection: true, + $Type: `node.odata.book`, + } + }, + }; + server.resource('book', { + author: { + type: String, + default: 'William Shakespeare' + } + }); + httpServer = server.listen(port); + const res = await request(host).get('/$metadata?$format=json'); + assertSuccess(res); + res.body.should.deepEqual(jsonDocument); + }); + + it('should return xml metadata with default value attribute', async function() { + const xmlDocument = + ` + + + + + + + + + + + + + + + + + `.replace(/\s*\s*/g, '>'); + server.resource('book', { + author: { + type: String, + default: 'William Shakespeare' + } + }); + httpServer = server.listen(port); + const res = await request(host).get('/$metadata'); + assertSuccess(res); + res.text.should.equal(xmlDocument); + }); + + it('should return json metadata with boolean property', async function() { + const jsonDocument = { + $Version: '4.0', + ObjectId: { + $Kind: "TypeDefinition", + $UnderlyingType: "Edm.String", + $MaxLength: 24 + }, + book: { + $Kind: "EntityType", + $Key: ["id"], + id: { + $Type: "node.odata.ObjectId", + $Nullable: false, + }, + salted: { + $Type: 'Edm.Boolean' + } + }, + $EntityContainer: 'node.odata', + ['node.odata']: { + $Kind: 'EntityContainer', + book: { + $Collection: true, + $Type: `node.odata.book`, + } + }, + }; + server.resource('book', { + salted: { + type: Boolean + } + }); + httpServer = server.listen(port); + const res = await request(host).get('/$metadata?$format=json'); + assertSuccess(res); + res.body.should.deepEqual(jsonDocument); + }); + + it('should return xml metadata with boolean property', async function() { + const xmlDocument = + ` + + + + + + + + + + + + + + + + + `.replace(/\s*\s*/g, '>'); + server.resource('book', { + salted: { + type: Boolean + } + }); + httpServer = server.listen(port); + const res = await request(host).get('/$metadata'); + assertSuccess(res); + res.text.should.equal(xmlDocument); + }); + +}); diff --git a/test/metadata.resource.complex.js b/test/metadata.resource.complex.js index ecdf50b..0d3844c 100644 --- a/test/metadata.resource.complex.js +++ b/test/metadata.resource.complex.js @@ -36,20 +36,20 @@ describe('metadata.resource.complex', () => { $Kind: "EntityType", $Key: ["id"], id: { - $Type: "self.ObjectId", + $Type: "node.odata.ObjectId", $Nullable: false, }, p1: { - $Type: 'self.p1Child1', + $Type: 'node.odata.p1Child1', $Collection: true } }, - $EntityContainer: 'org.example.DemoService', - ['org.example.DemoService']: { + $EntityContainer: 'node.odata', + ['node.odata']: { $Kind: 'EntityContainer', 'complex-model': { $Collection: true, - $Type: `self.complex-model`, + $Type: `node.odata.complex-model`, } }, }; @@ -68,7 +68,7 @@ describe('metadata.resource.complex', () => { const xmlDocument = ` - + @@ -78,11 +78,11 @@ describe('metadata.resource.complex', () => { - - + + - + @@ -110,7 +110,7 @@ describe('metadata.resource.complex', () => { $Kind: "EntityType", $Key: ["id"], id: { - $Type: "self.ObjectId", + $Type: "node.odata.ObjectId", $Nullable: false, }, p3: { @@ -118,12 +118,12 @@ describe('metadata.resource.complex', () => { $Collection: true } }, - $EntityContainer: 'org.example.DemoService', - ['org.example.DemoService']: { + $EntityContainer: 'node.odata', + ['node.odata']: { $Kind: 'EntityContainer', 'complex-model': { $Collection: true, - $Type: `self.complex-model`, + $Type: `node.odata.complex-model`, } }, }; @@ -140,18 +140,18 @@ describe('metadata.resource.complex', () => { const xmlDocument = ` - + - + - + @@ -177,19 +177,19 @@ describe('metadata.resource.complex', () => { $Kind: "EntityType", $Key: ["id"], id: { - $Type: "self.ObjectId", + $Type: "node.odata.ObjectId", $Nullable: false, }, 'p4.p5': { $Type: 'Edm.String' } }, - $EntityContainer: 'org.example.DemoService', - ['org.example.DemoService']: { + $EntityContainer: 'node.odata', + ['node.odata']: { $Kind: 'EntityContainer', 'complex-model': { $Collection: true, - $Type: `self.complex-model`, + $Type: `node.odata.complex-model`, } }, }; @@ -208,18 +208,18 @@ describe('metadata.resource.complex', () => { const xmlDocument = ` - + - + - + diff --git a/test/support/fake-db-model.js b/test/support/fake-db-model.js index 95315a2..945fb9f 100644 --- a/test/support/fake-db-model.js +++ b/test/support/fake-db-model.js @@ -18,41 +18,52 @@ export default class Model { static toPath(model, prefix) { let result = {}; - Object.keys(model).forEach((item) => { - const propName = prefix ? `${prefix}.${item}` : item; - - if (Array.isArray(model[item])) { + Object.keys(model).forEach((item) => { + const propName = prefix ? `${prefix}.${item}` : item; + + if (Array.isArray(model[item])) { + result[propName] = { + path: propName, + instance: 'Array' + }; + if (model[item][0].name) { + // Array of primitive Types + result[propName].options = { + type: [{ + name: model[item][0].name + }] + }; + } else { + // Array of objects + result[propName].schema = { + paths: Model.toPath(model[item][0]) + }; + } + } else if (typeof model[item] === 'object') { + if (model[item].type) { + // structured property e.g. author: { type: String } result[propName] = { path: propName, - instance: 'Array' + instance: model[item].type.name }; - if (model[item][0].name) { - // Array of primitive Types - result[propName].options = { - type: [{ - name: model[item][0].name - }] - }; - } else { - // Array of objects - result[propName].schema = { - paths: Model.toPath(model[item][0]) - }; + if (model[item].maxLength) { + result[propName].maxlengthValidator = () => {}; } - } else if ( typeof model[item] === 'object' ) { + } else { const subSchema = Model.toPath(model[item], propName); result = { ...result, ...subSchema }; - } else { - result[propName] = { - path: propName, - instance: model[item].name - }; } - }); + } else { + result[propName] = { + path: propName, + instance: model[item].name + }; + } + }); return result; } From 8ecd1921760f70d930801ce319f8196e09be3a9a Mon Sep 17 00:00:00 2001 From: Richard Martens Date: Fri, 4 Nov 2022 16:48:38 +0100 Subject: [PATCH 08/64] replacing dots in Names. Closes #106 --- .vscode/launch.json | 35 ++++++ src/db/idPlugin.js | 10 -- src/metadata/ODataMetadata.js | 7 +- src/metadata/ODataServiceDocument.js | 2 +- src/parser/countParser.js | 2 +- src/parser/filterParser.js | 175 ++++++++++++++++---------- src/pipes.js | 31 ++--- src/rest/list.js | 2 +- src/writer/jsonWriter.js | 70 +++++++++++ src/{metadata => writer}/xmlWriter.js | 2 +- test/metadata.resource.complex.js | 98 ++++++++++++++- test/model.complex.filter.js | 4 +- test/rest.get.js | 18 ++- 13 files changed, 346 insertions(+), 110 deletions(-) create mode 100644 .vscode/launch.json create mode 100644 src/writer/jsonWriter.js rename src/{metadata => writer}/xmlWriter.js (97%) diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000..914d4ea --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,35 @@ +{ + // Verwendet IntelliSense zum Ermitteln möglicher Attribute. + // Zeigen Sie auf vorhandene Attribute, um die zugehörigen Beschreibungen anzuzeigen. + // Weitere Informationen finden Sie unter https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [ + { + "type": "node", + "request": "launch", + "name": "Mocha single Test", + "skipFiles": [ + "/**" + ], + "program": "${workspaceFolder}/node_modules/mocha/bin/_mocha", + "args": [ + "--require", + "@babel/register", + "--reporter", + "dot", + "--timeout", + "300000", + "test/odata.query.filter.functions.js" + ] + }, + { + "type": "node", + "request": "launch", + "name": "Launch Complex Resource", + "skipFiles": [ + "/**" + ], + "program": "${workspaceFolder}/examples/complex-resource/index.js" + } + ] +} \ No newline at end of file diff --git a/src/db/idPlugin.js b/src/db/idPlugin.js index 10f6881..7b4b8cd 100644 --- a/src/db/idPlugin.js +++ b/src/db/idPlugin.js @@ -2,16 +2,6 @@ import * as uuid from 'uuid'; /*eslint-disable */ export default function (schema) { - // add _id to schema. - if (!schema.paths._id) { - schema.add({ - _id: { - type: String, - unique: true, - } - }); - } - // display value of _id when request id. if (!schema.paths.id) { schema.virtual('id').get(function getId() { diff --git a/src/metadata/ODataMetadata.js b/src/metadata/ODataMetadata.js index e720b44..e9a2012 100644 --- a/src/metadata/ODataMetadata.js +++ b/src/metadata/ODataMetadata.js @@ -116,9 +116,10 @@ export default class Metadata { .filter((path) => path !== '_id') .reduce((previousProperty, curentProperty) => { const modelProperty = this.resolveModelproperty(model, curentProperty); + const propertyName = curentProperty.replace(/\./g, '-'); const result = { ...previousProperty, - [curentProperty]: this.visitor('Property', node[curentProperty], modelProperty, root), + [propertyName]: this.visitor('Property', node[curentProperty], modelProperty, root), }; return result; @@ -139,9 +140,11 @@ export default class Metadata { const properties = Object.keys(node) .filter((item) => item !== '_id') .reduce((previousProperty, curentProperty) => { + const propertyName = curentProperty.replace(/\./g, '-'); + const modelProperty = this.resolveModelproperty(model, curentProperty); const result = { ...previousProperty, - [curentProperty]: this.visitor('Property', node[curentProperty], model[curentProperty], root), + [propertyName]: this.visitor('Property', node[curentProperty], modelProperty, root), }; return result; diff --git a/src/metadata/ODataServiceDocument.js b/src/metadata/ODataServiceDocument.js index fda01ed..424e2a5 100644 --- a/src/metadata/ODataServiceDocument.js +++ b/src/metadata/ODataServiceDocument.js @@ -63,7 +63,7 @@ export default class Metadata { return new Promise((resolve) => { resolve({ status: 200, - entity: document, + serviceDocument: document, }); }); } diff --git a/src/parser/countParser.js b/src/parser/countParser.js index 8f2b275..d656d78 100644 --- a/src/parser/countParser.js +++ b/src/parser/countParser.js @@ -12,7 +12,7 @@ export default (mongooseModel, $count, $filter) => new Promise((resolve, reject) switch ($count) { case 'true': { const query = mongooseModel.find(); - filterParser(query, $filter); + filterParser(query, $filter, mongooseModel); query.count((err, count) => { resolve(count); }); diff --git a/src/parser/filterParser.js b/src/parser/filterParser.js index 84ceb9c..a0fedb2 100644 --- a/src/parser/filterParser.js +++ b/src/parser/filterParser.js @@ -36,6 +36,48 @@ const stringHelper = { }, }; +class KeyParser { + constructor(model) { + this._model = model; + } + + getConvertedKey(input) { + let key = input; + + if (key === 'id') { + key = '_id'; + return key; + } + if (this._model[key]) { + // known simple property + return key; + } + + const match = key.match(/\s*(contains|indexof|year)\(\s*([\w+-]+)/); + + if (match) { + // contains function was called with id e.g. contains(title, 'ggm') + const functionKey = match[2]; + + key = key.replace(functionKey, this.getConvertedKey(functionKey)); + } else { + key = Object.keys(this._model.model.schema.paths).find((item) => { + const replacedDots = item.replace(/\./g, '(.|-){1}'); + const regex = new RegExp(`^${replacedDots}$`); + + return key.match(regex); + }); + if (!key) { + const error = new Error(`Unknown property '${this._input}' in entity '${this._model.name}'`); + + error.status = '400'; + throw error; + } + } + return key; + } +} + const validator = { formatValue: (value) => { let val; @@ -56,85 +98,90 @@ const validator = { }, }; -export default (query, $filter) => new Promise((resolve, reject) => { +export default (query, $filter, model) => new Promise((resolve, reject) => { if (!$filter) { resolve(); return; } - const condition = split($filter, ['and', 'or']) - .filter((item) => (item !== 'and' && item !== 'or')); + try { + const condition = split($filter, ['and', 'or']) + .filter((item) => (item !== 'and' && item !== 'or')); - condition.forEach((item) => { - // parse "indexof(title,'X1ML') gt 0" - const conditionArr = split(item, OPERATORS_KEYS); - if (conditionArr.length === 0) { - // parse "contains(title,'X1ML')" - conditionArr.push(item); - } - if (conditionArr.length !== 3 && conditionArr.length !== 1) { - return reject(new Error(`Syntax error at '${item}'.`)); - } + condition.forEach((item) => { + // parse "indexof(title,'X1ML') gt 0" + const conditionArr = split(item, OPERATORS_KEYS); + if (conditionArr.length === 0) { + // parse "contains(title,'X1ML')" + conditionArr.push(item); + } + if (conditionArr.length !== 3 && conditionArr.length !== 1) { + throw new Error(`Syntax error at '${item}'.`); + } - let key = conditionArr[0]; - const [, odataOperator, value] = conditionArr; + const keyParser = new KeyParser(model); + let key = conditionArr[0]; + const [, odataOperator, value] = conditionArr; - if (key === 'id') key = '_id'; + key = keyParser.getConvertedKey(key); - let val; - if (value !== undefined) { - const result = validator.formatValue(value); - if (result.err) { - return reject(result.err); + let val; + if (value !== undefined) { + const result = validator.formatValue(value); + if (result.err) { + return reject(result.err); + } + val = result.val; } - val = result.val; - } - // function query - const functionKey = key.substring(0, key.indexOf('(')); - if (['indexof', 'year', 'contains'].indexOf(functionKey) > -1) { - functions[functionKey](query, key, odataOperator, val); - } else { - if (conditionArr.length === 1) { - return reject(new Error(`Syntax error at '${item}'.`)); - } - if (value === 'null') { + // function query + const functionKey = key.substring(0, key.indexOf('(')); + if (['indexof', 'year', 'contains'].indexOf(functionKey) > -1) { + functions[functionKey](query, key, odataOperator, val); + } else { + if (conditionArr.length === 1) { + return reject(new Error(`Syntax error at '${item}'.`)); + } + if (value === 'null') { + switch (odataOperator) { + case 'eq': + query.exists(key, false); + return resolve(); + case 'ne': + query.exists(key, true); + return resolve(); + default: + break; + } + } + // operator query switch (odataOperator) { case 'eq': - query.exists(key, false); - return resolve(); + query.where(key).equals(val); + break; case 'ne': - query.exists(key, true); - return resolve(); - default: + query.where(key).ne(val); break; + case 'gt': + query.where(key).gt(val); + break; + case 'ge': + query.where(key).gte(val); + break; + case 'lt': + query.where(key).lt(val); + break; + case 'le': + query.where(key).lte(val); + break; + default: + return reject(new Error("Incorrect operator at '#{item}'.")); } } - // operator query - switch (odataOperator) { - case 'eq': - query.where(key).equals(val); - break; - case 'ne': - query.where(key).ne(val); - break; - case 'gt': - query.where(key).gt(val); - break; - case 'ge': - query.where(key).gte(val); - break; - case 'lt': - query.where(key).lt(val); - break; - case 'le': - query.where(key).lte(val); - break; - default: - return reject(new Error("Incorrect operator at '#{item}'.")); - } - } - return query; - }); - resolve(); + return query; + }); + resolve(); + } catch (error) { + reject(error); + } }); diff --git a/src/pipes.js b/src/pipes.js index e8acd1c..c016e44 100644 --- a/src/pipes.js +++ b/src/pipes.js @@ -1,13 +1,9 @@ import http from 'http'; -import XmlWriter from './metadata/xmlWriter'; +import XmlWriter from './writer/xmlWriter'; +import JsonWirter from './writer/jsonWriter'; const xmlWriter = new XmlWriter(); - -function writeJson(res, data, status, resolve) { - res.type('application/json'); - res.status(status).jsonp(data); - resolve(data); -} +const jsonWriter = new JsonWirter(); function getMediaType(accept, data) { // reduce multi mimetypes to most weigth mimetype @@ -27,7 +23,7 @@ function getMediaType(accept, data) { return result; }, {}); - if (!data.entity && mostWeightMimetype.mimetype.match(/((application|\*)\/(xml|\*)|^xml$)/)) { + if (data.metadata && mostWeightMimetype.mimetype.match(/((application|\*)\/(xml|\*)|^xml$)/)) { return 'application/xml'; } if (mostWeightMimetype.mimetype.match(/((application|\*)\/(json|\*)|^json$)/)) { return 'application/json'; @@ -53,10 +49,10 @@ function getWriter(req, result) { // xml representation of metadata switch (mediaType) { case 'application/json': - return writeJson; + return jsonWriter.writeJson.bind(jsonWriter); case 'application/xml': - if (result.entity) { + if (!result.metadata) { // xml wirter for entities and actions is not implemented const error406 = new Error('Not acceptable'); @@ -67,8 +63,8 @@ function getWriter(req, result) { default: // no media type requested set defaults depend of context - if (result.entity) { - return writeJson; // default for entities and actions + if (result.entity || result.serviceDocument) { + return jsonWriter.writeJson.bind(jsonWriter); // default for entities and actions } return xmlWriter.writeXml.bind(xmlWriter); // default for metadata @@ -105,17 +101,8 @@ const respondPipe = (req, res, result) => new Promise((resolve, reject) => { const status = result.status || 200; const writer = getWriter(req, result); - let data; - - if (result.entity) { - // json Representation of data - data = result.entity; - } else { - // xml representation of metadata - data = result.metadata; - } - writer(res, data, status, resolve); + writer(res, result, status, resolve); } catch (error) { reject(error); } diff --git a/src/rest/list.js b/src/rest/list.js index 9258e92..f827397 100644 --- a/src/rest/list.js +++ b/src/rest/list.js @@ -19,7 +19,7 @@ function _dataQuery(model, { }, options) { return new Promise((resolve, reject) => { const query = model.find(); - filterParser(query, filter) + filterParser(query, filter, model) .then(() => orderbyParser(query, orderby || options.orderby)) .then(() => skipParser(query, skip, options.maxSkip)) .then(() => topParser(query, top, options.maxTop)) diff --git a/src/writer/jsonWriter.js b/src/writer/jsonWriter.js new file mode 100644 index 0000000..9d01f22 --- /dev/null +++ b/src/writer/jsonWriter.js @@ -0,0 +1,70 @@ +export default class { + writeJson(res, data, status, resolve) { + let normalizedData = data.entity; + + if (data.entity) { + if (data.entity.toObject) { + normalizedData = data.entity.toObject(); + } else if (Array.isArray(data.entity.value)) { + normalizedData = { + value: data.entity.value.map((item) => { + const result = item.toObject ? item.toObject() : item; + + return result; + }), + '@odata.count': data.entity['@odata.count'], + }; + } else if (data.entity.value) { + normalizedData = { + value: data.entity.value.toObject ? data.entity.value.toObject() : data.entity.value, + }; + } + normalizedData = this.replaceDot(normalizedData); + } else { + normalizedData = data.metadata || data.serviceDocument; + } + + res.type('application/json'); + res.status(status).jsonp(normalizedData); + resolve(normalizedData); + } + + replaceDot(value) { + if (!(value === null || value === undefined || typeof value === 'function')) { + if (Array.isArray(value)) { + return this.replaceDotinArray(value); + } + if (typeof value === 'object') { + return this.replaceObject(value); + } + } + + return value; + } + + replaceDotinArray(array) { + const result = array; + + result.forEach((item, index) => { + result[index] = this.replaceDot(item); + }); + return result; + } + + replaceObject(obj) { + const result = obj; + + Object.keys(result).forEach((item) => { + if (item.match(/^[^@][^.]+(\.[^.]+)+/)) { + const newPropertyName = item.replace('.', '-'); + + result[newPropertyName] = this.replaceDot(result[item]); + delete result[item]; + } else { + result[item] = this.replaceDot(result[item]); + } + }); + + return result; + } +} diff --git a/src/metadata/xmlWriter.js b/src/writer/xmlWriter.js similarity index 97% rename from src/metadata/xmlWriter.js rename to src/writer/xmlWriter.js index dcf1d18..d842f37 100644 --- a/src/metadata/xmlWriter.js +++ b/src/writer/xmlWriter.js @@ -180,7 +180,7 @@ export default class XmlWriter { } writeXml(res, data, status, resolve) { - const xml = this.visitor('document', data, '', '').replace(/\s*\s*/g, '>'); + const xml = this.visitor('document', data.metadata, '', '').replace(/\s*\s*/g, '>'); res.type('application/xml'); res.status(status).send(xml); diff --git a/test/metadata.resource.complex.js b/test/metadata.resource.complex.js index 0d3844c..72b4137 100644 --- a/test/metadata.resource.complex.js +++ b/test/metadata.resource.complex.js @@ -165,7 +165,7 @@ describe('metadata.resource.complex', () => { res.text.should.equal(xmlDocument); }); - it('should return json metadata for nested document', async function() { + it('should return json metadata for nested document in document', async function() { const jsonDocument = { $Version: '4.0', ObjectId: { @@ -180,7 +180,7 @@ describe('metadata.resource.complex', () => { $Type: "node.odata.ObjectId", $Nullable: false, }, - 'p4.p5': { + 'p4-p5': { $Type: 'Edm.String' } }, @@ -204,7 +204,7 @@ describe('metadata.resource.complex', () => { res.body.should.deepEqual(jsonDocument); }); - it('should return xml metadata for nested document', async function() { + it('should return xml metadata for nested document in document', async function() { const xmlDocument = ` @@ -216,7 +216,7 @@ describe('metadata.resource.complex', () => { - + @@ -234,4 +234,94 @@ describe('metadata.resource.complex', () => { assertSuccess(res); res.text.should.equal(xmlDocument); }); + + it('should return json metadata for nested document in array', async function() { + const jsonDocument = { + $Version: '4.0', + ObjectId: { + $Kind: "TypeDefinition", + $UnderlyingType: "Edm.String", + $MaxLength: 24 + }, + p2Child1: { + $Kind: "ComplexType", + p3: { + $Type: 'Edm.String' + }, + 'p4-p5': { + $Type: 'Edm.String' + } + }, + p1: { + $Kind: "EntityType", + $Key: ["id"], + id: { + $Type: "node.odata.ObjectId", + $Nullable: false, + }, + p2: { + $Type: 'node.odata.p2Child1', + $Collection: true + } + }, + $EntityContainer: 'node.odata', + ['node.odata']: { + $Kind: 'EntityContainer', + p1: { + $Collection: true, + $Type: `node.odata.p1`, + } + }, + }; + server.resource('p1', { + p2: [{ + p3: String, + p4: { + p5: String + } + }] + }); + httpServer = server.listen(port); + const res = await request(host).get('/$metadata?$format=json'); + res.statusCode.should.equal(200); + res.body.should.deepEqual(jsonDocument); + }); + + it('should return xml metadata for nested document in document', async function() { + const xmlDocument = + ` + + + + + + + + + + + + + + + + + + + + + `.replace(/\s*\s*/g, '>'); + server.resource('p1', { + p2: [{ + p3: String, + p4: { + p5: String + } + }] + }); + httpServer = server.listen(port); + const res = await request(host).get('/$metadata'); + assertSuccess(res); + res.text.should.equal(xmlDocument); + }); }); diff --git a/test/model.complex.filter.js b/test/model.complex.filter.js index 325038a..afb6a7b 100644 --- a/test/model.complex.filter.js +++ b/test/model.complex.filter.js @@ -13,7 +13,7 @@ describe('model.complex.filter', () => { before(() => { db = new Db(); const server = odata(db); - const resource = server.resource('complex-model-filter', { product: [{ price: Number }] }); + const resource = server.resource('complex-model-filter', { product: { price: Number } }); mock = sinon.mock(resource.model); mock.expects('where').once().withArgs('product.price').returns(resource.model); @@ -26,7 +26,7 @@ describe('model.complex.filter', () => { }); it('should work when PUT a complex entity', async function() { - const res = await request(host).get(`/complex-model-filter?$filter=product.price gt 30`); + const res = await request(host).get(`/complex-model-filter?$filter=product-price gt 30`); assertSuccess(res); mock.verify(); diff --git a/test/rest.get.js b/test/rest.get.js index 81f6de9..8b94498 100644 --- a/test/rest.get.js +++ b/test/rest.get.js @@ -5,14 +5,24 @@ import books from './support/books.json'; import FakeDb from './support/fake-db'; describe('rest.get', () => { - let data, httpServer; + let data, cdata, httpServer; before(async function() { const db = new FakeDb(); const server = odata(db); - server.resource('book', bookSchema) + server.resource('book', bookSchema); + server.resource('complex-type', { + p1: { + p2: { + type: String + } + } + }); httpServer = server.listen(port); data = db.addData('book', books); + cdata = db.addData('complex-type', [{ + "p1.p2": "p1.p2 value" + }]); }); after(() => { @@ -37,4 +47,8 @@ describe('rest.get', () => { const res = await request(host).get(`/book(not-exist-id)`); res.status.should.be.equal(404); }); + it('should replace a dot in property names with -', async function() { + const res = await request(host).get(`/complex-type(${cdata[0].id})`); + res.body.should.be.have.property('p1-p2'); + }); }); From 2384476eec5abe4acdfcd5b4795f886d3b58f9d6 Mon Sep 17 00:00:00 2001 From: Richard Martens Date: Thu, 10 Nov 2022 01:14:22 +0100 Subject: [PATCH 09/64] batch operations implemented --- .vscode/launch.json | 3 +- src/ODataResource.js | 26 ++- src/pipes.js | 4 +- src/rest/index.js | 72 ++++--- src/server.js | 6 +- src/spezialResources/ODataBatch.js | 152 +++++++++++++++ .../ODataMetadata.js | 32 ++- .../ODataServiceDocument.js | 33 +++- src/writer/jsonWriter.js | 2 + test/odata.batch.js | 184 ++++++++++++++++++ test/support/fake-db-model.js | 16 ++ 11 files changed, 481 insertions(+), 49 deletions(-) create mode 100644 src/spezialResources/ODataBatch.js rename src/{metadata => spezialResources}/ODataMetadata.js (90%) rename src/{metadata => spezialResources}/ODataServiceDocument.js (65%) create mode 100644 test/odata.batch.js diff --git a/.vscode/launch.json b/.vscode/launch.json index 914d4ea..f5a2f0b 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -11,6 +11,7 @@ "skipFiles": [ "/**" ], + "runtimeArgs": ["--preserve-symlinks"], "program": "${workspaceFolder}/node_modules/mocha/bin/_mocha", "args": [ "--require", @@ -19,7 +20,7 @@ "dot", "--timeout", "300000", - "test/odata.query.filter.functions.js" + "test/odata.batch.js" ] }, { diff --git a/src/ODataResource.js b/src/ODataResource.js index 9be87f4..aa83110 100644 --- a/src/ODataResource.js +++ b/src/ODataResource.js @@ -153,7 +153,7 @@ export default class { return this; } - _router(setting = {}) { + _validateUrl() { // remove '/' if url is startwith it. if (this._url.indexOf('/') === 0) { this._url = this._url.substr(1); @@ -164,6 +164,26 @@ export default class { throw new Error(`Url of resource[${this._name}] can't contain "/",` + 'it can only be allowed to exist in the beginning.'); } + } + + match(method, url) { + const setting = this._server.getSettings(); + + this._validateUrl(); + + const routes = rest.getMiddlewares(this._url, this._hooks, this.model, { + maxTop: min([setting.maxTop, this._maxTop]), + maxSkip: min([setting.maxSkip, this._maxSkip]), + orderby: this._orderby || setting.orderby, + }); + const route = routes.find((item) => item.method === method + && url.match(item.regex)); + + return route ? route.middleware : undefined; + } + + _router(setting = {}) { + this._validateUrl(); const params = { url: this._url, @@ -177,8 +197,4 @@ export default class { return rest.getRouter(this.model, params); } - - find() { - return this.model.find(); - } } diff --git a/src/pipes.js b/src/pipes.js index c016e44..2fe671c 100644 --- a/src/pipes.js +++ b/src/pipes.js @@ -41,7 +41,7 @@ function getWriter(req, result) { if (req.query.$format) { // get requested media type from $format query mediaType = getMediaType(req.query.$format, result); - } else if (req.headers.accept) { + } else if (req.headers && req.headers.accept) { // get requested media type from accept header mediaType = getMediaType(req.headers.accept, result); } @@ -63,7 +63,7 @@ function getWriter(req, result) { default: // no media type requested set defaults depend of context - if (result.entity || result.serviceDocument) { + if (result.entity || result.serviceDocument || result.responses) { return jsonWriter.writeJson.bind(jsonWriter); // default for entities and actions } diff --git a/src/rest/index.js b/src/rest/index.js index bcd264a..b1e8d4d 100644 --- a/src/rest/index.js +++ b/src/rest/index.js @@ -7,69 +7,97 @@ import patch from './patch'; import get from './get'; import pipes from '../pipes'; -function addRestRoutes(router, routes, mongooseModel, options) { - return routes.map((route) => { - const { - method, url, ctrl, hook, - } = route; - return router[method](url, (req, res) => { - pipes.authorizePipe(req, res, hook.auth) - .then(() => pipes.beforePipe(req, res, hook.before)) - .then(() => ctrl(req, mongooseModel, options)) - .then((result) => pipes.respondPipe(req, res, result || {})) - .then((data) => pipes.afterPipe(req, res, hook.after, data)) - .catch((err) => pipes.errorPipe(req, res, err)); - }); - }); -} - -const getRouter = (mongooseModel, { url, hooks, options }) => { +const getRoutes = (url, hooks) => { const resourceListURL = `/${url}`; + const resourceListRegex = new RegExp(`(^\/${url}[?#])|(^\/${url}$)`); const resourceURL = `${resourceListURL}\\(:id\\)`; + const resourceRegex = new RegExp(`^\/${url}\\([^)]+\\)`); - const routes = [ + return [ { method: 'post', url: resourceListURL, + regex: resourceListRegex, ctrl: post, hook: hooks.post, }, { method: 'put', url: resourceURL, + regex: resourceRegex, ctrl: put, hook: hooks.put, }, { method: 'patch', url: resourceURL, - controller: patch, - config: hooks.patch, + regex: resourceRegex, + ctrl: patch, + hook: hooks.patch, }, { method: 'delete', url: resourceURL, + regex: resourceRegex, ctrl: del, hook: hooks.delete, }, { method: 'get', url: resourceURL, + regex: resourceRegex, ctrl: get, hook: hooks.get, }, { method: 'get', url: resourceListURL, + regex: resourceListRegex, ctrl: list, hook: hooks.list, }, ]; +}; + +const getMiddlewares = (url, hooks, mongooseModel, options) => { + const routes = getRoutes(url, hooks); + + return routes.map((route) => { + const { + ctrl, hook, + } = route; + + return { + ...route, + middleware: async (req, res) => { + try { + await pipes.authorizePipe(req, res, hook.auth); + await pipes.beforePipe(req, res, hook.before); + + const result = await ctrl(req, mongooseModel, options); + const data = await pipes.respondPipe(req, res, result || {}); + + pipes.afterPipe(req, res, hook.after, data); + } catch (err) { + pipes.errorPipe(req, res, err); + } + }, + }; + }); +}; +const getRouter = (mongooseModel, { url, hooks, options }) => { + const routes = getMiddlewares(url, hooks, mongooseModel, options); /*eslint-disable */ const router = Router(); /* eslint-enable */ - addRestRoutes(router, routes, mongooseModel, options); + + routes.forEach((route) => { + const { + method, middleware, + } = route; + router[method](route.url, middleware); + }); return router; }; @@ -87,4 +115,4 @@ const getOperationRouter = (resourceUrl, actionUrl, fn, auth) => { return router; }; -export default { getRouter, getOperationRouter }; +export default { getRouter, getMiddlewares, getOperationRouter }; diff --git a/src/server.js b/src/server.js index ac795a0..d702318 100644 --- a/src/server.js +++ b/src/server.js @@ -1,8 +1,9 @@ import createExpress from './express'; import Resource from './ODataResource'; import Func from './ODataFunction'; -import Metadata from './metadata/ODataMetadata'; -import ServiceDocument from './metadata/ODataServiceDocument'; +import Metadata from './spezialResources/ODataMetadata'; +import ServiceDocument from './spezialResources/ODataServiceDocument'; +import Batch from './spezialResources/ODataBatch'; import Db from './db/db'; function checkAuth(auth, req) { @@ -25,6 +26,7 @@ class Server { // 这里也许应该让 resources 支持 odata 查询的, 以方便直接在代码中使用 OData 查询方式来进行数据筛选, 达到隔离 mongo 的效果. this.resources = { $metadata: new Metadata(this), + $batch: new Batch(this), }; this._serviceDocument = new ServiceDocument(this); } diff --git a/src/spezialResources/ODataBatch.js b/src/spezialResources/ODataBatch.js new file mode 100644 index 0000000..a60f8ed --- /dev/null +++ b/src/spezialResources/ODataBatch.js @@ -0,0 +1,152 @@ +import { Router } from 'express'; +import pipes from '../pipes'; + +export default class Batch { + constructor(server) { + this._server = server; + this._hooks = { + }; + this._url = '/\\$batch'; + } + + post() { + return this; + } + + before(fn) { + this._hooks.before = fn; + return this; + } + + after(fn) { + this._hooks.after = fn; + return this; + } + + auth(fn) { + this._hooks.auth = fn; + return this; + } + + middleware = async (req, res) => { + try { + await pipes.authorizePipe(req, res, this._hooks.auth); + await pipes.beforePipe(req, res, this._hooks.before); + + const result = await this.ctrl(req); + const data = await pipes.respondPipe(req, res, result || {}); + + pipes.afterPipe(req, res, this._hooks.after, data); + } catch (err) { + pipes.errorPipe(req, res, err); + } + }; + + match(method, url) { + if (method === 'post' + && url === url.indexOf(this._url.replace(/\\/g, '') === 0)) { + return this.middleware; + } + return undefined; + } + + _router() { + /*eslint-disable */ + const router = Router(); + /* eslint-enable */ + router.post(this._url, this.middleware); + + return router; + } + + static mapToQuery(url) { + const match = url.match(/\?([^#]+)/); + + if (!match || !match.length) { + return {}; + } + + const queryString = match[1]; + const parameters = queryString.split('&').map((parameter) => { + const keyValue = parameter.split('='); + const result = { + key: decodeURIComponent(keyValue[0]), + } + + if (keyValue.length > 1) { + result.value = decodeURIComponent(keyValue[1]); + } + return result; + }); + + return parameters.filter(parameter => parameter.value) + .reduce((previous, current) => { + const result = { + ...previous + }; + + result[current.key] = current.value; + + return result; + }, {}); + } + + async ctrl(req) { + return new Promise((resolve, reject) => { + const responses = req.body.requests.map(async (request) => { + const handler = Object.keys(this._server.resources) + .map((name) => this._server.resources[name].match(request.method, request.url)) + .find((ctrl) => ctrl); + const result = { + id: request.id + }; + const currentRequest = { + headers: request.headers, + query: Batch.mapToQuery(request.url), + body: request.body + }; + + const paramsMatch = request.url.match(/^\/[^#?(]+\('(\w+)'\)/); + + if (paramsMatch && paramsMatch.length > 1) { + currentRequest.params = { + id: paramsMatch[1] + } + } + + if (!handler) { + result.status = 404; + result.body = 'Not Found'; + + } else { + await handler(currentRequest, { + type: (mimetype) => { + result.headers = { + 'content-type': mimetype, + }; + }, + status: (status) => { + result.status = status; + return { + jsonp: (body) => { + result.body = body; + }, + end: () => {} + }; + }, + }); + + } + + + return result; + }); + + Promise.all(responses).then((results) => { + resolve({ + responses: results, + }); + }).catch(error => reject(error)); + }); + } +} diff --git a/src/metadata/ODataMetadata.js b/src/spezialResources/ODataMetadata.js similarity index 90% rename from src/metadata/ODataMetadata.js rename to src/spezialResources/ODataMetadata.js index e9a2012..528e09d 100644 --- a/src/metadata/ODataMetadata.js +++ b/src/spezialResources/ODataMetadata.js @@ -9,6 +9,7 @@ export default class Metadata { this._hooks = { }; this._count = 0; + this._path = '/\\$metadata'; } get() { @@ -30,18 +31,33 @@ export default class Metadata { return this; } + match(method, url) { + if (method === 'get' + && url.indexOf(this._path.replace(/\\/g, '')) === 0) { + return this.middleware; + } + return undefined; + } + + middleware = async (req, res) => { + try { + await pipes.authorizePipe(req, res, this._hooks.auth); + await pipes.beforePipe(req, res, this._hooks.before); + + const result = await this.ctrl(req); + const data = await pipes.respondPipe(req, res, result || {}); + + pipes.afterPipe(req, res, this._hooks.after, data); + } catch (err) { + pipes.errorPipe(req, res, err); + } + }; + _router() { /*eslint-disable */ const router = Router(); /* eslint-enable */ - router.get('/\\$metadata', (req, res) => { - pipes.authorizePipe(req, res, this._hooks.auth) - .then(() => pipes.beforePipe(req, res, this._hooks.before)) - .then(() => this.ctrl(req)) - .then((result) => pipes.respondPipe(req, res, result || {})) - .then((data) => pipes.afterPipe(req, res, this._hooks.after, data)) - .catch((err) => pipes.errorPipe(req, res, err)); - }); + router.get(this._path, this.middleware.bind(this)); return router; } diff --git a/src/metadata/ODataServiceDocument.js b/src/spezialResources/ODataServiceDocument.js similarity index 65% rename from src/metadata/ODataServiceDocument.js rename to src/spezialResources/ODataServiceDocument.js index 424e2a5..4a3221c 100644 --- a/src/metadata/ODataServiceDocument.js +++ b/src/spezialResources/ODataServiceDocument.js @@ -7,7 +7,7 @@ export default class Metadata { this._server = server; this._hooks = { }; - this._count = 0; + this._path = '/'; } get() { @@ -29,18 +29,33 @@ export default class Metadata { return this; } + match(methods, url) { + if (methods === 'get' + && url.indexOf(this._path) === 0) { + return this.middleware; + } + return undefined; + } + + middleware = async (req, res) => { + try { + await pipes.authorizePipe(req, res, this._hooks.auth); + await pipes.beforePipe(req, res, this._hooks.before); + + const result = await this.ctrl(req); + const data = await pipes.respondPipe(req, res, result || {}); + + pipes.afterPipe(req, res, this._hooks.after, data); + } catch (err) { + pipes.errorPipe(req, res, err); + } + }; + _router() { /*eslint-disable */ const router = Router(); /* eslint-enable */ - router.get('/', (req, res) => { - pipes.authorizePipe(req, res, this._hooks.auth) - .then(() => pipes.beforePipe(req, res, this._hooks.before)) - .then(() => this.ctrl(req)) - .then((result) => pipes.respondPipe(req, res, result || {})) - .then((data) => pipes.afterPipe(req, res, this._hooks.after, data)) - .catch((err) => pipes.errorPipe(req, res, err)); - }); + router.get(this._path, this.middleware); return router; } diff --git a/src/writer/jsonWriter.js b/src/writer/jsonWriter.js index 9d01f22..cc414ed 100644 --- a/src/writer/jsonWriter.js +++ b/src/writer/jsonWriter.js @@ -20,6 +20,8 @@ export default class { }; } normalizedData = this.replaceDot(normalizedData); + } else if (data.responses) { + normalizedData = data; } else { normalizedData = data.metadata || data.serviceDocument; } diff --git a/test/odata.batch.js b/test/odata.batch.js new file mode 100644 index 0000000..eaf71d8 --- /dev/null +++ b/test/odata.batch.js @@ -0,0 +1,184 @@ +import 'should'; +import request from 'supertest'; +import { odata, host, port, bookSchema, assertSuccess } from './support/setup'; +import data from './support/books.json'; +import FakeDb from './support/fake-db'; +import sinon from 'sinon'; + +describe('odata.batch', () => { + let httpServer, books, resource, sandbox; + + beforeEach(async function () { + const db = new FakeDb(); + const server = odata(db); + resource = server.resource('book', bookSchema); + books = JSON.parse(JSON.stringify(db.addData('book', data))); + httpServer = server.listen(port); + sandbox = sinon.createSandbox(); + }); + + afterEach(() => { + httpServer.close(); + sandbox.restore(); + }); + + it('should work with get lists', async function () { + const result = [ + { + "title": "XML Developer's Guide" + }, { + "title": "MSXML3: A Comprehensive Guide" + }, { + "title": "Visual Studio 7: A Comprehensive Guide" + } + ]; + + const mock = sandbox.mock(resource.model); + mock.expects('select').once().withArgs({ + _id: 0, + title: 1 + }).returns(resource.model); + mock.expects('$where').once().withArgs('this.title.indexOf(\'Guide\') != -1').returns(resource.model); + const stub = sandbox.stub(resource.model, "exec"); + stub.callsArgWith(0, undefined, result); + const res = await request(host).post(`/$batch`).send({ + requests: [{ + id: "1", + method: "get", + url: `/book?$filter=contains(title, 'Guide')&$select=title` + }] + }); + assertSuccess(res); + mock.verify(); + res.body.should.deepEqual({ + responses: [{ + id: "1", + status: 200, + headers: { + 'content-type': 'application/json' + }, + body: { + value: result + } + }] + }); + }); + + it('should work with get entity', async function () { + const res = await request(host).post(`/$batch`).send({ + requests: [{ + id: "1", + method: "get", + url: `/book('${books[0].id}')` + }] + }); + assertSuccess(res); + res.body.should.deepEqual({ + responses: [{ + id: "1", + status: 200, + headers: { + 'content-type': 'application/json' + }, + body: books[0] + }] + }); + }); + + it('should work with post entity', async function () { + const result = { + title: "War and peace" + }; + const res = await request(host).post(`/$batch`).send({ + requests: [{ + id: "1", + method: "post", + url: `/book`, + body: result + }] + }); + assertSuccess(res); + result.id = (+books[books.length - 1].id + 1).toString(); + res.body.should.deepEqual({ + responses: [{ + id: "1", + status: 201, + headers: { + 'content-type': 'application/json' + }, + body: result + }] + }); + }); + + it('should work with put entity', async function () { + const result = { + id: "1", + title: "War and peace" + }; + const res = await request(host).post(`/$batch`).send({ + requests: [{ + id: "1", + method: "put", + url: `/book('1')`, + body: result + }] + }); + assertSuccess(res); + res.body.should.deepEqual({ + responses: [{ + id: "1", + status: 200, + headers: { + 'content-type': 'application/json' + }, + body: result + }] + }); + }); + + + it('should work with patch entity', async function () { + const result = { + id: "1", + title: "War and peace" + }; + const res = await request(host).post(`/$batch`).send({ + requests: [{ + id: "1", + method: "patch", + url: `/book('1')`, + body: result + }] + }); + assertSuccess(res); + res.body.should.deepEqual({ + responses: [{ + id: "1", + status: 200, + headers: { + 'content-type': 'application/json' + }, + body: result + }] + }); + }); + + + it('should work with delete entity', async function () { + const res = await request(host).post(`/$batch`).send({ + requests: [{ + id: "1", + method: "delete", + url: `/book('1')` + }] + }); + assertSuccess(res); + res.body.should.deepEqual({ + responses: [{ + id: "1", + status: 204 + }] + }); + }); +}); diff --git a/test/support/fake-db-model.js b/test/support/fake-db-model.js index 945fb9f..4b3c0de 100644 --- a/test/support/fake-db-model.js +++ b/test/support/fake-db-model.js @@ -96,6 +96,22 @@ export default class Model { return newItem; } + update(params, data, callback) { + const item = this._data.find(item => item.id === params.id); + + if (item) { + Object.keys(data).forEach(propName => { + item[propName] = data[propName]; + }); + callback(undefined, item); + } else { + const error = new Error('Not found'); + + error.status = 404; + callback(error); + } + } + exec(callback) { callback(null, this._data); } From 75d1a4cd47a957e00e72b902aaafd934fa28b0b6 Mon Sep 17 00:00:00 2001 From: Richard Martens Date: Sat, 25 Mar 2023 21:38:16 +0100 Subject: [PATCH 10/64] batch works --- .eslintrc | 1 - .vscode/launch.json | 2 +- src/express.js | 2 + src/index.js | 8 +- src/parser/mimetypeParser.js | 54 +++++++++ src/parser/multipartMixed.js | 53 +++++++++ src/pipes.js | 82 ++++++------- src/rest/index.js | 33 +++--- src/spezialResources/ODataBatch.js | 112 +++++++++--------- src/spezialResources/ODataMetadata.js | 2 +- src/writer/jsonWriter.js | 2 +- src/writer/multipartWriter.js | 28 +++++ src/writer/xmlWriter.js | 2 +- ...metadata.action..js => metadata.action.js} | 0 test/odata.batch.js | 54 ++++++++- 15 files changed, 309 insertions(+), 126 deletions(-) create mode 100644 src/parser/mimetypeParser.js create mode 100644 src/parser/multipartMixed.js create mode 100644 src/writer/multipartWriter.js rename test/{metadata.action..js => metadata.action.js} (100%) diff --git a/.eslintrc b/.eslintrc index a917ae1..01878e9 100644 --- a/.eslintrc +++ b/.eslintrc @@ -1,6 +1,5 @@ { "parser": "@babel/eslint-parser", - "extends": "airbnb/base", "env": { "browser": true, "node": true, diff --git a/.vscode/launch.json b/.vscode/launch.json index f5a2f0b..32f7e9a 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -20,7 +20,7 @@ "dot", "--timeout", "300000", - "test/odata.batch.js" + "test/model.complex.action.js" ] }, { diff --git a/src/express.js b/src/express.js index cafa642..153699b 100644 --- a/src/express.js +++ b/src/express.js @@ -2,6 +2,7 @@ import express from 'express'; import bodyParser from 'body-parser'; import methodOverride from 'method-override'; import cors from 'cors'; +import multipart from './parser/multipartMixed'; export default function orientExpress(options) { const app = express(); @@ -11,6 +12,7 @@ export default function orientExpress(options) { app.use(bodyParser.json(opts)); opts.extended = true; app.use(bodyParser.urlencoded(opts)); + app.use(multipart); app.use(methodOverride()); app.use(express.query()); app.use(cors(options && options.corsOptions)); diff --git a/src/index.js b/src/index.js index 7aa5b7f..9580799 100644 --- a/src/index.js +++ b/src/index.js @@ -1,10 +1,12 @@ import express from 'express'; import Server from './server'; +import mulpipartMixed from './parser/multipartMixed' -const server = function server(db, prefix, options) { +export const odata = function server(db, prefix, options) { return new Server(db, prefix, options); }; -server._express = express; +odata._express = express; + +export default odata; -export default server; diff --git a/src/parser/mimetypeParser.js b/src/parser/mimetypeParser.js new file mode 100644 index 0000000..86f0fab --- /dev/null +++ b/src/parser/mimetypeParser.js @@ -0,0 +1,54 @@ +export default class MimetypeParser { + constructor() { + this._regexes = { + 'application/xml': /((application|\*)\/(xml|\*)|^xml$)/, + 'application/json': /((application|\*)\/(json|\*)|^json$)/, + 'multipart/mixed': /multipart\/mixed/ + }; + } + + getmMediaType(format, accept, supportedFormats, requrestContentType) { + if (format) { + // get requested media type from $format query + return this._getMediaType(format, supportedFormats); + } else if (accept) { + // get requested media type from accept header + return this._getMediaType(accept, supportedFormats); + } else if (requrestContentType) { + return requrestContentType; + } + + return supportedFormats[0]; + } + + _getMediaType(header, supportedTypes) { + // reduce multi mimetypes to most weigth mimetype + // e.g. Accept: text/html, application/xhtml+xml, application/xml;q=0.9, image/webp, */*;q=0.8 + const mimeStructs = header.split(/[ ,]+/g); + const mostWeightMimetype = mimeStructs.reduce((previous, current) => { + const [mimetype, qualityParam] = current.split(/[ ;]+/); + const [, qualityValue] = qualityParam ? qualityParam.split(/=/) : ['q', 1]; + const result = { + ...previous, + }; + + const supported = supportedTypes.find(item => mimetype.match(this._regexes[item])); + + if (supported + && (!previous.mimetype || previous.qualityValue < qualityValue)) { + result.mimetype = supported; + result.qualityValue = qualityValue; + } + return result; + }, {}); + + if (mostWeightMimetype.mimetype) { + return mostWeightMimetype.mimetype; + } + + const error406 = new Error('Not acceptable'); + + error406.status = 406; + throw error406; + } +} \ No newline at end of file diff --git a/src/parser/multipartMixed.js b/src/parser/multipartMixed.js new file mode 100644 index 0000000..0029615 --- /dev/null +++ b/src/parser/multipartMixed.js @@ -0,0 +1,53 @@ +const bodyParser = require('body-parser'); + +function multipart(req, res, next) { + const header = req.headers['content-type']; + + if (!header || header.indexOf('multipart/mixed') === -1) { + next(); + return; + } + + const matchesBoundary = header.match(/boundary\s*=\s*([^;]*)/); + + req.body = { + requests: req.body.split(new RegExp(`\s*--${matchesBoundary[1]}[-]{0,2}\s*`)) + .filter(item => item.trim()) + .map(singleRequestText => { + const result = {}; + + if (singleRequestText.indexOf("Group ID: ") >= 0) { + return; //sap extension, not documentet in odata + } + const matchMethodUrl = singleRequestText.match(/^(GET|POST|PUT|PATCH|DELETE)\s+([\w-]+)\s*/m); + + if (!matchMethodUrl) { + throw new Error(`Method in ${singleRequestText} not supported`); + } + + result.method = matchMethodUrl[1].toLowerCase(); + result.url = matchMethodUrl[2]; + + const matchHeaders = singleRequestText.match(/^^([\w-]+)\s*:\s*([\w.-\/-]+)\s*$/gmi); + + result.headers = { + }; + matchHeaders.forEach((value) => { + const parts = value.split(':'); + + result.headers[parts[0].trim()] = parts[1].trim(); + }); + + const blocks = singleRequestText.split('\n\n'); + if (blocks.length > 2) { + result.body = JSON.parse(blocks[2]); + } + + return result; + }).filter(item => item) + }; + + next(); +} + +export default [bodyParser.text({type: 'multipart/mixed'}), multipart]; \ No newline at end of file diff --git a/src/pipes.js b/src/pipes.js index 2fe671c..64d2b2e 100644 --- a/src/pipes.js +++ b/src/pipes.js @@ -1,73 +1,60 @@ import http from 'http'; import XmlWriter from './writer/xmlWriter'; import JsonWirter from './writer/jsonWriter'; +import MultipartWriter from './writer/multipartWriter'; +import MimetypeParser from './parser/mimetypeParser' const xmlWriter = new XmlWriter(); const jsonWriter = new JsonWirter(); +const multipartWriter = new MultipartWriter(); -function getMediaType(accept, data) { - // reduce multi mimetypes to most weigth mimetype - // e.g. Accept: text/html, application/xhtml+xml, application/xml;q=0.9, image/webp, */*;q=0.8 - const mimeStructs = accept.split(/[ ,]+/g); - const mostWeightMimetype = mimeStructs.reduce((previous, current) => { - const [mimetype, qualityParam] = current.split(/[ ;]+/); - const [, qualityValue] = qualityParam ? qualityParam.split(/=/) : ['q', 1]; - const result = { - ...previous, - }; - - if (!previous.mimetype || previous.qualityValue < qualityValue) { - result.mimetype = mimetype; - result.qualityValue = qualityValue; - } - return result; - }, {}); +function getContentType(req) { + if (req.headers && req.headers['content-type']) { + const result = req.headers['content-type']; - if (data.metadata && mostWeightMimetype.mimetype.match(/((application|\*)\/(xml|\*)|^xml$)/)) { - return 'application/xml'; - } if (mostWeightMimetype.mimetype.match(/((application|\*)\/(json|\*)|^json$)/)) { - return 'application/json'; + return result.indexOf(';') > 0 ? result.split(';')[0] : result; } - - const error406 = new Error('Not acceptable'); - - error406.status = 406; - throw error406; } function getWriter(req, result) { - let mediaType; - - if (req.query.$format) { - // get requested media type from $format query - mediaType = getMediaType(req.query.$format, result); - } else if (req.headers && req.headers.accept) { - // get requested media type from accept header - mediaType = getMediaType(req.headers.accept, result); + let supportedFormats; + let format = req.query.$format; + + if (result.$metadata) { + supportedFormats = ['application/xml', 'application/json']; + } else if(result.responses) { + supportedFormats = ['multipart/mixed', 'application/json']; + format = ''; + } else { + supportedFormats = ['application/json']; + } + + let accept; + let requrestContentType; + if (req.headers) { + accept = req.headers.accept ? req.headers.accept : undefined; + requrestContentType = result.responses && getContentType(req) ? getContentType(req) : undefined; } + const mimetyeParser = new MimetypeParser(); + const mediaType = mimetyeParser.getmMediaType(format, accept, supportedFormats, requrestContentType); + // xml representation of metadata switch (mediaType) { case 'application/json': return jsonWriter.writeJson.bind(jsonWriter); case 'application/xml': - if (!result.metadata) { - // xml wirter for entities and actions is not implemented - const error406 = new Error('Not acceptable'); - - error406.status = 406; - throw error406; - } return xmlWriter.writeXml.bind(xmlWriter); + case 'multipart/mixed': + return multipartWriter.write.bind(multipartWriter); + default: - // no media type requested set defaults depend of context - if (result.entity || result.serviceDocument || result.responses) { - return jsonWriter.writeJson.bind(jsonWriter); // default for entities and actions - } + const error406 = new Error('Not acceptable'); - return xmlWriter.writeXml.bind(xmlWriter); // default for metadata + error406.status = 406; + throw error406; } } @@ -102,7 +89,8 @@ const respondPipe = (req, res, result) => new Promise((resolve, reject) => { const status = result.status || 200; const writer = getWriter(req, result); - writer(res, result, status, resolve); + res.setHeader('OData-Version',`4.0`); + writer(res, result, status, resolve, req.httpVersion); } catch (error) { reject(error); } diff --git a/src/rest/index.js b/src/rest/index.js index b1e8d4d..93f726f 100644 --- a/src/rest/index.js +++ b/src/rest/index.js @@ -9,9 +9,9 @@ import pipes from '../pipes'; const getRoutes = (url, hooks) => { const resourceListURL = `/${url}`; - const resourceListRegex = new RegExp(`(^\/${url}[?#])|(^\/${url}$)`); + const resourceListRegex = new RegExp(`(^\/?${url}[?#])|(^\/?${url}$)`); const resourceURL = `${resourceListURL}\\(:id\\)`; - const resourceRegex = new RegExp(`^\/${url}\\([^)]+\\)`); + const resourceRegex = new RegExp(`^\/?${url}\\([^)]+\\)`); return [ { @@ -67,21 +67,24 @@ const getMiddlewares = (url, hooks, mongooseModel, options) => { ctrl, hook, } = route; - return { - ...route, - middleware: async (req, res) => { - try { - await pipes.authorizePipe(req, res, hook.auth); - await pipes.beforePipe(req, res, hook.before); + const middleware = async (req, res) => { + try { + await pipes.authorizePipe(req, res, hook.auth); + await pipes.beforePipe(req, res, hook.before); - const result = await ctrl(req, mongooseModel, options); - const data = await pipes.respondPipe(req, res, result || {}); + const result = await ctrl(req, mongooseModel, options); + const data = await pipes.respondPipe(req, res, result || {}); - pipes.afterPipe(req, res, hook.after, data); - } catch (err) { - pipes.errorPipe(req, res, err); - } - }, + pipes.afterPipe(req, res, hook.after, data); + + } catch (err) { + pipes.errorPipe(req, res, err); + } + }; + + return { + ...route, + middleware }; }); }; diff --git a/src/spezialResources/ODataBatch.js b/src/spezialResources/ODataBatch.js index a60f8ed..45dd6b2 100644 --- a/src/spezialResources/ODataBatch.js +++ b/src/spezialResources/ODataBatch.js @@ -1,5 +1,7 @@ import { Router } from 'express'; import pipes from '../pipes'; +import MimetypeParser from '../parser/mimetypeParser'; +import { STATUS_CODES } from 'http'; export default class Batch { constructor(server) { @@ -71,7 +73,7 @@ export default class Batch { const keyValue = parameter.split('='); const result = { key: decodeURIComponent(keyValue[0]), - } + }; if (keyValue.length > 1) { result.value = decodeURIComponent(keyValue[1]); @@ -79,10 +81,10 @@ export default class Batch { return result; }); - return parameters.filter(parameter => parameter.value) + return parameters.filter((parameter) => parameter.value) .reduce((previous, current) => { const result = { - ...previous + ...previous, }; result[current.key] = current.value; @@ -92,61 +94,65 @@ export default class Batch { } async ctrl(req) { - return new Promise((resolve, reject) => { - const responses = req.body.requests.map(async (request) => { - const handler = Object.keys(this._server.resources) - .map((name) => this._server.resources[name].match(request.method, request.url)) - .find((ctrl) => ctrl); - const result = { - id: request.id - }; - const currentRequest = { - headers: request.headers, - query: Batch.mapToQuery(request.url), - body: request.body - }; - - const paramsMatch = request.url.match(/^\/[^#?(]+\('(\w+)'\)/); + const responses = req.body.requests.map(async function (request) { + const handler = Object.keys(this._server.resources) + .map((name) => this._server.resources[name].match(request.method, request.url)) + .find((ctrl) => ctrl); + let result = { + }; + if (request.id) { + result.id = request.id; + } + const currentRequest = { + headers: request.headers, + query: Batch.mapToQuery(request.url), + body: request.body, + }; - if (paramsMatch && paramsMatch.length > 1) { - currentRequest.params = { - id: paramsMatch[1] - } - } + const paramsMatch = request.url.match(/^\/[^#?(]+\('(\w+)'\)/); - if (!handler) { - result.status = 404; - result.body = 'Not Found'; - - } else { - await handler(currentRequest, { - type: (mimetype) => { - result.headers = { - 'content-type': mimetype, - }; - }, - status: (status) => { - result.status = status; - return { - jsonp: (body) => { - result.body = body; - }, - end: () => {} - }; - }, - }); + if (paramsMatch && paramsMatch.length > 1) { + currentRequest.params = { + id: paramsMatch[1], + }; + } + if (!handler) { + result.status = 404; + result.statusText = STATUS_CODES[404]; + result.body = STATUS_CODES[404]; + } else { + function appendHeader(name, value) { + if (!result.headers) { + result.headers = {}; + } + result.headers[name] = value; } - - - return result; - }); - - Promise.all(responses).then((results) => { - resolve({ - responses: results, + await handler(currentRequest, { + type: (mimetype) => { + appendHeader('content-type', mimetype); + }, + setHeader: appendHeader, + status: (status) => { + result.status = status; + result.statusText = STATUS_CODES[status]; + + return { + jsonp: (body) => { + result.body = body; + }, + end: () => { }, + }; + }, }); - }).catch(error => reject(error)); + } + return result; + }.bind(this)); + + return Promise.all(responses).then((results) => { + return { + responses: results + }; }); } } diff --git a/src/spezialResources/ODataMetadata.js b/src/spezialResources/ODataMetadata.js index 528e09d..d608805 100644 --- a/src/spezialResources/ODataMetadata.js +++ b/src/spezialResources/ODataMetadata.js @@ -271,7 +271,7 @@ export default class Metadata { return new Promise((resolve) => { resolve({ status: 200, - metadata: document, + $metadata: document, }); }); } diff --git a/src/writer/jsonWriter.js b/src/writer/jsonWriter.js index cc414ed..3873ae2 100644 --- a/src/writer/jsonWriter.js +++ b/src/writer/jsonWriter.js @@ -23,7 +23,7 @@ export default class { } else if (data.responses) { normalizedData = data; } else { - normalizedData = data.metadata || data.serviceDocument; + normalizedData = data.$metadata || data.serviceDocument; } res.type('application/json'); diff --git a/src/writer/multipartWriter.js b/src/writer/multipartWriter.js new file mode 100644 index 0000000..4763af0 --- /dev/null +++ b/src/writer/multipartWriter.js @@ -0,0 +1,28 @@ +export default class MultipartWriter { + write(res, result, status, resolve, httpVersion) { + const boundary = 'batch_1'; + let body = ''; + + result.responses.forEach(response => { + body += `--${boundary}\nContent-Type: application/http\n\nHTTP/${httpVersion} ${response.status} ${response.statusText}\n`; + if (response.headers) { + const headers = Object.keys(response.headers); + + headers.forEach(header => { + body += `${header}: ${response.headers[header]}\n` + }); + } + body += '\n'; + + const textBody = typeof response.body === 'string' ? response.body : JSON.stringify(response.body); + + body += `${textBody}\n`; + }); + + body += `--${boundary}--`; + + res.setHeader('content-type',`multipart/mixed;boundary=${boundary}`); + res.send(Buffer.from(body)).status(status); + resolve(); + } +} \ No newline at end of file diff --git a/src/writer/xmlWriter.js b/src/writer/xmlWriter.js index d842f37..813060e 100644 --- a/src/writer/xmlWriter.js +++ b/src/writer/xmlWriter.js @@ -180,7 +180,7 @@ export default class XmlWriter { } writeXml(res, data, status, resolve) { - const xml = this.visitor('document', data.metadata, '', '').replace(/\s*\s*/g, '>'); + const xml = this.visitor('document', data.$metadata, '', '').replace(/\s*\s*/g, '>'); res.type('application/xml'); res.status(status).send(xml); diff --git a/test/metadata.action..js b/test/metadata.action.js similarity index 100% rename from test/metadata.action..js rename to test/metadata.action.js diff --git a/test/odata.batch.js b/test/odata.batch.js index eaf71d8..234b150 100644 --- a/test/odata.batch.js +++ b/test/odata.batch.js @@ -54,7 +54,9 @@ describe('odata.batch', () => { responses: [{ id: "1", status: 200, + statusText: 'OK', headers: { + 'OData-Version': '4.0', 'content-type': 'application/json' }, body: { @@ -77,7 +79,9 @@ describe('odata.batch', () => { responses: [{ id: "1", status: 200, + statusText: 'OK', headers: { + 'OData-Version': '4.0', 'content-type': 'application/json' }, body: books[0] @@ -103,7 +107,9 @@ describe('odata.batch', () => { responses: [{ id: "1", status: 201, + statusText: 'Created', headers: { + 'OData-Version': '4.0', 'content-type': 'application/json' }, body: result @@ -129,7 +135,9 @@ describe('odata.batch', () => { responses: [{ id: "1", status: 200, + statusText: "OK", headers: { + 'OData-Version': '4.0', 'content-type': 'application/json' }, body: result @@ -156,8 +164,10 @@ describe('odata.batch', () => { responses: [{ id: "1", status: 200, + statusText: "OK", headers: { - 'content-type': 'application/json' + "OData-Version": "4.0", + "content-type": "application/json" }, body: result }] @@ -177,8 +187,46 @@ describe('odata.batch', () => { res.body.should.deepEqual({ responses: [{ id: "1", - status: 204 + status: 204, + statusText: "No Content" }] }); - }); + });/* + + it('should work with multipart request body', async function () { + const result = { + title: "War and peace" + }; + const res = await request(host) + .post(`/$batch`) + .send({}) + .set('Content-Type', 'multipart/mixed; boundary=batch_1') + .set('Host', host) + .serialize(() => ` +--batch_1 +Content-Type: application/http + +POST /book +Host: ${host} +Content-Type: application/json +Content-Lenght: ${JSON.stringify(result).length} + +${JSON.stringify(result)} +--batch_1-- + `); + + assertSuccess(res); + + res.body.should.equal(` +--batch-1 +Content-Type: application/http + +HTTP/1.1 200 Ok +Content-Type: application/json +Content-Length: ${JSON.stringify(result).length} + +${JSON.stringify(result)} +--batch-1— + `); + });*/ }); From 44af1af6c22534bb19e00500b2c845f6ba5a66ca Mon Sep 17 00:00:00 2001 From: Richard Martens Date: Fri, 31 Mar 2023 22:18:07 +0200 Subject: [PATCH 11/64] Post give an error for missing _id and batch test not working --- .babelrc | 5 ++++- .vscode/launch.json | 4 ++-- src/ODataResource.js | 10 ++++++++-- src/parser/multipartMixed.js | 2 +- src/pipes.js | 22 ++++++++++++++------ src/rest/count.js | 19 ++++++++++++++++++ src/rest/index.js | 10 +++++++++- src/spezialResources/ODataBatch.js | 3 +++ src/writer/multipartWriter.js | 8 ++++---- test/odata.batch.js | 17 ++++++++++++---- test/odata.count.js | 32 ++++++++++++++++++++++++++++++ test/odata.query.count.js | 1 + 12 files changed, 112 insertions(+), 21 deletions(-) create mode 100644 src/rest/count.js create mode 100644 test/odata.count.js diff --git a/.babelrc b/.babelrc index a539190..1deaf9e 100644 --- a/.babelrc +++ b/.babelrc @@ -21,5 +21,8 @@ "@babel/plugin-proposal-nullish-coalescing-operator", "@babel/plugin-proposal-do-expressions", "@babel/plugin-proposal-function-bind" - ] + ], + "targets": { + "chrome": "59" + } } \ No newline at end of file diff --git a/.vscode/launch.json b/.vscode/launch.json index 32f7e9a..2ad9ed3 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -20,7 +20,7 @@ "dot", "--timeout", "300000", - "test/model.complex.action.js" + "test/odata.batch.js" ] }, { @@ -30,7 +30,7 @@ "skipFiles": [ "/**" ], - "program": "${workspaceFolder}/examples/complex-resource/index.js" + "program": "${workspaceFolder}/examples/simple/index.js" } ] } \ No newline at end of file diff --git a/src/ODataResource.js b/src/ODataResource.js index aa83110..51e2828 100644 --- a/src/ODataResource.js +++ b/src/ODataResource.js @@ -36,6 +36,7 @@ export default class { put: {}, delete: {}, patch: {}, + count: {} }; this.actions = {}; this._options = { @@ -176,8 +177,13 @@ export default class { maxSkip: min([setting.maxSkip, this._maxSkip]), orderby: this._orderby || setting.orderby, }); - const route = routes.find((item) => item.method === method - && url.match(item.regex)); + const route = routes.find((item) => { + if (item.method === method) { + const match = url.match(item.regex); + + return match; + } + }); return route ? route.middleware : undefined; } diff --git a/src/parser/multipartMixed.js b/src/parser/multipartMixed.js index 0029615..1e5c6ee 100644 --- a/src/parser/multipartMixed.js +++ b/src/parser/multipartMixed.js @@ -19,7 +19,7 @@ function multipart(req, res, next) { if (singleRequestText.indexOf("Group ID: ") >= 0) { return; //sap extension, not documentet in odata } - const matchMethodUrl = singleRequestText.match(/^(GET|POST|PUT|PATCH|DELETE)\s+([\w-]+)\s*/m); + const matchMethodUrl = singleRequestText.match(/^(GET|POST|PUT|PATCH|DELETE)\s+([\w\/$-]+)\s*/m); if (!matchMethodUrl) { throw new Error(`Method in ${singleRequestText} not supported`); diff --git a/src/pipes.js b/src/pipes.js index 64d2b2e..7738dfb 100644 --- a/src/pipes.js +++ b/src/pipes.js @@ -16,13 +16,15 @@ function getContentType(req) { } } -function getWriter(req, result) { +function getWriter(req, res, result) { let supportedFormats; let format = req.query.$format; - - if (result.$metadata) { + + if (typeof result !== 'object') { + supportedFormats = ['text/plain']; + } else if (result.$metadata) { supportedFormats = ['application/xml', 'application/json']; - } else if(result.responses) { + } else if (result.responses) { supportedFormats = ['multipart/mixed', 'application/json']; format = ''; } else { @@ -39,6 +41,8 @@ function getWriter(req, result) { const mimetyeParser = new MimetypeParser(); const mediaType = mimetyeParser.getmMediaType(format, accept, supportedFormats, requrestContentType); + res.type(mediaType); + // xml representation of metadata switch (mediaType) { case 'application/json': @@ -50,6 +54,12 @@ function getWriter(req, result) { case 'multipart/mixed': return multipartWriter.write.bind(multipartWriter); + case 'text/plain': + return (res, data, status, resolve) => { + res.status(status).send(data); + resolve(data); + }; + default: const error406 = new Error('Not acceptable'); @@ -87,9 +97,9 @@ const respondPipe = (req, res, result) => new Promise((resolve, reject) => { } const status = result.status || 200; - const writer = getWriter(req, result); + const writer = getWriter(req, res, result); - res.setHeader('OData-Version',`4.0`); + res.setHeader('OData-Version', `4.0`); writer(res, result, status, resolve, req.httpVersion); } catch (error) { reject(error); diff --git a/src/rest/count.js b/src/rest/count.js new file mode 100644 index 0000000..b149bc9 --- /dev/null +++ b/src/rest/count.js @@ -0,0 +1,19 @@ +export default async (req, MongooseModel, options) => { + return new Promise((resolve, reject) => { + const query = MongooseModel.find(); + + query.count((err, count) => { + if (err) { + const result = new Error(err.message); + + result.previous = err; + result.status = 500; + reject(result); + + } else { + resolve(count.toString()); + + } + }); + }); +}; diff --git a/src/rest/index.js b/src/rest/index.js index 93f726f..c305ccc 100644 --- a/src/rest/index.js +++ b/src/rest/index.js @@ -6,6 +6,7 @@ import del from './delete'; import patch from './patch'; import get from './get'; import pipes from '../pipes'; +import count from './count'; const getRoutes = (url, hooks) => { const resourceListURL = `/${url}`; @@ -49,13 +50,20 @@ const getRoutes = (url, hooks) => { ctrl: get, hook: hooks.get, }, + { + method: 'get', + url: resourceListURL + '/([\$])count', + regex: new RegExp(`(^\/?${url}\/\\$count[?]?)|(^\/?${url}\/\\$count$)`), + ctrl: count, + hook: hooks.count, + }, { method: 'get', url: resourceListURL, regex: resourceListRegex, ctrl: list, hook: hooks.list, - }, + } ]; }; diff --git a/src/spezialResources/ODataBatch.js b/src/spezialResources/ODataBatch.js index 45dd6b2..5ed8369 100644 --- a/src/spezialResources/ODataBatch.js +++ b/src/spezialResources/ODataBatch.js @@ -142,6 +142,9 @@ export default class Batch { result.body = body; }, end: () => { }, + send: (body) => { + result.body = body; + } }; }, }); diff --git a/src/writer/multipartWriter.js b/src/writer/multipartWriter.js index 4763af0..8f5113e 100644 --- a/src/writer/multipartWriter.js +++ b/src/writer/multipartWriter.js @@ -4,19 +4,19 @@ export default class MultipartWriter { let body = ''; result.responses.forEach(response => { - body += `--${boundary}\nContent-Type: application/http\n\nHTTP/${httpVersion} ${response.status} ${response.statusText}\n`; + body += `--${boundary}\r\nContent-Type: application/http\r\n\r\nHTTP/${httpVersion} ${response.status} ${response.statusText}\r\n`; if (response.headers) { const headers = Object.keys(response.headers); headers.forEach(header => { - body += `${header}: ${response.headers[header]}\n` + body += `${header}: ${response.headers[header]}\r\n` }); } - body += '\n'; + body += '\r\n'; const textBody = typeof response.body === 'string' ? response.body : JSON.stringify(response.body); - body += `${textBody}\n`; + body += `${textBody}\r\n`; }); body += `--${boundary}--`; diff --git a/test/odata.batch.js b/test/odata.batch.js index 234b150..eb0188e 100644 --- a/test/odata.batch.js +++ b/test/odata.batch.js @@ -22,6 +22,7 @@ describe('odata.batch', () => { sandbox.restore(); }); + /* it('should work with get lists', async function () { const result = [ { @@ -191,7 +192,7 @@ describe('odata.batch', () => { statusText: "No Content" }] }); - });/* + });*/ it('should work with multipart request body', async function () { const result = { @@ -209,7 +210,7 @@ Content-Type: application/http POST /book Host: ${host} Content-Type: application/json -Content-Lenght: ${JSON.stringify(result).length} +Content-Length: ${JSON.stringify(result).length} ${JSON.stringify(result)} --batch_1-- @@ -217,7 +218,15 @@ ${JSON.stringify(result)} assertSuccess(res); - res.body.should.equal(` + const single = await new Promise((resolve, reject) => { + res.files.null.on('error', part => { + reject(part); + }); + res.files.null.on('data', part => { + resolve(part); + }); + }); + res.text.should.equal(` --batch-1 Content-Type: application/http @@ -228,5 +237,5 @@ Content-Length: ${JSON.stringify(result).length} ${JSON.stringify(result)} --batch-1— `); - });*/ + }); }); diff --git a/test/odata.count.js b/test/odata.count.js new file mode 100644 index 0000000..383ddf7 --- /dev/null +++ b/test/odata.count.js @@ -0,0 +1,32 @@ +import 'should'; +import request from 'supertest'; +import { odata, host, port, bookSchema } from './support/setup'; +import books from './support/books.json'; +import FakeDb from './support/fake-db'; + +describe('odata.query.count', function() { + let httpServer; + + before(async function() { + const db = new FakeDb(); + const server = odata(db); + server.resource('book', bookSchema); + db.addData('book', books); + httpServer = server.listen(port); + }); + + after(() => { + httpServer.close(); + }); + + it('should get count for entity set', async function() { + const res = await request(host).get('/book/$count'); + + if (res.error) { + res.error.status.should.be.equal(200); + } + res.text.should.be.equal('13'); + res.header.should.have.property('content-type'); + res.header['content-type'].should.containEql('text/plain'); + }); +}); diff --git a/test/odata.query.count.js b/test/odata.query.count.js index 164451e..b404c45 100644 --- a/test/odata.query.count.js +++ b/test/odata.query.count.js @@ -33,4 +33,5 @@ describe('odata.query.count', function() { const res = await request(host).get('/book?$count=1'); res.error.status.should.be.equal(500); }); + }); From 9ef5a7026ae019419466e03a16c3cab0cbd19374 Mon Sep 17 00:00:00 2001 From: Richard Martens Date: Sat, 1 Apr 2023 14:41:23 +0200 Subject: [PATCH 12/64] moved mocked tests to own folder --- Makefile | 2 +- test/{ => mocked}/api.Function.js | 4 ++-- test/{ => mocked}/api.Resource.js | 4 ++-- test/{ => mocked}/hook.all.after.js | 6 +++--- test/{ => mocked}/hook.all.before.js | 6 +++--- test/{ => mocked}/hook.delete.after.js | 6 +++--- test/{ => mocked}/hook.delete.before.js | 6 +++--- test/{ => mocked}/hook.get.after.js | 6 +++--- test/{ => mocked}/hook.get.before.js | 6 +++--- test/{ => mocked}/hook.list.after.js | 6 +++--- test/{ => mocked}/hook.list.before.js | 6 +++--- test/{ => mocked}/hook.post.after.js | 6 +++--- test/{ => mocked}/hook.post.before.js | 6 +++--- test/{ => mocked}/hook.put.after.js | 6 +++--- test/{ => mocked}/hook.put.before.js | 6 +++--- test/{ => mocked}/metadata.action.js | 4 ++-- test/{ => mocked}/metadata.format.js | 4 ++-- test/{ => mocked}/metadata.function.js | 4 ++-- test/{ => mocked}/metadata.js | 4 ++-- test/{ => mocked}/metadata.resource.complex.js | 4 ++-- test/{ => mocked}/mimetype.defaults.js | 4 ++-- test/{ => mocked}/model.complex.action.js | 4 ++-- test/{ => mocked}/model.complex.filter.js | 4 ++-- test/{ => mocked}/model.complex.js | 4 ++-- test/{ => mocked}/model.custom.id.js | 4 ++-- test/{ => mocked}/model.hidden.field.js | 4 ++-- test/{ => mocked}/model.special.name.js | 4 ++-- test/{ => mocked}/odata.actions.js | 6 +++--- test/{ => mocked}/odata.batch.js | 11 +++++------ test/{ => mocked}/odata.count.js | 6 +++--- test/{ => mocked}/odata.functions.js | 4 ++-- test/{ => mocked}/odata.query.count.js | 6 +++--- test/{ => mocked}/odata.query.filter.functions.js | 6 +++--- test/{ => mocked}/odata.query.filter.js | 6 +++--- test/{ => mocked}/odata.query.orderby.js | 6 +++--- test/{ => mocked}/odata.query.select.js | 6 +++--- test/{ => mocked}/odata.query.skip.js | 4 ++-- test/{ => mocked}/odata.query.top.js | 4 ++-- test/{ => mocked}/options.maxSkip.js | 4 ++-- test/{ => mocked}/options.maxTop.js | 4 ++-- test/{ => mocked}/options.prefix.js | 4 ++-- test/{ => mocked}/rest.delete.js | 6 +++--- test/{ => mocked}/rest.get.js | 6 +++--- test/{ => mocked}/rest.post.js | 4 ++-- test/{ => mocked}/rest.put.js | 6 +++--- test/{ => mocked}/service.document.js | 4 ++-- test/{ => mocked}/utils.js | 2 +- 47 files changed, 117 insertions(+), 118 deletions(-) rename test/{ => mocked}/api.Function.js (85%) rename test/{ => mocked}/api.Resource.js (81%) rename test/{ => mocked}/hook.all.after.js (86%) rename test/{ => mocked}/hook.all.before.js (87%) rename test/{ => mocked}/hook.delete.after.js (86%) rename test/{ => mocked}/hook.delete.before.js (87%) rename test/{ => mocked}/hook.get.after.js (87%) rename test/{ => mocked}/hook.get.before.js (87%) rename test/{ => mocked}/hook.list.after.js (87%) rename test/{ => mocked}/hook.list.before.js (86%) rename test/{ => mocked}/hook.post.after.js (88%) rename test/{ => mocked}/hook.post.before.js (88%) rename test/{ => mocked}/hook.put.after.js (87%) rename test/{ => mocked}/hook.put.before.js (88%) rename test/{ => mocked}/metadata.action.js (97%) rename test/{ => mocked}/metadata.format.js (98%) rename test/{ => mocked}/metadata.function.js (95%) rename test/{ => mocked}/metadata.js (98%) rename test/{ => mocked}/metadata.resource.complex.js (98%) rename test/{ => mocked}/mimetype.defaults.js (88%) rename test/{ => mocked}/model.complex.action.js (92%) rename test/{ => mocked}/model.complex.filter.js (89%) rename test/{ => mocked}/model.complex.js (91%) rename test/{ => mocked}/model.custom.id.js (89%) rename test/{ => mocked}/model.hidden.field.js (92%) rename test/{ => mocked}/model.special.name.js (85%) rename test/{ => mocked}/odata.actions.js (86%) rename test/{ => mocked}/odata.batch.js (96%) rename test/{ => mocked}/odata.count.js (82%) rename test/{ => mocked}/odata.functions.js (86%) rename test/{ => mocked}/odata.query.count.js (87%) rename test/{ => mocked}/odata.query.filter.functions.js (93%) rename test/{ => mocked}/odata.query.filter.js (97%) rename test/{ => mocked}/odata.query.orderby.js (92%) rename test/{ => mocked}/odata.query.select.js (93%) rename test/{ => mocked}/odata.query.skip.js (92%) rename test/{ => mocked}/odata.query.top.js (91%) rename test/{ => mocked}/options.maxSkip.js (94%) rename test/{ => mocked}/options.maxTop.js (94%) rename test/{ => mocked}/options.prefix.js (90%) rename test/{ => mocked}/rest.delete.js (87%) rename test/{ => mocked}/rest.get.js (91%) rename test/{ => mocked}/rest.post.js (87%) rename test/{ => mocked}/rest.put.js (90%) rename test/{ => mocked}/service.document.js (92%) rename test/{ => mocked}/utils.js (96%) diff --git a/Makefile b/Makefile index ce31d71..988ea3b 100644 --- a/Makefile +++ b/Makefile @@ -14,7 +14,7 @@ test: @node_modules/.bin/mocha\ --require @babel/register \ --reporter $(REPORTER) \ - test/*.js + test/**/*.js test-cov: @node node_modules/istanbul/lib/cli.js cover -x '**/examples/**' \ diff --git a/test/api.Function.js b/test/mocked/api.Function.js similarity index 85% rename from test/api.Function.js rename to test/mocked/api.Function.js index 376bf37..9ad6fbe 100644 --- a/test/api.Function.js +++ b/test/mocked/api.Function.js @@ -1,7 +1,7 @@ import 'should'; import request from 'supertest'; -import { odata, host, port } from './support/setup'; -import FakeDb from './support/fake-db'; +import { odata, host, port } from '../support/setup'; +import FakeDb from '../support/fake-db'; describe('odata.api.Function', () => { let httpServer; diff --git a/test/api.Resource.js b/test/mocked/api.Resource.js similarity index 81% rename from test/api.Resource.js rename to test/mocked/api.Resource.js index 4c583c3..f36d80d 100644 --- a/test/api.Resource.js +++ b/test/mocked/api.Resource.js @@ -1,7 +1,7 @@ import 'should'; import request from 'supertest'; -import { odata, host, port, bookSchema } from './support/setup'; -import FakeDb from './support/fake-db'; +import { odata, host, port, bookSchema } from '../support/setup'; +import FakeDb from '../support/fake-db'; describe('odata.api.Resouce', () => { let httpServer; diff --git a/test/hook.all.after.js b/test/mocked/hook.all.after.js similarity index 86% rename from test/hook.all.after.js rename to test/mocked/hook.all.after.js index f16e5f0..db56828 100644 --- a/test/hook.all.after.js +++ b/test/mocked/hook.all.after.js @@ -2,9 +2,9 @@ import 'should'; import 'should-sinon'; import request from 'supertest'; import sinon from 'sinon'; -import { odata, host, port, bookSchema, assertSuccess } from './support/setup'; -import FakeDb from './support/fake-db'; -import books from './support/books.json'; +import { odata, host, port, bookSchema, assertSuccess } from '../support/setup'; +import FakeDb from '../support/fake-db'; +import books from '../support/books.json'; describe('hook.all.after', function() { let data, httpServer, server, db; diff --git a/test/hook.all.before.js b/test/mocked/hook.all.before.js similarity index 87% rename from test/hook.all.before.js rename to test/mocked/hook.all.before.js index 6d68fce..922169a 100644 --- a/test/hook.all.before.js +++ b/test/mocked/hook.all.before.js @@ -2,9 +2,9 @@ import 'should'; import 'should-sinon'; import request from 'supertest'; import sinon from 'sinon'; -import { odata, host, port, bookSchema } from './support/setup'; -import FakeDb from './support/fake-db'; -import books from './support/books.json'; +import { odata, host, port, bookSchema } from '../support/setup'; +import FakeDb from '../support/fake-db'; +import books from '../support/books.json'; describe('hook.all.before', function() { let data, httpServer, server, db; diff --git a/test/hook.delete.after.js b/test/mocked/hook.delete.after.js similarity index 86% rename from test/hook.delete.after.js rename to test/mocked/hook.delete.after.js index e3dc0ae..2ccbbfb 100755 --- a/test/hook.delete.after.js +++ b/test/mocked/hook.delete.after.js @@ -2,9 +2,9 @@ import 'should'; import 'should-sinon'; import request from 'supertest'; import sinon from 'sinon'; -import { odata, host, port, bookSchema, assertSuccess } from './support/setup'; -import FakeDb from './support/fake-db'; -import books from './support/books.json'; +import { odata, host, port, bookSchema, assertSuccess } from '../support/setup'; +import FakeDb from '../support/fake-db'; +import books from '../support/books.json'; describe('hook.delete.after', function() { let data, httpServer, server, db; diff --git a/test/hook.delete.before.js b/test/mocked/hook.delete.before.js similarity index 87% rename from test/hook.delete.before.js rename to test/mocked/hook.delete.before.js index 398ca41..f3dfcbe 100644 --- a/test/hook.delete.before.js +++ b/test/mocked/hook.delete.before.js @@ -2,9 +2,9 @@ import 'should'; import 'should-sinon'; import request from 'supertest'; import sinon from 'sinon'; -import { odata, host, port, bookSchema } from './support/setup'; -import FakeDb from './support/fake-db'; -import books from './support/books.json'; +import { odata, host, port, bookSchema } from '../support/setup'; +import FakeDb from '../support/fake-db'; +import books from '../support/books.json'; describe('hook.delete.before', function() { let data, httpServer, server, db; diff --git a/test/hook.get.after.js b/test/mocked/hook.get.after.js similarity index 87% rename from test/hook.get.after.js rename to test/mocked/hook.get.after.js index 44729ab..d0654f2 100755 --- a/test/hook.get.after.js +++ b/test/mocked/hook.get.after.js @@ -2,9 +2,9 @@ import 'should'; import 'should-sinon'; import request from 'supertest'; import sinon from 'sinon'; -import { host, port, bookSchema, odata } from './support/setup'; -import FakeDb from './support/fake-db'; -import books from './support/books.json'; +import { host, port, bookSchema, odata } from '../support/setup'; +import FakeDb from '../support/fake-db'; +import books from '../support/books.json'; describe('hook.get.after', function() { let data, httpServer, server, db; diff --git a/test/hook.get.before.js b/test/mocked/hook.get.before.js similarity index 87% rename from test/hook.get.before.js rename to test/mocked/hook.get.before.js index da74993..a5e3fca 100644 --- a/test/hook.get.before.js +++ b/test/mocked/hook.get.before.js @@ -2,9 +2,9 @@ import 'should'; import 'should-sinon'; import request from 'supertest'; import sinon from 'sinon'; -import { host, port, bookSchema, odata } from './support/setup'; -import FakeDb from './support/fake-db'; -import books from './support/books.json'; +import { host, port, bookSchema, odata } from '../support/setup'; +import FakeDb from '../support/fake-db'; +import books from '../support/books.json'; describe('hook.get.before', function() { let data, httpServer, server, db; diff --git a/test/hook.list.after.js b/test/mocked/hook.list.after.js similarity index 87% rename from test/hook.list.after.js rename to test/mocked/hook.list.after.js index c8fa335..7b30564 100644 --- a/test/hook.list.after.js +++ b/test/mocked/hook.list.after.js @@ -2,9 +2,9 @@ import 'should'; import 'should-sinon'; import request from 'supertest'; import sinon from 'sinon'; -import { host, port, bookSchema, odata } from './support/setup'; -import FakeDb from './support/fake-db'; -import books from './support/books.json'; +import { host, port, bookSchema, odata } from '../support/setup'; +import FakeDb from '../support/fake-db'; +import books from '../support/books.json'; describe('hook.list.after', function() { let data, httpServer, server, db; diff --git a/test/hook.list.before.js b/test/mocked/hook.list.before.js similarity index 86% rename from test/hook.list.before.js rename to test/mocked/hook.list.before.js index 3b03423..f3428d7 100644 --- a/test/hook.list.before.js +++ b/test/mocked/hook.list.before.js @@ -2,9 +2,9 @@ import 'should'; import 'should-sinon'; import request from 'supertest'; import sinon from 'sinon'; -import { host, port, bookSchema, odata } from './support/setup'; -import FakeDb from './support/fake-db'; -import books from './support/books.json'; +import { host, port, bookSchema, odata } from '../support/setup'; +import FakeDb from '../support/fake-db'; +import books from '../support/books.json'; describe('hook.list.before', function() { let data, httpServer, server, db; diff --git a/test/hook.post.after.js b/test/mocked/hook.post.after.js similarity index 88% rename from test/hook.post.after.js rename to test/mocked/hook.post.after.js index 5cfce20..264d0ee 100755 --- a/test/hook.post.after.js +++ b/test/mocked/hook.post.after.js @@ -2,9 +2,9 @@ import 'should'; import 'should-sinon'; import request from 'supertest'; import sinon from 'sinon'; -import { odata, host, port, bookSchema } from './support/setup'; -import FakeDb from './support/fake-db'; -import books from './support/books.json'; +import { odata, host, port, bookSchema } from '../support/setup'; +import FakeDb from '../support/fake-db'; +import books from '../support/books.json'; describe('hook.post.after', function() { let data, httpServer, server, db; diff --git a/test/hook.post.before.js b/test/mocked/hook.post.before.js similarity index 88% rename from test/hook.post.before.js rename to test/mocked/hook.post.before.js index 4b66594..8d45eb6 100644 --- a/test/hook.post.before.js +++ b/test/mocked/hook.post.before.js @@ -2,9 +2,9 @@ import 'should'; import 'should-sinon'; import request from 'supertest'; import sinon from 'sinon'; -import { host, port, bookSchema, odata } from './support/setup'; -import FakeDb from './support/fake-db'; -import books from './support/books.json'; +import { host, port, bookSchema, odata } from '../support/setup'; +import FakeDb from '../support/fake-db'; +import books from '../support/books.json'; describe('hook.post.before', function() { let data, httpServer, server, db; diff --git a/test/hook.put.after.js b/test/mocked/hook.put.after.js similarity index 87% rename from test/hook.put.after.js rename to test/mocked/hook.put.after.js index 617bfdf..ae845bd 100755 --- a/test/hook.put.after.js +++ b/test/mocked/hook.put.after.js @@ -2,9 +2,9 @@ import 'should'; import 'should-sinon'; import request from 'supertest'; import sinon from 'sinon'; -import { host, port, bookSchema, odata } from './support/setup'; -import FakeDb from './support/fake-db'; -import books from './support/books.json'; +import { host, port, bookSchema, odata } from '../support/setup'; +import FakeDb from '../support/fake-db'; +import books from '../support/books.json'; describe('hook.put.after', function() { let data, httpServer, server, db; diff --git a/test/hook.put.before.js b/test/mocked/hook.put.before.js similarity index 88% rename from test/hook.put.before.js rename to test/mocked/hook.put.before.js index 5936888..42e419d 100644 --- a/test/hook.put.before.js +++ b/test/mocked/hook.put.before.js @@ -2,9 +2,9 @@ import 'should'; import 'should-sinon'; import request from 'supertest'; import sinon from 'sinon'; -import { host, port, bookSchema, odata } from './support/setup'; -import FakeDb from './support/fake-db'; -import books from './support/books.json'; +import { host, port, bookSchema, odata } from '../support/setup'; +import FakeDb from '../support/fake-db'; +import books from '../support/books.json'; describe('hook.put.before', function() { let data, httpServer, server, db; diff --git a/test/metadata.action.js b/test/mocked/metadata.action.js similarity index 97% rename from test/metadata.action.js rename to test/mocked/metadata.action.js index 4da1a2b..39c54b0 100644 --- a/test/metadata.action.js +++ b/test/mocked/metadata.action.js @@ -3,8 +3,8 @@ import 'should'; import request from 'supertest'; -import { host, conn, port, odata, assertSuccess } from './support/setup'; -import FakeDb from './support/fake-db'; +import { host, conn, port, odata, assertSuccess } from '../support/setup'; +import FakeDb from '../support/fake-db'; describe('metadata.action', () => { let httpServer, server, db; diff --git a/test/metadata.format.js b/test/mocked/metadata.format.js similarity index 98% rename from test/metadata.format.js rename to test/mocked/metadata.format.js index b256d6d..f54e324 100644 --- a/test/metadata.format.js +++ b/test/mocked/metadata.format.js @@ -3,8 +3,8 @@ import 'should'; import request from 'supertest'; -import { host, conn, port, bookSchema, odata, assertSuccess } from './support/setup'; -import FakeDb from './support/fake-db'; +import { host, conn, port, bookSchema, odata, assertSuccess } from '../support/setup'; +import FakeDb from '../support/fake-db'; describe('metadata.format', () => { let httpServer, server, db; diff --git a/test/metadata.function.js b/test/mocked/metadata.function.js similarity index 95% rename from test/metadata.function.js rename to test/mocked/metadata.function.js index bc93f26..0680917 100644 --- a/test/metadata.function.js +++ b/test/mocked/metadata.function.js @@ -3,8 +3,8 @@ import 'should'; import request from 'supertest'; -import { host, conn, port, odata, assertSuccess } from './support/setup'; -import FakeDb from './support/fake-db'; +import { host, conn, port, odata, assertSuccess } from '../support/setup'; +import FakeDb from '../support/fake-db'; describe('metadata.function', () => { let httpServer, server, db; diff --git a/test/metadata.js b/test/mocked/metadata.js similarity index 98% rename from test/metadata.js rename to test/mocked/metadata.js index a327767..04031c3 100644 --- a/test/metadata.js +++ b/test/mocked/metadata.js @@ -3,8 +3,8 @@ import 'should'; import request from 'supertest'; -import { host, conn, port, odata, assertSuccess } from './support/setup'; -import FakeDb from './support/fake-db'; +import { host, conn, port, odata, assertSuccess } from '../support/setup'; +import FakeDb from '../support/fake-db'; describe('metadata', () => { let httpServer, server, db; diff --git a/test/metadata.resource.complex.js b/test/mocked/metadata.resource.complex.js similarity index 98% rename from test/metadata.resource.complex.js rename to test/mocked/metadata.resource.complex.js index 72b4137..42de3e2 100644 --- a/test/metadata.resource.complex.js +++ b/test/mocked/metadata.resource.complex.js @@ -3,8 +3,8 @@ import 'should'; import request from 'supertest'; -import { host, conn, port, odata, assertSuccess } from './support/setup'; -import FakeDb from './support/fake-db'; +import { host, conn, port, odata, assertSuccess } from '../support/setup'; +import FakeDb from '../support/fake-db'; describe('metadata.resource.complex', () => { let httpServer, server, db; diff --git a/test/mimetype.defaults.js b/test/mocked/mimetype.defaults.js similarity index 88% rename from test/mimetype.defaults.js rename to test/mocked/mimetype.defaults.js index 184a6c1..09b5991 100644 --- a/test/mimetype.defaults.js +++ b/test/mocked/mimetype.defaults.js @@ -1,7 +1,7 @@ import 'should'; import request from 'supertest'; -import { host, port, bookSchema, odata, assertSuccess } from './support/setup'; -import FakeDb from './support/fake-db'; +import { host, port, bookSchema, odata, assertSuccess } from '../support/setup'; +import FakeDb from '../support/fake-db'; describe('mimetype.defaults', () => { let httpServer, server, db; diff --git a/test/model.complex.action.js b/test/mocked/model.complex.action.js similarity index 92% rename from test/model.complex.action.js rename to test/mocked/model.complex.action.js index c691519..0acea6a 100644 --- a/test/model.complex.action.js +++ b/test/mocked/model.complex.action.js @@ -2,8 +2,8 @@ import 'should'; import request from 'supertest'; -import { odata, host, port, assertSuccess } from './support/setup'; -import FakeDb from './support/fake-db'; +import { odata, host, port, assertSuccess } from '../support/setup'; +import FakeDb from '../support/fake-db'; describe('model.complex.action', () => { let httpServer; diff --git a/test/model.complex.filter.js b/test/mocked/model.complex.filter.js similarity index 89% rename from test/model.complex.filter.js rename to test/mocked/model.complex.filter.js index afb6a7b..2a1d634 100644 --- a/test/model.complex.filter.js +++ b/test/mocked/model.complex.filter.js @@ -3,8 +3,8 @@ import 'should'; import 'should-sinon'; import request from 'supertest'; -import { odata, assertSuccess, host, port } from './support/setup'; -import Db from './support/fake-db'; +import { odata, assertSuccess, host, port } from '../support/setup'; +import Db from '../support/fake-db'; import sinon from 'sinon'; describe('model.complex.filter', () => { diff --git a/test/model.complex.js b/test/mocked/model.complex.js similarity index 91% rename from test/model.complex.js rename to test/mocked/model.complex.js index 4c315c8..6731bcc 100644 --- a/test/model.complex.js +++ b/test/mocked/model.complex.js @@ -2,8 +2,8 @@ import 'should'; import request from 'supertest'; -import { odata, host, port } from './support/setup'; -import FakeDb from './support/fake-db'; +import { odata, host, port } from '../support/setup'; +import FakeDb from '../support/fake-db'; function addResource() { return request(host) diff --git a/test/model.custom.id.js b/test/mocked/model.custom.id.js similarity index 89% rename from test/model.custom.id.js rename to test/mocked/model.custom.id.js index f5702b8..984428c 100644 --- a/test/model.custom.id.js +++ b/test/mocked/model.custom.id.js @@ -1,7 +1,7 @@ import 'should'; import request from 'supertest'; -import fakeDb from './support/fake-db'; -import { odata, host, port } from './support/setup'; +import fakeDb from '../support/fake-db'; +import { odata, host, port } from '../support/setup'; describe('model.custom.id', () => { let httpServer; diff --git a/test/model.hidden.field.js b/test/mocked/model.hidden.field.js similarity index 92% rename from test/model.hidden.field.js rename to test/mocked/model.hidden.field.js index eee41cc..065b96e 100644 --- a/test/model.hidden.field.js +++ b/test/mocked/model.hidden.field.js @@ -1,8 +1,8 @@ import 'should'; import sinon from 'sinon'; import request from 'supertest'; -import FakeDb from './support/fake-db'; -import { odata, host, port } from './support/setup'; +import FakeDb from '../support/fake-db'; +import { odata, host, port } from '../support/setup'; describe('model.hidden.field', function () { let httpServer, id, resource, mock; diff --git a/test/model.special.name.js b/test/mocked/model.special.name.js similarity index 85% rename from test/model.special.name.js rename to test/mocked/model.special.name.js index 9f31649..cf96c8a 100644 --- a/test/model.special.name.js +++ b/test/mocked/model.special.name.js @@ -1,7 +1,7 @@ import 'should'; import request from 'supertest'; -import { odata, host, port } from './support/setup'; -import FakeDb from './support/fake-db'; +import { odata, host, port } from '../support/setup'; +import FakeDb from '../support/fake-db'; describe('model.special.name', () => { let httpServer; diff --git a/test/odata.actions.js b/test/mocked/odata.actions.js similarity index 86% rename from test/odata.actions.js rename to test/mocked/odata.actions.js index 14ecb23..bfbd8d8 100644 --- a/test/odata.actions.js +++ b/test/mocked/odata.actions.js @@ -1,8 +1,8 @@ import 'should'; import request from 'supertest'; -import { odata, host, port, bookSchema } from './support/setup'; -import data from './support/books.json'; -import FakeDb from './support/fake-db'; +import { odata, host, port, bookSchema } from '../support/setup'; +import data from '../support/books.json'; +import FakeDb from '../support/fake-db'; function requestToHalfPrice(id) { return request(host).post(`/book(${id})/50off`); diff --git a/test/odata.batch.js b/test/mocked/odata.batch.js similarity index 96% rename from test/odata.batch.js rename to test/mocked/odata.batch.js index eb0188e..9de7b28 100644 --- a/test/odata.batch.js +++ b/test/mocked/odata.batch.js @@ -1,8 +1,8 @@ import 'should'; import request from 'supertest'; -import { odata, host, port, bookSchema, assertSuccess } from './support/setup'; -import data from './support/books.json'; -import FakeDb from './support/fake-db'; +import { odata, host, port, bookSchema, assertSuccess } from '../support/setup'; +import data from '../support/books.json'; +import FakeDb from '../support/fake-db'; import sinon from 'sinon'; describe('odata.batch', () => { @@ -22,7 +22,6 @@ describe('odata.batch', () => { sandbox.restore(); }); - /* it('should work with get lists', async function () { const result = [ { @@ -192,7 +191,7 @@ describe('odata.batch', () => { statusText: "No Content" }] }); - });*/ + });/* it('should work with multipart request body', async function () { const result = { @@ -237,5 +236,5 @@ Content-Length: ${JSON.stringify(result).length} ${JSON.stringify(result)} --batch-1— `); - }); + });*/ }); diff --git a/test/odata.count.js b/test/mocked/odata.count.js similarity index 82% rename from test/odata.count.js rename to test/mocked/odata.count.js index 383ddf7..e6932b1 100644 --- a/test/odata.count.js +++ b/test/mocked/odata.count.js @@ -1,8 +1,8 @@ import 'should'; import request from 'supertest'; -import { odata, host, port, bookSchema } from './support/setup'; -import books from './support/books.json'; -import FakeDb from './support/fake-db'; +import { odata, host, port, bookSchema } from '../support/setup'; +import books from '../support/books.json'; +import FakeDb from '../support/fake-db'; describe('odata.query.count', function() { let httpServer; diff --git a/test/odata.functions.js b/test/mocked/odata.functions.js similarity index 86% rename from test/odata.functions.js rename to test/mocked/odata.functions.js index 6353209..9056281 100644 --- a/test/odata.functions.js +++ b/test/mocked/odata.functions.js @@ -1,7 +1,7 @@ import 'should'; import request from 'supertest'; -import { odata, host, port, assertSuccess } from './support/setup'; -import FakeDb from './support/fake-db'; +import { odata, host, port, assertSuccess } from '../support/setup'; +import FakeDb from '../support/fake-db'; describe('odata.functions', () => { ['get', 'post', 'put', 'delete'].map((method) => { diff --git a/test/odata.query.count.js b/test/mocked/odata.query.count.js similarity index 87% rename from test/odata.query.count.js rename to test/mocked/odata.query.count.js index b404c45..ef0dd3d 100644 --- a/test/odata.query.count.js +++ b/test/mocked/odata.query.count.js @@ -1,8 +1,8 @@ import 'should'; import request from 'supertest'; -import { odata, host, port, bookSchema } from './support/setup'; -import books from './support/books.json'; -import FakeDb from './support/fake-db'; +import { odata, host, port, bookSchema } from '../support/setup'; +import books from '../support/books.json'; +import FakeDb from '../support/fake-db'; describe('odata.query.count', function() { let httpServer; diff --git a/test/odata.query.filter.functions.js b/test/mocked/odata.query.filter.functions.js similarity index 93% rename from test/odata.query.filter.functions.js rename to test/mocked/odata.query.filter.functions.js index 705ee4e..f69bdab 100644 --- a/test/odata.query.filter.functions.js +++ b/test/mocked/odata.query.filter.functions.js @@ -1,9 +1,9 @@ import 'should'; import sinon from 'sinon'; import request from 'supertest'; -import { odata, host, port, bookSchema } from './support/setup'; -import data from './support/books.json'; -import FakeDb from './support/fake-db'; +import { odata, host, port, bookSchema } from '../support/setup'; +import data from '../support/books.json'; +import FakeDb from '../support/fake-db'; describe('odata.query.filter.functions', function () { let httpServer, mock, resource; diff --git a/test/odata.query.filter.js b/test/mocked/odata.query.filter.js similarity index 97% rename from test/odata.query.filter.js rename to test/mocked/odata.query.filter.js index 17ab8ac..0d6956d 100644 --- a/test/odata.query.filter.js +++ b/test/mocked/odata.query.filter.js @@ -1,9 +1,9 @@ import 'should'; import sinon from 'sinon'; import request from 'supertest'; -import { odata, host, port, bookSchema } from './support/setup'; -import data from './support/books.json'; -import FakeDb from './support/fake-db'; +import { odata, host, port, bookSchema } from '../support/setup'; +import data from '../support/books.json'; +import FakeDb from '../support/fake-db'; describe('odata.query.filter', function() { let httpServer, books, resource, mock; diff --git a/test/odata.query.orderby.js b/test/mocked/odata.query.orderby.js similarity index 92% rename from test/odata.query.orderby.js rename to test/mocked/odata.query.orderby.js index e2b1a3c..73d9fb4 100644 --- a/test/odata.query.orderby.js +++ b/test/mocked/odata.query.orderby.js @@ -1,9 +1,9 @@ import 'should'; import sinon from 'sinon'; import request from 'supertest'; -import { odata, host, port, bookSchema } from './support/setup'; -import data from './support/books.json'; -import FakeDb from './support/fake-db'; +import { odata, host, port, bookSchema } from '../support/setup'; +import data from '../support/books.json'; +import FakeDb from '../support/fake-db'; describe('odata.query.orderby', () => { let httpServer, mock, resource; diff --git a/test/odata.query.select.js b/test/mocked/odata.query.select.js similarity index 93% rename from test/odata.query.select.js rename to test/mocked/odata.query.select.js index fe9e567..377e020 100644 --- a/test/odata.query.select.js +++ b/test/mocked/odata.query.select.js @@ -1,9 +1,9 @@ import 'should'; import sinon from 'sinon'; import request from 'supertest'; -import { odata, host, port, bookSchema } from './support/setup'; -import data from './support/books.json'; -import FakeDb from './support/fake-db'; +import { odata, host, port, bookSchema } from '../support/setup'; +import data from '../support/books.json'; +import FakeDb from '../support/fake-db'; describe('odata.query.select', () => { let httpServer, mock, resource; diff --git a/test/odata.query.skip.js b/test/mocked/odata.query.skip.js similarity index 92% rename from test/odata.query.skip.js rename to test/mocked/odata.query.skip.js index ae146c5..4d21945 100644 --- a/test/odata.query.skip.js +++ b/test/mocked/odata.query.skip.js @@ -1,8 +1,8 @@ import 'should'; import sinon from 'sinon'; import request from 'supertest'; -import { odata, host, port, books, bookSchema } from './support/setup'; -import FakeDb from './support/fake-db'; +import { odata, host, port, books, bookSchema } from '../support/setup'; +import FakeDb from '../support/fake-db'; describe('odata.query.skip', () => { let httpServer, mock, resource; diff --git a/test/odata.query.top.js b/test/mocked/odata.query.top.js similarity index 91% rename from test/odata.query.top.js rename to test/mocked/odata.query.top.js index 334225b..72cbd46 100644 --- a/test/odata.query.top.js +++ b/test/mocked/odata.query.top.js @@ -1,8 +1,8 @@ import 'should'; import sinon from 'sinon'; import request from 'supertest'; -import FakeDb from './support/fake-db'; -import { odata, host, port, books, bookSchema } from './support/setup'; +import FakeDb from '../support/fake-db'; +import { odata, host, port, books, bookSchema } from '../support/setup'; describe('odata.query.top', () => { let httpServer, mock, resource; diff --git a/test/options.maxSkip.js b/test/mocked/options.maxSkip.js similarity index 94% rename from test/options.maxSkip.js rename to test/mocked/options.maxSkip.js index 5cce70f..b3766d8 100644 --- a/test/options.maxSkip.js +++ b/test/mocked/options.maxSkip.js @@ -1,8 +1,8 @@ import 'should'; import sinon from 'sinon'; import request from 'supertest'; -import { odata, host, port, books, bookSchema } from './support/setup'; -import FakeDb from './support/fake-db'; +import { odata, host, port, books, bookSchema } from '../support/setup'; +import FakeDb from '../support/fake-db'; describe('options.maxSkip', () => { let httpServer, server, mock, resource; diff --git a/test/options.maxTop.js b/test/mocked/options.maxTop.js similarity index 94% rename from test/options.maxTop.js rename to test/mocked/options.maxTop.js index 34143d5..0363631 100644 --- a/test/options.maxTop.js +++ b/test/mocked/options.maxTop.js @@ -1,8 +1,8 @@ import 'should'; import sinon from 'sinon'; import request from 'supertest'; -import { odata, host, port, bookSchema } from './support/setup'; -import FakeDb from './support/fake-db'; +import { odata, host, port, bookSchema } from '../support/setup'; +import FakeDb from '../support/fake-db'; describe('options.maxTop', () => { let httpServer, server, resource, mock; diff --git a/test/options.prefix.js b/test/mocked/options.prefix.js similarity index 90% rename from test/options.prefix.js rename to test/mocked/options.prefix.js index f188d38..731299f 100644 --- a/test/options.prefix.js +++ b/test/mocked/options.prefix.js @@ -1,7 +1,7 @@ import 'should'; import request from 'supertest'; -import { odata, host, port, bookSchema } from './support/setup'; -import FakeDb from './support/fake-db'; +import { odata, host, port, bookSchema } from '../support/setup'; +import FakeDb from '../support/fake-db'; describe('options.prefix', () => { let httpServer, db; diff --git a/test/rest.delete.js b/test/mocked/rest.delete.js similarity index 87% rename from test/rest.delete.js rename to test/mocked/rest.delete.js index 9760a54..1859452 100644 --- a/test/rest.delete.js +++ b/test/mocked/rest.delete.js @@ -1,8 +1,8 @@ import 'should'; import request from 'supertest'; -import { odata, host, port, bookSchema, assertSuccess } from './support/setup'; -import books from './support/books.json'; -import FakeDb from './support/fake-db'; +import { odata, host, port, bookSchema, assertSuccess } from '../support/setup'; +import books from '../support/books.json'; +import FakeDb from '../support/fake-db'; describe('rest.delete', function() { let data, httpServer; diff --git a/test/rest.get.js b/test/mocked/rest.get.js similarity index 91% rename from test/rest.get.js rename to test/mocked/rest.get.js index 8b94498..41adfcd 100644 --- a/test/rest.get.js +++ b/test/mocked/rest.get.js @@ -1,8 +1,8 @@ import 'should'; import request from 'supertest'; -import { odata, host, port, bookSchema } from './support/setup'; -import books from './support/books.json'; -import FakeDb from './support/fake-db'; +import { odata, host, port, bookSchema } from '../support/setup'; +import books from '../support/books.json'; +import FakeDb from '../support/fake-db'; describe('rest.get', () => { let data, cdata, httpServer; diff --git a/test/rest.post.js b/test/mocked/rest.post.js similarity index 87% rename from test/rest.post.js rename to test/mocked/rest.post.js index 6a4b6c1..822fd17 100644 --- a/test/rest.post.js +++ b/test/mocked/rest.post.js @@ -1,7 +1,7 @@ import 'should'; import request from 'supertest'; -import { odata, host, port, bookSchema } from './support/setup'; -import FakeDb from './support/fake-db'; +import { odata, host, port, bookSchema } from '../support/setup'; +import FakeDb from '../support/fake-db'; describe('rest.post', () => { let httpServer; diff --git a/test/rest.put.js b/test/mocked/rest.put.js similarity index 90% rename from test/rest.put.js rename to test/mocked/rest.put.js index 29bf1b4..a53dd2e 100644 --- a/test/rest.put.js +++ b/test/mocked/rest.put.js @@ -1,9 +1,9 @@ import * as uuid from 'uuid'; import 'should'; import request from 'supertest'; -import { odata, host, port, bookSchema } from './support/setup'; -import books from './support/books.json'; -import FakeDb from './support/fake-db'; +import { odata, host, port, bookSchema } from '../support/setup'; +import books from '../support/books.json'; +import FakeDb from '../support/fake-db'; describe('rest.put', () => { let data, httpServer; diff --git a/test/service.document.js b/test/mocked/service.document.js similarity index 92% rename from test/service.document.js rename to test/mocked/service.document.js index 8843c14..f50dec9 100644 --- a/test/service.document.js +++ b/test/mocked/service.document.js @@ -1,7 +1,7 @@ import 'should'; import request from 'supertest'; -import { host, port, bookSchema, odata, assertSuccess } from './support/setup'; -import FakeDb from './support/fake-db'; +import { host, port, bookSchema, odata, assertSuccess } from '../support/setup'; +import FakeDb from '../support/fake-db'; describe('service.document', () => { let httpServer, server, db; diff --git a/test/utils.js b/test/mocked/utils.js similarity index 96% rename from test/utils.js rename to test/mocked/utils.js index 9f3b6ca..1608f66 100644 --- a/test/utils.js +++ b/test/mocked/utils.js @@ -1,5 +1,5 @@ import 'should'; -import { min, split } from '../lib/utils'; +import { min, split } from '../../lib/utils'; describe('min', () => { return it('should work', () => { From 916c84c58c81e7f4e30d57a961b620d2d67cbb51 Mon Sep 17 00:00:00 2001 From: Richard Martens Date: Sat, 1 Apr 2023 22:09:59 +0200 Subject: [PATCH 13/64] no _id error fixed --- .vscode/launch.json | 2 +- src/db/idPlugin.js | 8 ++++---- test/neededDbRunning/rest.post.js | 32 +++++++++++++++++++++++++++++++ 3 files changed, 37 insertions(+), 5 deletions(-) create mode 100644 test/neededDbRunning/rest.post.js diff --git a/.vscode/launch.json b/.vscode/launch.json index 2ad9ed3..ae2201e 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -20,7 +20,7 @@ "dot", "--timeout", "300000", - "test/odata.batch.js" + "test/neededDbRunning/rest.post.js" ] }, { diff --git a/src/db/idPlugin.js b/src/db/idPlugin.js index 7b4b8cd..7186918 100644 --- a/src/db/idPlugin.js +++ b/src/db/idPlugin.js @@ -5,7 +5,7 @@ export default function (schema) { // display value of _id when request id. if (!schema.paths.id) { schema.virtual('id').get(function getId() { - return this._id; + return this._doc._id; }); schema.set('toObject', { virtuals: true }); schema.set('toJSON', { virtuals: true }); @@ -30,13 +30,13 @@ export default function (schema) { // genarate _id. schema.pre('save', function preSave(next) { - if (this.isNew && !this._id) { + if (this.isNew && !this._doc._id) { if (this.id) { // Use a user-defined id to save - this._id = this.id; + this._doc._id = this.id; } else { // Use uuid to save - this._id = uuid.v4(); + this._doc._id = uuid.v4(); } } return next(); diff --git a/test/neededDbRunning/rest.post.js b/test/neededDbRunning/rest.post.js new file mode 100644 index 0000000..2de5176 --- /dev/null +++ b/test/neededDbRunning/rest.post.js @@ -0,0 +1,32 @@ +import 'should'; +import request from 'supertest'; +import { odata, host, port, bookSchema, conn } from '../support/setup'; + +describe('rest.post', () => { + let httpServer; + + before(async function() { + const server = odata(conn); + server.resource('book', bookSchema) + httpServer = server.listen(port); + }); + + after(() => { + httpServer.close(); + }); + + it('should create new resource', async function() { + const res = await request(host) + .post(`/book`) + .send({ title: Math.random() }); + if (!res.ok) { + res.text.should.equal(''); + } + res.body.should.be.have.property('id'); + res.body.should.be.have.property('title'); + }); + it('should be 422 if post without data', async function() { + const res = await request(host).post(`/book`); + res.status.should.be.equal(422); + }); +}); From f02e1b3f2fa308a9bf424dfb4e9e4599388c34ff Mon Sep 17 00:00:00 2001 From: Richard Martens Date: Sat, 1 Apr 2023 22:31:43 +0200 Subject: [PATCH 14/64] resolve hanging tests --- src/db/db.js | 5 +++++ test/neededDbRunning/rest.post.js | 15 +++++++-------- 2 files changed, 12 insertions(+), 8 deletions(-) diff --git a/src/db/db.js b/src/db/db.js index 1d9db00..4bd295d 100644 --- a/src/db/db.js +++ b/src/db/db.js @@ -15,6 +15,11 @@ export default class { return this._connection; } + closeConnection() { + this._connection.close(); + delete this._connection; + } + on(name, event) { this._connection.on(name, event); } diff --git a/test/neededDbRunning/rest.post.js b/test/neededDbRunning/rest.post.js index 2de5176..d93a7b8 100644 --- a/test/neededDbRunning/rest.post.js +++ b/test/neededDbRunning/rest.post.js @@ -3,30 +3,29 @@ import request from 'supertest'; import { odata, host, port, bookSchema, conn } from '../support/setup'; describe('rest.post', () => { - let httpServer; + let httpServer, server; - before(async function() { - const server = odata(conn); + before(function() { + server = odata(conn); server.resource('book', bookSchema) httpServer = server.listen(port); }); after(() => { + const db = server.get('db'); + httpServer.close(); + db.closeConnection(); }); it('should create new resource', async function() { const res = await request(host) .post(`/book`) - .send({ title: Math.random() }); + .send({ title: 'rest.post.js' }); if (!res.ok) { res.text.should.equal(''); } res.body.should.be.have.property('id'); res.body.should.be.have.property('title'); }); - it('should be 422 if post without data', async function() { - const res = await request(host).post(`/book`); - res.status.should.be.equal(422); - }); }); From 7f845a7595cbfade40c889f990efef9cd17b49f0 Mon Sep 17 00:00:00 2001 From: Richard Martens Date: Sun, 2 Apr 2023 22:05:44 +0200 Subject: [PATCH 15/64] generating exmaple for stackoverflow --- .vscode/launch.json | 2 +- Makefile | 3 +- test/failing/stackoverflow.js | 61 +++++++++++++++++++++++++++++++++++ test/mocked/odata.batch.js | 12 ++----- 4 files changed, 66 insertions(+), 12 deletions(-) create mode 100644 test/failing/stackoverflow.js diff --git a/.vscode/launch.json b/.vscode/launch.json index ae2201e..069edc6 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -20,7 +20,7 @@ "dot", "--timeout", "300000", - "test/neededDbRunning/rest.post.js" + "test/mocked/odata.batch.js" ] }, { diff --git a/Makefile b/Makefile index 988ea3b..efa6e8d 100644 --- a/Makefile +++ b/Makefile @@ -14,7 +14,8 @@ test: @node_modules/.bin/mocha\ --require @babel/register \ --reporter $(REPORTER) \ - test/**/*.js + test/mocked/**/*.js + test/neededDbRunning/**/*.js test-cov: @node node_modules/istanbul/lib/cli.js cover -x '**/examples/**' \ diff --git a/test/failing/stackoverflow.js b/test/failing/stackoverflow.js new file mode 100644 index 0000000..1607d10 --- /dev/null +++ b/test/failing/stackoverflow.js @@ -0,0 +1,61 @@ +import 'should'; +import request from 'supertest'; +import { odata, host, port, bookSchema, assertSuccess } from '../support/setup'; +import data from '../support/books.json'; +import FakeDb from '../support/fake-db'; +import sinon from 'sinon'; + +describe('odata.batch', () => { + let httpServer, books, resource, sandbox; + + beforeEach(async function () { + const db = new FakeDb(); + const server = odata(db); + resource = server.resource('book', bookSchema); + books = JSON.parse(JSON.stringify(db.addData('book', data))); + httpServer = server.listen(port); + sandbox = sinon.createSandbox(); + }); + + afterEach(() => { + httpServer.close(); + sandbox.restore(); + }); + + it('should work with multipart request body', async function () { + const result = { + title: "War and peace" + }; + const res = await request(host) + .post(`/$batch`) + .send({}) + .set('Content-Type', 'multipart/mixed; boundary=batch_1') + .set('Host', host) + .serialize(() => ` +--batch_1 +Content-Type: application/http + +POST /book +Host: ${host} +Content-Type: application/json +Content-Length: ${JSON.stringify(result).length} + +${JSON.stringify(result)} +--batch_1-- + `); + + assertSuccess(res); + + res.text.should.equal(` +--batch-1 +Content-Type: application/http + +HTTP/1.1 200 Ok +Content-Type: application/json +Content-Length: ${JSON.stringify(result).length} + +${JSON.stringify(result)} +--batch-1— + `); + }); +}); diff --git a/test/mocked/odata.batch.js b/test/mocked/odata.batch.js index 9de7b28..81b8e95 100644 --- a/test/mocked/odata.batch.js +++ b/test/mocked/odata.batch.js @@ -191,8 +191,8 @@ describe('odata.batch', () => { statusText: "No Content" }] }); - });/* - + }); +/* it('should work with multipart request body', async function () { const result = { title: "War and peace" @@ -217,14 +217,6 @@ ${JSON.stringify(result)} assertSuccess(res); - const single = await new Promise((resolve, reject) => { - res.files.null.on('error', part => { - reject(part); - }); - res.files.null.on('data', part => { - resolve(part); - }); - }); res.text.should.equal(` --batch-1 Content-Type: application/http From cc1a8f9d18dcc1294f49a5f0ad9b8ddca6e6220c Mon Sep 17 00:00:00 2001 From: Richard Martens Date: Tue, 4 Apr 2023 22:48:38 +0200 Subject: [PATCH 16/64] partially support for enum arrays implemented --- .vscode/launch.json | 2 +- src/spezialResources/ODataMetadata.js | 4 ++- test/mocked/metadata.resource.complex.js | 34 ++++++++++++++++++++++++ test/support/fake-db-model.js | 10 +++++++ 4 files changed, 48 insertions(+), 2 deletions(-) diff --git a/.vscode/launch.json b/.vscode/launch.json index 069edc6..cc83e61 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -20,7 +20,7 @@ "dot", "--timeout", "300000", - "test/mocked/odata.batch.js" + "test/mocked/metadata.resource.complex.js" ] }, { diff --git a/src/spezialResources/ODataMetadata.js b/src/spezialResources/ODataMetadata.js index d608805..e8bfb12 100644 --- a/src/spezialResources/ODataMetadata.js +++ b/src/spezialResources/ODataMetadata.js @@ -102,7 +102,9 @@ export default class Metadata { result.$Type = `node.odata.${notClassifiedName}`; root(notClassifiedName, this.visitor('ComplexType', node.schema.paths, model[0], root)); } else { - const arrayItemType = this.visitor('Property', { instance: node.options.type[0].name }, model[0], root); + const arrayItemType = this.visitor('Property', { + instance: node.options.type[0].name || node.options.type[0].type.name //Enums have an object with enum and type + }, model[0], root); result.$Type = arrayItemType.$Type; } diff --git a/test/mocked/metadata.resource.complex.js b/test/mocked/metadata.resource.complex.js index 42de3e2..e1a0c9b 100644 --- a/test/mocked/metadata.resource.complex.js +++ b/test/mocked/metadata.resource.complex.js @@ -165,6 +165,40 @@ describe('metadata.resource.complex', () => { res.text.should.equal(xmlDocument); }); + + it('should return xml metadata for nested enum array', async function() { + const xmlDocument = + ` + + + + + + + + + + + + + + + + + `.replace(/\s*\s*/g, '>'); + server.resource('complex-model', { + p3: [{ + type: String, + enum: ['P4'] + }] + }); + httpServer = server.listen(port); + const res = await request(host).get('/$metadata').set('accept', 'application/xml'); + assertSuccess(res); + res.text.should.equal(xmlDocument); + }); + + it('should return json metadata for nested document in document', async function() { const jsonDocument = { $Version: '4.0', diff --git a/test/support/fake-db-model.js b/test/support/fake-db-model.js index 4b3c0de..53e2c5c 100644 --- a/test/support/fake-db-model.js +++ b/test/support/fake-db-model.js @@ -33,6 +33,16 @@ export default class Model { name: model[item][0].name }] }; + } else if(model[item][0].enum) { + // Array of Enum values + result[propName].options = { + type: [{ + type: { + name: model[item][0].type.name + }, + enum: model[item][0].enum + }] + }; } else { // Array of objects result[propName].schema = { From 2744e0ecc9b2e4f322cab3ecec8c3c17e522a638 Mon Sep 17 00:00:00 2001 From: Richard Martens Date: Fri, 7 Apr 2023 23:05:55 +0200 Subject: [PATCH 17/64] unbound Action works by direct call with metadata, but not per batch --- .vscode/launch.json | 2 +- examples/simple/index.js | 2 +- src/Action.js | 74 +++++++++++++++++ src/ODataResource.js | 25 ++---- src/server.js | 21 ++++- src/spezialResources/ODataMetadata.js | 63 ++++++++++++--- src/writer/xmlWriter.js | 42 ++++++---- test/mocked/metadata.action.js | 112 +++++++++++++++++++++++++- test/mocked/model.complex.action.js | 2 +- test/mocked/odata.actions.js | 52 +++++++----- 10 files changed, 328 insertions(+), 67 deletions(-) create mode 100644 src/Action.js diff --git a/.vscode/launch.json b/.vscode/launch.json index cc83e61..3bc785c 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -20,7 +20,7 @@ "dot", "--timeout", "300000", - "test/mocked/metadata.resource.complex.js" + "test/mocked/metadata.action.js" ] }, { diff --git a/examples/simple/index.js b/examples/simple/index.js index 109ca03..aac1dc2 100644 --- a/examples/simple/index.js +++ b/examples/simple/index.js @@ -12,7 +12,7 @@ var bookInfo = { }; server.resource('book', bookInfo) - .action('/50off', function(req, res, next){ + .action('50off', function(req, res, next){ server.repository('book').findById(req.params.id, function(err, book){ book.price = +(book.price / 2).toFixed(2); book.save(function(err){ diff --git a/src/Action.js b/src/Action.js new file mode 100644 index 0000000..ecea279 --- /dev/null +++ b/src/Action.js @@ -0,0 +1,74 @@ +import { Router } from 'express'; +import pipes from './pipes'; + +export default class Action { + constructor(name, fn, options) { + + this.name = name; + this.fn = fn; + + if (options) { + this.auth = options.auth; + this.binding = options.binding; + this.resource = options.resource; + this.server = options.server; + this.$Parameter = options.$Parameter; + } + } + + getRouter() { + if (!this.router) { + if (!this.name || !this.name.match(/^^[_a-zA-Z0-9][_a-zA-Z0-9.-]*$/)) { + throw new Error(`Invalid action name '${this.name}'`); + } + + const path = this.getPath(); + + this.router = this.getOperationRouter(path, this.fn, this.auth); + } + + return this.router; + } + + getPath() { + let path; + + switch (this.binding) { + case 'entity': + if (!this.resource) { + throw new Error(`Binding '${this.binding}' require a resource`) + } + path = `/${this.resource._url}\\(:id\\)/${this.name}`; + break; + case 'collection': + if (!this.resource) { + throw new Error(`Binding '${this.binding}' require a resource`) + } + path = `/${this.resource._url}/${this.name}`; + break; + default: + if (this.binding) { + throw new Error(`Invalid binding '${this.binding}'`); + } else { + if (this.resource) { + throw new Error(`Use of the unbound action '${this.name}' by a resource '${this.resource._url}' is not intended`) + } + path = `/${this.name}`; + } + break; + } + return path; + } + + getOperationRouter(path, fn, auth) { + const router = Router(); + + router.post(path, (req, res, next) => { + pipes.authorizePipe(req, res, auth) + .then(() => fn(req, res, next)) + .catch((result) => pipes.errorPipe(req, res, result)); + }); + + return router; + }; +} \ No newline at end of file diff --git a/src/ODataResource.js b/src/ODataResource.js index 51e2828..78a99cb 100644 --- a/src/ODataResource.js +++ b/src/ODataResource.js @@ -1,5 +1,6 @@ import rest from './rest'; import { min } from './utils'; +import Action from './Action'; function hook(resource, pos, fn) { let method = resource._currentMethod; @@ -54,23 +55,13 @@ export default class { this.model = model; } - action(url, fn, options) { - let auth; - let binding; - - if (options) { - auth = options.auth; - binding = options.binding; - } - - this.actions[url] = fn; - this.actions[url].auth = auth; - this.actions[url].binding = binding; - this.actions[url].resource = this._url; - - const resourceUrl = !binding || binding === 'entity' // 'entity' || 'collection' - ? `/${this._url}\\(:id\\)` : `/${this._url}`; - this.actions[url].router = rest.getOperationRouter(resourceUrl, url, fn, auth); + action(name, fn, options) { + this.actions[name] = new Action(name, fn, + { + ...options, + resource: this, + server: this._server + }); return this; } diff --git a/src/server.js b/src/server.js index d702318..d07abdf 100644 --- a/src/server.js +++ b/src/server.js @@ -5,6 +5,7 @@ import Metadata from './spezialResources/ODataMetadata'; import ServiceDocument from './spezialResources/ODataServiceDocument'; import Batch from './spezialResources/ODataBatch'; import Db from './db/db'; +import Action from './Action'; function checkAuth(auth, req) { return !auth || auth(req); @@ -28,6 +29,8 @@ class Server { $metadata: new Metadata(this), $batch: new Batch(this), }; + //unbound actions + this.actions = {}; this._serviceDocument = new ServiceDocument(this); } @@ -104,6 +107,16 @@ class Server { }); } + action(name, fn, options) { + this.actions[name] = new Action(name, fn, + { + ...options, + server: this + }); + + return this; + } + _getRouter() { const result = []; @@ -118,11 +131,17 @@ class Server { Object.keys(resource.actions).forEach((actionKey) => { const action = resource.actions[actionKey]; - result.push(action.router); + result.push(action.getRouter()); }); } }); + Object.keys(this.actions).forEach(actionKey => { + const action = this.actions[actionKey]; + + result.push(action.getRouter()); + }); + return result; } diff --git a/src/spezialResources/ODataMetadata.js b/src/spezialResources/ODataMetadata.js index e8bfb12..d3a6c46 100644 --- a/src/spezialResources/ODataMetadata.js +++ b/src/spezialResources/ODataMetadata.js @@ -102,7 +102,7 @@ export default class Metadata { result.$Type = `node.odata.${notClassifiedName}`; root(notClassifiedName, this.visitor('ComplexType', node.schema.paths, model[0], root)); } else { - const arrayItemType = this.visitor('Property', { + const arrayItemType = this.visitor('Property', { instance: node.options.type[0].name || node.options.type[0].type.name //Enums have an object with enum and type }, model[0], root); @@ -175,15 +175,37 @@ export default class Metadata { } static visitAction(node) { - return { - $Kind: 'Action', - $IsBound: true, - $Parameter: [{ - $Name: node.resource, - $Type: `node.odata.${node.resource}`, - $Collection: node.binding === 'collection' ? true : undefined, - }], + const result = { + $Kind: 'Action' }; + + if (node.binding) { + result.$IsBound = true; + result.$Parameter = [{ + $Name: node.resource._url, + $Type: `node.odata.${node.resource._url}`, + $Collection: node.binding === 'collection' ? true : undefined, + }]; + } + + if (node.$Parameter) { + if (!result.$Parameter) { + result.$Parameter = []; + } + + node.$Parameter.forEach(para => { + const item = para; + + if (para.$Type.search(/^edm/i) === -1 ) { + item.$Type = `node.odata.${para.$Type}`; + } + + result.$Parameter.push(item); + }); + result.$Parameter = result.$Parameter ? result.$Parameter.concat() : node.$Parameter; + } + + return result; } static visitFunction(node) { @@ -255,6 +277,27 @@ export default class Metadata { return result; }, {}); + const actionNames = Object.keys(this._server.actions); + const actionImports = actionNames.reduce((previousAction, currentAction) => { + const result = {...previousAction}; + const action = this._server.actions[currentAction]; + + result[`${currentAction}-import`] = { + $Action: `node.odata.${currentAction}` + }; + + return result; + }, {}) + const unboundActions = actionNames.reduce((previousAction, currentAction) => { + const result = {...previousAction}; + const action = this._server.actions[currentAction]; + const attachToRoot = (name, value) => { result[name] = value; }; + + result[currentAction] = this.visitor('Action', action, attachToRoot); + + return result; + }, {}) + const document = { $Version: '4.0', ObjectId: { @@ -263,10 +306,12 @@ export default class Metadata { $MaxLength: 24, }, ...entityTypes, + ...unboundActions, $EntityContainer: 'node.odata', ['node.odata']: { // eslint-disable-line no-useless-computed-key $Kind: 'EntityContainer', ...entitySets, + ...actionImports }, }; diff --git a/src/writer/xmlWriter.js b/src/writer/xmlWriter.js index 813060e..0682dc0 100644 --- a/src/writer/xmlWriter.js +++ b/src/writer/xmlWriter.js @@ -8,28 +8,31 @@ export default class XmlWriter { return this.visitEntityType(node, name); case 'Property': - return XmlWriter.visitProperty(node, name); + return this.visitProperty(node, name); case 'EntityContainer': return this.visitEntityContainter(node); case 'EntitySet': - return XmlWriter.visitEntitySet(node, name); + return this.visitEntitySet(node, name); case 'TypeDefinition': - return XmlWriter.visitTypeDefinition(node, name); + return this.visitTypeDefinition(node, name); case 'ComplexType': return this.visitComplexType(node, name); case 'Action': - return XmlWriter.visitAction(node, name); + return this.visitAction(node, name); + + case 'ActionImport': + return this.visitActionImport(node, name); case 'Function': - return XmlWriter.visitFunction(node, name); + return this.visitFunction(node, name); case 'FunctionImport': - return XmlWriter.visitFunctionImport(node, name); + return this.visitFunctionImport(node, name); default: throw new Error(`Type ${type} is not supported`); @@ -55,30 +58,33 @@ export default class XmlWriter { `); } - static visitEntitySet(node, name) { + visitEntitySet(node, name) { return ``; } visitEntityContainter(node) { let entitySets = ''; let functions = ''; + let actions = '' Object.keys(node) .filter((item) => item !== '$Kind') .forEach((item) => { if (node[item].$Type) { entitySets += this.visitor('EntitySet', node[item], item); + } else if (node[item].$Action) { + actions += this.visitor('ActionImport', node[item], item); } else { functions += this.visitor('FunctionImport', node[item], item); } }); return ( ` - ${entitySets}${functions} + ${entitySets}${functions}${actions} `); } - static visitProperty(node, name) { + visitProperty(node, name) { let attributes = ''; if (node.$Nullable === false) { @@ -115,7 +121,7 @@ export default class XmlWriter { `); } - static visitTypeDefinition(node, name) { + visitTypeDefinition(node, name) { let attributes = ''; if (node.$MaxLength) { @@ -142,9 +148,9 @@ export default class XmlWriter { `); } - static visitAction(node, name) { + visitAction(node, name) { const isBound = node.$IsBound ? ' IsBound="true"' : ''; - const parameter = node.$Parameter.map((item) => { + const parameter = node.$Parameter && node.$Parameter.map((item) => { let type = ''; if (item.$Collection) { @@ -158,12 +164,18 @@ export default class XmlWriter { return (` - ${parameter} + ${parameter || ''} `); } - static visitFunction(node, name) { + visitActionImport(node, name) { + return (` + + `); + } + + visitFunction(node, name) { const collection = node.$ReturnType.$Collection ? ' Collection="true"' : ''; return (` @@ -173,7 +185,7 @@ export default class XmlWriter { `); } - static visitFunctionImport(node, name) { + visitFunctionImport(node, name) { return (` `); diff --git a/test/mocked/metadata.action.js b/test/mocked/metadata.action.js index 39c54b0..af3a2c7 100644 --- a/test/mocked/metadata.action.js +++ b/test/mocked/metadata.action.js @@ -35,7 +35,7 @@ describe('metadata.action', () => { $Type: 'node.odata.book' }] }, - 'book': { + book: { $Kind: "EntityType", $Key: ["id"], id: { @@ -49,7 +49,7 @@ describe('metadata.action', () => { $EntityContainer: 'node.odata', ['node.odata']: { $Kind: 'EntityContainer', - 'book': { + book: { $Collection: true, $Type: `node.odata.book`, } @@ -117,7 +117,7 @@ describe('metadata.action', () => { $Collection: true }] }, - 'book': { + book: { $Kind: "EntityType", $Key: ["id"], id: { @@ -131,7 +131,7 @@ describe('metadata.action', () => { $EntityContainer: 'node.odata', ['node.odata']: { $Kind: 'EntityContainer', - 'book': { + book: { $Collection: true, $Type: `node.odata.book`, } @@ -181,4 +181,108 @@ describe('metadata.action', () => { assertSuccess(res); res.text.should.equal(xmlDocument); }); + + it('should not accept action names with special characters', function() { + try { + const resource = server.resource('book', { + author: String + }).action('/login', (req, res, next) => {}); + + resource.actions['/login'].getRouter(); + + throw new Error('Invalid name should not accepted'); + + } catch(error) { + error.message.should.equal(`Invalid action name '/login'`); + } + }); + + + it('should return json metadata for unbound action', async function() { + const jsonDocument = { + $Version: '4.0', + ObjectId: { + $Kind: "TypeDefinition", + $UnderlyingType: "Edm.String", + $MaxLength: 24 + }, + 'unbound-action': { + $Kind: 'Action', + $Parameter: [{ + $Name: 'book', + $Type: 'node.odata.book' + }] + }, + book: { + $Kind: "EntityType", + $Key: ["id"], + id: { + $Type: "node.odata.ObjectId", + $Nullable: false, + }, + author: { + $Type: 'Edm.String' + } + }, + $EntityContainer: 'node.odata', + ['node.odata']: { + $Kind: 'EntityContainer', + book: { + $Collection: true, + $Type: `node.odata.book`, + }, + 'unbound-action-import': { + $Action: 'node.odata.unbound-action' + } + } + }; + server.resource('book', { + author: String + }); + server.action('unbound-action', + (req, res, next) => {}, { + $Parameter: [{ + $Name: 'book', + $Type: 'book' + }] + }); + httpServer = server.listen(port); + const res = await request(host).get('/$metadata?$format=json'); + assertSuccess(res); + res.body.should.deepEqual(jsonDocument); + }); + + it('should return xml metadata for action without parameter', async function() { + const xmlDocument = + ` + + + + + + + + + + + + + + + + + + + + `.replace(/\s*\s*/g, '>'); + server.resource('book', { + author: String + }); + server.action('unbound-action', + (req, res, next) => {}); + httpServer = server.listen(port); + const res = await request(host).get('/$metadata').set('accept', 'application/xml'); + assertSuccess(res); + res.text.should.equal(xmlDocument); + }); }); diff --git a/test/mocked/model.complex.action.js b/test/mocked/model.complex.action.js index 0acea6a..3fc1642 100644 --- a/test/mocked/model.complex.action.js +++ b/test/mocked/model.complex.action.js @@ -13,7 +13,7 @@ describe('model.complex.action', () => { const server = odata(db); const resource = server.resource('order', { product: [{ price: Number }] }); - resource.action('/all-item-greater', (req, res, next) => { + resource.action('all-item-greater', (req, res, next) => { const { price } = req.query; const $elemMatch = { price: { $gt: price } }; server.resources.order.model.exec((err, data) => res.jsonp(data.slice(1))); diff --git a/test/mocked/odata.actions.js b/test/mocked/odata.actions.js index bfbd8d8..7981349 100644 --- a/test/mocked/odata.actions.js +++ b/test/mocked/odata.actions.js @@ -13,31 +13,47 @@ function halfPrice(price) { } describe('odata.actions', () => { - let httpServer, books; - - before(async function() { - const db = new FakeDb(); - const server = odata(db); - server - .resource('book', bookSchema) - .action('/50off', (req, res, next) => { - server.resources.book.model.findById(req.params.id, (err, book) => { - book.price = halfPrice(book.price); - book.save((err) => res.jsonp(book)); - }); - }); - books = JSON.parse( JSON.stringify( db.addData('book', data))); - httpServer = server.listen(port); + let httpServer, server, db; + + beforeEach(async function () { + db = new FakeDb(); + server = odata(db); }); - after(() => { - httpServer.close(); + afterEach(() => { + if (httpServer) { + httpServer.close(); + } }); - it('should work', async function() { + it('should work with bound action', async function () { + server.resource('book', bookSchema) + .action('50off', (req, res, next) => { + server.resources.book.model.findById(req.params.id, (err, book) => { + book.price = halfPrice(book.price); + book.save((err) => res.jsonp(book)); + }); + }, { + binding: 'entity' + }); + const books = JSON.parse(JSON.stringify(db.addData('book', data))); + httpServer = server.listen(port); const item = books[0]; + const res = await requestToHalfPrice(item.id); + const price = halfPrice(item.price); res.body.price.should.be.equal(price); }); + + it('should work with unbound action', async function () { + server.action('salam-aleikum', (req, res, next) => { + res.jsonp({result: 'Wa aleikum assalam'}) + }) + httpServer = server.listen(port); + + const res = await request(host).post(`/salam-aleikum`); + + res.body.result.should.be.equal('Wa aleikum assalam'); + }); }); From 602f6a5c01e79a4296902f323c82f79f123e87cd Mon Sep 17 00:00:00 2001 From: Richard Martens Date: Sat, 8 Apr 2023 23:33:12 +0200 Subject: [PATCH 18/64] implement support for actions in batch and workaround for ui5 bug with names of actions --- .vscode/launch.json | 2 +- src/Action.js | 20 +++++++--- src/ODataResource.js | 8 +++- src/parser/multipartMixed.js | 2 +- src/spezialResources/ODataBatch.js | 33 ++++++++++++----- test/mocked/odata.batch.js | 59 +++++++++++++++++++++++++++++- 6 files changed, 105 insertions(+), 19 deletions(-) diff --git a/.vscode/launch.json b/.vscode/launch.json index 3bc785c..069edc6 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -20,7 +20,7 @@ "dot", "--timeout", "300000", - "test/mocked/metadata.action.js" + "test/mocked/odata.batch.js" ] }, { diff --git a/src/Action.js b/src/Action.js index ecea279..d2af92f 100644 --- a/src/Action.js +++ b/src/Action.js @@ -16,21 +16,29 @@ export default class Action { } } + match(method, url) { + const regex = this.getPath(true); + + if (method === 'post' && url.match(regex)) { + return this.fn; + } + } + getRouter() { if (!this.router) { if (!this.name || !this.name.match(/^^[_a-zA-Z0-9][_a-zA-Z0-9.-]*$/)) { throw new Error(`Invalid action name '${this.name}'`); } - + const path = this.getPath(); - + this.router = this.getOperationRouter(path, this.fn, this.auth); } return this.router; } - getPath() { + getPath(asRegex) { let path; switch (this.binding) { @@ -38,13 +46,13 @@ export default class Action { if (!this.resource) { throw new Error(`Binding '${this.binding}' require a resource`) } - path = `/${this.resource._url}\\(:id\\)/${this.name}`; + path = asRegex ? new RegExp(`\/?${this.resource._url}\\('?[A-Fa-f0-9]*'?\\)\/${this.name}$`) : `/${this.resource._url}\\(:id\\)/${this.name}`; break; case 'collection': if (!this.resource) { throw new Error(`Binding '${this.binding}' require a resource`) } - path = `/${this.resource._url}/${this.name}`; + path = asRegex ? new RegExp(`\/?${this.resource._url}\/${this.name}$`) : `/${this.resource._url}/${this.name}`; break; default: if (this.binding) { @@ -53,7 +61,7 @@ export default class Action { if (this.resource) { throw new Error(`Use of the unbound action '${this.name}' by a resource '${this.resource._url}' is not intended`) } - path = `/${this.name}`; + path = asRegex ? new RegExp(`(node\.odata)?\/?${this.name}$`) : `/${this.name}`; } break; } diff --git a/src/ODataResource.js b/src/ODataResource.js index 78a99cb..178e95d 100644 --- a/src/ODataResource.js +++ b/src/ODataResource.js @@ -176,7 +176,13 @@ export default class { } }); - return route ? route.middleware : undefined; + if (route) { + return route.middleware; + } + + return Object.keys(this.actions) + .map(name => this.actions[name].match(method, url)) + .find(ctrl => ctrl); } _router(setting = {}) { diff --git a/src/parser/multipartMixed.js b/src/parser/multipartMixed.js index 1e5c6ee..020dd31 100644 --- a/src/parser/multipartMixed.js +++ b/src/parser/multipartMixed.js @@ -19,7 +19,7 @@ function multipart(req, res, next) { if (singleRequestText.indexOf("Group ID: ") >= 0) { return; //sap extension, not documentet in odata } - const matchMethodUrl = singleRequestText.match(/^(GET|POST|PUT|PATCH|DELETE)\s+([\w\/$-]+)\s*/m); + const matchMethodUrl = singleRequestText.match(/^(GET|POST|PUT|PATCH|DELETE)\s+([\w\/\.$-]+)\s*/m); if (!matchMethodUrl) { throw new Error(`Method in ${singleRequestText} not supported`); diff --git a/src/spezialResources/ODataBatch.js b/src/spezialResources/ODataBatch.js index 5ed8369..44dc706 100644 --- a/src/spezialResources/ODataBatch.js +++ b/src/spezialResources/ODataBatch.js @@ -30,12 +30,16 @@ export default class Batch { return this; } - middleware = async (req, res) => { + middleware = async (req) => { try { await pipes.authorizePipe(req, res, this._hooks.auth); await pipes.beforePipe(req, res, this._hooks.before); - const result = await this.ctrl(req); + const result = await this.ctrl(req, error => { + if (error instanceof Error) { + throw error; + } + }); const data = await pipes.respondPipe(req, res, result || {}); pipes.afterPipe(req, res, this._hooks.after, data); @@ -93,10 +97,12 @@ export default class Batch { }, {}); } - async ctrl(req) { + async ctrl(req, next) { const responses = req.body.requests.map(async function (request) { const handler = Object.keys(this._server.resources) .map((name) => this._server.resources[name].match(request.method, request.url)) + .concat(Object.keys(this._server.actions) + .map(name => this._server.actions[name].match(request.method, request.url))) .find((ctrl) => ctrl); let result = { }; @@ -124,30 +130,39 @@ export default class Batch { } else { function appendHeader(name, value) { if (!result.headers) { - result.headers = {}; + result.headers = { + 'OData-Version': '4.0' + }; } result.headers[name] = value; } - await handler(currentRequest, { + + function jsonp(body) { + result.body = body; + appendHeader('content-type', 'application/json'); + }; + + const currentResponse = { type: (mimetype) => { appendHeader('content-type', mimetype); }, setHeader: appendHeader, + jsonp, status: (status) => { result.status = status; result.statusText = STATUS_CODES[status]; return { - jsonp: (body) => { - result.body = body; - }, + jsonp, end: () => { }, send: (body) => { result.body = body; } }; }, - }); + }; + + await handler(currentRequest, currentResponse, next); } return result; }.bind(this)); diff --git a/test/mocked/odata.batch.js b/test/mocked/odata.batch.js index 81b8e95..c5557e9 100644 --- a/test/mocked/odata.batch.js +++ b/test/mocked/odata.batch.js @@ -11,7 +11,13 @@ describe('odata.batch', () => { beforeEach(async function () { const db = new FakeDb(); const server = odata(db); - resource = server.resource('book', bookSchema); + resource = server.resource('book', bookSchema) + .action('entity-action', (req, res, next) => { + res.status(200).jsonp({result: 'Hello! I am an action, that bound to entity.'}) + }, { binding: 'entity'}); + server.action('unbound-action', (req, res, next) => { + res.status(200).jsonp({result: 'Hello! I am an unbound action.'}) + }) books = JSON.parse(JSON.stringify(db.addData('book', data))); httpServer = server.listen(port); sandbox = sinon.createSandbox(); @@ -22,6 +28,7 @@ describe('odata.batch', () => { sandbox.restore(); }); + it('should work with get lists', async function () { const result = [ { @@ -192,6 +199,56 @@ describe('odata.batch', () => { }] }); }); + + it('should work with unbound action', async function () { + const res = await request(host).post(`/$batch`).send({ + requests: [{ + id: "1", + method: "post", + url: `/unbound-action` + }] + }); + assertSuccess(res); + res.body.should.deepEqual({ + responses: [{ + id: "1", + status: 200, + statusText: "OK", + headers: { + "OData-Version": "4.0", + "content-type": "application/json" + }, + body: { + result: 'Hello! I am an unbound action.' + } + }] + }); + }); + + it('should work with action, that bound to entity', async function () { + const res = await request(host).post(`/$batch`).send({ + requests: [{ + id: "1", + method: "post", + url: `/book(${books[0].id})/entity-action` + }] + }); + assertSuccess(res); + res.body.should.deepEqual({ + responses: [{ + id: "1", + status: 200, + statusText: "OK", + headers: { + "OData-Version": "4.0", + "content-type": "application/json" + }, + body: { + result: 'Hello! I am an action, that bound to entity.' + } + }] + }); + }); /* it('should work with multipart request body', async function () { const result = { From 286da4b70259d1f09279490f230ec75d8a785161 Mon Sep 17 00:00:00 2001 From: Richard Martens Date: Sun, 9 Apr 2023 21:43:47 +0200 Subject: [PATCH 19/64] fixed response bug in batch pipe --- src/spezialResources/ODataBatch.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/spezialResources/ODataBatch.js b/src/spezialResources/ODataBatch.js index 44dc706..ac8387d 100644 --- a/src/spezialResources/ODataBatch.js +++ b/src/spezialResources/ODataBatch.js @@ -30,7 +30,7 @@ export default class Batch { return this; } - middleware = async (req) => { + middleware = async (req, res) => { try { await pipes.authorizePipe(req, res, this._hooks.auth); await pipes.beforePipe(req, res, this._hooks.before); From b80b53df14cd942899327dae04a0796c399936fb Mon Sep 17 00:00:00 2001 From: Richard Martens Date: Tue, 11 Apr 2023 21:57:41 +0200 Subject: [PATCH 20/64] empty body bug in multipart/mixed --- src/parser/multipartMixed.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/parser/multipartMixed.js b/src/parser/multipartMixed.js index 020dd31..51de761 100644 --- a/src/parser/multipartMixed.js +++ b/src/parser/multipartMixed.js @@ -38,8 +38,8 @@ function multipart(req, res, next) { result.headers[parts[0].trim()] = parts[1].trim(); }); - const blocks = singleRequestText.split('\n\n'); - if (blocks.length > 2) { + const blocks = singleRequestText.split('\r\n\r\n'); + if (blocks.length > 2 && blocks[2].trim()) { result.body = JSON.parse(blocks[2]); } From fa5cf3014717d0a55b1e21314755c795b1e16b5b Mon Sep 17 00:00:00 2001 From: Richard Martens Date: Fri, 21 Apr 2023 22:44:05 +0200 Subject: [PATCH 21/64] adding async support for authorization pipep. closes #84 --- .vscode/launch.json | 2 +- Makefile | 4 +- src/Action.js | 2 +- src/pipes.js | 12 ++---- test/mocked/auth.action.js | 83 ++++++++++++++++++++++++++++++++++++ test/mocked/odata.actions.js | 6 ++- 6 files changed, 96 insertions(+), 13 deletions(-) create mode 100644 test/mocked/auth.action.js diff --git a/.vscode/launch.json b/.vscode/launch.json index 069edc6..a5171f4 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -20,7 +20,7 @@ "dot", "--timeout", "300000", - "test/mocked/odata.batch.js" + "test/mocked/odata.actions.js" ] }, { diff --git a/Makefile b/Makefile index efa6e8d..e365229 100644 --- a/Makefile +++ b/Makefile @@ -14,8 +14,8 @@ test: @node_modules/.bin/mocha\ --require @babel/register \ --reporter $(REPORTER) \ - test/mocked/**/*.js - test/neededDbRunning/**/*.js + --exclude test/failing/**/*.js \ + test/**/*.js test-cov: @node node_modules/istanbul/lib/cli.js cover -x '**/examples/**' \ diff --git a/src/Action.js b/src/Action.js index d2af92f..2b37441 100644 --- a/src/Action.js +++ b/src/Action.js @@ -61,7 +61,7 @@ export default class Action { if (this.resource) { throw new Error(`Use of the unbound action '${this.name}' by a resource '${this.resource._url}' is not intended`) } - path = asRegex ? new RegExp(`(node\.odata)?\/?${this.name}$`) : `/${this.name}`; + path = asRegex ? new RegExp(`(node\.odata)?\/?${this.name}$`) : `/node.odata.${this.name}`; } break; } diff --git a/src/pipes.js b/src/pipes.js index 7738dfb..279c800 100644 --- a/src/pipes.js +++ b/src/pipes.js @@ -68,18 +68,14 @@ function getWriter(req, res, result) { } } -const authorizePipe = (req, res, auth) => new Promise((resolve, reject) => { - if (auth !== undefined) { - if (!auth(req, res)) { +const authorizePipe = async (req, res, auth) => { + if (auth !== undefined && !await auth(req, res)) { const result = new Error(); result.status = 401; - reject(result); - return; - } + throw result; } - resolve(); -}); +}; const beforePipe = (req, res, before) => new Promise((resolve) => { if (before) { diff --git a/test/mocked/auth.action.js b/test/mocked/auth.action.js new file mode 100644 index 0000000..e44cc13 --- /dev/null +++ b/test/mocked/auth.action.js @@ -0,0 +1,83 @@ +import 'should'; +import request from 'supertest'; +import { odata, host, port, bookSchema } from '../support/setup'; +import data from '../support/books.json'; +import FakeDb from '../support/fake-db'; + +function requestToHalfPrice(id) { + return request(host).post(`/book(${id})/50off`); +} + +function halfPrice(price) { + return +(price / 2).toFixed(2); +} + +describe('odata.actions', () => { + let httpServer, server, db; + + beforeEach(async function () { + db = new FakeDb(); + server = odata(db); + }); + + afterEach(() => { + if (httpServer) { + httpServer.close(); + } + }); + + it('should work with boolean result', async function () { + server.action('salam-aleikum', (req, res, next) => { + res.jsonp({result: 'Wa aleikum assalam'}) + }, {auth: () => true}); + httpServer = server.listen(port); + + const res = await request(host).post(`/node.odata.salam-aleikum`); + + if (!res.ok) { + res.res.statusMessage.should.be.equal(''); + } + + res.body.result.should.be.equal('Wa aleikum assalam'); + }); + + + it('should fail without authorization', async function () { + server.action('salam-aleikum', (req, res, next) => { + res.jsonp({result: 'Wa aleikum assalam'}) + }, {auth: () => false}); + httpServer = server.listen(port); + + const res = await request(host).post(`/node.odata.salam-aleikum`); + + res.res.statusMessage.should.be.equal('Unauthorized'); + }); + + + it('should work with boolean result asynchron', async function () { + server.action('salam-aleikum', (req, res, next) => { + res.jsonp({result: 'Wa aleikum assalam'}) + }, {auth: async () => true}); + httpServer = server.listen(port); + + const res = await request(host).post(`/node.odata.salam-aleikum`); + + if (!res.ok) { + res.res.statusMessage.should.be.equal(''); + } + + res.body.result.should.be.equal('Wa aleikum assalam'); + }); + + + it('should fail without authorization asynchron', async function () { + server.action('salam-aleikum', (req, res, next) => { + res.jsonp({result: 'Wa aleikum assalam'}) + }, {auth: async () => false}); + httpServer = server.listen(port); + + const res = await request(host).post(`/node.odata.salam-aleikum`); + + res.res.statusMessage.should.be.equal('Unauthorized'); + }); +}); diff --git a/test/mocked/odata.actions.js b/test/mocked/odata.actions.js index 7981349..d4b008a 100644 --- a/test/mocked/odata.actions.js +++ b/test/mocked/odata.actions.js @@ -52,7 +52,11 @@ describe('odata.actions', () => { }) httpServer = server.listen(port); - const res = await request(host).post(`/salam-aleikum`); + const res = await request(host).post(`/node.odata.salam-aleikum`); + + if (!res.ok) { + res.res.statusMessage.should.be.equal(''); + } res.body.result.should.be.equal('Wa aleikum assalam'); }); From aef0109f243b66704441b52cae30a0b335a9186d Mon Sep 17 00:00:00 2001 From: Richard Martens Date: Tue, 9 May 2023 15:50:09 +0200 Subject: [PATCH 22/64] transform hooks to middleware --- .vscode/launch.json | 2 +- README.md | 80 ++++++++++++++++ src/Action.js | 36 +++++--- src/ODataResource.js | 5 +- src/middlewares/error.js | 8 ++ src/middlewares/writer.js | 83 +++++++++++++++++ src/pipes.js | 96 +------------------- src/rest/count.js | 6 +- src/rest/get.js | 5 +- src/rest/index.js | 55 ++++++++++- src/rest/list.js | 5 +- src/rest/patch.js | 5 +- src/rest/post.js | 5 +- src/rest/put.js | 10 +- src/server.js | 27 ++++-- src/spezialResources/Hooks.js | 44 +++++++++ src/spezialResources/ODataBatch.js | 78 ++++++++++------ src/spezialResources/ODataMetadata.js | 41 ++------- src/spezialResources/ODataServiceDocument.js | 38 ++------ src/writer/jsonWriter.js | 67 +------------- src/writer/multipartWriter.js | 3 +- src/writer/xmlWriter.js | 5 +- test/mocked/auth.action.js | 83 ----------------- test/mocked/hook.action.js | 96 ++++++++++++++++++++ test/mocked/metadata.action.js | 4 +- test/mocked/model.complex.action.js | 5 +- test/mocked/odata.actions.js | 18 +++- test/mocked/odata.batch.js | 10 +- test/mocked/odata.count.js | 2 +- test/mocked/rest.put.js | 2 +- 30 files changed, 540 insertions(+), 384 deletions(-) create mode 100644 src/middlewares/error.js create mode 100644 src/middlewares/writer.js create mode 100644 src/spezialResources/Hooks.js delete mode 100644 test/mocked/auth.action.js create mode 100644 test/mocked/hook.action.js diff --git a/.vscode/launch.json b/.vscode/launch.json index a5171f4..95ca1b9 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -20,7 +20,7 @@ "dot", "--timeout", "300000", - "test/mocked/odata.actions.js" + "test/mocked/rest.get.js" ] }, { diff --git a/README.md b/README.md index 8a296e0..c152af3 100644 --- a/README.md +++ b/README.md @@ -52,6 +52,86 @@ The odata constructor takes 3 arguments: ```odata(, , { + try { + res.$odata.status = 200; + await fn(req, res); + next(); + + } catch(err) { + next(err); + } + } + this.hooks = new Hooks(); if (options) { - this.auth = options.auth; this.binding = options.binding; this.resource = options.resource; - this.server = options.server; this.$Parameter = options.$Parameter; } } + addBefore(fn) { + this.hooks.addBefore(fn); + } + + addAfter(fn) { + this.hooks.addAfter(fn); + } + match(method, url) { const regex = this.getPath(true); @@ -32,7 +48,7 @@ export default class Action { const path = this.getPath(); - this.router = this.getOperationRouter(path, this.fn, this.auth); + this.router = this.getOperationRouter(path, this.fn); } return this.router; @@ -68,14 +84,10 @@ export default class Action { return path; } - getOperationRouter(path, fn, auth) { - const router = Router(); + getOperationRouter(path, fn) { + let router = Router(); - router.post(path, (req, res, next) => { - pipes.authorizePipe(req, res, auth) - .then(() => fn(req, res, next)) - .catch((result) => pipes.errorPipe(req, res, result)); - }); + router.post(path, ...this.hooks.before, fn, ...this.hooks.after); return router; }; diff --git a/src/ODataResource.js b/src/ODataResource.js index 178e95d..70fcb0e 100644 --- a/src/ODataResource.js +++ b/src/ODataResource.js @@ -59,11 +59,10 @@ export default class { this.actions[name] = new Action(name, fn, { ...options, - resource: this, - server: this._server + resource: this }); - return this; + return this.actions[name]; } maxTop(count) { diff --git a/src/middlewares/error.js b/src/middlewares/error.js new file mode 100644 index 0000000..00345a6 --- /dev/null +++ b/src/middlewares/error.js @@ -0,0 +1,8 @@ +import http from 'http'; + +export default function(err, req, res, next) { + debugger; + const status = err.status || 500; + const text = err.text || err.message || http.STATUS_CODES[status]; + res.status(status).send(text); +} \ No newline at end of file diff --git a/src/middlewares/writer.js b/src/middlewares/writer.js new file mode 100644 index 0000000..4378fc6 --- /dev/null +++ b/src/middlewares/writer.js @@ -0,0 +1,83 @@ +import XmlWriter from '../writer/xmlWriter'; +import JsonWirter from '../writer/jsonWriter'; +import MultipartWriter from '../writer/multipartWriter'; +import MimetypeParser from '../parser/mimetypeParser' + +const xmlWriter = new XmlWriter(); +const jsonWriter = new JsonWirter(); +const multipartWriter = new MultipartWriter(); + +function getContentType(req) { + if (req.headers && req.headers['content-type']) { + const result = req.headers['content-type']; + + return result.indexOf(';') > 0 ? result.split(';')[0] : result; + } +} + +function getWriter(req, res, result) { + const supportedFormats = res.$odata.supportedMimetypes; + let format = req.query.$format; + + let accept; + let requrestContentType; + if (req.headers) { + accept = req.headers.accept ? req.headers.accept : undefined; + requrestContentType = result.responses && getContentType(req) ? getContentType(req) : undefined; + } + + const mimetyeParser = new MimetypeParser(); + const mediaType = mimetyeParser.getmMediaType(format, accept, supportedFormats, requrestContentType); + + res.type(mediaType); + + // xml representation of metadata + switch (mediaType) { + case 'application/json': + return jsonWriter.writeJson.bind(jsonWriter); + + case 'application/xml': + return xmlWriter.writeXml.bind(xmlWriter); + + case 'multipart/mixed': + return multipartWriter.write.bind(multipartWriter); + + case 'text/plain': + return (res, data, status) => { + res.status(status).send(data); + }; + + default: + const error406 = new Error('Not acceptable'); + + error406.status = 406; + throw error406; + } +} + +export default async function writer(req, res) { + switch (res.$odata.status) { + case 404: + debugger; + // not found or no handler worked on + const err = new Error(); + err.status = 404; + throw err; + + case 204:// no content + res.status(res.$odata.status).end(); + return; + + case undefined: + throw new Error('Status not setted'); + + default: + } + + const status = res.$odata.status; + const writer = getWriter(req, res, res.$odata.result); + + res.setHeader('OData-Version', `4.0`); + writer(res, res.$odata.result, status, req.httpVersion); + +} \ No newline at end of file diff --git a/src/pipes.js b/src/pipes.js index 279c800..12a0fc4 100644 --- a/src/pipes.js +++ b/src/pipes.js @@ -1,72 +1,3 @@ -import http from 'http'; -import XmlWriter from './writer/xmlWriter'; -import JsonWirter from './writer/jsonWriter'; -import MultipartWriter from './writer/multipartWriter'; -import MimetypeParser from './parser/mimetypeParser' - -const xmlWriter = new XmlWriter(); -const jsonWriter = new JsonWirter(); -const multipartWriter = new MultipartWriter(); - -function getContentType(req) { - if (req.headers && req.headers['content-type']) { - const result = req.headers['content-type']; - - return result.indexOf(';') > 0 ? result.split(';')[0] : result; - } -} - -function getWriter(req, res, result) { - let supportedFormats; - let format = req.query.$format; - - if (typeof result !== 'object') { - supportedFormats = ['text/plain']; - } else if (result.$metadata) { - supportedFormats = ['application/xml', 'application/json']; - } else if (result.responses) { - supportedFormats = ['multipart/mixed', 'application/json']; - format = ''; - } else { - supportedFormats = ['application/json']; - } - - let accept; - let requrestContentType; - if (req.headers) { - accept = req.headers.accept ? req.headers.accept : undefined; - requrestContentType = result.responses && getContentType(req) ? getContentType(req) : undefined; - } - - const mimetyeParser = new MimetypeParser(); - const mediaType = mimetyeParser.getmMediaType(format, accept, supportedFormats, requrestContentType); - - res.type(mediaType); - - // xml representation of metadata - switch (mediaType) { - case 'application/json': - return jsonWriter.writeJson.bind(jsonWriter); - - case 'application/xml': - return xmlWriter.writeXml.bind(xmlWriter); - - case 'multipart/mixed': - return multipartWriter.write.bind(multipartWriter); - - case 'text/plain': - return (res, data, status, resolve) => { - res.status(status).send(data); - resolve(data); - }; - - default: - const error406 = new Error('Not acceptable'); - - error406.status = 406; - throw error406; - } -} const authorizePipe = async (req, res, auth) => { if (auth !== undefined && !await auth(req, res)) { @@ -84,23 +15,6 @@ const beforePipe = (req, res, before) => new Promise((resolve) => { resolve(); }); -const respondPipe = (req, res, result) => new Promise((resolve, reject) => { - try { - if (result.status === 204) { // no content - res.status(204).end(); - resolve(); - return; - } - - const status = result.status || 200; - const writer = getWriter(req, res, result); - - res.setHeader('OData-Version', `4.0`); - writer(res, result, status, resolve, req.httpVersion); - } catch (error) { - reject(error); - } -}); const afterPipe = (req, res, after, data) => new Promise((resolve) => { if (after) { @@ -109,16 +23,8 @@ const afterPipe = (req, res, after, data) => new Promise((resolve) => { resolve(); }); -const errorPipe = (req, res, err) => new Promise(() => { - const status = err.status || 500; - const text = err.text || err.message || http.STATUS_CODES[status]; - res.status(status).send(text); -}); - export default { afterPipe, authorizePipe, - beforePipe, - errorPipe, - respondPipe, + beforePipe }; diff --git a/src/rest/count.js b/src/rest/count.js index b149bc9..a92463e 100644 --- a/src/rest/count.js +++ b/src/rest/count.js @@ -11,7 +11,11 @@ export default async (req, MongooseModel, options) => { reject(result); } else { - resolve(count.toString()); + resolve({ + result: count.toString(), + status: 200, + supportedMimetypes: ['text/plain'] + }); } }); diff --git a/src/rest/get.js b/src/rest/get.js index b442b83..8fedb9b 100644 --- a/src/rest/get.js +++ b/src/rest/get.js @@ -11,6 +11,9 @@ export default (req, MongooseModel) => new Promise((resolve, reject) => { return reject(result); } - return resolve({ entity }); + return resolve({ + result: entity, + status: 200 + }); }); }); diff --git a/src/rest/index.js b/src/rest/index.js index c305ccc..c092f76 100644 --- a/src/rest/index.js +++ b/src/rest/index.js @@ -67,6 +67,45 @@ const getRoutes = (url, hooks) => { ]; }; +function replaceDot(value) { + if (!(value === null || value === undefined || typeof value === 'function')) { + if (Array.isArray(value)) { + return replaceDotinArray(value); + } + if (typeof value === 'object') { + return replaceObject(value); + } + } + + return value; +} + +function replaceDotinArray(array) { + const result = array; + + result.forEach((item, index) => { + result[index] = replaceDot(item); + }); + return result; +} + +function replaceObject(obj) { + const result = obj; + + Object.keys(result).forEach((item) => { + if (item.match(/^[^@][^.]+(\.[^.]+)+/)) { + const newPropertyName = item.replace('.', '-'); + + result[newPropertyName] = replaceDot(result[item]); + delete result[item]; + } else { + result[item] = replaceDot(result[item]); + } + }); + + return result; +} + const getMiddlewares = (url, hooks, mongooseModel, options) => { const routes = getRoutes(url, hooks); @@ -75,18 +114,24 @@ const getMiddlewares = (url, hooks, mongooseModel, options) => { ctrl, hook, } = route; - const middleware = async (req, res) => { + const middleware = async (req, res, next) => { try { await pipes.authorizePipe(req, res, hook.auth); await pipes.beforePipe(req, res, hook.before); const result = await ctrl(req, mongooseModel, options); - const data = await pipes.respondPipe(req, res, result || {}); - pipes.afterPipe(req, res, hook.after, data); + debugger; + res.$odata.result = result.result ? replaceDot(result.result) : result.result; + res.$odata.status = result.status || res.$odata.status; + res.$odata.supportedMimetypes = result.supportedMimetypes || res.$odata.supportedMimetypes; + + pipes.afterPipe(req, res, hook.after, res.$odata.result); + + next(); } catch (err) { - pipes.errorPipe(req, res, err); + next(err); } }; @@ -120,7 +165,7 @@ const getOperationRouter = (resourceUrl, actionUrl, fn, auth) => { router.post(`${resourceUrl}${actionUrl}`, (req, res, next) => { pipes.authorizePipe(req, res, auth) .then(() => fn(req, res, next)) - .catch((result) => pipes.errorPipe(req, res, result)); + .catch((result) => next(result)); }); return router; diff --git a/src/rest/list.js b/src/rest/list.js index f827397..b0c1a8a 100644 --- a/src/rest/list.js +++ b/src/rest/list.js @@ -51,7 +51,10 @@ export default (req, MongooseModel, options) => new Promise((resolve, reject) => _dataQuery(MongooseModel, params, options), ]).then((results) => { const entity = results.reduce((current, next) => ({ ...current, ...next })); - resolve({ entity }); + resolve({ + result: entity, + status: 200 + }); }).catch((err) => { const result = new Error(err.message); diff --git a/src/rest/patch.js b/src/rest/patch.js index 2663b61..5325631 100644 --- a/src/rest/patch.js +++ b/src/rest/patch.js @@ -7,7 +7,10 @@ export default (req, MongooseModel) => new Promise((resolve, reject) => { if (err1) { reject(err1); } else { - resolve({ entity: req.body, originEntity: entity }); + resolve({ + result: { ...entity, ...req.body }, + status: 200 + }); } }); } diff --git a/src/rest/post.js b/src/rest/post.js index ad63d59..3707e1a 100644 --- a/src/rest/post.js +++ b/src/rest/post.js @@ -11,7 +11,10 @@ export default (req, MongooseModel) => new Promise((resolve, reject) => { if (err) { reject(err); } else { - resolve({ status: 201, entity }); + resolve({ + status: 201, + result: entity + }); } }); } diff --git a/src/rest/put.js b/src/rest/put.js index 77a5970..df3bd2f 100644 --- a/src/rest/put.js +++ b/src/rest/put.js @@ -5,7 +5,10 @@ function _updateEntity(resolve, reject, MongooseModel, req, entity) { } const newEntity = req.body; newEntity.id = entity.id; - return resolve({ entity: newEntity, originEntity: entity }); + return resolve({ + result: newEntity, + status: 200 + }); }); } @@ -20,7 +23,10 @@ function _createEntity(resolve, reject, MongooseModel, req, entity) { if (err) { return reject(err); } - return resolve({ status: 201, entity: newEntity, originEntity: entity }); + return resolve({ + status: 201, + result: newEntity + }); }); } diff --git a/src/server.js b/src/server.js index d07abdf..6d7a662 100644 --- a/src/server.js +++ b/src/server.js @@ -6,6 +6,9 @@ import ServiceDocument from './spezialResources/ODataServiceDocument'; import Batch from './spezialResources/ODataBatch'; import Db from './db/db'; import Action from './Action'; +import error from './middlewares/error'; +import writer from './middlewares/writer'; +import Hooks from './spezialResources/Hooks'; function checkAuth(auth, req) { return !auth || auth(req); @@ -21,6 +24,20 @@ class Server { }; this.defaultConfiguration(db, prefix); + this.hooks = new Hooks(); + const dbValue = this.get('db'); + + this.hooks.addBefore(async (req, res) => { + req.$odata = { + mongo : dbValue._models + }; + res.$odata = { + status: 404, + supportedMimetypes: ['application/json'] + } + }); + this.hooks.addAfter(writer); + // TODO: Infact, resources is a mongooseModel instance, origin name is repositories. // Should mix _resources object and resources object: _resources + resource = resources. // Encapsulation to a object, separate mognoose, try to use *repository pattern*. @@ -108,13 +125,9 @@ class Server { } action(name, fn, options) { - this.actions[name] = new Action(name, fn, - { - ...options, - server: this - }); + this.actions[name] = new Action(name, fn, options); - return this; + return this.actions[name]; } _getRouter() { @@ -142,7 +155,7 @@ class Server { result.push(action.getRouter()); }); - return result; + return [...this.hooks.before, ...result, ...this.hooks.after, error]; } listen(...args) { diff --git a/src/spezialResources/Hooks.js b/src/spezialResources/Hooks.js new file mode 100644 index 0000000..6384338 --- /dev/null +++ b/src/spezialResources/Hooks.js @@ -0,0 +1,44 @@ +export default class Hooks { + constructor() { + this.before = []; + this.after = []; + } + + addBefore(fn) { + if (!fn) { + throw new Error(`Parameter 'fn' should be given`); + } + + if (Array.isArray(fn)) { + this.before = this.before.concat(fn.map(item => this.suppressNext(item))); + + } else { + this.before.push(this.suppressNext(fn)); + } + } + + suppressNext(fn) { + return async (req, res, next) => { + try { + await fn(req, res); + next(); + + } catch (err) { + next(err); + } + }; + } + + addAfter(fn) { + if (!fn) { + throw new Error(`Parameter 'fn' should be given`); + } + + if (Array.isArray(fn)) { + this.after = fn.map(item => this.suppressNext(item)).concat(this.after); + } else { + this.after.unshift(this.suppressNext(fn)); + } + } + +} \ No newline at end of file diff --git a/src/spezialResources/ODataBatch.js b/src/spezialResources/ODataBatch.js index ac8387d..05272f4 100644 --- a/src/spezialResources/ODataBatch.js +++ b/src/spezialResources/ODataBatch.js @@ -1,13 +1,11 @@ import { Router } from 'express'; -import pipes from '../pipes'; -import MimetypeParser from '../parser/mimetypeParser'; +import error from '../middlewares/error'; import { STATUS_CODES } from 'http'; +import writer from '../middlewares/writer'; export default class Batch { constructor(server) { this._server = server; - this._hooks = { - }; this._url = '/\\$batch'; } @@ -15,36 +13,20 @@ export default class Batch { return this; } - before(fn) { - this._hooks.before = fn; - return this; - } - - after(fn) { - this._hooks.after = fn; - return this; - } - - auth(fn) { - this._hooks.auth = fn; - return this; - } - - middleware = async (req, res) => { + middleware = async (req, res, next) => { try { - await pipes.authorizePipe(req, res, this._hooks.auth); - await pipes.beforePipe(req, res, this._hooks.before); - - const result = await this.ctrl(req, error => { + res.$odata.result = await this.ctrl(req, res, error => { if (error instanceof Error) { throw error; } }); - const data = await pipes.respondPipe(req, res, result || {}); + res.$odata.supportedMimetypes = ['multipart/mixed', 'application/json']; + res.$odata.status = 200; + + next(); - pipes.afterPipe(req, res, this._hooks.after, data); } catch (err) { - pipes.errorPipe(req, res, err); + next(err); } }; @@ -97,7 +79,43 @@ export default class Batch { }, {}); } - async ctrl(req, next) { + async executeSingleRequest(handler, req, res) { + try { + for (let i = 0; i < this._server.hooks.before.length; ++i) { + const hook = this._server.hooks.before[i]; + + await hook(req, res, err => { + if (err) { + throw err; + } + }); + } + + await handler(req, res, err => { + if (err) { + throw err; + } + }); + + + for(let i = 0; i < this._server.hooks.after.length; ++i) { + const hook = this._server.hooks.after[i]; + + if (hook !== error) { + await hook(req, res, err => { + if (err) { + throw err; + } + }); + } + } + + } catch (err) { + error(err, req, res); + } + } + + async ctrl(req, res, next) { const responses = req.body.requests.map(async function (request) { const handler = Object.keys(this._server.resources) .map((name) => this._server.resources[name].match(request.method, request.url)) @@ -113,6 +131,7 @@ export default class Batch { headers: request.headers, query: Batch.mapToQuery(request.url), body: request.body, + $odata: res.$odata }; const paramsMatch = request.url.match(/^\/[^#?(]+\('(\w+)'\)/); @@ -162,7 +181,8 @@ export default class Batch { }, }; - await handler(currentRequest, currentResponse, next); + await this.executeSingleRequest(handler, currentRequest, currentResponse); + } return result; }.bind(this)); diff --git a/src/spezialResources/ODataMetadata.js b/src/spezialResources/ODataMetadata.js index d3a6c46..1c55815 100644 --- a/src/spezialResources/ODataMetadata.js +++ b/src/spezialResources/ODataMetadata.js @@ -1,13 +1,10 @@ import { Router } from 'express'; -import pipes from '../pipes'; import Resource from '../ODataResource'; import Function from '../ODataFunction'; export default class Metadata { constructor(server) { this._server = server; - this._hooks = { - }; this._count = 0; this._path = '/\\$metadata'; } @@ -16,21 +13,6 @@ export default class Metadata { return this; } - before(fn) { - this._hooks.before = fn; - return this; - } - - after(fn) { - this._hooks.after = fn; - return this; - } - - auth(fn) { - this._hooks.auth = fn; - return this; - } - match(method, url) { if (method === 'get' && url.indexOf(this._path.replace(/\\/g, '')) === 0) { @@ -39,19 +21,17 @@ export default class Metadata { return undefined; } - middleware = async (req, res) => { + middleware = async (req, res, next) => { try { - await pipes.authorizePipe(req, res, this._hooks.auth); - await pipes.beforePipe(req, res, this._hooks.before); + res.$odata.result = await this.ctrl(req); + res.$odata.status = 200; + res.$odata.supportedMimetypes = ['application/xml', 'application/json']; + next(); - const result = await this.ctrl(req); - const data = await pipes.respondPipe(req, res, result || {}); - - pipes.afterPipe(req, res, this._hooks.after, data); - } catch (err) { - pipes.errorPipe(req, res, err); + } catch(err) { + next(err); } - }; + } _router() { /*eslint-disable */ @@ -316,10 +296,7 @@ export default class Metadata { }; return new Promise((resolve) => { - resolve({ - status: 200, - $metadata: document, - }); + resolve(document); }); } } diff --git a/src/spezialResources/ODataServiceDocument.js b/src/spezialResources/ODataServiceDocument.js index 4a3221c..c7e5f92 100644 --- a/src/spezialResources/ODataServiceDocument.js +++ b/src/spezialResources/ODataServiceDocument.js @@ -1,12 +1,9 @@ import { Router } from 'express'; -import pipes from '../pipes'; import Resource from '../ODataResource'; export default class Metadata { constructor(server) { this._server = server; - this._hooks = { - }; this._path = '/'; } @@ -14,21 +11,6 @@ export default class Metadata { return this; } - before(fn) { - this._hooks.before = fn; - return this; - } - - after(fn) { - this._hooks.after = fn; - return this; - } - - auth(fn) { - this._hooks.auth = fn; - return this; - } - match(methods, url) { if (methods === 'get' && url.indexOf(this._path) === 0) { @@ -37,17 +19,16 @@ export default class Metadata { return undefined; } - middleware = async (req, res) => { + middleware = async (req, res, next) => { try { - await pipes.authorizePipe(req, res, this._hooks.auth); - await pipes.beforePipe(req, res, this._hooks.before); - - const result = await this.ctrl(req); - const data = await pipes.respondPipe(req, res, result || {}); + res.$odata.result = await this.ctrl(req); + res.$odata.status = 200; + debugger; + + next(); - pipes.afterPipe(req, res, this._hooks.after, data); } catch (err) { - pipes.errorPipe(req, res, err); + next(err); } }; @@ -76,10 +57,7 @@ export default class Metadata { }; return new Promise((resolve) => { - resolve({ - status: 200, - serviceDocument: document, - }); + resolve(document); }); } } diff --git a/src/writer/jsonWriter.js b/src/writer/jsonWriter.js index 3873ae2..a7b0a3b 100644 --- a/src/writer/jsonWriter.js +++ b/src/writer/jsonWriter.js @@ -1,72 +1,13 @@ export default class { - writeJson(res, data, status, resolve) { - let normalizedData = data.entity; + writeJson(res, data, status) { + let normalizedData = data; - if (data.entity) { - if (data.entity.toObject) { - normalizedData = data.entity.toObject(); - } else if (Array.isArray(data.entity.value)) { - normalizedData = { - value: data.entity.value.map((item) => { - const result = item.toObject ? item.toObject() : item; - - return result; - }), - '@odata.count': data.entity['@odata.count'], - }; - } else if (data.entity.value) { - normalizedData = { - value: data.entity.value.toObject ? data.entity.value.toObject() : data.entity.value, - }; - } - normalizedData = this.replaceDot(normalizedData); - } else if (data.responses) { - normalizedData = data; - } else { - normalizedData = data.$metadata || data.serviceDocument; + if (data.toObject) { + normalizedData = data.toObject(); } res.type('application/json'); res.status(status).jsonp(normalizedData); - resolve(normalizedData); - } - - replaceDot(value) { - if (!(value === null || value === undefined || typeof value === 'function')) { - if (Array.isArray(value)) { - return this.replaceDotinArray(value); - } - if (typeof value === 'object') { - return this.replaceObject(value); - } - } - - return value; - } - - replaceDotinArray(array) { - const result = array; - - result.forEach((item, index) => { - result[index] = this.replaceDot(item); - }); - return result; } - replaceObject(obj) { - const result = obj; - - Object.keys(result).forEach((item) => { - if (item.match(/^[^@][^.]+(\.[^.]+)+/)) { - const newPropertyName = item.replace('.', '-'); - - result[newPropertyName] = this.replaceDot(result[item]); - delete result[item]; - } else { - result[item] = this.replaceDot(result[item]); - } - }); - - return result; - } } diff --git a/src/writer/multipartWriter.js b/src/writer/multipartWriter.js index 8f5113e..02756ee 100644 --- a/src/writer/multipartWriter.js +++ b/src/writer/multipartWriter.js @@ -1,5 +1,5 @@ export default class MultipartWriter { - write(res, result, status, resolve, httpVersion) { + write(res, result, status, httpVersion) { const boundary = 'batch_1'; let body = ''; @@ -23,6 +23,5 @@ export default class MultipartWriter { res.setHeader('content-type',`multipart/mixed;boundary=${boundary}`); res.send(Buffer.from(body)).status(status); - resolve(); } } \ No newline at end of file diff --git a/src/writer/xmlWriter.js b/src/writer/xmlWriter.js index 0682dc0..3063a9d 100644 --- a/src/writer/xmlWriter.js +++ b/src/writer/xmlWriter.js @@ -191,11 +191,10 @@ export default class XmlWriter { `); } - writeXml(res, data, status, resolve) { - const xml = this.visitor('document', data.$metadata, '', '').replace(/\s*\s*/g, '>'); + writeXml(res, data, status) { + const xml = this.visitor('document', data, '', '').replace(/\s*\s*/g, '>'); res.type('application/xml'); res.status(status).send(xml); - resolve(data); } } diff --git a/test/mocked/auth.action.js b/test/mocked/auth.action.js deleted file mode 100644 index e44cc13..0000000 --- a/test/mocked/auth.action.js +++ /dev/null @@ -1,83 +0,0 @@ -import 'should'; -import request from 'supertest'; -import { odata, host, port, bookSchema } from '../support/setup'; -import data from '../support/books.json'; -import FakeDb from '../support/fake-db'; - -function requestToHalfPrice(id) { - return request(host).post(`/book(${id})/50off`); -} - -function halfPrice(price) { - return +(price / 2).toFixed(2); -} - -describe('odata.actions', () => { - let httpServer, server, db; - - beforeEach(async function () { - db = new FakeDb(); - server = odata(db); - }); - - afterEach(() => { - if (httpServer) { - httpServer.close(); - } - }); - - it('should work with boolean result', async function () { - server.action('salam-aleikum', (req, res, next) => { - res.jsonp({result: 'Wa aleikum assalam'}) - }, {auth: () => true}); - httpServer = server.listen(port); - - const res = await request(host).post(`/node.odata.salam-aleikum`); - - if (!res.ok) { - res.res.statusMessage.should.be.equal(''); - } - - res.body.result.should.be.equal('Wa aleikum assalam'); - }); - - - it('should fail without authorization', async function () { - server.action('salam-aleikum', (req, res, next) => { - res.jsonp({result: 'Wa aleikum assalam'}) - }, {auth: () => false}); - httpServer = server.listen(port); - - const res = await request(host).post(`/node.odata.salam-aleikum`); - - res.res.statusMessage.should.be.equal('Unauthorized'); - }); - - - it('should work with boolean result asynchron', async function () { - server.action('salam-aleikum', (req, res, next) => { - res.jsonp({result: 'Wa aleikum assalam'}) - }, {auth: async () => true}); - httpServer = server.listen(port); - - const res = await request(host).post(`/node.odata.salam-aleikum`); - - if (!res.ok) { - res.res.statusMessage.should.be.equal(''); - } - - res.body.result.should.be.equal('Wa aleikum assalam'); - }); - - - it('should fail without authorization asynchron', async function () { - server.action('salam-aleikum', (req, res, next) => { - res.jsonp({result: 'Wa aleikum assalam'}) - }, {auth: async () => false}); - httpServer = server.listen(port); - - const res = await request(host).post(`/node.odata.salam-aleikum`); - - res.res.statusMessage.should.be.equal('Unauthorized'); - }); -}); diff --git a/test/mocked/hook.action.js b/test/mocked/hook.action.js new file mode 100644 index 0000000..a73dbd4 --- /dev/null +++ b/test/mocked/hook.action.js @@ -0,0 +1,96 @@ +import 'should'; +import 'should-sinon'; +import request from 'supertest'; +import { odata, host, port } from '../support/setup'; +import FakeDb from '../support/fake-db'; +import sinon from 'sinon'; + +function requestToHalfPrice(id) { + return request(host).post(`/book(${id})/50off`); +} + +function halfPrice(price) { + return +(price / 2).toFixed(2); +} + +describe('hook.action', () => { + let httpServer, server, db; + + beforeEach(async function () { + db = new FakeDb(); + server = odata(db); + }); + + afterEach(() => { + if (httpServer) { + httpServer.close(); + } + }); + + + it('should call fn and before hook', async function () { + const callbackFn = sinon.spy(); + const action = server.action('salam-aleikum', async (req, res) => { + callbackFn(); + res.$odata.result = { result: req.hook }; + }); + + const callbackHook = sinon.spy(); + action.addBefore(async (req, res) => { + req.hook = 'data drives'; + callbackHook(); + }); + httpServer = server.listen(port); + + const res = await request(host).post(`/node.odata.salam-aleikum`); + + res.body.should.deepEqual({result: 'data drives'}); + + callbackFn.should.be.callCount(1); + callbackHook.should.be.callCount(1); + }); + + it('should return error from before hook and not execute fn', async function () { + const callback = sinon.spy(); + const action = server.action('salam-aleikum', async (req, res) => { + callback(); + res.$odata.result = {result: 'authority check don`t works'}; + }); + + action.addBefore(async (req, res) => { + const error = new Error(); + + error.status = 401; + + throw error; + }); + httpServer = server.listen(port); + + const res = await request(host).post(`/node.odata.salam-aleikum`); + + res.res.statusMessage.should.be.equal('Unauthorized'); + callback.should.be.callCount(0); + }); + + it('should call fn and after hook', async function () { + const callbackFn = sinon.spy(); + const action = server.action('salam-aleikum', (req, res) => { + callbackFn(); + res.$odata.result = {result: 'data drives'}; + }); + + const callbackHook = sinon.spy(); + action.addAfter(async (req, res) => { + callbackHook(); + }); + httpServer = server.listen(port); + + const res = await request(host).post(`/node.odata.salam-aleikum`); + + res.body.should.deepEqual({result: 'data drives'}); + + callbackFn.should.be.callCount(1); + callbackHook.should.be.callCount(1); + }); + +}); diff --git a/test/mocked/metadata.action.js b/test/mocked/metadata.action.js index af3a2c7..dd62c09 100644 --- a/test/mocked/metadata.action.js +++ b/test/mocked/metadata.action.js @@ -184,11 +184,11 @@ describe('metadata.action', () => { it('should not accept action names with special characters', function() { try { - const resource = server.resource('book', { + const action = server.resource('book', { author: String }).action('/login', (req, res, next) => {}); - resource.actions['/login'].getRouter(); + action.getRouter(); throw new Error('Invalid name should not accepted'); diff --git a/test/mocked/model.complex.action.js b/test/mocked/model.complex.action.js index 3fc1642..300b317 100644 --- a/test/mocked/model.complex.action.js +++ b/test/mocked/model.complex.action.js @@ -16,7 +16,10 @@ describe('model.complex.action', () => { resource.action('all-item-greater', (req, res, next) => { const { price } = req.query; const $elemMatch = { price: { $gt: price } }; - server.resources.order.model.exec((err, data) => res.jsonp(data.slice(1))); + server.resources.order.model.exec((err, data) => { + res.$odata.result = data.slice(1); + res.$odata.status = 200; + }); }, { binding : 'collection' }); httpServer = server.listen(port); }); diff --git a/test/mocked/odata.actions.js b/test/mocked/odata.actions.js index d4b008a..f06eaa5 100644 --- a/test/mocked/odata.actions.js +++ b/test/mocked/odata.actions.js @@ -29,9 +29,9 @@ describe('odata.actions', () => { it('should work with bound action', async function () { server.resource('book', bookSchema) .action('50off', (req, res, next) => { - server.resources.book.model.findById(req.params.id, (err, book) => { + req.$odata.mongo.book.findById(req.params.id, (err, book) => { book.price = halfPrice(book.price); - book.save((err) => res.jsonp(book)); + book.save((err) => res.$odata.result = book); }); }, { binding: 'entity' @@ -48,7 +48,7 @@ describe('odata.actions', () => { it('should work with unbound action', async function () { server.action('salam-aleikum', (req, res, next) => { - res.jsonp({result: 'Wa aleikum assalam'}) + res.$odata.result = {result: 'Wa aleikum assalam'}; }) httpServer = server.listen(port); @@ -60,4 +60,16 @@ describe('odata.actions', () => { res.body.result.should.be.equal('Wa aleikum assalam'); }); + + it('should return 404 for action url without namespace', async function () { + server.action('salam-aleikum', (req, res, next) => { + res.$odata.result = {result: 'Wa aleikum assalam'}; + }) + httpServer = server.listen(port); + + const res = await request(host).post(`/salam-aleikum`); + + res.res.statusMessage.should.be.equal('Not Found'); + + }); }); diff --git a/test/mocked/odata.batch.js b/test/mocked/odata.batch.js index c5557e9..1e32278 100644 --- a/test/mocked/odata.batch.js +++ b/test/mocked/odata.batch.js @@ -11,12 +11,14 @@ describe('odata.batch', () => { beforeEach(async function () { const db = new FakeDb(); const server = odata(db); - resource = server.resource('book', bookSchema) - .action('entity-action', (req, res, next) => { - res.status(200).jsonp({result: 'Hello! I am an action, that bound to entity.'}) + resource = server.resource('book', bookSchema); + resource.action('entity-action', (req, res, next) => { + res.$odata.status = 200; + res.$odata.result = {result: 'Hello! I am an action, that bound to entity.'}; }, { binding: 'entity'}); server.action('unbound-action', (req, res, next) => { - res.status(200).jsonp({result: 'Hello! I am an unbound action.'}) + res.$odata.status = 200; + res.$odata.result = { result: 'Hello! I am an unbound action.'}; }) books = JSON.parse(JSON.stringify(db.addData('book', data))); httpServer = server.listen(port); diff --git a/test/mocked/odata.count.js b/test/mocked/odata.count.js index e6932b1..d478c7e 100644 --- a/test/mocked/odata.count.js +++ b/test/mocked/odata.count.js @@ -4,7 +4,7 @@ import { odata, host, port, bookSchema } from '../support/setup'; import books from '../support/books.json'; import FakeDb from '../support/fake-db'; -describe('odata.query.count', function() { +describe('odata.count', function() { let httpServer; before(async function() { diff --git a/test/mocked/rest.put.js b/test/mocked/rest.put.js index a53dd2e..6df40fe 100644 --- a/test/mocked/rest.put.js +++ b/test/mocked/rest.put.js @@ -19,7 +19,7 @@ describe('rest.put', () => { after(() => { httpServer.close(); }); - + it('should modify resource', async function() { const book = data[0]; book.title = 'modify book'; From 6e03530b146800955770609383086e26097e3cf0 Mon Sep 17 00:00:00 2001 From: Richard Martens Date: Wed, 10 May 2023 21:27:11 +0200 Subject: [PATCH 23/64] validation of action parameters --- .vscode/launch.json | 2 +- .vscode/settings.json | 1 + src/Action.js | 9 +- .../ODataBatch.js => odata/Batch.js} | 0 src/{spezialResources => odata}/Hooks.js | 0 .../ODataMetadata.js => odata/Metadata.js} | 2 +- .../ServiceDocument.js} | 0 src/odata/validator.js | 151 ++++++++++++++++++ src/server.js | 8 +- test/mocked/metadata.action.js | 4 +- 10 files changed, 166 insertions(+), 11 deletions(-) create mode 100644 .vscode/settings.json rename src/{spezialResources/ODataBatch.js => odata/Batch.js} (100%) rename src/{spezialResources => odata}/Hooks.js (100%) rename src/{spezialResources/ODataMetadata.js => odata/Metadata.js} (99%) rename src/{spezialResources/ODataServiceDocument.js => odata/ServiceDocument.js} (100%) create mode 100644 src/odata/validator.js diff --git a/.vscode/launch.json b/.vscode/launch.json index 95ca1b9..3bc785c 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -20,7 +20,7 @@ "dot", "--timeout", "300000", - "test/mocked/rest.get.js" + "test/mocked/metadata.action.js" ] }, { diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..9e26dfe --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1 @@ +{} \ No newline at end of file diff --git a/src/Action.js b/src/Action.js index e01a334..7fa8640 100644 --- a/src/Action.js +++ b/src/Action.js @@ -1,5 +1,6 @@ import { Router } from 'express'; -import Hooks from './spezialResources/Hooks'; +import Hooks from './odata/Hooks'; +import { validateParameters, validateIdentifier } from './odata/validator'; export default class Action { constructor(name, fn, options) { @@ -42,8 +43,10 @@ export default class Action { getRouter() { if (!this.router) { - if (!this.name || !this.name.match(/^^[_a-zA-Z0-9][_a-zA-Z0-9.-]*$/)) { - throw new Error(`Invalid action name '${this.name}'`); + validateIdentifier(this.name); + + if (this.$Parameter) { + validateParameters(this.$Parameter); } const path = this.getPath(); diff --git a/src/spezialResources/ODataBatch.js b/src/odata/Batch.js similarity index 100% rename from src/spezialResources/ODataBatch.js rename to src/odata/Batch.js diff --git a/src/spezialResources/Hooks.js b/src/odata/Hooks.js similarity index 100% rename from src/spezialResources/Hooks.js rename to src/odata/Hooks.js diff --git a/src/spezialResources/ODataMetadata.js b/src/odata/Metadata.js similarity index 99% rename from src/spezialResources/ODataMetadata.js rename to src/odata/Metadata.js index 1c55815..431db84 100644 --- a/src/spezialResources/ODataMetadata.js +++ b/src/odata/Metadata.js @@ -177,7 +177,7 @@ export default class Metadata { const item = para; if (para.$Type.search(/^edm/i) === -1 ) { - item.$Type = `node.odata.${para.$Type}`; + item.$Type = `${para.$Type}`; } result.$Parameter.push(item); diff --git a/src/spezialResources/ODataServiceDocument.js b/src/odata/ServiceDocument.js similarity index 100% rename from src/spezialResources/ODataServiceDocument.js rename to src/odata/ServiceDocument.js diff --git a/src/odata/validator.js b/src/odata/validator.js new file mode 100644 index 0000000..6168f71 --- /dev/null +++ b/src/odata/validator.js @@ -0,0 +1,151 @@ + +export const validateIdentifier = (identifier) => { + if (!identifier || !identifier.match(/^^[_a-zA-Z0-9][_a-zA-Z0-9.-]*$/)) { + throw new Error(`Invalid simple identifier '${identifier}'`); + } +} + + +function shouldContains(property, member, list) { + if (property[member] && list.indexOf(property[member]) === -1 + && (!property[member].match(/node\.odata/) || member != '$Type')) {// custom type + debugger; + throw new Error(`${member} '${property[member]}' is invalid`); + } +} + +function validateType(property, member) { + const types = ['Edm.Binary', 'Edm.Boolean', 'Edm.Byte', 'Edm.Date', + 'Edm.DateTimeOffset', 'Edm.Decimal', 'Edm.Double', 'Edm.Duration', 'Edm.Guid', + 'Edm.Int16', 'Edm.Int32', 'Edm.Int64', 'Edm.SByte', 'Edm.Single', + 'Edm.Stream', 'Edm.String', 'Edm.TimeOfDay', 'Edm.Geography', 'Edm.GeographyPoint', + 'Edm.GeographyLineString', 'Edm.GeographyPolygon', 'Edm.GeographyMultiPoint', 'Edm.GeographyMultiLineString', + 'Edm.GeographyMultiPolygon', 'Edm.GeographyCollection', 'Edm.Geometry', 'Edm.GeometryPoint', 'Edm.GeometryLineString', + 'Edm.GeometryPolygon', 'Edm.GeometryMultiPoint', 'Edm.GeometryMultiLineString', 'Edm.GeometryMultiPolygon', + 'Edm.GeometryCollection']; + + shouldContains(property, member.trim(), types); + +} + +function validateSRID(name, property) { + if (!property[name]) { + throw new Error(`If SRID is given, then the value had to be supplied`); + } + if (property[name] !== 'variable') { + const srid = +(property[name]); + + if (Number.isNaN(srid)) { + throw new Error(`'${srid}' is invalid value for SRID of ${name} property`); + } + if (srid < 0) { + throw new Error(`SRID has to be a non negative value. Current '${srid}'`); + } + } +} + +function validateMaxLength(name, property, member) { + const maxLength = property[name].member; + + if (!maxLength) { + throw new Error(`If MaxLength is given, than the value had to be supplied`); + } + + if (Number.isNaN(+maxLength)) { + throw new Error(`Value '${maxLength}' is invalid for MaxLength`); + } + + if (+maxLength < 1) { + throw new Error(`If MaxLength is given, than the value had to be supplied`) + } + +} + +export const validateProperty = (name, property) => { + validateIdentifier(name); + + const members = Object.keys(property); + + members.forEach(member => { + const allowedMembers = ['$Type', '$Collection', '$Nullable', '$MaxLength', + '$Unicode', '$Precision', '$Scale', '$SRID', '$DefaultValue']; + + switch (member.trim()) { + case '$Type': + validateType(property, member); + break; + + case '$Collection': + case '$Nullable': + case '$Unicode': + const boolean = [true, false]; + + shouldContains(property[name], member.trim(), boolean); + break; + + case '$SRID': + validateSRID(name, property); + break; + + case '$MaxLength': + validateMaxLength(name, property, member); + break; + + case '$DefaultValue': + break; + + default: + throw new Error(`'${member.trim()}' ist not allowed as member of property '${name}'`); + } + }); +} + +const validateParameter = parameter => { + if (!parameter) { + throw new Error('Parameter should not be undefined'); + } + + if (!parameter.$Name) { + throw new Error('$Name of Parameter should be given'); + } + + const clone = JSON.parse(JSON.stringify(parameter)); + + delete clone.$Name; + + validateProperty(parameter.$Name, clone); +} + +export const validateParameters = parameter => { + if (!parameter) { + throw new Error('Parameter should not be undefined or null'); + } + + if (!Array.isArray(parameter)) { + throw new Error('Parameter should be an array of parameters'); + } + + parameter.forEach(item => validateParameter(item)); +} + +function validateComplexType(node) { + const properties = Object.keys(node); + + properties.filter(name => name !== '$Kind') + .forEach(name => validateProperty(name, node[name])); +} + +export const validate = node => { + if (!node) { + throw new Error('For validation an object should not be undefined'); + } + + switch(node.$Kind) { + case 'ComplexType': + validateComplexType(node); + break; + + default: + throw new Error('For validation an object need a property $Kind'); + } +} \ No newline at end of file diff --git a/src/server.js b/src/server.js index 6d7a662..50ae894 100644 --- a/src/server.js +++ b/src/server.js @@ -1,14 +1,14 @@ import createExpress from './express'; import Resource from './ODataResource'; import Func from './ODataFunction'; -import Metadata from './spezialResources/ODataMetadata'; -import ServiceDocument from './spezialResources/ODataServiceDocument'; -import Batch from './spezialResources/ODataBatch'; +import Metadata from './odata/Metadata'; +import ServiceDocument from './odata/ServiceDocument'; +import Batch from './odata/Batch'; import Db from './db/db'; import Action from './Action'; import error from './middlewares/error'; import writer from './middlewares/writer'; -import Hooks from './spezialResources/Hooks'; +import Hooks from './odata/Hooks'; function checkAuth(auth, req) { return !auth || auth(req); diff --git a/test/mocked/metadata.action.js b/test/mocked/metadata.action.js index dd62c09..ba1d23f 100644 --- a/test/mocked/metadata.action.js +++ b/test/mocked/metadata.action.js @@ -193,7 +193,7 @@ describe('metadata.action', () => { throw new Error('Invalid name should not accepted'); } catch(error) { - error.message.should.equal(`Invalid action name '/login'`); + error.message.should.equal(`Invalid simple identifier '/login'`); } }); @@ -243,7 +243,7 @@ describe('metadata.action', () => { (req, res, next) => {}, { $Parameter: [{ $Name: 'book', - $Type: 'book' + $Type: 'node.odata.book' }] }); httpServer = server.listen(port); From fcc2f02c0ace31dd1555fc025cfe004cae5a29e0 Mon Sep 17 00:00:00 2001 From: Richard Martens Date: Fri, 12 May 2023 21:46:52 +0200 Subject: [PATCH 24/64] Actions dokumented --- README.md | 95 ++++++++++++++++++++++++++++++++++++++++--------------- 1 file changed, 70 insertions(+), 25 deletions(-) diff --git a/README.md b/README.md index c152af3..035ffe2 100644 --- a/README.md +++ b/README.md @@ -58,44 +58,80 @@ The options object currently only supports one parameter: ```expressRequestLimit ### Unbound Actions -Unbound Action will be defined over server directly. The interface of the passed function must correspond to the nodejs express middleware. In order for the after-hooks to be called, you should call the next callback. The errors are also passed on via the next callback. +Unbound Action will be defined over server directly. ``` -server.action('login', async function(req, res, next) { +server.action('login', async function(req, res) { // in req.$odata.mongo is your db instance - try { - const user = await req.$odata.mongo.user.findOne({ - email: req.body.email - }); - - next(); - } catch(err) { - next(err); - } + res.$odata.result = await req.$odata.mongo.user.findOne({ + email: req.body.email + }); }); ``` +Calling an unbound action + +``` +POST /node.odata.login +``` + +### Bound Actions + +Bound Action are defined over resource. An action can be bound to single resource or to collection of resources. + +#### Entity Actions + +``` +resource.action('bound-action', (req, res) => { + ... +}, { binding: 'entity' }); +``` + +will be called + +``` +POST /book('01234')/bound-action +``` + +#### Collection Actions + +``` +resource.action('bound-action', (req, res) => { + ... +}, { binding: 'collection' }); +``` + +will be called + +``` +POST /book/bound-action +``` + ### Implementation of an action -The interface of the passed function must correspond to the nodejs express middleware. In order for the after-hooks to be called, you should call the next callback. The errors are also passed on via the next callback. +The interface of the passed function must correspond to the nodejs express middleware. You should assign the result to the res.$odata.result attribute. An error can be thrown and it can contain the status attribute. ``` -server.action('login', async function(req, res, next) { +server.action('login', async function(req, res) { // in req.$odata.mongo is your db instance - try { - const user = await req.$odata.mongo.user.findOne({ - email: req.body.email - }); - next(); + res.$odata.result = { + user: await req.$odata.mongo.user.findOne({ + email: req.body.email + }) + } - } catch(err) { - next(err); - } + if (!res.$odata.result) { + const err = new Error('Login failed'); + + err.status = 403; + throw err; + } }); +``` ### Parameter @@ -118,17 +154,26 @@ server.action('login', async function(req, res, next) { ### Hooks -It is possible to specify nodejs express middlewares for the actions to be performed before or after the action. +It is possible to specify nodejs express middlewares for the actions to be performed before or after the action. Any data assigned to req.$odata or res.$odata will be available on action implementation and subsequent hooks. An error thrown in the hook interrupts further processing. ``` const action = server.action('login', ...); -action.addBefore(function(req, res, next) { +action.addBefore(async (req, res) => { ... - next(); + res.$odata.result = { result: 'any' }; // client receives: { result: 'any' } +}); + +action.addBefore(async (req, res) => { + if (!req.user) { + const err = new Error(); + + err.status = 401; + throw err; + } }); -action.addAfter(function(req, res, next) { +action.addAfter(async (req, res) => { ... }); ``` From c1f70c91e5cd2c72c93fbe82604ff92d7ddea0c4 Mon Sep 17 00:00:00 2001 From: Richard Martens Date: Mon, 15 May 2023 16:00:20 +0200 Subject: [PATCH 25/64] Actions documented --- README.md | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 035ffe2..1afb835 100644 --- a/README.md +++ b/README.md @@ -79,7 +79,7 @@ POST /node.odata.login ### Bound Actions -Bound Action are defined over resource. An action can be bound to single resource or to collection of resources. +Bound Action are defined over resource. An action can be bound to single resource or to collection of resources. For the bound action, the first parameter of the bound resource type is specified in the metadata. #### Entity Actions @@ -151,6 +151,17 @@ server.action('login', async function(req, res, next) { }] }); ``` +The following attributes can be specified for parameters: + +| Attribute | Type | Possible Values | +|---------------------------------------------------| +| $Type | string | Build-In Types(Edm.*) or custom defined types(node.odata.*) | +| $Collection | boolean | true/false | +| $Nullable | boolean | true/false | +| $MaxLength | number | Number bigger than zero | +| $DefaultValue | string | Any text | +| $Unicode | boolean | true/false | +| $SRID | number | Not negative Number | ### Hooks From ca2e24dc2f1c3eb5d90719526152910287a96f01 Mon Sep 17 00:00:00 2001 From: Richard Martens Date: Mon, 15 May 2023 21:41:29 +0200 Subject: [PATCH 26/64] Actions dokumentet --- README.md | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index 1afb835..f0ad7d4 100644 --- a/README.md +++ b/README.md @@ -153,15 +153,14 @@ server.action('login', async function(req, res, next) { ``` The following attributes can be specified for parameters: -| Attribute | Type | Possible Values | -|---------------------------------------------------| -| $Type | string | Build-In Types(Edm.*) or custom defined types(node.odata.*) | -| $Collection | boolean | true/false | -| $Nullable | boolean | true/false | -| $MaxLength | number | Number bigger than zero | -| $DefaultValue | string | Any text | -| $Unicode | boolean | true/false | -| $SRID | number | Not negative Number | +- $Type Build-In Types(Edm.\*) or custom defined types(node.odata.*) +- $Collection true/false +- $Nullable true/false +- $MaxLength Number bigger than zero +- $DefaultValue any text +- $Unicode true/false +- $SRID not negative Number + ### Hooks From de13b07d10bf6dbf8da7e034bd7a00d48eb94c8f Mon Sep 17 00:00:00 2001 From: Richard Martens Date: Tue, 6 Jun 2023 21:44:48 +0200 Subject: [PATCH 27/64] debug tracing implemented --- .vscode/launch.json | 7 ++- README.md | 12 ++-- src/Action.js | 28 ++++++--- src/db/model.js | 24 ++++---- src/middlewares/error.js | 17 +++++- src/middlewares/writer.js | 1 - src/odata/Hooks.js | 27 ++++++--- src/odata/Metadata.js | 57 +++++++++++------- src/odata/ServiceDocument.js | 1 - src/odata/validator.js | 1 - src/rest/index.js | 8 ++- src/server.js | 8 ++- src/writer/Console.js | 48 +++++++++++++++ test/mocked/hook.action.js | 34 +++++++++-- test/mocked/metadata.complex.type.js | 88 ++++++++++++++++++++++++++++ test/mocked/model.complex.action.js | 3 +- test/mocked/odata.actions.js | 9 ++- test/mocked/odata.batch.js | 6 +- test/mocked/odata.error.js | 83 ++++++++++++++++++++++++++ 19 files changed, 385 insertions(+), 77 deletions(-) create mode 100644 src/writer/Console.js create mode 100644 test/mocked/metadata.complex.type.js create mode 100644 test/mocked/odata.error.js diff --git a/.vscode/launch.json b/.vscode/launch.json index 3bc785c..c539ac6 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -20,8 +20,11 @@ "dot", "--timeout", "300000", - "test/mocked/metadata.action.js" - ] + "test/mocked/odata.error.js" + ], + "env": { + "LOG_LEVEL": "debug" + } }, { "type": "node", diff --git a/README.md b/README.md index f0ad7d4..70cde52 100644 --- a/README.md +++ b/README.md @@ -118,7 +118,7 @@ server.action('login', async function(req, res) { // in req.$odata.mongo is your db instance res.$odata.result = { - user: await req.$odata.mongo.user.findOne({ + user: await req.$odata.mongo.user.findOne({ email: req.body.email }) } @@ -164,7 +164,7 @@ The following attributes can be specified for parameters: ### Hooks -It is possible to specify nodejs express middlewares for the actions to be performed before or after the action. Any data assigned to req.$odata or res.$odata will be available on action implementation and subsequent hooks. An error thrown in the hook interrupts further processing. +It is possible to specify nodejs express middlewares for the actions to be performed before or after the action. Any data assigned to req.$odata or res.$odata will be available on action implementation and subsequent hooks. An error thrown in the hook interrupts further processing. it is possible to provide a name of hook for tracing. ``` const action = server.action('login', ...); @@ -172,7 +172,7 @@ const action = server.action('login', ...); action.addBefore(async (req, res) => { ... res.$odata.result = { result: 'any' }; // client receives: { result: 'any' } -}); +}, 'name-of-hook'); action.addBefore(async (req, res) => { if (!req.user) { @@ -185,9 +185,13 @@ action.addBefore(async (req, res) => { action.addAfter(async (req, res) => { ... -}); +}, 'name-of-hook'); ``` +## Loging + +In the event of an unexpected error, no meaningful error message is returned to the frontend. This is necessary to make it harder for hackers. However, the development will not be easy either. For this reason there are additional logging routines. Logging can be switched on and off by setting the log level. To do this, you would have to set the environment variable ```LOG_LEVEL``` to the value ```debug```. In this case, messages that are still not meaningful are sent to the frontend, but the exception objects are logged in the log files. In addition, the start of processing of each resource and each hook is also logged. + ## Current State node-odata is currently at an beta stage, it is stable but not 100% feature complete. diff --git a/src/Action.js b/src/Action.js index 7fa8640..be0974e 100644 --- a/src/Action.js +++ b/src/Action.js @@ -1,6 +1,7 @@ import { Router } from 'express'; import Hooks from './odata/Hooks'; import { validateParameters, validateIdentifier } from './odata/validator'; +import Console from './writer/Console'; export default class Action { constructor(name, fn, options) { @@ -8,12 +9,23 @@ export default class Action { this.name = name; this.fn = async (req, res, next) => { try { + const con = new Console(); + + con.debug(`Action ${this.name} started`); + res.$odata.status = 200; - await fn(req, res); - next(); + const result = fn(req, res, next); - } catch(err) { - next(err); + if (result || res.$odata.status === 204) { + if (result.then) { + await result; + } + + next(); + } + + } catch (error) { + next(error); } } this.hooks = new Hooks(); @@ -25,12 +37,12 @@ export default class Action { } } - addBefore(fn) { - this.hooks.addBefore(fn); + addBefore(fn, name) { + this.hooks.addBefore(fn, name); } - addAfter(fn) { - this.hooks.addAfter(fn); + addAfter(fn, name) { + this.hooks.addAfter(fn, name); } match(method, url) { diff --git a/src/db/model.js b/src/db/model.js index 0c19bf9..ae636d5 100644 --- a/src/db/model.js +++ b/src/db/model.js @@ -8,27 +8,31 @@ export default class { return new MongooseModel(data); } + async countDocuments() { + return await this.model.countDocuments(); + } + find() { return this.model.find(); } - findById(id, callback) { - this.model.findById(id, callback); + async findById(id, callback) { + return await this.model.findById(id, callback); } - findByIdAndUpdate(id, data, callback) { - this.model.findByIdAndUpdate(id, data, callback); + async findByIdAndUpdate(id, data, callback) { + return await this.model.findByIdAndUpdate(id, data, callback); } - findOne(filter, callback) { - this.model.findOne(filter, callback); + async findOne(filter, callback) { + return await this.model.findOne(filter, callback); } - remove(filter, callback) { - this.model.remove(filter, callback); + async remove(filter, callback) { + return await this.model.remove(filter, callback); } - update(filter, data, callback) { - this.model.update(filter, data, callback); + async update(filter, data, callback) { + return await this.model.update(filter, data, callback); } } diff --git a/src/middlewares/error.js b/src/middlewares/error.js index 00345a6..ec65135 100644 --- a/src/middlewares/error.js +++ b/src/middlewares/error.js @@ -1,8 +1,19 @@ import http from 'http'; +import Console from '../writer/Console'; export default function(err, req, res, next) { - debugger; const status = err.status || 500; - const text = err.text || err.message || http.STATUS_CODES[status]; - res.status(status).send(text); + const result = { + error: { + code: status.toString(), + message: status < 500 ? err.message || http.STATUS_CODES[status] : http.STATUS_CODES[status] + } + + }; + if (status >= 500) { + const cons = new Console(); + + cons.log(err); + } + res.status(status).jsonp(result); } \ No newline at end of file diff --git a/src/middlewares/writer.js b/src/middlewares/writer.js index 4378fc6..4dfacac 100644 --- a/src/middlewares/writer.js +++ b/src/middlewares/writer.js @@ -58,7 +58,6 @@ function getWriter(req, res, result) { export default async function writer(req, res) { switch (res.$odata.status) { case 404: - debugger; // not found or no handler worked on const err = new Error(); err.status = 404; diff --git a/src/odata/Hooks.js b/src/odata/Hooks.js index 6384338..85fe4f6 100644 --- a/src/odata/Hooks.js +++ b/src/odata/Hooks.js @@ -1,27 +1,36 @@ +import Console from "../writer/Console"; + export default class Hooks { constructor() { this.before = []; this.after = []; } - addBefore(fn) { + addBefore(fn, name) { if (!fn) { throw new Error(`Parameter 'fn' should be given`); } if (Array.isArray(fn)) { - this.before = this.before.concat(fn.map(item => this.suppressNext(item))); + this.before = this.before.concat(fn.map(item => this.suppressNext(item, name))); } else { - this.before.push(this.suppressNext(fn)); + this.before.push(this.suppressNext(fn, name)); } } - suppressNext(fn) { + suppressNext(fn, name) { return async (req, res, next) => { try { - await fn(req, res); - next(); + const con = new Console(); + + con.debug(`Hook ${name} started`); + + const prom = fn(req, res, next); + if (prom && prom.then) { + await prom; + next(); + } } catch (err) { next(err); @@ -29,15 +38,15 @@ export default class Hooks { }; } - addAfter(fn) { + addAfter(fn, name) { if (!fn) { throw new Error(`Parameter 'fn' should be given`); } if (Array.isArray(fn)) { - this.after = fn.map(item => this.suppressNext(item)).concat(this.after); + this.after = fn.map(item => this.suppressNext(item, name)).concat(this.after); } else { - this.after.unshift(this.suppressNext(fn)); + this.after.unshift(this.suppressNext(fn, name)); } } diff --git a/src/odata/Metadata.js b/src/odata/Metadata.js index 431db84..c71458e 100644 --- a/src/odata/Metadata.js +++ b/src/odata/Metadata.js @@ -1,12 +1,31 @@ import { Router } from 'express'; import Resource from '../ODataResource'; import Function from '../ODataFunction'; +import { validate, validateIdentifier } from './validator'; export default class Metadata { constructor(server) { this._server = server; this._count = 0; this._path = '/\\$metadata'; + + this.complexTypes = {}; + } + + complexType(name, properties) { + if (this.complexTypes[name]) { + throw new Error(`Complex type with name ${name} allready exists`); + } + + validateIdentifier(name); + + const typeObject = { + $Kind: 'ComplexType', + ...properties + }; + validate(typeObject); + + this.complexTypes[name] = typeObject; } get() { @@ -42,7 +61,7 @@ export default class Metadata { return router; } - visitProperty(node, model, root) { + visitProperty(node, model) { const result = {}; if (model.default) { @@ -80,11 +99,11 @@ export default class Metadata { const notClassifiedName = `${node.path}Child${this._count}`; // Array of complex type result.$Type = `node.odata.${notClassifiedName}`; - root(notClassifiedName, this.visitor('ComplexType', node.schema.paths, model[0], root)); + this.complexType(notClassifiedName, this.visitor('ComplexType', node.schema.paths, model[0])); } else { const arrayItemType = this.visitor('Property', { instance: node.options.type[0].name || node.options.type[0].type.name //Enums have an object with enum and type - }, model[0], root); + }, model[0]); result.$Type = arrayItemType.$Type; } @@ -109,7 +128,7 @@ export default class Metadata { return model[property]; } - visitEntityType(node, model, root) { + visitEntityType(node, model) { const properties = Object.keys(node) .filter((path) => path !== '_id') .reduce((previousProperty, curentProperty) => { @@ -117,7 +136,7 @@ export default class Metadata { const propertyName = curentProperty.replace(/\./g, '-'); const result = { ...previousProperty, - [propertyName]: this.visitor('Property', node[curentProperty], modelProperty, root), + [propertyName]: this.visitor('Property', node[curentProperty], modelProperty), }; return result; @@ -134,7 +153,7 @@ export default class Metadata { }; } - visitComplexType(node, model, root) { + visitComplexType(node, model) { const properties = Object.keys(node) .filter((item) => item !== '_id') .reduce((previousProperty, curentProperty) => { @@ -142,16 +161,13 @@ export default class Metadata { const modelProperty = this.resolveModelproperty(model, curentProperty); const result = { ...previousProperty, - [propertyName]: this.visitor('Property', node[curentProperty], modelProperty, root), + [propertyName]: this.visitor('Property', node[curentProperty], modelProperty), }; return result; }, {}); - return { - $Kind: 'ComplexType', - ...properties, - }; + return properties; } static visitAction(node) { @@ -195,22 +211,22 @@ export default class Metadata { }; } - visitor(type, node, model, root) { + visitor(type, node, model) { switch (type) { case 'Property': - return this.visitProperty(node, model, root); + return this.visitProperty(node, model); case 'ComplexType': - return this.visitComplexType(node, model, root); + return this.visitComplexType(node, model); case 'Action': return Metadata.visitAction(node); case 'Function': - return Metadata.visitFunction(node, root); + return Metadata.visitFunction(node); default: - return this.visitEntityType(node, model, root); + return this.visitEntityType(node, model); } } @@ -219,20 +235,19 @@ export default class Metadata { const entityTypes = entityTypeNames.reduce((previousResource, currentResource) => { const resource = this._server.resources[currentResource]; const result = { ...previousResource }; - const attachToRoot = (name, value) => { result[name] = value; }; if (resource instanceof Resource) { const { paths } = resource.model.model.schema; - result[currentResource] = this.visitor('EntityType', paths, resource._model, attachToRoot); + result[currentResource] = this.visitor('EntityType', paths, resource._model); const actions = Object.keys(resource.actions); if (actions && actions.length) { actions.forEach((action) => { - result[action] = this.visitor('Action', resource.actions[action], attachToRoot); + result[action] = this.visitor('Action', resource.actions[action]); }); } } else if (resource instanceof Function) { - result[currentResource] = this.visitor('Function', resource, attachToRoot); + result[currentResource] = this.visitor('Function', resource); } return result; @@ -260,7 +275,6 @@ export default class Metadata { const actionNames = Object.keys(this._server.actions); const actionImports = actionNames.reduce((previousAction, currentAction) => { const result = {...previousAction}; - const action = this._server.actions[currentAction]; result[`${currentAction}-import`] = { $Action: `node.odata.${currentAction}` @@ -285,6 +299,7 @@ export default class Metadata { $UnderlyingType: 'Edm.String', $MaxLength: 24, }, + ...this.complexTypes, ...entityTypes, ...unboundActions, $EntityContainer: 'node.odata', diff --git a/src/odata/ServiceDocument.js b/src/odata/ServiceDocument.js index c7e5f92..dc274c9 100644 --- a/src/odata/ServiceDocument.js +++ b/src/odata/ServiceDocument.js @@ -23,7 +23,6 @@ export default class Metadata { try { res.$odata.result = await this.ctrl(req); res.$odata.status = 200; - debugger; next(); diff --git a/src/odata/validator.js b/src/odata/validator.js index 6168f71..d4fd0eb 100644 --- a/src/odata/validator.js +++ b/src/odata/validator.js @@ -9,7 +9,6 @@ export const validateIdentifier = (identifier) => { function shouldContains(property, member, list) { if (property[member] && list.indexOf(property[member]) === -1 && (!property[member].match(/node\.odata/) || member != '$Type')) {// custom type - debugger; throw new Error(`${member} '${property[member]}' is invalid`); } } diff --git a/src/rest/index.js b/src/rest/index.js index c092f76..3731a09 100644 --- a/src/rest/index.js +++ b/src/rest/index.js @@ -7,6 +7,7 @@ import patch from './patch'; import get from './get'; import pipes from '../pipes'; import count from './count'; +import Console from '../writer/Console'; const getRoutes = (url, hooks) => { const resourceListURL = `/${url}`; @@ -111,17 +112,20 @@ const getMiddlewares = (url, hooks, mongooseModel, options) => { return routes.map((route) => { const { - ctrl, hook, + ctrl, hook, method, url } = route; const middleware = async (req, res, next) => { try { + const con = new Console(); + + con.debug(`resource handler for ${method} ${url} started`); + await pipes.authorizePipe(req, res, hook.auth); await pipes.beforePipe(req, res, hook.before); const result = await ctrl(req, mongooseModel, options); - debugger; res.$odata.result = result.result ? replaceDot(result.result) : result.result; res.$odata.status = result.status || res.$odata.status; res.$odata.supportedMimetypes = result.supportedMimetypes || res.$odata.supportedMimetypes; diff --git a/src/server.js b/src/server.js index 50ae894..49ccf38 100644 --- a/src/server.js +++ b/src/server.js @@ -35,8 +35,8 @@ class Server { status: 404, supportedMimetypes: ['application/json'] } - }); - this.hooks.addAfter(writer); + }, 'service-initialization'); + this.hooks.addAfter(writer, 'writer'); // TODO: Infact, resources is a mongooseModel instance, origin name is repositories. // Should mix _resources object and resources object: _resources + resource = resources. @@ -158,6 +158,10 @@ class Server { return [...this.hooks.before, ...result, ...this.hooks.after, error]; } + complexType(name, properties) { + this.resources.$metadata.complexType(name, properties); + } + listen(...args) { const router = this._getRouter(); diff --git a/src/writer/Console.js b/src/writer/Console.js new file mode 100644 index 0000000..157b066 --- /dev/null +++ b/src/writer/Console.js @@ -0,0 +1,48 @@ +export default class Console { + constructor(settings) { + const logLevel = settings && settings.logLevel || process.env.LOG_LEVEL || 'error'; + + switch (logLevel) { + case 'debug': + this.logLevel = 40; + break; + + case 'info': + this.logLevel = 30; + break; + + case 'warning': + this.logLevel = 20; + break; + + case 'error': + this.logLevel = 10; + break; + + default: + if (logLevel) { + console.error(`Unsupported log level ${logLevel}`); + } + this.logLevel = 10; + break; + } + + this.namespace = settings && settings.namespace || 'node.odata'; + } + + debug(msg) { + if (this.logLevel < 40) { + return; + } + + console.debug(`[${new Date().toUTCString()}] ${this.namespace ? this.namespace : ''}: ${msg}`); + } + + log(obj) { + if (this.logLevel < 40) { + return; + } + + console.log(obj); + } +} \ No newline at end of file diff --git a/test/mocked/hook.action.js b/test/mocked/hook.action.js index a73dbd4..e81d26f 100644 --- a/test/mocked/hook.action.js +++ b/test/mocked/hook.action.js @@ -27,7 +27,6 @@ describe('hook.action', () => { } }); - it('should call fn and before hook', async function () { const callbackFn = sinon.spy(); const action = server.action('salam-aleikum', async (req, res) => { @@ -39,7 +38,7 @@ describe('hook.action', () => { action.addBefore(async (req, res) => { req.hook = 'data drives'; callbackHook(); - }); + }, 'sample-before'); httpServer = server.listen(port); const res = await request(host).post(`/node.odata.salam-aleikum`); @@ -63,7 +62,7 @@ describe('hook.action', () => { error.status = 401; throw error; - }); + }, 'sample-bug-hook'); httpServer = server.listen(port); const res = await request(host).post(`/node.odata.salam-aleikum`); @@ -74,7 +73,7 @@ describe('hook.action', () => { it('should call fn and after hook', async function () { const callbackFn = sinon.spy(); - const action = server.action('salam-aleikum', (req, res) => { + const action = server.action('salam-aleikum', async (req, res) => { callbackFn(); res.$odata.result = {result: 'data drives'}; }); @@ -82,7 +81,7 @@ describe('hook.action', () => { const callbackHook = sinon.spy(); action.addAfter(async (req, res) => { callbackHook(); - }); + }, 'sample-after-hook'); httpServer = server.listen(port); const res = await request(host).post(`/node.odata.salam-aleikum`); @@ -93,4 +92,29 @@ describe('hook.action', () => { callbackHook.should.be.callCount(1); }); + it('should call next callback by using async func', async function () { + const action = server.action('salam-aleikum', async (req, res) => { + res.$odata.status = 204; + }); + + httpServer = server.listen(port); + + const res = await request(host).post(`/node.odata.salam-aleikum`); + + res.res.statusMessage.should.be.equal('No Content'); + }); + + it('should works with next callback', async function () { + const action = server.action('salam-aleikum', async (req, res, next) => { + res.$odata.status = 204; + next(); + }); + + httpServer = server.listen(port); + + const res = await request(host).post(`/node.odata.salam-aleikum`); + + res.res.statusMessage.should.be.equal('No Content'); + }); + }); diff --git a/test/mocked/metadata.complex.type.js b/test/mocked/metadata.complex.type.js new file mode 100644 index 0000000..1420a3e --- /dev/null +++ b/test/mocked/metadata.complex.type.js @@ -0,0 +1,88 @@ +// For issue: https://github.com/TossShinHwa/node-odata/issues/96 +// For issue: https://github.com/TossShinHwa/node-odata/issues/25 + +import 'should'; +import request from 'supertest'; +import { host, conn, port, odata, assertSuccess } from '../support/setup'; +import FakeDb from '../support/fake-db'; + +describe('metadata.complex.type', () => { + let httpServer, server, db; + + beforeEach(async function() { + db = new FakeDb(); + server = odata(db); + + }); + + afterEach(() => { + httpServer.close(); + }); + + it('should return explizit defined custom type in json format', async function() { + const jsonDocument = { + $Version: '4.0', + ObjectId: { + $Kind: 'TypeDefinition', + $UnderlyingType: 'Edm.String', + $MaxLength: 24, + }, + fullName: { + $Kind: "ComplexType", + first: { + $Type: "Edm.String" + }, + last: { + $Type: 'Edm.String' + } + }, + $EntityContainer: 'node.odata', + ['node.odata']: { + $Kind: 'EntityContainer' + }, + }; + server.complexType('fullName', { + first: { + $Type: 'Edm.String' + }, + last: { + $Type: 'Edm.String' + } + }); + httpServer = server.listen(port); + const res = await request(host).get('/$metadata?$format=json'); + assertSuccess(res); + res.body.should.deepEqual(jsonDocument); + }); + + it('should return explizit defined custom type in xml format', async function() { + const xmlDocument = + ` + + + + + + + + + + + + `.replace(/\s*\s*/g, '>'); + server.complexType('fullName', { + first: { + $Type: 'Edm.String' + }, + last: { + $Type: 'Edm.String' + } + }); + httpServer = server.listen(port); + const res = await request(host).get('/$metadata'); + assertSuccess(res); + res.text.should.equal(xmlDocument); + }); + + +}); diff --git a/test/mocked/model.complex.action.js b/test/mocked/model.complex.action.js index 300b317..6fec2f4 100644 --- a/test/mocked/model.complex.action.js +++ b/test/mocked/model.complex.action.js @@ -16,9 +16,10 @@ describe('model.complex.action', () => { resource.action('all-item-greater', (req, res, next) => { const { price } = req.query; const $elemMatch = { price: { $gt: price } }; - server.resources.order.model.exec((err, data) => { + req.$odata.mongo.order.exec((err, data) => { res.$odata.result = data.slice(1); res.$odata.status = 200; + next(); }); }, { binding : 'collection' }); httpServer = server.listen(port); diff --git a/test/mocked/odata.actions.js b/test/mocked/odata.actions.js index f06eaa5..d1d1520 100644 --- a/test/mocked/odata.actions.js +++ b/test/mocked/odata.actions.js @@ -31,7 +31,10 @@ describe('odata.actions', () => { .action('50off', (req, res, next) => { req.$odata.mongo.book.findById(req.params.id, (err, book) => { book.price = halfPrice(book.price); - book.save((err) => res.$odata.result = book); + book.save((err) => { + res.$odata.result = book; + next(); + }); }); }, { binding: 'entity' @@ -47,7 +50,7 @@ describe('odata.actions', () => { }); it('should work with unbound action', async function () { - server.action('salam-aleikum', (req, res, next) => { + server.action('salam-aleikum', async (req, res) => { res.$odata.result = {result: 'Wa aleikum assalam'}; }) httpServer = server.listen(port); @@ -62,7 +65,7 @@ describe('odata.actions', () => { }); it('should return 404 for action url without namespace', async function () { - server.action('salam-aleikum', (req, res, next) => { + server.action('salam-aleikum', async (req, res) => { res.$odata.result = {result: 'Wa aleikum assalam'}; }) httpServer = server.listen(port); diff --git a/test/mocked/odata.batch.js b/test/mocked/odata.batch.js index 1e32278..456a3cc 100644 --- a/test/mocked/odata.batch.js +++ b/test/mocked/odata.batch.js @@ -12,12 +12,10 @@ describe('odata.batch', () => { const db = new FakeDb(); const server = odata(db); resource = server.resource('book', bookSchema); - resource.action('entity-action', (req, res, next) => { - res.$odata.status = 200; + resource.action('entity-action', async (req, res) => { res.$odata.result = {result: 'Hello! I am an action, that bound to entity.'}; }, { binding: 'entity'}); - server.action('unbound-action', (req, res, next) => { - res.$odata.status = 200; + server.action('unbound-action', async (req, res) => { res.$odata.result = { result: 'Hello! I am an unbound action.'}; }) books = JSON.parse(JSON.stringify(db.addData('book', data))); diff --git a/test/mocked/odata.error.js b/test/mocked/odata.error.js new file mode 100644 index 0000000..f856ac9 --- /dev/null +++ b/test/mocked/odata.error.js @@ -0,0 +1,83 @@ +import 'should'; +import request from 'supertest'; +import { odata, host, port, bookSchema } from '../support/setup'; +import data from '../support/books.json'; +import FakeDb from '../support/fake-db'; + +describe('odata.actions', () => { + let httpServer, server, db; + + beforeEach(async function () { + db = new FakeDb(); + server = odata(db); + }); + + afterEach(() => { + if (httpServer) { + httpServer.close(); + } + }); + + it('should return odata error object with status 500 if no status given', async function () { + server.action('server-error', (req, res, next) => { + throw new Error("This message should not go to client"); + }); + httpServer = server.listen(port); + + const res = await request(host).post(`/node.odata.server-error`) + + res.status.should.be.equal(500); + res.res.statusMessage.should.be.equal('Internal Server Error'); + res.body.should.deepEqual({ + error: { + code: "500", + message: "Internal Server Error" + } + + }); + }); + + it('should not return custom message if status is bigger or equal 500', async function () { + server.action('server-error', (req, res, next) => { + const error = new Error("This message should not go to client"); + + error.status = 501; + throw error; + }); + httpServer = server.listen(port); + + const res = await request(host).post(`/node.odata.server-error`) + + res.status.should.be.equal(501); + res.res.statusMessage.should.be.equal('Not Implemented'); + res.body.should.deepEqual({ + error: { + code: "501", + message: "Not Implemented" + } + + }); + }); + + it('should not return custom message if status is letter or equal 500', async function () { + server.action('server-error', (req, res, next) => { + const error = new Error("I can only brew tea"); + + error.status = 418; + throw error; + }); + httpServer = server.listen(port); + + const res = await request(host).post(`/node.odata.server-error`) + + res.status.should.be.equal(418); + res.res.statusMessage.should.be.equal('I\'m a Teapot'); + res.body.should.deepEqual({ + error: { + code: "418", + message: "I can only brew tea" + } + + }); + }); +}); From ab663ab19f50016c1c50c759e318e3726cdb169d Mon Sep 17 00:00:00 2001 From: Richard Martens Date: Tue, 6 Jun 2023 22:25:39 +0200 Subject: [PATCH 28/64] remove unneccesary next call on the last hook --- src/middlewares/writer.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/middlewares/writer.js b/src/middlewares/writer.js index 4dfacac..3212b9d 100644 --- a/src/middlewares/writer.js +++ b/src/middlewares/writer.js @@ -55,7 +55,7 @@ function getWriter(req, res, result) { } } -export default async function writer(req, res) { +export default function writer(req, res) { switch (res.$odata.status) { case 404: // not found or no handler worked on From e99dc9ad562b43c6c35bd696f5c997c852dbb6fe Mon Sep 17 00:00:00 2001 From: Richard Martens Date: Thu, 22 Jun 2023 23:02:31 +0200 Subject: [PATCH 29/64] entity implemented --- .vscode/launch.json | 2 +- README.md | 60 ++++++++++++- src/ODataResource.js | 4 +- src/{ => odata}/Action.js | 42 ++++++++- src/odata/Batch.js | 16 ++-- src/odata/Entity.js | 153 +++++++++++++++++++++++++++++++++ src/odata/Hooks.js | 34 ++++++-- src/odata/Metadata.js | 63 ++++---------- src/odata/ServiceDocument.js | 3 +- src/odata/validator.js | 74 +++++++++++----- src/parser/multipartMixed.js | 3 +- src/server.js | 23 +++-- test/mocked/metadata.action.js | 8 +- test/mocked/metadata.js | 45 ++++++++++ test/mocked/odata.entity.js | 100 +++++++++++++++++++++ 15 files changed, 531 insertions(+), 99 deletions(-) rename src/{ => odata}/Action.js (71%) create mode 100644 src/odata/Entity.js create mode 100644 test/mocked/odata.entity.js diff --git a/.vscode/launch.json b/.vscode/launch.json index c539ac6..3e01241 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -20,7 +20,7 @@ "dot", "--timeout", "300000", - "test/mocked/odata.error.js" + "test/mocked/odata.batch.js" ], "env": { "LOG_LEVEL": "debug" diff --git a/README.md b/README.md index 70cde52..95a3547 100644 --- a/README.md +++ b/README.md @@ -54,6 +54,62 @@ The options object currently only supports one parameter: ```expressRequestLimit # How to +## Entities + +With entities you can provide a kind of virtual table via the OData service. The following operations can be implemented on an entity: + - list: Returns one or more items from the list. The result array must be encapsulated in a property named "value". e.g.({value: []}) + - get: Returns exactly one item + - post: Creates a new Item + - put: Updates an existing item + - delete: Deletes an exsiting item + - patch: Merges properties of an existing items with incomming attributes + - count: Returns a count of items in the list + +Here an example of an entity implementation. To define an entity, you must call the server.entity method. Pass the name of the entity as the first parameter. The second parameter allows you to pass the implementation for each operation. If you do not pass a handler for an operation, calling that operation returns "Not Implemented". With the third parameter you pass the description of your entity. An object with the $Key property in which you list the names of all key columns. The other properties of the object describe the properties of your entity. + +``` +const odata = require('node-odata'); +const server = odata(process.env.DATABASE || 'mongodb://localhost:27017/example'); + + +server.complexType('fullName', { + first: { + $Type: 'Edm.String' + }, + last: { + $Type: 'Edm.String' + } +}); + +const entity = server.entity('user', { + list: async (req, res) => { + res.$odata.status = 200; + res.$odata.result = { + value: [{ + id: '1', + name: { first: 'Max', last: 'Mustermann' } + }] + }; + + }, + count: async (req, res) => { + res.$odata.status = 200; + res.$odata.result = 1; + } +}, { + $Key: ['title'], + id: { + $Type: 'node.odata.ObjectId' + }, + name: { + $Type: 'node.odata.fullName' + }, + email: { + $Type: 'Edm.String' + } +}); +``` + ## Actions ### Unbound Actions @@ -162,9 +218,9 @@ The following attributes can be specified for parameters: - $SRID not negative Number -### Hooks +## Hooks -It is possible to specify nodejs express middlewares for the actions to be performed before or after the action. Any data assigned to req.$odata or res.$odata will be available on action implementation and subsequent hooks. An error thrown in the hook interrupts further processing. it is possible to provide a name of hook for tracing. +It is possible to specify nodejs express middlewares for the actions or entities to be performed before or after the action. Any data assigned to req.$odata or res.$odata will be available on action implementation and subsequent hooks. An error thrown in the hook interrupts further processing. it is possible to provide a name of hook for tracing. You can use a [passportjs](https://www.passportjs.org/) middleware as before hook for authentication. ``` const action = server.action('login', ...); diff --git a/src/ODataResource.js b/src/ODataResource.js index 70fcb0e..395c828 100644 --- a/src/ODataResource.js +++ b/src/ODataResource.js @@ -1,6 +1,6 @@ import rest from './rest'; import { min } from './utils'; -import Action from './Action'; +import Action from './odata/Action'; function hook(resource, pos, fn) { let method = resource._currentMethod; @@ -176,7 +176,7 @@ export default class { }); if (route) { - return route.middleware; + return [route.middleware]; } return Object.keys(this.actions) diff --git a/src/Action.js b/src/odata/Action.js similarity index 71% rename from src/Action.js rename to src/odata/Action.js index be0974e..71dc05c 100644 --- a/src/Action.js +++ b/src/odata/Action.js @@ -1,7 +1,7 @@ import { Router } from 'express'; -import Hooks from './odata/Hooks'; -import { validateParameters, validateIdentifier } from './odata/validator'; -import Console from './writer/Console'; +import Hooks from './Hooks'; +import { validateParameters, validateIdentifier } from './validator'; +import Console from '../writer/Console'; export default class Action { constructor(name, fn, options) { @@ -49,7 +49,7 @@ export default class Action { const regex = this.getPath(true); if (method === 'post' && url.match(regex)) { - return this.fn; + return [...this.hooks.before, this.fn, ...this.hooks.after]; } } @@ -69,6 +69,40 @@ export default class Action { return this.router; } + getMetadata() { + const result = { + $Kind: 'Action' + }; + + if (this.binding) { + result.$IsBound = true; + result.$Parameter = [{ + $Name: this.resource._url || this.resource.name, + $Type: `node.odata.${this.resource._url || this.resource.name}`, + $Collection: this.binding === 'collection' ? true : undefined, + }]; + } + + if (this.$Parameter) { + if (!result.$Parameter) { + result.$Parameter = []; + } + + this.$Parameter.forEach(para => { + const item = para; + + if (para.$Type.search(/^edm/i) === -1 ) { + item.$Type = `${para.$Type}`; + } + + result.$Parameter.push(item); + }); + result.$Parameter = result.$Parameter ? result.$Parameter.concat() : this.$Parameter; + } + + return result; + } + getPath(asRegex) { let path; diff --git a/src/odata/Batch.js b/src/odata/Batch.js index 05272f4..7355a88 100644 --- a/src/odata/Batch.js +++ b/src/odata/Batch.js @@ -91,12 +91,15 @@ export default class Batch { }); } - await handler(req, res, err => { - if (err) { - throw err; - } - }); + for (let i = 0; i < handler.length; ++i) { + const handlerOrHook = handler[i]; + await handlerOrHook(req, res, err => { + if (err) { + throw err; + } + }); + } for(let i = 0; i < this._server.hooks.after.length; ++i) { const hook = this._server.hooks.after[i]; @@ -162,6 +165,7 @@ export default class Batch { }; const currentResponse = { + end: (message) => { throw new Error(message); }, type: (mimetype) => { appendHeader('content-type', mimetype); }, @@ -178,7 +182,7 @@ export default class Batch { result.body = body; } }; - }, + } }; await this.executeSingleRequest(handler, currentRequest, currentResponse); diff --git a/src/odata/Entity.js b/src/odata/Entity.js new file mode 100644 index 0000000..5ad01fa --- /dev/null +++ b/src/odata/Entity.js @@ -0,0 +1,153 @@ +import { validateIdentifier, validate } from "./validator"; +import { Router } from 'express'; +import Hooks from "./Hooks"; + +export default class Entity { + constructor(name, handler, metadata) { + const notImplemented = (req, res) => { + const error = new Error(); + + error.status = 501; + throw error; + }; + + this.name = name; + this.handler = { + list: notImplemented, + get: notImplemented, + post: notImplemented, + put: notImplemented, + delete: notImplemented, + patch: notImplemented, + count: notImplemented, + ...handler + }; + this.metadata = { + $Kind: 'EntityType', + ...metadata + }; + + this.actions = {}; + this.hooks = new Hooks(); + } + + addBefore(fn, name) { + this.hooks.addBefore(fn, name); + } + + addAfter(fn, name) { + this.hooks.addAfter(fn, name); + } + + action(name, fn, options) { + this.actions[name] = new Action(name, fn, + { + ...options, + resource: this + }); + + return this.actions[name]; + } + + match(method, url) { + validateIdentifier(this.name); + + const routes = this.getRoutes(); + const route = routes.find((item) => { + if (item.method === method) { + const match = url.match(item.regex); + + return match; + } + }); + + if (route) { + return [ + ...this.hooks.before, + this.handler[route.name], + ...this.hooks.after + ]; + } + + return Object.keys(this.actions) + .map(name => this.actions[name].match(method, url)) + .find(ctrl => ctrl); + } + + getRouter() { + validateIdentifier(this.name); + validate(this.metadata); + + const router = Router(); + const routes = this.getRoutes(this.name); + + routes.forEach((route) => { + const { + name, method, url + } = route; + router[method](url, ...this.hooks.before, + (req, res, next) => { + res.$odata.status = 200; + this.handler[name](req, res, next); + }, ...this.hooks.after); + }); + return router; + } + + getMetadata() { + return this.metadata; + } + + getRoutes() { + const resourceListURL = `/${this.name}`; + const resourceListRegex = new RegExp(`(^\/?${this.name}[?#])|(^\/?${this.name}$)`); + const resourceURL = `${resourceListURL}\\(:id\\)`; + const resourceRegex = new RegExp(`^\/?${this.name}\\([^)]+\\)`); + + return [ + { + name: 'post', + method: 'post', + url: resourceListURL, + regex: resourceListRegex + }, + { + name: 'put', + method: 'put', + url: resourceURL, + regex: resourceRegex + }, + { + name: 'patch', + method: 'patch', + url: resourceURL, + regex: resourceRegex + }, + { + name: 'delete', + method: 'delete', + url: resourceURL, + regex: resourceRegex + }, + { + name: 'get', + method: 'get', + url: resourceURL, + regex: resourceRegex + }, + { + name: 'count', + method: 'get', + url: resourceListURL + '/([\$])count', + regex: new RegExp(`(^\/?${this.name}\/\\$count[?]?)|(^\/?${this.name}\/\\$count$)`) + }, + { + name: 'list', + method: 'get', + url: resourceListURL, + regex: resourceListRegex + } + ]; + } + +} \ No newline at end of file diff --git a/src/odata/Hooks.js b/src/odata/Hooks.js index 85fe4f6..7e0dab0 100644 --- a/src/odata/Hooks.js +++ b/src/odata/Hooks.js @@ -19,16 +19,36 @@ export default class Hooks { } } - suppressNext(fn, name) { + suppressNext(fn, name, isFinal) { return async (req, res, next) => { try { const con = new Console(); con.debug(`Hook ${name} started`); - const prom = fn(req, res, next); - if (prom && prom.then) { - await prom; + const combine = new Promise(async (resolve, reject) => { + try { + const prom = fn(req, res, err => { + if (err) { + reject(err); + } + resolve(); + }); + + if (prom && prom.then) { + await prom; + resolve(); + } else if (isFinal) { + resolve(); + } + + } catch(err) { + reject(err); + } + }); + + await combine; + if (!isFinal) { next(); } @@ -38,15 +58,15 @@ export default class Hooks { }; } - addAfter(fn, name) { + addAfter(fn, name, isFinal) { if (!fn) { throw new Error(`Parameter 'fn' should be given`); } if (Array.isArray(fn)) { - this.after = fn.map(item => this.suppressNext(item, name)).concat(this.after); + this.after = fn.map(item => this.suppressNext(item, name, isFinal)).concat(this.after); } else { - this.after.unshift(this.suppressNext(fn, name)); + this.after.unshift(this.suppressNext(fn, name, isFinal)); } } diff --git a/src/odata/Metadata.js b/src/odata/Metadata.js index c71458e..34a6d25 100644 --- a/src/odata/Metadata.js +++ b/src/odata/Metadata.js @@ -1,5 +1,6 @@ import { Router } from 'express'; -import Resource from '../ODataResource'; +import ODataResource from '../ODataResource'; +import Entity from './Entity'; import Function from '../ODataFunction'; import { validate, validateIdentifier } from './validator'; @@ -47,7 +48,7 @@ export default class Metadata { res.$odata.supportedMimetypes = ['application/xml', 'application/json']; next(); - } catch(err) { + } catch (err) { next(err); } } @@ -170,40 +171,6 @@ export default class Metadata { return properties; } - static visitAction(node) { - const result = { - $Kind: 'Action' - }; - - if (node.binding) { - result.$IsBound = true; - result.$Parameter = [{ - $Name: node.resource._url, - $Type: `node.odata.${node.resource._url}`, - $Collection: node.binding === 'collection' ? true : undefined, - }]; - } - - if (node.$Parameter) { - if (!result.$Parameter) { - result.$Parameter = []; - } - - node.$Parameter.forEach(para => { - const item = para; - - if (para.$Type.search(/^edm/i) === -1 ) { - item.$Type = `${para.$Type}`; - } - - result.$Parameter.push(item); - }); - result.$Parameter = result.$Parameter ? result.$Parameter.concat() : node.$Parameter; - } - - return result; - } - static visitFunction(node) { return { $Kind: 'Function', @@ -219,9 +186,6 @@ export default class Metadata { case 'ComplexType': return this.visitComplexType(node, model); - case 'Action': - return Metadata.visitAction(node); - case 'Function': return Metadata.visitFunction(node); @@ -236,16 +200,25 @@ export default class Metadata { const resource = this._server.resources[currentResource]; const result = { ...previousResource }; - if (resource instanceof Resource) { + if (resource instanceof ODataResource) { const { paths } = resource.model.model.schema; result[currentResource] = this.visitor('EntityType', paths, resource._model); const actions = Object.keys(resource.actions); if (actions && actions.length) { actions.forEach((action) => { - result[action] = this.visitor('Action', resource.actions[action]); + result[action] = resource.actions[action].getMetadata(); + }); + } + } else if (resource instanceof Entity) { + result[currentResource] = resource.getMetadata(); + const actions = Object.keys(resource.actions); + if (actions && actions.length) { + actions.forEach((action) => { + result[action] = resource.actions[action].getMetadata(); }); } + } else if (resource instanceof Function) { result[currentResource] = this.visitor('Function', resource); } @@ -258,7 +231,7 @@ export default class Metadata { const result = { ...previousResource }; const resource = this._server.resources[currentResource]; - if (resource instanceof Resource) { + if (resource instanceof ODataResource || resource instanceof Entity) { result[currentResource] = { $Collection: true, $Type: `node.odata.${currentResource}`, @@ -274,7 +247,7 @@ export default class Metadata { const actionNames = Object.keys(this._server.actions); const actionImports = actionNames.reduce((previousAction, currentAction) => { - const result = {...previousAction}; + const result = { ...previousAction }; result[`${currentAction}-import`] = { $Action: `node.odata.${currentAction}` @@ -283,11 +256,11 @@ export default class Metadata { return result; }, {}) const unboundActions = actionNames.reduce((previousAction, currentAction) => { - const result = {...previousAction}; + const result = { ...previousAction }; const action = this._server.actions[currentAction]; const attachToRoot = (name, value) => { result[name] = value; }; - result[currentAction] = this.visitor('Action', action, attachToRoot); + result[currentAction] = action.getMetadata(); return result; }, {}) diff --git a/src/odata/ServiceDocument.js b/src/odata/ServiceDocument.js index dc274c9..4534c98 100644 --- a/src/odata/ServiceDocument.js +++ b/src/odata/ServiceDocument.js @@ -1,5 +1,6 @@ import { Router } from 'express'; import Resource from '../ODataResource'; +import Entity from './Entity'; export default class Metadata { constructor(server) { @@ -43,7 +44,7 @@ export default class Metadata { ctrl(req) { const entityTypeNames = Object.keys(this._server.resources); const entitySets = entityTypeNames - .filter((item) => this._server.resources[item] instanceof Resource) + .filter((item) => this._server.resources[item] instanceof Resource || this._server.resources[item] instanceof Entity) .map((currentResource) => ({ name: currentResource, kind: 'EntitySet', diff --git a/src/odata/validator.js b/src/odata/validator.js index d4fd0eb..add58d4 100644 --- a/src/odata/validator.js +++ b/src/odata/validator.js @@ -6,7 +6,7 @@ export const validateIdentifier = (identifier) => { } -function shouldContains(property, member, list) { +function shouldContains(property, member, list) { if (property[member] && list.indexOf(property[member]) === -1 && (!property[member].match(/node\.odata/) || member != '$Type')) {// custom type throw new Error(`${member} '${property[member]}' is invalid`); @@ -43,19 +43,19 @@ function validateSRID(name, property) { } } -function validateMaxLength(name, property, member) { - const maxLength = property[name].member; +function shouldBePositive(name, property, member) { + const value = property[member]; - if (!maxLength) { - throw new Error(`If MaxLength is given, than the value had to be supplied`); + if (!value) { + throw new Error(`If '${member}' is given, than the value had to be supplied`); } - if (Number.isNaN(+maxLength)) { - throw new Error(`Value '${maxLength}' is invalid for MaxLength`); + if (Number.isNaN(+value)) { + throw new Error(`Value '${value}' is invalid for '${member}'`); } - if (+maxLength < 1) { - throw new Error(`If MaxLength is given, than the value had to be supplied`) + if (+value < 1) { + throw new Error(`If '${member}' is given, than the value had to be supplied`) } } @@ -64,11 +64,11 @@ export const validateProperty = (name, property) => { validateIdentifier(name); const members = Object.keys(property); + const allowedMembers = ['$Type', '$Collection', '$Nullable', '$MaxLength', + '$Unicode', '$Precision', '$Scale', '$SRID', '$DefaultValue']; + const boolean = [true, false]; members.forEach(member => { - const allowedMembers = ['$Type', '$Collection', '$Nullable', '$MaxLength', - '$Unicode', '$Precision', '$Scale', '$SRID', '$DefaultValue']; - switch (member.trim()) { case '$Type': validateType(property, member); @@ -77,9 +77,7 @@ export const validateProperty = (name, property) => { case '$Collection': case '$Nullable': case '$Unicode': - const boolean = [true, false]; - - shouldContains(property[name], member.trim(), boolean); + shouldContains(property, member.trim(), boolean); break; case '$SRID': @@ -87,7 +85,9 @@ export const validateProperty = (name, property) => { break; case '$MaxLength': - validateMaxLength(name, property, member); + case '$Precision': + case '$Scale': + shouldBePositive(name, property, member); break; case '$DefaultValue': @@ -132,6 +132,36 @@ function validateComplexType(node) { properties.filter(name => name !== '$Kind') .forEach(name => validateProperty(name, node[name])); + + if (!properties.length) { + throw new Error('ComplexType without properties is not allowed') + } +} + +function validateEntityType(node) { + const attributes = Object.keys(node); + + if (!node.$Key || !node.$Key.length) { + throw new Error('EntityType without key is not allowed') + } + + if (!Array.isArray(node.$Key)) { + throw new Error('$Key of Entitytype has to be an array of property names'); + } + + const properties = attributes.filter(name => name !== '$Kind' && name !== '$Key'); + + properties.forEach(name => validateProperty(name, node[name])); + + if (!properties.length) { + throw new Error('ComplexType without properties is not allowed') + } + + node.$Key.forEach(key => { + if (properties.indexOf(key) === -1) { + throw new Error(`EntityType has not a property for $Key with name "${key}"`) + } + }); } export const validate = node => { @@ -139,12 +169,16 @@ export const validate = node => { throw new Error('For validation an object should not be undefined'); } - switch(node.$Kind) { + switch (node.$Kind) { case 'ComplexType': validateComplexType(node); break; - - default: - throw new Error('For validation an object need a property $Kind'); + + case 'EntityType': + validateEntityType(node); + break; + + default: + throw new Error('For validation an object need a property $Kind'); } } \ No newline at end of file diff --git a/src/parser/multipartMixed.js b/src/parser/multipartMixed.js index 51de761..5e19957 100644 --- a/src/parser/multipartMixed.js +++ b/src/parser/multipartMixed.js @@ -28,7 +28,8 @@ function multipart(req, res, next) { result.method = matchMethodUrl[1].toLowerCase(); result.url = matchMethodUrl[2]; - const matchHeaders = singleRequestText.match(/^^([\w-]+)\s*:\s*([\w.-\/-]+)\s*$/gmi); + const headerText = singleRequestText.split(matchMethodUrl[1])[1]; + const matchHeaders = headerText.match(/^^([\w-]+)\s*:\s*([\w\s;=.\/-]+)\s*$/gmi); result.headers = { }; diff --git a/src/server.js b/src/server.js index 49ccf38..08603e1 100644 --- a/src/server.js +++ b/src/server.js @@ -1,11 +1,12 @@ import createExpress from './express'; -import Resource from './ODataResource'; +import ODataResource from './ODataResource'; +import Entity from './odata/Entity'; import Func from './ODataFunction'; import Metadata from './odata/Metadata'; import ServiceDocument from './odata/ServiceDocument'; import Batch from './odata/Batch'; import Db from './db/db'; -import Action from './Action'; +import Action from './odata/Action'; import error from './middlewares/error'; import writer from './middlewares/writer'; import Hooks from './odata/Hooks'; @@ -36,7 +37,7 @@ class Server { supportedMimetypes: ['application/json'] } }, 'service-initialization'); - this.hooks.addAfter(writer, 'writer'); + this.hooks.addAfter(writer, 'writer', true); // TODO: Infact, resources is a mongooseModel instance, origin name is repositories. // Should mix _resources object and resources object: _resources + resource = resources. @@ -63,13 +64,23 @@ class Server { } const db = this.get('db'); - this.resources[name] = new Resource(this, name, model); + this.resources[name] = new ODataResource(this, name, model); this.resources[name].setModel(db.register(name, model)); return this.resources[name]; } + entity(name, handler, metadata) { + if (this.resources[name]) { + throw new Error(`Entity with name "${name}" already defined`); + } + + this.resources[name] = new Entity(name, handler, metadata); + + return this.resources[name]; + } + defaultConfiguration(db, prefix = '') { this.set('app', this._app); this.set('db', db); @@ -138,7 +149,7 @@ class Server { Object.keys(this.resources).forEach((resourceKey) => { const resource = this.resources[resourceKey]; - result.push(resource._router(this.getSettings())); + result.push(resource._router ? resource._router(this.getSettings()) : resource.getRouter()); if (resource.actions) { Object.keys(resource.actions).forEach((actionKey) => { @@ -177,7 +188,7 @@ class Server { } use(...args) { - if (args[0] instanceof Resource) { + if (args[0] instanceof ODataResource) { const [resource] = args; this.resources[resource.getName()] = resource; return; diff --git a/test/mocked/metadata.action.js b/test/mocked/metadata.action.js index ba1d23f..df19c60 100644 --- a/test/mocked/metadata.action.js +++ b/test/mocked/metadata.action.js @@ -18,7 +18,7 @@ describe('metadata.action', () => { afterEach(() => { httpServer.close(); }); - +/* it('should return json metadata for action that bound to instance', async function() { const jsonDocument = { $Version: '4.0', @@ -88,7 +88,7 @@ describe('metadata.action', () => { - `.replace(/\s*\s*/g, '>'); + `.replace(/\s*\s*//*g, '>'); server.resource('book', { author: String }).action('bound-action', @@ -170,7 +170,7 @@ describe('metadata.action', () => { - `.replace(/\s*\s*/g, '>'); + `.replace(/\s*\s*//*g, '>'); server.resource('book', { author: String }).action('bound-action', @@ -196,7 +196,7 @@ describe('metadata.action', () => { error.message.should.equal(`Invalid simple identifier '/login'`); } }); - +*/ it('should return json metadata for unbound action', async function() { const jsonDocument = { diff --git a/test/mocked/metadata.js b/test/mocked/metadata.js index 04031c3..90b3130 100644 --- a/test/mocked/metadata.js +++ b/test/mocked/metadata.js @@ -335,4 +335,49 @@ describe('metadata', () => { res.text.should.equal(xmlDocument); }); + + it('should return json metadata for custom resource', async function() { + const jsonDocument = { + $Version: '4.0', + ObjectId: { + $Kind: "TypeDefinition", + $UnderlyingType: "Edm.String", + $MaxLength: 24 + }, + book: { + $Kind: "EntityType", + $Key: ["id"], + id: { + $Type: "node.odata.ObjectId", + $Nullable: false, + }, + salted: { + $Type: 'Edm.Boolean' + } + }, + $EntityContainer: 'node.odata', + ['node.odata']: { + $Kind: 'EntityContainer', + book: { + $Collection: true, + $Type: `node.odata.book`, + } + }, + }; + server.entity('book', {}, { + $Key: ["id"], + id: { + $Type: "node.odata.ObjectId", + $Nullable: false, + }, + salted: { + $Type: 'Edm.Boolean' + } + }); + httpServer = server.listen(port); + const res = await request(host).get('/$metadata?$format=json'); + assertSuccess(res); + res.body.should.deepEqual(jsonDocument); + }); + }); diff --git a/test/mocked/odata.entity.js b/test/mocked/odata.entity.js new file mode 100644 index 0000000..3abf554 --- /dev/null +++ b/test/mocked/odata.entity.js @@ -0,0 +1,100 @@ +import 'should'; +import request from 'supertest'; +import { odata, host, port, bookSchema } from '../support/setup'; +import data from '../support/books.json'; +import FakeDb from '../support/fake-db'; + +describe('odata.entity', () => { + let httpServer, server, db; + + beforeEach(async function () { + db = new FakeDb(); + server = odata(db); + }); + + afterEach(() => { + if (httpServer) { + httpServer.close(); + } + }); + + it('should work with custom implementation', async function () { + const result = [{ + "id": '1', + "price": 44.95, + "title": "XML Developer's Guide" + }, + { + "id": '1', + "price": 5.95, + "title": "Midnight Rain" + }]; + server.entity('book', { + list: (req, res, next) => { + res.$odata.result = result; + next(); + } + }, { + $Key: ['id'], + id: { + $Type: 'node.odata.ObjectId', + $Nullable: false + }, + title: { + $Type: 'Edm.String' + }, + price: { + $Type: 'Edm.Decimal', + $Precision: 10, + $Scale: 2 + } + }); + httpServer = server.listen(port); + + const res = await request(host).get(`/book`); + + if (!res.ok) { + res.res.statusMessage.should.be.equal(''); + } + + res.body.should.deepEqual(result); + }); + + + + it('should return 501 for not implemented methods', async function () { + const result = [{ + "id": '1', + "price": 44.95, + "title": "XML Developer's Guide" + }, + { + "id": '1', + "price": 5.95, + "title": "Midnight Rain" + }]; + server.entity('book', null, { + $Key: ['id'], + id: { + $Type: 'node.odata.ObjectId', + $Nullable: false + }, + title: { + $Type: 'Edm.String' + }, + price: { + $Type: 'Edm.Decimal', + $Precision: 10, + $Scale: 2 + } + }); + httpServer = server.listen(port); + + const res = await request(host).get(`/book`); + + res.res.statusMessage.should.be.equal('Not Implemented'); + + res.body.should.deepEqual({ error: { code: '501', message: 'Not Implemented' } }); + }); + +}); From a38a73af41e06aad0f0ecfef169e5a88892ccaeb Mon Sep 17 00:00:00 2001 From: Richard Martens Date: Sat, 24 Jun 2023 22:56:14 +0200 Subject: [PATCH 30/64] Fix bug in multipart parser for get entity request. Extend registration of a mongo resource with options --- src/db/db.js | 3 ++- src/parser/multipartMixed.js | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/src/db/db.js b/src/db/db.js index 4bd295d..8e72aea 100644 --- a/src/db/db.js +++ b/src/db/db.js @@ -24,11 +24,12 @@ export default class { this._connection.on(name, event); } - register(name, model) { + register(name, model, options) { const conf = { _id: false, versionKey: false, collection: name, + ...options }; const schema = new mongoose.Schema(model, conf); schema.plugin(id); diff --git a/src/parser/multipartMixed.js b/src/parser/multipartMixed.js index 5e19957..ab71c76 100644 --- a/src/parser/multipartMixed.js +++ b/src/parser/multipartMixed.js @@ -19,7 +19,7 @@ function multipart(req, res, next) { if (singleRequestText.indexOf("Group ID: ") >= 0) { return; //sap extension, not documentet in odata } - const matchMethodUrl = singleRequestText.match(/^(GET|POST|PUT|PATCH|DELETE)\s+([\w\/\.$-]+)\s*/m); + const matchMethodUrl = singleRequestText.match(/^(GET|POST|PUT|PATCH|DELETE)\s+([\w\/\.$-()]+)\s*/m); if (!matchMethodUrl) { throw new Error(`Method in ${singleRequestText} not supported`); From 14f51b66bb9787e9ac973eb988c07cd27c9cb896 Mon Sep 17 00:00:00 2001 From: Richard Martens Date: Fri, 30 Jun 2023 22:39:43 +0200 Subject: [PATCH 31/64] multipart-mixed 204 response in batch, DateTimeOffset to string implicit --- .vscode/launch.json | 2 +- src/odata/Entity.js | 30 ++++++++++++++++++++++++++++-- src/writer/multipartWriter.js | 19 ++++++++++++++++--- test/mocked/odata.entity.js | 34 ++++++++++++++++++++++++++++++---- 4 files changed, 75 insertions(+), 10 deletions(-) diff --git a/.vscode/launch.json b/.vscode/launch.json index 3e01241..3347b6d 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -20,7 +20,7 @@ "dot", "--timeout", "300000", - "test/mocked/odata.batch.js" + "test/mocked/odata.entity.js" ], "env": { "LOG_LEVEL": "debug" diff --git a/src/odata/Entity.js b/src/odata/Entity.js index 5ad01fa..ba766c2 100644 --- a/src/odata/Entity.js +++ b/src/odata/Entity.js @@ -65,7 +65,8 @@ export default class Entity { return [ ...this.hooks.before, this.handler[route.name], - ...this.hooks.after + ...this.hooks.after, + this.convertAttributes.bind(this) ]; } @@ -74,6 +75,29 @@ export default class Entity { .find(ctrl => ctrl); } + // convert DattimeOffset to valid value + convertAttributes(req, res, next) { + if (res.$odata.result?.value && Array.isArray(res.$odata.result?.value)) { + // list of entities + res.$odata.result.value.forEach(this.checkPropertyValues.bind(this)); + } else if (res.$odata.result) { + this.checkPropertyValues(res.$odata.result); + } + next(); + } + + checkPropertyValues(entity) { + const entityMetadata = this.getMetadata(); + const keys = Object.keys(entity); + + keys.forEach(member => { + if (entityMetadata[member]?.$Type === "Edm.DateTimeOffset" + && Object.prototype.toString.call(entity[member]) === '[object Date]') { + entity[member] = entity[member].toISOString().replace(/\.[0-9]{3}/,'') + } + }); + } + getRouter() { validateIdentifier(this.name); validate(this.metadata); @@ -89,7 +113,9 @@ export default class Entity { (req, res, next) => { res.$odata.status = 200; this.handler[name](req, res, next); - }, ...this.hooks.after); + }, + this.convertAttributes.bind(this), + ...this.hooks.after); }); return router; } diff --git a/src/writer/multipartWriter.js b/src/writer/multipartWriter.js index 02756ee..0066db8 100644 --- a/src/writer/multipartWriter.js +++ b/src/writer/multipartWriter.js @@ -4,7 +4,7 @@ export default class MultipartWriter { let body = ''; result.responses.forEach(response => { - body += `--${boundary}\r\nContent-Type: application/http\r\n\r\nHTTP/${httpVersion} ${response.status} ${response.statusText}\r\n`; + body += `--${boundary}\r\nContent-Type: application/http\r\n\r\nHTTP/${httpVersion} ${response.status} ${response.statusText}\r\n`; if (response.headers) { const headers = Object.keys(response.headers); @@ -14,14 +14,27 @@ export default class MultipartWriter { } body += '\r\n'; - const textBody = typeof response.body === 'string' ? response.body : JSON.stringify(response.body); + let textBody; + switch (typeof response.body) { + case 'string': + textBody = response.body; + break; + + case 'undefined': // http status 204 + textBody = '{}'; + break; + + default: + textBody = JSON.stringify(response.body); + break; + } body += `${textBody}\r\n`; }); body += `--${boundary}--`; - res.setHeader('content-type',`multipart/mixed;boundary=${boundary}`); + res.setHeader('content-type', `multipart/mixed;boundary=${boundary}`); res.send(Buffer.from(body)).status(status); } } \ No newline at end of file diff --git a/test/mocked/odata.entity.js b/test/mocked/odata.entity.js index 3abf554..0b132e7 100644 --- a/test/mocked/odata.entity.js +++ b/test/mocked/odata.entity.js @@ -1,7 +1,6 @@ import 'should'; import request from 'supertest'; -import { odata, host, port, bookSchema } from '../support/setup'; -import data from '../support/books.json'; +import { odata, host, port } from '../support/setup'; import FakeDb from '../support/fake-db'; describe('odata.entity', () => { @@ -60,8 +59,6 @@ describe('odata.entity', () => { res.body.should.deepEqual(result); }); - - it('should return 501 for not implemented methods', async function () { const result = [{ "id": '1', @@ -97,4 +94,33 @@ describe('odata.entity', () => { res.body.should.deepEqual({ error: { code: '501', message: 'Not Implemented' } }); }); + it('should return datetimeoffsets without milliseconds', async function () { + server.entity('book', { + get: (req, res, next) => { + res.$odata.result = { + "id": '1', + "createdAt": new Date(Date.parse("2019-01-01T00:00:00.000Z")) + }; + next(); + } + }, { + $Key: ['id'], + id: { + $Type: 'node.odata.ObjectId', + $Nullable: false + }, + createdAt: { + $Type: 'Edm.DateTimeOffset' + } + }); + httpServer = server.listen(port); + + const res = await request(host).get(`/book('1')`); + + res.body.should.deepEqual({ + "id": '1', + "createdAt": "2019-01-01T00:00:00Z" + }); + }); + }); From 2ba23ea8c05fee710540f34967bd4e500ed03e8f Mon Sep 17 00:00:00 2001 From: Richard Martens Date: Thu, 27 Jul 2023 22:00:09 +0200 Subject: [PATCH 32/64] Splitting of odata and database logik. Validation of requests against the metadata --- .vscode/launch.json | 4 +- examples/actions/50off.js | 10 + examples/complex-resource/index.js | 24 -- examples/db.js | 22 ++ examples/functions/license.js | 6 + examples/functions/server-time.js | 7 + examples/hidden-field/index.js | 16 - examples/models/book.js | 25 ++ examples/models/complex-resource.js | 31 ++ examples/models/user.js | 24 ++ examples/multi-resource/functions/license.js | 10 - .../multi-resource/functions/server-time.js | 11 - examples/multi-resource/index.js | 16 - examples/multi-resource/resources/book.js | 10 - examples/multi-resource/resources/user.js | 6 - examples/server.js | 48 +++ examples/simple-with-data/index.js | 11 - examples/simple/index.js | 39 --- package.json | 1 + src/ODataResource.js | 202 ----------- src/middlewares/writer.js | 3 + src/mongo/Entity.js | 269 +++++++++++++++ src/mongo/parser/filterParser.js | 61 ++++ src/mongo/parser/functionsParser.js | 85 +++++ src/{ => mongo}/parser/orderbyParser.js | 0 src/{ => mongo}/parser/selectParser.js | 4 +- src/mongo/parser/skipParser.js | 14 + src/mongo/parser/topParser.js | 13 + src/mongo/rest/count.js | 19 ++ src/mongo/rest/delete.js | 17 + src/mongo/rest/get.js | 17 + src/mongo/rest/list.js | 45 +++ src/mongo/rest/patch.js | 16 + src/mongo/rest/post.js | 20 ++ src/mongo/rest/put.js | 46 +++ src/odata/Action.js | 17 +- src/odata/Entity.js | 179 ---------- src/odata/Hooks.js | 10 +- src/odata/Metadata.js | 171 +--------- src/odata/ServiceDocument.js | 5 +- src/odata/entity/Entity.js | 317 ++++++++++++++++++ src/odata/entity/parser/count.js | 15 + src/odata/entity/parser/filter.js | 205 +++++++++++ src/odata/entity/parser/keys.js | 18 + src/odata/entity/parser/select.js | 14 + src/odata/entity/parser/value.js | 72 ++++ src/parser/countParser.js | 28 -- src/parser/filterParser.js | 187 ----------- src/parser/functionsParser.js | 78 ----- src/parser/skipParser.js | 16 - src/parser/topParser.js | 16 - src/pipes.js | 30 -- src/rest/count.js | 23 -- src/rest/delete.js | 16 - src/rest/get.js | 19 -- src/rest/index.js | 178 ---------- src/rest/list.js | 65 ---- src/rest/patch.js | 18 - src/rest/post.js | 21 -- src/rest/put.js | 42 --- src/server.js | 178 +++------- test/{mocked => }/api.Function.js | 6 +- test/{mocked => }/hook.action.js | 8 +- test/hook.entity.after.js | 58 ++++ test/hook.entity.before.js | 56 ++++ test/{mocked => }/metadata.action.js | 91 +++-- test/{mocked => }/metadata.complex.type.js | 6 +- test/{mocked => }/metadata.format.js | 73 ++-- test/{mocked => }/metadata.function.js | 8 +- test/metadata.js | 65 ++++ test/{mocked => }/mimetype.defaults.js | 18 +- test/mocked/api.Resource.js | 24 -- test/mocked/hook.all.after.js | 44 --- test/mocked/hook.all.before.js | 43 --- test/mocked/hook.delete.after.js | 42 --- test/mocked/hook.delete.before.js | 42 --- test/mocked/hook.get.after.js | 42 --- test/mocked/hook.get.before.js | 44 --- test/mocked/hook.list.after.js | 44 --- test/mocked/hook.list.before.js | 43 --- test/mocked/hook.post.after.js | 48 --- test/mocked/hook.post.before.js | 48 --- test/mocked/hook.put.after.js | 43 --- test/mocked/hook.put.before.js | 44 --- test/mocked/model.complex.action.js | 56 ---- test/mocked/model.complex.filter.js | 34 -- test/mocked/model.complex.js | 45 --- test/mocked/model.custom.id.js | 30 -- test/mocked/model.hidden.field.js | 52 --- test/mocked/model.special.name.js | 26 -- test/mocked/odata.count.js | 32 -- test/mocked/odata.query.count.js | 37 -- test/mocked/odata.query.filter.functions.js | 63 ---- test/mocked/odata.query.filter.js | 150 --------- test/mongo/connected/model.complex.js | 56 ++++ test/mongo/connected/model.special.name.js | 34 ++ test/mongo/connected/rest.list.js | 29 ++ .../connected}/rest.post.js | 14 +- test/{mocked => mongo}/metadata.js | 117 +++---- .../metadata.resource.complex.js | 122 +++++-- test/mongo/mocked/model.complex.filter.js | 69 ++++ test/mongo/mocked/model.custom.id.js | 89 +++++ test/mongo/mocked/model.hidden.field.js | 102 ++++++ test/mongo/mocked/odata.count.js | 47 +++ test/mongo/mocked/odata.query.count.js | 63 ++++ .../mocked/odata.query.filter.functions.js | 136 ++++++++ test/mongo/mocked/odata.query.filter.js | 243 ++++++++++++++ test/{mocked => }/odata.actions.js | 41 +-- test/{mocked => }/odata.batch.js | 227 ++++++++----- test/{mocked => }/odata.entity.js | 16 +- test/{mocked => }/odata.error.js | 9 +- test/odata.filter.js | 105 ++++++ test/{mocked => }/odata.functions.js | 6 +- test/support/books.json | 13 + test/support/books.model.js | 51 +++ test/support/db.js | 23 ++ test/support/setup.js | 11 +- 117 files changed, 3207 insertions(+), 2971 deletions(-) create mode 100644 examples/actions/50off.js delete mode 100644 examples/complex-resource/index.js create mode 100644 examples/db.js create mode 100644 examples/functions/license.js create mode 100644 examples/functions/server-time.js delete mode 100644 examples/hidden-field/index.js create mode 100644 examples/models/book.js create mode 100644 examples/models/complex-resource.js create mode 100644 examples/models/user.js delete mode 100644 examples/multi-resource/functions/license.js delete mode 100644 examples/multi-resource/functions/server-time.js delete mode 100644 examples/multi-resource/index.js delete mode 100644 examples/multi-resource/resources/book.js delete mode 100644 examples/multi-resource/resources/user.js create mode 100644 examples/server.js delete mode 100644 examples/simple-with-data/index.js delete mode 100644 examples/simple/index.js delete mode 100644 src/ODataResource.js create mode 100644 src/mongo/Entity.js create mode 100644 src/mongo/parser/filterParser.js create mode 100644 src/mongo/parser/functionsParser.js rename src/{ => mongo}/parser/orderbyParser.js (100%) rename src/{ => mongo}/parser/selectParser.js (89%) create mode 100644 src/mongo/parser/skipParser.js create mode 100644 src/mongo/parser/topParser.js create mode 100644 src/mongo/rest/count.js create mode 100644 src/mongo/rest/delete.js create mode 100644 src/mongo/rest/get.js create mode 100644 src/mongo/rest/list.js create mode 100644 src/mongo/rest/patch.js create mode 100644 src/mongo/rest/post.js create mode 100644 src/mongo/rest/put.js delete mode 100644 src/odata/Entity.js create mode 100644 src/odata/entity/Entity.js create mode 100644 src/odata/entity/parser/count.js create mode 100644 src/odata/entity/parser/filter.js create mode 100644 src/odata/entity/parser/keys.js create mode 100644 src/odata/entity/parser/select.js create mode 100644 src/odata/entity/parser/value.js delete mode 100644 src/parser/countParser.js delete mode 100644 src/parser/filterParser.js delete mode 100644 src/parser/functionsParser.js delete mode 100644 src/parser/skipParser.js delete mode 100644 src/parser/topParser.js delete mode 100644 src/pipes.js delete mode 100644 src/rest/count.js delete mode 100644 src/rest/delete.js delete mode 100644 src/rest/get.js delete mode 100644 src/rest/index.js delete mode 100644 src/rest/list.js delete mode 100644 src/rest/patch.js delete mode 100644 src/rest/post.js delete mode 100644 src/rest/put.js rename test/{mocked => }/api.Function.js (75%) rename test/{mocked => }/hook.action.js (94%) create mode 100644 test/hook.entity.after.js create mode 100644 test/hook.entity.before.js rename test/{mocked => }/metadata.action.js (84%) rename test/{mocked => }/metadata.complex.type.js (93%) rename test/{mocked => }/metadata.format.js (78%) rename test/{mocked => }/metadata.function.js (92%) create mode 100644 test/metadata.js rename test/{mocked => }/mimetype.defaults.js (71%) delete mode 100644 test/mocked/api.Resource.js delete mode 100644 test/mocked/hook.all.after.js delete mode 100644 test/mocked/hook.all.before.js delete mode 100755 test/mocked/hook.delete.after.js delete mode 100644 test/mocked/hook.delete.before.js delete mode 100755 test/mocked/hook.get.after.js delete mode 100644 test/mocked/hook.get.before.js delete mode 100644 test/mocked/hook.list.after.js delete mode 100644 test/mocked/hook.list.before.js delete mode 100755 test/mocked/hook.post.after.js delete mode 100644 test/mocked/hook.post.before.js delete mode 100755 test/mocked/hook.put.after.js delete mode 100644 test/mocked/hook.put.before.js delete mode 100644 test/mocked/model.complex.action.js delete mode 100644 test/mocked/model.complex.filter.js delete mode 100644 test/mocked/model.complex.js delete mode 100644 test/mocked/model.custom.id.js delete mode 100644 test/mocked/model.hidden.field.js delete mode 100644 test/mocked/model.special.name.js delete mode 100644 test/mocked/odata.count.js delete mode 100644 test/mocked/odata.query.count.js delete mode 100644 test/mocked/odata.query.filter.functions.js delete mode 100644 test/mocked/odata.query.filter.js create mode 100644 test/mongo/connected/model.complex.js create mode 100644 test/mongo/connected/model.special.name.js create mode 100644 test/mongo/connected/rest.list.js rename test/{neededDbRunning => mongo/connected}/rest.post.js (62%) rename test/{mocked => mongo}/metadata.js (86%) rename test/{mocked => mongo}/metadata.resource.complex.js (75%) create mode 100644 test/mongo/mocked/model.complex.filter.js create mode 100644 test/mongo/mocked/model.custom.id.js create mode 100644 test/mongo/mocked/model.hidden.field.js create mode 100644 test/mongo/mocked/odata.count.js create mode 100644 test/mongo/mocked/odata.query.count.js create mode 100644 test/mongo/mocked/odata.query.filter.functions.js create mode 100644 test/mongo/mocked/odata.query.filter.js rename test/{mocked => }/odata.actions.js (53%) rename test/{mocked => }/odata.batch.js (55%) rename test/{mocked => }/odata.entity.js (90%) rename test/{mocked => }/odata.error.js (89%) create mode 100644 test/odata.filter.js rename test/{mocked => }/odata.functions.js (80%) create mode 100644 test/support/books.model.js create mode 100644 test/support/db.js diff --git a/.vscode/launch.json b/.vscode/launch.json index 3347b6d..6dc9b1b 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -20,7 +20,7 @@ "dot", "--timeout", "300000", - "test/mocked/odata.entity.js" + "test/odata.filter.js" ], "env": { "LOG_LEVEL": "debug" @@ -33,7 +33,7 @@ "skipFiles": [ "/**" ], - "program": "${workspaceFolder}/examples/simple/index.js" + "program": "${workspaceFolder}/examples/server.js" } ] } \ No newline at end of file diff --git a/examples/actions/50off.js b/examples/actions/50off.js new file mode 100644 index 0000000..ba09388 --- /dev/null +++ b/examples/actions/50off.js @@ -0,0 +1,10 @@ +module.exports = function(req, res, next){ + req.$odata.mongo.Books.findById(req.$odata.$Key.id, function(err, book){ + book.price = +(book.price / 2).toFixed(2); + book.save(function(err){ + res.$odata.result = book.toObject(); + res.$odata.status = 200; + next(); + }); + }); +}; \ No newline at end of file diff --git a/examples/complex-resource/index.js b/examples/complex-resource/index.js deleted file mode 100644 index 3edb622..0000000 --- a/examples/complex-resource/index.js +++ /dev/null @@ -1,24 +0,0 @@ -var odata = require('../../'); - -server = odata('mongodb://localhost/odata-test'); - -var order = { - custom: { - id: String, - name: String - }, - orderItems: [{ - quantity: Number, - product: { - id: String, - name: String, - price: Number - } - }] -}; - -server.resource('orders', order); - -server.listen(3000, function(){ - console.log('OData services has started, you can visit by http://localhost:3000/orders'); -}); diff --git a/examples/db.js b/examples/db.js new file mode 100644 index 0000000..6c963f1 --- /dev/null +++ b/examples/db.js @@ -0,0 +1,22 @@ +const mongoose = require('mongoose'); + +require('./models/book'); +require('./models/complex-resource'); +require('./models/user'); + +module.exports = mongoose.connect(process.env.DATABASE || 'mongodb://localhost:27017/odata-test', null, (err) => { + if (err) { + console.error(err.message); + console.error('Failed to connect to database on startup.'); + process.exit(); + } +}); + +// provide a event listener to handle not able to connect DB. +// events +mongoose.connection.on('connected', function () { + console.log('MongoDB connected!'); +}); +mongoose.connection.on('disconnected', function () { + console.log('MongoDB disconnected!'); +}); \ No newline at end of file diff --git a/examples/functions/license.js b/examples/functions/license.js new file mode 100644 index 0000000..5be02d6 --- /dev/null +++ b/examples/functions/license.js @@ -0,0 +1,6 @@ +module.exports = function(req, res, next) { + res.$odata.result = { license: 'MIT' }; + res.$odata.status = 200; + next(); +}; + diff --git a/examples/functions/server-time.js b/examples/functions/server-time.js new file mode 100644 index 0000000..4efbf77 --- /dev/null +++ b/examples/functions/server-time.js @@ -0,0 +1,7 @@ +module.exports = function(req, res, next) { + res.$odata.result = { date: new Date() }; + res.$odata.status = 200; + next(); +}; + + diff --git a/examples/hidden-field/index.js b/examples/hidden-field/index.js deleted file mode 100644 index 6844faa..0000000 --- a/examples/hidden-field/index.js +++ /dev/null @@ -1,16 +0,0 @@ -var odata = require('../../'); - -server = odata('mongodb://localhost/odata-test'); - -server.resource('users', { - name: String, - password: { - type: String, - select: false - } -}); - -server.listen(3000, function(){ - console.log('OData services has started, you can visit by http://localhost:3000/users'); -}); - diff --git a/examples/models/book.js b/examples/models/book.js new file mode 100644 index 0000000..a066d2a --- /dev/null +++ b/examples/models/book.js @@ -0,0 +1,25 @@ +const mongoose = require('mongoose'); + +const Schema = mongoose.Schema; + +const data = { + author: String, + description: String, + genre: String, + price: Number, + publish_date: Date, + title: String +}; + +const ModelSchema = new Schema(data, + { + timestamps: true, + toObject: { + virtuals: true, + }, + toJSON: { + virtuals: true, + }, + }); + +module.exports = mongoose.model('Books', ModelSchema); \ No newline at end of file diff --git a/examples/models/complex-resource.js b/examples/models/complex-resource.js new file mode 100644 index 0000000..4825131 --- /dev/null +++ b/examples/models/complex-resource.js @@ -0,0 +1,31 @@ +const mongoose = require('mongoose'); + +const Schema = mongoose.Schema; + +const data = { + custom: { + id: String, + name: String + }, + orderItems: [{ + quantity: Number, + product: { + id: String, + name: String, + price: Number + } + }] +}; + +const ModelSchema = new Schema(data, + { + timestamps: true, + toObject: { + virtuals: true, + }, + toJSON: { + virtuals: true, + }, + }); + +module.exports = mongoose.model('ComplexResource', ModelSchema); \ No newline at end of file diff --git a/examples/models/user.js b/examples/models/user.js new file mode 100644 index 0000000..ef4138a --- /dev/null +++ b/examples/models/user.js @@ -0,0 +1,24 @@ +const mongoose = require('mongoose'); + +const Schema = mongoose.Schema; + +const data = { + name: String, + password: { + type: String, + select: false + } +}; + +const ModelSchema = new Schema(data, + { + timestamps: true, + toObject: { + virtuals: true, + }, + toJSON: { + virtuals: true, + }, + }); + +module.exports = mongoose.model('Users', ModelSchema); \ No newline at end of file diff --git a/examples/multi-resource/functions/license.js b/examples/multi-resource/functions/license.js deleted file mode 100644 index 7481149..0000000 --- a/examples/multi-resource/functions/license.js +++ /dev/null @@ -1,10 +0,0 @@ -var func = require('../../../').Function; - -var router = func(); - -router.get('/license', function(req, res, next) { - res.jsonp({ license: 'MIT' }); -}); - -module.exports = router; - diff --git a/examples/multi-resource/functions/server-time.js b/examples/multi-resource/functions/server-time.js deleted file mode 100644 index 7d2844d..0000000 --- a/examples/multi-resource/functions/server-time.js +++ /dev/null @@ -1,11 +0,0 @@ -var func = require('../../../').Function; - -var router = func(); - -router.get('/server-time', function(req, res, next) { - res.jsonp({ date: new Date() }); -}); - -module.exports = router; - - diff --git a/examples/multi-resource/index.js b/examples/multi-resource/index.js deleted file mode 100644 index 6685c8f..0000000 --- a/examples/multi-resource/index.js +++ /dev/null @@ -1,16 +0,0 @@ -var odata = require('../../'); - -var server = odata('mongodb://localhost/odata-test'); - -// init resources -server.use(requiere('./resources/book')); -server.use(requiere('./resources/user')); - -// init functions -server.use(requiere('./functions/license')); -server.use(requiere('./functions/server-time')); - -server.listen(3000, function(){ - console.log('OData services has started, you can visit by http://localhost:3000'); -}); - diff --git a/examples/multi-resource/resources/book.js b/examples/multi-resource/resources/book.js deleted file mode 100644 index a3d4279..0000000 --- a/examples/multi-resource/resources/book.js +++ /dev/null @@ -1,10 +0,0 @@ -var Resource = require('../../../').Resource; - -module.exports = Resource('book', { - author: String, - description: String, - genre: String, - price: Number, - publish_date: Date, - title: String -}); diff --git a/examples/multi-resource/resources/user.js b/examples/multi-resource/resources/user.js deleted file mode 100644 index 7eaa314..0000000 --- a/examples/multi-resource/resources/user.js +++ /dev/null @@ -1,6 +0,0 @@ -var Resource = require('../../../').Resource; - -module.exports = Resource('user', { - name: String, - password: String -}); diff --git a/examples/server.js b/examples/server.js new file mode 100644 index 0000000..3e0edf9 --- /dev/null +++ b/examples/server.js @@ -0,0 +1,48 @@ +const odata = require('../'); + +const server = odata(); + +// database +const connection = require('./db'); + +server.addBefore((req, res, next) => { + req.$odata = { + mongo: connection + }; + next(); +}); + +// entities +const comlexResource = require('./models/complex-resource'); +server.mongoEntity('complex-resource', comlexResource); + +const user = require('./models/user'); +server.mongoEntity('user', user); + +const bookModel = require('./models/book'); +const _50off = require('./actions/50off'); +const bookEntity = server.mongoEntity('book', bookModel); +bookEntity.action('50off', _50off, { + binding: 'entity' +}); + +// add some test data +const data = require('../test/support/books.json'); +bookModel.deleteMany({}, function(err, result) { + data.forEach(function(item) { + entity = new bookModel(item); + entity.save(); + }); +}); + +// unbind functions +const serverTime = require('./functions/server-time'); +server.function('server-time', serverTime); + +const license = require('./functions/license'); +server.function('license', license); + +// server start +server.listen(3000, function(){ + console.log('OData services has started, you can visit by http://localhost:3000/'); +}); \ No newline at end of file diff --git a/examples/simple-with-data/index.js b/examples/simple-with-data/index.js deleted file mode 100644 index f1f17c0..0000000 --- a/examples/simple-with-data/index.js +++ /dev/null @@ -1,11 +0,0 @@ -var server = require('../simple/'); -var data = require("../../test/support/books.json"); - -model = server._db.model('book'); - -model.remove({}, function(err, result) { - data.map(function(item) { - entity = new model(item); - entity.save(); - }); -}); diff --git a/examples/simple/index.js b/examples/simple/index.js deleted file mode 100644 index aac1dc2..0000000 --- a/examples/simple/index.js +++ /dev/null @@ -1,39 +0,0 @@ -var odata = require('../../'); - -var server = odata('mongodb://localhost/odata-test'); - -var bookInfo = { - author: String, - description: String, - genre: String, - price: Number, - publish_date: Date, - title: String -}; - -server.resource('book', bookInfo) - .action('50off', function(req, res, next){ - server.repository('book').findById(req.params.id, function(err, book){ - book.price = +(book.price / 2).toFixed(2); - book.save(function(err){ - res.jsonp(book); - }); - }); - }); - -server.get('/license', function(req, res, next){ - res.jsonp({license:'MIT'}); -}); - -server.on('connected', function() { - console.log('MongoDB connected!'); -}); -server.on('disconnected', function() { - console.log('MongoDB disconnected!'); -}); - -server.listen(3000, function(){ - console.log('OData services has started, you can visit by http://localhost:3000/book'); -}); - -module.exports = server; diff --git a/package.json b/package.json index 04bc10a..9c8c754 100644 --- a/package.json +++ b/package.json @@ -16,6 +16,7 @@ "node": ">=0.12" }, "scripts": { + "start": "node ./examples/server.js", "lint": "eslint src/", "prepublish": "make", "test": "make", diff --git a/src/ODataResource.js b/src/ODataResource.js deleted file mode 100644 index 395c828..0000000 --- a/src/ODataResource.js +++ /dev/null @@ -1,202 +0,0 @@ -import rest from './rest'; -import { min } from './utils'; -import Action from './odata/Action'; - -function hook(resource, pos, fn) { - let method = resource._currentMethod; - if (method === 'all') { - method = ['get', 'post', 'put', 'delete', 'patch', 'list']; - } else { - method = [method]; - } - /*eslint-disable */ - method.map((curr) => { - if (resource._hooks[curr][pos]) { - const _fn = resource._hooks[curr][pos]; - resource._hooks[curr][pos] = (...args) => { - _fn.apply(resource, args); - fn.apply(resource, args); - }; - } else { - resource._hooks[curr][pos] = fn; - } - }); - /* eslint-enable */ -} - -export default class { - constructor(server, name, userModel) { - this._server = server; - this._name = name; - this._url = name; - this._model = userModel; - this._hooks = { - list: {}, - get: {}, - post: {}, - put: {}, - delete: {}, - patch: {}, - count: {} - }; - this.actions = {}; - this._options = { - maxTop: 10000, - maxSkip: 10000, - orderby: undefined, - }; - } - - getName() { - return this._name; - } - - setModel(model) { - this.model = model; - } - - action(name, fn, options) { - this.actions[name] = new Action(name, fn, - { - ...options, - resource: this - }); - - return this.actions[name]; - } - - maxTop(count) { - this._maxTop = count; - return this; - } - - maxSkip(count) { - this._maxSkip = count; - return this; - } - - orderBy(field) { - this._orderby = field; - return this; - } - - list() { - this._currentMethod = 'list'; - return this; - } - - get() { - this._currentMethod = 'get'; - return this; - } - - post() { - this._currentMethod = 'post'; - return this; - } - - put() { - this._currentMethod = 'put'; - return this; - } - - delete() { - this._currentMethod = 'delete'; - return this; - } - - patch() { - this._currentMethod = 'patch'; - return this; - } - - all() { - this._currentMethod = 'all'; - return this; - } - - before(fn) { - hook(this, 'before', fn); - return this; - } - - after(fn) { - hook(this, 'after', fn); - return this; - } - - auth(fn) { - let method = this._currentMethod; - if (method === 'all') { - method = ['get', 'post', 'put', 'delete', 'patch', 'list']; - } else { - method = [method]; - } - method.map((curr) => { - this._hooks[curr].auth = fn; - return undefined; - }); - return this; - } - - url(url) { - this._url = url; - return this; - } - - _validateUrl() { - // remove '/' if url is startwith it. - if (this._url.indexOf('/') === 0) { - this._url = this._url.substr(1); - } - - // not allow contain '/' in url. - if (this._url.indexOf('/') >= 0) { - throw new Error(`Url of resource[${this._name}] can't contain "/",` - + 'it can only be allowed to exist in the beginning.'); - } - } - - match(method, url) { - const setting = this._server.getSettings(); - - this._validateUrl(); - - const routes = rest.getMiddlewares(this._url, this._hooks, this.model, { - maxTop: min([setting.maxTop, this._maxTop]), - maxSkip: min([setting.maxSkip, this._maxSkip]), - orderby: this._orderby || setting.orderby, - }); - const route = routes.find((item) => { - if (item.method === method) { - const match = url.match(item.regex); - - return match; - } - }); - - if (route) { - return [route.middleware]; - } - - return Object.keys(this.actions) - .map(name => this.actions[name].match(method, url)) - .find(ctrl => ctrl); - } - - _router(setting = {}) { - this._validateUrl(); - - const params = { - url: this._url, - options: { - maxTop: min([setting.maxTop, this._maxTop]), - maxSkip: min([setting.maxSkip, this._maxSkip]), - orderby: this._orderby || setting.orderby, - }, - hooks: this._hooks, - }; - - return rest.getRouter(this.model, params); - } -} diff --git a/src/middlewares/writer.js b/src/middlewares/writer.js index 3212b9d..95360da 100644 --- a/src/middlewares/writer.js +++ b/src/middlewares/writer.js @@ -71,6 +71,9 @@ export default function writer(req, res) { throw new Error('Status not setted'); default: + if (!res.$odata.result) { + throw new Error('If status not equal 204 res.$odata.result has to be set'); + } } const status = res.$odata.status; diff --git a/src/mongo/Entity.js b/src/mongo/Entity.js new file mode 100644 index 0000000..278e409 --- /dev/null +++ b/src/mongo/Entity.js @@ -0,0 +1,269 @@ +import list from './rest/list'; +import post from './rest/post'; +import put from './rest/put'; +import del from './rest/delete'; +import patch from './rest/patch'; +import get from './rest/get'; +import count from './rest/count'; +import { validate, validateIdentifier } from '../odata/validator'; + +export default class Entity { + constructor(name, model) { + this.name = name; + this.model = model; + + this.complexTypes = {}; + this.count = 0; + this.mapping = { + id: { + target: '_id', + attributes: { + $Type: 'node.odata.ObjectId', + $Nullable: false, + } + } + }; + + } + + getHandler() { + const rest = { + post, + put, + patch, + delete: del, + get, + count, + list + }; + const routes = Object.keys(rest); + let handler = {}; + + routes.forEach((route) => { + handler[route] = async (req, res, next) => { + try { + req.$odata = { + ...req.$odata, + Model: this.model + }; + await rest[route](req, res, next); + + } catch (err) { + next(err); + } + }; + }); + + return handler; + } + + getMetadata() { + if (!this.metadata) { + const { paths } = this.model.schema; + + this.metadata = this.visitor('EntityType', paths); + } + + return this.metadata; + } + + getMapping() { + if (!this.metadata) { + this.getMetadata(); + } + + return this.mapping; + } + + getComplexTypes() { + if (!this.metadata) { + this.getMetadata(); + } + + return this.complexTypes; + } + + visitor(type, node) { + switch (type) { + case 'Property': + return this.visitProperty(node); + + case 'ComplexType': + return this.visitComplexType(node); + + default: + return this.visitEntityType(node); + } + } + + visitProperty(node) { + const result = {}; + + if ('Array ObjectID'.indexOf(node.instance) === -1 && node.defaultValue) { + result.$DefaultValue = node.defaultValue; + } + + switch (node.instance) { + case 'ObjectID': + result.$Type = 'node.odata.ObjectId'; + break; + + case 'Boolean': + result.$Type = 'Edm.Boolean'; + break; + + case 'Number': + result.$Type = 'Edm.Double'; + break; + + case 'Date': + result.$Type = 'Edm.DateTimeOffset'; + break; + + case 'String': + result.$Type = 'Edm.String'; + if (node.options?.maxLength) { + result.$MaxLength = node.options.maxLength; + } + break; + + case 'Array': + result.$Collection = true; + if (node.schema && node.schema.paths) { + // Array of complex type + result.$Type = this.complexType(node); + } else { + const arrayItemType = this.visitor('Property', { + instance: node.options.type[0].name || node.options.type[0].type.name //Enums have an object with enum and type + }); + + result.$Type = arrayItemType.$Type; + } + break; + + default: + return null; + } + + return result; + } + + complexType(node) { + this.count += 1; + + const notClassifiedName = `${this.name}${node.path}Child${this.count}`; + const properties = this.visitor('ComplexType', node.schema.paths); + + if (this.complexTypes[notClassifiedName]) { + throw new Error(`Complex type with name ${notClassifiedName} allready exists`); + } + + validateIdentifier(notClassifiedName); + + const typeObject = { + $Kind: 'ComplexType', + ...properties + }; + validate(typeObject); + + this.complexTypes[notClassifiedName] = typeObject; + + return `node.odata.${notClassifiedName}`; + } + + visitComplexType(node) { + return this.reduceProperties(node); + } + + reduceProperties(node) { + const keys = Object.keys(node); + const simpleProperties = keys.filter((path) => path !== '__v' && path.indexOf('.') === -1) + .reduce((previousProperty, curentProperty) => { + let result; + let propertyName = Object.keys(this.mapping) + .find(name => this.mapping[name]?.target === curentProperty); + + if (propertyName && this.mapping[propertyName].attributes) { + result = { + ...previousProperty, + [propertyName]: this.mapping[propertyName].attributes + } + + } else { + propertyName = curentProperty.replace(/\./g, '-'); + if (propertyName !== curentProperty) { + this.addMapping(curentProperty, propertyName); + } + + result = { + ...previousProperty, + [propertyName]: this.visitor('Property', node[curentProperty]), + }; + } + + return result; + }, {}); + + const deepNodes = keys.filter(key => { + if (key.indexOf('.') >= 0) { + return true; + } + }) + .reduce((previousProperty, curentProperty) => { + const nameParts = curentProperty.split('.'); + const objName = nameParts[0]; + const propertyName = curentProperty.substring(objName.length + 1); + + if (!previousProperty[objName]) { + // not first property of an object + previousProperty[objName] = { + path: objName, + schema: { + paths: {} + } + }; + } + + previousProperty[objName].schema.paths[propertyName] = { + ...node[curentProperty], + path: propertyName + }; + + return previousProperty; + }, {}); + + const deepProperties = Object.keys(deepNodes) + .reduce((previousProperty, curentProperty) => { + previousProperty[curentProperty] = { + $Type: this.complexType(deepNodes[curentProperty]) + }; + + return previousProperty; + }, {}); + + return { + ...simpleProperties, + ...deepProperties + } + } + + visitEntityType(node) { + const properties = this.reduceProperties(node); + + return { + $Key: ['id'], + ...properties, + }; + } + + addMapping(mongoProperty, odataProperty) { + if (this.mapping[odataProperty]) { + throw new Error(`Mapping for property '${odataProperty}' is already set`); + } + + this.mapping[odataProperty] = { + target: mongoProperty + }; + } + +} diff --git a/src/mongo/parser/filterParser.js b/src/mongo/parser/filterParser.js new file mode 100644 index 0000000..cd7935a --- /dev/null +++ b/src/mongo/parser/filterParser.js @@ -0,0 +1,61 @@ +// Operator Description Example +// Comparison Operators +// eq Equal Address/City eq 'Redmond' +// ne Not equal Address/City ne 'London' +// gt Greater than Price gt 20 +// ge Greater than or equal Price ge 10 +// lt Less than Price lt 20 +// le Less than or equal Price le 100 +// has Has flags Style has Sales.Color'Yellow' #todo +// Logical Operators +// and Logical and Price le 200 and Price gt 3.5 +// or Logical or Price le 3.5 or Price gt 200 #todo +// not Logical negation not endswith(Description,'milk') #todo + +// eg. +// http://host/service/Products?$filter=Price lt 10.00 +// http://host/service/Categories?$filter=Products/$count lt 10 + +import functions from './functionsParser'; + +function parse($filter) { + // returns a valid monggose filter object + // odata functions are to be replaced + if (!$filter) { + return; + } + + const keys = Object.keys($filter); + + keys.forEach(name => { + if ($filter[name].$function) { + const func = { + ...functions[$filter[name].$function.$name](name, $filter[name].$function) + }; + + $filter[name] = func; + } + + // parsing rekursion + if(Array.isArray($filter[name])) { + $filter[name].forEach(subCondition => parse(subCondition)); + } + + if ($filter[name].eq === null) { + $filter[name].$exists = false; + delete $filter[name].eq; + } + + if ($filter[name].ne === null) { + $filter[name].$exists = true; + delete $filter[name].ne; + + } + + }); + + return $filter; + +}; + +export default parse; \ No newline at end of file diff --git a/src/mongo/parser/functionsParser.js b/src/mongo/parser/functionsParser.js new file mode 100644 index 0000000..eaf6bb9 --- /dev/null +++ b/src/mongo/parser/functionsParser.js @@ -0,0 +1,85 @@ +const convertToOperator = (odataOperator) => { + let operator; + switch (odataOperator) { + case 'eq': + operator = '=='; + break; + case 'ne': + operator = '!='; + break; + case 'gt': + operator = '>'; + break; + case 'ge': + operator = '>='; + break; + case 'lt': + operator = '<'; + break; + case 'le': + operator = '<='; + break; + default: + throw new Error('Invalid operator code, expected one of ["==", "!=", ">", ">=", "<", "<="].'); + } + return operator; +}; + +// contains(CompanyName,'icrosoft') +const contains = (name, $filter) => ({ + $where: `this.${name}.indexOf(${$filter.$parameter}) != -1` +}); + +// indexof(CompanyName,'X') eq 1 +const indexof = (name, $filter) => { + const { $parameter, $operator, $value } = $filter; + const operator = convertToOperator($operator); + + return { + $where: `this.${name}.indexOf(${$parameter}) ${operator} ${$value}` + }; +}; + +// year(publish_date) eq 2000 +const year = (name, $filter) => { + const result = {}; + const { $value, $operator } = $filter; + + const start = new Date(+$value, 0, 1); + const end = new Date(+$value + 1, 0, 1); + + switch ($operator) { + case 'eq': + result.$gte = start; + result.$lt = end; + break; + case 'ne': { + result = { + $or: [{ + $lt: start + }, { + $gte: end + }] + } ; + break; + } + case 'gt': + result.$gte = end; + break; + case 'ge': + result.$gte = start; + break; + case 'lt': + result.$lt = start; + break; + case 'le': + result.$lt = end; + break; + default: + throw new Error('Invalid operator code, expected one of ["==", "!=", ">", ">=", "<", "<="].'); + } + + return result; +}; + +export default { indexof, year, contains }; diff --git a/src/parser/orderbyParser.js b/src/mongo/parser/orderbyParser.js similarity index 100% rename from src/parser/orderbyParser.js rename to src/mongo/parser/orderbyParser.js diff --git a/src/parser/selectParser.js b/src/mongo/parser/selectParser.js similarity index 89% rename from src/parser/selectParser.js rename to src/mongo/parser/selectParser.js index 122a226..0553a13 100644 --- a/src/parser/selectParser.js +++ b/src/mongo/parser/selectParser.js @@ -2,12 +2,12 @@ // -> // query.select('Rating ReleaseDate') export default (query, $select) => new Promise((resolve) => { - if (!$select) { + if (!$select?.length) { resolve(); return; } - const list = $select.split(',').map((item) => item.trim()); + const list = $select; const selectFields = { _id: 0 }; const { tree } = query.model.schema; diff --git a/src/mongo/parser/skipParser.js b/src/mongo/parser/skipParser.js new file mode 100644 index 0000000..9032468 --- /dev/null +++ b/src/mongo/parser/skipParser.js @@ -0,0 +1,14 @@ +// ?$skip=10 +// -> +// query.skip(10) +export default (query, skip) => new Promise((resolve) => { + if (Number.isNaN(+skip)) { + resolve(); + return; + } + + if (skip > 0) { + query.skip(skip); + } + resolve(); +}); diff --git a/src/mongo/parser/topParser.js b/src/mongo/parser/topParser.js new file mode 100644 index 0000000..6ee5d0a --- /dev/null +++ b/src/mongo/parser/topParser.js @@ -0,0 +1,13 @@ +// ?$top=10 +// -> +// query.top(10) +export default (query, top) => new Promise((resolve) => { + if (Number.isNaN(+top)) { + resolve(); + return; + } + if (top > 0) { + query.limit(top); + } + resolve(); +}); diff --git a/src/mongo/rest/count.js b/src/mongo/rest/count.js new file mode 100644 index 0000000..39b7a72 --- /dev/null +++ b/src/mongo/rest/count.js @@ -0,0 +1,19 @@ +export default (req, res, next) => { + const query = req.$odata.Model.find(); + + query.count((err, count) => { + if (err) { + const result = new Error(err.message); + + result.previous = err; + result.status = 500; + next(result); + + } else { + res.$odata.result = count.toString(); + res.$odata.supportedMimetypes = ['text/plain']; + next(); + + } + }); +}; diff --git a/src/mongo/rest/delete.js b/src/mongo/rest/delete.js new file mode 100644 index 0000000..c55f40e --- /dev/null +++ b/src/mongo/rest/delete.js @@ -0,0 +1,17 @@ +export default (req, res, next) => { + req.$odata.Model.remove({ _id: req.params.id }, (err, result) => { + if (err) { + return next(err); + } + + if (JSON.parse(result).n === 0) { + const error = new Error('Not Found'); + + error.status = 404; + return next(error); + } + + res.$odata.status = 204; + next(); + }); +}; diff --git a/src/mongo/rest/get.js b/src/mongo/rest/get.js new file mode 100644 index 0000000..13076b2 --- /dev/null +++ b/src/mongo/rest/get.js @@ -0,0 +1,17 @@ +export default (req, res, next) => { + req.$odata.Model.findById(req.params.id, (err, entity) => { + if (err) { + return next(err); + } + + if (!entity) { + const result = new Error('Not Found'); + + result.status = 404; + return next(result); + } + + res.$odata.result = entity.toObject(); + return next(); + }); +}; diff --git a/src/mongo/rest/list.js b/src/mongo/rest/list.js new file mode 100644 index 0000000..4403f59 --- /dev/null +++ b/src/mongo/rest/list.js @@ -0,0 +1,45 @@ +import filterParser from '../parser/filterParser'; +import orderbyParser from '../parser/orderbyParser'; +import skipParser from '../parser/skipParser'; +import topParser from '../parser/topParser'; +import selectParser from '../parser/selectParser'; + +function _dataQuery(model, { + filter, orderby, skip, top, select, +}) { + return new Promise((resolve, reject) => { + const query = model.find(filterParser(filter)); + orderbyParser(query, orderby) + .then(() => skipParser(query, skip)) + .then(() => topParser(query, top)) + .then(() => selectParser(query, select)) + .then(() => query.exec((err, data) => { + if (err) { + return reject(err); + } + return resolve({ value: data.map(item => item.toObject()) }); + })) + .catch(reject); + }); +} + +export default (req, res, next) => { + const params = { + count: req.$odata.$count, + filter: req.$odata.$filter, + orderby: req.$odata.$orderby, + skip: req.$odata.$skip, + top: req.$odata.$top, + select: req.$odata.$select, + // TODO expand: req.$odata.$expand, + // TODO search: req.$odata.$search, + }; + + _dataQuery(req.$odata.Model, params).then((result) => { + res.$odata.result = { + ...res.$odata.result, + ...result + }; + next(); + }).catch(next); +}; diff --git a/src/mongo/rest/patch.js b/src/mongo/rest/patch.js new file mode 100644 index 0000000..f27a44a --- /dev/null +++ b/src/mongo/rest/patch.js @@ -0,0 +1,16 @@ +export default (req, res, next) => { + req.$odata.Model.findOne({ id: req.params.id }, (err, entity) => { + if (err) { + next(err); + } else { + req.$odata.Model.update({ id: req.params.id }, { ...entity, ...req.body }, (err1) => { + if (err1) { + next(err1); + } else { + res.$odata.result = { ...entity.toObject(), ...req.body }; + next(); + } + }); + } + }); +}; diff --git a/src/mongo/rest/post.js b/src/mongo/rest/post.js new file mode 100644 index 0000000..49c8e1d --- /dev/null +++ b/src/mongo/rest/post.js @@ -0,0 +1,20 @@ +export default (req, res, next) => { + if (!Object.keys(req.body).length) { + const error = new Error(); + + error.status = 422; + next(error); + } else { + const entity = new req.$odata.Model(req.body); + + entity.save((err) => { + if (err) { + next(err); + } else { + res.$odata.result = entity.toObject(); + res.$odata.status = 201; + next(); + } + }); + } +}; diff --git a/src/mongo/rest/put.js b/src/mongo/rest/put.js new file mode 100644 index 0000000..77d3198 --- /dev/null +++ b/src/mongo/rest/put.js @@ -0,0 +1,46 @@ +function _updateEntity(req, res, next, entity) { + req.$odata.Model.findByIdAndUpdate(entity.id, req.body, (err) => { + if (err) { + return next(err); + } + const newEntity = req.body; + + newEntity.id = entity.id; + res.$odata.result = newEntity; + + return next(); + }); +} + +function _createEntity(req, res, next) { + const uuidReg = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i; + if (!uuidReg.test(req.params.id)) { + const err = new Error('Id is invalid.'); + + err.status = 400; + return next(err); + } + const newEntity = MongooseModel.create(req.body); + newEntity._id = req.params.id; + return newEntity.save((err2) => { + if (err2) { + return next(err2); + } + res.$odata.result = newEntity.toObject(); + res.$odata.status = 201; + return next(); + }); +} + +export default (req, res, next) => { + req.$odata.Model.findOne({ _id: req.params.id }, (err, entity) => { + if (err) { + return next(err); + } + if (entity) { + _updateEntity(req, res, next, entity); + } else { + _createEntity(req, res, next); + } + }); +}; diff --git a/src/odata/Action.js b/src/odata/Action.js index 71dc05c..67bd1ee 100644 --- a/src/odata/Action.js +++ b/src/odata/Action.js @@ -47,9 +47,11 @@ export default class Action { match(method, url) { const regex = this.getPath(true); + const beforeHooks = this.binding === 'entity' ? [...this.resource.getNavigation().beforeHooks, ...this.hooks.before] : this.hooks.before; + if (method === 'post' && url.match(regex)) { - return [...this.hooks.before, this.fn, ...this.hooks.after]; + return [...beforeHooks, this.fn, ...this.hooks.after]; } } @@ -77,8 +79,8 @@ export default class Action { if (this.binding) { result.$IsBound = true; result.$Parameter = [{ - $Name: this.resource._url || this.resource.name, - $Type: `node.odata.${this.resource._url || this.resource.name}`, + $Name: this.resource.name, + $Type: `node.odata.${this.resource.name}`, $Collection: this.binding === 'collection' ? true : undefined, }]; } @@ -111,20 +113,20 @@ export default class Action { if (!this.resource) { throw new Error(`Binding '${this.binding}' require a resource`) } - path = asRegex ? new RegExp(`\/?${this.resource._url}\\('?[A-Fa-f0-9]*'?\\)\/${this.name}$`) : `/${this.resource._url}\\(:id\\)/${this.name}`; + path = asRegex ? new RegExp(`\/?${this.resource.name}\\('?[A-Fa-f0-9]*'?\\)\/${this.name}$`) : `${this.resource.getNavigation().url}/${this.name}`; break; case 'collection': if (!this.resource) { throw new Error(`Binding '${this.binding}' require a resource`) } - path = asRegex ? new RegExp(`\/?${this.resource._url}\/${this.name}$`) : `/${this.resource._url}/${this.name}`; + path = asRegex ? new RegExp(`\/?${this.resource.name}\/${this.name}$`) : `/${this.resource.name}/${this.name}`; break; default: if (this.binding) { throw new Error(`Invalid binding '${this.binding}'`); } else { if (this.resource) { - throw new Error(`Use of the unbound action '${this.name}' by a resource '${this.resource._url}' is not intended`) + throw new Error(`Use of the unbound action '${this.name}' by a resource '${this.resource.name}' is not intended`) } path = asRegex ? new RegExp(`(node\.odata)?\/?${this.name}$`) : `/node.odata.${this.name}`; } @@ -135,8 +137,9 @@ export default class Action { getOperationRouter(path, fn) { let router = Router(); + const beforeHooks = this.binding === 'entity' ? [...this.resource.getNavigation().beforeHooks, ...this.hooks.before] : this.hooks.before; - router.post(path, ...this.hooks.before, fn, ...this.hooks.after); + router.post(path, ...beforeHooks, fn, ...this.hooks.after); return router; }; diff --git a/src/odata/Entity.js b/src/odata/Entity.js deleted file mode 100644 index ba766c2..0000000 --- a/src/odata/Entity.js +++ /dev/null @@ -1,179 +0,0 @@ -import { validateIdentifier, validate } from "./validator"; -import { Router } from 'express'; -import Hooks from "./Hooks"; - -export default class Entity { - constructor(name, handler, metadata) { - const notImplemented = (req, res) => { - const error = new Error(); - - error.status = 501; - throw error; - }; - - this.name = name; - this.handler = { - list: notImplemented, - get: notImplemented, - post: notImplemented, - put: notImplemented, - delete: notImplemented, - patch: notImplemented, - count: notImplemented, - ...handler - }; - this.metadata = { - $Kind: 'EntityType', - ...metadata - }; - - this.actions = {}; - this.hooks = new Hooks(); - } - - addBefore(fn, name) { - this.hooks.addBefore(fn, name); - } - - addAfter(fn, name) { - this.hooks.addAfter(fn, name); - } - - action(name, fn, options) { - this.actions[name] = new Action(name, fn, - { - ...options, - resource: this - }); - - return this.actions[name]; - } - - match(method, url) { - validateIdentifier(this.name); - - const routes = this.getRoutes(); - const route = routes.find((item) => { - if (item.method === method) { - const match = url.match(item.regex); - - return match; - } - }); - - if (route) { - return [ - ...this.hooks.before, - this.handler[route.name], - ...this.hooks.after, - this.convertAttributes.bind(this) - ]; - } - - return Object.keys(this.actions) - .map(name => this.actions[name].match(method, url)) - .find(ctrl => ctrl); - } - - // convert DattimeOffset to valid value - convertAttributes(req, res, next) { - if (res.$odata.result?.value && Array.isArray(res.$odata.result?.value)) { - // list of entities - res.$odata.result.value.forEach(this.checkPropertyValues.bind(this)); - } else if (res.$odata.result) { - this.checkPropertyValues(res.$odata.result); - } - next(); - } - - checkPropertyValues(entity) { - const entityMetadata = this.getMetadata(); - const keys = Object.keys(entity); - - keys.forEach(member => { - if (entityMetadata[member]?.$Type === "Edm.DateTimeOffset" - && Object.prototype.toString.call(entity[member]) === '[object Date]') { - entity[member] = entity[member].toISOString().replace(/\.[0-9]{3}/,'') - } - }); - } - - getRouter() { - validateIdentifier(this.name); - validate(this.metadata); - - const router = Router(); - const routes = this.getRoutes(this.name); - - routes.forEach((route) => { - const { - name, method, url - } = route; - router[method](url, ...this.hooks.before, - (req, res, next) => { - res.$odata.status = 200; - this.handler[name](req, res, next); - }, - this.convertAttributes.bind(this), - ...this.hooks.after); - }); - return router; - } - - getMetadata() { - return this.metadata; - } - - getRoutes() { - const resourceListURL = `/${this.name}`; - const resourceListRegex = new RegExp(`(^\/?${this.name}[?#])|(^\/?${this.name}$)`); - const resourceURL = `${resourceListURL}\\(:id\\)`; - const resourceRegex = new RegExp(`^\/?${this.name}\\([^)]+\\)`); - - return [ - { - name: 'post', - method: 'post', - url: resourceListURL, - regex: resourceListRegex - }, - { - name: 'put', - method: 'put', - url: resourceURL, - regex: resourceRegex - }, - { - name: 'patch', - method: 'patch', - url: resourceURL, - regex: resourceRegex - }, - { - name: 'delete', - method: 'delete', - url: resourceURL, - regex: resourceRegex - }, - { - name: 'get', - method: 'get', - url: resourceURL, - regex: resourceRegex - }, - { - name: 'count', - method: 'get', - url: resourceListURL + '/([\$])count', - regex: new RegExp(`(^\/?${this.name}\/\\$count[?]?)|(^\/?${this.name}\/\\$count$)`) - }, - { - name: 'list', - method: 'get', - url: resourceListURL, - regex: resourceListRegex - } - ]; - } - -} \ No newline at end of file diff --git a/src/odata/Hooks.js b/src/odata/Hooks.js index 7e0dab0..a02e656 100644 --- a/src/odata/Hooks.js +++ b/src/odata/Hooks.js @@ -22,9 +22,11 @@ export default class Hooks { suppressNext(fn, name, isFinal) { return async (req, res, next) => { try { - const con = new Console(); + if (name) { + const con = new Console(); - con.debug(`Hook ${name} started`); + con.debug(`Hook ${name} started`); + } const combine = new Promise(async (resolve, reject) => { try { @@ -34,7 +36,7 @@ export default class Hooks { } resolve(); }); - + if (prom && prom.then) { await prom; resolve(); @@ -42,7 +44,7 @@ export default class Hooks { resolve(); } - } catch(err) { + } catch (err) { reject(err); } }); diff --git a/src/odata/Metadata.js b/src/odata/Metadata.js index 34a6d25..cd4bc47 100644 --- a/src/odata/Metadata.js +++ b/src/odata/Metadata.js @@ -1,38 +1,16 @@ import { Router } from 'express'; -import ODataResource from '../ODataResource'; -import Entity from './Entity'; +import Entity from './entity/Entity'; import Function from '../ODataFunction'; import { validate, validateIdentifier } from './validator'; export default class Metadata { constructor(server) { this._server = server; - this._count = 0; this._path = '/\\$metadata'; this.complexTypes = {}; } - complexType(name, properties) { - if (this.complexTypes[name]) { - throw new Error(`Complex type with name ${name} allready exists`); - } - - validateIdentifier(name); - - const typeObject = { - $Kind: 'ComplexType', - ...properties - }; - validate(typeObject); - - this.complexTypes[name] = typeObject; - } - - get() { - return this; - } - match(method, url) { if (method === 'get' && url.indexOf(this._path.replace(/\\/g, '')) === 0) { @@ -62,115 +40,6 @@ export default class Metadata { return router; } - visitProperty(node, model) { - const result = {}; - - if (model.default) { - result.$DefaultValue = model.default; - } - - switch (node.instance) { - case 'ObjectId': - result.$Type = 'node.odata.ObjectId'; - break; - - case 'Boolean': - result.$Type = 'Edm.Boolean'; - break; - - case 'Number': - result.$Type = 'Edm.Double'; - break; - - case 'Date': - result.$Type = 'Edm.DateTimeOffset'; - break; - - case 'String': - result.$Type = 'Edm.String'; - if (model.maxLength) { - result.$MaxLength = model.maxLength; - } - break; - - case 'Array': - result.$Collection = true; - if (node.schema && node.schema.paths) { - this._count += 1; - const notClassifiedName = `${node.path}Child${this._count}`; - // Array of complex type - result.$Type = `node.odata.${notClassifiedName}`; - this.complexType(notClassifiedName, this.visitor('ComplexType', node.schema.paths, model[0])); - } else { - const arrayItemType = this.visitor('Property', { - instance: node.options.type[0].name || node.options.type[0].type.name //Enums have an object with enum and type - }, model[0]); - - result.$Type = arrayItemType.$Type; - } - break; - - default: - return null; - } - - return result; - } - - resolveModelproperty(model, property) { - const props = property.split('.'); - - if (props.length > 1) { - const index = property.indexOf('.') + 1; - - return this.resolveModelproperty(model[props[0]], property.substr(index)); - } - - return model[property]; - } - - visitEntityType(node, model) { - const properties = Object.keys(node) - .filter((path) => path !== '_id') - .reduce((previousProperty, curentProperty) => { - const modelProperty = this.resolveModelproperty(model, curentProperty); - const propertyName = curentProperty.replace(/\./g, '-'); - const result = { - ...previousProperty, - [propertyName]: this.visitor('Property', node[curentProperty], modelProperty), - }; - - return result; - }, {}); - - return { - $Kind: 'EntityType', - $Key: ['id'], - id: { - $Type: 'node.odata.ObjectId', - $Nullable: false, - }, - ...properties, - }; - } - - visitComplexType(node, model) { - const properties = Object.keys(node) - .filter((item) => item !== '_id') - .reduce((previousProperty, curentProperty) => { - const propertyName = curentProperty.replace(/\./g, '-'); - const modelProperty = this.resolveModelproperty(model, curentProperty); - const result = { - ...previousProperty, - [propertyName]: this.visitor('Property', node[curentProperty], modelProperty), - }; - - return result; - }, {}); - - return properties; - } - static visitFunction(node) { return { $Kind: 'Function', @@ -179,19 +48,23 @@ export default class Metadata { } visitor(type, node, model) { - switch (type) { - case 'Property': - return this.visitProperty(node, model); + return Metadata.visitFunction(node); + } - case 'ComplexType': - return this.visitComplexType(node, model); + complexType(name, properties) { + if (this.complexTypes[name]) { + throw new Error(`Complex type with name ${name} allready exists`); + } - case 'Function': - return Metadata.visitFunction(node); + validateIdentifier(name); - default: - return this.visitEntityType(node, model); - } + const typeObject = { + $Kind: 'ComplexType', + ...properties + }; + validate(typeObject); + + this.complexTypes[name] = typeObject; } ctrl() { @@ -200,17 +73,7 @@ export default class Metadata { const resource = this._server.resources[currentResource]; const result = { ...previousResource }; - if (resource instanceof ODataResource) { - const { paths } = resource.model.model.schema; - - result[currentResource] = this.visitor('EntityType', paths, resource._model); - const actions = Object.keys(resource.actions); - if (actions && actions.length) { - actions.forEach((action) => { - result[action] = resource.actions[action].getMetadata(); - }); - } - } else if (resource instanceof Entity) { + if (resource instanceof Entity) { result[currentResource] = resource.getMetadata(); const actions = Object.keys(resource.actions); if (actions && actions.length) { @@ -231,7 +94,7 @@ export default class Metadata { const result = { ...previousResource }; const resource = this._server.resources[currentResource]; - if (resource instanceof ODataResource || resource instanceof Entity) { + if (resource instanceof Entity) { result[currentResource] = { $Collection: true, $Type: `node.odata.${currentResource}`, diff --git a/src/odata/ServiceDocument.js b/src/odata/ServiceDocument.js index 4534c98..4343def 100644 --- a/src/odata/ServiceDocument.js +++ b/src/odata/ServiceDocument.js @@ -1,6 +1,5 @@ import { Router } from 'express'; -import Resource from '../ODataResource'; -import Entity from './Entity'; +import Entity from './entity/Entity'; export default class Metadata { constructor(server) { @@ -44,7 +43,7 @@ export default class Metadata { ctrl(req) { const entityTypeNames = Object.keys(this._server.resources); const entitySets = entityTypeNames - .filter((item) => this._server.resources[item] instanceof Resource || this._server.resources[item] instanceof Entity) + .filter((item) => this._server.resources[item] instanceof Entity) .map((currentResource) => ({ name: currentResource, kind: 'EntitySet', diff --git a/src/odata/entity/Entity.js b/src/odata/entity/Entity.js new file mode 100644 index 0000000..16c6bd6 --- /dev/null +++ b/src/odata/entity/Entity.js @@ -0,0 +1,317 @@ +import { validateIdentifier, validate } from "../validator"; +import { Router } from 'express'; +import Hooks from "../Hooks"; +import { min } from '../../utils'; +import Action from '../Action'; +import parseSelect from './parser/select'; +import parseKeys from './parser/keys'; +import parseCount from './parser/count'; +import parseFilter from './parser/filter'; + +export default class Entity { + constructor(name, handler, metadata, settings, mapping) { + const notImplemented = (req, res) => { + const error = new Error(); + + error.status = 501; + throw error; + }; + + this.name = name; + this.handler = { + list: notImplemented, + get: notImplemented, + post: notImplemented, + put: notImplemented, + delete: notImplemented, + patch: notImplemented, + count: notImplemented, + ...handler + }; + this.metadata = { + $Kind: 'EntityType', + ...metadata + }; + + this.actions = {}; + this.hooks = new Hooks(); + + this.options = { + ...settings + }; + + this.mapping = mapping || {}; + } + + addBefore(fn, name) { + this.hooks.addBefore(fn, name); + } + + addAfter(fn, name) { + this.hooks.addAfter(fn, name); + } + + action(name, fn, options) { + this.actions[name] = new Action(name, fn, + { + ...options, + resource: this + }); + + return this.actions[name]; + } + + match(method, url) { + validateIdentifier(this.name); + + const action = Object.keys(this.actions) + .map(name => this.actions[name].match(method, url)) + .find(ctrl => ctrl); + if (action) { + return action + } + + const routes = this.getRoutes(); + const route = routes.find((item) => { + if (item.method === method) { + const match = url.match(item.regex); + + return match; + } + }); + + return [ + this.parsingMiddleware.bind(this), + ...this.hooks.before, + this.ctrl(route.name, this.handler[route.name]), + ...this.hooks.after, + this.adaptResultAccordingMetadata.bind(this) + ]; + + + } + + // convert DattimeOffset to valid value + adaptResultAccordingMetadata(req, res, next) { + if (res.$odata.result?.value && Array.isArray(res.$odata.result?.value)) { + // list of entities + res.$odata.result.value.forEach(this.adaptEntityAccordingMetadata.bind(this)); + } else if (res.$odata.result) { + this.adaptEntityAccordingMetadata(res.$odata.result); + } + next(); + } + + + parsingMiddleware(req, res, next) { + try { + req.$odata = req.$odata || {}; + + req.$odata.$Key = parseKeys(req, this.name, this.metadata); + req.$odata.$select = parseSelect(req, this.name, this.metadata); + req.$odata.$filter = parseFilter(req, this.name, this.metadata); + req.$odata.$count = parseCount(req, this.name, this.metadata); + + req.$odata = { + ...req.$odata, + body: req.body, + $top: req.query.$top && min([req.query.$top, this.options.maxTop]), + $skip: req.query.$skip && min([req.query.$skip, this.options.maxSkip]), + $orderby: this.options.orderby || req.query.$orderby, + $expand: req.query.$expand, // TODO : implement expand + $search: req.query.$search // TODO : implement search + }; + + next(); + + } catch (err) { + next(err); + } + } + + adaptEntityAccordingMetadata(entity) { + const entityMetadata = this.getMetadata(); + const keys = Object.keys(entity); + + keys.forEach(member => { + let propertyMetadata; + + if (entityMetadata[member]) { + propertyMetadata = entityMetadata[member]; + + } else { + const keys = Object.keys(this.mapping); + const mapping = keys.find(name => this.mapping[name].target === member); + + if (mapping) { + propertyMetadata = this.mapping[mapping].attributes; + entity[mapping] = entity[member]; + + } + + delete entity[member]; // hide attributes not exposed in metadata + + } + + if (!propertyMetadata) { + return; + } + + if (propertyMetadata.$Type === "Edm.DateTimeOffset" + && Object.prototype.toString.call(entity[member]) === '[object Date]') { + entity[member] = entity[member].toISOString().replace(/\.[0-9]{3}/, '') + } + }); + } + + getRouter() { + validateIdentifier(this.name); + validate(this.metadata); + + const actions = Object.keys(this.actions) + .map(name => this.actions[name].getRouter()); + + const router = Router(); + const routes = this.getRoutes(this.name); + + routes.forEach((route) => { + const { + name, method, url + } = route; + const hooksAfter = name === 'count' ? [...this.hooks.after] : [ + this.adaptResultAccordingMetadata.bind(this), + ...this.hooks.after + ]; + + router[method](url, + this.parsingMiddleware.bind(this), + ...this.hooks.before, + this.ctrl(name, this.handler[name]), + ...hooksAfter); + }); + + return [actions, router]; + } + + getNavigation() { + return { + url: this.getResourceUrl(), + beforeHooks: [this.parsingMiddleware.bind(this)] + }; + } + + ctrl(name, handler) { + return (req, res, next) => { + res.$odata.status = 200; + if (name === 'list' && req.$odata.$count) { + const countResponse = { + $odata: {} + }; + + this.handler.count(req, countResponse, err => { + if (err) { + next(err); + } + + debugger; + res.$odata.result = { + ['@odata.count']: countResponse.$odata.result + }; + handler(req, res, next); + }); + + } else { + handler(req, res, next); + + } + }; + } + + getMetadata() { + return this.metadata; + } + + getKeyParam(type, name) { + switch (type) { + case 'Edm.String': + case 'node.odata.ObjectId': + return `%27:${name}%27`; + + default: + return `:${name}`; + } + } + + getResourceUrl() { + const resourceListURL = `/${this.name}`; + + if (this.metadata.$Key.length === 1) { + const value = this.getKeyParam(this.metadata[this.metadata.$Key[0]].$Type, this.metadata.$Key[0]); + + return `${resourceListURL}\\(${value}\\)`; + + } else { + let result = this.metadata.$Key.reduce((previous, current, index) => { + const key = this.metadata[current]; + const value = this.getKeyParam(this.metadata[current].$Type, current); + + return !index ? `${previous}${key}=${value}` : `${previous},${key}=${value}`; + }, `${resourceListURL}\\(`); + + return `${result}\\)`; + } + } + + getRoutes() { + const resourceListURL = `/${this.name}`; + const resourceListRegex = new RegExp(`(^\/?${this.name}[?#])|(^\/?${this.name}$)`); + const resourceURL = this.getResourceUrl(); + const resourceRegex = new RegExp(`^\/?${this.name}\\([^)]+\\)`); + + return [ + { + name: 'post', + method: 'post', + url: resourceListURL, + regex: resourceListRegex + }, + { + name: 'put', + method: 'put', + url: resourceURL, + regex: resourceRegex + }, + { + name: 'patch', + method: 'patch', + url: resourceURL, + regex: resourceRegex + }, + { + name: 'delete', + method: 'delete', + url: resourceURL, + regex: resourceRegex + }, + { + name: 'get', + method: 'get', + url: resourceURL, + regex: resourceRegex + }, + { + name: 'count', + method: 'get', + url: resourceListURL + '/([\$])count', + regex: new RegExp(`(^\/?${this.name}\/\\$count[?]?)|(^\/?${this.name}\/\\$count$)`) + }, + { + name: 'list', + method: 'get', + url: resourceListURL, + regex: resourceListRegex + } + ]; + } + +} \ No newline at end of file diff --git a/src/odata/entity/parser/count.js b/src/odata/entity/parser/count.js new file mode 100644 index 0000000..cdc2d52 --- /dev/null +++ b/src/odata/entity/parser/count.js @@ -0,0 +1,15 @@ +export default function(req, entity, metadata) { + switch (req.query.$count) { + case 'true': + return true; + + case 'false': + return false; + + case undefined: + return undefined; + + default: + throw new Error('Unknown $count option, only "true" and "false" are supported.'); + } +} \ No newline at end of file diff --git a/src/odata/entity/parser/filter.js b/src/odata/entity/parser/filter.js new file mode 100644 index 0000000..2dbb3b3 --- /dev/null +++ b/src/odata/entity/parser/filter.js @@ -0,0 +1,205 @@ +import parseValue from './value'; + +export default function (req, entity, metadata) { + const replaceString = (filter, dictionary) => { + const replacer = (match, p1) => { + const result = `$${dictionary.length}`; + + dictionary[result] = p1; + return result; + }; + + if (filter.search(/'[^']*'/) === -1) { + return visitor('splitOr', filter, dictionary); + + } + + const replacedFilter = filter.replace(/'([^']*)'/g, replacer); + + return visitor('splitOr', replacedFilter, dictionary); + + }; + + const splitOr = (filter, dictionary) => { + const result = { + $or: [] + }; + + if (filter.search(/\s+or\s+/i) === -1) { + return visitor('splitAnd', filter, dictionary); + } + + const subConditions = filter.split(/\s+or\s+/i); + + subConditions.forEach(item => { + result.$or.push(visitor('splitAnd', item, dictionary)); + }); + + return result; + }; + + const splitAnd = (filter, dictionary) => { + const result = []; + + if (filter.search(/\s+and\s+/i) === -1) { + return visitor('splitCondition', filter, dictionary); + } + + const subConditions = filter.split(/\s+and\s+/i); + + subConditions.forEach(item => { + const newCondition = visitor('splitCondition', item, dictionary); + const properties = Object.keys(newCondition); + const oldCondition = result.find(item => + properties.find(name => item[name])); + const sameProperty = properties.find(name => oldCondition && oldCondition[name]); + + if (oldCondition) { + oldCondition[sameProperty] = { + ...oldCondition[sameProperty], + ...newCondition[sameProperty] + }; + } else { + result.push(newCondition); + } + }); + + return result.length > 1 ? { $and: result } : result[0]; + }; + + const splitCondition = (filter, dictionary) => { + const operatorIndex = filter.search(/\s+(eq|ne|gt|ge|lt|le)\s+/i); + + if (operatorIndex === -1) { + return visitor('parseFunction', filter, dictionary); + } + + const operands = filter.split(/\s+(eq|ne|gt|ge|lt|le)\s+/i); + + if (operands?.length != 3) { + const err = new Error(`Two operands and one operator was expected in '${filter}'`); + + err.status = 400; + throw err; + } + let operator; + const operatorPrettified = operands[1].trim().toLowerCase(); + + switch (operatorPrettified) { + case 'eq': + case 'ne': + case 'gt': + case 'lt': + operator = `$${operatorPrettified}`; + break; + + case 'ge': + operator = '$gte'; + break; + + case 'le': + operator = '$lte'; + break; + + default: + throw new Error(`Unexpected operator '${operatorPrettified}' in '${$filter}'`); + } + + const property = operands[0].trim(); + + if (!metadata[property]) { + const err = new Error(`Entity '${entity}' has not a property named '${property}'`); + + err.status = 400; + throw err; + } + + const value = operands[2].trim(); + + return { + [property]: { + [operator]: parseValue(dictionary[value] || value, metadata[property]) + } + }; + }; + + const parseFunction = (filter, dictionary) => { + // contains(CompanyName,'freds') + // indexof(CompanyName,'lfreds') eq 1 + // year(BirthDate) eq 0 + const match = filter.match(/(contains|indexof|year)\s*\(\s*([^,]+)\s*[,]?\s*([^)]*)\)\s*(eq|ne|gt|ge|lt|le)?\s*([0-9]*)/i); + + if (!match) { + const err = new Error(`Text '${filter}' can not be interpreted`); + + err.status = 400; + throw err; + } + + const func = match[1].toLowerCase(); + const property = match[2]; + + if (!metadata[property]) { + const err = new Error(`Entity '${entity}' has no property named '${property}'`); + + err.status = 400; + throw err; + } + + const parameter = match[3] && dictionary[match[3]] ? dictionary[match[3]] : match[3]; + + // 0 indexof(CompanyName,$0) eq 10 + // 1 indexof + // 2 CompanyName + // 3 $0 + // 4 eq + // 5 10 + + // 0 year(BirthDate) eq 0 + // 1 year + // 2 BirthDate + // 3 + // 4 eq + // 5 0 + + return { + [property]: { + $function: { + $name: func, + $parameter: parameter, + $operator: match[4], + $value: match[5] || undefined + } + } + }; + }; + + const visitor = (step, filter, dictionary) => { + if (!filter || !filter.trim()) { + return undefined; + } + + switch (step) { + case 'parseFunction': + return parseFunction(filter, dictionary); + + case 'replaceStrings': + return replaceString(filter, []); + + case 'splitAnd': + return splitAnd(filter, dictionary); + + case 'splitCondition': + return splitCondition(filter, dictionary); + + case 'splitOr': + return splitOr(filter, dictionary); + + default: + throw new Error(`Step '${step}' not implemented`); + } + }; + + return visitor('replaceStrings', req.query.$filter); + +} \ No newline at end of file diff --git a/src/odata/entity/parser/keys.js b/src/odata/entity/parser/keys.js new file mode 100644 index 0000000..e38f6e7 --- /dev/null +++ b/src/odata/entity/parser/keys.js @@ -0,0 +1,18 @@ +import parseValue from './value'; + +export default function(req, entity, metadata) { + const result = {}; + + if (req.params) { + const params = Object.keys(req.params) + .filter(param => metadata.$Key.indexOf(param) >= 0); + + if (params) { + params.forEach(param => { + result[param] = parseValue(req.params[param], metadata[param]); + }); + } + } + + return result; +} \ No newline at end of file diff --git a/src/odata/entity/parser/select.js b/src/odata/entity/parser/select.js new file mode 100644 index 0000000..4f3c89a --- /dev/null +++ b/src/odata/entity/parser/select.js @@ -0,0 +1,14 @@ +export default function(req, entity, metadata) { + return req.query.$select?.split(',').map((item) => { + const property = item.trim(); + + if (!metadata[property]) { + const err = new Error(`Entity '${entity}' have not a property with name '${property}'`); + + err.status = 400; + throw err; + } + + return property; + }); +} \ No newline at end of file diff --git a/src/odata/entity/parser/value.js b/src/odata/entity/parser/value.js new file mode 100644 index 0000000..f1ad9cb --- /dev/null +++ b/src/odata/entity/parser/value.js @@ -0,0 +1,72 @@ +function parseBoolean(value, metadata) { + if (value === 'true') { + return true; + } + + if (value === 'false') { + return false; + } + + const err = new Error(`Text '${value}' is not valid boolean representation`); + + err.status = 400; + throw err; +} + +function parseNumber(value, metadata) { + const result = +value; + + if (Number.isNaN(value)) { + const err = new Error(`Text '${value}' is not valid represenation of a number`); + + err.status = 400; + throw err; + } + + return result; +} + +function parseDate(value, metadata) { + const result = new Date(value); + + if (Number.isNaN(result.valueOf())) { + const err = new Error(`Text '${value}' is not valid represenation of a date`); + + err.status = 400; + throw err; + } +} + +export default function (value, metadata) { + const trimmed = value.trim(); + + if (metadata.$Nullable && trimmed === 'null') { + return null; + } + + switch (metadata.$Type) { + case 'Edm.Boolean': + return parseBoolean(trimmed, metadata); + + case 'Edm.Byte': + case 'Edm.Decimal': + case 'Edm.Double': + case 'Edm.Duration': + case 'Edm.Int16': + case 'Edm.Int32': + case 'Edm.Int64': + case 'Edm.SByte': + case 'Edm.Single': + return parseNumber(trimmed, metadata); + + case 'Edm.Date': + case 'Edm.DateTimeOffset': + case 'Edm.Duration': + case 'Edm.TimeOfDay': + return parseDate(trimmed, metadata); + + default: + return trimmed; + } + +} \ No newline at end of file diff --git a/src/parser/countParser.js b/src/parser/countParser.js deleted file mode 100644 index d656d78..0000000 --- a/src/parser/countParser.js +++ /dev/null @@ -1,28 +0,0 @@ -import filterParser from './filterParser'; - -// ?$count=10 -// -> -// query.count(10) -export default (mongooseModel, $count, $filter) => new Promise((resolve, reject) => { - if ($count === undefined) { - resolve(); - return; - } - - switch ($count) { - case 'true': { - const query = mongooseModel.find(); - filterParser(query, $filter, mongooseModel); - query.count((err, count) => { - resolve(count); - }); - break; - } - case 'false': - resolve(); - break; - default: - reject(new Error('Unknown $count option, only "true" and "false" are supported.')); - break; - } -}); diff --git a/src/parser/filterParser.js b/src/parser/filterParser.js deleted file mode 100644 index a0fedb2..0000000 --- a/src/parser/filterParser.js +++ /dev/null @@ -1,187 +0,0 @@ -// Operator Description Example -// Comparison Operators -// eq Equal Address/City eq 'Redmond' -// ne Not equal Address/City ne 'London' -// gt Greater than Price gt 20 -// ge Greater than or equal Price ge 10 -// lt Less than Price lt 20 -// le Less than or equal Price le 100 -// has Has flags Style has Sales.Color'Yellow' #todo -// Logical Operators -// and Logical and Price le 200 and Price gt 3.5 -// or Logical or Price le 3.5 or Price gt 200 #todo -// not Logical negation not endswith(Description,'milk') #todo - -// eg. -// http://host/service/Products?$filter=Price lt 10.00 -// http://host/service/Categories?$filter=Products/$count lt 10 - -import functions from './functionsParser'; -import { split } from '../utils'; - -const OPERATORS_KEYS = ['eq', 'ne', 'gt', 'ge', 'lt', 'le', 'has']; - -const stringHelper = { - has: (str, key) => str.indexOf(key) >= 0, - - isBeginWith: (str, key) => str.indexOf(key) === 0, - - isEndWith: (str, key) => str.lastIndexOf(key) === (str.length - key.length), - - removeEndOf: (str, key) => { - if (stringHelper.isEndWith(str, key)) { - return str.substr(0, str.length - key.length); - } - return str; - }, -}; - -class KeyParser { - constructor(model) { - this._model = model; - } - - getConvertedKey(input) { - let key = input; - - if (key === 'id') { - key = '_id'; - return key; - } - if (this._model[key]) { - // known simple property - return key; - } - - const match = key.match(/\s*(contains|indexof|year)\(\s*([\w+-]+)/); - - if (match) { - // contains function was called with id e.g. contains(title, 'ggm') - const functionKey = match[2]; - - key = key.replace(functionKey, this.getConvertedKey(functionKey)); - } else { - key = Object.keys(this._model.model.schema.paths).find((item) => { - const replacedDots = item.replace(/\./g, '(.|-){1}'); - const regex = new RegExp(`^${replacedDots}$`); - - return key.match(regex); - }); - if (!key) { - const error = new Error(`Unknown property '${this._input}' in entity '${this._model.name}'`); - - error.status = '400'; - throw error; - } - } - return key; - } -} - -const validator = { - formatValue: (value) => { - let val; - if (value === 'true') { - val = true; - } else if (value === 'false') { - val = false; - } else if (!Number.isNaN(+value)) { - val = +value; - } else if (stringHelper.isBeginWith(value, "'") && stringHelper.isEndWith(value, "'")) { - val = value.slice(1, -1); - } else if (value === 'null') { - val = value; - } else { - return ({ err: new Error(`Syntax error at '${value}'.`) }); - } - return ({ val }); - }, -}; - -export default (query, $filter, model) => new Promise((resolve, reject) => { - if (!$filter) { - resolve(); - return; - } - - try { - const condition = split($filter, ['and', 'or']) - .filter((item) => (item !== 'and' && item !== 'or')); - - condition.forEach((item) => { - // parse "indexof(title,'X1ML') gt 0" - const conditionArr = split(item, OPERATORS_KEYS); - if (conditionArr.length === 0) { - // parse "contains(title,'X1ML')" - conditionArr.push(item); - } - if (conditionArr.length !== 3 && conditionArr.length !== 1) { - throw new Error(`Syntax error at '${item}'.`); - } - - const keyParser = new KeyParser(model); - let key = conditionArr[0]; - const [, odataOperator, value] = conditionArr; - - key = keyParser.getConvertedKey(key); - - let val; - if (value !== undefined) { - const result = validator.formatValue(value); - if (result.err) { - return reject(result.err); - } - val = result.val; - } - - // function query - const functionKey = key.substring(0, key.indexOf('(')); - if (['indexof', 'year', 'contains'].indexOf(functionKey) > -1) { - functions[functionKey](query, key, odataOperator, val); - } else { - if (conditionArr.length === 1) { - return reject(new Error(`Syntax error at '${item}'.`)); - } - if (value === 'null') { - switch (odataOperator) { - case 'eq': - query.exists(key, false); - return resolve(); - case 'ne': - query.exists(key, true); - return resolve(); - default: - break; - } - } - // operator query - switch (odataOperator) { - case 'eq': - query.where(key).equals(val); - break; - case 'ne': - query.where(key).ne(val); - break; - case 'gt': - query.where(key).gt(val); - break; - case 'ge': - query.where(key).gte(val); - break; - case 'lt': - query.where(key).lt(val); - break; - case 'le': - query.where(key).lte(val); - break; - default: - return reject(new Error("Incorrect operator at '#{item}'.")); - } - } - return query; - }); - resolve(); - } catch (error) { - reject(error); - } -}); diff --git a/src/parser/functionsParser.js b/src/parser/functionsParser.js deleted file mode 100644 index e1382fc..0000000 --- a/src/parser/functionsParser.js +++ /dev/null @@ -1,78 +0,0 @@ -const convertToOperator = (odataOperator) => { - let operator; - switch (odataOperator) { - case 'eq': - operator = '=='; - break; - case 'ne': - operator = '!='; - break; - case 'gt': - operator = '>'; - break; - case 'ge': - operator = '>='; - break; - case 'lt': - operator = '<'; - break; - case 'le': - operator = '<='; - break; - default: - throw new Error('Invalid operator code, expected one of ["==", "!=", ">", ">=", "<", "<="].'); - } - return operator; -}; - -// contains(CompanyName,'icrosoft') -const contains = (query, fnKey) => { - let [key, target] = fnKey.substring(fnKey.indexOf('(') + 1, fnKey.indexOf(')')).split(','); - [key, target] = [key.trim(), target.trim()]; - query.$where(`this.${key}.indexOf(${target}) != -1`); -}; - -// indexof(CompanyName,'X') eq 1 -const indexof = (query, fnKey, odataOperator, value) => { - let [key, target] = fnKey.substring(fnKey.indexOf('(') + 1, fnKey.indexOf(')')).split(','); - [key, target] = [key.trim(), target.trim()]; - const operator = convertToOperator(odataOperator); - query.$where(`this.${key}.indexOf(${target}) ${operator} ${value}`); -}; - -// year(publish_date) eq 2000 -const year = (query, fnKey, odataOperator, value) => { - const key = fnKey.substring(fnKey.indexOf('(') + 1, fnKey.indexOf(')')); - - const start = new Date(+value, 0, 1); - const end = new Date(+value + 1, 0, 1); - - switch (odataOperator) { - case 'eq': - query.where(key).gte(start).lt(end); - break; - case 'ne': { - const condition = [{}, {}]; - condition[0][key] = { $lt: start }; - condition[1][key] = { $gte: end }; - query.or(condition); - break; - } - case 'gt': - query.where(key).gte(end); - break; - case 'ge': - query.where(key).gte(start); - break; - case 'lt': - query.where(key).lt(start); - break; - case 'le': - query.where(key).lt(end); - break; - default: - throw new Error('Invalid operator code, expected one of ["==", "!=", ">", ">=", "<", "<="].'); - } -}; - -export default { indexof, year, contains }; diff --git a/src/parser/skipParser.js b/src/parser/skipParser.js deleted file mode 100644 index 5fdda1e..0000000 --- a/src/parser/skipParser.js +++ /dev/null @@ -1,16 +0,0 @@ -import { min } from '../utils'; - -// ?$skip=10 -// -> -// query.skip(10) -export default (query, skip, maxSkip) => new Promise((resolve) => { - if (Number.isNaN(+skip)) { - resolve(); - return; - } - const _skip = min([maxSkip, skip]); - if (_skip > 0) { - query.skip(_skip); - } - resolve(); -}); diff --git a/src/parser/topParser.js b/src/parser/topParser.js deleted file mode 100644 index 59aa866..0000000 --- a/src/parser/topParser.js +++ /dev/null @@ -1,16 +0,0 @@ -import { min } from '../utils'; - -// ?$top=10 -// -> -// query.top(10) -export default (query, top, maxTop) => new Promise((resolve) => { - if (Number.isNaN(+top)) { - resolve(); - return; - } - const _top = min([maxTop, top]); - if (_top > 0) { - query.limit(_top); - } - resolve(); -}); diff --git a/src/pipes.js b/src/pipes.js deleted file mode 100644 index 12a0fc4..0000000 --- a/src/pipes.js +++ /dev/null @@ -1,30 +0,0 @@ - -const authorizePipe = async (req, res, auth) => { - if (auth !== undefined && !await auth(req, res)) { - const result = new Error(); - - result.status = 401; - throw result; - } -}; - -const beforePipe = (req, res, before) => new Promise((resolve) => { - if (before) { - before(req.body, req, res); - } - resolve(); -}); - - -const afterPipe = (req, res, after, data) => new Promise((resolve) => { - if (after) { - after(data, req.body, req, res); - } - resolve(); -}); - -export default { - afterPipe, - authorizePipe, - beforePipe -}; diff --git a/src/rest/count.js b/src/rest/count.js deleted file mode 100644 index a92463e..0000000 --- a/src/rest/count.js +++ /dev/null @@ -1,23 +0,0 @@ -export default async (req, MongooseModel, options) => { - return new Promise((resolve, reject) => { - const query = MongooseModel.find(); - - query.count((err, count) => { - if (err) { - const result = new Error(err.message); - - result.previous = err; - result.status = 500; - reject(result); - - } else { - resolve({ - result: count.toString(), - status: 200, - supportedMimetypes: ['text/plain'] - }); - - } - }); - }); -}; diff --git a/src/rest/delete.js b/src/rest/delete.js deleted file mode 100644 index 794c2d4..0000000 --- a/src/rest/delete.js +++ /dev/null @@ -1,16 +0,0 @@ -export default (req, MongooseModel) => new Promise((resolve, reject) => { - MongooseModel.remove({ _id: req.params.id }, (err, result) => { - if (err) { - return reject(err); - } - - if (JSON.parse(result).n === 0) { - const error = new Error('Not Found'); - - error.status = 404; - return reject(error); - } - - return resolve({ status: 204 }); - }); -}); diff --git a/src/rest/get.js b/src/rest/get.js deleted file mode 100644 index 8fedb9b..0000000 --- a/src/rest/get.js +++ /dev/null @@ -1,19 +0,0 @@ -export default (req, MongooseModel) => new Promise((resolve, reject) => { - MongooseModel.findById(req.params.id, (err, entity) => { - if (err) { - return reject(err); - } - - if (!entity) { - const result = new Error('Not Found'); - - result.status = 404; - return reject(result); - } - - return resolve({ - result: entity, - status: 200 - }); - }); -}); diff --git a/src/rest/index.js b/src/rest/index.js deleted file mode 100644 index 3731a09..0000000 --- a/src/rest/index.js +++ /dev/null @@ -1,178 +0,0 @@ -import { Router } from 'express'; -import list from './list'; -import post from './post'; -import put from './put'; -import del from './delete'; -import patch from './patch'; -import get from './get'; -import pipes from '../pipes'; -import count from './count'; -import Console from '../writer/Console'; - -const getRoutes = (url, hooks) => { - const resourceListURL = `/${url}`; - const resourceListRegex = new RegExp(`(^\/?${url}[?#])|(^\/?${url}$)`); - const resourceURL = `${resourceListURL}\\(:id\\)`; - const resourceRegex = new RegExp(`^\/?${url}\\([^)]+\\)`); - - return [ - { - method: 'post', - url: resourceListURL, - regex: resourceListRegex, - ctrl: post, - hook: hooks.post, - }, - { - method: 'put', - url: resourceURL, - regex: resourceRegex, - ctrl: put, - hook: hooks.put, - }, - { - method: 'patch', - url: resourceURL, - regex: resourceRegex, - ctrl: patch, - hook: hooks.patch, - }, - { - method: 'delete', - url: resourceURL, - regex: resourceRegex, - ctrl: del, - hook: hooks.delete, - }, - { - method: 'get', - url: resourceURL, - regex: resourceRegex, - ctrl: get, - hook: hooks.get, - }, - { - method: 'get', - url: resourceListURL + '/([\$])count', - regex: new RegExp(`(^\/?${url}\/\\$count[?]?)|(^\/?${url}\/\\$count$)`), - ctrl: count, - hook: hooks.count, - }, - { - method: 'get', - url: resourceListURL, - regex: resourceListRegex, - ctrl: list, - hook: hooks.list, - } - ]; -}; - -function replaceDot(value) { - if (!(value === null || value === undefined || typeof value === 'function')) { - if (Array.isArray(value)) { - return replaceDotinArray(value); - } - if (typeof value === 'object') { - return replaceObject(value); - } - } - - return value; -} - -function replaceDotinArray(array) { - const result = array; - - result.forEach((item, index) => { - result[index] = replaceDot(item); - }); - return result; -} - -function replaceObject(obj) { - const result = obj; - - Object.keys(result).forEach((item) => { - if (item.match(/^[^@][^.]+(\.[^.]+)+/)) { - const newPropertyName = item.replace('.', '-'); - - result[newPropertyName] = replaceDot(result[item]); - delete result[item]; - } else { - result[item] = replaceDot(result[item]); - } - }); - - return result; -} - -const getMiddlewares = (url, hooks, mongooseModel, options) => { - const routes = getRoutes(url, hooks); - - return routes.map((route) => { - const { - ctrl, hook, method, url - } = route; - - const middleware = async (req, res, next) => { - try { - const con = new Console(); - - con.debug(`resource handler for ${method} ${url} started`); - - await pipes.authorizePipe(req, res, hook.auth); - await pipes.beforePipe(req, res, hook.before); - - const result = await ctrl(req, mongooseModel, options); - - res.$odata.result = result.result ? replaceDot(result.result) : result.result; - res.$odata.status = result.status || res.$odata.status; - res.$odata.supportedMimetypes = result.supportedMimetypes || res.$odata.supportedMimetypes; - - pipes.afterPipe(req, res, hook.after, res.$odata.result); - - next(); - - } catch (err) { - next(err); - } - }; - - return { - ...route, - middleware - }; - }); -}; - -const getRouter = (mongooseModel, { url, hooks, options }) => { - const routes = getMiddlewares(url, hooks, mongooseModel, options); - /*eslint-disable */ - const router = Router(); - /* eslint-enable */ - - routes.forEach((route) => { - const { - method, middleware, - } = route; - router[method](route.url, middleware); - }); - return router; -}; - -const getOperationRouter = (resourceUrl, actionUrl, fn, auth) => { - /*eslint-disable */ - const router = Router(); - /* eslint-enable */ - - router.post(`${resourceUrl}${actionUrl}`, (req, res, next) => { - pipes.authorizePipe(req, res, auth) - .then(() => fn(req, res, next)) - .catch((result) => next(result)); - }); - - return router; -}; - -export default { getRouter, getMiddlewares, getOperationRouter }; diff --git a/src/rest/list.js b/src/rest/list.js deleted file mode 100644 index b0c1a8a..0000000 --- a/src/rest/list.js +++ /dev/null @@ -1,65 +0,0 @@ -import countParser from '../parser/countParser'; -import filterParser from '../parser/filterParser'; -import orderbyParser from '../parser/orderbyParser'; -import skipParser from '../parser/skipParser'; -import topParser from '../parser/topParser'; -import selectParser from '../parser/selectParser'; - -function _countQuery(model, { count, filter }) { - return new Promise((resolve, reject) => { - countParser(model, count, filter).then((dataCount) => (dataCount !== undefined - ? resolve({ '@odata.count': dataCount }) - : resolve({}) - )).catch(reject); - }); -} - -function _dataQuery(model, { - filter, orderby, skip, top, select, -}, options) { - return new Promise((resolve, reject) => { - const query = model.find(); - filterParser(query, filter, model) - .then(() => orderbyParser(query, orderby || options.orderby)) - .then(() => skipParser(query, skip, options.maxSkip)) - .then(() => topParser(query, top, options.maxTop)) - .then(() => selectParser(query, select)) - .then(() => query.exec((err, data) => { - if (err) { - return reject(err); - } - return resolve({ value: data }); - })) - .catch(reject); - }); -} - -export default (req, MongooseModel, options) => new Promise((resolve, reject) => { - const params = { - count: req.query.$count, - filter: req.query.$filter, - orderby: req.query.$orderby, - skip: req.query.$skip, - top: req.query.$top, - select: req.query.$select, - // TODO expand: req.query.$expand, - // TODO search: req.query.$search, - }; - - Promise.all([ - _countQuery(MongooseModel, params), - _dataQuery(MongooseModel, params, options), - ]).then((results) => { - const entity = results.reduce((current, next) => ({ ...current, ...next })); - resolve({ - result: entity, - status: 200 - }); - }).catch((err) => { - const result = new Error(err.message); - - result.previous = err; - result.status = 500; - reject(result); - }); -}); diff --git a/src/rest/patch.js b/src/rest/patch.js deleted file mode 100644 index 5325631..0000000 --- a/src/rest/patch.js +++ /dev/null @@ -1,18 +0,0 @@ -export default (req, MongooseModel) => new Promise((resolve, reject) => { - MongooseModel.findOne({ id: req.params.id }, (err, entity) => { - if (err) { - reject(err); - } else { - MongooseModel.update({ id: req.params.id }, { ...entity, ...req.body }, (err1) => { - if (err1) { - reject(err1); - } else { - resolve({ - result: { ...entity, ...req.body }, - status: 200 - }); - } - }); - } - }); -}); diff --git a/src/rest/post.js b/src/rest/post.js deleted file mode 100644 index 3707e1a..0000000 --- a/src/rest/post.js +++ /dev/null @@ -1,21 +0,0 @@ -export default (req, MongooseModel) => new Promise((resolve, reject) => { - if (!Object.keys(req.body).length) { - const error = new Error(); - - error.status = 422; - reject(error); - } else { - const entity = MongooseModel.create(req.body); - - entity.save((err) => { - if (err) { - reject(err); - } else { - resolve({ - status: 201, - result: entity - }); - } - }); - } -}); diff --git a/src/rest/put.js b/src/rest/put.js deleted file mode 100644 index df3bd2f..0000000 --- a/src/rest/put.js +++ /dev/null @@ -1,42 +0,0 @@ -function _updateEntity(resolve, reject, MongooseModel, req, entity) { - MongooseModel.findByIdAndUpdate(entity.id, req.body, (err) => { - if (err) { - return reject(err); - } - const newEntity = req.body; - newEntity.id = entity.id; - return resolve({ - result: newEntity, - status: 200 - }); - }); -} - -function _createEntity(resolve, reject, MongooseModel, req, entity) { - const uuidReg = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i; - if (!uuidReg.test(req.params.id)) { - return reject({ status: 400 }, { text: 'Id is invalid.' }); - } - const newEntity = MongooseModel.create(req.body); - newEntity._id = req.params.id; - return newEntity.save((err) => { - if (err) { - return reject(err); - } - return resolve({ - status: 201, - result: newEntity - }); - }); -} - -export default (req, MongooseModel) => new Promise((resolve, reject) => { - MongooseModel.findOne({ _id: req.params.id }, (err, entity) => { - if (err) { - return reject(err); - } - return entity - ? _updateEntity(resolve, reject, MongooseModel, req, entity) - : _createEntity(resolve, reject, MongooseModel, req, entity); - }); -}); diff --git a/src/server.js b/src/server.js index 08603e1..9cff5c0 100644 --- a/src/server.js +++ b/src/server.js @@ -1,11 +1,10 @@ import createExpress from './express'; -import ODataResource from './ODataResource'; -import Entity from './odata/Entity'; +import MongoEntity from './mongo/Entity'; +import Entity from './odata/entity/Entity'; import Func from './ODataFunction'; import Metadata from './odata/Metadata'; import ServiceDocument from './odata/ServiceDocument'; import Batch from './odata/Batch'; -import Db from './db/db'; import Action from './odata/Action'; import error from './middlewares/error'; import writer from './middlewares/writer'; @@ -16,22 +15,18 @@ function checkAuth(auth, req) { } class Server { - constructor(db, prefix, options) { + constructor(prefix, options) { this._app = createExpress(options); this._settings = { maxTop: 10000, maxSkip: 10000, orderby: undefined, }; - this.defaultConfiguration(db, prefix); + this.defaultConfiguration(prefix); this.hooks = new Hooks(); - const dbValue = this.get('db'); this.hooks.addBefore(async (req, res) => { - req.$odata = { - mongo : dbValue._models - }; res.$odata = { status: 404, supportedMimetypes: ['application/json'] @@ -52,87 +47,69 @@ class Server { this._serviceDocument = new ServiceDocument(this); } + addBefore(fn, name) { + this.hooks.addBefore(fn, name); + } + + addAfter(fn, name) { + this.hooks.addAfter(fn, name); + } + function(url, middleware, params) { const func = new Func(url.replace(/[ /]+/, ''), middleware, params); this.resources[func.getName()] = func; } - resource(name, model) { - if (model === undefined) { - return this.resources[name]; + entity(name, handler, metadata, settings, mapping) { + if (this.resources[name]) { + throw new Error(`Entity with name "${name}" already defined`); } - const db = this.get('db'); - this.resources[name] = new ODataResource(this, name, model); - - this.resources[name].setModel(db.register(name, model)); + this.resources[name] = new Entity(name, handler, metadata, { + maxSkip: this._settings.maxSkip, + maxTop: this._settings.maxTop, + orderby: this._settings.orderby, + ...settings + }, mapping); return this.resources[name]; } - entity(name, handler, metadata) { - if (this.resources[name]) { - throw new Error(`Entity with name "${name}" already defined`); + mongoEntity(name, model, handler, metadata, settings, mapping) { + if (model === undefined) { + return this.resources[name]; } - this.resources[name] = new Entity(name, handler, metadata); + const entity = new MongoEntity(name, model); - return this.resources[name]; - } - - defaultConfiguration(db, prefix = '') { - this.set('app', this._app); - this.set('db', db); - this.set('prefix', prefix); - } + //this.resources[name].setModel(db.register(name, model)); + const complexTypes = entity.getComplexTypes(); - post(url, callback, auth) { - const app = this.get('app'); - const prefix = this.get('prefix'); - app.post(`${prefix}${url}`, (req, res, next) => { - if (checkAuth(auth, req)) { - callback(req, res, next); - } else { - res.status(401).end(); - } - }); - } + if (complexTypes) { + Object.keys(complexTypes) + .forEach(typeName => { + const type = complexTypes[typeName]; - put(url, callback, auth) { - const app = this.get('app'); - const prefix = this.get('prefix'); - app.put(`${prefix}${url}`, (req, res, next) => { - if (checkAuth(auth, req)) { - callback(req, res, next); - } else { - res.status(401).end(); - } - }); - } + this.complexType(typeName, type); + }); + } - delete(url, callback, auth) { - const app = this.get('app'); - const prefix = this.get('prefix'); - app.delete(`${prefix}${url}`, (req, res, next) => { - if (checkAuth(auth, req)) { - callback(req, res, next); - } else { - res.status(401).end(); - } + return this.entity(name, { + ...entity.getHandler(), + ...handler + }, { + ...entity.getMetadata(), + ...metadata + }, settings, { + ...entity.getMapping(), + ...mapping }); } - - patch(url, callback, auth) { - const app = this.get('app'); - const prefix = this.get('prefix'); - app.patch(`${prefix}${url}`, (req, res, next) => { - if (checkAuth(auth, req)) { - callback(req, res, next); - } else { - res.status(401).end(); - } - }); + + defaultConfiguration(prefix = '') { + this.set('app', this._app); + this.set('prefix', prefix); } action(name, fn, options) { @@ -141,7 +118,7 @@ class Server { return this.actions[name]; } - _getRouter() { + getRouter() { const result = []; result.push(this._serviceDocument._router()); @@ -149,7 +126,7 @@ class Server { Object.keys(this.resources).forEach((resourceKey) => { const resource = this.resources[resourceKey]; - result.push(resource._router ? resource._router(this.getSettings()) : resource.getRouter()); + result.push(resource._router ? resource._router() : resource.getRouter()); if (resource.actions) { Object.keys(resource.actions).forEach((actionKey) => { @@ -174,7 +151,7 @@ class Server { } listen(...args) { - const router = this._getRouter(); + const router = this.getRouter(); router.forEach((item) => { this._app.use(this.get('prefix'), item); @@ -183,54 +160,12 @@ class Server { return this._app.listen(...args); } - getSettings() { - return this._settings; - } - - use(...args) { - if (args[0] instanceof ODataResource) { - const [resource] = args; - this.resources[resource.getName()] = resource; - return; - } - this._app.use(...args); - } - - get(key, callback, auth) { - if (callback === undefined) { - return this._settings[key]; - } - // TODO: Need to refactor, same as L70-L80 - const app = this.get('app'); - const prefix = this.get('prefix'); - return app.get(`${prefix}${key}`, (req, res, next) => { - if (checkAuth(auth, req)) { - callback(req, res, next); - } else { - res.status(401).end(); - } - }); + get(key) { + return this._settings[key]; } set(key, val) { switch (key) { - case 'db': { - let db = val; - - if (typeof val === 'string') { - db = new Db(); - db.createConnection(val, null, (err) => { - if (err) { - console.error(err.message); - console.error('Failed to connect to database on startup.'); - process.exit(); - } - }); - } - - this._settings[key] = db; - break; - } case 'prefix': { let prefix = val; if (prefix === '/') { @@ -250,15 +185,6 @@ class Server { return this; } - // provide a event listener to handle not able to connect DB. - on(name, event) { - if (['connected', 'disconnected'].indexOf(name) > -1) { - const db = this.get('db'); - - db.on(name, event); - } - } - engine(...args) { this._app.engine(...args); } diff --git a/test/mocked/api.Function.js b/test/api.Function.js similarity index 75% rename from test/mocked/api.Function.js rename to test/api.Function.js index 9ad6fbe..7c031bf 100644 --- a/test/mocked/api.Function.js +++ b/test/api.Function.js @@ -1,14 +1,12 @@ import 'should'; import request from 'supertest'; -import { odata, host, port } from '../support/setup'; -import FakeDb from '../support/fake-db'; +import { odata, host, port } from './support/setup'; describe('odata.api.Function', () => { let httpServer; before(() => { - const db = new FakeDb(); - const server = odata(db); + const server = odata(); server.function('/test', (req, res, next) => res.jsonp({ test: 'ok' })); httpServer = server.listen(port); }); diff --git a/test/mocked/hook.action.js b/test/hook.action.js similarity index 94% rename from test/mocked/hook.action.js rename to test/hook.action.js index e81d26f..6f78dd0 100644 --- a/test/mocked/hook.action.js +++ b/test/hook.action.js @@ -1,8 +1,7 @@ import 'should'; import 'should-sinon'; import request from 'supertest'; -import { odata, host, port } from '../support/setup'; -import FakeDb from '../support/fake-db'; +import { odata, host, port } from './support/setup'; import sinon from 'sinon'; function requestToHalfPrice(id) { @@ -14,11 +13,10 @@ function halfPrice(price) { } describe('hook.action', () => { - let httpServer, server, db; + let httpServer, server; beforeEach(async function () { - db = new FakeDb(); - server = odata(db); + server = odata(); }); afterEach(() => { diff --git a/test/hook.entity.after.js b/test/hook.entity.after.js new file mode 100644 index 0000000..2b7a472 --- /dev/null +++ b/test/hook.entity.after.js @@ -0,0 +1,58 @@ +import 'should'; +import 'should-sinon'; +import request from 'supertest'; +import sinon from 'sinon'; +import { odata, host, port, assertSuccess } from './support/setup'; +import { BookMetadata } from './support/books.model'; +import mongoose from 'mongoose'; + +describe('hook.entity.after', function() { + let data, httpServer, server, db; + + beforeEach(async function() { + server = odata(); + server.entity('book', { + get: (req, res, next) => { + res.$odata.result = { + title: "Test hook" + }; + next(); + } + }, BookMetadata); + }); + + afterEach(() => { + httpServer.close(); + mongoose.default.connection.close(); + }); + + it('should work', async function() { + const callback = sinon.spy(); + server.resources.book.addAfter((req, res, next) => { + res.$odata.result.should.be.have.property('title'); + res.$odata.result.title.should.be.equal("Test hook"); + callback(); + next(); + }); + httpServer = server.listen(port); + const res = await request(host).get(`/book('AFFE')`); + assertSuccess(res); + callback.should.be.called(); + }); + + it('should work with multiple hooks', async function() { + const callback = sinon.spy(); + const hook = (req, res, next) => { + callback(); + next(); + }; + + server.resources.book.addAfter(hook); + server.resources.book.addAfter(hook); + httpServer = server.listen(port); + const res = await request(host).get(`/book('AFFE')`); + assertSuccess(res); + callback.should.be.calledTwice(); + }); +}); + diff --git a/test/hook.entity.before.js b/test/hook.entity.before.js new file mode 100644 index 0000000..0dcda80 --- /dev/null +++ b/test/hook.entity.before.js @@ -0,0 +1,56 @@ +import 'should'; +import 'should-sinon'; +import request from 'supertest'; +import sinon from 'sinon'; +import { odata, host, port, assertSuccess } from './support/setup'; +import { BookMetadata } from './support/books.model'; +import mongoose from 'mongoose'; + +describe('hook.entity.before', function() { + let httpServer, server; + + beforeEach(async function() { + server = odata(); + server.entity('book', { + get: (req, res, next) => { + res.$odata.result = { + title: "Test hook" + }; + res.$odata.status = 200; + next(); + } + }, BookMetadata); + }); + + afterEach(() => { + httpServer.close(); + mongoose.default.connection.close(); + }); + + it('should work', async function() { + const callback = sinon.spy(); + server.resources.book.addBefore((req, res, next) => { + callback(); + req.$odata.$Key.should.be.have.property('id'); + req.$odata.$Key.id.should.be.equal('AFFE'); + next(); + }); + httpServer = server.listen(port); + const res = await request(host).get(`/book('AFFE')`); + assertSuccess(res); + callback.should.be.called(); + }); + it('should work with multiple hooks', async function() { + const callback = sinon.spy(); + const hook = (req, res, next) => { + callback(); + next(); + }; + server.resources.book.addBefore(hook); + server.resources.book.addBefore(hook); + httpServer = server.listen(port); + await request(host).get(`/book('AFFE')`); + callback.should.be.calledTwice(); + }); +}); + diff --git a/test/mocked/metadata.action.js b/test/metadata.action.js similarity index 84% rename from test/mocked/metadata.action.js rename to test/metadata.action.js index df19c60..e952a9b 100644 --- a/test/mocked/metadata.action.js +++ b/test/metadata.action.js @@ -3,22 +3,21 @@ import 'should'; import request from 'supertest'; -import { host, conn, port, odata, assertSuccess } from '../support/setup'; -import FakeDb from '../support/fake-db'; +import { host, conn, port, odata, assertSuccess } from './support/setup'; +import FakeDb from './support/fake-db'; describe('metadata.action', () => { let httpServer, server, db; beforeEach(async function() { - db = new FakeDb(); - server = odata(db); + server = odata(); }); afterEach(() => { httpServer.close(); }); -/* + it('should return json metadata for action that bound to instance', async function() { const jsonDocument = { $Version: '4.0', @@ -55,8 +54,15 @@ describe('metadata.action', () => { } }, }; - server.resource('book', { - author: String + server.entity('book', null, { + $Key: ['id'], + id: { + $Type: 'node.odata.ObjectId', + $Nullable: false + }, + author: { + $Type: 'Edm.String' + } }).action('bound-action', (req, res, next) => {}, { binding: 'entity' }); @@ -88,9 +94,16 @@ describe('metadata.action', () => { - `.replace(/\s*\s*//*g, '>'); - server.resource('book', { - author: String + `.replace(/\s*\s*/g, '>'); + server.entity('book', null, { + $Key: ['id'], + id: { + $Type: 'node.odata.ObjectId', + $Nullable: false + }, + author: { + $Type: 'Edm.String' + } }).action('bound-action', (req, res, next) => {}, { binding: 'entity' }); @@ -137,8 +150,15 @@ describe('metadata.action', () => { } }, }; - server.resource('book', { - author: String + server.entity('book', null, { + $Key: ['id'], + id: { + $Type: 'node.odata.ObjectId', + $Nullable: false + }, + author: { + $Type: 'Edm.String' + } }).action('bound-action', (req, res, next) => {}, { binding: 'collection' }); @@ -170,9 +190,16 @@ describe('metadata.action', () => { - `.replace(/\s*\s*//*g, '>'); - server.resource('book', { - author: String + `.replace(/\s*\s*/g, '>'); + server.entity('book', null, { + $Key: ['id'], + id: { + $Type: 'node.odata.ObjectId', + $Nullable: false + }, + author: { + $Type: 'Edm.String' + } }).action('bound-action', (req, res, next) => {}, { binding: 'collection' }); @@ -184,9 +211,7 @@ describe('metadata.action', () => { it('should not accept action names with special characters', function() { try { - const action = server.resource('book', { - author: String - }).action('/login', (req, res, next) => {}); + const action = server.action('/login', (req, res, next) => {}); action.getRouter(); @@ -196,7 +221,6 @@ describe('metadata.action', () => { error.message.should.equal(`Invalid simple identifier '/login'`); } }); -*/ it('should return json metadata for unbound action', async function() { const jsonDocument = { @@ -213,32 +237,14 @@ describe('metadata.action', () => { $Type: 'node.odata.book' }] }, - book: { - $Kind: "EntityType", - $Key: ["id"], - id: { - $Type: "node.odata.ObjectId", - $Nullable: false, - }, - author: { - $Type: 'Edm.String' - } - }, $EntityContainer: 'node.odata', ['node.odata']: { $Kind: 'EntityContainer', - book: { - $Collection: true, - $Type: `node.odata.book`, - }, 'unbound-action-import': { $Action: 'node.odata.unbound-action' } } }; - server.resource('book', { - author: String - }); server.action('unbound-action', (req, res, next) => {}, { $Parameter: [{ @@ -259,25 +265,14 @@ describe('metadata.action', () => { - - - - - - - - `.replace(/\s*\s*/g, '>'); - server.resource('book', { - author: String - }); server.action('unbound-action', (req, res, next) => {}); httpServer = server.listen(port); diff --git a/test/mocked/metadata.complex.type.js b/test/metadata.complex.type.js similarity index 93% rename from test/mocked/metadata.complex.type.js rename to test/metadata.complex.type.js index 1420a3e..f67d57c 100644 --- a/test/mocked/metadata.complex.type.js +++ b/test/metadata.complex.type.js @@ -3,15 +3,13 @@ import 'should'; import request from 'supertest'; -import { host, conn, port, odata, assertSuccess } from '../support/setup'; -import FakeDb from '../support/fake-db'; +import { host, port, odata, assertSuccess } from './support/setup'; describe('metadata.complex.type', () => { let httpServer, server, db; beforeEach(async function() { - db = new FakeDb(); - server = odata(db); + server = odata(); }); diff --git a/test/mocked/metadata.format.js b/test/metadata.format.js similarity index 78% rename from test/mocked/metadata.format.js rename to test/metadata.format.js index f54e324..753a383 100644 --- a/test/mocked/metadata.format.js +++ b/test/metadata.format.js @@ -3,12 +3,36 @@ import 'should'; import request from 'supertest'; -import { host, conn, port, bookSchema, odata, assertSuccess } from '../support/setup'; -import FakeDb from '../support/fake-db'; +import { host, port, odata, assertSuccess } from './support/setup'; describe('metadata.format', () => { - let httpServer, server, db; + let httpServer, server; + const metadata = { + $Key: ["id"], + id: { + $Type: "node.odata.ObjectId", + $Nullable: false, + }, + author: { + $Type: 'Edm.String' + }, + description: { + $Type: 'Edm.String' + }, + genre: { + $Type: 'Edm.String' + }, + price: { + $Type: 'Edm.Double' + }, + publish_date: { + $Type: 'Edm.DateTimeOffset' + }, + title: { + $Type: 'Edm.String' + } + }; const jsonDocument = { $Version: '4.0', ObjectId: { @@ -18,29 +42,7 @@ describe('metadata.format', () => { }, book: { $Kind: "EntityType", - $Key: ["id"], - id: { - $Type: "node.odata.ObjectId", - $Nullable: false, - }, - author: { - $Type: 'Edm.String' - }, - description: { - $Type: 'Edm.String' - }, - genre: { - $Type: 'Edm.String' - }, - price: { - $Type: 'Edm.Double' - }, - publish_date: { - $Type: 'Edm.DateTimeOffset' - }, - title: { - $Type: 'Edm.String' - } + ...metadata }, $EntityContainer: 'node.odata', ['node.odata']: { @@ -51,8 +53,8 @@ describe('metadata.format', () => { } }, }; - const xmlDocument = - ` + const xmlDocument = + ` @@ -76,10 +78,9 @@ describe('metadata.format', () => { `.replace(/\s*\s*/g, '>'); - beforeEach(async function() { - db = new FakeDb(); - server = odata(db); - server.resource('book', bookSchema); + beforeEach(async function () { + server = odata(); + server.entity('book', null, metadata); }); @@ -87,7 +88,7 @@ describe('metadata.format', () => { httpServer.close(); }); - it('should return json according accept header', async function() { + it('should return json according accept header', async function () { httpServer = server.listen(port); const res = await request(host).get('/$metadata').set('accept', 'application/json'); assertSuccess(res); @@ -95,7 +96,7 @@ describe('metadata.format', () => { res.body.should.deepEqual(jsonDocument); }); - it('should return json if $format overrides accept header', async function() { + it('should return json if $format overrides accept header', async function () { httpServer = server.listen(port); const res = await request(host).get('/$metadata?$format=json').set('accept', 'application/xml'); res.statusCode.should.equal(200); @@ -103,7 +104,7 @@ describe('metadata.format', () => { res.body.should.deepEqual(jsonDocument); }); - it('should return xml if xml has highest quality value', async function() { + it('should return xml if xml has highest quality value', async function () { httpServer = server.listen(port); const res = await request(host).get('/$metadata').set('accept', 'application/json;q=0.9, application/xml'); res.statusCode.should.equal(200); @@ -111,7 +112,7 @@ describe('metadata.format', () => { res.text.should.equal(xmlDocument); }); - it('should return xml if xml and json matched with asterix', async function() { + it('should return xml if xml and json matched with asterix', async function () { httpServer = server.listen(port); const res = await request(host).get('/$metadata').set('accept', '*/*'); res.statusCode.should.equal(200); diff --git a/test/mocked/metadata.function.js b/test/metadata.function.js similarity index 92% rename from test/mocked/metadata.function.js rename to test/metadata.function.js index 0680917..e3878cb 100644 --- a/test/mocked/metadata.function.js +++ b/test/metadata.function.js @@ -3,15 +3,13 @@ import 'should'; import request from 'supertest'; -import { host, conn, port, odata, assertSuccess } from '../support/setup'; -import FakeDb from '../support/fake-db'; +import { host, port, odata, assertSuccess } from './support/setup'; describe('metadata.function', () => { - let httpServer, server, db; + let httpServer, server; beforeEach(async function() { - db = new FakeDb(); - server = odata(db); + server = odata(); }); diff --git a/test/metadata.js b/test/metadata.js new file mode 100644 index 0000000..4d1415b --- /dev/null +++ b/test/metadata.js @@ -0,0 +1,65 @@ +// For issue: https://github.com/TossShinHwa/node-odata/issues/96 +// For issue: https://github.com/TossShinHwa/node-odata/issues/25 + +import 'should'; +import request from 'supertest'; +import { host, port, odata, assertSuccess } from './support/setup'; + +describe('metadata', () => { + let httpServer, server; + + beforeEach(async function() { + server = odata(); + + }); + + afterEach(() => { + httpServer.close(); + }); + + + it('should return json metadata for custom resource', async function() { + const jsonDocument = { + $Version: '4.0', + ObjectId: { + $Kind: "TypeDefinition", + $UnderlyingType: "Edm.String", + $MaxLength: 24 + }, + book: { + $Kind: "EntityType", + $Key: ["id"], + id: { + $Type: "node.odata.ObjectId", + $Nullable: false, + }, + salted: { + $Type: 'Edm.Boolean' + } + }, + $EntityContainer: 'node.odata', + ['node.odata']: { + $Kind: 'EntityContainer', + book: { + $Collection: true, + $Type: `node.odata.book`, + } + }, + }; + server.entity('book', {}, { + $Key: ["id"], + id: { + $Type: "node.odata.ObjectId", + $Nullable: false, + }, + salted: { + $Type: 'Edm.Boolean' + } + }); + httpServer = server.listen(port); + const res = await request(host).get('/$metadata?$format=json'); + assertSuccess(res); + res.body.should.deepEqual(jsonDocument); + }); + +}); diff --git a/test/mocked/mimetype.defaults.js b/test/mimetype.defaults.js similarity index 71% rename from test/mocked/mimetype.defaults.js rename to test/mimetype.defaults.js index 09b5991..bee9ced 100644 --- a/test/mocked/mimetype.defaults.js +++ b/test/mimetype.defaults.js @@ -1,15 +1,21 @@ import 'should'; import request from 'supertest'; -import { host, port, bookSchema, odata, assertSuccess } from '../support/setup'; -import FakeDb from '../support/fake-db'; +import { host, port, odata, assertSuccess } from './support/setup'; +import { BookMetadata } from './support/books.model'; describe('mimetype.defaults', () => { - let httpServer, server, db; + let httpServer, server; beforeEach(async function() { - db = new FakeDb(); - server = odata(db); - server.resource('book', bookSchema); + server = odata(); + server.entity('book', { + list: (req, res, next) => { + res.$odata.result = { + value: [] + }; + next(); + } + }, BookMetadata); }); diff --git a/test/mocked/api.Resource.js b/test/mocked/api.Resource.js deleted file mode 100644 index f36d80d..0000000 --- a/test/mocked/api.Resource.js +++ /dev/null @@ -1,24 +0,0 @@ -import 'should'; -import request from 'supertest'; -import { odata, host, port, bookSchema } from '../support/setup'; -import FakeDb from '../support/fake-db'; - -describe('odata.api.Resouce', () => { - let httpServer; - - before(() => { - const db = new FakeDb(); - const server = odata(db); - server.resource('book', bookSchema); - httpServer = server.listen(port); - }); - - after(() => { - httpServer.close(); - }); - - it('should work', async function() { - const res = await request(host).get('/book'); - res.body.should.be.have.property('value'); - }); -}); diff --git a/test/mocked/hook.all.after.js b/test/mocked/hook.all.after.js deleted file mode 100644 index db56828..0000000 --- a/test/mocked/hook.all.after.js +++ /dev/null @@ -1,44 +0,0 @@ -import 'should'; -import 'should-sinon'; -import request from 'supertest'; -import sinon from 'sinon'; -import { odata, host, port, bookSchema, assertSuccess } from '../support/setup'; -import FakeDb from '../support/fake-db'; -import books from '../support/books.json'; - -describe('hook.all.after', function() { - let data, httpServer, server, db; - - beforeEach(async function() { - db = new FakeDb(); - server = odata(db); - server.resource('book', bookSchema); - data = db.addData('book', books); - }); - - afterEach(() => { - httpServer.close(); - }); - - it('should work', async function() { - const callback = sinon.spy(); - server.resources.book.all().after((entity) => { - entity.should.be.have.property('title'); - callback(); - }); - httpServer = server.listen(port); - const res = await request(host).get(`/book(${data[0].id})`); - assertSuccess(res); - callback.should.be.called(); - }); - - it('should work with multiple hooks', async function() { - const callback = sinon.spy(); - server.resources.book.all().after(callback).after(callback); - httpServer = server.listen(port); - const res = await request(host).get(`/book(${data[0].id})`); - assertSuccess(res); - callback.should.be.calledTwice(); - }); -}); - diff --git a/test/mocked/hook.all.before.js b/test/mocked/hook.all.before.js deleted file mode 100644 index 922169a..0000000 --- a/test/mocked/hook.all.before.js +++ /dev/null @@ -1,43 +0,0 @@ -import 'should'; -import 'should-sinon'; -import request from 'supertest'; -import sinon from 'sinon'; -import { odata, host, port, bookSchema } from '../support/setup'; -import FakeDb from '../support/fake-db'; -import books from '../support/books.json'; - -describe('hook.all.before', function() { - let data, httpServer, server, db; - - beforeEach(async function() { - db = new FakeDb(); - server = odata(db); - server.resource('book', bookSchema); - - data = db.addData('book', books); - }); - - afterEach(() => { - httpServer.close(); - }); - - it('should work', async function() { - const callback = sinon.spy(); - server.resources.book.all().before((entity, req) => { - req.params.should.be.have.property('id'); - req.params.id.should.be.equal(data[0].id); - callback(); - }); - httpServer = server.listen(port); - await request(host).get(`/book(${data[0].id})`); - callback.should.be.called(); - }); - it('should work with multiple hooks', async function() { - const callback = sinon.spy(); - server.resources.book.all().before(callback).before(callback); - httpServer = server.listen(port); - await request(host).get(`/book(${data[0].id})`); - callback.should.be.calledTwice(); - }); -}); - diff --git a/test/mocked/hook.delete.after.js b/test/mocked/hook.delete.after.js deleted file mode 100755 index 2ccbbfb..0000000 --- a/test/mocked/hook.delete.after.js +++ /dev/null @@ -1,42 +0,0 @@ -import 'should'; -import 'should-sinon'; -import request from 'supertest'; -import sinon from 'sinon'; -import { odata, host, port, bookSchema, assertSuccess } from '../support/setup'; -import FakeDb from '../support/fake-db'; -import books from '../support/books.json'; - -describe('hook.delete.after', function() { - let data, httpServer, server, db; - - beforeEach(async function() { - db = new FakeDb(); - server = odata(db); - server.resource('book', bookSchema); - - data = db.addData('book', books); - }); - - afterEach(() => { - httpServer.close(); - }); - - it('should work', async function() { - const callback = sinon.spy(); - server.resources.book.delete().after((entity) => { - callback(); - }); - httpServer = server.listen(port); - const res = await request(host).delete(`/book(${data[0].id})`); - assertSuccess(res); - callback.should.be.called(); - }); - it('should work with multiple hooks', async function() { - const callback = sinon.spy(); - server.resources.book.delete().after(callback).after(callback); - httpServer = server.listen(port); - const res = await request(host).delete(`/book(${data[0].id})`); - assertSuccess(res); - callback.should.be.calledTwice(); - }); -}); diff --git a/test/mocked/hook.delete.before.js b/test/mocked/hook.delete.before.js deleted file mode 100644 index f3dfcbe..0000000 --- a/test/mocked/hook.delete.before.js +++ /dev/null @@ -1,42 +0,0 @@ -import 'should'; -import 'should-sinon'; -import request from 'supertest'; -import sinon from 'sinon'; -import { odata, host, port, bookSchema } from '../support/setup'; -import FakeDb from '../support/fake-db'; -import books from '../support/books.json'; - -describe('hook.delete.before', function() { - let data, httpServer, server, db; - - beforeEach(async function() { - db = new FakeDb(); - server = odata(db); - server.resource('book', bookSchema); - - data = db.addData('book', books); - }); - - afterEach(() => { - httpServer.close(); - }); - - it('should work', async function() { - const callback = sinon.spy(); - server.resources.book.delete().before((entity, req) => { - req.params.should.be.have.property('id'); - req.params.id.should.be.equal(data[0].id); - callback(); - }); - httpServer = server.listen(port); - await request(host).delete(`/book(${data[0].id})`); - callback.should.be.called(); - }); - it('should work with multiple hooks', async function() { - const callback = sinon.spy(); - server.resources.book.delete().before(callback).before(callback); - httpServer = server.listen(port); - await request(host).delete(`/book(${data[0].id})`); - callback.should.be.calledTwice(); - }); -}); diff --git a/test/mocked/hook.get.after.js b/test/mocked/hook.get.after.js deleted file mode 100755 index d0654f2..0000000 --- a/test/mocked/hook.get.after.js +++ /dev/null @@ -1,42 +0,0 @@ -import 'should'; -import 'should-sinon'; -import request from 'supertest'; -import sinon from 'sinon'; -import { host, port, bookSchema, odata } from '../support/setup'; -import FakeDb from '../support/fake-db'; -import books from '../support/books.json'; - -describe('hook.get.after', function() { - let data, httpServer, server, db; - - beforeEach(async function() { - db = new FakeDb(); - server = odata(db); - server.resource('book', bookSchema); - - data = db.addData('book', books); - }); - - afterEach(() => { - httpServer.close(); - }); - - it('should work', async function() { - const callback = sinon.spy(); - - server.resources.book.get().after((entity) => { - entity.should.be.have.property('title'); - callback(); - }); - httpServer = server.listen(port); - await request(host).get(`/book(${data[0].id})`); - callback.should.be.called(); - }); - it('should work with multiple hooks', async function() { - const callback = sinon.spy(); - server.resources.book.get().after(callback).after(callback); - httpServer = server.listen(port); - await request(host).get(`/book(${data[0].id})`); - callback.should.be.calledTwice(); - }); -}); diff --git a/test/mocked/hook.get.before.js b/test/mocked/hook.get.before.js deleted file mode 100644 index a5e3fca..0000000 --- a/test/mocked/hook.get.before.js +++ /dev/null @@ -1,44 +0,0 @@ -import 'should'; -import 'should-sinon'; -import request from 'supertest'; -import sinon from 'sinon'; -import { host, port, bookSchema, odata } from '../support/setup'; -import FakeDb from '../support/fake-db'; -import books from '../support/books.json'; - -describe('hook.get.before', function() { - let data, httpServer, server, db; - - beforeEach(async function() { - db = new FakeDb(); - server = odata(db); - server.resource('book', bookSchema); - - data = db.addData('book', books); - }); - - afterEach(() => { - httpServer.close(); - }); - - it('should work', async function() { - const callback = sinon.spy(); - - server.resources.book.get().before((entity, req) => { - req.params.should.be.have.property('id'); - req.params.id.should.be.equal(data[0].id); - callback(); - }); - httpServer = server.listen(port); - await request(host).get(`/book(${data[0].id})`); - callback.should.be.called(); - }); - it('should work with multiple hooks', async function() { - const callback = sinon.spy(); - - server.resources.book.get().before(callback).before(callback); - httpServer = server.listen(port); - await request(host).get(`/book(${data[0].id})`); - callback.should.be.calledTwice(); - }); -}); diff --git a/test/mocked/hook.list.after.js b/test/mocked/hook.list.after.js deleted file mode 100644 index 7b30564..0000000 --- a/test/mocked/hook.list.after.js +++ /dev/null @@ -1,44 +0,0 @@ -import 'should'; -import 'should-sinon'; -import request from 'supertest'; -import sinon from 'sinon'; -import { host, port, bookSchema, odata } from '../support/setup'; -import FakeDb from '../support/fake-db'; -import books from '../support/books.json'; - -describe('hook.list.after', function() { - let data, httpServer, server, db; - - beforeEach(async function() { - db = new FakeDb(); - server = odata(db); - server.resource('book', bookSchema); - - data = db.addData('book', books); - }); - - afterEach(() => { - httpServer.close(); - }); - - it('should work', async function() { - const callback = sinon.spy(); - - server.resources.book.list().after((result) => { - result.should.be.have.property('value'); - callback(); - }); - httpServer = server.listen(port); - await request(host).get(`/book`); - callback.should.be.called(); - }); - it('should work with multiple hooks', async function() { - const callback = sinon.spy(); - - server.resources.book.list().after(callback).after(callback); - httpServer = server.listen(port); - await request(host).get(`/book`); - callback.should.be.calledTwice(); - }); -}); - diff --git a/test/mocked/hook.list.before.js b/test/mocked/hook.list.before.js deleted file mode 100644 index f3428d7..0000000 --- a/test/mocked/hook.list.before.js +++ /dev/null @@ -1,43 +0,0 @@ -import 'should'; -import 'should-sinon'; -import request from 'supertest'; -import sinon from 'sinon'; -import { host, port, bookSchema, odata } from '../support/setup'; -import FakeDb from '../support/fake-db'; -import books from '../support/books.json'; - -describe('hook.list.before', function() { - let data, httpServer, server, db; - - beforeEach(async function() { - db = new FakeDb(); - server = odata(db); - server.resource('book', bookSchema); - - data = db.addData('book', books); - }); - - afterEach(() => { - httpServer.close(); - }); - - it('should work', async function() { - const callback = sinon.spy(); - - server.resources.book.list().before((entity, req) => { - callback(); - }); - httpServer = server.listen(port); - await request(host).get(`/book`); - callback.should.be.called(); - }); - it('should work with multiple hooks', async function() { - const callback = sinon.spy(); - - server.resources.book.list().before(callback).before(callback); - httpServer = server.listen(port); - await request(host).get(`/book`); - callback.should.be.calledTwice(); - }); -}); - diff --git a/test/mocked/hook.post.after.js b/test/mocked/hook.post.after.js deleted file mode 100755 index 264d0ee..0000000 --- a/test/mocked/hook.post.after.js +++ /dev/null @@ -1,48 +0,0 @@ -import 'should'; -import 'should-sinon'; -import request from 'supertest'; -import sinon from 'sinon'; -import { odata, host, port, bookSchema } from '../support/setup'; -import FakeDb from '../support/fake-db'; -import books from '../support/books.json'; - -describe('hook.post.after', function() { - let data, httpServer, server, db; - - beforeEach(async function() { - db = new FakeDb(); - server = odata(db); - server.resource('book', bookSchema); - - data = db.addData('book', books); - }); - - afterEach(() => { - httpServer.close(); - }); - - it('should work', async function() { - const callback = sinon.spy(); - const TITLE = 'HOOK_POST_AFTER'; - - server.resources.book.post().after((entity) => { - entity.should.be.have.property('title'); - entity.title.should.be.equal(TITLE); - callback(); - }); - httpServer = server.listen(port); - await request(host).post(`/book`).send({ title: TITLE }); - callback.should.be.called(); - }); - it('should work with multiple hooks', async function() { - const callback = sinon.spy(); - const TITLE = 'HOOK_POST_AFTER'; - - server.resources.book.post().after(callback).after(callback); - httpServer = server.listen(port); - await request(host).post(`/book`).send({ title: TITLE }); - callback.should.be.calledTwice(); - }); -}); - - diff --git a/test/mocked/hook.post.before.js b/test/mocked/hook.post.before.js deleted file mode 100644 index 8d45eb6..0000000 --- a/test/mocked/hook.post.before.js +++ /dev/null @@ -1,48 +0,0 @@ -import 'should'; -import 'should-sinon'; -import request from 'supertest'; -import sinon from 'sinon'; -import { host, port, bookSchema, odata } from '../support/setup'; -import FakeDb from '../support/fake-db'; -import books from '../support/books.json'; - -describe('hook.post.before', function() { - let data, httpServer, server, db; - - beforeEach(async function() { - db = new FakeDb(); - server = odata(db); - server.resource('book', bookSchema); - - data = db.addData('book', books); - }); - - afterEach(() => { - httpServer.close(); - }); - - it('should work', async function() { - const callback = sinon.spy(); - const TITLE = 'HOOK_POST_BEFORE'; - - server.resources.book.post().before((entity, req) => { - req.body.should.be.have.property('title'); - req.body.title.should.be.equal(TITLE); - callback(); - }); - httpServer = server.listen(port); - await request(host).post(`/book`).send({ title: TITLE }); - callback.should.be.called(); - }); - it('should work with multiple hooks', async function() { - const callback = sinon.spy(); - const TITLE = 'HOOK_POST_BEFORE'; - - server.resources.book.post().before(callback).before(callback); - httpServer = server.listen(port); - await request(host).post(`/book`).send({ title: TITLE }); - callback.should.be.calledTwice(); - }); -}); - - diff --git a/test/mocked/hook.put.after.js b/test/mocked/hook.put.after.js deleted file mode 100755 index ae845bd..0000000 --- a/test/mocked/hook.put.after.js +++ /dev/null @@ -1,43 +0,0 @@ -import 'should'; -import 'should-sinon'; -import request from 'supertest'; -import sinon from 'sinon'; -import { host, port, bookSchema, odata } from '../support/setup'; -import FakeDb from '../support/fake-db'; -import books from '../support/books.json'; - -describe('hook.put.after', function() { - let data, httpServer, server, db; - - beforeEach(async function() { - db = new FakeDb(); - server = odata(db); - server.resource('book', bookSchema); - - data = db.addData('book', books); - }); - - afterEach(() => { - httpServer.close(); - }); - - it('should work', async function() { - const callback = sinon.spy(); - - server.resources.book.put().after((entity) => { - entity.should.be.have.property('title'); - callback(); - }); - httpServer = server.listen(port); - await request(host).put(`/book(${data[0].id})`).send(data[0]); - callback.should.be.called(); - }); - it('should work with multiple hooks', async function() { - const callback = sinon.spy(); - - server.resources.book.put().after(callback).after(callback); - httpServer = server.listen(port); - await request(host).put(`/book(${data[0].id})`).send(data[0]); - callback.should.be.calledTwice(); - }); -}); diff --git a/test/mocked/hook.put.before.js b/test/mocked/hook.put.before.js deleted file mode 100644 index 42e419d..0000000 --- a/test/mocked/hook.put.before.js +++ /dev/null @@ -1,44 +0,0 @@ -import 'should'; -import 'should-sinon'; -import request from 'supertest'; -import sinon from 'sinon'; -import { host, port, bookSchema, odata } from '../support/setup'; -import FakeDb from '../support/fake-db'; -import books from '../support/books.json'; - -describe('hook.put.before', function() { - let data, httpServer, server, db; - - beforeEach(async function() { - db = new FakeDb(); - server = odata(db); - server.resource('book', bookSchema); - - data = db.addData('book', books); - }); - - afterEach(() => { - httpServer.close(); - }); - - it('should work', async function() { - const callback = sinon.spy(); - - server.resources.book.put().before((entity, req) => { - req.params.should.be.have.property('id'); - req.params.id.should.be.equal(data[0].id); - callback(); - }); - httpServer = server.listen(port); - await request(host).put(`/book(${data[0].id})`).send(data[0]); - callback.should.be.called(); - }); - it('should work with multiple hooks', async function() { - const callback = sinon.spy(); - - server.resources.book.put().before(callback).before(callback); - httpServer = server.listen(port); - await request(host).put(`/book(${data[0].id})`).send(data[0]); - callback.should.be.calledTwice(); - }); -}); diff --git a/test/mocked/model.complex.action.js b/test/mocked/model.complex.action.js deleted file mode 100644 index 6fec2f4..0000000 --- a/test/mocked/model.complex.action.js +++ /dev/null @@ -1,56 +0,0 @@ -// For issue: https://github.com/TossShinHwa/node-odata/issues/69 - -import 'should'; -import request from 'supertest'; -import { odata, host, port, assertSuccess } from '../support/setup'; -import FakeDb from '../support/fake-db'; - -describe('model.complex.action', () => { - let httpServer; - - before(() => { - const db = new FakeDb(); - const server = odata(db); - const resource = server.resource('order', { product: [{ price: Number }] }); - - resource.action('all-item-greater', (req, res, next) => { - const { price } = req.query; - const $elemMatch = { price: { $gt: price } }; - req.$odata.mongo.order.exec((err, data) => { - res.$odata.result = data.slice(1); - res.$odata.status = 200; - next(); - }); - }, { binding : 'collection' }); - httpServer = server.listen(port); - }); - - after(() => { - httpServer.close(); - }); - - it('should work when PUT a complex entity', async function () { - const entities = [{ - product: [ - { price: 1 }, - { price: 2 }, - { price: 4 }, - ], - }, { - product: [ - { price: 32 }, - { price: 64 }, - { price: 99 }, - ], - }]; - entities.forEach(async entity => await request(host).post('/order').send(entity)); - - const res = await request(host).post(`/order/all-item-greater`); - assertSuccess(res); - res.body.should.matchEach((item) => { - return item.product[0].price > 30 - && item.product[1].price > 30 - && item.product[2].price > 30; - }); - }); -}); diff --git a/test/mocked/model.complex.filter.js b/test/mocked/model.complex.filter.js deleted file mode 100644 index 2a1d634..0000000 --- a/test/mocked/model.complex.filter.js +++ /dev/null @@ -1,34 +0,0 @@ -// For issue: https://github.com/TossShinHwa/node-odata/issues/69 - -import 'should'; -import 'should-sinon'; -import request from 'supertest'; -import { odata, assertSuccess, host, port } from '../support/setup'; -import Db from '../support/fake-db'; -import sinon from 'sinon'; - -describe('model.complex.filter', () => { - let httpServer, db, mock; - - before(() => { - db = new Db(); - const server = odata(db); - const resource = server.resource('complex-model-filter', { product: { price: Number } }); - - mock = sinon.mock(resource.model); - mock.expects('where').once().withArgs('product.price').returns(resource.model); - mock.expects('gt').once().withArgs(30).returns(resource.model); - httpServer = server.listen(port); - }); - - after(() => { - httpServer.close(); - }); - - it('should work when PUT a complex entity', async function() { - const res = await request(host).get(`/complex-model-filter?$filter=product-price gt 30`); - - assertSuccess(res); - mock.verify(); - }); -}); diff --git a/test/mocked/model.complex.js b/test/mocked/model.complex.js deleted file mode 100644 index 6731bcc..0000000 --- a/test/mocked/model.complex.js +++ /dev/null @@ -1,45 +0,0 @@ -// For issue: https://github.com/TossShinHwa/node-odata/issues/55 - -import 'should'; -import request from 'supertest'; -import { odata, host, port } from '../support/setup'; -import FakeDb from '../support/fake-db'; - -function addResource() { - return request(host) - .post('/complex-model') - .send({ p1: [{ p2: 'origin' }] }); -} - -function updateResouce(id) { - return request(host) - .put(`/complex-model(${id})`) - .send({ p1: [{ p2: 'new' }] }); -} - -function queryResource(id) { - return request(host) - .get(`/complex-model(${id})`); -} - -describe('model.complex', () => { - let httpServer; - - before(() => { - const db = new FakeDb(); - const server = odata(db); - server.resource('complex-model', { p1: [{ p2: String }] }); - httpServer = server.listen(port); - }); - - after(() => { - httpServer.close(); - }); - - it('should work when PUT a complex entity', async function() { - const entity = await addResource(); - await updateResouce(entity.body.id); - const res = await queryResource(entity.body.id); - res.body.p1[0].p2.should.be.equal('new'); - }); -}); diff --git a/test/mocked/model.custom.id.js b/test/mocked/model.custom.id.js deleted file mode 100644 index 984428c..0000000 --- a/test/mocked/model.custom.id.js +++ /dev/null @@ -1,30 +0,0 @@ -import 'should'; -import request from 'supertest'; -import fakeDb from '../support/fake-db'; -import { odata, host, port } from '../support/setup'; - -describe('model.custom.id', () => { - let httpServer; - - before(async function() { - const db = new fakeDb(); - const server = odata(db); - server.resource('custom-id', { id: Number }); - httpServer = server.listen(port); - await request(host).post('/custom-id').send({ id: 100 }); - }); - - after(() => { - httpServer.close(); - }); - - it('should work when use a custom id to query specific entity', async function() { - const res = await request(host).get('/custom-id(100)'); - res.body.id.should.be.equal(100); - }); - - it('should work when use a custom id to query a list', async function() { - const res = await request(host).get('/custom-id?$filter=id eq \'100\''); - res.body.value.length.should.be.greaterThan(0); - }); -}); diff --git a/test/mocked/model.hidden.field.js b/test/mocked/model.hidden.field.js deleted file mode 100644 index 065b96e..0000000 --- a/test/mocked/model.hidden.field.js +++ /dev/null @@ -1,52 +0,0 @@ -import 'should'; -import sinon from 'sinon'; -import request from 'supertest'; -import FakeDb from '../support/fake-db'; -import { odata, host, port } from '../support/setup'; - -describe('model.hidden.field', function () { - let httpServer, id, resource, mock; - - before(async function () { - const db = new FakeDb(); - const server = odata(db); - resource = server.resource('hidden-field', { - name: String, - password: { - type: String, - select: false - } - }); - httpServer = server.listen(port); - const data = db.addData('hidden-field', [{ - name: 'zack', - password: '123' - }]); - id = data[0].id; - }); - - after(() => { - httpServer.close(); - }); - - afterEach(() => { - mock.restore(); - }); - - it('should work when get entities list even it is selected', async function () { - mock = sinon.mock(resource.model); - mock.expects('select').once().withArgs({ - _id: 0, - name: 1 - }).returns(resource.model); - await request(host).get('/hidden-field?$select=name, password'); - mock.verify(); - }); - - it('should work when get entities list even only it is selected', async function () { - mock = sinon.mock(resource.model); - mock.expects('select').never().returns(resource.model); - await request(host).get('/hidden-field?$select=password'); - mock.verify(); - }); -}); diff --git a/test/mocked/model.special.name.js b/test/mocked/model.special.name.js deleted file mode 100644 index cf96c8a..0000000 --- a/test/mocked/model.special.name.js +++ /dev/null @@ -1,26 +0,0 @@ -import 'should'; -import request from 'supertest'; -import { odata, host, port } from '../support/setup'; -import FakeDb from '../support/fake-db'; - -describe('model.special.name', () => { - let httpServer; - - before(() => { - const db = new FakeDb(); - const server = odata(db); - server.resource('funcion-keyword', { year: Number }); - httpServer = server.listen(port); - }); - - after(() => { - httpServer.close(); - }); - - it('should work when use odata function keyword', async function() { - const res = await request(host) - .post('/funcion-keyword') - .send({ year: 2015 }); - res.status.should.be.equal(201); - }); -}); diff --git a/test/mocked/odata.count.js b/test/mocked/odata.count.js deleted file mode 100644 index d478c7e..0000000 --- a/test/mocked/odata.count.js +++ /dev/null @@ -1,32 +0,0 @@ -import 'should'; -import request from 'supertest'; -import { odata, host, port, bookSchema } from '../support/setup'; -import books from '../support/books.json'; -import FakeDb from '../support/fake-db'; - -describe('odata.count', function() { - let httpServer; - - before(async function() { - const db = new FakeDb(); - const server = odata(db); - server.resource('book', bookSchema); - db.addData('book', books); - httpServer = server.listen(port); - }); - - after(() => { - httpServer.close(); - }); - - it('should get count for entity set', async function() { - const res = await request(host).get('/book/$count'); - - if (res.error) { - res.error.status.should.be.equal(200); - } - res.text.should.be.equal('13'); - res.header.should.have.property('content-type'); - res.header['content-type'].should.containEql('text/plain'); - }); -}); diff --git a/test/mocked/odata.query.count.js b/test/mocked/odata.query.count.js deleted file mode 100644 index ef0dd3d..0000000 --- a/test/mocked/odata.query.count.js +++ /dev/null @@ -1,37 +0,0 @@ -import 'should'; -import request from 'supertest'; -import { odata, host, port, bookSchema } from '../support/setup'; -import books from '../support/books.json'; -import FakeDb from '../support/fake-db'; - -describe('odata.query.count', function() { - let httpServer; - - before(async function() { - const db = new FakeDb(); - const server = odata(db); - server.resource('book', bookSchema); - db.addData('book', books); - httpServer = server.listen(port); - }); - - after(() => { - httpServer.close(); - }); - - it('should get count', async function() { - const res = await request(host).get('/book?$count=true'); - res.body.should.be.have.property('@odata.count'); - res.body.should.be.have.property('value'); - res.body['@odata.count'].should.be.equal(res.body.value.length); - }); - it('should not get count', async function() { - const res = await request(host).get('/book?$count=false'); - res.body.should.be.not.have.property('@odata.count'); - }); - it('should 500 when $count isn\'t \'true\' or \'false\'', async function() { - const res = await request(host).get('/book?$count=1'); - res.error.status.should.be.equal(500); - }); - -}); diff --git a/test/mocked/odata.query.filter.functions.js b/test/mocked/odata.query.filter.functions.js deleted file mode 100644 index f69bdab..0000000 --- a/test/mocked/odata.query.filter.functions.js +++ /dev/null @@ -1,63 +0,0 @@ -import 'should'; -import sinon from 'sinon'; -import request from 'supertest'; -import { odata, host, port, bookSchema } from '../support/setup'; -import data from '../support/books.json'; -import FakeDb from '../support/fake-db'; - -describe('odata.query.filter.functions', function () { - let httpServer, mock, resource; - - before(async function () { - const db = new FakeDb(); - const server = odata(db); - resource = server.resource('book', bookSchema) - httpServer = server.listen(port); - db.addData('book', data); - }); - - after(() => { - httpServer.close(); - }); - - describe('[contains]', () => { - it('should filter items', async function () { - mock = sinon.mock(resource.model); - mock.expects('$where').once().withArgs(`this.title.indexOf('i') != -1`).returns(resource.model); - await request(host).get(`/book?$filter=contains(title,'i')`); - mock.verify(); - }); - it('should filter items when it has extra spaces in query string', async function () { - mock = sinon.mock(resource.model); - mock.expects('$where').once().withArgs(`this.title.indexOf('Visual Studio') != -1`).returns(resource.model); - await request(host).get(`/book?$filter=contains(title,'Visual Studio')`); - mock.verify(); - }); - }); - - describe('[indexof]', () => { - it('should filter items', async function () { - mock = sinon.mock(resource.model); - mock.expects('$where').once().withArgs(`this.title.indexOf('i') >= 1`).returns(resource.model); - await request(host).get(`/book?$filter=indexof(title,'i') ge 1`); - mock.verify(); - }); - it('should filter items when it has extra spaces in query string', async function () { - mock = sinon.mock(resource.model); - mock.expects('$where').once().withArgs(`this.title.indexOf('Visual Studio') >= 0`).returns(resource.model); - const res = await request(host).get(`/book?$filter=indexof(title,'Visual Studio') ge 0`); - mock.verify(); - }); - }); - - describe('[year]', () => { - it('should filter items', async function () { - mock = sinon.mock(resource.model); - mock.expects('where').once().withArgs(`publish_date`).returns(resource.model); - mock.expects('gte').once().withArgs(new Date(2000, 0, 1)).returns(resource.model); - mock.expects('lt').once().withArgs(new Date(2001, 0, 1)).returns(resource.model); - const res = await request(host).get(`/book?$filter=year(publish_date) eq 2000`); - mock.verify(); - }); - }); -}); diff --git a/test/mocked/odata.query.filter.js b/test/mocked/odata.query.filter.js deleted file mode 100644 index 0d6956d..0000000 --- a/test/mocked/odata.query.filter.js +++ /dev/null @@ -1,150 +0,0 @@ -import 'should'; -import sinon from 'sinon'; -import request from 'supertest'; -import { odata, host, port, bookSchema } from '../support/setup'; -import data from '../support/books.json'; -import FakeDb from '../support/fake-db'; - -describe('odata.query.filter', function() { - let httpServer, books, resource, mock; - - before(async function() { - const db = new FakeDb(); - const server = odata(db); - server.resource('book', bookSchema); - resource = server.resources.book; - httpServer = server.listen(port); - books = db.addData('book', data); - }); - - after(() => { - httpServer.close(); - }); - - afterEach(() => { - mock.restore(); - }); - - describe('[Equal]', () => { - it('should filter items', async function(){ - mock = sinon.mock(resource.model); - mock.expects('where').once().withArgs('title').returns(resource.model); - mock.expects('equals').once().withArgs(data[1].title).returns(resource.model); - await request(host).get(`/book?$filter=title eq '${data[1].title}'`); - mock.verify(); - }); - it('should filter items when field has keyword', async function(){ - mock = sinon.mock(resource.model); - mock.expects('where').once().withArgs('author').returns(resource.model); - mock.expects('equals').once().withArgs('Ralls, Kim').returns(resource.model); - await request(host).get(`/book?$filter=author eq 'Ralls, Kim'`); - mock.verify(); - }); - it('should filter items when it has extra spaces at begin', async function(){ - mock = sinon.mock(resource.model); - mock.expects('where').once().withArgs('title').returns(resource.model); - mock.expects('equals').once().withArgs(data[1].title).returns(resource.model); - await request(host).get(`/book?$filter= title eq '${data[1].title}'`); - mock.verify(); - }); - it('should filter items when it has extra spaces at mid', async function(){ - mock = sinon.mock(resource.model); - mock.expects('where').once().withArgs('title').returns(resource.model); - mock.expects('equals').once().withArgs(data[1].title).returns(resource.model); - await request(host).get(`/book?$filter=title eq '${data[1].title}'`); - mock.verify(); - }); - it('should filter items when it has extra spaces at end', async function(){ - mock = sinon.mock(resource.model); - mock.expects('where').once().withArgs('title').returns(resource.model); - mock.expects('equals').once().withArgs(data[1].title).returns(resource.model); - await request(host).get(`/book?$filter=title eq '${data[1].title}' `); - mock.verify(); - }); - it('should filter items when use chinese keyword', async function(){ - mock = sinon.mock(resource.model); - mock.expects('where').once().withArgs('title').returns(resource.model); - mock.expects('equals').once().withArgs('代码大全').returns(resource.model); - await request(host).get(encodeURI(`/book?$filter=title eq '代码大全'`)); - mock.verify(); - }); - it('should filter items when use id', async function(){ - mock = sinon.mock(resource.model); - mock.expects('where').once().withArgs('_id').returns(resource.model); - mock.expects('equals').once().withArgs(books[1].id).returns(resource.model); - await request(host).get(encodeURI(`/book?$filter=id eq '${books[1].id}'`)); - mock.verify(); - }); - }); - - describe("[Not equal]", () => { - it('should filter items', async function(){ - mock = sinon.mock(resource.model); - mock.expects('where').once().withArgs('title').returns(resource.model); - mock.expects('ne').once().withArgs(data[1].title).returns(resource.model); - await request(host).get(`/book?$filter=title ne '${data[1].title}'`); - mock.verify(); - }); - }); - - describe("[Greater than]", () => { - it('should filter items', async function(){ - mock = sinon.mock(resource.model); - mock.expects('where').once().withArgs('price').returns(resource.model); - mock.expects('gt').once().withArgs(36.95).returns(resource.model); - await request(host).get(`/book?$filter=price gt 36.95`); - mock.verify(); - }); - }); - - describe('[Greater than or equal]', () => { - it('should filter items', async function(){ - mock = sinon.mock(resource.model); - mock.expects('where').once().withArgs('price').returns(resource.model); - mock.expects('gte').once().withArgs(36.95).returns(resource.model); - await request(host).get(`/book?$filter=price ge 36.95`); - mock.verify(); - }); - }); - - describe('[Less than]', () => { - it('should filter items', async function() { - mock = sinon.mock(resource.model); - mock.expects('where').once().withArgs('price').returns(resource.model); - mock.expects('lt').once().withArgs(36.95).returns(resource.model); - await request(host).get(`/book?$filter=price lt 36.95`); - mock.verify(); - }); - }); - - describe('[Less than or equal]', () => { - it('should filter items', async function() { - mock = sinon.mock(resource.model); - mock.expects('where').once().withArgs('price').returns(resource.model); - mock.expects('lte').once().withArgs(36.95).returns(resource.model); - await request(host).get(`/book?$filter=price le 36.95`); - mock.verify(); - }); - }); - - describe('[Logical and]', () => { - it("should filter items", async function() { - mock = sinon.mock(resource.model); - mock.expects('where').withArgs('title').returns(resource.model); - mock.expects('where').withArgs('price').returns(resource.model); - mock.expects('ne').once().withArgs(data[1].title).returns(resource.model); - mock.expects('gte').once().withArgs(36.95).returns(resource.model); - await request(host).get(`/book?$filter=title ne '${data[1].title}' and price ge 36.95`); - mock.verify(); - }); - it("should filter items when it has extra spaces", async function() { - mock = sinon.mock(resource.model); - mock.expects('where').withArgs('title').returns(resource.model); - mock.expects('where').withArgs('price').returns(resource.model); - mock.expects('ne').once().withArgs(data[1].title).returns(resource.model); - mock.expects('gte').once().withArgs(36.95).returns(resource.model); - await request(host).get(`/book?$filter=title ne '${data[1].title}' and price ge 36.95`); - mock.verify(); - }); - }); -}); diff --git a/test/mongo/connected/model.complex.js b/test/mongo/connected/model.complex.js new file mode 100644 index 0000000..937807e --- /dev/null +++ b/test/mongo/connected/model.complex.js @@ -0,0 +1,56 @@ +// For issue: https://github.com/TossShinHwa/node-odata/issues/55 + +import 'should'; +import request from 'supertest'; +import { odata, host, port, assertSuccess } from '../../support/setup'; +import mongoose from 'mongoose'; +import { connect } from '../../support/db'; + +const Schema = mongoose.Schema; + +function addResource() { + return request(host) + .post('/complex-model') + .send({ p1: [{ p2: 'origin' }] }); +} + +function updateResouce(id) { + return request(host) + .put(`/complex-model('${id}')`) + .send({ p1: [{ p2: 'new' }] }); +} + +function queryResource(id) { + return request(host) + .get(`/complex-model('${id}')`); +} + +describe('model.complex', () => { + let httpServer; + + before(() => { + const server = odata(); + const ComplexModelSchema = new Schema({ p1: [{ p2: String }] }); + + const ComplexModel = mongoose.model('p1', ComplexModelSchema); + server.mongoEntity('complex-model', ComplexModel); + connect(server); + httpServer = server.listen(port); + }); + + after(() => { + httpServer.close(); + mongoose.default.connection.close(); + }); + + it('should work when PUT a complex entity', async function() { + const entity = await addResource(); + entity.body.should.have.property('id'); + assertSuccess(entity) + let res = await updateResouce(entity.body.id); + assertSuccess(res); + res = await queryResource(entity.body.id); + assertSuccess(res); + res.body.p1[0].p2.should.be.equal('new'); + }); +}); diff --git a/test/mongo/connected/model.special.name.js b/test/mongo/connected/model.special.name.js new file mode 100644 index 0000000..b895903 --- /dev/null +++ b/test/mongo/connected/model.special.name.js @@ -0,0 +1,34 @@ +import 'should'; +import request from 'supertest'; +import { odata, host, port, assertSuccess } from '../../support/setup'; +import mongoose from 'mongoose'; +import { connect } from '../../support/db'; + +const Schema = mongoose.Schema; + +describe('model.special.name', () => { + let httpServer; + + before(() => { + const server = odata(); + const ModelSchema = new Schema({ year: Number }); + + const Model = mongoose.model('function-keyword', ModelSchema); + server.mongoEntity('function-keyword', Model); + connect(server); + httpServer = server.listen(port); + }); + + after(() => { + httpServer.close(); + mongoose.default.connection.close(); + }); + + it('should work when use odata function keyword', async function() { + const res = await request(host) + .post('/function-keyword') + .send({ year: 2015 }); + assertSuccess(res); + res.status.should.be.equal(201); + }); +}); diff --git a/test/mongo/connected/rest.list.js b/test/mongo/connected/rest.list.js new file mode 100644 index 0000000..7195fe6 --- /dev/null +++ b/test/mongo/connected/rest.list.js @@ -0,0 +1,29 @@ +import 'should'; +import request from 'supertest'; +import { odata, host, port } from '../../support/setup'; +import mongoose from 'mongoose'; +import { connect } from '../../support/db'; +import { BookModel } from '../../support/books.model'; + +describe('mongo.Entity', () => { + let httpServer + + before(() => { + const server = odata(); + + server.mongoEntity('book', BookModel); + connect(server); + + httpServer = server.listen(port); + }); + + after(() => { + httpServer.close(); + mongoose.default.connection.close(); + }); + + it('should work', async function () { + const res = await request(host).get('/book'); + res.body.should.be.have.property('value'); + }); +}); diff --git a/test/neededDbRunning/rest.post.js b/test/mongo/connected/rest.post.js similarity index 62% rename from test/neededDbRunning/rest.post.js rename to test/mongo/connected/rest.post.js index d93a7b8..8041cf2 100644 --- a/test/neededDbRunning/rest.post.js +++ b/test/mongo/connected/rest.post.js @@ -1,21 +1,23 @@ import 'should'; import request from 'supertest'; -import { odata, host, port, bookSchema, conn } from '../support/setup'; +import { odata, host, port } from '../../support/setup'; +import { connect } from '../../support/db'; +import mongoose from 'mongoose'; +import { BookModel } from '../../support/books.model'; describe('rest.post', () => { let httpServer, server; before(function() { - server = odata(conn); - server.resource('book', bookSchema) + server = odata(); + server.mongoEntity('book', BookModel); + connect(server); httpServer = server.listen(port); }); after(() => { - const db = server.get('db'); - httpServer.close(); - db.closeConnection(); + mongoose.default.connection.close(); }); it('should create new resource', async function() { diff --git a/test/mocked/metadata.js b/test/mongo/metadata.js similarity index 86% rename from test/mocked/metadata.js rename to test/mongo/metadata.js index 90b3130..a507395 100644 --- a/test/mocked/metadata.js +++ b/test/mongo/metadata.js @@ -3,20 +3,26 @@ import 'should'; import request from 'supertest'; -import { host, conn, port, odata, assertSuccess } from '../support/setup'; -import FakeDb from '../support/fake-db'; +import { host, port, odata, assertSuccess } from '../support/setup'; +import mongoose from 'mongoose'; + +const Schema = mongoose.Schema; describe('metadata', () => { - let httpServer, server, db; + let httpServer, server; + + before(() => { + mongoose.set('overwriteModels', true); + }) beforeEach(async function() { - db = new FakeDb(); - server = odata(db); + server = odata(); }); afterEach(() => { httpServer.close(); + debugger; }); it('should return json metadata and ignore unknown attributes', async function() { @@ -50,7 +56,7 @@ describe('metadata', () => { } }, }; - server.resource('book', { + const BookSchema = new Schema({ price: { type: Number, min: 1, @@ -69,6 +75,10 @@ describe('metadata', () => { }] } }); + + const BookModel = mongoose.model('book', BookSchema); + + server.mongoEntity('book', BookModel); httpServer = server.listen(port); const res = await request(host).get('/$metadata?$format=json'); assertSuccess(res); @@ -86,9 +96,9 @@ describe('metadata', () => { - + @@ -96,7 +106,7 @@ describe('metadata', () => { `.replace(/\s*\s*/g, '>'); - server.resource('book', { + const BookSchema = new Schema({ price: { type: Number, min: 1, @@ -113,6 +123,10 @@ describe('metadata', () => { }] } }); + + const BookModel = mongoose.model('book', BookSchema); + + server.mongoEntity('book', BookModel); httpServer = server.listen(port); const res = await request(host).get('/$metadata'); assertSuccess(res); @@ -148,12 +162,16 @@ describe('metadata', () => { } }, }; - server.resource('book', { + const BookSchema = new Schema({ author: { type: String, maxLength: 25 } }); + + const BookModel = mongoose.model('book', BookSchema); + + server.mongoEntity('book', BookModel); httpServer = server.listen(port); const res = await request(host).get('/$metadata?$format=json'); assertSuccess(res); @@ -171,8 +189,8 @@ describe('metadata', () => { - + @@ -180,12 +198,16 @@ describe('metadata', () => { `.replace(/\s*\s*/g, '>'); - server.resource('book', { + const BookSchema = new Schema({ author: { type: String, maxLength: 25 } }); + + const BookModel = mongoose.model('book', BookSchema); + + server.mongoEntity('book', BookModel); httpServer = server.listen(port); const res = await request(host).get('/$metadata'); assertSuccess(res); @@ -221,12 +243,16 @@ describe('metadata', () => { } }, }; - server.resource('book', { + const BookSchema = new Schema({ author: { type: String, default: 'William Shakespeare' } }); + + const BookModel = mongoose.model('book', BookSchema); + + server.mongoEntity('book', BookModel); httpServer = server.listen(port); const res = await request(host).get('/$metadata?$format=json'); assertSuccess(res); @@ -244,8 +270,8 @@ describe('metadata', () => { - + @@ -253,12 +279,16 @@ describe('metadata', () => { `.replace(/\s*\s*/g, '>'); - server.resource('book', { + const BookSchema = new Schema({ author: { type: String, default: 'William Shakespeare' } }); + + const BookModel = mongoose.model('book', BookSchema); + + server.mongoEntity('book', BookModel); httpServer = server.listen(port); const res = await request(host).get('/$metadata'); assertSuccess(res); @@ -293,11 +323,15 @@ describe('metadata', () => { } }, }; - server.resource('book', { + const BookSchema = new Schema({ salted: { type: Boolean } }); + + const BookModel = mongoose.model('book', BookSchema); + + server.mongoEntity('book', BookModel); httpServer = server.listen(port); const res = await request(host).get('/$metadata?$format=json'); assertSuccess(res); @@ -315,8 +349,8 @@ describe('metadata', () => { - + @@ -324,60 +358,19 @@ describe('metadata', () => { `.replace(/\s*\s*/g, '>'); - server.resource('book', { + const BookSchema = new Schema({ salted: { type: Boolean } }); + + const BookModel = mongoose.model('book', BookSchema); + + server.mongoEntity('book', BookModel); httpServer = server.listen(port); const res = await request(host).get('/$metadata'); assertSuccess(res); res.text.should.equal(xmlDocument); }); - - it('should return json metadata for custom resource', async function() { - const jsonDocument = { - $Version: '4.0', - ObjectId: { - $Kind: "TypeDefinition", - $UnderlyingType: "Edm.String", - $MaxLength: 24 - }, - book: { - $Kind: "EntityType", - $Key: ["id"], - id: { - $Type: "node.odata.ObjectId", - $Nullable: false, - }, - salted: { - $Type: 'Edm.Boolean' - } - }, - $EntityContainer: 'node.odata', - ['node.odata']: { - $Kind: 'EntityContainer', - book: { - $Collection: true, - $Type: `node.odata.book`, - } - }, - }; - server.entity('book', {}, { - $Key: ["id"], - id: { - $Type: "node.odata.ObjectId", - $Nullable: false, - }, - salted: { - $Type: 'Edm.Boolean' - } - }); - httpServer = server.listen(port); - const res = await request(host).get('/$metadata?$format=json'); - assertSuccess(res); - res.body.should.deepEqual(jsonDocument); - }); - }); diff --git a/test/mocked/metadata.resource.complex.js b/test/mongo/metadata.resource.complex.js similarity index 75% rename from test/mocked/metadata.resource.complex.js rename to test/mongo/metadata.resource.complex.js index e1a0c9b..6a38d74 100644 --- a/test/mocked/metadata.resource.complex.js +++ b/test/mongo/metadata.resource.complex.js @@ -3,15 +3,20 @@ import 'should'; import request from 'supertest'; -import { host, conn, port, odata, assertSuccess } from '../support/setup'; -import FakeDb from '../support/fake-db'; +import { host, port, odata, assertSuccess } from '../support/setup'; +import mongoose from 'mongoose'; + +const Schema = mongoose.Schema; describe('metadata.resource.complex', () => { - let httpServer, server, db; + let httpServer, server; + + before(() => { + mongoose.set('overwriteModels', true); + }) beforeEach(async function() { - db = new FakeDb(); - server = odata(db); + server = odata(); }); afterEach(() => { @@ -26,8 +31,12 @@ describe('metadata.resource.complex', () => { $UnderlyingType: "Edm.String", $MaxLength: 24 }, - p1Child1: { + "complex-modelp1Child1": { $Kind: 'ComplexType', + id: { + $Type: "node.odata.ObjectId", + $Nullable: false, + }, p2: { $Type: 'Edm.String' } @@ -40,7 +49,7 @@ describe('metadata.resource.complex', () => { $Nullable: false, }, p1: { - $Type: 'node.odata.p1Child1', + $Type: 'node.odata.complex-modelp1Child1', $Collection: true } }, @@ -53,11 +62,14 @@ describe('metadata.resource.complex', () => { } }, }; - server.resource('complex-model', { + const ComplexModelSchema = new Schema({ p1: [{ // array of objects p2: String }] }); + + const ComplexModel = mongoose.model('complex-model', ComplexModelSchema); + server.mongoEntity('complex-model', ComplexModel); httpServer = server.listen(port); const res = await request(host).get('/$metadata?$format=json'); assertSuccess(res); @@ -71,15 +83,16 @@ describe('metadata.resource.complex', () => { - + + + - @@ -87,11 +100,14 @@ describe('metadata.resource.complex', () => { `.replace(/\s*\s*/g, '>'); - server.resource('complex-model', { + const ComplexModelSchema = new Schema({ p1: [{ // array of objects p2: String }] }); + + const ComplexModel = mongoose.model('complex-model', ComplexModelSchema); + server.mongoEntity('complex-model', ComplexModel); httpServer = server.listen(port); const res = await request(host).get('/$metadata').set('accept', 'application/xml'); assertSuccess(res); @@ -127,9 +143,12 @@ describe('metadata.resource.complex', () => { } }, }; - server.resource('complex-model', { - p3: [String], // array of primitive type + const ComplexModelSchema = new Schema({ + p3: [String] // array of primitive type }); + + const ComplexModel = mongoose.model('complex-model', ComplexModelSchema); + server.mongoEntity('complex-model', ComplexModel); httpServer = server.listen(port); const res = await request(host).get('/$metadata?$format=json').set('accept', 'application/json'); res.statusCode.should.equal(200); @@ -147,8 +166,8 @@ describe('metadata.resource.complex', () => { - + @@ -156,9 +175,12 @@ describe('metadata.resource.complex', () => { `.replace(/\s*\s*/g, '>'); - server.resource('complex-model', { - p3: [String] + const ComplexModelSchema = new Schema({ + p3: [String] // array of primitive type }); + + const ComplexModel = mongoose.model('complex-model', ComplexModelSchema); + server.mongoEntity('complex-model', ComplexModel); httpServer = server.listen(port); const res = await request(host).get('/$metadata').set('accept', 'application/xml'); assertSuccess(res); @@ -177,8 +199,8 @@ describe('metadata.resource.complex', () => { - + @@ -186,12 +208,15 @@ describe('metadata.resource.complex', () => { `.replace(/\s*\s*/g, '>'); - server.resource('complex-model', { + const ComplexModelSchema = new Schema({ p3: [{ type: String, enum: ['P4'] }] }); + + const ComplexModel = mongoose.model('complex-model', ComplexModelSchema); + server.mongoEntity('complex-model', ComplexModel); httpServer = server.listen(port); const res = await request(host).get('/$metadata').set('accept', 'application/xml'); assertSuccess(res); @@ -207,6 +232,12 @@ describe('metadata.resource.complex', () => { $UnderlyingType: "Edm.String", $MaxLength: 24 }, + "complex-modelp4Child1": { + $Kind: 'ComplexType', + p5: { + $Type: 'Edm.String' + } + }, 'complex-model': { $Kind: "EntityType", $Key: ["id"], @@ -214,8 +245,8 @@ describe('metadata.resource.complex', () => { $Type: "node.odata.ObjectId", $Nullable: false, }, - 'p4-p5': { - $Type: 'Edm.String' + p4: { + $Type: 'node.odata.complex-modelp4Child1' } }, $EntityContainer: 'node.odata', @@ -227,11 +258,14 @@ describe('metadata.resource.complex', () => { } }, }; - server.resource('complex-model', { + const ComplexModelSchema = new Schema({ p4: { p5: String } }); + + const ComplexModel = mongoose.model('complex-model', ComplexModelSchema); + server.mongoEntity('complex-model', ComplexModel); httpServer = server.listen(port); const res = await request(host).get('/$metadata?$format=json'); res.statusCode.should.equal(200); @@ -245,12 +279,15 @@ describe('metadata.resource.complex', () => { + + + - + @@ -258,11 +295,14 @@ describe('metadata.resource.complex', () => { `.replace(/\s*\s*/g, '>'); - server.resource('complex-model', { + const ComplexModelSchema = new Schema({ p4: { p5: String } }); + + const ComplexModel = mongoose.model('complex-model', ComplexModelSchema); + server.mongoEntity('complex-model', ComplexModel); httpServer = server.listen(port); const res = await request(host).get('/$metadata'); assertSuccess(res); @@ -277,12 +317,22 @@ describe('metadata.resource.complex', () => { $UnderlyingType: "Edm.String", $MaxLength: 24 }, - p2Child1: { + p1p2Child1: { $Kind: "ComplexType", p3: { $Type: 'Edm.String' }, - 'p4-p5': { + p4: { + $Type: "node.odata.p1p4Child2" + }, + id: { + $Type: "node.odata.ObjectId", + $Nullable: false, + } + }, + p1p4Child2:{ + $Kind: "ComplexType", + p5: { $Type: 'Edm.String' } }, @@ -294,7 +344,7 @@ describe('metadata.resource.complex', () => { $Nullable: false, }, p2: { - $Type: 'node.odata.p2Child1', + $Type: 'node.odata.p1p2Child1', $Collection: true } }, @@ -307,7 +357,7 @@ describe('metadata.resource.complex', () => { } }, }; - server.resource('p1', { + const ComplexModelSchema = new Schema({ p2: [{ p3: String, p4: { @@ -315,6 +365,9 @@ describe('metadata.resource.complex', () => { } }] }); + + const ComplexModel = mongoose.model('p1', ComplexModelSchema); + server.mongoEntity('p1', ComplexModel); httpServer = server.listen(port); const res = await request(host).get('/$metadata?$format=json'); res.statusCode.should.equal(200); @@ -328,16 +381,20 @@ describe('metadata.resource.complex', () => { - + + + + - + + + - @@ -345,7 +402,7 @@ describe('metadata.resource.complex', () => { `.replace(/\s*\s*/g, '>'); - server.resource('p1', { + const ComplexModelSchema = new Schema({ p2: [{ p3: String, p4: { @@ -353,6 +410,9 @@ describe('metadata.resource.complex', () => { } }] }); + + const ComplexModel = mongoose.model('p1', ComplexModelSchema); + server.mongoEntity('p1', ComplexModel); httpServer = server.listen(port); const res = await request(host).get('/$metadata'); assertSuccess(res); diff --git a/test/mongo/mocked/model.complex.filter.js b/test/mongo/mocked/model.complex.filter.js new file mode 100644 index 0000000..7fdf161 --- /dev/null +++ b/test/mongo/mocked/model.complex.filter.js @@ -0,0 +1,69 @@ +// For issue: https://github.com/TossShinHwa/node-odata/issues/69 + +import 'should'; +import 'should-sinon'; +import request from 'supertest'; +import { odata, assertSuccess, host, port } from '../../support/setup'; +import sinon from 'sinon'; +import mongoose from 'mongoose'; +import { init } from '../../support/db'; + +const Schema = mongoose.Schema; + +describe('model.complex.filter', () => { + let httpServer, modelMock, queryMock; + + before(() => { + const server = odata(); + const ComplexModelSchema = new Schema({ + product: { price: Number } + }); + + mongoose.set('overwriteModels', true); + const ComplexModel = mongoose.model('complex-model-filter', ComplexModelSchema); + + server.mongoEntity('complex-model-filter', ComplexModel); + init(server); + + const query = { + where: () => { }, + gt: () => { }, + select: () => { }, + exec: () => { }, + model: ComplexModel + }; + modelMock = sinon.mock(ComplexModel); + queryMock = sinon.mock(query); + modelMock.expects('find').once().returns(query); + + queryMock.expects('where').once().withArgs('product.price').returns(query); + queryMock.expects('gt').once().withArgs(30); + queryMock.expects('select').once().withArgs({ _id: 0, product: 1}); + queryMock.expects('exec').once().callsArgWith(0, null, [{ + toObject: () => ({ + product: { + price: 50 + } + }) + }]); + httpServer = server.listen(port); + }); + + after(() => { + httpServer.close(); + }); + + it('should work when filter a complex entity', async function () { + let res = await request(host).get(`/complex-model-filter?$select=product&$filter=product-price gt 30`); + assertSuccess(res); + res.body.should.deepEqual({ + value: [{ + product: { + price: 50 + } + }] + }); + modelMock.verify(); + queryMock.verify(); + }); +}); diff --git a/test/mongo/mocked/model.custom.id.js b/test/mongo/mocked/model.custom.id.js new file mode 100644 index 0000000..fc408c0 --- /dev/null +++ b/test/mongo/mocked/model.custom.id.js @@ -0,0 +1,89 @@ +import 'should'; +import request from 'supertest'; +import { odata, host, port, assertSuccess } from '../../support/setup'; +import sinon from 'sinon'; +import mongoose from 'mongoose'; +import { init } from '../../support/db'; + +const Schema = mongoose.Schema; + +describe('model.custom.id', () => { + let httpServer, modelMock, queryMock, Model; + + before(async function () { + const server = odata(); + + const ModelSchema = new Schema({ + id: { + type: Number, + unique: true + } + }); + + Model = mongoose.model('custom-id', ModelSchema); + + server.mongoEntity('custom-id', Model, undefined, { + id: { + $Type: 'Edm.Int16' + } + }, undefined, { + id: { + target: 'id', + attributes: { + $Type: 'Edm.Int16' + } + } + }); + init(server); + + httpServer = server.listen(port); + }); + + after(() => { + httpServer.close(); + }); + + afterEach(() => { + modelMock.restore(); + queryMock?.restore(); + }) + + it('should work when use a custom id to query specific entity', async function () { + modelMock = sinon.mock(Model); + modelMock.expects('findById').once().callsArgWith(1, null, { + toObject: () => ({ + id: 100 + }) + }); + + const res = await request(host).get('/custom-id(100)'); + assertSuccess(res); + res.body.id.should.be.equal(100); + modelMock.verify(); + }); + + it('should work when use a custom id to query a list', async function () { + const query = { + where: () => { }, + equals: () => { }, + exec: () => { }, + model: Model + }; + modelMock = sinon.mock(Model); + queryMock = sinon.mock(query); + modelMock.expects('find').once().returns(query); + + queryMock.expects('where').once().withArgs('id').returns(query); + queryMock.expects('equals').once().withArgs(100); + queryMock.expects('exec').once().callsArgWith(0, null, [{ + toObject: () => ({ + id: 100 + }) + }]); + const res = await request(host).get('/custom-id?$filter=id eq 100'); + assertSuccess(res); + res.body.value.length.should.be.greaterThan(0); + queryMock.verify(); + modelMock.verify(); + }); +}); diff --git a/test/mongo/mocked/model.hidden.field.js b/test/mongo/mocked/model.hidden.field.js new file mode 100644 index 0000000..3c2a2ad --- /dev/null +++ b/test/mongo/mocked/model.hidden.field.js @@ -0,0 +1,102 @@ +import 'should'; +import sinon from 'sinon'; +import request from 'supertest'; +import { odata, host, port, assertSuccess } from '../../support/setup'; +import mongoose from 'mongoose'; +import { init } from '../../support/db'; + +const Schema = mongoose.Schema; + +describe('model.hidden.field', function () { + let httpServer, modelMock, queryMock, Model; + + before(async function () { + const server = odata(); + + const ModelSchema = new Schema({ + name: String, + password: { + type: String, + select: false + } + }); + + Model = mongoose.model('hidden-field', ModelSchema); + + server.mongoEntity('hidden-field', Model); + init(server); + + httpServer = server.listen(port); + + }); + + after(() => { + httpServer.close(); + }); + + afterEach(() => { + modelMock.restore(); + queryMock?.restore(); + }); + + it('should work when get entities list even it is selected', async function () { + const query = { + where: () => { }, + select: () => { }, + exec: () => { }, + model: Model + }; + modelMock = sinon.mock(Model); + queryMock = sinon.mock(query); + modelMock.expects('find').once().returns(query); + + queryMock.expects('select').once().withArgs({ + _id: 0, + name: 1 + }); + queryMock.expects('exec').once().callsArgWith(0, null, [{ + toObject: () => ({ + name: 'zack' + }) + }]); + const res = await request(host).get('/hidden-field?$select=name, password'); + assertSuccess(res); + res.body.should.deepEqual({ + value: [{ + name: 'zack' + }] + }); + modelMock.verify(); + queryMock.verify(); + }); + + it('should work when get entities list even only it is selected', async function () { + const query = { + where: () => { }, + select: () => { }, + exec: () => { }, + model: Model + }; + modelMock = sinon.mock(Model); + queryMock = sinon.mock(query); + modelMock.expects('find').once().returns(query); + + queryMock.expects('select').never(); + queryMock.expects('exec').once().callsArgWith(0, null, [{ + toObject: () => ({ + _id: 'AFFE', + name: 'zack' + }) + }]); + const res = await request(host).get('/hidden-field?$select=password'); + assertSuccess(res); + res.body.should.deepEqual({ + value: [{ + id: 'AFFE', + name: 'zack' + }] + }); + modelMock.verify(); + queryMock.verify(); + }); +}); diff --git a/test/mongo/mocked/odata.count.js b/test/mongo/mocked/odata.count.js new file mode 100644 index 0000000..867002d --- /dev/null +++ b/test/mongo/mocked/odata.count.js @@ -0,0 +1,47 @@ +import 'should'; +import sinon from 'sinon'; +import request from 'supertest'; +import { odata, host, port } from '../../support/setup'; +import { init } from '../../support/db'; +import { BookModel } from '../../support/books.model'; + +describe('odata.count', function() { + let httpServer, modelMock, queryMock; + + before(async function() { + const server = odata(); + server.mongoEntity('book', BookModel); + init(server); + + httpServer = server.listen(port); + }); + + after(() => { + httpServer.close(); + modelMock.restore(); + queryMock?.restore(); + }); + + it('should get count for entity set', async function() { + const query = { + count: () => { }, + model: BookModel + }; + modelMock = sinon.mock(BookModel); + queryMock = sinon.mock(query); + modelMock.expects('find').once().returns(query); + queryMock.expects('count').once().callsArgWith(0, null, 13); + + const res = await request(host).get('/book/$count'); + + if (res.error) { + res.error.status.should.be.equal(200); + } + res.text.should.be.equal('13'); + res.header.should.have.property('content-type'); + res.header['content-type'].should.containEql('text/plain'); + + modelMock.verify(); + queryMock.verify(); + }); +}); diff --git a/test/mongo/mocked/odata.query.count.js b/test/mongo/mocked/odata.query.count.js new file mode 100644 index 0000000..21acf18 --- /dev/null +++ b/test/mongo/mocked/odata.query.count.js @@ -0,0 +1,63 @@ +import 'should'; +import sinon from 'sinon'; +import request from 'supertest'; +import { odata, host, port, assertSuccess } from '../../support/setup'; +import books from '../../support/books.json'; +import { init } from '../../support/db'; +import { BookModel } from '../../support/books.model'; + +describe('odata.query.count', function () { + const query = { + count: () => { }, + exec: () => { }, + model: BookModel + }; + let httpServer, modelMock, queryMock; + + before(async function () { + const server = odata(); + server.mongoEntity('book', BookModel); + init(server); + httpServer = server.listen(port); + }); + + after(() => { + httpServer.close(); + }); + + afterEach(() => { + modelMock.restore(); + queryMock?.restore(); + }); + + it('should get count', async function () { + modelMock = sinon.mock(BookModel); + queryMock = sinon.mock(query); + modelMock.expects('find').twice().returns(query); + queryMock.expects('count').once().callsArgWith(0, null, 13); + queryMock.expects('exec').once().callsArgWith(0, null, books.map(item => ({ toObject: () => item }))); + + const res = await request(host).get('/book?$count=true'); + assertSuccess(res); + res.body.should.be.have.property('@odata.count'); + res.body.should.be.have.property('value'); + res.body['@odata.count'].should.be.equal(res.body.value.length.toString()); + + modelMock.verify(); + queryMock.verify(); + }); + it('should not get count', async function () { + modelMock = sinon.mock(BookModel); + queryMock = sinon.mock(query); + modelMock.expects('find').twice().returns(query); + queryMock.expects('exec').once().callsArgWith(0, null, books.map(item => ({ toObject: () => item }))); + + const res = await request(host).get('/book?$count=false'); + res.body.should.be.not.have.property('@odata.count'); + }); + it('should 500 when $count isn\'t \'true\' or \'false\'', async function () { + const res = await request(host).get('/book?$count=1'); + res.error.status.should.be.equal(500); + }); + +}); diff --git a/test/mongo/mocked/odata.query.filter.functions.js b/test/mongo/mocked/odata.query.filter.functions.js new file mode 100644 index 0000000..f36fa95 --- /dev/null +++ b/test/mongo/mocked/odata.query.filter.functions.js @@ -0,0 +1,136 @@ +import 'should'; +import sinon from 'sinon'; +import request from 'supertest'; +import { odata, host, port, assertSuccess } from '../../support/setup'; +import data from '../../support/books.json'; +import FakeDb from '../../support/fake-db'; +import { BookModel } from '../../support/books.model'; + +describe('odata.query.filter.functions', function () { + const query = { + $where: () => { }, + where: () => { }, + gte: () => { }, + lt: () => { }, + exec: () => { }, + model: BookModel + }; + let httpServer, modelMock, queryMock; + + before(async function () { + const server = odata(); + + server.mongoEntity('book', BookModel); + httpServer = server.listen(port); + }); + + after(() => { + httpServer.close(); + }); + + afterEach(() => { + modelMock?.restore(); + queryMock?.restore(); + }); + + describe('[contains]', () => { + it('should filter items', async function () { + const books = data.filter(item => item.title.indexOf('i') >= 0); + + modelMock = sinon.mock(BookModel); + queryMock = sinon.mock(query); + modelMock.expects('find').once().returns(query); + queryMock.expects('$where').once().withArgs(`this.title.indexOf('i') != -1`); + queryMock.expects('exec').once().callsArgWith(0, null, books.map(item => ({ toObject: () => item }))); + + const res = await request(host).get(`/book?$filter=contains(title,'i')`); + + assertSuccess(res); + modelMock.verify(); + queryMock.verify(); + res.body.should.deepEqual({ + value: books + }); + }); + it('should filter items when it has extra spaces in query string', async function () { + const books = data.filter(item => item.title.indexOf('Visual Studio') >= 0); + + modelMock = sinon.mock(BookModel); + queryMock = sinon.mock(query); + modelMock.expects('find').once().returns(query); + queryMock.expects('$where').once().withArgs(`this.title.indexOf('Visual Studio') != -1`); + queryMock.expects('exec').once().callsArgWith(0, null, books.map(item => ({ toObject: () => item }))); + + const res = await request(host).get(`/book?$filter=contains(title,'Visual Studio')`); + + assertSuccess(res); + modelMock.verify(); + queryMock.verify(); + res.body.should.deepEqual({ + value: books + }); + }); + }); + + describe('[indexof]', () => { + it('should filter items', async function () { + const books = data.filter(item => item.title.indexOf('i') >= 1); + + modelMock = sinon.mock(BookModel); + queryMock = sinon.mock(query); + modelMock.expects('find').once().returns(query); + queryMock.expects('$where').once().withArgs(`this.title.indexOf('i') >= 1`); + queryMock.expects('exec').once().callsArgWith(0, null, books.map(item => ({ toObject: () => item }))); + + const res = await request(host).get(`/book?$filter=indexof(title,'i') ge 1`); + + assertSuccess(res); + modelMock.verify(); + queryMock.verify(); + res.body.should.deepEqual({ + value: books + }); + }); + it('should filter items when it has extra spaces in query string', async function () { + const books = data.filter(item => item.title.indexOf('Visual Studio') >= 0); + + modelMock = sinon.mock(BookModel); + queryMock = sinon.mock(query); + modelMock.expects('find').once().returns(query); + queryMock.expects('$where').once().withArgs(`this.title.indexOf('Visual Studio') >= 0`); + queryMock.expects('exec').once().callsArgWith(0, null, books.map(item => ({ toObject: () => item }))); + + const res = await request(host).get(`/book?$filter=indexof(title,'Visual Studio') ge 0`); + + assertSuccess(res); + modelMock.verify(); + queryMock.verify(); + res.body.should.deepEqual({ + value: books + }); + }); + }); + + describe('[year]', () => { + it('should filter items', async function () { + const books = data.filter(item => item.publish_date >= new Date(2000, 0, 1) && item.publish_date < new Date(2001, 0, 1)); + + modelMock = sinon.mock(BookModel); + queryMock = sinon.mock(query); + modelMock.expects('find').once().returns(query); + queryMock.expects('where').once().withArgs(`publish_date`).returns(query); + queryMock.expects('gte').once().withArgs(new Date(2000, 0, 1)).returns(query); + queryMock.expects('lt').once().withArgs(new Date(2001, 0, 1)).returns(query); + queryMock.expects('exec').once().callsArgWith(0, null, books.map(item => ({ toObject: () => item }))); + + const res = await request(host).get(`/book?$filter=year(publish_date) eq 2000`); + + assertSuccess(res); + modelMock.verify(); + queryMock.verify(); + res.body.should.deepEqual({ + value: books + }); + }); + }); +}); diff --git a/test/mongo/mocked/odata.query.filter.js b/test/mongo/mocked/odata.query.filter.js new file mode 100644 index 0000000..b851d9b --- /dev/null +++ b/test/mongo/mocked/odata.query.filter.js @@ -0,0 +1,243 @@ +import 'should'; +import sinon from 'sinon'; +import request from 'supertest'; +import { odata, host, port, assertSuccess } from '../../support/setup'; +import data from '../../support/books.json'; +import { BookModel } from '../../support/books.model'; + +describe('odata.query.filter', function () { + const query = { + $where: () => { }, + where: () => { }, + equals: () => { }, + gte: () => { }, + lt: () => { }, + exec: () => { }, + model: BookModel + }; + let httpServer, modelMock, queryMock; + + before(async function () { + const server = odata(); + + server.mongoEntity('book', BookModel); + httpServer = server.listen(port); + + }); + + after(() => { + httpServer.close(); + }); + + afterEach(() => { + modelMock?.restore(); + queryMock?.restore(); + }); + + describe('[Equal]', () => { + it('should filter items', async function () { + const books = data.filter(item => item.title === 'Midnight Rain'); + + modelMock = sinon.mock(BookModel); + queryMock = sinon.mock(query); + modelMock.expects('find').once().returns(query); + queryMock.expects('where').once().withArgs(`title`).returns(query); + queryMock.expects('equals').once().withArgs('Midnight Rain'); + queryMock.expects('exec').once().callsArgWith(0, null, books.map(item => ({ toObject: () => item }))); + + const res = await request(host).get(`/book?$filter=title eq '${data[1].title}'`); + + assertSuccess(res); + modelMock.verify(); + queryMock.verify(); + res.body.should.deepEqual({ + value: books + }); + }); + it('should filter items when field has keyword', async function () { + const books = data.filter(item => item.author === 'Ralls, Kim'); + + modelMock = sinon.mock(BookModel); + queryMock = sinon.mock(query); + modelMock.expects('find').once().returns(query); + queryMock.expects('where').once().withArgs(`author`).returns(query); + queryMock.expects('equals').once().withArgs('Ralls, Kim'); + queryMock.expects('exec').once().callsArgWith(0, null, books.map(item => ({ toObject: () => item }))); + + const res = await request(host).get(`/book?$filter=author eq 'Ralls, Kim'`); + + assertSuccess(res); + modelMock.verify(); + queryMock.verify(); + res.body.should.deepEqual({ + value: books + }); + }); + it('should filter items when it has extra spaces at begin', async function () { + const books = data.filter(item => item.title === 'Midnight Rain'); + + modelMock = sinon.mock(BookModel); + queryMock = sinon.mock(query); + modelMock.expects('find').once().returns(query); + queryMock.expects('where').once().withArgs(`title`).returns(query); + queryMock.expects('equals').once().withArgs('Midnight Rain'); + queryMock.expects('exec').once().callsArgWith(0, null, books.map(item => ({ toObject: () => item }))); + + const res = await request(host).get(`/book?$filter= title eq 'Midnight Rain'`); + + assertSuccess(res); + modelMock.verify(); + queryMock.verify(); + res.body.should.deepEqual({ + value: books + }); + }); + it('should filter items when it has extra spaces at mid', async function () { + const books = data.filter(item => item.title === 'Midnight Rain'); + + modelMock = sinon.mock(BookModel); + queryMock = sinon.mock(query); + modelMock.expects('find').once().returns(query); + queryMock.expects('where').once().withArgs(`title`).returns(query); + queryMock.expects('equals').once().withArgs('Midnight Rain'); + queryMock.expects('exec').once().callsArgWith(0, null, books.map(item => ({ toObject: () => item }))); + + const res = await request(host).get(`/book?$filter=title eq 'Midnight Rain'`); + + assertSuccess(res); + modelMock.verify(); + queryMock.verify(); + res.body.should.deepEqual({ + value: books + }); + }); + it('should filter items when it has extra spaces at end', async function () { + const books = data.filter(item => item.title === 'Midnight Rain'); + + modelMock = sinon.mock(BookModel); + queryMock = sinon.mock(query); + modelMock.expects('find').once().returns(query); + queryMock.expects('where').once().withArgs(`title`).returns(query); + queryMock.expects('equals').once().withArgs('Midnight Rain'); + queryMock.expects('exec').once().callsArgWith(0, null, books.map(item => ({ toObject: () => item }))); + + const res = await request(host).get(`/book?$filter=title eq '${data[1].title}' `); + + assertSuccess(res); + modelMock.verify(); + queryMock.verify(); + res.body.should.deepEqual({ + value: books + }); + }); + it('should filter items when use chinese keyword', async function () { + const books = data.filter(item => item.title === '代码大全'); + + modelMock = sinon.mock(BookModel); + queryMock = sinon.mock(query); + modelMock.expects('find').once().returns(query); + queryMock.expects('where').once().withArgs(`title`).returns(query); + queryMock.expects('equals').once().withArgs('代码大全'); + queryMock.expects('exec').once().callsArgWith(0, null, books.map(item => ({ toObject: () => item }))); + + const res = await request(host).get(encodeURI(`/book?$filter=title eq '代码大全'`)); + + assertSuccess(res); + modelMock.verify(); + queryMock.verify(); + res.body.should.deepEqual({ + value: books + }); + }); + it('should filter items when use id', async function () { + const books = data.filter(item => item.id === '2'); + + modelMock = sinon.mock(BookModel); + queryMock = sinon.mock(query); + modelMock.expects('find').once().returns(query); + queryMock.expects('where').once().withArgs(`_id`).returns(query); + queryMock.expects('equals').once().withArgs('2'); + queryMock.expects('exec').once().callsArgWith(0, null, books.map(item => ({ toObject: () => item }))); + + const res = await request(host).get(encodeURI(`/book?$filter=id eq '2'`)); + + assertSuccess(res); + modelMock.verify(); + queryMock.verify(); + res.body.should.deepEqual({ + value: books + }); + }); + }); + + describe("[Not equal]", () => { + it('should filter items', async function () { + mock = sinon.mock(resource.model); + mock.expects('where').once().withArgs('title').returns(resource.model); + mock.expects('ne').once().withArgs(data[1].title).returns(resource.model); + await request(host).get(`/book?$filter=title ne '${data[1].title}'`); + mock.verify(); + }); + }); + + describe("[Greater than]", () => { + it('should filter items', async function () { + mock = sinon.mock(resource.model); + mock.expects('where').once().withArgs('price').returns(resource.model); + mock.expects('gt').once().withArgs(36.95).returns(resource.model); + await request(host).get(`/book?$filter=price gt 36.95`); + mock.verify(); + }); + }); + + describe('[Greater than or equal]', () => { + it('should filter items', async function () { + mock = sinon.mock(resource.model); + mock.expects('where').once().withArgs('price').returns(resource.model); + mock.expects('gte').once().withArgs(36.95).returns(resource.model); + await request(host).get(`/book?$filter=price ge 36.95`); + mock.verify(); + }); + }); + + describe('[Less than]', () => { + it('should filter items', async function () { + mock = sinon.mock(resource.model); + mock.expects('where').once().withArgs('price').returns(resource.model); + mock.expects('lt').once().withArgs(36.95).returns(resource.model); + await request(host).get(`/book?$filter=price lt 36.95`); + mock.verify(); + }); + }); + + describe('[Less than or equal]', () => { + it('should filter items', async function () { + mock = sinon.mock(resource.model); + mock.expects('where').once().withArgs('price').returns(resource.model); + mock.expects('lte').once().withArgs(36.95).returns(resource.model); + await request(host).get(`/book?$filter=price le 36.95`); + mock.verify(); + }); + }); + + describe('[Logical and]', () => { + it("should filter items", async function () { + mock = sinon.mock(resource.model); + mock.expects('where').withArgs('title').returns(resource.model); + mock.expects('where').withArgs('price').returns(resource.model); + mock.expects('ne').once().withArgs(data[1].title).returns(resource.model); + mock.expects('gte').once().withArgs(36.95).returns(resource.model); + await request(host).get(`/book?$filter=title ne '${data[1].title}' and price ge 36.95`); + mock.verify(); + }); + it("should filter items when it has extra spaces", async function () { + mock = sinon.mock(resource.model); + mock.expects('where').withArgs('title').returns(resource.model); + mock.expects('where').withArgs('price').returns(resource.model); + mock.expects('ne').once().withArgs(data[1].title).returns(resource.model); + mock.expects('gte').once().withArgs(36.95).returns(resource.model); + await request(host).get(`/book?$filter=title ne '${data[1].title}' and price ge 36.95`); + mock.verify(); + }); + }); +}); diff --git a/test/mocked/odata.actions.js b/test/odata.actions.js similarity index 53% rename from test/mocked/odata.actions.js rename to test/odata.actions.js index d1d1520..5ffc73c 100644 --- a/test/mocked/odata.actions.js +++ b/test/odata.actions.js @@ -1,23 +1,17 @@ import 'should'; import request from 'supertest'; -import { odata, host, port, bookSchema } from '../support/setup'; -import data from '../support/books.json'; -import FakeDb from '../support/fake-db'; +import { odata, host, port, assertSuccess } from './support/setup'; +import { BookMetadata } from './support/books.model'; function requestToHalfPrice(id) { - return request(host).post(`/book(${id})/50off`); -} - -function halfPrice(price) { - return +(price / 2).toFixed(2); + return request(host).post(`/book('${id}')/50off`); } describe('odata.actions', () => { - let httpServer, server, db; + let httpServer, server; beforeEach(async function () { - db = new FakeDb(); - server = odata(db); + server = odata(); }); afterEach(() => { @@ -27,31 +21,24 @@ describe('odata.actions', () => { }); it('should work with bound action', async function () { - server.resource('book', bookSchema) + server.entity('book', undefined, BookMetadata) .action('50off', (req, res, next) => { - req.$odata.mongo.book.findById(req.params.id, (err, book) => { - book.price = halfPrice(book.price); - book.save((err) => { - res.$odata.result = book; - next(); - }); - }); + res.$odata.status = 200; + res.$odata.result = {}; + req.$odata.$Key.id.should.be.equal('AFFE'); + next(); }, { binding: 'entity' }); - const books = JSON.parse(JSON.stringify(db.addData('book', data))); httpServer = server.listen(port); - const item = books[0]; - - const res = await requestToHalfPrice(item.id); - const price = halfPrice(item.price); - res.body.price.should.be.equal(price); + const res = await requestToHalfPrice('AFFE'); + assertSuccess(res); }); it('should work with unbound action', async function () { server.action('salam-aleikum', async (req, res) => { - res.$odata.result = {result: 'Wa aleikum assalam'}; + res.$odata.result = { result: 'Wa aleikum assalam' }; }) httpServer = server.listen(port); @@ -66,7 +53,7 @@ describe('odata.actions', () => { it('should return 404 for action url without namespace', async function () { server.action('salam-aleikum', async (req, res) => { - res.$odata.result = {result: 'Wa aleikum assalam'}; + res.$odata.result = { result: 'Wa aleikum assalam' }; }) httpServer = server.listen(port); diff --git a/test/mocked/odata.batch.js b/test/odata.batch.js similarity index 55% rename from test/mocked/odata.batch.js rename to test/odata.batch.js index 456a3cc..a1f7ea5 100644 --- a/test/mocked/odata.batch.js +++ b/test/odata.batch.js @@ -1,53 +1,43 @@ import 'should'; import request from 'supertest'; -import { odata, host, port, bookSchema, assertSuccess } from '../support/setup'; -import data from '../support/books.json'; -import FakeDb from '../support/fake-db'; -import sinon from 'sinon'; +import { odata, host, port, assertSuccess } from './support/setup'; +import data from './support/books.json'; +import { BookMetadata } from './support/books.model'; describe('odata.batch', () => { - let httpServer, books, resource, sandbox; - - beforeEach(async function () { - const db = new FakeDb(); - const server = odata(db); - resource = server.resource('book', bookSchema); - resource.action('entity-action', async (req, res) => { - res.$odata.result = {result: 'Hello! I am an action, that bound to entity.'}; - }, { binding: 'entity'}); - server.action('unbound-action', async (req, res) => { - res.$odata.result = { result: 'Hello! I am an unbound action.'}; - }) - books = JSON.parse(JSON.stringify(db.addData('book', data))); - httpServer = server.listen(port); - sandbox = sinon.createSandbox(); - }); + const books = data; + let httpServer, resource, sandbox; afterEach(() => { httpServer.close(); - sandbox.restore(); }); - it('should work with get lists', async function () { - const result = [ - { - "title": "XML Developer's Guide" - }, { - "title": "MSXML3: A Comprehensive Guide" - }, { - "title": "Visual Studio 7: A Comprehensive Guide" + const result = books.filter(item => item.title.indexOf('Guide') >= 0) + .map(item => ({ title: item.title })); + const server = odata(); + + server.entity('book', { + list: (req, res, next) => { + req.$odata.$filter.should.deepEqual({ + title: { + $function: { + $name: 'contains', + $parameter: 'Guide', + $operator: undefined, + $value: undefined + } + } + }); + req.$odata.$select.should.deepEqual(['title']); + res.$odata.result = { + value: result + }; + next(); } - ]; + }, BookMetadata); + httpServer = server.listen(port); - const mock = sandbox.mock(resource.model); - mock.expects('select').once().withArgs({ - _id: 0, - title: 1 - }).returns(resource.model); - mock.expects('$where').once().withArgs('this.title.indexOf(\'Guide\') != -1').returns(resource.model); - const stub = sandbox.stub(resource.model, "exec"); - stub.callsArgWith(0, undefined, result); const res = await request(host).post(`/$batch`).send({ requests: [{ id: "1", @@ -56,7 +46,6 @@ describe('odata.batch', () => { }] }); assertSuccess(res); - mock.verify(); res.body.should.deepEqual({ responses: [{ id: "1", @@ -74,6 +63,17 @@ describe('odata.batch', () => { }); it('should work with get entity', async function () { + const server = odata(); + + server.entity('book', { + get: (req, res, next) => { + req.$odata.$Key.id.should.be.equal(books[0].id); + res.$odata.result = books[0]; + next(); + } + }, BookMetadata); + httpServer = server.listen(port); + const res = await request(host).post(`/$batch`).send({ requests: [{ id: "1", @@ -100,6 +100,21 @@ describe('odata.batch', () => { const result = { title: "War and peace" }; + const server = odata(); + + server.entity('book', { + post: (req, res, next) => { + req.$odata.body.should.deepEqual(result); + res.$odata.result = { + id: "AFFE", + ...result + }; + res.$odata.status = 201; + next(); + } + }, BookMetadata); + httpServer = server.listen(port); + const res = await request(host).post(`/$batch`).send({ requests: [{ id: "1", @@ -109,7 +124,6 @@ describe('odata.batch', () => { }] }); assertSuccess(res); - result.id = (+books[books.length - 1].id + 1).toString(); res.body.should.deepEqual({ responses: [{ id: "1", @@ -119,7 +133,10 @@ describe('odata.batch', () => { 'OData-Version': '4.0', 'content-type': 'application/json' }, - body: result + body: { + id: "AFFE", + ...result + } }] }); }); @@ -129,6 +146,18 @@ describe('odata.batch', () => { id: "1", title: "War and peace" }; + const server = odata(); + + server.entity('book', { + put: (req, res, next) => { + req.$odata.body.should.deepEqual(result); + req.$odata.$Key.id.should.be.equal(result.id); + res.$odata.result = result; + next(); + } + }, BookMetadata); + httpServer = server.listen(port); + const res = await request(host).post(`/$batch`).send({ requests: [{ id: "1", @@ -158,6 +187,18 @@ describe('odata.batch', () => { id: "1", title: "War and peace" }; + const server = odata(); + + server.entity('book', { + patch: (req, res, next) => { + req.$odata.body.should.deepEqual(result); + req.$odata.$Key.id.should.be.equal(result.id); + res.$odata.result = result; + next(); + } + }, BookMetadata); + httpServer = server.listen(port); + const res = await request(host).post(`/$batch`).send({ requests: [{ id: "1", @@ -183,6 +224,17 @@ describe('odata.batch', () => { it('should work with delete entity', async function () { + const server = odata(); + + server.entity('book', { + delete: (req, res, next) => { + req.$odata.$Key.id.should.be.equal('1'); + res.$odata.status = 204; + next(); + } + }, BookMetadata); + httpServer = server.listen(port); + const res = await request(host).post(`/$batch`).send({ requests: [{ id: "1", @@ -201,6 +253,14 @@ describe('odata.batch', () => { }); it('should work with unbound action', async function () { + const server = odata(); + + server.action('unbound-action', (req, res, next) => { + res.$odata.result = { result: 'Hello! I am an unbound action.' }; + next(); + }); + httpServer = server.listen(port); + const res = await request(host).post(`/$batch`).send({ requests: [{ id: "1", @@ -226,11 +286,22 @@ describe('odata.batch', () => { }); it('should work with action, that bound to entity', async function () { + const server = odata(); + const resource = server.entity('book', { + }, BookMetadata); + resource.action('entity-action', (req, res, next) => { + req.$odata.$Key.id.should.be.equal(books[0].id); + res.$odata.result = { result: 'Hello! I am an action, that bound to entity.' }; + next(); + }, { binding: 'entity' }); + + httpServer = server.listen(port); + const res = await request(host).post(`/$batch`).send({ requests: [{ id: "1", method: "post", - url: `/book(${books[0].id})/entity-action` + url: `/book('${books[0].id}')/entity-action` }] }); assertSuccess(res); @@ -249,41 +320,41 @@ describe('odata.batch', () => { }] }); }); -/* - it('should work with multipart request body', async function () { - const result = { - title: "War and peace" - }; - const res = await request(host) - .post(`/$batch`) - .send({}) - .set('Content-Type', 'multipart/mixed; boundary=batch_1') - .set('Host', host) - .serialize(() => ` ---batch_1 -Content-Type: application/http - -POST /book -Host: ${host} -Content-Type: application/json -Content-Length: ${JSON.stringify(result).length} - -${JSON.stringify(result)} ---batch_1-- + /* + it('should work with multipart request body', async function () { + const result = { + title: "War and peace" + }; + const res = await request(host) + .post(`/$batch`) + .send({}) + .set('Content-Type', 'multipart/mixed; boundary=batch_1') + .set('Host', host) + .serialize(() => ` + --batch_1 + Content-Type: application/http + + POST /book + Host: ${host} + Content-Type: application/json + Content-Length: ${JSON.stringify(result).length} + + ${JSON.stringify(result)} + --batch_1-- + `); + + assertSuccess(res); + + res.text.should.equal(` + --batch-1 + Content-Type: application/http + + HTTP/1.1 200 Ok + Content-Type: application/json + Content-Length: ${JSON.stringify(result).length} + + ${JSON.stringify(result)} + --batch-1— `); - - assertSuccess(res); - - res.text.should.equal(` ---batch-1 -Content-Type: application/http - -HTTP/1.1 200 Ok -Content-Type: application/json -Content-Length: ${JSON.stringify(result).length} - -${JSON.stringify(result)} ---batch-1— - `); - });*/ + });*/ }); diff --git a/test/mocked/odata.entity.js b/test/odata.entity.js similarity index 90% rename from test/mocked/odata.entity.js rename to test/odata.entity.js index 0b132e7..2fca922 100644 --- a/test/mocked/odata.entity.js +++ b/test/odata.entity.js @@ -1,14 +1,12 @@ import 'should'; import request from 'supertest'; -import { odata, host, port } from '../support/setup'; -import FakeDb from '../support/fake-db'; +import { odata, host, port } from './support/setup'; describe('odata.entity', () => { - let httpServer, server, db; + let httpServer, server; beforeEach(async function () { - db = new FakeDb(); - server = odata(db); + server = odata(); }); afterEach(() => { @@ -30,7 +28,9 @@ describe('odata.entity', () => { }]; server.entity('book', { list: (req, res, next) => { - res.$odata.result = result; + res.$odata.result = { + value: result + }; next(); } }, { @@ -56,7 +56,9 @@ describe('odata.entity', () => { res.res.statusMessage.should.be.equal(''); } - res.body.should.deepEqual(result); + res.body.should.deepEqual({ + value: result + }); }); it('should return 501 for not implemented methods', async function () { diff --git a/test/mocked/odata.error.js b/test/odata.error.js similarity index 89% rename from test/mocked/odata.error.js rename to test/odata.error.js index f856ac9..b8e5fdf 100644 --- a/test/mocked/odata.error.js +++ b/test/odata.error.js @@ -1,15 +1,12 @@ import 'should'; import request from 'supertest'; -import { odata, host, port, bookSchema } from '../support/setup'; -import data from '../support/books.json'; -import FakeDb from '../support/fake-db'; +import { odata, host, port } from './support/setup'; describe('odata.actions', () => { - let httpServer, server, db; + let httpServer, server; beforeEach(async function () { - db = new FakeDb(); - server = odata(db); + server = odata(); }); afterEach(() => { diff --git a/test/odata.filter.js b/test/odata.filter.js new file mode 100644 index 0000000..5a238a9 --- /dev/null +++ b/test/odata.filter.js @@ -0,0 +1,105 @@ +import 'should'; +import request from 'supertest'; +import { odata, host, port, assertSuccess } from './support/setup'; +import { BookMetadata } from './support/books.model'; + +describe('odata.filter', () => { + let httpServer, server; + + beforeEach(async function () { + server = odata(); + }); + + afterEach(() => { + if (httpServer) { + httpServer.close(); + } + }); + + it('should work with single string equals', async function () { + server.entity('book', { + list: (req, res, next) => { + req.$odata.$filter.should.deepEqual({ title: { $eq: 'Midnight Rain' } }); + res.$odata.result = { value: [] }; + next(); + } + }, BookMetadata); + httpServer = server.listen(port); + + const res = await request(host).get(`/book?$filter=title eq 'Midnight Rain'`); + + assertSuccess(res); + + }); + + it('should work with single $lt operator', async function () { + server.entity('book', { + list: (req, res, next) => { + req.$odata.$filter.should.deepEqual({ price: { $lt: 5.95 } }); + res.$odata.result = { value: [] }; + next(); + } + }, BookMetadata); + httpServer = server.listen(port); + + const res = await request(host).get(`/book?$filter=price lt 5.95`); + + assertSuccess(res); + + }); + + it('should work with single $ge operator', async function () { + server.entity('book', { + list: (req, res, next) => { + req.$odata.$filter.should.deepEqual({ price: { $gte: 5.95 } }); + res.$odata.result = { value: [] }; + next(); + } + }, BookMetadata); + httpServer = server.listen(port); + + const res = await request(host).get(`/book?$filter=price ge 5.95`); + + assertSuccess(res); + + }); + + it('should work with single $le operator', async function () { + server.entity('book', { + list: (req, res, next) => { + req.$odata.$filter.should.deepEqual({ price: { $lte: 5.95 } }); + res.$odata.result = { value: [] }; + next(); + } + }, BookMetadata); + httpServer = server.listen(port); + + const res = await request(host).get(`/book?$filter=price le 5.95`); + + assertSuccess(res); + + }); + + it(`should work with 'and' and two conditions on same property`, async function () { + server.entity('book', { + list: (req, res, next) => { + req.$odata.$filter.should.deepEqual({ + price: { + $lte: 5.95, + $gte: 4 + } + }); + res.$odata.result = { value: [] }; + next(); + } + }, BookMetadata); + httpServer = server.listen(port); + + const res = await request(host).get(`/book?$filter=price le 5.95 and price ge 4`); + + assertSuccess(res); + + }); + + //TODO: Weitere Beispiele https://masteringjs.io/tutorials/mongoose/find +}); diff --git a/test/mocked/odata.functions.js b/test/odata.functions.js similarity index 80% rename from test/mocked/odata.functions.js rename to test/odata.functions.js index 9056281..bdb4a9f 100644 --- a/test/mocked/odata.functions.js +++ b/test/odata.functions.js @@ -1,7 +1,7 @@ import 'should'; import request from 'supertest'; -import { odata, host, port, assertSuccess } from '../support/setup'; -import FakeDb from '../support/fake-db'; +import { odata, host, port, assertSuccess } from './support/setup'; +import FakeDb from './support/fake-db'; describe('odata.functions', () => { ['get', 'post', 'put', 'delete'].map((method) => { @@ -9,7 +9,7 @@ describe('odata.functions', () => { let httpServer; before(() => { - const server = odata(new FakeDb()); + const server = odata(); server.function('test', (req, res, next) => res.jsonp({ test: 'ok' }), { method }); diff --git a/test/support/books.json b/test/support/books.json index b8c35b5..7635cbc 100644 --- a/test/support/books.json +++ b/test/support/books.json @@ -1,5 +1,6 @@ [ { + "id": "1", "author": "Gambardella, Matthew", "description": "An in-depth look at creating applications \n with XML.", "genre": "Computer", @@ -8,6 +9,7 @@ "title": "XML Developer's Guide" }, { + "id": "2", "author": "Ralls, Kim", "description": "A former architect battles corporate zombies, \n an evil sorceress, and her own childhood to become queen \n of the world.", "genre": "Fantasy", @@ -16,6 +18,7 @@ "title": "Midnight Rain" }, { + "id": "3", "author": "Corets, Eva", "description": "After the collapse of a nanotechnology \n society in England, the young survivors lay the \n foundation for a new society.", "genre": "Fantasy", @@ -24,6 +27,7 @@ "title": "Maeve Ascendant" }, { + "id": "4", "author": "Corets, Eva", "description": "In post-apocalypse England, the mysterious \n agent known only as Oberon helps to create a new life \n for the inhabitants of London. Sequel to Maeve \n Ascendant.", "genre": "Fantasy", @@ -32,6 +36,7 @@ "title": "Oberon's Legacy" }, { + "id": "5", "author": "Corets, Eva", "description": "The two daughters of Maeve, half-sisters, \n battle one another for control of England. Sequel to \n Oberon's Legacy.", "genre": "Fantasy", @@ -40,6 +45,7 @@ "title": "The Sundered Grail" }, { + "id": "6", "author": "Randall, Cynthia", "description": "When Carla meets Paul at an ornithology \n conference, tempers fly as feathers get ruffled.", "genre": "Romance", @@ -48,6 +54,7 @@ "title": "Lover Birds" }, { + "id": "7", "author": "Thurman, Paula", "description": "A deep sea diver finds true love twenty \n thousand leagues beneath the sea.", "genre": "Romance", @@ -56,6 +63,7 @@ "title": "Splish Splash" }, { + "id": "8", "author": "Knorr, Stefan", "description": "An anthology of horror stories about roaches,\n centipedes, scorpions and other insects.", "genre": "Horror", @@ -64,6 +72,7 @@ "title": "Creepy Crawlies" }, { + "id": "9", "author": "Kress, Peter", "description": "After an inadvertant trip through a Heisenberg\n Uncertainty Device, James Salway discovers the problems \n of being quantum.", "genre": "Science Fiction", @@ -72,6 +81,7 @@ "title": "Paradox Lost" }, { + "id": "A", "author": "O'Brien, Tim", "description": "Microsoft's .NET initiative is explored in \n detail in this deep programmer's reference.", "genre": "Computer", @@ -80,6 +90,7 @@ "title": "Microsoft .NET: The Programming Bible" }, { + "id": "B", "author": "O'Brien, Tim", "description": "The Microsoft MSXML3 parser is covered in \n detail, with attention to XML DOM interfaces, XSLT processing, \n SAX and more.", "genre": "Computer", @@ -88,6 +99,7 @@ "title": "MSXML3: A Comprehensive Guide" }, { + "id": "C", "author": "Galos, Mike", "description": "Microsoft Visual Studio 7 is explored in depth,\n looking at how Visual Basic, Visual C++, C#, and ASP+ are \n integrated into a comprehensive development \n environment.", "genre": "Computer", @@ -96,6 +108,7 @@ "title": "Visual Studio 7: A Comprehensive Guide" }, { + "id": "D", "author": "史蒂夫·迈克康奈尔", "description": "第2版的《代码大全》是著名IT畅销书作者史蒂夫·迈克康奈尔11年前的经典著作的全新演绎:第2版不是第一版的简单修订增补,而是完全进行了重写;增加了很多与时俱进的内容。这也是一本完整的软件构建手册,涵盖了软件构建过程中的所有细节。它从软件质量和编程思想等方面论述了软件构建的各个问题,并详细论述了紧跟潮流的新技术、高屋建瓴的观点、通用的概念,还含有丰富而典型的程序示例。这本书中所论述的技术不仅填补了初级与高级编程技术之间的空白,而且也为程序员们提供了一个有关编程技巧的信息来源。这本书对经验丰富的程序员、技术带头人、自学的程序员及几乎不懂太多编程技巧的学生们都是大有裨益的。可以说,无论是什么背景的读者,阅读这本书都有助于在更短的时间内、更容易地写出更好的程序。", "genre": "计算机", diff --git a/test/support/books.model.js b/test/support/books.model.js new file mode 100644 index 0000000..a775d48 --- /dev/null +++ b/test/support/books.model.js @@ -0,0 +1,51 @@ +import mongoose from 'mongoose'; + +const Schema = mongoose.Schema; + +export const BookSchema = new Schema({ + author: String, + description: String, + genre: String, + price: Number, + publish_date: Date, + title: String +}, + { + timestamps: true, + toObject: { + virtuals: true, + }, + toJSON: { + virtuals: true, + }, + }); + +export const BookModel = mongoose.model('Book', BookSchema); + +export const BookMetadata = { + $Key: ['id'], + id: { + $Type: 'node.odata.ObjectId' + }, + author: { + $Type: 'Edm.String' + }, + genre: { + $Type: 'Edm.String' + }, + price: { + $Type: 'Edm.Double' + }, + publish_date: { + $Type: 'Edm.DateTimeOffset' + }, + title: { + $Type: 'Edm.String' + }, + createdAt: { + $Type: 'Edm.DateTimeOffset' + }, + updatedAt: { + $Type: 'Edm.DateTimeOffset' + } +}; \ No newline at end of file diff --git a/test/support/db.js b/test/support/db.js new file mode 100644 index 0000000..af01bc2 --- /dev/null +++ b/test/support/db.js @@ -0,0 +1,23 @@ + +import mongoose from 'mongoose'; +import { conn } from '../support/setup'; + +export function init(server) { + server.addBefore((req, res, next) => { + req.$odata = { + mongo: mongoose.default.connection + }; + next(); + }); +} + +export function connect(server) { + mongoose.connect(process.env.DATABASE || conn, null, (err) => { + if (err) { + console.error(err.message); + console.error('Failed to connect to database on startup.'); + process.exit(); + } + }); + init(server); +} \ No newline at end of file diff --git a/test/support/setup.js b/test/support/setup.js index 92e5ec8..c1fa235 100644 --- a/test/support/setup.js +++ b/test/support/setup.js @@ -6,14 +6,9 @@ export const host = 'http://localhost:3000'; export const port = '3000'; export const conn = 'mongodb://localhost/odata-test'; -export const bookSchema = { - author: String, - description: String, - genre: String, - price: Number, - publish_date: Date, - title: String -}; +const { BookShema, BookModel } = require('./books.model'); + +export const model = BookModel; export const books = require('./books.json'); From 7f7e9185c228f4a1c85f285ecf2f6bc093d2b9c4 Mon Sep 17 00:00:00 2001 From: Richard Martens Date: Sat, 29 Jul 2023 21:36:39 +0200 Subject: [PATCH 33/64] [b] Splitting db and odata --- .vscode/launch.json | 2 +- src/mongo/parser/functionsParser.js | 4 +- src/mongo/parser/skipParser.js | 5 - src/mongo/parser/topParser.js | 4 - src/mongo/rest/delete.js | 2 +- src/mongo/rest/get.js | 2 +- src/mongo/rest/list.js | 9 +- src/odata/Metadata.js | 10 + src/odata/entity/Entity.js | 44 +++- src/odata/entity/parser/filter.js | 32 +-- src/odata/entity/parser/keys.js | 9 +- src/odata/entity/parser/orderby.js | 23 +++ src/odata/entity/parser/property.js | 10 + src/odata/entity/parser/select.js | 14 +- src/odata/entity/parser/skiptop.js | 34 ++++ src/odata/entity/validators/property.js | 20 ++ src/server.js | 29 ++- test/mocked/odata.query.orderby.js | 72 ------- test/mocked/odata.query.select.js | 83 -------- test/mocked/odata.query.skip.js | 50 ----- test/mocked/odata.query.top.js | 44 ---- test/mocked/options.maxSkip.js | 63 ------ test/mocked/options.maxTop.js | 62 ------ test/mocked/rest.delete.js | 41 ---- test/mocked/rest.get.js | 54 ----- test/mongo/mocked/model.complex.filter.js | 6 +- test/mongo/mocked/model.custom.id.js | 5 +- .../mocked/odata.query.filter.functions.js | 20 +- test/mongo/mocked/odata.query.filter.js | 165 +++++++-------- test/mongo/mocked/odata.query.orderby.js | 130 ++++++++++++ test/mongo/mocked/odata.query.select.js | 144 +++++++++++++ test/mongo/mocked/odata.query.skip.js | 81 ++++++++ test/mongo/mocked/odata.query.top.js | 67 ++++++ test/mongo/mocked/rest.delete.js | 50 +++++ test/mongo/mocked/rest.get.js | 94 +++++++++ test/odata.filter.js | 191 +++++++++++++++++- test/odata.functions.js | 1 - test/options.maxSkip.js | 99 +++++++++ test/options.maxTop.js | 99 +++++++++ test/{mocked => }/options.prefix.js | 38 ++-- test/support/db.js | 1 + 41 files changed, 1244 insertions(+), 669 deletions(-) create mode 100644 src/odata/entity/parser/orderby.js create mode 100644 src/odata/entity/parser/property.js create mode 100644 src/odata/entity/parser/skiptop.js create mode 100644 src/odata/entity/validators/property.js delete mode 100644 test/mocked/odata.query.orderby.js delete mode 100644 test/mocked/odata.query.select.js delete mode 100644 test/mocked/odata.query.skip.js delete mode 100644 test/mocked/odata.query.top.js delete mode 100644 test/mocked/options.maxSkip.js delete mode 100644 test/mocked/options.maxTop.js delete mode 100644 test/mocked/rest.delete.js delete mode 100644 test/mocked/rest.get.js create mode 100644 test/mongo/mocked/odata.query.orderby.js create mode 100644 test/mongo/mocked/odata.query.select.js create mode 100644 test/mongo/mocked/odata.query.skip.js create mode 100644 test/mongo/mocked/odata.query.top.js create mode 100644 test/mongo/mocked/rest.delete.js create mode 100644 test/mongo/mocked/rest.get.js create mode 100644 test/options.maxSkip.js create mode 100644 test/options.maxTop.js rename test/{mocked => }/options.prefix.js (56%) diff --git a/.vscode/launch.json b/.vscode/launch.json index 6dc9b1b..cd2ae43 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -20,7 +20,7 @@ "dot", "--timeout", "300000", - "test/odata.filter.js" + "test/mongo/mocked/rest.delete.js" ], "env": { "LOG_LEVEL": "debug" diff --git a/src/mongo/parser/functionsParser.js b/src/mongo/parser/functionsParser.js index eaf6bb9..2c9710a 100644 --- a/src/mongo/parser/functionsParser.js +++ b/src/mongo/parser/functionsParser.js @@ -27,7 +27,7 @@ const convertToOperator = (odataOperator) => { // contains(CompanyName,'icrosoft') const contains = (name, $filter) => ({ - $where: `this.${name}.indexOf(${$filter.$parameter}) != -1` + $where: `this.${name}.indexOf('${$filter.$parameter}') != -1` }); // indexof(CompanyName,'X') eq 1 @@ -36,7 +36,7 @@ const indexof = (name, $filter) => { const operator = convertToOperator($operator); return { - $where: `this.${name}.indexOf(${$parameter}) ${operator} ${$value}` + $where: `this.${name}.indexOf('${$parameter}') ${operator} ${$value}` }; }; diff --git a/src/mongo/parser/skipParser.js b/src/mongo/parser/skipParser.js index 9032468..d2ac25a 100644 --- a/src/mongo/parser/skipParser.js +++ b/src/mongo/parser/skipParser.js @@ -2,11 +2,6 @@ // -> // query.skip(10) export default (query, skip) => new Promise((resolve) => { - if (Number.isNaN(+skip)) { - resolve(); - return; - } - if (skip > 0) { query.skip(skip); } diff --git a/src/mongo/parser/topParser.js b/src/mongo/parser/topParser.js index 6ee5d0a..01edcd3 100644 --- a/src/mongo/parser/topParser.js +++ b/src/mongo/parser/topParser.js @@ -2,10 +2,6 @@ // -> // query.top(10) export default (query, top) => new Promise((resolve) => { - if (Number.isNaN(+top)) { - resolve(); - return; - } if (top > 0) { query.limit(top); } diff --git a/src/mongo/rest/delete.js b/src/mongo/rest/delete.js index c55f40e..5503bee 100644 --- a/src/mongo/rest/delete.js +++ b/src/mongo/rest/delete.js @@ -1,5 +1,5 @@ export default (req, res, next) => { - req.$odata.Model.remove({ _id: req.params.id }, (err, result) => { + req.$odata.Model.remove({ _id: req.$odata.$Key._id }, (err, result) => { if (err) { return next(err); } diff --git a/src/mongo/rest/get.js b/src/mongo/rest/get.js index 13076b2..fe9c76e 100644 --- a/src/mongo/rest/get.js +++ b/src/mongo/rest/get.js @@ -1,5 +1,5 @@ export default (req, res, next) => { - req.$odata.Model.findById(req.params.id, (err, entity) => { + req.$odata.Model.findById(req.$odata.$Key._id, (err, entity) => { if (err) { return next(err); } diff --git a/src/mongo/rest/list.js b/src/mongo/rest/list.js index 4403f59..6a56618 100644 --- a/src/mongo/rest/list.js +++ b/src/mongo/rest/list.js @@ -1,5 +1,4 @@ import filterParser from '../parser/filterParser'; -import orderbyParser from '../parser/orderbyParser'; import skipParser from '../parser/skipParser'; import topParser from '../parser/topParser'; import selectParser from '../parser/selectParser'; @@ -9,8 +8,12 @@ function _dataQuery(model, { }) { return new Promise((resolve, reject) => { const query = model.find(filterParser(filter)); - orderbyParser(query, orderby) - .then(() => skipParser(query, skip)) + + if (orderby) { + query.sort(orderby); + } + + skipParser(query, skip) .then(() => topParser(query, top)) .then(() => selectParser(query, select)) .then(() => query.exec((err, data) => { diff --git a/src/odata/Metadata.js b/src/odata/Metadata.js index cd4bc47..123176e 100644 --- a/src/odata/Metadata.js +++ b/src/odata/Metadata.js @@ -56,6 +56,16 @@ export default class Metadata { throw new Error(`Complex type with name ${name} allready exists`); } + if (!properties) { // get call + const returnType = name.replace('node.odata.', ''); + + if (!this.complexTypes[returnType]) { + throw new Error(`Complex type with name ${name} does not exists`); + } + + return this.complexTypes[returnType]; + } + validateIdentifier(name); const typeObject = { diff --git a/src/odata/entity/Entity.js b/src/odata/entity/Entity.js index 16c6bd6..5e829e4 100644 --- a/src/odata/entity/Entity.js +++ b/src/odata/entity/Entity.js @@ -1,12 +1,13 @@ import { validateIdentifier, validate } from "../validator"; import { Router } from 'express'; import Hooks from "../Hooks"; -import { min } from '../../utils'; import Action from '../Action'; import parseSelect from './parser/select'; import parseKeys from './parser/keys'; import parseCount from './parser/count'; import parseFilter from './parser/filter'; +import parseOrderBy from "./parser/orderby"; +import { parseSkip, parseTop } from "./parser/skiptop"; export default class Entity { constructor(name, handler, metadata, settings, mapping) { @@ -102,22 +103,47 @@ export default class Entity { next(); } + get(key) { + if (value) { + if (Number.isNaN(+value) || +value < 0) { + throw new Error(`Max-Skip value should be a positive number`); + } + this.options.maxSkip = value; + } + + return this.options[key]; + } + + set(key, value) { + const positiveOnly = value => { + if (value && (Number.isNaN(+value) || +value < 0)) { + throw new Error(`'${key}' value should be a positive number`); + } + }; + + switch (key) { + case 'maxSkip': + case 'maxTop': + positiveOnly(value); + this.options[key] = value; + break; + } + + } parsingMiddleware(req, res, next) { try { - req.$odata = req.$odata || {}; - - req.$odata.$Key = parseKeys(req, this.name, this.metadata); - req.$odata.$select = parseSelect(req, this.name, this.metadata); - req.$odata.$filter = parseFilter(req, this.name, this.metadata); + req.$odata.$Key = parseKeys(req, this.name, this.metadata, this.mapping); + req.$odata.$select = parseSelect(req, this.name, this.metadata, this.mapping); + req.$odata.$filter = parseFilter(req, this.name, this.metadata, this.mapping); req.$odata.$count = parseCount(req, this.name, this.metadata); + req.$odata.$orderby = parseOrderBy(req, this.name, this.metadata, this.mapping, this.options.orderby); + req.$odata.$skip = parseSkip(req, this.options.maxSkip); + req.$odata.$top = parseTop(req, this.options.maxTop); req.$odata = { ...req.$odata, body: req.body, - $top: req.query.$top && min([req.query.$top, this.options.maxTop]), - $skip: req.query.$skip && min([req.query.$skip, this.options.maxSkip]), - $orderby: this.options.orderby || req.query.$orderby, $expand: req.query.$expand, // TODO : implement expand $search: req.query.$search // TODO : implement search }; diff --git a/src/odata/entity/parser/filter.js b/src/odata/entity/parser/filter.js index 2dbb3b3..8305024 100644 --- a/src/odata/entity/parser/filter.js +++ b/src/odata/entity/parser/filter.js @@ -1,6 +1,10 @@ import parseValue from './value'; +import parseProperty from './property'; +import validateProperty from '../validators/property'; + +export default function (req, entity, metadata, mapping) { + const funcRegex = /(contains|indexof|year)\s*\(\s*([^,]+)\s*[,]?\s*([^)]*)\)\s*(eq|ne|gt|ge|lt|le)?\s*([0-9]*)/i; -export default function (req, entity, metadata) { const replaceString = (filter, dictionary) => { const replacer = (match, p1) => { const result = `$${dictionary.length}`; @@ -70,7 +74,7 @@ export default function (req, entity, metadata) { const splitCondition = (filter, dictionary) => { const operatorIndex = filter.search(/\s+(eq|ne|gt|ge|lt|le)\s+/i); - if (operatorIndex === -1) { + if (filter.match(funcRegex)) { return visitor('parseFunction', filter, dictionary); } @@ -105,20 +109,12 @@ export default function (req, entity, metadata) { throw new Error(`Unexpected operator '${operatorPrettified}' in '${$filter}'`); } - const property = operands[0].trim(); - - if (!metadata[property]) { - const err = new Error(`Entity '${entity}' has not a property named '${property}'`); - - err.status = 400; - throw err; - } - + const property = parseProperty(operands[0], mapping); const value = operands[2].trim(); return { [property]: { - [operator]: parseValue(dictionary[value] || value, metadata[property]) + [operator]: parseValue(dictionary[value] || value, validateProperty(operands[0].trim(), req, entity, metadata)) } }; }; @@ -127,7 +123,7 @@ export default function (req, entity, metadata) { // contains(CompanyName,'freds') // indexof(CompanyName,'lfreds') eq 1 // year(BirthDate) eq 0 - const match = filter.match(/(contains|indexof|year)\s*\(\s*([^,]+)\s*[,]?\s*([^)]*)\)\s*(eq|ne|gt|ge|lt|le)?\s*([0-9]*)/i); + const match = filter.match(funcRegex); if (!match) { const err = new Error(`Text '${filter}' can not be interpreted`); @@ -137,15 +133,7 @@ export default function (req, entity, metadata) { } const func = match[1].toLowerCase(); - const property = match[2]; - - if (!metadata[property]) { - const err = new Error(`Entity '${entity}' has no property named '${property}'`); - - err.status = 400; - throw err; - } - + const property = parseProperty(match[2], mapping); const parameter = match[3] && dictionary[match[3]] ? dictionary[match[3]] : match[3]; // 0 indexof(CompanyName,$0) eq 10 diff --git a/src/odata/entity/parser/keys.js b/src/odata/entity/parser/keys.js index e38f6e7..7667f5f 100644 --- a/src/odata/entity/parser/keys.js +++ b/src/odata/entity/parser/keys.js @@ -1,6 +1,8 @@ import parseValue from './value'; +import parseProperty from './property'; +import validateProperty from '../validators/property'; -export default function(req, entity, metadata) { +export default function(req, entity, metadata, mapping) { const result = {}; if (req.params) { @@ -9,7 +11,10 @@ export default function(req, entity, metadata) { if (params) { params.forEach(param => { - result[param] = parseValue(req.params[param], metadata[param]); + const property = parseProperty(param, mapping); + const propertyMetadata = validateProperty(param, req, entity, metadata); + + result[property] = parseValue(req.params[param], propertyMetadata); }); } } diff --git a/src/odata/entity/parser/orderby.js b/src/odata/entity/parser/orderby.js new file mode 100644 index 0000000..45bfaec --- /dev/null +++ b/src/odata/entity/parser/orderby.js @@ -0,0 +1,23 @@ +import parseProperty from "./property"; +import validateProperty from "../validators/property"; + +export default function parseOrderBy(req, entity, metadata, mapping, options) { + const orderby = options || req.query.$orderby; + + if (orderby) { + const found = orderby.match(/([^ ,]+)\s*(asc|desc)?/gi); + + if (!found) { + throw new Error(`Orderby value '${orderby}' can not be parsed`); + } + + return [...found.map(singleOrder => { + const operands = singleOrder.match(/([^ ,]+)\s*(asc|desc)?/i); + const property = parseProperty(operands[1], req, entity, metadata, mapping); + + validateProperty(operands[1], req, entity, metadata); + + return [property, operands[2]?.trim().toLowerCase() || 'asc'] + })]; + } +} \ No newline at end of file diff --git a/src/odata/entity/parser/property.js b/src/odata/entity/parser/property.js new file mode 100644 index 0000000..6d6ff73 --- /dev/null +++ b/src/odata/entity/parser/property.js @@ -0,0 +1,10 @@ +export default function parseProperty(filter, mapping) { + let property = filter?.trim(); + + if (mapping[property]) { + property = mapping[property].target; + + } + + return property; +} \ No newline at end of file diff --git a/src/odata/entity/parser/select.js b/src/odata/entity/parser/select.js index 4f3c89a..1ed7e09 100644 --- a/src/odata/entity/parser/select.js +++ b/src/odata/entity/parser/select.js @@ -1,13 +1,11 @@ -export default function(req, entity, metadata) { - return req.query.$select?.split(',').map((item) => { - const property = item.trim(); +import parseProperty from "./property"; +import validateProperty from "../validators/property"; - if (!metadata[property]) { - const err = new Error(`Entity '${entity}' have not a property with name '${property}'`); +export default function(req, entity, metadata, mapping) { + return req.query.$select?.split(',').map((item) => { + const property = parseProperty(item.trim(), mapping); - err.status = 400; - throw err; - } + validateProperty(item.trim(), req, entity, metadata); return property; }); diff --git a/src/odata/entity/parser/skiptop.js b/src/odata/entity/parser/skiptop.js new file mode 100644 index 0000000..1f75602 --- /dev/null +++ b/src/odata/entity/parser/skiptop.js @@ -0,0 +1,34 @@ +import { min } from '../../../utils'; + +function parse(value, options) { + const input = value; + + if (!input) { + return; + } + + const result = +input; + if (Number.isNaN(result)) { + const err = new Error(`Value '${input}' should be a number`); + + err.status = 400; + throw err; + } + + if (result < 0) { + const err = new Error(`Value '${result}' should be a positive number`); + + err.status = 400; + throw err; + } + + return min([result, options]); +} + +export function parseSkip(req, options) { + return parse(req.query.$skip, options); +} + +export function parseTop(req, options) { + return parse(req.query.$top, options); +} \ No newline at end of file diff --git a/src/odata/entity/validators/property.js b/src/odata/entity/validators/property.js new file mode 100644 index 0000000..d2d9182 --- /dev/null +++ b/src/odata/entity/validators/property.js @@ -0,0 +1,20 @@ +export default function validateProperty(name, req, entity, currentMetadata) { + const property = name.toString(); + + if (currentMetadata[property]) { + return currentMetadata[property]; + } + + const indexDot = property.indexOf('.'); + if ( indexDot > 0) { + const nextProperty = property.substr(indexDot + 1); + const complexType = property.substr(0, indexDot); + + return validateProperty(nextProperty, req, entity, req.$odata.$metadata.complexType(currentMetadata[complexType].$Type)); + } + + const err = new Error(`Entity '${entity}' has no property named '${property}'`); + + err.status = 400; + throw err; +} \ No newline at end of file diff --git a/src/server.js b/src/server.js index 9cff5c0..a1bb7ab 100644 --- a/src/server.js +++ b/src/server.js @@ -26,14 +26,6 @@ class Server { this.hooks = new Hooks(); - this.hooks.addBefore(async (req, res) => { - res.$odata = { - status: 404, - supportedMimetypes: ['application/json'] - } - }, 'service-initialization'); - this.hooks.addAfter(writer, 'writer', true); - // TODO: Infact, resources is a mongooseModel instance, origin name is repositories. // Should mix _resources object and resources object: _resources + resource = resources. // Encapsulation to a object, separate mognoose, try to use *repository pattern*. @@ -44,6 +36,19 @@ class Server { }; //unbound actions this.actions = {}; + + + this.hooks.addBefore(async (req, res) => { + req.$odata = { + $metadata: this.resources.$metadata + }; + res.$odata = { + status: 404, + supportedMimetypes: ['application/json'] + } + }, 'service-initialization'); + this.hooks.addAfter(writer, 'writer', true); + this._serviceDocument = new ServiceDocument(this); } @@ -179,6 +184,14 @@ class Server { } default: { this._settings[key] = val; + if (this.resources) { + Object.keys(this.resources) + .forEach(name => { + if (this.resources[name].set) { + this.resources[name].set(key, val); + } + }); + } break; } } diff --git a/test/mocked/odata.query.orderby.js b/test/mocked/odata.query.orderby.js deleted file mode 100644 index 73d9fb4..0000000 --- a/test/mocked/odata.query.orderby.js +++ /dev/null @@ -1,72 +0,0 @@ -import 'should'; -import sinon from 'sinon'; -import request from 'supertest'; -import { odata, host, port, bookSchema } from '../support/setup'; -import data from '../support/books.json'; -import FakeDb from '../support/fake-db'; - -describe('odata.query.orderby', () => { - let httpServer, mock, resource; - - before(async function() { - const db = new FakeDb(); - const server = odata(db); - resource = server.resource('book', bookSchema) - httpServer = server.listen(port); - db.addData('book', data); - }); - - after(() => { - httpServer.close(); - }); - - afterEach(() => { - mock.restore(); - }); - - it('should default let items order with asc', async function() { - mock = sinon.mock(resource.model); - mock.expects('sort').once().withArgs({ - price: 'asc' - }).returns(resource.model); - await request(host).get('/book?$orderby=price'); - mock.verify(); - }); - - it('should let items order asc', async function() { - mock = sinon.mock(resource.model); - mock.expects('sort').once().withArgs({ - price: 'asc' - }).returns(resource.model); - await request(host).get('/book?$orderby=price asc'); - mock.verify(); - }); - - it('should let items order desc', async function() { - mock = sinon.mock(resource.model); - mock.expects('sort').once().withArgs({ - price: 'desc' - }).returns(resource.model); - await request(host).get('/book?$orderby=price desc'); - mock.verify(); - }); - - it('should let items order when use multiple fields', async function() { - mock = sinon.mock(resource.model); - mock.expects('sort').once().withArgs({ - price: 'asc', - title: 'asc' - }).returns(resource.model); - await request(host).get('/book?$orderby=price,title'); - mock.verify(); - }); - - it("should be ignore when order by not exist field", async function() { - mock = sinon.mock(resource.model); - mock.expects('sort').once().withArgs({ - 'not-exist-field': 'asc' - }).returns(resource.model); - await request(host).get('/book?$orderby=not-exist-field'); - mock.verify(); - }); -}); diff --git a/test/mocked/odata.query.select.js b/test/mocked/odata.query.select.js deleted file mode 100644 index 377e020..0000000 --- a/test/mocked/odata.query.select.js +++ /dev/null @@ -1,83 +0,0 @@ -import 'should'; -import sinon from 'sinon'; -import request from 'supertest'; -import { odata, host, port, bookSchema } from '../support/setup'; -import data from '../support/books.json'; -import FakeDb from '../support/fake-db'; - -describe('odata.query.select', () => { - let httpServer, mock, resource; - - before(async function() { - const db = new FakeDb(); - const server = odata(db); - resource = server.resource('book', bookSchema); - resource.model.model.schema.tree = { - id: { - select: true - }, - price: { - select: true - }, - title: { - select: true - } - } - httpServer = server.listen(port); - db.addData('book', data); - }); - - after(() => { - httpServer.close(); - }); - - afterEach(() => { - mock.restore(); - }); - - it('should select anyone field', async function() { - mock = sinon.mock(resource.model); - mock.expects('select').once().withArgs({ - _id: 0, - price: 1 - }).returns(resource.model); - await request(host).get('/book?$select=price'); - mock.verify(); - }); - it('should select multiple field', async function() { - mock = sinon.mock(resource.model); - mock.expects('select').once().withArgs({ - _id: 0, - price: 1, - title: 1 - }).returns(resource.model); - await request(host).get('/book?$select=price,title'); - mock.verify(); - }); - it('should select multiple field with blank space', async function() { - mock = sinon.mock(resource.model); - mock.expects('select').once().withArgs({ - _id: 0, - price: 1, - title: 1 - }).returns(resource.model); - await request(host).get('/book?$select=price, title'); - mock.verify(); - }); - it('should select id field', async function() { - mock = sinon.mock(resource.model); - mock.expects('select').once().withArgs({ - _id: 1, - price: 1, - title: 1 - }).returns(resource.model); - await request(host).get('/book?$select=price,title,id'); - mock.verify(); - }); - it('should ignore when select not exist field', async function() { - mock = sinon.mock(resource.model); - mock.expects('select').never(); - await request(host).get('/book?$select=not-exist-field'); - mock.verify(); - }); -}); diff --git a/test/mocked/odata.query.skip.js b/test/mocked/odata.query.skip.js deleted file mode 100644 index 4d21945..0000000 --- a/test/mocked/odata.query.skip.js +++ /dev/null @@ -1,50 +0,0 @@ -import 'should'; -import sinon from 'sinon'; -import request from 'supertest'; -import { odata, host, port, books, bookSchema } from '../support/setup'; -import FakeDb from '../support/fake-db'; - -describe('odata.query.skip', () => { - let httpServer, mock, resource; - - before(async function() { - const db = new FakeDb(); - const server = odata(db); - resource = server.resource('book', bookSchema) - httpServer = server.listen(port); - db.addData('book', books); - }); - - after(() => { - httpServer.close(); - }); - - afterEach(() => { - mock.restore(); - }); - - it('should skip items', async function() { - mock = sinon.mock(resource.model); - mock.expects('skip').once().withArgs(1).returns(resource.model); - await request(host).get('/book?$skip=1'); - mock.verify(); - }); - it('should ignore when skip over count of items', async function() { - mock = sinon.mock(resource.model); - mock.expects('skip').once().withArgs(1024).returns(resource.model); - await request(host).get('/book?$skip=1024'); - mock.verify(); - }); - it('should ignore when skip not a number', async function() { - mock = sinon.mock(resource.model); - mock.expects('skip').never(); - await request(host).get('/book?$skip=not-a-number'); - mock.verify(); - }); - return it('should ignore when skip not a positive number', async function() { - mock = sinon.mock(resource.model); - mock.expects('skip').never(); - await request(host).get('/book?$skip=-1'); - mock.verify(); - }); -}); diff --git a/test/mocked/odata.query.top.js b/test/mocked/odata.query.top.js deleted file mode 100644 index 72cbd46..0000000 --- a/test/mocked/odata.query.top.js +++ /dev/null @@ -1,44 +0,0 @@ -import 'should'; -import sinon from 'sinon'; -import request from 'supertest'; -import FakeDb from '../support/fake-db'; -import { odata, host, port, books, bookSchema } from '../support/setup'; - -describe('odata.query.top', () => { - let httpServer, mock, resource; - - before(async function() { - const db = new FakeDb() - const server = odata(db); - resource = server.resource('book', bookSchema) - httpServer = server.listen(port); - db.addData('book', books); - }); - - after(() => { - httpServer.close(); - }); - - afterEach(() => { - mock.restore(); - }); - - it('should top items', async function() { - mock = sinon.mock(resource.model); - mock.expects('limit').once().withArgs(1).returns(resource.model); - const res = await request(host).get('/book?$top=1'); - mock.verify(); - }); - it('should iginre when top not a number', async function() { - mock = sinon.mock(resource.model); - mock.expects('limit').never(); - const res = await request(host).get('/book?$top=not-a-number'); - mock.verify(); - }); - it('should ignore when top not a positive number', async function() { - mock = sinon.mock(resource.model); - mock.expects('limit').never(); - const res = await request(host).get('/book?$top=-1'); - mock.verify(); - }); -}); diff --git a/test/mocked/options.maxSkip.js b/test/mocked/options.maxSkip.js deleted file mode 100644 index b3766d8..0000000 --- a/test/mocked/options.maxSkip.js +++ /dev/null @@ -1,63 +0,0 @@ -import 'should'; -import sinon from 'sinon'; -import request from 'supertest'; -import { odata, host, port, books, bookSchema } from '../support/setup'; -import FakeDb from '../support/fake-db'; - -describe('options.maxSkip', () => { - let httpServer, server, mock, resource; - - beforeEach(async function() { - const db = new FakeDb(); - server = odata(db); - resource = server.resource('book', bookSchema); - db.addData('book', books); - }); - - afterEach(() => { - httpServer.close(); - mock.restore(); - }); - - it('global-limit should work', async function() { - mock = sinon.mock(resource.model); - mock.expects('skip').once().withArgs(1).returns(resource.model); - server.set('maxSkip', 1); - httpServer = server.listen(port); - await request(host).get('/book?$skip=100'); - mock.verify(); - }); - it('resource-limit should work', async function() { - mock = sinon.mock(resource.model); - mock.expects('skip').once().withArgs(1).returns(resource.model); - resource.maxSkip(1); - httpServer = server.listen(port); - await request(host).get('/book?$skip=100'); - mock.verify(); - }); - it('should use resource-limit even global-limit already set', async function() { - mock = sinon.mock(resource.model); - mock.expects('skip').once().withArgs(1).returns(resource.model); - server.set('maxSkip', 2); - resource.maxSkip(1); - httpServer = server.listen(port); - await request(host).get('/book?$skip=100'); - mock.verify(); - }); - it('should use query-limit if it is minimum global-limit', async function() { - mock = sinon.mock(resource.model); - mock.expects('skip').once().withArgs(1).returns(resource.model); - server.set('maxSkip', 2); - httpServer = server.listen(port); - await request(host).get('/book?$skip=1'); - mock.verify(); - }); - it('should use query-limit if it is minimum resource-limit', async function() { - mock = sinon.mock(resource.model); - mock.expects('skip').once().withArgs(1).returns(resource.model); - resource.maxSkip(2); - httpServer = server.listen(port); - await request(host).get('/book?$skip=1'); - mock.verify(); - }); -}); diff --git a/test/mocked/options.maxTop.js b/test/mocked/options.maxTop.js deleted file mode 100644 index 0363631..0000000 --- a/test/mocked/options.maxTop.js +++ /dev/null @@ -1,62 +0,0 @@ -import 'should'; -import sinon from 'sinon'; -import request from 'supertest'; -import { odata, host, port, bookSchema } from '../support/setup'; -import FakeDb from '../support/fake-db'; - -describe('options.maxTop', () => { - let httpServer, server, resource, mock; - - beforeEach(async function() { - const db = new FakeDb(); - server = odata(db); - resource = server.resource('book', bookSchema); - }); - - afterEach(() => { - httpServer.close(); - mock.restore(); - }); - - it('global-limit should work', async function() { - mock = sinon.mock(resource.model); - mock.expects('limit').once().withArgs(1).returns(resource.model); - server.set('maxTop', 1); - httpServer = server.listen(port); - await request(host).get('/book?$top=100'); - mock.verify(); - }); - it('resource-limit should work', async function() { - mock = sinon.mock(resource.model); - mock.expects('limit').once().withArgs(1).returns(resource.model); - resource.maxTop(1); - httpServer = server.listen(port); - await request(host).get('/book?$top=2'); - mock.verify(); - }); - it('should use resource-limit even global-limit already set', async function() { - mock = sinon.mock(resource.model); - mock.expects('limit').once().withArgs(1).returns(resource.model); - server.set('maxTop', 2); - resource.maxTop(1); - httpServer = server.listen(port); - await request(host).get('/book?$top=100'); - mock.verify(); - }); - it('should use query-limit if it is minimum global-limit', async function() { - mock = sinon.mock(resource.model); - mock.expects('limit').once().withArgs(1).returns(resource.model); - server.set('maxTop', 2); - httpServer = server.listen(port); - await request(host).get('/book?$top=1'); - mock.verify(); - }); - it('should use query-limit if it is minimum resource-limit', async function() { - mock = sinon.mock(resource.model); - mock.expects('limit').once().withArgs(1).returns(resource.model); - resource.maxTop(2); - httpServer = server.listen(port); - await request(host).get('/book?$top=1'); - mock.verify(); - }); -}); diff --git a/test/mocked/rest.delete.js b/test/mocked/rest.delete.js deleted file mode 100644 index 1859452..0000000 --- a/test/mocked/rest.delete.js +++ /dev/null @@ -1,41 +0,0 @@ -import 'should'; -import request from 'supertest'; -import { odata, host, port, bookSchema, assertSuccess } from '../support/setup'; -import books from '../support/books.json'; -import FakeDb from '../support/fake-db'; - -describe('rest.delete', function() { - let data, httpServer; - - before(async function() { - const db = new FakeDb(); - const server = odata(db); - server.resource('book', bookSchema) - httpServer = server.listen(port); - data = db.addData('book', books); - }); - - after(() => { - httpServer.close(); - }); - - it('should delete resource if it exist', async function() { - const res = await request(host).del(`/book(${data[0].id})`); - assertSuccess(res); - res.status.should.be.equal(204); - }); - it('should be 404 if resource not exist', async function() { - const res = await request(host).del(`/book(not-exist-id)`); - res.status.should.be.equal(404); - }); - it('should be 404 if without id', async function() { - const res = await request(host).del(`/book`); - res.status.should.be.equal(404); - }); - it('should 404 if try to delete a resource twice', async function() { - const id = data[0].id; - await request(host).del(`/book(${id})`); - const res = await request(host).del(`/book(${id})`); - res.status.should.be.equal(404); - }); -}); diff --git a/test/mocked/rest.get.js b/test/mocked/rest.get.js deleted file mode 100644 index 41adfcd..0000000 --- a/test/mocked/rest.get.js +++ /dev/null @@ -1,54 +0,0 @@ -import 'should'; -import request from 'supertest'; -import { odata, host, port, bookSchema } from '../support/setup'; -import books from '../support/books.json'; -import FakeDb from '../support/fake-db'; - -describe('rest.get', () => { - let data, cdata, httpServer; - - before(async function() { - const db = new FakeDb(); - const server = odata(db); - server.resource('book', bookSchema); - server.resource('complex-type', { - p1: { - p2: { - type: String - } - } - }); - httpServer = server.listen(port); - data = db.addData('book', books); - cdata = db.addData('complex-type', [{ - "p1.p2": "p1.p2 value" - }]); - }); - - after(() => { - httpServer.close(); - }); - - it('should return all of the resources', async function() { - const res = await request(host).get(`/book`); - res.body.should.be.have.property('value'); - res.body.value.length.should.be.equal(data.length); - }); - it('should return special resource', async function() { - const res = await request(host).get(`/book(${data[0].id})`); - res.body.should.be.have.property('title'); - res.body.title.should.be.equal(data[0].title); - }); - it('should be 404 if resouce name not declare', async function() { - const res = await request(host).get(`/not-exist-resource`); - res.status.should.be.equal(404); - }); - it('should be 404 if resource not exist', async function() { - const res = await request(host).get(`/book(not-exist-id)`); - res.status.should.be.equal(404); - }); - it('should replace a dot in property names with -', async function() { - const res = await request(host).get(`/complex-type(${cdata[0].id})`); - res.body.should.be.have.property('p1-p2'); - }); -}); diff --git a/test/mongo/mocked/model.complex.filter.js b/test/mongo/mocked/model.complex.filter.js index 7fdf161..2b97b37 100644 --- a/test/mongo/mocked/model.complex.filter.js +++ b/test/mongo/mocked/model.complex.filter.js @@ -34,10 +34,8 @@ describe('model.complex.filter', () => { }; modelMock = sinon.mock(ComplexModel); queryMock = sinon.mock(query); - modelMock.expects('find').once().returns(query); + modelMock.expects('find').once().withArgs({"product.price": {$gt: 30}}).returns(query); - queryMock.expects('where').once().withArgs('product.price').returns(query); - queryMock.expects('gt').once().withArgs(30); queryMock.expects('select').once().withArgs({ _id: 0, product: 1}); queryMock.expects('exec').once().callsArgWith(0, null, [{ toObject: () => ({ @@ -54,7 +52,7 @@ describe('model.complex.filter', () => { }); it('should work when filter a complex entity', async function () { - let res = await request(host).get(`/complex-model-filter?$select=product&$filter=product-price gt 30`); + let res = await request(host).get(`/complex-model-filter?$select=product&$filter=product.price gt 30`); assertSuccess(res); res.body.should.deepEqual({ value: [{ diff --git a/test/mongo/mocked/model.custom.id.js b/test/mongo/mocked/model.custom.id.js index fc408c0..e82b734 100644 --- a/test/mongo/mocked/model.custom.id.js +++ b/test/mongo/mocked/model.custom.id.js @@ -71,10 +71,7 @@ describe('model.custom.id', () => { }; modelMock = sinon.mock(Model); queryMock = sinon.mock(query); - modelMock.expects('find').once().returns(query); - - queryMock.expects('where').once().withArgs('id').returns(query); - queryMock.expects('equals').once().withArgs(100); + modelMock.expects('find').once().withArgs({id: {$eq: 100}}).returns(query); queryMock.expects('exec').once().callsArgWith(0, null, [{ toObject: () => ({ id: 100 diff --git a/test/mongo/mocked/odata.query.filter.functions.js b/test/mongo/mocked/odata.query.filter.functions.js index f36fa95..07e86d1 100644 --- a/test/mongo/mocked/odata.query.filter.functions.js +++ b/test/mongo/mocked/odata.query.filter.functions.js @@ -3,7 +3,6 @@ import sinon from 'sinon'; import request from 'supertest'; import { odata, host, port, assertSuccess } from '../../support/setup'; import data from '../../support/books.json'; -import FakeDb from '../../support/fake-db'; import { BookModel } from '../../support/books.model'; describe('odata.query.filter.functions', function () { @@ -39,8 +38,7 @@ describe('odata.query.filter.functions', function () { modelMock = sinon.mock(BookModel); queryMock = sinon.mock(query); - modelMock.expects('find').once().returns(query); - queryMock.expects('$where').once().withArgs(`this.title.indexOf('i') != -1`); + modelMock.expects('find').once().withArgs({title: {$where: `this.title.indexOf('i') != -1`}}).returns(query); queryMock.expects('exec').once().callsArgWith(0, null, books.map(item => ({ toObject: () => item }))); const res = await request(host).get(`/book?$filter=contains(title,'i')`); @@ -57,8 +55,7 @@ describe('odata.query.filter.functions', function () { modelMock = sinon.mock(BookModel); queryMock = sinon.mock(query); - modelMock.expects('find').once().returns(query); - queryMock.expects('$where').once().withArgs(`this.title.indexOf('Visual Studio') != -1`); + modelMock.expects('find').once().withArgs({title: {$where: `this.title.indexOf('Visual Studio') != -1`}}).returns(query); queryMock.expects('exec').once().callsArgWith(0, null, books.map(item => ({ toObject: () => item }))); const res = await request(host).get(`/book?$filter=contains(title,'Visual Studio')`); @@ -78,8 +75,7 @@ describe('odata.query.filter.functions', function () { modelMock = sinon.mock(BookModel); queryMock = sinon.mock(query); - modelMock.expects('find').once().returns(query); - queryMock.expects('$where').once().withArgs(`this.title.indexOf('i') >= 1`); + modelMock.expects('find').once().withArgs({title: {$where: `this.title.indexOf('i') >= 1`}}).returns(query); queryMock.expects('exec').once().callsArgWith(0, null, books.map(item => ({ toObject: () => item }))); const res = await request(host).get(`/book?$filter=indexof(title,'i') ge 1`); @@ -96,8 +92,7 @@ describe('odata.query.filter.functions', function () { modelMock = sinon.mock(BookModel); queryMock = sinon.mock(query); - modelMock.expects('find').once().returns(query); - queryMock.expects('$where').once().withArgs(`this.title.indexOf('Visual Studio') >= 0`); + modelMock.expects('find').once().withArgs({title: {$where: `this.title.indexOf('Visual Studio') >= 0`}}).returns(query); queryMock.expects('exec').once().callsArgWith(0, null, books.map(item => ({ toObject: () => item }))); const res = await request(host).get(`/book?$filter=indexof(title,'Visual Studio') ge 0`); @@ -117,10 +112,9 @@ describe('odata.query.filter.functions', function () { modelMock = sinon.mock(BookModel); queryMock = sinon.mock(query); - modelMock.expects('find').once().returns(query); - queryMock.expects('where').once().withArgs(`publish_date`).returns(query); - queryMock.expects('gte').once().withArgs(new Date(2000, 0, 1)).returns(query); - queryMock.expects('lt').once().withArgs(new Date(2001, 0, 1)).returns(query); + modelMock.expects('find').once().withArgs({ + publish_date: {$gte: new Date(2000, 0, 1), $lt: new Date(2001, 0, 1)} + }).returns(query); queryMock.expects('exec').once().callsArgWith(0, null, books.map(item => ({ toObject: () => item }))); const res = await request(host).get(`/book?$filter=year(publish_date) eq 2000`); diff --git a/test/mongo/mocked/odata.query.filter.js b/test/mongo/mocked/odata.query.filter.js index b851d9b..041a202 100644 --- a/test/mongo/mocked/odata.query.filter.js +++ b/test/mongo/mocked/odata.query.filter.js @@ -40,12 +40,10 @@ describe('odata.query.filter', function () { modelMock = sinon.mock(BookModel); queryMock = sinon.mock(query); - modelMock.expects('find').once().returns(query); - queryMock.expects('where').once().withArgs(`title`).returns(query); - queryMock.expects('equals').once().withArgs('Midnight Rain'); + modelMock.expects('find').once().withArgs({title: {$eq: 'Midnight Rain'}}).returns(query); queryMock.expects('exec').once().callsArgWith(0, null, books.map(item => ({ toObject: () => item }))); - const res = await request(host).get(`/book?$filter=title eq '${data[1].title}'`); + const res = await request(host).get(`/book?$filter=title eq 'Midnight Rain'`); assertSuccess(res); modelMock.verify(); @@ -59,9 +57,7 @@ describe('odata.query.filter', function () { modelMock = sinon.mock(BookModel); queryMock = sinon.mock(query); - modelMock.expects('find').once().returns(query); - queryMock.expects('where').once().withArgs(`author`).returns(query); - queryMock.expects('equals').once().withArgs('Ralls, Kim'); + modelMock.expects('find').once().withArgs({author: {$eq: 'Ralls, Kim'}}).returns(query); queryMock.expects('exec').once().callsArgWith(0, null, books.map(item => ({ toObject: () => item }))); const res = await request(host).get(`/book?$filter=author eq 'Ralls, Kim'`); @@ -73,17 +69,15 @@ describe('odata.query.filter', function () { value: books }); }); - it('should filter items when it has extra spaces at begin', async function () { - const books = data.filter(item => item.title === 'Midnight Rain'); + it('should filter items when use id', async function () { + const books = data.filter(item => item.id === '2'); modelMock = sinon.mock(BookModel); queryMock = sinon.mock(query); - modelMock.expects('find').once().returns(query); - queryMock.expects('where').once().withArgs(`title`).returns(query); - queryMock.expects('equals').once().withArgs('Midnight Rain'); + modelMock.expects('find').once().withArgs({_id: {$eq: '2'}}).returns(query); queryMock.expects('exec').once().callsArgWith(0, null, books.map(item => ({ toObject: () => item }))); - const res = await request(host).get(`/book?$filter= title eq 'Midnight Rain'`); + const res = await request(host).get(encodeURI(`/book?$filter=id eq '2'`)); assertSuccess(res); modelMock.verify(); @@ -92,17 +86,18 @@ describe('odata.query.filter', function () { value: books }); }); - it('should filter items when it has extra spaces at mid', async function () { - const books = data.filter(item => item.title === 'Midnight Rain'); + }); + + describe("[Not equal]", () => { + it('should filter items', async function () { + const books = data.filter(item => item.author != 'Ralls, Kim'); modelMock = sinon.mock(BookModel); queryMock = sinon.mock(query); - modelMock.expects('find').once().returns(query); - queryMock.expects('where').once().withArgs(`title`).returns(query); - queryMock.expects('equals').once().withArgs('Midnight Rain'); + modelMock.expects('find').once().withArgs({author: {$ne: 'Ralls, Kim'}}).returns(query); queryMock.expects('exec').once().callsArgWith(0, null, books.map(item => ({ toObject: () => item }))); - const res = await request(host).get(`/book?$filter=title eq 'Midnight Rain'`); + const res = await request(host).get(`/book?$filter=author ne 'Ralls, Kim'`); assertSuccess(res); modelMock.verify(); @@ -111,17 +106,18 @@ describe('odata.query.filter', function () { value: books }); }); - it('should filter items when it has extra spaces at end', async function () { - const books = data.filter(item => item.title === 'Midnight Rain'); + }); + + describe("[Greater than]", () => { + it('should filter items', async function () { + const books = data.filter(item => item.price > 36.95); modelMock = sinon.mock(BookModel); queryMock = sinon.mock(query); - modelMock.expects('find').once().returns(query); - queryMock.expects('where').once().withArgs(`title`).returns(query); - queryMock.expects('equals').once().withArgs('Midnight Rain'); + modelMock.expects('find').once().withArgs({price: {$gt: 36.95}}).returns(query); queryMock.expects('exec').once().callsArgWith(0, null, books.map(item => ({ toObject: () => item }))); - const res = await request(host).get(`/book?$filter=title eq '${data[1].title}' `); + const res = await request(host).get(`/book?$filter=price gt 36.95`); assertSuccess(res); modelMock.verify(); @@ -130,17 +126,18 @@ describe('odata.query.filter', function () { value: books }); }); - it('should filter items when use chinese keyword', async function () { - const books = data.filter(item => item.title === '代码大全'); + }); + + describe('[Greater than or equal]', () => { + it('should filter items', async function () { + const books = data.filter(item => item.price >= 36.95); modelMock = sinon.mock(BookModel); queryMock = sinon.mock(query); - modelMock.expects('find').once().returns(query); - queryMock.expects('where').once().withArgs(`title`).returns(query); - queryMock.expects('equals').once().withArgs('代码大全'); + modelMock.expects('find').once().withArgs({price: {$gte: 36.95}}).returns(query); queryMock.expects('exec').once().callsArgWith(0, null, books.map(item => ({ toObject: () => item }))); - const res = await request(host).get(encodeURI(`/book?$filter=title eq '代码大全'`)); + const res = await request(host).get(`/book?$filter=price ge 36.95`); assertSuccess(res); modelMock.verify(); @@ -149,17 +146,18 @@ describe('odata.query.filter', function () { value: books }); }); - it('should filter items when use id', async function () { - const books = data.filter(item => item.id === '2'); + }); + + describe('[Less than]', () => { + it('should filter items', async function () { + const books = data.filter(item => item.price < 36.95); modelMock = sinon.mock(BookModel); queryMock = sinon.mock(query); - modelMock.expects('find').once().returns(query); - queryMock.expects('where').once().withArgs(`_id`).returns(query); - queryMock.expects('equals').once().withArgs('2'); + modelMock.expects('find').once().withArgs({price: {$lt: 36.95}}).returns(query); queryMock.expects('exec').once().callsArgWith(0, null, books.map(item => ({ toObject: () => item }))); - const res = await request(host).get(encodeURI(`/book?$filter=id eq '2'`)); + const res = await request(host).get(`/book?$filter=price lt 36.95`); assertSuccess(res); modelMock.verify(); @@ -170,74 +168,49 @@ describe('odata.query.filter', function () { }); }); - describe("[Not equal]", () => { - it('should filter items', async function () { - mock = sinon.mock(resource.model); - mock.expects('where').once().withArgs('title').returns(resource.model); - mock.expects('ne').once().withArgs(data[1].title).returns(resource.model); - await request(host).get(`/book?$filter=title ne '${data[1].title}'`); - mock.verify(); - }); - }); - - describe("[Greater than]", () => { + describe('[Less than or equal]', () => { it('should filter items', async function () { - mock = sinon.mock(resource.model); - mock.expects('where').once().withArgs('price').returns(resource.model); - mock.expects('gt').once().withArgs(36.95).returns(resource.model); - await request(host).get(`/book?$filter=price gt 36.95`); - mock.verify(); - }); - }); + const books = data.filter(item => item.price <= 36.95); - describe('[Greater than or equal]', () => { - it('should filter items', async function () { - mock = sinon.mock(resource.model); - mock.expects('where').once().withArgs('price').returns(resource.model); - mock.expects('gte').once().withArgs(36.95).returns(resource.model); - await request(host).get(`/book?$filter=price ge 36.95`); - mock.verify(); - }); - }); + modelMock = sinon.mock(BookModel); + queryMock = sinon.mock(query); + modelMock.expects('find').once().withArgs({price: {$lte: 36.95}}).returns(query); + queryMock.expects('exec').once().callsArgWith(0, null, books.map(item => ({ toObject: () => item }))); - describe('[Less than]', () => { - it('should filter items', async function () { - mock = sinon.mock(resource.model); - mock.expects('where').once().withArgs('price').returns(resource.model); - mock.expects('lt').once().withArgs(36.95).returns(resource.model); - await request(host).get(`/book?$filter=price lt 36.95`); - mock.verify(); - }); - }); + const res = await request(host).get(`/book?$filter=price le 36.95`); - describe('[Less than or equal]', () => { - it('should filter items', async function () { - mock = sinon.mock(resource.model); - mock.expects('where').once().withArgs('price').returns(resource.model); - mock.expects('lte').once().withArgs(36.95).returns(resource.model); - await request(host).get(`/book?$filter=price le 36.95`); - mock.verify(); + assertSuccess(res); + modelMock.verify(); + queryMock.verify(); + res.body.should.deepEqual({ + value: books + }); }); }); describe('[Logical and]', () => { it("should filter items", async function () { - mock = sinon.mock(resource.model); - mock.expects('where').withArgs('title').returns(resource.model); - mock.expects('where').withArgs('price').returns(resource.model); - mock.expects('ne').once().withArgs(data[1].title).returns(resource.model); - mock.expects('gte').once().withArgs(36.95).returns(resource.model); - await request(host).get(`/book?$filter=title ne '${data[1].title}' and price ge 36.95`); - mock.verify(); - }); - it("should filter items when it has extra spaces", async function () { - mock = sinon.mock(resource.model); - mock.expects('where').withArgs('title').returns(resource.model); - mock.expects('where').withArgs('price').returns(resource.model); - mock.expects('ne').once().withArgs(data[1].title).returns(resource.model); - mock.expects('gte').once().withArgs(36.95).returns(resource.model); - await request(host).get(`/book?$filter=title ne '${data[1].title}' and price ge 36.95`); - mock.verify(); + const books = data.filter(item => item.title != 'Midnight Rain' && item.price >= 36.95); + + modelMock = sinon.mock(BookModel); + queryMock = sinon.mock(query); + modelMock.expects('find').once() + .withArgs({ + $and: [{ + title: {$ne: 'Midnight Rain'} + }, { + price: {$gte: 36.95} + }]}).returns(query); + queryMock.expects('exec').once().callsArgWith(0, null, books.map(item => ({ toObject: () => item }))); + + const res = await request(host).get(`/book?$filter=title ne 'Midnight Rain' and price ge 36.95`); + + assertSuccess(res); + modelMock.verify(); + queryMock.verify(); + res.body.should.deepEqual({ + value: books + }); }); }); }); diff --git a/test/mongo/mocked/odata.query.orderby.js b/test/mongo/mocked/odata.query.orderby.js new file mode 100644 index 0000000..f4b86ed --- /dev/null +++ b/test/mongo/mocked/odata.query.orderby.js @@ -0,0 +1,130 @@ +import 'should'; +import sinon from 'sinon'; +import request from 'supertest'; +import { odata, host, port, assertSuccess } from '../../support/setup'; +import data from '../../support/books.json'; +import { BookModel } from '../../support/books.model'; + +describe('odata.query.orderby', () => { + const query = { + $where: () => { }, + where: () => { }, + equals: () => { }, + gte: () => { }, + sort: () => { }, + exec: () => { }, + model: BookModel + }; + let httpServer, modelMock, queryMock; + + before(async function() { + const server = odata(); + + server.mongoEntity('book', BookModel); + httpServer = server.listen(port); + + }); + + after(() => { + httpServer.close(); + }); + + afterEach(() => { + modelMock?.restore(); + queryMock?.restore(); + }); + + it('should default let items order with asc', async function() { + const books = data.sort((a, b) => a.price - b.price); + + modelMock = sinon.mock(BookModel); + queryMock = sinon.mock(query); + modelMock.expects('find').returns(query); + queryMock.expects('sort').once().withArgs([['price', 'asc']]); + queryMock.expects('exec').once().callsArgWith(0, null, books.map(item => ({ toObject: () => item }))); + + const res = await request(host).get('/book?$orderby=price'); + + assertSuccess(res); + modelMock.verify(); + queryMock.verify(); + res.body.should.deepEqual({ + value: books + }); + }); + + it('should let items order asc', async function() { + const books = data.sort((a, b) => a.price - b.price); + + modelMock = sinon.mock(BookModel); + queryMock = sinon.mock(query); + modelMock.expects('find').returns(query); + queryMock.expects('sort').once().withArgs([['price', 'asc']]); + queryMock.expects('exec').once().callsArgWith(0, null, books.map(item => ({ toObject: () => item }))); + + const res = await request(host).get('/book?$orderby=price asc'); + + assertSuccess(res); + modelMock.verify(); + queryMock.verify(); + res.body.should.deepEqual({ + value: books + }); + }); + + it('should let items order desc', async function() { + const books = data.sort((a, b) => b.price - a.price); + + modelMock = sinon.mock(BookModel); + queryMock = sinon.mock(query); + modelMock.expects('find').returns(query); + queryMock.expects('sort').once().withArgs([['price', 'desc']]); + queryMock.expects('exec').once().callsArgWith(0, null, books.map(item => ({ toObject: () => item }))); + + const res = await request(host).get('/book?$orderby=price desc'); + + assertSuccess(res); + modelMock.verify(); + queryMock.verify(); + res.body.should.deepEqual({ + value: books + }); + }); + + it('should let items order when use multiple fields', async function() { + const books = data.sort((a, b) => { + const result = a.price - b.price; + + if (!result) { + return a.title - b.title; + } + + return result; + }); + + modelMock = sinon.mock(BookModel); + queryMock = sinon.mock(query); + modelMock.expects('find').returns(query); + queryMock.expects('sort').once().withArgs([['price', 'asc'], ['title', 'asc']]); + queryMock.expects('exec').once().callsArgWith(0, null, books.map(item => ({ toObject: () => item }))); + + const res = await request(host).get('/book?$orderby=price,title'); + + assertSuccess(res); + modelMock.verify(); + queryMock.verify(); + res.body.should.deepEqual({ + value: books + }); + }); + + it("should fail by not exist field", async function() { + modelMock = sinon.mock(BookModel); + modelMock.expects('find').never(); + + const res = await request(host).get('/book?$orderby=not-exist-field'); + + modelMock.verify(); + res.status.should.be.equal(400); + }); +}); diff --git a/test/mongo/mocked/odata.query.select.js b/test/mongo/mocked/odata.query.select.js new file mode 100644 index 0000000..1c241ca --- /dev/null +++ b/test/mongo/mocked/odata.query.select.js @@ -0,0 +1,144 @@ +import 'should'; +import sinon from 'sinon'; +import request from 'supertest'; +import { odata, host, port, assertSuccess } from '../../support/setup'; +import data from '../../support/books.json'; +import { BookModel } from '../../support/books.model'; + +describe('odata.query.select', () => { + const query = { + $where: () => { }, + where: () => { }, + equals: () => { }, + select: () => { }, + sort: () => { }, + exec: () => { }, + model: BookModel + }; + let httpServer, modelMock, queryMock; + + before(async function() { + const server = odata(); + + server.mongoEntity('book', BookModel); + httpServer = server.listen(port); + }); + + after(() => { + httpServer.close(); + }); + + afterEach(() => { + modelMock?.restore(); + queryMock?.restore(); + }); + + it('should select anyone field', async function() { + const books = data.map(item => ({ + price: item.price + })); + + modelMock = sinon.mock(BookModel); + queryMock = sinon.mock(query); + modelMock.expects('find').returns(query); + queryMock.expects('select').once().withArgs({ + _id: 0, + price: 1 + }); + queryMock.expects('exec').once().callsArgWith(0, null, books.map(item => ({ toObject: () => item }))); + + const res = await request(host).get('/book?$select=price'); + + assertSuccess(res); + modelMock.verify(); + queryMock.verify(); + res.body.should.deepEqual({ + value: books + }); + }); + it('should select multiple field', async function() { + const books = data.map(item => ({ + price: item.price, + title: item.title + })); + + modelMock = sinon.mock(BookModel); + queryMock = sinon.mock(query); + modelMock.expects('find').returns(query); + queryMock.expects('select').once().withArgs({ + _id: 0, + price: 1, + title: 1 + }); + queryMock.expects('exec').once().callsArgWith(0, null, books.map(item => ({ toObject: () => item }))); + + const res = await request(host).get('/book?$select=price,title'); + + assertSuccess(res); + modelMock.verify(); + queryMock.verify(); + res.body.should.deepEqual({ + value: books + }); + }); + it('should select multiple field with blank space', async function() { + const books = data.map(item => ({ + price: item.price, + title: item.title + })); + + modelMock = sinon.mock(BookModel); + queryMock = sinon.mock(query); + modelMock.expects('find').returns(query); + queryMock.expects('select').once().withArgs({ + _id: 0, + price: 1, + title: 1 + }); + queryMock.expects('exec').once().callsArgWith(0, null, books.map(item => ({ toObject: () => item }))); + + const res = await request(host).get('/book?$select=price, title'); + + assertSuccess(res); + modelMock.verify(); + queryMock.verify(); + res.body.should.deepEqual({ + value: books + }); + }); + it('should select id field', async function() { + const books = data.map(item => ({ + price: item.price, + title: item.title, + id: item.id + })); + + modelMock = sinon.mock(BookModel); + queryMock = sinon.mock(query); + modelMock.expects('find').returns(query); + queryMock.expects('select').once().withArgs({ + _id: 1, + price: 1, + title: 1 + }); + queryMock.expects('exec').once().callsArgWith(0, null, books.map(item => ({ toObject: () => item }))); + + const res = await request(host).get('/book?$select=price,title,id'); + + assertSuccess(res); + modelMock.verify(); + queryMock.verify(); + res.body.should.deepEqual({ + value: books + }); + }); + it('should ignore when select not exist field', async function() { + modelMock = sinon.mock(BookModel); + modelMock.expects('find').never(); + + const res = await request(host).get('/book?$select=not-exist-field'); + + modelMock.verify(); + res.status.should.be.equal(400); + }); +}); diff --git a/test/mongo/mocked/odata.query.skip.js b/test/mongo/mocked/odata.query.skip.js new file mode 100644 index 0000000..bdd9b6c --- /dev/null +++ b/test/mongo/mocked/odata.query.skip.js @@ -0,0 +1,81 @@ +import 'should'; +import sinon from 'sinon'; +import request from 'supertest'; +import { odata, host, port, assertSuccess } from '../../support/setup'; +import { BookModel } from '../../support/books.model'; +import data from '../../support/books.json'; + +describe('odata.query.skip', () => { + const query = { + $where: () => { }, + where: () => { }, + skip: () => { }, + select: () => { }, + sort: () => { }, + exec: () => { }, + model: BookModel + }; + let httpServer, modelMock, queryMock; + + before(async function() { + const server = odata(); + + server.mongoEntity('book', BookModel) + httpServer = server.listen(port); + + }); + + after(() => { + httpServer.close(); + }); + + afterEach(() => { + modelMock?.restore(); + queryMock?.restore(); + }); + + it('should skip items', async function() { + modelMock = sinon.mock(BookModel); + queryMock = sinon.mock(query); + modelMock.expects('find').returns(query); + queryMock.expects('skip').once().withArgs(1); + queryMock.expects('exec').once().callsArgWith(0, null, data.map(item => ({ toObject: () => item }))); + + const res = await request(host).get('/book?$skip=1'); + + assertSuccess(res); + modelMock.verify(); + queryMock.verify(); + }); + it('should ignore when skip over count of items', async function() { + modelMock = sinon.mock(BookModel); + queryMock = sinon.mock(query); + modelMock.expects('find').returns(query); + queryMock.expects('skip').once().withArgs(1024); + queryMock.expects('exec').once().callsArgWith(0, null, data.map(item => ({ toObject: () => item }))); + + const res = await request(host).get('/book?$skip=1024'); + + assertSuccess(res); + modelMock.verify(); + queryMock.verify(); + }); + it('should ignore when skip not a number', async function() { + modelMock = sinon.mock(BookModel); + modelMock.expects('find').never(); + + const res = await request(host).get('/book?$skip=not-a-number'); + + modelMock.verify(); + res.status.should.be.equal(400); + }); + return it('should ignore when skip not a positive number', async function() { + modelMock = sinon.mock(BookModel); + modelMock.expects('find').never(); + + const res = await request(host).get('/book?$skip=-1'); + + modelMock.verify(); + res.status.should.be.equal(400); + }); +}); diff --git a/test/mongo/mocked/odata.query.top.js b/test/mongo/mocked/odata.query.top.js new file mode 100644 index 0000000..a1041bc --- /dev/null +++ b/test/mongo/mocked/odata.query.top.js @@ -0,0 +1,67 @@ +import 'should'; +import sinon from 'sinon'; +import request from 'supertest'; +import { odata, host, port, assertSuccess } from '../../support/setup'; +import { BookModel } from '../../support/books.model'; +import data from '../../support/books.json'; + +describe('odata.query.top', () => { + const query = { + $where: () => { }, + limit: () => { }, + skip: () => { }, + select: () => { }, + sort: () => { }, + exec: () => { }, + model: BookModel + }; + let httpServer, modelMock, queryMock; + + before(async function() { + const server = odata(); + + server.mongoEntity('book', BookModel) + httpServer = server.listen(port); + }); + + after(() => { + httpServer.close(); + }); + + afterEach(() => { + modelMock?.restore(); + queryMock?.restore(); + }); + + it('should top items', async function() { + modelMock = sinon.mock(BookModel); + queryMock = sinon.mock(query); + modelMock.expects('find').returns(query); + queryMock.expects('limit').once().withArgs(1); + queryMock.expects('exec').once().callsArgWith(0, null, data.map(item => ({ toObject: () => item }))); + + const res = await request(host).get('/book?$top=1'); + + assertSuccess(res); + modelMock.verify(); + queryMock.verify(); + }); + it('should iginre when top not a number', async function() { + modelMock = sinon.mock(BookModel); + modelMock.expects('find').never(); + + const res = await request(host).get('/book?$top=not-a-number'); + + modelMock.verify(); + res.status.should.be.equal(400); + }); + it('should ignore when top not a positive number', async function() { + modelMock = sinon.mock(BookModel); + modelMock.expects('find').never(); + + const res = await request(host).get('/book?$top=-1'); + + modelMock.verify(); + res.status.should.be.equal(400); + }); +}); diff --git a/test/mongo/mocked/rest.delete.js b/test/mongo/mocked/rest.delete.js new file mode 100644 index 0000000..cc84798 --- /dev/null +++ b/test/mongo/mocked/rest.delete.js @@ -0,0 +1,50 @@ +import 'should'; +import request from 'supertest'; +import { odata, host, port, assertSuccess } from '../../support/setup'; +import { BookModel } from '../../support/books.model'; +import sinon from 'sinon'; + +describe('rest.delete', function() { + let httpServer, modelMock; + + before(async function() { + const server = odata(); + server.mongoEntity('book', BookModel) + httpServer = server.listen(port); + }); + + after(() => { + httpServer.close(); + }); + + afterEach(() => { + modelMock?.restore(); + }); + + it('should delete resource if it exist', async function() { + modelMock = sinon.mock(BookModel); + modelMock.expects('remove').once().withArgs({_id: '1'}) + .callsArgWith(1, null, JSON.stringify({n:1})); + + const res = await request(host).del(`/book('1')`); + + assertSuccess(res); + res.status.should.be.equal(204); + modelMock.verify(); + }); + it('should be 404 if resource not exist', async function() { + modelMock = sinon.mock(BookModel); + modelMock.expects('remove').once().withArgs({_id: '666'}) + .callsArgWith(1, null, JSON.stringify({n:0})); + + const res = await request(host).del(`/book('666')`); + + res.status.should.be.equal(404); + modelMock.verify(); + }); + it('should be 404 if without id', async function() { + const res = await request(host).del(`/book`); + + res.status.should.be.equal(404); + }); +}); diff --git a/test/mongo/mocked/rest.get.js b/test/mongo/mocked/rest.get.js new file mode 100644 index 0000000..66ee09b --- /dev/null +++ b/test/mongo/mocked/rest.get.js @@ -0,0 +1,94 @@ +import 'should'; +import request from 'supertest'; +import { odata, host, port, bookSchema } from '../../support/setup'; +import { BookModel } from '../../support/books.model'; +import mongoose from 'mongoose'; +import sinon from 'sinon'; + +const Schema = mongoose.Schema; + +describe('rest.get', () => { + const query = { + $where: () => { }, + limit: () => { }, + skip: () => { }, + select: () => { }, + sort: () => { }, + exec: () => { } + }; + const bookQuery = { + ...query, + model: BookModel + }; + + let httpServer, modelMock, queryMock, complexQuery; + + before(async function() { + const server = odata(); + server.mongoEntity('book', BookModel); + const ComplexModelSchema = new Schema({ + p1: { + p2: { + type: String + } + } + }); + + mongoose.set('overwriteModels', true); + const ComplexModel = mongoose.model('complex-type', ComplexModelSchema); + + complexQuery = { + ...query, + model: ComplexModel + }; + server.mongoEntity('complex-type', ComplexModel); + httpServer = server.listen(port); + + }); + + after(() => { + httpServer.close(); + }); + + afterEach(() => { + modelMock?.restore(); + queryMock?.restore(); + }); + + it('should return all of the resources', async function() { + modelMock = sinon.mock(BookModel); + queryMock = sinon.mock(bookQuery); + modelMock.expects('find').once().returns(bookQuery); + queryMock.expects('exec').once().callsArgWith(0, null, []); + + const res = await request(host).get(`/book`); + + res.body.should.be.have.property('value'); + res.body.value.length.should.be.equal(0); + modelMock.verify(); + queryMock.verify(); + }); + it('should return special resource', async function() { + modelMock = sinon.mock(BookModel); + queryMock = sinon.mock(bookQuery); + modelMock.expects('findById').once().withArgs('1') + .callsArgWith(1, null, {toObject: () => ({title: 'Krieg und Frieden'})}) + + const res = await request(host).get(`/book('1')`); + + res.body.should.be.have.property('title'); + res.body.title.should.be.equal('Krieg und Frieden'); + }); + it('should be 404 if resouce name not declare', async function() { + const res = await request(host).get(`/not-exist-resource`); + res.status.should.be.equal(404); + }); + it('should be 404 if resource not exist', async function() { + const res = await request(host).get(`/book(not-exist-id)`); + res.status.should.be.equal(404); + }); + it('should replace a dot in property names with -', async function() { + const res = await request(host).get(`/complex-type(${cdata[0].id})`); + res.body.should.be.have.property('p1-p2'); + }); +}); diff --git a/test/odata.filter.js b/test/odata.filter.js index 5a238a9..9925548 100644 --- a/test/odata.filter.js +++ b/test/odata.filter.js @@ -80,11 +80,68 @@ describe('odata.filter', () => { }); + it('should filter items when it has extra spaces at begin', async function () { + server.entity('book', { + list: (req, res, next) => { + req.$odata.$filter.should.deepEqual({ title: { $eq: 'Midnight Rain' } }); + res.$odata.result = { value: [] }; + next(); + } + }, BookMetadata); + httpServer = server.listen(port); + + const res = await request(host).get(`/book?$filter= title eq 'Midnight Rain'`); + + assertSuccess(res); + }); + it('should filter items when it has extra spaces at mid', async function () { + server.entity('book', { + list: (req, res, next) => { + req.$odata.$filter.should.deepEqual({ title: { $eq: 'Midnight Rain' } }); + res.$odata.result = { value: [] }; + next(); + } + }, BookMetadata); + httpServer = server.listen(port); + + const res = await request(host).get(`/book?$filter=title eq 'Midnight Rain'`); + + assertSuccess(res); + }); + it('should filter items when it has extra spaces at end', async function () { + server.entity('book', { + list: (req, res, next) => { + req.$odata.$filter.should.deepEqual({ title: { $eq: 'Midnight Rain' } }); + res.$odata.result = { value: [] }; + next(); + } + }, BookMetadata); + httpServer = server.listen(port); + + const res = await request(host).get(`/book?$filter=title eq 'Midnight Rain' `); + + assertSuccess(res); + }); + it('should filter items when use chinese keyword', async function () { + server.entity('book', { + list: (req, res, next) => { + req.$odata.$filter.should.deepEqual({ title: { $eq: '代码大全' } }); + res.$odata.result = { value: [] }; + next(); + } + }, BookMetadata); + httpServer = server.listen(port); + + const res = await request(host).get(encodeURI(`/book?$filter=title eq '代码大全'`)); + + assertSuccess(res); + }); + it(`should work with 'and' and two conditions on same property`, async function () { server.entity('book', { list: (req, res, next) => { - req.$odata.$filter.should.deepEqual({ - price: { + req.$odata.$filter.should.deepEqual({ + price: { $lte: 5.95, $gte: 4 } @@ -101,5 +158,133 @@ describe('odata.filter', () => { }); - //TODO: Weitere Beispiele https://masteringjs.io/tutorials/mongoose/find + it("[and] should filter items when it has extra spaces", async function () { + server.entity('book', { + list: (req, res, next) => { + req.$odata.$filter.should.deepEqual({ + $and: [{ + title: { $ne: 'Midnight Rain' } + }, { + price: { $gte: 36.95 }, + }] + }); + res.$odata.result = { value: [] }; + next(); + } + }, BookMetadata); + httpServer = server.listen(port); + + const res = await request(host).get(`/book?$filter=title ne 'Midnight Rain' and price ge 36.95`); + + assertSuccess(res); + }); + + it(`should work with 'and' and two conditions on different properties`, async function () { + server.entity('book', { + list: (req, res, next) => { + req.$odata.$filter.should.deepEqual({ + $and: [{ + price: { + $lte: 5.95 + } + }, { + author: { + $ne: 'Knorr, Stefan' + } + }] + }); + res.$odata.result = { value: [] }; + next(); + } + }, BookMetadata); + httpServer = server.listen(port); + + const res = await request(host).get(`/book?$filter=price le 5.95 and author ne 'Knorr, Stefan'`); + + assertSuccess(res); + + }); + + it(`should work with 'or'`, async function () { + server.entity('book', { + list: (req, res, next) => { + req.$odata.$filter.should.deepEqual({ + $or: [{ + price: { + $lte: 5.95 + } + }, { + author: { + $ne: 'Knorr, Stefan' + } + }] + }); + res.$odata.result = { value: [] }; + next(); + } + }, BookMetadata); + httpServer = server.listen(port); + + const res = await request(host).get(`/book?$filter=price le 5.95 or author ne 'Knorr, Stefan'`); + + assertSuccess(res); + + }); + + it(`should work with 'and' and 'or'`, async function () { + server.entity('book', { + list: (req, res, next) => { + req.$odata.$filter.should.deepEqual({ + $or: [{ + $and: [{ + price: { + $lte: 5.95 + } + }, { + author: { + $ne: 'Knorr, Stefan' + } + }] + }, { + $and: [{ + price: { + $gte: 5.95 + } + }, { + author: { + $eq: 'Knorr, Stefan' + } + }] + }] + + }); + res.$odata.result = { value: [] }; + next(); + } + }, BookMetadata); + httpServer = server.listen(port); + + const res = await request(host).get(`/book?$filter=price le 5.95 and author ne 'Knorr, Stefan' or price ge 5.95 and author eq 'Knorr, Stefan'`); + + assertSuccess(res); + + }); + it('should use mapping', async function () { + server.entity('book', { + list: (req, res, next) => { + req.$odata.$filter.should.deepEqual({ _id: { $eq: '2' } }); + res.$odata.result = { value: [] }; + next(); + } + }, BookMetadata, null, { + id: { + target: '_id' + } + }); + httpServer = server.listen(port); + + const res = await request(host).get(encodeURI(`/book?$filter=id eq '2'`)); + + assertSuccess(res); + }); }); diff --git a/test/odata.functions.js b/test/odata.functions.js index bdb4a9f..1b2b9c3 100644 --- a/test/odata.functions.js +++ b/test/odata.functions.js @@ -1,7 +1,6 @@ import 'should'; import request from 'supertest'; import { odata, host, port, assertSuccess } from './support/setup'; -import FakeDb from './support/fake-db'; describe('odata.functions', () => { ['get', 'post', 'put', 'delete'].map((method) => { diff --git a/test/options.maxSkip.js b/test/options.maxSkip.js new file mode 100644 index 0000000..8a5d2f5 --- /dev/null +++ b/test/options.maxSkip.js @@ -0,0 +1,99 @@ +import 'should'; +import request from 'supertest'; +import { odata, host, port, assertSuccess } from './support/setup'; +import { BookMetadata } from './support/books.model'; + +describe('options.maxSkip', () => { + let httpServer, server; + + beforeEach(async function() { + server = odata(); + + }); + + afterEach(() => { + httpServer.close(); + }); + + it('global-limit should work', async function() { + server.entity('book', { + list: (req, res, next) => { + res.$odata.result = []; + res.$odata.status = 200; + req.$odata.$skip.should.be.equal(1); + next(); + } + }, BookMetadata); + server.set('maxSkip', 1); + httpServer = server.listen(port); + + const res = await request(host).get('/book?$skip=100'); + + assertSuccess(res); + }); + it('resource-limit should work', async function() { + const entity = server.entity('book', { + list: (req, res, next) => { + res.$odata.result = []; + res.$odata.status = 200; + req.$odata.$skip.should.be.equal(1); + next(); + } + }, BookMetadata); + entity.set('maxSkip', 1); + httpServer = server.listen(port); + + const res = await request(host).get('/book?$skip=100'); + + assertSuccess(res); + }); + it('should use resource-limit even global-limit already set', async function() { + const entity = server.entity('book', { + list: (req, res, next) => { + res.$odata.result = []; + res.$odata.status = 200; + req.$odata.$skip.should.be.equal(1); + next(); + } + }, BookMetadata); + server.set('maxSkip', 2); + entity.set('maxSkip', 1); + httpServer = server.listen(port); + + const res = await request(host).get('/book?$skip=100'); + + assertSuccess(res); + }); + it('should use query-limit if it is minimum global-limit', async function() { + server.entity('book', { + list: (req, res, next) => { + res.$odata.result = []; + res.$odata.status = 200; + req.$odata.$skip.should.be.equal(1); + next(); + } + }, BookMetadata); + server.set('maxSkip', 2); + httpServer = server.listen(port); + + const res = await request(host).get('/book?$skip=1'); + + assertSuccess(res); + }); + it('should use query-limit if it is minimum resource-limit', async function() { + const entity = server.entity('book', { + list: (req, res, next) => { + res.$odata.result = []; + res.$odata.status = 200; + req.$odata.$skip.should.be.equal(1); + next(); + } + }, BookMetadata); + entity.set('maxSkip', 2); + httpServer = server.listen(port); + + const res = await request(host).get('/book?$skip=1'); + + assertSuccess(res); + }); +}); diff --git a/test/options.maxTop.js b/test/options.maxTop.js new file mode 100644 index 0000000..50cfb30 --- /dev/null +++ b/test/options.maxTop.js @@ -0,0 +1,99 @@ +import 'should'; +import sinon from 'sinon'; +import request from 'supertest'; +import { odata, host, port, assertSuccess } from './support/setup'; +import { BookMetadata } from './support/books.model'; + +describe('options.maxTop', () => { + let httpServer, server; + + beforeEach(async function() { + server = odata(); + }); + + afterEach(() => { + httpServer.close(); + }); + + it('global-limit should work', async function() { + server.entity('book', { + list: (req, res, next) => { + res.$odata.result = []; + res.$odata.status = 200; + req.$odata.$top.should.be.equal(1); + next(); + } + }, BookMetadata); + server.set('maxTop', 1); + httpServer = server.listen(port); + + const res = await request(host).get('/book?$top=100'); + + assertSuccess(res); + }); + it('resource-limit should work', async function() { + const entity = server.entity('book', { + list: (req, res, next) => { + res.$odata.result = []; + res.$odata.status = 200; + req.$odata.$top.should.be.equal(1); + next(); + } + }, BookMetadata); + entity.set('maxTop', 1); + httpServer = server.listen(port); + + const res = await request(host).get('/book?$top=2'); + + assertSuccess(res); + }); + it('should use resource-limit even global-limit already set', async function() { + const entity = server.entity('book', { + list: (req, res, next) => { + res.$odata.result = []; + res.$odata.status = 200; + req.$odata.$top.should.be.equal(1); + next(); + } + }, BookMetadata); + server.set('maxTop', 2); + entity.set('maxTop', 1); + httpServer = server.listen(port); + + const res = await request(host).get('/book?$top=100'); + + assertSuccess(res); + }); + it('should use query-limit if it is minimum global-limit', async function() { + const entity = server.entity('book', { + list: (req, res, next) => { + res.$odata.result = []; + res.$odata.status = 200; + req.$odata.$top.should.be.equal(1); + next(); + } + }, BookMetadata); + server.set('maxTop', 2); + httpServer = server.listen(port); + + const res = await request(host).get('/book?$top=1'); + + assertSuccess(res); + }); + it('should use query-limit if it is minimum resource-limit', async function() { + const entity = server.entity('book', { + list: (req, res, next) => { + res.$odata.result = []; + res.$odata.status = 200; + req.$odata.$top.should.be.equal(1); + next(); + } + }, BookMetadata); + entity.set('maxTop', 2); + httpServer = server.listen(port); + + const res = await request(host).get('/book?$top=1'); + + assertSuccess(res); + }); +}); diff --git a/test/mocked/options.prefix.js b/test/options.prefix.js similarity index 56% rename from test/mocked/options.prefix.js rename to test/options.prefix.js index 731299f..c08dee5 100644 --- a/test/mocked/options.prefix.js +++ b/test/options.prefix.js @@ -1,13 +1,23 @@ import 'should'; import request from 'supertest'; -import { odata, host, port, bookSchema } from '../support/setup'; -import FakeDb from '../support/fake-db'; +import { odata, host, port, assertSuccess } from './support/setup'; +import { BookMetadata } from './support/books.model'; describe('options.prefix', () => { - let httpServer, db; + let httpServer, server; + function initEntity(server) { + server.entity('book', { + list: (req, res, next) => { + res.$odata.result = []; + res.$odata.status = 200; + next(); + } + }, BookMetadata); + } before(() => { - db = new FakeDb(); + server = odata(); + initEntity(server); }); afterEach(() => { @@ -15,26 +25,28 @@ describe('options.prefix', () => { }); it('should be work', async function() { - const server = odata(db); - server.resource('book', bookSchema); server.set('prefix', '/api'); httpServer = server.listen(port); + const res = await request(host).get('/api/book'); - res.status.should.be.equal(200); + + assertSuccess(res); }); it('should be 200 when do not add `/`', async function() { - const server = odata(db); - server.resource('book', bookSchema); server.set('prefix', 'api'); httpServer = server.listen(port); + const res = await request(host).get('/api/book'); - res.status.should.be.equal(200); + + assertSuccess(res); }); it('should be 200 when set it at init-function', async function() { - const server = odata(db, '/api'); - server.resource('book', bookSchema); + server = odata('/api'); + initEntity(server); httpServer = server.listen(port); + const res = await request(host).get('/api/book'); - res.status.should.be.equal(200); + + assertSuccess(res); }); }); diff --git a/test/support/db.js b/test/support/db.js index af01bc2..6067865 100644 --- a/test/support/db.js +++ b/test/support/db.js @@ -5,6 +5,7 @@ import { conn } from '../support/setup'; export function init(server) { server.addBefore((req, res, next) => { req.$odata = { + ...req.$odata, mongo: mongoose.default.connection }; next(); From 89511a80fb063e05b969b570875516c41be2fb73 Mon Sep 17 00:00:00 2001 From: Richard Martens Date: Sun, 30 Jul 2023 22:00:13 +0200 Subject: [PATCH 34/64] Splitting of db and odata logik completed --- .vscode/launch.json | 2 +- Makefile | 12 +- src/middlewares/writer.js | 2 +- src/mongo/rest/put.js | 12 +- src/odata/entity/Entity.js | 1 - test/metadata.action.js | 3 +- test/mocked/rest.post.js | 31 --- test/mocked/rest.put.js | 53 ----- test/mongo/connected/rest.post.js | 4 + test/mongo/mocked/model.complex.filter.js | 5 + test/mongo/mocked/odata.query.orderby.js | 9 +- test/mongo/mocked/rest.get.js | 35 +-- test/mongo/mocked/rest.put.js | 86 ++++++++ test/{mocked => }/service.document.js | 9 +- test/support/fake-db-model.js | 256 ---------------------- test/support/fake-db.js | 24 -- test/support/setup.js | 33 +-- test/{mocked => }/utils.js | 2 +- 18 files changed, 146 insertions(+), 433 deletions(-) delete mode 100644 test/mocked/rest.post.js delete mode 100644 test/mocked/rest.put.js create mode 100644 test/mongo/mocked/rest.put.js rename test/{mocked => }/service.document.js (86%) delete mode 100644 test/support/fake-db-model.js delete mode 100644 test/support/fake-db.js rename test/{mocked => }/utils.js (96%) diff --git a/.vscode/launch.json b/.vscode/launch.json index cd2ae43..48f68fb 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -20,7 +20,7 @@ "dot", "--timeout", "300000", - "test/mongo/mocked/rest.delete.js" + "test/mongo/mocked/*.js" ], "env": { "LOG_LEVEL": "debug" diff --git a/Makefile b/Makefile index e365229..352fd72 100644 --- a/Makefile +++ b/Makefile @@ -14,12 +14,18 @@ test: @node_modules/.bin/mocha\ --require @babel/register \ --reporter $(REPORTER) \ - --exclude test/failing/**/*.js \ - test/**/*.js + --exclude test/failing/*.js \ + --exclude test/support/*.js \ + test/**/**/*.js \ + test/**/*.js \ + test/*.js test-cov: @node node_modules/istanbul/lib/cli.js cover -x '**/examples/**' \ - ./node_modules/mocha/bin/_mocha test/*.js -- \ + ./node_modules/mocha/bin/_mocha test/*.js test/**/*.js test/**/**/*.js -- \ --require @babel/register \ --reporter $(REPORTER) \ + --exclude test/failing/*.js \ + test/**/**/*.js \ + test/**/*.js \ test/*.js \ diff --git a/src/middlewares/writer.js b/src/middlewares/writer.js index 95360da..674c9bb 100644 --- a/src/middlewares/writer.js +++ b/src/middlewares/writer.js @@ -59,7 +59,7 @@ export default function writer(req, res) { switch (res.$odata.status) { case 404: // not found or no handler worked on - const err = new Error(); + const err = new Error('Not found'); err.status = 404; throw err; diff --git a/src/mongo/rest/put.js b/src/mongo/rest/put.js index 77d3198..51eba1f 100644 --- a/src/mongo/rest/put.js +++ b/src/mongo/rest/put.js @@ -1,9 +1,9 @@ function _updateEntity(req, res, next, entity) { - req.$odata.Model.findByIdAndUpdate(entity.id, req.body, (err) => { + req.$odata.Model.findByIdAndUpdate(entity.id, req.$odata.body, (err) => { if (err) { return next(err); } - const newEntity = req.body; + const newEntity = req.$odata.body; newEntity.id = entity.id; res.$odata.result = newEntity; @@ -14,14 +14,14 @@ function _updateEntity(req, res, next, entity) { function _createEntity(req, res, next) { const uuidReg = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i; - if (!uuidReg.test(req.params.id)) { + if (!uuidReg.test(req.$odata.$Key._id)) { const err = new Error('Id is invalid.'); err.status = 400; return next(err); } - const newEntity = MongooseModel.create(req.body); - newEntity._id = req.params.id; + const newEntity = new req.$odata.Model(req.$odata.body); + newEntity._id = req.$odata.$Key._id; return newEntity.save((err2) => { if (err2) { return next(err2); @@ -33,7 +33,7 @@ function _createEntity(req, res, next) { } export default (req, res, next) => { - req.$odata.Model.findOne({ _id: req.params.id }, (err, entity) => { + req.$odata.Model.findOne({ _id: req.$odata.$Key._id }, (err, entity) => { if (err) { return next(err); } diff --git a/src/odata/entity/Entity.js b/src/odata/entity/Entity.js index 5e829e4..4e2ef02 100644 --- a/src/odata/entity/Entity.js +++ b/src/odata/entity/Entity.js @@ -239,7 +239,6 @@ export default class Entity { next(err); } - debugger; res.$odata.result = { ['@odata.count']: countResponse.$odata.result }; diff --git a/test/metadata.action.js b/test/metadata.action.js index e952a9b..8c3cae9 100644 --- a/test/metadata.action.js +++ b/test/metadata.action.js @@ -3,8 +3,7 @@ import 'should'; import request from 'supertest'; -import { host, conn, port, odata, assertSuccess } from './support/setup'; -import FakeDb from './support/fake-db'; +import { host, port, odata, assertSuccess } from './support/setup'; describe('metadata.action', () => { let httpServer, server, db; diff --git a/test/mocked/rest.post.js b/test/mocked/rest.post.js deleted file mode 100644 index 822fd17..0000000 --- a/test/mocked/rest.post.js +++ /dev/null @@ -1,31 +0,0 @@ -import 'should'; -import request from 'supertest'; -import { odata, host, port, bookSchema } from '../support/setup'; -import FakeDb from '../support/fake-db'; - -describe('rest.post', () => { - let httpServer; - - before(async function() { - const db = new FakeDb(); - const server = odata(db); - server.resource('book', bookSchema) - httpServer = server.listen(port); - }); - - after(() => { - httpServer.close(); - }); - - it('should create new resource', async function() { - const res = await request(host) - .post(`/book`) - .send({ title: Math.random() }); - res.body.should.be.have.property('id'); - res.body.should.be.have.property('title'); - }); - it('should be 422 if post without data', async function() { - const res = await request(host).post(`/book`); - res.status.should.be.equal(422); - }); -}); diff --git a/test/mocked/rest.put.js b/test/mocked/rest.put.js deleted file mode 100644 index 6df40fe..0000000 --- a/test/mocked/rest.put.js +++ /dev/null @@ -1,53 +0,0 @@ -import * as uuid from 'uuid'; -import 'should'; -import request from 'supertest'; -import { odata, host, port, bookSchema } from '../support/setup'; -import books from '../support/books.json'; -import FakeDb from '../support/fake-db'; - -describe('rest.put', () => { - let data, httpServer; - - before(async function() { - const db = new FakeDb(); - const server = odata(db); - server.resource('book', bookSchema) - httpServer = server.listen(port); - data = db.addData('book', books); - }); - - after(() => { - httpServer.close(); - }); - - it('should modify resource', async function() { - const book = data[0]; - book.title = 'modify book'; - const res = await request(host) - .put(`/book(${book.id})`) - .send(book); - res.body.should.be.have.property('title'); - res.body.title.should.be.equal(book.title); - }); - it('should create resource if send with a id which not exist', async function() { - const book = { - id: uuid.v4(), - title: 'new book', - }; - const res = await request(host) - .put(`/book(${book.id})`) - .send({ title: book.title }); - res.body.should.be.have.property('title'); - res.body.title.should.be.equal(book.title); - res.body.should.be.have.property('id'); - res.body.id.should.be.equal(book.id); - }); - it('should be 404 if without id', async function() { - const res = await request(host).put(`/book`).send(data[0]); - res.status.should.be.equal(404); - }); - it("should 400 if with a wrong id", async function() { - const res = await request(host).put(`/book(wrong-id)`).send(data[0]); - res.status.should.be.equal(400); - }); -}); diff --git a/test/mongo/connected/rest.post.js b/test/mongo/connected/rest.post.js index 8041cf2..af03e03 100644 --- a/test/mongo/connected/rest.post.js +++ b/test/mongo/connected/rest.post.js @@ -30,4 +30,8 @@ describe('rest.post', () => { res.body.should.be.have.property('id'); res.body.should.be.have.property('title'); }); + it('should be 422 if post without data', async function() { + const res = await request(host).post(`/book`); + res.status.should.be.equal(422); + }); }); diff --git a/test/mongo/mocked/model.complex.filter.js b/test/mongo/mocked/model.complex.filter.js index 2b97b37..c31661b 100644 --- a/test/mongo/mocked/model.complex.filter.js +++ b/test/mongo/mocked/model.complex.filter.js @@ -51,6 +51,11 @@ describe('model.complex.filter', () => { httpServer.close(); }); + afterEach(() => { + modelMock.restore(); + queryMock?.restore(); + }) + it('should work when filter a complex entity', async function () { let res = await request(host).get(`/complex-model-filter?$select=product&$filter=product.price gt 30`); assertSuccess(res); diff --git a/test/mongo/mocked/odata.query.orderby.js b/test/mongo/mocked/odata.query.orderby.js index f4b86ed..f39b0aa 100644 --- a/test/mongo/mocked/odata.query.orderby.js +++ b/test/mongo/mocked/odata.query.orderby.js @@ -16,6 +16,7 @@ describe('odata.query.orderby', () => { model: BookModel }; let httpServer, modelMock, queryMock; + const dataIsolated = JSON.parse(JSON.stringify(data)); before(async function() { const server = odata(); @@ -35,7 +36,7 @@ describe('odata.query.orderby', () => { }); it('should default let items order with asc', async function() { - const books = data.sort((a, b) => a.price - b.price); + const books = dataIsolated.sort((a, b) => a.price - b.price); modelMock = sinon.mock(BookModel); queryMock = sinon.mock(query); @@ -54,7 +55,7 @@ describe('odata.query.orderby', () => { }); it('should let items order asc', async function() { - const books = data.sort((a, b) => a.price - b.price); + const books = dataIsolated.sort((a, b) => a.price - b.price); modelMock = sinon.mock(BookModel); queryMock = sinon.mock(query); @@ -73,7 +74,7 @@ describe('odata.query.orderby', () => { }); it('should let items order desc', async function() { - const books = data.sort((a, b) => b.price - a.price); + const books = dataIsolated.sort((a, b) => b.price - a.price); modelMock = sinon.mock(BookModel); queryMock = sinon.mock(query); @@ -92,7 +93,7 @@ describe('odata.query.orderby', () => { }); it('should let items order when use multiple fields', async function() { - const books = data.sort((a, b) => { + const books = dataIsolated.sort((a, b) => { const result = a.price - b.price; if (!result) { diff --git a/test/mongo/mocked/rest.get.js b/test/mongo/mocked/rest.get.js index 66ee09b..f569369 100644 --- a/test/mongo/mocked/rest.get.js +++ b/test/mongo/mocked/rest.get.js @@ -1,6 +1,6 @@ import 'should'; import request from 'supertest'; -import { odata, host, port, bookSchema } from '../../support/setup'; +import { odata, host, port } from '../../support/setup'; import { BookModel } from '../../support/books.model'; import mongoose from 'mongoose'; import sinon from 'sinon'; @@ -21,7 +21,7 @@ describe('rest.get', () => { model: BookModel }; - let httpServer, modelMock, queryMock, complexQuery; + let httpServer, modelMock, queryMock, ComplexModel; before(async function() { const server = odata(); @@ -35,12 +35,8 @@ describe('rest.get', () => { }); mongoose.set('overwriteModels', true); - const ComplexModel = mongoose.model('complex-type', ComplexModelSchema); + ComplexModel = mongoose.model('complex-type', ComplexModelSchema); - complexQuery = { - ...query, - model: ComplexModel - }; server.mongoEntity('complex-type', ComplexModel); httpServer = server.listen(port); @@ -70,25 +66,38 @@ describe('rest.get', () => { }); it('should return special resource', async function() { modelMock = sinon.mock(BookModel); - queryMock = sinon.mock(bookQuery); modelMock.expects('findById').once().withArgs('1') - .callsArgWith(1, null, {toObject: () => ({title: 'Krieg und Frieden'})}) + .callsArgWith(1, null, {toObject: () => ({title: 'Krieg und Frieden'})}); const res = await request(host).get(`/book('1')`); res.body.should.be.have.property('title'); res.body.title.should.be.equal('Krieg und Frieden'); + modelMock.verify(); }); it('should be 404 if resouce name not declare', async function() { const res = await request(host).get(`/not-exist-resource`); res.status.should.be.equal(404); }); it('should be 404 if resource not exist', async function() { - const res = await request(host).get(`/book(not-exist-id)`); + modelMock = sinon.mock(BookModel); + queryMock = sinon.mock(bookQuery); + modelMock.expects('findById').once().withArgs('1') + .callsArgWith(1, null, null) + + const res = await request(host).get(`/book('1')`); res.status.should.be.equal(404); + modelMock.verify(); }); - it('should replace a dot in property names with -', async function() { - const res = await request(host).get(`/complex-type(${cdata[0].id})`); - res.body.should.be.have.property('p1-p2'); + it('should return deep structure', async function() { + modelMock = sinon.mock(ComplexModel); + modelMock.expects('findById').once().withArgs('1') + .callsArgWith(1, null, {toObject: () => ({p1:{p2: 'Krieg und Frieden'}})}) + + const res = await request(host).get(`/complex-type('1')`); + res.body.should.be.have.property('p1'); + res.body.p1.should.be.have.property('p2'); + res.body.p1.p2.should.be.equal('Krieg und Frieden'); + modelMock.verify(); }); }); diff --git a/test/mongo/mocked/rest.put.js b/test/mongo/mocked/rest.put.js new file mode 100644 index 0000000..3f23e4e --- /dev/null +++ b/test/mongo/mocked/rest.put.js @@ -0,0 +1,86 @@ +import 'should'; +import request from 'supertest'; +import { odata, host, port } from '../../support/setup'; +import books from '../../support/books.json'; +import { BookModel } from '../../support/books.model'; +import sinon from 'sinon'; + +describe('rest.put', () => { + let httpServer, modelMock, bookInstanceMock; + + before(async function () { + const server = odata(); + + server.mongoEntity('book', BookModel); + httpServer = server.listen(port); + }); + + after(() => { + httpServer.close(); + }); + + beforeEach(() => { + modelMock?.restore(); + bookInstanceMock?.restore(); + }); + + it('should modify resource', async function () { + const book = JSON.parse(JSON.stringify(books[0])); + + debugger; + + book.title = 'modify book'; + modelMock = sinon.mock(BookModel); + modelMock.expects('findOne').once().withArgs({_id: '1'}) + .callsArgWith(1, null, (JSON.parse(JSON.stringify(books[0])))); + modelMock.expects('findByIdAndUpdate').once().withArgs('1', book) + .callsArgWith(2, null); + + const res = await request(host) + .put(`/book('${book.id}')`) + .send(book); + + res.body.should.be.have.property('title'); + res.body.title.should.be.equal(book.title); + modelMock.verify(); + }); + it('should create resource if send with a id which not exist', async function () { + const book = JSON.parse(JSON.stringify(books[0])); + + book.id = '12345678-1234-1234-A123-123456789012'; + book.title = 'new book'; + modelMock = sinon.mock(BookModel); + modelMock.expects('findOne').once().withArgs({_id: book.id}) + .callsArgWith(1, null, null); + // mocking save method of created with new instance + bookInstanceMock = sinon.mock(BookModel.prototype); + bookInstanceMock.expects('save').once() + .callsArgWith(0, null); + + const res = await request(host) + .put(`/book('${book.id}')`) + .send({ title: book.title }); + + res.body.should.be.have.property('title'); + res.body.title.should.be.equal(book.title); + res.body.should.be.have.property('id'); + modelMock.verify(); + }); + it('should be 404 if without id', async function () { + const res = await request(host).put(`/book`).send(books[0]); + res.status.should.be.equal(404); + }); + it("should 400 if with a wrong id", async function () { + const book = JSON.parse(JSON.stringify(books[0])); + + book.title = 'new book'; + modelMock = sinon.mock(BookModel); + modelMock.expects('findOne').once().withArgs({_id: book.id}) + .callsArgWith(1, null, null); + + const res = await request(host).put(`/book('1')`).send(books[0]); + + res.status.should.be.equal(400); + modelMock.verify(); + }); +}); diff --git a/test/mocked/service.document.js b/test/service.document.js similarity index 86% rename from test/mocked/service.document.js rename to test/service.document.js index f50dec9..ab90665 100644 --- a/test/mocked/service.document.js +++ b/test/service.document.js @@ -1,7 +1,7 @@ import 'should'; import request from 'supertest'; -import { host, port, bookSchema, odata, assertSuccess } from '../support/setup'; -import FakeDb from '../support/fake-db'; +import { host, port, odata, assertSuccess } from './support/setup'; +import { BookMetadata } from './support/books.model'; describe('service.document', () => { let httpServer, server, db; @@ -15,9 +15,8 @@ describe('service.document', () => { }] }; beforeEach(async function() { - db = new FakeDb(); - server = odata(db); - server.resource('book', bookSchema); + server = odata(); + server.entity('book', null, BookMetadata); }); diff --git a/test/support/fake-db-model.js b/test/support/fake-db-model.js deleted file mode 100644 index 53e2c5c..0000000 --- a/test/support/fake-db-model.js +++ /dev/null @@ -1,256 +0,0 @@ -export default class Model { - constructor(name, model) { - this._name = name; - this._data = []; - this._count = 1; - this.model = { - schema: { - ...model, - tree: { - id: { select: true }, - ...model - }, - paths: Model.toPath(model) - } - }; - } - - static toPath(model, prefix) { - let result = {}; - - Object.keys(model).forEach((item) => { - const propName = prefix ? `${prefix}.${item}` : item; - - if (Array.isArray(model[item])) { - result[propName] = { - path: propName, - instance: 'Array' - }; - if (model[item][0].name) { - // Array of primitive Types - result[propName].options = { - type: [{ - name: model[item][0].name - }] - }; - } else if(model[item][0].enum) { - // Array of Enum values - result[propName].options = { - type: [{ - type: { - name: model[item][0].type.name - }, - enum: model[item][0].enum - }] - }; - } else { - // Array of objects - result[propName].schema = { - paths: Model.toPath(model[item][0]) - }; - } - } else if (typeof model[item] === 'object') { - if (model[item].type) { - // structured property e.g. author: { type: String } - result[propName] = { - path: propName, - instance: model[item].type.name - }; - if (model[item].maxLength) { - result[propName].maxlengthValidator = () => {}; - } - } else { - const subSchema = Model.toPath(model[item], propName); - - result = { - ...result, - ...subSchema - }; - } - } else { - result[propName] = { - path: propName, - instance: model[item].name - }; - } - }); - - return result; - } - - addData(data) { - this._data = data.map(item => ({ - ...item, - id: (this._count++).toString() - })); - - return this._data; - } - - create(data) { - const newItem = { - ...data, - id: data.id || (this._count++).toString(), - save: callback => { - const result = this._data.find(item => item.id === newItem.id); - - if (result._id) { - result.id = result._id; - } - callback(); - } - }; - - this._data.push(newItem); - - return newItem; - } - - update(params, data, callback) { - const item = this._data.find(item => item.id === params.id); - - if (item) { - Object.keys(data).forEach(propName => { - item[propName] = data[propName]; - }); - callback(undefined, item); - } else { - const error = new Error('Not found'); - - error.status = 404; - callback(error); - } - } - - exec(callback) { - callback(null, this._data); - } - - find() { - return this; - } - - findById(id, callback) { - let idInternal = 0; - - switch (this.model.schema.id) { - case Number: - idInternal = +id; - break; - - default: - idInternal = id; - break; - } - - let result = this._data.find(item => item.id === idInternal); - - if (result) { - result.save = (callback) => { - callback(); - }; - } - callback(null, result); - } - - findByIdAndUpdate(id, data, callback) { - const result = this._data.find(item => item.id === id); - const index = this._data.indexOf(result); - - this._data[index] = { - ...result, - ...data - }; - callback(); - - } - - findOne(filter, callback) { - const result = this._data.find(item => item.id === filter._id); - callback(null, result); - } - - remove(filter, callback) { - const toDelete = this._data.find(item => item.id === filter._id); - const deleteItem = item => { - const index = this._data.indexOf(item); - this._data.splice(index, 1); - }; - - if (toDelete) { - if (Array.isArray(toDelete)) { - toDelete.forEach(deleteItem); - callback(null, JSON.stringify({ n: toDelete.length })); - } else { - deleteItem(toDelete); - callback(null, JSON.stringify({ n: 1 })); - } - } else { - callback(null, JSON.stringify({ n: 0 })); - } - } - - limit() { - return this; - } - - select() { - return this; - } - - skip() { - return this; - } - - sort() { - return this; - } - - where() { - return this; - } - - $where(value) { - return this; - } - - gt() { - return this; - } - - gte() { - return this; - } - - lt() { - return this; - } - - lte() { - return this; - } - - equals() { - return this; - } - - ne() { - return this; - } - - exists() { - return this; - } - - or() { - return this; - } - - and() { - return this; - } - - count(callback) { - callback(null, this._data.length); - } -} \ No newline at end of file diff --git a/test/support/fake-db.js b/test/support/fake-db.js deleted file mode 100644 index 2f088d0..0000000 --- a/test/support/fake-db.js +++ /dev/null @@ -1,24 +0,0 @@ -import Model from './fake-db-model'; - -export default class { - constructor() { - this._models = {}; - } - - addData(name, data) { - return this._models[name].addData(data); - } - - createConnection() { - return this; - } - - register(name, model) { - this._models[name] = new Model(name, model); - return this._models[name]; - } - - on(name, event) { - - } -} \ No newline at end of file diff --git a/test/support/setup.js b/test/support/setup.js index c1fa235..e4e11d3 100644 --- a/test/support/setup.js +++ b/test/support/setup.js @@ -6,43 +6,12 @@ export const host = 'http://localhost:3000'; export const port = '3000'; export const conn = 'mongodb://localhost/odata-test'; -const { BookShema, BookModel } = require('./books.model'); +const { BookModel } = require('./books.model'); export const model = BookModel; export const books = require('./books.json'); -export function initData() { - return new Promise((resolve, reject) => { - const conf = { - _id: false, - versionKey: false, - collection: 'book', - }; - - const db = mongoose.createConnection(conn); - const schema = new mongoose.Schema(bookSchema, conf); - schema.plugin(id); - const model = db.model('book', schema); - - function clear() { - return new Promise((resolve) => { - model.remove({}, resolve); - }); - } - - function insert(item) { - return new Promise((resolve) => { - const entity = new model(item); - entity.save((err, result) => resolve(result)); - }); - } - - const promises = books.map(insert); - clear().then(() => Promise.all(promises).then(resolve)); - }); -} - export function assertSuccess(res) { if (res.error) { res.error.message.should.have.value(''); diff --git a/test/mocked/utils.js b/test/utils.js similarity index 96% rename from test/mocked/utils.js rename to test/utils.js index 1608f66..9f3b6ca 100644 --- a/test/mocked/utils.js +++ b/test/utils.js @@ -1,5 +1,5 @@ import 'should'; -import { min, split } from '../../lib/utils'; +import { min, split } from '../lib/utils'; describe('min', () => { return it('should work', () => { From 03675c5bcb39f3b581244b1ff842ab6d000fe00b Mon Sep 17 00:00:00 2001 From: Richard Martens Date: Fri, 4 Aug 2023 06:19:07 +0200 Subject: [PATCH 35/64] cleaning and some bug fixes --- .vscode/launch.json | 2 +- Makefile | 2 +- src/db/db.js | 40 --------------- src/db/idPlugin.js | 45 ---------------- src/db/model.js | 38 -------------- src/express.js | 5 +- src/index.js | 1 - src/model/idPlugin.js | 55 -------------------- src/model/index.js | 15 ------ src/odata/Batch.js | 69 +++++++++++++++++++------ src/odata/entity/Entity.js | 10 ++-- src/server.js | 14 +++-- test/mongo/mocked/rest.put.js | 2 - test/odata.batch.js | 97 +++++++++++++++++++++++++++++++++++ 14 files changed, 168 insertions(+), 227 deletions(-) delete mode 100644 src/db/db.js delete mode 100644 src/db/idPlugin.js delete mode 100644 src/db/model.js delete mode 100644 src/model/idPlugin.js delete mode 100644 src/model/index.js diff --git a/.vscode/launch.json b/.vscode/launch.json index 48f68fb..c740a88 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -20,7 +20,7 @@ "dot", "--timeout", "300000", - "test/mongo/mocked/*.js" + "test/odata.batch.js" ], "env": { "LOG_LEVEL": "debug" diff --git a/Makefile b/Makefile index 352fd72..8e6c332 100644 --- a/Makefile +++ b/Makefile @@ -21,7 +21,7 @@ test: test/*.js test-cov: - @node node_modules/istanbul/lib/cli.js cover -x '**/examples/**' \ + @node node_modules/istanbul/lib/cli.js cover -x '**/examples/**' -x '**/lib/**' \ ./node_modules/mocha/bin/_mocha test/*.js test/**/*.js test/**/**/*.js -- \ --require @babel/register \ --reporter $(REPORTER) \ diff --git a/src/db/db.js b/src/db/db.js deleted file mode 100644 index 8e72aea..0000000 --- a/src/db/db.js +++ /dev/null @@ -1,40 +0,0 @@ -import mongoose from 'mongoose'; -import Model from './model'; -import id from './idPlugin'; - -mongoose.Promise = global.Promise; - -export default class { - constructor() { - this._models = {}; - } - - createConnection(connection, options, onError) { - this._connection = mongoose.createConnection(connection, options, onError); - - return this._connection; - } - - closeConnection() { - this._connection.close(); - delete this._connection; - } - - on(name, event) { - this._connection.on(name, event); - } - - register(name, model, options) { - const conf = { - _id: false, - versionKey: false, - collection: name, - ...options - }; - const schema = new mongoose.Schema(model, conf); - schema.plugin(id); - const mongooseModel = this._connection.model(name, schema); - this._models[name] = new Model(mongooseModel); - return this._models[name]; - } -} diff --git a/src/db/idPlugin.js b/src/db/idPlugin.js deleted file mode 100644 index 7186918..0000000 --- a/src/db/idPlugin.js +++ /dev/null @@ -1,45 +0,0 @@ -import * as uuid from 'uuid'; - -/*eslint-disable */ -export default function (schema) { - // display value of _id when request id. - if (!schema.paths.id) { - schema.virtual('id').get(function getId() { - return this._doc._id; - }); - schema.set('toObject', { virtuals: true }); - schema.set('toJSON', { virtuals: true }); - } - - // reomove _id when serialization. - if (!schema.options.toObject) { - schema.options.toObject = {}; - } - if (!schema.options.toJSON) { - schema.options.toJSON = {}; - } - const remove = (doc, ret) => { - delete ret._id; - if (!ret.id) { - delete ret.id; - } - return ret; - }; - schema.options.toObject.transform = remove; - schema.options.toJSON.transform = remove; - - // genarate _id. - schema.pre('save', function preSave(next) { - if (this.isNew && !this._doc._id) { - if (this.id) { - // Use a user-defined id to save - this._doc._id = this.id; - } else { - // Use uuid to save - this._doc._id = uuid.v4(); - } - } - return next(); - }); -} -/* eslint-enable */ diff --git a/src/db/model.js b/src/db/model.js deleted file mode 100644 index ae636d5..0000000 --- a/src/db/model.js +++ /dev/null @@ -1,38 +0,0 @@ -export default class { - constructor(mongooseModel) { - this.model = mongooseModel; - } - - create(data) { - const MongooseModel = this.model; - return new MongooseModel(data); - } - - async countDocuments() { - return await this.model.countDocuments(); - } - - find() { - return this.model.find(); - } - - async findById(id, callback) { - return await this.model.findById(id, callback); - } - - async findByIdAndUpdate(id, data, callback) { - return await this.model.findByIdAndUpdate(id, data, callback); - } - - async findOne(filter, callback) { - return await this.model.findOne(filter, callback); - } - - async remove(filter, callback) { - return await this.model.remove(filter, callback); - } - - async update(filter, data, callback) { - return await this.model.update(filter, data, callback); - } -} diff --git a/src/express.js b/src/express.js index 153699b..b593306 100644 --- a/src/express.js +++ b/src/express.js @@ -1,18 +1,15 @@ import express from 'express'; -import bodyParser from 'body-parser'; import methodOverride from 'method-override'; import cors from 'cors'; -import multipart from './parser/multipartMixed'; +import bodyParser from 'body-parser'; export default function orientExpress(options) { const app = express(); const opts = (options && options.expressRequestLimit) ? { limit: options.expressRequestLimit } : {}; - app.use(bodyParser.json(opts)); opts.extended = true; app.use(bodyParser.urlencoded(opts)); - app.use(multipart); app.use(methodOverride()); app.use(express.query()); app.use(cors(options && options.corsOptions)); diff --git a/src/index.js b/src/index.js index 9580799..8437490 100644 --- a/src/index.js +++ b/src/index.js @@ -1,6 +1,5 @@ import express from 'express'; import Server from './server'; -import mulpipartMixed from './parser/multipartMixed' export const odata = function server(db, prefix, options) { return new Server(db, prefix, options); diff --git a/src/model/idPlugin.js b/src/model/idPlugin.js deleted file mode 100644 index 10f6881..0000000 --- a/src/model/idPlugin.js +++ /dev/null @@ -1,55 +0,0 @@ -import * as uuid from 'uuid'; - -/*eslint-disable */ -export default function (schema) { - // add _id to schema. - if (!schema.paths._id) { - schema.add({ - _id: { - type: String, - unique: true, - } - }); - } - - // display value of _id when request id. - if (!schema.paths.id) { - schema.virtual('id').get(function getId() { - return this._id; - }); - schema.set('toObject', { virtuals: true }); - schema.set('toJSON', { virtuals: true }); - } - - // reomove _id when serialization. - if (!schema.options.toObject) { - schema.options.toObject = {}; - } - if (!schema.options.toJSON) { - schema.options.toJSON = {}; - } - const remove = (doc, ret) => { - delete ret._id; - if (!ret.id) { - delete ret.id; - } - return ret; - }; - schema.options.toObject.transform = remove; - schema.options.toJSON.transform = remove; - - // genarate _id. - schema.pre('save', function preSave(next) { - if (this.isNew && !this._id) { - if (this.id) { - // Use a user-defined id to save - this._id = this.id; - } else { - // Use uuid to save - this._id = uuid.v4(); - } - } - return next(); - }); -} -/* eslint-enable */ diff --git a/src/model/index.js b/src/model/index.js deleted file mode 100644 index 5cbb21b..0000000 --- a/src/model/index.js +++ /dev/null @@ -1,15 +0,0 @@ -import mongoose from 'mongoose'; -import id from './idPlugin'; - -const register = (_db, name, model) => { - const conf = { - _id: false, - versionKey: false, - collection: name, - }; - const schema = new mongoose.Schema(model, conf); - schema.plugin(id); - return _db.model(name, schema); -}; - -export default { register }; diff --git a/src/odata/Batch.js b/src/odata/Batch.js index 7355a88..a34c706 100644 --- a/src/odata/Batch.js +++ b/src/odata/Batch.js @@ -1,7 +1,6 @@ import { Router } from 'express'; import error from '../middlewares/error'; import { STATUS_CODES } from 'http'; -import writer from '../middlewares/writer'; export default class Batch { constructor(server) { @@ -81,38 +80,73 @@ export default class Batch { async executeSingleRequest(handler, req, res) { try { + let promise = null; + for (let i = 0; i < this._server.hooks.before.length; ++i) { const hook = this._server.hooks.before[i]; - await hook(req, res, err => { - if (err) { - throw err; - } - }); + if (promise) { + promise = promise.then(() => { + return hook(req, res, err => { + if (err) { + throw err; + } + }); + }); + } else { + promise = hook(req, res, err => { + if (err) { + throw err; + } + }); + } + } for (let i = 0; i < handler.length; ++i) { const handlerOrHook = handler[i]; - await handlerOrHook(req, res, err => { - if (err) { - throw err; - } - }); + if (promise) { + promise = promise.then(() => { + return handlerOrHook(req, res, err => { + if (err) { + throw err; + } + }); + }); + } else { + promise = handlerOrHook(req, res, err => { + if (err) { + throw err; + } + }); + } } for(let i = 0; i < this._server.hooks.after.length; ++i) { const hook = this._server.hooks.after[i]; if (hook !== error) { - await hook(req, res, err => { - if (err) { - throw err; - } - }); + if (promise) { + promise = promise.then(() => { + return hook(req, res, err => { + if (err) { + throw err; + } + }); + }); + } else { + promise = hook(req, res, err => { + if (err) { + throw err; + } + }); + } } } + await promise; + } catch (err) { error(err, req, res); } @@ -131,7 +165,8 @@ export default class Batch { result.id = request.id; } const currentRequest = { - headers: request.headers, + headers: request.headers || {}, + method: request.method, query: Batch.mapToQuery(request.url), body: request.body, $odata: res.$odata diff --git a/src/odata/entity/Entity.js b/src/odata/entity/Entity.js index 4e2ef02..e5ff181 100644 --- a/src/odata/entity/Entity.js +++ b/src/odata/entity/Entity.js @@ -81,7 +81,7 @@ export default class Entity { } }); - return [ + return route && [ this.parsingMiddleware.bind(this), ...this.hooks.before, this.ctrl(route.name, this.handler[route.name]), @@ -227,14 +227,14 @@ export default class Entity { } ctrl(name, handler) { - return (req, res, next) => { + return async (req, res, next) => { res.$odata.status = 200; if (name === 'list' && req.$odata.$count) { const countResponse = { $odata: {} }; - this.handler.count(req, countResponse, err => { + this.handler.count(req, countResponse, async err => { if (err) { next(err); } @@ -242,11 +242,11 @@ export default class Entity { res.$odata.result = { ['@odata.count']: countResponse.$odata.result }; - handler(req, res, next); + await handler(req, res, next); }); } else { - handler(req, res, next); + await handler(req, res, next); } }; diff --git a/src/server.js b/src/server.js index a1bb7ab..af81ff5 100644 --- a/src/server.js +++ b/src/server.js @@ -1,4 +1,5 @@ import createExpress from './express'; +import bodyParser from 'body-parser'; import MongoEntity from './mongo/Entity'; import Entity from './odata/entity/Entity'; import Func from './ODataFunction'; @@ -9,6 +10,7 @@ import Action from './odata/Action'; import error from './middlewares/error'; import writer from './middlewares/writer'; import Hooks from './odata/Hooks'; +import multipartMixed from './parser/multipartMixed'; function checkAuth(auth, req) { return !auth || auth(req); @@ -16,6 +18,9 @@ function checkAuth(auth, req) { class Server { constructor(prefix, options) { + const opts = (options && options.expressRequestLimit) + ? { limit: options.expressRequestLimit } : {}; + this._app = createExpress(options); this._settings = { maxTop: 10000, @@ -37,7 +42,8 @@ class Server { //unbound actions this.actions = {}; - + this.hooks.addBefore(multipartMixed); + this.hooks.addBefore(bodyParser.json(opts)); this.hooks.addBefore(async (req, res) => { req.$odata = { $metadata: this.resources.$metadata @@ -82,13 +88,15 @@ class Server { } mongoEntity(name, model, handler, metadata, settings, mapping) { - if (model === undefined) { + if (name && !model) { + if (!this.resources[name]) { + throw new Error(`Entity '${name}' is not defined`); + } return this.resources[name]; } const entity = new MongoEntity(name, model); - //this.resources[name].setModel(db.register(name, model)); const complexTypes = entity.getComplexTypes(); if (complexTypes) { diff --git a/test/mongo/mocked/rest.put.js b/test/mongo/mocked/rest.put.js index 3f23e4e..2e94f52 100644 --- a/test/mongo/mocked/rest.put.js +++ b/test/mongo/mocked/rest.put.js @@ -27,8 +27,6 @@ describe('rest.put', () => { it('should modify resource', async function () { const book = JSON.parse(JSON.stringify(books[0])); - debugger; - book.title = 'modify book'; modelMock = sinon.mock(BookModel); modelMock.expects('findOne').once().withArgs({_id: '1'}) diff --git a/test/odata.batch.js b/test/odata.batch.js index a1f7ea5..b85b1f0 100644 --- a/test/odata.batch.js +++ b/test/odata.batch.js @@ -96,6 +96,42 @@ describe('odata.batch', () => { }); }); + it('should work with get one of multiple entity types', async function () { + const server = odata(); + + server.entity('affe', null, BookMetadata); + + server.entity('book', { + get: (req, res, next) => { + req.$odata.$Key.id.should.be.equal(books[0].id); + res.$odata.result = books[0]; + next(); + } + }, BookMetadata); + httpServer = server.listen(port); + + const res = await request(host).post(`/$batch`).send({ + requests: [{ + id: "1", + method: "get", + url: `/book('${books[0].id}')` + }] + }); + assertSuccess(res); + res.body.should.deepEqual({ + responses: [{ + id: "1", + status: 200, + statusText: 'OK', + headers: { + 'OData-Version': '4.0', + 'content-type': 'application/json' + }, + body: books[0] + }] + }); + }); + it('should work with post entity', async function () { const result = { title: "War and peace" @@ -320,6 +356,67 @@ describe('odata.batch', () => { }] }); }); + + it('Calls to bevore hooks should be done synchronously', async function () { + const server = odata(); + const resource = server.entity('book', { + delete: (req, res, next) => { + req.$odata.$Key.id.should.be.equal('1'); + res.$odata.status = 204; + next(); + } + }, BookMetadata); + + resource.addBefore([(req, res, next) => { + res.$odata.should.not.have.property('result'); + res.$odata.result = 0; + next(); + },(req, res, next) => { + res.$odata.result.should.be.equal(0); + res.$odata.result = 1; + next(); + }, async (req, res, next) => { + res.$odata.result.should.be.equal(1); + + const promise = new Promise((resolve, reject) => { + setTimeout(() => { + resolve(2); + }, 0); + }); + res.$odata.result = await promise; + }, async (req, res, next) => { + res.$odata.result.should.be.equal(2); + + const promise = new Promise((resolve, reject) => { + setTimeout(() => { + resolve(3); + }, 0); + }); + res.$odata.result = await promise; + next(); + }, (req, res, next) => { + res.$odata.result.should.be.equal(3); + next(); + }]) + + httpServer = server.listen(port); + + const res = await request(host).post(`/$batch`).send({ + requests: [{ + id: "1", + method: "delete", + url: `/book('1')` + }] + }); + assertSuccess(res); + res.body.should.deepEqual({ + responses: [{ + id: "1", + status: 204, + statusText: "No Content" + }] + }); + }); /* it('should work with multipart request body', async function () { const result = { From 95759a618ac307c5ad555f8ac59d25e6a99417b5 Mon Sep 17 00:00:00 2001 From: Richard Martens Date: Fri, 4 Aug 2023 07:25:04 +0200 Subject: [PATCH 36/64] [b] 501 entity and 0 writer bug fixed --- .vscode/launch.json | 2 +- src/middlewares/writer.js | 2 +- src/odata/entity/Entity.js | 7 +++++++ 3 files changed, 9 insertions(+), 2 deletions(-) diff --git a/.vscode/launch.json b/.vscode/launch.json index c740a88..0a309e5 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -20,7 +20,7 @@ "dot", "--timeout", "300000", - "test/odata.batch.js" + "test/odata.entity.js" ], "env": { "LOG_LEVEL": "debug" diff --git a/src/middlewares/writer.js b/src/middlewares/writer.js index 674c9bb..f0cc212 100644 --- a/src/middlewares/writer.js +++ b/src/middlewares/writer.js @@ -71,7 +71,7 @@ export default function writer(req, res) { throw new Error('Status not setted'); default: - if (!res.$odata.result) { + if (res.$odata.result === undefined) { throw new Error('If status not equal 204 res.$odata.result has to be set'); } } diff --git a/src/odata/entity/Entity.js b/src/odata/entity/Entity.js index e5ff181..6bcdf22 100644 --- a/src/odata/entity/Entity.js +++ b/src/odata/entity/Entity.js @@ -228,7 +228,9 @@ export default class Entity { ctrl(name, handler) { return async (req, res, next) => { + try { res.$odata.status = 200; + if (name === 'list' && req.$odata.$count) { const countResponse = { $odata: {} @@ -237,6 +239,7 @@ export default class Entity { this.handler.count(req, countResponse, async err => { if (err) { next(err); + return; } res.$odata.result = { @@ -249,6 +252,10 @@ export default class Entity { await handler(req, res, next); } + + } catch(err) { + next(err); + } }; } From 1b80d804d58c8cb46a73d242fe082a937fb2764b Mon Sep 17 00:00:00 2001 From: Richard Martens Date: Fri, 4 Aug 2023 11:30:35 +0200 Subject: [PATCH 37/64] first steps of singleton --- src/odata/Metadata.js | 14 ++- src/odata/ServiceDocument.js | 13 +- src/odata/entity/Entity.js | 2 +- src/odata/entity/Singleton.js | 115 ++++++++++++++++++ src/server.js | 13 +- src/writer/xmlWriter.js | 14 ++- .../action.js} | 4 +- .../complex.type.js} | 5 +- .../custom.resource.js} | 4 +- .../format.js} | 5 +- .../function.js} | 5 +- test/metadata/singleton.js | 78 ++++++++++++ .../entity.js} | 15 +-- test/service.document/singleton.js | 38 ++++++ test/singleton.js | 41 +++++++ test/support/checkContentType.js | 6 + 16 files changed, 334 insertions(+), 38 deletions(-) create mode 100644 src/odata/entity/Singleton.js rename test/{metadata.action.js => metadata/action.js} (98%) rename test/{metadata.complex.type.js => metadata/complex.type.js} (91%) rename test/{metadata.js => metadata/custom.resource.js} (92%) rename test/{metadata.format.js => metadata/format.js} (94%) rename test/{metadata.function.js => metadata/function.js} (91%) create mode 100644 test/metadata/singleton.js rename test/{service.document.js => service.document/entity.js} (79%) create mode 100644 test/service.document/singleton.js create mode 100644 test/singleton.js create mode 100644 test/support/checkContentType.js diff --git a/src/odata/Metadata.js b/src/odata/Metadata.js index 123176e..ba311a6 100644 --- a/src/odata/Metadata.js +++ b/src/odata/Metadata.js @@ -2,6 +2,7 @@ import { Router } from 'express'; import Entity from './entity/Entity'; import Function from '../ODataFunction'; import { validate, validateIdentifier } from './validator'; +import Singleton from './entity/Singleton'; export default class Metadata { constructor(server) { @@ -92,6 +93,9 @@ export default class Metadata { }); } + } if (resource instanceof Singleton && !result[currentResource]) { + result[currentResource] = resource.getMetadata(); + } else if (resource instanceof Function) { result[currentResource] = this.visitor('Function', resource); } @@ -104,11 +108,15 @@ export default class Metadata { const result = { ...previousResource }; const resource = this._server.resources[currentResource]; - if (resource instanceof Entity) { + if (resource instanceof Entity || resource instanceof Singleton) { result[currentResource] = { - $Collection: true, - $Type: `node.odata.${currentResource}`, + $Type: `node.odata.${currentResource}` }; + + if(resource instanceof Entity) { + result[currentResource].$Collection = true; + } + } else if (resource instanceof Function) { result[currentResource] = { $Function: `node.odata.${currentResource}`, diff --git a/src/odata/ServiceDocument.js b/src/odata/ServiceDocument.js index 4343def..5b5647f 100644 --- a/src/odata/ServiceDocument.js +++ b/src/odata/ServiceDocument.js @@ -1,5 +1,6 @@ import { Router } from 'express'; import Entity from './entity/Entity'; +import Singleton from './entity/Singleton'; export default class Metadata { constructor(server) { @@ -43,11 +44,13 @@ export default class Metadata { ctrl(req) { const entityTypeNames = Object.keys(this._server.resources); const entitySets = entityTypeNames - .filter((item) => this._server.resources[item] instanceof Entity) - .map((currentResource) => ({ - name: currentResource, - kind: 'EntitySet', - url: currentResource, + .filter((item) => + this._server.resources[item] instanceof Entity + || this._server.resources[item] instanceof Singleton ) + .map((item) => ({ + name: item, + kind: this._server.resources[item] instanceof Singleton ? 'Singleton' : 'EntitySet', + url: item, })); const document = { diff --git a/src/odata/entity/Entity.js b/src/odata/entity/Entity.js index 6bcdf22..221fac6 100644 --- a/src/odata/entity/Entity.js +++ b/src/odata/entity/Entity.js @@ -198,7 +198,7 @@ export default class Entity { .map(name => this.actions[name].getRouter()); const router = Router(); - const routes = this.getRoutes(this.name); + const routes = this.getRoutes(); routes.forEach((route) => { const { diff --git a/src/odata/entity/Singleton.js b/src/odata/entity/Singleton.js new file mode 100644 index 0000000..c043eac --- /dev/null +++ b/src/odata/entity/Singleton.js @@ -0,0 +1,115 @@ +import Entity from "./Entity"; +import { validate, validateIdentifier } from "../validator"; +import Hooks from "../Hooks"; +import { Router } from 'express'; + +export default class Singleton { + constructor(name, handler, metadata, settings, mapping) { + const notSupported = (req, res) => { + const error = new Error(); + + error.status = 405; + throw error; + }; + + this.name = name; + this.entity = new Entity(name, handler, metadata, mapping); + + this.handler = { + ...this.entity.handler, // get, post, put, delete, patch + list: notSupported, + count: notSupported, + ...handler + }; + + this.hooks = new Hooks(); + /* + this.metadata = { + $Kind: 'EntityType', + ...metadata + }; + + this.mapping = mapping || {};*/ + } + + addBefore(fn, name) { + this.hooks.addBefore(fn, name); + } + + addAfter(fn, name) { + this.hooks.addAfter(fn, name); + } + + getMetadata() { + return this.entity.getMetadata(); + } + + match(method, url) { + validateIdentifier(this.name); + + const routes = this.getRoutes() + const route = routes.find((item) => { + if (item.method === method) { + const match = url.match(item.regex); + + return match; + } + }); + + return route && [ + this.entity.parsingMiddleware.bind(this.entity), + ...this.hooks.before, + this.ctrl(route.name, this.handler[route.name]), + ...this.hooks.after, + this.entity.adaptResultAccordingMetadata.bind(this.entity) + ]; + + + } + + getRouter() { + validateIdentifier(this.name); + validate(this.entity.metadata); + + const router = Router(); + const routes = this.getRoutes(); + + routes.forEach((route) => { + const { + name, method, url + } = route; + + router[method](url, + this.entity.parsingMiddleware.bind(this.entity), + ...this.hooks.before, + this.ctrl(name, this.handler[name]), + this.entity.adaptResultAccordingMetadata.bind(this.entity), + ...this.hooks.after); + }); + + return [router]; + } + + ctrl(name, handler) { + return async (req, res, next) => { + try { + res.$odata.status = 200; + await handler(req, res, next); + + } catch(err) { + next(err); + } + }; + } + + getRoutes() { + const listRoute = this.entity.getRoutes().find(item => item.name === 'list'); + + return [ 'post', 'put', 'patch', 'delete', 'get'].map(item => ({ + name: item, + method: item, + url: listRoute.url, + regex: listRoute.regex + })); + } +} \ No newline at end of file diff --git a/src/server.js b/src/server.js index af81ff5..400473a 100644 --- a/src/server.js +++ b/src/server.js @@ -11,6 +11,7 @@ import error from './middlewares/error'; import writer from './middlewares/writer'; import Hooks from './odata/Hooks'; import multipartMixed from './parser/multipartMixed'; +import Singleton from './odata/entity/Singleton'; function checkAuth(auth, req) { return !auth || auth(req); @@ -78,7 +79,7 @@ class Server { } this.resources[name] = new Entity(name, handler, metadata, { - maxSkip: this._settings.maxSkip, + maxSkip: this._settings.maxSkip, //TODO: Validation of possible Mappings maxTop: this._settings.maxTop, orderby: this._settings.orderby, ...settings @@ -87,6 +88,16 @@ class Server { return this.resources[name]; } + singleton(name, handler, metadata, mapping) { + if (this.resources[name]) { + throw new Error(`Entity with name "${name}" already defined`); + } + + this.resources[name] = new Singleton(name, handler, metadata, mapping); + + return this.resources[name]; + } + mongoEntity(name, model, handler, metadata, settings, mapping) { if (name && !model) { if (!this.resources[name]) { diff --git a/src/writer/xmlWriter.js b/src/writer/xmlWriter.js index 3063a9d..fc5ebbf 100644 --- a/src/writer/xmlWriter.js +++ b/src/writer/xmlWriter.js @@ -16,6 +16,9 @@ export default class XmlWriter { case 'EntitySet': return this.visitEntitySet(node, name); + case 'Singleton': + return this.visitSingleton(node, name); + case 'TypeDefinition': return this.visitTypeDefinition(node, name); @@ -62,16 +65,23 @@ export default class XmlWriter { return ``; } + visitSingleton(node, name) { + return ``; + } + visitEntityContainter(node) { let entitySets = ''; + let singletons = ''; let functions = ''; let actions = '' Object.keys(node) .filter((item) => item !== '$Kind') .forEach((item) => { - if (node[item].$Type) { + if (node[item].$Collection === true) { entitySets += this.visitor('EntitySet', node[item], item); + } else if(node[item].$Type) { + singletons += this.visitor('Singleton', node[item], item); } else if (node[item].$Action) { actions += this.visitor('ActionImport', node[item], item); } else { @@ -80,7 +90,7 @@ export default class XmlWriter { }); return ( ` - ${entitySets}${functions}${actions} + ${entitySets}${singletons}${functions}${actions} `); } diff --git a/test/metadata.action.js b/test/metadata/action.js similarity index 98% rename from test/metadata.action.js rename to test/metadata/action.js index 8c3cae9..4d82adf 100644 --- a/test/metadata.action.js +++ b/test/metadata/action.js @@ -3,10 +3,10 @@ import 'should'; import request from 'supertest'; -import { host, port, odata, assertSuccess } from './support/setup'; +import { host, port, odata, assertSuccess } from '../support/setup'; describe('metadata.action', () => { - let httpServer, server, db; + let httpServer, server; beforeEach(async function() { server = odata(); diff --git a/test/metadata.complex.type.js b/test/metadata/complex.type.js similarity index 91% rename from test/metadata.complex.type.js rename to test/metadata/complex.type.js index f67d57c..82750e8 100644 --- a/test/metadata.complex.type.js +++ b/test/metadata/complex.type.js @@ -1,9 +1,6 @@ -// For issue: https://github.com/TossShinHwa/node-odata/issues/96 -// For issue: https://github.com/TossShinHwa/node-odata/issues/25 - import 'should'; import request from 'supertest'; -import { host, port, odata, assertSuccess } from './support/setup'; +import { host, port, odata, assertSuccess } from '../support/setup'; describe('metadata.complex.type', () => { let httpServer, server, db; diff --git a/test/metadata.js b/test/metadata/custom.resource.js similarity index 92% rename from test/metadata.js rename to test/metadata/custom.resource.js index 4d1415b..13e1cd9 100644 --- a/test/metadata.js +++ b/test/metadata/custom.resource.js @@ -3,9 +3,9 @@ import 'should'; import request from 'supertest'; -import { host, port, odata, assertSuccess } from './support/setup'; +import { host, port, odata, assertSuccess } from '../support/setup'; -describe('metadata', () => { +describe('metadata.custom.resource', () => { let httpServer, server; beforeEach(async function() { diff --git a/test/metadata.format.js b/test/metadata/format.js similarity index 94% rename from test/metadata.format.js rename to test/metadata/format.js index 753a383..a32fcce 100644 --- a/test/metadata.format.js +++ b/test/metadata/format.js @@ -1,9 +1,6 @@ -// For issue: https://github.com/TossShinHwa/node-odata/issues/96 -// For issue: https://github.com/TossShinHwa/node-odata/issues/25 - import 'should'; import request from 'supertest'; -import { host, port, odata, assertSuccess } from './support/setup'; +import { host, port, odata, assertSuccess } from '../support/setup'; describe('metadata.format', () => { let httpServer, server; diff --git a/test/metadata.function.js b/test/metadata/function.js similarity index 91% rename from test/metadata.function.js rename to test/metadata/function.js index e3878cb..5ce80ce 100644 --- a/test/metadata.function.js +++ b/test/metadata/function.js @@ -1,9 +1,6 @@ -// For issue: https://github.com/TossShinHwa/node-odata/issues/96 -// For issue: https://github.com/TossShinHwa/node-odata/issues/25 - import 'should'; import request from 'supertest'; -import { host, port, odata, assertSuccess } from './support/setup'; +import { host, port, odata, assertSuccess } from '../support/setup'; describe('metadata.function', () => { let httpServer, server; diff --git a/test/metadata/singleton.js b/test/metadata/singleton.js new file mode 100644 index 0000000..ee7d1da --- /dev/null +++ b/test/metadata/singleton.js @@ -0,0 +1,78 @@ +import 'should'; +import request from 'supertest'; +import { host, port, odata, assertSuccess } from '../support/setup'; +import { BookMetadata } from '../support/books.model'; + +describe('metadata.custom.resource', () => { + let httpServer, server; + + beforeEach(async function() { + server = odata(); + + }); + + afterEach(() => { + httpServer.close(); + }); + + it('[json] should render entity type if only singleton defined', async function() { + const jsonDocument = { + $Version: '4.0', + ObjectId: { + $Kind: "TypeDefinition", + $UnderlyingType: "Edm.String", + $MaxLength: 24 + }, + book: { + $Kind: "EntityType", + ...BookMetadata + }, + $EntityContainer: 'node.odata', + ['node.odata']: { + $Kind: 'EntityContainer', + book: { + $Type: `node.odata.book` + } + }, + }; + server.singleton('book', {}, BookMetadata); + httpServer = server.listen(port); + const res = await request(host).get('/$metadata?$format=json'); + assertSuccess(res); + res.body.should.deepEqual(jsonDocument); + }); + + it('[xml] should render entity type if only singleton defined', async function() { + const xmlDocument = + ` + + + + + + + + + + + + + + + + + + + + + + + `.replace(/\s*\s*/g, '>'); + + server.singleton('book', {}, BookMetadata); + httpServer = server.listen(port); + const res = await request(host).get('/$metadata'); + assertSuccess(res); + res.text.should.equal(xmlDocument); + }); +}); diff --git a/test/service.document.js b/test/service.document/entity.js similarity index 79% rename from test/service.document.js rename to test/service.document/entity.js index ab90665..e2ab579 100644 --- a/test/service.document.js +++ b/test/service.document/entity.js @@ -1,9 +1,10 @@ import 'should'; import request from 'supertest'; -import { host, port, odata, assertSuccess } from './support/setup'; -import { BookMetadata } from './support/books.model'; +import { host, port, odata, assertSuccess } from '../support/setup'; +import { BookMetadata } from '../support/books.model'; +import checkContentType from '../support/checkContentType'; -describe('service.document', () => { +describe('service.document.entity', () => { let httpServer, server, db; const jsonDocument = { @@ -46,10 +47,4 @@ describe('service.document', () => { res.status.should.be.equal(406); }); -}); - - -function checkContentType(res, value) { - res.header.should.have.property('content-type'); - res.header['content-type'].should.containEql(value); -} \ No newline at end of file +}); \ No newline at end of file diff --git a/test/service.document/singleton.js b/test/service.document/singleton.js new file mode 100644 index 0000000..7956d51 --- /dev/null +++ b/test/service.document/singleton.js @@ -0,0 +1,38 @@ +import 'should'; +import request from 'supertest'; +import { host, port, odata, assertSuccess } from '../support/setup'; +import { BookMetadata } from '../support/books.model'; +import checkContentType from '../support/checkContentType'; + +describe('service.document.singleton', () => { + let httpServer, server; + + const jsonDocument = { + '@context': 'http://localhost:3000/$metadata', + value: [{ + kind: 'Singleton', + name: 'book', + url: 'book' + }] + }; + beforeEach(async function() { + server = odata(); + server.singleton('book', null, BookMetadata); + + }); + + afterEach(() => { + httpServer.close(); + }); + + it('should return json if no format given', async function() { + httpServer = server.listen(port); + const res = await request(host).get('/'); + assertSuccess(res); + checkContentType(res, 'application/json'); + res.body.should.deepEqual(jsonDocument); + }); + +}); + + diff --git a/test/singleton.js b/test/singleton.js new file mode 100644 index 0000000..7de2ad4 --- /dev/null +++ b/test/singleton.js @@ -0,0 +1,41 @@ +import 'should'; +import request from 'supertest'; +import { odata, host, port } from './support/setup'; +import { BookMetadata } from './support/books.model'; + +describe('singleton', () => { + let httpServer, server; + + beforeEach(async function () { + server = odata(); + }); + + afterEach(() => { + if (httpServer) { + httpServer.close(); + } + }); + + it('should work with get', async function () { + const result = { + "id": '1', + "price": 44.95, + "title": "XML Developer's Guide" + }; + server.singleton('book', { + get: (req, res, next) => { + res.$odata.result = result; + next(); + } + }, BookMetadata); + httpServer = server.listen(port); + + const res = await request(host).get(`/book`); + + if (!res.ok) { + res.res.statusMessage.should.be.equal(''); + } + + res.body.should.deepEqual(result); + }); +}); diff --git a/test/support/checkContentType.js b/test/support/checkContentType.js new file mode 100644 index 0000000..52d7230 --- /dev/null +++ b/test/support/checkContentType.js @@ -0,0 +1,6 @@ +import 'should'; + +export default function checkContentType(res, value) { + res.header.should.have.property('content-type'); + res.header['content-type'].should.containEql(value); +} \ No newline at end of file From fe748529e47a2a9a50056dc612b4a226f7b60614 Mon Sep 17 00:00:00 2001 From: Richard Martens Date: Fri, 4 Aug 2023 13:58:58 +0200 Subject: [PATCH 38/64] [b] mongoose singleton implemented --- .vscode/launch.json | 2 +- src/mongo/Entity.js | 2 +- src/mongo/Singleton.js | 44 ++++++++++++++++++++++++++ src/mongo/rest/getSingleton.js | 32 +++++++++++++++++++ src/odata/entity/Entity.js | 18 +++++------ src/odata/entity/Singleton.js | 2 +- src/server.js | 48 +++++++++++++++++++++++----- test/mongo/mocked/singleton.js | 57 ++++++++++++++++++++++++++++++++++ 8 files changed, 186 insertions(+), 19 deletions(-) create mode 100644 src/mongo/Singleton.js create mode 100644 src/mongo/rest/getSingleton.js create mode 100644 test/mongo/mocked/singleton.js diff --git a/.vscode/launch.json b/.vscode/launch.json index 0a309e5..261cae9 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -20,7 +20,7 @@ "dot", "--timeout", "300000", - "test/odata.entity.js" + "test/mongo/mocked/singleton.js" ], "env": { "LOG_LEVEL": "debug" diff --git a/src/mongo/Entity.js b/src/mongo/Entity.js index 278e409..41a72a9 100644 --- a/src/mongo/Entity.js +++ b/src/mongo/Entity.js @@ -7,7 +7,7 @@ import get from './rest/get'; import count from './rest/count'; import { validate, validateIdentifier } from '../odata/validator'; -export default class Entity { +export default class MongoEntity { constructor(name, model) { this.name = name; this.model = model; diff --git a/src/mongo/Singleton.js b/src/mongo/Singleton.js new file mode 100644 index 0000000..55a3b6f --- /dev/null +++ b/src/mongo/Singleton.js @@ -0,0 +1,44 @@ +import MongoEntity from "./Entity"; +import post from './rest/post'; +import put from './rest/put'; +import del from './rest/delete'; +import patch from './rest/patch'; +import getSingleton from "./rest/getSingleton"; + +export default class MongoSingleton { + constructor(name, model) { + this.name = name; + this.entity = new MongoEntity(name, model); + + } + + getHandler() { + const rest = { + post, + put, + patch, + delete: del, + get: getSingleton + }; + const routes = Object.keys(rest); + let handler = {}; + + routes.forEach((route) => { + handler[route] = async (req, res, next) => { + try { + req.$odata = { + ...req.$odata, + Model: this.entity.model + }; + await rest[route](req, res, next); + + } catch (err) { + next(err); + } + }; + }); + + return handler; + } + +} \ No newline at end of file diff --git a/src/mongo/rest/getSingleton.js b/src/mongo/rest/getSingleton.js new file mode 100644 index 0000000..a14a21e --- /dev/null +++ b/src/mongo/rest/getSingleton.js @@ -0,0 +1,32 @@ +import selectParser from "../parser/selectParser"; + +export default async (req, res, next) => { + const query = req.$odata.Model.findOne(); + + await selectParser(query, req.$odata.$select); + + query.exec((err, entity) => { + if (err) { + return next(err); + } + + if (!entity) { + // return default properties of singleton + const result = new req.$odata.Model(); + + res.$odata.result = result.toObject(); + if (req.$odata.$select) { + Object.keys(res.$odata.result) + .forEach(item => { + if (req.$odata.$select.indexOf(item) === -1) { + delete req.$odata.$select; + } + }); + } + return next(); + } + + res.$odata.result = entity.toObject(); + return next(); + }); +}; diff --git a/src/odata/entity/Entity.js b/src/odata/entity/Entity.js index 221fac6..6d17e00 100644 --- a/src/odata/entity/Entity.js +++ b/src/odata/entity/Entity.js @@ -11,8 +11,8 @@ import { parseSkip, parseTop } from "./parser/skiptop"; export default class Entity { constructor(name, handler, metadata, settings, mapping) { - const notImplemented = (req, res) => { - const error = new Error(); + const notImplemented = op => (req, res) => { + const error = new Error(`Operation '${op}' is not implemented'`); error.status = 501; throw error; @@ -20,13 +20,13 @@ export default class Entity { this.name = name; this.handler = { - list: notImplemented, - get: notImplemented, - post: notImplemented, - put: notImplemented, - delete: notImplemented, - patch: notImplemented, - count: notImplemented, + list: notImplemented('list'), + get: notImplemented('get'), + post: notImplemented('post'), + put: notImplemented('put'), + delete: notImplemented('delete'), + patch: notImplemented('patch'), + count: notImplemented('count'), ...handler }; this.metadata = { diff --git a/src/odata/entity/Singleton.js b/src/odata/entity/Singleton.js index c043eac..6cb633a 100644 --- a/src/odata/entity/Singleton.js +++ b/src/odata/entity/Singleton.js @@ -4,7 +4,7 @@ import Hooks from "../Hooks"; import { Router } from 'express'; export default class Singleton { - constructor(name, handler, metadata, settings, mapping) { + constructor(name, handler, metadata, mapping) { const notSupported = (req, res) => { const error = new Error(); diff --git a/src/server.js b/src/server.js index 400473a..4fa694f 100644 --- a/src/server.js +++ b/src/server.js @@ -1,6 +1,7 @@ import createExpress from './express'; import bodyParser from 'body-parser'; import MongoEntity from './mongo/Entity'; +import MongoSingleton from './mongo/Singleton'; import Entity from './odata/entity/Entity'; import Func from './ODataFunction'; import Metadata from './odata/Metadata'; @@ -88,6 +89,39 @@ class Server { return this.resources[name]; } + mongoEntity(name, model, handler, metadata, settings, mapping) { + if (name && !model) { + if (!this.resources[name]) { + throw new Error(`Entity '${name}' is not defined`); + } + return this.resources[name]; + } + + const entity = new MongoEntity(name, model); + + const complexTypes = entity.getComplexTypes(); + + if (complexTypes) { + Object.keys(complexTypes) + .forEach(typeName => { + const type = complexTypes[typeName]; + + this.complexType(typeName, type); + }); + } + + return this.entity(name, { + ...entity.getHandler(), + ...handler + }, { + ...entity.getMetadata(), + ...metadata + }, settings, { + ...entity.getMapping(), + ...mapping + }); + } + singleton(name, handler, metadata, mapping) { if (this.resources[name]) { throw new Error(`Entity with name "${name}" already defined`); @@ -98,7 +132,7 @@ class Server { return this.resources[name]; } - mongoEntity(name, model, handler, metadata, settings, mapping) { + mongoSingleton(name, model, handler, metadata, mapping) { if (name && !model) { if (!this.resources[name]) { throw new Error(`Entity '${name}' is not defined`); @@ -106,9 +140,9 @@ class Server { return this.resources[name]; } - const entity = new MongoEntity(name, model); + const entity = new MongoSingleton(name, model); - const complexTypes = entity.getComplexTypes(); + const complexTypes = entity.entity.getComplexTypes(); if (complexTypes) { Object.keys(complexTypes) @@ -119,14 +153,14 @@ class Server { }); } - return this.entity(name, { + return this.singleton(name, { ...entity.getHandler(), ...handler }, { - ...entity.getMetadata(), + ...entity.entity.getMetadata(), ...metadata - }, settings, { - ...entity.getMapping(), + }, null, { + ...entity.entity.getMapping(), ...mapping }); } diff --git a/test/mongo/mocked/singleton.js b/test/mongo/mocked/singleton.js new file mode 100644 index 0000000..975ffbf --- /dev/null +++ b/test/mongo/mocked/singleton.js @@ -0,0 +1,57 @@ +import 'should'; +import sinon from 'sinon'; +import request from 'supertest'; +import { odata, host, port, assertSuccess } from '../../support/setup'; +import data from '../../support/books.json'; +import { BookModel } from '../../support/books.model'; + +describe('mongo.mocked.singleton', () => { + const query = { + $where: () => { }, + where: () => { }, + equals: () => { }, + select: () => { }, + sort: () => { }, + exec: () => { }, + model: BookModel + }; + let httpServer, modelMock, queryMock; + + before(async function() { + const server = odata(); + + server.mongoSingleton('book', BookModel); + httpServer = server.listen(port); + }); + + after(() => { + httpServer.close(); + }); + + afterEach(() => { + modelMock?.restore(); + queryMock?.restore(); + }); + + it('should select anyone field', async function() { + const books = data.map(item => ({ + price: item.price + })); + + modelMock = sinon.mock(BookModel); + queryMock = sinon.mock(query); + modelMock.expects('findOne').once().returns(query); + queryMock.expects('select').once().withArgs({ + _id: 0, + price: 1 + }); + queryMock.expects('exec').once().callsArgWith(0, null, { toObject: () => books[0] }); + + const res = await request(host).get('/book?$select=price'); + + assertSuccess(res); + modelMock.verify(); + queryMock.verify(); + res.body.should.deepEqual(books[0]); + }); +}); From 8b31a98eff313dc540c4721e732882d4ed083c41 Mon Sep 17 00:00:00 2001 From: Richard Martens Date: Fri, 4 Aug 2023 22:31:33 +0200 Subject: [PATCH 39/64] [b] upgrade to node 18.17.0 and all dependencies --- .vscode/launch.json | 2 +- package-lock.json | 6465 +++++++++-------- package.json | 64 +- src/mongo/rest/count.js | 24 +- src/mongo/rest/delete.js | 16 +- src/mongo/rest/get.js | 18 +- src/mongo/rest/getSingleton.js | 44 +- src/mongo/rest/list.js | 53 +- src/mongo/rest/patch.js | 29 +- src/mongo/rest/post.js | 31 +- src/mongo/rest/put.js | 74 +- test/mongo/connected/model.complex.js | 4 +- test/mongo/mocked/model.complex.filter.js | 7 +- test/mongo/mocked/model.custom.id.js | 15 +- test/mongo/mocked/model.hidden.field.js | 30 +- test/mongo/mocked/odata.count.js | 5 +- test/mongo/mocked/odata.query.count.js | 11 +- .../mocked/odata.query.filter.functions.js | 17 +- test/mongo/mocked/rest.delete.js | 10 +- test/mongo/mocked/rest.get.js | 10 +- test/mongo/mocked/rest.put.js | 12 +- test/mongo/mocked/singleton.js | 3 +- test/support/db.js | 19 +- 23 files changed, 3798 insertions(+), 3165 deletions(-) diff --git a/.vscode/launch.json b/.vscode/launch.json index 261cae9..753065e 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -20,7 +20,7 @@ "dot", "--timeout", "300000", - "test/mongo/mocked/singleton.js" + "test/mongo/mocked/rest.get.js" ], "env": { "LOG_LEVEL": "debug" diff --git a/package-lock.json b/package-lock.json index 5b22247..cc06bc1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,53 +9,62 @@ "version": "0.7.16", "license": "MIT", "dependencies": { - "body-parser": "^1.20.0", + "body-parser": "^1.20.2", "cors": "2.8.5", - "express": "^4.18.1", + "express": "^4.18.2", "method-override": "3.0.0", - "mongoose": "6.6.5", + "mongoose": "7.4.2", "uuid": "9.0.0" }, "devDependencies": { - "@babel/cli": "^7.19.3", - "@babel/core": "^7.19.3", - "@babel/eslint-parser": "^7.19.1", - "@babel/helpers": "^7.19.0", - "@babel/plugin-proposal-class-properties": "^7.0.0", - "@babel/plugin-proposal-decorators": "^7.0.0", - "@babel/plugin-proposal-do-expressions": "^7.0.0", - "@babel/plugin-proposal-export-default-from": "^7.0.0", - "@babel/plugin-proposal-export-namespace-from": "^7.0.0", - "@babel/plugin-proposal-function-bind": "^7.0.0", - "@babel/plugin-proposal-function-sent": "^7.0.0", - "@babel/plugin-proposal-json-strings": "^7.0.0", - "@babel/plugin-proposal-logical-assignment-operators": "^7.0.0", - "@babel/plugin-proposal-nullish-coalescing-operator": "^7.0.0", - "@babel/plugin-proposal-numeric-separator": "^7.0.0", - "@babel/plugin-proposal-optional-chaining": "^7.0.0", - "@babel/plugin-proposal-pipeline-operator": "^7.0.0", - "@babel/plugin-proposal-throw-expressions": "^7.0.0", - "@babel/plugin-syntax-dynamic-import": "^7.0.0", - "@babel/plugin-syntax-import-meta": "^7.0.0", - "@babel/preset-env": "^7.19.3", - "@babel/register": "^7.18.9", - "@babel/runtime": "^7.19.0", - "babel-loader": "8.2.5", - "eslint": "8.24.0", + "@babel/cli": "^7.22.9", + "@babel/core": "^7.22.9", + "@babel/eslint-parser": "^7.22.9", + "@babel/helpers": "^7.22.6", + "@babel/plugin-proposal-class-properties": "^7.18.6", + "@babel/plugin-proposal-decorators": "^7.22.7", + "@babel/plugin-proposal-do-expressions": "^7.22.5", + "@babel/plugin-proposal-export-default-from": "^7.22.5", + "@babel/plugin-proposal-export-namespace-from": "^7.18.9", + "@babel/plugin-proposal-function-bind": "^7.22.5", + "@babel/plugin-proposal-function-sent": "^7.22.5", + "@babel/plugin-proposal-json-strings": "^7.18.6", + "@babel/plugin-proposal-logical-assignment-operators": "^7.20.7", + "@babel/plugin-proposal-nullish-coalescing-operator": "^7.18.6", + "@babel/plugin-proposal-numeric-separator": "^7.18.6", + "@babel/plugin-proposal-optional-chaining": "^7.21.0", + "@babel/plugin-proposal-pipeline-operator": "^7.22.5", + "@babel/plugin-proposal-throw-expressions": "^7.22.5", + "@babel/plugin-syntax-dynamic-import": "^7.8.3", + "@babel/plugin-syntax-import-meta": "^7.10.4", + "@babel/preset-env": "^7.22.9", + "@babel/register": "^7.22.5", + "@babel/runtime": "^7.22.6", + "babel-loader": "9.1.3", + "eslint": "8.46.0", "eslint-config-airbnb": "19.0.4", "eslint-plugin-babel": "5.3.1", - "eslint-plugin-import": "^2.26.0", + "eslint-plugin-import": "^2.28.0", "istanbul": "1.1.0-alpha.1", - "mocha": "10.0.0", + "mocha": "10.2.0", "should": "13.2.3", "should-sinon": "0.0.6", - "sinon": "14.0.1", - "supertest": "6.3.0" + "sinon": "15.2.0", + "supertest": "6.3.3" }, "engines": { "node": ">=0.12" } }, + "node_modules/@aashutoshrathi/word-wrap": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/@aashutoshrathi/word-wrap/-/word-wrap-1.2.6.tgz", + "integrity": "sha512-1Yjs2SvM8TflER/OD3cOjhWWOZb58A2t7wpE2S9XfBYTiIl+XFhQG2bjy4Pu1I+EAlCNUzRDYDdFwFYUKvXcIA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/@ampproject/remapping": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.2.0.tgz", @@ -70,12 +79,12 @@ } }, "node_modules/@babel/cli": { - "version": "7.19.3", - "resolved": "https://registry.npmjs.org/@babel/cli/-/cli-7.19.3.tgz", - "integrity": "sha512-643/TybmaCAe101m2tSVHi9UKpETXP9c/Ff4mD2tAwkdP6esKIfaauZFc67vGEM6r9fekbEGid+sZhbEnSe3dg==", + "version": "7.22.9", + "resolved": "https://registry.npmjs.org/@babel/cli/-/cli-7.22.9.tgz", + "integrity": "sha512-nb2O7AThqRo7/E53EGiuAkMaRbb7J5Qp3RvN+dmua1U+kydm0oznkhqbTEG15yk26G/C3yL6OdZjzgl+DMXVVA==", "dev": true, "dependencies": { - "@jridgewell/trace-mapping": "^0.3.8", + "@jridgewell/trace-mapping": "^0.3.17", "commander": "^4.0.1", "convert-source-map": "^1.1.0", "fs-readdir-recursive": "^1.1.0", @@ -139,47 +148,47 @@ } }, "node_modules/@babel/code-frame": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.18.6.tgz", - "integrity": "sha512-TDCmlK5eOvH+eH7cdAFlNXeVJqWIQ7gW9tY1GJIpUtFb6CmjVyq2VM3u71bOyR8CRihcCgMUYoDNyLXao3+70Q==", + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.22.5.tgz", + "integrity": "sha512-Xmwn266vad+6DAqEB2A6V/CcZVp62BbwVmcOJc2RPuwih1kw02TjQvWVWlcKGbBPd+8/0V5DEkOcizRGYsspYQ==", "dev": true, "dependencies": { - "@babel/highlight": "^7.18.6" + "@babel/highlight": "^7.22.5" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/compat-data": { - "version": "7.19.3", - "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.19.3.tgz", - "integrity": "sha512-prBHMK4JYYK+wDjJF1q99KK4JLL+egWS4nmNqdlMUgCExMZ+iZW0hGhyC3VEbsPjvaN0TBhW//VIFwBrk8sEiw==", + "version": "7.22.9", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.22.9.tgz", + "integrity": "sha512-5UamI7xkUcJ3i9qVDS+KFDEK8/7oJ55/sJMB1Ge7IEapr7KfdfV/HErR+koZwOfd+SgtFKOKRhRakdg++DcJpQ==", "dev": true, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/core": { - "version": "7.19.3", - "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.19.3.tgz", - "integrity": "sha512-WneDJxdsjEvyKtXKsaBGbDeiyOjR5vYq4HcShxnIbG0qixpoHjI3MqeZM9NDvsojNCEBItQE4juOo/bU6e72gQ==", - "dev": true, - "dependencies": { - "@ampproject/remapping": "^2.1.0", - "@babel/code-frame": "^7.18.6", - "@babel/generator": "^7.19.3", - "@babel/helper-compilation-targets": "^7.19.3", - "@babel/helper-module-transforms": "^7.19.0", - "@babel/helpers": "^7.19.0", - "@babel/parser": "^7.19.3", - "@babel/template": "^7.18.10", - "@babel/traverse": "^7.19.3", - "@babel/types": "^7.19.3", + "version": "7.22.9", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.22.9.tgz", + "integrity": "sha512-G2EgeufBcYw27U4hhoIwFcgc1XU7TlXJ3mv04oOv1WCuo900U/anZSPzEqNjwdjgffkk2Gs0AN0dW1CKVLcG7w==", + "dev": true, + "dependencies": { + "@ampproject/remapping": "^2.2.0", + "@babel/code-frame": "^7.22.5", + "@babel/generator": "^7.22.9", + "@babel/helper-compilation-targets": "^7.22.9", + "@babel/helper-module-transforms": "^7.22.9", + "@babel/helpers": "^7.22.6", + "@babel/parser": "^7.22.7", + "@babel/template": "^7.22.5", + "@babel/traverse": "^7.22.8", + "@babel/types": "^7.22.5", "convert-source-map": "^1.7.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", - "json5": "^2.2.1", - "semver": "^6.3.0" + "json5": "^2.2.2", + "semver": "^6.3.1" }, "engines": { "node": ">=6.9.0" @@ -206,18 +215,6 @@ } } }, - "node_modules/@babel/core/node_modules/json5": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.1.tgz", - "integrity": "sha512-1hqLFMSrGHRHxav9q9gNjJ5EXznIxGVO09xQRrwplcS8qs28pZ8s8hupZAmqDwZUmVZ2Qb2jnyPOWcDH8m8dlA==", - "dev": true, - "bin": { - "json5": "lib/cli.js" - }, - "engines": { - "node": ">=6" - } - }, "node_modules/@babel/core/node_modules/ms": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", @@ -225,23 +222,23 @@ "dev": true }, "node_modules/@babel/core/node_modules/semver": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", - "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", "dev": true, "bin": { "semver": "bin/semver.js" } }, "node_modules/@babel/eslint-parser": { - "version": "7.19.1", - "resolved": "https://registry.npmjs.org/@babel/eslint-parser/-/eslint-parser-7.19.1.tgz", - "integrity": "sha512-AqNf2QWt1rtu2/1rLswy6CDP7H9Oh3mMhk177Y67Rg8d7RD9WfOLLv8CGn6tisFvS2htm86yIe1yLF6I1UDaGQ==", + "version": "7.22.9", + "resolved": "https://registry.npmjs.org/@babel/eslint-parser/-/eslint-parser-7.22.9.tgz", + "integrity": "sha512-xdMkt39/nviO/4vpVdrEYPwXCsYIXSSAr6mC7WQsNIlGnuxKyKE7GZjalcnbSWiC4OXGNNN3UQPeHfjSC6sTDA==", "dev": true, "dependencies": { "@nicolo-ribaudo/eslint-scope-5-internals": "5.1.1-v1", "eslint-visitor-keys": "^2.1.0", - "semver": "^6.3.0" + "semver": "^6.3.1" }, "engines": { "node": "^10.13.0 || ^12.13.0 || >=14.0.0" @@ -261,22 +258,23 @@ } }, "node_modules/@babel/eslint-parser/node_modules/semver": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", - "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", "dev": true, "bin": { "semver": "bin/semver.js" } }, "node_modules/@babel/generator": { - "version": "7.19.3", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.19.3.tgz", - "integrity": "sha512-fqVZnmp1ncvZU757UzDheKZpfPgatqY59XtW2/j/18H7u76akb8xqvjw82f+i2UKd/ksYsSick/BCLQUUtJ/qQ==", + "version": "7.22.9", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.22.9.tgz", + "integrity": "sha512-KtLMbmicyuK2Ak/FTCJVbDnkN1SlT8/kceFTiuDiiRUUSMnHMidxSCdG4ndkTOHHpoomWe/4xkvHkEOncwjYIw==", "dev": true, "dependencies": { - "@babel/types": "^7.19.3", + "@babel/types": "^7.22.5", "@jridgewell/gen-mapping": "^0.3.2", + "@jridgewell/trace-mapping": "^0.3.17", "jsesc": "^2.5.1" }, "engines": { @@ -310,40 +308,40 @@ } }, "node_modules/@babel/helper-annotate-as-pure": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.18.6.tgz", - "integrity": "sha512-duORpUiYrEpzKIop6iNbjnwKLAKnJ47csTyRACyEmWj0QdUrm5aqNJGHSSEQSUAvNW0ojX0dOmK9dZduvkfeXA==", + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.22.5.tgz", + "integrity": "sha512-LvBTxu8bQSQkcyKOU+a1btnNFQ1dMAd0R6PyW3arXes06F6QLWLIrd681bxRPIXlrMGR3XYnW9JyML7dP3qgxg==", "dev": true, "dependencies": { - "@babel/types": "^7.18.6" + "@babel/types": "^7.22.5" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-builder-binary-assignment-operator-visitor": { - "version": "7.18.9", - "resolved": "https://registry.npmjs.org/@babel/helper-builder-binary-assignment-operator-visitor/-/helper-builder-binary-assignment-operator-visitor-7.18.9.tgz", - "integrity": "sha512-yFQ0YCHoIqarl8BCRwBL8ulYUaZpz3bNsA7oFepAzee+8/+ImtADXNOmO5vJvsPff3qi+hvpkY/NYBTrBQgdNw==", + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-builder-binary-assignment-operator-visitor/-/helper-builder-binary-assignment-operator-visitor-7.22.5.tgz", + "integrity": "sha512-m1EP3lVOPptR+2DwD125gziZNcmoNSHGmJROKoy87loWUQyJaVXDgpmruWqDARZSmtYQ+Dl25okU8+qhVzuykw==", "dev": true, "dependencies": { - "@babel/helper-explode-assignable-expression": "^7.18.6", - "@babel/types": "^7.18.9" + "@babel/types": "^7.22.5" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-compilation-targets": { - "version": "7.19.3", - "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.19.3.tgz", - "integrity": "sha512-65ESqLGyGmLvgR0mst5AdW1FkNlj9rQsCKduzEoEPhBCDFGXvz2jW6bXFG6i0/MrV2s7hhXjjb2yAzcPuQlLwg==", + "version": "7.22.9", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.22.9.tgz", + "integrity": "sha512-7qYrNM6HjpnPHJbopxmb8hSPoZ0gsX8IvUS32JGVoy+pU9e5N0nLr1VjJoR6kA4d9dmGLxNYOjeB8sUDal2WMw==", "dev": true, "dependencies": { - "@babel/compat-data": "^7.19.3", - "@babel/helper-validator-option": "^7.18.6", - "browserslist": "^4.21.3", - "semver": "^6.3.0" + "@babel/compat-data": "^7.22.9", + "@babel/helper-validator-option": "^7.22.5", + "browserslist": "^4.21.9", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" }, "engines": { "node": ">=6.9.0" @@ -353,27 +351,29 @@ } }, "node_modules/@babel/helper-compilation-targets/node_modules/semver": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", - "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", "dev": true, "bin": { "semver": "bin/semver.js" } }, "node_modules/@babel/helper-create-class-features-plugin": { - "version": "7.19.0", - "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.19.0.tgz", - "integrity": "sha512-NRz8DwF4jT3UfrmUoZjd0Uph9HQnP30t7Ash+weACcyNkiYTywpIjDBgReJMKgr+n86sn2nPVVmJ28Dm053Kqw==", + "version": "7.22.9", + "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.22.9.tgz", + "integrity": "sha512-Pwyi89uO4YrGKxL/eNJ8lfEH55DnRloGPOseaA8NFNL6jAUnn+KccaISiFazCj5IolPPDjGSdzQzXVzODVRqUQ==", "dev": true, "dependencies": { - "@babel/helper-annotate-as-pure": "^7.18.6", - "@babel/helper-environment-visitor": "^7.18.9", - "@babel/helper-function-name": "^7.19.0", - "@babel/helper-member-expression-to-functions": "^7.18.9", - "@babel/helper-optimise-call-expression": "^7.18.6", - "@babel/helper-replace-supers": "^7.18.9", - "@babel/helper-split-export-declaration": "^7.18.6" + "@babel/helper-annotate-as-pure": "^7.22.5", + "@babel/helper-environment-visitor": "^7.22.5", + "@babel/helper-function-name": "^7.22.5", + "@babel/helper-member-expression-to-functions": "^7.22.5", + "@babel/helper-optimise-call-expression": "^7.22.5", + "@babel/helper-replace-supers": "^7.22.9", + "@babel/helper-skip-transparent-expression-wrappers": "^7.22.5", + "@babel/helper-split-export-declaration": "^7.22.6", + "semver": "^6.3.1" }, "engines": { "node": ">=6.9.0" @@ -382,14 +382,24 @@ "@babel/core": "^7.0.0" } }, + "node_modules/@babel/helper-create-class-features-plugin/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + } + }, "node_modules/@babel/helper-create-regexp-features-plugin": { - "version": "7.19.0", - "resolved": "https://registry.npmjs.org/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.19.0.tgz", - "integrity": "sha512-htnV+mHX32DF81amCDrwIDr8nrp1PTm+3wfBN9/v8QJOLEioOCOG7qNyq0nHeFiWbT3Eb7gsPwEmV64UCQ1jzw==", + "version": "7.22.9", + "resolved": "https://registry.npmjs.org/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.22.9.tgz", + "integrity": "sha512-+svjVa/tFwsNSG4NEy1h85+HQ5imbT92Q5/bgtS7P0GTQlP8WuFdqsiABmQouhiFGyV66oGxZFpeYHza1rNsKw==", "dev": true, "dependencies": { - "@babel/helper-annotate-as-pure": "^7.18.6", - "regexpu-core": "^5.1.0" + "@babel/helper-annotate-as-pure": "^7.22.5", + "regexpu-core": "^5.3.1", + "semver": "^6.3.1" }, "engines": { "node": ">=6.9.0" @@ -398,21 +408,29 @@ "@babel/core": "^7.0.0" } }, + "node_modules/@babel/helper-create-regexp-features-plugin/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + } + }, "node_modules/@babel/helper-define-polyfill-provider": { - "version": "0.3.3", - "resolved": "https://registry.npmjs.org/@babel/helper-define-polyfill-provider/-/helper-define-polyfill-provider-0.3.3.tgz", - "integrity": "sha512-z5aQKU4IzbqCC1XH0nAqfsFLMVSo22SBKUc0BxGrLkolTdPTructy0ToNnlO2zA4j9Q/7pjMZf0DSY+DSTYzww==", + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/@babel/helper-define-polyfill-provider/-/helper-define-polyfill-provider-0.4.2.tgz", + "integrity": "sha512-k0qnnOqHn5dK9pZpfD5XXZ9SojAITdCKRn2Lp6rnDGzIbaP0rHyMPk/4wsSxVBVz4RfN0q6VpXWP2pDGIoQ7hw==", "dev": true, "dependencies": { - "@babel/helper-compilation-targets": "^7.17.7", - "@babel/helper-plugin-utils": "^7.16.7", + "@babel/helper-compilation-targets": "^7.22.6", + "@babel/helper-plugin-utils": "^7.22.5", "debug": "^4.1.1", "lodash.debounce": "^4.0.8", - "resolve": "^1.14.2", - "semver": "^6.1.2" + "resolve": "^1.14.2" }, "peerDependencies": { - "@babel/core": "^7.4.0-0" + "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" } }, "node_modules/@babel/helper-define-polyfill-provider/node_modules/debug": { @@ -438,135 +456,113 @@ "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", "dev": true }, - "node_modules/@babel/helper-define-polyfill-provider/node_modules/semver": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", - "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", - "dev": true, - "bin": { - "semver": "bin/semver.js" - } - }, "node_modules/@babel/helper-environment-visitor": { - "version": "7.18.9", - "resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.18.9.tgz", - "integrity": "sha512-3r/aACDJ3fhQ/EVgFy0hpj8oHyHpQc+LPtJoY9SzTThAsStm4Ptegq92vqKoE3vD706ZVFWITnMnxucw+S9Ipg==", - "dev": true, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-explode-assignable-expression": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/helper-explode-assignable-expression/-/helper-explode-assignable-expression-7.18.6.tgz", - "integrity": "sha512-eyAYAsQmB80jNfg4baAtLeWAQHfHFiR483rzFK+BhETlGZaQC9bsfrugfXDCbRHLQbIA7U5NxhhOxN7p/dWIcg==", + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.22.5.tgz", + "integrity": "sha512-XGmhECfVA/5sAt+H+xpSg0mfrHq6FzNr9Oxh7PSEBBRUb/mL7Kz3NICXb194rCqAEdxkhPT1a88teizAFyvk8Q==", "dev": true, - "dependencies": { - "@babel/types": "^7.18.6" - }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-function-name": { - "version": "7.19.0", - "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.19.0.tgz", - "integrity": "sha512-WAwHBINyrpqywkUH0nTnNgI5ina5TFn85HKS0pbPDfxFfhyR/aNQEn4hGi1P1JyT//I0t4OgXUlofzWILRvS5w==", + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.22.5.tgz", + "integrity": "sha512-wtHSq6jMRE3uF2otvfuD3DIvVhOsSNshQl0Qrd7qC9oQJzHvOL4qQXlQn2916+CXGywIjpGuIkoyZRRxHPiNQQ==", "dev": true, "dependencies": { - "@babel/template": "^7.18.10", - "@babel/types": "^7.19.0" + "@babel/template": "^7.22.5", + "@babel/types": "^7.22.5" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-hoist-variables": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.18.6.tgz", - "integrity": "sha512-UlJQPkFqFULIcyW5sbzgbkxn2FKRgwWiRexcuaR8RNJRy8+LLveqPjwZV/bwrLZCN0eUHD/x8D0heK1ozuoo6Q==", + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.22.5.tgz", + "integrity": "sha512-wGjk9QZVzvknA6yKIUURb8zY3grXCcOZt+/7Wcy8O2uctxhplmUPkOdlgoNhmdVee2c92JXbf1xpMtVNbfoxRw==", "dev": true, "dependencies": { - "@babel/types": "^7.18.6" + "@babel/types": "^7.22.5" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-member-expression-to-functions": { - "version": "7.18.9", - "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.18.9.tgz", - "integrity": "sha512-RxifAh2ZoVU67PyKIO4AMi1wTenGfMR/O/ae0CCRqwgBAt5v7xjdtRw7UoSbsreKrQn5t7r89eruK/9JjYHuDg==", + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.22.5.tgz", + "integrity": "sha512-aBiH1NKMG0H2cGZqspNvsaBe6wNGjbJjuLy29aU+eDZjSbbN53BaxlpB02xm9v34pLTZ1nIQPFYn2qMZoa5BQQ==", "dev": true, "dependencies": { - "@babel/types": "^7.18.9" + "@babel/types": "^7.22.5" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-module-imports": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.18.6.tgz", - "integrity": "sha512-0NFvs3VkuSYbFi1x2Vd6tKrywq+z/cLeYC/RJNFrIX/30Bf5aiGYbtvGXolEktzJH8o5E5KJ3tT+nkxuuZFVlA==", + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.22.5.tgz", + "integrity": "sha512-8Dl6+HD/cKifutF5qGd/8ZJi84QeAKh+CEe1sBzz8UayBBGg1dAIJrdHOcOM5b2MpzWL2yuotJTtGjETq0qjXg==", "dev": true, "dependencies": { - "@babel/types": "^7.18.6" + "@babel/types": "^7.22.5" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-module-transforms": { - "version": "7.19.0", - "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.19.0.tgz", - "integrity": "sha512-3HBZ377Fe14RbLIA+ac3sY4PTgpxHVkFrESaWhoI5PuyXPBBX8+C34qblV9G89ZtycGJCmCI/Ut+VUDK4bltNQ==", + "version": "7.22.9", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.22.9.tgz", + "integrity": "sha512-t+WA2Xn5K+rTeGtC8jCsdAH52bjggG5TKRuRrAGNM/mjIbO4GxvlLMFOEz9wXY5I2XQ60PMFsAG2WIcG82dQMQ==", "dev": true, "dependencies": { - "@babel/helper-environment-visitor": "^7.18.9", - "@babel/helper-module-imports": "^7.18.6", - "@babel/helper-simple-access": "^7.18.6", - "@babel/helper-split-export-declaration": "^7.18.6", - "@babel/helper-validator-identifier": "^7.18.6", - "@babel/template": "^7.18.10", - "@babel/traverse": "^7.19.0", - "@babel/types": "^7.19.0" + "@babel/helper-environment-visitor": "^7.22.5", + "@babel/helper-module-imports": "^7.22.5", + "@babel/helper-simple-access": "^7.22.5", + "@babel/helper-split-export-declaration": "^7.22.6", + "@babel/helper-validator-identifier": "^7.22.5" }, "engines": { "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" } }, "node_modules/@babel/helper-optimise-call-expression": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.18.6.tgz", - "integrity": "sha512-HP59oD9/fEHQkdcbgFCnbmgH5vIQTJbxh2yf+CdM89/glUNnuzr87Q8GIjGEnOktTROemO0Pe0iPAYbqZuOUiA==", + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.22.5.tgz", + "integrity": "sha512-HBwaojN0xFRx4yIvpwGqxiV2tUfl7401jlok564NgB9EHS1y6QT17FmKWm4ztqjeVdXLuC4fSvHc5ePpQjoTbw==", "dev": true, "dependencies": { - "@babel/types": "^7.18.6" + "@babel/types": "^7.22.5" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-plugin-utils": { - "version": "7.19.0", - "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.19.0.tgz", - "integrity": "sha512-40Ryx7I8mT+0gaNxm8JGTZFUITNqdLAgdg0hXzeVZxVD6nFsdhQvip6v8dqkRHzsz1VFpFAaOCHNn0vKBL7Czw==", + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.22.5.tgz", + "integrity": "sha512-uLls06UVKgFG9QD4OeFYLEGteMIAa5kpTPcFL28yuCIIzsf6ZyKZMllKVOCZFhiZ5ptnwX4mtKdWCBE/uT4amg==", "dev": true, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-remap-async-to-generator": { - "version": "7.18.9", - "resolved": "https://registry.npmjs.org/@babel/helper-remap-async-to-generator/-/helper-remap-async-to-generator-7.18.9.tgz", - "integrity": "sha512-dI7q50YKd8BAv3VEfgg7PS7yD3Rtbi2J1XMXaalXO0W0164hYLnh8zpjRS0mte9MfVp/tltvr/cfdXPvJr1opA==", + "version": "7.22.9", + "resolved": "https://registry.npmjs.org/@babel/helper-remap-async-to-generator/-/helper-remap-async-to-generator-7.22.9.tgz", + "integrity": "sha512-8WWC4oR4Px+tr+Fp0X3RHDVfINGpF3ad1HIbrc8A77epiR6eMMc6jsgozkzT2uDiOOdoS9cLIQ+XD2XvI2WSmQ==", "dev": true, "dependencies": { - "@babel/helper-annotate-as-pure": "^7.18.6", - "@babel/helper-environment-visitor": "^7.18.9", - "@babel/helper-wrap-function": "^7.18.9", - "@babel/types": "^7.18.9" + "@babel/helper-annotate-as-pure": "^7.22.5", + "@babel/helper-environment-visitor": "^7.22.5", + "@babel/helper-wrap-function": "^7.22.9" }, "engines": { "node": ">=6.9.0" @@ -576,120 +572,120 @@ } }, "node_modules/@babel/helper-replace-supers": { - "version": "7.19.1", - "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.19.1.tgz", - "integrity": "sha512-T7ahH7wV0Hfs46SFh5Jz3s0B6+o8g3c+7TMxu7xKfmHikg7EAZ3I2Qk9LFhjxXq8sL7UkP5JflezNwoZa8WvWw==", + "version": "7.22.9", + "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.22.9.tgz", + "integrity": "sha512-LJIKvvpgPOPUThdYqcX6IXRuIcTkcAub0IaDRGCZH0p5GPUp7PhRU9QVgFcDDd51BaPkk77ZjqFwh6DZTAEmGg==", "dev": true, "dependencies": { - "@babel/helper-environment-visitor": "^7.18.9", - "@babel/helper-member-expression-to-functions": "^7.18.9", - "@babel/helper-optimise-call-expression": "^7.18.6", - "@babel/traverse": "^7.19.1", - "@babel/types": "^7.19.0" + "@babel/helper-environment-visitor": "^7.22.5", + "@babel/helper-member-expression-to-functions": "^7.22.5", + "@babel/helper-optimise-call-expression": "^7.22.5" }, "engines": { "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" } }, "node_modules/@babel/helper-simple-access": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.18.6.tgz", - "integrity": "sha512-iNpIgTgyAvDQpDj76POqg+YEt8fPxx3yaNBg3S30dxNKm2SWfYhD0TGrK/Eu9wHpUW63VQU894TsTg+GLbUa1g==", + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.22.5.tgz", + "integrity": "sha512-n0H99E/K+Bika3++WNL17POvo4rKWZ7lZEp1Q+fStVbUi8nxPQEBOlTmCOxW/0JsS56SKKQ+ojAe2pHKJHN35w==", "dev": true, "dependencies": { - "@babel/types": "^7.18.6" + "@babel/types": "^7.22.5" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-skip-transparent-expression-wrappers": { - "version": "7.18.9", - "resolved": "https://registry.npmjs.org/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.18.9.tgz", - "integrity": "sha512-imytd2gHi3cJPsybLRbmFrF7u5BIEuI2cNheyKi3/iOBC63kNn3q8Crn2xVuESli0aM4KYsyEqKyS7lFL8YVtw==", + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.22.5.tgz", + "integrity": "sha512-tK14r66JZKiC43p8Ki33yLBVJKlQDFoA8GYN67lWCDCqoL6EMMSuM9b+Iff2jHaM/RRFYl7K+iiru7hbRqNx8Q==", "dev": true, "dependencies": { - "@babel/types": "^7.18.9" + "@babel/types": "^7.22.5" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-split-export-declaration": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.18.6.tgz", - "integrity": "sha512-bde1etTx6ZyTmobl9LLMMQsaizFVZrquTEHOqKeQESMKo4PlObf+8+JA25ZsIpZhT/WEd39+vOdLXAFG/nELpA==", + "version": "7.22.6", + "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.22.6.tgz", + "integrity": "sha512-AsUnxuLhRYsisFiaJwvp1QF+I3KjD5FOxut14q/GzovUe6orHLesW2C7d754kRm53h5gqrz6sFl6sxc4BVtE/g==", "dev": true, "dependencies": { - "@babel/types": "^7.18.6" + "@babel/types": "^7.22.5" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-string-parser": { - "version": "7.18.10", - "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.18.10.tgz", - "integrity": "sha512-XtIfWmeNY3i4t7t4D2t02q50HvqHybPqW2ki1kosnvWCwuCMeo81Jf0gwr85jy/neUdg5XDdeFE/80DXiO+njw==", + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.22.5.tgz", + "integrity": "sha512-mM4COjgZox8U+JcXQwPijIZLElkgEpO5rsERVDJTc2qfCDfERyob6k5WegS14SX18IIjv+XD+GrqNumY5JRCDw==", "dev": true, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-validator-identifier": { - "version": "7.19.1", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.19.1.tgz", - "integrity": "sha512-awrNfaMtnHUr653GgGEs++LlAvW6w+DcPrOliSMXWCKo597CwL5Acf/wWdNkf/tfEQE3mjkeD1YOVZOUV/od1w==", + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.5.tgz", + "integrity": "sha512-aJXu+6lErq8ltp+JhkJUfk1MTGyuA4v7f3pA+BJ5HLfNC6nAQ0Cpi9uOquUj8Hehg0aUiHzWQbOVJGao6ztBAQ==", "dev": true, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-validator-option": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.18.6.tgz", - "integrity": "sha512-XO7gESt5ouv/LRJdrVjkShckw6STTaB7l9BrpBaAHDeF5YZT+01PCwmR0SJHnkW6i8OwW/EVWRShfi4j2x+KQw==", + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.22.5.tgz", + "integrity": "sha512-R3oB6xlIVKUnxNUxbmgq7pKjxpru24zlimpE8WK47fACIlM0II/Hm1RS8IaOI7NgCr6LNS+jl5l75m20npAziw==", "dev": true, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-wrap-function": { - "version": "7.19.0", - "resolved": "https://registry.npmjs.org/@babel/helper-wrap-function/-/helper-wrap-function-7.19.0.tgz", - "integrity": "sha512-txX8aN8CZyYGTwcLhlk87KRqncAzhh5TpQamZUa0/u3an36NtDpUP6bQgBCBcLeBs09R/OwQu3OjK0k/HwfNDg==", + "version": "7.22.9", + "resolved": "https://registry.npmjs.org/@babel/helper-wrap-function/-/helper-wrap-function-7.22.9.tgz", + "integrity": "sha512-sZ+QzfauuUEfxSEjKFmi3qDSHgLsTPK/pEpoD/qonZKOtTPTLbf59oabPQ4rKekt9lFcj/hTZaOhWwFYrgjk+Q==", "dev": true, "dependencies": { - "@babel/helper-function-name": "^7.19.0", - "@babel/template": "^7.18.10", - "@babel/traverse": "^7.19.0", - "@babel/types": "^7.19.0" + "@babel/helper-function-name": "^7.22.5", + "@babel/template": "^7.22.5", + "@babel/types": "^7.22.5" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helpers": { - "version": "7.19.0", - "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.19.0.tgz", - "integrity": "sha512-DRBCKGwIEdqY3+rPJgG/dKfQy9+08rHIAJx8q2p+HSWP87s2HCrQmaAMMyMll2kIXKCW0cO1RdQskx15Xakftg==", + "version": "7.22.6", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.22.6.tgz", + "integrity": "sha512-YjDs6y/fVOYFV8hAf1rxd1QvR9wJe1pDBZ2AREKq/SDayfPzgk0PBnVuTCE5X1acEpMMNOVUqoe+OwiZGJ+OaA==", "dev": true, "dependencies": { - "@babel/template": "^7.18.10", - "@babel/traverse": "^7.19.0", - "@babel/types": "^7.19.0" + "@babel/template": "^7.22.5", + "@babel/traverse": "^7.22.6", + "@babel/types": "^7.22.5" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/highlight": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.18.6.tgz", - "integrity": "sha512-u7stbOuYjaPezCuLj29hNW1v64M2Md2qupEKP1fHc7WdOA3DgLh37suiSrZYY7haUB7iBeQZ9P1uiRF359do3g==", + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.22.5.tgz", + "integrity": "sha512-BSKlD1hgnedS5XRnGOljZawtag7H1yPfQp0tdNJCHoH6AZ+Pcm9VvkrK59/Yy593Ypg0zMxH2BxD1VPYUQ7UIw==", "dev": true, "dependencies": { - "@babel/helper-validator-identifier": "^7.18.6", + "@babel/helper-validator-identifier": "^7.22.5", "chalk": "^2.0.0", "js-tokens": "^4.0.0" }, @@ -751,9 +747,9 @@ } }, "node_modules/@babel/parser": { - "version": "7.19.3", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.19.3.tgz", - "integrity": "sha512-pJ9xOlNWHiy9+FuFP09DEAFbAn4JskgRsVcc169w2xRBC3FRGuQEwjeIMMND9L2zc0iEhO/tGv4Zq+km+hxNpQ==", + "version": "7.22.7", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.22.7.tgz", + "integrity": "sha512-7NF8pOkHP5o2vpmGgNGcfAeCvOYhGLyA3Z4eBQkT1RJlWu47n63bCs93QfJ2hIAFCil7L5P2IWhs1oToVgrL0Q==", "dev": true, "bin": { "parser": "bin/babel-parser.js" @@ -763,12 +759,12 @@ } }, "node_modules/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression/-/plugin-bugfix-safari-id-destructuring-collision-in-function-expression-7.18.6.tgz", - "integrity": "sha512-Dgxsyg54Fx1d4Nge8UnvTrED63vrwOdPmyvPzlNN/boaliRP54pm3pGzZD1SJUwrBA+Cs/xdG8kXX6Mn/RfISQ==", + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression/-/plugin-bugfix-safari-id-destructuring-collision-in-function-expression-7.22.5.tgz", + "integrity": "sha512-NP1M5Rf+u2Gw9qfSO4ihjcTGW5zXTi36ITLd4/EoAcEhIZ0yjMqmftDNl3QC19CX7olhrjpyU454g/2W7X0jvQ==", "dev": true, "dependencies": { - "@babel/helper-plugin-utils": "^7.18.6" + "@babel/helper-plugin-utils": "^7.22.5" }, "engines": { "node": ">=6.9.0" @@ -778,14 +774,14 @@ } }, "node_modules/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": { - "version": "7.18.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining/-/plugin-bugfix-v8-spread-parameters-in-optional-chaining-7.18.9.tgz", - "integrity": "sha512-AHrP9jadvH7qlOj6PINbgSuphjQUAK7AOT7DPjBo9EHoLhQTnnK5u45e1Hd4DbSQEO9nqPWtQ89r+XEOWFScKg==", + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining/-/plugin-bugfix-v8-spread-parameters-in-optional-chaining-7.22.5.tgz", + "integrity": "sha512-31Bb65aZaUwqCbWMnZPduIZxCBngHFlzyN6Dq6KAJjtx+lx6ohKHubc61OomYi7XwVD4Ol0XCVz4h+pYFR048g==", "dev": true, "dependencies": { - "@babel/helper-plugin-utils": "^7.18.9", - "@babel/helper-skip-transparent-expression-wrappers": "^7.18.9", - "@babel/plugin-proposal-optional-chaining": "^7.18.9" + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/helper-skip-transparent-expression-wrappers": "^7.22.5", + "@babel/plugin-transform-optional-chaining": "^7.22.5" }, "engines": { "node": ">=6.9.0" @@ -794,24 +790,6 @@ "@babel/core": "^7.13.0" } }, - "node_modules/@babel/plugin-proposal-async-generator-functions": { - "version": "7.19.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-async-generator-functions/-/plugin-proposal-async-generator-functions-7.19.1.tgz", - "integrity": "sha512-0yu8vNATgLy4ivqMNBIwb1HebCelqN7YX8SL3FDXORv/RqT0zEEWUCH4GH44JsSrvCu6GqnAdR5EBFAPeNBB4Q==", - "dev": true, - "dependencies": { - "@babel/helper-environment-visitor": "^7.18.9", - "@babel/helper-plugin-utils": "^7.19.0", - "@babel/helper-remap-async-to-generator": "^7.18.9", - "@babel/plugin-syntax-async-generators": "^7.8.4" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, "node_modules/@babel/plugin-proposal-class-properties": { "version": "7.18.6", "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-class-properties/-/plugin-proposal-class-properties-7.18.6.tgz", @@ -828,34 +806,17 @@ "@babel/core": "^7.0.0-0" } }, - "node_modules/@babel/plugin-proposal-class-static-block": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-class-static-block/-/plugin-proposal-class-static-block-7.18.6.tgz", - "integrity": "sha512-+I3oIiNxrCpup3Gi8n5IGMwj0gOCAjcJUSQEcotNnCCPMEnixawOQ+KeJPlgfjzx+FKQ1QSyZOWe7wmoJp7vhw==", - "dev": true, - "dependencies": { - "@babel/helper-create-class-features-plugin": "^7.18.6", - "@babel/helper-plugin-utils": "^7.18.6", - "@babel/plugin-syntax-class-static-block": "^7.14.5" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.12.0" - } - }, "node_modules/@babel/plugin-proposal-decorators": { - "version": "7.19.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-decorators/-/plugin-proposal-decorators-7.19.3.tgz", - "integrity": "sha512-MbgXtNXqo7RTKYIXVchVJGPvaVufQH3pxvQyfbGvNw1DObIhph+PesYXJTcd8J4DdWibvf6Z2eanOyItX8WnJg==", + "version": "7.22.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-decorators/-/plugin-proposal-decorators-7.22.7.tgz", + "integrity": "sha512-omXqPF7Onq4Bb7wHxXjM3jSMSJvUUbvDvmmds7KI5n9Cq6Ln5I05I1W2nRlRof1rGdiUxJrxwe285WF96XlBXQ==", "dev": true, "dependencies": { - "@babel/helper-create-class-features-plugin": "^7.19.0", - "@babel/helper-plugin-utils": "^7.19.0", - "@babel/helper-replace-supers": "^7.19.1", - "@babel/helper-split-export-declaration": "^7.18.6", - "@babel/plugin-syntax-decorators": "^7.19.0" + "@babel/helper-create-class-features-plugin": "^7.22.6", + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/helper-replace-supers": "^7.22.5", + "@babel/helper-split-export-declaration": "^7.22.6", + "@babel/plugin-syntax-decorators": "^7.22.5" }, "engines": { "node": ">=6.9.0" @@ -865,29 +826,13 @@ } }, "node_modules/@babel/plugin-proposal-do-expressions": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-do-expressions/-/plugin-proposal-do-expressions-7.18.6.tgz", - "integrity": "sha512-ddToGCONJhCuL+l4FhtGnKl5ZYCj9fDVFiqiCdQDpeIbVn/NvMeSib+7T1/rk08jRafae4qNiP8OnJyuqlsuYA==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.18.6", - "@babel/plugin-syntax-do-expressions": "^7.18.6" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-proposal-dynamic-import": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-dynamic-import/-/plugin-proposal-dynamic-import-7.18.6.tgz", - "integrity": "sha512-1auuwmK+Rz13SJj36R+jqFPMJWyKEDd7lLSdOj4oJK0UTgGueSAtkrCvz9ewmgyU/P941Rv2fQwZJN8s6QruXw==", + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-do-expressions/-/plugin-proposal-do-expressions-7.22.5.tgz", + "integrity": "sha512-Qh6Sbkt6xCRbHykDc709db/EQB4RJlmaW3ENuvzzi7G6GKeDNQHx81P0OdI9oEXhhHEJvLRzIr08m8HNCJWS8g==", "dev": true, "dependencies": { - "@babel/helper-plugin-utils": "^7.18.6", - "@babel/plugin-syntax-dynamic-import": "^7.8.3" + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/plugin-syntax-do-expressions": "^7.22.5" }, "engines": { "node": ">=6.9.0" @@ -897,13 +842,13 @@ } }, "node_modules/@babel/plugin-proposal-export-default-from": { - "version": "7.18.10", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-export-default-from/-/plugin-proposal-export-default-from-7.18.10.tgz", - "integrity": "sha512-5H2N3R2aQFxkV4PIBUR/i7PUSwgTZjouJKzI8eKswfIjT0PhvzkPn0t0wIS5zn6maQuvtT0t1oHtMUz61LOuow==", + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-export-default-from/-/plugin-proposal-export-default-from-7.22.5.tgz", + "integrity": "sha512-UCe1X/hplyv6A5g2WnQ90tnHRvYL29dabCWww92lO7VdfMVTVReBTRrhiMrKQejHD9oVkdnRdwYuzUZkBVQisg==", "dev": true, "dependencies": { - "@babel/helper-plugin-utils": "^7.18.9", - "@babel/plugin-syntax-export-default-from": "^7.18.6" + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/plugin-syntax-export-default-from": "^7.22.5" }, "engines": { "node": ">=6.9.0" @@ -929,13 +874,13 @@ } }, "node_modules/@babel/plugin-proposal-function-bind": { - "version": "7.18.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-function-bind/-/plugin-proposal-function-bind-7.18.9.tgz", - "integrity": "sha512-9RfxqKkRBCCT0xoBl9AqieCMscJmSAL9HYixGMWH549jUpT9csWWK/HEYZEx9t9iW/PRSXgX95x9bDlgtAJGFA==", + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-function-bind/-/plugin-proposal-function-bind-7.22.5.tgz", + "integrity": "sha512-ckAugfDtdcrXKP49z7K7JI7QkA8SRidmsKxLizH8mg0UWOvcmvEd9/VDLzFcWlZqchvLDPUYpwuXNGAYjsscrw==", "dev": true, "dependencies": { - "@babel/helper-plugin-utils": "^7.18.9", - "@babel/plugin-syntax-function-bind": "^7.18.6" + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/plugin-syntax-function-bind": "^7.22.5" }, "engines": { "node": ">=6.9.0" @@ -945,14 +890,14 @@ } }, "node_modules/@babel/plugin-proposal-function-sent": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-function-sent/-/plugin-proposal-function-sent-7.18.6.tgz", - "integrity": "sha512-UdaOKPOLPt0O+Xu26tnw6oAZMLXhk+yMrXOzn6kAzTHBnWHJsoN1hlrgxFAQ+FRLS0ql1oYIQ2phvoFzmN3GMw==", + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-function-sent/-/plugin-proposal-function-sent-7.22.5.tgz", + "integrity": "sha512-YFEE5KDhaNCCD0I1j9vqPp5bpPuoDAPD+4Adk0QXdrL9TbmZluCuznuYvmlYqr14zfXCBGAZivfiLb6WI4GhSw==", "dev": true, "dependencies": { - "@babel/helper-plugin-utils": "^7.18.6", - "@babel/helper-wrap-function": "^7.18.6", - "@babel/plugin-syntax-function-sent": "^7.18.6" + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/helper-wrap-function": "^7.22.5", + "@babel/plugin-syntax-function-sent": "^7.22.5" }, "engines": { "node": ">=6.9.0" @@ -978,12 +923,12 @@ } }, "node_modules/@babel/plugin-proposal-logical-assignment-operators": { - "version": "7.18.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-logical-assignment-operators/-/plugin-proposal-logical-assignment-operators-7.18.9.tgz", - "integrity": "sha512-128YbMpjCrP35IOExw2Fq+x55LMP42DzhOhX2aNNIdI9avSWl2PI0yuBWarr3RYpZBSPtabfadkH2yeRiMD61Q==", + "version": "7.20.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-logical-assignment-operators/-/plugin-proposal-logical-assignment-operators-7.20.7.tgz", + "integrity": "sha512-y7C7cZgpMIjWlKE5T7eJwp+tnRYM89HmRvWM5EQuB5BoHEONjmQ8lSNmBUwOyy/GFRsohJED51YBF79hE1djug==", "dev": true, "dependencies": { - "@babel/helper-plugin-utils": "^7.18.9", + "@babel/helper-plugin-utils": "^7.20.2", "@babel/plugin-syntax-logical-assignment-operators": "^7.10.4" }, "engines": { @@ -1025,49 +970,14 @@ "@babel/core": "^7.0.0-0" } }, - "node_modules/@babel/plugin-proposal-object-rest-spread": { - "version": "7.18.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-object-rest-spread/-/plugin-proposal-object-rest-spread-7.18.9.tgz", - "integrity": "sha512-kDDHQ5rflIeY5xl69CEqGEZ0KY369ehsCIEbTGb4siHG5BE9sga/T0r0OUwyZNLMmZE79E1kbsqAjwFCW4ds6Q==", - "dev": true, - "dependencies": { - "@babel/compat-data": "^7.18.8", - "@babel/helper-compilation-targets": "^7.18.9", - "@babel/helper-plugin-utils": "^7.18.9", - "@babel/plugin-syntax-object-rest-spread": "^7.8.3", - "@babel/plugin-transform-parameters": "^7.18.8" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-proposal-optional-catch-binding": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-optional-catch-binding/-/plugin-proposal-optional-catch-binding-7.18.6.tgz", - "integrity": "sha512-Q40HEhs9DJQyaZfUjjn6vE8Cv4GmMHCYuMGIWUnlxH6400VGxOuwWsPt4FxXxJkC/5eOzgn0z21M9gMT4MOhbw==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.18.6", - "@babel/plugin-syntax-optional-catch-binding": "^7.8.3" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, "node_modules/@babel/plugin-proposal-optional-chaining": { - "version": "7.18.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-optional-chaining/-/plugin-proposal-optional-chaining-7.18.9.tgz", - "integrity": "sha512-v5nwt4IqBXihxGsW2QmCWMDS3B3bzGIk/EQVZz2ei7f3NJl8NzAJVvUmpDW5q1CRNY+Beb/k58UAH1Km1N411w==", + "version": "7.21.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-optional-chaining/-/plugin-proposal-optional-chaining-7.21.0.tgz", + "integrity": "sha512-p4zeefM72gpmEe2fkUr/OnOXpWEf8nAgk7ZYVqqfFiyIG7oFfVZcCrU64hWn5xp4tQ9LkV4bTIa5rD0KANpKNA==", "dev": true, "dependencies": { - "@babel/helper-plugin-utils": "^7.18.9", - "@babel/helper-skip-transparent-expression-wrappers": "^7.18.9", + "@babel/helper-plugin-utils": "^7.20.2", + "@babel/helper-skip-transparent-expression-wrappers": "^7.20.0", "@babel/plugin-syntax-optional-chaining": "^7.8.3" }, "engines": { @@ -1078,29 +988,13 @@ } }, "node_modules/@babel/plugin-proposal-pipeline-operator": { - "version": "7.18.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-pipeline-operator/-/plugin-proposal-pipeline-operator-7.18.9.tgz", - "integrity": "sha512-Pc33e6m8f4MJhRXVCUwiKZNtEm+W2CUPHIL0lyJNtkp+w6d75CLw3gsBKQ81VAMUgT9jVPIEU8gwJ5nJgmJ1Ag==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.18.9", - "@babel/plugin-syntax-pipeline-operator": "^7.18.6" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-proposal-private-methods": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-private-methods/-/plugin-proposal-private-methods-7.18.6.tgz", - "integrity": "sha512-nutsvktDItsNn4rpGItSNV2sz1XwS+nfU0Rg8aCx3W3NOKVzdMjJRu0O5OkgDp3ZGICSTbgRpxZoWsxoKRvbeA==", + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-pipeline-operator/-/plugin-proposal-pipeline-operator-7.22.5.tgz", + "integrity": "sha512-nSgHJB3uP+DORxBVhQgjub0qtU/LGj/mBDz2kQEGy1EUDy3f92VWDeB3J3/41ZjVLWmkNWOd6yjdNb4a5wDfZQ==", "dev": true, "dependencies": { - "@babel/helper-create-class-features-plugin": "^7.18.6", - "@babel/helper-plugin-utils": "^7.18.6" + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/plugin-syntax-pipeline-operator": "^7.22.5" }, "engines": { "node": ">=6.9.0" @@ -1110,16 +1004,10 @@ } }, "node_modules/@babel/plugin-proposal-private-property-in-object": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-private-property-in-object/-/plugin-proposal-private-property-in-object-7.18.6.tgz", - "integrity": "sha512-9Rysx7FOctvT5ouj5JODjAFAkgGoudQuLPamZb0v1TGLpapdNaftzifU8NTWQm0IRjqoYypdrSmyWgkocDQ8Dw==", + "version": "7.21.0-placeholder-for-preset-env.2", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-private-property-in-object/-/plugin-proposal-private-property-in-object-7.21.0-placeholder-for-preset-env.2.tgz", + "integrity": "sha512-SOSkfJDddaM7mak6cPEpswyTRnuRltl429hMraQEglW+OkovnCzsiszTmsrlY//qLFjCpQDFRvjdm2wA5pPm9w==", "dev": true, - "dependencies": { - "@babel/helper-annotate-as-pure": "^7.18.6", - "@babel/helper-create-class-features-plugin": "^7.18.6", - "@babel/helper-plugin-utils": "^7.18.6", - "@babel/plugin-syntax-private-property-in-object": "^7.14.5" - }, "engines": { "node": ">=6.9.0" }, @@ -1128,13 +1016,13 @@ } }, "node_modules/@babel/plugin-proposal-throw-expressions": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-throw-expressions/-/plugin-proposal-throw-expressions-7.18.6.tgz", - "integrity": "sha512-WHOrJyhGoGrdtW480L79cF7Iq/gZDZ/z6OqK7mVyFR5I37dTpog/wNgb6hmaM3HYZtULEJl++7VaMWkNZsOcHg==", + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-throw-expressions/-/plugin-proposal-throw-expressions-7.22.5.tgz", + "integrity": "sha512-34kY5YjNKDhjXbj2oNDkxl0xNl2+yQTEsWu8Ia6kCTb6wz76bBCd4DzmeZokfr6g68yneu3eg8qAyYgKbyesFg==", "dev": true, "dependencies": { - "@babel/helper-plugin-utils": "^7.18.6", - "@babel/plugin-syntax-throw-expressions": "^7.18.6" + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/plugin-syntax-throw-expressions": "^7.22.5" }, "engines": { "node": ">=6.9.0" @@ -1199,12 +1087,12 @@ } }, "node_modules/@babel/plugin-syntax-decorators": { - "version": "7.19.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-decorators/-/plugin-syntax-decorators-7.19.0.tgz", - "integrity": "sha512-xaBZUEDntt4faL1yN8oIFlhfXeQAWJW7CLKYsHTUqriCUbj8xOra8bfxxKGi/UwExPFBuPdH4XfHc9rGQhrVkQ==", + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-decorators/-/plugin-syntax-decorators-7.22.5.tgz", + "integrity": "sha512-avpUOBS7IU6al8MmF1XpAyj9QYeLPuSDJI5D4pVMSMdL7xQokKqJPYQC67RCT0aCTashUXPiGwMJ0DEXXCEmMA==", "dev": true, "dependencies": { - "@babel/helper-plugin-utils": "^7.19.0" + "@babel/helper-plugin-utils": "^7.22.5" }, "engines": { "node": ">=6.9.0" @@ -1214,12 +1102,12 @@ } }, "node_modules/@babel/plugin-syntax-do-expressions": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-do-expressions/-/plugin-syntax-do-expressions-7.18.6.tgz", - "integrity": "sha512-kTogvOsjBTVOSZtkkziiXB5hwGXqwhq2gBXDaiWVruRLDT7C2GqfbsMnicHJ7ePq2GE8UJeWS34YbNP6yDhwUA==", + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-do-expressions/-/plugin-syntax-do-expressions-7.22.5.tgz", + "integrity": "sha512-60pOTgQGY00/Kiozrtu286Aqg50IxDy/jIHhlMzXjYTs1Q8lbeOgqC9NLidtqfBNwdX6bZCT6FJ2i5xzt+JKzw==", "dev": true, "dependencies": { - "@babel/helper-plugin-utils": "^7.18.6" + "@babel/helper-plugin-utils": "^7.22.5" }, "engines": { "node": ">=6.9.0" @@ -1241,12 +1129,12 @@ } }, "node_modules/@babel/plugin-syntax-export-default-from": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-export-default-from/-/plugin-syntax-export-default-from-7.18.6.tgz", - "integrity": "sha512-Kr//z3ujSVNx6E9z9ih5xXXMqK07VVTuqPmqGe6Mss/zW5XPeLZeSDZoP9ab/hT4wPKqAgjl2PnhPrcpk8Seew==", + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-export-default-from/-/plugin-syntax-export-default-from-7.22.5.tgz", + "integrity": "sha512-ODAqWWXB/yReh/jVQDag/3/tl6lgBueQkk/TcfW/59Oykm4c8a55XloX0CTk2k2VJiFWMgHby9xNX29IbCv9dQ==", "dev": true, "dependencies": { - "@babel/helper-plugin-utils": "^7.18.6" + "@babel/helper-plugin-utils": "^7.22.5" }, "engines": { "node": ">=6.9.0" @@ -1268,12 +1156,12 @@ } }, "node_modules/@babel/plugin-syntax-function-bind": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-function-bind/-/plugin-syntax-function-bind-7.18.6.tgz", - "integrity": "sha512-wZN0Aq/AScknI9mKGcR3TpHdASMufFGaeJgc1rhPmLtZ/PniwjePSh8cfh8tXMB3U4kh/3cRKrLjDtedejg8jQ==", + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-function-bind/-/plugin-syntax-function-bind-7.22.5.tgz", + "integrity": "sha512-Sjy7XIhHF9L++0Mk/3Y4H4439cjI//wc/jE8Ly3+qGPkTUYYEhe4rzMv/JnyZpekfOBL22X6DAq42I7GM/3KzA==", "dev": true, "dependencies": { - "@babel/helper-plugin-utils": "^7.18.6" + "@babel/helper-plugin-utils": "^7.22.5" }, "engines": { "node": ">=6.9.0" @@ -1283,12 +1171,12 @@ } }, "node_modules/@babel/plugin-syntax-function-sent": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-function-sent/-/plugin-syntax-function-sent-7.18.6.tgz", - "integrity": "sha512-f3OJHIlFIkg+cP1Hfo2SInLhsg0pz2Ikmgo7jMdIIKC+3jVXQlHB0bgSapOWxeWI0SU28qIWmfn5ZKu1yPJHkg==", + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-function-sent/-/plugin-syntax-function-sent-7.22.5.tgz", + "integrity": "sha512-tKOWGUAVv+JGJ1tcOIFdCqxUX97lgAUnmLpWt/9JtEkgk9WQ5OolN+y9rWj6mtLM+d0kAzTGLu/kRQqr5/PEsA==", "dev": true, "dependencies": { - "@babel/helper-plugin-utils": "^7.18.6" + "@babel/helper-plugin-utils": "^7.22.5" }, "engines": { "node": ">=6.9.0" @@ -1298,12 +1186,27 @@ } }, "node_modules/@babel/plugin-syntax-import-assertions": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-assertions/-/plugin-syntax-import-assertions-7.18.6.tgz", - "integrity": "sha512-/DU3RXad9+bZwrgWJQKbr39gYbJpLJHezqEzRzi/BHRlJ9zsQb4CK2CA/5apllXNomwA1qHwzvHl+AdEmC5krQ==", + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-assertions/-/plugin-syntax-import-assertions-7.22.5.tgz", + "integrity": "sha512-rdV97N7KqsRzeNGoWUOK6yUsWarLjE5Su/Snk9IYPU9CwkWHs4t+rTGOvffTR8XGkJMTAdLfO0xVnXm8wugIJg==", "dev": true, "dependencies": { - "@babel/helper-plugin-utils": "^7.18.6" + "@babel/helper-plugin-utils": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-import-attributes": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.22.5.tgz", + "integrity": "sha512-KwvoWDeNKPETmozyFE0P2rOLqh39EoQHNjqizrI5B8Vt0ZNS7M56s7dAiAqbYfiAYOuIzIh96z3iR2ktgu3tEg==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.22.5" }, "engines": { "node": ">=6.9.0" @@ -1409,12 +1312,12 @@ } }, "node_modules/@babel/plugin-syntax-pipeline-operator": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-pipeline-operator/-/plugin-syntax-pipeline-operator-7.18.6.tgz", - "integrity": "sha512-pFtIdQomJtkTHWcNsGXhjJ5YUkL+AxJnP4G+Ol85UO6uT2fpHTPYLLE5bBeRA9cxf25qa/VKsJ3Fi67Gyqe3rA==", + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-pipeline-operator/-/plugin-syntax-pipeline-operator-7.22.5.tgz", + "integrity": "sha512-7yuGXd+h8gpR14FnPDTTCd5TfC/1B9njNZJT29GJ7UFF/WVbzkZy7728DynrENqgImqj5xyPTQAo8si9n3QVJQ==", "dev": true, "dependencies": { - "@babel/helper-plugin-utils": "^7.18.6" + "@babel/helper-plugin-utils": "^7.22.5" }, "engines": { "node": ">=6.9.0" @@ -1439,12 +1342,12 @@ } }, "node_modules/@babel/plugin-syntax-throw-expressions": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-throw-expressions/-/plugin-syntax-throw-expressions-7.18.6.tgz", - "integrity": "sha512-rp1CqEZXGv1z1YZ3qYffBH3rhnOxrTwQG8fh2yqulTurwv9zu3Gthfd+niZBLSOi1rY6146TgF+JmVeDXaX4TQ==", + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-throw-expressions/-/plugin-syntax-throw-expressions-7.22.5.tgz", + "integrity": "sha512-oCyfA7rDVcQIydA7ZOmnHCQTzz5JvG9arY++Z+ASL/q5q+mJLblaRNHoK6ggV54X2c14wCK/lQi7z1DujmEmZA==", "dev": true, "dependencies": { - "@babel/helper-plugin-utils": "^7.18.6" + "@babel/helper-plugin-utils": "^7.22.5" }, "engines": { "node": ">=6.9.0" @@ -1468,30 +1371,29 @@ "@babel/core": "^7.0.0-0" } }, - "node_modules/@babel/plugin-transform-arrow-functions": { + "node_modules/@babel/plugin-syntax-unicode-sets-regex": { "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.18.6.tgz", - "integrity": "sha512-9S9X9RUefzrsHZmKMbDXxweEH+YlE8JJEuat9FdvW9Qh1cw7W64jELCtWNkPBPX5En45uy28KGvA/AySqUh8CQ==", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-unicode-sets-regex/-/plugin-syntax-unicode-sets-regex-7.18.6.tgz", + "integrity": "sha512-727YkEAPwSIQTv5im8QHz3upqp92JTWhidIC81Tdx4VJYIte/VndKf1qKrfnnhPLiPghStWfvC/iFaMCQu7Nqg==", "dev": true, "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.18.6", "@babel/helper-plugin-utils": "^7.18.6" }, "engines": { "node": ">=6.9.0" }, "peerDependencies": { - "@babel/core": "^7.0.0-0" + "@babel/core": "^7.0.0" } }, - "node_modules/@babel/plugin-transform-async-to-generator": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-to-generator/-/plugin-transform-async-to-generator-7.18.6.tgz", - "integrity": "sha512-ARE5wZLKnTgPW7/1ftQmSi1CmkqqHo2DNmtztFhvgtOWSDfq0Cq9/9L+KnZNYSNrydBekhW3rwShduf59RoXag==", + "node_modules/@babel/plugin-transform-arrow-functions": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.22.5.tgz", + "integrity": "sha512-26lTNXoVRdAnsaDXPpvCNUq+OVWEVC6bx7Vvz9rC53F2bagUWW4u4ii2+h8Fejfh7RYqPxn+libeFBBck9muEw==", "dev": true, "dependencies": { - "@babel/helper-module-imports": "^7.18.6", - "@babel/helper-plugin-utils": "^7.18.6", - "@babel/helper-remap-async-to-generator": "^7.18.6" + "@babel/helper-plugin-utils": "^7.22.5" }, "engines": { "node": ">=6.9.0" @@ -1500,13 +1402,16 @@ "@babel/core": "^7.0.0-0" } }, - "node_modules/@babel/plugin-transform-block-scoped-functions": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoped-functions/-/plugin-transform-block-scoped-functions-7.18.6.tgz", - "integrity": "sha512-ExUcOqpPWnliRcPqves5HJcJOvHvIIWfuS4sroBUenPuMdmW+SMHDakmtS7qOo13sVppmUijqeTv7qqGsvURpQ==", + "node_modules/@babel/plugin-transform-async-generator-functions": { + "version": "7.22.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-generator-functions/-/plugin-transform-async-generator-functions-7.22.7.tgz", + "integrity": "sha512-7HmE7pk/Fmke45TODvxvkxRMV9RazV+ZZzhOL9AG8G29TLrr3jkjwF7uJfxZ30EoXpO+LJkq4oA8NjO2DTnEDg==", "dev": true, "dependencies": { - "@babel/helper-plugin-utils": "^7.18.6" + "@babel/helper-environment-visitor": "^7.22.5", + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/helper-remap-async-to-generator": "^7.22.5", + "@babel/plugin-syntax-async-generators": "^7.8.4" }, "engines": { "node": ">=6.9.0" @@ -1515,13 +1420,15 @@ "@babel/core": "^7.0.0-0" } }, - "node_modules/@babel/plugin-transform-block-scoping": { - "version": "7.18.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.18.9.tgz", - "integrity": "sha512-5sDIJRV1KtQVEbt/EIBwGy4T01uYIo4KRB3VUqzkhrAIOGx7AoctL9+Ux88btY0zXdDyPJ9mW+bg+v+XEkGmtw==", + "node_modules/@babel/plugin-transform-async-to-generator": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-to-generator/-/plugin-transform-async-to-generator-7.22.5.tgz", + "integrity": "sha512-b1A8D8ZzE/VhNDoV1MSJTnpKkCG5bJo+19R4o4oy03zM7ws8yEMK755j61Dc3EyvdysbqH5BOOTquJ7ZX9C6vQ==", "dev": true, "dependencies": { - "@babel/helper-plugin-utils": "^7.18.9" + "@babel/helper-module-imports": "^7.22.5", + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/helper-remap-async-to-generator": "^7.22.5" }, "engines": { "node": ">=6.9.0" @@ -1530,21 +1437,13 @@ "@babel/core": "^7.0.0-0" } }, - "node_modules/@babel/plugin-transform-classes": { - "version": "7.19.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-classes/-/plugin-transform-classes-7.19.0.tgz", - "integrity": "sha512-YfeEE9kCjqTS9IitkgfJuxjcEtLUHMqa8yUJ6zdz8vR7hKuo6mOy2C05P0F1tdMmDCeuyidKnlrw/iTppHcr2A==", - "dev": true, - "dependencies": { - "@babel/helper-annotate-as-pure": "^7.18.6", - "@babel/helper-compilation-targets": "^7.19.0", - "@babel/helper-environment-visitor": "^7.18.9", - "@babel/helper-function-name": "^7.19.0", - "@babel/helper-optimise-call-expression": "^7.18.6", - "@babel/helper-plugin-utils": "^7.19.0", - "@babel/helper-replace-supers": "^7.18.9", - "@babel/helper-split-export-declaration": "^7.18.6", - "globals": "^11.1.0" + "node_modules/@babel/plugin-transform-block-scoped-functions": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoped-functions/-/plugin-transform-block-scoped-functions-7.22.5.tgz", + "integrity": "sha512-tdXZ2UdknEKQWKJP1KMNmuF5Lx3MymtMN/pvA+p/VEkhK8jVcQ1fzSy8KM9qRYhAf2/lV33hoMPKI/xaI9sADA==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.22.5" }, "engines": { "node": ">=6.9.0" @@ -1553,22 +1452,13 @@ "@babel/core": "^7.0.0-0" } }, - "node_modules/@babel/plugin-transform-classes/node_modules/globals": { - "version": "11.12.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", - "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", - "dev": true, - "engines": { - "node": ">=4" - } - }, - "node_modules/@babel/plugin-transform-computed-properties": { - "version": "7.18.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-computed-properties/-/plugin-transform-computed-properties-7.18.9.tgz", - "integrity": "sha512-+i0ZU1bCDymKakLxn5srGHrsAPRELC2WIbzwjLhHW9SIE1cPYkLCL0NlnXMZaM1vhfgA2+M7hySk42VBvrkBRw==", + "node_modules/@babel/plugin-transform-block-scoping": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.22.5.tgz", + "integrity": "sha512-EcACl1i5fSQ6bt+YGuU/XGCeZKStLmyVGytWkpyhCLeQVA0eu6Wtiw92V+I1T/hnezUv7j74dA/Ro69gWcU+hg==", "dev": true, "dependencies": { - "@babel/helper-plugin-utils": "^7.18.9" + "@babel/helper-plugin-utils": "^7.22.5" }, "engines": { "node": ">=6.9.0" @@ -1577,13 +1467,14 @@ "@babel/core": "^7.0.0-0" } }, - "node_modules/@babel/plugin-transform-destructuring": { - "version": "7.18.13", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.18.13.tgz", - "integrity": "sha512-TodpQ29XekIsex2A+YJPj5ax2plkGa8YYY6mFjCohk/IG9IY42Rtuj1FuDeemfg2ipxIFLzPeA83SIBnlhSIow==", + "node_modules/@babel/plugin-transform-class-properties": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-class-properties/-/plugin-transform-class-properties-7.22.5.tgz", + "integrity": "sha512-nDkQ0NfkOhPTq8YCLiWNxp1+f9fCobEjCb0n8WdbNUBc4IB5V7P1QnX9IjpSoquKrXF5SKojHleVNs2vGeHCHQ==", "dev": true, "dependencies": { - "@babel/helper-plugin-utils": "^7.18.9" + "@babel/helper-create-class-features-plugin": "^7.22.5", + "@babel/helper-plugin-utils": "^7.22.5" }, "engines": { "node": ">=6.9.0" @@ -1592,29 +1483,125 @@ "@babel/core": "^7.0.0-0" } }, - "node_modules/@babel/plugin-transform-dotall-regex": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-dotall-regex/-/plugin-transform-dotall-regex-7.18.6.tgz", - "integrity": "sha512-6S3jpun1eEbAxq7TdjLotAsl4WpQI9DxfkycRcKrjhQYzU87qpXdknpBg/e+TdcMehqGnLFi7tnFUBR02Vq6wg==", + "node_modules/@babel/plugin-transform-class-static-block": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-class-static-block/-/plugin-transform-class-static-block-7.22.5.tgz", + "integrity": "sha512-SPToJ5eYZLxlnp1UzdARpOGeC2GbHvr9d/UV0EukuVx8atktg194oe+C5BqQ8jRTkgLRVOPYeXRSBg1IlMoVRA==", "dev": true, "dependencies": { - "@babel/helper-create-regexp-features-plugin": "^7.18.6", - "@babel/helper-plugin-utils": "^7.18.6" + "@babel/helper-create-class-features-plugin": "^7.22.5", + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/plugin-syntax-class-static-block": "^7.14.5" }, "engines": { "node": ">=6.9.0" }, "peerDependencies": { - "@babel/core": "^7.0.0-0" + "@babel/core": "^7.12.0" } }, - "node_modules/@babel/plugin-transform-duplicate-keys": { - "version": "7.18.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-duplicate-keys/-/plugin-transform-duplicate-keys-7.18.9.tgz", - "integrity": "sha512-d2bmXCtZXYc59/0SanQKbiWINadaJXqtvIQIzd4+hNwkWBgyCd5F/2t1kXoUdvPMrxzPvhK6EMQRROxsue+mfw==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.18.9" + "node_modules/@babel/plugin-transform-classes": { + "version": "7.22.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-classes/-/plugin-transform-classes-7.22.6.tgz", + "integrity": "sha512-58EgM6nuPNG6Py4Z3zSuu0xWu2VfodiMi72Jt5Kj2FECmaYk1RrTXA45z6KBFsu9tRgwQDwIiY4FXTt+YsSFAQ==", + "dev": true, + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.22.5", + "@babel/helper-compilation-targets": "^7.22.6", + "@babel/helper-environment-visitor": "^7.22.5", + "@babel/helper-function-name": "^7.22.5", + "@babel/helper-optimise-call-expression": "^7.22.5", + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/helper-replace-supers": "^7.22.5", + "@babel/helper-split-export-declaration": "^7.22.6", + "globals": "^11.1.0" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-classes/node_modules/globals": { + "version": "11.12.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", + "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/plugin-transform-computed-properties": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-computed-properties/-/plugin-transform-computed-properties-7.22.5.tgz", + "integrity": "sha512-4GHWBgRf0krxPX+AaPtgBAlTgTeZmqDynokHOX7aqqAB4tHs3U2Y02zH6ETFdLZGcg9UQSD1WCmkVrE9ErHeOg==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/template": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-destructuring": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.22.5.tgz", + "integrity": "sha512-GfqcFuGW8vnEqTUBM7UtPd5A4q797LTvvwKxXTgRsFjoqaJiEg9deBG6kWeQYkVEL569NpnmpC0Pkr/8BLKGnQ==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-dotall-regex": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-dotall-regex/-/plugin-transform-dotall-regex-7.22.5.tgz", + "integrity": "sha512-5/Yk9QxCQCl+sOIB1WelKnVRxTJDSAIxtJLL2/pqL14ZVlbH0fUQUZa/T5/UnQtBNgghR7mfB8ERBKyKPCi7Vw==", + "dev": true, + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.22.5", + "@babel/helper-plugin-utils": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-duplicate-keys": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-duplicate-keys/-/plugin-transform-duplicate-keys-7.22.5.tgz", + "integrity": "sha512-dEnYD+9BBgld5VBXHnF/DbYGp3fqGMsyxKbtD1mDyIA7AkTSpKXFhCVuj/oQVOoALfBs77DudA0BE4d5mcpmqw==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-dynamic-import": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-dynamic-import/-/plugin-transform-dynamic-import-7.22.5.tgz", + "integrity": "sha512-0MC3ppTB1AMxd8fXjSrbPa7LT9hrImt+/fcj+Pg5YMD7UQyWp/02+JWpdnCymmsXwIx5Z+sYn1bwCn4ZJNvhqQ==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/plugin-syntax-dynamic-import": "^7.8.3" }, "engines": { "node": ">=6.9.0" @@ -1624,13 +1611,29 @@ } }, "node_modules/@babel/plugin-transform-exponentiation-operator": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-exponentiation-operator/-/plugin-transform-exponentiation-operator-7.18.6.tgz", - "integrity": "sha512-wzEtc0+2c88FVR34aQmiz56dxEkxr2g8DQb/KfaFa1JYXOFVsbhvAonFN6PwVWj++fKmku8NP80plJ5Et4wqHw==", + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-exponentiation-operator/-/plugin-transform-exponentiation-operator-7.22.5.tgz", + "integrity": "sha512-vIpJFNM/FjZ4rh1myqIya9jXwrwwgFRHPjT3DkUA9ZLHuzox8jiXkOLvwm1H+PQIP3CqfC++WPKeuDi0Sjdj1g==", "dev": true, "dependencies": { - "@babel/helper-builder-binary-assignment-operator-visitor": "^7.18.6", - "@babel/helper-plugin-utils": "^7.18.6" + "@babel/helper-builder-binary-assignment-operator-visitor": "^7.22.5", + "@babel/helper-plugin-utils": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-export-namespace-from": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-export-namespace-from/-/plugin-transform-export-namespace-from-7.22.5.tgz", + "integrity": "sha512-X4hhm7FRnPgd4nDA4b/5V280xCx6oL7Oob5+9qVS5C13Zq4bh1qq7LU0GgRU6b5dBWBvhGaXYVB4AcN6+ol6vg==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/plugin-syntax-export-namespace-from": "^7.8.3" }, "engines": { "node": ">=6.9.0" @@ -1640,12 +1643,12 @@ } }, "node_modules/@babel/plugin-transform-for-of": { - "version": "7.18.8", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.18.8.tgz", - "integrity": "sha512-yEfTRnjuskWYo0k1mHUqrVWaZwrdq8AYbfrpqULOJOaucGSp4mNMVps+YtA8byoevxS/urwU75vyhQIxcCgiBQ==", + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.22.5.tgz", + "integrity": "sha512-3kxQjX1dU9uudwSshyLeEipvrLjBCVthCgeTp6CzE/9JYrlAIaeekVxRpCWsDDfYTfRZRoCeZatCQvwo+wvK8A==", "dev": true, "dependencies": { - "@babel/helper-plugin-utils": "^7.18.6" + "@babel/helper-plugin-utils": "^7.22.5" }, "engines": { "node": ">=6.9.0" @@ -1655,14 +1658,30 @@ } }, "node_modules/@babel/plugin-transform-function-name": { - "version": "7.18.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-function-name/-/plugin-transform-function-name-7.18.9.tgz", - "integrity": "sha512-WvIBoRPaJQ5yVHzcnJFor7oS5Ls0PYixlTYE63lCj2RtdQEl15M68FXQlxnG6wdraJIXRdR7KI+hQ7q/9QjrCQ==", + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-function-name/-/plugin-transform-function-name-7.22.5.tgz", + "integrity": "sha512-UIzQNMS0p0HHiQm3oelztj+ECwFnj+ZRV4KnguvlsD2of1whUeM6o7wGNj6oLwcDoAXQ8gEqfgC24D+VdIcevg==", "dev": true, "dependencies": { - "@babel/helper-compilation-targets": "^7.18.9", - "@babel/helper-function-name": "^7.18.9", - "@babel/helper-plugin-utils": "^7.18.9" + "@babel/helper-compilation-targets": "^7.22.5", + "@babel/helper-function-name": "^7.22.5", + "@babel/helper-plugin-utils": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-json-strings": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-json-strings/-/plugin-transform-json-strings-7.22.5.tgz", + "integrity": "sha512-DuCRB7fu8MyTLbEQd1ew3R85nx/88yMoqo2uPSjevMj3yoN7CDM8jkgrY0wmVxfJZyJ/B9fE1iq7EQppWQmR5A==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/plugin-syntax-json-strings": "^7.8.3" }, "engines": { "node": ">=6.9.0" @@ -1672,12 +1691,28 @@ } }, "node_modules/@babel/plugin-transform-literals": { - "version": "7.18.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-literals/-/plugin-transform-literals-7.18.9.tgz", - "integrity": "sha512-IFQDSRoTPnrAIrI5zoZv73IFeZu2dhu6irxQjY9rNjTT53VmKg9fenjvoiOWOkJ6mm4jKVPtdMzBY98Fp4Z4cg==", + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-literals/-/plugin-transform-literals-7.22.5.tgz", + "integrity": "sha512-fTLj4D79M+mepcw3dgFBTIDYpbcB9Sm0bpm4ppXPaO+U+PKFFyV9MGRvS0gvGw62sd10kT5lRMKXAADb9pWy8g==", "dev": true, "dependencies": { - "@babel/helper-plugin-utils": "^7.18.9" + "@babel/helper-plugin-utils": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-logical-assignment-operators": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-logical-assignment-operators/-/plugin-transform-logical-assignment-operators-7.22.5.tgz", + "integrity": "sha512-MQQOUW1KL8X0cDWfbwYP+TbVbZm16QmQXJQ+vndPtH/BoO0lOKpVoEDMI7+PskYxH+IiE0tS8xZye0qr1lGzSA==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/plugin-syntax-logical-assignment-operators": "^7.10.4" }, "engines": { "node": ">=6.9.0" @@ -1687,12 +1722,12 @@ } }, "node_modules/@babel/plugin-transform-member-expression-literals": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-member-expression-literals/-/plugin-transform-member-expression-literals-7.18.6.tgz", - "integrity": "sha512-qSF1ihLGO3q+/g48k85tUjD033C29TNTVB2paCwZPVmOsjn9pClvYYrM2VeJpBY2bcNkuny0YUyTNRyRxJ54KA==", + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-member-expression-literals/-/plugin-transform-member-expression-literals-7.22.5.tgz", + "integrity": "sha512-RZEdkNtzzYCFl9SE9ATaUMTj2hqMb4StarOJLrZRbqqU4HSBE7UlBw9WBWQiDzrJZJdUWiMTVDI6Gv/8DPvfew==", "dev": true, "dependencies": { - "@babel/helper-plugin-utils": "^7.18.6" + "@babel/helper-plugin-utils": "^7.22.5" }, "engines": { "node": ">=6.9.0" @@ -1702,14 +1737,13 @@ } }, "node_modules/@babel/plugin-transform-modules-amd": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-amd/-/plugin-transform-modules-amd-7.18.6.tgz", - "integrity": "sha512-Pra5aXsmTsOnjM3IajS8rTaLCy++nGM4v3YR4esk5PCsyg9z8NA5oQLwxzMUtDBd8F+UmVza3VxoAaWCbzH1rg==", + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-amd/-/plugin-transform-modules-amd-7.22.5.tgz", + "integrity": "sha512-R+PTfLTcYEmb1+kK7FNkhQ1gP4KgjpSO6HfH9+f8/yfp2Nt3ggBjiVpRwmwTlfqZLafYKJACy36yDXlEmI9HjQ==", "dev": true, "dependencies": { - "@babel/helper-module-transforms": "^7.18.6", - "@babel/helper-plugin-utils": "^7.18.6", - "babel-plugin-dynamic-import-node": "^2.3.3" + "@babel/helper-module-transforms": "^7.22.5", + "@babel/helper-plugin-utils": "^7.22.5" }, "engines": { "node": ">=6.9.0" @@ -1719,15 +1753,14 @@ } }, "node_modules/@babel/plugin-transform-modules-commonjs": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.18.6.tgz", - "integrity": "sha512-Qfv2ZOWikpvmedXQJDSbxNqy7Xr/j2Y8/KfijM0iJyKkBTmWuvCA1yeH1yDM7NJhBW/2aXxeucLj6i80/LAJ/Q==", + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.22.5.tgz", + "integrity": "sha512-B4pzOXj+ONRmuaQTg05b3y/4DuFz3WcCNAXPLb2Q0GT0TrGKGxNKV4jwsXts+StaM0LQczZbOpj8o1DLPDJIiA==", "dev": true, "dependencies": { - "@babel/helper-module-transforms": "^7.18.6", - "@babel/helper-plugin-utils": "^7.18.6", - "@babel/helper-simple-access": "^7.18.6", - "babel-plugin-dynamic-import-node": "^2.3.3" + "@babel/helper-module-transforms": "^7.22.5", + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/helper-simple-access": "^7.22.5" }, "engines": { "node": ">=6.9.0" @@ -1737,16 +1770,15 @@ } }, "node_modules/@babel/plugin-transform-modules-systemjs": { - "version": "7.19.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.19.0.tgz", - "integrity": "sha512-x9aiR0WXAWmOWsqcsnrzGR+ieaTMVyGyffPVA7F8cXAGt/UxefYv6uSHZLkAFChN5M5Iy1+wjE+xJuPt22H39A==", + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.22.5.tgz", + "integrity": "sha512-emtEpoaTMsOs6Tzz+nbmcePl6AKVtS1yC4YNAeMun9U8YCsgadPNxnOPQ8GhHFB2qdx+LZu9LgoC0Lthuu05DQ==", "dev": true, "dependencies": { - "@babel/helper-hoist-variables": "^7.18.6", - "@babel/helper-module-transforms": "^7.19.0", - "@babel/helper-plugin-utils": "^7.19.0", - "@babel/helper-validator-identifier": "^7.18.6", - "babel-plugin-dynamic-import-node": "^2.3.3" + "@babel/helper-hoist-variables": "^7.22.5", + "@babel/helper-module-transforms": "^7.22.5", + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/helper-validator-identifier": "^7.22.5" }, "engines": { "node": ">=6.9.0" @@ -1756,13 +1788,13 @@ } }, "node_modules/@babel/plugin-transform-modules-umd": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-umd/-/plugin-transform-modules-umd-7.18.6.tgz", - "integrity": "sha512-dcegErExVeXcRqNtkRU/z8WlBLnvD4MRnHgNs3MytRO1Mn1sHRyhbcpYbVMGclAqOjdW+9cfkdZno9dFdfKLfQ==", + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-umd/-/plugin-transform-modules-umd-7.22.5.tgz", + "integrity": "sha512-+S6kzefN/E1vkSsKx8kmQuqeQsvCKCd1fraCM7zXm4SFoggI099Tr4G8U81+5gtMdUeMQ4ipdQffbKLX0/7dBQ==", "dev": true, "dependencies": { - "@babel/helper-module-transforms": "^7.18.6", - "@babel/helper-plugin-utils": "^7.18.6" + "@babel/helper-module-transforms": "^7.22.5", + "@babel/helper-plugin-utils": "^7.22.5" }, "engines": { "node": ">=6.9.0" @@ -1772,13 +1804,13 @@ } }, "node_modules/@babel/plugin-transform-named-capturing-groups-regex": { - "version": "7.19.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-named-capturing-groups-regex/-/plugin-transform-named-capturing-groups-regex-7.19.1.tgz", - "integrity": "sha512-oWk9l9WItWBQYS4FgXD4Uyy5kq898lvkXpXQxoJEY1RnvPk4R/Dvu2ebXU9q8lP+rlMwUQTFf2Ok6d78ODa0kw==", + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-named-capturing-groups-regex/-/plugin-transform-named-capturing-groups-regex-7.22.5.tgz", + "integrity": "sha512-YgLLKmS3aUBhHaxp5hi1WJTgOUb/NCuDHzGT9z9WTt3YG+CPRhJs6nprbStx6DnWM4dh6gt7SU3sZodbZ08adQ==", "dev": true, "dependencies": { - "@babel/helper-create-regexp-features-plugin": "^7.19.0", - "@babel/helper-plugin-utils": "^7.19.0" + "@babel/helper-create-regexp-features-plugin": "^7.22.5", + "@babel/helper-plugin-utils": "^7.22.5" }, "engines": { "node": ">=6.9.0" @@ -1788,12 +1820,63 @@ } }, "node_modules/@babel/plugin-transform-new-target": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-new-target/-/plugin-transform-new-target-7.18.6.tgz", - "integrity": "sha512-DjwFA/9Iu3Z+vrAn+8pBUGcjhxKguSMlsFqeCKbhb9BAV756v0krzVK04CRDi/4aqmk8BsHb4a/gFcaA5joXRw==", + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-new-target/-/plugin-transform-new-target-7.22.5.tgz", + "integrity": "sha512-AsF7K0Fx/cNKVyk3a+DW0JLo+Ua598/NxMRvxDnkpCIGFh43+h/v2xyhRUYf6oD8gE4QtL83C7zZVghMjHd+iw==", "dev": true, "dependencies": { - "@babel/helper-plugin-utils": "^7.18.6" + "@babel/helper-plugin-utils": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-nullish-coalescing-operator": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-nullish-coalescing-operator/-/plugin-transform-nullish-coalescing-operator-7.22.5.tgz", + "integrity": "sha512-6CF8g6z1dNYZ/VXok5uYkkBBICHZPiGEl7oDnAx2Mt1hlHVHOSIKWJaXHjQJA5VB43KZnXZDIexMchY4y2PGdA==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-numeric-separator": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-numeric-separator/-/plugin-transform-numeric-separator-7.22.5.tgz", + "integrity": "sha512-NbslED1/6M+sXiwwtcAB/nieypGw02Ejf4KtDeMkCEpP6gWFMX1wI9WKYua+4oBneCCEmulOkRpwywypVZzs/g==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/plugin-syntax-numeric-separator": "^7.10.4" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-object-rest-spread": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-rest-spread/-/plugin-transform-object-rest-spread-7.22.5.tgz", + "integrity": "sha512-Kk3lyDmEslH9DnvCDA1s1kkd3YWQITiBOHngOtDL9Pt6BZjzqb6hiOlb8VfjiiQJ2unmegBqZu0rx5RxJb5vmQ==", + "dev": true, + "dependencies": { + "@babel/compat-data": "^7.22.5", + "@babel/helper-compilation-targets": "^7.22.5", + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/plugin-syntax-object-rest-spread": "^7.8.3", + "@babel/plugin-transform-parameters": "^7.22.5" }, "engines": { "node": ">=6.9.0" @@ -1803,13 +1886,46 @@ } }, "node_modules/@babel/plugin-transform-object-super": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-super/-/plugin-transform-object-super-7.18.6.tgz", - "integrity": "sha512-uvGz6zk+pZoS1aTZrOvrbj6Pp/kK2mp45t2B+bTDre2UgsZZ8EZLSJtUg7m/no0zOJUWgFONpB7Zv9W2tSaFlA==", + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-super/-/plugin-transform-object-super-7.22.5.tgz", + "integrity": "sha512-klXqyaT9trSjIUrcsYIfETAzmOEZL3cBYqOYLJxBHfMFFggmXOv+NYSX/Jbs9mzMVESw/WycLFPRx8ba/b2Ipw==", "dev": true, "dependencies": { - "@babel/helper-plugin-utils": "^7.18.6", - "@babel/helper-replace-supers": "^7.18.6" + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/helper-replace-supers": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-optional-catch-binding": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-optional-catch-binding/-/plugin-transform-optional-catch-binding-7.22.5.tgz", + "integrity": "sha512-pH8orJahy+hzZje5b8e2QIlBWQvGpelS76C63Z+jhZKsmzfNaPQ+LaW6dcJ9bxTpo1mtXbgHwy765Ro3jftmUg==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/plugin-syntax-optional-catch-binding": "^7.8.3" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-optional-chaining": { + "version": "7.22.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-optional-chaining/-/plugin-transform-optional-chaining-7.22.6.tgz", + "integrity": "sha512-Vd5HiWml0mDVtcLHIoEU5sw6HOUW/Zk0acLs/SAeuLzkGNOPc9DB4nkUajemhCmTIz3eiaKREZn2hQQqF79YTg==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/helper-skip-transparent-expression-wrappers": "^7.22.5", + "@babel/plugin-syntax-optional-chaining": "^7.8.3" }, "engines": { "node": ">=6.9.0" @@ -1819,12 +1935,46 @@ } }, "node_modules/@babel/plugin-transform-parameters": { - "version": "7.18.8", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.18.8.tgz", - "integrity": "sha512-ivfbE3X2Ss+Fj8nnXvKJS6sjRG4gzwPMsP+taZC+ZzEGjAYlvENixmt1sZ5Ca6tWls+BlKSGKPJ6OOXvXCbkFg==", + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.22.5.tgz", + "integrity": "sha512-AVkFUBurORBREOmHRKo06FjHYgjrabpdqRSwq6+C7R5iTCZOsM4QbcB27St0a4U6fffyAOqh3s/qEfybAhfivg==", "dev": true, "dependencies": { - "@babel/helper-plugin-utils": "^7.18.6" + "@babel/helper-plugin-utils": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-private-methods": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-private-methods/-/plugin-transform-private-methods-7.22.5.tgz", + "integrity": "sha512-PPjh4gyrQnGe97JTalgRGMuU4icsZFnWkzicB/fUtzlKUqvsWBKEpPPfr5a2JiyirZkHxnAqkQMO5Z5B2kK3fA==", + "dev": true, + "dependencies": { + "@babel/helper-create-class-features-plugin": "^7.22.5", + "@babel/helper-plugin-utils": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-private-property-in-object": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-private-property-in-object/-/plugin-transform-private-property-in-object-7.22.5.tgz", + "integrity": "sha512-/9xnaTTJcVoBtSSmrVyhtSvO3kbqS2ODoh2juEU72c3aYonNF0OMGiaz2gjukyKM2wBBYJP38S4JiE0Wfb5VMQ==", + "dev": true, + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.22.5", + "@babel/helper-create-class-features-plugin": "^7.22.5", + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/plugin-syntax-private-property-in-object": "^7.14.5" }, "engines": { "node": ">=6.9.0" @@ -1834,12 +1984,12 @@ } }, "node_modules/@babel/plugin-transform-property-literals": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-property-literals/-/plugin-transform-property-literals-7.18.6.tgz", - "integrity": "sha512-cYcs6qlgafTud3PAzrrRNbQtfpQ8+y/+M5tKmksS9+M1ckbH6kzY8MrexEM9mcA6JDsukE19iIRvAyYl463sMg==", + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-property-literals/-/plugin-transform-property-literals-7.22.5.tgz", + "integrity": "sha512-TiOArgddK3mK/x1Qwf5hay2pxI6wCZnvQqrFSqbtg1GLl2JcNMitVH/YnqjP+M31pLUeTfzY1HAXFDnUBV30rQ==", "dev": true, "dependencies": { - "@babel/helper-plugin-utils": "^7.18.6" + "@babel/helper-plugin-utils": "^7.22.5" }, "engines": { "node": ">=6.9.0" @@ -1849,13 +1999,13 @@ } }, "node_modules/@babel/plugin-transform-regenerator": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.18.6.tgz", - "integrity": "sha512-poqRI2+qiSdeldcz4wTSTXBRryoq3Gc70ye7m7UD5Ww0nE29IXqMl6r7Nd15WBgRd74vloEMlShtH6CKxVzfmQ==", + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.22.5.tgz", + "integrity": "sha512-rR7KePOE7gfEtNTh9Qw+iO3Q/e4DEsoQ+hdvM6QUDH7JRJ5qxq5AA52ZzBWbI5i9lfNuvySgOGP8ZN7LAmaiPw==", "dev": true, "dependencies": { - "@babel/helper-plugin-utils": "^7.18.6", - "regenerator-transform": "^0.15.0" + "@babel/helper-plugin-utils": "^7.22.5", + "regenerator-transform": "^0.15.1" }, "engines": { "node": ">=6.9.0" @@ -1865,12 +2015,12 @@ } }, "node_modules/@babel/plugin-transform-reserved-words": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-reserved-words/-/plugin-transform-reserved-words-7.18.6.tgz", - "integrity": "sha512-oX/4MyMoypzHjFrT1CdivfKZ+XvIPMFXwwxHp/r0Ddy2Vuomt4HDFGmft1TAY2yiTKiNSsh3kjBAzcM8kSdsjA==", + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-reserved-words/-/plugin-transform-reserved-words-7.22.5.tgz", + "integrity": "sha512-DTtGKFRQUDm8svigJzZHzb/2xatPc6TzNvAIJ5GqOKDsGFYgAskjRulbR/vGsPKq3OPqtexnz327qYpP57RFyA==", "dev": true, "dependencies": { - "@babel/helper-plugin-utils": "^7.18.6" + "@babel/helper-plugin-utils": "^7.22.5" }, "engines": { "node": ">=6.9.0" @@ -1880,12 +2030,12 @@ } }, "node_modules/@babel/plugin-transform-shorthand-properties": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-shorthand-properties/-/plugin-transform-shorthand-properties-7.18.6.tgz", - "integrity": "sha512-eCLXXJqv8okzg86ywZJbRn19YJHU4XUa55oz2wbHhaQVn/MM+XhukiT7SYqp/7o00dg52Rj51Ny+Ecw4oyoygw==", + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-shorthand-properties/-/plugin-transform-shorthand-properties-7.22.5.tgz", + "integrity": "sha512-vM4fq9IXHscXVKzDv5itkO1X52SmdFBFcMIBZ2FRn2nqVYqw6dBexUgMvAjHW+KXpPPViD/Yo3GrDEBaRC0QYA==", "dev": true, "dependencies": { - "@babel/helper-plugin-utils": "^7.18.6" + "@babel/helper-plugin-utils": "^7.22.5" }, "engines": { "node": ">=6.9.0" @@ -1895,13 +2045,13 @@ } }, "node_modules/@babel/plugin-transform-spread": { - "version": "7.19.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-spread/-/plugin-transform-spread-7.19.0.tgz", - "integrity": "sha512-RsuMk7j6n+r752EtzyScnWkQyuJdli6LdO5Klv8Yx0OfPVTcQkIUfS8clx5e9yHXzlnhOZF3CbQ8C2uP5j074w==", + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-spread/-/plugin-transform-spread-7.22.5.tgz", + "integrity": "sha512-5ZzDQIGyvN4w8+dMmpohL6MBo+l2G7tfC/O2Dg7/hjpgeWvUx8FzfeOKxGog9IimPa4YekaQ9PlDqTLOljkcxg==", "dev": true, "dependencies": { - "@babel/helper-plugin-utils": "^7.19.0", - "@babel/helper-skip-transparent-expression-wrappers": "^7.18.9" + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/helper-skip-transparent-expression-wrappers": "^7.22.5" }, "engines": { "node": ">=6.9.0" @@ -1911,12 +2061,12 @@ } }, "node_modules/@babel/plugin-transform-sticky-regex": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-sticky-regex/-/plugin-transform-sticky-regex-7.18.6.tgz", - "integrity": "sha512-kfiDrDQ+PBsQDO85yj1icueWMfGfJFKN1KCkndygtu/C9+XUfydLC8Iv5UYJqRwy4zk8EcplRxEOeLyjq1gm6Q==", + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-sticky-regex/-/plugin-transform-sticky-regex-7.22.5.tgz", + "integrity": "sha512-zf7LuNpHG0iEeiyCNwX4j3gDg1jgt1k3ZdXBKbZSoA3BbGQGvMiSvfbZRR3Dr3aeJe3ooWFZxOOG3IRStYp2Bw==", "dev": true, "dependencies": { - "@babel/helper-plugin-utils": "^7.18.6" + "@babel/helper-plugin-utils": "^7.22.5" }, "engines": { "node": ">=6.9.0" @@ -1926,12 +2076,12 @@ } }, "node_modules/@babel/plugin-transform-template-literals": { - "version": "7.18.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-template-literals/-/plugin-transform-template-literals-7.18.9.tgz", - "integrity": "sha512-S8cOWfT82gTezpYOiVaGHrCbhlHgKhQt8XH5ES46P2XWmX92yisoZywf5km75wv5sYcXDUCLMmMxOLCtthDgMA==", + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-template-literals/-/plugin-transform-template-literals-7.22.5.tgz", + "integrity": "sha512-5ciOehRNf+EyUeewo8NkbQiUs4d6ZxiHo6BcBcnFlgiJfu16q0bQUw9Jvo0b0gBKFG1SMhDSjeKXSYuJLeFSMA==", "dev": true, "dependencies": { - "@babel/helper-plugin-utils": "^7.18.9" + "@babel/helper-plugin-utils": "^7.22.5" }, "engines": { "node": ">=6.9.0" @@ -1941,12 +2091,12 @@ } }, "node_modules/@babel/plugin-transform-typeof-symbol": { - "version": "7.18.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typeof-symbol/-/plugin-transform-typeof-symbol-7.18.9.tgz", - "integrity": "sha512-SRfwTtF11G2aemAZWivL7PD+C9z52v9EvMqH9BuYbabyPuKUvSWks3oCg6041pT925L4zVFqaVBeECwsmlguEw==", + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typeof-symbol/-/plugin-transform-typeof-symbol-7.22.5.tgz", + "integrity": "sha512-bYkI5lMzL4kPii4HHEEChkD0rkc+nvnlR6+o/qdqR6zrm0Sv/nodmyLhlq2DO0YKLUNd2VePmPRjJXSBh9OIdA==", "dev": true, "dependencies": { - "@babel/helper-plugin-utils": "^7.18.9" + "@babel/helper-plugin-utils": "^7.22.5" }, "engines": { "node": ">=6.9.0" @@ -1956,12 +2106,28 @@ } }, "node_modules/@babel/plugin-transform-unicode-escapes": { - "version": "7.18.10", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-escapes/-/plugin-transform-unicode-escapes-7.18.10.tgz", - "integrity": "sha512-kKAdAI+YzPgGY/ftStBFXTI1LZFju38rYThnfMykS+IXy8BVx+res7s2fxf1l8I35DV2T97ezo6+SGrXz6B3iQ==", + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-escapes/-/plugin-transform-unicode-escapes-7.22.5.tgz", + "integrity": "sha512-biEmVg1IYB/raUO5wT1tgfacCef15Fbzhkx493D3urBI++6hpJ+RFG4SrWMn0NEZLfvilqKf3QDrRVZHo08FYg==", "dev": true, "dependencies": { - "@babel/helper-plugin-utils": "^7.18.9" + "@babel/helper-plugin-utils": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-unicode-property-regex": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-property-regex/-/plugin-transform-unicode-property-regex-7.22.5.tgz", + "integrity": "sha512-HCCIb+CbJIAE6sXn5CjFQXMwkCClcOfPCzTlilJ8cUatfzwHlWQkbtV0zD338u9dZskwvuOYTuuaMaA8J5EI5A==", + "dev": true, + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.22.5", + "@babel/helper-plugin-utils": "^7.22.5" }, "engines": { "node": ">=6.9.0" @@ -1971,13 +2137,13 @@ } }, "node_modules/@babel/plugin-transform-unicode-regex": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-regex/-/plugin-transform-unicode-regex-7.18.6.tgz", - "integrity": "sha512-gE7A6Lt7YLnNOL3Pb9BNeZvi+d8l7tcRrG4+pwJjK9hD2xX4mEvjlQW60G9EEmfXVYRPv9VRQcyegIVHCql/AA==", + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-regex/-/plugin-transform-unicode-regex-7.22.5.tgz", + "integrity": "sha512-028laaOKptN5vHJf9/Arr/HiJekMd41hOEZYvNsrsXqJ7YPYuX2bQxh31fkZzGmq3YqHRJzYFFAVYvKfMPKqyg==", "dev": true, "dependencies": { - "@babel/helper-create-regexp-features-plugin": "^7.18.6", - "@babel/helper-plugin-utils": "^7.18.6" + "@babel/helper-create-regexp-features-plugin": "^7.22.5", + "@babel/helper-plugin-utils": "^7.22.5" }, "engines": { "node": ">=6.9.0" @@ -1986,39 +2152,43 @@ "@babel/core": "^7.0.0-0" } }, + "node_modules/@babel/plugin-transform-unicode-sets-regex": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-sets-regex/-/plugin-transform-unicode-sets-regex-7.22.5.tgz", + "integrity": "sha512-lhMfi4FC15j13eKrh3DnYHjpGj6UKQHtNKTbtc1igvAhRy4+kLhV07OpLcsN0VgDEw/MjAvJO4BdMJsHwMhzCg==", + "dev": true, + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.22.5", + "@babel/helper-plugin-utils": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, "node_modules/@babel/preset-env": { - "version": "7.19.3", - "resolved": "https://registry.npmjs.org/@babel/preset-env/-/preset-env-7.19.3.tgz", - "integrity": "sha512-ziye1OTc9dGFOAXSWKUqQblYHNlBOaDl8wzqf2iKXJAltYiR3hKHUKmkt+S9PppW7RQpq4fFCrwwpIDj/f5P4w==", - "dev": true, - "dependencies": { - "@babel/compat-data": "^7.19.3", - "@babel/helper-compilation-targets": "^7.19.3", - "@babel/helper-plugin-utils": "^7.19.0", - "@babel/helper-validator-option": "^7.18.6", - "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": "^7.18.6", - "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": "^7.18.9", - "@babel/plugin-proposal-async-generator-functions": "^7.19.1", - "@babel/plugin-proposal-class-properties": "^7.18.6", - "@babel/plugin-proposal-class-static-block": "^7.18.6", - "@babel/plugin-proposal-dynamic-import": "^7.18.6", - "@babel/plugin-proposal-export-namespace-from": "^7.18.9", - "@babel/plugin-proposal-json-strings": "^7.18.6", - "@babel/plugin-proposal-logical-assignment-operators": "^7.18.9", - "@babel/plugin-proposal-nullish-coalescing-operator": "^7.18.6", - "@babel/plugin-proposal-numeric-separator": "^7.18.6", - "@babel/plugin-proposal-object-rest-spread": "^7.18.9", - "@babel/plugin-proposal-optional-catch-binding": "^7.18.6", - "@babel/plugin-proposal-optional-chaining": "^7.18.9", - "@babel/plugin-proposal-private-methods": "^7.18.6", - "@babel/plugin-proposal-private-property-in-object": "^7.18.6", - "@babel/plugin-proposal-unicode-property-regex": "^7.18.6", + "version": "7.22.9", + "resolved": "https://registry.npmjs.org/@babel/preset-env/-/preset-env-7.22.9.tgz", + "integrity": "sha512-wNi5H/Emkhll/bqPjsjQorSykrlfY5OWakd6AulLvMEytpKasMVUpVy8RL4qBIBs5Ac6/5i0/Rv0b/Fg6Eag/g==", + "dev": true, + "dependencies": { + "@babel/compat-data": "^7.22.9", + "@babel/helper-compilation-targets": "^7.22.9", + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/helper-validator-option": "^7.22.5", + "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": "^7.22.5", + "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": "^7.22.5", + "@babel/plugin-proposal-private-property-in-object": "7.21.0-placeholder-for-preset-env.2", "@babel/plugin-syntax-async-generators": "^7.8.4", "@babel/plugin-syntax-class-properties": "^7.12.13", "@babel/plugin-syntax-class-static-block": "^7.14.5", "@babel/plugin-syntax-dynamic-import": "^7.8.3", "@babel/plugin-syntax-export-namespace-from": "^7.8.3", - "@babel/plugin-syntax-import-assertions": "^7.18.6", + "@babel/plugin-syntax-import-assertions": "^7.22.5", + "@babel/plugin-syntax-import-attributes": "^7.22.5", + "@babel/plugin-syntax-import-meta": "^7.10.4", "@babel/plugin-syntax-json-strings": "^7.8.3", "@babel/plugin-syntax-logical-assignment-operators": "^7.10.4", "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3", @@ -2028,45 +2198,62 @@ "@babel/plugin-syntax-optional-chaining": "^7.8.3", "@babel/plugin-syntax-private-property-in-object": "^7.14.5", "@babel/plugin-syntax-top-level-await": "^7.14.5", - "@babel/plugin-transform-arrow-functions": "^7.18.6", - "@babel/plugin-transform-async-to-generator": "^7.18.6", - "@babel/plugin-transform-block-scoped-functions": "^7.18.6", - "@babel/plugin-transform-block-scoping": "^7.18.9", - "@babel/plugin-transform-classes": "^7.19.0", - "@babel/plugin-transform-computed-properties": "^7.18.9", - "@babel/plugin-transform-destructuring": "^7.18.13", - "@babel/plugin-transform-dotall-regex": "^7.18.6", - "@babel/plugin-transform-duplicate-keys": "^7.18.9", - "@babel/plugin-transform-exponentiation-operator": "^7.18.6", - "@babel/plugin-transform-for-of": "^7.18.8", - "@babel/plugin-transform-function-name": "^7.18.9", - "@babel/plugin-transform-literals": "^7.18.9", - "@babel/plugin-transform-member-expression-literals": "^7.18.6", - "@babel/plugin-transform-modules-amd": "^7.18.6", - "@babel/plugin-transform-modules-commonjs": "^7.18.6", - "@babel/plugin-transform-modules-systemjs": "^7.19.0", - "@babel/plugin-transform-modules-umd": "^7.18.6", - "@babel/plugin-transform-named-capturing-groups-regex": "^7.19.1", - "@babel/plugin-transform-new-target": "^7.18.6", - "@babel/plugin-transform-object-super": "^7.18.6", - "@babel/plugin-transform-parameters": "^7.18.8", - "@babel/plugin-transform-property-literals": "^7.18.6", - "@babel/plugin-transform-regenerator": "^7.18.6", - "@babel/plugin-transform-reserved-words": "^7.18.6", - "@babel/plugin-transform-shorthand-properties": "^7.18.6", - "@babel/plugin-transform-spread": "^7.19.0", - "@babel/plugin-transform-sticky-regex": "^7.18.6", - "@babel/plugin-transform-template-literals": "^7.18.9", - "@babel/plugin-transform-typeof-symbol": "^7.18.9", - "@babel/plugin-transform-unicode-escapes": "^7.18.10", - "@babel/plugin-transform-unicode-regex": "^7.18.6", + "@babel/plugin-syntax-unicode-sets-regex": "^7.18.6", + "@babel/plugin-transform-arrow-functions": "^7.22.5", + "@babel/plugin-transform-async-generator-functions": "^7.22.7", + "@babel/plugin-transform-async-to-generator": "^7.22.5", + "@babel/plugin-transform-block-scoped-functions": "^7.22.5", + "@babel/plugin-transform-block-scoping": "^7.22.5", + "@babel/plugin-transform-class-properties": "^7.22.5", + "@babel/plugin-transform-class-static-block": "^7.22.5", + "@babel/plugin-transform-classes": "^7.22.6", + "@babel/plugin-transform-computed-properties": "^7.22.5", + "@babel/plugin-transform-destructuring": "^7.22.5", + "@babel/plugin-transform-dotall-regex": "^7.22.5", + "@babel/plugin-transform-duplicate-keys": "^7.22.5", + "@babel/plugin-transform-dynamic-import": "^7.22.5", + "@babel/plugin-transform-exponentiation-operator": "^7.22.5", + "@babel/plugin-transform-export-namespace-from": "^7.22.5", + "@babel/plugin-transform-for-of": "^7.22.5", + "@babel/plugin-transform-function-name": "^7.22.5", + "@babel/plugin-transform-json-strings": "^7.22.5", + "@babel/plugin-transform-literals": "^7.22.5", + "@babel/plugin-transform-logical-assignment-operators": "^7.22.5", + "@babel/plugin-transform-member-expression-literals": "^7.22.5", + "@babel/plugin-transform-modules-amd": "^7.22.5", + "@babel/plugin-transform-modules-commonjs": "^7.22.5", + "@babel/plugin-transform-modules-systemjs": "^7.22.5", + "@babel/plugin-transform-modules-umd": "^7.22.5", + "@babel/plugin-transform-named-capturing-groups-regex": "^7.22.5", + "@babel/plugin-transform-new-target": "^7.22.5", + "@babel/plugin-transform-nullish-coalescing-operator": "^7.22.5", + "@babel/plugin-transform-numeric-separator": "^7.22.5", + "@babel/plugin-transform-object-rest-spread": "^7.22.5", + "@babel/plugin-transform-object-super": "^7.22.5", + "@babel/plugin-transform-optional-catch-binding": "^7.22.5", + "@babel/plugin-transform-optional-chaining": "^7.22.6", + "@babel/plugin-transform-parameters": "^7.22.5", + "@babel/plugin-transform-private-methods": "^7.22.5", + "@babel/plugin-transform-private-property-in-object": "^7.22.5", + "@babel/plugin-transform-property-literals": "^7.22.5", + "@babel/plugin-transform-regenerator": "^7.22.5", + "@babel/plugin-transform-reserved-words": "^7.22.5", + "@babel/plugin-transform-shorthand-properties": "^7.22.5", + "@babel/plugin-transform-spread": "^7.22.5", + "@babel/plugin-transform-sticky-regex": "^7.22.5", + "@babel/plugin-transform-template-literals": "^7.22.5", + "@babel/plugin-transform-typeof-symbol": "^7.22.5", + "@babel/plugin-transform-unicode-escapes": "^7.22.5", + "@babel/plugin-transform-unicode-property-regex": "^7.22.5", + "@babel/plugin-transform-unicode-regex": "^7.22.5", + "@babel/plugin-transform-unicode-sets-regex": "^7.22.5", "@babel/preset-modules": "^0.1.5", - "@babel/types": "^7.19.3", - "babel-plugin-polyfill-corejs2": "^0.3.3", - "babel-plugin-polyfill-corejs3": "^0.6.0", - "babel-plugin-polyfill-regenerator": "^0.4.1", - "core-js-compat": "^3.25.1", - "semver": "^6.3.0" + "@babel/types": "^7.22.5", + "babel-plugin-polyfill-corejs2": "^0.4.4", + "babel-plugin-polyfill-corejs3": "^0.8.2", + "babel-plugin-polyfill-regenerator": "^0.5.1", + "core-js-compat": "^3.31.0", + "semver": "^6.3.1" }, "engines": { "node": ">=6.9.0" @@ -2076,9 +2263,9 @@ } }, "node_modules/@babel/preset-env/node_modules/semver": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", - "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", "dev": true, "bin": { "semver": "bin/semver.js" @@ -2101,9 +2288,9 @@ } }, "node_modules/@babel/register": { - "version": "7.18.9", - "resolved": "https://registry.npmjs.org/@babel/register/-/register-7.18.9.tgz", - "integrity": "sha512-ZlbnXDcNYHMR25ITwwNKT88JiaukkdVj/nG7r3wnuXkOTHc60Uy05PwMCPre0hSkY68E6zK3xz+vUJSP2jWmcw==", + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/register/-/register-7.22.5.tgz", + "integrity": "sha512-vV6pm/4CijSQ8Y47RH5SopXzursN35RQINfGJkmOlcpAtGuf94miFvIPhCKGQN7WGIcsgG1BHEX2KVdTYwTwUQ==", "dev": true, "dependencies": { "clone-deep": "^4.0.1", @@ -2228,13 +2415,19 @@ "node": ">=6" } }, + "node_modules/@babel/regjsgen": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/@babel/regjsgen/-/regjsgen-0.8.0.tgz", + "integrity": "sha512-x/rqGMdzj+fWZvCOYForTghzbtqPDZ5gPwaoNGHdgDfF2QA/XZbCBp4Moo5scrkAMPhB7z26XM/AaHuIJdgauA==", + "dev": true + }, "node_modules/@babel/runtime": { - "version": "7.19.0", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.19.0.tgz", - "integrity": "sha512-eR8Lo9hnDS7tqkO7NsV+mKvCmv5boaXFSZ70DnfhcgiEne8hv9oCEd36Klw74EtizEqLsy4YnW8UWwpBVolHZA==", + "version": "7.22.6", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.22.6.tgz", + "integrity": "sha512-wDb5pWm4WDdF6LFUde3Jl8WzPA+3ZbxYqkC6xAXuD3irdEHN1k0NfTRrJD8ZD378SJ61miMLCqIOXYhd8x+AJQ==", "dev": true, "dependencies": { - "regenerator-runtime": "^0.13.4" + "regenerator-runtime": "^0.13.11" }, "engines": { "node": ">=6.9.0" @@ -2255,33 +2448,33 @@ } }, "node_modules/@babel/template": { - "version": "7.18.10", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.18.10.tgz", - "integrity": "sha512-TI+rCtooWHr3QJ27kJxfjutghu44DLnasDMwpDqCXVTal9RLp3RSYNh4NdBrRP2cQAoG9A8juOQl6P6oZG4JxA==", + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.22.5.tgz", + "integrity": "sha512-X7yV7eiwAxdj9k94NEylvbVHLiVG1nvzCV2EAowhxLTwODV1jl9UzZ48leOC0sH7OnuHrIkllaBgneUykIcZaw==", "dev": true, "dependencies": { - "@babel/code-frame": "^7.18.6", - "@babel/parser": "^7.18.10", - "@babel/types": "^7.18.10" + "@babel/code-frame": "^7.22.5", + "@babel/parser": "^7.22.5", + "@babel/types": "^7.22.5" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/traverse": { - "version": "7.19.3", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.19.3.tgz", - "integrity": "sha512-qh5yf6149zhq2sgIXmwjnsvmnNQC2iw70UFjp4olxucKrWd/dvlUsBI88VSLUsnMNF7/vnOiA+nk1+yLoCqROQ==", - "dev": true, - "dependencies": { - "@babel/code-frame": "^7.18.6", - "@babel/generator": "^7.19.3", - "@babel/helper-environment-visitor": "^7.18.9", - "@babel/helper-function-name": "^7.19.0", - "@babel/helper-hoist-variables": "^7.18.6", - "@babel/helper-split-export-declaration": "^7.18.6", - "@babel/parser": "^7.19.3", - "@babel/types": "^7.19.3", + "version": "7.22.8", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.22.8.tgz", + "integrity": "sha512-y6LPR+wpM2I3qJrsheCTwhIinzkETbplIgPBbwvqPKc+uljeA5gP+3nP8irdYt1mjQaDnlIcG+dw8OjAco4GXw==", + "dev": true, + "dependencies": { + "@babel/code-frame": "^7.22.5", + "@babel/generator": "^7.22.7", + "@babel/helper-environment-visitor": "^7.22.5", + "@babel/helper-function-name": "^7.22.5", + "@babel/helper-hoist-variables": "^7.22.5", + "@babel/helper-split-export-declaration": "^7.22.6", + "@babel/parser": "^7.22.7", + "@babel/types": "^7.22.5", "debug": "^4.1.0", "globals": "^11.1.0" }, @@ -2322,13 +2515,13 @@ "dev": true }, "node_modules/@babel/types": { - "version": "7.19.3", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.19.3.tgz", - "integrity": "sha512-hGCaQzIY22DJlDh9CH7NOxgKkFjBk0Cw9xDO1Xmh2151ti7wiGfQ3LauXzL4HP1fmFlTX6XjpRETTpUcv7wQLw==", + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.22.5.tgz", + "integrity": "sha512-zo3MIHGOkPOfoRXitsgHLjEXmlDaD/5KU1Uzuc9GNiZPhSqVxVRtxuPaSBZDsYZ9qV88AjtMtWW7ww98loJ9KA==", "dev": true, "dependencies": { - "@babel/helper-string-parser": "^7.18.10", - "@babel/helper-validator-identifier": "^7.19.1", + "@babel/helper-string-parser": "^7.22.5", + "@babel/helper-validator-identifier": "^7.22.5", "to-fast-properties": "^2.0.0" }, "engines": { @@ -2344,16 +2537,40 @@ "node": ">=4" } }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz", + "integrity": "sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA==", + "dev": true, + "dependencies": { + "eslint-visitor-keys": "^3.3.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.6.2.tgz", + "integrity": "sha512-pPTNuaAG3QMH+buKyBIGJs3g/S5y0caxw0ygM3YyE6yJFySwiGGSzA+mM3KJ8QQvzeLh3blwgSonkFjgQdxzMw==", + "dev": true, + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, "node_modules/@eslint/eslintrc": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-1.3.2.tgz", - "integrity": "sha512-AXYd23w1S/bv3fTs3Lz0vjiYemS08jWkI3hYyS9I1ry+0f+Yjs1wm+sU0BS8qDOPrBIkp4qHYC16I8uVtpLajQ==", + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.1.tgz", + "integrity": "sha512-9t7ZA7NGGK8ckelF0PQCfcxIUzs1Md5rrO6U/c+FIQNanea5UZC0wqKXH4vHBccmu4ZJgZ2idtPeW7+Q2npOEA==", "dev": true, "dependencies": { "ajv": "^6.12.4", "debug": "^4.3.2", - "espree": "^9.4.0", - "globals": "^13.15.0", + "espree": "^9.6.0", + "globals": "^13.19.0", "ignore": "^5.2.0", "import-fresh": "^3.2.1", "js-yaml": "^4.1.0", @@ -2391,9 +2608,9 @@ } }, "node_modules/@eslint/eslintrc/node_modules/globals": { - "version": "13.17.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-13.17.0.tgz", - "integrity": "sha512-1C+6nQRb1GwGMKm2dH/E7enFAMxGTmGI7/dEdhy/DNelv85w9B72t3uc5frtMNXIbzrarJJ/lTCjcaZwbLJmyw==", + "version": "13.20.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.20.0.tgz", + "integrity": "sha512-Qg5QtVkCy/kv3FUSlu4ukeZDVf9ee0iXLAUYX13gbR17bnejFTzr4iS9bY7kwCf1NztRNm1t91fjOiyx4CSwPQ==", "dev": true, "dependencies": { "type-fest": "^0.20.2" @@ -2417,33 +2634,30 @@ "js-yaml": "bin/js-yaml.js" } }, - "node_modules/@eslint/eslintrc/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dev": true, - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, "node_modules/@eslint/eslintrc/node_modules/ms": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", "dev": true }, + "node_modules/@eslint/js": { + "version": "8.46.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.46.0.tgz", + "integrity": "sha512-a8TLtmPi8xzPkCbp/OGFUo5yhRkHM2Ko9kOWP4znJr0WAhWyThaw3PnwX4vOTWOAMsV2uRt32PPDcEz63esSaA==", + "dev": true, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + } + }, "node_modules/@humanwhocodes/config-array": { - "version": "0.10.7", - "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.10.7.tgz", - "integrity": "sha512-MDl6D6sBsaV452/QSdX+4CXIjZhIcI0PELsxUjk4U828yd58vk3bTIvk/6w5FY+4hIy9sLW0sfrV7K7Kc++j/w==", + "version": "0.11.10", + "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.10.tgz", + "integrity": "sha512-KVVjQmNUepDVGXNuoRRdmmEjruj0KfiGSbS8LVc12LMsWDQzRXJ0qdhN8L8uUigKpfEHRhlaQFY0ib1tnUbNeQ==", "dev": true, "dependencies": { "@humanwhocodes/object-schema": "^1.2.1", "debug": "^4.1.1", - "minimatch": "^3.0.4" + "minimatch": "^3.0.5" }, "engines": { "node": ">=10.10.0" @@ -2472,16 +2686,6 @@ "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", "dev": true }, - "node_modules/@humanwhocodes/gitignore-to-minimatch": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@humanwhocodes/gitignore-to-minimatch/-/gitignore-to-minimatch-1.0.2.tgz", - "integrity": "sha512-rSqmMJDdLFUsyxR6FMtD00nfQKKLFb1kv+qBbOVKqErvloEIJLo5bDTJTQNTYgeyp78JsA7u/NPi5jT1GR/MuA==", - "dev": true, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/nzakas" - } - }, "node_modules/@humanwhocodes/module-importer": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", @@ -2565,13 +2769,13 @@ "dev": true }, "node_modules/@jridgewell/trace-mapping": { - "version": "0.3.15", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.15.tgz", - "integrity": "sha512-oWZNOULl+UbhsgB51uuZzglikfIKSUBO/M9W2OfEjn7cmqoAiCgmv9lyACTUacZwBz0ITnJ2NqjU8Tx0DHL88g==", + "version": "0.3.18", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.18.tgz", + "integrity": "sha512-w+niJYzMHdd7USdiH2U6869nqhD2nbfZXND5Yp93qIbEmnDNk7PD48o+YchRVpzMU7M6jVCbenTR7PA1FLQ9pA==", "dev": true, "dependencies": { - "@jridgewell/resolve-uri": "^3.0.3", - "@jridgewell/sourcemap-codec": "^1.4.10" + "@jridgewell/resolve-uri": "3.1.0", + "@jridgewell/sourcemap-codec": "1.4.14" } }, "node_modules/@nicolo-ribaudo/chokidar-2": { @@ -2648,34 +2852,43 @@ } }, "node_modules/@sinonjs/commons": { - "version": "1.8.3", - "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-1.8.3.tgz", - "integrity": "sha512-xkNcLAn/wZaX14RPlwizcKicDk9G3F8m2nU3L7Ukm5zBgTwiT0wsoFAHx9Jq56fJA1z/7uKGtCRu16sOUCLIHQ==", + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-3.0.0.tgz", + "integrity": "sha512-jXBtWAF4vmdNmZgD5FoKsVLv3rPgDnLgPbU84LIJ3otV44vJlDRokVng5v8NFJdCf/da9legHcKaRuZs4L7faA==", "dev": true, "dependencies": { "type-detect": "4.0.8" } }, "node_modules/@sinonjs/fake-timers": { - "version": "9.1.2", - "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-9.1.2.tgz", - "integrity": "sha512-BPS4ynJW/o92PUR4wgriz2Ud5gpST5vz6GQfMixEDK0Z8ZCUv2M7SkBLykH56T++Xs+8ln9zTGbOvNGIe02/jw==", + "version": "10.3.0", + "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-10.3.0.tgz", + "integrity": "sha512-V4BG07kuYSUkTCSBHG8G8TNhM+F19jXFWnQtzj+we8DrkpSBCee9Z3Ms8yiGer/dlmhe35/Xdgyo3/0rQKg7YA==", "dev": true, "dependencies": { - "@sinonjs/commons": "^1.7.0" + "@sinonjs/commons": "^3.0.0" } }, "node_modules/@sinonjs/samsam": { - "version": "6.1.1", - "resolved": "https://registry.npmjs.org/@sinonjs/samsam/-/samsam-6.1.1.tgz", - "integrity": "sha512-cZ7rKJTLiE7u7Wi/v9Hc2fs3Ucc3jrWeMgPHbbTCeVAB2S0wOBbYlkJVeNSL04i7fdhT8wIbDq1zhC/PXTD2SA==", + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/@sinonjs/samsam/-/samsam-8.0.0.tgz", + "integrity": "sha512-Bp8KUVlLp8ibJZrnvq2foVhP0IVX2CIprMJPK0vqGqgrDa0OHVKeZyBykqskkrdxV6yKBPmGasO8LVjAKR3Gew==", "dev": true, "dependencies": { - "@sinonjs/commons": "^1.6.0", + "@sinonjs/commons": "^2.0.0", "lodash.get": "^4.4.2", "type-detect": "^4.0.8" } }, + "node_modules/@sinonjs/samsam/node_modules/@sinonjs/commons": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-2.0.0.tgz", + "integrity": "sha512-uLa0j859mMrg2slwQYdO/AkrOfmH+X6LTVmNTS9CqexuE2IvVORIkSpJLqePAbEnKJ77aMmCwr1NUZ57120Xcg==", + "dev": true, + "dependencies": { + "type-detect": "4.0.8" + } + }, "node_modules/@sinonjs/text-encoding": { "version": "0.7.2", "resolved": "https://registry.npmjs.org/@sinonjs/text-encoding/-/text-encoding-0.7.2.tgz", @@ -2742,12 +2955,6 @@ "@types/webidl-conversions": "*" } }, - "node_modules/@ungap/promise-all-settled": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@ungap/promise-all-settled/-/promise-all-settled-1.1.2.tgz", - "integrity": "sha512-sL/cEvJWAnClXw0wHk85/2L0G6Sj8UB0Ctc1TEMbKSsmpRosqhwj9gWgFRZSrBr2f9tiXISwNhCPmlfqUqyb9Q==", - "dev": true - }, "node_modules/@webassemblyjs/ast": { "version": "1.11.1", "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.11.1.tgz", @@ -2942,9 +3149,9 @@ } }, "node_modules/acorn": { - "version": "8.8.0", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.8.0.tgz", - "integrity": "sha512-QOxyigPVrpZ2GXT+PFyZTl6TtOFc5egxHIP9IlQ+RbupQuX4RkT/Bee4/kQuC02Xkzg84JcT7oLYtDIQxp+v7w==", + "version": "8.10.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.10.0.tgz", + "integrity": "sha512-F0SAmZ8iUtS//m8DmCTA0jlh6TDKkHQyK6xc6V4KDTyZKA9dnvX9/3sRTVQrWm79glUAZbnmmNcdYwUIHWVybw==", "dev": true, "bin": { "acorn": "bin/acorn" @@ -2963,6 +3170,15 @@ "acorn": "^8" } }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, "node_modules/ajv": { "version": "6.12.6", "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", @@ -2979,11 +3195,51 @@ "url": "https://github.com/sponsors/epoberezkin" } }, + "node_modules/ajv-formats": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-2.1.1.tgz", + "integrity": "sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==", + "dev": true, + "dependencies": { + "ajv": "^8.0.0" + }, + "peerDependencies": { + "ajv": "^8.0.0" + }, + "peerDependenciesMeta": { + "ajv": { + "optional": true + } + } + }, + "node_modules/ajv-formats/node_modules/ajv": { + "version": "8.12.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.12.0.tgz", + "integrity": "sha512-sRu1kpcO9yLtYxBKvqfTeh9KzZEwO3STyX1HT+4CaDzC6HpTGYhIhPIzj9XuKU7KYDwnaeh5hcOwjy1QuJzBPA==", + "dev": true, + "dependencies": { + "fast-deep-equal": "^3.1.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ajv-formats/node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "dev": true + }, "node_modules/ajv-keywords": { "version": "3.5.2", "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz", "integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==", "dev": true, + "peer": true, "peerDependencies": { "ajv": "^6.9.1" } @@ -3072,21 +3328,34 @@ "node": ">=6.0" } }, + "node_modules/array-buffer-byte-length": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/array-buffer-byte-length/-/array-buffer-byte-length-1.0.0.tgz", + "integrity": "sha512-LPuwb2P+NrQw3XhxGc36+XSvuBPopovXYTR9Ew++Du9Yb/bx5AzBfrIsBoj0EZUifjQU+sHL21sseZ3jerWO/A==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "is-array-buffer": "^3.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/array-flatten": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", "integrity": "sha1-ml9pkFGx5wczKPKgCJaLZOopVdI=" }, "node_modules/array-includes": { - "version": "3.1.5", - "resolved": "https://registry.npmjs.org/array-includes/-/array-includes-3.1.5.tgz", - "integrity": "sha512-iSDYZMMyTPkiFasVqfuAQnWAYcvO/SeBSCGKePoEthjp4LEMTe4uLc7b025o4jAZpHhihh8xPo99TNWUWWkGDQ==", + "version": "3.1.6", + "resolved": "https://registry.npmjs.org/array-includes/-/array-includes-3.1.6.tgz", + "integrity": "sha512-sgTbLvL6cNnw24FnbaDyjmvddQ2ML8arZsgaJhoABMoplz/4QRhtrYS+alr1BUM1Bwp6dhx8vVCBSLG+StwOFw==", "dev": true, "dependencies": { "call-bind": "^1.0.2", "define-properties": "^1.1.4", - "es-abstract": "^1.19.5", - "get-intrinsic": "^1.1.1", + "es-abstract": "^1.20.4", + "get-intrinsic": "^1.1.3", "is-string": "^1.0.7" }, "engines": { @@ -3096,24 +3365,34 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/array-union": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz", - "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==", + "node_modules/array.prototype.findlastindex": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/array.prototype.findlastindex/-/array.prototype.findlastindex-1.2.2.tgz", + "integrity": "sha512-tb5thFFlUcp7NdNF6/MpDk/1r/4awWG1FIz3YqDf+/zJSTezBb+/5WViH41obXULHVpDzoiCLpJ/ZO9YbJMsdw==", "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.4", + "es-abstract": "^1.20.4", + "es-shim-unscopables": "^1.0.0", + "get-intrinsic": "^1.1.3" + }, "engines": { - "node": ">=8" + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, "node_modules/array.prototype.flat": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/array.prototype.flat/-/array.prototype.flat-1.3.0.tgz", - "integrity": "sha512-12IUEkHsAhA4DY5s0FPgNXIdc8VRSqD9Zp78a5au9abH/SOBrsp082JOWFNTjkMozh8mqcdiKuaLGhPeYztxSw==", + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/array.prototype.flat/-/array.prototype.flat-1.3.1.tgz", + "integrity": "sha512-roTU0KWIOmJ4DRLmwKd19Otg0/mT3qPNt0Qb3GWW8iObuZXxrjB/pzn0R3hqpRSWg4HCwqx+0vwOnWnvlOyeIA==", "dev": true, "dependencies": { "call-bind": "^1.0.2", - "define-properties": "^1.1.3", - "es-abstract": "^1.19.2", + "define-properties": "^1.1.4", + "es-abstract": "^1.20.4", "es-shim-unscopables": "^1.0.0" }, "engines": { @@ -3124,15 +3403,14 @@ } }, "node_modules/array.prototype.flatmap": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/array.prototype.flatmap/-/array.prototype.flatmap-1.3.0.tgz", - "integrity": "sha512-PZC9/8TKAIxcWKdyeb77EzULHPrIX/tIZebLJUQOMR1OwYosT8yggdfWScfTBCDj5utONvOuPQQumYsU2ULbkg==", + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/array.prototype.flatmap/-/array.prototype.flatmap-1.3.1.tgz", + "integrity": "sha512-8UGn9O1FDVvMNB0UlLv4voxRMze7+FpHyF5mSMRjWHUMlpoDViniy05870VlxhfgTnLbpuwTzvD76MTtWxB/mQ==", "dev": true, - "peer": true, "dependencies": { "call-bind": "^1.0.2", - "define-properties": "^1.1.3", - "es-abstract": "^1.19.2", + "define-properties": "^1.1.4", + "es-abstract": "^1.20.4", "es-shim-unscopables": "^1.0.0" }, "engines": { @@ -3142,6 +3420,26 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/arraybuffer.prototype.slice": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/arraybuffer.prototype.slice/-/arraybuffer.prototype.slice-1.0.1.tgz", + "integrity": "sha512-09x0ZWFEjj4WD8PDbykUwo3t9arLn8NIzmmYEJFpYekOAQjpkGSyrQhNoRTcwwcFRu+ycWF78QZ63oWTqSjBcw==", + "dev": true, + "dependencies": { + "array-buffer-byte-length": "^1.0.0", + "call-bind": "^1.0.2", + "define-properties": "^1.2.0", + "get-intrinsic": "^1.2.1", + "is-array-buffer": "^3.0.2", + "is-shared-array-buffer": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/asap": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/asap/-/asap-2.0.6.tgz", @@ -3170,6 +3468,18 @@ "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", "dev": true }, + "node_modules/available-typed-arrays": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.5.tgz", + "integrity": "sha512-DMD0KiN46eipeziST1LPP/STfDU0sufISXmjSgvVsoU2tqxctQeASejWcfNtxYKqETM1UxQ8sp2OrSBWpHY6sw==", + "dev": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/axe-core": { "version": "4.4.3", "resolved": "https://registry.npmjs.org/axe-core/-/axe-core-4.4.3.tgz", @@ -3215,66 +3525,20 @@ } }, "node_modules/babel-loader": { - "version": "8.2.5", - "resolved": "https://registry.npmjs.org/babel-loader/-/babel-loader-8.2.5.tgz", - "integrity": "sha512-OSiFfH89LrEMiWd4pLNqGz4CwJDtbs2ZVc+iGu2HrkRfPxId9F2anQj38IxWpmRfsUY0aBZYi1EFcd3mhtRMLQ==", + "version": "9.1.3", + "resolved": "https://registry.npmjs.org/babel-loader/-/babel-loader-9.1.3.tgz", + "integrity": "sha512-xG3ST4DglodGf8qSwv0MdeWLhrDsw/32QMdTO5T1ZIp9gQur0HkCyFs7Awskr10JKXFXwpAhiCuYX5oGXnRGbw==", "dev": true, "dependencies": { - "find-cache-dir": "^3.3.1", - "loader-utils": "^2.0.0", - "make-dir": "^3.1.0", - "schema-utils": "^2.6.5" + "find-cache-dir": "^4.0.0", + "schema-utils": "^4.0.0" }, "engines": { - "node": ">= 8.9" + "node": ">= 14.15.0" }, "peerDependencies": { - "@babel/core": "^7.0.0", - "webpack": ">=2" - } - }, - "node_modules/babel-loader/node_modules/big.js": { - "version": "5.2.2", - "resolved": "https://registry.npmjs.org/big.js/-/big.js-5.2.2.tgz", - "integrity": "sha512-vyL2OymJxmarO8gxMr0mhChsO9QGwhynfuu4+MHTAW6czfq9humCB7rKpUjDd9YUiDPU4mzpyupFSvOClAwbmQ==", - "dev": true, - "engines": { - "node": "*" - } - }, - "node_modules/babel-loader/node_modules/emojis-list": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/emojis-list/-/emojis-list-3.0.0.tgz", - "integrity": "sha512-/kyM18EfinwXZbno9FyUGeFh87KC8HRQBQGildHZbEuRyWFOmv1U10o9BBp8XVZDVNNuQKyIGIu5ZYAAXJ0V2Q==", - "dev": true, - "engines": { - "node": ">= 4" - } - }, - "node_modules/babel-loader/node_modules/json5": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.1.tgz", - "integrity": "sha512-1hqLFMSrGHRHxav9q9gNjJ5EXznIxGVO09xQRrwplcS8qs28pZ8s8hupZAmqDwZUmVZ2Qb2jnyPOWcDH8m8dlA==", - "dev": true, - "bin": { - "json5": "lib/cli.js" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/babel-loader/node_modules/loader-utils": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-2.0.2.tgz", - "integrity": "sha512-TM57VeHptv569d/GKh6TAYdzKblwDNiumOdkFnejjD0XwTH87K90w3O7AiJRqdQoXygvi1VQTJTLGhJl7WqA7A==", - "dev": true, - "dependencies": { - "big.js": "^5.2.2", - "emojis-list": "^3.0.0", - "json5": "^2.1.2" - }, - "engines": { - "node": ">=8.9.0" + "@babel/core": "^7.12.0", + "webpack": ">=5" } }, "node_modules/babel-messages": { @@ -3286,61 +3550,52 @@ "babel-runtime": "^6.22.0" } }, - "node_modules/babel-plugin-dynamic-import-node": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/babel-plugin-dynamic-import-node/-/babel-plugin-dynamic-import-node-2.3.3.tgz", - "integrity": "sha512-jZVI+s9Zg3IqA/kdi0i6UDCybUI3aSBLnglhYbSSjKlV7yF1F/5LWv8MakQmvYpnbJDS6fcBL2KzHSxNCMtWSQ==", - "dev": true, - "dependencies": { - "object.assign": "^4.1.0" - } - }, "node_modules/babel-plugin-polyfill-corejs2": { - "version": "0.3.3", - "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs2/-/babel-plugin-polyfill-corejs2-0.3.3.tgz", - "integrity": "sha512-8hOdmFYFSZhqg2C/JgLUQ+t52o5nirNwaWM2B9LWteozwIvM14VSwdsCAUET10qT+kmySAlseadmfeeSWFCy+Q==", + "version": "0.4.5", + "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs2/-/babel-plugin-polyfill-corejs2-0.4.5.tgz", + "integrity": "sha512-19hwUH5FKl49JEsvyTcoHakh6BE0wgXLLptIyKZ3PijHc/Ci521wygORCUCCred+E/twuqRyAkE02BAWPmsHOg==", "dev": true, "dependencies": { - "@babel/compat-data": "^7.17.7", - "@babel/helper-define-polyfill-provider": "^0.3.3", - "semver": "^6.1.1" + "@babel/compat-data": "^7.22.6", + "@babel/helper-define-polyfill-provider": "^0.4.2", + "semver": "^6.3.1" }, "peerDependencies": { - "@babel/core": "^7.0.0-0" + "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" } }, "node_modules/babel-plugin-polyfill-corejs2/node_modules/semver": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", - "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", "dev": true, "bin": { "semver": "bin/semver.js" } }, "node_modules/babel-plugin-polyfill-corejs3": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs3/-/babel-plugin-polyfill-corejs3-0.6.0.tgz", - "integrity": "sha512-+eHqR6OPcBhJOGgsIar7xoAB1GcSwVUA3XjAd7HJNzOXT4wv6/H7KIdA/Nc60cvUlDbKApmqNvD1B1bzOt4nyA==", + "version": "0.8.3", + "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs3/-/babel-plugin-polyfill-corejs3-0.8.3.tgz", + "integrity": "sha512-z41XaniZL26WLrvjy7soabMXrfPWARN25PZoriDEiLMxAp50AUW3t35BGQUMg5xK3UrpVTtagIDklxYa+MhiNA==", "dev": true, "dependencies": { - "@babel/helper-define-polyfill-provider": "^0.3.3", - "core-js-compat": "^3.25.1" + "@babel/helper-define-polyfill-provider": "^0.4.2", + "core-js-compat": "^3.31.0" }, "peerDependencies": { - "@babel/core": "^7.0.0-0" + "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" } }, "node_modules/babel-plugin-polyfill-regenerator": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-regenerator/-/babel-plugin-polyfill-regenerator-0.4.1.tgz", - "integrity": "sha512-NtQGmyQDXjQqQ+IzRkBVwEOz9lQ4zxAQZgoAYEtU9dJjnl1Oc98qnN7jcp+bE7O7aYzVpavXE3/VKXNzUbh7aw==", + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-regenerator/-/babel-plugin-polyfill-regenerator-0.5.2.tgz", + "integrity": "sha512-tAlOptU0Xj34V1Y2PNTL4Y0FOJMDB6bZmoW39FeCQIhigGLkqu3Fj6uiXpxIf6Ij274ENdYx64y6Au+ZKlb1IA==", "dev": true, "dependencies": { - "@babel/helper-define-polyfill-provider": "^0.3.3" + "@babel/helper-define-polyfill-provider": "^0.4.2" }, "peerDependencies": { - "@babel/core": "^7.0.0-0" + "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" } }, "node_modules/babel-runtime": { @@ -3410,46 +3665,27 @@ "babylon": "bin/babylon.js" } }, - "node_modules/balanced-match": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", - "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=", - "dev": true - }, - "node_modules/base64-js": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", - "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ] - }, + "node_modules/balanced-match": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", + "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=", + "dev": true + }, "node_modules/body-parser": { - "version": "1.20.0", - "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.0.tgz", - "integrity": "sha512-DfJ+q6EPcGKZD1QWUjSpqp+Q7bDQTsQIF4zfUAtZ6qk+H/3/QRhg9CEp39ss+/T2vw0+HaidC0ecJj/DRLIaKg==", + "version": "1.20.2", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.2.tgz", + "integrity": "sha512-ml9pReCu3M61kGlqoTm2umSXTlRTuGTx0bfYj+uIUKKYycG5NtSbeetV3faSU6R7ajOPw0g/J1PvK4qNy7s5bA==", "dependencies": { "bytes": "3.1.2", - "content-type": "~1.0.4", + "content-type": "~1.0.5", "debug": "2.6.9", "depd": "2.0.0", "destroy": "1.2.0", "http-errors": "2.0.0", "iconv-lite": "0.4.24", "on-finished": "2.4.1", - "qs": "6.10.3", - "raw-body": "2.5.1", + "qs": "6.11.0", + "raw-body": "2.5.2", "type-is": "~1.6.18", "unpipe": "1.0.0" }, @@ -3487,9 +3723,9 @@ "dev": true }, "node_modules/browserslist": { - "version": "4.21.4", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.21.4.tgz", - "integrity": "sha512-CBHJJdDmgjl3daYjN5Cp5kbTf1mUhZoS+beLklHIvkOWscs83YAhLlF3Wsh/lciQYAcbBJgTOD44VtG31ZM4Hw==", + "version": "4.21.10", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.21.10.tgz", + "integrity": "sha512-bipEBdZfVH5/pwrvqc+Ub0kUPVfGUhlKxbvfD+z1BDnPEO/X98ruXGA1WP5ASpAFKan7Qr6j736IacbZQuAlKQ==", "dev": true, "funding": [ { @@ -3499,13 +3735,17 @@ { "type": "tidelift", "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" } ], "dependencies": { - "caniuse-lite": "^1.0.30001400", - "electron-to-chromium": "^1.4.251", - "node-releases": "^2.0.6", - "update-browserslist-db": "^1.0.9" + "caniuse-lite": "^1.0.30001517", + "electron-to-chromium": "^1.4.477", + "node-releases": "^2.0.13", + "update-browserslist-db": "^1.0.11" }, "bin": { "browserslist": "cli.js" @@ -3515,37 +3755,11 @@ } }, "node_modules/bson": { - "version": "4.7.0", - "resolved": "https://registry.npmjs.org/bson/-/bson-4.7.0.tgz", - "integrity": "sha512-VrlEE4vuiO1WTpfof4VmaVolCVYkYTgB9iWgYNOrVlnifpME/06fhFRmONgBhClD5pFC1t9ZWqFUQEQAzY43bA==", - "dependencies": { - "buffer": "^5.6.0" - }, + "version": "5.4.0", + "resolved": "https://registry.npmjs.org/bson/-/bson-5.4.0.tgz", + "integrity": "sha512-WRZ5SQI5GfUuKnPTNmAYPiKIof3ORXAF4IRU5UcgmivNIon01rWQlw5RUH954dpu8yGL8T59YShVddIPaU/gFA==", "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/bson/node_modules/buffer": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", - "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "dependencies": { - "base64-js": "^1.3.1", - "ieee754": "^1.1.13" + "node": ">=14.20.1" } }, "node_modules/buffer-from": { @@ -3584,9 +3798,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001416", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001416.tgz", - "integrity": "sha512-06wzzdAkCPZO+Qm4e/eNghZBDfVNDsCgw33T27OwBH9unE9S478OYw//Q2L7Npf/zBzs7rjZOszIFQkwQKAEqA==", + "version": "1.0.30001519", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001519.tgz", + "integrity": "sha512-0QHgqR+Jv4bxHMp8kZ1Kn8CH55OikjKJ6JmKkZYP1F3D7w+lnFXF70nG5eNfsZS89jadi5Ywy5UCSKLAglIRkg==", "dev": true, "funding": [ { @@ -3596,6 +3810,10 @@ { "type": "tidelift", "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" } ] }, @@ -3764,6 +3982,12 @@ "integrity": "sha512-7j2y+40w61zy6YC2iRNpUe/NwhNyoXrYpHMrSunaMG64nRnaf96zO/KMQR4OyN/UnE5KLyEBnKHd4aG3rskjpQ==", "dev": true }, + "node_modules/common-path-prefix": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/common-path-prefix/-/common-path-prefix-3.0.0.tgz", + "integrity": "sha512-QE33hToZseCH3jS0qN96O/bSh3kaw/h+Tq7ngyY9eWDUnTlTNUyqfqvCXioLe5Na5jFsL78ra/wuBU4iuEgd4w==", + "dev": true + }, "node_modules/commondir": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/commondir/-/commondir-1.0.1.tgz", @@ -3819,9 +4043,9 @@ ] }, "node_modules/content-type": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.4.tgz", - "integrity": "sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA==", + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", "engines": { "node": ">= 0.6" } @@ -3849,9 +4073,9 @@ "integrity": "sha1-4wOogrNCzD7oylE6eZmXNNqzriw=" }, "node_modules/cookiejar": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/cookiejar/-/cookiejar-2.1.3.tgz", - "integrity": "sha512-JxbCBUdrfr6AQjOXrxoTvAMJO4HBTUIlBzslcJPAz+/KT8yk53fXun51u+RenNYvad/+Vc2DIz5o9UxlCDymFQ==", + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/cookiejar/-/cookiejar-2.1.4.tgz", + "integrity": "sha512-LDx6oHrK+PhzLKJU9j5S7/Y3jM/mUHvD/DeI1WQmJn652iPC5Y4TBzC9l+5OMOXlyTTA+SmVUPm0HQUwpD5Jqw==", "dev": true }, "node_modules/core-js": { @@ -3863,12 +4087,12 @@ "hasInstallScript": true }, "node_modules/core-js-compat": { - "version": "3.25.5", - "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.25.5.tgz", - "integrity": "sha512-ovcyhs2DEBUIE0MGEKHP4olCUW/XYte3Vroyxuh38rD1wAO4dHohsovUC4eAOuzFxE6b+RXvBU3UZ9o0YhUTkA==", + "version": "3.32.0", + "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.32.0.tgz", + "integrity": "sha512-7a9a3D1k4UCVKnLhrgALyFcP7YCsLOQIxPd0dKjf/6GuPcgyiGP70ewWdCGrSK7evyhymi0qO4EqCmSJofDeYw==", "dev": true, "dependencies": { - "browserslist": "^4.21.4" + "browserslist": "^4.21.9" }, "funding": { "type": "opencollective", @@ -3979,9 +4203,9 @@ } }, "node_modules/define-properties": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.1.4.tgz", - "integrity": "sha512-uckOqKcfaVvtBdsVkdPv3XjveQJsNQqmhXgRi8uhvWWuPYZCNlzT8qAyblUgNoXdHdjMTzAqeGjAoli8f+bzPA==", + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.0.tgz", + "integrity": "sha512-xvqAVKGfT1+UAvPwKTVw/njhdQ8ZhXK4lI0bCIuCMrp2up9nPnaDftrLtmpTazqd1o+UY4zgzU+avtMbDP+ldA==", "dev": true, "dependencies": { "has-property-descriptors": "^1.0.0", @@ -4003,14 +4227,6 @@ "node": ">=0.4.0" } }, - "node_modules/denque": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/denque/-/denque-2.1.0.tgz", - "integrity": "sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw==", - "engines": { - "node": ">=0.10" - } - }, "node_modules/depd": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", @@ -4041,9 +4257,9 @@ } }, "node_modules/dezalgo": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/dezalgo/-/dezalgo-1.0.3.tgz", - "integrity": "sha512-K7i4zNfT2kgQz3GylDw40ot9GAE47sFZ9EXHFSPP6zONLgH6kWXE0KWJchkbQJLBkRazq4APwZ4OwiFFlT95OQ==", + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/dezalgo/-/dezalgo-1.0.4.tgz", + "integrity": "sha512-rXSP0bf+5n0Qonsb+SVVfNfIsimO4HEtmnIpPHY8Q1UCzKlQrDMfdobr8nJOOsRgWCyMRqeSBQzmWUMq7zvVig==", "dev": true, "dependencies": { "asap": "^2.0.0", @@ -4059,18 +4275,6 @@ "node": ">=0.3.1" } }, - "node_modules/dir-glob": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", - "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==", - "dev": true, - "dependencies": { - "path-type": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/doctrine": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", @@ -4089,9 +4293,9 @@ "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==" }, "node_modules/electron-to-chromium": { - "version": "1.4.272", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.272.tgz", - "integrity": "sha512-KS6gPPGNrzpVv9HzFVq+Etd0AjZEPr5pvaTBn2yD6KV4+cKW4I0CJoJNgmTG6gUQPAMZ4wIPtcOuoou3qFAZCA==", + "version": "1.4.484", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.484.tgz", + "integrity": "sha512-nO3ZEomTK2PO/3TUXgEx0A97xZTpKVf4p427lABHuCpT1IQ2N+njVh29DkQkCk6Q4m2wjU+faK4xAcfFndwjvw==", "dev": true }, "node_modules/emoji-regex": { @@ -4124,35 +4328,50 @@ } }, "node_modules/es-abstract": { - "version": "1.20.3", - "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.20.3.tgz", - "integrity": "sha512-AyrnaKVpMzljIdwjzrj+LxGmj8ik2LckwXacHqrJJ/jxz6dDDBcZ7I7nlHM0FvEW8MfbWJwOd+yT2XzYW49Frw==", + "version": "1.22.1", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.22.1.tgz", + "integrity": "sha512-ioRRcXMO6OFyRpyzV3kE1IIBd4WG5/kltnzdxSCqoP8CMGs/Li+M1uF5o7lOkZVFjDs+NLesthnF66Pg/0q0Lw==", "dev": true, "dependencies": { + "array-buffer-byte-length": "^1.0.0", + "arraybuffer.prototype.slice": "^1.0.1", + "available-typed-arrays": "^1.0.5", "call-bind": "^1.0.2", + "es-set-tostringtag": "^2.0.1", "es-to-primitive": "^1.2.1", - "function-bind": "^1.1.1", "function.prototype.name": "^1.1.5", - "get-intrinsic": "^1.1.3", + "get-intrinsic": "^1.2.1", "get-symbol-description": "^1.0.0", + "globalthis": "^1.0.3", + "gopd": "^1.0.1", "has": "^1.0.3", "has-property-descriptors": "^1.0.0", + "has-proto": "^1.0.1", "has-symbols": "^1.0.3", - "internal-slot": "^1.0.3", - "is-callable": "^1.2.6", + "internal-slot": "^1.0.5", + "is-array-buffer": "^3.0.2", + "is-callable": "^1.2.7", "is-negative-zero": "^2.0.2", "is-regex": "^1.1.4", "is-shared-array-buffer": "^1.0.2", "is-string": "^1.0.7", + "is-typed-array": "^1.1.10", "is-weakref": "^1.0.2", - "object-inspect": "^1.12.2", + "object-inspect": "^1.12.3", "object-keys": "^1.1.1", "object.assign": "^4.1.4", - "regexp.prototype.flags": "^1.4.3", + "regexp.prototype.flags": "^1.5.0", + "safe-array-concat": "^1.0.0", "safe-regex-test": "^1.0.0", - "string.prototype.trimend": "^1.0.5", - "string.prototype.trimstart": "^1.0.5", - "unbox-primitive": "^1.0.2" + "string.prototype.trim": "^1.2.7", + "string.prototype.trimend": "^1.0.6", + "string.prototype.trimstart": "^1.0.6", + "typed-array-buffer": "^1.0.0", + "typed-array-byte-length": "^1.0.0", + "typed-array-byte-offset": "^1.0.0", + "typed-array-length": "^1.0.4", + "unbox-primitive": "^1.0.2", + "which-typed-array": "^1.1.10" }, "engines": { "node": ">= 0.4" @@ -4168,6 +4387,20 @@ "dev": true, "peer": true }, + "node_modules/es-set-tostringtag": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.0.1.tgz", + "integrity": "sha512-g3OMbtlwY3QewlqAiMLI47KywjWZoEytKr8pf6iTC8uJq5bIAH52Z9pnQ8pVL6whrCto53JZDuUIsifGeLorTg==", + "dev": true, + "dependencies": { + "get-intrinsic": "^1.1.3", + "has": "^1.0.3", + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/es-shim-unscopables": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/es-shim-unscopables/-/es-shim-unscopables-1.0.0.tgz", @@ -4218,49 +4451,47 @@ } }, "node_modules/eslint": { - "version": "8.24.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.24.0.tgz", - "integrity": "sha512-dWFaPhGhTAiPcCgm3f6LI2MBWbogMnTJzFBbhXVRQDJPkr9pGZvVjlVfXd+vyDcWPA2Ic9L2AXPIQM0+vk/cSQ==", + "version": "8.46.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.46.0.tgz", + "integrity": "sha512-cIO74PvbW0qU8e0mIvk5IV3ToWdCq5FYG6gWPHHkx6gNdjlbAYvtfHmlCMXxjcoVaIdwy/IAt3+mDkZkfvb2Dg==", "dev": true, "dependencies": { - "@eslint/eslintrc": "^1.3.2", - "@humanwhocodes/config-array": "^0.10.5", - "@humanwhocodes/gitignore-to-minimatch": "^1.0.2", + "@eslint-community/eslint-utils": "^4.2.0", + "@eslint-community/regexpp": "^4.6.1", + "@eslint/eslintrc": "^2.1.1", + "@eslint/js": "^8.46.0", + "@humanwhocodes/config-array": "^0.11.10", "@humanwhocodes/module-importer": "^1.0.1", - "ajv": "^6.10.0", + "@nodelib/fs.walk": "^1.2.8", + "ajv": "^6.12.4", "chalk": "^4.0.0", "cross-spawn": "^7.0.2", "debug": "^4.3.2", "doctrine": "^3.0.0", "escape-string-regexp": "^4.0.0", - "eslint-scope": "^7.1.1", - "eslint-utils": "^3.0.0", - "eslint-visitor-keys": "^3.3.0", - "espree": "^9.4.0", - "esquery": "^1.4.0", + "eslint-scope": "^7.2.2", + "eslint-visitor-keys": "^3.4.2", + "espree": "^9.6.1", + "esquery": "^1.4.2", "esutils": "^2.0.2", "fast-deep-equal": "^3.1.3", "file-entry-cache": "^6.0.1", "find-up": "^5.0.0", - "glob-parent": "^6.0.1", - "globals": "^13.15.0", - "globby": "^11.1.0", - "grapheme-splitter": "^1.0.4", + "glob-parent": "^6.0.2", + "globals": "^13.19.0", + "graphemer": "^1.4.0", "ignore": "^5.2.0", - "import-fresh": "^3.0.0", "imurmurhash": "^0.1.4", "is-glob": "^4.0.0", - "js-sdsl": "^4.1.4", + "is-path-inside": "^3.0.3", "js-yaml": "^4.1.0", "json-stable-stringify-without-jsonify": "^1.0.1", "levn": "^0.4.1", "lodash.merge": "^4.6.2", "minimatch": "^3.1.2", "natural-compare": "^1.4.0", - "optionator": "^0.9.1", - "regexpp": "^3.2.0", + "optionator": "^0.9.3", "strip-ansi": "^6.0.1", - "strip-json-comments": "^3.1.0", "text-table": "^0.2.0" }, "bin": { @@ -4323,13 +4554,14 @@ } }, "node_modules/eslint-import-resolver-node": { - "version": "0.3.6", - "resolved": "https://registry.npmjs.org/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.6.tgz", - "integrity": "sha512-0En0w03NRVMn9Uiyn8YRPDKvWjxCWkslUEhGNTdGx15RvPJYQ+lbOlqrlNI2vEAs4pDYK4f/HN2TbDmk5TP0iw==", + "version": "0.3.7", + "resolved": "https://registry.npmjs.org/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.7.tgz", + "integrity": "sha512-gozW2blMLJCeFpBwugLTGyvVjNoeo1knonXAcatC6bjPBZitotxdWf7Gimr25N4c0AAOo4eOUfaG82IJPDpqCA==", "dev": true, "dependencies": { "debug": "^3.2.7", - "resolve": "^1.20.0" + "is-core-module": "^2.11.0", + "resolve": "^1.22.1" } }, "node_modules/eslint-import-resolver-node/node_modules/debug": { @@ -4342,9 +4574,9 @@ } }, "node_modules/eslint-module-utils": { - "version": "2.7.4", - "resolved": "https://registry.npmjs.org/eslint-module-utils/-/eslint-module-utils-2.7.4.tgz", - "integrity": "sha512-j4GT+rqzCoRKHwURX7pddtIPGySnX9Si/cgMI5ztrcqOPtk5dDEeZ34CQVPphnqkJytlc97Vuk05Um2mJ3gEQA==", + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/eslint-module-utils/-/eslint-module-utils-2.8.0.tgz", + "integrity": "sha512-aWajIYfsqCKRDgUfjEXNN/JlrzauMuSEy5sbd7WXbtW3EH6A6MpwEh42c7qD+MqQo9QMJ6fWLAeIJynx0g6OAw==", "dev": true, "dependencies": { "debug": "^3.2.7" @@ -4383,24 +4615,29 @@ } }, "node_modules/eslint-plugin-import": { - "version": "2.26.0", - "resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.26.0.tgz", - "integrity": "sha512-hYfi3FXaM8WPLf4S1cikh/r4IxnO6zrhZbEGz2b660EJRbuxgpDS5gkCuYgGWg2xxh2rBuIr4Pvhve/7c31koA==", + "version": "2.28.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.28.0.tgz", + "integrity": "sha512-B8s/n+ZluN7sxj9eUf7/pRFERX0r5bnFA2dCaLHy2ZeaQEAz0k+ZZkFWRFHJAqxfxQDx6KLv9LeIki7cFdwW+Q==", "dev": true, "dependencies": { - "array-includes": "^3.1.4", - "array.prototype.flat": "^1.2.5", - "debug": "^2.6.9", + "array-includes": "^3.1.6", + "array.prototype.findlastindex": "^1.2.2", + "array.prototype.flat": "^1.3.1", + "array.prototype.flatmap": "^1.3.1", + "debug": "^3.2.7", "doctrine": "^2.1.0", - "eslint-import-resolver-node": "^0.3.6", - "eslint-module-utils": "^2.7.3", + "eslint-import-resolver-node": "^0.3.7", + "eslint-module-utils": "^2.8.0", "has": "^1.0.3", - "is-core-module": "^2.8.1", + "is-core-module": "^2.12.1", "is-glob": "^4.0.3", "minimatch": "^3.1.2", - "object.values": "^1.1.5", - "resolve": "^1.22.0", - "tsconfig-paths": "^3.14.1" + "object.fromentries": "^2.0.6", + "object.groupby": "^1.0.0", + "object.values": "^1.1.6", + "resolve": "^1.22.3", + "semver": "^6.3.1", + "tsconfig-paths": "^3.14.2" }, "engines": { "node": ">=4" @@ -4409,6 +4646,15 @@ "eslint": "^2 || ^3 || ^4 || ^5 || ^6 || ^7.2.0 || ^8" } }, + "node_modules/eslint-plugin-import/node_modules/debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dev": true, + "dependencies": { + "ms": "^2.1.1" + } + }, "node_modules/eslint-plugin-import/node_modules/doctrine": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz", @@ -4421,16 +4667,13 @@ "node": ">=0.10.0" } }, - "node_modules/eslint-plugin-import/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "node_modules/eslint-plugin-import/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", "dev": true, - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" + "bin": { + "semver": "bin/semver.js" } }, "node_modules/eslint-plugin-jsx-a11y": { @@ -4461,19 +4704,6 @@ "eslint": "^3 || ^4 || ^5 || ^6 || ^7 || ^8" } }, - "node_modules/eslint-plugin-jsx-a11y/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dev": true, - "peer": true, - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, "node_modules/eslint-plugin-jsx-a11y/node_modules/semver": { "version": "6.3.0", "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", @@ -4539,19 +4769,6 @@ "node": ">=0.10.0" } }, - "node_modules/eslint-plugin-react/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dev": true, - "peer": true, - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, "node_modules/eslint-plugin-react/node_modules/resolve": { "version": "2.0.0-next.4", "resolved": "https://registry.npmjs.org/resolve/-/resolve-2.0.0-next.4.tgz", @@ -4590,9 +4807,9 @@ } }, "node_modules/eslint-scope": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.1.1.tgz", - "integrity": "sha512-QKQM/UXpIiHcLqJ5AOyIW7XZmzjkzQXYE54n1++wb0u9V/abW3l9uQnxX8Z5Xd18xyKIMTUAyQ0k1e8pz6LUrw==", + "version": "7.2.2", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.2.tgz", + "integrity": "sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==", "dev": true, "dependencies": { "esrecurse": "^4.3.0", @@ -4600,42 +4817,21 @@ }, "engines": { "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - } - }, - "node_modules/eslint-utils": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/eslint-utils/-/eslint-utils-3.0.0.tgz", - "integrity": "sha512-uuQC43IGctw68pJA1RgbQS8/NP7rch6Cwd4j3ZBtgo4/8Flj4eGE7ZYSZRN3iq5pVUv6GPdW5Z1RFleo84uLDA==", - "dev": true, - "dependencies": { - "eslint-visitor-keys": "^2.0.0" - }, - "engines": { - "node": "^10.0.0 || ^12.0.0 || >= 14.0.0" }, "funding": { - "url": "https://github.com/sponsors/mysticatea" - }, - "peerDependencies": { - "eslint": ">=5" - } - }, - "node_modules/eslint-utils/node_modules/eslint-visitor-keys": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-2.1.0.tgz", - "integrity": "sha512-0rSmRBzXgDzIsD6mGdJgevzgezI534Cer5L/vyMX0kHzT/jiB43jRhd9YUlMGYLQy2zprNmoT8qasCGtY+QaKw==", - "dev": true, - "engines": { - "node": ">=10" + "url": "https://opencollective.com/eslint" } }, "node_modules/eslint-visitor-keys": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.3.0.tgz", - "integrity": "sha512-mQ+suqKJVyeuwGYHAdjMFqjCyfl8+Ldnxuyp3ldiMBFKkvytrXUZWaiPCEav8qDHKty44bD+qV1IP4T+w+xXRA==", + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.2.tgz", + "integrity": "sha512-8drBzUEyZ2llkpCA67iYrgEssKDUu68V8ChqqOfFupIaG/LCVPUT+CoGJpT77zJprs4T/W7p07LP7zAIMuweVw==", "dev": true, "engines": { "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" } }, "node_modules/eslint/node_modules/ansi-regex": { @@ -4760,9 +4956,9 @@ } }, "node_modules/eslint/node_modules/globals": { - "version": "13.17.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-13.17.0.tgz", - "integrity": "sha512-1C+6nQRb1GwGMKm2dH/E7enFAMxGTmGI7/dEdhy/DNelv85w9B72t3uc5frtMNXIbzrarJJ/lTCjcaZwbLJmyw==", + "version": "13.20.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.20.0.tgz", + "integrity": "sha512-Qg5QtVkCy/kv3FUSlu4ukeZDVf9ee0iXLAUYX13gbR17bnejFTzr4iS9bY7kwCf1NztRNm1t91fjOiyx4CSwPQ==", "dev": true, "dependencies": { "type-fest": "^0.20.2" @@ -4795,18 +4991,6 @@ "js-yaml": "bin/js-yaml.js" } }, - "node_modules/eslint/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dev": true, - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, "node_modules/eslint/node_modules/ms": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", @@ -4847,14 +5031,14 @@ } }, "node_modules/espree": { - "version": "9.4.0", - "resolved": "https://registry.npmjs.org/espree/-/espree-9.4.0.tgz", - "integrity": "sha512-DQmnRpLj7f6TgN/NYb0MTzJXL+vJF9h3pHy4JhCIs3zwcgez8xmGg3sXHcEO97BrmO2OSvCwMdfdlyl+E9KjOw==", + "version": "9.6.1", + "resolved": "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz", + "integrity": "sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==", "dev": true, "dependencies": { - "acorn": "^8.8.0", + "acorn": "^8.9.0", "acorn-jsx": "^5.3.2", - "eslint-visitor-keys": "^3.3.0" + "eslint-visitor-keys": "^3.4.1" }, "engines": { "node": "^12.22.0 || ^14.17.0 || >=16.0.0" @@ -4863,15 +5047,6 @@ "url": "https://opencollective.com/eslint" } }, - "node_modules/espree/node_modules/acorn-jsx": { - "version": "5.3.2", - "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", - "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", - "dev": true, - "peerDependencies": { - "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" - } - }, "node_modules/esprima": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", @@ -4886,9 +5061,9 @@ } }, "node_modules/esquery": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.4.0.tgz", - "integrity": "sha512-cCDispWt5vHHtwMY2YrAQ4ibFkAL8RbH5YGBnZBc90MolvvfkkQcJro/aZiAQUlQ3qgrYS6D6v8Gc5G5CQsc9w==", + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.5.0.tgz", + "integrity": "sha512-YQLXUplAwJgCydQ78IMJywZCceoqk1oH01OERdSAJc/7U2AylwjhSCLDEtqwg811idIS/9fIU5GjG73IgjKMVg==", "dev": true, "dependencies": { "estraverse": "^5.1.0" @@ -4946,13 +5121,13 @@ } }, "node_modules/express": { - "version": "4.18.1", - "resolved": "https://registry.npmjs.org/express/-/express-4.18.1.tgz", - "integrity": "sha512-zZBcOX9TfehHQhtupq57OF8lFZ3UZi08Y97dwFCkD8p9d/d2Y3M+ykKcwaMDEL+4qyUolgBDX6AblpR3fL212Q==", + "version": "4.18.2", + "resolved": "https://registry.npmjs.org/express/-/express-4.18.2.tgz", + "integrity": "sha512-5/PsL6iGPdfQ/lKM1UuielYgv3BUoJfz1aUwU9vHZ+J7gyvwdQXFEBIEIaxeGf0GIcreATNyBExtalisDbuMqQ==", "dependencies": { "accepts": "~1.3.8", "array-flatten": "1.1.1", - "body-parser": "1.20.0", + "body-parser": "1.20.1", "content-disposition": "0.5.4", "content-type": "~1.0.4", "cookie": "0.5.0", @@ -4971,7 +5146,7 @@ "parseurl": "~1.3.3", "path-to-regexp": "0.1.7", "proxy-addr": "~2.0.7", - "qs": "6.10.3", + "qs": "6.11.0", "range-parser": "~1.2.1", "safe-buffer": "5.2.1", "send": "0.18.0", @@ -4986,6 +5161,43 @@ "node": ">= 0.10.0" } }, + "node_modules/express/node_modules/body-parser": { + "version": "1.20.1", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.1.tgz", + "integrity": "sha512-jWi7abTbYwajOytWCQc37VulmWiRae5RyTpaCyDcS5/lMdtwSz5lOpDE67srw/HYe35f1z3fDQw+3txg7gNtWw==", + "dependencies": { + "bytes": "3.1.2", + "content-type": "~1.0.4", + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", + "on-finished": "2.4.1", + "qs": "6.11.0", + "raw-body": "2.5.1", + "type-is": "~1.6.18", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/express/node_modules/raw-body": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.1.tgz", + "integrity": "sha512-qqJBtEyVgS0ZmPGdCFPWJ3FreoqvG4MVQln/kCgF7Olq95IbOp0/BWyMwbdtn4VTvkM8Y7khCQ2Xgk/tcrCXig==", + "dependencies": { + "bytes": "3.1.2", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, "node_modules/express/node_modules/safe-buffer": { "version": "5.2.1", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", @@ -5011,35 +5223,6 @@ "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", "dev": true }, - "node_modules/fast-glob": { - "version": "3.2.12", - "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.2.12.tgz", - "integrity": "sha512-DVj4CQIYYow0BlaelwK1pHl5n5cRSJfM60UA0zK891sVInoPri2Ekj7+e1CT3/3qxXenpI+nBBmQAcJPJgaj4w==", - "dev": true, - "dependencies": { - "@nodelib/fs.stat": "^2.0.2", - "@nodelib/fs.walk": "^1.2.3", - "glob-parent": "^5.1.2", - "merge2": "^1.3.0", - "micromatch": "^4.0.4" - }, - "engines": { - "node": ">=8.6.0" - } - }, - "node_modules/fast-glob/node_modules/micromatch": { - "version": "4.0.5", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.5.tgz", - "integrity": "sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==", - "dev": true, - "dependencies": { - "braces": "^3.0.2", - "picomatch": "^2.3.1" - }, - "engines": { - "node": ">=8.6" - } - }, "node_modules/fast-json-stable-stringify": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", @@ -5059,9 +5242,9 @@ "dev": true }, "node_modules/fastq": { - "version": "1.13.0", - "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.13.0.tgz", - "integrity": "sha512-YpkpUnK8od0o1hmeSc7UUs/eB/vIPWJYjKck2QKIzAf71Vm1AAQ3EbuZB3g2JIy+pg+ERD0vqI79KyZiB2e2Nw==", + "version": "1.15.0", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.15.0.tgz", + "integrity": "sha512-wBrocU2LCXXa+lWBt8RoIRD89Fi8OdABODa/kEnyeyjS5aZO5/GNvI5sEINADqP/h8M29UHTHUb53sUu5Ihqdw==", "dev": true, "dependencies": { "reusify": "^1.0.4" @@ -5130,21 +5313,93 @@ "node": ">= 0.8" } }, - "node_modules/find-cache-dir": { - "version": "3.3.2", - "resolved": "https://registry.npmjs.org/find-cache-dir/-/find-cache-dir-3.3.2.tgz", - "integrity": "sha512-wXZV5emFEjrridIgED11OoUKLxiYjAcqot/NJdAkOhlJ+vGzwhOAfcG5OX1jP+S0PcjEn8bdMJv+g2jwQ3Onig==", + "node_modules/find-cache-dir": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/find-cache-dir/-/find-cache-dir-4.0.0.tgz", + "integrity": "sha512-9ZonPT4ZAK4a+1pUPVPZJapbi7O5qbbJPdYw/NOQWZZbVLdDTYM3A4R9z/DpAM08IDaFGsvPgiGZ82WEwUDWjg==", + "dev": true, + "dependencies": { + "common-path-prefix": "^3.0.0", + "pkg-dir": "^7.0.0" + }, + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/find-up": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-6.3.0.tgz", + "integrity": "sha512-v2ZsoEuVHYy8ZIlYqwPe/39Cy+cFDzp4dXPaxNvkEuouymu+2Jbz0PxpKarJHYJTmv2HWT3O382qY8l4jMWthw==", + "dev": true, + "dependencies": { + "locate-path": "^7.1.0", + "path-exists": "^5.0.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/find-up/node_modules/locate-path": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-7.2.0.tgz", + "integrity": "sha512-gvVijfZvn7R+2qyPX8mAuKcFGDf6Nc61GdvGafQsHL0sBIxfKzA+usWn4GFC/bk+QdwPUD4kWFJLhElipq+0VA==", + "dev": true, + "dependencies": { + "p-locate": "^6.0.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/find-up/node_modules/p-limit": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-4.0.0.tgz", + "integrity": "sha512-5b0R4txpzjPWVw/cXXUResoD4hb6U/x9BH08L7nw+GN1sezDzPdxeRvpc9c433fZhBan/wusjbCsqwqm4EIBIQ==", + "dev": true, + "dependencies": { + "yocto-queue": "^1.0.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/find-up/node_modules/p-locate": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-6.0.0.tgz", + "integrity": "sha512-wPrq66Llhl7/4AGC6I+cqxT07LhXvWL08LNXz1fENOw0Ap4sRZZ/gZpTTJ5jpurzzzfS2W/Ge9BY3LgLjCShcw==", + "dev": true, + "dependencies": { + "p-limit": "^4.0.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/find-up/node_modules/yocto-queue": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-1.0.0.tgz", + "integrity": "sha512-9bnSc/HEW2uRy67wc+T8UwauLuPJVn28jb+GtJY16iiKWyvmYJRXVT4UamsAEGQfPohgr2q4Tq0sQbQlxTfi1g==", "dev": true, - "dependencies": { - "commondir": "^1.0.1", - "make-dir": "^3.0.2", - "pkg-dir": "^4.1.0" - }, "engines": { - "node": ">=8" + "node": ">=12.20" }, "funding": { - "url": "https://github.com/avajs/find-cache-dir?sponsor=1" + "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/flat": { @@ -5190,6 +5445,15 @@ "integrity": "sha512-5nqDSxl8nn5BSNxyR3n4I6eDmbolI6WT+QqR547RwxQapgjQBmtktdP+HTBb/a/zLsbzERTONyUB5pefh5TtjQ==", "dev": true }, + "node_modules/for-each": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.3.tgz", + "integrity": "sha512-jqYfLp7mo9vIyQf8ykW2v7A+2N4QjeCeI5+Dz9XraiO1ign81wjiH7Fb9vSOWvQfNtmSa4H2RoQTrrXivdUZmw==", + "dev": true, + "dependencies": { + "is-callable": "^1.1.3" + } + }, "node_modules/form-data": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", @@ -5205,32 +5469,20 @@ } }, "node_modules/formidable": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/formidable/-/formidable-2.0.1.tgz", - "integrity": "sha512-rjTMNbp2BpfQShhFbR3Ruk3qk2y9jKpvMW78nJgx8QKtxjDVrwbZG+wvDOmVbifHyOUOQJXxqEy6r0faRrPzTQ==", + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/formidable/-/formidable-2.1.2.tgz", + "integrity": "sha512-CM3GuJ57US06mlpQ47YcunuUZ9jpm8Vx+P2CGt2j7HpgkKZO/DJYQ0Bobim8G6PFQmK5lOqOOdUXboU+h73A4g==", "dev": true, "dependencies": { - "dezalgo": "1.0.3", - "hexoid": "1.0.0", - "once": "1.4.0", - "qs": "6.9.3" + "dezalgo": "^1.0.4", + "hexoid": "^1.0.0", + "once": "^1.4.0", + "qs": "^6.11.0" }, "funding": { "url": "https://ko-fi.com/tunnckoCore/commissions" } }, - "node_modules/formidable/node_modules/qs": { - "version": "6.9.3", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.9.3.tgz", - "integrity": "sha512-EbZYNarm6138UKKq46tdx08Yo/q9ZhFoAXAI1meAFd2GtbRDhbZY2WQSICskT0c5q99aFzLG1D4nvTk9tqfXIw==", - "dev": true, - "engines": { - "node": ">=0.6" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/forwarded": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", @@ -5301,12 +5553,13 @@ } }, "node_modules/get-intrinsic": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.1.3.tgz", - "integrity": "sha512-QJVz1Tj7MS099PevUG5jvnt9tSkXN8K14dxQlikJuPt4uD9hHAHjLyLBiLR5zELelBdD9QNRAXZzsJx0WaDL9A==", + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.1.tgz", + "integrity": "sha512-2DcsyfABl+gVHEfCOaTrWgyt+tb6MSEGmKq+kI5HwLbIYgjgmMcV8KQ41uaKz1xxUcn9tJtgFbQUEVcEbd0FYw==", "dependencies": { "function-bind": "^1.1.1", "has": "^1.0.3", + "has-proto": "^1.0.1", "has-symbols": "^1.0.3" }, "funding": { @@ -5377,33 +5630,31 @@ "node": ">=0.10.0" } }, - "node_modules/globby": { - "version": "11.1.0", - "resolved": "https://registry.npmjs.org/globby/-/globby-11.1.0.tgz", - "integrity": "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==", + "node_modules/globalthis": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/globalthis/-/globalthis-1.0.3.tgz", + "integrity": "sha512-sFdI5LyBiNTHjRd7cGPWapiHWMOXKyuBNX/cWJ3NfzrZQVa8GI/8cofCl74AOVqq9W5kNmguTIzJ/1s2gyI9wA==", "dev": true, "dependencies": { - "array-union": "^2.1.0", - "dir-glob": "^3.0.1", - "fast-glob": "^3.2.9", - "ignore": "^5.2.0", - "merge2": "^1.4.1", - "slash": "^3.0.0" + "define-properties": "^1.1.3" }, "engines": { - "node": ">=10" + "node": ">= 0.4" }, "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/globby/node_modules/slash": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", - "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "node_modules/gopd": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz", + "integrity": "sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==", "dev": true, - "engines": { - "node": ">=8" + "dependencies": { + "get-intrinsic": "^1.1.3" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, "node_modules/graceful-fs": { @@ -5413,10 +5664,10 @@ "dev": true, "peer": true }, - "node_modules/grapheme-splitter": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/grapheme-splitter/-/grapheme-splitter-1.0.4.tgz", - "integrity": "sha512-bzh50DW9kTPM00T8y4o8vQg89Di9oLJVLW/KaOGIXJWP/iqCN6WKYkbNOF04vFLJhwcpYUh9ydh/+5vpOqV4YQ==", + "node_modules/graphemer": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", + "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", "dev": true }, "node_modules/handlebars": { @@ -5502,6 +5753,17 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/has-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.1.tgz", + "integrity": "sha512-7qE+iP+O+bgF9clE5+UoBFzE65mlBiVj3tKCrlNQ0Ogwm0BjpT/gK4SlLYDMybDh5I3TCTKnPPa0oMG7JDYrhg==", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/has-symbols": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", @@ -5577,29 +5839,10 @@ "node": ">=0.10.0" } }, - "node_modules/ieee754": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", - "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ] - }, "node_modules/ignore": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.2.0.tgz", - "integrity": "sha512-CmxgYGiEPCLhfLnpPp1MoRmifwEIOgjcHXxOBjv7mY96c+eWScsOP9c112ZyLdWHi0FxHjI+4uVhKYp/gcdRmQ==", + "version": "5.2.4", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.2.4.tgz", + "integrity": "sha512-MAb38BcSbH0eHNBxn7ql2NH/kX33OkB3lZ1BNdh7ENeRChHTYsTvWrMubiIAMNS2llXEEgZ1MUOBtXChP3kaFQ==", "dev": true, "engines": { "node": ">= 4" @@ -5647,12 +5890,12 @@ "dev": true }, "node_modules/internal-slot": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.0.3.tgz", - "integrity": "sha512-O0DB1JC/sPyZl7cIo78n5dR7eUSwwpYPiXRhTzNxZVAMUuB8vlnRFyLxdrVToks6XPLVnFfbzaVd5WLjhgg+vA==", + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.0.5.tgz", + "integrity": "sha512-Y+R5hJrzs52QCG2laLn4udYVnxsfny9CpOhNhUvk/SSSVyF6T27FzRbF0sroPidSu3X8oEAkOn2K804mjpt6UQ==", "dev": true, "dependencies": { - "get-intrinsic": "^1.1.0", + "get-intrinsic": "^1.2.0", "has": "^1.0.3", "side-channel": "^1.0.4" }, @@ -5682,6 +5925,20 @@ "node": ">= 0.10" } }, + "node_modules/is-array-buffer": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.2.tgz", + "integrity": "sha512-y+FyyR/w8vfIRq4eQcM1EYgSTnmHXPqaF+IgzgraytCFq5Xh8lllDVmAZolPJiZttZLeFSINPYMaEJ7/vWUa1w==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "get-intrinsic": "^1.2.0", + "is-typed-array": "^1.1.10" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/is-bigint": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/is-bigint/-/is-bigint-1.0.4.tgz", @@ -5723,9 +5980,9 @@ } }, "node_modules/is-core-module": { - "version": "2.10.0", - "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.10.0.tgz", - "integrity": "sha512-Erxj2n/LDAZ7H8WNJXd9tw38GYM3dv8rk8Zcs+jJuxYTW7sozH+SS8NtrSjVL1/vpLvWi1hxy96IzjJ3EHTJJg==", + "version": "2.12.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.12.1.tgz", + "integrity": "sha512-Q4ZuBAe2FUsKtyQJoQHlvP8OvBERxO3jEmy1I7hcRXcJBGGHFh/aJBswbXuS9sgrDH2QUO8ilkwNPHvHMd8clg==", "dev": true, "dependencies": { "has": "^1.0.3" @@ -5818,6 +6075,15 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-path-inside": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz", + "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, "node_modules/is-plain-obj": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-2.1.0.tgz", @@ -5906,6 +6172,21 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-typed-array": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.12.tgz", + "integrity": "sha512-Z14TF2JNG8Lss5/HMqt0//T9JeHXttXy5pH/DBU4vi98ozO2btxzq9MwYDZYnKwU8nRsz/+GVFVRDq3DkVuSPg==", + "dev": true, + "dependencies": { + "which-typed-array": "^1.1.11" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/is-unicode-supported": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz", @@ -5936,6 +6217,12 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/isarray": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", + "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", + "dev": true + }, "node_modules/isexe": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", @@ -6113,12 +6400,6 @@ "url": "https://github.com/chalk/supports-color?sponsor=1" } }, - "node_modules/js-sdsl": { - "version": "4.1.5", - "resolved": "https://registry.npmjs.org/js-sdsl/-/js-sdsl-4.1.5.tgz", - "integrity": "sha512-08bOAKweV2NUC1wqTtf3qZlnpOX/R2DU9ikpjOHs0H+ibQv3zpncVQg6um4uYtRtrwIX8M4Nh3ytK4HGlYAq7Q==", - "dev": true - }, "node_modules/js-tokens": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-3.0.2.tgz", @@ -6166,6 +6447,18 @@ "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", "dev": true }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, "node_modules/jsx-ast-utils": { "version": "3.3.3", "resolved": "https://registry.npmjs.org/jsx-ast-utils/-/jsx-ast-utils-3.3.3.tgz", @@ -6187,9 +6480,12 @@ "dev": true }, "node_modules/kareem": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/kareem/-/kareem-2.4.1.tgz", - "integrity": "sha512-aJ9opVoXroQUPfovYP5kaj2lM7Jn02Gw13bL0lg9v0V7SaUc0qavPs0Eue7d2DcC3NjqI6QAUElXNsuZSeM+EA==" + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/kareem/-/kareem-2.5.1.tgz", + "integrity": "sha512-7jFxRVm+jD+rkq3kY0iZDJfsO2/t4BBPeEb2qKn2lR/9KhuksYk5hxzfRYWMPV8P/x2d0kHD306YyWLzjjH+uA==", + "engines": { + "node": ">=12.0.0" + } }, "node_modules/language-subtag-registry": { "version": "0.3.22", @@ -6369,39 +6665,12 @@ } }, "node_modules/lru-cache": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", - "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", - "dev": true, - "dependencies": { - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/make-dir": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz", - "integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==", + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", "dev": true, "dependencies": { - "semver": "^6.0.0" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/make-dir/node_modules/semver": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", - "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", - "dev": true, - "bin": { - "semver": "bin/semver.js" + "yallist": "^3.0.2" } }, "node_modules/media-typer": { @@ -6430,15 +6699,6 @@ "dev": true, "peer": true }, - "node_modules/merge2": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", - "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", - "dev": true, - "engines": { - "node": ">= 8" - } - }, "node_modules/method-override": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/method-override/-/method-override-3.0.0.tgz", @@ -6505,9 +6765,9 @@ } }, "node_modules/minimatch": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", - "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", "dev": true, "dependencies": { "brace-expansion": "^1.1.7" @@ -6535,12 +6795,11 @@ } }, "node_modules/mocha": { - "version": "10.0.0", - "resolved": "https://registry.npmjs.org/mocha/-/mocha-10.0.0.tgz", - "integrity": "sha512-0Wl+elVUD43Y0BqPZBzZt8Tnkw9CMUdNYnUsTfOM1vuhJVZL+kiesFYsqwBkEEuEixaiPe5ZQdqDgX2jddhmoA==", + "version": "10.2.0", + "resolved": "https://registry.npmjs.org/mocha/-/mocha-10.2.0.tgz", + "integrity": "sha512-IDY7fl/BecMwFHzoqF2sg/SHHANeBoMMXFlS9r0OXKDssYE1M5O43wUY/9BVPeIvfH2zmEbBfseqN9gBQZzXkg==", "dev": true, "dependencies": { - "@ungap/promise-all-settled": "1.1.2", "ansi-colors": "4.1.1", "browser-stdout": "1.3.1", "chokidar": "3.5.3", @@ -6855,46 +7114,69 @@ } }, "node_modules/mongodb": { - "version": "4.9.1", - "resolved": "https://registry.npmjs.org/mongodb/-/mongodb-4.9.1.tgz", - "integrity": "sha512-ZhgI/qBf84fD7sI4waZBoLBNJYPQN5IOC++SBCiPiyhzpNKOxN/fi0tBHvH2dEC42HXtNEbFB0zmNz4+oVtorQ==", + "version": "5.7.0", + "resolved": "https://registry.npmjs.org/mongodb/-/mongodb-5.7.0.tgz", + "integrity": "sha512-zm82Bq33QbqtxDf58fLWBwTjARK3NSvKYjyz997KSy6hpat0prjeX/kxjbPVyZY60XYPDNETaHkHJI2UCzSLuw==", "dependencies": { - "bson": "^4.7.0", - "denque": "^2.1.0", - "mongodb-connection-string-url": "^2.5.3", - "socks": "^2.7.0" + "bson": "^5.4.0", + "mongodb-connection-string-url": "^2.6.0", + "socks": "^2.7.1" }, "engines": { - "node": ">=12.9.0" + "node": ">=14.20.1" }, "optionalDependencies": { "saslprep": "^1.0.3" + }, + "peerDependencies": { + "@aws-sdk/credential-providers": "^3.201.0", + "@mongodb-js/zstd": "^1.1.0", + "kerberos": "^2.0.1", + "mongodb-client-encryption": ">=2.3.0 <3", + "snappy": "^7.2.2" + }, + "peerDependenciesMeta": { + "@aws-sdk/credential-providers": { + "optional": true + }, + "@mongodb-js/zstd": { + "optional": true + }, + "kerberos": { + "optional": true + }, + "mongodb-client-encryption": { + "optional": true + }, + "snappy": { + "optional": true + } } }, "node_modules/mongodb-connection-string-url": { - "version": "2.5.4", - "resolved": "https://registry.npmjs.org/mongodb-connection-string-url/-/mongodb-connection-string-url-2.5.4.tgz", - "integrity": "sha512-SeAxuWs0ez3iI3vvmLk/j2y+zHwigTDKQhtdxTgt5ZCOQQS5+HW4g45/Xw5vzzbn7oQXCNQ24Z40AkJsizEy7w==", + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/mongodb-connection-string-url/-/mongodb-connection-string-url-2.6.0.tgz", + "integrity": "sha512-WvTZlI9ab0QYtTYnuMLgobULWhokRjtC7db9LtcVfJ+Hsnyr5eo6ZtNAt3Ly24XZScGMelOcGtm7lSn0332tPQ==", "dependencies": { "@types/whatwg-url": "^8.2.1", "whatwg-url": "^11.0.0" } }, "node_modules/mongoose": { - "version": "6.6.5", - "resolved": "https://registry.npmjs.org/mongoose/-/mongoose-6.6.5.tgz", - "integrity": "sha512-iA/oDpWOc+K2QYzA4Eq7Z1oUBQOz9FGDmUwPLgw872Bfs/qizA5Db+gJorAn+TnnGu3VoCK8iP4Y+TECUelwjA==", + "version": "7.4.2", + "resolved": "https://registry.npmjs.org/mongoose/-/mongoose-7.4.2.tgz", + "integrity": "sha512-sNolW2hyncwvWmZjIEIwAckjaSKtC1SE86zE1v2TKm3vPTRogZfBQf+3zLYYdrgrVTzoaoICieVpct9hjcn3EQ==", "dependencies": { - "bson": "^4.6.5", - "kareem": "2.4.1", - "mongodb": "4.9.1", + "bson": "^5.4.0", + "kareem": "2.5.1", + "mongodb": "5.7.0", "mpath": "0.9.0", - "mquery": "4.0.3", + "mquery": "5.0.0", "ms": "2.1.3", - "sift": "16.0.0" + "sift": "16.0.1" }, "engines": { - "node": ">=12.0.0" + "node": ">=14.20.1" }, "funding": { "type": "opencollective", @@ -6910,14 +7192,14 @@ } }, "node_modules/mquery": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/mquery/-/mquery-4.0.3.tgz", - "integrity": "sha512-J5heI+P08I6VJ2Ky3+33IpCdAvlYGTSUjwTPxkAr8i8EoduPMBX2OY/wa3IKZIQl7MU4SbFk8ndgSKyB/cl1zA==", + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/mquery/-/mquery-5.0.0.tgz", + "integrity": "sha512-iQMncpmEK8R8ncT8HJGsGc9Dsp8xcgYMVSbs5jgnm1lFHTZqMJTUWTDx1LBO8+mK3tPNZWFLBghQEIOULSTHZg==", "dependencies": { "debug": "4.x" }, "engines": { - "node": ">=12.0.0" + "node": ">=14.0.0" } }, "node_modules/mquery/node_modules/debug": { @@ -6979,18 +7261,27 @@ "dev": true }, "node_modules/nise": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/nise/-/nise-5.1.1.tgz", - "integrity": "sha512-yr5kW2THW1AkxVmCnKEh4nbYkJdB3I7LUkiUgOvEkOp414mc2UMaHMA7pjq1nYowhdoJZGwEKGaQVbxfpWj10A==", + "version": "5.1.4", + "resolved": "https://registry.npmjs.org/nise/-/nise-5.1.4.tgz", + "integrity": "sha512-8+Ib8rRJ4L0o3kfmyVCL7gzrohyDe0cMFTBa2d364yIrEGMEoetznKJx899YxjybU6bL9SQkYPSBBs1gyYs8Xg==", "dev": true, "dependencies": { - "@sinonjs/commons": "^1.8.3", - "@sinonjs/fake-timers": ">=5", + "@sinonjs/commons": "^2.0.0", + "@sinonjs/fake-timers": "^10.0.2", "@sinonjs/text-encoding": "^0.7.1", "just-extend": "^4.0.2", "path-to-regexp": "^1.7.0" } }, + "node_modules/nise/node_modules/@sinonjs/commons": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-2.0.0.tgz", + "integrity": "sha512-uLa0j859mMrg2slwQYdO/AkrOfmH+X6LTVmNTS9CqexuE2IvVORIkSpJLqePAbEnKJ77aMmCwr1NUZ57120Xcg==", + "dev": true, + "dependencies": { + "type-detect": "4.0.8" + } + }, "node_modules/nise/node_modules/isarray": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", @@ -7007,9 +7298,9 @@ } }, "node_modules/node-releases": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.6.tgz", - "integrity": "sha512-PiVXnNuFm5+iYkLBNeq5211hvO38y63T0i2KKh2KnUs3RpzJ+JtODFjkD8yjLwnDkTYF1eKXheUwdssR+NRZdg==", + "version": "2.0.13", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.13.tgz", + "integrity": "sha512-uYr7J37ae/ORWdZeQ1xxMJe3NtdmqMC/JZK+geofDrkLUApKRHPd18/TxtBOJ4A0/+uUIliorNrfYV6s1b02eQ==", "dev": true }, "node_modules/nopt": { @@ -7042,9 +7333,9 @@ } }, "node_modules/object-inspect": { - "version": "1.12.2", - "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.12.2.tgz", - "integrity": "sha512-z+cPxW0QGUp0mcqcsgQyLVRDoXFQbXOwBaqyF7VIgI4TWNQsDHrBpUQslRmIfAoYWdYzs6UlKJtB2XJpTaNSpQ==", + "version": "1.12.3", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.12.3.tgz", + "integrity": "sha512-geUvdk7c+eizMNUDkRpW1wJwgfOiOeHbxBR/hLXK1aT6zmVSO0jsQcs7fj6MGw89jC/cjGfLcNOrtMYtGqm81g==", "funding": { "url": "https://github.com/sponsors/ljharb" } @@ -7091,15 +7382,14 @@ } }, "node_modules/object.fromentries": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/object.fromentries/-/object.fromentries-2.0.5.tgz", - "integrity": "sha512-CAyG5mWQRRiBU57Re4FKoTBjXfDoNwdFVH2Y1tS9PqCsfUTymAohOkEMSG3aRNKmv4lV3O7p1et7c187q6bynw==", + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/object.fromentries/-/object.fromentries-2.0.6.tgz", + "integrity": "sha512-VciD13dswC4j1Xt5394WR4MzmAQmlgN72phd/riNp9vtD7tp4QQWJ0R4wvclXcafgcYK8veHRed2W6XeGBvcfg==", "dev": true, - "peer": true, "dependencies": { "call-bind": "^1.0.2", - "define-properties": "^1.1.3", - "es-abstract": "^1.19.1" + "define-properties": "^1.1.4", + "es-abstract": "^1.20.4" }, "engines": { "node": ">= 0.4" @@ -7108,6 +7398,18 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/object.groupby": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/object.groupby/-/object.groupby-1.0.0.tgz", + "integrity": "sha512-70MWG6NfRH9GnbZOikuhPPYzpUpof9iW2J9E4dW7FXTqPNb6rllE6u39SKwwiNh8lCwX3DDb5OgcKGiEBrTTyw==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.2.0", + "es-abstract": "^1.21.2", + "get-intrinsic": "^1.2.1" + } + }, "node_modules/object.hasown": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/object.hasown/-/object.hasown-1.1.1.tgz", @@ -7123,14 +7425,14 @@ } }, "node_modules/object.values": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/object.values/-/object.values-1.1.5.tgz", - "integrity": "sha512-QUZRW0ilQ3PnPpbNtgdNV1PDbEqLIiSFB3l+EnGtBQ/8SUTLj1PZwtQHABZtLgwpJZTSZhuGLOGk57Drx2IvYg==", + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/object.values/-/object.values-1.1.6.tgz", + "integrity": "sha512-FVVTkD1vENCsAcwNs9k6jea2uHC/X0+JcjG8YA60FN5CMaJmG95wT9jek/xX9nornqGRrBkKtzuAu2wuHpKqvw==", "dev": true, "dependencies": { "call-bind": "^1.0.2", - "define-properties": "^1.1.3", - "es-abstract": "^1.19.1" + "define-properties": "^1.1.4", + "es-abstract": "^1.20.4" }, "engines": { "node": ">= 0.4" @@ -7160,17 +7462,17 @@ } }, "node_modules/optionator": { - "version": "0.9.1", - "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.1.tgz", - "integrity": "sha512-74RlY5FCnhq4jRxVUPKDaRwrVNXMqsGsiW6AJw4XK8hmtm10wC0ypZBLw5IIp85NZMr91+qd1RvvENwg7jjRFw==", + "version": "0.9.3", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.3.tgz", + "integrity": "sha512-JjCoypp+jKn1ttEFExxhetCKeJt9zhAgAve5FXHixTvFDW/5aEktX9bufBKLRRMdU7bNtpLfcGu94B3cdEJgjg==", "dev": true, "dependencies": { + "@aashutoshrathi/word-wrap": "^1.2.3", "deep-is": "^0.1.3", "fast-levenshtein": "^2.0.6", "levn": "^0.4.1", "prelude-ls": "^1.2.1", - "type-check": "^0.4.0", - "word-wrap": "^1.2.3" + "type-check": "^0.4.0" }, "engines": { "node": ">= 0.8.0" @@ -7235,6 +7537,15 @@ "node": ">= 0.8" } }, + "node_modules/path-exists": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-5.0.0.tgz", + "integrity": "sha512-RjhtfwJOxzcFmNOi6ltcbcu4Iu+FL3zEj83dk4kAS+fVpTxXLO1b38RvJgT/0QwvV/L3aY9TAnyv0EOqW4GoMQ==", + "dev": true, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + } + }, "node_modules/path-is-absolute": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", @@ -7264,15 +7575,6 @@ "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz", "integrity": "sha1-32BBeABfUi8V60SQ5yR6G/qmf4w=" }, - "node_modules/path-type": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", - "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", - "dev": true, - "engines": { - "node": ">=8" - } - }, "node_modules/picocolors": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz", @@ -7301,76 +7603,18 @@ } }, "node_modules/pkg-dir": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz", - "integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==", - "dev": true, - "dependencies": { - "find-up": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/pkg-dir/node_modules/find-up": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", - "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", - "dev": true, - "dependencies": { - "locate-path": "^5.0.0", - "path-exists": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/pkg-dir/node_modules/locate-path": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", - "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", - "dev": true, - "dependencies": { - "p-locate": "^4.1.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/pkg-dir/node_modules/p-limit": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", - "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", - "dev": true, - "dependencies": { - "p-try": "^2.0.0" - }, - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/pkg-dir/node_modules/p-locate": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", - "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", - "dev": true, - "dependencies": { - "p-limit": "^2.2.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/pkg-dir/node_modules/path-exists": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", - "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-7.0.0.tgz", + "integrity": "sha512-Ie9z/WINcxxLp27BKOCHGde4ITq9UklYKDzVo1nhk5sqGEXU3FpkwP5GM2voTGJkGd9B3Otl+Q4uwSOeSUtOBA==", "dev": true, + "dependencies": { + "find-up": "^6.3.0" + }, "engines": { - "node": ">=8" + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/prelude-ls": { @@ -7406,10 +7650,18 @@ "node": ">= 0.10" } }, + "node_modules/punycode": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.0.tgz", + "integrity": "sha512-rRV+zQD8tVFys26lAGR9WUuS4iUAngJScM+ZRSKtvl5tKeZ2t5bvdNFdNHBW9FWR4guGHlgmsZ1G7BSm2wTbuA==", + "engines": { + "node": ">=6" + } + }, "node_modules/qs": { - "version": "6.10.3", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.10.3.tgz", - "integrity": "sha512-wr7M2E0OFRfIfJZjKGieI8lBKb7fRCH4Fv5KNPEs7gJ8jadvotdsS08PzOKR7opXhZ/Xkjtt3WF9g38drmyRqQ==", + "version": "6.11.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.11.0.tgz", + "integrity": "sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==", "dependencies": { "side-channel": "^1.0.4" }, @@ -7458,9 +7710,9 @@ } }, "node_modules/raw-body": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.1.tgz", - "integrity": "sha512-qqJBtEyVgS0ZmPGdCFPWJ3FreoqvG4MVQln/kCgF7Olq95IbOp0/BWyMwbdtn4VTvkM8Y7khCQ2Xgk/tcrCXig==", + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.2.tgz", + "integrity": "sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==", "dependencies": { "bytes": "3.1.2", "http-errors": "2.0.0", @@ -7497,29 +7749,29 @@ } }, "node_modules/regenerator-runtime": { - "version": "0.13.9", - "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.9.tgz", - "integrity": "sha512-p3VT+cOEgxFsRRA9X4lkI1E+k2/CtnKtU4gcxyaCUreilL/vqI6CdZ3wxVUx3UOUg+gnUOQQcRI7BmSI656MYA==", + "version": "0.13.11", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz", + "integrity": "sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==", "dev": true }, "node_modules/regenerator-transform": { - "version": "0.15.0", - "resolved": "https://registry.npmjs.org/regenerator-transform/-/regenerator-transform-0.15.0.tgz", - "integrity": "sha512-LsrGtPmbYg19bcPHwdtmXwbW+TqNvtY4riE3P83foeHRroMbH6/2ddFBfab3t7kbzc7v7p4wbkIecHImqt0QNg==", + "version": "0.15.1", + "resolved": "https://registry.npmjs.org/regenerator-transform/-/regenerator-transform-0.15.1.tgz", + "integrity": "sha512-knzmNAcuyxV+gQCufkYcvOqX/qIIfHLv0u5x79kRxuGojfYVky1f15TzZEu2Avte8QGepvUNTnLskf8E6X6Vyg==", "dev": true, "dependencies": { "@babel/runtime": "^7.8.4" } }, "node_modules/regexp.prototype.flags": { - "version": "1.4.3", - "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.4.3.tgz", - "integrity": "sha512-fjggEOO3slI6Wvgjwflkc4NFRCTZAu5CnNfBd5qOMYhWdn67nJBBu34/TkD++eeFmd8C9r9jfXJ27+nSiRkSUA==", + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.0.tgz", + "integrity": "sha512-0SutC3pNudRKgquxGoRGIz946MZVHqbNfPjBdxeOhBrdgDKlRoXmYLQN9xRbrR09ZXWeGAdPuif7egofn6v5LA==", "dev": true, "dependencies": { "call-bind": "^1.0.2", - "define-properties": "^1.1.3", - "functions-have-names": "^1.2.2" + "define-properties": "^1.2.0", + "functions-have-names": "^1.2.3" }, "engines": { "node": ">= 0.4" @@ -7528,41 +7780,23 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/regexpp": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/regexpp/-/regexpp-3.2.0.tgz", - "integrity": "sha512-pq2bWo9mVD43nbts2wGv17XLiNLya+GklZ8kaDLV2Z08gDCsGpnKn9BFMepvWuHCbyVvY7J5o5+BVvoQbmlJLg==", - "dev": true, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/mysticatea" - } - }, "node_modules/regexpu-core": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/regexpu-core/-/regexpu-core-5.2.1.tgz", - "integrity": "sha512-HrnlNtpvqP1Xkb28tMhBUO2EbyUHdQlsnlAhzWcwHy8WJR53UWr7/MAvqrsQKMbV4qdpv03oTMG8iIhfsPFktQ==", + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/regexpu-core/-/regexpu-core-5.3.2.tgz", + "integrity": "sha512-RAM5FlZz+Lhmo7db9L298p2vHP5ZywrVXmVXpmAD9GuL5MPH6t9ROw1iA/wfHkQ76Qe7AaPF0nGuim96/IrQMQ==", "dev": true, "dependencies": { + "@babel/regjsgen": "^0.8.0", "regenerate": "^1.4.2", "regenerate-unicode-properties": "^10.1.0", - "regjsgen": "^0.7.1", "regjsparser": "^0.9.1", "unicode-match-property-ecmascript": "^2.0.0", - "unicode-match-property-value-ecmascript": "^2.0.0" + "unicode-match-property-value-ecmascript": "^2.1.0" }, "engines": { "node": ">=4" } }, - "node_modules/regjsgen": { - "version": "0.7.1", - "resolved": "https://registry.npmjs.org/regjsgen/-/regjsgen-0.7.1.tgz", - "integrity": "sha512-RAt+8H2ZEzHeYWxZ3H2z6tF18zyyOnlcdaafLrm21Bguj7uZy6ULibiAFdXEtKQY4Sy7wDTwDiOazasMLc4KPA==", - "dev": true - }, "node_modules/regjsparser": { "version": "0.9.1", "resolved": "https://registry.npmjs.org/regjsparser/-/regjsparser-0.9.1.tgz", @@ -7605,13 +7839,22 @@ "node": ">=0.10.0" } }, + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/resolve": { - "version": "1.22.1", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.1.tgz", - "integrity": "sha512-nBpuuYuY5jFsli/JIs1oldw6fOQCBioohqWZg/2hiaOybXOft4lonv85uDOKXdf8rhyK159cxU5cDcK/NKk8zw==", + "version": "1.22.3", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.3.tgz", + "integrity": "sha512-P8ur/gp/AmbEzjr729bZnLjXK5Z+4P0zhIJgBgzqRih7hL7BOukHGtSTA3ACMY467GRFz3duQsi0bDZdR7DKdw==", "dev": true, "dependencies": { - "is-core-module": "^2.9.0", + "is-core-module": "^2.12.0", "path-parse": "^1.0.7", "supports-preserve-symlinks-flag": "^1.0.0" }, @@ -7676,6 +7919,24 @@ "queue-microtask": "^1.2.2" } }, + "node_modules/safe-array-concat": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/safe-array-concat/-/safe-array-concat-1.0.0.tgz", + "integrity": "sha512-9dVEFruWIsnie89yym+xWTAYASdpw3CJV7Li/6zBewGf9z2i1j31rP6jnY0pHEO4QZh6N0K11bFjWmdR8UGdPQ==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "get-intrinsic": "^1.2.0", + "has-symbols": "^1.0.3", + "isarray": "^2.0.5" + }, + "engines": { + "node": ">=0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/safe-buffer": { "version": "5.1.2", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", @@ -7714,23 +7975,58 @@ } }, "node_modules/schema-utils": { - "version": "2.7.1", - "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-2.7.1.tgz", - "integrity": "sha512-SHiNtMOUGWBQJwzISiVYKu82GiV4QYGePp3odlY1tuKO7gPtphAT5R/py0fA6xtbgLL/RvtJZnU9b8s0F1q0Xg==", + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.2.0.tgz", + "integrity": "sha512-L0jRsrPpjdckP3oPug3/VxNKt2trR8TcabrM6FOAAlvC/9Phcmm+cuAgTlxBqdBR1WJx7Naj9WHw+aOmheSVbw==", "dev": true, "dependencies": { - "@types/json-schema": "^7.0.5", - "ajv": "^6.12.4", - "ajv-keywords": "^3.5.2" + "@types/json-schema": "^7.0.9", + "ajv": "^8.9.0", + "ajv-formats": "^2.1.1", + "ajv-keywords": "^5.1.0" }, "engines": { - "node": ">= 8.9.0" + "node": ">= 12.13.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/webpack" } }, + "node_modules/schema-utils/node_modules/ajv": { + "version": "8.12.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.12.0.tgz", + "integrity": "sha512-sRu1kpcO9yLtYxBKvqfTeh9KzZEwO3STyX1HT+4CaDzC6HpTGYhIhPIzj9XuKU7KYDwnaeh5hcOwjy1QuJzBPA==", + "dev": true, + "dependencies": { + "fast-deep-equal": "^3.1.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/schema-utils/node_modules/ajv-keywords": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-5.1.0.tgz", + "integrity": "sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw==", + "dev": true, + "dependencies": { + "fast-deep-equal": "^3.1.3" + }, + "peerDependencies": { + "ajv": "^8.8.2" + } + }, + "node_modules/schema-utils/node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "dev": true + }, "node_modules/semver": { "version": "5.7.0", "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.0.tgz", @@ -7910,21 +8206,21 @@ } }, "node_modules/sift": { - "version": "16.0.0", - "resolved": "https://registry.npmjs.org/sift/-/sift-16.0.0.tgz", - "integrity": "sha512-ILTjdP2Mv9V1kIxWMXeMTIRbOBrqKc4JAXmFMnFq3fKeyQ2Qwa3Dw1ubcye3vR+Y6ofA0b9gNDr/y2t6eUeIzQ==" + "version": "16.0.1", + "resolved": "https://registry.npmjs.org/sift/-/sift-16.0.1.tgz", + "integrity": "sha512-Wv6BjQ5zbhW7VFefWusVP33T/EM0vYikCaQ2qR8yULbsilAT8/wQaXvuQ3ptGLpoKx+lihJE3y2UTgKDyyNHZQ==" }, "node_modules/sinon": { - "version": "14.0.1", - "resolved": "https://registry.npmjs.org/sinon/-/sinon-14.0.1.tgz", - "integrity": "sha512-JhJ0jCiyBWVAHDS+YSjgEbDn7Wgz9iIjA1/RK+eseJN0vAAWIWiXBdrnb92ELPyjsfreCYntD1ORtLSfIrlvSQ==", + "version": "15.2.0", + "resolved": "https://registry.npmjs.org/sinon/-/sinon-15.2.0.tgz", + "integrity": "sha512-nPS85arNqwBXaIsFCkolHjGIkFo+Oxu9vbgmBJizLAhqe6P2o3Qmj3KCUoRkfhHtvgDhZdWD3risLHAUJ8npjw==", "dev": true, "dependencies": { - "@sinonjs/commons": "^1.8.3", - "@sinonjs/fake-timers": "^9.1.2", - "@sinonjs/samsam": "^6.1.1", - "diff": "^5.0.0", - "nise": "^5.1.1", + "@sinonjs/commons": "^3.0.0", + "@sinonjs/fake-timers": "^10.3.0", + "@sinonjs/samsam": "^8.0.0", + "diff": "^5.1.0", + "nise": "^5.1.4", "supports-color": "^7.2.0" }, "funding": { @@ -7932,6 +8228,15 @@ "url": "https://opencollective.com/sinon" } }, + "node_modules/sinon/node_modules/diff": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/diff/-/diff-5.1.0.tgz", + "integrity": "sha512-D+mk+qE8VC/PAUrlAU34N+VfXev0ghe5ywmpqrawphmVZc1bEfn56uo9qpyGp1p4xpzOHkSW4ztBd6L7Xx4ACw==", + "dev": true, + "engines": { + "node": ">=0.3.1" + } + }, "node_modules/sinon/node_modules/has-flag": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", @@ -8046,29 +8351,46 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/string.prototype.trim": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/string.prototype.trim/-/string.prototype.trim-1.2.7.tgz", + "integrity": "sha512-p6TmeT1T3411M8Cgg9wBTMRtY2q9+PNy9EV1i2lIXUN/btt763oIfxwN3RR8VU6wHX8j/1CFy0L+YuThm6bgOg==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.4", + "es-abstract": "^1.20.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/string.prototype.trimend": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.5.tgz", - "integrity": "sha512-I7RGvmjV4pJ7O3kdf+LXFpVfdNOxtCW/2C8f6jNiW4+PQchwxkCDzlk1/7p+Wl4bqFIZeF47qAHXLuHHWKAxog==", + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.6.tgz", + "integrity": "sha512-JySq+4mrPf9EsDBEDYMOb/lM7XQLulwg5R/m1r0PXEFqrV0qHvl58sdTilSXtKOflCsK2E8jxf+GKC0T07RWwQ==", "dev": true, "dependencies": { "call-bind": "^1.0.2", "define-properties": "^1.1.4", - "es-abstract": "^1.19.5" + "es-abstract": "^1.20.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" } }, "node_modules/string.prototype.trimstart": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.5.tgz", - "integrity": "sha512-THx16TJCGlsN0o6dl2o6ncWUsdgnLRSA23rRE5pyGBw/mLr3Ej/R2LaqCtgP8VNMGZsvMWnf9ooZPyY2bHvUFg==", + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.6.tgz", + "integrity": "sha512-omqjMDaY92pbn5HOX7f9IccLA+U1tA9GvtU4JrodiXFfYB7jPzzHpRzpglLAjtUV6bB557zwClJezTqnAiYnQA==", "dev": true, "dependencies": { "call-bind": "^1.0.2", "define-properties": "^1.1.4", - "es-abstract": "^1.19.5" + "es-abstract": "^1.20.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" @@ -8108,22 +8430,21 @@ } }, "node_modules/superagent": { - "version": "8.0.2", - "resolved": "https://registry.npmjs.org/superagent/-/superagent-8.0.2.tgz", - "integrity": "sha512-QtYZ9uaNAMexI7XWl2vAXAh0j4q9H7T0WVEI/y5qaUB3QLwxo+voUgCQ217AokJzUTIVOp0RTo7fhZrwhD7A2Q==", - "deprecated": "Please use v8.0.0 until https://github.com/visionmedia/superagent/issues/1743 is resolved", + "version": "8.0.9", + "resolved": "https://registry.npmjs.org/superagent/-/superagent-8.0.9.tgz", + "integrity": "sha512-4C7Bh5pyHTvU33KpZgwrNKh/VQnvgtCSqPRfJAUdmrtSYePVzVg4E4OzsrbkhJj9O7SO6Bnv75K/F8XVZT8YHA==", "dev": true, "dependencies": { "component-emitter": "^1.3.0", - "cookiejar": "^2.1.3", + "cookiejar": "^2.1.4", "debug": "^4.3.4", "fast-safe-stringify": "^2.1.1", "form-data": "^4.0.0", - "formidable": "^2.0.1", + "formidable": "^2.1.2", "methods": "^1.1.2", "mime": "2.6.0", "qs": "^6.11.0", - "semver": "^7.3.7" + "semver": "^7.3.8" }, "engines": { "node": ">=6.4.0 <13 || >=14" @@ -8146,6 +8467,18 @@ } } }, + "node_modules/superagent/node_modules/lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "dev": true, + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/superagent/node_modules/mime": { "version": "2.6.0", "resolved": "https://registry.npmjs.org/mime/-/mime-2.6.0.tgz", @@ -8164,25 +8497,10 @@ "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", "dev": true }, - "node_modules/superagent/node_modules/qs": { - "version": "6.11.0", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.11.0.tgz", - "integrity": "sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==", - "dev": true, - "dependencies": { - "side-channel": "^1.0.4" - }, - "engines": { - "node": ">=0.6" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/superagent/node_modules/semver": { - "version": "7.3.8", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.8.tgz", - "integrity": "sha512-NB1ctGL5rlHrPJtFDVIVzTyQylMLu9N9VICA6HSFJo8MCGVTMW6gfpicwKmmK/dAjTOrqu5l63JJOpDSrAis3A==", + "version": "7.5.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", + "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", "dev": true, "dependencies": { "lru-cache": "^6.0.0" @@ -8194,14 +8512,20 @@ "node": ">=10" } }, + "node_modules/superagent/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true + }, "node_modules/supertest": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/supertest/-/supertest-6.3.0.tgz", - "integrity": "sha512-QgWju1cNoacP81Rv88NKkQ4oXTzGg0eNZtOoxp1ROpbS4OHY/eK5b8meShuFtdni161o5X0VQvgo7ErVyKK+Ow==", + "version": "6.3.3", + "resolved": "https://registry.npmjs.org/supertest/-/supertest-6.3.3.tgz", + "integrity": "sha512-EMCG6G8gDu5qEqRQ3JjjPs6+FYT1a7Hv5ApHvtSghmOFJYtsU5S+pSb6Y2EUeCEY3CmEL3mmQ8YWlPOzQomabA==", "dev": true, "dependencies": { "methods": "^1.1.2", - "superagent": "^8.0.0" + "superagent": "^8.0.5" }, "engines": { "node": ">=6.4.0" @@ -8345,14 +8669,6 @@ "node": ">=12" } }, - "node_modules/tr46/node_modules/punycode": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz", - "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==", - "engines": { - "node": ">=6" - } - }, "node_modules/trim-right": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/trim-right/-/trim-right-1.0.1.tgz", @@ -8363,21 +8679,21 @@ } }, "node_modules/tsconfig-paths": { - "version": "3.14.1", - "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.14.1.tgz", - "integrity": "sha512-fxDhWnFSLt3VuTwtvJt5fpwxBHg5AdKWMsgcPOOIilyjymcYVZoCQF8fvFRezCNfblEXmi+PcM1eYHeOAgXCOQ==", + "version": "3.14.2", + "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.14.2.tgz", + "integrity": "sha512-o/9iXgCYc5L/JxCHPe3Hvh8Q/2xm5Z+p18PESBU6Ff33695QnCHBEjcytY2q19ua7Mbl/DavtBOLq+oG0RCL+g==", "dev": true, "dependencies": { "@types/json5": "^0.0.29", - "json5": "^1.0.1", + "json5": "^1.0.2", "minimist": "^1.2.6", "strip-bom": "^3.0.0" } }, "node_modules/tsconfig-paths/node_modules/json5": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.1.tgz", - "integrity": "sha512-aKS4WQjPenRxiQsC93MNfjx+nbF4PAdYzmd/1JIj8HYzqfbu86beTuNgXDzPknWk0n0uARlyewZo4s++ES36Ow==", + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.2.tgz", + "integrity": "sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==", "dev": true, "dependencies": { "minimist": "^1.2.0" @@ -8431,6 +8747,71 @@ "node": ">= 0.6" } }, + "node_modules/typed-array-buffer": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.0.tgz", + "integrity": "sha512-Y8KTSIglk9OZEr8zywiIHG/kmQ7KWyjseXs1CbSo8vC42w7hg2HgYTxSWwP0+is7bWDc1H+Fo026CpHFwm8tkw==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "get-intrinsic": "^1.2.1", + "is-typed-array": "^1.1.10" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/typed-array-byte-length": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/typed-array-byte-length/-/typed-array-byte-length-1.0.0.tgz", + "integrity": "sha512-Or/+kvLxNpeQ9DtSydonMxCx+9ZXOswtwJn17SNLvhptaXYDJvkFFP5zbfU/uLmvnBJlI4yrnXRxpdWH/M5tNA==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "for-each": "^0.3.3", + "has-proto": "^1.0.1", + "is-typed-array": "^1.1.10" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/typed-array-byte-offset": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/typed-array-byte-offset/-/typed-array-byte-offset-1.0.0.tgz", + "integrity": "sha512-RD97prjEt9EL8YgAgpOkf3O4IF9lhJFr9g0htQkm0rchFp/Vx7LW5Q8fSXXub7BXAODyUQohRMyOc3faCPd0hg==", + "dev": true, + "dependencies": { + "available-typed-arrays": "^1.0.5", + "call-bind": "^1.0.2", + "for-each": "^0.3.3", + "has-proto": "^1.0.1", + "is-typed-array": "^1.1.10" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/typed-array-length": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/typed-array-length/-/typed-array-length-1.0.4.tgz", + "integrity": "sha512-KjZypGq+I/H7HI5HlOoGHkWUUGq+Q0TPhQurLbyrVrvnKTBgzLhIJ7j6J/XTQOi0d1RjyZ0wdas8bKs2p0x3Ng==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "for-each": "^0.3.3", + "is-typed-array": "^1.1.9" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/uglify-js": { "version": "3.6.0", "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.6.0.tgz", @@ -8496,9 +8877,9 @@ } }, "node_modules/unicode-match-property-value-ecmascript": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/unicode-match-property-value-ecmascript/-/unicode-match-property-value-ecmascript-2.0.0.tgz", - "integrity": "sha512-7Yhkc0Ye+t4PNYzOGKedDhXbYIBe1XEQYQxOPyhcXNMJ0WCABqqj6ckydd6pWRZTHV4GuCPKdBAUiMc60tsKVw==", + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/unicode-match-property-value-ecmascript/-/unicode-match-property-value-ecmascript-2.1.0.tgz", + "integrity": "sha512-qxkjQt6qjg/mYscYMC0XKRn3Rh0wFPlfxB0xkt9CfyTvpX1Ra0+rAmdX2QyAobptSEvuy4RtpPRui6XkV+8wjA==", "dev": true, "engines": { "node": ">=4" @@ -8522,9 +8903,9 @@ } }, "node_modules/update-browserslist-db": { - "version": "1.0.10", - "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.10.tgz", - "integrity": "sha512-OztqDenkfFkbSG+tRxBeAnCVPckDBcvibKd35yDONx6OU8N7sqgwc7rCbkJ/WcYtVRZ4ba68d6byhC21GFh7sQ==", + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.11.tgz", + "integrity": "sha512-dCwEFf0/oT85M1fHBg4F0jtLwJrutGoHSQXCh7u4o2t1drG+c0a9Flnqww6XUKSfQMPpJBRjU8d4RXB09qtvaA==", "dev": true, "funding": [ { @@ -8534,6 +8915,10 @@ { "type": "tidelift", "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" } ], "dependencies": { @@ -8541,7 +8926,7 @@ "picocolors": "^1.0.0" }, "bin": { - "browserslist-lint": "cli.js" + "update-browserslist-db": "cli.js" }, "peerDependencies": { "browserslist": ">= 4.21.0" @@ -8556,15 +8941,6 @@ "punycode": "^2.1.0" } }, - "node_modules/uri-js/node_modules/punycode": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz", - "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==", - "dev": true, - "engines": { - "node": ">=6" - } - }, "node_modules/utils-merge": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", @@ -8752,13 +9128,23 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/word-wrap": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.3.tgz", - "integrity": "sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ==", + "node_modules/which-typed-array": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.11.tgz", + "integrity": "sha512-qe9UWWpkeG5yzZ0tNYxDmd7vo58HDBc39mZ0xWWpolAGADdFOzkfamWLDxkOWcvHQKVmdTyQdLD4NOfjLWTKew==", "dev": true, + "dependencies": { + "available-typed-arrays": "^1.0.5", + "call-bind": "^1.0.2", + "for-each": "^0.3.3", + "gopd": "^1.0.1", + "has-tostringtag": "^1.0.0" + }, "engines": { - "node": ">=0.10.0" + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, "node_modules/wordwrap": { @@ -8780,9 +9166,9 @@ "dev": true }, "node_modules/yallist": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", "dev": true }, "node_modules/yargs-unparser": { @@ -8838,6 +9224,12 @@ } }, "dependencies": { + "@aashutoshrathi/word-wrap": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/@aashutoshrathi/word-wrap/-/word-wrap-1.2.6.tgz", + "integrity": "sha512-1Yjs2SvM8TflER/OD3cOjhWWOZb58A2t7wpE2S9XfBYTiIl+XFhQG2bjy4Pu1I+EAlCNUzRDYDdFwFYUKvXcIA==", + "dev": true + }, "@ampproject/remapping": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.2.0.tgz", @@ -8849,12 +9241,12 @@ } }, "@babel/cli": { - "version": "7.19.3", - "resolved": "https://registry.npmjs.org/@babel/cli/-/cli-7.19.3.tgz", - "integrity": "sha512-643/TybmaCAe101m2tSVHi9UKpETXP9c/Ff4mD2tAwkdP6esKIfaauZFc67vGEM6r9fekbEGid+sZhbEnSe3dg==", + "version": "7.22.9", + "resolved": "https://registry.npmjs.org/@babel/cli/-/cli-7.22.9.tgz", + "integrity": "sha512-nb2O7AThqRo7/E53EGiuAkMaRbb7J5Qp3RvN+dmua1U+kydm0oznkhqbTEG15yk26G/C3yL6OdZjzgl+DMXVVA==", "dev": true, "requires": { - "@jridgewell/trace-mapping": "^0.3.8", + "@jridgewell/trace-mapping": "^0.3.17", "@nicolo-ribaudo/chokidar-2": "2.1.8-no-fsevents.3", "chokidar": "^3.4.0", "commander": "^4.0.1", @@ -8896,41 +9288,41 @@ } }, "@babel/code-frame": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.18.6.tgz", - "integrity": "sha512-TDCmlK5eOvH+eH7cdAFlNXeVJqWIQ7gW9tY1GJIpUtFb6CmjVyq2VM3u71bOyR8CRihcCgMUYoDNyLXao3+70Q==", + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.22.5.tgz", + "integrity": "sha512-Xmwn266vad+6DAqEB2A6V/CcZVp62BbwVmcOJc2RPuwih1kw02TjQvWVWlcKGbBPd+8/0V5DEkOcizRGYsspYQ==", "dev": true, "requires": { - "@babel/highlight": "^7.18.6" + "@babel/highlight": "^7.22.5" } }, "@babel/compat-data": { - "version": "7.19.3", - "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.19.3.tgz", - "integrity": "sha512-prBHMK4JYYK+wDjJF1q99KK4JLL+egWS4nmNqdlMUgCExMZ+iZW0hGhyC3VEbsPjvaN0TBhW//VIFwBrk8sEiw==", + "version": "7.22.9", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.22.9.tgz", + "integrity": "sha512-5UamI7xkUcJ3i9qVDS+KFDEK8/7oJ55/sJMB1Ge7IEapr7KfdfV/HErR+koZwOfd+SgtFKOKRhRakdg++DcJpQ==", "dev": true }, "@babel/core": { - "version": "7.19.3", - "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.19.3.tgz", - "integrity": "sha512-WneDJxdsjEvyKtXKsaBGbDeiyOjR5vYq4HcShxnIbG0qixpoHjI3MqeZM9NDvsojNCEBItQE4juOo/bU6e72gQ==", - "dev": true, - "requires": { - "@ampproject/remapping": "^2.1.0", - "@babel/code-frame": "^7.18.6", - "@babel/generator": "^7.19.3", - "@babel/helper-compilation-targets": "^7.19.3", - "@babel/helper-module-transforms": "^7.19.0", - "@babel/helpers": "^7.19.0", - "@babel/parser": "^7.19.3", - "@babel/template": "^7.18.10", - "@babel/traverse": "^7.19.3", - "@babel/types": "^7.19.3", + "version": "7.22.9", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.22.9.tgz", + "integrity": "sha512-G2EgeufBcYw27U4hhoIwFcgc1XU7TlXJ3mv04oOv1WCuo900U/anZSPzEqNjwdjgffkk2Gs0AN0dW1CKVLcG7w==", + "dev": true, + "requires": { + "@ampproject/remapping": "^2.2.0", + "@babel/code-frame": "^7.22.5", + "@babel/generator": "^7.22.9", + "@babel/helper-compilation-targets": "^7.22.9", + "@babel/helper-module-transforms": "^7.22.9", + "@babel/helpers": "^7.22.6", + "@babel/parser": "^7.22.7", + "@babel/template": "^7.22.5", + "@babel/traverse": "^7.22.8", + "@babel/types": "^7.22.5", "convert-source-map": "^1.7.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", - "json5": "^2.2.1", - "semver": "^6.3.0" + "json5": "^2.2.2", + "semver": "^6.3.1" }, "dependencies": { "debug": { @@ -8942,12 +9334,6 @@ "ms": "2.1.2" } }, - "json5": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.1.tgz", - "integrity": "sha512-1hqLFMSrGHRHxav9q9gNjJ5EXznIxGVO09xQRrwplcS8qs28pZ8s8hupZAmqDwZUmVZ2Qb2jnyPOWcDH8m8dlA==", - "dev": true - }, "ms": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", @@ -8955,22 +9341,22 @@ "dev": true }, "semver": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", - "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", "dev": true } } }, "@babel/eslint-parser": { - "version": "7.19.1", - "resolved": "https://registry.npmjs.org/@babel/eslint-parser/-/eslint-parser-7.19.1.tgz", - "integrity": "sha512-AqNf2QWt1rtu2/1rLswy6CDP7H9Oh3mMhk177Y67Rg8d7RD9WfOLLv8CGn6tisFvS2htm86yIe1yLF6I1UDaGQ==", + "version": "7.22.9", + "resolved": "https://registry.npmjs.org/@babel/eslint-parser/-/eslint-parser-7.22.9.tgz", + "integrity": "sha512-xdMkt39/nviO/4vpVdrEYPwXCsYIXSSAr6mC7WQsNIlGnuxKyKE7GZjalcnbSWiC4OXGNNN3UQPeHfjSC6sTDA==", "dev": true, "requires": { "@nicolo-ribaudo/eslint-scope-5-internals": "5.1.1-v1", "eslint-visitor-keys": "^2.1.0", - "semver": "^6.3.0" + "semver": "^6.3.1" }, "dependencies": { "eslint-visitor-keys": { @@ -8980,21 +9366,22 @@ "dev": true }, "semver": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", - "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", "dev": true } } }, "@babel/generator": { - "version": "7.19.3", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.19.3.tgz", - "integrity": "sha512-fqVZnmp1ncvZU757UzDheKZpfPgatqY59XtW2/j/18H7u76akb8xqvjw82f+i2UKd/ksYsSick/BCLQUUtJ/qQ==", + "version": "7.22.9", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.22.9.tgz", + "integrity": "sha512-KtLMbmicyuK2Ak/FTCJVbDnkN1SlT8/kceFTiuDiiRUUSMnHMidxSCdG4ndkTOHHpoomWe/4xkvHkEOncwjYIw==", "dev": true, "requires": { - "@babel/types": "^7.19.3", + "@babel/types": "^7.22.5", "@jridgewell/gen-mapping": "^0.3.2", + "@jridgewell/trace-mapping": "^0.3.17", "jsesc": "^2.5.1" }, "dependencies": { @@ -9018,81 +9405,99 @@ } }, "@babel/helper-annotate-as-pure": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.18.6.tgz", - "integrity": "sha512-duORpUiYrEpzKIop6iNbjnwKLAKnJ47csTyRACyEmWj0QdUrm5aqNJGHSSEQSUAvNW0ojX0dOmK9dZduvkfeXA==", + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.22.5.tgz", + "integrity": "sha512-LvBTxu8bQSQkcyKOU+a1btnNFQ1dMAd0R6PyW3arXes06F6QLWLIrd681bxRPIXlrMGR3XYnW9JyML7dP3qgxg==", "dev": true, "requires": { - "@babel/types": "^7.18.6" + "@babel/types": "^7.22.5" } }, "@babel/helper-builder-binary-assignment-operator-visitor": { - "version": "7.18.9", - "resolved": "https://registry.npmjs.org/@babel/helper-builder-binary-assignment-operator-visitor/-/helper-builder-binary-assignment-operator-visitor-7.18.9.tgz", - "integrity": "sha512-yFQ0YCHoIqarl8BCRwBL8ulYUaZpz3bNsA7oFepAzee+8/+ImtADXNOmO5vJvsPff3qi+hvpkY/NYBTrBQgdNw==", + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-builder-binary-assignment-operator-visitor/-/helper-builder-binary-assignment-operator-visitor-7.22.5.tgz", + "integrity": "sha512-m1EP3lVOPptR+2DwD125gziZNcmoNSHGmJROKoy87loWUQyJaVXDgpmruWqDARZSmtYQ+Dl25okU8+qhVzuykw==", "dev": true, "requires": { - "@babel/helper-explode-assignable-expression": "^7.18.6", - "@babel/types": "^7.18.9" + "@babel/types": "^7.22.5" } }, "@babel/helper-compilation-targets": { - "version": "7.19.3", - "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.19.3.tgz", - "integrity": "sha512-65ESqLGyGmLvgR0mst5AdW1FkNlj9rQsCKduzEoEPhBCDFGXvz2jW6bXFG6i0/MrV2s7hhXjjb2yAzcPuQlLwg==", + "version": "7.22.9", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.22.9.tgz", + "integrity": "sha512-7qYrNM6HjpnPHJbopxmb8hSPoZ0gsX8IvUS32JGVoy+pU9e5N0nLr1VjJoR6kA4d9dmGLxNYOjeB8sUDal2WMw==", "dev": true, "requires": { - "@babel/compat-data": "^7.19.3", - "@babel/helper-validator-option": "^7.18.6", - "browserslist": "^4.21.3", - "semver": "^6.3.0" + "@babel/compat-data": "^7.22.9", + "@babel/helper-validator-option": "^7.22.5", + "browserslist": "^4.21.9", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" }, "dependencies": { "semver": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", - "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", "dev": true } } }, "@babel/helper-create-class-features-plugin": { - "version": "7.19.0", - "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.19.0.tgz", - "integrity": "sha512-NRz8DwF4jT3UfrmUoZjd0Uph9HQnP30t7Ash+weACcyNkiYTywpIjDBgReJMKgr+n86sn2nPVVmJ28Dm053Kqw==", + "version": "7.22.9", + "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.22.9.tgz", + "integrity": "sha512-Pwyi89uO4YrGKxL/eNJ8lfEH55DnRloGPOseaA8NFNL6jAUnn+KccaISiFazCj5IolPPDjGSdzQzXVzODVRqUQ==", "dev": true, "requires": { - "@babel/helper-annotate-as-pure": "^7.18.6", - "@babel/helper-environment-visitor": "^7.18.9", - "@babel/helper-function-name": "^7.19.0", - "@babel/helper-member-expression-to-functions": "^7.18.9", - "@babel/helper-optimise-call-expression": "^7.18.6", - "@babel/helper-replace-supers": "^7.18.9", - "@babel/helper-split-export-declaration": "^7.18.6" + "@babel/helper-annotate-as-pure": "^7.22.5", + "@babel/helper-environment-visitor": "^7.22.5", + "@babel/helper-function-name": "^7.22.5", + "@babel/helper-member-expression-to-functions": "^7.22.5", + "@babel/helper-optimise-call-expression": "^7.22.5", + "@babel/helper-replace-supers": "^7.22.9", + "@babel/helper-skip-transparent-expression-wrappers": "^7.22.5", + "@babel/helper-split-export-declaration": "^7.22.6", + "semver": "^6.3.1" + }, + "dependencies": { + "semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true + } } }, "@babel/helper-create-regexp-features-plugin": { - "version": "7.19.0", - "resolved": "https://registry.npmjs.org/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.19.0.tgz", - "integrity": "sha512-htnV+mHX32DF81amCDrwIDr8nrp1PTm+3wfBN9/v8QJOLEioOCOG7qNyq0nHeFiWbT3Eb7gsPwEmV64UCQ1jzw==", + "version": "7.22.9", + "resolved": "https://registry.npmjs.org/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.22.9.tgz", + "integrity": "sha512-+svjVa/tFwsNSG4NEy1h85+HQ5imbT92Q5/bgtS7P0GTQlP8WuFdqsiABmQouhiFGyV66oGxZFpeYHza1rNsKw==", "dev": true, "requires": { - "@babel/helper-annotate-as-pure": "^7.18.6", - "regexpu-core": "^5.1.0" + "@babel/helper-annotate-as-pure": "^7.22.5", + "regexpu-core": "^5.3.1", + "semver": "^6.3.1" + }, + "dependencies": { + "semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true + } } }, "@babel/helper-define-polyfill-provider": { - "version": "0.3.3", - "resolved": "https://registry.npmjs.org/@babel/helper-define-polyfill-provider/-/helper-define-polyfill-provider-0.3.3.tgz", - "integrity": "sha512-z5aQKU4IzbqCC1XH0nAqfsFLMVSo22SBKUc0BxGrLkolTdPTructy0ToNnlO2zA4j9Q/7pjMZf0DSY+DSTYzww==", + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/@babel/helper-define-polyfill-provider/-/helper-define-polyfill-provider-0.4.2.tgz", + "integrity": "sha512-k0qnnOqHn5dK9pZpfD5XXZ9SojAITdCKRn2Lp6rnDGzIbaP0rHyMPk/4wsSxVBVz4RfN0q6VpXWP2pDGIoQ7hw==", "dev": true, "requires": { - "@babel/helper-compilation-targets": "^7.17.7", - "@babel/helper-plugin-utils": "^7.16.7", + "@babel/helper-compilation-targets": "^7.22.6", + "@babel/helper-plugin-utils": "^7.22.5", "debug": "^4.1.1", "lodash.debounce": "^4.0.8", - "resolve": "^1.14.2", - "semver": "^6.1.2" + "resolve": "^1.14.2" }, "dependencies": { "debug": { @@ -9109,198 +9514,176 @@ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", "dev": true - }, - "semver": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", - "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", - "dev": true - } - } - }, - "@babel/helper-environment-visitor": { - "version": "7.18.9", - "resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.18.9.tgz", - "integrity": "sha512-3r/aACDJ3fhQ/EVgFy0hpj8oHyHpQc+LPtJoY9SzTThAsStm4Ptegq92vqKoE3vD706ZVFWITnMnxucw+S9Ipg==", - "dev": true - }, - "@babel/helper-explode-assignable-expression": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/helper-explode-assignable-expression/-/helper-explode-assignable-expression-7.18.6.tgz", - "integrity": "sha512-eyAYAsQmB80jNfg4baAtLeWAQHfHFiR483rzFK+BhETlGZaQC9bsfrugfXDCbRHLQbIA7U5NxhhOxN7p/dWIcg==", - "dev": true, - "requires": { - "@babel/types": "^7.18.6" + } } }, + "@babel/helper-environment-visitor": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.22.5.tgz", + "integrity": "sha512-XGmhECfVA/5sAt+H+xpSg0mfrHq6FzNr9Oxh7PSEBBRUb/mL7Kz3NICXb194rCqAEdxkhPT1a88teizAFyvk8Q==", + "dev": true + }, "@babel/helper-function-name": { - "version": "7.19.0", - "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.19.0.tgz", - "integrity": "sha512-WAwHBINyrpqywkUH0nTnNgI5ina5TFn85HKS0pbPDfxFfhyR/aNQEn4hGi1P1JyT//I0t4OgXUlofzWILRvS5w==", + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.22.5.tgz", + "integrity": "sha512-wtHSq6jMRE3uF2otvfuD3DIvVhOsSNshQl0Qrd7qC9oQJzHvOL4qQXlQn2916+CXGywIjpGuIkoyZRRxHPiNQQ==", "dev": true, "requires": { - "@babel/template": "^7.18.10", - "@babel/types": "^7.19.0" + "@babel/template": "^7.22.5", + "@babel/types": "^7.22.5" } }, "@babel/helper-hoist-variables": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.18.6.tgz", - "integrity": "sha512-UlJQPkFqFULIcyW5sbzgbkxn2FKRgwWiRexcuaR8RNJRy8+LLveqPjwZV/bwrLZCN0eUHD/x8D0heK1ozuoo6Q==", + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.22.5.tgz", + "integrity": "sha512-wGjk9QZVzvknA6yKIUURb8zY3grXCcOZt+/7Wcy8O2uctxhplmUPkOdlgoNhmdVee2c92JXbf1xpMtVNbfoxRw==", "dev": true, "requires": { - "@babel/types": "^7.18.6" + "@babel/types": "^7.22.5" } }, "@babel/helper-member-expression-to-functions": { - "version": "7.18.9", - "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.18.9.tgz", - "integrity": "sha512-RxifAh2ZoVU67PyKIO4AMi1wTenGfMR/O/ae0CCRqwgBAt5v7xjdtRw7UoSbsreKrQn5t7r89eruK/9JjYHuDg==", + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.22.5.tgz", + "integrity": "sha512-aBiH1NKMG0H2cGZqspNvsaBe6wNGjbJjuLy29aU+eDZjSbbN53BaxlpB02xm9v34pLTZ1nIQPFYn2qMZoa5BQQ==", "dev": true, "requires": { - "@babel/types": "^7.18.9" + "@babel/types": "^7.22.5" } }, "@babel/helper-module-imports": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.18.6.tgz", - "integrity": "sha512-0NFvs3VkuSYbFi1x2Vd6tKrywq+z/cLeYC/RJNFrIX/30Bf5aiGYbtvGXolEktzJH8o5E5KJ3tT+nkxuuZFVlA==", + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.22.5.tgz", + "integrity": "sha512-8Dl6+HD/cKifutF5qGd/8ZJi84QeAKh+CEe1sBzz8UayBBGg1dAIJrdHOcOM5b2MpzWL2yuotJTtGjETq0qjXg==", "dev": true, "requires": { - "@babel/types": "^7.18.6" + "@babel/types": "^7.22.5" } }, "@babel/helper-module-transforms": { - "version": "7.19.0", - "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.19.0.tgz", - "integrity": "sha512-3HBZ377Fe14RbLIA+ac3sY4PTgpxHVkFrESaWhoI5PuyXPBBX8+C34qblV9G89ZtycGJCmCI/Ut+VUDK4bltNQ==", + "version": "7.22.9", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.22.9.tgz", + "integrity": "sha512-t+WA2Xn5K+rTeGtC8jCsdAH52bjggG5TKRuRrAGNM/mjIbO4GxvlLMFOEz9wXY5I2XQ60PMFsAG2WIcG82dQMQ==", "dev": true, "requires": { - "@babel/helper-environment-visitor": "^7.18.9", - "@babel/helper-module-imports": "^7.18.6", - "@babel/helper-simple-access": "^7.18.6", - "@babel/helper-split-export-declaration": "^7.18.6", - "@babel/helper-validator-identifier": "^7.18.6", - "@babel/template": "^7.18.10", - "@babel/traverse": "^7.19.0", - "@babel/types": "^7.19.0" + "@babel/helper-environment-visitor": "^7.22.5", + "@babel/helper-module-imports": "^7.22.5", + "@babel/helper-simple-access": "^7.22.5", + "@babel/helper-split-export-declaration": "^7.22.6", + "@babel/helper-validator-identifier": "^7.22.5" } }, "@babel/helper-optimise-call-expression": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.18.6.tgz", - "integrity": "sha512-HP59oD9/fEHQkdcbgFCnbmgH5vIQTJbxh2yf+CdM89/glUNnuzr87Q8GIjGEnOktTROemO0Pe0iPAYbqZuOUiA==", + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.22.5.tgz", + "integrity": "sha512-HBwaojN0xFRx4yIvpwGqxiV2tUfl7401jlok564NgB9EHS1y6QT17FmKWm4ztqjeVdXLuC4fSvHc5ePpQjoTbw==", "dev": true, "requires": { - "@babel/types": "^7.18.6" + "@babel/types": "^7.22.5" } }, "@babel/helper-plugin-utils": { - "version": "7.19.0", - "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.19.0.tgz", - "integrity": "sha512-40Ryx7I8mT+0gaNxm8JGTZFUITNqdLAgdg0hXzeVZxVD6nFsdhQvip6v8dqkRHzsz1VFpFAaOCHNn0vKBL7Czw==", + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.22.5.tgz", + "integrity": "sha512-uLls06UVKgFG9QD4OeFYLEGteMIAa5kpTPcFL28yuCIIzsf6ZyKZMllKVOCZFhiZ5ptnwX4mtKdWCBE/uT4amg==", "dev": true }, "@babel/helper-remap-async-to-generator": { - "version": "7.18.9", - "resolved": "https://registry.npmjs.org/@babel/helper-remap-async-to-generator/-/helper-remap-async-to-generator-7.18.9.tgz", - "integrity": "sha512-dI7q50YKd8BAv3VEfgg7PS7yD3Rtbi2J1XMXaalXO0W0164hYLnh8zpjRS0mte9MfVp/tltvr/cfdXPvJr1opA==", + "version": "7.22.9", + "resolved": "https://registry.npmjs.org/@babel/helper-remap-async-to-generator/-/helper-remap-async-to-generator-7.22.9.tgz", + "integrity": "sha512-8WWC4oR4Px+tr+Fp0X3RHDVfINGpF3ad1HIbrc8A77epiR6eMMc6jsgozkzT2uDiOOdoS9cLIQ+XD2XvI2WSmQ==", "dev": true, "requires": { - "@babel/helper-annotate-as-pure": "^7.18.6", - "@babel/helper-environment-visitor": "^7.18.9", - "@babel/helper-wrap-function": "^7.18.9", - "@babel/types": "^7.18.9" + "@babel/helper-annotate-as-pure": "^7.22.5", + "@babel/helper-environment-visitor": "^7.22.5", + "@babel/helper-wrap-function": "^7.22.9" } }, "@babel/helper-replace-supers": { - "version": "7.19.1", - "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.19.1.tgz", - "integrity": "sha512-T7ahH7wV0Hfs46SFh5Jz3s0B6+o8g3c+7TMxu7xKfmHikg7EAZ3I2Qk9LFhjxXq8sL7UkP5JflezNwoZa8WvWw==", + "version": "7.22.9", + "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.22.9.tgz", + "integrity": "sha512-LJIKvvpgPOPUThdYqcX6IXRuIcTkcAub0IaDRGCZH0p5GPUp7PhRU9QVgFcDDd51BaPkk77ZjqFwh6DZTAEmGg==", "dev": true, "requires": { - "@babel/helper-environment-visitor": "^7.18.9", - "@babel/helper-member-expression-to-functions": "^7.18.9", - "@babel/helper-optimise-call-expression": "^7.18.6", - "@babel/traverse": "^7.19.1", - "@babel/types": "^7.19.0" + "@babel/helper-environment-visitor": "^7.22.5", + "@babel/helper-member-expression-to-functions": "^7.22.5", + "@babel/helper-optimise-call-expression": "^7.22.5" } }, "@babel/helper-simple-access": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.18.6.tgz", - "integrity": "sha512-iNpIgTgyAvDQpDj76POqg+YEt8fPxx3yaNBg3S30dxNKm2SWfYhD0TGrK/Eu9wHpUW63VQU894TsTg+GLbUa1g==", + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.22.5.tgz", + "integrity": "sha512-n0H99E/K+Bika3++WNL17POvo4rKWZ7lZEp1Q+fStVbUi8nxPQEBOlTmCOxW/0JsS56SKKQ+ojAe2pHKJHN35w==", "dev": true, "requires": { - "@babel/types": "^7.18.6" + "@babel/types": "^7.22.5" } }, "@babel/helper-skip-transparent-expression-wrappers": { - "version": "7.18.9", - "resolved": "https://registry.npmjs.org/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.18.9.tgz", - "integrity": "sha512-imytd2gHi3cJPsybLRbmFrF7u5BIEuI2cNheyKi3/iOBC63kNn3q8Crn2xVuESli0aM4KYsyEqKyS7lFL8YVtw==", + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.22.5.tgz", + "integrity": "sha512-tK14r66JZKiC43p8Ki33yLBVJKlQDFoA8GYN67lWCDCqoL6EMMSuM9b+Iff2jHaM/RRFYl7K+iiru7hbRqNx8Q==", "dev": true, "requires": { - "@babel/types": "^7.18.9" + "@babel/types": "^7.22.5" } }, "@babel/helper-split-export-declaration": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.18.6.tgz", - "integrity": "sha512-bde1etTx6ZyTmobl9LLMMQsaizFVZrquTEHOqKeQESMKo4PlObf+8+JA25ZsIpZhT/WEd39+vOdLXAFG/nELpA==", + "version": "7.22.6", + "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.22.6.tgz", + "integrity": "sha512-AsUnxuLhRYsisFiaJwvp1QF+I3KjD5FOxut14q/GzovUe6orHLesW2C7d754kRm53h5gqrz6sFl6sxc4BVtE/g==", "dev": true, "requires": { - "@babel/types": "^7.18.6" + "@babel/types": "^7.22.5" } }, "@babel/helper-string-parser": { - "version": "7.18.10", - "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.18.10.tgz", - "integrity": "sha512-XtIfWmeNY3i4t7t4D2t02q50HvqHybPqW2ki1kosnvWCwuCMeo81Jf0gwr85jy/neUdg5XDdeFE/80DXiO+njw==", + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.22.5.tgz", + "integrity": "sha512-mM4COjgZox8U+JcXQwPijIZLElkgEpO5rsERVDJTc2qfCDfERyob6k5WegS14SX18IIjv+XD+GrqNumY5JRCDw==", "dev": true }, "@babel/helper-validator-identifier": { - "version": "7.19.1", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.19.1.tgz", - "integrity": "sha512-awrNfaMtnHUr653GgGEs++LlAvW6w+DcPrOliSMXWCKo597CwL5Acf/wWdNkf/tfEQE3mjkeD1YOVZOUV/od1w==", + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.5.tgz", + "integrity": "sha512-aJXu+6lErq8ltp+JhkJUfk1MTGyuA4v7f3pA+BJ5HLfNC6nAQ0Cpi9uOquUj8Hehg0aUiHzWQbOVJGao6ztBAQ==", "dev": true }, "@babel/helper-validator-option": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.18.6.tgz", - "integrity": "sha512-XO7gESt5ouv/LRJdrVjkShckw6STTaB7l9BrpBaAHDeF5YZT+01PCwmR0SJHnkW6i8OwW/EVWRShfi4j2x+KQw==", + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.22.5.tgz", + "integrity": "sha512-R3oB6xlIVKUnxNUxbmgq7pKjxpru24zlimpE8WK47fACIlM0II/Hm1RS8IaOI7NgCr6LNS+jl5l75m20npAziw==", "dev": true }, "@babel/helper-wrap-function": { - "version": "7.19.0", - "resolved": "https://registry.npmjs.org/@babel/helper-wrap-function/-/helper-wrap-function-7.19.0.tgz", - "integrity": "sha512-txX8aN8CZyYGTwcLhlk87KRqncAzhh5TpQamZUa0/u3an36NtDpUP6bQgBCBcLeBs09R/OwQu3OjK0k/HwfNDg==", + "version": "7.22.9", + "resolved": "https://registry.npmjs.org/@babel/helper-wrap-function/-/helper-wrap-function-7.22.9.tgz", + "integrity": "sha512-sZ+QzfauuUEfxSEjKFmi3qDSHgLsTPK/pEpoD/qonZKOtTPTLbf59oabPQ4rKekt9lFcj/hTZaOhWwFYrgjk+Q==", "dev": true, "requires": { - "@babel/helper-function-name": "^7.19.0", - "@babel/template": "^7.18.10", - "@babel/traverse": "^7.19.0", - "@babel/types": "^7.19.0" + "@babel/helper-function-name": "^7.22.5", + "@babel/template": "^7.22.5", + "@babel/types": "^7.22.5" } }, "@babel/helpers": { - "version": "7.19.0", - "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.19.0.tgz", - "integrity": "sha512-DRBCKGwIEdqY3+rPJgG/dKfQy9+08rHIAJx8q2p+HSWP87s2HCrQmaAMMyMll2kIXKCW0cO1RdQskx15Xakftg==", + "version": "7.22.6", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.22.6.tgz", + "integrity": "sha512-YjDs6y/fVOYFV8hAf1rxd1QvR9wJe1pDBZ2AREKq/SDayfPzgk0PBnVuTCE5X1acEpMMNOVUqoe+OwiZGJ+OaA==", "dev": true, "requires": { - "@babel/template": "^7.18.10", - "@babel/traverse": "^7.19.0", - "@babel/types": "^7.19.0" + "@babel/template": "^7.22.5", + "@babel/traverse": "^7.22.6", + "@babel/types": "^7.22.5" } }, "@babel/highlight": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.18.6.tgz", - "integrity": "sha512-u7stbOuYjaPezCuLj29hNW1v64M2Md2qupEKP1fHc7WdOA3DgLh37suiSrZYY7haUB7iBeQZ9P1uiRF359do3g==", + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.22.5.tgz", + "integrity": "sha512-BSKlD1hgnedS5XRnGOljZawtag7H1yPfQp0tdNJCHoH6AZ+Pcm9VvkrK59/Yy593Ypg0zMxH2BxD1VPYUQ7UIw==", "dev": true, "requires": { - "@babel/helper-validator-identifier": "^7.18.6", + "@babel/helper-validator-identifier": "^7.22.5", "chalk": "^2.0.0", "js-tokens": "^4.0.0" }, @@ -9349,41 +9732,29 @@ } }, "@babel/parser": { - "version": "7.19.3", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.19.3.tgz", - "integrity": "sha512-pJ9xOlNWHiy9+FuFP09DEAFbAn4JskgRsVcc169w2xRBC3FRGuQEwjeIMMND9L2zc0iEhO/tGv4Zq+km+hxNpQ==", + "version": "7.22.7", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.22.7.tgz", + "integrity": "sha512-7NF8pOkHP5o2vpmGgNGcfAeCvOYhGLyA3Z4eBQkT1RJlWu47n63bCs93QfJ2hIAFCil7L5P2IWhs1oToVgrL0Q==", "dev": true }, "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression/-/plugin-bugfix-safari-id-destructuring-collision-in-function-expression-7.18.6.tgz", - "integrity": "sha512-Dgxsyg54Fx1d4Nge8UnvTrED63vrwOdPmyvPzlNN/boaliRP54pm3pGzZD1SJUwrBA+Cs/xdG8kXX6Mn/RfISQ==", + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression/-/plugin-bugfix-safari-id-destructuring-collision-in-function-expression-7.22.5.tgz", + "integrity": "sha512-NP1M5Rf+u2Gw9qfSO4ihjcTGW5zXTi36ITLd4/EoAcEhIZ0yjMqmftDNl3QC19CX7olhrjpyU454g/2W7X0jvQ==", "dev": true, "requires": { - "@babel/helper-plugin-utils": "^7.18.6" + "@babel/helper-plugin-utils": "^7.22.5" } }, "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": { - "version": "7.18.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining/-/plugin-bugfix-v8-spread-parameters-in-optional-chaining-7.18.9.tgz", - "integrity": "sha512-AHrP9jadvH7qlOj6PINbgSuphjQUAK7AOT7DPjBo9EHoLhQTnnK5u45e1Hd4DbSQEO9nqPWtQ89r+XEOWFScKg==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.18.9", - "@babel/helper-skip-transparent-expression-wrappers": "^7.18.9", - "@babel/plugin-proposal-optional-chaining": "^7.18.9" - } - }, - "@babel/plugin-proposal-async-generator-functions": { - "version": "7.19.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-async-generator-functions/-/plugin-proposal-async-generator-functions-7.19.1.tgz", - "integrity": "sha512-0yu8vNATgLy4ivqMNBIwb1HebCelqN7YX8SL3FDXORv/RqT0zEEWUCH4GH44JsSrvCu6GqnAdR5EBFAPeNBB4Q==", + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining/-/plugin-bugfix-v8-spread-parameters-in-optional-chaining-7.22.5.tgz", + "integrity": "sha512-31Bb65aZaUwqCbWMnZPduIZxCBngHFlzyN6Dq6KAJjtx+lx6ohKHubc61OomYi7XwVD4Ol0XCVz4h+pYFR048g==", "dev": true, "requires": { - "@babel/helper-environment-visitor": "^7.18.9", - "@babel/helper-plugin-utils": "^7.19.0", - "@babel/helper-remap-async-to-generator": "^7.18.9", - "@babel/plugin-syntax-async-generators": "^7.8.4" + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/helper-skip-transparent-expression-wrappers": "^7.22.5", + "@babel/plugin-transform-optional-chaining": "^7.22.5" } }, "@babel/plugin-proposal-class-properties": { @@ -9396,58 +9767,37 @@ "@babel/helper-plugin-utils": "^7.18.6" } }, - "@babel/plugin-proposal-class-static-block": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-class-static-block/-/plugin-proposal-class-static-block-7.18.6.tgz", - "integrity": "sha512-+I3oIiNxrCpup3Gi8n5IGMwj0gOCAjcJUSQEcotNnCCPMEnixawOQ+KeJPlgfjzx+FKQ1QSyZOWe7wmoJp7vhw==", - "dev": true, - "requires": { - "@babel/helper-create-class-features-plugin": "^7.18.6", - "@babel/helper-plugin-utils": "^7.18.6", - "@babel/plugin-syntax-class-static-block": "^7.14.5" - } - }, "@babel/plugin-proposal-decorators": { - "version": "7.19.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-decorators/-/plugin-proposal-decorators-7.19.3.tgz", - "integrity": "sha512-MbgXtNXqo7RTKYIXVchVJGPvaVufQH3pxvQyfbGvNw1DObIhph+PesYXJTcd8J4DdWibvf6Z2eanOyItX8WnJg==", + "version": "7.22.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-decorators/-/plugin-proposal-decorators-7.22.7.tgz", + "integrity": "sha512-omXqPF7Onq4Bb7wHxXjM3jSMSJvUUbvDvmmds7KI5n9Cq6Ln5I05I1W2nRlRof1rGdiUxJrxwe285WF96XlBXQ==", "dev": true, "requires": { - "@babel/helper-create-class-features-plugin": "^7.19.0", - "@babel/helper-plugin-utils": "^7.19.0", - "@babel/helper-replace-supers": "^7.19.1", - "@babel/helper-split-export-declaration": "^7.18.6", - "@babel/plugin-syntax-decorators": "^7.19.0" + "@babel/helper-create-class-features-plugin": "^7.22.6", + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/helper-replace-supers": "^7.22.5", + "@babel/helper-split-export-declaration": "^7.22.6", + "@babel/plugin-syntax-decorators": "^7.22.5" } }, "@babel/plugin-proposal-do-expressions": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-do-expressions/-/plugin-proposal-do-expressions-7.18.6.tgz", - "integrity": "sha512-ddToGCONJhCuL+l4FhtGnKl5ZYCj9fDVFiqiCdQDpeIbVn/NvMeSib+7T1/rk08jRafae4qNiP8OnJyuqlsuYA==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.18.6", - "@babel/plugin-syntax-do-expressions": "^7.18.6" - } - }, - "@babel/plugin-proposal-dynamic-import": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-dynamic-import/-/plugin-proposal-dynamic-import-7.18.6.tgz", - "integrity": "sha512-1auuwmK+Rz13SJj36R+jqFPMJWyKEDd7lLSdOj4oJK0UTgGueSAtkrCvz9ewmgyU/P941Rv2fQwZJN8s6QruXw==", + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-do-expressions/-/plugin-proposal-do-expressions-7.22.5.tgz", + "integrity": "sha512-Qh6Sbkt6xCRbHykDc709db/EQB4RJlmaW3ENuvzzi7G6GKeDNQHx81P0OdI9oEXhhHEJvLRzIr08m8HNCJWS8g==", "dev": true, "requires": { - "@babel/helper-plugin-utils": "^7.18.6", - "@babel/plugin-syntax-dynamic-import": "^7.8.3" + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/plugin-syntax-do-expressions": "^7.22.5" } }, "@babel/plugin-proposal-export-default-from": { - "version": "7.18.10", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-export-default-from/-/plugin-proposal-export-default-from-7.18.10.tgz", - "integrity": "sha512-5H2N3R2aQFxkV4PIBUR/i7PUSwgTZjouJKzI8eKswfIjT0PhvzkPn0t0wIS5zn6maQuvtT0t1oHtMUz61LOuow==", + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-export-default-from/-/plugin-proposal-export-default-from-7.22.5.tgz", + "integrity": "sha512-UCe1X/hplyv6A5g2WnQ90tnHRvYL29dabCWww92lO7VdfMVTVReBTRrhiMrKQejHD9oVkdnRdwYuzUZkBVQisg==", "dev": true, "requires": { - "@babel/helper-plugin-utils": "^7.18.9", - "@babel/plugin-syntax-export-default-from": "^7.18.6" + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/plugin-syntax-export-default-from": "^7.22.5" } }, "@babel/plugin-proposal-export-namespace-from": { @@ -9461,24 +9811,24 @@ } }, "@babel/plugin-proposal-function-bind": { - "version": "7.18.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-function-bind/-/plugin-proposal-function-bind-7.18.9.tgz", - "integrity": "sha512-9RfxqKkRBCCT0xoBl9AqieCMscJmSAL9HYixGMWH549jUpT9csWWK/HEYZEx9t9iW/PRSXgX95x9bDlgtAJGFA==", + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-function-bind/-/plugin-proposal-function-bind-7.22.5.tgz", + "integrity": "sha512-ckAugfDtdcrXKP49z7K7JI7QkA8SRidmsKxLizH8mg0UWOvcmvEd9/VDLzFcWlZqchvLDPUYpwuXNGAYjsscrw==", "dev": true, "requires": { - "@babel/helper-plugin-utils": "^7.18.9", - "@babel/plugin-syntax-function-bind": "^7.18.6" + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/plugin-syntax-function-bind": "^7.22.5" } }, "@babel/plugin-proposal-function-sent": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-function-sent/-/plugin-proposal-function-sent-7.18.6.tgz", - "integrity": "sha512-UdaOKPOLPt0O+Xu26tnw6oAZMLXhk+yMrXOzn6kAzTHBnWHJsoN1hlrgxFAQ+FRLS0ql1oYIQ2phvoFzmN3GMw==", + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-function-sent/-/plugin-proposal-function-sent-7.22.5.tgz", + "integrity": "sha512-YFEE5KDhaNCCD0I1j9vqPp5bpPuoDAPD+4Adk0QXdrL9TbmZluCuznuYvmlYqr14zfXCBGAZivfiLb6WI4GhSw==", "dev": true, "requires": { - "@babel/helper-plugin-utils": "^7.18.6", - "@babel/helper-wrap-function": "^7.18.6", - "@babel/plugin-syntax-function-sent": "^7.18.6" + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/helper-wrap-function": "^7.22.5", + "@babel/plugin-syntax-function-sent": "^7.22.5" } }, "@babel/plugin-proposal-json-strings": { @@ -9492,12 +9842,12 @@ } }, "@babel/plugin-proposal-logical-assignment-operators": { - "version": "7.18.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-logical-assignment-operators/-/plugin-proposal-logical-assignment-operators-7.18.9.tgz", - "integrity": "sha512-128YbMpjCrP35IOExw2Fq+x55LMP42DzhOhX2aNNIdI9avSWl2PI0yuBWarr3RYpZBSPtabfadkH2yeRiMD61Q==", + "version": "7.20.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-logical-assignment-operators/-/plugin-proposal-logical-assignment-operators-7.20.7.tgz", + "integrity": "sha512-y7C7cZgpMIjWlKE5T7eJwp+tnRYM89HmRvWM5EQuB5BoHEONjmQ8lSNmBUwOyy/GFRsohJED51YBF79hE1djug==", "dev": true, "requires": { - "@babel/helper-plugin-utils": "^7.18.9", + "@babel/helper-plugin-utils": "^7.20.2", "@babel/plugin-syntax-logical-assignment-operators": "^7.10.4" } }, @@ -9521,80 +9871,42 @@ "@babel/plugin-syntax-numeric-separator": "^7.10.4" } }, - "@babel/plugin-proposal-object-rest-spread": { - "version": "7.18.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-object-rest-spread/-/plugin-proposal-object-rest-spread-7.18.9.tgz", - "integrity": "sha512-kDDHQ5rflIeY5xl69CEqGEZ0KY369ehsCIEbTGb4siHG5BE9sga/T0r0OUwyZNLMmZE79E1kbsqAjwFCW4ds6Q==", - "dev": true, - "requires": { - "@babel/compat-data": "^7.18.8", - "@babel/helper-compilation-targets": "^7.18.9", - "@babel/helper-plugin-utils": "^7.18.9", - "@babel/plugin-syntax-object-rest-spread": "^7.8.3", - "@babel/plugin-transform-parameters": "^7.18.8" - } - }, - "@babel/plugin-proposal-optional-catch-binding": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-optional-catch-binding/-/plugin-proposal-optional-catch-binding-7.18.6.tgz", - "integrity": "sha512-Q40HEhs9DJQyaZfUjjn6vE8Cv4GmMHCYuMGIWUnlxH6400VGxOuwWsPt4FxXxJkC/5eOzgn0z21M9gMT4MOhbw==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.18.6", - "@babel/plugin-syntax-optional-catch-binding": "^7.8.3" - } - }, "@babel/plugin-proposal-optional-chaining": { - "version": "7.18.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-optional-chaining/-/plugin-proposal-optional-chaining-7.18.9.tgz", - "integrity": "sha512-v5nwt4IqBXihxGsW2QmCWMDS3B3bzGIk/EQVZz2ei7f3NJl8NzAJVvUmpDW5q1CRNY+Beb/k58UAH1Km1N411w==", + "version": "7.21.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-optional-chaining/-/plugin-proposal-optional-chaining-7.21.0.tgz", + "integrity": "sha512-p4zeefM72gpmEe2fkUr/OnOXpWEf8nAgk7ZYVqqfFiyIG7oFfVZcCrU64hWn5xp4tQ9LkV4bTIa5rD0KANpKNA==", "dev": true, "requires": { - "@babel/helper-plugin-utils": "^7.18.9", - "@babel/helper-skip-transparent-expression-wrappers": "^7.18.9", + "@babel/helper-plugin-utils": "^7.20.2", + "@babel/helper-skip-transparent-expression-wrappers": "^7.20.0", "@babel/plugin-syntax-optional-chaining": "^7.8.3" } }, "@babel/plugin-proposal-pipeline-operator": { - "version": "7.18.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-pipeline-operator/-/plugin-proposal-pipeline-operator-7.18.9.tgz", - "integrity": "sha512-Pc33e6m8f4MJhRXVCUwiKZNtEm+W2CUPHIL0lyJNtkp+w6d75CLw3gsBKQ81VAMUgT9jVPIEU8gwJ5nJgmJ1Ag==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.18.9", - "@babel/plugin-syntax-pipeline-operator": "^7.18.6" - } - }, - "@babel/plugin-proposal-private-methods": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-private-methods/-/plugin-proposal-private-methods-7.18.6.tgz", - "integrity": "sha512-nutsvktDItsNn4rpGItSNV2sz1XwS+nfU0Rg8aCx3W3NOKVzdMjJRu0O5OkgDp3ZGICSTbgRpxZoWsxoKRvbeA==", + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-pipeline-operator/-/plugin-proposal-pipeline-operator-7.22.5.tgz", + "integrity": "sha512-nSgHJB3uP+DORxBVhQgjub0qtU/LGj/mBDz2kQEGy1EUDy3f92VWDeB3J3/41ZjVLWmkNWOd6yjdNb4a5wDfZQ==", "dev": true, "requires": { - "@babel/helper-create-class-features-plugin": "^7.18.6", - "@babel/helper-plugin-utils": "^7.18.6" + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/plugin-syntax-pipeline-operator": "^7.22.5" } }, "@babel/plugin-proposal-private-property-in-object": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-private-property-in-object/-/plugin-proposal-private-property-in-object-7.18.6.tgz", - "integrity": "sha512-9Rysx7FOctvT5ouj5JODjAFAkgGoudQuLPamZb0v1TGLpapdNaftzifU8NTWQm0IRjqoYypdrSmyWgkocDQ8Dw==", + "version": "7.21.0-placeholder-for-preset-env.2", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-private-property-in-object/-/plugin-proposal-private-property-in-object-7.21.0-placeholder-for-preset-env.2.tgz", + "integrity": "sha512-SOSkfJDddaM7mak6cPEpswyTRnuRltl429hMraQEglW+OkovnCzsiszTmsrlY//qLFjCpQDFRvjdm2wA5pPm9w==", "dev": true, - "requires": { - "@babel/helper-annotate-as-pure": "^7.18.6", - "@babel/helper-create-class-features-plugin": "^7.18.6", - "@babel/helper-plugin-utils": "^7.18.6", - "@babel/plugin-syntax-private-property-in-object": "^7.14.5" - } + "requires": {} }, "@babel/plugin-proposal-throw-expressions": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-throw-expressions/-/plugin-proposal-throw-expressions-7.18.6.tgz", - "integrity": "sha512-WHOrJyhGoGrdtW480L79cF7Iq/gZDZ/z6OqK7mVyFR5I37dTpog/wNgb6hmaM3HYZtULEJl++7VaMWkNZsOcHg==", + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-throw-expressions/-/plugin-proposal-throw-expressions-7.22.5.tgz", + "integrity": "sha512-34kY5YjNKDhjXbj2oNDkxl0xNl2+yQTEsWu8Ia6kCTb6wz76bBCd4DzmeZokfr6g68yneu3eg8qAyYgKbyesFg==", "dev": true, "requires": { - "@babel/helper-plugin-utils": "^7.18.6", - "@babel/plugin-syntax-throw-expressions": "^7.18.6" + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/plugin-syntax-throw-expressions": "^7.22.5" } }, "@babel/plugin-proposal-unicode-property-regex": { @@ -9635,21 +9947,21 @@ } }, "@babel/plugin-syntax-decorators": { - "version": "7.19.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-decorators/-/plugin-syntax-decorators-7.19.0.tgz", - "integrity": "sha512-xaBZUEDntt4faL1yN8oIFlhfXeQAWJW7CLKYsHTUqriCUbj8xOra8bfxxKGi/UwExPFBuPdH4XfHc9rGQhrVkQ==", + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-decorators/-/plugin-syntax-decorators-7.22.5.tgz", + "integrity": "sha512-avpUOBS7IU6al8MmF1XpAyj9QYeLPuSDJI5D4pVMSMdL7xQokKqJPYQC67RCT0aCTashUXPiGwMJ0DEXXCEmMA==", "dev": true, "requires": { - "@babel/helper-plugin-utils": "^7.19.0" + "@babel/helper-plugin-utils": "^7.22.5" } }, "@babel/plugin-syntax-do-expressions": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-do-expressions/-/plugin-syntax-do-expressions-7.18.6.tgz", - "integrity": "sha512-kTogvOsjBTVOSZtkkziiXB5hwGXqwhq2gBXDaiWVruRLDT7C2GqfbsMnicHJ7ePq2GE8UJeWS34YbNP6yDhwUA==", + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-do-expressions/-/plugin-syntax-do-expressions-7.22.5.tgz", + "integrity": "sha512-60pOTgQGY00/Kiozrtu286Aqg50IxDy/jIHhlMzXjYTs1Q8lbeOgqC9NLidtqfBNwdX6bZCT6FJ2i5xzt+JKzw==", "dev": true, "requires": { - "@babel/helper-plugin-utils": "^7.18.6" + "@babel/helper-plugin-utils": "^7.22.5" } }, "@babel/plugin-syntax-dynamic-import": { @@ -9662,12 +9974,12 @@ } }, "@babel/plugin-syntax-export-default-from": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-export-default-from/-/plugin-syntax-export-default-from-7.18.6.tgz", - "integrity": "sha512-Kr//z3ujSVNx6E9z9ih5xXXMqK07VVTuqPmqGe6Mss/zW5XPeLZeSDZoP9ab/hT4wPKqAgjl2PnhPrcpk8Seew==", + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-export-default-from/-/plugin-syntax-export-default-from-7.22.5.tgz", + "integrity": "sha512-ODAqWWXB/yReh/jVQDag/3/tl6lgBueQkk/TcfW/59Oykm4c8a55XloX0CTk2k2VJiFWMgHby9xNX29IbCv9dQ==", "dev": true, "requires": { - "@babel/helper-plugin-utils": "^7.18.6" + "@babel/helper-plugin-utils": "^7.22.5" } }, "@babel/plugin-syntax-export-namespace-from": { @@ -9680,30 +9992,39 @@ } }, "@babel/plugin-syntax-function-bind": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-function-bind/-/plugin-syntax-function-bind-7.18.6.tgz", - "integrity": "sha512-wZN0Aq/AScknI9mKGcR3TpHdASMufFGaeJgc1rhPmLtZ/PniwjePSh8cfh8tXMB3U4kh/3cRKrLjDtedejg8jQ==", + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-function-bind/-/plugin-syntax-function-bind-7.22.5.tgz", + "integrity": "sha512-Sjy7XIhHF9L++0Mk/3Y4H4439cjI//wc/jE8Ly3+qGPkTUYYEhe4rzMv/JnyZpekfOBL22X6DAq42I7GM/3KzA==", "dev": true, "requires": { - "@babel/helper-plugin-utils": "^7.18.6" + "@babel/helper-plugin-utils": "^7.22.5" } }, "@babel/plugin-syntax-function-sent": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-function-sent/-/plugin-syntax-function-sent-7.18.6.tgz", - "integrity": "sha512-f3OJHIlFIkg+cP1Hfo2SInLhsg0pz2Ikmgo7jMdIIKC+3jVXQlHB0bgSapOWxeWI0SU28qIWmfn5ZKu1yPJHkg==", + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-function-sent/-/plugin-syntax-function-sent-7.22.5.tgz", + "integrity": "sha512-tKOWGUAVv+JGJ1tcOIFdCqxUX97lgAUnmLpWt/9JtEkgk9WQ5OolN+y9rWj6mtLM+d0kAzTGLu/kRQqr5/PEsA==", "dev": true, "requires": { - "@babel/helper-plugin-utils": "^7.18.6" + "@babel/helper-plugin-utils": "^7.22.5" } }, "@babel/plugin-syntax-import-assertions": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-assertions/-/plugin-syntax-import-assertions-7.18.6.tgz", - "integrity": "sha512-/DU3RXad9+bZwrgWJQKbr39gYbJpLJHezqEzRzi/BHRlJ9zsQb4CK2CA/5apllXNomwA1qHwzvHl+AdEmC5krQ==", + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-assertions/-/plugin-syntax-import-assertions-7.22.5.tgz", + "integrity": "sha512-rdV97N7KqsRzeNGoWUOK6yUsWarLjE5Su/Snk9IYPU9CwkWHs4t+rTGOvffTR8XGkJMTAdLfO0xVnXm8wugIJg==", "dev": true, "requires": { - "@babel/helper-plugin-utils": "^7.18.6" + "@babel/helper-plugin-utils": "^7.22.5" + } + }, + "@babel/plugin-syntax-import-attributes": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.22.5.tgz", + "integrity": "sha512-KwvoWDeNKPETmozyFE0P2rOLqh39EoQHNjqizrI5B8Vt0ZNS7M56s7dAiAqbYfiAYOuIzIh96z3iR2ktgu3tEg==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.22.5" } }, "@babel/plugin-syntax-import-meta": { @@ -9779,12 +10100,12 @@ } }, "@babel/plugin-syntax-pipeline-operator": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-pipeline-operator/-/plugin-syntax-pipeline-operator-7.18.6.tgz", - "integrity": "sha512-pFtIdQomJtkTHWcNsGXhjJ5YUkL+AxJnP4G+Ol85UO6uT2fpHTPYLLE5bBeRA9cxf25qa/VKsJ3Fi67Gyqe3rA==", + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-pipeline-operator/-/plugin-syntax-pipeline-operator-7.22.5.tgz", + "integrity": "sha512-7yuGXd+h8gpR14FnPDTTCd5TfC/1B9njNZJT29GJ7UFF/WVbzkZy7728DynrENqgImqj5xyPTQAo8si9n3QVJQ==", "dev": true, "requires": { - "@babel/helper-plugin-utils": "^7.18.6" + "@babel/helper-plugin-utils": "^7.22.5" } }, "@babel/plugin-syntax-private-property-in-object": { @@ -9797,12 +10118,12 @@ } }, "@babel/plugin-syntax-throw-expressions": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-throw-expressions/-/plugin-syntax-throw-expressions-7.18.6.tgz", - "integrity": "sha512-rp1CqEZXGv1z1YZ3qYffBH3rhnOxrTwQG8fh2yqulTurwv9zu3Gthfd+niZBLSOi1rY6146TgF+JmVeDXaX4TQ==", + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-throw-expressions/-/plugin-syntax-throw-expressions-7.22.5.tgz", + "integrity": "sha512-oCyfA7rDVcQIydA7ZOmnHCQTzz5JvG9arY++Z+ASL/q5q+mJLblaRNHoK6ggV54X2c14wCK/lQi7z1DujmEmZA==", "dev": true, "requires": { - "@babel/helper-plugin-utils": "^7.18.6" + "@babel/helper-plugin-utils": "^7.22.5" } }, "@babel/plugin-syntax-top-level-await": { @@ -9814,58 +10135,101 @@ "@babel/helper-plugin-utils": "^7.14.5" } }, - "@babel/plugin-transform-arrow-functions": { + "@babel/plugin-syntax-unicode-sets-regex": { "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.18.6.tgz", - "integrity": "sha512-9S9X9RUefzrsHZmKMbDXxweEH+YlE8JJEuat9FdvW9Qh1cw7W64jELCtWNkPBPX5En45uy28KGvA/AySqUh8CQ==", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-unicode-sets-regex/-/plugin-syntax-unicode-sets-regex-7.18.6.tgz", + "integrity": "sha512-727YkEAPwSIQTv5im8QHz3upqp92JTWhidIC81Tdx4VJYIte/VndKf1qKrfnnhPLiPghStWfvC/iFaMCQu7Nqg==", "dev": true, "requires": { + "@babel/helper-create-regexp-features-plugin": "^7.18.6", "@babel/helper-plugin-utils": "^7.18.6" } }, + "@babel/plugin-transform-arrow-functions": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.22.5.tgz", + "integrity": "sha512-26lTNXoVRdAnsaDXPpvCNUq+OVWEVC6bx7Vvz9rC53F2bagUWW4u4ii2+h8Fejfh7RYqPxn+libeFBBck9muEw==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.22.5" + } + }, + "@babel/plugin-transform-async-generator-functions": { + "version": "7.22.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-generator-functions/-/plugin-transform-async-generator-functions-7.22.7.tgz", + "integrity": "sha512-7HmE7pk/Fmke45TODvxvkxRMV9RazV+ZZzhOL9AG8G29TLrr3jkjwF7uJfxZ30EoXpO+LJkq4oA8NjO2DTnEDg==", + "dev": true, + "requires": { + "@babel/helper-environment-visitor": "^7.22.5", + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/helper-remap-async-to-generator": "^7.22.5", + "@babel/plugin-syntax-async-generators": "^7.8.4" + } + }, "@babel/plugin-transform-async-to-generator": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-to-generator/-/plugin-transform-async-to-generator-7.18.6.tgz", - "integrity": "sha512-ARE5wZLKnTgPW7/1ftQmSi1CmkqqHo2DNmtztFhvgtOWSDfq0Cq9/9L+KnZNYSNrydBekhW3rwShduf59RoXag==", + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-to-generator/-/plugin-transform-async-to-generator-7.22.5.tgz", + "integrity": "sha512-b1A8D8ZzE/VhNDoV1MSJTnpKkCG5bJo+19R4o4oy03zM7ws8yEMK755j61Dc3EyvdysbqH5BOOTquJ7ZX9C6vQ==", "dev": true, "requires": { - "@babel/helper-module-imports": "^7.18.6", - "@babel/helper-plugin-utils": "^7.18.6", - "@babel/helper-remap-async-to-generator": "^7.18.6" + "@babel/helper-module-imports": "^7.22.5", + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/helper-remap-async-to-generator": "^7.22.5" } }, "@babel/plugin-transform-block-scoped-functions": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoped-functions/-/plugin-transform-block-scoped-functions-7.18.6.tgz", - "integrity": "sha512-ExUcOqpPWnliRcPqves5HJcJOvHvIIWfuS4sroBUenPuMdmW+SMHDakmtS7qOo13sVppmUijqeTv7qqGsvURpQ==", + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoped-functions/-/plugin-transform-block-scoped-functions-7.22.5.tgz", + "integrity": "sha512-tdXZ2UdknEKQWKJP1KMNmuF5Lx3MymtMN/pvA+p/VEkhK8jVcQ1fzSy8KM9qRYhAf2/lV33hoMPKI/xaI9sADA==", "dev": true, "requires": { - "@babel/helper-plugin-utils": "^7.18.6" + "@babel/helper-plugin-utils": "^7.22.5" } }, "@babel/plugin-transform-block-scoping": { - "version": "7.18.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.18.9.tgz", - "integrity": "sha512-5sDIJRV1KtQVEbt/EIBwGy4T01uYIo4KRB3VUqzkhrAIOGx7AoctL9+Ux88btY0zXdDyPJ9mW+bg+v+XEkGmtw==", + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.22.5.tgz", + "integrity": "sha512-EcACl1i5fSQ6bt+YGuU/XGCeZKStLmyVGytWkpyhCLeQVA0eu6Wtiw92V+I1T/hnezUv7j74dA/Ro69gWcU+hg==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.22.5" + } + }, + "@babel/plugin-transform-class-properties": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-class-properties/-/plugin-transform-class-properties-7.22.5.tgz", + "integrity": "sha512-nDkQ0NfkOhPTq8YCLiWNxp1+f9fCobEjCb0n8WdbNUBc4IB5V7P1QnX9IjpSoquKrXF5SKojHleVNs2vGeHCHQ==", + "dev": true, + "requires": { + "@babel/helper-create-class-features-plugin": "^7.22.5", + "@babel/helper-plugin-utils": "^7.22.5" + } + }, + "@babel/plugin-transform-class-static-block": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-class-static-block/-/plugin-transform-class-static-block-7.22.5.tgz", + "integrity": "sha512-SPToJ5eYZLxlnp1UzdARpOGeC2GbHvr9d/UV0EukuVx8atktg194oe+C5BqQ8jRTkgLRVOPYeXRSBg1IlMoVRA==", "dev": true, "requires": { - "@babel/helper-plugin-utils": "^7.18.9" + "@babel/helper-create-class-features-plugin": "^7.22.5", + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/plugin-syntax-class-static-block": "^7.14.5" } }, "@babel/plugin-transform-classes": { - "version": "7.19.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-classes/-/plugin-transform-classes-7.19.0.tgz", - "integrity": "sha512-YfeEE9kCjqTS9IitkgfJuxjcEtLUHMqa8yUJ6zdz8vR7hKuo6mOy2C05P0F1tdMmDCeuyidKnlrw/iTppHcr2A==", - "dev": true, - "requires": { - "@babel/helper-annotate-as-pure": "^7.18.6", - "@babel/helper-compilation-targets": "^7.19.0", - "@babel/helper-environment-visitor": "^7.18.9", - "@babel/helper-function-name": "^7.19.0", - "@babel/helper-optimise-call-expression": "^7.18.6", - "@babel/helper-plugin-utils": "^7.19.0", - "@babel/helper-replace-supers": "^7.18.9", - "@babel/helper-split-export-declaration": "^7.18.6", + "version": "7.22.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-classes/-/plugin-transform-classes-7.22.6.tgz", + "integrity": "sha512-58EgM6nuPNG6Py4Z3zSuu0xWu2VfodiMi72Jt5Kj2FECmaYk1RrTXA45z6KBFsu9tRgwQDwIiY4FXTt+YsSFAQ==", + "dev": true, + "requires": { + "@babel/helper-annotate-as-pure": "^7.22.5", + "@babel/helper-compilation-targets": "^7.22.6", + "@babel/helper-environment-visitor": "^7.22.5", + "@babel/helper-function-name": "^7.22.5", + "@babel/helper-optimise-call-expression": "^7.22.5", + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/helper-replace-supers": "^7.22.5", + "@babel/helper-split-export-declaration": "^7.22.6", "globals": "^11.1.0" }, "dependencies": { @@ -9878,300 +10242,422 @@ } }, "@babel/plugin-transform-computed-properties": { - "version": "7.18.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-computed-properties/-/plugin-transform-computed-properties-7.18.9.tgz", - "integrity": "sha512-+i0ZU1bCDymKakLxn5srGHrsAPRELC2WIbzwjLhHW9SIE1cPYkLCL0NlnXMZaM1vhfgA2+M7hySk42VBvrkBRw==", + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-computed-properties/-/plugin-transform-computed-properties-7.22.5.tgz", + "integrity": "sha512-4GHWBgRf0krxPX+AaPtgBAlTgTeZmqDynokHOX7aqqAB4tHs3U2Y02zH6ETFdLZGcg9UQSD1WCmkVrE9ErHeOg==", "dev": true, "requires": { - "@babel/helper-plugin-utils": "^7.18.9" + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/template": "^7.22.5" } }, "@babel/plugin-transform-destructuring": { - "version": "7.18.13", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.18.13.tgz", - "integrity": "sha512-TodpQ29XekIsex2A+YJPj5ax2plkGa8YYY6mFjCohk/IG9IY42Rtuj1FuDeemfg2ipxIFLzPeA83SIBnlhSIow==", + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.22.5.tgz", + "integrity": "sha512-GfqcFuGW8vnEqTUBM7UtPd5A4q797LTvvwKxXTgRsFjoqaJiEg9deBG6kWeQYkVEL569NpnmpC0Pkr/8BLKGnQ==", "dev": true, "requires": { - "@babel/helper-plugin-utils": "^7.18.9" + "@babel/helper-plugin-utils": "^7.22.5" } }, "@babel/plugin-transform-dotall-regex": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-dotall-regex/-/plugin-transform-dotall-regex-7.18.6.tgz", - "integrity": "sha512-6S3jpun1eEbAxq7TdjLotAsl4WpQI9DxfkycRcKrjhQYzU87qpXdknpBg/e+TdcMehqGnLFi7tnFUBR02Vq6wg==", + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-dotall-regex/-/plugin-transform-dotall-regex-7.22.5.tgz", + "integrity": "sha512-5/Yk9QxCQCl+sOIB1WelKnVRxTJDSAIxtJLL2/pqL14ZVlbH0fUQUZa/T5/UnQtBNgghR7mfB8ERBKyKPCi7Vw==", "dev": true, "requires": { - "@babel/helper-create-regexp-features-plugin": "^7.18.6", - "@babel/helper-plugin-utils": "^7.18.6" + "@babel/helper-create-regexp-features-plugin": "^7.22.5", + "@babel/helper-plugin-utils": "^7.22.5" } }, "@babel/plugin-transform-duplicate-keys": { - "version": "7.18.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-duplicate-keys/-/plugin-transform-duplicate-keys-7.18.9.tgz", - "integrity": "sha512-d2bmXCtZXYc59/0SanQKbiWINadaJXqtvIQIzd4+hNwkWBgyCd5F/2t1kXoUdvPMrxzPvhK6EMQRROxsue+mfw==", + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-duplicate-keys/-/plugin-transform-duplicate-keys-7.22.5.tgz", + "integrity": "sha512-dEnYD+9BBgld5VBXHnF/DbYGp3fqGMsyxKbtD1mDyIA7AkTSpKXFhCVuj/oQVOoALfBs77DudA0BE4d5mcpmqw==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.22.5" + } + }, + "@babel/plugin-transform-dynamic-import": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-dynamic-import/-/plugin-transform-dynamic-import-7.22.5.tgz", + "integrity": "sha512-0MC3ppTB1AMxd8fXjSrbPa7LT9hrImt+/fcj+Pg5YMD7UQyWp/02+JWpdnCymmsXwIx5Z+sYn1bwCn4ZJNvhqQ==", "dev": true, "requires": { - "@babel/helper-plugin-utils": "^7.18.9" + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/plugin-syntax-dynamic-import": "^7.8.3" } }, "@babel/plugin-transform-exponentiation-operator": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-exponentiation-operator/-/plugin-transform-exponentiation-operator-7.18.6.tgz", - "integrity": "sha512-wzEtc0+2c88FVR34aQmiz56dxEkxr2g8DQb/KfaFa1JYXOFVsbhvAonFN6PwVWj++fKmku8NP80plJ5Et4wqHw==", + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-exponentiation-operator/-/plugin-transform-exponentiation-operator-7.22.5.tgz", + "integrity": "sha512-vIpJFNM/FjZ4rh1myqIya9jXwrwwgFRHPjT3DkUA9ZLHuzox8jiXkOLvwm1H+PQIP3CqfC++WPKeuDi0Sjdj1g==", "dev": true, "requires": { - "@babel/helper-builder-binary-assignment-operator-visitor": "^7.18.6", - "@babel/helper-plugin-utils": "^7.18.6" + "@babel/helper-builder-binary-assignment-operator-visitor": "^7.22.5", + "@babel/helper-plugin-utils": "^7.22.5" + } + }, + "@babel/plugin-transform-export-namespace-from": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-export-namespace-from/-/plugin-transform-export-namespace-from-7.22.5.tgz", + "integrity": "sha512-X4hhm7FRnPgd4nDA4b/5V280xCx6oL7Oob5+9qVS5C13Zq4bh1qq7LU0GgRU6b5dBWBvhGaXYVB4AcN6+ol6vg==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/plugin-syntax-export-namespace-from": "^7.8.3" } }, "@babel/plugin-transform-for-of": { - "version": "7.18.8", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.18.8.tgz", - "integrity": "sha512-yEfTRnjuskWYo0k1mHUqrVWaZwrdq8AYbfrpqULOJOaucGSp4mNMVps+YtA8byoevxS/urwU75vyhQIxcCgiBQ==", + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.22.5.tgz", + "integrity": "sha512-3kxQjX1dU9uudwSshyLeEipvrLjBCVthCgeTp6CzE/9JYrlAIaeekVxRpCWsDDfYTfRZRoCeZatCQvwo+wvK8A==", "dev": true, "requires": { - "@babel/helper-plugin-utils": "^7.18.6" + "@babel/helper-plugin-utils": "^7.22.5" } }, "@babel/plugin-transform-function-name": { - "version": "7.18.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-function-name/-/plugin-transform-function-name-7.18.9.tgz", - "integrity": "sha512-WvIBoRPaJQ5yVHzcnJFor7oS5Ls0PYixlTYE63lCj2RtdQEl15M68FXQlxnG6wdraJIXRdR7KI+hQ7q/9QjrCQ==", + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-function-name/-/plugin-transform-function-name-7.22.5.tgz", + "integrity": "sha512-UIzQNMS0p0HHiQm3oelztj+ECwFnj+ZRV4KnguvlsD2of1whUeM6o7wGNj6oLwcDoAXQ8gEqfgC24D+VdIcevg==", + "dev": true, + "requires": { + "@babel/helper-compilation-targets": "^7.22.5", + "@babel/helper-function-name": "^7.22.5", + "@babel/helper-plugin-utils": "^7.22.5" + } + }, + "@babel/plugin-transform-json-strings": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-json-strings/-/plugin-transform-json-strings-7.22.5.tgz", + "integrity": "sha512-DuCRB7fu8MyTLbEQd1ew3R85nx/88yMoqo2uPSjevMj3yoN7CDM8jkgrY0wmVxfJZyJ/B9fE1iq7EQppWQmR5A==", "dev": true, "requires": { - "@babel/helper-compilation-targets": "^7.18.9", - "@babel/helper-function-name": "^7.18.9", - "@babel/helper-plugin-utils": "^7.18.9" + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/plugin-syntax-json-strings": "^7.8.3" } }, "@babel/plugin-transform-literals": { - "version": "7.18.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-literals/-/plugin-transform-literals-7.18.9.tgz", - "integrity": "sha512-IFQDSRoTPnrAIrI5zoZv73IFeZu2dhu6irxQjY9rNjTT53VmKg9fenjvoiOWOkJ6mm4jKVPtdMzBY98Fp4Z4cg==", + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-literals/-/plugin-transform-literals-7.22.5.tgz", + "integrity": "sha512-fTLj4D79M+mepcw3dgFBTIDYpbcB9Sm0bpm4ppXPaO+U+PKFFyV9MGRvS0gvGw62sd10kT5lRMKXAADb9pWy8g==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.22.5" + } + }, + "@babel/plugin-transform-logical-assignment-operators": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-logical-assignment-operators/-/plugin-transform-logical-assignment-operators-7.22.5.tgz", + "integrity": "sha512-MQQOUW1KL8X0cDWfbwYP+TbVbZm16QmQXJQ+vndPtH/BoO0lOKpVoEDMI7+PskYxH+IiE0tS8xZye0qr1lGzSA==", "dev": true, "requires": { - "@babel/helper-plugin-utils": "^7.18.9" + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/plugin-syntax-logical-assignment-operators": "^7.10.4" } }, "@babel/plugin-transform-member-expression-literals": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-member-expression-literals/-/plugin-transform-member-expression-literals-7.18.6.tgz", - "integrity": "sha512-qSF1ihLGO3q+/g48k85tUjD033C29TNTVB2paCwZPVmOsjn9pClvYYrM2VeJpBY2bcNkuny0YUyTNRyRxJ54KA==", + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-member-expression-literals/-/plugin-transform-member-expression-literals-7.22.5.tgz", + "integrity": "sha512-RZEdkNtzzYCFl9SE9ATaUMTj2hqMb4StarOJLrZRbqqU4HSBE7UlBw9WBWQiDzrJZJdUWiMTVDI6Gv/8DPvfew==", "dev": true, "requires": { - "@babel/helper-plugin-utils": "^7.18.6" + "@babel/helper-plugin-utils": "^7.22.5" } }, "@babel/plugin-transform-modules-amd": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-amd/-/plugin-transform-modules-amd-7.18.6.tgz", - "integrity": "sha512-Pra5aXsmTsOnjM3IajS8rTaLCy++nGM4v3YR4esk5PCsyg9z8NA5oQLwxzMUtDBd8F+UmVza3VxoAaWCbzH1rg==", + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-amd/-/plugin-transform-modules-amd-7.22.5.tgz", + "integrity": "sha512-R+PTfLTcYEmb1+kK7FNkhQ1gP4KgjpSO6HfH9+f8/yfp2Nt3ggBjiVpRwmwTlfqZLafYKJACy36yDXlEmI9HjQ==", "dev": true, "requires": { - "@babel/helper-module-transforms": "^7.18.6", - "@babel/helper-plugin-utils": "^7.18.6", - "babel-plugin-dynamic-import-node": "^2.3.3" + "@babel/helper-module-transforms": "^7.22.5", + "@babel/helper-plugin-utils": "^7.22.5" } }, "@babel/plugin-transform-modules-commonjs": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.18.6.tgz", - "integrity": "sha512-Qfv2ZOWikpvmedXQJDSbxNqy7Xr/j2Y8/KfijM0iJyKkBTmWuvCA1yeH1yDM7NJhBW/2aXxeucLj6i80/LAJ/Q==", + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.22.5.tgz", + "integrity": "sha512-B4pzOXj+ONRmuaQTg05b3y/4DuFz3WcCNAXPLb2Q0GT0TrGKGxNKV4jwsXts+StaM0LQczZbOpj8o1DLPDJIiA==", "dev": true, "requires": { - "@babel/helper-module-transforms": "^7.18.6", - "@babel/helper-plugin-utils": "^7.18.6", - "@babel/helper-simple-access": "^7.18.6", - "babel-plugin-dynamic-import-node": "^2.3.3" + "@babel/helper-module-transforms": "^7.22.5", + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/helper-simple-access": "^7.22.5" } }, "@babel/plugin-transform-modules-systemjs": { - "version": "7.19.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.19.0.tgz", - "integrity": "sha512-x9aiR0WXAWmOWsqcsnrzGR+ieaTMVyGyffPVA7F8cXAGt/UxefYv6uSHZLkAFChN5M5Iy1+wjE+xJuPt22H39A==", + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.22.5.tgz", + "integrity": "sha512-emtEpoaTMsOs6Tzz+nbmcePl6AKVtS1yC4YNAeMun9U8YCsgadPNxnOPQ8GhHFB2qdx+LZu9LgoC0Lthuu05DQ==", "dev": true, "requires": { - "@babel/helper-hoist-variables": "^7.18.6", - "@babel/helper-module-transforms": "^7.19.0", - "@babel/helper-plugin-utils": "^7.19.0", - "@babel/helper-validator-identifier": "^7.18.6", - "babel-plugin-dynamic-import-node": "^2.3.3" + "@babel/helper-hoist-variables": "^7.22.5", + "@babel/helper-module-transforms": "^7.22.5", + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/helper-validator-identifier": "^7.22.5" } }, "@babel/plugin-transform-modules-umd": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-umd/-/plugin-transform-modules-umd-7.18.6.tgz", - "integrity": "sha512-dcegErExVeXcRqNtkRU/z8WlBLnvD4MRnHgNs3MytRO1Mn1sHRyhbcpYbVMGclAqOjdW+9cfkdZno9dFdfKLfQ==", + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-umd/-/plugin-transform-modules-umd-7.22.5.tgz", + "integrity": "sha512-+S6kzefN/E1vkSsKx8kmQuqeQsvCKCd1fraCM7zXm4SFoggI099Tr4G8U81+5gtMdUeMQ4ipdQffbKLX0/7dBQ==", "dev": true, "requires": { - "@babel/helper-module-transforms": "^7.18.6", - "@babel/helper-plugin-utils": "^7.18.6" + "@babel/helper-module-transforms": "^7.22.5", + "@babel/helper-plugin-utils": "^7.22.5" } }, "@babel/plugin-transform-named-capturing-groups-regex": { - "version": "7.19.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-named-capturing-groups-regex/-/plugin-transform-named-capturing-groups-regex-7.19.1.tgz", - "integrity": "sha512-oWk9l9WItWBQYS4FgXD4Uyy5kq898lvkXpXQxoJEY1RnvPk4R/Dvu2ebXU9q8lP+rlMwUQTFf2Ok6d78ODa0kw==", + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-named-capturing-groups-regex/-/plugin-transform-named-capturing-groups-regex-7.22.5.tgz", + "integrity": "sha512-YgLLKmS3aUBhHaxp5hi1WJTgOUb/NCuDHzGT9z9WTt3YG+CPRhJs6nprbStx6DnWM4dh6gt7SU3sZodbZ08adQ==", "dev": true, "requires": { - "@babel/helper-create-regexp-features-plugin": "^7.19.0", - "@babel/helper-plugin-utils": "^7.19.0" + "@babel/helper-create-regexp-features-plugin": "^7.22.5", + "@babel/helper-plugin-utils": "^7.22.5" } }, "@babel/plugin-transform-new-target": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-new-target/-/plugin-transform-new-target-7.18.6.tgz", - "integrity": "sha512-DjwFA/9Iu3Z+vrAn+8pBUGcjhxKguSMlsFqeCKbhb9BAV756v0krzVK04CRDi/4aqmk8BsHb4a/gFcaA5joXRw==", + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-new-target/-/plugin-transform-new-target-7.22.5.tgz", + "integrity": "sha512-AsF7K0Fx/cNKVyk3a+DW0JLo+Ua598/NxMRvxDnkpCIGFh43+h/v2xyhRUYf6oD8gE4QtL83C7zZVghMjHd+iw==", "dev": true, "requires": { - "@babel/helper-plugin-utils": "^7.18.6" + "@babel/helper-plugin-utils": "^7.22.5" + } + }, + "@babel/plugin-transform-nullish-coalescing-operator": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-nullish-coalescing-operator/-/plugin-transform-nullish-coalescing-operator-7.22.5.tgz", + "integrity": "sha512-6CF8g6z1dNYZ/VXok5uYkkBBICHZPiGEl7oDnAx2Mt1hlHVHOSIKWJaXHjQJA5VB43KZnXZDIexMchY4y2PGdA==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3" + } + }, + "@babel/plugin-transform-numeric-separator": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-numeric-separator/-/plugin-transform-numeric-separator-7.22.5.tgz", + "integrity": "sha512-NbslED1/6M+sXiwwtcAB/nieypGw02Ejf4KtDeMkCEpP6gWFMX1wI9WKYua+4oBneCCEmulOkRpwywypVZzs/g==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/plugin-syntax-numeric-separator": "^7.10.4" + } + }, + "@babel/plugin-transform-object-rest-spread": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-rest-spread/-/plugin-transform-object-rest-spread-7.22.5.tgz", + "integrity": "sha512-Kk3lyDmEslH9DnvCDA1s1kkd3YWQITiBOHngOtDL9Pt6BZjzqb6hiOlb8VfjiiQJ2unmegBqZu0rx5RxJb5vmQ==", + "dev": true, + "requires": { + "@babel/compat-data": "^7.22.5", + "@babel/helper-compilation-targets": "^7.22.5", + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/plugin-syntax-object-rest-spread": "^7.8.3", + "@babel/plugin-transform-parameters": "^7.22.5" } }, "@babel/plugin-transform-object-super": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-super/-/plugin-transform-object-super-7.18.6.tgz", - "integrity": "sha512-uvGz6zk+pZoS1aTZrOvrbj6Pp/kK2mp45t2B+bTDre2UgsZZ8EZLSJtUg7m/no0zOJUWgFONpB7Zv9W2tSaFlA==", + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-super/-/plugin-transform-object-super-7.22.5.tgz", + "integrity": "sha512-klXqyaT9trSjIUrcsYIfETAzmOEZL3cBYqOYLJxBHfMFFggmXOv+NYSX/Jbs9mzMVESw/WycLFPRx8ba/b2Ipw==", "dev": true, "requires": { - "@babel/helper-plugin-utils": "^7.18.6", - "@babel/helper-replace-supers": "^7.18.6" + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/helper-replace-supers": "^7.22.5" + } + }, + "@babel/plugin-transform-optional-catch-binding": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-optional-catch-binding/-/plugin-transform-optional-catch-binding-7.22.5.tgz", + "integrity": "sha512-pH8orJahy+hzZje5b8e2QIlBWQvGpelS76C63Z+jhZKsmzfNaPQ+LaW6dcJ9bxTpo1mtXbgHwy765Ro3jftmUg==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/plugin-syntax-optional-catch-binding": "^7.8.3" + } + }, + "@babel/plugin-transform-optional-chaining": { + "version": "7.22.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-optional-chaining/-/plugin-transform-optional-chaining-7.22.6.tgz", + "integrity": "sha512-Vd5HiWml0mDVtcLHIoEU5sw6HOUW/Zk0acLs/SAeuLzkGNOPc9DB4nkUajemhCmTIz3eiaKREZn2hQQqF79YTg==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/helper-skip-transparent-expression-wrappers": "^7.22.5", + "@babel/plugin-syntax-optional-chaining": "^7.8.3" } }, "@babel/plugin-transform-parameters": { - "version": "7.18.8", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.18.8.tgz", - "integrity": "sha512-ivfbE3X2Ss+Fj8nnXvKJS6sjRG4gzwPMsP+taZC+ZzEGjAYlvENixmt1sZ5Ca6tWls+BlKSGKPJ6OOXvXCbkFg==", + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.22.5.tgz", + "integrity": "sha512-AVkFUBurORBREOmHRKo06FjHYgjrabpdqRSwq6+C7R5iTCZOsM4QbcB27St0a4U6fffyAOqh3s/qEfybAhfivg==", "dev": true, "requires": { - "@babel/helper-plugin-utils": "^7.18.6" + "@babel/helper-plugin-utils": "^7.22.5" + } + }, + "@babel/plugin-transform-private-methods": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-private-methods/-/plugin-transform-private-methods-7.22.5.tgz", + "integrity": "sha512-PPjh4gyrQnGe97JTalgRGMuU4icsZFnWkzicB/fUtzlKUqvsWBKEpPPfr5a2JiyirZkHxnAqkQMO5Z5B2kK3fA==", + "dev": true, + "requires": { + "@babel/helper-create-class-features-plugin": "^7.22.5", + "@babel/helper-plugin-utils": "^7.22.5" + } + }, + "@babel/plugin-transform-private-property-in-object": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-private-property-in-object/-/plugin-transform-private-property-in-object-7.22.5.tgz", + "integrity": "sha512-/9xnaTTJcVoBtSSmrVyhtSvO3kbqS2ODoh2juEU72c3aYonNF0OMGiaz2gjukyKM2wBBYJP38S4JiE0Wfb5VMQ==", + "dev": true, + "requires": { + "@babel/helper-annotate-as-pure": "^7.22.5", + "@babel/helper-create-class-features-plugin": "^7.22.5", + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/plugin-syntax-private-property-in-object": "^7.14.5" } }, "@babel/plugin-transform-property-literals": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-property-literals/-/plugin-transform-property-literals-7.18.6.tgz", - "integrity": "sha512-cYcs6qlgafTud3PAzrrRNbQtfpQ8+y/+M5tKmksS9+M1ckbH6kzY8MrexEM9mcA6JDsukE19iIRvAyYl463sMg==", + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-property-literals/-/plugin-transform-property-literals-7.22.5.tgz", + "integrity": "sha512-TiOArgddK3mK/x1Qwf5hay2pxI6wCZnvQqrFSqbtg1GLl2JcNMitVH/YnqjP+M31pLUeTfzY1HAXFDnUBV30rQ==", "dev": true, "requires": { - "@babel/helper-plugin-utils": "^7.18.6" + "@babel/helper-plugin-utils": "^7.22.5" } }, "@babel/plugin-transform-regenerator": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.18.6.tgz", - "integrity": "sha512-poqRI2+qiSdeldcz4wTSTXBRryoq3Gc70ye7m7UD5Ww0nE29IXqMl6r7Nd15WBgRd74vloEMlShtH6CKxVzfmQ==", + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.22.5.tgz", + "integrity": "sha512-rR7KePOE7gfEtNTh9Qw+iO3Q/e4DEsoQ+hdvM6QUDH7JRJ5qxq5AA52ZzBWbI5i9lfNuvySgOGP8ZN7LAmaiPw==", "dev": true, "requires": { - "@babel/helper-plugin-utils": "^7.18.6", - "regenerator-transform": "^0.15.0" + "@babel/helper-plugin-utils": "^7.22.5", + "regenerator-transform": "^0.15.1" } }, "@babel/plugin-transform-reserved-words": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-reserved-words/-/plugin-transform-reserved-words-7.18.6.tgz", - "integrity": "sha512-oX/4MyMoypzHjFrT1CdivfKZ+XvIPMFXwwxHp/r0Ddy2Vuomt4HDFGmft1TAY2yiTKiNSsh3kjBAzcM8kSdsjA==", + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-reserved-words/-/plugin-transform-reserved-words-7.22.5.tgz", + "integrity": "sha512-DTtGKFRQUDm8svigJzZHzb/2xatPc6TzNvAIJ5GqOKDsGFYgAskjRulbR/vGsPKq3OPqtexnz327qYpP57RFyA==", "dev": true, "requires": { - "@babel/helper-plugin-utils": "^7.18.6" + "@babel/helper-plugin-utils": "^7.22.5" } }, "@babel/plugin-transform-shorthand-properties": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-shorthand-properties/-/plugin-transform-shorthand-properties-7.18.6.tgz", - "integrity": "sha512-eCLXXJqv8okzg86ywZJbRn19YJHU4XUa55oz2wbHhaQVn/MM+XhukiT7SYqp/7o00dg52Rj51Ny+Ecw4oyoygw==", + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-shorthand-properties/-/plugin-transform-shorthand-properties-7.22.5.tgz", + "integrity": "sha512-vM4fq9IXHscXVKzDv5itkO1X52SmdFBFcMIBZ2FRn2nqVYqw6dBexUgMvAjHW+KXpPPViD/Yo3GrDEBaRC0QYA==", "dev": true, "requires": { - "@babel/helper-plugin-utils": "^7.18.6" + "@babel/helper-plugin-utils": "^7.22.5" } }, "@babel/plugin-transform-spread": { - "version": "7.19.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-spread/-/plugin-transform-spread-7.19.0.tgz", - "integrity": "sha512-RsuMk7j6n+r752EtzyScnWkQyuJdli6LdO5Klv8Yx0OfPVTcQkIUfS8clx5e9yHXzlnhOZF3CbQ8C2uP5j074w==", + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-spread/-/plugin-transform-spread-7.22.5.tgz", + "integrity": "sha512-5ZzDQIGyvN4w8+dMmpohL6MBo+l2G7tfC/O2Dg7/hjpgeWvUx8FzfeOKxGog9IimPa4YekaQ9PlDqTLOljkcxg==", "dev": true, "requires": { - "@babel/helper-plugin-utils": "^7.19.0", - "@babel/helper-skip-transparent-expression-wrappers": "^7.18.9" + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/helper-skip-transparent-expression-wrappers": "^7.22.5" } }, "@babel/plugin-transform-sticky-regex": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-sticky-regex/-/plugin-transform-sticky-regex-7.18.6.tgz", - "integrity": "sha512-kfiDrDQ+PBsQDO85yj1icueWMfGfJFKN1KCkndygtu/C9+XUfydLC8Iv5UYJqRwy4zk8EcplRxEOeLyjq1gm6Q==", + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-sticky-regex/-/plugin-transform-sticky-regex-7.22.5.tgz", + "integrity": "sha512-zf7LuNpHG0iEeiyCNwX4j3gDg1jgt1k3ZdXBKbZSoA3BbGQGvMiSvfbZRR3Dr3aeJe3ooWFZxOOG3IRStYp2Bw==", "dev": true, "requires": { - "@babel/helper-plugin-utils": "^7.18.6" + "@babel/helper-plugin-utils": "^7.22.5" } }, "@babel/plugin-transform-template-literals": { - "version": "7.18.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-template-literals/-/plugin-transform-template-literals-7.18.9.tgz", - "integrity": "sha512-S8cOWfT82gTezpYOiVaGHrCbhlHgKhQt8XH5ES46P2XWmX92yisoZywf5km75wv5sYcXDUCLMmMxOLCtthDgMA==", + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-template-literals/-/plugin-transform-template-literals-7.22.5.tgz", + "integrity": "sha512-5ciOehRNf+EyUeewo8NkbQiUs4d6ZxiHo6BcBcnFlgiJfu16q0bQUw9Jvo0b0gBKFG1SMhDSjeKXSYuJLeFSMA==", "dev": true, "requires": { - "@babel/helper-plugin-utils": "^7.18.9" + "@babel/helper-plugin-utils": "^7.22.5" } }, "@babel/plugin-transform-typeof-symbol": { - "version": "7.18.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typeof-symbol/-/plugin-transform-typeof-symbol-7.18.9.tgz", - "integrity": "sha512-SRfwTtF11G2aemAZWivL7PD+C9z52v9EvMqH9BuYbabyPuKUvSWks3oCg6041pT925L4zVFqaVBeECwsmlguEw==", + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typeof-symbol/-/plugin-transform-typeof-symbol-7.22.5.tgz", + "integrity": "sha512-bYkI5lMzL4kPii4HHEEChkD0rkc+nvnlR6+o/qdqR6zrm0Sv/nodmyLhlq2DO0YKLUNd2VePmPRjJXSBh9OIdA==", "dev": true, "requires": { - "@babel/helper-plugin-utils": "^7.18.9" + "@babel/helper-plugin-utils": "^7.22.5" } }, "@babel/plugin-transform-unicode-escapes": { - "version": "7.18.10", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-escapes/-/plugin-transform-unicode-escapes-7.18.10.tgz", - "integrity": "sha512-kKAdAI+YzPgGY/ftStBFXTI1LZFju38rYThnfMykS+IXy8BVx+res7s2fxf1l8I35DV2T97ezo6+SGrXz6B3iQ==", + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-escapes/-/plugin-transform-unicode-escapes-7.22.5.tgz", + "integrity": "sha512-biEmVg1IYB/raUO5wT1tgfacCef15Fbzhkx493D3urBI++6hpJ+RFG4SrWMn0NEZLfvilqKf3QDrRVZHo08FYg==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.22.5" + } + }, + "@babel/plugin-transform-unicode-property-regex": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-property-regex/-/plugin-transform-unicode-property-regex-7.22.5.tgz", + "integrity": "sha512-HCCIb+CbJIAE6sXn5CjFQXMwkCClcOfPCzTlilJ8cUatfzwHlWQkbtV0zD338u9dZskwvuOYTuuaMaA8J5EI5A==", "dev": true, "requires": { - "@babel/helper-plugin-utils": "^7.18.9" + "@babel/helper-create-regexp-features-plugin": "^7.22.5", + "@babel/helper-plugin-utils": "^7.22.5" } }, "@babel/plugin-transform-unicode-regex": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-regex/-/plugin-transform-unicode-regex-7.18.6.tgz", - "integrity": "sha512-gE7A6Lt7YLnNOL3Pb9BNeZvi+d8l7tcRrG4+pwJjK9hD2xX4mEvjlQW60G9EEmfXVYRPv9VRQcyegIVHCql/AA==", + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-regex/-/plugin-transform-unicode-regex-7.22.5.tgz", + "integrity": "sha512-028laaOKptN5vHJf9/Arr/HiJekMd41hOEZYvNsrsXqJ7YPYuX2bQxh31fkZzGmq3YqHRJzYFFAVYvKfMPKqyg==", "dev": true, "requires": { - "@babel/helper-create-regexp-features-plugin": "^7.18.6", - "@babel/helper-plugin-utils": "^7.18.6" + "@babel/helper-create-regexp-features-plugin": "^7.22.5", + "@babel/helper-plugin-utils": "^7.22.5" + } + }, + "@babel/plugin-transform-unicode-sets-regex": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-sets-regex/-/plugin-transform-unicode-sets-regex-7.22.5.tgz", + "integrity": "sha512-lhMfi4FC15j13eKrh3DnYHjpGj6UKQHtNKTbtc1igvAhRy4+kLhV07OpLcsN0VgDEw/MjAvJO4BdMJsHwMhzCg==", + "dev": true, + "requires": { + "@babel/helper-create-regexp-features-plugin": "^7.22.5", + "@babel/helper-plugin-utils": "^7.22.5" } }, "@babel/preset-env": { - "version": "7.19.3", - "resolved": "https://registry.npmjs.org/@babel/preset-env/-/preset-env-7.19.3.tgz", - "integrity": "sha512-ziye1OTc9dGFOAXSWKUqQblYHNlBOaDl8wzqf2iKXJAltYiR3hKHUKmkt+S9PppW7RQpq4fFCrwwpIDj/f5P4w==", - "dev": true, - "requires": { - "@babel/compat-data": "^7.19.3", - "@babel/helper-compilation-targets": "^7.19.3", - "@babel/helper-plugin-utils": "^7.19.0", - "@babel/helper-validator-option": "^7.18.6", - "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": "^7.18.6", - "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": "^7.18.9", - "@babel/plugin-proposal-async-generator-functions": "^7.19.1", - "@babel/plugin-proposal-class-properties": "^7.18.6", - "@babel/plugin-proposal-class-static-block": "^7.18.6", - "@babel/plugin-proposal-dynamic-import": "^7.18.6", - "@babel/plugin-proposal-export-namespace-from": "^7.18.9", - "@babel/plugin-proposal-json-strings": "^7.18.6", - "@babel/plugin-proposal-logical-assignment-operators": "^7.18.9", - "@babel/plugin-proposal-nullish-coalescing-operator": "^7.18.6", - "@babel/plugin-proposal-numeric-separator": "^7.18.6", - "@babel/plugin-proposal-object-rest-spread": "^7.18.9", - "@babel/plugin-proposal-optional-catch-binding": "^7.18.6", - "@babel/plugin-proposal-optional-chaining": "^7.18.9", - "@babel/plugin-proposal-private-methods": "^7.18.6", - "@babel/plugin-proposal-private-property-in-object": "^7.18.6", - "@babel/plugin-proposal-unicode-property-regex": "^7.18.6", + "version": "7.22.9", + "resolved": "https://registry.npmjs.org/@babel/preset-env/-/preset-env-7.22.9.tgz", + "integrity": "sha512-wNi5H/Emkhll/bqPjsjQorSykrlfY5OWakd6AulLvMEytpKasMVUpVy8RL4qBIBs5Ac6/5i0/Rv0b/Fg6Eag/g==", + "dev": true, + "requires": { + "@babel/compat-data": "^7.22.9", + "@babel/helper-compilation-targets": "^7.22.9", + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/helper-validator-option": "^7.22.5", + "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": "^7.22.5", + "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": "^7.22.5", + "@babel/plugin-proposal-private-property-in-object": "7.21.0-placeholder-for-preset-env.2", "@babel/plugin-syntax-async-generators": "^7.8.4", "@babel/plugin-syntax-class-properties": "^7.12.13", "@babel/plugin-syntax-class-static-block": "^7.14.5", "@babel/plugin-syntax-dynamic-import": "^7.8.3", "@babel/plugin-syntax-export-namespace-from": "^7.8.3", - "@babel/plugin-syntax-import-assertions": "^7.18.6", + "@babel/plugin-syntax-import-assertions": "^7.22.5", + "@babel/plugin-syntax-import-attributes": "^7.22.5", + "@babel/plugin-syntax-import-meta": "^7.10.4", "@babel/plugin-syntax-json-strings": "^7.8.3", "@babel/plugin-syntax-logical-assignment-operators": "^7.10.4", "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3", @@ -10181,51 +10667,68 @@ "@babel/plugin-syntax-optional-chaining": "^7.8.3", "@babel/plugin-syntax-private-property-in-object": "^7.14.5", "@babel/plugin-syntax-top-level-await": "^7.14.5", - "@babel/plugin-transform-arrow-functions": "^7.18.6", - "@babel/plugin-transform-async-to-generator": "^7.18.6", - "@babel/plugin-transform-block-scoped-functions": "^7.18.6", - "@babel/plugin-transform-block-scoping": "^7.18.9", - "@babel/plugin-transform-classes": "^7.19.0", - "@babel/plugin-transform-computed-properties": "^7.18.9", - "@babel/plugin-transform-destructuring": "^7.18.13", - "@babel/plugin-transform-dotall-regex": "^7.18.6", - "@babel/plugin-transform-duplicate-keys": "^7.18.9", - "@babel/plugin-transform-exponentiation-operator": "^7.18.6", - "@babel/plugin-transform-for-of": "^7.18.8", - "@babel/plugin-transform-function-name": "^7.18.9", - "@babel/plugin-transform-literals": "^7.18.9", - "@babel/plugin-transform-member-expression-literals": "^7.18.6", - "@babel/plugin-transform-modules-amd": "^7.18.6", - "@babel/plugin-transform-modules-commonjs": "^7.18.6", - "@babel/plugin-transform-modules-systemjs": "^7.19.0", - "@babel/plugin-transform-modules-umd": "^7.18.6", - "@babel/plugin-transform-named-capturing-groups-regex": "^7.19.1", - "@babel/plugin-transform-new-target": "^7.18.6", - "@babel/plugin-transform-object-super": "^7.18.6", - "@babel/plugin-transform-parameters": "^7.18.8", - "@babel/plugin-transform-property-literals": "^7.18.6", - "@babel/plugin-transform-regenerator": "^7.18.6", - "@babel/plugin-transform-reserved-words": "^7.18.6", - "@babel/plugin-transform-shorthand-properties": "^7.18.6", - "@babel/plugin-transform-spread": "^7.19.0", - "@babel/plugin-transform-sticky-regex": "^7.18.6", - "@babel/plugin-transform-template-literals": "^7.18.9", - "@babel/plugin-transform-typeof-symbol": "^7.18.9", - "@babel/plugin-transform-unicode-escapes": "^7.18.10", - "@babel/plugin-transform-unicode-regex": "^7.18.6", + "@babel/plugin-syntax-unicode-sets-regex": "^7.18.6", + "@babel/plugin-transform-arrow-functions": "^7.22.5", + "@babel/plugin-transform-async-generator-functions": "^7.22.7", + "@babel/plugin-transform-async-to-generator": "^7.22.5", + "@babel/plugin-transform-block-scoped-functions": "^7.22.5", + "@babel/plugin-transform-block-scoping": "^7.22.5", + "@babel/plugin-transform-class-properties": "^7.22.5", + "@babel/plugin-transform-class-static-block": "^7.22.5", + "@babel/plugin-transform-classes": "^7.22.6", + "@babel/plugin-transform-computed-properties": "^7.22.5", + "@babel/plugin-transform-destructuring": "^7.22.5", + "@babel/plugin-transform-dotall-regex": "^7.22.5", + "@babel/plugin-transform-duplicate-keys": "^7.22.5", + "@babel/plugin-transform-dynamic-import": "^7.22.5", + "@babel/plugin-transform-exponentiation-operator": "^7.22.5", + "@babel/plugin-transform-export-namespace-from": "^7.22.5", + "@babel/plugin-transform-for-of": "^7.22.5", + "@babel/plugin-transform-function-name": "^7.22.5", + "@babel/plugin-transform-json-strings": "^7.22.5", + "@babel/plugin-transform-literals": "^7.22.5", + "@babel/plugin-transform-logical-assignment-operators": "^7.22.5", + "@babel/plugin-transform-member-expression-literals": "^7.22.5", + "@babel/plugin-transform-modules-amd": "^7.22.5", + "@babel/plugin-transform-modules-commonjs": "^7.22.5", + "@babel/plugin-transform-modules-systemjs": "^7.22.5", + "@babel/plugin-transform-modules-umd": "^7.22.5", + "@babel/plugin-transform-named-capturing-groups-regex": "^7.22.5", + "@babel/plugin-transform-new-target": "^7.22.5", + "@babel/plugin-transform-nullish-coalescing-operator": "^7.22.5", + "@babel/plugin-transform-numeric-separator": "^7.22.5", + "@babel/plugin-transform-object-rest-spread": "^7.22.5", + "@babel/plugin-transform-object-super": "^7.22.5", + "@babel/plugin-transform-optional-catch-binding": "^7.22.5", + "@babel/plugin-transform-optional-chaining": "^7.22.6", + "@babel/plugin-transform-parameters": "^7.22.5", + "@babel/plugin-transform-private-methods": "^7.22.5", + "@babel/plugin-transform-private-property-in-object": "^7.22.5", + "@babel/plugin-transform-property-literals": "^7.22.5", + "@babel/plugin-transform-regenerator": "^7.22.5", + "@babel/plugin-transform-reserved-words": "^7.22.5", + "@babel/plugin-transform-shorthand-properties": "^7.22.5", + "@babel/plugin-transform-spread": "^7.22.5", + "@babel/plugin-transform-sticky-regex": "^7.22.5", + "@babel/plugin-transform-template-literals": "^7.22.5", + "@babel/plugin-transform-typeof-symbol": "^7.22.5", + "@babel/plugin-transform-unicode-escapes": "^7.22.5", + "@babel/plugin-transform-unicode-property-regex": "^7.22.5", + "@babel/plugin-transform-unicode-regex": "^7.22.5", + "@babel/plugin-transform-unicode-sets-regex": "^7.22.5", "@babel/preset-modules": "^0.1.5", - "@babel/types": "^7.19.3", - "babel-plugin-polyfill-corejs2": "^0.3.3", - "babel-plugin-polyfill-corejs3": "^0.6.0", - "babel-plugin-polyfill-regenerator": "^0.4.1", - "core-js-compat": "^3.25.1", - "semver": "^6.3.0" + "@babel/types": "^7.22.5", + "babel-plugin-polyfill-corejs2": "^0.4.4", + "babel-plugin-polyfill-corejs3": "^0.8.2", + "babel-plugin-polyfill-regenerator": "^0.5.1", + "core-js-compat": "^3.31.0", + "semver": "^6.3.1" }, "dependencies": { "semver": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", - "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", "dev": true } } @@ -10244,9 +10747,9 @@ } }, "@babel/register": { - "version": "7.18.9", - "resolved": "https://registry.npmjs.org/@babel/register/-/register-7.18.9.tgz", - "integrity": "sha512-ZlbnXDcNYHMR25ITwwNKT88JiaukkdVj/nG7r3wnuXkOTHc60Uy05PwMCPre0hSkY68E6zK3xz+vUJSP2jWmcw==", + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/register/-/register-7.22.5.tgz", + "integrity": "sha512-vV6pm/4CijSQ8Y47RH5SopXzursN35RQINfGJkmOlcpAtGuf94miFvIPhCKGQN7WGIcsgG1BHEX2KVdTYwTwUQ==", "dev": true, "requires": { "clone-deep": "^4.0.1", @@ -10337,13 +10840,19 @@ } } }, + "@babel/regjsgen": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/@babel/regjsgen/-/regjsgen-0.8.0.tgz", + "integrity": "sha512-x/rqGMdzj+fWZvCOYForTghzbtqPDZ5gPwaoNGHdgDfF2QA/XZbCBp4Moo5scrkAMPhB7z26XM/AaHuIJdgauA==", + "dev": true + }, "@babel/runtime": { - "version": "7.19.0", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.19.0.tgz", - "integrity": "sha512-eR8Lo9hnDS7tqkO7NsV+mKvCmv5boaXFSZ70DnfhcgiEne8hv9oCEd36Klw74EtizEqLsy4YnW8UWwpBVolHZA==", + "version": "7.22.6", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.22.6.tgz", + "integrity": "sha512-wDb5pWm4WDdF6LFUde3Jl8WzPA+3ZbxYqkC6xAXuD3irdEHN1k0NfTRrJD8ZD378SJ61miMLCqIOXYhd8x+AJQ==", "dev": true, "requires": { - "regenerator-runtime": "^0.13.4" + "regenerator-runtime": "^0.13.11" } }, "@babel/runtime-corejs3": { @@ -10358,30 +10867,30 @@ } }, "@babel/template": { - "version": "7.18.10", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.18.10.tgz", - "integrity": "sha512-TI+rCtooWHr3QJ27kJxfjutghu44DLnasDMwpDqCXVTal9RLp3RSYNh4NdBrRP2cQAoG9A8juOQl6P6oZG4JxA==", + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.22.5.tgz", + "integrity": "sha512-X7yV7eiwAxdj9k94NEylvbVHLiVG1nvzCV2EAowhxLTwODV1jl9UzZ48leOC0sH7OnuHrIkllaBgneUykIcZaw==", "dev": true, "requires": { - "@babel/code-frame": "^7.18.6", - "@babel/parser": "^7.18.10", - "@babel/types": "^7.18.10" + "@babel/code-frame": "^7.22.5", + "@babel/parser": "^7.22.5", + "@babel/types": "^7.22.5" } }, "@babel/traverse": { - "version": "7.19.3", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.19.3.tgz", - "integrity": "sha512-qh5yf6149zhq2sgIXmwjnsvmnNQC2iw70UFjp4olxucKrWd/dvlUsBI88VSLUsnMNF7/vnOiA+nk1+yLoCqROQ==", - "dev": true, - "requires": { - "@babel/code-frame": "^7.18.6", - "@babel/generator": "^7.19.3", - "@babel/helper-environment-visitor": "^7.18.9", - "@babel/helper-function-name": "^7.19.0", - "@babel/helper-hoist-variables": "^7.18.6", - "@babel/helper-split-export-declaration": "^7.18.6", - "@babel/parser": "^7.19.3", - "@babel/types": "^7.19.3", + "version": "7.22.8", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.22.8.tgz", + "integrity": "sha512-y6LPR+wpM2I3qJrsheCTwhIinzkETbplIgPBbwvqPKc+uljeA5gP+3nP8irdYt1mjQaDnlIcG+dw8OjAco4GXw==", + "dev": true, + "requires": { + "@babel/code-frame": "^7.22.5", + "@babel/generator": "^7.22.7", + "@babel/helper-environment-visitor": "^7.22.5", + "@babel/helper-function-name": "^7.22.5", + "@babel/helper-hoist-variables": "^7.22.5", + "@babel/helper-split-export-declaration": "^7.22.6", + "@babel/parser": "^7.22.7", + "@babel/types": "^7.22.5", "debug": "^4.1.0", "globals": "^11.1.0" }, @@ -10410,13 +10919,13 @@ } }, "@babel/types": { - "version": "7.19.3", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.19.3.tgz", - "integrity": "sha512-hGCaQzIY22DJlDh9CH7NOxgKkFjBk0Cw9xDO1Xmh2151ti7wiGfQ3LauXzL4HP1fmFlTX6XjpRETTpUcv7wQLw==", + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.22.5.tgz", + "integrity": "sha512-zo3MIHGOkPOfoRXitsgHLjEXmlDaD/5KU1Uzuc9GNiZPhSqVxVRtxuPaSBZDsYZ9qV88AjtMtWW7ww98loJ9KA==", "dev": true, "requires": { - "@babel/helper-string-parser": "^7.18.10", - "@babel/helper-validator-identifier": "^7.19.1", + "@babel/helper-string-parser": "^7.22.5", + "@babel/helper-validator-identifier": "^7.22.5", "to-fast-properties": "^2.0.0" }, "dependencies": { @@ -10428,16 +10937,31 @@ } } }, + "@eslint-community/eslint-utils": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz", + "integrity": "sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA==", + "dev": true, + "requires": { + "eslint-visitor-keys": "^3.3.0" + } + }, + "@eslint-community/regexpp": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.6.2.tgz", + "integrity": "sha512-pPTNuaAG3QMH+buKyBIGJs3g/S5y0caxw0ygM3YyE6yJFySwiGGSzA+mM3KJ8QQvzeLh3blwgSonkFjgQdxzMw==", + "dev": true + }, "@eslint/eslintrc": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-1.3.2.tgz", - "integrity": "sha512-AXYd23w1S/bv3fTs3Lz0vjiYemS08jWkI3hYyS9I1ry+0f+Yjs1wm+sU0BS8qDOPrBIkp4qHYC16I8uVtpLajQ==", + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.1.tgz", + "integrity": "sha512-9t7ZA7NGGK8ckelF0PQCfcxIUzs1Md5rrO6U/c+FIQNanea5UZC0wqKXH4vHBccmu4ZJgZ2idtPeW7+Q2npOEA==", "dev": true, "requires": { "ajv": "^6.12.4", "debug": "^4.3.2", - "espree": "^9.4.0", - "globals": "^13.15.0", + "espree": "^9.6.0", + "globals": "^13.19.0", "ignore": "^5.2.0", "import-fresh": "^3.2.1", "js-yaml": "^4.1.0", @@ -10461,9 +10985,9 @@ } }, "globals": { - "version": "13.17.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-13.17.0.tgz", - "integrity": "sha512-1C+6nQRb1GwGMKm2dH/E7enFAMxGTmGI7/dEdhy/DNelv85w9B72t3uc5frtMNXIbzrarJJ/lTCjcaZwbLJmyw==", + "version": "13.20.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.20.0.tgz", + "integrity": "sha512-Qg5QtVkCy/kv3FUSlu4ukeZDVf9ee0iXLAUYX13gbR17bnejFTzr4iS9bY7kwCf1NztRNm1t91fjOiyx4CSwPQ==", "dev": true, "requires": { "type-fest": "^0.20.2" @@ -10478,15 +11002,6 @@ "argparse": "^2.0.1" } }, - "minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dev": true, - "requires": { - "brace-expansion": "^1.1.7" - } - }, "ms": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", @@ -10495,15 +11010,21 @@ } } }, + "@eslint/js": { + "version": "8.46.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.46.0.tgz", + "integrity": "sha512-a8TLtmPi8xzPkCbp/OGFUo5yhRkHM2Ko9kOWP4znJr0WAhWyThaw3PnwX4vOTWOAMsV2uRt32PPDcEz63esSaA==", + "dev": true + }, "@humanwhocodes/config-array": { - "version": "0.10.7", - "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.10.7.tgz", - "integrity": "sha512-MDl6D6sBsaV452/QSdX+4CXIjZhIcI0PELsxUjk4U828yd58vk3bTIvk/6w5FY+4hIy9sLW0sfrV7K7Kc++j/w==", + "version": "0.11.10", + "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.10.tgz", + "integrity": "sha512-KVVjQmNUepDVGXNuoRRdmmEjruj0KfiGSbS8LVc12LMsWDQzRXJ0qdhN8L8uUigKpfEHRhlaQFY0ib1tnUbNeQ==", "dev": true, "requires": { "@humanwhocodes/object-schema": "^1.2.1", "debug": "^4.1.1", - "minimatch": "^3.0.4" + "minimatch": "^3.0.5" }, "dependencies": { "debug": { @@ -10523,12 +11044,6 @@ } } }, - "@humanwhocodes/gitignore-to-minimatch": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@humanwhocodes/gitignore-to-minimatch/-/gitignore-to-minimatch-1.0.2.tgz", - "integrity": "sha512-rSqmMJDdLFUsyxR6FMtD00nfQKKLFb1kv+qBbOVKqErvloEIJLo5bDTJTQNTYgeyp78JsA7u/NPi5jT1GR/MuA==", - "dev": true - }, "@humanwhocodes/module-importer": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", @@ -10595,13 +11110,13 @@ "dev": true }, "@jridgewell/trace-mapping": { - "version": "0.3.15", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.15.tgz", - "integrity": "sha512-oWZNOULl+UbhsgB51uuZzglikfIKSUBO/M9W2OfEjn7cmqoAiCgmv9lyACTUacZwBz0ITnJ2NqjU8Tx0DHL88g==", + "version": "0.3.18", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.18.tgz", + "integrity": "sha512-w+niJYzMHdd7USdiH2U6869nqhD2nbfZXND5Yp93qIbEmnDNk7PD48o+YchRVpzMU7M6jVCbenTR7PA1FLQ9pA==", "dev": true, "requires": { - "@jridgewell/resolve-uri": "^3.0.3", - "@jridgewell/sourcemap-codec": "^1.4.10" + "@jridgewell/resolve-uri": "3.1.0", + "@jridgewell/sourcemap-codec": "1.4.14" } }, "@nicolo-ribaudo/chokidar-2": { @@ -10665,32 +11180,43 @@ } }, "@sinonjs/commons": { - "version": "1.8.3", - "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-1.8.3.tgz", - "integrity": "sha512-xkNcLAn/wZaX14RPlwizcKicDk9G3F8m2nU3L7Ukm5zBgTwiT0wsoFAHx9Jq56fJA1z/7uKGtCRu16sOUCLIHQ==", + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-3.0.0.tgz", + "integrity": "sha512-jXBtWAF4vmdNmZgD5FoKsVLv3rPgDnLgPbU84LIJ3otV44vJlDRokVng5v8NFJdCf/da9legHcKaRuZs4L7faA==", "dev": true, "requires": { "type-detect": "4.0.8" } }, "@sinonjs/fake-timers": { - "version": "9.1.2", - "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-9.1.2.tgz", - "integrity": "sha512-BPS4ynJW/o92PUR4wgriz2Ud5gpST5vz6GQfMixEDK0Z8ZCUv2M7SkBLykH56T++Xs+8ln9zTGbOvNGIe02/jw==", + "version": "10.3.0", + "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-10.3.0.tgz", + "integrity": "sha512-V4BG07kuYSUkTCSBHG8G8TNhM+F19jXFWnQtzj+we8DrkpSBCee9Z3Ms8yiGer/dlmhe35/Xdgyo3/0rQKg7YA==", "dev": true, "requires": { - "@sinonjs/commons": "^1.7.0" + "@sinonjs/commons": "^3.0.0" } }, "@sinonjs/samsam": { - "version": "6.1.1", - "resolved": "https://registry.npmjs.org/@sinonjs/samsam/-/samsam-6.1.1.tgz", - "integrity": "sha512-cZ7rKJTLiE7u7Wi/v9Hc2fs3Ucc3jrWeMgPHbbTCeVAB2S0wOBbYlkJVeNSL04i7fdhT8wIbDq1zhC/PXTD2SA==", + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/@sinonjs/samsam/-/samsam-8.0.0.tgz", + "integrity": "sha512-Bp8KUVlLp8ibJZrnvq2foVhP0IVX2CIprMJPK0vqGqgrDa0OHVKeZyBykqskkrdxV6yKBPmGasO8LVjAKR3Gew==", "dev": true, "requires": { - "@sinonjs/commons": "^1.6.0", + "@sinonjs/commons": "^2.0.0", "lodash.get": "^4.4.2", "type-detect": "^4.0.8" + }, + "dependencies": { + "@sinonjs/commons": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-2.0.0.tgz", + "integrity": "sha512-uLa0j859mMrg2slwQYdO/AkrOfmH+X6LTVmNTS9CqexuE2IvVORIkSpJLqePAbEnKJ77aMmCwr1NUZ57120Xcg==", + "dev": true, + "requires": { + "type-detect": "4.0.8" + } + } } }, "@sinonjs/text-encoding": { @@ -10759,12 +11285,6 @@ "@types/webidl-conversions": "*" } }, - "@ungap/promise-all-settled": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@ungap/promise-all-settled/-/promise-all-settled-1.1.2.tgz", - "integrity": "sha512-sL/cEvJWAnClXw0wHk85/2L0G6Sj8UB0Ctc1TEMbKSsmpRosqhwj9gWgFRZSrBr2f9tiXISwNhCPmlfqUqyb9Q==", - "dev": true - }, "@webassemblyjs/ast": { "version": "1.11.1", "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.11.1.tgz", @@ -10956,9 +11476,9 @@ } }, "acorn": { - "version": "8.8.0", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.8.0.tgz", - "integrity": "sha512-QOxyigPVrpZ2GXT+PFyZTl6TtOFc5egxHIP9IlQ+RbupQuX4RkT/Bee4/kQuC02Xkzg84JcT7oLYtDIQxp+v7w==", + "version": "8.10.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.10.0.tgz", + "integrity": "sha512-F0SAmZ8iUtS//m8DmCTA0jlh6TDKkHQyK6xc6V4KDTyZKA9dnvX9/3sRTVQrWm79glUAZbnmmNcdYwUIHWVybw==", "dev": true }, "acorn-import-assertions": { @@ -10969,6 +11489,13 @@ "peer": true, "requires": {} }, + "acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "requires": {} + }, "ajv": { "version": "6.12.6", "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", @@ -10981,11 +11508,41 @@ "uri-js": "^4.2.2" } }, + "ajv-formats": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-2.1.1.tgz", + "integrity": "sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==", + "dev": true, + "requires": { + "ajv": "^8.0.0" + }, + "dependencies": { + "ajv": { + "version": "8.12.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.12.0.tgz", + "integrity": "sha512-sRu1kpcO9yLtYxBKvqfTeh9KzZEwO3STyX1HT+4CaDzC6HpTGYhIhPIzj9XuKU7KYDwnaeh5hcOwjy1QuJzBPA==", + "dev": true, + "requires": { + "fast-deep-equal": "^3.1.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2", + "uri-js": "^4.2.2" + } + }, + "json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "dev": true + } + } + }, "ajv-keywords": { "version": "3.5.2", "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz", "integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==", "dev": true, + "peer": true, "requires": {} }, "ansi-colors": { @@ -11053,55 +11610,85 @@ "@babel/runtime-corejs3": "^7.10.2" } }, + "array-buffer-byte-length": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/array-buffer-byte-length/-/array-buffer-byte-length-1.0.0.tgz", + "integrity": "sha512-LPuwb2P+NrQw3XhxGc36+XSvuBPopovXYTR9Ew++Du9Yb/bx5AzBfrIsBoj0EZUifjQU+sHL21sseZ3jerWO/A==", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "is-array-buffer": "^3.0.1" + } + }, "array-flatten": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", "integrity": "sha1-ml9pkFGx5wczKPKgCJaLZOopVdI=" }, "array-includes": { - "version": "3.1.5", - "resolved": "https://registry.npmjs.org/array-includes/-/array-includes-3.1.5.tgz", - "integrity": "sha512-iSDYZMMyTPkiFasVqfuAQnWAYcvO/SeBSCGKePoEthjp4LEMTe4uLc7b025o4jAZpHhihh8xPo99TNWUWWkGDQ==", + "version": "3.1.6", + "resolved": "https://registry.npmjs.org/array-includes/-/array-includes-3.1.6.tgz", + "integrity": "sha512-sgTbLvL6cNnw24FnbaDyjmvddQ2ML8arZsgaJhoABMoplz/4QRhtrYS+alr1BUM1Bwp6dhx8vVCBSLG+StwOFw==", "dev": true, "requires": { "call-bind": "^1.0.2", "define-properties": "^1.1.4", - "es-abstract": "^1.19.5", - "get-intrinsic": "^1.1.1", + "es-abstract": "^1.20.4", + "get-intrinsic": "^1.1.3", "is-string": "^1.0.7" } }, - "array-union": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz", - "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==", - "dev": true + "array.prototype.findlastindex": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/array.prototype.findlastindex/-/array.prototype.findlastindex-1.2.2.tgz", + "integrity": "sha512-tb5thFFlUcp7NdNF6/MpDk/1r/4awWG1FIz3YqDf+/zJSTezBb+/5WViH41obXULHVpDzoiCLpJ/ZO9YbJMsdw==", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.4", + "es-abstract": "^1.20.4", + "es-shim-unscopables": "^1.0.0", + "get-intrinsic": "^1.1.3" + } }, "array.prototype.flat": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/array.prototype.flat/-/array.prototype.flat-1.3.0.tgz", - "integrity": "sha512-12IUEkHsAhA4DY5s0FPgNXIdc8VRSqD9Zp78a5au9abH/SOBrsp082JOWFNTjkMozh8mqcdiKuaLGhPeYztxSw==", + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/array.prototype.flat/-/array.prototype.flat-1.3.1.tgz", + "integrity": "sha512-roTU0KWIOmJ4DRLmwKd19Otg0/mT3qPNt0Qb3GWW8iObuZXxrjB/pzn0R3hqpRSWg4HCwqx+0vwOnWnvlOyeIA==", "dev": true, "requires": { "call-bind": "^1.0.2", - "define-properties": "^1.1.3", - "es-abstract": "^1.19.2", + "define-properties": "^1.1.4", + "es-abstract": "^1.20.4", "es-shim-unscopables": "^1.0.0" } }, "array.prototype.flatmap": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/array.prototype.flatmap/-/array.prototype.flatmap-1.3.0.tgz", - "integrity": "sha512-PZC9/8TKAIxcWKdyeb77EzULHPrIX/tIZebLJUQOMR1OwYosT8yggdfWScfTBCDj5utONvOuPQQumYsU2ULbkg==", + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/array.prototype.flatmap/-/array.prototype.flatmap-1.3.1.tgz", + "integrity": "sha512-8UGn9O1FDVvMNB0UlLv4voxRMze7+FpHyF5mSMRjWHUMlpoDViniy05870VlxhfgTnLbpuwTzvD76MTtWxB/mQ==", "dev": true, - "peer": true, "requires": { "call-bind": "^1.0.2", - "define-properties": "^1.1.3", - "es-abstract": "^1.19.2", + "define-properties": "^1.1.4", + "es-abstract": "^1.20.4", "es-shim-unscopables": "^1.0.0" } }, + "arraybuffer.prototype.slice": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/arraybuffer.prototype.slice/-/arraybuffer.prototype.slice-1.0.1.tgz", + "integrity": "sha512-09x0ZWFEjj4WD8PDbykUwo3t9arLn8NIzmmYEJFpYekOAQjpkGSyrQhNoRTcwwcFRu+ycWF78QZ63oWTqSjBcw==", + "dev": true, + "requires": { + "array-buffer-byte-length": "^1.0.0", + "call-bind": "^1.0.2", + "define-properties": "^1.2.0", + "get-intrinsic": "^1.2.1", + "is-array-buffer": "^3.0.2", + "is-shared-array-buffer": "^1.0.2" + } + }, "asap": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/asap/-/asap-2.0.6.tgz", @@ -11130,6 +11717,12 @@ "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", "dev": true }, + "available-typed-arrays": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.5.tgz", + "integrity": "sha512-DMD0KiN46eipeziST1LPP/STfDU0sufISXmjSgvVsoU2tqxctQeASejWcfNtxYKqETM1UxQ8sp2OrSBWpHY6sw==", + "dev": true + }, "axe-core": { "version": "4.4.3", "resolved": "https://registry.npmjs.org/axe-core/-/axe-core-4.4.3.tgz", @@ -11172,46 +11765,13 @@ } }, "babel-loader": { - "version": "8.2.5", - "resolved": "https://registry.npmjs.org/babel-loader/-/babel-loader-8.2.5.tgz", - "integrity": "sha512-OSiFfH89LrEMiWd4pLNqGz4CwJDtbs2ZVc+iGu2HrkRfPxId9F2anQj38IxWpmRfsUY0aBZYi1EFcd3mhtRMLQ==", + "version": "9.1.3", + "resolved": "https://registry.npmjs.org/babel-loader/-/babel-loader-9.1.3.tgz", + "integrity": "sha512-xG3ST4DglodGf8qSwv0MdeWLhrDsw/32QMdTO5T1ZIp9gQur0HkCyFs7Awskr10JKXFXwpAhiCuYX5oGXnRGbw==", "dev": true, "requires": { - "find-cache-dir": "^3.3.1", - "loader-utils": "^2.0.0", - "make-dir": "^3.1.0", - "schema-utils": "^2.6.5" - }, - "dependencies": { - "big.js": { - "version": "5.2.2", - "resolved": "https://registry.npmjs.org/big.js/-/big.js-5.2.2.tgz", - "integrity": "sha512-vyL2OymJxmarO8gxMr0mhChsO9QGwhynfuu4+MHTAW6czfq9humCB7rKpUjDd9YUiDPU4mzpyupFSvOClAwbmQ==", - "dev": true - }, - "emojis-list": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/emojis-list/-/emojis-list-3.0.0.tgz", - "integrity": "sha512-/kyM18EfinwXZbno9FyUGeFh87KC8HRQBQGildHZbEuRyWFOmv1U10o9BBp8XVZDVNNuQKyIGIu5ZYAAXJ0V2Q==", - "dev": true - }, - "json5": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.1.tgz", - "integrity": "sha512-1hqLFMSrGHRHxav9q9gNjJ5EXznIxGVO09xQRrwplcS8qs28pZ8s8hupZAmqDwZUmVZ2Qb2jnyPOWcDH8m8dlA==", - "dev": true - }, - "loader-utils": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-2.0.2.tgz", - "integrity": "sha512-TM57VeHptv569d/GKh6TAYdzKblwDNiumOdkFnejjD0XwTH87K90w3O7AiJRqdQoXygvi1VQTJTLGhJl7WqA7A==", - "dev": true, - "requires": { - "big.js": "^5.2.2", - "emojis-list": "^3.0.0", - "json5": "^2.1.2" - } - } + "find-cache-dir": "^4.0.0", + "schema-utils": "^4.0.0" } }, "babel-messages": { @@ -11223,51 +11783,42 @@ "babel-runtime": "^6.22.0" } }, - "babel-plugin-dynamic-import-node": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/babel-plugin-dynamic-import-node/-/babel-plugin-dynamic-import-node-2.3.3.tgz", - "integrity": "sha512-jZVI+s9Zg3IqA/kdi0i6UDCybUI3aSBLnglhYbSSjKlV7yF1F/5LWv8MakQmvYpnbJDS6fcBL2KzHSxNCMtWSQ==", - "dev": true, - "requires": { - "object.assign": "^4.1.0" - } - }, "babel-plugin-polyfill-corejs2": { - "version": "0.3.3", - "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs2/-/babel-plugin-polyfill-corejs2-0.3.3.tgz", - "integrity": "sha512-8hOdmFYFSZhqg2C/JgLUQ+t52o5nirNwaWM2B9LWteozwIvM14VSwdsCAUET10qT+kmySAlseadmfeeSWFCy+Q==", + "version": "0.4.5", + "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs2/-/babel-plugin-polyfill-corejs2-0.4.5.tgz", + "integrity": "sha512-19hwUH5FKl49JEsvyTcoHakh6BE0wgXLLptIyKZ3PijHc/Ci521wygORCUCCred+E/twuqRyAkE02BAWPmsHOg==", "dev": true, "requires": { - "@babel/compat-data": "^7.17.7", - "@babel/helper-define-polyfill-provider": "^0.3.3", - "semver": "^6.1.1" + "@babel/compat-data": "^7.22.6", + "@babel/helper-define-polyfill-provider": "^0.4.2", + "semver": "^6.3.1" }, "dependencies": { "semver": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", - "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", "dev": true } } }, "babel-plugin-polyfill-corejs3": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs3/-/babel-plugin-polyfill-corejs3-0.6.0.tgz", - "integrity": "sha512-+eHqR6OPcBhJOGgsIar7xoAB1GcSwVUA3XjAd7HJNzOXT4wv6/H7KIdA/Nc60cvUlDbKApmqNvD1B1bzOt4nyA==", + "version": "0.8.3", + "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs3/-/babel-plugin-polyfill-corejs3-0.8.3.tgz", + "integrity": "sha512-z41XaniZL26WLrvjy7soabMXrfPWARN25PZoriDEiLMxAp50AUW3t35BGQUMg5xK3UrpVTtagIDklxYa+MhiNA==", "dev": true, "requires": { - "@babel/helper-define-polyfill-provider": "^0.3.3", - "core-js-compat": "^3.25.1" + "@babel/helper-define-polyfill-provider": "^0.4.2", + "core-js-compat": "^3.31.0" } }, "babel-plugin-polyfill-regenerator": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-regenerator/-/babel-plugin-polyfill-regenerator-0.4.1.tgz", - "integrity": "sha512-NtQGmyQDXjQqQ+IzRkBVwEOz9lQ4zxAQZgoAYEtU9dJjnl1Oc98qnN7jcp+bE7O7aYzVpavXE3/VKXNzUbh7aw==", + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-regenerator/-/babel-plugin-polyfill-regenerator-0.5.2.tgz", + "integrity": "sha512-tAlOptU0Xj34V1Y2PNTL4Y0FOJMDB6bZmoW39FeCQIhigGLkqu3Fj6uiXpxIf6Ij274ENdYx64y6Au+ZKlb1IA==", "dev": true, "requires": { - "@babel/helper-define-polyfill-provider": "^0.3.3" + "@babel/helper-define-polyfill-provider": "^0.4.2" } }, "babel-runtime": { @@ -11342,26 +11893,21 @@ "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=", "dev": true }, - "base64-js": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", - "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==" - }, "body-parser": { - "version": "1.20.0", - "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.0.tgz", - "integrity": "sha512-DfJ+q6EPcGKZD1QWUjSpqp+Q7bDQTsQIF4zfUAtZ6qk+H/3/QRhg9CEp39ss+/T2vw0+HaidC0ecJj/DRLIaKg==", + "version": "1.20.2", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.2.tgz", + "integrity": "sha512-ml9pReCu3M61kGlqoTm2umSXTlRTuGTx0bfYj+uIUKKYycG5NtSbeetV3faSU6R7ajOPw0g/J1PvK4qNy7s5bA==", "requires": { "bytes": "3.1.2", - "content-type": "~1.0.4", + "content-type": "~1.0.5", "debug": "2.6.9", "depd": "2.0.0", "destroy": "1.2.0", "http-errors": "2.0.0", "iconv-lite": "0.4.24", "on-finished": "2.4.1", - "qs": "6.10.3", - "raw-body": "2.5.1", + "qs": "6.11.0", + "raw-body": "2.5.2", "type-is": "~1.6.18", "unpipe": "1.0.0" } @@ -11392,35 +11938,21 @@ "dev": true }, "browserslist": { - "version": "4.21.4", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.21.4.tgz", - "integrity": "sha512-CBHJJdDmgjl3daYjN5Cp5kbTf1mUhZoS+beLklHIvkOWscs83YAhLlF3Wsh/lciQYAcbBJgTOD44VtG31ZM4Hw==", + "version": "4.21.10", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.21.10.tgz", + "integrity": "sha512-bipEBdZfVH5/pwrvqc+Ub0kUPVfGUhlKxbvfD+z1BDnPEO/X98ruXGA1WP5ASpAFKan7Qr6j736IacbZQuAlKQ==", "dev": true, "requires": { - "caniuse-lite": "^1.0.30001400", - "electron-to-chromium": "^1.4.251", - "node-releases": "^2.0.6", - "update-browserslist-db": "^1.0.9" + "caniuse-lite": "^1.0.30001517", + "electron-to-chromium": "^1.4.477", + "node-releases": "^2.0.13", + "update-browserslist-db": "^1.0.11" } }, "bson": { - "version": "4.7.0", - "resolved": "https://registry.npmjs.org/bson/-/bson-4.7.0.tgz", - "integrity": "sha512-VrlEE4vuiO1WTpfof4VmaVolCVYkYTgB9iWgYNOrVlnifpME/06fhFRmONgBhClD5pFC1t9ZWqFUQEQAzY43bA==", - "requires": { - "buffer": "^5.6.0" - }, - "dependencies": { - "buffer": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", - "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", - "requires": { - "base64-js": "^1.3.1", - "ieee754": "^1.1.13" - } - } - } + "version": "5.4.0", + "resolved": "https://registry.npmjs.org/bson/-/bson-5.4.0.tgz", + "integrity": "sha512-WRZ5SQI5GfUuKnPTNmAYPiKIof3ORXAF4IRU5UcgmivNIon01rWQlw5RUH954dpu8yGL8T59YShVddIPaU/gFA==" }, "buffer-from": { "version": "1.1.2", @@ -11449,9 +11981,9 @@ "dev": true }, "caniuse-lite": { - "version": "1.0.30001416", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001416.tgz", - "integrity": "sha512-06wzzdAkCPZO+Qm4e/eNghZBDfVNDsCgw33T27OwBH9unE9S478OYw//Q2L7Npf/zBzs7rjZOszIFQkwQKAEqA==", + "version": "1.0.30001519", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001519.tgz", + "integrity": "sha512-0QHgqR+Jv4bxHMp8kZ1Kn8CH55OikjKJ6JmKkZYP1F3D7w+lnFXF70nG5eNfsZS89jadi5Ywy5UCSKLAglIRkg==", "dev": true }, "chalk": { @@ -11578,6 +12110,12 @@ "integrity": "sha512-7j2y+40w61zy6YC2iRNpUe/NwhNyoXrYpHMrSunaMG64nRnaf96zO/KMQR4OyN/UnE5KLyEBnKHd4aG3rskjpQ==", "dev": true }, + "common-path-prefix": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/common-path-prefix/-/common-path-prefix-3.0.0.tgz", + "integrity": "sha512-QE33hToZseCH3jS0qN96O/bSh3kaw/h+Tq7ngyY9eWDUnTlTNUyqfqvCXioLe5Na5jFsL78ra/wuBU4iuEgd4w==", + "dev": true + }, "commondir": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/commondir/-/commondir-1.0.1.tgz", @@ -11618,9 +12156,9 @@ } }, "content-type": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.4.tgz", - "integrity": "sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA==" + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==" }, "convert-source-map": { "version": "1.8.0", @@ -11642,9 +12180,9 @@ "integrity": "sha1-4wOogrNCzD7oylE6eZmXNNqzriw=" }, "cookiejar": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/cookiejar/-/cookiejar-2.1.3.tgz", - "integrity": "sha512-JxbCBUdrfr6AQjOXrxoTvAMJO4HBTUIlBzslcJPAz+/KT8yk53fXun51u+RenNYvad/+Vc2DIz5o9UxlCDymFQ==", + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/cookiejar/-/cookiejar-2.1.4.tgz", + "integrity": "sha512-LDx6oHrK+PhzLKJU9j5S7/Y3jM/mUHvD/DeI1WQmJn652iPC5Y4TBzC9l+5OMOXlyTTA+SmVUPm0HQUwpD5Jqw==", "dev": true }, "core-js": { @@ -11654,12 +12192,12 @@ "dev": true }, "core-js-compat": { - "version": "3.25.5", - "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.25.5.tgz", - "integrity": "sha512-ovcyhs2DEBUIE0MGEKHP4olCUW/XYte3Vroyxuh38rD1wAO4dHohsovUC4eAOuzFxE6b+RXvBU3UZ9o0YhUTkA==", + "version": "3.32.0", + "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.32.0.tgz", + "integrity": "sha512-7a9a3D1k4UCVKnLhrgALyFcP7YCsLOQIxPd0dKjf/6GuPcgyiGP70ewWdCGrSK7evyhymi0qO4EqCmSJofDeYw==", "dev": true, "requires": { - "browserslist": "^4.21.4" + "browserslist": "^4.21.9" } }, "core-js-pure": { @@ -11749,9 +12287,9 @@ } }, "define-properties": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.1.4.tgz", - "integrity": "sha512-uckOqKcfaVvtBdsVkdPv3XjveQJsNQqmhXgRi8uhvWWuPYZCNlzT8qAyblUgNoXdHdjMTzAqeGjAoli8f+bzPA==", + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.0.tgz", + "integrity": "sha512-xvqAVKGfT1+UAvPwKTVw/njhdQ8ZhXK4lI0bCIuCMrp2up9nPnaDftrLtmpTazqd1o+UY4zgzU+avtMbDP+ldA==", "dev": true, "requires": { "has-property-descriptors": "^1.0.0", @@ -11764,11 +12302,6 @@ "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", "dev": true }, - "denque": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/denque/-/denque-2.1.0.tgz", - "integrity": "sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw==" - }, "depd": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", @@ -11789,9 +12322,9 @@ } }, "dezalgo": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/dezalgo/-/dezalgo-1.0.3.tgz", - "integrity": "sha512-K7i4zNfT2kgQz3GylDw40ot9GAE47sFZ9EXHFSPP6zONLgH6kWXE0KWJchkbQJLBkRazq4APwZ4OwiFFlT95OQ==", + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/dezalgo/-/dezalgo-1.0.4.tgz", + "integrity": "sha512-rXSP0bf+5n0Qonsb+SVVfNfIsimO4HEtmnIpPHY8Q1UCzKlQrDMfdobr8nJOOsRgWCyMRqeSBQzmWUMq7zvVig==", "dev": true, "requires": { "asap": "^2.0.0", @@ -11804,15 +12337,6 @@ "integrity": "sha512-/VTCrvm5Z0JGty/BWHljh+BAiw3IK+2j87NGMu8Nwc/f48WoDAC395uomO9ZD117ZOBaHmkX1oyLvkVM/aIT3w==", "dev": true }, - "dir-glob": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", - "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==", - "dev": true, - "requires": { - "path-type": "^4.0.0" - } - }, "doctrine": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", @@ -11828,9 +12352,9 @@ "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==" }, "electron-to-chromium": { - "version": "1.4.272", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.272.tgz", - "integrity": "sha512-KS6gPPGNrzpVv9HzFVq+Etd0AjZEPr5pvaTBn2yD6KV4+cKW4I0CJoJNgmTG6gUQPAMZ4wIPtcOuoou3qFAZCA==", + "version": "1.4.484", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.484.tgz", + "integrity": "sha512-nO3ZEomTK2PO/3TUXgEx0A97xZTpKVf4p427lABHuCpT1IQ2N+njVh29DkQkCk6Q4m2wjU+faK4xAcfFndwjvw==", "dev": true }, "emoji-regex": { @@ -11857,35 +12381,50 @@ } }, "es-abstract": { - "version": "1.20.3", - "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.20.3.tgz", - "integrity": "sha512-AyrnaKVpMzljIdwjzrj+LxGmj8ik2LckwXacHqrJJ/jxz6dDDBcZ7I7nlHM0FvEW8MfbWJwOd+yT2XzYW49Frw==", + "version": "1.22.1", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.22.1.tgz", + "integrity": "sha512-ioRRcXMO6OFyRpyzV3kE1IIBd4WG5/kltnzdxSCqoP8CMGs/Li+M1uF5o7lOkZVFjDs+NLesthnF66Pg/0q0Lw==", "dev": true, "requires": { + "array-buffer-byte-length": "^1.0.0", + "arraybuffer.prototype.slice": "^1.0.1", + "available-typed-arrays": "^1.0.5", "call-bind": "^1.0.2", + "es-set-tostringtag": "^2.0.1", "es-to-primitive": "^1.2.1", - "function-bind": "^1.1.1", "function.prototype.name": "^1.1.5", - "get-intrinsic": "^1.1.3", + "get-intrinsic": "^1.2.1", "get-symbol-description": "^1.0.0", + "globalthis": "^1.0.3", + "gopd": "^1.0.1", "has": "^1.0.3", "has-property-descriptors": "^1.0.0", + "has-proto": "^1.0.1", "has-symbols": "^1.0.3", - "internal-slot": "^1.0.3", - "is-callable": "^1.2.6", + "internal-slot": "^1.0.5", + "is-array-buffer": "^3.0.2", + "is-callable": "^1.2.7", "is-negative-zero": "^2.0.2", "is-regex": "^1.1.4", "is-shared-array-buffer": "^1.0.2", "is-string": "^1.0.7", + "is-typed-array": "^1.1.10", "is-weakref": "^1.0.2", - "object-inspect": "^1.12.2", + "object-inspect": "^1.12.3", "object-keys": "^1.1.1", "object.assign": "^4.1.4", - "regexp.prototype.flags": "^1.4.3", + "regexp.prototype.flags": "^1.5.0", + "safe-array-concat": "^1.0.0", "safe-regex-test": "^1.0.0", - "string.prototype.trimend": "^1.0.5", - "string.prototype.trimstart": "^1.0.5", - "unbox-primitive": "^1.0.2" + "string.prototype.trim": "^1.2.7", + "string.prototype.trimend": "^1.0.6", + "string.prototype.trimstart": "^1.0.6", + "typed-array-buffer": "^1.0.0", + "typed-array-byte-length": "^1.0.0", + "typed-array-byte-offset": "^1.0.0", + "typed-array-length": "^1.0.4", + "unbox-primitive": "^1.0.2", + "which-typed-array": "^1.1.10" } }, "es-module-lexer": { @@ -11895,6 +12434,17 @@ "dev": true, "peer": true }, + "es-set-tostringtag": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.0.1.tgz", + "integrity": "sha512-g3OMbtlwY3QewlqAiMLI47KywjWZoEytKr8pf6iTC8uJq5bIAH52Z9pnQ8pVL6whrCto53JZDuUIsifGeLorTg==", + "dev": true, + "requires": { + "get-intrinsic": "^1.1.3", + "has": "^1.0.3", + "has-tostringtag": "^1.0.0" + } + }, "es-shim-unscopables": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/es-shim-unscopables/-/es-shim-unscopables-1.0.0.tgz", @@ -11933,49 +12483,47 @@ "dev": true }, "eslint": { - "version": "8.24.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.24.0.tgz", - "integrity": "sha512-dWFaPhGhTAiPcCgm3f6LI2MBWbogMnTJzFBbhXVRQDJPkr9pGZvVjlVfXd+vyDcWPA2Ic9L2AXPIQM0+vk/cSQ==", + "version": "8.46.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.46.0.tgz", + "integrity": "sha512-cIO74PvbW0qU8e0mIvk5IV3ToWdCq5FYG6gWPHHkx6gNdjlbAYvtfHmlCMXxjcoVaIdwy/IAt3+mDkZkfvb2Dg==", "dev": true, "requires": { - "@eslint/eslintrc": "^1.3.2", - "@humanwhocodes/config-array": "^0.10.5", - "@humanwhocodes/gitignore-to-minimatch": "^1.0.2", + "@eslint-community/eslint-utils": "^4.2.0", + "@eslint-community/regexpp": "^4.6.1", + "@eslint/eslintrc": "^2.1.1", + "@eslint/js": "^8.46.0", + "@humanwhocodes/config-array": "^0.11.10", "@humanwhocodes/module-importer": "^1.0.1", - "ajv": "^6.10.0", + "@nodelib/fs.walk": "^1.2.8", + "ajv": "^6.12.4", "chalk": "^4.0.0", "cross-spawn": "^7.0.2", "debug": "^4.3.2", "doctrine": "^3.0.0", "escape-string-regexp": "^4.0.0", - "eslint-scope": "^7.1.1", - "eslint-utils": "^3.0.0", - "eslint-visitor-keys": "^3.3.0", - "espree": "^9.4.0", - "esquery": "^1.4.0", + "eslint-scope": "^7.2.2", + "eslint-visitor-keys": "^3.4.2", + "espree": "^9.6.1", + "esquery": "^1.4.2", "esutils": "^2.0.2", "fast-deep-equal": "^3.1.3", "file-entry-cache": "^6.0.1", "find-up": "^5.0.0", - "glob-parent": "^6.0.1", - "globals": "^13.15.0", - "globby": "^11.1.0", - "grapheme-splitter": "^1.0.4", + "glob-parent": "^6.0.2", + "globals": "^13.19.0", + "graphemer": "^1.4.0", "ignore": "^5.2.0", - "import-fresh": "^3.0.0", "imurmurhash": "^0.1.4", "is-glob": "^4.0.0", - "js-sdsl": "^4.1.4", + "is-path-inside": "^3.0.3", "js-yaml": "^4.1.0", "json-stable-stringify-without-jsonify": "^1.0.1", "levn": "^0.4.1", "lodash.merge": "^4.6.2", "minimatch": "^3.1.2", "natural-compare": "^1.4.0", - "optionator": "^0.9.1", - "regexpp": "^3.2.0", + "optionator": "^0.9.3", "strip-ansi": "^6.0.1", - "strip-json-comments": "^3.1.0", "text-table": "^0.2.0" }, "dependencies": { @@ -12060,9 +12608,9 @@ } }, "globals": { - "version": "13.17.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-13.17.0.tgz", - "integrity": "sha512-1C+6nQRb1GwGMKm2dH/E7enFAMxGTmGI7/dEdhy/DNelv85w9B72t3uc5frtMNXIbzrarJJ/lTCjcaZwbLJmyw==", + "version": "13.20.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.20.0.tgz", + "integrity": "sha512-Qg5QtVkCy/kv3FUSlu4ukeZDVf9ee0iXLAUYX13gbR17bnejFTzr4iS9bY7kwCf1NztRNm1t91fjOiyx4CSwPQ==", "dev": true, "requires": { "type-fest": "^0.20.2" @@ -12083,15 +12631,6 @@ "argparse": "^2.0.1" } }, - "minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dev": true, - "requires": { - "brace-expansion": "^1.1.7" - } - }, "ms": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", @@ -12156,13 +12695,14 @@ } }, "eslint-import-resolver-node": { - "version": "0.3.6", - "resolved": "https://registry.npmjs.org/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.6.tgz", - "integrity": "sha512-0En0w03NRVMn9Uiyn8YRPDKvWjxCWkslUEhGNTdGx15RvPJYQ+lbOlqrlNI2vEAs4pDYK4f/HN2TbDmk5TP0iw==", + "version": "0.3.7", + "resolved": "https://registry.npmjs.org/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.7.tgz", + "integrity": "sha512-gozW2blMLJCeFpBwugLTGyvVjNoeo1knonXAcatC6bjPBZitotxdWf7Gimr25N4c0AAOo4eOUfaG82IJPDpqCA==", "dev": true, "requires": { "debug": "^3.2.7", - "resolve": "^1.20.0" + "is-core-module": "^2.11.0", + "resolve": "^1.22.1" }, "dependencies": { "debug": { @@ -12177,9 +12717,9 @@ } }, "eslint-module-utils": { - "version": "2.7.4", - "resolved": "https://registry.npmjs.org/eslint-module-utils/-/eslint-module-utils-2.7.4.tgz", - "integrity": "sha512-j4GT+rqzCoRKHwURX7pddtIPGySnX9Si/cgMI5ztrcqOPtk5dDEeZ34CQVPphnqkJytlc97Vuk05Um2mJ3gEQA==", + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/eslint-module-utils/-/eslint-module-utils-2.8.0.tgz", + "integrity": "sha512-aWajIYfsqCKRDgUfjEXNN/JlrzauMuSEy5sbd7WXbtW3EH6A6MpwEh42c7qD+MqQo9QMJ6fWLAeIJynx0g6OAw==", "dev": true, "requires": { "debug": "^3.2.7" @@ -12206,26 +12746,40 @@ } }, "eslint-plugin-import": { - "version": "2.26.0", - "resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.26.0.tgz", - "integrity": "sha512-hYfi3FXaM8WPLf4S1cikh/r4IxnO6zrhZbEGz2b660EJRbuxgpDS5gkCuYgGWg2xxh2rBuIr4Pvhve/7c31koA==", + "version": "2.28.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.28.0.tgz", + "integrity": "sha512-B8s/n+ZluN7sxj9eUf7/pRFERX0r5bnFA2dCaLHy2ZeaQEAz0k+ZZkFWRFHJAqxfxQDx6KLv9LeIki7cFdwW+Q==", "dev": true, "requires": { - "array-includes": "^3.1.4", - "array.prototype.flat": "^1.2.5", - "debug": "^2.6.9", + "array-includes": "^3.1.6", + "array.prototype.findlastindex": "^1.2.2", + "array.prototype.flat": "^1.3.1", + "array.prototype.flatmap": "^1.3.1", + "debug": "^3.2.7", "doctrine": "^2.1.0", - "eslint-import-resolver-node": "^0.3.6", - "eslint-module-utils": "^2.7.3", + "eslint-import-resolver-node": "^0.3.7", + "eslint-module-utils": "^2.8.0", "has": "^1.0.3", - "is-core-module": "^2.8.1", + "is-core-module": "^2.12.1", "is-glob": "^4.0.3", "minimatch": "^3.1.2", - "object.values": "^1.1.5", - "resolve": "^1.22.0", - "tsconfig-paths": "^3.14.1" + "object.fromentries": "^2.0.6", + "object.groupby": "^1.0.0", + "object.values": "^1.1.6", + "resolve": "^1.22.3", + "semver": "^6.3.1", + "tsconfig-paths": "^3.14.2" }, "dependencies": { + "debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dev": true, + "requires": { + "ms": "^2.1.1" + } + }, "doctrine": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz", @@ -12235,14 +12789,11 @@ "esutils": "^2.0.2" } }, - "minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dev": true, - "requires": { - "brace-expansion": "^1.1.7" - } + "semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true } } }, @@ -12268,16 +12819,6 @@ "semver": "^6.3.0" }, "dependencies": { - "minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dev": true, - "peer": true, - "requires": { - "brace-expansion": "^1.1.7" - } - }, "semver": { "version": "6.3.0", "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", @@ -12320,16 +12861,6 @@ "esutils": "^2.0.2" } }, - "minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dev": true, - "peer": true, - "requires": { - "brace-expansion": "^1.1.7" - } - }, "resolve": { "version": "2.0.0-next.4", "resolved": "https://registry.npmjs.org/resolve/-/resolve-2.0.0-next.4.tgz", @@ -12366,56 +12897,30 @@ "dev": true }, "eslint-scope": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.1.1.tgz", - "integrity": "sha512-QKQM/UXpIiHcLqJ5AOyIW7XZmzjkzQXYE54n1++wb0u9V/abW3l9uQnxX8Z5Xd18xyKIMTUAyQ0k1e8pz6LUrw==", + "version": "7.2.2", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.2.tgz", + "integrity": "sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==", "dev": true, "requires": { "esrecurse": "^4.3.0", "estraverse": "^5.2.0" } }, - "eslint-utils": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/eslint-utils/-/eslint-utils-3.0.0.tgz", - "integrity": "sha512-uuQC43IGctw68pJA1RgbQS8/NP7rch6Cwd4j3ZBtgo4/8Flj4eGE7ZYSZRN3iq5pVUv6GPdW5Z1RFleo84uLDA==", - "dev": true, - "requires": { - "eslint-visitor-keys": "^2.0.0" - }, - "dependencies": { - "eslint-visitor-keys": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-2.1.0.tgz", - "integrity": "sha512-0rSmRBzXgDzIsD6mGdJgevzgezI534Cer5L/vyMX0kHzT/jiB43jRhd9YUlMGYLQy2zprNmoT8qasCGtY+QaKw==", - "dev": true - } - } - }, "eslint-visitor-keys": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.3.0.tgz", - "integrity": "sha512-mQ+suqKJVyeuwGYHAdjMFqjCyfl8+Ldnxuyp3ldiMBFKkvytrXUZWaiPCEav8qDHKty44bD+qV1IP4T+w+xXRA==", + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.2.tgz", + "integrity": "sha512-8drBzUEyZ2llkpCA67iYrgEssKDUu68V8ChqqOfFupIaG/LCVPUT+CoGJpT77zJprs4T/W7p07LP7zAIMuweVw==", "dev": true }, "espree": { - "version": "9.4.0", - "resolved": "https://registry.npmjs.org/espree/-/espree-9.4.0.tgz", - "integrity": "sha512-DQmnRpLj7f6TgN/NYb0MTzJXL+vJF9h3pHy4JhCIs3zwcgez8xmGg3sXHcEO97BrmO2OSvCwMdfdlyl+E9KjOw==", + "version": "9.6.1", + "resolved": "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz", + "integrity": "sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==", "dev": true, "requires": { - "acorn": "^8.8.0", + "acorn": "^8.9.0", "acorn-jsx": "^5.3.2", - "eslint-visitor-keys": "^3.3.0" - }, - "dependencies": { - "acorn-jsx": { - "version": "5.3.2", - "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", - "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", - "dev": true, - "requires": {} - } + "eslint-visitor-keys": "^3.4.1" } }, "esprima": { @@ -12425,9 +12930,9 @@ "dev": true }, "esquery": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.4.0.tgz", - "integrity": "sha512-cCDispWt5vHHtwMY2YrAQ4ibFkAL8RbH5YGBnZBc90MolvvfkkQcJro/aZiAQUlQ3qgrYS6D6v8Gc5G5CQsc9w==", + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.5.0.tgz", + "integrity": "sha512-YQLXUplAwJgCydQ78IMJywZCceoqk1oH01OERdSAJc/7U2AylwjhSCLDEtqwg811idIS/9fIU5GjG73IgjKMVg==", "dev": true, "requires": { "estraverse": "^5.1.0" @@ -12467,13 +12972,13 @@ "peer": true }, "express": { - "version": "4.18.1", - "resolved": "https://registry.npmjs.org/express/-/express-4.18.1.tgz", - "integrity": "sha512-zZBcOX9TfehHQhtupq57OF8lFZ3UZi08Y97dwFCkD8p9d/d2Y3M+ykKcwaMDEL+4qyUolgBDX6AblpR3fL212Q==", + "version": "4.18.2", + "resolved": "https://registry.npmjs.org/express/-/express-4.18.2.tgz", + "integrity": "sha512-5/PsL6iGPdfQ/lKM1UuielYgv3BUoJfz1aUwU9vHZ+J7gyvwdQXFEBIEIaxeGf0GIcreATNyBExtalisDbuMqQ==", "requires": { "accepts": "~1.3.8", "array-flatten": "1.1.1", - "body-parser": "1.20.0", + "body-parser": "1.20.1", "content-disposition": "0.5.4", "content-type": "~1.0.4", "cookie": "0.5.0", @@ -12492,7 +12997,7 @@ "parseurl": "~1.3.3", "path-to-regexp": "0.1.7", "proxy-addr": "~2.0.7", - "qs": "6.10.3", + "qs": "6.11.0", "range-parser": "~1.2.1", "safe-buffer": "5.2.1", "send": "0.18.0", @@ -12504,6 +13009,36 @@ "vary": "~1.1.2" }, "dependencies": { + "body-parser": { + "version": "1.20.1", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.1.tgz", + "integrity": "sha512-jWi7abTbYwajOytWCQc37VulmWiRae5RyTpaCyDcS5/lMdtwSz5lOpDE67srw/HYe35f1z3fDQw+3txg7gNtWw==", + "requires": { + "bytes": "3.1.2", + "content-type": "~1.0.4", + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", + "on-finished": "2.4.1", + "qs": "6.11.0", + "raw-body": "2.5.1", + "type-is": "~1.6.18", + "unpipe": "1.0.0" + } + }, + "raw-body": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.1.tgz", + "integrity": "sha512-qqJBtEyVgS0ZmPGdCFPWJ3FreoqvG4MVQln/kCgF7Olq95IbOp0/BWyMwbdtn4VTvkM8Y7khCQ2Xgk/tcrCXig==", + "requires": { + "bytes": "3.1.2", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", + "unpipe": "1.0.0" + } + }, "safe-buffer": { "version": "5.2.1", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", @@ -12517,31 +13052,6 @@ "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", "dev": true }, - "fast-glob": { - "version": "3.2.12", - "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.2.12.tgz", - "integrity": "sha512-DVj4CQIYYow0BlaelwK1pHl5n5cRSJfM60UA0zK891sVInoPri2Ekj7+e1CT3/3qxXenpI+nBBmQAcJPJgaj4w==", - "dev": true, - "requires": { - "@nodelib/fs.stat": "^2.0.2", - "@nodelib/fs.walk": "^1.2.3", - "glob-parent": "^5.1.2", - "merge2": "^1.3.0", - "micromatch": "^4.0.4" - }, - "dependencies": { - "micromatch": { - "version": "4.0.5", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.5.tgz", - "integrity": "sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==", - "dev": true, - "requires": { - "braces": "^3.0.2", - "picomatch": "^2.3.1" - } - } - } - }, "fast-json-stable-stringify": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", @@ -12561,9 +13071,9 @@ "dev": true }, "fastq": { - "version": "1.13.0", - "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.13.0.tgz", - "integrity": "sha512-YpkpUnK8od0o1hmeSc7UUs/eB/vIPWJYjKck2QKIzAf71Vm1AAQ3EbuZB3g2JIy+pg+ERD0vqI79KyZiB2e2Nw==", + "version": "1.15.0", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.15.0.tgz", + "integrity": "sha512-wBrocU2LCXXa+lWBt8RoIRD89Fi8OdABODa/kEnyeyjS5aZO5/GNvI5sEINADqP/h8M29UHTHUb53sUu5Ihqdw==", "dev": true, "requires": { "reusify": "^1.0.4" @@ -12623,14 +13133,58 @@ } }, "find-cache-dir": { - "version": "3.3.2", - "resolved": "https://registry.npmjs.org/find-cache-dir/-/find-cache-dir-3.3.2.tgz", - "integrity": "sha512-wXZV5emFEjrridIgED11OoUKLxiYjAcqot/NJdAkOhlJ+vGzwhOAfcG5OX1jP+S0PcjEn8bdMJv+g2jwQ3Onig==", + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/find-cache-dir/-/find-cache-dir-4.0.0.tgz", + "integrity": "sha512-9ZonPT4ZAK4a+1pUPVPZJapbi7O5qbbJPdYw/NOQWZZbVLdDTYM3A4R9z/DpAM08IDaFGsvPgiGZ82WEwUDWjg==", "dev": true, "requires": { - "commondir": "^1.0.1", - "make-dir": "^3.0.2", - "pkg-dir": "^4.1.0" + "common-path-prefix": "^3.0.0", + "pkg-dir": "^7.0.0" + } + }, + "find-up": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-6.3.0.tgz", + "integrity": "sha512-v2ZsoEuVHYy8ZIlYqwPe/39Cy+cFDzp4dXPaxNvkEuouymu+2Jbz0PxpKarJHYJTmv2HWT3O382qY8l4jMWthw==", + "dev": true, + "requires": { + "locate-path": "^7.1.0", + "path-exists": "^5.0.0" + }, + "dependencies": { + "locate-path": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-7.2.0.tgz", + "integrity": "sha512-gvVijfZvn7R+2qyPX8mAuKcFGDf6Nc61GdvGafQsHL0sBIxfKzA+usWn4GFC/bk+QdwPUD4kWFJLhElipq+0VA==", + "dev": true, + "requires": { + "p-locate": "^6.0.0" + } + }, + "p-limit": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-4.0.0.tgz", + "integrity": "sha512-5b0R4txpzjPWVw/cXXUResoD4hb6U/x9BH08L7nw+GN1sezDzPdxeRvpc9c433fZhBan/wusjbCsqwqm4EIBIQ==", + "dev": true, + "requires": { + "yocto-queue": "^1.0.0" + } + }, + "p-locate": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-6.0.0.tgz", + "integrity": "sha512-wPrq66Llhl7/4AGC6I+cqxT07LhXvWL08LNXz1fENOw0Ap4sRZZ/gZpTTJ5jpurzzzfS2W/Ge9BY3LgLjCShcw==", + "dev": true, + "requires": { + "p-limit": "^4.0.0" + } + }, + "yocto-queue": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-1.0.0.tgz", + "integrity": "sha512-9bnSc/HEW2uRy67wc+T8UwauLuPJVn28jb+GtJY16iiKWyvmYJRXVT4UamsAEGQfPohgr2q4Tq0sQbQlxTfi1g==", + "dev": true + } } }, "flat": { @@ -12666,6 +13220,15 @@ "integrity": "sha512-5nqDSxl8nn5BSNxyR3n4I6eDmbolI6WT+QqR547RwxQapgjQBmtktdP+HTBb/a/zLsbzERTONyUB5pefh5TtjQ==", "dev": true }, + "for-each": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.3.tgz", + "integrity": "sha512-jqYfLp7mo9vIyQf8ykW2v7A+2N4QjeCeI5+Dz9XraiO1ign81wjiH7Fb9vSOWvQfNtmSa4H2RoQTrrXivdUZmw==", + "dev": true, + "requires": { + "is-callable": "^1.1.3" + } + }, "form-data": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", @@ -12678,23 +13241,15 @@ } }, "formidable": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/formidable/-/formidable-2.0.1.tgz", - "integrity": "sha512-rjTMNbp2BpfQShhFbR3Ruk3qk2y9jKpvMW78nJgx8QKtxjDVrwbZG+wvDOmVbifHyOUOQJXxqEy6r0faRrPzTQ==", + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/formidable/-/formidable-2.1.2.tgz", + "integrity": "sha512-CM3GuJ57US06mlpQ47YcunuUZ9jpm8Vx+P2CGt2j7HpgkKZO/DJYQ0Bobim8G6PFQmK5lOqOOdUXboU+h73A4g==", "dev": true, "requires": { - "dezalgo": "1.0.3", - "hexoid": "1.0.0", - "once": "1.4.0", - "qs": "6.9.3" - }, - "dependencies": { - "qs": { - "version": "6.9.3", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.9.3.tgz", - "integrity": "sha512-EbZYNarm6138UKKq46tdx08Yo/q9ZhFoAXAI1meAFd2GtbRDhbZY2WQSICskT0c5q99aFzLG1D4nvTk9tqfXIw==", - "dev": true - } + "dezalgo": "^1.0.4", + "hexoid": "^1.0.0", + "once": "^1.4.0", + "qs": "^6.11.0" } }, "forwarded": { @@ -12749,12 +13304,13 @@ "dev": true }, "get-intrinsic": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.1.3.tgz", - "integrity": "sha512-QJVz1Tj7MS099PevUG5jvnt9tSkXN8K14dxQlikJuPt4uD9hHAHjLyLBiLR5zELelBdD9QNRAXZzsJx0WaDL9A==", + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.1.tgz", + "integrity": "sha512-2DcsyfABl+gVHEfCOaTrWgyt+tb6MSEGmKq+kI5HwLbIYgjgmMcV8KQ41uaKz1xxUcn9tJtgFbQUEVcEbd0FYw==", "requires": { "function-bind": "^1.1.1", "has": "^1.0.3", + "has-proto": "^1.0.1", "has-symbols": "^1.0.3" } }, @@ -12804,26 +13360,22 @@ "integrity": "sha512-S0nG3CLEQiY/ILxqtztTWH/3iRRdyBLw6KMDxnKMchrtbj2OFmehVh0WUCfW3DUrIgx/qFrJPICrq4Z4sTR9UQ==", "dev": true }, - "globby": { - "version": "11.1.0", - "resolved": "https://registry.npmjs.org/globby/-/globby-11.1.0.tgz", - "integrity": "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==", + "globalthis": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/globalthis/-/globalthis-1.0.3.tgz", + "integrity": "sha512-sFdI5LyBiNTHjRd7cGPWapiHWMOXKyuBNX/cWJ3NfzrZQVa8GI/8cofCl74AOVqq9W5kNmguTIzJ/1s2gyI9wA==", + "dev": true, + "requires": { + "define-properties": "^1.1.3" + } + }, + "gopd": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz", + "integrity": "sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==", "dev": true, "requires": { - "array-union": "^2.1.0", - "dir-glob": "^3.0.1", - "fast-glob": "^3.2.9", - "ignore": "^5.2.0", - "merge2": "^1.4.1", - "slash": "^3.0.0" - }, - "dependencies": { - "slash": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", - "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", - "dev": true - } + "get-intrinsic": "^1.1.3" } }, "graceful-fs": { @@ -12833,10 +13385,10 @@ "dev": true, "peer": true }, - "grapheme-splitter": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/grapheme-splitter/-/grapheme-splitter-1.0.4.tgz", - "integrity": "sha512-bzh50DW9kTPM00T8y4o8vQg89Di9oLJVLW/KaOGIXJWP/iqCN6WKYkbNOF04vFLJhwcpYUh9ydh/+5vpOqV4YQ==", + "graphemer": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", + "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", "dev": true }, "handlebars": { @@ -12898,6 +13450,11 @@ "get-intrinsic": "^1.1.1" } }, + "has-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.1.tgz", + "integrity": "sha512-7qE+iP+O+bgF9clE5+UoBFzE65mlBiVj3tKCrlNQ0Ogwm0BjpT/gK4SlLYDMybDh5I3TCTKnPPa0oMG7JDYrhg==" + }, "has-symbols": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", @@ -12951,15 +13508,10 @@ "safer-buffer": ">= 2.1.2 < 3" } }, - "ieee754": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", - "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==" - }, "ignore": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.2.0.tgz", - "integrity": "sha512-CmxgYGiEPCLhfLnpPp1MoRmifwEIOgjcHXxOBjv7mY96c+eWScsOP9c112ZyLdWHi0FxHjI+4uVhKYp/gcdRmQ==", + "version": "5.2.4", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.2.4.tgz", + "integrity": "sha512-MAb38BcSbH0eHNBxn7ql2NH/kX33OkB3lZ1BNdh7ENeRChHTYsTvWrMubiIAMNS2llXEEgZ1MUOBtXChP3kaFQ==", "dev": true }, "import-fresh": { @@ -12995,12 +13547,12 @@ "dev": true }, "internal-slot": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.0.3.tgz", - "integrity": "sha512-O0DB1JC/sPyZl7cIo78n5dR7eUSwwpYPiXRhTzNxZVAMUuB8vlnRFyLxdrVToks6XPLVnFfbzaVd5WLjhgg+vA==", + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.0.5.tgz", + "integrity": "sha512-Y+R5hJrzs52QCG2laLn4udYVnxsfny9CpOhNhUvk/SSSVyF6T27FzRbF0sroPidSu3X8oEAkOn2K804mjpt6UQ==", "dev": true, "requires": { - "get-intrinsic": "^1.1.0", + "get-intrinsic": "^1.2.0", "has": "^1.0.3", "side-channel": "^1.0.4" } @@ -13024,6 +13576,17 @@ "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==" }, + "is-array-buffer": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.2.tgz", + "integrity": "sha512-y+FyyR/w8vfIRq4eQcM1EYgSTnmHXPqaF+IgzgraytCFq5Xh8lllDVmAZolPJiZttZLeFSINPYMaEJ7/vWUa1w==", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "get-intrinsic": "^1.2.0", + "is-typed-array": "^1.1.10" + } + }, "is-bigint": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/is-bigint/-/is-bigint-1.0.4.tgz", @@ -13050,9 +13613,9 @@ "dev": true }, "is-core-module": { - "version": "2.10.0", - "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.10.0.tgz", - "integrity": "sha512-Erxj2n/LDAZ7H8WNJXd9tw38GYM3dv8rk8Zcs+jJuxYTW7sozH+SS8NtrSjVL1/vpLvWi1hxy96IzjJ3EHTJJg==", + "version": "2.12.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.12.1.tgz", + "integrity": "sha512-Q4ZuBAe2FUsKtyQJoQHlvP8OvBERxO3jEmy1I7hcRXcJBGGHFh/aJBswbXuS9sgrDH2QUO8ilkwNPHvHMd8clg==", "dev": true, "requires": { "has": "^1.0.3" @@ -13112,6 +13675,12 @@ "has-tostringtag": "^1.0.0" } }, + "is-path-inside": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz", + "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==", + "dev": true + }, "is-plain-obj": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-2.1.0.tgz", @@ -13172,6 +13741,15 @@ "has-symbols": "^1.0.2" } }, + "is-typed-array": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.12.tgz", + "integrity": "sha512-Z14TF2JNG8Lss5/HMqt0//T9JeHXttXy5pH/DBU4vi98ozO2btxzq9MwYDZYnKwU8nRsz/+GVFVRDq3DkVuSPg==", + "dev": true, + "requires": { + "which-typed-array": "^1.1.11" + } + }, "is-unicode-supported": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz", @@ -13193,6 +13771,12 @@ "call-bind": "^1.0.2" } }, + "isarray": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", + "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", + "dev": true + }, "isexe": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", @@ -13359,12 +13943,6 @@ } } }, - "js-sdsl": { - "version": "4.1.5", - "resolved": "https://registry.npmjs.org/js-sdsl/-/js-sdsl-4.1.5.tgz", - "integrity": "sha512-08bOAKweV2NUC1wqTtf3qZlnpOX/R2DU9ikpjOHs0H+ibQv3zpncVQg6um4uYtRtrwIX8M4Nh3ytK4HGlYAq7Q==", - "dev": true - }, "js-tokens": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-3.0.2.tgz", @@ -13406,6 +13984,12 @@ "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", "dev": true }, + "json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true + }, "jsx-ast-utils": { "version": "3.3.3", "resolved": "https://registry.npmjs.org/jsx-ast-utils/-/jsx-ast-utils-3.3.3.tgz", @@ -13424,9 +14008,9 @@ "dev": true }, "kareem": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/kareem/-/kareem-2.4.1.tgz", - "integrity": "sha512-aJ9opVoXroQUPfovYP5kaj2lM7Jn02Gw13bL0lg9v0V7SaUc0qavPs0Eue7d2DcC3NjqI6QAUElXNsuZSeM+EA==" + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/kareem/-/kareem-2.5.1.tgz", + "integrity": "sha512-7jFxRVm+jD+rkq3kY0iZDJfsO2/t4BBPeEb2qKn2lR/9KhuksYk5hxzfRYWMPV8P/x2d0kHD306YyWLzjjH+uA==" }, "language-subtag-registry": { "version": "0.3.22", @@ -13566,29 +14150,12 @@ } }, "lru-cache": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", - "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", - "dev": true, - "requires": { - "yallist": "^4.0.0" - } - }, - "make-dir": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz", - "integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==", + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", "dev": true, "requires": { - "semver": "^6.0.0" - }, - "dependencies": { - "semver": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", - "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", - "dev": true - } + "yallist": "^3.0.2" } }, "media-typer": { @@ -13614,12 +14181,6 @@ "dev": true, "peer": true }, - "merge2": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", - "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", - "dev": true - }, "method-override": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/method-override/-/method-override-3.0.0.tgz", @@ -13670,9 +14231,9 @@ } }, "minimatch": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", - "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", "dev": true, "requires": { "brace-expansion": "^1.1.7" @@ -13694,12 +14255,11 @@ } }, "mocha": { - "version": "10.0.0", - "resolved": "https://registry.npmjs.org/mocha/-/mocha-10.0.0.tgz", - "integrity": "sha512-0Wl+elVUD43Y0BqPZBzZt8Tnkw9CMUdNYnUsTfOM1vuhJVZL+kiesFYsqwBkEEuEixaiPe5ZQdqDgX2jddhmoA==", + "version": "10.2.0", + "resolved": "https://registry.npmjs.org/mocha/-/mocha-10.2.0.tgz", + "integrity": "sha512-IDY7fl/BecMwFHzoqF2sg/SHHANeBoMMXFlS9r0OXKDssYE1M5O43wUY/9BVPeIvfH2zmEbBfseqN9gBQZzXkg==", "dev": true, "requires": { - "@ungap/promise-all-settled": "1.1.2", "ansi-colors": "4.1.1", "browser-stdout": "1.3.1", "chokidar": "3.5.3", @@ -13930,38 +14490,37 @@ } }, "mongodb": { - "version": "4.9.1", - "resolved": "https://registry.npmjs.org/mongodb/-/mongodb-4.9.1.tgz", - "integrity": "sha512-ZhgI/qBf84fD7sI4waZBoLBNJYPQN5IOC++SBCiPiyhzpNKOxN/fi0tBHvH2dEC42HXtNEbFB0zmNz4+oVtorQ==", + "version": "5.7.0", + "resolved": "https://registry.npmjs.org/mongodb/-/mongodb-5.7.0.tgz", + "integrity": "sha512-zm82Bq33QbqtxDf58fLWBwTjARK3NSvKYjyz997KSy6hpat0prjeX/kxjbPVyZY60XYPDNETaHkHJI2UCzSLuw==", "requires": { - "bson": "^4.7.0", - "denque": "^2.1.0", - "mongodb-connection-string-url": "^2.5.3", + "bson": "^5.4.0", + "mongodb-connection-string-url": "^2.6.0", "saslprep": "^1.0.3", - "socks": "^2.7.0" + "socks": "^2.7.1" } }, "mongodb-connection-string-url": { - "version": "2.5.4", - "resolved": "https://registry.npmjs.org/mongodb-connection-string-url/-/mongodb-connection-string-url-2.5.4.tgz", - "integrity": "sha512-SeAxuWs0ez3iI3vvmLk/j2y+zHwigTDKQhtdxTgt5ZCOQQS5+HW4g45/Xw5vzzbn7oQXCNQ24Z40AkJsizEy7w==", + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/mongodb-connection-string-url/-/mongodb-connection-string-url-2.6.0.tgz", + "integrity": "sha512-WvTZlI9ab0QYtTYnuMLgobULWhokRjtC7db9LtcVfJ+Hsnyr5eo6ZtNAt3Ly24XZScGMelOcGtm7lSn0332tPQ==", "requires": { "@types/whatwg-url": "^8.2.1", "whatwg-url": "^11.0.0" } }, "mongoose": { - "version": "6.6.5", - "resolved": "https://registry.npmjs.org/mongoose/-/mongoose-6.6.5.tgz", - "integrity": "sha512-iA/oDpWOc+K2QYzA4Eq7Z1oUBQOz9FGDmUwPLgw872Bfs/qizA5Db+gJorAn+TnnGu3VoCK8iP4Y+TECUelwjA==", + "version": "7.4.2", + "resolved": "https://registry.npmjs.org/mongoose/-/mongoose-7.4.2.tgz", + "integrity": "sha512-sNolW2hyncwvWmZjIEIwAckjaSKtC1SE86zE1v2TKm3vPTRogZfBQf+3zLYYdrgrVTzoaoICieVpct9hjcn3EQ==", "requires": { - "bson": "^4.6.5", - "kareem": "2.4.1", - "mongodb": "4.9.1", + "bson": "^5.4.0", + "kareem": "2.5.1", + "mongodb": "5.7.0", "mpath": "0.9.0", - "mquery": "4.0.3", + "mquery": "5.0.0", "ms": "2.1.3", - "sift": "16.0.0" + "sift": "16.0.1" } }, "mpath": { @@ -13970,9 +14529,9 @@ "integrity": "sha512-ikJRQTk8hw5DEoFVxHG1Gn9T/xcjtdnOKIU1JTmGjZZlg9LST2mBLmcX3/ICIbgJydT2GOc15RnNy5mHmzfSew==" }, "mquery": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/mquery/-/mquery-4.0.3.tgz", - "integrity": "sha512-J5heI+P08I6VJ2Ky3+33IpCdAvlYGTSUjwTPxkAr8i8EoduPMBX2OY/wa3IKZIQl7MU4SbFk8ndgSKyB/cl1zA==", + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/mquery/-/mquery-5.0.0.tgz", + "integrity": "sha512-iQMncpmEK8R8ncT8HJGsGc9Dsp8xcgYMVSbs5jgnm1lFHTZqMJTUWTDx1LBO8+mK3tPNZWFLBghQEIOULSTHZg==", "requires": { "debug": "4.x" }, @@ -14021,18 +14580,27 @@ "dev": true }, "nise": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/nise/-/nise-5.1.1.tgz", - "integrity": "sha512-yr5kW2THW1AkxVmCnKEh4nbYkJdB3I7LUkiUgOvEkOp414mc2UMaHMA7pjq1nYowhdoJZGwEKGaQVbxfpWj10A==", + "version": "5.1.4", + "resolved": "https://registry.npmjs.org/nise/-/nise-5.1.4.tgz", + "integrity": "sha512-8+Ib8rRJ4L0o3kfmyVCL7gzrohyDe0cMFTBa2d364yIrEGMEoetznKJx899YxjybU6bL9SQkYPSBBs1gyYs8Xg==", "dev": true, "requires": { - "@sinonjs/commons": "^1.8.3", - "@sinonjs/fake-timers": ">=5", + "@sinonjs/commons": "^2.0.0", + "@sinonjs/fake-timers": "^10.0.2", "@sinonjs/text-encoding": "^0.7.1", "just-extend": "^4.0.2", "path-to-regexp": "^1.7.0" }, "dependencies": { + "@sinonjs/commons": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-2.0.0.tgz", + "integrity": "sha512-uLa0j859mMrg2slwQYdO/AkrOfmH+X6LTVmNTS9CqexuE2IvVORIkSpJLqePAbEnKJ77aMmCwr1NUZ57120Xcg==", + "dev": true, + "requires": { + "type-detect": "4.0.8" + } + }, "isarray": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", @@ -14051,9 +14619,9 @@ } }, "node-releases": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.6.tgz", - "integrity": "sha512-PiVXnNuFm5+iYkLBNeq5211hvO38y63T0i2KKh2KnUs3RpzJ+JtODFjkD8yjLwnDkTYF1eKXheUwdssR+NRZdg==", + "version": "2.0.13", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.13.tgz", + "integrity": "sha512-uYr7J37ae/ORWdZeQ1xxMJe3NtdmqMC/JZK+geofDrkLUApKRHPd18/TxtBOJ4A0/+uUIliorNrfYV6s1b02eQ==", "dev": true }, "nopt": { @@ -14077,9 +14645,9 @@ "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=" }, "object-inspect": { - "version": "1.12.2", - "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.12.2.tgz", - "integrity": "sha512-z+cPxW0QGUp0mcqcsgQyLVRDoXFQbXOwBaqyF7VIgI4TWNQsDHrBpUQslRmIfAoYWdYzs6UlKJtB2XJpTaNSpQ==" + "version": "1.12.3", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.12.3.tgz", + "integrity": "sha512-geUvdk7c+eizMNUDkRpW1wJwgfOiOeHbxBR/hLXK1aT6zmVSO0jsQcs7fj6MGw89jC/cjGfLcNOrtMYtGqm81g==" }, "object-keys": { "version": "1.1.1", @@ -14111,15 +14679,26 @@ } }, "object.fromentries": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/object.fromentries/-/object.fromentries-2.0.5.tgz", - "integrity": "sha512-CAyG5mWQRRiBU57Re4FKoTBjXfDoNwdFVH2Y1tS9PqCsfUTymAohOkEMSG3aRNKmv4lV3O7p1et7c187q6bynw==", + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/object.fromentries/-/object.fromentries-2.0.6.tgz", + "integrity": "sha512-VciD13dswC4j1Xt5394WR4MzmAQmlgN72phd/riNp9vtD7tp4QQWJ0R4wvclXcafgcYK8veHRed2W6XeGBvcfg==", "dev": true, - "peer": true, "requires": { "call-bind": "^1.0.2", - "define-properties": "^1.1.3", - "es-abstract": "^1.19.1" + "define-properties": "^1.1.4", + "es-abstract": "^1.20.4" + } + }, + "object.groupby": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/object.groupby/-/object.groupby-1.0.0.tgz", + "integrity": "sha512-70MWG6NfRH9GnbZOikuhPPYzpUpof9iW2J9E4dW7FXTqPNb6rllE6u39SKwwiNh8lCwX3DDb5OgcKGiEBrTTyw==", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "define-properties": "^1.2.0", + "es-abstract": "^1.21.2", + "get-intrinsic": "^1.2.1" } }, "object.hasown": { @@ -14134,14 +14713,14 @@ } }, "object.values": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/object.values/-/object.values-1.1.5.tgz", - "integrity": "sha512-QUZRW0ilQ3PnPpbNtgdNV1PDbEqLIiSFB3l+EnGtBQ/8SUTLj1PZwtQHABZtLgwpJZTSZhuGLOGk57Drx2IvYg==", + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/object.values/-/object.values-1.1.6.tgz", + "integrity": "sha512-FVVTkD1vENCsAcwNs9k6jea2uHC/X0+JcjG8YA60FN5CMaJmG95wT9jek/xX9nornqGRrBkKtzuAu2wuHpKqvw==", "dev": true, "requires": { "call-bind": "^1.0.2", - "define-properties": "^1.1.3", - "es-abstract": "^1.19.1" + "define-properties": "^1.1.4", + "es-abstract": "^1.20.4" } }, "on-finished": { @@ -14162,17 +14741,17 @@ } }, "optionator": { - "version": "0.9.1", - "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.1.tgz", - "integrity": "sha512-74RlY5FCnhq4jRxVUPKDaRwrVNXMqsGsiW6AJw4XK8hmtm10wC0ypZBLw5IIp85NZMr91+qd1RvvENwg7jjRFw==", + "version": "0.9.3", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.3.tgz", + "integrity": "sha512-JjCoypp+jKn1ttEFExxhetCKeJt9zhAgAve5FXHixTvFDW/5aEktX9bufBKLRRMdU7bNtpLfcGu94B3cdEJgjg==", "dev": true, "requires": { + "@aashutoshrathi/word-wrap": "^1.2.3", "deep-is": "^0.1.3", "fast-levenshtein": "^2.0.6", "levn": "^0.4.1", "prelude-ls": "^1.2.1", - "type-check": "^0.4.0", - "word-wrap": "^1.2.3" + "type-check": "^0.4.0" } }, "p-limit": { @@ -14213,6 +14792,12 @@ "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==" }, + "path-exists": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-5.0.0.tgz", + "integrity": "sha512-RjhtfwJOxzcFmNOi6ltcbcu4Iu+FL3zEj83dk4kAS+fVpTxXLO1b38RvJgT/0QwvV/L3aY9TAnyv0EOqW4GoMQ==", + "dev": true + }, "path-is-absolute": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", @@ -14236,12 +14821,6 @@ "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz", "integrity": "sha1-32BBeABfUi8V60SQ5yR6G/qmf4w=" }, - "path-type": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", - "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", - "dev": true - }, "picocolors": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz", @@ -14261,57 +14840,12 @@ "dev": true }, "pkg-dir": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz", - "integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-7.0.0.tgz", + "integrity": "sha512-Ie9z/WINcxxLp27BKOCHGde4ITq9UklYKDzVo1nhk5sqGEXU3FpkwP5GM2voTGJkGd9B3Otl+Q4uwSOeSUtOBA==", "dev": true, "requires": { - "find-up": "^4.0.0" - }, - "dependencies": { - "find-up": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", - "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", - "dev": true, - "requires": { - "locate-path": "^5.0.0", - "path-exists": "^4.0.0" - } - }, - "locate-path": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", - "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", - "dev": true, - "requires": { - "p-locate": "^4.1.0" - } - }, - "p-limit": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", - "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", - "dev": true, - "requires": { - "p-try": "^2.0.0" - } - }, - "p-locate": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", - "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", - "dev": true, - "requires": { - "p-limit": "^2.2.0" - } - }, - "path-exists": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", - "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", - "dev": true - } + "find-up": "^6.3.0" } }, "prelude-ls": { @@ -14341,10 +14875,15 @@ "ipaddr.js": "1.9.1" } }, + "punycode": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.0.tgz", + "integrity": "sha512-rRV+zQD8tVFys26lAGR9WUuS4iUAngJScM+ZRSKtvl5tKeZ2t5bvdNFdNHBW9FWR4guGHlgmsZ1G7BSm2wTbuA==" + }, "qs": { - "version": "6.10.3", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.10.3.tgz", - "integrity": "sha512-wr7M2E0OFRfIfJZjKGieI8lBKb7fRCH4Fv5KNPEs7gJ8jadvotdsS08PzOKR7opXhZ/Xkjtt3WF9g38drmyRqQ==", + "version": "6.11.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.11.0.tgz", + "integrity": "sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==", "requires": { "side-channel": "^1.0.4" } @@ -14370,9 +14909,9 @@ "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==" }, "raw-body": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.1.tgz", - "integrity": "sha512-qqJBtEyVgS0ZmPGdCFPWJ3FreoqvG4MVQln/kCgF7Olq95IbOp0/BWyMwbdtn4VTvkM8Y7khCQ2Xgk/tcrCXig==", + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.2.tgz", + "integrity": "sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==", "requires": { "bytes": "3.1.2", "http-errors": "2.0.0", @@ -14403,57 +14942,45 @@ } }, "regenerator-runtime": { - "version": "0.13.9", - "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.9.tgz", - "integrity": "sha512-p3VT+cOEgxFsRRA9X4lkI1E+k2/CtnKtU4gcxyaCUreilL/vqI6CdZ3wxVUx3UOUg+gnUOQQcRI7BmSI656MYA==", + "version": "0.13.11", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz", + "integrity": "sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==", "dev": true }, "regenerator-transform": { - "version": "0.15.0", - "resolved": "https://registry.npmjs.org/regenerator-transform/-/regenerator-transform-0.15.0.tgz", - "integrity": "sha512-LsrGtPmbYg19bcPHwdtmXwbW+TqNvtY4riE3P83foeHRroMbH6/2ddFBfab3t7kbzc7v7p4wbkIecHImqt0QNg==", + "version": "0.15.1", + "resolved": "https://registry.npmjs.org/regenerator-transform/-/regenerator-transform-0.15.1.tgz", + "integrity": "sha512-knzmNAcuyxV+gQCufkYcvOqX/qIIfHLv0u5x79kRxuGojfYVky1f15TzZEu2Avte8QGepvUNTnLskf8E6X6Vyg==", "dev": true, "requires": { "@babel/runtime": "^7.8.4" } }, "regexp.prototype.flags": { - "version": "1.4.3", - "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.4.3.tgz", - "integrity": "sha512-fjggEOO3slI6Wvgjwflkc4NFRCTZAu5CnNfBd5qOMYhWdn67nJBBu34/TkD++eeFmd8C9r9jfXJ27+nSiRkSUA==", + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.0.tgz", + "integrity": "sha512-0SutC3pNudRKgquxGoRGIz946MZVHqbNfPjBdxeOhBrdgDKlRoXmYLQN9xRbrR09ZXWeGAdPuif7egofn6v5LA==", "dev": true, "requires": { "call-bind": "^1.0.2", - "define-properties": "^1.1.3", - "functions-have-names": "^1.2.2" + "define-properties": "^1.2.0", + "functions-have-names": "^1.2.3" } }, - "regexpp": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/regexpp/-/regexpp-3.2.0.tgz", - "integrity": "sha512-pq2bWo9mVD43nbts2wGv17XLiNLya+GklZ8kaDLV2Z08gDCsGpnKn9BFMepvWuHCbyVvY7J5o5+BVvoQbmlJLg==", - "dev": true - }, "regexpu-core": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/regexpu-core/-/regexpu-core-5.2.1.tgz", - "integrity": "sha512-HrnlNtpvqP1Xkb28tMhBUO2EbyUHdQlsnlAhzWcwHy8WJR53UWr7/MAvqrsQKMbV4qdpv03oTMG8iIhfsPFktQ==", + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/regexpu-core/-/regexpu-core-5.3.2.tgz", + "integrity": "sha512-RAM5FlZz+Lhmo7db9L298p2vHP5ZywrVXmVXpmAD9GuL5MPH6t9ROw1iA/wfHkQ76Qe7AaPF0nGuim96/IrQMQ==", "dev": true, "requires": { + "@babel/regjsgen": "^0.8.0", "regenerate": "^1.4.2", "regenerate-unicode-properties": "^10.1.0", - "regjsgen": "^0.7.1", "regjsparser": "^0.9.1", "unicode-match-property-ecmascript": "^2.0.0", - "unicode-match-property-value-ecmascript": "^2.0.0" + "unicode-match-property-value-ecmascript": "^2.1.0" } }, - "regjsgen": { - "version": "0.7.1", - "resolved": "https://registry.npmjs.org/regjsgen/-/regjsgen-0.7.1.tgz", - "integrity": "sha512-RAt+8H2ZEzHeYWxZ3H2z6tF18zyyOnlcdaafLrm21Bguj7uZy6ULibiAFdXEtKQY4Sy7wDTwDiOazasMLc4KPA==", - "dev": true - }, "regjsparser": { "version": "0.9.1", "resolved": "https://registry.npmjs.org/regjsparser/-/regjsparser-0.9.1.tgz", @@ -14486,13 +15013,19 @@ "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", "dev": true }, + "require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "dev": true + }, "resolve": { - "version": "1.22.1", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.1.tgz", - "integrity": "sha512-nBpuuYuY5jFsli/JIs1oldw6fOQCBioohqWZg/2hiaOybXOft4lonv85uDOKXdf8rhyK159cxU5cDcK/NKk8zw==", + "version": "1.22.3", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.3.tgz", + "integrity": "sha512-P8ur/gp/AmbEzjr729bZnLjXK5Z+4P0zhIJgBgzqRih7hL7BOukHGtSTA3ACMY467GRFz3duQsi0bDZdR7DKdw==", "dev": true, "requires": { - "is-core-module": "^2.9.0", + "is-core-module": "^2.12.0", "path-parse": "^1.0.7", "supports-preserve-symlinks-flag": "^1.0.0" } @@ -14527,6 +15060,18 @@ "queue-microtask": "^1.2.2" } }, + "safe-array-concat": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/safe-array-concat/-/safe-array-concat-1.0.0.tgz", + "integrity": "sha512-9dVEFruWIsnie89yym+xWTAYASdpw3CJV7Li/6zBewGf9z2i1j31rP6jnY0pHEO4QZh6N0K11bFjWmdR8UGdPQ==", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "get-intrinsic": "^1.2.0", + "has-symbols": "^1.0.3", + "isarray": "^2.0.5" + } + }, "safe-buffer": { "version": "5.1.2", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", @@ -14559,14 +15104,44 @@ } }, "schema-utils": { - "version": "2.7.1", - "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-2.7.1.tgz", - "integrity": "sha512-SHiNtMOUGWBQJwzISiVYKu82GiV4QYGePp3odlY1tuKO7gPtphAT5R/py0fA6xtbgLL/RvtJZnU9b8s0F1q0Xg==", + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.2.0.tgz", + "integrity": "sha512-L0jRsrPpjdckP3oPug3/VxNKt2trR8TcabrM6FOAAlvC/9Phcmm+cuAgTlxBqdBR1WJx7Naj9WHw+aOmheSVbw==", "dev": true, "requires": { - "@types/json-schema": "^7.0.5", - "ajv": "^6.12.4", - "ajv-keywords": "^3.5.2" + "@types/json-schema": "^7.0.9", + "ajv": "^8.9.0", + "ajv-formats": "^2.1.1", + "ajv-keywords": "^5.1.0" + }, + "dependencies": { + "ajv": { + "version": "8.12.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.12.0.tgz", + "integrity": "sha512-sRu1kpcO9yLtYxBKvqfTeh9KzZEwO3STyX1HT+4CaDzC6HpTGYhIhPIzj9XuKU7KYDwnaeh5hcOwjy1QuJzBPA==", + "dev": true, + "requires": { + "fast-deep-equal": "^3.1.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2", + "uri-js": "^4.2.2" + } + }, + "ajv-keywords": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-5.1.0.tgz", + "integrity": "sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw==", + "dev": true, + "requires": { + "fast-deep-equal": "^3.1.3" + } + }, + "json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "dev": true + } } }, "semver": { @@ -14724,24 +15299,30 @@ } }, "sift": { - "version": "16.0.0", - "resolved": "https://registry.npmjs.org/sift/-/sift-16.0.0.tgz", - "integrity": "sha512-ILTjdP2Mv9V1kIxWMXeMTIRbOBrqKc4JAXmFMnFq3fKeyQ2Qwa3Dw1ubcye3vR+Y6ofA0b9gNDr/y2t6eUeIzQ==" + "version": "16.0.1", + "resolved": "https://registry.npmjs.org/sift/-/sift-16.0.1.tgz", + "integrity": "sha512-Wv6BjQ5zbhW7VFefWusVP33T/EM0vYikCaQ2qR8yULbsilAT8/wQaXvuQ3ptGLpoKx+lihJE3y2UTgKDyyNHZQ==" }, "sinon": { - "version": "14.0.1", - "resolved": "https://registry.npmjs.org/sinon/-/sinon-14.0.1.tgz", - "integrity": "sha512-JhJ0jCiyBWVAHDS+YSjgEbDn7Wgz9iIjA1/RK+eseJN0vAAWIWiXBdrnb92ELPyjsfreCYntD1ORtLSfIrlvSQ==", + "version": "15.2.0", + "resolved": "https://registry.npmjs.org/sinon/-/sinon-15.2.0.tgz", + "integrity": "sha512-nPS85arNqwBXaIsFCkolHjGIkFo+Oxu9vbgmBJizLAhqe6P2o3Qmj3KCUoRkfhHtvgDhZdWD3risLHAUJ8npjw==", "dev": true, "requires": { - "@sinonjs/commons": "^1.8.3", - "@sinonjs/fake-timers": "^9.1.2", - "@sinonjs/samsam": "^6.1.1", - "diff": "^5.0.0", - "nise": "^5.1.1", + "@sinonjs/commons": "^3.0.0", + "@sinonjs/fake-timers": "^10.3.0", + "@sinonjs/samsam": "^8.0.0", + "diff": "^5.1.0", + "nise": "^5.1.4", "supports-color": "^7.2.0" }, "dependencies": { + "diff": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/diff/-/diff-5.1.0.tgz", + "integrity": "sha512-D+mk+qE8VC/PAUrlAU34N+VfXev0ghe5ywmpqrawphmVZc1bEfn56uo9qpyGp1p4xpzOHkSW4ztBd6L7Xx4ACw==", + "dev": true + }, "has-flag": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", @@ -14834,26 +15415,37 @@ "side-channel": "^1.0.4" } }, + "string.prototype.trim": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/string.prototype.trim/-/string.prototype.trim-1.2.7.tgz", + "integrity": "sha512-p6TmeT1T3411M8Cgg9wBTMRtY2q9+PNy9EV1i2lIXUN/btt763oIfxwN3RR8VU6wHX8j/1CFy0L+YuThm6bgOg==", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.4", + "es-abstract": "^1.20.4" + } + }, "string.prototype.trimend": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.5.tgz", - "integrity": "sha512-I7RGvmjV4pJ7O3kdf+LXFpVfdNOxtCW/2C8f6jNiW4+PQchwxkCDzlk1/7p+Wl4bqFIZeF47qAHXLuHHWKAxog==", + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.6.tgz", + "integrity": "sha512-JySq+4mrPf9EsDBEDYMOb/lM7XQLulwg5R/m1r0PXEFqrV0qHvl58sdTilSXtKOflCsK2E8jxf+GKC0T07RWwQ==", "dev": true, "requires": { "call-bind": "^1.0.2", "define-properties": "^1.1.4", - "es-abstract": "^1.19.5" + "es-abstract": "^1.20.4" } }, "string.prototype.trimstart": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.5.tgz", - "integrity": "sha512-THx16TJCGlsN0o6dl2o6ncWUsdgnLRSA23rRE5pyGBw/mLr3Ej/R2LaqCtgP8VNMGZsvMWnf9ooZPyY2bHvUFg==", + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.6.tgz", + "integrity": "sha512-omqjMDaY92pbn5HOX7f9IccLA+U1tA9GvtU4JrodiXFfYB7jPzzHpRzpglLAjtUV6bB557zwClJezTqnAiYnQA==", "dev": true, "requires": { "call-bind": "^1.0.2", "define-properties": "^1.1.4", - "es-abstract": "^1.19.5" + "es-abstract": "^1.20.4" } }, "strip-ansi": { @@ -14878,21 +15470,21 @@ "dev": true }, "superagent": { - "version": "8.0.2", - "resolved": "https://registry.npmjs.org/superagent/-/superagent-8.0.2.tgz", - "integrity": "sha512-QtYZ9uaNAMexI7XWl2vAXAh0j4q9H7T0WVEI/y5qaUB3QLwxo+voUgCQ217AokJzUTIVOp0RTo7fhZrwhD7A2Q==", + "version": "8.0.9", + "resolved": "https://registry.npmjs.org/superagent/-/superagent-8.0.9.tgz", + "integrity": "sha512-4C7Bh5pyHTvU33KpZgwrNKh/VQnvgtCSqPRfJAUdmrtSYePVzVg4E4OzsrbkhJj9O7SO6Bnv75K/F8XVZT8YHA==", "dev": true, "requires": { "component-emitter": "^1.3.0", - "cookiejar": "^2.1.3", + "cookiejar": "^2.1.4", "debug": "^4.3.4", "fast-safe-stringify": "^2.1.1", "form-data": "^4.0.0", - "formidable": "^2.0.1", + "formidable": "^2.1.2", "methods": "^1.1.2", "mime": "2.6.0", "qs": "^6.11.0", - "semver": "^7.3.7" + "semver": "^7.3.8" }, "dependencies": { "debug": { @@ -14904,6 +15496,15 @@ "ms": "2.1.2" } }, + "lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "dev": true, + "requires": { + "yallist": "^4.0.0" + } + }, "mime": { "version": "2.6.0", "resolved": "https://registry.npmjs.org/mime/-/mime-2.6.0.tgz", @@ -14916,34 +15517,31 @@ "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", "dev": true }, - "qs": { - "version": "6.11.0", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.11.0.tgz", - "integrity": "sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==", - "dev": true, - "requires": { - "side-channel": "^1.0.4" - } - }, "semver": { - "version": "7.3.8", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.8.tgz", - "integrity": "sha512-NB1ctGL5rlHrPJtFDVIVzTyQylMLu9N9VICA6HSFJo8MCGVTMW6gfpicwKmmK/dAjTOrqu5l63JJOpDSrAis3A==", + "version": "7.5.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", + "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", "dev": true, "requires": { "lru-cache": "^6.0.0" } + }, + "yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true } } }, "supertest": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/supertest/-/supertest-6.3.0.tgz", - "integrity": "sha512-QgWju1cNoacP81Rv88NKkQ4oXTzGg0eNZtOoxp1ROpbS4OHY/eK5b8meShuFtdni161o5X0VQvgo7ErVyKK+Ow==", + "version": "6.3.3", + "resolved": "https://registry.npmjs.org/supertest/-/supertest-6.3.3.tgz", + "integrity": "sha512-EMCG6G8gDu5qEqRQ3JjjPs6+FYT1a7Hv5ApHvtSghmOFJYtsU5S+pSb6Y2EUeCEY3CmEL3mmQ8YWlPOzQomabA==", "dev": true, "requires": { "methods": "^1.1.2", - "superagent": "^8.0.0" + "superagent": "^8.0.5" } }, "supports-color": { @@ -15029,13 +15627,6 @@ "integrity": "sha512-l7FvfAHlcmulp8kr+flpQZmVwtu7nfRV7NZujtN0OqES8EL4O4e0qqzL0DC5gAvx/ZC/9lk6rhcUwYvkBnBnYA==", "requires": { "punycode": "^2.1.1" - }, - "dependencies": { - "punycode": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz", - "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==" - } } }, "trim-right": { @@ -15045,21 +15636,21 @@ "dev": true }, "tsconfig-paths": { - "version": "3.14.1", - "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.14.1.tgz", - "integrity": "sha512-fxDhWnFSLt3VuTwtvJt5fpwxBHg5AdKWMsgcPOOIilyjymcYVZoCQF8fvFRezCNfblEXmi+PcM1eYHeOAgXCOQ==", + "version": "3.14.2", + "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.14.2.tgz", + "integrity": "sha512-o/9iXgCYc5L/JxCHPe3Hvh8Q/2xm5Z+p18PESBU6Ff33695QnCHBEjcytY2q19ua7Mbl/DavtBOLq+oG0RCL+g==", "dev": true, "requires": { "@types/json5": "^0.0.29", - "json5": "^1.0.1", + "json5": "^1.0.2", "minimist": "^1.2.6", "strip-bom": "^3.0.0" }, "dependencies": { "json5": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.1.tgz", - "integrity": "sha512-aKS4WQjPenRxiQsC93MNfjx+nbF4PAdYzmd/1JIj8HYzqfbu86beTuNgXDzPknWk0n0uARlyewZo4s++ES36Ow==", + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.2.tgz", + "integrity": "sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==", "dev": true, "requires": { "minimist": "^1.2.0" @@ -15097,6 +15688,53 @@ "mime-types": "~2.1.24" } }, + "typed-array-buffer": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.0.tgz", + "integrity": "sha512-Y8KTSIglk9OZEr8zywiIHG/kmQ7KWyjseXs1CbSo8vC42w7hg2HgYTxSWwP0+is7bWDc1H+Fo026CpHFwm8tkw==", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "get-intrinsic": "^1.2.1", + "is-typed-array": "^1.1.10" + } + }, + "typed-array-byte-length": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/typed-array-byte-length/-/typed-array-byte-length-1.0.0.tgz", + "integrity": "sha512-Or/+kvLxNpeQ9DtSydonMxCx+9ZXOswtwJn17SNLvhptaXYDJvkFFP5zbfU/uLmvnBJlI4yrnXRxpdWH/M5tNA==", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "for-each": "^0.3.3", + "has-proto": "^1.0.1", + "is-typed-array": "^1.1.10" + } + }, + "typed-array-byte-offset": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/typed-array-byte-offset/-/typed-array-byte-offset-1.0.0.tgz", + "integrity": "sha512-RD97prjEt9EL8YgAgpOkf3O4IF9lhJFr9g0htQkm0rchFp/Vx7LW5Q8fSXXub7BXAODyUQohRMyOc3faCPd0hg==", + "dev": true, + "requires": { + "available-typed-arrays": "^1.0.5", + "call-bind": "^1.0.2", + "for-each": "^0.3.3", + "has-proto": "^1.0.1", + "is-typed-array": "^1.1.10" + } + }, + "typed-array-length": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/typed-array-length/-/typed-array-length-1.0.4.tgz", + "integrity": "sha512-KjZypGq+I/H7HI5HlOoGHkWUUGq+Q0TPhQurLbyrVrvnKTBgzLhIJ7j6J/XTQOi0d1RjyZ0wdas8bKs2p0x3Ng==", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "for-each": "^0.3.3", + "is-typed-array": "^1.1.9" + } + }, "uglify-js": { "version": "3.6.0", "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.6.0.tgz", @@ -15146,9 +15784,9 @@ } }, "unicode-match-property-value-ecmascript": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/unicode-match-property-value-ecmascript/-/unicode-match-property-value-ecmascript-2.0.0.tgz", - "integrity": "sha512-7Yhkc0Ye+t4PNYzOGKedDhXbYIBe1XEQYQxOPyhcXNMJ0WCABqqj6ckydd6pWRZTHV4GuCPKdBAUiMc60tsKVw==", + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/unicode-match-property-value-ecmascript/-/unicode-match-property-value-ecmascript-2.1.0.tgz", + "integrity": "sha512-qxkjQt6qjg/mYscYMC0XKRn3Rh0wFPlfxB0xkt9CfyTvpX1Ra0+rAmdX2QyAobptSEvuy4RtpPRui6XkV+8wjA==", "dev": true }, "unicode-property-aliases-ecmascript": { @@ -15163,9 +15801,9 @@ "integrity": "sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw=" }, "update-browserslist-db": { - "version": "1.0.10", - "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.10.tgz", - "integrity": "sha512-OztqDenkfFkbSG+tRxBeAnCVPckDBcvibKd35yDONx6OU8N7sqgwc7rCbkJ/WcYtVRZ4ba68d6byhC21GFh7sQ==", + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.11.tgz", + "integrity": "sha512-dCwEFf0/oT85M1fHBg4F0jtLwJrutGoHSQXCh7u4o2t1drG+c0a9Flnqww6XUKSfQMPpJBRjU8d4RXB09qtvaA==", "dev": true, "requires": { "escalade": "^3.1.1", @@ -15179,14 +15817,6 @@ "dev": true, "requires": { "punycode": "^2.1.0" - }, - "dependencies": { - "punycode": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz", - "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==", - "dev": true - } } }, "utils-merge": { @@ -15323,11 +15953,18 @@ "is-symbol": "^1.0.3" } }, - "word-wrap": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.3.tgz", - "integrity": "sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ==", - "dev": true + "which-typed-array": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.11.tgz", + "integrity": "sha512-qe9UWWpkeG5yzZ0tNYxDmd7vo58HDBc39mZ0xWWpolAGADdFOzkfamWLDxkOWcvHQKVmdTyQdLD4NOfjLWTKew==", + "dev": true, + "requires": { + "available-typed-arrays": "^1.0.5", + "call-bind": "^1.0.2", + "for-each": "^0.3.3", + "gopd": "^1.0.1", + "has-tostringtag": "^1.0.0" + } }, "wordwrap": { "version": "1.0.0", @@ -15348,9 +15985,9 @@ "dev": true }, "yallist": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", "dev": true }, "yargs-unparser": { diff --git a/package.json b/package.json index 9c8c754..5a2efb6 100644 --- a/package.json +++ b/package.json @@ -29,47 +29,47 @@ ], "license": "MIT", "dependencies": { - "body-parser": "^1.20.0", + "body-parser": "^1.20.2", "cors": "2.8.5", - "express": "^4.18.1", + "express": "^4.18.2", "method-override": "3.0.0", - "mongoose": "6.6.5", + "mongoose": "7.4.2", "uuid": "9.0.0" }, "devDependencies": { - "@babel/cli": "^7.19.3", - "@babel/core": "^7.19.3", - "@babel/eslint-parser": "^7.19.1", - "@babel/helpers": "^7.19.0", - "@babel/plugin-proposal-class-properties": "^7.0.0", - "@babel/plugin-proposal-decorators": "^7.0.0", - "@babel/plugin-proposal-do-expressions": "^7.0.0", - "@babel/plugin-proposal-export-default-from": "^7.0.0", - "@babel/plugin-proposal-export-namespace-from": "^7.0.0", - "@babel/plugin-proposal-function-bind": "^7.0.0", - "@babel/plugin-proposal-function-sent": "^7.0.0", - "@babel/plugin-proposal-json-strings": "^7.0.0", - "@babel/plugin-proposal-logical-assignment-operators": "^7.0.0", - "@babel/plugin-proposal-nullish-coalescing-operator": "^7.0.0", - "@babel/plugin-proposal-numeric-separator": "^7.0.0", - "@babel/plugin-proposal-optional-chaining": "^7.0.0", - "@babel/plugin-proposal-pipeline-operator": "^7.0.0", - "@babel/plugin-proposal-throw-expressions": "^7.0.0", - "@babel/plugin-syntax-dynamic-import": "^7.0.0", - "@babel/plugin-syntax-import-meta": "^7.0.0", - "@babel/preset-env": "^7.19.3", - "@babel/register": "^7.18.9", - "@babel/runtime": "^7.19.0", - "babel-loader": "8.2.5", - "eslint": "8.24.0", + "@babel/cli": "^7.22.9", + "@babel/core": "^7.22.9", + "@babel/eslint-parser": "^7.22.9", + "@babel/helpers": "^7.22.6", + "@babel/plugin-proposal-class-properties": "^7.18.6", + "@babel/plugin-proposal-decorators": "^7.22.7", + "@babel/plugin-proposal-do-expressions": "^7.22.5", + "@babel/plugin-proposal-export-default-from": "^7.22.5", + "@babel/plugin-proposal-export-namespace-from": "^7.18.9", + "@babel/plugin-proposal-function-bind": "^7.22.5", + "@babel/plugin-proposal-function-sent": "^7.22.5", + "@babel/plugin-proposal-json-strings": "^7.18.6", + "@babel/plugin-proposal-logical-assignment-operators": "^7.20.7", + "@babel/plugin-proposal-nullish-coalescing-operator": "^7.18.6", + "@babel/plugin-proposal-numeric-separator": "^7.18.6", + "@babel/plugin-proposal-optional-chaining": "^7.21.0", + "@babel/plugin-proposal-pipeline-operator": "^7.22.5", + "@babel/plugin-proposal-throw-expressions": "^7.22.5", + "@babel/plugin-syntax-dynamic-import": "^7.8.3", + "@babel/plugin-syntax-import-meta": "^7.10.4", + "@babel/preset-env": "^7.22.9", + "@babel/register": "^7.22.5", + "@babel/runtime": "^7.22.6", + "babel-loader": "9.1.3", + "eslint": "8.46.0", "eslint-config-airbnb": "19.0.4", "eslint-plugin-babel": "5.3.1", - "eslint-plugin-import": "^2.26.0", + "eslint-plugin-import": "^2.28.0", "istanbul": "1.1.0-alpha.1", - "mocha": "10.0.0", + "mocha": "10.2.0", "should": "13.2.3", "should-sinon": "0.0.6", - "sinon": "14.0.1", - "supertest": "6.3.0" + "sinon": "15.2.0", + "supertest": "6.3.3" } } diff --git a/src/mongo/rest/count.js b/src/mongo/rest/count.js index 39b7a72..f583c05 100644 --- a/src/mongo/rest/count.js +++ b/src/mongo/rest/count.js @@ -1,19 +1,15 @@ -export default (req, res, next) => { - const query = req.$odata.Model.find(); +export default async (req, res, next) => { + try { + const query = req.$odata.Model.find(); + const count = await query.count(); - query.count((err, count) => { - if (err) { - const result = new Error(err.message); + res.$odata.result = count.toString(); + res.$odata.supportedMimetypes = ['text/plain']; - result.previous = err; - result.status = 500; - next(result); + next(); - } else { - res.$odata.result = count.toString(); - res.$odata.supportedMimetypes = ['text/plain']; - next(); + } catch(err) { + next(err); + } - } - }); }; diff --git a/src/mongo/rest/delete.js b/src/mongo/rest/delete.js index 5503bee..6906ff4 100644 --- a/src/mongo/rest/delete.js +++ b/src/mongo/rest/delete.js @@ -1,17 +1,19 @@ -export default (req, res, next) => { - req.$odata.Model.remove({ _id: req.$odata.$Key._id }, (err, result) => { - if (err) { - return next(err); - } +export default async (req, res, next) => { + try { + const result = await req.$odata.Model.deleteOne({ _id: req.$odata.$Key._id }); if (JSON.parse(result).n === 0) { const error = new Error('Not Found'); error.status = 404; - return next(error); + throw error; } res.$odata.status = 204; next(); - }); + + } catch(err) { + next(err); + } + }; diff --git a/src/mongo/rest/get.js b/src/mongo/rest/get.js index fe9c76e..aef1274 100644 --- a/src/mongo/rest/get.js +++ b/src/mongo/rest/get.js @@ -1,17 +1,19 @@ -export default (req, res, next) => { - req.$odata.Model.findById(req.$odata.$Key._id, (err, entity) => { - if (err) { - return next(err); - } +export default async (req, res, next) => { + try { + const entity = await req.$odata.Model.findById(req.$odata.$Key._id); if (!entity) { const result = new Error('Not Found'); result.status = 404; - return next(result); + throw result; } res.$odata.result = entity.toObject(); - return next(); - }); + next(); + + } catch (err) { + next(err); + } + }; diff --git a/src/mongo/rest/getSingleton.js b/src/mongo/rest/getSingleton.js index a14a21e..76f9f0e 100644 --- a/src/mongo/rest/getSingleton.js +++ b/src/mongo/rest/getSingleton.js @@ -1,32 +1,32 @@ import selectParser from "../parser/selectParser"; export default async (req, res, next) => { - const query = req.$odata.Model.findOne(); - - await selectParser(query, req.$odata.$select); - - query.exec((err, entity) => { - if (err) { - return next(err); - } + try { + const query = req.$odata.Model.findOne(); + + await selectParser(query, req.$odata.$select); + + let entity = await query.exec(); if (!entity) { // return default properties of singleton - const result = new req.$odata.Model(); - - res.$odata.result = result.toObject(); - if (req.$odata.$select) { - Object.keys(res.$odata.result) - .forEach(item => { - if (req.$odata.$select.indexOf(item) === -1) { - delete req.$odata.$select; - } - }); - } - return next(); + entity = new req.$odata.Model(); } res.$odata.result = entity.toObject(); - return next(); - }); + + if (req.$odata.$select) { + Object.keys(res.$odata.result) + .forEach(item => { + if (req.$odata.$select.indexOf(item) === -1) { + delete req.$odata.result[item]; + } + }); + } + next(); + + } catch(err) { + next(err); + } + }; diff --git a/src/mongo/rest/list.js b/src/mongo/rest/list.js index 6a56618..77be715 100644 --- a/src/mongo/rest/list.js +++ b/src/mongo/rest/list.js @@ -3,46 +3,33 @@ import skipParser from '../parser/skipParser'; import topParser from '../parser/topParser'; import selectParser from '../parser/selectParser'; -function _dataQuery(model, { - filter, orderby, skip, top, select, -}) { - return new Promise((resolve, reject) => { - const query = model.find(filterParser(filter)); +export default async (req, res, next) => { - if (orderby) { - query.sort(orderby); - } - - skipParser(query, skip) - .then(() => topParser(query, top)) - .then(() => selectParser(query, select)) - .then(() => query.exec((err, data) => { - if (err) { - return reject(err); - } - return resolve({ value: data.map(item => item.toObject()) }); - })) - .catch(reject); - }); -} - -export default (req, res, next) => { - const params = { - count: req.$odata.$count, - filter: req.$odata.$filter, - orderby: req.$odata.$orderby, - skip: req.$odata.$skip, - top: req.$odata.$top, - select: req.$odata.$select, + try { // TODO expand: req.$odata.$expand, // TODO search: req.$odata.$search, - }; + const query = req.$odata.Model.find(filterParser(req.$odata.$filter)); + + if (req.$odata.$orderby) { + query.sort(req.$odata.$orderby); + } + + await skipParser(query, req.$odata.$skip); + await topParser(query, req.$odata.$top); + await selectParser(query, req.$odata.$select); + + const data = await query.exec(); + const result = { value: data.map(item => item.toObject()) }; - _dataQuery(req.$odata.Model, params).then((result) => { res.$odata.result = { ...res.$odata.result, ...result }; + next(); - }).catch(next); + + } catch (err) { + next(err); + } + }; diff --git a/src/mongo/rest/patch.js b/src/mongo/rest/patch.js index f27a44a..31f4425 100644 --- a/src/mongo/rest/patch.js +++ b/src/mongo/rest/patch.js @@ -1,16 +1,13 @@ -export default (req, res, next) => { - req.$odata.Model.findOne({ id: req.params.id }, (err, entity) => { - if (err) { - next(err); - } else { - req.$odata.Model.update({ id: req.params.id }, { ...entity, ...req.body }, (err1) => { - if (err1) { - next(err1); - } else { - res.$odata.result = { ...entity.toObject(), ...req.body }; - next(); - } - }); - } - }); -}; +export default async (req, res, next) => { + try { + const entity = await req.$odata.Model.findOne({ id: req.params.id }); + const patched = { ...entity.toObject(), ...req.body }; + + await req.$odata.Model.update({ id: req.params.id }, patched); + res.$odata.result = patched; + next(); + + } catch (err) { + next(err); + } +} \ No newline at end of file diff --git a/src/mongo/rest/post.js b/src/mongo/rest/post.js index 49c8e1d..61c4d2e 100644 --- a/src/mongo/rest/post.js +++ b/src/mongo/rest/post.js @@ -1,20 +1,21 @@ -export default (req, res, next) => { - if (!Object.keys(req.body).length) { - const error = new Error(); +export default async (req, res, next) => { + try { + if (!Object.keys(req.body).length) { + const error = new Error(); + + error.status = 422; + throw error; + + } - error.status = 422; - next(error); - } else { const entity = new req.$odata.Model(req.body); - entity.save((err) => { - if (err) { - next(err); - } else { - res.$odata.result = entity.toObject(); - res.$odata.status = 201; - next(); - } - }); + await entity.save(); + res.$odata.result = entity.toObject(); + res.$odata.status = 201; + next(); + + } catch (err) { + next(err); } }; diff --git a/src/mongo/rest/put.js b/src/mongo/rest/put.js index 51eba1f..f0d7627 100644 --- a/src/mongo/rest/put.js +++ b/src/mongo/rest/put.js @@ -1,46 +1,36 @@ -function _updateEntity(req, res, next, entity) { - req.$odata.Model.findByIdAndUpdate(entity.id, req.$odata.body, (err) => { - if (err) { - return next(err); - } - const newEntity = req.$odata.body; - - newEntity.id = entity.id; - res.$odata.result = newEntity; - - return next(); - }); -} - -function _createEntity(req, res, next) { - const uuidReg = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i; - if (!uuidReg.test(req.$odata.$Key._id)) { - const err = new Error('Id is invalid.'); - - err.status = 400; - return next(err); - } - const newEntity = new req.$odata.Model(req.$odata.body); - newEntity._id = req.$odata.$Key._id; - return newEntity.save((err2) => { - if (err2) { - return next(err2); - } - res.$odata.result = newEntity.toObject(); - res.$odata.status = 201; - return next(); - }); -} - -export default (req, res, next) => { - req.$odata.Model.findOne({ _id: req.$odata.$Key._id }, (err, entity) => { - if (err) { - return next(err); - } +export default async (req, res, next) => { + try { + const entity = await req.$odata.Model.findOne({ _id: req.$odata.$Key._id }); + if (entity) { - _updateEntity(req, res, next, entity); + await req.$odata.Model.findByIdAndUpdate(entity.id, req.$odata.body); + + const newEntity = req.$odata.body; + + newEntity.id = entity.id; + res.$odata.result = newEntity; + } else { - _createEntity(req, res, next); + const uuidReg = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i; + + if (!uuidReg.test(req.$odata.$Key._id)) { + const err = new Error('Id is invalid.'); + + err.status = 400; + return next(err); + } + const newEntity = new req.$odata.Model(req.$odata.body); + + newEntity._id = req.$odata.$Key._id; + await newEntity.save(); + + res.$odata.result = newEntity.toObject(); + res.$odata.status = 201; } - }); + + next(); + + } catch (err) { + next(err); + } }; diff --git a/test/mongo/connected/model.complex.js b/test/mongo/connected/model.complex.js index 937807e..b74d50a 100644 --- a/test/mongo/connected/model.complex.js +++ b/test/mongo/connected/model.complex.js @@ -1,5 +1,3 @@ -// For issue: https://github.com/TossShinHwa/node-odata/issues/55 - import 'should'; import request from 'supertest'; import { odata, host, port, assertSuccess } from '../../support/setup'; @@ -25,7 +23,7 @@ function queryResource(id) { .get(`/complex-model('${id}')`); } -describe('model.complex', () => { +describe('mongo.connected.model.complex', () => { let httpServer; before(() => { diff --git a/test/mongo/mocked/model.complex.filter.js b/test/mongo/mocked/model.complex.filter.js index c31661b..9e9cc63 100644 --- a/test/mongo/mocked/model.complex.filter.js +++ b/test/mongo/mocked/model.complex.filter.js @@ -10,7 +10,7 @@ import { init } from '../../support/db'; const Schema = mongoose.Schema; -describe('model.complex.filter', () => { +describe('mongo.mocked.model.complex.filter', () => { let httpServer, modelMock, queryMock; before(() => { @@ -37,13 +37,14 @@ describe('model.complex.filter', () => { modelMock.expects('find').once().withArgs({"product.price": {$gt: 30}}).returns(query); queryMock.expects('select').once().withArgs({ _id: 0, product: 1}); - queryMock.expects('exec').once().callsArgWith(0, null, [{ + queryMock.expects('exec').once() + .returns(new Promise(resolve => resolve([{ toObject: () => ({ product: { price: 50 } }) - }]); + }]))); httpServer = server.listen(port); }); diff --git a/test/mongo/mocked/model.custom.id.js b/test/mongo/mocked/model.custom.id.js index e82b734..b3d2e58 100644 --- a/test/mongo/mocked/model.custom.id.js +++ b/test/mongo/mocked/model.custom.id.js @@ -7,7 +7,7 @@ import { init } from '../../support/db'; const Schema = mongoose.Schema; -describe('model.custom.id', () => { +describe('mongo.mocked.model.custom.id', () => { let httpServer, modelMock, queryMock, Model; before(async function () { @@ -50,13 +50,15 @@ describe('model.custom.id', () => { it('should work when use a custom id to query specific entity', async function () { modelMock = sinon.mock(Model); - modelMock.expects('findById').once().callsArgWith(1, null, { + modelMock.expects('findById').once() + .returns(new Promise(resolve => resolve({ toObject: () => ({ id: 100 }) - }); + }))); const res = await request(host).get('/custom-id(100)'); + assertSuccess(res); res.body.id.should.be.equal(100); modelMock.verify(); @@ -72,12 +74,15 @@ describe('model.custom.id', () => { modelMock = sinon.mock(Model); queryMock = sinon.mock(query); modelMock.expects('find').once().withArgs({id: {$eq: 100}}).returns(query); - queryMock.expects('exec').once().callsArgWith(0, null, [{ + queryMock.expects('exec').once() + .returns(new Promise(resolve => resolve([{ toObject: () => ({ id: 100 }) - }]); + }]))); + const res = await request(host).get('/custom-id?$filter=id eq 100'); + assertSuccess(res); res.body.value.length.should.be.greaterThan(0); queryMock.verify(); diff --git a/test/mongo/mocked/model.hidden.field.js b/test/mongo/mocked/model.hidden.field.js index 3c2a2ad..97ffa8c 100644 --- a/test/mongo/mocked/model.hidden.field.js +++ b/test/mongo/mocked/model.hidden.field.js @@ -7,7 +7,7 @@ import { init } from '../../support/db'; const Schema = mongoose.Schema; -describe('model.hidden.field', function () { +describe('mongo.mocked.model.hidden.field', function () { let httpServer, modelMock, queryMock, Model; before(async function () { @@ -54,12 +54,15 @@ describe('model.hidden.field', function () { _id: 0, name: 1 }); - queryMock.expects('exec').once().callsArgWith(0, null, [{ - toObject: () => ({ - name: 'zack' - }) - }]); + queryMock.expects('exec').once() + .returns(new Promise(resolve => resolve([{ + toObject: () => ({ + name: 'zack' + }) + }]))); + const res = await request(host).get('/hidden-field?$select=name, password'); + assertSuccess(res); res.body.should.deepEqual({ value: [{ @@ -82,13 +85,16 @@ describe('model.hidden.field', function () { modelMock.expects('find').once().returns(query); queryMock.expects('select').never(); - queryMock.expects('exec').once().callsArgWith(0, null, [{ - toObject: () => ({ - _id: 'AFFE', - name: 'zack' - }) - }]); + queryMock.expects('exec').once() + .returns(new Promise(resolve => resolve([{ + toObject: () => ({ + _id: 'AFFE', + name: 'zack' + }) + }]))); + const res = await request(host).get('/hidden-field?$select=password'); + assertSuccess(res); res.body.should.deepEqual({ value: [{ diff --git a/test/mongo/mocked/odata.count.js b/test/mongo/mocked/odata.count.js index 867002d..b2c1876 100644 --- a/test/mongo/mocked/odata.count.js +++ b/test/mongo/mocked/odata.count.js @@ -5,7 +5,7 @@ import { odata, host, port } from '../../support/setup'; import { init } from '../../support/db'; import { BookModel } from '../../support/books.model'; -describe('odata.count', function() { +describe('mongo.mocked.odata.count', function() { let httpServer, modelMock, queryMock; before(async function() { @@ -30,7 +30,8 @@ describe('odata.count', function() { modelMock = sinon.mock(BookModel); queryMock = sinon.mock(query); modelMock.expects('find').once().returns(query); - queryMock.expects('count').once().callsArgWith(0, null, 13); + queryMock.expects('count').once() + .returns(new Promise(resolve => resolve(13))); const res = await request(host).get('/book/$count'); diff --git a/test/mongo/mocked/odata.query.count.js b/test/mongo/mocked/odata.query.count.js index 21acf18..6f93def 100644 --- a/test/mongo/mocked/odata.query.count.js +++ b/test/mongo/mocked/odata.query.count.js @@ -6,7 +6,7 @@ import books from '../../support/books.json'; import { init } from '../../support/db'; import { BookModel } from '../../support/books.model'; -describe('odata.query.count', function () { +describe('mongo.mocked.odata.query.count', function () { const query = { count: () => { }, exec: () => { }, @@ -34,8 +34,10 @@ describe('odata.query.count', function () { modelMock = sinon.mock(BookModel); queryMock = sinon.mock(query); modelMock.expects('find').twice().returns(query); - queryMock.expects('count').once().callsArgWith(0, null, 13); - queryMock.expects('exec').once().callsArgWith(0, null, books.map(item => ({ toObject: () => item }))); + queryMock.expects('count').once() + .returns(new Promise(resolve => resolve(13))); + queryMock.expects('exec').once() + .returns(new Promise(resolve => resolve(books.map(item => ({ toObject: () => item }))))); const res = await request(host).get('/book?$count=true'); assertSuccess(res); @@ -50,7 +52,8 @@ describe('odata.query.count', function () { modelMock = sinon.mock(BookModel); queryMock = sinon.mock(query); modelMock.expects('find').twice().returns(query); - queryMock.expects('exec').once().callsArgWith(0, null, books.map(item => ({ toObject: () => item }))); + queryMock.expects('exec').once() + .returns(new Promise(resolve => resolve(books.map(item => ({ toObject: () => item }))))); const res = await request(host).get('/book?$count=false'); res.body.should.be.not.have.property('@odata.count'); diff --git a/test/mongo/mocked/odata.query.filter.functions.js b/test/mongo/mocked/odata.query.filter.functions.js index 07e86d1..2fb6515 100644 --- a/test/mongo/mocked/odata.query.filter.functions.js +++ b/test/mongo/mocked/odata.query.filter.functions.js @@ -5,7 +5,7 @@ import { odata, host, port, assertSuccess } from '../../support/setup'; import data from '../../support/books.json'; import { BookModel } from '../../support/books.model'; -describe('odata.query.filter.functions', function () { +describe('mongo.mocked.odata.query.filter.functions', function () { const query = { $where: () => { }, where: () => { }, @@ -39,7 +39,8 @@ describe('odata.query.filter.functions', function () { modelMock = sinon.mock(BookModel); queryMock = sinon.mock(query); modelMock.expects('find').once().withArgs({title: {$where: `this.title.indexOf('i') != -1`}}).returns(query); - queryMock.expects('exec').once().callsArgWith(0, null, books.map(item => ({ toObject: () => item }))); + queryMock.expects('exec').once() + .returns(new Promise(resolve => resolve(books.map(item => ({ toObject: () => item }))))); const res = await request(host).get(`/book?$filter=contains(title,'i')`); @@ -56,7 +57,8 @@ describe('odata.query.filter.functions', function () { modelMock = sinon.mock(BookModel); queryMock = sinon.mock(query); modelMock.expects('find').once().withArgs({title: {$where: `this.title.indexOf('Visual Studio') != -1`}}).returns(query); - queryMock.expects('exec').once().callsArgWith(0, null, books.map(item => ({ toObject: () => item }))); + queryMock.expects('exec').once() + .returns(new Promise(resolve => resolve(books.map(item => ({ toObject: () => item }))))); const res = await request(host).get(`/book?$filter=contains(title,'Visual Studio')`); @@ -76,7 +78,8 @@ describe('odata.query.filter.functions', function () { modelMock = sinon.mock(BookModel); queryMock = sinon.mock(query); modelMock.expects('find').once().withArgs({title: {$where: `this.title.indexOf('i') >= 1`}}).returns(query); - queryMock.expects('exec').once().callsArgWith(0, null, books.map(item => ({ toObject: () => item }))); + queryMock.expects('exec').once() + .returns(new Promise(resolve => resolve(books.map(item => ({ toObject: () => item }))))); const res = await request(host).get(`/book?$filter=indexof(title,'i') ge 1`); @@ -93,7 +96,8 @@ describe('odata.query.filter.functions', function () { modelMock = sinon.mock(BookModel); queryMock = sinon.mock(query); modelMock.expects('find').once().withArgs({title: {$where: `this.title.indexOf('Visual Studio') >= 0`}}).returns(query); - queryMock.expects('exec').once().callsArgWith(0, null, books.map(item => ({ toObject: () => item }))); + queryMock.expects('exec').once() + .returns(new Promise(resolve => resolve(books.map(item => ({ toObject: () => item }))))); const res = await request(host).get(`/book?$filter=indexof(title,'Visual Studio') ge 0`); @@ -115,7 +119,8 @@ describe('odata.query.filter.functions', function () { modelMock.expects('find').once().withArgs({ publish_date: {$gte: new Date(2000, 0, 1), $lt: new Date(2001, 0, 1)} }).returns(query); - queryMock.expects('exec').once().callsArgWith(0, null, books.map(item => ({ toObject: () => item }))); + queryMock.expects('exec').once() + .returns(new Promise(resolve => resolve(books.map(item => ({ toObject: () => item }))))); const res = await request(host).get(`/book?$filter=year(publish_date) eq 2000`); diff --git a/test/mongo/mocked/rest.delete.js b/test/mongo/mocked/rest.delete.js index cc84798..65fa792 100644 --- a/test/mongo/mocked/rest.delete.js +++ b/test/mongo/mocked/rest.delete.js @@ -4,7 +4,7 @@ import { odata, host, port, assertSuccess } from '../../support/setup'; import { BookModel } from '../../support/books.model'; import sinon from 'sinon'; -describe('rest.delete', function() { +describe('mongo.mocked.rest.delete', function() { let httpServer, modelMock; before(async function() { @@ -23,8 +23,8 @@ describe('rest.delete', function() { it('should delete resource if it exist', async function() { modelMock = sinon.mock(BookModel); - modelMock.expects('remove').once().withArgs({_id: '1'}) - .callsArgWith(1, null, JSON.stringify({n:1})); + modelMock.expects('deleteOne').once().withArgs({_id: '1'}) + .returns(new Promise(resolve => resolve(JSON.stringify({n:1})))); const res = await request(host).del(`/book('1')`); @@ -34,8 +34,8 @@ describe('rest.delete', function() { }); it('should be 404 if resource not exist', async function() { modelMock = sinon.mock(BookModel); - modelMock.expects('remove').once().withArgs({_id: '666'}) - .callsArgWith(1, null, JSON.stringify({n:0})); + modelMock.expects('deleteOne').once().withArgs({_id: '666'}) + .returns(new Promise(resolve => resolve(JSON.stringify({n:0})))); const res = await request(host).del(`/book('666')`); diff --git a/test/mongo/mocked/rest.get.js b/test/mongo/mocked/rest.get.js index f569369..50c6e97 100644 --- a/test/mongo/mocked/rest.get.js +++ b/test/mongo/mocked/rest.get.js @@ -7,7 +7,7 @@ import sinon from 'sinon'; const Schema = mongoose.Schema; -describe('rest.get', () => { +describe('mongo.mocked.rest.get', () => { const query = { $where: () => { }, limit: () => { }, @@ -55,7 +55,7 @@ describe('rest.get', () => { modelMock = sinon.mock(BookModel); queryMock = sinon.mock(bookQuery); modelMock.expects('find').once().returns(bookQuery); - queryMock.expects('exec').once().callsArgWith(0, null, []); + queryMock.expects('exec').once().returns(new Promise(resolve => resolve([]))); const res = await request(host).get(`/book`); @@ -67,7 +67,7 @@ describe('rest.get', () => { it('should return special resource', async function() { modelMock = sinon.mock(BookModel); modelMock.expects('findById').once().withArgs('1') - .callsArgWith(1, null, {toObject: () => ({title: 'Krieg und Frieden'})}); + .returns(new Promise(resolve => resolve({toObject: () => ({title: 'Krieg und Frieden'})}))); const res = await request(host).get(`/book('1')`); @@ -83,7 +83,7 @@ describe('rest.get', () => { modelMock = sinon.mock(BookModel); queryMock = sinon.mock(bookQuery); modelMock.expects('findById').once().withArgs('1') - .callsArgWith(1, null, null) + .returns(new Promise(resolve => resolve())); const res = await request(host).get(`/book('1')`); res.status.should.be.equal(404); @@ -92,7 +92,7 @@ describe('rest.get', () => { it('should return deep structure', async function() { modelMock = sinon.mock(ComplexModel); modelMock.expects('findById').once().withArgs('1') - .callsArgWith(1, null, {toObject: () => ({p1:{p2: 'Krieg und Frieden'}})}) + .returns(new Promise(resolve => resolve({toObject: () => ({p1:{p2: 'Krieg und Frieden'}})}))); const res = await request(host).get(`/complex-type('1')`); res.body.should.be.have.property('p1'); diff --git a/test/mongo/mocked/rest.put.js b/test/mongo/mocked/rest.put.js index 2e94f52..9e8f5b0 100644 --- a/test/mongo/mocked/rest.put.js +++ b/test/mongo/mocked/rest.put.js @@ -5,7 +5,7 @@ import books from '../../support/books.json'; import { BookModel } from '../../support/books.model'; import sinon from 'sinon'; -describe('rest.put', () => { +describe('mongo.mocked.rest.put', () => { let httpServer, modelMock, bookInstanceMock; before(async function () { @@ -30,9 +30,9 @@ describe('rest.put', () => { book.title = 'modify book'; modelMock = sinon.mock(BookModel); modelMock.expects('findOne').once().withArgs({_id: '1'}) - .callsArgWith(1, null, (JSON.parse(JSON.stringify(books[0])))); + .returns(new Promise((resolve, reject) => resolve(JSON.parse(JSON.stringify(books[0]))))); modelMock.expects('findByIdAndUpdate').once().withArgs('1', book) - .callsArgWith(2, null); + .returns(new Promise(resolve => resolve())); const res = await request(host) .put(`/book('${book.id}')`) @@ -49,11 +49,11 @@ describe('rest.put', () => { book.title = 'new book'; modelMock = sinon.mock(BookModel); modelMock.expects('findOne').once().withArgs({_id: book.id}) - .callsArgWith(1, null, null); + .returns(new Promise(resolve => resolve())) // mocking save method of created with new instance bookInstanceMock = sinon.mock(BookModel.prototype); bookInstanceMock.expects('save').once() - .callsArgWith(0, null); + .returns(new Promise(resolve => resolve())) const res = await request(host) .put(`/book('${book.id}')`) @@ -74,7 +74,7 @@ describe('rest.put', () => { book.title = 'new book'; modelMock = sinon.mock(BookModel); modelMock.expects('findOne').once().withArgs({_id: book.id}) - .callsArgWith(1, null, null); + .returns(new Promise(resolve => resolve())) const res = await request(host).put(`/book('1')`).send(books[0]); diff --git a/test/mongo/mocked/singleton.js b/test/mongo/mocked/singleton.js index 975ffbf..b8213a6 100644 --- a/test/mongo/mocked/singleton.js +++ b/test/mongo/mocked/singleton.js @@ -45,7 +45,8 @@ describe('mongo.mocked.singleton', () => { _id: 0, price: 1 }); - queryMock.expects('exec').once().callsArgWith(0, null, { toObject: () => books[0] }); + queryMock.expects('exec').once() + .returns(new Promise(resolve => resolve({ toObject: () => books[0] }))); const res = await request(host).get('/book?$select=price'); diff --git a/test/support/db.js b/test/support/db.js index 6067865..03ca6fc 100644 --- a/test/support/db.js +++ b/test/support/db.js @@ -12,13 +12,14 @@ export function init(server) { }); } -export function connect(server) { - mongoose.connect(process.env.DATABASE || conn, null, (err) => { - if (err) { - console.error(err.message); - console.error('Failed to connect to database on startup.'); - process.exit(); - } - }); - init(server); +export async function connect(server) { + try { + await mongoose.connect(process.env.DATABASE || conn); + init(server); + + } catch(err) { + console.error(err.message); + console.error('Failed to connect to database on startup.'); + process.exit(); + } } \ No newline at end of file From e77385466d8dbe9ec30c2fea8e923b8fb6c3d60f Mon Sep 17 00:00:00 2001 From: Richard Martens Date: Sat, 5 Aug 2023 21:58:27 +0200 Subject: [PATCH 40/64] upgarde to node 18.17.0 completed --- test/mongo/mocked/odata.query.filter.js | 29 ++++++++++++++++-------- test/mongo/mocked/odata.query.orderby.js | 14 ++++++++---- test/mongo/mocked/odata.query.select.js | 14 ++++++++---- test/mongo/mocked/odata.query.skip.js | 8 ++++--- test/mongo/mocked/odata.query.top.js | 5 ++-- 5 files changed, 45 insertions(+), 25 deletions(-) diff --git a/test/mongo/mocked/odata.query.filter.js b/test/mongo/mocked/odata.query.filter.js index 041a202..ecb6c5e 100644 --- a/test/mongo/mocked/odata.query.filter.js +++ b/test/mongo/mocked/odata.query.filter.js @@ -5,7 +5,7 @@ import { odata, host, port, assertSuccess } from '../../support/setup'; import data from '../../support/books.json'; import { BookModel } from '../../support/books.model'; -describe('odata.query.filter', function () { +describe('mongo.mocked.odata.query.filter', function () { const query = { $where: () => { }, where: () => { }, @@ -41,7 +41,8 @@ describe('odata.query.filter', function () { modelMock = sinon.mock(BookModel); queryMock = sinon.mock(query); modelMock.expects('find').once().withArgs({title: {$eq: 'Midnight Rain'}}).returns(query); - queryMock.expects('exec').once().callsArgWith(0, null, books.map(item => ({ toObject: () => item }))); + queryMock.expects('exec').once() + .returns(new Promise(resolve => resolve(books.map(item => ({ toObject: () => item }))))); const res = await request(host).get(`/book?$filter=title eq 'Midnight Rain'`); @@ -58,7 +59,8 @@ describe('odata.query.filter', function () { modelMock = sinon.mock(BookModel); queryMock = sinon.mock(query); modelMock.expects('find').once().withArgs({author: {$eq: 'Ralls, Kim'}}).returns(query); - queryMock.expects('exec').once().callsArgWith(0, null, books.map(item => ({ toObject: () => item }))); + queryMock.expects('exec').once() + .returns(new Promise(resolve => resolve(books.map(item => ({ toObject: () => item }))))); const res = await request(host).get(`/book?$filter=author eq 'Ralls, Kim'`); @@ -75,7 +77,8 @@ describe('odata.query.filter', function () { modelMock = sinon.mock(BookModel); queryMock = sinon.mock(query); modelMock.expects('find').once().withArgs({_id: {$eq: '2'}}).returns(query); - queryMock.expects('exec').once().callsArgWith(0, null, books.map(item => ({ toObject: () => item }))); + queryMock.expects('exec').once() + .returns(new Promise(resolve => resolve(books.map(item => ({ toObject: () => item }))))); const res = await request(host).get(encodeURI(`/book?$filter=id eq '2'`)); @@ -95,7 +98,8 @@ describe('odata.query.filter', function () { modelMock = sinon.mock(BookModel); queryMock = sinon.mock(query); modelMock.expects('find').once().withArgs({author: {$ne: 'Ralls, Kim'}}).returns(query); - queryMock.expects('exec').once().callsArgWith(0, null, books.map(item => ({ toObject: () => item }))); + queryMock.expects('exec').once() + .returns(new Promise(resolve => resolve(books.map(item => ({ toObject: () => item }))))); const res = await request(host).get(`/book?$filter=author ne 'Ralls, Kim'`); @@ -115,7 +119,8 @@ describe('odata.query.filter', function () { modelMock = sinon.mock(BookModel); queryMock = sinon.mock(query); modelMock.expects('find').once().withArgs({price: {$gt: 36.95}}).returns(query); - queryMock.expects('exec').once().callsArgWith(0, null, books.map(item => ({ toObject: () => item }))); + queryMock.expects('exec').once() + .returns(new Promise(resolve => resolve(books.map(item => ({ toObject: () => item }))))); const res = await request(host).get(`/book?$filter=price gt 36.95`); @@ -135,7 +140,8 @@ describe('odata.query.filter', function () { modelMock = sinon.mock(BookModel); queryMock = sinon.mock(query); modelMock.expects('find').once().withArgs({price: {$gte: 36.95}}).returns(query); - queryMock.expects('exec').once().callsArgWith(0, null, books.map(item => ({ toObject: () => item }))); + queryMock.expects('exec').once() + .returns(new Promise(resolve => resolve(books.map(item => ({ toObject: () => item }))))); const res = await request(host).get(`/book?$filter=price ge 36.95`); @@ -155,7 +161,8 @@ describe('odata.query.filter', function () { modelMock = sinon.mock(BookModel); queryMock = sinon.mock(query); modelMock.expects('find').once().withArgs({price: {$lt: 36.95}}).returns(query); - queryMock.expects('exec').once().callsArgWith(0, null, books.map(item => ({ toObject: () => item }))); + queryMock.expects('exec').once() + .returns(new Promise(resolve => resolve(books.map(item => ({ toObject: () => item }))))); const res = await request(host).get(`/book?$filter=price lt 36.95`); @@ -175,7 +182,8 @@ describe('odata.query.filter', function () { modelMock = sinon.mock(BookModel); queryMock = sinon.mock(query); modelMock.expects('find').once().withArgs({price: {$lte: 36.95}}).returns(query); - queryMock.expects('exec').once().callsArgWith(0, null, books.map(item => ({ toObject: () => item }))); + queryMock.expects('exec').once() + .returns(new Promise(resolve => resolve(books.map(item => ({ toObject: () => item }))))); const res = await request(host).get(`/book?$filter=price le 36.95`); @@ -201,7 +209,8 @@ describe('odata.query.filter', function () { }, { price: {$gte: 36.95} }]}).returns(query); - queryMock.expects('exec').once().callsArgWith(0, null, books.map(item => ({ toObject: () => item }))); + queryMock.expects('exec').once() + .returns(new Promise(resolve => resolve(books.map(item => ({ toObject: () => item }))))); const res = await request(host).get(`/book?$filter=title ne 'Midnight Rain' and price ge 36.95`); diff --git a/test/mongo/mocked/odata.query.orderby.js b/test/mongo/mocked/odata.query.orderby.js index f39b0aa..63e54bd 100644 --- a/test/mongo/mocked/odata.query.orderby.js +++ b/test/mongo/mocked/odata.query.orderby.js @@ -5,7 +5,7 @@ import { odata, host, port, assertSuccess } from '../../support/setup'; import data from '../../support/books.json'; import { BookModel } from '../../support/books.model'; -describe('odata.query.orderby', () => { +describe('mongo.mocked.odata.query.orderby', () => { const query = { $where: () => { }, where: () => { }, @@ -42,7 +42,8 @@ describe('odata.query.orderby', () => { queryMock = sinon.mock(query); modelMock.expects('find').returns(query); queryMock.expects('sort').once().withArgs([['price', 'asc']]); - queryMock.expects('exec').once().callsArgWith(0, null, books.map(item => ({ toObject: () => item }))); + queryMock.expects('exec').once() + .returns(new Promise(resolve => resolve(books.map(item => ({ toObject: () => item }))))); const res = await request(host).get('/book?$orderby=price'); @@ -61,7 +62,8 @@ describe('odata.query.orderby', () => { queryMock = sinon.mock(query); modelMock.expects('find').returns(query); queryMock.expects('sort').once().withArgs([['price', 'asc']]); - queryMock.expects('exec').once().callsArgWith(0, null, books.map(item => ({ toObject: () => item }))); + queryMock.expects('exec').once() + .returns(new Promise(resolve => resolve(books.map(item => ({ toObject: () => item }))))); const res = await request(host).get('/book?$orderby=price asc'); @@ -80,7 +82,8 @@ describe('odata.query.orderby', () => { queryMock = sinon.mock(query); modelMock.expects('find').returns(query); queryMock.expects('sort').once().withArgs([['price', 'desc']]); - queryMock.expects('exec').once().callsArgWith(0, null, books.map(item => ({ toObject: () => item }))); + queryMock.expects('exec').once() + .returns(new Promise(resolve => resolve(books.map(item => ({ toObject: () => item }))))); const res = await request(host).get('/book?$orderby=price desc'); @@ -107,7 +110,8 @@ describe('odata.query.orderby', () => { queryMock = sinon.mock(query); modelMock.expects('find').returns(query); queryMock.expects('sort').once().withArgs([['price', 'asc'], ['title', 'asc']]); - queryMock.expects('exec').once().callsArgWith(0, null, books.map(item => ({ toObject: () => item }))); + queryMock.expects('exec').once() + .returns(new Promise(resolve => resolve(books.map(item => ({ toObject: () => item }))))); const res = await request(host).get('/book?$orderby=price,title'); diff --git a/test/mongo/mocked/odata.query.select.js b/test/mongo/mocked/odata.query.select.js index 1c241ca..b1dcf24 100644 --- a/test/mongo/mocked/odata.query.select.js +++ b/test/mongo/mocked/odata.query.select.js @@ -5,7 +5,7 @@ import { odata, host, port, assertSuccess } from '../../support/setup'; import data from '../../support/books.json'; import { BookModel } from '../../support/books.model'; -describe('odata.query.select', () => { +describe('mongo.mocked.odata.query.select', () => { const query = { $where: () => { }, where: () => { }, @@ -45,7 +45,8 @@ describe('odata.query.select', () => { _id: 0, price: 1 }); - queryMock.expects('exec').once().callsArgWith(0, null, books.map(item => ({ toObject: () => item }))); + queryMock.expects('exec').once() + .returns(new Promise(resolve => resolve(books.map(item => ({ toObject: () => item }))))); const res = await request(host).get('/book?$select=price'); @@ -70,7 +71,8 @@ describe('odata.query.select', () => { price: 1, title: 1 }); - queryMock.expects('exec').once().callsArgWith(0, null, books.map(item => ({ toObject: () => item }))); + queryMock.expects('exec').once() + .returns(new Promise(resolve => resolve(books.map(item => ({ toObject: () => item }))))); const res = await request(host).get('/book?$select=price,title'); @@ -95,7 +97,8 @@ describe('odata.query.select', () => { price: 1, title: 1 }); - queryMock.expects('exec').once().callsArgWith(0, null, books.map(item => ({ toObject: () => item }))); + queryMock.expects('exec').once() + .returns(new Promise(resolve => resolve(books.map(item => ({ toObject: () => item }))))); const res = await request(host).get('/book?$select=price, title'); @@ -121,7 +124,8 @@ describe('odata.query.select', () => { price: 1, title: 1 }); - queryMock.expects('exec').once().callsArgWith(0, null, books.map(item => ({ toObject: () => item }))); + queryMock.expects('exec').once() + .returns(new Promise(resolve => resolve(books.map(item => ({ toObject: () => item }))))); const res = await request(host).get('/book?$select=price,title,id'); diff --git a/test/mongo/mocked/odata.query.skip.js b/test/mongo/mocked/odata.query.skip.js index bdd9b6c..1a2acd7 100644 --- a/test/mongo/mocked/odata.query.skip.js +++ b/test/mongo/mocked/odata.query.skip.js @@ -5,7 +5,7 @@ import { odata, host, port, assertSuccess } from '../../support/setup'; import { BookModel } from '../../support/books.model'; import data from '../../support/books.json'; -describe('odata.query.skip', () => { +describe('mongo.mocked.odata.query.skip', () => { const query = { $where: () => { }, where: () => { }, @@ -39,7 +39,8 @@ describe('odata.query.skip', () => { queryMock = sinon.mock(query); modelMock.expects('find').returns(query); queryMock.expects('skip').once().withArgs(1); - queryMock.expects('exec').once().callsArgWith(0, null, data.map(item => ({ toObject: () => item }))); + queryMock.expects('exec').once() + .returns(new Promise(resolve => resolve(data.map(item => ({ toObject: () => item }))))); const res = await request(host).get('/book?$skip=1'); @@ -52,7 +53,8 @@ describe('odata.query.skip', () => { queryMock = sinon.mock(query); modelMock.expects('find').returns(query); queryMock.expects('skip').once().withArgs(1024); - queryMock.expects('exec').once().callsArgWith(0, null, data.map(item => ({ toObject: () => item }))); + queryMock.expects('exec').once() + .returns(new Promise(resolve => resolve(data.map(item => ({ toObject: () => item }))))); const res = await request(host).get('/book?$skip=1024'); diff --git a/test/mongo/mocked/odata.query.top.js b/test/mongo/mocked/odata.query.top.js index a1041bc..8ceec9b 100644 --- a/test/mongo/mocked/odata.query.top.js +++ b/test/mongo/mocked/odata.query.top.js @@ -5,7 +5,7 @@ import { odata, host, port, assertSuccess } from '../../support/setup'; import { BookModel } from '../../support/books.model'; import data from '../../support/books.json'; -describe('odata.query.top', () => { +describe('mongo.mocked.odata.query.top', () => { const query = { $where: () => { }, limit: () => { }, @@ -38,7 +38,8 @@ describe('odata.query.top', () => { queryMock = sinon.mock(query); modelMock.expects('find').returns(query); queryMock.expects('limit').once().withArgs(1); - queryMock.expects('exec').once().callsArgWith(0, null, data.map(item => ({ toObject: () => item }))); + queryMock.expects('exec').once() + .returns(new Promise(resolve => resolve(data.map(item => ({ toObject: () => item }))))); const res = await request(host).get('/book?$top=1'); From 201878d4700c87404abee0b4cba91fdf33c582e6 Mon Sep 17 00:00:00 2001 From: Richard Martens Date: Fri, 18 Aug 2023 21:52:48 +0200 Subject: [PATCH 41/64] singleton implemented, hidden field attempt now to metadata, fix bug by xml presentation of metadata for array property --- .vscode/launch.json | 2 +- README.md | 3 +- src/mongo/Entity.js | 25 +++-- src/mongo/rest/getSingleton.js | 2 +- src/odata/Metadata.js | 21 ++-- src/odata/entity/Entity.js | 19 ++-- src/odata/entity/Singleton.js | 11 +-- src/parser/multipartMixed.js | 2 +- src/server.js | 10 ++ src/writer/xmlWriter.js | 14 ++- test/metadata/action.js | 49 +++------- test/metadata/complex.type.js | 6 -- test/metadata/custom.resource.js | 13 +-- test/metadata/format.js | 13 +-- test/metadata/function.js | 7 -- test/metadata/singleton.js | 40 ++++++-- test/mongo/connected/model.hidden.field.js | 46 +++++++++ test/mongo/metadata.js | 52 +++------- test/mongo/metadata.resource.complex.js | 81 +++++----------- test/mongo/mocked/model.hidden.field.js | 108 --------------------- test/mongo/mocked/singleton.js | 29 +++++- test/odata.entity.js | 12 +-- test/singleton.js | 25 +++++ test/support/books.model.js | 3 +- 24 files changed, 256 insertions(+), 337 deletions(-) create mode 100644 test/mongo/connected/model.hidden.field.js delete mode 100644 test/mongo/mocked/model.hidden.field.js diff --git a/.vscode/launch.json b/.vscode/launch.json index 753065e..faad020 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -20,7 +20,7 @@ "dot", "--timeout", "300000", - "test/mongo/mocked/rest.get.js" + "test/mongo/mocked/model.hidden.field.js" ], "env": { "LOG_LEVEL": "debug" diff --git a/README.md b/README.md index 95a3547..cf2effc 100644 --- a/README.md +++ b/README.md @@ -99,7 +99,8 @@ const entity = server.entity('user', { }, { $Key: ['title'], id: { - $Type: 'node.odata.ObjectId' + $Type: 'Edm.String', + $MaxLength: 24 }, name: { $Type: 'node.odata.fullName' diff --git a/src/mongo/Entity.js b/src/mongo/Entity.js index 41a72a9..9dfd257 100644 --- a/src/mongo/Entity.js +++ b/src/mongo/Entity.js @@ -18,8 +18,8 @@ export default class MongoEntity { id: { target: '_id', attributes: { - $Type: 'node.odata.ObjectId', - $Nullable: false, + $Type: 'Edm.String', + $MaxLength: 24 } } }; @@ -99,15 +99,15 @@ export default class MongoEntity { visitProperty(node) { const result = {}; + if (node.selected === false) {// hidden field + return; + } + if ('Array ObjectID'.indexOf(node.instance) === -1 && node.defaultValue) { result.$DefaultValue = node.defaultValue; } switch (node.instance) { - case 'ObjectID': - result.$Type = 'node.odata.ObjectId'; - break; - case 'Boolean': result.$Type = 'Edm.Boolean'; break; @@ -137,6 +137,10 @@ export default class MongoEntity { instance: node.options.type[0].name || node.options.type[0].type.name //Enums have an object with enum and type }); + if (!arrayItemType) { // hidden properties returns undefined + return; + } + result.$Type = arrayItemType.$Type; } break; @@ -195,10 +199,13 @@ export default class MongoEntity { this.addMapping(curentProperty, propertyName); } - result = { + const property = this.visitor('Property', node[curentProperty]); + + result = property ? { ...previousProperty, - [propertyName]: this.visitor('Property', node[curentProperty]), - }; + [propertyName]: property + } : previousProperty; + } return result; diff --git a/src/mongo/rest/getSingleton.js b/src/mongo/rest/getSingleton.js index 76f9f0e..eba2c67 100644 --- a/src/mongo/rest/getSingleton.js +++ b/src/mongo/rest/getSingleton.js @@ -19,7 +19,7 @@ export default async (req, res, next) => { Object.keys(res.$odata.result) .forEach(item => { if (req.$odata.$select.indexOf(item) === -1) { - delete req.$odata.result[item]; + delete res.$odata.result[item]; } }); } diff --git a/src/odata/Metadata.js b/src/odata/Metadata.js index ba311a6..8cfe9fc 100644 --- a/src/odata/Metadata.js +++ b/src/odata/Metadata.js @@ -93,7 +93,7 @@ export default class Metadata { }); } - } if (resource instanceof Singleton && !result[currentResource]) { + } if (resource instanceof Singleton && resource.name === resource.entity.name) { result[currentResource] = resource.getMetadata(); } else if (resource instanceof Function) { @@ -108,14 +108,18 @@ export default class Metadata { const result = { ...previousResource }; const resource = this._server.resources[currentResource]; - if (resource instanceof Entity || resource instanceof Singleton) { + if (resource instanceof Entity) { result[currentResource] = { - $Type: `node.odata.${currentResource}` + $Type: `node.odata.${currentResource}`, + $Collection: true }; - if(resource instanceof Entity) { - result[currentResource].$Collection = true; - } + } else if (resource instanceof Singleton) { + const singletonType = resource.name === resource.entity.name ? currentResource : resource.entity.name; + + result[currentResource] = { + $Type: `node.odata.${singletonType}` + }; } else if (resource instanceof Function) { result[currentResource] = { @@ -148,11 +152,6 @@ export default class Metadata { const document = { $Version: '4.0', - ObjectId: { - $Kind: 'TypeDefinition', - $UnderlyingType: 'Edm.String', - $MaxLength: 24, - }, ...this.complexTypes, ...entityTypes, ...unboundActions, diff --git a/src/odata/entity/Entity.js b/src/odata/entity/Entity.js index 6d17e00..749fee8 100644 --- a/src/odata/entity/Entity.js +++ b/src/odata/entity/Entity.js @@ -266,7 +266,6 @@ export default class Entity { getKeyParam(type, name) { switch (type) { case 'Edm.String': - case 'node.odata.ObjectId': return `%27:${name}%27`; default: @@ -274,8 +273,9 @@ export default class Entity { } } - getResourceUrl() { - const resourceListURL = `/${this.name}`; + getResourceUrl(name) { + const entityName = name || this.name; + const resourceListURL = `/${entityName}`; if (this.metadata.$Key.length === 1) { const value = this.getKeyParam(this.metadata[this.metadata.$Key[0]].$Type, this.metadata.$Key[0]); @@ -294,11 +294,12 @@ export default class Entity { } } - getRoutes() { - const resourceListURL = `/${this.name}`; - const resourceListRegex = new RegExp(`(^\/?${this.name}[?#])|(^\/?${this.name}$)`); - const resourceURL = this.getResourceUrl(); - const resourceRegex = new RegExp(`^\/?${this.name}\\([^)]+\\)`); + getRoutes(name) { + const entityName = name || this.name; + const resourceListURL = `/${entityName}`; + const resourceListRegex = new RegExp(`(^\/?${entityName}[?#])|(^\/?${entityName}$)`); + const resourceURL = this.getResourceUrl(name); + const resourceRegex = new RegExp(`^\/?${entityName}\\([^)]+\\)`); return [ { @@ -335,7 +336,7 @@ export default class Entity { name: 'count', method: 'get', url: resourceListURL + '/([\$])count', - regex: new RegExp(`(^\/?${this.name}\/\\$count[?]?)|(^\/?${this.name}\/\\$count$)`) + regex: new RegExp(`(^\/?${entityName}\/\\$count[?]?)|(^\/?${entityName}\/\\$count$)`) }, { name: 'list', diff --git a/src/odata/entity/Singleton.js b/src/odata/entity/Singleton.js index 6cb633a..f29e350 100644 --- a/src/odata/entity/Singleton.js +++ b/src/odata/entity/Singleton.js @@ -13,7 +13,7 @@ export default class Singleton { }; this.name = name; - this.entity = new Entity(name, handler, metadata, mapping); + this.entity = metadata instanceof Entity ? metadata : new Entity(name, handler, metadata, mapping); this.handler = { ...this.entity.handler, // get, post, put, delete, patch @@ -23,13 +23,7 @@ export default class Singleton { }; this.hooks = new Hooks(); - /* - this.metadata = { - $Kind: 'EntityType', - ...metadata - }; - this.mapping = mapping || {};*/ } addBefore(fn, name) { @@ -103,7 +97,8 @@ export default class Singleton { } getRoutes() { - const listRoute = this.entity.getRoutes().find(item => item.name === 'list'); + const name = this.name === this.entity.name ? undefined : this.name; + const listRoute = this.entity.getRoutes(name).find(item => item.name === 'list'); return [ 'post', 'put', 'patch', 'delete', 'get'].map(item => ({ name: item, diff --git a/src/parser/multipartMixed.js b/src/parser/multipartMixed.js index ab71c76..1b13ce2 100644 --- a/src/parser/multipartMixed.js +++ b/src/parser/multipartMixed.js @@ -19,7 +19,7 @@ function multipart(req, res, next) { if (singleRequestText.indexOf("Group ID: ") >= 0) { return; //sap extension, not documentet in odata } - const matchMethodUrl = singleRequestText.match(/^(GET|POST|PUT|PATCH|DELETE)\s+([\w\/\.$-()]+)\s*/m); + const matchMethodUrl = singleRequestText.match(/^(GET|POST|PUT|PATCH|DELETE)\s+([\w\/\.$?=\-()]+)\s*/m); if (!matchMethodUrl) { throw new Error(`Method in ${singleRequestText} not supported`); diff --git a/src/server.js b/src/server.js index 4fa694f..2b4cd61 100644 --- a/src/server.js +++ b/src/server.js @@ -132,6 +132,16 @@ class Server { return this.resources[name]; } + singleton(name, handler, entity) { + if (this.resources[name]) { + throw new Error(`Entity with name "${name}" already defined`); + } + + this.resources[name] = new Singleton(name, handler, entity); + + return this.resources[name]; + } + mongoSingleton(name, model, handler, metadata, mapping) { if (name && !model) { if (!this.resources[name]) { diff --git a/src/writer/xmlWriter.js b/src/writer/xmlWriter.js index fc5ebbf..ddfa13a 100644 --- a/src/writer/xmlWriter.js +++ b/src/writer/xmlWriter.js @@ -95,22 +95,20 @@ export default class XmlWriter { } visitProperty(node, name) { + const type = node.$Collection ? `Collection(${node.$Type})` : node.$Type; let attributes = ''; - if (node.$Nullable === false) { - attributes += ' Nullable="false"'; + if (node.$Nullable) { + attributes += ' Nullable="true"'; } if (node.$MaxLength) { attributes += ` MaxLength="${node.$MaxLength}"`; } - if (node.$Collection) { - attributes += ' Collection="true"'; - } if (node.$DefaultValue) { attributes += ` DefaultValue="${node.$DefaultValue}"`; } - return ``; + return ``; } visitEntityType(node, name) { @@ -186,11 +184,11 @@ export default class XmlWriter { } visitFunction(node, name) { - const collection = node.$ReturnType.$Collection ? ' Collection="true"' : ''; + const type = node.$ReturnType.$Collection ? `Collection(${node.$ReturnType.$Type})` : node.$ReturnType.$Type; return (` - + `); } diff --git a/test/metadata/action.js b/test/metadata/action.js index 4d82adf..d675045 100644 --- a/test/metadata/action.js +++ b/test/metadata/action.js @@ -20,11 +20,6 @@ describe('metadata.action', () => { it('should return json metadata for action that bound to instance', async function() { const jsonDocument = { $Version: '4.0', - ObjectId: { - $Kind: "TypeDefinition", - $UnderlyingType: "Edm.String", - $MaxLength: 24 - }, 'bound-action': { $Kind: 'Action', $IsBound: true, @@ -37,8 +32,8 @@ describe('metadata.action', () => { $Kind: "EntityType", $Key: ["id"], id: { - $Type: "node.odata.ObjectId", - $Nullable: false, + $Type: 'Edm.String', + $MaxLength: 24 }, author: { $Type: 'Edm.String' @@ -56,8 +51,8 @@ describe('metadata.action', () => { server.entity('book', null, { $Key: ['id'], id: { - $Type: 'node.odata.ObjectId', - $Nullable: false + $Type: 'Edm.String', + $MaxLength: 24 }, author: { $Type: 'Edm.String' @@ -76,13 +71,11 @@ describe('metadata.action', () => { ` - - - + @@ -97,8 +90,8 @@ describe('metadata.action', () => { server.entity('book', null, { $Key: ['id'], id: { - $Type: 'node.odata.ObjectId', - $Nullable: false + $Type: 'Edm.String', + $MaxLength: 24 }, author: { $Type: 'Edm.String' @@ -115,11 +108,6 @@ describe('metadata.action', () => { it('should return json metadata for action that bound to collection', async function() { const jsonDocument = { $Version: '4.0', - ObjectId: { - $Kind: "TypeDefinition", - $UnderlyingType: "Edm.String", - $MaxLength: 24 - }, 'bound-action': { $Kind: 'Action', $IsBound: true, @@ -133,8 +121,8 @@ describe('metadata.action', () => { $Kind: "EntityType", $Key: ["id"], id: { - $Type: "node.odata.ObjectId", - $Nullable: false, + $Type: 'Edm.String', + $MaxLength: 24 }, author: { $Type: 'Edm.String' @@ -152,8 +140,8 @@ describe('metadata.action', () => { server.entity('book', null, { $Key: ['id'], id: { - $Type: 'node.odata.ObjectId', - $Nullable: false + $Type: 'Edm.String', + $MaxLength: 24 }, author: { $Type: 'Edm.String' @@ -172,13 +160,11 @@ describe('metadata.action', () => { ` - - - + @@ -193,8 +179,8 @@ describe('metadata.action', () => { server.entity('book', null, { $Key: ['id'], id: { - $Type: 'node.odata.ObjectId', - $Nullable: false + $Type: 'Edm.String', + $MaxLength: 24 }, author: { $Type: 'Edm.String' @@ -224,11 +210,6 @@ describe('metadata.action', () => { it('should return json metadata for unbound action', async function() { const jsonDocument = { $Version: '4.0', - ObjectId: { - $Kind: "TypeDefinition", - $UnderlyingType: "Edm.String", - $MaxLength: 24 - }, 'unbound-action': { $Kind: 'Action', $Parameter: [{ @@ -262,8 +243,6 @@ describe('metadata.action', () => { ` - - diff --git a/test/metadata/complex.type.js b/test/metadata/complex.type.js index 82750e8..48e3a44 100644 --- a/test/metadata/complex.type.js +++ b/test/metadata/complex.type.js @@ -17,11 +17,6 @@ describe('metadata.complex.type', () => { it('should return explizit defined custom type in json format', async function() { const jsonDocument = { $Version: '4.0', - ObjectId: { - $Kind: 'TypeDefinition', - $UnderlyingType: 'Edm.String', - $MaxLength: 24, - }, fullName: { $Kind: "ComplexType", first: { @@ -55,7 +50,6 @@ describe('metadata.complex.type', () => { ` - diff --git a/test/metadata/custom.resource.js b/test/metadata/custom.resource.js index 13e1cd9..0d112bc 100644 --- a/test/metadata/custom.resource.js +++ b/test/metadata/custom.resource.js @@ -21,17 +21,12 @@ describe('metadata.custom.resource', () => { it('should return json metadata for custom resource', async function() { const jsonDocument = { $Version: '4.0', - ObjectId: { - $Kind: "TypeDefinition", - $UnderlyingType: "Edm.String", - $MaxLength: 24 - }, book: { $Kind: "EntityType", $Key: ["id"], id: { - $Type: "node.odata.ObjectId", - $Nullable: false, + $Type: 'Edm.String', + $MaxLength: 24 }, salted: { $Type: 'Edm.Boolean' @@ -49,8 +44,8 @@ describe('metadata.custom.resource', () => { server.entity('book', {}, { $Key: ["id"], id: { - $Type: "node.odata.ObjectId", - $Nullable: false, + $Type: 'Edm.String', + $MaxLength: 24 }, salted: { $Type: 'Edm.Boolean' diff --git a/test/metadata/format.js b/test/metadata/format.js index a32fcce..8b89372 100644 --- a/test/metadata/format.js +++ b/test/metadata/format.js @@ -8,8 +8,8 @@ describe('metadata.format', () => { const metadata = { $Key: ["id"], id: { - $Type: "node.odata.ObjectId", - $Nullable: false, + $Type: 'Edm.String', + $MaxLength: 24 }, author: { $Type: 'Edm.String' @@ -32,11 +32,6 @@ describe('metadata.format', () => { }; const jsonDocument = { $Version: '4.0', - ObjectId: { - $Kind: "TypeDefinition", - $UnderlyingType: "Edm.String", - $MaxLength: 24 - }, book: { $Kind: "EntityType", ...metadata @@ -54,13 +49,11 @@ describe('metadata.format', () => { ` - - - + diff --git a/test/metadata/function.js b/test/metadata/function.js index 5ce80ce..061c7c2 100644 --- a/test/metadata/function.js +++ b/test/metadata/function.js @@ -17,11 +17,6 @@ describe('metadata.function', () => { it('should return json metadata for function', async function() { const jsonDocument = { $Version: '4.0', - ObjectId: { - $Kind: "TypeDefinition", - $UnderlyingType: "Edm.String", - $MaxLength: 24 - }, 'odata-function': { $Kind: 'Function', $ReturnType: { @@ -53,8 +48,6 @@ describe('metadata.function', () => { ` - - diff --git a/test/metadata/singleton.js b/test/metadata/singleton.js index ee7d1da..ced24ce 100644 --- a/test/metadata/singleton.js +++ b/test/metadata/singleton.js @@ -18,11 +18,6 @@ describe('metadata.custom.resource', () => { it('[json] should render entity type if only singleton defined', async function() { const jsonDocument = { $Version: '4.0', - ObjectId: { - $Kind: "TypeDefinition", - $UnderlyingType: "Edm.String", - $MaxLength: 24 - }, book: { $Kind: "EntityType", ...BookMetadata @@ -47,13 +42,11 @@ describe('metadata.custom.resource', () => { ` - - - + @@ -75,4 +68,35 @@ describe('metadata.custom.resource', () => { assertSuccess(res); res.text.should.equal(xmlDocument); }); + + + + it('[json] should render entity type once', async function() { + const jsonDocument = { + $Version: '4.0', + book: { + $Kind: "EntityType", + ...BookMetadata + }, + $EntityContainer: 'node.odata', + ['node.odata']: { + $Kind: 'EntityContainer', + book: { + $Collection: true, + $Type: `node.odata.book` + }, + "current-book": { + $Type: `node.odata.book` + } + }, + }; + const bookEntity = server.entity('book', null, BookMetadata); + server.singleton('current-book', null, bookEntity); + httpServer = server.listen(port); + + const res = await request(host).get('/$metadata?$format=json'); + + assertSuccess(res); + res.body.should.deepEqual(jsonDocument); + }); }); diff --git a/test/mongo/connected/model.hidden.field.js b/test/mongo/connected/model.hidden.field.js new file mode 100644 index 0000000..aa4179c --- /dev/null +++ b/test/mongo/connected/model.hidden.field.js @@ -0,0 +1,46 @@ +import 'should'; +import request from 'supertest'; +import { odata, host, port } from '../../support/setup'; +import mongoose from 'mongoose'; +import { init } from '../../support/db'; + +const Schema = mongoose.Schema; + +describe('mongo.connected.model.hidden.field', function () { + let httpServer, Model; + + before(async function () { + const server = odata(); + + const ModelSchema = new Schema({ + name: String, + password: { + type: String, + select: false + } + }); + + Model = mongoose.model('hidden-field', ModelSchema); + + server.mongoEntity('hidden-field', Model); + init(server); + + httpServer = server.listen(port); + + }); + + after(() => { + httpServer.close(); + }); + + it('should fail because a property is requested that is not visible', async function () { + const res = await request(host).get('/hidden-field?$select=name, password'); + + res.should.have.property('error'); + res.error.should.not.be.equal(false); + res.body.error.code.should.be.equal('400'); + res.body.error.message.should.be.equal(`Entity 'hidden-field' has no property named 'password'`); + + }); + +}); diff --git a/test/mongo/metadata.js b/test/mongo/metadata.js index a507395..2c74067 100644 --- a/test/mongo/metadata.js +++ b/test/mongo/metadata.js @@ -28,17 +28,12 @@ describe('metadata', () => { it('should return json metadata and ignore unknown attributes', async function() { const jsonDocument = { $Version: '4.0', - ObjectId: { - $Kind: "TypeDefinition", - $UnderlyingType: "Edm.String", - $MaxLength: 24 - }, book: { $Kind: "EntityType", $Key: ["id"], id: { - $Type: "node.odata.ObjectId", - $Nullable: false, + $Type: 'Edm.String', + $MaxLength: 24 }, price: { $Type: 'Edm.Double' @@ -90,15 +85,13 @@ describe('metadata', () => { ` - - - + @@ -136,17 +129,12 @@ describe('metadata', () => { it('should return json metadata with maxLength attribute', async function() { const jsonDocument = { $Version: '4.0', - ObjectId: { - $Kind: "TypeDefinition", - $UnderlyingType: "Edm.String", - $MaxLength: 24 - }, book: { $Kind: "EntityType", $Key: ["id"], id: { - $Type: "node.odata.ObjectId", - $Nullable: false, + $Type: 'Edm.String', + $MaxLength: 24 }, author: { $Type: 'Edm.String', @@ -183,14 +171,12 @@ describe('metadata', () => { ` - - - + @@ -217,17 +203,12 @@ describe('metadata', () => { it('should return json metadata with default value attribute', async function() { const jsonDocument = { $Version: '4.0', - ObjectId: { - $Kind: "TypeDefinition", - $UnderlyingType: "Edm.String", - $MaxLength: 24 - }, book: { $Kind: "EntityType", $Key: ["id"], id: { - $Type: "node.odata.ObjectId", - $Nullable: false, + $Type: 'Edm.String', + $MaxLength: 24 }, author: { $Type: 'Edm.String', @@ -264,14 +245,12 @@ describe('metadata', () => { ` - - - + @@ -298,17 +277,12 @@ describe('metadata', () => { it('should return json metadata with boolean property', async function() { const jsonDocument = { $Version: '4.0', - ObjectId: { - $Kind: "TypeDefinition", - $UnderlyingType: "Edm.String", - $MaxLength: 24 - }, book: { $Kind: "EntityType", $Key: ["id"], id: { - $Type: "node.odata.ObjectId", - $Nullable: false, + $Type: 'Edm.String', + $MaxLength: 24 }, salted: { $Type: 'Edm.Boolean' @@ -343,14 +317,12 @@ describe('metadata', () => { ` - - - + diff --git a/test/mongo/metadata.resource.complex.js b/test/mongo/metadata.resource.complex.js index 6a38d74..c08c54d 100644 --- a/test/mongo/metadata.resource.complex.js +++ b/test/mongo/metadata.resource.complex.js @@ -1,6 +1,3 @@ -// For issue: https://github.com/TossShinHwa/node-odata/issues/96 -// For issue: https://github.com/TossShinHwa/node-odata/issues/25 - import 'should'; import request from 'supertest'; import { host, port, odata, assertSuccess } from '../support/setup'; @@ -8,7 +5,7 @@ import mongoose from 'mongoose'; const Schema = mongoose.Schema; -describe('metadata.resource.complex', () => { +describe('mongo.metadata.resource.complex', () => { let httpServer, server; before(() => { @@ -26,16 +23,11 @@ describe('metadata.resource.complex', () => { it('should return json metadata for nested document array', async function() { const jsonDocument = { $Version: '4.0', - ObjectId: { - $Kind: "TypeDefinition", - $UnderlyingType: "Edm.String", - $MaxLength: 24 - }, "complex-modelp1Child1": { $Kind: 'ComplexType', id: { - $Type: "node.odata.ObjectId", - $Nullable: false, + $Type: 'Edm.String', + $MaxLength: 24 }, p2: { $Type: 'Edm.String' @@ -45,8 +37,8 @@ describe('metadata.resource.complex', () => { $Kind: "EntityType", $Key: ["id"], id: { - $Type: "node.odata.ObjectId", - $Nullable: false, + $Type: 'Edm.String', + $MaxLength: 24 }, p1: { $Type: 'node.odata.complex-modelp1Child1', @@ -81,18 +73,16 @@ describe('metadata.resource.complex', () => { ` - - - + - - + + @@ -117,17 +107,12 @@ describe('metadata.resource.complex', () => { it('should return json metadata for nested array', async function() { const jsonDocument = { $Version: '4.0', - ObjectId: { - $Kind: "TypeDefinition", - $UnderlyingType: "Edm.String", - $MaxLength: 24 - }, 'complex-model': { $Kind: "EntityType", $Key: ["id"], id: { - $Type: "node.odata.ObjectId", - $Nullable: false, + $Type: 'Edm.String', + $MaxLength: 24 }, p3: { $Type: 'Edm.String', @@ -160,14 +145,12 @@ describe('metadata.resource.complex', () => { ` - - - - + + @@ -193,14 +176,12 @@ describe('metadata.resource.complex', () => { ` - - - - + + @@ -227,11 +208,6 @@ describe('metadata.resource.complex', () => { it('should return json metadata for nested document in document', async function() { const jsonDocument = { $Version: '4.0', - ObjectId: { - $Kind: "TypeDefinition", - $UnderlyingType: "Edm.String", - $MaxLength: 24 - }, "complex-modelp4Child1": { $Kind: 'ComplexType', p5: { @@ -242,8 +218,8 @@ describe('metadata.resource.complex', () => { $Kind: "EntityType", $Key: ["id"], id: { - $Type: "node.odata.ObjectId", - $Nullable: false, + $Type: 'Edm.String', + $MaxLength: 24 }, p4: { $Type: 'node.odata.complex-modelp4Child1' @@ -277,8 +253,6 @@ describe('metadata.resource.complex', () => { ` - - @@ -286,7 +260,7 @@ describe('metadata.resource.complex', () => { - + @@ -312,11 +286,6 @@ describe('metadata.resource.complex', () => { it('should return json metadata for nested document in array', async function() { const jsonDocument = { $Version: '4.0', - ObjectId: { - $Kind: "TypeDefinition", - $UnderlyingType: "Edm.String", - $MaxLength: 24 - }, p1p2Child1: { $Kind: "ComplexType", p3: { @@ -326,8 +295,8 @@ describe('metadata.resource.complex', () => { $Type: "node.odata.p1p4Child2" }, id: { - $Type: "node.odata.ObjectId", - $Nullable: false, + $Type: 'Edm.String', + $MaxLength: 24 } }, p1p4Child2:{ @@ -340,8 +309,8 @@ describe('metadata.resource.complex', () => { $Kind: "EntityType", $Key: ["id"], id: { - $Type: "node.odata.ObjectId", - $Nullable: false, + $Type: 'Edm.String', + $MaxLength: 24 }, p2: { $Type: 'node.odata.p1p2Child1', @@ -379,22 +348,20 @@ describe('metadata.resource.complex', () => { ` - - - + - - + + diff --git a/test/mongo/mocked/model.hidden.field.js b/test/mongo/mocked/model.hidden.field.js deleted file mode 100644 index 97ffa8c..0000000 --- a/test/mongo/mocked/model.hidden.field.js +++ /dev/null @@ -1,108 +0,0 @@ -import 'should'; -import sinon from 'sinon'; -import request from 'supertest'; -import { odata, host, port, assertSuccess } from '../../support/setup'; -import mongoose from 'mongoose'; -import { init } from '../../support/db'; - -const Schema = mongoose.Schema; - -describe('mongo.mocked.model.hidden.field', function () { - let httpServer, modelMock, queryMock, Model; - - before(async function () { - const server = odata(); - - const ModelSchema = new Schema({ - name: String, - password: { - type: String, - select: false - } - }); - - Model = mongoose.model('hidden-field', ModelSchema); - - server.mongoEntity('hidden-field', Model); - init(server); - - httpServer = server.listen(port); - - }); - - after(() => { - httpServer.close(); - }); - - afterEach(() => { - modelMock.restore(); - queryMock?.restore(); - }); - - it('should work when get entities list even it is selected', async function () { - const query = { - where: () => { }, - select: () => { }, - exec: () => { }, - model: Model - }; - modelMock = sinon.mock(Model); - queryMock = sinon.mock(query); - modelMock.expects('find').once().returns(query); - - queryMock.expects('select').once().withArgs({ - _id: 0, - name: 1 - }); - queryMock.expects('exec').once() - .returns(new Promise(resolve => resolve([{ - toObject: () => ({ - name: 'zack' - }) - }]))); - - const res = await request(host).get('/hidden-field?$select=name, password'); - - assertSuccess(res); - res.body.should.deepEqual({ - value: [{ - name: 'zack' - }] - }); - modelMock.verify(); - queryMock.verify(); - }); - - it('should work when get entities list even only it is selected', async function () { - const query = { - where: () => { }, - select: () => { }, - exec: () => { }, - model: Model - }; - modelMock = sinon.mock(Model); - queryMock = sinon.mock(query); - modelMock.expects('find').once().returns(query); - - queryMock.expects('select').never(); - queryMock.expects('exec').once() - .returns(new Promise(resolve => resolve([{ - toObject: () => ({ - _id: 'AFFE', - name: 'zack' - }) - }]))); - - const res = await request(host).get('/hidden-field?$select=password'); - - assertSuccess(res); - res.body.should.deepEqual({ - value: [{ - id: 'AFFE', - name: 'zack' - }] - }); - modelMock.verify(); - queryMock.verify(); - }); -}); diff --git a/test/mongo/mocked/singleton.js b/test/mongo/mocked/singleton.js index b8213a6..fdc336a 100644 --- a/test/mongo/mocked/singleton.js +++ b/test/mongo/mocked/singleton.js @@ -15,7 +15,7 @@ describe('mongo.mocked.singleton', () => { exec: () => { }, model: BookModel }; - let httpServer, modelMock, queryMock; + let httpServer, modelMock, queryMock, bookInstanceMock; before(async function() { const server = odata(); @@ -31,6 +31,7 @@ describe('mongo.mocked.singleton', () => { afterEach(() => { modelMock?.restore(); queryMock?.restore(); + bookInstanceMock?.restore(); }); it('should select anyone field', async function() { @@ -55,4 +56,30 @@ describe('mongo.mocked.singleton', () => { queryMock.verify(); res.body.should.deepEqual(books[0]); }); + + it('should select anyone field for upsert', async function() { + const books = data.map(item => ({ + price: item.price + })); + + modelMock = sinon.mock(BookModel); + queryMock = sinon.mock(query); + modelMock.expects('findOne').once().returns(query); + queryMock.expects('select').once().withArgs({ + _id: 0, + price: 1 + }); + queryMock.expects('exec').once() + .returns(new Promise(resolve => resolve(undefined))); + bookInstanceMock = sinon.mock(BookModel.prototype); + bookInstanceMock.expects('toObject').once().returns(JSON.parse(JSON.stringify(data[0]))); + + const res = await request(host).get('/book?$select=price'); + + assertSuccess(res); + modelMock.verify(); + queryMock.verify(); + bookInstanceMock.verify(); + res.body.should.deepEqual(books[0]); + }); }); diff --git a/test/odata.entity.js b/test/odata.entity.js index 2fca922..31145aa 100644 --- a/test/odata.entity.js +++ b/test/odata.entity.js @@ -36,8 +36,8 @@ describe('odata.entity', () => { }, { $Key: ['id'], id: { - $Type: 'node.odata.ObjectId', - $Nullable: false + $Type: 'Edm.String', + $MaxLength: 24 }, title: { $Type: 'Edm.String' @@ -75,8 +75,8 @@ describe('odata.entity', () => { server.entity('book', null, { $Key: ['id'], id: { - $Type: 'node.odata.ObjectId', - $Nullable: false + $Type: 'Edm.String', + $MaxLength: 24 }, title: { $Type: 'Edm.String' @@ -108,8 +108,8 @@ describe('odata.entity', () => { }, { $Key: ['id'], id: { - $Type: 'node.odata.ObjectId', - $Nullable: false + $Type: 'Edm.String', + $MaxLength: 24 }, createdAt: { $Type: 'Edm.DateTimeOffset' diff --git a/test/singleton.js b/test/singleton.js index 7de2ad4..194ea17 100644 --- a/test/singleton.js +++ b/test/singleton.js @@ -38,4 +38,29 @@ describe('singleton', () => { res.body.should.deepEqual(result); }); + + it('should work if entityset and singleton defined', async function () { + const result = { + "id": '1', + "price": 44.95, + "title": "XML Developer's Guide" + }; + const book = server.entity('book', null, BookMetadata); + server.singleton('current-book', { + get: (req, res, next) => { + res.$odata.result = result; + next(); + } + }, book); + httpServer = server.listen(port); + + const res = await request(host).get(`/current-book`); + + if (!res.ok) { + res.res.statusMessage.should.be.equal(''); + } + + res.body.should.deepEqual(result); + }); + }); diff --git a/test/support/books.model.js b/test/support/books.model.js index a775d48..a33a08c 100644 --- a/test/support/books.model.js +++ b/test/support/books.model.js @@ -25,7 +25,8 @@ export const BookModel = mongoose.model('Book', BookSchema); export const BookMetadata = { $Key: ['id'], id: { - $Type: 'node.odata.ObjectId' + $Type: 'Edm.String', + $MaxLength: 24 }, author: { $Type: 'Edm.String' From 073056600a55128ed560005e12ce6baaa9294a8b Mon Sep 17 00:00:00 2001 From: Richard Martens Date: Fri, 1 Sep 2023 09:37:46 +0200 Subject: [PATCH 42/64] singelton entity and annotations --- .vscode/launch.json | 2 +- src/mongo/Entity.js | 56 +++++--- src/mongo/Singleton.js | 4 +- src/odata/Action.js | 66 ++++++--- src/odata/Metadata.js | 3 + src/odata/Vocabulary.js | 106 ++++++++++++++ src/odata/entity/Entity.js | 26 +++- src/odata/entity/Singleton.js | 4 +- src/odata/entity/parser/filter.js | 2 +- src/odata/entity/parser/keys.js | 2 +- src/odata/{entity => }/parser/value.js | 0 src/odata/validator.js | 6 +- src/server.js | 26 ++-- src/writer/xmlWriter.js | 44 +++++- test/metadata/annotation.js | 179 ++++++++++++++++++++++++ test/mongo/connected/rest.list.js | 2 +- test/mongo/metadata.js | 21 +-- test/mongo/metadata.resource.complex.js | 164 +++++++++++++++------- test/odata.actions.parameter.js | 69 +++++++++ 19 files changed, 669 insertions(+), 113 deletions(-) create mode 100644 src/odata/Vocabulary.js rename src/odata/{entity => }/parser/value.js (100%) create mode 100644 test/metadata/annotation.js create mode 100644 test/odata.actions.parameter.js diff --git a/.vscode/launch.json b/.vscode/launch.json index faad020..bb44d54 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -20,7 +20,7 @@ "dot", "--timeout", "300000", - "test/mongo/mocked/model.hidden.field.js" + "test/odata.actions.parameter.js" ], "env": { "LOG_LEVEL": "debug" diff --git a/src/mongo/Entity.js b/src/mongo/Entity.js index 9dfd257..d9e86d0 100644 --- a/src/mongo/Entity.js +++ b/src/mongo/Entity.js @@ -8,9 +8,10 @@ import count from './rest/count'; import { validate, validateIdentifier } from '../odata/validator'; export default class MongoEntity { - constructor(name, model) { + constructor(name, model, annotations, mapping) { this.name = name; this.model = model; + this.annotations = annotations; this.complexTypes = {}; this.count = 0; @@ -21,7 +22,8 @@ export default class MongoEntity { $Type: 'Edm.String', $MaxLength: 24 } - } + }, + ...mapping }; } @@ -97,12 +99,16 @@ export default class MongoEntity { } visitProperty(node) { - const result = {}; + let result = {}; if (node.selected === false) {// hidden field return; } + if (!node.isRequired) { + result.$Nullable = true; + } + if ('Array ObjectID'.indexOf(node.instance) === -1 && node.defaultValue) { result.$DefaultValue = node.defaultValue; } @@ -117,6 +123,18 @@ export default class MongoEntity { break; case 'Date': + // annotate generated timestamps as readonly + const { createdAt, updatedAt } = this.model.schema.$timestamps; + + if ((node.path === createdAt || node.path === updatedAt) + && this.annotations.isDefined('readonly')) { + debugger; + result = { + ...result, + ...this.annotations.annotate('readonly', 'Property', true) + }; + } + result.$Type = 'Edm.DateTimeOffset'; break; @@ -153,6 +171,12 @@ export default class MongoEntity { } complexType(node) { + const mapping = Object.keys(this.mapping).find(item => this.mapping[item].target === node.path); + + if (mapping && this.mapping[mapping].attributes?.$Type) { + return this.mapping[mapping].attributes?.$Type; + } + this.count += 1; const notClassifiedName = `${this.name}${node.path}Child${this.count}`; @@ -191,7 +215,7 @@ export default class MongoEntity { result = { ...previousProperty, [propertyName]: this.mapping[propertyName].attributes - } + }; } else { propertyName = curentProperty.replace(/\./g, '-'); @@ -238,20 +262,20 @@ export default class MongoEntity { return previousProperty; }, {}); - - const deepProperties = Object.keys(deepNodes) - .reduce((previousProperty, curentProperty) => { - previousProperty[curentProperty] = { - $Type: this.complexType(deepNodes[curentProperty]) - }; - return previousProperty; - }, {}); + const deepProperties = Object.keys(deepNodes) + .reduce((previousProperty, curentProperty) => { + previousProperty[curentProperty] = { + $Type: this.complexType(deepNodes[curentProperty]) + }; - return { - ...simpleProperties, - ...deepProperties - } + return previousProperty; + }, {}); + + return { + ...simpleProperties, + ...deepProperties + } } visitEntityType(node) { diff --git a/src/mongo/Singleton.js b/src/mongo/Singleton.js index 55a3b6f..eb6cf81 100644 --- a/src/mongo/Singleton.js +++ b/src/mongo/Singleton.js @@ -6,9 +6,9 @@ import patch from './rest/patch'; import getSingleton from "./rest/getSingleton"; export default class MongoSingleton { - constructor(name, model) { + constructor(name, model, annotations, mapping) { this.name = name; - this.entity = new MongoEntity(name, model); + this.entity = new MongoEntity(name, model, annotations, mapping); } diff --git a/src/odata/Action.js b/src/odata/Action.js index 67bd1ee..9dceb35 100644 --- a/src/odata/Action.js +++ b/src/odata/Action.js @@ -33,8 +33,14 @@ export default class Action { if (options) { this.binding = options.binding; this.resource = options.resource; - this.$Parameter = options.$Parameter; + this.$Parameter = options.$Parameter || []; + + if (this.binding === 'entity') { + this.addBefore(this.resource.getNavigation().beforeHooks); + } } + + this.addBefore(this.parseParameter.bind(this)); } addBefore(fn, name) { @@ -47,25 +53,18 @@ export default class Action { match(method, url) { const regex = this.getPath(true); - const beforeHooks = this.binding === 'entity' ? [...this.resource.getNavigation().beforeHooks, ...this.hooks.before] : this.hooks.before; - if (method === 'post' && url.match(regex)) { - return [...beforeHooks, this.fn, ...this.hooks.after]; + return this.getMiddlewares(); } } getRouter() { if (!this.router) { - validateIdentifier(this.name); - - if (this.$Parameter) { - validateParameters(this.$Parameter); - } - const path = this.getPath(); - - this.router = this.getOperationRouter(path, this.fn); + + this.router = Router(); + this.router.post(path, ...this.getMiddlewares()); } return this.router; @@ -85,7 +84,7 @@ export default class Action { }]; } - if (this.$Parameter) { + if (this.$Parameter.length) { if (!result.$Parameter) { result.$Parameter = []; } @@ -135,12 +134,41 @@ export default class Action { return path; } - getOperationRouter(path, fn) { - let router = Router(); - const beforeHooks = this.binding === 'entity' ? [...this.resource.getNavigation().beforeHooks, ...this.hooks.before] : this.hooks.before; + getMiddlewares() { + if (!this.midddlewares) { + validateIdentifier(this.name); + + if (this.$Parameter.length) { + validateParameters(this.$Parameter); + } - router.post(path, ...beforeHooks, fn, ...this.hooks.after); + this.midddlewares = [...this.hooks.before, this.fn, ...this.hooks.after]; + } + + return this.midddlewares; + } + + parseParameter(req, res, next) { + if (req.body) { + req.$odata.$Parameter = {}; + } + + this.$Parameter.forEach(param => { + if (req.body && req.body[param.$Name]) { + req.$odata.$Parameter[param.$Name] = this.parseValue(param.$Type, req.body[param.$Name]); + + } else if (param.$Nullable && (!req.body || !req.body[param.$Name])) { + const error = new Error(`Obligatory parameter '${param.$Name}' not given`); + + error.status = 400; + + throw error; + } + }); + } + + parseValue(type, value) { + + } - return router; - }; } \ No newline at end of file diff --git a/src/odata/Metadata.js b/src/odata/Metadata.js index 8cfe9fc..55575b6 100644 --- a/src/odata/Metadata.js +++ b/src/odata/Metadata.js @@ -76,6 +76,8 @@ export default class Metadata { validate(typeObject); this.complexTypes[name] = typeObject; + + return this.complexTypes[name]; } ctrl() { @@ -152,6 +154,7 @@ export default class Metadata { const document = { $Version: '4.0', + ...this._server.annotations.getMetadata(), ...this.complexTypes, ...entityTypes, ...unboundActions, diff --git a/src/odata/Vocabulary.js b/src/odata/Vocabulary.js new file mode 100644 index 0000000..75dacf5 --- /dev/null +++ b/src/odata/Vocabulary.js @@ -0,0 +1,106 @@ +export default class Vocabulary { + constructor() { + this.terms = {}; + } + + getMetadata() { + const result = {}; + const names = Object.keys(this.terms) + .forEach(name => { + result[name] = { + $Kind: 'Term', + ...this.terms[name] + } + }); + + return result; + } + + isDefined(name) { + return this.terms[name] ? true : false; + } + + define(name, prototype, scope) { + if (this.terms[name]) { + throw new Error(`Annotation with name '${name}' is allready defined`); + } + + const type = this.getType(prototype); + const supportedTargets = [ 'Action', 'Action Import', 'Complex Type', 'Entity Container', + 'Entity Set', 'Entity Type', 'Enumeration Type', 'Enumeration Type Member', 'Function', + 'Function Import', 'Navigation Property', 'Parameter', 'Property', 'Return Type', + 'Singleton', 'Type Definition' ]; + + if (scope && scope.length) { + scope.forEach(target => { + if (supportedTargets.indexOf(target) === -1) { + throw new Error(`Target '${target}' is not supported`); + } + }); + } + + this.terms[name] = { + $AppliesTo: scope && scope.length ? scope : supportedTargets, + $Type: type + }; + } + + annotate(name, destination, value) { + const anno = this.terms[name]; + + if (!anno) { + throw new Error(`Annotation with name '${name}' is not defined`); + } + + const currentType = this.getTypeOf(value); + if (currentType !== anno.$Type) { + throw new Error(`Annotation '${name}' requires type '${anno.$Type}'. Given '${currentType}'`); + } + + if (!destination) { + throw new Error(`Parameter 'destination' should be given`); + } + + if (anno.$AppliesTo.indexOf(destination) === -1) { + throw new Error(`Annotation '${name}' can not assigned to '${destination}'`); + } + + return { + [`@${name}`]: value + } + } + + getTypeOf(value) { + const type = typeof value; + + switch (type) { + case 'number': + return 'Edm.Double'; + + case 'string': + return 'Edm.String'; + + case 'boolean': + return 'Edm.Boolean'; + + default: + throw Error(`type of value '${value}' is not supported`); + } + } + + getType(prototype) { + switch (prototype) { + case Number: + return 'Edm.Double'; + + case String: + return 'Edm.String'; + + case Boolean: + return 'Edm.Boolean'; + + default: + throw Error(`Type '${prototype}' is not supported`); + } + } +} \ No newline at end of file diff --git a/src/odata/entity/Entity.js b/src/odata/entity/Entity.js index 749fee8..dc535d8 100644 --- a/src/odata/entity/Entity.js +++ b/src/odata/entity/Entity.js @@ -10,7 +10,7 @@ import parseOrderBy from "./parser/orderby"; import { parseSkip, parseTop } from "./parser/skiptop"; export default class Entity { - constructor(name, handler, metadata, settings, mapping) { + constructor(name, handler, metadata, settings, annotations, mapping) { const notImplemented = op => (req, res) => { const error = new Error(`Operation '${op}' is not implemented'`); @@ -41,6 +41,7 @@ export default class Entity { ...settings }; + this.annotations = annotations; this.mapping = mapping || {}; } @@ -263,6 +264,29 @@ export default class Entity { return this.metadata; } + annotateProperty(prop, anno, value) { + if (!prop) { + throw new Error('Property name should be given'); + } + if (!anno) { + throw new Error('Name of annotation term should be given'); + } + const term = `@${anno}`; + + if (!this.metadata[prop]) { + throw new Error(`Entity '${this.name}' doesn't have property named '${prop}'`); + } + + if (this.metadata[prop][term]) { + throw new Error(`property '${prop}' is allready annotated with term '${anno}'`); + } + + this.metadata[prop] = { + ...this.metadata[prop], + ...this.annotations.annotate(anno, 'Property', value) + }; + } + getKeyParam(type, name) { switch (type) { case 'Edm.String': diff --git a/src/odata/entity/Singleton.js b/src/odata/entity/Singleton.js index f29e350..fa01748 100644 --- a/src/odata/entity/Singleton.js +++ b/src/odata/entity/Singleton.js @@ -4,7 +4,7 @@ import Hooks from "../Hooks"; import { Router } from 'express'; export default class Singleton { - constructor(name, handler, metadata, mapping) { + constructor(name, handler, metadata, annotations, mapping) { const notSupported = (req, res) => { const error = new Error(); @@ -13,7 +13,7 @@ export default class Singleton { }; this.name = name; - this.entity = metadata instanceof Entity ? metadata : new Entity(name, handler, metadata, mapping); + this.entity = metadata instanceof Entity ? metadata : new Entity(name, handler, metadata, annotations, mapping); this.handler = { ...this.entity.handler, // get, post, put, delete, patch diff --git a/src/odata/entity/parser/filter.js b/src/odata/entity/parser/filter.js index 8305024..62976f9 100644 --- a/src/odata/entity/parser/filter.js +++ b/src/odata/entity/parser/filter.js @@ -1,4 +1,4 @@ -import parseValue from './value'; +import parseValue from '../parser/value'; import parseProperty from './property'; import validateProperty from '../validators/property'; diff --git a/src/odata/entity/parser/keys.js b/src/odata/entity/parser/keys.js index 7667f5f..5ae0231 100644 --- a/src/odata/entity/parser/keys.js +++ b/src/odata/entity/parser/keys.js @@ -1,4 +1,4 @@ -import parseValue from './value'; +import parseValue from '../parser/value'; import parseProperty from './property'; import validateProperty from '../validators/property'; diff --git a/src/odata/entity/parser/value.js b/src/odata/parser/value.js similarity index 100% rename from src/odata/entity/parser/value.js rename to src/odata/parser/value.js diff --git a/src/odata/validator.js b/src/odata/validator.js index add58d4..aa4d32d 100644 --- a/src/odata/validator.js +++ b/src/odata/validator.js @@ -94,7 +94,11 @@ export const validateProperty = (name, property) => { break; default: - throw new Error(`'${member.trim()}' ist not allowed as member of property '${name}'`); + const trimmedMember = member.trim(); + + if (!trimmedMember.match(/^(@\w+(\.\w+)?(#\w+)?)+$/)) { // annotations should be ignored + throw new Error(`'${trimmedMember}' ist not allowed as member of property '${name}'`); + } } }); } diff --git a/src/server.js b/src/server.js index 2b4cd61..8df8a2d 100644 --- a/src/server.js +++ b/src/server.js @@ -13,6 +13,7 @@ import writer from './middlewares/writer'; import Hooks from './odata/Hooks'; import multipartMixed from './parser/multipartMixed'; import Singleton from './odata/entity/Singleton'; +import Vocabulary from './odata/Vocabulary'; function checkAuth(auth, req) { return !auth || auth(req); @@ -58,6 +59,11 @@ class Server { this.hooks.addAfter(writer, 'writer', true); this._serviceDocument = new ServiceDocument(this); + this.annotations = new Vocabulary(); + } + + vocabulary() { + return this.annotations; } addBefore(fn, name) { @@ -84,7 +90,7 @@ class Server { maxTop: this._settings.maxTop, orderby: this._settings.orderby, ...settings - }, mapping); + }, this.annotations, mapping); return this.resources[name]; } @@ -97,7 +103,7 @@ class Server { return this.resources[name]; } - const entity = new MongoEntity(name, model); + const entity = new MongoEntity(name, model, this.annotations, mapping); const complexTypes = entity.getComplexTypes(); @@ -116,10 +122,7 @@ class Server { }, { ...entity.getMetadata(), ...metadata - }, settings, { - ...entity.getMapping(), - ...mapping - }); + }, settings, entity.getMapping()); } singleton(name, handler, metadata, mapping) { @@ -127,7 +130,7 @@ class Server { throw new Error(`Entity with name "${name}" already defined`); } - this.resources[name] = new Singleton(name, handler, metadata, mapping); + this.resources[name] = new Singleton(name, handler, metadata, this.annotations, mapping); return this.resources[name]; } @@ -137,7 +140,7 @@ class Server { throw new Error(`Entity with name "${name}" already defined`); } - this.resources[name] = new Singleton(name, handler, entity); + this.resources[name] = new Singleton(name, handler, entity, this.annotations); return this.resources[name]; } @@ -150,7 +153,7 @@ class Server { return this.resources[name]; } - const entity = new MongoSingleton(name, model); + const entity = new MongoSingleton(name, model, this.annotations, mapping); const complexTypes = entity.entity.getComplexTypes(); @@ -215,7 +218,10 @@ class Server { } complexType(name, properties) { - this.resources.$metadata.complexType(name, properties); + if (!properties) { + throw new Error('Metadata for complex type should be given'); + } + return this.resources.$metadata.complexType(name, properties); } listen(...args) { diff --git a/src/writer/xmlWriter.js b/src/writer/xmlWriter.js index ddfa13a..cabd6c4 100644 --- a/src/writer/xmlWriter.js +++ b/src/writer/xmlWriter.js @@ -37,14 +37,29 @@ export default class XmlWriter { case 'FunctionImport': return this.visitFunctionImport(node, name); + case 'Term': + return this.visitTerm(node, name); + default: throw new Error(`Type ${type} is not supported`); } } + visitTerm(node, name) { + const appliesTo = node.$AppliesTo.reduce((previos, current) => { + return previos ? `${previos} ${current}` : current; + }, ""); + + return ` + + `; + } + visitDocument(node) { let body = ''; + this.document = node; + Object.keys(node).forEach((subnode) => { if (node[subnode].$Kind) { body += this.visitor(node[subnode].$Kind, node[subnode], subnode); @@ -80,7 +95,7 @@ export default class XmlWriter { .forEach((item) => { if (node[item].$Collection === true) { entitySets += this.visitor('EntitySet', node[item], item); - } else if(node[item].$Type) { + } else if (node[item].$Type) { singletons += this.visitor('Singleton', node[item], item); } else if (node[item].$Action) { actions += this.visitor('ActionImport', node[item], item); @@ -96,6 +111,9 @@ export default class XmlWriter { visitProperty(node, name) { const type = node.$Collection ? `Collection(${node.$Type})` : node.$Type; + const annotations = Object.keys(node) + .filter(attribute => attribute[0] === '@') + .reduce((previous, current) => `${previous}${this.visitAnnotation(node[current], current)}`, ""); let attributes = ''; if (node.$Nullable) { @@ -108,7 +126,29 @@ export default class XmlWriter { attributes += ` DefaultValue="${node.$DefaultValue}"`; } - return ``; + if (!annotations) { + return ``; + } + return ` + ${annotations} + `; + + } + + visitAnnotation(node, name) { + const termName = name.substr(1); + const term = this.document[termName]; + + if (!term) { + throw new Error(`Term '${termName}' is not defined in scope`); + } + + const type = term.$Type.split('.')[1]; + + return ` + + <${type}>${node} + `; } visitEntityType(node, name) { diff --git a/test/metadata/annotation.js b/test/metadata/annotation.js new file mode 100644 index 0000000..7522363 --- /dev/null +++ b/test/metadata/annotation.js @@ -0,0 +1,179 @@ +import 'should'; +import request from 'supertest'; +import { host, port, odata, assertSuccess } from '../support/setup'; +import should from 'should'; + +describe('metadata.annotations', () => { + let httpServer, server; + + beforeEach(async function() { + server = odata(); + + }); + + afterEach(() => { + httpServer.close(); + }); + + it('should return json metadata with property annotation', async function() { + const jsonDocument = { + $Version: '4.0', + readonly: { + $Kind: "Term", + $Type: "Edm.Boolean", + $AppliesTo: [ + "Property" + ], + }, + book: { + $Kind: "EntityType", + $Key: ["id"], + id: { + $Type: 'Edm.String', + $MaxLength: 24 + }, + author: { + $Type: 'Edm.String', + '@readonly': true + } + }, + $EntityContainer: 'node.odata', + ['node.odata']: { + $Kind: 'EntityContainer', + book: { + $Collection: true, + $Type: `node.odata.book`, + } + }, + }; + const vocabulary = server.vocabulary(); + + vocabulary.define('readonly', Boolean, ['Property']); + + server.entity('book', null, { + $Key: ['id'], + id: { + $Type: 'Edm.String', + $MaxLength: 24 + }, + author: { + $Type: 'Edm.String', + ...vocabulary.annotate('readonly', 'Property', true) + } + }); + + httpServer = server.listen(port); + const res = await request(host).get('/$metadata?$format=json'); + assertSuccess(res); + res.body.should.deepEqual(jsonDocument); + }); + + it('should return xml metadata with property annotation', async function() { + const xmlDocument = + ` + + + + + + + + + + + true + + + + + + + + + `.replace(/\s*\s*/g, '>'); + const vocabulary = server.vocabulary(); + + vocabulary.define('readonly', Boolean, ['Property']); + server.entity('book', null, { + $Key: ['id'], + id: { + $Type: 'Edm.String', + $MaxLength: 24 + }, + author: { + $Type: 'Edm.String', + ...vocabulary.annotate('readonly', 'Property', true) + } + }); + httpServer = server.listen(port); + const res = await request(host).get('/$metadata').set('accept', 'application/xml'); + assertSuccess(res); + res.text.should.equal(xmlDocument); + }); + + it('should fail for not defined property annotation', async function() { + try { + const vocabulary = server.vocabulary(); + vocabulary.annotate('unknown', 'Property', true); + should.fail(false, true, 'No exception thrown'); + + } catch(err) { + err.message.should.equal(`Annotation with name 'unknown' is not defined`); + } + }); + + it('should works with later annotations', async function() { + const jsonDocument = { + $Version: '4.0', + readonly: { + $Kind: "Term", + $Type: "Edm.Boolean", + $AppliesTo: [ + "Property" + ], + }, + book: { + $Kind: "EntityType", + $Key: ["id"], + id: { + $Type: 'Edm.String', + $MaxLength: 24 + }, + author: { + $Type: 'Edm.String', + '@readonly': true + } + }, + $EntityContainer: 'node.odata', + ['node.odata']: { + $Kind: 'EntityContainer', + book: { + $Collection: true, + $Type: `node.odata.book`, + } + }, + }; + const vocabulary = server.vocabulary(); + + vocabulary.define('readonly', Boolean, ['Property']); + + const book = server.entity('book', null, { + $Key: ['id'], + id: { + $Type: 'Edm.String', + $MaxLength: 24 + }, + author: { + $Type: 'Edm.String' + } + }); + + book.annotateProperty('author', 'readonly', true); + + httpServer = server.listen(port); + const res = await request(host).get('/$metadata?$format=json'); + assertSuccess(res); + res.body.should.deepEqual(jsonDocument); + }); + +}); diff --git a/test/mongo/connected/rest.list.js b/test/mongo/connected/rest.list.js index 7195fe6..52dfb3f 100644 --- a/test/mongo/connected/rest.list.js +++ b/test/mongo/connected/rest.list.js @@ -5,7 +5,7 @@ import mongoose from 'mongoose'; import { connect } from '../../support/db'; import { BookModel } from '../../support/books.model'; -describe('mongo.Entity', () => { +describe('mongo.connected.rest.list', () => { let httpServer before(() => { diff --git a/test/mongo/metadata.js b/test/mongo/metadata.js index 2c74067..05fb318 100644 --- a/test/mongo/metadata.js +++ b/test/mongo/metadata.js @@ -22,7 +22,6 @@ describe('metadata', () => { afterEach(() => { httpServer.close(); - debugger; }); it('should return json metadata and ignore unknown attributes', async function() { @@ -36,7 +35,8 @@ describe('metadata', () => { $MaxLength: 24 }, price: { - $Type: 'Edm.Double' + $Type: 'Edm.Double', + $Nullable: true }, author: { $Type: 'Edm.String' @@ -89,7 +89,7 @@ describe('metadata', () => { - + @@ -138,7 +138,8 @@ describe('metadata', () => { }, author: { $Type: 'Edm.String', - $MaxLength: 25 + $MaxLength: 25, + $Nullable: true } }, $EntityContainer: 'node.odata', @@ -175,7 +176,7 @@ describe('metadata', () => { - + @@ -212,7 +213,8 @@ describe('metadata', () => { }, author: { $Type: 'Edm.String', - $DefaultValue: "William Shakespeare" + $DefaultValue: "William Shakespeare", + $Nullable: true } }, $EntityContainer: 'node.odata', @@ -249,7 +251,7 @@ describe('metadata', () => { - + @@ -285,7 +287,8 @@ describe('metadata', () => { $MaxLength: 24 }, salted: { - $Type: 'Edm.Boolean' + $Type: 'Edm.Boolean', + $Nullable: true } }, $EntityContainer: 'node.odata', @@ -321,7 +324,7 @@ describe('metadata', () => { - + diff --git a/test/mongo/metadata.resource.complex.js b/test/mongo/metadata.resource.complex.js index c08c54d..bbaab38 100644 --- a/test/mongo/metadata.resource.complex.js +++ b/test/mongo/metadata.resource.complex.js @@ -12,7 +12,7 @@ describe('mongo.metadata.resource.complex', () => { mongoose.set('overwriteModels', true); }) - beforeEach(async function() { + beforeEach(async function () { server = odata(); }); @@ -20,7 +20,7 @@ describe('mongo.metadata.resource.complex', () => { httpServer.close(); }); - it('should return json metadata for nested document array', async function() { + it('should return json metadata for nested document array', async function () { const jsonDocument = { $Version: '4.0', "complex-modelp1Child1": { @@ -30,7 +30,8 @@ describe('mongo.metadata.resource.complex', () => { $MaxLength: 24 }, p2: { - $Type: 'Edm.String' + $Type: 'Edm.String', + $Nullable: true } }, 'complex-model': { @@ -42,7 +43,8 @@ describe('mongo.metadata.resource.complex', () => { }, p1: { $Type: 'node.odata.complex-modelp1Child1', - $Collection: true + $Collection: true, + $Nullable: true } }, $EntityContainer: 'node.odata', @@ -56,10 +58,10 @@ describe('mongo.metadata.resource.complex', () => { }; const ComplexModelSchema = new Schema({ p1: [{ // array of objects - p2: String + p2: String }] }); - + const ComplexModel = mongoose.model('complex-model', ComplexModelSchema); server.mongoEntity('complex-model', ComplexModel); httpServer = server.listen(port); @@ -68,20 +70,20 @@ describe('mongo.metadata.resource.complex', () => { res.body.should.deepEqual(jsonDocument); }); - it('should return xml metadata for nested document array', async function() { - const xmlDocument = - ` + it('should return xml metadata for nested document array', async function () { + const xmlDocument = + ` - + - + @@ -92,10 +94,10 @@ describe('mongo.metadata.resource.complex', () => { `.replace(/\s*\s*/g, '>'); const ComplexModelSchema = new Schema({ p1: [{ // array of objects - p2: String + p2: String }] }); - + const ComplexModel = mongoose.model('complex-model', ComplexModelSchema); server.mongoEntity('complex-model', ComplexModel); httpServer = server.listen(port); @@ -104,7 +106,7 @@ describe('mongo.metadata.resource.complex', () => { res.text.should.equal(xmlDocument); }); - it('should return json metadata for nested array', async function() { + it('should return json metadata for nested array', async function () { const jsonDocument = { $Version: '4.0', 'complex-model': { @@ -116,7 +118,8 @@ describe('mongo.metadata.resource.complex', () => { }, p3: { $Type: 'Edm.String', - $Collection: true + $Collection: true, + $Nullable: true } }, $EntityContainer: 'node.odata', @@ -131,7 +134,7 @@ describe('mongo.metadata.resource.complex', () => { const ComplexModelSchema = new Schema({ p3: [String] // array of primitive type }); - + const ComplexModel = mongoose.model('complex-model', ComplexModelSchema); server.mongoEntity('complex-model', ComplexModel); httpServer = server.listen(port); @@ -140,16 +143,16 @@ describe('mongo.metadata.resource.complex', () => { res.body.should.deepEqual(jsonDocument); }); - it('should return xml metadata for nested array', async function() { - const xmlDocument = - ` + it('should return xml metadata for nested array', async function () { + const xmlDocument = + ` - + @@ -161,7 +164,7 @@ describe('mongo.metadata.resource.complex', () => { const ComplexModelSchema = new Schema({ p3: [String] // array of primitive type }); - + const ComplexModel = mongoose.model('complex-model', ComplexModelSchema); server.mongoEntity('complex-model', ComplexModel); httpServer = server.listen(port); @@ -171,16 +174,16 @@ describe('mongo.metadata.resource.complex', () => { }); - it('should return xml metadata for nested enum array', async function() { - const xmlDocument = - ` + it('should return xml metadata for nested enum array', async function () { + const xmlDocument = + ` - + @@ -195,7 +198,7 @@ describe('mongo.metadata.resource.complex', () => { enum: ['P4'] }] }); - + const ComplexModel = mongoose.model('complex-model', ComplexModelSchema); server.mongoEntity('complex-model', ComplexModel); httpServer = server.listen(port); @@ -205,13 +208,14 @@ describe('mongo.metadata.resource.complex', () => { }); - it('should return json metadata for nested document in document', async function() { + it('should return json metadata for nested document in document', async function () { const jsonDocument = { $Version: '4.0', "complex-modelp4Child1": { $Kind: 'ComplexType', p5: { - $Type: 'Edm.String' + $Type: 'Edm.String', + $Nullable: true } }, 'complex-model': { @@ -239,7 +243,7 @@ describe('mongo.metadata.resource.complex', () => { p5: String } }); - + const ComplexModel = mongoose.model('complex-model', ComplexModelSchema); server.mongoEntity('complex-model', ComplexModel); httpServer = server.listen(port); @@ -248,13 +252,13 @@ describe('mongo.metadata.resource.complex', () => { res.body.should.deepEqual(jsonDocument); }); - it('should return xml metadata for nested document in document', async function() { - const xmlDocument = - ` + it('should return xml metadata for nested document in document', async function () { + const xmlDocument = + ` - + @@ -274,7 +278,7 @@ describe('mongo.metadata.resource.complex', () => { p5: String } }); - + const ComplexModel = mongoose.model('complex-model', ComplexModelSchema); server.mongoEntity('complex-model', ComplexModel); httpServer = server.listen(port); @@ -283,13 +287,14 @@ describe('mongo.metadata.resource.complex', () => { res.text.should.equal(xmlDocument); }); - it('should return json metadata for nested document in array', async function() { + it('should return json metadata for nested document in array', async function () { const jsonDocument = { $Version: '4.0', p1p2Child1: { $Kind: "ComplexType", p3: { - $Type: 'Edm.String' + $Type: 'Edm.String', + $Nullable: true }, p4: { $Type: "node.odata.p1p4Child2" @@ -299,10 +304,11 @@ describe('mongo.metadata.resource.complex', () => { $MaxLength: 24 } }, - p1p4Child2:{ + p1p4Child2: { $Kind: "ComplexType", p5: { - $Type: 'Edm.String' + $Type: 'Edm.String', + $Nullable: true } }, p1: { @@ -314,7 +320,8 @@ describe('mongo.metadata.resource.complex', () => { }, p2: { $Type: 'node.odata.p1p2Child1', - $Collection: true + $Collection: true, + $Nullable: true } }, $EntityContainer: 'node.odata', @@ -334,7 +341,7 @@ describe('mongo.metadata.resource.complex', () => { } }] }); - + const ComplexModel = mongoose.model('p1', ComplexModelSchema); server.mongoEntity('p1', ComplexModel); httpServer = server.listen(port); @@ -343,16 +350,16 @@ describe('mongo.metadata.resource.complex', () => { res.body.should.deepEqual(jsonDocument); }); - it('should return xml metadata for nested document in document', async function() { - const xmlDocument = - ` + it('should return xml metadata for nested document in document', async function () { + const xmlDocument = + ` - + - + @@ -360,7 +367,7 @@ describe('mongo.metadata.resource.complex', () => { - + @@ -377,7 +384,7 @@ describe('mongo.metadata.resource.complex', () => { } }] }); - + const ComplexModel = mongoose.model('p1', ComplexModelSchema); server.mongoEntity('p1', ComplexModel); httpServer = server.listen(port); @@ -385,4 +392,67 @@ describe('mongo.metadata.resource.complex', () => { assertSuccess(res); res.text.should.equal(xmlDocument); }); + + it('should use mapping if given', async function () { + const jsonDocument = { + $Version: '4.0', + myComlexType: { + $Kind: "ComplexType", + id: { + $Type: 'Edm.String', + $MaxLength: 24 + }, + p3: { + $Type: 'Edm.String' + } + }, + p1: { + $Kind: "EntityType", + $Key: ["id"], + id: { + $Type: 'Edm.String', + $MaxLength: 24 + }, + p2: { + $Type: 'node.odata.myComlexType' + } + }, + $EntityContainer: 'node.odata', + ['node.odata']: { + $Kind: 'EntityContainer', + p1: { + $Collection: true, + $Type: `node.odata.p1`, + } + }, + }; + const ComplexModelSchema = new Schema({ + p2: { + p3: String + } + }); + + const ComplexModel = mongoose.model('p1', ComplexModelSchema); + server.complexType('myComlexType', { + id: { + $Type: 'Edm.String', + $MaxLength: 24 + }, + p3: { + $Type: 'Edm.String' + } + }); + server.mongoEntity('p1', ComplexModel, undefined, undefined, undefined, { + p2: { + target: 'p2', + attributes: { + $Type: 'node.odata.myComlexType' + } + } + }); + httpServer = server.listen(port); + const res = await request(host).get('/$metadata?$format=json'); + res.statusCode.should.equal(200); + res.body.should.deepEqual(jsonDocument); + }); }); diff --git a/test/odata.actions.parameter.js b/test/odata.actions.parameter.js new file mode 100644 index 0000000..0a97ff8 --- /dev/null +++ b/test/odata.actions.parameter.js @@ -0,0 +1,69 @@ +import 'should'; +import request from 'supertest'; +import { odata, host, port } from './support/setup'; + +describe('odata.actions', () => { + let httpServer, server; + + beforeEach(async function () { + server = odata(); + }); + + afterEach(() => { + if (httpServer) { + httpServer.close(); + } + }); + + + it('should parse given parameter', async function () { + const parameter = { + newPassword: 'All parameters given', + repeat: 'Content is everything, but not yet' + }; + + server.action('change-password', (req, res, next) => { + req.$odata.$Parameter.should.deepEqual(parameter); + res.$odata.status = 204; + next(); + }, { + $Parameter: [{ + $Type: 'Edm.String', + $Name: 'newPassword' + }, { + $Type: 'Edm.String', + $Name: 'repeat' + }] + }); + httpServer = server.listen(port); + + const res = await request(host).post(`/node.odata.change-password`).send(parameter); + res.statusCode.should.equal(204); + }); + + it('should fail if required parameter is not given', async function () { + server.action('change-password', (req, res, next) => { + should.fail(false, true, 'Not aborted although required parameters were not specified') + }, { + $Parameter: [{ + $Type: 'Edm.String', + $Name: 'newPassword' + }, { + $Type: 'Edm.String', + $Name: 'repeat' + }] + }); + httpServer = server.listen(port); + + const res = await request(host).post(`/node.odata.change-password`).send({ + newPassword: 'Not all parameters given' + }); + res.statusCode.should.equal(400); + res.body.should.deepEqual({ + error: { + message: `Obligatory parameter 'repeat' not given` + } + }); + }); + +}); From c80a6352e14d4e8292d772e04791fb7ecf314e0e Mon Sep 17 00:00:00 2001 From: Richard Martens Date: Fri, 1 Sep 2023 16:19:39 +0200 Subject: [PATCH 43/64] [b] Action parameters can be annotated --- .vscode/launch.json | 2 +- src/mongo/Entity.js | 1 - src/odata/Action.js | 143 ++++++++++++++++++------------ src/odata/Hooks.js | 8 -- src/odata/Metadata.js | 3 +- src/odata/entity/Entity.js | 2 +- src/odata/entity/parser/filter.js | 2 +- src/odata/entity/parser/keys.js | 2 +- src/server.js | 2 +- src/writer/xmlWriter.js | 32 ++++--- test/metadata/annotation.js | 120 ++++++++++++++++++++++--- test/odata.actions.parameter.js | 4 +- 12 files changed, 222 insertions(+), 99 deletions(-) diff --git a/.vscode/launch.json b/.vscode/launch.json index bb44d54..a9d86b2 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -20,7 +20,7 @@ "dot", "--timeout", "300000", - "test/odata.actions.parameter.js" + "test/metadata/annotation.js" ], "env": { "LOG_LEVEL": "debug" diff --git a/src/mongo/Entity.js b/src/mongo/Entity.js index d9e86d0..bc673cb 100644 --- a/src/mongo/Entity.js +++ b/src/mongo/Entity.js @@ -128,7 +128,6 @@ export default class MongoEntity { if ((node.path === createdAt || node.path === updatedAt) && this.annotations.isDefined('readonly')) { - debugger; result = { ...result, ...this.annotations.annotate('readonly', 'Property', true) diff --git a/src/odata/Action.js b/src/odata/Action.js index 9dceb35..3901a3b 100644 --- a/src/odata/Action.js +++ b/src/odata/Action.js @@ -1,48 +1,68 @@ import { Router } from 'express'; import Hooks from './Hooks'; import { validateParameters, validateIdentifier } from './validator'; -import Console from '../writer/Console'; +import parseValue from './parser/value'; export default class Action { - constructor(name, fn, options) { + constructor(name, fn, annotations, options) { this.name = name; this.fn = async (req, res, next) => { try { - const con = new Console(); - - con.debug(`Action ${this.name} started`); - res.$odata.status = 200; + const result = fn(req, res, next); if (result || res.$odata.status === 204) { - if (result.then) { + if (result?.then) { await result; - } + } next(); } - + } catch (error) { next(error); } } this.hooks = new Hooks(); - if (options) { - this.binding = options.binding; - this.resource = options.resource; - this.$Parameter = options.$Parameter || []; + this.binding = options?.binding; + this.resource = options?.resource; + this.$Parameter = options?.$Parameter || []; + this.annotations = annotations; - if (this.binding === 'entity') { - this.addBefore(this.resource.getNavigation().beforeHooks); - } + if (this.binding === 'entity') { + this.addBefore(this.resource.getNavigation().beforeHooks); } this.addBefore(this.parseParameter.bind(this)); } + annotateParameter(name, anno, value) { + if (!name) { + throw new Error('Parameter name should be given'); + } + if (!anno) { + throw new Error('Name of annotation term should be given'); + } + const term = `@${anno}`; + + this.getMetadata(); + + const param = this.metadata.$Parameter.find(item => item.$Name === name); + + if (!param) { + throw new Error(`Entity '${this.name}' doesn't have parameter named '${name}'`); + } + + if (param[term]) { + throw new Error(`Parameter '${name}' is allready annotated with term '${anno}'`); + } + + param[term] = this.annotations.annotate(anno, 'Parameter', value)[term]; + } + addBefore(fn, name) { this.hooks.addBefore(fn, name); } @@ -62,7 +82,7 @@ export default class Action { getRouter() { if (!this.router) { const path = this.getPath(); - + this.router = Router(); this.router.post(path, ...this.getMiddlewares()); } @@ -71,37 +91,41 @@ export default class Action { } getMetadata() { - const result = { - $Kind: 'Action' - }; - - if (this.binding) { - result.$IsBound = true; - result.$Parameter = [{ - $Name: this.resource.name, - $Type: `node.odata.${this.resource.name}`, - $Collection: this.binding === 'collection' ? true : undefined, - }]; - } - - if (this.$Parameter.length) { - if (!result.$Parameter) { - result.$Parameter = []; + if (!this.metadata) { + const result = { + $Kind: 'Action' + }; + + if (this.binding) { + result.$IsBound = true; + result.$Parameter = [{ + $Name: this.resource.name, + $Type: `node.odata.${this.resource.name}`, + $Collection: this.binding === 'collection' ? true : undefined, + }]; } - this.$Parameter.forEach(para => { - const item = para; - - if (para.$Type.search(/^edm/i) === -1 ) { - item.$Type = `${para.$Type}`; + if (this.$Parameter.length) { + if (!result.$Parameter) { + result.$Parameter = []; } - result.$Parameter.push(item); - }); - result.$Parameter = result.$Parameter ? result.$Parameter.concat() : this.$Parameter; + this.$Parameter.forEach(para => { + const item = para; + + if (para.$Type.search(/^edm/i) === -1) { + item.$Type = `${para.$Type}`; + } + + result.$Parameter.push(item); + }); + result.$Parameter = result.$Parameter ? result.$Parameter.concat() : this.$Parameter; + } + + this.metadata = result; } - return result; + return this.metadata; } getPath(asRegex) { @@ -149,26 +173,29 @@ export default class Action { } parseParameter(req, res, next) { - if (req.body) { - req.$odata.$Parameter = {}; - } + try { + if (req.body) { + req.$odata.$Parameter = {}; + } - this.$Parameter.forEach(param => { - if (req.body && req.body[param.$Name]) { - req.$odata.$Parameter[param.$Name] = this.parseValue(param.$Type, req.body[param.$Name]); - - } else if (param.$Nullable && (!req.body || !req.body[param.$Name])) { - const error = new Error(`Obligatory parameter '${param.$Name}' not given`); + this.$Parameter.forEach(param => { + if (req.body && req.body[param.$Name]) { + req.$odata.$Parameter[param.$Name] = parseValue(req.body[param.$Name], param); - error.status = 400; + } else if (!param.$Nullable && (!req.body || !req.body[param.$Name])) { + const error = new Error(`Obligatory parameter '${param.$Name}' not given`); - throw error; - } - }); - } + error.status = 400; - parseValue(type, value) { - + throw error; + } + }); + + next(); + + } catch (err) { + next(err); + } } } \ No newline at end of file diff --git a/src/odata/Hooks.js b/src/odata/Hooks.js index a02e656..fa22c3b 100644 --- a/src/odata/Hooks.js +++ b/src/odata/Hooks.js @@ -1,5 +1,3 @@ -import Console from "../writer/Console"; - export default class Hooks { constructor() { this.before = []; @@ -22,12 +20,6 @@ export default class Hooks { suppressNext(fn, name, isFinal) { return async (req, res, next) => { try { - if (name) { - const con = new Console(); - - con.debug(`Hook ${name} started`); - } - const combine = new Promise(async (resolve, reject) => { try { const prom = fn(req, res, err => { diff --git a/src/odata/Metadata.js b/src/odata/Metadata.js index 55575b6..6fac6c7 100644 --- a/src/odata/Metadata.js +++ b/src/odata/Metadata.js @@ -145,7 +145,6 @@ export default class Metadata { const unboundActions = actionNames.reduce((previousAction, currentAction) => { const result = { ...previousAction }; const action = this._server.actions[currentAction]; - const attachToRoot = (name, value) => { result[name] = value; }; result[currentAction] = action.getMetadata(); @@ -163,7 +162,7 @@ export default class Metadata { $Kind: 'EntityContainer', ...entitySets, ...actionImports - }, + } }; return new Promise((resolve) => { diff --git a/src/odata/entity/Entity.js b/src/odata/entity/Entity.js index dc535d8..961d4da 100644 --- a/src/odata/entity/Entity.js +++ b/src/odata/entity/Entity.js @@ -54,7 +54,7 @@ export default class Entity { } action(name, fn, options) { - this.actions[name] = new Action(name, fn, + this.actions[name] = new Action(name, fn, this.annotations, { ...options, resource: this diff --git a/src/odata/entity/parser/filter.js b/src/odata/entity/parser/filter.js index 62976f9..4123595 100644 --- a/src/odata/entity/parser/filter.js +++ b/src/odata/entity/parser/filter.js @@ -1,4 +1,4 @@ -import parseValue from '../parser/value'; +import parseValue from '../../parser/value'; import parseProperty from './property'; import validateProperty from '../validators/property'; diff --git a/src/odata/entity/parser/keys.js b/src/odata/entity/parser/keys.js index 5ae0231..fdff432 100644 --- a/src/odata/entity/parser/keys.js +++ b/src/odata/entity/parser/keys.js @@ -1,4 +1,4 @@ -import parseValue from '../parser/value'; +import parseValue from '../../parser/value'; import parseProperty from './property'; import validateProperty from '../validators/property'; diff --git a/src/server.js b/src/server.js index 8df8a2d..419a410 100644 --- a/src/server.js +++ b/src/server.js @@ -184,7 +184,7 @@ class Server { } action(name, fn, options) { - this.actions[name] = new Action(name, fn, options); + this.actions[name] = new Action(name, fn, this.annotations, options); return this.actions[name]; } diff --git a/src/writer/xmlWriter.js b/src/writer/xmlWriter.js index cabd6c4..fbc521d 100644 --- a/src/writer/xmlWriter.js +++ b/src/writer/xmlWriter.js @@ -198,18 +198,28 @@ export default class XmlWriter { visitAction(node, name) { const isBound = node.$IsBound ? ' IsBound="true"' : ''; - const parameter = node.$Parameter && node.$Parameter.map((item) => { - let type = ''; - - if (item.$Collection) { - type = ` Type="Collection(${item.$Type})"`; - } else if (item.$Type) { - type = ` Type="${item.$Type}"`; - } - - return ``; - }); + const parameter = node.$Parameter && node.$Parameter + .map((item) => { + const annotations = Object.keys(item) + .filter(attribute => attribute[0] === '@') + .reduce((previous, current) => `${previous}${this.visitAnnotation(item[current], current)}`, ""); + let type = ''; + + if (item.$Collection) { + type = ` Type="Collection(${item.$Type})"`; + } else if (item.$Type) { + type = ` Type="${item.$Type}"`; + } + if (!annotations) { + return ``; + } + return ` + ${annotations} + `; + }) + .reduce((previos, current) => `${previos}${current}`, ''); + debugger; return (` ${parameter || ''} diff --git a/test/metadata/annotation.js b/test/metadata/annotation.js index 7522363..8478555 100644 --- a/test/metadata/annotation.js +++ b/test/metadata/annotation.js @@ -6,16 +6,16 @@ import should from 'should'; describe('metadata.annotations', () => { let httpServer, server; - beforeEach(async function() { + beforeEach(async function () { server = odata(); - + }); afterEach(() => { httpServer.close(); }); - it('should return json metadata with property annotation', async function() { + it('should return json metadata with property annotation', async function () { const jsonDocument = { $Version: '4.0', readonly: { @@ -59,7 +59,7 @@ describe('metadata.annotations', () => { author: { $Type: 'Edm.String', ...vocabulary.annotate('readonly', 'Property', true) - } + } }); httpServer = server.listen(port); @@ -67,10 +67,10 @@ describe('metadata.annotations', () => { assertSuccess(res); res.body.should.deepEqual(jsonDocument); }); - - it('should return xml metadata with property annotation', async function() { - const xmlDocument = - ` + + it('should return xml metadata with property annotation', async function () { + const xmlDocument = + ` @@ -103,7 +103,7 @@ describe('metadata.annotations', () => { author: { $Type: 'Edm.String', ...vocabulary.annotate('readonly', 'Property', true) - } + } }); httpServer = server.listen(port); const res = await request(host).get('/$metadata').set('accept', 'application/xml'); @@ -111,18 +111,18 @@ describe('metadata.annotations', () => { res.text.should.equal(xmlDocument); }); - it('should fail for not defined property annotation', async function() { + it('should fail for not defined property annotation', async function () { try { const vocabulary = server.vocabulary(); vocabulary.annotate('unknown', 'Property', true); should.fail(false, true, 'No exception thrown'); - } catch(err) { + } catch (err) { err.message.should.equal(`Annotation with name 'unknown' is not defined`); } }); - it('should works with later annotations', async function() { + it('should works with later annotations', async function () { const jsonDocument = { $Version: '4.0', readonly: { @@ -165,7 +165,7 @@ describe('metadata.annotations', () => { }, author: { $Type: 'Edm.String' - } + } }); book.annotateProperty('author', 'readonly', true); @@ -176,4 +176,98 @@ describe('metadata.annotations', () => { res.body.should.deepEqual(jsonDocument); }); + it('should works with action parameter annotations', async function () { + const jsonDocument = { + $Version: '4.0', + readonly: { + $Kind: "Term", + $Type: "Edm.Boolean", + $AppliesTo: [ + "Parameter" + ], + }, + 'changePassword': { + $Kind: 'Action', + $Parameter: [{ + $Type: 'Edm.String', + $Name: 'newPassword', + '@readonly': true + }, { + $Type: 'Edm.String', + $Name: 'repeat' + }] + }, + $EntityContainer: 'node.odata', + ['node.odata']: { + $Kind: 'EntityContainer', + 'changePassword-import': { + $Action: 'node.odata.changePassword' + } + }, + }; + const vocabulary = server.vocabulary(); + + vocabulary.define('readonly', Boolean, ['Parameter']); + + const action = server.action('changePassword', + (req, res, next) => { }, { + $Parameter: [{ + $Type: 'Edm.String', + $Name: 'newPassword' + }, { + $Type: 'Edm.String', + $Name: 'repeat' + }] + }); + + action.annotateParameter('newPassword', 'readonly', true); + + httpServer = server.listen(port); + const res = await request(host).get('/$metadata?$format=json'); + assertSuccess(res); + res.body.should.deepEqual(jsonDocument); + }); + + it('should works with action parameter annotations in xml format', async function () { + const xmlDocument = + ` + + + + + + + true + + + + + + + + + + `.replace(/\s*\s*/g, '>'); + const vocabulary = server.vocabulary(); + + vocabulary.define('readonly', Boolean, ['Parameter']); + + const action = server.action('changePassword', + (req, res, next) => { }, { + $Parameter: [{ + $Type: 'Edm.String', + $Name: 'newPassword' + }, { + $Type: 'Edm.String', + $Name: 'repeat' + }] + }); + + action.annotateParameter('newPassword', 'readonly', true); + + httpServer = server.listen(port); + const res = await request(host).get('/$metadata?$format=xml'); + assertSuccess(res); + res.text.should.equal(xmlDocument); + }); }); diff --git a/test/odata.actions.parameter.js b/test/odata.actions.parameter.js index 0a97ff8..f45be9f 100644 --- a/test/odata.actions.parameter.js +++ b/test/odata.actions.parameter.js @@ -43,7 +43,8 @@ describe('odata.actions', () => { it('should fail if required parameter is not given', async function () { server.action('change-password', (req, res, next) => { - should.fail(false, true, 'Not aborted although required parameters were not specified') + should.fail(false, true, 'Not aborted although required parameters were not specified'); + next(); }, { $Parameter: [{ $Type: 'Edm.String', @@ -61,6 +62,7 @@ describe('odata.actions', () => { res.statusCode.should.equal(400); res.body.should.deepEqual({ error: { + code: '400', message: `Obligatory parameter 'repeat' not given` } }); From a68063615971a4d52af56267b3dfdf740ef41f16 Mon Sep 17 00:00:00 2001 From: Richard Martens Date: Sat, 2 Sep 2023 07:00:12 +0200 Subject: [PATCH 44/64] [b] validation activated by mongo post and put --- src/mongo/rest/post.js | 5 ++++- src/mongo/rest/put.js | 5 ++++- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/src/mongo/rest/post.js b/src/mongo/rest/post.js index 61c4d2e..89f6435 100644 --- a/src/mongo/rest/post.js +++ b/src/mongo/rest/post.js @@ -10,7 +10,10 @@ export default async (req, res, next) => { const entity = new req.$odata.Model(req.body); - await entity.save(); + await entity.save({ + validateBeforeSave: true, + validateModifiedOnly: true + }); res.$odata.result = entity.toObject(); res.$odata.status = 201; next(); diff --git a/src/mongo/rest/put.js b/src/mongo/rest/put.js index f0d7627..e35deec 100644 --- a/src/mongo/rest/put.js +++ b/src/mongo/rest/put.js @@ -22,7 +22,10 @@ export default async (req, res, next) => { const newEntity = new req.$odata.Model(req.$odata.body); newEntity._id = req.$odata.$Key._id; - await newEntity.save(); + await newEntity.save({ + validateBeforeSave: true, + validateModifiedOnly: true + }); res.$odata.result = newEntity.toObject(); res.$odata.status = 201; From fa1ace52454d6d621b189214b6a3d926507b6306 Mon Sep 17 00:00:00 2001 From: Richard Martens Date: Mon, 4 Sep 2023 21:54:28 +0200 Subject: [PATCH 45/64] extended odata error structure with target, detail. New Mongooses erro handler and handling mongoose validation issues. Sap annotation for error in details included --- .vscode/launch.json | 2 +- src/middlewares/error.js | 16 +++++++--- src/mongo/middlewares/error.js | 24 ++++++++++++++ src/server.js | 3 +- src/writer/xmlWriter.js | 1 - test/mongo/connected/error.js | 57 ++++++++++++++++++++++++++++++++++ test/mongo/metadata.js | 2 +- test/support/db.js | 2 ++ 8 files changed, 98 insertions(+), 9 deletions(-) create mode 100644 src/mongo/middlewares/error.js create mode 100644 test/mongo/connected/error.js diff --git a/.vscode/launch.json b/.vscode/launch.json index a9d86b2..f06aeff 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -20,7 +20,7 @@ "dot", "--timeout", "300000", - "test/metadata/annotation.js" + "test/mongo/connected/error.js" ], "env": { "LOG_LEVEL": "debug" diff --git a/src/middlewares/error.js b/src/middlewares/error.js index ec65135..333fe64 100644 --- a/src/middlewares/error.js +++ b/src/middlewares/error.js @@ -5,15 +5,21 @@ export default function(err, req, res, next) { const status = err.status || 500; const result = { error: { - code: status.toString(), - message: status < 500 ? err.message || http.STATUS_CODES[status] : http.STATUS_CODES[status] + code: status.toString() } - }; - if (status >= 500) { + + if (status < 500) { + result.error.message = err.message || http.STATUS_CODES[status]; + result.error.target = err.target; + result.error.details = err.details; + + } else { const cons = new Console(); + result.error.message = http.STATUS_CODES[status]; cons.log(err); + } - res.status(status).jsonp(result); + res.status(+status).jsonp(result); } \ No newline at end of file diff --git a/src/mongo/middlewares/error.js b/src/mongo/middlewares/error.js new file mode 100644 index 0000000..9251ddf --- /dev/null +++ b/src/mongo/middlewares/error.js @@ -0,0 +1,24 @@ +import error from '../../middlewares/error'; + +export default function(err, req, res, next) { + let mappedError = err; + + if (err.name === 'ValidationError') { + const details = Object.keys(err.errors).map(name => { + return { + code: '400', + target: name, + message: err.errors[name].message, + '@com.sap.vocabularies.Common.v1.numericSeverity': 4 + }; + }); + mappedError = new Error(details[0].message); + mappedError.target = details[0].target; + mappedError.status = '400'; + if (details.lenght > 1) { + mappedError.details = details.slice(1); + } + } + + error(mappedError, req, res, next); +} \ No newline at end of file diff --git a/src/server.js b/src/server.js index 419a410..97a3574 100644 --- a/src/server.js +++ b/src/server.js @@ -60,6 +60,7 @@ class Server { this._serviceDocument = new ServiceDocument(this); this.annotations = new Vocabulary(); + this.error = error; } vocabulary() { @@ -214,7 +215,7 @@ class Server { result.push(action.getRouter()); }); - return [...this.hooks.before, ...result, ...this.hooks.after, error]; + return [...this.hooks.before, ...result, ...this.hooks.after, this.error]; } complexType(name, properties) { diff --git a/src/writer/xmlWriter.js b/src/writer/xmlWriter.js index fbc521d..3e5dedc 100644 --- a/src/writer/xmlWriter.js +++ b/src/writer/xmlWriter.js @@ -219,7 +219,6 @@ export default class XmlWriter { }) .reduce((previos, current) => `${previos}${current}`, ''); - debugger; return (` ${parameter || ''} diff --git a/test/mongo/connected/error.js b/test/mongo/connected/error.js new file mode 100644 index 0000000..52349fd --- /dev/null +++ b/test/mongo/connected/error.js @@ -0,0 +1,57 @@ +import 'should'; +import request from 'supertest'; +import { host, port, odata } from '../../support/setup'; +import mongoose from 'mongoose'; +import { connect } from '../../support/db'; + +const Schema = mongoose.Schema; + +describe('mongo.error', () => { + let httpServer, server; + + before(() => { + mongoose.set('overwriteModels', true); + }) + + beforeEach(async function() { + server = odata(); + + }); + + afterEach(() => { + httpServer.close(); + mongoose.default.connection.close(); + }); + + it('should return 400 for mongo validation errors', async function() { + const result = { + error: { + code: "400", + message: "Path `password` (`ggm`) is shorter than the minimum allowed length (8).", + target: "password" + } + }; + const UserSchema = new Schema({ + email: { + type: String + }, + password: { + type: String, + minLength: 8 + }, + }); + + const UserModel = mongoose.model('User', UserSchema); + + server.mongoEntity('User', UserModel); + await connect(server); + httpServer = server.listen(port); + + const res = await request(host) + .post(`/User`) + .send({ password: 'ggm' }); + + res.body.should.deepEqual(result); + }); + +}); diff --git a/test/mongo/metadata.js b/test/mongo/metadata.js index 05fb318..1cc62ab 100644 --- a/test/mongo/metadata.js +++ b/test/mongo/metadata.js @@ -8,7 +8,7 @@ import mongoose from 'mongoose'; const Schema = mongoose.Schema; -describe('metadata', () => { +describe('mongo.metadata', () => { let httpServer, server; before(() => { diff --git a/test/support/db.js b/test/support/db.js index 03ca6fc..45229e0 100644 --- a/test/support/db.js +++ b/test/support/db.js @@ -1,8 +1,10 @@ import mongoose from 'mongoose'; import { conn } from '../support/setup'; +import error from '../../src/mongo/middlewares/error'; export function init(server) { + server.error = error; server.addBefore((req, res, next) => { req.$odata = { ...req.$odata, From 6602371edb171708314400d4ec2a59ba4bb2f9a0 Mon Sep 17 00:00:00 2001 From: Richard Martens Date: Thu, 7 Sep 2023 22:03:51 +0200 Subject: [PATCH 46/64] fields annotation completed for action --- .vscode/launch.json | 2 +- src/odata/Action.js | 25 ++++++ src/odata/Vocabulary.js | 59 +++++++----- src/odata/entity/Entity.js | 24 +++++ src/odata/validator.js | 2 +- src/writer/xmlWriter.js | 27 ++++-- test/metadata/annotation.js | 175 ++++++++++++++++++++++++++++++++++-- 7 files changed, 279 insertions(+), 35 deletions(-) diff --git a/.vscode/launch.json b/.vscode/launch.json index f06aeff..a9d86b2 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -20,7 +20,7 @@ "dot", "--timeout", "300000", - "test/mongo/connected/error.js" + "test/metadata/annotation.js" ], "env": { "LOG_LEVEL": "debug" diff --git a/src/odata/Action.js b/src/odata/Action.js index 3901a3b..fe6e46a 100644 --- a/src/odata/Action.js +++ b/src/odata/Action.js @@ -39,6 +39,31 @@ export default class Action { this.addBefore(this.parseParameter.bind(this)); } + annotate(anno, value) { + if (!anno) { + throw new Error('Name of annotation term should be given'); + } + + const term = `@${anno}`; + const list = this.annotations.items[anno]; + + this.getMetadata(); + + if (list?.item?.indexOf('parameter') >= 0) { + if (!Array.isArray(value) || !value.length) { + throw new Error('List of parameter names was expected'); + } + + value.forEach(param => { + if (!this.metadata.$Parameter.find(item => item.$Name === param)) { + throw new Error(`Unknown parameter with name '${param}'`); + } + }); + } + + this.metadata[term] = this.annotations.annotate(anno, 'Action', value)[term]; + } + annotateParameter(name, anno, value) { if (!name) { throw new Error('Parameter name should be given'); diff --git a/src/odata/Vocabulary.js b/src/odata/Vocabulary.js index 75dacf5..fd571c2 100644 --- a/src/odata/Vocabulary.js +++ b/src/odata/Vocabulary.js @@ -1,6 +1,8 @@ export default class Vocabulary { constructor() { this.terms = {}; + this.enumerations = {}; + this.items = {}; } getMetadata() { @@ -21,11 +23,21 @@ export default class Vocabulary { } define(name, prototype, scope) { + debugger; if (this.terms[name]) { throw new Error(`Annotation with name '${name}' is allready defined`); } - const type = this.getType(prototype); + const isEnum = Array.isArray(prototype); + + if (isEnum) { + if (!prototype.length) { + throw new Error('For enumeration array at least one item is required'); + } + this.enumerations[name] = prototype; + } + + const type = isEnum ? this.getTypeOf(prototype[0]) : this.getType(prototype); const supportedTargets = [ 'Action', 'Action Import', 'Complex Type', 'Entity Container', 'Entity Set', 'Entity Type', 'Enumeration Type', 'Enumeration Type Member', 'Function', 'Function Import', 'Navigation Property', 'Parameter', 'Property', 'Return Type', @@ -40,13 +52,22 @@ export default class Vocabulary { } this.terms[name] = { - $AppliesTo: scope && scope.length ? scope : supportedTargets, - $Type: type + $AppliesTo: scope && scope.length ? scope : supportedTargets }; + + if (type) { + this.terms[name].$Type = type; + } + + if (prototype.item) { + this.terms[name].$Collection = true; + this.items[name] = prototype.item; + } } annotate(name, destination, value) { const anno = this.terms[name]; + const enumeration = this.enumerations[name]; if (!anno) { throw new Error(`Annotation with name '${name}' is not defined`); @@ -65,38 +86,34 @@ export default class Vocabulary { throw new Error(`Annotation '${name}' can not assigned to '${destination}'`); } + if (enumeration && enumeration.indexOf(value) === -1) { + throw new Error(`Annotation value '${value}' can not be used as '${name}'`); + } + return { [`@${name}`]: value } } getTypeOf(value) { - const type = typeof value; - - switch (type) { - case 'number': - return 'Edm.Double'; + const type = Array.isArray(value) && value.length > 0 ? typeof value[0] : typeof value; - case 'string': - return 'Edm.String'; - - case 'boolean': - return 'Edm.Boolean'; - - default: - throw Error(`type of value '${value}' is not supported`); - } + return this.getType(type); } getType(prototype) { + if (prototype.item) { + return this.getType(prototype.type); + } + switch (prototype) { - case Number: + case 'number': return 'Edm.Double'; - case String: - return 'Edm.String'; + case 'string': + return undefined; //Edm.String is default - case Boolean: + case 'boolean': return 'Edm.Boolean'; default: diff --git a/src/odata/entity/Entity.js b/src/odata/entity/Entity.js index 961d4da..e8f8aa2 100644 --- a/src/odata/entity/Entity.js +++ b/src/odata/entity/Entity.js @@ -263,6 +263,30 @@ export default class Entity { getMetadata() { return this.metadata; } + annotate(anno, value) { + if (!anno) { + throw new Error('Name of annotation term should be given'); + } + + const term = `@${anno}`; + const list = this.annotations.items[anno]; + + this.getMetadata(); + + if (list?.item?.indexOf('property') >= 0) { + if (!Array.isArray(value) || !value.length) { + throw new Error('List of property names was expected'); + } + + value.forEach(prop => { + if (!this.metadata[prop]) { + throw new Error(`Unknown property with name '${prop}'`); + } + }); + } + + this.metadata[term] = this.annotations.annotate(anno, 'Entity Type', value)[term]; + } annotateProperty(prop, anno, value) { if (!prop) { diff --git a/src/odata/validator.js b/src/odata/validator.js index aa4d32d..eb12746 100644 --- a/src/odata/validator.js +++ b/src/odata/validator.js @@ -153,7 +153,7 @@ function validateEntityType(node) { throw new Error('$Key of Entitytype has to be an array of property names'); } - const properties = attributes.filter(name => name !== '$Kind' && name !== '$Key'); + const properties = attributes.filter(name => name !== '$Kind' && name !== '$Key' && name[0] != '@'); properties.forEach(name => validateProperty(name, node[name])); diff --git a/src/writer/xmlWriter.js b/src/writer/xmlWriter.js index 3e5dedc..e9f302e 100644 --- a/src/writer/xmlWriter.js +++ b/src/writer/xmlWriter.js @@ -49,9 +49,12 @@ export default class XmlWriter { const appliesTo = node.$AppliesTo.reduce((previos, current) => { return previos ? `${previos} ${current}` : current; }, ""); + let type = node.$Collection ? `Collection(${node.$Type || 'Edm.String'})` : node.$Type; + + type = type ? `Type="${type}" ` : ''; return ` - + `; } @@ -143,11 +146,19 @@ export default class XmlWriter { throw new Error(`Term '${termName}' is not defined in scope`); } - const type = term.$Type.split('.')[1]; + const type = term.$Type ? term.$Type.split('.')[1] : 'String'; + let values; + + if (term.$Collection) { + values = node.reduce((previous, current) => `${previous}<${type}>${current}`, ""); + values = `${values}`; + } else { + values = `<${type}>${node}`; + } return ` - <${type}>${node} + ${values} `; } @@ -198,9 +209,12 @@ export default class XmlWriter { visitAction(node, name) { const isBound = node.$IsBound ? ' IsBound="true"' : ''; + const annotations = Object.keys(node) + .filter(attribute => attribute[0] === '@') + .reduce((previous, current) => `${previous}${this.visitAnnotation(node[current], current)}`, ""); const parameter = node.$Parameter && node.$Parameter .map((item) => { - const annotations = Object.keys(item) + const parameterAnnotations = Object.keys(item) .filter(attribute => attribute[0] === '@') .reduce((previous, current) => `${previous}${this.visitAnnotation(item[current], current)}`, ""); let type = ''; @@ -210,17 +224,18 @@ export default class XmlWriter { } else if (item.$Type) { type = ` Type="${item.$Type}"`; } - if (!annotations) { + if (!parameterAnnotations) { return ``; } return ` - ${annotations} + ${parameterAnnotations} `; }) .reduce((previos, current) => `${previos}${current}`, ''); return (` + ${annotations} ${parameter || ''} `); diff --git a/test/metadata/annotation.js b/test/metadata/annotation.js index 8478555..047ef9e 100644 --- a/test/metadata/annotation.js +++ b/test/metadata/annotation.js @@ -44,11 +44,11 @@ describe('metadata.annotations', () => { $Collection: true, $Type: `node.odata.book`, } - }, + } }; const vocabulary = server.vocabulary(); - vocabulary.define('readonly', Boolean, ['Property']); + vocabulary.define('readonly', 'boolean', ['Property']); server.entity('book', null, { $Key: ['id'], @@ -93,7 +93,7 @@ describe('metadata.annotations', () => { `.replace(/\s*\s*/g, '>'); const vocabulary = server.vocabulary(); - vocabulary.define('readonly', Boolean, ['Property']); + vocabulary.define('readonly', 'boolean', ['Property']); server.entity('book', null, { $Key: ['id'], id: { @@ -155,7 +155,7 @@ describe('metadata.annotations', () => { }; const vocabulary = server.vocabulary(); - vocabulary.define('readonly', Boolean, ['Property']); + vocabulary.define('readonly', 'boolean', ['Property']); const book = server.entity('book', null, { $Key: ['id'], @@ -207,7 +207,7 @@ describe('metadata.annotations', () => { }; const vocabulary = server.vocabulary(); - vocabulary.define('readonly', Boolean, ['Parameter']); + vocabulary.define('readonly', 'boolean', ['Parameter']); const action = server.action('changePassword', (req, res, next) => { }, { @@ -250,7 +250,7 @@ describe('metadata.annotations', () => { `.replace(/\s*\s*/g, '>'); const vocabulary = server.vocabulary(); - vocabulary.define('readonly', Boolean, ['Parameter']); + vocabulary.define('readonly', 'boolean', ['Parameter']); const action = server.action('changePassword', (req, res, next) => { }, { @@ -270,4 +270,167 @@ describe('metadata.annotations', () => { assertSuccess(res); res.text.should.equal(xmlDocument); }); + + + it('should works collection annotations', async function () { + const jsonDocument = { + $Version: '4.0', + fields: { + $Kind: "Term", + $AppliesTo: [ + "Action" + ], + $Collection: true + }, + 'changePassword': { + $Kind: 'Action', + $Parameter: [{ + $Type: 'Edm.String', + $Name: 'newPassword' + }, { + $Type: 'Edm.String', + $Name: 'repeat' + }], + '@fields': ['newPassword', 'repeat'] + }, + $EntityContainer: 'node.odata', + ['node.odata']: { + $Kind: 'EntityContainer', + 'changePassword-import': { + $Action: 'node.odata.changePassword' + } + }, + }; + const vocabulary = server.vocabulary(); + + vocabulary.define('fields', { + item: 'parameter', + type: 'string' + }, ['Action']); + + const action = server.action('changePassword', + (req, res, next) => { }, { + $Parameter: [{ + $Type: 'Edm.String', + $Name: 'newPassword' + }, { + $Type: 'Edm.String', + $Name: 'repeat' + }] + }); + + action.annotate('fields', ['newPassword', 'repeat']); + + httpServer = server.listen(port); + const res = await request(host).get('/$metadata?$format=json'); + assertSuccess(res); + res.body.should.deepEqual(jsonDocument); + }); + + it('should works collection annotations in xml', async function () { + const xmlDocument = + ` + + + + + + + newPassword + repeat + + + + + + + + + + + `.replace(/\s*\s*/g, '>'); + + const vocabulary = server.vocabulary(); + + vocabulary.define('fields', { + item: 'parameter', + type: 'string' + }, ['Action']); + + const action = server.action('changePassword', + (req, res, next) => { }, { + $Parameter: [{ + $Type: 'Edm.String', + $Name: 'newPassword' + }, { + $Type: 'Edm.String', + $Name: 'repeat' + }] + }); + + action.annotate('fields', ['newPassword', 'repeat']); + + httpServer = server.listen(port); + const res = await request(host).get('/$metadata?$format=xml'); + assertSuccess(res); + res.text.should.equal(xmlDocument); + }); + + + it('should works with collection annotations on entities', async function () { + const jsonDocument = { + $Version: '4.0', + fields: { + $Kind: "Term", + $AppliesTo: [ + "Entity Type" + ], + $Collection: true + }, + book: { + $Kind: "EntityType", + $Key: ["id"], + id: { + $Type: 'Edm.String', + $MaxLength: 24 + }, + author: { + $Type: 'Edm.String' + }, + '@fields': ['id', 'author'] + }, + $EntityContainer: 'node.odata', + ['node.odata']: { + $Kind: 'EntityContainer', + book: { + $Collection: true, + $Type: `node.odata.book`, + } + } + }; + const vocabulary = server.vocabulary(); + + vocabulary.define('fields', { + item: ['property'], + type: 'string' + }, ['Entity Type']); + + const entity = server.entity('book', null, { + $Key: ['id'], + id: { + $Type: 'Edm.String', + $MaxLength: 24 + }, + author: { + $Type: 'Edm.String' + } + }); + + entity.annotate('fields', ['id', 'author']); + + httpServer = server.listen(port); + const res = await request(host).get('/$metadata?$format=json'); + assertSuccess(res); + res.body.should.deepEqual(jsonDocument); + }); }); From b2d60ff6398e9cc08ea9623e3ce33b0535e700b1 Mon Sep 17 00:00:00 2001 From: Richard Martens Date: Tue, 19 Sep 2023 02:10:30 +0200 Subject: [PATCH 47/64] [b] writting null by selected nullable properties --- .vscode/launch.json | 2 +- src/odata/Vocabulary.js | 1 - src/odata/entity/Entity.js | 64 +++++++++++++++---- src/odata/entity/Singleton.js | 11 +++- src/server.js | 6 +- src/writer/xmlWriter.js | 10 ++- test/metadata/annotation.js | 52 ++++++++++++++++ test/metadata/singleton.js | 8 +-- test/mongo/metadata.js | 69 +++++++++++++++------ test/mongo/mocked/singleton.js | 110 ++++++++++++++++++++++++++------- test/odata.batch.js | 5 +- test/odata.entity.js | 29 +++++++++ test/singleton.js | 2 +- test/support/books.model.js | 6 +- 14 files changed, 305 insertions(+), 70 deletions(-) diff --git a/.vscode/launch.json b/.vscode/launch.json index a9d86b2..c740a88 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -20,7 +20,7 @@ "dot", "--timeout", "300000", - "test/metadata/annotation.js" + "test/odata.batch.js" ], "env": { "LOG_LEVEL": "debug" diff --git a/src/odata/Vocabulary.js b/src/odata/Vocabulary.js index fd571c2..1207e59 100644 --- a/src/odata/Vocabulary.js +++ b/src/odata/Vocabulary.js @@ -23,7 +23,6 @@ export default class Vocabulary { } define(name, prototype, scope) { - debugger; if (this.terms[name]) { throw new Error(`Annotation with name '${name}' is allready defined`); } diff --git a/src/odata/entity/Entity.js b/src/odata/entity/Entity.js index e8f8aa2..5f506ec 100644 --- a/src/odata/entity/Entity.js +++ b/src/odata/entity/Entity.js @@ -97,9 +97,9 @@ export default class Entity { adaptResultAccordingMetadata(req, res, next) { if (res.$odata.result?.value && Array.isArray(res.$odata.result?.value)) { // list of entities - res.$odata.result.value.forEach(this.adaptEntityAccordingMetadata.bind(this)); + res.$odata.result.value.forEach(entity => this.adaptEntityAccordingMetadata(entity, req, res)); } else if (res.$odata.result) { - this.adaptEntityAccordingMetadata(res.$odata.result); + this.adaptEntityAccordingMetadata(res.$odata.result, req, res); } next(); } @@ -156,24 +156,44 @@ export default class Entity { } } - adaptEntityAccordingMetadata(entity) { + getPropertyMetadata(member) { const entityMetadata = this.getMetadata(); - const keys = Object.keys(entity); + const result = { + propertyMetadata: null, + mapping: null + } - keys.forEach(member => { - let propertyMetadata; + if (entityMetadata[member]) { + result.propertyMetadata = entityMetadata[member]; - if (entityMetadata[member]) { - propertyMetadata = entityMetadata[member]; + } else { + const keys = Object.keys(this.mapping); + result.mapping = keys.find(name => this.mapping[name].target === member); - } else { - const keys = Object.keys(this.mapping); - const mapping = keys.find(name => this.mapping[name].target === member); + if (result.mapping) { + result.propertyMetadata = this.mapping[result.mapping].attributes; + } + + } + + return result; + } + + adaptEntityAccordingMetadata(entity, req, res) { + const entityMetadata = this.getMetadata(); + const keys = Object.keys(entity); + if (req.$odata.$count || res.$odata.status === 204) { + // no classic body + return; + } + + keys.forEach(member => { + const { propertyMetadata, mapping } = this.getPropertyMetadata(member); + + if (!entityMetadata[member]) { if (mapping) { - propertyMetadata = this.mapping[mapping].attributes; entity[mapping] = entity[member]; - } delete entity[member]; // hide attributes not exposed in metadata @@ -188,7 +208,24 @@ export default class Entity { && Object.prototype.toString.call(entity[member]) === '[object Date]') { entity[member] = entity[member].toISOString().replace(/\.[0-9]{3}/, '') } + }); + + const nullables = Object.keys(entityMetadata) + .filter(item => item != '$Key' && item != '$Kind' && !entity[item]); + + nullables.forEach(member => { + const { propertyMetadata } = this.getPropertyMetadata(member); + + if (req.$odata.$select && req.$odata.$select.indexOf(member) === -1) { + // explizit projection doesn't include current field + return; + } + + if (propertyMetadata.$Nullable) { + entity[member] = null; + } + }) } getRouter() { @@ -263,6 +300,7 @@ export default class Entity { getMetadata() { return this.metadata; } + annotate(anno, value) { if (!anno) { throw new Error('Name of annotation term should be given'); diff --git a/src/odata/entity/Singleton.js b/src/odata/entity/Singleton.js index fa01748..003ce8a 100644 --- a/src/odata/entity/Singleton.js +++ b/src/odata/entity/Singleton.js @@ -13,7 +13,7 @@ export default class Singleton { }; this.name = name; - this.entity = metadata instanceof Entity ? metadata : new Entity(name, handler, metadata, annotations, mapping); + this.entity = metadata instanceof Entity ? metadata : new Entity(name, handler, metadata, null, annotations, mapping); this.handler = { ...this.entity.handler, // get, post, put, delete, patch @@ -107,4 +107,13 @@ export default class Singleton { regex: listRoute.regex })); } + + + annotate(anno, value) { + this.entity.annotate(anno, value); + } + + annotateProperty(prop, anno, value) { + this.entity.annotateProperty(prop, anno, value); + } } \ No newline at end of file diff --git a/src/server.js b/src/server.js index 97a3574..dff5030 100644 --- a/src/server.js +++ b/src/server.js @@ -136,12 +136,12 @@ class Server { return this.resources[name]; } - singleton(name, handler, entity) { + singletonFrom(name, handler, entity, mapping) { if (this.resources[name]) { throw new Error(`Entity with name "${name}" already defined`); } - this.resources[name] = new Singleton(name, handler, entity, this.annotations); + this.resources[name] = new Singleton(name, handler, entity, this.annotations, mapping); return this.resources[name]; } @@ -173,7 +173,7 @@ class Server { }, { ...entity.entity.getMetadata(), ...metadata - }, null, { + }, { ...entity.entity.getMapping(), ...mapping }); diff --git a/src/writer/xmlWriter.js b/src/writer/xmlWriter.js index e9f302e..7083b09 100644 --- a/src/writer/xmlWriter.js +++ b/src/writer/xmlWriter.js @@ -164,19 +164,27 @@ export default class XmlWriter { visitEntityType(node, name) { let properties = ''; + let annotations = ''; Object.keys(node) - .filter((item) => item !== '$Kind' && item !== '$Key') + .filter((item) => item !== '$Kind' && item !== '$Key' && item[0] != '@') .forEach((item) => { properties += this.visitor('Property', node[item], item); }); + Object.keys(node) + .filter((item) => item[0] === '@') + .forEach((item) => { + annotations += this.visitAnnotation(node[item], item); + }); + return ( ` ${properties} + ${annotations} `); } diff --git a/test/metadata/annotation.js b/test/metadata/annotation.js index 047ef9e..2865981 100644 --- a/test/metadata/annotation.js +++ b/test/metadata/annotation.js @@ -433,4 +433,56 @@ describe('metadata.annotations', () => { assertSuccess(res); res.body.should.deepEqual(jsonDocument); }); + + it('should works with collection annotations on entities in xml', async function () { + const xmlDocument = + ` + + + + + + + + + + + + id + author + + + + + + + + + `.replace(/\s*\s*/g, '>'); + + const vocabulary = server.vocabulary(); + + vocabulary.define('fields', { + item: ['property'], + type: 'string' + }, ['Entity Type']); + + const entity = server.entity('book', null, { + $Key: ['id'], + id: { + $Type: 'Edm.String', + $MaxLength: 24 + }, + author: { + $Type: 'Edm.String' + } + }); + + entity.annotate('fields', ['id', 'author']); + + httpServer = server.listen(port); + const res = await request(host).get('/$metadata?$format=xml'); + assertSuccess(res); + res.text.should.equal(xmlDocument); + }); }); diff --git a/test/metadata/singleton.js b/test/metadata/singleton.js index ced24ce..c5e6dc5 100644 --- a/test/metadata/singleton.js +++ b/test/metadata/singleton.js @@ -3,7 +3,7 @@ import request from 'supertest'; import { host, port, odata, assertSuccess } from '../support/setup'; import { BookMetadata } from '../support/books.model'; -describe('metadata.custom.resource', () => { +describe('metadata.singleton', () => { let httpServer, server; beforeEach(async function() { @@ -52,8 +52,8 @@ describe('metadata.custom.resource', () => { - - + + @@ -91,7 +91,7 @@ describe('metadata.custom.resource', () => { }, }; const bookEntity = server.entity('book', null, BookMetadata); - server.singleton('current-book', null, bookEntity); + server.singletonFrom('current-book', null, bookEntity); httpServer = server.listen(port); const res = await request(host).get('/$metadata?$format=json'); diff --git a/test/mongo/metadata.js b/test/mongo/metadata.js index 1cc62ab..9206ae3 100644 --- a/test/mongo/metadata.js +++ b/test/mongo/metadata.js @@ -1,10 +1,8 @@ -// For issue: https://github.com/TossShinHwa/node-odata/issues/96 -// For issue: https://github.com/TossShinHwa/node-odata/issues/25 - import 'should'; import request from 'supertest'; import { host, port, odata, assertSuccess } from '../support/setup'; import mongoose from 'mongoose'; +import { BookModel } from '../support/books.model'; const Schema = mongoose.Schema; @@ -71,9 +69,9 @@ describe('mongo.metadata', () => { } }); - const BookModel = mongoose.model('book', BookSchema); + const CustomBookModel = mongoose.model('book', BookSchema); - server.mongoEntity('book', BookModel); + server.mongoEntity('book', CustomBookModel); httpServer = server.listen(port); const res = await request(host).get('/$metadata?$format=json'); assertSuccess(res); @@ -117,9 +115,9 @@ describe('mongo.metadata', () => { } }); - const BookModel = mongoose.model('book', BookSchema); + const CustomBookModel = mongoose.model('book', BookSchema); - server.mongoEntity('book', BookModel); + server.mongoEntity('book', CustomBookModel); httpServer = server.listen(port); const res = await request(host).get('/$metadata'); assertSuccess(res); @@ -158,9 +156,9 @@ describe('mongo.metadata', () => { } }); - const BookModel = mongoose.model('book', BookSchema); + const CustomBookModel = mongoose.model('book', BookSchema); - server.mongoEntity('book', BookModel); + server.mongoEntity('book', CustomBookModel); httpServer = server.listen(port); const res = await request(host).get('/$metadata?$format=json'); assertSuccess(res); @@ -192,9 +190,9 @@ describe('mongo.metadata', () => { } }); - const BookModel = mongoose.model('book', BookSchema); + const CustomBookModel = mongoose.model('book', BookSchema); - server.mongoEntity('book', BookModel); + server.mongoEntity('book', CustomBookModel); httpServer = server.listen(port); const res = await request(host).get('/$metadata'); assertSuccess(res); @@ -233,9 +231,9 @@ describe('mongo.metadata', () => { } }); - const BookModel = mongoose.model('book', BookSchema); + const CustomBookModel = mongoose.model('book', BookSchema); - server.mongoEntity('book', BookModel); + server.mongoEntity('book', CustomBookModel); httpServer = server.listen(port); const res = await request(host).get('/$metadata?$format=json'); assertSuccess(res); @@ -267,9 +265,9 @@ describe('mongo.metadata', () => { } }); - const BookModel = mongoose.model('book', BookSchema); + const CustomBookModel = mongoose.model('book', BookSchema); - server.mongoEntity('book', BookModel); + server.mongoEntity('book', CustomBookModel); httpServer = server.listen(port); const res = await request(host).get('/$metadata'); assertSuccess(res); @@ -306,9 +304,9 @@ describe('mongo.metadata', () => { } }); - const BookModel = mongoose.model('book', BookSchema); + const CustomBookModel = mongoose.model('book', BookSchema); - server.mongoEntity('book', BookModel); + server.mongoEntity('book', CustomBookModel); httpServer = server.listen(port); const res = await request(host).get('/$metadata?$format=json'); assertSuccess(res); @@ -339,13 +337,46 @@ describe('mongo.metadata', () => { } }); - const BookModel = mongoose.model('book', BookSchema); + const CustomBookModel = mongoose.model('book', BookSchema); - server.mongoEntity('book', BookModel); + server.mongoEntity('book', CustomBookModel); httpServer = server.listen(port); const res = await request(host).get('/$metadata'); assertSuccess(res); res.text.should.equal(xmlDocument); }); + + it('should render timestamps as nullable for singleton', async function() { + const xmlDocument = + ` + + + + + + + + + + + + + + + + + + + + + + `.replace(/\s*\s*/g, '>'); + + server.mongoSingleton('book', BookModel); + httpServer = server.listen(port); + const res = await request(host).get('/$metadata'); + assertSuccess(res); + res.text.should.equal(xmlDocument); + }); }); diff --git a/test/mongo/mocked/singleton.js b/test/mongo/mocked/singleton.js index fdc336a..4678676 100644 --- a/test/mongo/mocked/singleton.js +++ b/test/mongo/mocked/singleton.js @@ -4,37 +4,52 @@ import request from 'supertest'; import { odata, host, port, assertSuccess } from '../../support/setup'; import data from '../../support/books.json'; import { BookModel } from '../../support/books.model'; +import mongoose from 'mongoose'; -describe('mongo.mocked.singleton', () => { - const query = { - $where: () => { }, - where: () => { }, - equals: () => { }, - select: () => { }, - sort: () => { }, - exec: () => { }, - model: BookModel - }; - let httpServer, modelMock, queryMock, bookInstanceMock; - - before(async function() { - const server = odata(); - - server.mongoSingleton('book', BookModel); - httpServer = server.listen(port); +const Schema = mongoose.Schema; + +const ConfigSchema = new Schema({ + isAutoLogOffActive: { + type: Boolean, + default: true + }, +}, + { + timestamps: true, + toObject: { + virtuals: true, + }, + toJSON: { + virtuals: true, + }, }); - after(() => { - httpServer.close(); +const ConfigModel = mongoose.model('Config', ConfigSchema); + +describe('mongo.mocked.singleton', () => { + let httpServer, modelMock, queryMock, bookInstanceMock, server; + + beforeEach(async function () { + server = odata(); }); afterEach(() => { + httpServer.close(); modelMock?.restore(); queryMock?.restore(); bookInstanceMock?.restore(); }); - it('should select anyone field', async function() { + it('should select anyone field', async function () { + const query = { + $where: () => { }, + where: () => { }, + equals: () => { }, + select: () => { }, + sort: () => { }, + exec: () => { }, + model: BookModel + }; const books = data.map(item => ({ price: item.price })); @@ -47,7 +62,9 @@ describe('mongo.mocked.singleton', () => { price: 1 }); queryMock.expects('exec').once() - .returns(new Promise(resolve => resolve({ toObject: () => books[0] }))); + .returns(new Promise(resolve => resolve({ toObject: () => books[0] }))); + server.mongoSingleton('book', BookModel); + httpServer = server.listen(port); const res = await request(host).get('/book?$select=price'); @@ -57,7 +74,16 @@ describe('mongo.mocked.singleton', () => { res.body.should.deepEqual(books[0]); }); - it('should select anyone field for upsert', async function() { + it('should select anyone field for upsert', async function () { + const query = { + $where: () => { }, + where: () => { }, + equals: () => { }, + select: () => { }, + sort: () => { }, + exec: () => { }, + model: BookModel + }; const books = data.map(item => ({ price: item.price })); @@ -70,9 +96,11 @@ describe('mongo.mocked.singleton', () => { price: 1 }); queryMock.expects('exec').once() - .returns(new Promise(resolve => resolve(undefined))); + .returns(new Promise(resolve => resolve(undefined))); bookInstanceMock = sinon.mock(BookModel.prototype); bookInstanceMock.expects('toObject').once().returns(JSON.parse(JSON.stringify(data[0]))); + server.mongoSingleton('book', BookModel); + httpServer = server.listen(port); const res = await request(host).get('/book?$select=price'); @@ -82,4 +110,40 @@ describe('mongo.mocked.singleton', () => { bookInstanceMock.verify(); res.body.should.deepEqual(books[0]); }); + + it('should return default value', async function () { + const query = { + $where: () => { }, + where: () => { }, + equals: () => { }, + select: () => { }, + sort: () => { }, + exec: () => { }, + model: ConfigModel + }; + + modelMock = sinon.mock(ConfigModel); + queryMock = sinon.mock(query); + modelMock.expects('findOne').once().returns(query); + queryMock.expects('select').once().withArgs({ + _id: 0, + isAutoLogOffActive: 1 + }); + queryMock.expects('exec').once() + .returns(new Promise(resolve => resolve(undefined))); + bookInstanceMock = sinon.mock(ConfigModel.prototype); + bookInstanceMock.expects('toObject').once().returns(JSON.parse(JSON.stringify({ isAutoLogOffActive: true }))); + server.mongoSingleton('config', ConfigModel); + httpServer = server.listen(port); + + const res = await request(host).get('/config?$select=isAutoLogOffActive'); + + assertSuccess(res); + modelMock.verify(); + queryMock.verify(); + bookInstanceMock.verify(); + res.body.should.deepEqual({ + isAutoLogOffActive: true + }); + }); }); diff --git a/test/odata.batch.js b/test/odata.batch.js index b85b1f0..2e594b3 100644 --- a/test/odata.batch.js +++ b/test/odata.batch.js @@ -134,7 +134,9 @@ describe('odata.batch', () => { it('should work with post entity', async function () { const result = { - title: "War and peace" + title: "War and peace", + createdAt: null, + updatedAt: null }; const server = odata(); @@ -266,6 +268,7 @@ describe('odata.batch', () => { delete: (req, res, next) => { req.$odata.$Key.id.should.be.equal('1'); res.$odata.status = 204; + delete res.$odata.result; next(); } }, BookMetadata); diff --git a/test/odata.entity.js b/test/odata.entity.js index 31145aa..803c93c 100644 --- a/test/odata.entity.js +++ b/test/odata.entity.js @@ -125,4 +125,33 @@ describe('odata.entity', () => { }); }); + it('should return null for nullable values automatically', async function () { + server.entity('book', { + get: (req, res, next) => { + res.$odata.result = { + "id": '1' + }; + next(); + } + }, { + $Key: ['id'], + id: { + $Type: 'Edm.String', + $MaxLength: 24 + }, + createdAt: { + $Type: 'Edm.DateTimeOffset', + $Nullable: true + } + }); + httpServer = server.listen(port); + + const res = await request(host).get(`/book('1')`); + + res.body.should.deepEqual({ + "id": '1', + "createdAt": null + }); + }); + }); diff --git a/test/singleton.js b/test/singleton.js index 194ea17..d3d1fa6 100644 --- a/test/singleton.js +++ b/test/singleton.js @@ -46,7 +46,7 @@ describe('singleton', () => { "title": "XML Developer's Guide" }; const book = server.entity('book', null, BookMetadata); - server.singleton('current-book', { + server.singletonFrom('current-book', { get: (req, res, next) => { res.$odata.result = result; next(); diff --git a/test/support/books.model.js b/test/support/books.model.js index a33a08c..bb9b551 100644 --- a/test/support/books.model.js +++ b/test/support/books.model.js @@ -44,9 +44,11 @@ export const BookMetadata = { $Type: 'Edm.String' }, createdAt: { - $Type: 'Edm.DateTimeOffset' + $Type: 'Edm.DateTimeOffset', + $Nullable: true }, updatedAt: { - $Type: 'Edm.DateTimeOffset' + $Type: 'Edm.DateTimeOffset', + $Nullable: true } }; \ No newline at end of file From 6eff24b1d3957066e33aba595a79b5f028391b1b Mon Sep 17 00:00:00 2001 From: Richard Martens Date: Thu, 21 Sep 2023 21:18:57 +0200 Subject: [PATCH 48/64] fixed bug of projektion in xml batches --- .vscode/launch.json | 2 +- src/parser/multipartMixed.js | 2 +- test/mongo/mocked/odata.query.select.js | 6 +++++- .../mocked/{singleton.js => singelton.js} | 21 ++++++++++--------- 4 files changed, 18 insertions(+), 13 deletions(-) rename test/mongo/mocked/{singleton.js => singelton.js} (85%) diff --git a/.vscode/launch.json b/.vscode/launch.json index c740a88..2041195 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -20,7 +20,7 @@ "dot", "--timeout", "300000", - "test/odata.batch.js" + "test/mongo/mocked/singelton.js" ], "env": { "LOG_LEVEL": "debug" diff --git a/src/parser/multipartMixed.js b/src/parser/multipartMixed.js index 1b13ce2..3f44575 100644 --- a/src/parser/multipartMixed.js +++ b/src/parser/multipartMixed.js @@ -19,7 +19,7 @@ function multipart(req, res, next) { if (singleRequestText.indexOf("Group ID: ") >= 0) { return; //sap extension, not documentet in odata } - const matchMethodUrl = singleRequestText.match(/^(GET|POST|PUT|PATCH|DELETE)\s+([\w\/\.$?=\-()]+)\s*/m); + const matchMethodUrl = singleRequestText.match(/^(GET|POST|PUT|PATCH|DELETE)\s+([\w\/\.,$?=\-()]+)\s*/m); if (!matchMethodUrl) { throw new Error(`Method in ${singleRequestText} not supported`); diff --git a/test/mongo/mocked/odata.query.select.js b/test/mongo/mocked/odata.query.select.js index b1dcf24..e367da9 100644 --- a/test/mongo/mocked/odata.query.select.js +++ b/test/mongo/mocked/odata.query.select.js @@ -125,7 +125,11 @@ describe('mongo.mocked.odata.query.select', () => { title: 1 }); queryMock.expects('exec').once() - .returns(new Promise(resolve => resolve(books.map(item => ({ toObject: () => item }))))); + .returns(new Promise(resolve => resolve(books.map(item => ({ toObject: () => ({ + price: item.price, + title: item.title, + _id: item.id + }) }))))); const res = await request(host).get('/book?$select=price,title,id'); diff --git a/test/mongo/mocked/singleton.js b/test/mongo/mocked/singelton.js similarity index 85% rename from test/mongo/mocked/singleton.js rename to test/mongo/mocked/singelton.js index 4678676..e4e0e4c 100644 --- a/test/mongo/mocked/singleton.js +++ b/test/mongo/mocked/singelton.js @@ -27,7 +27,7 @@ const ConfigSchema = new Schema({ const ConfigModel = mongoose.model('Config', ConfigSchema); describe('mongo.mocked.singleton', () => { - let httpServer, modelMock, queryMock, bookInstanceMock, server; + let httpServer, modelMock, queryMock, instanceMock, server; beforeEach(async function () { server = odata(); @@ -37,7 +37,7 @@ describe('mongo.mocked.singleton', () => { httpServer.close(); modelMock?.restore(); queryMock?.restore(); - bookInstanceMock?.restore(); + instanceMock?.restore(); }); it('should select anyone field', async function () { @@ -97,8 +97,8 @@ describe('mongo.mocked.singleton', () => { }); queryMock.expects('exec').once() .returns(new Promise(resolve => resolve(undefined))); - bookInstanceMock = sinon.mock(BookModel.prototype); - bookInstanceMock.expects('toObject').once().returns(JSON.parse(JSON.stringify(data[0]))); + instanceMock = sinon.mock(BookModel.prototype); + instanceMock.expects('toObject').once().returns(JSON.parse(JSON.stringify(data[0]))); server.mongoSingleton('book', BookModel); httpServer = server.listen(port); @@ -107,7 +107,7 @@ describe('mongo.mocked.singleton', () => { assertSuccess(res); modelMock.verify(); queryMock.verify(); - bookInstanceMock.verify(); + instanceMock.verify(); res.body.should.deepEqual(books[0]); }); @@ -126,23 +126,24 @@ describe('mongo.mocked.singleton', () => { queryMock = sinon.mock(query); modelMock.expects('findOne').once().returns(query); queryMock.expects('select').once().withArgs({ - _id: 0, + _id: 1, isAutoLogOffActive: 1 }); queryMock.expects('exec').once() .returns(new Promise(resolve => resolve(undefined))); - bookInstanceMock = sinon.mock(ConfigModel.prototype); - bookInstanceMock.expects('toObject').once().returns(JSON.parse(JSON.stringify({ isAutoLogOffActive: true }))); + instanceMock = sinon.mock(ConfigModel.prototype); + instanceMock.expects('toObject').once().returns(JSON.parse(JSON.stringify({ isAutoLogOffActive: true, _id: '1' }))); server.mongoSingleton('config', ConfigModel); httpServer = server.listen(port); - const res = await request(host).get('/config?$select=isAutoLogOffActive'); + const res = await request(host).get('/config?$select=id,isAutoLogOffActive'); assertSuccess(res); modelMock.verify(); queryMock.verify(); - bookInstanceMock.verify(); + instanceMock.verify(); res.body.should.deepEqual({ + id: '1', isAutoLogOffActive: true }); }); From 9e5043136ab2627dfc03916c49af26ed323e9700 Mon Sep 17 00:00:00 2001 From: Richard Martens Date: Fri, 22 Sep 2023 14:35:08 +0200 Subject: [PATCH 49/64] [b] patch for singleton, fix for deep projection, messages in success payload --- .vscode/launch.json | 2 +- src/middlewares/writer.js | 22 ++++ src/mongo/Entity.js | 3 +- src/mongo/Singleton.js | 4 +- src/mongo/rest/getSingleton.js | 5 + src/mongo/rest/patch.js | 6 +- src/mongo/rest/patchSingleton.js | 28 ++++ src/odata/entity/Entity.js | 2 +- src/odata/entity/parser/select.js | 5 +- src/odata/entity/validators/property.js | 1 + src/server.js | 3 +- test/mongo/metadata.js | 22 ++-- test/mongo/metadata.resource.complex.js | 41 +++--- test/mongo/mocked/singelton.js | 41 +++++- test/odata.messages.js | 163 ++++++++++++++++++++++++ test/odata.select.js | 51 ++++++++ 16 files changed, 362 insertions(+), 37 deletions(-) create mode 100644 src/mongo/rest/patchSingleton.js create mode 100644 test/odata.messages.js create mode 100644 test/odata.select.js diff --git a/.vscode/launch.json b/.vscode/launch.json index 2041195..db65b3a 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -20,7 +20,7 @@ "dot", "--timeout", "300000", - "test/mongo/mocked/singelton.js" + "test/odata.select.js" ], "env": { "LOG_LEVEL": "debug" diff --git a/src/middlewares/writer.js b/src/middlewares/writer.js index f0cc212..bbbab27 100644 --- a/src/middlewares/writer.js +++ b/src/middlewares/writer.js @@ -55,7 +55,29 @@ function getWriter(req, res, result) { } } +function writeMessages(res) { + if (res.$odata.messages.length) { + res.$odata.messages.forEach(msg => { + if (!msg.code) { + throw new Error(`Missing 'code' property in message`); + } + if (!msg.message) { + throw new Error(`Missing 'message' property in message`); + } + if (!msg.numericSeverity) { + throw new Error(`Missing 'numericSeverity' property in message`); + } + if ([1,2,3,4].indexOf(msg.numericSeverity) === -1) { + throw new Error(`Value '${msg.numericSeverity}' is invalid for severity`); + } + }); + res.setHeader('sap-messages', JSON.stringify(res.$odata.messages)); + } +} + export default function writer(req, res) { + writeMessages(res); + switch (res.$odata.status) { case 404: // not found or no handler worked on diff --git a/src/mongo/Entity.js b/src/mongo/Entity.js index bc673cb..da83b18 100644 --- a/src/mongo/Entity.js +++ b/src/mongo/Entity.js @@ -20,7 +20,8 @@ export default class MongoEntity { target: '_id', attributes: { $Type: 'Edm.String', - $MaxLength: 24 + $MaxLength: 24, + $Nullable: true } }, ...mapping diff --git a/src/mongo/Singleton.js b/src/mongo/Singleton.js index eb6cf81..2e84176 100644 --- a/src/mongo/Singleton.js +++ b/src/mongo/Singleton.js @@ -2,8 +2,8 @@ import MongoEntity from "./Entity"; import post from './rest/post'; import put from './rest/put'; import del from './rest/delete'; -import patch from './rest/patch'; import getSingleton from "./rest/getSingleton"; +import patchSingleton from "./rest/patchSingleton"; export default class MongoSingleton { constructor(name, model, annotations, mapping) { @@ -16,7 +16,7 @@ export default class MongoSingleton { const rest = { post, put, - patch, + patch: patchSingleton, delete: del, get: getSingleton }; diff --git a/src/mongo/rest/getSingleton.js b/src/mongo/rest/getSingleton.js index eba2c67..965fc11 100644 --- a/src/mongo/rest/getSingleton.js +++ b/src/mongo/rest/getSingleton.js @@ -7,13 +7,18 @@ export default async (req, res, next) => { await selectParser(query, req.$odata.$select); let entity = await query.exec(); + let transient = false; if (!entity) { // return default properties of singleton entity = new req.$odata.Model(); + transient = true; } res.$odata.result = entity.toObject(); + if (transient) { + res.$odata.result._id = null; + } if (req.$odata.$select) { Object.keys(res.$odata.result) diff --git a/src/mongo/rest/patch.js b/src/mongo/rest/patch.js index 31f4425..1064df1 100644 --- a/src/mongo/rest/patch.js +++ b/src/mongo/rest/patch.js @@ -1,9 +1,9 @@ export default async (req, res, next) => { try { - const entity = await req.$odata.Model.findOne({ id: req.params.id }); - const patched = { ...entity.toObject(), ...req.body }; + const entity = await req.$odata.Model.findOne({ id: req.$odata.$Key.id }); + const patched = { ...entity.toObject(), ...req.$odata.body }; - await req.$odata.Model.update({ id: req.params.id }, patched); + await req.$odata.Model.updateOne({ _id: req.$odata.$Key.id }, patched); res.$odata.result = patched; next(); diff --git a/src/mongo/rest/patchSingleton.js b/src/mongo/rest/patchSingleton.js new file mode 100644 index 0000000..e462835 --- /dev/null +++ b/src/mongo/rest/patchSingleton.js @@ -0,0 +1,28 @@ +export default async (req, res, next) => { + try { + let entity = await req.$odata.Model.findOne(); + + if (entity) { + const patched = { ...entity.toObject(), ...req.$odata.body }; + + await req.$odata.Model.updateOne({ _id: entity._id }, patched); + res.$odata.result = patched; + + } else { + entity = new req.$odata.Model(); + Object.keys(req.$odata.body).forEach(property => + entity[property] = req.$odata.body[property] + ); + await entity.save({ + validateBeforeSave: true, + validateModifiedOnly: true + }); + res.$odata.result = entity.toObject(); + + } + next(); + + } catch (err) { + next(err); + } +} \ No newline at end of file diff --git a/src/odata/entity/Entity.js b/src/odata/entity/Entity.js index 5f506ec..46ec40d 100644 --- a/src/odata/entity/Entity.js +++ b/src/odata/entity/Entity.js @@ -222,7 +222,7 @@ export default class Entity { return; } - if (propertyMetadata.$Nullable) { + if (propertyMetadata.$Nullable && entity[member] === undefined) { entity[member] = null; } }) diff --git a/src/odata/entity/parser/select.js b/src/odata/entity/parser/select.js index 1ed7e09..6019179 100644 --- a/src/odata/entity/parser/select.js +++ b/src/odata/entity/parser/select.js @@ -3,9 +3,10 @@ import validateProperty from "../validators/property"; export default function(req, entity, metadata, mapping) { return req.query.$select?.split(',').map((item) => { - const property = parseProperty(item.trim(), mapping); + const name = item.trim().replace('/', '.'); + const property = parseProperty(name, mapping); - validateProperty(item.trim(), req, entity, metadata); + validateProperty(name, req, entity, metadata); return property; }); diff --git a/src/odata/entity/validators/property.js b/src/odata/entity/validators/property.js index d2d9182..6910de9 100644 --- a/src/odata/entity/validators/property.js +++ b/src/odata/entity/validators/property.js @@ -16,5 +16,6 @@ export default function validateProperty(name, req, entity, currentMetadata) { const err = new Error(`Entity '${entity}' has no property named '${property}'`); err.status = 400; + err.target = property; throw err; } \ No newline at end of file diff --git a/src/server.js b/src/server.js index dff5030..319b176 100644 --- a/src/server.js +++ b/src/server.js @@ -53,7 +53,8 @@ class Server { }; res.$odata = { status: 404, - supportedMimetypes: ['application/json'] + supportedMimetypes: ['application/json'], + messages: [] } }, 'service-initialization'); this.hooks.addAfter(writer, 'writer', true); diff --git a/test/mongo/metadata.js b/test/mongo/metadata.js index 9206ae3..7a67949 100644 --- a/test/mongo/metadata.js +++ b/test/mongo/metadata.js @@ -30,7 +30,8 @@ describe('mongo.metadata', () => { $Key: ["id"], id: { $Type: 'Edm.String', - $MaxLength: 24 + $MaxLength: 24, + $Nullable: true }, price: { $Type: 'Edm.Double', @@ -89,7 +90,7 @@ describe('mongo.metadata', () => { - + @@ -132,7 +133,8 @@ describe('mongo.metadata', () => { $Key: ["id"], id: { $Type: 'Edm.String', - $MaxLength: 24 + $MaxLength: 24, + $Nullable: true }, author: { $Type: 'Edm.String', @@ -175,7 +177,7 @@ describe('mongo.metadata', () => { - + @@ -207,7 +209,8 @@ describe('mongo.metadata', () => { $Key: ["id"], id: { $Type: 'Edm.String', - $MaxLength: 24 + $MaxLength: 24, + $Nullable: true }, author: { $Type: 'Edm.String', @@ -250,7 +253,7 @@ describe('mongo.metadata', () => { - + @@ -282,7 +285,8 @@ describe('mongo.metadata', () => { $Key: ["id"], id: { $Type: 'Edm.String', - $MaxLength: 24 + $MaxLength: 24, + $Nullable: true }, salted: { $Type: 'Edm.Boolean', @@ -323,7 +327,7 @@ describe('mongo.metadata', () => { - + @@ -362,7 +366,7 @@ describe('mongo.metadata', () => { - + diff --git a/test/mongo/metadata.resource.complex.js b/test/mongo/metadata.resource.complex.js index bbaab38..e643a8c 100644 --- a/test/mongo/metadata.resource.complex.js +++ b/test/mongo/metadata.resource.complex.js @@ -27,7 +27,8 @@ describe('mongo.metadata.resource.complex', () => { $Kind: 'ComplexType', id: { $Type: 'Edm.String', - $MaxLength: 24 + $MaxLength: 24, + $Nullable: true }, p2: { $Type: 'Edm.String', @@ -39,7 +40,8 @@ describe('mongo.metadata.resource.complex', () => { $Key: ["id"], id: { $Type: 'Edm.String', - $MaxLength: 24 + $MaxLength: 24, + $Nullable: true }, p1: { $Type: 'node.odata.complex-modelp1Child1', @@ -77,14 +79,14 @@ describe('mongo.metadata.resource.complex', () => { - + - + @@ -114,7 +116,8 @@ describe('mongo.metadata.resource.complex', () => { $Key: ["id"], id: { $Type: 'Edm.String', - $MaxLength: 24 + $MaxLength: 24, + $Nullable: true }, p3: { $Type: 'Edm.String', @@ -153,7 +156,7 @@ describe('mongo.metadata.resource.complex', () => { - + @@ -184,7 +187,7 @@ describe('mongo.metadata.resource.complex', () => { - + @@ -223,7 +226,8 @@ describe('mongo.metadata.resource.complex', () => { $Key: ["id"], id: { $Type: 'Edm.String', - $MaxLength: 24 + $MaxLength: 24, + $Nullable: true }, p4: { $Type: 'node.odata.complex-modelp4Child1' @@ -264,7 +268,7 @@ describe('mongo.metadata.resource.complex', () => { - + @@ -301,7 +305,8 @@ describe('mongo.metadata.resource.complex', () => { }, id: { $Type: 'Edm.String', - $MaxLength: 24 + $MaxLength: 24, + $Nullable: true } }, p1p4Child2: { @@ -316,7 +321,8 @@ describe('mongo.metadata.resource.complex', () => { $Key: ["id"], id: { $Type: 'Edm.String', - $MaxLength: 24 + $MaxLength: 24, + $Nullable: true }, p2: { $Type: 'node.odata.p1p2Child1', @@ -360,7 +366,7 @@ describe('mongo.metadata.resource.complex', () => { - + @@ -368,7 +374,7 @@ describe('mongo.metadata.resource.complex', () => { - + @@ -400,7 +406,8 @@ describe('mongo.metadata.resource.complex', () => { $Kind: "ComplexType", id: { $Type: 'Edm.String', - $MaxLength: 24 + $MaxLength: 24, + $Nullable: true }, p3: { $Type: 'Edm.String' @@ -411,7 +418,8 @@ describe('mongo.metadata.resource.complex', () => { $Key: ["id"], id: { $Type: 'Edm.String', - $MaxLength: 24 + $MaxLength: 24, + $Nullable: true }, p2: { $Type: 'node.odata.myComlexType' @@ -436,7 +444,8 @@ describe('mongo.metadata.resource.complex', () => { server.complexType('myComlexType', { id: { $Type: 'Edm.String', - $MaxLength: 24 + $MaxLength: 24, + $Nullable: true }, p3: { $Type: 'Edm.String' diff --git a/test/mongo/mocked/singelton.js b/test/mongo/mocked/singelton.js index e4e0e4c..24e59f6 100644 --- a/test/mongo/mocked/singelton.js +++ b/test/mongo/mocked/singelton.js @@ -143,8 +143,47 @@ describe('mongo.mocked.singleton', () => { queryMock.verify(); instanceMock.verify(); res.body.should.deepEqual({ - id: '1', + id: null, isAutoLogOffActive: true }); }); + + it('should supports upsert', async function () { + const now = new Date(); + const query = { + $where: () => { }, + where: () => { }, + equals: () => { }, + select: () => { }, + sort: () => { }, + exec: () => { }, + model: ConfigModel + }; + + modelMock = sinon.mock(ConfigModel); + modelMock.expects('findOne').once().returns(new Promise((resolve) => resolve())); + instanceMock = sinon.mock(ConfigModel.prototype); + instanceMock.expects('save').once().returns(new Promise((resolve) => resolve())); + instanceMock.expects('toObject').once().returns({ + isAutoLogOffActive: false, + _id: '1', + createdAt: now + }); + server.mongoSingleton('config', ConfigModel); + httpServer = server.listen(port); + + const res = await request(host).patch('/config').send({ + isAutoLogOffActive: false + }); + + assertSuccess(res); + modelMock.verify(); + instanceMock.verify(); + res.body.should.deepEqual({ + id: '1', + isAutoLogOffActive: false, + createdAt: now.toISOString().replace(/\.[0-9]{3}/, ''), + updatedAt: null + }); + }); }); diff --git a/test/odata.messages.js b/test/odata.messages.js new file mode 100644 index 0000000..02c9778 --- /dev/null +++ b/test/odata.messages.js @@ -0,0 +1,163 @@ +import 'should'; +import request from 'supertest'; +import { odata, host, port, assertSuccess } from './support/setup'; + +// https://sapui5.hana.ondemand.com/sdk/#/topic/fbe1cb5613cf4a40a841750bf813238e.html + +describe('odata.actions', () => { + let httpServer, server; + + beforeEach(async function () { + server = odata(); + }); + + afterEach(() => { + if (httpServer) { + httpServer.close(); + } + }); + + it('should return message header', async function () { + server.action('server-message', (req, res, next) => { + try { + res.$odata.messages.push({ + code: '0815', + message: 'Some message', + numericSeverity: 1 // 1 - success, 2 - info, 3 - warning, 4 - error + }); + res.$odata.status = 204; + next(); + + } catch (error) { + next(error); + } + }); + httpServer = server.listen(port); + + const res = await request(host).post(`/node.odata.server-message`) + + assertSuccess(res); + res.headers.should.have.property('sap-messages'); + JSON.parse(res.headers['sap-messages']).should.deepEqual([{ + code: '0815', + message: 'Some message', + numericSeverity: 1 + }]); + }); + + it('should fail if code missing', async function () { + server.action('server-message', (req, res, next) => { + try { + res.$odata.messages.push({ + message: 'Some message', + numericSeverity: 1 // 1 - success, 2 - info, 3 - warning, 4 - error + }); + res.$odata.status = 204; + next(); + + } catch (error) { + next(error); + } + }); + httpServer = server.listen(port); + + const res = await request(host).post(`/node.odata.server-message`) + + res.status.should.be.equal(500); + res.res.statusMessage.should.be.equal('Internal Server Error'); + res.body.should.deepEqual({ + error: { + code: "500", + message: "Internal Server Error" + } + + }); + }); + + it('should fail if message property missing', async function () { + server.action('server-message', (req, res, next) => { + try { + res.$odata.messages.push({ + code: '0815', + numericSeverity: 1 // 1 - success, 2 - info, 3 - warning, 4 - error + }); + res.$odata.status = 204; + next(); + + } catch (error) { + next(error); + } + }); + httpServer = server.listen(port); + + const res = await request(host).post(`/node.odata.server-message`) + + res.status.should.be.equal(500); + res.res.statusMessage.should.be.equal('Internal Server Error'); + res.body.should.deepEqual({ + error: { + code: "500", + message: "Internal Server Error" + } + + }); + }); + + it('should fail if severity property not given', async function () { + server.action('server-message', (req, res, next) => { + try { + res.$odata.messages.push({ + code: '0815', + message: 'Some message' + }); + res.$odata.status = 204; + next(); + + } catch (error) { + next(error); + } + }); + httpServer = server.listen(port); + + const res = await request(host).post(`/node.odata.server-message`) + + res.status.should.be.equal(500); + res.res.statusMessage.should.be.equal('Internal Server Error'); + res.body.should.deepEqual({ + error: { + code: "500", + message: "Internal Server Error" + } + + }); + }); + it('should fail if invalid severity given', async function () { + server.action('server-message', (req, res, next) => { + try { + res.$odata.messages.push({ + code: '0815', + message: 'Some message', + numericSeverity: 99 + }); + res.$odata.status = 204; + next(); + + } catch (error) { + next(error); + } + }); + httpServer = server.listen(port); + + const res = await request(host).post(`/node.odata.server-message`) + + res.status.should.be.equal(500); + res.res.statusMessage.should.be.equal('Internal Server Error'); + res.body.should.deepEqual({ + error: { + code: "500", + message: "Internal Server Error" + } + + }); + }); +}); diff --git a/test/odata.select.js b/test/odata.select.js new file mode 100644 index 0000000..7a29b5f --- /dev/null +++ b/test/odata.select.js @@ -0,0 +1,51 @@ +import 'should'; +import request from 'supertest'; +import { odata, host, port, assertSuccess } from './support/setup'; + +describe('odata.select', () => { + let httpServer, server; + + beforeEach(async function () { + server = odata(); + }); + + afterEach(() => { + if (httpServer) { + httpServer.close(); + } + }); + + it('should work deep property', async function () { + server.complexType('fullName', { + first: { + $Type: 'Edm.String' + }, + last: { + $Type: 'Edm.String' + } + }); + server.entity('user', { + list: (req, res, next) => { + req.$odata.$select.should.deepEqual(['name.first']) + res.$odata.status = 204; + next(); + } + }, { + $Key: ['id'], + id: { + $Type: 'Edm.String', + $MaxLength: 24, + $Nullable: true + }, + name: { + $Type: 'node.odata.fullName' + } + }); + httpServer = server.listen(port); + + const res = await request(host).get(`/user?$select=name/first`); + + assertSuccess(res); + }); + +}); From 30449b8c81c158ba85b08ea886ffaddbce29500f Mon Sep 17 00:00:00 2001 From: Richard Martens Date: Wed, 4 Oct 2023 22:25:35 +0200 Subject: [PATCH 50/64] some fixes with deep selects and multipart mixed parser --- src/mongo/parser/selectParser.js | 15 +--- src/odata/Action.js | 14 +++- src/odata/Batch.js | 2 +- src/odata/parser/value.js | 1 + src/parser/multipartMixed.js | 2 +- test/mongo/mocked/odata.query.select.js | 99 +++++++++++++++++++------ test/odata.actions.js | 45 +++++++++++ 7 files changed, 139 insertions(+), 39 deletions(-) diff --git a/src/mongo/parser/selectParser.js b/src/mongo/parser/selectParser.js index 0553a13..58c7742 100644 --- a/src/mongo/parser/selectParser.js +++ b/src/mongo/parser/selectParser.js @@ -1,6 +1,3 @@ -// ?$select=Rating,ReleaseDate -// -> -// query.select('Rating ReleaseDate') export default (query, $select) => new Promise((resolve) => { if (!$select?.length) { resolve(); @@ -10,17 +7,7 @@ export default (query, $select) => new Promise((resolve) => { const list = $select; const selectFields = { _id: 0 }; - const { tree } = query.model.schema; - Object.keys(tree).map((item) => { - if (list.indexOf(item) >= 0) { - if (item === 'id') { - selectFields._id = 1; - } else if (typeof tree[item] === 'function' || tree[item].select !== false) { - selectFields[item] = 1; - } - } - return undefined; - }); + list.forEach(item => selectFields[item] = 1 ); if (Object.keys(selectFields).length === 1 && selectFields._id === 0) { resolve(); diff --git a/src/odata/Action.js b/src/odata/Action.js index fe6e46a..8d4e2a1 100644 --- a/src/odata/Action.js +++ b/src/odata/Action.js @@ -27,6 +27,14 @@ export default class Action { } this.hooks = new Hooks(); + if (options) { + Object.keys(options).forEach(name => { + if (['binding', 'resource', '$Parameter'].indexOf(name) === -1) { + throw new Error(`Option '${name}' is not supported`); + } + }); + } + this.binding = options?.binding; this.resource = options?.resource; this.$Parameter = options?.$Parameter || []; @@ -205,7 +213,11 @@ export default class Action { this.$Parameter.forEach(param => { if (req.body && req.body[param.$Name]) { - req.$odata.$Parameter[param.$Name] = parseValue(req.body[param.$Name], param); + if (param.$Type.indexOf('node.odata') === -1) { + req.$odata.$Parameter[param.$Name] = parseValue(req.body[param.$Name], param); + } else { + req.$odata.$Parameter[param.$Name] = req.body[param.$Name]; + } } else if (!param.$Nullable && (!req.body || !req.body[param.$Name])) { const error = new Error(`Obligatory parameter '${param.$Name}' not given`); diff --git a/src/odata/Batch.js b/src/odata/Batch.js index a34c706..4839a8b 100644 --- a/src/odata/Batch.js +++ b/src/odata/Batch.js @@ -172,7 +172,7 @@ export default class Batch { $odata: res.$odata }; - const paramsMatch = request.url.match(/^\/[^#?(]+\('(\w+)'\)/); + const paramsMatch = request.url.match(/^\/?[^#?(]+\('(\w+)'\)/); if (paramsMatch && paramsMatch.length > 1) { currentRequest.params = { diff --git a/src/odata/parser/value.js b/src/odata/parser/value.js index f1ad9cb..cf0e1d8 100644 --- a/src/odata/parser/value.js +++ b/src/odata/parser/value.js @@ -40,6 +40,7 @@ function parseDate(value, metadata) { export default function (value, metadata) { const trimmed = value.trim(); + debugger; if (metadata.$Nullable && trimmed === 'null') { return null; } diff --git a/src/parser/multipartMixed.js b/src/parser/multipartMixed.js index 3f44575..cea8dfd 100644 --- a/src/parser/multipartMixed.js +++ b/src/parser/multipartMixed.js @@ -19,7 +19,7 @@ function multipart(req, res, next) { if (singleRequestText.indexOf("Group ID: ") >= 0) { return; //sap extension, not documentet in odata } - const matchMethodUrl = singleRequestText.match(/^(GET|POST|PUT|PATCH|DELETE)\s+([\w\/\.,$?=\-()]+)\s*/m); + const matchMethodUrl = singleRequestText.match(/^(GET|POST|PUT|PATCH|DELETE)\s+([\w\/\.,$?=\-()']+)\s*/m); if (!matchMethodUrl) { throw new Error(`Method in ${singleRequestText} not supported`); diff --git a/test/mongo/mocked/odata.query.select.js b/test/mongo/mocked/odata.query.select.js index e367da9..7f1135e 100644 --- a/test/mongo/mocked/odata.query.select.js +++ b/test/mongo/mocked/odata.query.select.js @@ -4,6 +4,9 @@ import request from 'supertest'; import { odata, host, port, assertSuccess } from '../../support/setup'; import data from '../../support/books.json'; import { BookModel } from '../../support/books.model'; +import mongoose from 'mongoose'; + +const Schema = mongoose.Schema; describe('mongo.mocked.odata.query.select', () => { const query = { @@ -15,25 +18,15 @@ describe('mongo.mocked.odata.query.select', () => { exec: () => { }, model: BookModel }; - let httpServer, modelMock, queryMock; - - before(async function() { - const server = odata(); - - server.mongoEntity('book', BookModel); - httpServer = server.listen(port); - }); - - after(() => { - httpServer.close(); - }); + let httpServer, modelMock, queryMock, server; afterEach(() => { + httpServer.close(); modelMock?.restore(); queryMock?.restore(); }); - it('should select anyone field', async function() { + it('should select anyone field', async function () { const books = data.map(item => ({ price: item.price })); @@ -47,6 +40,9 @@ describe('mongo.mocked.odata.query.select', () => { }); queryMock.expects('exec').once() .returns(new Promise(resolve => resolve(books.map(item => ({ toObject: () => item }))))); + server = odata(); + server.mongoEntity('book', BookModel); + httpServer = server.listen(port); const res = await request(host).get('/book?$select=price'); @@ -57,7 +53,7 @@ describe('mongo.mocked.odata.query.select', () => { value: books }); }); - it('should select multiple field', async function() { + it('should select multiple field', async function () { const books = data.map(item => ({ price: item.price, title: item.title @@ -73,6 +69,9 @@ describe('mongo.mocked.odata.query.select', () => { }); queryMock.expects('exec').once() .returns(new Promise(resolve => resolve(books.map(item => ({ toObject: () => item }))))); + server = odata(); + server.mongoEntity('book', BookModel); + httpServer = server.listen(port); const res = await request(host).get('/book?$select=price,title'); @@ -83,7 +82,7 @@ describe('mongo.mocked.odata.query.select', () => { value: books }); }); - it('should select multiple field with blank space', async function() { + it('should select multiple field with blank space', async function () { const books = data.map(item => ({ price: item.price, title: item.title @@ -99,6 +98,9 @@ describe('mongo.mocked.odata.query.select', () => { }); queryMock.expects('exec').once() .returns(new Promise(resolve => resolve(books.map(item => ({ toObject: () => item }))))); + server = odata(); + server.mongoEntity('book', BookModel); + httpServer = server.listen(port); const res = await request(host).get('/book?$select=price, title'); @@ -109,7 +111,7 @@ describe('mongo.mocked.odata.query.select', () => { value: books }); }); - it('should select id field', async function() { + it('should select id field', async function () { const books = data.map(item => ({ price: item.price, title: item.title, @@ -125,11 +127,16 @@ describe('mongo.mocked.odata.query.select', () => { title: 1 }); queryMock.expects('exec').once() - .returns(new Promise(resolve => resolve(books.map(item => ({ toObject: () => ({ - price: item.price, - title: item.title, - _id: item.id - }) }))))); + .returns(new Promise(resolve => resolve(books.map(item => ({ + toObject: () => ({ + price: item.price, + title: item.title, + _id: item.id + }) + }))))); + server = odata(); + server.mongoEntity('book', BookModel); + httpServer = server.listen(port); const res = await request(host).get('/book?$select=price,title,id'); @@ -140,13 +147,61 @@ describe('mongo.mocked.odata.query.select', () => { value: books }); }); - it('should ignore when select not exist field', async function() { + it('should ignore when select not exist field', async function () { modelMock = sinon.mock(BookModel); modelMock.expects('find').never(); + server = odata(); + server.mongoEntity('book', BookModel); + httpServer = server.listen(port); const res = await request(host).get('/book?$select=not-exist-field'); modelMock.verify(); res.status.should.be.equal(400); }); + + it('should select deep field', async function () { + const result = [{ + name: { + first: 'Max' + } + }]; + const UserSchema = new Schema({ + name: { + first: { + type: String + }, + last: { + type: String + } + } + }); + const UserModel = mongoose.model('user', UserSchema); + const userQuery = { + ...query, + model: UserModel + }; + + modelMock = sinon.mock(UserModel); + queryMock = sinon.mock(userQuery); + modelMock.expects('find').returns(userQuery); + queryMock.expects('select').once().withArgs({ + _id: 0, + "name.first": 1 + }); + queryMock.expects('exec').once() + .returns(new Promise(resolve => resolve(result.map(item => ({ toObject: () => item }))))); + server = odata(); + server.mongoEntity('user', UserModel); + httpServer = server.listen(port); + + const res = await request(host).get('/user?$select=name/first'); + + assertSuccess(res); + modelMock.verify(); + queryMock.verify(); + res.body.should.deepEqual({ + value: result + }); + }); }); diff --git a/test/odata.actions.js b/test/odata.actions.js index 5ffc73c..5085513 100644 --- a/test/odata.actions.js +++ b/test/odata.actions.js @@ -2,6 +2,7 @@ import 'should'; import request from 'supertest'; import { odata, host, port, assertSuccess } from './support/setup'; import { BookMetadata } from './support/books.model'; +import should from 'should'; function requestToHalfPrice(id) { return request(host).post(`/book('${id}')/50off`); @@ -62,4 +63,48 @@ describe('odata.actions', () => { res.res.statusMessage.should.be.equal('Not Found'); }); + + it('should throw error for not supported option', async function () { + should(() => { + server.action('salam-aleikum', async (req, res) => { + res.$odata.result = { result: 'Wa aleikum assalam' }; + }, { + unsupportedOption: true + }); + }).throw(`Option 'unsupportedOption' is not supported`); + + }); + + it('should work with complex parameters', async function () { + const name = { + first: 'Max', + last: 'Mustermann' + }; + + server.complexType('fullName', { + first: { + $Type: 'Edm.String' + }, + last: { + $Type: 'Edm.String' + } + }); + server.action('someThing', async (req, res) => { + req.$odata.$Parameter.should.have.property('name'); + req.$odata.$Parameter.name.should.deepEqual(name) + res.$odata.status = 204; + }, { + $Parameter: [{ + $Name: 'name', + $Type: 'node.odata.fullName' + }] + }) + httpServer = server.listen(port); + + const res = await request(host).post(`/node.odata.someThing`).send({name}); + + assertSuccess(res); + res.status.should.be.equal(204); + + }); }); From 00238beb10c3821373f99f6f0f778745d297ba3b Mon Sep 17 00:00:00 2001 From: Richard Martens Date: Wed, 4 Oct 2023 22:33:42 +0200 Subject: [PATCH 51/64] renaming project to nota --- .vscode/launch.json | 2 +- package.json | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.vscode/launch.json b/.vscode/launch.json index db65b3a..0ac71ca 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -20,7 +20,7 @@ "dot", "--timeout", "300000", - "test/odata.select.js" + "test/odata.actions.js" ], "env": { "LOG_LEVEL": "debug" diff --git a/package.json b/package.json index 5a2efb6..bd25939 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { - "name": "node-odata", - "version": "0.7.16", + "name": "@bitech-ag/nota", + "version": "0.1.0", "private": false, "description": "A module for easily create a REST API based on oData protocol", "main": "index.js", From 48993c1edc6bbecc5a9552b3a9124b5233047f38 Mon Sep 17 00:00:00 2001 From: Richard Martens Date: Wed, 4 Oct 2023 22:33:51 +0200 Subject: [PATCH 52/64] 0.1.0-alpha.0 --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index cc06bc1..325be1d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "node-odata", - "version": "0.7.16", + "version": "0.1.0-alpha.0", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "node-odata", - "version": "0.7.16", + "version": "0.1.0-alpha.0", "license": "MIT", "dependencies": { "body-parser": "^1.20.2", diff --git a/package.json b/package.json index bd25939..ce5c67c 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@bitech-ag/nota", - "version": "0.1.0", + "version": "0.1.0-alpha.0", "private": false, "description": "A module for easily create a REST API based on oData protocol", "main": "index.js", From 1bacb6dfde1ed1e49625ca37086e36036ef32e60 Mon Sep 17 00:00:00 2001 From: Richard Martens Date: Thu, 5 Oct 2023 21:47:41 +0200 Subject: [PATCH 53/64] switch version and module name back --- package.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index ce5c67c..5a2efb6 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { - "name": "@bitech-ag/nota", - "version": "0.1.0-alpha.0", + "name": "node-odata", + "version": "0.7.16", "private": false, "description": "A module for easily create a REST API based on oData protocol", "main": "index.js", From b48d0939a7390eed54a4b45720a61af98476867e Mon Sep 17 00:00:00 2001 From: Richard Martens Date: Wed, 18 Oct 2023 13:54:21 +0200 Subject: [PATCH 54/64] Readme and example updated. multipart tests deleted --- README.md | 422 +++++++++++++++--- examples/db.js | 16 +- examples/{actions => entities/book}/50off.js | 0 .../{models/book.js => entities/book/db.js} | 0 examples/entities/book/index.js | 8 + .../complex-resource/db.js} | 0 examples/entities/complex-resource/index.js | 4 + .../{models/user.js => entities/user/db.js} | 0 examples/entities/user/index.js | 4 + examples/functions/license.js | 7 +- examples/functions/server-time.js | 6 +- examples/index.js | 26 ++ examples/server.js | 47 +- test/failing/stackoverflow.js | 61 --- 14 files changed, 423 insertions(+), 178 deletions(-) rename examples/{actions => entities/book}/50off.js (100%) rename examples/{models/book.js => entities/book/db.js} (100%) create mode 100644 examples/entities/book/index.js rename examples/{models/complex-resource.js => entities/complex-resource/db.js} (100%) create mode 100644 examples/entities/complex-resource/index.js rename examples/{models/user.js => entities/user/db.js} (100%) create mode 100644 examples/entities/user/index.js create mode 100644 examples/index.js delete mode 100644 test/failing/stackoverflow.js diff --git a/README.md b/README.md index cf2effc..37e03f0 100644 --- a/README.md +++ b/README.md @@ -11,14 +11,25 @@ Create awesome REST APIs abide by [OData Protocol v4](http://www.odata.org/). I [![License](http://img.shields.io/npm/l/node-odata.svg?style=flat)](https://raw.githubusercontent.com/zackyang000/node-odata/master/LICENSE) ```JavaScript -var odata = require('node-odata'); - -var server = odata('mongodb://localhost/my-app'); - -server.resource('books', { +const odata = require('node-odata'); +const server = odata('mongodb://localhost/my-app'); +const mongoose = require('mongoose'); +const connection = mongoose.connect('mongodb://localhost:27017/example', null, (err) => { + if (err) { + console.error(err.message); + console.error('Failed to connect to database on startup.'); + process.exit(); + } +}); +const mongoose = require('mongoose'); +const Schema = mongoose.Schema; +const ModelSchema = new Schema({ title: String, price: Number }); +const Model = mongoose.model('books', ModelSchema); + +server.mongoEntity('books', Model); server.listen(3000); ``` @@ -27,11 +38,12 @@ Registers the following routes: ``` GET /books -GET /books(:id) +GET /books(':id') POST /books -PUT /books(:id) -DELETE /books(:id) +PUT /books(':id') +DELETE /books(':id') GET /books/$metadata +GET /books/$count ``` Use the following OData query: @@ -43,18 +55,74 @@ GET /books?$top=3&$skip=2 GET /books?$orderby=price desc GET /books?$filter=price gt 10 GET /books/$metadata +GET /books/$count GET ... ``` ### Further options -The odata constructor takes 3 arguments: ```odata(, , );``` +The odata constructor takes 2 arguments: ```odata(, );``` The options object currently only supports one parameter: ```expressRequestLimit```, this will be parsed to the express middelware as the "limit" option, which allows for configuring express to support larger requests. It can be either a number or a string like "50kb", 20mb", etc. # How to -## Entities +## With MongoDB + +For MongoDB, the entity and singleton operations have been implemented, so they require very little code to provide collections via OData. The database is decoupled from the OData implementation, so you need to inject the database connection first. This happens as follows: + +```JavaScript +const mongoose = require('mongoose'); +const connection = mongoose.connect('mongodb://localhost:27017/example', null, (err) => { + if (err) { + console.error(err.message); + console.error('Failed to connect to database on startup.'); + process.exit(); + } +}); +const odata = require('node.odata'); +const server = odata(); + +server.addBefore((req, res, next) => { + req.$odata = { + ...req.$odata, + mongo: connection + }; + next(); +}); +``` +You then have to define the collection as usual. + +```JavaScript +const mongoose = require('mongoose'); +const Schema = mongoose.Schema; +const ModelSchema = new Schema(...); +const Model = mongoose.model(...); +``` + +Afterwards, providing the collections is very easy. + +```JavaScript +server.entity('book', Model); + +// or singleton +server.singleton('config', Model); +``` + +For inserting code before or after the standard operations, you can use [Hooks](#hooks). In the after hook you find the result of the standard operation in res.$odata.result. + +## Custom implementation + +If the standard implementation cannot be used, you have the option of implementing an entity or singleton yourself. If you have injected the database as stated above, it will be available in the middlewares in req.$odata.mongo. The following things should be taken into account when implementing your own implementations: + +- The result must be written to res.$odata.result +- An http status must be written in req.$odata.status +- the next callback must be called at the end +- The response process should not be terminated. +- No data should be written to the response +- The status of the response should not be set + +### Entities With entities you can provide a kind of virtual table via the OData service. The following operations can be implemented on an entity: - list: Returns one or more items from the list. The result array must be encapsulated in a property named "value". e.g.({value: []}) @@ -67,10 +135,9 @@ With entities you can provide a kind of virtual table via the OData service. The Here an example of an entity implementation. To define an entity, you must call the server.entity method. Pass the name of the entity as the first parameter. The second parameter allows you to pass the implementation for each operation. If you do not pass a handler for an operation, calling that operation returns "Not Implemented". With the third parameter you pass the description of your entity. An object with the $Key property in which you list the names of all key columns. The other properties of the object describe the properties of your entity. -``` +```JavaScript const odata = require('node-odata'); -const server = odata(process.env.DATABASE || 'mongodb://localhost:27017/example'); - +const server = odata(); server.complexType('fullName', { first: { @@ -82,25 +149,39 @@ server.complexType('fullName', { }); const entity = server.entity('user', { - list: async (req, res) => { - res.$odata.status = 200; - res.$odata.result = { - value: [{ - id: '1', - name: { first: 'Max', last: 'Mustermann' } - }] - }; + list: (req, res, next) => { + try { + res.$odata.status = 200; + res.$odata.result = { + value: [{ + id: '1', + name: { first: 'Max', last: 'Mustermann' } + }] + }; + + next(); + + } catch(error) { + next(error); + } }, - count: async (req, res) => { - res.$odata.status = 200; - res.$odata.result = 1; - } + count: async (req, res) => { + try { + res.$odata.status = 200; + res.$odata.result = 1; + + next(); + + } catch(error) { + next(error); + } + } }, { - $Key: ['title'], + $Key: ['id'], id: { $Type: 'Edm.String', - $MaxLength: 24 + $MaxLength: 24 }, name: { $Type: 'node.odata.fullName' @@ -111,20 +192,87 @@ const entity = server.entity('user', { }); ``` +### Singleton + +With a Singleton entity, you don't provide a collection via OData, but rather a single object. Compared to Entity, Singleton does not support list and count operations. The difference lies in the URL of get requests too. This is what the requests for the currentUser singleton would look like. + +``` +GET current-user +POST current-user +PUT current-user +DELETE current-user +``` + +Singleton can be defined standalone. + +```JavaScript +const odata = require('node-odata'); +const server = odata(); + +const entity = server.singleton('user', { + get: (req, res, next) => { + try { + res.$odata.status = req.user ? 200 : 403; + res.$odata.result = req.user; + + next(); + + } catch(error) { + next(error); + } + + } +}, { + $Key: ['id'], + id: { + $Type: 'Edm.String', + $MaxLength: 24 + }, + email: { + $Type: 'Edm.String' + } +}); +``` + +Or a singleton can be created for an existing entity. + +```JavaScript +const odata = require('node-odata'); +const server = odata(); + +... + +const user = server.mongoEntity('user', Model); +server.singletonFrom('current-user', { + get: (req, res, next) => { + res.$odata.status = req.user ? 200 : 403; + res.$odata.result = req.user; + + next(); + } +}, user); +``` + ## Actions ### Unbound Actions Unbound Action will be defined over server directly. -``` +```JavaScript server.action('login', async function(req, res) { - // in req.$odata.mongo is your db instance + try { + // in req.$odata.mongo is your db instance - res.$odata.result = await req.$odata.mongo.user.findOne({ - email: req.body.email - }); + res.$odata.result = await req.$odata.mongo.user.findOne({ + email: req.body.email + }); + next(); + + } catch(error) { + next(error); + } }); ``` @@ -136,12 +284,12 @@ POST /node.odata.login ### Bound Actions -Bound Action are defined over resource. An action can be bound to single resource or to collection of resources. For the bound action, the first parameter of the bound resource type is specified in the metadata. +Bound Action are defined over entity. An action can be bound to single entity or to collection of entities. For the bound action, the first parameter of the bound entity type is specified in the metadata. #### Entity Actions -``` -resource.action('bound-action', (req, res) => { +```JavaScript +entity.action('bound-action', (req, res) => { ... }, { binding: 'entity' }); ``` @@ -154,8 +302,8 @@ POST /book('01234')/bound-action #### Collection Actions -``` -resource.action('bound-action', (req, res) => { +```JavaScript +entity.action('bound-action', (req, res) => { ... }, { binding: 'collection' }); ``` @@ -170,23 +318,29 @@ POST /book/bound-action The interface of the passed function must correspond to the nodejs express middleware. You should assign the result to the res.$odata.result attribute. An error can be thrown and it can contain the status attribute. -``` +```JavaScript server.action('login', async function(req, res) { - // in req.$odata.mongo is your db instance + try { + // in req.$odata.mongo is your db instance res.$odata.result = { - user: await req.$odata.mongo.user.findOne({ - email: req.body.email - }) - } + user: await req.$odata.mongo.user.findOne({ + email: req.body.email + }) + }; - if (!res.$odata.result) { - const err = new Error('Login failed'); + if (!res.$odata.result) { + const err = new Error('Login failed'); - err.status = 403; - throw err; - } + err.status = 403; + throw err; + } + next(); + + } catch(error) { + next(error); + } }); ``` @@ -195,7 +349,7 @@ server.action('login', async function(req, res) { Parameters can be defined for the action. These will be output in the metadata. -``` +```JavaScript server.action('login', async function(req, res, next) { ... }, { @@ -210,7 +364,7 @@ server.action('login', async function(req, res, next) { ``` The following attributes can be specified for parameters: -- $Type Build-In Types(Edm.\*) or custom defined types(node.odata.*) +- $Type Build-In Types(Edm.\*) or custom defined types(node.odata.\*) - $Collection true/false - $Nullable true/false - $MaxLength Number bigger than zero @@ -223,38 +377,179 @@ The following attributes can be specified for parameters: It is possible to specify nodejs express middlewares for the actions or entities to be performed before or after the action. Any data assigned to req.$odata or res.$odata will be available on action implementation and subsequent hooks. An error thrown in the hook interrupts further processing. it is possible to provide a name of hook for tracing. You can use a [passportjs](https://www.passportjs.org/) middleware as before hook for authentication. -``` +```JavaScript const action = server.action('login', ...); -action.addBefore(async (req, res) => { +action.addBefore((req, res, next) => { ... res.$odata.result = { result: 'any' }; // client receives: { result: 'any' } -}, 'name-of-hook'); + next(); +}); -action.addBefore(async (req, res) => { +action.addBefore((req, res, next) => { if (!req.user) { const err = new Error(); err.status = 401; - throw err; + next(err); } }); -action.addAfter(async (req, res) => { +action.addAfter(async (req, res, next) => { ... -}, 'name-of-hook'); +}); +``` + +## Batch request + +node-odata is able to process a collected request. This means the client can send multiple operations with one query. The request must be sent to the $batch Url with a POST request. + +``` +POST $batch +``` + +With such body + +```Json +{ + requests: [{ + id: "1", + method: "post", + url: "/book", + body: { + title: "Guide of War and Peace" + } + }, { + id: "2", + method: "get", + url: "/book?$filter=contains(title, 'Guide')&$select=title" + }] +} +``` + +The answer could look like this + +```JSON +{ + responses: [{ + id: "1", + status: 201, + statusText: "Created", + headers: { + 'OData-Version': "4.0", + 'content-type': "application/json" + }, + body: { + id: "AFFE", + title: "Guide of War and Peace" + } + }, { + id: "2", + status: 200, + statusText: "OK", + headers: { + 'OData-Version': "4.0", + 'content-type': "application/json" + }, + body: { + value: [{ + title: "Guide of War and Peace" + }] + } + } +} +``` + +## Annotations + +At the different levels of the service, you can extend the metadata using annotations. Before an annotation can be applied, it must first be defined. Here we defined a simple annotation called 'readonly' of type 'boolean'. The scope of annotation is limited to the properties of entities and singletons. + +```Javascript +const vocabulary = server.vocabulary(); + +vocabulary.define('readonly', 'boolean', ['Property']); +``` + +The metadata can be annotated directly in your definition + +```Javascript +server.entity('book', null, { + $Key: ['id'], + id: { + ... + }, + author: { + $Type: 'Edm.String', + ...vocabulary.annotate('readonly', 'Property', true) + } +}); +``` + +or later. This variant has the advantage that the name of the property can be validated against the metadata of the entity. + +```Javascript +const book = server.entity('book', null, { + $Key: ['id'], + id: { + ... + }, + author: { + $Type: 'Edm.String' + } +}); + +book.annotateProperty('author', 'readonly', true); ``` -## Loging +The parameters of the actions can also be annotated. + +```Javascript +vocabulary.define('readonly', 'boolean', ['Parameter']); -In the event of an unexpected error, no meaningful error message is returned to the frontend. This is necessary to make it harder for hackers. However, the development will not be easy either. For this reason there are additional logging routines. Logging can be switched on and off by setting the log level. To do this, you would have to set the environment variable ```LOG_LEVEL``` to the value ```debug```. In this case, messages that are still not meaningful are sent to the frontend, but the exception objects are logged in the log files. In addition, the start of processing of each resource and each hook is also logged. +const action = server.action('changePassword', + (req, res, next) => { }, { + $Parameter: [{ + $Type: 'Edm.String', + $Name: 'newPassword' + }, { + ... + }] +}); + +action.annotateParameter('newPassword', 'readonly', true); +``` +In addition, complex annotations can be defined. This allows entities, singletons and actions to be annotated. The passed list is validated against the properties or parameters. + +```Javascript +const vocabulary = server.vocabulary(); + +vocabulary.define('filterable', { + item: ['property'], // property for Entities and Singletons, parameter for Actions + type: 'string' +}, ['Entity Type']); // Entiy Type, Singleton, Action + +const entity = server.entity('book', null, { + $Key: ['id'], + id: { + ... + }, + author: { + $Type: 'Edm.String' + }, + title: { + $Type: 'Edm.String' + } +}); + +entity.annotate('filterable', ['author', 'title']); +``` ## Current State node-odata is currently at an beta stage, it is stable but not 100% feature complete. node-odata is written by ECMAScript 6 then compiled by [babel](https://babeljs.io/). -It currently have to dependent on MongoDB yet. -The current target is to add more features (eg. $metadata) and make to support other database. (eg. MySQL, PostgreSQL). +It currently supports MongoDB only. +The current target is to add more features and make to support other database. (eg. MySQL, PostgreSQL). ## Installation @@ -338,11 +633,14 @@ npm install node-odata * [x] $orderby * [ ] $expand * [x] $metadata generation +* [X] Batch request +* [X] Singleton +* [X] Annotations ## CONTRIBUTING -We always welcome contributions to help make node-odata better. Please feel free to contribute to this project. The package-lock.json file was last created with node version 16.14.2. +We always welcome contributions to help make node-odata better. Please feel free to contribute to this project. The package-lock.json file was last created with node version 18.17.0. Current implementation ist tested with MongoDB version 4.4.4. ## LICENSE diff --git a/examples/db.js b/examples/db.js index 6c963f1..818e7f0 100644 --- a/examples/db.js +++ b/examples/db.js @@ -1,15 +1,21 @@ const mongoose = require('mongoose'); +const server = require('./server'); -require('./models/book'); -require('./models/complex-resource'); -require('./models/user'); +server.addBefore(async (req, res, next) => { + try { + req.$odata = { + ...req.$odata, + mongo: await mongoose.connect(process.env.DATABASE || 'mongodb://localhost:27017/odata-test') + }; -module.exports = mongoose.connect(process.env.DATABASE || 'mongodb://localhost:27017/odata-test', null, (err) => { - if (err) { + next(); + + } catch(err) { console.error(err.message); console.error('Failed to connect to database on startup.'); process.exit(); } + }); // provide a event listener to handle not able to connect DB. diff --git a/examples/actions/50off.js b/examples/entities/book/50off.js similarity index 100% rename from examples/actions/50off.js rename to examples/entities/book/50off.js diff --git a/examples/models/book.js b/examples/entities/book/db.js similarity index 100% rename from examples/models/book.js rename to examples/entities/book/db.js diff --git a/examples/entities/book/index.js b/examples/entities/book/index.js new file mode 100644 index 0000000..1647551 --- /dev/null +++ b/examples/entities/book/index.js @@ -0,0 +1,8 @@ +const bookModel = require('./db'); +const server = require('../../server'); +const _50off = require('./50off'); +const bookEntity = server.mongoEntity('book', bookModel); + +bookEntity.action('50off', _50off, { + binding: 'entity' +}); \ No newline at end of file diff --git a/examples/models/complex-resource.js b/examples/entities/complex-resource/db.js similarity index 100% rename from examples/models/complex-resource.js rename to examples/entities/complex-resource/db.js diff --git a/examples/entities/complex-resource/index.js b/examples/entities/complex-resource/index.js new file mode 100644 index 0000000..5c790fa --- /dev/null +++ b/examples/entities/complex-resource/index.js @@ -0,0 +1,4 @@ +const server = require('../../server'); +const comlexResource = require('./db'); + +server.mongoEntity('complex-resource', comlexResource); \ No newline at end of file diff --git a/examples/models/user.js b/examples/entities/user/db.js similarity index 100% rename from examples/models/user.js rename to examples/entities/user/db.js diff --git a/examples/entities/user/index.js b/examples/entities/user/index.js new file mode 100644 index 0000000..b00a370 --- /dev/null +++ b/examples/entities/user/index.js @@ -0,0 +1,4 @@ +const server = require('../../server'); +const user = require('./db'); + +server.mongoEntity('user', user); \ No newline at end of file diff --git a/examples/functions/license.js b/examples/functions/license.js index 5be02d6..abd568a 100644 --- a/examples/functions/license.js +++ b/examples/functions/license.js @@ -1,6 +1,9 @@ -module.exports = function(req, res, next) { +const server = require('../server'); + +server.function('license', function(req, res, next) { res.$odata.result = { license: 'MIT' }; res.$odata.status = 200; next(); -}; +}); + diff --git a/examples/functions/server-time.js b/examples/functions/server-time.js index 4efbf77..0200c21 100644 --- a/examples/functions/server-time.js +++ b/examples/functions/server-time.js @@ -1,7 +1,9 @@ -module.exports = function(req, res, next) { +const server = require('../server'); + +server.function('server-time', function(req, res, next) { res.$odata.result = { date: new Date() }; res.$odata.status = 200; next(); -}; +}); diff --git a/examples/index.js b/examples/index.js new file mode 100644 index 0000000..fc71ff0 --- /dev/null +++ b/examples/index.js @@ -0,0 +1,26 @@ +require('./db'); // database connection +require('./entities/complex-resource'); +require('./entities/book'); +require('./entities/user'); +require('./functions/license'); +require('./functions/server-time'); + +const server = require('./server'); +const bookModel = require('./entities/book/db'); + +// add some test data +const data = require('../test/support/books.json'); +bookModel.deleteMany({}).then(function() { + data.forEach(function(item) { + const parseditem = JSON.parse(JSON.stringify(item)); + + delete parseditem.id; + entity = new bookModel(parseditem); + entity.save(); + }); +}); + +// server start +server.listen(3000, function(){ + console.log('OData services has started, you can visit by http://localhost:3000/'); +}); \ No newline at end of file diff --git a/examples/server.js b/examples/server.js index 3e0edf9..dbe6e22 100644 --- a/examples/server.js +++ b/examples/server.js @@ -1,48 +1,3 @@ const odata = require('../'); -const server = odata(); - -// database -const connection = require('./db'); - -server.addBefore((req, res, next) => { - req.$odata = { - mongo: connection - }; - next(); -}); - -// entities -const comlexResource = require('./models/complex-resource'); -server.mongoEntity('complex-resource', comlexResource); - -const user = require('./models/user'); -server.mongoEntity('user', user); - -const bookModel = require('./models/book'); -const _50off = require('./actions/50off'); -const bookEntity = server.mongoEntity('book', bookModel); -bookEntity.action('50off', _50off, { - binding: 'entity' -}); - -// add some test data -const data = require('../test/support/books.json'); -bookModel.deleteMany({}, function(err, result) { - data.forEach(function(item) { - entity = new bookModel(item); - entity.save(); - }); -}); - -// unbind functions -const serverTime = require('./functions/server-time'); -server.function('server-time', serverTime); - -const license = require('./functions/license'); -server.function('license', license); - -// server start -server.listen(3000, function(){ - console.log('OData services has started, you can visit by http://localhost:3000/'); -}); \ No newline at end of file +module.exports = odata(); \ No newline at end of file diff --git a/test/failing/stackoverflow.js b/test/failing/stackoverflow.js deleted file mode 100644 index 1607d10..0000000 --- a/test/failing/stackoverflow.js +++ /dev/null @@ -1,61 +0,0 @@ -import 'should'; -import request from 'supertest'; -import { odata, host, port, bookSchema, assertSuccess } from '../support/setup'; -import data from '../support/books.json'; -import FakeDb from '../support/fake-db'; -import sinon from 'sinon'; - -describe('odata.batch', () => { - let httpServer, books, resource, sandbox; - - beforeEach(async function () { - const db = new FakeDb(); - const server = odata(db); - resource = server.resource('book', bookSchema); - books = JSON.parse(JSON.stringify(db.addData('book', data))); - httpServer = server.listen(port); - sandbox = sinon.createSandbox(); - }); - - afterEach(() => { - httpServer.close(); - sandbox.restore(); - }); - - it('should work with multipart request body', async function () { - const result = { - title: "War and peace" - }; - const res = await request(host) - .post(`/$batch`) - .send({}) - .set('Content-Type', 'multipart/mixed; boundary=batch_1') - .set('Host', host) - .serialize(() => ` ---batch_1 -Content-Type: application/http - -POST /book -Host: ${host} -Content-Type: application/json -Content-Length: ${JSON.stringify(result).length} - -${JSON.stringify(result)} ---batch_1-- - `); - - assertSuccess(res); - - res.text.should.equal(` ---batch-1 -Content-Type: application/http - -HTTP/1.1 200 Ok -Content-Type: application/json -Content-Length: ${JSON.stringify(result).length} - -${JSON.stringify(result)} ---batch-1— - `); - }); -}); From 02915031ff6f14d1f3fddbb38c4718cf2a2f8918 Mon Sep 17 00:00:00 2001 From: Richard Martens Date: Wed, 18 Oct 2023 15:19:29 +0200 Subject: [PATCH 55/64] start script path updated --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 5a2efb6..2d600a0 100644 --- a/package.json +++ b/package.json @@ -16,7 +16,7 @@ "node": ">=0.12" }, "scripts": { - "start": "node ./examples/server.js", + "start": "node ./examples/index.js", "lint": "eslint src/", "prepublish": "make", "test": "make", From c0991297922565febb3af3a30fa096ad35fac06f Mon Sep 17 00:00:00 2001 From: Richard Martens Date: Fri, 3 Nov 2023 21:46:35 +0100 Subject: [PATCH 56/64] fix first clone bug --- test/support/setup.js | 3 --- 1 file changed, 3 deletions(-) diff --git a/test/support/setup.js b/test/support/setup.js index e4e11d3..ed55a12 100644 --- a/test/support/setup.js +++ b/test/support/setup.js @@ -1,6 +1,3 @@ -import mongoose from 'mongoose'; -import id from '../../lib/model/idPlugin'; - export odata from '../../src'; export const host = 'http://localhost:3000'; export const port = '3000'; From d0762e537393b1562632cf1fa90e49d31d21e101 Mon Sep 17 00:00:00 2001 From: Richard Martens Date: Sun, 29 Oct 2023 21:40:55 +0100 Subject: [PATCH 57/64] mapping refactored and client support for get implemented --- .vscode/launch.json | 2 +- src/mongo/Entity.js | 33 +++++---- src/mongo/Singleton.js | 12 ++- src/mongo/rest/get.js | 22 ++++-- src/odata/entity/Entity.js | 98 ++++++++++++++++--------- src/odata/entity/Singleton.js | 12 ++- src/odata/entity/parser/client.js | 8 ++ src/odata/entity/parser/property.js | 2 +- src/odata/parser/value.js | 1 - src/server.js | 64 +++++++++------- test/mongo/metadata.resource.complex.js | 12 ++- test/mongo/mocked/model.custom.id.js | 35 +++++---- test/mongo/mocked/odata.client.js | 55 ++++++++++++++ test/odata.entity.js | 2 +- test/odata.filter.js | 12 +-- 15 files changed, 256 insertions(+), 114 deletions(-) create mode 100644 src/odata/entity/parser/client.js create mode 100644 test/mongo/mocked/odata.client.js diff --git a/.vscode/launch.json b/.vscode/launch.json index 0ac71ca..90d3f72 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -20,7 +20,7 @@ "dot", "--timeout", "300000", - "test/odata.actions.js" + "test/mongo/mocked/odata.client.js" ], "env": { "LOG_LEVEL": "debug" diff --git a/src/mongo/Entity.js b/src/mongo/Entity.js index da83b18..b5ae4c7 100644 --- a/src/mongo/Entity.js +++ b/src/mongo/Entity.js @@ -8,23 +8,22 @@ import count from './rest/count'; import { validate, validateIdentifier } from '../odata/validator'; export default class MongoEntity { - constructor(name, model, annotations, mapping) { + constructor(name, model, annotations) { this.name = name; this.model = model; this.annotations = annotations; this.complexTypes = {}; this.count = 0; - this.mapping = { + this._mapping = { id: { - target: '_id', + intern: '_id', attributes: { $Type: 'Edm.String', $MaxLength: 24, $Nullable: true } - }, - ...mapping + } }; } @@ -70,12 +69,20 @@ export default class MongoEntity { return this.metadata; } - getMapping() { - if (!this.metadata) { - this.getMetadata(); - } + get mapping() { + return this._mapping; + } - return this.mapping; + set mapping(value) { + // update types of properties + Object.keys(value).forEach(name => { + //TODO: validation + if (this.metadata[name]?.$Type != value[name]?.attributes?.$Type) { + delete this.complexType[this.metadata[name].$Type]; + this.metadata[name].$Type = value[name].attributes.$Type; + } + }); + this._mapping = value; } getComplexTypes() { @@ -171,7 +178,7 @@ export default class MongoEntity { } complexType(node) { - const mapping = Object.keys(this.mapping).find(item => this.mapping[item].target === node.path); + const mapping = Object.keys(this.mapping).find(item => this.mapping[item].intern === node.path); if (mapping && this.mapping[mapping].attributes?.$Type) { return this.mapping[mapping].attributes?.$Type; @@ -209,7 +216,7 @@ export default class MongoEntity { .reduce((previousProperty, curentProperty) => { let result; let propertyName = Object.keys(this.mapping) - .find(name => this.mapping[name]?.target === curentProperty); + .find(name => this.mapping[name]?.intern === curentProperty); if (propertyName && this.mapping[propertyName].attributes) { result = { @@ -293,7 +300,7 @@ export default class MongoEntity { } this.mapping[odataProperty] = { - target: mongoProperty + intern: mongoProperty }; } diff --git a/src/mongo/Singleton.js b/src/mongo/Singleton.js index 2e84176..ec1a3e5 100644 --- a/src/mongo/Singleton.js +++ b/src/mongo/Singleton.js @@ -6,12 +6,20 @@ import getSingleton from "./rest/getSingleton"; import patchSingleton from "./rest/patchSingleton"; export default class MongoSingleton { - constructor(name, model, annotations, mapping) { + constructor(name, model, annotations) { this.name = name; - this.entity = new MongoEntity(name, model, annotations, mapping); + this.entity = new MongoEntity(name, model, annotations); } + get mapping() { + return this.entity.mapping; + } + + set mapping(value) { + this.entity.mapping = value; + } + getHandler() { const rest = { post, diff --git a/src/mongo/rest/get.js b/src/mongo/rest/get.js index aef1274..98a75b2 100644 --- a/src/mongo/rest/get.js +++ b/src/mongo/rest/get.js @@ -1,15 +1,25 @@ +function throwNotFound() { + const result = new Error('Not Found'); + + result.status = 404; + throw result; +} export default async (req, res, next) => { try { - const entity = await req.$odata.Model.findById(req.$odata.$Key._id); + let entity = await req.$odata.Model.findById(req.$odata.$Key._id); + + debugger; + if (!entity) { // client check + throwNotFound(); + } - if (!entity) { - const result = new Error('Not Found'); + entity = entity.toObject(); - result.status = 404; - throw result; + if (req.$odata.clientField && entity[req.$odata.clientField] !== req.$odata.client) { + throwNotFound(); } - res.$odata.result = entity.toObject(); + res.$odata.result = entity; next(); } catch (err) { diff --git a/src/odata/entity/Entity.js b/src/odata/entity/Entity.js index 46ec40d..4ebe8dd 100644 --- a/src/odata/entity/Entity.js +++ b/src/odata/entity/Entity.js @@ -7,10 +7,39 @@ import parseKeys from './parser/keys'; import parseCount from './parser/count'; import parseFilter from './parser/filter'; import parseOrderBy from "./parser/orderby"; +import parseClient from './parser/client'; import { parseSkip, parseTop } from "./parser/skiptop"; export default class Entity { - constructor(name, handler, metadata, settings, annotations, mapping) { + + get mapping() { + return this._mapping; + } + + set mapping(value) { + // update types of properties + Object.keys(value).forEach(name => { + //TODO: validation + if (this.metadata[name]?.$Type != value[name]?.attributes?.$Type) { + this.metadata[name].$Type = value[name].attributes.$Type; + } + }); + + this._mapping = value; + } + + get clientField() { + return this._clientField; + } + + set clientField(value) { + if (!this.metadata[value]) { + throw new Error(`Entity '${this.name}' does'nt have property named '${value}'`); + } + this._clientField = value; + } + + constructor(name, handler, metadata, settings, annotations) { const notImplemented = op => (req, res) => { const error = new Error(`Operation '${op}' is not implemented'`); @@ -42,7 +71,7 @@ export default class Entity { }; this.annotations = annotations; - this.mapping = mapping || {}; + this._mapping = {}; } addBefore(fn, name) { @@ -134,16 +163,17 @@ export default class Entity { parsingMiddleware(req, res, next) { try { - req.$odata.$Key = parseKeys(req, this.name, this.metadata, this.mapping); - req.$odata.$select = parseSelect(req, this.name, this.metadata, this.mapping); - req.$odata.$filter = parseFilter(req, this.name, this.metadata, this.mapping); - req.$odata.$count = parseCount(req, this.name, this.metadata); - req.$odata.$orderby = parseOrderBy(req, this.name, this.metadata, this.mapping, this.options.orderby); - req.$odata.$skip = parseSkip(req, this.options.maxSkip); - req.$odata.$top = parseTop(req, this.options.maxTop); - req.$odata = { ...req.$odata, + $Key: parseKeys(req, this.name, this.metadata, this.mapping), + $select: parseSelect(req, this.name, this.metadata, this.mapping), + $filter: parseFilter(req, this.name, this.metadata, this.mapping), + $count: parseCount(req, this.name, this.metadata), + $orderby: parseOrderBy(req, this.name, this.metadata, this.mapping, this.options.orderby), + $skip: parseSkip(req, this.options.maxSkip), + $top: parseTop(req, this.options.maxTop), + clientField: this.clientField, + client: parseClient(req, this.clientField, this.metadata), body: req.body, $expand: req.query.$expand, // TODO : implement expand $search: req.query.$search // TODO : implement search @@ -168,7 +198,7 @@ export default class Entity { } else { const keys = Object.keys(this.mapping); - result.mapping = keys.find(name => this.mapping[name].target === member); + result.mapping = keys.find(name => this.mapping[name].intern === member); if (result.mapping) { result.propertyMetadata = this.mapping[result.mapping].attributes; @@ -267,40 +297,40 @@ export default class Entity { ctrl(name, handler) { return async (req, res, next) => { try { - res.$odata.status = 200; + res.$odata.status = 200; + + if (name === 'list' && req.$odata.$count) { + const countResponse = { + $odata: {} + }; - if (name === 'list' && req.$odata.$count) { - const countResponse = { - $odata: {} - }; + this.handler.count(req, countResponse, async err => { + if (err) { + next(err); + return; + } - this.handler.count(req, countResponse, async err => { - if (err) { - next(err); - return; - } + res.$odata.result = { + ['@odata.count']: countResponse.$odata.result + }; + await handler(req, res, next); + }); - res.$odata.result = { - ['@odata.count']: countResponse.$odata.result - }; + } else { await handler(req, res, next); - }); - } else { - await handler(req, res, next); + } + } catch (err) { + next(err); } - - } catch(err) { - next(err); - } }; } getMetadata() { return this.metadata; } - + annotate(anno, value) { if (!anno) { throw new Error('Name of annotation term should be given'); @@ -360,7 +390,7 @@ export default class Entity { } getResourceUrl(name) { - const entityName = name || this.name; + const entityName = name || this.name; const resourceListURL = `/${entityName}`; if (this.metadata.$Key.length === 1) { @@ -395,7 +425,7 @@ export default class Entity { regex: resourceListRegex }, { - name: 'put', + name: 'put', method: 'put', url: resourceURL, regex: resourceRegex diff --git a/src/odata/entity/Singleton.js b/src/odata/entity/Singleton.js index 003ce8a..8fd1f5f 100644 --- a/src/odata/entity/Singleton.js +++ b/src/odata/entity/Singleton.js @@ -4,7 +4,7 @@ import Hooks from "../Hooks"; import { Router } from 'express'; export default class Singleton { - constructor(name, handler, metadata, annotations, mapping) { + constructor(name, handler, metadata, annotations) { const notSupported = (req, res) => { const error = new Error(); @@ -13,7 +13,7 @@ export default class Singleton { }; this.name = name; - this.entity = metadata instanceof Entity ? metadata : new Entity(name, handler, metadata, null, annotations, mapping); + this.entity = metadata instanceof Entity ? metadata : new Entity(name, handler, metadata, null, annotations); this.handler = { ...this.entity.handler, // get, post, put, delete, patch @@ -26,6 +26,14 @@ export default class Singleton { } + get mapping() { + return this.entity.mapping; + } + + set mapping(value) { + this.entity.mapping = value; + } + addBefore(fn, name) { this.hooks.addBefore(fn, name); } diff --git a/src/odata/entity/parser/client.js b/src/odata/entity/parser/client.js new file mode 100644 index 0000000..30aff10 --- /dev/null +++ b/src/odata/entity/parser/client.js @@ -0,0 +1,8 @@ +import parseValue from '../../parser/value'; + +module.exports = function parseClient(req, target, metadata) { + debugger; + if (req.query['sap-client']) { + return parseValue(req.query['sap-client'], metadata[target]); + } +} \ No newline at end of file diff --git a/src/odata/entity/parser/property.js b/src/odata/entity/parser/property.js index 6d6ff73..2348c1e 100644 --- a/src/odata/entity/parser/property.js +++ b/src/odata/entity/parser/property.js @@ -2,7 +2,7 @@ export default function parseProperty(filter, mapping) { let property = filter?.trim(); if (mapping[property]) { - property = mapping[property].target; + property = mapping[property].intern; } diff --git a/src/odata/parser/value.js b/src/odata/parser/value.js index cf0e1d8..f1ad9cb 100644 --- a/src/odata/parser/value.js +++ b/src/odata/parser/value.js @@ -40,7 +40,6 @@ function parseDate(value, metadata) { export default function (value, metadata) { const trimmed = value.trim(); - debugger; if (metadata.$Nullable && trimmed === 'null') { return null; } diff --git a/src/server.js b/src/server.js index 319b176..2608516 100644 --- a/src/server.js +++ b/src/server.js @@ -22,7 +22,7 @@ function checkAuth(auth, req) { class Server { constructor(prefix, options) { const opts = (options && options.expressRequestLimit) - ? { limit: options.expressRequestLimit } : {}; + ? { limit: options.expressRequestLimit } : {}; this._app = createExpress(options); this._settings = { @@ -75,14 +75,14 @@ class Server { addAfter(fn, name) { this.hooks.addAfter(fn, name); } - + function(url, middleware, params) { const func = new Func(url.replace(/[ /]+/, ''), middleware, params); this.resources[func.getName()] = func; } - entity(name, handler, metadata, settings, mapping) { + entity(name, handler, metadata, settings) { if (this.resources[name]) { throw new Error(`Entity with name "${name}" already defined`); } @@ -92,12 +92,12 @@ class Server { maxTop: this._settings.maxTop, orderby: this._settings.orderby, ...settings - }, this.annotations, mapping); + }, this.annotations); return this.resources[name]; } - mongoEntity(name, model, handler, metadata, settings, mapping) { + mongoEntity(name, model, handler, metadata, settings, registerComplexTypes = true) { if (name && !model) { if (!this.resources[name]) { throw new Error(`Entity '${name}' is not defined`); @@ -105,49 +105,55 @@ class Server { return this.resources[name]; } - const entity = new MongoEntity(name, model, this.annotations, mapping); + const mongoEntity = new MongoEntity(name, model, this.annotations); - const complexTypes = entity.getComplexTypes(); + if (registerComplexTypes) { + const complexTypes = mongoEntity.getComplexTypes(); - if (complexTypes) { - Object.keys(complexTypes) - .forEach(typeName => { - const type = complexTypes[typeName]; + if (complexTypes) { + Object.keys(complexTypes) + .forEach(typeName => { + const type = complexTypes[typeName]; - this.complexType(typeName, type); - }); + this.complexType(typeName, type); + }); + } } - return this.entity(name, { - ...entity.getHandler(), + const entity = this.entity(name, { + ...mongoEntity.getHandler(), ...handler }, { - ...entity.getMetadata(), + ...mongoEntity.getMetadata(), ...metadata - }, settings, entity.getMapping()); + }, settings); + + entity.mapping = mongoEntity.mapping; + + return entity; } - singleton(name, handler, metadata, mapping) { + singleton(name, handler, metadata) { if (this.resources[name]) { throw new Error(`Entity with name "${name}" already defined`); } - this.resources[name] = new Singleton(name, handler, metadata, this.annotations, mapping); + this.resources[name] = new Singleton(name, handler, metadata, this.annotations); return this.resources[name]; } - singletonFrom(name, handler, entity, mapping) { + singletonFrom(name, handler, entity) { if (this.resources[name]) { throw new Error(`Entity with name "${name}" already defined`); } - this.resources[name] = new Singleton(name, handler, entity, this.annotations, mapping); + this.resources[name] = new Singleton(name, handler, entity, this.annotations); return this.resources[name]; } - mongoSingleton(name, model, handler, metadata, mapping) { + mongoSingleton(name, model, handler, metadata) { if (name && !model) { if (!this.resources[name]) { throw new Error(`Entity '${name}' is not defined`); @@ -155,7 +161,7 @@ class Server { return this.resources[name]; } - const entity = new MongoSingleton(name, model, this.annotations, mapping); + const entity = new MongoSingleton(name, model, this.annotations); const complexTypes = entity.entity.getComplexTypes(); @@ -168,18 +174,20 @@ class Server { }); } - return this.singleton(name, { + const singletonEntity = this.singleton(name, { ...entity.getHandler(), ...handler }, { ...entity.entity.getMetadata(), ...metadata - }, { - ...entity.entity.getMapping(), - ...mapping }); + + singletonEntity.mapping = { + ...singletonEntity.mapping, + ...entity.mapping + } } - + defaultConfiguration(prefix = '') { this.set('app', this._app); this.set('prefix', prefix); diff --git a/test/mongo/metadata.resource.complex.js b/test/mongo/metadata.resource.complex.js index e643a8c..d73ce8c 100644 --- a/test/mongo/metadata.resource.complex.js +++ b/test/mongo/metadata.resource.complex.js @@ -451,16 +451,22 @@ describe('mongo.metadata.resource.complex', () => { $Type: 'Edm.String' } }); - server.mongoEntity('p1', ComplexModel, undefined, undefined, undefined, { + const entity = server.mongoEntity('p1', ComplexModel, undefined, undefined, undefined, false); + + entity.mapping = { + ...entity.mapping, p2: { - target: 'p2', + intern: 'p2', attributes: { $Type: 'node.odata.myComlexType' } } - }); + }; + entity.update httpServer = server.listen(port); + const res = await request(host).get('/$metadata?$format=json'); + res.statusCode.should.equal(200); res.body.should.deepEqual(jsonDocument); }); diff --git a/test/mongo/mocked/model.custom.id.js b/test/mongo/mocked/model.custom.id.js index b3d2e58..797bf7e 100644 --- a/test/mongo/mocked/model.custom.id.js +++ b/test/mongo/mocked/model.custom.id.js @@ -22,18 +22,21 @@ describe('mongo.mocked.model.custom.id', () => { Model = mongoose.model('custom-id', ModelSchema); - server.mongoEntity('custom-id', Model, undefined, { + const entity = server.mongoEntity('custom-id', Model, undefined, { id: { $Type: 'Edm.Int16' } - }, undefined, { + }, undefined); + + entity.mapping = { id: { - target: 'id', + intern: 'id', attributes: { $Type: 'Edm.Int16' } } - }); + }; + init(server); httpServer = server.listen(port); @@ -51,14 +54,14 @@ describe('mongo.mocked.model.custom.id', () => { it('should work when use a custom id to query specific entity', async function () { modelMock = sinon.mock(Model); modelMock.expects('findById').once() - .returns(new Promise(resolve => resolve({ - toObject: () => ({ - id: 100 - }) - }))); + .returns(new Promise(resolve => resolve({ + toObject: () => ({ + id: 100 + }) + }))); const res = await request(host).get('/custom-id(100)'); - + assertSuccess(res); res.body.id.should.be.equal(100); modelMock.verify(); @@ -73,13 +76,13 @@ describe('mongo.mocked.model.custom.id', () => { }; modelMock = sinon.mock(Model); queryMock = sinon.mock(query); - modelMock.expects('find').once().withArgs({id: {$eq: 100}}).returns(query); + modelMock.expects('find').once().withArgs({ id: { $eq: 100 } }).returns(query); queryMock.expects('exec').once() - .returns(new Promise(resolve => resolve([{ - toObject: () => ({ - id: 100 - }) - }]))); + .returns(new Promise(resolve => resolve([{ + toObject: () => ({ + id: 100 + }) + }]))); const res = await request(host).get('/custom-id?$filter=id eq 100'); diff --git a/test/mongo/mocked/odata.client.js b/test/mongo/mocked/odata.client.js new file mode 100644 index 0000000..1c1cb53 --- /dev/null +++ b/test/mongo/mocked/odata.client.js @@ -0,0 +1,55 @@ +import 'should'; +import request from 'supertest'; +import sinon from 'sinon'; +import { odata, host, port, assertSuccess } from '../../support/setup'; +import mongoose from 'mongoose'; +import { init } from '../../support/db'; + +const Schema = mongoose.Schema; +const ModelSchema = new Schema({ + client: Number +}); + +const Model = mongoose.model('client', ModelSchema); + +describe('mongo.mocked.odata.entity', () => { + let httpServer, server, modelMock; + + beforeEach(async function () { + server = odata(); + init(server); + + const entity = server.mongoEntity('client', Model); + + entity.clientField = 'client'; + + }); + + afterEach(() => { + if (httpServer) { + httpServer.close(); + } + modelMock?.restore(); + }); + + + it('should apply client to the key', async function () { + modelMock = sinon.mock(Model); + modelMock.expects('findById').once() + .withArgs('1') + .returns(new Promise(resolve => resolve({ + toObject: () => ({ + _id: 1, + client: 99 + }) + }))); + httpServer = server.listen(port); + + const res = await request(host).get(`/client('1')?sap-client=099`); + + assertSuccess(res); + + modelMock.verify(); + }); + +}); diff --git a/test/odata.entity.js b/test/odata.entity.js index 803c93c..074270d 100644 --- a/test/odata.entity.js +++ b/test/odata.entity.js @@ -1,6 +1,6 @@ import 'should'; import request from 'supertest'; -import { odata, host, port } from './support/setup'; +import { odata, host, port, assertSuccess } from './support/setup'; describe('odata.entity', () => { let httpServer, server; diff --git a/test/odata.filter.js b/test/odata.filter.js index 9925548..658112b 100644 --- a/test/odata.filter.js +++ b/test/odata.filter.js @@ -270,17 +270,17 @@ describe('odata.filter', () => { }); it('should use mapping', async function () { - server.entity('book', { + const book = server.entity('book', { list: (req, res, next) => { req.$odata.$filter.should.deepEqual({ _id: { $eq: '2' } }); res.$odata.result = { value: [] }; next(); } - }, BookMetadata, null, { - id: { - target: '_id' - } - }); + }, BookMetadata, null); + + book.mapping.id = { + intern: '_id' + }; httpServer = server.listen(port); const res = await request(host).get(encodeURI(`/book?$filter=id eq '2'`)); From 7daf813dc736774bc249a0a806ec826f6595b6a8 Mon Sep 17 00:00:00 2001 From: Richard Martens Date: Mon, 30 Oct 2023 21:17:55 +0100 Subject: [PATCH 58/64] client support for delete and count --- src/mongo/rest/count.js | 5 +- src/mongo/rest/delete.js | 25 +++++-- src/mongo/rest/get.js | 5 +- src/odata/entity/Entity.js | 2 +- src/odata/entity/parser/client.js | 10 ++- test/mongo/mocked/odata.client.js | 104 +++++++++++++++++++++++++++++- 6 files changed, 138 insertions(+), 13 deletions(-) diff --git a/src/mongo/rest/count.js b/src/mongo/rest/count.js index f583c05..dc2d677 100644 --- a/src/mongo/rest/count.js +++ b/src/mongo/rest/count.js @@ -1,6 +1,9 @@ export default async (req, res, next) => { try { - const query = req.$odata.Model.find(); + const filter = req.$odata.clientField && req.$odata.client ? { // special client filter + [req.$odata.clientField]: req.$odata.client + } : undefined; + const query = req.$odata.Model.find(filter); const count = await query.count(); res.$odata.result = count.toString(); diff --git a/src/mongo/rest/delete.js b/src/mongo/rest/delete.js index 6906ff4..289c196 100644 --- a/src/mongo/rest/delete.js +++ b/src/mongo/rest/delete.js @@ -1,12 +1,27 @@ export default async (req, res, next) => { try { - const result = await req.$odata.Model.deleteOne({ _id: req.$odata.$Key._id }); + if (req.$odata.clientField) { + const entity = await req.$odata.Model.findById(req.$odata.$Key._id); - if (JSON.parse(result).n === 0) { - const error = new Error('Not Found'); + if (!entity || entity[req.$odata.clientField] !== req.$odata.client) { + const error = new Error('Not Found'); + + error.status = 404; + throw error; + } + + await entity.deleteOne(); + + } else { + const result = await req.$odata.Model.deleteOne({ _id: req.$odata.$Key._id }); + + if (JSON.parse(result).n === 0) { + const error = new Error('Not Found'); + + error.status = 404; + throw error; + } - error.status = 404; - throw error; } res.$odata.status = 204; diff --git a/src/mongo/rest/get.js b/src/mongo/rest/get.js index 98a75b2..3809f16 100644 --- a/src/mongo/rest/get.js +++ b/src/mongo/rest/get.js @@ -8,14 +8,13 @@ export default async (req, res, next) => { try { let entity = await req.$odata.Model.findById(req.$odata.$Key._id); - debugger; - if (!entity) { // client check + if (!entity) { throwNotFound(); } entity = entity.toObject(); - if (req.$odata.clientField && entity[req.$odata.clientField] !== req.$odata.client) { + if (req.$odata.clientField && entity[req.$odata.clientField] !== req.$odata.client) { // client check throwNotFound(); } diff --git a/src/odata/entity/Entity.js b/src/odata/entity/Entity.js index 4ebe8dd..95860d1 100644 --- a/src/odata/entity/Entity.js +++ b/src/odata/entity/Entity.js @@ -173,7 +173,7 @@ export default class Entity { $skip: parseSkip(req, this.options.maxSkip), $top: parseTop(req, this.options.maxTop), clientField: this.clientField, - client: parseClient(req, this.clientField, this.metadata), + client: parseClient(req, this.name, this.metadata, this.clientField), body: req.body, $expand: req.query.$expand, // TODO : implement expand $search: req.query.$search // TODO : implement search diff --git a/src/odata/entity/parser/client.js b/src/odata/entity/parser/client.js index 30aff10..3d90c4f 100644 --- a/src/odata/entity/parser/client.js +++ b/src/odata/entity/parser/client.js @@ -1,8 +1,14 @@ import parseValue from '../../parser/value'; -module.exports = function parseClient(req, target, metadata) { - debugger; +module.exports = function parseClient(req, name, metadata, target) { if (req.query['sap-client']) { return parseValue(req.query['sap-client'], metadata[target]); + + } else if (target) { + const err = new Error(`For entity '${name}' you must send a client value`); + + err.status = 400; + throw err; + } } \ No newline at end of file diff --git a/test/mongo/mocked/odata.client.js b/test/mongo/mocked/odata.client.js index 1c1cb53..d032a46 100644 --- a/test/mongo/mocked/odata.client.js +++ b/test/mongo/mocked/odata.client.js @@ -13,7 +13,17 @@ const ModelSchema = new Schema({ const Model = mongoose.model('client', ModelSchema); describe('mongo.mocked.odata.entity', () => { - let httpServer, server, modelMock; + const query = { + $where: () => { }, + where: () => { }, + equals: () => { }, + gte: () => { }, + lt: () => { }, + exec: () => { }, + count: () => new Promise((resolve) => resolve(1)), + model: Model + }; + let httpServer, server, modelMock, instanceMock; beforeEach(async function () { server = odata(); @@ -30,8 +40,25 @@ describe('mongo.mocked.odata.entity', () => { httpServer.close(); } modelMock?.restore(); + instanceMock?.restore(); }); + it('should fail for client collection without client', async function () { + modelMock = sinon.mock(Model); + modelMock.expects('findById').never(); + httpServer = server.listen(port); + + const res = await request(host).get(`/client('1')`); + + res.body.should.deepEqual({ + error: { + code: '400', + message: `For entity 'client' you must send a client value` + } + }); + + modelMock.verify(); + }); it('should apply client to the key', async function () { modelMock = sinon.mock(Model); @@ -52,4 +79,79 @@ describe('mongo.mocked.odata.entity', () => { modelMock.verify(); }); + it('should fail with correct key and wrong client', async function () { + modelMock = sinon.mock(Model); + modelMock.expects('findById').once() + .withArgs('1') + .returns(new Promise(resolve => resolve({ + toObject: () => ({ + _id: 1, + client: 98 + }) + }))); + httpServer = server.listen(port); + + const res = await request(host).get(`/client('1')?sap-client=099`); + + res.status.should.be.equal(404); + + modelMock.verify(); + }); + + it('should apply client to the count', async function () { + modelMock = sinon.mock(Model); + modelMock.expects('find').once() + .withArgs({ + client: 99 + }).returns(query); + httpServer = server.listen(port); + + const res = await request(host).get(`/client/$count?sap-client=099`); + + assertSuccess(res); + + modelMock.verify(); + }); + + it('should fail on delete with wrong client', async function () { + modelMock = sinon.mock(Model); + modelMock.expects('findById').once() + .withArgs('1') + .returns(new Promise(resolve => resolve({ + toObject: () => ({ + _id: 1, + client: 98 + }) + }))); + httpServer = server.listen(port); + + const res = await request(host).delete(`/client('1')?sap-client=099`); + + res.status.should.be.equal(404); + + modelMock.verify(); + }); + + it('should work on delete with client', async function () { + const instance = { + client: 99, + deleteOne: async () => {} + }; + modelMock = sinon.mock(Model); + modelMock.expects('findById').once() + .withArgs('1') + .returns(new Promise(resolve => resolve(instance))); + + instanceMock = sinon.mock(instance); + instanceMock.expects('deleteOne').once(); + httpServer = server.listen(port); + + const res = await request(host).delete(`/client('1')?sap-client=099`); + + res.status.should.be.equal(204); + + modelMock.verify(); + instanceMock.verify(); + }); + }); From bec8c1e075241f55d3b7f5f1ad0adf2a6a498df9 Mon Sep 17 00:00:00 2001 From: Richard Martens Date: Fri, 3 Nov 2023 21:13:03 +0100 Subject: [PATCH 59/64] client support for singleton --- src/mongo/Singleton.js | 10 +++- src/mongo/rest/getSingleton.js | 7 ++- src/odata/entity/Singleton.js | 18 +++++- src/server.js | 1 + test/mongo/mocked/odata.client.js | 97 +++++++++++++++++++++++++++++-- 5 files changed, 124 insertions(+), 9 deletions(-) diff --git a/src/mongo/Singleton.js b/src/mongo/Singleton.js index ec1a3e5..359e638 100644 --- a/src/mongo/Singleton.js +++ b/src/mongo/Singleton.js @@ -8,10 +8,18 @@ import patchSingleton from "./rest/patchSingleton"; export default class MongoSingleton { constructor(name, model, annotations) { this.name = name; - this.entity = new MongoEntity(name, model, annotations); + this._entity = new MongoEntity(name, model, annotations); } + get entity () { + return this._entity; + } + + set entity(value) { + this._entity = value; + } + get mapping() { return this.entity.mapping; } diff --git a/src/mongo/rest/getSingleton.js b/src/mongo/rest/getSingleton.js index 965fc11..9650dd7 100644 --- a/src/mongo/rest/getSingleton.js +++ b/src/mongo/rest/getSingleton.js @@ -2,7 +2,9 @@ import selectParser from "../parser/selectParser"; export default async (req, res, next) => { try { - const query = req.$odata.Model.findOne(); + debugger; + const param = req.$odata.clientField ? { [req.$odata.clientField]: req.$odata.client } : undefined; + const query = req.$odata.Model.findOne(param); await selectParser(query, req.$odata.$select); @@ -18,6 +20,9 @@ export default async (req, res, next) => { res.$odata.result = entity.toObject(); if (transient) { res.$odata.result._id = null; + if (req.$odata.clientField) { + res.$odata.result[req.$odata.clientField] = req.$odata.client; + } } if (req.$odata.$select) { diff --git a/src/odata/entity/Singleton.js b/src/odata/entity/Singleton.js index 8fd1f5f..4bdf0e5 100644 --- a/src/odata/entity/Singleton.js +++ b/src/odata/entity/Singleton.js @@ -13,7 +13,7 @@ export default class Singleton { }; this.name = name; - this.entity = metadata instanceof Entity ? metadata : new Entity(name, handler, metadata, null, annotations); + this._entity = metadata instanceof Entity ? metadata : new Entity(name, handler, metadata, null, annotations); this.handler = { ...this.entity.handler, // get, post, put, delete, patch @@ -26,6 +26,14 @@ export default class Singleton { } + get entity () { + return this._entity; + } + + set entity(value) { + this._entity = value; + } + get mapping() { return this.entity.mapping; } @@ -34,6 +42,14 @@ export default class Singleton { this.entity.mapping = value; } + get clientField() { + return this.entity.clientField; + } + + set clientField(value) { + this.entity.clientField = value; + } + addBefore(fn, name) { this.hooks.addBefore(fn, name); } diff --git a/src/server.js b/src/server.js index 2608516..3194092 100644 --- a/src/server.js +++ b/src/server.js @@ -186,6 +186,7 @@ class Server { ...singletonEntity.mapping, ...entity.mapping } + return singletonEntity; } defaultConfiguration(prefix = '') { diff --git a/test/mongo/mocked/odata.client.js b/test/mongo/mocked/odata.client.js index d032a46..97b4b97 100644 --- a/test/mongo/mocked/odata.client.js +++ b/test/mongo/mocked/odata.client.js @@ -12,7 +12,7 @@ const ModelSchema = new Schema({ const Model = mongoose.model('client', ModelSchema); -describe('mongo.mocked.odata.entity', () => { +describe('mongo.mocked.odata.client', () => { const query = { $where: () => { }, where: () => { }, @@ -23,16 +23,12 @@ describe('mongo.mocked.odata.entity', () => { count: () => new Promise((resolve) => resolve(1)), model: Model }; - let httpServer, server, modelMock, instanceMock; + let httpServer, server, modelMock, instanceMock, queryMock; beforeEach(async function () { server = odata(); init(server); - const entity = server.mongoEntity('client', Model); - - entity.clientField = 'client'; - }); afterEach(() => { @@ -41,9 +37,78 @@ describe('mongo.mocked.odata.entity', () => { } modelMock?.restore(); instanceMock?.restore(); + queryMock?.restore(); + }); + + it('should returns a transient singleton with wrong client', async function () { + const entity = server.mongoSingleton('client', Model); + + entity.clientField = 'client'; + + modelMock = sinon.mock(Model); + modelMock.expects('findOne').once() + .withArgs({ + client: 99 + }) + .returns(query); + queryMock = sinon.mock(query); + queryMock.expects('exec').once() + .returns(new Promise(resolve => resolve())); + instanceMock = sinon.mock(Model.prototype); + instanceMock.expects('toObject').once() + .returns({}); + httpServer = server.listen(port); + + const res = await request(host).get(`/client?sap-client=099`); + + res.body.should.deepEqual({ + id: null, + client: 99 + }); + + modelMock.verify(); + queryMock.verify(); + instanceMock.verify(); + }); + + it('should returns a singleton with client', async function () { + const entity = server.mongoSingleton('client', Model); + + entity.clientField = 'client'; + + modelMock = sinon.mock(Model); + modelMock.expects('findOne').once() + .withArgs({ + client: 99 + }) + .returns(query); + queryMock = sinon.mock(query); + queryMock.expects('exec').once() + .returns(new Promise(resolve => resolve({ + toObject: () => ({ + id: '1', + client: 99 + }) + }))); + httpServer = server.listen(port); + + const res = await request(host).get(`/client?sap-client=099`); + + res.body.should.deepEqual({ + id: '1', + client: 99 + }); + + modelMock.verify(); + queryMock.verify(); + instanceMock.verify(); }); it('should fail for client collection without client', async function () { + const entity = server.mongoEntity('client', Model); + + entity.clientField = 'client'; + modelMock = sinon.mock(Model); modelMock.expects('findById').never(); httpServer = server.listen(port); @@ -61,6 +126,10 @@ describe('mongo.mocked.odata.entity', () => { }); it('should apply client to the key', async function () { + const entity = server.mongoEntity('client', Model); + + entity.clientField = 'client'; + modelMock = sinon.mock(Model); modelMock.expects('findById').once() .withArgs('1') @@ -80,6 +149,10 @@ describe('mongo.mocked.odata.entity', () => { }); it('should fail with correct key and wrong client', async function () { + const entity = server.mongoEntity('client', Model); + + entity.clientField = 'client'; + modelMock = sinon.mock(Model); modelMock.expects('findById').once() .withArgs('1') @@ -99,6 +172,10 @@ describe('mongo.mocked.odata.entity', () => { }); it('should apply client to the count', async function () { + const entity = server.mongoEntity('client', Model); + + entity.clientField = 'client'; + modelMock = sinon.mock(Model); modelMock.expects('find').once() .withArgs({ @@ -114,6 +191,10 @@ describe('mongo.mocked.odata.entity', () => { }); it('should fail on delete with wrong client', async function () { + const entity = server.mongoEntity('client', Model); + + entity.clientField = 'client'; + modelMock = sinon.mock(Model); modelMock.expects('findById').once() .withArgs('1') @@ -133,6 +214,10 @@ describe('mongo.mocked.odata.entity', () => { }); it('should work on delete with client', async function () { + const entity = server.mongoEntity('client', Model); + + entity.clientField = 'client'; + const instance = { client: 99, deleteOne: async () => {} From 6ace358f8ddd880b9956c43b5af6d46b96be2875 Mon Sep 17 00:00:00 2001 From: Richard Martens Date: Sat, 4 Nov 2023 21:44:00 +0100 Subject: [PATCH 60/64] reorg of client tests, working of client support for list --- test/mongo/mocked/client/base.js | 110 ++++++++++++ test/mongo/mocked/client/count.js | 62 +++++++ test/mongo/mocked/client/delete.js | 92 ++++++++++ test/mongo/mocked/client/list.js | 64 +++++++ test/mongo/mocked/client/singleton.js | 106 +++++++++++ test/mongo/mocked/odata.client.js | 242 -------------------------- 6 files changed, 434 insertions(+), 242 deletions(-) create mode 100644 test/mongo/mocked/client/base.js create mode 100644 test/mongo/mocked/client/count.js create mode 100644 test/mongo/mocked/client/delete.js create mode 100644 test/mongo/mocked/client/list.js create mode 100644 test/mongo/mocked/client/singleton.js delete mode 100644 test/mongo/mocked/odata.client.js diff --git a/test/mongo/mocked/client/base.js b/test/mongo/mocked/client/base.js new file mode 100644 index 0000000..092fec9 --- /dev/null +++ b/test/mongo/mocked/client/base.js @@ -0,0 +1,110 @@ +import 'should'; +import request from 'supertest'; +import sinon from 'sinon'; +import { odata, host, port, assertSuccess } from '../../../support/setup'; +import mongoose from 'mongoose'; +import { init } from '../../../support/db'; + +const Schema = mongoose.Schema; +const ModelSchema = new Schema({ + client: Number +}); + +const Model = mongoose.model('client', ModelSchema); + +describe('mongo.mocked.odata.client', () => { + const query = { + $where: () => { }, + where: () => { }, + equals: () => { }, + gte: () => { }, + lt: () => { }, + exec: () => { }, + count: () => new Promise((resolve) => resolve(1)), + model: Model + }; + let httpServer, server, modelMock, instanceMock, queryMock; + + beforeEach(async function () { + server = odata(); + init(server); + + }); + + afterEach(() => { + if (httpServer) { + httpServer.close(); + } + modelMock?.restore(); + instanceMock?.restore(); + queryMock?.restore(); + }); + + it('should fail for client collection without client', async function () { + const entity = server.mongoEntity('client', Model); + + entity.clientField = 'client'; + + modelMock = sinon.mock(Model); + modelMock.expects('findById').never(); + httpServer = server.listen(port); + + const res = await request(host).get(`/client('1')`); + + res.body.should.deepEqual({ + error: { + code: '400', + message: `For entity 'client' you must send a client value` + } + }); + + modelMock.verify(); + }); + + it('should apply client to the key', async function () { + const entity = server.mongoEntity('client', Model); + + entity.clientField = 'client'; + + modelMock = sinon.mock(Model); + modelMock.expects('findById').once() + .withArgs('1') + .returns(new Promise(resolve => resolve({ + toObject: () => ({ + _id: 1, + client: 99 + }) + }))); + httpServer = server.listen(port); + + const res = await request(host).get(`/client('1')?sap-client=099`); + + assertSuccess(res); + + modelMock.verify(); + }); + + it('should fail with correct key and wrong client', async function () { + const entity = server.mongoEntity('client', Model); + + entity.clientField = 'client'; + + modelMock = sinon.mock(Model); + modelMock.expects('findById').once() + .withArgs('1') + .returns(new Promise(resolve => resolve({ + toObject: () => ({ + _id: 1, + client: 98 + }) + }))); + httpServer = server.listen(port); + + const res = await request(host).get(`/client('1')?sap-client=099`); + + res.status.should.be.equal(404); + + modelMock.verify(); + }); + +}); diff --git a/test/mongo/mocked/client/count.js b/test/mongo/mocked/client/count.js new file mode 100644 index 0000000..b49a930 --- /dev/null +++ b/test/mongo/mocked/client/count.js @@ -0,0 +1,62 @@ +import 'should'; +import request from 'supertest'; +import sinon from 'sinon'; +import { odata, host, port, assertSuccess } from '../../../support/setup'; +import mongoose from 'mongoose'; +import { init } from '../../../support/db'; + +const Schema = mongoose.Schema; +const ModelSchema = new Schema({ + client: Number +}); + +const Model = mongoose.model('client', ModelSchema); + +describe('mongo.mocked.odata.client', () => { + const query = { + $where: () => { }, + where: () => { }, + equals: () => { }, + gte: () => { }, + lt: () => { }, + exec: () => { }, + count: () => new Promise((resolve) => resolve(1)), + model: Model + }; + let httpServer, server, modelMock, instanceMock, queryMock; + + beforeEach(async function () { + server = odata(); + init(server); + + }); + + afterEach(() => { + if (httpServer) { + httpServer.close(); + } + modelMock?.restore(); + instanceMock?.restore(); + queryMock?.restore(); + }); + + it('should apply client to the count', async function () { + const entity = server.mongoEntity('client', Model); + + entity.clientField = 'client'; + + modelMock = sinon.mock(Model); + modelMock.expects('find').once() + .withArgs({ + client: 99 + }).returns(query); + httpServer = server.listen(port); + + const res = await request(host).get(`/client/$count?sap-client=099`); + + assertSuccess(res); + + modelMock.verify(); + }); + +}); diff --git a/test/mongo/mocked/client/delete.js b/test/mongo/mocked/client/delete.js new file mode 100644 index 0000000..73fbc6a --- /dev/null +++ b/test/mongo/mocked/client/delete.js @@ -0,0 +1,92 @@ +import 'should'; +import request from 'supertest'; +import sinon from 'sinon'; +import { odata, host, port, assertSuccess } from '../../../support/setup'; +import mongoose from 'mongoose'; +import { init } from '../../../support/db'; + +const Schema = mongoose.Schema; +const ModelSchema = new Schema({ + client: Number +}); + +const Model = mongoose.model('client', ModelSchema); + +describe('mongo.mocked.odata.client', () => { + const query = { + $where: () => { }, + where: () => { }, + equals: () => { }, + gte: () => { }, + lt: () => { }, + exec: () => { }, + count: () => new Promise((resolve) => resolve(1)), + model: Model + }; + let httpServer, server, modelMock, instanceMock, queryMock; + + beforeEach(async function () { + server = odata(); + init(server); + + }); + + afterEach(() => { + if (httpServer) { + httpServer.close(); + } + modelMock?.restore(); + instanceMock?.restore(); + queryMock?.restore(); + }); + + it('should fail on delete with wrong client', async function () { + const entity = server.mongoEntity('client', Model); + + entity.clientField = 'client'; + + modelMock = sinon.mock(Model); + modelMock.expects('findById').once() + .withArgs('1') + .returns(new Promise(resolve => resolve({ + toObject: () => ({ + _id: 1, + client: 98 + }) + }))); + httpServer = server.listen(port); + + const res = await request(host).delete(`/client('1')?sap-client=099`); + + res.status.should.be.equal(404); + + modelMock.verify(); + }); + + it('should work on delete with client', async function () { + const entity = server.mongoEntity('client', Model); + + entity.clientField = 'client'; + + const instance = { + client: 99, + deleteOne: async () => {} + }; + modelMock = sinon.mock(Model); + modelMock.expects('findById').once() + .withArgs('1') + .returns(new Promise(resolve => resolve(instance))); + + instanceMock = sinon.mock(instance); + instanceMock.expects('deleteOne').once(); + httpServer = server.listen(port); + + const res = await request(host).delete(`/client('1')?sap-client=099`); + + res.status.should.be.equal(204); + + modelMock.verify(); + instanceMock.verify(); + }); + +}); diff --git a/test/mongo/mocked/client/list.js b/test/mongo/mocked/client/list.js new file mode 100644 index 0000000..70a4b12 --- /dev/null +++ b/test/mongo/mocked/client/list.js @@ -0,0 +1,64 @@ +import 'should'; +import request from 'supertest'; +import sinon from 'sinon'; +import { odata, host, port, assertSuccess } from '../../../support/setup'; +import mongoose from 'mongoose'; +import { init } from '../../../support/db'; + +const Schema = mongoose.Schema; +const ModelSchema = new Schema({ + client: Number +}); + +const Model = mongoose.model('client', ModelSchema); + +describe('mongo.mocked.odata.client', () => { + const query = { + $where: () => { }, + where: () => { }, + equals: () => { }, + gte: () => { }, + lt: () => { }, + exec: () => { }, + count: () => new Promise((resolve) => resolve(1)), + model: Model + }; + let httpServer, server, modelMock, instanceMock, queryMock; + + beforeEach(async function () { + server = odata(); + init(server); + + }); + + afterEach(() => { + if (httpServer) { + httpServer.close(); + } + modelMock?.restore(); + instanceMock?.restore(); + queryMock?.restore(); + }); + + it('should creates filter with client', async function () { + const entity = server.mongoEntity('client', Model); + + entity.clientField = 'client'; + + modelMock = sinon.mock(Model); + modelMock.expects('find').once() + .withArgs({ + client: { + eq: 99 + } + }).returns(query); + httpServer = server.listen(port); + + const res = await request(host).get(`/client?sap-client=099`); + + assertSuccess(res); + + modelMock.verify(); + }); + +}); diff --git a/test/mongo/mocked/client/singleton.js b/test/mongo/mocked/client/singleton.js new file mode 100644 index 0000000..41635a4 --- /dev/null +++ b/test/mongo/mocked/client/singleton.js @@ -0,0 +1,106 @@ +import 'should'; +import request from 'supertest'; +import sinon from 'sinon'; +import { odata, host, port, assertSuccess } from '../../../support/setup'; +import mongoose from 'mongoose'; +import { init } from '../../../support/db'; + +const Schema = mongoose.Schema; +const ModelSchema = new Schema({ + client: Number +}); + +const Model = mongoose.model('client', ModelSchema); + +describe('mongo.mocked.odata.client', () => { + const query = { + $where: () => { }, + where: () => { }, + equals: () => { }, + gte: () => { }, + lt: () => { }, + exec: () => { }, + count: () => new Promise((resolve) => resolve(1)), + model: Model + }; + let httpServer, server, modelMock, instanceMock, queryMock; + + beforeEach(async function () { + server = odata(); + init(server); + + }); + + afterEach(() => { + if (httpServer) { + httpServer.close(); + } + modelMock?.restore(); + instanceMock?.restore(); + queryMock?.restore(); + }); + + it('should returns a transient singleton with wrong client', async function () { + const entity = server.mongoSingleton('client', Model); + + entity.clientField = 'client'; + + modelMock = sinon.mock(Model); + modelMock.expects('findOne').once() + .withArgs({ + client: 99 + }) + .returns(query); + queryMock = sinon.mock(query); + queryMock.expects('exec').once() + .returns(new Promise(resolve => resolve())); + instanceMock = sinon.mock(Model.prototype); + instanceMock.expects('toObject').once() + .returns({}); + httpServer = server.listen(port); + + const res = await request(host).get(`/client?sap-client=099`); + + res.body.should.deepEqual({ + id: null, + client: 99 + }); + + modelMock.verify(); + queryMock.verify(); + instanceMock.verify(); + }); + + it('should returns a singleton with client', async function () { + const entity = server.mongoSingleton('client', Model); + + entity.clientField = 'client'; + + modelMock = sinon.mock(Model); + modelMock.expects('findOne').once() + .withArgs({ + client: 99 + }) + .returns(query); + queryMock = sinon.mock(query); + queryMock.expects('exec').once() + .returns(new Promise(resolve => resolve({ + toObject: () => ({ + id: '1', + client: 99 + }) + }))); + httpServer = server.listen(port); + + const res = await request(host).get(`/client?sap-client=099`); + + res.body.should.deepEqual({ + id: '1', + client: 99 + }); + + modelMock.verify(); + queryMock.verify(); + instanceMock.verify(); + }); +}); diff --git a/test/mongo/mocked/odata.client.js b/test/mongo/mocked/odata.client.js deleted file mode 100644 index 97b4b97..0000000 --- a/test/mongo/mocked/odata.client.js +++ /dev/null @@ -1,242 +0,0 @@ -import 'should'; -import request from 'supertest'; -import sinon from 'sinon'; -import { odata, host, port, assertSuccess } from '../../support/setup'; -import mongoose from 'mongoose'; -import { init } from '../../support/db'; - -const Schema = mongoose.Schema; -const ModelSchema = new Schema({ - client: Number -}); - -const Model = mongoose.model('client', ModelSchema); - -describe('mongo.mocked.odata.client', () => { - const query = { - $where: () => { }, - where: () => { }, - equals: () => { }, - gte: () => { }, - lt: () => { }, - exec: () => { }, - count: () => new Promise((resolve) => resolve(1)), - model: Model - }; - let httpServer, server, modelMock, instanceMock, queryMock; - - beforeEach(async function () { - server = odata(); - init(server); - - }); - - afterEach(() => { - if (httpServer) { - httpServer.close(); - } - modelMock?.restore(); - instanceMock?.restore(); - queryMock?.restore(); - }); - - it('should returns a transient singleton with wrong client', async function () { - const entity = server.mongoSingleton('client', Model); - - entity.clientField = 'client'; - - modelMock = sinon.mock(Model); - modelMock.expects('findOne').once() - .withArgs({ - client: 99 - }) - .returns(query); - queryMock = sinon.mock(query); - queryMock.expects('exec').once() - .returns(new Promise(resolve => resolve())); - instanceMock = sinon.mock(Model.prototype); - instanceMock.expects('toObject').once() - .returns({}); - httpServer = server.listen(port); - - const res = await request(host).get(`/client?sap-client=099`); - - res.body.should.deepEqual({ - id: null, - client: 99 - }); - - modelMock.verify(); - queryMock.verify(); - instanceMock.verify(); - }); - - it('should returns a singleton with client', async function () { - const entity = server.mongoSingleton('client', Model); - - entity.clientField = 'client'; - - modelMock = sinon.mock(Model); - modelMock.expects('findOne').once() - .withArgs({ - client: 99 - }) - .returns(query); - queryMock = sinon.mock(query); - queryMock.expects('exec').once() - .returns(new Promise(resolve => resolve({ - toObject: () => ({ - id: '1', - client: 99 - }) - }))); - httpServer = server.listen(port); - - const res = await request(host).get(`/client?sap-client=099`); - - res.body.should.deepEqual({ - id: '1', - client: 99 - }); - - modelMock.verify(); - queryMock.verify(); - instanceMock.verify(); - }); - - it('should fail for client collection without client', async function () { - const entity = server.mongoEntity('client', Model); - - entity.clientField = 'client'; - - modelMock = sinon.mock(Model); - modelMock.expects('findById').never(); - httpServer = server.listen(port); - - const res = await request(host).get(`/client('1')`); - - res.body.should.deepEqual({ - error: { - code: '400', - message: `For entity 'client' you must send a client value` - } - }); - - modelMock.verify(); - }); - - it('should apply client to the key', async function () { - const entity = server.mongoEntity('client', Model); - - entity.clientField = 'client'; - - modelMock = sinon.mock(Model); - modelMock.expects('findById').once() - .withArgs('1') - .returns(new Promise(resolve => resolve({ - toObject: () => ({ - _id: 1, - client: 99 - }) - }))); - httpServer = server.listen(port); - - const res = await request(host).get(`/client('1')?sap-client=099`); - - assertSuccess(res); - - modelMock.verify(); - }); - - it('should fail with correct key and wrong client', async function () { - const entity = server.mongoEntity('client', Model); - - entity.clientField = 'client'; - - modelMock = sinon.mock(Model); - modelMock.expects('findById').once() - .withArgs('1') - .returns(new Promise(resolve => resolve({ - toObject: () => ({ - _id: 1, - client: 98 - }) - }))); - httpServer = server.listen(port); - - const res = await request(host).get(`/client('1')?sap-client=099`); - - res.status.should.be.equal(404); - - modelMock.verify(); - }); - - it('should apply client to the count', async function () { - const entity = server.mongoEntity('client', Model); - - entity.clientField = 'client'; - - modelMock = sinon.mock(Model); - modelMock.expects('find').once() - .withArgs({ - client: 99 - }).returns(query); - httpServer = server.listen(port); - - const res = await request(host).get(`/client/$count?sap-client=099`); - - assertSuccess(res); - - modelMock.verify(); - }); - - it('should fail on delete with wrong client', async function () { - const entity = server.mongoEntity('client', Model); - - entity.clientField = 'client'; - - modelMock = sinon.mock(Model); - modelMock.expects('findById').once() - .withArgs('1') - .returns(new Promise(resolve => resolve({ - toObject: () => ({ - _id: 1, - client: 98 - }) - }))); - httpServer = server.listen(port); - - const res = await request(host).delete(`/client('1')?sap-client=099`); - - res.status.should.be.equal(404); - - modelMock.verify(); - }); - - it('should work on delete with client', async function () { - const entity = server.mongoEntity('client', Model); - - entity.clientField = 'client'; - - const instance = { - client: 99, - deleteOne: async () => {} - }; - modelMock = sinon.mock(Model); - modelMock.expects('findById').once() - .withArgs('1') - .returns(new Promise(resolve => resolve(instance))); - - instanceMock = sinon.mock(instance); - instanceMock.expects('deleteOne').once(); - httpServer = server.listen(port); - - const res = await request(host).delete(`/client('1')?sap-client=099`); - - res.status.should.be.equal(204); - - modelMock.verify(); - instanceMock.verify(); - }); - -}); From b80169b1b9127a9be73abcca49a424153ab54e18 Mon Sep 17 00:00:00 2001 From: Richard Martens Date: Sun, 5 Nov 2023 21:59:36 +0100 Subject: [PATCH 61/64] client support for list request --- .vscode/launch.json | 2 +- Makefile | 1 + src/mongo/parser/filterParser.js | 13 ++++- src/mongo/rest/list.js | 4 +- test/mongo/mocked/client/base.js | 48 +++++++++------- test/mongo/mocked/client/count.js | 44 ++++++++------- test/mongo/mocked/client/delete.js | 50 +++++++++-------- test/mongo/mocked/client/list.js | 81 ++++++++++++++++++++------- test/mongo/mocked/client/singleton.js | 55 ++++++++++-------- 9 files changed, 189 insertions(+), 109 deletions(-) diff --git a/.vscode/launch.json b/.vscode/launch.json index 90d3f72..a258dbc 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -20,7 +20,7 @@ "dot", "--timeout", "300000", - "test/mongo/mocked/odata.client.js" + "test/mongo/mocked/client/list.js" ], "env": { "LOG_LEVEL": "debug" diff --git a/Makefile b/Makefile index 8e6c332..841a558 100644 --- a/Makefile +++ b/Makefile @@ -16,6 +16,7 @@ test: --reporter $(REPORTER) \ --exclude test/failing/*.js \ --exclude test/support/*.js \ + test/**/**/**/*.js \ test/**/**/*.js \ test/**/*.js \ test/*.js diff --git a/src/mongo/parser/filterParser.js b/src/mongo/parser/filterParser.js index cd7935a..253a6b2 100644 --- a/src/mongo/parser/filterParser.js +++ b/src/mongo/parser/filterParser.js @@ -58,4 +58,15 @@ function parse($filter) { }; -export default parse; \ No newline at end of file +export default ($filter, $odata) => { + let result = parse($filter); + + debugger; + if ($odata.clientField) { + const clientCondition = { [$odata.clientField]: { $eq: $odata.client } }; + + result = result ? { $and: [ clientCondition, result ] } : clientCondition + } + + return result; +}; \ No newline at end of file diff --git a/src/mongo/rest/list.js b/src/mongo/rest/list.js index 77be715..f9ca719 100644 --- a/src/mongo/rest/list.js +++ b/src/mongo/rest/list.js @@ -8,7 +8,9 @@ export default async (req, res, next) => { try { // TODO expand: req.$odata.$expand, // TODO search: req.$odata.$search, - const query = req.$odata.Model.find(filterParser(req.$odata.$filter)); + const filter = filterParser(req.$odata.$filter, req.$odata); + debugger; + const query = req.$odata.Model.find(filter); if (req.$odata.$orderby) { query.sort(req.$odata.$orderby); diff --git a/test/mongo/mocked/client/base.js b/test/mongo/mocked/client/base.js index 092fec9..732dac2 100644 --- a/test/mongo/mocked/client/base.js +++ b/test/mongo/mocked/client/base.js @@ -5,25 +5,31 @@ import { odata, host, port, assertSuccess } from '../../../support/setup'; import mongoose from 'mongoose'; import { init } from '../../../support/db'; -const Schema = mongoose.Schema; -const ModelSchema = new Schema({ - client: Number -}); +describe('mongo.mocked.client.base', () => { + let httpServer, server, modelMock, instanceMock, queryMock, query, Model; -const Model = mongoose.model('client', ModelSchema); - -describe('mongo.mocked.odata.client', () => { - const query = { - $where: () => { }, - where: () => { }, - equals: () => { }, - gte: () => { }, - lt: () => { }, - exec: () => { }, - count: () => new Promise((resolve) => resolve(1)), - model: Model - }; - let httpServer, server, modelMock, instanceMock, queryMock; + before(() => { + const Schema = mongoose.Schema; + const ModelSchema = new Schema({ + client: Number + }); + + mongoose.set('overwriteModels', true); + + + Model = mongoose.model('client', ModelSchema); + + query = { + $where: () => { }, + where: () => { }, + equals: () => { }, + gte: () => { }, + lt: () => { }, + exec: () => { }, + count: () => new Promise((resolve) => resolve(1)), + model: Model + }; + }); beforeEach(async function () { server = odata(); @@ -51,6 +57,7 @@ describe('mongo.mocked.odata.client', () => { const res = await request(host).get(`/client('1')`); + modelMock.verify(); res.body.should.deepEqual({ error: { code: '400', @@ -58,7 +65,6 @@ describe('mongo.mocked.odata.client', () => { } }); - modelMock.verify(); }); it('should apply client to the key', async function () { @@ -79,9 +85,9 @@ describe('mongo.mocked.odata.client', () => { const res = await request(host).get(`/client('1')?sap-client=099`); + modelMock.verify(); assertSuccess(res); - modelMock.verify(); }); it('should fail with correct key and wrong client', async function () { @@ -102,9 +108,9 @@ describe('mongo.mocked.odata.client', () => { const res = await request(host).get(`/client('1')?sap-client=099`); + modelMock.verify(); res.status.should.be.equal(404); - modelMock.verify(); }); }); diff --git a/test/mongo/mocked/client/count.js b/test/mongo/mocked/client/count.js index b49a930..5b4e27c 100644 --- a/test/mongo/mocked/client/count.js +++ b/test/mongo/mocked/client/count.js @@ -5,25 +5,31 @@ import { odata, host, port, assertSuccess } from '../../../support/setup'; import mongoose from 'mongoose'; import { init } from '../../../support/db'; -const Schema = mongoose.Schema; -const ModelSchema = new Schema({ - client: Number -}); +describe('mongo.mocked.client.count', () => { + let httpServer, server, modelMock, instanceMock, queryMock, query, Model; + + before(() => { + const Schema = mongoose.Schema; + const ModelSchema = new Schema({ + client: Number + }); + + mongoose.set('overwriteModels', true); -const Model = mongoose.model('client', ModelSchema); - -describe('mongo.mocked.odata.client', () => { - const query = { - $where: () => { }, - where: () => { }, - equals: () => { }, - gte: () => { }, - lt: () => { }, - exec: () => { }, - count: () => new Promise((resolve) => resolve(1)), - model: Model - }; - let httpServer, server, modelMock, instanceMock, queryMock; + + Model = mongoose.model('client', ModelSchema); + + query = { + $where: () => { }, + where: () => { }, + equals: () => { }, + gte: () => { }, + lt: () => { }, + exec: () => { }, + count: () => new Promise((resolve) => resolve(1)), + model: Model + }; + }); beforeEach(async function () { server = odata(); @@ -54,9 +60,9 @@ describe('mongo.mocked.odata.client', () => { const res = await request(host).get(`/client/$count?sap-client=099`); + modelMock.verify(); assertSuccess(res); - modelMock.verify(); }); }); diff --git a/test/mongo/mocked/client/delete.js b/test/mongo/mocked/client/delete.js index 73fbc6a..f1bd689 100644 --- a/test/mongo/mocked/client/delete.js +++ b/test/mongo/mocked/client/delete.js @@ -5,25 +5,31 @@ import { odata, host, port, assertSuccess } from '../../../support/setup'; import mongoose from 'mongoose'; import { init } from '../../../support/db'; -const Schema = mongoose.Schema; -const ModelSchema = new Schema({ - client: Number -}); - -const Model = mongoose.model('client', ModelSchema); - -describe('mongo.mocked.odata.client', () => { - const query = { - $where: () => { }, - where: () => { }, - equals: () => { }, - gte: () => { }, - lt: () => { }, - exec: () => { }, - count: () => new Promise((resolve) => resolve(1)), - model: Model - }; - let httpServer, server, modelMock, instanceMock, queryMock; +describe('mongo.mocked.client.delete', () => { + let httpServer, server, modelMock, instanceMock, queryMock, query, Model; + + before(() => { + const Schema = mongoose.Schema; + const ModelSchema = new Schema({ + client: Number + }); + + mongoose.set('overwriteModels', true); + + + Model = mongoose.model('client', ModelSchema); + + query = { + $where: () => { }, + where: () => { }, + equals: () => { }, + gte: () => { }, + lt: () => { }, + exec: () => { }, + count: () => new Promise((resolve) => resolve(1)), + model: Model + }; + }); beforeEach(async function () { server = odata(); @@ -58,9 +64,9 @@ describe('mongo.mocked.odata.client', () => { const res = await request(host).delete(`/client('1')?sap-client=099`); + modelMock.verify(); res.status.should.be.equal(404); - modelMock.verify(); }); it('should work on delete with client', async function () { @@ -83,10 +89,10 @@ describe('mongo.mocked.odata.client', () => { const res = await request(host).delete(`/client('1')?sap-client=099`); - res.status.should.be.equal(204); - modelMock.verify(); instanceMock.verify(); + res.status.should.be.equal(204); + }); }); diff --git a/test/mongo/mocked/client/list.js b/test/mongo/mocked/client/list.js index 70a4b12..54ed018 100644 --- a/test/mongo/mocked/client/list.js +++ b/test/mongo/mocked/client/list.js @@ -5,25 +5,31 @@ import { odata, host, port, assertSuccess } from '../../../support/setup'; import mongoose from 'mongoose'; import { init } from '../../../support/db'; -const Schema = mongoose.Schema; -const ModelSchema = new Schema({ - client: Number -}); +describe('mongo.mocked.client.list', () => { + let httpServer, server, modelMock, instanceMock, queryMock, query, Model; + + before(() => { + const Schema = mongoose.Schema; + const ModelSchema = new Schema({ + client: Number + }); + + mongoose.set('overwriteModels', true); + -const Model = mongoose.model('client', ModelSchema); - -describe('mongo.mocked.odata.client', () => { - const query = { - $where: () => { }, - where: () => { }, - equals: () => { }, - gte: () => { }, - lt: () => { }, - exec: () => { }, - count: () => new Promise((resolve) => resolve(1)), - model: Model - }; - let httpServer, server, modelMock, instanceMock, queryMock; + Model = mongoose.model('client', ModelSchema); + + query = { + $where: () => { }, + where: () => { }, + equals: () => { }, + gte: () => { }, + lt: () => { }, + exec: () => { }, + count: () => new Promise((resolve) => resolve(1)), + model: Model + }; + }); beforeEach(async function () { server = odata(); @@ -40,6 +46,37 @@ describe('mongo.mocked.odata.client', () => { queryMock?.restore(); }); + it('should extend filter with client', async function () { + const entity = server.mongoEntity('client', Model); + + entity.clientField = 'client'; + + modelMock = sinon.mock(Model); + modelMock.expects('find').once() + .withArgs({ + $and: [{ + client: { + $eq: 99 + } + }, { + client: { + $eq: 88 + } + }] + }).returns(query); + queryMock = sinon.mock(query); + queryMock.expects('exec').once() + .returns(new Promise(resolve => resolve([]))); + httpServer = server.listen(port); + + const res = await request(host).get(`/client?$filter=client eq 88&sap-client=099`); + + modelMock.verify(); + + assertSuccess(res); + + }); + it('should creates filter with client', async function () { const entity = server.mongoEntity('client', Model); @@ -49,16 +86,20 @@ describe('mongo.mocked.odata.client', () => { modelMock.expects('find').once() .withArgs({ client: { - eq: 99 + $eq: 99 } }).returns(query); + queryMock = sinon.mock(query); + queryMock.expects('exec').once() + .returns(new Promise(resolve => resolve([]))); httpServer = server.listen(port); const res = await request(host).get(`/client?sap-client=099`); + modelMock.verify(); + assertSuccess(res); - modelMock.verify(); }); }); diff --git a/test/mongo/mocked/client/singleton.js b/test/mongo/mocked/client/singleton.js index 41635a4..2e7e53b 100644 --- a/test/mongo/mocked/client/singleton.js +++ b/test/mongo/mocked/client/singleton.js @@ -5,25 +5,32 @@ import { odata, host, port, assertSuccess } from '../../../support/setup'; import mongoose from 'mongoose'; import { init } from '../../../support/db'; -const Schema = mongoose.Schema; -const ModelSchema = new Schema({ - client: Number -}); -const Model = mongoose.model('client', ModelSchema); - -describe('mongo.mocked.odata.client', () => { - const query = { - $where: () => { }, - where: () => { }, - equals: () => { }, - gte: () => { }, - lt: () => { }, - exec: () => { }, - count: () => new Promise((resolve) => resolve(1)), - model: Model - }; - let httpServer, server, modelMock, instanceMock, queryMock; +describe('mongo.mocked.client.singleton', () => { + let httpServer, server, modelMock, instanceMock, queryMock, query, Model; + + before(() => { + const Schema = mongoose.Schema; + const ModelSchema = new Schema({ + client: Number + }); + + mongoose.set('overwriteModels', true); + + + Model = mongoose.model('client', ModelSchema); + + query = { + $where: () => { }, + where: () => { }, + equals: () => { }, + gte: () => { }, + lt: () => { }, + exec: () => { }, + count: () => new Promise((resolve) => resolve(1)), + model: Model + }; + }); beforeEach(async function () { server = odata(); @@ -61,14 +68,14 @@ describe('mongo.mocked.odata.client', () => { const res = await request(host).get(`/client?sap-client=099`); + modelMock.verify(); + queryMock.verify(); + instanceMock.verify(); res.body.should.deepEqual({ id: null, client: 99 }); - modelMock.verify(); - queryMock.verify(); - instanceMock.verify(); }); it('should returns a singleton with client', async function () { @@ -94,13 +101,13 @@ describe('mongo.mocked.odata.client', () => { const res = await request(host).get(`/client?sap-client=099`); + modelMock.verify(); + queryMock.verify(); + instanceMock.verify(); res.body.should.deepEqual({ id: '1', client: 99 }); - modelMock.verify(); - queryMock.verify(); - instanceMock.verify(); }); }); From da61b3d034fe7a8c7575eb5b02baf59b1c1119fd Mon Sep 17 00:00:00 2001 From: Richard Martens Date: Mon, 6 Nov 2023 21:48:07 +0100 Subject: [PATCH 62/64] client support for patch entity --- .vscode/launch.json | 2 +- src/mongo/parser/filterParser.js | 1 - src/mongo/rest/getSingleton.js | 1 - src/mongo/rest/list.js | 1 - src/mongo/rest/patch.js | 24 ++- test/mongo/mocked/client/base.js | 46 ----- test/mongo/mocked/client/get.js | 96 +++++++++ .../client/{singleton.js => getSingleton.js} | 2 +- test/mongo/mocked/client/patch.js | 195 ++++++++++++++++++ 9 files changed, 314 insertions(+), 54 deletions(-) create mode 100644 test/mongo/mocked/client/get.js rename test/mongo/mocked/client/{singleton.js => getSingleton.js} (98%) create mode 100644 test/mongo/mocked/client/patch.js diff --git a/.vscode/launch.json b/.vscode/launch.json index a258dbc..d5cad93 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -20,7 +20,7 @@ "dot", "--timeout", "300000", - "test/mongo/mocked/client/list.js" + "test/mongo/mocked/client/patch.js" ], "env": { "LOG_LEVEL": "debug" diff --git a/src/mongo/parser/filterParser.js b/src/mongo/parser/filterParser.js index 253a6b2..fcf4456 100644 --- a/src/mongo/parser/filterParser.js +++ b/src/mongo/parser/filterParser.js @@ -61,7 +61,6 @@ function parse($filter) { export default ($filter, $odata) => { let result = parse($filter); - debugger; if ($odata.clientField) { const clientCondition = { [$odata.clientField]: { $eq: $odata.client } }; diff --git a/src/mongo/rest/getSingleton.js b/src/mongo/rest/getSingleton.js index 9650dd7..19c0511 100644 --- a/src/mongo/rest/getSingleton.js +++ b/src/mongo/rest/getSingleton.js @@ -2,7 +2,6 @@ import selectParser from "../parser/selectParser"; export default async (req, res, next) => { try { - debugger; const param = req.$odata.clientField ? { [req.$odata.clientField]: req.$odata.client } : undefined; const query = req.$odata.Model.findOne(param); diff --git a/src/mongo/rest/list.js b/src/mongo/rest/list.js index f9ca719..8dd4291 100644 --- a/src/mongo/rest/list.js +++ b/src/mongo/rest/list.js @@ -9,7 +9,6 @@ export default async (req, res, next) => { // TODO expand: req.$odata.$expand, // TODO search: req.$odata.$search, const filter = filterParser(req.$odata.$filter, req.$odata); - debugger; const query = req.$odata.Model.find(filter); if (req.$odata.$orderby) { diff --git a/src/mongo/rest/patch.js b/src/mongo/rest/patch.js index 1064df1..9d0f9e2 100644 --- a/src/mongo/rest/patch.js +++ b/src/mongo/rest/patch.js @@ -1,9 +1,27 @@ export default async (req, res, next) => { try { - const entity = await req.$odata.Model.findOne({ id: req.$odata.$Key.id }); - const patched = { ...entity.toObject(), ...req.$odata.body }; + const entity = (await req.$odata.Model.findById(req.$odata.$Key._id)).toObject(); - await req.$odata.Model.updateOne({ _id: req.$odata.$Key.id }, patched); + if (req.$odata.clientField) { + if (entity[req.$odata.clientField] !== req.$odata.client) { + const error1 = new Error('Not found'); + + error1.status = 404; + throw error1; + } + + const bodyClient = req.$odata.body[req.$odata.clientField]; + if (bodyClient && bodyClient !== req.$odata.client) { + const error2 = new Error('Client value in custom parameter differs from client value in body'); + + error2.status = 400; + throw error2; + } + } + + const patched = { ...entity, ...req.$odata.body }; + + await req.$odata.Model.updateOne({ _id: req.$odata.$Key._id }, patched); res.$odata.result = patched; next(); diff --git a/test/mongo/mocked/client/base.js b/test/mongo/mocked/client/base.js index 732dac2..834face 100644 --- a/test/mongo/mocked/client/base.js +++ b/test/mongo/mocked/client/base.js @@ -67,50 +67,4 @@ describe('mongo.mocked.client.base', () => { }); - it('should apply client to the key', async function () { - const entity = server.mongoEntity('client', Model); - - entity.clientField = 'client'; - - modelMock = sinon.mock(Model); - modelMock.expects('findById').once() - .withArgs('1') - .returns(new Promise(resolve => resolve({ - toObject: () => ({ - _id: 1, - client: 99 - }) - }))); - httpServer = server.listen(port); - - const res = await request(host).get(`/client('1')?sap-client=099`); - - modelMock.verify(); - assertSuccess(res); - - }); - - it('should fail with correct key and wrong client', async function () { - const entity = server.mongoEntity('client', Model); - - entity.clientField = 'client'; - - modelMock = sinon.mock(Model); - modelMock.expects('findById').once() - .withArgs('1') - .returns(new Promise(resolve => resolve({ - toObject: () => ({ - _id: 1, - client: 98 - }) - }))); - httpServer = server.listen(port); - - const res = await request(host).get(`/client('1')?sap-client=099`); - - modelMock.verify(); - res.status.should.be.equal(404); - - }); - }); diff --git a/test/mongo/mocked/client/get.js b/test/mongo/mocked/client/get.js new file mode 100644 index 0000000..1825c3d --- /dev/null +++ b/test/mongo/mocked/client/get.js @@ -0,0 +1,96 @@ +import 'should'; +import request from 'supertest'; +import sinon from 'sinon'; +import { odata, host, port, assertSuccess } from '../../../support/setup'; +import mongoose from 'mongoose'; +import { init } from '../../../support/db'; + +describe('mongo.mocked.client.get', () => { + let httpServer, server, modelMock, instanceMock, queryMock, query, Model; + + before(() => { + const Schema = mongoose.Schema; + const ModelSchema = new Schema({ + client: Number + }); + + mongoose.set('overwriteModels', true); + + + Model = mongoose.model('client', ModelSchema); + + query = { + $where: () => { }, + where: () => { }, + equals: () => { }, + gte: () => { }, + lt: () => { }, + exec: () => { }, + count: () => new Promise((resolve) => resolve(1)), + model: Model + }; + }); + + beforeEach(async function () { + server = odata(); + init(server); + + }); + + afterEach(() => { + if (httpServer) { + httpServer.close(); + } + modelMock?.restore(); + instanceMock?.restore(); + queryMock?.restore(); + }); + + + it('should apply client to the key', async function () { + const entity = server.mongoEntity('client', Model); + + entity.clientField = 'client'; + + modelMock = sinon.mock(Model); + modelMock.expects('findById').once() + .withArgs('1') + .returns(new Promise(resolve => resolve({ + toObject: () => ({ + _id: 1, + client: 99 + }) + }))); + httpServer = server.listen(port); + + const res = await request(host).get(`/client('1')?sap-client=099`); + + modelMock.verify(); + assertSuccess(res); + + }); + + it('should fail with correct key and wrong client', async function () { + const entity = server.mongoEntity('client', Model); + + entity.clientField = 'client'; + + modelMock = sinon.mock(Model); + modelMock.expects('findById').once() + .withArgs('1') + .returns(new Promise(resolve => resolve({ + toObject: () => ({ + _id: 1, + client: 98 + }) + }))); + httpServer = server.listen(port); + + const res = await request(host).get(`/client('1')?sap-client=099`); + + modelMock.verify(); + res.status.should.be.equal(404); + + }); + +}); diff --git a/test/mongo/mocked/client/singleton.js b/test/mongo/mocked/client/getSingleton.js similarity index 98% rename from test/mongo/mocked/client/singleton.js rename to test/mongo/mocked/client/getSingleton.js index 2e7e53b..a5cb069 100644 --- a/test/mongo/mocked/client/singleton.js +++ b/test/mongo/mocked/client/getSingleton.js @@ -6,7 +6,7 @@ import mongoose from 'mongoose'; import { init } from '../../../support/db'; -describe('mongo.mocked.client.singleton', () => { +describe('mongo.mocked.client.getSingleton', () => { let httpServer, server, modelMock, instanceMock, queryMock, query, Model; before(() => { diff --git a/test/mongo/mocked/client/patch.js b/test/mongo/mocked/client/patch.js new file mode 100644 index 0000000..418f775 --- /dev/null +++ b/test/mongo/mocked/client/patch.js @@ -0,0 +1,195 @@ +import 'should'; +import request from 'supertest'; +import sinon from 'sinon'; +import { odata, host, port, assertSuccess } from '../../../support/setup'; +import mongoose from 'mongoose'; +import { init } from '../../../support/db'; + +describe('mongo.mocked.client.patch', () => { + let httpServer, server, modelMock, instanceMock, queryMock, query, Model; + + before(() => { + const Schema = mongoose.Schema; + const ModelSchema = new Schema({ + client: Number, + text: String + }); + + mongoose.set('overwriteModels', true); + + + Model = mongoose.model('client', ModelSchema); + + query = { + $where: () => { }, + where: () => { }, + equals: () => { }, + gte: () => { }, + lt: () => { }, + exec: () => { }, + count: () => new Promise((resolve) => resolve(1)), + model: Model + }; + }); + + beforeEach(async function () { + server = odata(); + init(server); + + }); + + afterEach(() => { + if (httpServer) { + httpServer.close(); + } + modelMock?.restore(); + instanceMock?.restore(); + queryMock?.restore(); + }); + + it('should wrong if client is in body too', async function () { + const entity = server.mongoEntity('client', Model); + + entity.clientField = 'client'; + + modelMock = sinon.mock(Model); + modelMock.expects('findById').once() + .withArgs('1') + .returns(new Promise(resolve => resolve({ + toObject: () => ({ + _id: '1', + client: 99, + text: 'original' + }) + }))); + modelMock.expects('updateOne').once() + .withArgs({ + _id: '1' + }, { + _id: '1', + client: 99, + text: 'patched' + }).returns(new Promise(resolve => resolve())); + httpServer = server.listen(port); + + const res = (await request(host).patch(`/client('1')?sap-client=099`) + .send({ + client: 99, + text: 'patched' + })); + + modelMock.verify(); + assertSuccess(res); + + res.body.should.deepEqual({ + id: '1', + client: 99, + text: 'patched' + }); + + }); + + it('should fail with wrong client in body', async function () { + const entity = server.mongoEntity('client', Model); + + entity.clientField = 'client'; + + modelMock = sinon.mock(Model); + modelMock.expects('findById').once() + .withArgs('1') + .returns(new Promise(resolve => resolve({ + toObject: () => ({ + _id: '1', + client: 99, + text: 'original' + }) + }))); + modelMock.expects('updateOne').never(); + httpServer = server.listen(port); + + const res = (await request(host).patch(`/client('1')?sap-client=099`) + .send({ + client: 98, + text: 'patched' + })); + + modelMock.verify(); + res.body.should.deepEqual({ + error: { + message: 'Client value in custom parameter differs from client value in body', + code: '400' + } + }); + + }); + + it('should fail with correct key and wrong client', async function () { + const entity = server.mongoEntity('client', Model); + + entity.clientField = 'client'; + + modelMock = sinon.mock(Model); + modelMock.expects('findById').once() + .withArgs('1') + .returns(new Promise(resolve => resolve({ + toObject: () => ({ + _id: '1', + client: 99, + text: 'original' + }) + }))); + modelMock.expects('updateOne').never(); + httpServer = server.listen(port); + + const res = (await request(host).patch(`/client('1')?sap-client=098`) + .send({ + text: 'patched' + })); + + modelMock.verify(); + res.status.should.be.equal(404); + + }); + + it('should apply client to the key', async function () { + const entity = server.mongoEntity('client', Model); + + entity.clientField = 'client'; + + modelMock = sinon.mock(Model); + modelMock.expects('findById').once() + .withArgs('1') + .returns(new Promise(resolve => resolve({ + toObject: () => ({ + _id: '1', + client: 99, + text: 'original' + }) + }))); + modelMock.expects('updateOne').once() + .withArgs({ + _id: '1' + }, { + _id: '1', + client: 99, + text: 'patched' + }).returns(new Promise(resolve => resolve())); + httpServer = server.listen(port); + + const res = (await request(host).patch(`/client('1')?sap-client=099`) + .send({ + text: 'patched' + })); + + modelMock.verify(); + assertSuccess(res); + + res.body.should.deepEqual({ + id: '1', + client: 99, + text: 'patched' + }); + + }); + +}); From 7407dec1102d311a12d68c21b2a3583ab978c63f Mon Sep 17 00:00:00 2001 From: Richard Martens Date: Sun, 12 Nov 2023 21:21:14 +0100 Subject: [PATCH 63/64] client support implemented for patch, post and put --- src/mongo/applyClient.js | 7 + src/mongo/rest/patch.js | 4 + src/mongo/rest/patchSingleton.js | 21 +- src/mongo/rest/post.js | 15 ++ src/mongo/rest/put.js | 27 ++- test/mongo/mocked/client/patch.js | 2 +- test/mongo/mocked/client/patchSingleton.js | 173 +++++++++++++++++ test/mongo/mocked/client/post.js | 111 +++++++++++ test/mongo/mocked/client/put.js | 215 +++++++++++++++++++++ test/mongo/mocked/rest.put.js | 7 +- 10 files changed, 575 insertions(+), 7 deletions(-) create mode 100644 src/mongo/applyClient.js create mode 100644 test/mongo/mocked/client/patchSingleton.js create mode 100644 test/mongo/mocked/client/post.js create mode 100644 test/mongo/mocked/client/put.js diff --git a/src/mongo/applyClient.js b/src/mongo/applyClient.js new file mode 100644 index 0000000..5901786 --- /dev/null +++ b/src/mongo/applyClient.js @@ -0,0 +1,7 @@ +export default function applyClient(req, entity) { + if (!req.$odata.clientField) { + return; + } + + entity[req.$odata.clientField] = req.$odata.client; +} \ No newline at end of file diff --git a/src/mongo/rest/patch.js b/src/mongo/rest/patch.js index 9d0f9e2..2545236 100644 --- a/src/mongo/rest/patch.js +++ b/src/mongo/rest/patch.js @@ -1,3 +1,5 @@ +import applyClient from "../applyClient"; + export default async (req, res, next) => { try { const entity = (await req.$odata.Model.findById(req.$odata.$Key._id)).toObject(); @@ -21,6 +23,8 @@ export default async (req, res, next) => { const patched = { ...entity, ...req.$odata.body }; + applyClient(req, patched); + await req.$odata.Model.updateOne({ _id: req.$odata.$Key._id }, patched); res.$odata.result = patched; next(); diff --git a/src/mongo/rest/patchSingleton.js b/src/mongo/rest/patchSingleton.js index e462835..39e4a62 100644 --- a/src/mongo/rest/patchSingleton.js +++ b/src/mongo/rest/patchSingleton.js @@ -1,18 +1,37 @@ +import applyClient from "../applyClient"; + export default async (req, res, next) => { try { - let entity = await req.$odata.Model.findOne(); + const filter = req.$odata.clientField ? { [req.$odata.clientField]: req.$odata.client } : undefined; + let entity = await req.$odata.Model.findOne(filter); + + if (req.$odata.clientField) { + const bodyClient = req.$odata.body[req.$odata.clientField]; + + if (bodyClient && bodyClient !== req.$odata.client) { + const error = new Error('Client value in custom parameter differs from client value in body'); + + error.status = 400; + throw error; + } + } if (entity) { const patched = { ...entity.toObject(), ...req.$odata.body }; + + applyClient(req, patched); await req.$odata.Model.updateOne({ _id: entity._id }, patched); res.$odata.result = patched; } else { entity = new req.$odata.Model(); + Object.keys(req.$odata.body).forEach(property => entity[property] = req.$odata.body[property] ); + applyClient(req, entity); + await entity.save({ validateBeforeSave: true, validateModifiedOnly: true diff --git a/src/mongo/rest/post.js b/src/mongo/rest/post.js index 89f6435..133e02b 100644 --- a/src/mongo/rest/post.js +++ b/src/mongo/rest/post.js @@ -1,3 +1,5 @@ +import applyClient from "../applyClient"; + export default async (req, res, next) => { try { if (!Object.keys(req.body).length) { @@ -6,10 +8,23 @@ export default async (req, res, next) => { error.status = 422; throw error; + } + if (req.$odata.clientField) { + const bodyField = req.body[req.$odata.clientField]; + + if (bodyField && bodyField !== req.$odata.client) { + const error = new Error('Client value in custom parameter differs from client value in body'); + + error.status = 400; + throw error; + } + } const entity = new req.$odata.Model(req.body); + applyClient(req, entity); + await entity.save({ validateBeforeSave: true, validateModifiedOnly: true diff --git a/src/mongo/rest/put.js b/src/mongo/rest/put.js index e35deec..9950fb0 100644 --- a/src/mongo/rest/put.js +++ b/src/mongo/rest/put.js @@ -1,13 +1,34 @@ +import applyClient from "../applyClient"; + export default async (req, res, next) => { try { const entity = await req.$odata.Model.findOne({ _id: req.$odata.$Key._id }); - if (entity) { - await req.$odata.Model.findByIdAndUpdate(entity.id, req.$odata.body); + if (req.$odata.clientField) { + if (entity && entity.client !== req.$odata.client) { + const error1 = new Error('Not found'); + + error1.status = 404; + throw error1; + } + + const bodyClient = req.$odata.body[req.$odata.clientField]; + + if (bodyClient && bodyClient !== req.$odata.client) { + const error2 = new Error('Client value in custom parameter differs from client value in body'); + error2.status = 400; + throw error2; + } + } + + if (entity) { const newEntity = req.$odata.body; - newEntity.id = entity.id; + applyClient(req, newEntity); + await req.$odata.Model.findByIdAndUpdate(entity._id, req.$odata.body); + + newEntity._id = entity._id; res.$odata.result = newEntity; } else { diff --git a/test/mongo/mocked/client/patch.js b/test/mongo/mocked/client/patch.js index 418f775..c28bff6 100644 --- a/test/mongo/mocked/client/patch.js +++ b/test/mongo/mocked/client/patch.js @@ -47,7 +47,7 @@ describe('mongo.mocked.client.patch', () => { queryMock?.restore(); }); - it('should wrong if client is in body too', async function () { + it('should work if client is in body too', async function () { const entity = server.mongoEntity('client', Model); entity.clientField = 'client'; diff --git a/test/mongo/mocked/client/patchSingleton.js b/test/mongo/mocked/client/patchSingleton.js new file mode 100644 index 0000000..81bf879 --- /dev/null +++ b/test/mongo/mocked/client/patchSingleton.js @@ -0,0 +1,173 @@ +import 'should'; +import request from 'supertest'; +import sinon from 'sinon'; +import { odata, host, port, assertSuccess } from '../../../support/setup'; +import mongoose from 'mongoose'; +import { init } from '../../../support/db'; + +describe('mongo.mocked.client.patchSingleton', () => { + let httpServer, server, modelMock, instanceMock, queryMock, query, Model; + + before(() => { + const Schema = mongoose.Schema; + const ModelSchema = new Schema({ + client: Number, + text: String + }); + + mongoose.set('overwriteModels', true); + + + Model = mongoose.model('client', ModelSchema); + + query = { + $where: () => { }, + where: () => { }, + equals: () => { }, + gte: () => { }, + lt: () => { }, + exec: () => { }, + count: () => new Promise((resolve) => resolve(1)), + model: Model + }; + }); + + beforeEach(async function () { + server = odata(); + init(server); + + }); + + afterEach(() => { + if (httpServer) { + httpServer.close(); + } + modelMock?.restore(); + instanceMock?.restore(); + queryMock?.restore(); + }); + + it('should work for upsert', async function () { + const entity = server.mongoSingleton('client', Model); + + entity.clientField = 'client'; + + modelMock = sinon.mock(Model); + modelMock.expects('findOne').once() + .withArgs({ + client: 99 + }) + .returns(new Promise(resolve => resolve())); + instanceMock = sinon.mock(Model.prototype); + instanceMock.expects('save').once() + .withArgs({ + validateBeforeSave: true, + validateModifiedOnly: true + }).returns(new Promise(resolve => resolve())); + instanceMock.expects('toObject').once() + .returns({ + _id: '1', + client: 99, + text: 'patched' + }); + httpServer = server.listen(port); + + const res = (await request(host).patch(`/client?sap-client=099`) + .send({ + text: 'patched' + })); + + modelMock.verify(); + instanceMock.verify(); + assertSuccess(res); + + res.body.should.deepEqual({ + id: '1', + client: 99, + text: 'patched' + }); + + }); + + it('should fail with wrong client in body', async function () { + const entity = server.mongoSingleton('client', Model); + + entity.clientField = 'client'; + + modelMock = sinon.mock(Model); + modelMock.expects('findOne').once() + .withArgs({ + client: 99 + }) + .returns(new Promise(resolve => resolve({ + _id: '1', + toObject: () => ({ + _id: '1', + client: 99, + text: 'original' + }) + }))); + modelMock.expects('updateOne').never(); + httpServer = server.listen(port); + + const res = (await request(host).patch(`/client?sap-client=099`) + .send({ + client: 98, + text: 'patched' + })); + + modelMock.verify(); + res.body.should.deepEqual({ + error: { + message: 'Client value in custom parameter differs from client value in body', + code: '400' + } + }); + + }); + + it('should work if client is in body too', async function () { + const entity = server.mongoSingleton('client', Model); + + entity.clientField = 'client'; + + modelMock = sinon.mock(Model); + modelMock.expects('findOne').once() + .withArgs({ + client: 99 + }).returns(new Promise(resolve => resolve({ + _id: '1', + toObject: () => ({ + _id: '1', + client: 99, + text: 'original' + }) + }))); + modelMock.expects('updateOne').once() + .withArgs({ + _id: '1' + }, { + _id: '1', + client: 99, + text: 'patched' + }).returns(new Promise(resolve => resolve())); + httpServer = server.listen(port); + + const res = (await request(host).patch(`/client?sap-client=099`) + .send({ + client: 99, + text: 'patched' + })); + + modelMock.verify(); + assertSuccess(res); + + res.body.should.deepEqual({ + id: '1', + client: 99, + text: 'patched' + }); + + }); + +}); diff --git a/test/mongo/mocked/client/post.js b/test/mongo/mocked/client/post.js new file mode 100644 index 0000000..f371f08 --- /dev/null +++ b/test/mongo/mocked/client/post.js @@ -0,0 +1,111 @@ +import 'should'; +import request from 'supertest'; +import sinon from 'sinon'; +import { odata, host, port, assertSuccess } from '../../../support/setup'; +import mongoose from 'mongoose'; +import { init } from '../../../support/db'; + +describe('mongo.mocked.client.post', () => { + let httpServer, server, modelMock, instanceMock, queryMock, query, Model; + + before(() => { + const Schema = mongoose.Schema; + const ModelSchema = new Schema({ + client: Number, + text: String + }); + + mongoose.set('overwriteModels', true); + + + Model = mongoose.model('client', ModelSchema); + + query = { + $where: () => { }, + where: () => { }, + equals: () => { }, + gte: () => { }, + lt: () => { }, + exec: () => { }, + count: () => new Promise((resolve) => resolve(1)), + model: Model + }; + }); + + beforeEach(async function () { + server = odata(); + init(server); + + }); + + afterEach(() => { + if (httpServer) { + httpServer.close(); + } + modelMock?.restore(); + instanceMock?.restore(); + queryMock?.restore(); + }); + + it('should fail with wrong client in body', async function () { + const entity = server.mongoEntity('client', Model); + + entity.clientField = 'client'; + + instanceMock = sinon.mock(Model.prototype); + instanceMock.expects('save').never(); + httpServer = server.listen(port); + + const res = (await request(host).post(`/client?sap-client=099`) + .send({ + client: 98, + text: 'patched' + })); + + instanceMock.verify(); + res.body.should.deepEqual({ + error: { + message: 'Client value in custom parameter differs from client value in body', + code: '400' + } + }); + + }); + + it('should work if client is in body too', async function () { + const entity = server.mongoEntity('client', Model); + + entity.clientField = 'client'; + + instanceMock = sinon.mock(Model.prototype); + instanceMock.expects('save').once() + .withArgs({ + validateBeforeSave: true, + validateModifiedOnly: true + }); + instanceMock.expects('toObject').once() + .returns({ + _id: '1', + client: 99, + text: 'original' + }) + httpServer = server.listen(port); + + const res = (await request(host).post(`/client?sap-client=099`) + .send({ + client: 99, + text: 'original' + })); + + instanceMock.verify(); + assertSuccess(res); + + res.body.should.deepEqual({ + id: '1', + client: 99, + text: 'original' + }); + + }); + +}); diff --git a/test/mongo/mocked/client/put.js b/test/mongo/mocked/client/put.js new file mode 100644 index 0000000..da5b140 --- /dev/null +++ b/test/mongo/mocked/client/put.js @@ -0,0 +1,215 @@ +import 'should'; +import request from 'supertest'; +import sinon from 'sinon'; +import { odata, host, port, assertSuccess } from '../../../support/setup'; +import mongoose from 'mongoose'; +import { init } from '../../../support/db'; + +describe('mongo.mocked.client.put', () => { + let httpServer, server, modelMock, instanceMock, queryMock, query, Model; + + before(() => { + const Schema = mongoose.Schema; + const ModelSchema = new Schema({ + client: Number, + text: String + }); + + mongoose.set('overwriteModels', true); + + + Model = mongoose.model('client', ModelSchema); + + query = { + $where: () => { }, + where: () => { }, + equals: () => { }, + gte: () => { }, + lt: () => { }, + exec: () => { }, + count: () => new Promise((resolve) => resolve(1)), + model: Model + }; + }); + + beforeEach(async function () { + server = odata(); + init(server); + + }); + + afterEach(() => { + if (httpServer) { + httpServer.close(); + } + modelMock?.restore(); + instanceMock?.restore(); + queryMock?.restore(); + }); + + it('should work if client is in body too', async function () { + const entity = server.mongoEntity('client', Model); + + entity.clientField = 'client'; + + modelMock = sinon.mock(Model); + modelMock.expects('findOne').once() + .withArgs({ + _id: '1' + }) + .returns(new Promise(resolve => resolve({ + _id: '1', + client: 99, + toObject: () => ( + { + _id: '1', + client: 99, + text: 'original' + }) + }))); + modelMock.expects('findByIdAndUpdate').once() + .withArgs('1', { + _id: '1', + client: 99, + text: 'patched' + }).returns(new Promise(resolve => resolve())); + httpServer = server.listen(port); + + const res = (await request(host).put(`/client('1')?sap-client=099`) + .send({ + _id: '1', + client: 99, + text: 'patched' + })); + + modelMock.verify(); + assertSuccess(res); + + res.body.should.deepEqual({ + id: '1', + client: 99, + text: 'patched' + }); + + }); + + it('should fail with wrong client in body', async function () { + const entity = server.mongoEntity('client', Model); + + entity.clientField = 'client'; + + modelMock = sinon.mock(Model); + modelMock.expects('findOne').once() + .withArgs({ + _id: '1' + }) + .returns(new Promise(resolve => resolve({ + _id: '1', + client: 99, + toObject: () => ( + { + _id: '1', + client: 99, + text: 'original' + }) + }))); + modelMock.expects('findByIdAndUpdate').never(); + httpServer = server.listen(port); + + const res = (await request(host).put(`/client('1')?sap-client=099`) + .send({ + _id: '1', + client: 98, + text: 'patched' + })); + + modelMock.verify(); + res.body.should.deepEqual({ + error: { + message: 'Client value in custom parameter differs from client value in body', + code: '400' + } + }); + + }); + + it('should fail with correct key and wrong client', async function () { + const entity = server.mongoEntity('client', Model); + + entity.clientField = 'client'; + + modelMock = sinon.mock(Model); + modelMock.expects('findOne').once() + .withArgs({ + _id: '1' + }) + .returns(new Promise(resolve => resolve({ + _id: '1', + client: 99, + toObject: () => ( + { + _id: '1', + client: 99, + text: 'original' + }) + }))); + modelMock.expects('findByIdAndUpdate').never(); + httpServer = server.listen(port); + + const res = (await request(host).put(`/client('1')?sap-client=098`) + .send({ + _id: '1', + client: 98, + text: 'patched' + })); + + modelMock.verify(); + res.status.should.be.equal(404); + + }); + + it('should apply client to the key', async function () { + const entity = server.mongoEntity('client', Model); + + entity.clientField = 'client'; + + modelMock = sinon.mock(Model); + modelMock.expects('findOne').once() + .withArgs({ + _id: '1' + }) + .returns(new Promise(resolve => resolve({ + _id: '1', + client: 99, + toObject: () => ({ + _id: '1', + client: 99, + text: 'original' + }) + }))); + modelMock.expects('findByIdAndUpdate').once() + .withArgs('1', { + _id: '1', + client: 99, + text: 'patched' + }).returns(new Promise(resolve => resolve())); + httpServer = server.listen(port); + + const res = (await request(host).put(`/client('1')?sap-client=099`) + .send({ + _id: '1', + text: 'patched' + })); + + modelMock.verify(); + assertSuccess(res); + + res.body.should.deepEqual({ + id: '1', + client: 99, + text: 'patched' + }); + + }); + +}); diff --git a/test/mongo/mocked/rest.put.js b/test/mongo/mocked/rest.put.js index 9e8f5b0..c370a42 100644 --- a/test/mongo/mocked/rest.put.js +++ b/test/mongo/mocked/rest.put.js @@ -30,7 +30,10 @@ describe('mongo.mocked.rest.put', () => { book.title = 'modify book'; modelMock = sinon.mock(BookModel); modelMock.expects('findOne').once().withArgs({_id: '1'}) - .returns(new Promise((resolve, reject) => resolve(JSON.parse(JSON.stringify(books[0]))))); + .returns(new Promise((resolve, reject) => resolve(JSON.parse(JSON.stringify({ + ...books[0], + _id: books[0].id + }))))); modelMock.expects('findByIdAndUpdate').once().withArgs('1', book) .returns(new Promise(resolve => resolve())); @@ -38,9 +41,9 @@ describe('mongo.mocked.rest.put', () => { .put(`/book('${book.id}')`) .send(book); + modelMock.verify(); res.body.should.be.have.property('title'); res.body.title.should.be.equal(book.title); - modelMock.verify(); }); it('should create resource if send with a id which not exist', async function () { const book = JSON.parse(JSON.stringify(books[0])); From fabc948482a8a6e03e24eed4bdb48443717b29bd Mon Sep 17 00:00:00 2001 From: Richard Martens Date: Sun, 12 Nov 2023 21:38:55 +0100 Subject: [PATCH 64/64] Client support documented --- README.md | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/README.md b/README.md index 37e03f0..8c7d303 100644 --- a/README.md +++ b/README.md @@ -544,6 +544,29 @@ const entity = server.entity('book', null, { entity.annotate('filterable', ['author', 'title']); ``` +## Client support + +Clients are supported via the custom parameter ```sap-client```. Querying an entity with client support could look like this: + +``` +books?sap-client=99 +``` + + Client support is built in for Mongo Entity and Singleton. If you implement the entity handler yourself, the passed client is available to you in ```req.$odata.client```. To activate tenant support for an entity or singleton, all you need to do is specify the name of the corresponding property in the collection. + +```Javascript +const BookSchema = new Schema({ + MANDT: String + author: String +}); + +const BookModel = mongoose.model('Book', BookSchema); + + const entity = server.entity('book', BookModel); + + entity.clientField = 'MANDT'; + ``` + ## Current State node-odata is currently at an beta stage, it is stable but not 100% feature complete.