@memberjunction/server 5.38.0 → 5.39.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.
Files changed (108) hide show
  1. package/README.md +67 -0
  2. package/dist/apolloServer/index.d.ts +0 -8
  3. package/dist/apolloServer/index.d.ts.map +1 -1
  4. package/dist/apolloServer/index.js +61 -0
  5. package/dist/apolloServer/index.js.map +1 -1
  6. package/dist/config.d.ts +68 -0
  7. package/dist/config.d.ts.map +1 -1
  8. package/dist/config.js +24 -0
  9. package/dist/config.js.map +1 -1
  10. package/dist/context.d.ts.map +1 -1
  11. package/dist/context.js +2 -21
  12. package/dist/context.js.map +1 -1
  13. package/dist/generated/generated.d.ts +75 -0
  14. package/dist/generated/generated.d.ts.map +1 -1
  15. package/dist/generated/generated.js +314 -1
  16. package/dist/generated/generated.js.map +1 -1
  17. package/dist/generic/FireAndForgetHeartbeat.d.ts +51 -0
  18. package/dist/generic/FireAndForgetHeartbeat.d.ts.map +1 -0
  19. package/dist/generic/FireAndForgetHeartbeat.js +44 -0
  20. package/dist/generic/FireAndForgetHeartbeat.js.map +1 -0
  21. package/dist/index.d.ts +3 -0
  22. package/dist/index.d.ts.map +1 -1
  23. package/dist/index.js +120 -2
  24. package/dist/index.js.map +1 -1
  25. package/dist/logging/NoLog.d.ts +50 -0
  26. package/dist/logging/NoLog.d.ts.map +1 -0
  27. package/dist/logging/NoLog.js +80 -0
  28. package/dist/logging/NoLog.js.map +1 -0
  29. package/dist/logging/bootAudit.d.ts +43 -0
  30. package/dist/logging/bootAudit.d.ts.map +1 -0
  31. package/dist/logging/bootAudit.js +83 -0
  32. package/dist/logging/bootAudit.js.map +1 -0
  33. package/dist/logging/boundaryLogPayload.d.ts +18 -0
  34. package/dist/logging/boundaryLogPayload.d.ts.map +1 -0
  35. package/dist/logging/boundaryLogPayload.js +18 -0
  36. package/dist/logging/boundaryLogPayload.js.map +1 -0
  37. package/dist/logging/secretRedactor.d.ts +23 -0
  38. package/dist/logging/secretRedactor.d.ts.map +1 -0
  39. package/dist/logging/secretRedactor.js +53 -0
  40. package/dist/logging/secretRedactor.js.map +1 -0
  41. package/dist/logging/shortenForLog.d.ts +8 -0
  42. package/dist/logging/shortenForLog.d.ts.map +1 -0
  43. package/dist/logging/shortenForLog.js +21 -0
  44. package/dist/logging/shortenForLog.js.map +1 -0
  45. package/dist/logging/variablesLoggingMiddleware.d.ts +22 -0
  46. package/dist/logging/variablesLoggingMiddleware.d.ts.map +1 -0
  47. package/dist/logging/variablesLoggingMiddleware.js +127 -0
  48. package/dist/logging/variablesLoggingMiddleware.js.map +1 -0
  49. package/dist/resolvers/GetDataResolver.d.ts.map +1 -1
  50. package/dist/resolvers/GetDataResolver.js +8 -4
  51. package/dist/resolvers/GetDataResolver.js.map +1 -1
  52. package/dist/resolvers/IntegrationDiscoveryResolver.d.ts +259 -2
  53. package/dist/resolvers/IntegrationDiscoveryResolver.d.ts.map +1 -1
  54. package/dist/resolvers/IntegrationDiscoveryResolver.js +1276 -117
  55. package/dist/resolvers/IntegrationDiscoveryResolver.js.map +1 -1
  56. package/dist/resolvers/IntegrationProgressResolver.d.ts +90 -0
  57. package/dist/resolvers/IntegrationProgressResolver.d.ts.map +1 -0
  58. package/dist/resolvers/IntegrationProgressResolver.js +196 -0
  59. package/dist/resolvers/IntegrationProgressResolver.js.map +1 -0
  60. package/dist/resolvers/MCPResolver.d.ts.map +1 -1
  61. package/dist/resolvers/MCPResolver.js +19 -14
  62. package/dist/resolvers/MCPResolver.js.map +1 -1
  63. package/dist/resolvers/RunAIAgentResolver.d.ts.map +1 -1
  64. package/dist/resolvers/RunAIAgentResolver.js +26 -5
  65. package/dist/resolvers/RunAIAgentResolver.js.map +1 -1
  66. package/dist/resolvers/RunTestResolver.d.ts.map +1 -1
  67. package/dist/resolvers/RunTestResolver.js +12 -2
  68. package/dist/resolvers/RunTestResolver.js.map +1 -1
  69. package/dist/resolvers/SearchEntitiesResolver.d.ts +46 -0
  70. package/dist/resolvers/SearchEntitiesResolver.d.ts.map +1 -0
  71. package/dist/resolvers/SearchEntitiesResolver.js +216 -0
  72. package/dist/resolvers/SearchEntitiesResolver.js.map +1 -0
  73. package/dist/resolvers/VectorizeEntityResolver.d.ts.map +1 -1
  74. package/dist/resolvers/VectorizeEntityResolver.js +14 -1
  75. package/dist/resolvers/VectorizeEntityResolver.js.map +1 -1
  76. package/dist/services/ScheduledJobsService.d.ts.map +1 -1
  77. package/dist/services/ScheduledJobsService.js +14 -2
  78. package/dist/services/ScheduledJobsService.js.map +1 -1
  79. package/package.json +75 -74
  80. package/src/__tests__/NoLog.test.ts +76 -0
  81. package/src/__tests__/bootAudit.test.ts +188 -0
  82. package/src/__tests__/boundaryLogPayload.test.ts +31 -0
  83. package/src/__tests__/getDataTokenRedaction.test.ts +84 -0
  84. package/src/__tests__/secretRedactor.test.ts +163 -0
  85. package/src/__tests__/subscriptionRedaction.test.ts +217 -0
  86. package/src/apolloServer/index.ts +58 -0
  87. package/src/config.ts +27 -0
  88. package/src/context.ts +2 -19
  89. package/src/generated/generated.ts +239 -1
  90. package/src/generic/FireAndForgetHeartbeat.ts +85 -0
  91. package/src/generic/__tests__/FireAndForgetHeartbeat.test.ts +99 -0
  92. package/src/index.ts +137 -2
  93. package/src/logging/NoLog.ts +88 -0
  94. package/src/logging/bootAudit.ts +117 -0
  95. package/src/logging/boundaryLogPayload.ts +19 -0
  96. package/src/logging/secretRedactor.ts +82 -0
  97. package/src/logging/shortenForLog.ts +17 -0
  98. package/src/logging/variablesLoggingMiddleware.ts +191 -0
  99. package/src/resolvers/GetDataResolver.ts +9 -5
  100. package/src/resolvers/IntegrationDiscoveryResolver.ts +1111 -120
  101. package/src/resolvers/IntegrationProgressResolver.ts +220 -0
  102. package/src/resolvers/MCPResolver.ts +18 -14
  103. package/src/resolvers/RunAIAgentResolver.ts +28 -5
  104. package/src/resolvers/RunTestResolver.ts +14 -2
  105. package/src/resolvers/SearchEntitiesResolver.ts +173 -0
  106. package/src/resolvers/VectorizeEntityResolver.ts +14 -1
  107. package/src/resolvers/__tests__/IntegrationProgressResolver.test.ts +170 -0
  108. package/src/services/ScheduledJobsService.ts +15 -2
