@memberjunction/server 2.128.0 → 2.129.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 (56) hide show
  1. package/dist/agents/skip-sdk.d.ts.map +1 -1
  2. package/dist/agents/skip-sdk.js +5 -1
  3. package/dist/agents/skip-sdk.js.map +1 -1
  4. package/dist/auth/index.d.ts.map +1 -1
  5. package/dist/auth/index.js +2 -3
  6. package/dist/auth/index.js.map +1 -1
  7. package/dist/config.d.ts +33 -0
  8. package/dist/config.d.ts.map +1 -1
  9. package/dist/config.js +5 -0
  10. package/dist/config.js.map +1 -1
  11. package/dist/generated/generated.d.ts +9531 -9131
  12. package/dist/generated/generated.d.ts.map +1 -1
  13. package/dist/generated/generated.js +49224 -46720
  14. package/dist/generated/generated.js.map +1 -1
  15. package/dist/generic/ResolverBase.d.ts +4 -2
  16. package/dist/generic/ResolverBase.d.ts.map +1 -1
  17. package/dist/generic/ResolverBase.js +122 -10
  18. package/dist/generic/ResolverBase.js.map +1 -1
  19. package/dist/generic/RunViewResolver.d.ts +22 -0
  20. package/dist/generic/RunViewResolver.d.ts.map +1 -1
  21. package/dist/generic/RunViewResolver.js +171 -0
  22. package/dist/generic/RunViewResolver.js.map +1 -1
  23. package/dist/index.d.ts +1 -0
  24. package/dist/index.d.ts.map +1 -1
  25. package/dist/index.js +20 -4
  26. package/dist/index.js.map +1 -1
  27. package/dist/resolvers/AskSkipResolver.d.ts.map +1 -1
  28. package/dist/resolvers/AskSkipResolver.js +5 -1
  29. package/dist/resolvers/AskSkipResolver.js.map +1 -1
  30. package/dist/resolvers/QueryResolver.d.ts +37 -0
  31. package/dist/resolvers/QueryResolver.d.ts.map +1 -1
  32. package/dist/resolvers/QueryResolver.js +304 -2
  33. package/dist/resolvers/QueryResolver.js.map +1 -1
  34. package/dist/resolvers/SyncDataResolver.js +5 -5
  35. package/dist/resolvers/SyncDataResolver.js.map +1 -1
  36. package/dist/resolvers/TelemetryResolver.d.ts +120 -0
  37. package/dist/resolvers/TelemetryResolver.d.ts.map +1 -0
  38. package/dist/resolvers/TelemetryResolver.js +731 -0
  39. package/dist/resolvers/TelemetryResolver.js.map +1 -0
  40. package/dist/services/TaskOrchestrator.d.ts.map +1 -1
  41. package/dist/services/TaskOrchestrator.js.map +1 -1
  42. package/package.json +43 -42
  43. package/src/agents/skip-sdk.ts +5 -1
  44. package/src/auth/index.ts +2 -3
  45. package/src/config.ts +9 -0
  46. package/src/generated/generated.ts +28167 -26581
  47. package/src/generic/ResolverBase.ts +201 -12
  48. package/src/generic/RunViewResolver.ts +157 -3
  49. package/src/index.ts +25 -5
  50. package/src/resolvers/AskSkipResolver.ts +18 -20
  51. package/src/resolvers/QueryResolver.ts +276 -2
  52. package/src/resolvers/RunAIAgentResolver.ts +2 -2
  53. package/src/resolvers/RunAIPromptResolver.ts +1 -1
  54. package/src/resolvers/SyncDataResolver.ts +5 -5
  55. package/src/resolvers/TelemetryResolver.ts +567 -0
  56. package/src/services/TaskOrchestrator.ts +2 -1
@@ -26,7 +26,8 @@ import { httpTransport, CloudEvent, emitterFor } from 'cloudevents';
26
26
  import { RunViewGenericParams, UserPayload } from '../types.js';
27
27
  import { RunDynamicViewInput, RunViewByIDInput, RunViewByNameInput } from './RunViewResolver.js';
28
28
  import { DeleteOptionsInput } from './DeleteOptionsInput.js';
29
- import { MJEvent, MJEventType, MJGlobal } from '@memberjunction/global';
29
+ import { MJEvent, MJEventType, MJGlobal, ENCRYPTED_SENTINEL, IsValueEncrypted, IsOnlyTimezoneShift } from '@memberjunction/global';
30
+ import { EncryptionEngine } from '@memberjunction/encryption';
30
31
  import { PUSH_STATUS_UPDATES_TOPIC } from './PushStatusResolver.js';
31
32
  import { FieldMapper } from '@memberjunction/graphql-dataprovider';
