@memberjunction/server 2.99.0 → 2.100.1
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/config.d.ts +213 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +15 -0
- package/dist/config.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/resolvers/AskSkipResolver.d.ts +3 -0
- package/dist/resolvers/AskSkipResolver.d.ts.map +1 -1
- package/dist/resolvers/AskSkipResolver.js +10 -5
- package/dist/resolvers/AskSkipResolver.js.map +1 -1
- package/dist/resolvers/ComponentRegistryResolver.d.ts +49 -0
- package/dist/resolvers/ComponentRegistryResolver.d.ts.map +1 -0
- package/dist/resolvers/ComponentRegistryResolver.js +448 -0
- package/dist/resolvers/ComponentRegistryResolver.js.map +1 -0
- package/package.json +41 -40
- package/src/config.ts +17 -0
- package/src/index.ts +1 -0
- package/src/resolvers/AskSkipResolver.ts +11 -5
- package/src/resolvers/ComponentRegistryResolver.ts +535 -0
|
@@ -54,7 +54,7 @@ import {
|
|
|
54
54
|
UserNotificationEntity,
|
|
55
55
|
AIAgentEntityExtended
|
|
56
56
|
} from '@memberjunction/core-entities';
|
|
57
|
-
import { apiKey, baseUrl, publicUrl, configInfo, graphqlPort, graphqlRootPath, mj_core_schema } from '../config.js';
|
|
57
|
+
import { apiKey as callbackAPIKey, AskSkipInfo, baseUrl, publicUrl, configInfo, graphqlPort, graphqlRootPath, mj_core_schema } from '../config.js';
|
|
58
58
|
import mssql from 'mssql';
|
|
59
59
|
|
|
60
60
|
import { registerEnumType } from 'type-graphql';
|
|
@@ -731,7 +731,7 @@ export class AskSkipResolver {
|
|
|
731
731
|
const skipConfigInfo = configInfo.askSkip;
|
|
732
732
|
LogStatus(` >>> HandleSimpleSkipLearningPostRequest Sending request to Skip API: ${skipConfigInfo.learningCycleURL}`);
|
|
733
733
|
|
|
734
|
-
const response = await sendPostRequest(skipConfigInfo.learningCycleURL, input, true,
|
|
734
|
+
const response = await sendPostRequest(skipConfigInfo.learningCycleURL, input, true, this.buildSkipPostHeaders());
|
|
735
735
|
|
|
736
736
|
if (response && response.length > 0) {
|
|
737
737
|
// the last object in the response array is the final response from the Skip API
|
|
@@ -792,7 +792,7 @@ export class AskSkipResolver {
|
|
|
792
792
|
LogStatus(` >>> HandleSimpleSkipChatPostRequest Sending request to Skip API: ${skipConfigInfo.chatURL}`);
|
|
793
793
|
|
|
794
794
|
try {
|
|
795
|
-
const response = await sendPostRequest(skipConfigInfo.chatURL, input, true,
|
|
795
|
+
const response = await sendPostRequest(skipConfigInfo.chatURL, input, true, this.buildSkipPostHeaders());
|
|
796
796
|
|
|
797
797
|
if (response && response.length > 0) {
|
|
798
798
|
// the last object in the response array is the final response from the Skip API
|
|
@@ -1065,7 +1065,7 @@ cycle.`);
|
|
|
1065
1065
|
// Favors public URL for conciseness or when behind a proxy for local development
|
|
1066
1066
|
// otherwise uses base URL and GraphQL port/path from configuration
|
|
1067
1067
|
callingServerURL: accessToken ? (publicUrl || `${baseUrl}:${graphqlPort}${graphqlRootPath}`) : undefined,
|
|
1068
|
-
callingServerAPIKey: accessToken ?
|
|
1068
|
+
callingServerAPIKey: accessToken ? callbackAPIKey : undefined,
|
|
1069
1069
|
callingServerAccessToken: accessToken ? accessToken.Token : undefined
|
|
1070
1070
|
};
|
|
1071
1071
|
}
|
|
@@ -2462,7 +2462,7 @@ cycle.`);
|
|
|
2462
2462
|
skipConfigInfo.chatURL,
|
|
2463
2463
|
input,
|
|
2464
2464
|
true,
|
|
2465
|
-
|
|
2465
|
+
this.buildSkipPostHeaders(),
|
|
2466
2466
|
(message: {
|
|
2467
2467
|
type: string;
|
|
2468
2468
|
value: {
|
|
@@ -2608,6 +2608,12 @@ cycle.`);
|
|
|
2608
2608
|
}
|
|
2609
2609
|
}
|
|
2610
2610
|
|
|
2611
|
+
protected buildSkipPostHeaders(): { [key: string]: string } {
|
|
2612
|
+
return {
|
|
2613
|
+
'x-api-key': configInfo.askSkip?.apiKey ?? '',
|
|
2614
|
+
};
|
|
2615
|
+
}
|
|
2616
|
+
|
|
2611
2617
|
/**
|
|
2612
2618
|
* Publishes a status update message to the user based on the Skip API response
|
|
2613
2619
|
* Provides feedback about what phase of processing is happening
|
|
@@ -0,0 +1,535 @@
|
|
|
1
|
+
import { Arg, Ctx, Field, InputType, ObjectType, Query, Resolver } from 'type-graphql';
|
|
2
|
+
import { UserInfo, Metadata, LogError, LogStatus } from '@memberjunction/core';
|
|
3
|
+
import { UserCache } from '@memberjunction/sqlserver-dataprovider';
|
|
4
|
+
import { ComponentEntity, ComponentRegistryEntity, ComponentMetadataEngine } from '@memberjunction/core-entities';
|
|
5
|
+
import { ComponentSpec } from '@memberjunction/interactive-component-types';
|
|
6
|
+
import {
|
|
7
|
+
ComponentRegistryClient,
|
|
8
|
+
ComponentResponse,
|
|
9
|
+
ComponentSearchResult,
|
|
10
|
+
DependencyTree,
|
|
11
|
+
RegistryError,
|
|
12
|
+
RegistryErrorCode
|
|
13
|
+
} from '@memberjunction/component-registry-client-sdk';
|
|
14
|
+
import { AppContext } from '../types';
|
|
15
|
+
import { configInfo } from '../config';
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* GraphQL types for Component Registry operations
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
@ObjectType()
|
|
22
|
+
class ComponentSpecWithHashType {
|
|
23
|
+
@Field(() => String, { nullable: true })
|
|
24
|
+
specification?: string; // JSON string of ComponentSpec
|
|
25
|
+
|
|
26
|
+
@Field(() => String)
|
|
27
|
+
hash: string;
|
|
28
|
+
|
|
29
|
+
@Field(() => Boolean)
|
|
30
|
+
notModified: boolean;
|
|
31
|
+
|
|
32
|
+
@Field(() => String, { nullable: true })
|
|
33
|
+
message?: string;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
@InputType()
|
|
37
|
+
class SearchRegistryComponentsInput {
|
|
38
|
+
@Field({ nullable: true })
|
|
39
|
+
registryId?: string;
|
|
40
|
+
|
|
41
|
+
@Field({ nullable: true })
|
|
42
|
+
namespace?: string;
|
|
43
|
+
|
|
44
|
+
@Field({ nullable: true })
|
|
45
|
+
query?: string;
|
|
46
|
+
|
|
47
|
+
@Field({ nullable: true })
|
|
48
|
+
type?: string;
|
|
49
|
+
|
|
50
|
+
@Field(() => [String], { nullable: true })
|
|
51
|
+
tags?: string[];
|
|
52
|
+
|
|
53
|
+
@Field({ nullable: true })
|
|
54
|
+
limit?: number;
|
|
55
|
+
|
|
56
|
+
@Field({ nullable: true })
|
|
57
|
+
offset?: number;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
@ObjectType()
|
|
61
|
+
class RegistryComponentSearchResultType {
|
|
62
|
+
@Field(() => [String])
|
|
63
|
+
components: string[]; // Array of JSON strings of ComponentSpec
|
|
64
|
+
|
|
65
|
+
@Field()
|
|
66
|
+
total: number;
|
|
67
|
+
|
|
68
|
+
@Field()
|
|
69
|
+
offset: number;
|
|
70
|
+
|
|
71
|
+
@Field()
|
|
72
|
+
limit: number;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
@ObjectType()
|
|
76
|
+
class ComponentDependencyTreeType {
|
|
77
|
+
@Field()
|
|
78
|
+
componentId: string;
|
|
79
|
+
|
|
80
|
+
@Field({ nullable: true })
|
|
81
|
+
name?: string;
|
|
82
|
+
|
|
83
|
+
@Field({ nullable: true })
|
|
84
|
+
namespace?: string;
|
|
85
|
+
|
|
86
|
+
@Field({ nullable: true })
|
|
87
|
+
version?: string;
|
|
88
|
+
|
|
89
|
+
@Field({ nullable: true })
|
|
90
|
+
circular?: boolean;
|
|
91
|
+
|
|
92
|
+
@Field({ nullable: true })
|
|
93
|
+
totalCount?: number;
|
|
94
|
+
|
|
95
|
+
@Field(() => [ComponentDependencyTreeType], { nullable: true })
|
|
96
|
+
dependencies?: ComponentDependencyTreeType[];
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Resolver for Component Registry operations
|
|
101
|
+
*
|
|
102
|
+
* Environment Variables for Development:
|
|
103
|
+
* - REGISTRY_URI_OVERRIDE_<REGISTRY_NAME>: Override the URI for a specific registry
|
|
104
|
+
* Example: REGISTRY_URI_OVERRIDE_MJ_CENTRAL=http://localhost:8080
|
|
105
|
+
* Registry names are converted to uppercase with non-alphanumeric chars replaced by underscores
|
|
106
|
+
*
|
|
107
|
+
* - REGISTRY_API_KEY_<REGISTRY_NAME>: API key for authenticating with the registry
|
|
108
|
+
* Example: REGISTRY_API_KEY_MJ_CENTRAL=your-api-key-here
|
|
109
|
+
*/
|
|
110
|
+
@Resolver()
|
|
111
|
+
export class ComponentRegistryExtendedResolver {
|
|
112
|
+
private componentEngine = ComponentMetadataEngine.Instance;
|
|
113
|
+
|
|
114
|
+
constructor() {
|
|
115
|
+
// No longer pre-initialize clients - create on demand
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Get a component from a registry with optional hash for caching
|
|
120
|
+
*/
|
|
121
|
+
@Query(() => ComponentSpecWithHashType)
|
|
122
|
+
async GetRegistryComponent(
|
|
123
|
+
@Arg('registryName') registryName: string,
|
|
124
|
+
@Arg('namespace') namespace: string,
|
|
125
|
+
@Arg('name') name: string,
|
|
126
|
+
@Ctx() { userPayload }: AppContext,
|
|
127
|
+
@Arg('version', { nullable: true }) version?: string,
|
|
128
|
+
@Arg('hash', { nullable: true }) hash?: string
|
|
129
|
+
): Promise<ComponentSpecWithHashType> {
|
|
130
|
+
try {
|
|
131
|
+
// Get user from cache
|
|
132
|
+
const user = UserCache.Instance.Users.find((u) => u.Email.trim().toLowerCase() === userPayload.email?.trim().toLowerCase());
|
|
133
|
+
if (!user) throw new Error(`User ${userPayload.email} not found in UserCache`);
|
|
134
|
+
|
|
135
|
+
// Get registry from database by name
|
|
136
|
+
const registry = await this.getRegistryByName(registryName, user);
|
|
137
|
+
if (!registry) {
|
|
138
|
+
throw new Error(`Registry not found: ${registryName}`);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// Check user permissions (use registry ID for permission check)
|
|
142
|
+
await this.checkUserAccess(user, registry.ID);
|
|
143
|
+
|
|
144
|
+
// Initialize component engine
|
|
145
|
+
await this.componentEngine.Config(false, user);
|
|
146
|
+
|
|
147
|
+
// Create client on-demand for this registry
|
|
148
|
+
const registryClient = this.createClientForRegistry(registry);
|
|
149
|
+
|
|
150
|
+
// Fetch component from registry with hash support
|
|
151
|
+
const response = await registryClient.getComponentWithHash({
|
|
152
|
+
registry: registry.Name,
|
|
153
|
+
namespace,
|
|
154
|
+
name,
|
|
155
|
+
version: version || 'latest',
|
|
156
|
+
hash: hash
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
// If not modified (304), return response with notModified flag
|
|
160
|
+
if (response.notModified) {
|
|
161
|
+
LogStatus(`Component ${namespace}/${name} not modified (hash: ${response.hash})`);
|
|
162
|
+
return {
|
|
163
|
+
specification: undefined,
|
|
164
|
+
hash: response.hash,
|
|
165
|
+
notModified: true,
|
|
166
|
+
message: response.message || 'Not modified'
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// Extract the specification from the response
|
|
171
|
+
const component = response.specification;
|
|
172
|
+
if (!component) {
|
|
173
|
+
throw new Error(`Component ${namespace}/${name} returned without specification`);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// Optional: Cache in database if configured
|
|
177
|
+
if (this.shouldCache(registry)) {
|
|
178
|
+
await this.cacheComponent(component, registryName, user);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// Return the ComponentSpec as a JSON string
|
|
182
|
+
return {
|
|
183
|
+
specification: JSON.stringify(component),
|
|
184
|
+
hash: response.hash,
|
|
185
|
+
notModified: false,
|
|
186
|
+
message: undefined
|
|
187
|
+
};
|
|
188
|
+
} catch (error) {
|
|
189
|
+
if (error instanceof RegistryError) {
|
|
190
|
+
// Log specific registry errors
|
|
191
|
+
LogError(`Registry error [${error.code}]: ${error.message}`);
|
|
192
|
+
if (error.code === RegistryErrorCode.COMPONENT_NOT_FOUND) {
|
|
193
|
+
// Return an error response structure
|
|
194
|
+
return {
|
|
195
|
+
specification: undefined,
|
|
196
|
+
hash: '',
|
|
197
|
+
notModified: false,
|
|
198
|
+
message: 'Component not found'
|
|
199
|
+
};
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
LogError(error);
|
|
203
|
+
throw error;
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* Search for components in registries
|
|
209
|
+
*/
|
|
210
|
+
@Query(() => RegistryComponentSearchResultType)
|
|
211
|
+
async SearchRegistryComponents(
|
|
212
|
+
@Arg('params') params: SearchRegistryComponentsInput,
|
|
213
|
+
@Ctx() { userPayload }: AppContext
|
|
214
|
+
): Promise<RegistryComponentSearchResultType> {
|
|
215
|
+
try {
|
|
216
|
+
// Get user from cache
|
|
217
|
+
const user = UserCache.Instance.Users.find((u) => u.Email.trim().toLowerCase() === userPayload.email?.trim().toLowerCase());
|
|
218
|
+
if (!user) throw new Error(`User ${userPayload.email} not found in UserCache`);
|
|
219
|
+
|
|
220
|
+
// If a specific registry is specified, use only that one
|
|
221
|
+
if (params.registryId) {
|
|
222
|
+
await this.checkUserAccess(user, params.registryId);
|
|
223
|
+
|
|
224
|
+
// Get registry and create client on-demand
|
|
225
|
+
const registry = await this.getRegistry(params.registryId, user);
|
|
226
|
+
if (!registry) {
|
|
227
|
+
throw new Error(`Registry not found: ${params.registryId}`);
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
const client = this.createClientForRegistry(registry);
|
|
231
|
+
|
|
232
|
+
const result = await client.searchComponents({
|
|
233
|
+
namespace: params.namespace,
|
|
234
|
+
query: params.query,
|
|
235
|
+
type: params.type,
|
|
236
|
+
tags: params.tags,
|
|
237
|
+
limit: params.limit || 10,
|
|
238
|
+
offset: params.offset || 0
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
return this.mapSearchResult(result);
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
// Otherwise, search across all active registries
|
|
245
|
+
const allResults: ComponentSpec[] = [];
|
|
246
|
+
|
|
247
|
+
// Get all active registries from database
|
|
248
|
+
await this.componentEngine.Config(false, user);
|
|
249
|
+
const activeRegistries = this.componentEngine.ComponentRegistries?.filter(
|
|
250
|
+
r => r.Status === 'Active'
|
|
251
|
+
) || [];
|
|
252
|
+
|
|
253
|
+
for (const registry of activeRegistries) {
|
|
254
|
+
try {
|
|
255
|
+
await this.checkUserAccess(user, registry.ID);
|
|
256
|
+
|
|
257
|
+
const client = this.createClientForRegistry(registry);
|
|
258
|
+
const result = await client.searchComponents({
|
|
259
|
+
namespace: params.namespace,
|
|
260
|
+
query: params.query,
|
|
261
|
+
type: params.type,
|
|
262
|
+
tags: params.tags,
|
|
263
|
+
limit: params.limit || 10,
|
|
264
|
+
offset: 0 // Reset offset for each registry
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
allResults.push(...result.components);
|
|
268
|
+
} catch (error) {
|
|
269
|
+
// Log but continue with other registries
|
|
270
|
+
LogError(`Failed to search registry ${registry.Name}: ${error}`);
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
// Apply pagination to combined results
|
|
275
|
+
const offset = params.offset || 0;
|
|
276
|
+
const limit = params.limit || 10;
|
|
277
|
+
const paginatedResults = allResults.slice(offset, offset + limit);
|
|
278
|
+
|
|
279
|
+
return {
|
|
280
|
+
components: paginatedResults.map(spec => JSON.stringify(spec)),
|
|
281
|
+
total: allResults.length,
|
|
282
|
+
offset,
|
|
283
|
+
limit
|
|
284
|
+
};
|
|
285
|
+
} catch (error) {
|
|
286
|
+
LogError(error);
|
|
287
|
+
throw error;
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
/**
|
|
292
|
+
* Resolve component dependencies
|
|
293
|
+
*/
|
|
294
|
+
@Query(() => ComponentDependencyTreeType, { nullable: true })
|
|
295
|
+
async ResolveComponentDependencies(
|
|
296
|
+
@Arg('registryName') registryName: string,
|
|
297
|
+
@Arg('componentId') componentId: string,
|
|
298
|
+
@Ctx() { userPayload }: AppContext
|
|
299
|
+
): Promise<ComponentDependencyTreeType | null> {
|
|
300
|
+
try {
|
|
301
|
+
// Get user from cache
|
|
302
|
+
const user = UserCache.Instance.Users.find((u) => u.Email.trim().toLowerCase() === userPayload.email?.trim().toLowerCase());
|
|
303
|
+
if (!user) throw new Error(`User ${userPayload.email} not found in UserCache`);
|
|
304
|
+
|
|
305
|
+
// Get registry to find its ID for permission check
|
|
306
|
+
const registry = await this.getRegistryByName(registryName, user);
|
|
307
|
+
if (!registry) {
|
|
308
|
+
throw new Error(`Registry not found: ${registryName}`);
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
await this.checkUserAccess(user, registry.ID);
|
|
312
|
+
|
|
313
|
+
// Create client on-demand
|
|
314
|
+
const client = this.createClientForRegistry(registry);
|
|
315
|
+
|
|
316
|
+
const tree = await client.resolveDependencies(componentId);
|
|
317
|
+
return tree as ComponentDependencyTreeType;
|
|
318
|
+
} catch (error) {
|
|
319
|
+
LogError(error);
|
|
320
|
+
throw error;
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
/**
|
|
325
|
+
* Check if user has access to a registry
|
|
326
|
+
*/
|
|
327
|
+
private async checkUserAccess(userInfo: UserInfo | undefined, registryId: string): Promise<void> {
|
|
328
|
+
// TODO: Implement actual permission checking
|
|
329
|
+
// For now, just ensure user is authenticated
|
|
330
|
+
if (!userInfo) {
|
|
331
|
+
throw new Error('User must be authenticated to access component registries');
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
/**
|
|
336
|
+
* Get registry entity from database by ID
|
|
337
|
+
*/
|
|
338
|
+
private async getRegistry(registryId: string, userInfo: UserInfo): Promise<ComponentRegistryEntity | null> {
|
|
339
|
+
try {
|
|
340
|
+
await this.componentEngine.Config(false, userInfo);
|
|
341
|
+
|
|
342
|
+
const registry = this.componentEngine.ComponentRegistries?.find(
|
|
343
|
+
r => r.ID === registryId
|
|
344
|
+
);
|
|
345
|
+
|
|
346
|
+
return registry || null;
|
|
347
|
+
} catch (error) {
|
|
348
|
+
LogError(error);
|
|
349
|
+
return null;
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
/**
|
|
354
|
+
* Get registry entity from database by Name
|
|
355
|
+
*/
|
|
356
|
+
private async getRegistryByName(registryName: string, userInfo: UserInfo): Promise<ComponentRegistryEntity | null> {
|
|
357
|
+
try {
|
|
358
|
+
await this.componentEngine.Config(false, userInfo);
|
|
359
|
+
|
|
360
|
+
const registry = this.componentEngine.ComponentRegistries?.find(
|
|
361
|
+
r => r.Name === registryName && r.Status === 'Active'
|
|
362
|
+
);
|
|
363
|
+
|
|
364
|
+
return registry || null;
|
|
365
|
+
} catch (error) {
|
|
366
|
+
LogError(error);
|
|
367
|
+
return null;
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
/**
|
|
372
|
+
* Get the registry URI, checking for environment variable override first
|
|
373
|
+
* Environment variable format: REGISTRY_URI_OVERRIDE_<REGISTRY_NAME>
|
|
374
|
+
* Example: REGISTRY_URI_OVERRIDE_MJ_CENTRAL=http://localhost:8080
|
|
375
|
+
*/
|
|
376
|
+
private getRegistryUri(registry: ComponentRegistryEntity): string {
|
|
377
|
+
if (!registry.Name) {
|
|
378
|
+
return registry.URI || '';
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
// Convert registry name to environment variable format
|
|
382
|
+
// Replace spaces, hyphens, and other non-alphanumeric chars with underscores
|
|
383
|
+
const envVarName = `REGISTRY_URI_OVERRIDE_${registry.Name.replace(/[^A-Za-z0-9]/g, '_').toUpperCase()}`;
|
|
384
|
+
|
|
385
|
+
// Check for environment variable override
|
|
386
|
+
const override = process.env[envVarName];
|
|
387
|
+
if (override) {
|
|
388
|
+
LogStatus(`Using URI override for registry ${registry.Name}: ${override}`);
|
|
389
|
+
return override;
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
// Use production URI from database
|
|
393
|
+
return registry.URI || '';
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
/**
|
|
397
|
+
* Create a client for a registry on-demand
|
|
398
|
+
* Checks configuration first, then falls back to default settings
|
|
399
|
+
*/
|
|
400
|
+
private createClientForRegistry(registry: ComponentRegistryEntity): ComponentRegistryClient {
|
|
401
|
+
// Check if there's a configuration for this registry
|
|
402
|
+
const config = configInfo.componentRegistries?.find(r =>
|
|
403
|
+
r.id === registry.ID || r.name === registry.Name
|
|
404
|
+
);
|
|
405
|
+
|
|
406
|
+
// Get API key from environment or config
|
|
407
|
+
const apiKey = process.env[`REGISTRY_API_KEY_${registry.ID.replace(/-/g, '_').toUpperCase()}`] ||
|
|
408
|
+
process.env[`REGISTRY_API_KEY_${registry.Name?.replace(/-/g, '_').toUpperCase()}`] ||
|
|
409
|
+
config?.apiKey;
|
|
410
|
+
|
|
411
|
+
// Get the registry URI (with possible override)
|
|
412
|
+
const baseUrl = this.getRegistryUri(registry);
|
|
413
|
+
|
|
414
|
+
// Build retry policy with defaults
|
|
415
|
+
const retryPolicy = {
|
|
416
|
+
maxRetries: config?.retryPolicy?.maxRetries ?? 3,
|
|
417
|
+
initialDelay: config?.retryPolicy?.initialDelay ?? 1000,
|
|
418
|
+
maxDelay: config?.retryPolicy?.maxDelay ?? 10000,
|
|
419
|
+
backoffMultiplier: config?.retryPolicy?.backoffMultiplier ?? 2
|
|
420
|
+
};
|
|
421
|
+
|
|
422
|
+
// Use config settings if available, otherwise defaults
|
|
423
|
+
return new ComponentRegistryClient({
|
|
424
|
+
baseUrl: baseUrl,
|
|
425
|
+
apiKey: apiKey,
|
|
426
|
+
timeout: config?.timeout || 30000,
|
|
427
|
+
retryPolicy: retryPolicy,
|
|
428
|
+
headers: config?.headers
|
|
429
|
+
});
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
/**
|
|
433
|
+
* Check if component should be cached
|
|
434
|
+
*/
|
|
435
|
+
private shouldCache(registry: ComponentRegistryEntity): boolean {
|
|
436
|
+
// Check config for caching settings
|
|
437
|
+
const config = configInfo.componentRegistries?.find(r =>
|
|
438
|
+
r.id === registry.ID || r.name === registry.Name
|
|
439
|
+
);
|
|
440
|
+
return config?.cache !== false; // Cache by default
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
/**
|
|
444
|
+
* Cache component in database
|
|
445
|
+
*/
|
|
446
|
+
private async cacheComponent(
|
|
447
|
+
component: ComponentSpec,
|
|
448
|
+
registryId: string,
|
|
449
|
+
userInfo: UserInfo
|
|
450
|
+
): Promise<void> {
|
|
451
|
+
try {
|
|
452
|
+
// Find or create component entity
|
|
453
|
+
const md = new Metadata();
|
|
454
|
+
const componentEntity = await md.GetEntityObject<ComponentEntity>('MJ: Components', userInfo);
|
|
455
|
+
|
|
456
|
+
// Check if component already exists
|
|
457
|
+
const existingComponent = this.componentEngine.Components?.find(
|
|
458
|
+
c => c.Name === component.name &&
|
|
459
|
+
c.Namespace === component.namespace &&
|
|
460
|
+
c.SourceRegistryID === registryId
|
|
461
|
+
);
|
|
462
|
+
|
|
463
|
+
if (existingComponent) {
|
|
464
|
+
// Update existing component
|
|
465
|
+
if (!await componentEntity.Load(existingComponent.ID)) {
|
|
466
|
+
throw new Error(`Failed to load component: ${existingComponent.ID}`);
|
|
467
|
+
}
|
|
468
|
+
} else {
|
|
469
|
+
// Create new component
|
|
470
|
+
componentEntity.NewRecord();
|
|
471
|
+
componentEntity.SourceRegistryID = registryId;
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
// Update component fields
|
|
475
|
+
componentEntity.Name = component.name;
|
|
476
|
+
componentEntity.Namespace = component.namespace || '';
|
|
477
|
+
componentEntity.Version = component.version || '1.0.0';
|
|
478
|
+
componentEntity.Title = component.title;
|
|
479
|
+
componentEntity.Description = component.description;
|
|
480
|
+
componentEntity.Type = this.mapComponentType(component.type);
|
|
481
|
+
componentEntity.FunctionalRequirements = component.functionalRequirements;
|
|
482
|
+
componentEntity.TechnicalDesign = component.technicalDesign;
|
|
483
|
+
componentEntity.Specification = JSON.stringify(component);
|
|
484
|
+
componentEntity.LastSyncedAt = new Date();
|
|
485
|
+
|
|
486
|
+
if (!existingComponent) {
|
|
487
|
+
componentEntity.ReplicatedAt = new Date();
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
// Save component
|
|
491
|
+
const result = await componentEntity.Save();
|
|
492
|
+
if (!result) {
|
|
493
|
+
throw new Error(`Failed to cache component: ${component.name}`);
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
// Refresh metadata cache
|
|
497
|
+
await this.componentEngine.Config(true, userInfo);
|
|
498
|
+
} catch (error) {
|
|
499
|
+
// Log but don't throw - caching failure shouldn't break the operation
|
|
500
|
+
LogError('Failed to cache component:');
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
/**
|
|
505
|
+
* Map component type string to entity enum
|
|
506
|
+
*/
|
|
507
|
+
private mapComponentType(type: string): ComponentEntity['Type'] {
|
|
508
|
+
const typeMap: Record<string, ComponentEntity['Type']> = {
|
|
509
|
+
'report': 'Report',
|
|
510
|
+
'dashboard': 'Dashboard',
|
|
511
|
+
'form': 'Form',
|
|
512
|
+
'table': 'Table',
|
|
513
|
+
'chart': 'Chart',
|
|
514
|
+
'navigation': 'Navigation',
|
|
515
|
+
'search': 'Search',
|
|
516
|
+
'widget': 'Widget',
|
|
517
|
+
'utility': 'Utility',
|
|
518
|
+
'other': 'Other'
|
|
519
|
+
};
|
|
520
|
+
|
|
521
|
+
return typeMap[type.toLowerCase()] || 'Other';
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
/**
|
|
525
|
+
* Map search result to GraphQL type
|
|
526
|
+
*/
|
|
527
|
+
private mapSearchResult(result: ComponentSearchResult): RegistryComponentSearchResultType {
|
|
528
|
+
return {
|
|
529
|
+
components: result.components.map(spec => JSON.stringify(spec)),
|
|
530
|
+
total: result.total,
|
|
531
|
+
offset: result.offset,
|
|
532
|
+
limit: result.limit
|
|
533
|
+
};
|
|
534
|
+
}
|
|
535
|
+
}
|