@memberjunction/core 2.72.0 → 2.74.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (46) hide show
  1. package/dist/generic/applicationInfo.d.ts +92 -1
  2. package/dist/generic/applicationInfo.d.ts.map +1 -1
  3. package/dist/generic/applicationInfo.js +92 -1
  4. package/dist/generic/applicationInfo.js.map +1 -1
  5. package/dist/generic/baseInfo.d.ts +15 -0
  6. package/dist/generic/baseInfo.d.ts.map +1 -1
  7. package/dist/generic/baseInfo.js +15 -0
  8. package/dist/generic/baseInfo.js.map +1 -1
  9. package/dist/generic/entityInfo.d.ts +184 -3
  10. package/dist/generic/entityInfo.d.ts.map +1 -1
  11. package/dist/generic/entityInfo.js +184 -3
  12. package/dist/generic/entityInfo.js.map +1 -1
  13. package/dist/generic/interfaces.d.ts +119 -4
  14. package/dist/generic/interfaces.d.ts.map +1 -1
  15. package/dist/generic/interfaces.js +44 -3
  16. package/dist/generic/interfaces.js.map +1 -1
  17. package/dist/generic/providerBase.d.ts +248 -8
  18. package/dist/generic/providerBase.d.ts.map +1 -1
  19. package/dist/generic/providerBase.js +185 -2
  20. package/dist/generic/providerBase.js.map +1 -1
  21. package/dist/generic/queryInfo.d.ts +312 -1
  22. package/dist/generic/queryInfo.d.ts.map +1 -1
  23. package/dist/generic/queryInfo.js +371 -2
  24. package/dist/generic/queryInfo.js.map +1 -1
  25. package/dist/generic/querySQLFilters.d.ts +54 -0
  26. package/dist/generic/querySQLFilters.d.ts.map +1 -0
  27. package/dist/generic/querySQLFilters.js +84 -0
  28. package/dist/generic/querySQLFilters.js.map +1 -0
  29. package/dist/generic/runQuery.d.ts +42 -0
  30. package/dist/generic/runQuery.d.ts.map +1 -1
  31. package/dist/generic/runQuery.js +26 -0
  32. package/dist/generic/runQuery.js.map +1 -1
  33. package/dist/generic/runQuerySQLFilterImplementations.d.ts +51 -0
  34. package/dist/generic/runQuerySQLFilterImplementations.d.ts.map +1 -0
  35. package/dist/generic/runQuerySQLFilterImplementations.js +238 -0
  36. package/dist/generic/runQuerySQLFilterImplementations.js.map +1 -0
  37. package/dist/generic/securityInfo.d.ts +212 -13
  38. package/dist/generic/securityInfo.d.ts.map +1 -1
  39. package/dist/generic/securityInfo.js +200 -14
  40. package/dist/generic/securityInfo.js.map +1 -1
  41. package/dist/index.d.ts +4 -0
  42. package/dist/index.d.ts.map +1 -1
  43. package/dist/index.js +4 -0
  44. package/dist/index.js.map +1 -1
  45. package/package.json +2 -2
  46. package/readme.md +550 -1
package/readme.md CHANGED
@@ -299,13 +299,16 @@ const users = dynamicResults.Results; // Properly typed as UserEntity[]
299
299
 
300
300
  ### RunQuery Class
301
301
 
302
- Execute stored queries with parameters:
302
+ The `RunQuery` class provides secure execution of parameterized stored queries with advanced SQL injection protection and type-safe parameter handling.
303
+
304
+ #### Basic Usage
303
305
 
