fms-odata-js 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +160 -0
- package/LICENSE +22 -0
- package/README.md +381 -0
- package/dist/batch.d.ts +154 -0
- package/dist/batch.d.ts.map +1 -0
- package/dist/client.d.ts +160 -0
- package/dist/client.d.ts.map +1 -0
- package/dist/containers.d.ts +190 -0
- package/dist/containers.d.ts.map +1 -0
- package/dist/entity.d.ts +149 -0
- package/dist/entity.d.ts.map +1 -0
- package/dist/errors.d.ts +69 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/fms-odata.esm.js +2201 -0
- package/dist/fms-odata.esm.min.js +6 -0
- package/dist/fms-odata.iife.min.js +6 -0
- package/dist/http.d.ts +51 -0
- package/dist/http.d.ts.map +1 -0
- package/dist/index.d.ts +31 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/metadata.d.ts +85 -0
- package/dist/metadata.d.ts.map +1 -0
- package/dist/query.d.ts +198 -0
- package/dist/query.d.ts.map +1 -0
- package/dist/scripts.d.ts +114 -0
- package/dist/scripts.d.ts.map +1 -0
- package/dist/types.d.ts +26 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/url.d.ts +49 -0
- package/dist/url.d.ts.map +1 -0
- package/package.json +61 -0
- package/src/batch.ts +477 -0
- package/src/client.ts +268 -0
- package/src/containers.ts +457 -0
- package/src/entity.ts +334 -0
- package/src/errors.ts +134 -0
- package/src/http.ts +190 -0
- package/src/index.ts +81 -0
- package/src/metadata.ts +354 -0
- package/src/query.ts +442 -0
- package/src/scripts.ts +279 -0
- package/src/types.ts +30 -0
- package/src/url.ts +98 -0
package/src/query.ts
ADDED
|
@@ -0,0 +1,442 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Fluent OData query builder.
|
|
3
|
+
*
|
|
4
|
+
* `Query` accumulates `$select`, `$filter`, `$expand`, `$orderby`, `$top`,
|
|
5
|
+
* `$skip`, `$count`, and `$search` options and serializes them into an absolute
|
|
6
|
+
* URL via `toURL()`. Actual HTTP execution (`.get()`) lands in M3 alongside
|
|
7
|
+
* auth, error handling, and the mock server.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import type { FMSOData } from './client.js'
|
|
11
|
+
import {
|
|
12
|
+
buildContainerJsonBody,
|
|
13
|
+
type ContainerJsonValue,
|
|
14
|
+
} from './containers.js'
|
|
15
|
+
import { EntityRef } from './entity.js'
|
|
16
|
+
import { executeJson } from './http.js'
|
|
17
|
+
import { runScriptAtEntitySet, type ScriptOptions, type ScriptResult } from './scripts.js'
|
|
18
|
+
import type { RequestOptions } from './types.js'
|
|
19
|
+
import {
|
|
20
|
+
buildQueryString,
|
|
21
|
+
encodePathSegment,
|
|
22
|
+
formatLiteral,
|
|
23
|
+
type ODataLiteral,
|
|
24
|
+
} from './url.js'
|
|
25
|
+
import type { AggregateFunction } from '@fms-odata/spec-ts'
|
|
26
|
+
|
|
27
|
+
// ---------------------------------------------------------------------------
|
|
28
|
+
// Filter
|
|
29
|
+
// ---------------------------------------------------------------------------
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Opaque filter expression produced by `filterFactory`. Use `.and()`, `.or()`,
|
|
33
|
+
* `.not()` to compose; pass to `Query#filter`.
|
|
34
|
+
*/
|
|
35
|
+
export class Filter {
|
|
36
|
+
constructor(readonly expr: string) {}
|
|
37
|
+
|
|
38
|
+
toString(): string {
|
|
39
|
+
return this.expr
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
and(other: Filter | string): Filter {
|
|
43
|
+
return new Filter(`(${this.expr}) and (${Filter.coerce(other)})`)
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
or(other: Filter | string): Filter {
|
|
47
|
+
return new Filter(`(${this.expr}) or (${Filter.coerce(other)})`)
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
not(): Filter {
|
|
51
|
+
return new Filter(`not (${this.expr})`)
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** @internal */
|
|
55
|
+
static coerce(x: Filter | string): string {
|
|
56
|
+
return x instanceof Filter ? x.expr : x
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** Factory passed to callback-form `Query#filter(f => ...)`. */
|
|
61
|
+
export interface FilterFactory {
|
|
62
|
+
eq(field: string, value: ODataLiteral): Filter
|
|
63
|
+
ne(field: string, value: ODataLiteral): Filter
|
|
64
|
+
gt(field: string, value: ODataLiteral): Filter
|
|
65
|
+
ge(field: string, value: ODataLiteral): Filter
|
|
66
|
+
lt(field: string, value: ODataLiteral): Filter
|
|
67
|
+
le(field: string, value: ODataLiteral): Filter
|
|
68
|
+
startswith(field: string, value: string): Filter
|
|
69
|
+
endswith(field: string, value: string): Filter
|
|
70
|
+
contains(field: string, value: string): Filter
|
|
71
|
+
and(a: Filter | string, b: Filter | string): Filter
|
|
72
|
+
or(a: Filter | string, b: Filter | string): Filter
|
|
73
|
+
not(a: Filter | string): Filter
|
|
74
|
+
/** Escape hatch: embed a raw OData filter fragment verbatim. */
|
|
75
|
+
raw(expr: string): Filter
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export const filterFactory: FilterFactory = {
|
|
79
|
+
eq: (f, v) => new Filter(`${f} eq ${formatLiteral(v)}`),
|
|
80
|
+
ne: (f, v) => new Filter(`${f} ne ${formatLiteral(v)}`),
|
|
81
|
+
gt: (f, v) => new Filter(`${f} gt ${formatLiteral(v)}`),
|
|
82
|
+
ge: (f, v) => new Filter(`${f} ge ${formatLiteral(v)}`),
|
|
83
|
+
lt: (f, v) => new Filter(`${f} lt ${formatLiteral(v)}`),
|
|
84
|
+
le: (f, v) => new Filter(`${f} le ${formatLiteral(v)}`),
|
|
85
|
+
startswith: (f, v) => new Filter(`startswith(${f},${formatLiteral(v)})`),
|
|
86
|
+
endswith: (f, v) => new Filter(`endswith(${f},${formatLiteral(v)})`),
|
|
87
|
+
contains: (f, v) => new Filter(`contains(${f},${formatLiteral(v)})`),
|
|
88
|
+
and: (a, b) => new Filter(`(${Filter.coerce(a)}) and (${Filter.coerce(b)})`),
|
|
89
|
+
or: (a, b) => new Filter(`(${Filter.coerce(a)}) or (${Filter.coerce(b)})`),
|
|
90
|
+
not: (a) => new Filter(`not (${Filter.coerce(a)})`),
|
|
91
|
+
raw: (s) => new Filter(s),
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export type FilterInput =
|
|
95
|
+
| Filter
|
|
96
|
+
| string
|
|
97
|
+
| ((f: FilterFactory) => Filter | string)
|
|
98
|
+
|
|
99
|
+
function resolveFilter(input: FilterInput): string {
|
|
100
|
+
if (typeof input === 'function') return Filter.coerce(input(filterFactory))
|
|
101
|
+
return Filter.coerce(input)
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// ---------------------------------------------------------------------------
|
|
105
|
+
// Query state
|
|
106
|
+
// ---------------------------------------------------------------------------
|
|
107
|
+
|
|
108
|
+
export type OrderDir = 'asc' | 'desc'
|
|
109
|
+
|
|
110
|
+
/** @internal */
|
|
111
|
+
export interface QueryOptionsState {
|
|
112
|
+
select?: string[]
|
|
113
|
+
filter?: string
|
|
114
|
+
expand?: Array<{ name: string; options?: QueryOptionsState }>
|
|
115
|
+
orderby?: Array<{ field: string; dir: OrderDir }>
|
|
116
|
+
top?: number
|
|
117
|
+
skip?: number
|
|
118
|
+
count?: boolean
|
|
119
|
+
search?: string
|
|
120
|
+
apply?: string
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// ---------------------------------------------------------------------------
|
|
124
|
+
// Query builder
|
|
125
|
+
// ---------------------------------------------------------------------------
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Fluent query builder. Methods mutate and return `this` for chaining.
|
|
129
|
+
*/
|
|
130
|
+
/** Result envelope returned by `Query#get()`. */
|
|
131
|
+
export interface QueryResult<T> {
|
|
132
|
+
value: T[]
|
|
133
|
+
/** Present when `.count()` was enabled on the query. */
|
|
134
|
+
count?: number
|
|
135
|
+
/** Server-driven paging link. */
|
|
136
|
+
nextLink?: string
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
export class Query<T = Record<string, unknown>> {
|
|
140
|
+
/** @internal */ readonly _state: QueryOptionsState = {}
|
|
141
|
+
/** @internal */ readonly _baseUrl: string
|
|
142
|
+
/** @internal */ readonly _entitySet: string
|
|
143
|
+
/** @internal */ readonly _client: FMSOData | undefined
|
|
144
|
+
|
|
145
|
+
constructor(baseUrl: string, entitySet: string, client?: FMSOData) {
|
|
146
|
+
this._baseUrl = baseUrl
|
|
147
|
+
this._entitySet = entitySet
|
|
148
|
+
if (client) this._client = client
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
select(...fields: string[]): this {
|
|
152
|
+
this._state.select = [...(this._state.select ?? []), ...fields]
|
|
153
|
+
return this
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
filter(input: FilterInput): this {
|
|
157
|
+
const expr = resolveFilter(input)
|
|
158
|
+
this._state.filter = this._state.filter
|
|
159
|
+
? `(${this._state.filter}) and (${expr})`
|
|
160
|
+
: expr
|
|
161
|
+
return this
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
or(input: FilterInput): this {
|
|
165
|
+
const expr = resolveFilter(input)
|
|
166
|
+
this._state.filter = this._state.filter
|
|
167
|
+
? `(${this._state.filter}) or (${expr})`
|
|
168
|
+
: expr
|
|
169
|
+
return this
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
expand(name: string, build?: (q: Query) => Query | void): this {
|
|
173
|
+
const entry: { name: string; options?: QueryOptionsState } = { name }
|
|
174
|
+
if (build) {
|
|
175
|
+
const nested = new Query('', name)
|
|
176
|
+
build(nested)
|
|
177
|
+
entry.options = nested._state
|
|
178
|
+
}
|
|
179
|
+
this._state.expand = [...(this._state.expand ?? []), entry]
|
|
180
|
+
return this
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
orderby(field: string, dir: OrderDir = 'asc'): this {
|
|
184
|
+
this._state.orderby = [...(this._state.orderby ?? []), { field, dir }]
|
|
185
|
+
return this
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
top(n: number): this {
|
|
189
|
+
if (!Number.isInteger(n) || n < 0) {
|
|
190
|
+
throw new RangeError(`Query#top: expected non-negative integer, got ${n}`)
|
|
191
|
+
}
|
|
192
|
+
this._state.top = n
|
|
193
|
+
return this
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
skip(n: number): this {
|
|
197
|
+
if (!Number.isInteger(n) || n < 0) {
|
|
198
|
+
throw new RangeError(`Query#skip: expected non-negative integer, got ${n}`)
|
|
199
|
+
}
|
|
200
|
+
this._state.skip = n
|
|
201
|
+
return this
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
count(enabled: boolean = true): this {
|
|
205
|
+
this._state.count = enabled
|
|
206
|
+
return this
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
search(term: string): this {
|
|
210
|
+
this._state.search = term
|
|
211
|
+
return this
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// -------------------------------------------------------------------------
|
|
215
|
+
// $apply (aggregation) — requires FMS 22.0.1+ (FileMaker 2024)
|
|
216
|
+
// -------------------------------------------------------------------------
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* Set a raw `$apply` expression. Use this for advanced transformations
|
|
220
|
+
* that the `aggregate()` / `groupBy()` helpers don't cover.
|
|
221
|
+
*
|
|
222
|
+
* Requires FileMaker Server 2024+ (v22). Use `db.hasFeature('applyAggregation')`
|
|
223
|
+
* to check before calling.
|
|
224
|
+
*
|
|
225
|
+
* @example
|
|
226
|
+
* ```ts
|
|
227
|
+
* const result = await db.from('orders').apply('aggregate(total with sum as totalSum)')
|
|
228
|
+
* .get()
|
|
229
|
+
* ```
|
|
230
|
+
*/
|
|
231
|
+
apply(expr: string): this {
|
|
232
|
+
this._state.apply = expr
|
|
233
|
+
return this
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* Aggregate the entity set. Produces a `$apply=aggregate(...)` expression.
|
|
238
|
+
*
|
|
239
|
+
* Requires FileMaker Server 2024+ (v22).
|
|
240
|
+
*
|
|
241
|
+
* @example
|
|
242
|
+
* ```ts
|
|
243
|
+
* const result = await db.from('orders')
|
|
244
|
+
* .aggregate([{ field: 'total', function: 'sum', alias: 'totalSum' }])
|
|
245
|
+
* .get()
|
|
246
|
+
* // $apply=aggregate(total with sum as totalSum)
|
|
247
|
+
* ```
|
|
248
|
+
*/
|
|
249
|
+
aggregate(expressions: Array<{ field: string; function: AggregateFunction; alias: string }>): this {
|
|
250
|
+
const parts = expressions.map((e) => `${e.field} with ${e.function} as ${e.alias}`)
|
|
251
|
+
this._state.apply = `aggregate(${parts.join(',')})`
|
|
252
|
+
return this
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
/**
|
|
256
|
+
* Group the entity set by one or more fields, optionally with aggregation.
|
|
257
|
+
* Produces a `$apply=groupby((fields), aggregate(...))` expression.
|
|
258
|
+
*
|
|
259
|
+
* Requires FileMaker Server 2024+ (v22).
|
|
260
|
+
*
|
|
261
|
+
* @example
|
|
262
|
+
* ```ts
|
|
263
|
+
* const result = await db.from('orders')
|
|
264
|
+
* .groupBy(
|
|
265
|
+
* ['customerId'],
|
|
266
|
+
* [{ field: 'total', function: 'sum', alias: 'totalSum' }],
|
|
267
|
+
* )
|
|
268
|
+
* .get()
|
|
269
|
+
* // $apply=groupby((customerId),aggregate(total with sum as totalSum))
|
|
270
|
+
* ```
|
|
271
|
+
*/
|
|
272
|
+
groupBy(
|
|
273
|
+
fields: string[],
|
|
274
|
+
aggregateExpressions?: Array<{ field: string; function: AggregateFunction; alias: string }>,
|
|
275
|
+
): this {
|
|
276
|
+
const fieldList = fields.join(',')
|
|
277
|
+
if (aggregateExpressions && aggregateExpressions.length > 0) {
|
|
278
|
+
const aggParts = aggregateExpressions.map((e) => `${e.field} with ${e.function} as ${e.alias}`)
|
|
279
|
+
this._state.apply = `groupby((${fieldList}),aggregate(${aggParts.join(',')}))`
|
|
280
|
+
} else {
|
|
281
|
+
this._state.apply = `groupby((${fieldList}))`
|
|
282
|
+
}
|
|
283
|
+
return this
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
/** Build the absolute request URL for this query. */
|
|
287
|
+
toURL(): string {
|
|
288
|
+
const qs = serializeOptions(this._state, { topLevel: true })
|
|
289
|
+
const base = `${this._baseUrl}/${encodePathSegment(this._entitySet)}`
|
|
290
|
+
return qs ? `${base}?${qs}` : base
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
/**
|
|
294
|
+
* Get a handle to a single entity by its primary key. Subsequent operations
|
|
295
|
+
* (`.get()`, `.patch()`, `.delete()`) hit `/<EntitySet>(<key>)`.
|
|
296
|
+
*/
|
|
297
|
+
byKey(key: string | number): EntityRef<T> {
|
|
298
|
+
if (!this._client) {
|
|
299
|
+
throw new Error('Query#byKey: no client attached (use FMSOData#from)')
|
|
300
|
+
}
|
|
301
|
+
return new EntityRef<T>(this._client, this._entitySet, key)
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
/**
|
|
305
|
+
* `POST` a new entity to the collection. Returns the created row (FMS echoes
|
|
306
|
+
* it by default).
|
|
307
|
+
*/
|
|
308
|
+
async create(
|
|
309
|
+
body: Partial<T> | Record<string, unknown>,
|
|
310
|
+
opts: RequestOptions = {},
|
|
311
|
+
): Promise<T> {
|
|
312
|
+
if (!this._client) {
|
|
313
|
+
throw new Error('Query#create: no client attached (use FMSOData#from)')
|
|
314
|
+
}
|
|
315
|
+
const url = `${this._baseUrl}/${encodePathSegment(this._entitySet)}`
|
|
316
|
+
const json = await executeJson<T>(this._client._ctx, url, {
|
|
317
|
+
method: 'POST',
|
|
318
|
+
headers: { 'Content-Type': 'application/json' },
|
|
319
|
+
body: JSON.stringify(body),
|
|
320
|
+
accept: 'json',
|
|
321
|
+
...(opts.signal ? { signal: opts.signal } : {}),
|
|
322
|
+
})
|
|
323
|
+
return json
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
/**
|
|
327
|
+
* `POST` a new entity carrying one or more container fields. Maps to the
|
|
328
|
+
* Claris "Operation 1" — the request body is a JSON object containing
|
|
329
|
+
* regular field values plus, for each container field, the base64 data
|
|
330
|
+
* and the `@com.filemaker.odata.{ContentType,Filename}` annotations.
|
|
331
|
+
*
|
|
332
|
+
* Each container value's `data` must already be base64-encoded.
|
|
333
|
+
*
|
|
334
|
+
* @example
|
|
335
|
+
* await db.from('contact').createWithContainers(
|
|
336
|
+
* { first_name: 'Bob', last_name: 'Jones' },
|
|
337
|
+
* { photo: { data: photoB64, contentType: 'image/png', filename: 'BJONES.png' } },
|
|
338
|
+
* )
|
|
339
|
+
*/
|
|
340
|
+
async createWithContainers(
|
|
341
|
+
regularFields: Partial<T> | Record<string, unknown>,
|
|
342
|
+
containers: Record<string, ContainerJsonValue>,
|
|
343
|
+
opts: RequestOptions = {},
|
|
344
|
+
): Promise<T> {
|
|
345
|
+
if (!this._client) {
|
|
346
|
+
throw new Error('Query#createWithContainers: no client attached (use FMSOData#from)')
|
|
347
|
+
}
|
|
348
|
+
const url = `${this._baseUrl}/${encodePathSegment(this._entitySet)}`
|
|
349
|
+
const body = buildContainerJsonBody(
|
|
350
|
+
containers,
|
|
351
|
+
regularFields as Record<string, unknown>,
|
|
352
|
+
)
|
|
353
|
+
const json = await executeJson<T>(this._client._ctx, url, {
|
|
354
|
+
method: 'POST',
|
|
355
|
+
headers: { 'Content-Type': 'application/json' },
|
|
356
|
+
body: JSON.stringify(body),
|
|
357
|
+
accept: 'json',
|
|
358
|
+
...(opts.signal ? { signal: opts.signal } : {}),
|
|
359
|
+
})
|
|
360
|
+
return json
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
/**
|
|
364
|
+
* Execute the query. Returns the parsed OData collection envelope.
|
|
365
|
+
*/
|
|
366
|
+
async get(opts: RequestOptions = {}): Promise<QueryResult<T>> {
|
|
367
|
+
if (!this._client) {
|
|
368
|
+
throw new Error('Query#get: no client attached (use FMSOData#from)')
|
|
369
|
+
}
|
|
370
|
+
type Envelope = {
|
|
371
|
+
value?: T[]
|
|
372
|
+
'@odata.count'?: number
|
|
373
|
+
'@odata.nextLink'?: string
|
|
374
|
+
}
|
|
375
|
+
const json = await executeJson<Envelope>(this._client._ctx, this.toURL(), {
|
|
376
|
+
method: 'GET',
|
|
377
|
+
accept: 'json',
|
|
378
|
+
...(opts.signal ? { signal: opts.signal } : {}),
|
|
379
|
+
})
|
|
380
|
+
const out: QueryResult<T> = { value: json?.value ?? [] }
|
|
381
|
+
if (json && typeof json['@odata.count'] === 'number') out.count = json['@odata.count']
|
|
382
|
+
if (json && typeof json['@odata.nextLink'] === 'string') out.nextLink = json['@odata.nextLink']
|
|
383
|
+
return out
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
/**
|
|
387
|
+
* Invoke a FileMaker script in the context of this query's entity set.
|
|
388
|
+
*
|
|
389
|
+
* Filter / select / orderby / paging state on the `Query` is **ignored** —
|
|
390
|
+
* the underlying OData Action only cares about the entity set. Use
|
|
391
|
+
* `EntityRef#script` to run a script in the context of a specific record.
|
|
392
|
+
*/
|
|
393
|
+
async script(name: string, opts: ScriptOptions = {}): Promise<ScriptResult> {
|
|
394
|
+
if (!this._client) {
|
|
395
|
+
throw new Error('Query#script: no client attached (use FMSOData#from)')
|
|
396
|
+
}
|
|
397
|
+
return runScriptAtEntitySet(this._client, this._entitySet, name, opts)
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
/**
|
|
402
|
+
* Serialize query options either as a top-level querystring (percent-encoded)
|
|
403
|
+
* or as a nested `$expand` option block (semicolon-joined, unencoded — the
|
|
404
|
+
* outer param encoder will encode the whole block once).
|
|
405
|
+
*
|
|
406
|
+
* @internal
|
|
407
|
+
*/
|
|
408
|
+
export function serializeOptions(
|
|
409
|
+
s: QueryOptionsState,
|
|
410
|
+
opts: { topLevel: boolean },
|
|
411
|
+
): string {
|
|
412
|
+
const pairs: Array<[string, string]> = []
|
|
413
|
+
|
|
414
|
+
if (s.select && s.select.length > 0) {
|
|
415
|
+
pairs.push(['$select', s.select.join(',')])
|
|
416
|
+
}
|
|
417
|
+
if (s.filter) {
|
|
418
|
+
pairs.push(['$filter', s.filter])
|
|
419
|
+
}
|
|
420
|
+
if (s.expand && s.expand.length > 0) {
|
|
421
|
+
const parts = s.expand.map((e) => {
|
|
422
|
+
if (!e.options) return e.name
|
|
423
|
+
const inner = serializeOptions(e.options, { topLevel: false })
|
|
424
|
+
return inner ? `${e.name}(${inner})` : e.name
|
|
425
|
+
})
|
|
426
|
+
pairs.push(['$expand', parts.join(',')])
|
|
427
|
+
}
|
|
428
|
+
if (s.orderby && s.orderby.length > 0) {
|
|
429
|
+
pairs.push([
|
|
430
|
+
'$orderby',
|
|
431
|
+
s.orderby.map((o) => `${o.field} ${o.dir}`).join(','),
|
|
432
|
+
])
|
|
433
|
+
}
|
|
434
|
+
if (s.top !== undefined) pairs.push(['$top', String(s.top)])
|
|
435
|
+
if (s.skip !== undefined) pairs.push(['$skip', String(s.skip)])
|
|
436
|
+
if (s.count) pairs.push(['$count', 'true'])
|
|
437
|
+
if (s.search) pairs.push(['$search', s.search])
|
|
438
|
+
if (s.apply) pairs.push(['$apply', s.apply])
|
|
439
|
+
|
|
440
|
+
if (opts.topLevel) return buildQueryString(pairs)
|
|
441
|
+
return pairs.map(([k, v]) => `${k}=${v}`).join(';')
|
|
442
|
+
}
|