@memberjunction/core 6.1.0-edge.2 → 6.1.0-edge.3
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/LICENSE +180 -4
- package/dist/generic/JSONType-interfaces/IEntityConfiguration.d.ts +120 -0
- package/dist/generic/JSONType-interfaces/IEntityConfiguration.d.ts.map +1 -0
- package/dist/generic/JSONType-interfaces/IEntityConfiguration.js +6 -0
- package/dist/generic/JSONType-interfaces/IEntityConfiguration.js.map +1 -0
- package/dist/generic/JSONType-interfaces/IEntityFieldConfiguration.d.ts +31 -0
- package/dist/generic/JSONType-interfaces/IEntityFieldConfiguration.d.ts.map +1 -0
- package/dist/generic/JSONType-interfaces/IEntityFieldConfiguration.js +6 -0
- package/dist/generic/JSONType-interfaces/IEntityFieldConfiguration.js.map +1 -0
- package/dist/generic/JSONType-interfaces/IEntityRelationshipConfiguration.d.ts +75 -0
- package/dist/generic/JSONType-interfaces/IEntityRelationshipConfiguration.d.ts.map +1 -0
- package/dist/generic/JSONType-interfaces/IEntityRelationshipConfiguration.js +6 -0
- package/dist/generic/JSONType-interfaces/IEntityRelationshipConfiguration.js.map +1 -0
- package/dist/generic/JSONType-interfaces/IFileStorageProviderConfiguration.d.ts +19 -0
- package/dist/generic/JSONType-interfaces/IFileStorageProviderConfiguration.d.ts.map +1 -0
- package/dist/generic/JSONType-interfaces/IFileStorageProviderConfiguration.js +6 -0
- package/dist/generic/JSONType-interfaces/IFileStorageProviderConfiguration.js.map +1 -0
- package/dist/generic/authTypes.d.ts +69 -0
- package/dist/generic/authTypes.d.ts.map +1 -1
- package/dist/generic/baseEngine.d.ts +14 -0
- package/dist/generic/baseEngine.d.ts.map +1 -1
- package/dist/generic/baseEngine.js +53 -13
- package/dist/generic/baseEngine.js.map +1 -1
- package/dist/generic/baseEntity.d.ts +113 -20
- package/dist/generic/baseEntity.d.ts.map +1 -1
- package/dist/generic/baseEntity.js +351 -49
- package/dist/generic/baseEntity.js.map +1 -1
- package/dist/generic/baseInfo.d.ts.map +1 -1
- package/dist/generic/baseInfo.js +16 -4
- package/dist/generic/baseInfo.js.map +1 -1
- package/dist/generic/compositeKey.d.ts +30 -0
- package/dist/generic/compositeKey.d.ts.map +1 -1
- package/dist/generic/compositeKey.js +97 -0
- package/dist/generic/compositeKey.js.map +1 -1
- package/dist/generic/databaseProviderBase.d.ts.map +1 -1
- package/dist/generic/databaseProviderBase.js +9 -5
- package/dist/generic/databaseProviderBase.js.map +1 -1
- package/dist/generic/embeddedRecord.d.ts +123 -0
- package/dist/generic/embeddedRecord.d.ts.map +1 -0
- package/dist/generic/embeddedRecord.js +356 -0
- package/dist/generic/embeddedRecord.js.map +1 -0
- package/dist/generic/entityCompanion.d.ts +20 -2
- package/dist/generic/entityCompanion.d.ts.map +1 -1
- package/dist/generic/entityCompanion.js +17 -1
- package/dist/generic/entityCompanion.js.map +1 -1
- package/dist/generic/entityConfiguration.d.ts +131 -0
- package/dist/generic/entityConfiguration.d.ts.map +1 -0
- package/dist/generic/entityConfiguration.js +346 -0
- package/dist/generic/entityConfiguration.js.map +1 -0
- package/dist/generic/entityInfo.d.ts +274 -4
- package/dist/generic/entityInfo.d.ts.map +1 -1
- package/dist/generic/entityInfo.js +502 -14
- package/dist/generic/entityInfo.js.map +1 -1
- package/dist/generic/entitySavePlan.d.ts +40 -11
- package/dist/generic/entitySavePlan.d.ts.map +1 -1
- package/dist/generic/entitySavePlan.js +70 -2
- package/dist/generic/entitySavePlan.js.map +1 -1
- package/dist/generic/interfaces.d.ts +6 -24
- package/dist/generic/interfaces.d.ts.map +1 -1
- package/dist/generic/interfaces.js +6 -24
- package/dist/generic/interfaces.js.map +1 -1
- package/dist/generic/localCacheManager.d.ts.map +1 -1
- package/dist/generic/localCacheManager.js +10 -0
- package/dist/generic/localCacheManager.js.map +1 -1
- package/dist/generic/providerBase.d.ts +19 -0
- package/dist/generic/providerBase.d.ts.map +1 -1
- package/dist/generic/providerBase.js +71 -6
- package/dist/generic/providerBase.js.map +1 -1
- package/dist/generic/relatedRecordCollection.d.ts +25 -3
- package/dist/generic/relatedRecordCollection.d.ts.map +1 -1
- package/dist/generic/relatedRecordCollection.js +53 -7
- package/dist/generic/relatedRecordCollection.js.map +1 -1
- package/dist/generic/runQuery.d.ts +14 -0
- package/dist/generic/runQuery.d.ts.map +1 -1
- package/dist/generic/runQuery.js.map +1 -1
- package/dist/generic/saveEntityGraphOperation.js +1 -1
- package/dist/generic/saveEntityGraphOperation.js.map +1 -1
- package/dist/generic/securityInfo.d.ts.map +1 -1
- package/dist/generic/securityInfo.js +15 -5
- package/dist/generic/securityInfo.js.map +1 -1
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -0
- package/dist/index.js.map +1 -1
- package/dist/views/runView.d.ts +23 -0
- package/dist/views/runView.d.ts.map +1 -1
- package/dist/views/runView.js +20 -0
- package/dist/views/runView.js.map +1 -1
- package/package.json +5 -4
- package/readme.md +20 -1
|
@@ -4,6 +4,7 @@ import { TypeScriptTypeFromSQLType, SQLFullType, SQLMaxLength, FormatValue, Code
|
|
|
4
4
|
import { IsFixedWidthStringSQLType } from "@memberjunction/sql-dialect";
|
|
5
5
|
import { LogError } from "./logging.js";
|
|
6
6
|
import { WarningManager, SafeJSONParse, UUIDsEqual } from "@memberjunction/global";
|
|
7
|
+
import { ReadRelationshipJoinFields, } from "./entityConfiguration.js";
|
|
7
8
|
/**
|
|
8
9
|
* The possible status values for a record change
|
|
9
10
|
*/
|
|
@@ -47,6 +48,41 @@ export class RecordChange extends BaseInfo {
|
|
|
47
48
|
* Defines foreign key relationships and how they should be represented in the user interface.
|
|
48
49
|
*/
|
|
49
50
|
export class EntityRelationshipInfo extends BaseInfo {
|
|
51
|
+
/**
|
|
52
|
+
* Optional JSON configuration bag (shape = {@link IEntityRelationshipConfiguration}).
|
|
53
|
+
* Nested `UI.inclusion` is Primary, More, or None (omit = Auto ranker).
|
|
54
|
+
* `UI.FormRole` is an accepted alias (`Detail` = More). Distinct from
|
|
55
|
+
* RelatedRecordCollection, DisplayComponentConfiguration, and AdditionalFieldsToInclude.
|
|
56
|
+
* Parsed lazily on first access and cached using {@link SafeJSONParse}.
|
|
57
|
+
*
|
|
58
|
+
* @see packages/MJCore/src/generic/entityConfiguration.ts
|
|
59
|
+
*/
|
|
60
|
+
get Configuration() {
|
|
61
|
+
if (this._configurationObject === undefined) {
|
|
62
|
+
this._configurationObject = this._configuration ? SafeJSONParse(this._configuration, false) : null;
|
|
63
|
+
}
|
|
64
|
+
return this._configurationObject;
|
|
65
|
+
}
|
|
66
|
+
set Configuration(value) {
|
|
67
|
+
if (typeof value === 'string') {
|
|
68
|
+
this._configuration = value;
|
|
69
|
+
this._configurationObject = undefined;
|
|
70
|
+
}
|
|
71
|
+
else if (value && typeof value === 'object') {
|
|
72
|
+
this._configurationObject = value;
|
|
73
|
+
this._configuration = JSON.stringify(value);
|
|
74
|
+
}
|
|
75
|
+
else {
|
|
76
|
+
this._configuration = null;
|
|
77
|
+
this._configurationObject = null;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Parsed {@link Configuration}. Null when the column is empty or not valid JSON.
|
|
82
|
+
*/
|
|
83
|
+
get ConfigurationObject() {
|
|
84
|
+
return this.Configuration;
|
|
85
|
+
}
|
|
50
86
|
constructor(initData) {
|
|
51
87
|
super();
|
|
52
88
|
this.ID = null;
|
|
@@ -79,6 +115,14 @@ export class EntityRelationshipInfo extends BaseInfo {
|
|
|
79
115
|
* * Description: Indicates whether this relationship should be automatically updated by CodeGen. When set to 0, the record will not be modified by CodeGen. Defaults to true.
|
|
80
116
|
*/
|
|
81
117
|
this.AutoUpdateFromSchema = true;
|
|
118
|
+
/**
|
|
119
|
+
* Comma-delimited list of extra related-entity fields to project in base views.
|
|
120
|
+
*/
|
|
121
|
+
this.AdditionalFieldsToInclude = null;
|
|
122
|
+
/**
|
|
123
|
+
* Whether CodeGen automatically updates AdditionalFieldsToInclude from schema metadata.
|
|
124
|
+
*/
|
|
125
|
+
this.AutoUpdateAdditionalFieldsToInclude = true;
|
|
82
126
|
/**
|
|
83
127
|
* * Field Name: RelatedRecordCollection
|
|
84
128
|
* * SQL Data Type: nvarchar(MAX), nullable
|
|
@@ -100,6 +144,11 @@ export class EntityRelationshipInfo extends BaseInfo {
|
|
|
100
144
|
* @see guides/TRANSACTIONS_AND_BATCHING_GUIDE.md
|
|
101
145
|
*/
|
|
102
146
|
this.RelatedRecordCollection = null;
|
|
147
|
+
/**
|
|
148
|
+
* Raw string representation of Configuration from metadata.
|
|
149
|
+
*/
|
|
150
|
+
this._configuration = null;
|
|
151
|
+
this._configurationObject = undefined;
|
|
103
152
|
// virtual fields - returned by the database VIEW
|
|
104
153
|
this.Entity = null;
|
|
105
154
|
this.EntityBaseTable = null;
|
|
@@ -379,6 +428,49 @@ export const GeneratedFormSectionType = {
|
|
|
379
428
|
* Includes data types, relationships, defaults, and UI display preferences.
|
|
380
429
|
*/
|
|
381
430
|
export class EntityFieldInfo extends BaseInfo {
|
|
431
|
+
/**
|
|
432
|
+
* Optional JSON configuration bag (shape = {@link IEntityFieldConfiguration}).
|
|
433
|
+
* Defines field-level configurations such as Hierarchy options (IsHierarchy, MaxDepth).
|
|
434
|
+
* Parsed lazily on first access and cached using {@link SafeJSONParse}.
|
|
435
|
+
*/
|
|
436
|
+
get Configuration() {
|
|
437
|
+
if (this._configurationObject === undefined) {
|
|
438
|
+
this._configurationObject = this._configuration ? SafeJSONParse(this._configuration, false) : null;
|
|
439
|
+
}
|
|
440
|
+
return this._configurationObject;
|
|
441
|
+
}
|
|
442
|
+
set Configuration(value) {
|
|
443
|
+
if (typeof value === 'string') {
|
|
444
|
+
this._configuration = value;
|
|
445
|
+
this._configurationObject = undefined;
|
|
446
|
+
}
|
|
447
|
+
else if (value && typeof value === 'object') {
|
|
448
|
+
this._configurationObject = value;
|
|
449
|
+
this._configuration = JSON.stringify(value);
|
|
450
|
+
}
|
|
451
|
+
else {
|
|
452
|
+
this._configuration = null;
|
|
453
|
+
this._configurationObject = null;
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
/**
|
|
457
|
+
* Parsed {@link Configuration}. Null when the column is empty or not valid JSON.
|
|
458
|
+
*/
|
|
459
|
+
get ConfigurationObject() {
|
|
460
|
+
return this.Configuration;
|
|
461
|
+
}
|
|
462
|
+
/**
|
|
463
|
+
* Returns true if this field is explicitly configured as an intentional recursive tree hierarchy.
|
|
464
|
+
*/
|
|
465
|
+
get IsHierarchy() {
|
|
466
|
+
return this.ConfigurationObject?.Hierarchy?.IsHierarchy === true;
|
|
467
|
+
}
|
|
468
|
+
/**
|
|
469
|
+
* Maximum recursion depth configured for this hierarchy field (defaults to 100).
|
|
470
|
+
*/
|
|
471
|
+
get HierarchyMaxDepth() {
|
|
472
|
+
return this.ConfigurationObject?.Hierarchy?.MaxDepth ?? 100;
|
|
473
|
+
}
|
|
382
474
|
/**
|
|
383
475
|
* JSON configuration for additional fields to join from the related entity.
|
|
384
476
|
* Parsed from the RelatedEntityJoinFields column. Uses lazy initialization and caching
|
|
@@ -432,6 +524,233 @@ export class EntityFieldInfo extends BaseInfo {
|
|
|
432
524
|
}
|
|
433
525
|
}
|
|
434
526
|
}
|
|
527
|
+
/**
|
|
528
|
+
* Upper bound on how many legal values a validation message enumerates before it truncates.
|
|
529
|
+
* A long list would otherwise produce an error message no user can read.
|
|
530
|
+
*/
|
|
531
|
+
static { this.MaxValueListValuesInErrorMessage = 25; }
|
|
532
|
+
/**
|
|
533
|
+
* Normalizes a value for comparison against a value list: stringified, trimmed, lower-cased.
|
|
534
|
+
*
|
|
535
|
+
* Each part earns its place, and the reasons are NOT equally strong — stated precisely, because
|
|
536
|
+
* a future maintainer will use this to decide whether to tighten the comparison:
|
|
537
|
+
* * **Lower-casing is load-bearing on MJ core itself.** Not merely defensive: the default-value
|
|
538
|
+
* path puts a field's SQL default into a new record (`EntityField`'s constructor assigns
|
|
539
|
+
* `DefaultValue` when no value is supplied), and two MJ core fields have a default that
|
|
540
|
+
* matches their value list by CASE ALONE — `MJ: Entity AI Actions`.TriggerEvent defaults to
|
|
541
|
+
* `'After Save'` against a list of `before save | after save`, and its OutputType defaults to
|
|
542
|
+
* `'FIeld'` against `entity | field`. Under a case-sensitive comparison, creating either
|
|
543
|
+
* record at its database default would fail validation. Separately, SQL Server's default
|
|
544
|
+
* collation is case-insensitive, so `Status = 'active'` is accepted by
|
|
545
|
+
* `CHECK (Status IN ('Active', ...))` and refusing it here would turn a save that succeeds
|
|
546
|
+
* today into a failure. (PostgreSQL IS case-sensitive, so on PG a case variant is still
|
|
547
|
+
* refused — by its CHECK, not by this rung.)
|
|
548
|
+
* * **Stringifying is required.** `EntityFieldValue.Value` is always a string in metadata while
|
|
549
|
+
* the field's runtime value may be a number, so a strict `===` would reject every legal value
|
|
550
|
+
* on a numeric list. It is not lossless: `String(1.0)` is `'1'`, so a metadata value written
|
|
551
|
+
* as `'1.0'` would fail closed. No numeric value lists exist today (CodeGen cannot produce
|
|
552
|
+
* one — see the note in ValueIsPermittedByValueList), so this is recorded rather than solved.
|
|
553
|
+
* * **Trimming is cheap insurance, NOT the load-bearing rule it was first documented as.** An
|
|
554
|
+
* earlier version of this comment claimed an untrimmed comparison would reject 9,301 existing
|
|
555
|
+
* rows in fixed-width `nchar` columns (`MJ: Action Params`.Type, `MJ: Record Changes`.Status
|
|
556
|
+
* and others). That measurement was taken over RAW SQL ROWS and does not describe this code
|
|
557
|
+
* path: `EntityField`'s value setter already strips trailing padding on fixed-width columns
|
|
558
|
+
* (see `FixedWidthColumn`), and hydration assigns through that setter, so the padding is gone
|
|
559
|
+
* before `Validate()` ever reads the value. Trimming is kept because it still covers LEADING
|
|
560
|
+
* whitespace, stray spaces on the metadata side, and any caller that assigns a padded value
|
|
561
|
+
* directly — none of which the setter handles.
|
|
562
|
+
*/
|
|
563
|
+
static NormalizeValueListValue(value) {
|
|
564
|
+
return String(value).trim().toLowerCase();
|
|
565
|
+
}
|
|
566
|
+
/**
|
|
567
|
+
* Whether `value` is permitted by this field's exhaustive value list (MJ issue #3969).
|
|
568
|
+
*
|
|
569
|
+
* A field whose `ValueListType` is `List` carries an exhaustive set of legal values in
|
|
570
|
+
* `__mj.EntityFieldValue`, and for an `IN (...)` CHECK constraint that list is the ONLY runtime
|
|
571
|
+
* representation CodeGen produces — `ParseCheckConstraints` emits the value list rather than a
|
|
572
|
+
* generated `Validate()` method, since the list is also what the UI needs to render a dropdown.
|
|
573
|
+
* So this is the only place such a constraint can be caught before the database refuses it as a
|
|
574
|
+
* raw violation attributed to no field.
|
|
575
|
+
*
|
|
576
|
+
* The normalized set is built ONCE per field and reused, because it derives from metadata that
|
|
577
|
+
* is immutable after load and is shared by every `EntityField` instance of this field — at
|
|
578
|
+
* import scale (thousands to millions of rows) rebuilding it per record is pure waste.
|
|
579
|
+
*
|
|
580
|
+
* Four boundaries keep the rule safe to apply everywhere:
|
|
581
|
+
* * `ListOrUserEntry` is never checked — that mode exists precisely to permit values outside
|
|
582
|
+
* the list, so validating it would break every field that opted into free text.
|
|
583
|
+
* * A `List` field with no `EntityFieldValue` rows permits everything. Strictly it describes
|
|
584
|
+
* a field where nothing is legal, which should never exist; it means the metadata is
|
|
585
|
+
* broken, not that every value is wrong, so this logs loudly (once per EntityFieldInfo
|
|
586
|
+
* instance, which means it re-arms after a metadata refresh rather than being once ever)
|
|
587
|
+
* and permits rather than failing every save on the field.
|
|
588
|
+
* * Null/undefined is the nullability check's job, so one mistake never produces two errors.
|
|
589
|
+
* An EMPTY or whitespace-only string is NOT absence: SQL Server pads on comparison, so
|
|
590
|
+
* `''` and `' '` are the same value to a CHECK constraint and it refuses both. Skipping
|
|
591
|
+
* them would leave a hole exactly where a blanked-out field lands.
|
|
592
|
+
* * Only string and number values are checked, and the gate fails OPEN — an unsupported type
|
|
593
|
+
* skips validation rather than manufacturing a failure. Nothing in the schema restricts
|
|
594
|
+
* which columns may carry a value list (`CK_EntityField_ValueListType_New` constrains the
|
|
595
|
+
* mode, not the column type), but in practice every one is a string column: measured on a
|
|
596
|
+
* current 6.x instance, 455 nvarchar + 7 nchar and nothing else, which follows from
|
|
597
|
+
* CodeGen's constraint parser only ever extracting quoted literals. `number` is admitted
|
|
598
|
+
* because the generated union type anticipates a non-quoted list via `NeedsQuotes`.
|
|
599
|
+
* Booleans and Dates are excluded deliberately: a bit column carrying a `'1'`/`'0'` list
|
|
600
|
+
* would see `String(true) === 'true'` and reject every legal value, and a Date has no sane
|
|
601
|
+
* string form to compare — so guessing there would break saves rather than guard them.
|
|
602
|
+
*
|
|
603
|
+
* TWO PRODUCERS, ONE OF WHICH HAS NO DATABASE FLOOR. A CHECK-derived list is safe by
|
|
604
|
+
* construction: the database refuses anything this rung refuses, so validating can only move a
|
|
605
|
+
* failure earlier. The other producer is `applyValueListConfig` in CodeGen, which applies
|
|
606
|
+
* DBAutoDoc's LLM enum detection from `additionalSchemaInfo` — those fields have NO CHECK
|
|
607
|
+
* constraint, so for them this rung converts a sampled, confidence-scored guess into a hard save
|
|
608
|
+
* refusal for any value the model did not see. It is opt-in (the config must exist) and arguably
|
|
609
|
+
* the intended reading of `List` as a closed set, with `ListOrUserEntry` available when unsure —
|
|
610
|
+
* but it means "MJ never refuses what the database would accept" holds for the first producer
|
|
611
|
+
* only.
|
|
612
|
+
*
|
|
613
|
+
* @param value the field's current runtime value
|
|
614
|
+
* @returns true when the value is permitted, INCLUDING when the rule does not apply
|
|
615
|
+
*/
|
|
616
|
+
ValueIsPermittedByValueList(value) {
|
|
617
|
+
if (this.ValueListTypeEnum !== EntityFieldValueListType.List) {
|
|
618
|
+
return true;
|
|
619
|
+
}
|
|
620
|
+
if (this._normalizedValueListValues === undefined) {
|
|
621
|
+
const values = this.EntityFieldValues ?? [];
|
|
622
|
+
this._normalizedValueListValues = new Set(values.map(v => EntityFieldInfo.NormalizeValueListValue(v.Value)));
|
|
623
|
+
}
|
|
624
|
+
// The broken-metadata report comes FIRST, before the null and type gates, so that it is
|
|
625
|
+
// value-independent: a `List` field with no values whose column happens to hold null (or a
|
|
626
|
+
// boolean) would otherwise never report at all, making "loud" mean "loud if someone happens
|
|
627
|
+
// to set a string". The cost is building one memoized set on a field that would build it
|
|
628
|
+
// anyway.
|
|
629
|
+
if (this._normalizedValueListValues.size === 0) {
|
|
630
|
+
if (!this._loggedEmptyValueList) {
|
|
631
|
+
this._loggedEmptyValueList = true; // latched: one report per field, not per row
|
|
632
|
+
LogError(`Entity field ${this.Entity}.${this.Name} has ValueListType='List' but no EntityFieldValue rows. ` +
|
|
633
|
+
`That describes a field where no value is legal, which is broken metadata rather than a rule, ` +
|
|
634
|
+
`so value-list validation is being SKIPPED for this field. Re-run CodeGen for the entity, or ` +
|
|
635
|
+
`set ValueListType='None' if the field is not meant to be constrained.`);
|
|
636
|
+
}
|
|
637
|
+
return true;
|
|
638
|
+
}
|
|
639
|
+
if (value === null || value === undefined) {
|
|
640
|
+
return true; // an unset nullable field or a new record — the nullability check's business
|
|
641
|
+
}
|
|
642
|
+
// Dates are compared on the calendar date rather than the string form — see
|
|
643
|
+
// dateValueIsPermittedByValueList. A `date` column CAN carry a value list: SQL Server stores
|
|
644
|
+
// `CHECK (D IN ('2026-01-01','2026-07-01'))` as quoted literals, which is exactly the shape
|
|
645
|
+
// CodeGen's parser captures, so this is a reachable case rather than a hypothetical one.
|
|
646
|
+
if (value instanceof Date) {
|
|
647
|
+
return this.dateValueIsPermittedByValueList(value);
|
|
648
|
+
}
|
|
649
|
+
if (typeof value !== 'string' && typeof value !== 'number') {
|
|
650
|
+
// The rule cannot be applied to this type at all, and that is a mismatch rather than a
|
|
651
|
+
// state to absorb: either the field should not declare a value list (one on a bit column
|
|
652
|
+
// — which CodeGen never produces, since SQL Server renders `IN (0,1)` as unquoted
|
|
653
|
+
// `([B]=(1) OR [B]=(0))`, the same reason no NUMERIC list exists either; see MJ #3978)
|
|
654
|
+
// or a caller assigned the wrong type. Skipping it silently would
|
|
655
|
+
// leave the caller believing a guard is on when it is not, which is the exact failure
|
|
656
|
+
// mode this rung was added to fix — so it is reported, once per field.
|
|
657
|
+
this.reportUnsupportedValueListValue(`a ${typeof value} value (SQL type ${this.SQLFullType})`, 'value-list validation compares strings, numbers and dates only — comparing anything ' +
|
|
658
|
+
'else would reject legal values rather than guard them');
|
|
659
|
+
return true;
|
|
660
|
+
}
|
|
661
|
+
return this._normalizedValueListValues.has(EntityFieldInfo.NormalizeValueListValue(value));
|
|
662
|
+
}
|
|
663
|
+
/**
|
|
664
|
+
* Value-list membership for a `Date` runtime value, compared on the CALENDAR DATE rather than
|
|
665
|
+
* the instant.
|
|
666
|
+
*
|
|
667
|
+
* Only `date` columns are compared. A column carrying a time component is deliberately skipped:
|
|
668
|
+
* the metadata value has no timezone, so deciding whether `'2026-01-01T00:00:00'` is the same
|
|
669
|
+
* instant as the value read back would mean guessing the database's interpretation, and guessing
|
|
670
|
+
* wrong rejects a legal value.
|
|
671
|
+
*
|
|
672
|
+
* For a `date` column the comparison accepts EITHER the value's UTC calendar date or its LOCAL
|
|
673
|
+
* one. That asymmetry is deliberate: a value read back from SQL Server arrives as UTC midnight,
|
|
674
|
+
* while application code that builds a date with `new Date(2026, 6, 1)` produces LOCAL midnight —
|
|
675
|
+
* whose UTC calendar date is the previous day west of Greenwich. Insisting on one representation
|
|
676
|
+
* would refuse legal values for half the world, so a date is in the list if either reading of it
|
|
677
|
+
* is. The cost is that an adjacent day can slip through when both days are in the list, which is
|
|
678
|
+
* a far better trade than a false failure.
|
|
679
|
+
*/
|
|
680
|
+
dateValueIsPermittedByValueList(value) {
|
|
681
|
+
if (this._valueListDateKeys === undefined) {
|
|
682
|
+
this._valueListDateKeys = this.buildValueListDateKeys();
|
|
683
|
+
}
|
|
684
|
+
if (this._valueListDateKeys === null) {
|
|
685
|
+
this.reportUnsupportedValueListValue(`a Date value (SQL type ${this.SQLFullType})`, 'only a `date` column whose value list is entirely yyyy-mm-dd literals can be ' +
|
|
686
|
+
'compared — anything else would require guessing how the database interprets a ' +
|
|
687
|
+
'time-zone-less literal');
|
|
688
|
+
return true;
|
|
689
|
+
}
|
|
690
|
+
if (Number.isNaN(value.getTime())) {
|
|
691
|
+
return true; // an Invalid Date is not an out-of-list value; leave it to the date check
|
|
692
|
+
}
|
|
693
|
+
return this._valueListDateKeys.has(value.toISOString().slice(0, 10)) ||
|
|
694
|
+
this._valueListDateKeys.has(EntityFieldInfo.LocalCalendarDate(value));
|
|
695
|
+
}
|
|
696
|
+
/**
|
|
697
|
+
* Builds the set of yyyy-mm-dd keys for a `date` field's value list, or null when the field is
|
|
698
|
+
* not a plain `date` column or any of its values is not a yyyy-mm-dd literal. Reads the already
|
|
699
|
+
* normalized values, so it costs no extra pass over the metadata (lower-casing cannot affect a
|
|
700
|
+
* numeric date literal).
|
|
701
|
+
*/
|
|
702
|
+
buildValueListDateKeys() {
|
|
703
|
+
if ((this.Type ?? '').trim().toLowerCase() !== 'date') {
|
|
704
|
+
return null;
|
|
705
|
+
}
|
|
706
|
+
const keys = new Set();
|
|
707
|
+
for (const normalized of this._normalizedValueListValues ?? []) {
|
|
708
|
+
const match = /^(\d{4}-\d{2}-\d{2})$/.exec(normalized);
|
|
709
|
+
if (!match) {
|
|
710
|
+
return null; // not a date list — refuse to guess rather than reject legal values
|
|
711
|
+
}
|
|
712
|
+
keys.add(match[1]);
|
|
713
|
+
}
|
|
714
|
+
return keys.size > 0 ? keys : null;
|
|
715
|
+
}
|
|
716
|
+
/** The Date's LOCAL calendar date as yyyy-mm-dd (its UTC one is `toISOString().slice(0, 10)`). */
|
|
717
|
+
static LocalCalendarDate(value) {
|
|
718
|
+
const pad = (n) => String(n).padStart(2, '0');
|
|
719
|
+
return `${value.getFullYear()}-${pad(value.getMonth() + 1)}-${pad(value.getDate())}`;
|
|
720
|
+
}
|
|
721
|
+
/**
|
|
722
|
+
* Reports, once per field, that a value could not be checked against the field's value list.
|
|
723
|
+
* Latched because the metadata is shared by every record: a bulk load would otherwise emit the
|
|
724
|
+
* same line per row, which is the noise the memoization elsewhere in this class exists to avoid.
|
|
725
|
+
*/
|
|
726
|
+
reportUnsupportedValueListValue(received, reason) {
|
|
727
|
+
if (this._loggedUnsupportedValueListType) {
|
|
728
|
+
return;
|
|
729
|
+
}
|
|
730
|
+
this._loggedUnsupportedValueListType = true;
|
|
731
|
+
LogError(`Entity field ${this.Entity}.${this.Name} has ValueListType='List' but was given ${received}: ` +
|
|
732
|
+
`${reason}, so value-list validation is being SKIPPED for this field. Either the field's ` +
|
|
733
|
+
`metadata should not declare a value list, or the caller is assigning the wrong type.`);
|
|
734
|
+
}
|
|
735
|
+
/**
|
|
736
|
+
* This field's legal values formatted for a validation message, truncated past
|
|
737
|
+
* {@link MaxValueListValuesInErrorMessage}. Memoized — a bad bulk load fails on the same field
|
|
738
|
+
* repeatedly, and the string never changes.
|
|
739
|
+
*/
|
|
740
|
+
get ValueListValuesForDisplay() {
|
|
741
|
+
if (this._valueListDisplayCache !== undefined) {
|
|
742
|
+
return this._valueListDisplayCache;
|
|
743
|
+
}
|
|
744
|
+
// De-duplicated: EntityFieldValue rows are not unique in practice (`MJ: Action Params`.ValueType
|
|
745
|
+
// carries repeats today), and listing the same value twice — or double-counting it in the
|
|
746
|
+
// "(N total)" tail — makes the message look like a bug in the message.
|
|
747
|
+
const values = [...new Set((this.EntityFieldValues ?? []).map(v => v.Value))];
|
|
748
|
+
const max = EntityFieldInfo.MaxValueListValuesInErrorMessage;
|
|
749
|
+
this._valueListDisplayCache = values.length > max ?
|
|
750
|
+
`${values.slice(0, max).join(', ')}, ... (${values.length} total)` :
|
|
751
|
+
values.join(', ');
|
|
752
|
+
return this._valueListDisplayCache;
|
|
753
|
+
}
|
|
435
754
|
get GeneratedFormSectionType() {
|
|
436
755
|
return GeneratedFormSectionType[this.GeneratedFormSection];
|
|
437
756
|
}
|
|
@@ -753,6 +1072,18 @@ export class EntityFieldInfo extends BaseInfo {
|
|
|
753
1072
|
*/
|
|
754
1073
|
this.DisplayName = null;
|
|
755
1074
|
this.Description = null;
|
|
1075
|
+
/**
|
|
1076
|
+
* Whether CodeGen automatically updates Description from the underlying schema object.
|
|
1077
|
+
*/
|
|
1078
|
+
this.AutoUpdateDescription = true;
|
|
1079
|
+
/**
|
|
1080
|
+
* Whether CodeGen automatically updates UserSearchPredicateAPI from schema heuristics.
|
|
1081
|
+
*/
|
|
1082
|
+
this.AutoUpdateUserSearchPredicate = true;
|
|
1083
|
+
/**
|
|
1084
|
+
* Whether CodeGen automatically updates FullTextSearchEnabled from schema indexes.
|
|
1085
|
+
*/
|
|
1086
|
+
this.AutoUpdateFullTextSearch = true;
|
|
756
1087
|
/**
|
|
757
1088
|
* If true, the field is the primary key for the entity. There must be one primary key field per entity.
|
|
758
1089
|
*/
|
|
@@ -824,6 +1155,22 @@ export class EntityFieldInfo extends BaseInfo {
|
|
|
824
1155
|
* Parsed from the RelatedEntityJoinFields column.
|
|
825
1156
|
*/
|
|
826
1157
|
this.RelatedEntityJoinFields = null;
|
|
1158
|
+
/**
|
|
1159
|
+
* Optional JSON policy object declaring this foreign-key field as a first-class
|
|
1160
|
+
* **embedded record** — a 1:1 peer that loads, validates and persists as one
|
|
1161
|
+
* unit with its owner. Shape is `IEmbeddedRecordConfig` (`OnClear`, `LoadNested`).
|
|
1162
|
+
*
|
|
1163
|
+
* `RelatedEntityID` and this field's `Name` are the join; they are deliberately
|
|
1164
|
+
* not repeated inside the JSON. `AllowsNull` on this same field decides whether
|
|
1165
|
+
* `GetEntityObject` provisions the object (required FK) or the caller uses
|
|
1166
|
+
* `{FieldName}_EnsureObject()` (nullable FK).
|
|
1167
|
+
*
|
|
1168
|
+
* `null` (the default, and every pre-feature row) means the field is an ordinary
|
|
1169
|
+
* FK: nothing is generated and nothing is constructed at `GetEntityObject` time.
|
|
1170
|
+
*
|
|
1171
|
+
* @see packages/MJCore/docs/embedded-records.md
|
|
1172
|
+
*/
|
|
1173
|
+
this.EmbeddedRecord = null;
|
|
827
1174
|
/**
|
|
828
1175
|
* The name of the TypeScript interface/type for this JSON field.
|
|
829
1176
|
* When set, CodeGen will emit a strongly-typed getter/setter using this type
|
|
@@ -841,6 +1188,11 @@ export class EntityFieldInfo extends BaseInfo {
|
|
|
841
1188
|
* Can include imports, multiple types, or any valid TypeScript.
|
|
842
1189
|
*/
|
|
843
1190
|
this.JSONTypeDefinition = null;
|
|
1191
|
+
/**
|
|
1192
|
+
* Raw string representation of Configuration from metadata.
|
|
1193
|
+
*/
|
|
1194
|
+
this._configuration = null;
|
|
1195
|
+
this._configurationObject = undefined;
|
|
844
1196
|
this.RelatedEntityDisplayType = null;
|
|
845
1197
|
this.EntityIDFieldName = null;
|
|
846
1198
|
this.__mj_CreatedAt = null;
|
|
@@ -1040,6 +1392,24 @@ export class EntityFieldInfo extends BaseInfo {
|
|
|
1040
1392
|
this.RelatedEntityClassName = null;
|
|
1041
1393
|
this._rawEntityFieldValues = null;
|
|
1042
1394
|
this._entityFieldValuesConstructed = false;
|
|
1395
|
+
/**
|
|
1396
|
+
* Memoized state for value-list validation. NOTE THE NAMES: none of these may be the getter's
|
|
1397
|
+
* name minus its underscore. `BaseInfo.toJSON` walks own keys, and for a `_`-prefixed one it
|
|
1398
|
+
* looks for a public getter of the matching PascalCase name and serializes THROUGH it. A memo
|
|
1399
|
+
* called `_valueListValuesForDisplay` would therefore add `ValueListValuesForDisplay` to every
|
|
1400
|
+
* serialized field — including the ~5,700 MJ core fields with no value list — bloating the
|
|
1401
|
+
* browser metadata cache and forcing EntityFieldValues hydration purely to serialize a string
|
|
1402
|
+
* that only ever appears in an error message. Hence `_valueListDisplayCache`.
|
|
1403
|
+
*/
|
|
1404
|
+
this._normalizedValueListValues = undefined;
|
|
1405
|
+
/** Memoized message form of the value list, built on first validation failure. */
|
|
1406
|
+
this._valueListDisplayCache = undefined;
|
|
1407
|
+
/** Latches the broken-metadata error so a bulk load cannot emit it once per row. */
|
|
1408
|
+
this._loggedEmptyValueList = false;
|
|
1409
|
+
/** Latches the unsupported-value-type error for the same reason. */
|
|
1410
|
+
this._loggedUnsupportedValueListType = false;
|
|
1411
|
+
/** Memoized yyyy-mm-dd keys for a `date` field's value list; null when it cannot be compared. */
|
|
1412
|
+
this._valueListDateKeys = undefined;
|
|
1043
1413
|
/**
|
|
1044
1414
|
* Cached parsed RelatedEntityJoinFieldsConfig to avoid repeated JSON.parse calls.
|
|
1045
1415
|
* Lazy-initialized on first access.
|
|
@@ -1207,6 +1577,40 @@ export class EntityInfo extends BaseInfo {
|
|
|
1207
1577
|
const layered = this.GeneratedBaseViewName?.trim();
|
|
1208
1578
|
return !!layered && layered.toLowerCase() !== (this.BaseView ?? '').toLowerCase();
|
|
1209
1579
|
}
|
|
1580
|
+
/**
|
|
1581
|
+
* Optional JSON configuration bag (shape = {@link IEntityConfiguration}).
|
|
1582
|
+
* Nested `UI.Form` holds generated-form chrome: layout, auto left-nav
|
|
1583
|
+
* threshold, related-role policy, and the Primary related budget.
|
|
1584
|
+
* Parsed lazily on first access and cached using {@link SafeJSONParse}.
|
|
1585
|
+
*
|
|
1586
|
+
* @see packages/MJCore/src/generic/entityConfiguration.ts
|
|
1587
|
+
*/
|
|
1588
|
+
get Configuration() {
|
|
1589
|
+
if (this._configurationObject === undefined) {
|
|
1590
|
+
this._configurationObject = this._configuration ? SafeJSONParse(this._configuration, false) : null;
|
|
1591
|
+
}
|
|
1592
|
+
return this._configurationObject;
|
|
1593
|
+
}
|
|
1594
|
+
set Configuration(value) {
|
|
1595
|
+
if (typeof value === 'string') {
|
|
1596
|
+
this._configuration = value;
|
|
1597
|
+
this._configurationObject = undefined;
|
|
1598
|
+
}
|
|
1599
|
+
else if (value && typeof value === 'object') {
|
|
1600
|
+
this._configurationObject = value;
|
|
1601
|
+
this._configuration = JSON.stringify(value);
|
|
1602
|
+
}
|
|
1603
|
+
else {
|
|
1604
|
+
this._configuration = null;
|
|
1605
|
+
this._configurationObject = null;
|
|
1606
|
+
}
|
|
1607
|
+
}
|
|
1608
|
+
/**
|
|
1609
|
+
* Parsed {@link Configuration}. Null when the column is empty or not valid JSON.
|
|
1610
|
+
*/
|
|
1611
|
+
get ConfigurationObject() {
|
|
1612
|
+
return this.Configuration;
|
|
1613
|
+
}
|
|
1210
1614
|
/**
|
|
1211
1615
|
* O(1) case-insensitive field lookup by name. Use this instead of `Fields.find(f => f.Name === name)`
|
|
1212
1616
|
* on hot paths — it builds a lowercased+trimmed `Map` once (lazily) and reuses it.
|
|
@@ -1784,6 +2188,16 @@ export class EntityInfo extends BaseInfo {
|
|
|
1784
2188
|
* @returns
|
|
1785
2189
|
*/
|
|
1786
2190
|
static BuildRelationshipViewParams(record, relationship, filter, maxRecords) {
|
|
2191
|
+
const joinFields = ReadRelationshipJoinFields(relationship.Configuration);
|
|
2192
|
+
if (joinFields && joinFields.length > 1) {
|
|
2193
|
+
const multi = EntityInfo.BuildRelationshipViewParamsForJoinFields(record, relationship.RelatedEntity, joinFields);
|
|
2194
|
+
if (filter && filter.length > 0 && multi.ExtraFilter) {
|
|
2195
|
+
multi.ExtraFilter = `(${multi.ExtraFilter}) AND (${filter})`;
|
|
2196
|
+
}
|
|
2197
|
+
if (maxRecords && maxRecords > 0)
|
|
2198
|
+
multi.MaxRows = maxRecords;
|
|
2199
|
+
return multi;
|
|
2200
|
+
}
|
|
1787
2201
|
const params = {};
|
|
1788
2202
|
let quotes = '';
|
|
1789
2203
|
let keyValue = '';
|
|
@@ -1821,20 +2235,72 @@ export class EntityInfo extends BaseInfo {
|
|
|
1821
2235
|
return params;
|
|
1822
2236
|
}
|
|
1823
2237
|
/**
|
|
1824
|
-
*
|
|
1825
|
-
|
|
2238
|
+
* One related-entity grid over several join fields (Bill-To OR Ship-To).
|
|
2239
|
+
*/
|
|
2240
|
+
static BuildRelationshipViewParamsForJoinFields(record, relatedEntityName, joinFields) {
|
|
2241
|
+
const fields = joinFields.map((f) => f.trim()).filter((f) => f.length > 0);
|
|
2242
|
+
if (fields.length === 0)
|
|
2243
|
+
return { EntityName: relatedEntityName };
|
|
2244
|
+
if (fields.length === 1) {
|
|
2245
|
+
const rel = record.EntityInfo.RelatedEntities.find((r) => r.RelatedEntity.trim().toLowerCase() === relatedEntityName.trim().toLowerCase()
|
|
2246
|
+
&& r.RelatedEntityJoinField.trim().toLowerCase() === fields[0].toLowerCase());
|
|
2247
|
+
if (rel)
|
|
2248
|
+
return EntityInfo.BuildRelationshipViewParams(record, rel);
|
|
2249
|
+
}
|
|
2250
|
+
const firstKey = record.FirstPrimaryKey;
|
|
2251
|
+
const keyValue = firstKey.Value;
|
|
2252
|
+
const quotes = keyValue && firstKey.NeedsQuotes ? "'" : '';
|
|
2253
|
+
const clauses = fields.map((field) => `[${field}] = ${quotes}${keyValue}${quotes}`);
|
|
2254
|
+
return {
|
|
2255
|
+
EntityName: relatedEntityName,
|
|
2256
|
+
ExtraFilter: clauses.join(' OR '),
|
|
2257
|
+
};
|
|
2258
|
+
}
|
|
2259
|
+
/**
|
|
2260
|
+
* Default field values for a new related record so it links back to `record`.
|
|
2261
|
+
* When the relationship's Configuration declares `UI.join.fields`, every
|
|
2262
|
+
* listed FK is set (Bill-To AND Ship-To). Otherwise only
|
|
2263
|
+
* `RelatedEntityJoinField` is set.
|
|
1826
2264
|
*/
|
|
1827
2265
|
static BuildRelationshipNewRecordValues(record, relationship) {
|
|
1828
|
-
|
|
1829
|
-
|
|
1830
|
-
|
|
2266
|
+
if (!record || !relationship)
|
|
2267
|
+
return {};
|
|
2268
|
+
const joinFields = ReadRelationshipJoinFields(relationship.Configuration);
|
|
2269
|
+
if (joinFields && joinFields.length > 0) {
|
|
2270
|
+
return EntityInfo.BuildRelationshipNewRecordValuesForJoinFields(record, joinFields, relationship);
|
|
2271
|
+
}
|
|
2272
|
+
const joinField = (relationship.RelatedEntityJoinField ?? '').trim();
|
|
2273
|
+
if (!joinField)
|
|
2274
|
+
return {};
|
|
2275
|
+
return { [joinField]: EntityInfo.resolveRelationshipKeyValue(record, relationship) };
|
|
2276
|
+
}
|
|
2277
|
+
/**
|
|
2278
|
+
* Default field values for a new related record, setting every listed join
|
|
2279
|
+
* field to the parent key. Use this when one grid filters on several FKs
|
|
2280
|
+
* (Bill-To OR Ship-To) so "New" still auto-links the child to this parent.
|
|
2281
|
+
*/
|
|
2282
|
+
static BuildRelationshipNewRecordValuesForJoinFields(record, joinFields, relationship) {
|
|
2283
|
+
if (!record)
|
|
2284
|
+
return {};
|
|
2285
|
+
const fields = joinFields.map((f) => f.trim()).filter((f) => f.length > 0);
|
|
2286
|
+
if (fields.length === 0)
|
|
2287
|
+
return {};
|
|
2288
|
+
const keyValue = EntityInfo.resolveRelationshipKeyValue(record, relationship);
|
|
1831
2289
|
const obj = {};
|
|
1832
|
-
|
|
1833
|
-
|
|
1834
|
-
obj[relationship.RelatedEntityJoinField] = record.Get(keyField);
|
|
2290
|
+
for (const field of fields) {
|
|
2291
|
+
obj[field] = keyValue;
|
|
1835
2292
|
}
|
|
1836
2293
|
return obj;
|
|
1837
2294
|
}
|
|
2295
|
+
static resolveRelationshipKeyValue(record, relationship) {
|
|
2296
|
+
const explicit = relationship?.EntityKeyField?.trim();
|
|
2297
|
+
if (explicit)
|
|
2298
|
+
return record.Get(explicit);
|
|
2299
|
+
const first = record.FirstPrimaryKey;
|
|
2300
|
+
if (first?.Name)
|
|
2301
|
+
return record.Get(first.Name);
|
|
2302
|
+
return first?.Value;
|
|
2303
|
+
}
|
|
1838
2304
|
/**
|
|
1839
2305
|
* Returns a RunViewParams object configured to query the related entity matched via an organic key.
|
|
1840
2306
|
* Supports both direct field matching and transitive matching via SQL views/tables.
|
|
@@ -1946,8 +2412,11 @@ export class EntityInfo extends BaseInfo {
|
|
|
1946
2412
|
// Fall back to exact match if no custom expression defined
|
|
1947
2413
|
return `${fieldExpression} = '${escapedValue}'`;
|
|
1948
2414
|
}
|
|
1949
|
-
|
|
1950
|
-
|
|
2415
|
+
// Replacement functions: `escapedValue` is a data value, so `$&`/`` $` ``/
|
|
2416
|
+
// `$'`/`$$` in it would otherwise splice the custom expression's own text
|
|
2417
|
+
// into the SQL literal. See issue #3171.
|
|
2418
|
+
const normalizedField = expr.replace(/\{\{FieldName\}\}/g, () => fieldExpression);
|
|
2419
|
+
const normalizedValue = expr.replace(/\{\{FieldName\}\}/g, () => `'${escapedValue}'`);
|
|
1951
2420
|
return `${normalizedField} = ${normalizedValue}`;
|
|
1952
2421
|
}
|
|
1953
2422
|
default:
|
|
@@ -1976,7 +2445,8 @@ export class EntityInfo extends BaseInfo {
|
|
|
1976
2445
|
const expr = organicKey.CustomNormalizationExpression;
|
|
1977
2446
|
if (!expr)
|
|
1978
2447
|
return fieldExpression;
|
|
1979
|
-
|
|
2448
|
+
// Replacement function — see WrapWithNormalization (#3171).
|
|
2449
|
+
return expr.replace(/\{\{FieldName\}\}/g, () => fieldExpression);
|
|
1980
2450
|
}
|
|
1981
2451
|
default: return fieldExpression;
|
|
1982
2452
|
}
|
|
@@ -1991,7 +2461,8 @@ export class EntityInfo extends BaseInfo {
|
|
|
1991
2461
|
const expr = organicKey.CustomNormalizationExpression;
|
|
1992
2462
|
if (!expr)
|
|
1993
2463
|
return `'${escapedValue}'`;
|
|
1994
|
-
|
|
2464
|
+
// Replacement function — see WrapWithNormalization (#3171).
|
|
2465
|
+
return expr.replace(/\{\{FieldName\}\}/g, () => `'${escapedValue}'`);
|
|
1995
2466
|
}
|
|
1996
2467
|
default: return `'${escapedValue}'`;
|
|
1997
2468
|
}
|
|
@@ -2200,9 +2671,21 @@ export class EntityInfo extends BaseInfo {
|
|
|
2200
2671
|
*/
|
|
2201
2672
|
this.AllowUserSearchAPI = false;
|
|
2202
2673
|
/**
|
|
2203
|
-
* Whether full
|
|
2674
|
+
* Whether full text search is enabled for this entity
|
|
2204
2675
|
*/
|
|
2205
|
-
this.FullTextSearchEnabled =
|
|
2676
|
+
this.FullTextSearchEnabled = null;
|
|
2677
|
+
/**
|
|
2678
|
+
* Whether CodeGen automatically updates FullTextSearchEnabled from database catalog/index availability.
|
|
2679
|
+
*/
|
|
2680
|
+
this.AutoUpdateFullTextSearch = true;
|
|
2681
|
+
/**
|
|
2682
|
+
* Whether CodeGen automatically updates AllowUserSearchAPI from schema rules.
|
|
2683
|
+
*/
|
|
2684
|
+
this.AutoUpdateAllowUserSearchAPI = true;
|
|
2685
|
+
/**
|
|
2686
|
+
* Whether external changes to records are detected.
|
|
2687
|
+
*/
|
|
2688
|
+
this.DetectExternalChanges = false;
|
|
2206
2689
|
/**
|
|
2207
2690
|
* Name of the SQL Server full-text catalog used for searching
|
|
2208
2691
|
*/
|
|
@@ -2306,6 +2789,11 @@ export class EntityInfo extends BaseInfo {
|
|
|
2306
2789
|
* CSS class or icon identifier for displaying this entity in the UI
|
|
2307
2790
|
*/
|
|
2308
2791
|
this.Icon = null;
|
|
2792
|
+
/**
|
|
2793
|
+
* Raw string representation of Configuration from metadata.
|
|
2794
|
+
*/
|
|
2795
|
+
this._configuration = null;
|
|
2796
|
+
this._configurationObject = undefined;
|
|
2309
2797
|
/**
|
|
2310
2798
|
* Date and time when this entity was created
|
|
2311
2799
|
*/
|