@memberjunction/server 5.16.0 → 5.17.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/README.md +66 -3
- package/dist/config.d.ts +51 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +7 -0
- package/dist/config.js.map +1 -1
- package/dist/generated/generated.d.ts +46 -46
- package/dist/generated/generated.d.ts.map +1 -1
- package/dist/generated/generated.js +332 -332
- package/dist/generated/generated.js.map +1 -1
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +311 -1
- package/dist/index.js.map +1 -1
- package/dist/resolvers/IntegrationDiscoveryResolver.d.ts +484 -0
- package/dist/resolvers/IntegrationDiscoveryResolver.d.ts.map +1 -1
- package/dist/resolvers/IntegrationDiscoveryResolver.js +3867 -328
- package/dist/resolvers/IntegrationDiscoveryResolver.js.map +1 -1
- package/dist/resolvers/RSUResolver.d.ts +89 -0
- package/dist/resolvers/RSUResolver.d.ts.map +1 -0
- package/dist/resolvers/RSUResolver.js +424 -0
- package/dist/resolvers/RSUResolver.js.map +1 -0
- package/package.json +63 -61
- package/src/config.ts +9 -0
- package/src/generated/generated.ts +269 -269
- package/src/index.ts +350 -1
- package/src/resolvers/IntegrationDiscoveryResolver.ts +2970 -39
- package/src/resolvers/RSUResolver.ts +351 -0
package/src/index.ts
CHANGED
|
@@ -4,7 +4,7 @@ dotenv.config({ quiet: true });
|
|
|
4
4
|
|
|
5
5
|
import { expressMiddleware } from '@as-integrations/express5';
|
|
6
6
|
import { mergeSchemas } from '@graphql-tools/schema';
|
|
7
|
-
import { Metadata, DatabasePlatform, SetProvider, StartupManager as StartupManagerImport, BaseEntity, BaseEntityEvent } from '@memberjunction/core';
|
|
7
|
+
import { Metadata, DatabasePlatform, SetProvider, StartupManager as StartupManagerImport, BaseEntity, BaseEntityEvent, RunView } from '@memberjunction/core';
|
|
8
8
|
import { MJGlobal, MJEventType, UUIDsEqual } from '@memberjunction/global';
|
|
9
9
|
import { setupSQLServerClient, SQLServerDataProvider, SQLServerProviderConfigData, UserCache } from '@memberjunction/sqlserver-dataprovider';
|
|
10
10
|
import { extendConnectionPoolWithQuery } from './util.js';
|
|
@@ -43,6 +43,16 @@ import { RedisLocalStorageProvider } from '@memberjunction/redis-provider';
|
|
|
43
43
|
import { GenericDatabaseProvider } from '@memberjunction/generic-database-provider';
|
|
44
44
|
import { PubSubManager } from './generic/PubSubManager.js';
|
|
45
45
|
import { CACHE_INVALIDATION_TOPIC } from './generic/CacheInvalidationResolver.js';
|
|
46
|
+
import { ConnectorFactory, IntegrationEngine, IntegrationSyncOptions } from '@memberjunction/integration-engine';
|
|
47
|
+
import { CronExpressionHelper } from '@memberjunction/scheduling-engine';
|
|
48
|
+
import {
|
|
49
|
+
MJCompanyIntegrationEntity,
|
|
50
|
+
MJIntegrationEntity,
|
|
51
|
+
MJCompanyIntegrationEntityMapEntity,
|
|
52
|
+
MJCompanyIntegrationFieldMapEntity,
|
|
53
|
+
MJScheduledJobEntity,
|
|
54
|
+
} from '@memberjunction/core-entities';
|
|
55
|
+
import { ServerExtensionLoader, ServerExtensionConfig } from '@memberjunction/server-extensions-core';
|
|
46
56
|
|
|
47
57
|
const cacheRefreshInterval = configInfo.databaseSettings.metadataCacheRefreshInterval;
|
|
48
58
|
|
|
@@ -62,6 +72,8 @@ export { MaxLength } from 'class-validator';
|
|
|
62
72
|
export * from 'type-graphql';
|
|
63
73
|
export { NewUserBase } from './auth/newUsers.js';
|
|
64
74
|
export { configInfo, DEFAULT_SERVER_CONFIG } from './config.js';
|
|
75
|
+
export { ServerExtensionLoader, BaseServerExtension } from '@memberjunction/server-extensions-core';
|
|
76
|
+
export type { ServerExtensionConfig, ExtensionInitResult, ExtensionHealthResult } from '@memberjunction/server-extensions-core';
|
|
65
77
|
export * from './directives/index.js';
|
|
66
78
|
export * from './entitySubclasses/MJEntityPermissionEntityServer.server.js';
|
|
67
79
|
export * from './types.js';
|
|
@@ -121,6 +133,7 @@ export * from './resolvers/UserResolver.js';
|
|
|
121
133
|
export * from './resolvers/UserViewResolver.js';
|
|
122
134
|
export * from './resolvers/VersionHistoryResolver.js';
|
|
123
135
|
export * from './resolvers/CurrentUserContextResolver.js';
|
|
136
|
+
export * from './resolvers/RSUResolver.js';
|
|
124
137
|
export { GetReadOnlyDataSource, GetReadWriteDataSource, GetReadWriteProvider, GetReadOnlyProvider } from './util.js';
|
|
125
138
|
|
|
126
139
|
export * from './generated/generated.js';
|
|
@@ -303,6 +316,65 @@ export const serve = async (resolverPaths: Array<string>, app: Application = cre
|
|
|
303
316
|
tPhase = lap('Metadata + Provider Setup', tPhase);
|
|
304
317
|
const md = new Metadata();
|
|
305
318
|
console.log(`Data Source has been initialized. ${md?.Entities ? md.Entities.length : 0} entities loaded.`);
|
|
319
|
+
|
|
320
|
+
// Set up CodeGen-credentialed provider for RSU DDL operations (CREATE TABLE, CREATE SCHEMA, etc.)
|
|
321
|
+
const codegenUser = process.env.CODEGEN_DB_USERNAME;
|
|
322
|
+
const codegenPass = process.env.CODEGEN_DB_PASSWORD;
|
|
323
|
+
if (codegenUser && codegenPass) {
|
|
324
|
+
try {
|
|
325
|
+
const codegenPool = new sql.ConnectionPool({
|
|
326
|
+
...createMSSQLConfig(),
|
|
327
|
+
user: codegenUser,
|
|
328
|
+
password: codegenPass,
|
|
329
|
+
});
|
|
330
|
+
codegenPool.on('error', (err) => {
|
|
331
|
+
console.error('[ConnectionPool] CodeGen pool connection error:', err.message);
|
|
332
|
+
});
|
|
333
|
+
await codegenPool.connect();
|
|
334
|
+
|
|
335
|
+
const { RuntimeSchemaManager } = await import('@memberjunction/schema-engine');
|
|
336
|
+
const codegenConfig = new SQLServerProviderConfigData(codegenPool, mj_core_schema, cacheRefreshInterval);
|
|
337
|
+
const codegenProvider = new SQLServerDataProvider();
|
|
338
|
+
await codegenProvider.Config(codegenConfig);
|
|
339
|
+
RuntimeSchemaManager.Instance.SetDDLProvider(codegenProvider);
|
|
340
|
+
console.log('RSU DDL provider initialized with CodeGen credentials.');
|
|
341
|
+
|
|
342
|
+
// Set up in-process CodeGen runner for RSU
|
|
343
|
+
try {
|
|
344
|
+
const { RunCodeGenBase } = await import('@memberjunction/codegen-lib');
|
|
345
|
+
const { SQLServerCodeGenConnection } = await import('@memberjunction/codegen-lib/dist/Database/providers/sqlserver/SQLServerCodeGenConnection.js');
|
|
346
|
+
|
|
347
|
+
const codegenConnection = new SQLServerCodeGenConnection(codegenPool);
|
|
348
|
+
const codegenCurrentUser = UserCache.Instance.Users.find(u => u.Type?.trim().toLowerCase() === 'owner') ?? UserCache.Instance.Users[0];
|
|
349
|
+
|
|
350
|
+
const codegenDataSource = {
|
|
351
|
+
provider: codegenProvider,
|
|
352
|
+
connection: codegenConnection,
|
|
353
|
+
currentUser: codegenCurrentUser,
|
|
354
|
+
connectionInfo: `${configInfo.dbHost}:${configInfo.dbPort}/${configInfo.dbDatabase} (CodeGen)`,
|
|
355
|
+
};
|
|
356
|
+
|
|
357
|
+
const runObject = MJGlobal.Instance.ClassFactory.CreateInstance(RunCodeGenBase) as InstanceType<typeof RunCodeGenBase>;
|
|
358
|
+
|
|
359
|
+
const rsuWorkDir = process.env.RSU_WORK_DIR || process.cwd();
|
|
360
|
+
RuntimeSchemaManager.Instance.SetCodeGenRunner({
|
|
361
|
+
RunInProcess: (skipDB) => runObject.RunInProcess(codegenDataSource, skipDB, rsuWorkDir),
|
|
362
|
+
});
|
|
363
|
+
console.log('RSU in-process CodeGen runner initialized.');
|
|
364
|
+
|
|
365
|
+
// Inject CodeGen output paths for targeted git staging
|
|
366
|
+
const { initializeConfig } = await import('@memberjunction/codegen-lib');
|
|
367
|
+
const codegenConfig = initializeConfig(rsuWorkDir);
|
|
368
|
+
const outputPaths = (codegenConfig.output ?? []).map((o: { directory: string }) => o.directory);
|
|
369
|
+
RuntimeSchemaManager.Instance.SetCodeGenOutputPaths(outputPaths);
|
|
370
|
+
console.log(`RSU CodeGen output paths: ${outputPaths.length} directories configured.`);
|
|
371
|
+
} catch (codegenErr) {
|
|
372
|
+
console.warn(`RSU in-process CodeGen runner setup failed (will fall back to child process): ${(codegenErr as Error).message}`);
|
|
373
|
+
}
|
|
374
|
+
} catch (err) {
|
|
375
|
+
console.warn(`RSU DDL provider setup failed (RSU will fall back to default provider): ${(err as Error).message}`);
|
|
376
|
+
}
|
|
377
|
+
}
|
|
306
378
|
}
|
|
307
379
|
|
|
308
380
|
let tServe = performance.now();
|
|
@@ -648,6 +720,23 @@ export const serve = async (resolverPaths: Array<string>, app: Application = cre
|
|
|
648
720
|
// client from reading the error code and triggering token refresh.
|
|
649
721
|
app.use(cors<cors.CorsRequest>());
|
|
650
722
|
|
|
723
|
+
// ─── Server extensions (before auth — extensions handle their own auth) ─────
|
|
724
|
+
// Slack uses HMAC signature verification, Teams uses Bot Framework JWT validation.
|
|
725
|
+
// These must be registered before the unified auth middleware so webhook
|
|
726
|
+
// requests aren't rejected for lacking an MJ bearer token.
|
|
727
|
+
const extensionLoader = new ServerExtensionLoader();
|
|
728
|
+
const extensionConfigs = (configInfo.serverExtensions ?? []) as ServerExtensionConfig[];
|
|
729
|
+
if (extensionConfigs.length > 0) {
|
|
730
|
+
await extensionLoader.LoadExtensions(app, extensionConfigs);
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
// Extension health endpoint (always available, returns empty array if no extensions)
|
|
734
|
+
app.get('/health/extensions', async (_req, res) => {
|
|
735
|
+
const results = await extensionLoader.HealthCheckAll();
|
|
736
|
+
const allHealthy = results.length === 0 || results.every(r => r.Healthy);
|
|
737
|
+
res.status(allHealthy ? 200 : 503).json({ extensions: results });
|
|
738
|
+
});
|
|
739
|
+
|
|
651
740
|
// ─── Unified auth middleware (replaces both REST authMiddleware and contextFunction auth) ─────
|
|
652
741
|
app.use(createUnifiedAuthMiddleware(dataSources));
|
|
653
742
|
|
|
@@ -745,10 +834,23 @@ export const serve = async (resolverPaths: Array<string>, app: Application = cre
|
|
|
745
834
|
console.log(`📦 Connected to database: ${dbHost}:${dbPort}/${dbDatabase}`);
|
|
746
835
|
console.log(`🚀 Server ready at http://localhost:${graphqlPort}/`);
|
|
747
836
|
|
|
837
|
+
// Process pending RSU work from pre-restart (entity maps, field maps, sync)
|
|
838
|
+
processRSUPendingWork().catch(err => console.warn(`RSU pending work processing failed: ${err}`));
|
|
839
|
+
|
|
748
840
|
// Set up graceful shutdown handlers
|
|
749
841
|
const gracefulShutdown = async (signal: string) => {
|
|
750
842
|
console.log(`\n${signal} received, shutting down gracefully...`);
|
|
751
843
|
|
|
844
|
+
// Stop server extensions
|
|
845
|
+
if (extensionLoader.ExtensionCount > 0) {
|
|
846
|
+
try {
|
|
847
|
+
await extensionLoader.ShutdownAll();
|
|
848
|
+
console.log('✅ Server extensions shut down');
|
|
849
|
+
} catch (error) {
|
|
850
|
+
console.error('❌ Error shutting down server extensions:', error);
|
|
851
|
+
}
|
|
852
|
+
}
|
|
853
|
+
|
|
752
854
|
// Stop scheduled jobs service
|
|
753
855
|
if (scheduledJobsService?.IsRunning) {
|
|
754
856
|
try {
|
|
@@ -784,6 +886,253 @@ export const serve = async (resolverPaths: Array<string>, app: Application = cre
|
|
|
784
886
|
});
|
|
785
887
|
};
|
|
786
888
|
|
|
889
|
+
/**
|
|
890
|
+
* Process pending RSU work left from a pre-restart Apply All.
|
|
891
|
+
* Reads pending work files, creates entity maps + field maps, starts sync.
|
|
892
|
+
*/
|
|
893
|
+
async function processRSUPendingWork(): Promise<void> {
|
|
894
|
+
// Dynamic import — schema-engine is not yet published to npm, only exists as a workspace package
|
|
895
|
+
const { RuntimeSchemaManager } = await import('@memberjunction/schema-engine');
|
|
896
|
+
const rsm = RuntimeSchemaManager.Instance;
|
|
897
|
+
const pendingItems = await rsm.ReadAndClearPendingWork();
|
|
898
|
+
if (pendingItems.length === 0) return;
|
|
899
|
+
|
|
900
|
+
console.log(`[RSU] Processing ${pendingItems.length} pending work item(s) from pre-restart...`);
|
|
901
|
+
|
|
902
|
+
// Wait a moment for metadata to be fully loaded
|
|
903
|
+
await new Promise(resolve => setTimeout(resolve, 3000));
|
|
904
|
+
|
|
905
|
+
for (const item of pendingItems) {
|
|
906
|
+
try {
|
|
907
|
+
const md = new Metadata();
|
|
908
|
+
|
|
909
|
+
// Get system user for server-side operations
|
|
910
|
+
const systemUser = UserCache.Instance.Users.find(u => u.Type?.trim().toLowerCase() === 'owner') ?? UserCache.Instance.Users[0];
|
|
911
|
+
if (!systemUser) {
|
|
912
|
+
console.warn(`[RSU] No system user found, skipping pending work for ${item.CompanyIntegrationID}`);
|
|
913
|
+
continue;
|
|
914
|
+
}
|
|
915
|
+
|
|
916
|
+
await Metadata.Provider.Refresh();
|
|
917
|
+
|
|
918
|
+
// Resolve connector
|
|
919
|
+
const rv = new RunView();
|
|
920
|
+
const ciResult = await rv.RunView<MJCompanyIntegrationEntity>({
|
|
921
|
+
EntityName: 'MJ: Company Integrations',
|
|
922
|
+
ExtraFilter: `ID='${item.CompanyIntegrationID}'`,
|
|
923
|
+
ResultType: 'entity_object',
|
|
924
|
+
}, systemUser);
|
|
925
|
+
const companyIntegration = ciResult.Results[0];
|
|
926
|
+
if (!companyIntegration) {
|
|
927
|
+
console.warn(`[RSU] CompanyIntegration ${item.CompanyIntegrationID} not found`);
|
|
928
|
+
continue;
|
|
929
|
+
}
|
|
930
|
+
|
|
931
|
+
const integrationName = companyIntegration.Integration;
|
|
932
|
+
const integrationResult = await rv.RunView<MJIntegrationEntity>({
|
|
933
|
+
EntityName: 'MJ: Integrations',
|
|
934
|
+
ExtraFilter: `Name='${integrationName}'`,
|
|
935
|
+
ResultType: 'entity_object',
|
|
936
|
+
}, systemUser);
|
|
937
|
+
const integrationEntity = integrationResult.Results[0];
|
|
938
|
+
if (!integrationEntity) {
|
|
939
|
+
console.warn(`[RSU] Integration entity for ${integrationName} not found`);
|
|
940
|
+
continue;
|
|
941
|
+
}
|
|
942
|
+
const connector = ConnectorFactory.Resolve(integrationEntity);
|
|
943
|
+
if (!connector) {
|
|
944
|
+
console.warn(`[RSU] Connector for ${integrationName} not found`);
|
|
945
|
+
continue;
|
|
946
|
+
}
|
|
947
|
+
|
|
948
|
+
// Create entity maps + field maps for each source object
|
|
949
|
+
const createdEntityMapIDs: string[] = [];
|
|
950
|
+
const rvPending = new RunView();
|
|
951
|
+
const sourceObjectFields: Record<string, string[] | null> = item.SourceObjectFields ?? {};
|
|
952
|
+
|
|
953
|
+
for (const objName of item.SourceObjectNames) {
|
|
954
|
+
const tableName = objName.replace(/[^A-Za-z0-9_]/g, '_').toLowerCase();
|
|
955
|
+
const entity = md.Entities.find(
|
|
956
|
+
e => e.SchemaName.toLowerCase() === item.SchemaName.toLowerCase() &&
|
|
957
|
+
e.BaseTable.toLowerCase() === tableName
|
|
958
|
+
);
|
|
959
|
+
if (!entity) {
|
|
960
|
+
console.warn(`[RSU] Entity not found for ${item.SchemaName}.${tableName}`);
|
|
961
|
+
continue;
|
|
962
|
+
}
|
|
963
|
+
|
|
964
|
+
// Check if entity map already exists for this connector + entity
|
|
965
|
+
const existingMapResult = await rvPending.RunView<MJCompanyIntegrationEntityMapEntity>({
|
|
966
|
+
EntityName: 'MJ: Company Integration Entity Maps',
|
|
967
|
+
ExtraFilter: `CompanyIntegrationID='${item.CompanyIntegrationID}' AND EntityID='${entity.ID}'`,
|
|
968
|
+
ResultType: 'entity_object',
|
|
969
|
+
}, systemUser);
|
|
970
|
+
|
|
971
|
+
let entityMapID: string;
|
|
972
|
+
let isNewMap = false;
|
|
973
|
+
|
|
974
|
+
if (existingMapResult.Success && existingMapResult.Results.length > 0) {
|
|
975
|
+
entityMapID = existingMapResult.Results[0].ID;
|
|
976
|
+
console.log(`[RSU] Entity map already exists for ${objName} → ${entity.Name} (${entityMapID})`);
|
|
977
|
+
} else {
|
|
978
|
+
const entityMap = await md.GetEntityObject<MJCompanyIntegrationEntityMapEntity>(
|
|
979
|
+
'MJ: Company Integration Entity Maps', systemUser
|
|
980
|
+
);
|
|
981
|
+
entityMap.NewRecord();
|
|
982
|
+
entityMap.CompanyIntegrationID = item.CompanyIntegrationID;
|
|
983
|
+
entityMap.EntityID = entity.ID;
|
|
984
|
+
entityMap.ExternalObjectName = objName;
|
|
985
|
+
entityMap.SyncDirection = 'Pull';
|
|
986
|
+
entityMap.Status = 'Active';
|
|
987
|
+
entityMap.SyncEnabled = true;
|
|
988
|
+
const mapSaved = await entityMap.Save();
|
|
989
|
+
if (!mapSaved) {
|
|
990
|
+
console.warn(`[RSU] Failed to save entity map for ${objName}`);
|
|
991
|
+
continue;
|
|
992
|
+
}
|
|
993
|
+
entityMapID = entityMap.ID;
|
|
994
|
+
isNewMap = true;
|
|
995
|
+
}
|
|
996
|
+
|
|
997
|
+
if (isNewMap) createdEntityMapIDs.push(entityMapID);
|
|
998
|
+
|
|
999
|
+
// Create field maps — filter by SourceObjectFields (null = all)
|
|
1000
|
+
try {
|
|
1001
|
+
const introspect = connector.IntrospectSchema.bind(connector) as
|
|
1002
|
+
(ci: unknown, u: unknown) => Promise<{ Objects: Array<{ ExternalName: string; Fields: Array<{ Name: string }> }> }>;
|
|
1003
|
+
const schema = await introspect(companyIntegration, systemUser);
|
|
1004
|
+
const sourceObj = schema.Objects.find(o => o.ExternalName.toLowerCase() === objName.toLowerCase());
|
|
1005
|
+
|
|
1006
|
+
const selectedFields = sourceObjectFields[objName]; // null = all, string[] = specific
|
|
1007
|
+
const fieldsToMap = selectedFields
|
|
1008
|
+
? (sourceObj?.Fields ?? []).filter(f => selectedFields.some(sf => sf.toLowerCase() === f.Name.toLowerCase()))
|
|
1009
|
+
: (sourceObj?.Fields ?? []);
|
|
1010
|
+
|
|
1011
|
+
// Load existing field maps to avoid duplicates
|
|
1012
|
+
const existingFieldMaps = await rvPending.RunView<MJCompanyIntegrationFieldMapEntity>({
|
|
1013
|
+
EntityName: 'MJ: Company Integration Field Maps',
|
|
1014
|
+
ExtraFilter: `EntityMapID='${entityMapID}'`,
|
|
1015
|
+
ResultType: 'simple',
|
|
1016
|
+
Fields: ['SourceFieldName'],
|
|
1017
|
+
}, systemUser);
|
|
1018
|
+
const existingFieldNames = new Set(
|
|
1019
|
+
(existingFieldMaps.Success ? existingFieldMaps.Results : []).map((fm: { SourceFieldName: string }) => fm.SourceFieldName.toLowerCase())
|
|
1020
|
+
);
|
|
1021
|
+
|
|
1022
|
+
let fieldCount = 0;
|
|
1023
|
+
for (const field of fieldsToMap) {
|
|
1024
|
+
if (existingFieldNames.has(field.Name.toLowerCase())) continue;
|
|
1025
|
+
const fieldMap = await md.GetEntityObject<MJCompanyIntegrationFieldMapEntity>(
|
|
1026
|
+
'MJ: Company Integration Field Maps', systemUser
|
|
1027
|
+
);
|
|
1028
|
+
fieldMap.NewRecord();
|
|
1029
|
+
fieldMap.EntityMapID = entityMapID;
|
|
1030
|
+
fieldMap.SourceFieldName = field.Name;
|
|
1031
|
+
fieldMap.DestinationFieldName = field.Name.replace(/[^A-Za-z0-9_]/g, '_');
|
|
1032
|
+
fieldMap.Status = 'Active';
|
|
1033
|
+
if (await fieldMap.Save()) fieldCount++;
|
|
1034
|
+
}
|
|
1035
|
+
console.log(`[RSU] Created entity map for ${objName} → ${entity.Name} with ${fieldCount} field maps${isNewMap ? '' : ' (existing map, new fields only)'}`);
|
|
1036
|
+
} catch (fieldErr) {
|
|
1037
|
+
console.warn(`[RSU] Field map creation failed for ${objName}: ${fieldErr}`);
|
|
1038
|
+
}
|
|
1039
|
+
}
|
|
1040
|
+
|
|
1041
|
+
// Start sync if requested
|
|
1042
|
+
if (item.StartSync !== false) {
|
|
1043
|
+
try {
|
|
1044
|
+
await IntegrationEngine.Instance.Config(false, systemUser);
|
|
1045
|
+
const syncOptions: IntegrationSyncOptions = {};
|
|
1046
|
+
if (item.SyncScope !== 'all' && createdEntityMapIDs.length > 0) syncOptions.EntityMapIDs = createdEntityMapIDs;
|
|
1047
|
+
if (item.FullSync) syncOptions.FullSync = true;
|
|
1048
|
+
const opts = Object.keys(syncOptions).length > 0 ? syncOptions : undefined;
|
|
1049
|
+
IntegrationEngine.Instance.RunSync(item.CompanyIntegrationID, systemUser, 'Manual', undefined, undefined, opts);
|
|
1050
|
+
console.log(`[RSU] Sync started for ${item.CompanyIntegrationID} (EntityMaps: ${createdEntityMapIDs.length}, FullSync: ${!!item.FullSync})`);
|
|
1051
|
+
} catch (syncErr) {
|
|
1052
|
+
console.warn(`[RSU] Sync start failed: ${syncErr}`);
|
|
1053
|
+
}
|
|
1054
|
+
} else {
|
|
1055
|
+
console.log(`[RSU] Sync skipped for ${item.CompanyIntegrationID} (StartSync=false)`);
|
|
1056
|
+
}
|
|
1057
|
+
|
|
1058
|
+
// Create or update schedule if CronExpression provided
|
|
1059
|
+
if (item.CronExpression) {
|
|
1060
|
+
try {
|
|
1061
|
+
const rvSched = new RunView();
|
|
1062
|
+
|
|
1063
|
+
// Find existing schedule by loading all integration sync jobs and matching Configuration JSON exactly
|
|
1064
|
+
const allJobsResult = await rvSched.RunView<MJScheduledJobEntity>({
|
|
1065
|
+
EntityName: 'MJ: Scheduled Jobs',
|
|
1066
|
+
ExtraFilter: `Status IN ('Active', 'Paused')`,
|
|
1067
|
+
ResultType: 'entity_object',
|
|
1068
|
+
}, systemUser);
|
|
1069
|
+
|
|
1070
|
+
let existingJob: MJScheduledJobEntity | null = null;
|
|
1071
|
+
if (allJobsResult.Success) {
|
|
1072
|
+
for (const j of allJobsResult.Results) {
|
|
1073
|
+
try {
|
|
1074
|
+
const config = JSON.parse(j.Configuration || '{}') as Record<string, unknown>;
|
|
1075
|
+
if (config.CompanyIntegrationID === item.CompanyIntegrationID) {
|
|
1076
|
+
existingJob = j;
|
|
1077
|
+
break;
|
|
1078
|
+
}
|
|
1079
|
+
} catch { /* skip invalid JSON */ }
|
|
1080
|
+
}
|
|
1081
|
+
}
|
|
1082
|
+
|
|
1083
|
+
let job: MJScheduledJobEntity;
|
|
1084
|
+
let isUpdate = false;
|
|
1085
|
+
|
|
1086
|
+
if (existingJob) {
|
|
1087
|
+
job = existingJob;
|
|
1088
|
+
isUpdate = true;
|
|
1089
|
+
} else {
|
|
1090
|
+
const jobTypeResult = await rvSched.RunView<{ ID: string }>({
|
|
1091
|
+
EntityName: 'MJ: Scheduled Job Types',
|
|
1092
|
+
ExtraFilter: `DriverClass='IntegrationSyncScheduledJobDriver'`,
|
|
1093
|
+
MaxRows: 1,
|
|
1094
|
+
ResultType: 'simple',
|
|
1095
|
+
Fields: ['ID']
|
|
1096
|
+
}, systemUser);
|
|
1097
|
+
|
|
1098
|
+
if (!jobTypeResult.Success || jobTypeResult.Results.length === 0) {
|
|
1099
|
+
console.warn(`[RSU] IntegrationSyncScheduledJobDriver job type not found`);
|
|
1100
|
+
throw new Error('Job type not found');
|
|
1101
|
+
}
|
|
1102
|
+
|
|
1103
|
+
job = await md.GetEntityObject<MJScheduledJobEntity>('MJ: Scheduled Jobs', systemUser);
|
|
1104
|
+
job.NewRecord();
|
|
1105
|
+
job.JobTypeID = jobTypeResult.Results[0].ID;
|
|
1106
|
+
job.OwnerUserID = systemUser.ID;
|
|
1107
|
+
job.Configuration = JSON.stringify({ CompanyIntegrationID: item.CompanyIntegrationID });
|
|
1108
|
+
}
|
|
1109
|
+
|
|
1110
|
+
job.Name = `${integrationName} Scheduled Sync`;
|
|
1111
|
+
job.CronExpression = item.CronExpression;
|
|
1112
|
+
job.Timezone = item.ScheduleTimezone || 'UTC';
|
|
1113
|
+
job.Status = 'Active';
|
|
1114
|
+
job.NextRunAt = CronExpressionHelper.GetNextRunTime(item.CronExpression, item.ScheduleTimezone || 'UTC');
|
|
1115
|
+
if (await job.Save()) {
|
|
1116
|
+
console.log(`[RSU] ${isUpdate ? 'Updated' : 'Created'} schedule "${job.Name}" (${item.CronExpression}, NextRunAt=${job.NextRunAt.toISOString()}) for ${item.CompanyIntegrationID}`);
|
|
1117
|
+
companyIntegration.ScheduleEnabled = true;
|
|
1118
|
+
companyIntegration.ScheduleType = 'Cron';
|
|
1119
|
+
companyIntegration.CronExpression = item.CronExpression;
|
|
1120
|
+
await companyIntegration.Save();
|
|
1121
|
+
} else {
|
|
1122
|
+
console.warn(`[RSU] Failed to save schedule for ${item.CompanyIntegrationID}`);
|
|
1123
|
+
}
|
|
1124
|
+
} catch (schedErr) {
|
|
1125
|
+
console.warn(`[RSU] Schedule creation failed: ${schedErr}`);
|
|
1126
|
+
}
|
|
1127
|
+
}
|
|
1128
|
+
} catch (err) {
|
|
1129
|
+
console.error(`[RSU] Failed to process pending work for ${item.CompanyIntegrationID}: ${err}`);
|
|
1130
|
+
}
|
|
1131
|
+
}
|
|
1132
|
+
|
|
1133
|
+
console.log(`[RSU] Pending work processing complete`);
|
|
1134
|
+
}
|
|
1135
|
+
|
|
787
1136
|
/**
|
|
788
1137
|
* Creates a MSSQL ConnectionPool-compatible wrapper around a pg.Pool.
|
|
789
1138
|
* This allows existing code that references DataSourceInfo.dataSource (typed as sql.ConnectionPool)
|