@@ -15,6 +15,63 @@ import { SQLServerDataProvider } from '@memberjunction/sqlserver-dataprovider';
15
15
  * @param servers - HTTP server and WebSocket cleanup disposable
16
16
  * @param additionalPlugins - Optional additional plugins to merge with built-in plugins
17
17
  */
18
+ /**
19
+ * Apollo plugin: log every Integration* GraphQL operation to stdout as
20
+ * structured JSON. Lets operators tailing the MJAPI log see exactly which
21
+ * resolver the wizard / Explorer hit on each press, with arg names (values
22
+ * redacted for credentials). Diagnostic-only — no behavior change.
23
+ *
24
+ * Emits two events per operation:
25
+ * {"event":"gql.integration.request","method":"...","fieldName":"...","argNames":[...]}
26
+ * {"event":"gql.integration.response","method":"...","fieldName":"...","durationMs":N,"hasErrors":bool}
27
+ *
28
+ * Filter from the log:
29
+ * tail -f /tmp/mjapi.log | grep '"event":"gql\.integration\.'
30
+ */
31
+ const integrationOperationTracer: ApolloServerPlugin = {
32
+ async requestDidStart(requestCtx) {
33
+ // Off by default — the plugin stays registered but emits nothing unless the
34
+ // MJ_INTEGRATION_TRACE debug flag is explicitly set. Avoids logging on every
35
+ // request in normal operation.
36
+ if (process.env.MJ_INTEGRATION_TRACE !== 'true') return undefined;
37
+ const opName = requestCtx.request.operationName ?? 'anonymous';
38
+ const query = requestCtx.request.query ?? '';
39
+ // Lightweight match — fire only for operations that touch an Integration
40
+ // resolver field. Avoid logging every Color / User / Task query.
41
+ if (!query.includes('Integration')) return undefined;
42
+ const startedAt = Date.now();
43
+ return {
44
+ async willSendResponse(rc) {
45
+ // Walk the fields the operation queried; only those starting with
46
+ // "Integration" matter. Variables surfaced as keys only.
47
+ const op = rc.operation;
48
+ if (!op) return;
49
+ const integrationFields: string[] = [];
50
+ for (const sel of op.selectionSet.selections) {
51
+ if (sel.kind === 'Field' && sel.name.value.startsWith('Integration')) {
52
+ integrationFields.push(sel.name.value);
53
+ }
54
+ }
55
+ if (integrationFields.length === 0) return;
56
+ const argNames = Object.keys(rc.request.variables ?? {});
57
+ const durationMs = Date.now() - startedAt;
58
+ const hasErrors = !!(rc.response.body.kind === 'single' && rc.response.body.singleResult.errors?.length);
59
+ for (const field of integrationFields) {
60
+ console.log(JSON.stringify({
61
+ ts: new Date().toISOString(),
62
+ event: 'gql.integration.response',
63
+ operationName: opName,
64
+ method: field,
65
+ argNames,
66
+ durationMs,
67
+ hasErrors,
68
+ }));
69
+ }
70
+ },
71
+ };
72
+ },
73
+ };
74
+
18
75
  const buildApolloServer = (
19
76
  configOverride: ApolloServerOptions<AppContext>,
20
77
  { httpServer, serverCleanup }: { httpServer: Server; serverCleanup: Disposable },
@@ -22,6 +79,7 @@ const buildApolloServer = (
22
79
  ) => {
23
80
  const builtInPlugins: ApolloServerPlugin[] = [
24
81
  ApolloServerPluginDrainHttpServer({ httpServer }),
82
+ integrationOperationTracer,
25
83
  {
26
84
  async serverWillStart() {
27
85
  return {
package/src/config.ts CHANGED
@@ -205,6 +205,21 @@ const cacheSettingsSchema = z.object({
205
205
  verboseLogging: z.boolean().optional().default(false),
206
206
  });
207
207
 
208
+ const loggingSettingsSchema = z.object({
209
+ graphql: z.object({
210
+ /**
211
+ * When true, emit a redacted variables block per root resolver call via the
212
+ * type-graphql global middleware. Default: false in all environments regardless
213
+ * of NODE_ENV. Env override: `MJ_LOG_GRAPHQL_VARIABLES`.
214
+ *
215
+ * SECURITY: this is an opt-in verbose-echo path for developers debugging locally.
216
+ * The always-on request log line in `context.ts` does NOT emit variables — that
217
+ * is the load-bearing leak fix. This flag is additive on top of the always-on log.
218
+ */
219
+ logVariables: z.boolean().optional().default(false),
220
+ }).optional().default({}),
221
+ });
222
+
208
223
  const feedbackGithubSettingsSchema = z.object({
209
224
  owner: z.string().optional(),
210
225
  repo: z.string().optional(),
@@ -236,6 +251,7 @@ const configInfoSchema = z.object({
236
251
  multiTenancy: multiTenancySchema.optional().default({}),
237
252
  serverExtensions: z.array(serverExtensionSchema).optional().default([]),
238
253
  cacheSettings: cacheSettingsSchema.optional().default({}),
254
+ loggingSettings: loggingSettingsSchema.optional().default({}),
239
255
  feedbackSettings: feedbackSettingsSchema.optional().default({}),
240
256
 
241
257
  apiKey: z.string().optional(),
@@ -284,6 +300,7 @@ export type QueryDialectConfig = z.infer<typeof queryDialectSchema>;
284
300
  export type MultiTenancyConfig = z.infer<typeof multiTenancySchema>;
285
301
  export type ServerExtensionConfig = z.infer<typeof serverExtensionSchema>;
286
302
  export type CacheSettingsConfig = z.infer<typeof cacheSettingsSchema>;
303
+ export type LoggingSettingsConfig = z.infer<typeof loggingSettingsSchema>;
287
304
  export type FeedbackGithubSettingsConfig = z.infer<typeof feedbackGithubSettingsSchema>;
288
305
  export type FeedbackSettingsConfig = z.infer<typeof feedbackSettingsSchema>;
289
306
  export type ConfigInfo = z.infer<typeof configInfoSchema>;
@@ -421,6 +438,16 @@ export const DEFAULT_SERVER_CONFIG: Partial<ConfigInfo> = {
421
438
  verboseLogging: false,
422
439
  },
423
440
 
441
+ // Logging settings defaults — variables logging is always off unless the operator
442
+ // sets MJ_LOG_GRAPHQL_VARIABLES=true (or sets logVariables in mj.config.cjs).
443
+ // NOTE: this only governs the opt-in verbose-echo middleware. The always-on request
444
+ // log in context.ts already strips variables unconditionally.
445
+ loggingSettings: {
446
+ graphql: {
447
+ logVariables: parseBooleanEnv(process.env.MJ_LOG_GRAPHQL_VARIABLES),
448
+ },
449
+ },
450
+
424
451
  // Auth providers (environment-driven)
425
452
  authProviders: [
426
453
  // Microsoft Azure AD / Entra ID
package/src/context.ts CHANGED
@@ -9,6 +9,7 @@ import { getSigningKeys, getSystemUser, getValidationOptions, verifyUserRecord,
9
9
  import { TokenExpiredError, AuthProviderFactory } from '@memberjunction/auth-providers';
10
10
  import { authCache } from './cache.js';
11
11
  import { userEmailMap, apiKey, mj_core_schema } from './config.js';
12
+ import { buildBoundaryLogPayload } from './logging/boundaryLogPayload.js';
12
13
  import { DataSourceInfo, UserPayload } from './types.js';
13
14
  import { GetReadOnlyDataSource, GetReadWriteDataSource } from './util.js';
14
15
  import { v4 as uuidv4 } from 'uuid';
@@ -21,24 +22,6 @@ import { UUIDsEqual } from '@memberjunction/global';
21
22
  import { resolveDbPlatformFromEnv } from '@memberjunction/generic-database-provider';
22
23
  import { GetAPIKeyEngine } from '@memberjunction/api-keys';
23
24
 
24
- /**
25
- * Renders a value for one-line console logging without Node's `[Object]` truncation.
26
- * Arrays keep their structure; non-array objects collapse to JSON, truncated at `maxLen`.
27
- * Objects whose JSON exceeds `maxLen` and contain nested structure are recursed into so
28
- * outer keys remain visible.
29
- */
30
- function shortenForLog(value: unknown, maxLen = 300): unknown {
31
- if (value === null || typeof value !== 'object') return value;
32
- if (Array.isArray(value)) return value.map((v) => shortenForLog(v, maxLen));
33
- const json = JSON.stringify(value);
34
- if (json.length <= maxLen) return json;
35
- const result: Record<string, unknown> = {};
36
- for (const [k, v] of Object.entries(value as Record<string, unknown>)) {
37
- result[k] = shortenForLog(v, maxLen);
38
- }
39
- return result;
40
- }
41
-
42
25
  const verifyAsync = async (issuer: string, token: string): Promise<jwt.JwtPayload> =>
43
26
  new Promise((resolve, reject) => {
44
27
  const options = getValidationOptions(issuer);
@@ -342,7 +325,7 @@ export const contextFunction =
342
325
  const reqAny = req as any;
343
326
  const operationName: string | undefined = reqAny.body?.operationName;
344
327
  if (operationName !== 'IntrospectionQuery') {
345
- console.dir({ operationName, variables: shortenForLog(reqAny.body?.variables) }, { depth: null, breakLength: 200 });
328
+ console.dir(buildBoundaryLogPayload(operationName), { depth: null, breakLength: 200 });
346
329
  }
347
330
 
348
331
  // Auth already happened in the unified auth middleware — just read the result
@@ -7593,6 +7593,12 @@ each time the agent processes a prompt step.`})
7593
7593
  @MaxLength(36)
7594
7594
  CompanyID?: string;
7595
7595
 
7596
+ @Field(() => Int, {nullable: true, description: `Total input tokens served from the AI provider's prompt cache (cache reads / hits) across this agent run, summed from child prompt runs' TokensCacheReadRollup and sub-agent runs' TotalCacheReadTokensUsed. Counts only; the cost impact (cache reads are billed at a steep discount) is reflected in TotalCost. The cache counterpart of TotalPromptTokensUsed.`})
7597
+ TotalCacheReadTokensUsed?: number;
7598
+
7599
+ @Field(() => Int, {nullable: true, description: `Total input tokens written to the AI provider's prompt cache (cache writes / creation) across this agent run, summed from child prompt runs' TokensCacheWriteRollup and sub-agent runs' TotalCacheWriteTokensUsed. Populated for providers that bill cache creation (e.g. Anthropic); 0 or NULL otherwise. The cache counterpart of TotalCompletionTokensUsed.`})
7600
+ TotalCacheWriteTokensUsed?: number;
7601
+
7596
7602
  @Field({nullable: true})
7597
7603
  @MaxLength(255)
7598
7604
  Agent?: string;
@@ -7814,6 +7820,12 @@ export class CreateMJAIAgentRunInput {
7814
7820
  @Field({ nullable: true })
7815
7821
  CompanyID: string | null;
7816
7822
 
7823
+ @Field(() => Int, { nullable: true })
7824
+ TotalCacheReadTokensUsed: number | null;
7825
+
7826
+ @Field(() => Int, { nullable: true })
7827
+ TotalCacheWriteTokensUsed: number | null;
7828
+
7817
7829
  @Field(() => RestoreContextInput, { nullable: true })
7818
7830
  RestoreContext___?: RestoreContextInput;
7819
7831
  }
@@ -7956,6 +7968,12 @@ export class UpdateMJAIAgentRunInput {
7956
7968
  @Field({ nullable: true })
7957
7969
  CompanyID?: string | null;
7958
7970
 
7971
+ @Field(() => Int, { nullable: true })
7972
+ TotalCacheReadTokensUsed?: number | null;
7973
+
7974
+ @Field(() => Int, { nullable: true })
7975
+ TotalCacheWriteTokensUsed?: number | null;
7976
+
7959
7977
  @Field(() => [KeyValuePairInput], { nullable: true })
7960
7978
  OldValues___?: KeyValuePairInput[];
7961
7979
 
@@ -12243,6 +12261,12 @@ export class MJAIModelCost_ {
12243
12261
  @Field()
12244
12262
  _mj__UpdatedAt: Date;
12245
12263
 
12264
+ @Field(() => Float, {nullable: true, description: `Optional price per unit for input tokens served from the AI provider's prompt cache (cache reads / hits), expressed in the same currency and UnitType (e.g. per 1M tokens) as InputPricePerUnit. When NULL, cache-read tokens are priced at InputPricePerUnit. Cache reads are usually far cheaper than uncached input (e.g. ~0.1x for Anthropic/Gemini, ~0.5x for OpenAI).`})
12265
+ CacheReadPricePerUnit?: number;
12266
+
12267
+ @Field(() => Float, {nullable: true, description: `Optional price per unit for input tokens written to the AI provider's prompt cache (cache writes / creation), expressed in the same currency and UnitType as InputPricePerUnit. When NULL, cache-write tokens are priced at InputPricePerUnit. Populated for providers that bill cache creation separately (e.g. Anthropic, ~1.25x input); leave NULL for providers that do not (OpenAI, Gemini), which also report 0 cache-write tokens.`})
12268
+ CacheWritePricePerUnit?: number;
12269
+
12246
12270
  @Field()
12247
12271
  @MaxLength(50)
12248
12272
  Model: string;
@@ -12305,6 +12329,12 @@ export class CreateMJAIModelCostInput {
12305
12329
  @Field({ nullable: true })
12306
12330
  Comments: string | null;
12307
12331
 
12332
+ @Field(() => Float, { nullable: true })
12333
+ CacheReadPricePerUnit: number | null;
12334
+
12335
+ @Field(() => Float, { nullable: true })
12336
+ CacheWritePricePerUnit: number | null;
12337
+
12308
12338
  @Field(() => RestoreContextInput, { nullable: true })
12309
12339
  RestoreContext___?: RestoreContextInput;
12310
12340
  }
@@ -12354,6 +12384,12 @@ export class UpdateMJAIModelCostInput {
12354
12384
  @Field({ nullable: true })
12355
12385
  Comments?: string | null;
12356
12386
 
12387
+ @Field(() => Float, { nullable: true })
12388
+ CacheReadPricePerUnit?: number | null;
12389
+
12390
+ @Field(() => Float, { nullable: true })
12391
+ CacheWritePricePerUnit?: number | null;
12392
+
12357
12393
  @Field(() => [KeyValuePairInput], { nullable: true })
12358
12394
  OldValues___?: KeyValuePairInput[];
12359
12395
 
@@ -15236,6 +15272,18 @@ export class MJAIPromptRun_ {
15236
15272
  @Field({nullable: true, description: `The assistant prefill text that was used during this prompt execution. Records whether native prefill or fallback was applied. NULL means no prefill was used.`})
15237
15273
  AssistantPrefill?: string;
15238
15274
 
15275
+ @Field(() => Int, {nullable: true, description: `Number of input tokens served from the AI provider's prompt cache (a cache READ / hit) for this run, as reported by the provider. Counts only; no cost is derived here. NULL if the provider did not report cache reads or caching did not engage. Distinct from CacheHit/CacheKey, which track MemberJunction's own result cache.`})
15276
+ TokensCacheRead?: number;
15277
+
15278
+ @Field(() => Int, {nullable: true, description: `Number of input tokens written to the AI provider's prompt cache (a cache WRITE / creation) for this run, as reported by the provider. Populated for providers that report cache writes (e.g. Anthropic cache_creation_input_tokens); NULL or 0 for providers that do not bill/report writes (OpenAI, Gemini, Groq, Cerebras). Counts only; no cost is derived here.`})
15279
+ TokensCacheWrite?: number;
15280
+
15281
+ @Field(() => Int, {nullable: true, description: `Rollup of TokensCacheRead across this prompt run and all of its descendant prompt runs (e.g. the individual attempts behind a parallel / multi-attempt / failover consolidation). For a leaf run this equals TokensCacheRead. Use this (not TokensCacheRead) when aggregating cache reads up a prompt-run or agent-run hierarchy so fan-out provider calls are not under-counted.`})
15282
+ TokensCacheReadRollup?: number;
15283
+
15284
+ @Field(() => Int, {nullable: true, description: `Rollup of TokensCacheWrite across this prompt run and all of its descendant prompt runs. For a leaf run this equals TokensCacheWrite. Mirrors TokensUsedRollup/TokensPromptRollup; populated for providers that report cache writes (e.g. Anthropic), otherwise 0 or NULL.`})
15285
+ TokensCacheWriteRollup?: number;
15286
+
15239
15287
  @Field()
15240
15288
  @MaxLength(255)
15241
15289
  Prompt: string;
@@ -15563,6 +15611,18 @@ export class CreateMJAIPromptRunInput {
15563
15611
  @Field({ nullable: true })
15564
15612
  AssistantPrefill: string | null;
15565
15613
 
15614
+ @Field(() => Int, { nullable: true })
15615
+ TokensCacheRead: number | null;
15616
+
15617
+ @Field(() => Int, { nullable: true })
15618
+ TokensCacheWrite: number | null;
15619
+
15620
+ @Field(() => Int, { nullable: true })
15621
+ TokensCacheReadRollup: number | null;
15622
+
15623
+ @Field(() => Int, { nullable: true })
15624
+ TokensCacheWriteRollup: number | null;
15625
+
15566
15626
  @Field(() => RestoreContextInput, { nullable: true })
15567
15627
  RestoreContext___?: RestoreContextInput;
15568
15628
  }
@@ -15822,6 +15882,18 @@ export class UpdateMJAIPromptRunInput {
15822
15882
  @Field({ nullable: true })
15823
15883
  AssistantPrefill?: string | null;
15824
15884
 
15885
+ @Field(() => Int, { nullable: true })
15886
+ TokensCacheRead?: number | null;
15887
+
15888
+ @Field(() => Int, { nullable: true })
15889
+ TokensCacheWrite?: number | null;
15890
+
15891
+ @Field(() => Int, { nullable: true })
15892
+ TokensCacheReadRollup?: number | null;
15893
+
15894
+ @Field(() => Int, { nullable: true })
15895
+ TokensCacheWriteRollup?: number | null;
15896
+
15825
15897
  @Field(() => [KeyValuePairInput], { nullable: true })
15826
15898
  OldValues___?: KeyValuePairInput[];
15827
15899
 
@@ -44228,7 +44300,7 @@ export class MJEntityField_ {
44228
44300
  @MaxLength(20)
44229
44301
  ValueListType: string;
44230
44302
 
44231
- @Field({nullable: true, description: `Defines extended behaviors for a field such as for Email, Web URLs, Code, etc.`})
44303
+ @Field({nullable: true, description: `Defines extended behaviors for a field such as Email, Web URLs, Code, Markdown, HTML, and Icon. When set to 'Icon', the field's values are treated as icon CSS classes (e.g. Font Awesome) for per-row display in the UI.`})
44232
44304
  @MaxLength(50)
44233
44305
  ExtendedType?: string;
44234
44306
 
@@ -49627,6 +49699,10 @@ export class MJIntegrationObjectField_ {
49627
49699
  @Field(() => Boolean, {description: `When true, this field was dynamically discovered by IntrospectSchema and is not defined in static connector metadata.`})
49628
49700
  IsCustom: boolean;
49629
49701
 
49702
+ @Field({description: `Provenance of this IntegrationObjectField row: Declared (from static research/docs), Discovered (from runtime API introspection), Custom (customer-defined custom field, e.g., HubSpot custom property on standard object). Drives merge precedence — discovered/runtime wins for type/constraints; declared wins for description/label/sequence/category.`})
49703
+ @MaxLength(20)
49704
+ MetadataSource: string;
49705
+
49630
49706
  @Field()
49631
49707
  @MaxLength(255)
49632
49708
  IntegrationObject: string;
@@ -49708,6 +49784,9 @@ export class CreateMJIntegrationObjectFieldInput {
49708
49784
  @Field(() => Boolean, { nullable: true })
49709
49785
  IsCustom?: boolean;
49710
49786
 
49787
+ @Field({ nullable: true })
49788
+ MetadataSource?: string;
49789
+
49711
49790
  @Field(() => RestoreContextInput, { nullable: true })
49712
49791
  RestoreContext___?: RestoreContextInput;
49713
49792
  }
@@ -49784,6 +49863,9 @@ export class UpdateMJIntegrationObjectFieldInput {
49784
49863
  @Field(() => Boolean, { nullable: true })
49785
49864
  IsCustom?: boolean;
49786
49865
 
49866
+ @Field({ nullable: true })
49867
+ MetadataSource?: string;
49868
+
49787
49869
  @Field(() => [KeyValuePairInput], { nullable: true })
49788
49870
  OldValues___?: KeyValuePairInput[];
49789
49871
 
@@ -49963,6 +50045,59 @@ export class MJIntegrationObject_ {
49963
50045
  @Field(() => Boolean, {description: `When true, this object was dynamically discovered by IntrospectSchema and is not defined in static connector metadata.`})
49964
50046
  IsCustom: boolean;
49965
50047
 
50048
+ @Field({nullable: true, description: `HTTP path template for create operations. Generic CRUD in BaseRESTIntegrationConnector substitutes parent IDs into {var} placeholders. NULL means create not supported via metadata-driven path.`})
50049
+ CreateAPIPath?: string;
50050
+
50051
+ @Field({nullable: true, description: `HTTP method for create (typically POST). NULL means create not supported via metadata-driven path.`})
50052
+ @MaxLength(20)
50053
+ CreateMethod?: string;
50054
+
50055
+ @Field({nullable: true, description: `Request body shape for create: flat (top-level fields), wrapped (under CreateBodyKey), or literal (connector overrides CreateRecord and supplies own body).`})
50056
+ @MaxLength(50)
50057
+ CreateBodyShape?: string;
50058
+
50059
+ @Field({nullable: true, description: `Wrapper key for create body when CreateBodyShape=wrapped. Example: 'member' for YourMembership which wraps body as {member:{...}}.`})
50060
+ @MaxLength(100)
50061
+ CreateBodyKey?: string;
50062
+
50063
+ @Field({nullable: true, description: `Where the created record ID is found in the create response: path (URL of returned Location header), body (parsed from JSON response), header (specific named header).`})
50064
+ @MaxLength(20)
50065
+ CreateIDLocation?: string;
50066
+
50067
+ @Field({nullable: true, description: `HTTP path template for update operations. Typically contains {ID} placeholder substituted with the record ExternalID at runtime.`})
50068
+ UpdateAPIPath?: string;
50069
+
50070
+ @Field({nullable: true, description: `HTTP method for update (typically PATCH or PUT).`})
50071
+ @MaxLength(20)
50072
+ UpdateMethod?: string;
50073
+
50074
+ @Field({nullable: true, description: `Request body shape for update: flat | wrapped | literal. See CreateBodyShape.`})
50075
+ @MaxLength(50)
50076
+ UpdateBodyShape?: string;
50077
+
50078
+ @Field({nullable: true, description: `Wrapper key for update body when UpdateBodyShape=wrapped.`})
50079
+ @MaxLength(100)
50080
+ UpdateBodyKey?: string;
50081
+
50082
+ @Field({nullable: true, description: `For update: where the target record ID is located in the request — typically 'path' (substituted into UpdateAPIPath URL template).`})
50083
+ @MaxLength(20)
50084
+ UpdateIDLocation?: string;
50085
+
50086
+ @Field({nullable: true, description: `HTTP path template for delete operations. Typically contains {ID} placeholder. NULL means delete not supported via metadata-driven path. (Existing DeleteMethod column carries the verb.)`})
50087
+ DeleteAPIPath?: string;
50088
+
50089
+ @Field({nullable: true, description: `For delete: where the target record ID is located — typically 'path'.`})
50090
+ @MaxLength(20)
50091
+ DeleteIDLocation?: string;
50092
+
50093
+ @Field({nullable: true, description: `Vendor field name marking "last changed" — drives incremental sync filter when SupportsIncrementalSync=1. The exact filter syntax (e.g., $filter=Modified gt {value} or modified_since={value}) lives in Configuration.incrementalFilterFormat. Provable-only: leave NULL if docs do not name a watermark field.`})
50094
+ @MaxLength(255)
50095
+ IncrementalWatermarkField?: string;
50096
+
50097
+ @Field({description: `Provenance of this IntegrationObject row: Declared (from static research/docs), Discovered (from runtime API introspection like Salesforce /describe), Custom (genuinely customer-created, e.g., HubSpot custom objects). Drives merge precedence in IntegrationSchemaSync.`})
50098
+ @MaxLength(20)
50099
+ MetadataSource: string;
50100
+
49966
50101
  @Field()
49967
50102
  @MaxLength(100)
49968
50103
  Integration: string;
@@ -50043,6 +50178,48 @@ export class CreateMJIntegrationObjectInput {
50043
50178
  @Field(() => Boolean, { nullable: true })
50044
50179
  IsCustom?: boolean;
50045
50180
 
50181
+ @Field({ nullable: true })
50182
+ CreateAPIPath: string | null;
50183
+
50184
+ @Field({ nullable: true })
50185
+ CreateMethod: string | null;
50186
+
50187
+ @Field({ nullable: true })
50188
+ CreateBodyShape: string | null;
50189
+
50190
+ @Field({ nullable: true })
50191
+ CreateBodyKey: string | null;
50192
+
50193
+ @Field({ nullable: true })
50194
+ CreateIDLocation: string | null;
50195
+
50196
+ @Field({ nullable: true })
50197
+ UpdateAPIPath: string | null;
50198
+
50199
+ @Field({ nullable: true })
50200
+ UpdateMethod: string | null;
50201
+
50202
+ @Field({ nullable: true })
50203
+ UpdateBodyShape: string | null;
50204
+
50205
+ @Field({ nullable: true })
50206
+ UpdateBodyKey: string | null;
50207
+
50208
+ @Field({ nullable: true })
50209
+ UpdateIDLocation: string | null;
50210
+
50211
+ @Field({ nullable: true })
50212
+ DeleteAPIPath: string | null;
50213
+
50214
+ @Field({ nullable: true })
50215
+ DeleteIDLocation: string | null;
50216
+
50217
+ @Field({ nullable: true })
50218
+ IncrementalWatermarkField: string | null;
50219
+
50220
+ @Field({ nullable: true })
50221
+ MetadataSource?: string;
50222
+
50046
50223
  @Field(() => RestoreContextInput, { nullable: true })
50047
50224
  RestoreContext___?: RestoreContextInput;
50048
50225
  }
@@ -50116,6 +50293,48 @@ export class UpdateMJIntegrationObjectInput {
50116
50293
  @Field(() => Boolean, { nullable: true })
50117
50294
  IsCustom?: boolean;
50118
50295
 
50296
+ @Field({ nullable: true })
50297
+ CreateAPIPath?: string | null;
50298
+
50299
+ @Field({ nullable: true })
50300
+ CreateMethod?: string | null;
50301
+
50302
+ @Field({ nullable: true })
50303
+ CreateBodyShape?: string | null;
50304
+
50305
+ @Field({ nullable: true })
50306
+ CreateBodyKey?: string | null;
50307
+
50308
+ @Field({ nullable: true })
50309
+ CreateIDLocation?: string | null;
50310
+
50311
+ @Field({ nullable: true })
50312
+ UpdateAPIPath?: string | null;
50313
+
50314
+ @Field({ nullable: true })
50315
+ UpdateMethod?: string | null;
50316
+
50317
+ @Field({ nullable: true })
50318
+ UpdateBodyShape?: string | null;
50319
+
50320
+ @Field({ nullable: true })
50321
+ UpdateBodyKey?: string | null;
50322
+
50323
+ @Field({ nullable: true })
50324
+ UpdateIDLocation?: string | null;
50325
+
50326
+ @Field({ nullable: true })
50327
+ DeleteAPIPath?: string | null;
50328
+
50329
+ @Field({ nullable: true })
50330
+ DeleteIDLocation?: string | null;
50331
+
50332
+ @Field({ nullable: true })
50333
+ IncrementalWatermarkField?: string | null;
50334
+
50335
+ @Field({ nullable: true })
50336
+ MetadataSource?: string;
50337
+
50119
50338
  @Field(() => [KeyValuePairInput], { nullable: true })
50120
50339
  OldValues___?: KeyValuePairInput[];
50121
50340
 
@@ -65639,6 +65858,9 @@ export class MJScheduledJob_ {
65639
65858
  @Field()
65640
65859
  _mj__UpdatedAt: Date;
65641
65860
 
65861
+ @Field(() => Boolean, {description: `When true AND LastRunAt IS NULL, the scheduler sets NextRunAt to now() instead of the next cron tick on initialization, so the job runs on the next polling cycle. Useful for newly-seeded jobs that should not wait up to a full cron interval before their first execution.`})
65862
+ RunImmediatelyIfNeverRun: boolean;
65863
+
65642
65864
  @Field()
65643
65865
  @MaxLength(100)
65644
65866
  JobType: string;
@@ -65742,6 +65964,9 @@ export class CreateMJScheduledJobInput {
65742
65964
  @Field({ nullable: true })
65743
65965
  ConcurrencyMode?: string;
65744
65966
 
65967
+ @Field(() => Boolean, { nullable: true })
65968
+ RunImmediatelyIfNeverRun?: boolean;
65969
+
65745
65970
  @Field(() => RestoreContextInput, { nullable: true })
65746
65971
  RestoreContext___?: RestoreContextInput;
65747
65972
  }
@@ -65830,6 +66055,9 @@ export class UpdateMJScheduledJobInput {
65830
66055
  @Field({ nullable: true })
65831
66056
  ConcurrencyMode?: string;
65832
66057
 
66058
+ @Field(() => Boolean, { nullable: true })
66059
+ RunImmediatelyIfNeverRun?: boolean;
66060
+
65833
66061
  @Field(() => [KeyValuePairInput], { nullable: true })
65834
66062
  OldValues___?: KeyValuePairInput[];
65835
66063
 
@@ -69621,6 +69849,10 @@ export class MJTagSynonym_ {
69621
69849
  @Field()
69622
69850
  _mj__UpdatedAt: Date;
69623
69851
 
69852
+ @Field({description: `Approval state of the synonym. Active = resolves to its tag during classification. Pending = proposed (e.g. by the LLM or a bulk import) and awaiting human review; does not resolve until approved. Rejected = reviewed and declined; retained for audit and to suppress re-proposal.`})
69853
+ @MaxLength(20)
69854
+ Status: string;
69855
+
69624
69856
  @Field()
69625
69857
  @MaxLength(255)
69626
69858
  Tag: string;
@@ -69644,6 +69876,9 @@ export class CreateMJTagSynonymInput {
69644
69876
  @Field({ nullable: true })
69645
69877
  Source?: string;
69646
69878
 
69879
+ @Field({ nullable: true })
69880
+ Status?: string;
69881
+
69647
69882
  @Field(() => RestoreContextInput, { nullable: true })
69648
69883
  RestoreContext___?: RestoreContextInput;
69649
69884
  }
@@ -69666,6 +69901,9 @@ export class UpdateMJTagSynonymInput {
69666
69901
  @Field({ nullable: true })
69667
69902
  Source?: string;
69668
69903
 
69904
+ @Field({ nullable: true })
69905
+ Status?: string;
69906
+
69669
69907
  @Field(() => [KeyValuePairInput], { nullable: true })
69670
69908
  OldValues___?: KeyValuePairInput[];
69671
69909
 
@@ -0,0 +1,85 @@
1
+ import { PubSubEngine } from 'type-graphql';
2
+ import { LogError } from '@memberjunction/core';
3
+ import { PUSH_STATUS_UPDATES_TOPIC } from './PushStatusResolver.js';
4
+
5
+ /** Default cadence for fire-and-forget liveness pulses (5 minutes). */
6
+ export const DEFAULT_PULSE_INTERVAL_MS = 5 * 60 * 1000;
7
+
8
+ /** The `type` discriminator carried by liveness pulse messages. */
9
+ export const HEARTBEAT_MESSAGE_TYPE = 'Heartbeat';
10
+
11
+ /**
12
+ * Lightweight status snapshot included in an enriched pulse so the client can
13
+ * correlate the pulse with a run record and surface a coarse "still running"
14
+ * signal. Read from the in-memory run entity — never a DB query.
15
+ */
16
+ export interface PulseStatus {
17
+ /** Primary key of the persisted run record (AIAgentRun, TestRun, TestSuiteRun). */
18
+ runId?: string;
19
+ /** Current run status, e.g. 'Running'. */
20
+ status?: string;
21
+ /** Optional human-readable current step for UI display. */
22
+ currentStep?: string;
23
+ }
24
+
25
+ /** Handle returned by {@link startLivenessPulse}; call `stop()` when the work settles. */
26
+ export interface LivenessPulseHandle {
27
+ stop(): void;
28
+ }
29
+
30
+ export interface LivenessPulseOptions {
31
+ /** PubSub engine used to publish on the shared push-status topic. */
32
+ pubSub: PubSubEngine;
33
+ /** Session the client is subscribed on (used by the subscription filter). */
34
+ sessionId: string;
35
+ /** Resolver label echoed in the message envelope (e.g. 'RunAIAgentResolver'). */
36
+ resolver: string;
37
+ /** Pulse cadence in ms. Defaults to {@link DEFAULT_PULSE_INTERVAL_MS}. */
38
+ intervalMs?: number;
39
+ /**
40
+ * Optional cheap status reader invoked on each tick. Should read from an
41
+ * in-memory ref, not the database. Errors are swallowed so a transient read
42
+ * never kills the pulse loop.
43
+ */
44
+ readStatus?: () => PulseStatus | undefined;
45
+ }
46
+
47
+ /**
48
+ * Publishes a periodic liveness pulse on {@link PUSH_STATUS_UPDATES_TOPIC} while a
49
+ * fire-and-forget background operation runs. Each pulse resets the client's idle
50
+ * timer, so a long-but-active operation never spuriously times out. The pulse
51
+ * stops when the caller invokes `stop()` (typically in the background promise's
52
+ * `finally` block).
53
+ *
54
+ * The envelope matches the resolvers' existing push-status messages
55
+ * (`{ message: JSON.stringify({ resolver, type, status, data }), sessionId }`),
56
+ * so the client receives it through the same subscription with no special parsing.
57
+ */
58
+ export function startLivenessPulse(options: LivenessPulseOptions): LivenessPulseHandle {
59
+ const { pubSub, sessionId, resolver, readStatus } = options;
60
+ const intervalMs = options.intervalMs ?? DEFAULT_PULSE_INTERVAL_MS;
61
+
62
+ const timer = setInterval(() => {
63
+ let data: PulseStatus | undefined;
64
+ try {
65
+ data = readStatus?.();
66
+ } catch (e) {
67
+ // A status read failure must not break the liveness loop.
68
+ LogError(`[LivenessPulse:${resolver}] readStatus failed: ${(e as Error).message}`);
69
+ }
70
+
71
+ pubSub.publish(PUSH_STATUS_UPDATES_TOPIC, {
72
+ message: JSON.stringify({
73
+ resolver,
74
+ type: HEARTBEAT_MESSAGE_TYPE,
75
+ status: 'ok',
76
+ data: data ?? {},
77
+ }),
78
+ sessionId,
79
+ });
80
+ }, intervalMs);
81
+
82
+ return {
83
+ stop: () => clearInterval(timer),
84
+ };
85
+ }