├── .gitignore ├── .istanbul.yaml ├── .npmignore ├── .tslint ├── README.md ├── package-lock.json ├── package.json ├── src ├── example │ ├── advanced.ts │ ├── bigdata.ts │ ├── categories.ts │ ├── es6.js │ ├── inheritance.ts │ ├── model.ts │ ├── northwind.ts │ ├── products.ts │ ├── schema.ts │ ├── simple.ts │ ├── stream.ts │ └── test.ts ├── lib │ ├── controller.ts │ ├── edm.ts │ ├── error.ts │ ├── index.ts │ ├── metadata.ts │ ├── odata.ts │ ├── processor.ts │ ├── result.ts │ ├── server.ts │ ├── utils.ts │ └── visitor.ts └── test │ ├── TODO.md │ ├── benchmark.ts │ ├── define.spec.ts │ ├── execute.spec.ts │ ├── fixtures │ └── logo_jaystack.png │ ├── http.spec.ts │ ├── metadata.spec.ts │ ├── metadata │ ├── $actionfunction.xml │ ├── $defineentities.xml │ ├── $enumserver.xml │ ├── $metadata.xml │ ├── $schemajson.xml │ └── $typedefserver.xml │ ├── mocha.opts │ ├── model │ ├── ModelsForGenerator.ts │ ├── ModelsForPromise.ts │ ├── ModelsForStream.ts │ ├── categories.ts │ ├── model.ts │ └── products.ts │ ├── odata.spec.ts │ ├── projection.spec.ts │ ├── server.spec.ts │ ├── stream.spec.ts │ ├── test.model.ts │ ├── utils │ └── queryOptions.ts │ └── validator.spec.ts ├── tsconfig.json └── tslint.json /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .vscode 3 | *.log* 4 | coverage 5 | report 6 | build 7 | docs 8 | .nyc_output 9 | tmp.* -------------------------------------------------------------------------------- /.istanbul.yaml: -------------------------------------------------------------------------------- 1 | verbose: false 2 | instrumentation: 3 | root: . 4 | extensions: 5 | - .js 6 | default-excludes: true 7 | excludes: [] 8 | embed-source: false 9 | variable: __coverage__ 10 | compact: true 11 | preserve-comments: false 12 | complete-copy: false 13 | save-baseline: false 14 | baseline-file: ./coverage/coverage-baseline.json 15 | include-all-sources: false 16 | include-pid: false 17 | es-modules: false 18 | reporting: 19 | print: summary 20 | reports: 21 | - text 22 | dir: ./coverage 23 | watermarks: 24 | statements: [50, 80] 25 | lines: [50, 80] 26 | functions: [50, 80] 27 | branches: [50, 80] 28 | report-config: 29 | clover: {file: clover.xml} 30 | cobertura: {file: cobertura-coverage.xml} 31 | json: {file: coverage-final.json} 32 | json-summary: {file: coverage-summary.json} 33 | lcovonly: {file: lcov.info} 34 | teamcity: {file: null, blockName: Code Coverage Summary} 35 | text: {file: null, maxCols: 0} 36 | text-lcov: {file: lcov.info} 37 | text-summary: {file: null} 38 | hooks: 39 | hook-run-in-context: false 40 | post-require-hook: null 41 | handle-sigint: false 42 | check: 43 | global: 44 | statements: 0 45 | lines: 0 46 | branches: 0 47 | functions: 0 48 | excludes: [] 49 | each: 50 | statements: 0 51 | lines: 0 52 | branches: 0 53 | functions: 0 54 | excludes: [] -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | .vscode 2 | coverage 3 | report 4 | docs 5 | .nyc_output 6 | *.log* -------------------------------------------------------------------------------- /.tslint: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jaystack/odata-v4-server/1a1a9c426573c245446af2d6db0d01bfc51f96d0/.tslint -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # JayStack OData V4 Server 2 | 3 | OData V4 server for node.js 4 | 5 | ## Features 6 | 7 | * OASIS Standard OData Version 4.0 server 8 | * usable as a standalone server, as an Express router, as a node.js stream or as a library 9 | * expose service document and service metadata - $metadata 10 | * setup metadata using decorators or [metadata JSON](https://github.com/jaystack/odata-v4-service-metadata) 11 | * supported data types are Edm primitives, complex types, navigation properties 12 | * support create, read, update, and delete entity sets, action imports, function imports, collection and entity bound actions and functions 13 | * support for full OData query language using [odata-v4-parser](https://github.com/jaystack/odata-v4-parser) 14 | * filtering entities - $filter 15 | * sorting - $orderby 16 | * paging - $skip and $top 17 | * projection of entities - $select 18 | * expanding entities - $expand 19 | * $count 20 | * support sync and async controller functions 21 | * support async controller functions using Promise, async/await or ES6 generator functions 22 | * support result streaming 23 | * support media entities 24 | 25 | ## Controller and server functions parameter injection decorators 26 | 27 | * @odata.key 28 | * @odata.filter 29 | * @odata.query 30 | * @odata.context 31 | * @odata.body 32 | * @odata.result 33 | * @odata.stream 34 | 35 | ## Example Northwind server 36 | 37 | ```typescript 38 | export class ProductsController extends ODataController{ 39 | @odata.GET 40 | find(@odata.filter filter:ODataQuery){ 41 | if (filter) return products.filter(createFilter(filter)); 42 | return products; 43 | } 44 | 45 | @odata.GET 46 | findOne(@odata.key key:string){ 47 | return products.filter(product => product._id == key)[0]; 48 | } 49 | 50 | @odata.POST 51 | insert(@odata.body product:any){ 52 | product._id = new ObjectID().toString(); 53 | products.push(product); 54 | return product; 55 | } 56 | 57 | @odata.PATCH 58 | update(@odata.key key:string, @odata.body delta:any){ 59 | let product = products.filter(product => product._id == key)[0]; 60 | for (let prop in delta){ 61 | product[prop] = delta[prop]; 62 | } 63 | } 64 | 65 | @odata.DELETE 66 | remove(@odata.key key:string){ 67 | products.splice(products.indexOf(products.filter(product => product._id == key)[0]), 1); 68 | } 69 | } 70 | 71 | export class CategoriesController extends ODataController{ 72 | @odata.GET 73 | find(@odata.filter filter:ODataQuery){ 74 | if (filter) return categories.filter(createFilter(filter)); 75 | return categories; 76 | } 77 | 78 | @odata.GET 79 | findOne(@odata.key key:string){ 80 | return categories.filter(category => category._id == key)[0]; 81 | } 82 | 83 | @odata.POST 84 | insert(@odata.body category:any){ 85 | category._id = new ObjectID().toString(); 86 | categories.push(category); 87 | return category; 88 | } 89 | 90 | @odata.PATCH 91 | update(@odata.key key:string, @odata.body delta:any){ 92 | let category = categories.filter(category => category._id == key)[0]; 93 | for (let prop in delta){ 94 | category[prop] = delta[prop]; 95 | } 96 | } 97 | 98 | @odata.DELETE 99 | remove(@odata.key key:string){ 100 | categories.splice(categories.indexOf(categories.filter(category => category._id == key)[0]), 1); 101 | } 102 | } 103 | 104 | @odata.cors 105 | @odata.controller(ProductsController, true) 106 | @odata.controller(CategoriesController, true) 107 | export class NorthwindODataServer extends ODataServer{} 108 | NorthwindODataServer.create("/odata", 3000); 109 | ``` 110 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "odata-v4-server", 3 | "version": "0.2.13", 4 | "description": "OData V4 Server", 5 | "main": "build/lib/index.js", 6 | "typings": "build/lib/index", 7 | "scripts": { 8 | "prebuild": "rimraf build", 9 | "build": "npm run tsc", 10 | "pretsc": "npm run lint", 11 | "tsc": "tsc", 12 | "prewatch": "rimraf build", 13 | "watch": "npm-watch", 14 | "es6": "copyfiles -u 1 src/**/*.js build", 15 | "lint": "tslint src/lib/**/*.ts -t verbose --force > .tslint", 16 | "pretest": "rimraf report && rimraf coverage", 17 | "test": "nyc mocha --reporter mochawesome --reporter-options reportDir=report,reportName=odata-v4-server,reportTitle=\"OData V4 Server\" src/test/**/*.spec.ts", 18 | "test:http": "nyc mocha --reporter mochawesome --reporter-options reportDir=report,reportName=odata-v4-server,reportTitle=\"OData V4 Server\" src/test/**/http.spec.ts", 19 | "test:execute": "nyc mocha --reporter mochawesome --reporter-options reportDir=report,reportName=odata-v4-server,reportTitle=\"OData V4 Server\" src/test/**/execute.spec.ts", 20 | "test:stream": "nyc mocha --reporter mochawesome --reporter-options reportDir=report,reportName=odata-v4-server,reportTitle=\"OData V4 Server\" src/test/**/stream.spec.ts", 21 | "test:metadata": "nyc mocha --reporter mochawesome --reporter-options reportDir=report,reportName=odata-v4-server,reportTitle=\"OData V4 Server\" src/test/**/metadata.spec.ts", 22 | "test:validator": "nyc mocha --reporter mochawesome --reporter-options reportDir=report,reportName=odata-v4-server,reportTitle=\"OData V4 Server\" src/test/**/validator.spec.ts", 23 | "test:projection": "nyc mocha --reporter mochawesome --reporter-options reportDir=report,reportName=odata-v4-server,reportTitle=\"OData V4 Server\" src/test/**/projection.spec.ts", 24 | "prepare": "npm test && npm run build", 25 | "typedoc": "rimraf docs && typedoc --name \"JayStack OData v4 Server\" --exclude \"**/?(utils|index).ts\" --excludeExternals --excludeNotExported --hideGenerator --excludePrivate --out docs src/lib" 26 | }, 27 | "watch": { 28 | "tsc": { 29 | "patterns": [ 30 | "src" 31 | ], 32 | "extensions": "ts", 33 | "quiet": false 34 | }, 35 | "es6": { 36 | "patterns": [ 37 | "src" 38 | ], 39 | "extensions": "js", 40 | "quiet": false 41 | } 42 | }, 43 | "nyc": { 44 | "include": [ 45 | "src/lib/*.ts" 46 | ], 47 | "extension": [ 48 | ".ts" 49 | ], 50 | "exclude": [ 51 | "build/**/*", 52 | "**/*.spec.ts", 53 | "**/*.d.ts" 54 | ], 55 | "require": [ 56 | "ts-node/register" 57 | ], 58 | "reporter": [ 59 | "text-summary", 60 | "html" 61 | ], 62 | "sourceMap": true, 63 | "instrument": true 64 | }, 65 | "repository": { 66 | "type": "git", 67 | "url": "git+https://github.com/jaystack/odata-v4-server.git" 68 | }, 69 | "keywords": [ 70 | "OData", 71 | "server", 72 | "V4", 73 | "server" 74 | ], 75 | "author": "JayStack", 76 | "license": "MIT", 77 | "bugs": { 78 | "url": "https://github.com/jaystack/odata-v4-server/issues" 79 | }, 80 | "homepage": "https://github.com/jaystack/odata-v4-server#readme", 81 | "dependencies": { 82 | "@types/body-parser": "1.17.0", 83 | "@types/cors": "2.8.4", 84 | "@types/express": "^4.16.0", 85 | "@types/qs": "^6.5.1", 86 | "body-parser": "^1.18.3", 87 | "cors": "^2.8.4", 88 | "deepmerge": "^2.1.1", 89 | "express": "^4.16.3", 90 | "odata-v4-literal": "^0.1.1", 91 | "odata-v4-metadata": "^0.1.5", 92 | "odata-v4-parser": "^0.1.29", 93 | "odata-v4-service-document": "0.0.3", 94 | "odata-v4-service-metadata": "^0.1.6", 95 | "qs": "^6.5.2", 96 | "reflect-metadata": "^0.1.12", 97 | "tslib": "^1.9.3" 98 | }, 99 | "devDependencies": { 100 | "@types/benchmark": "^1.0.31", 101 | "@types/deepmerge": "^2.1.0", 102 | "@types/event-stream": "^3.3.34", 103 | "@types/jsonstream": "^0.8.30", 104 | "@types/lodash": "^4.14.112", 105 | "@types/mocha": "^5.2.5", 106 | "@types/mongodb": "^3.1.1", 107 | "@types/node": "^10.5.2", 108 | "@types/request": "^2.47.1", 109 | "@types/request-promise": "^4.1.42", 110 | "@types/stream-buffers": "^3.0.2", 111 | "JSONStream": "^1.3.3", 112 | "benchmark": "^2.1.4", 113 | "chai": "^4.1.2", 114 | "copyfiles": "^2.0.0", 115 | "event-stream": "^3.3.4", 116 | "istanbul": "^1.1.0-alpha.1", 117 | "mocha": "^5.2.0", 118 | "mochawesome": "^3.0.2", 119 | "mongodb": "^3.1.1", 120 | "mssql": "^4.1.0", 121 | "npm-watch": "^0.3.0", 122 | "nyc": "^12.0.2", 123 | "odata-v4-inmemory": "^0.1.9", 124 | "odata-v4-mongodb": "^0.1.12", 125 | "remap-istanbul": "^0.11.1", 126 | "request": "^2.87.0", 127 | "request-promise": "^4.2.2", 128 | "rimraf": "^2.6.2", 129 | "source-map-support": "^0.5.6", 130 | "stream-buffers": "^3.0.2", 131 | "ts-node": "^7.0.0", 132 | "tslint": "^5.11.0", 133 | "typedoc": "^0.11.1", 134 | "typedoc-default-themes": "^0.5.0", 135 | "typedoc-plugin-external-module-name": "^1.1.1", 136 | "typescript": "^2.9.2", 137 | "xml-beautifier": "^0.4.0" 138 | } 139 | } 140 | -------------------------------------------------------------------------------- /src/example/advanced.ts: -------------------------------------------------------------------------------- 1 | import { MongoClient, ObjectID, Db } from "mongodb"; 2 | import { createQuery } from "odata-v4-mongodb"; 3 | import { Edm, odata, ODataController, ODataServer, ODataQuery, createODataServer, ODataErrorHandler } from "../lib/index"; 4 | import { Category, Product, NorthwindTypes } from "./model"; 5 | import { Writable } from "stream"; 6 | let categories = require("./categories"); 7 | let products = require("./products"); 8 | 9 | const mongodb = async function():Promise{ 10 | return (await MongoClient.connect("mongodb://localhost:27017/odataserver")).db(); 11 | }; 12 | 13 | @odata.type(Product) 14 | @Edm.EntitySet("Products") 15 | export class ProductsController extends ODataController{ 16 | @odata.GET 17 | async find(@odata.query query:ODataQuery, @odata.stream stream:Writable){ 18 | let mongodbQuery = createQuery(query); 19 | return (await mongodb()).collection("Products").find(mongodbQuery.query, { projection: mongodbQuery.projection, skip: mongodbQuery.skip, limit: mongodbQuery.limit }).stream().pipe(stream); 20 | } 21 | 22 | @odata.GET 23 | async findOne(@odata.key key:string, @odata.query query:ODataQuery){ 24 | let mongodbQuery = createQuery(query); 25 | return (await mongodb()).collection("Products").findOne({ _id: key }, { 26 | fields: mongodbQuery.projection 27 | }); 28 | } 29 | 30 | /*@odata.GET("Category") 31 | async getCategory(@odata.result result:any){ 32 | return (await mongodb()).collection("Categories").findOne({ _id: result.CategoryId }); 33 | }*/ 34 | } 35 | 36 | @odata.type(Category) 37 | @Edm.EntitySet("Categories") 38 | export class CategoriesController extends ODataController{ 39 | @odata.GET 40 | async find(@odata.query query:ODataQuery, @odata.stream stream:Writable){ 41 | let mongodbQuery = createQuery(query); 42 | return (await mongodb()).collection("Categories").find(mongodbQuery.query, { projection: mongodbQuery.projection, skip: mongodbQuery.skip, limit: mongodbQuery.limit }).stream().pipe(stream); 43 | } 44 | 45 | @odata.GET 46 | async findOne(@odata.key() key:string, @odata.query query:ODataQuery){ 47 | let mongodbQuery = createQuery(query); 48 | return (await mongodb()).collection("Categories").findOne({ _id: key }, { 49 | fields: mongodbQuery.projection 50 | }); 51 | } 52 | 53 | /*@odata.GET("Products") 54 | async getProducts(@odata.result result:any, @odata.query query:ODataQuery, @odata.stream stream:Writable){ 55 | let mongodbQuery = createQuery(query); 56 | mongodbQuery.query = { $and: [mongodbQuery.query, { CategoryId: result._id }] }; 57 | return (await mongodb()).collection("Products").find(mongodbQuery.query, mongodbQuery.projection, mongodbQuery.skip, mongodbQuery.limit).stream().pipe(stream); 58 | }*/ 59 | } 60 | 61 | @odata.namespace("Northwind") 62 | @Edm.Container(NorthwindTypes) 63 | @odata.container("NorthwindContext") 64 | @odata.controller(ProductsController) 65 | @odata.controller(CategoriesController) 66 | @odata.cors 67 | export class NorthwindODataServer extends ODataServer{ 68 | @Edm.EntityType(Category) 69 | @Edm.FunctionImport 70 | *GetCategoryById(@Edm.String id:string){ 71 | return yield categories.filter((category: any) => category._id.toString() == id)[0]; 72 | } 73 | 74 | @Edm.ActionImport 75 | async initDb():Promise{ 76 | let db = await mongodb(); 77 | await db.dropDatabase(); 78 | let categoryCollection = db.collection("Categories"); 79 | let productsCollection = db.collection("Products"); 80 | await categoryCollection.insertMany(categories); 81 | await productsCollection.insertMany(products); 82 | } 83 | 84 | static errorHandler(err, req, res, next){ 85 | delete err.stack; 86 | ODataErrorHandler(err, req, res, next); 87 | } 88 | } 89 | 90 | createODataServer(NorthwindODataServer, "/odata", 3000); 91 | 92 | process.on("warning", warning => { 93 | console.log(warning.stack); 94 | }); 95 | 96 | Error.stackTraceLimit = -1; -------------------------------------------------------------------------------- /src/example/bigdata.ts: -------------------------------------------------------------------------------- 1 | import { createFilter } from "odata-v4-inmemory"; 2 | import { Token } from "odata-v4-parser/lib/lexer"; 3 | import { ODataServer, ODataController, Edm, odata } from "../lib/index"; 4 | 5 | let schemaJson = { 6 | version: "4.0", 7 | dataServices: { 8 | schema: [{ 9 | namespace: "BigData", 10 | entityType: [{ 11 | name: "Index", 12 | key: [{ 13 | propertyRef: [{ 14 | name: "id" 15 | }] 16 | }], 17 | property: [{ 18 | name: "id", 19 | type: "Edm.Int64", 20 | nullable: false 21 | }] 22 | }], 23 | entityContainer: { 24 | name: "BigDataContext", 25 | entitySet: [{ 26 | name: "Indices", 27 | entityType: "BigData.Index" 28 | }] 29 | } 30 | }] 31 | } 32 | }; 33 | 34 | let schemaEntityTypeProperties = schemaJson.dataServices.schema[0].entityType[0].property; 35 | const propertyCount = 1000; 36 | for (let i = 0; i < propertyCount; i++){ 37 | schemaEntityTypeProperties.push({ 38 | name: `Property${i}`, 39 | type: "Edm.String", 40 | nullable: true 41 | }); 42 | } 43 | console.log("Schema ready."); 44 | 45 | let bigdata = []; 46 | const dataCount = 100; 47 | for (let d = 0; d < dataCount; d++){ 48 | let data = { 49 | id: d 50 | }; 51 | for (let i = 0; i < propertyCount; i++){ 52 | data[`Property${i}`] = `StringData${i}-${d}`; 53 | } 54 | bigdata.push(data); 55 | } 56 | console.log("Data ready."); 57 | 58 | @Edm.OpenType 59 | class Index{ 60 | @Edm.Int64 61 | id: number 62 | } 63 | 64 | @odata.type(Index) 65 | class IndicesController extends ODataController { 66 | @odata.GET 67 | find( @odata.filter filter: Token, @odata.query query: Token ) { 68 | let mapper = it => it; 69 | if (query){ 70 | let $select = query.value.options.find(t => t.type == "Select"); 71 | if ($select){ 72 | let props = $select.value.items.map(t => t.raw); 73 | mapper = it => { 74 | let r = {}; 75 | props.forEach(p => r[p] = it[p]); 76 | return r; 77 | }; 78 | } 79 | } 80 | if (filter) return bigdata.filter(createFilter(filter)).map(mapper); 81 | return bigdata.map(mapper); 82 | } 83 | } 84 | 85 | @odata.namespace("BigData") 86 | @odata.controller(IndicesController, true) 87 | class BigDataServer extends ODataServer {} 88 | BigDataServer.$metadata(schemaJson); 89 | BigDataServer.create("/odata", 3000); 90 | console.log("OData ready."); -------------------------------------------------------------------------------- /src/example/categories.ts: -------------------------------------------------------------------------------- 1 | import { ObjectID } from "mongodb"; 2 | 3 | export = [ 4 | {"_id": new ObjectID("578f2baa12eaebabec4af289"),"Description":"Soft drinks","Name":"Beverages"}, 5 | {"_id": new ObjectID("578f2baa12eaebabec4af28a"),"Description":"Breads","Name":"Grains/Cereals"}, 6 | {"_id": new ObjectID("578f2baa12eaebabec4af28b"),"Description":"Prepared meats","Name":"Meat/Poultry"}, 7 | {"_id": new ObjectID("578f2baa12eaebabec4af28c"),"Description":"Dried fruit and bean curd","Name":"Produce"}, 8 | {"_id": new ObjectID("578f2baa12eaebabec4af28d"),"Description":"Seaweed and fish","Name":"Seafood"}, 9 | {"_id": new ObjectID("578f2baa12eaebabec4af28e"),"Description":"Sweet and savory sauces","Name":"Condiments"}, 10 | {"_id": new ObjectID("578f2baa12eaebabec4af28f"),"Description":"Cheeses","Name":"Dairy Products"}, 11 | {"_id": new ObjectID("578f2baa12eaebabec4af290"),"Description":"Desserts","Name":"Confections"} 12 | ]; -------------------------------------------------------------------------------- /src/example/es6.js: -------------------------------------------------------------------------------- 1 | const { ODataServer, ODataController, ODataEntity, odata, Edm } = require('../lib'); 2 | 3 | class ES6Type extends ODataEntity{} 4 | ES6Type.define({ 5 | id: [Edm.Int32, Edm.Key, Edm.Computed], 6 | key: Edm.String, 7 | value: Edm.String 8 | }); 9 | 10 | class ES6Controller extends ODataController{ 11 | all(){ 12 | return [Object.assign(new ES6Type(), { 13 | id: 1, 14 | key: 'almafa', 15 | value: 'kiscica' 16 | })]; 17 | } 18 | one(key){ 19 | return Object.assign(new ES6Type(), { 20 | id: key, 21 | key: 'almafa2', 22 | value: 'kiscica2' 23 | }); 24 | } 25 | } 26 | ES6Controller.define(odata.type(ES6Type), { 27 | all: odata.GET, 28 | one: [odata.GET, { 29 | key: odata.key 30 | }] 31 | }); 32 | 33 | class ES6Server extends ODataServer{} 34 | ES6Server.define(odata.controller(ES6Controller, true)); 35 | ES6Server.create(3000); 36 | 37 | console.log('ES6 OData server listening on port: 3000'); -------------------------------------------------------------------------------- /src/example/inheritance.ts: -------------------------------------------------------------------------------- 1 | import { Edm, odata, ODataController, ODataServer } from "../lib"; 2 | 3 | @odata.namespace("InheritanceSchema") 4 | export class Category{ 5 | @Edm.Key 6 | @Edm.Computed 7 | @Edm.Int32 8 | id: number; 9 | 10 | @Edm.String 11 | title: string; 12 | 13 | constructor(title:string){ 14 | this.id = Math.floor(Math.random() * 100); 15 | this.title = title; 16 | } 17 | } 18 | 19 | @odata.namespace("Default") 20 | export class Subcategory extends Category{ 21 | @Edm.String 22 | subtitle: string; 23 | 24 | constructor(title:string, subtitle:string){ 25 | super(title); 26 | this.subtitle = subtitle; 27 | } 28 | } 29 | 30 | @odata.namespace("Default") 31 | export class Subcategory2 extends Category{ 32 | @Edm.String 33 | subtitle2: string; 34 | 35 | constructor(title:string, subtitle:string){ 36 | super(title); 37 | this.subtitle2 = subtitle; 38 | } 39 | } 40 | 41 | export class SubcategoryDetails extends Subcategory{ 42 | @Edm.String 43 | description: string; 44 | 45 | @Edm.Key 46 | @Edm.Int32 47 | subid: number 48 | 49 | constructor(title:string, subtitle:string, description:string){ 50 | super(title, subtitle); 51 | this.description = description; 52 | this.subid = Math.floor(Math.random() * 100) + 1000; 53 | } 54 | } 55 | 56 | @odata.type(Subcategory) 57 | export class InheritanceController extends ODataController{ 58 | @odata.GET 59 | all(){ 60 | return [ 61 | { id: 123, title: "Games", "@odata.type": Category }, 62 | new Category("Games"), 63 | new Subcategory("Games", "Hearthstone"), 64 | new Subcategory2("Games", "Diablo 3"), 65 | new SubcategoryDetails("Games", "Diablo 3", "RPG game") 66 | ]; 67 | } 68 | 69 | @odata.GET 70 | one(@odata.key _: number, @odata.key __: number){ 71 | return new SubcategoryDetails("Games", "Diablo 3", "RPG game"); 72 | } 73 | 74 | @odata.POST 75 | insert(@odata.body data:any, @odata.type type:string){ 76 | console.log('@odata.type', type, data); 77 | return data; 78 | } 79 | } 80 | 81 | @odata.controller(InheritanceController, true) 82 | @odata.controller(InheritanceController, "Inheritance2") 83 | export class InheritanceServer extends ODataServer{} 84 | 85 | InheritanceServer.create(3000); -------------------------------------------------------------------------------- /src/example/model.ts: -------------------------------------------------------------------------------- 1 | import { ObjectID } from "mongodb"; 2 | import { Edm } from "../lib/index"; 3 | 4 | const toObjectID = _id => _id && !(_id instanceof ObjectID) ? ObjectID.createFromHexString(_id) : _id; 5 | 6 | @Edm.Annotate({ 7 | term: "UI.DisplayName", 8 | string: "Products" 9 | }) 10 | export class Product{ 11 | @Edm.Key 12 | @Edm.Computed 13 | @Edm.TypeDefinition(ObjectID) 14 | @Edm.Annotate({ 15 | term: "UI.DisplayName", 16 | string: "Product identifier" 17 | }, { 18 | term: "UI.ControlHint", 19 | string: "ReadOnly" 20 | }) 21 | _id:ObjectID 22 | 23 | @Edm.TypeDefinition(ObjectID) 24 | @Edm.Required 25 | CategoryId:ObjectID 26 | 27 | @Edm.ForeignKey("CategoryId") 28 | @Edm.EntityType(Edm.ForwardRef(() => Category)) 29 | @Edm.Partner("Products") 30 | Category:Category 31 | 32 | @Edm.Boolean 33 | Discontinued:boolean 34 | 35 | @Edm.String 36 | @Edm.Annotate({ 37 | term: "UI.DisplayName", 38 | string: "Product title" 39 | }, { 40 | term: "UI.ControlHint", 41 | string: "ShortText" 42 | }) 43 | Name:string 44 | 45 | @Edm.String 46 | @Edm.Annotate({ 47 | term: "UI.DisplayName", 48 | string: "Product English name" 49 | }, { 50 | term: "UI.ControlHint", 51 | string: "ShortText" 52 | }) 53 | QuantityPerUnit:string 54 | 55 | @Edm.Decimal 56 | @Edm.Annotate({ 57 | term: "UI.DisplayName", 58 | string: "Unit price of product" 59 | }, { 60 | term: "UI.ControlHint", 61 | string: "Decimal" 62 | }) 63 | UnitPrice:number 64 | } 65 | 66 | @Edm.OpenType 67 | @Edm.Annotate({ 68 | term: "UI.DisplayName", 69 | string: "Categories" 70 | }) 71 | export class Category{ 72 | @Edm.Key 73 | @Edm.Computed 74 | @Edm.TypeDefinition(ObjectID) 75 | @Edm.Annotate({ 76 | term: "UI.DisplayName", 77 | string: "Category identifier" 78 | }, 79 | { 80 | term: "UI.ControlHint", 81 | string: "ReadOnly" 82 | }) 83 | _id:ObjectID 84 | 85 | @Edm.String 86 | Description:string 87 | 88 | @Edm.String 89 | @Edm.Annotate({ 90 | term: "UI.DisplayName", 91 | string: "Category name" 92 | }, 93 | { 94 | term: "UI.ControlHint", 95 | string: "ShortText" 96 | }) 97 | Name:string 98 | 99 | @Edm.ForeignKey("CategoryId") 100 | @Edm.Collection(Edm.EntityType(Product)) 101 | @Edm.Partner("Category") 102 | Products:Product[] 103 | 104 | @Edm.Collection(Edm.String) 105 | @Edm.Function 106 | echo(){ 107 | return ["echotest"]; 108 | } 109 | } 110 | 111 | export class NorthwindTypes extends Edm.ContainerBase{ 112 | @Edm.String 113 | @Edm.URLDeserialize((value:string) => new ObjectID(value)) 114 | @Edm.Deserialize(value => new ObjectID(value)) 115 | ObjectID = ObjectID 116 | } -------------------------------------------------------------------------------- /src/example/northwind.ts: -------------------------------------------------------------------------------- 1 | import { MongoClient, Db, ObjectID } from "mongodb"; 2 | import { createQuery } from "odata-v4-mongodb"; 3 | import { Token } from "odata-v4-parser/lib/lexer"; 4 | import { ODataServer, ODataController, odata } from "../lib/index"; 5 | let schemaJson = require("./schema"); 6 | let categories = require("./categories"); 7 | let products = require("./products"); 8 | 9 | const mongodb = async function (): Promise { 10 | return (await MongoClient.connect("mongodb://localhost:27017/odataserver")).db(); 11 | }; 12 | 13 | class ProductsController extends ODataController { 14 | @odata.GET 15 | *find( @odata.query query: Token) { 16 | let db: Db = yield mongodb(); 17 | let mongodbQuery = createQuery(query); 18 | if (typeof mongodbQuery.query._id == "string") mongodbQuery.query._id = new ObjectID(mongodbQuery.query._id); 19 | if (typeof mongodbQuery.query.CategoryId == "string") mongodbQuery.query.CategoryId = new ObjectID(mongodbQuery.query.CategoryId); 20 | return db.collection("Products").find( 21 | mongodbQuery.query, { 22 | projection: mongodbQuery.projection, 23 | skip: mongodbQuery.skip, 24 | limit: mongodbQuery.limit 25 | } 26 | ).toArray(); 27 | } 28 | 29 | @odata.GET 30 | *findOne( @odata.key key: string, @odata.query query: Token) { 31 | let db: Db = yield mongodb(); 32 | let mongodbQuery = createQuery(query); 33 | return db.collection("Products").findOne({ _id: new ObjectID(key) }, { 34 | fields: mongodbQuery.projection 35 | }); 36 | } 37 | 38 | @odata.POST 39 | async insert( @odata.body data: any) { 40 | let db = await mongodb(); 41 | if (data.CategoryId) data.CategoryId = new ObjectID(data.CategoryId); 42 | return await db.collection("Products").insert(data).then((result) => { 43 | data._id = result.insertedId; 44 | return data; 45 | }); 46 | } 47 | } 48 | 49 | class CategoriesController extends ODataController { 50 | @odata.GET 51 | *find( @odata.query query: Token): any { 52 | let db: Db = yield mongodb(); 53 | let mongodbQuery = createQuery(query); 54 | if (typeof mongodbQuery.query._id == "string") mongodbQuery.query._id = new ObjectID(mongodbQuery.query._id); 55 | return db.collection("Categories").find( 56 | mongodbQuery.query, { 57 | projection: mongodbQuery.projection, 58 | skip: mongodbQuery.skip, 59 | limit: mongodbQuery.limit 60 | } 61 | ).toArray(); 62 | } 63 | 64 | @odata.GET 65 | *findOne( @odata.key key: string, @odata.query query: Token) { 66 | let db: Db = yield mongodb(); 67 | let mongodbQuery = createQuery(query); 68 | return db.collection("Categories").findOne({ _id: new ObjectID(key) }, { 69 | fields: mongodbQuery.projection 70 | }); 71 | } 72 | } 73 | 74 | @odata.controller(ProductsController, true) 75 | @odata.controller(CategoriesController, true) 76 | class NorthwindServer extends ODataServer { 77 | async initDb() { 78 | let db = await mongodb(); 79 | await db.dropDatabase(); 80 | let categoryCollection = db.collection("Categories"); 81 | let productsCollection = db.collection("Products"); 82 | await categoryCollection.insertMany(categories); 83 | await productsCollection.insertMany(products); 84 | } 85 | } 86 | NorthwindServer.$metadata(schemaJson); 87 | NorthwindServer.create("/odata", 3000); -------------------------------------------------------------------------------- /src/example/products.ts: -------------------------------------------------------------------------------- 1 | import { ObjectID } from "mongodb"; 2 | 3 | export = [ 4 | {"_id": new ObjectID("578f2b8c12eaebabec4af23c"),"QuantityPerUnit":"10 boxes x 20 bags","UnitPrice":39,"CategoryId": new ObjectID("578f2baa12eaebabec4af289"),"Name":"Chai","Discontinued":false}, 5 | {"_id": new ObjectID("578f2b8c12eaebabec4af23d"),"QuantityPerUnit":"24 - 12 oz bottles","UnitPrice":19.0,"CategoryId": new ObjectID("578f2baa12eaebabec4af289"),"Name":"Chang","Discontinued":true}, 6 | {"_id": new ObjectID("578f2b8c12eaebabec4af23e"),"QuantityPerUnit":"12 - 550 ml bottles","UnitPrice":10.0,"CategoryId": new ObjectID("578f2baa12eaebabec4af28e"),"Name":"Aniseed Syrup","Discontinued":false}, 7 | {"_id": new ObjectID("578f2b8c12eaebabec4af23f"),"QuantityPerUnit":"48 - 6 oz jars","UnitPrice":22.0,"CategoryId": new ObjectID("578f2baa12eaebabec4af28e"),"Name":"Chef Anton's Cajun Seasoning","Discontinued":true}, 8 | {"_id": new ObjectID("578f2b8c12eaebabec4af240"),"QuantityPerUnit":"36 boxes","UnitPrice":21.35,"CategoryId": new ObjectID("578f2baa12eaebabec4af28e"),"Name":"Chef Anton's Gumbo Mix","Discontinued":false}, 9 | {"_id": new ObjectID("578f2b8c12eaebabec4af241"),"QuantityPerUnit":"12 - 8 oz jars","UnitPrice":25.0,"CategoryId": new ObjectID("578f2baa12eaebabec4af28e"),"Name":"Grandma's Boysenberry Spread","Discontinued":false}, 10 | {"_id": new ObjectID("578f2b8c12eaebabec4af242"),"QuantityPerUnit":"12 - 200 ml jars","UnitPrice":31.0,"CategoryId": new ObjectID("578f2baa12eaebabec4af28d"),"Name":"Ikura","Discontinued":false}, 11 | {"_id": new ObjectID("578f2b8c12eaebabec4af243"),"QuantityPerUnit":"1 kg pkg.","UnitPrice":21.0,"CategoryId": new ObjectID("578f2baa12eaebabec4af28f"),"Name":"Queso Cabrales","Discontinued":false}, 12 | {"_id": new ObjectID("578f2b8c12eaebabec4af244"),"QuantityPerUnit":"10 - 500 g pkgs.","UnitPrice":38.0,"CategoryId": new ObjectID("578f2baa12eaebabec4af28f"),"Name":"Queso Manchego La Pastora","Discontinued":true}, 13 | {"_id": new ObjectID("578f2b8c12eaebabec4af245"),"QuantityPerUnit":"2 kg box","UnitPrice":6.0,"CategoryId": new ObjectID("578f2baa12eaebabec4af28d"),"Name":"Konbu","Discontinued":false}, 14 | {"_id": new ObjectID("578f2b8c12eaebabec4af246"),"QuantityPerUnit":"40 - 100 g pkgs.","UnitPrice":23.25,"CategoryId": new ObjectID("578f2baa12eaebabec4af28c"),"Name":"Tofu","Discontinued":false}, 15 | {"_id": new ObjectID("578f2b8c12eaebabec4af247"),"QuantityPerUnit":"24 - 250 ml bottles","UnitPrice":15.5,"CategoryId": new ObjectID("578f2baa12eaebabec4af28e"),"Name":"Genen Shouyu","Discontinued":false}, 16 | {"_id": new ObjectID("578f2b8c12eaebabec4af248"),"QuantityPerUnit":"32 - 500 g boxes","UnitPrice":17.45,"CategoryId": new ObjectID("578f2baa12eaebabec4af290"),"Name":"Pavlova","Discontinued":false}, 17 | {"_id": new ObjectID("578f2b8c12eaebabec4af249"),"QuantityPerUnit":"20 - 1 kg tins","UnitPrice":39.0,"CategoryId": new ObjectID("578f2baa12eaebabec4af28b"),"Name":"Alice Mutton","Discontinued":false}, 18 | {"_id": new ObjectID("578f2b8c12eaebabec4af24a"),"QuantityPerUnit":"16 kg pkg.","UnitPrice":62.5,"CategoryId": new ObjectID("578f2baa12eaebabec4af28d"),"Name":"Carnarvon Tigers","Discontinued":false}, 19 | {"_id": new ObjectID("578f2b8c12eaebabec4af24b"),"QuantityPerUnit":"10 boxes x 12 pieces","UnitPrice":9.2,"CategoryId": new ObjectID("578f2baa12eaebabec4af290"),"Name":"Teatime Chocolate Biscuits","Discontinued":true}, 20 | {"_id": new ObjectID("578f2b8c12eaebabec4af24c"),"QuantityPerUnit":"30 gift boxes","UnitPrice":81.0,"CategoryId": new ObjectID("578f2baa12eaebabec4af290"),"Name":"Sir Rodney's Marmalade","Discontinued":false}, 21 | {"_id": new ObjectID("578f2b8c12eaebabec4af24d"),"QuantityPerUnit":"24 pkgs. x 4 pieces","UnitPrice":10.0,"CategoryId": new ObjectID("578f2baa12eaebabec4af290"),"Name":"Sir Rodney's Scones","Discontinued":false}, 22 | {"_id": new ObjectID("578f2b8c12eaebabec4af24e"),"QuantityPerUnit":"12 - 1 lb pkgs.","UnitPrice":30.0,"CategoryId": new ObjectID("578f2baa12eaebabec4af28c"),"Name":"Uncle Bob's Organic Dried Pears","Discontinued":true}, 23 | {"_id": new ObjectID("578f2b8c12eaebabec4af24f"),"QuantityPerUnit":"12 - 12 oz jars","UnitPrice":40.0,"CategoryId": new ObjectID("578f2baa12eaebabec4af28e"),"Name":"Northwoods Cranberry Sauce","Discontinued":false}, 24 | {"_id": new ObjectID("578f2b8c12eaebabec4af250"),"QuantityPerUnit":"18 - 500 g pkgs.","UnitPrice":97.0,"CategoryId": new ObjectID("578f2baa12eaebabec4af28b"),"Name":"Mishi Kobe Niku","Discontinued":false}, 25 | {"_id": new ObjectID("578f2b8c12eaebabec4af251"),"QuantityPerUnit":"24 - 500 g pkgs.","UnitPrice":21.0,"CategoryId": new ObjectID("578f2baa12eaebabec4af28a"),"Name":"Gustaf's Knäckebröd","Discontinued":false}, 26 | {"_id": new ObjectID("578f2b8c12eaebabec4af252"),"QuantityPerUnit":"12 - 250 g pkgs.","UnitPrice":9.0,"CategoryId": new ObjectID("578f2baa12eaebabec4af28a"),"Name":"Tunnbröd","Discontinued":false}, 27 | {"_id": new ObjectID("578f2b8c12eaebabec4af253"),"QuantityPerUnit":"12 - 355 ml cans","UnitPrice":4.5,"CategoryId": new ObjectID("578f2baa12eaebabec4af289"),"Name":"Guaraná Fantástica","Discontinued":false}, 28 | {"_id": new ObjectID("578f2b8c12eaebabec4af254"),"QuantityPerUnit":"20 - 450 g glasses","UnitPrice":14.0,"CategoryId": new ObjectID("578f2baa12eaebabec4af290"),"Name":"NuNuCa Nuß-Nougat-Creme","Discontinued":true}, 29 | {"_id": new ObjectID("578f2b8c12eaebabec4af255"),"QuantityPerUnit":"100 - 250 g bags","UnitPrice":31.23,"CategoryId": new ObjectID("578f2baa12eaebabec4af290"),"Name":"Gumbär Gummibärchen","Discontinued":false}, 30 | {"_id": new ObjectID("578f2b8c12eaebabec4af256"),"QuantityPerUnit":"10 - 200 g glasses","UnitPrice":25.89,"CategoryId": new ObjectID("578f2baa12eaebabec4af28d"),"Name":"Nord-Ost Matjeshering","Discontinued":true}, 31 | {"_id": new ObjectID("578f2b8c12eaebabec4af257"),"QuantityPerUnit":"12 - 100 g pkgs","UnitPrice":12.5,"CategoryId": new ObjectID("578f2baa12eaebabec4af28f"),"Name":"Gorgonzola Telino","Discontinued":false}, 32 | {"_id": new ObjectID("578f2b8c12eaebabec4af258"),"QuantityPerUnit":"24 - 200 g pkgs.","UnitPrice":32.0,"CategoryId": new ObjectID("578f2baa12eaebabec4af28f"),"Name":"Mascarpone Fabioli","Discontinued":false}, 33 | {"_id": new ObjectID("578f2b8c12eaebabec4af259"),"QuantityPerUnit":"500 g","UnitPrice":2.5,"CategoryId": new ObjectID("578f2baa12eaebabec4af28f"),"Name":"Geitost","Discontinued":false}, 34 | {"_id": new ObjectID("578f2b8c12eaebabec4af25a"),"QuantityPerUnit":"24 - 12 oz bottles","UnitPrice":14.0,"CategoryId": new ObjectID("578f2baa12eaebabec4af289"),"Name":"Sasquatch Ale","Discontinued":false}, 35 | {"_id": new ObjectID("578f2b8c12eaebabec4af25b"),"QuantityPerUnit":"24 - 12 oz bottles","UnitPrice":18.0,"CategoryId": new ObjectID("578f2baa12eaebabec4af289"),"Name":"Steeleye Stout","Discontinued":false}, 36 | {"_id": new ObjectID("578f2b8c12eaebabec4af25c"),"QuantityPerUnit":"24 - 250 g jars","UnitPrice":19.0,"CategoryId": new ObjectID("578f2baa12eaebabec4af28d"),"Name":"Inlagd Sill","Discontinued":false}, 37 | {"_id": new ObjectID("578f2b8c12eaebabec4af25d"),"QuantityPerUnit":"12 - 500 g pkgs.","UnitPrice":26.0,"CategoryId": new ObjectID("578f2baa12eaebabec4af28d"),"Name":"Gravad lax","Discontinued":false}, 38 | {"_id": new ObjectID("578f2b8c12eaebabec4af25e"),"QuantityPerUnit":"12 - 75 cl bottles","UnitPrice":263.5,"CategoryId": new ObjectID("578f2baa12eaebabec4af289"),"Name":"Côte de Blaye","Discontinued":false}, 39 | {"_id": new ObjectID("578f2b8c12eaebabec4af25f"),"QuantityPerUnit":"750 cc per bottle","UnitPrice":18.0,"CategoryId": new ObjectID("578f2baa12eaebabec4af289"),"Name":"Chartreuse verte","Discontinued":false}, 40 | {"_id": new ObjectID("578f2b8c12eaebabec4af260"),"QuantityPerUnit":"24 - 4 oz tins","UnitPrice":18.4,"CategoryId": new ObjectID("578f2baa12eaebabec4af28d"),"Name":"Boston Crab Meat","Discontinued":false}, 41 | {"_id": new ObjectID("578f2b8c12eaebabec4af261"),"QuantityPerUnit":"12 - 12 oz cans","UnitPrice":9.65,"CategoryId": new ObjectID("578f2baa12eaebabec4af28d"),"Name":"Jack's New England Clam Chowder","Discontinued":true}, 42 | {"_id": new ObjectID("578f2b8c12eaebabec4af262"),"QuantityPerUnit":"32 - 1 kg pkgs.","UnitPrice":14.0,"CategoryId": new ObjectID("578f2baa12eaebabec4af28a"),"Name":"Singaporean Hokkien Fried Mee","Discontinued":true}, 43 | {"_id": new ObjectID("578f2b8c12eaebabec4af263"),"QuantityPerUnit":"16 - 500 g tins","UnitPrice":46.0,"CategoryId": new ObjectID("578f2baa12eaebabec4af289"),"Name":"Ipoh Coffee","Discontinued":false}, 44 | {"_id": new ObjectID("578f2b8c12eaebabec4af265"),"QuantityPerUnit":"1k pkg.","UnitPrice":9.5,"CategoryId": new ObjectID("578f2baa12eaebabec4af28d"),"Name":"Rogede sild","Discontinued":false}, 45 | {"_id": new ObjectID("578f2b8c12eaebabec4af266"),"QuantityPerUnit":"4 - 450 g glasses","UnitPrice":12.0,"CategoryId": new ObjectID("578f2baa12eaebabec4af28d"),"Name":"Spegesild","Discontinued":false}, 46 | {"_id": new ObjectID("578f2b8c12eaebabec4af267"),"QuantityPerUnit":"10 - 4 oz boxes","UnitPrice":9.5,"CategoryId": new ObjectID("578f2baa12eaebabec4af290"),"Name":"Zaanse koeken","Discontinued":false}, 47 | {"_id": new ObjectID("578f2b8c12eaebabec4af268"),"QuantityPerUnit":"100 - 100 g pieces","UnitPrice":43.9,"CategoryId": new ObjectID("578f2baa12eaebabec4af290"),"Name":"Schoggi Schokolade","Discontinued":false}, 48 | {"_id": new ObjectID("578f2b8c12eaebabec4af269"),"QuantityPerUnit":"25 - 825 g cans","UnitPrice":45.6,"CategoryId": new ObjectID("578f2baa12eaebabec4af28c"),"Name":"Rössle Sauerkraut","Discontinued":false}, 49 | {"_id": new ObjectID("578f2b8c12eaebabec4af26a"),"QuantityPerUnit":"50 bags x 30 sausgs.","UnitPrice":123.79,"CategoryId": new ObjectID("578f2baa12eaebabec4af28b"),"Name":"Thüringer Rostbratwurst","Discontinued":true}, 50 | {"_id": new ObjectID("578f2b8c12eaebabec4af26b"),"QuantityPerUnit":"10 pkgs.","UnitPrice":12.75,"CategoryId": new ObjectID("578f2baa12eaebabec4af290"),"Name":"Chocolade","Discontinued":false}, 51 | {"_id": new ObjectID("578f2b8c12eaebabec4af26c"),"QuantityPerUnit":"24 - 50 g pkgs.","UnitPrice":20.0,"CategoryId": new ObjectID("578f2baa12eaebabec4af290"),"Name":"Maxilaku","Discontinued":false}, 52 | {"_id": new ObjectID("578f2b8c12eaebabec4af26d"),"QuantityPerUnit":"12 - 100 g bars","UnitPrice":16.25,"CategoryId": new ObjectID("578f2baa12eaebabec4af290"),"Name":"Valkoinen suklaa","Discontinued":true}, 53 | {"_id": new ObjectID("578f2b8c12eaebabec4af26e"),"QuantityPerUnit":"50 - 300 g pkgs.","UnitPrice":53.0,"CategoryId": new ObjectID("578f2baa12eaebabec4af28c"),"Name":"Manjimup Dried Apples","Discontinued":true}, 54 | {"_id": new ObjectID("578f2b8c12eaebabec4af26f"),"QuantityPerUnit":"16 - 2 kg boxes","UnitPrice":7.0,"CategoryId": new ObjectID("578f2baa12eaebabec4af28a"),"Name":"Filo Mix","Discontinued":false}, 55 | {"_id": new ObjectID("578f2b8c12eaebabec4af270"),"QuantityPerUnit":"24 - 250 g pkgs.","UnitPrice":38.0,"CategoryId": new ObjectID("578f2baa12eaebabec4af28a"),"Name":"Gnocchi di nonna Alice","Discontinued":true}, 56 | {"_id": new ObjectID("578f2b8c12eaebabec4af271"),"QuantityPerUnit":"24 - 250 g pkgs.","UnitPrice":19.5,"CategoryId": new ObjectID("578f2baa12eaebabec4af28a"),"Name":"Ravioli Angelo","Discontinued":false}, 57 | {"_id": new ObjectID("578f2b8c12eaebabec4af272"),"QuantityPerUnit":"24 pieces","UnitPrice":13.25,"CategoryId": new ObjectID("578f2baa12eaebabec4af28d"),"Name":"Escargots de Bourgogne","Discontinued":false}, 58 | {"_id": new ObjectID("578f2b8c12eaebabec4af273"),"QuantityPerUnit":"5 kg pkg.","UnitPrice":55.0,"CategoryId": new ObjectID("578f2baa12eaebabec4af28f"),"Name":"Raclette Courdavault","Discontinued":false}, 59 | {"_id": new ObjectID("578f2b8c12eaebabec4af274"),"QuantityPerUnit":"15 - 300 g rounds","UnitPrice":34.0,"CategoryId": new ObjectID("578f2baa12eaebabec4af28f"),"Name":"Camembert Pierrot","Discontinued":true}, 60 | {"_id": new ObjectID("578f2b8c12eaebabec4af275"),"QuantityPerUnit":"24 - 500 ml bottles","UnitPrice":28.5,"CategoryId": new ObjectID("578f2baa12eaebabec4af28e"),"Name":"Sirop d'érable","Discontinued":true}, 61 | {"_id": new ObjectID("578f2b8c12eaebabec4af276"),"QuantityPerUnit":"48 pies","UnitPrice":49.3,"CategoryId": new ObjectID("578f2baa12eaebabec4af290"),"Name":"Tarte au sucre","Discontinued":false}, 62 | {"_id": new ObjectID("578f2b8c12eaebabec4af277"),"QuantityPerUnit":"15 - 625 g jars","UnitPrice":43.9,"CategoryId": new ObjectID("578f2baa12eaebabec4af28e"),"Name":"Vegie-spread","Discontinued":false}, 63 | {"_id": new ObjectID("578f2b8c12eaebabec4af278"),"QuantityPerUnit":"20 bags x 4 pieces","UnitPrice":33.25,"CategoryId": new ObjectID("578f2baa12eaebabec4af28a"),"Name":"Wimmers gute Semmelknödel","Discontinued":true}, 64 | {"_id": new ObjectID("578f2b8c12eaebabec4af279"),"QuantityPerUnit":"32 - 8 oz bottles","UnitPrice":21.05,"CategoryId": new ObjectID("578f2baa12eaebabec4af28e"),"Name":"Louisiana Fiery Hot Pepper Sauce","Discontinued":true}, 65 | {"_id": new ObjectID("578f2b8c12eaebabec4af27a"),"QuantityPerUnit":"24 - 8 oz jars","UnitPrice":17.0,"CategoryId": new ObjectID("578f2baa12eaebabec4af28e"),"Name":"Louisiana Hot Spiced Okra","Discontinued":false}, 66 | {"_id": new ObjectID("578f2b8c12eaebabec4af27b"),"QuantityPerUnit":"24 - 12 oz bottles","UnitPrice":14.0,"CategoryId": new ObjectID("578f2baa12eaebabec4af289"),"Name":"Laughing Lumberjack Lager","Discontinued":true}, 67 | {"_id": new ObjectID("578f2b8c12eaebabec4af27c"),"QuantityPerUnit":"10 boxes x 8 pieces","UnitPrice":12.5,"CategoryId": new ObjectID("578f2baa12eaebabec4af290"),"Name":"Scottish Longbreads","Discontinued":false}, 68 | {"_id": new ObjectID("578f2b8c12eaebabec4af27d"),"QuantityPerUnit":"Crate","UnitPrice":666,"CategoryId": new ObjectID("578f2baa12eaebabec4af28f"),"Name":"MyProduct","Discontinued":true}, 69 | {"_id": new ObjectID("578f2b8c12eaebabec4af27e"),"QuantityPerUnit":"24 - 355 ml bottles","UnitPrice":15.0,"CategoryId": new ObjectID("578f2baa12eaebabec4af289"),"Name":"Outback Lager","Discontinued":false}, 70 | {"_id": new ObjectID("578f2b8c12eaebabec4af27f"),"QuantityPerUnit":"10 - 500 g pkgs.","UnitPrice":21.5,"CategoryId": new ObjectID("578f2baa12eaebabec4af28f"),"Name":"Flotemysost","Discontinued":false}, 71 | {"_id": new ObjectID("578f2b8c12eaebabec4af280"),"QuantityPerUnit":"24 - 200 g pkgs.","UnitPrice":34.8,"CategoryId": new ObjectID("578f2baa12eaebabec4af28f"),"Name":"Mozzarella di Giovanni","Discontinued":false}, 72 | {"_id": new ObjectID("578f2b8c12eaebabec4af281"),"QuantityPerUnit":"24 - 150 g jars","UnitPrice":15.0,"CategoryId": new ObjectID("578f2baa12eaebabec4af28d"),"Name":"Röd Kaviar","Discontinued":false}, 73 | {"_id": new ObjectID("578f2b8c12eaebabec4af282"),"QuantityPerUnit":"48 pieces","UnitPrice":32.8,"CategoryId": new ObjectID("578f2baa12eaebabec4af28b"),"Name":"Perth Pasties","Discontinued":true}, 74 | {"_id": new ObjectID("578f2b8c12eaebabec4af283"),"QuantityPerUnit":"16 pies","UnitPrice":7.45,"CategoryId": new ObjectID("578f2baa12eaebabec4af28b"),"Name":"Tourtière","Discontinued":true}, 75 | {"_id": new ObjectID("578f2b8c12eaebabec4af284"),"QuantityPerUnit":"24 boxes x 2 pies","UnitPrice":24.0,"CategoryId": new ObjectID("578f2baa12eaebabec4af28b"),"Name":"Pâté chinois","Discontinued":true}, 76 | {"_id": new ObjectID("578f2b8c12eaebabec4af285"),"QuantityPerUnit":"5 kg pkg.","UnitPrice":10.0,"CategoryId": new ObjectID("578f2baa12eaebabec4af28c"),"Name":"Longlife Tofu","Discontinued":false}, 77 | {"_id": new ObjectID("578f2b8c12eaebabec4af286"),"QuantityPerUnit":"24 - 0.5 l bottles","UnitPrice":7.75,"CategoryId": new ObjectID("578f2baa12eaebabec4af289"),"Name":"Rhönbräu Klosterbier","Discontinued":true}, 78 | {"_id": new ObjectID("578f2b8c12eaebabec4af287"),"QuantityPerUnit":"500 ml","UnitPrice":18.0,"CategoryId": new ObjectID("578f2baa12eaebabec4af289"),"Name":"Lakkalikööri","Discontinued":false}, 79 | {"_id": new ObjectID("578f2b8c12eaebabec4af288"),"QuantityPerUnit":"12 boxes","UnitPrice":13.0,"CategoryId": new ObjectID("578f2baa12eaebabec4af28e"),"Name":"Original Frankfurter grüne Soße","Discontinued":false} 80 | ]; -------------------------------------------------------------------------------- /src/example/schema.ts: -------------------------------------------------------------------------------- 1 | export = { 2 | "version": "4.0", 3 | "dataServices": { 4 | "schema": [ 5 | { 6 | "namespace": "Northwind", 7 | "entityType": [ 8 | { 9 | "name": "Product", 10 | "key": [ 11 | { 12 | "propertyRef": [ 13 | { 14 | "name": "_id" 15 | } 16 | ] 17 | } 18 | ], 19 | "property": [ 20 | { 21 | "name": "QuantityPerUnit", 22 | "type": "Edm.String", 23 | "nullable": "false" 24 | }, 25 | { 26 | "name": "UnitPrice", 27 | "type": "Edm.Decimal", 28 | "nullable": "false" 29 | }, 30 | { 31 | "name": "_id", 32 | "type": "Edm.String", 33 | "nullable": "false" 34 | }, 35 | { 36 | "name": "Name", 37 | "type": "Edm.String", 38 | "nullable": "false" 39 | }, 40 | { 41 | "name": "CategoryId", 42 | "type": "Edm.String", 43 | "nullable": "false" 44 | }, 45 | { 46 | "name": "Discontinued", 47 | "type": "Edm.Boolean", 48 | "nullable": "false" 49 | } 50 | ] 51 | }, 52 | { 53 | "name": "Category", 54 | "key": [ 55 | { 56 | "propertyRef": [ 57 | { 58 | "name": "_id" 59 | } 60 | ] 61 | } 62 | ], 63 | "property": [ 64 | { 65 | "name": "Description", 66 | "type": "Edm.String", 67 | "nullable": "false" 68 | }, 69 | { 70 | "name": "_id", 71 | "type": "Edm.String", 72 | "nullable": "false" 73 | }, 74 | { 75 | "name": "Name", 76 | "type": "Edm.String", 77 | "nullable": "false" 78 | } 79 | ] 80 | } 81 | ], 82 | "annotations": [ 83 | { 84 | "target": "Northwind.Product/_id", 85 | "annotation": [ 86 | { 87 | "term": "UI.DisplayName", 88 | "string": "Product identifier" 89 | }, 90 | { 91 | "term": "UI.ControlHint", 92 | "string": "ReadOnly" 93 | } 94 | ] 95 | }, 96 | { 97 | "target": "Northwind.Category/_id", 98 | "annotation": [ 99 | { 100 | "term": "UI.DisplayName", 101 | "string": "Category identifier" 102 | }, 103 | { 104 | "term": "UI.ControlHint", 105 | "string": "ReadOnly" 106 | } 107 | ] 108 | }, 109 | { 110 | "target": "Northwind.Category", 111 | "annotation": [ 112 | { 113 | "term": "UI.DisplayName", 114 | "string": "Categories" 115 | } 116 | ] 117 | }, 118 | { 119 | "target": "Northwind.Category/Name", 120 | "annotation": [ 121 | { 122 | "term": "UI.DisplayName", 123 | "string": "Category name" 124 | }, 125 | { 126 | "term": "UI.ControlHint", 127 | "string": "ShortText" 128 | } 129 | ] 130 | }, 131 | { 132 | "target": "Northwind.Product", 133 | "annotation": [ 134 | { 135 | "term": "UI.DisplayName", 136 | "string": "Products" 137 | } 138 | ] 139 | }, 140 | { 141 | "target": "Northwind.Product/Name", 142 | "annotation": [ 143 | { 144 | "term": "UI.DisplayName", 145 | "string": "Product title" 146 | }, 147 | { 148 | "term": "UI.ControlHint", 149 | "string": "ShortText" 150 | } 151 | ] 152 | }, 153 | { 154 | "target": "Northwind.Product/QuantityPerUnit", 155 | "annotation": [ 156 | { 157 | "term": "UI.DisplayName", 158 | "string": "Product English name" 159 | }, 160 | { 161 | "term": "UI.ControlHint", 162 | "string": "ShortText" 163 | } 164 | ] 165 | }, 166 | { 167 | "target": "Northwind.Product/UnitPrice", 168 | "annotation": [ 169 | { 170 | "term": "UI.DisplayName", 171 | "string": "Unit price of product" 172 | }, 173 | { 174 | "term": "UI.ControlHint", 175 | "string": "Decimal" 176 | } 177 | ] 178 | }, 179 | ] 180 | }, 181 | { 182 | "namespace": "JayStack", 183 | "action": { 184 | "name": "initDb" 185 | }, 186 | "entityContainer": { 187 | "name": "NorthwindContext", 188 | "entitySet": [{ 189 | "name": "Products", 190 | "entityType": "Northwind.Product" 191 | }, 192 | { 193 | "name": "Categories", 194 | "entityType": "Northwind.Category" 195 | }], 196 | "actionImport": { 197 | "name": "initDb", 198 | "action": "JayStack.initDb" 199 | } 200 | } 201 | } 202 | ] 203 | } 204 | }; -------------------------------------------------------------------------------- /src/example/simple.ts: -------------------------------------------------------------------------------- 1 | import { ObjectID } from "mongodb"; 2 | import { createFilter } from "odata-v4-inmemory"; 3 | import { odata, ODataController, ODataServer, ODataQuery } from "../lib/index"; 4 | let categories = require("./categories").map((category) => { 5 | category._id = category._id.toString(); 6 | return category; 7 | }); 8 | let products = require("./products").map((product) => { 9 | product._id = product._id.toString(); 10 | product.CategoryId = product.CategoryId.toString(); 11 | return product; 12 | }); 13 | 14 | export class ProductsController extends ODataController{ 15 | @odata.GET 16 | find(@odata.filter filter:ODataQuery){ 17 | if (filter) return products.filter(createFilter(filter)); 18 | return products; 19 | } 20 | 21 | @odata.GET 22 | findOne(@odata.key key:string){ 23 | return products.filter(product => product._id == key)[0]; 24 | } 25 | 26 | @odata.POST 27 | insert(@odata.body product:any){ 28 | product._id = new ObjectID().toString(); 29 | products.push(product); 30 | return product; 31 | } 32 | 33 | @odata.PATCH 34 | update(@odata.key key:string, @odata.body delta:any){ 35 | let product = products.filter(product => product._id == key)[0]; 36 | for (let prop in delta){ 37 | product[prop] = delta[prop]; 38 | } 39 | } 40 | 41 | @odata.DELETE 42 | remove(@odata.key key:string){ 43 | products.splice(products.indexOf(products.filter(product => product._id == key)[0]), 1); 44 | } 45 | } 46 | 47 | export class CategoriesController extends ODataController{ 48 | @odata.GET 49 | find(@odata.filter filter:ODataQuery){ 50 | if (filter) return categories.filter(createFilter(filter)); 51 | return categories; 52 | } 53 | 54 | @odata.GET 55 | findOne(@odata.key key:string){ 56 | return categories.filter(category => category._id == key)[0]; 57 | } 58 | 59 | @odata.POST 60 | insert(@odata.body category:any){ 61 | category._id = new ObjectID().toString(); 62 | categories.push(category); 63 | return category; 64 | } 65 | 66 | @odata.PATCH 67 | update(@odata.key key:string, @odata.body delta:any){ 68 | let category = categories.filter(category => category._id == key)[0]; 69 | for (let prop in delta){ 70 | category[prop] = delta[prop]; 71 | } 72 | } 73 | 74 | @odata.DELETE 75 | remove(@odata.key key:string){ 76 | categories.splice(categories.indexOf(categories.filter(category => category._id == key)[0]), 1); 77 | } 78 | } 79 | 80 | @odata.cors 81 | @odata.controller(ProductsController, true) 82 | @odata.controller(CategoriesController, true) 83 | export class NorthwindODataServer extends ODataServer{} 84 | NorthwindODataServer.create("/odata", 3000); -------------------------------------------------------------------------------- /src/example/stream.ts: -------------------------------------------------------------------------------- 1 | import * as fs from "fs"; 2 | import * as path from "path"; 3 | import { Writable } from "stream"; 4 | import { MongoClient, Db, ObjectID } from "mongodb"; 5 | import { createQuery, createFilter } from "odata-v4-mongodb"; 6 | import { Readable, PassThrough } from "stream"; 7 | import { ODataServer, ODataController, Edm, odata, ODataStream, ODataQuery, ODataHttpContext } from "../lib"; 8 | import { Category, Product } from "./model"; 9 | import { createMetadataJSON } from "../lib/metadata"; 10 | 11 | const mongodb = async function():Promise{ 12 | return (await MongoClient.connect("mongodb://localhost:27017/odataserver")).db(); 13 | }; 14 | 15 | const delay = async function(ms:number):Promise{ 16 | return new Promise(resolve => setTimeout(resolve, ms)); 17 | }; 18 | 19 | @odata.type(Product) 20 | class ProductsController extends ODataController{ 21 | /*@odata.GET 22 | *find(@odata.query query:ODataQuery, @odata.stream stream:Writable):any{ 23 | let db:Db = yield mongodb(); 24 | let mongodbQuery = createQuery(query); 25 | if (typeof mongodbQuery.query._id == "string") mongodbQuery.query._id = new ObjectID(mongodbQuery.query._id); 26 | if (typeof mongodbQuery.query.CategoryId == "string") mongodbQuery.query.CategoryId = new ObjectID(mongodbQuery.query.CategoryId); 27 | return db.collection("Products") 28 | .find( 29 | mongodbQuery.query, 30 | mongodbQuery.projection, 31 | mongodbQuery.skip, 32 | mongodbQuery.limit 33 | ).stream().pipe(stream); 34 | }*/ 35 | // example using generator with mongodb .next() and passing entity data into OData stream 36 | @odata.GET 37 | *find(@odata.query query:ODataQuery, @odata.stream stream:Writable){ 38 | let db:Db = yield mongodb(); 39 | let mongodbQuery = createQuery(query); 40 | if (typeof mongodbQuery.query._id == "string") mongodbQuery.query._id = new ObjectID(mongodbQuery.query._id); 41 | if (typeof mongodbQuery.query.CategoryId == "string") mongodbQuery.query.CategoryId = new ObjectID(mongodbQuery.query.CategoryId); 42 | let cursor = db.collection("Products") 43 | .find( 44 | mongodbQuery.query, { 45 | projection: mongodbQuery.projection, 46 | skip: mongodbQuery.skip, 47 | limit: mongodbQuery.limit 48 | } 49 | ); 50 | let item = yield cursor.next(); 51 | while (item){ 52 | stream.write(item); 53 | item = yield cursor.next(); 54 | } 55 | stream.end(); 56 | } 57 | 58 | @odata.GET 59 | *findOne(@odata.key() key:string, @odata.query query:ODataQuery){ 60 | let db:Db = yield mongodb(); 61 | let mongodbQuery = createQuery(query); 62 | return db.collection("Products").findOne({ _id: new ObjectID(key) }, { 63 | fields: mongodbQuery.projection 64 | }); 65 | } 66 | 67 | @odata.POST 68 | async insert(@odata.body data:any){ 69 | let db = await mongodb(); 70 | if (data.CategoryId) data.CategoryId = new ObjectID(data.CategoryId); 71 | return await db.collection("Products").insert(data).then((result) => { 72 | data._id = result.insertedId; 73 | return data; 74 | }); 75 | } 76 | } 77 | 78 | @odata.type(Category) 79 | class CategoriesController extends ODataController{ 80 | @odata.GET 81 | *find(@odata.query query:ODataQuery):any{ 82 | let db:Db = yield mongodb(); 83 | let mongodbQuery = createQuery(query); 84 | if (typeof mongodbQuery.query._id == "string") mongodbQuery.query._id = new ObjectID(mongodbQuery.query._id); 85 | let cursor = db.collection("Categories") 86 | .find( 87 | mongodbQuery.query, { 88 | projection: mongodbQuery.projection, 89 | skip: mongodbQuery.skip, 90 | limit: mongodbQuery.limit 91 | } 92 | ); 93 | let result = yield cursor.toArray(); 94 | result.inlinecount = yield cursor.count(false); 95 | return result; 96 | } 97 | 98 | @odata.GET 99 | *findOne(@odata.key() key:string, @odata.query query:ODataQuery){ 100 | let db:Db = yield mongodb(); 101 | let mongodbQuery = createQuery(query); 102 | return db.collection("Categories").findOne({ _id: new ObjectID(key) }, { 103 | fields: mongodbQuery.projection 104 | }); 105 | } 106 | } 107 | 108 | enum Genre{ 109 | Unknown, 110 | Pop, 111 | Rock, 112 | Metal, 113 | Classic 114 | } 115 | 116 | @Edm.MediaEntity("audio/mp3") 117 | class Music extends PassThrough{ 118 | @Edm.Key 119 | @Edm.Computed 120 | @Edm.TypeDefinition(ObjectID) 121 | //@Edm.Int32 122 | Id:ObjectID 123 | 124 | @Edm.String 125 | Artist:string 126 | 127 | @Edm.String 128 | Title:string 129 | 130 | @Edm.EnumType(Genre) 131 | Genre:Genre 132 | 133 | @Edm.TypeDefinition(ObjectID) 134 | uid:ObjectID 135 | } 136 | 137 | @odata.namespace("NorthwindTypes") 138 | class NorthwindTypes extends Edm.ContainerBase{ 139 | @Edm.Flags 140 | @Edm.Int64 141 | @Edm.Serialize(value => `NorthwindTypes.Genre2'${value}'`) 142 | Genre2 = Genre 143 | 144 | @Edm.String 145 | @Edm.URLDeserialize((value:string) => new Promise(resolve => setTimeout(_ => resolve(new ObjectID(value)), 1000))) 146 | @Edm.Deserialize(value => new ObjectID(value)) 147 | ObjectID2 = ObjectID 148 | 149 | Music2 = Music 150 | } 151 | 152 | @odata.type(Music) 153 | @odata.container("Media") 154 | class MusicController extends ODataController{ 155 | @odata.GET 156 | find(@odata.filter filter:ODataQuery, @odata.query query:ODataQuery){ 157 | console.log(JSON.stringify(createQuery(query).query, null, 2), JSON.stringify(createFilter(filter), null, 2)); 158 | let music = new Music(); 159 | music.Id = new ObjectID; 160 | music.Artist = "Dream Theater"; 161 | music.Title = "Six degrees of inner turbulence"; 162 | music.Genre = Genre.Metal; 163 | music.uid = new ObjectID(); 164 | return [music]; 165 | } 166 | 167 | @odata.GET 168 | findOne(@odata.key() _:number){ 169 | let music = new Music(); 170 | music.Id = new ObjectID; 171 | music.Artist = "Dream Theater"; 172 | music.Title = "Six degrees of inner turbulence"; 173 | music.Genre = Genre.Metal; 174 | music.uid = new ObjectID(); 175 | return music; 176 | } 177 | 178 | @odata.POST 179 | insert(@odata.body body: Music){ 180 | body.Id = new ObjectID(); 181 | console.log(body); 182 | return body; 183 | } 184 | 185 | @odata.GET.$value 186 | mp3(@odata.key _:number, @odata.context context:ODataHttpContext){ 187 | let file = fs.createReadStream("tmp.mp3"); 188 | return new Promise((resolve, reject) => { 189 | file.on("open", () => { 190 | context.response.on("finish", () => { 191 | file.close(); 192 | }); 193 | resolve(file); 194 | }).on("error", reject); 195 | }); 196 | } 197 | 198 | @odata.POST.$value 199 | post(@odata.key _:number, @odata.body upload:Readable){ 200 | let file = fs.createWriteStream("tmp.mp3"); 201 | return new Promise((resolve, reject) => { 202 | file.on('open', () => { 203 | upload.pipe(file); 204 | }).on('error', reject); 205 | upload.on('end', resolve); 206 | }); 207 | } 208 | } 209 | 210 | class ImageMember{ 211 | @Edm.String 212 | value:string 213 | } 214 | 215 | @Edm.OpenType 216 | class Image{ 217 | @Edm.Key 218 | @Edm.Computed 219 | @Edm.Int32 220 | Id:number 221 | 222 | @Edm.String 223 | Filename:string 224 | 225 | @Edm.Collection(Edm.ComplexType(ImageMember)) 226 | Members:ImageMember[] 227 | 228 | @Edm.Stream("image/png") 229 | Data:ODataStream 230 | 231 | @Edm.Stream("image/png") 232 | Data2:ODataStream 233 | } 234 | 235 | @odata.type(Image) 236 | @odata.container("Media") 237 | class ImagesController extends ODataController{ 238 | @odata.GET 239 | images(@odata.key id:number){ 240 | let image = new Image(); 241 | image.Id = id; 242 | image.Filename = "tmp.png"; 243 | (image).mm = [[1,2],[3,4]]; 244 | return image; 245 | } 246 | 247 | @odata.GET("Members") 248 | *getMembers(@odata.key _:number, @odata.stream stream:Writable){ 249 | for (let i = 0; i < 10; i++){ 250 | stream.write({ value: `Member #${i}` }); 251 | yield delay(1); 252 | } 253 | stream.end(); 254 | } 255 | 256 | @odata.GET("Data") 257 | @odata.GET("Data2").$value 258 | getData(@odata.key _:number, @odata.context context:ODataHttpContext, @odata.result result: Image){ 259 | return new ODataStream(fs.createReadStream(result.Filename)).pipe(context.response); 260 | } 261 | 262 | @odata.POST("Data") 263 | @odata.POST("Data2").$value 264 | postData(@odata.key _:number, @odata.body data:Readable, @odata.result result: Image){ 265 | return new ODataStream(fs.createWriteStream(result.Filename)).write(data); 266 | } 267 | } 268 | 269 | @Edm.OpenType 270 | class PlainObject{} 271 | 272 | @Edm.Container(NorthwindTypes) 273 | @odata.controller(ProductsController, true) 274 | @odata.controller(CategoriesController, true) 275 | @odata.controller(MusicController, true) 276 | @odata.controller(ImagesController, true) 277 | class StreamServer extends ODataServer{ 278 | @Edm.TypeDefinition(ObjectID) 279 | @Edm.FunctionImport 280 | objid(@Edm.TypeDefinition(ObjectID) v:ObjectID){ 281 | return v.toHexString(); 282 | } 283 | 284 | @Edm.FunctionImport(Edm.String) 285 | stringify(@Edm.EntityType(PlainObject) obj:any):string { 286 | return JSON.stringify(obj); 287 | } 288 | 289 | @odata.container("almafa") 290 | @Edm.FunctionImport(Edm.Stream) 291 | async Fetch(@Edm.String filename:string, @odata.stream stream:Writable, @odata.context context:any){ 292 | let file = fs.createReadStream(filename); 293 | return file.on("open", () => { 294 | context.response.contentType(path.extname(filename)); 295 | file.pipe(stream); 296 | }); 297 | } 298 | } 299 | //console.dir(createMetadataJSON(StreamServer).dataServices.schema[0]["function"][1].parameter); 300 | //console.log(createMetadataJSON(StreamServer).dataServices.schema[0].entityType[2]); 301 | //console.log(StreamServer.$metadata().edmx.dataServices.schemas[0].typeDefinitions); 302 | StreamServer.create("/odata", 3000); -------------------------------------------------------------------------------- /src/example/test.ts: -------------------------------------------------------------------------------- 1 | import * as es from "event-stream"; 2 | import { ODataServer, ODataController, odata } from "../lib/index"; 3 | 4 | class TestController extends ODataController{ 5 | @odata.GET 6 | find(){ 7 | return [{ 8 | a: 1 9 | }, { 10 | b: 2 11 | }, { 12 | t: Date.now() 13 | }]; 14 | } 15 | 16 | @odata.GET 17 | findOne(@odata.key key:number){ 18 | return { 19 | id: key, 20 | t: Date.now() 21 | }; 22 | } 23 | } 24 | 25 | @odata.controller(TestController, true) 26 | export class TestServer extends ODataServer{} 27 | 28 | TestServer.execute("/Test/$count", "GET").then((result) => { 29 | console.log(result); 30 | }); 31 | 32 | let server = new TestServer(); 33 | server.pipe(es.mapSync(data => console.log(data))); 34 | 35 | setInterval(() => { 36 | server.write({ 37 | url: "/Test(" + Math.floor(Math.random() * 1000) + ")", 38 | method: "GET" 39 | }); 40 | }, 1000); -------------------------------------------------------------------------------- /src/lib/controller.ts: -------------------------------------------------------------------------------- 1 | import { Token } from "odata-v4-parser/lib/lexer"; 2 | import * as odata from "./odata"; 3 | import { ODataBase } from "./odata"; 4 | import { getFunctionParameters } from "./utils"; 5 | 6 | export class ODataControllerBase{ 7 | entitySetName:string 8 | elementType:Function 9 | static containerName:string 10 | static validator:(odataQuery:string | Token) => null; 11 | 12 | static on(method:string, fn:Function | string, ...keys:string[]){ 13 | let fnName = ((fn).name || fn); 14 | odata.method(method)(this.prototype, fnName); 15 | if (keys && keys.length > 0){ 16 | fn = this.prototype[fnName]; 17 | let parameterNames = getFunctionParameters(fn); 18 | keys.forEach((key) => { 19 | odata.key(this.prototype, fnName, parameterNames.indexOf(key)); 20 | }); 21 | } 22 | } 23 | 24 | /** Enables the filtering 25 | * @param fn 26 | * @param param 27 | */ 28 | static enableFilter(fn:Function | string, param?:string){ 29 | let fnName = ((fn).name || fn); 30 | fn = this.prototype[fnName]; 31 | let parameterNames = getFunctionParameters(fn); 32 | odata.filter(this.prototype, fnName, parameterNames.indexOf(param || parameterNames[0])); 33 | } 34 | } 35 | export class ODataController extends ODataBase(ODataControllerBase){} -------------------------------------------------------------------------------- /src/lib/error.ts: -------------------------------------------------------------------------------- 1 | export class CustomError extends Error{ 2 | constructor(message?:string){ 3 | super(message); 4 | this.message = message; 5 | this.name = (this as any).constructor.name; 6 | Error.captureStackTrace(this, CustomError); 7 | } 8 | } 9 | 10 | export class HttpRequestError extends CustomError{ 11 | statusCode:number 12 | constructor(statusCode:number, message:string){ 13 | super(message); 14 | this.statusCode = statusCode; 15 | } 16 | } 17 | 18 | export class NotImplementedError extends HttpRequestError{ 19 | static MESSAGE:string = "Not implemented."; 20 | constructor(){ 21 | super(501, NotImplementedError.MESSAGE); 22 | } 23 | } 24 | 25 | export class ResourceNotFoundError extends HttpRequestError{ 26 | static MESSAGE:string = "Resource not found."; 27 | constructor(){ 28 | super(404, ResourceNotFoundError.MESSAGE); 29 | } 30 | } 31 | 32 | export class MethodNotAllowedError extends HttpRequestError{ 33 | static MESSAGE:string = "Method not allowed."; 34 | constructor(){ 35 | super(405, MethodNotAllowedError.MESSAGE); 36 | } 37 | } 38 | 39 | export class UnsupportedMediaTypeError extends HttpRequestError{ 40 | static MESSAGE:string = "Unsupported media type."; 41 | constructor(){ 42 | super(415, UnsupportedMediaTypeError.MESSAGE); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/lib/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @exports Edm decorator system 3 | */ 4 | export * from "./edm"; 5 | import * as _Edm from "./edm"; 6 | export const Edm = _Edm; 7 | export * from "./odata"; 8 | import * as _odata from "./odata"; 9 | export const odata = _odata; 10 | export * from "./controller"; 11 | export * from "./processor"; 12 | export * from "./server"; 13 | export * from "./metadata"; 14 | export * from "./result"; 15 | export * from "./visitor"; 16 | export * from "./error"; 17 | export { Token as ODataQuery } from "odata-v4-parser/lib/lexer"; -------------------------------------------------------------------------------- /src/lib/result.ts: -------------------------------------------------------------------------------- 1 | import { Readable, Writable } from "stream"; 2 | 3 | export class ODataStream{ 4 | stream:any 5 | contentType:string 6 | 7 | constructor(contentType:string); 8 | constructor(stream:any, contentType?:string); 9 | constructor(stream:any, contentType?:string){ 10 | if (typeof stream == "string"){ 11 | this.stream = null; 12 | this.contentType = stream; 13 | }else{ 14 | this.stream = stream; 15 | this.contentType = contentType; 16 | } 17 | this.contentType = this.contentType || "application/octet-stream"; 18 | } 19 | 20 | pipe(destination:Writable):Promise{ 21 | return new Promise((resolve, reject) => { 22 | this.stream.on("open", () => { 23 | if (typeof this.stream.close == "function"){ 24 | destination.on("finish", () => { 25 | this.stream.close(); 26 | }); 27 | } 28 | resolve(this); 29 | }).on("error", reject); 30 | }); 31 | } 32 | 33 | write(source:Readable):Promise{ 34 | return new Promise((resolve, reject) => { 35 | this.stream.on("open", () => { 36 | if (typeof this.stream.close == "function"){ 37 | source.on("finish", () => { 38 | this.stream.close(); 39 | }); 40 | } 41 | source.pipe(this.stream); 42 | }).on("error", reject); 43 | source.on("end", () => resolve(this)); 44 | }); 45 | } 46 | } 47 | 48 | export interface IODataResult{ 49 | "@odata.context"?:string 50 | "@odata.count"?:number 51 | value?:T[] 52 | [x: string]:any 53 | } 54 | 55 | export class ODataResult{ 56 | statusCode:number 57 | body:IODataResult & T 58 | elementType:Function 59 | contentType:string 60 | stream?:any 61 | 62 | constructor(statusCode:number, contentType?:string, result?:any){ 63 | this.statusCode = statusCode; 64 | if (typeof result != "undefined"){ 65 | this.body = result; 66 | if (result && result.constructor) this.elementType = result.constructor; 67 | this.contentType = contentType || "application/json"; 68 | } 69 | } 70 | 71 | static Created = function Created(result:any, contentType?:string):Promise{ 72 | if (result && typeof result.then == 'function'){ 73 | return result.then((result) => { 74 | return new ODataResult(201, contentType, result); 75 | }); 76 | }else{ 77 | return Promise.resolve(new ODataResult(201, contentType, result)); 78 | } 79 | } 80 | 81 | static Ok = function Ok(result:any, contentType?:string):Promise{ 82 | let inlinecount; 83 | if (result && typeof result.then == 'function'){ 84 | return result.then((result) => { 85 | if (result && Array.isArray(result)){ 86 | if (result && (result).inlinecount && typeof (result).inlinecount == "number"){ 87 | inlinecount = (result).inlinecount; 88 | delete (result).inlinecount; 89 | } 90 | result = { value: result }; 91 | if (typeof inlinecount != "undefined") result["@odata.count"] = inlinecount; 92 | }else{ 93 | if (typeof result == "object" && result && typeof inlinecount == "number"){ 94 | result["@odata.count"] = inlinecount; 95 | } 96 | } 97 | return new ODataResult(200, contentType, result); 98 | }); 99 | }else{ 100 | if (result && Array.isArray(result)){ 101 | if (result && (result).inlinecount && typeof (result).inlinecount == "number"){ 102 | inlinecount = (result).inlinecount; 103 | delete (result).inlinecount; 104 | } 105 | result = { value: result }; 106 | if (typeof inlinecount == "number") result["@odata.count"] = inlinecount; 107 | }else{ 108 | if (typeof result == "object" && result && typeof inlinecount == "number"){ 109 | result["@odata.count"] = inlinecount; 110 | } 111 | } 112 | return Promise.resolve(new ODataResult(200, contentType, result)); 113 | } 114 | }; 115 | 116 | static NoContent = function NoContent(result?:any, contentType?:string):Promise{ 117 | if (result && typeof result.then == 'function'){ 118 | return result.then(_ => new ODataResult(204, contentType)); 119 | }else{ 120 | return Promise.resolve(new ODataResult(204, contentType)); 121 | } 122 | } 123 | } -------------------------------------------------------------------------------- /src/lib/server.ts: -------------------------------------------------------------------------------- 1 | import { ServiceMetadata } from "odata-v4-service-metadata"; 2 | import { ServiceDocument } from "odata-v4-service-document"; 3 | import { Edm as Metadata } from "odata-v4-metadata"; 4 | import * as ODataParser from "odata-v4-parser"; 5 | import { Token, TokenType } from "odata-v4-parser/lib/lexer"; 6 | import * as express from "express"; 7 | import * as http from "http"; 8 | import * as bodyParser from "body-parser"; 9 | import * as cors from "cors"; 10 | import { Transform, TransformOptions } from "stream"; 11 | import { ODataResult } from "./result"; 12 | import { ODataController } from "./controller"; 13 | import * as odata from "./odata"; 14 | import { ODataBase, IODataConnector } from "./odata"; 15 | import { createMetadataJSON } from "./metadata"; 16 | import { ODataProcessor, ODataProcessorOptions, ODataMetadataType } from "./processor"; 17 | import { HttpRequestError, UnsupportedMediaTypeError } from "./error"; 18 | import { ContainerBase } from "./edm"; 19 | import { Readable, Writable } from "stream"; 20 | 21 | /** HTTP context interface when using the server HTTP request handler */ 22 | export interface ODataHttpContext{ 23 | url:string 24 | method:string 25 | protocol:"http"|"https" 26 | host:string 27 | base:string 28 | request:express.Request & Readable 29 | response:express.Response & Writable 30 | } 31 | 32 | function ensureODataMetadataType(req, res){ 33 | let metadata:ODataMetadataType = ODataMetadataType.minimal; 34 | if (req.headers && req.headers.accept && req.headers.accept.indexOf("odata.metadata=") >= 0){ 35 | if (req.headers.accept.indexOf("odata.metadata=full") >= 0) metadata = ODataMetadataType.full; 36 | else if (req.headers.accept.indexOf("odata.metadata=none") >= 0) metadata = ODataMetadataType.none; 37 | } 38 | 39 | res["metadata"] = metadata; 40 | } 41 | function ensureODataContentType(req, res, contentType?){ 42 | contentType = contentType || "application/json"; 43 | if (contentType.indexOf("odata.metadata=") < 0) contentType += `;odata.metadata=${ODataMetadataType[res["metadata"]]}`; 44 | if (contentType.indexOf("odata.streaming=") < 0) contentType += ";odata.streaming=true"; 45 | if (contentType.indexOf("IEEE754Compatible=") < 0) contentType += ";IEEE754Compatible=false"; 46 | if (req.headers.accept && req.headers.accept.indexOf("charset") > 0){ 47 | contentType += `;charset=${res["charset"]}`; 48 | } 49 | res.contentType(contentType); 50 | } 51 | function ensureODataHeaders(req, res, next?){ 52 | res.setHeader("OData-Version", "4.0"); 53 | 54 | ensureODataMetadataType(req, res); 55 | let charset = req.headers["accept-charset"] || "utf-8"; 56 | res["charset"] = charset; 57 | ensureODataContentType(req, res); 58 | 59 | if ((req.headers.accept && req.headers.accept.indexOf("charset") < 0) || req.headers["accept-charset"]){ 60 | const bufferEncoding = { 61 | "utf-8": "utf8", 62 | "utf-16": "utf16le" 63 | }; 64 | let origsend = res.send; 65 | res.send = ((data) => { 66 | if (typeof data == "object") data = JSON.stringify(data); 67 | origsend.call(res, Buffer.from(data, bufferEncoding[charset])); 68 | }); 69 | } 70 | 71 | if (typeof next == "function") next(); 72 | } 73 | 74 | /** ODataServer base class to be extended by concrete OData Server data sources */ 75 | export class ODataServerBase extends Transform{ 76 | private static _metadataCache:any 77 | static namespace:string 78 | static container = new ContainerBase(); 79 | static parser = ODataParser; 80 | static connector:IODataConnector 81 | static validator:(odataQuery:string | Token) => null; 82 | static errorHandler:express.ErrorRequestHandler = ODataErrorHandler; 83 | private serverType:typeof ODataServer 84 | 85 | static requestHandler(){ 86 | return (req:express.Request, res:express.Response, next:express.NextFunction) => { 87 | try{ 88 | ensureODataHeaders(req, res); 89 | let processor = this.createProcessor({ 90 | url: req.url, 91 | method: req.method, 92 | protocol: req.secure ? "https" : "http", 93 | host: req.headers.host, 94 | base: req.baseUrl, 95 | request: req, 96 | response: res 97 | }, { 98 | metadata: res["metadata"] 99 | }); 100 | processor.on("header", (headers) => { 101 | for (let prop in headers){ 102 | if (prop.toLowerCase() == "content-type"){ 103 | ensureODataContentType(req, res, headers[prop]); 104 | }else{ 105 | res.setHeader(prop, headers[prop]); 106 | } 107 | } 108 | }); 109 | let hasError = false; 110 | processor.on("data", (chunk, encoding, done) => { 111 | if (!hasError){ 112 | res.write(chunk, encoding, done); 113 | } 114 | }); 115 | let body = req.body && Object.keys(req.body).length > 0 ? req.body : req; 116 | let origStatus = res.statusCode; 117 | processor.execute(body).then((result:ODataResult) => { 118 | try{ 119 | if (result){ 120 | res.status((origStatus != res.statusCode && res.statusCode) || result.statusCode || 200); 121 | if (!res.headersSent){ 122 | ensureODataContentType(req, res, result.contentType || "text/plain"); 123 | } 124 | if (typeof result.body != "undefined"){ 125 | if (typeof result.body != "object") res.send("" + result.body); 126 | else if (!res.headersSent) res.send(result.body); 127 | } 128 | } 129 | res.end(); 130 | }catch(err){ 131 | hasError = true; 132 | next(err); 133 | } 134 | }, (err) => { 135 | hasError = true; 136 | next(err); 137 | }); 138 | }catch(err){ 139 | next(err); 140 | } 141 | }; 142 | } 143 | 144 | static execute(url:string, body?:object):Promise>; 145 | static execute(url:string, method?:string, body?:object):Promise>; 146 | static execute(context:object, body?:object):Promise>; 147 | static execute(url:string | object, method?:string | object, body?:object):Promise>{ 148 | let context:any = {}; 149 | if (typeof url == "object"){ 150 | context = Object.assign(context, url); 151 | if (typeof method == "object"){ 152 | body = method; 153 | } 154 | url = undefined; 155 | method = undefined; 156 | }else if (typeof url == "string"){ 157 | context.url = url; 158 | if (typeof method == "object"){ 159 | body = method; 160 | method = "POST"; 161 | } 162 | context.method = method || "GET"; 163 | } 164 | context.method = context.method || "GET"; 165 | let processor = this.createProcessor(context, { 166 | objectMode: true, 167 | metadata: context.metadata || ODataMetadataType.minimal 168 | }); 169 | let values = []; 170 | let flushObject; 171 | let response = ""; 172 | if (context.response instanceof Writable) processor.pipe(context.response); 173 | processor.on("data", (chunk:any) => { 174 | if (!(typeof chunk == "string" || chunk instanceof Buffer)){ 175 | if (chunk["@odata.context"] && chunk.value && Array.isArray(chunk.value) && chunk.value.length == 0){ 176 | flushObject = chunk; 177 | flushObject.value = values; 178 | }else{ 179 | values.push(chunk); 180 | } 181 | }else response += chunk.toString(); 182 | }); 183 | return processor.execute(context.body || body).then((result:ODataResult) => { 184 | if (flushObject){ 185 | result.body = flushObject; 186 | if (!result.elementType || typeof result.elementType == "object") result.elementType = flushObject.elementType; 187 | delete flushObject.elementType; 188 | result.contentType = result.contentType || "application/json"; 189 | }else if (result && response){ 190 | result.body = response; 191 | } 192 | return result; 193 | }); 194 | } 195 | 196 | constructor(opts?:TransformOptions){ 197 | super(Object.assign({ 198 | objectMode: true 199 | }, opts)); 200 | this.serverType = Object.getPrototypeOf(this).constructor; 201 | } 202 | 203 | _transform(chunk:any, _?:string, done?:Function){ 204 | if ((chunk instanceof Buffer) || typeof chunk == "string"){ 205 | try{ 206 | chunk = JSON.parse(chunk.toString()); 207 | }catch(err){ 208 | return done(err); 209 | } 210 | } 211 | this.serverType.execute(chunk).then((result) => { 212 | this.push(result); 213 | if (typeof done == "function") done(); 214 | }, done); 215 | } 216 | 217 | _flush(done?:Function){ 218 | if (typeof done == "function") done(); 219 | } 220 | 221 | static createProcessor(context:any, options?:ODataProcessorOptions){ 222 | return new ODataProcessor(context, this, options); 223 | } 224 | 225 | static $metadata():ServiceMetadata; 226 | static $metadata(metadata:Metadata.Edmx | any); 227 | static $metadata(metadata?):ServiceMetadata{ 228 | if (metadata){ 229 | if (!(metadata instanceof Metadata.Edmx)){ 230 | if (metadata.version && metadata.dataServices && Array.isArray(metadata.dataServices.schema)) this._metadataCache = ServiceMetadata.processMetadataJson(metadata); 231 | else this._metadataCache = ServiceMetadata.defineEntities(metadata); 232 | } 233 | } 234 | return this._metadataCache || (this._metadataCache = ServiceMetadata.processMetadataJson(createMetadataJSON(this))); 235 | } 236 | 237 | static document():ServiceDocument{ 238 | return ServiceDocument.processEdmx(this.$metadata().edmx); 239 | } 240 | 241 | static addController(controller:typeof ODataController, isPublic?:boolean); 242 | static addController(controller:typeof ODataController, isPublic?:boolean, elementType?:Function); 243 | static addController(controller:typeof ODataController, entitySetName?:string, elementType?:Function); 244 | static addController(controller:typeof ODataController, entitySetName?:string | boolean, elementType?:Function){ 245 | odata.controller(controller, entitySetName, elementType)(this); 246 | } 247 | static getController(elementType:Function){ 248 | for (let i in this.prototype){ 249 | if (this.prototype[i] && 250 | this.prototype[i].prototype && 251 | this.prototype[i].prototype instanceof ODataController && 252 | this.prototype[i].prototype.elementType == elementType){ 253 | return this.prototype[i]; 254 | } 255 | } 256 | return null; 257 | } 258 | 259 | static create():express.Router; 260 | static create(port:number):http.Server; 261 | static create(path:string, port:number):http.Server; 262 | static create(port:number, hostname:string):http.Server; 263 | static create(path?:string | RegExp | number, port?:number | string, hostname?:string):http.Server; 264 | static create(path?:string | RegExp | number, port?:number | string, hostname?:string):http.Server | express.Router{ 265 | let server = this; 266 | let router = express.Router(); 267 | router.use((req, _, next) => { 268 | req.url = req.url.replace(/[\/]+/g, "/").replace(":/", "://"); 269 | if (req.headers["odata-maxversion"] && req.headers["odata-maxversion"] < "4.0") return next(new HttpRequestError(500, "Only OData version 4.0 supported")); 270 | next(); 271 | }); 272 | router.use(bodyParser.json()); 273 | if ((server).cors) router.use(cors()); 274 | router.use((req, res, next) => { 275 | res.setHeader("OData-Version", "4.0"); 276 | if (req.headers.accept && 277 | req.headers.accept.indexOf("application/json") < 0 && 278 | req.headers.accept.indexOf("text/html") < 0 && 279 | req.headers.accept.indexOf("*/*") < 0 && 280 | req.headers.accept.indexOf("xml") < 0){ 281 | next(new UnsupportedMediaTypeError()); 282 | }else next(); 283 | }); 284 | router.get("/", ensureODataHeaders, (req, _, next) => { 285 | if (typeof req.query == "object" && Object.keys(req.query).length > 0) return next(new HttpRequestError(500, "Unsupported query")); 286 | next(); 287 | }, server.document().requestHandler()); 288 | router.get("/\\$metadata", server.$metadata().requestHandler()); 289 | router.use(server.requestHandler()); 290 | router.use(server.errorHandler); 291 | 292 | if (typeof path == "number"){ 293 | if (typeof port == "string"){ 294 | hostname = "" + port; 295 | } 296 | port = parseInt(path, 10); 297 | path = undefined; 298 | } 299 | if (typeof port == "number"){ 300 | let app = express(); 301 | app.use((path) || "/", router); 302 | return app.listen(port, hostname); 303 | } 304 | return router; 305 | } 306 | } 307 | export class ODataServer extends ODataBase(ODataServerBase){} 308 | 309 | /** ?????????? */ 310 | /** Create Express middleware for OData error handling */ 311 | export function ODataErrorHandler(err, _, res, next){ 312 | if (err){ 313 | if (res.headersSent) { 314 | return next(err); 315 | } 316 | let statusCode = err.statusCode || err.status || (res.statusCode < 400 ? 500 : res.statusCode); 317 | if (!res.statusCode || res.statusCode < 400) res.status(statusCode); 318 | res.send({ 319 | error: { 320 | code: statusCode, 321 | message: err.message, 322 | stack: process.env.ODATA_V4_DISABLE_STACKTRACE ? undefined : err.stack 323 | } 324 | }); 325 | }else next(); 326 | } 327 | 328 | /** Create Express server for OData Server 329 | * @param server OData Server instance 330 | * @return Express Router object 331 | */ 332 | export function createODataServer(server:typeof ODataServer):express.Router; 333 | /** Create Express server for OData Server 334 | * @param server OData Server instance 335 | * @param port port number for Express to listen to 336 | */ 337 | export function createODataServer(server:typeof ODataServer, port:number):http.Server; 338 | /** Create Express server for OData Server 339 | * @param server OData Server instance 340 | * @param path routing path for Express 341 | * @param port port number for Express to listen to 342 | */ 343 | export function createODataServer(server:typeof ODataServer, path:string, port:number):http.Server; 344 | /** Create Express server for OData Server 345 | * @param server OData Server instance 346 | * @param port port number for Express to listen to 347 | * @param hostname hostname for Express 348 | */ 349 | export function createODataServer(server:typeof ODataServer, port:number, hostname:string):http.Server; 350 | /** Create Express server for OData Server 351 | * @param server OData Server instance 352 | * @param path routing path for Express 353 | * @param port port number for Express to listen to 354 | * @param hostname hostname for Express 355 | * @return Express Router object 356 | */ 357 | export function createODataServer(server:typeof ODataServer, path?:string | RegExp | number, port?:number | string, hostname?:string):http.Server | express.Router{ 358 | return server.create(path, port, hostname); 359 | } 360 | -------------------------------------------------------------------------------- /src/lib/utils.ts: -------------------------------------------------------------------------------- 1 | import { Transform } from "stream"; 2 | import { ODataStream } from "./result"; 3 | 4 | const patternSource = "[^(]*\\(([^)]*)\\)"; 5 | const pattern = new RegExp(patternSource); 6 | export const getFunctionParameters = function(fn:Function, name?:string){ 7 | let params = typeof name == "string" && typeof fn[name] == "function" 8 | ? fn[name].toString().match(new RegExp(`(?:${name})?` + patternSource)) 9 | : fn.toString().match(pattern); 10 | return params[1].split(/,(?:\s)?/).map(p => p.split(" ")[0]); 11 | }; 12 | export const getAllPropertyNames = function(proto:any):string[]{ 13 | let propNames = Object.getOwnPropertyNames(proto); 14 | proto = Object.getPrototypeOf(proto); 15 | if (proto !== Object.prototype && proto !== Transform.prototype) propNames = propNames.concat(getAllPropertyNames(proto)); 16 | return propNames; 17 | }; 18 | let GeneratorFunction; 19 | try{ GeneratorFunction = eval("(function*() {}).constructor"); }catch(err){} 20 | 21 | export function isIterator(value){ 22 | return value instanceof GeneratorFunction; 23 | } 24 | 25 | export function isPromise(value){ 26 | return value && typeof value.then == "function"; 27 | } 28 | 29 | export function isStream(stream){ 30 | return stream !== null && typeof stream == "object" && typeof stream.pipe == "function" && !(stream instanceof ODataStream); 31 | } 32 | 33 | export interface PropertyDecorator{ 34 | (target?:any, targetKey?:string): T; 35 | } 36 | export interface Decorator{ 37 | (target?:any, targetKey?:string, parameterIndex?:number | TypedPropertyDescriptor): T 38 | } -------------------------------------------------------------------------------- /src/test/TODO.md: -------------------------------------------------------------------------------- 1 | # Test cases 2 | 3 | * ~~Inherited ODataController: LogsController extends InMemoryController{}~~ 4 | * ~~Type definition URL deserializer (to handle ObjectID conversion from Edm.String to ObjectID instance)~~ 5 | * ~~Type definition URL serializer (to handle ObjectID conversion from ObjectID to Edm.String as entity key)~~ 6 | * ~~Type definition with @odata.type property to define type name, without using Edm.ContainerBase~~ 7 | * ~~Type definition in Edm.ContainerBase and use full name for type name, like 'Sytem.ObjectID'~~ 8 | * ~~Type definition with new schema namespace in server~~ 9 | * ~~Same as above 3 for EnumTypes~~ 10 | * More inheritance test cases 11 | * ~~Entity and entity collection bound function with entity type as return type~~ 12 | * ~~Entity and entity collection bound function with enum and type definition parameters~~ 13 | * ~~Entity and entity collection bound action with parameters~~ 14 | * ~~More enum types and type definitions~~ 15 | * ~~@odata.type as action/function parameter~~ 16 | * ~~Use @odata.method('methodname', 'navprop')~~ 17 | * ~~Use @odata.link('keyname')~~ 18 | * ~~Use @odata.id~~ 19 | * ~~Use .define() to define models, controllers, server, see es6.js example~~ 20 | * ~~Create HTTP Accept headers tests~~ 21 | * ~~Throw NotImplementedError~~ 22 | * ~~Use controller class static .on() like @odata.GET, etc.~~ 23 | * ~~Use controller class static .enableFilter() like @odata.filter~~ 24 | * ~~Enum type on inherited entity type~~ 25 | * ~~Enum type as action/function parameters~~ 26 | * ~~Type definition on inherited entity type~~ 27 | * ~~Multi-level inheritance for Edm.Container~~ 28 | * ~~Use server .execute() with context object as first parameter~~ 29 | * ~~Use server as stream~~ 30 | * ~~Use server static .addController() like @odata.controller~~ 31 | * ~~Use metadata JSON for public $metadata definition, like in the bigdata.ts example~~ 32 | * ~~Use HTTP header OData-MaxVersion with less than 4.0~~ 33 | * ~~Use HTTP Accept headers with text/html, */*, some xml and an unsupported type~~ 34 | * ~~Try to use OData query parameters on service document, expect 500 Unsupported query error~~ 35 | * ~~Start server on specific hostname (localhost)~~ 36 | * ~~Set some HTTP headers and response status code in controller or action/function import~~ 37 | * ~~Use $select OData query, expect @odata.context to be valid based on selection~~ 38 | * ~~Use resource path like Categories(1)/Products(1)~~ 39 | * ~~PUT handler in ODataController implement upsert, return entity if it was an insert, return null/undefined/void if it was an update~~ 40 | * ~~Use $count on empty result set~~ 41 | * ~~Use $count with bad result set~~ 42 | * ~~Use @odata.key aliases (different key and parameter name)~~ 43 | * ~~Use $value on primitive property~~ 44 | * ~~Use $value on stream property~~ 45 | * ~~Use @odata.GET.$ref~~ 46 | * ~~Use @odata.link aliases~~ 47 | * ~~Use generator function and Promise result in $ref handler~~ 48 | * ~~Use advanced generator functions returning Promise, stream or another generator function~~ 49 | * Use stream result when using $expand 50 | * ~~Try to access non-existent entity set~~ 51 | * ~~Implement inline count for stream result with @odata.count or inlinecount~~ 52 | * Use navigation property on stream result 53 | * ~~Use HTTP Accept header including odata.metadata=full|none~~ 54 | * ~~Implement navigation property with @odata.GET('navprop')~~ 55 | * ~~Implement navigation property POST (to insert into a navigation property set)~~ 56 | * ~~Implement unexposed controllers (without a public entity set, available on a navigation property)~~ 57 | * ~~Implement primitive property DELETE with PATCH handler~~ 58 | * More Edm.Stream properties test cases, like /Data/$value 59 | * Test $expand with subquery, multiple cases 60 | * ~~POST new entity to entity set using inheritance~~ 61 | * ~~Implement deserializer using @Edm.Deserializer to deserialize POST body~~ 62 | * ~~Include @odata field in POST body~~ 63 | * ~~Implement action/function with generator function and stream as result (like Fetch in stream.ts example)~~ 64 | * ~~Use $count after stream result set (Categories(1)/Products/$count)~~ 65 | * ~~Use @odata.type as function pointer in inheritance result set~~ 66 | * ~~Use $expand with single entity~~ 67 | * ~~Use $expand with HTTP Accept header odata.metadata=full, expect @odata.associationLink and @odata.navigationLink in result~~ 68 | * Use ODataStream class for Edm.Stream property implementation 69 | * Use ODataResult static Created, Ok, NoContent directly -------------------------------------------------------------------------------- /src/test/benchmark.ts: -------------------------------------------------------------------------------- 1 | import * as Benchmark from "benchmark"; 2 | import { createFilter as inmemory } from "odata-v4-inmemory"; 3 | import { createFilter as mongodb } from "odata-v4-mongodb"; 4 | import { ODataServer, ODataController, ODataQuery, odata } from "../lib/index"; 5 | 6 | let data = []; 7 | let suite = new Benchmark.Suite(); 8 | 9 | class InMemoryController extends ODataController{ 10 | @odata.GET 11 | find(@odata.filter filter:ODataQuery){ 12 | inmemory(filter); 13 | return data; 14 | } 15 | } 16 | 17 | class MongoDBController extends ODataController{ 18 | @odata.GET 19 | find(@odata.filter filter:ODataQuery){ 20 | mongodb(filter); 21 | return data; 22 | } 23 | } 24 | 25 | @odata.controller(InMemoryController, true) 26 | @odata.controller(MongoDBController, true) 27 | class BenchmarkServer extends ODataServer{} 28 | 29 | console.log("Benchmarking..."); 30 | suite.add("InMemory#filter", { 31 | defer: true, 32 | fn: (defer) => { 33 | BenchmarkServer.execute("/InMemory?$filter=Title eq 'Title'", "GET").then(() => defer.resolve()); 34 | } 35 | }).add("MongoDB#filter", { 36 | defer: true, 37 | fn: (defer) => { 38 | BenchmarkServer.execute("/MongoDB?$filter=Title eq 'Title'", "GET").then(() => defer.resolve()); 39 | } 40 | }).on("cycle", (event) => { 41 | console.log(event.target.toString()); 42 | }).run(); -------------------------------------------------------------------------------- /src/test/define.spec.ts: -------------------------------------------------------------------------------- 1 | /// 2 | import { Token } from "odata-v4-parser/lib/lexer"; 3 | import { createFilter } from "odata-v4-inmemory"; 4 | import { ODataController, ODataServer, ODataProcessor, ODataMethodType, ODataResult, Edm, odata, ODataHttpContext, ODataStream, ODataEntity } from "../lib/index"; 5 | import { DefTest } from "./test.model"; 6 | const { expect } = require("chai"); 7 | 8 | describe("OData ES6 .define()", () => { 9 | class DefTestController extends ODataController { 10 | all() { 11 | } 12 | one(key) { 13 | } 14 | } 15 | 16 | it("should throw decorator error", () => { 17 | try{ 18 | DefTestController.define(odata.type(DefTest), { 19 | all: odata.GET, 20 | one: [odata.GET, { 21 | key: odata.key 22 | }] 23 | }, "ex"); 24 | }catch(err){ 25 | expect(err.message).to.equal("Unsupported decorator on DefTestController using ex"); 26 | } 27 | }); 28 | 29 | it("should throw member decorator error", () => { 30 | try{ 31 | DefTestController.define(odata.type(DefTest), { 32 | all: odata.GET, 33 | one: [odata.GET, { 34 | key: odata.key 35 | }], 36 | ex: "ex" 37 | }); 38 | }catch(err){ 39 | expect(err.message).to.equal("Unsupported member decorator on DefTestController at ex using ex"); 40 | } 41 | }); 42 | 43 | it("should throw parameter decorator error", () => { 44 | try{ 45 | DefTestController.define(odata.type(DefTest), { 46 | all: odata.GET, 47 | one: [odata.GET, { 48 | key: odata.key, 49 | ex: "ex" 50 | }] 51 | }); 52 | }catch(err){ 53 | expect(err.message).to.equal("Unsupported parameter decorator on DefTestController at one.ex using ex"); 54 | } 55 | }); 56 | }); -------------------------------------------------------------------------------- /src/test/execute.spec.ts: -------------------------------------------------------------------------------- 1 | /// 2 | import { TestServer, Foobar } from './test.model'; 3 | import { ODataServer, NotImplementedError } from "../lib/index"; 4 | import { testFactory } from './server.spec' 5 | import { Product, Category } from "./model/model"; 6 | import * as fs from "fs"; 7 | import * as path from "path"; 8 | import * as streamBuffers from "stream-buffers"; 9 | 10 | const { expect } = require("chai"); 11 | const extend = require("extend"); 12 | let categories = require("./model/categories"); 13 | let products = require("./model/products"); 14 | 15 | function createTestFactory(it) { 16 | return function createTest(testcase: string, server: typeof ODataServer, command: string, compare: any, body?: any) { 17 | it(`${testcase} (${command})`, () => { 18 | let test = command.split(" "); 19 | return server.execute(test.slice(1).join(" "), test[0], body).then((result) => { 20 | expect(result).to.deep.equal(compare); 21 | }); 22 | }); 23 | } 24 | } 25 | 26 | const createTest: any = createTestFactory(it); 27 | createTest.only = createTestFactory(it.only); 28 | 29 | describe("OData execute", () => { 30 | testFactory(createTest); 31 | 32 | it("should update foobar's foo property ", () => { 33 | return TestServer.execute("/EntitySet(1)/foo", "PUT", { 34 | foo: "PUT" 35 | }).then((result) => { 36 | expect(result).to.deep.equal({ 37 | statusCode: 204 38 | }); 39 | 40 | return TestServer.execute("/EntitySet(1)", "GET").then((result) => { 41 | expect(result).to.deep.equal({ 42 | statusCode: 200, 43 | body: { 44 | "@odata.context": "http://localhost/$metadata#EntitySet/$entity", 45 | "@odata.id": "http://localhost/EntitySet(1)", 46 | "@odata.editLink": "http://localhost/EntitySet(1)", 47 | id: 1, 48 | foo: "PUT" 49 | }, 50 | elementType: Foobar, 51 | contentType: "application/json" 52 | }); 53 | }); 54 | }); 55 | }); 56 | 57 | it("should delete foobar's foo property ", () => { 58 | return TestServer.execute("/EntitySet(1)/foo", "DELETE").then((result) => { 59 | expect(result).to.deep.equal({ 60 | statusCode: 204 61 | }); 62 | 63 | return TestServer.execute("/EntitySet(1)", "GET").then((result) => { 64 | expect(result).to.deep.equal({ 65 | statusCode: 200, 66 | body: { 67 | "@odata.context": "http://localhost/$metadata#EntitySet/$entity", 68 | "@odata.id": "http://localhost/EntitySet(1)", 69 | "@odata.editLink": "http://localhost/EntitySet(1)", 70 | id: 1, 71 | foo: null 72 | }, 73 | elementType: Foobar, 74 | contentType: "application/json" 75 | }); 76 | }); 77 | }); 78 | }); 79 | 80 | it("should delta update foobar's foo property ", () => { 81 | return TestServer.execute("/EntitySet(1)/foo", "PATCH", { 82 | foo: "bar" 83 | }).then((result) => { 84 | expect(result).to.deep.equal({ 85 | statusCode: 204 86 | }); 87 | 88 | return TestServer.execute("/EntitySet(1)", "GET").then((result) => { 89 | expect(result).to.deep.equal({ 90 | statusCode: 200, 91 | body: { 92 | "@odata.context": "http://localhost/$metadata#EntitySet/$entity", 93 | "@odata.id": "http://localhost/EntitySet(1)", 94 | "@odata.editLink": "http://localhost/EntitySet(1)", 95 | id: 1, 96 | foo: "bar" 97 | }, 98 | elementType: Foobar, 99 | contentType: "application/json" 100 | }); 101 | }); 102 | }); 103 | }); 104 | 105 | it("should create product reference on category", () => { 106 | return TestServer.execute("/Categories('578f2baa12eaebabec4af28e')/Products('578f2b8c12eaebabec4af242')/$ref", "POST").then((result) => { 107 | expect(result).to.deep.equal({ 108 | statusCode: 204 109 | }); 110 | return TestServer.execute("/Products('578f2b8c12eaebabec4af242')/Category", "GET").then((result) => { 111 | expect(result).to.deep.equal({ 112 | statusCode: 200, 113 | body: extend({ 114 | "@odata.context": "http://localhost/$metadata#Categories/$entity" 115 | }, categories.filter(category => category._id.toString() == "578f2baa12eaebabec4af28e").map(category => extend({ 116 | "@odata.id": `http://localhost/Categories('${category._id}')` 117 | }, category))[0] 118 | ), 119 | elementType: Category, 120 | contentType: "application/json" 121 | }) 122 | }); 123 | }); 124 | }); 125 | 126 | it("should delete product reference on category", () => { 127 | return TestServer.execute("/Categories('578f2baa12eaebabec4af28e')/Products('578f2b8c12eaebabec4af242')/$ref", "DELETE").then((result) => { 128 | expect(result).to.deep.equal({ 129 | statusCode: 204 130 | }); 131 | return TestServer.execute("/Products('578f2b8c12eaebabec4af242')/Category", "GET").then((result) => { 132 | throw new Error("Category reference should be deleted."); 133 | }, (err) => { 134 | expect(err.name).to.equal("ResourceNotFoundError"); 135 | });; 136 | }); 137 | }); 138 | 139 | it("should update product reference on category", () => { 140 | return TestServer.execute("/Categories('578f2baa12eaebabec4af28d')/Products('578f2b8c12eaebabec4af242')/$ref", "PUT").then((result) => { 141 | expect(result).to.deep.equal({ 142 | statusCode: 204 143 | }); 144 | return TestServer.execute("/Products('578f2b8c12eaebabec4af242')/Category", "GET").then((result) => { 145 | expect(result).to.deep.equal({ 146 | statusCode: 200, 147 | body: extend({ 148 | "@odata.context": "http://localhost/$metadata#Categories/$entity" 149 | }, categories.filter(category => category._id.toString() == "578f2baa12eaebabec4af28d").map(category => extend({ 150 | "@odata.id": `http://localhost/Categories('${category._id}')` 151 | }, category))[0] 152 | ), 153 | elementType: Category, 154 | contentType: "application/json" 155 | }) 156 | }); 157 | }); 158 | }); 159 | 160 | it("should delete product reference on category by ref id", () => { 161 | return TestServer.execute("/Categories('578f2baa12eaebabec4af28b')/Products/$ref?$id=http://localhost/Products('578f2b8c12eaebabec4af284')", "DELETE").then((result) => { 162 | expect(result).to.deep.equal({ 163 | statusCode: 204 164 | }); 165 | 166 | return TestServer.execute("/Products('578f2b8c12eaebabec4af284')/Category", "GET").then((result) => { 167 | throw new Error("Category reference should be deleted."); 168 | }, (err) => { 169 | expect(err.name).to.equal("ResourceNotFoundError"); 170 | }); 171 | }); 172 | }); 173 | 174 | it("should delta update product reference on category", () => { 175 | return TestServer.execute("/Categories('578f2baa12eaebabec4af28b')/Products('578f2b8c12eaebabec4af284')/$ref", "PATCH").then((result) => { 176 | expect(result).to.deep.equal({ 177 | statusCode: 204 178 | }); 179 | return TestServer.execute("/Products('578f2b8c12eaebabec4af284')/Category", "GET").then((result) => { 180 | expect(result).to.deep.equal({ 181 | statusCode: 200, 182 | body: extend({ 183 | "@odata.context": "http://localhost/$metadata#Categories/$entity" 184 | }, categories.filter(category => category._id.toString() == "578f2baa12eaebabec4af28b").map(category => extend({ 185 | "@odata.id": `http://localhost/Categories('${category._id}')` 186 | }, category))[0] 187 | ), 188 | elementType: Category, 189 | contentType: "application/json" 190 | }) 191 | }); 192 | }); 193 | }); 194 | 195 | it("should create category reference on product", () => { 196 | return TestServer.execute("/Products('578f2b8c12eaebabec4af286')/Category/$ref", "POST", { 197 | "@odata.id": "http://localhost/Categories(categoryId='578f2baa12eaebabec4af28c')" 198 | }).then((result) => { 199 | expect(result).to.deep.equal({ 200 | statusCode: 204 201 | }); 202 | return TestServer.execute("/Products('578f2b8c12eaebabec4af286')/Category", "GET").then((result) => { 203 | expect(result).to.deep.equal({ 204 | statusCode: 200, 205 | body: extend({ 206 | "@odata.context": "http://localhost/$metadata#Categories/$entity" 207 | }, categories.filter(category => category._id.toString() == "578f2baa12eaebabec4af28c").map(category => extend({ 208 | "@odata.id": `http://localhost/Categories('${category._id}')` 209 | }, category))[0] 210 | ), 211 | elementType: Category, 212 | contentType: "application/json" 213 | }) 214 | }); 215 | }); 216 | }); 217 | 218 | it("should delete category reference on product", () => { 219 | return TestServer.execute("/Products('578f2b8c12eaebabec4af286')/Category/$ref", "DELETE", { 220 | "@odata.id": "http://localhost/Categories('578f2baa12eaebabec4af28c')" 221 | }).then((result) => { 222 | expect(result).to.deep.equal({ 223 | statusCode: 204 224 | }); 225 | 226 | return TestServer.execute("/Products('578f2b8c12eaebabec4af286')/Category", "GET").then((result) => { 227 | throw new Error("Category reference should be deleted."); 228 | }, (err) => { 229 | expect(err.name).to.equal("ResourceNotFoundError"); 230 | }); 231 | }); 232 | }); 233 | 234 | it("should update category reference on product", () => { 235 | return TestServer.execute("/Products('578f2b8c12eaebabec4af286')/Category/$ref", "PUT", { 236 | "@odata.id": "http://localhost/Categories(categoryId='578f2baa12eaebabec4af289')" 237 | }).then((result) => { 238 | expect(result).to.deep.equal({ 239 | statusCode: 204 240 | }); 241 | return TestServer.execute("/Products('578f2b8c12eaebabec4af286')/Category", "GET").then((result) => { 242 | expect(result).to.deep.equal({ 243 | statusCode: 200, 244 | body: extend({ 245 | "@odata.context": "http://localhost/$metadata#Categories/$entity" 246 | }, categories.filter(category => category._id.toString() == "578f2baa12eaebabec4af289").map(category => extend({ 247 | "@odata.id": `http://localhost/Categories('${category._id}')` 248 | }, category))[0] 249 | ), 250 | elementType: Category, 251 | contentType: "application/json" 252 | }) 253 | }); 254 | }); 255 | }); 256 | 257 | describe("Execute parameter is object", () => { 258 | it("should update foobar's foo property ", () => { 259 | let context: any = {}; 260 | context.url = '/EntitySet(1)/foo'; 261 | context.method = 'PUT'; 262 | return TestServer.execute(context, { foo: "PUT" }).then((result) => { 263 | expect(result).to.deep.equal({ 264 | statusCode: 204 265 | }); 266 | let ctx: any = {}; 267 | ctx.url = '/EntitySet(1)'; 268 | ctx.method = 'GET'; 269 | return TestServer.execute(ctx).then((result) => { 270 | expect(result).to.deep.equal({ 271 | statusCode: 200, 272 | body: { 273 | "@odata.context": "http://localhost/$metadata#EntitySet/$entity", 274 | "@odata.id": "http://localhost/EntitySet(1)", 275 | "@odata.editLink": "http://localhost/EntitySet(1)", 276 | id: 1, 277 | foo: "PUT" 278 | }, 279 | elementType: Foobar, 280 | contentType: "application/json" 281 | }); 282 | }); 283 | }); 284 | }); 285 | 286 | it("should delta update foobar's foo property ", () => { 287 | let context: any = {}; 288 | context.url = '/EntitySet(1)/foo'; 289 | context.method = 'PATCH'; 290 | context.body = { foo: "bar" }; 291 | return TestServer.execute(context).then((result) => { 292 | expect(result).to.deep.equal({ 293 | statusCode: 204 294 | }); 295 | let ctx: any = {}; 296 | ctx.url = '/EntitySet(1)'; 297 | ctx.method = 'GET'; 298 | return TestServer.execute(ctx).then((result) => { 299 | expect(result).to.deep.equal({ 300 | statusCode: 200, 301 | body: { 302 | "@odata.context": "http://localhost/$metadata#EntitySet/$entity", 303 | "@odata.id": "http://localhost/EntitySet(1)", 304 | "@odata.editLink": "http://localhost/EntitySet(1)", 305 | id: 1, 306 | foo: "bar" 307 | }, 308 | elementType: Foobar, 309 | contentType: "application/json" 310 | }); 311 | }); 312 | }); 313 | }); 314 | }); 315 | 316 | describe("Stream properties", () => { 317 | it("stream property POST", () => { 318 | let readableStrBuffer = new streamBuffers.ReadableStreamBuffer(); 319 | readableStrBuffer.put('tmp.png'); 320 | return TestServer.execute("/ImagesControllerEntitySet(1)/Data", "POST", readableStrBuffer).then((result) => { 321 | readableStrBuffer.stop(); 322 | expect(result).to.deep.equal({ 323 | statusCode: 204 324 | }); 325 | }); 326 | }); 327 | 328 | it("stream property GET", () => { 329 | let writableStrBuffer = new streamBuffers.WritableStreamBuffer(); 330 | return TestServer.execute({ 331 | url: "/ImagesControllerEntitySet(1)/Data", 332 | method: "GET", 333 | response: writableStrBuffer 334 | }).then(_ => { 335 | expect(writableStrBuffer.getContentsAsString()).to.equal("tmp.png"); 336 | }); 337 | }); 338 | 339 | it("stream property with ODataStream POST", () => { 340 | return TestServer.execute("/ImagesControllerEntitySet(1)/Data2", "POST", fs.createReadStream(path.join(__dirname, "fixtures", "logo_jaystack.png"))).then((result) => { 341 | expect(result).to.deep.equal({ 342 | statusCode: 204 343 | }); 344 | expect(fs.readFileSync(path.join(__dirname, "fixtures", "logo_jaystack.png"))).to.deep.equal(fs.readFileSync(path.join(__dirname, "fixtures", "tmp.png"))); 345 | if (fs.existsSync(path.join(__dirname, "fixtures", "tmp.png"))) { 346 | fs.unlinkSync(path.join(__dirname, "fixtures", "tmp.png")); 347 | } 348 | }); 349 | }); 350 | 351 | it("stream property with ODataStream GET", (done) => { 352 | let tmp = fs.createWriteStream(path.join(__dirname, "fixtures", "tmp.png")) 353 | tmp.on("open", _ => { 354 | TestServer.execute({ 355 | url: "/ImagesControllerEntitySet(1)/Data2", 356 | method: "GET", 357 | response: tmp 358 | }).then(_ => { 359 | expect(fs.readFileSync(path.join(__dirname, "fixtures", "tmp.png"))).to.deep.equal(fs.readFileSync(path.join(__dirname, "fixtures", "logo_jaystack.png"))); 360 | try { 361 | if (fs.existsSync(path.join(__dirname, "fixtures", "tmp.png"))) { 362 | fs.unlinkSync(path.join(__dirname, "fixtures", "tmp.png")); 363 | } 364 | done(); 365 | } catch (err) { 366 | done(err); 367 | } 368 | }, done); 369 | }).on("error", done); 370 | }); 371 | 372 | it("should return 204 after POST Data2 using generator function that yields stream", () => { 373 | return TestServer.execute("/Images2ControllerEntitySet(1)/Data2", "POST", fs.createReadStream(path.join(__dirname, "fixtures", "logo_jaystack.png"))).then((result) => { 374 | expect(result).to.deep.equal({ 375 | statusCode: 204 376 | }); 377 | expect(fs.readFileSync(path.join(__dirname, "fixtures", "logo_jaystack.png"))).to.deep.equal(fs.readFileSync(path.join(__dirname, "fixtures", "tmp.png"))); 378 | if (fs.existsSync(path.join(__dirname, "fixtures", "tmp.png"))) { 379 | fs.unlinkSync(path.join(__dirname, "fixtures", "tmp.png")); 380 | } 381 | }); 382 | }); 383 | 384 | it("should return 200 after GET Data2 using generator function that yields stream", (done) => { 385 | let tmp = fs.createWriteStream(path.join(__dirname, "fixtures", "tmp.png")) 386 | tmp.on("open", _ => { 387 | TestServer.execute({ 388 | url: "/Images2ControllerEntitySet(1)/Data2", 389 | method: "GET", 390 | response: tmp 391 | }).then(_ => { 392 | expect(fs.readFileSync(path.join(__dirname, "fixtures", "tmp.png"))).to.deep.equal(fs.readFileSync(path.join(__dirname, "fixtures", "logo_jaystack.png"))); 393 | try { 394 | if (fs.existsSync(path.join(__dirname, "fixtures", "tmp.png"))) { 395 | fs.unlinkSync(path.join(__dirname, "fixtures", "tmp.png")); 396 | } 397 | done(); 398 | } catch (err) { 399 | done(err); 400 | } 401 | }, done); 402 | }).on("error", done); 403 | }); 404 | }); 405 | 406 | describe("Media entity", () => { 407 | it("media entity POST", () => { 408 | let readableStrBuffer = new streamBuffers.ReadableStreamBuffer(); 409 | readableStrBuffer.put('tmp.mp3'); 410 | return TestServer.execute("/MusicControllerEntitySet(1)/$value", "POST", readableStrBuffer).then((result) => { 411 | expect(result).to.deep.equal({ 412 | statusCode: 204 413 | }); 414 | }); 415 | }); 416 | }); 417 | 418 | describe("Not implemented error", () => { 419 | it("should return not implemented error", () => { 420 | return TestServer.execute("/EntitySet", "GET").then(() => { 421 | try { 422 | throw new NotImplementedError(); 423 | } catch (err) { 424 | expect(err.message).to.equal("Not implemented."); 425 | } 426 | }); 427 | }); 428 | }); 429 | 430 | describe("Non existent entity", () => { 431 | it('should return cannot read property node error', () => { 432 | return TestServer.execute("/NonExistent", "GET") 433 | .then((result) => {}) 434 | .catch(err => { 435 | expect(err.message).to.equal("Cannot read property 'node' of undefined"); 436 | }); 437 | }); 438 | }); 439 | }); 440 | -------------------------------------------------------------------------------- /src/test/fixtures/logo_jaystack.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jaystack/odata-v4-server/1a1a9c426573c245446af2d6db0d01bfc51f96d0/src/test/fixtures/logo_jaystack.png -------------------------------------------------------------------------------- /src/test/metadata/$actionfunction.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | 169 | 170 | 171 | 172 | 173 | 174 | 175 | 176 | 177 | 178 | 179 | 180 | 181 | 182 | 183 | 184 | 185 | 186 | 187 | 188 | 189 | 190 | 191 | 192 | 193 | 194 | 195 | 196 | 197 | 198 | 199 | 200 | 201 | 202 | 203 | 204 | 205 | 206 | 207 | 208 | 209 | 210 | 211 | 212 | 213 | 214 | 215 | 216 | 217 | 218 | 219 | 220 | 221 | 222 | 223 | 224 | 225 | 226 | 227 | 228 | 229 | 230 | 231 | 232 | 233 | 234 | 235 | 236 | 237 | 238 | 239 | 240 | 241 | 242 | 243 | 244 | 245 | 246 | 247 | 248 | 249 | 250 | 251 | 252 | 253 | 254 | 255 | 256 | 257 | 258 | 259 | 260 | 261 | 262 | 263 | 264 | 265 | 266 | 267 | 268 | 269 | 270 | 271 | 272 | 273 | 274 | 275 | 276 | 277 | 278 | 279 | 280 | 281 | 282 | 283 | 284 | 285 | 286 | 287 | 288 | 289 | -------------------------------------------------------------------------------- /src/test/metadata/$defineentities.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /src/test/metadata/$enumserver.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /src/test/metadata/$metadata.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | 169 | 170 | 171 | 172 | 173 | 174 | 175 | 176 | 177 | 178 | 179 | 180 | 181 | 182 | 183 | 184 | 185 | 186 | 187 | 188 | 189 | 190 | 191 | 192 | 193 | 194 | 195 | 196 | 197 | 198 | 199 | 200 | 201 | 202 | 203 | 204 | 205 | 206 | 207 | 208 | 209 | 210 | 211 | 212 | 213 | 214 | 215 | 216 | 217 | 218 | 219 | 220 | 221 | 222 | 223 | 224 | 225 | 226 | 227 | 228 | 229 | 230 | 231 | 232 | 233 | 234 | 235 | 236 | 237 | 238 | 239 | 240 | 241 | 242 | 243 | 244 | 245 | 246 | 247 | 248 | 249 | 250 | 251 | 252 | 253 | 254 | 255 | 256 | 257 | 258 | 259 | 260 | 261 | 262 | 263 | 264 | 265 | 266 | 267 | 268 | 269 | 270 | 271 | 272 | 273 | 274 | 275 | 276 | 277 | 278 | 279 | 280 | 281 | 282 | 283 | 284 | 285 | 286 | 287 | 288 | 289 | 290 | 291 | 292 | 293 | 294 | 295 | -------------------------------------------------------------------------------- /src/test/metadata/$schemajson.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /src/test/metadata/$typedefserver.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /src/test/mocha.opts: -------------------------------------------------------------------------------- 1 | --compilers ts-node/register 2 | --require source-map-support/register 3 | --full-trace 4 | --bail 5 | src/**/*.spec.ts -------------------------------------------------------------------------------- /src/test/model/ModelsForGenerator.ts: -------------------------------------------------------------------------------- 1 | import { ObjectID } from "mongodb"; 2 | import { Edm } from "../../lib/index"; 3 | 4 | const toObjectID = _id => _id && !(_id instanceof ObjectID) ? ObjectID.createFromHexString(_id) : _id; 5 | 6 | @Edm.Annotate({ term: "UI.DisplayName", string: "GeneratorProduct" }) 7 | export class GeneratorProduct { 8 | @Edm.Key 9 | @Edm.Computed 10 | @Edm.String 11 | @Edm.Convert(toObjectID) 12 | @Edm.Annotate( 13 | { term: "UI.DisplayName", string: "ProductPromise identifier" }, 14 | { term: "UI.ControlHint", string: "ReadOnly" } 15 | ) 16 | _id: ObjectID 17 | 18 | @Edm.String 19 | @Edm.Required 20 | @Edm.Convert(toObjectID) 21 | CategoryId: ObjectID 22 | 23 | @Edm.ForeignKey("CategoryId") 24 | @Edm.EntityType(Edm.ForwardRef(() => GeneratorCategory)) 25 | @Edm.Partner("GeneratorProduct") 26 | GeneratorCategory: GeneratorCategory 27 | 28 | @Edm.Boolean 29 | Discontinued: boolean 30 | 31 | @Edm.String 32 | @Edm.Annotate( 33 | { term: "UI.DisplayName", string: "GeneratorProduct title" }, 34 | { term: "UI.ControlHint", string: "ShortText" } 35 | ) 36 | Name: string 37 | 38 | @Edm.String 39 | @Edm.Annotate( 40 | { term: "UI.DisplayName", string: "GeneratorProduct English name" }, 41 | { term: "UI.ControlHint", string: "ShortText" } 42 | ) 43 | QuantityPerUnit: string 44 | 45 | @Edm.Decimal 46 | @Edm.Annotate( 47 | { term: "UI.DisplayName", string: "Unit price of GeneratorProduct" }, 48 | { term: "UI.ControlHint", string: "Decimal" } 49 | ) 50 | UnitPrice: number 51 | } 52 | 53 | @Edm.OpenType 54 | @Edm.Annotate({ term: "UI.DisplayName", string: "GeneratorCategory" }) 55 | export class GeneratorCategory { 56 | @Edm.Key 57 | @Edm.Computed 58 | @Edm.String 59 | @Edm.Convert(toObjectID) 60 | @Edm.Annotate( 61 | { term: "UI.DisplayName", string: "GeneratorCategory identifier" }, 62 | { term: "UI.ControlHint", string: "ReadOnly" } 63 | ) 64 | _id: ObjectID 65 | 66 | @Edm.String 67 | Description: string 68 | 69 | @Edm.String 70 | @Edm.Annotate( 71 | { term: "UI.DisplayName", string: "GeneratorCategory name" }, 72 | { term: "UI.ControlHint", string: "ShortText" } 73 | ) 74 | Name: string 75 | 76 | @Edm.ForeignKey("CategoryId") 77 | @Edm.Collection(Edm.EntityType(GeneratorProduct)) 78 | @Edm.Partner("GeneratorCategory") 79 | GeneratorProducts: GeneratorProduct[] 80 | 81 | @Edm.Collection(Edm.String) 82 | @Edm.Function 83 | echo() { 84 | return ["echotest"]; 85 | } 86 | } -------------------------------------------------------------------------------- /src/test/model/ModelsForPromise.ts: -------------------------------------------------------------------------------- 1 | import { ObjectID } from "mongodb"; 2 | import { Edm } from "../../lib/index"; 3 | 4 | const toObjectID = _id => _id && !(_id instanceof ObjectID) ? ObjectID.createFromHexString(_id) : _id; 5 | 6 | @Edm.Annotate({ 7 | term: "UI.DisplayName", 8 | string: "ProductPromise" 9 | }) 10 | export class ProductPromise { 11 | @Edm.Key 12 | @Edm.Computed 13 | @Edm.String 14 | @Edm.Convert(toObjectID) 15 | @Edm.Annotate({ 16 | term: "UI.DisplayName", 17 | string: "ProductPromise identifier" 18 | }, { 19 | term: "UI.ControlHint", 20 | string: "ReadOnly" 21 | }) 22 | _id:ObjectID 23 | 24 | @Edm.String 25 | @Edm.Required 26 | @Edm.Convert(toObjectID) 27 | CategoryId:ObjectID 28 | 29 | @Edm.ForeignKey("CategoryId") 30 | @Edm.EntityType(Edm.ForwardRef(() => CategoryPromise)) 31 | @Edm.Partner("ProductPromise") 32 | CategoryPromise:CategoryPromise 33 | 34 | @Edm.Boolean 35 | Discontinued:boolean 36 | 37 | @Edm.String 38 | @Edm.Annotate({ 39 | term: "UI.DisplayName", 40 | string: "ProductPromise title" 41 | }, { 42 | term: "UI.ControlHint", 43 | string: "ShortText" 44 | }) 45 | Name:string 46 | 47 | @Edm.String 48 | @Edm.Annotate({ 49 | term: "UI.DisplayName", 50 | string: "ProductPromise English name" 51 | }, { 52 | term: "UI.ControlHint", 53 | string: "ShortText" 54 | }) 55 | QuantityPerUnit:string 56 | 57 | @Edm.Decimal 58 | @Edm.Annotate({ 59 | term: "UI.DisplayName", 60 | string: "Unit price of ProductPromise" 61 | }, { 62 | term: "UI.ControlHint", 63 | string: "Decimal" 64 | }) 65 | UnitPrice:number 66 | } 67 | 68 | @Edm.OpenType 69 | @Edm.Annotate({ 70 | term: "UI.DisplayName", 71 | string: "CategoryPromise" 72 | }) 73 | export class CategoryPromise { 74 | @Edm.Key 75 | @Edm.Computed 76 | @Edm.String 77 | @Edm.Convert(toObjectID) 78 | @Edm.Annotate({ 79 | term: "UI.DisplayName", 80 | string: "CategoryPromise identifier" 81 | }, 82 | { 83 | term: "UI.ControlHint", 84 | string: "ReadOnly" 85 | }) 86 | _id:ObjectID 87 | 88 | @Edm.String 89 | Description:string 90 | 91 | @Edm.String 92 | @Edm.Annotate({ 93 | term: "UI.DisplayName", 94 | string: "CategoryPromise name" 95 | }, 96 | { 97 | term: "UI.ControlHint", 98 | string: "ShortText" 99 | }) 100 | Name:string 101 | 102 | @Edm.ForeignKey("CategoryId") 103 | @Edm.Collection(Edm.EntityType(ProductPromise)) 104 | @Edm.Partner("CategoryPromise") 105 | ProductPromises:ProductPromise[] 106 | 107 | @Edm.Collection(Edm.String) 108 | @Edm.Function 109 | echo(){ 110 | return ["echotest"]; 111 | } 112 | } -------------------------------------------------------------------------------- /src/test/model/ModelsForStream.ts: -------------------------------------------------------------------------------- 1 | import { ObjectID } from "mongodb"; 2 | import { Edm } from "../../lib/index"; 3 | 4 | const toObjectID = _id => _id && !(_id instanceof ObjectID) ? ObjectID.createFromHexString(_id) : _id; 5 | 6 | @Edm.Annotate({ 7 | term: "UI.DisplayName", 8 | string: "StreamProduct" 9 | }) 10 | export class StreamProduct { 11 | @Edm.Key 12 | @Edm.Computed 13 | @Edm.String 14 | @Edm.Convert(toObjectID) 15 | @Edm.Annotate( 16 | { term: "UI.DisplayName", string: "StreamProduct identifier" }, 17 | { term: "UI.ControlHint", string: "ReadOnly" } 18 | ) 19 | _id: ObjectID 20 | 21 | @Edm.String 22 | @Edm.Required 23 | @Edm.Convert(toObjectID) 24 | CategoryId: ObjectID 25 | 26 | @Edm.ForeignKey("CategoryId") 27 | @Edm.Partner("StreamProduct") 28 | @Edm.EntityType(Edm.ForwardRef(() => StreamCategory)) 29 | StreamCategory: StreamCategory 30 | 31 | @Edm.Boolean 32 | Discontinued: boolean 33 | 34 | @Edm.String 35 | @Edm.Annotate( 36 | { term: "UI.DisplayName", string: "StreamProduct title" }, 37 | { term: "UI.ControlHint", string: "ShortText" } 38 | ) 39 | Name: string 40 | 41 | @Edm.String 42 | @Edm.Annotate( 43 | { term: "UI.DisplayName", string: "StreamProduct English name" }, 44 | { term: "UI.ControlHint", string: "ShortText" } 45 | ) 46 | QuantityPerUnit: string 47 | 48 | @Edm.Decimal 49 | @Edm.Annotate({ 50 | term: "UI.DisplayName", 51 | string: "Unit price of StreamProduct" 52 | }, { 53 | term: "UI.ControlHint", 54 | string: "Decimal" 55 | }) 56 | UnitPrice: number 57 | } 58 | 59 | @Edm.OpenType 60 | @Edm.Annotate({ 61 | term: "UI.DisplayName", 62 | string: "StreamCategory" 63 | }) 64 | export class StreamCategory { 65 | @Edm.Key 66 | @Edm.Computed 67 | @Edm.String 68 | @Edm.Convert(toObjectID) 69 | @Edm.Annotate( 70 | { term: "UI.DisplayName", string: "StreamCategory identifier" }, 71 | { term: "UI.ControlHint", string: "ReadOnly" } 72 | ) 73 | _id: ObjectID 74 | 75 | @Edm.String 76 | Description: string 77 | 78 | @Edm.String 79 | @Edm.Annotate( 80 | { term: "UI.DisplayName", string: "CategoryPromise name" }, 81 | { term: "UI.ControlHint", string: "ShortText" } 82 | ) 83 | Name: string 84 | 85 | @Edm.ForeignKey("CategoryId") 86 | @Edm.Partner("StreamCategory") 87 | @Edm.Collection(Edm.EntityType(StreamProduct)) 88 | StreamProducts: StreamProduct[] 89 | 90 | @Edm.Collection(Edm.String) 91 | @Edm.Function 92 | echo() { return ["echotest"]; } 93 | } -------------------------------------------------------------------------------- /src/test/model/categories.ts: -------------------------------------------------------------------------------- 1 | import { ObjectID } from "mongodb"; 2 | 3 | export = [ 4 | {"_id": new ObjectID("578f2baa12eaebabec4af289"),"Description":"Soft drinks","Name":"Beverages"}, 5 | {"_id": new ObjectID("578f2baa12eaebabec4af28a"),"Description":"Breads","Name":"Grains/Cereals"}, 6 | {"_id": new ObjectID("578f2baa12eaebabec4af28b"),"Description":"Prepared meats","Name":"Meat/Poultry"}, 7 | {"_id": new ObjectID("578f2baa12eaebabec4af28c"),"Description":"Dried fruit and bean curd","Name":"Produce"}, 8 | {"_id": new ObjectID("578f2baa12eaebabec4af28d"),"Description":"Seaweed and fish","Name":"Seafood"}, 9 | {"_id": new ObjectID("578f2baa12eaebabec4af28e"),"Description":"Sweet and savory sauces","Name":"Condiments"}, 10 | {"_id": new ObjectID("578f2baa12eaebabec4af28f"),"Description":"Cheeses","Name":"Dairy Products"}, 11 | {"_id": new ObjectID("578f2baa12eaebabec4af290"),"Description":"Desserts","Name":"Confections"} 12 | ]; -------------------------------------------------------------------------------- /src/test/model/model.ts: -------------------------------------------------------------------------------- 1 | import { ObjectID } from "mongodb"; 2 | import { Edm } from "../../lib/index"; 3 | 4 | const toObjectID = _id => _id && !(_id instanceof ObjectID) ? ObjectID.createFromHexString(_id) : _id; 5 | 6 | @Edm.Annotate({ 7 | term: "UI.DisplayName", 8 | string: "Products" 9 | }) 10 | export class Product{ 11 | @Edm.Key 12 | @Edm.Computed 13 | @Edm.String 14 | @Edm.Convert(toObjectID) 15 | @Edm.Annotate({ 16 | term: "UI.DisplayName", 17 | string: "Product identifier" 18 | }, { 19 | term: "UI.ControlHint", 20 | string: "ReadOnly" 21 | }) 22 | _id:ObjectID 23 | 24 | @Edm.String 25 | @Edm.Required 26 | @Edm.Convert(toObjectID) 27 | CategoryId:ObjectID 28 | 29 | @Edm.ForeignKey("CategoryId") 30 | @Edm.EntityType(Edm.ForwardRef(() => Category)) 31 | @Edm.Partner("Products") 32 | Category:Category 33 | 34 | @Edm.Boolean 35 | Discontinued:boolean 36 | 37 | @Edm.String 38 | @Edm.Annotate({ 39 | term: "UI.DisplayName", 40 | string: "Product title" 41 | }, { 42 | term: "UI.ControlHint", 43 | string: "ShortText" 44 | }) 45 | Name:string 46 | 47 | @Edm.String 48 | @Edm.Annotate({ 49 | term: "UI.DisplayName", 50 | string: "Product English name" 51 | }, { 52 | term: "UI.ControlHint", 53 | string: "ShortText" 54 | }) 55 | QuantityPerUnit:string 56 | 57 | @Edm.Decimal 58 | @Edm.Annotate({ 59 | term: "UI.DisplayName", 60 | string: "Unit price of product" 61 | }, { 62 | term: "UI.ControlHint", 63 | string: "Decimal" 64 | }) 65 | UnitPrice:number 66 | } 67 | 68 | @Edm.OpenType 69 | @Edm.Annotate({ 70 | term: "UI.DisplayName", 71 | string: "Categories" 72 | }) 73 | export class Category{ 74 | @Edm.Key 75 | @Edm.Computed 76 | @Edm.String 77 | @Edm.Convert(toObjectID) 78 | @Edm.Annotate({ 79 | term: "UI.DisplayName", 80 | string: "Category identifier" 81 | }, 82 | { 83 | term: "UI.ControlHint", 84 | string: "ReadOnly" 85 | }) 86 | _id:ObjectID 87 | 88 | @Edm.String 89 | Description:string 90 | 91 | @Edm.String 92 | @Edm.Annotate({ 93 | term: "UI.DisplayName", 94 | string: "Category name" 95 | }, 96 | { 97 | term: "UI.ControlHint", 98 | string: "ShortText" 99 | }) 100 | Name:string 101 | 102 | @Edm.ForeignKey("CategoryId") 103 | @Edm.Collection(Edm.EntityType(Product)) 104 | @Edm.Partner("Category") 105 | Products:Product[] 106 | 107 | @Edm.Collection(Edm.String) 108 | @Edm.Function 109 | echo(){ 110 | return ["echotest"]; 111 | } 112 | } -------------------------------------------------------------------------------- /src/test/model/products.ts: -------------------------------------------------------------------------------- 1 | import { ObjectID } from "mongodb"; 2 | 3 | export = [ 4 | {"_id": new ObjectID("578f2b8c12eaebabec4af23c"),"QuantityPerUnit":"10 boxes x 20 bags","UnitPrice":39,"CategoryId": new ObjectID("578f2baa12eaebabec4af289"),"Name":"Chai","Discontinued":false}, 5 | {"_id": new ObjectID("578f2b8c12eaebabec4af23d"),"QuantityPerUnit":"24 - 12 oz bottles","UnitPrice":19.0,"CategoryId": new ObjectID("578f2baa12eaebabec4af289"),"Name":"Chang","Discontinued":true}, 6 | {"_id": new ObjectID("578f2b8c12eaebabec4af23e"),"QuantityPerUnit":"12 - 550 ml bottles","UnitPrice":10.0,"CategoryId": new ObjectID("578f2baa12eaebabec4af28e"),"Name":"Aniseed Syrup","Discontinued":false}, 7 | {"_id": new ObjectID("578f2b8c12eaebabec4af23f"),"QuantityPerUnit":"48 - 6 oz jars","UnitPrice":22.0,"CategoryId": new ObjectID("578f2baa12eaebabec4af28e"),"Name":"Chef Anton's Cajun Seasoning","Discontinued":true}, 8 | {"_id": new ObjectID("578f2b8c12eaebabec4af240"),"QuantityPerUnit":"36 boxes","UnitPrice":21.35,"CategoryId": new ObjectID("578f2baa12eaebabec4af28e"),"Name":"Chef Anton's Gumbo Mix","Discontinued":false}, 9 | {"_id": new ObjectID("578f2b8c12eaebabec4af241"),"QuantityPerUnit":"12 - 8 oz jars","UnitPrice":25.0,"CategoryId": new ObjectID("578f2baa12eaebabec4af28e"),"Name":"Grandma's Boysenberry Spread","Discontinued":false}, 10 | {"_id": new ObjectID("578f2b8c12eaebabec4af242"),"QuantityPerUnit":"12 - 200 ml jars","UnitPrice":31.0,"CategoryId": new ObjectID("578f2baa12eaebabec4af28d"),"Name":"Ikura","Discontinued":false}, 11 | {"_id": new ObjectID("578f2b8c12eaebabec4af243"),"QuantityPerUnit":"1 kg pkg.","UnitPrice":21.0,"CategoryId": new ObjectID("578f2baa12eaebabec4af28f"),"Name":"Queso Cabrales","Discontinued":false}, 12 | {"_id": new ObjectID("578f2b8c12eaebabec4af244"),"QuantityPerUnit":"10 - 500 g pkgs.","UnitPrice":38.0,"CategoryId": new ObjectID("578f2baa12eaebabec4af28f"),"Name":"Queso Manchego La Pastora","Discontinued":true}, 13 | {"_id": new ObjectID("578f2b8c12eaebabec4af245"),"QuantityPerUnit":"2 kg box","UnitPrice":6.0,"CategoryId": new ObjectID("578f2baa12eaebabec4af28d"),"Name":"Konbu","Discontinued":false}, 14 | {"_id": new ObjectID("578f2b8c12eaebabec4af246"),"QuantityPerUnit":"40 - 100 g pkgs.","UnitPrice":23.25,"CategoryId": new ObjectID("578f2baa12eaebabec4af28c"),"Name":"Tofu","Discontinued":false}, 15 | {"_id": new ObjectID("578f2b8c12eaebabec4af247"),"QuantityPerUnit":"24 - 250 ml bottles","UnitPrice":15.5,"CategoryId": new ObjectID("578f2baa12eaebabec4af28e"),"Name":"Genen Shouyu","Discontinued":false}, 16 | {"_id": new ObjectID("578f2b8c12eaebabec4af248"),"QuantityPerUnit":"32 - 500 g boxes","UnitPrice":17.45,"CategoryId": new ObjectID("578f2baa12eaebabec4af290"),"Name":"Pavlova","Discontinued":false}, 17 | {"_id": new ObjectID("578f2b8c12eaebabec4af249"),"QuantityPerUnit":"20 - 1 kg tins","UnitPrice":39.0,"CategoryId": new ObjectID("578f2baa12eaebabec4af28b"),"Name":"Alice Mutton","Discontinued":false}, 18 | {"_id": new ObjectID("578f2b8c12eaebabec4af24a"),"QuantityPerUnit":"16 kg pkg.","UnitPrice":62.5,"CategoryId": new ObjectID("578f2baa12eaebabec4af28d"),"Name":"Carnarvon Tigers","Discontinued":false}, 19 | {"_id": new ObjectID("578f2b8c12eaebabec4af24b"),"QuantityPerUnit":"10 boxes x 12 pieces","UnitPrice":9.2,"CategoryId": new ObjectID("578f2baa12eaebabec4af290"),"Name":"Teatime Chocolate Biscuits","Discontinued":true}, 20 | {"_id": new ObjectID("578f2b8c12eaebabec4af24c"),"QuantityPerUnit":"30 gift boxes","UnitPrice":81.0,"CategoryId": new ObjectID("578f2baa12eaebabec4af290"),"Name":"Sir Rodney's Marmalade","Discontinued":false}, 21 | {"_id": new ObjectID("578f2b8c12eaebabec4af24d"),"QuantityPerUnit":"24 pkgs. x 4 pieces","UnitPrice":10.0,"CategoryId": new ObjectID("578f2baa12eaebabec4af290"),"Name":"Sir Rodney's Scones","Discontinued":false}, 22 | {"_id": new ObjectID("578f2b8c12eaebabec4af24e"),"QuantityPerUnit":"12 - 1 lb pkgs.","UnitPrice":30.0,"CategoryId": new ObjectID("578f2baa12eaebabec4af28c"),"Name":"Uncle Bob's Organic Dried Pears","Discontinued":true}, 23 | {"_id": new ObjectID("578f2b8c12eaebabec4af24f"),"QuantityPerUnit":"12 - 12 oz jars","UnitPrice":40.0,"CategoryId": new ObjectID("578f2baa12eaebabec4af28e"),"Name":"Northwoods Cranberry Sauce","Discontinued":false}, 24 | {"_id": new ObjectID("578f2b8c12eaebabec4af250"),"QuantityPerUnit":"18 - 500 g pkgs.","UnitPrice":97.0,"CategoryId": new ObjectID("578f2baa12eaebabec4af28b"),"Name":"Mishi Kobe Niku","Discontinued":false}, 25 | {"_id": new ObjectID("578f2b8c12eaebabec4af251"),"QuantityPerUnit":"24 - 500 g pkgs.","UnitPrice":21.0,"CategoryId": new ObjectID("578f2baa12eaebabec4af28a"),"Name":"Gustaf's Knäckebröd","Discontinued":false}, 26 | {"_id": new ObjectID("578f2b8c12eaebabec4af252"),"QuantityPerUnit":"12 - 250 g pkgs.","UnitPrice":9.0,"CategoryId": new ObjectID("578f2baa12eaebabec4af28a"),"Name":"Tunnbröd","Discontinued":false}, 27 | {"_id": new ObjectID("578f2b8c12eaebabec4af253"),"QuantityPerUnit":"12 - 355 ml cans","UnitPrice":4.5,"CategoryId": new ObjectID("578f2baa12eaebabec4af289"),"Name":"Guaraná Fantástica","Discontinued":false}, 28 | {"_id": new ObjectID("578f2b8c12eaebabec4af254"),"QuantityPerUnit":"20 - 450 g glasses","UnitPrice":14.0,"CategoryId": new ObjectID("578f2baa12eaebabec4af290"),"Name":"NuNuCa Nuß-Nougat-Creme","Discontinued":true}, 29 | {"_id": new ObjectID("578f2b8c12eaebabec4af255"),"QuantityPerUnit":"100 - 250 g bags","UnitPrice":31.23,"CategoryId": new ObjectID("578f2baa12eaebabec4af290"),"Name":"Gumbär Gummibärchen","Discontinued":false}, 30 | {"_id": new ObjectID("578f2b8c12eaebabec4af256"),"QuantityPerUnit":"10 - 200 g glasses","UnitPrice":25.89,"CategoryId": new ObjectID("578f2baa12eaebabec4af28d"),"Name":"Nord-Ost Matjeshering","Discontinued":true}, 31 | {"_id": new ObjectID("578f2b8c12eaebabec4af257"),"QuantityPerUnit":"12 - 100 g pkgs","UnitPrice":12.5,"CategoryId": new ObjectID("578f2baa12eaebabec4af28f"),"Name":"Gorgonzola Telino","Discontinued":false}, 32 | {"_id": new ObjectID("578f2b8c12eaebabec4af258"),"QuantityPerUnit":"24 - 200 g pkgs.","UnitPrice":32.0,"CategoryId": new ObjectID("578f2baa12eaebabec4af28f"),"Name":"Mascarpone Fabioli","Discontinued":false}, 33 | {"_id": new ObjectID("578f2b8c12eaebabec4af259"),"QuantityPerUnit":"500 g","UnitPrice":2.5,"CategoryId": new ObjectID("578f2baa12eaebabec4af28f"),"Name":"Geitost","Discontinued":false}, 34 | {"_id": new ObjectID("578f2b8c12eaebabec4af25a"),"QuantityPerUnit":"24 - 12 oz bottles","UnitPrice":14.0,"CategoryId": new ObjectID("578f2baa12eaebabec4af289"),"Name":"Sasquatch Ale","Discontinued":false}, 35 | {"_id": new ObjectID("578f2b8c12eaebabec4af25b"),"QuantityPerUnit":"24 - 12 oz bottles","UnitPrice":18.0,"CategoryId": new ObjectID("578f2baa12eaebabec4af289"),"Name":"Steeleye Stout","Discontinued":false}, 36 | {"_id": new ObjectID("578f2b8c12eaebabec4af25c"),"QuantityPerUnit":"24 - 250 g jars","UnitPrice":19.0,"CategoryId": new ObjectID("578f2baa12eaebabec4af28d"),"Name":"Inlagd Sill","Discontinued":false}, 37 | {"_id": new ObjectID("578f2b8c12eaebabec4af25d"),"QuantityPerUnit":"12 - 500 g pkgs.","UnitPrice":26.0,"CategoryId": new ObjectID("578f2baa12eaebabec4af28d"),"Name":"Gravad lax","Discontinued":false}, 38 | {"_id": new ObjectID("578f2b8c12eaebabec4af25e"),"QuantityPerUnit":"12 - 75 cl bottles","UnitPrice":263.5,"CategoryId": new ObjectID("578f2baa12eaebabec4af289"),"Name":"Côte de Blaye","Discontinued":false}, 39 | {"_id": new ObjectID("578f2b8c12eaebabec4af25f"),"QuantityPerUnit":"750 cc per bottle","UnitPrice":18.0,"CategoryId": new ObjectID("578f2baa12eaebabec4af289"),"Name":"Chartreuse verte","Discontinued":false}, 40 | {"_id": new ObjectID("578f2b8c12eaebabec4af260"),"QuantityPerUnit":"24 - 4 oz tins","UnitPrice":18.4,"CategoryId": new ObjectID("578f2baa12eaebabec4af28d"),"Name":"Boston Crab Meat","Discontinued":false}, 41 | {"_id": new ObjectID("578f2b8c12eaebabec4af261"),"QuantityPerUnit":"12 - 12 oz cans","UnitPrice":9.65,"CategoryId": new ObjectID("578f2baa12eaebabec4af28d"),"Name":"Jack's New England Clam Chowder","Discontinued":true}, 42 | {"_id": new ObjectID("578f2b8c12eaebabec4af262"),"QuantityPerUnit":"32 - 1 kg pkgs.","UnitPrice":14.0,"CategoryId": new ObjectID("578f2baa12eaebabec4af28a"),"Name":"Singaporean Hokkien Fried Mee","Discontinued":true}, 43 | {"_id": new ObjectID("578f2b8c12eaebabec4af263"),"QuantityPerUnit":"16 - 500 g tins","UnitPrice":46.0,"CategoryId": new ObjectID("578f2baa12eaebabec4af289"),"Name":"Ipoh Coffee","Discontinued":false}, 44 | {"_id": new ObjectID("578f2b8c12eaebabec4af265"),"QuantityPerUnit":"1k pkg.","UnitPrice":9.5,"CategoryId": new ObjectID("578f2baa12eaebabec4af28d"),"Name":"Rogede sild","Discontinued":false}, 45 | {"_id": new ObjectID("578f2b8c12eaebabec4af266"),"QuantityPerUnit":"4 - 450 g glasses","UnitPrice":12.0,"CategoryId": new ObjectID("578f2baa12eaebabec4af28d"),"Name":"Spegesild","Discontinued":false}, 46 | {"_id": new ObjectID("578f2b8c12eaebabec4af267"),"QuantityPerUnit":"10 - 4 oz boxes","UnitPrice":9.5,"CategoryId": new ObjectID("578f2baa12eaebabec4af290"),"Name":"Zaanse koeken","Discontinued":false}, 47 | {"_id": new ObjectID("578f2b8c12eaebabec4af268"),"QuantityPerUnit":"100 - 100 g pieces","UnitPrice":43.9,"CategoryId": new ObjectID("578f2baa12eaebabec4af290"),"Name":"Schoggi Schokolade","Discontinued":false}, 48 | {"_id": new ObjectID("578f2b8c12eaebabec4af269"),"QuantityPerUnit":"25 - 825 g cans","UnitPrice":45.6,"CategoryId": new ObjectID("578f2baa12eaebabec4af28c"),"Name":"Rössle Sauerkraut","Discontinued":false}, 49 | {"_id": new ObjectID("578f2b8c12eaebabec4af26a"),"QuantityPerUnit":"50 bags x 30 sausgs.","UnitPrice":123.79,"CategoryId": new ObjectID("578f2baa12eaebabec4af28b"),"Name":"Thüringer Rostbratwurst","Discontinued":true}, 50 | {"_id": new ObjectID("578f2b8c12eaebabec4af26b"),"QuantityPerUnit":"10 pkgs.","UnitPrice":12.75,"CategoryId": new ObjectID("578f2baa12eaebabec4af290"),"Name":"Chocolade","Discontinued":false}, 51 | {"_id": new ObjectID("578f2b8c12eaebabec4af26c"),"QuantityPerUnit":"24 - 50 g pkgs.","UnitPrice":20.0,"CategoryId": new ObjectID("578f2baa12eaebabec4af290"),"Name":"Maxilaku","Discontinued":false}, 52 | {"_id": new ObjectID("578f2b8c12eaebabec4af26d"),"QuantityPerUnit":"12 - 100 g bars","UnitPrice":16.25,"CategoryId": new ObjectID("578f2baa12eaebabec4af290"),"Name":"Valkoinen suklaa","Discontinued":true}, 53 | {"_id": new ObjectID("578f2b8c12eaebabec4af26e"),"QuantityPerUnit":"50 - 300 g pkgs.","UnitPrice":53.0,"CategoryId": new ObjectID("578f2baa12eaebabec4af28c"),"Name":"Manjimup Dried Apples","Discontinued":true}, 54 | {"_id": new ObjectID("578f2b8c12eaebabec4af26f"),"QuantityPerUnit":"16 - 2 kg boxes","UnitPrice":7.0,"CategoryId": new ObjectID("578f2baa12eaebabec4af28a"),"Name":"Filo Mix","Discontinued":false}, 55 | {"_id": new ObjectID("578f2b8c12eaebabec4af270"),"QuantityPerUnit":"24 - 250 g pkgs.","UnitPrice":38.0,"CategoryId": new ObjectID("578f2baa12eaebabec4af28a"),"Name":"Gnocchi di nonna Alice","Discontinued":true}, 56 | {"_id": new ObjectID("578f2b8c12eaebabec4af271"),"QuantityPerUnit":"24 - 250 g pkgs.","UnitPrice":19.5,"CategoryId": new ObjectID("578f2baa12eaebabec4af28a"),"Name":"Ravioli Angelo","Discontinued":false}, 57 | {"_id": new ObjectID("578f2b8c12eaebabec4af272"),"QuantityPerUnit":"24 pieces","UnitPrice":13.25,"CategoryId": new ObjectID("578f2baa12eaebabec4af28d"),"Name":"Escargots de Bourgogne","Discontinued":false}, 58 | {"_id": new ObjectID("578f2b8c12eaebabec4af273"),"QuantityPerUnit":"5 kg pkg.","UnitPrice":55.0,"CategoryId": new ObjectID("578f2baa12eaebabec4af28f"),"Name":"Raclette Courdavault","Discontinued":false}, 59 | {"_id": new ObjectID("578f2b8c12eaebabec4af274"),"QuantityPerUnit":"15 - 300 g rounds","UnitPrice":34.0,"CategoryId": new ObjectID("578f2baa12eaebabec4af28f"),"Name":"Camembert Pierrot","Discontinued":true}, 60 | {"_id": new ObjectID("578f2b8c12eaebabec4af275"),"QuantityPerUnit":"24 - 500 ml bottles","UnitPrice":28.5,"CategoryId": new ObjectID("578f2baa12eaebabec4af28e"),"Name":"Sirop d'érable","Discontinued":true}, 61 | {"_id": new ObjectID("578f2b8c12eaebabec4af276"),"QuantityPerUnit":"48 pies","UnitPrice":49.3,"CategoryId": new ObjectID("578f2baa12eaebabec4af290"),"Name":"Tarte au sucre","Discontinued":false}, 62 | {"_id": new ObjectID("578f2b8c12eaebabec4af277"),"QuantityPerUnit":"15 - 625 g jars","UnitPrice":43.9,"CategoryId": new ObjectID("578f2baa12eaebabec4af28e"),"Name":"Vegie-spread","Discontinued":false}, 63 | {"_id": new ObjectID("578f2b8c12eaebabec4af278"),"QuantityPerUnit":"20 bags x 4 pieces","UnitPrice":33.25,"CategoryId": new ObjectID("578f2baa12eaebabec4af28a"),"Name":"Wimmers gute Semmelknödel","Discontinued":true}, 64 | {"_id": new ObjectID("578f2b8c12eaebabec4af279"),"QuantityPerUnit":"32 - 8 oz bottles","UnitPrice":21.05,"CategoryId": new ObjectID("578f2baa12eaebabec4af28e"),"Name":"Louisiana Fiery Hot Pepper Sauce","Discontinued":true}, 65 | {"_id": new ObjectID("578f2b8c12eaebabec4af27a"),"QuantityPerUnit":"24 - 8 oz jars","UnitPrice":17.0,"CategoryId": new ObjectID("578f2baa12eaebabec4af28e"),"Name":"Louisiana Hot Spiced Okra","Discontinued":false}, 66 | {"_id": new ObjectID("578f2b8c12eaebabec4af27b"),"QuantityPerUnit":"24 - 12 oz bottles","UnitPrice":14.0,"CategoryId": new ObjectID("578f2baa12eaebabec4af289"),"Name":"Laughing Lumberjack Lager","Discontinued":true}, 67 | {"_id": new ObjectID("578f2b8c12eaebabec4af27c"),"QuantityPerUnit":"10 boxes x 8 pieces","UnitPrice":12.5,"CategoryId": new ObjectID("578f2baa12eaebabec4af290"),"Name":"Scottish Longbreads","Discontinued":false}, 68 | {"_id": new ObjectID("578f2b8c12eaebabec4af27d"),"QuantityPerUnit":"Crate","UnitPrice":666,"CategoryId": new ObjectID("578f2baa12eaebabec4af28f"),"Name":"MyProduct","Discontinued":true}, 69 | {"_id": new ObjectID("578f2b8c12eaebabec4af27e"),"QuantityPerUnit":"24 - 355 ml bottles","UnitPrice":15.0,"CategoryId": new ObjectID("578f2baa12eaebabec4af289"),"Name":"Outback Lager","Discontinued":false}, 70 | {"_id": new ObjectID("578f2b8c12eaebabec4af27f"),"QuantityPerUnit":"10 - 500 g pkgs.","UnitPrice":21.5,"CategoryId": new ObjectID("578f2baa12eaebabec4af28f"),"Name":"Flotemysost","Discontinued":false}, 71 | {"_id": new ObjectID("578f2b8c12eaebabec4af280"),"QuantityPerUnit":"24 - 200 g pkgs.","UnitPrice":34.8,"CategoryId": new ObjectID("578f2baa12eaebabec4af28f"),"Name":"Mozzarella di Giovanni","Discontinued":false}, 72 | {"_id": new ObjectID("578f2b8c12eaebabec4af281"),"QuantityPerUnit":"24 - 150 g jars","UnitPrice":15.0,"CategoryId": new ObjectID("578f2baa12eaebabec4af28d"),"Name":"Röd Kaviar","Discontinued":false}, 73 | {"_id": new ObjectID("578f2b8c12eaebabec4af282"),"QuantityPerUnit":"48 pieces","UnitPrice":32.8,"CategoryId": new ObjectID("578f2baa12eaebabec4af28b"),"Name":"Perth Pasties","Discontinued":true}, 74 | {"_id": new ObjectID("578f2b8c12eaebabec4af283"),"QuantityPerUnit":"16 pies","UnitPrice":7.45,"CategoryId": new ObjectID("578f2baa12eaebabec4af28b"),"Name":"Tourtière","Discontinued":true}, 75 | {"_id": new ObjectID("578f2b8c12eaebabec4af284"),"QuantityPerUnit":"24 boxes x 2 pies","UnitPrice":24.0,"CategoryId": new ObjectID("578f2baa12eaebabec4af28b"),"Name":"Pâté chinois","Discontinued":true}, 76 | {"_id": new ObjectID("578f2b8c12eaebabec4af285"),"QuantityPerUnit":"5 kg pkg.","UnitPrice":10.0,"CategoryId": new ObjectID("578f2baa12eaebabec4af28c"),"Name":"Longlife Tofu","Discontinued":false}, 77 | {"_id": new ObjectID("578f2b8c12eaebabec4af286"),"QuantityPerUnit":"24 - 0.5 l bottles","UnitPrice":7.75,"CategoryId": new ObjectID("578f2baa12eaebabec4af289"),"Name":"Rhönbräu Klosterbier","Discontinued":true}, 78 | {"_id": new ObjectID("578f2b8c12eaebabec4af287"),"QuantityPerUnit":"500 ml","UnitPrice":18.0,"CategoryId": new ObjectID("578f2baa12eaebabec4af289"),"Name":"Lakkalikööri","Discontinued":false}, 79 | {"_id": new ObjectID("578f2b8c12eaebabec4af288"),"QuantityPerUnit":"12 boxes","UnitPrice":13.0,"CategoryId": new ObjectID("578f2baa12eaebabec4af28e"),"Name":"Original Frankfurter grüne Soße","Discontinued":false} 80 | ]; -------------------------------------------------------------------------------- /src/test/odata.spec.ts: -------------------------------------------------------------------------------- 1 | /// 2 | import { NoServer, AuthenticationServer } from './test.model'; 3 | import { Edm, odata } from "../lib/index"; 4 | const { expect } = require("chai"); 5 | 6 | describe("Code coverage", () => { 7 | it("should return empty object when no public controllers on server", () => { 8 | expect(odata.getPublicControllers(NoServer)).to.deep.equal({}); 9 | }); 10 | 11 | it("should not allow non-OData methods", () => { 12 | try { 13 | NoServer.execute("/dev/null", "MERGE"); 14 | throw new Error("MERGE should not be allowed"); 15 | } catch (err) { 16 | expect(err.message).to.equal("Method not allowed."); 17 | } 18 | }); 19 | 20 | it("should throw resource not found error", () => { 21 | return AuthenticationServer.execute("/Users", "DELETE").then(() => { 22 | throw new Error("should throw error"); 23 | }, (err) => { 24 | expect(err.message).to.equal("Resource not found."); 25 | }); 26 | }); 27 | }); -------------------------------------------------------------------------------- /src/test/projection.spec.ts: -------------------------------------------------------------------------------- 1 | /// 2 | import { ODataServer, ODataController, odata, Edm, ODataMetadataType } from "../lib/index"; 3 | import * as request from 'request-promise'; 4 | const { expect } = require("chai"); 5 | 6 | class Address { 7 | @Edm.String 8 | City: string 9 | 10 | @Edm.String 11 | Address: string 12 | 13 | @Edm.String 14 | Zip: string 15 | 16 | @Edm.Int32 17 | Nr: number 18 | } 19 | 20 | class User { 21 | @Edm.Key 22 | @Edm.Int32 23 | Id: number 24 | 25 | @Edm.ComplexType(Address) 26 | Address: Address 27 | } 28 | 29 | @odata.type(User) 30 | class UsersController extends ODataController{ 31 | @odata.GET 32 | users(){ 33 | return [{ 34 | Id: 1, 35 | Address: { 36 | City: "Gadgetzan", 37 | Address: "Mean Street", 38 | Zip: "1234", 39 | Nr: 1234 40 | } 41 | }] 42 | } 43 | } 44 | 45 | @odata.controller(UsersController, true) 46 | class TestServer extends ODataServer{} 47 | 48 | describe("OData projection", () => { 49 | it("should return projected entities when using $select", () => { 50 | return TestServer.execute("/Users?$select=Id").then(result => expect(result).to.deep.equal({ 51 | statusCode: 200, 52 | body: { 53 | "@odata.context": "http://localhost/$metadata#Users(Id)", 54 | value: [{ 55 | "@odata.id": "http://localhost/Users(1)", 56 | Id: 1 57 | }] 58 | }, 59 | contentType: "application/json", 60 | elementType: User 61 | })); 62 | }); 63 | 64 | it("should return projected entities with complex type when using $select", () => { 65 | return TestServer.execute("/Users?$select=Address").then(result => expect(result).to.deep.equal({ 66 | statusCode: 200, 67 | body: { 68 | "@odata.context": "http://localhost/$metadata#Users(Address)", 69 | value: [{ 70 | "@odata.id": "http://localhost/Users(1)", 71 | Address: { 72 | City: "Gadgetzan", 73 | Address: "Mean Street", 74 | Zip: "1234", 75 | Nr: 1234 76 | } 77 | }] 78 | }, 79 | contentType: "application/json", 80 | elementType: User 81 | })); 82 | }); 83 | 84 | it("should return projected entities with projected complex type when using $select", () => { 85 | return TestServer.execute("/Users?$select=Address/City").then(result => expect(result).to.deep.equal({ 86 | statusCode: 200, 87 | body: { 88 | "@odata.context": "http://localhost/$metadata#Users(Address/City)", 89 | value: [{ 90 | "@odata.id": "http://localhost/Users(1)", 91 | Address: { 92 | City: "Gadgetzan" 93 | } 94 | }] 95 | }, 96 | contentType: "application/json", 97 | elementType: User 98 | })); 99 | }); 100 | 101 | it("should return projected entities with projected complex type when using $select and odata.metadata=full", () => { 102 | return TestServer.execute({ 103 | url: "/Users?$select=Address/City,Address/Nr", 104 | metadata: ODataMetadataType.full 105 | }).then(result => expect(result).to.deep.equal({ 106 | statusCode: 200, 107 | body: { 108 | "@odata.context": "http://localhost/$metadata#Users(Address/City,Address/Nr)", 109 | value: [{ 110 | "@odata.id": "http://localhost/Users(1)", 111 | "@odata.type": "#Default.User", 112 | Address: { 113 | "@odata.type": "#Default.Address", 114 | City: "Gadgetzan", 115 | Nr: 1234, 116 | "Nr@odata.type": "#Int32" 117 | }, 118 | "Address@odata.type": "#Default.Address" 119 | }] 120 | }, 121 | contentType: "application/json", 122 | elementType: User 123 | })); 124 | }); 125 | }); -------------------------------------------------------------------------------- /src/test/utils/queryOptions.ts: -------------------------------------------------------------------------------- 1 | import { Token, TokenType } from "odata-v4-parser/lib/lexer"; 2 | 3 | export const processQueries = async (_query: Token) => { 4 | const query = await clone(_query); 5 | return new Promise((resolve) => { 6 | const options = { skipNumber : 0, topNumber : 0, orderby: { fields: [], order: "" } } 7 | if (query && query.value && query.value.options) { 8 | for (let token of query.value.options) { 9 | if (token.type === "Skip") options.skipNumber = token.value.raw; 10 | if (token.type === "Top") options.topNumber = token.value.raw; 11 | if (token.type === "OrderBy") { 12 | const raw = decodeURIComponent(token.raw).replace(/'/g, '').replace(/\"/g, "") 13 | options.orderby.fields = raw.split("=")[1].split(" ")[0].split(","); 14 | options.orderby.order = raw.split("=")[1].split(" ")[1] || "asc"; 15 | } 16 | } 17 | return resolve(options); 18 | } 19 | return resolve(options); 20 | }) 21 | } 22 | 23 | export const doOrderby = async (_response: any[], _options: any) => { 24 | const response = await clone(_response); 25 | const options = await clone(_options); 26 | return new Promise((resolve) => { 27 | if (options.orderby && !!options.orderby.fields && !!options.orderby.order) { 28 | const sorted = response.sort((a, b) => { 29 | if (a[`${options.orderby.fields[0]}`] > b[`${options.orderby.fields[0]}`]) { 30 | return options.orderby.order === "asc" ? 1 : -1; 31 | } 32 | if (a[`${options.orderby.fields[0]}`] < b[`${options.orderby.fields[0]}`]) { 33 | return options.orderby.order === "asc" ? -1 : 1; 34 | } 35 | return 0; 36 | }) 37 | return resolve(sorted); 38 | } 39 | return resolve(response); 40 | }) 41 | } 42 | 43 | export const doSkip = async (_response: any[], _options: any) => { 44 | const response = await clone(_response); 45 | const options = await clone(_options); 46 | return new Promise((resolve) => { 47 | if (options.skipNumber > 0) return resolve(response.filter((c, idx) => idx >= options.skipNumber)); 48 | return resolve(response); 49 | }) 50 | } 51 | 52 | export const doTop = async (_response: any[], _options: any) => { 53 | const response = await clone(_response); 54 | const options = await clone(_options); 55 | return new Promise((resolve) => { 56 | if (options.topNumber > 0) return resolve(response.filter((c, idx) => idx < options.topNumber)); 57 | return resolve(response); 58 | }) 59 | } 60 | 61 | export const clone = (object: any) => { 62 | return JSON.parse(JSON.stringify(object)); 63 | } -------------------------------------------------------------------------------- /src/test/validator.spec.ts: -------------------------------------------------------------------------------- 1 | /// 2 | import { ODataController, ODataServer, ODataQuery, odata, HttpRequestError } from "../lib/index"; 3 | 4 | class ValidationError extends HttpRequestError{ 5 | constructor(){ 6 | super(400, "ODataValidationError"); 7 | } 8 | } 9 | 10 | class MyCustomValidation{ 11 | static validate(query:string | ODataQuery){ 12 | // throw error when using any query 13 | if ((typeof query == "object" && query && query.type != "ODataUri") || typeof query == "string") throw new ValidationError(); 14 | } 15 | } 16 | 17 | class BaseController extends ODataController{ 18 | @odata.GET 19 | query(@odata.query ast:ODataQuery){ 20 | return []; 21 | } 22 | 23 | @odata.GET 24 | filter(@odata.key id:number, @odata.filter ast:ODataQuery){ 25 | return {}; 26 | } 27 | } 28 | 29 | @odata.validation(MyCustomValidation, {}) 30 | class ValidationController extends BaseController{} 31 | 32 | class NoValidationController extends BaseController{} 33 | 34 | @odata.validation(MyCustomValidation, {}) 35 | @odata.controller(ValidationController, true) 36 | @odata.controller(NoValidationController, true) 37 | class ValidationServer extends ODataServer{} 38 | 39 | describe("ODataValidation", () => { 40 | it("should throw validation error (@odata.query)", () => { 41 | return new Promise((resolve, reject) => { 42 | try{ 43 | ValidationServer.execute("/Validation?$filter=Id eq 1").then(() => { 44 | reject(new Error("should throw validation error")); 45 | }, (err) => { 46 | if (err instanceof ValidationError) return resolve(); 47 | reject(new Error("should throw validation error")); 48 | }).catch(err => { 49 | resolve(); 50 | }); 51 | }catch(err){ 52 | if (err instanceof ValidationError) return resolve(); 53 | reject(new Error("should throw validation error")); 54 | } 55 | }); 56 | }); 57 | 58 | it("should throw validation error (@odata.filter)", () => { 59 | return new Promise((resolve, reject) => { 60 | try{ 61 | ValidationServer.execute("/Validation(1)?$filter=Id eq 1").then(() => { 62 | reject(new Error("should throw validation error")); 63 | }, (err) => { 64 | if (err instanceof ValidationError) return resolve(); 65 | reject(new Error("should throw validation error")); 66 | }).catch(err => { 67 | resolve(); 68 | }); 69 | }catch(err){ 70 | if (err instanceof ValidationError) return resolve(); 71 | reject(new Error("should throw validation error")); 72 | } 73 | }); 74 | }); 75 | 76 | it("should pass without validation error (@odata.query)", () => { 77 | return new Promise((resolve, reject) => { 78 | ValidationServer.execute("/Validation").then(() => { 79 | resolve(); 80 | }, (err) => { 81 | if (err instanceof ValidationError) return reject(new Error("should pass without validation error")); 82 | resolve(); 83 | }); 84 | }); 85 | }); 86 | 87 | it("should pass without validation error (@odata.filter)", () => { 88 | return new Promise((resolve, reject) => { 89 | ValidationServer.execute("/Validation(1)").then(() => { 90 | resolve(); 91 | }, (err) => { 92 | if (err instanceof ValidationError) return reject(new Error("should pass without validation error")); 93 | resolve(); 94 | }); 95 | }); 96 | }); 97 | 98 | it("should pass without validation error (@odata.query without @odata.validation)", () => { 99 | return new Promise((resolve, reject) => { 100 | ValidationServer.execute("/NoValidation?$filter=Id eq 1").then(() => { 101 | resolve(); 102 | }, (err) => { 103 | if (err instanceof ValidationError) return reject(new Error("should pass without validation error")); 104 | resolve(); 105 | }); 106 | }); 107 | }); 108 | 109 | it("should pass without validation error (@odata.filter without @odata.validation)", () => { 110 | return new Promise((resolve, reject) => { 111 | ValidationServer.execute("/NoValidation(1)?$filter=Id eq 1").then(() => { 112 | resolve(); 113 | }, (err) => { 114 | if (err instanceof ValidationError) return reject(new Error("should pass without validation error")); 115 | resolve(); 116 | }); 117 | }); 118 | }); 119 | }); -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es6", 4 | "module": "commonjs", 5 | "declaration": true, 6 | "experimentalDecorators": true, 7 | "outDir": "build", 8 | "noUnusedLocals": false, 9 | "noUnusedParameters": false, 10 | "noImplicitAny": false, 11 | "noImplicitReturns": true, 12 | "noImplicitThis": true, 13 | "noEmitHelpers": true, 14 | "importHelpers": true, 15 | "sourceMap": true 16 | }, 17 | "exclude": [ 18 | "node_modules", 19 | "build" 20 | ] 21 | } -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "rules": { 3 | "no-unused-expression": true, 4 | "no-duplicate-variable": true, 5 | "class-name": true, 6 | "semicolon": ["always", "ignore-interfaces"], 7 | "no-consecutive-blank-lines": [true] 8 | } 9 | } --------------------------------------------------------------------------------