@mj-biz-apps/contracts-core-entities-server 0.1.0
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 +183 -0
- package/dist/ContractEntityServer.d.ts +352 -0
- package/dist/ContractEntityServer.d.ts.map +1 -0
- package/dist/ContractEntityServer.js +729 -0
- package/dist/ContractEntityServer.js.map +1 -0
- package/dist/ContractTemplateEntityServer.d.ts +39 -0
- package/dist/ContractTemplateEntityServer.d.ts.map +1 -0
- package/dist/ContractTemplateEntityServer.js +99 -0
- package/dist/ContractTemplateEntityServer.js.map +1 -0
- package/dist/ContractTemplateModificationEntityServer.d.ts +119 -0
- package/dist/ContractTemplateModificationEntityServer.d.ts.map +1 -0
- package/dist/ContractTemplateModificationEntityServer.js +242 -0
- package/dist/ContractTemplateModificationEntityServer.js.map +1 -0
- package/dist/ContractTemplateProvisionEntityServer.d.ts +74 -0
- package/dist/ContractTemplateProvisionEntityServer.d.ts.map +1 -0
- package/dist/ContractTemplateProvisionEntityServer.js +171 -0
- package/dist/ContractTemplateProvisionEntityServer.js.map +1 -0
- package/dist/ContractTypeEntityServers.d.ts +28 -0
- package/dist/ContractTypeEntityServers.d.ts.map +1 -0
- package/dist/ContractTypeEntityServers.js +65 -0
- package/dist/ContractTypeEntityServers.js.map +1 -0
- package/dist/SupersedeOperation.d.ts +53 -0
- package/dist/SupersedeOperation.d.ts.map +1 -0
- package/dist/SupersedeOperation.js +112 -0
- package/dist/SupersedeOperation.js.map +1 -0
- package/dist/delete-guard.d.ts +89 -0
- package/dist/delete-guard.d.ts.map +1 -0
- package/dist/delete-guard.js +107 -0
- package/dist/delete-guard.js.map +1 -0
- package/dist/index.d.ts +15 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +49 -0
- package/dist/index.js.map +1 -0
- package/package.json +36 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
Business Source License 1.1
|
|
2
|
+
|
|
3
|
+
License text copyright (c) 2024 MariaDB plc, All Rights Reserved.
|
|
4
|
+
"Business Source License" is a trademark of MariaDB plc.
|
|
5
|
+
|
|
6
|
+
-----------------------------------------------------------------------------
|
|
7
|
+
|
|
8
|
+
Parameters
|
|
9
|
+
|
|
10
|
+
Licensor: Blue Cypress, Inc.
|
|
11
|
+
|
|
12
|
+
Licensed Work: MemberJunction.
|
|
13
|
+
The Licensed Work is (c) 2023-2026 Blue Cypress, Inc.
|
|
14
|
+
|
|
15
|
+
Additional Use Grant: Subject to the terms of this License, Licensor grants
|
|
16
|
+
you the following additional rights to make Production
|
|
17
|
+
Use of the Licensed Work.
|
|
18
|
+
|
|
19
|
+
1. Internal Use
|
|
20
|
+
|
|
21
|
+
You may make production use of the Licensed Work for
|
|
22
|
+
your own internal business or organizational operations.
|
|
23
|
+
|
|
24
|
+
2. Nonprofit Use
|
|
25
|
+
|
|
26
|
+
If you are a Nonprofit, you may make production use of
|
|
27
|
+
the Licensed Work for the operations and activities of
|
|
28
|
+
your Organizational Family.
|
|
29
|
+
|
|
30
|
+
3. MemberJunction Certified Program Use
|
|
31
|
+
|
|
32
|
+
If you are authorized by Licensor under the
|
|
33
|
+
MemberJunction Certified Program to provide professional
|
|
34
|
+
services using the Licensed Work, you may make
|
|
35
|
+
production use of the Licensed Work in providing such
|
|
36
|
+
professional services to a client, provided that:
|
|
37
|
+
|
|
38
|
+
(a) the Licensed Work is deployed in, and the applicable
|
|
39
|
+
production use occurs within, an environment owned,
|
|
40
|
+
leased, licensed, subscribed to, or otherwise controlled
|
|
41
|
+
by that client; and
|
|
42
|
+
|
|
43
|
+
(b) the production use is for that client's own internal
|
|
44
|
+
business or organizational operations or is otherwise
|
|
45
|
+
independently permitted to that client under this
|
|
46
|
+
Additional Use Grant.
|
|
47
|
+
|
|
48
|
+
4. Definitions Applicable to the Additional Use Grant
|
|
49
|
+
|
|
50
|
+
"Affiliate" means, with respect to a specified Person,
|
|
51
|
+
any other Person that directly or indirectly Controls,
|
|
52
|
+
is Controlled by, or is under common Control with such
|
|
53
|
+
specified Person.
|
|
54
|
+
|
|
55
|
+
"Control" (including the terms "Controls," "Controlled
|
|
56
|
+
by," and "under common Control with") means the direct
|
|
57
|
+
or indirect possession of the power to direct or cause
|
|
58
|
+
the direction of the management and policies of a
|
|
59
|
+
Person, whether through ownership of voting interests,
|
|
60
|
+
by contract, or otherwise.
|
|
61
|
+
|
|
62
|
+
"Organizational Family" means, with respect to a Person,
|
|
63
|
+
(a) such Person and its Affiliates, and (b) any
|
|
64
|
+
nonprofit organization, governmental entity, chapter,
|
|
65
|
+
division, local affiliate, regional affiliate, state
|
|
66
|
+
affiliate, national affiliate, or other entity that is
|
|
67
|
+
formally affiliated with such Person through governing
|
|
68
|
+
documents, a charter, bylaws, a membership agreement, or
|
|
69
|
+
another written organizational instrument, and is
|
|
70
|
+
recognized under such documents as part of the same
|
|
71
|
+
organizational structure.
|
|
72
|
+
|
|
73
|
+
"Nonprofit" means a Person recognized by the Internal
|
|
74
|
+
Revenue Service as exempt from federal income taxation
|
|
75
|
+
under Section 501(c)(3), 501(c)(4), 501(c)(5), or
|
|
76
|
+
501(c)(6) of the Internal Revenue Code, or a foreign
|
|
77
|
+
organization recognized under substantially equivalent
|
|
78
|
+
laws.
|
|
79
|
+
|
|
80
|
+
A Person claiming eligibility as a Nonprofit shall, upon
|
|
81
|
+
Licensor's reasonable request, provide documentation
|
|
82
|
+
reasonably sufficient to demonstrate that it qualifies
|
|
83
|
+
as a Nonprofit. If such Person materially misrepresents,
|
|
84
|
+
or is unable to demonstrate, its qualification as a
|
|
85
|
+
Nonprofit, the rights granted to such Person under
|
|
86
|
+
Section 2 of this Additional Use Grant shall terminate.
|
|
87
|
+
|
|
88
|
+
"MemberJunction Certified Program" means Licensor's
|
|
89
|
+
then-current program for certifying and authorizing a
|
|
90
|
+
Person to provide professional services using the
|
|
91
|
+
Licensed Work.
|
|
92
|
+
|
|
93
|
+
"Person" means any individual, corporation, limited
|
|
94
|
+
liability company, partnership, association, nonprofit
|
|
95
|
+
organization, governmental entity, or other legal or
|
|
96
|
+
organizational entity.
|
|
97
|
+
|
|
98
|
+
Change Date: Four (4) years from the date the Licensed Work is first
|
|
99
|
+
made available.
|
|
100
|
+
|
|
101
|
+
Change License: MIT License.
|
|
102
|
+
|
|
103
|
+
For information about alternative licensing arrangements for the Licensed
|
|
104
|
+
Work, please contact Blue Cypress, Inc.
|
|
105
|
+
|
|
106
|
+
-----------------------------------------------------------------------------
|
|
107
|
+
|
|
108
|
+
Terms
|
|
109
|
+
|
|
110
|
+
The Licensor hereby grants you the right to copy, modify, create derivative
|
|
111
|
+
works, redistribute, and make non-production use of the Licensed Work. The
|
|
112
|
+
Licensor may make an Additional Use Grant, above, permitting limited
|
|
113
|
+
production use.
|
|
114
|
+
|
|
115
|
+
Effective on the Change Date, or the fourth anniversary of the first publicly
|
|
116
|
+
available distribution of a specific version of the Licensed Work under this
|
|
117
|
+
License, whichever comes first, the Licensor hereby grants you rights under
|
|
118
|
+
the terms of the Change License, and the rights granted in the paragraph
|
|
119
|
+
above terminate.
|
|
120
|
+
|
|
121
|
+
If your use of the Licensed Work does not comply with the requirements
|
|
122
|
+
currently in effect as described in this License, you must purchase a
|
|
123
|
+
commercial license from the Licensor, its affiliated entities, or authorized
|
|
124
|
+
resellers, or you must refrain from using the Licensed Work.
|
|
125
|
+
|
|
126
|
+
All copies of the original and modified Licensed Work, and derivative works
|
|
127
|
+
of the Licensed Work, are subject to this License. This License applies
|
|
128
|
+
separately for each version of the Licensed Work and the Change Date may vary
|
|
129
|
+
for each version of the Licensed Work released by Licensor.
|
|
130
|
+
|
|
131
|
+
You must conspicuously display this License on each original or modified copy
|
|
132
|
+
of the Licensed Work. If you receive the Licensed Work in original or
|
|
133
|
+
modified form from a third party, the terms and conditions set forth in this
|
|
134
|
+
License apply to your use of that work.
|
|
135
|
+
|
|
136
|
+
Any use of the Licensed Work in violation of this License will automatically
|
|
137
|
+
terminate your rights under this License for the current and all other
|
|
138
|
+
versions of the Licensed Work.
|
|
139
|
+
|
|
140
|
+
This License does not grant you any right in any trademark or logo of
|
|
141
|
+
Licensor or its affiliates (provided that you may use a trademark or logo of
|
|
142
|
+
Licensor as expressly required by this License).
|
|
143
|
+
|
|
144
|
+
TO THE EXTENT PERMITTED BY APPLICABLE LAW, THE LICENSED WORK IS PROVIDED ON
|
|
145
|
+
AN "AS IS" BASIS. LICENSOR HEREBY DISCLAIMS ALL WARRANTIES AND CONDITIONS,
|
|
146
|
+
EXPRESS OR IMPLIED, INCLUDING (WITHOUT LIMITATION) WARRANTIES OF
|
|
147
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, NON-INFRINGEMENT, AND
|
|
148
|
+
TITLE.
|
|
149
|
+
|
|
150
|
+
MariaDB hereby grants you permission to use this License's text to license
|
|
151
|
+
your works, and to refer to it using the trademark "Business Source License",
|
|
152
|
+
as long as you comply with the Covenants of Licensor below.
|
|
153
|
+
|
|
154
|
+
-----------------------------------------------------------------------------
|
|
155
|
+
|
|
156
|
+
Covenants of Licensor
|
|
157
|
+
|
|
158
|
+
In consideration of the right to use this License's text and the "Business
|
|
159
|
+
Source License" name and trademark, Licensor covenants to MariaDB, and to all
|
|
160
|
+
other recipients of the licensed work to be provided by Licensor:
|
|
161
|
+
|
|
162
|
+
1. To specify as the Change License the GPL Version 2.0 or any later version,
|
|
163
|
+
or a license that is compatible with GPL Version 2.0 or a later version,
|
|
164
|
+
where "compatible" means that software provided under the Change License
|
|
165
|
+
can be included in a program with software provided under GPL Version 2.0
|
|
166
|
+
or a later version. Licensor may specify additional Change Licenses without
|
|
167
|
+
limitation.
|
|
168
|
+
|
|
169
|
+
2. To either: (a) specify an additional grant of rights to use that does not
|
|
170
|
+
impose any additional restriction on the right granted in this License, as
|
|
171
|
+
the Additional Use Grant; or (b) insert the text "None".
|
|
172
|
+
|
|
173
|
+
3. To specify a Change Date.
|
|
174
|
+
|
|
175
|
+
4. Not to modify this License in any other way.
|
|
176
|
+
|
|
177
|
+
-----------------------------------------------------------------------------
|
|
178
|
+
|
|
179
|
+
Notice
|
|
180
|
+
|
|
181
|
+
The Business Source License (this document, or the "License") is not an Open
|
|
182
|
+
Source license. However, the Licensed Work will eventually be made available
|
|
183
|
+
under an Open Source License, as stated in this License.
|
|
@@ -0,0 +1,352 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview `ContractEntityServer` — the rules that CANNOT run in a browser.
|
|
3
|
+
*
|
|
4
|
+
* Registered later than the shared `ContractEntity`, so MJ's ClassFactory resolves this one on the
|
|
5
|
+
* server while the browser keeps the shared class (plan §6.3). Everything a client should be able to
|
|
6
|
+
* preflight lives on the shared class instead; what is here needs a lock or a cross-entity read.
|
|
7
|
+
*
|
|
8
|
+
* @module @mj-biz-apps/contracts-core-entities-server
|
|
9
|
+
*/
|
|
10
|
+
import { BaseEntity, type EntityDeleteOptions, ValidationResult } from '@memberjunction/core';
|
|
11
|
+
import { ContractEntity } from '@mj-biz-apps/contracts-entities';
|
|
12
|
+
/**
|
|
13
|
+
* Whether a foreign key is being CHOSEN right now, as opposed to merely being present.
|
|
14
|
+
*
|
|
15
|
+
* A free function so it can be unit-tested against a duck-typed record without a provider — the same
|
|
16
|
+
* reason `ValidateValueLists` and `ExplainMissingRequiredFields` are free functions. The semantics
|
|
17
|
+
* are the whole point and they are easy to get subtly wrong, so they are pinned by tests rather than
|
|
18
|
+
* by this comment:
|
|
19
|
+
*
|
|
20
|
+
* · NEW record -> any value is a new selection. (accounting's identity lock skips this
|
|
21
|
+
* case, correctly for its purpose and wrongly for ours)
|
|
22
|
+
* · SAVED, key changed -> a new selection.
|
|
23
|
+
* · SAVED, key the same -> NOT a new selection, whatever the referenced row now says.
|
|
24
|
+
*
|
|
25
|
+
* The comparison is case-insensitive and string-wise because these are UUIDs, which MJ can hand back
|
|
26
|
+
* in either casing depending on how the record was loaded (MJ's UUID_COMPARISON_GUIDE). A `!==` here
|
|
27
|
+
* would report a change on a record nobody touched.
|
|
28
|
+
*/
|
|
29
|
+
export declare function IsNewlySelected(entity: BaseEntity, fieldName: string): boolean;
|
|
30
|
+
export declare class ContractEntityServer extends ContractEntity {
|
|
31
|
+
/**
|
|
32
|
+
* Mint `ContractNumber` on first save, then persist.
|
|
33
|
+
*
|
|
34
|
+
* WHY THE NUMBER IS MINTED HERE and not defaulted in SQL. It comes from a singleton counter that
|
|
35
|
+
* has to be taken under a lock inside the save's own transaction, so a rollback releases the
|
|
36
|
+
* number instead of burning it. A column default cannot do that, and a client cannot be trusted
|
|
37
|
+
* with it.
|
|
38
|
+
*
|
|
39
|
+
* Contracts needs no `SkipRelatedCollections` dance, unlike orders: nothing here prepares child
|
|
40
|
+
* rows before the graph executor runs, so the plain save carries `Modifications` with it.
|
|
41
|
+
*/
|
|
42
|
+
Save(): Promise<boolean>;
|
|
43
|
+
/**
|
|
44
|
+
* Server-authoritative rules that need to read another entity.
|
|
45
|
+
*
|
|
46
|
+
* SETTLE THE `HasModifications` UNKNOWN CASE. The shared `ContractEntity.Validate()` only rejects
|
|
47
|
+
* a false flag when it can PROVE rows exist — `Modifications.Count > 0`. On an ordinary form load
|
|
48
|
+
* the collection is not loaded, so `Count === 0` means *unknown*, and the shared guard stays
|
|
49
|
+
* silent by design rather than refusing a legitimate save.
|
|
50
|
+
*
|
|
51
|
+
* That design is only honest if something settles the unknown case, and until now nothing did:
|
|
52
|
+
* load a saved contract without its modifications, set `HasModifications = false`, save, and it
|
|
53
|
+
* succeeded — flag false with rows in the table, which ERD §4.4 explicitly rejects and which the
|
|
54
|
+
* modification subclass's own comment calls the single most misleading state this app can produce.
|
|
55
|
+
* The shared class's comment PROMISED this check existed. Caught on review of PR #9.
|
|
56
|
+
*
|
|
57
|
+
* One `EXISTS`, and only when it can matter (`IsSaved && HasModifications === false`), so the
|
|
58
|
+
* ordinary save pays nothing. The message is the shared class's, deliberately — the user should
|
|
59
|
+
* not be able to tell which tier refused them.
|
|
60
|
+
*
|
|
61
|
+
* THE TYPE DECIDES WHERE IN THE TREE A CONTRACT MAY SIT, and whether it needs its own paper.
|
|
62
|
+
* `ContractType.MustBeRoot` / `MustBeChild` / `TemplateRequired` — three booleans, all read from the
|
|
63
|
+
* one memoised type row. The rules need that row, which is a join the browser has no business
|
|
64
|
+
* doing, so the shared class cannot carry them (ERD §7.1).
|
|
65
|
+
*
|
|
66
|
+
* ⚠ TWO GENERATIONS OF DEFECT ARE BURIED HERE, and both are worth remembering.
|
|
67
|
+
*
|
|
68
|
+
* FIRST, this compared the type's NAME to the string 'Change Order'. A display name is not a rule:
|
|
69
|
+
* renaming that lookup row — an ordinary thing to do — silently stopped the check from ever firing,
|
|
70
|
+
* with nothing failing and no error to notice (fixed 2026-08-19).
|
|
71
|
+
*
|
|
72
|
+
* SECOND, the column that replaced the name match was a THREE-STATE STRING,
|
|
73
|
+
* `ParentStatusRequirement IN ('Required','Prohibited')` or NULL. Ruled out by Marcelo on
|
|
74
|
+
* 2026-08-20 as confusing and overly complex, with the transposition argument as the proof: a
|
|
75
|
+
* column whose values INVERT the rule when read in the wrong order is a column that will
|
|
76
|
+
* eventually be read in the wrong order. Now two booleans, mutually exclusive by
|
|
77
|
+
* `CK_ContractType_RootOrChild`, and "both false" means unrestricted — a real and in fact majority
|
|
78
|
+
* state (V202608200300).
|
|
79
|
+
*
|
|
80
|
+
* Both placement directions are enforced, because a rule that only ever checks one of them is half
|
|
81
|
+
* decoration: `MustBeChild` refuses a missing parent (a change order that amends nothing would
|
|
82
|
+
* never appear in the original agreement's lineage), and `MustBeRoot` refuses a present one (a root
|
|
83
|
+
* agreement sitting under another contract is a lineage ring waiting to happen — see R-3).
|
|
84
|
+
*/
|
|
85
|
+
ValidateAsync(): Promise<ValidationResult>;
|
|
86
|
+
/**
|
|
87
|
+
* R-8 — a contract that other records point at cannot be deleted, and the refusal says who.
|
|
88
|
+
*
|
|
89
|
+
* THREE DEPENDENCIES, and the two lineage ones are the reason this is not obvious. A contract is
|
|
90
|
+
* referenced not only by its modifications but by any contract naming it as `ParentContractID`
|
|
91
|
+
* (its change orders) or as `SupersededByContractID` (the agreement it replaced). Both of those are
|
|
92
|
+
* self-references on `Contract`, so the rows blocking the delete are *other contracts* — and the
|
|
93
|
+
* user is looking at the record they wanted gone, not at the ones pointing to it. Naming them is
|
|
94
|
+
* the whole value.
|
|
95
|
+
*
|
|
96
|
+
* Deleting a superseded contract is the case worth spelling out: it is the *predecessor* in a
|
|
97
|
+
* re-papering chain, so removing it destroys the history the successor exists to continue.
|
|
98
|
+
*/
|
|
99
|
+
Delete(options?: EntityDeleteOptions): Promise<boolean>;
|
|
100
|
+
/**
|
|
101
|
+
* R-5 — A RETIRED TYPE MUST NOT BE *NEWLY* SELECTED, and the word doing the work is "newly".
|
|
102
|
+
*
|
|
103
|
+
* A contract that already references a type keeps working. We assume the type was Active when it
|
|
104
|
+
* was chosen, and retiring a type is not a statement about the contracts already signed under it —
|
|
105
|
+
* refusing to save an existing contract because a lookup row was retired years later would make
|
|
106
|
+
* the retirement itself a destructive act. So this is a rule about the EDIT, not about the row.
|
|
107
|
+
*
|
|
108
|
+
* "Newly selected" means one of exactly two things, and both must be covered:
|
|
109
|
+
* · the record is NEW and names the type at all, or
|
|
110
|
+
* · the record is SAVED and the foreign key CHANGED.
|
|
111
|
+
*
|
|
112
|
+
* That first clause is why this does not reuse `GLAccountEntityServer`'s shape verbatim.
|
|
113
|
+
* Accounting's identity lock returns early when `OldValue` is null — correct there, because it is
|
|
114
|
+
* asking "did an existing value change". Here the same early return would skip every CREATE, which
|
|
115
|
+
* is the primary case the rule exists for: the Configuration page promises a retired type "stops
|
|
116
|
+
* being offered for new contracts", and a create is the newest selection there is.
|
|
117
|
+
*
|
|
118
|
+
* TIER: code only, no trigger. `Status` lives on another table so a CHECK cannot see it, and the
|
|
119
|
+
* plan ruled a trigger not worth it — the failure mode is a person picking a retired value in our
|
|
120
|
+
* own UI, not silent corruption by an outside writer.
|
|
121
|
+
*
|
|
122
|
+
* COST: the `ContractTypeID` half is FREE — `typeRule()` already reads that row for the
|
|
123
|
+
* parent-status rule, and `Status` now rides along on the same RunView. The `ContractTemplateID`
|
|
124
|
+
* half costs two reads (template -> its type -> that type's `Status`) and only runs when the
|
|
125
|
+
* template FK is newly selected, which is rare. There is no single view exposing the template's
|
|
126
|
+
* type's status; `vwContractTemplates` carries the type's NAME, not its `Status`.
|
|
127
|
+
*/
|
|
128
|
+
private refuseRetiredSelections;
|
|
129
|
+
/**
|
|
130
|
+
* Whether this foreign key is being chosen right now, as opposed to merely being present.
|
|
131
|
+
*
|
|
132
|
+
* On a NEW record any value is a new selection. On a SAVED one, only a change counts — and the
|
|
133
|
+
* comparison is case-insensitively string-wise because these are UUIDs, which MJ can hand back in
|
|
134
|
+
* either casing depending on the path that loaded them (see MJ's UUID_COMPARISON_GUIDE). Comparing
|
|
135
|
+
* them with `!==` would report a change on a record nobody touched, which would refuse saves of
|
|
136
|
+
* contracts whose type is perfectly legal.
|
|
137
|
+
*/
|
|
138
|
+
private isNewlySelected;
|
|
139
|
+
/**
|
|
140
|
+
* The NAME of the template's type when that type is retired, or null.
|
|
141
|
+
*
|
|
142
|
+
* Two hops, because `Status` lives on `ContractTemplateType` and the contract points at a
|
|
143
|
+
* `ContractTemplate`. Returns the type's name rather than a boolean so the refusal can say WHICH
|
|
144
|
+
* kind of template was retired — "that template is an Order Form, which we no longer issue" is
|
|
145
|
+
* actionable where "invalid template" sends the user back to the same picker.
|
|
146
|
+
*/
|
|
147
|
+
private retiredTemplateType;
|
|
148
|
+
/**
|
|
149
|
+
* R-3 — a contract may not end up in its own lineage, on either axis.
|
|
150
|
+
*
|
|
151
|
+
* THE HOLE. `CK_Contract_ParentNotSelf` and `CK_Contract_SupersededNotSelf` stop only `A -> A`.
|
|
152
|
+
* Nothing stopped `A -> B -> A`, or a longer ring, because every individual save is legal in
|
|
153
|
+
* isolation: setting B's parent to A is fine, and later setting A's parent to B is fine — the row
|
|
154
|
+
* being written references a different row, which is all a CHECK can see.
|
|
155
|
+
*
|
|
156
|
+
* A cycle can only be created by THE EDGE BEING ADDED, which is what makes the check cheap: the
|
|
157
|
+
* question is not "is the graph acyclic" but "starting from the parent I am proposing, can I reach
|
|
158
|
+
* myself?" One upward walk along an indexed FK, one row per level, and change-order chains are 1–3
|
|
159
|
+
* deep in practice. `Depth < 50` caps it and — more importantly — means an ALREADY-corrupt ring
|
|
160
|
+
* terminates instead of spinning.
|
|
161
|
+
*
|
|
162
|
+
* ⚠ THE CONSEQUENCE OF A CYCLE IS WORSE THAN THE PLAN SAYS, and this is worth recording because it
|
|
163
|
+
* changes the tier. R-3 argued a cycle "corrupts nothing silently — it makes the Lineage panel walk
|
|
164
|
+
* forever, which is loud." That was true when written. It is not true now: `vwContracts` computes
|
|
165
|
+
* `RootParentContractID` and `RootSupersededByContractID` through
|
|
166
|
+
* `fnContract*_GetRootID`, whose CTE walks to `Depth < 100` and then selects the row
|
|
167
|
+
* `WHERE ParentContractID IS NULL`. In a ring **no row satisfies that**, so the function returns no
|
|
168
|
+
* row and the column comes back **NULL** — silently wrong, on the app's primary view, for every
|
|
169
|
+
* contract in the ring, while also costing 100 levels of recursion per row on every read. So a
|
|
170
|
+
* cycle is quiet after all, which is exactly the shape of defect this app keeps designing away.
|
|
171
|
+
*
|
|
172
|
+
* SKIPPED ON A CREATE. An unsaved contract has an ID but no row, so nothing can point at it and no
|
|
173
|
+
* walk can reach it. Skipped when the proposed target IS this record, too — that is `A -> A`, which
|
|
174
|
+
* the generated CHECK validator already reports, and two errors for one mistake is its own defect.
|
|
175
|
+
*/
|
|
176
|
+
private refuseLineageCycles;
|
|
177
|
+
/**
|
|
178
|
+
* A contract may not be its own parent, nor its own successor. Reported HERE, unconditionally.
|
|
179
|
+
*
|
|
180
|
+
* WHY THIS EXISTS when `CK_Contract_ParentNotSelf` / `CK_Contract_SupersededNotSelf` and their
|
|
181
|
+
* generated validators all cover it: because the generated validators DO NOT actually cover it.
|
|
182
|
+
* They are LLM-authored from the constraint text (`generateValidatorFunctionFromCheckConstraint`
|
|
183
|
+
* → the `CodeGen: Check Constraint Parser` prompt), and both emitted `===` on a `uniqueidentifier`:
|
|
184
|
+
*
|
|
185
|
+
* if (this.SupersededByContractID != null && this.SupersededByContractID === this.ID) { ... }
|
|
186
|
+
*
|
|
187
|
+
* MJ returns UUIDs in either casing depending on how a row was loaded, so a lowercase value against
|
|
188
|
+
* an uppercase `ID` is the SAME contract and that check silently misses it. Filed in MJ-UPSTREAM.md.
|
|
189
|
+
*
|
|
190
|
+
* AND THE RULE IS CHECKED WHOLE, not only the part the generated code misses. Covering just the
|
|
191
|
+
* casing-differs case and deferring the rest would bless a genuinely invalid state on an assumption
|
|
192
|
+
* about code this class does not own — and the moment that code changes, an invalid state starts
|
|
193
|
+
* reading as success. The cost of overlap is one duplicated message; the cost of the gap is a wrong
|
|
194
|
+
* save. This is also why the two lineage guards below delegate here rather than each deciding: one
|
|
195
|
+
* owner, checked fully, in this file, where the skip can be verified by reading it.
|
|
196
|
+
*/
|
|
197
|
+
private refuseSelfReferences;
|
|
198
|
+
/**
|
|
199
|
+
* A contract may only supersede a contract at the SAME LEVEL of the tree — same `ParentContractID`.
|
|
200
|
+
*
|
|
201
|
+
* WHY THIS IS NOT "ROOT ONLY" (ruled by Marcelo, 2026-08-20). The obvious-looking rule is that only
|
|
202
|
+
* a top-level agreement may be re-papered. It is wrong in both directions. A change order genuinely
|
|
203
|
+
* does get renegotiated and replaced by a second change order under the same order form — forbidding
|
|
204
|
+
* that would force somebody to DELETE the first one, destroying the history re-papering exists to
|
|
205
|
+
* preserve. And "root only" would still permit the incoherent case: a change order claiming to
|
|
206
|
+
* replace a whole order form, which asserts that a subordinate document replaced the agreement it
|
|
207
|
+
* hangs off.
|
|
208
|
+
*
|
|
209
|
+
* So the axis that matters is not depth, it is SIBLINGHOOD. `ParentContractID` says what a document
|
|
210
|
+
* sits under; `SupersededByContractID` says what replaced it. A supersession that crosses levels is
|
|
211
|
+
* the only genuinely meaningless combination, and same-parent is exactly the predicate that excludes
|
|
212
|
+
* it while allowing everything real: two roots (both NULL) or two siblings (same parent).
|
|
213
|
+
*
|
|
214
|
+
* TYPE IS DELIBERATELY NOT CONSTRAINED. An Order Form replaced by an Order Form is the common case,
|
|
215
|
+
* but a Payment Link customer graduating to signed paper is a real cross-type supersession — and
|
|
216
|
+
* `Payment Link -> Order Form` is precisely the upgrade path the two types exist to distinguish.
|
|
217
|
+
* Constraining `ContractTypeID` here would forbid it for no benefit.
|
|
218
|
+
*
|
|
219
|
+
* WHY THIS IS NOT A CHECK CONSTRAINT. The rule compares THIS row's `ParentContractID` with ANOTHER
|
|
220
|
+
* row's, and a CHECK sees only the row being written — the same reason `refuseLineageCycles` lives
|
|
221
|
+
* here. One indexed read of the target contract is the whole cost.
|
|
222
|
+
*
|
|
223
|
+
* RUNS ON CREATE TOO, unlike `refuseLineageCycles`. A cycle is impossible before the row exists
|
|
224
|
+
* (nothing can point at it yet), but a cross-level supersession is entirely possible on a brand-new
|
|
225
|
+
* contract — a successor composed in the browser names its predecessor before either is saved, which
|
|
226
|
+
* is the primary flow this rule exists to police. Gated on `isNewlySelected` instead, so an ordinary
|
|
227
|
+
* re-save of an existing chain is not re-policed and legacy rows are not retro-refused.
|
|
228
|
+
*/
|
|
229
|
+
private refuseCrossLevelSupersession;
|
|
230
|
+
/**
|
|
231
|
+
* Walk up `<column>` from `startID`; return the chain as text if it reaches this record, else null.
|
|
232
|
+
*
|
|
233
|
+
* Returns the CHAIN rather than a boolean so the refusal can show the ring — "A -> B -> A" is
|
|
234
|
+
* something a person can act on, where "cycle detected" sends them to guess which edge to remove.
|
|
235
|
+
*
|
|
236
|
+
* The column name comes from a fixed internal list, never from input, and both IDs are checked
|
|
237
|
+
* against a UUID shape before interpolation — `ExecuteSQL`'s object-parameter form needs the
|
|
238
|
+
* concrete `SQLServerDataProvider` type rather than the `DatabaseProviderBase` this class holds
|
|
239
|
+
* (see the note on `assignContractNumber`), so a guard plus interpolation is the honest option here
|
|
240
|
+
* rather than a cast that switches off checking.
|
|
241
|
+
*/
|
|
242
|
+
private lineageReachesSelf;
|
|
243
|
+
/**
|
|
244
|
+
* A shrotcut to get the datbaseProvider.
|
|
245
|
+
*
|
|
246
|
+
* `ExecuteSQL<T>(query, parameters?, options?, contextUser?)` is a public member of
|
|
247
|
+
* `DatabaseProviderBase`, which `@memberjunction/core` exports — so the four
|
|
248
|
+
* `as unknown as { ExecuteSQL: … }` casts this replaces were never necessary. Each of them
|
|
249
|
+
* re-declared a narrower signature than the real one: no generic (forcing a second cast on every
|
|
250
|
+
* result), and no `contextUser` parameter, which made the API look as though it could not carry a
|
|
251
|
+
* context user at all. Worse, `as unknown as` switches off checking, so a signature change
|
|
252
|
+
* upstream would have compiled here and failed at runtime.
|
|
253
|
+
*
|
|
254
|
+
* `ProviderToUse` is typed as the metadata-provider interface, which does not declare
|
|
255
|
+
* `ExecuteSQL`; this is a narrowing cast to the concrete base class, which is what a server-side
|
|
256
|
+
* entity subclass always has. It is one cast in one place instead of four scattered ones, and it
|
|
257
|
+
* keeps the real generic signature.
|
|
258
|
+
*/
|
|
259
|
+
private get db();
|
|
260
|
+
/**
|
|
261
|
+
* Whether any modification row exists for this contract, read from the DATABASE rather than the
|
|
262
|
+
* collection — which is the whole point: the collection is exactly what the browser could not
|
|
263
|
+
* know about. `TOP 1` because existence is the question, not the count.
|
|
264
|
+
*/
|
|
265
|
+
private modificationRowsExist;
|
|
266
|
+
/**
|
|
267
|
+
* What this contract's TYPE says about naming a parent: `'Required'`, `'Prohibited'` or null.
|
|
268
|
+
*
|
|
269
|
+
* One scalar read, no entity load, and memoised for the call — `ValidateAsync` asks for the
|
|
270
|
+
* requirement and then for the type's name to build the message, and a validation pass should not
|
|
271
|
+
* make two round trips to say one thing.
|
|
272
|
+
*/
|
|
273
|
+
private typeRule;
|
|
274
|
+
/**
|
|
275
|
+
* Cleared on nothing, and that is safe: a contract's type can change between saves, but this cache
|
|
276
|
+
* lives only for the duration of one `ValidateAsync` call tree — the field is re-read from the
|
|
277
|
+
* database on the next validation because a new entity instance is what a new save uses.
|
|
278
|
+
*/
|
|
279
|
+
private cachedTypeRule;
|
|
280
|
+
/**
|
|
281
|
+
* R-12 — a contract may not reference a template nobody can read.
|
|
282
|
+
*
|
|
283
|
+
* `IsUsable` is derived in `vwContractTemplates`: 1 when the template records a `SourceURL` **or**
|
|
284
|
+
* has a file attached, 0 when it has neither. The flag and this refusal do different jobs and both
|
|
285
|
+
* are needed (ruled 2026-08-20):
|
|
286
|
+
*
|
|
287
|
+
* · **the flag is the affordance** — someone authoring a template sees the red "Unusable" chip and
|
|
288
|
+
* fixes it, rather than being blocked by a condition they cannot see;
|
|
289
|
+
* · **this refusal is the floor** — a contract citing standard terms nobody can read is the actual
|
|
290
|
+
* harm, and it is worth stopping at the moment it would happen.
|
|
291
|
+
*
|
|
292
|
+
* Shipping only the flag leaves the harm unprevented; shipping only the refusal is the confusing
|
|
293
|
+
* version this item started as, where a person is refused for something invisible.
|
|
294
|
+
*
|
|
295
|
+
* ONLY ON A NEW REFERENCE. Whether an EXISTING contract should be affected when its template later
|
|
296
|
+
* becomes unusable is a lifecycle question, and it is R-15's — not this one. So this fires on the
|
|
297
|
+
* same `isNewlySelected` gate as R-5, which also means an ordinary re-save of an existing contract
|
|
298
|
+
* costs nothing.
|
|
299
|
+
*/
|
|
300
|
+
private refuseUnusableTemplate;
|
|
301
|
+
/**
|
|
302
|
+
* R-4 (certain half) — do not clear `ContractTemplateID` while modifications on THIS contract point
|
|
303
|
+
* at provisions of THAT template.
|
|
304
|
+
*
|
|
305
|
+
* Clearing it would leave every one of those modifications comparing negotiated language against
|
|
306
|
+
* standard language the contract no longer claims to incorporate — the modification rows survive
|
|
307
|
+
* and quietly stop meaning anything, which is the app's worst failure shape.
|
|
308
|
+
*
|
|
309
|
+
* ⚠ THIS IS THE NARROW HALF ON PURPOSE. The full rule is *"reject clearing when a modification
|
|
310
|
+
* ANYWHERE IN THE TREE references that template and it is not reachable elsewhere in the tree"*,
|
|
311
|
+
* which the plan DEFERS to ride with R-14 — not on cost (it is the same walk, and chains are 1–3
|
|
312
|
+
* deep) but on sequencing, so the ancestor walk is written once against a settled tree model. This
|
|
313
|
+
* half covers modifications on this contract only. A modification on a CHILD contract pointing at
|
|
314
|
+
* the template this contract is clearing is **not yet caught here**; the modification-side rule
|
|
315
|
+
* below refuses to SAVE such a row, so the gap is the pre-existing row, not a new one.
|
|
316
|
+
*/
|
|
317
|
+
private refuseClearingAReferencedTemplate;
|
|
318
|
+
/** How many of THIS contract's modifications cite a provision of the given template. */
|
|
319
|
+
private modificationsAgainstTemplate;
|
|
320
|
+
/**
|
|
321
|
+
* Mint the next contract number by calling the database's own numbering procedure.
|
|
322
|
+
*
|
|
323
|
+
* WHY A SPROC AND NOT `RunView` / `RunQuery` / `BaseEntity`. This is an atomic
|
|
324
|
+
* read-modify-write, and none of those can express one: `RunView` is read-only, `RunQuery` is a
|
|
325
|
+
* read surface (hiding DML inside a "query" would be worse than being honest about it), and the
|
|
326
|
+
* entity path removes the lock — two concurrent creates would both read N, both mint the same
|
|
327
|
+
* number, and the collision would surface later as a unique-index violation on the insert rather
|
|
328
|
+
* than as a serialised wait.
|
|
329
|
+
*
|
|
330
|
+
* So the lock stays in the database, in a DATABASE OBJECT rather than a TypeScript string.
|
|
331
|
+
* `spAssignNextContractNumber` (V202608192200) holds the `HOLDLOCK, UPDLOCK` and the format; this
|
|
332
|
+
* method is one dialect-free `EXEC`. That is accounting's pattern, adopted rather than invented —
|
|
333
|
+
* `SequenceService.ts` there keeps `spAssignNextJournalEntryNumber` at DB level for the same
|
|
334
|
+
* stated reason. Orders still inlines the same UPDATE in four places, which is what this stops
|
|
335
|
+
* doing.
|
|
336
|
+
*
|
|
337
|
+
* It also makes the PostgreSQL port a database exercise: write a PG function with this name and
|
|
338
|
+
* signature and no application code changes.
|
|
339
|
+
*
|
|
340
|
+
* `isMutation: true` matters and was missing while this was inline: MJ's SQL-statement logging
|
|
341
|
+
* uses it to know a statement CHANGED data, which is what lets a logging session capture the
|
|
342
|
+
* mutation. `description` names the operation in that log.
|
|
343
|
+
*
|
|
344
|
+
* ⚠ NOT gap-free. On a standalone save the number is consumed before the row's own transaction
|
|
345
|
+
* opens, so a later failure has already spent it; inside a graph save a rollback releases it. The
|
|
346
|
+
* unique index is the real guard.
|
|
347
|
+
*/
|
|
348
|
+
private assignContractNumber;
|
|
349
|
+
}
|
|
350
|
+
/** Anti-tree-shake anchor — see the note in index.ts. */
|
|
351
|
+
export declare function LoadContractEntityServer(): void;
|
|
352
|
+
//# sourceMappingURL=ContractEntityServer.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ContractEntityServer.d.ts","sourceRoot":"","sources":["../src/ContractEntityServer.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AACH,OAAO,EACH,UAAU,EAEV,KAAK,mBAAmB,EAIxB,gBAAgB,EACnB,MAAM,sBAAsB,CAAC;AAE9B,OAAO,EAAE,cAAc,EAAuB,MAAM,iCAAiC,CAAC;AAGtF;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAgB,eAAe,CAAC,MAAM,EAAE,UAAU,EAAE,SAAS,EAAE,MAAM,GAAG,OAAO,CAK9E;AAqBD,qBACa,oBAAqB,SAAQ,cAAc;IACpD;;;;;;;;;;OAUG;IACmB,IAAI,IAAI,OAAO,CAAC,OAAO,CAAC;IA0B9C;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;OAyCG;IACmB,aAAa,IAAI,OAAO,CAAC,gBAAgB,CAAC;IA+EhE;;;;;;;;;;;;OAYG;IACmB,MAAM,CAAC,OAAO,CAAC,EAAE,mBAAmB,GAAG,OAAO,CAAC,OAAO,CAAC;IAiC7E;;;;;;;;;;;;;;;;;;;;;;;;;;;OA2BG;YACW,uBAAuB;IAmCrC;;;;;;;;OAQG;IACH,OAAO,CAAC,eAAe;IAIvB;;;;;;;OAOG;YACW,mBAAmB;IA6BjC;;;;;;;;;;;;;;;;;;;;;;;;;;;OA2BG;YACW,mBAAmB;IA4CjC;;;;;;;;;;;;;;;;;;;OAmBG;IACH,OAAO,CAAC,oBAAoB;IAY5B;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;OA8BG;YACW,4BAA4B;IA6D1C;;;;;;;;;;;OAWG;YACW,kBAAkB;IA0BhC;;;;;;;;;;;;;;;OAeG;IACH,OAAO,KAAK,EAAE,GAEb;IAED;;;;OAIG;YACW,qBAAqB;IAkBnC;;;;;;OAMG;YACW,QAAQ;IAkBtB;;;;OAIG;IACH,OAAO,CAAC,cAAc,CAAkD;IAGxE;;;;;;;;;;;;;;;;;;;OAmBG;YACW,sBAAsB;IAuDpC;;;;;;;;;;;;;;;OAeG;YACW,iCAAiC;IA0B/C,wFAAwF;YAC1E,4BAA4B;IAgB1C;;;;;;;;;;;;;;;;;;;;;;;;;;;OA2BG;YACW,oBAAoB;CAyBrC;AAED,yDAAyD;AACzD,wBAAgB,wBAAwB,IAAI,IAAI,CAE/C"}
|