@memberjunction/server 5.27.1 → 5.29.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 (50) hide show
  1. package/dist/config.d.ts +151 -0
  2. package/dist/config.d.ts.map +1 -1
  3. package/dist/config.js +15 -0
  4. package/dist/config.js.map +1 -1
  5. package/dist/generated/generated.d.ts +959 -5
  6. package/dist/generated/generated.d.ts.map +1 -1
  7. package/dist/generated/generated.js +4639 -280
  8. package/dist/generated/generated.js.map +1 -1
  9. package/dist/generic/ResolverBase.d.ts +14 -0
  10. package/dist/generic/ResolverBase.d.ts.map +1 -1
  11. package/dist/generic/ResolverBase.js +37 -3
  12. package/dist/generic/ResolverBase.js.map +1 -1
  13. package/dist/generic/RestoreContextInput.d.ts +27 -0
  14. package/dist/generic/RestoreContextInput.d.ts.map +1 -0
  15. package/dist/generic/RestoreContextInput.js +39 -0
  16. package/dist/generic/RestoreContextInput.js.map +1 -0
  17. package/dist/index.d.ts +2 -0
  18. package/dist/index.d.ts.map +1 -1
  19. package/dist/index.js +21 -4
  20. package/dist/index.js.map +1 -1
  21. package/dist/resolvers/FeedbackResolver.d.ts +150 -0
  22. package/dist/resolvers/FeedbackResolver.d.ts.map +1 -0
  23. package/dist/resolvers/FeedbackResolver.js +876 -0
  24. package/dist/resolvers/FeedbackResolver.js.map +1 -0
  25. package/dist/resolvers/FileResolver.d.ts +27 -0
  26. package/dist/resolvers/FileResolver.d.ts.map +1 -1
  27. package/dist/resolvers/FileResolver.js +32 -3
  28. package/dist/resolvers/FileResolver.js.map +1 -1
  29. package/dist/resolvers/IntegrationDiscoveryResolver.d.ts +18 -1
  30. package/dist/resolvers/IntegrationDiscoveryResolver.d.ts.map +1 -1
  31. package/dist/resolvers/IntegrationDiscoveryResolver.js +247 -22
  32. package/dist/resolvers/IntegrationDiscoveryResolver.js.map +1 -1
  33. package/dist/resolvers/MCPResolver.d.ts +77 -0
  34. package/dist/resolvers/MCPResolver.d.ts.map +1 -1
  35. package/dist/resolvers/MCPResolver.js +300 -1
  36. package/dist/resolvers/MCPResolver.js.map +1 -1
  37. package/dist/resolvers/RunAIAgentResolver.d.ts.map +1 -1
  38. package/dist/resolvers/RunAIAgentResolver.js +87 -32
  39. package/dist/resolvers/RunAIAgentResolver.js.map +1 -1
  40. package/package.json +68 -66
  41. package/src/config.ts +19 -0
  42. package/src/generated/generated.ts +3430 -281
  43. package/src/generic/ResolverBase.ts +41 -4
  44. package/src/generic/RestoreContextInput.ts +32 -0
  45. package/src/index.ts +22 -5
  46. package/src/resolvers/FeedbackResolver.ts +940 -0
  47. package/src/resolvers/FileResolver.ts +33 -4
  48. package/src/resolvers/IntegrationDiscoveryResolver.ts +224 -20
  49. package/src/resolvers/MCPResolver.ts +297 -1
  50. package/src/resolvers/RunAIAgentResolver.ts +89 -32
@@ -153,6 +153,10 @@ export class ResolverBase {
153
153
  Key: mapper.ReverseMapFieldName(item.Key),
154
154
  Value: item.Value,
155
155
  }));
156
+ } else if (key === 'RestoreContext___') {
157
+ // Pass through the restore-context blob unchanged — its inner field
158
+ // names (SourceChangeID, Reason) are not entity-field names.
159
+ mapped[key] = input[key];
156
160
  } else {
157
161
  mapped[mapper.ReverseMapFieldName(key)] = input[key];
158
162
  }
