@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.
Files changed (43) hide show
  1. package/.turbo/turbo-build.log +11 -10
  2. package/CHANGELOG.md +15 -1
  3. package/dist/compiler/component-compiler.d.ts.map +1 -1
  4. package/dist/compiler/component-compiler.js +56 -66
  5. package/dist/compiler/component-compiler.js.map +1 -1
  6. package/dist/component-manager/component-manager.d.ts.map +1 -1
  7. package/dist/component-manager/component-manager.js +42 -48
  8. package/dist/component-manager/component-manager.js.map +1 -1
  9. package/dist/component-manager/types.d.ts +1 -1
  10. package/dist/component-manager/types.d.ts.map +1 -1
  11. package/dist/component-manager/types.js.map +1 -1
  12. package/dist/registry/component-registry-service.d.ts +1 -1
  13. package/dist/registry/component-registry-service.d.ts.map +1 -1
  14. package/dist/registry/component-registry-service.js +34 -34
  15. package/dist/registry/component-registry-service.js.map +1 -1
  16. package/dist/registry/component-resolver.d.ts +1 -1
  17. package/dist/registry/component-resolver.d.ts.map +1 -1
  18. package/dist/registry/component-resolver.js +11 -10
  19. package/dist/registry/component-resolver.js.map +1 -1
  20. package/dist/runtime/component-hierarchy.d.ts +1 -1
  21. package/dist/runtime/component-hierarchy.d.ts.map +1 -1
  22. package/dist/runtime/component-hierarchy.js +43 -43
  23. package/dist/runtime/component-hierarchy.js.map +1 -1
  24. package/dist/runtime/prop-builder.d.ts.map +1 -1
  25. package/dist/runtime/prop-builder.js +24 -22
  26. package/dist/runtime/prop-builder.js.map +1 -1
  27. package/dist/runtime.umd.js +511 -494
  28. package/dist/types/index.d.ts.map +1 -1
  29. package/dist/types/index.js.map +1 -1
  30. package/dist/utilities/library-registry.d.ts +1 -1
  31. package/dist/utilities/library-registry.d.ts.map +1 -1
  32. package/dist/utilities/library-registry.js +10 -10
  33. package/dist/utilities/library-registry.js.map +1 -1
  34. package/package.json +6 -5
  35. package/src/compiler/component-compiler.ts +164 -186
  36. package/src/component-manager/component-manager.ts +216 -162
  37. package/src/component-manager/types.ts +27 -27
  38. package/src/registry/component-registry-service.ts +218 -190
  39. package/src/registry/component-resolver.ts +98 -87
  40. package/src/runtime/component-hierarchy.ts +94 -57
  41. package/src/runtime/prop-builder.ts +33 -28
  42. package/src/types/index.ts +4 -3
  43. 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/global';
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; // SHA-256 hash of the spec used for compilation
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
- 'Please use HTTPS or localhost for development. ' +
192
- 'Note: crypto.subtle is available in Node.js 15+ and all modern browsers on secure contexts.'
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((b) => b.toString(16).padStart(2, '0')).join('');
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(componentId: string, referenceId?: string, contextUser?: UserInfo): Promise<ComponentObject> {
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
- } catch (compileEx) {
267
+ }
268
+ catch (compileEx) {
263
269
  // log then throw
264
- console.error(`🔴 Error compiling component ${component.Name}`, compileEx);
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((r) => r.Name === registryName && r.Status === 'Active');
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, // Pass registry name, not ID
362
+ registryName: registry.Name, // Pass registry name, not ID
357
363
  namespace,
358
364
  name,
359
365
  version,
360
- hash: cached?.specHash, // Pass cached hash if available
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, // This is from an external registry
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, // Store the hash for future comparison
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(componentId: string, contextUser?: UserInfo): Promise<ComponentSpec> {
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((r) => r.ID === component.SourceRegistryID);
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 = (await response.json()) as ComponentSpec;
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(componentId: string, spec: ComponentSpec, contextUser?: UserInfo): Promise<void> {
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 (!(await componentEntity.Load(componentId))) {
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(componentId: string, contextUser?: UserInfo): Promise<ComponentDependencyInfo[]> {
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((d) => d.ComponentID === componentId) || [];
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((c: ComponentEntity) => c.ID === dep.DependencyComponentID);
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(componentId: string, contextUser?: UserInfo, visited = new Set<string>()): Promise<DependencyTree> {
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
- (c) =>
715
- c.Name.trim().toLowerCase() === dep.name.trim().toLowerCase() &&
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(depComponent.ID, contextUser, visited);
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(rootComponentId: string, contextUser?: UserInfo): Promise<string[]> {
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((cached) => {
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`, // Rough estimate
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
+ }