@memberjunction/server 6.1.0-edge.2 → 6.1.0-edge.4
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/LICENSE +180 -4
- package/dist/agentSessions/SessionManager.d.ts +19 -5
- package/dist/agentSessions/SessionManager.d.ts.map +1 -1
- package/dist/agentSessions/SessionManager.js +27 -9
- package/dist/agentSessions/SessionManager.js.map +1 -1
- package/dist/auth/AuthProviderCatalogRouter.d.ts +31 -0
- package/dist/auth/AuthProviderCatalogRouter.d.ts.map +1 -0
- package/dist/auth/AuthProviderCatalogRouter.js +69 -0
- package/dist/auth/AuthProviderCatalogRouter.js.map +1 -0
- package/dist/auth/AuthProviderEngine.d.ts +96 -0
- package/dist/auth/AuthProviderEngine.d.ts.map +1 -0
- package/dist/auth/AuthProviderEngine.js +245 -0
- package/dist/auth/AuthProviderEngine.js.map +1 -0
- package/dist/auth/index.d.ts +12 -0
- package/dist/auth/index.d.ts.map +1 -1
- package/dist/auth/index.js +57 -13
- package/dist/auth/index.js.map +1 -1
- package/dist/auth/initializeProviders.d.ts +64 -1
- package/dist/auth/initializeProviders.d.ts.map +1 -1
- package/dist/auth/initializeProviders.js +117 -5
- package/dist/auth/initializeProviders.js.map +1 -1
- package/dist/auth/scopeLimitedPrincipal.d.ts +39 -0
- package/dist/auth/scopeLimitedPrincipal.d.ts.map +1 -0
- package/dist/auth/scopeLimitedPrincipal.js +49 -0
- package/dist/auth/scopeLimitedPrincipal.js.map +1 -0
- package/dist/config.d.ts +89 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +52 -35
- package/dist/config.js.map +1 -1
- package/dist/context.d.ts.map +1 -1
- package/dist/context.js +21 -1
- package/dist/context.js.map +1 -1
- package/dist/entitySubclasses/MJEntityPermissionEntityServer.server.d.ts.map +1 -1
- package/dist/entitySubclasses/MJEntityPermissionEntityServer.server.js +8 -7
- package/dist/entitySubclasses/MJEntityPermissionEntityServer.server.js.map +1 -1
- package/dist/generated/generated.d.ts +619 -1597
- package/dist/generated/generated.d.ts.map +1 -1
- package/dist/generated/generated.js +3341 -17021
- package/dist/generated/generated.js.map +1 -1
- package/dist/generic/ResolverBase.d.ts +28 -1
- package/dist/generic/ResolverBase.d.ts.map +1 -1
- package/dist/generic/ResolverBase.js +57 -8
- package/dist/generic/ResolverBase.js.map +1 -1
- package/dist/generic/RunViewResolver.d.ts +4 -0
- package/dist/generic/RunViewResolver.d.ts.map +1 -1
- package/dist/generic/RunViewResolver.js +31 -0
- package/dist/generic/RunViewResolver.js.map +1 -1
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +147 -14
- package/dist/index.js.map +1 -1
- package/dist/integration/CustomColumnPromoter.d.ts +79 -15
- package/dist/integration/CustomColumnPromoter.d.ts.map +1 -1
- package/dist/integration/CustomColumnPromoter.js +472 -97
- package/dist/integration/CustomColumnPromoter.js.map +1 -1
- package/dist/integration/InactiveRowWarnings.d.ts +48 -0
- package/dist/integration/InactiveRowWarnings.d.ts.map +1 -0
- package/dist/integration/InactiveRowWarnings.js +57 -0
- package/dist/integration/InactiveRowWarnings.js.map +1 -0
- package/dist/integration/RSUProgressBridge.d.ts +83 -0
- package/dist/integration/RSUProgressBridge.d.ts.map +1 -0
- package/dist/integration/RSUProgressBridge.js +230 -0
- package/dist/integration/RSUProgressBridge.js.map +1 -0
- package/dist/integration/SchemaRefreshLaunch.d.ts +62 -0
- package/dist/integration/SchemaRefreshLaunch.d.ts.map +1 -0
- package/dist/integration/SchemaRefreshLaunch.js +67 -0
- package/dist/integration/SchemaRefreshLaunch.js.map +1 -0
- package/dist/providerConfigUnits.d.ts +20 -0
- package/dist/providerConfigUnits.d.ts.map +1 -0
- package/dist/providerConfigUnits.js +22 -0
- package/dist/providerConfigUnits.js.map +1 -0
- package/dist/resolvers/AdhocQueryResolver.d.ts +4 -0
- package/dist/resolvers/AdhocQueryResolver.d.ts.map +1 -1
- package/dist/resolvers/AdhocQueryResolver.js +20 -6
- package/dist/resolvers/AdhocQueryResolver.js.map +1 -1
- package/dist/resolvers/FileResolver.d.ts +34 -4
- package/dist/resolvers/FileResolver.d.ts.map +1 -1
- package/dist/resolvers/FileResolver.js +218 -28
- package/dist/resolvers/FileResolver.js.map +1 -1
- package/dist/resolvers/IdentityClaimRedemptionResolver.d.ts +37 -0
- package/dist/resolvers/IdentityClaimRedemptionResolver.d.ts.map +1 -0
- package/dist/resolvers/IdentityClaimRedemptionResolver.js +220 -0
- package/dist/resolvers/IdentityClaimRedemptionResolver.js.map +1 -0
- package/dist/resolvers/IntegrationDiscoveryResolver.d.ts +108 -3
- package/dist/resolvers/IntegrationDiscoveryResolver.d.ts.map +1 -1
- package/dist/resolvers/IntegrationDiscoveryResolver.js +374 -87
- package/dist/resolvers/IntegrationDiscoveryResolver.js.map +1 -1
- package/dist/resolvers/QueryResolver.d.ts +5 -4
- package/dist/resolvers/QueryResolver.d.ts.map +1 -1
- package/dist/resolvers/QueryResolver.js +29 -14
- package/dist/resolvers/QueryResolver.js.map +1 -1
- package/dist/resolvers/QuerySystemUserResolver.d.ts +12 -3
- package/dist/resolvers/QuerySystemUserResolver.d.ts.map +1 -1
- package/dist/resolvers/QuerySystemUserResolver.js +33 -14
- package/dist/resolvers/QuerySystemUserResolver.js.map +1 -1
- package/dist/resolvers/RunAIAgentResolver.d.ts +3 -1
- package/dist/resolvers/RunAIAgentResolver.d.ts.map +1 -1
- package/dist/resolvers/RunAIAgentResolver.js +20 -7
- package/dist/resolvers/RunAIAgentResolver.js.map +1 -1
- package/dist/resolvers/TaskGraphFrameResolver.d.ts +18 -0
- package/dist/resolvers/TaskGraphFrameResolver.d.ts.map +1 -1
- package/dist/resolvers/TaskGraphFrameResolver.js +75 -0
- package/dist/resolvers/TaskGraphFrameResolver.js.map +1 -1
- package/dist/resolvers/UserResolver.d.ts +19 -17
- package/dist/resolvers/UserResolver.d.ts.map +1 -1
- package/dist/resolvers/UserResolver.js +66 -67
- package/dist/resolvers/UserResolver.js.map +1 -1
- package/dist/resolvers/currentUserRoles.d.ts +19 -0
- package/dist/resolvers/currentUserRoles.d.ts.map +1 -0
- package/dist/resolvers/currentUserRoles.js +36 -0
- package/dist/resolvers/currentUserRoles.js.map +1 -0
- package/dist/resolvers/meetingRecordingRegistration.d.ts.map +1 -1
- package/dist/resolvers/meetingRecordingRegistration.js +3 -6
- package/dist/resolvers/meetingRecordingRegistration.js.map +1 -1
- package/dist/rest/MediaAccessKeys.d.ts +17 -0
- package/dist/rest/MediaAccessKeys.d.ts.map +1 -1
- package/dist/rest/MediaAccessKeys.js +42 -1
- package/dist/rest/MediaAccessKeys.js.map +1 -1
- package/dist/rest/MediaStreamHandler.d.ts +3 -2
- package/dist/rest/MediaStreamHandler.d.ts.map +1 -1
- package/dist/rest/MediaStreamHandler.js +83 -2
- package/dist/rest/MediaStreamHandler.js.map +1 -1
- package/dist/rest/OAuthCallbackHandler.d.ts +49 -1
- package/dist/rest/OAuthCallbackHandler.d.ts.map +1 -1
- package/dist/rest/OAuthCallbackHandler.js +129 -43
- package/dist/rest/OAuthCallbackHandler.js.map +1 -1
- package/dist/rest/UploadTokenManager.d.ts +90 -0
- package/dist/rest/UploadTokenManager.d.ts.map +1 -0
- package/dist/rest/UploadTokenManager.js +156 -0
- package/dist/rest/UploadTokenManager.js.map +1 -0
- package/dist/services/IntegrationSyncWorkerService.d.ts +48 -0
- package/dist/services/IntegrationSyncWorkerService.d.ts.map +1 -0
- package/dist/services/IntegrationSyncWorkerService.js +132 -0
- package/dist/services/IntegrationSyncWorkerService.js.map +1 -0
- package/dist/services/StartTaskGraphDispatcher.d.ts +0 -10
- package/dist/services/StartTaskGraphDispatcher.d.ts.map +1 -1
- package/dist/services/StartTaskGraphDispatcher.js +21 -1
- package/dist/services/StartTaskGraphDispatcher.js.map +1 -1
- package/dist/services/TaskGraphAgentRunner.d.ts +7 -0
- package/dist/services/TaskGraphAgentRunner.d.ts.map +1 -1
- package/dist/services/TaskGraphAgentRunner.js +23 -0
- package/dist/services/TaskGraphAgentRunner.js.map +1 -1
- package/dist/services/TaskGraphContinuationDeliverer.d.ts.map +1 -1
- package/dist/services/TaskGraphContinuationDeliverer.js +12 -1
- package/dist/services/TaskGraphContinuationDeliverer.js.map +1 -1
- package/dist/types.d.ts +14 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js.map +1 -1
- package/package.json +92 -92
- package/src/__tests__/APIKeyScopeAuth.test.ts +475 -0
- package/src/__tests__/AdhocQueryResolver.scopeGuard.test.ts +84 -0
- package/src/__tests__/AuthProviderEngine.test.ts +278 -0
- package/src/__tests__/CustomColumnPromoterOnePass.test.ts +387 -0
- package/src/__tests__/EntityCRUDHandler.test.ts +378 -0
- package/src/__tests__/InactiveRowWarnings.test.ts +87 -0
- package/src/__tests__/IntegrationSyncWorkerService.test.ts +275 -0
- package/src/__tests__/OAuthCallbackHandler.openRedirect.test.ts +117 -0
- package/src/__tests__/OAuthCallbackHandler.xss.test.ts +4 -1
- package/src/__tests__/RESTEndpointHandler.test.ts +672 -0
- package/src/__tests__/RSUProgressBridge.test.ts +250 -0
- package/src/__tests__/ReactivateRefreshMessage.test.ts +85 -0
- package/src/__tests__/RealtimeBridgeResolver.test.ts +10 -2
- package/src/__tests__/ResolverBase.filterEscaping.test.ts +147 -0
- package/src/__tests__/SchemaRefreshLaunch.test.ts +114 -0
- package/src/__tests__/SessionManager.test.ts +55 -0
- package/src/__tests__/UserResolver.currentUser.test.ts +87 -0
- package/src/__tests__/ViewOperationsHandler.test.ts +277 -0
- package/src/__tests__/config-units.test.ts +74 -0
- package/src/__tests__/initializeProviders.refresh.test.ts +146 -0
- package/src/__tests__/mjapi-bootstrap.test.ts +0 -6
- package/src/__tests__/newUsers.test.ts +726 -0
- package/src/__tests__/uploadTokenManager.test.ts +159 -0
- package/src/agentSessions/SessionManager.ts +27 -9
- package/src/auth/AuthProviderCatalogRouter.ts +76 -0
- package/src/auth/AuthProviderEngine.ts +253 -0
- package/src/auth/index.ts +60 -15
- package/src/auth/initializeProviders.ts +128 -7
- package/src/auth/scopeLimitedPrincipal.ts +50 -0
- package/src/config.ts +55 -36
- package/src/context.ts +26 -2
- package/src/entitySubclasses/MJEntityPermissionEntityServer.server.ts +8 -7
- package/src/generated/generated.ts +2347 -10551
- package/src/generic/ResolverBase.ts +67 -8
- package/src/generic/RunViewResolver.ts +31 -0
- package/src/index.ts +165 -16
- package/src/integration/CustomColumnPromoter.ts +520 -98
- package/src/integration/InactiveRowWarnings.ts +85 -0
- package/src/integration/RSUProgressBridge.ts +248 -0
- package/src/integration/SchemaRefreshLaunch.ts +93 -0
- package/src/providerConfigUnits.ts +21 -0
- package/src/resolvers/AdhocQueryResolver.ts +21 -6
- package/src/resolvers/FileResolver.ts +200 -40
- package/src/resolvers/IdentityClaimRedemptionResolver.ts +184 -0
- package/src/resolvers/IntegrationDiscoveryResolver.ts +390 -71
- package/src/resolvers/QueryResolver.ts +25 -11
- package/src/resolvers/QuerySystemUserResolver.ts +28 -13
- package/src/resolvers/RunAIAgentResolver.ts +21 -6
- package/src/resolvers/TaskGraphFrameResolver.ts +67 -0
- package/src/resolvers/UserResolver.ts +99 -60
- package/src/resolvers/currentUserRoles.ts +47 -0
- package/src/resolvers/meetingRecordingRegistration.ts +3 -7
- package/src/rest/MediaAccessKeys.ts +47 -1
- package/src/rest/MediaStreamHandler.ts +98 -3
- package/src/rest/OAuthCallbackHandler.ts +149 -41
- package/src/rest/UploadTokenManager.ts +211 -0
- package/src/services/IntegrationSyncWorkerService.ts +141 -0
- package/src/services/StartTaskGraphDispatcher.ts +21 -1
- package/src/services/TaskGraphAgentRunner.ts +23 -0
- package/src/services/TaskGraphContinuationDeliverer.ts +11 -1
- package/src/types.ts +14 -0
|
@@ -31,7 +31,7 @@ import { httpTransport, CloudEvent, emitterFor } from 'cloudevents';
|
|
|
31
31
|
import { RunViewGenericParams, UserPayload } from '../types.js';
|
|
32
32
|
import { RunDynamicViewInput, RunViewByIDInput, RunViewByNameInput } from './RunViewResolver.js';
|
|
33
33
|
import { DeleteOptionsInput } from './DeleteOptionsInput.js';
|
|
34
|
-
import { MJEvent, MJEventType, MJGlobal, ENCRYPTED_SENTINEL, IsValueEncrypted, IsOnlyTimezoneShift } from '@memberjunction/global';
|
|
34
|
+
import { MJEvent, MJEventType, MJGlobal, ENCRYPTED_SENTINEL, EscapeSQLString, IsValueEncrypted, IsOnlyTimezoneShift } from '@memberjunction/global';
|
|
35
35
|
import { EncryptionEngine } from '@memberjunction/encryption';
|
|
36
36
|
import { PUSH_STATUS_UPDATES_TOPIC, publishStatusUpdate } from './PushStatusResolver.js';
|
|
37
37
|
import { CACHE_INVALIDATION_TOPIC } from './CacheInvalidationResolver.js';
|
|
@@ -341,6 +341,58 @@ export class ResolverBase {
|
|
|
341
341
|
return dataObjectArray;
|
|
342
342
|
}
|
|
343
343
|
|
|
344
|
+
/**
|
|
345
|
+
* Renders one `findBy` predicate value as the SQL literal text it will become inside the
|
|
346
|
+
* `ExtraFilter` that method builds.
|
|
347
|
+
*
|
|
348
|
+
* The two slots have different exposure, so they are handled differently:
|
|
349
|
+
*
|
|
350
|
+
* - **Quoted slot** (`NeedsQuotes` — every type except Number and Boolean). The value sits
|
|
351
|
+
* inside a single-quoted literal, so it is escaped with `EscapeSQLString`: quotes are
|
|
352
|
+
* doubled and null bytes stripped, leaving the payload inert *inside* the literal.
|
|
353
|
+
* - **Unquoted slot** (Number and Boolean fields). There is no literal to break out of
|
|
354
|
+
* here — the value lands in the clause bare, so a string value simply *is* SQL and no
|
|
355
|
+
* amount of quote escaping helps. Only a genuine number or boolean is accepted; anything
|
|
356
|
+
* else is refused rather than interpolated. Numeric and boolean strings are accepted too
|
|
357
|
+
* (a client may legitimately send `"42"`), since neither can carry SQL.
|
|
358
|
+
*
|
|
359
|
+
* A `null`/`undefined` value is refused in either slot. `EscapeSQLString` maps both to `''`
|
|
360
|
+
* by design, which would silently turn a by-value lookup into `Field = ''` — for this
|
|
361
|
+
* caller a missing value is a bug, not a query.
|
|
362
|
+
*
|
|
363
|
+
* @param value the raw value supplied for the field
|
|
364
|
+
* @param fieldName field name, for the error message
|
|
365
|
+
* @param entity entity name, for the error message
|
|
366
|
+
* @param needsQuotes whether the field's type requires a quoted literal
|
|
367
|
+
* @returns the literal text (including quotes when the slot is quoted)
|
|
368
|
+
* @throws when the value cannot be rendered safely for the slot it is destined for
|
|
369
|
+
*/
|
|
370
|
+
private formatFilterValue(value: unknown, fieldName: string, entity: string, needsQuotes: boolean): string {
|
|
371
|
+
if (value === null || value === undefined) {
|
|
372
|
+
throw new Error(
|
|
373
|
+
`Field ${fieldName} in entity ${entity} was given a ${value === null ? 'null' : 'undefined'} value. findBy builds an equality predicate, which needs a value to compare against.`
|
|
374
|
+
);
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
if (needsQuotes) {
|
|
378
|
+
return `'${EscapeSQLString(String(value))}'`;
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
// Unquoted slot — accept only what cannot carry SQL, and render it exactly as before.
|
|
382
|
+
if (typeof value === 'boolean' || (typeof value === 'number' && Number.isFinite(value))) {
|
|
383
|
+
return String(value);
|
|
384
|
+
}
|
|
385
|
+
if (typeof value === 'string') {
|
|
386
|
+
const trimmed = value.trim();
|
|
387
|
+
if (/^-?\d+(\.\d+)?$/.test(trimmed)) return trimmed;
|
|
388
|
+
if (/^(true|false)$/i.test(trimmed)) return trimmed.toLowerCase();
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
throw new Error(
|
|
392
|
+
`Field ${fieldName} in entity ${entity} is a numeric or boolean field, so its value is not quoted in the filter — only a number or boolean can be used. Received: ${typeof value}.`
|
|
393
|
+
);
|
|
394
|
+
}
|
|
395
|
+
|
|
344
396
|
protected async findBy<T = any>(provider: DatabaseProviderBase, entity: string, params: any, contextUser: UserInfo): Promise<Array<T>> {
|
|
345
397
|
// build the SQL query based on the params passed in
|
|
346
398
|
const rv = provider as any as IRunViewProvider;
|
|
@@ -354,8 +406,7 @@ export class ResolverBase {
|
|
|
354
406
|
// look up the field in the entityInfo to see if it needs quotes
|
|
355
407
|
const field = e.Fields.find((f) => f.Name === k);
|
|
356
408
|
if (!field) throw new Error(`Field ${k} not found in entity ${entity}`);
|
|
357
|
-
|
|
358
|
-
extraFilter += `${k} = ${quotes}${params[k]}${quotes}`;
|
|
409
|
+
extraFilter += `${k} = ${this.formatFilterValue(params[k], k, entity, field.NeedsQuotes)}`;
|
|
359
410
|
});
|
|
360
411
|
|
|
361
412
|
// ok, now we have a SQL string, run it and return the results
|
|
@@ -382,7 +433,8 @@ export class ResolverBase {
|
|
|
382
433
|
const rv = provider as any as IRunViewProvider;
|
|
383
434
|
const result = await rv.RunView<MJUserViewEntityExtended>({
|
|
384
435
|
EntityName: 'MJ: User Views',
|
|
385
|
-
|
|
436
|
+
// SECURITY: the view name is client-supplied and lands inside a SQL literal.
|
|
437
|
+
ExtraFilter: "Name='" + EscapeSQLString(viewInput.ViewName) + "'",
|
|
386
438
|
}, userPayload.userRecord);
|
|
387
439
|
if (result && result.Success && result.Results.length > 0) {
|
|
388
440
|
const viewInfo = result.Results[0];
|
|
@@ -408,7 +460,8 @@ export class ResolverBase {
|
|
|
408
460
|
viewInput.AfterKey
|
|
409
461
|
? CompositeKey.FromKeyValuePairs((viewInput.AfterKey as { KeyValuePairs: { FieldName: string; Value: string }[] }).KeyValuePairs)
|
|
410
462
|
: undefined,
|
|
411
|
-
viewInput.BypassCache
|
|
463
|
+
viewInput.BypassCache,
|
|
464
|
+
viewInput.DataSource
|
|
412
465
|
);
|
|
413
466
|
}
|
|
414
467
|
else {
|
|
@@ -451,7 +504,8 @@ export class ResolverBase {
|
|
|
451
504
|
viewInput.StartRow,
|
|
452
505
|
viewInput.Aggregates,
|
|
453
506
|
undefined,
|
|
454
|
-
viewInput.BypassCache
|
|
507
|
+
viewInput.BypassCache,
|
|
508
|
+
viewInput.DataSource
|
|
455
509
|
);
|
|
456
510
|
} catch (err) {
|
|
457
511
|
console.log(err);
|
|
@@ -497,7 +551,8 @@ export class ResolverBase {
|
|
|
497
551
|
viewInput.StartRow,
|
|
498
552
|
viewInput.Aggregates,
|
|
499
553
|
undefined,
|
|
500
|
-
viewInput.BypassCache
|
|
554
|
+
viewInput.BypassCache,
|
|
555
|
+
viewInput.DataSource
|
|
501
556
|
);
|
|
502
557
|
} catch (err) {
|
|
503
558
|
console.log(err);
|
|
@@ -571,6 +626,7 @@ export class ResolverBase {
|
|
|
571
626
|
userPayload,
|
|
572
627
|
aggregates: viewInput.Aggregates,
|
|
573
628
|
bypassCache: viewInput.BypassCache,
|
|
629
|
+
dataSource: viewInput.DataSource,
|
|
574
630
|
});
|
|
575
631
|
} catch (err) {
|
|
576
632
|
LogError(err);
|
|
@@ -767,7 +823,8 @@ export class ResolverBase {
|
|
|
767
823
|
startRow: number | undefined,
|
|
768
824
|
aggregates?: AggregateExpression[],
|
|
769
825
|
afterKey?: CompositeKey,
|
|
770
|
-
bypassCache?: boolean
|
|
826
|
+
bypassCache?: boolean,
|
|
827
|
+
dataSource?: 'Live' | 'Materialized'
|
|
771
828
|
) {
|
|
772
829
|
try {
|
|
773
830
|
if (!viewInfo || !userPayload) return null;
|
|
@@ -837,6 +894,7 @@ export class ResolverBase {
|
|
|
837
894
|
ResultType: rt,
|
|
838
895
|
Aggregates: aggregates,
|
|
839
896
|
BypassCache: bypassCache,
|
|
897
|
+
DataSource: dataSource,
|
|
840
898
|
},
|
|
841
899
|
user
|
|
842
900
|
);
|
|
@@ -972,6 +1030,7 @@ export class ResolverBase {
|
|
|
972
1030
|
ResultType: rt,
|
|
973
1031
|
Aggregates: param.aggregates,
|
|
974
1032
|
BypassCache: param.bypassCache,
|
|
1033
|
+
DataSource: param.dataSource,
|
|
975
1034
|
});
|
|
976
1035
|
}
|
|
977
1036
|
|
|
@@ -177,6 +177,13 @@ export class RunViewByIDInput {
|
|
|
177
177
|
'Optional, when true bypasses ALL server-side caching for this view run — the pre-check cache lookup is skipped and the result is not stored in the cache. Use for maintenance/audit queries that must see true database state, or to force-refresh views whose filters reference rows the server cache invalidator cannot follow (e.g., cross-entity subqueries against vwListDetails).',
|
|
178
178
|
})
|
|
179
179
|
BypassCache?: boolean;
|
|
180
|
+
|
|
181
|
+
@Field(() => String, {
|
|
182
|
+
nullable: true,
|
|
183
|
+
description:
|
|
184
|
+
"Optional source-of-truth selector for a base-view materialization: 'Live' (default) reads the entity's live base view; 'Materialized' reads its materialized snapshot wrapper view (materialized_vw<CodeName>). Only request 'Materialized' for an entity that actually has a base-view materialization: it resolves to the wrapper view by naming convention with NO existence check or fallback, so requesting it for a non-materialized entity targets a view that does not exist and the query errors.",
|
|
185
|
+
})
|
|
186
|
+
DataSource?: 'Live' | 'Materialized';
|
|
180
187
|
}
|
|
181
188
|
|
|
182
189
|
@InputType()
|
|
@@ -291,6 +298,13 @@ export class RunViewByNameInput {
|
|
|
291
298
|
'Optional, when true bypasses ALL server-side caching for this view run — the pre-check cache lookup is skipped and the result is not stored in the cache. Use for maintenance/audit queries that must see true database state, or to force-refresh views whose filters reference rows the server cache invalidator cannot follow (e.g., cross-entity subqueries against vwListDetails).',
|
|
292
299
|
})
|
|
293
300
|
BypassCache?: boolean;
|
|
301
|
+
|
|
302
|
+
@Field(() => String, {
|
|
303
|
+
nullable: true,
|
|
304
|
+
description:
|
|
305
|
+
"Optional source-of-truth selector for a base-view materialization: 'Live' (default) reads the entity's live base view; 'Materialized' reads its materialized snapshot wrapper view (materialized_vw<CodeName>). Only request 'Materialized' for an entity that actually has a base-view materialization: it resolves to the wrapper view by naming convention with NO existence check or fallback, so requesting it for a non-materialized entity targets a view that does not exist and the query errors.",
|
|
306
|
+
})
|
|
307
|
+
DataSource?: 'Live' | 'Materialized';
|
|
294
308
|
}
|
|
295
309
|
|
|
296
310
|
@InputType()
|
|
@@ -391,6 +405,13 @@ export class RunDynamicViewInput {
|
|
|
391
405
|
'Optional, when true bypasses ALL server-side caching for this view run — the pre-check cache lookup is skipped and the result is not stored in the cache. Use for maintenance/audit queries that must see true database state, or to force-refresh views whose filters reference rows the server cache invalidator cannot follow (e.g., cross-entity subqueries against vwListDetails).',
|
|
392
406
|
})
|
|
393
407
|
BypassCache?: boolean;
|
|
408
|
+
|
|
409
|
+
@Field(() => String, {
|
|
410
|
+
nullable: true,
|
|
411
|
+
description:
|
|
412
|
+
"Optional source-of-truth selector for a base-view materialization: 'Live' (default) reads the entity's live base view; 'Materialized' reads its materialized snapshot wrapper view (materialized_vw<CodeName>). Only request 'Materialized' for an entity that actually has a base-view materialization: it resolves to the wrapper view by naming convention with NO existence check or fallback, so requesting it for a non-materialized entity targets a view that does not exist and the query errors.",
|
|
413
|
+
})
|
|
414
|
+
DataSource?: 'Live' | 'Materialized';
|
|
394
415
|
}
|
|
395
416
|
|
|
396
417
|
@InputType()
|
|
@@ -520,6 +541,13 @@ export class RunViewGenericInput {
|
|
|
520
541
|
'Optional, when true bypasses ALL server-side caching for this view run — the pre-check cache lookup is skipped and the result is not stored in the cache. Use for maintenance/audit queries that must see true database state, or to force-refresh views whose filters reference rows the server cache invalidator cannot follow (e.g., cross-entity subqueries against vwListDetails).',
|
|
521
542
|
})
|
|
522
543
|
BypassCache?: boolean;
|
|
544
|
+
|
|
545
|
+
@Field(() => String, {
|
|
546
|
+
nullable: true,
|
|
547
|
+
description:
|
|
548
|
+
"Optional source-of-truth selector for a base-view materialization: 'Live' (default) reads the entity's live base view; 'Materialized' reads its materialized snapshot wrapper view (materialized_vw<CodeName>). Only request 'Materialized' for an entity that actually has a base-view materialization: it resolves to the wrapper view by naming convention with NO existence check or fallback, so requesting it for a non-materialized entity targets a view that does not exist and the query errors.",
|
|
549
|
+
})
|
|
550
|
+
DataSource?: 'Live' | 'Materialized';
|
|
523
551
|
}
|
|
524
552
|
|
|
525
553
|
//****************************************************************************
|
|
@@ -1082,6 +1110,9 @@ export class RunViewResolver extends ResolverBase {
|
|
|
1082
1110
|
// Forward the aggregate request to the engine (B40) — omitted here as well as in the
|
|
1083
1111
|
// client's input map, so aggregates never reached InternalRunView on this transport.
|
|
1084
1112
|
Aggregates: item.params.Aggregates,
|
|
1113
|
+
// DataSource must be forwarded on this transport too (same as the InternalRunView/RunViews maps):
|
|
1114
|
+
// otherwise a CacheLocal batch's DataSource:'Materialized' request silently reads the live view.
|
|
1115
|
+
DataSource: item.params.DataSource,
|
|
1085
1116
|
AfterKey: item.params.AfterKey
|
|
1086
1117
|
? CompositeKey.FromKeyValuePairs(item.params.AfterKey.KeyValuePairs)
|
|
1087
1118
|
: undefined,
|
package/src/index.ts
CHANGED
|
@@ -9,7 +9,7 @@ import { UserCache, resolveDbPlatformFromEnv } from '@memberjunction/generic-dat
|
|
|
9
9
|
import { MJGlobal, MJEventType, UUIDsEqual, ShutdownRegistry } from '@memberjunction/global';
|
|
10
10
|
import { setupSQLServerClient, SQLServerDataProvider, SQLServerProviderConfigData } from '@memberjunction/sqlserver-dataprovider';
|
|
11
11
|
import { extendConnectionPoolWithQuery } from './util.js';
|
|
12
|
-
import { registerIntegrationCustomColumnPromoter } from './integration/CustomColumnPromoter.js';
|
|
12
|
+
import { registerIntegrationCustomColumnPromoter, IntegrationCustomColumnPromoter } from './integration/CustomColumnPromoter.js';
|
|
13
13
|
import { DisableUnselectedEntityMaps, ReenableFieldMapsForEntityMap } from './integration/EntityMapLifecycle.js';
|
|
14
14
|
import { default as BodyParser } from 'body-parser';
|
|
15
15
|
import compression from 'compression'; // Add compression middleware
|
|
@@ -57,14 +57,17 @@ import { DataSourceInfo, raiseEvent } from './types.js';
|
|
|
57
57
|
|
|
58
58
|
import { ExternalChangeDetectorEngine } from '@memberjunction/external-change-detection';
|
|
59
59
|
import { ScheduledJobsService } from './services/ScheduledJobsService.js';
|
|
60
|
+
import { IntegrationSyncWorkerService } from './services/IntegrationSyncWorkerService.js';
|
|
60
61
|
import { LocalCacheManager, StartupManager, TelemetryManager, TelemetryLevel, LogStatus, SetVerboseLogging } from '@memberjunction/core';
|
|
61
|
-
import { getSystemUser } from './auth/index.js';
|
|
62
|
+
import { getSystemUser, validateAuthProvidersRegistered } from './auth/index.js';
|
|
63
|
+
import { createAuthProviderCatalogRouter, AUTH_CATALOG_MOUNT_PATH } from './auth/AuthProviderCatalogRouter.js';
|
|
62
64
|
import { GetAPIKeyEngine } from '@memberjunction/api-keys';
|
|
63
65
|
import { RedisLocalStorageProvider } from '@memberjunction/redis-provider';
|
|
64
66
|
import { GenericDatabaseProvider } from '@memberjunction/generic-database-provider';
|
|
65
67
|
import { PubSubManager } from './generic/PubSubManager.js';
|
|
66
68
|
import { IntegrationProgressEmitter } from '@memberjunction/integration-progress-artifacts';
|
|
67
69
|
import { PublishIntegrationProgress } from './resolvers/IntegrationProgressResolver.js';
|
|
70
|
+
import { RegisterRSUProgressBridge } from './integration/RSUProgressBridge.js';
|
|
68
71
|
import { ClientToolRequestManager, AgentRunWatchdog } from '@memberjunction/ai-agents';
|
|
69
72
|
import { SessionJanitor } from './agentSessions/index.js';
|
|
70
73
|
import { StartTaskGraphDispatcher } from './services/StartTaskGraphDispatcher.js';
|
|
@@ -79,9 +82,12 @@ import {
|
|
|
79
82
|
MJScheduledJobEntity,
|
|
80
83
|
} from '@memberjunction/core-entities';
|
|
81
84
|
import { ServerExtensionLoader, ServerExtensionConfig } from '@memberjunction/server-extensions-core';
|
|
85
|
+
import { MetadataCacheRefreshIntervalSeconds } from './providerConfigUnits.js';
|
|
82
86
|
|
|
83
87
|
const cacheRefreshInterval = configInfo.databaseSettings.metadataCacheRefreshInterval;
|
|
84
88
|
|
|
89
|
+
export { MetadataCacheRefreshIntervalSeconds } from './providerConfigUnits.js';
|
|
90
|
+
|
|
85
91
|
/**
|
|
86
92
|
* Returns the configured database platform from the `DB_PLATFORM` environment
|
|
87
93
|
* variable, falling back to `'sqlserver'` when the env var is unset. An
|
|
@@ -100,6 +106,10 @@ export function getDbType(): DatabasePlatform {
|
|
|
100
106
|
|
|
101
107
|
export { MaxLength } from 'class-validator';
|
|
102
108
|
export * from 'type-graphql';
|
|
109
|
+
// Named re-export so Open App generated resolvers get a live ESM binding for
|
|
110
|
+
// Int/Float/ID. `export *` from type-graphql can leave these undefined for
|
|
111
|
+
// later importers, which makes schema build fail on ViewResult.RowCount.
|
|
112
|
+
export { Int, Float, ID } from 'type-graphql';
|
|
103
113
|
export { NewUserBase } from './auth/newUsers.js';
|
|
104
114
|
export { configInfo, DEFAULT_SERVER_CONFIG } from './config.js';
|
|
105
115
|
export { ServerExtensionLoader, BaseServerExtension } from '@memberjunction/server-extensions-core';
|
|
@@ -135,6 +145,7 @@ export * from './resolvers/RunClusterAnalysisResolver.js';
|
|
|
135
145
|
export * from './resolvers/GenerateSeedTaxonomyResolver.js';
|
|
136
146
|
export * from './resolvers/PipelineProgressResolver.js';
|
|
137
147
|
export * from './resolvers/IntegrationProgressResolver.js';
|
|
148
|
+
export * from './resolvers/IdentityClaimRedemptionResolver.js';
|
|
138
149
|
export * from './resolvers/ClientToolRequestResolver.js';
|
|
139
150
|
export * from './resolvers/AutotagPipelineResolver.js';
|
|
140
151
|
export * from './resolvers/TagGovernanceResolver.js';
|
|
@@ -204,6 +215,7 @@ import type { RequestHandler, ErrorRequestHandler } from 'express';
|
|
|
204
215
|
import type { ApolloServerPlugin } from '@apollo/server';
|
|
205
216
|
import type { GraphQLSchema } from 'graphql';
|
|
206
217
|
import { BaseServerMiddleware } from './middleware/BaseServerMiddleware.js';
|
|
218
|
+
import { SuppressTaskGraphSubmission } from '@memberjunction/ai-core-plus';
|
|
207
219
|
|
|
208
220
|
export type MJServerOptions = {
|
|
209
221
|
onBeforeServe?: () => void | Promise<void>;
|
|
@@ -339,6 +351,10 @@ export const serve = async (resolverPaths: Array<string>, app: Application = cre
|
|
|
339
351
|
const pgStartupMode = ResolveStartupMode({ configValue: configInfo.startup?.mode, defaultMode: 'full' });
|
|
340
352
|
await StartupManagerImport.Instance.Startup(false, sysUser || backupSysUser, provider, { mode: pgStartupMode.mode });
|
|
341
353
|
|
|
354
|
+
// Both provider sources have now had their turn — config/env at module load, metadata via
|
|
355
|
+
// AuthProviderEngine's startup hook — so "no providers at all" is finally a meaningful check.
|
|
356
|
+
validateAuthProvidersRegistered();
|
|
357
|
+
|
|
342
358
|
// Monkey-patch SQLServerDataProvider.ExecuteSQLWithPool to support PostgreSQL
|
|
343
359
|
// Generated resolvers call this static method with bracket-quoted SQL.
|
|
344
360
|
// When the pool is our PG-compat wrapper, translate and execute via pg.Pool.
|
|
@@ -489,11 +505,16 @@ export const serve = async (resolverPaths: Array<string>, app: Application = cre
|
|
|
489
505
|
startupLog.LogIf('verbose', 'Read-only Connection Pool has been initialized.');
|
|
490
506
|
}
|
|
491
507
|
|
|
492
|
-
|
|
508
|
+
// cacheRefreshInterval is configured in ms; checkRefreshIntervalSeconds declares seconds — see providerConfigUnits.ts
|
|
509
|
+
const config = new SQLServerProviderConfigData(pool, mj_core_schema, MetadataCacheRefreshIntervalSeconds(cacheRefreshInterval));
|
|
493
510
|
// MJAPI is a long-running server, so entry-point default is 'full' engine pre-warm;
|
|
494
511
|
// MJ_STARTUP_MODE / mj.config.cjs startup.mode can override per the shared precedence chain
|
|
495
512
|
const startupMode = ResolveStartupMode({ configValue: configInfo.startup?.mode, defaultMode: 'full' });
|
|
496
513
|
await setupSQLServerClient(config, { mode: startupMode.mode });
|
|
514
|
+
|
|
515
|
+
// See the note on the PostgreSQL path above: this is the first point at which both the
|
|
516
|
+
// config/env providers and the metadata catalog have been registered.
|
|
517
|
+
validateAuthProvidersRegistered();
|
|
497
518
|
lap('Metadata + Provider Setup', tPhase);
|
|
498
519
|
startupLog.BeginPhase('Initializing data provider');
|
|
499
520
|
const md = new Metadata(); // global-provider-ok: bootstrap
|
|
@@ -524,7 +545,8 @@ export const serve = async (resolverPaths: Array<string>, app: Application = cre
|
|
|
524
545
|
await codegenPool.connect();
|
|
525
546
|
|
|
526
547
|
const { RuntimeSchemaManager } = await import('@memberjunction/schema-engine');
|
|
527
|
-
|
|
548
|
+
// Same ms→seconds seam as the main provider config above — see providerConfigUnits.ts
|
|
549
|
+
const codegenConfig = new SQLServerProviderConfigData(codegenPool, mj_core_schema, MetadataCacheRefreshIntervalSeconds(cacheRefreshInterval));
|
|
528
550
|
const codegenProvider = new SQLServerDataProvider();
|
|
529
551
|
await codegenProvider.Config(codegenConfig);
|
|
530
552
|
RuntimeSchemaManager.Instance.SetDDLProvider(codegenProvider);
|
|
@@ -835,6 +857,14 @@ export const serve = async (resolverPaths: Array<string>, app: Application = cre
|
|
|
835
857
|
})
|
|
836
858
|
);
|
|
837
859
|
|
|
860
|
+
// Give Runtime Schema Update runs the same durable, tailable event stream as syncs and connector
|
|
861
|
+
// builds. `IntegrationRunKind` has always had an 'RSU' kind and RUN_KIND_TO_TOPIC has always
|
|
862
|
+
// mapped it to an 'RSU' channel, but nothing published to it — the only live signal was polling
|
|
863
|
+
// RuntimeSchemaUpdateStatus, which reports the current step and keeps no history, and which goes
|
|
864
|
+
// silent entirely across the API restart the pipeline performs on itself. Registered AFTER the
|
|
865
|
+
// publish hook above so the first RSU event also reaches live subscribers.
|
|
866
|
+
RegisterRSUProgressBridge();
|
|
867
|
+
|
|
838
868
|
// Global listener: broadcast CACHE_INVALIDATION to all browser clients whenever
|
|
839
869
|
// ANY BaseEntity save/delete occurs on this server — regardless of whether it
|
|
840
870
|
// originated from a GraphQL mutation or internal server-side code (agents, actions,
|
|
@@ -1107,7 +1137,9 @@ export const serve = async (resolverPaths: Array<string>, app: Application = cre
|
|
|
1107
1137
|
const { callbackRouter, authenticatedRouter } = createOAuthCallbackHandler({
|
|
1108
1138
|
publicUrl: oauthPublicUrl,
|
|
1109
1139
|
successRedirectUrl: `${oauthPublicUrl}/oauth/success`,
|
|
1110
|
-
errorRedirectUrl: `${oauthPublicUrl}/oauth/error
|
|
1140
|
+
errorRedirectUrl: `${oauthPublicUrl}/oauth/error`,
|
|
1141
|
+
// Constrains where a caller-supplied frontendReturnUrl may point (open-redirect guard).
|
|
1142
|
+
allowedFrontendOrigins: configInfo.cors?.allowedOrigins ?? ['*']
|
|
1111
1143
|
});
|
|
1112
1144
|
oauthAuthenticatedRouter = authenticatedRouter;
|
|
1113
1145
|
|
|
@@ -1256,6 +1288,14 @@ export const serve = async (resolverPaths: Array<string>, app: Application = cre
|
|
|
1256
1288
|
res.status(allHealthy ? 200 : 503).json({ extensions: results });
|
|
1257
1289
|
});
|
|
1258
1290
|
|
|
1291
|
+
// ─── Public authentication-provider catalog (PUBLIC, before auth mw) ──────
|
|
1292
|
+
// The browser needs the provider list BEFORE it holds a token, so this is necessarily
|
|
1293
|
+
// unauthenticated and must mount ahead of the auth middleware. It publishes only the
|
|
1294
|
+
// non-secret allow-list (see AuthProviderEngine.GetPublicCatalog) — the same values a
|
|
1295
|
+
// single-provider SPA already compiled into its bundle.
|
|
1296
|
+
app.use(AUTH_CATALOG_MOUNT_PATH, cors<cors.CorsRequest>(), createAuthProviderCatalogRouter());
|
|
1297
|
+
startupLog.LogIf('verbose', `[Auth] Public provider catalog registered at ${AUTH_CATALOG_MOUNT_PATH}/providers`);
|
|
1298
|
+
|
|
1259
1299
|
// ─── Unified auth middleware (replaces both REST authMiddleware and contextFunction auth) ─────
|
|
1260
1300
|
app.use(createUnifiedAuthMiddleware(dataSources));
|
|
1261
1301
|
|
|
@@ -1354,6 +1394,20 @@ export const serve = async (resolverPaths: Array<string>, app: Application = cre
|
|
|
1354
1394
|
}
|
|
1355
1395
|
}
|
|
1356
1396
|
|
|
1397
|
+
// Initialize and start the integration sync worker if enabled (PR 1 item 8).
|
|
1398
|
+
// Off by default — deployments that don't opt in keep running syncs inline.
|
|
1399
|
+
let integrationSyncWorker: IntegrationSyncWorkerService | null = null;
|
|
1400
|
+
if (configInfo.integrationSyncWorker?.enabled) {
|
|
1401
|
+
try {
|
|
1402
|
+
integrationSyncWorker = new IntegrationSyncWorkerService(configInfo.integrationSyncWorker);
|
|
1403
|
+
await integrationSyncWorker.Initialize();
|
|
1404
|
+
integrationSyncWorker.Start();
|
|
1405
|
+
} catch (error) {
|
|
1406
|
+
console.error('❌ Failed to start integration sync worker:', error);
|
|
1407
|
+
// Don't throw — an unavailable worker must not prevent the API from serving
|
|
1408
|
+
}
|
|
1409
|
+
}
|
|
1410
|
+
|
|
1357
1411
|
// Data hooks are now registered via BaseServerMiddleware classes above
|
|
1358
1412
|
// (e.g., MJTenantFilterMiddleware registers PreRunView and PreSave hooks).
|
|
1359
1413
|
// No config-bag hook registration needed.
|
|
@@ -1425,7 +1479,15 @@ export const serve = async (resolverPaths: Array<string>, app: Application = cre
|
|
|
1425
1479
|
const taskGraphPool = dataSources[0]?.dataSource;
|
|
1426
1480
|
const taskGraphDispatcherDisabled = process.env.MJ_DISABLE_TASK_GRAPH_DISPATCHER === '1';
|
|
1427
1481
|
if (taskGraphDispatcherDisabled) {
|
|
1428
|
-
|
|
1482
|
+
// AND REFUSE SUBMISSIONS, not just execution (R3-11). The durable submitter registers through
|
|
1483
|
+
// the generated manifest unconditionally, so without this the host went on ACCEPTING graphs it
|
|
1484
|
+
// had no intention of running: the agent submitted, promised the user a follow-up, and parked
|
|
1485
|
+
// its run `Paused` — with the graph `Pending` and the run parked forever, no per-submission
|
|
1486
|
+
// diagnostics anywhere, and the stale graph executing hours later if anyone unset the flag.
|
|
1487
|
+
// The entity-action seam already had this treatment (its submitter registers inside
|
|
1488
|
+
// StartTaskGraphDispatcher); this gives the agent seam the same.
|
|
1489
|
+
SuppressTaskGraphSubmission('MJ_DISABLE_TASK_GRAPH_DISPATCHER=1 is set on this host');
|
|
1490
|
+
LogStatus('[TaskGraphDispatcher] Disabled by MJ_DISABLE_TASK_GRAPH_DISPATCHER=1 — this process will neither accept nor execute task graphs.');
|
|
1429
1491
|
} else if (resumeUser && taskGraphPool instanceof sql.ConnectionPool) {
|
|
1430
1492
|
StartTaskGraphDispatcher(taskGraphPool, resumeUser)
|
|
1431
1493
|
.catch(err => console.warn(`[TaskGraphDispatcher] Startup failed: ${err}`));
|
|
@@ -1472,6 +1534,17 @@ export const serve = async (resolverPaths: Array<string>, app: Application = cre
|
|
|
1472
1534
|
}
|
|
1473
1535
|
}
|
|
1474
1536
|
|
|
1537
|
+
// Stop the integration sync worker's polling. In-flight runs finish on their own —
|
|
1538
|
+
// their leases are heartbeat-renewed, and killing them here would only strand rows.
|
|
1539
|
+
if (integrationSyncWorker?.IsRunning) {
|
|
1540
|
+
try {
|
|
1541
|
+
integrationSyncWorker.Stop();
|
|
1542
|
+
console.log(`✅ Integration sync worker stopped (${integrationSyncWorker.InFlightCount} run(s) still in flight)`);
|
|
1543
|
+
} catch (error) {
|
|
1544
|
+
console.error('❌ Error stopping integration sync worker:', error);
|
|
1545
|
+
}
|
|
1546
|
+
}
|
|
1547
|
+
|
|
1475
1548
|
// Drain anything self-registered with ShutdownRegistry — QueueManager,
|
|
1476
1549
|
// future engines/services with timers/intervals/listeners. Each is
|
|
1477
1550
|
// responsible for being idempotent and not throwing.
|
|
@@ -1510,15 +1583,75 @@ export const serve = async (resolverPaths: Array<string>, app: Application = cre
|
|
|
1510
1583
|
});
|
|
1511
1584
|
};
|
|
1512
1585
|
|
|
1586
|
+
/**
|
|
1587
|
+
* Age at which an unprocessed `MJ: RSU Pending Works` row is reported as stranded.
|
|
1588
|
+
* A row older than this survived at least one full restart cycle without being completed.
|
|
1589
|
+
*/
|
|
1590
|
+
const RSU_PENDING_WORK_STALE_MINUTES = 30;
|
|
1591
|
+
|
|
1513
1592
|
/**
|
|
1514
1593
|
* Process pending RSU work left from a pre-restart Apply All.
|
|
1515
|
-
* Reads
|
|
1594
|
+
* Reads the durable `MJ: RSU Pending Works` queue, creates entity maps + field maps,
|
|
1595
|
+
* starts sync, and marks each row Completed only AFTER its work actually succeeded —
|
|
1596
|
+
* so a crash mid-processing leaves the row Pending and re-processable on the next boot.
|
|
1516
1597
|
*/
|
|
1598
|
+
/**
|
|
1599
|
+
* Complete a custom-column promotion whose DDL landed before the restart.
|
|
1600
|
+
*
|
|
1601
|
+
* Promotion registers this rather than finishing inline, because the restart is what loads the
|
|
1602
|
+
* regenerated entity classes — so the IntegrationObjectField rows, the field maps and the
|
|
1603
|
+
* overflow→column spread all belong on this side of it, where the columns are real typed
|
|
1604
|
+
* properties rather than dynamic .Get/.Set.
|
|
1605
|
+
*
|
|
1606
|
+
* The work itself lives on the promoter, so promotion logic stays in one class.
|
|
1607
|
+
*/
|
|
1608
|
+
async function ProcessPromoteColumnsPendingWork(
|
|
1609
|
+
// Typed structurally rather than by name: schema-engine is only reachable here through a dynamic
|
|
1610
|
+
// import (it is a workspace package, not published), so a static type import is not available.
|
|
1611
|
+
// Deriving the payload from CompletePromotion's own signature keeps the two in step regardless.
|
|
1612
|
+
item: { PromotedColumns?: Parameters<IntegrationCustomColumnPromoter['CompletePromotion']>[0] },
|
|
1613
|
+
pendingWorkID: string,
|
|
1614
|
+
rsm: {
|
|
1615
|
+
CompletePendingWork(id: string, user: unknown): Promise<unknown>;
|
|
1616
|
+
FailPendingWork(id: string, message: string, user: unknown): Promise<unknown>;
|
|
1617
|
+
},
|
|
1618
|
+
systemUser: ConstructorParameters<typeof IntegrationCustomColumnPromoter>[0],
|
|
1619
|
+
): Promise<void> {
|
|
1620
|
+
const promoted = item.PromotedColumns ?? [];
|
|
1621
|
+
if (promoted.length === 0) {
|
|
1622
|
+
// Nothing to do, but the row must not linger and be retried forever.
|
|
1623
|
+
await rsm.CompletePendingWork(pendingWorkID, systemUser);
|
|
1624
|
+
console.warn('[RSU] promote-columns pending work carried no PromotedColumns — nothing to complete.');
|
|
1625
|
+
return;
|
|
1626
|
+
}
|
|
1627
|
+
try {
|
|
1628
|
+
const promoter = new IntegrationCustomColumnPromoter(systemUser);
|
|
1629
|
+
const columns = await promoter.CompletePromotion(promoted);
|
|
1630
|
+
// Completed only after the work actually succeeded: a crash before this leaves the row
|
|
1631
|
+
// visible and re-processable, which is the whole point of the durable queue.
|
|
1632
|
+
await rsm.CompletePendingWork(pendingWorkID, systemUser);
|
|
1633
|
+
console.log(`[RSU] promote-columns: completed ${columns.length} column(s) across ${promoted.length} entity(ies).`);
|
|
1634
|
+
} catch (e) {
|
|
1635
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
1636
|
+
await rsm.FailPendingWork(pendingWorkID, `promote-columns completion failed: ${msg}`, systemUser);
|
|
1637
|
+
console.error(`[RSU] promote-columns completion failed: ${msg}`);
|
|
1638
|
+
}
|
|
1639
|
+
}
|
|
1640
|
+
|
|
1517
1641
|
async function processRSUPendingWork(): Promise<void> {
|
|
1518
1642
|
// Dynamic import — schema-engine is not yet published to npm, only exists as a workspace package
|
|
1519
1643
|
const { RuntimeSchemaManager } = await import('@memberjunction/schema-engine');
|
|
1520
1644
|
const rsm = RuntimeSchemaManager.Instance;
|
|
1521
|
-
|
|
1645
|
+
|
|
1646
|
+
// Get system user for server-side operations — needed to read the queue itself
|
|
1647
|
+
const systemUser = UserCache.Instance.Users.find(u => u.Type?.trim().toLowerCase() === 'owner') ?? UserCache.Instance.Users[0];
|
|
1648
|
+
if (!systemUser) {
|
|
1649
|
+
console.warn(`[RSU] No system user found — cannot process pending work`);
|
|
1650
|
+
return;
|
|
1651
|
+
}
|
|
1652
|
+
|
|
1653
|
+
// Rows older than this were left behind by an earlier process and are surfaced as stale.
|
|
1654
|
+
const pendingItems = await rsm.ReadPendingWork(systemUser, RSU_PENDING_WORK_STALE_MINUTES);
|
|
1522
1655
|
if (pendingItems.length === 0) return;
|
|
1523
1656
|
|
|
1524
1657
|
console.log(`[RSU] Processing ${pendingItems.length} pending work item(s) from pre-restart...`);
|
|
@@ -1526,18 +1659,26 @@ async function processRSUPendingWork(): Promise<void> {
|
|
|
1526
1659
|
// Wait a moment for metadata to be fully loaded
|
|
1527
1660
|
await new Promise(resolve => setTimeout(resolve, 3000));
|
|
1528
1661
|
|
|
1529
|
-
for (const
|
|
1662
|
+
for (const pending of pendingItems) {
|
|
1663
|
+
const pendingWorkID = pending.ID;
|
|
1664
|
+
const item = pending.Work;
|
|
1530
1665
|
try {
|
|
1531
1666
|
const md = new Metadata(); // global-provider-ok: server startup recovery — runs once before any per-request context exists
|
|
1532
|
-
// Get system user for server-side operations
|
|
1533
|
-
const systemUser = UserCache.Instance.Users.find(u => u.Type?.trim().toLowerCase() === 'owner') ?? UserCache.Instance.Users[0];
|
|
1534
|
-
if (!systemUser) {
|
|
1535
|
-
console.warn(`[RSU] No system user found, skipping pending work for ${item.CompanyIntegrationID}`);
|
|
1536
|
-
continue;
|
|
1537
|
-
}
|
|
1538
1667
|
|
|
1539
1668
|
await Metadata.Provider.Refresh(); // global-provider-ok: server startup recovery — one-shot global cache refresh
|
|
1540
1669
|
|
|
1670
|
+
// Custom-column promotion registers its follow-up here rather than finishing inline, because
|
|
1671
|
+
// the restart is what loads the regenerated entity classes. Everything downstream of the
|
|
1672
|
+
// ADD COLUMN — the IntegrationObjectField rows, the field maps, the overflow spread — runs
|
|
1673
|
+
// now, with typed access to the columns that did not exist in the previous process.
|
|
1674
|
+
//
|
|
1675
|
+
// Absent WorkType means apply-objects: every row written before that field existed is one,
|
|
1676
|
+
// and the branch below must keep treating it that way.
|
|
1677
|
+
if (item.WorkType === 'promote-columns') {
|
|
1678
|
+
await ProcessPromoteColumnsPendingWork(item, pendingWorkID, rsm, systemUser);
|
|
1679
|
+
continue;
|
|
1680
|
+
}
|
|
1681
|
+
|
|
1541
1682
|
// Resolve connector
|
|
1542
1683
|
const rv = new RunView();
|
|
1543
1684
|
const ciResult = await rv.RunView<MJCompanyIntegrationEntity>({
|
|
@@ -1547,6 +1688,7 @@ async function processRSUPendingWork(): Promise<void> {
|
|
|
1547
1688
|
}, systemUser);
|
|
1548
1689
|
const companyIntegration = ciResult.Results[0];
|
|
1549
1690
|
if (!companyIntegration) {
|
|
1691
|
+
await rsm.FailPendingWork(pendingWorkID, `CompanyIntegration ${item.CompanyIntegrationID} not found`, systemUser);
|
|
1550
1692
|
console.warn(`[RSU] CompanyIntegration ${item.CompanyIntegrationID} not found`);
|
|
1551
1693
|
continue;
|
|
1552
1694
|
}
|
|
@@ -1559,11 +1701,13 @@ async function processRSUPendingWork(): Promise<void> {
|
|
|
1559
1701
|
}, systemUser);
|
|
1560
1702
|
const integrationEntity = integrationResult.Results[0];
|
|
1561
1703
|
if (!integrationEntity) {
|
|
1704
|
+
await rsm.FailPendingWork(pendingWorkID, `Integration entity for ${integrationName} not found`, systemUser);
|
|
1562
1705
|
console.warn(`[RSU] Integration entity for ${integrationName} not found`);
|
|
1563
1706
|
continue;
|
|
1564
1707
|
}
|
|
1565
1708
|
const connector = ConnectorFactory.Resolve(integrationEntity);
|
|
1566
1709
|
if (!connector) {
|
|
1710
|
+
await rsm.FailPendingWork(pendingWorkID, `Connector for ${integrationName} not found`, systemUser);
|
|
1567
1711
|
console.warn(`[RSU] Connector for ${integrationName} not found`);
|
|
1568
1712
|
continue;
|
|
1569
1713
|
}
|
|
@@ -1788,8 +1932,13 @@ async function processRSUPendingWork(): Promise<void> {
|
|
|
1788
1932
|
console.warn(`[RSU] Schedule creation failed: ${schedErr}`);
|
|
1789
1933
|
}
|
|
1790
1934
|
}
|
|
1935
|
+
|
|
1936
|
+
// Only NOW is the work durably done — close the row out.
|
|
1937
|
+
await rsm.CompletePendingWork(pendingWorkID, systemUser);
|
|
1791
1938
|
} catch (err) {
|
|
1792
|
-
|
|
1939
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
1940
|
+
console.error(`[RSU] Failed to process pending work for ${item.CompanyIntegrationID}: ${message}`);
|
|
1941
|
+
await rsm.FailPendingWork(pendingWorkID, message, systemUser);
|
|
1793
1942
|
}
|
|
1794
1943
|
}
|
|
1795
1944
|
|