@memberjunction/core 5.51.0 → 6.1.0-edge.1

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.
Files changed (73) hide show
  1. package/LICENSE +7 -0
  2. package/dist/generic/baseEngine.d.ts.map +1 -1
  3. package/dist/generic/baseEngine.js +13 -2
  4. package/dist/generic/baseEngine.js.map +1 -1
  5. package/dist/generic/baseEngineRegistry.d.ts +14 -0
  6. package/dist/generic/baseEngineRegistry.d.ts.map +1 -1
  7. package/dist/generic/baseEngineRegistry.js +32 -0
  8. package/dist/generic/baseEngineRegistry.js.map +1 -1
  9. package/dist/generic/baseEntity.d.ts +329 -26
  10. package/dist/generic/baseEntity.d.ts.map +1 -1
  11. package/dist/generic/baseEntity.js +788 -79
  12. package/dist/generic/baseEntity.js.map +1 -1
  13. package/dist/generic/databaseProviderBase.d.ts +54 -17
  14. package/dist/generic/databaseProviderBase.d.ts.map +1 -1
  15. package/dist/generic/databaseProviderBase.js +133 -52
  16. package/dist/generic/databaseProviderBase.js.map +1 -1
  17. package/dist/generic/entityCompanion.d.ts +218 -0
  18. package/dist/generic/entityCompanion.d.ts.map +1 -0
  19. package/dist/generic/entityCompanion.js +170 -0
  20. package/dist/generic/entityCompanion.js.map +1 -0
  21. package/dist/generic/entityInfo.d.ts +146 -0
  22. package/dist/generic/entityInfo.d.ts.map +1 -1
  23. package/dist/generic/entityInfo.js +188 -0
  24. package/dist/generic/entityInfo.js.map +1 -1
  25. package/dist/generic/entitySavePlan.d.ts +199 -0
  26. package/dist/generic/entitySavePlan.d.ts.map +1 -0
  27. package/dist/generic/entitySavePlan.js +213 -0
  28. package/dist/generic/entitySavePlan.js.map +1 -0
  29. package/dist/generic/entityTransactionScope.d.ts +125 -0
  30. package/dist/generic/entityTransactionScope.d.ts.map +1 -0
  31. package/dist/generic/entityTransactionScope.js +115 -0
  32. package/dist/generic/entityTransactionScope.js.map +1 -0
  33. package/dist/generic/interfaces.d.ts +93 -35
  34. package/dist/generic/interfaces.d.ts.map +1 -1
  35. package/dist/generic/interfaces.js +27 -0
  36. package/dist/generic/interfaces.js.map +1 -1
  37. package/dist/generic/providerBase.d.ts +13 -0
  38. package/dist/generic/providerBase.d.ts.map +1 -1
  39. package/dist/generic/providerBase.js +64 -5
  40. package/dist/generic/providerBase.js.map +1 -1
  41. package/dist/generic/relatedRecordBatchLoader.d.ts +39 -0
  42. package/dist/generic/relatedRecordBatchLoader.d.ts.map +1 -0
  43. package/dist/generic/relatedRecordBatchLoader.js +154 -0
  44. package/dist/generic/relatedRecordBatchLoader.js.map +1 -0
  45. package/dist/generic/relatedRecordCollection.d.ts +578 -0
  46. package/dist/generic/relatedRecordCollection.d.ts.map +1 -0
  47. package/dist/generic/relatedRecordCollection.js +1004 -0
  48. package/dist/generic/relatedRecordCollection.js.map +1 -0
  49. package/dist/generic/saveEntityGraphOperation.d.ts +148 -0
  50. package/dist/generic/saveEntityGraphOperation.d.ts.map +1 -0
  51. package/dist/generic/saveEntityGraphOperation.js +157 -0
  52. package/dist/generic/saveEntityGraphOperation.js.map +1 -0
  53. package/dist/generic/securityInfo.d.ts +99 -1
  54. package/dist/generic/securityInfo.d.ts.map +1 -1
  55. package/dist/generic/securityInfo.js +88 -6
  56. package/dist/generic/securityInfo.js.map +1 -1
  57. package/dist/generic/telemetryManager.d.ts +21 -1
  58. package/dist/generic/telemetryManager.d.ts.map +1 -1
  59. package/dist/generic/telemetryManager.js +21 -6
  60. package/dist/generic/telemetryManager.js.map +1 -1
  61. package/dist/index.d.ts +6 -1
  62. package/dist/index.d.ts.map +1 -1
  63. package/dist/index.js +6 -3
  64. package/dist/index.js.map +1 -1
  65. package/dist/views/runView.d.ts +31 -0
  66. package/dist/views/runView.d.ts.map +1 -1
  67. package/dist/views/runView.js.map +1 -1
  68. package/package.json +13 -13
  69. package/readme.md +159 -1
  70. package/dist/generic/runReport.d.ts +0 -25
  71. package/dist/generic/runReport.d.ts.map +0 -1
  72. package/dist/generic/runReport.js +0 -38
  73. package/dist/generic/runReport.js.map +0 -1
