├── .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 | [](https://npmjs.com/package/simple-odata-server)
6 | [](http://opensource.org/licenses/MIT)
7 | [](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 |
--------------------------------------------------------------------------------