@memberjunction/core 6.1.0-edge.5 → 6.1.0-edge.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/generic/JSONType-interfaces/IEntitySubtypeSelectorConfig.d.ts +27 -0
- package/dist/generic/JSONType-interfaces/IEntitySubtypeSelectorConfig.d.ts.map +1 -0
- package/dist/generic/JSONType-interfaces/IEntitySubtypeSelectorConfig.js +6 -0
- package/dist/generic/JSONType-interfaces/IEntitySubtypeSelectorConfig.js.map +1 -0
- package/dist/generic/baseEngine.d.ts +52 -1
- package/dist/generic/baseEngine.d.ts.map +1 -1
- package/dist/generic/baseEngine.js +95 -7
- package/dist/generic/baseEngine.js.map +1 -1
- package/dist/generic/baseEntity.d.ts +292 -0
- package/dist/generic/baseEntity.d.ts.map +1 -1
- package/dist/generic/baseEntity.js +878 -98
- package/dist/generic/baseEntity.js.map +1 -1
- package/dist/generic/baseInfo.d.ts +6 -0
- package/dist/generic/baseInfo.d.ts.map +1 -1
- package/dist/generic/baseInfo.js +19 -1
- package/dist/generic/baseInfo.js.map +1 -1
- package/dist/generic/compositeKey.d.ts +108 -0
- package/dist/generic/compositeKey.d.ts.map +1 -1
- package/dist/generic/compositeKey.js +225 -10
- package/dist/generic/compositeKey.js.map +1 -1
- package/dist/generic/databaseProviderBase.d.ts +49 -0
- package/dist/generic/databaseProviderBase.d.ts.map +1 -1
- package/dist/generic/databaseProviderBase.js +90 -11
- package/dist/generic/databaseProviderBase.js.map +1 -1
- package/dist/generic/embeddedRecord.d.ts +2 -1
- package/dist/generic/embeddedRecord.d.ts.map +1 -1
- package/dist/generic/embeddedRecord.js +9 -2
- package/dist/generic/embeddedRecord.js.map +1 -1
- package/dist/generic/entityCompanion.d.ts +6 -1
- package/dist/generic/entityCompanion.d.ts.map +1 -1
- package/dist/generic/entityCompanion.js +7 -0
- package/dist/generic/entityCompanion.js.map +1 -1
- package/dist/generic/entityInfo.d.ts +550 -8
- package/dist/generic/entityInfo.d.ts.map +1 -1
- package/dist/generic/entityInfo.js +695 -20
- package/dist/generic/entityInfo.js.map +1 -1
- package/dist/generic/entitySubtypeResolver.d.ts +19 -0
- package/dist/generic/entitySubtypeResolver.d.ts.map +1 -0
- package/dist/generic/entitySubtypeResolver.js +24 -0
- package/dist/generic/entitySubtypeResolver.js.map +1 -0
- package/dist/generic/entityTransactionScope.d.ts +6 -3
- package/dist/generic/entityTransactionScope.d.ts.map +1 -1
- package/dist/generic/entityTransactionScope.js +6 -3
- package/dist/generic/entityTransactionScope.js.map +1 -1
- package/dist/generic/extendedTypeValue.d.ts +47 -0
- package/dist/generic/extendedTypeValue.d.ts.map +1 -0
- package/dist/generic/extendedTypeValue.js +152 -0
- package/dist/generic/extendedTypeValue.js.map +1 -0
- package/dist/generic/filters/compositeFilter.d.ts +77 -0
- package/dist/generic/filters/compositeFilter.d.ts.map +1 -0
- package/dist/generic/filters/compositeFilter.js +324 -0
- package/dist/generic/filters/compositeFilter.js.map +1 -0
- package/dist/generic/filters/filter.types.d.ts +29 -0
- package/dist/generic/filters/filter.types.d.ts.map +1 -0
- package/dist/generic/filters/filter.types.js +28 -0
- package/dist/generic/filters/filter.types.js.map +1 -0
- package/dist/generic/filters/index.d.ts +3 -0
- package/dist/generic/filters/index.d.ts.map +1 -0
- package/dist/generic/filters/index.js +3 -0
- package/dist/generic/filters/index.js.map +1 -0
- package/dist/generic/geoFields.d.ts +64 -0
- package/dist/generic/geoFields.d.ts.map +1 -0
- package/dist/generic/geoFields.js +108 -0
- package/dist/generic/geoFields.js.map +1 -0
- package/dist/generic/interfaces.d.ts +16 -2
- package/dist/generic/interfaces.d.ts.map +1 -1
- package/dist/generic/interfaces.js.map +1 -1
- package/dist/generic/localCacheManager.d.ts +34 -3
- package/dist/generic/localCacheManager.d.ts.map +1 -1
- package/dist/generic/localCacheManager.js +53 -4
- package/dist/generic/localCacheManager.js.map +1 -1
- package/dist/generic/providerBase.d.ts +282 -5
- package/dist/generic/providerBase.d.ts.map +1 -1
- package/dist/generic/providerBase.js +754 -93
- package/dist/generic/providerBase.js.map +1 -1
- package/dist/generic/recordChangeFieldSecurity.d.ts +164 -0
- package/dist/generic/recordChangeFieldSecurity.d.ts.map +1 -0
- package/dist/generic/recordChangeFieldSecurity.js +279 -0
- package/dist/generic/recordChangeFieldSecurity.js.map +1 -0
- package/dist/generic/relatedRecordBatchLoader.js +2 -2
- package/dist/generic/relatedRecordBatchLoader.js.map +1 -1
- package/dist/generic/relatedRecordCollection.d.ts +2 -1
- package/dist/generic/relatedRecordCollection.d.ts.map +1 -1
- package/dist/generic/relatedRecordCollection.js +16 -4
- package/dist/generic/relatedRecordCollection.js.map +1 -1
- package/dist/generic/saveEntityGraphOperation.d.ts +10 -0
- package/dist/generic/saveEntityGraphOperation.d.ts.map +1 -1
- package/dist/generic/saveEntityGraphOperation.js +2 -1
- package/dist/generic/saveEntityGraphOperation.js.map +1 -1
- package/dist/generic/wellKnownUserSource.d.ts +70 -0
- package/dist/generic/wellKnownUserSource.d.ts.map +1 -0
- package/dist/generic/wellKnownUserSource.js +82 -0
- package/dist/generic/wellKnownUserSource.js.map +1 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +7 -0
- package/dist/index.js.map +1 -1
- package/package.json +3 -3
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
import { EntityInfo } from "./entityInfo.js";
|
|
2
|
+
import { UserInfo } from "./securityInfo.js";
|
|
3
|
+
/**
|
|
4
|
+
* The two metadata lookups this projector needs, and nothing else.
|
|
5
|
+
*
|
|
6
|
+
* Narrower than `IMetadataProvider` on purpose: both `ProviderBase` (which passes `this`) and
|
|
7
|
+
* `Metadata` (which the GraphQL resolvers fall back to) satisfy this, while only the first
|
|
8
|
+
* satisfies the full provider interface. Asking for exactly what is used keeps the resolver half
|
|
9
|
+
* from having to invent a provider it does not have.
|
|
10
|
+
*/
|
|
11
|
+
export interface RecordChangeMetadataSource {
|
|
12
|
+
readonly Entities: EntityInfo[];
|
|
13
|
+
EntityByID(entityID: string): EntityInfo | undefined;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* The audit entity whose rows carry OTHER entities' field values. Matched by name rather than by
|
|
17
|
+
* ID because every other enforcement point in this feature keys off the entity name too, and a
|
|
18
|
+
* name is the thing a reader can check against metadata without a lookup.
|
|
19
|
+
*/
|
|
20
|
+
export declare const RecordChangesEntityName = "MJ: Record Changes";
|
|
21
|
+
/**
|
|
22
|
+
* The Record Change column naming the entity a row is ABOUT. Everything in this module hangs off
|
|
23
|
+
* it: it is what makes the denied set per-ROW rather than per-result.
|
|
24
|
+
*/
|
|
25
|
+
export declare const RecordChangeEntityIDField = "EntityID";
|
|
26
|
+
/**
|
|
27
|
+
* Payload columns holding a JSON object keyed by the target entity's field names — projected
|
|
28
|
+
* key by key against the denied set. `ChangesJSON` is `{ [field]: { field, oldValue, newValue } }`;
|
|
29
|
+
* `FullRecordJSON` is a flat whole-row snapshot. Both are top-level-keyed by field name, which is
|
|
30
|
+
* what lets one projection handle them.
|
|
31
|
+
*/
|
|
32
|
+
export declare const RecordChangeJSONPayloadFields: readonly ["ChangesJSON", "FullRecordJSON"];
|
|
33
|
+
/**
|
|
34
|
+
* The human-prose payload column — withheld outright, never redacted. See
|
|
35
|
+
* {@link RecordChangeFieldSecurityProjector}.
|
|
36
|
+
*/
|
|
37
|
+
export declare const RecordChangeProsePayloadField = "ChangesDescription";
|
|
38
|
+
/**
|
|
39
|
+
* Every payload column this module touches, for callers that need to act on the set as a whole —
|
|
40
|
+
* notably the write guard, which must refuse a narrowed value coming back in.
|
|
41
|
+
*/
|
|
42
|
+
export declare const RecordChangePayloadFields: readonly string[];
|
|
43
|
+
/**
|
|
44
|
+
* Outcome of projecting one JSON payload column: either a value to write back, or a decision to
|
|
45
|
+
* withhold the column entirely because the payload could not be inspected.
|
|
46
|
+
*
|
|
47
|
+
* Deliberately an interface with an optional `Value` rather than a discriminated union — MJCore
|
|
48
|
+
* compiles without `strictNullChecks`, under which TypeScript does not narrow a union on a
|
|
49
|
+
* boolean discriminant, so the union shape would force a cast at every read.
|
|
50
|
+
*/
|
|
51
|
+
export interface RecordChangePayloadProjection {
|
|
52
|
+
/** When true the caller drops the column; `Value` carries nothing. */
|
|
53
|
+
Withhold: boolean;
|
|
54
|
+
/** The value to write back. Meaningful only when {@link Withhold} is false. */
|
|
55
|
+
Value?: unknown;
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Narrows one Record Change JSON payload to the fields the caller may read.
|
|
59
|
+
*
|
|
60
|
+
* **Withholds rather than guesses.** A payload that is not a string, or is a string that does not
|
|
61
|
+
* parse, or parses to something other than a plain object, is a payload this cannot prove is
|
|
62
|
+
* clean — so the column is dropped instead of passed through. A null/undefined/empty value is
|
|
63
|
+
* kept as-is: there is nothing stored to leak, and `ChangesJSON` is legitimately `''` on rows
|
|
64
|
+
* whose diff produced nothing.
|
|
65
|
+
*
|
|
66
|
+
* @param raw the column's stored value
|
|
67
|
+
* @param deniedLowercase field names the caller may not read, already lowercased
|
|
68
|
+
*/
|
|
69
|
+
export declare function ProjectRecordChangePayloadJSON(raw: unknown, deniedLowercase: Set<string>): RecordChangePayloadProjection;
|
|
70
|
+
/**
|
|
71
|
+
* Field-level security for the payload columns of `MJ: Record Changes`.
|
|
72
|
+
*
|
|
73
|
+
* ## Why this is not `ProviderBase.ApplyFieldSecurityProjection`
|
|
74
|
+
*
|
|
75
|
+
* Every other FLS enforcement point computes the denied set against **the entity being read**. A
|
|
76
|
+
* Record Change row is about a DIFFERENT entity — the one named by its own `EntityID` column —
|
|
77
|
+
* and `MJ: Record Changes` itself has field security switched off in every default deployment.
|
|
78
|
+
* The main projection therefore short-circuits on `EnableFieldLevelSecurity` and returns the row
|
|
79
|
+
* untouched, with a denied field's old and new values sitting in `ChangesJSON` in plain text.
|
|
80
|
+
* That is the leak this closes: a user with entity-level read on the audit trail could read a
|
|
81
|
+
* salary they were denied on `MJ: Employees` straight out of it, in the default configuration.
|
|
82
|
+
*
|
|
83
|
+
* ## What happens to each column
|
|
84
|
+
*
|
|
85
|
+
* | Column | Treatment |
|
|
86
|
+
* |---|---|
|
|
87
|
+
* | `ChangesJSON` | projected — denied field keys dropped |
|
|
88
|
+
* | `FullRecordJSON` | projected — same, it is a whole-row snapshot |
|
|
89
|
+
* | `ChangesDescription` | **withheld entirely** whenever the caller is denied anything on the target entity |
|
|
90
|
+
*
|
|
91
|
+
* `ChangesDescription` is human prose ("Salary changed from 100000 to 120000"). Redacting prose
|
|
92
|
+
* on the fly leaks on the first value that appears in an unexpected form, and regenerating it
|
|
93
|
+
* from the filtered JSON is a larger surface than this is worth. Withholding the whole column is
|
|
94
|
+
* coarse and it is safe, which is the right trade for a field whose only job is to be readable
|
|
95
|
+
* text — and callers already degrade, the record-changes UI falling back to 'Changes made'.
|
|
96
|
+
*
|
|
97
|
+
* ## Rows in one result span different entities
|
|
98
|
+
*
|
|
99
|
+
* `EntityID` varies row to row, and `EntityInfo.GetDeniedReadFields` walks every field on the
|
|
100
|
+
* entity — its documented performance contract is that it must not be called in a row loop. Every
|
|
101
|
+
* lookup here is memoized on the raw `EntityID` string for the projector's lifetime, which is one
|
|
102
|
+
* request. Construct one projector per request, never one per row.
|
|
103
|
+
*/
|
|
104
|
+
export declare class RecordChangeFieldSecurityProjector {
|
|
105
|
+
private readonly provider;
|
|
106
|
+
private readonly user;
|
|
107
|
+
private readonly deniedByEntityID;
|
|
108
|
+
private carriesDenials;
|
|
109
|
+
private payloadKeys;
|
|
110
|
+
constructor(provider: RecordChangeMetadataSource, user: UserInfo);
|
|
111
|
+
/**
|
|
112
|
+
* Whether an entity is the audit trail this projector guards. Callers gate on this before
|
|
113
|
+
* constructing a projector, so nothing is paid on the other 99% of reads.
|
|
114
|
+
*/
|
|
115
|
+
static IsRecordChangesEntity(entity: EntityInfo | null | undefined): boolean;
|
|
116
|
+
/**
|
|
117
|
+
* Projects a whole result set. Returns the ORIGINAL array when nothing needed changing, so an
|
|
118
|
+
* unrestricted caller's rows are never rebuilt — and, on the cache-hit paths, so the cache's
|
|
119
|
+
* own frozen objects are handed back by reference exactly as they are today.
|
|
120
|
+
*/
|
|
121
|
+
ProjectRows<T>(rows: T[]): T[];
|
|
122
|
+
/**
|
|
123
|
+
* Single-row form, for the GraphQL read boundary. Returns the original object untouched when
|
|
124
|
+
* there is nothing to project.
|
|
125
|
+
*/
|
|
126
|
+
ProjectRow<T>(row: T): T;
|
|
127
|
+
/**
|
|
128
|
+
* Whether this caller is denied ANY field on ANY field-security-enabled entity.
|
|
129
|
+
*
|
|
130
|
+
* The whole projector is a no-op for everyone else, and this gate is what makes that true —
|
|
131
|
+
* including for the fail-closed branch in {@link deniedFieldsFor}, which would otherwise blank
|
|
132
|
+
* audit rows on deployments that never enabled field security at all. Cost is one pass over
|
|
133
|
+
* the entity list plus one denied-set walk per ENABLED entity, and the flag is off on every
|
|
134
|
+
* entity an administrator has not explicitly opted in.
|
|
135
|
+
*
|
|
136
|
+
* Public because the WRITE side needs the same question: a caller who could have been served a
|
|
137
|
+
* narrowed payload must not be allowed to write one back. See
|
|
138
|
+
* `ResolverBase.StripRecordChangePayloadFromClientInput`.
|
|
139
|
+
*/
|
|
140
|
+
CallerCarriesAnyDenial(): boolean;
|
|
141
|
+
private callerCarriesDenials;
|
|
142
|
+
private projectOne;
|
|
143
|
+
/**
|
|
144
|
+
* The denied-read set for the entity a Record Change row is ABOUT, or `null` to fail closed.
|
|
145
|
+
*
|
|
146
|
+
* **`null` drops all three payload columns.** Two shapes reach it and both have to. The first
|
|
147
|
+
* is an `EntityID` that no longer resolves to a known entity. The second is the one that
|
|
148
|
+
* decides the policy: a row that does not carry `EntityID` at all. A caller can ask for
|
|
149
|
+
* `Fields: ['ChangesJSON']` and nothing else, and field narrowing runs BEFORE this projection
|
|
150
|
+
* — so failing open on a missing `EntityID` would be a one-parameter bypass of the entire
|
|
151
|
+
* control.
|
|
152
|
+
*
|
|
153
|
+
* The cost of failing closed is paid only by callers who already carry denials somewhere (see
|
|
154
|
+
* {@link callerCarriesDenials}), and lands on audit rows for entities that are gone from
|
|
155
|
+
* metadata — rows for which no caller can render an entity name either way.
|
|
156
|
+
*/
|
|
157
|
+
private deniedFieldsFor;
|
|
158
|
+
/**
|
|
159
|
+
* Resolves the row's actual key casing once. Returns null — cached as null — when the rows
|
|
160
|
+
* carry no payload column at all, which makes every later row a single map read.
|
|
161
|
+
*/
|
|
162
|
+
private resolvePayloadKeys;
|
|
163
|
+
}
|
|
164
|
+
//# sourceMappingURL=recordChangeFieldSecurity.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"recordChangeFieldSecurity.d.ts","sourceRoot":"","sources":["../../src/generic/recordChangeFieldSecurity.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,cAAc,CAAC;AAC1C,OAAO,EAAE,QAAQ,EAAE,MAAM,gBAAgB,CAAC;AAE1C;;;;;;;GAOG;AACH,MAAM,WAAW,0BAA0B;IACvC,QAAQ,CAAC,QAAQ,EAAE,UAAU,EAAE,CAAC;IAChC,UAAU,CAAC,QAAQ,EAAE,MAAM,GAAG,UAAU,GAAG,SAAS,CAAC;CACxD;AAED;;;;GAIG;AACH,eAAO,MAAM,uBAAuB,uBAAuB,CAAC;AAE5D;;;GAGG;AACH,eAAO,MAAM,yBAAyB,aAAa,CAAC;AAEpD;;;;;GAKG;AACH,eAAO,MAAM,6BAA6B,4CAA6C,CAAC;AAExF;;;GAGG;AACH,eAAO,MAAM,6BAA6B,uBAAuB,CAAC;AAElE;;;GAGG;AACH,eAAO,MAAM,yBAAyB,EAAE,SAAS,MAAM,EAGtD,CAAC;AAEF;;;;;;;GAOG;AACH,MAAM,WAAW,6BAA6B;IAC1C,sEAAsE;IACtE,QAAQ,EAAE,OAAO,CAAC;IAClB,+EAA+E;IAC/E,KAAK,CAAC,EAAE,OAAO,CAAC;CACnB;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,8BAA8B,CAAC,GAAG,EAAE,OAAO,EAAE,eAAe,EAAE,GAAG,CAAC,MAAM,CAAC,GAAG,6BAA6B,CAiCxH;AA8BD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAiCG;AACH,qBAAa,kCAAkC;IAMvC,OAAO,CAAC,QAAQ,CAAC,QAAQ;IACzB,OAAO,CAAC,QAAQ,CAAC,IAAI;IANzB,OAAO,CAAC,QAAQ,CAAC,gBAAgB,CAAyC;IAC1E,OAAO,CAAC,cAAc,CAAwB;IAC9C,OAAO,CAAC,WAAW,CAAyD;gBAGvD,QAAQ,EAAE,0BAA0B,EACpC,IAAI,EAAE,QAAQ;IAGnC;;;OAGG;WACW,qBAAqB,CAAC,MAAM,EAAE,UAAU,GAAG,IAAI,GAAG,SAAS,GAAG,OAAO;IAInF;;;;OAIG;IACI,WAAW,CAAC,CAAC,EAAE,IAAI,EAAE,CAAC,EAAE,GAAG,CAAC,EAAE;IAerC;;;OAGG;IACI,UAAU,CAAC,CAAC,EAAE,GAAG,EAAE,CAAC,GAAG,CAAC;IAO/B;;;;;;;;;;;;OAYG;IACI,sBAAsB,IAAI,OAAO;IAIxC,OAAO,CAAC,oBAAoB;IAS5B,OAAO,CAAC,UAAU;IAiClB;;;;;;;;;;;;;OAaG;IACH,OAAO,CAAC,eAAe;IAkBvB;;;OAGG;IACH,OAAO,CAAC,kBAAkB;CAsB7B"}
|
|
@@ -0,0 +1,279 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The audit entity whose rows carry OTHER entities' field values. Matched by name rather than by
|
|
3
|
+
* ID because every other enforcement point in this feature keys off the entity name too, and a
|
|
4
|
+
* name is the thing a reader can check against metadata without a lookup.
|
|
5
|
+
*/
|
|
6
|
+
export const RecordChangesEntityName = 'MJ: Record Changes';
|
|
7
|
+
/**
|
|
8
|
+
* The Record Change column naming the entity a row is ABOUT. Everything in this module hangs off
|
|
9
|
+
* it: it is what makes the denied set per-ROW rather than per-result.
|
|
10
|
+
*/
|
|
11
|
+
export const RecordChangeEntityIDField = 'EntityID';
|
|
12
|
+
/**
|
|
13
|
+
* Payload columns holding a JSON object keyed by the target entity's field names — projected
|
|
14
|
+
* key by key against the denied set. `ChangesJSON` is `{ [field]: { field, oldValue, newValue } }`;
|
|
15
|
+
* `FullRecordJSON` is a flat whole-row snapshot. Both are top-level-keyed by field name, which is
|
|
16
|
+
* what lets one projection handle them.
|
|
17
|
+
*/
|
|
18
|
+
export const RecordChangeJSONPayloadFields = ['ChangesJSON', 'FullRecordJSON'];
|
|
19
|
+
/**
|
|
20
|
+
* The human-prose payload column — withheld outright, never redacted. See
|
|
21
|
+
* {@link RecordChangeFieldSecurityProjector}.
|
|
22
|
+
*/
|
|
23
|
+
export const RecordChangeProsePayloadField = 'ChangesDescription';
|
|
24
|
+
/**
|
|
25
|
+
* Every payload column this module touches, for callers that need to act on the set as a whole —
|
|
26
|
+
* notably the write guard, which must refuse a narrowed value coming back in.
|
|
27
|
+
*/
|
|
28
|
+
export const RecordChangePayloadFields = [
|
|
29
|
+
...RecordChangeJSONPayloadFields,
|
|
30
|
+
RecordChangeProsePayloadField,
|
|
31
|
+
];
|
|
32
|
+
/**
|
|
33
|
+
* Narrows one Record Change JSON payload to the fields the caller may read.
|
|
34
|
+
*
|
|
35
|
+
* **Withholds rather than guesses.** A payload that is not a string, or is a string that does not
|
|
36
|
+
* parse, or parses to something other than a plain object, is a payload this cannot prove is
|
|
37
|
+
* clean — so the column is dropped instead of passed through. A null/undefined/empty value is
|
|
38
|
+
* kept as-is: there is nothing stored to leak, and `ChangesJSON` is legitimately `''` on rows
|
|
39
|
+
* whose diff produced nothing.
|
|
40
|
+
*
|
|
41
|
+
* @param raw the column's stored value
|
|
42
|
+
* @param deniedLowercase field names the caller may not read, already lowercased
|
|
43
|
+
*/
|
|
44
|
+
export function ProjectRecordChangePayloadJSON(raw, deniedLowercase) {
|
|
45
|
+
if (raw === null || raw === undefined) {
|
|
46
|
+
return { Withhold: false, Value: raw };
|
|
47
|
+
}
|
|
48
|
+
if (typeof raw !== 'string') {
|
|
49
|
+
return { Withhold: true };
|
|
50
|
+
}
|
|
51
|
+
if (raw.trim().length === 0) {
|
|
52
|
+
return { Withhold: false, Value: raw };
|
|
53
|
+
}
|
|
54
|
+
let parsed;
|
|
55
|
+
try {
|
|
56
|
+
parsed = JSON.parse(raw);
|
|
57
|
+
}
|
|
58
|
+
catch {
|
|
59
|
+
return { Withhold: true };
|
|
60
|
+
}
|
|
61
|
+
if (typeof parsed !== 'object' || parsed === null || Array.isArray(parsed)) {
|
|
62
|
+
return { Withhold: true };
|
|
63
|
+
}
|
|
64
|
+
const source = parsed;
|
|
65
|
+
const kept = {};
|
|
66
|
+
for (const key of Object.keys(source)) {
|
|
67
|
+
if (deniedLowercase.has(key.trim().toLowerCase())) {
|
|
68
|
+
continue;
|
|
69
|
+
}
|
|
70
|
+
if (isDeniedChangeEntry(source[key], deniedLowercase)) {
|
|
71
|
+
continue;
|
|
72
|
+
}
|
|
73
|
+
kept[key] = source[key];
|
|
74
|
+
}
|
|
75
|
+
return { Withhold: false, Value: JSON.stringify(kept) };
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* A `ChangesJSON` entry is `{ field, oldValue, newValue }` stored under a key that is that same
|
|
79
|
+
* field name, so the key check already catches it. This second check costs one property read and
|
|
80
|
+
* covers the case where the two disagree — a payload written by something other than
|
|
81
|
+
* `DatabaseProviderBase.DiffObjects`, or hand-edited. Where they disagree, the entry is dropped.
|
|
82
|
+
*/
|
|
83
|
+
function isDeniedChangeEntry(value, deniedLowercase) {
|
|
84
|
+
if (typeof value !== 'object' || value === null || Array.isArray(value)) {
|
|
85
|
+
return false;
|
|
86
|
+
}
|
|
87
|
+
const field = value['field'];
|
|
88
|
+
return typeof field === 'string' && deniedLowercase.has(field.trim().toLowerCase());
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* Field-level security for the payload columns of `MJ: Record Changes`.
|
|
92
|
+
*
|
|
93
|
+
* ## Why this is not `ProviderBase.ApplyFieldSecurityProjection`
|
|
94
|
+
*
|
|
95
|
+
* Every other FLS enforcement point computes the denied set against **the entity being read**. A
|
|
96
|
+
* Record Change row is about a DIFFERENT entity — the one named by its own `EntityID` column —
|
|
97
|
+
* and `MJ: Record Changes` itself has field security switched off in every default deployment.
|
|
98
|
+
* The main projection therefore short-circuits on `EnableFieldLevelSecurity` and returns the row
|
|
99
|
+
* untouched, with a denied field's old and new values sitting in `ChangesJSON` in plain text.
|
|
100
|
+
* That is the leak this closes: a user with entity-level read on the audit trail could read a
|
|
101
|
+
* salary they were denied on `MJ: Employees` straight out of it, in the default configuration.
|
|
102
|
+
*
|
|
103
|
+
* ## What happens to each column
|
|
104
|
+
*
|
|
105
|
+
* | Column | Treatment |
|
|
106
|
+
* |---|---|
|
|
107
|
+
* | `ChangesJSON` | projected — denied field keys dropped |
|
|
108
|
+
* | `FullRecordJSON` | projected — same, it is a whole-row snapshot |
|
|
109
|
+
* | `ChangesDescription` | **withheld entirely** whenever the caller is denied anything on the target entity |
|
|
110
|
+
*
|
|
111
|
+
* `ChangesDescription` is human prose ("Salary changed from 100000 to 120000"). Redacting prose
|
|
112
|
+
* on the fly leaks on the first value that appears in an unexpected form, and regenerating it
|
|
113
|
+
* from the filtered JSON is a larger surface than this is worth. Withholding the whole column is
|
|
114
|
+
* coarse and it is safe, which is the right trade for a field whose only job is to be readable
|
|
115
|
+
* text — and callers already degrade, the record-changes UI falling back to 'Changes made'.
|
|
116
|
+
*
|
|
117
|
+
* ## Rows in one result span different entities
|
|
118
|
+
*
|
|
119
|
+
* `EntityID` varies row to row, and `EntityInfo.GetDeniedReadFields` walks every field on the
|
|
120
|
+
* entity — its documented performance contract is that it must not be called in a row loop. Every
|
|
121
|
+
* lookup here is memoized on the raw `EntityID` string for the projector's lifetime, which is one
|
|
122
|
+
* request. Construct one projector per request, never one per row.
|
|
123
|
+
*/
|
|
124
|
+
export class RecordChangeFieldSecurityProjector {
|
|
125
|
+
constructor(provider, user) {
|
|
126
|
+
this.provider = provider;
|
|
127
|
+
this.user = user;
|
|
128
|
+
this.deniedByEntityID = new Map();
|
|
129
|
+
this.carriesDenials = null;
|
|
130
|
+
this.payloadKeys = undefined;
|
|
131
|
+
}
|
|
132
|
+
/**
|
|
133
|
+
* Whether an entity is the audit trail this projector guards. Callers gate on this before
|
|
134
|
+
* constructing a projector, so nothing is paid on the other 99% of reads.
|
|
135
|
+
*/
|
|
136
|
+
static IsRecordChangesEntity(entity) {
|
|
137
|
+
return entity?.Name?.trim().toLowerCase() === RecordChangesEntityName.toLowerCase();
|
|
138
|
+
}
|
|
139
|
+
/**
|
|
140
|
+
* Projects a whole result set. Returns the ORIGINAL array when nothing needed changing, so an
|
|
141
|
+
* unrestricted caller's rows are never rebuilt — and, on the cache-hit paths, so the cache's
|
|
142
|
+
* own frozen objects are handed back by reference exactly as they are today.
|
|
143
|
+
*/
|
|
144
|
+
ProjectRows(rows) {
|
|
145
|
+
if (!rows?.length || !this.callerCarriesDenials()) {
|
|
146
|
+
return rows;
|
|
147
|
+
}
|
|
148
|
+
let mutated = false;
|
|
149
|
+
const projected = rows.map((row) => {
|
|
150
|
+
const next = this.projectOne(row);
|
|
151
|
+
if (next !== row) {
|
|
152
|
+
mutated = true;
|
|
153
|
+
}
|
|
154
|
+
return next;
|
|
155
|
+
});
|
|
156
|
+
return mutated ? projected : rows;
|
|
157
|
+
}
|
|
158
|
+
/**
|
|
159
|
+
* Single-row form, for the GraphQL read boundary. Returns the original object untouched when
|
|
160
|
+
* there is nothing to project.
|
|
161
|
+
*/
|
|
162
|
+
ProjectRow(row) {
|
|
163
|
+
if (!row || !this.callerCarriesDenials()) {
|
|
164
|
+
return row;
|
|
165
|
+
}
|
|
166
|
+
return this.projectOne(row);
|
|
167
|
+
}
|
|
168
|
+
/**
|
|
169
|
+
* Whether this caller is denied ANY field on ANY field-security-enabled entity.
|
|
170
|
+
*
|
|
171
|
+
* The whole projector is a no-op for everyone else, and this gate is what makes that true —
|
|
172
|
+
* including for the fail-closed branch in {@link deniedFieldsFor}, which would otherwise blank
|
|
173
|
+
* audit rows on deployments that never enabled field security at all. Cost is one pass over
|
|
174
|
+
* the entity list plus one denied-set walk per ENABLED entity, and the flag is off on every
|
|
175
|
+
* entity an administrator has not explicitly opted in.
|
|
176
|
+
*
|
|
177
|
+
* Public because the WRITE side needs the same question: a caller who could have been served a
|
|
178
|
+
* narrowed payload must not be allowed to write one back. See
|
|
179
|
+
* `ResolverBase.StripRecordChangePayloadFromClientInput`.
|
|
180
|
+
*/
|
|
181
|
+
CallerCarriesAnyDenial() {
|
|
182
|
+
return this.callerCarriesDenials();
|
|
183
|
+
}
|
|
184
|
+
callerCarriesDenials() {
|
|
185
|
+
if (this.carriesDenials === null) {
|
|
186
|
+
this.carriesDenials = this.provider.Entities.some((e) => e.EnableFieldLevelSecurity && e.GetDeniedReadFields(this.user).size > 0);
|
|
187
|
+
}
|
|
188
|
+
return this.carriesDenials;
|
|
189
|
+
}
|
|
190
|
+
projectOne(row) {
|
|
191
|
+
const source = row;
|
|
192
|
+
if (!source || typeof source !== 'object') {
|
|
193
|
+
return row;
|
|
194
|
+
}
|
|
195
|
+
const keys = this.resolvePayloadKeys(source);
|
|
196
|
+
if (!keys) {
|
|
197
|
+
return row; // the caller selected none of the payload columns — nothing to protect
|
|
198
|
+
}
|
|
199
|
+
const denied = this.deniedFieldsFor(source, keys);
|
|
200
|
+
if (denied !== null && denied.size === 0) {
|
|
201
|
+
return row; // caller reads every field on the entity this row is about
|
|
202
|
+
}
|
|
203
|
+
const projected = { ...source };
|
|
204
|
+
if (keys.Prose) {
|
|
205
|
+
delete projected[keys.Prose];
|
|
206
|
+
}
|
|
207
|
+
for (const key of keys.JSON) {
|
|
208
|
+
if (denied === null) {
|
|
209
|
+
delete projected[key];
|
|
210
|
+
continue;
|
|
211
|
+
}
|
|
212
|
+
const result = ProjectRecordChangePayloadJSON(source[key], denied);
|
|
213
|
+
if (result.Withhold) {
|
|
214
|
+
delete projected[key];
|
|
215
|
+
}
|
|
216
|
+
else {
|
|
217
|
+
projected[key] = result.Value;
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
return projected;
|
|
221
|
+
}
|
|
222
|
+
/**
|
|
223
|
+
* The denied-read set for the entity a Record Change row is ABOUT, or `null` to fail closed.
|
|
224
|
+
*
|
|
225
|
+
* **`null` drops all three payload columns.** Two shapes reach it and both have to. The first
|
|
226
|
+
* is an `EntityID` that no longer resolves to a known entity. The second is the one that
|
|
227
|
+
* decides the policy: a row that does not carry `EntityID` at all. A caller can ask for
|
|
228
|
+
* `Fields: ['ChangesJSON']` and nothing else, and field narrowing runs BEFORE this projection
|
|
229
|
+
* — so failing open on a missing `EntityID` would be a one-parameter bypass of the entire
|
|
230
|
+
* control.
|
|
231
|
+
*
|
|
232
|
+
* The cost of failing closed is paid only by callers who already carry denials somewhere (see
|
|
233
|
+
* {@link callerCarriesDenials}), and lands on audit rows for entities that are gone from
|
|
234
|
+
* metadata — rows for which no caller can render an entity name either way.
|
|
235
|
+
*/
|
|
236
|
+
deniedFieldsFor(source, keys) {
|
|
237
|
+
if (!keys.EntityID) {
|
|
238
|
+
return null;
|
|
239
|
+
}
|
|
240
|
+
const rawEntityID = source[keys.EntityID];
|
|
241
|
+
if (typeof rawEntityID !== 'string' || rawEntityID.trim().length === 0) {
|
|
242
|
+
return null;
|
|
243
|
+
}
|
|
244
|
+
const cached = this.deniedByEntityID.get(rawEntityID);
|
|
245
|
+
if (cached !== undefined) {
|
|
246
|
+
return cached;
|
|
247
|
+
}
|
|
248
|
+
const entity = this.provider.EntityByID(rawEntityID);
|
|
249
|
+
const denied = entity ? entity.GetDeniedReadFields(this.user) : null;
|
|
250
|
+
this.deniedByEntityID.set(rawEntityID, denied);
|
|
251
|
+
return denied;
|
|
252
|
+
}
|
|
253
|
+
/**
|
|
254
|
+
* Resolves the row's actual key casing once. Returns null — cached as null — when the rows
|
|
255
|
+
* carry no payload column at all, which makes every later row a single map read.
|
|
256
|
+
*/
|
|
257
|
+
resolvePayloadKeys(source) {
|
|
258
|
+
if (this.payloadKeys !== undefined) {
|
|
259
|
+
return this.payloadKeys;
|
|
260
|
+
}
|
|
261
|
+
const byLowerName = new Map();
|
|
262
|
+
for (const key of Object.keys(source)) {
|
|
263
|
+
byLowerName.set(key.trim().toLowerCase(), key);
|
|
264
|
+
}
|
|
265
|
+
const jsonKeys = RecordChangeJSONPayloadFields
|
|
266
|
+
.map((name) => byLowerName.get(name.toLowerCase()))
|
|
267
|
+
.filter((key) => key !== undefined);
|
|
268
|
+
const proseKey = byLowerName.get(RecordChangeProsePayloadField.toLowerCase()) ?? null;
|
|
269
|
+
this.payloadKeys = (jsonKeys.length === 0 && proseKey === null)
|
|
270
|
+
? null
|
|
271
|
+
: {
|
|
272
|
+
JSON: jsonKeys,
|
|
273
|
+
Prose: proseKey,
|
|
274
|
+
EntityID: byLowerName.get(RecordChangeEntityIDField.toLowerCase()) ?? null,
|
|
275
|
+
};
|
|
276
|
+
return this.payloadKeys;
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
//# sourceMappingURL=recordChangeFieldSecurity.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"recordChangeFieldSecurity.js","sourceRoot":"","sources":["../../src/generic/recordChangeFieldSecurity.ts"],"names":[],"mappings":"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"}
|
|
@@ -114,7 +114,7 @@ function distributeChildren(parents, collectionName, foreignKey, children) {
|
|
|
114
114
|
if (!(collection instanceof RelatedRecordCollection)) {
|
|
115
115
|
continue;
|
|
116
116
|
}
|
|
117
|
-
const key = normalizeKey(parent.FirstPrimaryKey?.Value);
|
|
117
|
+
const key = normalizeKey(parent.FirstPrimaryKey?.Value); // first-pk-ok: RelatedEntityJoinField is one FK column, so the parent key it holds is single-column by design
|
|
118
118
|
collection.SetLoadedItems(byParent.get(key) ?? []);
|
|
119
119
|
}
|
|
120
120
|
}
|
|
@@ -127,7 +127,7 @@ function distributeChildren(parents, collectionName, foreignKey, children) {
|
|
|
127
127
|
function collectParentKeys(parents) {
|
|
128
128
|
const keys = new Set();
|
|
129
129
|
for (const parent of parents) {
|
|
130
|
-
const value = parent.FirstPrimaryKey?.Value;
|
|
130
|
+
const value = parent.FirstPrimaryKey?.Value; // first-pk-ok: RelatedEntityJoinField is one FK column, so the parent key it holds is single-column by design
|
|
131
131
|
if (value !== null && value !== undefined && value !== '') {
|
|
132
132
|
keys.add(String(value));
|
|
133
133
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"relatedRecordBatchLoader.js","sourceRoot":"","sources":["../../src/generic/relatedRecordBatchLoader.ts"],"names":[],"mappings":"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
|
|
1
|
+
{"version":3,"file":"relatedRecordBatchLoader.js","sourceRoot":"","sources":["../../src/generic/relatedRecordBatchLoader.ts"],"names":[],"mappings":"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"}
|
|
@@ -26,7 +26,7 @@ import type { BaseEntity } from './baseEntity.js';
|
|
|
26
26
|
import { EntityCompanion, EntityCompanionDeserializeMode, EntityCompanionPayload } from './entityCompanion.js';
|
|
27
27
|
import type { EntitySavePlan } from './entitySavePlan.js';
|
|
28
28
|
import { ValidationResult } from './entityInfo.js';
|
|
29
|
-
import type { EntitySaveOptions } from './interfaces.js';
|
|
29
|
+
import type { EntitySaveOptions, IEntityDataProvider } from './interfaces.js';
|
|
30
30
|
/**
|
|
31
31
|
* When a child collection populates itself from the database.
|
|
32
32
|
*
|
|
@@ -606,5 +606,6 @@ export declare class RelatedRecordCollection<T extends BaseEntity = BaseEntity>
|
|
|
606
606
|
* callers assume when they display or reference "line 3".
|
|
607
607
|
*/
|
|
608
608
|
private applySequence;
|
|
609
|
+
BindProvider(provider: IEntityDataProvider | null): void;
|
|
609
610
|
}
|
|
610
611
|
//# sourceMappingURL=relatedRecordCollection.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"relatedRecordCollection.d.ts","sourceRoot":"","sources":["../../src/generic/relatedRecordCollection.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AAGH,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,cAAc,CAAC;AAI/C,OAAO,EAAE,eAAe,EAAE,8BAA8B,EAAE,sBAAsB,EAAE,MAAM,mBAAmB,CAAC;AAE5G,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,kBAAkB,CAAC;AACvD,OAAO,EAA4C,gBAAgB,EAAE,MAAM,cAAc,CAAC;AAC1F,OAAO,KAAK,EAAE,iBAAiB,EAAuC,MAAM,cAAc,CAAC;
|
|
1
|
+
{"version":3,"file":"relatedRecordCollection.d.ts","sourceRoot":"","sources":["../../src/generic/relatedRecordCollection.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AAGH,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,cAAc,CAAC;AAI/C,OAAO,EAAE,eAAe,EAAE,8BAA8B,EAAE,sBAAsB,EAAE,MAAM,mBAAmB,CAAC;AAE5G,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,kBAAkB,CAAC;AACvD,OAAO,EAA4C,gBAAgB,EAAE,MAAM,cAAc,CAAC;AAC1F,OAAO,KAAK,EAAE,iBAAiB,EAAE,mBAAmB,EAAuC,MAAM,cAAc,CAAC;AAGhH;;;;;;;;;;;;;;;;;;GAkBG;AACH,MAAM,MAAM,qBAAqB,GAAG,UAAU,GAAG,WAAW,GAAG,MAAM,GAAG,OAAO,CAAC;AAEhF;;;;;;;;;;;GAWG;AACH,MAAM,MAAM,mBAAmB,GAAG,UAAU,GAAG,OAAO,CAAC;AAEvD;;;;;;;;GAQG;AACH,MAAM,MAAM,wBAAwB,GAAG,QAAQ,GAAG,QAAQ,GAAG,QAAQ,CAAC;AAEtE;;GAEG;AACH,MAAM,MAAM,qBAAqB,GAAG;IAChC,yEAAyE;IACzE,KAAK,EAAE,MAAM,CAAC;IACd,4DAA4D;IAC5D,IAAI,CAAC,EAAE,MAAM,CAAC;CACjB,CAAC;AAEF;;;;;;;;;;;;;;;;GAgBG;AACH,MAAM,MAAM,8BAA8B,GAAG;IACzC;;;OAGG;IACH,IAAI,EAAE,MAAM,CAAC;IACb;;;OAGG;IACH,aAAa,EAAE,MAAM,CAAC;IACtB;;;OAGG;IACH,sBAAsB,EAAE,MAAM,CAAC;IAC/B,0FAA0F;IAC1F,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,sEAAsE;IACtE,IAAI,CAAC,EAAE,qBAAqB,CAAC;IAC7B,yDAAyD;IACzD,MAAM,CAAC,EAAE,mBAAmB,CAAC;IAC7B;;;;OAIG;IACH,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,kDAAkD;IAClD,QAAQ,CAAC,EAAE,wBAAwB,CAAC;IACpC,uEAAuE;IACvE,QAAQ,CAAC,EAAE,qBAAqB,CAAC;IACjC;;;;;;OAMG;IACH,cAAc,CAAC,EAAE,OAAO,CAAC;CAC5B,CAAC;AAEF;;GAEG;AACH,MAAM,MAAM,+BAA+B,GAAG;IAC1C;;;OAGG;IACH,UAAU,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3B,2DAA2D;IAC3D,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAChC;;;;;;;OAOG;IACH,KAAK,EAAE,OAAO,CAAC;IACf;;;OAGG;IACH,UAAU,CAAC,EAAE,sBAAsB,EAAE,GAAG,IAAI,CAAC;CAChD,CAAC;AAEF;;GAEG;AACH,MAAM,MAAM,2BAA2B,GAAG;IACtC,2BAA2B;IAC3B,KAAK,EAAE,+BAA+B,EAAE,CAAC;IACzC,2FAA2F;IAC3F,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,CAAC;CACtC,CAAC;AAEF;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8BG;AACH,qBAAa,uBAAuB,CAAC,CAAC,SAAS,UAAU,GAAG,UAAU,CAAE,SAAQ,eAAe,CAAC,2BAA2B,CAAC;IACxH,OAAO,CAAC,KAAK,CAAW;IACxB,OAAO,CAAC,OAAO,CAAW;IAC1B,OAAO,CAAC,MAAM,CAAS;IACvB,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAiC;IAEzD;;;OAGG;gBACS,KAAK,EAAE,UAAU,EAAE,OAAO,EAAE,8BAA8B;IAMtE;;;;;;;;OAQG;IACH,OAAO,CAAC,2BAA2B;IAqBnC,kBAAkB;IAClB,IAAW,IAAI,IAAI,MAAM,CAExB;IAED,8CAA8C;IAC9C,IAAW,iBAAiB,IAAI,MAAM,CAErC;IAED,kEAAkE;IAClE,IAAW,sBAAsB,IAAI,MAAM,CAE1C;IAED,8DAA8D;IAC9D,IAAW,aAAa,IAAI,MAAM,GAAG,SAAS,CAE7C;IAED,6CAA6C;IAC7C,IAAW,QAAQ,IAAI,qBAAqB,CAE3C;IAED,8CAA8C;IAC9C,IAAW,WAAW,IAAI,wBAAwB,CAEjD;IAED,2EAA2E;IAC3E,IAAW,MAAM,IAAI,mBAAmB,CAEvC;IAED;;;;;;OAMG;IACH,IAAW,UAAU,IAAI,OAAO,CAE/B;IAED;;;;;;OAMG;IACH,IAAW,KAAK,IAAI,SAAS,CAAC,EAAE,CAU/B;IAED;;;;;;;;;;;;;;;;;;;;;OAqBG;IACI,CAAC,MAAM,CAAC,QAAQ,CAAC,IAAI,QAAQ,CAAC,CAAC,CAAC;IAIvC;;;OAGG;IACH,IAAW,MAAM,IAAI,MAAM,CAE1B;IAED;;;;OAIG;IACH,IAAW,OAAO,IAAI,SAAS,CAAC,EAAE,CAEjC;IAED;;;;;;;;OAQG;IACH,IAAW,KAAK,IAAI,MAAM,CAEzB;IAED,oEAAoE;IACpE,IAAW,QAAQ,IAAI,OAAO,CAE7B;IAED;;;OAGG;IACH,IAAoB,KAAK,IAAI,OAAO,CAYnC;IAED;;;;;;;;OAQG;IACI,GAAG,CAAC,IAAI,EAAE,CAAC,GAAG,CAAC;IAWtB;;;;;;;OAOG;IACU,MAAM,IAAI,OAAO,CAAC,CAAC,CAAC;IAWjC;;;;;;;;OAQG;IACI,MAAM,CAAC,WAAW,EAAE,CAAC,GAAG,MAAM,GAAG,IAAI;IAsB5C,2BAA2B;IACpB,KAAK,IAAI,IAAI;IAOpB;;;;;;;;;;;;;;;;;;;;;;;OAuBG;IACU,IAAI,CAAC,KAAK,UAAQ,GAAG,OAAO,CAAC,IAAI,CAAC;IAyD/C,kBAAkB;IACI,SAAS,CAAC,QAAQ,CAAC,EAAE,GAAG,CAAC,MAAM,CAAC,GAAG,OAAO,CAAC,IAAI,CAAC;IAMtE;;;;;;;OAOG;IACI,cAAc,CAAC,KAAK,EAAE,CAAC,EAAE,GAAG,IAAI;IAMvC;;;;;;;OAOG;IACI,WAAW,CAAC,OAAO,EAAE,CAAC,EAAE,OAAO,EAAE,CAAC,GAAG,OAAO;IAWnD;;;;OAIG;IACH,OAAO,CAAC,aAAa;IAYrB;;;;;;;;OAQG;IACU,gBAAgB,IAAI,OAAO,CAAC,OAAO,CAAC;IAYjD;;;;;;;;OAQG;IACH;;;;;;;;;;OAUG;IACH,OAAO,CAAC,uBAAuB;IA6B/B;;;;;;;;;;;;OAYG;IACH,OAAO,CAAC,mBAAmB;IAsC3B;;;;;;;;;;;;;;;;;;;;;;;;OAwBG;IACH,IAAW,WAAW,IAAI,OAAO,CAUhC;IAED;;;;;;;;;;;;;OAaG;IACH,OAAO,CAAC,UAAU,CAA4G;IAE9H,OAAO,CAAC,iBAAiB;IAezB;;;;;OAKG;IACH,OAAO,CAAC,iBAAiB;IAqCzB;;;;;;;;;;OAUG;IACH,OAAO,CAAC,eAAe;IAuBvB;;;;;;;;;;OAUG;IACH,OAAO,CAAC,YAAY;IAuBpB;;;;;;;OAOG;YACW,WAAW;IAwCzB,kBAAkB;IACF,QAAQ,CAAC,MAAM,EAAE,gBAAgB,GAAG,IAAI;IA2BxD,kBAAkB;IACI,aAAa,CAAC,MAAM,EAAE,gBAAgB,GAAG,OAAO,CAAC,IAAI,CAAC;IAc5E;;;;;;;;;OASG;IACH,OAAO,CAAC,YAAY;IAYpB,kBAAkB;IACF,kBAAkB,CAAC,IAAI,EAAE,cAAc,EAAE,OAAO,CAAC,EAAE,iBAAiB,GAAG,IAAI;IAqC3F;;;;;;;;OAQG;IACH,OAAO,CAAC,6BAA6B;IAcrC,kBAAkB;IACF,oBAAoB,CAAC,IAAI,EAAE,cAAc,GAAG,IAAI;IAgBhE,kBAAkB;IACF,aAAa,IAAI,IAAI;IAQrC,kBAAkB;IACI,SAAS,CAAC,IAAI,GAAE,8BAA0C,GAAG,OAAO,CAAC,2BAA2B,GAAG,IAAI,CAAC;IAiC9H,kBAAkB;IACI,WAAW,CAC7B,IAAI,EAAE,2BAA2B,EACjC,IAAI,GAAE,8BAA0C,GACjD,OAAO,CAAC,IAAI,CAAC;IAahB;;;;;;;;;;;;;OAaG;YACW,cAAc;IAyD5B;;;;;;;;;;OAUG;YACW,iBAAiB;IAe/B;;;;;;;;OAQG;IACH,OAAO,CAAC,YAAY;IAUpB;;;;;;;;;;;;OAYG;IACH,OAAO,CAAC,cAAc;IAUtB;;;;;OAKG;IACH,OAAO,CAAC,aAAa;IAoBL,YAAY,CAAC,QAAQ,EAAE,mBAAmB,GAAG,IAAI,GAAG,IAAI;CAY3E"}
|
|
@@ -365,7 +365,7 @@ export class RelatedRecordCollection extends EntityCompanion {
|
|
|
365
365
|
if (!provider) {
|
|
366
366
|
throw new Error(`RelatedRecordCollection '${this.Name}': owner has no provider; cannot load.`);
|
|
367
367
|
}
|
|
368
|
-
const parentKey = this.Owner.FirstPrimaryKey?.Value;
|
|
368
|
+
const parentKey = this.Owner.FirstPrimaryKey?.Value; // first-pk-ok: RelatedEntityJoinField is one FK column, so the parent key it holds is single-column by design
|
|
369
369
|
const result = await provider.RunView({
|
|
370
370
|
EntityName: this.RelatedEntityName,
|
|
371
371
|
ExtraFilter: `${this.options.RelatedEntityJoinField} = '${String(parentKey).replace(/'/g, "''")}'`,
|
|
@@ -487,7 +487,7 @@ export class RelatedRecordCollection extends EntityCompanion {
|
|
|
487
487
|
// survives the engine reassigning it wholesale, which is the case a captured array
|
|
488
488
|
// reference misses. Re-running discovery here would also risk silently binding to a
|
|
489
489
|
// DIFFERENT engine mid-life if two happened to cache the same entity.
|
|
490
|
-
const parentKey = this.Owner.FirstPrimaryKey?.Value;
|
|
490
|
+
const parentKey = this.Owner.FirstPrimaryKey?.Value; // first-pk-ok: RelatedEntityJoinField is one FK column, so the parent key it holds is single-column by design
|
|
491
491
|
if (parentKey === null || parentKey === undefined || parentKey === '') {
|
|
492
492
|
return;
|
|
493
493
|
}
|
|
@@ -598,7 +598,7 @@ export class RelatedRecordCollection extends EntityCompanion {
|
|
|
598
598
|
* entity — in which case the caller falls back to a database load.
|
|
599
599
|
*/
|
|
600
600
|
findCachedRecords() {
|
|
601
|
-
const parentKey = this.Owner.FirstPrimaryKey?.Value;
|
|
601
|
+
const parentKey = this.Owner.FirstPrimaryKey?.Value; // first-pk-ok: RelatedEntityJoinField is one FK column, so the parent key it holds is single-column by design
|
|
602
602
|
if (parentKey === null || parentKey === undefined || parentKey === '') {
|
|
603
603
|
return null; // an unsaved parent owns no persisted related records
|
|
604
604
|
}
|
|
@@ -1030,7 +1030,7 @@ export class RelatedRecordCollection extends EntityCompanion {
|
|
|
1030
1030
|
* already be correct.
|
|
1031
1031
|
*/
|
|
1032
1032
|
stampParentKey() {
|
|
1033
|
-
const parentKey = this.Owner.FirstPrimaryKey?.Value;
|
|
1033
|
+
const parentKey = this.Owner.FirstPrimaryKey?.Value; // first-pk-ok: RelatedEntityJoinField is one FK column, so the parent key it holds is single-column by design
|
|
1034
1034
|
if (parentKey === null || parentKey === undefined || parentKey === '') {
|
|
1035
1035
|
return;
|
|
1036
1036
|
}
|
|
@@ -1062,5 +1062,17 @@ export class RelatedRecordCollection extends EntityCompanion {
|
|
|
1062
1062
|
}
|
|
1063
1063
|
});
|
|
1064
1064
|
}
|
|
1065
|
+
BindProvider(provider) {
|
|
1066
|
+
for (const item of this.items) {
|
|
1067
|
+
if (item.BoundProvider !== provider) {
|
|
1068
|
+
item.BindProvider(provider);
|
|
1069
|
+
}
|
|
1070
|
+
}
|
|
1071
|
+
for (const item of this.removed) {
|
|
1072
|
+
if (item.BoundProvider !== provider) {
|
|
1073
|
+
item.BindProvider(provider);
|
|
1074
|
+
}
|
|
1075
|
+
}
|
|
1076
|
+
}
|
|
1065
1077
|
}
|
|
1066
1078
|
//# sourceMappingURL=relatedRecordCollection.js.map
|