@memberjunction/server 5.25.0 → 5.27.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@memberjunction/server",
3
- "version": "5.25.0",
3
+ "version": "5.27.0",
4
4
  "description": "MemberJunction: This project provides API access via GraphQL to the common data store.",
5
5
  "main": "./dist/index.js",
6
6
  "types": "./src/index.ts",
@@ -27,68 +27,71 @@
27
27
  "@as-integrations/express5": "^1.0.0",
28
28
  "@graphql-tools/schema": "latest",
29
29
  "@graphql-tools/utils": "^11.0.0",
30
- "@memberjunction/actions": "5.25.0",
31
- "@memberjunction/actions-apollo": "5.25.0",
32
- "@memberjunction/actions-base": "5.25.0",
33
- "@memberjunction/actions-bizapps-accounting": "5.25.0",
34
- "@memberjunction/actions-bizapps-crm": "5.25.0",
35
- "@memberjunction/actions-bizapps-formbuilders": "5.25.0",
36
- "@memberjunction/actions-bizapps-lms": "5.25.0",
37
- "@memberjunction/actions-bizapps-social": "5.25.0",
38
- "@memberjunction/ai": "5.25.0",
39
- "@memberjunction/ai-agent-manager": "5.25.0",
40
- "@memberjunction/ai-agent-manager-actions": "5.25.0",
41
- "@memberjunction/ai-agents": "5.25.0",
42
- "@memberjunction/ai-core-plus": "5.25.0",
43
- "@memberjunction/ai-mcp-client": "5.25.0",
44
- "@memberjunction/ai-prompts": "5.25.0",
45
- "@memberjunction/ai-provider-bundle": "5.25.0",
46
- "@memberjunction/ai-vector-sync": "5.25.0",
47
- "@memberjunction/ai-vectors-pinecone": "5.25.0",
48
- "@memberjunction/aiengine": "5.25.0",
49
- "@memberjunction/api-keys": "5.25.0",
50
- "@memberjunction/auth-providers": "5.25.0",
51
- "@memberjunction/codegen-lib": "5.25.0",
52
- "@memberjunction/communication-ms-graph": "5.25.0",
53
- "@memberjunction/communication-sendgrid": "5.25.0",
54
- "@memberjunction/communication-types": "5.25.0",
55
- "@memberjunction/component-registry-client-sdk": "5.25.0",
56
- "@memberjunction/computer-use-engine": "5.25.0",
57
- "@memberjunction/config": "5.25.0",
58
- "@memberjunction/core": "5.25.0",
59
- "@memberjunction/core-actions": "5.25.0",
60
- "@memberjunction/core-entities": "5.25.0",
61
- "@memberjunction/core-entities-server": "5.25.0",
62
- "@memberjunction/data-context": "5.25.0",
63
- "@memberjunction/data-context-server": "5.25.0",
64
- "@memberjunction/doc-utils": "5.25.0",
65
- "@memberjunction/encryption": "5.25.0",
66
- "@memberjunction/entity-communications-base": "5.25.0",
67
- "@memberjunction/entity-communications-server": "5.25.0",
68
- "@memberjunction/external-change-detection": "5.25.0",
69
- "@memberjunction/generic-database-provider": "5.25.0",
70
- "@memberjunction/global": "5.25.0",
71
- "@memberjunction/graphql-dataprovider": "5.25.0",
72
- "@memberjunction/integration-engine": "5.25.0",
73
- "@memberjunction/integration-schema-builder": "5.25.0",
74
- "@memberjunction/interactive-component-types": "5.25.0",
75
- "@memberjunction/notifications": "5.25.0",
76
- "@memberjunction/postgresql-dataprovider": "5.25.0",
77
- "@memberjunction/queue": "5.25.0",
78
- "@memberjunction/redis-provider": "5.25.0",
79
- "@memberjunction/scheduling-actions": "5.25.0",
80
- "@memberjunction/scheduling-base-types": "5.25.0",
81
- "@memberjunction/scheduling-engine": "5.25.0",
82
- "@memberjunction/scheduling-engine-base": "5.25.0",
83
- "@memberjunction/server-extensions-core": "5.25.0",
84
- "@memberjunction/skip-types": "5.25.0",
85
- "@memberjunction/sql-dialect": "5.25.0",
86
- "@memberjunction/sqlserver-dataprovider": "5.25.0",
87
- "@memberjunction/storage": "5.25.0",
88
- "@memberjunction/templates": "5.25.0",
89
- "@memberjunction/testing-engine": "5.25.0",
90
- "@memberjunction/testing-engine-base": "5.25.0",
91
- "@memberjunction/version-history": "5.25.0",
30
+ "@memberjunction/actions": "5.27.0",
31
+ "@memberjunction/actions-apollo": "5.27.0",
32
+ "@memberjunction/actions-base": "5.27.0",
33
+ "@memberjunction/actions-bizapps-accounting": "5.27.0",
34
+ "@memberjunction/actions-bizapps-crm": "5.27.0",
35
+ "@memberjunction/actions-bizapps-formbuilders": "5.27.0",
36
+ "@memberjunction/actions-bizapps-lms": "5.27.0",
37
+ "@memberjunction/actions-bizapps-social": "5.27.0",
38
+ "@memberjunction/ai": "5.27.0",
39
+ "@memberjunction/ai-agent-manager": "5.27.0",
40
+ "@memberjunction/ai-agent-manager-actions": "5.27.0",
41
+ "@memberjunction/ai-agents": "5.27.0",
42
+ "@memberjunction/ai-core-plus": "5.27.0",
43
+ "@memberjunction/ai-mcp-client": "5.27.0",
44
+ "@memberjunction/ai-prompts": "5.27.0",
45
+ "@memberjunction/ai-provider-bundle": "5.27.0",
46
+ "@memberjunction/ai-vector-sync": "5.27.0",
47
+ "@memberjunction/ai-vectordb": "5.27.0",
48
+ "@memberjunction/ai-vectors-pinecone": "5.27.0",
49
+ "@memberjunction/aiengine": "5.27.0",
50
+ "@memberjunction/api-keys": "5.27.0",
51
+ "@memberjunction/auth-providers": "5.27.0",
52
+ "@memberjunction/codegen-lib": "5.27.0",
53
+ "@memberjunction/communication-ms-graph": "5.27.0",
54
+ "@memberjunction/communication-sendgrid": "5.27.0",
55
+ "@memberjunction/communication-types": "5.27.0",
56
+ "@memberjunction/component-registry-client-sdk": "5.27.0",
57
+ "@memberjunction/computer-use-engine": "5.27.0",
58
+ "@memberjunction/config": "5.27.0",
59
+ "@memberjunction/core": "5.27.0",
60
+ "@memberjunction/core-actions": "5.27.0",
61
+ "@memberjunction/core-entities": "5.27.0",
62
+ "@memberjunction/core-entities-server": "5.27.0",
63
+ "@memberjunction/data-context": "5.27.0",
64
+ "@memberjunction/data-context-server": "5.27.0",
65
+ "@memberjunction/doc-utils": "5.27.0",
66
+ "@memberjunction/encryption": "5.27.0",
67
+ "@memberjunction/entity-communications-base": "5.27.0",
68
+ "@memberjunction/entity-communications-server": "5.27.0",
69
+ "@memberjunction/external-change-detection": "5.27.0",
70
+ "@memberjunction/generic-database-provider": "5.27.0",
71
+ "@memberjunction/global": "5.27.0",
72
+ "@memberjunction/graphql-dataprovider": "5.27.0",
73
+ "@memberjunction/integration-engine": "5.27.0",
74
+ "@memberjunction/integration-schema-builder": "5.27.0",
75
+ "@memberjunction/interactive-component-types": "5.27.0",
76
+ "@memberjunction/notifications": "5.27.0",
77
+ "@memberjunction/postgresql-dataprovider": "5.27.0",
78
+ "@memberjunction/queue": "5.27.0",
79
+ "@memberjunction/redis-provider": "5.27.0",
80
+ "@memberjunction/scheduling-actions": "5.27.0",
81
+ "@memberjunction/scheduling-base-types": "5.27.0",
82
+ "@memberjunction/scheduling-engine": "5.27.0",
83
+ "@memberjunction/scheduling-engine-base": "5.27.0",
84
+ "@memberjunction/schema-engine": "5.27.0",
85
+ "@memberjunction/search-engine": "5.27.0",
86
+ "@memberjunction/server-extensions-core": "5.27.0",
87
+ "@memberjunction/skip-types": "5.27.0",
88
+ "@memberjunction/sql-dialect": "5.27.0",
89
+ "@memberjunction/sqlserver-dataprovider": "5.27.0",
90
+ "@memberjunction/storage": "5.27.0",
91
+ "@memberjunction/templates": "5.27.0",
92
+ "@memberjunction/testing-engine": "5.27.0",
93
+ "@memberjunction/testing-engine-base": "5.27.0",
94
+ "@memberjunction/version-history": "5.27.0",
92
95
  "@types/compression": "^1.8.1",