32
33
  import { Subscription } from 'rxjs';
@@ -47,7 +48,20 @@ export class ResolverBase {
47
48
  return g[ResolverBase._eventSubscriptionKey];
48
49
  }
49
50
 
50
- protected MapFieldNamesToCodeNames(entityName: string, dataObject: any) {
51
+ /**
52
+ * Maps field names to their GraphQL-safe CodeNames and handles encryption for API responses.
53
+ *
54
+ * For encrypted fields coming from raw SQL queries (not entity objects):
55
+ * - AllowDecryptInAPI=true: Decrypt the value before sending to client
56
+ * - AllowDecryptInAPI=false + SendEncryptedValue=true: Keep encrypted ciphertext
57
+ * - AllowDecryptInAPI=false + SendEncryptedValue=false: Replace with sentinel
58
+ *
59
+ * @param entityName - The entity name
60
+ * @param dataObject - The data object with field values
61
+ * @param contextUser - Optional user context for decryption (required for encrypted fields)
62
+ * @returns The processed data object
63
+ */
64
+ protected async MapFieldNamesToCodeNames(entityName: string, dataObject: any, contextUser?: UserInfo): Promise<any> {
51
65
  // for the given entity name provided, check to see if there are any fields
52
66
  // where the code name is different from the field name, and for just those
53
67
  // fields, iterate through the dataObject and REPLACE the property that has the field name
@@ -69,17 +83,153 @@ export class ResolverBase {
69
83
  }
70
84
  }
71
85
  });
86
+
87
+ // Handle encrypted fields - data from raw SQL queries is still encrypted
88
+ const encryptedFields = entityInfo.EncryptedFields;
89
+ if (encryptedFields.length > 0) {
90
+ for (const field of encryptedFields) {
91
+ const fieldName = field.CodeName;
92
+ const value = dataObject[fieldName];
93
+
94
+ // Skip null/undefined values
95
+ if (value === null || value === undefined) continue;
96
+
97
+ // Check if value is encrypted (raw SQL returns encrypted values)
98
+ const engine = EncryptionEngine.Instance;
99
+ await engine.Config(false, contextUser);
100
+ const keyMarker = field.EncryptionKeyID ? engine.GetKeyByID(field.EncryptionKeyID)?.Marker : undefined;
101
+ if (typeof value === 'string' && IsValueEncrypted(value, keyMarker)) {
102
+ if (field.AllowDecryptInAPI) {
103
+ // Decrypt the value for the client
104
+ if (contextUser) {
105
+ try {
106
+ const decryptedValue = await engine.Decrypt(value, contextUser);
107
+ dataObject[fieldName] = decryptedValue;
108
+ } catch (err) {
109
+ LogError(`Failed to decrypt field ${fieldName} for API response: ${err}`);
110
+ // On decryption failure, use sentinel for safety
111
+ dataObject[fieldName] = ENCRYPTED_SENTINEL;
112
+ }
113
+ } else {
114
+ // No context user, can't decrypt - use sentinel
115
+ dataObject[fieldName] = ENCRYPTED_SENTINEL;
116
+ }
117
+ } else if (!field.SendEncryptedValue) {
118
+ // AllowDecryptInAPI=false and SendEncryptedValue=false - use sentinel
119
+ dataObject[fieldName] = ENCRYPTED_SENTINEL;
120
+ }
121
+ // else: AllowDecryptInAPI=false and SendEncryptedValue=true - keep encrypted value as-is
122
+ }
123
+ }
124
+ }
72
125
  }
73
126
  return dataObject;
74
127
  }
75
128
 
