@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.
- package/dist/generic/applicationInfo.d.ts +92 -1
- package/dist/generic/applicationInfo.d.ts.map +1 -1
- package/dist/generic/applicationInfo.js +92 -1
- package/dist/generic/applicationInfo.js.map +1 -1
- package/dist/generic/baseInfo.d.ts +15 -0
- package/dist/generic/baseInfo.d.ts.map +1 -1
- package/dist/generic/baseInfo.js +15 -0
- package/dist/generic/baseInfo.js.map +1 -1
- package/dist/generic/entityInfo.d.ts +184 -3
- package/dist/generic/entityInfo.d.ts.map +1 -1
- package/dist/generic/entityInfo.js +184 -3
- package/dist/generic/entityInfo.js.map +1 -1
- package/dist/generic/interfaces.d.ts +119 -4
- package/dist/generic/interfaces.d.ts.map +1 -1
- package/dist/generic/interfaces.js +44 -3
- package/dist/generic/interfaces.js.map +1 -1
- package/dist/generic/providerBase.d.ts +248 -8
- package/dist/generic/providerBase.d.ts.map +1 -1
- package/dist/generic/providerBase.js +185 -2
- package/dist/generic/providerBase.js.map +1 -1
- package/dist/generic/queryInfo.d.ts +312 -1
- package/dist/generic/queryInfo.d.ts.map +1 -1
- package/dist/generic/queryInfo.js +371 -2
- package/dist/generic/queryInfo.js.map +1 -1
- package/dist/generic/querySQLFilters.d.ts +54 -0
- package/dist/generic/querySQLFilters.d.ts.map +1 -0
- package/dist/generic/querySQLFilters.js +84 -0
- package/dist/generic/querySQLFilters.js.map +1 -0
- package/dist/generic/runQuery.d.ts +42 -0
- package/dist/generic/runQuery.d.ts.map +1 -1
- package/dist/generic/runQuery.js +26 -0
- package/dist/generic/runQuery.js.map +1 -1
- package/dist/generic/runQuerySQLFilterImplementations.d.ts +51 -0
- package/dist/generic/runQuerySQLFilterImplementations.d.ts.map +1 -0
- package/dist/generic/runQuerySQLFilterImplementations.js +238 -0
- package/dist/generic/runQuerySQLFilterImplementations.js.map +1 -0
- package/dist/generic/securityInfo.d.ts +212 -13
- package/dist/generic/securityInfo.d.ts.map +1 -1
- package/dist/generic/securityInfo.js +200 -14
- package/dist/generic/securityInfo.js.map +1 -1
- package/dist/index.d.ts +4 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -0
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
- 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
|
-
|
|
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
|