@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.
- package/README.md +1 -0
- package/dist/apolloServer/index.d.ts +10 -2
- package/dist/apolloServer/index.d.ts.map +1 -1
- package/dist/apolloServer/index.js +22 -8
- package/dist/apolloServer/index.js.map +1 -1
- package/dist/config.d.ts +125 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +23 -0
- package/dist/config.js.map +1 -1
- package/dist/context.d.ts +17 -0
- package/dist/context.d.ts.map +1 -1
- package/dist/context.js +144 -62
- package/dist/context.js.map +1 -1
- package/dist/generated/generated.d.ts +207 -0
- package/dist/generated/generated.d.ts.map +1 -1
- package/dist/generated/generated.js +1112 -76
- package/dist/generated/generated.js.map +1 -1
- package/dist/generic/CacheInvalidationResolver.d.ts +32 -0
- package/dist/generic/CacheInvalidationResolver.d.ts.map +1 -0
- package/dist/generic/CacheInvalidationResolver.js +80 -0
- package/dist/generic/CacheInvalidationResolver.js.map +1 -0
- package/dist/generic/PubSubManager.d.ts +27 -0
- package/dist/generic/PubSubManager.d.ts.map +1 -0
- package/dist/generic/PubSubManager.js +42 -0
- package/dist/generic/PubSubManager.js.map +1 -0
- package/dist/generic/ResolverBase.d.ts +14 -0
- package/dist/generic/ResolverBase.d.ts.map +1 -1
- package/dist/generic/ResolverBase.js +50 -0
- package/dist/generic/ResolverBase.js.map +1 -1
- package/dist/hooks.d.ts +65 -0
- package/dist/hooks.d.ts.map +1 -0
- package/dist/hooks.js +14 -0
- package/dist/hooks.js.map +1 -0
- package/dist/index.d.ts +6 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +172 -45
- package/dist/index.js.map +1 -1
- package/dist/multiTenancy/index.d.ts +47 -0
- package/dist/multiTenancy/index.d.ts.map +1 -0
- package/dist/multiTenancy/index.js +152 -0
- package/dist/multiTenancy/index.js.map +1 -0
- package/dist/resolvers/IntegrationDiscoveryResolver.d.ts +123 -0
- package/dist/resolvers/IntegrationDiscoveryResolver.d.ts.map +1 -0
- package/dist/resolvers/IntegrationDiscoveryResolver.js +624 -0
- package/dist/resolvers/IntegrationDiscoveryResolver.js.map +1 -0
- package/dist/rest/RESTEndpointHandler.d.ts +3 -1
- package/dist/rest/RESTEndpointHandler.d.ts.map +1 -1
- package/dist/rest/RESTEndpointHandler.js +14 -33
- package/dist/rest/RESTEndpointHandler.js.map +1 -1
- package/dist/types.d.ts +9 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js.map +1 -1
- package/package.json +61 -57
- package/src/__tests__/multiTenancy.security.test.ts +334 -0
- package/src/__tests__/multiTenancy.test.ts +225 -0
- package/src/__tests__/unifiedAuth.test.ts +416 -0
- package/src/apolloServer/index.ts +32 -16
- package/src/config.ts +25 -0
- package/src/context.ts +205 -98
- package/src/generated/generated.ts +736 -1
- package/src/generic/CacheInvalidationResolver.ts +66 -0
- package/src/generic/PubSubManager.ts +47 -0
- package/src/generic/ResolverBase.ts +53 -0
- package/src/hooks.ts +77 -0
- package/src/index.ts +198 -49
- package/src/multiTenancy/index.ts +183 -0
- package/src/resolvers/IntegrationDiscoveryResolver.ts +584 -0
- package/src/rest/RESTEndpointHandler.ts +23 -42
- 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
|
|
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
|
-
*
|
|
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
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
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:
|
|
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:
|
|
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
|
-
|
|
283
|
-
|
|
284
|
-
Description: role.Description
|
|
264
|
+
RoleID: role.RoleID,
|
|
265
|
+
Role: role.Role
|
|
285
266
|
}))
|
|
286
267
|
});
|
|
287
268
|
} catch (error) {
|