304
306
  ```typescript
305
307
  import { RunQuery, RunQueryParams } from '@memberjunction/core';
306
308
 
307
309
  const rq = new RunQuery();
308
310
 
311
+ // Execute by Query ID
309
312
  const params: RunQueryParams = {
310
313
  QueryID: '12345',
311
314
  Parameters: {
@@ -316,6 +319,363 @@ const params: RunQueryParams = {
316
319
  };
317
320
 
318
321
  const results = await rq.RunQuery(params);
322
+
323
+ // Execute by Query Name and Category
324
+ const namedParams: RunQueryParams = {
325
+ QueryName: 'Monthly Sales Report',
326
+ CategoryName: 'Sales',
327
+ Parameters: {
328
+ Month: 12,
329
+ Year: 2024,
330
+ MinAmount: 1000
331
+ }
332
+ };
333
+
334
+ const namedResults = await rq.RunQuery(namedParams);
335
+ ```
336
+
337
+ #### Parameterized Queries
338
+
339
+ RunQuery supports powerful parameterized queries using Nunjucks templates with built-in SQL injection protection:
340
+
341
+ ```sql
342
+ -- Example stored query in the database
343
+ SELECT
344
+ o.ID,
345
+ o.OrderDate,
346
+ o.TotalAmount,
347
+ c.CustomerName
348
+ FROM Orders o
349
+ INNER JOIN Customers c ON o.CustomerID = c.ID
350
+ WHERE
351
+ o.OrderDate >= {{ startDate | sqlDate }} AND
352
+ o.OrderDate <= {{ endDate | sqlDate }} AND
353
+ o.Status IN {{ statusList | sqlIn }} AND
354
+ o.TotalAmount >= {{ minAmount | sqlNumber }}
355
+ {% if includeCustomerInfo %}
356
+ AND c.IsActive = {{ isActive | sqlBoolean }}
357
+ {% endif %}
358
+ ORDER BY {{ orderClause | sqlNoKeywordsExpression }}
359
+ ```
360
+
361
+ #### SQL Security Filters
362
+
363
+ RunQuery includes comprehensive SQL filters to prevent injection attacks:
364
+
365
+ ##### sqlString Filter
366
+ Safely escapes string values by doubling single quotes and wrapping in quotes:
367
+
368
+ ```sql
369
+ -- Template
370
+ WHERE CustomerName = {{ name | sqlString }}
371
+
372
+ -- Input: "O'Brien"
373
+ -- Output: WHERE CustomerName = 'O''Brien'
374
+ ```
375
+
376
+ ##### sqlNumber Filter
377
+ Validates and formats numeric values:
378
+
379
+ ```sql
380
+ -- Template
381
+ WHERE Amount >= {{ minAmount | sqlNumber }}
382
+
383
+ -- Input: "1000.50"
384
+ -- Output: WHERE Amount >= 1000.5
385
+ ```
386
+
387
+ ##### sqlDate Filter
388
+ Formats dates in ISO 8601 format:
389
+
390
+ ```sql
391
+ -- Template
392
+ WHERE CreatedDate >= {{ startDate | sqlDate }}
393
+
394
+ -- Input: "2024-01-15"
395
+ -- Output: WHERE CreatedDate >= '2024-01-15T00:00:00.000Z'
396
+ ```
397
+
398
+ ##### sqlBoolean Filter
399
+ Converts boolean values to SQL bit representation:
400
+
401
+ ```sql
402
+ -- Template
403
+ WHERE IsActive = {{ active | sqlBoolean }}
404
+
405
+ -- Input: true
406
+ -- Output: WHERE IsActive = 1
407
+ ```
408
+
409
+ ##### sqlIdentifier Filter
410
+ Safely formats SQL identifiers (table/column names):
411
+
412
+ ```sql
413
+ -- Template
414
+ SELECT * FROM {{ tableName | sqlIdentifier }}
415
+
416
+ -- Input: "UserAccounts"
417
+ -- Output: SELECT * FROM [UserAccounts]
418
+ ```
419
+
420
+ ##### sqlIn Filter
421
+ Formats arrays for SQL IN clauses:
422
+
423
+ ```sql
424
+ -- Template
425
+ WHERE Status IN {{ statusList | sqlIn }}
426
+
427
+ -- Input: ['Active', 'Pending', 'Review']
428
+ -- Output: WHERE Status IN ('Active', 'Pending', 'Review')
429
+ ```
430
+
431
+ ##### sqlNoKeywordsExpression Filter (NEW)
432
+ Validates SQL expressions by blocking dangerous keywords while allowing safe expressions:
433
+
434
+ ```sql
435
+ -- Template
436
+ ORDER BY {{ orderClause | sqlNoKeywordsExpression }}
437
+
438
+ -- ✅ ALLOWED: "Revenue DESC, CreatedDate ASC"
439
+ -- ✅ ALLOWED: "SUM(Amount) DESC"
440
+ -- ✅ ALLOWED: "CASE WHEN Amount > 1000 THEN 1 ELSE 0 END"
441
+ -- ❌ BLOCKED: "Revenue; DROP TABLE Users"
442
+ -- ❌ BLOCKED: "Revenue UNION SELECT * FROM Secrets"
443
+ ```
444
+
445
+ #### Parameter Types and Validation
446
+
447
+ Query parameters are defined in the `QueryParameter` entity with automatic validation:
448
+
449
+ ```typescript
450
+ // Example parameter definitions
451
+ {
452
+ name: 'startDate',
453
+ type: 'date',
454
+ isRequired: true,
455
+ description: 'Start date for filtering records',
456
+ sampleValue: '2024-01-01'
457
+ },
458
+ {
459
+ name: 'statusList',
460
+ type: 'array',
461
+ isRequired: false,
462
+ defaultValue: '["Active", "Pending"]',
463
+ description: 'List of allowed status values'
464
+ },
465
+ {
466
+ name: 'minAmount',
467
+ type: 'number',
468
+ isRequired: true,
469
+ description: 'Minimum amount threshold'
470
+ }
471
+ ```
472
+
473
+ #### Query Permissions
474
+
475
+ Queries support role-based access control:
476
+
477
+ ```typescript
478
+ // Check if user can run a query (server-side or client-side)
479
+ const query = md.Provider.Queries.find(q => q.ID === queryId);
480
+ const canRun = query.UserCanRun(contextUser);
481
+ const hasPermission = query.UserHasRunPermissions(contextUser);
482
+
483
+ // Queries are only executable if:
484
+ // 1. User has required role permissions
485
+ // 2. Query status is 'Approved'
486
+ ```
487
+
488
+ #### Advanced Features
489
+
490
+ ##### Conditional SQL Blocks
491
+ Use Nunjucks conditionals for dynamic query structure:
492
+
493
+ ```sql
494
+ SELECT
495
+ CustomerID,
496
+ CustomerName,
497
+ TotalOrders
498
+ {% if includeRevenue %}
499
+ , TotalRevenue
500
+ {% endif %}
501
+ FROM CustomerSummary
502
+ WHERE CreatedDate >= {{ startDate | sqlDate }}
503
+ {% if filterByRegion %}
504
+ AND Region = {{ region | sqlString }}
505
+ {% endif %}
506
+ ```
507
+
508
+ ##### Complex Parameter Examples
509
+
510
+ ```typescript
511
+ const complexParams: RunQueryParams = {
512
+ QueryName: 'Advanced Sales Analysis',
513
+ Parameters: {
514
+ // Date range
515
+ startDate: '2024-01-01',
516
+ endDate: '2024-12-31',
517
+
518
+ // Array parameters
519
+ regions: ['North', 'South', 'East'],
520
+ productCategories: [1, 2, 5, 8],
521
+
522
+ // Boolean flags
523
+ includeDiscounts: true,
524
+ excludeReturns: false,
525
+
526
+ // Numeric thresholds
527
+ minOrderValue: 500.00,
528
+ maxOrderValue: 10000.00,
529
+
530
+ // Dynamic expressions (safely validated)
531
+ orderBy: 'TotalRevenue DESC, CustomerName ASC',
532
+ groupingExpression: 'Region, ProductCategory'
533
+ }
534
+ };
535
+ ```
536
+
537
+ #### Error Handling
538
+
539
+ RunQuery provides detailed error information:
540
+
541
+ ```typescript
542
+ const result = await rq.RunQuery(params);
543
+
544
+ if (!result.Success) {
545
+ console.error('Query failed:', result.ErrorMessage);
546
+
547
+ // Common error types:
548
+ // - "Query not found"
549
+ // - "User does not have permission to run this query"
550
+ // - "Query is not in an approved status (current status: Pending)"
551
+ // - "Parameter validation failed: Required parameter 'startDate' is missing"
552
+ // - "Dangerous SQL keyword detected: DROP"
553
+ // - "Template processing failed: Invalid date: 'not-a-date'"
554
+ } else {
555
+ console.log('Query executed successfully');
556
+ console.log('Rows returned:', result.RowCount);
557
+ console.log('Execution time:', result.ExecutionTime, 'ms');
558
+ console.log('Applied parameters:', result.AppliedParameters);
559
+
560
+ // Process results
561
+ result.Results.forEach(row => {
562
+ console.log('Row data:', row);
563
+ });
564
+ }
565
+ ```
566
+
567
+ #### Query Categories
568
+
569
+ Organize queries using categories for better management:
570
+
571
+ ```typescript
572
+ // Query by category
573
+ const categoryParams: RunQueryParams = {
574
+ QueryName: 'Top Customers',
575
+ CategoryName: 'Sales Reports',
576
+ Parameters: { limit: 10 }
577
+ };
578
+
579
+ // Query with category ID
580
+ const categoryIdParams: RunQueryParams = {
581
+ QueryName: 'Revenue Trends',
582
+ CategoryID: 'sales-cat-123',
583
+ Parameters: { months: 12 }
584
+ };
585
+ ```
586
+
587
+ #### Best Practices for RunQuery
588
+
589
+ 1. **Always Use Filters**: Apply the appropriate SQL filter to every parameter
590
+ 2. **Define Clear Parameters**: Use descriptive names and provide sample values
591
+ 3. **Set Proper Permissions**: Restrict query access to appropriate roles
592
+ 4. **Validate Input Types**: Use the built-in type system (string, number, date, boolean, array)
593
+ 5. **Handle Errors Gracefully**: Check Success and provide meaningful error messages
594
+ 6. **Use Approved Queries**: Only execute queries with 'Approved' status
595
+ 7. **Leverage Categories**: Organize queries by functional area or team
596
+ 8. **Test Parameter Combinations**: Verify all conditional blocks work correctly
597
+ 9. **Document Query Purpose**: Add clear descriptions for queries and parameters
598
+ 10. **Review SQL Security**: Regular audit of complex expressions and dynamic SQL
599
+
600
+ #### Performance Considerations
601
+
602
+ - **Parameter Indexing**: Ensure filtered columns have appropriate database indexes
603
+ - **Query Optimization**: Use efficient JOINs and WHERE clauses
604
+ - **Result Limiting**: Consider adding TOP/LIMIT clauses for large datasets
605
+ - **Caching**: Results are not automatically cached - implement application-level caching if needed
606
+ - **Connection Pooling**: RunQuery leverages provider connection pooling automatically
607
+
608
+ #### Integration with AI Systems
609
+
610
+ RunQuery is designed to work seamlessly with AI systems:
611
+
612
+ - **Token-Efficient Metadata**: Filter definitions are optimized for AI prompts
613
+ - **Self-Documenting**: Parameter definitions include examples and descriptions
614
+ - **Safe Code Generation**: AI can generate queries using the secure filter system
615
+ - **Validation Feedback**: Clear error messages help AI systems learn and adapt
616
+
617
+ #### Example: Complete Sales Dashboard Query
618
+
619
+ ```sql
620
+ -- Stored query: "Sales Dashboard Data"
621
+ SELECT
622
+ DATEPART(month, o.OrderDate) AS Month,
623
+ DATEPART(year, o.OrderDate) AS Year,
624
+ COUNT(*) AS OrderCount,
625
+ SUM(o.TotalAmount) AS TotalRevenue,
626
+ AVG(o.TotalAmount) AS AvgOrderValue,
627
+ COUNT(DISTINCT o.CustomerID) AS UniqueCustomers
628
+ {% if includeProductBreakdown %}
629
+ , p.CategoryName
630
+ , SUM(od.Quantity) AS TotalQuantity
631
+ {% endif %}
632
+ FROM Orders o
633
+ {% if includeProductBreakdown %}
634
+ INNER JOIN OrderDetails od ON o.ID = od.OrderID
635
+ INNER JOIN Products p ON od.ProductID = p.ID
636
+ {% endif %}
637
+ WHERE
638
+ o.OrderDate >= {{ startDate | sqlDate }} AND
639
+ o.OrderDate <= {{ endDate | sqlDate }} AND
640
+ o.Status IN {{ allowedStatuses | sqlIn }}
641
+ {% if filterByRegion %}
642
+ AND o.Region = {{ region | sqlString }}
643
+ {% endif %}
644
+ {% if minOrderValue %}
645
+ AND o.TotalAmount >= {{ minOrderValue | sqlNumber }}
646
+ {% endif %}
647
+ GROUP BY
648
+ DATEPART(month, o.OrderDate),
649
+ DATEPART(year, o.OrderDate)
650
+ {% if includeProductBreakdown %}
651
+ , p.CategoryName
652
+ {% endif %}
653
+ ORDER BY {{ orderExpression | sqlNoKeywordsExpression }}
654
+ ```
655
+
656
+ ```typescript
657
+ // Execute the dashboard query
658
+ const dashboardResult = await rq.RunQuery({
659
+ QueryName: 'Sales Dashboard Data',
660
+ CategoryName: 'Analytics',
661
+ Parameters: {
662
+ startDate: '2024-01-01',
663
+ endDate: '2024-12-31',
664
+ allowedStatuses: ['Completed', 'Shipped'],
665
+ includeProductBreakdown: true,
666
+ filterByRegion: true,
667
+ region: 'North America',
668
+ minOrderValue: 100,
669
+ orderExpression: 'Year DESC, Month DESC, TotalRevenue DESC'
670
+ }
671
+ });
672
+
673
+ if (dashboardResult.Success) {
674
+ // Process the comprehensive dashboard data
675
+ const monthlyData = dashboardResult.Results;
676
+ console.log(`Generated dashboard with ${monthlyData.length} data points`);
677
+ console.log(`Parameters applied:`, dashboardResult.AppliedParameters);
678
+ }
319
679
  ```
