@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.
Files changed (34) hide show
  1. package/LICENSE +183 -0
  2. package/dist/ContractEntityServer.d.ts +352 -0
  3. package/dist/ContractEntityServer.d.ts.map +1 -0
  4. package/dist/ContractEntityServer.js +729 -0
  5. package/dist/ContractEntityServer.js.map +1 -0
  6. package/dist/ContractTemplateEntityServer.d.ts +39 -0
  7. package/dist/ContractTemplateEntityServer.d.ts.map +1 -0
  8. package/dist/ContractTemplateEntityServer.js +99 -0
  9. package/dist/ContractTemplateEntityServer.js.map +1 -0
  10. package/dist/ContractTemplateModificationEntityServer.d.ts +119 -0
  11. package/dist/ContractTemplateModificationEntityServer.d.ts.map +1 -0
  12. package/dist/ContractTemplateModificationEntityServer.js +242 -0
  13. package/dist/ContractTemplateModificationEntityServer.js.map +1 -0
  14. package/dist/ContractTemplateProvisionEntityServer.d.ts +74 -0
  15. package/dist/ContractTemplateProvisionEntityServer.d.ts.map +1 -0
  16. package/dist/ContractTemplateProvisionEntityServer.js +171 -0
  17. package/dist/ContractTemplateProvisionEntityServer.js.map +1 -0
  18. package/dist/ContractTypeEntityServers.d.ts +28 -0
  19. package/dist/ContractTypeEntityServers.d.ts.map +1 -0
  20. package/dist/ContractTypeEntityServers.js +65 -0
  21. package/dist/ContractTypeEntityServers.js.map +1 -0
  22. package/dist/SupersedeOperation.d.ts +53 -0
  23. package/dist/SupersedeOperation.d.ts.map +1 -0
  24. package/dist/SupersedeOperation.js +112 -0
  25. package/dist/SupersedeOperation.js.map +1 -0
  26. package/dist/delete-guard.d.ts +89 -0
  27. package/dist/delete-guard.d.ts.map +1 -0
  28. package/dist/delete-guard.js +107 -0
  29. package/dist/delete-guard.js.map +1 -0
  30. package/dist/index.d.ts +15 -0
  31. package/dist/index.d.ts.map +1 -0
  32. package/dist/index.js +49 -0
  33. package/dist/index.js.map +1 -0
  34. 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":";;;;;;AAAA;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AACH,OAAO,EACH,UAAU,EAEV,mBAAmB,EACnB,mBAAmB,GAEtB,MAAM,sBAAsB,CAAC;AAC9B,OAAO,EAAE,aAAa,EAAE,MAAM,wBAAwB,CAAC;AACvD,OAAO,EAAE,+BAA+B,EAAE,kBAAkB,EAAE,MAAM,iCAAiC,CAAC;AACtG,OAAO,EAAE,aAAa,EAAE,YAAY,EAAE,MAAM,EAAE,MAAM,mBAAmB,CAAC;AAExE;;;;;GAKG;AACH,MAAM,kBAAkB,GAAmD;IACvE,EAAE,IAAI,EAAE,iBAAiB,EAAE,KAAK,EAAE,mBAAmB,EAAE;IACvD,EAAE,IAAI,EAAE,OAAO,EAAE,KAAK,EAAE,kBAAkB,EAAE;IAC5C,EAAE,IAAI,EAAE,eAAe,EAAE,KAAK,EAAE,sBAAsB,EAAE;CAC3D,CAAC;AAEF;;;GAGG;AAEI,IAAM,qCAAqC,GAA3C,MAAM,qCAAsC,SAAQ,+BAA+B;IACtF;;;;;;;;;;;OAWG;IACa,KAAK,CAAC,aAAa;QAC/B,MAAM,MAAM,GAAG,MAAM,KAAK,CAAC,aAAa,EAAE,CAAC;QAE3C,0FAA0F;QAC1F,8FAA8F;QAC9F,gFAAgF;QAChF,4FAA4F;QAC5F,4FAA4F;QAC5F,IAAI,CAAC,IAAI,CAAC,OAAO,EAAE,CAAC;YAChB,IAAI,IAAI,CAAC,kBAAkB,IAAI,CAAC,MAAM,IAAI,CAAC,mBAAmB,EAAE,CAAC,EAAE,CAAC;gBAChE,MAAM,CAAC,OAAO,GAAG,KAAK,CAAC;gBACvB,MAAM,CAAC,MAAM,CAAC,IAAI,CACd,IAAI,mBAAmB,CACnB,oBAAoB,EACpB,sFAAsF;oBAClF,kFAAkF;oBAClF,kBAAkB,EACtB,IAAI,CAAC,kBAAkB,EACvB,mBAAmB,CAAC,OAAO,CAC9B,CACJ,CAAC;YACN,CAAC;YACD,OAAO,MAAM,CAAC;QAClB,CAAC;QAED,MAAM,OAAO,GAAG,kBAAkB,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,IAAI,CAAC,YAAY,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC;QAC5E,IAAI,OAAO,CAAC,MAAM,KAAK,CAAC;YAAE,OAAO,MAAM,CAAC;QAExC,kCAAkC;QAClC,IAAI,CAAC,CAAC,MAAM,IAAI,CAAC,mBAAmB,EAAE,CAAC;YAAE,OAAO,MAAM,CAAC;QAEvD,KAAK,MAAM,KAAK,IAAI,OAAO,EAAE,CAAC;YAC1B,MAAM,CAAC,OAAO,GAAG,KAAK,CAAC;YACvB,MAAM,CAAC,MAAM,CAAC,IAAI,CACd,IAAI,mBAAmB,CACnB,KAAK,CAAC,IAAI,EACV,2CAA2C,KAAK,CAAC,KAAK,oCAAoC;gBACtF,4FAA4F;gBAC5F,kFAAkF,EACtF,IAAI,CAAC,GAAG,CAAC,KAAK,CAAC,IAAI,CAAC,EACpB,mBAAmB,CAAC,OAAO,CAC9B,CACJ,CAAC;QACN,CAAC;QAED,OAAO,MAAM,CAAC;IAClB,CAAC;IAED;;;;OAIG;IACa,KAAK,CAAC,MAAM,CAAC,OAA6B;QACtD,0FAA0F;QAC1F,yFAAyF;QACzF,yFAAyF;QACzF,IAAI,MAAM,IAAI,CAAC,mBAAmB,EAAE,EAAE,CAAC;YACnC,OAAO,YAAY,CACf,IAAI,EACJ,aAAa,IAAI,CAAC,eAAe,IAAI,EAAE,2DAA2D;gBAC9F,0FAA0F;gBAC1F,gCAAgC,CACvC,CAAC;QACN,CAAC;QACD,OAAO,aAAa,CAChB,IAAI,EACJ,OAAO,EACP,aAAa,IAAI,CAAC,eAAe,IAAI,EAAE,qBAAqB,EAC5D;YACI;gBACI,UAAU,EAAE,uDAAuD;gBACnE,MAAM,EAAE,kCAAkC,IAAI,CAAC,EAAE,GAAG;gBACpD,QAAQ,EAAE,CAAC,CAAC,EAAE,EAAE,CACZ,GAAG,MAAM,CAAC,CAAC,EAAE,UAAU,CAAC,oEAAoE;oBAC5F,+DAA+D;aACtE;SACJ,EACD,CAAC,CAAC,EAAE,EAAE,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC,CAAC,CACzB,CAAC;IACN,CAAC;IAED;;;;;;;;;OASG;IACK,YAAY,CAAC,SAAiB;QAClC,MAAM,KAAK,GAAG,IAAI,CAAC,cAAc,CAAC,SAAS,CAAC,CAAC;QAC7C,IAAI,CAAC,KAAK;YAAE,OAAO,KAAK,CAAC;QACzB,MAAM,MAAM,GAAG,KAAK,CAAC,QAAQ,CAAC;QAC9B,MAAM,KAAK,GAAG,KAAK,CAAC,KAAK,CAAC;QAC1B,IAAI,MAAM,KAAK,KAAK;YAAE,OAAO,KAAK,CAAC;QACnC,OAAO,MAAM,CAAC,MAAM,IAAI,EAAE,CAAC,CAAC,IAAI,EAAE,KAAK,MAAM,CAAC,KAAK,IAAI,EAAE,CAAC,CAAC,IAAI,EAAE,CAAC;IACtE,CAAC;IAED;;;;;;;;;OASG;IACK,KAAK,CAAC,mBAAmB;QAC7B,MAAM,MAAM,GAAG,MAAM,IAAI,CAAC,oBAAoB,CAAC,OAAO,CAClD;YACI,UAAU,EAAE,0CAA0C;YACtD,WAAW,EAAE,SAAS,IAAI,CAAC,kBAAkB,GAAG;YAChD,MAAM,EAAE,CAAC,QAAQ,CAAC;YAClB,UAAU,EAAE,QAAQ;SACvB,EACD,IAAI,CAAC,kBAAkB,CAC1B,CAAC;QACF,IAAI,CAAC,MAAM,EAAE,OAAO,EAAE,CAAC;YACnB,gFAAgF;YAChF,MAAM,IAAI,KAAK,CACX,+EAA+E;gBAC3E,GAAG,MAAM,EAAE,YAAY,IAAI,eAAe,+BAA+B,CAChF,CAAC;QACN,CAAC;QACD,OAAO,MAAM,CAAC,OAAO,EAAE,CAAC,CAAC,CAAC,EAAE,MAAM,KAAK,kBAAkB,CAAC;IAC9D,CAAC;CACJ,CAAA;AA/IY,qCAAqC;IADjD,aAAa,CAAC,UAAU,EAAE,oDAAoD,CAAC;GACnE,qCAAqC,CA+IjD;;AAED,yDAAyD;AACzD,MAAM,UAAU,yCAAyC;IACrD,KAAK,qCAAqC,CAAC;AAC/C,CAAC"}
@@ -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"}