@memberjunction/core 6.2.0-edge.3 → 6.2.0-edge.5
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/BaseEntitySaveQueue.d.ts +7 -0
- package/dist/generic/BaseEntitySaveQueue.d.ts.map +1 -1
- package/dist/generic/BaseEntitySaveQueue.js +22 -4
- package/dist/generic/BaseEntitySaveQueue.js.map +1 -1
- package/dist/generic/JSONType-interfaces/IEntityConfiguration.d.ts +33 -0
- package/dist/generic/JSONType-interfaces/IEntityConfiguration.d.ts.map +1 -1
- package/dist/generic/RegisterForStartup.d.ts +44 -0
- package/dist/generic/RegisterForStartup.d.ts.map +1 -1
- package/dist/generic/RegisterForStartup.js +124 -48
- package/dist/generic/RegisterForStartup.js.map +1 -1
- package/dist/generic/baseEngine.d.ts +222 -7
- package/dist/generic/baseEngine.d.ts.map +1 -1
- package/dist/generic/baseEngine.js +555 -37
- package/dist/generic/baseEngine.js.map +1 -1
- package/dist/generic/baseEngineSweeper.d.ts +35 -0
- package/dist/generic/baseEngineSweeper.d.ts.map +1 -0
- package/dist/generic/baseEngineSweeper.js +97 -0
- package/dist/generic/baseEngineSweeper.js.map +1 -0
- package/dist/generic/baseEntity.d.ts +45 -1
- package/dist/generic/baseEntity.d.ts.map +1 -1
- package/dist/generic/baseEntity.js +308 -187
- package/dist/generic/baseEntity.js.map +1 -1
- package/dist/generic/baseInfo.d.ts +31 -3
- package/dist/generic/baseInfo.d.ts.map +1 -1
- package/dist/generic/baseInfo.js +138 -40
- package/dist/generic/baseInfo.js.map +1 -1
- package/dist/generic/databaseProviderBase.d.ts +57 -19
- package/dist/generic/databaseProviderBase.d.ts.map +1 -1
- package/dist/generic/databaseProviderBase.js +203 -80
- package/dist/generic/databaseProviderBase.js.map +1 -1
- package/dist/generic/entityConfiguration.d.ts +8 -2
- package/dist/generic/entityConfiguration.d.ts.map +1 -1
- package/dist/generic/entityConfiguration.js +9 -0
- package/dist/generic/entityConfiguration.js.map +1 -1
- package/dist/generic/entityEventBatch.d.ts +124 -0
- package/dist/generic/entityEventBatch.d.ts.map +1 -0
- package/dist/generic/entityEventBatch.js +194 -0
- package/dist/generic/entityEventBatch.js.map +1 -0
- package/dist/generic/entityInfo.d.ts +103 -8
- package/dist/generic/entityInfo.d.ts.map +1 -1
- package/dist/generic/entityInfo.js +376 -60
- package/dist/generic/entityInfo.js.map +1 -1
- package/dist/generic/infoProviderResolution.d.ts +16 -0
- package/dist/generic/infoProviderResolution.d.ts.map +1 -0
- package/dist/generic/infoProviderResolution.js +17 -0
- package/dist/generic/infoProviderResolution.js.map +1 -0
- package/dist/generic/interfaces.d.ts +143 -3
- package/dist/generic/interfaces.d.ts.map +1 -1
- package/dist/generic/interfaces.js.map +1 -1
- package/dist/generic/localCacheManager.d.ts +505 -56
- package/dist/generic/localCacheManager.d.ts.map +1 -1
- package/dist/generic/localCacheManager.js +1185 -175
- package/dist/generic/localCacheManager.js.map +1 -1
- package/dist/generic/logging.d.ts +15 -0
- package/dist/generic/logging.d.ts.map +1 -1
- package/dist/generic/logging.js +17 -0
- package/dist/generic/logging.js.map +1 -1
- package/dist/generic/metadata.d.ts +1 -1
- package/dist/generic/metadata.d.ts.map +1 -1
- package/dist/generic/metadata.js.map +1 -1
- package/dist/generic/mjapiUrl.d.ts +21 -0
- package/dist/generic/mjapiUrl.d.ts.map +1 -0
- package/dist/generic/mjapiUrl.js +25 -0
- package/dist/generic/mjapiUrl.js.map +1 -0
- package/dist/generic/providerBase.d.ts +301 -5
- package/dist/generic/providerBase.d.ts.map +1 -1
- package/dist/generic/providerBase.js +750 -168
- package/dist/generic/providerBase.js.map +1 -1
- package/dist/generic/queryInfo.d.ts +10 -9
- package/dist/generic/queryInfo.d.ts.map +1 -1
- package/dist/generic/queryInfo.js +60 -33
- package/dist/generic/queryInfo.js.map +1 -1
- package/dist/generic/scoring/ReciprocalRankFusion.d.ts.map +1 -1
- package/dist/generic/scoring/ReciprocalRankFusion.js +10 -3
- package/dist/generic/scoring/ReciprocalRankFusion.js.map +1 -1
- package/dist/generic/securityInfo.d.ts +11 -6
- package/dist/generic/securityInfo.d.ts.map +1 -1
- package/dist/generic/securityInfo.js +15 -10
- package/dist/generic/securityInfo.js.map +1 -1
- package/dist/generic/telemetryManager.d.ts.map +1 -1
- package/dist/generic/telemetryManager.js +4 -1
- package/dist/generic/telemetryManager.js.map +1 -1
- package/dist/generic/transactionGroup.d.ts +27 -0
- package/dist/generic/transactionGroup.d.ts.map +1 -1
- package/dist/generic/transactionGroup.js +58 -5
- package/dist/generic/transactionGroup.js.map +1 -1
- package/dist/generic/util.d.ts.map +1 -1
- package/dist/generic/util.js +3 -0
- package/dist/generic/util.js.map +1 -1
- package/dist/generic/wellKnownUserSource.d.ts +3 -2
- package/dist/generic/wellKnownUserSource.d.ts.map +1 -1
- package/dist/generic/wellKnownUserSource.js +18 -9
- package/dist/generic/wellKnownUserSource.js.map +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/package.json +3 -3
- package/readme.md +29 -4
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { BaseInfo } from "./baseInfo.js";
|
|
2
2
|
import { Metadata } from "./metadata.js";
|
|
3
|
+
import { MetadataProviderFor } from "./infoProviderResolution.js";
|
|
3
4
|
import { TypeScriptTypeFromSQLType, SQLFullType, SQLMaxLength, SQLMaxByteLength, FormatValue, CodeNameFromString } from "./util.js";
|
|
4
5
|
import { IsFixedWidthStringSQLType } from "@memberjunction/sql-dialect";
|
|
5
6
|
import { LogError } from "./logging.js";
|
|
@@ -369,9 +370,13 @@ export class EntityPermissionInfo extends BaseInfo {
|
|
|
369
370
|
break;
|
|
370
371
|
}
|
|
371
372
|
if (fID && fID.length > 0)
|
|
372
|
-
return
|
|
373
|
+
return MetadataProviderFor(this)?.RowLevelSecurityFilters?.find(f => UUIDsEqual(f.ID, fID));
|
|
373
374
|
}
|
|
374
|
-
|
|
375
|
+
/**
|
|
376
|
+
* @param initData - the raw permission row
|
|
377
|
+
* @param md - the provider that owns this metadata; RLS filters resolve through it (see {@link BaseInfo.OwningProvider})
|
|
378
|
+
*/
|
|
379
|
+
constructor(initData, md) {
|
|
375
380
|
super();
|
|
376
381
|
this.ID = null;
|
|
377
382
|
this.EntityID = null;
|
|
@@ -401,6 +406,7 @@ export class EntityPermissionInfo extends BaseInfo {
|
|
|
401
406
|
this.CreateRLSFilter = null;
|
|
402
407
|
this.UpdateRLSFilter = null;
|
|
403
408
|
this.DeleteRLSFilter = null;
|
|
409
|
+
this.SetOwningProvider(md);
|
|
404
410
|
this.copyInitData(initData);
|
|
405
411
|
}
|
|
406
412
|
}
|
|
@@ -927,14 +933,16 @@ export class EntityFieldInfo extends BaseInfo {
|
|
|
927
933
|
static AggregateFieldRulesForUser(rules, user) {
|
|
928
934
|
const allow = { CanRead: false, CanUpdate: false, CanCreate: false };
|
|
929
935
|
const deny = { CanRead: false, CanUpdate: false, CanCreate: false };
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
936
|
+
const userRoleIds = getUserRoleIdSet(user);
|
|
937
|
+
if (userRoleIds.size > 0) {
|
|
938
|
+
for (const fp of rules) {
|
|
939
|
+
if (!fp.RoleID || !userRoleIds.has(fp.RoleID.trim().toLowerCase())) {
|
|
940
|
+
continue; // user does not hold this role
|
|
941
|
+
}
|
|
942
|
+
EntityFieldInfo.applyAccessToBuckets(fp.ReadAccess, allow, deny, 'CanRead');
|
|
943
|
+
EntityFieldInfo.applyAccessToBuckets(fp.UpdateAccess, allow, deny, 'CanUpdate');
|
|
944
|
+
EntityFieldInfo.applyAccessToBuckets(fp.CreateAccess, allow, deny, 'CanCreate');
|
|
934
945
|
}
|
|
935
|
-
EntityFieldInfo.applyAccessToBuckets(fp.ReadAccess, allow, deny, 'CanRead');
|
|
936
|
-
EntityFieldInfo.applyAccessToBuckets(fp.UpdateAccess, allow, deny, 'CanUpdate');
|
|
937
|
-
EntityFieldInfo.applyAccessToBuckets(fp.CreateAccess, allow, deny, 'CanCreate');
|
|
938
946
|
}
|
|
939
947
|
const effective = {
|
|
940
948
|
CanRead: allow.CanRead && !deny.CanRead,
|
|
@@ -971,17 +979,23 @@ export class EntityFieldInfo extends BaseInfo {
|
|
|
971
979
|
* Returns the ValueListType using the EntityFieldValueListType enum.
|
|
972
980
|
*/
|
|
973
981
|
get ValueListTypeEnum() {
|
|
974
|
-
if (this.
|
|
975
|
-
return
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
982
|
+
if (this._valueListTypeEnumCache !== undefined) {
|
|
983
|
+
return this._valueListTypeEnumCache;
|
|
984
|
+
}
|
|
985
|
+
if (this.ValueListType == null) {
|
|
986
|
+
this._valueListTypeEnumCache = EntityFieldValueListType.None;
|
|
987
|
+
return this._valueListTypeEnumCache;
|
|
988
|
+
}
|
|
989
|
+
const target = this.ValueListType.toLowerCase().trim();
|
|
990
|
+
for (const enumMember in EntityFieldValueListType) {
|
|
991
|
+
if (typeof EntityFieldValueListType[enumMember] === 'string' &&
|
|
992
|
+
enumMember.toLowerCase().trim() === target) {
|
|
993
|
+
this._valueListTypeEnumCache = EntityFieldValueListType[enumMember];
|
|
994
|
+
return this._valueListTypeEnumCache;
|
|
983
995
|
}
|
|
984
996
|
}
|
|
997
|
+
this._valueListTypeEnumCache = EntityFieldValueListType.None;
|
|
998
|
+
return this._valueListTypeEnumCache;
|
|
985
999
|
}
|
|
986
1000
|
/**
|
|
987
1001
|
* Upper bound on how many legal values a validation message enumerates before it truncates.
|
|
@@ -1458,19 +1472,28 @@ export class EntityFieldInfo extends BaseInfo {
|
|
|
1458
1472
|
* Returns true if the field is the CreatedAt field, a special field that is used to track the creation date of a record. This is only used when the entity has TrackRecordChanges=1
|
|
1459
1473
|
*/
|
|
1460
1474
|
get IsCreatedAtField() {
|
|
1461
|
-
|
|
1475
|
+
if (this._isCreatedAtFieldCache !== undefined)
|
|
1476
|
+
return this._isCreatedAtFieldCache;
|
|
1477
|
+
this._isCreatedAtFieldCache = this.Name.trim().toLowerCase() === EntityInfo.CreatedAtFieldName.trim().toLowerCase();
|
|
1478
|
+
return this._isCreatedAtFieldCache;
|
|
1462
1479
|
}
|
|
1463
1480
|
/**
|
|
1464
1481
|
* Returns true if the field is the UpdatedAt field, a special field that is used to track the last update date of a record. This is only used when the entity has TrackRecordChanges=1
|
|
1465
1482
|
*/
|
|
1466
1483
|
get IsUpdatedAtField() {
|
|
1467
|
-
|
|
1484
|
+
if (this._isUpdatedAtFieldCache !== undefined)
|
|
1485
|
+
return this._isUpdatedAtFieldCache;
|
|
1486
|
+
this._isUpdatedAtFieldCache = this.Name.trim().toLowerCase() === EntityInfo.UpdatedAtFieldName.trim().toLowerCase();
|
|
1487
|
+
return this._isUpdatedAtFieldCache;
|
|
1468
1488
|
}
|
|
1469
1489
|
/**
|
|
1470
1490
|
* Returns true if the field is the DeletedAt field, a special field that is used to track the deletion date of a record. This is only used when the entity has DeleteType=Soft
|
|
1471
1491
|
*/
|
|
1472
1492
|
get IsDeletedAtField() {
|
|
1473
|
-
|
|
1493
|
+
if (this._isDeletedAtFieldCache !== undefined)
|
|
1494
|
+
return this._isDeletedAtFieldCache;
|
|
1495
|
+
this._isDeletedAtFieldCache = this.Name.trim().toLowerCase() === EntityInfo.DeletedAtFieldName.trim().toLowerCase();
|
|
1496
|
+
return this._isDeletedAtFieldCache;
|
|
1474
1497
|
}
|
|
1475
1498
|
/**
|
|
1476
1499
|
* Returns true if the field is a GUID/UUID column in the database.
|
|
@@ -1479,8 +1502,11 @@ export class EntityFieldInfo extends BaseInfo {
|
|
|
1479
1502
|
* `uuid`, so a `uniqueidentifier`-only check would miss every UUID column.
|
|
1480
1503
|
*/
|
|
1481
1504
|
get IsUniqueIdentifier() {
|
|
1505
|
+
if (this._isUniqueIdentifierCache !== undefined)
|
|
1506
|
+
return this._isUniqueIdentifierCache;
|
|
1482
1507
|
const t = this.Type.trim().toLowerCase();
|
|
1483
|
-
|
|
1508
|
+
this._isUniqueIdentifierCache = t === 'uniqueidentifier' || t === 'uuid';
|
|
1509
|
+
return this._isUniqueIdentifierCache;
|
|
1484
1510
|
}
|
|
1485
1511
|
/**
|
|
1486
1512
|
* Returns true if the field's SQL type is **fixed-width / space-padded**
|
|
@@ -1493,7 +1519,10 @@ export class EntityFieldInfo extends BaseInfo {
|
|
|
1493
1519
|
* (un-padded) form of the value.
|
|
1494
1520
|
*/
|
|
1495
1521
|
get FixedWidthColumn() {
|
|
1496
|
-
|
|
1522
|
+
if (this._fixedWidthColumnCache !== undefined)
|
|
1523
|
+
return this._fixedWidthColumnCache;
|
|
1524
|
+
this._fixedWidthColumnCache = IsFixedWidthStringSQLType(this.Type);
|
|
1525
|
+
return this._fixedWidthColumnCache;
|
|
1497
1526
|
}
|
|
1498
1527
|
/**
|
|
1499
1528
|
* Returns true if the field has a default value set
|
|
@@ -1623,6 +1652,10 @@ export class EntityFieldInfo extends BaseInfo {
|
|
|
1623
1652
|
FormatValue(value, decimals = 2, currency = 'USD', maxLength = 0, trailingChars = "...") {
|
|
1624
1653
|
return FormatValue(this.Type, value, decimals, currency, maxLength, trailingChars);
|
|
1625
1654
|
}
|
|
1655
|
+
static { this.CHILD_INIT_KEYS = new Set([
|
|
1656
|
+
'EntityFieldValues', '_EntityFieldValues',
|
|
1657
|
+
'EntityFieldPermissions', '_FieldPermissions', 'FieldPermissions',
|
|
1658
|
+
]); }
|
|
1626
1659
|
constructor(initData = null) {
|
|
1627
1660
|
super();
|
|
1628
1661
|
this.ID = null;
|
|
@@ -2007,6 +2040,7 @@ export class EntityFieldInfo extends BaseInfo {
|
|
|
2007
2040
|
*/
|
|
2008
2041
|
this._relatedEntityJoinFieldsFailedParsing = false;
|
|
2009
2042
|
this._fieldPermissions = [];
|
|
2043
|
+
this._valueListTypeEnumCache = undefined;
|
|
2010
2044
|
/** Memoized {@link TSType} — `Type` is immutable after metadata load, so the classification never changes. */
|
|
2011
2045
|
this._tsType = undefined;
|
|
2012
2046
|
/**
|
|
@@ -2014,8 +2048,14 @@ export class EntityFieldInfo extends BaseInfo {
|
|
|
2014
2048
|
* This property returns a consistent CodeName you can use everywhere to refer to the field when generated variable names
|
|
2015
2049
|
*/
|
|
2016
2050
|
this._codeName = null;
|
|
2051
|
+
this._isCreatedAtFieldCache = undefined;
|
|
2052
|
+
this._isUpdatedAtFieldCache = undefined;
|
|
2053
|
+
this._isDeletedAtFieldCache = undefined;
|
|
2054
|
+
this._isUniqueIdentifierCache = undefined;
|
|
2055
|
+
this._fixedWidthColumnCache = undefined;
|
|
2056
|
+
this._parsedDefaultValueCache = undefined;
|
|
2017
2057
|
if (initData) {
|
|
2018
|
-
this.copyInitData(initData);
|
|
2058
|
+
this.copyInitData(initData, EntityFieldInfo.CHILD_INIT_KEYS);
|
|
2019
2059
|
// Lazy-construct EntityFieldValueInfo objects — stash raw data now,
|
|
2020
2060
|
// construct typed instances on first access via the getter.
|
|
2021
2061
|
// This eliminates ~36,000 object constructions during startup deserialization.
|
|
@@ -2096,7 +2136,99 @@ export class EntityFieldInfo extends BaseInfo {
|
|
|
2096
2136
|
// Using includes() because the value might be wrapped in parentheses like (getdate())
|
|
2097
2137
|
return EntityFieldInfo.SQL_CURRENT_DATE_FUNCTIONS.some(func => normalizedValue.includes(func));
|
|
2098
2138
|
}
|
|
2139
|
+
static { this.PG_TYPED_LITERAL_REGEX = /^'((?:[^']|'')*)'::[A-Za-z][\w "()\[\],]*$/; }
|
|
2140
|
+
/**
|
|
2141
|
+
* Parses a field's SQL DefaultValue into its runtime JS representation.
|
|
2142
|
+
* Exposed as a static helper so unit tests passing plain-object mocks for EntityFieldInfo
|
|
2143
|
+
* work identically to real EntityFieldInfo instances.
|
|
2144
|
+
*/
|
|
2145
|
+
static ParseDefaultValue(field) {
|
|
2146
|
+
const rawDefault = field.DefaultValue;
|
|
2147
|
+
if (!rawDefault) {
|
|
2148
|
+
return { source: rawDefault, hasValue: true, value: null, isCurrentDateFunction: false };
|
|
2149
|
+
}
|
|
2150
|
+
if (field.TSType === EntityFieldTSType.Boolean) {
|
|
2151
|
+
const val = typeof rawDefault === 'string' &&
|
|
2152
|
+
(rawDefault.trim() === '1' || rawDefault.trim().toLowerCase() === 'true');
|
|
2153
|
+
return { source: rawDefault, hasValue: true, value: val, isCurrentDateFunction: false };
|
|
2154
|
+
}
|
|
2155
|
+
if (field.TSType === EntityFieldTSType.Number) {
|
|
2156
|
+
const num = Number(rawDefault);
|
|
2157
|
+
if (!isNaN(num)) {
|
|
2158
|
+
return { source: rawDefault, hasValue: true, value: num, isCurrentDateFunction: false };
|
|
2159
|
+
}
|
|
2160
|
+
if (typeof rawDefault === 'string' && rawDefault.trim().toLowerCase() === 'null') {
|
|
2161
|
+
return { source: rawDefault, hasValue: true, value: null, isCurrentDateFunction: false };
|
|
2162
|
+
}
|
|
2163
|
+
return { source: rawDefault, hasValue: false, value: undefined, isCurrentDateFunction: false };
|
|
2164
|
+
}
|
|
2165
|
+
const isUniqueId = field.IsUniqueIdentifier !== undefined
|
|
2166
|
+
? field.IsUniqueIdentifier
|
|
2167
|
+
: (typeof field.Type === 'string' && (field.Type.trim().toLowerCase() === 'uniqueidentifier' || field.Type.trim().toLowerCase() === 'uuid'));
|
|
2168
|
+
if (isUniqueId) {
|
|
2169
|
+
return { source: rawDefault, hasValue: true, value: null, isCurrentDateFunction: false };
|
|
2170
|
+
}
|
|
2171
|
+
if (field.TSType === EntityFieldTSType.Date) {
|
|
2172
|
+
if (typeof rawDefault === 'string' && rawDefault.trim().length > 0) {
|
|
2173
|
+
const temp = new Date(rawDefault);
|
|
2174
|
+
if (!isNaN(temp.getTime())) {
|
|
2175
|
+
return { source: rawDefault, hasValue: true, value: temp, isCurrentDateFunction: false };
|
|
2176
|
+
}
|
|
2177
|
+
if (EntityFieldInfo.IsDefaultValueSQLCurrentDateFunction(rawDefault)) {
|
|
2178
|
+
if (field.IsSpecialDateField) {
|
|
2179
|
+
return { source: rawDefault, hasValue: true, value: null, isCurrentDateFunction: false };
|
|
2180
|
+
}
|
|
2181
|
+
return { source: rawDefault, hasValue: true, value: null, isCurrentDateFunction: true };
|
|
2182
|
+
}
|
|
2183
|
+
throw new Error(`Invalid default value for ${field.Entity}.${field.Name} of ${rawDefault}. Default values for date fields must be a valid date or a getdate() type default`);
|
|
2184
|
+
}
|
|
2185
|
+
return { source: rawDefault, hasValue: false, value: undefined, isCurrentDateFunction: false };
|
|
2186
|
+
}
|
|
2187
|
+
const dv = typeof rawDefault === 'string' ? rawDefault.trim() : String(rawDefault);
|
|
2188
|
+
const m = dv.match(EntityFieldInfo.PG_TYPED_LITERAL_REGEX);
|
|
2189
|
+
const strVal = m ? m[1].replace(/''/g, "'") : rawDefault;
|
|
2190
|
+
return { source: rawDefault, hasValue: true, value: strVal, isCurrentDateFunction: false };
|
|
2191
|
+
}
|
|
2192
|
+
/**
|
|
2193
|
+
* Parses and memoizes the SQL DefaultValue into its runtime JS representation once per
|
|
2194
|
+
* EntityFieldInfo instead of re-parsing (trimming, regex matching, Number/Date parsing)
|
|
2195
|
+
* on every EntityField instantiation.
|
|
2196
|
+
*/
|
|
2197
|
+
GetParsedDefaultValue() {
|
|
2198
|
+
const rawDefault = this.DefaultValue;
|
|
2199
|
+
if (this._parsedDefaultValueCache !== undefined && this._parsedDefaultValueCache.source === rawDefault) {
|
|
2200
|
+
return this._parsedDefaultValueCache;
|
|
2201
|
+
}
|
|
2202
|
+
this._parsedDefaultValueCache = EntityFieldInfo.ParseDefaultValue(this);
|
|
2203
|
+
return this._parsedDefaultValueCache;
|
|
2204
|
+
}
|
|
2205
|
+
}
|
|
2206
|
+
const _emptyStringSet = new Set();
|
|
2207
|
+
const _userRoleIdSetCache = new WeakMap();
|
|
2208
|
+
function getUserRoleIdSet(user) {
|
|
2209
|
+
if (!user?.UserRoles || user.UserRoles.length === 0) {
|
|
2210
|
+
return _emptyStringSet;
|
|
2211
|
+
}
|
|
2212
|
+
const roles = user.UserRoles;
|
|
2213
|
+
const len = roles.length;
|
|
2214
|
+
const firstId = roles[0]?.RoleID;
|
|
2215
|
+
const cached = _userRoleIdSetCache.get(user);
|
|
2216
|
+
if (cached && cached.rolesRef === roles && cached.rolesLen === len && cached.firstRoleId === firstId) {
|
|
2217
|
+
return cached.roleIds;
|
|
2218
|
+
}
|
|
2219
|
+
const roleIds = new Set();
|
|
2220
|
+
for (let i = 0; i < len; i++) {
|
|
2221
|
+
const rid = roles[i]?.RoleID;
|
|
2222
|
+
if (rid) {
|
|
2223
|
+
roleIds.add(rid.trim().toLowerCase());
|
|
2224
|
+
}
|
|
2225
|
+
}
|
|
2226
|
+
_userRoleIdSetCache.set(user, { rolesRef: roles, rolesLen: len, firstRoleId: firstId, roleIds });
|
|
2227
|
+
return roleIds;
|
|
2099
2228
|
}
|
|
2229
|
+
const _entityFieldIndexCache = new WeakMap();
|
|
2230
|
+
const _entityDeniedFieldsCache = new WeakMap();
|
|
2231
|
+
const _entityChildEntitiesCache = new WeakMap();
|
|
2100
2232
|
/**
|
|
2101
2233
|
* Entity Document Type Info object has information about the document types that exist across all entities. When Entity Documents are created they are associated with a document type.
|
|
2102
2234
|
*/
|
|
@@ -2139,6 +2271,11 @@ export class EntitySettingInfo extends BaseInfo {
|
|
|
2139
2271
|
* Catalog of all entities across all schemas.
|
|
2140
2272
|
* Contains comprehensive metadata about each entity including its database mappings, security settings, and UI preferences.
|
|
2141
2273
|
*/
|
|
2274
|
+
/** Backing list for {@link EntityInfo.ConfigurableAtCreationAttributes}. */
|
|
2275
|
+
const ENTITY_CONFIGURABLE_AT_CREATION_ATTRIBUTES = [
|
|
2276
|
+
'IncludeInAPI', 'AllowUserSearchAPI', 'AllowCaching', 'TrackRecordChanges', 'AuditRecordAccess',
|
|
2277
|
+
'AuditViewRuns', 'AllowAllRowsAPI', 'AllowCreateAPI', 'AllowUpdateAPI', 'AllowDeleteAPI', 'UserViewMaxRows',
|
|
2278
|
+
];
|
|
2142
2279
|
export class EntityInfo extends BaseInfo {
|
|
2143
2280
|
/**
|
|
2144
2281
|
* The view CodeGen actually WRITES for this entity.
|
|
@@ -2322,6 +2459,30 @@ export class EntityInfo extends BaseInfo {
|
|
|
2322
2459
|
get CloneConfig() {
|
|
2323
2460
|
return this.ConfigurationObject?.Clone ?? null;
|
|
2324
2461
|
}
|
|
2462
|
+
/**
|
|
2463
|
+
* Controls which columns a save operation (`spCreate` / `spUpdate`) returns from the database
|
|
2464
|
+
* for this entity. Defaults to `'full'` when `Configuration.SaveReturnMode` is omitted or not
|
|
2465
|
+
* `'datestamps_only'`.
|
|
2466
|
+
*/
|
|
2467
|
+
get SaveReturnMode() {
|
|
2468
|
+
return this.ConfigurationObject?.SaveReturnMode === 'datestamps_only' ? 'datestamps_only' : 'full';
|
|
2469
|
+
}
|
|
2470
|
+
/**
|
|
2471
|
+
* Returns the ordered list of base-table fields returned by a `'datestamps_only'` save:
|
|
2472
|
+
* the entity's primary key field(s), followed by `__mj_CreatedAt` and `__mj_UpdatedAt`
|
|
2473
|
+
* when those columns exist on the entity's base table.
|
|
2474
|
+
*/
|
|
2475
|
+
get DatestampReturnFields() {
|
|
2476
|
+
const result = [...this.PrimaryKeys];
|
|
2477
|
+
const pkNames = new Set(result.map(k => k.Name.trim().toLowerCase()));
|
|
2478
|
+
const createdAt = this.Fields.find(f => !f.IsVirtual && f.IsCreatedAtField && !pkNames.has(f.Name.trim().toLowerCase()));
|
|
2479
|
+
if (createdAt)
|
|
2480
|
+
result.push(createdAt);
|
|
2481
|
+
const updatedAt = this.Fields.find(f => !f.IsVirtual && f.IsUpdatedAtField && !pkNames.has(f.Name.trim().toLowerCase()));
|
|
2482
|
+
if (updatedAt)
|
|
2483
|
+
result.push(updatedAt);
|
|
2484
|
+
return result;
|
|
2485
|
+
}
|
|
2325
2486
|
/**
|
|
2326
2487
|
* Returns true if cloning is explicitly enabled for this entity as a ROOT record.
|
|
2327
2488
|
*/
|
|
@@ -2389,7 +2550,7 @@ export class EntityInfo extends BaseInfo {
|
|
|
2389
2550
|
* `size === 0` as the single "nothing to do" condition.
|
|
2390
2551
|
*/
|
|
2391
2552
|
GetDeniedReadFields(user) {
|
|
2392
|
-
return this.
|
|
2553
|
+
return this.getCachedDeniedFields(user, 'read', (p) => !p.CanRead);
|
|
2393
2554
|
}
|
|
2394
2555
|
/**
|
|
2395
2556
|
* Whether this user may READ the named field — the single-field question, answered the same
|
|
@@ -2458,7 +2619,7 @@ export class EntityInfo extends BaseInfo {
|
|
|
2458
2619
|
* actually need rather than relying on that.
|
|
2459
2620
|
*/
|
|
2460
2621
|
GetDeniedUpdateFields(user) {
|
|
2461
|
-
return this.
|
|
2622
|
+
return this.getCachedDeniedFields(user, 'update', (p) => !p.CanUpdate);
|
|
2462
2623
|
}
|
|
2463
2624
|
/**
|
|
2464
2625
|
* The set of field names this user may NOT supply a value for when CREATING a record. Same
|
|
@@ -2468,7 +2629,49 @@ export class EntityInfo extends BaseInfo {
|
|
|
2468
2629
|
* create-denied field is dropped and the column takes its default.
|
|
2469
2630
|
*/
|
|
2470
2631
|
GetDeniedCreateFields(user) {
|
|
2471
|
-
return this.
|
|
2632
|
+
return this.getCachedDeniedFields(user, 'create', (p) => !p.CanCreate);
|
|
2633
|
+
}
|
|
2634
|
+
getCachedDeniedFields(user, verb, isDenied) {
|
|
2635
|
+
if (!this.EnableFieldLevelSecurity) {
|
|
2636
|
+
return new Set();
|
|
2637
|
+
}
|
|
2638
|
+
if (!user || typeof user !== 'object') {
|
|
2639
|
+
return this.getDeniedFields(user, isDenied);
|
|
2640
|
+
}
|
|
2641
|
+
let totalPerms = 0;
|
|
2642
|
+
for (let i = 0; i < this._fields.length; i++) {
|
|
2643
|
+
totalPerms += this._fields[i].FieldPermissions.length;
|
|
2644
|
+
}
|
|
2645
|
+
const rolesRef = user.UserRoles;
|
|
2646
|
+
const rolesLen = rolesRef?.length ?? 0;
|
|
2647
|
+
let byUser = _entityDeniedFieldsCache.get(this);
|
|
2648
|
+
if (!byUser) {
|
|
2649
|
+
byUser = new WeakMap();
|
|
2650
|
+
_entityDeniedFieldsCache.set(this, byUser);
|
|
2651
|
+
}
|
|
2652
|
+
let entry = byUser.get(user);
|
|
2653
|
+
if (!entry ||
|
|
2654
|
+
entry.fieldsRef !== this._fields ||
|
|
2655
|
+
entry.fieldsLen !== this._fields.length ||
|
|
2656
|
+
entry.totalPerms !== totalPerms ||
|
|
2657
|
+
entry.rolesRef !== rolesRef ||
|
|
2658
|
+
entry.rolesLen !== rolesLen) {
|
|
2659
|
+
entry = {
|
|
2660
|
+
fieldsRef: this._fields,
|
|
2661
|
+
fieldsLen: this._fields.length,
|
|
2662
|
+
totalPerms,
|
|
2663
|
+
rolesRef,
|
|
2664
|
+
rolesLen,
|
|
2665
|
+
};
|
|
2666
|
+
byUser.set(user, entry);
|
|
2667
|
+
}
|
|
2668
|
+
const existing = entry[verb];
|
|
2669
|
+
if (existing !== undefined) {
|
|
2670
|
+
return existing;
|
|
2671
|
+
}
|
|
2672
|
+
const computed = this.getDeniedFields(user, isDenied);
|
|
2673
|
+
entry[verb] = computed;
|
|
2674
|
+
return computed;
|
|
2472
2675
|
}
|
|
2473
2676
|
/**
|
|
2474
2677
|
* Shared walk behind {@link GetDeniedReadFields} / {@link GetDeniedUpdateFields} /
|
|
@@ -2523,8 +2726,9 @@ export class EntityInfo extends BaseInfo {
|
|
|
2523
2726
|
if (name == null)
|
|
2524
2727
|
return undefined;
|
|
2525
2728
|
if (this._fieldByNameMap === null) {
|
|
2729
|
+
const fields = this._fields ?? this.Fields ?? [];
|
|
2526
2730
|
const map = new Map();
|
|
2527
|
-
for (const f of
|
|
2731
|
+
for (const f of fields) {
|
|
2528
2732
|
if (f.Name != null)
|
|
2529
2733
|
map.set(f.Name.trim().toLowerCase(), f);
|
|
2530
2734
|
}
|
|
@@ -2532,6 +2736,62 @@ export class EntityInfo extends BaseInfo {
|
|
|
2532
2736
|
}
|
|
2533
2737
|
return this._fieldByNameMap.get(name.trim().toLowerCase());
|
|
2534
2738
|
}
|
|
2739
|
+
ensureFieldIndexEntry() {
|
|
2740
|
+
const fields = this._fields ?? this.Fields ?? [];
|
|
2741
|
+
let entry = _entityFieldIndexCache.get(this);
|
|
2742
|
+
if (!entry || entry.fieldsRef !== fields || entry.fieldsLen !== fields.length) {
|
|
2743
|
+
const byName = new Map();
|
|
2744
|
+
const byCodeName = new Map();
|
|
2745
|
+
for (let i = 0; i < fields.length; i++) {
|
|
2746
|
+
const f = fields[i];
|
|
2747
|
+
if (f.Name != null) {
|
|
2748
|
+
const nameKey = f.Name.trim().toLowerCase();
|
|
2749
|
+
if (!byName.has(nameKey)) {
|
|
2750
|
+
byName.set(nameKey, i);
|
|
2751
|
+
}
|
|
2752
|
+
const rawCodeName = f.CodeName ?? f.Name;
|
|
2753
|
+
const codeKey = rawCodeName.trim().toLowerCase();
|
|
2754
|
+
if (!byCodeName.has(codeKey)) {
|
|
2755
|
+
byCodeName.set(codeKey, i);
|
|
2756
|
+
}
|
|
2757
|
+
}
|
|
2758
|
+
}
|
|
2759
|
+
entry = { fieldsRef: fields, fieldsLen: fields.length, byName, byCodeName };
|
|
2760
|
+
_entityFieldIndexCache.set(this, entry);
|
|
2761
|
+
}
|
|
2762
|
+
return entry;
|
|
2763
|
+
}
|
|
2764
|
+
/**
|
|
2765
|
+
* O(1) case-insensitive lookup of a field's positional index in `Fields` by `Name`.
|
|
2766
|
+
* Shared across all `BaseEntity` instances of this `EntityInfo` so individual entity
|
|
2767
|
+
* instances do not need to allocate their own `Map<string, EntityField>` on hydration.
|
|
2768
|
+
*/
|
|
2769
|
+
FieldIndexByName(name) {
|
|
2770
|
+
if (!name)
|
|
2771
|
+
return -1;
|
|
2772
|
+
return this.ensureFieldIndexEntry().byName.get(name.trim().toLowerCase()) ?? -1;
|
|
2773
|
+
}
|
|
2774
|
+
/**
|
|
2775
|
+
* O(1) case-insensitive lookup of a field's positional index in `Fields` by `CodeName`.
|
|
2776
|
+
*/
|
|
2777
|
+
FieldIndexByCodeName(codeName) {
|
|
2778
|
+
if (!codeName)
|
|
2779
|
+
return -1;
|
|
2780
|
+
return this.ensureFieldIndexEntry().byCodeName.get(codeName.trim().toLowerCase()) ?? -1;
|
|
2781
|
+
}
|
|
2782
|
+
/**
|
|
2783
|
+
* O(1) case-insensitive field lookup by `CodeName`. Uses the shared `_entityFieldIndexCache`
|
|
2784
|
+
* index so GraphQL resolvers and mutation handlers can resolve client `CodeName` keys without
|
|
2785
|
+
* linear `Fields.find(...)` scans.
|
|
2786
|
+
*
|
|
2787
|
+
* @param codeName field CodeName (matched case-insensitively, whitespace-trimmed)
|
|
2788
|
+
* @returns the matching EntityFieldInfo, or undefined if not found
|
|
2789
|
+
*/
|
|
2790
|
+
FieldByCodeName(codeName) {
|
|
2791
|
+
const idx = this.FieldIndexByCodeName(codeName);
|
|
2792
|
+
const fields = this._fields ?? this.Fields;
|
|
2793
|
+
return idx >= 0 && fields ? fields[idx] : undefined;
|
|
2794
|
+
}
|
|
2535
2795
|
/**
|
|
2536
2796
|
* Returns the primary key field for the entity. For entities with a composite primary key, use the PrimaryKeys property which returns all.
|
|
2537
2797
|
* In the case of a composite primary key, the PrimaryKey property will return the first field in the sequence of the primary key fields.
|
|
@@ -2731,6 +2991,14 @@ export class EntityInfo extends BaseInfo {
|
|
|
2731
2991
|
static get DeletedAtFieldName() {
|
|
2732
2992
|
return EntityInfo.__deletedAtFieldName;
|
|
2733
2993
|
}
|
|
2994
|
+
/**
|
|
2995
|
+
* Entity flags that a per-table entry in CodeGen's additionalSchemaInfo config may set when CodeGen first
|
|
2996
|
+
* creates the entity, overriding the global `newEntityDefaults`. Typed against `EntityInfo`, so a name that
|
|
2997
|
+
* stops being an entity property fails to compile.
|
|
2998
|
+
*/
|
|
2999
|
+
static get ConfigurableAtCreationAttributes() {
|
|
3000
|
+
return ENTITY_CONFIGURABLE_AT_CREATION_ATTRIBUTES;
|
|
3001
|
+
}
|
|
2734
3002
|
/**
|
|
2735
3003
|
* This static factory method is used to check to see if the entity in question is active or not
|
|
2736
3004
|
* If it is not active, it will throw an exception or log a warning depending on the status of the entity being
|
|
@@ -2846,7 +3114,7 @@ export class EntityInfo extends BaseInfo {
|
|
|
2846
3114
|
get ParentEntityInfo() {
|
|
2847
3115
|
if (!this.ParentID)
|
|
2848
3116
|
return null;
|
|
2849
|
-
const p =
|
|
3117
|
+
const p = MetadataProviderFor(this);
|
|
2850
3118
|
if (p?.EntityByID) {
|
|
2851
3119
|
return p.EntityByID(this.ParentID) ?? null;
|
|
2852
3120
|
}
|
|
@@ -2862,7 +3130,23 @@ export class EntityInfo extends BaseInfo {
|
|
|
2862
3130
|
* only one child type is allowed per parent record (disjoint subtypes).
|
|
2863
3131
|
*/
|
|
2864
3132
|
get ChildEntities() {
|
|
2865
|
-
|
|
3133
|
+
const provider = MetadataProviderFor(this);
|
|
3134
|
+
const entities = provider?.Entities;
|
|
3135
|
+
if (!entities || entities.length === 0) {
|
|
3136
|
+
return [];
|
|
3137
|
+
}
|
|
3138
|
+
const cached = _entityChildEntitiesCache.get(this);
|
|
3139
|
+
if (cached && cached.provider === provider && cached.entitiesRef === entities && cached.entitiesLen === entities.length) {
|
|
3140
|
+
return cached.children;
|
|
3141
|
+
}
|
|
3142
|
+
const children = entities.filter(e => UUIDsEqual(e.ParentID, this.ID));
|
|
3143
|
+
_entityChildEntitiesCache.set(this, {
|
|
3144
|
+
provider,
|
|
3145
|
+
entitiesRef: entities,
|
|
3146
|
+
entitiesLen: entities.length,
|
|
3147
|
+
children,
|
|
3148
|
+
});
|
|
3149
|
+
return children;
|
|
2866
3150
|
}
|
|
2867
3151
|
/**
|
|
2868
3152
|
* Convenience alias: returns true when this entity is a parent type that allows
|
|
@@ -3035,23 +3319,21 @@ export class EntityInfo extends BaseInfo {
|
|
|
3035
3319
|
*/
|
|
3036
3320
|
GetUserPermisions(user) {
|
|
3037
3321
|
try {
|
|
3038
|
-
const
|
|
3039
|
-
|
|
3040
|
-
const ep = this.Permissions[j];
|
|
3041
|
-
const roleMatch = user.UserRoles?.find((r) => UUIDsEqual(r.RoleID, ep.RoleID));
|
|
3042
|
-
if (roleMatch) // user has this role
|
|
3043
|
-
permissionList.push(ep);
|
|
3044
|
-
}
|
|
3045
|
-
// Aggregate Allow and Deny separately, then subtract Deny from Allow per action.
|
|
3322
|
+
const _ = user.UserRoles; // preserve throw-on-null behavior
|
|
3323
|
+
const userRoleIds = getUserRoleIdSet(user);
|
|
3046
3324
|
const allow = { CanCreate: false, CanRead: false, CanUpdate: false, CanDelete: false };
|
|
3047
3325
|
const deny = { CanCreate: false, CanRead: false, CanUpdate: false, CanDelete: false };
|
|
3048
|
-
|
|
3049
|
-
|
|
3050
|
-
|
|
3051
|
-
|
|
3052
|
-
|
|
3053
|
-
|
|
3054
|
-
|
|
3326
|
+
if (userRoleIds.size > 0) {
|
|
3327
|
+
for (let j = 0; j < this.Permissions.length; j++) {
|
|
3328
|
+
const ep = this.Permissions[j];
|
|
3329
|
+
if (ep.RoleID && userRoleIds.has(ep.RoleID.trim().toLowerCase())) {
|
|
3330
|
+
const bucket = ep.IsDeny ? deny : allow;
|
|
3331
|
+
bucket.CanCreate = bucket.CanCreate || !!ep.CanCreate;
|
|
3332
|
+
bucket.CanRead = bucket.CanRead || !!ep.CanRead;
|
|
3333
|
+
bucket.CanUpdate = bucket.CanUpdate || !!ep.CanUpdate;
|
|
3334
|
+
bucket.CanDelete = bucket.CanDelete || !!ep.CanDelete;
|
|
3335
|
+
}
|
|
3336
|
+
}
|
|
3055
3337
|
}
|
|
3056
3338
|
// An API-driven action also requires that the entity ALLOWS that action at all
|
|
3057
3339
|
// (`Allow*API`). No role grant can create/update/delete records of an entity whose
|
|
@@ -3086,13 +3368,17 @@ export class EntityInfo extends BaseInfo {
|
|
|
3086
3368
|
* @returns
|
|
3087
3369
|
*/
|
|
3088
3370
|
UserExemptFromRowLevelSecurity(user, type) {
|
|
3371
|
+
const _ = user.UserRoles; // preserve throw-on-null behavior if user is null/undefined
|
|
3372
|
+
const userRoleIds = getUserRoleIdSet(user);
|
|
3373
|
+
if (userRoleIds.size === 0) {
|
|
3374
|
+
return false;
|
|
3375
|
+
}
|
|
3089
3376
|
for (let j = 0; j < this.Permissions.length; j++) {
|
|
3090
3377
|
const ep = this.Permissions[j];
|
|
3091
3378
|
if (ep.IsDeny) {
|
|
3092
3379
|
continue; // a Deny row's Can* flags are denials, never grants — it cannot exempt anyone
|
|
3093
3380
|
}
|
|
3094
|
-
|
|
3095
|
-
if (roleMatch) { // user has this role
|
|
3381
|
+
if (ep.RoleID && userRoleIds.has(ep.RoleID.trim().toLowerCase())) { // user has this role
|
|
3096
3382
|
switch (type) {
|
|
3097
3383
|
case EntityPermissionType.Create:
|
|
3098
3384
|
if (ep.CanCreate && !ep.CreateRLSFilterID)
|
|
@@ -3133,14 +3419,18 @@ export class EntityInfo extends BaseInfo {
|
|
|
3133
3419
|
* @returns
|
|
3134
3420
|
*/
|
|
3135
3421
|
GetUserRowLevelSecurityInfo(user, type) {
|
|
3422
|
+
const _ = user.UserRoles; // preserve throw-on-null behavior if user is null/undefined
|
|
3136
3423
|
const rlsList = [];
|
|
3424
|
+
const userRoleIds = getUserRoleIdSet(user);
|
|
3425
|
+
if (userRoleIds.size === 0) {
|
|
3426
|
+
return rlsList;
|
|
3427
|
+
}
|
|
3137
3428
|
for (let j = 0; j < this.Permissions.length; j++) {
|
|
3138
3429
|
const ep = this.Permissions[j];
|
|
3139
3430
|
if (ep.IsDeny) {
|
|
3140
3431
|
continue; // never a grant — see the doc comment
|
|
3141
3432
|
}
|
|
3142
|
-
|
|
3143
|
-
if (roleMatch) { // user has this role
|
|
3433
|
+
if (ep.RoleID && userRoleIds.has(ep.RoleID.trim().toLowerCase())) { // user has this role
|
|
3144
3434
|
let matchObject = null;
|
|
3145
3435
|
switch (type) {
|
|
3146
3436
|
case EntityPermissionType.Create:
|
|
@@ -3256,7 +3546,7 @@ export class EntityInfo extends BaseInfo {
|
|
|
3256
3546
|
const clauses = [...mine]
|
|
3257
3547
|
.sort((a, b) => a.FilterID.localeCompare(b.FilterID)) // deterministic order for the cache fingerprint (INV-2)
|
|
3258
3548
|
.map(b => {
|
|
3259
|
-
const filter =
|
|
3549
|
+
const filter = MetadataProviderFor(this)?.RowLevelSecurityFilters?.find(f => UUIDsEqual(f.ID, b.FilterID));
|
|
3260
3550
|
if (!filter) {
|
|
3261
3551
|
LogError(`API-key row filter ${b.FilterID} for entity ${this.Name} not found in metadata — resolving to (1=0)`);
|
|
3262
3552
|
return '(1=0)';
|
|
@@ -3428,7 +3718,7 @@ export class EntityInfo extends BaseInfo {
|
|
|
3428
3718
|
const conditions = [];
|
|
3429
3719
|
// Resolve the spoke entity's own organic key (matching by Name) to pull its
|
|
3430
3720
|
// per-column normalization. Falls back to the hub's expression if not found.
|
|
3431
|
-
const spokeOrganicKey = EntityInfo.resolveSpokeOrganicKey(relatedEntity, organicKey);
|
|
3721
|
+
const spokeOrganicKey = EntityInfo.resolveSpokeOrganicKey(relatedEntity, organicKey, record.EntityInfo?.OwningProvider);
|
|
3432
3722
|
for (let i = 0; i < matchFields.length; i++) {
|
|
3433
3723
|
const value = record.Get(matchFields[i]);
|
|
3434
3724
|
if (value == null) {
|
|
@@ -3552,7 +3842,20 @@ export class EntityInfo extends BaseInfo {
|
|
|
3552
3842
|
default: return `'${escapedValue}'`;
|
|
3553
3843
|
}
|
|
3554
3844
|
}
|
|
3555
|
-
|
|
3845
|
+
static { this.CHILD_INIT_KEYS = new Set([
|
|
3846
|
+
'EntityFields', '_Fields', 'Fields',
|
|
3847
|
+
'EntityPermissions', '_Permissions', 'Permissions',
|
|
3848
|
+
'EntitySettings', '_Settings', 'Settings',
|
|
3849
|
+
'EntityRelationships', '_RelatedEntities', 'RelatedEntities',
|
|
3850
|
+
'EntityOrganicKeys', '_OrganicKeys', 'OrganicKeys',
|
|
3851
|
+
]); }
|
|
3852
|
+
/**
|
|
3853
|
+
* @param initData - the raw entity row (or another EntityInfo being re-instantiated)
|
|
3854
|
+
* @param md - the provider that owns this metadata. Sibling lookups ({@link ParentEntityInfo},
|
|
3855
|
+
* {@link ChildEntities}, RLS filters) resolve through it rather than the global provider, so
|
|
3856
|
+
* they stay correct in a multi-provider process. Optional for backward compatibility.
|
|
3857
|
+
*/
|
|
3858
|
+
constructor(initData = null, md) {
|
|
3556
3859
|
super();
|
|
3557
3860
|
/**
|
|
3558
3861
|
* Unique identifier for the entity
|
|
@@ -3677,11 +3980,21 @@ export class EntityInfo extends BaseInfo {
|
|
|
3677
3980
|
*/
|
|
3678
3981
|
this.AuditViewRuns = null;
|
|
3679
3982
|
/**
|
|
3680
|
-
*
|
|
3681
|
-
*
|
|
3682
|
-
*
|
|
3683
|
-
*
|
|
3684
|
-
*
|
|
3983
|
+
* Whether every change to this entity's rows fires a `BaseEntity` event. True (the default)
|
|
3984
|
+
* declares that all mutations flow through `BaseEntity.Save()`/`Delete()`, so a cached copy
|
|
3985
|
+
* kept current by those events can be trusted. False declares that rows can change without an
|
|
3986
|
+
* event: raw SQL, a stored procedure (e.g. Record Changes created by
|
|
3987
|
+
* `spCreateRecordChange_Internal`), or another application writing the same table.
|
|
3988
|
+
*
|
|
3989
|
+
* Each cache layer responds to `false` in the way that suits it:
|
|
3990
|
+
* - **Server RunView cache:** does not cache the entity, since re-validating would cost as
|
|
3991
|
+
* much as running the query.
|
|
3992
|
+
* - **Client RunView cache:** caches as before; every read is validated against the database.
|
|
3993
|
+
* - **Engine caches (`BaseEngine`):** keep the rows, and the engine sweep re-checks them on its
|
|
3994
|
+
* interval. The user cache and the metadata sweep do the same for their entities.
|
|
3995
|
+
*
|
|
3996
|
+
* To keep an entity out of caching altogether, set {@link AllowCaching} to false instead.
|
|
3997
|
+
* Setting any `AllowDirectSQL*` flag requires this to be false (a database CHECK enforces it).
|
|
3685
3998
|
*/
|
|
3686
3999
|
this.TrustServerCacheCompletely = true;
|
|
3687
4000
|
/**
|
|
@@ -4022,8 +4335,9 @@ export class EntityInfo extends BaseInfo {
|
|
|
4022
4335
|
this._parentChainCache = null;
|
|
4023
4336
|
// Cache for ParentEntityFieldNames
|
|
4024
4337
|
this._parentEntityFieldNamesCache = null;
|
|
4338
|
+
this.SetOwningProvider(md);
|
|
4025
4339
|
if (initData) {
|
|
4026
|
-
this.copyInitData(initData);
|
|
4340
|
+
this.copyInitData(initData, EntityInfo.CHILD_INIT_KEYS);
|
|
4027
4341
|
// do some special handling to create class instances instead of just data objects
|
|
4028
4342
|
// copy the Entity Fields (accept EntityFields, _Fields, or Fields as input names)
|
|
4029
4343
|
this._fields = [];
|
|
@@ -4054,7 +4368,9 @@ export class EntityInfo extends BaseInfo {
|
|
|
4054
4368
|
const ef = initData.EntityFields || initData._Fields || initData.Fields;
|
|
4055
4369
|
if (ef) {
|
|
4056
4370
|
for (let j = 0; j < ef.length; j++) {
|
|
4057
|
-
|
|
4371
|
+
const field = new EntityFieldInfo(ef[j]);
|
|
4372
|
+
field.SetOwningProvider(md);
|
|
4373
|
+
this._fields.push(field);
|
|
4058
4374
|
}
|
|
4059
4375
|
}
|
|
4060
4376
|
// copy the Entity Permissions
|
|
@@ -4062,7 +4378,7 @@ export class EntityInfo extends BaseInfo {
|
|
|
4062
4378
|
const ep = initData.EntityPermissions || initData._Permissions || initData.Permissions;
|
|
4063
4379
|
if (ep) {
|
|
4064
4380
|
for (let j = 0; j < ep.length; j++) {
|
|
4065
|
-
this._permissions.push(new EntityPermissionInfo(ep[j]));
|
|
4381
|
+
this._permissions.push(new EntityPermissionInfo(ep[j], md));
|
|
4066
4382
|
}
|
|
4067
4383
|
}
|
|
4068
4384
|
// copy the Entity settings
|