@memberjunction/server 5.8.0 → 5.9.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 (69) hide show
  1. package/README.md +1 -0
  2. package/dist/apolloServer/index.d.ts +10 -2
  3. package/dist/apolloServer/index.d.ts.map +1 -1
  4. package/dist/apolloServer/index.js +22 -8
  5. package/dist/apolloServer/index.js.map +1 -1
  6. package/dist/config.d.ts +125 -0
  7. package/dist/config.d.ts.map +1 -1
  8. package/dist/config.js +23 -0
  9. package/dist/config.js.map +1 -1
  10. package/dist/context.d.ts +17 -0
  11. package/dist/context.d.ts.map +1 -1
  12. package/dist/context.js +144 -62
  13. package/dist/context.js.map +1 -1
  14. package/dist/generated/generated.d.ts +207 -0
  15. package/dist/generated/generated.d.ts.map +1 -1
  16. package/dist/generated/generated.js +1112 -76
  17. package/dist/generated/generated.js.map +1 -1
  18. package/dist/generic/CacheInvalidationResolver.d.ts +32 -0
  19. package/dist/generic/CacheInvalidationResolver.d.ts.map +1 -0
  20. package/dist/generic/CacheInvalidationResolver.js +80 -0
  21. package/dist/generic/CacheInvalidationResolver.js.map +1 -0
  22. package/dist/generic/PubSubManager.d.ts +27 -0
  23. package/dist/generic/PubSubManager.d.ts.map +1 -0
  24. package/dist/generic/PubSubManager.js +42 -0
  25. package/dist/generic/PubSubManager.js.map +1 -0
  26. package/dist/generic/ResolverBase.d.ts +14 -0
  27. package/dist/generic/ResolverBase.d.ts.map +1 -1
  28. package/dist/generic/ResolverBase.js +50 -0
  29. package/dist/generic/ResolverBase.js.map +1 -1
  30. package/dist/hooks.d.ts +65 -0
  31. package/dist/hooks.d.ts.map +1 -0
  32. package/dist/hooks.js +14 -0
  33. package/dist/hooks.js.map +1 -0
  34. package/dist/index.d.ts +6 -1
  35. package/dist/index.d.ts.map +1 -1
  36. package/dist/index.js +172 -45
  37. package/dist/index.js.map +1 -1
  38. package/dist/multiTenancy/index.d.ts +47 -0
  39. package/dist/multiTenancy/index.d.ts.map +1 -0
  40. package/dist/multiTenancy/index.js +152 -0
  41. package/dist/multiTenancy/index.js.map +1 -0
  42. package/dist/resolvers/IntegrationDiscoveryResolver.d.ts +123 -0
  43. package/dist/resolvers/IntegrationDiscoveryResolver.d.ts.map +1 -0
  44. package/dist/resolvers/IntegrationDiscoveryResolver.js +624 -0
  45. package/dist/resolvers/IntegrationDiscoveryResolver.js.map +1 -0
  46. package/dist/rest/RESTEndpointHandler.d.ts +3 -1
  47. package/dist/rest/RESTEndpointHandler.d.ts.map +1 -1
  48. package/dist/rest/RESTEndpointHandler.js +14 -33
  49. package/dist/rest/RESTEndpointHandler.js.map +1 -1
  50. package/dist/types.d.ts +9 -0
  51. package/dist/types.d.ts.map +1 -1
  52. package/dist/types.js.map +1 -1
  53. package/package.json +61 -57
  54. package/src/__tests__/multiTenancy.security.test.ts +334 -0
  55. package/src/__tests__/multiTenancy.test.ts +225 -0
  56. package/src/__tests__/unifiedAuth.test.ts +416 -0
  57. package/src/apolloServer/index.ts +32 -16
  58. package/src/config.ts +25 -0
  59. package/src/context.ts +205 -98
  60. package/src/generated/generated.ts +736 -1
  61. package/src/generic/CacheInvalidationResolver.ts +66 -0
  62. package/src/generic/PubSubManager.ts +47 -0
  63. package/src/generic/ResolverBase.ts +53 -0
  64. package/src/hooks.ts +77 -0
  65. package/src/index.ts +198 -49
  66. package/src/multiTenancy/index.ts +183 -0
  67. package/src/resolvers/IntegrationDiscoveryResolver.ts +584 -0
  68. package/src/rest/RESTEndpointHandler.ts +23 -42
  69. package/src/types.ts +10 -0