93
96
  "@types/cors": "^2.8.19",
94
97
  "@types/jsonwebtoken": "9.0.10",
@@ -17,12 +17,6 @@ import {
17
17
  SkipAPIRequestAPIKey,
18
18
  SkipQueryInfo,
19
19
  SkipQueryCatalogEntry,
20
- SkipEntityInfo,
21
- SkipEntityFieldInfo,
22
- SkipEntityFieldValueInfo,
23
- SkipEntityRelationshipInfo,
24
- SkipEntityOrganicKeyInfo,
25
- SkipEntityOrganicKeyRelatedEntityInfo,
26
20
  SkipAPIAgentNote,
27
21
  SkipAPIAgentNoteType,
28
22
  SkipAPIArtifact,
@@ -30,7 +24,7 @@ import {
30
24
  SkipAPIArtifactType
31
25
  } from '@memberjunction/skip-types';
32
26
  import { DataContext } from '@memberjunction/data-context';
33
- import { UserInfo, LogStatus, LogError, Metadata, RunQuery, EntityInfo, EntityFieldInfo, EntityRelationshipInfo, EntityOrganicKeyInfo, EntityOrganicKeyRelatedEntityInfo } from '@memberjunction/core';
27
+ import { UserInfo, LogStatus, LogError, Metadata, RunQuery, EntityInfo, EntityFieldInfo, EntityFieldValueInfo, DatabaseProviderBase } from '@memberjunction/core';
34
28
  import { request as httpRequest } from 'http';
35
29
  import { request as httpsRequest } from 'https';
36
30
  import { gzip as gzipCompress, createGunzip } from 'zlib';
@@ -206,7 +200,7 @@ export class SkipSDK {
206
200
  private config: SkipSDKConfig;
207
201
 
208
202
  // Static cache for Skip entities (shared across all instances)
209
- private static __skipEntitiesCache$: BehaviorSubject<Promise<SkipEntityInfo[]> | null> = new BehaviorSubject<Promise<SkipEntityInfo[]> | null>(null);
203
+ private static __skipEntitiesCache$: BehaviorSubject<Promise<EntityInfo[]> | null> = new BehaviorSubject<Promise<EntityInfo[]> | null>(null);
210
204
  private static __lastRefreshTime: number = 0;
211
205
 
212
206
  constructor(config?: SkipSDKConfig) {
@@ -396,7 +390,7 @@ export class SkipSDK {
396
390
  includeCallbackAuth: boolean,
397
391
  additionalTokenInfo: any = {}
398
392
  ): Promise<Partial<SkipAPIRequest>> {
399
- const entities = includeEntities ? await this.buildEntities(dataSource, forceEntityRefresh) : [];
393
+ const entities = includeEntities ? await this.buildEntities(forceEntityRefresh) : [];
400
394
  const queries = includeQueries ? this.buildQueries() : [];
401
395
  // Always build the lightweight query catalog for collision detection,
402
396
  // regardless of whether full queries are included
@@ -442,7 +436,7 @@ export class SkipSDK {
442
436
  * Build entity metadata for Skip
443
437
  * Copied from AskSkipResolver.BuildSkipEntities - uses cached metadata with refresh logic
444
438
  */
445
- private async buildEntities(dataSource: mssql.ConnectionPool, forceRefresh: boolean, refreshIntervalMinutes: number = 15): Promise<SkipEntityInfo[]> {
439
+ private async buildEntities(forceRefresh: boolean, refreshIntervalMinutes: number = 15): Promise<EntityInfo[]> {
446
440
  try {
447
441
  const now = Date.now();
448
442
  const cacheExpired = (now - SkipSDK.__lastRefreshTime) > (refreshIntervalMinutes * 60 * 1000);
@@ -450,7 +444,7 @@ export class SkipSDK {
450
444
  // If force refresh is requested OR cache expired OR cache is empty, refresh
451
445
  if (forceRefresh || cacheExpired || SkipSDK.__skipEntitiesCache$.value === null) {
452
446
  LogStatus(`[SkipSDK] Refreshing Skip entities cache (force: ${forceRefresh}, expired: ${cacheExpired})`);
453
- const newData = this.refreshSkipEntities(dataSource);
447
+ const newData = this.refreshSkipEntities();
454
448
  SkipSDK.__skipEntitiesCache$.next(newData);
455
449
  }
456
450
 
@@ -856,9 +850,11 @@ export class SkipSDK {
856
850
  /**
857
851
  * Refreshes the Skip entities cache
858
852
  * Rebuilds the entity information that is provided to Skip
859
- * Copied from AskSkipResolver.refreshSkipEntities
853
+ * Refreshes the entity metadata cache. Filters entities by schema/scope config,
854
+ * filters fields by AI scope, and enriches field values from the database.
855
+ * Returns EntityInfo objects directly — no intermediate Skip-specific types needed.
860
856
  */
861
- private async refreshSkipEntities(dataSource: mssql.ConnectionPool): Promise<SkipEntityInfo[]> {
857
+ private async refreshSkipEntities(): Promise<EntityInfo[]> {
862
858
  try {
863
859
  const md = new Metadata();
864
860
 
@@ -891,12 +887,12 @@ export class SkipSDK {
891
887
  LogError(`[SkipSDK.refreshSkipEntities] WARNING: No entities passed filtering! This will result in empty Skip entities list.`);
892
888
  }
893
889
 
894
- // Now we have our list of entities, pack em up
895
- const result = await Promise.all(entities.map((e) => this.packSingleSkipEntityInfo(e, dataSource)));
890
+ // Build enriched EntityInfo objects with filtered fields and packed values
891
+ const result = await Promise.all(entities.map((e) => this.buildEntityForSkip(e)));
896
892
 
897
893
  LogStatus(`[SkipSDK.refreshSkipEntities] Successfully packed ${result.length} entities for Skip`);
898
894
 
899
- SkipSDK.__lastRefreshTime = Date.now(); // Update last refresh time
895
+ SkipSDK.__lastRefreshTime = Date.now();
900
896
  return result;
901
897
  }
902
898
  catch (e) {
@@ -906,258 +902,95 @@ export class SkipSDK {
906
902
  }
907
903
 
908
904
  /**
909
- * Packs information about a single entity for Skip
910
- * Includes fields, relationships, and sample data
911
- * Copied from AskSkipResolver.PackSingleSkipEntityInfo
905
+ * Builds an EntityInfo object for Skip, filtering fields by AI scope and
906
+ * enriching field values from the database. Returns a new EntityInfo
907
+ * constructed from a plain object so it serializes cleanly via toJSON().
912
908
  */
913
- private async packSingleSkipEntityInfo(e: EntityInfo, dataSource: mssql.ConnectionPool): Promise<SkipEntityInfo> {
909
+ private async buildEntityForSkip(e: EntityInfo): Promise<EntityInfo> {
914
910
  try {
915
- const ret: SkipEntityInfo = {
916
- id: e.ID,
917
- name: e.Name,
918
- schemaName: e.SchemaName,
919
- baseView: e.BaseView,
920
- description: e.Description,
921
-
922
- fields: await Promise.all(e.Fields.filter(f => {
923
- // we want to check the scopes for the field level and make sure it is either All or AI or has both
924
- const scopes = f.ScopeDefault?.split(',').map((s) => s.trim().toLowerCase());
925
- return !scopes || scopes.length === 0 || scopes.includes('all') || scopes.includes('ai');
926
- }).map(f => {
927
- return this.packSingleSkipEntityField(f, dataSource);
928
- })),
929
-
930
- relatedEntities: e.RelatedEntities.map((r) => {
931
- return this.packSingleSkipEntityRelationship(r);
932
- }),
933
-
934
- organicKeys: (e.OrganicKeys ?? [])
935
- .filter((ok) => ok.Status === 'Active')
936
- .map((ok) => this.packSingleSkipOrganicKey(ok))
937
- .filter((ok): ok is SkipEntityOrganicKeyInfo => ok !== null)
938
- };
939
- return ret;
940
- }
941
- catch (e) {
942
- LogError(`[SkipSDK] packSingleSkipEntityInfo error: ${e}`);
943
- return null;
944
- }
945
- }
946
-
947
- /**
948
- * Packs information about a single organic key for Skip.
949
- * Organic keys express cross-entity relationships via shared business data
950
- * (email, acronym, etc.) rather than database FK constraints.
951
- */
952
- private packSingleSkipOrganicKey(ok: EntityOrganicKeyInfo): SkipEntityOrganicKeyInfo | null {
953
- try {
954
- return {
955
- id: ok.ID,
956
- name: ok.Name,
957
- description: ok.Description ?? undefined,
958
- matchFieldNames: ok.MatchFieldNamesArray,
959
- normalizationStrategy: ok.NormalizationStrategy,
960
- customNormalizationExpression: ok.CustomNormalizationExpression ?? undefined,
961
- sequence: ok.Sequence,
962
- relatedEntities: ok.RelatedEntities
963
- .map((re) => this.packSingleSkipOrganicKeyRelatedEntity(re))
964
- .filter((re): re is SkipEntityOrganicKeyRelatedEntityInfo => re !== null)
965
- };
966
- }
967
- catch (e) {
968
- LogError(`[SkipSDK] packSingleSkipOrganicKey error: ${e}`);
969
- return null;
970
- }
971
- }
911
+ // Filter fields by scope (only include fields visible to AI)
912
+ const filteredFields = e.Fields.filter(f => {
913
+ const scopes = f.ScopeDefault?.split(',').map((s) => s.trim().toLowerCase());
914
+ return !scopes || scopes.length === 0 || scopes.includes('all') || scopes.includes('ai');
915
+ });
972
916
 
973
- /**
974
- * Packs information about a single organic key related entity for Skip.
975
- * Looks up schema name and base view from metadata since they are not
976
- * stored on EntityOrganicKeyRelatedEntityInfo directly.
977
- */
978
- private packSingleSkipOrganicKeyRelatedEntity(
979
- re: EntityOrganicKeyRelatedEntityInfo
980
- ): SkipEntityOrganicKeyRelatedEntityInfo | null {
981
- try {
982
- // Look up the related entity to obtain schema name and base view, which
983
- // Skip needs in order to generate schema-qualified SQL.
984
- const relatedEntity = Metadata.Provider.Entities.find(
985
- (ent) => UUIDsEqual(ent.ID, re.RelatedEntityID)
986
- );
987
- if (!relatedEntity) {
988
- LogError(
989
- `[SkipSDK] packSingleSkipOrganicKeyRelatedEntity: related entity not found for ID ${re.RelatedEntityID}`
990
- );
991
- return null;
992
- }
917
+ // Enrich each field with packed possible values
918
+ const enrichedFields = await Promise.all(filteredFields.map(f => this.enrichFieldValues(f)));
993
919
 
994
- return {
995
- id: re.ID,
996
- relatedEntityID: re.RelatedEntityID,
997
- relatedEntityName: relatedEntity.Name,
998
- relatedEntitySchemaName: relatedEntity.SchemaName,
999
- relatedEntityBaseView: relatedEntity.BaseView,
1000
- isDirectMatch: re.IsDirectMatch,
1001
- isTransitiveMatch: re.IsTransitiveMatch,
1002
- relatedEntityFieldNames: re.IsDirectMatch
1003
- ? re.RelatedEntityFieldNamesArray
1004
- : undefined,
1005
- transitiveObjectName: re.TransitiveObjectName ?? undefined,
1006
- transitiveObjectMatchFieldNames: re.IsTransitiveMatch
1007
- ? re.TransitiveObjectMatchFieldNamesArray
1008
- : undefined,
1009
- transitiveObjectOutputFieldName: re.TransitiveObjectOutputFieldName ?? undefined,
1010
- relatedEntityJoinFieldName: re.RelatedEntityJoinFieldName ?? undefined,
1011
- displayName: re.DisplayName ?? undefined,
1012
- sequence: re.Sequence
1013
- };
1014
- }
1015
- catch (e) {
1016
- LogError(`[SkipSDK] packSingleSkipOrganicKeyRelatedEntity error: ${e}`);
1017
- return null;
1018
- }
1019
- }
1020
-
1021
- /**
1022
- * Packs information about a single entity relationship
1023
- * These relationships help Skip understand the data model
1024
- * Copied from AskSkipResolver.PackSingleSkipEntityRelationship
1025
- */
1026
- private packSingleSkipEntityRelationship(r: EntityRelationshipInfo): SkipEntityRelationshipInfo {
1027
- try {
1028
- return {
1029
- entityID: r.EntityID,
1030
- relatedEntityID: r.RelatedEntityID,
1031
- type: r.Type,
1032
- entityKeyField: r.EntityKeyField,
1033
- relatedEntityJoinField: r.RelatedEntityJoinField,
1034
- joinView: r.JoinView,
1035
- joinEntityJoinField: r.JoinEntityJoinField,
1036
- joinEntityInverseJoinField: r.JoinEntityInverseJoinField,
1037
- entity: r.Entity,
1038
- entityBaseView: r.EntityBaseView,
1039
- relatedEntity: r.RelatedEntity,
1040
- relatedEntityBaseView: r.RelatedEntityBaseView,
1041
- };
920
+ // Clone the entity via toJSON, then swap in filtered+enriched fields and Skip-specific
921
+ // Active-only organic keys. Any future EntityInfo properties flow through automatically.
922
+ return new EntityInfo({
923
+ ...e.toJSON(),
924
+ Fields: enrichedFields,
925
+ OrganicKeys: e.OrganicKeys.filter(ok => ok.Status === 'Active'),
926
+ });
1042
927
  }
1043
- catch (e) {
1044
- LogError(`[SkipSDK] packSingleSkipEntityRelationship error: ${e}`);
928
+ catch (err) {
929
+ LogError(`[SkipSDK] buildEntityForSkip error: ${err}`);
1045
930
  return null;
1046
931
  }
1047
932
  }
1048
933
 
1049
934
  /**
1050
- * Packs information about a single entity field
1051
- * Includes metadata and possible values
1052
- * Copied from AskSkipResolver.PackSingleSkipEntityField
935
+ * Enriches a field's EntityFieldValues with possible values from the database.
936
+ * Returns a plain object that can be used to construct an EntityFieldInfo.
1053
937
  */
1054
- private async packSingleSkipEntityField(f: EntityFieldInfo, dataSource: mssql.ConnectionPool): Promise<SkipEntityFieldInfo> {
1055
- try {
1056
- return {
1057
- entityID: f.EntityID,
1058
- sequence: f.Sequence,
1059
- name: f.Name,
1060
- displayName: f.DisplayName,
1061
- category: f.Category,
1062
- type: f.Type,
1063
- description: f.Description,
1064
- isPrimaryKey: f.IsPrimaryKey,
1065
- allowsNull: f.AllowsNull,
1066
- isUnique: f.IsUnique,
1067
- length: f.Length,
1068
- precision: f.Precision,
1069
- scale: f.Scale,
1070
- sqlFullType: f.SQLFullType,
1071
- defaultValue: f.DefaultValue,
1072
- autoIncrement: f.AutoIncrement,
1073
- valueListType: f.ValueListType,
1074
- extendedType: f.ExtendedType,
1075
- defaultInView: f.DefaultInView,
1076
- defaultColumnWidth: f.DefaultColumnWidth,
1077
- isVirtual: f.IsVirtual,
1078
- isNameField: f.IsNameField,
1079
- relatedEntityID: f.RelatedEntityID,
1080
- relatedEntityFieldName: f.RelatedEntityFieldName,
1081
- relatedEntity: f.RelatedEntity,
1082
- relatedEntitySchemaName: f.RelatedEntitySchemaName,
1083
- relatedEntityBaseView: f.RelatedEntityBaseView,
1084
- possibleValues: await this.packFieldPossibleValues(f, dataSource),
1085
- };
1086
- }
1087
- catch (e) {
1088
- LogError(`[SkipSDK] packSingleSkipEntityField error: ${e}`);
1089
- return null;
1090
- }
938
+ private async enrichFieldValues(f: EntityFieldInfo): Promise<Record<string, unknown>> {
939
+ return {
940
+ ...f.toJSON(),
941
+ EntityFieldValues: await this.packFieldValues(f),
942
+ };
1091
943
  }
1092
944
 
1093
945
  /**
1094
- * Packs possible values for an entity field
1095
- * These values help Skip understand the domain and valid values for fields
1096
- * Copied from AskSkipResolver.PackFieldPossibleValues
946
+ * Packs possible values for an entity field based on the ValuesToPackWithSchema setting.
947
+ * Returns EntityFieldValueInfo-compatible objects.
1097
948
  */
1098
- private async packFieldPossibleValues(f: EntityFieldInfo, dataSource: mssql.ConnectionPool): Promise<SkipEntityFieldValueInfo[]> {
949
+ private async packFieldValues(f: EntityFieldInfo): Promise<EntityFieldValueInfo[]> {
1099
950
  try {
1100
951
  if (f.ValuesToPackWithSchema === 'None') {
1101
- return []; // don't pack anything
952
+ return [];
1102
953
  }
1103
954
  else if (f.ValuesToPackWithSchema === 'All') {
1104
- // wants ALL of the distinct values
1105
- return await this.getFieldDistinctValues(f, dataSource);
955
+ return await this.getFieldDistinctValues(f);
1106
956
  }
1107
957
  else if (f.ValuesToPackWithSchema === 'Auto') {
1108
- // default setting - pack based on the ValueListType
1109
958
  if (f.ValueListTypeEnum === 'List') {
1110
- // simple list of values in the Entity Field Values table
1111
- return f.EntityFieldValues.map((v) => {
1112
- return { value: v.Value, displayValue: v.Value };
1113
- });
959
+ return f.EntityFieldValues.map((v) => new EntityFieldValueInfo({ Value: v.Value, Code: v.Value }));
1114
960
  }
1115
961
  else if (f.ValueListTypeEnum === 'ListOrUserEntry') {
1116
- // could be a user provided value, OR the values in the list of possible values.
1117
- // get the distinct list of values from the DB and concat that with the f.EntityFieldValues array - deduped and return
1118
- const values = await this.getFieldDistinctValues(f, dataSource);
962
+ const values = await this.getFieldDistinctValues(f);
1119
963
  if (!values || values.length === 0) {
1120
- // no result, just return the EntityFieldValues
1121
- return f.EntityFieldValues.map((v) => {
1122
- return { value: v.Value, displayValue: v.Value };
1123
- });
964
+ return f.EntityFieldValues.map((v) => new EntityFieldValueInfo({ Value: v.Value, Code: v.Value }));
1124
965
  }
1125
966
  else {
1126
- return [...new Set([...f.EntityFieldValues.map((v) => {
1127
- return { value: v.Value, displayValue: v.Value };
1128
- }), ...values])];
967
+ const fromEntityFieldValues = f.EntityFieldValues.map((v) => new EntityFieldValueInfo({ Value: v.Value, Code: v.Value }));
968
+ return [...new Set([...fromEntityFieldValues, ...values])];
1129
969
  }
1130
970
  }
1131
971
  }
1132
- return []; // if we get here, nothing to pack
972
+ return [];
1133
973
  }
1134
974
  catch (e) {
1135
- LogError(`[SkipSDK] packFieldPossibleValues error: ${e}`);
975
+ LogError(`[SkipSDK] packFieldValues error: ${e}`);
1136
976
  return [];
1137
977
  }
1138
978
  }
1139
979
 
1140
980
  /**
1141
- * Gets distinct values for a field from the database
1142
- * Used to provide Skip with information about the possible values
1143
- * Copied from AskSkipResolver.GetFieldDistinctValues
981
+ * Gets distinct values for a field from the database.
982
+ * Returns EntityFieldValueInfo objects.
1144
983
  */
1145
- private async getFieldDistinctValues(f: EntityFieldInfo, dataSource: mssql.ConnectionPool): Promise<SkipEntityFieldValueInfo[]> {
984
+ private async getFieldDistinctValues(f: EntityFieldInfo): Promise<EntityFieldValueInfo[]> {
1146
985
  try {
986
+ // Uses the provider's ExecuteSQL so this works on both SQL Server and PostgreSQL.
987
+ const provider = Metadata.Provider as DatabaseProviderBase;
1147
988
  const sql = `SELECT DISTINCT ${f.Name} FROM ${f.SchemaName}.${f.BaseView}`;
1148
- const request = new mssql.Request(dataSource);
1149
- const result = await request.query(sql);
1150
- if (!result || !result.recordset) {
989
+ const rows = await provider.ExecuteSQL<Record<string, unknown>>(sql);
990
+ if (!rows || rows.length === 0) {
1151
991
  return [];
1152
992
  }
1153
- else {
1154
- return result.recordset.map((r) => {
1155
- return {
1156
- value: r[f.Name],
1157
- displayValue: r[f.Name]
1158
- };
1159
- });
1160
- }
993
+ return rows.map((r) => new EntityFieldValueInfo({ Value: r[f.Name] as string, Code: r[f.Name] as string }));
1161
994
  }
1162
995
  catch (e) {
1163
996
  LogError(`[SkipSDK] getFieldDistinctValues error: ${e}`);
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
  }