@memberjunction/server 2.98.0 → 2.100.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/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/generated/generated.d.ts +6 -0
- package/dist/generated/generated.d.ts.map +1 -1
- package/dist/generated/generated.js +32 -0
- package/dist/generated/generated.js.map +1 -1
- package/dist/generic/RunViewResolver.d.ts +5 -3
- package/dist/generic/RunViewResolver.d.ts.map +1 -1
- package/dist/generic/RunViewResolver.js +30 -18
- package/dist/generic/RunViewResolver.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 +4 -0
- package/dist/resolvers/AskSkipResolver.d.ts.map +1 -1
- package/dist/resolvers/AskSkipResolver.js +11 -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/dist/resolvers/FileResolver.js +2 -2
- package/dist/resolvers/FileResolver.js.map +1 -1
- package/package.json +40 -40
- package/src/config.ts +17 -0
- package/src/generated/generated.ts +20 -0
- package/src/generic/RunViewResolver.ts +30 -15
- package/src/index.ts +1 -0
- package/src/resolvers/AskSkipResolver.ts +15 -6
- package/src/resolvers/ComponentRegistryResolver.ts +535 -0
- package/src/resolvers/FileResolver.ts +2 -2
|
@@ -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
|
+
}
|
|
@@ -107,7 +107,7 @@ export class FileResolver extends FileResolverBase {
|
|
|
107
107
|
@PubSub() pubSub: PubSubEngine
|
|
108
108
|
) {
|
|
109
109
|
// if the name is changing, rename the target object as well
|
|
110
|
-
const md = GetReadOnlyProvider(context.providers);
|
|
110
|
+
const md = GetReadOnlyProvider(context.providers, {allowFallbackToReadWrite: true});
|
|
111
111
|
const user = this.GetUserFromPayload(context.userPayload);
|
|
112
112
|
const fileEntity = await md.GetEntityObject<FileEntity>('Files', user);
|
|
113
113
|
fileEntity.CheckPermissions(EntityPermissionType.Update, true);
|
|
@@ -135,7 +135,7 @@ export class FileResolver extends FileResolverBase {
|
|
|
135
135
|
@Ctx() context: AppContext,
|
|
136
136
|
@PubSub() pubSub: PubSubEngine
|
|
137
137
|
) {
|
|
138
|
-
const md = GetReadOnlyProvider(context.providers);
|
|
138
|
+
const md = GetReadOnlyProvider(context.providers, {allowFallbackToReadWrite: true});
|
|
139
139
|
const userInfo = this.GetUserFromPayload(context.userPayload);
|
|
140
140
|
|
|
141
141
|
const fileEntity = await md.GetEntityObject<FileEntity>('Files', userInfo);
|