From 91c694d4a48fbeba3a099b58477b7406ddcb2a4d Mon Sep 17 00:00:00 2001 From: Theo Sun Date: Wed, 7 Jul 2021 17:43:17 +0800 Subject: [PATCH] feat: support odata v2 --- .vscode/launch.json | 19 ++++++++ .vscode/settings.json | 4 ++ package-lock.json | 38 ++++++++-------- package.json | 2 +- src/builder/filter.ts | 88 ++++++++++++++++++++++++------------- src/builder/param.ts | 18 ++++++-- src/builder/types.ts | 19 +++++++- test/builder/filter.test.ts | 18 ++++---- test/builder/params.test.ts | 5 +++ 9 files changed, 147 insertions(+), 64 deletions(-) create mode 100644 .vscode/launch.json diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000..5fdfce8 --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,19 @@ +{ + "version": "0.2.0", + "configurations": [ + { + "type": "node", + "name": "vscode-jest-tests", + "request": "launch", + "args": [ + "--runInBand", + "--watchAll=false" + ], + "cwd": "${workspaceFolder}", + "console": "integratedTerminal", + "internalConsoleOptions": "neverOpen", + "disableOptimisticBPs": true, + "program": "${workspaceFolder}/node_modules/.bin/jest" + } + ] +} \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json index b45c7ab..3a8d953 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -6,6 +6,10 @@ "jest.autoEnable": false, "cSpell.words": [ "Theo", + "allpages", + "datetime", + "datetimeoffset", + "inlinecount", "odata" ] } \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 3981207..b84a69b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1053,12 +1053,12 @@ } }, "@odata/metadata": { - "version": "0.2.5", - "resolved": "https://registry.npmjs.org/@odata/metadata/-/metadata-0.2.5.tgz", - "integrity": "sha512-OkJYVban1qDH10xHM2b4wNqemDuIY8e3KZUiFBpdEW9St8qH3i4314+gduJhCg6Wx+k2i+dBI4v0c5NpAb2JDg==", + "version": "0.2.6", + "resolved": "https://registry.npmjs.org/@odata/metadata/-/metadata-0.2.6.tgz", + "integrity": "sha512-WgAgeIMWdbDbmoDm0Ce68Zl3/OlVKMND/fCpJLPZinkNXCAToCehyKeR3v0eT4TrDmOYdOHuJenTZR/tTB9zCw==", "requires": { "@newdash/newdash": "^5.19.0", - "@types/express": "^4.17.12", + "@types/express": "^4.17.13", "reflect-metadata": "^0.1.13" } }, @@ -1128,26 +1128,26 @@ } }, "@types/body-parser": { - "version": "1.19.0", - "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.0.tgz", - "integrity": "sha512-W98JrE0j2K78swW4ukqMleo8R7h/pFETjM2DQ90MF6XK2i4LO4W3gQ71Lt4w3bfm2EvVSyWHplECvB5sK22yFQ==", + "version": "1.19.1", + "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.1.tgz", + "integrity": "sha512-a6bTJ21vFOGIkwM0kzh9Yr89ziVxq4vYH2fQ6N8AeipEzai/cFK6aGMArIkUeIdRIgpwQa+2bXiLuUJCpSf2Cg==", "requires": { "@types/connect": "*", "@types/node": "*" } }, "@types/connect": { - "version": "3.4.34", - "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.34.tgz", - "integrity": "sha512-ePPA/JuI+X0vb+gSWlPKOY0NdNAie/rPUqX2GUPpbZwiKTkSPhjXWuee47E4MtE54QVzGCQMQkAL6JhV2E1+cQ==", + "version": "3.4.35", + "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.35.tgz", + "integrity": "sha512-cdeYyv4KWoEgpBISTxWvqYsVy444DOqehiF3fM3ne10AmJ62RSyNkUnxMJXHQWRQQX2eR94m5y1IZyDwBjV9FQ==", "requires": { "@types/node": "*" } }, "@types/express": { - "version": "4.17.12", - "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.12.tgz", - "integrity": "sha512-pTYas6FrP15B1Oa0bkN5tQMNqOcVXa9j4FTFtO8DWI9kppKib+6NJtfTOOLcwxuuYvcX2+dVG6et1SxW/Kc17Q==", + "version": "4.17.13", + "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.13.tgz", + "integrity": "sha512-6bSZTPaTIACxn48l50SR+axgrqm6qXFIxrdAKaG6PaJk3+zuUr35hBlgT7vOmJcum+OEaIBLtHV/qloEAFITeA==", "requires": { "@types/body-parser": "*", "@types/express-serve-static-core": "^4.17.18", @@ -1156,9 +1156,9 @@ } }, "@types/express-serve-static-core": { - "version": "4.17.21", - "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.17.21.tgz", - "integrity": "sha512-gwCiEZqW6f7EoR8TTEfalyEhb1zA5jQJnRngr97+3pzMaO1RKoI1w2bw07TK72renMUVWcWS5mLI6rk1NqN0nA==", + "version": "4.17.23", + "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.17.23.tgz", + "integrity": "sha512-WYqTtTPTJn9kXMdnAH5HPPb7ctXvBpP4PfuOb8MV4OHPQWHhDZixGlhgR159lJPpKm23WOdoCkt2//cCEaOJkw==", "requires": { "@types/node": "*", "@types/qs": "*", @@ -1253,9 +1253,9 @@ "integrity": "sha512-ewFXqrQHlFsgc09MK5jP5iR7vumV/BYayNC6PgJO2LPe8vrnNFyjQjSppfEngITi0qvfKtzFvgKymGheFM9UOA==" }, "@types/serve-static": { - "version": "1.13.9", - "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.13.9.tgz", - "integrity": "sha512-ZFqF6qa48XsPdjXV5Gsz0Zqmux2PerNd3a/ktL45mHpa19cuMi/cL8tcxdAx497yRh+QtYPuofjT9oWw9P7nkA==", + "version": "1.13.10", + "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.13.10.tgz", + "integrity": "sha512-nCkHGI4w7ZgAdNkrEu0bv+4xNV/XDqW+DydknebMOQwkpDGx8G+HTlj7R7ABI8i8nKxVw0wtKPi1D+lPOkh4YQ==", "requires": { "@types/mime": "^1", "@types/node": "*" diff --git a/package.json b/package.json index 9addf05..045bd65 100644 --- a/package.json +++ b/package.json @@ -48,7 +48,7 @@ "typescript": "^4.3.5" }, "dependencies": { - "@odata/metadata": "^0.2.5", + "@odata/metadata": "^0.2.6", "@newdash/newdash": "^5.19.0" }, "keywords": [ diff --git a/src/builder/filter.ts b/src/builder/filter.ts index f0f6cfd..61be997 100644 --- a/src/builder/filter.ts +++ b/src/builder/filter.ts @@ -1,6 +1,6 @@ import join from '@newdash/newdash/join'; import { Edm } from '@odata/metadata'; -import { convertPrimitiveValueToString } from './types'; +import { convertPrimitiveValueToString, ODataVersion } from './types'; export enum ExprOperator { eq = 'eq', @@ -8,12 +8,12 @@ export enum ExprOperator { gt = 'gt', lt = 'lt', ge = 'ge', - le = 'le', + le = 'le' } type FieldExpr = { op: ExprOperator; - value: string; + value: any; }; type FieldExprMappings = { @@ -52,33 +52,15 @@ class ODataFieldExpr { private _addExpr(op: ExprOperator, value: any) { if (value === null) { this._getFieldExprs().push({ op, value: 'null' }); + return; } switch (typeof value) { case 'number': case 'boolean': - this._getFieldExprs().push({ op, value: `${value}` }); - break; case 'string': - if (value.startsWith("'") || value.startsWith('datetime')) { - this._getFieldExprs().push({ op, value }); - } else { - this._getFieldExprs().push({ op, value: `'${value}'` }); - } - break; case 'object': - if (value instanceof Edm.PrimitiveTypeValue) { - this._getFieldExprs().push({ - op, - value: convertPrimitiveValueToString(value) - }); - } else { - throw new Error( - `Not support object ${ - value?.constructor?.name || typeof value - } in odata filter eq/ne/gt/ge/ne/nt ...` - ); - } + this._getFieldExprs().push({ op, value }); break; case 'undefined': throw new Error( @@ -228,28 +210,72 @@ export class ODataFilter { return new ODataFieldExpr(this, name as string, this.getExprMapping()); } - toString(): string { - return this.build(); + public toString(version: ODataVersion = 'v4'): string { + return this.build(version); + } + + private _buildExprLit(value: any, version: ODataVersion = 'v4') { + if (value === null) { + return 'null'; + } + + switch (typeof value) { + case 'number': + case 'boolean': + return `${value}`; + case 'string': + if (value.startsWith("'") || value.startsWith('datetime')) { + return value; + } + return `'${value}'`; + case 'object': + if (value instanceof Edm.PrimitiveTypeValue) { + return convertPrimitiveValueToString(value, version); + } + throw new Error( + `Not support object ${ + value?.constructor?.name || typeof value + } in odata filter eq/ne/gt/ge/ne/nt ...` + ); + + case 'undefined': + throw new Error( + `You must set value in odata filter eq/ne/gt/ge/ne/nt ...` + ); + default: + throw new Error( + `Not support typeof ${typeof value}: ${value} in odata filter eq/ne/gt/ge/ne/nt ...` + ); + } } - protected _buildFieldExprString(field: string): string { + protected _buildFieldExprString( + field: string, + version: ODataVersion = 'v4' + ): string { const exprs = this.getExprMapping()[field]; if (exprs.length > 0) { if (exprs.filter((expr) => expr.op == ExprOperator.eq).length == 0) { return `(${join( - exprs.map(({ op, value }) => `${field} ${op} ${value}`), + exprs.map( + ({ op, value }) => + `${field} ${op} ${this._buildExprLit(value, version)}` + ), ' and ' )})`; } return `(${join( - exprs.map(({ op, value }) => `${field} ${op} ${value}`), + exprs.map( + ({ op, value }) => + `${field} ${op} ${this._buildExprLit(value, version)}` + ), ' or ' )})`; } return ''; } - build(): string { + public build(version: ODataVersion = 'v4'): string { let _rt = ''; _rt = join( // join all fields exprs string @@ -261,10 +287,10 @@ export class ODataFilter { // only have one expr case 1: const { op, value } = exprs[0]; - return `${fieldName} ${op} ${value}`; + return `${fieldName} ${op} ${this._buildExprLit(value, version)}`; default: // multi exprs - return this._buildFieldExprString(fieldName); + return this._buildFieldExprString(fieldName, version); } }), ' and ' diff --git a/src/builder/param.ts b/src/builder/param.ts index 4eb8214..c44812d 100644 --- a/src/builder/param.ts +++ b/src/builder/param.ts @@ -3,6 +3,7 @@ import isArray from '@newdash/newdash/isArray'; import join from '@newdash/newdash/join'; import uniq from '@newdash/newdash/uniq'; import { ODataFilter } from './filter'; +import { ODataVersion } from './types'; class SearchParams { _store = new Map(); @@ -197,7 +198,7 @@ export class ODataQueryParam { return this; } - toString(): string { + toString(version: ODataVersion = 'v4'): string { const rt = new SearchParams(); if (this.$format) { rt.append('$format', this.$format); @@ -223,8 +224,19 @@ export class ODataQueryParam { if (this.$expand && this.$expand.length > 0) { rt.append('$expand', this.$expand.join(',')); } - if (this.$count) { - rt.append('$count', 'true'); + switch (version) { + case 'v2': + if (this.$count) { + rt.append('$inlinecount', 'allpages'); + } + break; + case 'v4': + if (this.$count) { + rt.append('$count', 'true'); + } + break; + default: + break; } return rt.toString(); } diff --git a/src/builder/types.ts b/src/builder/types.ts index b24662e..e438de4 100644 --- a/src/builder/types.ts +++ b/src/builder/types.ts @@ -1,11 +1,16 @@ import { Edm } from '@odata/metadata'; +export type ODataVersion = 'v2' | 'v4'; + /** * * @param value primitive literal value * @returns the string representation */ -export function convertPrimitiveValueToString(value: Edm.PrimitiveTypeValue) { +export function convertPrimitiveValueToString( + value: Edm.PrimitiveTypeValue, + version: ODataVersion = 'v4' +) { if (value?.getValue?.() === null) { return 'null'; } @@ -35,11 +40,23 @@ export function convertPrimitiveValueToString(value: Edm.PrimitiveTypeValue) { case Edm.Duration: // TODO integrate with some other duration lib return value.getValue(); + case Edm.DateTime: + let vd = value.getValue(); + if (typeof vd === 'string') { + vd = new Date(vd); + } + if (version === 'v2') { + return `datetime'${vd.toISOString()}'`; + } + throw new Error("OData V4 is not support 'Edm.DateTime' values"); case Edm.DateTimeOffset: let v1 = value.getValue(); if (typeof v1 === 'string') { v1 = new Date(v1); } + if (version === 'v2') { + return `datetimeoffset'${v1.toISOString()}'`; + } return v1.toISOString(); case Edm.Date: const v2 = value.getValue(); diff --git a/test/builder/filter.test.ts b/test/builder/filter.test.ts index d384029..0624c2f 100644 --- a/test/builder/filter.test.ts +++ b/test/builder/filter.test.ts @@ -5,7 +5,7 @@ describe('OData Query Builder - Filter Test Suite', () => { it('should support filter by value/name', () => { expect(ODataFilter.New().field('A').eq('a').toString()).toBe("A eq 'a'"); - expect(ODataFilter.New().field('A').eq(literalValues.String("a")).toString()).toBe("A eq 'a'"); + expect(ODataFilter.New().field('A').eq(literalValues.String('a')).toString()).toBe("A eq 'a'"); expect(ODataFilter.New().field('A').eq(1).toString()).toBe('A eq 1'); }); @@ -30,21 +30,21 @@ describe('OData Query Builder - Filter Test Suite', () => { it('should support filter guid', () => { expect( filter() - .field("A") - .eq(literalValues.Guid("253f842d-d739-41b8-ac8c-139ac7a9dd14")) + .field('A') + .eq(literalValues.Guid('253f842d-d739-41b8-ac8c-139ac7a9dd14')) .build() - ).toBe("A eq 253f842d-d739-41b8-ac8c-139ac7a9dd14") + ).toBe('A eq 253f842d-d739-41b8-ac8c-139ac7a9dd14'); expect( - filter({ A: literalValues.Guid("253f842d-d739-41b8-ac8c-139ac7a9dd14") }) + filter({ A: literalValues.Guid('253f842d-d739-41b8-ac8c-139ac7a9dd14') }) .build() - ).toBe("A eq 253f842d-d739-41b8-ac8c-139ac7a9dd14") + ).toBe('A eq 253f842d-d739-41b8-ac8c-139ac7a9dd14'); }); it('should support filter with type', () => { - expect(filter({ A: 1 }).build()).toBe("A eq 1") - expect(filter({ A: literalValues.String('1') }).build()).toBe("A eq '1'") - expect(filter({ A: literalValues.Guid("253f842d-d739-41b8-ac8c-139ac7a9dd14") }).build()).toBe("A eq 253f842d-d739-41b8-ac8c-139ac7a9dd14") + expect(filter({ A: 1 }).build()).toBe('A eq 1'); + expect(filter({ A: literalValues.String('1') }).build()).toBe("A eq '1'"); + expect(filter({ A: literalValues.Guid('253f842d-d739-41b8-ac8c-139ac7a9dd14') }).build()).toBe('A eq 253f842d-d739-41b8-ac8c-139ac7a9dd14'); }); }); diff --git a/test/builder/params.test.ts b/test/builder/params.test.ts index 0e700ac..81f7c5c 100644 --- a/test/builder/params.test.ts +++ b/test/builder/params.test.ts @@ -78,4 +78,9 @@ describe('ODataParams Test', () => { expect(param().top(1).toString()).toBe('$top=1'); }); + it('should support params with count', () => { + expect(param().count(true).toString('v2')).toBe('$inlinecount=allpages'); + expect(param().count(true).toString('v4')).toBe('$count=true'); + }); + });