├── .eslintrc ├── .github └── workflows │ └── main.yml ├── .gitignore ├── LICENSE ├── README.md ├── example ├── package.json └── simple.js ├── index.js ├── lib ├── collections.js ├── insert.js ├── metadata.js ├── odataServer.js ├── prune.js ├── query.js ├── queryTransform.js ├── remove.js ├── router.js └── update.js ├── package-lock.json ├── package.json └── test ├── metadataTest.js ├── model.js ├── odataServerTest.js ├── pruneTest.js └── queryTransformTest.js /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "standard", 3 | "env": { 4 | "node": true, 5 | "mocha": true 6 | }, 7 | "globals": { 8 | "define": true 9 | } 10 | } -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: Node CI 2 | 3 | on: [push] 4 | 5 | jobs: 6 | build: 7 | 8 | runs-on: ubuntu-latest 9 | 10 | steps: 11 | - uses: actions/checkout@v3 12 | - uses: actions/setup-node@v3 13 | with: 14 | node-version: 16 15 | - run: npm ci 16 | - run: npm test -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | 5 | # Runtime data 6 | pids 7 | *.pid 8 | *.seed 9 | 10 | # Directory for instrumented libs generated by jscoverage/JSCover 11 | lib-cov 12 | 13 | # Coverage directory used by tools like istanbul 14 | coverage 15 | 16 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 17 | .grunt 18 | 19 | # Compiled binary addons (http://nodejs.org/api/addons.html) 20 | build/Release 21 | 22 | # Dependency directory 23 | # Commenting this out is preferred by some people, see 24 | # https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git- 25 | node_modules 26 | 27 | # Users Environment Variables 28 | .lock-wscript 29 | 30 | .idea 31 | 32 | .vscode -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Jan Blaha 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ⚠️ This repository isn't being maintained. It's stable and still used in [jsreport](https://github.com/jsreport/jsreport), but we are too busy to provide adequate maintenance. Don't hesitate to let me know if you plan to maintain a fork so I can share it here.. 2 | -- 3 | 4 | # Node simple OData server 5 | [![NPM Version](http://img.shields.io/npm/v/simple-odata-server.svg?style=flat-square)](https://npmjs.com/package/simple-odata-server) 6 | [![License](http://img.shields.io/npm/l/simple-odata-server.svg?style=flat-square)](http://opensource.org/licenses/MIT) 7 | [![build status](https://github.com/pofider/node-simple-odata-server/actions/workflows/main.yml/badge.svg)](https://github.com/pofider/node-simple-odata-server/actions) 8 | 9 | **Super simple implementation of OData server running on Node.js with easy adapters for mongodb and nedb.** Just define an OData model, provide a mongo or nedb database, hook into node.js http server and run. 10 | 11 | It supports basic operations you would expect like providing $metadata, filtering and also operations for insert, update and delete. On the other hand it suppose to be really simple so you don't get support for entity links, batch operations, atom feeds and many others. 12 | 13 | The implementation is tested with [.net OData client](https://github.com/object/Simple.OData.Client) and it should fulfill basic protocol requirements. 14 | 15 | ## Get started 16 | 17 | This is how you can create an OData server with node.js http module and nedb. 18 | ```js 19 | var http = require('http'); 20 | var Datastore = require('nedb'); 21 | var db = new Datastore( { inMemoryOnly: true }); 22 | var ODataServer = require('simple-odata-server'); 23 | var Adapter = require('simple-odata-server-nedb'); 24 | 25 | var model = { 26 | namespace: "jsreport", 27 | entityTypes: { 28 | "UserType": { 29 | "_id": {"type": "Edm.String", key: true}, 30 | "test": {"type": "Edm.String"}, 31 | } 32 | }, 33 | entitySets: { 34 | "users": { 35 | entityType: "jsreport.UserType" 36 | } 37 | } 38 | }; 39 | 40 | var odataServer = ODataServer("http://localhost:1337") 41 | .model(model) 42 | .adapter(Adapter(function(es, cb) { cb(null, db)})); 43 | 44 | 45 | http.createServer(odataServer.handle.bind(odataServer)).listen(1337); 46 | ``` 47 | 48 | Now you can try requests like:
49 | GET [http://localhost:1337/$metadata]()
50 | GET [http://localhost:1337/users?$filter=test eq 'a' or test eq 'b'&$skip=1&$take=5]()
51 | GET [http://localhost:1337/users('aaaa')]()
52 | GET [http://localhost:1337/users?$orderby=test desc]()
53 | GET [http://localhost:1337/users/$count]()
54 | POST, PATCH, DELETE 55 | 56 | ## Adapters 57 | There are currently two adapters implemented. 58 | 59 | - [mongodb](https://www.mongodb.com/) - [pofider/node-simple-odata-server-mongodb](https://github.com/pofider/node-simple-odata-server-mongodb) 60 | - [nedb](https://github.com/louischatriot/nedb) - [pofider/node-simple-odata-server-nedb](https://github.com/pofider/node-simple-odata-server-nedb) 61 | 62 | The `mongo` adapter can be used as 63 | ```js 64 | var Adapter = require('simple-odata-server-mongodb') 65 | MongoClient.connect(url, function(err, db) { 66 | odataServer.adapter(Adapter(function(cb) { 67 | cb(err, db.db('myodatadb')); 68 | })); 69 | }); 70 | ``` 71 | 72 | ## express.js 73 | It works well also with the express.js. You even don't need to provide service uri in the `ODataServer` constructor because it is taken from the express.js request. 74 | 75 | ```js 76 | app.use("/odata", function (req, res) { 77 | odataServer.handle(req, res); 78 | }); 79 | ``` 80 | 81 | ## cors 82 | You can quickly set up cors without using express and middlewares using this call 83 | 84 | ```js 85 | odataServer.cors('*') 86 | ``` 87 | 88 | ## Configurations 89 | Using existing `adapter` is just a simple way for initializing `ODataServer`. You can implement your own data layer or override default behavior using following methods: 90 | 91 | ```js 92 | odataServer 93 | .query(fn(setName, query, req, cb)) 94 | .update(fn(setName, query, update, req, cb)) 95 | .insert(fn(setName, doc, req, cb)) 96 | .remove(fn(setName, query, req, cb)) 97 | .beforeQuery(fn(setName, query, req, cb)) 98 | .beforeUpdate(fn(setName, query, req, update)) 99 | .beforeInsert(fn(setName, doc, req, cb)) 100 | .beforeRemove(fn(setName, query, req, cb)) 101 | .afterRead(fn(setName, result)); 102 | //add hook to error which you can handle or pass to default 103 | .error(fn(req, res, error, default)) 104 | ``` 105 | 106 | 107 | 108 | ## Contributions 109 | I will maintain this repository for a while because I use it in [jsreport](https://github.com/jsreport/jsreport). You are more than welcome to contribute with pull requests and add other basic operations you require. 110 | 111 | ## Limitations 112 | - document ids must have name **_id** 113 | - no entity links 114 | - no batch operations 115 | - no validations 116 | - ... this would be a very long list, so rather check yourself 117 | 118 | ## License 119 | See [license](https://github.com/pofider/node-simple-odata-server/blob/master/LICENSE) 120 | 121 | -------------------------------------------------------------------------------- /example/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "example", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "simple.js", 6 | "scripts": { 7 | "start": "node simple.js" 8 | }, 9 | "author": "", 10 | "license": "ISC", 11 | "dependencies": { 12 | "simple-odata-server": "github:pofider/node-simple-odata-server", 13 | "simple-odata-server-nedb": "github:pofider/node-simple-odata-server-nedb" 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /example/simple.js: -------------------------------------------------------------------------------- 1 | const ODataServer = require('simple-odata-server') 2 | const Adapter = require('simple-odata-server-nedb') 3 | const http = require('http') 4 | const Datastore = require('nedb') 5 | const db = new Datastore({ inMemoryOnly: true }) 6 | 7 | const model = { 8 | namespace: 'jsreport', 9 | entityTypes: { 10 | UserType: { 11 | _id: { type: 'Edm.String', key: true }, 12 | test: { type: 'Edm.String' }, 13 | num: { type: 'Edm.Int32' }, 14 | d: { type: 'Edm.DateTimeOffset' }, 15 | addresses: { type: 'Collection(jsreport.AddressType)' } 16 | } 17 | }, 18 | complexTypes: { 19 | AddressType: { 20 | street: { type: 'Edm.String' } 21 | } 22 | }, 23 | entitySets: { 24 | users: { 25 | entityType: 'jsreport.UserType' 26 | } 27 | } 28 | } 29 | 30 | const odataServer = ODataServer('http://localhost:1337') 31 | .model(model) 32 | .adapter(Adapter(function (es, cb) { cb(null, db) })) 33 | 34 | http.createServer(odataServer.handle.bind(odataServer)).listen(1337) 35 | 36 | db.insert({ _id: '1', test: 'a', num: 1, addresses: [{ street: 'a1' }] }) 37 | db.insert({ _id: '2', test: 'b', num: 2, addresses: [{ street: 'a2' }] }) 38 | db.insert({ _id: '3', test: 'c', num: 3 }) 39 | db.insert({ _id: '4', test: 'd', num: 4 }) 40 | db.insert({ _id: '5', test: 'e', num: 5 }) 41 | 42 | console.log('server running on http://localhost:1337') 43 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * Copyright(c) 2014 Jan Blaha (pofider) 3 | * 4 | * Simple OData server with adapters for mongodb and nedb 5 | */ 6 | 7 | const ODataServer = require('./lib/odataServer.js') 8 | 9 | module.exports = function (options) { 10 | return new ODataServer(options) 11 | } 12 | -------------------------------------------------------------------------------- /lib/collections.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * Copyright(c) 2014 Jan Blaha (pofider) 3 | * 4 | * Orchestrate the OData / request 5 | */ 6 | 7 | module.exports = function (cfg) { 8 | const collections = [] 9 | for (const key in cfg.model.entitySets) { 10 | collections.push({ 11 | kind: 'EntitySet', 12 | name: key, 13 | url: key 14 | }) 15 | } 16 | 17 | return JSON.stringify({ 18 | '@odata.context': cfg.serviceUrl + '/$metadata', 19 | value: collections 20 | }) 21 | } 22 | -------------------------------------------------------------------------------- /lib/insert.js: -------------------------------------------------------------------------------- 1 | function keys (o) { 2 | const res = [] 3 | const k = Object.keys(o) 4 | for (const i in k) { 5 | if (k[i].lastIndexOf('@', 0) === 0) { 6 | res.splice(0, 0, k[i]) 7 | } else { 8 | res.push(k[i]) 9 | } 10 | } 11 | return res 12 | }; 13 | 14 | function sortProperties (o) { 15 | const res = {} 16 | const props = keys(o) 17 | 18 | for (let i = 0; i < props.length; i++) { 19 | res[props[i]] = o[props[i]] 20 | } 21 | return res 22 | }; 23 | 24 | function removeOdataType (doc) { 25 | if (doc instanceof Array) { 26 | for (const i in doc) { 27 | if (typeof doc[i] === 'object' && doc[i] !== null) { 28 | removeOdataType(doc[i]) 29 | } 30 | } 31 | } 32 | 33 | delete doc['@odata.type'] 34 | 35 | for (const prop in doc) { 36 | if (typeof doc[prop] === 'object' && doc[prop] !== null) { 37 | removeOdataType(doc[prop]) 38 | } 39 | } 40 | } 41 | 42 | function processBody (data, cfg, req, res) { 43 | try { 44 | removeOdataType(data) 45 | cfg.base64ToBuffer(req.params.collection, data) 46 | cfg.executeInsert(req.params.collection, data, req, function (err, entity) { 47 | if (err) { 48 | return res.odataError(err) 49 | } 50 | 51 | res.statusCode = 201 52 | res.setHeader('Content-Type', 'application/json;odata.metadata=minimal;odata.streaming=true;IEEE754Compatible=false;charset=utf-8') 53 | res.setHeader('OData-Version', '4.0') 54 | res.setHeader('Location', cfg.serviceUrl + '/' + req.params.collection + "/('" + encodeURI(entity._id) + "')") 55 | cfg.addCorsToResponse(res) 56 | 57 | cfg.pruneResults(req.params.collection, entity) 58 | 59 | // odata.context must be first 60 | entity['@odata.id'] = cfg.serviceUrl + '/' + req.params.collection + "('" + entity._id + "')" 61 | entity['@odata.editLink'] = cfg.serviceUrl + '/' + req.params.collection + "('" + entity._id + "')" 62 | entity['@odata.context'] = cfg.serviceUrl + '/$metadata#' + req.params.collection + '/$entity' 63 | 64 | entity = sortProperties(entity) 65 | cfg.bufferToBase64(req.params.collection, [entity]) 66 | 67 | return res.end(JSON.stringify(entity)) 68 | }) 69 | } catch (e) { 70 | res.odataError(e) 71 | } 72 | } 73 | 74 | module.exports = function (cfg, req, res) { 75 | if (req.body) { 76 | return processBody(req.body, cfg, req, res) 77 | } 78 | 79 | let body = '' 80 | req.on('data', function (data) { 81 | body += data 82 | if (body.length > 1e6) { 83 | req.connection.destroy() 84 | } 85 | }) 86 | req.on('end', function () { 87 | return processBody(JSON.parse(body), cfg, req, res) 88 | }) 89 | } 90 | -------------------------------------------------------------------------------- /lib/metadata.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * Copyright(c) 2014 Jan Blaha (pofider) 3 | * 4 | * Orchestrate the OData /$metadata request 5 | */ 6 | 7 | /* eslint no-redeclare:0 */ 8 | 9 | const builder = require('xmlbuilder') 10 | 11 | module.exports = function (cfg) { 12 | return buildMetadata(cfg.model) 13 | } 14 | 15 | function buildMetadata (model) { 16 | const entityTypes = [] 17 | for (const typeKey in model.entityTypes) { 18 | const entityType = { 19 | '@Name': typeKey, 20 | Property: [] 21 | } 22 | 23 | for (const propKey in model.entityTypes[typeKey]) { 24 | const property = model.entityTypes[typeKey][propKey] 25 | const finalObject = { '@Name': propKey, '@Type': property.type } 26 | if (Object.prototype.hasOwnProperty.call(property, 'nullable')) { 27 | finalObject['@Nullable'] = property.nullable 28 | } 29 | entityType.Property.push(finalObject) 30 | 31 | if (property.key) { 32 | entityType.Key = { 33 | PropertyRef: { 34 | '@Name': propKey 35 | } 36 | } 37 | } 38 | } 39 | 40 | entityTypes.push(entityType) 41 | } 42 | 43 | const complexTypes = [] 44 | for (const typeKey in model.complexTypes) { 45 | const complexType = { 46 | '@Name': typeKey, 47 | Property: [] 48 | } 49 | 50 | for (const propKey in model.complexTypes[typeKey]) { 51 | const property = model.complexTypes[typeKey][propKey] 52 | 53 | complexType.Property.push({ '@Name': propKey, '@Type': property.type }) 54 | } 55 | 56 | complexTypes.push(complexType) 57 | } 58 | 59 | const container = { 60 | '@Name': 'Context', 61 | EntitySet: [] 62 | } 63 | 64 | for (const setKey in model.entitySets) { 65 | container.EntitySet.push({ 66 | '@EntityType': model.entitySets[setKey].entityType, 67 | '@Name': setKey 68 | }) 69 | } 70 | 71 | const returnObject = { 72 | 'edmx:Edmx': { 73 | '@xmlns:edmx': 'http://docs.oasis-open.org/odata/ns/edmx', 74 | '@Version': '4.0', 75 | 'edmx:DataServices': { 76 | Schema: { 77 | '@xmlns': 'http://docs.oasis-open.org/odata/ns/edm', 78 | '@Namespace': model.namespace, 79 | EntityType: entityTypes, 80 | EntityContainer: container 81 | } 82 | } 83 | } 84 | } 85 | 86 | if (complexTypes.length) { 87 | returnObject['edmx:Edmx']['edmx:DataServices'].Schema.ComplexType = complexTypes 88 | } 89 | 90 | return builder.create(returnObject).end({ pretty: true }) 91 | } 92 | -------------------------------------------------------------------------------- /lib/odataServer.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * Copyright(c) 2014 Jan Blaha (pofider) 3 | * 4 | * ODataServer class - main facade 5 | */ 6 | 7 | /* eslint no-useless-escape: 0 */ 8 | 9 | const Emitter = require('events').EventEmitter 10 | const util = require('util') 11 | const url = require('url') 12 | const metadata = require('./metadata.js') 13 | const collections = require('./collections.js') 14 | const query = require('./query.js') 15 | const insert = require('./insert.js') 16 | const update = require('./update.js') 17 | const remove = require('./remove.js') 18 | const Router = require('./router.js') 19 | const prune = require('./prune.js') 20 | const Buffer = require('safe-buffer').Buffer 21 | 22 | function ODataServer (serviceUrl) { 23 | this.serviceUrl = serviceUrl 24 | 25 | this.cfg = { 26 | serviceUrl, 27 | afterRead: function () {}, 28 | beforeQuery: function (col, query, req, cb) { cb() }, 29 | executeQuery: ODataServer.prototype.executeQuery.bind(this), 30 | beforeInsert: function (col, query, req, cb) { cb() }, 31 | executeInsert: ODataServer.prototype.executeInsert.bind(this), 32 | beforeUpdate: function (col, query, update, req, cb) { cb() }, 33 | executeUpdate: ODataServer.prototype.executeUpdate.bind(this), 34 | beforeRemove: function (col, query, req, cb) { cb() }, 35 | executeRemove: ODataServer.prototype.executeRemove.bind(this), 36 | base64ToBuffer: ODataServer.prototype.base64ToBuffer.bind(this), 37 | bufferToBase64: ODataServer.prototype.bufferToBase64.bind(this), 38 | pruneResults: ODataServer.prototype.pruneResults.bind(this), 39 | addCorsToResponse: ODataServer.prototype.addCorsToResponse.bind(this) 40 | } 41 | } 42 | 43 | util.inherits(ODataServer, Emitter) 44 | 45 | ODataServer.prototype.handle = function (req, res) { 46 | if (!this.cfg.serviceUrl && !req.protocol) { 47 | throw new Error('Unable to determine service url from the express request or value provided in the ODataServer constructor.') 48 | } 49 | 50 | function escapeRegExp (str) { 51 | return str.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g, '\\$&') 52 | } 53 | 54 | // If mounted in express, trim off the subpath (req.url) giving us just the base path 55 | const path = (req.originalUrl || '/').replace(new RegExp(escapeRegExp(req.url) + '$'), '') 56 | this.cfg.serviceUrl = this.serviceUrl ? this.serviceUrl : (req.protocol + '://' + req.get('host') + path) 57 | 58 | const prefix = url.parse(this.cfg.serviceUrl).pathname // eslint-disable-line 59 | if (!this.router || (prefix !== this.router.prefix)) { 60 | this.router = new Router(prefix) 61 | this._initializeRoutes() 62 | } 63 | 64 | this.router.dispatch(req, res) 65 | } 66 | 67 | ODataServer.prototype._initializeRoutes = function () { 68 | const self = this 69 | this.router.get('/\$metadata', function (req, res) { 70 | const result = metadata(self.cfg) 71 | 72 | res.statusCode = 200 73 | res.setHeader('Content-Type', 'application/xml') 74 | res.setHeader('DataServiceVersion', '4.0') 75 | res.setHeader('OData-Version', '4.0') 76 | self.cfg.addCorsToResponse(res) 77 | 78 | return res.end(result) 79 | }) 80 | this.router.get('/:collection/\$count', function (req, res) { 81 | req.params.$count = true 82 | query(self.cfg, req, res) 83 | }) 84 | this.router.get('/:collection\\(:id\\)', function (req, res) { 85 | query(self.cfg, req, res) 86 | }) 87 | this.router.get('/:collection', function (req, res) { 88 | query(self.cfg, req, res) 89 | }) 90 | this.router.get('/', function (req, res) { 91 | const result = collections(self.cfg) 92 | 93 | res.statusCode = 200 94 | res.setHeader('Content-Type', 'application/json') 95 | self.cfg.addCorsToResponse(res) 96 | 97 | return res.end(result) 98 | }) 99 | this.router.post('/:collection', function (req, res) { 100 | insert(self.cfg, req, res) 101 | }) 102 | this.router.patch('/:collection\\(:id\\)', function (req, res) { 103 | update(self.cfg, req, res) 104 | }) 105 | this.router.delete('/:collection\\(:id\\)', function (req, res) { 106 | remove(self.cfg, req, res) 107 | }) 108 | 109 | if (this.cfg.cors) { 110 | this.router.options('/(.*)', function (req, res) { 111 | res.statusCode = 200 112 | res.setHeader('Access-Control-Allow-Origin', self.cfg.cors) 113 | res.end() 114 | }) 115 | } 116 | 117 | this.router.error(function (req, res, error) { 118 | function def (e) { 119 | self.emit('odata-error', e) 120 | 121 | res.statusCode = (error.code && error.code >= 100 && error.code < 600) ? error.code : 500 122 | res.setHeader('Content-Type', 'application/json') 123 | self.cfg.addCorsToResponse(res) 124 | 125 | res.end(JSON.stringify({ 126 | error: { 127 | code: error.code || 500, 128 | message: e.message, 129 | stack: e.stack, 130 | target: req.url, 131 | details: [] 132 | }, 133 | innererror: { } 134 | })) 135 | } 136 | if (self.cfg.error) { 137 | self.cfg.error(req, res, error, def) 138 | } else { 139 | def(error) 140 | } 141 | }) 142 | } 143 | 144 | ODataServer.prototype.error = function (fn) { 145 | this.cfg.error = fn.bind(this) 146 | return this 147 | } 148 | 149 | ODataServer.prototype.query = function (fn) { 150 | this.cfg.query = fn.bind(this) 151 | return this 152 | } 153 | 154 | ODataServer.prototype.cors = function (domains) { 155 | this.cfg.cors = domains 156 | return this 157 | } 158 | 159 | ODataServer.prototype.beforeQuery = function (fn) { 160 | if (fn.length === 3) { 161 | console.warn('Listener function should accept request parameter.') 162 | const origFn = fn 163 | fn = function (col, query, req, cb) { 164 | origFn(col, query, cb) 165 | } 166 | } 167 | 168 | this.cfg.beforeQuery = fn.bind(this) 169 | return this 170 | } 171 | 172 | ODataServer.prototype.executeQuery = function (col, query, req, cb) { 173 | const self = this 174 | 175 | this.cfg.beforeQuery(col, query, req, function (err) { 176 | if (err) { 177 | return cb(err) 178 | } 179 | 180 | self.cfg.query(col, query, req, function (err, res) { 181 | if (err) { 182 | return cb(err) 183 | } 184 | 185 | self.cfg.afterRead(col, res, req) 186 | cb(null, res) 187 | }) 188 | }) 189 | } 190 | 191 | ODataServer.prototype.insert = function (fn) { 192 | this.cfg.insert = fn.bind(this) 193 | return this 194 | } 195 | 196 | ODataServer.prototype.beforeInsert = function (fn) { 197 | if (fn.length === 3) { 198 | console.warn('Listener function should accept request parameter.') 199 | const origFn = fn 200 | fn = function (col, doc, req, cb) { 201 | origFn(col, doc, cb) 202 | } 203 | } 204 | 205 | this.cfg.beforeInsert = fn.bind(this) 206 | return this 207 | } 208 | 209 | ODataServer.prototype.executeInsert = function (col, doc, req, cb) { 210 | const self = this 211 | this.cfg.beforeInsert(col, doc, req, function (err) { 212 | if (err) { 213 | return cb(err) 214 | } 215 | 216 | self.cfg.insert(col, doc, req, cb) 217 | }) 218 | } 219 | 220 | ODataServer.prototype.update = function (fn) { 221 | this.cfg.update = fn.bind(this) 222 | return this 223 | } 224 | 225 | ODataServer.prototype.beforeUpdate = function (fn) { 226 | if (fn.length === 4) { 227 | console.warn('Listener function should accept request parameter.') 228 | const origFn = fn 229 | fn = function (col, query, update, req, cb) { 230 | origFn(col, query, update, cb) 231 | } 232 | } 233 | 234 | this.cfg.beforeUpdate = fn.bind(this) 235 | return this 236 | } 237 | 238 | ODataServer.prototype.executeUpdate = function (col, query, update, req, cb) { 239 | const self = this 240 | 241 | this.cfg.beforeUpdate(col, query, update, req, function (err) { 242 | if (err) { 243 | return cb(err) 244 | } 245 | 246 | self.cfg.update(col, query, update, req, cb) 247 | }) 248 | } 249 | 250 | ODataServer.prototype.remove = function (fn) { 251 | this.cfg.remove = fn.bind(this) 252 | return this 253 | } 254 | 255 | ODataServer.prototype.beforeRemove = function (fn) { 256 | if (fn.length === 3) { 257 | console.warn('Listener function should accept request parameter.') 258 | const origFn = fn 259 | fn = function (col, query, req, cb) { 260 | origFn(col, query, cb) 261 | } 262 | } 263 | 264 | this.cfg.beforeRemove = fn.bind(this) 265 | return this 266 | } 267 | 268 | ODataServer.prototype.executeRemove = function (col, query, req, cb) { 269 | const self = this 270 | this.cfg.beforeRemove(col, query, req, function (err) { 271 | if (err) { 272 | return cb(err) 273 | } 274 | 275 | self.cfg.remove(col, query, req, cb) 276 | }) 277 | } 278 | 279 | ODataServer.prototype.afterRead = function (fn) { 280 | this.cfg.afterRead = fn 281 | return this 282 | } 283 | 284 | ODataServer.prototype.model = function (model) { 285 | this.cfg.model = model 286 | return this 287 | } 288 | 289 | ODataServer.prototype.adapter = function (adapter) { 290 | adapter(this) 291 | return this 292 | } 293 | 294 | ODataServer.prototype.pruneResults = function (collection, res) { 295 | prune(this.cfg.model, collection, res) 296 | } 297 | 298 | ODataServer.prototype.base64ToBuffer = function (collection, doc) { 299 | const model = this.cfg.model 300 | const entitySet = model.entitySets[collection] 301 | const entityType = model.entityTypes[entitySet.entityType.replace(model.namespace + '.', '')] 302 | 303 | for (const prop in doc) { 304 | if (!prop) { 305 | continue 306 | } 307 | 308 | const propDef = entityType[prop] 309 | 310 | if (!propDef) { 311 | continue 312 | } 313 | 314 | if (propDef.type === 'Edm.Binary') { 315 | doc[prop] = Buffer.from(doc[prop], 'base64') 316 | } 317 | } 318 | } 319 | 320 | ODataServer.prototype.bufferToBase64 = function (collection, res) { 321 | const model = this.cfg.model 322 | const entitySet = model.entitySets[collection] 323 | const entityType = model.entityTypes[entitySet.entityType.replace(model.namespace + '.', '')] 324 | 325 | for (const i in res) { 326 | const doc = res[i] 327 | for (const prop in doc) { 328 | if (!prop) { 329 | continue 330 | } 331 | 332 | const propDef = entityType[prop] 333 | 334 | if (!propDef) { 335 | continue 336 | } 337 | 338 | if (propDef.type === 'Edm.Binary') { 339 | // nedb returns object instead of buffer on node 4 340 | if (!Buffer.isBuffer(doc[prop]) && !doc[prop].length) { 341 | let obj = doc[prop] 342 | obj = obj.data || obj 343 | doc[prop] = Object.keys(obj).map(function (key) { return obj[key] }) 344 | } 345 | 346 | // unwrap mongo style buffers 347 | if (doc[prop]._bsontype === 'Binary') { 348 | doc[prop] = doc[prop].buffer 349 | } 350 | 351 | doc[prop] = Buffer.from(doc[prop]).toString('base64') 352 | } 353 | } 354 | } 355 | } 356 | 357 | ODataServer.prototype.addCorsToResponse = function (res) { 358 | if (this.cfg.cors) { 359 | res.setHeader('Access-Control-Allow-Origin', this.cfg.cors) 360 | } 361 | } 362 | 363 | module.exports = ODataServer 364 | -------------------------------------------------------------------------------- /lib/prune.js: -------------------------------------------------------------------------------- 1 | /* eslint no-redeclare:0 */ 2 | function prune (doc, model, type) { 3 | if (doc instanceof Array) { 4 | for (const i in doc) { 5 | prune(doc[i], model, type) 6 | } 7 | return 8 | } 9 | 10 | for (const prop in doc) { 11 | if (!prop || doc[prop] === undefined || prop.toString().substring(0, 6) === '@odata') { 12 | continue 13 | } 14 | 15 | const propDef = type[prop] 16 | 17 | if (!propDef) { 18 | delete doc[prop] 19 | continue 20 | } 21 | 22 | if (propDef.type.indexOf('Collection') === 0) { 23 | if (propDef.type.indexOf('Collection(Edm') === 0) { 24 | continue 25 | } 26 | let complexTypeName = propDef.type.replace('Collection(' + model.namespace + '.', '') 27 | complexTypeName = complexTypeName.substring(0, complexTypeName.length - 1) 28 | const complexType = model.complexTypes[complexTypeName] 29 | if (!complexType) { 30 | throw new Error('Complex type ' + complexTypeName + ' was not found.') 31 | } 32 | 33 | for (const i in doc[prop]) { 34 | prune(doc[prop][i], model, complexType) 35 | } 36 | continue 37 | } 38 | 39 | if (propDef.type.indexOf('Edm') !== 0) { 40 | const complexTypeName = propDef.type.replace(model.namespace + '.', '') 41 | const complexType = model.complexTypes[complexTypeName] 42 | if (!complexType) { 43 | throw new Error('Complex type ' + complexTypeName + ' was not found.') 44 | } 45 | prune(doc[prop], model, complexType) 46 | } 47 | } 48 | } 49 | 50 | module.exports = function (model, collection, docs) { 51 | const entitySet = model.entitySets[collection] 52 | const entityType = model.entityTypes[entitySet.entityType.replace(model.namespace + '.', '')] 53 | 54 | prune(docs, model, entityType) 55 | } 56 | -------------------------------------------------------------------------------- /lib/query.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * Copyright(c) 2014 Jan Blaha (pofider) 3 | * 4 | * Orchestrate the OData query GET requests 5 | */ 6 | 7 | /* eslint no-useless-escape: 0 */ 8 | /* eslint no-redeclare:0 */ 9 | 10 | const parser = require('odata-parser') 11 | const queryTransform = require('./queryTransform.js') 12 | const url = require('url') 13 | const querystring = require('querystring') 14 | 15 | module.exports = function (cfg, req, res) { 16 | if (!cfg.model.entitySets[req.params.collection]) { 17 | const error = new Error('Entity set not Found') 18 | error.code = 404 19 | res.odataError(error) 20 | return 21 | } 22 | 23 | let queryOptions = { 24 | $filter: {} 25 | } 26 | 27 | const _url = url.parse(req.url, true) // eslint-disable-line 28 | if (_url.search) { 29 | const query = _url.query 30 | const fixedQS = {} 31 | if (query.$) fixedQS.$ = query.$ 32 | if (query.$expand) fixedQS.$expand = query.$expand 33 | if (query.$filter) fixedQS.$filter = query.$filter 34 | if (query.$format) fixedQS.$format = query.$format 35 | if (query.$inlinecount) fixedQS.$inlinecount = query.$inlinecount 36 | if (query.$select) fixedQS.$select = query.$select 37 | if (query.$skip) fixedQS.$skip = query.$skip 38 | if (query.$top) fixedQS.$top = query.$top 39 | if (query.$orderby) fixedQS.$orderby = query.$orderby 40 | 41 | const encodedQS = decodeURIComponent(querystring.stringify(fixedQS)) 42 | if (encodedQS) { 43 | queryOptions = queryTransform(parser.parse(encodedQS)) 44 | } 45 | if (query.$count) { 46 | queryOptions.$inlinecount = true 47 | } 48 | } 49 | 50 | queryOptions.collection = req.params.collection 51 | 52 | if (req.params.$count) { 53 | queryOptions.$count = true 54 | } 55 | 56 | if (req.params.id) { 57 | req.params.id = req.params.id.replace(/\"/g, '').replace(/'/g, '') 58 | queryOptions.$filter = { 59 | _id: req.params.id 60 | } 61 | } 62 | 63 | cfg.executeQuery(queryOptions.collection, queryOptions, req, function (err, result) { 64 | if (err) { 65 | return res.odataError(err) 66 | } 67 | 68 | res.statusCode = 200 69 | res.setHeader('Content-Type', 'application/json;odata.metadata=minimal') 70 | res.setHeader('OData-Version', '4.0') 71 | cfg.addCorsToResponse(res) 72 | 73 | let out = {} 74 | // define the @odataContext in case of selection 75 | let sAdditionIntoContext = '' 76 | const oSelect = queryOptions.$select 77 | if (oSelect) { 78 | const countProp = Object.keys(oSelect).length 79 | let ctr = 1 80 | for (const key in oSelect) { 81 | sAdditionIntoContext += key.toString() + (ctr < countProp ? ',' : '') 82 | ctr++ 83 | } 84 | } 85 | if (Object.prototype.hasOwnProperty.call(queryOptions.$filter, '_id')) { 86 | sAdditionIntoContext = sAdditionIntoContext.length > 0 ? '(' + sAdditionIntoContext + ')/$entity' : '/$entity' 87 | out['@odata.context'] = cfg.serviceUrl + '/$metadata#' + req.params.collection + sAdditionIntoContext 88 | if (result.length > 0) { 89 | for (const key in result[0]) { 90 | out[key] = result[0][key] 91 | } 92 | } 93 | // this shouldn't be done, but for backcompatibility we keep it for now 94 | out.value = result 95 | } else { 96 | sAdditionIntoContext = sAdditionIntoContext.length > 0 ? '(' + sAdditionIntoContext + ')' : '' 97 | out = { 98 | '@odata.context': cfg.serviceUrl + '/$metadata#' + req.params.collection + sAdditionIntoContext, 99 | value: result 100 | } 101 | } 102 | 103 | if (queryOptions.$inlinecount) { 104 | out['@odata.count'] = result.count 105 | out.value = result.value 106 | } 107 | cfg.pruneResults(queryOptions.collection, out.value) 108 | 109 | cfg.bufferToBase64(queryOptions.collection, out.value) 110 | 111 | return res.end(JSON.stringify(out)) 112 | }) 113 | } 114 | -------------------------------------------------------------------------------- /lib/queryTransform.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * Copyright(c) 2014 Jan Blaha (pofider) 3 | * 4 | * Parse query string OData params and transform into mongo/nedb type of query 5 | */ 6 | 7 | module.exports = function (query) { 8 | if (query.$filter) { 9 | query.$filter = new Node(query.$filter.type, query.$filter.left, query.$filter.right, query.$filter.func, query.$filter.args).transform() 10 | } else { 11 | query.$filter = {} 12 | } 13 | 14 | if (query.$top) { 15 | query.$limit = query.$top 16 | } 17 | 18 | if (query.$orderby) { 19 | query.$sort = {} 20 | query.$orderby.forEach(function (prop) { 21 | const propName = Object.keys(prop)[0] 22 | query.$sort[propName] = prop[propName] === 'desc' ? -1 : 1 23 | }) 24 | } 25 | 26 | if (query.$inlinecount === 'allpages') { 27 | query.$count = true 28 | } 29 | 30 | const select = {} 31 | for (const key in query.$select || []) { 32 | select[query.$select[key]] = 1 33 | } 34 | query.$select = select 35 | 36 | return query 37 | } 38 | 39 | function Node (type, left, right, func, args) { 40 | this.type = type 41 | this.left = left 42 | this.right = right 43 | this.func = func 44 | this.args = args 45 | } 46 | 47 | Node.prototype._prop = function (result, left, rightValue) { 48 | if (left.type === 'property' && left.name.indexOf('/') !== -1) { 49 | const fragments = left.name.split('/') 50 | const obj = result[fragments[0]] || {} 51 | 52 | for (let i = 1; i < fragments.length; i++) { 53 | if (i === (fragments.length - 1)) { 54 | obj[fragments[i]] = rightValue 55 | } else { 56 | obj[fragments[i]] = obj[fragments[i]] || {} 57 | } 58 | } 59 | 60 | result[fragments[0]] = obj 61 | } else { 62 | result[left.name] = rightValue 63 | } 64 | } 65 | 66 | Node.prototype.transform = function () { 67 | const result = {} 68 | 69 | if (this.type === 'eq' && this.right.type === 'literal') { 70 | // odata parser returns ['null', ''] for a filter with "field eq null" 71 | // we handle the case by fixing the query in case this happens 72 | if ( 73 | Array.isArray(this.right.value) && 74 | this.right.value.length === 2 && 75 | this.right.value[0] === 'null' && 76 | this.right.value[1] === '' 77 | ) { 78 | this._prop(result, this.left, null) 79 | } else { 80 | this._prop(result, this.left, this.right.value) 81 | } 82 | } 83 | 84 | if (this.type === 'lt' && this.right.type === 'literal') { 85 | this._prop(result, this.left, { $lt: this.right.value }) 86 | } 87 | 88 | if (this.type === 'gt' && this.right.type === 'literal') { 89 | this._prop(result, this.left, { $gt: this.right.value }) 90 | } 91 | 92 | if (this.type === 'le' && this.right.type === 'literal') { 93 | this._prop(result, this.left, { $lte: this.right.value }) 94 | } 95 | 96 | if (this.type === 'ge' && this.right.type === 'literal') { 97 | this._prop(result, this.left, { $gte: this.right.value }) 98 | } 99 | 100 | if (this.type === 'ne' && this.right.type === 'literal') { 101 | // odata parser returns ['null', ''] for a filter with "field eq null" 102 | // we handle the case by fixing the query in case this happens 103 | if ( 104 | Array.isArray(this.right.value) && 105 | this.right.value.length === 2 && 106 | this.right.value[0] === 'null' && 107 | this.right.value[1] === '' 108 | ) { 109 | this._prop(result, this.left, { $ne: null }) 110 | } else { 111 | this._prop(result, this.left, { $ne: this.right.value }) 112 | } 113 | } 114 | 115 | if (this.type === 'and') { 116 | result.$and = result.$and || [] 117 | result.$and.push(new Node(this.left.type, this.left.left, this.left.right, this.func, this.args).transform()) 118 | result.$and.push(new Node(this.right.type, this.right.left, this.right.right, this.func, this.args).transform()) 119 | } 120 | 121 | if (this.type === 'or') { 122 | result.$or = result.$or || [] 123 | result.$or.push(new Node(this.left.type, this.left.left, this.left.right, this.func, this.args).transform()) 124 | result.$or.push(new Node(this.right.type, this.right.left, this.right.right, this.func, this.args).transform()) 125 | } 126 | 127 | if (this.type === 'functioncall') { 128 | switch (this.func) { 129 | case 'substringof': substringof(this, result) 130 | } 131 | } 132 | 133 | return result 134 | } 135 | 136 | function substringof (node, result) { 137 | const prop = node.args[0].type === 'property' ? node.args[0] : node.args[1] 138 | const lit = node.args[0].type === 'literal' ? node.args[0] : node.args[1] 139 | 140 | result[prop.name] = new RegExp(lit.value) 141 | } 142 | -------------------------------------------------------------------------------- /lib/remove.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * Copyright(c) 2014 Jan Blaha (pofider) 3 | * 4 | * Orchestrate the OData DELETE request 5 | */ 6 | 7 | /* eslint no-useless-escape: 0 */ 8 | 9 | module.exports = function (cfg, req, res) { 10 | try { 11 | const query = { 12 | _id: req.params.id.replace(/\"/g, '').replace(/'/g, '') 13 | } 14 | 15 | cfg.executeRemove(req.params.collection, query, req, function (e) { 16 | if (e) { 17 | return res.odataError(e) 18 | } 19 | 20 | res.statusCode = 204 21 | cfg.addCorsToResponse(res) 22 | 23 | res.end() 24 | }) 25 | } catch (e) { 26 | return res.odataError(e) 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /lib/router.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * Copyright(c) 2014 Jan Blaha (pofider) 3 | * 4 | * Simple regex based http router 5 | */ 6 | 7 | const url = require('url') 8 | const { pathToRegexp } = require('path-to-regexp') 9 | const methods = require('methods') 10 | 11 | function Router (prefix) { 12 | this.routes = {} 13 | this.prefix = prefix === '/' ? '' : prefix 14 | const self = this 15 | methods.forEach(function (m) { 16 | self.routes[m] = [] 17 | }) 18 | } 19 | 20 | const router = Router.prototype 21 | 22 | module.exports = Router 23 | 24 | methods.forEach(function (m) { 25 | router[m] = function (route, fn) { 26 | this.routes[m].push({ 27 | route: this.prefix + route, 28 | fn 29 | }) 30 | } 31 | }) 32 | 33 | router.error = function (fn) { 34 | this._errFn = fn 35 | } 36 | 37 | router.dispatch = function (req, res) { 38 | const self = this 39 | const m = req.method.toLowerCase() 40 | res.odataError = function (err) { 41 | self._errFn(req, res, err) 42 | } 43 | 44 | const pathname = url.parse(req.originalUrl || req.url).pathname // eslint-disable-line 45 | 46 | let match = false 47 | 48 | for (const i in this.routes[m]) { 49 | const el = this.routes[m][i] 50 | const keys = [] 51 | const re = pathToRegexp(el.route, keys) 52 | const ex = re.exec(pathname) 53 | 54 | if (ex) { 55 | match = true 56 | const args = ex.slice(1).map(decode) 57 | req.params = {} 58 | for (let j = 0; j < keys.length; j++) { 59 | req.params[keys[j].name] = args[j] 60 | } 61 | 62 | try { 63 | el.fn(req, res) 64 | } catch (e) { 65 | self._errFn(req, res, e) 66 | } 67 | 68 | break 69 | } 70 | } 71 | 72 | if (!match) { 73 | const error = new Error('Not Found') 74 | error.code = 404 75 | res.odataError(error) 76 | } 77 | } 78 | 79 | function decode (val) { 80 | if (val) return decodeURIComponent(val) 81 | } 82 | -------------------------------------------------------------------------------- /lib/update.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * Copyright(c) 2014 Jan Blaha (pofider) 3 | * 4 | * Orchestrate the OData PATCH requests 5 | */ 6 | 7 | /* eslint no-useless-escape: 0 */ 8 | 9 | function removeOdataType (doc) { 10 | if (doc instanceof Array) { 11 | for (const i in doc) { 12 | if (typeof doc[i] === 'object' && doc[i] !== null) { 13 | removeOdataType(doc[i]) 14 | } 15 | } 16 | } 17 | 18 | delete doc['@odata.type'] 19 | 20 | for (const prop in doc) { 21 | if (typeof doc[prop] === 'object' && doc[prop] !== null) { 22 | removeOdataType(doc[prop]) 23 | } 24 | } 25 | } 26 | 27 | function processBody (body, cfg, req, res) { 28 | removeOdataType(body) 29 | 30 | const query = { 31 | _id: req.params.id.replace(/\"/g, '').replace(/'/g, '') 32 | } 33 | 34 | const update = { 35 | $set: body 36 | } 37 | 38 | try { 39 | cfg.base64ToBuffer(req.params.collection, update.$set) 40 | cfg.executeUpdate(req.params.collection, query, update, req, function (e, entity) { 41 | if (e) { 42 | return res.odataError(e) 43 | } 44 | 45 | res.statusCode = 204 46 | cfg.addCorsToResponse(res) 47 | 48 | res.end() 49 | }) 50 | } catch (e) { 51 | res.odataError(e) 52 | } 53 | } 54 | 55 | module.exports = function (cfg, req, res) { 56 | if (req.body) { 57 | return processBody(req.body, cfg, req, res) 58 | } 59 | let body = '' 60 | req.on('data', function (data) { 61 | body += data 62 | if (body.length > 1e6) { 63 | req.connection.destroy() 64 | } 65 | }) 66 | req.on('end', function () { 67 | return processBody(JSON.parse(body), cfg, req, res) 68 | }) 69 | } 70 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "simple-odata-server", 3 | "version": "1.2.1", 4 | "description": "OData server with adapter for mongodb and nedb", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "standard && mocha --exit -R spec" 8 | }, 9 | "author": "Jan Blaha", 10 | "license": "MIT", 11 | "repository": "pofider/node-simple-odata-server", 12 | "keywords": [ 13 | "OData", 14 | "server", 15 | "mongodb", 16 | "nedb" 17 | ], 18 | "standard": { 19 | "ignore": "example/**", 20 | "env": { 21 | "mocha": true, 22 | "node": true 23 | } 24 | }, 25 | "dependencies": { 26 | "methods": "1.1.2", 27 | "odata-parser": "1.4.1", 28 | "path-to-regexp": "6.3.0", 29 | "safe-buffer": "5.2.1", 30 | "xmlbuilder": "15.1.1" 31 | }, 32 | "devDependencies": { 33 | "eslint": "8.19.0", 34 | "mocha": "10.0.0", 35 | "mongodb": "4.7.0", 36 | "nedb": "1.8.0", 37 | "should": "13.2.3", 38 | "standard": "17.0.0", 39 | "supertest": "6.2.4", 40 | "xml2js": "0.4.23" 41 | }, 42 | "files": [ 43 | "lib", 44 | "index.js" 45 | ] 46 | } 47 | -------------------------------------------------------------------------------- /test/metadataTest.js: -------------------------------------------------------------------------------- 1 | /* eslint-env mocha */ 2 | require('should') 3 | const model = require('./model.js') 4 | const metadata = require('../lib/metadata.js') 5 | const xml2js = require('xml2js') 6 | 7 | describe('metadata', function () { 8 | it('xml should be parseable', function (done) { 9 | const xml = metadata({ model }) 10 | 11 | xml2js.Parser().parseString(xml, function (err, data) { 12 | if (err) done(err) 13 | 14 | data['edmx:Edmx'].should.be.ok() 15 | data['edmx:Edmx'].$.Version.should.be.eql('4.0') 16 | 17 | data['edmx:Edmx']['edmx:DataServices'].should.be.ok() 18 | data['edmx:Edmx']['edmx:DataServices'][0].Schema.should.be.ok() 19 | data['edmx:Edmx']['edmx:DataServices'][0].Schema[0].should.be.ok() 20 | 21 | const schema = data['edmx:Edmx']['edmx:DataServices'][0].Schema[0] 22 | schema.$.Namespace.should.be.eql('jsreport') 23 | 24 | const entityType = schema.EntityType[0] 25 | entityType.should.be.ok() 26 | entityType.$.Name.should.be.eql('UserType') 27 | entityType.Key[0].PropertyRef[0].$.Name.should.be.eql('_id') 28 | entityType.Property[0].$.Name.should.be.eql('_id') 29 | entityType.Property[0].$.Type.should.be.eql('Edm.String') 30 | entityType.Property[0].$.Nullable.should.be.eql('false') 31 | entityType.Property[1].$.Name.should.be.eql('test') 32 | entityType.Property[1].$.Type.should.be.eql('Edm.String') 33 | entityType.Property[2].$.Name.should.be.eql('num') 34 | entityType.Property[2].$.Type.should.be.eql('Edm.Int32') 35 | entityType.Property[3].$.Name.should.be.eql('addresses') 36 | entityType.Property[3].$.Type.should.be.eql('Collection(jsreport.AddressType)') 37 | 38 | const complexType = schema.ComplexType[0] 39 | complexType.should.be.ok() 40 | complexType.$.Name.should.be.eql('AddressType') 41 | complexType.Property[0].$.Name.should.be.eql('street') 42 | complexType.Property[0].$.Type.should.be.eql('Edm.String') 43 | 44 | const entityContainer = schema.EntityContainer[0] 45 | entityContainer.should.be.ok() 46 | entityContainer.EntitySet[0].$.Name.should.be.eql('users') 47 | entityContainer.EntitySet[0].$.EntityType.should.be.eql('jsreport.UserType') 48 | 49 | done() 50 | }) 51 | }) 52 | }) 53 | -------------------------------------------------------------------------------- /test/model.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | namespace: 'jsreport', 3 | entityTypes: { 4 | UserType: { 5 | _id: { type: 'Edm.String', key: true, nullable: false }, 6 | test: { type: 'Edm.String' }, 7 | num: { type: 'Edm.Int32' }, 8 | addresses: { type: 'Collection(jsreport.AddressType)' }, 9 | image: { type: 'Edm.Binary' } 10 | } 11 | }, 12 | complexTypes: { 13 | AddressType: { 14 | street: { type: 'Edm.String' } 15 | } 16 | }, 17 | entitySets: { 18 | users: { 19 | entityType: 'jsreport.UserType' 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /test/odataServerTest.js: -------------------------------------------------------------------------------- 1 | /* eslint-env mocha */ 2 | require('should') 3 | const request = require('supertest') 4 | const http = require('http') 5 | const ODataServer = require('../index.js') 6 | const model = require('./model.js') 7 | 8 | describe('odata server', function () { 9 | let odataServer 10 | let server 11 | 12 | beforeEach(function () { 13 | odataServer = ODataServer('http://localhost:1234') 14 | odataServer.model(model) 15 | server = http.createServer(function (req, res) { 16 | odataServer.handle(req, res) 17 | }) 18 | }) 19 | 20 | it('get collection', function (done) { 21 | odataServer.query(function (col, query, req, cb) { 22 | cb(null, [{ 23 | test: 'a' 24 | }]) 25 | }) 26 | 27 | odataServer.on('odata-error', done) 28 | 29 | request(server) 30 | .get('/users') 31 | .expect('Content-Type', /application\/json/) 32 | .expect(200) 33 | .expect(function (res) { 34 | res.body.value.should.be.ok() 35 | res.body.value.length.should.be.eql(1) 36 | res.body.value[0].test.should.be.eql('a') 37 | }) 38 | .end(function (err, res) { 39 | done(err) 40 | }) 41 | }) 42 | 43 | it('get should ignore invalid query string', function (done) { 44 | odataServer.query(function (col, query, req, cb) { 45 | cb(null, [{ 46 | test: 'a' 47 | }]) 48 | }) 49 | 50 | odataServer.on('odata-error', done) 51 | 52 | request(server) 53 | .get('/users?foo=a') 54 | .expect('Content-Type', /application\/json/) 55 | .expect(200) 56 | .expect(function (res) { 57 | res.body.value.should.be.ok() 58 | res.body.value.length.should.be.eql(1) 59 | res.body.value[0].test.should.be.eql('a') 60 | }) 61 | .end(function (err, res) { 62 | done(err) 63 | }) 64 | }) 65 | 66 | it('get should prune properties', function (done) { 67 | odataServer.query(function (col, query, req, cb) { 68 | cb(null, [{ 69 | test: 'a', 70 | a: 'b' 71 | }]) 72 | }) 73 | 74 | odataServer.on('odata-error', done) 75 | 76 | request(server) 77 | .get('/users') 78 | .expect(200) 79 | .expect(function (res) { 80 | res.body.value.should.be.ok() 81 | res.body.value[0].should.have.property('test') 82 | res.body.value[0].should.not.have.property('a') 83 | }) 84 | .end(function (err, res) { 85 | done(err) 86 | }) 87 | }) 88 | 89 | it('get should always return response with header with odata.metadata=minimal', function (done) { 90 | odataServer.query(function (col, query, req, cb) { 91 | cb(null, [{ 92 | test: 'a', 93 | a: 'b' 94 | }]) 95 | }) 96 | 97 | odataServer.on('odata-error', done) 98 | request(server) 99 | .get('/users') 100 | .expect(200) 101 | .expect('Content-Type', /odata.metadata=minimal/) 102 | .end(function (err, res) { 103 | done(err) 104 | }) 105 | }) 106 | 107 | it('get $count should return number of entries', function (done) { 108 | odataServer.query(function (col, query, req, cb) { 109 | cb(null, 1) 110 | }) 111 | 112 | odataServer.on('odata-error', done) 113 | 114 | request(server) 115 | .get('/users/$count') 116 | .expect(200) 117 | .expect(function (res) { 118 | res.body.value.should.be.eql(1) 119 | }) 120 | .end(function (err, res) { 121 | done(err) 122 | }) 123 | }) 124 | 125 | it('get by _id', function (done) { 126 | odataServer.query(function (col, query, req, cb) { 127 | cb(null, [{ 128 | _id: '123', 129 | test: 'a' 130 | }]) 131 | }) 132 | 133 | request(server) 134 | .get('/users(\'123\')') 135 | .expect(200) 136 | .expect(function (res) { 137 | res.body.should.not.have.property('0') 138 | res.body.test.should.be.eql('a') 139 | Array.isArray(res.body.value).should.be.true() 140 | }) 141 | .end(function (err, res) { 142 | done(err) 143 | }) 144 | }) 145 | 146 | it('get should have the selection fields in its @odata.context if $select is passed', function (done) { 147 | const selectedField1 = 'num' 148 | const selectedField2 = 'image' 149 | const expectedResult = { 150 | context: 'http://localhost:1234/$metadata#users(' + selectedField1 + ',' + selectedField2 + ')' 151 | } 152 | odataServer.query(function (col, query, req, cb) { 153 | cb(null, [{ 154 | num: 1, 155 | a: 'b' 156 | }]) 157 | }) 158 | odataServer.on('odata-error', done) 159 | request(server) 160 | .get('/users?$select=' + selectedField1 + ',' + selectedField2) 161 | .expect(200) 162 | .expect(function (res) { 163 | res.body.value.should.be.ok() 164 | res.body.value[0].should.have.property('num') 165 | res.body.value[0].should.not.have.property('a') 166 | res.body['@odata.context'].should.be.eql(expectedResult.context) 167 | }) 168 | .end(function (err, res) { 169 | done(err) 170 | }) 171 | }) 172 | 173 | it('get should have the selection fields along with $entity in @odata.context for filtered query', function (done) { 174 | const key = 'someKey' 175 | const result = { 176 | num: 1 177 | } 178 | odataServer.query(function (col, query, req, cb) { 179 | cb(null, { 180 | num: 1 181 | }) 182 | }) 183 | 184 | odataServer.on('odata-error', done) 185 | request(server) 186 | .get('/users($' + key + ')?$select=num') 187 | .expect(200) 188 | .expect(function (res) { 189 | res.body.value.should.be.ok() 190 | res.body['@odata.context'].should.be.eql('http://localhost:1234/$metadata#users(num)/$entity') 191 | res.body.should.have.property('value') 192 | res.body.value.should.be.eql(result) 193 | }) 194 | .end(function (err, res) { 195 | done(err) 196 | }) 197 | }) 198 | 199 | it('post should prune properties', function (done) { 200 | odataServer.insert(function (collection, doc, req, cb) { 201 | doc.addresses[0].should.not.have.property('@odata.type') 202 | cb(null, { 203 | test: 'foo', 204 | _id: 'aa', 205 | a: 'a' 206 | }) 207 | }) 208 | 209 | request(server) 210 | .post('/users') 211 | .expect('Content-Type', /application\/json/) 212 | .send({ 213 | test: 'foo', 214 | addresses: [{ 215 | street: 'street', 216 | '@odata.type': 'jsreport.AddressType' 217 | }] 218 | }) 219 | .expect(201) 220 | .expect(function (res) { 221 | res.body.should.be.ok() 222 | res.body._id.should.be.ok() 223 | res.body.should.not.have.property('a') 224 | }) 225 | .end(function (err, res) { 226 | done(err) 227 | }) 228 | }) 229 | 230 | it('get should prune properties also with by id query', function (done) { 231 | odataServer.query(function (col, query, req, cb) { 232 | cb(null, [{ 233 | test: 'a', 234 | a: 'b', 235 | _id: 'foo' 236 | }]) 237 | }) 238 | 239 | odataServer.on('odata-error', done) 240 | 241 | request(server) 242 | .get("/users('foo')") 243 | .expect(200) 244 | .expect(function (res) { 245 | res.body.value.should.be.ok() 246 | res.body.value[0].should.have.property('test') 247 | res.body.value[0].should.not.have.property('a') 248 | }) 249 | .end(function (err, res) { 250 | done(err) 251 | }) 252 | }) 253 | 254 | it('get should prune properties also with count enabled', function (done) { 255 | odataServer.query(function (col, query, req, cb) { 256 | cb(null, { 257 | count: 1, 258 | value: [{ 259 | test: 'a', 260 | a: 'b' 261 | }] 262 | }) 263 | }) 264 | 265 | odataServer.on('odata-error', done) 266 | 267 | request(server) 268 | .get('/users?$count=true') 269 | .expect(200) 270 | .expect(function (res) { 271 | res.body.value.should.be.ok() 272 | res.body.value[0].should.have.property('test') 273 | res.body.value[0].should.not.have.property('a') 274 | }) 275 | .end(function (err, res) { 276 | done(err) 277 | }) 278 | }) 279 | 280 | it('get with error should be propagated to response', function (done) { 281 | odataServer.query(function (query, req, cb) { 282 | cb(new Error('test')) 283 | }) 284 | 285 | request(server) 286 | .get('/users') 287 | .expect(500) 288 | .end(function (err, res) { 289 | done(err) 290 | }) 291 | }) 292 | 293 | it('post document', function (done) { 294 | odataServer.insert(function (collection, doc, req, cb) { 295 | cb(null, { 296 | test: 'foo', 297 | _id: 'aa' 298 | }) 299 | }) 300 | 301 | request(server) 302 | .post('/users') 303 | .expect('Content-Type', /application\/json/) 304 | .send({ 305 | test: 'foo' 306 | }) 307 | .expect(201) 308 | .expect(function (res) { 309 | res.body.should.be.ok() 310 | res.body._id.should.be.ok() 311 | res.body.test.should.be.eql('foo') 312 | }) 313 | .end(function (err, res) { 314 | done(err) 315 | }) 316 | }) 317 | 318 | it('post with base64 should store buffer and return base64', function (done) { 319 | odataServer.insert(function (collection, doc, req, cb) { 320 | doc.image.should.be.instanceOf(Buffer) 321 | doc._id = 'xx' 322 | cb(null, doc) 323 | }) 324 | 325 | request(server) 326 | .post('/users') 327 | .expect('Content-Type', /application\/json/) 328 | .send({ 329 | image: 'aaaa' 330 | }) 331 | .expect(201) 332 | .expect(function (res) { 333 | res.body.should.be.ok() 334 | res.body.image.should.be.instanceOf(String) 335 | }) 336 | .end(function (err, res) { 337 | done(err) 338 | }) 339 | }) 340 | 341 | it('patch with base64 should store buffer', function (done) { 342 | odataServer.update(function (collection, query, update, req, cb) { 343 | update.$set.image.should.be.instanceOf(Buffer) 344 | cb(null) 345 | }) 346 | 347 | request(server) 348 | .patch("/users('1')") 349 | .send({ 350 | image: 'aaaa' 351 | }) 352 | .expect(204) 353 | .end(function (err, res) { 354 | done(err) 355 | }) 356 | }) 357 | 358 | it('post with error should be propagated to the response', function (done) { 359 | odataServer.insert(function (collection, doc, req, cb) { 360 | cb(new Error('test')) 361 | }) 362 | 363 | request(server) 364 | .post('/users') 365 | .send({ 366 | test: 'foo' 367 | }) 368 | .expect(500) 369 | .end(function (err, res) { 370 | done(err) 371 | }) 372 | }) 373 | 374 | it('patch document', function (done) { 375 | odataServer.update(function (collection, query, update, req, cb) { 376 | query._id.should.be.eql('1') 377 | update.$set.test.should.be.eql('foo') 378 | update.$set.addresses[0].should.not.have.property('@odata.type') 379 | cb(null, { 380 | test: 'foo' 381 | }) 382 | }) 383 | 384 | request(server) 385 | .patch("/users('1')") 386 | .send({ 387 | test: 'foo', 388 | addresses: [{ 389 | street: 'street', 390 | '@odata.type': 'jsreport.AddressType' 391 | }] 392 | }) 393 | .expect(204) 394 | .end(function (err, res) { 395 | done(err) 396 | }) 397 | }) 398 | 399 | it('patch error should be propagated to response', function (done) { 400 | odataServer.update(function (query, update, req, cb) { 401 | cb(new Error('test')) 402 | }) 403 | 404 | request(server) 405 | .patch('/users(1)') 406 | .send({ 407 | test: 'foo' 408 | }) 409 | .expect(500) 410 | .end(function (err, res) { 411 | done(err) 412 | }) 413 | }) 414 | 415 | it('delete document', function (done) { 416 | odataServer.remove(function (collection, query, req, cb) { 417 | cb(null) 418 | }) 419 | 420 | request(server) 421 | .delete("/users('1')") 422 | .expect(204) 423 | .end(function (err, res) { 424 | done(err) 425 | }) 426 | }) 427 | 428 | it('$metadata should response xml', function (done) { 429 | request(server) 430 | .get('/$metadata') 431 | .expect('Content-Type', /application\/xml/) 432 | .expect(200) 433 | .end(function (err, res) { 434 | done(err) 435 | }) 436 | }) 437 | 438 | it('/ should response collections json', function (done) { 439 | request(server) 440 | .get('/') 441 | .expect('Content-Type', /application\/json/) 442 | .expect(200) 443 | .expect(function (res) { 444 | res.body.value.length.should.be.eql(1) 445 | res.body.value[0].name.should.be.eql('users') 446 | res.body.value[0].name.should.be.eql('users') 447 | res.body.value[0].kind.should.be.eql('EntitySet') 448 | }) 449 | .end(function (err, res) { 450 | done(err) 451 | }) 452 | }) 453 | 454 | it('executeQuery should fire beforeQuery listener', function (done) { 455 | odataServer.beforeQuery(function (col, query, req, cb) { 456 | col.should.be.eql('users') 457 | query.isQuery.should.be.ok() 458 | req.isReq.should.be.ok() 459 | cb.should.be.a.Function() 460 | done() 461 | }) 462 | 463 | odataServer.executeQuery('users', { 464 | isQuery: true 465 | }, { 466 | isReq: true 467 | }, function () {}) 468 | }) 469 | 470 | it('executeQuery should fire beforeQuery listener when no request param is accepted', function (done) { 471 | odataServer.beforeQuery(function (col, query, req, cb) { 472 | col.should.be.eql('users') 473 | query.isQuery.should.be.ok() 474 | cb.should.be.a.Function() 475 | done() 476 | }) 477 | 478 | odataServer.executeQuery('users', { 479 | isQuery: true 480 | }, { 481 | isReq: true 482 | }, function () {}) 483 | }) 484 | 485 | it('executeInsert should fire beforeInsert listener', function (done) { 486 | odataServer.beforeInsert(function (col, doc, req, cb) { 487 | col.should.be.eql('users') 488 | doc.isDoc.should.be.ok() 489 | req.isReq.should.be.ok() 490 | cb.should.be.a.Function() 491 | done() 492 | }) 493 | 494 | odataServer.executeInsert('users', { 495 | isDoc: true 496 | }, { 497 | isReq: true 498 | }, function () {}) 499 | }) 500 | 501 | it('executeInsert should fire beforeInsert listener when no request param is accepted', function (done) { 502 | odataServer.beforeInsert(function (col, doc, cb) { 503 | col.should.be.eql('users') 504 | doc.isDoc.should.be.ok() 505 | cb.should.be.a.Function() 506 | done() 507 | }) 508 | 509 | odataServer.executeInsert('users', { 510 | isDoc: true 511 | }, { 512 | isReq: true 513 | }, function () {}) 514 | }) 515 | 516 | it('executeRemove should fire beforeRemove listener', function (done) { 517 | odataServer.beforeRemove(function (col, query, req, cb) { 518 | col.should.be.eql('users') 519 | query.isQuery.should.be.ok() 520 | req.isReq.should.be.ok() 521 | cb.should.be.a.Function() 522 | done() 523 | }) 524 | 525 | odataServer.executeRemove('users', { 526 | isQuery: true 527 | }, { 528 | isReq: true 529 | }, function () {}) 530 | }) 531 | 532 | it('executeRemove should fire beforeRemove listener when no request param is accepted', function (done) { 533 | odataServer.beforeRemove(function (col, query, cb) { 534 | col.should.be.eql('users') 535 | query.isQuery.should.be.ok() 536 | cb.should.be.a.Function() 537 | done() 538 | }) 539 | 540 | odataServer.executeRemove('users', { 541 | isQuery: true 542 | }, { 543 | isReq: true 544 | }, function () {}) 545 | }) 546 | 547 | it('executeUpdate should fire beforeUpdate listener', function (done) { 548 | odataServer.beforeUpdate(function (col, query, update, req, cb) { 549 | col.should.be.eql('users') 550 | query.isQuery.should.be.ok() 551 | update.isUpdate.should.be.ok() 552 | req.isReq.should.be.ok() 553 | cb.should.be.a.Function() 554 | done() 555 | }) 556 | 557 | odataServer.executeUpdate('users', { 558 | isQuery: true 559 | }, { 560 | isUpdate: true 561 | }, { 562 | isReq: true 563 | }, function () {}) 564 | }) 565 | 566 | it('executeUpdate should fire beforeUpdate listener when no request param is accepted', function (done) { 567 | odataServer.beforeUpdate(function (col, query, update, cb) { 568 | col.should.be.eql('users') 569 | query.isQuery.should.be.ok() 570 | update.isUpdate.should.be.ok() 571 | cb.should.be.a.Function() 572 | done() 573 | }) 574 | 575 | odataServer.executeUpdate('users', { 576 | isQuery: true 577 | }, { 578 | isUpdate: true 579 | }, { 580 | isReq: true 581 | }, function () {}) 582 | }) 583 | }) 584 | 585 | describe('odata server with cors', function () { 586 | let odataServer 587 | let server 588 | 589 | it('options on * should response 200 with Access-Control-Allow-Origin', function (done) { 590 | odataServer = ODataServer('http://localhost:1234') 591 | odataServer.model(model).cors('test.com') 592 | server = http.createServer(function (req, res) { 593 | odataServer.handle(req, res) 594 | }) 595 | 596 | request(server) 597 | .options('/$metadata') 598 | .expect('Access-Control-Allow-Origin', /test.com/) 599 | .expect(200) 600 | .end(function (err, res) { 601 | done(err) 602 | }) 603 | }) 604 | 605 | it('get on * should response 200 with Access-Control-Allow-Origin', function (done) { 606 | odataServer = ODataServer('http://localhost:1234') 607 | odataServer.model(model).cors('test.com') 608 | server = http.createServer(function (req, res) { 609 | odataServer.handle(req, res) 610 | }) 611 | odataServer.query(function (collection, query, req, cb) { 612 | cb(null) 613 | }) 614 | 615 | request(server) 616 | .get('/users') 617 | .expect('Access-Control-Allow-Origin', /test.com/) 618 | .expect(200) 619 | .end(function (err, res) { 620 | done(err) 621 | }) 622 | }) 623 | 624 | it('post on * should response 200 with Access-Control-Allow-Origin', function (done) { 625 | odataServer = ODataServer('http://localhost:1234') 626 | odataServer.model(model).cors('test.com') 627 | server = http.createServer(function (req, res) { 628 | odataServer.handle(req, res) 629 | }) 630 | odataServer.query(function (collection, query, req, cb) { 631 | cb(null) 632 | }) 633 | 634 | odataServer.insert(function (collection, doc, req, cb) { 635 | cb(null, { 636 | test: 'foo', 637 | _id: 'aa', 638 | a: 'a' 639 | }) 640 | }) 641 | 642 | request(server) 643 | .post('/users') 644 | .expect('Access-Control-Allow-Origin', /test.com/) 645 | .send({ 646 | test: 'foo' 647 | }) 648 | .expect(201) 649 | .end(function (err, res) { 650 | done(err) 651 | }) 652 | }) 653 | 654 | it('delete on * should response 200 with Access-Control-Allow-Origin', function (done) { 655 | odataServer = ODataServer('http://localhost:1234') 656 | odataServer.model(model).cors('test.com') 657 | server = http.createServer(function (req, res) { 658 | odataServer.handle(req, res) 659 | }) 660 | odataServer.remove(function (collection, query, req, cb) { 661 | cb(null) 662 | }) 663 | 664 | request(server) 665 | .delete("/users('1')") 666 | .expect('Access-Control-Allow-Origin', /test.com/) 667 | .expect(204) 668 | .end(function (err, res) { 669 | done(err) 670 | }) 671 | }) 672 | 673 | it('patch on * should response 200 with Access-Control-Allow-Origin', function (done) { 674 | odataServer = ODataServer('http://localhost:1234') 675 | odataServer.model(model).cors('test.com') 676 | server = http.createServer(function (req, res) { 677 | odataServer.handle(req, res) 678 | }) 679 | 680 | odataServer.update(function (collection, query, update, req, cb) { 681 | query._id.should.be.eql('1') 682 | update.$set.test.should.be.eql('foo') 683 | cb(null, { 684 | test: 'foo' 685 | }) 686 | }) 687 | 688 | request(server) 689 | .patch("/users('1')") 690 | .send({ 691 | test: 'foo' 692 | }) 693 | .expect('Access-Control-Allow-Origin', /test.com/) 694 | .expect(204) 695 | .end(function (err, res) { 696 | done(err) 697 | }) 698 | }) 699 | }) 700 | -------------------------------------------------------------------------------- /test/pruneTest.js: -------------------------------------------------------------------------------- 1 | /* eslint-env mocha */ 2 | require('should') 3 | const prune = require('../lib/prune.js') 4 | 5 | describe('prune', function () { 6 | let model 7 | 8 | beforeEach(function () { 9 | model = { 10 | namespace: 'jsreport', 11 | entityTypes: { 12 | UserType: { 13 | _id: { 14 | type: 'Edm.String', 15 | key: true 16 | }, 17 | test: { 18 | type: 'Edm.String' 19 | }, 20 | addresses: { 21 | type: 'Collection(jsreport.AddressType)' 22 | }, 23 | address: { 24 | type: 'jsreport.AddressType' 25 | }, 26 | nested: { 27 | type: 'jsreport.NestedType' 28 | } 29 | } 30 | }, 31 | complexTypes: { 32 | AddressType: { 33 | street: { 34 | type: 'Edm.String' 35 | } 36 | }, 37 | NestedInnerType: { 38 | name: { 39 | type: 'Edm.String' 40 | } 41 | }, 42 | NestedType: { 43 | items: { 44 | type: 'jsreport.NestedInnerType' 45 | } 46 | } 47 | }, 48 | entitySets: { 49 | users: { 50 | entityType: 'jsreport.UserType' 51 | } 52 | } 53 | } 54 | }) 55 | 56 | it('should remove properties not specified in entity type', function () { 57 | const doc = { 58 | test: 'x', 59 | a: 'a' 60 | } 61 | prune(model, 'users', doc) 62 | doc.should.not.have.property('a') 63 | doc.should.have.property('test') 64 | }) 65 | 66 | it('should accept arrays on input', function () { 67 | const doc = { 68 | test: 'x', 69 | a: 'a' 70 | } 71 | prune(model, 'users', [doc]) 72 | doc.should.not.have.property('a') 73 | doc.should.have.property('test') 74 | }) 75 | 76 | it('should prune also in nested complex types', function () { 77 | const doc = { 78 | address: { 79 | street: 'street', 80 | a: 'a' 81 | } 82 | } 83 | prune(model, 'users', doc) 84 | 85 | doc.should.have.property('address') 86 | doc.address.should.have.property('street') 87 | doc.address.should.not.have.property('a') 88 | }) 89 | 90 | it('should not remove nested complex type when pruning', function () { 91 | const doc = { 92 | nested: { 93 | items: [{ 94 | name: 'foo' 95 | }] 96 | } 97 | } 98 | prune(model, 'users', doc) 99 | 100 | doc.should.have.property('nested') 101 | doc.nested.should.have.property('items') 102 | doc.nested.items.should.have.length(1) 103 | doc.nested.items[0].should.have.property('name') 104 | doc.nested.items[0].name.should.be.eql('foo') 105 | }) 106 | it('should not prune prefixes with @odata', function () { 107 | const doc = { 108 | '@odata.type': 'someEntityType', 109 | '@odata.id': 'someOdataEndpoint/setName/(key)', 110 | '@odata.editLink': "setName('key')", 111 | test: 'x' 112 | } 113 | prune(model, 'users', doc) 114 | doc.should.have.property('@odata.type') 115 | doc.should.have.property('@odata.id') 116 | doc.should.have.property('@odata.editLink') 117 | doc.should.have.property('test') 118 | }) 119 | }) 120 | -------------------------------------------------------------------------------- /test/queryTransformTest.js: -------------------------------------------------------------------------------- 1 | /* eslint-env mocha */ 2 | const should = require('should') 3 | const transform = require('../lib/queryTransform.js') 4 | 5 | describe('transform', function () { 6 | it('$top to $limit', function () { 7 | transform({ 8 | $top: 5 9 | }).$limit.should.be.eql(5) 10 | }) 11 | 12 | it('$orderby to $sort asc', function () { 13 | transform({ 14 | $orderby: [{ test: 'asc' }] 15 | }).$sort.test.should.be.eql(1) 16 | }) 17 | 18 | it('$orderby to $sort desc', function () { 19 | transform({ 20 | $orderby: [{ test: 'desc' }] 21 | }).$sort.test.should.be.eql(-1) 22 | }) 23 | 24 | it("Name eq 'John' and LastName lt 'Doe", function () { 25 | const result = transform({ 26 | $filter: { 27 | type: 'and', 28 | left: { 29 | type: 'eq', 30 | left: { 31 | type: 'property', 32 | name: 'Name' 33 | }, 34 | right: { 35 | type: 'literal', 36 | value: 'John' 37 | } 38 | }, 39 | right: { 40 | type: 'lt', 41 | left: { 42 | type: 'property', 43 | name: 'LastName' 44 | }, 45 | right: { 46 | type: 'literal', 47 | value: 'Doe' 48 | } 49 | } 50 | } 51 | }) 52 | 53 | result.$filter.$and.should.be.ok() 54 | result.$filter.$and.length.should.be.eql(2) 55 | result.$filter.$and[0].Name.should.be.eql('John') 56 | result.$filter.$and[1].LastName.$lt.should.be.eql('Doe') 57 | }) 58 | 59 | it("Name eq 'John' or LastName gt 'Doe", function () { 60 | const result = transform({ 61 | $filter: { 62 | type: 'or', 63 | left: { 64 | type: 'eq', 65 | left: { 66 | type: 'property', 67 | name: 'Name' 68 | }, 69 | right: { 70 | type: 'literal', 71 | value: 'John' 72 | } 73 | }, 74 | right: { 75 | type: 'gt', 76 | left: { 77 | type: 'property', 78 | name: 'LastName' 79 | }, 80 | right: { 81 | type: 'literal', 82 | value: 'Doe' 83 | } 84 | } 85 | } 86 | }) 87 | 88 | result.$filter.$or.should.be.ok() 89 | result.$filter.$or.length.should.be.eql(2) 90 | result.$filter.$or[0].Name.should.be.eql('John') 91 | result.$filter.$or[1].LastName.$gt.should.be.eql('Doe') 92 | }) 93 | 94 | it('$filter substringof to regex', function () { 95 | transform({ 96 | $filter: { 97 | type: 'functioncall', 98 | func: 'substringof', 99 | args: [{ type: 'literal', value: 'foo' }, { type: 'property', name: 'data' }] 100 | } 101 | }).$filter.data.should.be.eql(/foo/) 102 | }) 103 | 104 | it('$select should create mongo style projection', function () { 105 | const query = transform({ 106 | $select: ['foo', 'x', '_id'] 107 | }) 108 | query.$select.should.have.property('_id') 109 | query.$select.should.have.property('x') 110 | }) 111 | 112 | it('$filter on null value', function () { 113 | const query = transform({ 114 | $filter: { 115 | type: 'eq', 116 | left: { 117 | type: 'property', 118 | name: 'foo' 119 | }, 120 | right: { 121 | type: 'literal', 122 | // odata parser returns this shape when doing a filter with null 123 | // $filter=field eq null 124 | value: ['null', ''] 125 | } 126 | } 127 | }) 128 | query.$filter.should.have.property('foo') 129 | should(query.$filter.foo).be.null() 130 | }) 131 | 132 | it('$filter on nested property', function () { 133 | const result = transform({ 134 | $filter: { 135 | type: 'eq', 136 | left: { 137 | type: 'property', 138 | name: 'address/street' 139 | }, 140 | right: { 141 | type: 'literal', 142 | value: 'foo' 143 | } 144 | } 145 | }) 146 | result.$filter.address.street.should.be.eql('foo') 147 | }) 148 | }) 149 | --------------------------------------------------------------------------------