@memberjunction/server 5.42.0 → 5.43.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/dist/agentSessions/SessionJanitor.d.ts.map +1 -1
- package/dist/agentSessions/SessionJanitor.js +5 -1
- package/dist/agentSessions/SessionJanitor.js.map +1 -1
- package/dist/agentSessions/SessionManager.d.ts +15 -2
- package/dist/agentSessions/SessionManager.d.ts.map +1 -1
- package/dist/agentSessions/SessionManager.js +50 -11
- package/dist/agentSessions/SessionManager.js.map +1 -1
- package/dist/generated/generated.d.ts +21 -0
- package/dist/generated/generated.d.ts.map +1 -1
- package/dist/generated/generated.js +86 -0
- package/dist/generated/generated.js.map +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +5 -1
- package/dist/index.js.map +1 -1
- package/dist/resolvers/ExecuteRemoteOperationResolver.d.ts +20 -1
- package/dist/resolvers/ExecuteRemoteOperationResolver.d.ts.map +1 -1
- package/dist/resolvers/ExecuteRemoteOperationResolver.js +61 -5
- package/dist/resolvers/ExecuteRemoteOperationResolver.js.map +1 -1
- package/dist/resolvers/IntegrationDiscoveryResolver.d.ts.map +1 -1
- package/dist/resolvers/IntegrationDiscoveryResolver.js +98 -29
- package/dist/resolvers/IntegrationDiscoveryResolver.js.map +1 -1
- package/dist/resolvers/RealtimeBridgeResolver.d.ts +9 -0
- package/dist/resolvers/RealtimeBridgeResolver.d.ts.map +1 -1
- package/dist/resolvers/RealtimeBridgeResolver.js +50 -3
- package/dist/resolvers/RealtimeBridgeResolver.js.map +1 -1
- package/dist/resolvers/RunTemplateResolver.d.ts +8 -0
- package/dist/resolvers/RunTemplateResolver.d.ts.map +1 -1
- package/dist/resolvers/RunTemplateResolver.js +8 -0
- package/dist/resolvers/RunTemplateResolver.js.map +1 -1
- package/package.json +82 -82
- package/src/agentSessions/SessionJanitor.ts +15 -1
- package/src/agentSessions/SessionManager.ts +59 -10
- package/src/generated/generated.ts +65 -0
- package/src/index.ts +5 -1
- package/src/resolvers/ExecuteRemoteOperationResolver.ts +60 -4
- package/src/resolvers/IntegrationDiscoveryResolver.ts +108 -30
- package/src/resolvers/RealtimeBridgeResolver.ts +46 -3
- package/src/resolvers/RunTemplateResolver.ts +8 -0
|
@@ -45276,6 +45276,10 @@ export class MJEntityActionInvocation_ {
|
|
|
45276
45276
|
@Field()
|
|
45277
45277
|
_mj__UpdatedAt: Date;
|
|
45278
45278
|
|
|
45279
|
+
@Field({nullable: true, description: `Optional class name of a registered runtime-UX driver component (a BaseEntityActionRuntimeUX subclass resolved via MJGlobal.ClassFactory) that owns this invocation's interaction — parameter collection, dry-run preview, confirmation, and progress. NULL invokes the action directly with no custom UX. This lets any action opt into a richer, reusable runtime experience while the grid/toolbar stays operation-agnostic.`})
|
|
45280
|
+
@MaxLength(255)
|
|
45281
|
+
RuntimeUXDriverClass?: string;
|
|
45282
|
+
|
|
45279
45283
|
@Field()
|
|
45280
45284
|
@MaxLength(425)
|
|
45281
45285
|
EntityAction: string;
|
|
@@ -45303,6 +45307,9 @@ export class CreateMJEntityActionInvocationInput {
|
|
|
45303
45307
|
@Field({ nullable: true })
|
|
45304
45308
|
Status?: string;
|
|
45305
45309
|
|
|
45310
|
+
@Field({ nullable: true })
|
|
45311
|
+
RuntimeUXDriverClass: string | null;
|
|
45312
|
+
|
|
45306
45313
|
@Field(() => RestoreContextInput, { nullable: true })
|
|
45307
45314
|
RestoreContext___?: RestoreContextInput;
|
|
45308
45315
|
}
|
|
@@ -45325,6 +45332,9 @@ export class UpdateMJEntityActionInvocationInput {
|
|
|
45325
45332
|
@Field({ nullable: true })
|
|
45326
45333
|
Status?: string;
|
|
45327
45334
|
|
|
45335
|
+
@Field({ nullable: true })
|
|
45336
|
+
RuntimeUXDriverClass?: string | null;
|
|
45337
|
+
|
|
45328
45338
|
@Field(() => [KeyValuePairInput], { nullable: true })
|
|
45329
45339
|
OldValues___?: KeyValuePairInput[];
|
|
45330
45340
|
|
|
@@ -60698,6 +60708,10 @@ export class MJOpenApp_ {
|
|
|
60698
60708
|
@Field()
|
|
60699
60709
|
_mj__UpdatedAt: Date;
|
|
60700
60710
|
|
|
60711
|
+
@Field({nullable: true, description: `In-repo subdirectory the app was installed from for multi-app repositories (e.g. 'CRM/HubSpot'). NULL when the app's mj-app.json is at the repository root.`})
|
|
60712
|
+
@MaxLength(500)
|
|
60713
|
+
Subpath?: string;
|
|
60714
|
+
|
|
60701
60715
|
@Field()
|
|
60702
60716
|
@MaxLength(100)
|
|
60703
60717
|
InstalledByUser: string;
|
|
@@ -60772,6 +60786,9 @@ export class CreateMJOpenAppInput {
|
|
|
60772
60786
|
@Field({ nullable: true })
|
|
60773
60787
|
Status?: string;
|
|
60774
60788
|
|
|
60789
|
+
@Field({ nullable: true })
|
|
60790
|
+
Subpath: string | null;
|
|
60791
|
+
|
|
60775
60792
|
@Field(() => RestoreContextInput, { nullable: true })
|
|
60776
60793
|
RestoreContext___?: RestoreContextInput;
|
|
60777
60794
|
}
|
|
@@ -60836,6 +60853,9 @@ export class UpdateMJOpenAppInput {
|
|
|
60836
60853
|
@Field({ nullable: true })
|
|
60837
60854
|
Status?: string;
|
|
60838
60855
|
|
|
60856
|
+
@Field({ nullable: true })
|
|
60857
|
+
Subpath?: string | null;
|
|
60858
|
+
|
|
60839
60859
|
@Field(() => [KeyValuePairInput], { nullable: true })
|
|
60840
60860
|
OldValues___?: KeyValuePairInput[];
|
|
60841
60861
|
|
|
@@ -62039,6 +62059,9 @@ export class MJProcessRun_ {
|
|
|
62039
62059
|
@Field()
|
|
62040
62060
|
_mj__UpdatedAt: Date;
|
|
62041
62061
|
|
|
62062
|
+
@Field(() => Boolean, {description: `When 1, this run was a dry-run (compute-only) preview: the per-record diffs were computed and persisted as Process Run Details, but no changes were written back to the target records. When 0, the run applied its changes.`})
|
|
62063
|
+
DryRun: boolean;
|
|
62064
|
+
|
|
62042
62065
|
@Field({nullable: true})
|
|
62043
62066
|
@MaxLength(255)
|
|
62044
62067
|
RecordProcess?: string;
|
|
@@ -62134,6 +62157,9 @@ export class CreateMJProcessRunInput {
|
|
|
62134
62157
|
@Field({ nullable: true })
|
|
62135
62158
|
StartedByUserID: string | null;
|
|
62136
62159
|
|
|
62160
|
+
@Field(() => Boolean, { nullable: true })
|
|
62161
|
+
DryRun?: boolean;
|
|
62162
|
+
|
|
62137
62163
|
@Field(() => RestoreContextInput, { nullable: true })
|
|
62138
62164
|
RestoreContext___?: RestoreContextInput;
|
|
62139
62165
|
}
|
|
@@ -62213,6 +62239,9 @@ export class UpdateMJProcessRunInput {
|
|
|
62213
62239
|
@Field({ nullable: true })
|
|
62214
62240
|
StartedByUserID?: string | null;
|
|
62215
62241
|
|
|
62242
|
+
@Field(() => Boolean, { nullable: true })
|
|
62243
|
+
DryRun?: boolean;
|
|
62244
|
+
|
|
62216
62245
|
@Field(() => [KeyValuePairInput], { nullable: true })
|
|
62217
62246
|
OldValues___?: KeyValuePairInput[];
|
|
62218
62247
|
|
|
@@ -68054,6 +68083,9 @@ export class MJRecordProcess_ {
|
|
|
68054
68083
|
@Field()
|
|
68055
68084
|
_mj__UpdatedAt: Date;
|
|
68056
68085
|
|
|
68086
|
+
@Field({nullable: true, description: `JSON configuration for the process's work, used by work types that need structured config beyond Input/Output mappings. For WorkType='FieldRules' this holds the serialized FieldRuleSet (the rules applied to each record). NULL for work types that do not use it.`})
|
|
68087
|
+
Configuration?: string;
|
|
68088
|
+
|
|
68057
68089
|
@Field({nullable: true})
|
|
68058
68090
|
@MaxLength(255)
|
|
68059
68091
|
Category?: string;
|
|
@@ -68176,6 +68208,9 @@ export class CreateMJRecordProcessInput {
|
|
|
68176
68208
|
@Field(() => Int, { nullable: true })
|
|
68177
68209
|
MaxConcurrency?: number | null;
|
|
68178
68210
|
|
|
68211
|
+
@Field({ nullable: true })
|
|
68212
|
+
Configuration: string | null;
|
|
68213
|
+
|
|
68179
68214
|
@Field(() => RestoreContextInput, { nullable: true })
|
|
68180
68215
|
RestoreContext___?: RestoreContextInput;
|
|
68181
68216
|
}
|
|
@@ -68267,6 +68302,9 @@ export class UpdateMJRecordProcessInput {
|
|
|
68267
68302
|
@Field(() => Int, { nullable: true })
|
|
68268
68303
|
MaxConcurrency?: number | null;
|
|
68269
68304
|
|
|
68305
|
+
@Field({ nullable: true })
|
|
68306
|
+
Configuration?: string | null;
|
|
68307
|
+
|
|
68270
68308
|
@Field(() => [KeyValuePairInput], { nullable: true })
|
|
68271
68309
|
OldValues___?: KeyValuePairInput[];
|
|
68272
68310
|
|
|
@@ -68670,6 +68708,15 @@ export class MJRemoteOperation_ {
|
|
|
68670
68708
|
@Field()
|
|
68671
68709
|
_mj__UpdatedAt: Date;
|
|
68672
68710
|
|
|
68711
|
+
@Field(() => Boolean, {description: `When 1, the AI-generated Code is frozen and Save() will not regenerate it even if Description changes (the Generated-Actions CodeLocked analog). Default 0.`})
|
|
68712
|
+
CodeLocked: boolean;
|
|
68713
|
+
|
|
68714
|
+
@Field({nullable: true, description: `The model's explanation / comments for the AI-generated Code (populated alongside Code when GenerationType=AI). Human-facing review aid.`})
|
|
68715
|
+
CodeComments?: string;
|
|
68716
|
+
|
|
68717
|
+
@Field({nullable: true, description: `JSON array of the libraries the generated body imports: [{ "Library": "@memberjunction/ai-prompts", "ItemsUsed": ["AIPromptRunner"] }, ...]. Bound to the RemoteOperationLibrary JSONType via metadata sync so CodeGen emits a typed LibrariesObject accessor; CodeGen uses it to emit the imports at the top of the generated remote_operations.ts. NULL/empty = only the default always-available libraries are imported.`})
|
|
68718
|
+
Libraries?: string;
|
|
68719
|
+
|
|
68673
68720
|
@Field({nullable: true})
|
|
68674
68721
|
@MaxLength(255)
|
|
68675
68722
|
Category?: string;
|
|
@@ -68757,6 +68804,15 @@ export class CreateMJRemoteOperationInput {
|
|
|
68757
68804
|
@Field(() => Int, { nullable: true })
|
|
68758
68805
|
MaxConcurrency: number | null;
|
|
68759
68806
|
|
|
68807
|
+
@Field(() => Boolean, { nullable: true })
|
|
68808
|
+
CodeLocked?: boolean;
|
|
68809
|
+
|
|
68810
|
+
@Field({ nullable: true })
|
|
68811
|
+
CodeComments: string | null;
|
|
68812
|
+
|
|
68813
|
+
@Field({ nullable: true })
|
|
68814
|
+
Libraries: string | null;
|
|
68815
|
+
|
|
68760
68816
|
@Field(() => RestoreContextInput, { nullable: true })
|
|
68761
68817
|
RestoreContext___?: RestoreContextInput;
|
|
68762
68818
|
}
|
|
@@ -68839,6 +68895,15 @@ export class UpdateMJRemoteOperationInput {
|
|
|
68839
68895
|
@Field(() => Int, { nullable: true })
|
|
68840
68896
|
MaxConcurrency?: number | null;
|
|
68841
68897
|
|
|
68898
|
+
@Field(() => Boolean, { nullable: true })
|
|
68899
|
+
CodeLocked?: boolean;
|
|
68900
|
+
|
|
68901
|
+
@Field({ nullable: true })
|
|
68902
|
+
CodeComments?: string | null;
|
|
68903
|
+
|
|
68904
|
+
@Field({ nullable: true })
|
|
68905
|
+
Libraries?: string | null;
|
|
68906
|
+
|
|
68842
68907
|
@Field(() => [KeyValuePairInput], { nullable: true })
|
|
68843
68908
|
OldValues___?: KeyValuePairInput[];
|
|
68844
68909
|
|
package/src/index.ts
CHANGED
|
@@ -863,7 +863,11 @@ export const serve = async (resolverPaths: Array<string>, app: Application = cre
|
|
|
863
863
|
onConnect: async (ctx) => {
|
|
864
864
|
try {
|
|
865
865
|
const token = String(ctx.connectionParams?.Authorization);
|
|
866
|
-
|
|
866
|
+
// Carry API keys from connectionParams so API-key / MCP / Node clients can authenticate the socket
|
|
867
|
+
// (validated the same way as the HTTP x-mj-api-key / x-mj-user-api-key headers).
|
|
868
|
+
const systemApiKey = ctx.connectionParams?.['x-mj-api-key'] ? String(ctx.connectionParams['x-mj-api-key']) : undefined;
|
|
869
|
+
const userApiKey = ctx.connectionParams?.['x-mj-user-api-key'] ? String(ctx.connectionParams['x-mj-user-api-key']) : undefined;
|
|
870
|
+
const userPayload = await getUserPayload(token, undefined, dataSources, undefined, systemApiKey, userApiKey);
|
|
867
871
|
|
|
868
872
|
// Store validated payload on the connection for use in context()
|
|
869
873
|
ctx.extra.userPayload = userPayload;
|
|
@@ -1,10 +1,14 @@
|
|
|
1
|
-
import { Arg, Ctx, Field, InputType, Mutation, ObjectType, Resolver } from 'type-graphql';
|
|
1
|
+
import { Arg, Ctx, Field, ID, InputType, Mutation, ObjectType, PubSub, PubSubEngine, Resolver, Root, Subscription } from 'type-graphql';
|
|
2
2
|
import { MJGlobal } from '@memberjunction/global';
|
|
3
|
-
import { BaseRemotableOperation, RemoteOpInvokeMode } from '@memberjunction/core';
|
|
3
|
+
import { BaseRemotableOperation, RemoteOpInvokeMode, RemoteOpProgress } from '@memberjunction/core';
|
|
4
|
+
import { RemoteOperationEngineBase } from '@memberjunction/core-entities';
|
|
4
5
|
import { ResolverBase } from '../generic/ResolverBase.js';
|
|
5
6
|
import { AppContext } from '../types.js';
|
|
6
7
|
import { GetReadWriteProvider } from '../util.js';
|
|
7
8
|
|
|
9
|
+
/** PubSub topic carrying RemoteOpProgress envelopes for attached over-the-wire callers (RO-3). */
|
|
10
|
+
const REMOTE_OP_PROGRESS_TOPIC = 'RemoteOperationProgress';
|
|
11
|
+
|
|
8
12
|
/** Input for the generic Remote Operation transport mutation. */
|
|
9
13
|
@InputType()
|
|
10
14
|
export class ExecuteRemoteOperationInput {
|
|
@@ -16,6 +20,25 @@ export class ExecuteRemoteOperationInput {
|
|
|
16
20
|
|
|
17
21
|
@Field()
|
|
18
22
|
invokeMode: string;
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Optional client-generated channel id. When set (attached mode over the wire), the server publishes every
|
|
26
|
+
* `RemoteOpProgress` the operation emits to the `RemoteOperationProgress` subscription filtered by this id,
|
|
27
|
+
* so the caller's `onProgress` fires live while the operation runs. Omitted for sync / no-progress calls.
|
|
28
|
+
*/
|
|
29
|
+
@Field(() => String, { nullable: true })
|
|
30
|
+
progressChannelId?: string;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** A single streamed progress envelope delivered over the `RemoteOperationProgress` subscription. */
|
|
34
|
+
@ObjectType()
|
|
35
|
+
export class RemoteOperationProgressNotification {
|
|
36
|
+
@Field()
|
|
37
|
+
ChannelId: string;
|
|
38
|
+
|
|
39
|
+
/** JSON-serialized {@link RemoteOpProgress}. */
|
|
40
|
+
@Field()
|
|
41
|
+
ProgressJSON: string;
|
|
19
42
|
}
|
|
20
43
|
|
|
21
44
|
/** Result of the generic Remote Operation transport mutation. */
|
|
@@ -50,6 +73,7 @@ export class ExecuteRemoteOperationResolver extends ResolverBase {
|
|
|
50
73
|
async ExecuteRemoteOperation(
|
|
51
74
|
@Arg('input') input: ExecuteRemoteOperationInput,
|
|
52
75
|
@Ctx() ctx: AppContext,
|
|
76
|
+
@PubSub() pubSub: PubSubEngine,
|
|
53
77
|
): Promise<ExecuteRemoteOperationResultType> {
|
|
54
78
|
try {
|
|
55
79
|
const key = input.operationKey?.trim();
|
|
@@ -78,6 +102,15 @@ export class ExecuteRemoteOperationResolver extends ResolverBase {
|
|
|
78
102
|
return fail('NO_USER', 'Not authenticated');
|
|
79
103
|
}
|
|
80
104
|
|
|
105
|
+
// (4) Metadata gate (defense-in-depth): honor the op's MJ: Remote Operations Status/approval, so a
|
|
106
|
+
// registered-but-disabled (or unapproved AI) op is rejected even though its class is still in code.
|
|
107
|
+
const provider = GetReadWriteProvider(ctx.providers);
|
|
108
|
+
await RemoteOperationEngineBase.Instance.Config(false, user, provider);
|
|
109
|
+
const invokability = RemoteOperationEngineBase.Instance.IsInvokable(key);
|
|
110
|
+
if (!invokability.Invokable) {
|
|
111
|
+
return fail(invokability.ResultCode ?? 'OPERATION_UNAVAILABLE', invokability.Reason ?? `Operation '${key}' is unavailable`);
|
|
112
|
+
}
|
|
113
|
+
|
|
81
114
|
let parsedInput: unknown;
|
|
82
115
|
try {
|
|
83
116
|
parsedInput = input.inputJSON ? JSON.parse(input.inputJSON) : {};
|
|
@@ -85,9 +118,16 @@ export class ExecuteRemoteOperationResolver extends ResolverBase {
|
|
|
85
118
|
return fail('INVALID_INPUT_JSON', `Invalid input JSON: ${e instanceof Error ? e.message : String(e)}`);
|
|
86
119
|
}
|
|
87
120
|
|
|
121
|
+
// Attached over-the-wire (RO-3): forward each emitted RemoteOpProgress to the progress channel so a
|
|
122
|
+
// remote caller's onProgress fires live. No-op when the caller didn't open a channel (sync calls).
|
|
123
|
+
const onProgress = input.progressChannelId
|
|
124
|
+
? (p: RemoteOpProgress) => {
|
|
125
|
+
void pubSub.publish(REMOTE_OP_PROGRESS_TOPIC, { ChannelId: input.progressChannelId!, ProgressJSON: JSON.stringify(p) });
|
|
126
|
+
}
|
|
127
|
+
: undefined;
|
|
128
|
+
|
|
88
129
|
// Route through the per-request provider (server in-process path; runs the op's Authorize + InternalExecute).
|
|
89
|
-
const
|
|
90
|
-
const result = await provider.RouteOperation(key, parsedInput, { user, mode: input.invokeMode as RemoteOpInvokeMode });
|
|
130
|
+
const result = await provider.RouteOperation(key, parsedInput, { user, mode: input.invokeMode as RemoteOpInvokeMode, onProgress });
|
|
91
131
|
|
|
92
132
|
return {
|
|
93
133
|
success: result.Success,
|
|
@@ -100,6 +140,22 @@ export class ExecuteRemoteOperationResolver extends ResolverBase {
|
|
|
100
140
|
return fail('ERROR', e instanceof Error ? e.message : String(e));
|
|
101
141
|
}
|
|
102
142
|
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Subscription the client opens (with a self-generated `channelId`) immediately before calling
|
|
146
|
+
* `ExecuteRemoteOperation` with the same id in `progressChannelId`. The server publishes one event per
|
|
147
|
+
* `RemoteOpProgress` the operation emits; filtered by `channelId` so concurrent calls never interleave.
|
|
148
|
+
*/
|
|
149
|
+
@Subscription(() => RemoteOperationProgressNotification, {
|
|
150
|
+
topics: REMOTE_OP_PROGRESS_TOPIC,
|
|
151
|
+
filter: ({ payload, args }: { payload: RemoteOperationProgressNotification; args: { channelId: string } }) => payload.ChannelId === args.channelId,
|
|
152
|
+
})
|
|
153
|
+
RemoteOperationProgress(
|
|
154
|
+
@Root() notification: RemoteOperationProgressNotification,
|
|
155
|
+
@Arg('channelId', () => ID) _channelId: string,
|
|
156
|
+
): RemoteOperationProgressNotification {
|
|
157
|
+
return notification;
|
|
158
|
+
}
|
|
103
159
|
}
|
|
104
160
|
|
|
105
161
|
/** Builds a failed result. */
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Resolver, Query, Mutation, Arg, Ctx, ObjectType, Field, InputType, Int, Float } from "type-graphql";
|
|
2
|
-
import { CompositeKey, DatabaseProviderBase, LocalCacheManager, Metadata, RunView, UserInfo, LogError, LogStatus, IMetadataProvider, TransactionGroupBase } from "@memberjunction/core";
|
|
2
|
+
import { CompositeKey, DatabaseProviderBase, EntityInfo, LocalCacheManager, Metadata, RunView, UserInfo, LogError, LogStatus, IMetadataProvider, TransactionGroupBase } from "@memberjunction/core";
|
|
3
3
|
import { GetReadOnlyProvider, GetReadWriteProvider } from "../util.js";
|
|
4
4
|
import { CronExpressionHelper } from "@memberjunction/scheduling-engine";
|
|
5
5
|
import {
|
|
@@ -3451,9 +3451,29 @@ export class IntegrationDiscoveryResolver extends ResolverBase {
|
|
|
3451
3451
|
): Promise<ApplyAllEntityMapCreated[]> {
|
|
3452
3452
|
const results: ApplyAllEntityMapCreated[] = [];
|
|
3453
3453
|
|
|
3454
|
+
// PERF (large-catalog ApplyAll, e.g. Salesforce 1695 objects): precompute O(1) lookups ONCE before
|
|
3455
|
+
// the per-object loop. The per-object path previously did an O(N) `provider.Entities.find` + a
|
|
3456
|
+
// BypassCache RunView for the existing entity map PER object → O(N²) scans + N DB round-trips, so a
|
|
3457
|
+
// 1695-object connector burned its whole budget in ApplyAll and FullSync never started. One entity
|
|
3458
|
+
// index + one batched entity-map query make both O(1) per object.
|
|
3459
|
+
const entityByKey = new Map(
|
|
3460
|
+
provider.Entities.map(e => [`${e.SchemaName.toLowerCase()}|${e.BaseTable.toLowerCase()}`, e] as const)
|
|
3461
|
+
);
|
|
3462
|
+
const existingMapByExternalName = new Map<string, MJCompanyIntegrationEntityMapEntity>();
|
|
3463
|
+
const allMaps = await new RunView().RunView<MJCompanyIntegrationEntityMapEntity>({
|
|
3464
|
+
EntityName: 'MJ: Company Integration Entity Maps',
|
|
3465
|
+
ExtraFilter: `CompanyIntegrationID='${companyIntegrationID}'`,
|
|
3466
|
+
ResultType: 'entity_object',
|
|
3467
|
+
BypassCache: true, // idempotency must read COMMITTED state, not a possibly-stale filtered cache
|
|
3468
|
+
}, user);
|
|
3469
|
+
if (allMaps.Success) {
|
|
3470
|
+
for (const m of allMaps.Results) existingMapByExternalName.set((m.ExternalObjectName ?? '').toLowerCase(), m);
|
|
3471
|
+
}
|
|
3472
|
+
|
|
3454
3473
|
for (const obj of objects) {
|
|
3455
3474
|
const entityMapResult = await this.createSingleEntityMap(
|
|
3456
|
-
companyIntegrationID, obj, connector, companyIntegration, schemaName, user, provider, defaultSyncDirection
|
|
3475
|
+
companyIntegrationID, obj, connector, companyIntegration, schemaName, user, provider, defaultSyncDirection,
|
|
3476
|
+
entityByKey, existingMapByExternalName
|
|
3457
3477
|
);
|
|
3458
3478
|
if (entityMapResult) {
|
|
3459
3479
|
results.push(entityMapResult);
|
|
@@ -3471,10 +3491,14 @@ export class IntegrationDiscoveryResolver extends ResolverBase {
|
|
|
3471
3491
|
schemaName: string,
|
|
3472
3492
|
user: UserInfo,
|
|
3473
3493
|
md: IMetadataProvider,
|
|
3474
|
-
defaultSyncDirection: string = 'Pull'
|
|
3494
|
+
defaultSyncDirection: string = 'Pull',
|
|
3495
|
+
entityByKey?: Map<string, EntityInfo>,
|
|
3496
|
+
existingMapByExternalName?: Map<string, MJCompanyIntegrationEntityMapEntity>
|
|
3475
3497
|
): Promise<ApplyAllEntityMapCreated | null> {
|
|
3476
|
-
// Find the entity by schema + table name
|
|
3477
|
-
|
|
3498
|
+
// Find the entity by schema + table name. PERF: O(1) precomputed index when provided
|
|
3499
|
+
// (large-catalog ApplyAll); falls back to the O(N) scan when called without it.
|
|
3500
|
+
const entityInfo = entityByKey?.get(`${schemaName.toLowerCase()}|${obj.TableName.toLowerCase()}`)
|
|
3501
|
+
?? md.Entities.find(
|
|
3478
3502
|
e => e.SchemaName.toLowerCase() === schemaName.toLowerCase()
|
|
3479
3503
|
&& e.BaseTable.toLowerCase() === obj.TableName.toLowerCase()
|
|
3480
3504
|
);
|
|
@@ -3488,19 +3512,27 @@ export class IntegrationDiscoveryResolver extends ResolverBase {
|
|
|
3488
3512
|
// multiplies the maps in lockstep (N applies → N duplicate maps per object), which
|
|
3489
3513
|
// silently corrupts the record-map 1:1 completeness gate and makes the forward sync
|
|
3490
3514
|
// process each object N times. Reuse the existing (connection, external object) map instead.
|
|
3491
|
-
|
|
3492
|
-
|
|
3493
|
-
|
|
3494
|
-
|
|
3495
|
-
|
|
3496
|
-
|
|
3497
|
-
|
|
3498
|
-
|
|
3499
|
-
|
|
3515
|
+
// PERF: use the precomputed existing-map index when provided (one batched query for the whole
|
|
3516
|
+
// ApplyAll instead of one BypassCache RunView per object); falls back to a per-object query.
|
|
3517
|
+
let existing: MJCompanyIntegrationEntityMapEntity | undefined;
|
|
3518
|
+
if (existingMapByExternalName) {
|
|
3519
|
+
existing = existingMapByExternalName.get(obj.SourceObjectName.toLowerCase());
|
|
3520
|
+
} else {
|
|
3521
|
+
const escapedObjectName = obj.SourceObjectName.replace(/'/g, "''");
|
|
3522
|
+
const existingMapResult = await new RunView().RunView<MJCompanyIntegrationEntityMapEntity>({
|
|
3523
|
+
EntityName: 'MJ: Company Integration Entity Maps',
|
|
3524
|
+
ExtraFilter: `CompanyIntegrationID='${companyIntegrationID}' AND ExternalObjectName='${escapedObjectName}'`,
|
|
3525
|
+
OrderBy: '__mj_CreatedAt ASC',
|
|
3526
|
+
MaxRows: 1,
|
|
3527
|
+
ResultType: 'entity_object',
|
|
3528
|
+
BypassCache: true, // idempotency must read COMMITTED state, not a possibly-stale filtered cache
|
|
3529
|
+
}, user);
|
|
3530
|
+
if (existingMapResult.Success && existingMapResult.Results.length > 0) existing = existingMapResult.Results[0];
|
|
3531
|
+
}
|
|
3500
3532
|
|
|
3501
3533
|
let em: MJCompanyIntegrationEntityMapEntity;
|
|
3502
|
-
if (
|
|
3503
|
-
em =
|
|
3534
|
+
if (existing) {
|
|
3535
|
+
em = existing; // reuse — keeps the map stable across re-applies
|
|
3504
3536
|
} else {
|
|
3505
3537
|
em = await md.GetEntityObject<MJCompanyIntegrationEntityMapEntity>('MJ: Company Integration Entity Maps', user);
|
|
3506
3538
|
em.NewRecord();
|
|
@@ -3634,26 +3666,70 @@ export class IntegrationDiscoveryResolver extends ResolverBase {
|
|
|
3634
3666
|
* still surfaces every object/field; only materialization is capped. Per-table column count = the selected
|
|
3635
3667
|
* field subset, or the object's full discovered field count when all fields are selected.
|
|
3636
3668
|
*/
|
|
3637
|
-
private enforceSchemaLimits(
|
|
3669
|
+
private async enforceSchemaLimits(
|
|
3638
3670
|
objects: SchemaPreviewObjectInput[],
|
|
3639
3671
|
filteredSchema: SourceSchemaInfo,
|
|
3640
|
-
|
|
3641
|
-
|
|
3642
|
-
|
|
3643
|
-
|
|
3644
|
-
|
|
3672
|
+
companyIntegration: MJCompanyIntegrationEntity,
|
|
3673
|
+
user: UserInfo,
|
|
3674
|
+
md: IMetadataProvider,
|
|
3675
|
+
): Promise<string[]> {
|
|
3676
|
+
const warnings: string[] = [];
|
|
3645
3677
|
const fullCountByName = new Map(filteredSchema.Objects.map(o => [o.ExternalName.toLowerCase(), o.Fields.length]));
|
|
3646
|
-
//
|
|
3678
|
+
// Use the MAX of the input object's Fields and the filtered-schema count — the batch path can pass
|
|
3679
|
+
// objects with empty Fields (the real columns come from filteredSchema, which is what buildSchema
|
|
3680
|
+
// materializes). `?? ` would wrongly read an empty [] as 0 (0 is not nullish); Math.max is robust to
|
|
3681
|
+
// either source being empty and reflects the count that will actually become columns.
|
|
3682
|
+
const colCount = (o: SchemaPreviewObjectInput): number =>
|
|
3683
|
+
Math.max(fullCountByName.get(o.SourceObjectName.toLowerCase()) ?? 0, o.Fields?.length ?? 0);
|
|
3684
|
+
|
|
3685
|
+
// ── Per-table COLUMN limit: DISABLE-the-offender, NOT reject-the-apply. The limit is clamped at
|
|
3686
|
+
// startup to ≤ IntegrationEngine.MAX_COLUMNS_CEILING (SQL Server's 1024 minus framework column
|
|
3687
|
+
// headroom). An object exceeding it is set Status='Disabled' (reversible — it re-enables on a later
|
|
3688
|
+
// discovery if it has shrunk back under the limit) and dropped from THIS apply, so the materializable
|
|
3689
|
+
// objects still sync instead of the whole apply failing CREATE TABLE. One CLI warning per disabled object.
|
|
3690
|
+
const limit = IntegrationEngine.Instance.MaxColumnsPerTable;
|
|
3691
|
+
const overLimit = objects.filter(o => colCount(o) > limit);
|
|
3692
|
+
if (overLimit.length > 0) {
|
|
3693
|
+
const ioIDByName = new Map(
|
|
3694
|
+
IntegrationEngineBase.Instance.GetActiveIntegrationObjects(companyIntegration.IntegrationID)
|
|
3695
|
+
.map(io => [io.Name.toLowerCase(), io.ID] as [string, string])
|
|
3696
|
+
);
|
|
3697
|
+
for (const o of overLimit) {
|
|
3698
|
+
warnings.push(`Disabled "${o.SourceObjectName}" (${colCount(o)} columns > limit ${limit}) — too wide to materialize as one table; reduce its columns or it stays disabled.`);
|
|
3699
|
+
const ioID = ioIDByName.get(o.SourceObjectName.toLowerCase());
|
|
3700
|
+
if (ioID) {
|
|
3701
|
+
const io = await md.GetEntityObject<MJIntegrationObjectEntity>('MJ: Integration Objects', user);
|
|
3702
|
+
if (await io.Load(ioID)) {
|
|
3703
|
+
io.Status = 'Disabled';
|
|
3704
|
+
if (!await io.Save()) {
|
|
3705
|
+
LogError(`enforceSchemaLimits: failed to disable over-wide IO '${o.SourceObjectName}': ${io.LatestResult?.CompleteMessage ?? 'unknown error'}`);
|
|
3706
|
+
}
|
|
3707
|
+
}
|
|
3708
|
+
}
|
|
3709
|
+
}
|
|
3710
|
+
// Drop the disabled objects from BOTH the shared objects array (used downstream by
|
|
3711
|
+
// buildTargetConfigs + createEntityAndFieldMaps) and filteredSchema, in place.
|
|
3712
|
+
const disabled = new Set(overLimit.map(o => o.SourceObjectName.toLowerCase()));
|
|
3713
|
+
for (let i = objects.length - 1; i >= 0; i--) {
|
|
3714
|
+
if (disabled.has(objects[i].SourceObjectName.toLowerCase())) objects.splice(i, 1);
|
|
3715
|
+
}
|
|
3716
|
+
filteredSchema.Objects = filteredSchema.Objects.filter(o => !disabled.has(o.ExternalName.toLowerCase()));
|
|
3717
|
+
}
|
|
3718
|
+
|
|
3719
|
+
// MaxTables remains an operator/env REJECT guardrail (unchanged), evaluated on the surviving objects.
|
|
3720
|
+
// Columns are handled above via disable, so the pure decision is asked about tables only here.
|
|
3721
|
+
const maxTables = ((): number | null => {
|
|
3722
|
+
const v = parseInt(process.env.MJ_INTEGRATION_MAX_TABLES ?? '', 10);
|
|
3723
|
+
return Number.isFinite(v) && v > 0 ? v : null;
|
|
3724
|
+
})();
|
|
3647
3725
|
const violations = decideSchemaLimitViolations({
|
|
3648
3726
|
TableCount: objects.length,
|
|
3649
|
-
ColumnCountByTable:
|
|
3650
|
-
|
|
3651
|
-
|
|
3652
|
-
})),
|
|
3653
|
-
MaxTables: envInt('MJ_INTEGRATION_MAX_TABLES'),
|
|
3654
|
-
MaxColumnsPerTable: envInt('MJ_INTEGRATION_MAX_COLUMNS_PER_TABLE'),
|
|
3727
|
+
ColumnCountByTable: [],
|
|
3728
|
+
MaxTables: maxTables,
|
|
3729
|
+
MaxColumnsPerTable: null,
|
|
3655
3730
|
});
|
|
3656
3731
|
if (violations.length > 0) throw new Error(violations.join(' '));
|
|
3732
|
+
return warnings;
|
|
3657
3733
|
}
|
|
3658
3734
|
|
|
3659
3735
|
private async buildSchemaForConnector(
|
|
@@ -3717,7 +3793,7 @@ export class IntegrationDiscoveryResolver extends ResolverBase {
|
|
|
3717
3793
|
// §B — reject an over-limit table/column SELECTION before ANY table is materialized. This is the
|
|
3718
3794
|
// single shared gate for ApplyAll / ApplyAllBatch / ApplySchemaBatch (all route through here).
|
|
3719
3795
|
// Caps are operator/env guardrails (MJ_INTEGRATION_MAX_TABLES / _MAX_COLUMNS_PER_TABLE).
|
|
3720
|
-
this.enforceSchemaLimits(objects, filteredSchema);
|
|
3796
|
+
const limitWarnings = await this.enforceSchemaLimits(objects, filteredSchema, companyIntegration, user, provider);
|
|
3721
3797
|
|
|
3722
3798
|
const targetConfigs = this.buildTargetConfigs(objects, filteredSchema, platform, connector);
|
|
3723
3799
|
|
|
@@ -3736,6 +3812,8 @@ export class IntegrationDiscoveryResolver extends ResolverBase {
|
|
|
3736
3812
|
|
|
3737
3813
|
const builder = new SchemaBuilder();
|
|
3738
3814
|
const schemaOutput = builder.BuildSchema(input);
|
|
3815
|
+
// Surface the auto-disabled over-wide objects (if any) in the apply's Warnings → CLI output.
|
|
3816
|
+
if (limitWarnings.length > 0) schemaOutput.Warnings.unshift(...limitWarnings);
|
|
3739
3817
|
|
|
3740
3818
|
if (schemaOutput.Errors.length > 0) {
|
|
3741
3819
|
throw new Error(`Schema generation failed: ${schemaOutput.Errors.join('; ')}`);
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import { Resolver, Mutation, Query, Arg, Ctx, ObjectType, InputType, Field } from 'type-graphql';
|
|
2
2
|
import { randomUUID } from 'crypto';
|
|
3
|
-
import { LogError, UserInfo, IMetadataProvider } from '@memberjunction/core';
|
|
3
|
+
import { LogError, LogStatusEx, UserInfo, IMetadataProvider } from '@memberjunction/core';
|
|
4
4
|
import { LiveKitTokenService, LiveKitAgentRoomCoordinator, LiveKitEgressService } from '@memberjunction/livekit-room-server';
|
|
5
5
|
import { AppContext } from '../types.js';
|
|
6
6
|
import { ResolverBase } from '../generic/ResolverBase.js';
|
|
7
7
|
import { GetReadWriteProvider } from '../util.js';
|
|
8
|
-
import { CreateBridgeRealtimeSession, FinalizeBridgeCoAgentRuns, GetRealtimeModelVoices, CreateBridgeRoomTranscriptSink } from '@memberjunction/ai-agents';
|
|
8
|
+
import { CreateBridgeRealtimeSession, FinalizeBridgeCoAgentRuns, GetRealtimeModelVoices, CreateBridgeRoomTranscriptSink, RealtimeTurnModeratorDecision } from '@memberjunction/ai-agents';
|
|
9
9
|
import { AIBridgeEngine } from '@memberjunction/ai-bridge-server';
|
|
10
10
|
import { SessionManager } from '../agentSessions/SessionManager.js';
|
|
11
11
|
import { NotificationEngine } from '@memberjunction/notifications';
|
|
@@ -36,9 +36,25 @@ AIBridgeEngine.Instance.SetSessionRunFinalizer(FinalizeBridgeCoAgentRuns);
|
|
|
36
36
|
* "Meeting Room"/scope choices live HERE (the Meet composition layer), keeping the engine generic.
|
|
37
37
|
*/
|
|
38
38
|
AIBridgeEngine.Instance.SetTranscriptSink(
|
|
39
|
-
CreateBridgeRoomTranscriptSink({ ConversationType: 'Meeting Room', ApplicationScope: 'Application' }),
|
|
39
|
+
CreateBridgeRoomTranscriptSink({ ConversationType: 'Meeting Room', ApplicationScope: 'Application', ApplicationName: 'Meet' }),
|
|
40
40
|
);
|
|
41
41
|
|
|
42
|
+
/**
|
|
43
|
+
* Binds the room **turn moderator** onto the bridge engine — **OPT-IN, off by default**. When
|
|
44
|
+
* `MJ_REALTIME_MODERATOR_MODE=on`, a multi-agent room routes each turn through a fast LLM prompt that decides
|
|
45
|
+
* who speaks (see `RealtimeTurnModerator` in `@memberjunction/ai-agents`). By default it's OFF: agents run in
|
|
46
|
+
* plain auto-response, hear everything, and self-moderate (no STT-driven router in the loop) — the
|
|
47
|
+
* coordinator likewise skips meeting mode when the flag is off, so the two stay consistent. We keep the
|
|
48
|
+
* moderator wired-but-toggleable for controlled scenarios (webinars, large rooms, weaker models).
|
|
49
|
+
*/
|
|
50
|
+
if (process.env.MJ_REALTIME_MODERATOR_MODE === 'on') {
|
|
51
|
+
AIBridgeEngine.Instance.SetTurnModerator(RealtimeTurnModeratorDecision);
|
|
52
|
+
console.log('[RealtimeBridge] turn MODERATOR mode is ON (MJ_REALTIME_MODERATOR_MODE=on) — multi-agent rooms use the LLM router.');
|
|
53
|
+
} else {
|
|
54
|
+
// Default mode — only surface this at startup when verbose (MJ_VERBOSE) is on; it's the expected state and otherwise just noise.
|
|
55
|
+
LogStatusEx({ message: '[RealtimeBridge] turn moderator mode is OFF (default) — multi-agent rooms run free-for-all: all agents auto-respond + hear everything.', verboseOnly: true });
|
|
56
|
+
}
|
|
57
|
+
|
|
42
58
|
/**
|
|
43
59
|
* GraphQL surface for the MJ-native LiveKit room: mints scoped client access tokens and starts an
|
|
44
60
|
* agent's presence in a room. The thin resolver delegates to `@memberjunction/livekit-room-server`
|
|
@@ -319,6 +335,33 @@ export class RealtimeBridgeResolver extends ResolverBase {
|
|
|
319
335
|
}
|
|
320
336
|
}
|
|
321
337
|
|
|
338
|
+
/**
|
|
339
|
+
* **Ends the meeting for everyone**: stops EVERY agent bot bridged into a room (by room name, via the
|
|
340
|
+
* coordinator's server-side roster). This is the "End meeting" half of the Zoom-style leave control —
|
|
341
|
+
* usable by any participant, including one who only *joined* the room and never tracked the bridge ids.
|
|
342
|
+
* Returns `true` when the teardown ran (even if the room held zero agents). Best-effort: any error → `false`.
|
|
343
|
+
*
|
|
344
|
+
* @param roomName The LiveKit room to end.
|
|
345
|
+
*/
|
|
346
|
+
@Mutation(() => Boolean)
|
|
347
|
+
async EndLiveKitRoom(
|
|
348
|
+
@Arg('roomName', () => String) roomName: string,
|
|
349
|
+
@Ctx() context: AppContext = {} as AppContext,
|
|
350
|
+
): Promise<boolean> {
|
|
351
|
+
try {
|
|
352
|
+
const user = this.GetUserFromPayload(context.userPayload);
|
|
353
|
+
if (!user) {
|
|
354
|
+
return false;
|
|
355
|
+
}
|
|
356
|
+
const provider = GetReadWriteProvider(context.providers) as unknown as IMetadataProvider;
|
|
357
|
+
await LiveKitAgentRoomCoordinator.Instance.StopAllAgentsInRoom(roomName, 'Explicit', user, provider);
|
|
358
|
+
return true;
|
|
359
|
+
} catch (error) {
|
|
360
|
+
LogError(`EndLiveKitRoom failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
361
|
+
return false;
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
|
|
322
365
|
/**
|
|
323
366
|
* Lists active Realtime models with the voices each driver supports — the source for the dev model/voice
|
|
324
367
|
* picker (gated client-side by the `Realtime: Advanced Session Controls` authorization). Read-only; returns
|
|
@@ -21,8 +21,16 @@ export class TemplateRunResult {
|
|
|
21
21
|
executionTimeMs?: number;
|
|
22
22
|
}
|
|
23
23
|
|
|
24
|
+
/**
|
|
25
|
+
* @deprecated Prefer the `Template.Run` Remote Operation (`@memberjunction/templates-base-types`
|
|
26
|
+
* `TemplateRunOperation`) — one typed call site that routes over the generic `ExecuteRemoteOperation`
|
|
27
|
+
* transport on the client and in-process on the server. This bespoke `RunTemplate` mutation is retained
|
|
28
|
+
* only for backward compatibility with existing external API consumers and may be removed in a future
|
|
29
|
+
* major version.
|
|
30
|
+
*/
|
|
24
31
|
@Resolver()
|
|
25
32
|
export class RunTemplateResolver extends ResolverBase {
|
|
33
|
+
/** @deprecated Use the `Template.Run` Remote Operation instead. Retained for backcompat. */
|
|
26
34
|
@Mutation(() => TemplateRunResult)
|
|
27
35
|
async RunTemplate(
|
|
28
36
|
@Arg('templateId') templateId: string,
|