@memberjunction/server 5.37.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 (121) hide show
  1. package/README.md +79 -12
  2. package/dist/agents/skip-sdk.d.ts.map +1 -1
  3. package/dist/agents/skip-sdk.js +16 -12
  4. package/dist/agents/skip-sdk.js.map +1 -1
  5. package/dist/apolloServer/index.d.ts +0 -8
  6. package/dist/apolloServer/index.d.ts.map +1 -1
  7. package/dist/apolloServer/index.js +61 -0
  8. package/dist/apolloServer/index.js.map +1 -1
  9. package/dist/config.d.ts +68 -0
  10. package/dist/config.d.ts.map +1 -1
  11. package/dist/config.js +24 -0
  12. package/dist/config.js.map +1 -1
  13. package/dist/context.d.ts.map +1 -1
  14. package/dist/context.js +2 -21
  15. package/dist/context.js.map +1 -1
  16. package/dist/generated/generated.d.ts +172 -5
  17. package/dist/generated/generated.d.ts.map +1 -1
  18. package/dist/generated/generated.js +867 -14
  19. package/dist/generated/generated.js.map +1 -1
  20. package/dist/generic/FireAndForgetHeartbeat.d.ts +51 -0
  21. package/dist/generic/FireAndForgetHeartbeat.d.ts.map +1 -0
  22. package/dist/generic/FireAndForgetHeartbeat.js +44 -0
  23. package/dist/generic/FireAndForgetHeartbeat.js.map +1 -0
  24. package/dist/index.d.ts +3 -0
  25. package/dist/index.d.ts.map +1 -1
  26. package/dist/index.js +177 -16
  27. package/dist/index.js.map +1 -1
  28. package/dist/logging/NoLog.d.ts +50 -0
  29. package/dist/logging/NoLog.d.ts.map +1 -0
  30. package/dist/logging/NoLog.js +80 -0
  31. package/dist/logging/NoLog.js.map +1 -0
  32. package/dist/logging/bootAudit.d.ts +43 -0
  33. package/dist/logging/bootAudit.d.ts.map +1 -0
  34. package/dist/logging/bootAudit.js +83 -0
  35. package/dist/logging/bootAudit.js.map +1 -0
  36. package/dist/logging/boundaryLogPayload.d.ts +18 -0
  37. package/dist/logging/boundaryLogPayload.d.ts.map +1 -0
  38. package/dist/logging/boundaryLogPayload.js +18 -0
  39. package/dist/logging/boundaryLogPayload.js.map +1 -0
  40. package/dist/logging/secretRedactor.d.ts +23 -0
  41. package/dist/logging/secretRedactor.d.ts.map +1 -0
  42. package/dist/logging/secretRedactor.js +53 -0
  43. package/dist/logging/secretRedactor.js.map +1 -0
  44. package/dist/logging/shortenForLog.d.ts +8 -0
  45. package/dist/logging/shortenForLog.d.ts.map +1 -0
  46. package/dist/logging/shortenForLog.js +21 -0
  47. package/dist/logging/shortenForLog.js.map +1 -0
  48. package/dist/logging/variablesLoggingMiddleware.d.ts +22 -0
  49. package/dist/logging/variablesLoggingMiddleware.d.ts.map +1 -0
  50. package/dist/logging/variablesLoggingMiddleware.js +127 -0
  51. package/dist/logging/variablesLoggingMiddleware.js.map +1 -0
  52. package/dist/resolvers/GetDataResolver.d.ts.map +1 -1
  53. package/dist/resolvers/GetDataResolver.js +8 -4
  54. package/dist/resolvers/GetDataResolver.js.map +1 -1
  55. package/dist/resolvers/IntegrationDiscoveryResolver.d.ts +259 -2
  56. package/dist/resolvers/IntegrationDiscoveryResolver.d.ts.map +1 -1
  57. package/dist/resolvers/IntegrationDiscoveryResolver.js +1276 -117
  58. package/dist/resolvers/IntegrationDiscoveryResolver.js.map +1 -1
  59. package/dist/resolvers/IntegrationProgressResolver.d.ts +90 -0
  60. package/dist/resolvers/IntegrationProgressResolver.d.ts.map +1 -0
  61. package/dist/resolvers/IntegrationProgressResolver.js +196 -0
  62. package/dist/resolvers/IntegrationProgressResolver.js.map +1 -0
  63. package/dist/resolvers/MCPResolver.d.ts.map +1 -1
  64. package/dist/resolvers/MCPResolver.js +19 -14
  65. package/dist/resolvers/MCPResolver.js.map +1 -1
  66. package/dist/resolvers/QueryResolver.d.ts.map +1 -1
  67. package/dist/resolvers/QueryResolver.js +5 -4
  68. package/dist/resolvers/QueryResolver.js.map +1 -1
  69. package/dist/resolvers/QuerySystemUserResolver.d.ts.map +1 -1
  70. package/dist/resolvers/QuerySystemUserResolver.js +6 -18
  71. package/dist/resolvers/QuerySystemUserResolver.js.map +1 -1
  72. package/dist/resolvers/RunAIAgentResolver.d.ts.map +1 -1
  73. package/dist/resolvers/RunAIAgentResolver.js +32 -64
  74. package/dist/resolvers/RunAIAgentResolver.js.map +1 -1
  75. package/dist/resolvers/RunTestResolver.d.ts.map +1 -1
  76. package/dist/resolvers/RunTestResolver.js +12 -2
  77. package/dist/resolvers/RunTestResolver.js.map +1 -1
  78. package/dist/resolvers/SearchEntitiesResolver.d.ts +46 -0
  79. package/dist/resolvers/SearchEntitiesResolver.d.ts.map +1 -0
  80. package/dist/resolvers/SearchEntitiesResolver.js +216 -0
  81. package/dist/resolvers/SearchEntitiesResolver.js.map +1 -0
  82. package/dist/resolvers/VectorizeEntityResolver.d.ts.map +1 -1
  83. package/dist/resolvers/VectorizeEntityResolver.js +14 -1
  84. package/dist/resolvers/VectorizeEntityResolver.js.map +1 -1
  85. package/dist/services/ScheduledJobsService.d.ts.map +1 -1
  86. package/dist/services/ScheduledJobsService.js +14 -2
  87. package/dist/services/ScheduledJobsService.js.map +1 -1
  88. package/package.json +75 -74
  89. package/src/__tests__/NoLog.test.ts +76 -0
  90. package/src/__tests__/bootAudit.test.ts +188 -0
  91. package/src/__tests__/boundaryLogPayload.test.ts +31 -0
  92. package/src/__tests__/getDataTokenRedaction.test.ts +84 -0
  93. package/src/__tests__/resolverBase.rls.test.ts +224 -0
  94. package/src/__tests__/secretRedactor.test.ts +163 -0
  95. package/src/__tests__/subscriptionRedaction.test.ts +217 -0
  96. package/src/agents/skip-sdk.ts +16 -13
  97. package/src/apolloServer/index.ts +58 -0
  98. package/src/config.ts +27 -0
  99. package/src/context.ts +2 -19
  100. package/src/generated/generated.ts +627 -15
  101. package/src/generic/FireAndForgetHeartbeat.ts +85 -0
  102. package/src/generic/__tests__/FireAndForgetHeartbeat.test.ts +99 -0
  103. package/src/index.ts +199 -19
  104. package/src/logging/NoLog.ts +88 -0
  105. package/src/logging/bootAudit.ts +117 -0
  106. package/src/logging/boundaryLogPayload.ts +19 -0
  107. package/src/logging/secretRedactor.ts +82 -0
  108. package/src/logging/shortenForLog.ts +17 -0
  109. package/src/logging/variablesLoggingMiddleware.ts +191 -0
  110. package/src/resolvers/GetDataResolver.ts +9 -5
  111. package/src/resolvers/IntegrationDiscoveryResolver.ts +1111 -120
  112. package/src/resolvers/IntegrationProgressResolver.ts +220 -0
  113. package/src/resolvers/MCPResolver.ts +18 -14
  114. package/src/resolvers/QueryResolver.ts +7 -6
  115. package/src/resolvers/QuerySystemUserResolver.ts +11 -25
  116. package/src/resolvers/RunAIAgentResolver.ts +34 -71
  117. package/src/resolvers/RunTestResolver.ts +14 -2
  118. package/src/resolvers/SearchEntitiesResolver.ts +173 -0
  119. package/src/resolvers/VectorizeEntityResolver.ts +14 -1
  120. package/src/resolvers/__tests__/IntegrationProgressResolver.test.ts +170 -0
  121. package/src/services/ScheduledJobsService.ts +15 -2
