├── .babelrc ├── .eslintignore ├── .eslintrc ├── .gitignore ├── .npmignore ├── .travis.yml ├── CHANGELOG.md ├── LICENSE ├── Makefile ├── README.md ├── _.md ├── examples ├── complex-resource │ └── index.js ├── hidden-field │ └── index.js ├── multi-resource │ ├── functions │ │ ├── license.js │ │ └── server-time.js │ ├── index.js │ └── resources │ │ ├── book.js │ │ └── user.js ├── simple-with-data │ └── index.js └── simple │ └── index.js ├── index.js ├── package-lock.json ├── package.json ├── src ├── ODataFunction.js ├── ODataResource.js ├── db │ ├── db.js │ ├── idPlugin.js │ └── model.js ├── express.js ├── index.js ├── metadata │ ├── ODataMetadata.js │ └── xmlWriter.js ├── model │ ├── idPlugin.js │ └── index.js ├── parser │ ├── countParser.js │ ├── filterParser.js │ ├── functionsParser.js │ ├── orderbyParser.js │ ├── selectParser.js │ ├── skipParser.js │ └── topParser.js ├── pipes.js ├── rest │ ├── delete.js │ ├── get.js │ ├── index.js │ ├── list.js │ ├── patch.js │ ├── post.js │ └── put.js ├── server.js └── utils.js └── test ├── api.Function.js ├── api.Resource.js ├── hook.all.after.js ├── hook.all.before.js ├── hook.delete.after.js ├── hook.delete.before.js ├── hook.get.after.js ├── hook.get.before.js ├── hook.list.after.js ├── hook.list.before.js ├── hook.post.after.js ├── hook.post.before.js ├── hook.put.after.js ├── hook.put.before.js ├── metadata.action..js ├── metadata.format.js ├── metadata.function.js ├── metadata.resource.complex.js ├── model.complex.action.js ├── model.complex.filter.js ├── model.complex.js ├── model.custom.id.js ├── model.hidden.field.js ├── model.special.name.js ├── odata.actions.js ├── odata.functions.js ├── odata.query.count.js ├── odata.query.filter.functions.js ├── odata.query.filter.js ├── odata.query.orderby.js ├── odata.query.select.js ├── odata.query.skip.js ├── odata.query.top.js ├── options.maxSkip.js ├── options.maxTop.js ├── options.prefix.js ├── rest.delete.js ├── rest.get.js ├── rest.post.js ├── rest.put.js ├── support ├── books.json ├── fake-db-model.js ├── fake-db.js └── setup.js └── utils.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["@babel/preset-env"], 3 | "plugins": [ 4 | "@babel/plugin-syntax-dynamic-import", 5 | "@babel/plugin-syntax-import-meta", 6 | "@babel/plugin-proposal-class-properties", 7 | "@babel/plugin-proposal-json-strings", 8 | "@babel/plugin-proposal-function-sent", 9 | "@babel/plugin-proposal-export-namespace-from", 10 | "@babel/plugin-proposal-numeric-separator", 11 | "@babel/plugin-proposal-throw-expressions", 12 | "@babel/plugin-proposal-export-default-from", 13 | "@babel/plugin-proposal-logical-assignment-operators", 14 | "@babel/plugin-proposal-optional-chaining", 15 | [ 16 | "@babel/plugin-proposal-pipeline-operator", 17 | { 18 | "proposal": "minimal" 19 | } 20 | ], 21 | "@babel/plugin-proposal-nullish-coalescing-operator", 22 | "@babel/plugin-proposal-do-expressions", 23 | "@babel/plugin-proposal-function-bind" 24 | ] 25 | } -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zackyang000/node-odata/4ab8d31f312db840f03f27512898a8906b12857c/.eslintignore -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "parser": "@babel/eslint-parser", 3 | "extends": "airbnb/base", 4 | "env": { 5 | "browser": true, 6 | "node": true, 7 | "mocha": true, 8 | "es6": true 9 | }, 10 | "rules": { 11 | "generator-star-spacing": 0, 12 | "no-underscore-dangle": 0 13 | }, 14 | "plugins": [ 15 | "babel" 16 | ] 17 | } 18 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | coverage/ 3 | npm-debug.log 4 | nohup.out 5 | lib/ 6 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | coverage/ 3 | npm-debug.log 4 | nohup.out 5 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | 3 | node_js: 4 | - "4" 5 | 6 | services: 7 | - mongodb 8 | 9 | before_script: 10 | - npm install --quiet 11 | 12 | script: 13 | - make lint compile test-cov 14 | 15 | after_script: 16 | - npm install coveralls@2.10.0 && cat ./coverage/lcov.info | coveralls 17 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | 0.7.12 (2016-05-23) 2 | =================== 3 | - Fixed return value have no id when put resource. 4 | 5 | 0.7.11 (2016-05-04) 6 | =================== 7 | Fixed update with PUT not working when use a complex model. [#55](https://github.com/TossShinHwa/node-odata/issues/55) 8 | 9 | 0.7.10 (2016-04-09) 10 | =================== 11 | - Support null value for query. [#52](https://github.com/TossShinHwa/node-odata/issues/52) 12 | 13 | 0.7.9 (2016-04-08) 14 | =================== 15 | - Fixed If inside an 'and' expression, a property contains the string 'and' (like 'brand'), it crashes. [#49](https://github.com/TossShinHwa/node-odata/issues/49) 16 | 17 | 0.7.8 (2016-01-20) 18 | =================== 19 | - Fixed require's file name case for case-sensitive filesystems. [#46](https://github.com/TossShinHwa/node-odata/issues/46) 20 | 21 | 0.7.7 (2016-01-20) 22 | =================== 23 | - Fixed http state code not be correct when mongodb throw a internal error. [#45](https://github.com/TossShinHwa/node-odata/issues/45) 24 | 25 | 0.7.6 (2015-07-30) 26 | =================== 27 | - Fixed it can not be saved,when edit the entity at put-before-hook. [#37](https://github.com/TossShinHwa/node-odata/issues/37) 28 | - Fixed parse error details can not be displayed on the client. 29 | 30 | 0.7.5 (2015-07-23) 31 | =================== 32 | - Support add muti-hooks for one method of resources. 33 | - Hooks resource.all be implemented. [#34](https://github.com/TossShinHwa/node-odata/issues/34) 34 | - Fixed hidden-field not work when use $select to query resource. [#33](https://github.com/TossShinHwa/node-odata/issues/33) 35 | 36 | 0.7.4 (2015-07-16) 37 | =================== 38 | - Improved API to config resource: from `resource.getAll()` to `resource.list()`. 39 | - Fixed `resource.getAll()` does not work. 40 | 41 | 0.7.3 (2015-07-15) 42 | =================== 43 | - Improved API to use mongoose: from `server.repository.get(name)` to `server.resources.name`. 44 | 45 | 0.7.2 (2015-07-14) 46 | =================== 47 | - Add `Resource` and `Function` object for odata. 48 | 49 | 0.7.1 (2015-07-12) 50 | =================== 51 | - Fixed `put/post/delete` of functions does not work. 52 | 53 | 0.7.0 (2015-07-10) 54 | =================== 55 | - Improved regist resource's API to fluent API. ([#3](https://github.com/TossShinHwa/node-odata/issues/3), [#22](https://github.com/TossShinHwa/node-odata/issues/22)) 56 | - Fix function named `before` of resource will not be execute. ([#31](https://github.com/TossShinHwa/node-odata/issues/31)) 57 | - Add `.url(url)` for set a url, different of resource name. ([#26](https://github.com/TossShinHwa/node-odata/issues/26)) 58 | - Change default url prefix form `/odata` to `/`. 59 | - Modify url of resource to standard format. from `/resource/:id` to `/resource(:id)`. ([#2](https://github.com/TossShinHwa/node-odata/issues/2)) 60 | 61 | 0.6.0 (2015-05-30) 62 | =================== 63 | - **Convert project language from CoffeeScript to ECMAScript6.** 64 | - Default support cors. ([#16](https://github.com/TossShinHwa/node-odata/issues/16)) 65 | - Allow put to create entity. ([#18](https://github.com/TossShinHwa/node-odata/issues/18)) 66 | 67 | 0.5.0 (2015-05-08) 68 | =================== 69 | - Add detail links for metadata info. 70 | - Add simple API for functions. ([#7](https://github.com/TossShinHwa/node-odata/issues/7)) 71 | 72 | 0.4.0 (2015-04-09) 73 | =================== 74 | - Optimized initialization method: from `odata` to `odata()`. 75 | - Fix maxTop and maxSkip of global-limit in options is not work. ([#14](https://github.com/TossShinHwa/node-odata/issues/14)) 76 | 77 | 0.3.0 (2015-01-20) 78 | =================== 79 | - Support the use of complex objects to define resource. 80 | - Remove mongoose field: `__v`. ([#6](https://github.com/TossShinHwa/node-odata/issues/6)) 81 | - Edit field `_id` to `id`. ([#5](https://github.com/TossShinHwa/node-odata/issues/5)) 82 | - Fix build-in query function's keyword can't use in field of resource or get error when this field. ([#9](https://github.com/TossShinHwa/node-odata/issues/9)) 83 | 84 | 0.2.0 (2015-01-04) 85 | =================== 86 | - Improved regist resource's API. 87 | - Wrap 'Express' module. 88 | 89 | 0.1.0 (2014-10-16) 90 | =================== 91 | - OData query supported more keywords include: `$count`. 92 | - $filter supported more keywords include: `indexof`, `year`. 93 | - Added example. 94 | - Added test case. 95 | 96 | 0.0.1 (2014-10-05) 97 | =================== 98 | - Full CRUD supported. 99 | - OData query supported keywords include: `$filter`, `$select`, `$top`, `$skip`, `$orderby`. 100 | - $filter supported keywords include: `eq`, `ne`, `lt`, `le`, `gt`, `ge`, `and`. 101 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014-2016 Zack Yang 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 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | REPORTER = dot 2 | 3 | .PHONY: run compile test test-cov lint 4 | 5 | run: lint compile test 6 | 7 | lint: 8 | node_modules/.bin/eslint src/ 9 | 10 | compile: 11 | node_modules/.bin/babel src --out-dir lib 12 | 13 | test: 14 | @node_modules/.bin/mocha\ 15 | --require @babel/register \ 16 | --reporter $(REPORTER) \ 17 | test/*.js 18 | 19 | test-cov: 20 | @node node_modules/istanbul/lib/cli.js cover -x '**/examples/**' \ 21 | ./node_modules/mocha/bin/_mocha test/*.js -- \ 22 | --require @babel/register \ 23 | --reporter $(REPORTER) \ 24 | test/*.js \ 25 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | node-odata 2 | ========== 3 | 4 | Create awesome REST APIs abide by [OData Protocol v4](http://www.odata.org/). Its purpose is to easier to creating APIs, make you more focus on business logic. 5 | 6 | [![NPM Version](https://img.shields.io/npm/v/node-odata.svg?style=flat)](https://www.npmjs.org/package/node-odata) 7 | [![npm](https://img.shields.io/npm/dm/node-odata.svg?style=flat)](https://www.npmjs.org/package/node-odata) 8 | [![Build Status](https://travis-ci.org/zackyang000/node-odata.svg?branch=master)](https://travis-ci.org/zackyang000/node-odata) 9 | [![Coverage Status](https://coveralls.io/repos/github/zackyang000/node-odata/badge.svg?branch=master)](https://coveralls.io/github/zackyang000/node-odata?branch=master) 10 | [![Dependency Status](https://david-dm.org/zackyang000/node-odata.svg?style=flat)](https://david-dm.org/zackyang000/node-odata) 11 | [![License](http://img.shields.io/npm/l/node-odata.svg?style=flat)](https://raw.githubusercontent.com/zackyang000/node-odata/master/LICENSE) 12 | 13 | ```JavaScript 14 | var odata = require('node-odata'); 15 | 16 | var server = odata('mongodb://localhost/my-app'); 17 | 18 | server.resource('books', { 19 | title: String, 20 | price: Number 21 | }); 22 | 23 | server.listen(3000); 24 | ``` 25 | 26 | Registers the following routes: 27 | 28 | ``` 29 | GET /books 30 | GET /books(:id) 31 | POST /books 32 | PUT /books(:id) 33 | DELETE /books(:id) 34 | GET /books/$metadata 35 | ``` 36 | 37 | Use the following OData query: 38 | 39 | ``` 40 | Example 41 | GET /books?$select=id, title 42 | GET /books?$top=3&$skip=2 43 | GET /books?$orderby=price desc 44 | GET /books?$filter=price gt 10 45 | GET /books/$metadata 46 | GET ... 47 | ``` 48 | 49 | ### Further options 50 | 51 | The odata constructor takes 3 arguments: ```odata(, , );``` 52 | 53 | The options object currently only supports one parameter: ```expressRequestLimit```, this will be parsed to the express middelware as the "limit" option, which allows for configuring express to support larger requests. It can be either a number or a string like "50kb", 20mb", etc. 54 | 55 | 56 | ## Current State 57 | 58 | node-odata is currently at an beta stage, it is stable but not 100% feature complete. 59 | node-odata is written by ECMAScript 6 then compiled by [babel](https://babeljs.io/). 60 | It currently have to dependent on MongoDB yet. 61 | The current target is to add more features (eg. $metadata) and make to support other database. (eg. MySQL, PostgreSQL). 62 | 63 | ## Installation 64 | 65 | ``` 66 | npm install node-odata 67 | ``` 68 | 69 | 70 | ## DOCUMENTATION 71 | 72 | - [ENGLISH](http://zackyang000.github.io/node-odata/en/) 73 | - [中文](http://zackyang000.github.io/node-odata/cn/) 74 | 75 | 76 | ## Demo 77 | 78 | [Live demo](http://books.zackyang.com/book) and try it: 79 | 80 | * GET [/books?$select=id, title](http://books.zackyang.com/books?$select=id,%20title) 81 | * GET [/books?$top=3&$skip=2](http://books.zackyang.com/books?$top=3&$skip=2) 82 | * GET [/books?$orderby=price desc](http://books.zackyang.com/books?$orderby=price%20desc) 83 | * GET [/books?$filter=price gt 10](http://books.zackyang.com/books?$filter=price%20gt%2010) 84 | * GET [/books/$metadata](http://books.zackyang.com/books/$metadata) 85 | 86 | ## Support Feature 87 | 88 | * [x] Full CRUD Support 89 | * [x] $count 90 | * [x] $filter 91 | * [x] Comparison Operators 92 | * [x] eq 93 | * [x] ne 94 | * [x] lt 95 | * [x] le 96 | * [x] gt 97 | * [x] ge 98 | * [ ] Logical Operators 99 | * [x] and 100 | * [x] or 101 | * [ ] not 102 | * [ ] Comparison Operators 103 | * [ ] has 104 | * [ ] String Functions 105 | * [x] indexof 106 | * [x] contains 107 | * [ ] endswith 108 | * [ ] startswith 109 | * [ ] length 110 | * [ ] substring 111 | * [ ] tolower 112 | * [ ] toupper 113 | * [ ] trim 114 | * [ ] concat 115 | * [ ] Arithmetic Operators 116 | * [ ] add 117 | * [ ] sub 118 | * [ ] mul 119 | * [ ] div 120 | * [ ] mod 121 | * [ ] Date Functions 122 | * [x] year 123 | * [ ] month 124 | * [ ] day 125 | * [ ] hour 126 | * [ ] minute 127 | * [ ] second 128 | * [ ] fractionalseconds 129 | * [ ] date 130 | * [ ] time 131 | * [ ] totaloffsetminutes 132 | * [ ] now 133 | * [ ] mindatetime 134 | * [ ] maxdatetime 135 | * [ ] Math Functions 136 | * [ ] round 137 | * [ ] floor 138 | * [ ] ceiling 139 | * [x] $select 140 | * [x] $top 141 | * [x] $skip 142 | * [x] $orderby 143 | * [ ] $expand 144 | * [x] $metadata generation 145 | 146 | 147 | ## CONTRIBUTING 148 | 149 | We always welcome contributions to help make node-odata better. Please feel free to contribute to this project. The package-lock.json file was last created with node version 16.14.2. 150 | 151 | 152 | ## LICENSE 153 | 154 | node-odata is licensed under the MIT license. See [LICENSE](LICENSE) for more information. 155 | -------------------------------------------------------------------------------- /_.md: -------------------------------------------------------------------------------- 1 | # Plan to split project 2 | 3 | - node-data 4 | - standalone-node-odata 5 | - express-node-odata 6 | - koa-node-odata 7 | - egg-node-odata 8 | - node-odata-core 9 | - node-odata-parser 10 | - node-odata-metadata 11 | - node-odata-adapter 12 | - node-odata-mongodb 13 | 14 | 15 | # next version to write 16 | 17 | ```js 18 | import { Resource } from 'node-odata'; 19 | 20 | const model = { 21 | title: String, 22 | price: Number, 23 | }; 24 | 25 | function checkUserAuth() {} 26 | function router() {} 27 | function queryable() {} 28 | 29 | export default class Book extends Resource { 30 | constructor() { 31 | super('book', model); 32 | } 33 | 34 | @queryable({ pageSize: 10, maxTop: 100 }) 35 | async onGetList(next) { 36 | const entities = await next(); 37 | } 38 | 39 | async onGet(next) { 40 | // do something before 41 | const entity = await next(); 42 | // dosomething after 43 | } 44 | 45 | async onCreate(next) { 46 | const entity = await next(); 47 | } 48 | 49 | @checkUserAuth 50 | async onRemove(next) { 51 | const entity = await next(); 52 | } 53 | 54 | @checkUserAuth 55 | async onUpdate(next) { 56 | const entity = await next(); 57 | } 58 | 59 | @router['/50off'] 60 | async halfPriceAction(id, query) { 61 | const entity = await this.findOneById(id); 62 | entity.price /= 2; 63 | await entity.save(); 64 | return entity; 65 | } 66 | } 67 | ``` 68 | -------------------------------------------------------------------------------- /examples/complex-resource/index.js: -------------------------------------------------------------------------------- 1 | var odata = require('../../'); 2 | 3 | server = odata('mongodb://localhost/odata-test'); 4 | 5 | var order = { 6 | custom: { 7 | id: String, 8 | name: String 9 | }, 10 | orderItems: [{ 11 | quantity: Number, 12 | product: { 13 | id: String, 14 | name: String, 15 | price: Number 16 | } 17 | }] 18 | }; 19 | 20 | server.resource('orders', order); 21 | 22 | server.listen(3000, function(){ 23 | console.log('OData services has started, you can visit by http://localhost:3000/orders'); 24 | }); 25 | -------------------------------------------------------------------------------- /examples/hidden-field/index.js: -------------------------------------------------------------------------------- 1 | var odata = require('../../'); 2 | 3 | server = odata('mongodb://localhost/odata-test'); 4 | 5 | server.resource('users', { 6 | name: String, 7 | password: { 8 | type: String, 9 | select: false 10 | } 11 | }); 12 | 13 | server.listen(3000, function(){ 14 | console.log('OData services has started, you can visit by http://localhost:3000/users'); 15 | }); 16 | 17 | -------------------------------------------------------------------------------- /examples/multi-resource/functions/license.js: -------------------------------------------------------------------------------- 1 | var func = require('../../../').Function; 2 | 3 | var router = func(); 4 | 5 | router.get('/license', function(req, res, next) { 6 | res.jsonp({ license: 'MIT' }); 7 | }); 8 | 9 | module.exports = router; 10 | 11 | -------------------------------------------------------------------------------- /examples/multi-resource/functions/server-time.js: -------------------------------------------------------------------------------- 1 | var func = require('../../../').Function; 2 | 3 | var router = func(); 4 | 5 | router.get('/server-time', function(req, res, next) { 6 | res.jsonp({ date: new Date() }); 7 | }); 8 | 9 | module.exports = router; 10 | 11 | 12 | -------------------------------------------------------------------------------- /examples/multi-resource/index.js: -------------------------------------------------------------------------------- 1 | var odata = require('../../'); 2 | 3 | var server = odata('mongodb://localhost/odata-test'); 4 | 5 | // init resources 6 | server.use(requiere('./resources/book')); 7 | server.use(requiere('./resources/user')); 8 | 9 | // init functions 10 | server.use(requiere('./functions/license')); 11 | server.use(requiere('./functions/server-time')); 12 | 13 | server.listen(3000, function(){ 14 | console.log('OData services has started, you can visit by http://localhost:3000'); 15 | }); 16 | 17 | -------------------------------------------------------------------------------- /examples/multi-resource/resources/book.js: -------------------------------------------------------------------------------- 1 | var Resource = require('../../../').Resource; 2 | 3 | module.exports = Resource('book', { 4 | author: String, 5 | description: String, 6 | genre: String, 7 | price: Number, 8 | publish_date: Date, 9 | title: String 10 | }); 11 | -------------------------------------------------------------------------------- /examples/multi-resource/resources/user.js: -------------------------------------------------------------------------------- 1 | var Resource = require('../../../').Resource; 2 | 3 | module.exports = Resource('user', { 4 | name: String, 5 | password: String 6 | }); 7 | -------------------------------------------------------------------------------- /examples/simple-with-data/index.js: -------------------------------------------------------------------------------- 1 | var server = require('../simple/'); 2 | var data = require("../../test/support/books.json"); 3 | 4 | model = server._db.model('book'); 5 | 6 | model.remove({}, function(err, result) { 7 | data.map(function(item) { 8 | entity = new model(item); 9 | entity.save(); 10 | }); 11 | }); 12 | -------------------------------------------------------------------------------- /examples/simple/index.js: -------------------------------------------------------------------------------- 1 | var odata = require('../../'); 2 | 3 | var server = odata('mongodb://localhost/odata-test'); 4 | 5 | var bookInfo = { 6 | author: String, 7 | description: String, 8 | genre: String, 9 | price: Number, 10 | publish_date: Date, 11 | title: String 12 | }; 13 | 14 | server.resource('book', bookInfo) 15 | .action('/50off', function(req, res, next){ 16 | server.repository('book').findById(req.params.id, function(err, book){ 17 | book.price = +(book.price / 2).toFixed(2); 18 | book.save(function(err){ 19 | res.jsonp(book); 20 | }); 21 | }); 22 | }); 23 | 24 | server.get('/license', function(req, res, next){ 25 | res.jsonp({license:'MIT'}); 26 | }); 27 | 28 | server.on('connected', function() { 29 | console.log('MongoDB connected!'); 30 | }); 31 | server.on('disconnected', function() { 32 | console.log('MongoDB disconnected!'); 33 | }); 34 | 35 | server.listen(3000, function(){ 36 | console.log('OData services has started, you can visit by http://localhost:3000/book'); 37 | }); 38 | 39 | module.exports = server; 40 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | module.exports = require("./lib").default; 2 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "node-odata", 3 | "version": "0.7.16", 4 | "private": false, 5 | "description": "A module for easily create a REST API based on oData protocol", 6 | "main": "index.js", 7 | "author": { 8 | "name": "Zack", 9 | "email": "zackyang@outlook.com" 10 | }, 11 | "repository": { 12 | "type": "git", 13 | "url": "https://github.com/TossShinHwa/node-odata.git" 14 | }, 15 | "engines": { 16 | "node": ">=0.12" 17 | }, 18 | "scripts": { 19 | "lint": "eslint src/", 20 | "prepublish": "make", 21 | "test": "make", 22 | "test:some": "node_modules/.bin/mocha --require @babel/register --reporter dot" 23 | }, 24 | "keywords": [ 25 | "OData", 26 | "REST", 27 | "RESTful" 28 | ], 29 | "license": "MIT", 30 | "dependencies": { 31 | "body-parser": "^1.20.0", 32 | "cors": "2.8.5", 33 | "express": "^4.18.1", 34 | "method-override": "3.0.0", 35 | "mongoose": "6.6.5", 36 | "uuid": "9.0.0" 37 | }, 38 | "devDependencies": { 39 | "@babel/cli": "^7.19.3", 40 | "@babel/core": "^7.19.3", 41 | "@babel/eslint-parser": "^7.19.1", 42 | "@babel/helpers": "^7.19.0", 43 | "@babel/plugin-proposal-class-properties": "^7.0.0", 44 | "@babel/plugin-proposal-decorators": "^7.0.0", 45 | "@babel/plugin-proposal-do-expressions": "^7.0.0", 46 | "@babel/plugin-proposal-export-default-from": "^7.0.0", 47 | "@babel/plugin-proposal-export-namespace-from": "^7.0.0", 48 | "@babel/plugin-proposal-function-bind": "^7.0.0", 49 | "@babel/plugin-proposal-function-sent": "^7.0.0", 50 | "@babel/plugin-proposal-json-strings": "^7.0.0", 51 | "@babel/plugin-proposal-logical-assignment-operators": "^7.0.0", 52 | "@babel/plugin-proposal-nullish-coalescing-operator": "^7.0.0", 53 | "@babel/plugin-proposal-numeric-separator": "^7.0.0", 54 | "@babel/plugin-proposal-optional-chaining": "^7.0.0", 55 | "@babel/plugin-proposal-pipeline-operator": "^7.0.0", 56 | "@babel/plugin-proposal-throw-expressions": "^7.0.0", 57 | "@babel/plugin-syntax-dynamic-import": "^7.0.0", 58 | "@babel/plugin-syntax-import-meta": "^7.0.0", 59 | "@babel/preset-env": "^7.19.3", 60 | "@babel/register": "^7.18.9", 61 | "@babel/runtime": "^7.19.0", 62 | "babel-loader": "8.2.5", 63 | "eslint": "8.24.0", 64 | "eslint-config-airbnb": "19.0.4", 65 | "eslint-plugin-babel": "5.3.1", 66 | "eslint-plugin-import": "^2.26.0", 67 | "istanbul": "1.1.0-alpha.1", 68 | "mocha": "10.0.0", 69 | "should": "13.2.3", 70 | "should-sinon": "0.0.6", 71 | "sinon": "14.0.1", 72 | "supertest": "6.3.0" 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /src/ODataFunction.js: -------------------------------------------------------------------------------- 1 | import express from 'express'; 2 | 3 | export default class { 4 | constructor(name, middleware, params) { 5 | this._name = name; 6 | this._middleware = middleware; 7 | this.params = params || {}; 8 | } 9 | 10 | getName() { 11 | return this._name; 12 | } 13 | 14 | _router() { 15 | const router = express.Router(); 16 | const method = this.params.method || 'get'; 17 | 18 | router[method](`/${this._name}`, this._middleware); 19 | 20 | return router; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/ODataResource.js: -------------------------------------------------------------------------------- 1 | import rest from './rest'; 2 | import { min } from './utils'; 3 | 4 | function hook(resource, pos, fn) { 5 | let method = resource._currentMethod; 6 | if (method === 'all') { 7 | method = ['get', 'post', 'put', 'delete', 'patch', 'list']; 8 | } else { 9 | method = [method]; 10 | } 11 | /*eslint-disable */ 12 | method.map((curr) => { 13 | if (resource._hooks[curr][pos]) { 14 | const _fn = resource._hooks[curr][pos]; 15 | resource._hooks[curr][pos] = (...args) => { 16 | _fn.apply(resource, args); 17 | fn.apply(resource, args); 18 | }; 19 | } else { 20 | resource._hooks[curr][pos] = fn; 21 | } 22 | }); 23 | /* eslint-enable */ 24 | } 25 | 26 | export default class { 27 | constructor(server, name, userModel) { 28 | this._server = server; 29 | this._name = name; 30 | this._url = name; 31 | this._model = userModel; 32 | this._hooks = { 33 | list: {}, 34 | get: {}, 35 | post: {}, 36 | put: {}, 37 | delete: {}, 38 | patch: {}, 39 | }; 40 | this.actions = {}; 41 | this._options = { 42 | maxTop: 10000, 43 | maxSkip: 10000, 44 | orderby: undefined, 45 | }; 46 | } 47 | 48 | getName() { 49 | return this._name; 50 | } 51 | 52 | setModel(model) { 53 | this.model = model; 54 | } 55 | 56 | action(url, fn, options) { 57 | let auth; 58 | let binding; 59 | 60 | if (options) { 61 | auth = options.auth; 62 | binding = options.binding; 63 | } 64 | 65 | this.actions[url] = fn; 66 | this.actions[url].auth = auth; 67 | this.actions[url].binding = binding; 68 | this.actions[url].resource = this._url; 69 | 70 | const resourceUrl = !binding || binding === 'entity' // 'entity' || 'collection' 71 | ? `/${this._url}\\(:id\\)` : `/${this._url}`; 72 | this.actions[url].router = rest.getOperationRouter(resourceUrl, url, fn, auth); 73 | 74 | return this; 75 | } 76 | 77 | maxTop(count) { 78 | this._maxTop = count; 79 | return this; 80 | } 81 | 82 | maxSkip(count) { 83 | this._maxSkip = count; 84 | return this; 85 | } 86 | 87 | orderBy(field) { 88 | this._orderby = field; 89 | return this; 90 | } 91 | 92 | list() { 93 | this._currentMethod = 'list'; 94 | return this; 95 | } 96 | 97 | get() { 98 | this._currentMethod = 'get'; 99 | return this; 100 | } 101 | 102 | post() { 103 | this._currentMethod = 'post'; 104 | return this; 105 | } 106 | 107 | put() { 108 | this._currentMethod = 'put'; 109 | return this; 110 | } 111 | 112 | delete() { 113 | this._currentMethod = 'delete'; 114 | return this; 115 | } 116 | 117 | patch() { 118 | this._currentMethod = 'patch'; 119 | return this; 120 | } 121 | 122 | all() { 123 | this._currentMethod = 'all'; 124 | return this; 125 | } 126 | 127 | before(fn) { 128 | hook(this, 'before', fn); 129 | return this; 130 | } 131 | 132 | after(fn) { 133 | hook(this, 'after', fn); 134 | return this; 135 | } 136 | 137 | auth(fn) { 138 | let method = this._currentMethod; 139 | if (method === 'all') { 140 | method = ['get', 'post', 'put', 'delete', 'patch', 'list']; 141 | } else { 142 | method = [method]; 143 | } 144 | method.map((curr) => { 145 | this._hooks[curr].auth = fn; 146 | return undefined; 147 | }); 148 | return this; 149 | } 150 | 151 | url(url) { 152 | this._url = url; 153 | return this; 154 | } 155 | 156 | _router(setting = {}) { 157 | // remove '/' if url is startwith it. 158 | if (this._url.indexOf('/') === 0) { 159 | this._url = this._url.substr(1); 160 | } 161 | 162 | // not allow contain '/' in url. 163 | if (this._url.indexOf('/') >= 0) { 164 | throw new Error(`Url of resource[${this._name}] can't contain "/",` 165 | + 'it can only be allowed to exist in the beginning.'); 166 | } 167 | 168 | const params = { 169 | url: this._url, 170 | options: { 171 | maxTop: min([setting.maxTop, this._maxTop]), 172 | maxSkip: min([setting.maxSkip, this._maxSkip]), 173 | orderby: this._orderby || setting.orderby, 174 | }, 175 | hooks: this._hooks, 176 | }; 177 | 178 | return rest.getRouter(this.model, params); 179 | } 180 | 181 | find() { 182 | return this.model.find(); 183 | } 184 | } 185 | -------------------------------------------------------------------------------- /src/db/db.js: -------------------------------------------------------------------------------- 1 | import mongoose from 'mongoose'; 2 | import Model from './model'; 3 | import id from './idPlugin'; 4 | 5 | mongoose.Promise = global.Promise; 6 | 7 | export default class { 8 | constructor() { 9 | this._models = {}; 10 | } 11 | 12 | createConnection(connection, optionsIn, onError) { 13 | const options = { 14 | ...optionsIn, 15 | server: { reconnectTries: Number.MAX_VALUE }, 16 | }; 17 | 18 | this._connection = mongoose.createConnection(connection, options, onError); 19 | 20 | return this._connection; 21 | } 22 | 23 | on(name, event) { 24 | this._connection.on(name, event); 25 | } 26 | 27 | register(name, model) { 28 | const conf = { 29 | _id: false, 30 | versionKey: false, 31 | collection: name, 32 | }; 33 | const schema = new mongoose.Schema(model, conf); 34 | schema.plugin(id); 35 | const mongooseModel = this._connection.model(name, schema); 36 | this._models[name] = new Model(mongooseModel); 37 | return this._models[name]; 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/db/idPlugin.js: -------------------------------------------------------------------------------- 1 | import * as uuid from 'uuid'; 2 | 3 | /*eslint-disable */ 4 | export default function (schema) { 5 | // add _id to schema. 6 | if (!schema.paths._id) { 7 | schema.add({ 8 | _id: { 9 | type: String, 10 | unique: true, 11 | } 12 | }); 13 | } 14 | 15 | // display value of _id when request id. 16 | if (!schema.paths.id) { 17 | schema.virtual('id').get(function getId() { 18 | return this._id; 19 | }); 20 | schema.set('toObject', { virtuals: true }); 21 | schema.set('toJSON', { virtuals: true }); 22 | } 23 | 24 | // reomove _id when serialization. 25 | if (!schema.options.toObject) { 26 | schema.options.toObject = {}; 27 | } 28 | if (!schema.options.toJSON) { 29 | schema.options.toJSON = {}; 30 | } 31 | const remove = (doc, ret) => { 32 | delete ret._id; 33 | if (!ret.id) { 34 | delete ret.id; 35 | } 36 | return ret; 37 | }; 38 | schema.options.toObject.transform = remove; 39 | schema.options.toJSON.transform = remove; 40 | 41 | // genarate _id. 42 | schema.pre('save', function preSave(next) { 43 | if (this.isNew && !this._id) { 44 | if (this.id) { 45 | // Use a user-defined id to save 46 | this._id = this.id; 47 | } else { 48 | // Use uuid to save 49 | this._id = uuid.v4(); 50 | } 51 | } 52 | return next(); 53 | }); 54 | } 55 | /* eslint-enable */ 56 | -------------------------------------------------------------------------------- /src/db/model.js: -------------------------------------------------------------------------------- 1 | export default class { 2 | constructor(mongooseModel) { 3 | this.model = mongooseModel; 4 | } 5 | 6 | create(data) { 7 | const MongooseModel = this.model; 8 | return new MongooseModel(data); 9 | } 10 | 11 | find() { 12 | return this.model.find(); 13 | } 14 | 15 | findById(id, callback) { 16 | this.model.findById(id, callback); 17 | } 18 | 19 | findByIdAndUpdate(id, data, callback) { 20 | this.model.findByIdAndUpdate(id, data, callback); 21 | } 22 | 23 | findOne(filter, callback) { 24 | this.model.findOne(filter, callback); 25 | } 26 | 27 | remove(filter, callback) { 28 | this.model.remove(filter, callback); 29 | } 30 | 31 | update(filter, data, callback) { 32 | this.model.update(filter, data, callback); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/express.js: -------------------------------------------------------------------------------- 1 | import express from 'express'; 2 | import bodyParser from 'body-parser'; 3 | import methodOverride from 'method-override'; 4 | import cors from 'cors'; 5 | 6 | export default function orientExpress(options) { 7 | const app = express(); 8 | const opts = (options && options.expressRequestLimit) 9 | ? { limit: options.expressRequestLimit } : {}; 10 | 11 | app.use(bodyParser.json(opts)); 12 | opts.extended = true; 13 | app.use(bodyParser.urlencoded(opts)); 14 | app.use(methodOverride()); 15 | app.use(express.query()); 16 | app.use(cors(options && options.corsOptions)); 17 | app.disable('x-powered-by'); 18 | return app; 19 | } 20 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import express from 'express'; 2 | import Server from './server'; 3 | 4 | const server = function server(db, prefix, options) { 5 | return new Server(db, prefix, options); 6 | }; 7 | 8 | server._express = express; 9 | 10 | export default server; 11 | -------------------------------------------------------------------------------- /src/metadata/ODataMetadata.js: -------------------------------------------------------------------------------- 1 | import { Router } from 'express'; 2 | import pipes from '../pipes'; 3 | import Resource from '../ODataResource'; 4 | 5 | export default class Metadata { 6 | constructor(server) { 7 | this._server = server; 8 | this._hooks = { 9 | }; 10 | this._count = 0; 11 | } 12 | 13 | get() { 14 | return this; 15 | } 16 | 17 | before(fn) { 18 | this._hooks.before = fn; 19 | return this; 20 | } 21 | 22 | after(fn) { 23 | this._hooks.after = fn; 24 | return this; 25 | } 26 | 27 | auth(fn) { 28 | this._hooks.auth = fn; 29 | return this; 30 | } 31 | 32 | _router() { 33 | /*eslint-disable */ 34 | const router = Router(); 35 | /* eslint-enable */ 36 | router.get('/\\$metadata', (req, res) => { 37 | pipes.authorizePipe(req, res, this._hooks.auth) 38 | .then(() => pipes.beforePipe(req, res, this._hooks.before)) 39 | .then(() => this.ctrl(req)) 40 | .then((result) => pipes.respondPipe(req, res, result || {})) 41 | .then((data) => pipes.afterPipe(req, res, this._hooks.after, data)) 42 | .catch((err) => pipes.errorPipe(req, res, err)); 43 | }); 44 | 45 | return router; 46 | } 47 | 48 | visitProperty(node, root) { 49 | const result = {}; 50 | 51 | switch (node.instance) { 52 | case 'ObjectId': 53 | result.$Type = 'self.ObjectId'; 54 | break; 55 | 56 | case 'Number': 57 | result.$Type = 'Edm.Double'; 58 | break; 59 | 60 | case 'Date': 61 | result.$Type = 'Edm.DateTimeOffset'; 62 | break; 63 | 64 | case 'String': 65 | result.$Type = 'Edm.String'; 66 | break; 67 | 68 | case 'Array': // node.path = p1; node.schema.paths 69 | result.$Collection = true; 70 | if (node.schema && node.schema.paths) { 71 | this._count += 1; 72 | const notClassifiedName = `${node.path}Child${this._count}`; 73 | // Array of complex type 74 | result.$Type = `self.${notClassifiedName}`; 75 | root(notClassifiedName, this.visitor('ComplexType', node.schema.paths, root)); 76 | } else { 77 | const arrayItemType = this.visitor('Property', { instance: node.options.type[0].name }, root); 78 | 79 | result.$Type = arrayItemType.$Type; 80 | } 81 | break; 82 | 83 | default: 84 | return null; 85 | } 86 | 87 | return result; 88 | } 89 | 90 | visitEntityType(node, root) { 91 | const properties = Object.keys(node) 92 | .filter((path) => path !== '_id') 93 | .reduce((previousProperty, curentProperty) => { 94 | const result = { 95 | ...previousProperty, 96 | [curentProperty]: this.visitor('Property', node[curentProperty], root), 97 | }; 98 | 99 | return result; 100 | }, {}); 101 | 102 | return { 103 | $Kind: 'EntityType', 104 | $Key: ['id'], 105 | id: { 106 | $Type: 'self.ObjectId', 107 | $Nullable: false, 108 | }, 109 | ...properties, 110 | }; 111 | } 112 | 113 | visitComplexType(node, root) { 114 | const properties = Object.keys(node) 115 | .filter((item) => item !== '_id') 116 | .reduce((previousProperty, curentProperty) => { 117 | const result = { 118 | ...previousProperty, 119 | [curentProperty]: this.visitor('Property', node[curentProperty], root), 120 | }; 121 | 122 | return result; 123 | }, {}); 124 | 125 | return { 126 | $Kind: 'ComplexType', 127 | ...properties, 128 | }; 129 | } 130 | 131 | static visitAction(node) { 132 | return { 133 | $Kind: 'Action', 134 | $IsBound: true, 135 | $Parameter: [{ 136 | $Name: node.resource, 137 | $Type: `self.${node.resource}`, 138 | $Collection: node.binding === 'collection' ? true : undefined, 139 | }], 140 | }; 141 | } 142 | 143 | static visitFunction(node) { 144 | return { 145 | $Kind: 'Function', 146 | ...node.params, 147 | }; 148 | } 149 | 150 | visitor(type, node, root) { 151 | switch (type) { 152 | case 'Property': 153 | return this.visitProperty(node, root); 154 | 155 | case 'ComplexType': 156 | return this.visitComplexType(node, root); 157 | 158 | case 'Action': 159 | return Metadata.visitAction(node); 160 | 161 | case 'Function': 162 | return Metadata.visitFunction(node, root); 163 | 164 | default: 165 | return this.visitEntityType(node, root); 166 | } 167 | } 168 | 169 | ctrl() { 170 | const entityTypeNames = Object.keys(this._server.resources); 171 | const entityTypes = entityTypeNames.reduce((previousResource, currentResource) => { 172 | const resource = this._server.resources[currentResource]; 173 | const result = { ...previousResource }; 174 | const attachToRoot = (name, value) => { result[name] = value; }; 175 | 176 | if (resource instanceof Resource) { 177 | const { paths } = resource.model.model.schema; 178 | 179 | result[currentResource] = this.visitor('EntityType', paths, attachToRoot); 180 | const actions = Object.keys(resource.actions); 181 | if (actions && actions.length) { 182 | actions.forEach((action) => { 183 | result[action] = this.visitor('Action', resource.actions[action], attachToRoot); 184 | }); 185 | } 186 | } else { 187 | result[currentResource] = this.visitor('Function', resource, attachToRoot); 188 | } 189 | 190 | return result; 191 | }, {}); 192 | 193 | const entitySetNames = Object.keys(this._server.resources); 194 | const entitySets = entitySetNames.reduce((previousResource, currentResource) => { 195 | const result = { ...previousResource }; 196 | result[currentResource] = this._server.resources[currentResource] instanceof Resource ? { 197 | $Collection: true, 198 | $Type: `self.${currentResource}`, 199 | } : { 200 | $Function: `self.${currentResource}`, 201 | }; 202 | 203 | return result; 204 | }, {}); 205 | 206 | const document = { 207 | $Version: '4.0', 208 | ObjectId: { 209 | $Kind: 'TypeDefinition', 210 | $UnderlyingType: 'Edm.String', 211 | $MaxLength: 24, 212 | }, 213 | ...entityTypes, 214 | $EntityContainer: 'org.example.DemoService', 215 | ['org.example.DemoService']: { // eslint-disable-line no-useless-computed-key 216 | $Kind: 'EntityContainer', 217 | ...entitySets, 218 | }, 219 | }; 220 | 221 | return new Promise((resolve) => { 222 | resolve({ 223 | status: 200, 224 | metadata: document, 225 | }); 226 | }); 227 | } 228 | } 229 | -------------------------------------------------------------------------------- /src/metadata/xmlWriter.js: -------------------------------------------------------------------------------- 1 | export default class XmlWriter { 2 | visitor(type, node, name) { 3 | switch (type) { 4 | case 'document': 5 | return this.visitDocument(node); 6 | 7 | case 'EntityType': 8 | return this.visitEntityType(node, name); 9 | 10 | case 'Property': 11 | return XmlWriter.visitProperty(node, name); 12 | 13 | case 'EntityContainer': 14 | return this.visitEntityContainter(node); 15 | 16 | case 'EntitySet': 17 | return XmlWriter.visitEntitySet(node, name); 18 | 19 | case 'TypeDefinition': 20 | return XmlWriter.visitTypeDefinition(node, name); 21 | 22 | case 'ComplexType': 23 | return this.visitComplexType(node, name); 24 | 25 | case 'Action': 26 | return XmlWriter.visitAction(node, name); 27 | 28 | case 'Function': 29 | return XmlWriter.visitFunction(node, name); 30 | 31 | case 'FunctionImport': 32 | return XmlWriter.visitFunctionImport(node, name); 33 | 34 | default: 35 | throw new Error(`Type ${type} is not supported`); 36 | } 37 | } 38 | 39 | visitDocument(node) { 40 | let body = ''; 41 | 42 | Object.keys(node).forEach((subnode) => { 43 | if (node[subnode].$Kind) { 44 | body += this.visitor(node[subnode].$Kind, node[subnode], subnode); 45 | } 46 | }); 47 | 48 | return ( 49 | ` 50 | 51 | 52 | ${body} 53 | 54 | 55 | `); 56 | } 57 | 58 | static visitEntitySet(node, name) { 59 | return ``; 60 | } 61 | 62 | visitEntityContainter(node) { 63 | let entitySets = ''; 64 | let functions = ''; 65 | 66 | Object.keys(node) 67 | .filter((item) => item !== '$Kind') 68 | .forEach((item) => { 69 | if (node[item].$Type) { 70 | entitySets += this.visitor('EntitySet', node[item], item); 71 | } else { 72 | functions += this.visitor('FunctionImport', node[item], item); 73 | } 74 | }); 75 | return ( 76 | ` 77 | ${entitySets}${functions} 78 | `); 79 | } 80 | 81 | static visitProperty(node, name) { 82 | let attributes = ''; 83 | 84 | if (node.$Nullable === false) { 85 | attributes += ' Nullable="false"'; 86 | } 87 | if (node.$MaxLength) { 88 | attributes += ` MaxLength="${node.$MaxLength}"`; 89 | } 90 | if (node.$Collection) { 91 | attributes += ' Collection="true"'; 92 | } 93 | 94 | return ``; 95 | } 96 | 97 | visitEntityType(node, name) { 98 | let properties = ''; 99 | 100 | Object.keys(node) 101 | .filter((item) => item !== '$Kind' && item !== '$Key') 102 | .forEach((item) => { 103 | properties += this.visitor('Property', node[item], item); 104 | }); 105 | 106 | return ( 107 | ` 108 | 109 | 110 | 111 | ${properties} 112 | `); 113 | } 114 | 115 | static visitTypeDefinition(node, name) { 116 | let attributes = ''; 117 | 118 | if (node.$MaxLength) { 119 | attributes += ` MaxLength="${node.$MaxLength}"`; 120 | } 121 | 122 | return ( 123 | ` 124 | `); 125 | } 126 | 127 | visitComplexType(node, name) { 128 | let properties = ''; 129 | 130 | Object.keys(node) 131 | .filter((item) => item !== '$Kind') 132 | .forEach((item) => { 133 | properties += this.visitor('Property', node[item], item); 134 | }); 135 | 136 | return (` 137 | 138 | ${properties} 139 | `); 140 | } 141 | 142 | static visitAction(node, name) { 143 | const isBound = node.$IsBound ? ' IsBound="true"' : ''; 144 | const parameter = node.$Parameter.map((item) => { 145 | let type = ''; 146 | 147 | if (item.$Collection) { 148 | type = ` Type="Collection(${item.$Type})"`; 149 | } else if (item.$Type) { 150 | type = ` Type="${item.$Type}"`; 151 | } 152 | 153 | return ``; 154 | }); 155 | 156 | return (` 157 | 158 | ${parameter} 159 | 160 | `); 161 | } 162 | 163 | static visitFunction(node, name) { 164 | const collection = node.$ReturnType.$Collection ? ' Collection="true"' : ''; 165 | 166 | return (` 167 | 168 | 169 | 170 | `); 171 | } 172 | 173 | static visitFunctionImport(node, name) { 174 | return (` 175 | 176 | `); 177 | } 178 | 179 | writeXml(res, data, status, resolve) { 180 | const xml = this.visitor('document', data, '', '').replace(/\s*\s*/g, '>'); 181 | 182 | res.type('application/xml'); 183 | res.status(status).send(xml); 184 | resolve(data); 185 | } 186 | } 187 | -------------------------------------------------------------------------------- /src/model/idPlugin.js: -------------------------------------------------------------------------------- 1 | import * as uuid from 'uuid'; 2 | 3 | /*eslint-disable */ 4 | export default function (schema) { 5 | // add _id to schema. 6 | if (!schema.paths._id) { 7 | schema.add({ 8 | _id: { 9 | type: String, 10 | unique: true, 11 | } 12 | }); 13 | } 14 | 15 | // display value of _id when request id. 16 | if (!schema.paths.id) { 17 | schema.virtual('id').get(function getId() { 18 | return this._id; 19 | }); 20 | schema.set('toObject', { virtuals: true }); 21 | schema.set('toJSON', { virtuals: true }); 22 | } 23 | 24 | // reomove _id when serialization. 25 | if (!schema.options.toObject) { 26 | schema.options.toObject = {}; 27 | } 28 | if (!schema.options.toJSON) { 29 | schema.options.toJSON = {}; 30 | } 31 | const remove = (doc, ret) => { 32 | delete ret._id; 33 | if (!ret.id) { 34 | delete ret.id; 35 | } 36 | return ret; 37 | }; 38 | schema.options.toObject.transform = remove; 39 | schema.options.toJSON.transform = remove; 40 | 41 | // genarate _id. 42 | schema.pre('save', function preSave(next) { 43 | if (this.isNew && !this._id) { 44 | if (this.id) { 45 | // Use a user-defined id to save 46 | this._id = this.id; 47 | } else { 48 | // Use uuid to save 49 | this._id = uuid.v4(); 50 | } 51 | } 52 | return next(); 53 | }); 54 | } 55 | /* eslint-enable */ 56 | -------------------------------------------------------------------------------- /src/model/index.js: -------------------------------------------------------------------------------- 1 | import mongoose from 'mongoose'; 2 | import id from './idPlugin'; 3 | 4 | const register = (_db, name, model) => { 5 | const conf = { 6 | _id: false, 7 | versionKey: false, 8 | collection: name, 9 | }; 10 | const schema = new mongoose.Schema(model, conf); 11 | schema.plugin(id); 12 | return _db.model(name, schema); 13 | }; 14 | 15 | export default { register }; 16 | -------------------------------------------------------------------------------- /src/parser/countParser.js: -------------------------------------------------------------------------------- 1 | import filterParser from './filterParser'; 2 | 3 | // ?$count=10 4 | // -> 5 | // query.count(10) 6 | export default (mongooseModel, $count, $filter) => new Promise((resolve, reject) => { 7 | if ($count === undefined) { 8 | resolve(); 9 | return; 10 | } 11 | 12 | switch ($count) { 13 | case 'true': { 14 | const query = mongooseModel.find(); 15 | filterParser(query, $filter); 16 | query.count((err, count) => { 17 | resolve(count); 18 | }); 19 | break; 20 | } 21 | case 'false': 22 | resolve(); 23 | break; 24 | default: 25 | reject(new Error('Unknown $count option, only "true" and "false" are supported.')); 26 | break; 27 | } 28 | }); 29 | -------------------------------------------------------------------------------- /src/parser/filterParser.js: -------------------------------------------------------------------------------- 1 | // Operator Description Example 2 | // Comparison Operators 3 | // eq Equal Address/City eq 'Redmond' 4 | // ne Not equal Address/City ne 'London' 5 | // gt Greater than Price gt 20 6 | // ge Greater than or equal Price ge 10 7 | // lt Less than Price lt 20 8 | // le Less than or equal Price le 100 9 | // has Has flags Style has Sales.Color'Yellow' #todo 10 | // Logical Operators 11 | // and Logical and Price le 200 and Price gt 3.5 12 | // or Logical or Price le 3.5 or Price gt 200 #todo 13 | // not Logical negation not endswith(Description,'milk') #todo 14 | 15 | // eg. 16 | // http://host/service/Products?$filter=Price lt 10.00 17 | // http://host/service/Categories?$filter=Products/$count lt 10 18 | 19 | import functions from './functionsParser'; 20 | import { split } from '../utils'; 21 | 22 | const OPERATORS_KEYS = ['eq', 'ne', 'gt', 'ge', 'lt', 'le', 'has']; 23 | 24 | const stringHelper = { 25 | has: (str, key) => str.indexOf(key) >= 0, 26 | 27 | isBeginWith: (str, key) => str.indexOf(key) === 0, 28 | 29 | isEndWith: (str, key) => str.lastIndexOf(key) === (str.length - key.length), 30 | 31 | removeEndOf: (str, key) => { 32 | if (stringHelper.isEndWith(str, key)) { 33 | return str.substr(0, str.length - key.length); 34 | } 35 | return str; 36 | }, 37 | }; 38 | 39 | const validator = { 40 | formatValue: (value) => { 41 | let val; 42 | if (value === 'true') { 43 | val = true; 44 | } else if (value === 'false') { 45 | val = false; 46 | } else if (!Number.isNaN(+value)) { 47 | val = +value; 48 | } else if (stringHelper.isBeginWith(value, "'") && stringHelper.isEndWith(value, "'")) { 49 | val = value.slice(1, -1); 50 | } else if (value === 'null') { 51 | val = value; 52 | } else { 53 | return ({ err: new Error(`Syntax error at '${value}'.`) }); 54 | } 55 | return ({ val }); 56 | }, 57 | }; 58 | 59 | export default (query, $filter) => new Promise((resolve, reject) => { 60 | if (!$filter) { 61 | resolve(); 62 | return; 63 | } 64 | 65 | const condition = split($filter, ['and', 'or']) 66 | .filter((item) => (item !== 'and' && item !== 'or')); 67 | 68 | condition.forEach((item) => { 69 | // parse "indexof(title,'X1ML') gt 0" 70 | const conditionArr = split(item, OPERATORS_KEYS); 71 | if (conditionArr.length === 0) { 72 | // parse "contains(title,'X1ML')" 73 | conditionArr.push(item); 74 | } 75 | if (conditionArr.length !== 3 && conditionArr.length !== 1) { 76 | return reject(new Error(`Syntax error at '${item}'.`)); 77 | } 78 | 79 | let key = conditionArr[0]; 80 | const [, odataOperator, value] = conditionArr; 81 | 82 | if (key === 'id') key = '_id'; 83 | 84 | let val; 85 | if (value !== undefined) { 86 | const result = validator.formatValue(value); 87 | if (result.err) { 88 | return reject(result.err); 89 | } 90 | val = result.val; 91 | } 92 | 93 | // function query 94 | const functionKey = key.substring(0, key.indexOf('(')); 95 | if (['indexof', 'year', 'contains'].indexOf(functionKey) > -1) { 96 | functions[functionKey](query, key, odataOperator, val); 97 | } else { 98 | if (conditionArr.length === 1) { 99 | return reject(new Error(`Syntax error at '${item}'.`)); 100 | } 101 | if (value === 'null') { 102 | switch (odataOperator) { 103 | case 'eq': 104 | query.exists(key, false); 105 | return resolve(); 106 | case 'ne': 107 | query.exists(key, true); 108 | return resolve(); 109 | default: 110 | break; 111 | } 112 | } 113 | // operator query 114 | switch (odataOperator) { 115 | case 'eq': 116 | query.where(key).equals(val); 117 | break; 118 | case 'ne': 119 | query.where(key).ne(val); 120 | break; 121 | case 'gt': 122 | query.where(key).gt(val); 123 | break; 124 | case 'ge': 125 | query.where(key).gte(val); 126 | break; 127 | case 'lt': 128 | query.where(key).lt(val); 129 | break; 130 | case 'le': 131 | query.where(key).lte(val); 132 | break; 133 | default: 134 | return reject(new Error("Incorrect operator at '#{item}'.")); 135 | } 136 | } 137 | return query; 138 | }); 139 | resolve(); 140 | }); 141 | -------------------------------------------------------------------------------- /src/parser/functionsParser.js: -------------------------------------------------------------------------------- 1 | const convertToOperator = (odataOperator) => { 2 | let operator; 3 | switch (odataOperator) { 4 | case 'eq': 5 | operator = '=='; 6 | break; 7 | case 'ne': 8 | operator = '!='; 9 | break; 10 | case 'gt': 11 | operator = '>'; 12 | break; 13 | case 'ge': 14 | operator = '>='; 15 | break; 16 | case 'lt': 17 | operator = '<'; 18 | break; 19 | case 'le': 20 | operator = '<='; 21 | break; 22 | default: 23 | throw new Error('Invalid operator code, expected one of ["==", "!=", ">", ">=", "<", "<="].'); 24 | } 25 | return operator; 26 | }; 27 | 28 | // contains(CompanyName,'icrosoft') 29 | const contains = (query, fnKey) => { 30 | let [key, target] = fnKey.substring(fnKey.indexOf('(') + 1, fnKey.indexOf(')')).split(','); 31 | [key, target] = [key.trim(), target.trim()]; 32 | query.$where(`this.${key}.indexOf(${target}) != -1`); 33 | }; 34 | 35 | // indexof(CompanyName,'X') eq 1 36 | const indexof = (query, fnKey, odataOperator, value) => { 37 | let [key, target] = fnKey.substring(fnKey.indexOf('(') + 1, fnKey.indexOf(')')).split(','); 38 | [key, target] = [key.trim(), target.trim()]; 39 | const operator = convertToOperator(odataOperator); 40 | query.$where(`this.${key}.indexOf(${target}) ${operator} ${value}`); 41 | }; 42 | 43 | // year(publish_date) eq 2000 44 | const year = (query, fnKey, odataOperator, value) => { 45 | const key = fnKey.substring(fnKey.indexOf('(') + 1, fnKey.indexOf(')')); 46 | 47 | const start = new Date(+value, 0, 1); 48 | const end = new Date(+value + 1, 0, 1); 49 | 50 | switch (odataOperator) { 51 | case 'eq': 52 | query.where(key).gte(start).lt(end); 53 | break; 54 | case 'ne': { 55 | const condition = [{}, {}]; 56 | condition[0][key] = { $lt: start }; 57 | condition[1][key] = { $gte: end }; 58 | query.or(condition); 59 | break; 60 | } 61 | case 'gt': 62 | query.where(key).gte(end); 63 | break; 64 | case 'ge': 65 | query.where(key).gte(start); 66 | break; 67 | case 'lt': 68 | query.where(key).lt(start); 69 | break; 70 | case 'le': 71 | query.where(key).lt(end); 72 | break; 73 | default: 74 | throw new Error('Invalid operator code, expected one of ["==", "!=", ">", ">=", "<", "<="].'); 75 | } 76 | }; 77 | 78 | export default { indexof, year, contains }; 79 | -------------------------------------------------------------------------------- /src/parser/orderbyParser.js: -------------------------------------------------------------------------------- 1 | // ?$skip=10 2 | // -> 3 | // query.skip(10) 4 | export default (query, $orderby) => new Promise((resolve, reject) => { 5 | if (!$orderby) { 6 | resolve(); 7 | return; 8 | } 9 | 10 | const order = {}; 11 | const orderbyArr = $orderby.split(','); 12 | 13 | orderbyArr.map((item) => { 14 | const data = item.trim().split(' '); 15 | if (data.length > 2) { 16 | return reject(new Error(`odata: Syntax error at '${$orderby}', ` 17 | + 'it\'s should be like \'ReleaseDate asc, Rating desc\'')); 18 | } 19 | const key = data[0].trim(); 20 | const value = data[1] || 'asc'; 21 | order[key] = value; 22 | return undefined; 23 | }); 24 | query.sort(order); 25 | resolve(); 26 | }); 27 | -------------------------------------------------------------------------------- /src/parser/selectParser.js: -------------------------------------------------------------------------------- 1 | // ?$select=Rating,ReleaseDate 2 | // -> 3 | // query.select('Rating ReleaseDate') 4 | export default (query, $select) => new Promise((resolve) => { 5 | if (!$select) { 6 | resolve(); 7 | return; 8 | } 9 | 10 | const list = $select.split(',').map((item) => item.trim()); 11 | 12 | const selectFields = { _id: 0 }; 13 | const { tree } = query.model.schema; 14 | Object.keys(tree).map((item) => { 15 | if (list.indexOf(item) >= 0) { 16 | if (item === 'id') { 17 | selectFields._id = 1; 18 | } else if (typeof tree[item] === 'function' || tree[item].select !== false) { 19 | selectFields[item] = 1; 20 | } 21 | } 22 | return undefined; 23 | }); 24 | 25 | if (Object.keys(selectFields).length === 1 && selectFields._id === 0) { 26 | resolve(); 27 | return; 28 | } 29 | 30 | query.select(selectFields); 31 | resolve(); 32 | }); 33 | -------------------------------------------------------------------------------- /src/parser/skipParser.js: -------------------------------------------------------------------------------- 1 | import { min } from '../utils'; 2 | 3 | // ?$skip=10 4 | // -> 5 | // query.skip(10) 6 | export default (query, skip, maxSkip) => new Promise((resolve) => { 7 | if (Number.isNaN(+skip)) { 8 | resolve(); 9 | return; 10 | } 11 | const _skip = min([maxSkip, skip]); 12 | if (_skip > 0) { 13 | query.skip(_skip); 14 | } 15 | resolve(); 16 | }); 17 | -------------------------------------------------------------------------------- /src/parser/topParser.js: -------------------------------------------------------------------------------- 1 | import { min } from '../utils'; 2 | 3 | // ?$top=10 4 | // -> 5 | // query.top(10) 6 | export default (query, top, maxTop) => new Promise((resolve) => { 7 | if (Number.isNaN(+top)) { 8 | resolve(); 9 | return; 10 | } 11 | const _top = min([maxTop, top]); 12 | if (_top > 0) { 13 | query.limit(_top); 14 | } 15 | resolve(); 16 | }); 17 | -------------------------------------------------------------------------------- /src/pipes.js: -------------------------------------------------------------------------------- 1 | import http from 'http'; 2 | import XmlWriter from './metadata/xmlWriter'; 3 | 4 | const xmlWriter = new XmlWriter(); 5 | 6 | function writeJson(res, data, status, resolve) { 7 | res.type('application/json'); 8 | res.status(status).jsonp(data); 9 | resolve(data); 10 | } 11 | 12 | function getMediaType(accept) { 13 | if (accept.match(/(application\/)?json/)) { 14 | return 'application/json'; 15 | } if (accept.match(/(application\/)?xml/)) { 16 | return 'application/xml'; 17 | } 18 | 19 | const error406 = new Error('Not acceptable'); 20 | 21 | error406.status = 406; 22 | throw error406; 23 | } 24 | 25 | function getWriter(req, result) { 26 | let mediaType; 27 | 28 | if (req.query.$format) { 29 | // get requested media type from $format query 30 | mediaType = getMediaType(req.query.$format); 31 | } else if (req.headers.accept) { 32 | // get requested media type from accept header 33 | mediaType = getMediaType(req.headers.accept); 34 | } 35 | 36 | // xml representation of metadata 37 | switch (mediaType) { 38 | case 'application/json': 39 | return writeJson; 40 | 41 | case 'application/xml': 42 | if (result.entity) { 43 | // xml wirter for entities and actions is not implemented 44 | const error406 = new Error('Not acceptable'); 45 | 46 | error406.status = 406; 47 | throw error406; 48 | } 49 | return xmlWriter.writeXml.bind(xmlWriter); 50 | 51 | default: 52 | // no media type requested set defaults depend of context 53 | if (result.entity) { 54 | return writeJson; // default for entities and actions 55 | } 56 | 57 | return xmlWriter.writeXml.bind(xmlWriter); // default for metadata 58 | } 59 | } 60 | 61 | const authorizePipe = (req, res, auth) => new Promise((resolve, reject) => { 62 | if (auth !== undefined) { 63 | if (!auth(req, res)) { 64 | const result = new Error(); 65 | 66 | result.status = 401; 67 | reject(result); 68 | return; 69 | } 70 | } 71 | resolve(); 72 | }); 73 | 74 | const beforePipe = (req, res, before) => new Promise((resolve) => { 75 | if (before) { 76 | before(req.body, req, res); 77 | } 78 | resolve(); 79 | }); 80 | 81 | const respondPipe = (req, res, result) => new Promise((resolve, reject) => { 82 | try { 83 | if (result.status === 204) { // no content 84 | res.status(204).end(); 85 | resolve(); 86 | return; 87 | } 88 | 89 | const status = result.status || 200; 90 | const writer = getWriter(req, result); 91 | let data; 92 | 93 | if (result.entity) { 94 | // json Representation of data 95 | data = result.entity; 96 | } else { 97 | // xml representation of metadata 98 | data = result.metadata; 99 | } 100 | 101 | writer(res, data, status, resolve); 102 | } catch (error) { 103 | reject(error); 104 | } 105 | }); 106 | 107 | const afterPipe = (req, res, after, data) => new Promise((resolve) => { 108 | if (after) { 109 | after(data, req.body, req, res); 110 | } 111 | resolve(); 112 | }); 113 | 114 | const errorPipe = (req, res, err) => new Promise(() => { 115 | const status = err.status || 500; 116 | const text = err.text || err.message || http.STATUS_CODES[status]; 117 | res.status(status).send(text); 118 | }); 119 | 120 | export default { 121 | afterPipe, 122 | authorizePipe, 123 | beforePipe, 124 | errorPipe, 125 | respondPipe, 126 | }; 127 | -------------------------------------------------------------------------------- /src/rest/delete.js: -------------------------------------------------------------------------------- 1 | export default (req, MongooseModel) => new Promise((resolve, reject) => { 2 | MongooseModel.remove({ _id: req.params.id }, (err, result) => { 3 | if (err) { 4 | return reject(err); 5 | } 6 | 7 | if (JSON.parse(result).n === 0) { 8 | const error = new Error('Not Found'); 9 | 10 | error.status = 404; 11 | return reject(error); 12 | } 13 | 14 | return resolve({ status: 204 }); 15 | }); 16 | }); 17 | -------------------------------------------------------------------------------- /src/rest/get.js: -------------------------------------------------------------------------------- 1 | export default (req, MongooseModel) => new Promise((resolve, reject) => { 2 | MongooseModel.findById(req.params.id, (err, entity) => { 3 | if (err) { 4 | return reject(err); 5 | } 6 | 7 | if (!entity) { 8 | const result = new Error('Not Found'); 9 | 10 | result.status = 404; 11 | return reject(result); 12 | } 13 | 14 | return resolve({ entity }); 15 | }); 16 | }); 17 | -------------------------------------------------------------------------------- /src/rest/index.js: -------------------------------------------------------------------------------- 1 | import { Router } from 'express'; 2 | import list from './list'; 3 | import post from './post'; 4 | import put from './put'; 5 | import del from './delete'; 6 | import patch from './patch'; 7 | import get from './get'; 8 | import pipes from '../pipes'; 9 | 10 | function addRestRoutes(router, routes, mongooseModel, options) { 11 | return routes.map((route) => { 12 | const { 13 | method, url, ctrl, hook, 14 | } = route; 15 | return router[method](url, (req, res) => { 16 | pipes.authorizePipe(req, res, hook.auth) 17 | .then(() => pipes.beforePipe(req, res, hook.before)) 18 | .then(() => ctrl(req, mongooseModel, options)) 19 | .then((result) => pipes.respondPipe(req, res, result || {})) 20 | .then((data) => pipes.afterPipe(req, res, hook.after, data)) 21 | .catch((err) => pipes.errorPipe(req, res, err)); 22 | }); 23 | }); 24 | } 25 | 26 | const getRouter = (mongooseModel, { url, hooks, options }) => { 27 | const resourceListURL = `/${url}`; 28 | const resourceURL = `${resourceListURL}\\(:id\\)`; 29 | 30 | const routes = [ 31 | { 32 | method: 'post', 33 | url: resourceListURL, 34 | ctrl: post, 35 | hook: hooks.post, 36 | }, 37 | { 38 | method: 'put', 39 | url: resourceURL, 40 | ctrl: put, 41 | hook: hooks.put, 42 | }, 43 | { 44 | method: 'patch', 45 | url: resourceURL, 46 | controller: patch, 47 | config: hooks.patch, 48 | }, 49 | { 50 | method: 'delete', 51 | url: resourceURL, 52 | ctrl: del, 53 | hook: hooks.delete, 54 | }, 55 | { 56 | method: 'get', 57 | url: resourceURL, 58 | ctrl: get, 59 | hook: hooks.get, 60 | }, 61 | { 62 | method: 'get', 63 | url: resourceListURL, 64 | ctrl: list, 65 | hook: hooks.list, 66 | }, 67 | ]; 68 | 69 | /*eslint-disable */ 70 | const router = Router(); 71 | /* eslint-enable */ 72 | addRestRoutes(router, routes, mongooseModel, options); 73 | return router; 74 | }; 75 | 76 | const getOperationRouter = (resourceUrl, actionUrl, fn, auth) => { 77 | /*eslint-disable */ 78 | const router = Router(); 79 | /* eslint-enable */ 80 | 81 | router.post(`${resourceUrl}${actionUrl}`, (req, res, next) => { 82 | pipes.authorizePipe(req, res, auth) 83 | .then(() => fn(req, res, next)) 84 | .catch((result) => pipes.errorPipe(req, res, result)); 85 | }); 86 | 87 | return router; 88 | }; 89 | 90 | export default { getRouter, getOperationRouter }; 91 | -------------------------------------------------------------------------------- /src/rest/list.js: -------------------------------------------------------------------------------- 1 | import countParser from '../parser/countParser'; 2 | import filterParser from '../parser/filterParser'; 3 | import orderbyParser from '../parser/orderbyParser'; 4 | import skipParser from '../parser/skipParser'; 5 | import topParser from '../parser/topParser'; 6 | import selectParser from '../parser/selectParser'; 7 | 8 | function _countQuery(model, { count, filter }) { 9 | return new Promise((resolve, reject) => { 10 | countParser(model, count, filter).then((dataCount) => (dataCount !== undefined 11 | ? resolve({ '@odata.count': dataCount }) 12 | : resolve({}) 13 | )).catch(reject); 14 | }); 15 | } 16 | 17 | function _dataQuery(model, { 18 | filter, orderby, skip, top, select, 19 | }, options) { 20 | return new Promise((resolve, reject) => { 21 | const query = model.find(); 22 | filterParser(query, filter) 23 | .then(() => orderbyParser(query, orderby || options.orderby)) 24 | .then(() => skipParser(query, skip, options.maxSkip)) 25 | .then(() => topParser(query, top, options.maxTop)) 26 | .then(() => selectParser(query, select)) 27 | .then(() => query.exec((err, data) => { 28 | if (err) { 29 | return reject(err); 30 | } 31 | return resolve({ value: data }); 32 | })) 33 | .catch(reject); 34 | }); 35 | } 36 | 37 | export default (req, MongooseModel, options) => new Promise((resolve, reject) => { 38 | const params = { 39 | count: req.query.$count, 40 | filter: req.query.$filter, 41 | orderby: req.query.$orderby, 42 | skip: req.query.$skip, 43 | top: req.query.$top, 44 | select: req.query.$select, 45 | // TODO expand: req.query.$expand, 46 | // TODO search: req.query.$search, 47 | }; 48 | 49 | Promise.all([ 50 | _countQuery(MongooseModel, params), 51 | _dataQuery(MongooseModel, params, options), 52 | ]).then((results) => { 53 | const entity = results.reduce((current, next) => ({ ...current, ...next })); 54 | resolve({ entity }); 55 | }).catch((err) => { 56 | const result = new Error(err.message); 57 | 58 | result.previous = err; 59 | result.status = 500; 60 | reject(result); 61 | }); 62 | }); 63 | -------------------------------------------------------------------------------- /src/rest/patch.js: -------------------------------------------------------------------------------- 1 | export default (req, MongooseModel) => new Promise((resolve, reject) => { 2 | MongooseModel.findOne({ id: req.params.id }, (err, entity) => { 3 | if (err) { 4 | reject(err); 5 | } else { 6 | MongooseModel.update({ id: req.params.id }, { ...entity, ...req.body }, (err1) => { 7 | if (err1) { 8 | reject(err1); 9 | } else { 10 | resolve({ entity: req.body, originEntity: entity }); 11 | } 12 | }); 13 | } 14 | }); 15 | }); 16 | -------------------------------------------------------------------------------- /src/rest/post.js: -------------------------------------------------------------------------------- 1 | export default (req, MongooseModel) => new Promise((resolve, reject) => { 2 | if (!Object.keys(req.body).length) { 3 | const error = new Error(); 4 | 5 | error.status = 422; 6 | reject(error); 7 | } else { 8 | const entity = MongooseModel.create(req.body); 9 | 10 | entity.save((err) => { 11 | if (err) { 12 | reject(err); 13 | } else { 14 | resolve({ status: 201, entity }); 15 | } 16 | }); 17 | } 18 | }); 19 | -------------------------------------------------------------------------------- /src/rest/put.js: -------------------------------------------------------------------------------- 1 | function _updateEntity(resolve, reject, MongooseModel, req, entity) { 2 | MongooseModel.findByIdAndUpdate(entity.id, req.body, (err) => { 3 | if (err) { 4 | return reject(err); 5 | } 6 | const newEntity = req.body; 7 | newEntity.id = entity.id; 8 | return resolve({ entity: newEntity, originEntity: entity }); 9 | }); 10 | } 11 | 12 | function _createEntity(resolve, reject, MongooseModel, req, entity) { 13 | const uuidReg = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i; 14 | if (!uuidReg.test(req.params.id)) { 15 | return reject({ status: 400 }, { text: 'Id is invalid.' }); 16 | } 17 | const newEntity = MongooseModel.create(req.body); 18 | newEntity._id = req.params.id; 19 | return newEntity.save((err) => { 20 | if (err) { 21 | return reject(err); 22 | } 23 | return resolve({ status: 201, entity: newEntity, originEntity: entity }); 24 | }); 25 | } 26 | 27 | export default (req, MongooseModel) => new Promise((resolve, reject) => { 28 | MongooseModel.findOne({ _id: req.params.id }, (err, entity) => { 29 | if (err) { 30 | return reject(err); 31 | } 32 | return entity 33 | ? _updateEntity(resolve, reject, MongooseModel, req, entity) 34 | : _createEntity(resolve, reject, MongooseModel, req, entity); 35 | }); 36 | }); 37 | -------------------------------------------------------------------------------- /src/server.js: -------------------------------------------------------------------------------- 1 | import createExpress from './express'; 2 | import Resource from './ODataResource'; 3 | import Func from './ODataFunction'; 4 | import Metadata from './metadata/ODataMetadata'; 5 | import Db from './db/db'; 6 | 7 | function checkAuth(auth, req) { 8 | return !auth || auth(req); 9 | } 10 | 11 | class Server { 12 | constructor(db, prefix, options) { 13 | this._app = createExpress(options); 14 | this._settings = { 15 | maxTop: 10000, 16 | maxSkip: 10000, 17 | orderby: undefined, 18 | }; 19 | this.defaultConfiguration(db, prefix); 20 | 21 | // TODO: Infact, resources is a mongooseModel instance, origin name is repositories. 22 | // Should mix _resources object and resources object: _resources + resource = resources. 23 | // Encapsulation to a object, separate mognoose, try to use *repository pattern*. 24 | // 这里也许应该让 resources 支持 odata 查询的, 以方便直接在代码中使用 OData 查询方式来进行数据筛选, 达到隔离 mongo 的效果. 25 | this.resources = {}; 26 | this._metadata = new Metadata(this); 27 | } 28 | 29 | function(url, middleware, params) { 30 | const func = new Func(url.replace(/[ /]+/, ''), middleware, params); 31 | 32 | this.resources[func.getName()] = func; 33 | } 34 | 35 | resource(name, model) { 36 | if (model === undefined) { 37 | return this.resources[name]; 38 | } 39 | 40 | const db = this.get('db'); 41 | this.resources[name] = new Resource(this, name, model); 42 | 43 | this.resources[name].setModel(db.register(name, model)); 44 | 45 | return this.resources[name]; 46 | } 47 | 48 | defaultConfiguration(db, prefix = '') { 49 | this.set('app', this._app); 50 | this.set('db', db); 51 | this.set('prefix', prefix); 52 | } 53 | 54 | post(url, callback, auth) { 55 | const app = this.get('app'); 56 | const prefix = this.get('prefix'); 57 | app.post(`${prefix}${url}`, (req, res, next) => { 58 | if (checkAuth(auth, req)) { 59 | callback(req, res, next); 60 | } else { 61 | res.status(401).end(); 62 | } 63 | }); 64 | } 65 | 66 | put(url, callback, auth) { 67 | const app = this.get('app'); 68 | const prefix = this.get('prefix'); 69 | app.put(`${prefix}${url}`, (req, res, next) => { 70 | if (checkAuth(auth, req)) { 71 | callback(req, res, next); 72 | } else { 73 | res.status(401).end(); 74 | } 75 | }); 76 | } 77 | 78 | delete(url, callback, auth) { 79 | const app = this.get('app'); 80 | const prefix = this.get('prefix'); 81 | app.delete(`${prefix}${url}`, (req, res, next) => { 82 | if (checkAuth(auth, req)) { 83 | callback(req, res, next); 84 | } else { 85 | res.status(401).end(); 86 | } 87 | }); 88 | } 89 | 90 | patch(url, callback, auth) { 91 | const app = this.get('app'); 92 | const prefix = this.get('prefix'); 93 | app.patch(`${prefix}${url}`, (req, res, next) => { 94 | if (checkAuth(auth, req)) { 95 | callback(req, res, next); 96 | } else { 97 | res.status(401).end(); 98 | } 99 | }); 100 | } 101 | 102 | listen(...args) { 103 | const router = this._metadata._router(); 104 | 105 | this._app.use(this.get('prefix'), router); 106 | 107 | Object.keys(this.resources).forEach((resourceKey) => { 108 | const resource = this.resources[resourceKey]; 109 | const resourceRouter = resource._router(this.getSettings()); 110 | 111 | this.use(this.get('prefix'), resourceRouter); 112 | 113 | if (resource.actions) { 114 | Object.keys(resource.actions).forEach((actionKey) => { 115 | const action = resource.actions[actionKey]; 116 | 117 | this.use(action.router); 118 | }); 119 | } 120 | }); 121 | 122 | return this._app.listen(...args); 123 | } 124 | 125 | getSettings() { 126 | return this._settings; 127 | } 128 | 129 | use(...args) { 130 | if (args[0] instanceof Resource) { 131 | const [resource] = args; 132 | this.resources[resource.getName()] = resource; 133 | return; 134 | } 135 | this._app.use(...args); 136 | } 137 | 138 | get(key, callback, auth) { 139 | if (callback === undefined) { 140 | return this._settings[key]; 141 | } 142 | // TODO: Need to refactor, same as L70-L80 143 | const app = this.get('app'); 144 | const prefix = this.get('prefix'); 145 | return app.get(`${prefix}${key}`, (req, res, next) => { 146 | if (checkAuth(auth, req)) { 147 | callback(req, res, next); 148 | } else { 149 | res.status(401).end(); 150 | } 151 | }); 152 | } 153 | 154 | set(key, val) { 155 | switch (key) { 156 | case 'db': { 157 | let db = val; 158 | 159 | if (typeof val === 'string') { 160 | db = new Db(); 161 | db.createConnection(val, null, (err) => { 162 | if (err) { 163 | console.error(err.message); 164 | console.error('Failed to connect to database on startup.'); 165 | process.exit(); 166 | } 167 | }); 168 | } 169 | 170 | this._settings[key] = db; 171 | break; 172 | } 173 | case 'prefix': { 174 | let prefix = val; 175 | if (prefix === '/') { 176 | prefix = ''; 177 | } 178 | if (prefix.length > 0 && prefix[0] !== '/') { 179 | prefix = `/${prefix}`; 180 | } 181 | this._settings[key] = prefix; 182 | break; 183 | } 184 | default: { 185 | this._settings[key] = val; 186 | break; 187 | } 188 | } 189 | return this; 190 | } 191 | 192 | // provide a event listener to handle not able to connect DB. 193 | on(name, event) { 194 | if (['connected', 'disconnected'].indexOf(name) > -1) { 195 | const db = this.get('db'); 196 | 197 | db.on(name, event); 198 | } 199 | } 200 | 201 | engine(...args) { 202 | this._app.engine(...args); 203 | } 204 | } 205 | 206 | export default Server; 207 | -------------------------------------------------------------------------------- /src/utils.js: -------------------------------------------------------------------------------- 1 | // like _.min 2 | export function min(arr) { 3 | return arr.map((item) => +item) 4 | .filter((item) => Number.isInteger(item)) 5 | .reduce((current, next) => (current < next ? current : next)); 6 | } 7 | 8 | function merge(list) { 9 | return list.join(' ').trim(); 10 | } 11 | 12 | /** 13 | * split by multiple keywords in a sentence 14 | * 15 | * @example 16 | split('Price le 200 and Price gt 3.5 or length(CompanyName) eq 19', ['and', 'or']) 17 | 18 | [ 19 | 'Price le 200', 20 | 'and', 21 | 'Price gt 3.5', 22 | 'or', 23 | 'length(CompanyName) eq 19' 24 | ] 25 | */ 26 | export function split(sentence, keys = []) { 27 | let keysArray = keys; 28 | if (!(keysArray instanceof Array)) { 29 | keysArray = [keysArray]; 30 | } 31 | const result = []; 32 | let tmp = []; 33 | const words = sentence.split(' '); 34 | words.forEach((word) => { 35 | if (keysArray.indexOf(word) > -1) { 36 | result.push(merge(tmp)); 37 | result.push(word); 38 | tmp = []; 39 | } else { 40 | tmp.push(word); 41 | } 42 | }); 43 | result.push(merge(tmp)); 44 | return result; 45 | } 46 | -------------------------------------------------------------------------------- /test/api.Function.js: -------------------------------------------------------------------------------- 1 | import 'should'; 2 | import request from 'supertest'; 3 | import { odata, host, port } from './support/setup'; 4 | import FakeDb from './support/fake-db'; 5 | 6 | describe('odata.api.Function', () => { 7 | let httpServer; 8 | 9 | before(() => { 10 | const db = new FakeDb(); 11 | const server = odata(db); 12 | server.function('/test', (req, res, next) => res.jsonp({ test: 'ok' })); 13 | httpServer = server.listen(port); 14 | }); 15 | 16 | after(() => { 17 | httpServer.close(); 18 | }); 19 | 20 | it('should work', async function() { 21 | const res = await request(host).get('/test'); 22 | res.body.should.be.have.property('test'); 23 | res.body.test.should.be.equal('ok'); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /test/api.Resource.js: -------------------------------------------------------------------------------- 1 | import 'should'; 2 | import request from 'supertest'; 3 | import { odata, host, port, bookSchema } from './support/setup'; 4 | import FakeDb from './support/fake-db'; 5 | 6 | describe('odata.api.Resouce', () => { 7 | let httpServer; 8 | 9 | before(() => { 10 | const db = new FakeDb(); 11 | const server = odata(db); 12 | server.resource('book', bookSchema); 13 | httpServer = server.listen(port); 14 | }); 15 | 16 | after(() => { 17 | httpServer.close(); 18 | }); 19 | 20 | it('should work', async function() { 21 | const res = await request(host).get('/book'); 22 | res.body.should.be.have.property('value'); 23 | }); 24 | }); 25 | -------------------------------------------------------------------------------- /test/hook.all.after.js: -------------------------------------------------------------------------------- 1 | import 'should'; 2 | import 'should-sinon'; 3 | import request from 'supertest'; 4 | import sinon from 'sinon'; 5 | import { odata, host, port, bookSchema, assertSuccess } from './support/setup'; 6 | import FakeDb from './support/fake-db'; 7 | import books from './support/books.json'; 8 | 9 | describe('hook.all.after', function() { 10 | let data, httpServer, server, db; 11 | 12 | beforeEach(async function() { 13 | db = new FakeDb(); 14 | server = odata(db); 15 | server.resource('book', bookSchema); 16 | data = db.addData('book', books); 17 | }); 18 | 19 | afterEach(() => { 20 | httpServer.close(); 21 | }); 22 | 23 | it('should work', async function() { 24 | const callback = sinon.spy(); 25 | server.resources.book.all().after((entity) => { 26 | entity.should.be.have.property('title'); 27 | callback(); 28 | }); 29 | httpServer = server.listen(port); 30 | const res = await request(host).get(`/book(${data[0].id})`); 31 | assertSuccess(res); 32 | callback.should.be.called(); 33 | }); 34 | 35 | it('should work with multiple hooks', async function() { 36 | const callback = sinon.spy(); 37 | server.resources.book.all().after(callback).after(callback); 38 | httpServer = server.listen(port); 39 | const res = await request(host).get(`/book(${data[0].id})`); 40 | assertSuccess(res); 41 | callback.should.be.calledTwice(); 42 | }); 43 | }); 44 | 45 | -------------------------------------------------------------------------------- /test/hook.all.before.js: -------------------------------------------------------------------------------- 1 | import 'should'; 2 | import 'should-sinon'; 3 | import request from 'supertest'; 4 | import sinon from 'sinon'; 5 | import { odata, host, port, bookSchema } from './support/setup'; 6 | import FakeDb from './support/fake-db'; 7 | import books from './support/books.json'; 8 | 9 | describe('hook.all.before', function() { 10 | let data, httpServer, server, db; 11 | 12 | beforeEach(async function() { 13 | db = new FakeDb(); 14 | server = odata(db); 15 | server.resource('book', bookSchema); 16 | 17 | data = db.addData('book', books); 18 | }); 19 | 20 | afterEach(() => { 21 | httpServer.close(); 22 | }); 23 | 24 | it('should work', async function() { 25 | const callback = sinon.spy(); 26 | server.resources.book.all().before((entity, req) => { 27 | req.params.should.be.have.property('id'); 28 | req.params.id.should.be.equal(data[0].id); 29 | callback(); 30 | }); 31 | httpServer = server.listen(port); 32 | await request(host).get(`/book(${data[0].id})`); 33 | callback.should.be.called(); 34 | }); 35 | it('should work with multiple hooks', async function() { 36 | const callback = sinon.spy(); 37 | server.resources.book.all().before(callback).before(callback); 38 | httpServer = server.listen(port); 39 | await request(host).get(`/book(${data[0].id})`); 40 | callback.should.be.calledTwice(); 41 | }); 42 | }); 43 | 44 | -------------------------------------------------------------------------------- /test/hook.delete.after.js: -------------------------------------------------------------------------------- 1 | import 'should'; 2 | import 'should-sinon'; 3 | import request from 'supertest'; 4 | import sinon from 'sinon'; 5 | import { odata, host, port, bookSchema, assertSuccess } from './support/setup'; 6 | import FakeDb from './support/fake-db'; 7 | import books from './support/books.json'; 8 | 9 | describe('hook.delete.after', function() { 10 | let data, httpServer, server, db; 11 | 12 | beforeEach(async function() { 13 | db = new FakeDb(); 14 | server = odata(db); 15 | server.resource('book', bookSchema); 16 | 17 | data = db.addData('book', books); 18 | }); 19 | 20 | afterEach(() => { 21 | httpServer.close(); 22 | }); 23 | 24 | it('should work', async function() { 25 | const callback = sinon.spy(); 26 | server.resources.book.delete().after((entity) => { 27 | callback(); 28 | }); 29 | httpServer = server.listen(port); 30 | const res = await request(host).delete(`/book(${data[0].id})`); 31 | assertSuccess(res); 32 | callback.should.be.called(); 33 | }); 34 | it('should work with multiple hooks', async function() { 35 | const callback = sinon.spy(); 36 | server.resources.book.delete().after(callback).after(callback); 37 | httpServer = server.listen(port); 38 | const res = await request(host).delete(`/book(${data[0].id})`); 39 | assertSuccess(res); 40 | callback.should.be.calledTwice(); 41 | }); 42 | }); 43 | -------------------------------------------------------------------------------- /test/hook.delete.before.js: -------------------------------------------------------------------------------- 1 | import 'should'; 2 | import 'should-sinon'; 3 | import request from 'supertest'; 4 | import sinon from 'sinon'; 5 | import { odata, host, port, bookSchema } from './support/setup'; 6 | import FakeDb from './support/fake-db'; 7 | import books from './support/books.json'; 8 | 9 | describe('hook.delete.before', function() { 10 | let data, httpServer, server, db; 11 | 12 | beforeEach(async function() { 13 | db = new FakeDb(); 14 | server = odata(db); 15 | server.resource('book', bookSchema); 16 | 17 | data = db.addData('book', books); 18 | }); 19 | 20 | afterEach(() => { 21 | httpServer.close(); 22 | }); 23 | 24 | it('should work', async function() { 25 | const callback = sinon.spy(); 26 | server.resources.book.delete().before((entity, req) => { 27 | req.params.should.be.have.property('id'); 28 | req.params.id.should.be.equal(data[0].id); 29 | callback(); 30 | }); 31 | httpServer = server.listen(port); 32 | await request(host).delete(`/book(${data[0].id})`); 33 | callback.should.be.called(); 34 | }); 35 | it('should work with multiple hooks', async function() { 36 | const callback = sinon.spy(); 37 | server.resources.book.delete().before(callback).before(callback); 38 | httpServer = server.listen(port); 39 | await request(host).delete(`/book(${data[0].id})`); 40 | callback.should.be.calledTwice(); 41 | }); 42 | }); 43 | -------------------------------------------------------------------------------- /test/hook.get.after.js: -------------------------------------------------------------------------------- 1 | import 'should'; 2 | import 'should-sinon'; 3 | import request from 'supertest'; 4 | import sinon from 'sinon'; 5 | import { host, port, bookSchema, odata } from './support/setup'; 6 | import FakeDb from './support/fake-db'; 7 | import books from './support/books.json'; 8 | 9 | describe('hook.get.after', function() { 10 | let data, httpServer, server, db; 11 | 12 | beforeEach(async function() { 13 | db = new FakeDb(); 14 | server = odata(db); 15 | server.resource('book', bookSchema); 16 | 17 | data = db.addData('book', books); 18 | }); 19 | 20 | afterEach(() => { 21 | httpServer.close(); 22 | }); 23 | 24 | it('should work', async function() { 25 | const callback = sinon.spy(); 26 | 27 | server.resources.book.get().after((entity) => { 28 | entity.should.be.have.property('title'); 29 | callback(); 30 | }); 31 | httpServer = server.listen(port); 32 | await request(host).get(`/book(${data[0].id})`); 33 | callback.should.be.called(); 34 | }); 35 | it('should work with multiple hooks', async function() { 36 | const callback = sinon.spy(); 37 | server.resources.book.get().after(callback).after(callback); 38 | httpServer = server.listen(port); 39 | await request(host).get(`/book(${data[0].id})`); 40 | callback.should.be.calledTwice(); 41 | }); 42 | }); 43 | -------------------------------------------------------------------------------- /test/hook.get.before.js: -------------------------------------------------------------------------------- 1 | import 'should'; 2 | import 'should-sinon'; 3 | import request from 'supertest'; 4 | import sinon from 'sinon'; 5 | import { host, port, bookSchema, odata } from './support/setup'; 6 | import FakeDb from './support/fake-db'; 7 | import books from './support/books.json'; 8 | 9 | describe('hook.get.before', function() { 10 | let data, httpServer, server, db; 11 | 12 | beforeEach(async function() { 13 | db = new FakeDb(); 14 | server = odata(db); 15 | server.resource('book', bookSchema); 16 | 17 | data = db.addData('book', books); 18 | }); 19 | 20 | afterEach(() => { 21 | httpServer.close(); 22 | }); 23 | 24 | it('should work', async function() { 25 | const callback = sinon.spy(); 26 | 27 | server.resources.book.get().before((entity, req) => { 28 | req.params.should.be.have.property('id'); 29 | req.params.id.should.be.equal(data[0].id); 30 | callback(); 31 | }); 32 | httpServer = server.listen(port); 33 | await request(host).get(`/book(${data[0].id})`); 34 | callback.should.be.called(); 35 | }); 36 | it('should work with multiple hooks', async function() { 37 | const callback = sinon.spy(); 38 | 39 | server.resources.book.get().before(callback).before(callback); 40 | httpServer = server.listen(port); 41 | await request(host).get(`/book(${data[0].id})`); 42 | callback.should.be.calledTwice(); 43 | }); 44 | }); 45 | -------------------------------------------------------------------------------- /test/hook.list.after.js: -------------------------------------------------------------------------------- 1 | import 'should'; 2 | import 'should-sinon'; 3 | import request from 'supertest'; 4 | import sinon from 'sinon'; 5 | import { host, port, bookSchema, odata } from './support/setup'; 6 | import FakeDb from './support/fake-db'; 7 | import books from './support/books.json'; 8 | 9 | describe('hook.list.after', function() { 10 | let data, httpServer, server, db; 11 | 12 | beforeEach(async function() { 13 | db = new FakeDb(); 14 | server = odata(db); 15 | server.resource('book', bookSchema); 16 | 17 | data = db.addData('book', books); 18 | }); 19 | 20 | afterEach(() => { 21 | httpServer.close(); 22 | }); 23 | 24 | it('should work', async function() { 25 | const callback = sinon.spy(); 26 | 27 | server.resources.book.list().after((result) => { 28 | result.should.be.have.property('value'); 29 | callback(); 30 | }); 31 | httpServer = server.listen(port); 32 | await request(host).get(`/book`); 33 | callback.should.be.called(); 34 | }); 35 | it('should work with multiple hooks', async function() { 36 | const callback = sinon.spy(); 37 | 38 | server.resources.book.list().after(callback).after(callback); 39 | httpServer = server.listen(port); 40 | await request(host).get(`/book`); 41 | callback.should.be.calledTwice(); 42 | }); 43 | }); 44 | 45 | -------------------------------------------------------------------------------- /test/hook.list.before.js: -------------------------------------------------------------------------------- 1 | import 'should'; 2 | import 'should-sinon'; 3 | import request from 'supertest'; 4 | import sinon from 'sinon'; 5 | import { host, port, bookSchema, odata } from './support/setup'; 6 | import FakeDb from './support/fake-db'; 7 | import books from './support/books.json'; 8 | 9 | describe('hook.list.before', function() { 10 | let data, httpServer, server, db; 11 | 12 | beforeEach(async function() { 13 | db = new FakeDb(); 14 | server = odata(db); 15 | server.resource('book', bookSchema); 16 | 17 | data = db.addData('book', books); 18 | }); 19 | 20 | afterEach(() => { 21 | httpServer.close(); 22 | }); 23 | 24 | it('should work', async function() { 25 | const callback = sinon.spy(); 26 | 27 | server.resources.book.list().before((entity, req) => { 28 | callback(); 29 | }); 30 | httpServer = server.listen(port); 31 | await request(host).get(`/book`); 32 | callback.should.be.called(); 33 | }); 34 | it('should work with multiple hooks', async function() { 35 | const callback = sinon.spy(); 36 | 37 | server.resources.book.list().before(callback).before(callback); 38 | httpServer = server.listen(port); 39 | await request(host).get(`/book`); 40 | callback.should.be.calledTwice(); 41 | }); 42 | }); 43 | 44 | -------------------------------------------------------------------------------- /test/hook.post.after.js: -------------------------------------------------------------------------------- 1 | import 'should'; 2 | import 'should-sinon'; 3 | import request from 'supertest'; 4 | import sinon from 'sinon'; 5 | import { odata, host, port, bookSchema } from './support/setup'; 6 | import FakeDb from './support/fake-db'; 7 | import books from './support/books.json'; 8 | 9 | describe('hook.post.after', function() { 10 | let data, httpServer, server, db; 11 | 12 | beforeEach(async function() { 13 | db = new FakeDb(); 14 | server = odata(db); 15 | server.resource('book', bookSchema); 16 | 17 | data = db.addData('book', books); 18 | }); 19 | 20 | afterEach(() => { 21 | httpServer.close(); 22 | }); 23 | 24 | it('should work', async function() { 25 | const callback = sinon.spy(); 26 | const TITLE = 'HOOK_POST_AFTER'; 27 | 28 | server.resources.book.post().after((entity) => { 29 | entity.should.be.have.property('title'); 30 | entity.title.should.be.equal(TITLE); 31 | callback(); 32 | }); 33 | httpServer = server.listen(port); 34 | await request(host).post(`/book`).send({ title: TITLE }); 35 | callback.should.be.called(); 36 | }); 37 | it('should work with multiple hooks', async function() { 38 | const callback = sinon.spy(); 39 | const TITLE = 'HOOK_POST_AFTER'; 40 | 41 | server.resources.book.post().after(callback).after(callback); 42 | httpServer = server.listen(port); 43 | await request(host).post(`/book`).send({ title: TITLE }); 44 | callback.should.be.calledTwice(); 45 | }); 46 | }); 47 | 48 | 49 | -------------------------------------------------------------------------------- /test/hook.post.before.js: -------------------------------------------------------------------------------- 1 | import 'should'; 2 | import 'should-sinon'; 3 | import request from 'supertest'; 4 | import sinon from 'sinon'; 5 | import { host, port, bookSchema, odata } from './support/setup'; 6 | import FakeDb from './support/fake-db'; 7 | import books from './support/books.json'; 8 | 9 | describe('hook.post.before', function() { 10 | let data, httpServer, server, db; 11 | 12 | beforeEach(async function() { 13 | db = new FakeDb(); 14 | server = odata(db); 15 | server.resource('book', bookSchema); 16 | 17 | data = db.addData('book', books); 18 | }); 19 | 20 | afterEach(() => { 21 | httpServer.close(); 22 | }); 23 | 24 | it('should work', async function() { 25 | const callback = sinon.spy(); 26 | const TITLE = 'HOOK_POST_BEFORE'; 27 | 28 | server.resources.book.post().before((entity, req) => { 29 | req.body.should.be.have.property('title'); 30 | req.body.title.should.be.equal(TITLE); 31 | callback(); 32 | }); 33 | httpServer = server.listen(port); 34 | await request(host).post(`/book`).send({ title: TITLE }); 35 | callback.should.be.called(); 36 | }); 37 | it('should work with multiple hooks', async function() { 38 | const callback = sinon.spy(); 39 | const TITLE = 'HOOK_POST_BEFORE'; 40 | 41 | server.resources.book.post().before(callback).before(callback); 42 | httpServer = server.listen(port); 43 | await request(host).post(`/book`).send({ title: TITLE }); 44 | callback.should.be.calledTwice(); 45 | }); 46 | }); 47 | 48 | 49 | -------------------------------------------------------------------------------- /test/hook.put.after.js: -------------------------------------------------------------------------------- 1 | import 'should'; 2 | import 'should-sinon'; 3 | import request from 'supertest'; 4 | import sinon from 'sinon'; 5 | import { host, port, bookSchema, odata } from './support/setup'; 6 | import FakeDb from './support/fake-db'; 7 | import books from './support/books.json'; 8 | 9 | describe('hook.put.after', function() { 10 | let data, httpServer, server, db; 11 | 12 | beforeEach(async function() { 13 | db = new FakeDb(); 14 | server = odata(db); 15 | server.resource('book', bookSchema); 16 | 17 | data = db.addData('book', books); 18 | }); 19 | 20 | afterEach(() => { 21 | httpServer.close(); 22 | }); 23 | 24 | it('should work', async function() { 25 | const callback = sinon.spy(); 26 | 27 | server.resources.book.put().after((entity) => { 28 | entity.should.be.have.property('title'); 29 | callback(); 30 | }); 31 | httpServer = server.listen(port); 32 | await request(host).put(`/book(${data[0].id})`).send(data[0]); 33 | callback.should.be.called(); 34 | }); 35 | it('should work with multiple hooks', async function() { 36 | const callback = sinon.spy(); 37 | 38 | server.resources.book.put().after(callback).after(callback); 39 | httpServer = server.listen(port); 40 | await request(host).put(`/book(${data[0].id})`).send(data[0]); 41 | callback.should.be.calledTwice(); 42 | }); 43 | }); 44 | -------------------------------------------------------------------------------- /test/hook.put.before.js: -------------------------------------------------------------------------------- 1 | import 'should'; 2 | import 'should-sinon'; 3 | import request from 'supertest'; 4 | import sinon from 'sinon'; 5 | import { host, port, bookSchema, odata } from './support/setup'; 6 | import FakeDb from './support/fake-db'; 7 | import books from './support/books.json'; 8 | 9 | describe('hook.put.before', function() { 10 | let data, httpServer, server, db; 11 | 12 | beforeEach(async function() { 13 | db = new FakeDb(); 14 | server = odata(db); 15 | server.resource('book', bookSchema); 16 | 17 | data = db.addData('book', books); 18 | }); 19 | 20 | afterEach(() => { 21 | httpServer.close(); 22 | }); 23 | 24 | it('should work', async function() { 25 | const callback = sinon.spy(); 26 | 27 | server.resources.book.put().before((entity, req) => { 28 | req.params.should.be.have.property('id'); 29 | req.params.id.should.be.equal(data[0].id); 30 | callback(); 31 | }); 32 | httpServer = server.listen(port); 33 | await request(host).put(`/book(${data[0].id})`).send(data[0]); 34 | callback.should.be.called(); 35 | }); 36 | it('should work with multiple hooks', async function() { 37 | const callback = sinon.spy(); 38 | 39 | server.resources.book.put().before(callback).before(callback); 40 | httpServer = server.listen(port); 41 | await request(host).put(`/book(${data[0].id})`).send(data[0]); 42 | callback.should.be.calledTwice(); 43 | }); 44 | }); 45 | -------------------------------------------------------------------------------- /test/metadata.action..js: -------------------------------------------------------------------------------- 1 | // For issue: https://github.com/TossShinHwa/node-odata/issues/96 2 | // For issue: https://github.com/TossShinHwa/node-odata/issues/25 3 | 4 | import 'should'; 5 | import request from 'supertest'; 6 | import { host, conn, port, odata, assertSuccess } from './support/setup'; 7 | import FakeDb from './support/fake-db'; 8 | 9 | describe('metadata.action', () => { 10 | let httpServer, server, db; 11 | 12 | beforeEach(async function() { 13 | db = new FakeDb(); 14 | server = odata(db); 15 | 16 | }); 17 | 18 | afterEach(() => { 19 | httpServer.close(); 20 | }); 21 | 22 | it('should return json metadata for action that bound to instance', async function() { 23 | const jsonDocument = { 24 | $Version: '4.0', 25 | ObjectId: { 26 | $Kind: "TypeDefinition", 27 | $UnderlyingType: "Edm.String", 28 | $MaxLength: 24 29 | }, 30 | 'bound-action': { 31 | $Kind: 'Action', 32 | $IsBound: true, 33 | $Parameter: [{ 34 | $Name: 'book', 35 | $Type: 'self.book' 36 | }] 37 | }, 38 | 'book': { 39 | $Kind: "EntityType", 40 | $Key: ["id"], 41 | id: { 42 | $Type: "self.ObjectId", 43 | $Nullable: false, 44 | }, 45 | author: { 46 | $Type: 'Edm.String' 47 | } 48 | }, 49 | $EntityContainer: 'org.example.DemoService', 50 | ['org.example.DemoService']: { 51 | $Kind: 'EntityContainer', 52 | 'book': { 53 | $Collection: true, 54 | $Type: `self.book`, 55 | } 56 | }, 57 | }; 58 | server.resource('book', { 59 | author: String 60 | }).action('bound-action', 61 | (req, res, next) => {}, 62 | { binding: 'entity' }); 63 | httpServer = server.listen(port); 64 | const res = await request(host).get('/$metadata?$format=json'); 65 | assertSuccess(res); 66 | res.body.should.deepEqual(jsonDocument); 67 | }); 68 | 69 | it('should return xml metadata for action that bound to instance', async function() { 70 | const xmlDocument = 71 | ` 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | `.replace(/\s*\s*/g, '>'); 92 | server.resource('book', { 93 | author: String 94 | }).action('bound-action', 95 | (req, res, next) => {}, 96 | { binding: 'entity' }); 97 | httpServer = server.listen(port); 98 | const res = await request(host).get('/$metadata').set('accept', 'application/xml'); 99 | assertSuccess(res); 100 | res.text.should.equal(xmlDocument); 101 | }); 102 | 103 | it('should return json metadata for action that bound to collection', async function() { 104 | const jsonDocument = { 105 | $Version: '4.0', 106 | ObjectId: { 107 | $Kind: "TypeDefinition", 108 | $UnderlyingType: "Edm.String", 109 | $MaxLength: 24 110 | }, 111 | 'bound-action': { 112 | $Kind: 'Action', 113 | $IsBound: true, 114 | $Parameter: [{ 115 | $Name: 'book', 116 | $Type: 'self.book', 117 | $Collection: true 118 | }] 119 | }, 120 | 'book': { 121 | $Kind: "EntityType", 122 | $Key: ["id"], 123 | id: { 124 | $Type: "self.ObjectId", 125 | $Nullable: false, 126 | }, 127 | author: { 128 | $Type: 'Edm.String' 129 | } 130 | }, 131 | $EntityContainer: 'org.example.DemoService', 132 | ['org.example.DemoService']: { 133 | $Kind: 'EntityContainer', 134 | 'book': { 135 | $Collection: true, 136 | $Type: `self.book`, 137 | } 138 | }, 139 | }; 140 | server.resource('book', { 141 | author: String 142 | }).action('bound-action', 143 | (req, res, next) => {}, 144 | { binding: 'collection' }); 145 | httpServer = server.listen(port); 146 | const res = await request(host).get('/$metadata?$format=json'); 147 | assertSuccess(res); 148 | res.body.should.deepEqual(jsonDocument); 149 | }); 150 | 151 | it('should return xml metadata for action that bound to collection', async function() { 152 | const xmlDocument = 153 | ` 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | 169 | 170 | 171 | 172 | 173 | `.replace(/\s*\s*/g, '>'); 174 | server.resource('book', { 175 | author: String 176 | }).action('bound-action', 177 | (req, res, next) => {}, 178 | { binding: 'collection' }); 179 | httpServer = server.listen(port); 180 | const res = await request(host).get('/$metadata').set('accept', 'application/xml'); 181 | assertSuccess(res); 182 | res.text.should.equal(xmlDocument); 183 | }); 184 | }); 185 | -------------------------------------------------------------------------------- /test/metadata.format.js: -------------------------------------------------------------------------------- 1 | // For issue: https://github.com/TossShinHwa/node-odata/issues/96 2 | // For issue: https://github.com/TossShinHwa/node-odata/issues/25 3 | 4 | import 'should'; 5 | import request from 'supertest'; 6 | import { host, conn, port, bookSchema, odata, assertSuccess } from './support/setup'; 7 | import FakeDb from './support/fake-db'; 8 | 9 | describe('metadata.format', () => { 10 | let httpServer, server, db; 11 | 12 | const jsonDocument = { 13 | $Version: '4.0', 14 | ObjectId: { 15 | $Kind: "TypeDefinition", 16 | $UnderlyingType: "Edm.String", 17 | $MaxLength: 24 18 | }, 19 | book: { 20 | $Kind: "EntityType", 21 | $Key: ["id"], 22 | id: { 23 | $Type: "self.ObjectId", 24 | $Nullable: false, 25 | }, 26 | author: { 27 | $Type: 'Edm.String' 28 | }, 29 | description: { 30 | $Type: 'Edm.String' 31 | }, 32 | genre: { 33 | $Type: 'Edm.String' 34 | }, 35 | price: { 36 | $Type: 'Edm.Double' 37 | }, 38 | publish_date: { 39 | $Type: 'Edm.DateTimeOffset' 40 | }, 41 | title: { 42 | $Type: 'Edm.String' 43 | } 44 | }, 45 | $EntityContainer: 'org.example.DemoService', 46 | ['org.example.DemoService']: { 47 | $Kind: 'EntityContainer', 48 | book: { 49 | $Collection: true, 50 | $Type: `self.book`, 51 | } 52 | }, 53 | }; 54 | const xmlDocument = 55 | ` 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | `.replace(/\s*\s*/g, '>'); 78 | 79 | beforeEach(async function() { 80 | db = new FakeDb(); 81 | server = odata(db); 82 | server.resource('book', bookSchema); 83 | 84 | }); 85 | 86 | afterEach(() => { 87 | httpServer.close(); 88 | }); 89 | 90 | it('should return xml if no format given', async function() { 91 | httpServer = server.listen(port); 92 | const res = await request(host).get('/$metadata'); 93 | assertSuccess(res); 94 | checkContentType(res, 'application/xml'); 95 | res.text.should.equal(xmlDocument); 96 | }); 97 | 98 | it('should return json according accept header', async function() { 99 | httpServer = server.listen(port); 100 | const res = await request(host).get('/$metadata').set('accept', 'application/json'); 101 | assertSuccess(res); 102 | checkContentType(res, 'application/json'); 103 | res.body.should.deepEqual(jsonDocument); 104 | }); 105 | 106 | it('should return xml if $format overrides accept header', async function() { 107 | httpServer = server.listen(port); 108 | const res = await request(host).get('/$metadata?$format=json').set('accept', 'application/xml'); 109 | res.statusCode.should.equal(200); 110 | checkContentType(res, 'application/json'); 111 | res.body.should.deepEqual(jsonDocument); 112 | }); 113 | }); 114 | 115 | 116 | function checkContentType(res, value) { 117 | res.header.should.have.property('content-type'); 118 | res.header['content-type'].should.containEql(value); 119 | } -------------------------------------------------------------------------------- /test/metadata.function.js: -------------------------------------------------------------------------------- 1 | // For issue: https://github.com/TossShinHwa/node-odata/issues/96 2 | // For issue: https://github.com/TossShinHwa/node-odata/issues/25 3 | 4 | import 'should'; 5 | import request from 'supertest'; 6 | import { host, conn, port, odata, assertSuccess } from './support/setup'; 7 | import FakeDb from './support/fake-db'; 8 | 9 | describe('metadata.function', () => { 10 | let httpServer, server, db; 11 | 12 | beforeEach(async function() { 13 | db = new FakeDb(); 14 | server = odata(db); 15 | 16 | }); 17 | 18 | afterEach(() => { 19 | httpServer.close(); 20 | }); 21 | 22 | it('should return json metadata for function', async function() { 23 | const jsonDocument = { 24 | $Version: '4.0', 25 | ObjectId: { 26 | $Kind: "TypeDefinition", 27 | $UnderlyingType: "Edm.String", 28 | $MaxLength: 24 29 | }, 30 | 'odata-function': { 31 | $Kind: 'Function', 32 | $ReturnType: { 33 | $Type: 'Edm.String' 34 | } 35 | }, 36 | $EntityContainer: 'org.example.DemoService', 37 | ['org.example.DemoService']: { 38 | $Kind: 'EntityContainer', 39 | 'odata-function': { 40 | $Function: 'self.odata-function' 41 | } 42 | }, 43 | }; 44 | server.function('/odata-function', 45 | () => {}, 46 | { 47 | $ReturnType: { 48 | $Type: 'Edm.String' 49 | }}); 50 | httpServer = server.listen(port); 51 | const res = await request(host).get('/$metadata?$format=json'); 52 | assertSuccess(res); 53 | res.body.should.deepEqual(jsonDocument); 54 | }); 55 | 56 | it('should return xml metadata for function', async function() { 57 | const xmlDocument = 58 | ` 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | `.replace(/\s*\s*/g, '>'); 72 | server.function('odata-function', 73 | (req, res, next) => {}, 74 | { $ReturnType: { 75 | $Type: 'Edm.String' 76 | }}); 77 | httpServer = server.listen(port); 78 | const res = await request(host).get('/$metadata'); 79 | assertSuccess(res); 80 | res.text.should.equal(xmlDocument); 81 | }); 82 | }); 83 | -------------------------------------------------------------------------------- /test/metadata.resource.complex.js: -------------------------------------------------------------------------------- 1 | // For issue: https://github.com/TossShinHwa/node-odata/issues/96 2 | // For issue: https://github.com/TossShinHwa/node-odata/issues/25 3 | 4 | import 'should'; 5 | import request from 'supertest'; 6 | import { host, conn, port, odata, assertSuccess } from './support/setup'; 7 | import FakeDb from './support/fake-db'; 8 | 9 | describe('metadata.resource.complex', () => { 10 | let httpServer, server, db; 11 | 12 | beforeEach(async function() { 13 | db = new FakeDb(); 14 | server = odata(db); 15 | }); 16 | 17 | afterEach(() => { 18 | httpServer.close(); 19 | }); 20 | 21 | it('should return json metadata for nested document array', async function() { 22 | const jsonDocument = { 23 | $Version: '4.0', 24 | ObjectId: { 25 | $Kind: "TypeDefinition", 26 | $UnderlyingType: "Edm.String", 27 | $MaxLength: 24 28 | }, 29 | p1Child1: { 30 | $Kind: 'ComplexType', 31 | p2: { 32 | $Type: 'Edm.String' 33 | } 34 | }, 35 | 'complex-model': { 36 | $Kind: "EntityType", 37 | $Key: ["id"], 38 | id: { 39 | $Type: "self.ObjectId", 40 | $Nullable: false, 41 | }, 42 | p1: { 43 | $Type: 'self.p1Child1', 44 | $Collection: true 45 | } 46 | }, 47 | $EntityContainer: 'org.example.DemoService', 48 | ['org.example.DemoService']: { 49 | $Kind: 'EntityContainer', 50 | 'complex-model': { 51 | $Collection: true, 52 | $Type: `self.complex-model`, 53 | } 54 | }, 55 | }; 56 | server.resource('complex-model', { 57 | p1: [{ // array of objects 58 | p2: String 59 | }] 60 | }); 61 | httpServer = server.listen(port); 62 | const res = await request(host).get('/$metadata?$format=json'); 63 | assertSuccess(res); 64 | res.body.should.deepEqual(jsonDocument); 65 | }); 66 | 67 | it('should return xml metadata for nested document array', async function() { 68 | const xmlDocument = 69 | ` 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | `.replace(/\s*\s*/g, '>'); 90 | server.resource('complex-model', { 91 | p1: [{ // array of objects 92 | p2: String 93 | }] 94 | }); 95 | httpServer = server.listen(port); 96 | const res = await request(host).get('/$metadata').set('accept', 'application/xml'); 97 | assertSuccess(res); 98 | res.text.should.equal(xmlDocument); 99 | }); 100 | 101 | it('should return json metadata for nested array', async function() { 102 | const jsonDocument = { 103 | $Version: '4.0', 104 | ObjectId: { 105 | $Kind: "TypeDefinition", 106 | $UnderlyingType: "Edm.String", 107 | $MaxLength: 24 108 | }, 109 | 'complex-model': { 110 | $Kind: "EntityType", 111 | $Key: ["id"], 112 | id: { 113 | $Type: "self.ObjectId", 114 | $Nullable: false, 115 | }, 116 | p3: { 117 | $Type: 'Edm.String', 118 | $Collection: true 119 | } 120 | }, 121 | $EntityContainer: 'org.example.DemoService', 122 | ['org.example.DemoService']: { 123 | $Kind: 'EntityContainer', 124 | 'complex-model': { 125 | $Collection: true, 126 | $Type: `self.complex-model`, 127 | } 128 | }, 129 | }; 130 | server.resource('complex-model', { 131 | p3: [String], // array of primitive type 132 | }); 133 | httpServer = server.listen(port); 134 | const res = await request(host).get('/$metadata?$format=json').set('accept', 'application/json'); 135 | res.statusCode.should.equal(200); 136 | res.body.should.deepEqual(jsonDocument); 137 | }); 138 | 139 | it('should return xml metadata for nested array', async function() { 140 | const xmlDocument = 141 | ` 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | `.replace(/\s*\s*/g, '>'); 159 | server.resource('complex-model', { 160 | p3: [String] 161 | }); 162 | httpServer = server.listen(port); 163 | const res = await request(host).get('/$metadata').set('accept', 'application/xml'); 164 | assertSuccess(res); 165 | res.text.should.equal(xmlDocument); 166 | }); 167 | 168 | it('should return json metadata for nested document', async function() { 169 | const jsonDocument = { 170 | $Version: '4.0', 171 | ObjectId: { 172 | $Kind: "TypeDefinition", 173 | $UnderlyingType: "Edm.String", 174 | $MaxLength: 24 175 | }, 176 | 'complex-model': { 177 | $Kind: "EntityType", 178 | $Key: ["id"], 179 | id: { 180 | $Type: "self.ObjectId", 181 | $Nullable: false, 182 | }, 183 | 'p4.p5': { 184 | $Type: 'Edm.String' 185 | } 186 | }, 187 | $EntityContainer: 'org.example.DemoService', 188 | ['org.example.DemoService']: { 189 | $Kind: 'EntityContainer', 190 | 'complex-model': { 191 | $Collection: true, 192 | $Type: `self.complex-model`, 193 | } 194 | }, 195 | }; 196 | server.resource('complex-model', { 197 | p4: { 198 | p5: String 199 | } 200 | }); 201 | httpServer = server.listen(port); 202 | const res = await request(host).get('/$metadata?$format=json'); 203 | res.statusCode.should.equal(200); 204 | res.body.should.deepEqual(jsonDocument); 205 | }); 206 | 207 | it('should return xml metadata for nested document', async function() { 208 | const xmlDocument = 209 | ` 210 | 211 | 212 | 213 | 214 | 215 | 216 | 217 | 218 | 219 | 220 | 221 | 222 | 223 | 224 | 225 | 226 | `.replace(/\s*\s*/g, '>'); 227 | server.resource('complex-model', { 228 | p4: { 229 | p5: String 230 | } 231 | }); 232 | httpServer = server.listen(port); 233 | const res = await request(host).get('/$metadata'); 234 | assertSuccess(res); 235 | res.text.should.equal(xmlDocument); 236 | }); 237 | }); 238 | -------------------------------------------------------------------------------- /test/model.complex.action.js: -------------------------------------------------------------------------------- 1 | // For issue: https://github.com/TossShinHwa/node-odata/issues/69 2 | 3 | import 'should'; 4 | import request from 'supertest'; 5 | import { odata, host, port, assertSuccess } from './support/setup'; 6 | import FakeDb from './support/fake-db'; 7 | 8 | describe('model.complex.action', () => { 9 | let httpServer; 10 | 11 | before(() => { 12 | const db = new FakeDb(); 13 | const server = odata(db); 14 | const resource = server.resource('order', { product: [{ price: Number }] }); 15 | 16 | resource.action('/all-item-greater', (req, res, next) => { 17 | const { price } = req.query; 18 | const $elemMatch = { price: { $gt: price } }; 19 | server.resources.order.model.exec((err, data) => res.jsonp(data.slice(1))); 20 | }, { binding : 'collection' }); 21 | httpServer = server.listen(port); 22 | }); 23 | 24 | after(() => { 25 | httpServer.close(); 26 | }); 27 | 28 | it('should work when PUT a complex entity', async function () { 29 | const entities = [{ 30 | product: [ 31 | { price: 1 }, 32 | { price: 2 }, 33 | { price: 4 }, 34 | ], 35 | }, { 36 | product: [ 37 | { price: 32 }, 38 | { price: 64 }, 39 | { price: 99 }, 40 | ], 41 | }]; 42 | entities.forEach(async entity => await request(host).post('/order').send(entity)); 43 | 44 | const res = await request(host).post(`/order/all-item-greater`); 45 | assertSuccess(res); 46 | res.body.should.matchEach((item) => { 47 | return item.product[0].price > 30 48 | && item.product[1].price > 30 49 | && item.product[2].price > 30; 50 | }); 51 | }); 52 | }); 53 | -------------------------------------------------------------------------------- /test/model.complex.filter.js: -------------------------------------------------------------------------------- 1 | // For issue: https://github.com/TossShinHwa/node-odata/issues/69 2 | 3 | import 'should'; 4 | import 'should-sinon'; 5 | import request from 'supertest'; 6 | import { odata, assertSuccess, host, port } from './support/setup'; 7 | import Db from './support/fake-db'; 8 | import sinon from 'sinon'; 9 | 10 | describe('model.complex.filter', () => { 11 | let httpServer, db, mock; 12 | 13 | before(() => { 14 | db = new Db(); 15 | const server = odata(db); 16 | const resource = server.resource('complex-model-filter', { product: [{ price: Number }] }); 17 | 18 | mock = sinon.mock(resource.model); 19 | mock.expects('where').once().withArgs('product.price').returns(resource.model); 20 | mock.expects('gt').once().withArgs(30).returns(resource.model); 21 | httpServer = server.listen(port); 22 | }); 23 | 24 | after(() => { 25 | httpServer.close(); 26 | }); 27 | 28 | it('should work when PUT a complex entity', async function() { 29 | const res = await request(host).get(`/complex-model-filter?$filter=product.price gt 30`); 30 | 31 | assertSuccess(res); 32 | mock.verify(); 33 | }); 34 | }); 35 | -------------------------------------------------------------------------------- /test/model.complex.js: -------------------------------------------------------------------------------- 1 | // For issue: https://github.com/TossShinHwa/node-odata/issues/55 2 | 3 | import 'should'; 4 | import request from 'supertest'; 5 | import { odata, host, port } from './support/setup'; 6 | import FakeDb from './support/fake-db'; 7 | 8 | function addResource() { 9 | return request(host) 10 | .post('/complex-model') 11 | .send({ p1: [{ p2: 'origin' }] }); 12 | } 13 | 14 | function updateResouce(id) { 15 | return request(host) 16 | .put(`/complex-model(${id})`) 17 | .send({ p1: [{ p2: 'new' }] }); 18 | } 19 | 20 | function queryResource(id) { 21 | return request(host) 22 | .get(`/complex-model(${id})`); 23 | } 24 | 25 | describe('model.complex', () => { 26 | let httpServer; 27 | 28 | before(() => { 29 | const db = new FakeDb(); 30 | const server = odata(db); 31 | server.resource('complex-model', { p1: [{ p2: String }] }); 32 | httpServer = server.listen(port); 33 | }); 34 | 35 | after(() => { 36 | httpServer.close(); 37 | }); 38 | 39 | it('should work when PUT a complex entity', async function() { 40 | const entity = await addResource(); 41 | await updateResouce(entity.body.id); 42 | const res = await queryResource(entity.body.id); 43 | res.body.p1[0].p2.should.be.equal('new'); 44 | }); 45 | }); 46 | -------------------------------------------------------------------------------- /test/model.custom.id.js: -------------------------------------------------------------------------------- 1 | import 'should'; 2 | import request from 'supertest'; 3 | import fakeDb from './support/fake-db'; 4 | import { odata, host, port } from './support/setup'; 5 | 6 | describe('model.custom.id', () => { 7 | let httpServer; 8 | 9 | before(async function() { 10 | const db = new fakeDb(); 11 | const server = odata(db); 12 | server.resource('custom-id', { id: Number }); 13 | httpServer = server.listen(port); 14 | await request(host).post('/custom-id').send({ id: 100 }); 15 | }); 16 | 17 | after(() => { 18 | httpServer.close(); 19 | }); 20 | 21 | it('should work when use a custom id to query specific entity', async function() { 22 | const res = await request(host).get('/custom-id(100)'); 23 | res.body.id.should.be.equal(100); 24 | }); 25 | 26 | it('should work when use a custom id to query a list', async function() { 27 | const res = await request(host).get('/custom-id?$filter=id eq \'100\''); 28 | res.body.value.length.should.be.greaterThan(0); 29 | }); 30 | }); 31 | -------------------------------------------------------------------------------- /test/model.hidden.field.js: -------------------------------------------------------------------------------- 1 | import 'should'; 2 | import sinon from 'sinon'; 3 | import request from 'supertest'; 4 | import FakeDb from './support/fake-db'; 5 | import { odata, host, port } from './support/setup'; 6 | 7 | describe('model.hidden.field', function () { 8 | let httpServer, id, resource, mock; 9 | 10 | before(async function () { 11 | const db = new FakeDb(); 12 | const server = odata(db); 13 | resource = server.resource('hidden-field', { 14 | name: String, 15 | password: { 16 | type: String, 17 | select: false 18 | } 19 | }); 20 | httpServer = server.listen(port); 21 | const data = db.addData('hidden-field', [{ 22 | name: 'zack', 23 | password: '123' 24 | }]); 25 | id = data[0].id; 26 | }); 27 | 28 | after(() => { 29 | httpServer.close(); 30 | }); 31 | 32 | afterEach(() => { 33 | mock.restore(); 34 | }); 35 | 36 | it('should work when get entities list even it is selected', async function () { 37 | mock = sinon.mock(resource.model); 38 | mock.expects('select').once().withArgs({ 39 | _id: 0, 40 | name: 1 41 | }).returns(resource.model); 42 | await request(host).get('/hidden-field?$select=name, password'); 43 | mock.verify(); 44 | }); 45 | 46 | it('should work when get entities list even only it is selected', async function () { 47 | mock = sinon.mock(resource.model); 48 | mock.expects('select').never().returns(resource.model); 49 | await request(host).get('/hidden-field?$select=password'); 50 | mock.verify(); 51 | }); 52 | }); 53 | -------------------------------------------------------------------------------- /test/model.special.name.js: -------------------------------------------------------------------------------- 1 | import 'should'; 2 | import request from 'supertest'; 3 | import { odata, host, port } from './support/setup'; 4 | import FakeDb from './support/fake-db'; 5 | 6 | describe('model.special.name', () => { 7 | let httpServer; 8 | 9 | before(() => { 10 | const db = new FakeDb(); 11 | const server = odata(db); 12 | server.resource('funcion-keyword', { year: Number }); 13 | httpServer = server.listen(port); 14 | }); 15 | 16 | after(() => { 17 | httpServer.close(); 18 | }); 19 | 20 | it('should work when use odata function keyword', async function() { 21 | const res = await request(host) 22 | .post('/funcion-keyword') 23 | .send({ year: 2015 }); 24 | res.status.should.be.equal(201); 25 | }); 26 | }); 27 | -------------------------------------------------------------------------------- /test/odata.actions.js: -------------------------------------------------------------------------------- 1 | import 'should'; 2 | import request from 'supertest'; 3 | import { odata, host, port, bookSchema } from './support/setup'; 4 | import data from './support/books.json'; 5 | import FakeDb from './support/fake-db'; 6 | 7 | function requestToHalfPrice(id) { 8 | return request(host).post(`/book(${id})/50off`); 9 | } 10 | 11 | function halfPrice(price) { 12 | return +(price / 2).toFixed(2); 13 | } 14 | 15 | describe('odata.actions', () => { 16 | let httpServer, books; 17 | 18 | before(async function() { 19 | const db = new FakeDb(); 20 | const server = odata(db); 21 | server 22 | .resource('book', bookSchema) 23 | .action('/50off', (req, res, next) => { 24 | server.resources.book.model.findById(req.params.id, (err, book) => { 25 | book.price = halfPrice(book.price); 26 | book.save((err) => res.jsonp(book)); 27 | }); 28 | }); 29 | books = JSON.parse( JSON.stringify( db.addData('book', data))); 30 | httpServer = server.listen(port); 31 | }); 32 | 33 | after(() => { 34 | httpServer.close(); 35 | }); 36 | 37 | it('should work', async function() { 38 | const item = books[0]; 39 | const res = await requestToHalfPrice(item.id); 40 | const price = halfPrice(item.price); 41 | res.body.price.should.be.equal(price); 42 | }); 43 | }); 44 | -------------------------------------------------------------------------------- /test/odata.functions.js: -------------------------------------------------------------------------------- 1 | import 'should'; 2 | import request from 'supertest'; 3 | import { odata, host, port, assertSuccess } from './support/setup'; 4 | import FakeDb from './support/fake-db'; 5 | 6 | describe('odata.functions', () => { 7 | ['get', 'post', 'put', 'delete'].map((method) => { 8 | describe(method, function(done) { 9 | let httpServer; 10 | 11 | before(() => { 12 | const server = odata(new FakeDb()); 13 | server.function('test', 14 | (req, res, next) => res.jsonp({ test: 'ok' }), 15 | { method }); 16 | httpServer = server.listen(port); 17 | }); 18 | 19 | after(() => { 20 | httpServer.close(); 21 | }); 22 | 23 | it('should work', async function() { 24 | const res = await request(host)[method]("/test"); 25 | assertSuccess(res); 26 | res.body.test.should.be.equal('ok'); 27 | }); 28 | }); 29 | }); 30 | }); 31 | -------------------------------------------------------------------------------- /test/odata.query.count.js: -------------------------------------------------------------------------------- 1 | import 'should'; 2 | import request from 'supertest'; 3 | import { odata, host, port, bookSchema } from './support/setup'; 4 | import books from './support/books.json'; 5 | import FakeDb from './support/fake-db'; 6 | 7 | describe('odata.query.count', function() { 8 | let httpServer; 9 | 10 | before(async function() { 11 | const db = new FakeDb(); 12 | const server = odata(db); 13 | server.resource('book', bookSchema); 14 | db.addData('book', books); 15 | httpServer = server.listen(port); 16 | }); 17 | 18 | after(() => { 19 | httpServer.close(); 20 | }); 21 | 22 | it('should get count', async function() { 23 | const res = await request(host).get('/book?$count=true'); 24 | res.body.should.be.have.property('@odata.count'); 25 | res.body.should.be.have.property('value'); 26 | res.body['@odata.count'].should.be.equal(res.body.value.length); 27 | }); 28 | it('should not get count', async function() { 29 | const res = await request(host).get('/book?$count=false'); 30 | res.body.should.be.not.have.property('@odata.count'); 31 | }); 32 | it('should 500 when $count isn\'t \'true\' or \'false\'', async function() { 33 | const res = await request(host).get('/book?$count=1'); 34 | res.error.status.should.be.equal(500); 35 | }); 36 | }); 37 | -------------------------------------------------------------------------------- /test/odata.query.filter.functions.js: -------------------------------------------------------------------------------- 1 | import 'should'; 2 | import sinon from 'sinon'; 3 | import request from 'supertest'; 4 | import { odata, host, port, bookSchema } from './support/setup'; 5 | import data from './support/books.json'; 6 | import FakeDb from './support/fake-db'; 7 | 8 | describe('odata.query.filter.functions', function () { 9 | let httpServer, mock, resource; 10 | 11 | before(async function () { 12 | const db = new FakeDb(); 13 | const server = odata(db); 14 | resource = server.resource('book', bookSchema) 15 | httpServer = server.listen(port); 16 | db.addData('book', data); 17 | }); 18 | 19 | after(() => { 20 | httpServer.close(); 21 | }); 22 | 23 | describe('[contains]', () => { 24 | it('should filter items', async function () { 25 | mock = sinon.mock(resource.model); 26 | mock.expects('$where').once().withArgs(`this.title.indexOf('i') != -1`).returns(resource.model); 27 | await request(host).get(`/book?$filter=contains(title,'i')`); 28 | mock.verify(); 29 | }); 30 | it('should filter items when it has extra spaces in query string', async function () { 31 | mock = sinon.mock(resource.model); 32 | mock.expects('$where').once().withArgs(`this.title.indexOf('Visual Studio') != -1`).returns(resource.model); 33 | await request(host).get(`/book?$filter=contains(title,'Visual Studio')`); 34 | mock.verify(); 35 | }); 36 | }); 37 | 38 | describe('[indexof]', () => { 39 | it('should filter items', async function () { 40 | mock = sinon.mock(resource.model); 41 | mock.expects('$where').once().withArgs(`this.title.indexOf('i') >= 1`).returns(resource.model); 42 | await request(host).get(`/book?$filter=indexof(title,'i') ge 1`); 43 | mock.verify(); 44 | }); 45 | it('should filter items when it has extra spaces in query string', async function () { 46 | mock = sinon.mock(resource.model); 47 | mock.expects('$where').once().withArgs(`this.title.indexOf('Visual Studio') >= 0`).returns(resource.model); 48 | const res = await request(host).get(`/book?$filter=indexof(title,'Visual Studio') ge 0`); 49 | mock.verify(); 50 | }); 51 | }); 52 | 53 | describe('[year]', () => { 54 | it('should filter items', async function () { 55 | mock = sinon.mock(resource.model); 56 | mock.expects('where').once().withArgs(`publish_date`).returns(resource.model); 57 | mock.expects('gte').once().withArgs(new Date(2000, 0, 1)).returns(resource.model); 58 | mock.expects('lt').once().withArgs(new Date(2001, 0, 1)).returns(resource.model); 59 | const res = await request(host).get(`/book?$filter=year(publish_date) eq 2000`); 60 | mock.verify(); 61 | }); 62 | }); 63 | }); 64 | -------------------------------------------------------------------------------- /test/odata.query.filter.js: -------------------------------------------------------------------------------- 1 | import 'should'; 2 | import sinon from 'sinon'; 3 | import request from 'supertest'; 4 | import { odata, host, port, bookSchema } from './support/setup'; 5 | import data from './support/books.json'; 6 | import FakeDb from './support/fake-db'; 7 | 8 | describe('odata.query.filter', function() { 9 | let httpServer, books, resource, mock; 10 | 11 | before(async function() { 12 | const db = new FakeDb(); 13 | const server = odata(db); 14 | server.resource('book', bookSchema); 15 | resource = server.resources.book; 16 | httpServer = server.listen(port); 17 | books = db.addData('book', data); 18 | }); 19 | 20 | after(() => { 21 | httpServer.close(); 22 | }); 23 | 24 | afterEach(() => { 25 | mock.restore(); 26 | }); 27 | 28 | describe('[Equal]', () => { 29 | it('should filter items', async function(){ 30 | mock = sinon.mock(resource.model); 31 | mock.expects('where').once().withArgs('title').returns(resource.model); 32 | mock.expects('equals').once().withArgs(data[1].title).returns(resource.model); 33 | await request(host).get(`/book?$filter=title eq '${data[1].title}'`); 34 | mock.verify(); 35 | }); 36 | it('should filter items when field has keyword', async function(){ 37 | mock = sinon.mock(resource.model); 38 | mock.expects('where').once().withArgs('author').returns(resource.model); 39 | mock.expects('equals').once().withArgs('Ralls, Kim').returns(resource.model); 40 | await request(host).get(`/book?$filter=author eq 'Ralls, Kim'`); 41 | mock.verify(); 42 | }); 43 | it('should filter items when it has extra spaces at begin', async function(){ 44 | mock = sinon.mock(resource.model); 45 | mock.expects('where').once().withArgs('title').returns(resource.model); 46 | mock.expects('equals').once().withArgs(data[1].title).returns(resource.model); 47 | await request(host).get(`/book?$filter= title eq '${data[1].title}'`); 48 | mock.verify(); 49 | }); 50 | it('should filter items when it has extra spaces at mid', async function(){ 51 | mock = sinon.mock(resource.model); 52 | mock.expects('where').once().withArgs('title').returns(resource.model); 53 | mock.expects('equals').once().withArgs(data[1].title).returns(resource.model); 54 | await request(host).get(`/book?$filter=title eq '${data[1].title}'`); 55 | mock.verify(); 56 | }); 57 | it('should filter items when it has extra spaces at end', async function(){ 58 | mock = sinon.mock(resource.model); 59 | mock.expects('where').once().withArgs('title').returns(resource.model); 60 | mock.expects('equals').once().withArgs(data[1].title).returns(resource.model); 61 | await request(host).get(`/book?$filter=title eq '${data[1].title}' `); 62 | mock.verify(); 63 | }); 64 | it('should filter items when use chinese keyword', async function(){ 65 | mock = sinon.mock(resource.model); 66 | mock.expects('where').once().withArgs('title').returns(resource.model); 67 | mock.expects('equals').once().withArgs('代码大全').returns(resource.model); 68 | await request(host).get(encodeURI(`/book?$filter=title eq '代码大全'`)); 69 | mock.verify(); 70 | }); 71 | it('should filter items when use id', async function(){ 72 | mock = sinon.mock(resource.model); 73 | mock.expects('where').once().withArgs('_id').returns(resource.model); 74 | mock.expects('equals').once().withArgs(books[1].id).returns(resource.model); 75 | await request(host).get(encodeURI(`/book?$filter=id eq '${books[1].id}'`)); 76 | mock.verify(); 77 | }); 78 | }); 79 | 80 | describe("[Not equal]", () => { 81 | it('should filter items', async function(){ 82 | mock = sinon.mock(resource.model); 83 | mock.expects('where').once().withArgs('title').returns(resource.model); 84 | mock.expects('ne').once().withArgs(data[1].title).returns(resource.model); 85 | await request(host).get(`/book?$filter=title ne '${data[1].title}'`); 86 | mock.verify(); 87 | }); 88 | }); 89 | 90 | describe("[Greater than]", () => { 91 | it('should filter items', async function(){ 92 | mock = sinon.mock(resource.model); 93 | mock.expects('where').once().withArgs('price').returns(resource.model); 94 | mock.expects('gt').once().withArgs(36.95).returns(resource.model); 95 | await request(host).get(`/book?$filter=price gt 36.95`); 96 | mock.verify(); 97 | }); 98 | }); 99 | 100 | describe('[Greater than or equal]', () => { 101 | it('should filter items', async function(){ 102 | mock = sinon.mock(resource.model); 103 | mock.expects('where').once().withArgs('price').returns(resource.model); 104 | mock.expects('gte').once().withArgs(36.95).returns(resource.model); 105 | await request(host).get(`/book?$filter=price ge 36.95`); 106 | mock.verify(); 107 | }); 108 | }); 109 | 110 | describe('[Less than]', () => { 111 | it('should filter items', async function() { 112 | mock = sinon.mock(resource.model); 113 | mock.expects('where').once().withArgs('price').returns(resource.model); 114 | mock.expects('lt').once().withArgs(36.95).returns(resource.model); 115 | await request(host).get(`/book?$filter=price lt 36.95`); 116 | mock.verify(); 117 | }); 118 | }); 119 | 120 | describe('[Less than or equal]', () => { 121 | it('should filter items', async function() { 122 | mock = sinon.mock(resource.model); 123 | mock.expects('where').once().withArgs('price').returns(resource.model); 124 | mock.expects('lte').once().withArgs(36.95).returns(resource.model); 125 | await request(host).get(`/book?$filter=price le 36.95`); 126 | mock.verify(); 127 | }); 128 | }); 129 | 130 | describe('[Logical and]', () => { 131 | it("should filter items", async function() { 132 | mock = sinon.mock(resource.model); 133 | mock.expects('where').withArgs('title').returns(resource.model); 134 | mock.expects('where').withArgs('price').returns(resource.model); 135 | mock.expects('ne').once().withArgs(data[1].title).returns(resource.model); 136 | mock.expects('gte').once().withArgs(36.95).returns(resource.model); 137 | await request(host).get(`/book?$filter=title ne '${data[1].title}' and price ge 36.95`); 138 | mock.verify(); 139 | }); 140 | it("should filter items when it has extra spaces", async function() { 141 | mock = sinon.mock(resource.model); 142 | mock.expects('where').withArgs('title').returns(resource.model); 143 | mock.expects('where').withArgs('price').returns(resource.model); 144 | mock.expects('ne').once().withArgs(data[1].title).returns(resource.model); 145 | mock.expects('gte').once().withArgs(36.95).returns(resource.model); 146 | await request(host).get(`/book?$filter=title ne '${data[1].title}' and price ge 36.95`); 147 | mock.verify(); 148 | }); 149 | }); 150 | }); 151 | -------------------------------------------------------------------------------- /test/odata.query.orderby.js: -------------------------------------------------------------------------------- 1 | import 'should'; 2 | import sinon from 'sinon'; 3 | import request from 'supertest'; 4 | import { odata, host, port, bookSchema } from './support/setup'; 5 | import data from './support/books.json'; 6 | import FakeDb from './support/fake-db'; 7 | 8 | describe('odata.query.orderby', () => { 9 | let httpServer, mock, resource; 10 | 11 | before(async function() { 12 | const db = new FakeDb(); 13 | const server = odata(db); 14 | resource = server.resource('book', bookSchema) 15 | httpServer = server.listen(port); 16 | db.addData('book', data); 17 | }); 18 | 19 | after(() => { 20 | httpServer.close(); 21 | }); 22 | 23 | afterEach(() => { 24 | mock.restore(); 25 | }); 26 | 27 | it('should default let items order with asc', async function() { 28 | mock = sinon.mock(resource.model); 29 | mock.expects('sort').once().withArgs({ 30 | price: 'asc' 31 | }).returns(resource.model); 32 | await request(host).get('/book?$orderby=price'); 33 | mock.verify(); 34 | }); 35 | 36 | it('should let items order asc', async function() { 37 | mock = sinon.mock(resource.model); 38 | mock.expects('sort').once().withArgs({ 39 | price: 'asc' 40 | }).returns(resource.model); 41 | await request(host).get('/book?$orderby=price asc'); 42 | mock.verify(); 43 | }); 44 | 45 | it('should let items order desc', async function() { 46 | mock = sinon.mock(resource.model); 47 | mock.expects('sort').once().withArgs({ 48 | price: 'desc' 49 | }).returns(resource.model); 50 | await request(host).get('/book?$orderby=price desc'); 51 | mock.verify(); 52 | }); 53 | 54 | it('should let items order when use multiple fields', async function() { 55 | mock = sinon.mock(resource.model); 56 | mock.expects('sort').once().withArgs({ 57 | price: 'asc', 58 | title: 'asc' 59 | }).returns(resource.model); 60 | await request(host).get('/book?$orderby=price,title'); 61 | mock.verify(); 62 | }); 63 | 64 | it("should be ignore when order by not exist field", async function() { 65 | mock = sinon.mock(resource.model); 66 | mock.expects('sort').once().withArgs({ 67 | 'not-exist-field': 'asc' 68 | }).returns(resource.model); 69 | await request(host).get('/book?$orderby=not-exist-field'); 70 | mock.verify(); 71 | }); 72 | }); 73 | -------------------------------------------------------------------------------- /test/odata.query.select.js: -------------------------------------------------------------------------------- 1 | import 'should'; 2 | import sinon from 'sinon'; 3 | import request from 'supertest'; 4 | import { odata, host, port, bookSchema } from './support/setup'; 5 | import data from './support/books.json'; 6 | import FakeDb from './support/fake-db'; 7 | 8 | describe('odata.query.select', () => { 9 | let httpServer, mock, resource; 10 | 11 | before(async function() { 12 | const db = new FakeDb(); 13 | const server = odata(db); 14 | resource = server.resource('book', bookSchema); 15 | resource.model.model.schema.tree = { 16 | id: { 17 | select: true 18 | }, 19 | price: { 20 | select: true 21 | }, 22 | title: { 23 | select: true 24 | } 25 | } 26 | httpServer = server.listen(port); 27 | db.addData('book', data); 28 | }); 29 | 30 | after(() => { 31 | httpServer.close(); 32 | }); 33 | 34 | afterEach(() => { 35 | mock.restore(); 36 | }); 37 | 38 | it('should select anyone field', async function() { 39 | mock = sinon.mock(resource.model); 40 | mock.expects('select').once().withArgs({ 41 | _id: 0, 42 | price: 1 43 | }).returns(resource.model); 44 | await request(host).get('/book?$select=price'); 45 | mock.verify(); 46 | }); 47 | it('should select multiple field', async function() { 48 | mock = sinon.mock(resource.model); 49 | mock.expects('select').once().withArgs({ 50 | _id: 0, 51 | price: 1, 52 | title: 1 53 | }).returns(resource.model); 54 | await request(host).get('/book?$select=price,title'); 55 | mock.verify(); 56 | }); 57 | it('should select multiple field with blank space', async function() { 58 | mock = sinon.mock(resource.model); 59 | mock.expects('select').once().withArgs({ 60 | _id: 0, 61 | price: 1, 62 | title: 1 63 | }).returns(resource.model); 64 | await request(host).get('/book?$select=price, title'); 65 | mock.verify(); 66 | }); 67 | it('should select id field', async function() { 68 | mock = sinon.mock(resource.model); 69 | mock.expects('select').once().withArgs({ 70 | _id: 1, 71 | price: 1, 72 | title: 1 73 | }).returns(resource.model); 74 | await request(host).get('/book?$select=price,title,id'); 75 | mock.verify(); 76 | }); 77 | it('should ignore when select not exist field', async function() { 78 | mock = sinon.mock(resource.model); 79 | mock.expects('select').never(); 80 | await request(host).get('/book?$select=not-exist-field'); 81 | mock.verify(); 82 | }); 83 | }); 84 | -------------------------------------------------------------------------------- /test/odata.query.skip.js: -------------------------------------------------------------------------------- 1 | import 'should'; 2 | import sinon from 'sinon'; 3 | import request from 'supertest'; 4 | import { odata, host, port, books, bookSchema } from './support/setup'; 5 | import FakeDb from './support/fake-db'; 6 | 7 | describe('odata.query.skip', () => { 8 | let httpServer, mock, resource; 9 | 10 | before(async function() { 11 | const db = new FakeDb(); 12 | const server = odata(db); 13 | resource = server.resource('book', bookSchema) 14 | httpServer = server.listen(port); 15 | db.addData('book', books); 16 | }); 17 | 18 | after(() => { 19 | httpServer.close(); 20 | }); 21 | 22 | afterEach(() => { 23 | mock.restore(); 24 | }); 25 | 26 | it('should skip items', async function() { 27 | mock = sinon.mock(resource.model); 28 | mock.expects('skip').once().withArgs(1).returns(resource.model); 29 | await request(host).get('/book?$skip=1'); 30 | mock.verify(); 31 | }); 32 | it('should ignore when skip over count of items', async function() { 33 | mock = sinon.mock(resource.model); 34 | mock.expects('skip').once().withArgs(1024).returns(resource.model); 35 | await request(host).get('/book?$skip=1024'); 36 | mock.verify(); 37 | }); 38 | it('should ignore when skip not a number', async function() { 39 | mock = sinon.mock(resource.model); 40 | mock.expects('skip').never(); 41 | await request(host).get('/book?$skip=not-a-number'); 42 | mock.verify(); 43 | }); 44 | return it('should ignore when skip not a positive number', async function() { 45 | mock = sinon.mock(resource.model); 46 | mock.expects('skip').never(); 47 | await request(host).get('/book?$skip=-1'); 48 | mock.verify(); 49 | }); 50 | }); 51 | -------------------------------------------------------------------------------- /test/odata.query.top.js: -------------------------------------------------------------------------------- 1 | import 'should'; 2 | import sinon from 'sinon'; 3 | import request from 'supertest'; 4 | import FakeDb from './support/fake-db'; 5 | import { odata, host, port, books, bookSchema } from './support/setup'; 6 | 7 | describe('odata.query.top', () => { 8 | let httpServer, mock, resource; 9 | 10 | before(async function() { 11 | const db = new FakeDb() 12 | const server = odata(db); 13 | resource = server.resource('book', bookSchema) 14 | httpServer = server.listen(port); 15 | db.addData('book', books); 16 | }); 17 | 18 | after(() => { 19 | httpServer.close(); 20 | }); 21 | 22 | afterEach(() => { 23 | mock.restore(); 24 | }); 25 | 26 | it('should top items', async function() { 27 | mock = sinon.mock(resource.model); 28 | mock.expects('limit').once().withArgs(1).returns(resource.model); 29 | const res = await request(host).get('/book?$top=1'); 30 | mock.verify(); 31 | }); 32 | it('should iginre when top not a number', async function() { 33 | mock = sinon.mock(resource.model); 34 | mock.expects('limit').never(); 35 | const res = await request(host).get('/book?$top=not-a-number'); 36 | mock.verify(); 37 | }); 38 | it('should ignore when top not a positive number', async function() { 39 | mock = sinon.mock(resource.model); 40 | mock.expects('limit').never(); 41 | const res = await request(host).get('/book?$top=-1'); 42 | mock.verify(); 43 | }); 44 | }); 45 | -------------------------------------------------------------------------------- /test/options.maxSkip.js: -------------------------------------------------------------------------------- 1 | import 'should'; 2 | import sinon from 'sinon'; 3 | import request from 'supertest'; 4 | import { odata, host, port, books, bookSchema } from './support/setup'; 5 | import FakeDb from './support/fake-db'; 6 | 7 | describe('options.maxSkip', () => { 8 | let httpServer, server, mock, resource; 9 | 10 | beforeEach(async function() { 11 | const db = new FakeDb(); 12 | server = odata(db); 13 | resource = server.resource('book', bookSchema); 14 | db.addData('book', books); 15 | }); 16 | 17 | afterEach(() => { 18 | httpServer.close(); 19 | mock.restore(); 20 | }); 21 | 22 | it('global-limit should work', async function() { 23 | mock = sinon.mock(resource.model); 24 | mock.expects('skip').once().withArgs(1).returns(resource.model); 25 | server.set('maxSkip', 1); 26 | httpServer = server.listen(port); 27 | await request(host).get('/book?$skip=100'); 28 | mock.verify(); 29 | }); 30 | it('resource-limit should work', async function() { 31 | mock = sinon.mock(resource.model); 32 | mock.expects('skip').once().withArgs(1).returns(resource.model); 33 | resource.maxSkip(1); 34 | httpServer = server.listen(port); 35 | await request(host).get('/book?$skip=100'); 36 | mock.verify(); 37 | }); 38 | it('should use resource-limit even global-limit already set', async function() { 39 | mock = sinon.mock(resource.model); 40 | mock.expects('skip').once().withArgs(1).returns(resource.model); 41 | server.set('maxSkip', 2); 42 | resource.maxSkip(1); 43 | httpServer = server.listen(port); 44 | await request(host).get('/book?$skip=100'); 45 | mock.verify(); 46 | }); 47 | it('should use query-limit if it is minimum global-limit', async function() { 48 | mock = sinon.mock(resource.model); 49 | mock.expects('skip').once().withArgs(1).returns(resource.model); 50 | server.set('maxSkip', 2); 51 | httpServer = server.listen(port); 52 | await request(host).get('/book?$skip=1'); 53 | mock.verify(); 54 | }); 55 | it('should use query-limit if it is minimum resource-limit', async function() { 56 | mock = sinon.mock(resource.model); 57 | mock.expects('skip').once().withArgs(1).returns(resource.model); 58 | resource.maxSkip(2); 59 | httpServer = server.listen(port); 60 | await request(host).get('/book?$skip=1'); 61 | mock.verify(); 62 | }); 63 | }); 64 | -------------------------------------------------------------------------------- /test/options.maxTop.js: -------------------------------------------------------------------------------- 1 | import 'should'; 2 | import sinon from 'sinon'; 3 | import request from 'supertest'; 4 | import { odata, host, port, bookSchema } from './support/setup'; 5 | import FakeDb from './support/fake-db'; 6 | 7 | describe('options.maxTop', () => { 8 | let httpServer, server, resource, mock; 9 | 10 | beforeEach(async function() { 11 | const db = new FakeDb(); 12 | server = odata(db); 13 | resource = server.resource('book', bookSchema); 14 | }); 15 | 16 | afterEach(() => { 17 | httpServer.close(); 18 | mock.restore(); 19 | }); 20 | 21 | it('global-limit should work', async function() { 22 | mock = sinon.mock(resource.model); 23 | mock.expects('limit').once().withArgs(1).returns(resource.model); 24 | server.set('maxTop', 1); 25 | httpServer = server.listen(port); 26 | await request(host).get('/book?$top=100'); 27 | mock.verify(); 28 | }); 29 | it('resource-limit should work', async function() { 30 | mock = sinon.mock(resource.model); 31 | mock.expects('limit').once().withArgs(1).returns(resource.model); 32 | resource.maxTop(1); 33 | httpServer = server.listen(port); 34 | await request(host).get('/book?$top=2'); 35 | mock.verify(); 36 | }); 37 | it('should use resource-limit even global-limit already set', async function() { 38 | mock = sinon.mock(resource.model); 39 | mock.expects('limit').once().withArgs(1).returns(resource.model); 40 | server.set('maxTop', 2); 41 | resource.maxTop(1); 42 | httpServer = server.listen(port); 43 | await request(host).get('/book?$top=100'); 44 | mock.verify(); 45 | }); 46 | it('should use query-limit if it is minimum global-limit', async function() { 47 | mock = sinon.mock(resource.model); 48 | mock.expects('limit').once().withArgs(1).returns(resource.model); 49 | server.set('maxTop', 2); 50 | httpServer = server.listen(port); 51 | await request(host).get('/book?$top=1'); 52 | mock.verify(); 53 | }); 54 | it('should use query-limit if it is minimum resource-limit', async function() { 55 | mock = sinon.mock(resource.model); 56 | mock.expects('limit').once().withArgs(1).returns(resource.model); 57 | resource.maxTop(2); 58 | httpServer = server.listen(port); 59 | await request(host).get('/book?$top=1'); 60 | mock.verify(); 61 | }); 62 | }); 63 | -------------------------------------------------------------------------------- /test/options.prefix.js: -------------------------------------------------------------------------------- 1 | import 'should'; 2 | import request from 'supertest'; 3 | import { odata, host, port, bookSchema } from './support/setup'; 4 | import FakeDb from './support/fake-db'; 5 | 6 | describe('options.prefix', () => { 7 | let httpServer, db; 8 | 9 | before(() => { 10 | db = new FakeDb(); 11 | }); 12 | 13 | afterEach(() => { 14 | httpServer.close(); 15 | }); 16 | 17 | it('should be work', async function() { 18 | const server = odata(db); 19 | server.resource('book', bookSchema); 20 | server.set('prefix', '/api'); 21 | httpServer = server.listen(port); 22 | const res = await request(host).get('/api/book'); 23 | res.status.should.be.equal(200); 24 | }); 25 | it('should be 200 when do not add `/`', async function() { 26 | const server = odata(db); 27 | server.resource('book', bookSchema); 28 | server.set('prefix', 'api'); 29 | httpServer = server.listen(port); 30 | const res = await request(host).get('/api/book'); 31 | res.status.should.be.equal(200); 32 | }); 33 | it('should be 200 when set it at init-function', async function() { 34 | const server = odata(db, '/api'); 35 | server.resource('book', bookSchema); 36 | httpServer = server.listen(port); 37 | const res = await request(host).get('/api/book'); 38 | res.status.should.be.equal(200); 39 | }); 40 | }); 41 | -------------------------------------------------------------------------------- /test/rest.delete.js: -------------------------------------------------------------------------------- 1 | import 'should'; 2 | import request from 'supertest'; 3 | import { odata, host, port, bookSchema, assertSuccess } from './support/setup'; 4 | import books from './support/books.json'; 5 | import FakeDb from './support/fake-db'; 6 | 7 | describe('rest.delete', function() { 8 | let data, httpServer; 9 | 10 | before(async function() { 11 | const db = new FakeDb(); 12 | const server = odata(db); 13 | server.resource('book', bookSchema) 14 | httpServer = server.listen(port); 15 | data = db.addData('book', books); 16 | }); 17 | 18 | after(() => { 19 | httpServer.close(); 20 | }); 21 | 22 | it('should delete resource if it exist', async function() { 23 | const res = await request(host).del(`/book(${data[0].id})`); 24 | assertSuccess(res); 25 | res.status.should.be.equal(204); 26 | }); 27 | it('should be 404 if resource not exist', async function() { 28 | const res = await request(host).del(`/book(not-exist-id)`); 29 | res.status.should.be.equal(404); 30 | }); 31 | it('should be 404 if without id', async function() { 32 | const res = await request(host).del(`/book`); 33 | res.status.should.be.equal(404); 34 | }); 35 | it('should 404 if try to delete a resource twice', async function() { 36 | const id = data[0].id; 37 | await request(host).del(`/book(${id})`); 38 | const res = await request(host).del(`/book(${id})`); 39 | res.status.should.be.equal(404); 40 | }); 41 | }); 42 | -------------------------------------------------------------------------------- /test/rest.get.js: -------------------------------------------------------------------------------- 1 | import 'should'; 2 | import request from 'supertest'; 3 | import { odata, host, port, bookSchema } from './support/setup'; 4 | import books from './support/books.json'; 5 | import FakeDb from './support/fake-db'; 6 | 7 | describe('rest.get', () => { 8 | let data, httpServer; 9 | 10 | before(async function() { 11 | const db = new FakeDb(); 12 | const server = odata(db); 13 | server.resource('book', bookSchema) 14 | httpServer = server.listen(port); 15 | data = db.addData('book', books); 16 | }); 17 | 18 | after(() => { 19 | httpServer.close(); 20 | }); 21 | 22 | it('should return all of the resources', async function() { 23 | const res = await request(host).get(`/book`); 24 | res.body.should.be.have.property('value'); 25 | res.body.value.length.should.be.equal(data.length); 26 | }); 27 | it('should return special resource', async function() { 28 | const res = await request(host).get(`/book(${data[0].id})`); 29 | res.body.should.be.have.property('title'); 30 | res.body.title.should.be.equal(data[0].title); 31 | }); 32 | it('should be 404 if resouce name not declare', async function() { 33 | const res = await request(host).get(`/not-exist-resource`); 34 | res.status.should.be.equal(404); 35 | }); 36 | it('should be 404 if resource not exist', async function() { 37 | const res = await request(host).get(`/book(not-exist-id)`); 38 | res.status.should.be.equal(404); 39 | }); 40 | }); 41 | -------------------------------------------------------------------------------- /test/rest.post.js: -------------------------------------------------------------------------------- 1 | import 'should'; 2 | import request from 'supertest'; 3 | import { odata, host, port, bookSchema } from './support/setup'; 4 | import FakeDb from './support/fake-db'; 5 | 6 | describe('rest.post', () => { 7 | let httpServer; 8 | 9 | before(async function() { 10 | const db = new FakeDb(); 11 | const server = odata(db); 12 | server.resource('book', bookSchema) 13 | httpServer = server.listen(port); 14 | }); 15 | 16 | after(() => { 17 | httpServer.close(); 18 | }); 19 | 20 | it('should create new resource', async function() { 21 | const res = await request(host) 22 | .post(`/book`) 23 | .send({ title: Math.random() }); 24 | res.body.should.be.have.property('id'); 25 | res.body.should.be.have.property('title'); 26 | }); 27 | it('should be 422 if post without data', async function() { 28 | const res = await request(host).post(`/book`); 29 | res.status.should.be.equal(422); 30 | }); 31 | }); 32 | -------------------------------------------------------------------------------- /test/rest.put.js: -------------------------------------------------------------------------------- 1 | import * as uuid from 'uuid'; 2 | import 'should'; 3 | import request from 'supertest'; 4 | import { odata, host, port, bookSchema } from './support/setup'; 5 | import books from './support/books.json'; 6 | import FakeDb from './support/fake-db'; 7 | 8 | describe('rest.put', () => { 9 | let data, httpServer; 10 | 11 | before(async function() { 12 | const db = new FakeDb(); 13 | const server = odata(db); 14 | server.resource('book', bookSchema) 15 | httpServer = server.listen(port); 16 | data = db.addData('book', books); 17 | }); 18 | 19 | after(() => { 20 | httpServer.close(); 21 | }); 22 | 23 | it('should modify resource', async function() { 24 | const book = data[0]; 25 | book.title = 'modify book'; 26 | const res = await request(host) 27 | .put(`/book(${book.id})`) 28 | .send(book); 29 | res.body.should.be.have.property('title'); 30 | res.body.title.should.be.equal(book.title); 31 | }); 32 | it('should create resource if send with a id which not exist', async function() { 33 | const book = { 34 | id: uuid.v4(), 35 | title: 'new book', 36 | }; 37 | const res = await request(host) 38 | .put(`/book(${book.id})`) 39 | .send({ title: book.title }); 40 | res.body.should.be.have.property('title'); 41 | res.body.title.should.be.equal(book.title); 42 | res.body.should.be.have.property('id'); 43 | res.body.id.should.be.equal(book.id); 44 | }); 45 | it('should be 404 if without id', async function() { 46 | const res = await request(host).put(`/book`).send(data[0]); 47 | res.status.should.be.equal(404); 48 | }); 49 | it("should 400 if with a wrong id", async function() { 50 | const res = await request(host).put(`/book(wrong-id)`).send(data[0]); 51 | res.status.should.be.equal(400); 52 | }); 53 | }); 54 | -------------------------------------------------------------------------------- /test/support/books.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "author": "Gambardella, Matthew", 4 | "description": "An in-depth look at creating applications \n with XML.", 5 | "genre": "Computer", 6 | "price": 44.95, 7 | "publish_date": "2000-10-01", 8 | "title": "XML Developer's Guide" 9 | }, 10 | { 11 | "author": "Ralls, Kim", 12 | "description": "A former architect battles corporate zombies, \n an evil sorceress, and her own childhood to become queen \n of the world.", 13 | "genre": "Fantasy", 14 | "price": 5.95, 15 | "publish_date": "2000-12-16", 16 | "title": "Midnight Rain" 17 | }, 18 | { 19 | "author": "Corets, Eva", 20 | "description": "After the collapse of a nanotechnology \n society in England, the young survivors lay the \n foundation for a new society.", 21 | "genre": "Fantasy", 22 | "price": 5.95, 23 | "publish_date": "2000-11-17", 24 | "title": "Maeve Ascendant" 25 | }, 26 | { 27 | "author": "Corets, Eva", 28 | "description": "In post-apocalypse England, the mysterious \n agent known only as Oberon helps to create a new life \n for the inhabitants of London. Sequel to Maeve \n Ascendant.", 29 | "genre": "Fantasy", 30 | "price": 5.95, 31 | "publish_date": "2001-03-10", 32 | "title": "Oberon's Legacy" 33 | }, 34 | { 35 | "author": "Corets, Eva", 36 | "description": "The two daughters of Maeve, half-sisters, \n battle one another for control of England. Sequel to \n Oberon's Legacy.", 37 | "genre": "Fantasy", 38 | "price": 5.95, 39 | "publish_date": "2001-09-10", 40 | "title": "The Sundered Grail" 41 | }, 42 | { 43 | "author": "Randall, Cynthia", 44 | "description": "When Carla meets Paul at an ornithology \n conference, tempers fly as feathers get ruffled.", 45 | "genre": "Romance", 46 | "price": 4.95, 47 | "publish_date": "2000-09-02", 48 | "title": "Lover Birds" 49 | }, 50 | { 51 | "author": "Thurman, Paula", 52 | "description": "A deep sea diver finds true love twenty \n thousand leagues beneath the sea.", 53 | "genre": "Romance", 54 | "price": 4.95, 55 | "publish_date": "2000-11-02", 56 | "title": "Splish Splash" 57 | }, 58 | { 59 | "author": "Knorr, Stefan", 60 | "description": "An anthology of horror stories about roaches,\n centipedes, scorpions and other insects.", 61 | "genre": "Horror", 62 | "price": 4.95, 63 | "publish_date": "2000-12-06", 64 | "title": "Creepy Crawlies" 65 | }, 66 | { 67 | "author": "Kress, Peter", 68 | "description": "After an inadvertant trip through a Heisenberg\n Uncertainty Device, James Salway discovers the problems \n of being quantum.", 69 | "genre": "Science Fiction", 70 | "price": 6.95, 71 | "publish_date": "2000-11-02", 72 | "title": "Paradox Lost" 73 | }, 74 | { 75 | "author": "O'Brien, Tim", 76 | "description": "Microsoft's .NET initiative is explored in \n detail in this deep programmer's reference.", 77 | "genre": "Computer", 78 | "price": 36.95, 79 | "publish_date": "2000-12-09", 80 | "title": "Microsoft .NET: The Programming Bible" 81 | }, 82 | { 83 | "author": "O'Brien, Tim", 84 | "description": "The Microsoft MSXML3 parser is covered in \n detail, with attention to XML DOM interfaces, XSLT processing, \n SAX and more.", 85 | "genre": "Computer", 86 | "price": 36.95, 87 | "publish_date": "2000-12-01", 88 | "title": "MSXML3: A Comprehensive Guide" 89 | }, 90 | { 91 | "author": "Galos, Mike", 92 | "description": "Microsoft Visual Studio 7 is explored in depth,\n looking at how Visual Basic, Visual C++, C#, and ASP+ are \n integrated into a comprehensive development \n environment.", 93 | "genre": "Computer", 94 | "price": 49.95, 95 | "publish_date": "2001-04-16", 96 | "title": "Visual Studio 7: A Comprehensive Guide" 97 | }, 98 | { 99 | "author": "史蒂夫·迈克康奈尔", 100 | "description": "第2版的《代码大全》是著名IT畅销书作者史蒂夫·迈克康奈尔11年前的经典著作的全新演绎:第2版不是第一版的简单修订增补,而是完全进行了重写;增加了很多与时俱进的内容。这也是一本完整的软件构建手册,涵盖了软件构建过程中的所有细节。它从软件质量和编程思想等方面论述了软件构建的各个问题,并详细论述了紧跟潮流的新技术、高屋建瓴的观点、通用的概念,还含有丰富而典型的程序示例。这本书中所论述的技术不仅填补了初级与高级编程技术之间的空白,而且也为程序员们提供了一个有关编程技巧的信息来源。这本书对经验丰富的程序员、技术带头人、自学的程序员及几乎不懂太多编程技巧的学生们都是大有裨益的。可以说,无论是什么背景的读者,阅读这本书都有助于在更短的时间内、更容易地写出更好的程序。", 101 | "genre": "计算机", 102 | "price": 128, 103 | "publish_date": "2006-03-01", 104 | "title": "代码大全" 105 | } 106 | ] 107 | -------------------------------------------------------------------------------- /test/support/fake-db-model.js: -------------------------------------------------------------------------------- 1 | export default class Model { 2 | constructor(name, model) { 3 | this._name = name; 4 | this._data = []; 5 | this._count = 1; 6 | this.model = { 7 | schema: { 8 | ...model, 9 | tree: { 10 | id: { select: true }, 11 | ...model 12 | }, 13 | paths: Model.toPath(model) 14 | } 15 | }; 16 | } 17 | 18 | static toPath(model, prefix) { 19 | let result = {}; 20 | 21 | Object.keys(model).forEach((item) => { 22 | const propName = prefix ? `${prefix}.${item}` : item; 23 | 24 | if (Array.isArray(model[item])) { 25 | result[propName] = { 26 | path: propName, 27 | instance: 'Array' 28 | }; 29 | if (model[item][0].name) { 30 | // Array of primitive Types 31 | result[propName].options = { 32 | type: [{ 33 | name: model[item][0].name 34 | }] 35 | }; 36 | } else { 37 | // Array of objects 38 | result[propName].schema = { 39 | paths: Model.toPath(model[item][0]) 40 | }; 41 | } 42 | } else if ( typeof model[item] === 'object' ) { 43 | const subSchema = Model.toPath(model[item], propName); 44 | 45 | result = { 46 | ...result, 47 | ...subSchema 48 | }; 49 | } else { 50 | result[propName] = { 51 | path: propName, 52 | instance: model[item].name 53 | }; 54 | } 55 | }); 56 | 57 | return result; 58 | } 59 | 60 | addData(data) { 61 | this._data = data.map(item => ({ 62 | ...item, 63 | id: (this._count++).toString() 64 | })); 65 | 66 | return this._data; 67 | } 68 | 69 | create(data) { 70 | const newItem = { 71 | ...data, 72 | id: data.id || (this._count++).toString(), 73 | save: callback => { 74 | const result = this._data.find(item => item.id === newItem.id); 75 | 76 | if (result._id) { 77 | result.id = result._id; 78 | } 79 | callback(); 80 | } 81 | }; 82 | 83 | this._data.push(newItem); 84 | 85 | return newItem; 86 | } 87 | 88 | exec(callback) { 89 | callback(null, this._data); 90 | } 91 | 92 | find() { 93 | return this; 94 | } 95 | 96 | findById(id, callback) { 97 | let idInternal = 0; 98 | 99 | switch (this.model.schema.id) { 100 | case Number: 101 | idInternal = +id; 102 | break; 103 | 104 | default: 105 | idInternal = id; 106 | break; 107 | } 108 | 109 | let result = this._data.find(item => item.id === idInternal); 110 | 111 | if (result) { 112 | result.save = (callback) => { 113 | callback(); 114 | }; 115 | } 116 | callback(null, result); 117 | } 118 | 119 | findByIdAndUpdate(id, data, callback) { 120 | const result = this._data.find(item => item.id === id); 121 | const index = this._data.indexOf(result); 122 | 123 | this._data[index] = { 124 | ...result, 125 | ...data 126 | }; 127 | callback(); 128 | 129 | } 130 | 131 | findOne(filter, callback) { 132 | const result = this._data.find(item => item.id === filter._id); 133 | callback(null, result); 134 | } 135 | 136 | remove(filter, callback) { 137 | const toDelete = this._data.find(item => item.id === filter._id); 138 | const deleteItem = item => { 139 | const index = this._data.indexOf(item); 140 | this._data.splice(index, 1); 141 | }; 142 | 143 | if (toDelete) { 144 | if (Array.isArray(toDelete)) { 145 | toDelete.forEach(deleteItem); 146 | callback(null, JSON.stringify({ n: toDelete.length })); 147 | } else { 148 | deleteItem(toDelete); 149 | callback(null, JSON.stringify({ n: 1 })); 150 | } 151 | } else { 152 | callback(null, JSON.stringify({ n: 0 })); 153 | } 154 | } 155 | 156 | limit() { 157 | return this; 158 | } 159 | 160 | select() { 161 | return this; 162 | } 163 | 164 | skip() { 165 | return this; 166 | } 167 | 168 | sort() { 169 | return this; 170 | } 171 | 172 | where() { 173 | return this; 174 | } 175 | 176 | $where(value) { 177 | return this; 178 | } 179 | 180 | gt() { 181 | return this; 182 | } 183 | 184 | gte() { 185 | return this; 186 | } 187 | 188 | lt() { 189 | return this; 190 | } 191 | 192 | lte() { 193 | return this; 194 | } 195 | 196 | equals() { 197 | return this; 198 | } 199 | 200 | ne() { 201 | return this; 202 | } 203 | 204 | exists() { 205 | return this; 206 | } 207 | 208 | or() { 209 | return this; 210 | } 211 | 212 | and() { 213 | return this; 214 | } 215 | 216 | count(callback) { 217 | callback(null, this._data.length); 218 | } 219 | } -------------------------------------------------------------------------------- /test/support/fake-db.js: -------------------------------------------------------------------------------- 1 | import Model from './fake-db-model'; 2 | 3 | export default class { 4 | constructor() { 5 | this._models = {}; 6 | } 7 | 8 | addData(name, data) { 9 | return this._models[name].addData(data); 10 | } 11 | 12 | createConnection() { 13 | return this; 14 | } 15 | 16 | register(name, model) { 17 | this._models[name] = new Model(name, model); 18 | return this._models[name]; 19 | } 20 | 21 | on(name, event) { 22 | 23 | } 24 | } -------------------------------------------------------------------------------- /test/support/setup.js: -------------------------------------------------------------------------------- 1 | import mongoose from 'mongoose'; 2 | import id from '../../lib/model/idPlugin'; 3 | 4 | export odata from '../../src'; 5 | export const host = 'http://localhost:3000'; 6 | export const port = '3000'; 7 | export const conn = 'mongodb://localhost/odata-test'; 8 | 9 | export const bookSchema = { 10 | author: String, 11 | description: String, 12 | genre: String, 13 | price: Number, 14 | publish_date: Date, 15 | title: String 16 | }; 17 | 18 | export const books = require('./books.json'); 19 | 20 | export function initData() { 21 | return new Promise((resolve, reject) => { 22 | const conf = { 23 | _id: false, 24 | versionKey: false, 25 | collection: 'book', 26 | }; 27 | 28 | const db = mongoose.createConnection(conn); 29 | const schema = new mongoose.Schema(bookSchema, conf); 30 | schema.plugin(id); 31 | const model = db.model('book', schema); 32 | 33 | function clear() { 34 | return new Promise((resolve) => { 35 | model.remove({}, resolve); 36 | }); 37 | } 38 | 39 | function insert(item) { 40 | return new Promise((resolve) => { 41 | const entity = new model(item); 42 | entity.save((err, result) => resolve(result)); 43 | }); 44 | } 45 | 46 | const promises = books.map(insert); 47 | clear().then(() => Promise.all(promises).then(resolve)); 48 | }); 49 | } 50 | 51 | export function assertSuccess(res) { 52 | if (res.error) { 53 | res.error.message.should.have.value(''); 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /test/utils.js: -------------------------------------------------------------------------------- 1 | import 'should'; 2 | import { min, split } from '../lib/utils'; 3 | 4 | describe('min', () => { 5 | return it('should work', () => { 6 | min([1, 10, 100]).should.be.equal(1); 7 | min([100, 10, 1]).should.be.equal(1); 8 | min([-1, 0, 1]).should.be.equal(-1); 9 | min([1, 0, -1]).should.be.equal(-1); 10 | min([5, 0, -5, 10, -10]).should.be.equal(-10); 11 | min([1, undefined]).should.be.equal(1); 12 | min([undefined, 1]).should.be.equal(1); 13 | min([1, 'a']).should.be.equal(1); 14 | min(['a', 1]).should.be.equal(1); 15 | min([1, 1]).should.be.equal(1); 16 | }); 17 | }); 18 | 19 | describe('split', () => { 20 | it('should work with one keyword', () => { 21 | const result = split("title eq 'something' and price gt 10", 'and'); 22 | result[0].should.be.equal("title eq 'something'"); 23 | result[1].should.be.equal("and"); 24 | result[2].should.be.equal("price gt 10"); 25 | }); 26 | it('should work with one keyword(array)', () => { 27 | const result = split("title eq 'something' and price gt 10", ['and']); 28 | result[0].should.be.equal("title eq 'something'"); 29 | result[1].should.be.equal("and"); 30 | result[2].should.be.equal("price gt 10"); 31 | }); 32 | return it('should work with multiple keywords', () => { 33 | const result = split("title eq 'something' and price gt 10 or author eq 'somebody'", ['and', 'or']); 34 | result[0].should.be.equal("title eq 'something'"); 35 | result[1].should.be.equal("and"); 36 | result[2].should.be.equal("price gt 10"); 37 | result[3].should.be.equal("or"); 38 | result[4].should.be.equal("author eq 'somebody'"); 39 | }); 40 | }); 41 | --------------------------------------------------------------------------------