@memberjunction/core 3.4.0 → 4.1.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 (107) hide show
  1. package/dist/generic/InMemoryLocalStorageProvider.d.ts +1 -1
  2. package/dist/generic/InMemoryLocalStorageProvider.js +2 -6
  3. package/dist/generic/InMemoryLocalStorageProvider.js.map +1 -1
  4. package/dist/generic/QueryCache.d.ts +1 -1
  5. package/dist/generic/QueryCache.js +6 -10
  6. package/dist/generic/QueryCache.js.map +1 -1
  7. package/dist/generic/QueryCacheConfig.js +1 -2
  8. package/dist/generic/RegisterForStartup.d.ts +2 -2
  9. package/dist/generic/RegisterForStartup.js +7 -12
  10. package/dist/generic/RegisterForStartup.js.map +1 -1
  11. package/dist/generic/applicationInfo.d.ts +3 -3
  12. package/dist/generic/applicationInfo.js +4 -10
  13. package/dist/generic/applicationInfo.js.map +1 -1
  14. package/dist/generic/authEvaluator.d.ts +1 -1
  15. package/dist/generic/authEvaluator.js +4 -8
  16. package/dist/generic/authEvaluator.js.map +1 -1
  17. package/dist/generic/authTypes.js +1 -4
  18. package/dist/generic/authTypes.js.map +1 -1
  19. package/dist/generic/baseEngine.d.ts +5 -5
  20. package/dist/generic/baseEngine.js +51 -56
  21. package/dist/generic/baseEngine.js.map +1 -1
  22. package/dist/generic/baseEngineRegistry.js +13 -17
  23. package/dist/generic/baseEngineRegistry.js.map +1 -1
  24. package/dist/generic/baseEntity.d.ts +171 -5
  25. package/dist/generic/baseEntity.d.ts.map +1 -1
  26. package/dist/generic/baseEntity.js +651 -121
  27. package/dist/generic/baseEntity.js.map +1 -1
  28. package/dist/generic/baseInfo.js +3 -7
  29. package/dist/generic/baseInfo.js.map +1 -1
  30. package/dist/generic/compositeKey.d.ts +2 -2
  31. package/dist/generic/compositeKey.js +5 -11
  32. package/dist/generic/compositeKey.js.map +1 -1
  33. package/dist/generic/databaseProviderBase.d.ts +2 -2
  34. package/dist/generic/databaseProviderBase.js +2 -6
  35. package/dist/generic/databaseProviderBase.js.map +1 -1
  36. package/dist/generic/entityInfo.d.ts +84 -5
  37. package/dist/generic/entityInfo.d.ts.map +1 -1
  38. package/dist/generic/entityInfo.js +235 -108
  39. package/dist/generic/entityInfo.js.map +1 -1
  40. package/dist/generic/explorerNavigationItem.d.ts +1 -1
  41. package/dist/generic/explorerNavigationItem.js +2 -6
  42. package/dist/generic/explorerNavigationItem.js.map +1 -1
  43. package/dist/generic/graphqlTypeNames.d.ts +1 -1
  44. package/dist/generic/graphqlTypeNames.js +4 -9
  45. package/dist/generic/graphqlTypeNames.js.map +1 -1
  46. package/dist/generic/interfaces.d.ts +104 -14
  47. package/dist/generic/interfaces.d.ts.map +1 -1
  48. package/dist/generic/interfaces.js +28 -30
  49. package/dist/generic/interfaces.js.map +1 -1
  50. package/dist/generic/libraryInfo.d.ts +1 -1
  51. package/dist/generic/libraryInfo.js +2 -6
  52. package/dist/generic/libraryInfo.js.map +1 -1
  53. package/dist/generic/localCacheManager.d.ts +2 -2
  54. package/dist/generic/localCacheManager.js +44 -48
  55. package/dist/generic/localCacheManager.js.map +1 -1
  56. package/dist/generic/logging.d.ts.map +1 -1
  57. package/dist/generic/logging.js +54 -67
  58. package/dist/generic/logging.js.map +1 -1
  59. package/dist/generic/metadata.d.ts +12 -12
  60. package/dist/generic/metadata.d.ts.map +1 -1
  61. package/dist/generic/metadata.js +21 -25
  62. package/dist/generic/metadata.js.map +1 -1
  63. package/dist/generic/metadataUtil.d.ts +1 -1
  64. package/dist/generic/metadataUtil.js +3 -7
  65. package/dist/generic/metadataUtil.js.map +1 -1
  66. package/dist/generic/providerBase.d.ts +63 -16
  67. package/dist/generic/providerBase.d.ts.map +1 -1
  68. package/dist/generic/providerBase.js +253 -130
  69. package/dist/generic/providerBase.js.map +1 -1
  70. package/dist/generic/queryInfo.d.ts +5 -5
  71. package/dist/generic/queryInfo.js +21 -30
  72. package/dist/generic/queryInfo.js.map +1 -1
  73. package/dist/generic/queryInfoInterfaces.js +1 -2
  74. package/dist/generic/queryInfoInterfaces.js.map +1 -1
  75. package/dist/generic/querySQLFilters.js +5 -10
  76. package/dist/generic/querySQLFilters.js.map +1 -1
  77. package/dist/generic/runQuery.d.ts +2 -2
  78. package/dist/generic/runQuery.js +5 -9
  79. package/dist/generic/runQuery.js.map +1 -1
  80. package/dist/generic/runQuerySQLFilterImplementations.d.ts +1 -1
  81. package/dist/generic/runQuerySQLFilterImplementations.js +4 -8
  82. package/dist/generic/runQuerySQLFilterImplementations.js.map +1 -1
  83. package/dist/generic/runReport.d.ts +2 -2
  84. package/dist/generic/runReport.js +5 -9
  85. package/dist/generic/runReport.js.map +1 -1
  86. package/dist/generic/securityInfo.d.ts +2 -2
  87. package/dist/generic/securityInfo.js +10 -20
  88. package/dist/generic/securityInfo.js.map +1 -1
  89. package/dist/generic/telemetryManager.js +20 -32
  90. package/dist/generic/telemetryManager.js.map +1 -1
  91. package/dist/generic/transactionGroup.d.ts +1 -1
  92. package/dist/generic/transactionGroup.d.ts.map +1 -1
  93. package/dist/generic/transactionGroup.js +11 -19
  94. package/dist/generic/transactionGroup.js.map +1 -1
  95. package/dist/generic/util.js +15 -31
  96. package/dist/generic/util.js.map +1 -1
  97. package/dist/index.d.ts +34 -34
  98. package/dist/index.js +45 -63
  99. package/dist/index.js.map +1 -1
  100. package/dist/views/runView.d.ts +3 -3
  101. package/dist/views/runView.js +6 -11
  102. package/dist/views/runView.js.map +1 -1
  103. package/dist/views/viewInfo.d.ts +3 -3
  104. package/dist/views/viewInfo.js +10 -17
  105. package/dist/views/viewInfo.js.map +1 -1
  106. package/package.json +11 -10
  107. package/readme.md +871 -1271
package/readme.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # @memberjunction/core
2
2
 
3
- The `@memberjunction/core` library provides a comprehensive interface for accessing and managing metadata within MemberJunction, along with facilities for working with entities, applications, and various other aspects central to the MemberJunction ecosystem. This library serves as the foundation for all MemberJunction applications and provides essential functionality for data access, manipulation, and metadata management.
3
+ The `@memberjunction/core` library is the foundational package of the MemberJunction ecosystem. It provides a comprehensive, tier-independent interface for metadata management, entity data access, view and query execution, transaction management, security, and more. All MemberJunction applications -- whether running on the server, in the browser, or via API -- depend on this package.
4
4
 
5
5
  ## Installation
6
6
 
@@ -8,44 +8,39 @@ The `@memberjunction/core` library provides a comprehensive interface for access
8
8
  npm install @memberjunction/core
9
9
  ```
10
10
 
11
- ## Key Features
12
-
13
- - **Metadata-Driven Architecture**: Complete access to MemberJunction metadata including entities, fields, relationships, and permissions
14
- - **Entity Data Access**: Type-safe base classes for loading, saving, and manipulating entity records
15
- - **View Execution**: Powerful view running capabilities for both stored and dynamic views with filtering and pagination
16
- - **Query Execution**: Secure parameterized query execution with SQL injection protection
17
- - **Transaction Management**: Support for grouped database transactions with atomic commits
18
- - **Provider Architecture**: Flexible provider model supporting different execution environments (server, client, API)
19
- - **Bulk Data Loading**: Dataset system for efficient loading of related entity collections
20
- - **Vector Embeddings**: Built-in support for AI-powered text embeddings and similarity search
21
- - **Enhanced Logging**: Structured logging with metadata, categories, and verbose control
22
-
23
- ## Architecture
11
+ ## Architecture Overview
24
12
 
25
13
  ```mermaid
26
14
  flowchart TB
27
15
  subgraph Application["Application Layer"]
28
- App[Your Application]
16
+ style Application fill:#2d6a9f,stroke:#1a4971,color:#fff
17
+ App["Your Application<br/>(Angular, Node.js, etc.)"]
29
18
  end
30
19
 
31
20
  subgraph Core["@memberjunction/core"]
32
- MD[Metadata]
33
- BE[BaseEntity]
34
- RV[RunView]
35
- RQ[RunQuery]
36
- RR[RunReport]
37
- TG[TransactionGroup]
38
- DS[Datasets]
21
+ style Core fill:#2d8659,stroke:#1a5c3a,color:#fff
22
+ MD["Metadata"]
23
+ BE["BaseEntity"]
24
+ RV["RunView"]
25
+ RQ["RunQuery"]
26
+ RR["RunReport"]
27
+ TG["TransactionGroup"]
28
+ DS["Datasets"]
29
+ LC["LocalCacheManager"]
30
+ TM["TelemetryManager"]
31
+ LOG["Logging"]
39
32
  end
40
33
 
41
34
  subgraph Providers["Provider Layer"]
42
- SP[Server Provider]
43
- CP[Client Provider]
35
+ style Providers fill:#7c5295,stroke:#563a6b,color:#fff
36
+ SP["Server Provider<br/>(SQLServerDataProvider)"]
37
+ CP["Client Provider<br/>(GraphQLDataProvider)"]
44
38
  end
45
39
 
46
40
  subgraph Data["Data Layer"]
47
- DB[(Database)]
48
- API[GraphQL API]
41
+ style Data fill:#b8762f,stroke:#8a5722,color:#fff
42
+ DB[("SQL Server<br/>Database")]
43
+ API["GraphQL API"]
49
44
  end
50
45
 
51
46
  App --> MD
@@ -65,108 +60,208 @@ flowchart TB
65
60
  API --> DB
66
61
  ```
67
62
 
68
- ## Overview
63
+ The package uses a **provider model** that allows the same application code to run transparently on different tiers. On the server, a `SQLServerDataProvider` communicates directly with the database. On the client, a `GraphQLDataProvider` routes requests through the GraphQL API. Your code does not need to know which provider is active.
64
+
65
+ ## Key Features
69
66
 
70
- The `@memberjunction/core` library is the central package in the MemberJunction ecosystem, providing:
67
+ - **Metadata-Driven Architecture** -- Complete access to MemberJunction metadata including entities, fields, relationships, and permissions
68
+ - **Entity Data Access** -- Type-safe base classes for loading, saving, and manipulating entity records with dirty tracking and validation
69
+ - **View Execution** -- Powerful view running capabilities for both stored and dynamic views with filtering, pagination, and aggregation
70
+ - **Query Execution** -- Secure parameterized query execution with Nunjucks templates and SQL injection protection
71
+ - **Transaction Management** -- Support for grouped database transactions with atomic commits
72
+ - **Provider Architecture** -- Flexible provider model supporting different execution environments (server, client, API)
73
+ - **Bulk Data Loading** -- Dataset system for efficient loading of related entity collections
74
+ - **Local Caching** -- Intelligent local cache manager with TTL, LRU eviction, and differential updates
75
+ - **Vector Embeddings** -- Built-in support for AI-powered text embeddings and similarity search
76
+ - **Enhanced Logging** -- Structured logging with metadata, categories, severity levels, and verbose control
77
+ - **Telemetry** -- Session-level event tracking for performance monitoring and pattern detection
78
+ - **BaseEngine Pattern** -- Abstract engine base class for building singleton services with automatic data loading
79
+
80
+ ## Module Structure
71
81
 
72
- - **Metadata Management**: Complete access to MemberJunction metadata including entities, fields, relationships, permissions, and more
73
- - **Entity Data Access**: Base classes and utilities for loading, saving, and manipulating entity records
74
- - **View Execution**: Powerful view running capabilities for both stored and dynamic views
75
- - **Query & Report Execution**: Tools for running queries and reports
76
- - **Security & Authorization**: User authentication, role management, and permission handling
77
- - **Transaction Management**: Support for grouped database transactions
78
- - **Provider Architecture**: Flexible provider model supporting different execution environments
82
+ ```mermaid
83
+ flowchart LR
84
+ subgraph index["index.ts (Public API)"]
85
+ style index fill:#64748b,stroke:#475569,color:#fff
86
+ EX["Exports"]
87
+ end
88
+
89
+ subgraph generic["generic/"]
90
+ style generic fill:#2d6a9f,stroke:#1a4971,color:#fff
91
+ metadata["metadata.ts"]
92
+ baseEntity["baseEntity.ts"]
93
+ providerBase["providerBase.ts"]
94
+ entityInfo["entityInfo.ts"]
95
+ securityInfo["securityInfo.ts"]
96
+ interfaces["interfaces.ts"]
97
+ transactionGroup["transactionGroup.ts"]
98
+ baseEngine["baseEngine.ts"]
99
+ compositeKey["compositeKey.ts"]
100
+ logging["logging.ts"]
101
+ runQuery["runQuery.ts"]
102
+ runReport["runReport.ts"]
103
+ localCacheManager["localCacheManager.ts"]
104
+ telemetryManager["telemetryManager.ts"]
105
+ util["util.ts"]
106
+ queryCache["QueryCache.ts"]
107
+ databaseProvider["databaseProviderBase.ts"]
108
+ end
109
+
110
+ subgraph views["views/"]
111
+ style views fill:#2d8659,stroke:#1a5c3a,color:#fff
112
+ runView["runView.ts"]
113
+ viewInfo["viewInfo.ts"]
114
+ end
115
+
116
+ EX --> metadata
117
+ EX --> baseEntity
118
+ EX --> runView
119
+ EX --> providerBase
120
+ EX --> runQuery
121
+ EX --> baseEngine
122
+ ```
123
+
124
+ | File | Purpose |
125
+ |------|---------|
126
+ | `metadata.ts` | Primary entry point for accessing MemberJunction metadata and creating entity objects |
127
+ | `baseEntity.ts` | Foundation class for all entity record manipulation with state tracking and events |
128
+ | `providerBase.ts` | Abstract base class all providers extend, contains caching and refresh logic |
129
+ | `entityInfo.ts` | Entity metadata classes: `EntityInfo`, `EntityFieldInfo`, `EntityRelationshipInfo`, etc. |
130
+ | `securityInfo.ts` | Security classes: `UserInfo`, `RoleInfo`, `AuthorizationInfo`, `AuditLogTypeInfo` |
131
+ | `interfaces.ts` | Core interfaces: `IMetadataProvider`, `IEntityDataProvider`, `IRunViewProvider`, etc. |
132
+ | `compositeKey.ts` | `CompositeKey` and `KeyValuePair` for multi-field primary key support |
133
+ | `transactionGroup.ts` | `TransactionGroupBase` for atomic multi-entity operations |
134
+ | `baseEngine.ts` | `BaseEngine` abstract singleton for building services with auto-loaded data |
135
+ | `runQuery.ts` | `RunQuery` class for secure parameterized query execution |
136
+ | `runReport.ts` | `RunReport` class for report generation |
137
+ | `logging.ts` | `LogStatus`, `LogError`, `LogStatusEx`, `LogErrorEx`, verbose controls |
138
+ | `localCacheManager.ts` | `LocalCacheManager` for client-side caching with TTL and LRU eviction |
139
+ | `telemetryManager.ts` | `TelemetryManager` for operation tracking and pattern detection |
140
+ | `queryCache.ts` / `QueryCacheConfig.ts` | LRU query result cache with TTL support |
141
+ | `databaseProviderBase.ts` | `DatabaseProviderBase` for server-side SQL execution and transactions |
142
+ | `util.ts` | Utility functions: `TypeScriptTypeFromSQLType`, `FormatValue`, `CodeNameFromString` |
143
+ | `runView.ts` | `RunView` class and `RunViewParams` for executing stored and dynamic views |
144
+ | `viewInfo.ts` | View metadata classes: `ViewInfo`, `ViewColumnInfo`, `ViewFilterInfo` |
145
+
146
+ ---
79
147
 
80
148
  ## Core Components
81
149
 
82
- ### Metadata Class
150
+ ### Metadata
83
151
 
84
- The `Metadata` class is the primary entry point for accessing MemberJunction metadata and instantiating entity objects.
152
+ The `Metadata` class is the primary entry point for accessing MemberJunction metadata and instantiating entity objects. It delegates to a provider set at application startup.
85
153
 
86
154
  ```typescript
