@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.
- package/dist/generic/InMemoryLocalStorageProvider.d.ts +1 -1
- package/dist/generic/InMemoryLocalStorageProvider.js +2 -6
- package/dist/generic/InMemoryLocalStorageProvider.js.map +1 -1
- package/dist/generic/QueryCache.d.ts +1 -1
- package/dist/generic/QueryCache.js +6 -10
- package/dist/generic/QueryCache.js.map +1 -1
- package/dist/generic/QueryCacheConfig.js +1 -2
- package/dist/generic/RegisterForStartup.d.ts +2 -2
- package/dist/generic/RegisterForStartup.js +7 -12
- package/dist/generic/RegisterForStartup.js.map +1 -1
- package/dist/generic/applicationInfo.d.ts +3 -3
- package/dist/generic/applicationInfo.js +4 -10
- package/dist/generic/applicationInfo.js.map +1 -1
- package/dist/generic/authEvaluator.d.ts +1 -1
- package/dist/generic/authEvaluator.js +4 -8
- package/dist/generic/authEvaluator.js.map +1 -1
- package/dist/generic/authTypes.js +1 -4
- package/dist/generic/authTypes.js.map +1 -1
- package/dist/generic/baseEngine.d.ts +5 -5
- package/dist/generic/baseEngine.js +51 -56
- package/dist/generic/baseEngine.js.map +1 -1
- package/dist/generic/baseEngineRegistry.js +13 -17
- package/dist/generic/baseEngineRegistry.js.map +1 -1
- package/dist/generic/baseEntity.d.ts +171 -5
- package/dist/generic/baseEntity.d.ts.map +1 -1
- package/dist/generic/baseEntity.js +651 -121
- package/dist/generic/baseEntity.js.map +1 -1
- package/dist/generic/baseInfo.js +3 -7
- package/dist/generic/baseInfo.js.map +1 -1
- package/dist/generic/compositeKey.d.ts +2 -2
- package/dist/generic/compositeKey.js +5 -11
- package/dist/generic/compositeKey.js.map +1 -1
- package/dist/generic/databaseProviderBase.d.ts +2 -2
- package/dist/generic/databaseProviderBase.js +2 -6
- package/dist/generic/databaseProviderBase.js.map +1 -1
- package/dist/generic/entityInfo.d.ts +84 -5
- package/dist/generic/entityInfo.d.ts.map +1 -1
- package/dist/generic/entityInfo.js +235 -108
- package/dist/generic/entityInfo.js.map +1 -1
- package/dist/generic/explorerNavigationItem.d.ts +1 -1
- package/dist/generic/explorerNavigationItem.js +2 -6
- package/dist/generic/explorerNavigationItem.js.map +1 -1
- package/dist/generic/graphqlTypeNames.d.ts +1 -1
- package/dist/generic/graphqlTypeNames.js +4 -9
- package/dist/generic/graphqlTypeNames.js.map +1 -1
- package/dist/generic/interfaces.d.ts +104 -14
- package/dist/generic/interfaces.d.ts.map +1 -1
- package/dist/generic/interfaces.js +28 -30
- package/dist/generic/interfaces.js.map +1 -1
- package/dist/generic/libraryInfo.d.ts +1 -1
- package/dist/generic/libraryInfo.js +2 -6
- package/dist/generic/libraryInfo.js.map +1 -1
- package/dist/generic/localCacheManager.d.ts +2 -2
- package/dist/generic/localCacheManager.js +44 -48
- package/dist/generic/localCacheManager.js.map +1 -1
- package/dist/generic/logging.d.ts.map +1 -1
- package/dist/generic/logging.js +54 -67
- package/dist/generic/logging.js.map +1 -1
- package/dist/generic/metadata.d.ts +12 -12
- package/dist/generic/metadata.d.ts.map +1 -1
- package/dist/generic/metadata.js +21 -25
- package/dist/generic/metadata.js.map +1 -1
- package/dist/generic/metadataUtil.d.ts +1 -1
- package/dist/generic/metadataUtil.js +3 -7
- package/dist/generic/metadataUtil.js.map +1 -1
- package/dist/generic/providerBase.d.ts +63 -16
- package/dist/generic/providerBase.d.ts.map +1 -1
- package/dist/generic/providerBase.js +253 -130
- package/dist/generic/providerBase.js.map +1 -1
- package/dist/generic/queryInfo.d.ts +5 -5
- package/dist/generic/queryInfo.js +21 -30
- package/dist/generic/queryInfo.js.map +1 -1
- package/dist/generic/queryInfoInterfaces.js +1 -2
- package/dist/generic/queryInfoInterfaces.js.map +1 -1
- package/dist/generic/querySQLFilters.js +5 -10
- package/dist/generic/querySQLFilters.js.map +1 -1
- package/dist/generic/runQuery.d.ts +2 -2
- package/dist/generic/runQuery.js +5 -9
- package/dist/generic/runQuery.js.map +1 -1
- package/dist/generic/runQuerySQLFilterImplementations.d.ts +1 -1
- package/dist/generic/runQuerySQLFilterImplementations.js +4 -8
- package/dist/generic/runQuerySQLFilterImplementations.js.map +1 -1
- package/dist/generic/runReport.d.ts +2 -2
- package/dist/generic/runReport.js +5 -9
- package/dist/generic/runReport.js.map +1 -1
- package/dist/generic/securityInfo.d.ts +2 -2
- package/dist/generic/securityInfo.js +10 -20
- package/dist/generic/securityInfo.js.map +1 -1
- package/dist/generic/telemetryManager.js +20 -32
- package/dist/generic/telemetryManager.js.map +1 -1
- package/dist/generic/transactionGroup.d.ts +1 -1
- package/dist/generic/transactionGroup.d.ts.map +1 -1
- package/dist/generic/transactionGroup.js +11 -19
- package/dist/generic/transactionGroup.js.map +1 -1
- package/dist/generic/util.js +15 -31
- package/dist/generic/util.js.map +1 -1
- package/dist/index.d.ts +34 -34
- package/dist/index.js +45 -63
- package/dist/index.js.map +1 -1
- package/dist/views/runView.d.ts +3 -3
- package/dist/views/runView.js +6 -11
- package/dist/views/runView.js.map +1 -1
- package/dist/views/viewInfo.d.ts +3 -3
- package/dist/views/viewInfo.js +10 -17
- package/dist/views/viewInfo.js.map +1 -1
- package/package.json +11 -10
- package/readme.md +871 -1271
package/readme.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# @memberjunction/core
|
|
2
2
|
|
|
3
|
-
The `@memberjunction/core` library
|
|
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
|
-
##
|
|
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
|
-
|
|
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
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
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
|
-
|
|
43
|
-
|
|
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
|
-
|
|
48
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
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
|
|
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
|
|
96
|
-
const
|
|
97
|
-
const
|
|
98
|
-
const currentUser = md.CurrentUser;
|
|
99
|
-
const roles = md.Roles;
|
|
100
|
-
const
|
|
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
|
-
####
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
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
|
-
//
|
|
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
|
-
//
|
|
129
|
-
const
|
|
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
|
-
//
|
|
132
|
-
const
|
|
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
|
-
//
|
|
135
|
-
const
|
|
210
|
+
// Record merging
|
|
211
|
+
const mergeResult = await md.MergeRecords(mergeRequest);
|
|
136
212
|
|
|
137
|
-
//
|
|
138
|
-
const
|
|
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()
|
|
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
|
-
|
|
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
|
-
//
|
|
149
|
-
const
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
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
|
|
255
|
+
#### Loading Existing Records
|
|
159
256
|
|
|
160
257
|
```typescript
|
|
161
|
-
|
|
162
|
-
const user = await md.GetEntityObject<UserEntity>('Users');
|
|
163
|
-
await user.Load(userId);
|
|
258
|
+
import { CompositeKey } from '@memberjunction/core';
|
|
164
259
|
|
|
165
|
-
//
|
|
166
|
-
const
|
|
260
|
+
// Load by ID (most common)
|
|
261
|
+
const user = await md.GetEntityObject<UserEntity>('Users', CompositeKey.FromID(userId));
|
|
167
262
|
|
|
168
|
-
// Load by
|
|
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
|
-
|
|
279
|
+
### BaseEntity
|
|
185
280
|
|
|
186
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
247
|
-
const
|
|
286
|
+
// Type-safe property access (via generated getters/setters)
|
|
287
|
+
const name = user.FirstName;
|
|
288
|
+
user.FirstName = 'Jane';
|
|
248
289
|
|
|
249
|
-
//
|
|
290
|
+
// Dynamic field access
|
|
291
|
+
const value = user.Get('FirstName');
|
|
250
292
|
user.Set('FirstName', 'Jane');
|
|
251
293
|
|
|
252
|
-
//
|
|
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.
|
|
258
|
-
console.log(field.
|
|
259
|
-
console.log(field.
|
|
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
|
-
####
|
|
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
|
-
|
|
300
|
-
|
|
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
|
-
|
|
317
|
+
// Delete
|
|
318
|
+
await entity.Delete();
|
|
319
|
+
```
|
|
311
320
|
|
|
312
|
-
####
|
|
321
|
+
#### GetAll() for Spread Operator
|
|
313
322
|
|
|
314
|
-
|
|
323
|
+
BaseEntity uses getter/setter properties, so the spread operator will not capture field values. Use `GetAll()` instead.
|
|
315
324
|
|
|
316
325
|
```typescript
|
|
317
|
-
|
|
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
|
-
|
|
325
|
-
|
|
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
|
|
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
|
-
|
|
335
|
+
BaseEntity provides comprehensive state tracking and lifecycle events.
|
|
351
336
|
|
|
352
337
|
```typescript
|
|
353
338
|
import { BaseEntityEvent } from '@memberjunction/core';
|
|
354
339
|
|
|
355
|
-
|
|
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
|
|
358
|
-
const subscription =
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
386
|
-
hideLoadingIndicator();
|
|
365
|
+
console.log('Load completed');
|
|
387
366
|
break;
|
|
388
|
-
|
|
389
|
-
// Other events
|
|
390
367
|
case 'new_record':
|
|
391
|
-
console.log('NewRecord()
|
|
368
|
+
console.log('NewRecord() called');
|
|
392
369
|
break;
|
|
393
370
|
}
|
|
394
371
|
});
|
|
395
372
|
|
|
396
|
-
//
|
|
373
|
+
// Unsubscribe when done
|
|
397
374
|
subscription.unsubscribe();
|
|
398
375
|
```
|
|
399
376
|
|
|
400
|
-
####
|
|
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
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
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
|
|
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,
|
|
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
|
|
394
|
+
// result1 === result2
|
|
449
395
|
```
|
|
450
396
|
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
#### Awaiting In-Progress Operations
|
|
397
|
+
#### Global Event Subscription
|
|
454
398
|
|
|
455
|
-
|
|
399
|
+
Monitor all entity operations across the application.
|
|
456
400
|
|
|
457
401
|
```typescript
|
|
458
|
-
|
|
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
|
-
|
|
465
|
-
|
|
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
|
-
|
|
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
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
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
|
-
|
|
424
|
+
### CompositeKey
|
|
490
425
|
|
|
491
|
-
|
|
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
|
-
|
|
496
|
-
|
|
428
|
+
```typescript
|
|
429
|
+
import { CompositeKey, KeyValuePair } from '@memberjunction/core';
|
|
497
430
|
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
}
|
|
501
|
-
```
|
|
431
|
+
// Single ID field
|
|
432
|
+
const key = CompositeKey.FromID('abc-123');
|
|
502
433
|
|
|
503
|
-
|
|
504
|
-
|
|
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
|
-
|
|
437
|
+
// Composite key
|
|
438
|
+
const key3 = CompositeKey.FromKeyValuePairs([
|
|
439
|
+
{ FieldName: 'OrderID', Value: orderId },
|
|
440
|
+
{ FieldName: 'ProductID', Value: productId }
|
|
441
|
+
]);
|
|
510
442
|
|
|
511
|
-
|
|
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
|
-
|
|
514
|
-
class EntityFormComponent {
|
|
515
|
-
private entity: BaseEntity;
|
|
516
|
-
private subscription: Subscription;
|
|
450
|
+
---
|
|
517
451
|
|
|
518
|
-
|
|
519
|
-
this.entity = await md.GetEntityObject<UserEntity>('Users');
|
|
452
|
+
### RunView
|
|
520
453
|
|
|
521
|
-
|
|
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
|
-
|
|
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
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
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
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
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
|
-
|
|
554
|
-
|
|
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
|
-
|
|
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
|
|
495
|
+
const result = await rv.RunView({
|
|
570
496
|
ViewName: 'Active Users',
|
|
571
|
-
ExtraFilter:
|
|
572
|
-
|
|
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
|
|
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'
|
|
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
|
-
//
|
|
587
|
-
const users = dynamicResults.Results; // Properly typed as UserEntity[]
|
|
555
|
+
// Aggregate results are in result.AggregateResults[]
|
|
588
556
|
```
|
|
589
557
|
|
|
590
|
-
####
|
|
558
|
+
#### ResultType and Fields Optimization
|
|
591
559
|
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
####
|
|
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
|
|
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
|
|
628
|
+
const namedResult = await rq.RunQuery({
|
|
633
629
|
QueryName: 'Monthly Sales Report',
|
|
634
|
-
CategoryPath: '/Sales/',
|
|
635
|
-
Parameters: {
|
|
636
|
-
|
|
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
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
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
|
-
|
|
643
|
+
Parameterized queries use Nunjucks templates with built-in SQL injection protection filters:
|
|
672
644
|
|
|
673
|
-
|
|
674
|
-
|
|
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
|
-
|
|
677
|
-
-- Template
|
|
678
|
-
WHERE CustomerName = {{ name | sqlString }}
|
|
655
|
+
---
|
|
679
656
|
|
|
680
|
-
|
|
681
|
-
-- Output: WHERE CustomerName = 'O''Brien'
|
|
682
|
-
```
|
|
657
|
+
### RunReport
|
|
683
658
|
|
|
684
|
-
|
|
685
|
-
Validates and formats numeric values:
|
|
659
|
+
Execute reports by ID.
|
|
686
660
|
|
|
687
|
-
```
|
|
688
|
-
|
|
689
|
-
WHERE Amount >= {{ minAmount | sqlNumber }}
|
|
661
|
+
```typescript
|
|
662
|
+
import { RunReport, RunReportParams } from '@memberjunction/core';
|
|
690
663
|
|
|
691
|
-
|
|
692
|
-
|
|
664
|
+
const rr = new RunReport();
|
|
665
|
+
const result = await rr.RunReport({ ReportID: '12345' });
|
|
693
666
|
```
|
|
694
667
|
|
|
695
|
-
|
|
696
|
-
Formats dates in ISO 8601 format:
|
|
668
|
+
---
|
|
697
669
|
|
|
698
|
-
|
|
699
|
-
-- Template
|
|
700
|
-
WHERE CreatedDate >= {{ startDate | sqlDate }}
|
|
670
|
+
### TransactionGroup
|
|
701
671
|
|
|
702
|
-
|
|
703
|
-
-- Output: WHERE CreatedDate >= '2024-01-15T00:00:00.000Z'
|
|
704
|
-
```
|
|
672
|
+
Group multiple entity operations into an atomic transaction.
|
|
705
673
|
|
|
706
|
-
|
|
707
|
-
|
|
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
|
-
|
|
718
|
-
|
|
677
|
+
const md = new Metadata();
|
|
678
|
+
const txGroup = await md.CreateTransactionGroup();
|
|
719
679
|
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
680
|
+
// Add entities to the transaction
|
|
681
|
+
await txGroup.AddTransaction(entity1);
|
|
682
|
+
await txGroup.AddTransaction(entity2);
|
|
723
683
|
|
|
724
|
-
|
|
725
|
-
|
|
684
|
+
// Submit all operations as a single transaction
|
|
685
|
+
const results = await txGroup.Submit();
|
|
726
686
|
```
|
|
727
687
|
|
|
728
|
-
|
|
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
|
-
|
|
732
|
-
-- Template
|
|
733
|
-
WHERE Status IN {{ statusList | sqlIn }}
|
|
690
|
+
---
|
|
734
691
|
|
|
735
|
-
|
|
736
|
-
-- Output: WHERE Status IN ('Active', 'Pending', 'Review')
|
|
737
|
-
```
|
|
692
|
+
### Datasets
|
|
738
693
|
|
|
739
|
-
|
|
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
|
-
```
|
|
743
|
-
|
|
744
|
-
|
|
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
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
730
|
+
const md = new Metadata();
|
|
782
731
|
|
|
783
|
-
|
|
732
|
+
// Load dataset with caching
|
|
733
|
+
const dataset = await md.GetAndCacheDatasetByName('ProductCatalog');
|
|
784
734
|
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
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
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
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
|
-
|
|
748
|
+
// Check if cache is up-to-date
|
|
749
|
+
const isUpToDate = await md.IsDatasetCacheUpToDate('ProductCatalog');
|
|
817
750
|
|
|
818
|
-
|
|
819
|
-
|
|
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
|
-
|
|
755
|
+
---
|
|
846
756
|
|
|
847
|
-
|
|
757
|
+
### BaseEngine
|
|
848
758
|
|
|
849
|
-
|
|
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
|
-
|
|
853
|
-
|
|
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
|
-
|
|
764
|
+
export class MyEngine extends BaseEngine<MyEngine> {
|
|
765
|
+
public static get Instance(): MyEngine {
|
|
766
|
+
return super.getInstance<MyEngine>();
|
|
767
|
+
}
|
|
876
768
|
|
|
877
|
-
|
|
769
|
+
public MyData: SomeEntity[] = [];
|
|
878
770
|
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
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
|
-
|
|
896
|
-
|
|
897
|
-
|
|
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
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
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
|
-
|
|
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
|
-
###
|
|
798
|
+
### LocalCacheManager
|
|
990
799
|
|
|
991
|
-
|
|
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 {
|
|
803
|
+
import { LocalCacheManager } from '@memberjunction/core';
|
|
995
804
|
|
|
996
|
-
const
|
|
805
|
+
const cache = LocalCacheManager.Instance;
|
|
997
806
|
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
Parameters: {
|
|
1001
|
-
Year: 2024,
|
|
1002
|
-
Department: 'Sales'
|
|
1003
|
-
}
|
|
1004
|
-
};
|
|
807
|
+
// Initialize with a storage provider
|
|
808
|
+
cache.Init(localStorageProvider);
|
|
1005
809
|
|
|
1006
|
-
|
|
1007
|
-
|
|
810
|
+
// Cache statistics
|
|
811
|
+
const stats = cache.GetStats();
|
|
812
|
+
console.log(`Entries: ${stats.totalEntries}, Hits: ${stats.hits}, Misses: ${stats.misses}`);
|
|
1008
813
|
|
|
1009
|
-
|
|
814
|
+
// Clear all cached data
|
|
815
|
+
await cache.ClearAll();
|
|
816
|
+
```
|
|
1010
817
|
|
|
1011
|
-
|
|
818
|
+
To use caching with RunView, set `CacheLocal: true` in your `RunViewParams`:
|
|
1012
819
|
|
|
1013
820
|
```typescript
|
|
1014
|
-
|
|
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
|
-
|
|
1017
|
-
// See your specific provider documentation for implementation details
|
|
829
|
+
---
|
|
1018
830
|
|
|
1019
|
-
|
|
1020
|
-
const transaction = new TransactionGroupBase('MyTransaction');
|
|
831
|
+
### DatabaseProviderBase
|
|
1021
832
|
|
|
1022
|
-
|
|
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
|
-
|
|
1027
|
-
|
|
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
|
-
|
|
845
|
+
---
|
|
1031
846
|
|
|
1032
|
-
##
|
|
847
|
+
## Provider Architecture
|
|
1033
848
|
|
|
1034
|
-
MemberJunction
|
|
849
|
+
MemberJunction uses a provider model set once at application startup via `SetProvider()`.
|
|
1035
850
|
|
|
1036
851
|
```typescript
|
|
1037
|
-
|
|
1038
|
-
const order = await md.GetEntityObject<OrderEntity>('Orders');
|
|
1039
|
-
await order.Load(123, ['OrderDetails', 'Customer']);
|
|
852
|
+
import { SetProvider } from '@memberjunction/core';
|
|
1040
853
|
|
|
1041
|
-
//
|
|
1042
|
-
|
|
1043
|
-
const customer = order.Customer; // CustomerEntity
|
|
854
|
+
// The provider handles all data access transparently
|
|
855
|
+
SetProvider(myProvider);
|
|
1044
856
|
```
|
|
1045
857
|
|
|
1046
|
-
|
|
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
|
-
|
|
1054
|
-
const user = md.CurrentUser;
|
|
1055
|
-
console.log('Current user:', user.Email);
|
|
860
|
+
### Metadata Caching Optimization
|
|
1056
861
|
|
|
1057
|
-
|
|
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
|
-
|
|
1061
|
-
const
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
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
|
-
|
|
874
|
+
---
|
|
1068
875
|
|
|
1069
|
-
|
|
876
|
+
## Security and Permissions
|
|
1070
877
|
|
|
1071
878
|
```typescript
|
|
1072
|
-
const
|
|
1073
|
-
const
|
|
879
|
+
const md = new Metadata();
|
|
880
|
+
const user = md.CurrentUser;
|
|
881
|
+
console.log(user.Email, user.IsActive, user.Type);
|
|
1074
882
|
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
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
|
-
|
|
896
|
+
---
|
|
1091
897
|
|
|
1092
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
1132
|
-
additionalArgs: [item1, item2, item3]
|
|
919
|
+
category: 'BatchProcessor'
|
|
1133
920
|
});
|
|
1134
921
|
|
|
1135
|
-
//
|
|
1136
|
-
|
|
1137
|
-
message: '
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
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
|
-
|
|
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 {
|
|
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
|
-
|
|
1165
|
-
|
|
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
|
-
//
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
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
|
-
|
|
963
|
+
---
|
|
964
|
+
|
|
965
|
+
## Vector Embeddings Support
|
|
1187
966
|
|
|
1188
|
-
|
|
967
|
+
BaseEntity includes built-in methods for generating and managing vector embeddings for text fields.
|
|
1189
968
|
|
|
1190
969
|
```typescript
|
|
1191
|
-
|
|
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
|
-
//
|
|
1199
|
-
|
|
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
|
-
|
|
1202
|
-
//
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
##
|
|
1220
|
-
|
|
1221
|
-
MemberJunction uses a provider model to support different execution environments:
|
|
999
|
+
## Utility Functions
|
|
1222
1000
|
|
|
1223
1001
|
```typescript
|
|
1224
|
-
import {
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
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
|
-
|
|
1024
|
+
---
|
|
1025
|
+
|
|
1026
|
+
## Error Handling
|
|
1232
1027
|
|
|
1233
|
-
|
|
1028
|
+
RunView and RunQuery do NOT throw exceptions on failure. Always check `Success`:
|
|
1234
1029
|
|
|
1235
1030
|
```typescript
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
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
|
-
|
|
1036
|
+
if (result.Success) {
|
|
1037
|
+
const users = result.Results;
|
|
1038
|
+
} else {
|
|
1039
|
+
console.error('View failed:', result.ErrorMessage);
|
|
1040
|
+
}
|
|
1041
|
+
```
|
|
1256
1042
|
|
|
1257
|
-
|
|
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
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
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
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
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
|
-
|
|
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()
|
|
1277
|
-
2. **Use generic types** with RunView for type
|
|
1278
|
-
3. **
|
|
1279
|
-
4. **Use
|
|
1280
|
-
5. **
|
|
1281
|
-
6. **
|
|
1282
|
-
7. **Use
|
|
1283
|
-
8. **
|
|
1284
|
-
9. **
|
|
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
|
-
|
|
1074
|
+
---
|
|
1287
1075
|
|
|
1288
|
-
|
|
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
|
-
|
|
1302
|
-
|
|
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
|
-
|
|
1307
|
-
|
|
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
|
-
|
|
1312
|
-
|
|
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
|
-
|
|
1317
|
-
|
|
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
|
-
|
|
1116
|
+
| Package | Description |
|
|
1117
|
+
|---------|-------------|
|
|
1118
|
+
| [@memberjunction/communication-engine](../Communication/engine/README.md) | Multi-channel communication framework |
|
|
1322
1119
|
|
|
1323
1120
|
### Actions
|
|
1324
1121
|
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1122
|
+
| Package | Description |
|
|
1123
|
+
|---------|-------------|
|
|
1124
|
+
| [@memberjunction/actions](../Actions/Engine/README.md) | Business logic action framework |
|
|
1328
1125
|
|
|
1329
|
-
|
|
1126
|
+
---
|
|
1330
1127
|
|
|
1331
|
-
##
|
|
1128
|
+
## Breaking Changes
|
|
1332
1129
|
|
|
1333
|
-
|
|
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
|
-
###
|
|
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
|
-
|
|
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
|
-
###
|
|
1144
|
+
### v2.52.0
|
|
1145
|
+
- **LoadFromData() is now async**: Update calls to use `await`.
|
|
1340
1146
|
|
|
1341
|
-
|
|
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
|
-
|
|
1149
|
+
## TypeScript Support
|
|
1350
1150
|
|
|
1351
|
-
|
|
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
|
-
|
|
1153
|
+
## License
|
|
1358
1154
|
|
|
1359
|
-
|
|
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
|
-
|
|
1157
|
+
## Virtual Entities
|
|
1368
1158
|
|
|
1369
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1374
|
-
|
|
1168
|
+
subgraph Virtual["Virtual Entity"]
|
|
1169
|
+
VV[SQL View Only] --> VE[Entity Metadata]
|
|
1170
|
+
end
|
|
1375
1171
|
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
const dataset = await md.GetDatasetByName('MJ_Metadata');
|
|
1172
|
+
RE --> API[GraphQL API / RunView]
|
|
1173
|
+
VE --> API
|
|
1379
1174
|
|
|
1380
|
-
|
|
1381
|
-
|
|
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
|
-
|
|
1389
|
-
Retrieves and caches the dataset, using cached version if up-to-date:
|
|
1179
|
+
### Key Properties
|
|
1390
1180
|
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1407
|
-
|
|
1408
|
-
if (
|
|
1409
|
-
|
|
1410
|
-
|
|
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
|
-
|
|
1415
|
-
Removes a dataset from local cache:
|
|
1207
|
+
### Using Virtual Entities
|
|
1416
1208
|
|
|
1417
1209
|
```typescript
|
|
1418
|
-
|
|
1419
|
-
await md.ClearDatasetCache('ProductCatalog');
|
|
1210
|
+
import { Metadata, RunView } from '@memberjunction/core';
|
|
1420
1211
|
|
|
1421
|
-
//
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
|
|
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
|
-
|
|
1430
|
-
const
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
|
|
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
|
-
|
|
1226
|
+
// Save() and Delete() will throw — virtual entities are read-only
|
|
1442
1227
|
```
|
|
1443
1228
|
|
|
1444
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
|
|
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
|
-
###
|
|
1258
|
+
### How It Works
|
|
1487
1259
|
|
|
1488
|
-
|
|
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
|
-
###
|
|
1262
|
+
### EntityInfo IS-A Properties
|
|
1495
1263
|
|
|
1496
1264
|
```typescript
|
|
1497
|
-
|
|
1498
|
-
const
|
|
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
|
-
|
|
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
|
-
|
|
1549
|
-
//
|
|
1550
|
-
|
|
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
|
-
###
|
|
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
|
-
|
|
1281
|
+
For IS-A child entities, parent fields are automatically routed to the parent entity:
|
|
1610
1282
|
|
|
1611
1283
|
```typescript
|
|
1612
|
-
|
|
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
|
-
|
|
1636
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
###
|
|
1297
|
+
### Save & Delete Orchestration
|
|
1652
1298
|
|
|
1653
|
-
|
|
1654
|
-
- **
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1306
|
+
For detailed guides on specific topics, see the [docs/](./docs/) folder:
|
|
1705
1307
|
|
|
1706
|
-
|
|
1707
|
-
|
|
1708
|
-
|
|
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).
|