simplesvelte 2.2.11 → 2.2.13
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/dist/Select.svelte +63 -28
- package/dist/ag-grid-refactored.js +30 -1
- package/dist/powerAppQuery.d.ts +398 -0
- package/dist/powerAppQuery.js +789 -0
- package/package.json +1 -1
- package/dist/ag-grid.d.ts +0 -551
- package/dist/ag-grid.js +0 -901
|
@@ -0,0 +1,789 @@
|
|
|
1
|
+
// ============================================================================
|
|
2
|
+
// Helper: Build OData $select Clause
|
|
3
|
+
// ============================================================================
|
|
4
|
+
/**
|
|
5
|
+
* Builds the OData $select query parameter from column selections
|
|
6
|
+
*
|
|
7
|
+
* @param columns - Array of column names to select
|
|
8
|
+
* @returns OData $select string (e.g., "name,revenue,statecode")
|
|
9
|
+
*
|
|
10
|
+
* @example
|
|
11
|
+
* ```typescript
|
|
12
|
+
* buildODataSelect(['accountid', 'name', 'revenue'])
|
|
13
|
+
* // Returns: "accountid,name,revenue"
|
|
14
|
+
*
|
|
15
|
+
* buildODataSelect(['contactid', 'fullname', 'parentcustomerid_account/name'])
|
|
16
|
+
* // Returns: "contactid,fullname,parentcustomerid_account/name"
|
|
17
|
+
* ```
|
|
18
|
+
*/
|
|
19
|
+
export function buildODataSelect(columns) {
|
|
20
|
+
if (!columns || columns.length === 0)
|
|
21
|
+
return undefined;
|
|
22
|
+
return columns.join(',');
|
|
23
|
+
}
|
|
24
|
+
// ============================================================================
|
|
25
|
+
// Helper: Build OData $orderby Clause
|
|
26
|
+
// ============================================================================
|
|
27
|
+
/**
|
|
28
|
+
* Builds the OData $orderby query parameter from AG Grid sort model
|
|
29
|
+
*
|
|
30
|
+
* OData orderby syntax: "field1 asc,field2 desc"
|
|
31
|
+
*
|
|
32
|
+
* @param sortModel - AG Grid sort model
|
|
33
|
+
* @param defaultOrderBy - Fallback sort if no sort model provided
|
|
34
|
+
* @returns OData $orderby string (e.g., "name asc,revenue desc")
|
|
35
|
+
*
|
|
36
|
+
* @example
|
|
37
|
+
* ```typescript
|
|
38
|
+
* buildODataOrderBy([
|
|
39
|
+
* { colId: 'name', sort: 'asc' },
|
|
40
|
+
* { colId: 'revenue', sort: 'desc' }
|
|
41
|
+
* ])
|
|
42
|
+
* // Returns: "name asc,revenue desc"
|
|
43
|
+
* ```
|
|
44
|
+
*/
|
|
45
|
+
export function buildODataOrderBy(sortModel, defaultOrderBy) {
|
|
46
|
+
if (!sortModel || sortModel.length === 0) {
|
|
47
|
+
return defaultOrderBy;
|
|
48
|
+
}
|
|
49
|
+
return sortModel.map((sort) => `${sort.colId} ${sort.sort}`).join(',');
|
|
50
|
+
}
|
|
51
|
+
// ============================================================================
|
|
52
|
+
// Helper: Build OData $filter Clause
|
|
53
|
+
// ============================================================================
|
|
54
|
+
/**
|
|
55
|
+
* Escapes single quotes in OData string values
|
|
56
|
+
* OData requires single quotes to be doubled (e.g., "O'Bryan" -> "O''Bryan")
|
|
57
|
+
*/
|
|
58
|
+
function escapeODataString(value) {
|
|
59
|
+
return value.replace(/'/g, "''");
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Formats a value for use in OData filter expressions
|
|
63
|
+
*/
|
|
64
|
+
function formatODataValue(value) {
|
|
65
|
+
if (value === null || value === undefined) {
|
|
66
|
+
return 'null';
|
|
67
|
+
}
|
|
68
|
+
if (typeof value === 'string') {
|
|
69
|
+
// Dates in ISO format should not be quoted
|
|
70
|
+
if (/^\d{4}-\d{2}-\d{2}T/.test(value)) {
|
|
71
|
+
return value;
|
|
72
|
+
}
|
|
73
|
+
return `'${escapeODataString(value)}'`;
|
|
74
|
+
}
|
|
75
|
+
if (typeof value === 'boolean') {
|
|
76
|
+
return value ? 'true' : 'false';
|
|
77
|
+
}
|
|
78
|
+
if (value instanceof Date) {
|
|
79
|
+
return value.toISOString();
|
|
80
|
+
}
|
|
81
|
+
// Numbers and other primitives
|
|
82
|
+
return String(value);
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Converts AG Grid text filter to OData expression
|
|
86
|
+
*/
|
|
87
|
+
function convertTextFilter(field, filter) {
|
|
88
|
+
const type = filter.type;
|
|
89
|
+
const value = filter.filter;
|
|
90
|
+
switch (type) {
|
|
91
|
+
case 'equals':
|
|
92
|
+
return `${field} eq ${formatODataValue(value)}`;
|
|
93
|
+
case 'notEqual':
|
|
94
|
+
return `${field} ne ${formatODataValue(value)}`;
|
|
95
|
+
case 'contains':
|
|
96
|
+
return `contains(${field},${formatODataValue(value)})`;
|
|
97
|
+
case 'notContains':
|
|
98
|
+
return `not contains(${field},${formatODataValue(value)})`;
|
|
99
|
+
case 'startsWith':
|
|
100
|
+
return `startswith(${field},${formatODataValue(value)})`;
|
|
101
|
+
case 'endsWith':
|
|
102
|
+
return `endswith(${field},${formatODataValue(value)})`;
|
|
103
|
+
case 'blank':
|
|
104
|
+
case 'empty':
|
|
105
|
+
return `${field} eq null`;
|
|
106
|
+
case 'notBlank':
|
|
107
|
+
return `${field} ne null`;
|
|
108
|
+
default:
|
|
109
|
+
console.warn('Unknown text filter type:', type);
|
|
110
|
+
return '';
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* Converts AG Grid number filter to OData expression
|
|
115
|
+
*/
|
|
116
|
+
function convertNumberFilter(field, filter) {
|
|
117
|
+
const type = filter.type;
|
|
118
|
+
const value = filter.filter;
|
|
119
|
+
const valueTo = filter.filterTo;
|
|
120
|
+
switch (type) {
|
|
121
|
+
case 'equals':
|
|
122
|
+
return `${field} eq ${value}`;
|
|
123
|
+
case 'notEqual':
|
|
124
|
+
return `${field} ne ${value}`;
|
|
125
|
+
case 'greaterThan':
|
|
126
|
+
return `${field} gt ${value}`;
|
|
127
|
+
case 'greaterThanOrEqual':
|
|
128
|
+
return `${field} ge ${value}`;
|
|
129
|
+
case 'lessThan':
|
|
130
|
+
return `${field} lt ${value}`;
|
|
131
|
+
case 'lessThanOrEqual':
|
|
132
|
+
return `${field} le ${value}`;
|
|
133
|
+
case 'inRange':
|
|
134
|
+
return `${field} ge ${value} and ${field} le ${valueTo}`;
|
|
135
|
+
case 'blank':
|
|
136
|
+
case 'empty':
|
|
137
|
+
return `${field} eq null`;
|
|
138
|
+
case 'notBlank':
|
|
139
|
+
return `${field} ne null`;
|
|
140
|
+
default:
|
|
141
|
+
console.warn('Unknown number filter type:', type);
|
|
142
|
+
return '';
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
/**
|
|
146
|
+
* Converts AG Grid date filter to OData expression
|
|
147
|
+
*/
|
|
148
|
+
function convertDateFilter(field, filter) {
|
|
149
|
+
const type = filter.type;
|
|
150
|
+
let dateFrom = filter.dateFrom;
|
|
151
|
+
let dateTo = filter.dateTo;
|
|
152
|
+
// Ensure dates are in ISO format
|
|
153
|
+
if (dateFrom && !/^\d{4}-\d{2}-\d{2}T/.test(dateFrom)) {
|
|
154
|
+
dateFrom = new Date(dateFrom).toISOString();
|
|
155
|
+
}
|
|
156
|
+
if (dateTo && !/^\d{4}-\d{2}-\d{2}T/.test(dateTo)) {
|
|
157
|
+
dateTo = new Date(dateTo).toISOString();
|
|
158
|
+
}
|
|
159
|
+
switch (type) {
|
|
160
|
+
case 'equals':
|
|
161
|
+
return `${field} eq ${dateFrom}`;
|
|
162
|
+
case 'notEqual':
|
|
163
|
+
return `${field} ne ${dateFrom}`;
|
|
164
|
+
case 'greaterThan':
|
|
165
|
+
return `${field} gt ${dateFrom}`;
|
|
166
|
+
case 'greaterThanOrEqual':
|
|
167
|
+
return `${field} ge ${dateFrom}`;
|
|
168
|
+
case 'lessThan':
|
|
169
|
+
return `${field} lt ${dateFrom}`;
|
|
170
|
+
case 'lessThanOrEqual':
|
|
171
|
+
return `${field} le ${dateFrom}`;
|
|
172
|
+
case 'inRange':
|
|
173
|
+
return `${field} ge ${dateFrom} and ${field} le ${dateTo}`;
|
|
174
|
+
case 'blank':
|
|
175
|
+
case 'empty':
|
|
176
|
+
return `${field} eq null`;
|
|
177
|
+
case 'notBlank':
|
|
178
|
+
return `${field} ne null`;
|
|
179
|
+
default:
|
|
180
|
+
console.warn('Unknown date filter type:', type);
|
|
181
|
+
return '';
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
/**
|
|
185
|
+
* Converts AG Grid set filter (multi-select) to OData expression
|
|
186
|
+
*/
|
|
187
|
+
function convertSetFilter(field, filter) {
|
|
188
|
+
const values = filter.values;
|
|
189
|
+
if (!values || values.length === 0) {
|
|
190
|
+
return '';
|
|
191
|
+
}
|
|
192
|
+
// Check if this is a boolean filter
|
|
193
|
+
const booleanValues = new Set(['Yes', 'No', 'true', 'false', true, false]);
|
|
194
|
+
const isBooleanFilter = values.every((v) => booleanValues.has(v));
|
|
195
|
+
if (isBooleanFilter) {
|
|
196
|
+
// Convert to boolean
|
|
197
|
+
const converted = values.map((v) => v === 'Yes' || v === 'true' || v === true);
|
|
198
|
+
// Single value - use direct equality
|
|
199
|
+
if (converted.length === 1) {
|
|
200
|
+
return `${field} eq ${converted[0]}`;
|
|
201
|
+
}
|
|
202
|
+
// Both true and false selected - no filter needed (show all)
|
|
203
|
+
if (converted.length === 2) {
|
|
204
|
+
return '';
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
// For multiple values, use 'or' conditions
|
|
208
|
+
// OData doesn't have a native 'in' operator like SQL, so we use multiple 'or' conditions
|
|
209
|
+
const conditions = values.map((v) => `${field} eq ${formatODataValue(v)}`);
|
|
210
|
+
return `(${conditions.join(' or ')})`;
|
|
211
|
+
}
|
|
212
|
+
/**
|
|
213
|
+
* Converts a single AG Grid filter to OData expression
|
|
214
|
+
*/
|
|
215
|
+
function convertFilterToOData(field, filterValue) {
|
|
216
|
+
if (!filterValue || typeof filterValue !== 'object') {
|
|
217
|
+
return '';
|
|
218
|
+
}
|
|
219
|
+
const filter = filterValue;
|
|
220
|
+
const filterType = filter.filterType;
|
|
221
|
+
switch (filterType) {
|
|
222
|
+
case 'text':
|
|
223
|
+
return convertTextFilter(field, filter);
|
|
224
|
+
case 'number':
|
|
225
|
+
return convertNumberFilter(field, filter);
|
|
226
|
+
case 'date':
|
|
227
|
+
return convertDateFilter(field, filter);
|
|
228
|
+
case 'set':
|
|
229
|
+
return convertSetFilter(field, filter);
|
|
230
|
+
default:
|
|
231
|
+
console.warn('Unknown filter type:', filterType, 'for field:', field);
|
|
232
|
+
return '';
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
/**
|
|
236
|
+
* Builds the OData $filter query parameter from AG Grid filter model
|
|
237
|
+
*
|
|
238
|
+
* Converts AG Grid's filter model to OData filter syntax, supporting:
|
|
239
|
+
* - Text filters: eq, ne, contains, startswith, endswith
|
|
240
|
+
* - Number filters: eq, ne, gt, ge, lt, le, inRange
|
|
241
|
+
* - Date filters: eq, ne, gt, ge, lt, le, inRange
|
|
242
|
+
* - Set filters: multi-value OR conditions
|
|
243
|
+
* - Null checks: blank/notBlank
|
|
244
|
+
*
|
|
245
|
+
* @param filterModel - AG Grid filter model
|
|
246
|
+
* @returns OData $filter string
|
|
247
|
+
*
|
|
248
|
+
* @example
|
|
249
|
+
* ```typescript
|
|
250
|
+
* // Text filter
|
|
251
|
+
* buildODataFilter({ name: { filterType: 'text', type: 'contains', filter: 'John' } })
|
|
252
|
+
* // Returns: "contains(name,'John')"
|
|
253
|
+
*
|
|
254
|
+
* // Number range
|
|
255
|
+
* buildODataFilter({
|
|
256
|
+
* revenue: { filterType: 'number', type: 'inRange', filter: 1000, filterTo: 5000 }
|
|
257
|
+
* })
|
|
258
|
+
* // Returns: "revenue ge 1000 and revenue le 5000"
|
|
259
|
+
*
|
|
260
|
+
* // Multiple filters (AND logic)
|
|
261
|
+
* buildODataFilter({
|
|
262
|
+
* name: { filterType: 'text', type: 'contains', filter: 'Corp' },
|
|
263
|
+
* revenue: { filterType: 'number', type: 'greaterThan', filter: 10000 }
|
|
264
|
+
* })
|
|
265
|
+
* // Returns: "contains(name,'Corp') and revenue gt 10000"
|
|
266
|
+
*
|
|
267
|
+
* // Set filter (OR logic for multiple values)
|
|
268
|
+
* buildODataFilter({
|
|
269
|
+
* status: { filterType: 'set', values: ['Active', 'Pending'] }
|
|
270
|
+
* })
|
|
271
|
+
* // Returns: "(status eq 'Active' or status eq 'Pending')"
|
|
272
|
+
* ```
|
|
273
|
+
*/
|
|
274
|
+
export function buildODataFilter(filterModel) {
|
|
275
|
+
if (!filterModel || Object.keys(filterModel).length === 0) {
|
|
276
|
+
return undefined;
|
|
277
|
+
}
|
|
278
|
+
const filters = [];
|
|
279
|
+
for (const [field, filterValue] of Object.entries(filterModel)) {
|
|
280
|
+
const filterExpression = convertFilterToOData(field, filterValue);
|
|
281
|
+
if (filterExpression) {
|
|
282
|
+
filters.push(filterExpression);
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
if (filters.length === 0) {
|
|
286
|
+
return undefined;
|
|
287
|
+
}
|
|
288
|
+
// Multiple filters are combined with AND
|
|
289
|
+
return filters.length === 1 ? filters[0] : filters.join(' and ');
|
|
290
|
+
}
|
|
291
|
+
/**
|
|
292
|
+
* Builds OData expand options for a single navigation property
|
|
293
|
+
*/
|
|
294
|
+
function buildExpandOptions(config) {
|
|
295
|
+
const options = [];
|
|
296
|
+
if (config.select && config.select.length > 0) {
|
|
297
|
+
options.push(`$select=${config.select.join(',')}`);
|
|
298
|
+
}
|
|
299
|
+
if (config.filter) {
|
|
300
|
+
options.push(`$filter=${config.filter}`);
|
|
301
|
+
}
|
|
302
|
+
if (config.orderBy) {
|
|
303
|
+
options.push(`$orderby=${config.orderBy}`);
|
|
304
|
+
}
|
|
305
|
+
if (config.top !== undefined) {
|
|
306
|
+
options.push(`$top=${config.top}`);
|
|
307
|
+
}
|
|
308
|
+
if (config.expand && config.expand.length > 0) {
|
|
309
|
+
const nestedExpands = config.expand.map(buildSingleExpand).join(',');
|
|
310
|
+
options.push(`$expand=${nestedExpands}`);
|
|
311
|
+
}
|
|
312
|
+
return options.join(';');
|
|
313
|
+
}
|
|
314
|
+
/**
|
|
315
|
+
* Builds a single expand expression
|
|
316
|
+
*/
|
|
317
|
+
function buildSingleExpand(config) {
|
|
318
|
+
const options = buildExpandOptions(config);
|
|
319
|
+
if (options) {
|
|
320
|
+
return `${config.navigationProperty}(${options})`;
|
|
321
|
+
}
|
|
322
|
+
return config.navigationProperty;
|
|
323
|
+
}
|
|
324
|
+
/**
|
|
325
|
+
* Builds the OData $expand query parameter for joining related tables
|
|
326
|
+
*
|
|
327
|
+
* Supports:
|
|
328
|
+
* - Single-valued navigation properties (many-to-one lookups)
|
|
329
|
+
* - Collection-valued navigation properties (one-to-many relationships)
|
|
330
|
+
* - Nested expands (up to recommended limit)
|
|
331
|
+
* - Expand with $select, $filter, $orderby, $top
|
|
332
|
+
*
|
|
333
|
+
* Limitations:
|
|
334
|
+
* - Max 15 $expand options recommended per query
|
|
335
|
+
* - $orderby and $top not supported with nested $expand on collections
|
|
336
|
+
* - Nested $expand not supported with N:N relationships
|
|
337
|
+
*
|
|
338
|
+
* @param expands - Array of expand configurations
|
|
339
|
+
* @returns OData $expand string
|
|
340
|
+
*
|
|
341
|
+
* @example Single expand with select
|
|
342
|
+
* ```typescript
|
|
343
|
+
* buildODataExpand([
|
|
344
|
+
* { navigationProperty: 'primarycontactid', select: ['fullname', 'emailaddress1'] }
|
|
345
|
+
* ])
|
|
346
|
+
* // Returns: "primarycontactid($select=fullname,emailaddress1)"
|
|
347
|
+
* ```
|
|
348
|
+
*
|
|
349
|
+
* @example Multiple expands
|
|
350
|
+
* ```typescript
|
|
351
|
+
* buildODataExpand([
|
|
352
|
+
* { navigationProperty: 'primarycontactid', select: ['fullname'] },
|
|
353
|
+
* { navigationProperty: 'createdby', select: ['fullname'] }
|
|
354
|
+
* ])
|
|
355
|
+
* // Returns: "primarycontactid($select=fullname),createdby($select=fullname)"
|
|
356
|
+
* ```
|
|
357
|
+
*
|
|
358
|
+
* @example Collection with filter and order
|
|
359
|
+
* ```typescript
|
|
360
|
+
* buildODataExpand([
|
|
361
|
+
* {
|
|
362
|
+
* navigationProperty: 'Account_Tasks',
|
|
363
|
+
* select: ['subject', 'createdon'],
|
|
364
|
+
* filter: "contains(subject,'Task')",
|
|
365
|
+
* orderBy: 'createdon desc',
|
|
366
|
+
* top: 10
|
|
367
|
+
* }
|
|
368
|
+
* ])
|
|
369
|
+
* // Returns: "Account_Tasks($select=subject,createdon;$filter=contains(subject,'Task');$orderby=createdon desc;$top=10)"
|
|
370
|
+
* ```
|
|
371
|
+
*
|
|
372
|
+
* @example Nested expand
|
|
373
|
+
* ```typescript
|
|
374
|
+
* buildODataExpand([
|
|
375
|
+
* {
|
|
376
|
+
* navigationProperty: 'primarycontactid',
|
|
377
|
+
* select: ['fullname'],
|
|
378
|
+
* expand: [
|
|
379
|
+
* { navigationProperty: 'createdby', select: ['fullname'] }
|
|
380
|
+
* ]
|
|
381
|
+
* }
|
|
382
|
+
* ])
|
|
383
|
+
* // Returns: "primarycontactid($select=fullname;$expand=createdby($select=fullname))"
|
|
384
|
+
* ```
|
|
385
|
+
*/
|
|
386
|
+
export function buildODataExpand(expands) {
|
|
387
|
+
if (!expands || expands.length === 0) {
|
|
388
|
+
return undefined;
|
|
389
|
+
}
|
|
390
|
+
// Warning for too many expands (affects performance)
|
|
391
|
+
if (expands.length > 15) {
|
|
392
|
+
console.warn(`Query contains ${expands.length} $expand options. ` + 'Consider limiting to 15 or fewer for better performance.');
|
|
393
|
+
}
|
|
394
|
+
return expands.map(buildSingleExpand).join(',');
|
|
395
|
+
}
|
|
396
|
+
/**
|
|
397
|
+
* Simplified expand builder for basic use cases
|
|
398
|
+
* Use this when you just need to expand navigation properties with optional column selection
|
|
399
|
+
*
|
|
400
|
+
* @param navigationProperties - Array of navigation property names or objects with select
|
|
401
|
+
* @returns OData $expand string
|
|
402
|
+
*
|
|
403
|
+
* @example
|
|
404
|
+
* ```typescript
|
|
405
|
+
* buildSimpleExpand(['primarycontactid', 'createdby'])
|
|
406
|
+
* // Returns: "primarycontactid,createdby"
|
|
407
|
+
*
|
|
408
|
+
* buildSimpleExpand([
|
|
409
|
+
* 'primarycontactid',
|
|
410
|
+
* { navigationProperty: 'createdby', select: ['fullname'] }
|
|
411
|
+
* ])
|
|
412
|
+
* // Returns: "primarycontactid,createdby($select=fullname)"
|
|
413
|
+
* ```
|
|
414
|
+
*/
|
|
415
|
+
export function buildSimpleExpand(navigationProperties) {
|
|
416
|
+
if (!navigationProperties || navigationProperties.length === 0) {
|
|
417
|
+
return undefined;
|
|
418
|
+
}
|
|
419
|
+
const configs = navigationProperties.map((prop) => {
|
|
420
|
+
if (typeof prop === 'string') {
|
|
421
|
+
return { navigationProperty: prop };
|
|
422
|
+
}
|
|
423
|
+
return prop;
|
|
424
|
+
});
|
|
425
|
+
return buildODataExpand(configs);
|
|
426
|
+
}
|
|
427
|
+
// ============================================================================
|
|
428
|
+
// Main Query Builder
|
|
429
|
+
// ============================================================================
|
|
430
|
+
/**
|
|
431
|
+
* Creates a Power Apps query handler for server-side data fetching
|
|
432
|
+
*
|
|
433
|
+
* This is the main API for querying Power Apps Web API. It accepts AG Grid requests
|
|
434
|
+
* and returns data in AG Grid response format, handling all OData translation automatically.
|
|
435
|
+
*
|
|
436
|
+
* @param config - Power Apps query configuration
|
|
437
|
+
* @returns Function that processes AG Grid requests and returns responses
|
|
438
|
+
*
|
|
439
|
+
* @example Basic usage
|
|
440
|
+
* ```typescript
|
|
441
|
+
* const accountsQuery = createPowerAppsQuery({
|
|
442
|
+
* baseUrl: 'https://org.crm.dynamics.com/api/data/v9.2',
|
|
443
|
+
* entitySet: 'accounts',
|
|
444
|
+
* headers: {
|
|
445
|
+
* 'Authorization': 'Bearer YOUR_TOKEN',
|
|
446
|
+
* 'Prefer': 'odata.include-annotations="*"'
|
|
447
|
+
* },
|
|
448
|
+
* defaultSelect: ['accountid', 'name', 'revenue']
|
|
449
|
+
* })
|
|
450
|
+
*
|
|
451
|
+
* const response = await accountsQuery(agGridRequest)
|
|
452
|
+
* // Returns: { rows: [...], lastRow: 1000 }
|
|
453
|
+
* ```
|
|
454
|
+
*
|
|
455
|
+
* @example With related data
|
|
456
|
+
* ```typescript
|
|
457
|
+
* const contactsQuery = createPowerAppsQuery({
|
|
458
|
+
* baseUrl: 'https://org.crm.dynamics.com/api/data/v9.2',
|
|
459
|
+
* entitySet: 'contacts',
|
|
460
|
+
* expand: [
|
|
461
|
+
* { navigationProperty: 'parentcustomerid_account', select: ['name'] }
|
|
462
|
+
* ]
|
|
463
|
+
* })
|
|
464
|
+
* ```
|
|
465
|
+
*
|
|
466
|
+
* @example With transformation
|
|
467
|
+
* ```typescript
|
|
468
|
+
* const query = createPowerAppsQuery({
|
|
469
|
+
* baseUrl: 'https://org.crm.dynamics.com/api/data/v9.2',
|
|
470
|
+
* entitySet: 'accounts',
|
|
471
|
+
* transformResponse: (data) => data.map(item => ({
|
|
472
|
+
* id: item.accountid,
|
|
473
|
+
* name: item.name
|
|
474
|
+
* }))
|
|
475
|
+
* })
|
|
476
|
+
* ```
|
|
477
|
+
*/
|
|
478
|
+
export function createPowerAppsQuery(config) {
|
|
479
|
+
return async (request) => {
|
|
480
|
+
const { startRow = 0, endRow = 100, filterModel, sortModel } = request;
|
|
481
|
+
// Build query parameters
|
|
482
|
+
const params = {
|
|
483
|
+
$count: true, // Always request count for total rows
|
|
484
|
+
};
|
|
485
|
+
// Add select
|
|
486
|
+
if (config.defaultSelect) {
|
|
487
|
+
params.$select = buildODataSelect(config.defaultSelect);
|
|
488
|
+
}
|
|
489
|
+
// Add filter
|
|
490
|
+
if (filterModel && Object.keys(filterModel).length > 0) {
|
|
491
|
+
params.$filter = buildODataFilter(filterModel);
|
|
492
|
+
}
|
|
493
|
+
// Add orderby
|
|
494
|
+
if (sortModel && sortModel.length > 0) {
|
|
495
|
+
params.$orderby = buildODataOrderBy(sortModel, config.defaultOrderBy);
|
|
496
|
+
}
|
|
497
|
+
else if (config.defaultOrderBy) {
|
|
498
|
+
params.$orderby = config.defaultOrderBy;
|
|
499
|
+
}
|
|
500
|
+
// Add pagination
|
|
501
|
+
const pageSize = endRow - startRow;
|
|
502
|
+
params.$top = pageSize;
|
|
503
|
+
// Add expand
|
|
504
|
+
if (config.expand) {
|
|
505
|
+
const expandConfigs = config.expand.map((exp) => typeof exp === 'string' ? { navigationProperty: exp } : exp);
|
|
506
|
+
params.$expand = buildODataExpand(expandConfigs);
|
|
507
|
+
}
|
|
508
|
+
// Build URL
|
|
509
|
+
const url = buildQueryUrl(config.baseUrl, config.entitySet, params);
|
|
510
|
+
// Check URL length
|
|
511
|
+
if (url.length > (config.maxUrlLength || 32768)) {
|
|
512
|
+
console.warn(`Query URL length (${url.length} chars) exceeds recommended limit. ` +
|
|
513
|
+
'Consider using $batch operations for complex queries. ' +
|
|
514
|
+
'See: https://learn.microsoft.com/en-us/power-apps/developer/data-platform/webapi/execute-batch-operations-using-web-api');
|
|
515
|
+
}
|
|
516
|
+
// Build headers
|
|
517
|
+
const headers = {
|
|
518
|
+
Accept: 'application/json',
|
|
519
|
+
'OData-MaxVersion': '4.0',
|
|
520
|
+
'OData-Version': '4.0',
|
|
521
|
+
Prefer: `odata.maxpagesize=${pageSize}`,
|
|
522
|
+
'If-None-Match': 'null', // Override browser caching for fresh data
|
|
523
|
+
...config.headers,
|
|
524
|
+
};
|
|
525
|
+
try {
|
|
526
|
+
// Execute request
|
|
527
|
+
const response = await fetch(url, {
|
|
528
|
+
method: 'GET',
|
|
529
|
+
headers,
|
|
530
|
+
});
|
|
531
|
+
if (!response.ok) {
|
|
532
|
+
throw await parseErrorResponse(response);
|
|
533
|
+
}
|
|
534
|
+
const data = await response.json();
|
|
535
|
+
// Extract rows
|
|
536
|
+
let rows = data.value || [];
|
|
537
|
+
// Apply transformation if provided
|
|
538
|
+
if (config.transformResponse) {
|
|
539
|
+
rows = config.transformResponse(rows);
|
|
540
|
+
}
|
|
541
|
+
// Calculate total count
|
|
542
|
+
const totalCount = data['@odata.count'];
|
|
543
|
+
const lastRow = totalCount !== undefined ? totalCount : undefined;
|
|
544
|
+
return {
|
|
545
|
+
rows,
|
|
546
|
+
lastRow,
|
|
547
|
+
};
|
|
548
|
+
}
|
|
549
|
+
catch (error) {
|
|
550
|
+
console.error('Power Apps query failed:', error);
|
|
551
|
+
throw error;
|
|
552
|
+
}
|
|
553
|
+
};
|
|
554
|
+
}
|
|
555
|
+
/**
|
|
556
|
+
* Builds the complete query URL with parameters
|
|
557
|
+
*/
|
|
558
|
+
function buildQueryUrl(baseUrl, entitySet, params) {
|
|
559
|
+
// Remove trailing slash from baseUrl
|
|
560
|
+
const base = baseUrl.endsWith('/') ? baseUrl.slice(0, -1) : baseUrl;
|
|
561
|
+
// Build query string
|
|
562
|
+
const queryParts = [];
|
|
563
|
+
if (params.$select) {
|
|
564
|
+
queryParts.push(`$select=${encodeURIComponent(params.$select)}`);
|
|
565
|
+
}
|
|
566
|
+
if (params.$filter) {
|
|
567
|
+
queryParts.push(`$filter=${encodeURIComponent(params.$filter)}`);
|
|
568
|
+
}
|
|
569
|
+
if (params.$orderby) {
|
|
570
|
+
queryParts.push(`$orderby=${encodeURIComponent(params.$orderby)}`);
|
|
571
|
+
}
|
|
572
|
+
if (params.$expand) {
|
|
573
|
+
queryParts.push(`$expand=${encodeURIComponent(params.$expand)}`);
|
|
574
|
+
}
|
|
575
|
+
if (params.$top !== undefined) {
|
|
576
|
+
queryParts.push(`$top=${params.$top}`);
|
|
577
|
+
}
|
|
578
|
+
if (params.$skip !== undefined) {
|
|
579
|
+
queryParts.push(`$skip=${params.$skip}`);
|
|
580
|
+
}
|
|
581
|
+
if (params.$count) {
|
|
582
|
+
queryParts.push('$count=true');
|
|
583
|
+
}
|
|
584
|
+
const queryString = queryParts.join('&');
|
|
585
|
+
return `${base}/${entitySet}${queryString ? '?' + queryString : ''}`;
|
|
586
|
+
}
|
|
587
|
+
/**
|
|
588
|
+
* Parses error response from Power Apps API
|
|
589
|
+
*/
|
|
590
|
+
async function parseErrorResponse(response) {
|
|
591
|
+
try {
|
|
592
|
+
const errorData = await response.json();
|
|
593
|
+
const message = errorData.error?.message || 'Unknown error';
|
|
594
|
+
const code = errorData.error?.code || 'UNKNOWN';
|
|
595
|
+
return new Error(`Power Apps API Error [${code}]: ${message}`);
|
|
596
|
+
}
|
|
597
|
+
catch {
|
|
598
|
+
return new Error(`Power Apps API Error: ${response.status} ${response.statusText}`);
|
|
599
|
+
}
|
|
600
|
+
}
|
|
601
|
+
/**
|
|
602
|
+
* Fetches the next page using the @odata.nextLink URL
|
|
603
|
+
*
|
|
604
|
+
* Use this to implement manual pagination when you need to fetch additional pages
|
|
605
|
+
* beyond what AG Grid requests automatically.
|
|
606
|
+
*
|
|
607
|
+
* @param nextLink - The @odata.nextLink URL from a previous response
|
|
608
|
+
* @param headers - Headers to include in the request (auth, etc.)
|
|
609
|
+
* @returns The next page of data
|
|
610
|
+
*
|
|
611
|
+
* @example
|
|
612
|
+
* ```typescript
|
|
613
|
+
* const firstPage = await accountsQuery(agGridRequest)
|
|
614
|
+
*
|
|
615
|
+
* // If there's more data, fetch the next page
|
|
616
|
+
* if (firstPage.nextLink) {
|
|
617
|
+
* const secondPage = await fetchNextPage(firstPage.nextLink, {
|
|
618
|
+
* 'Authorization': 'Bearer YOUR_TOKEN'
|
|
619
|
+
* })
|
|
620
|
+
* }
|
|
621
|
+
* ```
|
|
622
|
+
*/
|
|
623
|
+
export async function fetchNextPage(nextLink, headers) {
|
|
624
|
+
const defaultHeaders = {
|
|
625
|
+
Accept: 'application/json',
|
|
626
|
+
'OData-MaxVersion': '4.0',
|
|
627
|
+
'OData-Version': '4.0',
|
|
628
|
+
...headers,
|
|
629
|
+
};
|
|
630
|
+
const response = await fetch(nextLink, {
|
|
631
|
+
method: 'GET',
|
|
632
|
+
headers: defaultHeaders,
|
|
633
|
+
});
|
|
634
|
+
if (!response.ok) {
|
|
635
|
+
throw await parseErrorResponse(response);
|
|
636
|
+
}
|
|
637
|
+
return await response.json();
|
|
638
|
+
}
|
|
639
|
+
/**
|
|
640
|
+
* Enhanced query function that returns additional metadata
|
|
641
|
+
*
|
|
642
|
+
* Use this when you need access to pagination links and other OData metadata
|
|
643
|
+
*
|
|
644
|
+
* @param config - Power Apps query configuration
|
|
645
|
+
* @returns Function that processes requests and returns enhanced responses
|
|
646
|
+
*
|
|
647
|
+
* @example
|
|
648
|
+
* ```typescript
|
|
649
|
+
* const query = createPowerAppsQueryWithMetadata({
|
|
650
|
+
* baseUrl: 'https://org.crm.dynamics.com/api/data/v9.2',
|
|
651
|
+
* entitySet: 'accounts'
|
|
652
|
+
* })
|
|
653
|
+
*
|
|
654
|
+
* const result = await query(agGridRequest)
|
|
655
|
+
* console.log(`Got ${result.rows.length} rows, total: ${result.totalCount}`)
|
|
656
|
+
*
|
|
657
|
+
* if (result.nextLink) {
|
|
658
|
+
* console.log('More data available at:', result.nextLink)
|
|
659
|
+
* }
|
|
660
|
+
* ```
|
|
661
|
+
*/
|
|
662
|
+
export function createPowerAppsQueryWithMetadata(config) {
|
|
663
|
+
return async (request) => {
|
|
664
|
+
const { startRow = 0, endRow = 100, filterModel, sortModel } = request;
|
|
665
|
+
// Build query parameters
|
|
666
|
+
const params = {
|
|
667
|
+
$count: true,
|
|
668
|
+
};
|
|
669
|
+
if (config.defaultSelect) {
|
|
670
|
+
params.$select = buildODataSelect(config.defaultSelect);
|
|
671
|
+
}
|
|
672
|
+
if (filterModel && Object.keys(filterModel).length > 0) {
|
|
673
|
+
params.$filter = buildODataFilter(filterModel);
|
|
674
|
+
}
|
|
675
|
+
if (sortModel && sortModel.length > 0) {
|
|
676
|
+
params.$orderby = buildODataOrderBy(sortModel, config.defaultOrderBy);
|
|
677
|
+
}
|
|
678
|
+
else if (config.defaultOrderBy) {
|
|
679
|
+
params.$orderby = config.defaultOrderBy;
|
|
680
|
+
}
|
|
681
|
+
const pageSize = endRow - startRow;
|
|
682
|
+
params.$top = pageSize;
|
|
683
|
+
if (config.expand) {
|
|
684
|
+
const expandConfigs = config.expand.map((exp) => typeof exp === 'string' ? { navigationProperty: exp } : exp);
|
|
685
|
+
params.$expand = buildODataExpand(expandConfigs);
|
|
686
|
+
}
|
|
687
|
+
const url = buildQueryUrl(config.baseUrl, config.entitySet, params);
|
|
688
|
+
if (url.length > (config.maxUrlLength || 32768)) {
|
|
689
|
+
console.warn(`Query URL length (${url.length} chars) exceeds recommended limit. ` +
|
|
690
|
+
'Consider using $batch operations for complex queries.');
|
|
691
|
+
}
|
|
692
|
+
const headers = {
|
|
693
|
+
Accept: 'application/json',
|
|
694
|
+
'OData-MaxVersion': '4.0',
|
|
695
|
+
'OData-Version': '4.0',
|
|
696
|
+
Prefer: `odata.maxpagesize=${pageSize}`,
|
|
697
|
+
'If-None-Match': 'null',
|
|
698
|
+
...config.headers,
|
|
699
|
+
};
|
|
700
|
+
try {
|
|
701
|
+
const response = await fetch(url, {
|
|
702
|
+
method: 'GET',
|
|
703
|
+
headers,
|
|
704
|
+
});
|
|
705
|
+
if (!response.ok) {
|
|
706
|
+
throw await parseErrorResponse(response);
|
|
707
|
+
}
|
|
708
|
+
const data = await response.json();
|
|
709
|
+
let rows = data.value || [];
|
|
710
|
+
if (config.transformResponse) {
|
|
711
|
+
rows = config.transformResponse(rows);
|
|
712
|
+
}
|
|
713
|
+
return {
|
|
714
|
+
rows,
|
|
715
|
+
lastRow: data['@odata.count'],
|
|
716
|
+
nextLink: data['@odata.nextLink'],
|
|
717
|
+
totalCount: data['@odata.count'],
|
|
718
|
+
odataContext: data['@odata.context'],
|
|
719
|
+
};
|
|
720
|
+
}
|
|
721
|
+
catch (error) {
|
|
722
|
+
console.error('Power Apps query failed:', error);
|
|
723
|
+
throw error;
|
|
724
|
+
}
|
|
725
|
+
};
|
|
726
|
+
}
|
|
727
|
+
/**
|
|
728
|
+
* Utility to fetch all pages of data (use with caution for large datasets)
|
|
729
|
+
*
|
|
730
|
+
* This will continue fetching pages until no more data is available.
|
|
731
|
+
* Be careful with large datasets as this can consume significant memory and time.
|
|
732
|
+
*
|
|
733
|
+
* @param queryFn - The query function created by createPowerAppsQueryWithMetadata
|
|
734
|
+
* @param request - Initial AG Grid request
|
|
735
|
+
* @param maxPages - Optional limit on number of pages to fetch (default: unlimited)
|
|
736
|
+
* @returns All rows across all pages
|
|
737
|
+
*
|
|
738
|
+
* @example
|
|
739
|
+
* ```typescript
|
|
740
|
+
* const query = createPowerAppsQueryWithMetadata({
|
|
741
|
+
* baseUrl: 'https://org.crm.dynamics.com/api/data/v9.2',
|
|
742
|
+
* entitySet: 'accounts'
|
|
743
|
+
* })
|
|
744
|
+
*
|
|
745
|
+
* // Fetch up to 10 pages (1000 records if pageSize is 100)
|
|
746
|
+
* const allData = await fetchAllPages(query, agGridRequest, 10)
|
|
747
|
+
* console.log(`Fetched ${allData.length} total records`)
|
|
748
|
+
* ```
|
|
749
|
+
*/
|
|
750
|
+
export async function fetchAllPages(queryFn, request, maxPages) {
|
|
751
|
+
const allRows = [];
|
|
752
|
+
let pageCount = 0;
|
|
753
|
+
const currentResult = await queryFn(request);
|
|
754
|
+
allRows.push(...currentResult.rows);
|
|
755
|
+
pageCount++;
|
|
756
|
+
// Note: Full pagination would require parsing nextLink and creating new requests
|
|
757
|
+
// For now, this serves as a placeholder for the pattern
|
|
758
|
+
if (currentResult.nextLink && (!maxPages || pageCount < maxPages)) {
|
|
759
|
+
console.warn('fetchAllPages requires manual nextLink handling. ' + 'Use fetchNextPage() directly for better control.');
|
|
760
|
+
}
|
|
761
|
+
return allRows;
|
|
762
|
+
}
|
|
763
|
+
// ============================================================================
|
|
764
|
+
// Export Summary
|
|
765
|
+
// ============================================================================
|
|
766
|
+
/**
|
|
767
|
+
* Main exports:
|
|
768
|
+
*
|
|
769
|
+
* Query Builders:
|
|
770
|
+
* - createPowerAppsQuery() - Main query function (returns AG Grid format)
|
|
771
|
+
* - createPowerAppsQueryWithMetadata() - Enhanced query with pagination metadata
|
|
772
|
+
*
|
|
773
|
+
* Helper Functions:
|
|
774
|
+
* - buildODataSelect() - Build $select clause
|
|
775
|
+
* - buildODataOrderBy() - Build $orderby clause
|
|
776
|
+
* - buildODataFilter() - Build $filter clause (from AG Grid filters)
|
|
777
|
+
* - buildODataExpand() - Build $expand clause
|
|
778
|
+
* - buildSimpleExpand() - Simplified expand builder
|
|
779
|
+
* - fetchNextPage() - Fetch next page using @odata.nextLink
|
|
780
|
+
* - fetchAllPages() - Fetch all pages (use with caution)
|
|
781
|
+
*
|
|
782
|
+
* Types:
|
|
783
|
+
* - PowerAppsQueryConfig - Configuration for query builder
|
|
784
|
+
* - PowerAppsODataResponse - OData response format
|
|
785
|
+
* - PowerAppsError - Error response format
|
|
786
|
+
* - PowerAppsQueryResult - Enhanced response with metadata
|
|
787
|
+
* - ExpandConfig - Configuration for expanding related entities
|
|
788
|
+
* - ODataQueryParams - OData query parameters
|
|
789
|
+
*/
|