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