@@ -0,0 +1,218 @@
1
+ /**
2
+ * @fileoverview Entity Companions — named, serialisable side-channels attached to a `BaseEntity`.
3
+ *
4
+ * ## What a companion is
5
+ *
6
+ * A companion is a piece of state that belongs to a record but is **not one of its fields**, and
7
+ * that needs to travel with the record across the client/server boundary, participate in its
8
+ * validation, and contribute work to its save.
9
+ *
10
+ * The canonical example is a child collection — an order's lines, a journal entry's lines, a
11
+ * payment's allocations — but the abstraction is deliberately not about children. It is about
12
+ * "extra state that rides with the record", because MemberJunction has grown that concept ad hoc
13
+ * three times already:
14
+ *
15
+ * - `OldValues___` on the generated GraphQL save input (concurrency check payload)
16
+ * - `RestoreContext___` on the same input (restore lineage for the RecordChange row)
17
+ * - `EntityObjectJSON` in the TransactionGroup wire format (a whole record as opaque JSON)
18
+ *
19
+ * Each solved its own problem with its own bespoke slot, resolver branch and hand-written
20
+ * serialisation. Companions give the next such need — and the ones already in flight — one
21
+ * mechanism instead of a fourth bespoke field.
22
+ *
23
+ * ## What a companion is not
24
+ *
25
+ * A companion is **not** a way to smuggle arbitrary untyped data through the platform. Every
26
+ * companion has a stable {@link EntityCompanion.Name}, a typed wire shape, and explicit control
27
+ * over what it contributes to validation and save. Data with no owner and no contract belongs in a
28
+ * field, not here.
29
+ *
30
+ * ## Lifecycle
31
+ *
32
+ * ```text
33
+ * declare → subclass constructor calls RegisterCompanion() (usually via DeclareRelatedRecords())
34
+ * load → BaseEntity.Load()/LoadFromData() gives eager companions a chance to populate
35
+ * mutate → application code works with the typed companion API
36
+ * validate → BaseEntity.Validate()/ValidateAsync() fans out to every companion
37
+ * save → companions contribute nodes to the EntitySavePlan; the graph executes atomically
38
+ * transport→ Serialize() on the way out, Deserialize() on the way in
39
+ * ```
40
+ *
41
+ * @module @memberjunction/core
42
+ */
43
+ import { BaseEntity } from './baseEntity.js';
44
+ import { ValidationResult } from './entityInfo.js';
45
+ import type { EntitySavePlan } from './entitySavePlan.js';
46
+ import type { EntitySaveOptions } from './interfaces.js';
47
+ /**
48
+ * The serialized form of a single companion as it crosses the wire.
49
+ *
50
+ * Deliberately minimal: a stable name plus an opaque JSON payload. Keeping the payload opaque is
51
+ * what lets a new companion type ship without touching the transport, the resolvers, or CodeGen.
52
+ */
53
+ export type EntityCompanionPayload = {
54
+ /** The companion's stable {@link EntityCompanion.Name}. */
55
+ Name: string;
56
+ /** The companion's own serialized state, JSON-safe. */
57
+ Data: unknown;
58
+ };
59
+ /**
60
+ * The property name under which companion payloads are carried inside the plain object produced by
61
+ * `BaseEntity.GetDataObject()` and consumed by `BaseEntity.LoadFromData()`.
62
+ *
63
+ * The trailing triple underscore follows the convention already set by `OldValues___` and
64
+ * `RestoreContext___`: it marks a reserved, non-field slot and makes accidental collision with a
65
+ * real column name effectively impossible.
66
+ */
67
+ export declare const COMPANION_PAYLOAD_KEY = "Companions___";
68
+ /**
69
+ * Which direction a companion payload is travelling, which changes what the receiver must do with
70
+ * it.
71
+ *
72
+ * - `'request'` — the payload is a **caller's intent**, arriving at the tier that will persist it.
73
+ * Records that already exist must be loaded from the database first, so dirty tracking and
74
+ * old-value comparisons are computed against real stored values. Skipping that load makes an
75
+ * edited record look clean (old == new), its save is skipped, and the edit is silently lost.
76
+ * - `'result'` — the payload is **authoritative post-save state** coming back from the tier that
77
+ * just persisted it. It is adopted as-is: no queries, and the records land clean. Re-loading here
78
+ * would be one wasted round trip per record for data the sender already has.
79
+ */
80
+ export type EntityCompanionDeserializeMode = 'request' | 'result';
81
+ /**
82
+ * Base class for all entity companions.
83
+ *
84
+ * Subclass this only when you need a genuinely new *kind* of companion. For the common case of a
85
+ * parent/child collection, use `RelatedRecordCollection<T>` via `BaseEntity.DeclareRelatedRecords()` rather than
86
+ * writing a companion by hand.
87
+ *
88
+ * @typeParam TWire - The JSON-safe shape this companion serializes to and from.
89
+ *
90
+ * @example Declaring a custom companion on a shared (client + server) entity subclass
91
+ * ```typescript
92
+ * class ApprovalTrailCompanion extends EntityCompanion<ApprovalWire> {
93
+ * public readonly Name = 'ApprovalTrail';
94
+ *
95
+ * public async Serialize(): Promise<ApprovalWire | null> {
96
+ * return this.entries.length ? { Entries: this.entries } : null;
97
+ * }
98
+ *
99
+ * public async Deserialize(data: ApprovalWire): Promise<void> {
100
+ * this.entries = data.Entries ?? [];
101
+ * }
102
+ * }
103
+ * ```
104
+ */
105
+ export declare abstract class EntityCompanion<TWire = unknown> {
106
+ /**
107
+ * The entity this companion is attached to. Set by {@link BaseEntity.RegisterCompanion}.
108
+ */
109
+ readonly Owner: BaseEntity;
110
+ /**
111
+ * @param owner - The entity this companion belongs to.
112
+ */
113
+ constructor(owner: BaseEntity);
114
+ /**
115
+ * Stable identifier for this companion, unique within its owning entity.
116
+ *
117
+ * This is the wire key: it appears in serialized payloads and is how the receiving tier finds
118
+ * the companion to deserialize into. **Treat it as a published contract** — renaming it breaks
119
+ * in-flight payloads and any persisted snapshot that captured them.
120
+ */
121
+ abstract readonly Name: string;
122
+ /**
123
+ * Produces this companion's JSON-safe state for transport, or `null` when it has nothing to
124
+ * send.
125
+ *
126
+ * Returning `null` keeps the companion out of the payload entirely, which matters: a save that
127
+ * touches only header fields should not ship an empty children array and pay for it on every
128
+ * request.
129
+ *
130
+ * @returns The wire payload, or `null` to omit this companion.
131
+ */
132
+ abstract Serialize(): Promise<TWire | null>;
133
+ /**
134
+ * Restores this companion's state from a wire payload produced by {@link Serialize} on the
135
+ * other tier.
136
+ *
137
+ * Implementations must be tolerant of payloads written by an older version of themselves —
138
+ * a companion is a wire contract, and rolling deploys mean both versions run at once.
139
+ *
140
+ * @param data - The payload previously produced by {@link Serialize}.
141
+ * @param mode - Whether this payload is an inbound request or an authoritative result. See
142
+ * {@link EntityCompanionDeserializeMode}; the distinction is load-bearing.
143
+ */
144
+ abstract Deserialize(data: TWire, mode: EntityCompanionDeserializeMode): Promise<void>;
145
+ /**
146
+ * Whether this companion holds unsaved changes.
147
+ *
148
+ * Rolled up into `BaseEntity.Dirty`, which is what makes a save actually happen when only the
149
+ * companion changed. Before companions existed, a clean parent with three new children returned
150
+ * early from `Save()` and silently persisted nothing.
151
+ *
152
+ * @returns True when saving would produce work.
153
+ */
154
+ get Dirty(): boolean;
155
+ /**
156
+ * Synchronous, in-memory validation contributed by this companion.
157
+ *
158
+ * Runs as part of the owner's `Validate()`, **before any write**, over the companion's complete
159
+ * state — including pending removals. That ordering is what lets cross-child invariants such as
160
+ * "debits must equal credits" be enforced correctly rather than after half the graph has landed.
161
+ *
162
+ * Push errors onto `result.Errors` and set `result.Success = false` to fail the save.
163
+ *
164
+ * @param _result - The accumulating validation result to contribute to.
165
+ */
166
+ Validate(_result: ValidationResult): void;
167
+ /**
168
+ * Asynchronous validation contributed by this companion — anything that needs a round trip.
169
+ *
170
+ * @remarks
171
+ * Unlike an entity's own `ValidateAsync()`, this is **not** governed by
172
+ * `BaseEntity.DefaultSkipAsyncValidation`. That flag exists so an entity can opt out of its own
173
+ * expensive async rules; applying it to companions silently skipped cross-child invariants,
174
+ * which is how `OrderEntityServer.ValidateAsync` came to be dead code on every save. Companion
175
+ * validation runs whenever the companion is dirty.
176
+ *
177
+ * @param _result - The accumulating validation result to contribute to.
178
+ */
179
+ ValidateAsync(_result: ValidationResult): Promise<void>;
180
+ /**
181
+ * Contributes this companion's work to the owner's save plan.
182
+ *
183
+ * Called after the owner's own node has been added, so implementations may assume the parent
184
+ * node exists and order their nodes relative to it. Add nothing when there is no work — an
185
+ * empty contribution keeps the save on the fast single-row path.
186
+ *
187
+ * @param _plan - The plan being assembled for this unit of work.
188
+ * @param _options - The caller's save options. Implementations that decide what counts as work
189
+ * (skipping clean children, most importantly) must honor flags such as
190
+ * `IgnoreDirtyState` that demand a full write-out.
191
+ */
192
+ ContributeSaveWork(_plan: EntitySavePlan, _options?: EntitySaveOptions): void;
193
+ /**
194
+ * Contributes this companion's work to the owner's delete plan.
195
+ *
196
+ * Called before the owner's own node, because children must generally be removed before the
197
+ * parent row they point at. Implementations that rely on database-level cascade delete should
198
+ * contribute nothing.
199
+ *
200
+ * @param _plan - The plan being assembled for this unit of work.
201
+ */
202
+ ContributeDeleteWork(_plan: EntitySavePlan): void;
203
+ /**
204
+ * Populates this companion from the database, when it is configured to load eagerly.
205
+ *
206
+ * Called by `BaseEntity.Load()` after the record's own fields are populated. **Never** called
207
+ * from `LoadFromData()` — that is the row-materialization path for
208
+ * `RunView(ResultType:'entity_object')`, so loading children there turns one view into an N+1
209
+ * storm. Set-oriented eager loading is handled by `RunView`'s batched child loading instead.
210
+ */
211
+ LoadEager(): Promise<void>;
212
+ /**
213
+ * Resets the companion to its post-save state — clearing pending removals, rebasing dirty
214
+ * tracking, and so on. Called after the graph commits successfully.
215
+ */
216
+ AcceptChanges(): void;
217
+ }
218
+ //# sourceMappingURL=entityCompanion.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"entityCompanion.d.ts","sourceRoot":"","sources":["../../src/generic/entityCompanion.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAyCG;AAEH,OAAO,EAAE,UAAU,EAAE,MAAM,cAAc,CAAC;AAC1C,OAAO,EAAE,gBAAgB,EAAE,MAAM,cAAc,CAAC;AAChD,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,kBAAkB,CAAC;AACvD,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,cAAc,CAAC;AAEtD;;;;;GAKG;AACH,MAAM,MAAM,sBAAsB,GAAG;IACjC,2DAA2D;IAC3D,IAAI,EAAE,MAAM,CAAC;IACb,uDAAuD;IACvD,IAAI,EAAE,OAAO,CAAC;CACjB,CAAC;AAEF;;;;;;;GAOG;AACH,eAAO,MAAM,qBAAqB,kBAAkB,CAAC;AAErD;;;;;;;;;;;GAWG;AACH,MAAM,MAAM,8BAA8B,GAAG,SAAS,GAAG,QAAQ,CAAC;AAElE;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AACH,8BAAsB,eAAe,CAAC,KAAK,GAAG,OAAO;IACjD;;OAEG;IACH,SAAgB,KAAK,EAAE,UAAU,CAAC;IAElC;;OAEG;gBACS,KAAK,EAAE,UAAU;IAI7B;;;;;;OAMG;IACH,kBAAyB,IAAI,EAAE,MAAM,CAAC;IAEtC;;;;;;;;;OASG;aACa,SAAS,IAAI,OAAO,CAAC,KAAK,GAAG,IAAI,CAAC;IAElD;;;;;;;;;;OAUG;aACa,WAAW,CAAC,IAAI,EAAE,KAAK,EAAE,IAAI,EAAE,8BAA8B,GAAG,OAAO,CAAC,IAAI,CAAC;IAE7F;;;;;;;;OAQG;IACH,IAAW,KAAK,IAAI,OAAO,CAE1B;IAED;;;;;;;;;;OAUG;IACI,QAAQ,CAAC,OAAO,EAAE,gBAAgB,GAAG,IAAI;IAIhD;;;;;;;;;;;OAWG;IACU,aAAa,CAAC,OAAO,EAAE,gBAAgB,GAAG,OAAO,CAAC,IAAI,CAAC;IAIpE;;;;;;;;;;;OAWG;IACI,kBAAkB,CAAC,KAAK,EAAE,cAAc,EAAE,QAAQ,CAAC,EAAE,iBAAiB,GAAG,IAAI;IAIpF;;;;;;;;OAQG;IACI,oBAAoB,CAAC,KAAK,EAAE,cAAc,GAAG,IAAI;IAIxD;;;;;;;OAOG;IACU,SAAS,IAAI,OAAO,CAAC,IAAI,CAAC;IAIvC;;;OAGG;IACI,aAAa,IAAI,IAAI;CAG/B"}
@@ -0,0 +1,170 @@
1
+ /**
2
+ * @fileoverview Entity Companions — named, serialisable side-channels attached to a `BaseEntity`.
3
+ *
4
+ * ## What a companion is
5
+ *
6
+ * A companion is a piece of state that belongs to a record but is **not one of its fields**, and
7
+ * that needs to travel with the record across the client/server boundary, participate in its
8
+ * validation, and contribute work to its save.
9
+ *
10
+ * The canonical example is a child collection — an order's lines, a journal entry's lines, a
11
+ * payment's allocations — but the abstraction is deliberately not about children. It is about
12
+ * "extra state that rides with the record", because MemberJunction has grown that concept ad hoc
13
+ * three times already:
14
+ *
15
+ * - `OldValues___` on the generated GraphQL save input (concurrency check payload)
16
+ * - `RestoreContext___` on the same input (restore lineage for the RecordChange row)
17
+ * - `EntityObjectJSON` in the TransactionGroup wire format (a whole record as opaque JSON)
18
+ *
19
+ * Each solved its own problem with its own bespoke slot, resolver branch and hand-written
20
+ * serialisation. Companions give the next such need — and the ones already in flight — one
21
+ * mechanism instead of a fourth bespoke field.
22
+ *
23
+ * ## What a companion is not
24
+ *
25
+ * A companion is **not** a way to smuggle arbitrary untyped data through the platform. Every
26
+ * companion has a stable {@link EntityCompanion.Name}, a typed wire shape, and explicit control
27
+ * over what it contributes to validation and save. Data with no owner and no contract belongs in a
28
+ * field, not here.
29
+ *
30
+ * ## Lifecycle
31
+ *
32
+ * ```text
33
+ * declare → subclass constructor calls RegisterCompanion() (usually via DeclareRelatedRecords())
34
+ * load → BaseEntity.Load()/LoadFromData() gives eager companions a chance to populate
35
+ * mutate → application code works with the typed companion API
36
+ * validate → BaseEntity.Validate()/ValidateAsync() fans out to every companion
37
+ * save → companions contribute nodes to the EntitySavePlan; the graph executes atomically
38
+ * transport→ Serialize() on the way out, Deserialize() on the way in
39
+ * ```
40
+ *
41
+ * @module @memberjunction/core
42
+ */
43
+ /**
44
+ * The property name under which companion payloads are carried inside the plain object produced by
45
+ * `BaseEntity.GetDataObject()` and consumed by `BaseEntity.LoadFromData()`.
46
+ *
47
+ * The trailing triple underscore follows the convention already set by `OldValues___` and
48
+ * `RestoreContext___`: it marks a reserved, non-field slot and makes accidental collision with a
49
+ * real column name effectively impossible.
50
+ */
51
+ export const COMPANION_PAYLOAD_KEY = 'Companions___';
52
+ /**
53
+ * Base class for all entity companions.
54
+ *
55
+ * Subclass this only when you need a genuinely new *kind* of companion. For the common case of a
56
+ * parent/child collection, use `RelatedRecordCollection<T>` via `BaseEntity.DeclareRelatedRecords()` rather than
57
+ * writing a companion by hand.
58
+ *
59
+ * @typeParam TWire - The JSON-safe shape this companion serializes to and from.
60
+ *
61
+ * @example Declaring a custom companion on a shared (client + server) entity subclass
62
+ * ```typescript
63
+ * class ApprovalTrailCompanion extends EntityCompanion<ApprovalWire> {
64
+ * public readonly Name = 'ApprovalTrail';
65
+ *
66
+ * public async Serialize(): Promise<ApprovalWire | null> {
67
+ * return this.entries.length ? { Entries: this.entries } : null;
68
+ * }
69
+ *
70
+ * public async Deserialize(data: ApprovalWire): Promise<void> {
71
+ * this.entries = data.Entries ?? [];
72
+ * }
73
+ * }
74
+ * ```
75
+ */
76
+ export class EntityCompanion {
77
+ /**
78
+ * @param owner - The entity this companion belongs to.
79
+ */
80
+ constructor(owner) {
81
+ this.Owner = owner;
82
+ }
83
+ /**
84
+ * Whether this companion holds unsaved changes.
85
+ *
86
+ * Rolled up into `BaseEntity.Dirty`, which is what makes a save actually happen when only the
87
+ * companion changed. Before companions existed, a clean parent with three new children returned
88
+ * early from `Save()` and silently persisted nothing.
89
+ *
90
+ * @returns True when saving would produce work.
91
+ */
92
+ get Dirty() {
93
+ return false;
94
+ }
95
+ /**
96
+ * Synchronous, in-memory validation contributed by this companion.
97
+ *
98
+ * Runs as part of the owner's `Validate()`, **before any write**, over the companion's complete
99
+ * state — including pending removals. That ordering is what lets cross-child invariants such as
100
+ * "debits must equal credits" be enforced correctly rather than after half the graph has landed.
101
+ *
102
+ * Push errors onto `result.Errors` and set `result.Success = false` to fail the save.
103
+ *
104
+ * @param _result - The accumulating validation result to contribute to.
105
+ */
106
+ Validate(_result) {
107
+ /* no-op by default */
108
+ }
109
+ /**
110
+ * Asynchronous validation contributed by this companion — anything that needs a round trip.
111
+ *
112
+ * @remarks
113
+ * Unlike an entity's own `ValidateAsync()`, this is **not** governed by
114
+ * `BaseEntity.DefaultSkipAsyncValidation`. That flag exists so an entity can opt out of its own
115
+ * expensive async rules; applying it to companions silently skipped cross-child invariants,
116
+ * which is how `OrderEntityServer.ValidateAsync` came to be dead code on every save. Companion
117
+ * validation runs whenever the companion is dirty.
118
+ *
119
+ * @param _result - The accumulating validation result to contribute to.
120
+ */
121
+ async ValidateAsync(_result) {
122
+ /* no-op by default */
123
+ }
124
+ /**
125
+ * Contributes this companion's work to the owner's save plan.
126
+ *
127
+ * Called after the owner's own node has been added, so implementations may assume the parent
128
+ * node exists and order their nodes relative to it. Add nothing when there is no work — an
129
+ * empty contribution keeps the save on the fast single-row path.
130
+ *
131
+ * @param _plan - The plan being assembled for this unit of work.
132
+ * @param _options - The caller's save options. Implementations that decide what counts as work
133
+ * (skipping clean children, most importantly) must honor flags such as
134
+ * `IgnoreDirtyState` that demand a full write-out.
135
+ */
136
+ ContributeSaveWork(_plan, _options) {
137
+ /* no-op by default */
138
+ }
139
+ /**
140
+ * Contributes this companion's work to the owner's delete plan.
141
+ *
142
+ * Called before the owner's own node, because children must generally be removed before the
143
+ * parent row they point at. Implementations that rely on database-level cascade delete should
144
+ * contribute nothing.
145
+ *
146
+ * @param _plan - The plan being assembled for this unit of work.
147
+ */
148
+ ContributeDeleteWork(_plan) {
149
+ /* no-op by default */
150
+ }
151
+ /**
152
+ * Populates this companion from the database, when it is configured to load eagerly.
153
+ *
154
+ * Called by `BaseEntity.Load()` after the record's own fields are populated. **Never** called
155
+ * from `LoadFromData()` — that is the row-materialization path for
156
+ * `RunView(ResultType:'entity_object')`, so loading children there turns one view into an N+1
157
+ * storm. Set-oriented eager loading is handled by `RunView`'s batched child loading instead.
158
+ */
159
+ async LoadEager() {
160
+ /* no-op by default */
161
+ }
162
+ /**
163
+ * Resets the companion to its post-save state — clearing pending removals, rebasing dirty
164
+ * tracking, and so on. Called after the graph commits successfully.
165
+ */
166
+ AcceptChanges() {
167
+ /* no-op by default */
168
+ }
169
+ }
170
+ //# sourceMappingURL=entityCompanion.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"entityCompanion.js","sourceRoot":"","sources":["../../src/generic/entityCompanion.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAyCG;AAoBH;;;;;;;GAOG;AACH,MAAM,CAAC,MAAM,qBAAqB,GAAG,eAAe,CAAC;AAgBrD;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AACH,MAAM,OAAgB,eAAe;IAMjC;;OAEG;IACH,YAAY,KAAiB;QACzB,IAAI,CAAC,KAAK,GAAG,KAAK,CAAC;IACvB,CAAC;IAoCD;;;;;;;;OAQG;IACH,IAAW,KAAK;QACZ,OAAO,KAAK,CAAC;IACjB,CAAC;IAED;;;;;;;;;;OAUG;IACI,QAAQ,CAAC,OAAyB;QACrC,sBAAsB;IAC1B,CAAC;IAED;;;;;;;;;;;OAWG;IACI,KAAK,CAAC,aAAa,CAAC,OAAyB;QAChD,sBAAsB;IAC1B,CAAC;IAED;;;;;;;;;;;OAWG;IACI,kBAAkB,CAAC,KAAqB,EAAE,QAA4B;QACzE,sBAAsB;IAC1B,CAAC;IAED;;;;;;;;OAQG;IACI,oBAAoB,CAAC,KAAqB;QAC7C,sBAAsB;IAC1B,CAAC;IAED;;;;;;;OAOG;IACI,KAAK,CAAC,SAAS;QAClB,sBAAsB;IAC1B,CAAC;IAED;;;OAGG;IACI,aAAa;QAChB,sBAAsB;IAC1B,CAAC;CACJ"}
@@ -73,6 +73,27 @@ export declare class EntityRelationshipInfo extends BaseInfo {
73
73
  * * 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.
74
74
  */
75
75
  AutoUpdateFromSchema: boolean;
76
+ /**
77
+ * * Field Name: RelatedRecordCollection
78
+ * * SQL Data Type: nvarchar(MAX), nullable
79
+ *
80
+ * Optional JSON policy object declaring this relationship as a first-class **related-record
81
+ * collection** — a set of records that load, validate and persist as one unit with the parent.
82
+ * When non-null, CodeGen emits a typed `DeclareRelatedRecords(...)` declaration onto the
83
+ * generated entity subclass, so applications no longer hand-write it.
84
+ *
85
+ * Shape is `IRelatedRecordCollectionConfig`: `Name` (the generated property name, e.g. `Lines`),
86
+ * `Load`, `OnRemove`, `OrderBy`, `Sequence` and `ClearAfterSave`. {@link RelatedEntity} and
87
+ * {@link RelatedEntityJoinField} are deliberately **not** repeated inside the JSON — they are
88
+ * already columns on this row, and duplicating them would create two sources of truth with the
89
+ * JSON copy winning silently.
90
+ *
91
+ * `null` (the default, and every pre-6.2 row) means the relationship is not a declared
92
+ * collection: nothing is generated and nothing loads eagerly.
93
+ *
94
+ * @see guides/TRANSACTIONS_AND_BATCHING_GUIDE.md
95
+ */
96
+ RelatedRecordCollection: string;
76
97
  Entity: string;
77
98
  EntityBaseTable: string;
78
99
  EntityBaseView: string;
@@ -954,6 +975,57 @@ export declare class EntityInfo extends BaseInfo {
954
975
  * Whether the base view is generated by CodeGen (true) or manually created (false)
955
976
  */
956
977
  BaseViewGenerated: boolean;
978
+ /**
979
+ * When set, CodeGen generates the entity's full base view under THIS name instead of
980
+ * {@link BaseView}, and the application owns `BaseView` — which is expected to wrap it:
981
+ *
982
+ * ```sql
983
+ * CREATE VIEW vwOrderHeaders AS
984
+ * SELECT g.*, CASE WHEN ... END AS IsOverdue
985
+ * FROM vwOrderHeadersGenerated g
986
+ * ```
987
+ *
988
+ * This is how an entity gets a custom base view WITHOUT inheriting the generated SQL. With
989
+ * `BaseViewGenerated = 0` alone the application takes over the whole view — every related-entity
990
+ * display join, the geo join, the recursive root-ID apply — and must hand-maintain it forever;
991
+ * a foreign key added later then silently never appears, because nothing regenerates the join.
992
+ * Naming an inner view keeps all of that regenerating underneath a thin, reviewable custom layer.
993
+ *
994
+ * NULL (the default, and every pre-existing entity) preserves the original behaviour exactly:
995
+ * `BaseViewGenerated` alone decides whether CodeGen writes `BaseView`, and there is no second view.
996
+ *
997
+ * `BaseView` remains the public surface either way — field discovery, permissions and the
998
+ * generated CRUD procedures all target it, so a column added by the custom layer becomes a
999
+ * first-class virtual `EntityField`.
1000
+ */
1001
+ GeneratedBaseViewName: string;
1002
+ /**
1003
+ * The view CodeGen actually WRITES for this entity.
1004
+ *
1005
+ * Normally {@link BaseView}. When {@link GeneratedBaseViewName} is set, the generated SQL goes
1006
+ * there instead and `BaseView` belongs to the application, which layers over it — see
1007
+ * {@link HasLayeredBaseView}.
1008
+ *
1009
+ * Resolved in ONE place because the two names must never drift: several call sites decide where
1010
+ * to write the view, what to call the emitted file, and which object to refresh, and a
1011
+ * disagreement between any two of them produces a view that exists under a name nothing reads.
1012
+ *
1013
+ * Derived FROM {@link HasLayeredBaseView} rather than re-testing `GeneratedBaseViewName`, so the
1014
+ * two getters cannot disagree by construction. Testing the raw column here would diverge on a
1015
+ * name that differs from `BaseView` only by case: `HasLayeredBaseView` would say "not layered"
1016
+ * (it compares case-insensitively, because SQL Server object names are) while this getter
1017
+ * returned the differently-cased string — leaving CodeGen writing to one object while every
1018
+ * layering-gated code path believed there was no second view at all.
1019
+ */
1020
+ get GeneratedViewName(): string;
1021
+ /**
1022
+ * True when this entity has a generated inner view with an application-owned `BaseView` on top.
1023
+ *
1024
+ * In that arrangement CodeGen still generates everything — related-entity display fields, geo
1025
+ * columns, recursive root-ID columns — into {@link GeneratedViewName}, so the custom layer stays
1026
+ * thin and does not go stale when the schema gains a foreign key.
1027
+ */
1028
+ get HasLayeredBaseView(): boolean;
957
1029
  /**
958
1030
  * Database schema that contains this entity's table and view
959
1031
  */
@@ -1038,6 +1110,47 @@ export declare class EntityInfo extends BaseInfo {
1038
1110
  * Global flag controlling whether records can be deleted via API
1039
1111
  */
1040
1112
  AllowDeleteAPI: boolean;
1113
+ /**
1114
+ * Whether rows may be INSERTed by SQL that does not go through {@link BaseEntity}.Save() —
1115
+ * bulk loads, ETL/integration sync, or rows created as a side effect of a stored procedure.
1116
+ *
1117
+ * `false` (the default, and every entity that has not opted in) means all inserts are expected
1118
+ * to flow through `BaseEntity`, which is the only path where record-change tracking, entity
1119
+ * actions, validation and cache invalidation actually run.
1120
+ *
1121
+ * **This DECLARES intent; it enforces nothing.** No constraint, trigger or grant prevents
1122
+ * anyone from executing SQL. It exists so the code paths and tooling that *choose* to honour
1123
+ * the platform contract — bulk/ETL and integration sync, record-set processing, and generators
1124
+ * or agents authoring SQL — have one authoritative answer instead of tribal knowledge.
1125
+ *
1126
+ * A database CHECK requires {@link TrackRecordChanges} and {@link TrustServerCacheCompletely}
1127
+ * to both be `false` when this is set, because a direct insert produces neither an audit row
1128
+ * nor a cache-invalidation event — leaving either on yields an audit trail that looks complete
1129
+ * but is not, and a server cache that serves stale rows indefinitely.
1130
+ */
1131
+ AllowDirectSQLInsert: boolean;
1132
+ /**
1133
+ * Whether rows may be UPDATEd by SQL that does not go through {@link BaseEntity}.Save() —
1134
+ * bulk backfills, integration sync, or maintenance routines.
1135
+ *
1136
+ * `false` (the default) means all updates are expected to flow through `BaseEntity`. See
1137
+ * {@link AllowDirectSQLInsert} for the full rationale, the "declares, does not enforce"
1138
+ * caveat, and the `TrackRecordChanges` / `TrustServerCacheCompletely` requirement.
1139
+ */
1140
+ AllowDirectSQLUpdate: boolean;
1141
+ /**
1142
+ * Whether rows may be DELETEd by SQL that does not go through {@link BaseEntity}.Delete() —
1143
+ * purge and retention routines, or integration sync reconciling against a remote source.
1144
+ *
1145
+ * `false` (the default) means all deletes are expected to flow through `BaseEntity`. See
1146
+ * {@link AllowDirectSQLInsert} for the full rationale and the `TrackRecordChanges` /
1147
+ * {@link TrustServerCacheCompletely} requirement.
1148
+ *
1149
+ * Additionally requires {@link DeleteType} to be `'Hard'`: a direct `DELETE` removes the row
1150
+ * outright rather than setting `DeletedAt`, so sanctioning it on a soft-delete entity would
1151
+ * quietly defeat soft delete. A database CHECK refuses the combination.
1152
+ */
1153
+ AllowDirectSQLDelete: boolean;
1041
1154
  /**
1042
1155
  * If true, uses a custom resolver for GraphQL operations instead of standard CRUD
1043
1156
  */
@@ -1483,12 +1596,45 @@ export declare class EntityInfo extends BaseInfo {
1483
1596
  GetUserRowLevelSecurityInfo(user: UserInfo, type: EntityPermissionType): RowLevelSecurityFilterInfo[];
1484
1597
  /**
1485
1598
  * Generates a where clause for SQL filtering for a given entity for a given user and permission type. If there is no RLS for a given entity or the user is exempt from RLS for the entity, a blank string is returned.
1599
+ *
1600
+ * @deprecated ROLE RLS ONLY — this method is subject to the role-RLS exemption and
1601
+ * silently omits API-key row filters, so a caller reaching for this familiar name gets
1602
+ * a clause that is fail-open for filtered API-key sessions. Use
1603
+ * {@link GetEffectiveRowFilterWhereClause}, which composes every filter layer. A repo
1604
+ * test asserts no non-test caller exists outside that method.
1486
1605
  * @param user
1487
1606
  * @param type
1488
1607
  * @param returnPrefix
1489
1608
  * @returns
1490
1609
  */
1491
1610
  GetUserRowLevelSecurityWhereClause(user: UserInfo, type: EntityPermissionType, returnPrefix: string): string;
1611
+ /**
1612
+ * The EFFECTIVE row-filter clause for a user + permission type: role RLS (subject to the
1613
+ * role exemption) AND the API-key row filters carried on the session (NOT subject to the
1614
+ * role exemption — a key ceiling exists precisely to bind principals whose roles are
1615
+ * unrestricted; see UserExemptFromRowLevelSecurity, which exempts off the mere presence
1616
+ * of a filter-less permission row). Composition: OR within the role layer (roles are
1617
+ * additive), AND across layers (no layer can widen another). Returns '' only when no
1618
+ * layer contributes.
1619
+ *
1620
+ * This is THE method every enforcement point must call. Deterministic by construction —
1621
+ * key-filter clauses render in FilterID order and list tokens sort their elements —
1622
+ * because the identical clause participates in the RunView cache fingerprint (INV-2):
1623
+ * any nondeterminism silently splits or merges cache slots.
1624
+ *
1625
+ * The application-ceiling layer (APIApplicationScope.RowFilterID) is deferred to v2 by
1626
+ * design decision; the column ships unused and the term composes here when it lands.
1627
+ */
1628
+ GetEffectiveRowFilterWhereClause(user: UserInfo, type: EntityPermissionType, returnPrefix: string): string;
1629
+ /**
1630
+ * Resolves the API-key row-filter clause for this entity + permission type from the
1631
+ * bindings stamped on the session UserInfo. Multiple matching bindings AND together
1632
+ * (most-restrictive-wins — an unfiltered grant elsewhere cannot cancel a filter).
1633
+ * Fail-closed at every edge: a binding whose filter is missing from metadata (dangling
1634
+ * or unloaded) contributes `(1=0)`, and unresolved tokens inside a filter collapse it
1635
+ * to `(1=0)` via the match-nothing markup mode.
1636
+ */
1637
+ private getAPIKeyRowFilterClause;
1492
1638
  /**
1493
1639
  * Returns a RunViewParams object that is setup to filter the related entity for the provided record
1494
1640
  * @param record