From d7497521e5d1dfda77f9b8348b9d2afb2b707c65 Mon Sep 17 00:00:00 2001 From: Kyriakos Barbounakis Date: Mon, 28 Oct 2024 18:58:50 +0200 Subject: [PATCH] Use sync open data parser (#113) * implement sync open data parser * implement expression as super class (#112) * update type declarations * 2.5.30 --- expressions.js | 5 - index.d.ts | 1 + index.js | 3 + odata.d.ts | 5 +- odata.js | 19 +- package-lock.json | 22 +- package.json | 5 +- simple-open-data-parser.d.ts | 17 ++ simple-open-data-parser.js | 403 ++++++++++++++++++++++++++++++ spec/SimpleOpenDataParser.spec.js | 177 +++++++++++++ 10 files changed, 639 insertions(+), 18 deletions(-) create mode 100644 simple-open-data-parser.d.ts create mode 100644 simple-open-data-parser.js create mode 100644 spec/SimpleOpenDataParser.spec.js diff --git a/expressions.js b/expressions.js index 271131e..e65152f 100644 --- a/expressions.js +++ b/expressions.js @@ -201,8 +201,6 @@ MethodCallExpression.prototype.exprOf = function() { var name = '$'.concat(this.name); //set arguments array method[name] = [] ; - if (this.args.length===0) - throw new Error('Unsupported method expression. Method arguments cannot be empty.'); method[name].push.apply(method[name], this.args.map(function (arg) { if (typeof arg.exprOf === 'function') { return arg.exprOf(); @@ -306,9 +304,6 @@ LangUtils.inherits(SimpleMethodCallExpression, MethodCallExpression); SimpleMethodCallExpression.prototype.exprOf = function() { var method = {}; var name = '$'.concat(this.name); - //set arguments array - if (this.args.length === 0) - throw new Error('Unsupported method expression. Method arguments cannot be empty.'); if (this.args.length === 1) { method[name] = {}; var arg; diff --git a/index.d.ts b/index.d.ts index b7579c0..ab6ebdc 100644 --- a/index.d.ts +++ b/index.d.ts @@ -6,3 +6,4 @@ export * from "./expressions"; export * from "./query"; export * from "./utils"; export * from "./object-name.validator"; +export * from "./simple-open-data-parser"; diff --git a/index.js b/index.js index cdf9cd6..453e251 100644 --- a/index.js +++ b/index.js @@ -6,6 +6,7 @@ var _expressions = require("./expressions"); var _query = require("./query"); var _utils = require("./utils"); var _validator = require("./object-name.validator"); +var { SimpleOpenDataParser } = require("./simple-open-data-parser"); module.exports.SqlFormatter = _formatter.SqlFormatter; @@ -58,3 +59,5 @@ module.exports.SqlUtils = _utils.SqlUtils; module.exports.ObjectNameValidator = _validator.ObjectNameValidator; module.exports.InvalidObjectNameError = _validator.InvalidObjectNameError; + +module.exports.SimpleOpenDataParser = SimpleOpenDataParser; diff --git a/odata.d.ts b/odata.d.ts index 037d594..4cf5a04 100644 --- a/odata.d.ts +++ b/odata.d.ts @@ -27,6 +27,7 @@ export declare interface OperatorType { } export declare class Token { + syntax: string; constructor(tokenType: string); static TokenType: TokenType; @@ -108,7 +109,7 @@ export declare class OpenDataParser { parse(str: string, callback: (err?: Error, res?: any) => void); parseAsync(str: string): Promise; - getOperator(token: string): string; + getOperator(token: Token | IdentifierToken | SyntaxToken | LiteralToken): string; moveNext(); expect(); expectAny(); @@ -190,4 +191,4 @@ export declare class OpenDataParser { }): Promise; -} \ No newline at end of file +} diff --git a/odata.js b/odata.js index d3e20ba..3b3d9a4 100644 --- a/odata.js +++ b/odata.js @@ -3,12 +3,12 @@ var _ = require("lodash"); var {trim} = require('lodash'); var {LangUtils} = require("@themost/common"); var {sprintf} = require('sprintf-js'); -var {SwitchExpression, SelectAnyExpression, OrderByAnyExpression, isLogicalOperator, +var {SwitchExpression, SelectAnyExpression, OrderByAnyExpression, AnyExpressionFormatter, isLogicalOperator, createLogicalExpression, isArithmeticOperator, createArithmeticExpression, isArithmeticExpression, isLogicalExpression, isComparisonOperator, createMemberExpression, createComparisonExpression, isMethodCallExpression, isMemberExpression} = require('./expressions'); -var {whilst} = require('async'); +var {whilst, series} = require('async'); const { MethodCallExpression } = require('./expressions'); /** * @class @@ -36,7 +36,7 @@ function OpenDataParser() { this.tokens = []; /** * Gets current token - * @type {Token} + * @type {Token | IdentifierToken | LiteralToken | SyntaxToken} */ this.currentToken = undefined; /** @@ -84,7 +84,7 @@ OpenDataParser.create = function() { /** * Gets the logical or arithmetic operator of the given token - * @param token + * @param {Token | IdentifierToken} token */ OpenDataParser.prototype.getOperator = function(token) { if (token.type===Token.TokenType.Identifier) { @@ -485,7 +485,7 @@ OpenDataParser.prototype.parseOrderBySequenceAsync = function(str) { /** * @param {{$select?:string,$filter?:string,$orderBy?:string,$groupBy?:string,$top:number,$skip:number}} queryOptions - * @param {function(Error,*)} callback + * @param {function(Error,*=)} callback */ OpenDataParser.prototype.parseQueryOptions = function(queryOptions, callback) { const self = this; @@ -588,7 +588,7 @@ OpenDataParser.prototype.parseCommon = function(callback) { if (self.atEnd()) { callback.call(self, null, result); } - //method call exception for [,] or [)] tokens e.g indexOf(Title,'...') + //method call exception for "," or "()" tokens e.g indexOf(Title,'...') else if ((self.currentToken.syntax===SyntaxToken.Comma.syntax) || (self.currentToken.syntax===SyntaxToken.ParenClose.syntax)) { callback.call(self, null, result); @@ -600,7 +600,7 @@ OpenDataParser.prototype.parseCommon = function(callback) { } else { self.moveNext(); - // if current operator is a logical operator ($or, $and etc) + // if current operator is a logical operator ($or, $and etc.) // parse right operand by using parseCommon() method // important note: the current expression probably is not using parentheses // e.g. (category eq 'Laptops' or category eq 'Desktops') and round(price,2) ge 500 and round(price,2) le 1000 @@ -903,7 +903,7 @@ OpenDataParser.prototype.parseMember = function(callback) { //format identifier identifier += '/' + this.currentToken.identifier; } - //support member to member comparison (with $it identifier e.g. $it/address/city or $it/category etc) + //support member to member comparison (with $it identifier e.g. $it/address/city or $it/category etc.) if (/^\$it\//.test(identifier)) { identifier= identifier.replace(/^\$it\//,''); } @@ -1577,6 +1577,7 @@ Token.Operator ={ */ function LiteralToken(value, literalType) { + // noinspection JSUnresolvedReference LiteralToken.super_.call(this, Token.TokenType.Literal); this.value = value; this.literalType = literalType; @@ -1623,6 +1624,7 @@ LiteralToken.Null = new LiteralToken(null, LiteralToken.LiteralType.Null); */ function IdentifierToken(name) { + // noinspection JSUnresolvedReference IdentifierToken.super_.call(this, Token.TokenType.Identifier); this.identifier = name; } @@ -1639,6 +1641,7 @@ IdentifierToken.prototype.valueOf = function() { */ function SyntaxToken(chr) { + // noinspection JSUnresolvedReference SyntaxToken.super_.call(this, Token.TokenType.Syntax); this.syntax = chr; } diff --git a/package-lock.json b/package-lock.json index 007098b..23c3da5 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@themost/query", - "version": "2.5.29", + "version": "2.5.30", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@themost/query", - "version": "2.5.29", + "version": "2.5.30", "license": "BSD-3-Clause", "dependencies": { "@themost/events": "^1.0.5", @@ -43,6 +43,9 @@ "source-map-support": "^0.5.21", "sql.js": "^1.8.0" }, + "engines": { + "node": ">=14" + }, "peerDependencies": { "@themost/common": "^2.5.0" } @@ -7279,6 +7282,21 @@ "is-typedarray": "^1.0.0" } }, + "node_modules/typescript": { + "version": "4.9.5", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.5.tgz", + "integrity": "sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==", + "dev": true, + "license": "Apache-2.0", + "peer": true, + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=4.2.0" + } + }, "node_modules/undici-types": { "version": "6.19.8", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz", diff --git a/package.json b/package.json index 8584d03..16b0b7e 100644 --- a/package.json +++ b/package.json @@ -1,11 +1,14 @@ { "name": "@themost/query", - "version": "2.5.29", + "version": "2.5.30", "description": "@themost/query is a query builder for SQL. It includes a wide variety of helper functions for building complex SQL queries under node.js.", "main": "index.js", "scripts": { "test": "jest" }, + "engines": { + "node": ">=14" + }, "peerDependencies": { "@themost/common": "^2.5.0" }, diff --git a/simple-open-data-parser.d.ts b/simple-open-data-parser.d.ts new file mode 100644 index 0000000..cbca3d7 --- /dev/null +++ b/simple-open-data-parser.d.ts @@ -0,0 +1,17 @@ +import { SyncSeriesEventEmitter } from "@themost/events"; +import { ExpressionBase, OrderByAnyExpression } from "./expressions"; +import { OpenDataParser } from "./odata"; + +export declare class SimpleOpenDataParser extends OpenDataParser { + + resolvingMember: SyncSeriesEventEmitter<{target: SimpleOpenDataParser, member: string}>; + resolvingMethod: SyncSeriesEventEmitter<{target: SimpleOpenDataParser, method: string}>; + + parseSync(data: string): ExpressionBase; + parseSelectSequenceSync(str: string): Array; + parseOrderBySequenceSync(str: string): Array; + parseGroupBySequenceSync(str: string): Array; + parseQueryOptionsSync(queryOptions: { $select?: string; $filter?: string; $expand?: string; $groupBy?: string; $orderBy?: string; $levels?: any; $top?: any; $skip?: any; }): any; + parseExpandSequenceSync(str: string): Array<{ name: string; options: { $select?: string; $filter?: string; $expand?: string; $groupBy?: string; $orderBy?: string; $levels?: any; $top?: any; $skip?: any; }; }>; + +} \ No newline at end of file diff --git a/simple-open-data-parser.js b/simple-open-data-parser.js new file mode 100644 index 0000000..69278a5 --- /dev/null +++ b/simple-open-data-parser.js @@ -0,0 +1,403 @@ +const { Token } = require('./odata'); +// eslint-disable-next-line no-unused-vars +const { ExpressionBase, SwitchExpression, isLogicalOperator, createMemberExpression, MethodCallExpression, SelectAnyExpression, OrderByAnyExpression } = require('./expressions'); +const { Args } = require("@themost/common"); +const { OpenDataParser, SyntaxToken } = require('./odata'); +const { SyncSeriesEventEmitter } = require('@themost/events'); + +class SimpleOpenDataParser extends OpenDataParser { + constructor() { + super(); + this.resolvingMember = new SyncSeriesEventEmitter(); + this.resolvingMethod = new SyncSeriesEventEmitter(); + } + + /** + * @protected + * @returns {import('./expressions').MemberExpression|undefined} + */ + parseMemberSync() { + if (this.tokens.length === 0) { + return; + } + if (this.currentToken.type !== 'Identifier') { + throw new Error('Expected identifier.'); + } + let identifier = this.currentToken.identifier; + while (this.nextToken && this.nextToken.syntax === SyntaxToken.Slash.syntax) { + //read syntax token + this.moveNext(); + //get next token + if (this.nextToken.type !== 'Identifier') { + throw new Error('Expected identifier.'); + } + //read identifier token + this.moveNext(); + //format identifier + identifier += '/' + this.currentToken.identifier; + } + //support member to member comparison (with $it identifier e.g. $it/address/city or $it/category etc) + if (/^\$it\//.test(identifier)) { + identifier = identifier.replace(/^\$it\//, ''); + } + const event = { + target: this, + member: identifier + } + this.resolvingMember.emit(event); + return createMemberExpression(event.member); + } + + /** + * @protected + * @returns {import('./expressions').MethodCallExpression|undefined} + */ + parseCommonItemSync() { + if (this.tokens.length===0) { + return; + } + let value; + switch (this.currentToken.type) { + case Token.TokenType.Identifier: + //if next token is an open parenthesis token and the current token is not an operator. current=indexOf, next=( + if (this.nextToken && this.nextToken.syntax === SyntaxToken.ParenOpen.syntax + && this.getOperator(this.currentToken) == null) { + //then parse method call + return this.parseMethodCallSync(); + } else if (this.getOperator(this.currentToken) === Token.Operator.Not){ + throw new Error('Not operator is not yet implemented.'); + } else { + const result = this.parseMemberSync(); + while (!this.atEnd() && this.currentToken.syntax === SyntaxToken.Slash.syntax) { + throw new Error('Slash syntax is not yet implemented.'); + } + this.moveNext(); + return result; + } + case Token.TokenType.Literal: + value = this.currentToken.value; + this.moveNext(); + return value; + case Token.TokenType.Syntax: + if (this.currentToken.syntax === SyntaxToken.Negative.syntax) { + throw new Error('Negative syntax is not yet implemented.'); + } + if (this.currentToken.syntax === SyntaxToken.ParenOpen.syntax) { + this.moveNext(); + const result = this.parseCommonSync(); + this.expect(SyntaxToken.ParenClose); + return result; + } + else { + throw new Error('Expected syntax.'); + } + default:break; + } + } + + /** + * @protected + * @returns {ExpressionBase|undefined} + */ + parseCommonSync() { + if (this.tokens.length===0) { + return; + } + let result = this.parseCommonItemSync(); + if (this.atEnd()) { + return result; + } else if ((this.currentToken.syntax===SyntaxToken.Comma.syntax) || + (this.currentToken.syntax===SyntaxToken.ParenClose.syntax)) { + return result; + } + // get current operator + let oper = this.getOperator(this.currentToken); + Args.check(oper != null, new Error('Expected operator.')); + this.moveNext(); + // if current operator is a logical operator ($or, $and etc) + // parse right operand by using parseCommon() method + // important note: the current expression probably is not using parentheses + // e.g. (category eq 'Laptops' or category eq 'Desktops') and round(price,2) ge 500 and round(price,2) le 1000 + // instead of (category eq 'Laptops' or category eq 'Desktops') and (round(price,2) ge 500) and (round(price,2) le 1000) + if (this.atEnd() === false && isLogicalOperator(oper)) { + // parse next expression + const expr = this.parseCommonSync(); + // return expression + return this.createExpression(result, oper, expr); + } + // otherwise, parse right operand by using parseCommonItemSync() method + const right = this.parseCommonItemSync(); + // create expression + const expr = this.createExpression(result, oper, right); + const atEnd = this.atEnd(); + if (!atEnd && (isLogicalOperator(this.getOperator(this.currentToken)))) { + oper = this.getOperator(this.currentToken); + this.moveNext(); + result = this.parseCommonSync(); + return this.createExpression(expr, oper, result); + } + return expr; + } + + /** + * @protected + * @returns {Expression|undefined} + */ + parseMethodCallSync() { + if (this.tokens.length === 0) { + return; + } + //get method name + const method = this.currentToken.identifier; + this.moveNext(); + this.expect(SyntaxToken.ParenOpen); + if (method === 'case') { + const expr = this.parseSwitchMethodBranchesSync([], undefined); + return new SwitchExpression(expr.branches, expr.defaultValue); + } + const args = []; + this.parseMethodCallArgumentsSync(args); + const event = { + target: this, + method: method, + args: args + } + this.resolvingMethod.emit(event); + return new MethodCallExpression(event.method, event.args); + } + + /** + * @protected + */ + parseMethodCallArgumentsSync(args) { + //ensure callback + args = args || []; + this.expectAny(); + if (this.currentToken.syntax===SyntaxToken.Comma.syntax) { + this.moveNext(); + this.expectAny(); + return this.parseMethodCallArgumentsSync(args); + } else if (this.currentToken.syntax===SyntaxToken.ParenClose.syntax) { + this.moveNext(); + return Array.from(arguments) + } + else { + const result = this.parseCommonItemSync(); + args.push(result); + return this.parseMethodCallArgumentsSync(args); + } + } + + /** + * @protected + * @param {*} branches + * @param {*} defaultValue + * @returns + */ + parseSwitchMethodBranchesSync(branches, defaultValue) { + /** + * @type {Token|SyntaxToken|LiteralToken|*} + */ + let currentToken = this.currentToken; + if (currentToken.type === Token.TokenType.Literal && + currentToken.value === true) { + this.moveNext(); + this.expect(SyntaxToken.Colon); + const defaultValue = this.parseCommonSync(); + return { + branches: branches, + defaultValue: defaultValue + }; + } + const caseExpr = this.parseCommonSync(); + this.expect(SyntaxToken.Colon); + const thenExpr = this.parseCommonSync(); + branches.push({ + case: caseExpr, + then: thenExpr + }); + currentToken = this.currentToken; + if (currentToken.type === Token.TokenType.Syntax && + currentToken.syntax === SyntaxToken.ParenClose) { + this.moveNext(); + return { + branches: branches + }; + } + this.expect(SyntaxToken.Comma); + return this.parseSwitchMethodBranchesSync(branches, defaultValue); + } + + /** + * Parses OData $select query option + * @param {string} str + * @returns {Array} + */ + parseSelectSequenceSync(str) { + this.source = str; + //get tokens + this.tokens = this.toList(); + //reset offset + this.offset = 0; this.current = 0; + const tokens = this.tokens; + const results = []; + if (tokens.length === 0) { + return results; + } + while(this.atEnd() === false) { + let offset = this.offset; + let result = this.parseCommonItemSync(); + if (this.currentToken && this.currentToken.type === Token.TokenType.Identifier && + this.currentToken.identifier.toLowerCase() === 'as') { + // get next token + this.moveNext(); + // get alias identifier + if (this.currentToken != null && this.currentToken.type === Token.TokenType.Identifier) { + result = new SelectAnyExpression(result, this.currentToken.identifier); + this.moveNext(); + } + } + Object.assign(result, { + source: this.getSource(offset, this.offset) + }); + results.push(result); + if (this.atEnd() === false && this.currentToken.syntax === SyntaxToken.Comma.syntax) { + this.moveNext(); + } + } + return results; + } + /** + * Parses OData $groupby query option e.g. $groupby=category or $groupby=category,year(dateReleased) etc. + * @param {string} str + * @returns {Array} + */ + parseGroupBySequenceSync(str) { + return this.parseSelectSequenceSync(str); + } + /** + * Parses OData $expand query option e.g. $expand=category,products or $expand=category($expand=products($select=name,price)) etc. + * @param {string} str + * @returns {Array} + */ + parseExpandSequenceSync(str) { + return this.parseExpandSequence(str); + } + + /** + * Parses OData $orderby query option e.g. $orderby=category asc, price desc or $orderby=round(price,2) asc etc. + * @param {string} str + * @returns {Array} + */ + parseOrderBySequenceSync(str) { + this.source = str; + //get tokens + this.tokens = this.toList(); + //reset offset + this.offset = 0; + this.current = 0; + const tokens = this.tokens; + const results = []; + if (tokens.length === 0) { + return results; + } + while(this.atEnd() === false) { + let offset = this.offset; + let result = this.parseCommonItemSync(); + let direction = 'asc'; + if (this.currentToken && this.currentToken.type === Token.TokenType.Identifier && + (this.currentToken.identifier.toLowerCase() === 'asc' || + this.currentToken.identifier.toLowerCase() === 'desc')) { + result.source = this.getSource(offset, this.offset); + direction = this.currentToken.identifier.toLowerCase(); + result = new OrderByAnyExpression(result, direction); + // go to next token + this.moveNext(); + } else { + result = new OrderByAnyExpression(result, direction); + } + Object.assign(result, { + source: this.getSource(offset, this.offset) + }); + results.push(result); + if (this.atEnd() === false && this.currentToken.syntax === SyntaxToken.Comma.syntax) { + this.moveNext(); + } + } + return results; + } + + /** + * + * @param {string} str + * @returns {ExpressionBase|undefined} + */ + parseSync(str) { + if (str == null) { + return; + } + Args.check(typeof str === 'string', new Error('The argument str must be a string.')); + this.current = 0; + this.offset = 0; + this.source = str; + //get tokens + this.tokens = this.toList(); + //reset offset + this.offset=0; + this.current=0; + const result = this.parseCommonSync(); + if (typeof result.exprOf === 'function') { + return result.exprOf(); + } + return result; + } + + /** + * @param {{$select?:string,$filter?:string,$orderBy?:string,$groupBy?:string,$top:number,$skip:number}} queryOptions + * @returns {{$where?:ExpressionBase,$select?:Array,$orderBy?:Array,$groupBy?:Array,$expand?:Array,$take?:number,$skip?:number}} + */ + parseQueryOptionsSync(queryOptions) { + const result = {}; + if (queryOptions.$filter) { + const $where = this.parseSync(queryOptions.$filter); + if ($where) { + result.$where = $where; + } + } + if (queryOptions.$select) { + const $select = this.parseSelectSequenceSync(queryOptions.$select); + if ($select) { + result.$select = $select; + } + } + if (queryOptions.$orderBy) { + const $orderby = this.parseOrderBySequenceSync(queryOptions.$orderBy); + if ($orderby) { + result.$orderBy = $orderby; + } + } + if (queryOptions.$expand) { + const $expand = this.parseExpandSequenceSync(queryOptions.$expand); + if ($expand) { + result.$expand = $expand; + } + } + if (queryOptions.$groupBy) { + const $groupBy = this.parseGroupBySequenceSync(queryOptions.$groupBy); + if ($groupBy) { + result.$groupBy = $groupBy; + } + } + if (typeof queryOptions.$top === 'number') { + result.$take = queryOptions.$top; + } + if (typeof queryOptions.$skip === 'number') { + result.$skip = queryOptions.$skip; + } + return result; + } + +} +module.exports = { + SimpleOpenDataParser +} diff --git a/spec/SimpleOpenDataParser.spec.js b/spec/SimpleOpenDataParser.spec.js new file mode 100644 index 0000000..78ced7e --- /dev/null +++ b/spec/SimpleOpenDataParser.spec.js @@ -0,0 +1,177 @@ +import { SqlFormatter, SimpleOpenDataParser } from '../index'; +import { trim } from 'lodash'; +import { QueryExpression, QueryEntity } from '../index'; +import { QueryField } from '../index'; +import { AnyExpressionFormatter } from '../index'; +describe('SimpleOpenDataParser', () => { + + it('should parser filter', async() => { + const parser = new SimpleOpenDataParser(); + let expr = parser.parseSync('id eq 100'); + expect(expr). toEqual({ + $eq: [ + { $name: 'id' }, + 100 + ] + }); + expr = parser.parseSync('active eq true and category eq \'Laptops\''); + expect(expr).toEqual({ + $and: [ + { + $eq: [ + { $name: 'active' }, + true + ] + }, + { + $eq: [ + { $name: 'category' }, + 'Laptops' + ] + } + ] + }); + }); + + it('should parser filter with custom function', async() => { + const parser = new SimpleOpenDataParser(); + let expr = parser.parseSync('createdBy eq me()'); + expect(expr). toEqual({ + $eq: [ + { $name: 'createdBy' }, + { $me: [] } + ] + }); + }); + + it('should parse select statement', async() => { + const parser = new SimpleOpenDataParser(); + let expr = parser.parseSelectSequenceSync('id,year(dateCreated) as yearCreated,name,dateCreated'); + expect(expr).toBeTruthy(); + parser.resolvingMember.subscribe((event) => { + event.member = event.member.replace('/', '.'); + }); + expr = parser.parseSelectSequenceSync('id,year(dateCreated) as yearDateCreated,year(orderedItem/releaseDate) as yearReleased'); + expect(expr).toBeTruthy(); + let select =Object.assign( new QueryExpression(), { + $select: { + 'Order': new AnyExpressionFormatter().formatMany(expr) + } + }); + select.join(new QueryEntity('Product').as('orderedItem')).with( + new QueryExpression().where(new QueryField('product')) + .equal(new QueryField('id').from('orderedItem')) + ); + let formatter = new SqlFormatter(); + let selectSql = formatter.formatSelect(select); + expect(selectSql).toBeTruthy(); + + expr = parser.parseSelectSequenceSync('id,indexof(name, \'Samsung\') as index1'); + expect(expr).toBeTruthy(); + }); + + it('should parse group by statement', async() => { + const parser = new SimpleOpenDataParser(); + let expr = parser.parseGroupBySequenceSync('year(dateCreated),month(dateCreated),day(dateCreated)'); + expect(expr).toBeTruthy(); + let groupBy =new AnyExpressionFormatter().formatMany(expr); + let formatter = new SqlFormatter(); + let groupBySql = formatter.formatGroupBy(groupBy); + expect(trim(groupBySql)).toBe('GROUP BY YEAR(dateCreated), MONTH(dateCreated), DAY(dateCreated)'); + }); + + it('should parser order by statement', async() => { + const parser = new SimpleOpenDataParser(); + let expr = parser.parseOrderBySequenceSync('releaseDate desc,name'); + let orderBy = new AnyExpressionFormatter().formatMany(expr); + let formatter = new SqlFormatter(); + let orderBySql = formatter.formatOrder(orderBy); + expect(trim(orderBySql)).toBe('ORDER BY releaseDate DESC, name ASC'); + expr = parser.parseOrderBySequenceSync('year(releaseDate) desc,month(releaseDate) desc'); + formatter = new SqlFormatter(); + orderBy = new AnyExpressionFormatter().formatMany(expr); + orderBySql = formatter.formatOrder(orderBy); + expect(trim(orderBySql)).toBe('ORDER BY YEAR(releaseDate) DESC, MONTH(releaseDate) DESC'); + }); + + it('should parse expand statement', async() => { + const parser = new SimpleOpenDataParser(); + const expr = parser.parseExpandSequenceSync( + 'customer($select=id,name;$expand=address),orderedItem' + ); + expect(expr).toEqual([ + { + name: 'customer', + options: { + $select: 'id,name', + $expand: 'address' + }, + source: 'customer($select=id,name;$expand=address)' + }, + { + name: 'orderedItem', + source: 'orderedItem' + } + ]); + }); + + it('should parse expand statement with select', async() => { + const parser = new SimpleOpenDataParser(); + const expr = parser.parseExpandSequenceSync( + 'customer($select=id,name,year(dateCreated) as dateCreated;$expand=address),orderedItem' + ); + expect(expr).toEqual([ + { + name: 'customer', + options: { + $select: 'id,name,year(dateCreated) as dateCreated', + $expand: 'address' + }, + source: 'customer($select=id,name,year(dateCreated) as dateCreated;$expand=address)' + }, + { + name: 'orderedItem', + source: 'orderedItem' + } + ]); + }); + + it('should parse expand statement with another expand', async() => { + const parser = new SimpleOpenDataParser(); + const expr = parser.parseExpandSequenceSync( + 'customer($expand=address($expand=location)),orderedItem' + ); + expect(expr).toEqual([ + { + name: 'customer', + options: { + $expand: 'address($expand=location)' + }, + source: 'customer($expand=address($expand=location))' + }, + { + name: 'orderedItem', + source: 'orderedItem' + } + ]); + }); + + it('should parse expand statement with filter', async() => { + const parser = new SimpleOpenDataParser(); + const expr = parser.parseExpandSequenceSync( + 'orders($filter=orderStatus/alternateName eq \'OrderStatusDelivered\';$top=10;$orderby=orderDate desc)' + ); + expect(expr).toEqual([ + { + name: 'orders', + options: { + $filter: 'orderStatus/alternateName eq \'OrderStatusDelivered\'', + $top: 10, + $orderby: 'orderDate desc' + }, + source: 'orders($filter=orderStatus/alternateName eq \'OrderStatusDelivered\';$top=10;$orderby=orderDate desc)' + } + ]); + }); + +}); \ No newline at end of file