76
- protected ArrayMapFieldNamesToCodeNames(entityName: string, dataObjectArray: any[]) {
129
+ protected async ArrayMapFieldNamesToCodeNames(entityName: string, dataObjectArray: any[], contextUser?: UserInfo): Promise<any[]> {
77
130
  // iterate through the array and call MapFieldNamesToCodeNames for each element
78
131
  if (dataObjectArray && dataObjectArray.length > 0) {
79
- dataObjectArray.forEach((element) => {
80
- this.MapFieldNamesToCodeNames(entityName, element);
81
- });
132
+ for (const element of dataObjectArray) {
133
+ await this.MapFieldNamesToCodeNames(entityName, element, contextUser);
134
+ }
135
+ }
136
+ return dataObjectArray;
137
+ }
138
+
139
+ /**
140
+ * Filters encrypted field values before sending to the API client.
141
+ *
142
+ * For each encrypted field in the entity:
143
+ * - If AllowDecryptInAPI is true: value passes through unchanged (already decrypted by data provider)
144
+ * - If AllowDecryptInAPI is false and SendEncryptedValue is true: re-encrypt and send ciphertext
145
+ * - If AllowDecryptInAPI is false and SendEncryptedValue is false: replace with sentinel value
146
+ *
147
+ * @param entityName - Name of the entity
148
+ * @param dataObject - The data object containing field values
149
+ * @param encryptionEngine - Optional encryption engine for re-encryption (lazy loaded if needed)
150
+ * @param contextUser - User context for encryption operations
151
+ * @returns The filtered data object
152
+ */
153
+ protected async FilterEncryptedFieldsForAPI(
154
+ entityName: string,
155
+ dataObject: Record<string, unknown>,
156
+ contextUser: UserInfo
157
+ ): Promise<Record<string, unknown>> {
158
+ if (!dataObject) return dataObject;
159
+
160
+ const md = new Metadata();
161
+ const entityInfo = md.Entities.find((e) => e.Name === entityName);
162
+ if (!entityInfo) return dataObject;
163
+
164
+ // Find all encrypted fields that need filtering
165
+ const encryptedFields = entityInfo.EncryptedFields;
166
+ if (encryptedFields.length === 0) return dataObject;
167
+
168
+ // Process each encrypted field
169
+ for (const field of encryptedFields) {
170
+ const fieldName = field.CodeName;
171
+ const value = dataObject[fieldName];
172
+
173
+ // Skip null/undefined values
174
+ if (value === null || value === undefined) continue;
175
+
176
+ // If AllowDecryptInAPI is true, the decrypted value passes through
177
+ if (field.AllowDecryptInAPI) continue;
178
+
179
+ // AllowDecryptInAPI is false - we need to filter the value
180
+ const engine = EncryptionEngine.Instance;
181
+ await engine.Config(false, contextUser);
182
+ const keyMarker = field.EncryptionKeyID ? engine.GetKeyByID(field.EncryptionKeyID)?.Marker : undefined;
183
+ if (field.SendEncryptedValue) {
184
+ // Re-encrypt the value before sending
185
+ // Only re-encrypt if it's not already encrypted (data provider decrypted it)
186
+ if (typeof value === 'string' && !IsValueEncrypted(value, keyMarker)) {
187
+ try {
188
+ const encryptedValue = await engine.Encrypt(
189
+ value,
190
+ field.EncryptionKeyID as string,
191
+ contextUser
192
+ );
193
+ dataObject[fieldName] = encryptedValue;
194
+ } catch (err) {
195
+ // If re-encryption fails, use sentinel for safety
196
+ LogError(`Failed to re-encrypt field ${fieldName} for API response: ${err}`);
197
+ dataObject[fieldName] = ENCRYPTED_SENTINEL;
198
+ }
199
+ }
200
+ // If already encrypted (shouldn't happen normally), keep as-is
201
+ } else {
202
+ // SendEncryptedValue is false - replace with sentinel
203
+ dataObject[fieldName] = ENCRYPTED_SENTINEL;
204
+ }
82
205
  }
206
+
207
+ return dataObject;
208
+ }
209
+
210
+ /**
211
+ * Filters encrypted fields for an array of data objects
212
+ */
213
+ protected async ArrayFilterEncryptedFieldsForAPI(
214
+ entityName: string,
215
+ dataObjectArray: Record<string, unknown>[],
216
+ contextUser: UserInfo
217
+ ): Promise<Record<string, unknown>[]> {
218
+ if (!dataObjectArray || dataObjectArray.length === 0) return dataObjectArray;
219
+
220
+ // Check if entity has any encrypted fields first to avoid unnecessary processing
221
+ const md = new Metadata();
222
+ const entityInfo = md.Entities.find((e) => e.Name === entityName);
223
+ if (!entityInfo) return dataObjectArray;
224
+
225
+ const encryptedFields = entityInfo.Fields.filter(f => f.Encrypt && !f.AllowDecryptInAPI);
226
+ if (encryptedFields.length === 0) return dataObjectArray;
227
+
228
+ // Process each element
229
+ for (const element of dataObjectArray) {
230
+ await this.FilterEncryptedFieldsForAPI(entityName, element, contextUser);
231
+ }
232
+
83
233
  return dataObjectArray;
84
234
  }
85
235
 
@@ -438,8 +588,14 @@ export class ResolverBase {
438
588
  for (const r of result.Results) {
439
589
  mapper.MapFields(r);
440
590
  }
591
+ // Filter encrypted fields before sending to API client
592
+ await this.ArrayFilterEncryptedFieldsForAPI(
593
+ viewInfo.Entity,
594
+ result.Results as Record<string, unknown>[],
595
+ user
596
+ );
441
597
  }
442
-
598
+
443
599
  return result;
444
600
  } catch (err) {
445
601
  // Fix #9: Improved error handling with structured logging
@@ -534,11 +690,22 @@ export class ResolverBase {
534
690
 
535
691
  // Process results
536
692
  const mapper = new FieldMapper();
537
- for (const runViewResult of runViewResults) {
693
+ for (let i = 0; i < runViewResults.length; i++) {
694
+ const runViewResult = runViewResults[i];
538
695
  if (runViewResult?.Success && runViewResult.Results?.length) {
539
696
  for (const result of runViewResult.Results) {
540
697
  mapper.MapFields(result);
541
698
  }
699
+ // Filter encrypted fields before sending to API client
700
+ // Use the corresponding param's entity name
701
+ const entityName = params[i]?.viewInfo?.Entity;
702
+ if (entityName && contextUser) {
703
+ await this.ArrayFilterEncryptedFieldsForAPI(
704
+ entityName,
705
+ runViewResult.Results as Record<string, unknown>[],
706
+ contextUser
707
+ );
708
+ }
542
709
  }
543
710
  }
544
711
 
@@ -720,7 +887,9 @@ export class ResolverBase {
720
887
  if (await entityObject.Save()) {
721
888
  // save worked, fire the AfterCreate event and then return all the data
722
889
  await this.AfterCreate(provider, input); // fire event
723
- return this.MapFieldNamesToCodeNames(entityName, entityObject.GetAll());
890
+ const contextUser = this.GetUserFromPayload(userPayload);
891
+ // MapFieldNamesToCodeNames now handles encryption filtering as well
892
+ return await this.MapFieldNamesToCodeNames(entityName, entityObject.GetAll(), contextUser);
724
893
  }
725
894
  // save failed, return null
726
895
  else throw entityObject.LatestResult?.Message;
@@ -790,8 +959,9 @@ export class ResolverBase {
790
959
  if (await entityObject.Save()) {
791
960
  // save worked, fire afterevent and return all the data
792
961
  await this.AfterUpdate(provider, input); // fire event
793
-
794
- return this.MapFieldNamesToCodeNames(entityName, entityObject.GetAll());
962
+
963
+ // MapFieldNamesToCodeNames now handles encryption filtering as well
964
+ return await this.MapFieldNamesToCodeNames(entityName, entityObject.GetAll(), userInfo);
795
965
  } else {
796
966
  throw new GraphQLError(entityObject.LatestResult?.Message ?? 'Unknown error', {
797
967
  extensions: { code: 'SAVE_ENTITY_ERROR', entityName },
@@ -884,7 +1054,26 @@ export class ResolverBase {
884
1054
  break;
885
1055
  case 'object':
886
1056
  if (clientOldValues[key] instanceof Date) {
887
- different = clientOldValues[key].getTime() !== dbValues[key].getTime();
1057
+ const clientDate = clientOldValues[key] as Date;
1058
+ const dbDate = dbValues[key];
1059
+ if (dbDate == null || !(dbDate instanceof Date)) {
1060
+ different = true;
1061
+ } else if (clientDate.getTime() !== dbDate.getTime()) {
1062
+ // Check if this is a datetime/datetime2 field with only a timezone hour shift
1063
+ const sqlType = f?.SQLFullType?.trim().toLowerCase() || '';
1064
+ if (sqlType !== 'datetimeoffset' && IsOnlyTimezoneShift(clientDate, dbDate)) {
1065
+ console.warn(
1066
+ `Timezone hour shift detected on field "${key}" (${sqlType || 'datetime'}). ` +
1067
+ `Client: ${clientDate.toISOString()}, DB: ${dbDate.toISOString()}. ` +
1068
+ `Consider using datetimeoffset to avoid timezone ambiguity.`
1069
+ );
1070
+ different = false; // Allow timezone shifts through for non-datetimeoffset fields
1071
+ } else {
1072
+ different = true;
1073
+ }
1074
+ }
1075
+ } else if (clientOldValues[key] === null) {
1076
+ different = dbValues[key] !== null;
888
1077
  }
889
1078
  break;
890
1079
  default:
@@ -1,11 +1,12 @@
1
1
  import { Arg, Ctx, Field, InputType, Int, ObjectType, PubSubEngine, Query, Resolver } from 'type-graphql';
2
2
  import { AppContext } from '../types.js';
3
3
  import { ResolverBase } from './ResolverBase.js';
4
- import { LogError, LogStatus, EntityInfo } from '@memberjunction/core';
4
+ import { LogError, LogStatus, EntityInfo, RunViewWithCacheCheckResult, RunViewsWithCacheCheckResponse, RunViewWithCacheCheckParams } from '@memberjunction/core';
5
5
  import { RequireSystemUser } from '../directives/RequireSystemUser.js';
6
6
  import { GetReadOnlyProvider } from '../util.js';
7
7
  import { UserViewEntityExtended } from '@memberjunction/core-entities';
8
8
  import { KeyValuePairOutputType } from './KeyInputOutputTypes.js';
9
+ import { SQLServerDataProvider } from '@memberjunction/sqlserver-dataprovider';
9
10
 
10
11
  /********************************************************************************
11
12
  * The PURPOSE of this resolver is to provide a generic way to run a view and return the results.
@@ -383,10 +384,75 @@ export class RunViewGenericInput {
383
384
  StartRow?: number;
384
385
  }
385
386
 
387
+ //****************************************************************************
388
+ // INPUT/OUTPUT TYPES for RunViewsWithCacheCheck
389
+ //****************************************************************************
390
+
391
+ @InputType()
392
+ export class RunViewCacheStatusInput {
393
+ @Field(() => String, { description: 'The maximum __mj_UpdatedAt value from cached results' })
394
+ maxUpdatedAt: string;
395
+
396
+ @Field(() => Int, { description: 'The number of rows in cached results' })
397
+ rowCount: number;
398
+ }
399
+
400
+ @InputType()
401
+ export class RunViewWithCacheCheckInput {
402
+ @Field(() => RunDynamicViewInput, { description: 'The RunView parameters' })
403
+ params: RunDynamicViewInput;
404
+
405
+ @Field(() => RunViewCacheStatusInput, {
406
+ nullable: true,
407
+ description: 'Optional cache status - if provided, server will check if cache is current'
408
+ })
409
+ cacheStatus?: RunViewCacheStatusInput;
410
+ }
411
+
412
+ @ObjectType()
413
+ export class RunViewWithCacheCheckResultOutput {
414
+ @Field(() => Int, { description: 'The index of this view in the batch request' })
415
+ viewIndex: number;
416
+
417
+ @Field(() => String, { description: "'current', 'stale', or 'error'" })
418
+ status: string;
419
+
420
+ @Field(() => [RunViewGenericResultRow], {
421
+ nullable: true,
422
+ description: 'Fresh results - only populated when status is stale'
423
+ })
424
+ Results?: RunViewGenericResultRow[];
425
+
426
+ @Field(() => String, { nullable: true, description: 'Max __mj_UpdatedAt from results when stale' })
427
+ maxUpdatedAt?: string;
428
+
429
+ @Field(() => Int, { nullable: true, description: 'Row count of results when stale' })
430
+ rowCount?: number;
431
+
432
+ @Field(() => String, { nullable: true, description: 'Error message if status is error' })
433
+ errorMessage?: string;
434
+ }
435
+
436
+ @ObjectType()
437
+ export class RunViewsWithCacheCheckOutput {
438
+ @Field(() => Boolean, { description: 'Whether the overall operation succeeded' })
439
+ success: boolean;
440
+
441
+ @Field(() => [RunViewWithCacheCheckResultOutput], { description: 'Results for each view in the batch' })
442
+ results: RunViewWithCacheCheckResultOutput[];
443
+
444
+ @Field(() => String, { nullable: true, description: 'Overall error message if success is false' })
445
+ errorMessage?: string;
446
+ }
447
+
448
+ //****************************************************************************
449
+ // OUTPUT TYPES for RunView Results
450
+ //****************************************************************************
451
+
386
452
  @ObjectType()
387
453
  export class RunViewResultRow {
388
- @Field(() => [KeyValuePairOutputType], {
389
- description: 'Primary key values for the record'
454
+ @Field(() => [KeyValuePairOutputType], {
455
+ description: 'Primary key values for the record'
390
456
  })
391
457
  PrimaryKey: KeyValuePairOutputType[];
392
458
 
@@ -771,6 +837,94 @@ export class RunViewResolver extends ResolverBase {
771
837
  }
772
838
  }
773
839
 
840
+ /**
841
+ * RunViewsWithCacheCheck - Smart cache validation for batch RunViews.
842
+ * For each view, if cacheStatus is provided, the server checks if the cache is current.
843
+ * If current, returns status='current' with no data. If stale, returns status='stale' with fresh data.
844
+ */
845
+ @Query(() => RunViewsWithCacheCheckOutput)
846
+ async RunViewsWithCacheCheck(
847
+ @Arg('input', () => [RunViewWithCacheCheckInput]) input: RunViewWithCacheCheckInput[],
848
+ @Ctx() { providers, userPayload }: AppContext
849
+ ): Promise<RunViewsWithCacheCheckOutput> {
850
+ try {
851
+ const provider = GetReadOnlyProvider(providers, { allowFallbackToReadWrite: true });
852
+
853
+ // Cast provider to SQLServerDataProvider to access RunViewsWithCacheCheck method
854
+ const sqlProvider = provider as unknown as SQLServerDataProvider;
855
+ if (!sqlProvider.RunViewsWithCacheCheck) {
856
+ throw new Error('Provider does not support RunViewsWithCacheCheck');
857
+ }
858
+
859
+ // Convert GraphQL input types to core types
860
+ const coreParams: RunViewWithCacheCheckParams[] = input.map(item => ({
861
+ params: {
862
+ EntityName: item.params.EntityName,
863
+ ExtraFilter: item.params.ExtraFilter,
864
+ OrderBy: item.params.OrderBy,
865
+ Fields: item.params.Fields,
866
+ UserSearchString: item.params.UserSearchString,
867
+ ExcludeUserViewRunID: item.params.ExcludeUserViewRunID,
868
+ OverrideExcludeFilter: item.params.OverrideExcludeFilter,
869
+ IgnoreMaxRows: item.params.IgnoreMaxRows,
870
+ MaxRows: item.params.MaxRows,
871
+ ForceAuditLog: item.params.ForceAuditLog,
872
+ AuditLogDescription: item.params.AuditLogDescription,
873
+ ResultType: (item.params.ResultType || 'simple') as 'simple' | 'entity_object' | 'count_only',
874
+ StartRow: item.params.StartRow,
875
+ },
876
+ cacheStatus: item.cacheStatus ? {
877
+ maxUpdatedAt: item.cacheStatus.maxUpdatedAt,
878
+ rowCount: item.cacheStatus.rowCount,
879
+ } : undefined,
880
+ }));
881
+
882
+ const response = await sqlProvider.RunViewsWithCacheCheck(coreParams, userPayload.userRecord);
883
+
884
+ // Transform results to include processed data rows
885
+ const transformedResults: RunViewWithCacheCheckResultOutput[] = response.results.map((result, index) => {
886
+ const inputItem = input[index];
887
+ const entity = provider.Entities.find(e => e.Name === inputItem.params.EntityName);
888
+
889
+ if (result.status === 'stale' && result.results && entity) {
890
+ // Process raw data into GraphQL-compatible format
891
+ const processedRows = this.processRawData(result.results as Record<string, unknown>[], entity.ID, entity);
892
+ return {
893
+ viewIndex: result.viewIndex,
894
+ status: result.status,
895
+ Results: processedRows,
896
+ maxUpdatedAt: result.maxUpdatedAt,
897
+ rowCount: result.rowCount,
898
+ errorMessage: result.errorMessage,
899
+ };
900
+ }
901
+
902
+ return {
903
+ viewIndex: result.viewIndex,
904
+ status: result.status,
905
+ Results: undefined,
906
+ maxUpdatedAt: result.maxUpdatedAt,
907
+ rowCount: result.rowCount,
908
+ errorMessage: result.errorMessage,
909
+ };
910
+ });
911
+
912
+ return {
913
+ success: response.success,
914
+ results: transformedResults,
915
+ errorMessage: response.errorMessage,
916
+ };
917
+ } catch (err) {
918
+ const errorMessage = err instanceof Error ? err.message : 'Unknown error occurred';
919
+ LogError(err);
920
+ return {
921
+ success: false,
922
+ results: [],
923
+ errorMessage,
924
+ };
925
+ }
926
+ }
927
+
774
928
  protected processRawData(rawData: any[], entityId: string, entityInfo: EntityInfo): RunViewResultRow[] {
775
929
  const returnResult = [];
776
930
  for (let i = 0; i < rawData.length; i++) {
package/src/index.ts CHANGED
@@ -69,6 +69,8 @@ LoadSendGridProvider();
69
69
 
70
70
  import { ExternalChangeDetectorEngine } from '@memberjunction/external-change-detection';
71
71
  import { ScheduledJobsService } from './services/ScheduledJobsService.js';
72
+ import { LocalCacheManager, StartupManager, TelemetryManager, TelemetryLevel } from '@memberjunction/core';
73
+ import { getSystemUser } from './auth/index.js';
72
74
 
73
75
  const cacheRefreshInterval = configInfo.databaseSettings.metadataCacheRefreshInterval;
74
76
 
@@ -110,6 +112,7 @@ export * from './resolvers/GetDataResolver.js';
110
112
  export * from './resolvers/GetDataContextDataResolver.js';
111
113
  export * from './resolvers/TransactionGroupResolver.js';
112
114
  export * from './resolvers/CreateQueryResolver.js';
115
+ export * from './resolvers/TelemetryResolver.js';
113
116
  export { GetReadOnlyDataSource, GetReadWriteDataSource, GetReadWriteProvider, GetReadOnlyProvider } from './util.js';
114
117
 
115
118
  export * from './generated/generated.js';
@@ -147,11 +150,6 @@ export const serve = async (resolverPaths: Array<string>, app = createApp(), opt
147
150
  const setupComplete$ = new ReplaySubject(1);
148
151
  await pool.connect();
149
152
 
150
- const config = new SQLServerProviderConfigData(pool, mj_core_schema, cacheRefreshInterval);
151
- await setupSQLServerClient(config); // datasource is already initialized, so we can setup the client right away
152
- const md = new Metadata();
153
- console.log(`Data Source has been initialized. ${md?.Entities ? md.Entities.length : 0} entities loaded.`);
154
-
155
153
  const dataSources = [new DataSourceInfo({dataSource: pool, type: 'Read-Write', host: dbHost, port: dbPort, database: dbDatabase, userName: dbUsername})];
156
154
 
157
155
  // Establish a second read-only connection to the database if dbReadOnlyUsername and dbReadOnlyPassword exist
@@ -170,6 +168,28 @@ export const serve = async (resolverPaths: Array<string>, app = createApp(), opt
170
168
  console.log('Read-only Connection Pool has been initialized.');
171
169
  }
172
170
 
171
+ const config = new SQLServerProviderConfigData(pool, mj_core_schema, cacheRefreshInterval);
172
+ await setupSQLServerClient(config); // datasource is already initialized, so we can setup the client right away
173
+ const md = new Metadata();
174
+ console.log(`Data Source has been initialized. ${md?.Entities ? md.Entities.length : 0} entities loaded.`);
175
+
176
+ // Initialize server telemetry based on config
177
+ const tm = TelemetryManager.Instance;
178
+ if (configInfo.telemetry?.enabled) {
179
+ tm.SetEnabled(true);
180
+ if (configInfo.telemetry?.level) {
181
+ tm.UpdateSettings({ level: configInfo.telemetry.level as TelemetryLevel });
182
+ }
183
+ console.log(`Server telemetry enabled with level: ${configInfo.telemetry.level || 'standard'}`);
184
+ } else {
185
+ tm.SetEnabled(false);
186
+ console.log('Server telemetry disabled');
187
+ }
188
+
189
+ // Initialize LocalCacheManager with the server-side storage provider (in-memory)
190
+ await LocalCacheManager.Instance.Initialize(Metadata.Provider.LocalStorageProvider);
191
+ console.log('LocalCacheManager initialized');
192
+
173
193
  setupComplete$.next(true);
174
194
  raiseEvent('setupComplete', dataSources, null, this);
175
195
 
@@ -1,11 +1,10 @@
1
- import { Arg, Ctx, Field, InputType, Mutation, ObjectType, PubSub, PubSubEngine, Query, Resolver } from 'type-graphql';
1
+ import { Arg, Ctx, Field, ObjectType, PubSub, PubSubEngine, Query, Resolver } from 'type-graphql';
2
2
  import { LogError, LogStatus, Metadata, RunView, UserInfo, CompositeKey, EntityFieldInfo, EntityInfo, EntityRelationshipInfo, EntitySaveOptions, EntityDeleteOptions, IMetadataProvider } from '@memberjunction/core';
3
- import { AppContext, UserPayload, MJ_SERVER_EVENT_CODE } from '../types.js';
3
+ import { AppContext, UserPayload } from '../types.js';
4
4
  import { BehaviorSubject } from 'rxjs';
5
5
  import { UserCache } from '@memberjunction/sqlserver-dataprovider';
6
6
  import { DataContext } from '@memberjunction/data-context';
7
- import { LoadDataContextItemsServer } from '@memberjunction/data-context-server';
8
- import { LearningCycleScheduler } from '../scheduler/LearningCycleScheduler.js';
7
+ import { LoadDataContextItemsServer } from '@memberjunction/data-context-server';
9
8
  LoadDataContextItemsServer(); // prevent tree shaking since the DataContextItemServer class is not directly referenced in this file or otherwise statically instantiated, so it could be removed by the build process
10
9
 
11
10
  import {
@@ -16,8 +15,7 @@ import {
16
15
  SkipAPIDataRequestResponse,
17
16
  SkipAPIClarifyingQuestionResponse,
18
17
  SkipEntityInfo,
19
- SkipQueryInfo,
20
- SkipQueryEntityInfo,
18
+ SkipQueryInfo,
21
19
  SkipAPIRunScriptRequest,
22
20
  SkipAPIRequestAPIKey,
23
21
  SkipRequestPhase,
@@ -26,13 +24,10 @@ import {
26
24
  SkipEntityFieldInfo,
27
25
  SkipEntityRelationshipInfo,
28
26
  SkipEntityFieldValueInfo,
29
- SkipAPILearningCycleRequest,
30
- SkipAPILearningCycleResponse,
31
- SkipLearningCycleNoteChange,
27
+ SkipAPILearningCycleRequest,
32
28
  SkipConversation,
33
29
  SkipAPIArtifact,
34
- SkipAPIAgentRequest,
35
- SkipAPIArtifactRequest,
30
+ SkipAPIAgentRequest,
36
31
  SkipAPIArtifactType,
37
32
  SkipAPIArtifactVersion,
38
33
  } from '@memberjunction/skip-types';
@@ -50,10 +45,9 @@ import {
50
45
  ConversationEntity,
51
46
  DataContextEntity,
52
47
  DataContextItemEntity,
53
- UserNotificationEntity,
54
- AIAgentEntityExtended
48
+ UserNotificationEntity
55
49
  } from '@memberjunction/core-entities';
56
- import { apiKey as callbackAPIKey, AskSkipInfo, baseUrl, publicUrl, configInfo, graphqlPort, graphqlRootPath, mj_core_schema } from '../config.js';
50
+ import { apiKey as callbackAPIKey, baseUrl, publicUrl, configInfo, graphqlPort, graphqlRootPath } from '../config.js';
57
51
  import mssql from 'mssql';
58
52
 
59
53
  import { registerEnumType } from 'type-graphql';
@@ -656,7 +650,7 @@ export class AskSkipResolver {
656
650
  // learningCycleEntity.StartedAt = startTime;
657
651
 
658
652
  // if (!(await learningCycleEntity.Save())) {
659
- // throw new Error(`Failed to create learning cycle record: ${learningCycleEntity.LatestResult.Error}`);
653
+ // throw new Error(`Failed to create learning cycle record: ${learningCycleEntity.LatestResult.CompleteMessage}`);
660
654
  // }
661
655
 
662
656
  // const learningCycleId = learningCycleEntity.ID;
@@ -676,7 +670,7 @@ export class AskSkipResolver {
676
670
  // learningCycleEntity.AgentSummary = 'No new conversations to process, learning cycle skipped, but recorded for audit purposes.';
677
671
  // learningCycleEntity.EndedAt = new Date();
678
672
  // if (!(await learningCycleEntity.Save())) {
679
- // LogError(`Failed to update learning cycle record: ${learningCycleEntity.LatestResult.Error}`);
673
+ // LogError(`Failed to update learning cycle record: ${learningCycleEntity.LatestResult.CompleteMessage}`);
680
674
  // }
681
675
  // const result: SkipAPILearningCycleResponse = {
682
676
  // success: true,
@@ -702,7 +696,7 @@ export class AskSkipResolver {
702
696
  // learningCycleEntity.EndedAt = endTime;
703
697
 
704
698
  // if (!(await learningCycleEntity.Save())) {
705
- // LogError(`Failed to update learning cycle record: ${learningCycleEntity.LatestResult.Error}`);
699
+ // LogError(`Failed to update learning cycle record: ${learningCycleEntity.LatestResult.CompleteMessage}`);
706
700
  // }
707
701
 
708
702
  // return response;
@@ -945,7 +939,7 @@ export class AskSkipResolver {
945
939
 
946
940
  // // Save the note
947
941
  // if (!(await noteEntity.Save())) {
948
- // LogError(`Error saving AI Agent Note: ${noteEntity.LatestResult.Error}`);
942
+ // LogError(`Error saving AI Agent Note: ${noteEntity.LatestResult.CompleteMessage}`);
949
943
  // return false;
950
944
  // }
951
945
 
@@ -986,7 +980,7 @@ export class AskSkipResolver {
986
980
 
987
981
  // // Proceed with deletion
988
982
  // if (!(await noteEntity.Delete())) {
989
- // LogError(`Error deleting AI Agent Note: ${noteEntity.LatestResult.Error}`);
983
+ // LogError(`Error deleting AI Agent Note: ${noteEntity.LatestResult.CompleteMessage}`);
990
984
  // return false;
991
985
  // }
992
986
 
@@ -1732,7 +1726,11 @@ export class AskSkipResolver {
1732
1726
  QueryID: e.QueryID,
1733
1727
  EntityID: e.EntityID,
1734
1728
  Entity: e.Entity
1735
- }))
1729
+ })),
1730
+ CacheEnabled: q.CacheEnabled,
1731
+ CacheMaxSize: q.CacheMaxSize,
1732
+ CacheTTLMinutes: q.CacheMaxSize,
1733
+ CacheValidationSQL: q.CacheValidationSQL
1736
1734
  }));
1737
1735
  }
1738
1736