From 418aee3a558b7266cf87237a14c2660646031bae Mon Sep 17 00:00:00 2001 From: Richard Martens Date: Thu, 13 Oct 2022 23:25:51 +0200 Subject: [PATCH] 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');