@memberjunction/graphql-dataprovider 2.42.1 → 2.44.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/README.md +299 -129
  2. package/package.json +5 -5
package/README.md CHANGED
@@ -1,12 +1,12 @@
1
1
  # MemberJunction GraphQL Data Provider
2
2
 
3
- A flexible GraphQL client for MemberJunction that provides a complete data access layer for connecting applications to MemberJunction APIs.
3
+ A comprehensive GraphQL client for MemberJunction that provides a complete data access layer for connecting applications to MemberJunction APIs.
4
4
 
5
5
  ## Overview
6
6
 
7
7
  The `@memberjunction/graphql-dataprovider` package is a full-featured GraphQL client implementation for MemberJunction applications. It provides a standardized way to interact with MemberJunction's GraphQL API, handling queries, mutations, subscriptions, and complex operations like transaction groups and entity relationships.
8
8
 
9
- This data provider is particularly useful for frontend applications that need to communicate with a MemberJunction backend API, offering a consistent interface regardless of the underlying database technology.
9
+ This data provider is designed for both frontend and backend applications that need to communicate with a MemberJunction API server, offering a consistent interface regardless of the underlying database technology.
10
10
 
11
11
  ## Installation
12
12
 
@@ -18,30 +18,47 @@ npm install @memberjunction/graphql-dataprovider
18
18
 
19
19
  - **Complete Entity Operations**: CRUD operations for all MemberJunction entities
20
20
  - **View and Report Execution**: Run database views and reports with parameters
21
+ - **Query Execution**: Execute custom queries with full parameter support
21
22
  - **Transaction Support**: Execute complex operations as atomic transactions
23
+ - **Action Execution**: Execute entity actions and general actions through GraphQL
22
24
  - **WebSocket Subscriptions**: Real-time data updates via GraphQL subscriptions
23
25
  - **Offline Caching**: IndexedDB-based caching for offline functionality
24
26
  - **Type Safety**: Full TypeScript support with generated types
25
27
  - **Authentication Integration**: Works with MemberJunction's authentication system
26
28
  - **Field Mapping**: Automatic mapping between client and server field names
29
+ - **Session Management**: Persistent session IDs with automatic storage
30
+ - **System User Client**: Specialized client for server-to-server communication
31
+ - **Duplicate Detection**: Built-in support for finding and merging duplicate records
27
32
 
28
33
  ## Usage
29
34
 
30
35
  ### Setting up the GraphQL Client
31
36
 
32
37
  ```typescript
33
- import { GraphQLDataProvider } from '@memberjunction/graphql-dataprovider';
34
- import { RunViewOptions } from '@memberjunction/core';
35
-
36
- // Create a data provider instance
37
- const dataProvider = new GraphQLDataProvider({
38
- graphQLEndpoint: 'https://api.example.com/graphql',
39
- subscriptionEndpoint: 'wss://api.example.com/graphql',
40
- authToken: 'your-auth-token', // Optional, can be updated later
41
- });
42
-
43
- // Set authentication token (e.g., after user login)
44
- dataProvider.setAuthToken('updated-auth-token');
38
+ import { setupGraphQLClient, GraphQLProviderConfigData } from '@memberjunction/graphql-dataprovider';
39
+
40
+ // Create configuration
41
+ const config = new GraphQLProviderConfigData(
42
+ 'your-jwt-token',
43
+ 'https://api.example.com/graphql',
44
+ 'wss://api.example.com/graphql',
45
+ async () => {
46
+ // Refresh token function - called when JWT expires
47
+ const newToken = await refreshAuthToken();
48
+ return newToken;
49
+ },
50
+ '__mj', // Optional: MJ Core schema name (defaults to '__mj')
51
+ ['schema1', 'schema2'], // Optional: Include only these schemas
52
+ ['excluded_schema'], // Optional: Exclude these schemas
53
+ 'mj-api-key' // Optional: For server-to-server communication
54
+ );
55
+
56
+ // Setup the client (returns configured instance)
57
+ const dataProvider = await setupGraphQLClient(config);
58
+
59
+ // Or create and configure manually
60
+ const dataProvider = new GraphQLDataProvider();
61
+ await dataProvider.Config(config);
45
62
  ```
46
63
 
47
64
  ### Working with Entities
