@memberjunction/core 6.2.0-edge.4 → 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 +18 -0
- package/dist/generic/JSONType-interfaces/IEntityConfiguration.d.ts.map +1 -1
- package/dist/generic/baseEngine.d.ts +29 -5
- package/dist/generic/baseEngine.d.ts.map +1 -1
- package/dist/generic/baseEngine.js +117 -11
- package/dist/generic/baseEngine.js.map +1 -1
- 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 +37 -8
- package/dist/generic/databaseProviderBase.d.ts.map +1 -1
- package/dist/generic/databaseProviderBase.js +162 -65
- 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/entityInfo.d.ts +78 -3
- package/dist/generic/entityInfo.d.ts.map +1 -1
- package/dist/generic/entityInfo.js +348 -55
- 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 +25 -1
- package/dist/generic/interfaces.d.ts.map +1 -1
- package/dist/generic/interfaces.js.map +1 -1
- package/dist/generic/localCacheManager.d.ts +238 -40
- package/dist/generic/localCacheManager.d.ts.map +1 -1
- package/dist/generic/localCacheManager.js +466 -71
- package/dist/generic/localCacheManager.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 +118 -3
- package/dist/generic/providerBase.d.ts.map +1 -1
- package/dist/generic/providerBase.js +398 -100
- 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 +6 -6
- package/dist/generic/securityInfo.d.ts.map +1 -1
- package/dist/generic/securityInfo.js +10 -10
- package/dist/generic/securityInfo.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 +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -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
|
+
}
|
|
2099
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;
|
|
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
|
*/
|
|
@@ -2327,6 +2459,30 @@ export class EntityInfo extends BaseInfo {
|
|
|
2327
2459
|
get CloneConfig() {
|
|
2328
2460
|
return this.ConfigurationObject?.Clone ?? null;
|
|
2329
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
|
+
}
|
|
2330
2486
|
/**
|
|
2331
2487
|
* Returns true if cloning is explicitly enabled for this entity as a ROOT record.
|
|
2332
2488
|
*/
|
|
@@ -2394,7 +2550,7 @@ export class EntityInfo extends BaseInfo {
|
|
|
2394
2550
|
* `size === 0` as the single "nothing to do" condition.
|
|
2395
2551
|
*/
|
|
2396
2552
|
GetDeniedReadFields(user) {
|
|
2397
|
-
return this.
|
|
2553
|
+
return this.getCachedDeniedFields(user, 'read', (p) => !p.CanRead);
|
|
2398
2554
|
}
|
|
2399
2555
|
/**
|
|
2400
2556
|
* Whether this user may READ the named field — the single-field question, answered the same
|
|
@@ -2463,7 +2619,7 @@ export class EntityInfo extends BaseInfo {
|
|
|
2463
2619
|
* actually need rather than relying on that.
|
|
2464
2620
|
*/
|
|
2465
2621
|
GetDeniedUpdateFields(user) {
|
|
2466
|
-
return this.
|
|
2622
|
+
return this.getCachedDeniedFields(user, 'update', (p) => !p.CanUpdate);
|
|
2467
2623
|
}
|
|
2468
2624
|
/**
|
|
2469
2625
|
* The set of field names this user may NOT supply a value for when CREATING a record. Same
|
|
@@ -2473,7 +2629,49 @@ export class EntityInfo extends BaseInfo {
|
|
|
2473
2629
|
* create-denied field is dropped and the column takes its default.
|
|
2474
2630
|
*/
|
|
2475
2631
|
GetDeniedCreateFields(user) {
|
|
2476
|
-
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;
|
|
2477
2675
|
}
|
|
2478
2676
|
/**
|
|
2479
2677
|
* Shared walk behind {@link GetDeniedReadFields} / {@link GetDeniedUpdateFields} /
|
|
@@ -2528,8 +2726,9 @@ export class EntityInfo extends BaseInfo {
|
|
|
2528
2726
|
if (name == null)
|
|
2529
2727
|
return undefined;
|
|
2530
2728
|
if (this._fieldByNameMap === null) {
|
|
2729
|
+
const fields = this._fields ?? this.Fields ?? [];
|
|
2531
2730
|
const map = new Map();
|
|
2532
|
-
for (const f of
|
|
2731
|
+
for (const f of fields) {
|
|
2533
2732
|
if (f.Name != null)
|
|
2534
2733
|
map.set(f.Name.trim().toLowerCase(), f);
|
|
2535
2734
|
}
|
|
@@ -2537,6 +2736,62 @@ export class EntityInfo extends BaseInfo {
|
|
|
2537
2736
|
}
|
|
2538
2737
|
return this._fieldByNameMap.get(name.trim().toLowerCase());
|
|
2539
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
|
+
}
|
|
2540
2795
|
/**
|
|
2541
2796
|
* Returns the primary key field for the entity. For entities with a composite primary key, use the PrimaryKeys property which returns all.
|
|
2542
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.
|
|
@@ -2859,7 +3114,7 @@ export class EntityInfo extends BaseInfo {
|
|
|
2859
3114
|
get ParentEntityInfo() {
|
|
2860
3115
|
if (!this.ParentID)
|
|
2861
3116
|
return null;
|
|
2862
|
-
const p =
|
|
3117
|
+
const p = MetadataProviderFor(this);
|
|
2863
3118
|
if (p?.EntityByID) {
|
|
2864
3119
|
return p.EntityByID(this.ParentID) ?? null;
|
|
2865
3120
|
}
|
|
@@ -2875,7 +3130,23 @@ export class EntityInfo extends BaseInfo {
|
|
|
2875
3130
|
* only one child type is allowed per parent record (disjoint subtypes).
|
|
2876
3131
|
*/
|
|
2877
3132
|
get ChildEntities() {
|
|
2878
|
-
|
|
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;
|
|
2879
3150
|
}
|
|
2880
3151
|
/**
|
|
2881
3152
|
* Convenience alias: returns true when this entity is a parent type that allows
|
|
@@ -3048,23 +3319,21 @@ export class EntityInfo extends BaseInfo {
|
|
|
3048
3319
|
*/
|
|
3049
3320
|
GetUserPermisions(user) {
|
|
3050
3321
|
try {
|
|
3051
|
-
const
|
|
3052
|
-
|
|
3053
|
-
const ep = this.Permissions[j];
|
|
3054
|
-
const roleMatch = user.UserRoles?.find((r) => UUIDsEqual(r.RoleID, ep.RoleID));
|
|
3055
|
-
if (roleMatch) // user has this role
|
|
3056
|
-
permissionList.push(ep);
|
|
3057
|
-
}
|
|
3058
|
-
// 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);
|
|
3059
3324
|
const allow = { CanCreate: false, CanRead: false, CanUpdate: false, CanDelete: false };
|
|
3060
3325
|
const deny = { CanCreate: false, CanRead: false, CanUpdate: false, CanDelete: false };
|
|
3061
|
-
|
|
3062
|
-
|
|
3063
|
-
|
|
3064
|
-
|
|
3065
|
-
|
|
3066
|
-
|
|
3067
|
-
|
|
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
|
+
}
|
|
3068
3337
|
}
|
|
3069
3338
|
// An API-driven action also requires that the entity ALLOWS that action at all
|
|
3070
3339
|
// (`Allow*API`). No role grant can create/update/delete records of an entity whose
|
|
@@ -3099,13 +3368,17 @@ export class EntityInfo extends BaseInfo {
|
|
|
3099
3368
|
* @returns
|
|
3100
3369
|
*/
|
|
3101
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
|
+
}
|
|
3102
3376
|
for (let j = 0; j < this.Permissions.length; j++) {
|
|
3103
3377
|
const ep = this.Permissions[j];
|
|
3104
3378
|
if (ep.IsDeny) {
|
|
3105
3379
|
continue; // a Deny row's Can* flags are denials, never grants — it cannot exempt anyone
|
|
3106
3380
|
}
|
|
3107
|
-
|
|
3108
|
-
if (roleMatch) { // user has this role
|
|
3381
|
+
if (ep.RoleID && userRoleIds.has(ep.RoleID.trim().toLowerCase())) { // user has this role
|
|
3109
3382
|
switch (type) {
|
|
3110
3383
|
case EntityPermissionType.Create:
|
|
3111
3384
|
if (ep.CanCreate && !ep.CreateRLSFilterID)
|
|
@@ -3146,14 +3419,18 @@ export class EntityInfo extends BaseInfo {
|
|
|
3146
3419
|
* @returns
|
|
3147
3420
|
*/
|
|
3148
3421
|
GetUserRowLevelSecurityInfo(user, type) {
|
|
3422
|
+
const _ = user.UserRoles; // preserve throw-on-null behavior if user is null/undefined
|
|
3149
3423
|
const rlsList = [];
|
|
3424
|
+
const userRoleIds = getUserRoleIdSet(user);
|
|
3425
|
+
if (userRoleIds.size === 0) {
|
|
3426
|
+
return rlsList;
|
|
3427
|
+
}
|
|
3150
3428
|
for (let j = 0; j < this.Permissions.length; j++) {
|
|
3151
3429
|
const ep = this.Permissions[j];
|
|
3152
3430
|
if (ep.IsDeny) {
|
|
3153
3431
|
continue; // never a grant — see the doc comment
|
|
3154
3432
|
}
|
|
3155
|
-
|
|
3156
|
-
if (roleMatch) { // user has this role
|
|
3433
|
+
if (ep.RoleID && userRoleIds.has(ep.RoleID.trim().toLowerCase())) { // user has this role
|
|
3157
3434
|
let matchObject = null;
|
|
3158
3435
|
switch (type) {
|
|
3159
3436
|
case EntityPermissionType.Create:
|
|
@@ -3269,7 +3546,7 @@ export class EntityInfo extends BaseInfo {
|
|
|
3269
3546
|
const clauses = [...mine]
|
|
3270
3547
|
.sort((a, b) => a.FilterID.localeCompare(b.FilterID)) // deterministic order for the cache fingerprint (INV-2)
|
|
3271
3548
|
.map(b => {
|
|
3272
|
-
const filter =
|
|
3549
|
+
const filter = MetadataProviderFor(this)?.RowLevelSecurityFilters?.find(f => UUIDsEqual(f.ID, b.FilterID));
|
|
3273
3550
|
if (!filter) {
|
|
3274
3551
|
LogError(`API-key row filter ${b.FilterID} for entity ${this.Name} not found in metadata — resolving to (1=0)`);
|
|
3275
3552
|
return '(1=0)';
|
|
@@ -3441,7 +3718,7 @@ export class EntityInfo extends BaseInfo {
|
|
|
3441
3718
|
const conditions = [];
|
|
3442
3719
|
// Resolve the spoke entity's own organic key (matching by Name) to pull its
|
|
3443
3720
|
// per-column normalization. Falls back to the hub's expression if not found.
|
|
3444
|
-
const spokeOrganicKey = EntityInfo.resolveSpokeOrganicKey(relatedEntity, organicKey);
|
|
3721
|
+
const spokeOrganicKey = EntityInfo.resolveSpokeOrganicKey(relatedEntity, organicKey, record.EntityInfo?.OwningProvider);
|
|
3445
3722
|
for (let i = 0; i < matchFields.length; i++) {
|
|
3446
3723
|
const value = record.Get(matchFields[i]);
|
|
3447
3724
|
if (value == null) {
|
|
@@ -3565,7 +3842,20 @@ export class EntityInfo extends BaseInfo {
|
|
|
3565
3842
|
default: return `'${escapedValue}'`;
|
|
3566
3843
|
}
|
|
3567
3844
|
}
|
|
3568
|
-
|
|
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) {
|
|
3569
3859
|
super();
|
|
3570
3860
|
/**
|
|
3571
3861
|
* Unique identifier for the entity
|
|
@@ -4045,8 +4335,9 @@ export class EntityInfo extends BaseInfo {
|
|
|
4045
4335
|
this._parentChainCache = null;
|
|
4046
4336
|
// Cache for ParentEntityFieldNames
|
|
4047
4337
|
this._parentEntityFieldNamesCache = null;
|
|
4338
|
+
this.SetOwningProvider(md);
|
|
4048
4339
|
if (initData) {
|
|
4049
|
-
this.copyInitData(initData);
|
|
4340
|
+
this.copyInitData(initData, EntityInfo.CHILD_INIT_KEYS);
|
|
4050
4341
|
// do some special handling to create class instances instead of just data objects
|
|
4051
4342
|
// copy the Entity Fields (accept EntityFields, _Fields, or Fields as input names)
|
|
4052
4343
|
this._fields = [];
|
|
@@ -4077,7 +4368,9 @@ export class EntityInfo extends BaseInfo {
|
|
|
4077
4368
|
const ef = initData.EntityFields || initData._Fields || initData.Fields;
|
|
4078
4369
|
if (ef) {
|
|
4079
4370
|
for (let j = 0; j < ef.length; j++) {
|
|
4080
|
-
|
|
4371
|
+
const field = new EntityFieldInfo(ef[j]);
|
|
4372
|
+
field.SetOwningProvider(md);
|
|
4373
|
+
this._fields.push(field);
|
|
4081
4374
|
}
|
|
4082
4375
|
}
|
|
4083
4376
|
// copy the Entity Permissions
|
|
@@ -4085,7 +4378,7 @@ export class EntityInfo extends BaseInfo {
|
|
|
4085
4378
|
const ep = initData.EntityPermissions || initData._Permissions || initData.Permissions;
|
|
4086
4379
|
if (ep) {
|
|
4087
4380
|
for (let j = 0; j < ep.length; j++) {
|
|
4088
|
-
this._permissions.push(new EntityPermissionInfo(ep[j]));
|
|
4381
|
+
this._permissions.push(new EntityPermissionInfo(ep[j], md));
|
|
4089
4382
|
}
|
|
4090
4383
|
}
|
|
4091
4384
|
// copy the Entity settings
|