87
155
  import { Metadata } from '@memberjunction/core';
88
156
 
89
- // Create metadata instance
90
157
  const md = new Metadata();
91
158
 
92
159
  // Refresh cached metadata
93
160
  await md.Refresh();
94
161
 
95
- // Access various metadata collections
96
- const applications = md.Applications;
97
- const entities = md.Entities;
98
- const currentUser = md.CurrentUser;
99
- const roles = md.Roles;
100
- const authorizations = md.Authorizations;
162
+ // Access metadata collections
163
+ const entities = md.Entities; // EntityInfo[]
164
+ const applications = md.Applications; // ApplicationInfo[]
165
+ const currentUser = md.CurrentUser; // UserInfo
166
+ const roles = md.Roles; // RoleInfo[]
167
+ const queries = md.Queries; // QueryInfo[]
101
168
  ```
102
169
 
103
- #### Key Metadata Properties
104
-
105
- - `Applications`: Array of all applications in the system
106
- - `Entities`: Array of all entity definitions
107
- - `CurrentUser`: Current authenticated user (when available)
108
- - `Roles`: System roles
109
- - `AuditLogTypes`: Available audit log types
110
- - `Authorizations`: Authorization definitions
111
- - `Libraries`: Registered libraries
112
- - `Queries`: Query definitions
113
- - `QueryFields`: Query field metadata
114
- - `QueryCategories`: Query categorization
115
- - `QueryPermissions`: Query-level permissions
116
- - `VisibleExplorerNavigationItems`: Navigation items visible to current user
117
- - `AllExplorerNavigationItems`: All navigation items (including hidden)
170
+ #### Metadata Properties
171
+
172
+ | Property | Type | Description |
173
+ |----------|------|-------------|
174
+ | `Applications` | `ApplicationInfo[]` | All applications in the system |
175
+ | `Entities` | `EntityInfo[]` | All entity definitions with fields, relationships, permissions |
176
+ | `CurrentUser` | `UserInfo` | Current authenticated user (client-side only) |
177
+ | `Roles` | `RoleInfo[]` | System roles |
178
+ | `AuditLogTypes` | `AuditLogTypeInfo[]` | Available audit log types |
179
+ | `Authorizations` | `AuthorizationInfo[]` | Authorization definitions |
180
+ | `Libraries` | `LibraryInfo[]` | Registered libraries |
181
+ | `Queries` | `QueryInfo[]` | Query definitions |
182
+ | `QueryFields` | `QueryFieldInfo[]` | Query field metadata |
183
+ | `QueryCategories` | `QueryCategoryInfo[]` | Query categorization |
184
+ | `QueryPermissions` | `QueryPermissionInfo[]` | Query-level permissions |
185
+ | `VisibleExplorerNavigationItems` | `ExplorerNavigationItem[]` | Navigation items visible to the current user |
186
+ | `AllExplorerNavigationItems` | `ExplorerNavigationItem[]` | All navigation items (including hidden) |
187
+ | `ProviderType` | `'Database' \| 'Network'` | Whether the active provider connects directly to DB or via network |
188
+ | `LocalStorageProvider` | `ILocalStorageProvider` | Persistent local storage (IndexedDB, file, memory) |
118
189
 
119
190
  #### Helper Methods
120
191
 
121
192
  ```typescript
122
- // Get entity ID from name
193
+ // Look up entities by name or ID
123
194
  const entityId = md.EntityIDFromName('Users');
124
-
125
- // Get entity name from ID
126
195
  const entityName = md.EntityNameFromID('12345');
196
+ const entityInfo = md.EntityByName('users'); // case-insensitive
197
+ const entity = md.EntityByID('12345');
127
198
 
128
- // Find entity by name (case-insensitive)
129
- const userEntity = md.EntityByName('users');
199
+ // Record operations
200
+ const name = await md.GetEntityRecordName('Users', compositeKey);
201
+ const names = await md.GetEntityRecordNames(infoArray);
202
+ const isFavorite = await md.GetRecordFavoriteStatus(userId, 'Orders', key);
203
+ await md.SetRecordFavoriteStatus(userId, 'Orders', key, true);
130
204
 
131
- // Find entity by ID
132
- const entity = md.EntityByID('12345');
205
+ // Dependencies and duplicates
206
+ const deps = await md.GetRecordDependencies('Orders', primaryKey);
207
+ const entityDeps = await md.GetEntityDependencies('Orders');
208
+ const dupes = await md.GetRecordDuplicates(duplicateRequest);
133
209
 
134
- // Get entity object instance (IMPORTANT: Always use this pattern)
135
- const user = await md.GetEntityObject<UserEntity>('Users');
210
+ // Record merging
211
+ const mergeResult = await md.MergeRecords(mergeRequest);
136
212
 
137
- // NEW in v2.58.0: Load existing records directly with GetEntityObject
138
- const existingUser = await md.GetEntityObject<UserEntity>('Users', CompositeKey.FromID(userId));
213
+ // Record change history (built-in version control)
214
+ const changes = await md.GetRecordChanges<RecordChangeEntity>('Users', primaryKey);
215
+
216
+ // Transactions
217
+ const txGroup = await md.CreateTransactionGroup();
139
218
  ```
140
219
 
141
- ### GetEntityObject() - Enhanced in v2.58.0
220
+ ### GetEntityObject()
221
+
222
+ `GetEntityObject<T>()` is the correct way to create entity instances. It uses the MemberJunction class factory to ensure the proper subclass is instantiated and supports two overloads.
223
+
224
+ ```mermaid
225
+ flowchart LR
226
+ subgraph Creation["Entity Creation Flow"]
227
+ style Creation fill:#2d6a9f,stroke:#1a4971,color:#fff
228
+ GEO["GetEntityObject()"] --> CF["ClassFactory<br/>Lookup"]
229
+ CF --> SC["Subclass<br/>Instantiation"]
230
+ SC --> NR["NewRecord()<br/>(auto-called)"]
231
+ NR --> READY["Entity Ready"]
232
+ end
142
233
 
143
- The `GetEntityObject()` method now supports overloaded signatures for both creating new records and loading existing ones in a single call:
234
+ subgraph Loading["Entity Loading Flow"]
235
+ style Loading fill:#2d8659,stroke:#1a5c3a,color:#fff
236
+ GEO2["GetEntityObject()<br/>with CompositeKey"] --> CF2["ClassFactory<br/>Lookup"]
237
+ CF2 --> SC2["Subclass<br/>Instantiation"]
238
+ SC2 --> LD["Load()<br/>from Database"]
239
+ LD --> READY2["Entity Ready<br/>(with data)"]
240
+ end
241
+ ```
144
242
 
145
243
  #### Creating New Records
146
244
 
147
245
  ```typescript
148
- // Traditional approach - still supported
149
- const user = await md.GetEntityObject<UserEntity>('Users');
150
- user.NewRecord(); // No longer needed - done automatically!
151
-
152
- // v2.58.0+ - NewRecord() is called automatically
153
- const newUser = await md.GetEntityObject<UserEntity>('Users');
154
- newUser.FirstName = 'John';
155
- await newUser.Save();
246
+ // NewRecord() is called automatically
247
+ const customer = await md.GetEntityObject<CustomerEntity>('Customers');
248
+ customer.Name = 'Acme Corp';
249
+ await customer.Save();
250
+
251
+ // Server-side with context user
252
+ const order = await md.GetEntityObject<OrderEntity>('Orders', contextUser);
156
253
  ```
157
254
 
158
- #### Loading Existing Records (NEW)
255
+ #### Loading Existing Records
159
256
 
160
257
  ```typescript
161
- // Traditional approach - still supported
162
- const user = await md.GetEntityObject<UserEntity>('Users');
163
- await user.Load(userId);
258
+ import { CompositeKey } from '@memberjunction/core';
164
259
 
165
- // v2.58.0+ - Load in a single call using CompositeKey
166
- const existingUser = await md.GetEntityObject<UserEntity>('Users', CompositeKey.FromID(userId));
260
+ // Load by ID (most common)
261
+ const user = await md.GetEntityObject<UserEntity>('Users', CompositeKey.FromID(userId));
167
262
 
168
- // Load by custom field
169
- const userByEmail = await md.GetEntityObject<UserEntity>('Users',
263
+ // Load by named field
264
+ const userByEmail = await md.GetEntityObject<UserEntity>('Users',
170
265
  CompositeKey.FromKeyValuePair('Email', 'user@example.com'));
171
266
 
172
267
  // Load with composite primary key
@@ -177,388 +272,219 @@ const orderItem = await md.GetEntityObject<OrderItemEntity>('OrderItems',
177
272
  ]));
178
273
 
179
274
  // Server-side with context user