@@ -160,6 +164,24 @@ export class ResolverBase {
160
164
  return mapped;
161
165
  }
162
166
 
167
+ /**
168
+ * Applies an inbound RestoreContext___ blob to a server-side BaseEntity.
169
+ * Mirrors the OldValues___ pattern — the client-side BaseEntity's
170
+ * `_restoreContext` doesn't traverse the network, so the server must
171
+ * reconstruct it from the mutation input before calling Save().
172
+ *
173
+ * Returns true when context was applied; false when no context was on the input.
174
+ */
175
+ protected applyRestoreContext(
176
+ entityObject: BaseEntity,
177
+ input: { RestoreContext___?: { SourceChangeID?: string; Reason?: string | null } | null },
178
+ ): boolean {
179
+ const ctx = input?.RestoreContext___;
180
+ if (!ctx || !ctx.SourceChangeID) return false;
181
+ entityObject.SetRestoreContext(ctx.SourceChangeID, ctx.Reason ?? null);
182
+ return true;
183
+ }
184
+
163
185
  protected async ArrayMapFieldNamesToCodeNames(entityName: string, dataObjectArray: any[], contextUser?: UserInfo): Promise<any[]> {
164
186
  // iterate through the array and call MapFieldNamesToCodeNames for each element
165
187
  if (dataObjectArray && dataObjectArray.length > 0) {
@@ -1053,7 +1075,17 @@ export class ResolverBase {
1053
1075
  // fire event and proceed if it wasn't cancelled
1054
1076
  const entityObject = await provider.GetEntityObject(entityName, this.GetUserFromPayload(userPayload));
1055
1077
  entityObject.NewRecord();
1056
- entityObject.SetMany(input);
1078
+ // Strip the RestoreContext___ blob from the field assignments — it's
1079
+ // metadata for the upcoming Save(), not a field on the record.
1080
+ const fieldsForSet: Record<string, unknown> = {};
1081
+ for (const key of Object.keys(input)) {
1082
+ if (key !== 'RestoreContext___') fieldsForSet[key] = input[key];
1083
+ }
1084
+ entityObject.SetMany(fieldsForSet);
1085
+
1086
+ // Reconstruct the client-side restore context, if any, on this server
1087
+ // entity so the data provider writes the lineage columns on Save().
1088
+ this.applyRestoreContext(entityObject, input);
1057
1089
 
1058
1090
  this.ListenForEntityMessages(entityObject, pubSub, userPayload);
1059
1091
 
@@ -1095,10 +1127,11 @@ export class ResolverBase {
1095
1127
  const entityInfo = entityObject.EntityInfo;
1096
1128
  const clientNewValues = {};
1097
1129
  Object.keys(input).forEach((key) => {
1098
- if (key !== 'OldValues___') {
1130
+ // Skip metadata blobs that aren't actual entity fields.
1131
+ if (key !== 'OldValues___' && key !== 'RestoreContext___') {
1099
1132
  clientNewValues[key] = input[key];
1100
1133
  }
1101
- }); // grab all the props except for the OldValues property
1134
+ });
1102
1135
 
1103
1136
  if (entityInfo.TrackRecordChanges || !input.OldValues___) {
1104
1137
  // We get here because EITHER the entity tracks record changes OR the client did not provide OldValues, so we need to load the old values from the DB
@@ -1140,8 +1173,12 @@ export class ResolverBase {
1140
1173
  entityObject.SetMany(clientNewValues);
1141
1174
  }
1142
1175
 
1176
+ // Reconstruct the client-side restore context, if any, on this server
1177
+ // entity so the data provider writes the lineage columns on Save().
1178
+ this.applyRestoreContext(entityObject, input);
1179
+
1143
1180
  this.ListenForEntityMessages(entityObject, pubSub, userPayload);
1144
-
1181
+
1145
1182
  if (await entityObject.Save()) {
1146
1183
  // save worked, fire afterevent and return all the data
1147
1184
  await this.AfterUpdate(provider, input); // fire event
@@ -0,0 +1,32 @@
1
+ import { Field, InputType } from 'type-graphql';
2
+
3
+ /**
4
+ * GraphQL InputType carrying restore-lineage context across the network.
5
+ *
6
+ * Set as the `RestoreContext___` reserved field on any entity Create or
7
+ * Update mutation input when the operation is a restore. The server-side
8
+ * resolver detects it, calls `BaseEntity.SetRestoreContext()` on the
9
+ * server-side entity instance before `Save()`, and the data provider then
10
+ * writes the resulting RecordChange row with `Source='Restore'`,
11
+ * `RestoredFromID = SourceChangeID`, and `RestoreReason = Reason`.
12
+ *
13
+ * Mirrors the pattern used by `OldValues___` (KeyValuePairInput[]) — a
14
+ * non-field metadata blob carried alongside the regular field values
15
+ * through the GraphQL mutation input.
16
+ */
17
+ @InputType()
18
+ export class RestoreContextInput {
19
+ /**
20
+ * ID of the historical RecordChange row whose state is being restored.
21
+ * Persisted to RecordChange.RestoredFromID on the new change row.
22
+ */
23
+ @Field(() => String)
24
+ SourceChangeID: string;
25
+
26
+ /**
27
+ * Optional user-entered explanation for the restore. Persisted to
28
+ * RecordChange.RestoreReason. NULL when the user did not enter one.
29
+ */
30
+ @Field(() => String, { nullable: true })
31
+ Reason?: string | null;
32
+ }
package/src/index.ts CHANGED
@@ -104,6 +104,7 @@ export * from './resolvers/TaskResolver.js';
104
104
  export * from './generic/KeyValuePairInput.js';
105
105
  export * from './generic/KeyInputOutputTypes.js';
106
106
  export * from './generic/DeleteOptionsInput.js';
107
+ export * from './generic/RestoreContextInput.js';
107
108
 
108
109
  export * from './agents/skip-agent.js';
109
110
  export * from './agents/skip-sdk.js';
@@ -130,6 +131,7 @@ export * from './resolvers/MCPResolver.js';
130
131
  export * from './resolvers/ActionResolver.js';
131
132
  export * from './resolvers/CacheStatsResolver.js';
132
133
  export * from './resolvers/EntityCommunicationsResolver.js';
134
+ export * from './resolvers/FeedbackResolver.js';
133
135
  export * from './resolvers/EntityResolver.js';
134
136
  export * from './resolvers/ISAEntityResolver.js';
135
137
  export * from './resolvers/ArtifactFileResolver.js';
@@ -867,6 +869,13 @@ export const serve = async (resolverPaths: Array<string>, app: Application = cre
867
869
  // Process pending RSU work from pre-restart (entity maps, field maps, sync)
868
870
  processRSUPendingWork().catch(err => console.warn(`RSU pending work processing failed: ${err}`));
869
871
 
872
+ // Resume any integration syncs that were orphaned by the previous process restart
873
+ const resumeUser = UserCache.Instance.GetSystemUser();
874
+ if (resumeUser) {
875
+ IntegrationEngine.Instance.ResumeOrphanedSyncs(resumeUser)
876
+ .catch(err => console.warn(`[IntegrationEngine] Orphaned sync resume failed: ${err}`));
877
+ }
878
+
870
879
  // Set up graceful shutdown handlers
871
880
  const gracefulShutdown = async (signal: string) => {
872
881
  console.log(`\n${signal} received, shutting down gracefully...`);
@@ -980,6 +989,11 @@ async function processRSUPendingWork(): Promise<void> {
980
989
  const rvPending = new RunView();
981
990
  const sourceObjectFields: Record<string, string[] | null> = item.SourceObjectFields ?? {};
982
991
 
992
+ // Introspect schema ONCE for the entire connector, then reuse per object
993
+ const introspect = connector.IntrospectSchema.bind(connector) as
994
+ (ci: unknown, u: unknown) => Promise<{ Objects: Array<{ ExternalName: string; Fields: Array<{ Name: string; IsPrimaryKey?: boolean; IsRequired?: boolean }> }> }>;
995
+ const schema = await introspect(companyIntegration, systemUser);
996
+
983
997
  for (const objName of item.SourceObjectNames) {
984
998
  const tableName = objName.replace(/[^A-Za-z0-9_]/g, '_').toLowerCase();
985
999
  const entity = md.Entities.find(
@@ -1028,9 +1042,6 @@ async function processRSUPendingWork(): Promise<void> {
1028
1042
 
1029
1043
  // Create field maps — filter by SourceObjectFields (null = all)
1030
1044
  try {
1031
- const introspect = connector.IntrospectSchema.bind(connector) as
1032
- (ci: unknown, u: unknown) => Promise<{ Objects: Array<{ ExternalName: string; Fields: Array<{ Name: string }> }> }>;
1033
- const schema = await introspect(companyIntegration, systemUser);
1034
1045
  const sourceObj = schema.Objects.find(o => o.ExternalName.toLowerCase() === objName.toLowerCase());
1035
1046
 
1036
1047
  const selectedFields = sourceObjectFields[objName]; // null = all, string[] = specific
@@ -1059,6 +1070,9 @@ async function processRSUPendingWork(): Promise<void> {
1059
1070
  fieldMap.EntityMapID = entityMapID;
1060
1071
  fieldMap.SourceFieldName = field.Name;
1061
1072
  fieldMap.DestinationFieldName = field.Name.replace(/[^A-Za-z0-9_]/g, '_');
1073
+ fieldMap.IsKeyField = field.IsPrimaryKey ?? false;
1074
+ fieldMap.IsRequired = field.IsRequired ?? false;
1075
+ fieldMap.Direction = 'SourceToDest';
1062
1076
  fieldMap.Status = 'Active';
1063
1077
  if (await fieldMap.Save()) fieldCount++;
1064
1078
  }
@@ -1075,9 +1089,10 @@ async function processRSUPendingWork(): Promise<void> {
1075
1089
  const syncOptions: IntegrationSyncOptions = {};
1076
1090
  if (item.SyncScope !== 'all' && createdEntityMapIDs.length > 0) syncOptions.EntityMapIDs = createdEntityMapIDs;
1077
1091
  if (item.FullSync) syncOptions.FullSync = true;
1092
+ if (item.SyncDirection) syncOptions.SyncDirection = item.SyncDirection;
1078
1093
  const opts = Object.keys(syncOptions).length > 0 ? syncOptions : undefined;
1079
1094
  IntegrationEngine.Instance.RunSync(item.CompanyIntegrationID, systemUser, 'Manual', undefined, undefined, opts);
1080
- console.log(`[RSU] Sync started for ${item.CompanyIntegrationID} (EntityMaps: ${createdEntityMapIDs.length}, FullSync: ${!!item.FullSync})`);
1095
+ console.log(`[RSU] Sync started for ${item.CompanyIntegrationID} (EntityMaps: ${createdEntityMapIDs.length}, FullSync: ${!!item.FullSync}, SyncDirection: ${item.SyncDirection ?? 'entity-map default'})`);
1081
1096
  } catch (syncErr) {
1082
1097
  console.warn(`[RSU] Sync start failed: ${syncErr}`);
1083
1098
  }
@@ -1134,7 +1149,9 @@ async function processRSUPendingWork(): Promise<void> {
1134
1149
  job.NewRecord();
1135
1150
  job.JobTypeID = jobTypeResult.Results[0].ID;
1136
1151
  job.OwnerUserID = systemUser.ID;
1137
- job.Configuration = JSON.stringify({ CompanyIntegrationID: item.CompanyIntegrationID });
1152
+ const schedConfig: Record<string, unknown> = { CompanyIntegrationID: item.CompanyIntegrationID };
1153
+ if (item.ScheduleSyncDirection) schedConfig.SyncDirection = item.ScheduleSyncDirection;
1154
+ job.Configuration = JSON.stringify(schedConfig);
1138
1155
  }
1139
1156
 
1140
1157
  job.Name = `${integrationName} Scheduled Sync`;