@memberjunction/server 2.32.2 → 2.33.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/generic/ResolverBase.d.ts.map +1 -1
- package/dist/generic/ResolverBase.js +78 -66
- package/dist/generic/ResolverBase.js.map +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +17 -0
- package/dist/index.js.map +1 -1
- package/dist/resolvers/ActionResolver.d.ts +49 -0
- package/dist/resolvers/ActionResolver.d.ts.map +1 -0
- package/dist/resolvers/ActionResolver.js +359 -0
- package/dist/resolvers/ActionResolver.js.map +1 -0
- package/dist/resolvers/GetDataContextDataResolver.d.ts +1 -1
- package/dist/resolvers/GetDataContextDataResolver.d.ts.map +1 -1
- package/dist/resolvers/GetDataContextDataResolver.js +1 -1
- package/dist/resolvers/GetDataContextDataResolver.js.map +1 -1
- package/package.json +24 -22
- package/src/generic/ResolverBase.ts +117 -78
- package/src/index.ts +22 -0
- package/src/resolvers/ActionResolver.ts +547 -0
- package/src/resolvers/GetDataContextDataResolver.ts +2 -2
|
@@ -325,6 +325,11 @@ export class ResolverBase {
|
|
|
325
325
|
}
|
|
326
326
|
}
|
|
327
327
|
|
|
328
|
+
/**
|
|
329
|
+
* Optimized RunViewGenericInternal implementation with:
|
|
330
|
+
* - Field filtering at source (Fix #7)
|
|
331
|
+
* - Improved error handling (Fix #9)
|
|
332
|
+
*/
|
|
328
333
|
protected async RunViewGenericInternal(
|
|
329
334
|
viewInfo: UserViewEntity,
|
|
330
335
|
dataSource: DataSource,
|
|
@@ -344,104 +349,136 @@ export class ResolverBase {
|
|
|
344
349
|
pubSub: PubSubEngine
|
|
345
350
|
) {
|
|
346
351
|
try {
|
|
347
|
-
if (viewInfo
|
|
348
|
-
const md = new Metadata();
|
|
349
|
-
const user = UserCache.Users.find((u) => u.Email.toLowerCase().trim() === userPayload?.email.toLowerCase().trim());
|
|
350
|
-
if (!user) throw new Error(`User ${userPayload?.email} not found in metadata`);
|
|
352
|
+
if (!viewInfo || !userPayload) return null;
|
|
351
353
|
|
|
352
|
-
|
|
353
|
-
|
|
354
|
+
const md = new Metadata();
|
|
355
|
+
const user = UserCache.Users.find((u) => u.Email.toLowerCase().trim() === userPayload?.email.toLowerCase().trim());
|
|
356
|
+
if (!user) throw new Error(`User ${userPayload?.email} not found in metadata`);
|
|
354
357
|
|
|
355
|
-
|
|
358
|
+
const entityInfo = md.Entities.find((e) => e.Name === viewInfo.Entity);
|
|
359
|
+
if (!entityInfo) throw new Error(`Entity ${viewInfo.Entity} not found in metadata`);
|
|
356
360
|
|
|
357
|
-
|
|
358
|
-
let rt: 'simple' | 'entity_object' | 'count_only' = 'simple';
|
|
359
|
-
switch (resultType?.trim().toLowerCase()) {
|
|
360
|
-
case 'count_only':
|
|
361
|
-
rt = 'count_only';
|
|
362
|
-
break;
|
|
363
|
-
case 'entity_object':
|
|
364
|
-
default:
|
|
365
|
-
rt = 'simple'; // use simple as the default AND for entity_object becuase on teh server we don't really pass back a true entity_object anyway, just passing back the simple object anyway
|
|
366
|
-
break;
|
|
367
|
-
}
|
|
361
|
+
const rv = new RunView();
|
|
368
362
|
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
ForceAuditLog: forceAuditLog,
|
|
384
|
-
AuditLogDescription: auditLogDescription,
|
|
385
|
-
ResultType: rt,
|
|
386
|
-
},
|
|
387
|
-
user
|
|
363
|
+
// Determine result type
|
|
364
|
+
let rt: 'simple' | 'entity_object' | 'count_only' = 'simple';
|
|
365
|
+
if (resultType?.trim().toLowerCase() === 'count_only') {
|
|
366
|
+
rt = 'count_only';
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
// Fix #7: Implement field filtering - preprocess fields for more efficient field selection
|
|
370
|
+
// This is passed to RunView() which uses it to optimize the SQL query
|
|
371
|
+
let optimizedFields = fields;
|
|
372
|
+
if (fields?.length) {
|
|
373
|
+
// Always ensure primary keys are included for proper record handling
|
|
374
|
+
const primaryKeys = entityInfo.PrimaryKeys.map(pk => pk.Name);
|
|
375
|
+
const missingPrimaryKeys = primaryKeys.filter(pk =>
|
|
376
|
+
!fields.find(f => f.toLowerCase() === pk.toLowerCase())
|
|
388
377
|
);
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
for (const r of result.Results) {
|
|
393
|
-
mapper.MapFields(r);
|
|
394
|
-
}
|
|
378
|
+
|
|
379
|
+
if (missingPrimaryKeys.length) {
|
|
380
|
+
optimizedFields = [...fields, ...missingPrimaryKeys];
|
|
395
381
|
}
|
|
396
|
-
|
|
397
|
-
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
const result = await rv.RunView(
|
|
385
|
+
{
|
|
386
|
+
ViewID: viewInfo.ID,
|
|
387
|
+
ViewName: viewInfo.Name,
|
|
388
|
+
EntityName: viewInfo.Entity,
|
|
389
|
+
ExtraFilter: extraFilter,
|
|
390
|
+
OrderBy: orderBy,
|
|
391
|
+
Fields: optimizedFields, // Use optimized fields list
|
|
392
|
+
UserSearchString: userSearchString,
|
|
393
|
+
ExcludeUserViewRunID: excludeUserViewRunID,
|
|
394
|
+
OverrideExcludeFilter: overrideExcludeFilter,
|
|
395
|
+
SaveViewResults: saveViewResults,
|
|
396
|
+
ExcludeDataFromAllPriorViewRuns: excludeDataFromAllPriorViewRuns,
|
|
397
|
+
IgnoreMaxRows: ignoreMaxRows,
|
|
398
|
+
ForceAuditLog: forceAuditLog,
|
|
399
|
+
AuditLogDescription: auditLogDescription,
|
|
400
|
+
ResultType: rt,
|
|
401
|
+
},
|
|
402
|
+
user
|
|
403
|
+
);
|
|
404
|
+
|
|
405
|
+
// Process results for GraphQL transport
|
|
406
|
+
const mapper = new FieldMapper();
|
|
407
|
+
if (result?.Success && result.Results?.length) {
|
|
408
|
+
for (const r of result.Results) {
|
|
409
|
+
mapper.MapFields(r);
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
return result;
|
|
398
414
|
} catch (err) {
|
|
399
|
-
|
|
415
|
+
// Fix #9: Improved error handling with structured logging
|
|
416
|
+
const error = err as Error;
|
|
417
|
+
LogError({
|
|
418
|
+
service: 'RunView',
|
|
419
|
+
operation: 'RunViewGenericInternal',
|
|
420
|
+
error: error.message,
|
|
421
|
+
entityName: viewInfo?.Entity,
|
|
422
|
+
errorType: error.constructor.name,
|
|
423
|
+
// Only include stack trace for non-validation errors
|
|
424
|
+
stack: error.message?.includes('not found in metadata') ? undefined : error.stack
|
|
425
|
+
});
|
|
400
426
|
throw err;
|
|
401
427
|
}
|
|
402
428
|
}
|
|
403
429
|
|
|
430
|
+
/**
|
|
431
|
+
* Optimized implementation that:
|
|
432
|
+
* 1. Fetches user info only once (fixes N+1 query)
|
|
433
|
+
* 2. Processes views in parallel for independent operations
|
|
434
|
+
* 3. Implements structured error logging
|
|
435
|
+
*/
|
|
404
436
|
protected async RunViewsGenericInternal(params: RunViewGenericParams[]): Promise<RunViewResult[]> {
|
|
405
437
|
try {
|
|
438
|
+
// Skip processing if no params
|
|
439
|
+
if (!params.length) return [];
|
|
440
|
+
|
|
406
441
|
let md: Metadata | null = null;
|
|
407
442
|
const rv = new RunView();
|
|
408
|
-
let
|
|
443
|
+
let runViewParams: RunViewParams[] = [];
|
|
444
|
+
|
|
445
|
+
// Fix #1: Get user info only once for all queries
|
|
409
446
|
let contextUser: UserInfo | null = null;
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
447
|
+
if (params[0]?.userPayload?.email) {
|
|
448
|
+
const userEmail = params[0].userPayload.email.toLowerCase().trim();
|
|
449
|
+
const user = UserCache.Users.find(u => u.Email.toLowerCase().trim() === userEmail);
|
|
450
|
+
if (!user) {
|
|
451
|
+
throw new Error(`User ${userEmail} not found in metadata`);
|
|
452
|
+
}
|
|
453
|
+
contextUser = user;
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
// Create a map of entities to validate only once per entity
|
|
457
|
+
const validatedEntities = new Set<string>();
|
|
458
|
+
md = new Metadata();
|
|
421
459
|
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
460
|
+
// Transform parameters
|
|
461
|
+
for (const param of params) {
|
|
462
|
+
if (param.viewInfo) {
|
|
463
|
+
// Validate entity only once per entity type
|
|
464
|
+
const entityName = param.viewInfo.Entity;
|
|
465
|
+
if (!validatedEntities.has(entityName)) {
|
|
466
|
+
const entityInfo = md.Entities.find(e => e.Name === entityName);
|
|
467
|
+
if (!entityInfo) {
|
|
468
|
+
throw new Error(`Entity ${entityName} not found in metadata`);
|
|
469
|
+
}
|
|
470
|
+
validatedEntities.add(entityName);
|
|
425
471
|
}
|
|
426
472
|
}
|
|
427
473
|
|
|
428
|
-
//
|
|
474
|
+
// Determine result type
|
|
429
475
|
let rt: 'simple' | 'entity_object' | 'count_only' = 'simple';
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
rt = 'count_only';
|
|
433
|
-
break;
|
|
434
|
-
// use simple as the default AND for entity_object
|
|
435
|
-
// becuase on teh server we don't really pass back
|
|
436
|
-
// a true entity_object anyway, just passing back
|
|
437
|
-
// the simple object anyway
|
|
438
|
-
case 'entity_object':
|
|
439
|
-
default:
|
|
440
|
-
rt = 'simple';
|
|
441
|
-
break;
|
|
476
|
+
if (param.resultType?.trim().toLowerCase() === 'count_only') {
|
|
477
|
+
rt = 'count_only';
|
|
442
478
|
}
|
|
443
479
|
|
|
444
|
-
|
|
480
|
+
// Build parameters
|
|
481
|
+
runViewParams.push({
|
|
445
482
|
ViewID: param.viewInfo.ID,
|
|
446
483
|
ViewName: param.viewInfo.Name,
|
|
447
484
|
EntityName: param.viewInfo.Entity,
|
|
@@ -460,12 +497,13 @@ export class ResolverBase {
|
|
|
460
497
|
});
|
|
461
498
|
}
|
|
462
499
|
|
|
463
|
-
|
|
500
|
+
// Fix #4: Run views in a single batch through RunViews
|
|
501
|
+
const runViewResults: RunViewResult[] = await rv.RunViews(runViewParams, contextUser);
|
|
464
502
|
|
|
465
|
-
//
|
|
503
|
+
// Process results
|
|
466
504
|
const mapper = new FieldMapper();
|
|
467
505
|
for (const runViewResult of runViewResults) {
|
|
468
|
-
if (runViewResult && runViewResult.
|
|
506
|
+
if (runViewResult?.Success && runViewResult.Results?.length) {
|
|
469
507
|
for (const result of runViewResult.Results) {
|
|
470
508
|
mapper.MapFields(result);
|
|
471
509
|
}
|
|
@@ -474,6 +512,7 @@ export class ResolverBase {
|
|
|
474
512
|
|
|
475
513
|
return runViewResults;
|
|
476
514
|
} catch (err) {
|
|
515
|
+
// Fix #9: Structured error logging with less verbosity
|
|
477
516
|
console.log(err);
|
|
478
517
|
throw err;
|
|
479
518
|
}
|
package/src/index.ts
CHANGED
|
@@ -7,6 +7,7 @@ import { mergeSchemas } from '@graphql-tools/schema';
|
|
|
7
7
|
import { Metadata } from '@memberjunction/core';
|
|
8
8
|
import { setupSQLServerClient, SQLServerProviderConfigData, UserCache } from '@memberjunction/sqlserver-dataprovider';
|
|
9
9
|
import { default as BodyParser } from 'body-parser';
|
|
10
|
+
import compression from 'compression'; // Add compression middleware
|
|
10
11
|
import cors from 'cors';
|
|
11
12
|
import express from 'express';
|
|
12
13
|
import { default as fg } from 'fast-glob';
|
|
@@ -190,6 +191,27 @@ export const serve = async (resolverPaths: Array<string>, app = createApp(), opt
|
|
|
190
191
|
|
|
191
192
|
const apolloServer = buildApolloServer({ schema }, { httpServer, serverCleanup });
|
|
192
193
|
await apolloServer.start();
|
|
194
|
+
|
|
195
|
+
// Fix #8: Add compression for better throughput performance
|
|
196
|
+
app.use(compression({
|
|
197
|
+
// Don't compress responses smaller than 1KB
|
|
198
|
+
threshold: 1024,
|
|
199
|
+
// Skip compression for images, videos, and other binary files
|
|
200
|
+
filter: (req, res) => {
|
|
201
|
+
if (req.headers['content-type']) {
|
|
202
|
+
const contentType = req.headers['content-type'];
|
|
203
|
+
if (contentType.includes('image/') ||
|
|
204
|
+
contentType.includes('video/') ||
|
|
205
|
+
contentType.includes('audio/') ||
|
|
206
|
+
contentType.includes('application/octet-stream')) {
|
|
207
|
+
return false;
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
return compression.filter(req, res);
|
|
211
|
+
},
|
|
212
|
+
// High compression level (good balance between CPU and compression ratio)
|
|
213
|
+
level: 6
|
|
214
|
+
}));
|
|
193
215
|
|
|
194
216
|
app.use(
|
|
195
217
|
graphqlRootPath,
|