@@ -25,7 +25,7 @@ import {
25
25
  } from '@memberjunction/skip-types';
26
26
  import { DataContext } from '@memberjunction/data-context';
27
27
  import { IMetadataProvider, UserInfo, LogStatus, LogError, Metadata, RunQuery, RunView, EntityInfo, EntityFieldInfo, EntityFieldValueInfo, DatabaseProviderBase } from '@memberjunction/core';
28
- import { MJConversationDetailEntity } from '@memberjunction/core-entities';
28
+ import { MJConversationDetailEntity, QueryEngine } from '@memberjunction/core-entities';
29
29
  import { request as httpRequest } from 'http';
30
30
  import { request as httpsRequest } from 'https';
31
31
  import { gzip as gzipCompress, createGunzip } from 'zlib';
@@ -483,15 +483,15 @@ export class SkipSDK {
483
483
  * Build saved queries for Skip
484
484
  */
485
485
  private buildQueries(status: "Pending" | "In-Review" | "Approved" | "Rejected" | "Obsolete" = 'Approved'): SkipQueryInfo[] {
486
- const md = this.Provider;
487
- const approvedQueries = md.Queries.filter((q) => q.Status === status);
486
+ const qe = QueryEngine.Instance;
487
+ const approvedQueries = qe.Queries.filter((q) => q.Status === status);
488
488
 
489
489
  return approvedQueries.map((q) => ({
490
490
  ID: q.ID,
491
491
  Name: q.Name,
492
492
  Description: q.Description,
493
493
  Category: q.Category,
494
- CategoryPath: this.buildQueryCategoryPath(md, q.CategoryID),
494
+ CategoryPath: this.buildQueryCategoryPath(qe, q.CategoryID),
495
495
  CategoryID: q.CategoryID,
496
496
  SQL: q.SQL,
497
497
  Status: q.Status,
@@ -501,7 +501,7 @@ export class SkipSDK {
501
501
  EmbeddingModelID: q.EmbeddingModelID,
502
502
  EmbeddingModelName: q.EmbeddingModel,
503
503
  TechnicalDescription: q.TechnicalDescription,
504
- Fields: q.Fields.map((f) => ({
504
+ Fields: qe.GetQueryFields(q.ID).map((f) => ({
505
505
  ID: f.ID,
506
506
  QueryID: f.QueryID,
507
507
  Name: f.Name,
@@ -517,7 +517,7 @@ export class SkipSDK {
517
517
  IsSummary: f.IsSummary,
518
518
  SummaryDescription: f.SummaryDescription
519
519
  })),
520
- Parameters: q.Parameters.map((p) => ({
520
+ Parameters: qe.GetQueryParameters(q.ID).map((p) => ({
521
521
  ID: p.ID,
522
522
  QueryID: p.QueryID,
523
523
  Name: p.Name,
@@ -528,12 +528,15 @@ export class SkipSDK {
528
528
  SampleValue: p.SampleValue,
529
529
  ValidationFilters: p.ValidationFilters
530
530
  })),
531
- Entities: q.Entities.map((e) => ({
531
+ Entities: qe.QueryEntities.filter(e => UUIDsEqual(e.QueryID, q.ID)).map((e) => ({
532
532
  ID: e.ID,
533
533
  QueryID: e.QueryID,
534
534
  EntityID: e.EntityID,
535
535
  Entity: e.Entity
536
536
  })),
537
+ SQLDialectID: q.SQLDialectID,
538
+ UsesTemplate: q.UsesTemplate,
539
+ IsApproved: q.IsApproved,
537
540
  CacheEnabled: q.CacheEnabled,
538
541
  CacheMaxSize: q.CacheMaxSize,
539
542
  CacheTTLMinutes: q.CacheTTLMinutes,
@@ -544,11 +547,11 @@ export class SkipSDK {
544
547
  /**
545
548
  * Recursively build category path for a query
546
549
  */
547
- private buildQueryCategoryPath(md: IMetadataProvider, categoryID: string): string {
548
- const cat = md.QueryCategories.find((c) => UUIDsEqual(c.ID, categoryID));
550
+ private buildQueryCategoryPath(qe: QueryEngine, categoryID: string): string {
551
+ const cat = qe.Categories.find((c) => UUIDsEqual(c.ID, categoryID));
549
552
  if (!cat) return '';
550
553
  if (!cat.ParentID) return cat.Name;
551
- const parentPath = this.buildQueryCategoryPath(md, cat.ParentID);
554
+ const parentPath = this.buildQueryCategoryPath(qe, cat.ParentID);
552
555
  return parentPath ? `${parentPath}/${cat.Name}` : cat.Name;
553
556
  }
554
557
 
@@ -560,11 +563,11 @@ export class SkipSDK {
560
563
  * across all queries, not just approved ones.
561
564
  */
562
565
  private buildQueryCatalog(): SkipQueryCatalogEntry[] {
563
- const md = this.Provider;
566
+ const qe = QueryEngine.Instance;
564
567
 
565
- return md.Queries.map((q) => ({
568
+ return qe.Queries.map((q) => ({
566
569
  Name: q.Name,
567
- CategoryPath: this.buildQueryCategoryPath(md, q.CategoryID)
570
+ CategoryPath: this.buildQueryCategoryPath(qe, q.CategoryID)
568
571
  }));
569
572
  }
570
573
 
@@ -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