@@ -61,36 +78,61 @@ async function getUserById(userId: number) {
61
78
 
62
79
  // Create a new entity
63
80
  async function createUser(userData: any) {
64
- const result = await dataProvider.saveEntity('User', {
81
+ const entityData = {
65
82
  ID: 0, // 0 indicates a new entity
66
83
  FirstName: userData.firstName,
67
84
  LastName: userData.lastName,
68
85
  Email: userData.email,
69
86
  // other fields...
70
- });
87
+ };
88
+
89
+ const options = {
90
+ IgnoreDirtyFields: false, // Save all fields
91
+ SkipValidation: false // Run validation before save
92
+ };
93
+
94
+ const result = await dataProvider.SaveEntity(
95
+ entityData,
96
+ 'User',
97
+ options
98
+ );
71
99
  return result;
72
100
  }
73
101
 
74
102
  // Update an existing entity
75
103
  async function updateUser(userId: number, updatedData: any) {
76
- const loadResult = await dataProvider.loadEntity('User', userId);
104
+ // Load the entity
105
+ const entity = await dataProvider.GetEntityObject(
106
+ 'User',
107
+ { ID: userId }
108
+ );
77
109
 
78
- if (loadResult.success) {
79
- const user = loadResult.entity;
110
+ if (entity) {
80
111
  // Update fields
81
- Object.assign(user, updatedData);
112
+ Object.assign(entity.GetData(), updatedData);
82
113
 
83
114
  // Save changes
84
- const saveResult = await dataProvider.saveEntity('User', user);
85
- return saveResult;
115
+ const result = await dataProvider.SaveEntity(
116
+ entity.GetData(),
117
+ 'User'
118
+ );
119
+ return result;
86
120
  }
87
121
 
88
- return { success: false, error: 'User not found' };
122
+ return { Success: false, Message: 'User not found' };
89
123
  }
90
124
 
91
125
  // Delete an entity
92
126
  async function deleteUser(userId: number) {
93
- const result = await dataProvider.deleteEntity('User', userId);
127
+ const options = {
128
+ IgnoreWarnings: false // Show warnings if any
129
+ };
130
+
131
+ const result = await dataProvider.DeleteEntity(
132
+ 'User',
133
+ { ID: userId },
134
+ options
135
+ );
94
136
  return result;
95
137
  }
96
138
  ```
@@ -99,34 +141,58 @@ async function deleteUser(userId: number) {
99
141
 
100
142
  ```typescript
101
143
  import { GraphQLDataProvider } from '@memberjunction/graphql-dataprovider';
102
- import { RunViewOptions } from '@memberjunction/core';
144
+ import { RunViewParams } from '@memberjunction/core';
103
145
 
104
- const dataProvider = new GraphQLDataProvider({
105
- graphQLEndpoint: 'https://api.example.com/graphql',
106
- });
146
+ const dataProvider = new GraphQLDataProvider();
107
147
 
108
148
  // Execute a view
109
149
  async function getActiveUsers() {
110
- const options: RunViewOptions = {
111
- EntityName: 'vwUsers',
150
+ const params: RunViewParams = {
151
+ EntityName: 'Users',
112
152
  ExtraFilter: "Status = 'Active'",
113
153
  OrderBy: 'LastName, FirstName',
114
- PageSize: 50,
115
- PageNumber: 1
154
+ Fields: ['ID', 'FirstName', 'LastName', 'Email'], // Optional: specific fields
155
+ IgnoreMaxRows: false,
156
+ MaxRows: 50,
157
+ ResultType: 'entity_object', // or 'simple' for raw data
158
+ ForceAuditLog: true,
159
+ AuditLogDescription: 'Loading active users for report'
116
160
  };
117
161
 
118
- const result = await dataProvider.runView(options);
119
- return result.success ? result.Results : [];
162
+ const result = await dataProvider.RunView(params);
163
+ return result.Success ? result.Results : [];
164
+ }
165
+
166
+ // Execute multiple views in parallel
167
+ async function getMultipleDatasets() {
168
+ const viewParams: RunViewParams[] = [
169
+ { EntityName: 'Users', ExtraFilter: "Status = 'Active'" },
170
+ { EntityName: 'Orders', ExtraFilter: "OrderDate >= '2024-01-01'" }
171
+ ];
172
+
173
+ const results = await dataProvider.RunViews(viewParams);
174
+ return results;
120
175
  }
121
176
 
122
177
  // Execute a report
123
- async function getSalesReport(startDate: Date, endDate: Date) {
124
- const result = await dataProvider.runReport('SalesReport', {
125
- StartDate: startDate,
126
- EndDate: endDate
127
- });
178
+ async function getSalesReport(reportId: string) {
179
+ const params = {
180
+ ReportID: reportId
181
+ };
128
182
 
129
- return result.success ? result.results : [];
183
+ const result = await dataProvider.RunReport(params);
184
+ return result.Success ? result.Results : [];
185
+ }
186
+
187
+ // Execute a query
188
+ async function runCustomQuery(queryId: string, parameters: any) {
189
+ const params = {
190
+ QueryID: queryId,
191
+ Parameters: parameters
192
+ };
193
+
194
+ const result = await dataProvider.RunQuery(params);
195
+ return result;
130
196
  }
131
197
  ```
132
198
 
@@ -150,62 +216,81 @@ class OrderTransactionGroup extends TransactionGroupBase {
150
216
  // Use the transaction group
151
217
  async function createOrderWithItems(orderData: any, items: any[]) {
152
218
  // Create transaction group
153
- const transaction = new OrderTransactionGroup();
219
+ const transaction = await dataProvider.CreateTransactionGroup();
154
220
 
155
- // Add order
156
- transaction.addEntity('Order', {
157
- ID: 0,
158
- CustomerID: orderData.customerId,
159
- OrderDate: new Date(),
160
- Status: 'New',
161
- // other fields...
162
- });
221
+ // Create order entity
222
+ const orderEntity = await dataProvider.GetEntityObject('Order');
223
+ orderEntity.NewRecord();
224
+ orderEntity.Set('CustomerID', orderData.customerId);
225
+ orderEntity.Set('OrderDate', new Date());
226
+ orderEntity.Set('Status', 'New');
227
+
228
+ // Add to transaction
229
+ const orderItem = transaction.AddTransaction(orderEntity, 'create');
163
230
 
164
- // Add order items
231
+ // Add order items with references
165
232
  for (const item of items) {
166
- transaction.addEntity('OrderItem', {
167
- ID: 0,
168
- OrderID: '@Order.1', // Reference to the first Order entity in this transaction
169
- ProductID: item.productId,
170
- Quantity: item.quantity,
171
- Price: item.price,
172
- // other fields...
173
- });
233
+ const itemEntity = await dataProvider.GetEntityObject('OrderItem');
234
+ itemEntity.NewRecord();
235
+ itemEntity.Set('ProductID', item.productId);
236
+ itemEntity.Set('Quantity', item.quantity);
237
+ itemEntity.Set('Price', item.price);
238
+
239
+ // Reference the order using a variable
240
+ const orderTransaction = transaction.AddTransaction(itemEntity, 'create');
241
+ transaction.AddVariable(
242
+ 'orderID',
243
+ 'ID',
244
+ 'FieldValue',
245
+ orderItem.BaseEntity,
246
+ orderTransaction.BaseEntity,
247
+ 'OrderID'
248
+ );
174
249
  }
175
250
 
176
251
  // Execute transaction
177
- const result = await dataProvider.executeTransactionGroup(transaction);
178
- return result;
252
+ const results = await transaction.Submit();
253
+ return results;
179
254
  }
180
255
  ```
181
256
 
182
257
  ### Executing Actions
183
258
 
184
259
  ```typescript
185
- import { GraphQLDataProvider } from '@memberjunction/graphql-dataprovider';
260
+ import { GraphQLActionClient } from '@memberjunction/graphql-dataprovider';
261
+ import { ActionParam } from '@memberjunction/actions-base';
186
262
 
187
- const dataProvider = new GraphQLDataProvider({
188
- graphQLEndpoint: 'https://api.example.com/graphql',
189
- });
263
+ const actionClient = new GraphQLActionClient(dataProvider);
190
264
 
191
- // Execute an entity action
192
- async function sendUserWelcomeEmail(userId: number) {
193
- const result = await dataProvider.executeAction('User', 'SendWelcomeEmail', userId, {
194
- templateId: 'welcome-template',
195
- includeSurvey: true
196
- });
265
+ // Execute a standalone action
266
+ async function runAction(actionId: string) {
267
+ const params: ActionParam[] = [
268
+ { Name: 'parameter1', Value: 'value1', Type: 'Input' },
269
+ { Name: 'parameter2', Value: 123, Type: 'Input' }
270
+ ];
271
+
272
+ const result = await actionClient.RunAction(
273
+ actionId,
274
+ params,
275
+ false // skipActionLog
276
+ );
197
277
 
198
- return result.success;
278
+ if (result.Success) {
279
+ console.log('Action result:', result.ResultCode);
280
+ }
281
+ return result;
199
282
  }
200
283
 
201
- // Execute a general action (not tied to a specific entity record)
202
- async function generateReport() {
203
- const result = await dataProvider.executeAction('ReportGenerator', 'GenerateMonthlyReport', null, {
204
- month: new Date().getMonth(),
205
- year: new Date().getFullYear(),
206
- format: 'PDF'
207
- });
284
+ // Execute an entity action
285
+ async function runEntityAction() {
286
+ const params = {
287
+ EntityAction: entityAction, // EntityActionEntity instance
288
+ InvocationType: { Name: 'SingleRecord' },
289
+ EntityObject: userEntity, // BaseEntity instance
290
+ ContextUser: currentUser // UserInfo instance
291
+ };
208
292
 
293
+ const result = await actionClient.RunEntityAction(params);
209
294
  return result;
210
295
  }
211
296
  ```
@@ -213,82 +298,167 @@ async function generateReport() {
213
298
  ### Field Mapping
214
299
 
215
300
  ```typescript
216
- import { GraphQLDataProvider } from '@memberjunction/graphql-dataprovider';
301
+ import { FieldMapper } from '@memberjunction/graphql-dataprovider';
217
302
 
218
- const dataProvider = new GraphQLDataProvider({
219
- graphQLEndpoint: 'https://api.example.com/graphql',
220
- // Configure field mapping for entities
221
- fieldMappings: {
222
- User: {
223
- // Map client field names to server field names
224
- firstName: 'FirstName',
225
- lastName: 'LastName',
226
- emailAddress: 'Email'
227
- }
228
- }
303
+ // The GraphQL provider automatically handles field mapping for system fields
304
+ // __mj_CreatedAt <-> _mj__CreatedAt
305
+ // __mj_UpdatedAt <-> _mj__UpdatedAt
306
+ // __mj_DeletedAt <-> _mj__DeletedAt
307
+
308
+ // You can also use the FieldMapper directly
309
+ const mapper = new FieldMapper();
310
+
311
+ // Map fields in an object
312
+ const mappedData = mapper.MapFields({
313
+ __mj_CreatedAt: '2024-01-01',
314
+ Name: 'John Doe'
229
315
  });
316
+ // Result: { _mj__CreatedAt: '2024-01-01', Name: 'John Doe' }
230
317
 
231
- // Now you can use client field names in your code
232
- async function createUser(userData: any) {
233
- const result = await dataProvider.saveEntity('User', {
234
- ID: 0,
235
- firstName: userData.firstName, // Will be mapped to FirstName
236
- lastName: userData.lastName, // Will be mapped to LastName
237
- emailAddress: userData.email // Will be mapped to Email
238
- });
239
- return result;
240
- }
318
+ // Map individual field names
319
+ const mappedField = mapper.MapFieldName('__mj_CreatedAt');
320
+ // Result: '_mj__CreatedAt'
321
+
322
+ // Reverse mapping
323
+ const originalField = mapper.ReverseMapFieldName('_mj__CreatedAt');
324
+ // Result: '__mj_CreatedAt'
241
325
  ```
242
326
 
243
327
  ### WebSocket Subscriptions
244
328
 
245
329
  ```typescript
246
330
  import { GraphQLDataProvider } from '@memberjunction/graphql-dataprovider';
247
-
248
- const dataProvider = new GraphQLDataProvider({
249
- graphQLEndpoint: 'https://api.example.com/graphql',
250
- subscriptionEndpoint: 'wss://api.example.com/graphql'
251
- });
252
-
253
- // Subscribe to entity changes
254
- function subscribeToUserChanges() {
255
- const subscription = dataProvider.subscribeToEntity('User', (entity) => {
256
- console.log('User entity updated:', entity);
257
- // Update UI or application state
331
+ import { Observable } from 'rxjs';
332
+
333
+ const dataProvider = new GraphQLDataProvider();
334
+
335
+ // Subscribe to record changes
336
+ function subscribeToRecordChanges() {
337
+ const observable: Observable<RecordChange[]> =
338
+ await dataProvider.GetRecordChanges(
339
+ 'User',
340
+ { ID: 123 },
341
+ ['update', 'delete'], // Watch for these operations
342
+ true // Return initial values
343
+ );
344
+
345
+ const subscription = observable.subscribe(changes => {
346
+ console.log('Record changes:', changes);
347
+ // Handle changes
258
348
  });
259
349
 
260
- // Later, unsubscribe when no longer needed
350
+ // Later, unsubscribe
261
351
  subscription.unsubscribe();
262
352
  }
353
+ ```
263
354
 
264
- // Subscribe to specific entity record changes
265
- function subscribeToSpecificUser(userId: number) {
266
- const subscription = dataProvider.subscribeToEntityRecord('User', userId, (entity) => {
267
- console.log('Specific user updated:', entity);
268
- // Update UI or application state
269
- });
270
-
271
- // Later, unsubscribe when no longer needed
272
- subscription.unsubscribe();
355
+ ### System User Client
356
+
357
+ ```typescript
358
+ import { GraphQLSystemUserClient } from '@memberjunction/graphql-dataprovider';
359
+
360
+ // Create system user client for server-to-server communication
361
+ const systemClient = new GraphQLSystemUserClient(
362
+ 'https://api.example.com/graphql',
363
+ '', // No JWT token needed
364
+ 'session-id',
365
+ 'mj-api-key' // Shared secret key
366
+ );
367
+
368
+ // Execute queries as system user
369
+ const queries = [
370
+ 'SELECT * FROM Users WHERE Active = 1',
371
+ 'SELECT COUNT(*) as Total FROM Orders'
372
+ ];
373
+
374
+ const result = await systemClient.GetData(
375
+ queries,
376
+ 'access-token' // Short-lived access token
377
+ );
378
+
379
+ if (result.Success) {
380
+ console.log('Query results:', result.Results);
273
381
  }
382
+
383
+ // Sync roles and users
384
+ const syncResult = await systemClient.SyncRolesAndUsers({
385
+ Roles: [
386
+ { ID: '1', Name: 'Admin', Description: 'Administrator role' }
387
+ ],
388
+ Users: [
389
+ {
390
+ ID: '1',
391
+ Name: 'john.doe',
392
+ Email: 'john@example.com',
393
+ Type: 'User',
394
+ FirstName: 'John',
395
+ LastName: 'Doe',
396
+ Roles: [{ ID: '1', Name: 'Admin', Description: 'Administrator role' }]
397
+ }
398
+ ]
399
+ });
274
400
  ```
275
401
 
276
- ## Key Classes
402
+ ## Key Classes and Types
277
403
 
278
- | Class | Description |
404
+ | Class/Type | Description |
279
405
  |-------|-------------|
280
- | `GraphQLDataProvider` | Main class implementing the MemberJunction data provider interface |
281
- | `GraphQLClient` | Handles GraphQL operations (queries, mutations, subscriptions) |
282
- | `EntityMapper` | Maps between client-side and server-side entity structures |
283
- | `SubscriptionManager` | Manages WebSocket connections and GraphQL subscriptions |
284
- | `OfflineCache` | Provides IndexedDB-based caching for offline operations |
285
- | `EntitySerializer` | Serializes and deserializes entity data for GraphQL operations |
406
+ | `GraphQLDataProvider` | Main class implementing IEntityDataProvider, IMetadataProvider, IRunViewProvider, IRunReportProvider, and IRunQueryProvider interfaces |
407
+ | `GraphQLProviderConfigData` | Configuration class for setting up the GraphQL provider with authentication and connection details |
408
+ | `GraphQLActionClient` | Client for executing actions and entity actions through GraphQL |
409
+ | `GraphQLSystemUserClient` | Specialized client for server-to-server communication using API keys |
410
+ | `GraphQLTransactionGroup` | Manages complex multi-entity transactions with variable support |
411
+ | `FieldMapper` | Handles automatic field name mapping between client and server |
412
+ | `setupGraphQLClient` | Helper function to quickly setup and configure the GraphQL client |
413
+
414
+ ## API Documentation
415
+
416
+ ### Core Methods
417
+
418
+ #### Entity Operations
419
+ - `GetEntityObject(entityName: string, compositeKey?: CompositeKey)` - Get an entity object instance
420
+ - `SaveEntity(entityData: any, entityName: string, options?: EntitySaveOptions)` - Save entity data
421
+ - `DeleteEntity(entityName: string, compositeKey: CompositeKey, options?: EntityDeleteOptions)` - Delete an entity
422
+ - `GetRecordChanges(entityName: string, compositeKey: CompositeKey, operations: string[], includeInitial: boolean)` - Subscribe to entity changes
423
+
424
+ #### View and Query Operations
425
+ - `RunView(params: RunViewParams)` - Execute a single view
426
+ - `RunViews(params: RunViewParams[])` - Execute multiple views in parallel
427
+ - `RunReport(params: RunReportParams)` - Execute a report
428
+ - `RunQuery(params: RunQueryParams)` - Execute a custom query
429
+
430
+ #### Transaction Operations
431
+ - `CreateTransactionGroup()` - Create a new transaction group
432
+ - `ExecuteTransaction(transaction: TransactionGroupBase)` - Execute a transaction group
433
+
434
+ #### Duplicate Detection
435
+ - `GetRecordDuplicates(request: PotentialDuplicateRequest)` - Find potential duplicate records
436
+ - `MergeRecords(request: RecordMergeRequest)` - Merge duplicate records
437
+
438
+ #### Metadata Operations
439
+ - `GetEntityRecordName(entityName: string, compositeKey: CompositeKey)` - Get display name for a record
440
+ - `GetEntityRecordNames(info: EntityRecordNameInput[])` - Get display names for multiple records
441
+ - `GetEntityDependencies(entityName: string, compositeKey: CompositeKey)` - Get record dependencies
442
+
443
+ ## Dependencies
444
+
445
+ - `@memberjunction/core` - Core MemberJunction functionality
446
+ - `@memberjunction/core-entities` - Entity definitions
447
+ - `@memberjunction/actions-base` - Action system base classes
448
+ - `@memberjunction/global` - Global utilities
449
+ - `graphql` - GraphQL language and execution
450
+ - `graphql-request` - Minimal GraphQL client
451
+ - `graphql-ws` - GraphQL WebSocket client for subscriptions
452
+ - `@tempfix/idb` - IndexedDB wrapper for offline storage
453
+ - `rxjs` - Reactive extensions for subscriptions
454
+ - `uuid` - UUID generation for session IDs
286
455
 
287
456
  ## Requirements
288
457
 
289
458
  - Node.js 16+
290
459
  - Modern browser with WebSocket support (for subscriptions)
291
460
  - MemberJunction GraphQL API endpoint
461
+ - Valid JWT token or MJ API key for authentication
292
462
 
293
463
  ## License
294
464
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@memberjunction/graphql-dataprovider",
3
- "version": "2.42.1",
3
+ "version": "2.44.0",
4
4
  "description": "MemberJunction: GraphQL Client Data Provider",
5
5
  "main": "./dist/index.cjs",
6
6
  "module": "./dist/index.mjs",
@@ -34,10 +34,10 @@
34
34
  "@esbuild/win32-x64": "0.24.0"
35
35
  },
36
36
  "dependencies": {
37
- "@memberjunction/actions-base": "2.42.1",
38
- "@memberjunction/core": "2.42.1",
39
- "@memberjunction/core-entities": "2.42.1",
40
- "@memberjunction/global": "2.42.1",
37
+ "@memberjunction/actions-base": "2.44.0",
38
+ "@memberjunction/core": "2.44.0",
39
+ "@memberjunction/core-entities": "2.44.0",
40
+ "@memberjunction/global": "2.44.0",
41
41
  "graphql": "^16.8.0",
42
42
  "graphql-request": "^5.2.0",
43
43
  "graphql-ws": "^5.14.0",