@memberjunction/server 5.15.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/auth/index.d.ts +0 -3
- package/dist/auth/index.d.ts.map +1 -1
- package/dist/auth/index.js +5 -7
- package/dist/auth/index.js.map +1 -1
- package/dist/auth/initializeProviders.js +2 -2
- package/dist/auth/initializeProviders.js.map +1 -1
- 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/context.d.ts.map +1 -1
- package/dist/context.js +3 -3
- package/dist/context.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 +4 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +327 -2
- package/dist/index.js.map +1 -1
- package/dist/resolvers/DatasetResolver.d.ts +5 -0
- package/dist/resolvers/DatasetResolver.d.ts.map +1 -1
- package/dist/resolvers/DatasetResolver.js +35 -0
- package/dist/resolvers/DatasetResolver.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 -60
- package/src/__tests__/unifiedAuth.test.ts +3 -2
- package/src/auth/__tests__/backward-compatibility.test.ts +2 -3
- package/src/auth/index.ts +5 -8
- package/src/auth/initializeProviders.ts +2 -2
- package/src/config.ts +9 -0
- package/src/context.ts +3 -3
- package/src/generated/generated.ts +269 -269
- package/src/index.ts +371 -4
- package/src/resolvers/DatasetResolver.ts +36 -0
- package/src/resolvers/IntegrationDiscoveryResolver.ts +2970 -39
- package/src/resolvers/RSUResolver.ts +351 -0
- package/dist/auth/AuthProviderFactory.d.ts +0 -68
- package/dist/auth/AuthProviderFactory.d.ts.map +0 -1
- package/dist/auth/AuthProviderFactory.js +0 -155
- package/dist/auth/AuthProviderFactory.js.map +0 -1
- package/dist/auth/BaseAuthProvider.d.ts +0 -41
- package/dist/auth/BaseAuthProvider.d.ts.map +0 -1
- package/dist/auth/BaseAuthProvider.js +0 -102
- package/dist/auth/BaseAuthProvider.js.map +0 -1
- package/dist/auth/IAuthProvider.d.ts +0 -46
- package/dist/auth/IAuthProvider.d.ts.map +0 -1
- package/dist/auth/IAuthProvider.js +0 -2
- package/dist/auth/IAuthProvider.js.map +0 -1
- package/dist/auth/providers/Auth0Provider.d.ts +0 -18
- package/dist/auth/providers/Auth0Provider.d.ts.map +0 -1
- package/dist/auth/providers/Auth0Provider.js +0 -52
- package/dist/auth/providers/Auth0Provider.js.map +0 -1
- package/dist/auth/providers/CognitoProvider.d.ts +0 -18
- package/dist/auth/providers/CognitoProvider.d.ts.map +0 -1
- package/dist/auth/providers/CognitoProvider.js +0 -56
- package/dist/auth/providers/CognitoProvider.js.map +0 -1
- package/dist/auth/providers/GoogleProvider.d.ts +0 -18
- package/dist/auth/providers/GoogleProvider.d.ts.map +0 -1
- package/dist/auth/providers/GoogleProvider.js +0 -51
- package/dist/auth/providers/GoogleProvider.js.map +0 -1
- package/dist/auth/providers/MSALProvider.d.ts +0 -18
- package/dist/auth/providers/MSALProvider.d.ts.map +0 -1
- package/dist/auth/providers/MSALProvider.js +0 -52
- package/dist/auth/providers/MSALProvider.js.map +0 -1
- package/dist/auth/providers/OktaProvider.d.ts +0 -18
- package/dist/auth/providers/OktaProvider.d.ts.map +0 -1
- package/dist/auth/providers/OktaProvider.js +0 -52
- package/dist/auth/providers/OktaProvider.js.map +0 -1
- package/dist/auth/tokenExpiredError.d.ts +0 -5
- package/dist/auth/tokenExpiredError.d.ts.map +0 -1
- package/dist/auth/tokenExpiredError.js +0 -12
- package/dist/auth/tokenExpiredError.js.map +0 -1
- package/src/auth/AuthProviderFactory.ts +0 -182
- package/src/auth/BaseAuthProvider.ts +0 -137
- package/src/auth/IAuthProvider.ts +0 -54
- package/src/auth/providers/Auth0Provider.ts +0 -45
- package/src/auth/providers/CognitoProvider.ts +0 -50
- package/src/auth/providers/GoogleProvider.ts +0 -45
- package/src/auth/providers/MSALProvider.ts +0 -45
- package/src/auth/providers/OktaProvider.ts +0 -46
- package/src/auth/tokenExpiredError.ts +0 -12
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,17 +72,16 @@ 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';
|
|
68
80
|
export {
|
|
69
|
-
TokenExpiredError,
|
|
70
81
|
getSystemUser,
|
|
71
82
|
getSigningKeys,
|
|
72
83
|
extractUserInfoFromPayload,
|
|
73
84
|
verifyUserRecord,
|
|
74
|
-
AuthProviderFactory,
|
|
75
|
-
IAuthProvider,
|
|
76
85
|
} from './auth/index.js';
|
|
77
86
|
export * from './auth/APIKeyScopeAuth.js';
|
|
78
87
|
|
|
@@ -124,6 +133,7 @@ export * from './resolvers/UserResolver.js';
|
|
|
124
133
|
export * from './resolvers/UserViewResolver.js';
|
|
125
134
|
export * from './resolvers/VersionHistoryResolver.js';
|
|
126
135
|
export * from './resolvers/CurrentUserContextResolver.js';
|
|
136
|
+
export * from './resolvers/RSUResolver.js';
|
|
127
137
|
export { GetReadOnlyDataSource, GetReadWriteDataSource, GetReadWriteProvider, GetReadOnlyProvider } from './util.js';
|
|
128
138
|
|
|
129
139
|
export * from './generated/generated.js';
|
|
@@ -152,6 +162,13 @@ const localPath = (p: string) => {
|
|
|
152
162
|
export const createApp = (): Application => express();
|
|
153
163
|
|
|
154
164
|
export const serve = async (resolverPaths: Array<string>, app: Application = createApp(), options?: MJServerOptions): Promise<void> => {
|
|
165
|
+
const t0 = performance.now();
|
|
166
|
+
const lap = (label: string, since: number) => {
|
|
167
|
+
const ms = performance.now() - since;
|
|
168
|
+
console.log(`⏱️ [Startup] ${label}: ${ms.toFixed(0)}ms`);
|
|
169
|
+
return performance.now();
|
|
170
|
+
};
|
|
171
|
+
|
|
155
172
|
const localResolverPaths = ['resolvers/**/*Resolver.{js,ts}', 'generic/*Resolver.{js,ts}', 'generated/generated.{js,ts}'].map(localPath);
|
|
156
173
|
|
|
157
174
|
const combinedResolverPaths = [...resolverPaths, ...localResolverPaths];
|
|
@@ -260,6 +277,7 @@ export const serve = async (resolverPaths: Array<string>, app: Application = cre
|
|
|
260
277
|
} else {
|
|
261
278
|
// ─── SQL Server Path (existing behavior) ───────────────────────
|
|
262
279
|
console.log('Database type: SQL Server');
|
|
280
|
+
let tPhase = performance.now();
|
|
263
281
|
const pool = new sql.ConnectionPool(createMSSQLConfig());
|
|
264
282
|
|
|
265
283
|
// Handle connection-level errors from dead/stale connections in the pool.
|
|
@@ -270,6 +288,7 @@ export const serve = async (resolverPaths: Array<string>, app: Application = cre
|
|
|
270
288
|
});
|
|
271
289
|
|
|
272
290
|
await pool.connect();
|
|
291
|
+
tPhase = lap('DB Pool Connect', tPhase);
|
|
273
292
|
|
|
274
293
|
dataSources.push(new DataSourceInfo({dataSource: pool, type: 'Read-Write', host: dbHost, port: dbPort, database: dbDatabase, userName: dbUsername}));
|
|
275
294
|
|
|
@@ -294,10 +313,72 @@ export const serve = async (resolverPaths: Array<string>, app: Application = cre
|
|
|
294
313
|
|
|
295
314
|
const config = new SQLServerProviderConfigData(pool, mj_core_schema, cacheRefreshInterval);
|
|
296
315
|
await setupSQLServerClient(config);
|
|
316
|
+
tPhase = lap('Metadata + Provider Setup', tPhase);
|
|
297
317
|
const md = new Metadata();
|
|
298
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
|
+
}
|
|
299
378
|
}
|
|
300
379
|
|
|
380
|
+
let tServe = performance.now();
|
|
381
|
+
|
|
301
382
|
// Store queryDialects config in GlobalObjectStore so MJQueryEntityServer can
|
|
302
383
|
// read it without a circular dependency on MJServer
|
|
303
384
|
if (configInfo.queryDialects) {
|
|
@@ -387,6 +468,8 @@ export const serve = async (resolverPaths: Array<string>, app: Application = cre
|
|
|
387
468
|
/******TEST HARNESS FOR CHANGE DETECTION */
|
|
388
469
|
/******TEST HARNESS FOR CHANGE DETECTION */
|
|
389
470
|
|
|
471
|
+
tServe = lap('Telemetry + Cache + APIKey Init', tServe);
|
|
472
|
+
|
|
390
473
|
const dynamicModules = await Promise.all(
|
|
391
474
|
paths.map((modulePath) => {
|
|
392
475
|
try {
|
|
@@ -518,6 +601,8 @@ export const serve = async (resolverPaths: Array<string>, app: Application = cre
|
|
|
518
601
|
}
|
|
519
602
|
});
|
|
520
603
|
|
|
604
|
+
tServe = lap('Resolver + Middleware Discovery', tServe);
|
|
605
|
+
|
|
521
606
|
let schema = mergeSchemas({
|
|
522
607
|
schemas: [
|
|
523
608
|
buildSchemaSync({
|
|
@@ -538,6 +623,8 @@ export const serve = async (resolverPaths: Array<string>, app: Application = cre
|
|
|
538
623
|
schema = transformer(schema);
|
|
539
624
|
}
|
|
540
625
|
|
|
626
|
+
tServe = lap('Schema Build', tServe);
|
|
627
|
+
|
|
541
628
|
const httpServer = createServer(app);
|
|
542
629
|
|
|
543
630
|
const webSocketServer = new WebSocketServer({ server: httpServer, path: graphqlRootPath });
|
|
@@ -633,6 +720,23 @@ export const serve = async (resolverPaths: Array<string>, app: Application = cre
|
|
|
633
720
|
// client from reading the error code and triggering token refresh.
|
|
634
721
|
app.use(cors<cors.CorsRequest>());
|
|
635
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
|
+
|
|
636
740
|
// ─── Unified auth middleware (replaces both REST authMiddleware and contextFunction auth) ─────
|
|
637
741
|
app.use(createUnifiedAuthMiddleware(dataSources));
|
|
638
742
|
|
|
@@ -723,14 +827,30 @@ export const serve = async (resolverPaths: Array<string>, app: Application = cre
|
|
|
723
827
|
await Promise.resolve(options.onBeforeServe());
|
|
724
828
|
}
|
|
725
829
|
|
|
830
|
+
tServe = lap('Apollo + Express Setup', tServe);
|
|
831
|
+
|
|
726
832
|
await new Promise<void>((resolve) => httpServer.listen({ port: graphqlPort }, resolve));
|
|
833
|
+
lap('Total Startup', t0);
|
|
727
834
|
console.log(`📦 Connected to database: ${dbHost}:${dbPort}/${dbDatabase}`);
|
|
728
835
|
console.log(`🚀 Server ready at http://localhost:${graphqlPort}/`);
|
|
729
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
|
+
|
|
730
840
|
// Set up graceful shutdown handlers
|
|
731
841
|
const gracefulShutdown = async (signal: string) => {
|
|
732
842
|
console.log(`\n${signal} received, shutting down gracefully...`);
|
|
733
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
|
+
|
|
734
854
|
// Stop scheduled jobs service
|
|
735
855
|
if (scheduledJobsService?.IsRunning) {
|
|
736
856
|
try {
|
|
@@ -766,6 +886,253 @@ export const serve = async (resolverPaths: Array<string>, app: Application = cre
|
|
|
766
886
|
});
|
|
767
887
|
};
|
|
768
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
|
+
|
|
769
1136
|
/**
|
|
770
1137
|
* Creates a MSSQL ConnectionPool-compatible wrapper around a pg.Pool.
|
|
771
1138
|
* This allows existing code that references DataSourceInfo.dataSource (typed as sql.ConnectionPool)
|
|
@@ -120,4 +120,40 @@ export class DatasetStatusResolver extends ResolverBase {
|
|
|
120
120
|
throw new Error('Error retrieving Dataset Status: ' + DatasetName + '\n\n' + err);
|
|
121
121
|
}
|
|
122
122
|
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Batch version: fetch status for multiple datasets in a single round-trip.
|
|
126
|
+
* Reduces N separate GetDatasetStatusByName calls to 1 network request.
|
|
127
|
+
*/
|
|
128
|
+
@Query(() => [DatasetStatusResultType])
|
|
129
|
+
async GetMultipleDatasetStatusByName(
|
|
130
|
+
@Arg('DatasetNames', () => [String]) DatasetNames: string[],
|
|
131
|
+
@Ctx() { providers, userPayload }: AppContext,
|
|
132
|
+
): Promise<DatasetStatusResultType[]> {
|
|
133
|
+
const md = GetReadOnlyProvider(providers, {allowFallbackToReadWrite: true});
|
|
134
|
+
const results: DatasetStatusResultType[] = [];
|
|
135
|
+
|
|
136
|
+
// Execute all status checks in parallel
|
|
137
|
+
const statusPromises = DatasetNames.map(async (name) => {
|
|
138
|
+
await this.CheckAPIKeyScopeAuthorization('dataset:read', name, userPayload);
|
|
139
|
+
return md.GetDatasetStatusByName(name);
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
const statuses = await Promise.all(statusPromises);
|
|
143
|
+
|
|
144
|
+
for (const result of statuses) {
|
|
145
|
+
if (result) {
|
|
146
|
+
results.push({
|
|
147
|
+
DatasetID: result.DatasetID,
|
|
148
|
+
DatasetName: result.DatasetName,
|
|
149
|
+
Success: result.Success,
|
|
150
|
+
Status: result.Status,
|
|
151
|
+
LatestUpdateDate: result.LatestUpdateDate,
|
|
152
|
+
EntityUpdateDates: JSON.stringify(result.EntityUpdateDates),
|
|
153
|
+
} as DatasetStatusResultType);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
return results;
|
|
158
|
+
}
|
|
123
159
|
}
|