320
680
 
321
681
  ### RunReport Class
@@ -594,6 +954,195 @@ SetProvider(myProvider);
594
954
 
595
955
  This library is written in TypeScript and provides full type definitions. All generated entity classes include proper typing for IntelliSense support.
596
956
 
957
+ ## Datasets
958
+
959
+ Datasets are a powerful performance optimization feature in MemberJunction that allows efficient bulk loading of related entity data. Instead of making multiple individual API calls to load different entities, datasets enable you to load collections of related data in a single operation.
960
+
961
+ ### What Are Datasets?
962
+
963
+ Datasets are pre-defined collections of related entity data that can be loaded together. Each dataset contains multiple "dataset items" where each item represents data from a specific entity. This approach dramatically reduces database round trips and improves application performance.
964
+
965
+ ### How Datasets Work
966
+
967
+ 1. **Dataset Definition**: Datasets are defined in the `Datasets` entity with a unique name and description
968
+ 2. **Dataset Items**: Each dataset contains multiple items defined in the `Dataset Items` entity, where each item specifies:
969
+ - The entity to load
970
+ - An optional filter to apply
971
+ - A unique code to identify the item within the dataset
972
+ 3. **Bulk Loading**: When you request a dataset, all items are loaded in parallel in a single database operation
973
+ 4. **Caching**: Datasets can be cached locally for offline use or improved performance
974
+
975
+ ### Key Benefits
976
+
977
+ - **Reduced Database Round Trips**: Load multiple entities in one operation instead of many
978
+ - **Better Performance**: Parallel loading and optimized queries
979
+ - **Caching Support**: Built-in local caching with automatic cache invalidation
980
+ - **Offline Capability**: Cached datasets enable offline functionality
981
+ - **Consistency**: All data in a dataset is loaded at the same point in time
982
+
983
+ ### The MJ_Metadata Dataset
984
+
985
+ The most important dataset in MemberJunction is `MJ_Metadata`, which loads all system metadata including:
986
+ - Entities and their fields
987
+ - Applications and settings
988
+ - User roles and permissions
989
+ - Query definitions
990
+ - Navigation items
991
+ - And more...
992
+
993
+ This dataset is used internally by MemberJunction to bootstrap the metadata system efficiently.
994
+
995
+ ### Dataset API Methods
996
+
997
+ The Metadata class provides several methods for working with datasets:
998
+
999
+ #### GetDatasetByName()
1000
+ Always retrieves fresh data from the server without checking cache:
1001
+
1002
+ ```typescript
1003
+ const md = new Metadata();
1004
+ const dataset = await md.GetDatasetByName('MJ_Metadata');
1005
+
1006
+ if (dataset.Success) {
1007
+ // Process the dataset results
1008
+ for (const item of dataset.Results) {
1009
+ console.log(`Loaded ${item.Results.length} records from ${item.EntityName}`);
1010
+ }
1011
+ }
1012
+ ```
1013
+
1014
+ #### GetAndCacheDatasetByName()
1015
+ Retrieves and caches the dataset, using cached version if up-to-date:
1016
+
1017
+ ```typescript
1018
+ // This will use cache if available and up-to-date
1019
+ const dataset = await md.GetAndCacheDatasetByName('ProductCatalog');
1020
+
1021
+ // With custom filters for specific items
1022
+ const filters: DatasetItemFilterType[] = [
1023
+ { ItemCode: 'Products', Filter: 'IsActive = 1' },
1024
+ { ItemCode: 'Categories', Filter: 'ParentID IS NULL' }
1025
+ ];
1026
+ const filteredDataset = await md.GetAndCacheDatasetByName('ProductCatalog', filters);
1027
+ ```
1028
+
1029
+ #### IsDatasetCacheUpToDate()
1030
+ Checks if the cached version is current without loading the data:
1031
+
1032
+ ```typescript
1033
+ const isUpToDate = await md.IsDatasetCacheUpToDate('ProductCatalog');
1034
+ if (!isUpToDate) {
1035
+ console.log('Cache is stale, refreshing...');
1036
+ await md.GetAndCacheDatasetByName('ProductCatalog');
1037
+ }
1038
+ ```
1039
+
1040
+ #### ClearDatasetCache()
1041
+ Removes a dataset from local cache:
1042
+
1043
+ ```typescript
1044
+ // Clear specific dataset
1045
+ await md.ClearDatasetCache('ProductCatalog');
1046
+
1047
+ // Clear dataset with specific filters
1048
+ await md.ClearDatasetCache('ProductCatalog', filters);
1049
+ ```
1050
+
1051
+ ### Dataset Filtering
1052
+
1053
+ You can apply filters to individual dataset items to load subsets of data:
1054
+
1055
+ ```typescript
1056
+ const filters: DatasetItemFilterType[] = [
1057
+ {
1058
+ ItemCode: 'Orders',
1059
+ Filter: "OrderDate >= '2024-01-01' AND Status = 'Active'"
1060
+ },
1061
+ {
1062
+ ItemCode: 'OrderDetails',
1063
+ Filter: "OrderID IN (SELECT ID FROM Orders WHERE OrderDate >= '2024-01-01')"
1064
+ }
1065
+ ];
1066
+
1067
+ const dataset = await md.GetAndCacheDatasetByName('RecentOrders', filters);
1068
+ ```
1069
+
1070
+ ### Dataset Caching
1071
+
1072
+ Datasets are cached using the provider's local storage implementation:
1073
+ - **Browser**: IndexedDB or localStorage
1074
+ - **Node.js**: File system or memory cache
1075
+ - **React Native**: AsyncStorage
1076
+
1077
+ The cache key includes:
1078
+ - Dataset name
1079
+ - Applied filters (if any)
1080
+ - Connection string (to prevent cache conflicts between environments)
1081
+
1082
+ ### Cache Invalidation
1083
+
1084
+ The cache is automatically invalidated when:
1085
+ - Any entity in the dataset has newer data on the server
1086
+ - Row counts differ between cache and server
1087
+ - You manually clear the cache
1088
+
1089
+ ### Creating Custom Datasets
1090
+
1091
+ To create your own dataset:
1092
+
1093
+ 1. Create a record in the `Datasets` entity:
1094
+ ```typescript
1095
+ const datasetEntity = await md.GetEntityObject<DatasetEntity>('Datasets');
1096
+ datasetEntity.Name = 'CustomerDashboard';
1097
+ datasetEntity.Description = 'All data needed for customer dashboard';
1098
+ await datasetEntity.Save();
1099
+ ```
1100
+
1101
+ 2. Add dataset items for each entity to include:
1102
+ ```typescript
1103
+ const itemEntity = await md.GetEntityObject<DatasetItemEntity>('Dataset Items');
1104
+ itemEntity.DatasetID = datasetEntity.ID;
1105
+ itemEntity.Code = 'Customers';
1106
+ itemEntity.EntityID = md.EntityByName('Customers').ID;
1107
+ itemEntity.Sequence = 1;
1108
+ itemEntity.WhereClause = 'IsActive = 1';
1109
+ await itemEntity.Save();
1110
+ ```
1111
+
1112
+ ### Best Practices
1113
+
1114
+ 1. **Use Datasets for Related Data**: When you need multiple entities that are logically related
1115
+ 2. **Cache Strategically**: Use `GetAndCacheDatasetByName()` for data that doesn't change frequently
1116
+ 3. **Apply Filters Wisely**: Filters reduce data volume but make cache keys more specific
1117
+ 4. **Monitor Cache Size**: Large datasets can consume significant local storage
1118
+ 5. **Refresh When Needed**: Use `IsDatasetCacheUpToDate()` to check before using cached data
1119
+
1120
+ ### Example: Loading a Dashboard
1121
+
1122
+ ```typescript
1123
+ // Define a dataset for a sales dashboard
1124
+ const dashboardFilters: DatasetItemFilterType[] = [
1125
+ { ItemCode: 'Sales', Filter: "Date >= DATEADD(day, -30, GETDATE())" },
1126
+ { ItemCode: 'Customers', Filter: "LastOrderDate >= DATEADD(day, -30, GETDATE())" },
1127
+ { ItemCode: 'Products', Filter: "StockLevel < ReorderLevel" }
1128
+ ];
1129
+
1130
+ // Load with caching for performance
1131
+ const dashboard = await md.GetAndCacheDatasetByName('SalesDashboard', dashboardFilters);
1132
+
1133
+ if (dashboard.Success) {
1134
+ // Extract individual entity results
1135
+ const recentSales = dashboard.Results.find(r => r.Code === 'Sales')?.Results || [];
1136
+ const activeCustomers = dashboard.Results.find(r => r.Code === 'Customers')?.Results || [];
1137
+ const lowStockProducts = dashboard.Results.find(r => r.Code === 'Products')?.Results || [];
1138
+
1139
+ // Use the data to render your dashboard
1140
+ console.log(`Recent sales: ${recentSales.length}`);
1141
+ console.log(`Active customers: ${activeCustomers.length}`);
1142
+ console.log(`Low stock products: ${lowStockProducts.length}`);
1143
+ }
1144
+ ```
1145
+
597
1146
  ## License
598
1147
 
599
1148
  ISC License - see LICENSE file for details