@@ -0,0 +1,584 @@
1
+ import { Resolver, Query, Arg, Ctx, ObjectType, Field, InputType } from "type-graphql";
2
+ import { Metadata, UserInfo, LogError } from "@memberjunction/core";
3
+ import { MJCompanyIntegrationEntity, MJIntegrationEntity } from "@memberjunction/core-entities";
4
+ import {
5
+ BaseIntegrationConnector,
6
+ ConnectorFactory,
7
+ ExternalObjectSchema,
8
+ ExternalFieldSchema,
9
+ ConnectionTestResult,
10
+ SourceSchemaInfo
11
+ } from "@memberjunction/integration-engine";
12
+ import {
13
+ SchemaBuilder,
14
+ TypeMapper,
15
+ SchemaBuilderInput,
16
+ TargetTableConfig,
17
+ TargetColumnConfig
18
+ } from "@memberjunction/integration-schema-builder";
19
+ import { ResolverBase } from "../generic/ResolverBase.js";
20
+ import { AppContext } from "../types.js";
21
+
22
+ // --- Output Types ---
23
+
24
+ @ObjectType()
25
+ class ExternalObjectOutput {
26
+ @Field()
27
+ Name: string;
28
+
29
+ @Field()
30
+ Label: string;
31
+
32
+ @Field()
33
+ SupportsIncrementalSync: boolean;
34
+
35
+ @Field()
36
+ SupportsWrite: boolean;
37
+ }
38
+
39
+ @ObjectType()
40
+ class DiscoverObjectsOutput {
41
+ @Field()
42
+ Success: boolean;
43
+
44
+ @Field()
45
+ Message: string;
46
+
47
+ @Field(() => [ExternalObjectOutput], { nullable: true })
48
+ Objects?: ExternalObjectOutput[];
49
+ }
50
+
51
+ @ObjectType()
52
+ class ExternalFieldOutput {
53
+ @Field()
54
+ Name: string;
55
+
56
+ @Field()
57
+ Label: string;
58
+
59
+ @Field()
60
+ DataType: string;
61
+
62
+ @Field()
63
+ IsRequired: boolean;
64
+
65
+ @Field()
66
+ IsUniqueKey: boolean;
67
+
68
+ @Field()
69
+ IsReadOnly: boolean;
70
+ }
71
+
72
+ @ObjectType()
73
+ class DiscoverFieldsOutput {
74
+ @Field()
75
+ Success: boolean;
76
+
77
+ @Field()
78
+ Message: string;
79
+
80
+ @Field(() => [ExternalFieldOutput], { nullable: true })
81
+ Fields?: ExternalFieldOutput[];
82
+ }
83
+
84
+ @ObjectType()
85
+ class ConnectionTestOutput {
86
+ @Field()
87
+ Success: boolean;
88
+
89
+ @Field()
90
+ Message: string;
91
+
92
+ @Field(() => String, { nullable: true })
93
+ ServerVersion?: string;
94
+ }
95
+
96
+ // --- Preview Data Types ---
97
+
98
+ @ObjectType()
99
+ class PreviewRecordOutput {
100
+ @Field(() => String)
101
+ Data: string; // JSON-serialized record fields
102
+ }
103
+
104
+ @ObjectType()
105
+ class PreviewDataOutput {
106
+ @Field()
107
+ Success: boolean;
108
+
109
+ @Field()
110
+ Message: string;
111
+
112
+ @Field(() => [PreviewRecordOutput], { nullable: true })
113
+ Records?: PreviewRecordOutput[];
114
+ }
115
+
116
+ // --- Schema Preview Types ---
117
+
118
+ @InputType()
119
+ class SchemaPreviewObjectInput {
120
+ @Field()
121
+ SourceObjectName: string;
122
+
123
+ @Field()
124
+ SchemaName: string;
125
+
126
+ @Field()
127
+ TableName: string;
128
+
129
+ @Field()
130
+ EntityName: string;
131
+ }
132
+
133
+ @ObjectType()
134
+ class SchemaPreviewFileOutput {
135
+ @Field()
136
+ FilePath: string;
137
+
138
+ @Field()
139
+ Content: string;
140
+
141
+ @Field()
142
+ Description: string;
143
+ }
144
+
145
+ @ObjectType()
146
+ class SchemaPreviewOutput {
147
+ @Field()
148
+ Success: boolean;
149
+
150
+ @Field()
151
+ Message: string;
152
+
153
+ @Field(() => [SchemaPreviewFileOutput], { nullable: true })
154
+ Files?: SchemaPreviewFileOutput[];
155
+
156
+ @Field(() => [String], { nullable: true })
157
+ Warnings?: string[];
158
+ }
159
+
160
+ // --- Default Configuration Types ---
161
+
162
+ @ObjectType()
163
+ class DefaultFieldMappingOutput {
164
+ @Field()
165
+ SourceFieldName: string;
166
+
167
+ @Field()
168
+ DestinationFieldName: string;
169
+
170
+ @Field({ nullable: true })
171
+ IsKeyField?: boolean;
172
+ }
173
+
174
+ @ObjectType()
175
+ class DefaultObjectConfigOutput {
176
+ @Field()
177
+ SourceObjectName: string;
178
+
179
+ @Field()
180
+ TargetTableName: string;
181
+
182
+ @Field()
183
+ TargetEntityName: string;
184
+
185
+ @Field()
186
+ SyncEnabled: boolean;
187
+
188
+ @Field(() => [DefaultFieldMappingOutput])
189
+ FieldMappings: DefaultFieldMappingOutput[];
190
+ }
191
+
192
+ @ObjectType()
193
+ class DefaultConfigOutput {
194
+ @Field()
195
+ Success: boolean;
196
+
197
+ @Field()
198
+ Message: string;
199
+
200
+ @Field({ nullable: true })
201
+ DefaultSchemaName?: string;
202
+
203
+ @Field(() => [DefaultObjectConfigOutput], { nullable: true })
204
+ DefaultObjects?: DefaultObjectConfigOutput[];
205
+ }
206
+
207
+ // --- Resolver ---
208
+
209
+ /**
210
+ * GraphQL resolver for integration discovery operations.
211
+ * Provides endpoints to test connections, discover objects, and discover fields
212
+ * on external systems via their registered connectors.
213
+ */
214
+ @Resolver()
215
+ export class IntegrationDiscoveryResolver extends ResolverBase {
216
+
217
+ /**
218
+ * Discovers available objects/tables in the external system.
219
+ */
220
+ @Query(() => DiscoverObjectsOutput)
221
+ async IntegrationDiscoverObjects(
222
+ @Arg("companyIntegrationID") companyIntegrationID: string,
223
+ @Ctx() ctx: AppContext
224
+ ): Promise<DiscoverObjectsOutput> {
225
+ try {
226
+ const user = this.getAuthenticatedUser(ctx);
227
+ const { connector, companyIntegration } = await this.resolveConnector(companyIntegrationID, user);
228
+
229
+ // Cast through unknown to bridge duplicate package type declarations
230
+ // (integration-engine resolves its own node_modules copies of core/core-entities)
231
+ const discoverObjects = connector.DiscoverObjects.bind(connector) as
232
+ (ci: unknown, u: unknown) => Promise<ExternalObjectSchema[]>;
233
+ const objects = await discoverObjects(companyIntegration, user);
234
+
235
+ return {
236
+ Success: true,
237
+ Message: `Discovered ${objects.length} objects`,
238
+ Objects: objects.map(o => ({
239
+ Name: o.Name,
240
+ Label: o.Label,
241
+ SupportsIncrementalSync: o.SupportsIncrementalSync,
242
+ SupportsWrite: o.SupportsWrite
243
+ }))
244
+ };
245
+ } catch (e) {
246
+ return this.handleDiscoveryError(e);
247
+ }
248
+ }
249
+
250
+ /**
251
+ * Discovers fields on a specific external object.
252
+ */
253
+ @Query(() => DiscoverFieldsOutput)
254
+ async IntegrationDiscoverFields(
255
+ @Arg("companyIntegrationID") companyIntegrationID: string,
256
+ @Arg("objectName") objectName: string,
257
+ @Ctx() ctx: AppContext
258
+ ): Promise<DiscoverFieldsOutput> {
259
+ try {
260
+ const user = this.getAuthenticatedUser(ctx);
261
+ const { connector, companyIntegration } = await this.resolveConnector(companyIntegrationID, user);
262
+
263
+ // Cast through unknown to bridge duplicate package type declarations
264
+ const discoverFields = connector.DiscoverFields.bind(connector) as
265
+ (ci: unknown, obj: string, u: unknown) => Promise<ExternalFieldSchema[]>;
266
+ const fields = await discoverFields(companyIntegration, objectName, user);
267
+
268
+ return {
269
+ Success: true,
270
+ Message: `Discovered ${fields.length} fields on "${objectName}"`,
271
+ Fields: fields.map(f => ({
272
+ Name: f.Name,
273
+ Label: f.Label,
274
+ DataType: f.DataType,
275
+ IsRequired: f.IsRequired,
276
+ IsUniqueKey: f.IsUniqueKey,
277
+ IsReadOnly: f.IsReadOnly
278
+ }))
279
+ };
280
+ } catch (e) {
281
+ return this.handleDiscoveryError(e);
282
+ }
283
+ }
284
+
285
+ /**
286
+ * Tests connectivity to the external system.
287
+ */
288
+ @Query(() => ConnectionTestOutput)
289
+ async IntegrationTestConnection(
290
+ @Arg("companyIntegrationID") companyIntegrationID: string,
291
+ @Ctx() ctx: AppContext
292
+ ): Promise<ConnectionTestOutput> {
293
+ try {
294
+ const user = this.getAuthenticatedUser(ctx);
295
+ const { connector, companyIntegration } = await this.resolveConnector(companyIntegrationID, user);
296
+
297
+ // Cast through unknown to bridge duplicate package type declarations
298
+ const testConnection = connector.TestConnection.bind(connector) as
299
+ (ci: unknown, u: unknown) => Promise<ConnectionTestResult>;
300
+ const result = await testConnection(companyIntegration, user);
301
+
302
+ return {
303
+ Success: result.Success,
304
+ Message: result.Message,
305
+ ServerVersion: result.ServerVersion
306
+ };
307
+ } catch (e) {
308
+ const error = e as Error;
309
+ LogError(`IntegrationTestConnection error: ${error}`);
310
+ return {
311
+ Success: false,
312
+ Message: `Error: ${error.message}`
313
+ };
314
+ }
315
+ }
316
+
317
+ /**
318
+ * Returns the connector's default configuration for quick setup.
319
+ * Not all connectors provide defaults — returns Success: false if unavailable.
320
+ */
321
+ @Query(() => DefaultConfigOutput)
322
+ async IntegrationGetDefaultConfig(
323
+ @Arg("companyIntegrationID") companyIntegrationID: string,
324
+ @Ctx() ctx: AppContext
325
+ ): Promise<DefaultConfigOutput> {
326
+ try {
327
+ const user = this.getAuthenticatedUser(ctx);
328
+ const { connector } = await this.resolveConnector(companyIntegrationID, user);
329
+
330
+ const config = connector.GetDefaultConfiguration();
331
+ if (!config) {
332
+ return {
333
+ Success: false,
334
+ Message: 'This connector does not provide a default configuration'
335
+ };
336
+ }
337
+
338
+ return {
339
+ Success: true,
340
+ Message: `Default configuration with ${config.DefaultObjects.length} objects`,
341
+ DefaultSchemaName: config.DefaultSchemaName,
342
+ DefaultObjects: config.DefaultObjects.map(o => ({
343
+ SourceObjectName: o.SourceObjectName,
344
+ TargetTableName: o.TargetTableName,
345
+ TargetEntityName: o.TargetEntityName,
346
+ SyncEnabled: o.SyncEnabled,
347
+ FieldMappings: o.FieldMappings.map(f => ({
348
+ SourceFieldName: f.SourceFieldName,
349
+ DestinationFieldName: f.DestinationFieldName,
350
+ IsKeyField: f.IsKeyField
351
+ }))
352
+ }))
353
+ };
354
+ } catch (e) {
355
+ const error = e as Error;
356
+ LogError(`IntegrationGetDefaultConfig error: ${error}`);
357
+ return {
358
+ Success: false,
359
+ Message: `Error: ${error.message}`
360
+ };
361
+ }
362
+ }
363
+
364
+ /**
365
+ * Generates a DDL preview for creating tables from discovered external objects.
366
+ * Introspects the source schema and runs SchemaBuilder to produce migration SQL.
367
+ */
368
+ @Query(() => SchemaPreviewOutput)
369
+ async IntegrationSchemaPreview(
370
+ @Arg("companyIntegrationID") companyIntegrationID: string,
371
+ @Arg("objects", () => [SchemaPreviewObjectInput]) objects: SchemaPreviewObjectInput[],
372
+ @Arg("platform", { defaultValue: "sqlserver" }) platform: string,
373
+ @Ctx() ctx: AppContext
374
+ ): Promise<SchemaPreviewOutput> {
375
+ try {
376
+ const user = this.getAuthenticatedUser(ctx);
377
+ const { connector, companyIntegration } = await this.resolveConnector(companyIntegrationID, user);
378
+
379
+ // Introspect schema from the external system
380
+ const introspect = connector.IntrospectSchema.bind(connector) as
381
+ (ci: unknown, u: unknown) => Promise<SourceSchemaInfo>;
382
+ const sourceSchema = await introspect(companyIntegration, user);
383
+
384
+ // Filter to only requested objects
385
+ const requestedNames = new Set(objects.map(o => o.SourceObjectName));
386
+ const filteredSchema: SourceSchemaInfo = {
387
+ Objects: sourceSchema.Objects.filter(o => requestedNames.has(o.ExternalName))
388
+ };
389
+
390
+ // Build target configs from user input + source schema
391
+ const targetConfigs = this.buildTargetConfigs(objects, filteredSchema, platform as 'sqlserver' | 'postgresql');
392
+
393
+ // Run SchemaBuilder
394
+ const input: SchemaBuilderInput = {
395
+ SourceSchema: filteredSchema,
396
+ TargetConfigs: targetConfigs,
397
+ Platform: platform as 'sqlserver' | 'postgresql',
398
+ MJVersion: '5.7.0',
399
+ SourceType: companyIntegration.Integration,
400
+ AdditionalSchemaInfoPath: 'additionalSchemaInfo.json',
401
+ MigrationsDir: 'migrations/v2',
402
+ MetadataDir: 'metadata',
403
+ ExistingTables: [],
404
+ EntitySettingsForTargets: {}
405
+ };
406
+
407
+ const builder = new SchemaBuilder();
408
+ const output = builder.BuildSchema(input);
409
+
410
+ if (output.Errors.length > 0) {
411
+ return {
412
+ Success: false,
413
+ Message: `Schema generation failed: ${output.Errors.join('; ')}`,
414
+ Warnings: output.Warnings
415
+ };
416
+ }
417
+
418
+ const allFiles = [
419
+ ...output.MigrationFiles,
420
+ ...(output.AdditionalSchemaInfoUpdate ? [output.AdditionalSchemaInfoUpdate] : []),
421
+ ...output.MetadataFiles
422
+ ];
423
+
424
+ return {
425
+ Success: true,
426
+ Message: `Generated ${allFiles.length} files`,
427
+ Files: allFiles.map(f => ({
428
+ FilePath: f.FilePath,
429
+ Content: f.Content,
430
+ Description: f.Description
431
+ })),
432
+ Warnings: output.Warnings.length > 0 ? output.Warnings : undefined
433
+ };
434
+ } catch (e) {
435
+ const error = e as Error;
436
+ LogError(`IntegrationSchemaPreview error: ${error}`);
437
+ return {
438
+ Success: false,
439
+ Message: `Error: ${error.message}`
440
+ };
441
+ }
442
+ }
443
+
444
+ /**
445
+ * Fetches a small sample of records from an external object for preview purposes.
446
+ * Uses the connector's FetchChanges with a small batch size and no watermark.
447
+ */
448
+ @Query(() => PreviewDataOutput)
449
+ async IntegrationPreviewData(
450
+ @Arg("companyIntegrationID") companyIntegrationID: string,
451
+ @Arg("objectName") objectName: string,
452
+ @Arg("limit", { defaultValue: 5 }) limit: number,
453
+ @Ctx() ctx: AppContext
454
+ ): Promise<PreviewDataOutput> {
455
+ try {
456
+ const user = this.getAuthenticatedUser(ctx);
457
+ const { connector, companyIntegration } = await this.resolveConnector(companyIntegrationID, user);
458
+
459
+ const fetchChanges = connector.FetchChanges.bind(connector) as
460
+ (ctx: unknown) => Promise<{ Records: Array<{ ExternalID: string; ObjectType: string; Fields: Record<string, unknown> }>; HasMore: boolean }>;
461
+
462
+ const result = await fetchChanges({
463
+ CompanyIntegration: companyIntegration,
464
+ ObjectName: objectName,
465
+ WatermarkValue: null,
466
+ BatchSize: Math.min(limit, 10),
467
+ ContextUser: user
468
+ });
469
+
470
+ return {
471
+ Success: true,
472
+ Message: `Fetched ${result.Records.length} preview records`,
473
+ Records: result.Records.map(r => ({
474
+ Data: JSON.stringify(r.Fields)
475
+ }))
476
+ };
477
+ } catch (e) {
478
+ const error = e as Error;
479
+ LogError(`IntegrationPreviewData error: ${error}`);
480
+ return {
481
+ Success: false,
482
+ Message: `Error: ${error.message}`
483
+ };
484
+ }
485
+ }
486
+
487
+ // --- Private Helpers ---
488
+
489
+ private buildTargetConfigs(
490
+ objects: SchemaPreviewObjectInput[],
491
+ sourceSchema: SourceSchemaInfo,
492
+ platform: 'sqlserver' | 'postgresql'
493
+ ): TargetTableConfig[] {
494
+ const mapper = new TypeMapper();
495
+
496
+ return objects.map(obj => {
497
+ const sourceObj = sourceSchema.Objects.find(o => o.ExternalName === obj.SourceObjectName);
498
+ const columns: TargetColumnConfig[] = (sourceObj?.Fields ?? []).map(f => ({
499
+ SourceFieldName: f.Name,
500
+ TargetColumnName: f.Name.replace(/[^A-Za-z0-9_]/g, '_'),
501
+ TargetSqlType: mapper.MapSourceType(f.SourceType, platform, f),
502
+ IsNullable: !f.IsRequired,
503
+ MaxLength: f.MaxLength,
504
+ Precision: f.Precision,
505
+ Scale: f.Scale,
506
+ DefaultValue: f.DefaultValue
507
+ }));
508
+
509
+ const primaryKeyFields = (sourceObj?.Fields ?? [])
510
+ .filter(f => f.IsPrimaryKey)
511
+ .map(f => f.Name.replace(/[^A-Za-z0-9_]/g, '_'));
512
+
513
+ return {
514
+ SourceObjectName: obj.SourceObjectName,
515
+ SchemaName: obj.SchemaName,
516
+ TableName: obj.TableName,
517
+ EntityName: obj.EntityName,
518
+ Columns: columns,
519
+ PrimaryKeyFields: primaryKeyFields.length > 0 ? primaryKeyFields : ['ID'],
520
+ SoftForeignKeys: []
521
+ };
522
+ });
523
+ }
524
+
525
+ private getAuthenticatedUser(ctx: AppContext): UserInfo {
526
+ const user = ctx.userPayload.userRecord;
527
+ if (!user) {
528
+ throw new Error("User is not authenticated");
529
+ }
530
+ return user;
531
+ }
532
+
533
+ /**
534
+ * Loads the CompanyIntegration + its parent Integration, then resolves the
535
+ * appropriate connector via ConnectorFactory.
536
+ *
537
+ * NOTE: Entity objects loaded here come from the MJServer's copy of core-entities.
538
+ * The integration-engine package may resolve its own copy of core-entities, causing
539
+ * TypeScript nominal type mismatches. At runtime the objects are structurally identical,
540
+ * so we cast through `unknown` at the boundary calls.
541
+ */
542
+ private async resolveConnector(
543
+ companyIntegrationID: string,
544
+ contextUser: UserInfo
545
+ ): Promise<{ connector: BaseIntegrationConnector; companyIntegration: MJCompanyIntegrationEntity }> {
546
+ const md = new Metadata();
547
+
548
+ // Load the CompanyIntegration record
549
+ const companyIntegration = await md.GetEntityObject<MJCompanyIntegrationEntity>(
550
+ 'MJ: Company Integrations',
551
+ contextUser
552
+ );
553
+ const loaded = await companyIntegration.Load(companyIntegrationID);
554
+ if (!loaded) {
555
+ throw new Error(`CompanyIntegration with ID "${companyIntegrationID}" not found`);
556
+ }
557
+
558
+ // Load the parent Integration record
559
+ const integration = await md.GetEntityObject<MJIntegrationEntity>(
560
+ 'MJ: Integrations',
561
+ contextUser
562
+ );
563
+ const integrationLoaded = await integration.Load(companyIntegration.IntegrationID);
564
+ if (!integrationLoaded) {
565
+ throw new Error(`Integration with ID "${companyIntegration.IntegrationID}" not found`);
566
+ }
567
+
568
+ // ConnectorFactory expects its own copy of core-entities types — cast through unknown
569
+ const connector = ConnectorFactory.Resolve(
570
+ integration as unknown as Parameters<typeof ConnectorFactory.Resolve>[0]
571
+ );
572
+
573
+ return { connector, companyIntegration };
574
+ }
575
+
576
+ private handleDiscoveryError(e: unknown): DiscoverObjectsOutput & DiscoverFieldsOutput {
577
+ const error = e as Error;
578
+ LogError(`Integration discovery error: ${error}`);
579
+ return {
580
+ Success: false,
581
+ Message: `Error: ${error.message}`
582
+ };
583
+ }
584
+ }
@@ -1,8 +1,8 @@
1
1
  import express from 'express';
