@memberjunction/server 5.25.0 → 5.26.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/config.d.ts +70 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +21 -0
- package/dist/config.js.map +1 -1
- package/dist/generated/generated.d.ts +6 -0
- package/dist/generated/generated.d.ts.map +1 -1
- package/dist/generated/generated.js +24 -0
- package/dist/generated/generated.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +16 -2
- package/dist/index.js.map +1 -1
- package/dist/resolvers/CacheStatsResolver.d.ts +31 -0
- package/dist/resolvers/CacheStatsResolver.d.ts.map +1 -0
- package/dist/resolvers/CacheStatsResolver.js +181 -0
- package/dist/resolvers/CacheStatsResolver.js.map +1 -0
- package/package.json +66 -63
- package/src/config.ts +24 -0
- package/src/generated/generated.ts +18 -0
- package/src/index.ts +16 -2
- package/src/resolvers/CacheStatsResolver.ts +142 -0
package/src/config.ts
CHANGED
|
@@ -192,6 +192,19 @@ const serverExtensionSchema = z.object({
|
|
|
192
192
|
Settings: z.record(z.unknown()).default({})
|
|
193
193
|
}).passthrough();
|
|
194
194
|
|
|
195
|
+
const cacheSettingsSchema = z.object({
|
|
196
|
+
/** Maximum total estimated memory for all cached results in MB. Default: 150. Set to 0 to disable memory-based eviction. */
|
|
197
|
+
maxMemoryMB: z.number().optional().default(150),
|
|
198
|
+
/** Maximum percentage of total cache memory that any single entity can occupy. Default: 50. Set to 0 to disable. */
|
|
199
|
+
maxPercentOfCachePerEntity: z.number().optional().default(50),
|
|
200
|
+
/** Default TTL in seconds. 0 = no TTL, rely on event-based invalidation. Default: 0. */
|
|
201
|
+
defaultTTLSeconds: z.number().optional().default(0),
|
|
202
|
+
/** Interval in seconds for periodic eviction sweep. 0 = disabled. Default: 300 (5 minutes). */
|
|
203
|
+
evictionSweepIntervalSeconds: z.number().optional().default(300),
|
|
204
|
+
/** Enable verbose cache logging (hits, misses, evictions). Default: false. */
|
|
205
|
+
verboseLogging: z.boolean().optional().default(false),
|
|
206
|
+
});
|
|
207
|
+
|
|
195
208
|
const configInfoSchema = z.object({
|
|
196
209
|
userHandling: userHandlingInfoSchema,
|
|
197
210
|
databaseSettings: databaseSettingsInfoSchema,
|
|
@@ -206,6 +219,7 @@ const configInfoSchema = z.object({
|
|
|
206
219
|
queryDialects: queryDialectSchema.optional().default({}),
|
|
207
220
|
multiTenancy: multiTenancySchema.optional().default({}),
|
|
208
221
|
serverExtensions: z.array(serverExtensionSchema).optional().default([]),
|
|
222
|
+
cacheSettings: cacheSettingsSchema.optional().default({}),
|
|
209
223
|
|
|
210
224
|
apiKey: z.string().optional(),
|
|
211
225
|
baseUrl: z.string().default('http://localhost'),
|
|
@@ -252,6 +266,7 @@ export type TelemetryConfig = z.infer<typeof telemetrySchema>;
|
|
|
252
266
|
export type QueryDialectConfig = z.infer<typeof queryDialectSchema>;
|
|
253
267
|
export type MultiTenancyConfig = z.infer<typeof multiTenancySchema>;
|
|
254
268
|
export type ServerExtensionConfig = z.infer<typeof serverExtensionSchema>;
|
|
269
|
+
export type CacheSettingsConfig = z.infer<typeof cacheSettingsSchema>;
|
|
255
270
|
export type ConfigInfo = z.infer<typeof configInfoSchema>;
|
|
256
271
|
|
|
257
272
|
/**
|
|
@@ -378,6 +393,15 @@ export const DEFAULT_SERVER_CONFIG: Partial<ConfigInfo> = {
|
|
|
378
393
|
level: 'standard'
|
|
379
394
|
},
|
|
380
395
|
|
|
396
|
+
// Cache settings defaults
|
|
397
|
+
cacheSettings: {
|
|
398
|
+
maxMemoryMB: 150,
|
|
399
|
+
maxPercentOfCachePerEntity: 50,
|
|
400
|
+
defaultTTLSeconds: 0,
|
|
401
|
+
evictionSweepIntervalSeconds: 300,
|
|
402
|
+
verboseLogging: false,
|
|
403
|
+
},
|
|
404
|
+
|
|
381
405
|
// Auth providers (environment-driven)
|
|
382
406
|
authProviders: [
|
|
383
407
|
// Microsoft Azure AD / Entra ID
|
|
@@ -37711,6 +37711,12 @@ export class MJEntity_ {
|
|
|
37711
37711
|
@Field(() => Boolean, {description: `When true (default), CodeGen can automatically set SupportsGeoCoding based on LLM analysis of entity fields. Set to 0 to lock the value and prevent CodeGen from changing it.`})
|
|
37712
37712
|
AutoUpdateSupportsGeoCoding: boolean;
|
|
37713
37713
|
|
|
37714
|
+
@Field(() => Boolean, {description: `Controls whether this entity participates in server-side and client-side caching. When false, all cache operations (PreRunView checks, auto-cache storage, BaseEntity event fingerprint scans, client-side IndexedDB cache) are skipped entirely. This column is the single source of truth at runtime; schema-level defaults are applied at CodeGen time via newEntityDefaults.AllowCachingBySchema.`})
|
|
37715
|
+
AllowCaching: boolean;
|
|
37716
|
+
|
|
37717
|
+
@Field(() => Boolean, {description: `When set to 1 AND TrackRecordChanges is also 1, the external change detection system will scan this entity for changes made outside the MJ framework (direct SQL, third-party tools, etc.) and replay them through Save() to create proper RecordChange audit entries. Default is 0 (opt-out) because most entities, especially __mj schema metadata tables, are managed by migrations/CodeGen and should not be scanned.`})
|
|
37718
|
+
DetectExternalChanges: boolean;
|
|
37719
|
+
|
|
37714
37720
|
@Field({nullable: true, description: `Schema-based programmatic code name derived from the entity Name. Uses GetClassNameSchemaPrefix(SchemaName) as the prefix, then strips EntityNamePrefix from the Name and removes spaces. For "__mj" schema with entity "MJ: AI Models", this produces "MJAIModels". For entities in other schemas, the sanitized schema name is prepended. Used in GraphQL type generation and internal code references.`})
|
|
37715
37721
|
CodeName?: string;
|
|
37716
37722
|
|
|
@@ -38080,6 +38086,12 @@ export class CreateMJEntityInput {
|
|
|
38080
38086
|
|
|
38081
38087
|
@Field(() => Boolean, { nullable: true })
|
|
38082
38088
|
AutoUpdateSupportsGeoCoding?: boolean;
|
|
38089
|
+
|
|
38090
|
+
@Field(() => Boolean, { nullable: true })
|
|
38091
|
+
AllowCaching?: boolean;
|
|
38092
|
+
|
|
38093
|
+
@Field(() => Boolean, { nullable: true })
|
|
38094
|
+
DetectExternalChanges?: boolean;
|
|
38083
38095
|
}
|
|
38084
38096
|
|
|
38085
38097
|
|
|
@@ -38265,6 +38277,12 @@ export class UpdateMJEntityInput {
|
|
|
38265
38277
|
@Field(() => Boolean, { nullable: true })
|
|
38266
38278
|
AutoUpdateSupportsGeoCoding?: boolean;
|
|
38267
38279
|
|
|
38280
|
+
@Field(() => Boolean, { nullable: true })
|
|
38281
|
+
AllowCaching?: boolean;
|
|
38282
|
+
|
|
38283
|
+
@Field(() => Boolean, { nullable: true })
|
|
38284
|
+
DetectExternalChanges?: boolean;
|
|
38285
|
+
|
|
38268
38286
|
@Field(() => [KeyValuePairInput], { nullable: true })
|
|
38269
38287
|
OldValues___?: KeyValuePairInput[];
|
|
38270
38288
|
}
|
package/src/index.ts
CHANGED
|
@@ -128,6 +128,7 @@ export * from './resolvers/TelemetryResolver.js';
|
|
|
128
128
|
export * from './resolvers/APIKeyResolver.js';
|
|
129
129
|
export * from './resolvers/MCPResolver.js';
|
|
130
130
|
export * from './resolvers/ActionResolver.js';
|
|
131
|
+
export * from './resolvers/CacheStatsResolver.js';
|
|
131
132
|
export * from './resolvers/EntityCommunicationsResolver.js';
|
|
132
133
|
export * from './resolvers/EntityResolver.js';
|
|
133
134
|
export * from './resolvers/ISAEntityResolver.js';
|
|
@@ -451,8 +452,21 @@ export const serve = async (resolverPaths: Array<string>, app: Application = cre
|
|
|
451
452
|
}
|
|
452
453
|
// Ensure LocalCacheManager is initialized (no-op if already done during engine loading)
|
|
453
454
|
if (!LocalCacheManager.Instance.IsInitialized) {
|
|
454
|
-
|
|
455
|
-
|
|
455
|
+
// Build cache config from mj.config.cjs cacheSettings
|
|
456
|
+
const cs = configInfo.cacheSettings;
|
|
457
|
+
const cacheConfig = {
|
|
458
|
+
maxSizeBytes: (cs.maxMemoryMB ?? 150) * 1024 * 1024,
|
|
459
|
+
maxPercentOfCachePerEntity: cs.maxPercentOfCachePerEntity ?? 50,
|
|
460
|
+
defaultTTLMs: (cs.defaultTTLSeconds ?? 0) * 1000,
|
|
461
|
+
evictionSweepIntervalMs: (cs.evictionSweepIntervalSeconds ?? 300) * 1000,
|
|
462
|
+
verboseLogging: cs.verboseLogging ?? false,
|
|
463
|
+
};
|
|
464
|
+
await LocalCacheManager.Instance.Initialize(Metadata.Provider.LocalStorageProvider, cacheConfig);
|
|
465
|
+
console.log('LocalCacheManager initialized with cache config:', JSON.stringify({
|
|
466
|
+
maxMemoryMB: cs.maxMemoryMB ?? 150,
|
|
467
|
+
maxPercentOfCachePerEntity: cs.maxPercentOfCachePerEntity ?? 50,
|
|
468
|
+
evictionSweepIntervalSeconds: cs.evictionSweepIntervalSeconds ?? 300,
|
|
469
|
+
}));
|
|
456
470
|
}
|
|
457
471
|
|
|
458
472
|
// Initialize APIKeyEngine singleton — reads apiKeyGeneration from mj.config.cjs automatically
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
import { Field, Float, Int, ObjectType, Query, Resolver } from 'type-graphql';
|
|
2
|
+
import { LocalCacheManager, CacheStats, CacheEntryType } from '@memberjunction/core';
|
|
3
|
+
import { RequireSystemUser } from '../directives/index.js';
|
|
4
|
+
|
|
5
|
+
// ============================================================================
|
|
6
|
+
// GraphQL Object Types
|
|
7
|
+
// ============================================================================
|
|
8
|
+
|
|
9
|
+
@ObjectType()
|
|
10
|
+
class CacheStatsByTypeGQL {
|
|
11
|
+
@Field(() => String)
|
|
12
|
+
Type: CacheEntryType;
|
|
13
|
+
|
|
14
|
+
@Field(() => Int)
|
|
15
|
+
Count: number;
|
|
16
|
+
|
|
17
|
+
@Field(() => Int)
|
|
18
|
+
SizeBytes: number;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
@ObjectType()
|
|
22
|
+
class CacheStatsGQL {
|
|
23
|
+
@Field(() => Int)
|
|
24
|
+
TotalEntries: number;
|
|
25
|
+
|
|
26
|
+
@Field(() => Int)
|
|
27
|
+
TotalSizeBytes: number;
|
|
28
|
+
|
|
29
|
+
@Field(() => [CacheStatsByTypeGQL])
|
|
30
|
+
ByType: CacheStatsByTypeGQL[];
|
|
31
|
+
|
|
32
|
+
@Field(() => Float)
|
|
33
|
+
OldestEntry: number;
|
|
34
|
+
|
|
35
|
+
@Field(() => Float)
|
|
36
|
+
NewestEntry: number;
|
|
37
|
+
|
|
38
|
+
@Field(() => Int)
|
|
39
|
+
Hits: number;
|
|
40
|
+
|
|
41
|
+
@Field(() => Int)
|
|
42
|
+
Misses: number;
|
|
43
|
+
|
|
44
|
+
@Field(() => Float)
|
|
45
|
+
HitRate: number;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
@ObjectType()
|
|
49
|
+
class CacheEntityBreakdownGQL {
|
|
50
|
+
@Field(() => String)
|
|
51
|
+
EntityName: string;
|
|
52
|
+
|
|
53
|
+
@Field(() => Int)
|
|
54
|
+
EntryCount: number;
|
|
55
|
+
|
|
56
|
+
@Field(() => Int)
|
|
57
|
+
TotalSizeBytes: number;
|
|
58
|
+
|
|
59
|
+
@Field(() => Int)
|
|
60
|
+
TotalAccessCount: number;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
@ObjectType()
|
|
64
|
+
class CacheStatsDetailGQL extends CacheStatsGQL {
|
|
65
|
+
@Field(() => [CacheEntityBreakdownGQL])
|
|
66
|
+
EntityBreakdown: CacheEntityBreakdownGQL[];
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// ============================================================================
|
|
70
|
+
// Helpers (pure core)
|
|
71
|
+
// ============================================================================
|
|
72
|
+
|
|
73
|
+
function toGQL(stats: CacheStats): CacheStatsGQL {
|
|
74
|
+
const gql = new CacheStatsGQL();
|
|
75
|
+
gql.TotalEntries = stats.totalEntries;
|
|
76
|
+
gql.TotalSizeBytes = stats.totalSizeBytes;
|
|
77
|
+
gql.ByType = (['dataset', 'runview', 'runquery'] as const).map(t => {
|
|
78
|
+
const bt = stats.byType[t];
|
|
79
|
+
const entry = new CacheStatsByTypeGQL();
|
|
80
|
+
entry.Type = t;
|
|
81
|
+
entry.Count = bt.count;
|
|
82
|
+
entry.SizeBytes = bt.sizeBytes;
|
|
83
|
+
return entry;
|
|
84
|
+
});
|
|
85
|
+
gql.OldestEntry = stats.oldestEntry;
|
|
86
|
+
gql.NewestEntry = stats.newestEntry;
|
|
87
|
+
gql.Hits = stats.hits;
|
|
88
|
+
gql.Misses = stats.misses;
|
|
89
|
+
gql.HitRate = (stats.hits + stats.misses) > 0
|
|
90
|
+
? (stats.hits / (stats.hits + stats.misses)) * 100
|
|
91
|
+
: 0;
|
|
92
|
+
return gql;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function buildEntityBreakdown(): CacheEntityBreakdownGQL[] {
|
|
96
|
+
const entries = LocalCacheManager.Instance.GetAllEntries();
|
|
97
|
+
const entityMap = new Map<string, { count: number; sizeBytes: number; accessCount: number }>();
|
|
98
|
+
|
|
99
|
+
for (const entry of entries) {
|
|
100
|
+
if (entry.type !== 'runview' || !entry.name) continue;
|
|
101
|
+
const existing = entityMap.get(entry.name) ?? { count: 0, sizeBytes: 0, accessCount: 0 };
|
|
102
|
+
existing.count++;
|
|
103
|
+
existing.sizeBytes += entry.sizeBytes;
|
|
104
|
+
existing.accessCount += entry.accessCount;
|
|
105
|
+
entityMap.set(entry.name, existing);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
return [...entityMap.entries()]
|
|
109
|
+
.map(([name, data]) => {
|
|
110
|
+
const gql = new CacheEntityBreakdownGQL();
|
|
111
|
+
gql.EntityName = name;
|
|
112
|
+
gql.EntryCount = data.count;
|
|
113
|
+
gql.TotalSizeBytes = data.sizeBytes;
|
|
114
|
+
gql.TotalAccessCount = data.accessCount;
|
|
115
|
+
return gql;
|
|
116
|
+
})
|
|
117
|
+
.sort((a, b) => b.EntryCount - a.EntryCount);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// ============================================================================
|
|
121
|
+
// Resolver
|
|
122
|
+
// ============================================================================
|
|
123
|
+
|
|
124
|
+
@Resolver()
|
|
125
|
+
export class CacheStatsResolver {
|
|
126
|
+
@RequireSystemUser()
|
|
127
|
+
@Query(() => CacheStatsGQL)
|
|
128
|
+
CacheStats(): CacheStatsGQL {
|
|
129
|
+
const stats = LocalCacheManager.Instance.GetStats();
|
|
130
|
+
return toGQL(stats);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
@RequireSystemUser()
|
|
134
|
+
@Query(() => CacheStatsDetailGQL)
|
|
135
|
+
CacheStatsDetail(): CacheStatsDetailGQL {
|
|
136
|
+
const stats = LocalCacheManager.Instance.GetStats();
|
|
137
|
+
const result = new CacheStatsDetailGQL();
|
|
138
|
+
Object.assign(result, toGQL(stats));
|
|
139
|
+
result.EntityBreakdown = buildEntityBreakdown();
|
|
140
|
+
return result;
|
|
141
|
+
}
|
|
142
|
+
}
|