diff --git a/lib/odata-query-provider.ts b/lib/odata-query-provider.ts index ebbe849..273a4f9 100644 --- a/lib/odata-query-provider.ts +++ b/lib/odata-query-provider.ts @@ -64,28 +64,28 @@ export class ODataQueryProvider implements IQueryProvider { const sel = e.args[1] ? this.handlePartArg(e.args[1]) : null; const path = exp.split('/'); - let ec = es[path[0]] || (es[path[0]] = { children: { } }); + let ec = es[path[0]] || (es[path[0]] = { children: {} }); path.slice(1).forEach(p => { - ec = ec.children[p] || (ec.children[p] = { children: { } }); + ec = ec.children[p] || (ec.children[p] = { children: {} }); }); ec.select = sel; }); - queryParams.push({ key: '$expand', value: expands.join(',') }); + queryParams.push({ key: '$expand', value: walkExpands(es) }); } if (orders.length) { const value = orders.map(o => { const v = this.handlePartArg(o.args[0]); - return o.type.endsWith('Descending') ? (v + ' desc') : v; + return ~descFuncs.indexOf(o.type) ? (v + ' desc') : v; }).join(','); queryParams.push({ key: '$orderby', value }); } for (var p in params) { - queryParams.push({ key: '$' + p.replace('where', 'filter').toLowerCase(), value: this.handlePartArg(params[p]) }); + queryParams.push({ key: '$' + p.replace('where', 'filter'), value: this.handlePartArg(params[p]) }); } return this.requestProvider.request(queryParams, options); @@ -98,6 +98,11 @@ export class ODataQueryProvider implements IQueryProvider { : this.expToStr(arg.exp, arg.scopes, arg.exp.type === ExpressionType.Func ? (arg.exp as FuncExpression).parameters : []) } + handleExp(exp: Expression, scopes: any[]) { + this.rootLambda = true; + return this.expToStr(exp, scopes, exp.type === ExpressionType.Func ? (exp as FuncExpression).parameters : []) + } + expToStr(exp: Expression, scopes: any[], parameters: string[]): string { switch (exp.type) { case ExpressionType.Literal: @@ -193,11 +198,15 @@ export class ODataQueryProvider implements IQueryProvider { if (callee.type !== ExpressionType.Member && callee.type !== ExpressionType.Variable) throw new Error(`Invalid function call ${this.expToStr(exp.callee, scopes, parameters)}`); - let args = exp.args.map(a => this.expToStr(a, scopes, parameters)).join(', '); + let args: string; if (callee.type === ExpressionType.Member) { const member = callee as MemberExpression; const ownerStr = this.expToStr(member.owner, scopes, parameters); + if (member.name === '$expand') + return ownerStr + '/' + this.handleExp(exp.args[0], scopes); + + args = exp.args.map(a => this.expToStr(a, scopes, parameters)).join(', '); // handle Math functions if (~mathFuncs.indexOf(callee.name) && ownerStr === 'Math') return `${callee.name}(${args})`; @@ -208,9 +217,12 @@ export class ODataQueryProvider implements IQueryProvider { if (callee.name === 'any' || callee.name === 'all') return `${ownerStr}/${callee.name}(${args})`; - // other supported functions takes owner as the first argument` + // other supported functions takes owner as the first argument args = args ? `${ownerStr}, ${args}` : ownerStr; } + else { + args = exp.args.map(a => this.expToStr(a, scopes, parameters)).join(', '); + } const oDataFunc = functions[callee.name] || callee.name; return `${oDataFunc}(${args})`; @@ -285,4 +297,15 @@ const functions = { type ExpandContainer = { select?: string, children: ExpandCollection }; type ExpandCollection = { [expand: string]: ExpandContainer }; -// TODO: multi navigation expand desteği için diziye $expand extension metodu. callToStr işlerken $expand için metod adı yerine owner/metod dön! \ No newline at end of file +function walkExpands(e: ExpandCollection) { + const expStrs = []; + for (const p in e) { + const exp = e[p]; + let childStr = walkExpands(exp.children); + const expStr = exp.select + ? `${p}(${childStr ? `$expand=${childStr},` : ''}$select=${exp.select})` + : childStr ? `${p}/${childStr}` : p; + expStrs.push(expStr); + } + return expStrs.join(','); +} diff --git a/lib/odata-query.ts b/lib/odata-query.ts index 7f29543..b6c02e3 100644 --- a/lib/odata-query.ts +++ b/lib/odata-query.ts @@ -1,5 +1,5 @@ import { - IQueryProvider, IQueryPart, Predicate, Func1, Func2, IQueryBase, + IQueryProvider, IQueryPart, Predicate, Func1, IQueryBase, InlineCountInfo, QueryPart, PartArgument, AjaxOptions, AjaxFuncs } from "jinqu"; @@ -44,7 +44,7 @@ export class ODataQuery implements IODataQuery { return this.create(QueryPart.select(selector, scopes)); } - expand(navigationSelector: Func2, selector?: Func2, ...scopes): IODataQuery { + expand(navigationSelector: Func1, selector?: Func1, ...scopes): IODataQuery { return this.create(createExpandPart(navigationSelector, selector, scopes)); } @@ -71,7 +71,7 @@ export interface IODataQuery extends IQueryBase { orderBy(keySelector: Func1, ...scopes): IOrderedODataQuery; orderByDescending(keySelector: Func1, ...scopes): IOrderedODataQuery; select(selector: Func1, ...scopes): IODataQuery; - expand(navigationSelector: Func2, selector?: Func2, ...scopes): IODataQuery; + expand(navigationSelector: Func1, selector?: Func1, ...scopes): IODataQuery; skip(count: number): IODataQuery; top(count: number): IODataQuery; toArrayAsync(): PromiseLike; @@ -82,7 +82,7 @@ export interface IOrderedODataQuery extends IODataQuery { thenByDescending(keySelector: Func1, ...scopes): IOrderedODataQuery; } -export function createExpandPart(navigationSelector: Func2, selector: Func2, scopes: any[]) { +export function createExpandPart(navigationSelector: Func1, selector: Func1, scopes: any[]) { const args = [PartArgument.identifier(navigationSelector, scopes)]; if (selector) { args.push(PartArgument.identifier(selector, scopes)); @@ -96,7 +96,7 @@ export const ODataFuncs = { declare global { interface Array { - $expand(navigationSelector: Func1, selector?: Func1): TNav; + $expand(navigationSelector: Func1): TNav; } } diff --git a/test/fixture.ts b/test/fixture.ts index 279a904..23a82cf 100644 --- a/test/fixture.ts +++ b/test/fixture.ts @@ -11,9 +11,12 @@ export class MockRequestProvider implements IAjaxProvider { } } -export class Country {} +export class Country { + name: string; +} export class City { + name: string; country: Country; } diff --git a/test/service.spec.ts b/test/service.spec.ts index 89a6286..e445b7b 100644 --- a/test/service.spec.ts +++ b/test/service.spec.ts @@ -2,7 +2,7 @@ import { expect } from 'chai'; import 'mocha'; import chai = require('chai'); import chaiAsPromised = require('chai-as-promised'); -import { CompanyService, MockRequestProvider } from './fixture'; +import { CompanyService, MockRequestProvider, Address } from './fixture'; chai.use(chaiAsPromised); @@ -30,4 +30,79 @@ describe('Service tests', () => { expect(prm).property('key').to.equal('$orderby'); expect(prm).property('value').to.equal('id,name desc'); }); -}); + + it('should create expand with multi level', () => { + const query = service.companies().expand(c => c.addresses.$expand(a => a.city).country); + expect(query.toArrayAsync()).to.be.fulfilled.and.eventually.be.null; + expect(provider).property('options').property('params').to.have.length(1); + + const prm = provider.options.params[0]; + expect(prm).property('key').to.equal('$expand'); + expect(prm).property('value').to.equal('addresses/city/country'); + }); + + it('should create expand with multi level using strings 1', () => { + const query = service.companies().expand('addresses.city.country'); + expect(query.toArrayAsync()).to.be.fulfilled.and.eventually.be.null; + expect(provider).property('options').property('params').to.have.length(1); + + const prm = provider.options.params[0]; + expect(prm).property('key').to.equal('$expand'); + expect(prm).property('value').to.equal('addresses/city/country'); + }); + + it('should create expand with multi level using strings 2', () => { + const query = service.companies().expand('c => addresses.city.country'); + expect(query.toArrayAsync()).to.be.fulfilled.and.eventually.be.null; + expect(provider).property('options').property('params').to.have.length(1); + + const prm = provider.options.params[0]; + expect(prm).property('key').to.equal('$expand'); + expect(prm).property('value').to.equal('addresses/city/country'); + }); + + it('should create expand with multi level using strings 3', () => { + const query = service.companies().expand('c => c.addresses.$expand(a => a.city).country'); + expect(query.toArrayAsync()).to.be.fulfilled.and.eventually.be.null; + expect(provider).property('options').property('params').to.have.length(1); + + const prm = provider.options.params[0]; + expect(prm).property('key').to.equal('$expand'); + expect(prm).property('value').to.equal('addresses/city/country'); + }); + + it('should create expand with multi level with explicit calls', () => { + const query = service.companies().expand(c => c.addresses) + .expand(c => c.addresses.$expand(a => a.city)) + .expand(c => c.addresses.$expand(a => a.city).country); + expect(query.toArrayAsync()).to.be.fulfilled.and.eventually.be.null; + expect(provider).property('options').property('params').to.have.length(1); + + const prm = provider.options.params[0]; + expect(prm).property('key').to.equal('$expand'); + expect(prm).property('value').to.equal('addresses/city/country'); + }); + + it('should create expand with multi level with explicit calls and selects', () => { + const query = service.companies().expand(c => c.addresses, a => a.city) + .expand(c => c.addresses.$expand(a => a.city), c => c.country) + .expand(c => c.addresses.$expand(a => a.city).country, c => c.name); + expect(query.toArrayAsync()).to.be.fulfilled.and.eventually.be.null; + expect(provider).property('options').property('params').to.have.length(1); + + const prm = provider.options.params[0]; + expect(prm).property('key').to.equal('$expand'); + expect(prm).property('value').to.equal('addresses($expand=city($expand=country($select=name),$select=country),$select=city)'); + }); + + it('should create expand with multi level with explicit calls and mixed selects', () => { + const query = service.companies().expand(c => c.addresses, a => a.city) + .expand(c => c.addresses.$expand(a => a.city)) + .expand(c => c.addresses.$expand(a => a.city).country, c => c.name); + expect(query.toArrayAsync()).to.be.fulfilled.and.eventually.be.null; + expect(provider).property('options').property('params').to.have.length(1); + + const prm = provider.options.params[0]; + expect(prm).property('key').to.equal('$expand'); + expect(prm).property('value').to.equal('addresses($expand=city/country($select=name),$select=city)'); + });});