Skip to content

Commit 1d6553a

Browse files
authored
Merge pull request #75 from diegomvh/master
Has Operator, Aliases Refactoring and Undefined vs Null
2 parents 97787ec + 280c612 commit 1d6553a

File tree

2 files changed

+94
-84
lines changed

2 files changed

+94
-84
lines changed

src/index.ts

Lines changed: 68 additions & 80 deletions
Original file line numberDiff line numberDiff line change
@@ -51,31 +51,20 @@ export type GroupBy<T> = {
5151
transform?: Transform<T>;
5252
}
5353

54-
export type Value = {
55-
type: 'raw' | 'guid' | 'duration' | 'binary' | 'json' | 'alias';
56-
value: any;
57-
}
58-
59-
export type Alias = Value & {
60-
name: string;
61-
handleName(): string;
62-
handleValue(): string;
63-
}
64-
65-
export const raw = (value: string): Value => ({type: 'raw', value});
66-
export const guid = (value: string): Value => ({type: 'guid', value});
67-
export const duration = (value: string): Value => ({type: 'duration', value});
68-
export const binary = (value: string): Value => ({type: 'binary', value});
69-
export const json = (value: PlainObject): Value => ({type: 'json', value});
70-
export const alias = (name: string, value: PlainObject): Alias => ({
71-
type: 'alias', name, value,
72-
handleName() {
73-
return `@${this.name}`;
74-
},
75-
handleValue() {
76-
return handleValue(this.value);
77-
}
78-
});
54+
export type Raw = { type: 'raw'; value: any; }
55+
export type Guid = { type: 'guid'; value: any; }
56+
export type Duration = { type: 'duration'; value: any; }
57+
export type Binary = { type: 'binary'; value: any; }
58+
export type Json = { type: 'json'; value: any; }
59+
export type Alias = { type: 'alias'; name: string; value: any; }
60+
export type Value = string | Date | number | boolean | Raw | Guid | Duration | Binary | Json | Alias;
61+
62+
export const raw = (value: string): Raw => ({ type: 'raw', value });
63+
export const guid = (value: string): Guid => ({ type: 'guid', value });
64+
export const duration = (value: string): Duration => ({ type: 'duration', value });
65+
export const binary = (value: string): Binary => ({ type: 'binary', value });
66+
export const json = (value: PlainObject): Json => ({ type: 'json', value });
67+
export const alias = (name: string, value: PlainObject): Alias => ({ type: 'alias', name, value });
7968

