@memberjunction/core 6.2.0-edge.0 → 6.2.0-edge.2
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/IEntityConfiguration.d.ts +180 -0
- package/dist/generic/JSONType-interfaces/IEntityConfiguration.d.ts.map +1 -1
- package/dist/generic/JSONType-interfaces/IEntityFieldConfiguration.d.ts +28 -0
- package/dist/generic/JSONType-interfaces/IEntityFieldConfiguration.d.ts.map +1 -1
- package/dist/generic/JSONType-interfaces/IEntityRelationshipConfiguration.d.ts +76 -0
- package/dist/generic/JSONType-interfaces/IEntityRelationshipConfiguration.d.ts.map +1 -1
- package/dist/generic/JSONType-interfaces/IEntitySubtypeSelectorConfig.d.ts +14 -0
- package/dist/generic/JSONType-interfaces/IEntitySubtypeSelectorConfig.d.ts.map +1 -1
- 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 +9 -1
- package/dist/generic/baseEngine.d.ts.map +1 -1
- package/dist/generic/baseEngine.js +13 -3
- 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 +422 -28
- package/dist/generic/baseEntity.d.ts.map +1 -1
- package/dist/generic/baseEntity.js +930 -202
- 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 +13 -7
- package/dist/generic/databaseProviderBase.d.ts.map +1 -1
- package/dist/generic/databaseProviderBase.js +20 -11
- package/dist/generic/databaseProviderBase.js.map +1 -1
- package/dist/generic/entityConfiguration.d.ts +1 -0
- package/dist/generic/entityConfiguration.d.ts.map +1 -1
- package/dist/generic/entityConfiguration.js.map +1 -1
- package/dist/generic/entityInfo.d.ts +199 -39
- package/dist/generic/entityInfo.d.ts.map +1 -1
- package/dist/generic/entityInfo.js +351 -78
- 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 +13 -4
- 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 +89 -8
- package/dist/generic/providerBase.d.ts.map +1 -1
- package/dist/generic/providerBase.js +180 -45
- package/dist/generic/providerBase.js.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/remoteEventRow.d.ts +55 -0
- package/dist/generic/remoteEventRow.d.ts.map +1 -0
- package/dist/generic/remoteEventRow.js +110 -0
- package/dist/generic/remoteEventRow.js.map +1 -0
- 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/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
|
|
@@ -101,6 +101,14 @@ export class EntityRelationshipInfo extends BaseInfo {
|
|
|
101
101
|
get ConfigurationObject() {
|
|
102
102
|
return this.Configuration;
|
|
103
103
|
}
|
|
104
|
+
/**
|
|
105
|
+
* Parsed clone configuration for this relationship.
|
|
106
|
+
* Specifies the policy (Deep, Reference, Skip), locked status, field rules, etc.
|
|
107
|
+
* @see plans/record-cloning/README.md §4.2
|
|
108
|
+
*/
|
|
109
|
+
get CloneConfig() {
|
|
110
|
+
return this.ConfigurationObject?.Clone ?? null;
|
|
111
|
+
}
|
|
104
112
|
constructor(initData) {
|
|
105
113
|
super();
|
|
106
114
|
this.ID = null;
|
|
@@ -216,7 +224,7 @@ export class EntityOrganicKeyInfo extends BaseInfo {
|
|
|
216
224
|
this.__mj_UpdatedAt = null;
|
|
217
225
|
// virtual fields from the database view
|
|
218
226
|
this.Entity = null;
|
|
219
|
-
this._RelatedEntities = [];
|
|
227
|
+
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
228
|
if (initData) {
|
|
221
229
|
this.copyInitData(initData);
|
|
222
230
|
this._RelatedEntities = [];
|
|
@@ -623,6 +631,7 @@ export class EntityFieldValueInfo extends BaseInfo {
|
|
|
623
631
|
return {
|
|
624
632
|
Value: this.Value,
|
|
625
633
|
Code: this.Code,
|
|
634
|
+
Description: this.Description ?? undefined,
|
|
626
635
|
};
|
|
627
636
|
}
|
|
628
637
|
}
|
|
@@ -674,6 +683,14 @@ export class EntityFieldInfo extends BaseInfo {
|
|
|
674
683
|
get ConfigurationObject() {
|
|
675
684
|
return this.Configuration;
|
|
676
685
|
}
|
|
686
|
+
/**
|
|
687
|
+
* Parsed clone configuration for this field.
|
|
688
|
+
* Specifies copy/reset/remap/suffix policy, value transformations, JSON remaps, etc.
|
|
689
|
+
* @see plans/record-cloning/README.md §4.3
|
|
690
|
+
*/
|
|
691
|
+
get CloneConfig() {
|
|
692
|
+
return this.ConfigurationObject?.Clone ?? null;
|
|
693
|
+
}
|
|
677
694
|
/**
|
|
678
695
|
* Returns true if this field is explicitly configured as an intentional recursive tree hierarchy.
|
|
679
696
|
*/
|
|
@@ -686,6 +703,38 @@ export class EntityFieldInfo extends BaseInfo {
|
|
|
686
703
|
get HierarchyMaxDepth() {
|
|
687
704
|
return this.ConfigurationObject?.Hierarchy?.MaxDepth ?? 100;
|
|
688
705
|
}
|
|
706
|
+
/** @deprecated Use {@link RelatedEntityTableAlias}. */
|
|
707
|
+
get _RelatedEntityTableAlias() {
|
|
708
|
+
return this.RelatedEntityTableAlias;
|
|
709
|
+
}
|
|
710
|
+
/** @deprecated Use {@link RelatedEntityTableAlias}. */
|
|
711
|
+
set _RelatedEntityTableAlias(value) {
|
|
712
|
+
this.RelatedEntityTableAlias = value;
|
|
713
|
+
}
|
|
714
|
+
/** @deprecated Use {@link RelatedEntityNameFieldIsVirtual}. */
|
|
715
|
+
get _RelatedEntityNameFieldIsVirtual() {
|
|
716
|
+
return this.RelatedEntityNameFieldIsVirtual;
|
|
717
|
+
}
|
|
718
|
+
/** @deprecated Use {@link RelatedEntityNameFieldIsVirtual}. */
|
|
719
|
+
set _RelatedEntityNameFieldIsVirtual(value) {
|
|
720
|
+
this.RelatedEntityNameFieldIsVirtual = value;
|
|
721
|
+
}
|
|
722
|
+
/** @deprecated Use {@link RelatedEntityNameFieldIsComputed}. */
|
|
723
|
+
get _RelatedEntityNameFieldIsComputed() {
|
|
724
|
+
return this.RelatedEntityNameFieldIsComputed;
|
|
725
|
+
}
|
|
726
|
+
/** @deprecated Use {@link RelatedEntityNameFieldIsComputed}. */
|
|
727
|
+
set _RelatedEntityNameFieldIsComputed(value) {
|
|
728
|
+
this.RelatedEntityNameFieldIsComputed = value;
|
|
729
|
+
}
|
|
730
|
+
/** @deprecated Use {@link RelatedEntityJoinFieldMappings}. */
|
|
731
|
+
get _RelatedEntityJoinFieldMappings() {
|
|
732
|
+
return this.RelatedEntityJoinFieldMappings;
|
|
733
|
+
}
|
|
734
|
+
/** @deprecated Use {@link RelatedEntityJoinFieldMappings}. */
|
|
735
|
+
set _RelatedEntityJoinFieldMappings(value) {
|
|
736
|
+
this.RelatedEntityJoinFieldMappings = value;
|
|
737
|
+
}
|
|
689
738
|
/**
|
|
690
739
|
* JSON configuration for additional fields to join from the related entity.
|
|
691
740
|
* Parsed from the RelatedEntityJoinFields column. Uses lazy initialization and caching
|
|
@@ -728,7 +777,7 @@ export class EntityFieldInfo extends BaseInfo {
|
|
|
728
777
|
* Empty for the overwhelming majority of fields — see {@link HasFieldPermissions}.
|
|
729
778
|
*/
|
|
730
779
|
get FieldPermissions() {
|
|
731
|
-
return this.
|
|
780
|
+
return this._fieldPermissions;
|
|
732
781
|
}
|
|
733
782
|
/**
|
|
734
783
|
* True when at least one {@link EntityFieldPermissionInfo} record exists for this field.
|
|
@@ -739,7 +788,7 @@ export class EntityFieldInfo extends BaseInfo {
|
|
|
739
788
|
* enabled entity a field with no records is denied, not open.
|
|
740
789
|
*/
|
|
741
790
|
get HasFieldPermissions() {
|
|
742
|
-
return this.
|
|
791
|
+
return this._fieldPermissions.length > 0;
|
|
743
792
|
}
|
|
744
793
|
/**
|
|
745
794
|
* Entities whose fields can never be restricted by field-level security.
|
|
@@ -760,7 +809,7 @@ export class EntityFieldInfo extends BaseInfo {
|
|
|
760
809
|
*
|
|
761
810
|
* Stored lowercased; compare with a trimmed, lowercased entity name.
|
|
762
811
|
*/
|
|
763
|
-
static { this.
|
|
812
|
+
static { this.unrestrictableEntityNames = new Set([
|
|
764
813
|
'mj: entities',
|
|
765
814
|
'mj: entity fields',
|
|
766
815
|
'mj: entity permissions',
|
|
@@ -774,7 +823,7 @@ export class EntityFieldInfo extends BaseInfo {
|
|
|
774
823
|
* See {@link EntityFieldInfo.UnrestrictableEntityNames} for the rationale.
|
|
775
824
|
*/
|
|
776
825
|
get IsOnUnrestrictableEntity() {
|
|
777
|
-
return EntityFieldInfo.
|
|
826
|
+
return EntityFieldInfo.unrestrictableEntityNames.has((this.Entity ?? '').trim().toLowerCase());
|
|
778
827
|
}
|
|
779
828
|
/**
|
|
780
829
|
* True for fields that must remain readable regardless of any permission record:
|
|
@@ -859,7 +908,7 @@ export class EntityFieldInfo extends BaseInfo {
|
|
|
859
908
|
* arithmetic.
|
|
860
909
|
*/
|
|
861
910
|
aggregateUserFieldPermissions(user) {
|
|
862
|
-
return EntityFieldInfo.AggregateFieldRulesForUser(this.
|
|
911
|
+
return EntityFieldInfo.AggregateFieldRulesForUser(this._fieldPermissions, user);
|
|
863
912
|
}
|
|
864
913
|
/**
|
|
865
914
|
* The same aggregation {@link GetUserFieldPermissions} performs, over a rule list the caller
|
|
@@ -958,8 +1007,9 @@ export class EntityFieldInfo extends BaseInfo {
|
|
|
958
1007
|
* * **Stringifying is required.** `EntityFieldValue.Value` is always a string in metadata while
|
|
959
1008
|
* the field's runtime value may be a number, so a strict `===` would reject every legal value
|
|
960
1009
|
* on a numeric list. It is not lossless: `String(1.0)` is `'1'`, so a metadata value written
|
|
961
|
-
* as `'1.
|
|
962
|
-
*
|
|
1010
|
+
* as `'1.00'` would fail closed here — which is why a NUMERIC column's list is not compared
|
|
1011
|
+
* through this function at all but by value, in numericValueIsPermittedByValueList (MJ #3978).
|
|
1012
|
+
* This normalization is what a STRING column's list is compared with.
|
|
963
1013
|
* * **Trimming is cheap insurance, NOT the load-bearing rule it was first documented as.** An
|
|
964
1014
|
* earlier version of this comment claimed an untrimmed comparison would reject 9,301 existing
|
|
965
1015
|
* rows in fixed-width `nchar` columns (`MJ: Action Params`.Type, `MJ: Record Changes`.Status
|
|
@@ -1002,10 +1052,10 @@ export class EntityFieldInfo extends BaseInfo {
|
|
|
1002
1052
|
* * Only string and number values are checked, and the gate fails OPEN — an unsupported type
|
|
1003
1053
|
* skips validation rather than manufacturing a failure. Nothing in the schema restricts
|
|
1004
1054
|
* which columns may carry a value list (`CK_EntityField_ValueListType_New` constrains the
|
|
1005
|
-
* mode, not the column type)
|
|
1006
|
-
*
|
|
1007
|
-
*
|
|
1008
|
-
*
|
|
1055
|
+
* mode, not the column type). Every list WAS on a string column — measured on a 6.x
|
|
1056
|
+
* instance, 455 nvarchar + 7 nchar and nothing else — because CodeGen's constraint parser
|
|
1057
|
+
* only extracted quoted literals; since MJ #3978 it extracts the unquoted numeric form too,
|
|
1058
|
+
* so a numeric column's list is now a real case and is compared by value (below).
|
|
1009
1059
|
* Booleans and Dates are excluded deliberately: a bit column carrying a `'1'`/`'0'` list
|
|
1010
1060
|
* would see `String(true) === 'true'` and reject every legal value, and a Date has no sane
|
|
1011
1061
|
* string form to compare — so guessing there would break saves rather than guard them.
|
|
@@ -1059,17 +1109,72 @@ export class EntityFieldInfo extends BaseInfo {
|
|
|
1059
1109
|
if (typeof value !== 'string' && typeof value !== 'number') {
|
|
1060
1110
|
// The rule cannot be applied to this type at all, and that is a mismatch rather than a
|
|
1061
1111
|
// 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
|
|
1112
|
+
// — which CodeGen still never produces: it captures the unquoted numeric form as of MJ
|
|
1113
|
+
// #3978, but excludes `bit` fields, since `IN (0,1)` is vacuous on a bit and `= 1` is a
|
|
1114
|
+
// validator rather than a dropdown) or a caller assigned the wrong type. Skipping it silently would
|
|
1065
1115
|
// leave the caller believing a guard is on when it is not, which is the exact failure
|
|
1066
1116
|
// mode this rung was added to fix — so it is reported, once per field.
|
|
1067
1117
|
this.reportUnsupportedValueListValue(`a ${typeof value} value (SQL type ${this.SQLFullType})`, 'value-list validation compares strings, numbers and dates only — comparing anything ' +
|
|
1068
1118
|
'else would reject legal values rather than guard them');
|
|
1069
1119
|
return true;
|
|
1070
1120
|
}
|
|
1121
|
+
// A NUMERIC column's list is compared by VALUE, not by string form — see
|
|
1122
|
+
// numericValueIsPermittedByValueList. Falls through to the string comparison when the list is
|
|
1123
|
+
// not entirely numeric, which is the only case where there is nothing to compare against.
|
|
1124
|
+
if (this.TSType === EntityFieldTSType.Number) {
|
|
1125
|
+
const numericKeys = this.valueListNumericKeys();
|
|
1126
|
+
if (numericKeys) {
|
|
1127
|
+
return this.numericValueIsPermittedByValueList(value, numericKeys);
|
|
1128
|
+
}
|
|
1129
|
+
}
|
|
1071
1130
|
return this._normalizedValueListValues.has(EntityFieldInfo.NormalizeValueListValue(value));
|
|
1072
1131
|
}
|
|
1132
|
+
/**
|
|
1133
|
+
* Value-list membership for a NUMERIC column, compared on the numeric VALUE rather than the
|
|
1134
|
+
* string form (MJ issue #3978).
|
|
1135
|
+
*
|
|
1136
|
+
* Necessary because the two sides are written by different hands: SQL Server stores
|
|
1137
|
+
* `CHECK (Price IN (0.50, 1.00))` as the unquoted literals `([Price]=(1.00) OR [Price]=(0.50))`,
|
|
1138
|
+
* CodeGen captures those literals verbatim, and the runtime value of that column is the number
|
|
1139
|
+
* `1`. `String(1) === '1'` is not `'1.00'`, so a string comparison would refuse a value the
|
|
1140
|
+
* database accepts — the exact failure mode this rung exists to avoid. Comparing as numbers makes
|
|
1141
|
+
* the scale of the literal irrelevant.
|
|
1142
|
+
*
|
|
1143
|
+
* The comparison is gated on the column being numeric, NOT merely on the list looking numeric: on
|
|
1144
|
+
* a string column `CHECK (Code IN ('1','2'))` really is a string list, and comparing `'01'`
|
|
1145
|
+
* numerically would permit a value the database refuses.
|
|
1146
|
+
*
|
|
1147
|
+
* A value that is not a finite number falls back to the string comparison rather than being
|
|
1148
|
+
* refused here, so a value list that happens to hold non-numeric text on a numeric column (which
|
|
1149
|
+
* CodeGen cannot produce, but `applyValueListConfig` could) still behaves as it did before.
|
|
1150
|
+
*/
|
|
1151
|
+
numericValueIsPermittedByValueList(value, numericKeys) {
|
|
1152
|
+
const candidate = typeof value === 'number' ? value : Number(value.trim());
|
|
1153
|
+
if (!Number.isFinite(candidate)) {
|
|
1154
|
+
return this._normalizedValueListValues.has(EntityFieldInfo.NormalizeValueListValue(value));
|
|
1155
|
+
}
|
|
1156
|
+
return numericKeys.has(candidate);
|
|
1157
|
+
}
|
|
1158
|
+
/**
|
|
1159
|
+
* The value list as a set of numbers, or null when any of its values is not a finite number (so
|
|
1160
|
+
* there is nothing to compare numerically). Memoized alongside the normalized string set, and
|
|
1161
|
+
* built from it — lower-casing and trimming cannot change how a numeric literal parses.
|
|
1162
|
+
*/
|
|
1163
|
+
valueListNumericKeys() {
|
|
1164
|
+
if (this._valueListNumericKeys === undefined) {
|
|
1165
|
+
const keys = new Set();
|
|
1166
|
+
for (const normalized of this._normalizedValueListValues ?? []) {
|
|
1167
|
+
const parsed = Number(normalized);
|
|
1168
|
+
if (normalized.length === 0 || !Number.isFinite(parsed)) {
|
|
1169
|
+
this._valueListNumericKeys = null;
|
|
1170
|
+
return null;
|
|
1171
|
+
}
|
|
1172
|
+
keys.add(parsed);
|
|
1173
|
+
}
|
|
1174
|
+
this._valueListNumericKeys = keys.size > 0 ? keys : null;
|
|
1175
|
+
}
|
|
1176
|
+
return this._valueListNumericKeys;
|
|
1177
|
+
}
|
|
1073
1178
|
/**
|
|
1074
1179
|
* Value-list membership for a `Date` runtime value, compared on the CALENDAR DATE rather than
|
|
1075
1180
|
* the instant.
|
|
@@ -1101,7 +1206,7 @@ export class EntityFieldInfo extends BaseInfo {
|
|
|
1101
1206
|
return true; // an Invalid Date is not an out-of-list value; leave it to the date check
|
|
1102
1207
|
}
|
|
1103
1208
|
return this._valueListDateKeys.has(value.toISOString().slice(0, 10)) ||
|
|
1104
|
-
this._valueListDateKeys.has(EntityFieldInfo.
|
|
1209
|
+
this._valueListDateKeys.has(EntityFieldInfo.localCalendarDate(value));
|
|
1105
1210
|
}
|
|
1106
1211
|
/**
|
|
1107
1212
|
* Builds the set of yyyy-mm-dd keys for a `date` field's value list, or null when the field is
|
|
@@ -1124,7 +1229,7 @@ export class EntityFieldInfo extends BaseInfo {
|
|
|
1124
1229
|
return keys.size > 0 ? keys : null;
|
|
1125
1230
|
}
|
|
1126
1231
|
/** The Date's LOCAL calendar date as yyyy-mm-dd (its UTC one is `toISOString().slice(0, 10)`). */
|
|
1127
|
-
static
|
|
1232
|
+
static localCalendarDate(value) {
|
|
1128
1233
|
const pad = (n) => String(n).padStart(2, '0');
|
|
1129
1234
|
return `${value.getFullYear()}-${pad(value.getMonth() + 1)}-${pad(value.getDate())}`;
|
|
1130
1235
|
}
|
|
@@ -1872,6 +1977,8 @@ export class EntityFieldInfo extends BaseInfo {
|
|
|
1872
1977
|
this._loggedUnsupportedValueListType = false;
|
|
1873
1978
|
/** Memoized yyyy-mm-dd keys for a `date` field's value list; null when it cannot be compared. */
|
|
1874
1979
|
this._valueListDateKeys = undefined;
|
|
1980
|
+
/** Memoized numeric form of a numeric field's value list; null when it is not entirely numeric. */
|
|
1981
|
+
this._valueListNumericKeys = undefined;
|
|
1875
1982
|
/**
|
|
1876
1983
|
* Cached parsed RelatedEntityJoinFieldsConfig to avoid repeated JSON.parse calls.
|
|
1877
1984
|
* Lazy-initialized on first access.
|
|
@@ -1881,7 +1988,7 @@ export class EntityFieldInfo extends BaseInfo {
|
|
|
1881
1988
|
* Flag to track if RelatedEntityJoinFieldsConfig parsing failed to avoid repeated parse attempts on bad JSON.
|
|
1882
1989
|
*/
|
|
1883
1990
|
this._relatedEntityJoinFieldsFailedParsing = false;
|
|
1884
|
-
this.
|
|
1991
|
+
this._fieldPermissions = [];
|
|
1885
1992
|
/** Memoized {@link TSType} — `Type` is immutable after metadata load, so the classification never changes. */
|
|
1886
1993
|
this._tsType = undefined;
|
|
1887
1994
|
/**
|
|
@@ -1911,7 +2018,7 @@ export class EntityFieldInfo extends BaseInfo {
|
|
|
1911
2018
|
// would cost more than the construction it avoids.
|
|
1912
2019
|
const efp = initData.EntityFieldPermissions || initData._FieldPermissions || initData.FieldPermissions;
|
|
1913
2020
|
if (efp && efp.length > 0) {
|
|
1914
|
-
this.
|
|
2021
|
+
this._fieldPermissions = efp.map((p) => new EntityFieldPermissionInfo(p));
|
|
1915
2022
|
}
|
|
1916
2023
|
}
|
|
1917
2024
|
}
|
|
@@ -2058,7 +2165,10 @@ export class EntityInfo extends BaseInfo {
|
|
|
2058
2165
|
try {
|
|
2059
2166
|
const parsed = JSON.parse(this.SubtypeSelector);
|
|
2060
2167
|
if (parsed && typeof parsed['Path'] === 'string' && parsed['Path'].trim().length > 0) {
|
|
2061
|
-
this._subtypeSelectorConfig = {
|
|
2168
|
+
this._subtypeSelectorConfig = {
|
|
2169
|
+
Path: parsed['Path'].trim(),
|
|
2170
|
+
UseForLoadedRecords: this.parseSubtypeSelectorUseForLoadedRecords(parsed),
|
|
2171
|
+
};
|
|
2062
2172
|
}
|
|
2063
2173
|
else {
|
|
2064
2174
|
LogError(`EntityInfo '${this.Name}': SubtypeSelector JSON must contain a non-empty 'Path' string property. Found: ${this.SubtypeSelector}`);
|
|
@@ -2076,6 +2186,19 @@ export class EntityInfo extends BaseInfo {
|
|
|
2076
2186
|
}
|
|
2077
2187
|
return this._subtypeSelectorConfig;
|
|
2078
2188
|
}
|
|
2189
|
+
/**
|
|
2190
|
+
* Reads the selector's optional `UseForLoadedRecords` flag. Only a JSON `true` turns load hints
|
|
2191
|
+
* on; a value of any other type is logged and leaves them off, so a quoted `"true"` can't look
|
|
2192
|
+
* like an opt-in that silently does nothing.
|
|
2193
|
+
*/
|
|
2194
|
+
parseSubtypeSelectorUseForLoadedRecords(parsed) {
|
|
2195
|
+
const value = parsed['UseForLoadedRecords'];
|
|
2196
|
+
if (value === undefined || value === null || typeof value === 'boolean') {
|
|
2197
|
+
return value === true;
|
|
2198
|
+
}
|
|
2199
|
+
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.`);
|
|
2200
|
+
return false;
|
|
2201
|
+
}
|
|
2079
2202
|
/**
|
|
2080
2203
|
* True when at least one field is a writable Geo* source (street and/or native lat/lng).
|
|
2081
2204
|
* Person/Org PrimaryAddress* are virtual display fields and do **not** count.
|
|
@@ -2083,6 +2206,62 @@ export class EntityInfo extends BaseInfo {
|
|
|
2083
2206
|
get HasWritableGeoSourceFields() {
|
|
2084
2207
|
return (this.Fields ?? []).some(f => f.IsWritableGeoField);
|
|
2085
2208
|
}
|
|
2209
|
+
/** @deprecated Use {@link SpCreate}. */
|
|
2210
|
+
get spCreate() {
|
|
2211
|
+
return this.SpCreate;
|
|
2212
|
+
}
|
|
2213
|
+
/** @deprecated Use {@link SpCreate}. */
|
|
2214
|
+
set spCreate(value) {
|
|
2215
|
+
this.SpCreate = value;
|
|
2216
|
+
}
|
|
2217
|
+
/** @deprecated Use {@link SpUpdate}. */
|
|
2218
|
+
get spUpdate() {
|
|
2219
|
+
return this.SpUpdate;
|
|
2220
|
+
}
|
|
2221
|
+
/** @deprecated Use {@link SpUpdate}. */
|
|
2222
|
+
set spUpdate(value) {
|
|
2223
|
+
this.SpUpdate = value;
|
|
2224
|
+
}
|
|
2225
|
+
/** @deprecated Use {@link SpDelete}. */
|
|
2226
|
+
get spDelete() {
|
|
2227
|
+
return this.SpDelete;
|
|
2228
|
+
}
|
|
2229
|
+
/** @deprecated Use {@link SpDelete}. */
|
|
2230
|
+
set spDelete(value) {
|
|
2231
|
+
this.SpDelete = value;
|
|
2232
|
+
}
|
|
2233
|
+
/** @deprecated Use {@link SpCreateGenerated}. */
|
|
2234
|
+
get spCreateGenerated() {
|
|
2235
|
+
return this.SpCreateGenerated;
|
|
2236
|
+
}
|
|
2237
|
+
/** @deprecated Use {@link SpCreateGenerated}. */
|
|
2238
|
+
set spCreateGenerated(value) {
|
|
2239
|
+
this.SpCreateGenerated = value;
|
|
2240
|
+
}
|
|
2241
|
+
/** @deprecated Use {@link SpUpdateGenerated}. */
|
|
2242
|
+
get spUpdateGenerated() {
|
|
2243
|
+
return this.SpUpdateGenerated;
|
|
2244
|
+
}
|
|
2245
|
+
/** @deprecated Use {@link SpUpdateGenerated}. */
|
|
2246
|
+
set spUpdateGenerated(value) {
|
|
2247
|
+
this.SpUpdateGenerated = value;
|
|
2248
|
+
}
|
|
2249
|
+
/** @deprecated Use {@link SpDeleteGenerated}. */
|
|
2250
|
+
get spDeleteGenerated() {
|
|
2251
|
+
return this.SpDeleteGenerated;
|
|
2252
|
+
}
|
|
2253
|
+
/** @deprecated Use {@link SpDeleteGenerated}. */
|
|
2254
|
+
set spDeleteGenerated(value) {
|
|
2255
|
+
this.SpDeleteGenerated = value;
|
|
2256
|
+
}
|
|
2257
|
+
/** @deprecated Use {@link SpMatch}. */
|
|
2258
|
+
get spMatch() {
|
|
2259
|
+
return this.SpMatch;
|
|
2260
|
+
}
|
|
2261
|
+
/** @deprecated Use {@link SpMatch}. */
|
|
2262
|
+
set spMatch(value) {
|
|
2263
|
+
this.SpMatch = value;
|
|
2264
|
+
}
|
|
2086
2265
|
/**
|
|
2087
2266
|
* Optional JSON configuration bag (shape = {@link IEntityConfiguration}).
|
|
2088
2267
|
* Nested `UI.Form` holds generated-form chrome: layout, auto left-nav
|
|
@@ -2117,6 +2296,66 @@ export class EntityInfo extends BaseInfo {
|
|
|
2117
2296
|
get ConfigurationObject() {
|
|
2118
2297
|
return this.Configuration;
|
|
2119
2298
|
}
|
|
2299
|
+
/**
|
|
2300
|
+
* Parsed clone configuration for this entity.
|
|
2301
|
+
* Controls clone enablement, not-cloneable status, caps, naming, field rules, etc.
|
|
2302
|
+
* @see plans/record-cloning/README.md §4.1
|
|
2303
|
+
*/
|
|
2304
|
+
get CloneConfig() {
|
|
2305
|
+
return this.ConfigurationObject?.Clone ?? null;
|
|
2306
|
+
}
|
|
2307
|
+
/**
|
|
2308
|
+
* Returns true if cloning is explicitly enabled for this entity as a ROOT record.
|
|
2309
|
+
*/
|
|
2310
|
+
get CloneEnabled() {
|
|
2311
|
+
return this.CloneConfig?.Enabled === true;
|
|
2312
|
+
}
|
|
2313
|
+
/**
|
|
2314
|
+
* Returns true if this entity is strictly not cloneable (neither as root nor child).
|
|
2315
|
+
*/
|
|
2316
|
+
get NotCloneable() {
|
|
2317
|
+
return this.CloneConfig?.NotCloneable === true;
|
|
2318
|
+
}
|
|
2319
|
+
/** @deprecated Use {@link HasIdField}. */
|
|
2320
|
+
get _hasIdField() {
|
|
2321
|
+
return this.HasIdField;
|
|
2322
|
+
}
|
|
2323
|
+
/** @deprecated Use {@link HasIdField}. */
|
|
2324
|
+
set _hasIdField(value) {
|
|
2325
|
+
this.HasIdField = value;
|
|
2326
|
+
}
|
|
2327
|
+
/** @deprecated Use {@link VirtualCount}. */
|
|
2328
|
+
get _virtualCount() {
|
|
2329
|
+
return this.VirtualCount;
|
|
2330
|
+
}
|
|
2331
|
+
/** @deprecated Use {@link VirtualCount}. */
|
|
2332
|
+
set _virtualCount(value) {
|
|
2333
|
+
this.VirtualCount = value;
|
|
2334
|
+
}
|
|
2335
|
+
/** @deprecated Use {@link ManyToManyCount}. */
|
|
2336
|
+
get _manyToManyCount() {
|
|
2337
|
+
return this.ManyToManyCount;
|
|
2338
|
+
}
|
|
2339
|
+
/** @deprecated Use {@link ManyToManyCount}. */
|
|
2340
|
+
set _manyToManyCount(value) {
|
|
2341
|
+
this.ManyToManyCount = value;
|
|
2342
|
+
}
|
|
2343
|
+
/** @deprecated Use {@link OneToManyCount}. */
|
|
2344
|
+
get _oneToManyCount() {
|
|
2345
|
+
return this.OneToManyCount;
|
|
2346
|
+
}
|
|
2347
|
+
/** @deprecated Use {@link OneToManyCount}. */
|
|
2348
|
+
set _oneToManyCount(value) {
|
|
2349
|
+
this.OneToManyCount = value;
|
|
2350
|
+
}
|
|
2351
|
+
/** @deprecated Use {@link FloatCount}. */
|
|
2352
|
+
get _floatCount() {
|
|
2353
|
+
return this.FloatCount;
|
|
2354
|
+
}
|
|
2355
|
+
/** @deprecated Use {@link FloatCount}. */
|
|
2356
|
+
set _floatCount(value) {
|
|
2357
|
+
this.FloatCount = value;
|
|
2358
|
+
}
|
|
2120
2359
|
/**
|
|
2121
2360
|
* The set of field names this user may NOT READ on this entity — the per-request primitive
|
|
2122
2361
|
* every field-security enforcement point is built on.
|
|
@@ -2243,7 +2482,7 @@ export class EntityInfo extends BaseInfo {
|
|
|
2243
2482
|
if (!this.EnableFieldLevelSecurity) {
|
|
2244
2483
|
return denied;
|
|
2245
2484
|
}
|
|
2246
|
-
for (const field of this.
|
|
2485
|
+
for (const field of this._fields) {
|
|
2247
2486
|
if (isDenied(field.GetUserFieldPermissions(user, true))) {
|
|
2248
2487
|
denied.add(field.Name.trim().toLowerCase());
|
|
2249
2488
|
denied.add(field.CodeName.trim().toLowerCase());
|
|
@@ -2267,7 +2506,7 @@ export class EntityInfo extends BaseInfo {
|
|
|
2267
2506
|
return undefined;
|
|
2268
2507
|
if (this._fieldByNameMap === null) {
|
|
2269
2508
|
const map = new Map();
|
|
2270
|
-
for (const f of this.
|
|
2509
|
+
for (const f of this._fields) {
|
|
2271
2510
|
if (f.Name != null)
|
|
2272
2511
|
map.set(f.Name.trim().toLowerCase(), f);
|
|
2273
2512
|
}
|
|
@@ -2348,7 +2587,7 @@ export class EntityInfo extends BaseInfo {
|
|
|
2348
2587
|
* @returns {EntityFieldInfo[]} Array of all entity fields
|
|
2349
2588
|
*/
|
|
2350
2589
|
get Fields() {
|
|
2351
|
-
return this.
|
|
2590
|
+
return this._fields;
|
|
2352
2591
|
}
|
|
2353
2592
|
/**
|
|
2354
2593
|
* Returns true if ANY field on this entity is `Deprecated` or `Disabled` (i.e. not `Active`).
|
|
@@ -2363,7 +2602,7 @@ export class EntityInfo extends BaseInfo {
|
|
|
2363
2602
|
*/
|
|
2364
2603
|
get HasInactiveFields() {
|
|
2365
2604
|
if (this._hasInactiveFields === undefined) {
|
|
2366
|
-
this._hasInactiveFields = this.
|
|
2605
|
+
this._hasInactiveFields = this._fields.some(f => f.Status === 'Deprecated' || f.Status === 'Disabled');
|
|
2367
2606
|
}
|
|
2368
2607
|
return this._hasInactiveFields;
|
|
2369
2608
|
}
|
|
@@ -2389,7 +2628,7 @@ export class EntityInfo extends BaseInfo {
|
|
|
2389
2628
|
*/
|
|
2390
2629
|
get HasSearchFields() {
|
|
2391
2630
|
if (this._hasSearchFields === undefined) {
|
|
2392
|
-
this._hasSearchFields = this.
|
|
2631
|
+
this._hasSearchFields = this._fields.some(f => f.IncludeInUserSearchAPI);
|
|
2393
2632
|
}
|
|
2394
2633
|
return this._hasSearchFields;
|
|
2395
2634
|
}
|
|
@@ -2398,21 +2637,21 @@ export class EntityInfo extends BaseInfo {
|
|
|
2398
2637
|
* @returns {EntityRelationshipInfo[]} Array of entity relationships
|
|
2399
2638
|
*/
|
|
2400
2639
|
get RelatedEntities() {
|
|
2401
|
-
return this.
|
|
2640
|
+
return this._relatedEntities;
|
|
2402
2641
|
}
|
|
2403
2642
|
/**
|
|
2404
2643
|
* Gets the security permissions for this entity by role.
|
|
2405
2644
|
* @returns {EntityPermissionInfo[]} Array of permission settings
|
|
2406
2645
|
*/
|
|
2407
2646
|
get Permissions() {
|
|
2408
|
-
return this.
|
|
2647
|
+
return this._permissions;
|
|
2409
2648
|
}
|
|
2410
2649
|
/**
|
|
2411
2650
|
* Gets custom configuration settings for this entity.
|
|
2412
2651
|
* @returns {EntitySettingInfo[]} Array of entity-specific settings
|
|
2413
2652
|
*/
|
|
2414
2653
|
get Settings() {
|
|
2415
|
-
return this.
|
|
2654
|
+
return this._settings;
|
|
2416
2655
|
}
|
|
2417
2656
|
/**
|
|
2418
2657
|
* Gets the parsed FieldCategoryInfo map for this entity, keyed by category name.
|
|
@@ -2420,7 +2659,7 @@ export class EntityInfo extends BaseInfo {
|
|
|
2420
2659
|
* during EntityInfo construction. Returns null if no category info is configured.
|
|
2421
2660
|
*/
|
|
2422
2661
|
get FieldCategories() {
|
|
2423
|
-
return this.
|
|
2662
|
+
return this._fieldCategories;
|
|
2424
2663
|
}
|
|
2425
2664
|
/**
|
|
2426
2665
|
* Gets the organic keys defined on this entity. Organic keys enable cross-entity
|
|
@@ -2429,7 +2668,7 @@ export class EntityInfo extends BaseInfo {
|
|
|
2429
2668
|
* @returns {EntityOrganicKeyInfo[]} Array of organic key definitions with their related entities
|
|
2430
2669
|
*/
|
|
2431
2670
|
get OrganicKeys() {
|
|
2432
|
-
return this.
|
|
2671
|
+
return this._organicKeys;
|
|
2433
2672
|
}
|
|
2434
2673
|
static { this.__createdAtFieldName = '__mj_CreatedAt'; }
|
|
2435
2674
|
static { this.__updatedAtFieldName = '__mj_UpdatedAt'; }
|
|
@@ -2486,6 +2725,38 @@ export class EntityInfo extends BaseInfo {
|
|
|
2486
2725
|
get DisplayNameOrName() {
|
|
2487
2726
|
return this.DisplayName ? this.DisplayName : this.Name;
|
|
2488
2727
|
}
|
|
2728
|
+
/**
|
|
2729
|
+
* Returns a business-user-friendly PLURAL of the entity's display name, e.g. "Contacts", "Companies",
|
|
2730
|
+
* "Addresses". Derived from `DisplayNameOrName` via `GeneratePluralName`, so a per-deployment
|
|
2731
|
+
* `DisplayName` override flows through (rename the entity's DisplayName to "Member" and this returns
|
|
2732
|
+
* "Members"). Handles irregular plurals and the common English rules (`-y` to `-ies`, `-s/ch/sh/x/z` to `-es`).
|
|
2733
|
+
*
|
|
2734
|
+
* This is the seam for surfacing the user's own domain nouns in place of the platform meta-noun
|
|
2735
|
+
* "entity" on business-user surfaces (empty states, counts, headers). It is display-only: never use it
|
|
2736
|
+
* as a lookup key. If the display name is already plural, `GeneratePluralName` returns it unchanged.
|
|
2737
|
+
*
|
|
2738
|
+
* Memoized per instance: templates read this on every change-detection pass, and the source name
|
|
2739
|
+
* is fixed per metadata load (the same assumption `_nameFieldCache` relies on). The cache is keyed
|
|
2740
|
+
* by the source name so it stays correct if `DisplayName` is ever mutated in place.
|
|
2741
|
+
*/
|
|
2742
|
+
get DisplayNamePlural() {
|
|
2743
|
+
const source = this.DisplayNameOrName;
|
|
2744
|
+
if (this._displayNamePluralCache?.source !== source) {
|
|
2745
|
+
this._displayNamePluralCache = { source, plural: EntityInfo.matchLeadingCase(source, GeneratePluralName(source)) };
|
|
2746
|
+
}
|
|
2747
|
+
return this._displayNamePluralCache.plural;
|
|
2748
|
+
}
|
|
2749
|
+
/**
|
|
2750
|
+
* Gives `plural` the same leading-letter case as `source`. The irregular-plural table stores
|
|
2751
|
+
* lowercase values ('person' → 'people'), so without this an empty state reads "No people to
|
|
2752
|
+
* display". Only the first letter is touched: `capitalizeFirstLetterOnly` would also capitalize a
|
|
2753
|
+
* name that deliberately starts lowercase ("iPhone" → "IPhones").
|
|
2754
|
+
*/
|
|
2755
|
+
static matchLeadingCase(source, plural) {
|
|
2756
|
+
const lead = source.charAt(0);
|
|
2757
|
+
const startsUpper = lead !== lead.toLowerCase();
|
|
2758
|
+
return startsUpper && plural.length > 0 ? plural.charAt(0).toUpperCase() + plural.slice(1) : plural;
|
|
2759
|
+
}
|
|
2489
2760
|
/**
|
|
2490
2761
|
* 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.
|
|
2491
2762
|
* If no fields match, if there is a field called "Name", that is returned. If there is no field called "Name", null is returned.
|
|
@@ -3089,10 +3360,10 @@ export class EntityInfo extends BaseInfo {
|
|
|
3089
3360
|
const params = {};
|
|
3090
3361
|
const matchFields = organicKey.MatchFieldNamesArray;
|
|
3091
3362
|
if (organicKeyRelatedEntity.IsTransitiveMatch) {
|
|
3092
|
-
params.ExtraFilter = EntityInfo.
|
|
3363
|
+
params.ExtraFilter = EntityInfo.buildTransitiveOrganicKeyFilter(record, organicKeyRelatedEntity, organicKey, matchFields);
|
|
3093
3364
|
}
|
|
3094
3365
|
else {
|
|
3095
|
-
params.ExtraFilter = EntityInfo.
|
|
3366
|
+
params.ExtraFilter = EntityInfo.buildDirectOrganicKeyFilter(record, organicKeyRelatedEntity, organicKey, matchFields);
|
|
3096
3367
|
}
|
|
3097
3368
|
if (filter && filter.length > 0) {
|
|
3098
3369
|
params.ExtraFilter = `(${params.ExtraFilter}) AND (${filter})`;
|
|
@@ -3112,12 +3383,12 @@ export class EntityInfo extends BaseInfo {
|
|
|
3112
3383
|
* matching organic key (same Name) to find its expression. Falls back to the hub's
|
|
3113
3384
|
* expression on both sides if the spoke doesn't carry its own.
|
|
3114
3385
|
*/
|
|
3115
|
-
static
|
|
3386
|
+
static buildDirectOrganicKeyFilter(record, relatedEntity, organicKey, matchFields) {
|
|
3116
3387
|
const relatedFields = relatedEntity.RelatedEntityFieldNamesArray;
|
|
3117
3388
|
const conditions = [];
|
|
3118
3389
|
// Resolve the spoke entity's own organic key (matching by Name) to pull its
|
|
3119
3390
|
// per-column normalization. Falls back to the hub's expression if not found.
|
|
3120
|
-
const spokeOrganicKey = EntityInfo.
|
|
3391
|
+
const spokeOrganicKey = EntityInfo.resolveSpokeOrganicKey(relatedEntity, organicKey);
|
|
3121
3392
|
for (let i = 0; i < matchFields.length; i++) {
|
|
3122
3393
|
const value = record.Get(matchFields[i]);
|
|
3123
3394
|
if (value == null) {
|
|
@@ -3126,7 +3397,7 @@ export class EntityInfo extends BaseInfo {
|
|
|
3126
3397
|
}
|
|
3127
3398
|
const relatedField = relatedFields[i] || matchFields[i];
|
|
3128
3399
|
const escapedValue = String(value).replace(/'/g, "''");
|
|
3129
|
-
conditions.push(EntityInfo.
|
|
3400
|
+
conditions.push(EntityInfo.wrapBothSidesWithNormalization(`[${relatedField}]`, spokeOrganicKey ?? organicKey, escapedValue, organicKey));
|
|
3130
3401
|
}
|
|
3131
3402
|
return conditions.join(' AND ');
|
|
3132
3403
|
}
|
|
@@ -3135,7 +3406,7 @@ export class EntityInfo extends BaseInfo {
|
|
|
3135
3406
|
* the spoke's own normalization function on the spoke side. Returns undefined if the
|
|
3136
3407
|
* spoke entity doesn't have a parallel organic key — caller falls back to the hub's.
|
|
3137
3408
|
*/
|
|
3138
|
-
static
|
|
3409
|
+
static resolveSpokeOrganicKey(relatedEntity, hubOrganicKey, provider) {
|
|
3139
3410
|
const md = provider ?? Metadata.Provider;
|
|
3140
3411
|
if (!md)
|
|
3141
3412
|
return undefined;
|
|
@@ -3148,7 +3419,7 @@ export class EntityInfo extends BaseInfo {
|
|
|
3148
3419
|
/**
|
|
3149
3420
|
* Builds an ExtraFilter for transitive organic key matching (via SQL view/table subquery).
|
|
3150
3421
|
*/
|
|
3151
|
-
static
|
|
3422
|
+
static buildTransitiveOrganicKeyFilter(record, relatedEntity, organicKey, matchFields) {
|
|
3152
3423
|
const transitiveMatchFields = relatedEntity.TransitiveObjectMatchFieldNamesArray;
|
|
3153
3424
|
const conditions = [];
|
|
3154
3425
|
// Transitive bridge values are normalized using the HUB's expression on both sides —
|
|
@@ -3161,7 +3432,7 @@ export class EntityInfo extends BaseInfo {
|
|
|
3161
3432
|
}
|
|
3162
3433
|
const transitiveField = transitiveMatchFields[i] || matchFields[i];
|
|
3163
3434
|
const escapedValue = String(value).replace(/'/g, "''");
|
|
3164
|
-
conditions.push(EntityInfo.
|
|
3435
|
+
conditions.push(EntityInfo.wrapWithNormalization(`[${transitiveField}]`, organicKey, escapedValue));
|
|
3165
3436
|
}
|
|
3166
3437
|
// TransitiveObjectName is schema-qualified (e.g. "mailchimp.vwBridge") — bracket each part separately
|
|
3167
3438
|
const transitiveObject = relatedEntity.TransitiveObjectName.split('.').map(p => `[${p}]`).join('.');
|
|
@@ -3172,7 +3443,7 @@ export class EntityInfo extends BaseInfo {
|
|
|
3172
3443
|
* based on the organic key's NormalizationStrategy.
|
|
3173
3444
|
* Returns a SQL comparison expression like: LOWER(LTRIM(RTRIM([Field]))) = LOWER(LTRIM(RTRIM('value')))
|
|
3174
3445
|
*/
|
|
3175
|
-
static
|
|
3446
|
+
static wrapWithNormalization(fieldExpression, organicKey, escapedValue) {
|
|
3176
3447
|
switch (organicKey.NormalizationStrategy) {
|
|
3177
3448
|
case 'LowerCaseTrim':
|
|
3178
3449
|
return `LOWER(LTRIM(RTRIM(${fieldExpression}))) = LOWER(LTRIM(RTRIM('${escapedValue}')))`;
|
|
@@ -3204,13 +3475,13 @@ export class EntityInfo extends BaseInfo {
|
|
|
3204
3475
|
* when the expressions agree; different transforms applied independently when they
|
|
3205
3476
|
* don't (the per-column normalization case).
|
|
3206
3477
|
*/
|
|
3207
|
-
static
|
|
3208
|
-
const leftSide = EntityInfo.
|
|
3209
|
-
const rightSide = EntityInfo.
|
|
3478
|
+
static wrapBothSidesWithNormalization(fieldExpression, fieldOrganicKey, escapedValue, valueOrganicKey) {
|
|
3479
|
+
const leftSide = EntityInfo.normalizeFieldExpression(fieldExpression, fieldOrganicKey);
|
|
3480
|
+
const rightSide = EntityInfo.normalizeLiteralExpression(escapedValue, valueOrganicKey);
|
|
3210
3481
|
return `${leftSide} = ${rightSide}`;
|
|
3211
3482
|
}
|
|
3212
3483
|
/** Apply an organic key's normalization to a SQL field expression (left side of compare). */
|
|
3213
|
-
static
|
|
3484
|
+
static normalizeFieldExpression(fieldExpression, organicKey) {
|
|
3214
3485
|
switch (organicKey.NormalizationStrategy) {
|
|
3215
3486
|
case 'LowerCaseTrim': return `LOWER(LTRIM(RTRIM(${fieldExpression})))`;
|
|
3216
3487
|
case 'Trim': return `LTRIM(RTRIM(${fieldExpression}))`;
|
|
@@ -3226,7 +3497,7 @@ export class EntityInfo extends BaseInfo {
|
|
|
3226
3497
|
}
|
|
3227
3498
|
}
|
|
3228
3499
|
/** Apply an organic key's normalization to a quoted literal value (right side of compare). */
|
|
3229
|
-
static
|
|
3500
|
+
static normalizeLiteralExpression(escapedValue, organicKey) {
|
|
3230
3501
|
switch (organicKey.NormalizationStrategy) {
|
|
3231
3502
|
case 'LowerCaseTrim': return `LOWER(LTRIM(RTRIM('${escapedValue}')))`;
|
|
3232
3503
|
case 'Trim': return `LTRIM(RTRIM('${escapedValue}'))`;
|
|
@@ -3522,27 +3793,27 @@ export class EntityInfo extends BaseInfo {
|
|
|
3522
3793
|
/**
|
|
3523
3794
|
* Name of the stored procedure for creating records
|
|
3524
3795
|
*/
|
|
3525
|
-
this.
|
|
3796
|
+
this.SpCreate = null;
|
|
3526
3797
|
/**
|
|
3527
3798
|
* Name of the stored procedure for updating records
|
|
3528
3799
|
*/
|
|
3529
|
-
this.
|
|
3800
|
+
this.SpUpdate = null;
|
|
3530
3801
|
/**
|
|
3531
3802
|
* Name of the stored procedure for deleting records
|
|
3532
3803
|
*/
|
|
3533
|
-
this.
|
|
3804
|
+
this.SpDelete = null;
|
|
3534
3805
|
/**
|
|
3535
3806
|
* Whether the create stored procedure is generated by CodeGen
|
|
3536
3807
|
*/
|
|
3537
|
-
this.
|
|
3808
|
+
this.SpCreateGenerated = null;
|
|
3538
3809
|
/**
|
|
3539
3810
|
* Whether the update stored procedure is generated by CodeGen
|
|
3540
3811
|
*/
|
|
3541
|
-
this.
|
|
3812
|
+
this.SpUpdateGenerated = null;
|
|
3542
3813
|
/**
|
|
3543
3814
|
* Whether the delete stored procedure is generated by CodeGen
|
|
3544
3815
|
*/
|
|
3545
|
-
this.
|
|
3816
|
+
this.SpDeleteGenerated = null;
|
|
3546
3817
|
/**
|
|
3547
3818
|
* Whether to automatically delete related records when a parent is deleted
|
|
3548
3819
|
*/
|
|
@@ -3558,7 +3829,7 @@ export class EntityInfo extends BaseInfo {
|
|
|
3558
3829
|
/**
|
|
3559
3830
|
* Name of the stored procedure used for matching/duplicate detection
|
|
3560
3831
|
*/
|
|
3561
|
-
this.
|
|
3832
|
+
this.SpMatch = null;
|
|
3562
3833
|
/**
|
|
3563
3834
|
* Default display type for relationships: Search (type-ahead) or Dropdown
|
|
3564
3835
|
*/
|
|
@@ -3683,13 +3954,13 @@ export class EntityInfo extends BaseInfo {
|
|
|
3683
3954
|
this.ParentEntity = null;
|
|
3684
3955
|
this.ParentBaseTable = null;
|
|
3685
3956
|
this.ParentBaseView = null;
|
|
3686
|
-
this.
|
|
3687
|
-
this.
|
|
3688
|
-
this.
|
|
3689
|
-
this.
|
|
3690
|
-
this.
|
|
3691
|
-
this.
|
|
3692
|
-
this.
|
|
3957
|
+
this._fieldCategories = null;
|
|
3958
|
+
this._organicKeys = [];
|
|
3959
|
+
this.HasIdField = false;
|
|
3960
|
+
this.VirtualCount = 0;
|
|
3961
|
+
this.ManyToManyCount = 0;
|
|
3962
|
+
this.OneToManyCount = 0;
|
|
3963
|
+
this.FloatCount = 0;
|
|
3693
3964
|
// --- Lazy caches for immutable field-derived collections ---------------------------------
|
|
3694
3965
|
// `_Fields` is populated once in the constructor and never reassigned, so these caches never
|
|
3695
3966
|
// need invalidation. They replace per-access `.filter()`/`.find()` scans on hot paths
|
|
@@ -3702,6 +3973,8 @@ export class EntityInfo extends BaseInfo {
|
|
|
3702
3973
|
this._encryptedFieldsCache = null;
|
|
3703
3974
|
this._datetimeFieldsCache = null;
|
|
3704
3975
|
this._nameFieldCache = undefined;
|
|
3976
|
+
/** Memoized computed plural, keyed by the display name it was derived from (see `DisplayNamePlural`). */
|
|
3977
|
+
this._displayNamePluralCache = undefined;
|
|
3705
3978
|
this._hasInactiveFields = undefined;
|
|
3706
3979
|
this._hasSearchFields = undefined;
|
|
3707
3980
|
// Cache for ParentChain to avoid repeated walks
|
|
@@ -3712,7 +3985,7 @@ export class EntityInfo extends BaseInfo {
|
|
|
3712
3985
|
this.copyInitData(initData);
|
|
3713
3986
|
// do some special handling to create class instances instead of just data objects
|
|
3714
3987
|
// copy the Entity Fields (accept EntityFields, _Fields, or Fields as input names)
|
|
3715
|
-
this.
|
|
3988
|
+
this._fields = [];
|
|
3716
3989
|
// Reset every lazy field-derived memo cache whenever _Fields is (re)assigned.
|
|
3717
3990
|
// `entityInfo.cacheReset.test.ts` mirrors this list in `runProductionCacheReset`;
|
|
3718
3991
|
// a cache added here needs adding there too, or its reset goes unexercised.
|
|
@@ -3739,27 +4012,27 @@ export class EntityInfo extends BaseInfo {
|
|
|
3739
4012
|
const ef = initData.EntityFields || initData._Fields || initData.Fields;
|
|
3740
4013
|
if (ef) {
|
|
3741
4014
|
for (let j = 0; j < ef.length; j++) {
|
|
3742
|
-
this.
|
|
4015
|
+
this._fields.push(new EntityFieldInfo(ef[j]));
|
|
3743
4016
|
}
|
|
3744
4017
|
}
|
|
3745
4018
|
// copy the Entity Permissions
|
|
3746
|
-
this.
|
|
4019
|
+
this._permissions = [];
|
|
3747
4020
|
const ep = initData.EntityPermissions || initData._Permissions || initData.Permissions;
|
|
3748
4021
|
if (ep) {
|
|
3749
4022
|
for (let j = 0; j < ep.length; j++) {
|
|
3750
|
-
this.
|
|
4023
|
+
this._permissions.push(new EntityPermissionInfo(ep[j]));
|
|
3751
4024
|
}
|
|
3752
4025
|
}
|
|
3753
4026
|
// copy the Entity settings
|
|
3754
|
-
this.
|
|
4027
|
+
this._settings = [];
|
|
3755
4028
|
const es = initData.EntitySettings || initData._Settings || initData.Settings;
|
|
3756
4029
|
if (es) {
|
|
3757
|
-
es.map((s) => this.
|
|
4030
|
+
es.map((s) => this._settings.push(new EntitySettingInfo(s)));
|
|
3758
4031
|
}
|
|
3759
4032
|
// auto-populate FieldCategories from the FieldCategoryInfo setting
|
|
3760
|
-
this.
|
|
4033
|
+
this._fieldCategories = this.parseFieldCategoriesFromSettings();
|
|
3761
4034
|
// copy the Related Entities (accept EntityRelationships, _RelatedEntities, or RelatedEntities as input names)
|
|
3762
|
-
this.
|
|
4035
|
+
this._relatedEntities = [];
|
|
3763
4036
|
const er = initData.EntityRelationships || initData._RelatedEntities || initData.RelatedEntities;
|
|
3764
4037
|
if (er) {
|
|
3765
4038
|
// check to see if ANY of the records in the er array have a non-null or non-zero sequence value. The reason is
|
|
@@ -3782,15 +4055,15 @@ export class EntityInfo extends BaseInfo {
|
|
|
3782
4055
|
}
|
|
3783
4056
|
// now that we have prepared the er array by sorting it, if needed, let's load up the related entities
|
|
3784
4057
|
for (let j = 0; j < er.length; j++) {
|
|
3785
|
-
this.
|
|
4058
|
+
this._relatedEntities.push(new EntityRelationshipInfo(er[j]));
|
|
3786
4059
|
}
|
|
3787
4060
|
}
|
|
3788
4061
|
// copy the Organic Keys (sorted by sequence inside EntityOrganicKeyInfo constructor)
|
|
3789
|
-
this.
|
|
4062
|
+
this._organicKeys = [];
|
|
3790
4063
|
const ok = initData.EntityOrganicKeys || initData._OrganicKeys || initData.OrganicKeys;
|
|
3791
4064
|
if (ok && Array.isArray(ok)) {
|
|
3792
4065
|
for (const item of ok) {
|
|
3793
|
-
this.
|
|
4066
|
+
this._organicKeys.push(new EntityOrganicKeyInfo(item));
|
|
3794
4067
|
}
|
|
3795
4068
|
}
|
|
3796
4069
|
this.prepareSpecialFields();
|
|
@@ -3810,9 +4083,9 @@ export class EntityInfo extends BaseInfo {
|
|
|
3810
4083
|
virtualCount += f.IsVirtual ? 1 : 0;
|
|
3811
4084
|
floatCount += f.IsFloat ? 1 : 0;
|
|
3812
4085
|
}
|
|
3813
|
-
this.
|
|
3814
|
-
this.
|
|
3815
|
-
this.
|
|
4086
|
+
this.HasIdField = hasIdField;
|
|
4087
|
+
this.FloatCount = floatCount;
|
|
4088
|
+
this.VirtualCount = virtualCount;
|
|
3816
4089
|
// now see if there are any relationships and count the one to many and many to many
|
|
3817
4090
|
for (let j = 0; j < this.RelatedEntities.length; ++j) {
|
|
3818
4091
|
const r = this.RelatedEntities[j];
|
|
@@ -3821,8 +4094,8 @@ export class EntityInfo extends BaseInfo {
|
|
|
3821
4094
|
else
|
|
3822
4095
|
manyToManyCount++;
|
|
3823
4096
|
}
|
|
3824
|
-
this.
|
|
3825
|
-
this.
|
|
4097
|
+
this.ManyToManyCount = manyToManyCount;
|
|
4098
|
+
this.OneToManyCount = oneToManyCount;
|
|
3826
4099
|
}
|
|
3827
4100
|
catch (e) {
|
|
3828
4101
|
LogError(e);
|
|
@@ -3833,11 +4106,11 @@ export class EntityInfo extends BaseInfo {
|
|
|
3833
4106
|
* Called once during construction so the result is cached on _FieldCategories.
|
|
3834
4107
|
*/
|
|
3835
4108
|
parseFieldCategoriesFromSettings() {
|
|
3836
|
-
if (!this.
|
|
4109
|
+
if (!this._settings || this._settings.length === 0) {
|
|
3837
4110
|
return null;
|
|
3838
4111
|
}
|
|
3839
4112
|
// Try new format first
|
|
3840
|
-
const infoSetting = this.
|
|
4113
|
+
const infoSetting = this._settings.find(s => s.Name === 'FieldCategoryInfo');
|
|
3841
4114
|
if (infoSetting?.Value) {
|
|
3842
4115
|
const parsed = SafeJSONParse(infoSetting.Value, false);
|
|
3843
4116
|
if (parsed) {
|
|
@@ -3845,7 +4118,7 @@ export class EntityInfo extends BaseInfo {
|
|
|
3845
4118
|
}
|
|
3846
4119
|
}
|
|
3847
4120
|
// Fallback to legacy FieldCategoryIcons format (icon-only map)
|
|
3848
|
-
const iconSetting = this.
|
|
4121
|
+
const iconSetting = this._settings.find(s => s.Name === 'FieldCategoryIcons');
|
|
3849
4122
|
if (iconSetting?.Value) {
|
|
3850
4123
|
const icons = SafeJSONParse(iconSetting.Value, false);
|
|
3851
4124
|
if (icons) {
|