@mkja/o-data 0.0.1
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/README.md +416 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +13 -0
- package/dist/filter.d.ts +40 -0
- package/dist/filter.js +278 -0
- package/dist/index.d.ts +101 -0
- package/dist/index.js +419 -0
- package/dist/operations.d.ts +75 -0
- package/dist/operations.js +3 -0
- package/dist/parser/config.d.ts +152 -0
- package/dist/parser/config.js +3 -0
- package/dist/parser/index.d.ts +1 -0
- package/dist/parser/index.js +1157 -0
- package/dist/query.d.ts +43 -0
- package/dist/query.js +3 -0
- package/dist/response.d.ts +132 -0
- package/dist/response.js +3 -0
- package/dist/runtime.d.ts +4 -0
- package/dist/runtime.js +113 -0
- package/dist/schema.d.ts +128 -0
- package/dist/schema.js +11 -0
- package/dist/serialization.d.ts +42 -0
- package/dist/serialization.js +533 -0
- package/dist/types.d.ts +155 -0
- package/dist/types.js +3 -0
- package/package.json +34 -0
|
@@ -0,0 +1,533 @@
|
|
|
1
|
+
// ============================================================================
|
|
2
|
+
// Query String Serialization
|
|
3
|
+
// ============================================================================
|
|
4
|
+
import { createFilterHelpers, serializeFilter } from './filter.js';
|
|
5
|
+
import { buildQueryableEntity, findEntitySetsForEntityType } from './runtime.js';
|
|
6
|
+
// ============================================================================
|
|
7
|
+
// URL Path Normalization
|
|
8
|
+
// ============================================================================
|
|
9
|
+
/**
|
|
10
|
+
* Normalizes URL path segments by:
|
|
11
|
+
* - Removing trailing slashes from baseUrl (preserving protocol ://)
|
|
12
|
+
* - Removing leading slashes from path segments
|
|
13
|
+
* - Joining with single /
|
|
14
|
+
* - Normalizing multiple consecutive slashes (except protocol)
|
|
15
|
+
*/
|
|
16
|
+
export function normalizePath(baseUrl, ...paths) {
|
|
17
|
+
// Remove trailing slashes from baseUrl, but preserve protocol ://
|
|
18
|
+
let normalized = baseUrl.replace(/([^:]\/)\/+$/, '$1');
|
|
19
|
+
// Process each path segment
|
|
20
|
+
for (const path of paths) {
|
|
21
|
+
if (!path)
|
|
22
|
+
continue;
|
|
23
|
+
// Remove leading slashes from path segment
|
|
24
|
+
const cleanPath = path.replace(/^\/+/, '');
|
|
25
|
+
if (!cleanPath)
|
|
26
|
+
continue;
|
|
27
|
+
// Ensure single / between baseUrl and path
|
|
28
|
+
if (normalized && !normalized.endsWith('/')) {
|
|
29
|
+
normalized += '/';
|
|
30
|
+
}
|
|
31
|
+
normalized += cleanPath;
|
|
32
|
+
}
|
|
33
|
+
// Normalize multiple consecutive slashes (except protocol ://)
|
|
34
|
+
normalized = normalized.replace(/([^:]\/)\/+/g, '$1');
|
|
35
|
+
return normalized;
|
|
36
|
+
}
|
|
37
|
+
// ============================================================================
|
|
38
|
+
// Serialize Expand Options
|
|
39
|
+
// ============================================================================
|
|
40
|
+
function serializeExpandOptions(navQuery, navEntityDef, schema) {
|
|
41
|
+
const nestedParams = [];
|
|
42
|
+
if (navQuery.select) {
|
|
43
|
+
nestedParams.push(`$select=${navQuery.select.join(',')}`);
|
|
44
|
+
}
|
|
45
|
+
if (navQuery.expand) {
|
|
46
|
+
const nestedExpandParams = [];
|
|
47
|
+
for (const [nestedNavKey, nestedNavQuery] of Object.entries(navQuery.expand)) {
|
|
48
|
+
if (nestedNavQuery) {
|
|
49
|
+
let nestedNavEntityDef;
|
|
50
|
+
if (navEntityDef && nestedNavKey in navEntityDef.navigations) {
|
|
51
|
+
const nav = navEntityDef.navigations[nestedNavKey];
|
|
52
|
+
if (nav) {
|
|
53
|
+
const targetEntitysetKey = nav.targetEntitysetKey;
|
|
54
|
+
nestedNavEntityDef = buildQueryableEntity(schema, targetEntitysetKey);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
const nestedExpandOptionsStr = serializeExpandOptions(nestedNavQuery, nestedNavEntityDef, schema);
|
|
58
|
+
nestedExpandParams.push(`${nestedNavKey}${nestedExpandOptionsStr}`);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
if (nestedExpandParams.length > 0) {
|
|
62
|
+
nestedParams.push(`$expand=${nestedExpandParams.join(',')}`);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
const collectionQuery = navQuery;
|
|
66
|
+
if ('top' in collectionQuery && collectionQuery.top !== undefined) {
|
|
67
|
+
nestedParams.push(`$top=${collectionQuery.top}`);
|
|
68
|
+
}
|
|
69
|
+
if ('orderby' in collectionQuery && collectionQuery.orderby) {
|
|
70
|
+
const orderby = collectionQuery.orderby;
|
|
71
|
+
// orderby is readonly [keyof Properties, 'asc' | 'desc']
|
|
72
|
+
const [prop, dir] = orderby;
|
|
73
|
+
const orderbyValue = `${String(prop)} ${dir}`;
|
|
74
|
+
nestedParams.push(`$orderby=${orderbyValue}`);
|
|
75
|
+
}
|
|
76
|
+
if ('filter' in collectionQuery && collectionQuery.filter) {
|
|
77
|
+
if (typeof collectionQuery.filter === 'function') {
|
|
78
|
+
if (!navEntityDef) {
|
|
79
|
+
throw new Error('Entity definition required for filter builder in expand');
|
|
80
|
+
}
|
|
81
|
+
const helpers = createFilterHelpers(navEntityDef, schema);
|
|
82
|
+
const builder = collectionQuery.filter(helpers);
|
|
83
|
+
const state = builder.state;
|
|
84
|
+
const filterString = serializeFilter(state, 0, undefined, navEntityDef, schema);
|
|
85
|
+
nestedParams.push(`$filter=${encodeURIComponent(filterString)}`);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
if ('count' in collectionQuery && collectionQuery.count) {
|
|
89
|
+
nestedParams.push('$count=true');
|
|
90
|
+
}
|
|
91
|
+
return nestedParams.length > 0 ? `(${nestedParams.join(';')})` : '';
|
|
92
|
+
}
|
|
93
|
+
// ============================================================================
|
|
94
|
+
// Build Query String
|
|
95
|
+
// ============================================================================
|
|
96
|
+
export function buildQueryString(query, entityDef, schema) {
|
|
97
|
+
const params = [];
|
|
98
|
+
// $select
|
|
99
|
+
if (query.select) {
|
|
100
|
+
params.push(`$select=${query.select.join(',')}`);
|
|
101
|
+
}
|
|
102
|
+
// $expand
|
|
103
|
+
if (query.expand) {
|
|
104
|
+
const expandParams = [];
|
|
105
|
+
for (const [navKey, navQuery] of Object.entries(query.expand)) {
|
|
106
|
+
if (navQuery) {
|
|
107
|
+
let navEntityDef;
|
|
108
|
+
if (navKey in entityDef.navigations) {
|
|
109
|
+
const nav = entityDef.navigations[navKey];
|
|
110
|
+
if (nav) {
|
|
111
|
+
const targetEntitysetKey = nav.targetEntitysetKey;
|
|
112
|
+
navEntityDef = buildQueryableEntity(schema, targetEntitysetKey);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
const expandOptionsStr = serializeExpandOptions(navQuery, navEntityDef, schema);
|
|
116
|
+
expandParams.push(`${navKey}${expandOptionsStr}`);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
if (expandParams.length > 0) {
|
|
120
|
+
params.push(`$expand=${expandParams.join(',')}`);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
// Check if this is a collection query (has collection-specific params or filter/orderby)
|
|
124
|
+
// SingleQueryObject doesn't have filter/orderby, so if they exist, it's a CollectionQueryObject
|
|
125
|
+
const isCollectionQuery = 'top' in query ||
|
|
126
|
+
'skip' in query ||
|
|
127
|
+
'count' in query ||
|
|
128
|
+
'filter' in query ||
|
|
129
|
+
'orderby' in query;
|
|
130
|
+
if (isCollectionQuery) {
|
|
131
|
+
const collectionQuery = query;
|
|
132
|
+
if ('top' in collectionQuery && collectionQuery.top !== undefined) {
|
|
133
|
+
params.push(`$top=${collectionQuery.top}`);
|
|
134
|
+
}
|
|
135
|
+
if ('skip' in collectionQuery && collectionQuery.skip !== undefined) {
|
|
136
|
+
params.push(`$skip=${collectionQuery.skip}`);
|
|
137
|
+
}
|
|
138
|
+
if ('orderby' in collectionQuery && collectionQuery.orderby) {
|
|
139
|
+
const orderby = collectionQuery.orderby;
|
|
140
|
+
// orderby is readonly [keyof Properties, 'asc' | 'desc']
|
|
141
|
+
const [prop, dir] = orderby;
|
|
142
|
+
const orderbyValue = `${String(prop)} ${dir}`;
|
|
143
|
+
params.push(`$orderby=${orderbyValue}`);
|
|
144
|
+
}
|
|
145
|
+
if ('filter' in collectionQuery && collectionQuery.filter) {
|
|
146
|
+
if (typeof collectionQuery.filter === 'function') {
|
|
147
|
+
const helpers = createFilterHelpers(entityDef, schema);
|
|
148
|
+
const builder = collectionQuery.filter(helpers);
|
|
149
|
+
const state = builder.state;
|
|
150
|
+
const filterString = serializeFilter(state, 0, undefined, entityDef, schema);
|
|
151
|
+
params.push(`$filter=${encodeURIComponent(filterString)}`);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
if ('count' in collectionQuery && collectionQuery.count) {
|
|
155
|
+
params.push('$count=true');
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
// Note: Single entity queries (SingleQueryObject) only have select and expand,
|
|
159
|
+
// so filter and orderby are not serialized (they don't exist on the type)
|
|
160
|
+
return params.length > 0 ? `?${params.join('&')}` : '';
|
|
161
|
+
}
|
|
162
|
+
// ============================================================================
|
|
163
|
+
// Create/Update Object Transformation
|
|
164
|
+
// ============================================================================
|
|
165
|
+
/**
|
|
166
|
+
* Transform create object to handle navigation properties with @odata.bind format
|
|
167
|
+
*/
|
|
168
|
+
export function transformCreateObjectForBind(createObject, entityDef, schema) {
|
|
169
|
+
if (!entityDef || !entityDef.navigations)
|
|
170
|
+
return createObject;
|
|
171
|
+
const transformed = {};
|
|
172
|
+
for (const [key, value] of Object.entries(createObject)) {
|
|
173
|
+
// Check for batch reference first
|
|
174
|
+
if (typeof value === 'string' && value.startsWith('$')) {
|
|
175
|
+
transformed[`${key}@odata.bind`] = value;
|
|
176
|
+
continue;
|
|
177
|
+
}
|
|
178
|
+
const navDef = entityDef.navigations[key];
|
|
179
|
+
if (navDef && navDef.targetEntitysetKey) {
|
|
180
|
+
const isCollection = navDef.collection === true;
|
|
181
|
+
if (!isCollection) {
|
|
182
|
+
// Single-valued navigation
|
|
183
|
+
if (Array.isArray(value) &&
|
|
184
|
+
value.length === 2 &&
|
|
185
|
+
typeof value[0] === 'string' &&
|
|
186
|
+
(typeof value[1] === 'string' || typeof value[1] === 'number')) {
|
|
187
|
+
// Explicit entityset format: [entityset, id]
|
|
188
|
+
const [set, id] = value;
|
|
189
|
+
transformed[`${key}@odata.bind`] = `/${set}(${id})`;
|
|
190
|
+
}
|
|
191
|
+
else if (typeof value === 'string' || typeof value === 'number') {
|
|
192
|
+
// Plain ID - resolve entityset from navigation
|
|
193
|
+
const target = Array.isArray(navDef.targetEntitysetKey)
|
|
194
|
+
? navDef.targetEntitysetKey[0]
|
|
195
|
+
: navDef.targetEntitysetKey;
|
|
196
|
+
transformed[`${key}@odata.bind`] = `/${target}(${value})`;
|
|
197
|
+
}
|
|
198
|
+
else if (typeof value === 'object' && value !== null) {
|
|
199
|
+
// Deep insert - recursive transformation
|
|
200
|
+
const targetEntitysetKey = Array.isArray(navDef.targetEntitysetKey)
|
|
201
|
+
? navDef.targetEntitysetKey[0]
|
|
202
|
+
: navDef.targetEntitysetKey;
|
|
203
|
+
if (targetEntitysetKey != null) {
|
|
204
|
+
const targetEntity = buildQueryableEntity(schema, targetEntitysetKey);
|
|
205
|
+
transformed[key] = transformCreateObjectForBind(value, targetEntity, schema);
|
|
206
|
+
}
|
|
207
|
+
else {
|
|
208
|
+
transformed[key] = value;
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
else {
|
|
212
|
+
transformed[key] = value;
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
else {
|
|
216
|
+
// Collection navigation
|
|
217
|
+
if (Array.isArray(value)) {
|
|
218
|
+
if (value.length > 0 && (typeof value[0] === 'string' || typeof value[0] === 'number')) {
|
|
219
|
+
// Array of string/number IDs (or batch references)
|
|
220
|
+
const target = Array.isArray(navDef.targetEntitysetKey)
|
|
221
|
+
? navDef.targetEntitysetKey[0]
|
|
222
|
+
: navDef.targetEntitysetKey;
|
|
223
|
+
transformed[`${key}@odata.bind`] = value.map((v) => typeof v === 'string' && v.startsWith('$') ? v : `/${target}(${v})`);
|
|
224
|
+
}
|
|
225
|
+
else if (value.length > 0 && Array.isArray(value[0])) {
|
|
226
|
+
// Array of [entityset, id] tuples
|
|
227
|
+
transformed[`${key}@odata.bind`] = value.map(([set, id]) => `/${set}(${id})`);
|
|
228
|
+
}
|
|
229
|
+
else {
|
|
230
|
+
// Array of objects - deep insert (recursive)
|
|
231
|
+
const targetEntitysetKey = Array.isArray(navDef.targetEntitysetKey)
|
|
232
|
+
? navDef.targetEntitysetKey[0]
|
|
233
|
+
: navDef.targetEntitysetKey;
|
|
234
|
+
if (targetEntitysetKey != null) {
|
|
235
|
+
const targetEntity = buildQueryableEntity(schema, targetEntitysetKey);
|
|
236
|
+
transformed[key] = value.map((item) => typeof item === 'object' && item !== null
|
|
237
|
+
? transformCreateObjectForBind(item, targetEntity, schema)
|
|
238
|
+
: item);
|
|
239
|
+
}
|
|
240
|
+
else {
|
|
241
|
+
transformed[key] = value;
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
else {
|
|
246
|
+
transformed[key] = value;
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
else {
|
|
251
|
+
transformed[key] = value;
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
return transformed;
|
|
255
|
+
}
|
|
256
|
+
/**
|
|
257
|
+
* Transform update object to handle navigation properties with @odata.bind format
|
|
258
|
+
*/
|
|
259
|
+
export function transformUpdateObjectForBind(updateObject, entityDef, schema) {
|
|
260
|
+
if (!entityDef || !entityDef.navigations)
|
|
261
|
+
return updateObject;
|
|
262
|
+
const transformed = {};
|
|
263
|
+
for (const [key, value] of Object.entries(updateObject)) {
|
|
264
|
+
// Check for batch reference first
|
|
265
|
+
if (typeof value === 'string' && value.startsWith('$')) {
|
|
266
|
+
transformed[`${key}@odata.bind`] = value;
|
|
267
|
+
continue;
|
|
268
|
+
}
|
|
269
|
+
const navDef = entityDef.navigations[key];
|
|
270
|
+
if (navDef && navDef.targetEntitysetKey) {
|
|
271
|
+
if (value === null) {
|
|
272
|
+
// Set navigation to null
|
|
273
|
+
transformed[key] = null;
|
|
274
|
+
}
|
|
275
|
+
else if (Array.isArray(value) && !navDef.collection && value.length === 2) {
|
|
276
|
+
// Single-valued navigation with explicit entityset: [entityset, id]
|
|
277
|
+
const [set, id] = value;
|
|
278
|
+
transformed[`${key}@odata.bind`] = `/${set}(${id})`;
|
|
279
|
+
}
|
|
280
|
+
else if ((typeof value === 'string' || typeof value === 'number') && !navDef.collection) {
|
|
281
|
+
// Single-valued navigation with plain ID
|
|
282
|
+
const target = Array.isArray(navDef.targetEntitysetKey)
|
|
283
|
+
? navDef.targetEntitysetKey[0]
|
|
284
|
+
: navDef.targetEntitysetKey;
|
|
285
|
+
transformed[`${key}@odata.bind`] = `/${target}(${value})`;
|
|
286
|
+
}
|
|
287
|
+
else if (typeof value === 'object' && value !== null) {
|
|
288
|
+
// Check if it's a collection operation spec
|
|
289
|
+
const spec = value;
|
|
290
|
+
if (spec.replace || spec.add || spec.remove) {
|
|
291
|
+
// Collection operation
|
|
292
|
+
const transformedSpec = {};
|
|
293
|
+
const targetEntitysetKey = Array.isArray(navDef.targetEntitysetKey)
|
|
294
|
+
? navDef.targetEntitysetKey[0]
|
|
295
|
+
: navDef.targetEntitysetKey;
|
|
296
|
+
const formatRef = (v) => {
|
|
297
|
+
// Check for batch reference
|
|
298
|
+
if (typeof v === 'string' && v.startsWith('$'))
|
|
299
|
+
return v;
|
|
300
|
+
// Explicit entityset format
|
|
301
|
+
if (Array.isArray(v))
|
|
302
|
+
return `/${v[0]}(${v[1]})`;
|
|
303
|
+
// Use resolved entityset
|
|
304
|
+
return `/${targetEntitysetKey}(${v})`;
|
|
305
|
+
};
|
|
306
|
+
if (spec.replace && Array.isArray(spec.replace)) {
|
|
307
|
+
transformedSpec.replace = spec.replace.map(formatRef);
|
|
308
|
+
}
|
|
309
|
+
if (spec.add && Array.isArray(spec.add)) {
|
|
310
|
+
transformedSpec.add = spec.add.map(formatRef);
|
|
311
|
+
}
|
|
312
|
+
if (spec.remove && Array.isArray(spec.remove)) {
|
|
313
|
+
transformedSpec.remove = spec.remove.map(formatRef);
|
|
314
|
+
}
|
|
315
|
+
transformed[key] = transformedSpec;
|
|
316
|
+
}
|
|
317
|
+
else {
|
|
318
|
+
// Regular object - pass through (not a navigation operation)
|
|
319
|
+
transformed[key] = value;
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
else {
|
|
323
|
+
transformed[key] = value;
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
else {
|
|
327
|
+
transformed[key] = value;
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
return transformed;
|
|
331
|
+
}
|
|
332
|
+
// ============================================================================
|
|
333
|
+
// Build Create/Update Requests
|
|
334
|
+
// ============================================================================
|
|
335
|
+
/**
|
|
336
|
+
* Build HTTP Request for create operation
|
|
337
|
+
*/
|
|
338
|
+
export function buildCreateRequest(path, createObject, options, baseUrl, entityDef, schema) {
|
|
339
|
+
let url = normalizePath(baseUrl, path);
|
|
340
|
+
const headers = new Headers({ 'Content-Type': 'application/json', Accept: 'application/json' });
|
|
341
|
+
const select = options?.select;
|
|
342
|
+
const preferParts = [];
|
|
343
|
+
if (options?.prefer?.return_representation === true ||
|
|
344
|
+
(select && Array.isArray(select) && select.length > 0)) {
|
|
345
|
+
preferParts.push('return=representation');
|
|
346
|
+
}
|
|
347
|
+
if (preferParts.length > 0) {
|
|
348
|
+
headers.set('Prefer', preferParts.join(','));
|
|
349
|
+
}
|
|
350
|
+
if (options?.headers) {
|
|
351
|
+
for (const [key, value] of Object.entries(options.headers)) {
|
|
352
|
+
headers.set(key, value);
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
if (select && Array.isArray(select) && select.length > 0) {
|
|
356
|
+
url += `?$select=${select.join(',')}`;
|
|
357
|
+
}
|
|
358
|
+
const transformedObject = transformCreateObjectForBind(createObject, entityDef, schema);
|
|
359
|
+
return new Request(url, { method: 'POST', headers, body: JSON.stringify(transformedObject) });
|
|
360
|
+
}
|
|
361
|
+
/**
|
|
362
|
+
* Build HTTP Request for update operation
|
|
363
|
+
*/
|
|
364
|
+
export function buildUpdateRequest(path, updateObject, options, baseUrl, entityDef, schema) {
|
|
365
|
+
let url = normalizePath(baseUrl, path);
|
|
366
|
+
const headers = new Headers({ 'Content-Type': 'application/json', Accept: 'application/json' });
|
|
367
|
+
const select = options?.select;
|
|
368
|
+
const preferParts = [];
|
|
369
|
+
if (options?.prefer?.return_representation === true ||
|
|
370
|
+
(select && Array.isArray(select) && select.length > 0)) {
|
|
371
|
+
preferParts.push('return=representation');
|
|
372
|
+
}
|
|
373
|
+
if (preferParts.length > 0) {
|
|
374
|
+
headers.set('Prefer', preferParts.join(','));
|
|
375
|
+
}
|
|
376
|
+
if (options?.headers) {
|
|
377
|
+
for (const [key, value] of Object.entries(options.headers)) {
|
|
378
|
+
headers.set(key, value);
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
if (select && Array.isArray(select) && select.length > 0) {
|
|
382
|
+
url += `?$select=${select.join(',')}`;
|
|
383
|
+
}
|
|
384
|
+
const transformedObject = transformUpdateObjectForBind(updateObject, entityDef, schema);
|
|
385
|
+
return new Request(url, { method: 'PATCH', headers, body: JSON.stringify(transformedObject) });
|
|
386
|
+
}
|
|
387
|
+
// ============================================================================
|
|
388
|
+
// Action/Function Request Serialization
|
|
389
|
+
// ============================================================================
|
|
390
|
+
/**
|
|
391
|
+
* Transform action/function parameters to handle navigation (entity type) parameters.
|
|
392
|
+
* Converts string/number IDs to @odata.bind format and handles deep inserts.
|
|
393
|
+
*/
|
|
394
|
+
export function transformActionParameters(parameters, parameterDefs, schema) {
|
|
395
|
+
const transformed = {};
|
|
396
|
+
for (const [key, value] of Object.entries(parameters)) {
|
|
397
|
+
const paramDef = parameterDefs[key];
|
|
398
|
+
// Check if this parameter is a navigation type (entity type parameter)
|
|
399
|
+
if (paramDef && typeof paramDef === 'object' && 'type' in paramDef && paramDef.type === 'navigation') {
|
|
400
|
+
const navDef = paramDef;
|
|
401
|
+
const targetEntityType = navDef.target;
|
|
402
|
+
const isCollection = navDef.collection === true;
|
|
403
|
+
// Resolve entityset(s) for this entity type
|
|
404
|
+
const entitysetKey = findEntitySetsForEntityType(schema, targetEntityType);
|
|
405
|
+
if (!entitysetKey) {
|
|
406
|
+
// No entityset found - pass through as-is (shouldn't happen in valid schemas)
|
|
407
|
+
transformed[key] = value;
|
|
408
|
+
continue;
|
|
409
|
+
}
|
|
410
|
+
// Resolve target entityset (use first if multiple)
|
|
411
|
+
const targetEntitysetKey = Array.isArray(entitysetKey) ? entitysetKey[0] : entitysetKey;
|
|
412
|
+
if (!isCollection) {
|
|
413
|
+
// Single-valued navigation parameter
|
|
414
|
+
if (typeof value === 'string' && value.startsWith('$')) {
|
|
415
|
+
// Batch reference
|
|
416
|
+
transformed[`${key}@odata.bind`] = value;
|
|
417
|
+
}
|
|
418
|
+
else if (Array.isArray(value) &&
|
|
419
|
+
value.length === 2 &&
|
|
420
|
+
typeof value[0] === 'string' &&
|
|
421
|
+
(typeof value[1] === 'string' || typeof value[1] === 'number')) {
|
|
422
|
+
// Explicit entityset format: [entityset, id]
|
|
423
|
+
const [set, id] = value;
|
|
424
|
+
transformed[`${key}@odata.bind`] = `/${set}(${id})`;
|
|
425
|
+
}
|
|
426
|
+
else if (typeof value === 'string' || typeof value === 'number') {
|
|
427
|
+
// Plain ID - resolve entityset from parameter definition
|
|
428
|
+
transformed[`${key}@odata.bind`] = `/${targetEntitysetKey}(${value})`;
|
|
429
|
+
}
|
|
430
|
+
else if (typeof value === 'object' && value !== null) {
|
|
431
|
+
// Deep insert - recursive transformation
|
|
432
|
+
if (targetEntitysetKey != null) {
|
|
433
|
+
const targetEntity = buildQueryableEntity(schema, targetEntitysetKey);
|
|
434
|
+
transformed[key] = transformCreateObjectForBind(value, targetEntity, schema);
|
|
435
|
+
}
|
|
436
|
+
else {
|
|
437
|
+
transformed[key] = value;
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
else {
|
|
441
|
+
transformed[key] = value;
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
else {
|
|
445
|
+
// Collection navigation parameter
|
|
446
|
+
if (Array.isArray(value)) {
|
|
447
|
+
if (value.length > 0 && (typeof value[0] === 'string' || typeof value[0] === 'number')) {
|
|
448
|
+
// Array of string/number IDs (or batch references)
|
|
449
|
+
transformed[`${key}@odata.bind`] = value.map((v) => typeof v === 'string' && v.startsWith('$') ? v : `/${targetEntitysetKey}(${v})`);
|
|
450
|
+
}
|
|
451
|
+
else if (value.length > 0 && Array.isArray(value[0])) {
|
|
452
|
+
// Array of [entityset, id] tuples
|
|
453
|
+
transformed[`${key}@odata.bind`] = value.map(([set, id]) => `/${set}(${id})`);
|
|
454
|
+
}
|
|
455
|
+
else {
|
|
456
|
+
// Array of objects - deep insert (recursive)
|
|
457
|
+
if (targetEntitysetKey != null) {
|
|
458
|
+
const targetEntity = buildQueryableEntity(schema, targetEntitysetKey);
|
|
459
|
+
transformed[key] = value.map((item) => typeof item === 'object' && item !== null
|
|
460
|
+
? transformCreateObjectForBind(item, targetEntity, schema)
|
|
461
|
+
: item);
|
|
462
|
+
}
|
|
463
|
+
else {
|
|
464
|
+
transformed[key] = value;
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
else {
|
|
469
|
+
transformed[key] = value;
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
else {
|
|
474
|
+
// Not a navigation parameter - pass through as-is
|
|
475
|
+
transformed[key] = value;
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
return transformed;
|
|
479
|
+
}
|
|
480
|
+
/**
|
|
481
|
+
* Build a POST request for an OData action.
|
|
482
|
+
*/
|
|
483
|
+
export function buildActionRequest(path, namespace, actionName, parameters, parameterDefs, schema, baseUrl = '', useFQN = true) {
|
|
484
|
+
const fullActionName = useFQN ? `${namespace}.${actionName}` : actionName;
|
|
485
|
+
const url = normalizePath(baseUrl, path, fullActionName);
|
|
486
|
+
const headers = new Headers({
|
|
487
|
+
'Content-Type': 'application/json',
|
|
488
|
+
Accept: 'application/json',
|
|
489
|
+
});
|
|
490
|
+
// Transform parameters - handle entity parameters for deep inserts/binds
|
|
491
|
+
const transformedParams = transformActionParameters(parameters, parameterDefs, schema);
|
|
492
|
+
return new Request(url, {
|
|
493
|
+
method: 'POST',
|
|
494
|
+
headers,
|
|
495
|
+
body: JSON.stringify(transformedParams),
|
|
496
|
+
});
|
|
497
|
+
}
|
|
498
|
+
/**
|
|
499
|
+
* Build a GET request for an OData function.
|
|
500
|
+
*/
|
|
501
|
+
export function buildFunctionRequest(path, namespace, functionName, parameters, baseUrl = '', useFQN = true) {
|
|
502
|
+
const fullFuncName = useFQN ? `${namespace}.${functionName}` : functionName;
|
|
503
|
+
const paramKeys = Object.keys(parameters);
|
|
504
|
+
let urlParamsStr = '';
|
|
505
|
+
const queryParams = [];
|
|
506
|
+
if (paramKeys.length > 0) {
|
|
507
|
+
urlParamsStr = '(' + paramKeys.map((k) => `${k}=@${k}`).join(',') + ')';
|
|
508
|
+
for (const [key, value] of Object.entries(parameters)) {
|
|
509
|
+
let serializedValue;
|
|
510
|
+
if (typeof value === 'string') {
|
|
511
|
+
serializedValue = `'${value}'`;
|
|
512
|
+
}
|
|
513
|
+
else if (value instanceof Date) {
|
|
514
|
+
serializedValue = value.toISOString();
|
|
515
|
+
}
|
|
516
|
+
else if (typeof value === 'object' && value !== null) {
|
|
517
|
+
serializedValue = JSON.stringify(value);
|
|
518
|
+
}
|
|
519
|
+
else {
|
|
520
|
+
serializedValue = String(value);
|
|
521
|
+
}
|
|
522
|
+
queryParams.push(`@${key}=${encodeURIComponent(serializedValue)}`);
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
let url = normalizePath(baseUrl, path, fullFuncName + urlParamsStr);
|
|
526
|
+
if (queryParams.length > 0) {
|
|
527
|
+
url += '?' + queryParams.join('&');
|
|
528
|
+
}
|
|
529
|
+
return new Request(url, {
|
|
530
|
+
method: 'GET',
|
|
531
|
+
headers: new Headers({ Accept: 'application/json' }),
|
|
532
|
+
});
|
|
533
|
+
}
|