@memberjunction/server 5.33.0 → 5.34.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/README.md +2 -1
- package/dist/agents/skip-agent.d.ts +3 -1
- package/dist/agents/skip-agent.d.ts.map +1 -1
- package/dist/agents/skip-agent.js +15 -3
- package/dist/agents/skip-agent.js.map +1 -1
- package/dist/generated/generated.d.ts +551 -5
- package/dist/generated/generated.d.ts.map +1 -1
- package/dist/generated/generated.js +3277 -120
- package/dist/generated/generated.js.map +1 -1
- package/dist/generic/ResolverBase.d.ts +1 -1
- package/dist/generic/ResolverBase.d.ts.map +1 -1
- package/dist/generic/ResolverBase.js +9 -2
- package/dist/generic/ResolverBase.js.map +1 -1
- package/dist/generic/RunViewResolver.d.ts +5 -1
- package/dist/generic/RunViewResolver.d.ts.map +1 -1
- package/dist/generic/RunViewResolver.js +33 -2
- package/dist/generic/RunViewResolver.js.map +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +17 -2
- package/dist/index.js.map +1 -1
- package/dist/resolvers/AvailableSearchProvidersResolver.d.ts +26 -0
- package/dist/resolvers/AvailableSearchProvidersResolver.d.ts.map +1 -0
- package/dist/resolvers/AvailableSearchProvidersResolver.js +65 -0
- package/dist/resolvers/AvailableSearchProvidersResolver.js.map +1 -0
- package/dist/resolvers/ComponentRegistryResolver.d.ts +11 -25
- package/dist/resolvers/ComponentRegistryResolver.d.ts.map +1 -1
- package/dist/resolvers/ComponentRegistryResolver.js +51 -93
- package/dist/resolvers/ComponentRegistryResolver.js.map +1 -1
- package/dist/resolvers/QuerySystemUserResolver.d.ts +6 -0
- package/dist/resolvers/QuerySystemUserResolver.d.ts.map +1 -1
- package/dist/resolvers/QuerySystemUserResolver.js +31 -1
- package/dist/resolvers/QuerySystemUserResolver.js.map +1 -1
- package/dist/resolvers/SearchKnowledgeResolver.d.ts +44 -1
- package/dist/resolvers/SearchKnowledgeResolver.d.ts.map +1 -1
- package/dist/resolvers/SearchKnowledgeResolver.js +217 -7
- package/dist/resolvers/SearchKnowledgeResolver.js.map +1 -1
- package/dist/resolvers/SearchKnowledgeStreamResolver.d.ts +79 -0
- package/dist/resolvers/SearchKnowledgeStreamResolver.d.ts.map +1 -0
- package/dist/resolvers/SearchKnowledgeStreamResolver.js +342 -0
- package/dist/resolvers/SearchKnowledgeStreamResolver.js.map +1 -0
- package/dist/types.d.ts +6 -1
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js.map +1 -1
- package/package.json +68 -68
- package/src/agents/skip-agent.ts +18 -4
- package/src/generated/generated.ts +2229 -80
- package/src/generic/ResolverBase.ts +11 -2
- package/src/generic/RunViewResolver.ts +29 -2
- package/src/index.ts +17 -2
- package/src/resolvers/AvailableSearchProvidersResolver.ts +43 -0
- package/src/resolvers/ComponentRegistryResolver.ts +71 -123
- package/src/resolvers/QuerySystemUserResolver.ts +27 -1
- package/src/resolvers/SearchKnowledgeResolver.ts +205 -4
- package/src/resolvers/SearchKnowledgeStreamResolver.ts +338 -0
- package/src/types.ts +6 -1
|
@@ -356,7 +356,10 @@ export class ResolverBase {
|
|
|
356
356
|
userPayload,
|
|
357
357
|
viewInput.MaxRows,
|
|
358
358
|
viewInput.StartRow,
|
|
359
|
-
viewInput.Aggregates
|
|
359
|
+
viewInput.Aggregates,
|
|
360
|
+
viewInput.AfterKey
|
|
361
|
+
? CompositeKey.FromKeyValuePairs((viewInput.AfterKey as { KeyValuePairs: { FieldName: string; Value: string }[] }).KeyValuePairs)
|
|
362
|
+
: undefined
|
|
360
363
|
);
|
|
361
364
|
}
|
|
362
365
|
else {
|
|
@@ -505,6 +508,9 @@ export class ResolverBase {
|
|
|
505
508
|
ignoreMaxRows: viewInput.IgnoreMaxRows,
|
|
506
509
|
maxRows: viewInput.MaxRows,
|
|
507
510
|
startRow: viewInput.StartRow,
|
|
511
|
+
afterKey: viewInput.AfterKey
|
|
512
|
+
? CompositeKey.FromKeyValuePairs((viewInput.AfterKey as { KeyValuePairs: { FieldName: string; Value: string }[] }).KeyValuePairs)
|
|
513
|
+
: undefined,
|
|
508
514
|
excludeDataFromAllPriorViewRuns: viewInput.EntityName ? false : viewInput.ExcludeDataFromAllPriorViewRuns,
|
|
509
515
|
forceAuditLog: viewInput.ForceAuditLog,
|
|
510
516
|
auditLogDescription: viewInput.AuditLogDescription,
|
|
@@ -686,7 +692,8 @@ export class ResolverBase {
|
|
|
686
692
|
userPayload: UserPayload | null,
|
|
687
693
|
maxRows: number | undefined,
|
|
688
694
|
startRow: number | undefined,
|
|
689
|
-
aggregates?: AggregateExpression[]
|
|
695
|
+
aggregates?: AggregateExpression[],
|
|
696
|
+
afterKey?: CompositeKey
|
|
690
697
|
) {
|
|
691
698
|
try {
|
|
692
699
|
if (!viewInfo || !userPayload) return null;
|
|
@@ -745,6 +752,7 @@ export class ResolverBase {
|
|
|
745
752
|
IgnoreMaxRows: ignoreMaxRows,
|
|
746
753
|
MaxRows: maxRows,
|
|
747
754
|
StartRow: startRow,
|
|
755
|
+
AfterKey: afterKey,
|
|
748
756
|
ForceAuditLog: forceAuditLog,
|
|
749
757
|
AuditLogDescription: auditLogDescription,
|
|
750
758
|
ResultType: rt,
|
|
@@ -857,6 +865,7 @@ export class ResolverBase {
|
|
|
857
865
|
IgnoreMaxRows: param.ignoreMaxRows,
|
|
858
866
|
MaxRows: param.maxRows,
|
|
859
867
|
StartRow: param.startRow,
|
|
868
|
+
AfterKey: param.afterKey,
|
|
860
869
|
ForceAuditLog: param.forceAuditLog,
|
|
861
870
|
AuditLogDescription: param.auditLogDescription,
|
|
862
871
|
ResultType: rt,
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
import { Arg, Ctx, Field, InputType, Int, ObjectType, PubSubEngine, Query, Resolver } from 'type-graphql';
|
|
2
2
|
import { AppContext } from '../types.js';
|
|
3
3
|
import { ResolverBase } from './ResolverBase.js';
|
|
4
|
-
import { LogError, LogStatus, EntityInfo, RunViewWithCacheCheckResult, RunViewsWithCacheCheckResponse, RunViewWithCacheCheckParams, AggregateResult } from '@memberjunction/core';
|
|
4
|
+
import { LogError, LogStatus, EntityInfo, RunViewWithCacheCheckResult, RunViewsWithCacheCheckResponse, RunViewWithCacheCheckParams, AggregateResult, CompositeKey } from '@memberjunction/core';
|
|
5
5
|
import { UUIDsEqual } from '@memberjunction/global';
|
|
6
6
|
import { RequireSystemUser } from '../directives/RequireSystemUser.js';
|
|
7
7
|
import { GetReadOnlyProvider } from '../util.js';
|
|
8
8
|
import { MJUserViewEntityExtended } from '@memberjunction/core-entities';
|
|
9
|
-
import { KeyValuePairOutputType } from './KeyInputOutputTypes.js';
|
|
9
|
+
import { CompositeKeyInputType, KeyValuePairOutputType } from './KeyInputOutputTypes.js';
|
|
10
10
|
import { SQLServerDataProvider } from '@memberjunction/sqlserver-dataprovider';
|
|
11
11
|
|
|
12
12
|
/********************************************************************************
|
|
@@ -159,6 +159,12 @@ export class RunViewByIDInput {
|
|
|
159
159
|
})
|
|
160
160
|
StartRow?: number;
|
|
161
161
|
|
|
162
|
+
@Field(() => CompositeKeyInputType, {
|
|
163
|
+
nullable: true,
|
|
164
|
+
description: 'Keyset (seek) pagination cursor. When provided, returns the next page of records after the given PK value, ordered by the PK column. Requires a single-column PK on the entity. Cannot be combined with StartRow.',
|
|
165
|
+
})
|
|
166
|
+
AfterKey?: CompositeKeyInputType;
|
|
167
|
+
|
|
162
168
|
@Field(() => [AggregateExpressionInput], {
|
|
163
169
|
nullable: true,
|
|
164
170
|
description: 'Optional aggregate expressions to calculate on the full result set (e.g., SUM, COUNT, AVG). Results are returned in AggregateResults.',
|
|
@@ -260,6 +266,12 @@ export class RunViewByNameInput {
|
|
|
260
266
|
})
|
|
261
267
|
StartRow?: number;
|
|
262
268
|
|
|
269
|
+
@Field(() => CompositeKeyInputType, {
|
|
270
|
+
nullable: true,
|
|
271
|
+
description: 'Keyset (seek) pagination cursor. When provided, returns the next page of records after the given PK value, ordered by the PK column. Requires a single-column PK on the entity. Cannot be combined with StartRow.',
|
|
272
|
+
})
|
|
273
|
+
AfterKey?: CompositeKeyInputType;
|
|
274
|
+
|
|
263
275
|
@Field(() => [AggregateExpressionInput], {
|
|
264
276
|
nullable: true,
|
|
265
277
|
description: 'Optional aggregate expressions to calculate on the full result set (e.g., SUM, COUNT, AVG). Results are returned in AggregateResults.',
|
|
@@ -347,6 +359,12 @@ export class RunDynamicViewInput {
|
|
|
347
359
|
})
|
|
348
360
|
StartRow?: number;
|
|
349
361
|
|
|
362
|
+
@Field(() => CompositeKeyInputType, {
|
|
363
|
+
nullable: true,
|
|
364
|
+
description: 'Keyset (seek) pagination cursor. When provided, returns the next page of records after the given PK value, ordered by the PK column. Requires a single-column PK on the entity. Cannot be combined with StartRow.',
|
|
365
|
+
})
|
|
366
|
+
AfterKey?: CompositeKeyInputType;
|
|
367
|
+
|
|
350
368
|
@Field(() => [AggregateExpressionInput], {
|
|
351
369
|
nullable: true,
|
|
352
370
|
description: 'Optional aggregate expressions to calculate on the full result set (e.g., SUM, COUNT, AVG). Results are returned in AggregateResults.',
|
|
@@ -463,6 +481,12 @@ export class RunViewGenericInput {
|
|
|
463
481
|
})
|
|
464
482
|
StartRow?: number;
|
|
465
483
|
|
|
484
|
+
@Field(() => CompositeKeyInputType, {
|
|
485
|
+
nullable: true,
|
|
486
|
+
description: 'Keyset (seek) pagination cursor. When provided, returns the next page of records after the given PK value, ordered by the PK column. Requires a single-column PK on the entity. Cannot be combined with StartRow.',
|
|
487
|
+
})
|
|
488
|
+
AfterKey?: CompositeKeyInputType;
|
|
489
|
+
|
|
466
490
|
@Field(() => [AggregateExpressionInput], {
|
|
467
491
|
nullable: true,
|
|
468
492
|
description: 'Optional aggregate expressions to calculate on the full result set (e.g., SUM, COUNT, AVG). Results are returned in AggregateResults.',
|
|
@@ -1017,6 +1041,9 @@ export class RunViewResolver extends ResolverBase {
|
|
|
1017
1041
|
AuditLogDescription: item.params.AuditLogDescription,
|
|
1018
1042
|
ResultType: (item.params.ResultType || 'simple') as 'simple' | 'entity_object' | 'count_only',
|
|
1019
1043
|
StartRow: item.params.StartRow,
|
|
1044
|
+
AfterKey: item.params.AfterKey
|
|
1045
|
+
? CompositeKey.FromKeyValuePairs(item.params.AfterKey.KeyValuePairs)
|
|
1046
|
+
: undefined,
|
|
1020
1047
|
},
|
|
1021
1048
|
cacheStatus: item.cacheStatus ? {
|
|
1022
1049
|
maxUpdatedAt: item.cacheStatus.maxUpdatedAt,
|
package/src/index.ts
CHANGED
|
@@ -6,7 +6,7 @@ import { expressMiddleware } from '@as-integrations/express5';
|
|
|
6
6
|
import { mergeSchemas } from '@graphql-tools/schema';
|
|
7
7
|
import { Metadata, DatabasePlatform, SetProvider, StartupManager as StartupManagerImport, BaseEntity, BaseEntityEvent, RunView } from '@memberjunction/core';
|
|
8
8
|
import { resolveDbPlatformFromEnv } from '@memberjunction/generic-database-provider';
|
|
9
|
-
import { MJGlobal, MJEventType, UUIDsEqual } from '@memberjunction/global';
|
|
9
|
+
import { MJGlobal, MJEventType, UUIDsEqual, ShutdownRegistry } from '@memberjunction/global';
|
|
10
10
|
import { setupSQLServerClient, SQLServerDataProvider, SQLServerProviderConfigData, UserCache } from '@memberjunction/sqlserver-dataprovider';
|
|
11
11
|
import { extendConnectionPoolWithQuery } from './util.js';
|
|
12
12
|
import { default as BodyParser } from 'body-parser';
|
|
@@ -101,6 +101,8 @@ export * from './resolvers/RunAIPromptResolver.js';
|
|
|
101
101
|
export * from './resolvers/RunAIAgentResolver.js';
|
|
102
102
|
export * from './resolvers/VectorizeEntityResolver.js';
|
|
103
103
|
export * from './resolvers/SearchKnowledgeResolver.js';
|
|
104
|
+
export * from './resolvers/SearchKnowledgeStreamResolver.js';
|
|
105
|
+
export * from './resolvers/AvailableSearchProvidersResolver.js';
|
|
104
106
|
export * from './resolvers/FetchEntityVectorsResolver.js';
|
|
105
107
|
export * from './resolvers/PipelineProgressResolver.js';
|
|
106
108
|
export * from './resolvers/ClientToolRequestResolver.js';
|
|
@@ -422,7 +424,7 @@ export const serve = async (resolverPaths: Array<string>, app: Application = cre
|
|
|
422
424
|
url: process.env.REDIS_URL,
|
|
423
425
|
keyPrefix: process.env.REDIS_KEY_PREFIX || 'mj',
|
|
424
426
|
enablePubSub: true,
|
|
425
|
-
enableLogging:
|
|
427
|
+
enableLogging: configInfo.cacheSettings?.verboseLogging ?? false,
|
|
426
428
|
});
|
|
427
429
|
(Metadata.Provider as GenericDatabaseProvider).SetLocalStorageProvider(redisProvider); // global-provider-ok: bootstrap (Redis cache wiring)
|
|
428
430
|
await redisProvider.StartListening();
|
|
@@ -906,6 +908,19 @@ export const serve = async (resolverPaths: Array<string>, app: Application = cre
|
|
|
906
908
|
}
|
|
907
909
|
}
|
|
908
910
|
|
|
911
|
+
// Drain anything self-registered with ShutdownRegistry — QueueManager,
|
|
912
|
+
// future engines/services with timers/intervals/listeners. Each is
|
|
913
|
+
// responsible for being idempotent and not throwing.
|
|
914
|
+
try {
|
|
915
|
+
const count = ShutdownRegistry.Instance.Count;
|
|
916
|
+
if (count > 0) {
|
|
917
|
+
await ShutdownRegistry.Instance.ShutdownAll();
|
|
918
|
+
console.log(`✅ ShutdownRegistry drained ${count} registered service(s)`);
|
|
919
|
+
}
|
|
920
|
+
} catch (error) {
|
|
921
|
+
console.error('❌ Error draining ShutdownRegistry:', error);
|
|
922
|
+
}
|
|
923
|
+
|
|
909
924
|
// Close server
|
|
910
925
|
httpServer.close(() => {
|
|
911
926
|
console.log('✅ HTTP server closed');
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { Field, ObjectType, Query, Resolver } from 'type-graphql';
|
|
2
|
+
import { BaseSearchProvider } from '@memberjunction/search-engine';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Surfaces the runtime ClassFactory registrations of `BaseSearchProvider`
|
|
6
|
+
* subclasses for UI consumers (P5.5 — SearchScope form's provider dropdown).
|
|
7
|
+
*
|
|
8
|
+
* Why a GraphQL query and not a hardcoded client-side list:
|
|
9
|
+
* - `@memberjunction/search-engine` is a Node-only package; the browser
|
|
10
|
+
* can't import it directly, so it can't read `ClassFactory` itself.
|
|
11
|
+
* - Third-party packages can register additional providers at server boot
|
|
12
|
+
* without modifying any UI code; this query keeps the form auto-discovering.
|
|
13
|
+
* - The list is also useful for ops debugging ("which providers does this
|
|
14
|
+
* server build know about?").
|
|
15
|
+
*
|
|
16
|
+
* The resolver is intentionally read-only and does not require system-user
|
|
17
|
+
* privileges — knowing the registered driver-class names on a given server
|
|
18
|
+
* isn't sensitive, and the SearchScope form itself is gated by entity-level
|
|
19
|
+
* permission checks. Adjust if your deployment treats the provider catalog
|
|
20
|
+
* as confidential.
|
|
21
|
+
*/
|
|
22
|
+
@ObjectType()
|
|
23
|
+
export class AvailableSearchProviderGQL {
|
|
24
|
+
@Field()
|
|
25
|
+
DriverClass!: string;
|
|
26
|
+
|
|
27
|
+
@Field()
|
|
28
|
+
SourceType!: string;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
@Resolver()
|
|
32
|
+
export class AvailableSearchProvidersResolver {
|
|
33
|
+
@Query(() => [AvailableSearchProviderGQL])
|
|
34
|
+
AvailableSearchProviders(): AvailableSearchProviderGQL[] {
|
|
35
|
+
const registrations = BaseSearchProvider.GetAvailableProviders();
|
|
36
|
+
return registrations.map(r => {
|
|
37
|
+
const gql = new AvailableSearchProviderGQL();
|
|
38
|
+
gql.DriverClass = r.DriverClass;
|
|
39
|
+
gql.SourceType = r.SourceType;
|
|
40
|
+
return gql;
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
}
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { Arg, Ctx, Field, InputType, ObjectType, Query, Mutation, Resolver } from 'type-graphql';
|
|
2
|
-
import { UserInfo,
|
|
3
|
-
import { UUIDsEqual } from '@memberjunction/global';
|
|
2
|
+
import { UserInfo, LogError, LogStatus } from '@memberjunction/core';
|
|
3
|
+
import { UUIDsEqual, MJLruCache } from '@memberjunction/global';
|
|
4
4
|
import { UserCache } from '@memberjunction/sqlserver-dataprovider';
|
|
5
|
-
import {
|
|
5
|
+
import { MJComponentRegistryEntity, ComponentMetadataEngine } from '@memberjunction/core-entities';
|
|
6
6
|
import { ComponentSpec } from '@memberjunction/interactive-component-types';
|
|
7
7
|
import {
|
|
8
8
|
ComponentRegistryClient,
|
|
@@ -16,7 +16,6 @@ import {
|
|
|
16
16
|
} from '@memberjunction/component-registry-client-sdk';
|
|
17
17
|
import { AppContext } from '../types.js';
|
|
18
18
|
import { configInfo } from '../config.js';
|
|
19
|
-
import { GetReadWriteProvider } from '../util.js';
|
|
20
19
|
|
|
21
20
|
/**
|
|
22
21
|
* GraphQL types for Component Registry operations
|
|
@@ -166,23 +165,43 @@ class ComponentFeedbackResponse {
|
|
|
166
165
|
* - REGISTRY_API_KEY_<REGISTRY_NAME>: API key for authenticating with the registry
|
|
167
166
|
* Example: REGISTRY_API_KEY_MJ_CENTRAL=your-api-key-here
|
|
168
167
|
*/
|
|
168
|
+
/** Server-side cache for registry component specs. Uses MJLruCache for isolated
|
|
169
|
+
* LRU eviction with TTL — avoids sharing eviction budgets with RunView/RunQuery
|
|
170
|
+
* data in LocalCacheManager. */
|
|
171
|
+
const componentSpecCache = new MJLruCache<string, { specJson: string; hash: string }>({
|
|
172
|
+
maxSize: 200,
|
|
173
|
+
ttlMs: 60 * 60 * 1000 // 1 hour — components are immutable; TTL governs "latest" pointer freshness
|
|
174
|
+
});
|
|
175
|
+
|
|
169
176
|
@Resolver()
|
|
170
177
|
export class ComponentRegistryExtendedResolver {
|
|
171
178
|
private componentEngine = ComponentMetadataEngine.Instance;
|
|
172
|
-
|
|
179
|
+
|
|
173
180
|
constructor() {
|
|
174
181
|
// No longer pre-initialize clients - create on demand
|
|
175
182
|
}
|
|
176
|
-
|
|
183
|
+
|
|
177
184
|
/**
|
|
178
|
-
*
|
|
185
|
+
* Build a deterministic cache key for a registry component.
|
|
186
|
+
*/
|
|
187
|
+
private getComponentCacheKey(registryName: string, namespace: string, name: string, version: string): string {
|
|
188
|
+
return `RegistryComponent|${registryName}|${namespace}|${name}|${version}`;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* Get a component from a registry with optional hash for caching.
|
|
193
|
+
*
|
|
194
|
+
* Caching strategy (three tiers):
|
|
195
|
+
* 1. Client sends hash from a previous fetch → server can return 304 if unchanged
|
|
196
|
+
* 2. Server checks in-memory LRU cache for a cached spec from a recent fetch
|
|
197
|
+
* 3. On miss, fetches from the external registry, caches the result, and returns it
|
|
179
198
|
*/
|
|
180
199
|
@Query(() => ComponentSpecWithHashType)
|
|
181
200
|
async GetRegistryComponent(
|
|
182
201
|
@Arg('registryName') registryName: string,
|
|
183
202
|
@Arg('namespace') namespace: string,
|
|
184
203
|
@Arg('name') name: string,
|
|
185
|
-
@Ctx() { userPayload
|
|
204
|
+
@Ctx() { userPayload }: AppContext,
|
|
186
205
|
@Arg('version', { nullable: true }) version?: string,
|
|
187
206
|
@Arg('hash', { nullable: true }) hash?: string
|
|
188
207
|
): Promise<ComponentSpecWithHashType> {
|
|
@@ -190,35 +209,59 @@ export class ComponentRegistryExtendedResolver {
|
|
|
190
209
|
// Get user from cache
|
|
191
210
|
const user = UserCache.Instance.Users.find((u) => u.Email.trim().toLowerCase() === userPayload.email?.trim().toLowerCase());
|
|
192
211
|
if (!user) throw new Error(`User ${userPayload.email} not found in UserCache`);
|
|
193
|
-
|
|
212
|
+
|
|
194
213
|
// Get registry from database by name
|
|
195
214
|
const registry = await this.getRegistryByName(registryName, user);
|
|
196
215
|
if (!registry) {
|
|
197
216
|
throw new Error(`Registry not found: ${registryName}`);
|
|
198
217
|
}
|
|
199
|
-
|
|
218
|
+
|
|
200
219
|
// Check user permissions (use registry ID for permission check)
|
|
201
220
|
await this.checkUserAccess(user, registry.ID);
|
|
202
|
-
|
|
221
|
+
|
|
203
222
|
// Initialize component engine
|
|
204
223
|
await this.componentEngine.Config(false, user);
|
|
205
|
-
|
|
206
|
-
|
|
224
|
+
|
|
225
|
+
const resolvedVersion = version || 'latest';
|
|
226
|
+
const cacheKey = this.getComponentCacheKey(registry.Name, namespace, name, resolvedVersion);
|
|
227
|
+
|
|
228
|
+
// --- Tier 1: Check server-side cache ---
|
|
229
|
+
const cached = componentSpecCache.Get(cacheKey);
|
|
230
|
+
|
|
231
|
+
if (cached) {
|
|
232
|
+
// If client sent a hash and it matches, return 304
|
|
233
|
+
if (hash && hash === cached.hash) {
|
|
234
|
+
return {
|
|
235
|
+
specification: undefined,
|
|
236
|
+
hash: cached.hash,
|
|
237
|
+
notModified: true,
|
|
238
|
+
message: 'Not modified'
|
|
239
|
+
};
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
// Client has no hash or a different hash — return cached spec
|
|
243
|
+
return {
|
|
244
|
+
specification: cached.specJson,
|
|
245
|
+
hash: cached.hash,
|
|
246
|
+
notModified: false,
|
|
247
|
+
message: undefined
|
|
248
|
+
};
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
// --- Tier 2: Fetch from external registry ---
|
|
207
252
|
const registryClient = this.createClientForRegistry(registry);
|
|
208
|
-
|
|
209
|
-
// Fetch component from registry with hash support
|
|
253
|
+
|
|
210
254
|
const response = await registryClient.getComponentWithHash({
|
|
211
255
|
registry: registry.Name,
|
|
212
256
|
namespace,
|
|
213
257
|
name,
|
|
214
|
-
version:
|
|
258
|
+
version: resolvedVersion,
|
|
215
259
|
hash: hash,
|
|
216
260
|
userEmail: user.Email
|
|
217
261
|
});
|
|
218
|
-
|
|
219
|
-
// If not modified (304), return
|
|
262
|
+
|
|
263
|
+
// If not modified (304 from registry), return to client
|
|
220
264
|
if (response.notModified) {
|
|
221
|
-
LogStatus(`Component ${namespace}/${name} not modified (hash: ${response.hash})`);
|
|
222
265
|
return {
|
|
223
266
|
specification: undefined,
|
|
224
267
|
hash: response.hash,
|
|
@@ -226,22 +269,20 @@ export class ComponentRegistryExtendedResolver {
|
|
|
226
269
|
message: response.message || 'Not modified'
|
|
227
270
|
};
|
|
228
271
|
}
|
|
229
|
-
|
|
272
|
+
|
|
230
273
|
// Extract the specification from the response
|
|
231
274
|
const component = response.specification;
|
|
232
275
|
if (!component) {
|
|
233
276
|
throw new Error(`Component ${namespace}/${name} returned without specification`);
|
|
234
277
|
}
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
// Return the ComponentSpec as a JSON string
|
|
278
|
+
|
|
279
|
+
const specJson = JSON.stringify(component);
|
|
280
|
+
|
|
281
|
+
// --- Cache the result for subsequent requests ---
|
|
282
|
+
componentSpecCache.Set(cacheKey, { specJson, hash: response.hash });
|
|
283
|
+
|
|
243
284
|
return {
|
|
244
|
-
specification:
|
|
285
|
+
specification: specJson,
|
|
245
286
|
hash: response.hash,
|
|
246
287
|
notModified: false,
|
|
247
288
|
message: undefined
|
|
@@ -489,100 +530,7 @@ export class ComponentRegistryExtendedResolver {
|
|
|
489
530
|
headers: config?.headers
|
|
490
531
|
});
|
|
491
532
|
}
|
|
492
|
-
|
|
493
|
-
/**
|
|
494
|
-
* Check if component should be cached
|
|
495
|
-
*/
|
|
496
|
-
private shouldCache(registry: MJComponentRegistryEntity): boolean {
|
|
497
|
-
// Check config for caching settings
|
|
498
|
-
const config = configInfo.componentRegistries?.find(r =>
|
|
499
|
-
UUIDsEqual(r.id, registry.ID) || r.name === registry.Name
|
|
500
|
-
);
|
|
501
|
-
return config?.cache !== false; // Cache by default
|
|
502
|
-
}
|
|
503
|
-
|
|
504
|
-
/**
|
|
505
|
-
* Cache component in database
|
|
506
|
-
*/
|
|
507
|
-
private async cacheComponent(
|
|
508
|
-
component: ComponentSpec,
|
|
509
|
-
registryId: string,
|
|
510
|
-
userInfo: UserInfo,
|
|
511
|
-
provider?: IMetadataProvider
|
|
512
|
-
): Promise<void> {
|
|
513
|
-
try {
|
|
514
|
-
// Find or create component entity
|
|
515
|
-
const md = provider ?? new Metadata();
|
|
516
|
-
const componentEntity = await md.GetEntityObject<MJComponentEntity>('MJ: Components', userInfo);
|
|
517
|
-
|
|
518
|
-
// Check if component already exists
|
|
519
|
-
const existingComponent = this.componentEngine.Components?.find(
|
|
520
|
-
c => c.Name === component.name &&
|
|
521
|
-
c.Namespace === component.namespace &&
|
|
522
|
-
UUIDsEqual(c.SourceRegistryID, registryId)
|
|
523
|
-
);
|
|
524
|
-
|
|
525
|
-
if (existingComponent) {
|
|
526
|
-
// Update existing component
|
|
527
|
-
if (!await componentEntity.Load(existingComponent.ID)) {
|
|
528
|
-
throw new Error(`Failed to load component: ${existingComponent.ID}`);
|
|
529
|
-
}
|
|
530
|
-
} else {
|
|
531
|
-
// Create new component
|
|
532
|
-
componentEntity.NewRecord();
|
|
533
|
-
componentEntity.SourceRegistryID = registryId;
|
|
534
|
-
}
|
|
535
|
-
|
|
536
|
-
// Update component fields
|
|
537
|
-
componentEntity.Name = component.name;
|
|
538
|
-
componentEntity.Namespace = component.namespace || '';
|
|
539
|
-
componentEntity.Version = component.version || '1.0.0';
|
|
540
|
-
componentEntity.Title = component.title;
|
|
541
|
-
componentEntity.Description = component.description;
|
|
542
|
-
componentEntity.Type = this.mapComponentType(component.type);
|
|
543
|
-
componentEntity.FunctionalRequirements = component.functionalRequirements;
|
|
544
|
-
componentEntity.TechnicalDesign = component.technicalDesign;
|
|
545
|
-
componentEntity.Specification = JSON.stringify(component);
|
|
546
|
-
componentEntity.LastSyncedAt = new Date();
|
|
547
|
-
|
|
548
|
-
if (!existingComponent) {
|
|
549
|
-
componentEntity.ReplicatedAt = new Date();
|
|
550
|
-
}
|
|
551
|
-
|
|
552
|
-
// Save component
|
|
553
|
-
const result = await componentEntity.Save();
|
|
554
|
-
if (!result) {
|
|
555
|
-
throw new Error(`Failed to cache component: ${component.name}`);
|
|
556
|
-
}
|
|
557
|
-
|
|
558
|
-
// Refresh metadata cache
|
|
559
|
-
await this.componentEngine.Config(true, userInfo);
|
|
560
|
-
} catch (error) {
|
|
561
|
-
// Log but don't throw - caching failure shouldn't break the operation
|
|
562
|
-
LogError('Failed to cache component:');
|
|
563
|
-
}
|
|
564
|
-
}
|
|
565
|
-
|
|
566
|
-
/**
|
|
567
|
-
* Map component type string to entity enum
|
|
568
|
-
*/
|
|
569
|
-
private mapComponentType(type: string): MJComponentEntity['Type'] {
|
|
570
|
-
const typeMap: Record<string, MJComponentEntity['Type']> = {
|
|
571
|
-
'report': 'Report',
|
|
572
|
-
'dashboard': 'Dashboard',
|
|
573
|
-
'form': 'Form',
|
|
574
|
-
'table': 'Table',
|
|
575
|
-
'chart': 'Chart',
|
|
576
|
-
'navigation': 'Navigation',
|
|
577
|
-
'search': 'Search',
|
|
578
|
-
'widget': 'Widget',
|
|
579
|
-
'utility': 'Utility',
|
|
580
|
-
'other': 'Other'
|
|
581
|
-
};
|
|
582
|
-
|
|
583
|
-
return typeMap[type.toLowerCase()] || 'Other';
|
|
584
|
-
}
|
|
585
|
-
|
|
533
|
+
|
|
586
534
|
/**
|
|
587
535
|
* Map search result to GraphQL type
|
|
588
536
|
*/
|
|
@@ -43,6 +43,15 @@ export class QueryPermissionInputType {
|
|
|
43
43
|
RoleID!: string;
|
|
44
44
|
}
|
|
45
45
|
|
|
46
|
+
@InputType()
|
|
47
|
+
export class QueryParameterHintInput {
|
|
48
|
+
@Field(() => String)
|
|
49
|
+
Name!: string;
|
|
50
|
+
|
|
51
|
+
@Field(() => String)
|
|
52
|
+
Value!: string;
|
|
53
|
+
}
|
|
54
|
+
|
|
46
55
|
@InputType()
|
|
47
56
|
export class CreateQuerySystemUserInput {
|
|
48
57
|
@Field(() => String)
|
|
@@ -98,6 +107,9 @@ export class CreateQuerySystemUserInput {
|
|
|
98
107
|
|
|
99
108
|
@Field(() => [QueryPermissionInputType], { nullable: true })
|
|
100
109
|
Permissions?: QueryPermissionInputType[];
|
|
110
|
+
|
|
111
|
+
@Field(() => [QueryParameterHintInput], { nullable: true })
|
|
112
|
+
ParameterHints?: QueryParameterHintInput[];
|
|
101
113
|
}
|
|
102
114
|
|
|
103
115
|
@InputType()
|
|
@@ -158,6 +170,9 @@ export class UpdateQuerySystemUserInput {
|
|
|
158
170
|
|
|
159
171
|
@Field(() => [QueryPermissionInputType], { nullable: true })
|
|
160
172
|
Permissions?: QueryPermissionInputType[];
|
|
173
|
+
|
|
174
|
+
@Field(() => [QueryParameterHintInput], { nullable: true })
|
|
175
|
+
ParameterHints?: QueryParameterHintInput[];
|
|
161
176
|
}
|
|
162
177
|
|
|
163
178
|
/**
|
|
@@ -246,7 +261,7 @@ export class MJQueryResolverExtended extends MJQueryResolver {
|
|
|
246
261
|
const record = await provider.GetEntityObject<MJQueryEntityServer>("MJ: Queries", context.userPayload.userRecord);
|
|
247
262
|
|
|
248
263
|
// Destructure out non-database fields, keep only fields to persist
|
|
249
|
-
const { Permissions: _permissions, CategoryPath: _categoryPath, ...fieldsToSet } = {
|
|
264
|
+
const { Permissions: _permissions, CategoryPath: _categoryPath, ParameterHints: _parameterHints, ...fieldsToSet } = {
|
|
250
265
|
...input,
|
|
251
266
|
CategoryID: finalCategoryID || input.CategoryID,
|
|
252
267
|
Status: input.Status || 'Approved',
|
|
@@ -259,6 +274,12 @@ export class MJQueryResolverExtended extends MJQueryResolver {
|
|
|
259
274
|
};
|
|
260
275
|
|
|
261
276
|
record.SetMany(fieldsToSet, true);
|
|
277
|
+
|
|
278
|
+
// Pass caller-provided parameter sample values to override LLM guesses
|
|
279
|
+
if (input.ParameterHints && input.ParameterHints.length > 0) {
|
|
280
|
+
record.ParameterHints = new Map(input.ParameterHints.map(h => [h.Name, h.Value]));
|
|
281
|
+
}
|
|
282
|
+
|
|
262
283
|
this.ListenForEntityMessages(record, pubSub, context.userPayload.userRecord);
|
|
263
284
|
|
|
264
285
|
// Attempt to save the query
|
|
@@ -480,6 +501,11 @@ export class MJQueryResolverExtended extends MJQueryResolver {
|
|
|
480
501
|
// Use SetMany to update all fields at once
|
|
481
502
|
queryEntity.SetMany(updateFields);
|
|
482
503
|
|
|
504
|
+
// Pass caller-provided parameter sample values to override LLM guesses
|
|
505
|
+
if (input.ParameterHints && input.ParameterHints.length > 0) {
|
|
506
|
+
queryEntity.ParameterHints = new Map(input.ParameterHints.map(h => [h.Name, h.Value]));
|
|
507
|
+
}
|
|
508
|
+
|
|
483
509
|
// Save the updated query
|
|
484
510
|
const saveResult = await queryEntity.Save();
|
|
485
511
|
if (!saveResult) {
|