8069
export type QueryOptions<T> = ExpandOptions<T> & {
8170
search: string;
@@ -106,26 +95,20 @@ export default function <T>({
10695
count,
10796
expand,
10897
action,
109-
func,
110-
aliases
98+
func
11199
}: Partial<QueryOptions<T>> = {}) {
112-
let path = '';
100+
let path: string = '';
101+
let aliases: Alias[] = [];
113102

114103
const params: any = {};
115104

116-
if (key) {
117-
if (typeof key === 'object') {
118-
const keys = Object.keys(key)
119-
.map(k => `${k}=${key[k]}`)
120-
.join(',');
121-
path += `(${keys})`;
122-
} else {
123-
path += `(${key})`;
105+
// key is not (null, undefined)
106+
if (key != undefined) {
107+
path += `(${handleValue(key as Value, aliases)})`;
124108
}
125-
}
126109

127110
if (filter || typeof count === 'object')
128-
params.$filter = buildFilter(typeof count === 'object' ? count : filter);
111+
params.$filter = buildFilter(typeof count === 'object' ? count : filter, aliases);
129112

130113
if (transform)
131114
params.$apply = buildTransforms(transform);
@@ -161,43 +144,41 @@ export default function <T>({
161144
path += `/${func}`;
162145
} else if (typeof func === 'object') {
163146
const [funcName] = Object.keys(func);
164-
const funcArgs = Object.keys(func[funcName]).reduce(
165-
(acc: string[], item) => [...acc, `${item}=${handleValue(func[funcName][item])}`],
166-
[]
167-
);
147+
const funcArgs = handleValue(func[funcName] as Value, aliases);
168148

169149
path += `/${funcName}`;
170-
if (funcArgs.length) {
171-
path += `(${funcArgs.join(',')})`;
150+
if (funcArgs !== "") {
151+
path += `(${funcArgs})`;
172152
}
173153
}
174154
}
175155

176-
if (aliases) {
177-
aliases
178-
.reduce((acc, alias) => Object.assign(acc, {[alias.handleName()]: alias.handleValue()}), params);
156+
if (aliases.length > 0) {
157+
Object.assign(params, aliases.reduce((acc, alias) =>
158+
Object.assign(acc, { [`@${alias.name}`]: handleValue(alias.value) })
159+
, {}));
179160
}
180161

181162
return buildUrl(path, { $select, $search, $skiptoken, $format, ...params });
182163
}
183164

184-
function renderPrimitiveValue(key: string, val: any) {
185-
return `${key} eq ${handleValue(val)}`
165+
function renderPrimitiveValue(key: string, val: any, aliases: Alias[] = []) {
166+
return `${key} eq ${handleValue(val, aliases)}`
186167
}
187168

188-
function buildFilter(filters: Filter = {}, propPrefix = ''): string {
169+
function buildFilter(filters: Filter = {}, aliases: Alias[] = [], propPrefix = ''): string {
189170
return ((Array.isArray(filters) ? filters : [filters])
190171
.reduce((acc: string[], filter) => {
191172
if (filter) {
192-
const builtFilter = buildFilterCore(filter, propPrefix);
173+
const builtFilter = buildFilterCore(filter, aliases, propPrefix);
193174
if (builtFilter) {
194175
acc.push(builtFilter);
195176
}
196177
}
197178
return acc;
198179
}, []) as string[]).join(' and ');
199180

200-
function buildFilterCore(filter: Filter = {}, propPrefix = '') {
181+
function buildFilterCore(filter: Filter = {}, aliases: Alias[] = [], propPrefix = '') {
201182
let filterExpr = "";
202183
if (typeof filter === 'string') {
203184
// Use raw filter string
@@ -233,11 +214,11 @@ function buildFilter(filters: Filter = {}, propPrefix = ''): string {
233214
value === null
234215
) {
235216
// Simple key/value handled as equals operator
236-
result.push(renderPrimitiveValue(propName, value));
217+
result.push(renderPrimitiveValue(propName, value, aliases));
237218
} else if (Array.isArray(value)) {
238219
const op = filterKey;
239220
const builtFilters = value
240-
.map(v => buildFilter(v, propPrefix))
221+
.map(v => buildFilter(v, aliases, propPrefix))
241222
.filter(f => f)
242223
.map(f => (LOGICAL_OPERATORS.indexOf(op) !== -1 ? `(${f})` : f));
243224
if (builtFilters.length) {
@@ -265,27 +246,29 @@ function buildFilter(filters: Filter = {}, propPrefix = ''): string {
265246
result.push(`${builtFilters.join(` ${op} `)}`);
266247
}
267248
}
268-
} else if (value instanceof Object) {
249+
} else if (typeof value === 'object') {
269250
if ('type' in value) {
270-
result.push(renderPrimitiveValue(propName, value));
251+
result.push(renderPrimitiveValue(propName, value, aliases));
271252
} else {
272253
const operators = Object.keys(value);
273254
operators.forEach(op => {
274255
if (COMPARISON_OPERATORS.indexOf(op) !== -1) {
275-
result.push(`${propName} ${op} ${handleValue(value[op])}`);
256+
result.push(`${propName} ${op} ${handleValue(value[op], aliases)}`);
276257
} else if (LOGICAL_OPERATORS.indexOf(op) !== -1) {
277258
if (Array.isArray(value[op])) {
278259
result.push(
279260
value[op]
280-
.map((v: any) => '(' + buildFilterCore(v, propName) + ')')
261+
.map((v: any) => '(' + buildFilterCore(v, aliases, propName) + ')')
281262
.join(` ${op} `)
282263
);
283264
} else {
284-
result.push('(' + buildFilterCore(value[op], propName) + ')');
265+
result.push('(' + buildFilterCore(value[op], aliases, propName) + ')');
285266
}
286267
} else if (COLLECTION_OPERATORS.indexOf(op) !== -1) {
287268
const collectionClause = buildCollectionClause(filterKey.toLowerCase(), value[op], op, propName);
288269
if (collectionClause) { result.push(collectionClause); }
270+
} else if (op === 'has') {
271+
result.push(`${propName} ${op} ${handleValue(value[op], aliases)}`);
289272
} else if (op === 'in') {
290273
const resultingValues = Array.isArray(value[op])
291274
? value[op]
@@ -295,14 +278,14 @@ function buildFilter(filters: Filter = {}, propPrefix = ''): string {
295278
}));
296279

297280
result.push(
298-
propName + ' in (' + resultingValues.map((v: any) => handleValue(v)).join(',') + ')'
281+
propName + ' in (' + resultingValues.map((v: any) => handleValue(v, aliases)).join(',') + ')'
299282
);
300283
} else if (BOOLEAN_FUNCTIONS.indexOf(op) !== -1) {
301284
// Simple boolean functions (startswith, endswith, contains)
302-
result.push(`${op}(${propName},${handleValue(value[op])})`);
285+
result.push(`${op}(${propName},${handleValue(value[op], aliases)})`);
303286
} else {
304287
// Nested property
305-
const filter = buildFilterCore(value, propName);
288+
const filter = buildFilterCore(value, aliases, propName);
306289
if (filter) {
307290
result.push(filter);
308291
}
@@ -347,7 +330,7 @@ function buildFilter(filters: Filter = {}, propPrefix = ''): string {
347330
return {...acc, ...item}
348331
}, {}) : value;
349332

350-
const filter = buildFilterCore(filterValue, lambdaParameter);
333+
const filter = buildFilterCore(filterValue, aliases, lambdaParameter);
351334
clause = `${propName}/${op}(${filter ? `${lambdaParameter}:${filter}` : ''})`;
352335
}
353336
return clause;
@@ -373,36 +356,41 @@ function escapeIllegalChars(string: string) {
373356
return string;
374357
}
375358

376-
function handleValue(value: any) {
359+
function handleValue(value: Value, aliases?: Alias[]): any {
377360
if (typeof value === 'string') {
378361
return `'${escapeIllegalChars(value)}'`;
379362
} else if (value instanceof Date) {
380363
return value.toISOString();
381-
} else if (value instanceof Number) {
364+
} else if (typeof value === 'number') {
382365
return value;
383366
} else if (Array.isArray(value)) {
384-
// Double quote strings to keep them after `.join`
385-
const arr = value.map(d => (typeof d === 'string' ? `'${d}'` : d));
386-
return `[${arr.join(',')}]`;
387-
} else {
388-
// TODO: Figure out how best to specify types. See: https://github.com/devnixs/ODataAngularResources/blob/master/src/odatavalue.js
389-
switch (value && value.type) {
390-
case 'guid':
367+
return `[${value.map(d => handleValue(d)).join(',')}]`;
368+
} else if (value === null) {
369+
return value;
370+
} else if (typeof value === 'object') {
371+
if (value.type === 'raw') {
372+
return value.value;
373+
} else if (value.type === 'guid') {
391374
return value.value;
392-
case 'duration':
375+
} else if (value.type === 'duration') {
393376
return `duration'${value.value}'`;
394-
case 'raw':
395-
return value.value;
396-
case 'binary':
377+
} else if (value.type === 'binary') {
397378
return `binary'${value.value}'`;
398-
case 'alias':
399-
return (value as Alias).handleName();
400-
case 'json':
379+
} else if (value.type === 'alias') {
380+
// Store
381+
if (Array.isArray(aliases))
382+
aliases.push(value as Alias);
383+
return `@${(value as Alias).name}`;
384+
} else if (value.type === 'json') {
401385
return escape(JSON.stringify(value.value));
386+
} else {
387+
return Object.entries(value)
388+
.filter(([, v]) => v !== undefined)
389+
.map(([k, v]) => `${k}=${handleValue(v as Value, aliases)}`).join(',');
390+
}
402391
}
403392
return value;
404393
}
405-
}
406394

407395
function buildExpand<T>(expands: Expand<T>): string {
408396
if (typeof expands === 'number') {

test/index.test.ts

Lines changed: 26 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,14 @@ describe('filter', () => {
3939
expect(actual).toEqual(expected);
4040
});
4141

42+
it('should allow "has" operator', () => {
43+
const filter = { SomeProp: { has: { type: "raw", value: "Sales.Pattern'Yellow'" } } };
44+
const expected =
45+
"?$filter=SomeProp has Sales.Pattern'Yellow'";
46+
const actual = buildQuery({ filter });
47+
expect(actual).toEqual(expected);
48+
});
49+
4250
it('should allow "in" operator', () => {
4351
const filter = { SomeProp: { in: [1, 2, 3] } };
4452
const expected =
@@ -294,12 +302,12 @@ describe('filter', () => {
294302
const filter = { DateProp: { ge: start, le: end } };
295303
let expected =
296304
'?$filter=DateProp ge @start and DateProp le @end&@start=2017-01-01T00:00:00.000Z&@end=2017-03-01T00:00:00.000Z';
297-
let actual = buildQuery({ filter, aliases: [start, end] });
305+
let actual = buildQuery({ filter });
298306
expect(actual).toEqual(expected);
299307
end.value = new Date(Date.UTC(2017, 5, 1));
300308
expected =
301309
'?$filter=DateProp ge @start and DateProp le @end&@start=2017-01-01T00:00:00.000Z&@end=2017-06-01T00:00:00.000Z';
302-
actual = buildQuery({ filter, aliases: [start, end] });
310+
actual = buildQuery({ filter });
303311
expect(actual).toEqual(expected);
304312
});
305313

@@ -1572,6 +1580,20 @@ describe('function', () => {
15721580
expect(actual).toEqual(expected);
15731581
});
15741582

1583+
it('should support a function on a collection with one null parameter', () => {
1584+
const func = { Test: { One: 1, Two: null } };
1585+
const expected = '/Test(One=1,Two=null)';
1586+
const actual = buildQuery({ func });
1587+
expect(actual).toEqual(expected);
1588+
});
1589+
1590+
it('should support a function on a collection with undefined parameter', () => {
1591+
const func = { Test: { One: 1, Two: undefined } };
1592+
const expected = '/Test(One=1)';
1593+
const actual = buildQuery({ func });
1594+
expect(actual).toEqual(expected);
1595+
});
1596+
15751597
it('should support a function on an entity with parameters', () => {
15761598
const key = 1;
15771599
const func = { Test: { One: 1, Two: 2 } };
@@ -1608,7 +1630,7 @@ describe('function', () => {
16081630
};
16091631
const expected =
16101632
"/Test(SomeCollection=@SomeCollection)?@SomeCollection=['Sean','Jason']";
1611-
const actual = buildQuery({ func, aliases: [someCollection] });
1633+
const actual = buildQuery({ func });
16121634
expect(actual).toEqual(expected);
16131635
});
16141636

@@ -1619,7 +1641,7 @@ describe('function', () => {
16191641
};
16201642
const expected =
16211643
'/Test(SomeCollection=@SomeCollection)?@SomeCollection=%5B%7B%22Name%22%3A%22Sean%22%7D%2C%7B%22Name%22%3A%22Jason%22%7D%5D';
1622-
const actual = buildQuery({ func, aliases: [someCollection] });
1644+
const actual = buildQuery({ func });
16231645
expect(actual).toEqual(expected);
16241646
});
16251647
});

0 commit comments

Comments
 (0)