@memberjunction/core 6.1.4 → 6.2.0-edge.1
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/generic/JSONType-interfaces/IEntitySubtypeSelectorConfig.d.ts +14 -0
- package/dist/generic/JSONType-interfaces/IEntitySubtypeSelectorConfig.d.ts.map +1 -1
- package/dist/generic/JSONType-interfaces/IQueryConfiguration.d.ts +60 -0
- package/dist/generic/JSONType-interfaces/IQueryConfiguration.d.ts.map +1 -0
- package/dist/generic/JSONType-interfaces/IQueryConfiguration.js +6 -0
- package/dist/generic/JSONType-interfaces/IQueryConfiguration.js.map +1 -0
- package/dist/generic/QueryCache.d.ts +17 -0
- package/dist/generic/QueryCache.d.ts.map +1 -1
- package/dist/generic/QueryCache.js +25 -5
- package/dist/generic/QueryCache.js.map +1 -1
- package/dist/generic/RegisterForStartup.d.ts +5 -5
- package/dist/generic/RegisterForStartup.js +12 -12
- package/dist/generic/applicationInfo.d.ts +4 -2
- package/dist/generic/applicationInfo.d.ts.map +1 -1
- package/dist/generic/applicationInfo.js +13 -9
- package/dist/generic/applicationInfo.js.map +1 -1
- package/dist/generic/baseEngine.d.ts +16 -1
- package/dist/generic/baseEngine.d.ts.map +1 -1
- package/dist/generic/baseEngine.js +61 -20
- package/dist/generic/baseEngine.js.map +1 -1
- package/dist/generic/baseEngineRegistry.d.ts +22 -9
- package/dist/generic/baseEngineRegistry.d.ts.map +1 -1
- package/dist/generic/baseEngineRegistry.js +43 -25
- package/dist/generic/baseEngineRegistry.js.map +1 -1
- package/dist/generic/baseEntity.d.ts +232 -18
- package/dist/generic/baseEntity.d.ts.map +1 -1
- package/dist/generic/baseEntity.js +581 -113
- package/dist/generic/baseEntity.js.map +1 -1
- package/dist/generic/baseInfo.d.ts +12 -0
- package/dist/generic/baseInfo.d.ts.map +1 -1
- package/dist/generic/baseInfo.js +22 -1
- package/dist/generic/baseInfo.js.map +1 -1
- package/dist/generic/column-descriptors.d.ts +25 -5
- package/dist/generic/column-descriptors.d.ts.map +1 -1
- package/dist/generic/column-descriptors.js +45 -5
- package/dist/generic/column-descriptors.js.map +1 -1
- package/dist/generic/compositeKey.d.ts +1 -1
- package/dist/generic/compositeKey.js +2 -2
- package/dist/generic/data-table.js +2 -2
- package/dist/generic/data-table.js.map +1 -1
- package/dist/generic/databaseProviderBase.d.ts +7 -0
- package/dist/generic/databaseProviderBase.d.ts.map +1 -1
- package/dist/generic/databaseProviderBase.js +19 -4
- package/dist/generic/databaseProviderBase.js.map +1 -1
- package/dist/generic/entityInfo.d.ts +209 -38
- package/dist/generic/entityInfo.d.ts.map +1 -1
- package/dist/generic/entityInfo.js +362 -77
- package/dist/generic/entityInfo.js.map +1 -1
- package/dist/generic/entitySubtypeResolver.d.ts +51 -5
- package/dist/generic/entitySubtypeResolver.d.ts.map +1 -1
- package/dist/generic/entitySubtypeResolver.js +50 -3
- package/dist/generic/entitySubtypeResolver.js.map +1 -1
- package/dist/generic/filters/compositeFilter.d.ts +19 -19
- package/dist/generic/filters/compositeFilter.js +60 -60
- package/dist/generic/graphqlTypeNames.d.ts +6 -0
- package/dist/generic/graphqlTypeNames.d.ts.map +1 -1
- package/dist/generic/graphqlTypeNames.js +18 -6
- package/dist/generic/graphqlTypeNames.js.map +1 -1
- package/dist/generic/interfaces.d.ts +35 -1
- package/dist/generic/interfaces.d.ts.map +1 -1
- package/dist/generic/interfaces.js +3 -3
- package/dist/generic/interfaces.js.map +1 -1
- package/dist/generic/logging.d.ts +2 -0
- package/dist/generic/logging.d.ts.map +1 -1
- package/dist/generic/logging.js +7 -3
- package/dist/generic/logging.js.map +1 -1
- package/dist/generic/metadata.d.ts +1 -1
- package/dist/generic/metadata.js +3 -3
- package/dist/generic/providerBase.d.ts +35 -7
- package/dist/generic/providerBase.d.ts.map +1 -1
- package/dist/generic/providerBase.js +87 -27
- package/dist/generic/providerBase.js.map +1 -1
- package/dist/generic/queryInfo.d.ts +47 -0
- package/dist/generic/queryInfo.d.ts.map +1 -1
- package/dist/generic/queryInfo.js +76 -0
- package/dist/generic/queryInfo.js.map +1 -1
- package/dist/generic/queryInfoInterfaces.d.ts +6 -0
- package/dist/generic/queryInfoInterfaces.d.ts.map +1 -1
- package/dist/generic/queryResultEnricher.d.ts +2 -0
- package/dist/generic/queryResultEnricher.d.ts.map +1 -1
- package/dist/generic/queryResultEnricher.js +5 -1
- package/dist/generic/queryResultEnricher.js.map +1 -1
- package/dist/generic/querySQLFilters.d.ts +4 -0
- package/dist/generic/querySQLFilters.d.ts.map +1 -1
- package/dist/generic/querySQLFilters.js +10 -2
- package/dist/generic/querySQLFilters.js.map +1 -1
- package/dist/generic/relatedRecordCollection.d.ts +2 -0
- package/dist/generic/relatedRecordCollection.d.ts.map +1 -1
- package/dist/generic/relatedRecordCollection.js +5 -1
- package/dist/generic/relatedRecordCollection.js.map +1 -1
- package/dist/generic/remoteOperationDispatch.d.ts +2 -0
- package/dist/generic/remoteOperationDispatch.d.ts.map +1 -1
- package/dist/generic/remoteOperationDispatch.js +5 -1
- package/dist/generic/remoteOperationDispatch.js.map +1 -1
- package/dist/generic/runQuerySQLFilterImplementations.d.ts +8 -0
- package/dist/generic/runQuerySQLFilterImplementations.d.ts.map +1 -1
- package/dist/generic/runQuerySQLFilterImplementations.js +20 -4
- package/dist/generic/runQuerySQLFilterImplementations.js.map +1 -1
- package/dist/generic/schemaHydration.d.ts +8 -0
- package/dist/generic/schemaHydration.d.ts.map +1 -1
- package/dist/generic/schemaHydration.js +21 -5
- package/dist/generic/schemaHydration.js.map +1 -1
- package/dist/generic/securityInfo.d.ts +11 -9
- package/dist/generic/securityInfo.d.ts.map +1 -1
- package/dist/generic/securityInfo.js +31 -27
- package/dist/generic/securityInfo.js.map +1 -1
- package/dist/generic/telemetryManager.d.ts +16 -0
- package/dist/generic/telemetryManager.d.ts.map +1 -1
- package/dist/generic/telemetryManager.js +52 -20
- package/dist/generic/telemetryManager.js.map +1 -1
- package/dist/generic/transactionGroup.d.ts +1 -1
- package/dist/generic/transactionGroup.js +4 -4
- package/dist/generic/transactionGroup.js.map +1 -1
- package/dist/generic/util.d.ts +22 -0
- package/dist/generic/util.d.ts.map +1 -1
- package/dist/generic/util.js +33 -0
- package/dist/generic/util.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/views/viewInfo.d.ts +2 -2
- package/dist/views/viewInfo.js +13 -13
- package/dist/views/viewInfo.js.map +1 -1
- package/package.json +3 -3
|
@@ -3,7 +3,7 @@ import { Metadata } from "./metadata.js";
|
|
|
3
3
|
import { TypeScriptTypeFromSQLType, SQLFullType, SQLMaxLength, FormatValue, CodeNameFromString } from "./util.js";
|
|
4
4
|
import { IsFixedWidthStringSQLType } from "@memberjunction/sql-dialect";
|
|
5
5
|
import { LogError } from "./logging.js";
|
|
6
|
-
import { WarningManager, SafeJSONParse, UUIDsEqual, ordinalCompare } from "@memberjunction/global";
|
|
6
|
+
import { WarningManager, SafeJSONParse, UUIDsEqual, ordinalCompare, GeneratePluralName } from "@memberjunction/global";
|
|
7
7
|
import { ReadRelationshipJoinFields, } from "./entityConfiguration.js";
|
|
8
8
|
/**
|
|
9
9
|
* Runtime domain for {@link EntityFieldInfo.ExtendedType}. This array is the single source of
|
|
@@ -216,7 +216,7 @@ export class EntityOrganicKeyInfo extends BaseInfo {
|
|
|
216
216
|
this.__mj_UpdatedAt = null;
|
|
217
217
|
// virtual fields from the database view
|
|
218
218
|
this.Entity = null;
|
|
219
|
-
this._RelatedEntities = [];
|
|
219
|
+
this._RelatedEntities = []; // case-violation-ok-legacy-back-compat: a class in the same hierarchy already declares the camelCase name — TypeScript rejects two declarations of one private property (TS2415)
|
|
220
220
|
if (initData) {
|
|
221
221
|
this.copyInitData(initData);
|
|
222
222
|
this._RelatedEntities = [];
|
|
@@ -686,6 +686,38 @@ export class EntityFieldInfo extends BaseInfo {
|
|
|
686
686
|
get HierarchyMaxDepth() {
|
|
687
687
|
return this.ConfigurationObject?.Hierarchy?.MaxDepth ?? 100;
|
|
688
688
|
}
|
|
689
|
+
/** @deprecated Use {@link RelatedEntityTableAlias}. */
|
|
690
|
+
get _RelatedEntityTableAlias() {
|
|
691
|
+
return this.RelatedEntityTableAlias;
|
|
692
|
+
}
|
|
693
|
+
/** @deprecated Use {@link RelatedEntityTableAlias}. */
|
|
694
|
+
set _RelatedEntityTableAlias(value) {
|
|
695
|
+
this.RelatedEntityTableAlias = value;
|
|
696
|
+
}
|
|
697
|
+
/** @deprecated Use {@link RelatedEntityNameFieldIsVirtual}. */
|
|
698
|
+
get _RelatedEntityNameFieldIsVirtual() {
|
|
699
|
+
return this.RelatedEntityNameFieldIsVirtual;
|
|
700
|
+
}
|
|
701
|
+
/** @deprecated Use {@link RelatedEntityNameFieldIsVirtual}. */
|
|
702
|
+
set _RelatedEntityNameFieldIsVirtual(value) {
|
|
703
|
+
this.RelatedEntityNameFieldIsVirtual = value;
|
|
704
|
+
}
|
|
705
|
+
/** @deprecated Use {@link RelatedEntityNameFieldIsComputed}. */
|
|
706
|
+
get _RelatedEntityNameFieldIsComputed() {
|
|
707
|
+
return this.RelatedEntityNameFieldIsComputed;
|
|
708
|
+
}
|
|
709
|
+
/** @deprecated Use {@link RelatedEntityNameFieldIsComputed}. */
|
|
710
|
+
set _RelatedEntityNameFieldIsComputed(value) {
|
|
711
|
+
this.RelatedEntityNameFieldIsComputed = value;
|
|
712
|
+
}
|
|
713
|
+
/** @deprecated Use {@link RelatedEntityJoinFieldMappings}. */
|
|
714
|
+
get _RelatedEntityJoinFieldMappings() {
|
|
715
|
+
return this.RelatedEntityJoinFieldMappings;
|
|
716
|
+
}
|
|
717
|
+
/** @deprecated Use {@link RelatedEntityJoinFieldMappings}. */
|
|
718
|
+
set _RelatedEntityJoinFieldMappings(value) {
|
|
719
|
+
this.RelatedEntityJoinFieldMappings = value;
|
|
720
|
+
}
|
|
689
721
|
/**
|
|
690
722
|
* JSON configuration for additional fields to join from the related entity.
|
|
691
723
|
* Parsed from the RelatedEntityJoinFields column. Uses lazy initialization and caching
|
|
@@ -728,7 +760,7 @@ export class EntityFieldInfo extends BaseInfo {
|
|
|
728
760
|
* Empty for the overwhelming majority of fields — see {@link HasFieldPermissions}.
|
|
729
761
|
*/
|
|
730
762
|
get FieldPermissions() {
|
|
731
|
-
return this.
|
|
763
|
+
return this._fieldPermissions;
|
|
732
764
|
}
|
|
733
765
|
/**
|
|
734
766
|
* True when at least one {@link EntityFieldPermissionInfo} record exists for this field.
|
|
@@ -739,7 +771,7 @@ export class EntityFieldInfo extends BaseInfo {
|
|
|
739
771
|
* enabled entity a field with no records is denied, not open.
|
|
740
772
|
*/
|
|
741
773
|
get HasFieldPermissions() {
|
|
742
|
-
return this.
|
|
774
|
+
return this._fieldPermissions.length > 0;
|
|
743
775
|
}
|
|
744
776
|
/**
|
|
745
777
|
* Entities whose fields can never be restricted by field-level security.
|
|
@@ -760,7 +792,7 @@ export class EntityFieldInfo extends BaseInfo {
|
|
|
760
792
|
*
|
|
761
793
|
* Stored lowercased; compare with a trimmed, lowercased entity name.
|
|
762
794
|
*/
|
|
763
|
-
static { this.
|
|
795
|
+
static { this.unrestrictableEntityNames = new Set([
|
|
764
796
|
'mj: entities',
|
|
765
797
|
'mj: entity fields',
|
|
766
798
|
'mj: entity permissions',
|
|
@@ -774,7 +806,7 @@ export class EntityFieldInfo extends BaseInfo {
|
|
|
774
806
|
* See {@link EntityFieldInfo.UnrestrictableEntityNames} for the rationale.
|
|
775
807
|
*/
|
|
776
808
|
get IsOnUnrestrictableEntity() {
|
|
777
|
-
return EntityFieldInfo.
|
|
809
|
+
return EntityFieldInfo.unrestrictableEntityNames.has((this.Entity ?? '').trim().toLowerCase());
|
|
778
810
|
}
|
|
779
811
|
/**
|
|
780
812
|
* True for fields that must remain readable regardless of any permission record:
|
|
@@ -859,7 +891,7 @@ export class EntityFieldInfo extends BaseInfo {
|
|
|
859
891
|
* arithmetic.
|
|
860
892
|
*/
|
|
861
893
|
aggregateUserFieldPermissions(user) {
|
|
862
|
-
return EntityFieldInfo.AggregateFieldRulesForUser(this.
|
|
894
|
+
return EntityFieldInfo.AggregateFieldRulesForUser(this._fieldPermissions, user);
|
|
863
895
|
}
|
|
864
896
|
/**
|
|
865
897
|
* The same aggregation {@link GetUserFieldPermissions} performs, over a rule list the caller
|
|
@@ -958,8 +990,9 @@ export class EntityFieldInfo extends BaseInfo {
|
|
|
958
990
|
* * **Stringifying is required.** `EntityFieldValue.Value` is always a string in metadata while
|
|
959
991
|
* the field's runtime value may be a number, so a strict `===` would reject every legal value
|
|
960
992
|
* on a numeric list. It is not lossless: `String(1.0)` is `'1'`, so a metadata value written
|
|
961
|
-
* as `'1.
|
|
962
|
-
*
|
|
993
|
+
* as `'1.00'` would fail closed here — which is why a NUMERIC column's list is not compared
|
|
994
|
+
* through this function at all but by value, in numericValueIsPermittedByValueList (MJ #3978).
|
|
995
|
+
* This normalization is what a STRING column's list is compared with.
|
|
963
996
|
* * **Trimming is cheap insurance, NOT the load-bearing rule it was first documented as.** An
|
|
964
997
|
* earlier version of this comment claimed an untrimmed comparison would reject 9,301 existing
|
|
965
998
|
* rows in fixed-width `nchar` columns (`MJ: Action Params`.Type, `MJ: Record Changes`.Status
|
|
@@ -1002,10 +1035,10 @@ export class EntityFieldInfo extends BaseInfo {
|
|
|
1002
1035
|
* * Only string and number values are checked, and the gate fails OPEN — an unsupported type
|
|
1003
1036
|
* skips validation rather than manufacturing a failure. Nothing in the schema restricts
|
|
1004
1037
|
* which columns may carry a value list (`CK_EntityField_ValueListType_New` constrains the
|
|
1005
|
-
* mode, not the column type)
|
|
1006
|
-
*
|
|
1007
|
-
*
|
|
1008
|
-
*
|
|
1038
|
+
* mode, not the column type). Every list WAS on a string column — measured on a 6.x
|
|
1039
|
+
* instance, 455 nvarchar + 7 nchar and nothing else — because CodeGen's constraint parser
|
|
1040
|
+
* only extracted quoted literals; since MJ #3978 it extracts the unquoted numeric form too,
|
|
1041
|
+
* so a numeric column's list is now a real case and is compared by value (below).
|
|
1009
1042
|
* Booleans and Dates are excluded deliberately: a bit column carrying a `'1'`/`'0'` list
|
|
1010
1043
|
* would see `String(true) === 'true'` and reject every legal value, and a Date has no sane
|
|
1011
1044
|
* string form to compare — so guessing there would break saves rather than guard them.
|
|
@@ -1059,17 +1092,72 @@ export class EntityFieldInfo extends BaseInfo {
|
|
|
1059
1092
|
if (typeof value !== 'string' && typeof value !== 'number') {
|
|
1060
1093
|
// The rule cannot be applied to this type at all, and that is a mismatch rather than a
|
|
1061
1094
|
// state to absorb: either the field should not declare a value list (one on a bit column
|
|
1062
|
-
// — which CodeGen never produces
|
|
1063
|
-
// `
|
|
1064
|
-
// or a caller assigned the wrong type. Skipping it silently would
|
|
1095
|
+
// — which CodeGen still never produces: it captures the unquoted numeric form as of MJ
|
|
1096
|
+
// #3978, but excludes `bit` fields, since `IN (0,1)` is vacuous on a bit and `= 1` is a
|
|
1097
|
+
// validator rather than a dropdown) or a caller assigned the wrong type. Skipping it silently would
|
|
1065
1098
|
// leave the caller believing a guard is on when it is not, which is the exact failure
|
|
1066
1099
|
// mode this rung was added to fix — so it is reported, once per field.
|
|
1067
1100
|
this.reportUnsupportedValueListValue(`a ${typeof value} value (SQL type ${this.SQLFullType})`, 'value-list validation compares strings, numbers and dates only — comparing anything ' +
|
|
1068
1101
|
'else would reject legal values rather than guard them');
|
|
1069
1102
|
return true;
|
|
1070
1103
|
}
|
|
1104
|
+
// A NUMERIC column's list is compared by VALUE, not by string form — see
|
|
1105
|
+
// numericValueIsPermittedByValueList. Falls through to the string comparison when the list is
|
|
1106
|
+
// not entirely numeric, which is the only case where there is nothing to compare against.
|
|
1107
|
+
if (this.TSType === EntityFieldTSType.Number) {
|
|
1108
|
+
const numericKeys = this.valueListNumericKeys();
|
|
1109
|
+
if (numericKeys) {
|
|
1110
|
+
return this.numericValueIsPermittedByValueList(value, numericKeys);
|
|
1111
|
+
}
|
|
1112
|
+
}
|
|
1071
1113
|
return this._normalizedValueListValues.has(EntityFieldInfo.NormalizeValueListValue(value));
|
|
1072
1114
|
}
|
|
1115
|
+
/**
|
|
1116
|
+
* Value-list membership for a NUMERIC column, compared on the numeric VALUE rather than the
|
|
1117
|
+
* string form (MJ issue #3978).
|
|
1118
|
+
*
|
|
1119
|
+
* Necessary because the two sides are written by different hands: SQL Server stores
|
|
1120
|
+
* `CHECK (Price IN (0.50, 1.00))` as the unquoted literals `([Price]=(1.00) OR [Price]=(0.50))`,
|
|
1121
|
+
* CodeGen captures those literals verbatim, and the runtime value of that column is the number
|
|
1122
|
+
* `1`. `String(1) === '1'` is not `'1.00'`, so a string comparison would refuse a value the
|
|
1123
|
+
* database accepts — the exact failure mode this rung exists to avoid. Comparing as numbers makes
|
|
1124
|
+
* the scale of the literal irrelevant.
|
|
1125
|
+
*
|
|
1126
|
+
* The comparison is gated on the column being numeric, NOT merely on the list looking numeric: on
|
|
1127
|
+
* a string column `CHECK (Code IN ('1','2'))` really is a string list, and comparing `'01'`
|
|
1128
|
+
* numerically would permit a value the database refuses.
|
|
1129
|
+
*
|
|
1130
|
+
* A value that is not a finite number falls back to the string comparison rather than being
|
|
1131
|
+
* refused here, so a value list that happens to hold non-numeric text on a numeric column (which
|
|
1132
|
+
* CodeGen cannot produce, but `applyValueListConfig` could) still behaves as it did before.
|
|
1133
|
+
*/
|
|
1134
|
+
numericValueIsPermittedByValueList(value, numericKeys) {
|
|
1135
|
+
const candidate = typeof value === 'number' ? value : Number(value.trim());
|
|
1136
|
+
if (!Number.isFinite(candidate)) {
|
|
1137
|
+
return this._normalizedValueListValues.has(EntityFieldInfo.NormalizeValueListValue(value));
|
|
1138
|
+
}
|
|
1139
|
+
return numericKeys.has(candidate);
|
|
1140
|
+
}
|
|
1141
|
+
/**
|
|
1142
|
+
* The value list as a set of numbers, or null when any of its values is not a finite number (so
|
|
1143
|
+
* there is nothing to compare numerically). Memoized alongside the normalized string set, and
|
|
1144
|
+
* built from it — lower-casing and trimming cannot change how a numeric literal parses.
|
|
1145
|
+
*/
|
|
1146
|
+
valueListNumericKeys() {
|
|
1147
|
+
if (this._valueListNumericKeys === undefined) {
|
|
1148
|
+
const keys = new Set();
|
|
1149
|
+
for (const normalized of this._normalizedValueListValues ?? []) {
|
|
1150
|
+
const parsed = Number(normalized);
|
|
1151
|
+
if (normalized.length === 0 || !Number.isFinite(parsed)) {
|
|
1152
|
+
this._valueListNumericKeys = null;
|
|
1153
|
+
return null;
|
|
1154
|
+
}
|
|
1155
|
+
keys.add(parsed);
|
|
1156
|
+
}
|
|
1157
|
+
this._valueListNumericKeys = keys.size > 0 ? keys : null;
|
|
1158
|
+
}
|
|
1159
|
+
return this._valueListNumericKeys;
|
|
1160
|
+
}
|
|
1073
1161
|
/**
|
|
1074
1162
|
* Value-list membership for a `Date` runtime value, compared on the CALENDAR DATE rather than
|
|
1075
1163
|
* the instant.
|
|
@@ -1101,7 +1189,7 @@ export class EntityFieldInfo extends BaseInfo {
|
|
|
1101
1189
|
return true; // an Invalid Date is not an out-of-list value; leave it to the date check
|
|
1102
1190
|
}
|
|
1103
1191
|
return this._valueListDateKeys.has(value.toISOString().slice(0, 10)) ||
|
|
1104
|
-
this._valueListDateKeys.has(EntityFieldInfo.
|
|
1192
|
+
this._valueListDateKeys.has(EntityFieldInfo.localCalendarDate(value));
|
|
1105
1193
|
}
|
|
1106
1194
|
/**
|
|
1107
1195
|
* Builds the set of yyyy-mm-dd keys for a `date` field's value list, or null when the field is
|
|
@@ -1124,7 +1212,7 @@ export class EntityFieldInfo extends BaseInfo {
|
|
|
1124
1212
|
return keys.size > 0 ? keys : null;
|
|
1125
1213
|
}
|
|
1126
1214
|
/** The Date's LOCAL calendar date as yyyy-mm-dd (its UTC one is `toISOString().slice(0, 10)`). */
|
|
1127
|
-
static
|
|
1215
|
+
static localCalendarDate(value) {
|
|
1128
1216
|
const pad = (n) => String(n).padStart(2, '0');
|
|
1129
1217
|
return `${value.getFullYear()}-${pad(value.getMonth() + 1)}-${pad(value.getDate())}`;
|
|
1130
1218
|
}
|
|
@@ -1640,6 +1728,22 @@ export class EntityFieldInfo extends BaseInfo {
|
|
|
1640
1728
|
this._configuration = null;
|
|
1641
1729
|
this._configurationObject = undefined;
|
|
1642
1730
|
this.RelatedEntityDisplayType = null;
|
|
1731
|
+
/**
|
|
1732
|
+
* * Field Name: RelatedEntityFilter
|
|
1733
|
+
* * SQL Data Type: nvarchar(MAX)
|
|
1734
|
+
* * Description: Optional SQL WHERE fragment applied to every lookup on this foreign key
|
|
1735
|
+
* (e.g. `Status = 'Active'`), AND-ed with whatever the user types. Scopes a picker from
|
|
1736
|
+
* metadata rather than from every form template that renders the field. Authored, not
|
|
1737
|
+
* derived from the catalog.
|
|
1738
|
+
*/
|
|
1739
|
+
this.RelatedEntityFilter = null;
|
|
1740
|
+
/**
|
|
1741
|
+
* * Field Name: RelatedEntityOrderBy
|
|
1742
|
+
* * SQL Data Type: nvarchar(500)
|
|
1743
|
+
* * Description: Optional ORDER BY fragment for the empty-query browse list on this foreign
|
|
1744
|
+
* key. Defaults to the related entity's name field.
|
|
1745
|
+
*/
|
|
1746
|
+
this.RelatedEntityOrderBy = null;
|
|
1643
1747
|
this.EntityIDFieldName = null;
|
|
1644
1748
|
this.__mj_CreatedAt = null;
|
|
1645
1749
|
this.__mj_UpdatedAt = null;
|
|
@@ -1856,6 +1960,8 @@ export class EntityFieldInfo extends BaseInfo {
|
|
|
1856
1960
|
this._loggedUnsupportedValueListType = false;
|
|
1857
1961
|
/** Memoized yyyy-mm-dd keys for a `date` field's value list; null when it cannot be compared. */
|
|
1858
1962
|
this._valueListDateKeys = undefined;
|
|
1963
|
+
/** Memoized numeric form of a numeric field's value list; null when it is not entirely numeric. */
|
|
1964
|
+
this._valueListNumericKeys = undefined;
|
|
1859
1965
|
/**
|
|
1860
1966
|
* Cached parsed RelatedEntityJoinFieldsConfig to avoid repeated JSON.parse calls.
|
|
1861
1967
|
* Lazy-initialized on first access.
|
|
@@ -1865,7 +1971,7 @@ export class EntityFieldInfo extends BaseInfo {
|
|
|
1865
1971
|
* Flag to track if RelatedEntityJoinFieldsConfig parsing failed to avoid repeated parse attempts on bad JSON.
|
|
1866
1972
|
*/
|
|
1867
1973
|
this._relatedEntityJoinFieldsFailedParsing = false;
|
|
1868
|
-
this.
|
|
1974
|
+
this._fieldPermissions = [];
|
|
1869
1975
|
/** Memoized {@link TSType} — `Type` is immutable after metadata load, so the classification never changes. */
|
|
1870
1976
|
this._tsType = undefined;
|
|
1871
1977
|
/**
|
|
@@ -1895,7 +2001,7 @@ export class EntityFieldInfo extends BaseInfo {
|
|
|
1895
2001
|
// would cost more than the construction it avoids.
|
|
1896
2002
|
const efp = initData.EntityFieldPermissions || initData._FieldPermissions || initData.FieldPermissions;
|
|
1897
2003
|
if (efp && efp.length > 0) {
|
|
1898
|
-
this.
|
|
2004
|
+
this._fieldPermissions = efp.map((p) => new EntityFieldPermissionInfo(p));
|
|
1899
2005
|
}
|
|
1900
2006
|
}
|
|
1901
2007
|
}
|
|
@@ -2042,7 +2148,10 @@ export class EntityInfo extends BaseInfo {
|
|
|
2042
2148
|
try {
|
|
2043
2149
|
const parsed = JSON.parse(this.SubtypeSelector);
|
|
2044
2150
|
if (parsed && typeof parsed['Path'] === 'string' && parsed['Path'].trim().length > 0) {
|
|
2045
|
-
this._subtypeSelectorConfig = {
|
|
2151
|
+
this._subtypeSelectorConfig = {
|
|
2152
|
+
Path: parsed['Path'].trim(),
|
|
2153
|
+
UseForLoadedRecords: this.parseSubtypeSelectorUseForLoadedRecords(parsed),
|
|
2154
|
+
};
|
|
2046
2155
|
}
|
|
2047
2156
|
else {
|
|
2048
2157
|
LogError(`EntityInfo '${this.Name}': SubtypeSelector JSON must contain a non-empty 'Path' string property. Found: ${this.SubtypeSelector}`);
|
|
@@ -2060,6 +2169,19 @@ export class EntityInfo extends BaseInfo {
|
|
|
2060
2169
|
}
|
|
2061
2170
|
return this._subtypeSelectorConfig;
|
|
2062
2171
|
}
|
|
2172
|
+
/**
|
|
2173
|
+
* Reads the selector's optional `UseForLoadedRecords` flag. Only a JSON `true` turns load hints
|
|
2174
|
+
* on; a value of any other type is logged and leaves them off, so a quoted `"true"` can't look
|
|
2175
|
+
* like an opt-in that silently does nothing.
|
|
2176
|
+
*/
|
|
2177
|
+
parseSubtypeSelectorUseForLoadedRecords(parsed) {
|
|
2178
|
+
const value = parsed['UseForLoadedRecords'];
|
|
2179
|
+
if (value === undefined || value === null || typeof value === 'boolean') {
|
|
2180
|
+
return value === true;
|
|
2181
|
+
}
|
|
2182
|
+
LogError(`EntityInfo '${this.Name}': SubtypeSelector 'UseForLoadedRecords' must be true or false, but is ${JSON.stringify(value)}. Loads of this entity won't use the selector.`);
|
|
2183
|
+
return false;
|
|
2184
|
+
}
|
|
2063
2185
|
/**
|
|
2064
2186
|
* True when at least one field is a writable Geo* source (street and/or native lat/lng).
|
|
2065
2187
|
* Person/Org PrimaryAddress* are virtual display fields and do **not** count.
|
|
@@ -2067,6 +2189,62 @@ export class EntityInfo extends BaseInfo {
|
|
|
2067
2189
|
get HasWritableGeoSourceFields() {
|
|
2068
2190
|
return (this.Fields ?? []).some(f => f.IsWritableGeoField);
|
|
2069
2191
|
}
|
|
2192
|
+
/** @deprecated Use {@link SpCreate}. */
|
|
2193
|
+
get spCreate() {
|
|
2194
|
+
return this.SpCreate;
|
|
2195
|
+
}
|
|
2196
|
+
/** @deprecated Use {@link SpCreate}. */
|
|
2197
|
+
set spCreate(value) {
|
|
2198
|
+
this.SpCreate = value;
|
|
2199
|
+
}
|
|
2200
|
+
/** @deprecated Use {@link SpUpdate}. */
|
|
2201
|
+
get spUpdate() {
|
|
2202
|
+
return this.SpUpdate;
|
|
2203
|
+
}
|
|
2204
|
+
/** @deprecated Use {@link SpUpdate}. */
|
|
2205
|
+
set spUpdate(value) {
|
|
2206
|
+
this.SpUpdate = value;
|
|
2207
|
+
}
|
|
2208
|
+
/** @deprecated Use {@link SpDelete}. */
|
|
2209
|
+
get spDelete() {
|
|
2210
|
+
return this.SpDelete;
|
|
2211
|
+
}
|
|
2212
|
+
/** @deprecated Use {@link SpDelete}. */
|
|
2213
|
+
set spDelete(value) {
|
|
2214
|
+
this.SpDelete = value;
|
|
2215
|
+
}
|
|
2216
|
+
/** @deprecated Use {@link SpCreateGenerated}. */
|
|
2217
|
+
get spCreateGenerated() {
|
|
2218
|
+
return this.SpCreateGenerated;
|
|
2219
|
+
}
|
|
2220
|
+
/** @deprecated Use {@link SpCreateGenerated}. */
|
|
2221
|
+
set spCreateGenerated(value) {
|
|
2222
|
+
this.SpCreateGenerated = value;
|
|
2223
|
+
}
|
|
2224
|
+
/** @deprecated Use {@link SpUpdateGenerated}. */
|
|
2225
|
+
get spUpdateGenerated() {
|
|
2226
|
+
return this.SpUpdateGenerated;
|
|
2227
|
+
}
|
|
2228
|
+
/** @deprecated Use {@link SpUpdateGenerated}. */
|
|
2229
|
+
set spUpdateGenerated(value) {
|
|
2230
|
+
this.SpUpdateGenerated = value;
|
|
2231
|
+
}
|
|
2232
|
+
/** @deprecated Use {@link SpDeleteGenerated}. */
|
|
2233
|
+
get spDeleteGenerated() {
|
|
2234
|
+
return this.SpDeleteGenerated;
|
|
2235
|
+
}
|
|
2236
|
+
/** @deprecated Use {@link SpDeleteGenerated}. */
|
|
2237
|
+
set spDeleteGenerated(value) {
|
|
2238
|
+
this.SpDeleteGenerated = value;
|
|
2239
|
+
}
|
|
2240
|
+
/** @deprecated Use {@link SpMatch}. */
|
|
2241
|
+
get spMatch() {
|
|
2242
|
+
return this.SpMatch;
|
|
2243
|
+
}
|
|
2244
|
+
/** @deprecated Use {@link SpMatch}. */
|
|
2245
|
+
set spMatch(value) {
|
|
2246
|
+
this.SpMatch = value;
|
|
2247
|
+
}
|
|
2070
2248
|
/**
|
|
2071
2249
|
* Optional JSON configuration bag (shape = {@link IEntityConfiguration}).
|
|
2072
2250
|
* Nested `UI.Form` holds generated-form chrome: layout, auto left-nav
|
|
@@ -2101,6 +2279,46 @@ export class EntityInfo extends BaseInfo {
|
|
|
2101
2279
|
get ConfigurationObject() {
|
|
2102
2280
|
return this.Configuration;
|
|
2103
2281
|
}
|
|
2282
|
+
/** @deprecated Use {@link HasIdField}. */
|
|
2283
|
+
get _hasIdField() {
|
|
2284
|
+
return this.HasIdField;
|
|
2285
|
+
}
|
|
2286
|
+
/** @deprecated Use {@link HasIdField}. */
|
|
2287
|
+
set _hasIdField(value) {
|
|
2288
|
+
this.HasIdField = value;
|
|
2289
|
+
}
|
|
2290
|
+
/** @deprecated Use {@link VirtualCount}. */
|
|
2291
|
+
get _virtualCount() {
|
|
2292
|
+
return this.VirtualCount;
|
|
2293
|
+
}
|
|
2294
|
+
/** @deprecated Use {@link VirtualCount}. */
|
|
2295
|
+
set _virtualCount(value) {
|
|
2296
|
+
this.VirtualCount = value;
|
|
2297
|
+
}
|
|
2298
|
+
/** @deprecated Use {@link ManyToManyCount}. */
|
|
2299
|
+
get _manyToManyCount() {
|
|
2300
|
+
return this.ManyToManyCount;
|
|
2301
|
+
}
|
|
2302
|
+
/** @deprecated Use {@link ManyToManyCount}. */
|
|
2303
|
+
set _manyToManyCount(value) {
|
|
2304
|
+
this.ManyToManyCount = value;
|
|
2305
|
+
}
|
|
2306
|
+
/** @deprecated Use {@link OneToManyCount}. */
|
|
2307
|
+
get _oneToManyCount() {
|
|
2308
|
+
return this.OneToManyCount;
|
|
2309
|
+
}
|
|
2310
|
+
/** @deprecated Use {@link OneToManyCount}. */
|
|
2311
|
+
set _oneToManyCount(value) {
|
|
2312
|
+
this.OneToManyCount = value;
|
|
2313
|
+
}
|
|
2314
|
+
/** @deprecated Use {@link FloatCount}. */
|
|
2315
|
+
get _floatCount() {
|
|
2316
|
+
return this.FloatCount;
|
|
2317
|
+
}
|
|
2318
|
+
/** @deprecated Use {@link FloatCount}. */
|
|
2319
|
+
set _floatCount(value) {
|
|
2320
|
+
this.FloatCount = value;
|
|
2321
|
+
}
|
|
2104
2322
|
/**
|
|
2105
2323
|
* The set of field names this user may NOT READ on this entity — the per-request primitive
|
|
2106
2324
|
* every field-security enforcement point is built on.
|
|
@@ -2227,7 +2445,7 @@ export class EntityInfo extends BaseInfo {
|
|
|
2227
2445
|
if (!this.EnableFieldLevelSecurity) {
|
|
2228
2446
|
return denied;
|
|
2229
2447
|
}
|
|
2230
|
-
for (const field of this.
|
|
2448
|
+
for (const field of this._fields) {
|
|
2231
2449
|
if (isDenied(field.GetUserFieldPermissions(user, true))) {
|
|
2232
2450
|
denied.add(field.Name.trim().toLowerCase());
|
|
2233
2451
|
denied.add(field.CodeName.trim().toLowerCase());
|
|
@@ -2251,7 +2469,7 @@ export class EntityInfo extends BaseInfo {
|
|
|
2251
2469
|
return undefined;
|
|
2252
2470
|
if (this._fieldByNameMap === null) {
|
|
2253
2471
|
const map = new Map();
|
|
2254
|
-
for (const f of this.
|
|
2472
|
+
for (const f of this._fields) {
|
|
2255
2473
|
if (f.Name != null)
|
|
2256
2474
|
map.set(f.Name.trim().toLowerCase(), f);
|
|
2257
2475
|
}
|
|
@@ -2332,7 +2550,7 @@ export class EntityInfo extends BaseInfo {
|
|
|
2332
2550
|
* @returns {EntityFieldInfo[]} Array of all entity fields
|
|
2333
2551
|
*/
|
|
2334
2552
|
get Fields() {
|
|
2335
|
-
return this.
|
|
2553
|
+
return this._fields;
|
|
2336
2554
|
}
|
|
2337
2555
|
/**
|
|
2338
2556
|
* Returns true if ANY field on this entity is `Deprecated` or `Disabled` (i.e. not `Active`).
|
|
@@ -2347,30 +2565,56 @@ export class EntityInfo extends BaseInfo {
|
|
|
2347
2565
|
*/
|
|
2348
2566
|
get HasInactiveFields() {
|
|
2349
2567
|
if (this._hasInactiveFields === undefined) {
|
|
2350
|
-
this._hasInactiveFields = this.
|
|
2568
|
+
this._hasInactiveFields = this._fields.some(f => f.Status === 'Deprecated' || f.Status === 'Disabled');
|
|
2351
2569
|
}
|
|
2352
2570
|
return this._hasInactiveFields;
|
|
2353
2571
|
}
|
|
2572
|
+
/**
|
|
2573
|
+
* Returns true if ANY field on this entity participates in `UserSearchString` matching
|
|
2574
|
+
* (`EntityField.IncludeInUserSearchAPI`).
|
|
2575
|
+
*
|
|
2576
|
+
* Computed once on first access and cached for the lifetime of this EntityInfo. Like
|
|
2577
|
+
* {@link HasInactiveFields} this is a property of the entity DEFINITION, shared across every
|
|
2578
|
+
* record instance, so an entity is scanned at most once however many searches run against it.
|
|
2579
|
+
*
|
|
2580
|
+
* It answers "does this entity have a search surface at all", which is NOT the same question as
|
|
2581
|
+
* "did this search produce a predicate". A field can be excluded from a particular search at
|
|
2582
|
+
* runtime — denied by field-level security, or not a sensible text-search target — on an entity
|
|
2583
|
+
* that does declare searchable fields. Providers use the distinction to tell "the caller asked
|
|
2584
|
+
* to filter by something this entity does not have" (ignore the term) from "every candidate
|
|
2585
|
+
* field dropped out" (match nothing). See MJ#4581.
|
|
2586
|
+
*
|
|
2587
|
+
* Caching assumes `EntityField.IncludeInUserSearchAPI` is not mutated in place after this
|
|
2588
|
+
* EntityInfo is built — the same assumption {@link HasInactiveFields} makes about `Status`.
|
|
2589
|
+
* A metadata refresh constructs new EntityInfo objects rather than editing existing ones, and
|
|
2590
|
+
* the cache is reset wherever `_Fields` is (re)assigned, so both paths stay correct.
|
|
2591
|
+
*/
|
|
2592
|
+
get HasSearchFields() {
|
|
2593
|
+
if (this._hasSearchFields === undefined) {
|
|
2594
|
+
this._hasSearchFields = this._fields.some(f => f.IncludeInUserSearchAPI);
|
|
2595
|
+
}
|
|
2596
|
+
return this._hasSearchFields;
|
|
2597
|
+
}
|
|
2354
2598
|
/**
|
|
2355
2599
|
* Gets all relationships where other entities reference this entity.
|
|
2356
2600
|
* @returns {EntityRelationshipInfo[]} Array of entity relationships
|
|
2357
2601
|
*/
|
|
2358
2602
|
get RelatedEntities() {
|
|
2359
|
-
return this.
|
|
2603
|
+
return this._relatedEntities;
|
|
2360
2604
|
}
|
|
2361
2605
|
/**
|
|
2362
2606
|
* Gets the security permissions for this entity by role.
|
|
2363
2607
|
* @returns {EntityPermissionInfo[]} Array of permission settings
|
|
2364
2608
|
*/
|
|
2365
2609
|
get Permissions() {
|
|
2366
|
-
return this.
|
|
2610
|
+
return this._permissions;
|
|
2367
2611
|
}
|
|
2368
2612
|
/**
|
|
2369
2613
|
* Gets custom configuration settings for this entity.
|
|
2370
2614
|
* @returns {EntitySettingInfo[]} Array of entity-specific settings
|
|
2371
2615
|
*/
|
|
2372
2616
|
get Settings() {
|
|
2373
|
-
return this.
|
|
2617
|
+
return this._settings;
|
|
2374
2618
|
}
|
|
2375
2619
|
/**
|
|
2376
2620
|
* Gets the parsed FieldCategoryInfo map for this entity, keyed by category name.
|
|
@@ -2378,7 +2622,7 @@ export class EntityInfo extends BaseInfo {
|
|
|
2378
2622
|
* during EntityInfo construction. Returns null if no category info is configured.
|
|
2379
2623
|
*/
|
|
2380
2624
|
get FieldCategories() {
|
|
2381
|
-
return this.
|
|
2625
|
+
return this._fieldCategories;
|
|
2382
2626
|
}
|
|
2383
2627
|
/**
|
|
2384
2628
|
* Gets the organic keys defined on this entity. Organic keys enable cross-entity
|
|
@@ -2387,7 +2631,7 @@ export class EntityInfo extends BaseInfo {
|
|
|
2387
2631
|
* @returns {EntityOrganicKeyInfo[]} Array of organic key definitions with their related entities
|
|
2388
2632
|
*/
|
|
2389
2633
|
get OrganicKeys() {
|
|
2390
|
-
return this.
|
|
2634
|
+
return this._organicKeys;
|
|
2391
2635
|
}
|
|
2392
2636
|
static { this.__createdAtFieldName = '__mj_CreatedAt'; }
|
|
2393
2637
|
static { this.__updatedAtFieldName = '__mj_UpdatedAt'; }
|
|
@@ -2444,6 +2688,38 @@ export class EntityInfo extends BaseInfo {
|
|
|
2444
2688
|
get DisplayNameOrName() {
|
|
2445
2689
|
return this.DisplayName ? this.DisplayName : this.Name;
|
|
2446
2690
|
}
|
|
2691
|
+
/**
|
|
2692
|
+
* Returns a business-user-friendly PLURAL of the entity's display name, e.g. "Contacts", "Companies",
|
|
2693
|
+
* "Addresses". Derived from `DisplayNameOrName` via `GeneratePluralName`, so a per-deployment
|
|
2694
|
+
* `DisplayName` override flows through (rename the entity's DisplayName to "Member" and this returns
|
|
2695
|
+
* "Members"). Handles irregular plurals and the common English rules (`-y` to `-ies`, `-s/ch/sh/x/z` to `-es`).
|
|
2696
|
+
*
|
|
2697
|
+
* This is the seam for surfacing the user's own domain nouns in place of the platform meta-noun
|
|
2698
|
+
* "entity" on business-user surfaces (empty states, counts, headers). It is display-only: never use it
|
|
2699
|
+
* as a lookup key. If the display name is already plural, `GeneratePluralName` returns it unchanged.
|
|
2700
|
+
*
|
|
2701
|
+
* Memoized per instance: templates read this on every change-detection pass, and the source name
|
|
2702
|
+
* is fixed per metadata load (the same assumption `_nameFieldCache` relies on). The cache is keyed
|
|
2703
|
+
* by the source name so it stays correct if `DisplayName` is ever mutated in place.
|
|
2704
|
+
*/
|
|
2705
|
+
get DisplayNamePlural() {
|
|
2706
|
+
const source = this.DisplayNameOrName;
|
|
2707
|
+
if (this._displayNamePluralCache?.source !== source) {
|
|
2708
|
+
this._displayNamePluralCache = { source, plural: EntityInfo.matchLeadingCase(source, GeneratePluralName(source)) };
|
|
2709
|
+
}
|
|
2710
|
+
return this._displayNamePluralCache.plural;
|
|
2711
|
+
}
|
|
2712
|
+
/**
|
|
2713
|
+
* Gives `plural` the same leading-letter case as `source`. The irregular-plural table stores
|
|
2714
|
+
* lowercase values ('person' → 'people'), so without this an empty state reads "No people to
|
|
2715
|
+
* display". Only the first letter is touched: `capitalizeFirstLetterOnly` would also capitalize a
|
|
2716
|
+
* name that deliberately starts lowercase ("iPhone" → "IPhones").
|
|
2717
|
+
*/
|
|
2718
|
+
static matchLeadingCase(source, plural) {
|
|
2719
|
+
const lead = source.charAt(0);
|
|
2720
|
+
const startsUpper = lead !== lead.toLowerCase();
|
|
2721
|
+
return startsUpper && plural.length > 0 ? plural.charAt(0).toUpperCase() + plural.slice(1) : plural;
|
|
2722
|
+
}
|
|
2447
2723
|
/**
|
|
2448
2724
|
* Returns the EntityField object for the Field that has IsNameField set to true. If multiple fields have IsNameField on, the function will return the first field (by sequence) that matches.
|
|
2449
2725
|
* If no fields match, if there is a field called "Name", that is returned. If there is no field called "Name", null is returned.
|
|
@@ -3047,10 +3323,10 @@ export class EntityInfo extends BaseInfo {
|
|
|
3047
3323
|
const params = {};
|
|
3048
3324
|
const matchFields = organicKey.MatchFieldNamesArray;
|
|
3049
3325
|
if (organicKeyRelatedEntity.IsTransitiveMatch) {
|
|
3050
|
-
params.ExtraFilter = EntityInfo.
|
|
3326
|
+
params.ExtraFilter = EntityInfo.buildTransitiveOrganicKeyFilter(record, organicKeyRelatedEntity, organicKey, matchFields);
|
|
3051
3327
|
}
|
|
3052
3328
|
else {
|
|
3053
|
-
params.ExtraFilter = EntityInfo.
|
|
3329
|
+
params.ExtraFilter = EntityInfo.buildDirectOrganicKeyFilter(record, organicKeyRelatedEntity, organicKey, matchFields);
|
|
3054
3330
|
}
|
|
3055
3331
|
if (filter && filter.length > 0) {
|
|
3056
3332
|
params.ExtraFilter = `(${params.ExtraFilter}) AND (${filter})`;
|
|
@@ -3070,12 +3346,12 @@ export class EntityInfo extends BaseInfo {
|
|
|
3070
3346
|
* matching organic key (same Name) to find its expression. Falls back to the hub's
|
|
3071
3347
|
* expression on both sides if the spoke doesn't carry its own.
|
|
3072
3348
|
*/
|
|
3073
|
-
static
|
|
3349
|
+
static buildDirectOrganicKeyFilter(record, relatedEntity, organicKey, matchFields) {
|
|
3074
3350
|
const relatedFields = relatedEntity.RelatedEntityFieldNamesArray;
|
|
3075
3351
|
const conditions = [];
|
|
3076
3352
|
// Resolve the spoke entity's own organic key (matching by Name) to pull its
|
|
3077
3353
|
// per-column normalization. Falls back to the hub's expression if not found.
|
|
3078
|
-
const spokeOrganicKey = EntityInfo.
|
|
3354
|
+
const spokeOrganicKey = EntityInfo.resolveSpokeOrganicKey(relatedEntity, organicKey);
|
|
3079
3355
|
for (let i = 0; i < matchFields.length; i++) {
|
|
3080
3356
|
const value = record.Get(matchFields[i]);
|
|
3081
3357
|
if (value == null) {
|
|
@@ -3084,7 +3360,7 @@ export class EntityInfo extends BaseInfo {
|
|
|
3084
3360
|
}
|
|
3085
3361
|
const relatedField = relatedFields[i] || matchFields[i];
|
|
3086
3362
|
const escapedValue = String(value).replace(/'/g, "''");
|
|
3087
|
-
conditions.push(EntityInfo.
|
|
3363
|
+
conditions.push(EntityInfo.wrapBothSidesWithNormalization(`[${relatedField}]`, spokeOrganicKey ?? organicKey, escapedValue, organicKey));
|
|
3088
3364
|
}
|
|
3089
3365
|
return conditions.join(' AND ');
|
|
3090
3366
|
}
|
|
@@ -3093,7 +3369,7 @@ export class EntityInfo extends BaseInfo {
|
|
|
3093
3369
|
* the spoke's own normalization function on the spoke side. Returns undefined if the
|
|
3094
3370
|
* spoke entity doesn't have a parallel organic key — caller falls back to the hub's.
|
|
3095
3371
|
*/
|
|
3096
|
-
static
|
|
3372
|
+
static resolveSpokeOrganicKey(relatedEntity, hubOrganicKey, provider) {
|
|
3097
3373
|
const md = provider ?? Metadata.Provider;
|
|
3098
3374
|
if (!md)
|
|
3099
3375
|
return undefined;
|
|
@@ -3106,7 +3382,7 @@ export class EntityInfo extends BaseInfo {
|
|
|
3106
3382
|
/**
|
|
3107
3383
|
* Builds an ExtraFilter for transitive organic key matching (via SQL view/table subquery).
|
|
3108
3384
|
*/
|
|
3109
|
-
static
|
|
3385
|
+
static buildTransitiveOrganicKeyFilter(record, relatedEntity, organicKey, matchFields) {
|
|
3110
3386
|
const transitiveMatchFields = relatedEntity.TransitiveObjectMatchFieldNamesArray;
|
|
3111
3387
|
const conditions = [];
|
|
3112
3388
|
// Transitive bridge values are normalized using the HUB's expression on both sides —
|
|
@@ -3119,7 +3395,7 @@ export class EntityInfo extends BaseInfo {
|
|
|
3119
3395
|
}
|
|
3120
3396
|
const transitiveField = transitiveMatchFields[i] || matchFields[i];
|
|
3121
3397
|
const escapedValue = String(value).replace(/'/g, "''");
|
|
3122
|
-
conditions.push(EntityInfo.
|
|
3398
|
+
conditions.push(EntityInfo.wrapWithNormalization(`[${transitiveField}]`, organicKey, escapedValue));
|
|
3123
3399
|
}
|
|
3124
3400
|
// TransitiveObjectName is schema-qualified (e.g. "mailchimp.vwBridge") — bracket each part separately
|
|
3125
3401
|
const transitiveObject = relatedEntity.TransitiveObjectName.split('.').map(p => `[${p}]`).join('.');
|
|
@@ -3130,7 +3406,7 @@ export class EntityInfo extends BaseInfo {
|
|
|
3130
3406
|
* based on the organic key's NormalizationStrategy.
|
|
3131
3407
|
* Returns a SQL comparison expression like: LOWER(LTRIM(RTRIM([Field]))) = LOWER(LTRIM(RTRIM('value')))
|
|
3132
3408
|
*/
|
|
3133
|
-
static
|
|
3409
|
+
static wrapWithNormalization(fieldExpression, organicKey, escapedValue) {
|
|
3134
3410
|
switch (organicKey.NormalizationStrategy) {
|
|
3135
3411
|
case 'LowerCaseTrim':
|
|
3136
3412
|
return `LOWER(LTRIM(RTRIM(${fieldExpression}))) = LOWER(LTRIM(RTRIM('${escapedValue}')))`;
|
|
@@ -3162,13 +3438,13 @@ export class EntityInfo extends BaseInfo {
|
|
|
3162
3438
|
* when the expressions agree; different transforms applied independently when they
|
|
3163
3439
|
* don't (the per-column normalization case).
|
|
3164
3440
|
*/
|
|
3165
|
-
static
|
|
3166
|
-
const leftSide = EntityInfo.
|
|
3167
|
-
const rightSide = EntityInfo.
|
|
3441
|
+
static wrapBothSidesWithNormalization(fieldExpression, fieldOrganicKey, escapedValue, valueOrganicKey) {
|
|
3442
|
+
const leftSide = EntityInfo.normalizeFieldExpression(fieldExpression, fieldOrganicKey);
|
|
3443
|
+
const rightSide = EntityInfo.normalizeLiteralExpression(escapedValue, valueOrganicKey);
|
|
3168
3444
|
return `${leftSide} = ${rightSide}`;
|
|
3169
3445
|
}
|
|
3170
3446
|
/** Apply an organic key's normalization to a SQL field expression (left side of compare). */
|
|
3171
|
-
static
|
|
3447
|
+
static normalizeFieldExpression(fieldExpression, organicKey) {
|
|
3172
3448
|
switch (organicKey.NormalizationStrategy) {
|
|
3173
3449
|
case 'LowerCaseTrim': return `LOWER(LTRIM(RTRIM(${fieldExpression})))`;
|
|
3174
3450
|
case 'Trim': return `LTRIM(RTRIM(${fieldExpression}))`;
|
|
@@ -3184,7 +3460,7 @@ export class EntityInfo extends BaseInfo {
|
|
|
3184
3460
|
}
|
|
3185
3461
|
}
|
|
3186
3462
|
/** Apply an organic key's normalization to a quoted literal value (right side of compare). */
|
|
3187
|
-
static
|
|
3463
|
+
static normalizeLiteralExpression(escapedValue, organicKey) {
|
|
3188
3464
|
switch (organicKey.NormalizationStrategy) {
|
|
3189
3465
|
case 'LowerCaseTrim': return `LOWER(LTRIM(RTRIM('${escapedValue}')))`;
|
|
3190
3466
|
case 'Trim': return `LTRIM(RTRIM('${escapedValue}'))`;
|
|
@@ -3480,27 +3756,27 @@ export class EntityInfo extends BaseInfo {
|
|
|
3480
3756
|
/**
|
|
3481
3757
|
* Name of the stored procedure for creating records
|
|
3482
3758
|
*/
|
|
3483
|
-
this.
|
|
3759
|
+
this.SpCreate = null;
|
|
3484
3760
|
/**
|
|
3485
3761
|
* Name of the stored procedure for updating records
|
|
3486
3762
|
*/
|
|
3487
|
-
this.
|
|
3763
|
+
this.SpUpdate = null;
|
|
3488
3764
|
/**
|
|
3489
3765
|
* Name of the stored procedure for deleting records
|
|
3490
3766
|
*/
|
|
3491
|
-
this.
|
|
3767
|
+
this.SpDelete = null;
|
|
3492
3768
|
/**
|
|
3493
3769
|
* Whether the create stored procedure is generated by CodeGen
|
|
3494
3770
|
*/
|
|
3495
|
-
this.
|
|
3771
|
+
this.SpCreateGenerated = null;
|
|
3496
3772
|
/**
|
|
3497
3773
|
* Whether the update stored procedure is generated by CodeGen
|
|
3498
3774
|
*/
|
|
3499
|
-
this.
|
|
3775
|
+
this.SpUpdateGenerated = null;
|
|
3500
3776
|
/**
|
|
3501
3777
|
* Whether the delete stored procedure is generated by CodeGen
|
|
3502
3778
|
*/
|
|
3503
|
-
this.
|
|
3779
|
+
this.SpDeleteGenerated = null;
|
|
3504
3780
|
/**
|
|
3505
3781
|
* Whether to automatically delete related records when a parent is deleted
|
|
3506
3782
|
*/
|
|
@@ -3516,7 +3792,7 @@ export class EntityInfo extends BaseInfo {
|
|
|
3516
3792
|
/**
|
|
3517
3793
|
* Name of the stored procedure used for matching/duplicate detection
|
|
3518
3794
|
*/
|
|
3519
|
-
this.
|
|
3795
|
+
this.SpMatch = null;
|
|
3520
3796
|
/**
|
|
3521
3797
|
* Default display type for relationships: Search (type-ahead) or Dropdown
|
|
3522
3798
|
*/
|
|
@@ -3641,13 +3917,13 @@ export class EntityInfo extends BaseInfo {
|
|
|
3641
3917
|
this.ParentEntity = null;
|
|
3642
3918
|
this.ParentBaseTable = null;
|
|
3643
3919
|
this.ParentBaseView = null;
|
|
3644
|
-
this.
|
|
3645
|
-
this.
|
|
3646
|
-
this.
|
|
3647
|
-
this.
|
|
3648
|
-
this.
|
|
3649
|
-
this.
|
|
3650
|
-
this.
|
|
3920
|
+
this._fieldCategories = null;
|
|
3921
|
+
this._organicKeys = [];
|
|
3922
|
+
this.HasIdField = false;
|
|
3923
|
+
this.VirtualCount = 0;
|
|
3924
|
+
this.ManyToManyCount = 0;
|
|
3925
|
+
this.OneToManyCount = 0;
|
|
3926
|
+
this.FloatCount = 0;
|
|
3651
3927
|
// --- Lazy caches for immutable field-derived collections ---------------------------------
|
|
3652
3928
|
// `_Fields` is populated once in the constructor and never reassigned, so these caches never
|
|
3653
3929
|
// need invalidation. They replace per-access `.filter()`/`.find()` scans on hot paths
|
|
@@ -3660,7 +3936,10 @@ export class EntityInfo extends BaseInfo {
|
|
|
3660
3936
|
this._encryptedFieldsCache = null;
|
|
3661
3937
|
this._datetimeFieldsCache = null;
|
|
3662
3938
|
this._nameFieldCache = undefined;
|
|
3939
|
+
/** Memoized computed plural, keyed by the display name it was derived from (see `DisplayNamePlural`). */
|
|
3940
|
+
this._displayNamePluralCache = undefined;
|
|
3663
3941
|
this._hasInactiveFields = undefined;
|
|
3942
|
+
this._hasSearchFields = undefined;
|
|
3664
3943
|
// Cache for ParentChain to avoid repeated walks
|
|
3665
3944
|
this._parentChainCache = null;
|
|
3666
3945
|
// Cache for ParentEntityFieldNames
|
|
@@ -3669,8 +3948,10 @@ export class EntityInfo extends BaseInfo {
|
|
|
3669
3948
|
this.copyInitData(initData);
|
|
3670
3949
|
// do some special handling to create class instances instead of just data objects
|
|
3671
3950
|
// copy the Entity Fields (accept EntityFields, _Fields, or Fields as input names)
|
|
3672
|
-
this.
|
|
3951
|
+
this._fields = [];
|
|
3673
3952
|
// Reset every lazy field-derived memo cache whenever _Fields is (re)assigned.
|
|
3953
|
+
// `entityInfo.cacheReset.test.ts` mirrors this list in `runProductionCacheReset`;
|
|
3954
|
+
// a cache added here needs adding there too, or its reset goes unexercised.
|
|
3674
3955
|
// These caches (FieldByName map, PrimaryKeys, UniqueKeys, ForeignKeys, EncryptedFields,
|
|
3675
3956
|
// DatetimeFields, NameField, FirstPrimaryKey) are populated lazily off this.Fields and
|
|
3676
3957
|
// were previously relying on an implicit "_Fields is write-once after construction"
|
|
@@ -3687,30 +3968,34 @@ export class EntityInfo extends BaseInfo {
|
|
|
3687
3968
|
this._encryptedFieldsCache = null;
|
|
3688
3969
|
this._datetimeFieldsCache = null;
|
|
3689
3970
|
this._nameFieldCache = undefined;
|
|
3971
|
+
this._hasSearchFields = undefined;
|
|
3972
|
+
// Added late: HasInactiveFields (Jun 18) post-dates this block (Jun 15) and was never
|
|
3973
|
+
// listed here, so it carried exactly the staleness the block exists to prevent.
|
|
3974
|
+
this._hasInactiveFields = undefined;
|
|
3690
3975
|
const ef = initData.EntityFields || initData._Fields || initData.Fields;
|
|
3691
3976
|
if (ef) {
|
|
3692
3977
|
for (let j = 0; j < ef.length; j++) {
|
|
3693
|
-
this.
|
|
3978
|
+
this._fields.push(new EntityFieldInfo(ef[j]));
|
|
3694
3979
|
}
|
|
3695
3980
|
}
|
|
3696
3981
|
// copy the Entity Permissions
|
|
3697
|
-
this.
|
|
3982
|
+
this._permissions = [];
|
|
3698
3983
|
const ep = initData.EntityPermissions || initData._Permissions || initData.Permissions;
|
|
3699
3984
|
if (ep) {
|
|
3700
3985
|
for (let j = 0; j < ep.length; j++) {
|
|
3701
|
-
this.
|
|
3986
|
+
this._permissions.push(new EntityPermissionInfo(ep[j]));
|
|
3702
3987
|
}
|
|
3703
3988
|
}
|
|
3704
3989
|
// copy the Entity settings
|
|
3705
|
-
this.
|
|
3990
|
+
this._settings = [];
|
|
3706
3991
|
const es = initData.EntitySettings || initData._Settings || initData.Settings;
|
|
3707
3992
|
if (es) {
|
|
3708
|
-
es.map((s) => this.
|
|
3993
|
+
es.map((s) => this._settings.push(new EntitySettingInfo(s)));
|
|
3709
3994
|
}
|
|
3710
3995
|
// auto-populate FieldCategories from the FieldCategoryInfo setting
|
|
3711
|
-
this.
|
|
3996
|
+
this._fieldCategories = this.parseFieldCategoriesFromSettings();
|
|
3712
3997
|
// copy the Related Entities (accept EntityRelationships, _RelatedEntities, or RelatedEntities as input names)
|
|
3713
|
-
this.
|
|
3998
|
+
this._relatedEntities = [];
|
|
3714
3999
|
const er = initData.EntityRelationships || initData._RelatedEntities || initData.RelatedEntities;
|
|
3715
4000
|
if (er) {
|
|
3716
4001
|
// check to see if ANY of the records in the er array have a non-null or non-zero sequence value. The reason is
|
|
@@ -3733,15 +4018,15 @@ export class EntityInfo extends BaseInfo {
|
|
|
3733
4018
|
}
|
|
3734
4019
|
// now that we have prepared the er array by sorting it, if needed, let's load up the related entities
|
|
3735
4020
|
for (let j = 0; j < er.length; j++) {
|
|
3736
|
-
this.
|
|
4021
|
+
this._relatedEntities.push(new EntityRelationshipInfo(er[j]));
|
|
3737
4022
|
}
|
|
3738
4023
|
}
|
|
3739
4024
|
// copy the Organic Keys (sorted by sequence inside EntityOrganicKeyInfo constructor)
|
|
3740
|
-
this.
|
|
4025
|
+
this._organicKeys = [];
|
|
3741
4026
|
const ok = initData.EntityOrganicKeys || initData._OrganicKeys || initData.OrganicKeys;
|
|
3742
4027
|
if (ok && Array.isArray(ok)) {
|
|
3743
4028
|
for (const item of ok) {
|
|
3744
|
-
this.
|
|
4029
|
+
this._organicKeys.push(new EntityOrganicKeyInfo(item));
|
|
3745
4030
|
}
|
|
3746
4031
|
}
|
|
3747
4032
|
this.prepareSpecialFields();
|
|
@@ -3761,9 +4046,9 @@ export class EntityInfo extends BaseInfo {
|
|
|
3761
4046
|
virtualCount += f.IsVirtual ? 1 : 0;
|
|
3762
4047
|
floatCount += f.IsFloat ? 1 : 0;
|
|
3763
4048
|
}
|
|
3764
|
-
this.
|
|
3765
|
-
this.
|
|
3766
|
-
this.
|
|
4049
|
+
this.HasIdField = hasIdField;
|
|
4050
|
+
this.FloatCount = floatCount;
|
|
4051
|
+
this.VirtualCount = virtualCount;
|
|
3767
4052
|
// now see if there are any relationships and count the one to many and many to many
|
|
3768
4053
|
for (let j = 0; j < this.RelatedEntities.length; ++j) {
|
|
3769
4054
|
const r = this.RelatedEntities[j];
|
|
@@ -3772,8 +4057,8 @@ export class EntityInfo extends BaseInfo {
|
|
|
3772
4057
|
else
|
|
3773
4058
|
manyToManyCount++;
|
|
3774
4059
|
}
|
|
3775
|
-
this.
|
|
3776
|
-
this.
|
|
4060
|
+
this.ManyToManyCount = manyToManyCount;
|
|
4061
|
+
this.OneToManyCount = oneToManyCount;
|
|
3777
4062
|
}
|
|
3778
4063
|
catch (e) {
|
|
3779
4064
|
LogError(e);
|
|
@@ -3784,11 +4069,11 @@ export class EntityInfo extends BaseInfo {
|
|
|
3784
4069
|
* Called once during construction so the result is cached on _FieldCategories.
|
|
3785
4070
|
*/
|
|
3786
4071
|
parseFieldCategoriesFromSettings() {
|
|
3787
|
-
if (!this.
|
|
4072
|
+
if (!this._settings || this._settings.length === 0) {
|
|
3788
4073
|
return null;
|
|
3789
4074
|
}
|
|
3790
4075
|
// Try new format first
|
|
3791
|
-
const infoSetting = this.
|
|
4076
|
+
const infoSetting = this._settings.find(s => s.Name === 'FieldCategoryInfo');
|
|
3792
4077
|
if (infoSetting?.Value) {
|
|
3793
4078
|
const parsed = SafeJSONParse(infoSetting.Value, false);
|
|
3794
4079
|
if (parsed) {
|
|
@@ -3796,7 +4081,7 @@ export class EntityInfo extends BaseInfo {
|
|
|
3796
4081
|
}
|
|
3797
4082
|
}
|
|
3798
4083
|
// Fallback to legacy FieldCategoryIcons format (icon-only map)
|
|
3799
|
-
const iconSetting = this.
|
|
4084
|
+
const iconSetting = this._settings.find(s => s.Name === 'FieldCategoryIcons');
|
|
3800
4085
|
if (iconSetting?.Value) {
|
|
3801
4086
|
const icons = SafeJSONParse(iconSetting.Value, false);
|
|
3802
4087
|
if (icons) {
|