180
- const order = await md.GetEntityObject<OrderEntity>('Orders',
275
+ const order = await md.GetEntityObject<OrderEntity>('Orders',
181
276
  CompositeKey.FromID(orderId), contextUser);
182
277
  ```
183
278
 
184
- #### Automatic UUID Generation (NEW in v2.58.0)
279
+ ### BaseEntity
185
280
 
186
- For entities with non-auto-increment uniqueidentifier primary keys, UUIDs are now automatically generated when calling `NewRecord()`:
187
-
188
- ```typescript
189
- // UUID is automatically generated for eligible entities
190
- const action = await md.GetEntityObject<ActionEntity>('Actions');
191
- console.log(action.ID); // '550e8400-e29b-41d4-a716-446655440000' (auto-generated)
192
- ```
281
+ The `BaseEntity` class is the foundation for all entity record manipulation. All entity classes generated by CodeGen extend it.
193
282
 
194
- ### BaseEntity Class
195
-
196
- The `BaseEntity` class is the foundation for all entity record manipulation in MemberJunction. All entity classes generated by the MemberJunction code generator extend this class.
197
-
198
- #### Loading Data from Objects (v2.52.0+)
199
-
200
- The `LoadFromData()` method is now async to support subclasses that need to perform additional loading operations:
201
-
202
- ```typescript
203
- // Loading data from a plain object (now async)
204
- const userData = {
205
- ID: '123',
206
- FirstName: 'Jane',
207
- LastName: 'Doe',
208
- Email: 'jane@example.com'
209
- };
210
-
211
- // LoadFromData is now async as of v2.52.0
212
- await user.LoadFromData(userData);
213
-
214
- // This change enables subclasses to perform async operations
215
- // Example subclass implementation:
216
- class ExtendedUserEntity extends UserEntity {
217
- public override async LoadFromData(data: any): Promise<boolean> {
218
- const result = await super.LoadFromData(data);
219
- if (result) {
220
- // Can now perform async operations
221
- await this.LoadUserPreferences();
222
- await this.LoadUserRoles();
223
- }
224
- return result;
225
- }
226
-
227
- // Important: Also override Load() for consistency
228
- public override async Load(ID: string): Promise<boolean> {
229
- const result = await super.Load(ID);
230
- if (result) {
231
- await this.LoadUserPreferences();
232
- await this.LoadUserRoles();
233
- }
234
- return result;
235
- }
236
- }
237
- ```
238
-
239
- **Important**: Subclasses that perform additional loading should override BOTH `LoadFromData()` and `Load()` methods to ensure consistent behavior regardless of how the entity is populated.
240
-
241
- #### Entity Fields
242
-
243
- Each entity field is represented by an `EntityField` object that tracks value, dirty state, and metadata:
283
+ #### Field Access
244
284
 
245
285
  ```typescript
246
- // Access field value
247
- const firstName = user.Get('FirstName');
286
+ // Type-safe property access (via generated getters/setters)
287
+ const name = user.FirstName;
288
+ user.FirstName = 'Jane';
248
289
 
249
- // Set field value
290
+ // Dynamic field access
291
+ const value = user.Get('FirstName');
250
292
  user.Set('FirstName', 'Jane');
251
293
 
252
- // Check if field is dirty
253
- const isDirty = user.Fields.find(f => f.Name === 'FirstName').Dirty;
254
-
255
- // Access field metadata
294
+ // Field metadata
256
295
  const field = user.Fields.find(f => f.Name === 'Email');
257
- console.log(field.IsUnique); // true/false
258
- console.log(field.IsPrimaryKey); // true/false
259
- console.log(field.ReadOnly); // true/false
296
+ console.log(field.Dirty); // Has the value changed?
297
+ console.log(field.IsUnique); // Unique constraint?
298
+ console.log(field.IsPrimaryKey); // Primary key?
299
+ console.log(field.ReadOnly); // Read-only field?
260
300
  ```
261
301
 
262
- #### Working with BaseEntity and Spread Operator
263
-
264
- **IMPORTANT**: BaseEntity uses TypeScript getter/setter properties for all entity fields. This means the JavaScript spread operator (`...`) will NOT capture entity field values because getters are not enumerable properties.
265
-
266
- ```typescript
267
- // ❌ WRONG - Spread operator doesn't capture getter properties
268
- const userData = {
269
- ...userEntity, // This will NOT include ID, FirstName, LastName, etc.
270
- customField: 'value'
271
- };
272
-
273
- // ✅ CORRECT - Use GetAll() to get plain object with all field values
274
- const userData = {
275
- ...userEntity.GetAll(), // Returns { ID: '...', FirstName: '...', LastName: '...', etc. }
276
- customField: 'value'
277
- };
278
-
279
- // ✅ ALSO CORRECT - Access properties individually
280
- const userData = {
281
- ID: userEntity.ID,
282
- FirstName: userEntity.FirstName,
283
- LastName: userEntity.LastName,
284
- customField: 'value'
285
- };
286
- ```
287
-
288
- The `GetAll()` method returns a plain JavaScript object containing all entity field values, which can be safely used with the spread operator. This design choice enables:
289
- - Clean property access syntax (`entity.Name` vs `entity.getName()`)
290
- - Full TypeScript/IntelliSense support
291
- - Easy property overriding in subclasses
292
- - Proper encapsulation with validation and side effects
293
-
294
- #### Save Options
302
+ #### Save and Delete
295
303
 
296
304
  ```typescript
297
305
  import { EntitySaveOptions } from '@memberjunction/core';
298
306
 
299
- const options = new EntitySaveOptions();
300
- options.IgnoreDirtyState = true; // Force save even if no changes detected
301
- options.SkipEntityAIActions = true; // Skip AI-related actions
302
- options.SkipEntityActions = true; // Skip entity actions
303
- options.SkipOldValuesCheck = true; // Skip concurrency check (client-side only)
307
+ // Simple save
308
+ const success = await entity.Save();
304
309
 
310
+ // Save with options
311
+ const options = new EntitySaveOptions();
312
+ options.IgnoreDirtyState = true; // Force save even if no changes detected
313
+ options.SkipEntityAIActions = true; // Skip AI-related actions
314
+ options.SkipEntityActions = true; // Skip entity actions
305
315
  await entity.Save(options);
306
- ```
307
-
308
- ### Entity State Tracking & Events (v2.131.0+)
309
316
 
310
- BaseEntity provides comprehensive state tracking and event notification for all database operations. This enables UI components to show loading indicators, disable buttons during operations, and react to entity lifecycle changes.
317
+ // Delete
318
+ await entity.Delete();
319
+ ```
311
320
 
312
- #### Operation State Getters
321
+ #### GetAll() for Spread Operator
313
322
 
314
- Check if an entity is currently performing a database operation:
323
+ BaseEntity uses getter/setter properties, so the spread operator will not capture field values. Use `GetAll()` instead.
315
324
 
316
325
  ```typescript
317
- const user = await md.GetEntityObject<UserEntity>('Users');
318
-
319
- // Check individual operation states
320
- if (user.IsSaving) {
321
- console.log('Save operation in progress...');
322
- }
326
+ // WRONG -- spread ignores getter properties
327
+ const data = { ...entity };
323
328
 
324
- if (user.IsDeleting) {
325
- console.log('Delete operation in progress...');
326
- }
327
-
328
- if (user.IsLoading) {
329
- console.log('Load operation in progress...');
330
- }
331
-
332
- // Convenience getter - true if ANY operation is in progress
333
- if (user.IsBusy) {
334
- disableAllButtons();
335
- showSpinner();
336
- }
329
+ // CORRECT -- GetAll() returns a plain object with all field values
330
+ const data = { ...entity.GetAll(), customField: 'value' };
337
331
  ```
338
332
 
339
- #### State Getter Reference
340
-
341
- | Getter | Description |
342
- |--------|-------------|
343
- | `IsSaving` | Returns `true` when a `Save()` operation is in progress |
344
- | `IsDeleting` | Returns `true` when a `Delete()` operation is in progress |
345
- | `IsLoading` | Returns `true` when a `Load()` operation is in progress |
346
- | `IsBusy` | Returns `true` when any of the above operations is in progress |
347
-
348
- #### Operation Lifecycle Events
333
+ #### State Tracking and Events
349
334
 
350
- Subscribe to entity lifecycle events to react when operations start and complete:
335
+ BaseEntity provides comprehensive state tracking and lifecycle events.
351
336
 
352
337
  ```typescript
353
338
  import { BaseEntityEvent } from '@memberjunction/core';
354
339
 
355
- const user = await md.GetEntityObject<UserEntity>('Users');
340
+ // Check operation states
341
+ if (entity.IsSaving) { /* Save in progress */ }
342
+ if (entity.IsDeleting) { /* Delete in progress */ }
343
+ if (entity.IsLoading) { /* Load in progress */ }
344
+ if (entity.IsBusy) { /* Any operation in progress */ }
356
345
 
357
- // Subscribe to entity events
358
- const subscription = user.RegisterEventHandler((event: BaseEntityEvent) => {
346
+ // Subscribe to lifecycle events
347
+ const subscription = entity.RegisterEventHandler((event: BaseEntityEvent) => {
359
348
  switch (event.type) {
360
- // Operation START events
361
349
  case 'save_started':
362
350
  console.log(`Save started (${event.saveSubType})`); // 'create' or 'update'
363
- showSavingIndicator();
351
+ break;
352
+ case 'save':
353
+ console.log('Save completed');
364
354
  break;
365
355
  case 'delete_started':
366
356
  console.log('Delete started');
367
- showDeletingIndicator();
368
- break;
369
- case 'load_started':
370
- console.log('Load started for key:', event.payload?.CompositeKey);
371
- showLoadingIndicator();
372
- break;
373
-
374
- // Operation COMPLETE events
375
- case 'save':
376
- console.log(`Save completed (${event.saveSubType})`);
377
- hideSavingIndicator();
378
- showSuccessMessage();
379
357
  break;
380
358
  case 'delete':
381
359
  console.log('Delete completed, old values:', event.payload?.OldValues);
382
- hideDeletingIndicator();
360
+ break;
361
+ case 'load_started':
362
+ console.log('Load started for key:', event.payload?.CompositeKey);
383
363
  break;
384
364
  case 'load_complete':
385
- console.log('Load completed for key:', event.payload?.CompositeKey);
386
- hideLoadingIndicator();
365
+ console.log('Load completed');
387
366
  break;
388
-
389
- // Other events
390
367
  case 'new_record':
391
- console.log('NewRecord() was called');
368
+ console.log('NewRecord() called');
392
369
  break;
393
370
  }
394
371
  });
395
372
 
396
- // Later: unsubscribe when done
373
+ // Unsubscribe when done
397
374
  subscription.unsubscribe();
398
375
  ```
399
376
 
400
- #### Event Type Reference
401
-
402
- | Event Type | Description | Payload |
403
- |------------|-------------|---------|
404
- | `save_started` | Raised when Save() begins (only if entity will actually save) | `null` |
405
- | `save` | Raised when Save() completes successfully | `null` |
406
- | `delete_started` | Raised when Delete() begins | `null` |
407
- | `delete` | Raised when Delete() completes successfully | `{ OldValues: object }` |
408
- | `load_started` | Raised when Load() begins | `{ CompositeKey: CompositeKey }` |
409
- | `load_complete` | Raised when Load() completes successfully | `{ CompositeKey: CompositeKey }` |
410
- | `new_record` | Raised when NewRecord() is called | `null` |
411
- | `transaction_ready` | Internal: signals transaction preprocessing complete | `null` |
412
-
413
- #### Global Event Subscription
414
-
415
- Subscribe to entity events globally via MJGlobal to monitor all entity operations across your application:
377
+ #### Awaiting In-Progress Operations
416
378
 
417
379
  ```typescript
418
- import { MJGlobal, MJEventType, BaseEntity, BaseEntityEvent } from '@memberjunction/core';
419
-
420
- // Subscribe to all entity events globally
421
- MJGlobal.Instance.GetEventListener(true).subscribe((event) => {
422
- if (event.event === MJEventType.ComponentEvent &&
423
- event.eventCode === BaseEntity.BaseEventCode) {
424
-
425
- const entityEvent = event.args as BaseEntityEvent;
426
- const entity = entityEvent.baseEntity;
427
-
428
- console.log(`[${entity.EntityInfo.Name}] ${entityEvent.type}`);
429
-
430
- // Global logging, analytics, or audit trail
431
- if (entityEvent.type === 'save') {
432
- trackEntitySave(entity.EntityInfo.Name, entityEvent.saveSubType);
433
- }
434
- }
435
- });
380
+ // Wait for an in-progress save to complete before proceeding
381
+ await entity.EnsureSaveComplete();
382
+ await entity.EnsureDeleteComplete();
383
+ await entity.EnsureLoadComplete();
436
384
  ```
437
385
 
438
386
  #### Save Debouncing
439
387
 
440
- Multiple rapid calls to `Save()` are automatically debounced - the second call receives the same result as the first:
388
+ Multiple rapid calls to `Save()` or `Delete()` are automatically debounced -- the second call receives the same result as the first.
441
389
 
442
390
  ```typescript
443
- // These two calls result in only ONE database save
444
391
  const promise1 = entity.Save();
445
- const promise2 = entity.Save(); // Returns same promise, doesn't trigger new save
446
-
392
+ const promise2 = entity.Save(); // Returns same promise, no duplicate save
447
393
  const [result1, result2] = await Promise.all([promise1, promise2]);
448
- // result1 === result2 (both get the same result from the single save)
394
+ // result1 === result2
449
395
  ```
450
396
 
451
- The same debouncing behavior applies to `Delete()` operations.
452
-
453
- #### Awaiting In-Progress Operations
397
+ #### Global Event Subscription
454
398
 
455
- BaseEntity provides built-in methods to wait for any in-progress operation to complete. These methods return immediately if no operation is in progress, or wait for the completion event if one is:
399
+ Monitor all entity operations across the application.
456
400
 
457
401
  ```typescript
458
- // Wait for an in-progress save to complete
459
- await entity.EnsureSaveComplete();
460
-
461
- // Wait for an in-progress delete to complete
462
- await entity.EnsureDeleteComplete();
402
+ import { MJGlobal, MJEventType, BaseEntity, BaseEntityEvent } from '@memberjunction/core';
463
403
 
464
- // Wait for an in-progress load to complete
465
- await entity.EnsureLoadComplete();
404
+ MJGlobal.Instance.GetEventListener(true).subscribe((event) => {
405
+ if (event.event === MJEventType.ComponentEvent &&
406
+ event.eventCode === BaseEntity.BaseEventCode) {
407
+ const entityEvent = event.args as BaseEntityEvent;
408
+ console.log(`[${entityEvent.baseEntity.EntityInfo.Name}] ${entityEvent.type}`);
409
+ }
410
+ });
466
411
  ```
467
412
 
468
- **Method Reference:**
469
-
470
- | Method | Waits For | Event Listened |
471
- |--------|-----------|----------------|
472
- | `EnsureSaveComplete()` | `IsSaving` to become false | `save` |
473
- | `EnsureDeleteComplete()` | `IsDeleting` to become false | `delete` |
474
- | `EnsureLoadComplete()` | `IsLoading` to become false | `load_complete` |
475
-
476
- **Example: Coordinating Dependent Operations**
413
+ #### Validation
477
414
 
478
415
  ```typescript
479
- async function performDependentOperation(entity: BaseEntity) {
480
- // Ensure any in-progress save is complete before proceeding
481
- await entity.EnsureSaveComplete();
482
-
483
- // Now safe to perform operations that depend on the saved state
484
- console.log('Entity is saved, proceeding with dependent operation');
485
- await someOperationThatNeedsSavedData(entity);
416
+ const result = entity.Validate();
417
+ if (!result.Success) {
418
+ for (const error of result.Errors) {
419
+ console.error(`${error.Source}: ${error.Message}`);
420
+ }
486
421
  }
487
422
  ```
488
423
 
489
- **Example: Cleanup After Delete**
424
+ ### CompositeKey
490
425
 
491
- ```typescript
492
- async function deleteAndNavigate(entity: BaseEntity) {
493
- entity.Delete(); // Fire and forget the delete
426
+ The `CompositeKey` class provides flexible primary key representation supporting both single and multi-field primary keys.
494
427
 
495
- // Wait for the delete to complete before navigating
496
- await entity.EnsureDeleteComplete();
428
+ ```typescript
429
+ import { CompositeKey, KeyValuePair } from '@memberjunction/core';
497
430
 
498
- // Now safe to navigate away
499
- navigateToList();
500
- }
501
- ```
431
+ // Single ID field
432
+ const key = CompositeKey.FromID('abc-123');
502
433
 
503
- These methods are useful when:
504
- - You need to ensure data is persisted before performing a dependent operation
505
- - You're coordinating between multiple components that might trigger operations
506
- - You want to avoid race conditions when chaining operations
507
- - You need to perform cleanup after an operation completes
434
+ // Named single field
435
+ const key2 = CompositeKey.FromKeyValuePair('Email', 'user@example.com');
508
436
 
509
- #### UI Integration Example
437
+ // Composite key
438
+ const key3 = CompositeKey.FromKeyValuePairs([
439
+ { FieldName: 'OrderID', Value: orderId },
440
+ { FieldName: 'ProductID', Value: productId }
441
+ ]);
510
442
 
511
- A complete example showing state tracking in a UI component:
443
+ // Key operations
444
+ const value = key.GetValueByFieldName('ID');
445
+ const str = key.ToString(); // "ID=abc-123"
446
+ const concat = key.ToConcatenatedString(); // "abc-123"
447
+ const valid = key.Validate(); // { IsValid: boolean, ErrorMessage: string }
448
+ ```
512
449
 
513
- ```typescript
514
- class EntityFormComponent {
515
- private entity: BaseEntity;
516
- private subscription: Subscription;
450
+ ---
517
451
 
518
- async loadEntity(id: string) {
519
- this.entity = await md.GetEntityObject<UserEntity>('Users');
452
+ ### RunView
520
453
 
521
- // Set up event handlers
522
- this.subscription = this.entity.RegisterEventHandler((event) => {
523
- this.updateUI(event);
524
- });
454
+ The `RunView` class provides powerful view execution capabilities for both stored and dynamic queries.
525
455
 
526
- await this.entity.Load(id);
527
- }
456
+ ```mermaid
457
+ flowchart LR
458
+ subgraph Params["RunViewParams"]
459
+ style Params fill:#2d6a9f,stroke:#1a4971,color:#fff
460
+ SV["Stored View<br/>(ViewID/ViewName)"]
461
+ DV["Dynamic View<br/>(EntityName + Filter)"]
462
+ end
528
463
 
529
- updateUI(event: BaseEntityEvent) {
530
- // Update loading states
531
- this.saveButton.disabled = this.entity.IsBusy;
532
- this.deleteButton.disabled = this.entity.IsBusy;
533
-
534
- // Show operation-specific indicators
535
- this.savingSpinner.visible = this.entity.IsSaving;
536
- this.deletingSpinner.visible = this.entity.IsDeleting;
537
- this.loadingSpinner.visible = this.entity.IsLoading;
538
-
539
- // React to completion events
540
- if (event.type === 'save') {
541
- this.showToast('Saved successfully!');
542
- } else if (event.type === 'delete') {
543
- this.navigateToList();
544
- }
545
- }
464
+ subgraph RunView["RunView"]
465
+ style RunView fill:#2d8659,stroke:#1a5c3a,color:#fff
466
+ RV["RunView()"]
467
+ RVS["RunViews()"]
468
+ end
546
469
 
547
- async save() {
548
- // IsBusy automatically becomes true during save
549
- await this.entity.Save();
550
- // IsBusy automatically becomes false when done
551
- }
470
+ subgraph Result["RunViewResult"]
471
+ style Result fill:#b8762f,stroke:#8a5722,color:#fff
472
+ S["Success"]
473
+ R["Results[]"]
474
+ TC["TotalRowCount"]
475
+ AG["AggregateResults"]
476
+ end
552
477
 
553
- destroy() {
554
- this.subscription?.unsubscribe();
555
- }
556
- }
478
+ SV --> RV
479
+ DV --> RV
480
+ DV --> RVS
481
+ RV --> S
482
+ RV --> R
483
+ RVS --> S
484
+ RV --> AG
557
485
  ```
558
486
 
559
- ### RunView Class
560
-
561
- The `RunView` class provides powerful view execution capabilities for both stored views and dynamic queries.
487
+ #### Basic Usage
562
488
 
563
489
  ```typescript
564
490
  import { RunView, RunViewParams } from '@memberjunction/core';
@@ -566,50 +492,122 @@ import { RunView, RunViewParams } from '@memberjunction/core';
566
492
  const rv = new RunView();
567
493
 
568
494
  // Run a stored view by name
569
- const params: RunViewParams = {
495
+ const result = await rv.RunView({
570
496
  ViewName: 'Active Users',
571
- ExtraFilter: 'CreatedDate > \'2024-01-01\'',
572
- UserSearchString: 'john'
573
- };
574
-
575
- const results = await rv.RunView(params);
497
+ ExtraFilter: "CreatedDate > '2024-01-01'"
498
+ });
576
499
 
