@memberjunction/react-runtime 2.112.0 → 2.113.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/.turbo/turbo-build.log +11 -10
- package/CHANGELOG.md +15 -1
- package/dist/compiler/component-compiler.d.ts.map +1 -1
- package/dist/compiler/component-compiler.js +56 -66
- package/dist/compiler/component-compiler.js.map +1 -1
- package/dist/component-manager/component-manager.d.ts.map +1 -1
- package/dist/component-manager/component-manager.js +42 -48
- package/dist/component-manager/component-manager.js.map +1 -1
- package/dist/component-manager/types.d.ts +1 -1
- package/dist/component-manager/types.d.ts.map +1 -1
- package/dist/component-manager/types.js.map +1 -1
- package/dist/registry/component-registry-service.d.ts +1 -1
- package/dist/registry/component-registry-service.d.ts.map +1 -1
- package/dist/registry/component-registry-service.js +34 -34
- package/dist/registry/component-registry-service.js.map +1 -1
- package/dist/registry/component-resolver.d.ts +1 -1
- package/dist/registry/component-resolver.d.ts.map +1 -1
- package/dist/registry/component-resolver.js +11 -10
- package/dist/registry/component-resolver.js.map +1 -1
- package/dist/runtime/component-hierarchy.d.ts +1 -1
- package/dist/runtime/component-hierarchy.d.ts.map +1 -1
- package/dist/runtime/component-hierarchy.js +43 -43
- package/dist/runtime/component-hierarchy.js.map +1 -1
- package/dist/runtime/prop-builder.d.ts.map +1 -1
- package/dist/runtime/prop-builder.js +24 -22
- package/dist/runtime/prop-builder.js.map +1 -1
- package/dist/runtime.umd.js +511 -494
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/index.js.map +1 -1
- package/dist/utilities/library-registry.d.ts +1 -1
- package/dist/utilities/library-registry.d.ts.map +1 -1
- package/dist/utilities/library-registry.js +10 -10
- package/dist/utilities/library-registry.js.map +1 -1
- package/package.json +6 -5
- package/src/compiler/component-compiler.ts +164 -186
- package/src/component-manager/component-manager.ts +216 -162
- package/src/component-manager/types.ts +27 -27
- package/src/registry/component-registry-service.ts +218 -190
- package/src/registry/component-resolver.ts +98 -87
- package/src/runtime/component-hierarchy.ts +94 -57
- package/src/runtime/prop-builder.ts +33 -28
- package/src/types/index.ts +4 -3
- package/src/utilities/library-registry.ts +19 -14
|
@@ -6,20 +6,20 @@
|
|
|
6
6
|
import { ComponentSpec, ComponentObject } from '@memberjunction/interactive-component-types';
|
|
7
7
|
import { ComponentCompiler } from '../compiler';
|
|
8
8
|
import { RuntimeContext } from '../types';
|
|
9
|
-
import {
|
|
10
|
-
RegistryProvider,
|
|
9
|
+
import {
|
|
10
|
+
RegistryProvider,
|
|
11
11
|
RegistryComponentResponse,
|
|
12
12
|
ComponentDependencyInfo,
|
|
13
|
-
DependencyTree,
|
|
14
|
-
RegistryComponentMetadata
|
|
13
|
+
DependencyTree,
|
|
14
|
+
RegistryComponentMetadata
|
|
15
15
|
} from './registry-provider';
|
|
16
|
-
import { UserInfo, Metadata } from '@memberjunction/
|
|
17
|
-
import {
|
|
18
|
-
ComponentEntity,
|
|
16
|
+
import { UserInfo, Metadata } from '@memberjunction/core';
|
|
17
|
+
import {
|
|
18
|
+
ComponentEntity,
|
|
19
19
|
ComponentRegistryEntity,
|
|
20
20
|
ComponentDependencyEntity,
|
|
21
21
|
ComponentLibraryLinkEntity,
|
|
22
|
-
ComponentMetadataEngine
|
|
22
|
+
ComponentMetadataEngine
|
|
23
23
|
} from '@memberjunction/core-entities';
|
|
24
24
|
|
|
25
25
|
// Type-only import for TypeScript - won't be included in UMD bundle
|
|
@@ -28,6 +28,7 @@ import type { GraphQLDataProvider } from '@memberjunction/graphql-dataprovider';
|
|
|
28
28
|
// Dynamic import of GraphQLComponentRegistryClient to avoid breaking UMD build
|
|
29
29
|
let GraphQLComponentRegistryClient: any;
|
|
30
30
|
|
|
31
|
+
|
|
31
32
|
/**
|
|
32
33
|
* Cached compiled component with metadata
|
|
33
34
|
*/
|
|
@@ -37,7 +38,7 @@ interface CachedCompiledComponent {
|
|
|
37
38
|
compiledAt: Date;
|
|
38
39
|
lastUsed: Date;
|
|
39
40
|
useCount: number;
|
|
40
|
-
specHash?: string;
|
|
41
|
+
specHash?: string; // SHA-256 hash of the spec used for compilation
|
|
41
42
|
}
|
|
42
43
|
|
|
43
44
|
/**
|
|
@@ -58,11 +59,11 @@ export interface IComponentRegistryClient {
|
|
|
58
59
|
*/
|
|
59
60
|
export class ComponentRegistryService {
|
|
60
61
|
private static instance: ComponentRegistryService | null = null;
|
|
61
|
-
|
|
62
|
+
|
|
62
63
|
// Caches
|
|
63
64
|
private compiledComponentCache = new Map<string, CachedCompiledComponent>();
|
|
64
65
|
private componentReferences = new Map<string, Set<string>>();
|
|
65
|
-
|
|
66
|
+
|
|
66
67
|
// Dependencies
|
|
67
68
|
private compiler: ComponentCompiler;
|
|
68
69
|
private runtimeContext: RuntimeContext;
|
|
@@ -70,7 +71,7 @@ export class ComponentRegistryService {
|
|
|
70
71
|
private registryProviders = new Map<string, RegistryProvider>();
|
|
71
72
|
private debug: boolean = false;
|
|
72
73
|
private graphQLClient?: IComponentRegistryClient;
|
|
73
|
-
|
|
74
|
+
|
|
74
75
|
private constructor(
|
|
75
76
|
compiler: ComponentCompiler,
|
|
76
77
|
runtimeContext: RuntimeContext,
|
|
@@ -82,12 +83,12 @@ export class ComponentRegistryService {
|
|
|
82
83
|
this.debug = debug;
|
|
83
84
|
this.graphQLClient = graphQLClient;
|
|
84
85
|
}
|
|
85
|
-
|
|
86
|
+
|
|
86
87
|
/**
|
|
87
88
|
* Get or create the singleton instance
|
|
88
89
|
*/
|
|
89
90
|
static getInstance(
|
|
90
|
-
compiler: ComponentCompiler,
|
|
91
|
+
compiler: ComponentCompiler,
|
|
91
92
|
context: RuntimeContext,
|
|
92
93
|
debug: boolean = false,
|
|
93
94
|
graphQLClient?: IComponentRegistryClient
|
|
@@ -97,7 +98,7 @@ export class ComponentRegistryService {
|
|
|
97
98
|
}
|
|
98
99
|
return ComponentRegistryService.instance;
|
|
99
100
|
}
|
|
100
|
-
|
|
101
|
+
|
|
101
102
|
/**
|
|
102
103
|
* Set the GraphQL client for registry operations
|
|
103
104
|
*/
|
|
@@ -107,12 +108,12 @@ export class ComponentRegistryService {
|
|
|
107
108
|
console.log('✅ GraphQL client configured for component registry');
|
|
108
109
|
}
|
|
109
110
|
}
|
|
110
|
-
|
|
111
|
+
|
|
111
112
|
/**
|
|
112
113
|
* Cached GraphQL client instance created from Metadata.Provider
|
|
113
114
|
*/
|
|
114
115
|
private cachedProviderClient: IComponentRegistryClient | null = null;
|
|
115
|
-
|
|
116
|
+
|
|
116
117
|
/**
|
|
117
118
|
* Get the GraphQL client, using the provided one or falling back to creating one with Metadata.Provider
|
|
118
119
|
* @returns The GraphQL client if available
|
|
@@ -122,12 +123,12 @@ export class ComponentRegistryService {
|
|
|
122
123
|
if (this.graphQLClient) {
|
|
123
124
|
return this.graphQLClient;
|
|
124
125
|
}
|
|
125
|
-
|
|
126
|
+
|
|
126
127
|
// If we've already created one from the provider, reuse it
|
|
127
128
|
if (this.cachedProviderClient) {
|
|
128
129
|
return this.cachedProviderClient;
|
|
129
130
|
}
|
|
130
|
-
|
|
131
|
+
|
|
131
132
|
// Try to create GraphQLComponentRegistryClient with Metadata.Provider
|
|
132
133
|
try {
|
|
133
134
|
const provider = Metadata?.Provider;
|
|
@@ -144,7 +145,7 @@ export class ComponentRegistryService {
|
|
|
144
145
|
return null;
|
|
145
146
|
}
|
|
146
147
|
}
|
|
147
|
-
|
|
148
|
+
|
|
148
149
|
// Create the client if we have the class
|
|
149
150
|
if (GraphQLComponentRegistryClient) {
|
|
150
151
|
try {
|
|
@@ -167,10 +168,10 @@ export class ComponentRegistryService {
|
|
|
167
168
|
console.log('⚠️ [ComponentRegistryService] Could not access Metadata.Provider:', error);
|
|
168
169
|
}
|
|
169
170
|
}
|
|
170
|
-
|
|
171
|
+
|
|
171
172
|
return null;
|
|
172
173
|
}
|
|
173
|
-
|
|
174
|
+
|
|
174
175
|
/**
|
|
175
176
|
* Initialize the service with metadata
|
|
176
177
|
*/
|
|
@@ -178,7 +179,7 @@ export class ComponentRegistryService {
|
|
|
178
179
|
// Initialize metadata engine
|
|
179
180
|
await this.componentEngine.Config(false, contextUser);
|
|
180
181
|
}
|
|
181
|
-
|
|
182
|
+
|
|
182
183
|
/**
|
|
183
184
|
* Calculate SHA-256 hash of a component spec for cache comparison
|
|
184
185
|
* Uses Web Crypto API which is available in modern browsers and Node.js 15+
|
|
@@ -188,88 +189,93 @@ export class ComponentRegistryService {
|
|
|
188
189
|
if (typeof crypto === 'undefined' || !crypto.subtle) {
|
|
189
190
|
throw new Error(
|
|
190
191
|
'Web Crypto API not available. This typically happens when running in an insecure context. ' +
|
|
191
|
-
|
|
192
|
-
|
|
192
|
+
'Please use HTTPS or localhost for development. ' +
|
|
193
|
+
'Note: crypto.subtle is available in Node.js 15+ and all modern browsers on secure contexts.'
|
|
193
194
|
);
|
|
194
195
|
}
|
|
195
|
-
|
|
196
|
+
|
|
196
197
|
const specString = JSON.stringify(spec);
|
|
197
198
|
const encoder = new TextEncoder();
|
|
198
199
|
const data = encoder.encode(specString);
|
|
199
|
-
|
|
200
|
+
|
|
200
201
|
// Calculate SHA-256 hash using Web Crypto API
|
|
201
202
|
const hashBuffer = await crypto.subtle.digest('SHA-256', data);
|
|
202
|
-
|
|
203
|
+
|
|
203
204
|
// Convert ArrayBuffer to hex string
|
|
204
205
|
const hashArray = Array.from(new Uint8Array(hashBuffer));
|
|
205
|
-
const hashHex = hashArray.map(
|
|
206
|
-
|
|
206
|
+
const hashHex = hashArray.map(b => b.toString(16).padStart(2, '0')).join('');
|
|
207
|
+
|
|
207
208
|
return hashHex;
|
|
208
209
|
}
|
|
209
|
-
|
|
210
|
+
|
|
210
211
|
/**
|
|
211
212
|
* Get a compiled component, using cache if available
|
|
212
213
|
*/
|
|
213
|
-
async getCompiledComponent(
|
|
214
|
+
async getCompiledComponent(
|
|
215
|
+
componentId: string,
|
|
216
|
+
referenceId?: string,
|
|
217
|
+
contextUser?: UserInfo
|
|
218
|
+
): Promise<ComponentObject> {
|
|
214
219
|
await this.initialize(contextUser);
|
|
215
|
-
|
|
220
|
+
|
|
216
221
|
// Find component in metadata
|
|
217
222
|
const component = this.componentEngine.Components.find((c: ComponentEntity) => c.ID === componentId);
|
|
218
223
|
if (!component) {
|
|
219
224
|
throw new Error(`Component not found: ${componentId}`);
|
|
220
225
|
}
|
|
221
|
-
|
|
226
|
+
|
|
222
227
|
const key = this.getComponentKey(component.Name, component.Namespace, component.Version, component.SourceRegistryID);
|
|
223
|
-
|
|
228
|
+
|
|
224
229
|
// Check if already compiled and cached
|
|
225
230
|
if (this.compiledComponentCache.has(key)) {
|
|
226
231
|
const cached = this.compiledComponentCache.get(key)!;
|
|
227
232
|
cached.lastUsed = new Date();
|
|
228
233
|
cached.useCount++;
|
|
229
|
-
|
|
234
|
+
|
|
230
235
|
// Track reference if provided
|
|
231
236
|
if (referenceId) {
|
|
232
237
|
this.addComponentReference(key, referenceId);
|
|
233
238
|
}
|
|
234
|
-
|
|
239
|
+
|
|
235
240
|
if (this.debug) {
|
|
236
241
|
console.log(`✅ Reusing compiled component from cache: ${key} (use count: ${cached.useCount})`);
|
|
237
242
|
}
|
|
238
243
|
// Call the factory function to get the ComponentObject
|
|
239
244
|
return cached.component(this.runtimeContext);
|
|
240
245
|
}
|
|
241
|
-
|
|
246
|
+
|
|
242
247
|
// Not in cache, need to load and compile
|
|
243
248
|
if (this.debug) {
|
|
244
249
|
console.log(`🔄 Loading and compiling component: ${key}`);
|
|
245
250
|
}
|
|
246
|
-
|
|
251
|
+
|
|
247
252
|
// Get the component specification
|
|
248
253
|
const spec = await this.getComponentSpec(componentId, contextUser);
|
|
249
|
-
|
|
254
|
+
|
|
250
255
|
// Compile the component
|
|
251
256
|
// Load all libraries from metadata engine
|
|
252
257
|
const allLibraries = this.componentEngine.ComponentLibraries || [];
|
|
253
|
-
|
|
258
|
+
|
|
254
259
|
let compilationResult;
|
|
255
260
|
try {
|
|
256
261
|
compilationResult = await this.compiler.compile({
|
|
257
262
|
componentName: component.Name,
|
|
258
263
|
componentCode: spec.code,
|
|
259
264
|
libraries: spec.libraries,
|
|
260
|
-
allLibraries
|
|
265
|
+
allLibraries
|
|
261
266
|
});
|
|
262
|
-
}
|
|
267
|
+
}
|
|
268
|
+
catch (compileEx) {
|
|
263
269
|
// log then throw
|
|
264
|
-
console.error(`🔴 Error compiling component ${component.Name}`,
|
|
270
|
+
console.error(`🔴 Error compiling component ${component.Name}`,compileEx);
|
|
265
271
|
throw compileEx;
|
|
266
272
|
}
|
|
267
|
-
|
|
273
|
+
|
|
268
274
|
if (!compilationResult.success) {
|
|
269
275
|
console.error(`🔴 Error compiling component ${component.Name}`, compilationResult, 'Code', spec.code);
|
|
270
276
|
throw new Error(`Failed to compile component ${component.Name}: ${compilationResult.error}`);
|
|
271
277
|
}
|
|
272
|
-
|
|
278
|
+
|
|
273
279
|
// Cache the compiled component
|
|
274
280
|
const metadata: RegistryComponentMetadata = {
|
|
275
281
|
name: component.Name,
|
|
@@ -284,9 +290,9 @@ export class ComponentRegistryService {
|
|
|
284
290
|
libraries: spec.libraries,
|
|
285
291
|
dependencies: spec.dependencies,
|
|
286
292
|
sourceRegistryID: component.SourceRegistryID,
|
|
287
|
-
isLocal: !component.SourceRegistryID
|
|
293
|
+
isLocal: !component.SourceRegistryID
|
|
288
294
|
};
|
|
289
|
-
|
|
295
|
+
|
|
290
296
|
if (!compilationResult.component) {
|
|
291
297
|
throw new Error(`Component compilation succeeded but no component returned`);
|
|
292
298
|
}
|
|
@@ -296,18 +302,18 @@ export class ComponentRegistryService {
|
|
|
296
302
|
metadata,
|
|
297
303
|
compiledAt: new Date(),
|
|
298
304
|
lastUsed: new Date(),
|
|
299
|
-
useCount: 1
|
|
305
|
+
useCount: 1
|
|
300
306
|
});
|
|
301
|
-
|
|
307
|
+
|
|
302
308
|
// Track reference
|
|
303
309
|
if (referenceId) {
|
|
304
310
|
this.addComponentReference(key, referenceId);
|
|
305
311
|
}
|
|
306
|
-
|
|
312
|
+
|
|
307
313
|
// Call the factory function to get the ComponentObject
|
|
308
314
|
return compiledComponentFactory(this.runtimeContext);
|
|
309
315
|
}
|
|
310
|
-
|
|
316
|
+
|
|
311
317
|
/**
|
|
312
318
|
* Get compiled component from external registry by registry name
|
|
313
319
|
* This is used when spec.registry field is populated
|
|
@@ -321,45 +327,45 @@ export class ComponentRegistryService {
|
|
|
321
327
|
contextUser?: UserInfo
|
|
322
328
|
): Promise<any> {
|
|
323
329
|
await this.initialize(contextUser);
|
|
324
|
-
|
|
330
|
+
|
|
325
331
|
if (this.debug) {
|
|
326
332
|
console.log(`🌐 [ComponentRegistryService] Fetching from external registry: ${registryName}/${namespace}/${name}@${version}`);
|
|
327
333
|
}
|
|
328
|
-
|
|
334
|
+
|
|
329
335
|
// Find the registry by name in ComponentRegistries
|
|
330
|
-
const registry = this.componentEngine.ComponentRegistries?.find(
|
|
331
|
-
|
|
336
|
+
const registry = this.componentEngine.ComponentRegistries?.find(
|
|
337
|
+
r => r.Name === registryName && r.Status === 'Active'
|
|
338
|
+
);
|
|
339
|
+
|
|
332
340
|
if (!registry) {
|
|
333
341
|
throw new Error(`Registry not found or inactive: ${registryName}`);
|
|
334
342
|
}
|
|
335
|
-
|
|
343
|
+
|
|
336
344
|
if (this.debug) {
|
|
337
345
|
console.log(`✅ [ComponentRegistryService] Found registry: ${registry.Name} (ID: ${registry.ID})`);
|
|
338
346
|
}
|
|
339
|
-
|
|
347
|
+
|
|
340
348
|
// Get GraphQL client - use provided one or fallback to Metadata.Provider
|
|
341
349
|
const graphQLClient = await this.getGraphQLClient();
|
|
342
350
|
if (!graphQLClient) {
|
|
343
|
-
throw new Error(
|
|
344
|
-
'GraphQL client not available for external registry fetching. No client provided and Metadata.Provider is not a GraphQLDataProvider.'
|
|
345
|
-
);
|
|
351
|
+
throw new Error('GraphQL client not available for external registry fetching. No client provided and Metadata.Provider is not a GraphQLDataProvider.');
|
|
346
352
|
}
|
|
347
|
-
|
|
353
|
+
|
|
348
354
|
// Check if we have a cached version first
|
|
349
355
|
const key = `external:${registryName}:${namespace}:${name}:${version}`;
|
|
350
356
|
const cached = this.compiledComponentCache.get(key);
|
|
351
|
-
|
|
357
|
+
|
|
352
358
|
try {
|
|
353
359
|
// Fetch component spec from external registry via MJServer
|
|
354
360
|
// Pass cached hash if available for efficient caching
|
|
355
361
|
const spec = await graphQLClient.GetRegistryComponent({
|
|
356
|
-
registryName: registry.Name,
|
|
362
|
+
registryName: registry.Name, // Pass registry name, not ID
|
|
357
363
|
namespace,
|
|
358
364
|
name,
|
|
359
365
|
version,
|
|
360
|
-
hash: cached?.specHash
|
|
366
|
+
hash: cached?.specHash // Pass cached hash if available
|
|
361
367
|
});
|
|
362
|
-
|
|
368
|
+
|
|
363
369
|
// If null returned, it means not modified (304)
|
|
364
370
|
if (!spec && cached?.specHash) {
|
|
365
371
|
if (this.debug) {
|
|
@@ -367,27 +373,27 @@ export class ComponentRegistryService {
|
|
|
367
373
|
}
|
|
368
374
|
cached.lastUsed = new Date();
|
|
369
375
|
cached.useCount++;
|
|
370
|
-
|
|
376
|
+
|
|
371
377
|
// Track reference
|
|
372
378
|
if (referenceId) {
|
|
373
379
|
this.addComponentReference(key, referenceId);
|
|
374
380
|
}
|
|
375
|
-
|
|
381
|
+
|
|
376
382
|
// Call the factory function to get the ComponentObject
|
|
377
383
|
return cached.component(this.runtimeContext);
|
|
378
384
|
}
|
|
379
|
-
|
|
385
|
+
|
|
380
386
|
if (!spec) {
|
|
381
387
|
throw new Error(`Component not found in registry ${registryName}: ${namespace}/${name}@${version}`);
|
|
382
388
|
}
|
|
383
|
-
|
|
389
|
+
|
|
384
390
|
if (this.debug) {
|
|
385
391
|
console.log(`✅ [ComponentRegistryService] Fetched spec from external registry: ${spec.name}`);
|
|
386
392
|
}
|
|
387
|
-
|
|
393
|
+
|
|
388
394
|
// Calculate hash of the fetched spec
|
|
389
395
|
const specHash = await this.calculateSpecHash(spec);
|
|
390
|
-
|
|
396
|
+
|
|
391
397
|
// Check if hash matches cached version (shouldn't happen if server works correctly)
|
|
392
398
|
if (cached && cached.specHash === specHash) {
|
|
393
399
|
if (this.debug) {
|
|
@@ -395,37 +401,35 @@ export class ComponentRegistryService {
|
|
|
395
401
|
}
|
|
396
402
|
cached.lastUsed = new Date();
|
|
397
403
|
cached.useCount++;
|
|
398
|
-
|
|
404
|
+
|
|
399
405
|
// Track reference
|
|
400
406
|
if (referenceId) {
|
|
401
407
|
this.addComponentReference(key, referenceId);
|
|
402
408
|
}
|
|
403
|
-
|
|
409
|
+
|
|
404
410
|
// Call the factory function to get the ComponentObject
|
|
405
411
|
return cached.component(this.runtimeContext);
|
|
406
412
|
}
|
|
407
|
-
|
|
413
|
+
|
|
408
414
|
// Spec has changed or is new, need to compile
|
|
409
415
|
if (cached && this.debug) {
|
|
410
|
-
console.log(
|
|
411
|
-
`🔄 [ComponentRegistryService] Spec changed for: ${key}, recompiling (old hash: ${cached.specHash?.substring(0, 8)}..., new hash: ${specHash.substring(0, 8)}...)`
|
|
412
|
-
);
|
|
416
|
+
console.log(`🔄 [ComponentRegistryService] Spec changed for: ${key}, recompiling (old hash: ${cached.specHash?.substring(0, 8)}..., new hash: ${specHash.substring(0, 8)}...)`);
|
|
413
417
|
}
|
|
414
|
-
|
|
418
|
+
|
|
415
419
|
// Load all libraries from metadata engine
|
|
416
420
|
const allLibraries = this.componentEngine.ComponentLibraries || [];
|
|
417
|
-
|
|
421
|
+
|
|
418
422
|
// Compile the component
|
|
419
423
|
const compilationResult = await this.compiler.compile({
|
|
420
424
|
componentName: spec.name,
|
|
421
425
|
componentCode: spec.code || '',
|
|
422
|
-
allLibraries: allLibraries
|
|
426
|
+
allLibraries: allLibraries
|
|
423
427
|
});
|
|
424
|
-
|
|
428
|
+
|
|
425
429
|
if (!compilationResult.success || !compilationResult.component) {
|
|
426
430
|
throw new Error(`Failed to compile component: ${compilationResult.error?.message || 'Unknown error'}`);
|
|
427
431
|
}
|
|
428
|
-
|
|
432
|
+
|
|
429
433
|
// Cache the compiled component with spec hash
|
|
430
434
|
this.compiledComponentCache.set(key, {
|
|
431
435
|
component: compilationResult.component.factory,
|
|
@@ -435,23 +439,23 @@ export class ComponentRegistryService {
|
|
|
435
439
|
version: spec.version || '1.0.0',
|
|
436
440
|
description: spec.description || '',
|
|
437
441
|
type: spec.type,
|
|
438
|
-
isLocal: false
|
|
442
|
+
isLocal: false // This is from an external registry
|
|
439
443
|
},
|
|
440
444
|
compiledAt: new Date(),
|
|
441
445
|
lastUsed: new Date(),
|
|
442
446
|
useCount: 1,
|
|
443
|
-
specHash: specHash
|
|
447
|
+
specHash: specHash // Store the hash for future comparison
|
|
444
448
|
});
|
|
445
|
-
|
|
449
|
+
|
|
446
450
|
// Track reference
|
|
447
451
|
if (referenceId) {
|
|
448
452
|
this.addComponentReference(key, referenceId);
|
|
449
453
|
}
|
|
450
|
-
|
|
454
|
+
|
|
451
455
|
if (this.debug) {
|
|
452
456
|
console.log(`🎯 [ComponentRegistryService] Successfully compiled external component: ${spec.name}`);
|
|
453
457
|
}
|
|
454
|
-
|
|
458
|
+
|
|
455
459
|
// Call the factory function to get the ComponentObject
|
|
456
460
|
return compilationResult.component.factory(this.runtimeContext);
|
|
457
461
|
} catch (error) {
|
|
@@ -459,18 +463,21 @@ export class ComponentRegistryService {
|
|
|
459
463
|
throw error;
|
|
460
464
|
}
|
|
461
465
|
}
|
|
462
|
-
|
|
466
|
+
|
|
463
467
|
/**
|
|
464
468
|
* Get component specification from database or external registry
|
|
465
469
|
*/
|
|
466
|
-
async getComponentSpec(
|
|
470
|
+
async getComponentSpec(
|
|
471
|
+
componentId: string,
|
|
472
|
+
contextUser?: UserInfo
|
|
473
|
+
): Promise<ComponentSpec> {
|
|
467
474
|
await this.initialize(contextUser);
|
|
468
|
-
|
|
475
|
+
|
|
469
476
|
const component = this.componentEngine.Components.find((c: ComponentEntity) => c.ID === componentId);
|
|
470
477
|
if (!component) {
|
|
471
478
|
throw new Error(`Component not found: ${componentId}`);
|
|
472
479
|
}
|
|
473
|
-
|
|
480
|
+
|
|
474
481
|
if (!component.SourceRegistryID) {
|
|
475
482
|
// LOCAL: Use specification from database
|
|
476
483
|
if (!component.Specification) {
|
|
@@ -478,7 +485,7 @@ export class ComponentRegistryService {
|
|
|
478
485
|
}
|
|
479
486
|
return JSON.parse(component.Specification);
|
|
480
487
|
}
|
|
481
|
-
|
|
488
|
+
|
|
482
489
|
// EXTERNAL: Check if we have a cached version
|
|
483
490
|
if (component.Specification && component.LastSyncedAt) {
|
|
484
491
|
// For now, always use cached version (no expiration)
|
|
@@ -487,33 +494,35 @@ export class ComponentRegistryService {
|
|
|
487
494
|
}
|
|
488
495
|
return JSON.parse(component.Specification);
|
|
489
496
|
}
|
|
490
|
-
|
|
497
|
+
|
|
491
498
|
// Need to fetch from external registry
|
|
492
|
-
const registry = this.componentEngine.ComponentRegistries?.find(
|
|
493
|
-
|
|
499
|
+
const registry = this.componentEngine.ComponentRegistries?.find(
|
|
500
|
+
r => r.ID === component.SourceRegistryID
|
|
501
|
+
);
|
|
502
|
+
|
|
494
503
|
if (!registry) {
|
|
495
504
|
throw new Error(`Registry not found: ${component.SourceRegistryID}`);
|
|
496
505
|
}
|
|
497
|
-
|
|
506
|
+
|
|
498
507
|
// Try GraphQL client first if available
|
|
499
508
|
let spec: ComponentSpec;
|
|
500
|
-
|
|
509
|
+
|
|
501
510
|
if (this.graphQLClient) {
|
|
502
511
|
if (this.debug) {
|
|
503
512
|
console.log(`Fetching from registry via GraphQL: ${component.Name}`);
|
|
504
513
|
}
|
|
505
|
-
|
|
514
|
+
|
|
506
515
|
const result = await this.graphQLClient.GetRegistryComponent({
|
|
507
516
|
registryName: registry.Name,
|
|
508
517
|
namespace: component.Namespace || '',
|
|
509
518
|
name: component.Name,
|
|
510
|
-
version: component.Version
|
|
519
|
+
version: component.Version
|
|
511
520
|
});
|
|
512
|
-
|
|
521
|
+
|
|
513
522
|
if (!result) {
|
|
514
523
|
throw new Error(`Component not found in registry: ${component.Name}`);
|
|
515
524
|
}
|
|
516
|
-
|
|
525
|
+
|
|
517
526
|
spec = result;
|
|
518
527
|
} else {
|
|
519
528
|
// Fallback to direct HTTP if no GraphQL client
|
|
@@ -525,13 +534,13 @@ export class ComponentRegistryService {
|
|
|
525
534
|
this.getRegistryApiKey(registry.ID)
|
|
526
535
|
);
|
|
527
536
|
}
|
|
528
|
-
|
|
537
|
+
|
|
529
538
|
// Store in local database for future use
|
|
530
539
|
await this.cacheExternalComponent(componentId, spec, contextUser);
|
|
531
|
-
|
|
540
|
+
|
|
532
541
|
return spec;
|
|
533
542
|
}
|
|
534
|
-
|
|
543
|
+
|
|
535
544
|
/**
|
|
536
545
|
* Fetch component from external registry via HTTP
|
|
537
546
|
*/
|
|
@@ -543,132 +552,141 @@ export class ComponentRegistryService {
|
|
|
543
552
|
apiKey?: string
|
|
544
553
|
): Promise<ComponentSpec> {
|
|
545
554
|
const url = `${uri}/components/${encodeURIComponent(namespace)}/${encodeURIComponent(name)}/${version}`;
|
|
546
|
-
|
|
555
|
+
|
|
547
556
|
const headers: HeadersInit = {
|
|
548
|
-
Accept: 'application/json'
|
|
557
|
+
'Accept': 'application/json'
|
|
549
558
|
};
|
|
550
|
-
|
|
559
|
+
|
|
551
560
|
if (apiKey) {
|
|
552
561
|
headers['Authorization'] = `Bearer ${apiKey}`;
|
|
553
562
|
}
|
|
554
|
-
|
|
563
|
+
|
|
555
564
|
if (this.debug) {
|
|
556
565
|
console.log(`Fetching from external registry: ${url}`);
|
|
557
566
|
}
|
|
558
|
-
|
|
567
|
+
|
|
559
568
|
const response = await fetch(url, { headers });
|
|
560
|
-
|
|
569
|
+
|
|
561
570
|
if (!response.ok) {
|
|
562
571
|
throw new Error(`Registry fetch failed: ${response.status} ${response.statusText}`);
|
|
563
572
|
}
|
|
564
|
-
|
|
565
|
-
const spec =
|
|
573
|
+
|
|
574
|
+
const spec = await response.json() as ComponentSpec;
|
|
566
575
|
return spec;
|
|
567
576
|
}
|
|
568
|
-
|
|
577
|
+
|
|
569
578
|
/**
|
|
570
579
|
* Cache an external component in the local database
|
|
571
580
|
*/
|
|
572
|
-
private async cacheExternalComponent(
|
|
581
|
+
private async cacheExternalComponent(
|
|
582
|
+
componentId: string,
|
|
583
|
+
spec: ComponentSpec,
|
|
584
|
+
contextUser?: UserInfo
|
|
585
|
+
): Promise<void> {
|
|
573
586
|
// Get the actual entity object to save
|
|
574
587
|
const md = new Metadata();
|
|
575
588
|
const componentEntity = await md.GetEntityObject<ComponentEntity>('MJ: Components', contextUser);
|
|
576
|
-
|
|
589
|
+
|
|
577
590
|
// Load the existing component
|
|
578
|
-
if (!
|
|
591
|
+
if (!await componentEntity.Load(componentId)) {
|
|
579
592
|
throw new Error(`Failed to load component entity: ${componentId}`);
|
|
580
593
|
}
|
|
581
|
-
|
|
594
|
+
|
|
582
595
|
// Update with fetched specification and all fields from spec
|
|
583
596
|
componentEntity.Specification = JSON.stringify(spec);
|
|
584
597
|
componentEntity.LastSyncedAt = new Date();
|
|
585
|
-
|
|
598
|
+
|
|
586
599
|
// Set ReplicatedAt only on first fetch
|
|
587
600
|
if (!componentEntity.ReplicatedAt) {
|
|
588
601
|
componentEntity.ReplicatedAt = new Date();
|
|
589
602
|
}
|
|
590
|
-
|
|
603
|
+
|
|
591
604
|
// Update all fields from the spec with strong typing
|
|
592
605
|
if (spec.name) {
|
|
593
606
|
componentEntity.Name = spec.name;
|
|
594
607
|
}
|
|
595
|
-
|
|
608
|
+
|
|
596
609
|
if (spec.namespace) {
|
|
597
610
|
componentEntity.Namespace = spec.namespace;
|
|
598
611
|
}
|
|
599
|
-
|
|
612
|
+
|
|
600
613
|
if (spec.version) {
|
|
601
614
|
componentEntity.Version = spec.version;
|
|
602
615
|
}
|
|
603
|
-
|
|
616
|
+
|
|
604
617
|
if (spec.title) {
|
|
605
618
|
componentEntity.Title = spec.title;
|
|
606
619
|
}
|
|
607
|
-
|
|
620
|
+
|
|
608
621
|
if (spec.description) {
|
|
609
622
|
componentEntity.Description = spec.description;
|
|
610
623
|
}
|
|
611
|
-
|
|
624
|
+
|
|
612
625
|
if (spec.type) {
|
|
613
626
|
// Map spec type to entity type (entity has specific enum values)
|
|
614
627
|
const typeMap: Record<string, ComponentEntity['Type']> = {
|
|
615
|
-
report: 'Report',
|
|
616
|
-
dashboard: 'Dashboard',
|
|
617
|
-
form: 'Form',
|
|
618
|
-
table: 'Table',
|
|
619
|
-
chart: 'Chart',
|
|
620
|
-
navigation: 'Navigation',
|
|
621
|
-
search: 'Search',
|
|
622
|
-
widget: 'Widget',
|
|
623
|
-
utility: 'Utility',
|
|
624
|
-
other: 'Other'
|
|
628
|
+
'report': 'Report',
|
|
629
|
+
'dashboard': 'Dashboard',
|
|
630
|
+
'form': 'Form',
|
|
631
|
+
'table': 'Table',
|
|
632
|
+
'chart': 'Chart',
|
|
633
|
+
'navigation': 'Navigation',
|
|
634
|
+
'search': 'Search',
|
|
635
|
+
'widget': 'Widget',
|
|
636
|
+
'utility': 'Utility',
|
|
637
|
+
'other': 'Other'
|
|
625
638
|
};
|
|
626
|
-
|
|
639
|
+
|
|
627
640
|
const mappedType = typeMap[spec.type.toLowerCase()];
|
|
628
641
|
if (mappedType) {
|
|
629
642
|
componentEntity.Type = mappedType;
|
|
630
643
|
}
|
|
631
644
|
}
|
|
632
|
-
|
|
645
|
+
|
|
633
646
|
if (spec.functionalRequirements) {
|
|
634
647
|
componentEntity.FunctionalRequirements = spec.functionalRequirements;
|
|
635
648
|
}
|
|
636
|
-
|
|
649
|
+
|
|
637
650
|
if (spec.technicalDesign) {
|
|
638
651
|
componentEntity.TechnicalDesign = spec.technicalDesign;
|
|
639
652
|
}
|
|
640
|
-
|
|
653
|
+
|
|
641
654
|
// Save back to database
|
|
642
655
|
const result = await componentEntity.Save();
|
|
643
656
|
if (!result) {
|
|
644
|
-
throw new Error(
|
|
645
|
-
`Failed to save cached component: ${componentEntity.Name}\n${componentEntity.LatestResult.Message || componentEntity.LatestResult.Error || componentEntity.LatestResult.Errors?.join(',')}`
|
|
646
|
-
);
|
|
657
|
+
throw new Error(`Failed to save cached component: ${componentEntity.Name}\n${componentEntity.LatestResult.Message || componentEntity.LatestResult.Error || componentEntity.LatestResult.Errors?.join(',')}`);
|
|
647
658
|
}
|
|
648
|
-
|
|
659
|
+
|
|
649
660
|
if (this.debug) {
|
|
650
661
|
console.log(`Cached external component: ${componentEntity.Name} at ${componentEntity.LastSyncedAt}`);
|
|
651
662
|
}
|
|
652
|
-
|
|
663
|
+
|
|
653
664
|
// Refresh metadata cache after saving
|
|
654
665
|
await this.componentEngine.Config(true, contextUser);
|
|
655
666
|
}
|
|
656
|
-
|
|
667
|
+
|
|
657
668
|
/**
|
|
658
669
|
* Load component dependencies from database
|
|
659
670
|
*/
|
|
660
|
-
async loadDependencies(
|
|
671
|
+
async loadDependencies(
|
|
672
|
+
componentId: string,
|
|
673
|
+
contextUser?: UserInfo
|
|
674
|
+
): Promise<ComponentDependencyInfo[]> {
|
|
661
675
|
await this.initialize(contextUser);
|
|
662
|
-
|
|
676
|
+
|
|
663
677
|
// Get dependencies from metadata cache
|
|
664
|
-
const dependencies = this.componentEngine.ComponentDependencies?.filter(
|
|
665
|
-
|
|
678
|
+
const dependencies = this.componentEngine.ComponentDependencies?.filter(
|
|
679
|
+
d => d.ComponentID === componentId
|
|
680
|
+
) || [];
|
|
681
|
+
|
|
666
682
|
const result: ComponentDependencyInfo[] = [];
|
|
667
|
-
|
|
683
|
+
|
|
668
684
|
for (const dep of dependencies) {
|
|
669
685
|
// Find the dependency component
|
|
670
|
-
const depComponent = this.componentEngine.Components.find(
|
|
671
|
-
|
|
686
|
+
const depComponent = this.componentEngine.Components.find(
|
|
687
|
+
(c: ComponentEntity) => c.ID === dep.DependencyComponentID
|
|
688
|
+
);
|
|
689
|
+
|
|
672
690
|
if (depComponent) {
|
|
673
691
|
result.push({
|
|
674
692
|
name: depComponent.Name,
|
|
@@ -676,68 +694,78 @@ export class ComponentRegistryService {
|
|
|
676
694
|
version: depComponent.Version, // Version comes from the linked Component record
|
|
677
695
|
isRequired: true, // All dependencies are required in MemberJunction
|
|
678
696
|
location: depComponent.SourceRegistryID ? 'registry' : 'embedded',
|
|
679
|
-
sourceRegistryID: depComponent.SourceRegistryID
|
|
697
|
+
sourceRegistryID: depComponent.SourceRegistryID
|
|
680
698
|
});
|
|
681
699
|
}
|
|
682
700
|
}
|
|
683
|
-
|
|
701
|
+
|
|
684
702
|
return result;
|
|
685
703
|
}
|
|
686
|
-
|
|
704
|
+
|
|
687
705
|
/**
|
|
688
706
|
* Resolve full dependency tree for a component
|
|
689
707
|
*/
|
|
690
|
-
async resolveDependencyTree(
|
|
708
|
+
async resolveDependencyTree(
|
|
709
|
+
componentId: string,
|
|
710
|
+
contextUser?: UserInfo,
|
|
711
|
+
visited = new Set<string>()
|
|
712
|
+
): Promise<DependencyTree> {
|
|
691
713
|
if (visited.has(componentId)) {
|
|
692
|
-
return {
|
|
693
|
-
componentId,
|
|
694
|
-
circular: true
|
|
714
|
+
return {
|
|
715
|
+
componentId,
|
|
716
|
+
circular: true
|
|
695
717
|
};
|
|
696
718
|
}
|
|
697
719
|
visited.add(componentId);
|
|
698
|
-
|
|
720
|
+
|
|
699
721
|
await this.initialize(contextUser);
|
|
700
|
-
|
|
722
|
+
|
|
701
723
|
const component = this.componentEngine.Components.find((c: ComponentEntity) => c.ID === componentId);
|
|
702
724
|
if (!component) {
|
|
703
725
|
return { componentId, dependencies: [] };
|
|
704
726
|
}
|
|
705
|
-
|
|
727
|
+
|
|
706
728
|
// Get direct dependencies
|
|
707
729
|
const directDeps = await this.loadDependencies(componentId, contextUser);
|
|
708
|
-
|
|
730
|
+
|
|
709
731
|
// Recursively resolve each dependency
|
|
710
732
|
const dependencies: DependencyTree[] = [];
|
|
711
733
|
for (const dep of directDeps) {
|
|
712
734
|
// Find the dependency component
|
|
713
735
|
const depComponent = this.componentEngine.Components.find(
|
|
714
|
-
(
|
|
715
|
-
|
|
716
|
-
c.Namespace?.trim().toLowerCase() === dep.namespace?.trim().toLowerCase()
|
|
736
|
+
c => c.Name.trim().toLowerCase() === dep.name.trim().toLowerCase() &&
|
|
737
|
+
c.Namespace?.trim().toLowerCase() === dep.namespace?.trim().toLowerCase()
|
|
717
738
|
);
|
|
718
|
-
|
|
739
|
+
|
|
719
740
|
if (depComponent) {
|
|
720
|
-
const subTree = await this.resolveDependencyTree(
|
|
741
|
+
const subTree = await this.resolveDependencyTree(
|
|
742
|
+
depComponent.ID,
|
|
743
|
+
contextUser,
|
|
744
|
+
visited
|
|
745
|
+
);
|
|
721
746
|
dependencies.push(subTree);
|
|
722
747
|
}
|
|
723
748
|
}
|
|
724
|
-
|
|
749
|
+
|
|
725
750
|
return {
|
|
726
751
|
componentId,
|
|
727
752
|
name: component.Name,
|
|
728
753
|
namespace: component.Namespace || undefined,
|
|
729
754
|
version: component.Version,
|
|
730
755
|
dependencies,
|
|
731
|
-
totalCount: dependencies.reduce((sum, d) => sum + (d.totalCount || 1), 1)
|
|
756
|
+
totalCount: dependencies.reduce((sum, d) => sum + (d.totalCount || 1), 1)
|
|
732
757
|
};
|
|
733
758
|
}
|
|
734
|
-
|
|
759
|
+
|
|
735
760
|
/**
|
|
736
761
|
* Get components to load in dependency order
|
|
737
762
|
*/
|
|
738
|
-
async getComponentsToLoad(
|
|
763
|
+
async getComponentsToLoad(
|
|
764
|
+
rootComponentId: string,
|
|
765
|
+
contextUser?: UserInfo
|
|
766
|
+
): Promise<string[]> {
|
|
739
767
|
const tree = await this.resolveDependencyTree(rootComponentId, contextUser);
|
|
740
|
-
|
|
768
|
+
|
|
741
769
|
// Flatten tree in dependency order (depth-first)
|
|
742
770
|
const ordered: string[] = [];
|
|
743
771
|
const processNode = (node: DependencyTree) => {
|
|
@@ -749,10 +777,10 @@ export class ComponentRegistryService {
|
|
|
749
777
|
}
|
|
750
778
|
};
|
|
751
779
|
processNode(tree);
|
|
752
|
-
|
|
780
|
+
|
|
753
781
|
return ordered;
|
|
754
782
|
}
|
|
755
|
-
|
|
783
|
+
|
|
756
784
|
/**
|
|
757
785
|
* Add a reference to a component
|
|
758
786
|
*/
|
|
@@ -762,7 +790,7 @@ export class ComponentRegistryService {
|
|
|
762
790
|
}
|
|
763
791
|
this.componentReferences.get(componentKey)!.add(referenceId);
|
|
764
792
|
}
|
|
765
|
-
|
|
793
|
+
|
|
766
794
|
/**
|
|
767
795
|
* Remove a reference to a component
|
|
768
796
|
*/
|
|
@@ -770,14 +798,14 @@ export class ComponentRegistryService {
|
|
|
770
798
|
const refs = this.componentReferences.get(componentKey);
|
|
771
799
|
if (refs) {
|
|
772
800
|
refs.delete(referenceId);
|
|
773
|
-
|
|
801
|
+
|
|
774
802
|
// If no more references and cache cleanup is enabled
|
|
775
803
|
if (refs.size === 0) {
|
|
776
804
|
this.considerCacheEviction(componentKey);
|
|
777
805
|
}
|
|
778
806
|
}
|
|
779
807
|
}
|
|
780
|
-
|
|
808
|
+
|
|
781
809
|
/**
|
|
782
810
|
* Consider evicting a component from cache
|
|
783
811
|
*/
|
|
@@ -786,7 +814,7 @@ export class ComponentRegistryService {
|
|
|
786
814
|
if (cached) {
|
|
787
815
|
const timeSinceLastUse = Date.now() - cached.lastUsed.getTime();
|
|
788
816
|
const evictionThreshold = 5 * 60 * 1000; // 5 minutes
|
|
789
|
-
|
|
817
|
+
|
|
790
818
|
if (timeSinceLastUse > evictionThreshold) {
|
|
791
819
|
if (this.debug) {
|
|
792
820
|
console.log(`🗑️ Evicting unused component from cache: ${componentKey}`);
|
|
@@ -795,7 +823,7 @@ export class ComponentRegistryService {
|
|
|
795
823
|
}
|
|
796
824
|
}
|
|
797
825
|
}
|
|
798
|
-
|
|
826
|
+
|
|
799
827
|
/**
|
|
800
828
|
* Get API key for a registry from secure configuration
|
|
801
829
|
* @param registryId - Registry ID
|
|
@@ -808,7 +836,7 @@ export class ComponentRegistryService {
|
|
|
808
836
|
const envKey = `REGISTRY_API_KEY_${registryId.replace(/-/g, '_').toUpperCase()}`;
|
|
809
837
|
return process.env[envKey];
|
|
810
838
|
}
|
|
811
|
-
|
|
839
|
+
|
|
812
840
|
/**
|
|
813
841
|
* Get cache statistics
|
|
814
842
|
*/
|
|
@@ -818,17 +846,17 @@ export class ComponentRegistryService {
|
|
|
818
846
|
memoryEstimate: string;
|
|
819
847
|
} {
|
|
820
848
|
let totalUseCount = 0;
|
|
821
|
-
this.compiledComponentCache.forEach(
|
|
849
|
+
this.compiledComponentCache.forEach(cached => {
|
|
822
850
|
totalUseCount += cached.useCount;
|
|
823
851
|
});
|
|
824
|
-
|
|
852
|
+
|
|
825
853
|
return {
|
|
826
854
|
compiledComponents: this.compiledComponentCache.size,
|
|
827
855
|
totalUseCount,
|
|
828
|
-
memoryEstimate: `~${this.compiledComponentCache.size * 50}KB
|
|
856
|
+
memoryEstimate: `~${(this.compiledComponentCache.size * 50)}KB` // Rough estimate
|
|
829
857
|
};
|
|
830
858
|
}
|
|
831
|
-
|
|
859
|
+
|
|
832
860
|
/**
|
|
833
861
|
* Clear all caches
|
|
834
862
|
*/
|
|
@@ -860,18 +888,18 @@ export class ComponentRegistryService {
|
|
|
860
888
|
ComponentRegistryService.instance = null;
|
|
861
889
|
}
|
|
862
890
|
}
|
|
863
|
-
|
|
891
|
+
|
|
864
892
|
/**
|
|
865
893
|
* Generate a cache key for a component
|
|
866
894
|
*/
|
|
867
895
|
private getComponentKey(
|
|
868
|
-
name: string,
|
|
869
|
-
namespace: string | null | undefined,
|
|
870
|
-
version: string,
|
|
896
|
+
name: string,
|
|
897
|
+
namespace: string | null | undefined,
|
|
898
|
+
version: string,
|
|
871
899
|
sourceRegistryId: string | null | undefined
|
|
872
900
|
): string {
|
|
873
901
|
const registryPart = sourceRegistryId || 'local';
|
|
874
902
|
const namespacePart = namespace || 'global';
|
|
875
903
|
return `${registryPart}/${namespacePart}/${name}@${version}`;
|
|
876
904
|
}
|
|
877
|
-
}
|
|
905
|
+
}
|