@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
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ContractTemplateProvisionEntityServer.d.ts","sourceRoot":"","sources":["../src/ContractTemplateProvisionEntityServer.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AACH,OAAO,EAEH,KAAK,mBAAmB,EAGxB,gBAAgB,EACnB,MAAM,sBAAsB,CAAC;AAE9B,OAAO,EAAE,+BAA+B,EAAsB,MAAM,iCAAiC,CAAC;AAmBtG,qBACa,qCAAsC,SAAQ,+BAA+B;IACtF;;;;;;;;;;;OAWG;IACmB,aAAa,IAAI,OAAO,CAAC,gBAAgB,CAAC;IAgDhE;;;;OAIG;IACmB,MAAM,CAAC,OAAO,CAAC,EAAE,mBAAmB,GAAG,OAAO,CAAC,OAAO,CAAC;IA6B7E;;;;;;;;;OASG;IACH,OAAO,CAAC,YAAY;IASpB;;;;;;;;;OASG;YACW,mBAAmB;CAmBpC;AAED,yDAAyD;AACzD,wBAAgB,yCAAyC,IAAI,IAAI,CAEhE"}
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
|
|
2
|
+
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
3
|
+
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
4
|
+
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
5
|
+
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
6
|
+
};
|
|
7
|
+
/**
|
|
8
|
+
* @fileoverview `ContractTemplateProvisionEntityServer` — a signed clause is a historical record.
|
|
9
|
+
*
|
|
10
|
+
* Two rules, both about the same fact: once a contract incorporates a template, that template's
|
|
11
|
+
* provisions are what somebody agreed to.
|
|
12
|
+
*
|
|
13
|
+
* **R-1** the TERMS (`ProvisionNumber`, `Title`, `ProvisionText`) cannot change, and the provision
|
|
14
|
+
* cannot be deleted, while any contract references its template.
|
|
15
|
+
* **R-8** and independently of R-1, a provision cannot be deleted while modifications cite it.
|
|
16
|
+
*
|
|
17
|
+
* ## The trigger is the floor; this class exists for the sentence
|
|
18
|
+
*
|
|
19
|
+
* `trg_ContractTemplateProvision_Immutability` (V202608200100) enforces R-1 against ANY writer — raw
|
|
20
|
+
* SQL, a future service, another app, a data load — because rewriting signed terms is the case where a
|
|
21
|
+
* bypass is silent corruption rather than a bad edit in our own UI. What a trigger cannot do is
|
|
22
|
+
* explain itself: it reaches the user as a raw SQL error naming no field.
|
|
23
|
+
*
|
|
24
|
+
* **That is not a nicety here, it is a correctness requirement for R-8.** R-8's whole point is that a
|
|
25
|
+
* refused delete reads as a sentence instead of a constraint. If this class checked only the
|
|
26
|
+
* modification dependency and let the trigger catch the template-referenced case, then deleting an
|
|
27
|
+
* unmodified provision of a referenced template would sail past the guard and die in the database —
|
|
28
|
+
* producing exactly the raw error R-8 was written to eliminate. So the code half must cover **both**
|
|
29
|
+
* conditions the trigger covers, or the guard has a hole shaped like the trigger.
|
|
30
|
+
*
|
|
31
|
+
* @module @mj-biz-apps/contracts-core-entities-server
|
|
32
|
+
*/
|
|
33
|
+
import { BaseEntity, ValidationErrorInfo, ValidationErrorType, } from '@memberjunction/core';
|
|
34
|
+
import { RegisterClass } from '@memberjunction/global';
|
|
35
|
+
import { ContractTemplateProvisionEntity, TEMPLATE_PUBLISHED } from '@mj-biz-apps/contracts-entities';
|
|
36
|
+
import { GuardedDelete, RefuseDelete, plural } from './delete-guard.js';
|
|
37
|
+
/**
|
|
38
|
+
* The columns that ARE the terms. `Description` and `Sequence` are deliberately absent: ordering a
|
|
39
|
+
* document and annotating it internally are not changing what it says, so they stay editable on a
|
|
40
|
+
* referenced template. This list must stay in step with the trigger's comparison — the trigger is the
|
|
41
|
+
* floor and a divergence would mean the code permits an edit the database then refuses raw.
|
|
42
|
+
*/
|
|
43
|
+
const FROZEN_TERM_FIELDS = [
|
|
44
|
+
{ Name: 'ProvisionNumber', Label: 'the clause number' },
|
|
45
|
+
{ Name: 'Title', Label: 'the clause title' },
|
|
46
|
+
{ Name: 'ProvisionText', Label: 'the standard wording' },
|
|
47
|
+
];
|
|
48
|
+
/*
|
|
49
|
+
* EXTENDS THE SHARED SUBCLASS, not the generated one — so an API caller gets the required-field prose
|
|
50
|
+
* too, not just a browser. Same reason `ContractEntityServer extends ContractEntity`.
|
|
51
|
+
*/
|
|
52
|
+
let ContractTemplateProvisionEntityServer = class ContractTemplateProvisionEntityServer extends ContractTemplateProvisionEntity {
|
|
53
|
+
/**
|
|
54
|
+
* R-1 — refuse an edit to the terms of a provision whose template is referenced.
|
|
55
|
+
*
|
|
56
|
+
* Gated on `IsSaved` (a provision being created cannot have been agreed to yet) and on a field
|
|
57
|
+
* actually being dirty, so the ordinary save of an unreferenced or cosmetically-edited provision
|
|
58
|
+
* pays **nothing** — no probe at all. The reference count runs only when a frozen field really
|
|
59
|
+
* changed, which is the shape that makes belt-and-braces affordable.
|
|
60
|
+
*
|
|
61
|
+
* Compares `OldValue` to `Value` in memory rather than probing the database, for the same reason
|
|
62
|
+
* `GLAccountEntityServer` does: the entity already knows what it loaded. One error per changed
|
|
63
|
+
* field, each naming that field, so the form marks the offending input rather than showing a banner.
|
|
64
|
+
*/
|
|
65
|
+
async ValidateAsync() {
|
|
66
|
+
const result = await super.ValidateAsync();
|
|
67
|
+
// CREATE on a published version. The trigger is the floor here too, but it cannot explain
|
|
68
|
+
// itself on this path at all: MJ calls the generated sproc as `INSERT ... EXEC`, inside which
|
|
69
|
+
// SQL Server forbids a trigger from issuing ROLLBACK, so the refusal arrived as
|
|
70
|
+
// "Cannot use the ROLLBACK statement within an INSERT-EXEC statement" — a message about the
|
|
71
|
+
// mechanism, naming neither the rule nor the field. This is the sentence a user should get.
|
|
72
|
+
if (!this.IsSaved) {
|
|
73
|
+
if (this.ContractTemplateID && (await this.templateIsPublished())) {
|
|
74
|
+
result.Success = false;
|
|
75
|
+
result.Errors.push(new ValidationErrorInfo('ContractTemplateID', `This agreement version is published, so a clause cannot be added to it — that would ` +
|
|
76
|
+
`silently grow the terms of every contract already referencing it. Publish a new ` +
|
|
77
|
+
`version instead.`, this.ContractTemplateID, ValidationErrorType.Failure));
|
|
78
|
+
}
|
|
79
|
+
return result;
|
|
80
|
+
}
|
|
81
|
+
const changed = FROZEN_TERM_FIELDS.filter((f) => this.fieldChanged(f.Name));
|
|
82
|
+
if (changed.length === 0)
|
|
83
|
+
return result;
|
|
84
|
+
// Only now is a read worth doing.
|
|
85
|
+
if (!(await this.templateIsPublished()))
|
|
86
|
+
return result;
|
|
87
|
+
for (const field of changed) {
|
|
88
|
+
result.Success = false;
|
|
89
|
+
result.Errors.push(new ValidationErrorInfo(field.Name, `This agreement version is published, so ${field.Label} cannot change — editing it would ` +
|
|
90
|
+
`rewrite what any customer who signed against it agreed to. Publish a new version instead; ` +
|
|
91
|
+
`a published version is a historical record. (The description is still editable.)`, this.Get(field.Name), ValidationErrorType.Failure));
|
|
92
|
+
}
|
|
93
|
+
return result;
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* Both delete conditions — R-8's modifications, and R-1's referenced template.
|
|
97
|
+
*
|
|
98
|
+
* Reported together rather than first-wins, so someone is not refused twice for two reasons.
|
|
99
|
+
*/
|
|
100
|
+
async Delete(options) {
|
|
101
|
+
// The published check first, and separately from the dependency list, because it is not a
|
|
102
|
+
// dependency — nothing points at this row, the VERSION is simply closed. Pre-empting the
|
|
103
|
+
// trigger here is what turns its raw SQL error into a sentence (the whole point of R-8).
|
|
104
|
+
if (await this.templateIsPublished()) {
|
|
105
|
+
return RefuseDelete(this, `Provision ${this.ProvisionNumber ?? ''} cannot be deleted: this agreement version is published, ` +
|
|
106
|
+
`and removing a clause from an agreement someone signed is the same act as rewriting it. ` +
|
|
107
|
+
`Publish a new version instead.`);
|
|
108
|
+
}
|
|
109
|
+
return GuardedDelete(this, options, `Provision ${this.ProvisionNumber ?? ''} cannot be deleted.`, [
|
|
110
|
+
{
|
|
111
|
+
EntityName: 'MJ_BizApps_Contracts: Contract Template Modifications',
|
|
112
|
+
Filter: `ContractTemplateProvisionID = '${this.ID}'`,
|
|
113
|
+
Describe: (n) => `${plural(n, 'contract')} negotiated different wording for this clause, and the negotiated ` +
|
|
114
|
+
`text is only meaningful beside the standard text it replaces.`,
|
|
115
|
+
},
|
|
116
|
+
], (o) => super.Delete(o));
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* Whether a field's value differs from what was loaded.
|
|
120
|
+
*
|
|
121
|
+
* Trimmed string comparison: `ProvisionText` is free text, and MJ round-trips trailing whitespace
|
|
122
|
+
* inconsistently enough that treating `"x "` as an edit to `"x"` would refuse a save nobody meant
|
|
123
|
+
* to make. The trigger uses exact comparison, so the code is very slightly more permissive than the
|
|
124
|
+
* floor — which is the correct direction for the pair: the code never permits something the trigger
|
|
125
|
+
* would reject on a DIFFERENT value, it only declines to complain about a whitespace-only diff that
|
|
126
|
+
* the trigger would also see. If that ever diverges materially the trigger still holds the line.
|
|
127
|
+
*/
|
|
128
|
+
fieldChanged(fieldName) {
|
|
129
|
+
const field = this.GetFieldByName(fieldName);
|
|
130
|
+
if (!field)
|
|
131
|
+
return false;
|
|
132
|
+
const before = field.OldValue;
|
|
133
|
+
const after = field.Value;
|
|
134
|
+
if (before === after)
|
|
135
|
+
return false;
|
|
136
|
+
return String(before ?? '').trim() !== String(after ?? '').trim();
|
|
137
|
+
}
|
|
138
|
+
/**
|
|
139
|
+
* Whether this provision's template is PUBLISHED.
|
|
140
|
+
*
|
|
141
|
+
* ⚠ THIS USED TO ASK "do any contracts reference the template", and that gate was wrong in BOTH
|
|
142
|
+
* directions (V202608200900). Too strict: a provision added after a contract referenced the
|
|
143
|
+
* template was never part of what anyone signed, yet became immediately unremovable — and the live
|
|
144
|
+
* data had referenced templates still being authored, which the rule stranded with no way to
|
|
145
|
+
* finish. Too loose: it said nothing about INSERT, so a clause could be ADDED to a signed version.
|
|
146
|
+
* Publication is now an explicit act and the gate reads it directly.
|
|
147
|
+
*/
|
|
148
|
+
async templateIsPublished() {
|
|
149
|
+
const result = await this.RunViewProviderToUse.RunView({
|
|
150
|
+
EntityName: 'MJ_BizApps_Contracts: Contract Templates',
|
|
151
|
+
ExtraFilter: `ID = '${this.ContractTemplateID}'`,
|
|
152
|
+
Fields: ['Status'],
|
|
153
|
+
ResultType: 'simple',
|
|
154
|
+
}, this.ContextCurrentUser);
|
|
155
|
+
if (!result?.Success) {
|
|
156
|
+
// Must not read as "draft" — that would wave through the very edit this guards.
|
|
157
|
+
throw new Error(`Could not read the publication status of this provision's agreement version: ` +
|
|
158
|
+
`${result?.ErrorMessage ?? 'unknown error'}. The save was not attempted.`);
|
|
159
|
+
}
|
|
160
|
+
return result.Results?.[0]?.Status === TEMPLATE_PUBLISHED;
|
|
161
|
+
}
|
|
162
|
+
};
|
|
163
|
+
ContractTemplateProvisionEntityServer = __decorate([
|
|
164
|
+
RegisterClass(BaseEntity, 'MJ_BizApps_Contracts: Contract Template Provisions')
|
|
165
|
+
], ContractTemplateProvisionEntityServer);
|
|
166
|
+
export { ContractTemplateProvisionEntityServer };
|
|
167
|
+
/** Anti-tree-shake anchor — see the note in index.ts. */
|
|
168
|
+
export function LoadContractTemplateProvisionEntityServer() {
|
|
169
|
+
void ContractTemplateProvisionEntityServer;
|
|
170
|
+
}
|
|
171
|
+
//# sourceMappingURL=ContractTemplateProvisionEntityServer.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ContractTemplateProvisionEntityServer.js","sourceRoot":"","sources":["../src/ContractTemplateProvisionEntityServer.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview The two lookup types' delete guards — and the usability trap they close.
|
|
3
|
+
*
|
|
4
|
+
* The Configuration page tells people to retire a type by setting `Status = 'Inactive'`, and the grid
|
|
5
|
+
* then offers Delete anyway with no explanation of why it fails. So the refusal here does more than
|
|
6
|
+
* name the blocker: it says what to do instead, because the user has already been told the right
|
|
7
|
+
* answer somewhere they are not currently looking.
|
|
8
|
+
*
|
|
9
|
+
* This is the pair R-5 completes. R-5 stops a retired type being newly SELECTED; these stop it being
|
|
10
|
+
* DELETED. Between them, "retire" is a coherent operation: existing records keep working, new ones
|
|
11
|
+
* cannot choose it, and nobody has to delete anything.
|
|
12
|
+
*
|
|
13
|
+
* TWO CLASSES IN ONE FILE, matching `ContractTypeEntity.ts` on the shared side: they are the same rule
|
|
14
|
+
* on the two lookup tables, and splitting them would put four lines in each of two files.
|
|
15
|
+
*
|
|
16
|
+
* @module @mj-biz-apps/contracts-core-entities-server
|
|
17
|
+
*/
|
|
18
|
+
import { type EntityDeleteOptions } from '@memberjunction/core';
|
|
19
|
+
import { mjBizAppsContractsContractTemplateTypeEntity, mjBizAppsContractsContractTypeEntity } from '@mj-biz-apps/contracts-entities';
|
|
20
|
+
export declare class ContractTypeEntityServer extends mjBizAppsContractsContractTypeEntity {
|
|
21
|
+
Delete(options?: EntityDeleteOptions): Promise<boolean>;
|
|
22
|
+
}
|
|
23
|
+
export declare class ContractTemplateTypeEntityServer extends mjBizAppsContractsContractTemplateTypeEntity {
|
|
24
|
+
Delete(options?: EntityDeleteOptions): Promise<boolean>;
|
|
25
|
+
}
|
|
26
|
+
/** Anti-tree-shake anchor — see the note in index.ts. */
|
|
27
|
+
export declare function LoadContractTypeEntityServers(): void;
|
|
28
|
+
//# sourceMappingURL=ContractTypeEntityServers.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ContractTypeEntityServers.d.ts","sourceRoot":"","sources":["../src/ContractTypeEntityServers.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AACH,OAAO,EAAc,KAAK,mBAAmB,EAAE,MAAM,sBAAsB,CAAC;AAE5E,OAAO,EACH,4CAA4C,EAC5C,oCAAoC,EACvC,MAAM,iCAAiC,CAAC;AAMzC,qBACa,wBAAyB,SAAQ,oCAAoC;IACxD,MAAM,CAAC,OAAO,CAAC,EAAE,mBAAmB,GAAG,OAAO,CAAC,OAAO,CAAC;CAehF;AAED,qBACa,gCAAiC,SAAQ,4CAA4C;IACxE,MAAM,CAAC,OAAO,CAAC,EAAE,mBAAmB,GAAG,OAAO,CAAC,OAAO,CAAC;CAehF;AAED,yDAAyD;AACzD,wBAAgB,6BAA6B,IAAI,IAAI,CAGpD"}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
|
|
2
|
+
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
3
|
+
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
4
|
+
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
5
|
+
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
6
|
+
};
|
|
7
|
+
/**
|
|
8
|
+
* @fileoverview The two lookup types' delete guards — and the usability trap they close.
|
|
9
|
+
*
|
|
10
|
+
* The Configuration page tells people to retire a type by setting `Status = 'Inactive'`, and the grid
|
|
11
|
+
* then offers Delete anyway with no explanation of why it fails. So the refusal here does more than
|
|
12
|
+
* name the blocker: it says what to do instead, because the user has already been told the right
|
|
13
|
+
* answer somewhere they are not currently looking.
|
|
14
|
+
*
|
|
15
|
+
* This is the pair R-5 completes. R-5 stops a retired type being newly SELECTED; these stop it being
|
|
16
|
+
* DELETED. Between them, "retire" is a coherent operation: existing records keep working, new ones
|
|
17
|
+
* cannot choose it, and nobody has to delete anything.
|
|
18
|
+
*
|
|
19
|
+
* TWO CLASSES IN ONE FILE, matching `ContractTypeEntity.ts` on the shared side: they are the same rule
|
|
20
|
+
* on the two lookup tables, and splitting them would put four lines in each of two files.
|
|
21
|
+
*
|
|
22
|
+
* @module @mj-biz-apps/contracts-core-entities-server
|
|
23
|
+
*/
|
|
24
|
+
import { BaseEntity } from '@memberjunction/core';
|
|
25
|
+
import { RegisterClass } from '@memberjunction/global';
|
|
26
|
+
import { mjBizAppsContractsContractTemplateTypeEntity, mjBizAppsContractsContractTypeEntity, } from '@mj-biz-apps/contracts-entities';
|
|
27
|
+
import { GuardedDelete, plural } from './delete-guard.js';
|
|
28
|
+
/** How to retire instead of deleting — the same advice in both messages, written once. */
|
|
29
|
+
const RETIRE_INSTEAD = `Retiring is what you want: set Status to Inactive, which stops it being offered for new records and leaves the existing ones working.`;
|
|
30
|
+
let ContractTypeEntityServer = class ContractTypeEntityServer extends mjBizAppsContractsContractTypeEntity {
|
|
31
|
+
async Delete(options) {
|
|
32
|
+
return GuardedDelete(this, options, `Contract type "${this.Name ?? this.ID}" cannot be deleted.`, [
|
|
33
|
+
{
|
|
34
|
+
EntityName: 'MJ_BizApps_Contracts: Contracts',
|
|
35
|
+
Filter: `ContractTypeID = '${this.ID}'`,
|
|
36
|
+
Describe: (n) => `${plural(n, 'contract')} ${n === 1 ? 'is' : 'are'} of this type. ${RETIRE_INSTEAD}`,
|
|
37
|
+
},
|
|
38
|
+
], (o) => super.Delete(o));
|
|
39
|
+
}
|
|
40
|
+
};
|
|
41
|
+
ContractTypeEntityServer = __decorate([
|
|
42
|
+
RegisterClass(BaseEntity, 'MJ_BizApps_Contracts: Contract Types')
|
|
43
|
+
], ContractTypeEntityServer);
|
|
44
|
+
export { ContractTypeEntityServer };
|
|
45
|
+
let ContractTemplateTypeEntityServer = class ContractTemplateTypeEntityServer extends mjBizAppsContractsContractTemplateTypeEntity {
|
|
46
|
+
async Delete(options) {
|
|
47
|
+
return GuardedDelete(this, options, `Template type "${this.Name ?? this.ID}" cannot be deleted.`, [
|
|
48
|
+
{
|
|
49
|
+
EntityName: 'MJ_BizApps_Contracts: Contract Templates',
|
|
50
|
+
Filter: `ContractTemplateTypeID = '${this.ID}'`,
|
|
51
|
+
Describe: (n) => `${plural(n, 'template')} ${n === 1 ? 'is' : 'are'} of this type. ${RETIRE_INSTEAD}`,
|
|
52
|
+
},
|
|
53
|
+
], (o) => super.Delete(o));
|
|
54
|
+
}
|
|
55
|
+
};
|
|
56
|
+
ContractTemplateTypeEntityServer = __decorate([
|
|
57
|
+
RegisterClass(BaseEntity, 'MJ_BizApps_Contracts: Contract Template Types')
|
|
58
|
+
], ContractTemplateTypeEntityServer);
|
|
59
|
+
export { ContractTemplateTypeEntityServer };
|
|
60
|
+
/** Anti-tree-shake anchor — see the note in index.ts. */
|
|
61
|
+
export function LoadContractTypeEntityServers() {
|
|
62
|
+
void ContractTypeEntityServer;
|
|
63
|
+
void ContractTemplateTypeEntityServer;
|
|
64
|
+
}
|
|
65
|
+
//# sourceMappingURL=ContractTypeEntityServers.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ContractTypeEntityServers.js","sourceRoot":"","sources":["../src/ContractTypeEntityServers.ts"],"names":[],"mappings":";;;;;;AAAA;;;;;;;;;;;;;;;;GAgBG;AACH,OAAO,EAAE,UAAU,EAA4B,MAAM,sBAAsB,CAAC;AAC5E,OAAO,EAAE,aAAa,EAAE,MAAM,wBAAwB,CAAC;AACvD,OAAO,EACH,4CAA4C,EAC5C,oCAAoC,GACvC,MAAM,iCAAiC,CAAC;AACzC,OAAO,EAAE,aAAa,EAAE,MAAM,EAAE,MAAM,mBAAmB,CAAC;AAE1D,0FAA0F;AAC1F,MAAM,cAAc,GAAG,uIAAuI,CAAC;AAGxJ,IAAM,wBAAwB,GAA9B,MAAM,wBAAyB,SAAQ,oCAAoC;IAC9D,KAAK,CAAC,MAAM,CAAC,OAA6B;QACtD,OAAO,aAAa,CAChB,IAAI,EACJ,OAAO,EACP,kBAAkB,IAAI,CAAC,IAAI,IAAI,IAAI,CAAC,EAAE,sBAAsB,EAC5D;YACI;gBACI,UAAU,EAAE,iCAAiC;gBAC7C,MAAM,EAAE,qBAAqB,IAAI,CAAC,EAAE,GAAG;gBACvC,QAAQ,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC,GAAG,MAAM,CAAC,CAAC,EAAE,UAAU,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,KAAK,kBAAkB,cAAc,EAAE;aACxG;SACJ,EACD,CAAC,CAAC,EAAE,EAAE,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC,CAAC,CACzB,CAAC;IACN,CAAC;CACJ,CAAA;AAhBY,wBAAwB;IADpC,aAAa,CAAC,UAAU,EAAE,sCAAsC,CAAC;GACrD,wBAAwB,CAgBpC;;AAGM,IAAM,gCAAgC,GAAtC,MAAM,gCAAiC,SAAQ,4CAA4C;IAC9E,KAAK,CAAC,MAAM,CAAC,OAA6B;QACtD,OAAO,aAAa,CAChB,IAAI,EACJ,OAAO,EACP,kBAAkB,IAAI,CAAC,IAAI,IAAI,IAAI,CAAC,EAAE,sBAAsB,EAC5D;YACI;gBACI,UAAU,EAAE,0CAA0C;gBACtD,MAAM,EAAE,6BAA6B,IAAI,CAAC,EAAE,GAAG;gBAC/C,QAAQ,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC,GAAG,MAAM,CAAC,CAAC,EAAE,UAAU,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,KAAK,kBAAkB,cAAc,EAAE;aACxG;SACJ,EACD,CAAC,CAAC,EAAE,EAAE,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC,CAAC,CACzB,CAAC;IACN,CAAC;CACJ,CAAA;AAhBY,gCAAgC;IAD5C,aAAa,CAAC,UAAU,EAAE,+CAA+C,CAAC;GAC9D,gCAAgC,CAgB5C;;AAED,yDAAyD;AACzD,MAAM,UAAU,6BAA6B;IACzC,KAAK,wBAAwB,CAAC;IAC9B,KAAK,gCAAgC,CAAC;AAC1C,CAAC"}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview `Contracts.Supersede` — set (or clear) which contract an agreement replaces.
|
|
3
|
+
*
|
|
4
|
+
* WHY A REMOTABLE OPERATION AND NOT ENTITY CALLS FROM THE PANEL. The write targets a DIFFERENT record
|
|
5
|
+
* from the one on screen: `SupersededByContractID` lives on the PREDECESSOR, so the successor's form
|
|
6
|
+
* has to reach over and write another contract. Doing that from the browser meant the panel loading
|
|
7
|
+
* and saving foreign entities itself — server work done in the client. Ruled by Marcelo (2026-08-21):
|
|
8
|
+
* a cross-record write with rules is exactly what a remote operation is for.
|
|
9
|
+
*
|
|
10
|
+
* It also makes the feature immune to MJ#4002, where the browser resolves the CodeGen-generated entity
|
|
11
|
+
* class instead of the app's subclass, so `ContractEntity.Supersede()` is simply absent client-side.
|
|
12
|
+
* Here the work runs on the server, where the server subclass resolves correctly and every rule in
|
|
13
|
+
* `ContractEntityServer.ValidateAsync()` — same-level, self-reference, lineage cycles — actually fires.
|
|
14
|
+
*
|
|
15
|
+
* REPLACE, NOT ADD. The schema deliberately allows MANY predecessors to name one successor
|
|
16
|
+
* (consolidation), so setting a new predecessor without releasing the old one leaves the agreement
|
|
17
|
+
* quietly superseding BOTH. The UI is a single-select, so one predecessor is the intent: release the
|
|
18
|
+
* others, then link. Passing `PredecessorID: null` releases everything and links nothing, which is
|
|
19
|
+
* what "unlink" is.
|
|
20
|
+
*
|
|
21
|
+
* NO transaction wrapper, deliberately. Each contract is saved through its own entity so every rule
|
|
22
|
+
* runs, and the outcome is reported per-record. A half-applied change is visible in the returned list
|
|
23
|
+
* rather than hidden behind a rollback that also discards the reason it failed.
|
|
24
|
+
*
|
|
25
|
+
* @module @mj-biz-apps/contracts-core-entities-server
|
|
26
|
+
*/
|
|
27
|
+
import { BaseRemotableOperation, type IMetadataProvider, type RemoteOpServerContext, type UserInfo } from '@memberjunction/core';
|
|
28
|
+
export interface SupersedeInput {
|
|
29
|
+
/** The agreement doing the superseding — the record the user is looking at. */
|
|
30
|
+
SuccessorID: string;
|
|
31
|
+
/** The agreement being replaced. `null` releases every predecessor and links none (unlink). */
|
|
32
|
+
PredecessorID: string | null;
|
|
33
|
+
}
|
|
34
|
+
export interface SupersedeOutput {
|
|
35
|
+
/** What the successor supersedes AFTER the operation — the panel renders this, so it can never be stale. */
|
|
36
|
+
Supersedes: Array<{
|
|
37
|
+
ID: string;
|
|
38
|
+
ContractNumber: string;
|
|
39
|
+
}>;
|
|
40
|
+
/** Contract numbers released by this call. */
|
|
41
|
+
Released: string[];
|
|
42
|
+
/** Set when the link was refused; carries the entity's own message, not a generic failure. */
|
|
43
|
+
Refused?: string;
|
|
44
|
+
}
|
|
45
|
+
export declare class SupersedeOperation extends BaseRemotableOperation<SupersedeInput, SupersedeOutput> {
|
|
46
|
+
readonly OperationKey = "Contracts.Supersede";
|
|
47
|
+
protected InternalExecute(input: SupersedeInput, provider: IMetadataProvider, user: UserInfo, _context: RemoteOpServerContext): Promise<SupersedeOutput>;
|
|
48
|
+
/** Live read of what a contract supersedes — returned to the caller so its list is never cached. */
|
|
49
|
+
private readSupersedes;
|
|
50
|
+
}
|
|
51
|
+
/** Anti-tree-shake anchor — without a live reference the @RegisterClass side effect can be dropped. */
|
|
52
|
+
export declare function LoadSupersedeOperation(): void;
|
|
53
|
+
//# sourceMappingURL=SupersedeOperation.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"SupersedeOperation.d.ts","sourceRoot":"","sources":["../src/SupersedeOperation.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AACH,OAAO,EACH,sBAAsB,EACtB,KAAK,iBAAiB,EACtB,KAAK,qBAAqB,EAE1B,KAAK,QAAQ,EAChB,MAAM,sBAAsB,CAAC;AAM9B,MAAM,WAAW,cAAc;IAC3B,+EAA+E;IAC/E,WAAW,EAAE,MAAM,CAAC;IACpB,+FAA+F;IAC/F,aAAa,EAAE,MAAM,GAAG,IAAI,CAAC;CAChC;AAED,MAAM,WAAW,eAAe;IAC5B,4GAA4G;IAC5G,UAAU,EAAE,KAAK,CAAC;QAAE,EAAE,EAAE,MAAM,CAAC;QAAC,cAAc,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IAC1D,8CAA8C;IAC9C,QAAQ,EAAE,MAAM,EAAE,CAAC;IACnB,8FAA8F;IAC9F,OAAO,CAAC,EAAE,MAAM,CAAC;CACpB;AAED,qBACa,kBAAmB,SAAQ,sBAAsB,CAAC,cAAc,EAAE,eAAe,CAAC;IAC3F,SAAgB,YAAY,yBAAyB;cAE5B,eAAe,CACpC,KAAK,EAAE,cAAc,EACrB,QAAQ,EAAE,iBAAiB,EAC3B,IAAI,EAAE,QAAQ,EACd,QAAQ,EAAE,qBAAqB,GAChC,OAAO,CAAC,eAAe,CAAC;IAsD3B,oGAAoG;YACtF,cAAc;CAkB/B;AAED,uGAAuG;AACvG,wBAAgB,sBAAsB,IAAI,IAAI,CAE7C"}
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
|
|
2
|
+
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
3
|
+
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
4
|
+
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
5
|
+
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
6
|
+
};
|
|
7
|
+
/**
|
|
8
|
+
* @fileoverview `Contracts.Supersede` — set (or clear) which contract an agreement replaces.
|
|
9
|
+
*
|
|
10
|
+
* WHY A REMOTABLE OPERATION AND NOT ENTITY CALLS FROM THE PANEL. The write targets a DIFFERENT record
|
|
11
|
+
* from the one on screen: `SupersededByContractID` lives on the PREDECESSOR, so the successor's form
|
|
12
|
+
* has to reach over and write another contract. Doing that from the browser meant the panel loading
|
|
13
|
+
* and saving foreign entities itself — server work done in the client. Ruled by Marcelo (2026-08-21):
|
|
14
|
+
* a cross-record write with rules is exactly what a remote operation is for.
|
|
15
|
+
*
|
|
16
|
+
* It also makes the feature immune to MJ#4002, where the browser resolves the CodeGen-generated entity
|
|
17
|
+
* class instead of the app's subclass, so `ContractEntity.Supersede()` is simply absent client-side.
|
|
18
|
+
* Here the work runs on the server, where the server subclass resolves correctly and every rule in
|
|
19
|
+
* `ContractEntityServer.ValidateAsync()` — same-level, self-reference, lineage cycles — actually fires.
|
|
20
|
+
*
|
|
21
|
+
* REPLACE, NOT ADD. The schema deliberately allows MANY predecessors to name one successor
|
|
22
|
+
* (consolidation), so setting a new predecessor without releasing the old one leaves the agreement
|
|
23
|
+
* quietly superseding BOTH. The UI is a single-select, so one predecessor is the intent: release the
|
|
24
|
+
* others, then link. Passing `PredecessorID: null` releases everything and links nothing, which is
|
|
25
|
+
* what "unlink" is.
|
|
26
|
+
*
|
|
27
|
+
* NO transaction wrapper, deliberately. Each contract is saved through its own entity so every rule
|
|
28
|
+
* runs, and the outcome is reported per-record. A half-applied change is visible in the returned list
|
|
29
|
+
* rather than hidden behind a rollback that also discards the reason it failed.
|
|
30
|
+
*
|
|
31
|
+
* @module @mj-biz-apps/contracts-core-entities-server
|
|
32
|
+
*/
|
|
33
|
+
import { BaseRemotableOperation, RunView, } from '@memberjunction/core';
|
|
34
|
+
import { RegisterClass } from '@memberjunction/global';
|
|
35
|
+
const E_CONTRACT = 'MJ_BizApps_Contracts: Contracts';
|
|
36
|
+
let SupersedeOperation = class SupersedeOperation extends BaseRemotableOperation {
|
|
37
|
+
constructor() {
|
|
38
|
+
super(...arguments);
|
|
39
|
+
this.OperationKey = 'Contracts.Supersede';
|
|
40
|
+
}
|
|
41
|
+
async InternalExecute(input, provider, user, _context) {
|
|
42
|
+
if (!input?.SuccessorID)
|
|
43
|
+
throw new Error('SuccessorID is required.');
|
|
44
|
+
const successor = await provider.GetEntityObject(E_CONTRACT, user);
|
|
45
|
+
if (!(await successor.Load(input.SuccessorID))) {
|
|
46
|
+
throw new Error('The contract doing the superseding could not be loaded.');
|
|
47
|
+
}
|
|
48
|
+
const released = [];
|
|
49
|
+
const same = (a, b) => !!a && !!b && a.toLowerCase() === b.toLowerCase();
|
|
50
|
+
// Release every predecessor except the one being (re-)linked, so re-picking the same contract
|
|
51
|
+
// is a no-op rather than a clear-and-reset.
|
|
52
|
+
for (const current of await this.readSupersedes(provider, user, input.SuccessorID)) {
|
|
53
|
+
if (same(current.ID, input.PredecessorID))
|
|
54
|
+
continue;
|
|
55
|
+
const previous = await provider.GetEntityObject(E_CONTRACT, user);
|
|
56
|
+
if (!(await previous.Load(current.ID)))
|
|
57
|
+
continue;
|
|
58
|
+
previous.SupersededByContractID = null;
|
|
59
|
+
if (!(await previous.Save())) {
|
|
60
|
+
return {
|
|
61
|
+
Supersedes: await this.readSupersedes(provider, user, input.SuccessorID),
|
|
62
|
+
Released: released,
|
|
63
|
+
Refused: previous.LatestResult?.Message ??
|
|
64
|
+
`Could not release ${current.ContractNumber}; nothing else was changed.`,
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
released.push(current.ContractNumber);
|
|
68
|
+
}
|
|
69
|
+
if (input.PredecessorID && !same(input.PredecessorID, input.SuccessorID)) {
|
|
70
|
+
const predecessor = await provider.GetEntityObject(E_CONTRACT, user);
|
|
71
|
+
if (!(await predecessor.Load(input.PredecessorID))) {
|
|
72
|
+
throw new Error('The contract to be superseded could not be loaded.');
|
|
73
|
+
}
|
|
74
|
+
// Through Supersede(), not a bare field write, so the entity keeps owning the rule.
|
|
75
|
+
predecessor.Supersede(successor);
|
|
76
|
+
if (!(await predecessor.Save())) {
|
|
77
|
+
return {
|
|
78
|
+
Supersedes: await this.readSupersedes(provider, user, input.SuccessorID),
|
|
79
|
+
Released: released,
|
|
80
|
+
// The server guards produce field-named prose (same-level, self-reference, cycles);
|
|
81
|
+
// that message is far more use than "the save failed".
|
|
82
|
+
Refused: predecessor.LatestResult?.Message ?? 'That contract could not be marked superseded.',
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
return {
|
|
87
|
+
Supersedes: await this.readSupersedes(provider, user, input.SuccessorID),
|
|
88
|
+
Released: released,
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
/** Live read of what a contract supersedes — returned to the caller so its list is never cached. */
|
|
92
|
+
async readSupersedes(provider, user, successorID) {
|
|
93
|
+
const rv = RunView.FromMetadataProvider(provider);
|
|
94
|
+
const r = await rv.RunView({
|
|
95
|
+
EntityName: E_CONTRACT,
|
|
96
|
+
Fields: ['ID', 'ContractNumber'],
|
|
97
|
+
ExtraFilter: `SupersededByContractID = '${successorID.replace(/'/g, "''")}'`,
|
|
98
|
+
OrderBy: 'ContractNumber ASC',
|
|
99
|
+
ResultType: 'simple',
|
|
100
|
+
}, user);
|
|
101
|
+
return r?.Success ? r.Results : [];
|
|
102
|
+
}
|
|
103
|
+
};
|
|
104
|
+
SupersedeOperation = __decorate([
|
|
105
|
+
RegisterClass(BaseRemotableOperation, 'Contracts.Supersede')
|
|
106
|
+
], SupersedeOperation);
|
|
107
|
+
export { SupersedeOperation };
|
|
108
|
+
/** Anti-tree-shake anchor — without a live reference the @RegisterClass side effect can be dropped. */
|
|
109
|
+
export function LoadSupersedeOperation() {
|
|
110
|
+
// no-op
|
|
111
|
+
}
|
|
112
|
+
//# sourceMappingURL=SupersedeOperation.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"SupersedeOperation.js","sourceRoot":"","sources":["../src/SupersedeOperation.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview R-8 — turning a foreign-key violation into a sentence.
|
|
3
|
+
*
|
|
4
|
+
* Every FK in this schema is `NO_ACTION`, which is the correct behaviour: a delete that would orphan
|
|
5
|
+
* data is refused. The problem is only that it reaches the user as a constraint name. Four entities
|
|
6
|
+
* need the same treatment and differ ONLY in which rows to count, so this is one guard parameterised
|
|
7
|
+
* by the dependencies rather than four copies of the same shape drifting apart — the review test from
|
|
8
|
+
* ORCHESTRATION.md ("if I had to change this logic, how many places would I have to find?").
|
|
9
|
+
*
|
|
10
|
+
* ## Why `Delete()` and not a validation hook
|
|
11
|
+
*
|
|
12
|
+
* `BaseEntity` has no delete-side counterpart to `Validate()` — no `ValidateDelete`, no `BeforeDelete`,
|
|
13
|
+
* no vetoable event. Verified by reading `_InnerDelete`, which runs no validation at all where
|
|
14
|
+
* `_InnerSave` runs `Validate()`/`ValidateAsync()` first. So the only seam is overriding `Delete()`.
|
|
15
|
+
* Filed upstream as [MJ#3971](https://github.com/MemberJunction/MJ/issues/3971), and a fix is already
|
|
16
|
+
* authored there; when it lands, these overrides become `ValidateDeleteAsync()` bodies and this file
|
|
17
|
+
* shrinks rather than disappears.
|
|
18
|
+
*
|
|
19
|
+
* ## Why it REGISTERS A RESULT and returns false, instead of throwing
|
|
20
|
+
*
|
|
21
|
+
* `bizapps-accounting`'s two precedents (`JournalEntryTypeEntityServer`, `JournalEntryLineEntityServer`)
|
|
22
|
+
* **throw**. That works, and it is not what this does, deliberately:
|
|
23
|
+
*
|
|
24
|
+
* · `Delete()` is declared `Promise<boolean>`. A caller written against that signature does
|
|
25
|
+
* `if (!await entity.Delete())` and gets an exception instead of `false`.
|
|
26
|
+
* · `LatestResult` is how MJ carries the reason for a refused write — it is what `ResolverBase`
|
|
27
|
+
* reads to build the GraphQL error, and what a refused `Save()` already populates. Throwing routes
|
|
28
|
+
* around the one channel built for this.
|
|
29
|
+
* · `RegisterResultHistoryEntry` is public precisely so a subclass can do this.
|
|
30
|
+
*
|
|
31
|
+
* ⚠ **This depends on an MJ fix to be VISIBLE.** `ResolverBase.DeleteRecord` read
|
|
32
|
+
* `LatestResult?.Message` rather than `CompleteMessage`, so a registered refusal reached the API as
|
|
33
|
+
* the literal string "Unknown error". Measured on this instance; the one-word fix is applied locally
|
|
34
|
+
* in `instances/contracts-mj6/mj` and was commented onto MJ#3973. **On an unpatched MJ these messages
|
|
35
|
+
* are correct and invisible** — the delete is still refused, so this is a message-quality dependency,
|
|
36
|
+
* not a correctness one.
|
|
37
|
+
*
|
|
38
|
+
* @module @mj-biz-apps/contracts-core-entities-server
|
|
39
|
+
*/
|
|
40
|
+
import { BaseEntity, type EntityDeleteOptions } from '@memberjunction/core';
|
|
41
|
+
/**
|
|
42
|
+
* One thing that can block a delete: the rows to look for, and how to describe them once counted.
|
|
43
|
+
*
|
|
44
|
+
* `describe` takes the count because the useful sentence names it — "3 contracts negotiated this
|
|
45
|
+
* clause" tells someone how much work the cleanup is, where "this clause is in use" does not.
|
|
46
|
+
*/
|
|
47
|
+
export type DeleteDependency = {
|
|
48
|
+
/** The MJ entity name holding the referencing rows. */
|
|
49
|
+
EntityName: string;
|
|
50
|
+
/** `ExtraFilter` selecting the rows that reference the record being deleted. */
|
|
51
|
+
Filter: string;
|
|
52
|
+
/** The clause describing what was found, given how many. */
|
|
53
|
+
Describe: (count: number) => string;
|
|
54
|
+
};
|
|
55
|
+
/**
|
|
56
|
+
* Count each dependency and return the refusal text, or null when nothing blocks the delete.
|
|
57
|
+
*
|
|
58
|
+
* COUNTS, NOT EXISTENCE. `count_only` costs the same as `TOP 1` on an indexed FK (MJ auto-indexes
|
|
59
|
+
* every FK), and the number is most of the value of the message.
|
|
60
|
+
*
|
|
61
|
+
* ALL dependencies are checked, not just the first. Deleting a template usually trips both its
|
|
62
|
+
* provisions and its contracts, and being told about one, fixing it, and then being refused again for
|
|
63
|
+
* the other is the worst version of this interaction.
|
|
64
|
+
*
|
|
65
|
+
* A FAILED COUNT ABORTS rather than defaulting to zero. Reading "could not verify" as "nothing
|
|
66
|
+
* references it" would let this guard wave through exactly the delete it exists to stop; the FK is
|
|
67
|
+
* still the floor, but a guard that fails open is worse than no guard because it looks like one.
|
|
68
|
+
*/
|
|
69
|
+
export declare function DescribeDeleteBlockers(entity: BaseEntity, dependencies: readonly DeleteDependency[]): Promise<string | null>;
|
|
70
|
+
/**
|
|
71
|
+
* Record a delete refusal on the entity so `LatestResult` carries the reason, and return false.
|
|
72
|
+
*
|
|
73
|
+
* Registered BEFORE `super.Delete()` is ever called, so nothing else has written a result — `Message`
|
|
74
|
+
* is what `CompleteMessage` reads first, so the sentence survives without needing an `Errors` entry.
|
|
75
|
+
* There is no field to name: a delete refusal is about the record, not about one of its columns, which
|
|
76
|
+
* is part of why `ValidationErrorInfo` is the wrong vehicle here and MJ#3971 asks for a delete-shaped
|
|
77
|
+
* one.
|
|
78
|
+
*/
|
|
79
|
+
export declare function RefuseDelete(entity: BaseEntity, message: string): false;
|
|
80
|
+
/**
|
|
81
|
+
* The whole guard: count the blockers, refuse with an explanation if there are any, otherwise delete.
|
|
82
|
+
*
|
|
83
|
+
* Every subclass override is one call to this, which is the point — the only thing that varies between
|
|
84
|
+
* the four is the dependency list and the leading sentence.
|
|
85
|
+
*/
|
|
86
|
+
export declare function GuardedDelete(entity: BaseEntity, options: EntityDeleteOptions | undefined, lead: string, dependencies: readonly DeleteDependency[], superDelete: (options?: EntityDeleteOptions) => Promise<boolean>): Promise<boolean>;
|
|
87
|
+
/** `'1 contract'` / `'3 contracts'` — so a message never reads "1 contracts". */
|
|
88
|
+
export declare function plural(count: number, singular: string, pluralForm?: string): string;
|
|
89
|
+
//# sourceMappingURL=delete-guard.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"delete-guard.d.ts","sourceRoot":"","sources":["../src/delete-guard.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAsCG;AACH,OAAO,EAAE,UAAU,EAAoB,KAAK,mBAAmB,EAAE,MAAM,sBAAsB,CAAC;AAE9F;;;;;GAKG;AACH,MAAM,MAAM,gBAAgB,GAAG;IAC3B,uDAAuD;IACvD,UAAU,EAAE,MAAM,CAAC;IACnB,gFAAgF;IAChF,MAAM,EAAE,MAAM,CAAC;IACf,4DAA4D;IAC5D,QAAQ,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,MAAM,CAAC;CACvC,CAAC;AAEF;;;;;;;;;;;;;GAaG;AACH,wBAAsB,sBAAsB,CAAC,MAAM,EAAE,UAAU,EAAE,YAAY,EAAE,SAAS,gBAAgB,EAAE,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAuBlI;AAED;;;;;;;;GAQG;AACH,wBAAgB,YAAY,CAAC,MAAM,EAAE,UAAU,EAAE,OAAO,EAAE,MAAM,GAAG,KAAK,CAQvE;AAED;;;;;GAKG;AACH,wBAAsB,aAAa,CAC/B,MAAM,EAAE,UAAU,EAClB,OAAO,EAAE,mBAAmB,GAAG,SAAS,EACxC,IAAI,EAAE,MAAM,EACZ,YAAY,EAAE,SAAS,gBAAgB,EAAE,EACzC,WAAW,EAAE,CAAC,OAAO,CAAC,EAAE,mBAAmB,KAAK,OAAO,CAAC,OAAO,CAAC,GACjE,OAAO,CAAC,OAAO,CAAC,CAIlB;AAED,iFAAiF;AACjF,wBAAgB,MAAM,CAAC,KAAK,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,EAAE,UAAU,CAAC,EAAE,MAAM,GAAG,MAAM,CAEnF"}
|