577
500
  // Run a dynamic view with entity objects returned
578
- const dynamicResults = await rv.RunView<UserEntity>({
501
+ const typedResult = await rv.RunView<UserEntity>({
579
502
  EntityName: 'Users',
580
503
  ExtraFilter: 'IsActive = 1',
581
504
  OrderBy: 'LastName ASC, FirstName ASC',
582
505
  Fields: ['ID', 'FirstName', 'LastName', 'Email'],
583
- ResultType: 'entity_object' // Returns actual entity objects
506
+ ResultType: 'entity_object'
507
+ });
508
+
509
+ // Access results
510
+ if (typedResult.Success) {
511
+ const users = typedResult.Results; // UserEntity[]
512
+ console.log(`Found ${users.length} users`);
513
+ }
514
+ ```
515
+
516
+ #### Batch Multiple Views
517
+
518
+ Use `RunViews` (plural) to execute multiple independent queries in a single operation.
519
+
520
+ ```typescript
521
+ const [users, roles, permissions] = await rv.RunViews([
522
+ {
523
+ EntityName: 'Users',
524
+ ExtraFilter: 'IsActive = 1',
525
+ ResultType: 'entity_object'
526
+ },
527
+ {
528
+ EntityName: 'Roles',
529
+ OrderBy: 'Name',
530
+ ResultType: 'entity_object'
531
+ },
532
+ {
533
+ EntityName: 'Entity Permissions',
534
+ ResultType: 'simple'
535
+ }
536
+ ]);
537
+ ```
538
+
539
+ #### Aggregates
540
+
541
+ Request aggregate calculations that run in parallel with the main query, unaffected by pagination.
542
+
543
+ ```typescript
544
+ const result = await rv.RunView<OrderEntity>({
545
+ EntityName: 'Orders',
546
+ ExtraFilter: "Status = 'Completed'",
547
+ MaxRows: 50,
548
+ Aggregates: [
549
+ { expression: 'SUM(TotalAmount)', alias: 'TotalRevenue' },
550
+ { expression: 'COUNT(*)', alias: 'OrderCount' },
551
+ { expression: 'AVG(TotalAmount)', alias: 'AverageOrder' }
552
+ ]
584
553
  });
585
554
 
586
- // Access typed results
587
- const users = dynamicResults.Results; // Properly typed as UserEntity[]
555
+ // Aggregate results are in result.AggregateResults[]
588
556
  ```
589
557
 
590
- #### RunView Parameters
558
+ #### ResultType and Fields Optimization
591
559
 
592
- - `ViewID`: ID of stored view to run
593
- - `ViewName`: Name of stored view to run
594
- - `ViewEntity`: Pre-loaded view entity object (optimal for performance)
595
- - `EntityName`: Entity name for dynamic views
596
- - `ExtraFilter`: Additional SQL WHERE clause
597
- - `OrderBy`: SQL ORDER BY clause (overrides stored view sorting)
598
- - `Fields`: Array of field names to return
599
- - `UserSearchString`: User search term
600
- - `ExcludeUserViewRunID`: Exclude records from specific prior run
601
- - `ExcludeDataFromAllPriorViewRuns`: Exclude all previously returned records
602
- - `SaveViewResults`: Store run results for future exclusion
603
- - `IgnoreMaxRows`: Bypass entity MaxRows setting
604
- - `MaxRows`: Maximum rows to return
605
- - `StartRow`: Row offset for pagination
606
- - `ResultType`: 'simple' (default) or 'entity_object'
560
+ ```typescript
561
+ // entity_object -- full BaseEntity objects for mutation (Fields is ignored)
562
+ const mutableResult = await rv.RunView<UserEntity>({
563
+ EntityName: 'Users',
564
+ ResultType: 'entity_object'
565
+ });
607
566
 
608
- ### RunQuery Class
567
+ // simple -- plain JavaScript objects for read-only use (use Fields for performance)
568
+ const readOnlyResult = await rv.RunView<{ ID: string; Name: string }>({
569
+ EntityName: 'Users',
570
+ Fields: ['ID', 'Name'],
571
+ ResultType: 'simple'
572
+ });
609
573
 
610
- The `RunQuery` class provides secure execution of parameterized stored queries with advanced SQL injection protection and type-safe parameter handling.
574
+ // count_only -- returns only TotalRowCount, no rows
575
+ const countResult = await rv.RunView({
576
+ EntityName: 'Users',
577
+ ExtraFilter: 'IsActive = 1',
578
+ ResultType: 'count_only'
579
+ });
580
+ ```
611
581
 
612
- #### Basic Usage
582
+ #### RunViewParams Reference
583
+
584
+ | Parameter | Type | Description |
585
+ |-----------|------|-------------|
586
+ | `ViewID` | `string` | ID of stored view to run |
587
+ | `ViewName` | `string` | Name of stored view to run |
588
+ | `ViewEntity` | `BaseEntity` | Pre-loaded view entity (for performance) |
589
+ | `EntityName` | `string` | Entity name for dynamic views |
590
+ | `ExtraFilter` | `string` | Additional SQL WHERE clause |
591
+ | `OrderBy` | `string` | SQL ORDER BY clause |
592
+ | `Fields` | `string[]` | Field names to return (simple mode only) |
593
+ | `UserSearchString` | `string` | User search term |
594
+ | `MaxRows` | `number` | Maximum rows to return |
595
+ | `StartRow` | `number` | Row offset for pagination |
596
+ | `ResultType` | `'simple' \| 'entity_object' \| 'count_only'` | Result format |
597
+ | `IgnoreMaxRows` | `boolean` | Bypass entity MaxRows setting |
598
+ | `SaveViewResults` | `boolean` | Store run results for future exclusion |
599
+ | `ExcludeUserViewRunID` | `string` | Exclude records from a specific prior run |
600
+ | `ExcludeDataFromAllPriorViewRuns` | `boolean` | Exclude all previously returned records |
601
+ | `ForceAuditLog` | `boolean` | Force audit log entry |
602
+ | `CacheLocal` | `boolean` | Use LocalCacheManager for caching |
603
+ | `CacheLocalTTL` | `number` | Cache TTL in milliseconds |
604
+ | `Aggregates` | `AggregateExpression[]` | Aggregate expressions to compute |
605
+
606
+ ---
607
+
608
+ ### RunQuery
609
+
610
+ The `RunQuery` class provides secure execution of parameterized stored queries with Nunjucks templates and SQL injection protection.
613
611
 
614
612
  ```typescript
615
613
  import { RunQuery, RunQueryParams } from '@memberjunction/core';
@@ -617,1098 +615,700 @@ import { RunQuery, RunQueryParams } from '@memberjunction/core';
617
615
  const rq = new RunQuery();
618
616
 
619
617
  // Execute by Query ID
620
- const params: RunQueryParams = {
618
+ const result = await rq.RunQuery({
621
619
  QueryID: '12345',
622
620
  Parameters: {
623
621
  StartDate: '2024-01-01',
624
622
  EndDate: '2024-12-31',
625
623
  Status: 'Active'
626
624
  }
627
- };
628
-
629
- const results = await rq.RunQuery(params);
625
+ });
630
626
 
631
627
  // Execute by Query Name and Category Path
632
- const namedParams: RunQueryParams = {
628
+ const namedResult = await rq.RunQuery({
633
629
  QueryName: 'Monthly Sales Report',
634
- CategoryPath: '/Sales/', // Hierarchical path notation
635
- Parameters: {
636
- Month: 12,
637
- Year: 2024,
638
- MinAmount: 1000
639
- }
640
- };
641
-
642
- const namedResults = await rq.RunQuery(namedParams);
643
- ```
630
+ CategoryPath: '/Sales/',
631
+ Parameters: { Month: 12, Year: 2024 }
632
+ });
644
633
 
645
- #### Parameterized Queries
646
-
647
- RunQuery supports powerful parameterized queries using Nunjucks templates with built-in SQL injection protection:
648
-
649
- ```sql
650
- -- Example stored query in the database
651
- SELECT
652
- o.ID,
653
- o.OrderDate,
654
- o.TotalAmount,
655
- c.CustomerName
656
- FROM Orders o
657
- INNER JOIN Customers c ON o.CustomerID = c.ID
658
- WHERE
659
- o.OrderDate >= {{ startDate | sqlDate }} AND
660
- o.OrderDate <= {{ endDate | sqlDate }} AND
661
- o.Status IN {{ statusList | sqlIn }} AND
662
- o.TotalAmount >= {{ minAmount | sqlNumber }}
663
- {% if includeCustomerInfo %}
664
- AND c.IsActive = {{ isActive | sqlBoolean }}
665
- {% endif %}
666
- ORDER BY {{ orderClause | sqlNoKeywordsExpression }}
634
+ if (result.Success) {
635
+ console.log(`Rows: ${result.RowCount}, Time: ${result.ExecutionTime}ms`);
636
+ } else {
637
+ console.error('Query failed:', result.ErrorMessage);
638
+ }
667
639
  ```
668
640
 
669
641
  #### SQL Security Filters
670
642
 
671
- RunQuery includes comprehensive SQL filters to prevent injection attacks:
643
+ Parameterized queries use Nunjucks templates with built-in SQL injection protection filters:
672
644
 
673
- ##### sqlString Filter
674
- Safely escapes string values by doubling single quotes and wrapping in quotes:
645
+ | Filter | Purpose | Example |
646
+ |--------|---------|---------|
647
+ | `sqlString` | Escapes strings, wraps in quotes | `{{ name \| sqlString }}` produces `'O''Brien'` |
648
+ | `sqlNumber` | Validates numeric values | `{{ amount \| sqlNumber }}` produces `1000.5` |
649
+ | `sqlDate` | Formats dates as ISO 8601 | `{{ date \| sqlDate }}` produces `'2024-01-15T00:00:00.000Z'` |
650
+ | `sqlBoolean` | Converts to SQL bit | `{{ flag \| sqlBoolean }}` produces `1` |
651
+ | `sqlIdentifier` | Brackets identifiers | `{{ table \| sqlIdentifier }}` produces `[UserAccounts]` |
652
+ | `sqlIn` | Formats arrays for IN clauses | `{{ list \| sqlIn }}` produces `('A', 'B', 'C')` |
653
+ | `sqlNoKeywordsExpression` | Blocks dangerous SQL keywords | Allows `Revenue DESC`, blocks `DROP TABLE` |
675
654
 
676
- ```sql
677
- -- Template
678
- WHERE CustomerName = {{ name | sqlString }}
655
+ ---
679
656
 
680
- -- Input: "O'Brien"
681
- -- Output: WHERE CustomerName = 'O''Brien'
682
- ```
657
+ ### RunReport
683
658
 
684
- ##### sqlNumber Filter
685
- Validates and formats numeric values:
659
+ Execute reports by ID.
686
660
 
687
- ```sql
688
- -- Template
689
- WHERE Amount >= {{ minAmount | sqlNumber }}
661
+ ```typescript
662
+ import { RunReport, RunReportParams } from '@memberjunction/core';
690
663
 
691
- -- Input: "1000.50"
692
- -- Output: WHERE Amount >= 1000.5
664
+ const rr = new RunReport();
665
+ const result = await rr.RunReport({ ReportID: '12345' });
693
666
  ```
694
667
 
695
- ##### sqlDate Filter
696
- Formats dates in ISO 8601 format:
668
+ ---
697
669
 
698
- ```sql
699
- -- Template
700
- WHERE CreatedDate >= {{ startDate | sqlDate }}
670
+ ### TransactionGroup
701
671
 
702
- -- Input: "2024-01-15"
703
- -- Output: WHERE CreatedDate >= '2024-01-15T00:00:00.000Z'
704
- ```
672
+ Group multiple entity operations into an atomic transaction.
705
673
 
706
- ##### sqlBoolean Filter
707
- Converts boolean values to SQL bit representation:
708
-
709
- ```sql
710
- -- Template
711
- WHERE IsActive = {{ active | sqlBoolean }}
712
-
713
- -- Input: true
714
- -- Output: WHERE IsActive = 1
715
- ```
674
+ ```typescript
675
+ import { Metadata } from '@memberjunction/core';
716
676
 
717
- ##### sqlIdentifier Filter
718
- Safely formats SQL identifiers (table/column names):
677
+ const md = new Metadata();
678
+ const txGroup = await md.CreateTransactionGroup();
719
679
 
720
- ```sql
721
- -- Template
722
- SELECT * FROM {{ tableName | sqlIdentifier }}
680
+ // Add entities to the transaction
681
+ await txGroup.AddTransaction(entity1);
682
+ await txGroup.AddTransaction(entity2);
723
683
 
724
- -- Input: "UserAccounts"
725
- -- Output: SELECT * FROM [UserAccounts]
684
+ // Submit all operations as a single transaction
685
+ const results = await txGroup.Submit();
726
686
  ```
727
687
 
728
- ##### sqlIn Filter
729
- Formats arrays for SQL IN clauses:
688
+ Each `TransactionResult` in the returned array contains a `Success` flag. If any operation fails, all are rolled back.
730
689
 
731
- ```sql
732
- -- Template
733
- WHERE Status IN {{ statusList | sqlIn }}
690
+ ---
734
691
 
735
- -- Input: ['Active', 'Pending', 'Review']
736
- -- Output: WHERE Status IN ('Active', 'Pending', 'Review')
737
- ```
692
+ ### Datasets
738
693
 
739
- ##### sqlNoKeywordsExpression Filter (NEW)
740
- Validates SQL expressions by blocking dangerous keywords while allowing safe expressions:
694
+ Datasets enable efficient bulk loading of related entity collections in a single operation, reducing database round trips.
741
695
 
742
- ```sql
743
- -- Template
744
- ORDER BY {{ orderClause | sqlNoKeywordsExpression }}
696
+ ```mermaid
697
+ flowchart TB
698
+ subgraph Dataset["Dataset System"]
699
+ style Dataset fill:#2d6a9f,stroke:#1a4971,color:#fff
700
+ DEF["Dataset Definition<br/>(name, description)"]
701
+ ITEMS["Dataset Items<br/>(entity, filter, code)"]
702
+ end
745
703
 
746
- -- ✅ ALLOWED: "Revenue DESC, CreatedDate ASC"
747
- -- ALLOWED: "SUM(Amount) DESC"
748
- -- ✅ ALLOWED: "CASE WHEN Amount > 1000 THEN 1 ELSE 0 END"
749
- -- BLOCKED: "Revenue; DROP TABLE Users"
750
- -- ❌ BLOCKED: "Revenue UNION SELECT * FROM Secrets"
751
- ```
704
+ subgraph Loading["Loading Strategies"]
705
+ style Loading fill:#2d8659,stroke:#1a5c3a,color:#fff
706
+ FRESH["GetDatasetByName()<br/>(always fresh)"]
707
+ CACHED["GetAndCacheDatasetByName()<br/>(uses cache if valid)"]
708
+ CHECK["IsDatasetCacheUpToDate()<br/>(check freshness)"]
709
+ CLEAR["ClearDatasetCache()<br/>(invalidate)"]
710
+ end
752
711
 
753
- #### Parameter Types and Validation
712
+ subgraph Storage["Cache Storage"]
713
+ style Storage fill:#b8762f,stroke:#8a5722,color:#fff
714
+ IDB["IndexedDB<br/>(Browser)"]
715
+ FS["File System<br/>(Node.js)"]
716
+ MEM["Memory<br/>(Fallback)"]
717
+ end
754
718
 
755
- Query parameters are defined in the `QueryParameter` entity with automatic validation:
719
+ DEF --> ITEMS
720
+ ITEMS --> FRESH
721
+ ITEMS --> CACHED
722
+ CACHED --> IDB
723
+ CACHED --> FS
724
+ CACHED --> MEM
725
+ ```
756
726
 
757
727
  ```typescript
758
- // Example parameter definitions
759
- {
760
- name: 'startDate',
761
- type: 'date',
762
- isRequired: true,
763
- description: 'Start date for filtering records',
764
- sampleValue: '2024-01-01'
765
- },
766
- {
767
- name: 'statusList',
768
- type: 'array',
769
- isRequired: false,
770
- defaultValue: '["Active", "Pending"]',
771
- description: 'List of allowed status values'
772
- },
773
- {
774
- name: 'minAmount',
775
- type: 'number',
776
- isRequired: true,
777
- description: 'Minimum amount threshold'
778
- }
779
- ```
728
+ import { DatasetItemFilterType } from '@memberjunction/core';
780
729
 
781
- #### Query Permissions
730
+ const md = new Metadata();
782
731
 
783
- Queries support role-based access control:
732
+ // Load dataset with caching
733
+ const dataset = await md.GetAndCacheDatasetByName('ProductCatalog');
784
734
 
785
- ```typescript
786
- // Check if user can run a query (server-side or client-side)
787
- const query = md.Provider.Queries.find(q => q.ID === queryId);
788
- const canRun = query.UserCanRun(contextUser);
789
- const hasPermission = query.UserHasRunPermissions(contextUser);
790
-
791
- // Queries are only executable if:
792
- // 1. User has required role permissions
793
- // 2. Query status is 'Approved'
794
- ```
735
+ // Load with item-specific filters
736
+ const filters: DatasetItemFilterType[] = [
737
+ { ItemCode: 'Products', Filter: 'IsActive = 1' },
738
+ { ItemCode: 'Categories', Filter: 'ParentID IS NULL' }
739
+ ];
740
+ const filteredDataset = await md.GetAndCacheDatasetByName('ProductCatalog', filters);
795
741
 
796
- #### Advanced Features
797
-
798
- ##### Conditional SQL Blocks
799
- Use Nunjucks conditionals for dynamic query structure:
800
-
801
- ```sql
802
- SELECT
803
- CustomerID,
804
- CustomerName,
805
- TotalOrders
806
- {% if includeRevenue %}
807
- , TotalRevenue
808
- {% endif %}
809
- FROM CustomerSummary
810
- WHERE CreatedDate >= {{ startDate | sqlDate }}
811
- {% if filterByRegion %}
812
- AND Region = {{ region | sqlString }}
813
- {% endif %}
814
- ```
742
+ if (filteredDataset.Success) {
743
+ for (const item of filteredDataset.Results) {
744
+ console.log(`Loaded ${item.Results.length} records for ${item.EntityName}`);
745
+ }
746
+ }
815
747
 
816
- ##### Complex Parameter Examples
748
+ // Check if cache is up-to-date
749
+ const isUpToDate = await md.IsDatasetCacheUpToDate('ProductCatalog');
817
750
 
818
- ```typescript
819
- const complexParams: RunQueryParams = {
820
- QueryName: 'Advanced Sales Analysis',
821
- Parameters: {
822
- // Date range
823
- startDate: '2024-01-01',
824
- endDate: '2024-12-31',
825
-
826
- // Array parameters
827
- regions: ['North', 'South', 'East'],
828
- productCategories: [1, 2, 5, 8],
829
-
830
- // Boolean flags
831
- includeDiscounts: true,
832
- excludeReturns: false,
833
-
834
- // Numeric thresholds
835
- minOrderValue: 500.00,
836
- maxOrderValue: 10000.00,
837
-
838
- // Dynamic expressions (safely validated)
839
- orderBy: 'TotalRevenue DESC, CustomerName ASC',
840
- groupingExpression: 'Region, ProductCategory'
841
- }
842
- };
751
+ // Clear cache
752
+ await md.ClearDatasetCache('ProductCatalog');
843
753
  ```
844
754
 
845
- #### Error Handling
755
+ ---
846
756
 
847
- RunQuery provides detailed error information:
757
+ ### BaseEngine
848
758
 
849
- ```typescript
850
- const result = await rq.RunQuery(params);
759
+ The `BaseEngine` abstract class is a singleton pattern for building engine/service classes that auto-load and auto-refresh data from entities or datasets.
851
760
 
852
- if (!result.Success) {
853
- console.error('Query failed:', result.ErrorMessage);
854
-
855
- // Common error types:
856
- // - "Query not found"
857
- // - "User does not have permission to run this query"
858
- // - "Query is not in an approved status (current status: Pending)"
859
- // - "Parameter validation failed: Required parameter 'startDate' is missing"
860
- // - "Dangerous SQL keyword detected: DROP"
861
- // - "Template processing failed: Invalid date: 'not-a-date'"
862
- } else {
863
- console.log('Query executed successfully');
864
- console.log('Rows returned:', result.RowCount);
865
- console.log('Execution time:', result.ExecutionTime, 'ms');
866
- console.log('Applied parameters:', result.AppliedParameters);
867
-
868
- // Process results
869
- result.Results.forEach(row => {
870
- console.log('Row data:', row);
871
- });
872
- }
873
- ```
761
+ ```typescript
762
+ import { BaseEngine, BaseEnginePropertyConfig } from '@memberjunction/core';
874
763
 
875
- #### Query Categories
764
+ export class MyEngine extends BaseEngine<MyEngine> {
765
+ public static get Instance(): MyEngine {
766
+ return super.getInstance<MyEngine>();
767
+ }
876
768
 
877
- Organize queries using categories for better management:
769
+ public MyData: SomeEntity[] = [];
878
770
 
879
- ```typescript
880
- // Query by category path
881
- const categoryParams: RunQueryParams = {
882
- QueryName: 'Top Customers',
883
- CategoryPath: '/Sales Reports/', // Hierarchical path notation
884
- Parameters: { limit: 10 }
885
- };
886
-
887
- // Query with category ID
888
- const categoryIdParams: RunQueryParams = {
889
- QueryName: 'Revenue Trends',
890
- CategoryID: 'sales-cat-123',
891
- Parameters: { months: 12 }
892
- };
893
- ```
771
+ protected get Config(): BaseEnginePropertyConfig[] {
772
+ return [
773
+ {
774
+ PropertyName: 'MyData',
775
+ EntityName: 'Some Entity',
776
+ Filter: 'IsActive = 1',
777
+ OrderBy: 'Name ASC',
778
+ AutoRefresh: true // Auto-refresh on entity save/delete events
779
+ }
780
+ ];
781
+ }
782
+ }
894
783
 
895
- #### Best Practices for RunQuery
896
-
897
- 1. **Always Use Filters**: Apply the appropriate SQL filter to every parameter
898
- 2. **Define Clear Parameters**: Use descriptive names and provide sample values
899
- 3. **Set Proper Permissions**: Restrict query access to appropriate roles
900
- 4. **Validate Input Types**: Use the built-in type system (string, number, date, boolean, array)
901
- 5. **Handle Errors Gracefully**: Check Success and provide meaningful error messages
902
- 6. **Use Approved Queries**: Only execute queries with 'Approved' status
903
- 7. **Leverage Categories**: Organize queries by functional area or team
904
- 8. **Test Parameter Combinations**: Verify all conditional blocks work correctly
905
- 9. **Document Query Purpose**: Add clear descriptions for queries and parameters
906
- 10. **Review SQL Security**: Regular audit of complex expressions and dynamic SQL
907
-
908
- #### Performance Considerations
909
-
910
- - **Parameter Indexing**: Ensure filtered columns have appropriate database indexes
911
- - **Query Optimization**: Use efficient JOINs and WHERE clauses
912
- - **Result Limiting**: Consider adding TOP/LIMIT clauses for large datasets
913
- - **Caching**: Results are not automatically cached - implement application-level caching if needed
914
- - **Connection Pooling**: RunQuery leverages provider connection pooling automatically
915
-
916
- #### Integration with AI Systems
917
-
918
- RunQuery is designed to work seamlessly with AI systems:
919
-
920
- - **Token-Efficient Metadata**: Filter definitions are optimized for AI prompts
921
- - **Self-Documenting**: Parameter definitions include examples and descriptions
922
- - **Safe Code Generation**: AI can generate queries using the secure filter system
923
- - **Validation Feedback**: Clear error messages help AI systems learn and adapt
924
-
925
- #### Example: Complete Sales Dashboard Query
926
-
927
- ```sql
928
- -- Stored query: "Sales Dashboard Data"
929
- SELECT
930
- DATEPART(month, o.OrderDate) AS Month,
931
- DATEPART(year, o.OrderDate) AS Year,
932
- COUNT(*) AS OrderCount,
933
- SUM(o.TotalAmount) AS TotalRevenue,
934
- AVG(o.TotalAmount) AS AvgOrderValue,
935
- COUNT(DISTINCT o.CustomerID) AS UniqueCustomers
936
- {% if includeProductBreakdown %}
937
- , p.CategoryPath
938
- , SUM(od.Quantity) AS TotalQuantity
939
- {% endif %}
940
- FROM Orders o
941
- {% if includeProductBreakdown %}
942
- INNER JOIN OrderDetails od ON o.ID = od.OrderID
943
- INNER JOIN Products p ON od.ProductID = p.ID
944
- {% endif %}
945
- WHERE
946
- o.OrderDate >= {{ startDate | sqlDate }} AND
947
- o.OrderDate <= {{ endDate | sqlDate }} AND
948
- o.Status IN {{ allowedStatuses | sqlIn }}
949
- {% if filterByRegion %}
950
- AND o.Region = {{ region | sqlString }}
951
- {% endif %}
952
- {% if minOrderValue %}
953
- AND o.TotalAmount >= {{ minOrderValue | sqlNumber }}
954
- {% endif %}
955
- GROUP BY
956
- DATEPART(month, o.OrderDate),
957
- DATEPART(year, o.OrderDate)
958
- {% if includeProductBreakdown %}
959
- , p.CategoryPath
960
- {% endif %}
961
- ORDER BY {{ orderExpression | sqlNoKeywordsExpression }}
784
+ // Usage
785
+ await MyEngine.Instance.Config(false, contextUser);
786
+ const data = MyEngine.Instance.MyData;
962
787
  ```
963
788
 
964
- ```typescript
965
- // Execute the dashboard query
966
- const dashboardResult = await rq.RunQuery({
967
- QueryName: 'Sales Dashboard Data',
968
- CategoryPath: '/Analytics/', // Hierarchical path notation
969
- Parameters: {
970
- startDate: '2024-01-01',
971
- endDate: '2024-12-31',
972
- allowedStatuses: ['Completed', 'Shipped'],
973
- includeProductBreakdown: true,
974
- filterByRegion: true,
975
- region: 'North America',
976
- minOrderValue: 100,
977
- orderExpression: 'Year DESC, Month DESC, TotalRevenue DESC'
978
- }
979
- });
789
+ Key features:
790
+ - Singleton per class via `BaseSingleton`
791
+ - Declarative data loading via `BaseEnginePropertyConfig`
792
+ - Automatic refresh when entities are saved or deleted (debounced)
793
+ - Local caching support via `CacheLocal` and `CacheLocalTTL` options
794
+ - Supports both entity and dataset loading
980
795
 
981
- if (dashboardResult.Success) {
982
- // Process the comprehensive dashboard data
983
- const monthlyData = dashboardResult.Results;
984
- console.log(`Generated dashboard with ${monthlyData.length} data points`);
985
- console.log(`Parameters applied:`, dashboardResult.AppliedParameters);
986
- }
987
- ```
796
+ ---
988
797
 
989
- ### RunReport Class
798
+ ### LocalCacheManager
990
799
 
991
- Execute reports with various output formats:
800
+ The `LocalCacheManager` provides intelligent client-side caching for RunView and RunQuery results with TTL, LRU eviction, and differential updates.
992
801
 
993
802
  ```typescript
994
- import { RunReport, RunReportParams } from '@memberjunction/core';
803
+ import { LocalCacheManager } from '@memberjunction/core';
995
804
 
996
- const rr = new RunReport();
805
+ const cache = LocalCacheManager.Instance;
997
806
 
998
- const params: RunReportParams = {
999
- ReportID: '12345',
1000
- Parameters: {
1001
- Year: 2024,
1002
- Department: 'Sales'
1003
- }
1004
- };
807
+ // Initialize with a storage provider
808
+ cache.Init(localStorageProvider);
1005
809
 
1006
- const results = await rr.RunReport(params);
1007
- ```
810
+ // Cache statistics
811
+ const stats = cache.GetStats();
812
+ console.log(`Entries: ${stats.totalEntries}, Hits: ${stats.hits}, Misses: ${stats.misses}`);
1008
813
 
1009
- ### Transaction Management
814
+ // Clear all cached data
815
+ await cache.ClearAll();
816
+ ```
1010
817
 
1011
- Group multiple operations in a transaction:
818
+ To use caching with RunView, set `CacheLocal: true` in your `RunViewParams`:
1012
819
 
1013
820
  ```typescript
1014
- import { TransactionGroupBase } from '@memberjunction/core';
821
+ const result = await rv.RunView({
822
+ EntityName: 'Products',
823
+ ExtraFilter: 'IsActive = 1',
824
+ CacheLocal: true,
825
+ CacheLocalTTL: 300000 // 5 minutes
826
+ });
827
+ ```
1015
828
 
1016
- // Transaction groups allow you to execute multiple entity operations atomically
1017
- // See your specific provider documentation for implementation details
829
+ ---
1018
830
 
1019
- // Example using SQLServerDataProvider:
1020
- const transaction = new TransactionGroupBase('MyTransaction');
831
+ ### DatabaseProviderBase
1021
832
 
1022
- // Add entities to the transaction
1023
- await transaction.AddTransaction(entity1);
1024
- await transaction.AddTransaction(entity2);
833
+ An abstract class for server-side providers that need direct SQL execution and transaction control.
1025
834
 
1026
- // Submit all operations as a single transaction
1027
- const results = await transaction.Submit();
835
+ ```typescript
836
+ // Implemented by providers like SQLServerDataProvider
837
+ abstract class DatabaseProviderBase extends ProviderBase {
838
+ abstract ExecuteSQL<T>(query: string, parameters?: unknown[], options?: ExecuteSQLOptions): Promise<T[]>;
839
+ abstract BeginTransaction(): Promise<void>;
840
+ abstract CommitTransaction(): Promise<void>;
841
+ abstract RollbackTransaction(): Promise<void>;
842
+ }
1028
843
  ```
1029
844
 
1030
- For instance-level transactions in multi-user environments, each provider instance maintains its own transaction state, providing automatic isolation between concurrent requests.
845
+ ---
1031
846
 
1032
- ## Entity Relationships
847
+ ## Provider Architecture
1033
848
 
1034
- MemberJunction automatically handles entity relationships through the metadata system:
849
+ MemberJunction uses a provider model set once at application startup via `SetProvider()`.
1035
850
 
1036
851
  ```typescript
1037
- // Load an entity with related data
1038
- const order = await md.GetEntityObject<OrderEntity>('Orders');
1039
- await order.Load(123, ['OrderDetails', 'Customer']);
852
+ import { SetProvider } from '@memberjunction/core';
1040
853
 
1041
- // Access related entities
1042
- const orderDetails = order.OrderDetails; // Array of OrderDetailEntity
1043
- const customer = order.Customer; // CustomerEntity
854
+ // The provider handles all data access transparently
855
+ SetProvider(myProvider);
1044
856
  ```
1045
857
 
1046
- ## Security & Permissions
1047
-
1048
- The library provides comprehensive security features:
1049
-
1050
- ```typescript
1051
- const md = new Metadata();
858
+ This single call configures the provider for `Metadata`, `BaseEntity`, `RunView`, `RunReport`, and `RunQuery` simultaneously.
1052
859
 
1053
- // Check current user
1054
- const user = md.CurrentUser;
1055
- console.log('Current user:', user.Email);
860
+ ### Metadata Caching Optimization
1056
861
 
1057
- // Check user roles
1058
- const isAdmin = user.RoleName.includes('Admin');
862
+ Subsequent provider instances can reuse cached metadata from the first loaded instance to avoid redundant database calls in multi-user server environments.
1059
863
 
1060
- // Entity permissions
1061
- const entity = md.EntityByName('Orders');
1062
- const canCreate = entity.CanCreate;
1063
- const canUpdate = entity.CanUpdate;
1064
- const canDelete = entity.CanDelete;
864
+ ```typescript
865
+ const config = new ProviderConfigDataBase(
866
+ connectionPool,
867
+ '__mj',
868
+ undefined,
869
+ undefined,
870
+ false // ignoreExistingMetadata = false to reuse cached metadata
871
+ );
1065
872
  ```
1066
873
 
1067
- ## Error Handling
874
+ ---
1068
875
 
1069
- All operations return detailed error information:
876
+ ## Security and Permissions
1070
877
 
1071
878
  ```typescript
1072
- const entity = await md.GetEntityObject('Users');
1073
- const result = await entity.Save();
879
+ const md = new Metadata();
880
+ const user = md.CurrentUser;
881
+ console.log(user.Email, user.IsActive, user.Type);
1074
882
 
1075
- if (!result) {
1076
- // Access detailed error information
1077
- const error = entity.LatestResult;
1078
- console.error('Error:', error.Message);
1079
- console.error('Details:', error.Details);
1080
-
1081
- // Check validation errors
1082
- if (error.ValidationErrors && error.ValidationErrors.length > 0) {
1083
- error.ValidationErrors.forEach(ve => {
1084
- console.error(`Field ${ve.FieldName}: ${ve.Message}`);
1085
- });
1086
- }
883
+ // Entity-level permissions
884
+ const entity = md.EntityByName('Orders');
885
+ console.log(entity.AllowCreateAPI); // Can create via API?
886
+ console.log(entity.AllowUpdateAPI); // Can update via API?
887
+ console.log(entity.AllowDeleteAPI); // Can delete via API?
888
+
889
+ // Role-based permissions
890
+ const permissions = entity.Permissions; // EntityPermissionInfo[]
891
+ for (const perm of permissions) {
892
+ console.log(perm.RoleName, perm.CanCreate, perm.CanRead, perm.CanUpdate, perm.CanDelete);
1087
893
  }
1088
894
  ```
1089
895
 
1090
- ## Logging
896
+ ---
1091
897
 
1092
- MemberJunction provides enhanced logging capabilities with both simple and advanced APIs:
898
+ ## Logging
1093
899
 
1094
900
  ### Basic Logging
1095
901
 
1096
902
  ```typescript
1097
903
  import { LogStatus, LogError } from '@memberjunction/core';
1098
904
 
1099
- // Simple status logging
1100
905
  LogStatus('Operation completed successfully');
1101
-
1102
- // Error logging
1103
- LogError('Operation failed', null, additionalData1, additionalData2);
1104
-
1105
- // Logging to file
906
+ LogError('Something went wrong', null, additionalData);
1106
907
  LogStatus('Writing to file', '/logs/output.log');
1107
908
  ```
1108
909
 
1109
- ### Enhanced Logging (v2.59.0+)
1110
-
1111
- The enhanced logging functions provide structured logging with metadata, categories, and conditional verbose output:
1112
-
1113
- #### LogStatusEx - Enhanced Status Logging
910
+ ### Enhanced Logging
1114
911
 
1115
912
  ```typescript
1116
- import { LogStatusEx, IsVerboseLoggingEnabled, SetVerboseLogging } from '@memberjunction/core';
1117
-
1118
- // Simple usage - same as LogStatus
1119
- LogStatusEx('Process started');
913
+ import { LogStatusEx, LogErrorEx, IsVerboseLoggingEnabled, SetVerboseLogging } from '@memberjunction/core';
1120
914
 
1121
- // Verbose-only logging (respects MJ_VERBOSE environment variable)
915
+ // Verbose-only logging
1122
916
  LogStatusEx({
1123
917
  message: 'Detailed trace information',
1124
- verboseOnly: true
1125
- });
1126
-
1127
- // With custom verbose check
1128
- LogStatusEx({
1129
- message: 'Processing items:',
1130
918
  verboseOnly: true,
1131
- isVerboseEnabled: () => myConfig.debugMode === true,
1132
- additionalArgs: [item1, item2, item3]
919
+ category: 'BatchProcessor'
1133
920
  });
1134
921
 
1135
- // With category and file output
1136
- LogStatusEx({
1137
- message: 'Batch job completed',
1138
- category: 'BatchProcessor',
1139
- logToFileName: '/logs/batch.log',
1140
- additionalArgs: [processedCount, errorCount]
922
+ // Structured error logging
923
+ LogErrorEx({
924
+ message: 'Failed to process request',
925
+ error: new Error('Network timeout'),
926
+ severity: 'critical',
927
+ category: 'NetworkError',
928
+ metadata: { url: 'https://api.example.com', timeout: 5000 },
929
+ includeStack: true
1141
930
  });
931
+
932
+ // Control verbose logging
933
+ SetVerboseLogging(true); // Browser: sets window.MJ_VERBOSE and localStorage
934
+ if (IsVerboseLoggingEnabled()) { /* ... */ }
1142
935
  ```
1143
936
 
1144
- #### LogErrorEx - Enhanced Error Logging
937
+ Verbose logging is controlled by the `MJ_VERBOSE` environment variable (Node.js), global variable, localStorage item, or URL parameter (browser).
938
+
939
+ ---
940
+
941
+ ## TelemetryManager
942
+
943
+ Session-level performance tracking with pattern detection.
1145
944
 
1146
945
  ```typescript
1147
- import { LogErrorEx } from '@memberjunction/core';
1148
-
1149
- // Simple usage - same as LogError
1150
- LogErrorEx('Something went wrong');
1151
-
1152
- // With error object and severity
1153
- try {
1154
- await riskyOperation();
1155
- } catch (error) {
1156
- LogErrorEx({
1157
- message: 'Failed to complete operation',
1158
- error: error as Error,
1159
- severity: 'critical',
1160
- category: 'DataProcessing'
1161
- });
1162
- }
946
+ import { TelemetryManager } from '@memberjunction/core';
1163
947
 
1164
- // With metadata and additional arguments
1165
- LogErrorEx({
1166
- message: 'Validation failed',
1167
- severity: 'warning',
1168
- category: 'Validation',
1169
- metadata: {
1170
- userId: user.ID,
1171
- attemptCount: 3,
1172
- validationRules: ['email', 'uniqueness']
1173
- },
1174
- additionalArgs: [validationResult, user]
1175
- });
948
+ const tm = TelemetryManager.Instance;
949
+ tm.SetEnabled(true);
1176
950
 
1177
- // Control stack trace inclusion
1178
- LogErrorEx({
1179
- message: 'Network timeout',
1180
- error: timeoutError,
1181
- includeStack: false, // Omit stack trace
1182
- metadata: { url: apiUrl, timeout: 5000 }
951
+ // Track an operation
952
+ const eventId = tm.StartEvent('RunView', 'MyComponent.LoadData', {
953
+ EntityName: 'Users',
954
+ ResultType: 'entity_object'
1183
955
  });
956
+ // ... perform operation
957
+ tm.EndEvent(eventId, { cacheHit: false, resultCount: 50 });
958
+
959
+ // Get patterns for analysis
960
+ const patterns = tm.GetPatterns({ category: 'RunView', minCount: 2 });
1184
961
  ```
1185
962
 
1186
- ### Verbose Logging Control
963
+ ---
964
+
965
+ ## Vector Embeddings Support
1187
966
 
1188
- Control verbose logging globally across your application:
967
+ BaseEntity includes built-in methods for generating and managing vector embeddings for text fields.
1189
968
 
1190
969
  ```typescript
1191
- // Check if verbose logging is enabled
1192
- if (IsVerboseLoggingEnabled()) {
1193
- // Perform expensive logging operations
1194
- const debugInfo = gatherDetailedDebugInfo();
1195
- LogStatus('Debug info:', debugInfo);
1196
- }
970
+ import { BaseEntity, SimpleEmbeddingResult } from '@memberjunction/core';
1197
971
 
1198
- // Enable verbose logging in browser environments
1199
- SetVerboseLogging(true);
972
+ // In a server-side entity subclass:
973
+ export class MyEntityServer extends MyEntity {
974
+ public async Save(): Promise<boolean> {
975
+ await this.GenerateEmbeddingsByFieldName([
976
+ {
977
+ fieldName: 'Description',
978
+ vectorFieldName: 'DescriptionVector',
979
+ modelFieldName: 'DescriptionVectorModelID'
980
+ }
981
+ ]);
982
+ return await super.Save();
983
+ }
1200
984
 
1201
- // Verbose logging is controlled by:
1202
- // 1. MJ_VERBOSE environment variable (Node.js)
1203
- // 2. MJ_VERBOSE global variable (Browser)
1204
- // 3. MJ_VERBOSE localStorage item (Browser)
1205
- // 4. MJ_VERBOSE URL parameter (Browser)
985
+ protected async EmbedTextLocal(textToEmbed: string): Promise<SimpleEmbeddingResult> {
986
+ // Implement with your AI provider
987
+ return { vector: [...], modelID: '...' };
988
+ }
989
+ }
1206
990
  ```
1207
991
 
1208
- ### Logging Features
992
+ Features:
993
+ - **Dirty Detection** -- Only generates embeddings when source text changes
994
+ - **Null Handling** -- Clears vector fields when source text is empty
995
+ - **Parallel Processing** -- Multiple embeddings generated concurrently
1209
996
 
1210
- - **Severity Levels**: `warning`, `error`, `critical` for LogErrorEx
1211
- - **Categories**: Organize logs by functional area
1212
- - **Metadata**: Attach structured data to logs
1213
- - **Varargs Support**: Pass additional arguments that get forwarded to console.log/error
1214
- - **File Logging**: Direct logs to files (Node.js environments)
1215
- - **Conditional Logging**: Skip verbose logs based on environment settings
1216
- - **Error Objects**: Automatic error message and stack trace extraction
1217
- - **Cross-Platform**: Works in both Node.js and browser environments
997
+ ---
1218
998
 
1219
- ## Provider Architecture
1220
-
1221
- MemberJunction uses a provider model to support different execution environments:
999
+ ## Utility Functions
1222
1000
 
1223
1001
  ```typescript
1224
- import { SetProvider } from '@memberjunction/core';
1225
-
1226
- // Provider setup is typically handled by your application initialization
1227
- // The provider determines how data is accessed (direct database, API, etc.)
1228
- SetProvider(myProvider);
1002
+ import {
1003
+ TypeScriptTypeFromSQLType,
1004
+ FormatValue,
1005
+ CodeNameFromString,
1006
+ SQLFullType,
1007
+ SQLMaxLength
1008
+ } from '@memberjunction/core';
1009
+
1010
+ // SQL type to TypeScript type mapping
1011
+ TypeScriptTypeFromSQLType('nvarchar'); // 'string'
1012
+ TypeScriptTypeFromSQLType('int'); // 'number'
1013
+ TypeScriptTypeFromSQLType('bit'); // 'boolean'
1014
+ TypeScriptTypeFromSQLType('datetime'); // 'Date'
1015
+
1016
+ // Format values for display
1017
+ FormatValue('money', 1234.5); // '$1,234.50'
1018
+ FormatValue('nvarchar', longText, 2, 'USD', 50); // Truncated with '...'
1019
+
1020
+ // Generate code-safe names
1021
+ CodeNameFromString('First Name'); // 'FirstName'
1229
1022
  ```
1230
1023
 
1231
- ### Metadata Caching Optimization
1024
+ ---
1025
+
1026
+ ## Error Handling
1232
1027
 
1233
- Starting in v2.0, providers support intelligent metadata caching for improved performance in multi-user environments:
1028
+ RunView and RunQuery do NOT throw exceptions on failure. Always check `Success`:
1234
1029
 
1235
1030
  ```typescript
1236
- // First provider instance loads metadata from the database
1237
- const provider1 = new SQLServerDataProvider(connectionPool);
1238
- await provider1.Config(config); // Loads metadata from database
1239
-
1240
- // Subsequent instances can reuse cached metadata
1241
- const config2 = new SQLServerProviderConfigData(
1242
- connectionPool,
1243
- '__mj',
1244
- 0,
1245
- undefined,
1246
- undefined,
1247
- false // ignoreExistingMetadata = false to reuse cached metadata
1248
- );
1249
- const provider2 = new SQLServerDataProvider(connectionPool);
1250
- await provider2.Config(config2); // Reuses metadata from provider1
1251
- ```
1252
-
1253
- This optimization is particularly beneficial in server environments where each request gets its own provider instance.
1031
+ const result = await rv.RunView<UserEntity>({
1032
+ EntityName: 'Users',
1033
+ ExtraFilter: 'IsActive = 1'
1034
+ });
1254
1035
 
1255
- ## Breaking Changes
1036
+ if (result.Success) {
1037
+ const users = result.Results;
1038
+ } else {
1039
+ console.error('View failed:', result.ErrorMessage);
1040
+ }
1041
+ ```
1256
1042
 
1257
- ### v2.131.0
1258
- - **Entity State Tracking**: New `IsSaving`, `IsDeleting`, `IsLoading`, and `IsBusy` getters on BaseEntity to track in-progress operations.
1259
- - **Operation Lifecycle Events**: New event types `save_started`, `delete_started`, `load_started`, and `load_complete` raised during entity operations. The `save_started` event now only fires when the entity will actually be saved (not skipped due to clean state).
1260
- - **Delete Debouncing**: `Delete()` now has the same debouncing behavior as `Save()` - multiple rapid calls return the same promise.
1261
- - **Global Event Broadcasting**: All operation events (start and complete) are now broadcast globally via MJGlobal for application-wide monitoring.
1043
+ For BaseEntity operations, check the return value and `LatestResult`:
1262
1044
 
1263
- ### v2.59.0
1264
- - **Enhanced Logging Functions**: New `LogStatusEx` and `LogErrorEx` functions provide structured logging with metadata, categories, and severity levels. The existing `LogStatus` and `LogError` functions now internally use the enhanced versions, maintaining full backward compatibility.
1265
- - **Verbose Logging Control**: New global functions `IsVerboseLoggingEnabled()` and `SetVerboseLogging()` provide centralized verbose logging control across environments.
1045
+ ```typescript
1046
+ const saved = await entity.Save();
1047
+ if (!saved) {
1048
+ const error = entity.LatestResult;
1049
+ console.error('Save failed:', error.Message);
1266
1050
 
1267
- ### v2.58.0
1268
- - **GetEntityObject() now calls NewRecord() automatically**: When creating new entities, `NewRecord()` is now called automatically. While calling it again is harmless, it's no longer necessary.
1269
- - **UUID Generation**: Entities with non-auto-increment uniqueidentifier primary keys now have UUIDs generated automatically in `NewRecord()`.
1051
+ if (error.ValidationErrors?.length > 0) {
1052
+ for (const ve of error.ValidationErrors) {
1053
+ console.error(`${ve.Source}: ${ve.Message}`);
1054
+ }
1055
+ }
1056
+ }
1057
+ ```
1270
1058
 
1271
- ### v2.52.0
1272
- - **LoadFromData() is now async**: The `LoadFromData()` method in BaseEntity is now async to support subclasses that need to perform additional asynchronous operations during data loading. Update any direct calls to this method to use `await`.
1059
+ ---
1273
1060
 
1274
1061
  ## Best Practices
1275
1062
 
1276
- 1. **Always use Metadata.GetEntityObject()** to create entity instances - never use `new`
1277
- 2. **Use generic types** with RunView for type-safe results
1278
- 3. **Handle errors properly** - check return values and LatestResult
1279
- 4. **Use transactions** for related operations that must succeed/fail together
1280
- 5. **Leverage metadata** for dynamic UI generation and validation
1281
- 6. **Respect permissions** - always check CanCreate/Update/Delete before operations
1282
- 7. **Use ExtraFilter** carefully - ensure SQL injection protection
1283
- 8. **Cache metadata instances** when possible to improve performance
1284
- 9. **Override both Load() and LoadFromData()** in subclasses that need additional loading logic to ensure consistent behavior
1063
+ 1. **Always use `Metadata.GetEntityObject()`** to create entity instances -- never use `new` directly
1064
+ 2. **Use generic types** with `RunView<T>` and `GetEntityObject<T>` for type safety
1065
+ 3. **Use `RunViews` (plural)** to batch multiple independent queries into one operation
1066
+ 4. **Use `ResultType: 'simple'` with `Fields`** for read-only data to improve performance
1067
+ 5. **Check `Success`** on RunView/RunQuery results -- these methods do not throw on failure
1068
+ 6. **Pass `contextUser`** in server-side code for proper security and audit tracking
1069
+ 7. **Use `GetAll()`** instead of the spread operator on BaseEntity instances
1070
+ 8. **Override both `Load()` and `LoadFromData()`** in subclasses that need custom loading logic
1071
+ 9. **Use transactions** for related operations that must succeed or fail together
1072
+ 10. **Leverage entity metadata** for dynamic UI generation and validation
1285
1073
 
1286
- ## Dependencies
1074
+ ---
1287
1075
 
1288
- This package depends on:
1076
+ ## Dependencies
1289
1077
 
1290
1078
  | Package | Description |
1291
1079
  |---------|-------------|
1292
1080
  | [@memberjunction/global](../MJGlobal/README.md) | Core global utilities, class factory, and singleton patterns |
1293
- | rxjs | Reactive programming support for observables |
1294
- | zod | Schema validation for entity fields |
1295
- | debug | Debug logging utilities |
1081
+ | [rxjs](https://rxjs.dev/) | Reactive programming support for observables and event streams |
1082
+ | [zod](https://zod.dev/) | Schema validation for entity fields |
1083
+ | [debug](https://www.npmjs.com/package/debug) | Debug logging utilities with namespace support |
1296
1084
 
1297
1085
  ## Related Packages
1298
1086
 
1299
1087
  ### Provider Implementations
1300
1088
 
1301
- - [@memberjunction/sqlserver-dataprovider](../SQLServerDataProvider/README.md) - SQL Server database provider for server-side operations
1302
- - [@memberjunction/graphql-dataprovider](../GraphQLDataProvider/README.md) - GraphQL provider for client-side applications
1089
+ | Package | Description |
1090
+ |---------|-------------|
1091
+ | [@memberjunction/sqlserver-dataprovider](../SQLServerDataProvider/README.md) | SQL Server database provider for server-side operations |
1092
+ | [@memberjunction/graphql-dataprovider](../GraphQLDataProvider/README.md) | GraphQL provider for client-side applications |
1303
1093
 
1304
1094
  ### Entity Extensions
1305
1095
 
1306
- - [@memberjunction/core-entities](../MJCoreEntities/README.md) - Extended entity classes for MemberJunction system entities
1307
- - [@memberjunction/generated-entities](../GeneratedEntities/README.md) - Auto-generated entity classes for your database
1096
+ | Package | Description |
1097
+ |---------|-------------|
1098
+ | [@memberjunction/core-entities](../MJCoreEntities/README.md) | Extended entity classes for MemberJunction system entities |
1308
1099
 
1309
1100
  ### UI Frameworks
1310
1101
 
1311
- - [@memberjunction/ng-shared](../Angular/Shared/README.md) - Angular-specific components and services
1312
- - [@memberjunction/ng-explorer-core](../Angular/Explorer/core/README.md) - Core Angular explorer components
1102
+ | Package | Description |
1103
+ |---------|-------------|
1104
+ | [@memberjunction/ng-shared](../Angular/Shared/README.md) | Angular-specific components and services |
1105
+ | [@memberjunction/ng-explorer-core](../Angular/Explorer/core/README.md) | Core Angular explorer components |
1313
1106
 
1314
1107
  ### AI Integration
1315
1108
 
1316
- - [@memberjunction/ai](../AI/Core/README.md) - AI framework core abstractions
1317
- - [@memberjunction/aiengine](../AI/Engine/README.md) - AI orchestration engine
1109
+ | Package | Description |
1110
+ |---------|-------------|
1111
+ | [@memberjunction/ai](../AI/Core/README.md) | AI framework core abstractions |
1112
+ | [@memberjunction/aiengine](../AI/Engine/README.md) | AI orchestration engine |
1318
1113
 
1319
1114
  ### Communication
1320
1115
 
1321
- - [@memberjunction/communication-engine](../Communication/engine/README.md) - Multi-channel communication framework
1116
+ | Package | Description |
1117
+ |---------|-------------|
1118
+ | [@memberjunction/communication-engine](../Communication/engine/README.md) | Multi-channel communication framework |
1322
1119
 
1323
1120
  ### Actions
1324
1121
 
1325
- - [@memberjunction/actions](../Actions/Engine/README.md) - Business logic action framework
1326
-
1327
- ## TypeScript Support
1122
+ | Package | Description |
1123
+ |---------|-------------|
1124
+ | [@memberjunction/actions](../Actions/Engine/README.md) | Business logic action framework |
1328
1125
 
1329
- This library is written in TypeScript and provides full type definitions. All generated entity classes include proper typing for IntelliSense support.
1126
+ ---
1330
1127
 
1331
- ## Datasets
1128
+ ## Breaking Changes
1332
1129
 
1333
- 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.
1130
+ ### v2.131.0
1131
+ - **Entity State Tracking**: New `IsSaving`, `IsDeleting`, `IsLoading`, and `IsBusy` getters on BaseEntity.
1132
+ - **Operation Lifecycle Events**: New event types `save_started`, `delete_started`, `load_started`, and `load_complete`.
1133
+ - **Delete Debouncing**: `Delete()` now has the same debouncing behavior as `Save()`.
1134
+ - **Global Event Broadcasting**: All operation events are broadcast globally via MJGlobal.
1334
1135
 
1335
- ### What Are Datasets?
1136
+ ### v2.59.0
1137
+ - **Enhanced Logging Functions**: New `LogStatusEx` and `LogErrorEx` with structured logging. Existing `LogStatus` and `LogError` remain fully backward compatible.
1138
+ - **Verbose Logging Control**: New `IsVerboseLoggingEnabled()` and `SetVerboseLogging()` functions.
1336
1139
 
1337
- 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.
1140
+ ### v2.58.0
1141
+ - **GetEntityObject() auto-calls NewRecord()**: No longer necessary to call `NewRecord()` manually.
1142
+ - **UUID Generation**: Entities with non-auto-increment uniqueidentifier primary keys get UUIDs automatically.
1338
1143
 
1339
- ### How Datasets Work
1144
+ ### v2.52.0
1145
+ - **LoadFromData() is now async**: Update calls to use `await`.
1340
1146
 
1341
- 1. **Dataset Definition**: Datasets are defined in the `Datasets` entity with a unique name and description
1342
- 2. **Dataset Items**: Each dataset contains multiple items defined in the `Dataset Items` entity, where each item specifies:
1343
- - The entity to load
1344
- - An optional filter to apply
1345
- - A unique code to identify the item within the dataset
1346
- 3. **Bulk Loading**: When you request a dataset, all items are loaded in parallel in a single database operation
1347
- 4. **Caching**: Datasets can be cached locally for offline use or improved performance
1147
+ ---
1348
1148
 
1349
- ### Key Benefits
1149
+ ## TypeScript Support
1350
1150
 
1351
- - **Reduced Database Round Trips**: Load multiple entities in one operation instead of many
1352
- - **Better Performance**: Parallel loading and optimized queries
1353
- - **Caching Support**: Built-in local caching with automatic cache invalidation
1354
- - **Offline Capability**: Cached datasets enable offline functionality
1355
- - **Consistency**: All data in a dataset is loaded at the same point in time
1151
+ This library is written in TypeScript and provides full type definitions. All generated entity classes include proper typing for IntelliSense support. The package uses TypeScript strict mode and enforces strong typing throughout -- `any` types are not used.
1356
1152
 
1357
- ### The MJ_Metadata Dataset
1153
+ ## License
1358
1154
 
1359
- The most important dataset in MemberJunction is `MJ_Metadata`, which loads all system metadata including:
1360
- - Entities and their fields
1361
- - Applications and settings
1362
- - User roles and permissions
1363
- - Query definitions
1364
- - Navigation items
1365
- - And more...
1155
+ ISC License - see LICENSE file for details.
1366
1156
 
1367
- This dataset is used internally by MemberJunction to bootstrap the metadata system efficiently.
1157
+ ## Virtual Entities
1368
1158
 
1369
- ### Dataset API Methods
1159
+ Virtual entities are **read-only entities backed by SQL views** rather than physical database tables. They appear in the metadata catalog alongside regular entities but have no underlying base table — only a base view. This makes them ideal for exposing aggregated data, cross-database views, or complex computed datasets as first-class entities.
1370
1160
 
1371
- The Metadata class provides several methods for working with datasets:
1161
+ ```mermaid
1162
+ flowchart LR
1163
+ subgraph Regular["Regular Entity"]
1164
+ RT[Base Table] --> RV[Base View]
1165
+ RV --> RE[Entity Metadata]
1166
+ end
1372
1167
 
1373
- #### GetDatasetByName()
1374
- Always retrieves fresh data from the server without checking cache:
1168
+ subgraph Virtual["Virtual Entity"]
1169
+ VV[SQL View Only] --> VE[Entity Metadata]
1170
+ end
1375
1171
 
1376
- ```typescript
1377
- const md = new Metadata();
1378
- const dataset = await md.GetDatasetByName('MJ_Metadata');
1172
+ RE --> API[GraphQL API / RunView]
1173
+ VE --> API
1379
1174
 
1380
- if (dataset.Success) {
1381
- // Process the dataset results
1382
- for (const item of dataset.Results) {
1383
- console.log(`Loaded ${item.Results.length} records from ${item.EntityName}`);
1384
- }
1385
- }
1175
+ style Virtual fill:#e8d5f5,stroke:#7b2d8e
1176
+ style Regular fill:#d5e8f5,stroke:#2d5f8e
1386
1177
  ```
1387
1178
 
1388
- #### GetAndCacheDatasetByName()
1389
- Retrieves and caches the dataset, using cached version if up-to-date:
1179
+ ### Key Properties
1390
1180
 
1391
- ```typescript
1392
- // This will use cache if available and up-to-date
1393
- const dataset = await md.GetAndCacheDatasetByName('ProductCatalog');
1181
+ | Property | Regular Entity | Virtual Entity |
1182
+ |----------|---------------|----------------|
1183
+ | `VirtualEntity` | `false` | `true` |
1184
+ | `BaseTable` | Physical table name | Same as `BaseView` |
1185
+ | `AllowCreateAPI` | Configurable | Always `false` |
1186
+ | `AllowUpdateAPI` | Configurable | Always `false` |
1187
+ | Stored procedures | Generated | None |
1394
1188
 
1395
- // With custom filters for specific items
1396
- const filters: DatasetItemFilterType[] = [
1397
- { ItemCode: 'Products', Filter: 'IsActive = 1' },
1398
- { ItemCode: 'Categories', Filter: 'ParentID IS NULL' }
1399
- ];
1400
- const filteredDataset = await md.GetAndCacheDatasetByName('ProductCatalog', filters);
1401
- ```
1189
+ ### Read-Only Enforcement
1402
1190
 
1403
- #### IsDatasetCacheUpToDate()
1404
- Checks if the cached version is current without loading the data:
1191
+ Virtual entities are enforced as read-only at multiple layers:
1405
1192
 
1406
- ```typescript
1407
- const isUpToDate = await md.IsDatasetCacheUpToDate('ProductCatalog');
1408
- if (!isUpToDate) {
1409
- console.log('Cache is stale, refreshing...');
1410
- await md.GetAndCacheDatasetByName('ProductCatalog');
1411
- }
1412
- ```
1193
+ 1. **Runtime Guard** — `BaseEntity.CheckPermissions()` blocks Create, Update, and Delete:
1194
+ ```typescript
1195
+ if (this.EntityInfo.VirtualEntity &&
1196
+ (type === EntityPermissionType.Create ||
1197
+ type === EntityPermissionType.Update ||
1198
+ type === EntityPermissionType.Delete)) {
1199
+ throw new Error(
1200
+ `Cannot ${type} on virtual entity '${this.EntityInfo.Name}' — virtual entities are read-only`
1201
+ );
1202
+ }
1203
+ ```
1204
+ 2. **API Flags** — `AllowCreateAPI`, `AllowUpdateAPI`, `AllowDeleteAPI` are all `false`
1205
+ 3. **CodeGen** — No stored procedures are generated
1413
1206
 
1414
- #### ClearDatasetCache()
1415
- Removes a dataset from local cache:
1207
+ ### Using Virtual Entities
1416
1208
 
1417
1209
  ```typescript
1418
- // Clear specific dataset
1419
- await md.ClearDatasetCache('ProductCatalog');
1210
+ import { Metadata, RunView } from '@memberjunction/core';
1420
1211
 
1421
- // Clear dataset with specific filters
1422
- await md.ClearDatasetCache('ProductCatalog', filters);
1423
- ```
1424
-
1425
- ### Dataset Filtering
1426
-
1427
- You can apply filters to individual dataset items to load subsets of data:
1212
+ // Read operations work identically to regular entities
1213
+ const rv = new RunView();
1214
+ const result = await rv.RunView({
1215
+ EntityName: 'Sales Summary',
1216
+ ExtraFilter: `RegionID = '${regionId}'`,
1217
+ ResultType: 'simple'
1218
+ });
1428
1219
 
1429
- ```typescript
1430
- const filters: DatasetItemFilterType[] = [
1431
- {
1432
- ItemCode: 'Orders',
1433
- Filter: "OrderDate >= '2024-01-01' AND Status = 'Active'"
1434
- },
1435
- {
1436
- ItemCode: 'OrderDetails',
1437
- Filter: "OrderID IN (SELECT ID FROM Orders WHERE OrderDate >= '2024-01-01')"
1438
- }
1439
- ];
1220
+ // Access metadata
1221
+ const md = new Metadata();
1222
+ const entity = md.EntityByName('Sales Summary');
1223
+ console.log(entity.VirtualEntity); // true
1224
+ console.log(entity.BaseView); // 'vwSalesSummary'
1440
1225
 
1441
- const dataset = await md.GetAndCacheDatasetByName('RecentOrders', filters);
1226
+ // Save() and Delete() will throw — virtual entities are read-only
1442
1227
  ```
1443
1228
 
1444
- ### Dataset Caching
1445
-
1446
- Datasets are cached using the provider's local storage implementation:
1447
- - **Browser**: IndexedDB or localStorage
1448
- - **Node.js**: File system or memory cache
1449
- - **React Native**: AsyncStorage
1229
+ > **Full Guide**: See [Virtual Entities Guide](./docs/virtual-entities.md) for config-driven creation, LLM-assisted field decoration, field metadata, and troubleshooting.
1450
1230
 
1451
- The cache key includes:
1452
- - Dataset name
1453
- - Applied filters (if any)
1454
- - Connection string (to prevent cache conflicts between environments)
1231
+ ## IS-A Type Relationships (Type Inheritance)
1455
1232
 
1456
- ### Cache Invalidation
1233
+ MemberJunction supports **IS-A type relationships** (also called Table-Per-Type / TPT) where a child entity shares its parent's primary key and inherits all parent fields. This enables type hierarchies like `Meeting IS-A Product` or `Webinar IS-A Meeting IS-A Product`.
1457
1234
 
1458
- The cache is automatically invalidated when:
1459
- - Any entity in the dataset has newer data on the server
1460
- - Row counts differ between cache and server
1461
- - You manually clear the cache
1462
-
1463
- ### Creating Custom Datasets
1464
-
1465
- To create your own dataset:
1466
-
1467
- 1. Create a record in the `Datasets` entity:
1468
- ```typescript
1469
- const datasetEntity = await md.GetEntityObject<DatasetEntity>('Datasets');
1470
- datasetEntity.Name = 'CustomerDashboard';
1471
- datasetEntity.Description = 'All data needed for customer dashboard';
1472
- await datasetEntity.Save();
1473
- ```
1474
-
1475
- 2. Add dataset items for each entity to include:
1476
- ```typescript
1477
- const itemEntity = await md.GetEntityObject<DatasetItemEntity>('Dataset Items');
1478
- itemEntity.DatasetID = datasetEntity.ID;
1479
- itemEntity.Code = 'Customers';
1480
- itemEntity.EntityID = md.EntityByName('Customers').ID;
1481
- itemEntity.Sequence = 1;
1482
- itemEntity.WhereClause = 'IsActive = 1';
1483
- await itemEntity.Save();
1235
+ ```mermaid
1236
+ erDiagram
1237
+ Product ||--o{ Meeting : "IS-A"
1238
+ Product ||--o{ Publication : "IS-A"
1239
+ Meeting ||--o{ Webinar : "IS-A"
1240
+
1241
+ Product {
1242
+ uuid ID PK
1243
+ string Name
1244
+ decimal Price
1245
+ }
1246
+ Meeting {
1247
+ uuid ID PK,FK
1248
+ datetime StartTime
1249
+ int MaxAttendees
1250
+ }
1251
+ Webinar {
1252
+ uuid ID PK,FK
1253
+ string PlatformURL
1254
+ boolean IsRecorded
1255
+ }
1484
1256
  ```
1485
1257
 
1486
- ### Best Practices
1258
+ ### How It Works
1487
1259
 
1488
- 1. **Use Datasets for Related Data**: When you need multiple entities that are logically related
1489
- 2. **Cache Strategically**: Use `GetAndCacheDatasetByName()` for data that doesn't change frequently
1490
- 3. **Apply Filters Wisely**: Filters reduce data volume but make cache keys more specific
1491
- 4. **Monitor Cache Size**: Large datasets can consume significant local storage
1492
- 5. **Refresh When Needed**: Use `IsDatasetCacheUpToDate()` to check before using cached data
1260
+ Child entities share the parent's primary key (same UUID). At runtime, `BaseEntity` uses **persistent composition** each child instance holds a live reference to its parent entity through `_parentEntity`. All field access, dirty tracking, validation, and save/delete orchestration flow through this composition chain automatically.
1493
1261
 
1494
- ### Example: Loading a Dashboard
1262
+ ### EntityInfo IS-A Properties
1495
1263
 
1496
1264
  ```typescript
1497
- // Define a dataset for a sales dashboard
1498
- const dashboardFilters: DatasetItemFilterType[] = [
1499
- { ItemCode: 'Sales', Filter: "Date >= DATEADD(day, -30, GETDATE())" },
1500
- { ItemCode: 'Customers', Filter: "LastOrderDate >= DATEADD(day, -30, GETDATE())" },
1501
- { ItemCode: 'Products', Filter: "StockLevel < ReorderLevel" }
1502
- ];
1503
-
1504
- // Load with caching for performance
1505
- const dashboard = await md.GetAndCacheDatasetByName('SalesDashboard', dashboardFilters);
1506
-
1507
- if (dashboard.Success) {
1508
- // Extract individual entity results
1509
- const recentSales = dashboard.Results.find(r => r.Code === 'Sales')?.Results || [];
1510
- const activeCustomers = dashboard.Results.find(r => r.Code === 'Customers')?.Results || [];
1511
- const lowStockProducts = dashboard.Results.find(r => r.Code === 'Products')?.Results || [];
1512
-
1513
- // Use the data to render your dashboard
1514
- console.log(`Recent sales: ${recentSales.length}`);
1515
- console.log(`Active customers: ${activeCustomers.length}`);
1516
- console.log(`Low stock products: ${lowStockProducts.length}`);
1517
- }
1518
- ```
1519
-
1520
- ## Contributing
1521
-
1522
- When contributing to @memberjunction/core:
1523
-
1524
- 1. **Follow TypeScript Best Practices**: Use strict typing, avoid `any` types
1525
- 2. **Maintain Backward Compatibility**: Existing code should continue to work
1526
- 3. **Document Breaking Changes**: Add version notes to the Breaking Changes section
1527
- 4. **Add TSDoc Comments**: Document all public APIs with TSDoc
1528
- 5. **Test Thoroughly**: Ensure changes work in both server and client environments
1529
-
1530
- See the [MemberJunction Contributing Guide](../../CONTRIBUTING.md) for development setup and guidelines.
1531
-
1532
- ## License
1533
-
1534
- ISC License - see LICENSE file for details
1535
-
1536
- ## Vector Embeddings Support (v2.90.0+)
1537
-
1538
- MemberJunction now provides built-in support for generating and managing vector embeddings for text fields in entities. This feature enables similarity search, duplicate detection, and AI-powered features across your data.
1539
-
1540
- ### Overview
1541
-
1542
- The BaseEntity class now includes methods for generating vector embeddings from text fields and storing them alongside the original data. This functionality is designed to be used by server-side entity subclasses that have access to AI embedding models.
1543
-
1544
- ### Core Methods
1265
+ const md = new Metadata();
1266
+ const meeting = md.EntityByName('Meetings');
1545
1267
 
1546
- BaseEntity provides four methods for managing vector embeddings:
1268
+ meeting.IsChildType; // true has a ParentID
1269
+ meeting.ParentEntityInfo; // EntityInfo for 'Products'
1270
+ meeting.ParentChain; // [ProductsEntityInfo] — all ancestors
1271
+ meeting.AllParentFields; // EntityFieldInfo[] — inherited fields (excludes PKs, timestamps)
1272
+ meeting.ParentEntityFieldNames; // Set<string> — cached for O(1) lookup
1547
1273
 
1548
- ```typescript
1549
- // Generate embeddings for multiple fields by field names
1550
- protected async GenerateEmbeddingsByFieldName(fields: Array<{
1551
- fieldName: string, // Source text field name
1552
- vectorFieldName: string, // Target vector storage field name
1553
- modelFieldName: string // Field to store the model ID used
1554
- }>): Promise<boolean>
1555
-
1556
- // Generate embedding for a single field by name
1557
- protected async GenerateEmbeddingByFieldName(
1558
- fieldName: string,
1559
- vectorFieldName: string,
1560
- modelFieldName: string
1561
- ): Promise<boolean>
1562
-
1563
- // Generate embeddings for multiple fields using EntityField objects
1564
- protected async GenerateEmbeddings(fields: Array<{
1565
- field: EntityField,
1566
- vectorField: EntityField,
1567
- modelField: EntityField
1568
- }>): Promise<boolean>
1569
-
1570
- // Generate embedding for a single field
1571
- protected async GenerateEmbedding(
1572
- field: EntityField,
1573
- vectorField: EntityField,
1574
- modelField: EntityField
1575
- ): Promise<boolean>
1274
+ const product = md.EntityByName('Products');
1275
+ product.IsParentType; // true has child entities
1276
+ product.ChildEntities; // [MeetingsEntityInfo, PublicationsEntityInfo]
1576
1277
  ```
1577
1278
 
1578
- ### Implementation Pattern
1579
-
1580
- To use vector embeddings in your entity:
1581
-
1582
- 1. **Add Vector Storage Fields** to your database table:
1583
- - A field to store the vector (typically NVARCHAR(MAX))
1584
- - A field to store the model ID that generated the vector
1585
-
1586
- 2. **Implement EmbedTextLocal** in your server-side entity subclass:
1587
-
1588
- ```typescript
1589
- import { BaseEntity, SimpleEmbeddingResult } from "@memberjunction/core";
1590
- import { AIEngine } from "@memberjunction/aiengine";
1591
-
1592
- export class MyEntityServer extends MyEntity {
1593
- protected async EmbedTextLocal(textToEmbed: string): Promise<SimpleEmbeddingResult> {
1594
- await AIEngine.Instance.Config(false, this.ContextCurrentUser);
1595
- const result = await AIEngine.Instance.EmbedTextLocal(textToEmbed);
1596
-
1597
- if (!result?.result?.vector || !result?.model?.ID) {
1598
- throw new Error('Failed to generate embedding');
1599
- }
1600
-
1601
- return {
1602
- vector: result.result.vector,
1603
- modelID: result.model.ID
1604
- };
1605
- }
1606
- }
1607
- ```
1279
+ ### BaseEntity Set/Get Routing
1608
1280
 
1609
- 3. **Call GenerateEmbeddings** in your Save method:
1281
+ For IS-A child entities, parent fields are automatically routed to the parent entity:
1610
1282
 
1611
1283
  ```typescript
1612
- public async Save(): Promise<boolean> {
1613
- // Generate embeddings before saving
1614
- await this.GenerateEmbeddingsByFieldName([
1615
- {
1616
- fieldName: "Description",
1617
- vectorFieldName: "DescriptionVector",
1618
- modelFieldName: "DescriptionVectorModelID"
1619
- },
1620
- {
1621
- fieldName: "Content",
1622
- vectorFieldName: "ContentVector",
1623
- modelFieldName: "ContentVectorModelID"
1624
- }
1625
- ]);
1626
-
1627
- return await super.Save();
1628
- }
1629
- ```
1630
-
1631
- ### Automatic Features
1632
-
1633
- The embedding generation system includes several automatic optimizations:
1284
+ const meetingEntity = await md.GetEntityObject<MeetingEntity>('Meetings');
1634
1285
 
1635
- - **Dirty Detection**: Only generates embeddings for new records or when source text changes
1636
- - **Null Handling**: Clears vector fields when source text is empty
1637
- - **Parallel Processing**: Multiple embeddings are generated concurrently for performance
1638
- - **Error Resilience**: Returns false on failure without throwing exceptions
1286
+ // Own field stored locally
1287
+ meetingEntity.Set('StartTime', new Date());
1639
1288
 
1640
- ### Type Definitions
1289
+ // Parent field — automatically routed to ProductEntity._parentEntity
1290
+ meetingEntity.Set('Name', 'Annual Conference');
1291
+ meetingEntity.Get('Name'); // Returns from _parentEntity (authoritative)
1641
1292
 
1642
- The `SimpleEmbeddingResult` type is defined in @memberjunction/core:
1643
-
1644
- ```typescript
1645
- export type SimpleEmbeddingResult = {
1646
- vector: number[]; // The embedding vector
1647
- modelID: string; // ID of the AI model used
1648
- }
1293
+ // Dirty tracking spans the chain
1294
+ meetingEntity.Dirty; // true if ANY field in chain is modified
1649
1295
  ```
1650
1296
 
1651
- ### Architecture Benefits
1297
+ ### Save & Delete Orchestration
1652
1298
 
1653
- This design provides:
1654
- - **Clean Separation**: Core orchestration logic in BaseEntity, AI integration in subclasses
1655
- - **No Dependency Issues**: BaseEntity doesn't depend on AI packages
1656
- - **Reusability**: Any server-side entity can add embeddings with minimal code
1657
- - **Type Safety**: Full TypeScript support throughout
1299
+ - **Save** — Parent entities are saved first (inner-to-outer), then the child. On server, a shared SQL transaction wraps the entire chain.
1300
+ - **Delete** Child is deleted first, then parents (outer-to-inner). Disjoint subtype enforcement prevents a parent from being multiple child types simultaneously.
1658
1301
 
1659
- ### Example: Complete Implementation
1302
+ > **Full Guide**: See [IS-A Relationships Guide](./docs/isa-relationships.md) for the complete data model, runtime object model, save/delete orchestration sequences, provider implementations, CodeGen integration, and troubleshooting.
1660
1303
 
1661
- ```typescript
1662
- // In your server-side entity file
1663
- import { BaseEntity, SimpleEmbeddingResult } from "@memberjunction/core";
1664
- import { RegisterClass } from "@memberjunction/global";
1665
- import { ComponentEntityExtended } from "@memberjunction/core-entities";
1666
- import { AIEngine } from "@memberjunction/aiengine";
1667
-
1668
- @RegisterClass(BaseEntity, 'Components')
1669
- export class ComponentEntityServer extends ComponentEntityExtended {
1670
- public async Save(): Promise<boolean> {
1671
- // Generate embeddings for text fields
1672
- await this.GenerateEmbeddingsByFieldName([
1673
- {
1674
- fieldName: "TechnicalDesign",
1675
- vectorFieldName: "TechnicalDesignVector",
1676
- modelFieldName: "TechnicalDesignVectorModelID"
1677
- },
1678
- {
1679
- fieldName: "FunctionalRequirements",
1680
- vectorFieldName: "FunctionalRequirementsVector",
1681
- modelFieldName: "FunctionalRequirementsVectorModelID"
1682
- }
1683
- ]);
1684
-
1685
- return await super.Save();
1686
- }
1687
-
1688
- protected async EmbedTextLocal(textToEmbed: string): Promise<SimpleEmbeddingResult> {
1689
- await AIEngine.Instance.Config(false, this.ContextCurrentUser);
1690
- const e = await AIEngine.Instance.EmbedTextLocal(textToEmbed);
1691
-
1692
- if (!e?.result?.vector || !e?.model?.ID) {
1693
- throw new Error('Failed to generate embedding - no vector or model ID returned');
1694
- }
1695
-
1696
- return {
1697
- vector: e.result.vector,
1698
- modelID: e.model.ID
1699
- };
1700
- }
1701
- }
1702
- ```
1304
+ ## Documentation
1703
1305
 
1704
- ### Best Practices
1306
+ For detailed guides on specific topics, see the [docs/](./docs/) folder:
1705
1307
 
1706
- 1. **Database Schema**: Store vectors as JSON strings in NVARCHAR(MAX) fields
1707
- 2. **Model Tracking**: Always store the model ID to track which model generated each vector
1708
- 3. **Error Handling**: Implement proper error handling in your EmbedTextLocal override
1709
- 4. **Performance**: Use batch methods when generating multiple embeddings
1710
- 5. **Security**: Ensure proper user context is passed for multi-tenant scenarios
1308
+ - [Virtual Entities](./docs/virtual-entities.md) Config-driven creation, LLM decoration, read-only enforcement
1309
+ - [IS-A Relationships](./docs/isa-relationships.md) Type inheritance, save/delete orchestration, provider integration
1310
+ - [RunQuery Pagination](./docs/runquery-pagination.md) Parameterized queries with pagination support
1711
1311
 
1712
1312
  ## Support
1713
1313
 
1714
- For support, documentation, and examples, visit [MemberJunction.com](https://www.memberjunction.com)
1314
+ For support, documentation, and examples, visit [MemberJunction.com](https://www.memberjunction.com).