2
- import {
3
- BaseEntity, CompositeKey, EntityDeleteOptions, EntityInfo,
4
- EntityPermissionType, EntitySaveOptions, LogError, Metadata,
5
- RunView, RunViewParams
2
+ import {
3
+ BaseEntity, CompositeKey, EntityDeleteOptions, EntityInfo,
4
+ EntityPermissionType, EntitySaveOptions, LogError, Metadata,
5
+ RunView, RunViewParams, UserInfo
6
6
  } from '@memberjunction/core';
7
7
  import { EntityCRUDHandler } from './EntityCRUDHandler.js';
8
8
  import { ViewOperationsHandler } from './ViewOperationsHandler.js';
@@ -145,8 +145,8 @@ export class RESTEndpointHandler {
145
145
  * Set up all the API routes for the REST endpoints
146
146
  */
147
147
  private setupRoutes() {
148
- // Middleware to extract MJ user
149
- this.router.use(this.extractMJUser);
148
+ // Middleware to verify MJ user was set by upstream auth
149
+ this.router.use(this.extractMJUser.bind(this));
150
150
 
151
151
  // Middleware to check entity allowlist/blocklist
152
152
  this.router.use('/entities/:entityName', this.checkEntityAccess.bind(this));
@@ -194,7 +194,7 @@ export class RESTEndpointHandler {
194
194
  this.router.post('/queries/run', this.runQuery.bind(this));
195
195
 
196
196
  // Error handling
197
- this.router.use(this.errorHandler);
197
+ this.router.use(this.errorHandler.bind(this));
198
198
  }
199
199
 
200
200
  /**
@@ -220,47 +220,30 @@ export class RESTEndpointHandler {
220
220
  }
221
221
 
222
222
  /**
223
- * Middleware to extract MJ user from request
223
+ * Guard middleware: verifies that the unified auth middleware (or a custom
224
+ * per-route authMiddleware) has already set req['mjUser'] to a UserInfo.
225
+ * Does NOT overwrite the value — just returns 401 if it is missing.
224
226
  */
225
- private async extractMJUser(req: express.Request, res: express.Response, next: express.NextFunction): Promise<void> {
226
- try {
227
- // If authentication middleware has already set req.user with basic info
228
- if (req['user']) {
229
- // Get the full MemberJunction user
230
- const md = new Metadata();
231
- const userInfo = req['user'];
232
- // Get user info based on email or ID
233
- // Note: The actual implementation here would depend on how the MemberJunction core handles user lookup
234
- // This is a simplification that would need to be implemented properly
235
- req['mjUser'] = userInfo;
236
-
237
- if (!req['mjUser']) {
238
- res.status(401).json({ error: 'User not found in MemberJunction' });
239
- return;
240
- }
241
- } else {
242
- res.status(401).json({ error: 'Authentication required' });
243
- return;
244
- }
245
-
246
- next();
247
- } catch (error) {
248
- next(error);
227
+ private extractMJUser(req: express.Request, res: express.Response, next: express.NextFunction): void {
228
+ if (!req['mjUser']) {
229
+ res.status(401).json({ error: 'Authentication required' });
230
+ return;
249
231
  }
232
+ next();
250
233
  }
251
234
 
252
235
  /**
253
236
  * Error handling middleware
254
237
  */
255
- private errorHandler(err: any, req: express.Request, res: express.Response, next: express.NextFunction): void {
238
+ private errorHandler(err: Error, req: express.Request, res: express.Response, next: express.NextFunction): void {
256
239
  LogError(err);
257
-
240
+
258
241
  if (err.name === 'UnauthorizedError') {
259
242
  res.status(401).json({ error: 'Invalid token' });
260
243
  return;
261
244
  }
262
-
263
- res.status(500).json({ error: (err as Error)?.message || 'Internal server error' });
245
+
246
+ res.status(500).json({ error: err.message || 'Internal server error' });
264
247
  }
265
248
 
266
249
  /**
@@ -268,8 +251,8 @@ export class RESTEndpointHandler {
268
251
  */
269
252
  private async getCurrentUser(req: express.Request, res: express.Response): Promise<void> {
270
253
  try {
271
- const user = req['mjUser'];
272
-
254
+ const user = req['mjUser'] as UserInfo;
255
+
273
256
  // Return user info without sensitive data
274
257
  res.json({
275
258
  ID: user.ID,
@@ -277,11 +260,9 @@ export class RESTEndpointHandler {
277
260
  Email: user.Email,
278
261
  FirstName: user.FirstName,
279
262
  LastName: user.LastName,
280
- IsAdmin: user.IsAdmin,
281
263
  UserRoles: user.UserRoles.map(role => ({
282
- ID: role.ID,
283
- Name: role.Name,
284
- Description: role.Description
264
+ RoleID: role.RoleID,
265
+ Role: role.Role
285
266
  }))
286
267
  });
287
268
  } catch (error) {