@memberjunction/server 6.1.0-edge.1 → 6.1.0-edge.3
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/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/exampleNewUserSubClass.js +1 -1
- package/dist/auth/exampleNewUserSubClass.js.map +1 -1
- package/dist/auth/index.d.ts +11 -0
- package/dist/auth/index.d.ts.map +1 -1
- package/dist/auth/index.js +65 -18
- 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/magicLink/MagicLinkService.js +1 -1
- package/dist/auth/magicLink/MagicLinkService.js.map +1 -1
- package/dist/auth/newUsers.js +1 -1
- package/dist/auth/newUsers.js.map +1 -1
- 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.js +2 -2
- package/dist/context.js.map +1 -1
- package/dist/generated/generated.d.ts +540 -1595
- package/dist/generated/generated.d.ts.map +1 -1
- package/dist/generated/generated.js +2755 -16857
- package/dist/generated/generated.js.map +1 -1
- package/dist/generic/ResolverBase.d.ts +20 -4
- package/dist/generic/ResolverBase.d.ts.map +1 -1
- package/dist/generic/ResolverBase.js +58 -21
- 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 +183 -54
- package/dist/index.js.map +1 -1
- 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 +52 -0
- package/dist/integration/SchemaRefreshLaunch.d.ts.map +1 -0
- package/dist/integration/SchemaRefreshLaunch.js +50 -0
- package/dist/integration/SchemaRefreshLaunch.js.map +1 -0
- package/dist/logging/secretRedactor.d.ts +9 -0
- package/dist/logging/secretRedactor.d.ts.map +1 -1
- package/dist/logging/secretRedactor.js +39 -0
- package/dist/logging/secretRedactor.js.map +1 -1
- package/dist/middleware/BaseServerMiddleware.d.ts +5 -0
- package/dist/middleware/BaseServerMiddleware.d.ts.map +1 -1
- package/dist/middleware/BaseServerMiddleware.js +5 -0
- package/dist/middleware/BaseServerMiddleware.js.map +1 -1
- 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/realtimeWidget/WidgetSessionService.js +1 -1
- package/dist/realtimeWidget/WidgetSessionService.js.map +1 -1
- package/dist/realtimeWidget/widgetGuestElevation.js +1 -1
- package/dist/realtimeWidget/widgetGuestElevation.js.map +1 -1
- package/dist/resolvers/ActionResolver.d.ts.map +1 -1
- package/dist/resolvers/ActionResolver.js +12 -0
- package/dist/resolvers/ActionResolver.js.map +1 -1
- package/dist/resolvers/ComponentRegistryResolver.js +1 -1
- package/dist/resolvers/ComponentRegistryResolver.js.map +1 -1
- package/dist/resolvers/EntityPermissionResolver.js +1 -1
- package/dist/resolvers/EntityPermissionResolver.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/IntegrationDiscoveryResolver.d.ts +85 -3
- package/dist/resolvers/IntegrationDiscoveryResolver.d.ts.map +1 -1
- package/dist/resolvers/IntegrationDiscoveryResolver.js +302 -82
- package/dist/resolvers/IntegrationDiscoveryResolver.js.map +1 -1
- package/dist/resolvers/PotentialDuplicateRecordResolver.js +1 -1
- package/dist/resolvers/PotentialDuplicateRecordResolver.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/RealtimeClientSessionResolver.d.ts +10 -0
- package/dist/resolvers/RealtimeClientSessionResolver.d.ts.map +1 -1
- package/dist/resolvers/RealtimeClientSessionResolver.js +5 -0
- package/dist/resolvers/RealtimeClientSessionResolver.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/SearchEntitiesResolver.js +1 -1
- package/dist/resolvers/SearchEntitiesResolver.js.map +1 -1
- package/dist/resolvers/SqlLoggingConfigResolver.d.ts.map +1 -1
- package/dist/resolvers/SqlLoggingConfigResolver.js +1 -1
- package/dist/resolvers/SqlLoggingConfigResolver.js.map +1 -1
- package/dist/resolvers/SyncRolesUsersResolver.js +2 -2
- package/dist/resolvers/SyncRolesUsersResolver.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/UserFavoriteResolver.js +1 -1
- package/dist/resolvers/UserFavoriteResolver.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/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 +130 -44
- package/dist/rest/OAuthCallbackHandler.js.map +1 -1
- package/dist/rest/SignatureWebhookHandler.js +1 -1
- package/dist/rest/SignatureWebhookHandler.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/DurableEntityActionTaskSubmitter.d.ts +26 -0
- package/dist/services/DurableEntityActionTaskSubmitter.d.ts.map +1 -0
- package/dist/services/DurableEntityActionTaskSubmitter.js +55 -0
- package/dist/services/DurableEntityActionTaskSubmitter.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/ScheduledJobsService.js +1 -1
- package/dist/services/ScheduledJobsService.js.map +1 -1
- package/dist/services/StartTaskGraphDispatcher.d.ts +0 -10
- package/dist/services/StartTaskGraphDispatcher.d.ts.map +1 -1
- package/dist/services/StartTaskGraphDispatcher.js +37 -2
- package/dist/services/StartTaskGraphDispatcher.js.map +1 -1
- package/dist/services/TaskGraphActionRunner.d.ts +34 -0
- package/dist/services/TaskGraphActionRunner.d.ts.map +1 -0
- package/dist/services/TaskGraphActionRunner.js +95 -0
- package/dist/services/TaskGraphActionRunner.js.map +1 -0
- package/dist/services/TaskGraphAgentRunner.d.ts +20 -0
- package/dist/services/TaskGraphAgentRunner.d.ts.map +1 -1
- package/dist/services/TaskGraphAgentRunner.js +53 -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/services/TaskGraphPromptRunner.d.ts +31 -0
- package/dist/services/TaskGraphPromptRunner.d.ts.map +1 -0
- package/dist/services/TaskGraphPromptRunner.js +134 -0
- package/dist/services/TaskGraphPromptRunner.js.map +1 -0
- package/dist/telephony/RingCentralTelephonyService.js +1 -1
- package/dist/telephony/RingCentralTelephonyService.js.map +1 -1
- package/dist/telephony/TwilioTelephonyRouter.js +1 -1
- package/dist/telephony/TwilioTelephonyRouter.js.map +1 -1
- package/dist/telephony/VonageTelephonyRouter.js +1 -1
- package/dist/telephony/VonageTelephonyRouter.js.map +1 -1
- package/dist/types.d.ts +6 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js.map +1 -1
- package/package.json +91 -91
- package/src/__tests__/APIKeyScopeAuth.test.ts +475 -0
- package/src/__tests__/AuthProviderEngine.test.ts +278 -0
- package/src/__tests__/EntityCRUDHandler.test.ts +378 -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__/RealtimeBridgeResolver.test.ts +10 -2
- package/src/__tests__/RealtimeClientSessionResolver.test.ts +1 -1
- package/src/__tests__/ResolverBase.frozenRecordMapping.test.ts +133 -0
- package/src/__tests__/ResolverBase.transportMapping.test.ts +289 -0
- package/src/__tests__/SchemaRefreshLaunch.test.ts +114 -0
- package/src/__tests__/SessionManager.test.ts +1 -1
- package/src/__tests__/UserResolver.currentUser.test.ts +87 -0
- package/src/__tests__/ViewOperationsHandler.test.ts +277 -0
- package/src/__tests__/WidgetSessionService.test.ts +1 -1
- package/src/__tests__/config-units.test.ts +74 -0
- package/src/__tests__/databaseAbstraction.test.ts +1 -1
- package/src/__tests__/initializeProviders.refresh.test.ts +146 -0
- package/src/__tests__/magicLinkService.provisionLog.test.ts +1 -1
- package/src/__tests__/mjapi-bootstrap.test.ts +0 -6
- package/src/__tests__/newUsers.test.ts +726 -0
- package/src/__tests__/resolverBase.rls.test.ts +3 -0
- package/src/__tests__/secretRedactor.failOpen.test.ts +135 -0
- package/src/__tests__/uploadTokenManager.test.ts +159 -0
- package/src/__tests__/widgetGuestElevation.test.ts +1 -1
- package/src/auth/AuthProviderCatalogRouter.ts +76 -0
- package/src/auth/AuthProviderEngine.ts +253 -0
- package/src/auth/exampleNewUserSubClass.ts +1 -1
- package/src/auth/index.ts +69 -21
- package/src/auth/initializeProviders.ts +128 -7
- package/src/auth/magicLink/MagicLinkService.ts +1 -1
- package/src/auth/newUsers.ts +1 -1
- package/src/config.ts +55 -36
- package/src/context.ts +2 -2
- package/src/generated/generated.ts +2038 -10529
- package/src/generic/ResolverBase.ts +63 -21
- package/src/generic/RunViewResolver.ts +31 -0
- package/src/index.ts +199 -56
- package/src/integration/RSUProgressBridge.ts +248 -0
- package/src/integration/SchemaRefreshLaunch.ts +77 -0
- package/src/logging/secretRedactor.ts +43 -0
- package/src/middleware/BaseServerMiddleware.ts +5 -0
- package/src/providerConfigUnits.ts +21 -0
- package/src/realtimeWidget/WidgetSessionService.ts +1 -1
- package/src/realtimeWidget/widgetGuestElevation.ts +1 -1
- package/src/resolvers/ActionResolver.ts +13 -0
- package/src/resolvers/ComponentRegistryResolver.ts +1 -1
- package/src/resolvers/EntityPermissionResolver.ts +1 -1
- package/src/resolvers/FileResolver.ts +200 -40
- package/src/resolvers/IntegrationDiscoveryResolver.ts +328 -70
- package/src/resolvers/PotentialDuplicateRecordResolver.ts +1 -1
- package/src/resolvers/QueryResolver.ts +25 -11
- package/src/resolvers/QuerySystemUserResolver.ts +28 -13
- package/src/resolvers/RealtimeClientSessionResolver.ts +13 -0
- package/src/resolvers/RunAIAgentResolver.ts +21 -6
- package/src/resolvers/SearchEntitiesResolver.ts +1 -1
- package/src/resolvers/SqlLoggingConfigResolver.ts +1 -2
- package/src/resolvers/SyncRolesUsersResolver.ts +2 -2
- package/src/resolvers/TaskGraphFrameResolver.ts +67 -0
- package/src/resolvers/UserFavoriteResolver.ts +1 -1
- package/src/resolvers/UserResolver.ts +99 -60
- package/src/resolvers/currentUserRoles.ts +47 -0
- package/src/rest/MediaAccessKeys.ts +47 -1
- package/src/rest/MediaStreamHandler.ts +98 -3
- package/src/rest/OAuthCallbackHandler.ts +150 -42
- package/src/rest/SignatureWebhookHandler.ts +1 -1
- package/src/rest/UploadTokenManager.ts +211 -0
- package/src/services/DurableEntityActionTaskSubmitter.ts +78 -0
- package/src/services/IntegrationSyncWorkerService.ts +141 -0
- package/src/services/ScheduledJobsService.ts +1 -1
- package/src/services/StartTaskGraphDispatcher.ts +38 -1
- package/src/services/TaskGraphActionRunner.ts +99 -0
- package/src/services/TaskGraphAgentRunner.ts +58 -1
- package/src/services/TaskGraphContinuationDeliverer.ts +11 -1
- package/src/services/TaskGraphPromptRunner.ts +144 -0
- package/src/telephony/RingCentralTelephonyService.ts +1 -1
- package/src/telephony/TwilioTelephonyRouter.ts +1 -1
- package/src/telephony/VonageTelephonyRouter.ts +1 -1
- package/src/types.ts +6 -0
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
|
|
2
|
+
import { mkdtempSync, rmSync, readFileSync, existsSync } from 'node:fs';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
import { tmpdir } from 'node:os';
|
|
5
|
+
import { RSUProgressBridge } from '../integration/RSUProgressBridge.js';
|
|
6
|
+
import type { RSUObserverEvent } from '@memberjunction/schema-engine';
|
|
7
|
+
import type { IntegrationProgressEvent, IntegrationRunManifest, IntegrationRunResult } from '@memberjunction/integration-progress-artifacts';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* The bridge is what makes an RSU run tailable: `IntegrationRunKind` has always had an 'RSU' kind
|
|
11
|
+
* and the resolver has always mapped it to an 'RSU' subscription channel, but nothing published to
|
|
12
|
+
* it. These tests drive the observer with the events the pipeline emits and assert the durable
|
|
13
|
+
* artifact that comes out — reading the real JSONL, not a mocked emitter, because the artifact IS
|
|
14
|
+
* the deliverable.
|
|
15
|
+
*/
|
|
16
|
+
describe('RSUProgressBridge', () => {
|
|
17
|
+
let rootDir: string;
|
|
18
|
+
let bridge: RSUProgressBridge;
|
|
19
|
+
|
|
20
|
+
beforeEach(() => {
|
|
21
|
+
rootDir = mkdtempSync(join(tmpdir(), 'rsu-bridge-'));
|
|
22
|
+
// The bridge is a BaseSingleton, so every test in this file shares ONE instance. Reset()
|
|
23
|
+
// returns it to idle (dropping an emitter or heartbeat a prior test left in flight) and
|
|
24
|
+
// Configure() repoints artifacts at this test's temp dir. Without both, one test's in-flight
|
|
25
|
+
// run would be inherited by the next and its writes would land in a deleted directory.
|
|
26
|
+
bridge = RSUProgressBridge.Instance;
|
|
27
|
+
bridge.Reset();
|
|
28
|
+
bridge.Configure({ rootDir });
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
afterEach(async () => {
|
|
32
|
+
// Drain FIRST. The emitter writes through chained promises, and `Reset()` only drops this
|
|
33
|
+
// bridge's reference to it — it cannot cancel a write already queued. A test that starts a run
|
|
34
|
+
// without awaiting `settle()` would otherwise have its manifest.json land after the rmSync
|
|
35
|
+
// below, surfacing as an unhandled ENOENT that fails the suite while every test still reports
|
|
36
|
+
// as passing. Then Reset, so no heartbeat interval outlives the test or keeps the process up.
|
|
37
|
+
await settle();
|
|
38
|
+
bridge.Reset();
|
|
39
|
+
rmSync(rootDir, { recursive: true, force: true });
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
const RUN_START: RSUObserverEvent = {
|
|
43
|
+
Kind: 'run.start',
|
|
44
|
+
ItemCount: 2,
|
|
45
|
+
Descriptions: ['add Orders', 'add OrderItems'],
|
|
46
|
+
AffectedTables: ['Orders', 'OrderItems'],
|
|
47
|
+
StepTotal: 12,
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
/** The emitter's writes are chained promises; let them drain before reading from disk. */
|
|
51
|
+
async function settle(): Promise<void> {
|
|
52
|
+
await new Promise(resolve => setTimeout(resolve, 50));
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function runDir(): string {
|
|
56
|
+
return join(rootDir, bridge.CurrentRunID ?? '');
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function readEvents(dir: string): IntegrationProgressEvent[] {
|
|
60
|
+
return readFileSync(join(dir, 'progress.jsonl'), 'utf-8')
|
|
61
|
+
.split('\n')
|
|
62
|
+
.filter(Boolean)
|
|
63
|
+
.map(line => JSON.parse(line) as IntegrationProgressEvent);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
it('is a singleton, so a resolver can reach the in-flight run to tail it', async () => {
|
|
67
|
+
// The point of the singleton. Previously the bridge was constructed inside
|
|
68
|
+
// RegisterRSUProgressBridge and the only surviving reference was the observer closure handed
|
|
69
|
+
// to RuntimeSchemaManager -- so CurrentRunID, whose whole purpose is letting a caller that
|
|
70
|
+
// triggered an RSU hand its client a run to tail, was unreachable from any caller.
|
|
71
|
+
expect(RSUProgressBridge.Instance).toBe(bridge);
|
|
72
|
+
|
|
73
|
+
bridge.Observe(RUN_START);
|
|
74
|
+
expect(RSUProgressBridge.Instance.CurrentRunID).toBe(bridge.CurrentRunID);
|
|
75
|
+
expect(RSUProgressBridge.Instance.CurrentRunID).toMatch(/^rsu-/);
|
|
76
|
+
await settle();
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it('Reset returns the bridge to idle without emitting a terminal event', async () => {
|
|
80
|
+
bridge.Observe(RUN_START);
|
|
81
|
+
expect(bridge.CurrentRunID).not.toBeNull();
|
|
82
|
+
await settle();
|
|
83
|
+
|
|
84
|
+
bridge.Reset();
|
|
85
|
+
|
|
86
|
+
// Idle again, and the next run starts clean rather than inheriting the abandoned emitter.
|
|
87
|
+
expect(bridge.CurrentRunID).toBeNull();
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
it('opens a run tagged RSU carrying the batch context, and exposes its ID to tail', async () => {
|
|
91
|
+
bridge.Observe(RUN_START);
|
|
92
|
+
const dir = runDir();
|
|
93
|
+
expect(bridge.CurrentRunID).toMatch(/^rsu-/);
|
|
94
|
+
await settle();
|
|
95
|
+
|
|
96
|
+
const manifest = JSON.parse(readFileSync(join(dir, 'manifest.json'), 'utf-8')) as IntegrationRunManifest;
|
|
97
|
+
// The run kind is what routes this onto the 'RSU' subscription channel.
|
|
98
|
+
expect(manifest.runKind).toBe('RSU');
|
|
99
|
+
expect(manifest.triggerType).toBe('Pipeline');
|
|
100
|
+
expect(manifest.context).toMatchObject({
|
|
101
|
+
itemCount: 2,
|
|
102
|
+
affectedTables: ['Orders', 'OrderItems'],
|
|
103
|
+
stepTotal: 12,
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
const events = readEvents(dir);
|
|
107
|
+
expect(events[0].eventType).toBe('run.start');
|
|
108
|
+
expect(events[0].message).toContain('2 migration(s)');
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
it('records each step as a stage, with its position in the determinate sequence', async () => {
|
|
112
|
+
bridge.Observe(RUN_START);
|
|
113
|
+
const dir = runDir();
|
|
114
|
+
bridge.Observe({ Kind: 'step.start', Name: 'RunCodeGen', StepIndex: 4, StepTotal: 12 });
|
|
115
|
+
bridge.Observe({ Kind: 'step.end', Name: 'RunCodeGen', Status: 'success', DurationMs: 91_000, Message: 'ok' });
|
|
116
|
+
await settle();
|
|
117
|
+
|
|
118
|
+
const events = readEvents(dir);
|
|
119
|
+
const start = events.find(e => e.eventType === 'stage.start' && e.stage === 'RunCodeGen');
|
|
120
|
+
const done = events.find(e => e.eventType === 'stage.complete' && e.stage === 'RunCodeGen');
|
|
121
|
+
|
|
122
|
+
expect(start?.message).toBe('step 4 of 12');
|
|
123
|
+
expect(done).toBeDefined();
|
|
124
|
+
// Steps are not a countable unit — counts belong to migrations, emitted once at run end.
|
|
125
|
+
expect(done?.counts).toBeUndefined();
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
it('records a failed step as a stage error without terminating the run', async () => {
|
|
129
|
+
bridge.Observe(RUN_START);
|
|
130
|
+
const dir = runDir();
|
|
131
|
+
bridge.Observe({
|
|
132
|
+
Kind: 'step.end',
|
|
133
|
+
Name: 'ExecuteMigration',
|
|
134
|
+
Status: 'failed',
|
|
135
|
+
DurationMs: 120,
|
|
136
|
+
Message: 'FK violation on Orders',
|
|
137
|
+
});
|
|
138
|
+
await settle();
|
|
139
|
+
|
|
140
|
+
const err = readEvents(dir).find(e => e.eventType === 'stage.error');
|
|
141
|
+
expect(err?.stage).toBe('ExecuteMigration');
|
|
142
|
+
expect(err?.message).toBe('FK violation on Orders');
|
|
143
|
+
expect(err?.data).toMatchObject({ code: 'RSU_STEP_FAILED' });
|
|
144
|
+
// A per-item failure does not abort the batch, so the run must still be open.
|
|
145
|
+
expect(existsSync(join(dir, 'result.json'))).toBe(false);
|
|
146
|
+
expect(bridge.CurrentRunID).not.toBeNull();
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
it('completes the run with the migration quartet as the authoritative counts', async () => {
|
|
150
|
+
bridge.Observe(RUN_START);
|
|
151
|
+
const dir = runDir();
|
|
152
|
+
bridge.Observe({ Kind: 'run.end', Success: true, SuccessCount: 2, FailureCount: 0, TotalCount: 2 });
|
|
153
|
+
await settle();
|
|
154
|
+
|
|
155
|
+
const result = JSON.parse(readFileSync(join(dir, 'result.json'), 'utf-8')) as IntegrationRunResult;
|
|
156
|
+
expect(result.success).toBe(true);
|
|
157
|
+
expect(result.exitReason).toBe('completed');
|
|
158
|
+
// Counts are in MIGRATIONS (2), not steps.
|
|
159
|
+
expect(result.aggregateCounts).toMatchObject({ processed: 2, succeeded: 2, failed: 0 });
|
|
160
|
+
// Idle again — nothing to tail.
|
|
161
|
+
expect(bridge.CurrentRunID).toBeNull();
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
it('fails the run naming the step that broke it', async () => {
|
|
165
|
+
bridge.Observe(RUN_START);
|
|
166
|
+
const dir = runDir();
|
|
167
|
+
bridge.Observe({
|
|
168
|
+
Kind: 'run.end',
|
|
169
|
+
Success: false,
|
|
170
|
+
SuccessCount: 1,
|
|
171
|
+
FailureCount: 1,
|
|
172
|
+
TotalCount: 2,
|
|
173
|
+
ErrorMessage: 'FK violation on Orders',
|
|
174
|
+
ErrorStep: 'ExecuteMigration',
|
|
175
|
+
});
|
|
176
|
+
await settle();
|
|
177
|
+
|
|
178
|
+
const result = JSON.parse(readFileSync(join(dir, 'result.json'), 'utf-8')) as IntegrationRunResult;
|
|
179
|
+
expect(result.success).toBe(false);
|
|
180
|
+
expect(result.exitReason).toBe('failed');
|
|
181
|
+
expect(result.errors?.[0].message).toContain('ExecuteMigration');
|
|
182
|
+
expect(result.errors?.[0].message).toContain('FK violation on Orders');
|
|
183
|
+
expect(result.errors?.[0].code).toBe('rsu-pipeline-failed');
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
it('treats a second run.start as a NEW run and closes the abandoned one', async () => {
|
|
187
|
+
// RunPipelineBatchWithRetry re-enters the pipeline, so a retry legitimately starts a fresh
|
|
188
|
+
// run. What must not happen is the previous run staying in-flight forever.
|
|
189
|
+
bridge.Observe(RUN_START);
|
|
190
|
+
const firstDir = runDir();
|
|
191
|
+
bridge.Observe(RUN_START);
|
|
192
|
+
const secondDir = runDir();
|
|
193
|
+
await settle();
|
|
194
|
+
|
|
195
|
+
expect(secondDir).not.toBe(firstDir);
|
|
196
|
+
const abandoned = JSON.parse(readFileSync(join(firstDir, 'result.json'), 'utf-8')) as IntegrationRunResult;
|
|
197
|
+
expect(abandoned.success).toBe(false);
|
|
198
|
+
expect(abandoned.errors?.[0].code).toBe('rsu-run-abandoned');
|
|
199
|
+
// The new run is open and untouched by the abandonment.
|
|
200
|
+
expect(existsSync(join(secondDir, 'result.json'))).toBe(false);
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
it('reports liveness while a long step is in flight, and stops at step.end', async () => {
|
|
204
|
+
// CodeGen and compile run for minutes with no intermediate observer event. Without a
|
|
205
|
+
// heartbeat the stream goes silent for the whole step and a watcher cannot distinguish
|
|
206
|
+
// "compiling" from "hung".
|
|
207
|
+
vi.useFakeTimers();
|
|
208
|
+
try {
|
|
209
|
+
bridge.Observe(RUN_START);
|
|
210
|
+
const dir = runDir();
|
|
211
|
+
bridge.Observe({ Kind: 'step.start', Name: 'CodeGen', StepIndex: 5, StepTotal: 12 });
|
|
212
|
+
|
|
213
|
+
vi.advanceTimersByTime(95_000); // three 30s ticks
|
|
214
|
+
bridge.Observe({ Kind: 'step.end', Name: 'CodeGen', Status: 'success', DurationMs: 95_000, Message: 'ok' });
|
|
215
|
+
vi.advanceTimersByTime(120_000); // no further ticks once the step closed
|
|
216
|
+
|
|
217
|
+
vi.useRealTimers();
|
|
218
|
+
await settle();
|
|
219
|
+
|
|
220
|
+
const beats = readEvents(dir).filter(e => e.eventType === 'progress.heartbeat');
|
|
221
|
+
expect(beats).toHaveLength(3);
|
|
222
|
+
expect(beats[0].stage).toBe('CodeGen');
|
|
223
|
+
expect(beats[0].message).toMatch(/still running/);
|
|
224
|
+
} finally {
|
|
225
|
+
vi.useRealTimers();
|
|
226
|
+
}
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
it('does not drop the position label for a zero-based step index', async () => {
|
|
230
|
+
// `stepIndex && stepTotal` would silently omit the label at index 0.
|
|
231
|
+
bridge.Observe(RUN_START);
|
|
232
|
+
const dir = runDir();
|
|
233
|
+
bridge.Observe({ Kind: 'step.start', Name: 'ValidateEnvironment', StepIndex: 0, StepTotal: 12 });
|
|
234
|
+
await settle();
|
|
235
|
+
|
|
236
|
+
const start = readEvents(dir).find(e => e.stage === 'ValidateEnvironment');
|
|
237
|
+
expect(start?.message).toBe('step 0 of 12');
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
it('ignores step and run.end events that arrive with no run open', async () => {
|
|
241
|
+
// Defensive: an observer registered mid-run would see a trailing run.end for a run it never
|
|
242
|
+
// saw start. That must be a no-op, not a crash.
|
|
243
|
+
expect(() => {
|
|
244
|
+
bridge.Observe({ Kind: 'step.start', Name: 'AcquireLock' });
|
|
245
|
+
bridge.Observe({ Kind: 'step.end', Name: 'AcquireLock', Status: 'success', DurationMs: 1, Message: 'ok' });
|
|
246
|
+
bridge.Observe({ Kind: 'run.end', Success: true, SuccessCount: 1, FailureCount: 0, TotalCount: 1 });
|
|
247
|
+
}).not.toThrow();
|
|
248
|
+
expect(bridge.CurrentRunID).toBeNull();
|
|
249
|
+
});
|
|
250
|
+
});
|
|
@@ -17,11 +17,19 @@ const h = vi.hoisted(() => ({
|
|
|
17
17
|
stopRecording: vi.fn(async () => ({ EgressID: 'eg-1', RoomName: 'room-1', Status: 'EGRESS_COMPLETE' })),
|
|
18
18
|
}));
|
|
19
19
|
|
|
20
|
+
// These two are instantiated with `new` by the resolver, so they must be constructible. They were
|
|
21
|
+
// `vi.fn(() => ({...}))`, which vitest 3 tolerated as a constructor but vitest 4 rejects with
|
|
22
|
+
// "is not a constructor" — classes express the intent and work under both.
|
|
20
23
|
vi.mock('@memberjunction/livekit-room-server', () => ({
|
|
21
|
-
LiveKitTokenService:
|
|
24
|
+
LiveKitTokenService: class {
|
|
25
|
+
MintClientToken = h.mintClientToken;
|
|
26
|
+
},
|
|
22
27
|
// SetSessionFactory is exercised by the resolver's module-load binding of the realtime-session factory.
|
|
23
28
|
LiveKitAgentRoomCoordinator: { Instance: { StartAgentRoomSession: vi.fn(), SetSessionFactory: vi.fn() } },
|
|
24
|
-
LiveKitEgressService:
|
|
29
|
+
LiveKitEgressService: class {
|
|
30
|
+
StartRoomRecording = h.startRecording;
|
|
31
|
+
StopRecording = h.stopRecording;
|
|
32
|
+
},
|
|
25
33
|
}));
|
|
26
34
|
|
|
27
35
|
// Mock the agent factory so importing the resolver doesn't pull the heavy @memberjunction/ai-agents graph
|
|
@@ -148,7 +148,7 @@ vi.mock('../util.js', () => ({
|
|
|
148
148
|
// --- Partial mock: control ONLY UserCache.GetSystemUser (the scoped-anonymous elevation seam,
|
|
149
149
|
// issue #3371); everything else in the data provider stays real ---
|
|
150
150
|
const getSystemUserMock = vi.fn<[], UserInfo | undefined>();
|
|
151
|
-
vi.mock('@memberjunction/
|
|
151
|
+
vi.mock('@memberjunction/generic-database-provider', async (importOriginal) => {
|
|
152
152
|
const actual = await importOriginal<Record<string, unknown>>();
|
|
153
153
|
return {
|
|
154
154
|
...actual,
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
// ResolverBase transitively pulls in type-graphql decorators, which need the
|
|
2
|
+
// Reflect.metadata polyfill at import time.
|
|
3
|
+
import 'reflect-metadata';
|
|
4
|
+
import { describe, it, expect } from 'vitest';
|
|
5
|
+
import type { IMetadataProvider, UserInfo } from '@memberjunction/core';
|
|
6
|
+
import { ResolverBase } from '../generic/ResolverBase.js';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* `MapFieldNamesToCodeNames` must not write into the rows it is handed
|
|
10
|
+
* (PR #3425 review, finding M2).
|
|
11
|
+
*
|
|
12
|
+
* This is the SINGLE-RECORD half of the transport rename. The RunView half was fixed to
|
|
13
|
+
* copy-then-map, but this one still renamed `__mj_*` → `_mj__*` by writing onto its argument —
|
|
14
|
+
* and its arguments are frequently rows straight out of `findBy`/`RunView`, i.e. the server
|
|
15
|
+
* cache's own objects.
|
|
16
|
+
*
|
|
17
|
+
* Now that `LocalCacheManager` deep-freezes cached rows, that in-place rename does not corrupt
|
|
18
|
+
* the cache — it throws. Reproduced live against a running MJAPI before the fix:
|
|
19
|
+
*
|
|
20
|
+
* UserByEmail → "Cannot add property _mj__CreatedAt, object is not extensible"
|
|
21
|
+
*
|
|
22
|
+
* deterministically, on every call, because `MJ: Users` has caching enabled. `UserByID` and
|
|
23
|
+
* `UserByEmployeeID` share the code path, and so does every CodeGen-generated single-record
|
|
24
|
+
* resolver in `generated.ts` (`MapFieldNamesToCodeNames(entity, rows[0], ...)`), which is why
|
|
25
|
+
* the fix belongs in the shared helper rather than at the call sites.
|
|
26
|
+
*/
|
|
27
|
+
|
|
28
|
+
/** Exposes the two protected mappers and supplies metadata without a live provider. */
|
|
29
|
+
class MappingProbe extends ResolverBase {
|
|
30
|
+
public MapOne(entityName: string, dataObject: unknown, provider: IMetadataProvider): Promise<unknown> {
|
|
31
|
+
return this.MapFieldNamesToCodeNames(entityName, dataObject, undefined, provider);
|
|
32
|
+
}
|
|
33
|
+
public MapMany(entityName: string, rows: unknown[], provider: IMetadataProvider, contextUser?: UserInfo): Promise<unknown[]> {
|
|
34
|
+
return this.ArrayMapFieldNamesToCodeNames(entityName, rows as Record<string, unknown>[], contextUser, provider);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const ENTITY_NAME = 'MJ: Users';
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Minimal metadata: the field set drives the rename, `EncryptedFields` empty so the
|
|
42
|
+
* EncryptionEngine is never consulted.
|
|
43
|
+
*/
|
|
44
|
+
function fakeProvider(): IMetadataProvider {
|
|
45
|
+
return {
|
|
46
|
+
EntityByName: (name: string) =>
|
|
47
|
+
name === ENTITY_NAME
|
|
48
|
+
? {
|
|
49
|
+
Name: ENTITY_NAME,
|
|
50
|
+
Fields: [
|
|
51
|
+
{ Name: 'ID', CodeName: 'ID' },
|
|
52
|
+
{ Name: 'Name', CodeName: 'Name' },
|
|
53
|
+
{ Name: '__mj_CreatedAt', CodeName: '__mj_CreatedAt' },
|
|
54
|
+
{ Name: '__mj_UpdatedAt', CodeName: '__mj_UpdatedAt' },
|
|
55
|
+
],
|
|
56
|
+
EncryptedFields: [],
|
|
57
|
+
}
|
|
58
|
+
: undefined,
|
|
59
|
+
} as unknown as IMetadataProvider;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** A row as the cache hands it out: deep-frozen. */
|
|
63
|
+
function frozenCachedRow(): Record<string, unknown> {
|
|
64
|
+
return Object.freeze({
|
|
65
|
+
ID: 'u-1',
|
|
66
|
+
Name: 'Ada',
|
|
67
|
+
__mj_CreatedAt: 'T0',
|
|
68
|
+
__mj_UpdatedAt: 'T1',
|
|
69
|
+
}) as Record<string, unknown>;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
describe('MapFieldNamesToCodeNames on frozen cache rows', () => {
|
|
73
|
+
it('maps a FROZEN row to transport keys instead of throwing (the live UserByEmail 500)', async () => {
|
|
74
|
+
const row = frozenCachedRow();
|
|
75
|
+
|
|
76
|
+
const mapped = (await new MappingProbe().MapOne(ENTITY_NAME, row, fakeProvider())) as Record<string, unknown>;
|
|
77
|
+
|
|
78
|
+
expect(mapped._mj__CreatedAt).toBe('T0');
|
|
79
|
+
expect(mapped._mj__UpdatedAt).toBe('T1');
|
|
80
|
+
expect(mapped.ID).toBe('u-1');
|
|
81
|
+
expect(mapped.Name).toBe('Ada');
|
|
82
|
+
// The transport aliases replace the originals in the OUTGOING shape.
|
|
83
|
+
expect(mapped.__mj_CreatedAt).toBeUndefined();
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
it('returns a copy — the caller\'s row keeps its entity field names', async () => {
|
|
87
|
+
// Unfrozen input, so a regression to in-place mapping would silently pass the frozen
|
|
88
|
+
// test above only if it also stopped mapping. This one pins non-mutation directly.
|
|
89
|
+
const row: Record<string, unknown> = { ID: 'u-1', Name: 'Ada', __mj_CreatedAt: 'T0', __mj_UpdatedAt: 'T1' };
|
|
90
|
+
|
|
91
|
+
const mapped = (await new MappingProbe().MapOne(ENTITY_NAME, row, fakeProvider())) as Record<string, unknown>;
|
|
92
|
+
|
|
93
|
+
expect(row.__mj_CreatedAt).toBe('T0');
|
|
94
|
+
expect(row._mj__CreatedAt).toBeUndefined();
|
|
95
|
+
expect(mapped).not.toBe(row);
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
it('still returns null for empty/absent input (contract unchanged)', async () => {
|
|
99
|
+
const probe = new MappingProbe();
|
|
100
|
+
expect(await probe.MapOne(ENTITY_NAME, null, fakeProvider())).toBeNull();
|
|
101
|
+
expect(await probe.MapOne(ENTITY_NAME, {}, fakeProvider())).toBeNull();
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
it('leaves rows with no __mj_ fields structurally intact', async () => {
|
|
105
|
+
const row = Object.freeze({ ID: 'u-1', Name: 'Ada' }) as Record<string, unknown>;
|
|
106
|
+
|
|
107
|
+
const mapped = (await new MappingProbe().MapOne(ENTITY_NAME, row, fakeProvider())) as Record<string, unknown>;
|
|
108
|
+
|
|
109
|
+
expect(mapped).toEqual({ ID: 'u-1', Name: 'Ada' });
|
|
110
|
+
});
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
describe('ArrayMapFieldNamesToCodeNames on frozen cache rows', () => {
|
|
114
|
+
it('maps a frozen ARRAY of frozen rows without mutating either', async () => {
|
|
115
|
+
// The array itself is frozen too — LocalCacheManager freezes the array as well as the
|
|
116
|
+
// rows, because `results.sort()`/`.push()` on a live cache array is its own hazard.
|
|
117
|
+
const rows = Object.freeze([frozenCachedRow(), frozenCachedRow()]) as unknown as Record<string, unknown>[];
|
|
118
|
+
|
|
119
|
+
const mapped = (await new MappingProbe().MapMany(ENTITY_NAME, rows, fakeProvider())) as Record<string, unknown>[];
|
|
120
|
+
|
|
121
|
+
expect(mapped).toHaveLength(2);
|
|
122
|
+
expect(mapped[0]._mj__CreatedAt).toBe('T0');
|
|
123
|
+
expect(mapped[1]._mj__UpdatedAt).toBe('T1');
|
|
124
|
+
// Inputs untouched.
|
|
125
|
+
expect(rows[0].__mj_CreatedAt).toBe('T0');
|
|
126
|
+
expect(rows[0]._mj__CreatedAt).toBeUndefined();
|
|
127
|
+
expect(mapped[0]).not.toBe(rows[0]);
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
it('passes an empty array straight through', async () => {
|
|
131
|
+
expect(await new MappingProbe().MapMany(ENTITY_NAME, [], fakeProvider())).toEqual([]);
|
|
132
|
+
});
|
|
133
|
+
});
|