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
package/dist/ag-grid.js
DELETED
|
@@ -1,901 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* AG Grid Server-Side Row Model (SSRM) - Server-Side Implementation API
|
|
3
|
-
*
|
|
4
|
-
* This module provides a clean, strongly-typed interface for implementing
|
|
5
|
-
* AG Grid's Server-Side Row Model on the backend with any data source.
|
|
6
|
-
*
|
|
7
|
-
* @example Server-Side Usage (SvelteKit Remote Function)
|
|
8
|
-
* ```typescript
|
|
9
|
-
* import { createAGGridQuery, agGridRequestSchema } from './ag-grid'
|
|
10
|
-
* import { query } from '$app/server'
|
|
11
|
-
*
|
|
12
|
-
* export const getUsersPaginated = query(agGridRequestSchema, async (request) => {
|
|
13
|
-
* return await createAGGridQuery({
|
|
14
|
-
* async fetch(params) {
|
|
15
|
-
* // Your data fetching logic
|
|
16
|
-
* const users = await DB.user.findMany({
|
|
17
|
-
* where: params.where,
|
|
18
|
-
* orderBy: params.orderBy,
|
|
19
|
-
* skip: params.skip,
|
|
20
|
-
* take: params.take,
|
|
21
|
-
* })
|
|
22
|
-
* return users
|
|
23
|
-
* },
|
|
24
|
-
* async count(params) {
|
|
25
|
-
* return await DB.user.count({ where: params.where })
|
|
26
|
-
* }
|
|
27
|
-
* })(request)
|
|
28
|
-
* })
|
|
29
|
-
* ```
|
|
30
|
-
*/
|
|
31
|
-
import { z } from 'zod';
|
|
32
|
-
// ============================================================================
|
|
33
|
-
// Zod Schema for Remote Functions
|
|
34
|
-
// ============================================================================
|
|
35
|
-
/**
|
|
36
|
-
* Zod schema for AG Grid column configuration
|
|
37
|
-
*/
|
|
38
|
-
export const agGridColumnSchema = z.object({
|
|
39
|
-
id: z.string(),
|
|
40
|
-
displayName: z.string(),
|
|
41
|
-
field: z.string().optional(),
|
|
42
|
-
aggFunc: z.string().optional(),
|
|
43
|
-
});
|
|
44
|
-
/**
|
|
45
|
-
* Zod schema for AG Grid sort configuration
|
|
46
|
-
*/
|
|
47
|
-
export const agGridSortSchema = z.object({
|
|
48
|
-
colId: z.string(),
|
|
49
|
-
sort: z.enum(['asc', 'desc']),
|
|
50
|
-
});
|
|
51
|
-
/**
|
|
52
|
-
* Zod schema for AG Grid request
|
|
53
|
-
* Use this in your remote function schema: query(agGridRequestSchema, async (request) => ...)
|
|
54
|
-
*/
|
|
55
|
-
export const agGridRequestSchema = z.object({
|
|
56
|
-
startRow: z.number().optional(),
|
|
57
|
-
endRow: z.number().optional(),
|
|
58
|
-
filterModel: z.record(z.unknown()).optional(),
|
|
59
|
-
sortModel: z.array(agGridSortSchema),
|
|
60
|
-
rowGroupCols: z.array(agGridColumnSchema),
|
|
61
|
-
groupKeys: z.array(z.string()),
|
|
62
|
-
valueCols: z.array(agGridColumnSchema),
|
|
63
|
-
pivotCols: z.array(agGridColumnSchema),
|
|
64
|
-
pivotMode: z.boolean(),
|
|
65
|
-
});
|
|
66
|
-
// ============================================================================
|
|
67
|
-
// Main Server-Side API
|
|
68
|
-
// ============================================================================
|
|
69
|
-
/**
|
|
70
|
-
* Creates an AG Grid query handler for server-side data fetching
|
|
71
|
-
*
|
|
72
|
-
* This is the main server-side API. Use this in your remote functions or API endpoints
|
|
73
|
-
* to handle AG Grid requests with any database/data source.
|
|
74
|
-
*
|
|
75
|
-
* @param config - Configuration for data fetching and field mappings
|
|
76
|
-
* @returns Function that processes AG Grid requests and returns responses
|
|
77
|
-
*
|
|
78
|
-
* @example Basic Prisma Usage
|
|
79
|
-
* ```typescript
|
|
80
|
-
* export const getUsersPaginated = query(agGridRequestSchema, async (request) => {
|
|
81
|
-
* return await createAGGridQuery({
|
|
82
|
-
* async fetch(params) {
|
|
83
|
-
* return await DB.user.findMany({
|
|
84
|
-
* where: params.where,
|
|
85
|
-
* orderBy: params.orderBy,
|
|
86
|
-
* skip: params.skip,
|
|
87
|
-
* take: params.take,
|
|
88
|
-
* })
|
|
89
|
-
* },
|
|
90
|
-
* async count(params) {
|
|
91
|
-
* return await DB.user.count({ where: params.where })
|
|
92
|
-
* },
|
|
93
|
-
* defaultSort: { createdAt: 'desc' }
|
|
94
|
-
* })(request)
|
|
95
|
-
* })
|
|
96
|
-
* ```
|
|
97
|
-
*
|
|
98
|
-
* @example With Nested Relations (Use Dot Notation)
|
|
99
|
-
* ```typescript
|
|
100
|
-
* // Server-side - just include the relation
|
|
101
|
-
* return await createAGGridQuery({
|
|
102
|
-
* async fetch(params) {
|
|
103
|
-
* return await DB.intervention.findMany({
|
|
104
|
-
* where: params.where,
|
|
105
|
-
* orderBy: params.orderBy,
|
|
106
|
-
* skip: params.skip,
|
|
107
|
-
* take: params.take,
|
|
108
|
-
* include: { location: true }
|
|
109
|
-
* })
|
|
110
|
-
* },
|
|
111
|
-
* async count(params) {
|
|
112
|
-
* return await DB.intervention.count({ where: params.where })
|
|
113
|
-
* }
|
|
114
|
-
* })(request)
|
|
115
|
-
*
|
|
116
|
-
* // Client-side - use dot notation in column definitions
|
|
117
|
-
* const columnDefs = [
|
|
118
|
-
* { field: 'id' },
|
|
119
|
-
* { field: 'location.name', headerName: 'Location', filter: 'agTextColumnFilter' },
|
|
120
|
-
* ]
|
|
121
|
-
* // That's it! Auto-handles display, filtering, sorting, and grouping
|
|
122
|
-
* ```
|
|
123
|
-
*
|
|
124
|
-
* @example With Computed Fields (Complex Calculations)
|
|
125
|
-
* ```typescript
|
|
126
|
-
* return await createAGGridQuery({
|
|
127
|
-
* async fetch(params) {
|
|
128
|
-
* return await DB.intervention.findMany({
|
|
129
|
-
* where: params.where,
|
|
130
|
-
* orderBy: params.orderBy,
|
|
131
|
-
* skip: params.skip,
|
|
132
|
-
* take: params.take,
|
|
133
|
-
* })
|
|
134
|
-
* },
|
|
135
|
-
* async count(params) {
|
|
136
|
-
* return await DB.intervention.count({ where: params.where })
|
|
137
|
-
* },
|
|
138
|
-
* computedFields: [
|
|
139
|
-
* {
|
|
140
|
-
* // Complex computed field - provide custom handlers
|
|
141
|
-
* columnId: 'weekEnding',
|
|
142
|
-
* valueGetter: (record) => calculateWeekEnding(record.date),
|
|
143
|
-
* filterHandler: (filterValue, where) => {
|
|
144
|
-
* // Custom filter logic for week ending
|
|
145
|
-
* }
|
|
146
|
-
* }
|
|
147
|
-
* ]
|
|
148
|
-
* })(request)
|
|
149
|
-
* ```
|
|
150
|
-
*/
|
|
151
|
-
export function createAGGridQuery(config) {
|
|
152
|
-
return async (request) => {
|
|
153
|
-
const { startRow = 0, endRow = 100, filterModel, sortModel, rowGroupCols = [], groupKeys = [] } = request;
|
|
154
|
-
// Determine if we're handling groups or leaf data
|
|
155
|
-
const isGroupRequest = rowGroupCols.length > 0 && groupKeys.length < rowGroupCols.length;
|
|
156
|
-
const groupLevel = groupKeys.length;
|
|
157
|
-
const groupColumn = isGroupRequest ? rowGroupCols[groupLevel] : undefined;
|
|
158
|
-
// Build WHERE clause
|
|
159
|
-
let where = {};
|
|
160
|
-
// Apply group keys (drill-down filters)
|
|
161
|
-
if (groupKeys.length > 0) {
|
|
162
|
-
for (let i = 0; i < groupKeys.length; i++) {
|
|
163
|
-
const col = rowGroupCols[i];
|
|
164
|
-
const key = groupKeys[i];
|
|
165
|
-
// Get the field name for type checking
|
|
166
|
-
const fieldName = col.field || col.id;
|
|
167
|
-
// Normalize date strings to ISO-8601 format for Prisma
|
|
168
|
-
const normalizedKey = normalizeValue(key, fieldName);
|
|
169
|
-
const computedField = config.computedFields?.find((cf) => cf.columnId === col.id);
|
|
170
|
-
if (computedField?.groupHandler) {
|
|
171
|
-
// Use custom group handler (pass normalized key)
|
|
172
|
-
computedField.groupHandler(normalizedKey, where);
|
|
173
|
-
}
|
|
174
|
-
else if (computedField?.dbField) {
|
|
175
|
-
// Auto-handle nested fields (e.g., 'location.name')
|
|
176
|
-
if (computedField.dbField.includes('.')) {
|
|
177
|
-
applyNestedFilter(where, computedField.dbField, normalizedKey);
|
|
178
|
-
}
|
|
179
|
-
else {
|
|
180
|
-
;
|
|
181
|
-
where[computedField.dbField] = normalizedKey;
|
|
182
|
-
}
|
|
183
|
-
}
|
|
184
|
-
else if (col.id.includes('.')) {
|
|
185
|
-
// Auto-handle nested fields from column id (e.g., 'location.name')
|
|
186
|
-
applyNestedFilter(where, col.id, normalizedKey);
|
|
187
|
-
}
|
|
188
|
-
else {
|
|
189
|
-
;
|
|
190
|
-
where[col.id] = normalizedKey;
|
|
191
|
-
}
|
|
192
|
-
}
|
|
193
|
-
}
|
|
194
|
-
// Apply filters
|
|
195
|
-
if (filterModel) {
|
|
196
|
-
for (const [columnId, filterValue] of Object.entries(filterModel)) {
|
|
197
|
-
if (!filterValue)
|
|
198
|
-
continue;
|
|
199
|
-
const computedField = config.computedFields?.find((cf) => cf.columnId === columnId);
|
|
200
|
-
if (computedField?.filterHandler) {
|
|
201
|
-
// Use custom filter handler
|
|
202
|
-
computedField.filterHandler(filterValue, where);
|
|
203
|
-
}
|
|
204
|
-
else if (computedField?.dbField) {
|
|
205
|
-
// Auto-handle using dbField
|
|
206
|
-
applyFilterToField(where, computedField.dbField, filterValue);
|
|
207
|
-
}
|
|
208
|
-
else {
|
|
209
|
-
applyFilterToField(where, columnId, filterValue);
|
|
210
|
-
}
|
|
211
|
-
}
|
|
212
|
-
}
|
|
213
|
-
// Apply custom where transformations
|
|
214
|
-
if (config.transformWhere) {
|
|
215
|
-
where = config.transformWhere(where, request);
|
|
216
|
-
}
|
|
217
|
-
// Build ORDER BY clause
|
|
218
|
-
const orderBy = [];
|
|
219
|
-
if (sortModel && sortModel.length > 0) {
|
|
220
|
-
for (const sort of sortModel) {
|
|
221
|
-
const computedField = config.computedFields?.find((cf) => cf.columnId === sort.colId);
|
|
222
|
-
if (computedField) {
|
|
223
|
-
// If computed field has a dbField, use it for sorting
|
|
224
|
-
if (computedField.dbField) {
|
|
225
|
-
orderBy.push(createNestedSort(computedField.dbField, sort.sort));
|
|
226
|
-
}
|
|
227
|
-
// If no dbField, skip adding to database query (pure computation)
|
|
228
|
-
// Sorting will need to be handled client-side or in-memory
|
|
229
|
-
}
|
|
230
|
-
else {
|
|
231
|
-
// Not a computed field, check if it's a nested field path
|
|
232
|
-
if (sort.colId.includes('.')) {
|
|
233
|
-
orderBy.push(createNestedSort(sort.colId, sort.sort));
|
|
234
|
-
}
|
|
235
|
-
else {
|
|
236
|
-
// Simple field, use directly
|
|
237
|
-
orderBy.push({ [sort.colId]: sort.sort });
|
|
238
|
-
}
|
|
239
|
-
}
|
|
240
|
-
}
|
|
241
|
-
}
|
|
242
|
-
else if (config.defaultSort) {
|
|
243
|
-
for (const [field, direction] of Object.entries(config.defaultSort)) {
|
|
244
|
-
if (field.includes('.')) {
|
|
245
|
-
orderBy.push(createNestedSort(field, direction));
|
|
246
|
-
}
|
|
247
|
-
else {
|
|
248
|
-
orderBy.push({ [field]: direction });
|
|
249
|
-
}
|
|
250
|
-
}
|
|
251
|
-
}
|
|
252
|
-
// Build query params
|
|
253
|
-
const queryParams = {
|
|
254
|
-
where,
|
|
255
|
-
orderBy,
|
|
256
|
-
skip: startRow,
|
|
257
|
-
take: endRow - startRow,
|
|
258
|
-
groupLevel,
|
|
259
|
-
groupKeys,
|
|
260
|
-
groupColumn,
|
|
261
|
-
isGroupRequest,
|
|
262
|
-
};
|
|
263
|
-
if (isGroupRequest) {
|
|
264
|
-
// Fetch all records to compute groups (for now - can be optimized)
|
|
265
|
-
const allRecords = await config.fetch({ ...queryParams, skip: 0, take: 999999 });
|
|
266
|
-
const groupMap = new Map();
|
|
267
|
-
for (const record of allRecords) {
|
|
268
|
-
let groupValue = undefined;
|
|
269
|
-
const computedField = config.computedFields?.find((cf) => cf.columnId === groupColumn.id);
|
|
270
|
-
if (computedField?.valueGetter) {
|
|
271
|
-
// Use custom value getter
|
|
272
|
-
groupValue = computedField.valueGetter(record);
|
|
273
|
-
}
|
|
274
|
-
else if (computedField?.dbField) {
|
|
275
|
-
// Auto-extract nested value (e.g., 'location.name')
|
|
276
|
-
groupValue = getNestedValue(record, computedField.dbField);
|
|
277
|
-
}
|
|
278
|
-
else {
|
|
279
|
-
// Support nested field paths like 'location.name' by traversing the object
|
|
280
|
-
if (groupColumn.id.includes('.')) {
|
|
281
|
-
groupValue = getNestedValue(record, groupColumn.id);
|
|
282
|
-
}
|
|
283
|
-
else {
|
|
284
|
-
groupValue = record[groupColumn.id];
|
|
285
|
-
}
|
|
286
|
-
}
|
|
287
|
-
// Only include non-null, non-undefined values (convert to string for grouping)
|
|
288
|
-
if (groupValue !== null && groupValue !== undefined) {
|
|
289
|
-
const groupKey = String(groupValue);
|
|
290
|
-
if (groupKey.trim()) {
|
|
291
|
-
// Skip empty strings
|
|
292
|
-
groupMap.set(groupKey, (groupMap.get(groupKey) || 0) + 1);
|
|
293
|
-
}
|
|
294
|
-
}
|
|
295
|
-
}
|
|
296
|
-
// Convert to group rows
|
|
297
|
-
const groups = Array.from(groupMap.entries())
|
|
298
|
-
.map(([key, count]) => {
|
|
299
|
-
const groupRow = { childCount: count };
|
|
300
|
-
// For nested fields like 'location.name', create the nested structure
|
|
301
|
-
if (groupColumn.id.includes('.')) {
|
|
302
|
-
const parts = groupColumn.id.split('.');
|
|
303
|
-
let current = groupRow;
|
|
304
|
-
for (let i = 0; i < parts.length - 1; i++) {
|
|
305
|
-
current[parts[i]] = {};
|
|
306
|
-
current = current[parts[i]];
|
|
307
|
-
}
|
|
308
|
-
current[parts[parts.length - 1]] = key;
|
|
309
|
-
}
|
|
310
|
-
else {
|
|
311
|
-
// For flat fields, just set directly
|
|
312
|
-
groupRow[groupColumn.id] = key;
|
|
313
|
-
}
|
|
314
|
-
return groupRow;
|
|
315
|
-
})
|
|
316
|
-
.sort((a, b) => {
|
|
317
|
-
// Respect sortModel if provided
|
|
318
|
-
if (sortModel && sortModel.length > 0) {
|
|
319
|
-
// Use the first sort that matches a group column or any sort
|
|
320
|
-
const sort = sortModel.find((s) => rowGroupCols.some((col) => col.id === s.colId)) || sortModel[0];
|
|
321
|
-
const aVal = getNestedValue(a, sort.colId) ?? getNestedValue(a, groupColumn.id) ?? '';
|
|
322
|
-
const bVal = getNestedValue(b, sort.colId) ?? getNestedValue(b, groupColumn.id) ?? '';
|
|
323
|
-
// Handle numeric comparison
|
|
324
|
-
if (typeof aVal === 'number' && typeof bVal === 'number') {
|
|
325
|
-
return sort.sort === 'asc' ? aVal - bVal : bVal - aVal;
|
|
326
|
-
}
|
|
327
|
-
// String comparison
|
|
328
|
-
const comparison = String(aVal).localeCompare(String(bVal));
|
|
329
|
-
return sort.sort === 'asc' ? comparison : -comparison;
|
|
330
|
-
}
|
|
331
|
-
// Default: sort by group column value (ascending)
|
|
332
|
-
const aVal = getNestedValue(a, groupColumn.id) ?? '';
|
|
333
|
-
const bVal = getNestedValue(b, groupColumn.id) ?? '';
|
|
334
|
-
// Handle numeric comparison
|
|
335
|
-
if (typeof aVal === 'number' && typeof bVal === 'number') {
|
|
336
|
-
return aVal - bVal;
|
|
337
|
-
}
|
|
338
|
-
return String(aVal).localeCompare(String(bVal));
|
|
339
|
-
});
|
|
340
|
-
const paginatedGroups = groups.slice(startRow, endRow);
|
|
341
|
-
return {
|
|
342
|
-
rows: paginatedGroups,
|
|
343
|
-
lastRow: groups.length,
|
|
344
|
-
};
|
|
345
|
-
}
|
|
346
|
-
else {
|
|
347
|
-
// Fetch leaf data
|
|
348
|
-
const [rows, totalCount] = await Promise.all([config.fetch(queryParams), config.count(queryParams)]);
|
|
349
|
-
// Apply computed field value getters
|
|
350
|
-
const mappedRows = rows.map((record) => {
|
|
351
|
-
const mapped = { ...record };
|
|
352
|
-
if (config.computedFields) {
|
|
353
|
-
for (const computedField of config.computedFields) {
|
|
354
|
-
if (computedField.valueGetter) {
|
|
355
|
-
// Use custom value getter
|
|
356
|
-
;
|
|
357
|
-
mapped[computedField.columnId] = computedField.valueGetter(record);
|
|
358
|
-
}
|
|
359
|
-
else if (computedField.dbField) {
|
|
360
|
-
// Auto-extract nested value (e.g., 'location.name' -> record.location.name)
|
|
361
|
-
;
|
|
362
|
-
mapped[computedField.columnId] = getNestedValue(record, computedField.dbField);
|
|
363
|
-
}
|
|
364
|
-
}
|
|
365
|
-
}
|
|
366
|
-
return mapped;
|
|
367
|
-
});
|
|
368
|
-
return {
|
|
369
|
-
rows: mappedRows,
|
|
370
|
-
lastRow: groupKeys.length > 0 ? mappedRows.length : totalCount,
|
|
371
|
-
};
|
|
372
|
-
}
|
|
373
|
-
};
|
|
374
|
-
}
|
|
375
|
-
// ============================================================================
|
|
376
|
-
// Helper Functions for Query Building
|
|
377
|
-
// ============================================================================
|
|
378
|
-
/**
|
|
379
|
-
* Detects if a string is a JavaScript Date string
|
|
380
|
-
* Examples: "Sat Oct 18 2025 18:00:00 GMT-0600", "2025-10-18T00:00:00.000Z"
|
|
381
|
-
*/
|
|
382
|
-
function isDateString(value) {
|
|
383
|
-
if (typeof value !== 'string')
|
|
384
|
-
return false;
|
|
385
|
-
// Don't treat pure numbers as dates (e.g., "51.6" shouldn't become a date)
|
|
386
|
-
if (/^-?\d+\.?\d*$/.test(value.trim()))
|
|
387
|
-
return false;
|
|
388
|
-
// Check if it's a valid date string
|
|
389
|
-
const date = new Date(value);
|
|
390
|
-
if (isNaN(date.getTime()))
|
|
391
|
-
return false;
|
|
392
|
-
// Additional validation: must contain date-like patterns
|
|
393
|
-
// (year, month name, or ISO format)
|
|
394
|
-
return /\d{4}|Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec|GMT|UTC|T\d{2}:/i.test(value);
|
|
395
|
-
}
|
|
396
|
-
/**
|
|
397
|
-
* Converts a JavaScript Date string or Date object to ISO-8601 format
|
|
398
|
-
* This ensures Prisma receives dates in the correct format
|
|
399
|
-
*/
|
|
400
|
-
function toISOString(value) {
|
|
401
|
-
if (value instanceof Date) {
|
|
402
|
-
return value.toISOString();
|
|
403
|
-
}
|
|
404
|
-
if (typeof value === 'string' && isDateString(value)) {
|
|
405
|
-
return new Date(value).toISOString();
|
|
406
|
-
}
|
|
407
|
-
return value;
|
|
408
|
-
}
|
|
409
|
-
/**
|
|
410
|
-
* Normalizes a value for database queries (converts dates to ISO-8601, parses numeric strings)
|
|
411
|
-
*/
|
|
412
|
-
function normalizeValue(value, fieldName) {
|
|
413
|
-
// Handle dates (but not numeric strings that look like dates)
|
|
414
|
-
if (value instanceof Date || (typeof value === 'string' && isDateString(value))) {
|
|
415
|
-
return toISOString(value);
|
|
416
|
-
}
|
|
417
|
-
// Parse numeric strings back to numbers (e.g., "51.6" -> 51.6)
|
|
418
|
-
// BUT: Skip conversion for fields containing "jde" (these are always strings)
|
|
419
|
-
if (typeof value === 'string') {
|
|
420
|
-
const trimmed = value.trim();
|
|
421
|
-
if (/^-?\d+\.?\d*$/.test(trimmed)) {
|
|
422
|
-
// Don't convert if field name contains "jde" (case insensitive)
|
|
423
|
-
if (fieldName && /jde/i.test(fieldName)) {
|
|
424
|
-
return value;
|
|
425
|
-
}
|
|
426
|
-
const num = Number(trimmed);
|
|
427
|
-
if (!isNaN(num)) {
|
|
428
|
-
return num;
|
|
429
|
-
}
|
|
430
|
-
}
|
|
431
|
-
}
|
|
432
|
-
if (Array.isArray(value)) {
|
|
433
|
-
return value.map((v) => normalizeValue(v, fieldName));
|
|
434
|
-
}
|
|
435
|
-
if (value && typeof value === 'object') {
|
|
436
|
-
const normalized = {};
|
|
437
|
-
for (const [key, val] of Object.entries(value)) {
|
|
438
|
-
normalized[key] = normalizeValue(val, key);
|
|
439
|
-
}
|
|
440
|
-
return normalized;
|
|
441
|
-
}
|
|
442
|
-
return value;
|
|
443
|
-
}
|
|
444
|
-
/**
|
|
445
|
-
* Applies a filter to a field (handles nested fields)
|
|
446
|
-
*/
|
|
447
|
-
function applyFilterToField(where, field, filterValue) {
|
|
448
|
-
if (!filterValue || typeof filterValue !== 'object')
|
|
449
|
-
return;
|
|
450
|
-
const filter = filterValue;
|
|
451
|
-
// Number/Date filter with type (e.g., equals, lessThan, greaterThan, inRange)
|
|
452
|
-
if ('filterType' in filter || 'type' in filter) {
|
|
453
|
-
// AG Grid sends both filterType ("number"/"date") and type ("equals"/"lessThan"/etc)
|
|
454
|
-
// We need the comparison type, not the filter type
|
|
455
|
-
const comparisonType = (filter.type || filter.filterType);
|
|
456
|
-
// Handle blank/notBlank/empty filters (no filter value needed)
|
|
457
|
-
if (comparisonType === 'blank' || comparisonType === 'empty') {
|
|
458
|
-
// Filter for null or undefined values
|
|
459
|
-
if (field.includes('.')) {
|
|
460
|
-
applyNestedFilter(where, field, null);
|
|
461
|
-
}
|
|
462
|
-
else {
|
|
463
|
-
;
|
|
464
|
-
where[field] = null;
|
|
465
|
-
}
|
|
466
|
-
return;
|
|
467
|
-
}
|
|
468
|
-
if (comparisonType === 'notBlank') {
|
|
469
|
-
// Filter for non-null values
|
|
470
|
-
const condition = { not: null };
|
|
471
|
-
if (field.includes('.')) {
|
|
472
|
-
applyNestedFilter(where, field, condition);
|
|
473
|
-
}
|
|
474
|
-
else {
|
|
475
|
-
;
|
|
476
|
-
where[field] = condition;
|
|
477
|
-
}
|
|
478
|
-
return;
|
|
479
|
-
}
|
|
480
|
-
// Handle simple comparison operators
|
|
481
|
-
// Date filters may use 'dateFrom' instead of 'filter' for the value
|
|
482
|
-
const filterValue = 'filter' in filter ? filter.filter : 'dateFrom' in filter ? filter.dateFrom : null;
|
|
483
|
-
if (filterValue !== null && filterValue !== undefined) {
|
|
484
|
-
const normalizedValue = normalizeValue(filterValue, field);
|
|
485
|
-
const condition = {};
|
|
486
|
-
switch (comparisonType) {
|
|
487
|
-
case 'equals':
|
|
488
|
-
if (field.includes('.')) {
|
|
489
|
-
applyNestedFilter(where, field, normalizedValue);
|
|
490
|
-
}
|
|
491
|
-
else {
|
|
492
|
-
;
|
|
493
|
-
where[field] = normalizedValue;
|
|
494
|
-
}
|
|
495
|
-
return;
|
|
496
|
-
case 'notEqual':
|
|
497
|
-
condition.not = normalizedValue;
|
|
498
|
-
break;
|
|
499
|
-
case 'lessThan':
|
|
500
|
-
condition.lt = normalizedValue;
|
|
501
|
-
break;
|
|
502
|
-
case 'lessThanOrEqual':
|
|
503
|
-
condition.lte = normalizedValue;
|
|
504
|
-
break;
|
|
505
|
-
case 'greaterThan':
|
|
506
|
-
condition.gt = normalizedValue;
|
|
507
|
-
break;
|
|
508
|
-
case 'greaterThanOrEqual':
|
|
509
|
-
condition.gte = normalizedValue;
|
|
510
|
-
break;
|
|
511
|
-
case 'contains':
|
|
512
|
-
condition.contains = normalizedValue;
|
|
513
|
-
condition.mode = 'insensitive';
|
|
514
|
-
break;
|
|
515
|
-
case 'notContains':
|
|
516
|
-
condition.not = { contains: normalizedValue, mode: 'insensitive' };
|
|
517
|
-
break;
|
|
518
|
-
case 'startsWith':
|
|
519
|
-
condition.startsWith = normalizedValue;
|
|
520
|
-
condition.mode = 'insensitive';
|
|
521
|
-
break;
|
|
522
|
-
case 'endsWith':
|
|
523
|
-
condition.endsWith = normalizedValue;
|
|
524
|
-
condition.mode = 'insensitive';
|
|
525
|
-
break;
|
|
526
|
-
}
|
|
527
|
-
if (Object.keys(condition).length > 0) {
|
|
528
|
-
if (field.includes('.')) {
|
|
529
|
-
applyNestedFilter(where, field, condition);
|
|
530
|
-
}
|
|
531
|
-
else {
|
|
532
|
-
;
|
|
533
|
-
where[field] = condition;
|
|
534
|
-
}
|
|
535
|
-
}
|
|
536
|
-
return;
|
|
537
|
-
}
|
|
538
|
-
// Handle inRange (for number and date filters)
|
|
539
|
-
if (comparisonType === 'inRange' && 'filter' in filter && 'filterTo' in filter) {
|
|
540
|
-
const normalizedFrom = normalizeValue(filter.filter, field);
|
|
541
|
-
const normalizedTo = normalizeValue(filter.filterTo, field);
|
|
542
|
-
const rangeCondition = {};
|
|
543
|
-
if (normalizedFrom !== null && normalizedFrom !== undefined) {
|
|
544
|
-
rangeCondition.gte = normalizedFrom;
|
|
545
|
-
}
|
|
546
|
-
if (normalizedTo !== null && normalizedTo !== undefined) {
|
|
547
|
-
rangeCondition.lte = normalizedTo;
|
|
548
|
-
}
|
|
549
|
-
if (Object.keys(rangeCondition).length > 0) {
|
|
550
|
-
if (field.includes('.')) {
|
|
551
|
-
applyNestedFilter(where, field, rangeCondition);
|
|
552
|
-
}
|
|
553
|
-
else {
|
|
554
|
-
;
|
|
555
|
-
where[field] = rangeCondition;
|
|
556
|
-
}
|
|
557
|
-
}
|
|
558
|
-
return;
|
|
559
|
-
}
|
|
560
|
-
}
|
|
561
|
-
// Text filter (legacy/fallback for simple string filters)
|
|
562
|
-
if ('filter' in filter && typeof filter.filter === 'string' && !('filterType' in filter) && !('type' in filter)) {
|
|
563
|
-
const normalizedFilter = normalizeValue(filter.filter);
|
|
564
|
-
if (field.includes('.')) {
|
|
565
|
-
applyNestedFilter(where, field, { contains: normalizedFilter, mode: 'insensitive' });
|
|
566
|
-
}
|
|
567
|
-
else {
|
|
568
|
-
;
|
|
569
|
-
where[field] = { contains: normalizedFilter, mode: 'insensitive' };
|
|
570
|
-
}
|
|
571
|
-
}
|
|
572
|
-
// Set filter
|
|
573
|
-
if ('values' in filter && Array.isArray(filter.values)) {
|
|
574
|
-
const normalizedValues = normalizeValue(filter.values);
|
|
575
|
-
// Check if this is a boolean filter (values are "Yes"/"No", "true"/"false", or boolean primitives)
|
|
576
|
-
const isBooleanFilter = normalizedValues.every((v) => v === 'Yes' || v === 'No' || v === 'true' || v === 'false' || v === true || v === false);
|
|
577
|
-
if (isBooleanFilter) {
|
|
578
|
-
// Convert "Yes"/"No"/"true"/"false" to boolean for Prisma
|
|
579
|
-
const booleanValues = normalizedValues.map((v) => {
|
|
580
|
-
if (v === 'Yes' || v === 'true' || v === true)
|
|
581
|
-
return true;
|
|
582
|
-
if (v === 'No' || v === 'false' || v === false)
|
|
583
|
-
return false;
|
|
584
|
-
return v;
|
|
585
|
-
});
|
|
586
|
-
// For boolean fields, use equals or OR instead of `in`
|
|
587
|
-
if (booleanValues.length === 1) {
|
|
588
|
-
// Single value - use direct equality
|
|
589
|
-
if (field.includes('.')) {
|
|
590
|
-
applyNestedFilter(where, field, booleanValues[0]);
|
|
591
|
-
}
|
|
592
|
-
else {
|
|
593
|
-
;
|
|
594
|
-
where[field] = booleanValues[0];
|
|
595
|
-
}
|
|
596
|
-
}
|
|
597
|
-
else if (booleanValues.length === 2) {
|
|
598
|
-
// Both true and false selected - don't filter at all
|
|
599
|
-
// (this means "show all")
|
|
600
|
-
return;
|
|
601
|
-
}
|
|
602
|
-
}
|
|
603
|
-
else {
|
|
604
|
-
// Non-boolean set filter - use `in` operator
|
|
605
|
-
if (field.includes('.')) {
|
|
606
|
-
applyNestedFilter(where, field, { in: normalizedValues });
|
|
607
|
-
}
|
|
608
|
-
else {
|
|
609
|
-
;
|
|
610
|
-
where[field] = { in: normalizedValues };
|
|
611
|
-
}
|
|
612
|
-
}
|
|
613
|
-
}
|
|
614
|
-
// Date filter (legacy dateFrom/dateTo format)
|
|
615
|
-
// Only apply if this wasn't already handled by the type-based filter above
|
|
616
|
-
if (('dateFrom' in filter || 'dateTo' in filter) && !('type' in filter) && !('filterType' in filter)) {
|
|
617
|
-
const dateCondition = {};
|
|
618
|
-
if (filter.dateFrom) {
|
|
619
|
-
const date = new Date(filter.dateFrom);
|
|
620
|
-
dateCondition.gte = date.toISOString();
|
|
621
|
-
}
|
|
622
|
-
if (filter.dateTo) {
|
|
623
|
-
const date = new Date(filter.dateTo);
|
|
624
|
-
dateCondition.lte = date.toISOString();
|
|
625
|
-
}
|
|
626
|
-
if (field.includes('.')) {
|
|
627
|
-
applyNestedFilter(where, field, dateCondition);
|
|
628
|
-
}
|
|
629
|
-
else {
|
|
630
|
-
;
|
|
631
|
-
where[field] = dateCondition;
|
|
632
|
-
}
|
|
633
|
-
}
|
|
634
|
-
}
|
|
635
|
-
/**
|
|
636
|
-
* Applies a nested filter (e.g., 'location.name')
|
|
637
|
-
*/
|
|
638
|
-
function applyNestedFilter(where, path, value) {
|
|
639
|
-
const [relation, field] = path.split('.');
|
|
640
|
-
const whereObj = where;
|
|
641
|
-
if (!whereObj[relation]) {
|
|
642
|
-
whereObj[relation] = {};
|
|
643
|
-
}
|
|
644
|
-
;
|
|
645
|
-
whereObj[relation][field] = value;
|
|
646
|
-
}
|
|
647
|
-
/**
|
|
648
|
-
* Creates a nested sort object (e.g., 'location.name' -> { location: { name: 'asc' } })
|
|
649
|
-
* Handles deeply nested paths like 'user.profile.name' -> { user: { profile: { name: 'asc' } } }
|
|
650
|
-
*/
|
|
651
|
-
function createNestedSort(path, direction) {
|
|
652
|
-
if (!path.includes('.')) {
|
|
653
|
-
return { [path]: direction };
|
|
654
|
-
}
|
|
655
|
-
const parts = path.split('.');
|
|
656
|
-
const result = {};
|
|
657
|
-
// Build nested structure from outermost to innermost
|
|
658
|
-
let current = result;
|
|
659
|
-
for (let i = 0; i < parts.length - 1; i++) {
|
|
660
|
-
current[parts[i]] = {};
|
|
661
|
-
current = current[parts[i]];
|
|
662
|
-
}
|
|
663
|
-
// Set the final field to the sort direction
|
|
664
|
-
current[parts[parts.length - 1]] = direction;
|
|
665
|
-
return result;
|
|
666
|
-
}
|
|
667
|
-
/**
|
|
668
|
-
* Gets a nested value from an object (e.g., 'location.name' from record)
|
|
669
|
-
*/
|
|
670
|
-
function getNestedValue(obj, path) {
|
|
671
|
-
const parts = path.split('.');
|
|
672
|
-
let value = obj;
|
|
673
|
-
for (const part of parts) {
|
|
674
|
-
if (value && typeof value === 'object') {
|
|
675
|
-
value = value[part];
|
|
676
|
-
}
|
|
677
|
-
else {
|
|
678
|
-
return undefined;
|
|
679
|
-
}
|
|
680
|
-
}
|
|
681
|
-
return value;
|
|
682
|
-
}
|
|
683
|
-
// ============================================================================
|
|
684
|
-
// Client-Side Datasource Creation
|
|
685
|
-
// ============================================================================
|
|
686
|
-
/**
|
|
687
|
-
* Creates an AG Grid server-side datasource from a data fetcher function
|
|
688
|
-
*
|
|
689
|
-
* This is the main entry point for implementing SSRM. Simply provide a function
|
|
690
|
-
* that fetches data based on AG Grid's request, and this handles all the
|
|
691
|
-
* AG Grid datasource protocol.
|
|
692
|
-
*
|
|
693
|
-
* @param fetcher - Function that fetches data based on AG Grid's request
|
|
694
|
-
* @param options - Optional configuration for error handling and logging
|
|
695
|
-
* @returns IServerSideDatasource compatible with AG Grid
|
|
696
|
-
*
|
|
697
|
-
* @example
|
|
698
|
-
* ```typescript
|
|
699
|
-
* const datasource = createAGGridDatasource(async (request) => {
|
|
700
|
-
* const { startRow, endRow, filterModel, sortModel } = request
|
|
701
|
-
*
|
|
702
|
-
* // Your data fetching logic here
|
|
703
|
-
* const data = await fetchFromAPI({
|
|
704
|
-
* offset: startRow,
|
|
705
|
-
* limit: endRow - startRow,
|
|
706
|
-
* filters: filterModel,
|
|
707
|
-
* sorts: sortModel
|
|
708
|
-
* })
|
|
709
|
-
*
|
|
710
|
-
* return {
|
|
711
|
-
* rows: data.items,
|
|
712
|
-
* lastRow: data.total
|
|
713
|
-
* }
|
|
714
|
-
* })
|
|
715
|
-
* ```
|
|
716
|
-
*/
|
|
717
|
-
export function createAGGridDatasource(fetcher, options = {}) {
|
|
718
|
-
const { onError, onBeforeRequest, onAfterResponse, debug = false } = options;
|
|
719
|
-
return {
|
|
720
|
-
getRows: async (params) => {
|
|
721
|
-
try {
|
|
722
|
-
// Extract request details from AG Grid
|
|
723
|
-
const request = {
|
|
724
|
-
startRow: params.request.startRow,
|
|
725
|
-
endRow: params.request.endRow,
|
|
726
|
-
filterModel: params.request.filterModel,
|
|
727
|
-
sortModel: params.request.sortModel,
|
|
728
|
-
rowGroupCols: params.request.rowGroupCols,
|
|
729
|
-
groupKeys: params.request.groupKeys,
|
|
730
|
-
valueCols: params.request.valueCols,
|
|
731
|
-
pivotCols: params.request.pivotCols,
|
|
732
|
-
pivotMode: params.request.pivotMode,
|
|
733
|
-
};
|
|
734
|
-
if (debug) {
|
|
735
|
-
console.log('[AG Grid SSRM] Request:', request);
|
|
736
|
-
}
|
|
737
|
-
// Call lifecycle hook
|
|
738
|
-
if (onBeforeRequest) {
|
|
739
|
-
onBeforeRequest(request);
|
|
740
|
-
}
|
|
741
|
-
// Fetch data using provided fetcher
|
|
742
|
-
const response = await fetcher(request);
|
|
743
|
-
if (debug) {
|
|
744
|
-
console.log('[AG Grid SSRM] Response:', {
|
|
745
|
-
rowCount: response.rows.length,
|
|
746
|
-
lastRow: response.lastRow,
|
|
747
|
-
});
|
|
748
|
-
}
|
|
749
|
-
// Call lifecycle hook
|
|
750
|
-
if (onAfterResponse) {
|
|
751
|
-
onAfterResponse(response);
|
|
752
|
-
}
|
|
753
|
-
// Send success response to AG Grid
|
|
754
|
-
params.success({
|
|
755
|
-
rowData: response.rows,
|
|
756
|
-
rowCount: response.lastRow,
|
|
757
|
-
groupLevelInfo: response.groupLevelInfo,
|
|
758
|
-
pivotResultFields: response.pivotResultFields,
|
|
759
|
-
});
|
|
760
|
-
}
|
|
761
|
-
catch (error) {
|
|
762
|
-
if (debug) {
|
|
763
|
-
console.error('[AG Grid SSRM] Error:', error);
|
|
764
|
-
}
|
|
765
|
-
if (onError) {
|
|
766
|
-
onError(error);
|
|
767
|
-
}
|
|
768
|
-
// Notify AG Grid of failure
|
|
769
|
-
params.fail();
|
|
770
|
-
}
|
|
771
|
-
},
|
|
772
|
-
};
|
|
773
|
-
}
|
|
774
|
-
// ============================================================================
|
|
775
|
-
// Helper Functions
|
|
776
|
-
// ============================================================================
|
|
777
|
-
/**
|
|
778
|
-
* Checks if the current request is for leaf-level data (not groups)
|
|
779
|
-
*
|
|
780
|
-
* @param request - AG Grid request
|
|
781
|
-
* @returns true if requesting leaf data, false if requesting groups
|
|
782
|
-
*/
|
|
783
|
-
export function isLeafDataRequest(request) {
|
|
784
|
-
const hasGrouping = request.rowGroupCols && request.rowGroupCols.length > 0;
|
|
785
|
-
if (!hasGrouping)
|
|
786
|
-
return true;
|
|
787
|
-
const currentGroupLevel = request.groupKeys?.length ?? 0;
|
|
788
|
-
return currentGroupLevel >= request.rowGroupCols.length;
|
|
789
|
-
}
|
|
790
|
-
/**
|
|
791
|
-
* Gets the current grouping level (0-based)
|
|
792
|
-
*
|
|
793
|
-
* @param request - AG Grid request
|
|
794
|
-
* @returns Current group level, or -1 if not grouping
|
|
795
|
-
*/
|
|
796
|
-
export function getCurrentGroupLevel(request) {
|
|
797
|
-
if (!request.rowGroupCols || request.rowGroupCols.length === 0)
|
|
798
|
-
return -1;
|
|
799
|
-
return request.groupKeys?.length ?? 0;
|
|
800
|
-
}
|
|
801
|
-
/**
|
|
802
|
-
* Gets the column being grouped at the current level
|
|
803
|
-
*
|
|
804
|
-
* @param request - AG Grid request
|
|
805
|
-
* @returns Column configuration for current group level, or undefined
|
|
806
|
-
*/
|
|
807
|
-
export function getCurrentGroupColumn(request) {
|
|
808
|
-
const level = getCurrentGroupLevel(request);
|
|
809
|
-
if (level < 0 || !request.rowGroupCols)
|
|
810
|
-
return undefined;
|
|
811
|
-
return request.rowGroupCols[level];
|
|
812
|
-
}
|
|
813
|
-
// ============================================================================
|
|
814
|
-
// Default Grid Configuration
|
|
815
|
-
// ============================================================================
|
|
816
|
-
/**
|
|
817
|
-
* Default column definition for SSRM grids
|
|
818
|
-
*/
|
|
819
|
-
export const defaultSSRMColDef = {
|
|
820
|
-
sortable: true,
|
|
821
|
-
resizable: true,
|
|
822
|
-
filter: true,
|
|
823
|
-
floatingFilter: true,
|
|
824
|
-
enableRowGroup: true,
|
|
825
|
-
flex: 1,
|
|
826
|
-
menuTabs: ['filterMenuTab', 'generalMenuTab'],
|
|
827
|
-
};
|
|
828
|
-
/**
|
|
829
|
-
* Default grid options for SSRM
|
|
830
|
-
*/
|
|
831
|
-
export const defaultSSRMGridOptions = {
|
|
832
|
-
rowModelType: 'serverSide',
|
|
833
|
-
pagination: true,
|
|
834
|
-
paginationPageSize: 100,
|
|
835
|
-
cacheBlockSize: 100,
|
|
836
|
-
rowGroupPanelShow: 'always',
|
|
837
|
-
groupDisplayType: 'groupRows',
|
|
838
|
-
animateRows: true,
|
|
839
|
-
};
|
|
840
|
-
/**
|
|
841
|
-
* Predefined filter configurations for common column types
|
|
842
|
-
*/
|
|
843
|
-
export const filterConfigs = {
|
|
844
|
-
/** Text column with text filter */
|
|
845
|
-
text: {
|
|
846
|
-
filter: 'agTextColumnFilter',
|
|
847
|
-
filterParams: {
|
|
848
|
-
buttons: ['clear'],
|
|
849
|
-
suppressAndOrCondition: true,
|
|
850
|
-
},
|
|
851
|
-
},
|
|
852
|
-
/** Number column with number filter */
|
|
853
|
-
number: {
|
|
854
|
-
filter: 'agNumberColumnFilter',
|
|
855
|
-
filterParams: {
|
|
856
|
-
buttons: ['clear'],
|
|
857
|
-
suppressAndOrCondition: true,
|
|
858
|
-
},
|
|
859
|
-
},
|
|
860
|
-
/** Date column with date filter */
|
|
861
|
-
date: {
|
|
862
|
-
filter: 'agDateColumnFilter',
|
|
863
|
-
filterParams: {
|
|
864
|
-
buttons: ['clear'],
|
|
865
|
-
suppressAndOrCondition: true,
|
|
866
|
-
},
|
|
867
|
-
},
|
|
868
|
-
/** Date range filter (for start dates) */
|
|
869
|
-
startDate: {
|
|
870
|
-
filter: 'agDateColumnFilter',
|
|
871
|
-
filterParams: {
|
|
872
|
-
buttons: ['clear'],
|
|
873
|
-
suppressAndOrCondition: true,
|
|
874
|
-
filterOptions: ['greaterThan', 'inRange'],
|
|
875
|
-
},
|
|
876
|
-
},
|
|
877
|
-
/** Date range filter (for end dates) */
|
|
878
|
-
endDate: {
|
|
879
|
-
filter: 'agDateColumnFilter',
|
|
880
|
-
filterParams: {
|
|
881
|
-
buttons: ['clear'],
|
|
882
|
-
suppressAndOrCondition: true,
|
|
883
|
-
filterOptions: ['lessThan', 'inRange'],
|
|
884
|
-
},
|
|
885
|
-
},
|
|
886
|
-
/** Set filter (for categories) */
|
|
887
|
-
set: {
|
|
888
|
-
filter: 'agSetColumnFilter',
|
|
889
|
-
filterParams: {
|
|
890
|
-
buttons: ['clear'],
|
|
891
|
-
},
|
|
892
|
-
},
|
|
893
|
-
/** Boolean filter (Yes/No) */
|
|
894
|
-
boolean: {
|
|
895
|
-
filter: 'agSetColumnFilter',
|
|
896
|
-
filterParams: {
|
|
897
|
-
values: ['Yes', 'No'],
|
|
898
|
-
buttons: ['clear'],
|
|
899
|
-
},
|
|
900
|
-
},
|
|
901
|
-
};
|