Skip to content

Commit

Permalink
service document request implemented. closes zackyang000#101
Browse files Browse the repository at this point in the history
  • Loading branch information
r1mar committed Oct 13, 2022
1 parent cf29fa0 commit 418aee3
Show file tree
Hide file tree
Showing 5 changed files with 48 additions and 188 deletions.
21 changes: 14 additions & 7 deletions src/metadata/ODataMetadata.js
Original file line number Diff line number Diff line change
@@ -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) {
Expand Down Expand Up @@ -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);
}

Expand All @@ -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;
}, {});
Expand Down
182 changes: 12 additions & 170 deletions src/metadata/ODataServiceDocument.js
Original file line number Diff line number Diff line change
Expand Up @@ -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))
Expand All @@ -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,
});
});
}
Expand Down
8 changes: 4 additions & 4 deletions src/pipes.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -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';
Expand All @@ -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
Expand Down
9 changes: 6 additions & 3 deletions src/server.js
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand All @@ -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) {
Expand Down Expand Up @@ -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];
Expand Down
16 changes: 12 additions & 4 deletions test/service.document.js
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand All @@ -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');
Expand Down

0 comments on commit 418aee3

Please sign in to comment.