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/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
- };