@serve.zone/interfaces 32.70.0 → 32.72.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/changelog.md +12 -0
- package/dist_ts/00_commitinfo_data.js +1 -1
- package/dist_ts/data/backup.d.ts +12 -1
- package/dist_ts/data/backup.js +1 -1
- package/dist_ts/data/backuprestorerefusal.d.ts +100 -0
- package/dist_ts/data/backuprestorerefusal.js +247 -0
- package/dist_ts/data/index.d.ts +1 -0
- package/dist_ts/data/index.js +2 -1
- package/package.json +1 -1
- package/ts/00_commitinfo_data.ts +1 -1
- package/ts/data/backup.ts +12 -1
- package/ts/data/backuprestorerefusal.ts +354 -0
- package/ts/data/index.ts +1 -0
package/changelog.md
CHANGED
|
@@ -1,5 +1,17 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 2026-11-05 - 32.72.0
|
|
4
|
+
|
|
5
|
+
### Features
|
|
6
|
+
|
|
7
|
+
- Restore operation refusal (Linus 2026-11-05 ruling): `data.backupRestoreRefusalNames` adds `restore-operation-unavailable` (`data.IBackupRestoreOperationUnavailableRefusal`, exactly `{ name }`, built by `data.buildBackupRestoreOperationUnavailableRefusal`). Cloudly answers it for every in-place restore while a restore is not yet an operation the node owns (a restore relayed as one request is cut at the transport timeout); it names the missing capability, not a version. `data.validateBackupRestoreRefusal`, `data.isBackupRestoreRefusal`, `data.readBackupRestoreRefusal` and `data.validateBackupRestoreEvent` accept it as the refusal of a `refused` restore event, and it stays in the contract because stored restore events carry it. A reader built before this version refuses it as an unnamed refusal; Cloudly, its only producer, reads its own restore history, so readers of restore events upgrade before Cloudly and Cloudly is not rolled back below a release that stored it. Spec: `test.backuprestorerefusal`.
|
|
8
|
+
|
|
9
|
+
## 2026-11-05 - 32.71.0
|
|
10
|
+
|
|
11
|
+
### Features
|
|
12
|
+
|
|
13
|
+
- All-or-nothing backup restore refusals (AGL-1501, Linus 2026-11-05 ruling): `data.backupRestoreRefusalNames` freezes the three reasons an in-place restore is refused before it writes anything: `restore-snapshot-unavailable` (`missing`, a non-empty list of `data.IBackupRestoreMissingSnapshot`: `snapshotId`, `capability` (`data.TBackupResourceType`) and an optional `resourceName`), `restore-preflight-unsupported` (the node's Corestore offers no restore check) and `restore-relay-contract-behind` (`relayInterfacesVersion`, a release version or `null`, below `minimumInterfacesVersion`). `data.TBackupRestoreRefusal` is discriminated by `name`; `data.validateBackupRestoreRefusal`, `data.isBackupRestoreRefusal` and `data.readBackupRestoreRefusal` judge untrusted `errorData` against each exact schema, and `data.buildBackupRestoreSnapshotUnavailableRefusal`, `data.buildBackupRestorePreflightUnsupportedRefusal` and `data.buildBackupRestoreRelayContractBehindRefusal` build them; bounds are `data.backupRestoreRefusalContract`. A restore event (`data.IBackupRestoreEvent`) adds the status `refused`, meaning nothing was written, with its `refusal`; `data.validateBackupRestoreEvent` judges only `status` and `refusal`, requiring `refusal` exactly when the status is `refused`. Additive: every existing restore event stays valid, including the unjudged `resourceTypes` and null `errorText` Cloudly already stores. Spec: `test.backuprestorerefusal`.
|
|
14
|
+
|
|
3
15
|
## 2026-11-05 - 32.70.0
|
|
4
16
|
|
|
5
17
|
### Features
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
*/
|
|
4
4
|
export const commitinfo = {
|
|
5
5
|
name: '@serve.zone/interfaces',
|
|
6
|
-
version: '32.
|
|
6
|
+
version: '32.72.0',
|
|
7
7
|
description: 'Shared TypeScript interfaces and TypedRequest contracts for the serve.zone ecosystem.'
|
|
8
8
|
};
|
|
9
9
|
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiMDBfY29tbWl0aW5mb19kYXRhLmpzIiwic291cmNlUm9vdCI6IiIsInNvdXJjZXMiOlsiLi4vdHMvMDBfY29tbWl0aW5mb19kYXRhLnRzIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiJBQUFBOztHQUVHO0FBQ0gsTUFBTSxDQUFDLE1BQU0sVUFBVSxHQUFHO0lBQ3hCLElBQUksRUFBRSx3QkFBd0I7SUFDOUIsT0FBTyxFQUFFLFNBQVM7SUFDbEIsV0FBVyxFQUFFLHVGQUF1RjtDQUNyRyxDQUFBIn0=
|
package/dist_ts/data/backup.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { TBackupRestoreRefusal } from './backuprestorerefusal.js';
|
|
1
2
|
import type { ICorestoreDatabaseAllocationReference } from './corestore.js';
|
|
2
3
|
export type TBackupStatus = 'pending' | 'running' | 'replicating' | 'replicated' | 'ready' | 'failed' | 'restoring' | 'restored';
|
|
3
4
|
export type TBackupTrigger = 'manual' | 'scheduled';
|
|
@@ -83,9 +84,17 @@ export type TBackupFailure = {
|
|
|
83
84
|
} | {
|
|
84
85
|
kind: 'archive-unreadable';
|
|
85
86
|
};
|
|
87
|
+
/**
|
|
88
|
+
* One restore of a backup, appended to its `restoreHistory`; `validateBackupRestoreEvent` judges its
|
|
89
|
+
* `status` and `refusal`.
|
|
90
|
+
*
|
|
91
|
+
* `refused`: the restore was refused before it wrote anything (`refusal` says why), so the service
|
|
92
|
+
* keeps the data it had. `failed`: it failed after it began writing.
|
|
93
|
+
*/
|
|
86
94
|
export interface IBackupRestoreEvent {
|
|
95
|
+
/** When the restore ended, or when it was refused. */
|
|
87
96
|
restoredAt: number;
|
|
88
|
-
status: 'restored' | 'failed';
|
|
97
|
+
status: 'restored' | 'failed' | 'refused';
|
|
89
98
|
errorText?: string;
|
|
90
99
|
/** The resource types the restore was limited to; absent when it restored every snapshot. */
|
|
91
100
|
resourceTypes?: TBackupResourceType[];
|
|
@@ -94,6 +103,8 @@ export interface IBackupRestoreEvent {
|
|
|
94
103
|
* none of the backup's `failure.uncaptured`.
|
|
95
104
|
*/
|
|
96
105
|
partial?: true;
|
|
106
|
+
/** Why a `refused` restore wrote nothing; present exactly when `status` is `refused`. */
|
|
107
|
+
refusal?: TBackupRestoreRefusal;
|
|
97
108
|
}
|
|
98
109
|
/**
|
|
99
110
|
* Durable retention hold owned by one isolated restore. An entry is active
|
package/dist_ts/data/backup.js
CHANGED
|
@@ -13,4 +13,4 @@ export const readPartialRestoreResourceTypes = (backupArg) => {
|
|
|
13
13
|
const uncapturedTypes = new Set(failure.uncaptured.map((resourceArg) => resourceArg.type));
|
|
14
14
|
return ['database', 'objectstorage'].filter((typeArg) => (!uncapturedTypes.has(typeArg) && backupArg.snapshots.some((snapshotArg) => snapshotArg.type === typeArg)));
|
|
15
15
|
};
|
|
16
|
-
//# sourceMappingURL=data:application/json;base64,
|
|
16
|
+
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiYmFja3VwLmpzIiwic291cmNlUm9vdCI6IiIsInNvdXJjZXMiOlsiLi4vLi4vdHMvZGF0YS9iYWNrdXAudHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6IkFBK0tBOzs7Ozs7O0dBT0c7QUFDSCxNQUFNLENBQUMsTUFBTSwrQkFBK0IsR0FBRyxDQUM3QyxTQUFrSCxFQUNuRSxFQUFFO0lBQ2pELE1BQU0sT0FBTyxHQUFHLFNBQVMsQ0FBQyxPQUFPLENBQUM7SUFDbEMsSUFBSSxTQUFTLENBQUMsTUFBTSxLQUFLLFFBQVEsSUFBSSxPQUFPLEVBQUUsSUFBSSxLQUFLLFlBQVk7UUFBRSxPQUFPLEVBQUUsQ0FBQztJQUMvRSxNQUFNLGVBQWUsR0FBRyxJQUFJLEdBQUcsQ0FBc0IsT0FBTyxDQUFDLFVBQVUsQ0FBQyxHQUFHLENBQUMsQ0FBQyxXQUFXLEVBQUUsRUFBRSxDQUFDLFdBQVcsQ0FBQyxJQUFJLENBQUMsQ0FBQyxDQUFDO0lBQ2hILE9BQVEsQ0FBQyxVQUFVLEVBQUUsZUFBZSxDQUFXLENBQUMsTUFBTSxDQUFDLENBQUMsT0FBTyxFQUFFLEVBQUUsQ0FBQyxDQUNsRSxDQUFDLGVBQWUsQ0FBQyxHQUFHLENBQUMsT0FBTyxDQUFDLElBQUksU0FBUyxDQUFDLFNBQVMsQ0FBQyxJQUFJLENBQUMsQ0FBQyxXQUFXLEVBQUUsRUFBRSxDQUFDLFdBQVcsQ0FBQyxJQUFJLEtBQUssT0FBTyxDQUFDLENBQ3pHLENBQUMsQ0FBQztBQUNMLENBQUMsQ0FBQyJ9
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import type { TBackupResourceType } from './backup.js';
|
|
2
|
+
/**
|
|
3
|
+
* Why an in-place restore of a backup (`IReq_Any_Cloudly_RestoreServiceBackup`) was refused before
|
|
4
|
+
* it wrote anything. A restore is all-or-nothing: Corestore checks that it can read every snapshot
|
|
5
|
+
* the restore names before it replaces any data, and Cloudly dispatches the restore only to a relay
|
|
6
|
+
* whose contract carries that check. A refusal therefore always means nothing was written; the
|
|
7
|
+
* service keeps the data it had.
|
|
8
|
+
*
|
|
9
|
+
* - `restore-snapshot-unavailable`: the node's Corestore cannot read one or more snapshots the
|
|
10
|
+
* restore names; `missing` names each of them.
|
|
11
|
+
* - `restore-preflight-unsupported`: the node's Corestore does not offer the restore check (a
|
|
12
|
+
* Corestore built before it answers the check route with HTTP 404), so it cannot prove the
|
|
13
|
+
* restore would be whole and nothing is restored.
|
|
14
|
+
* - `restore-relay-contract-behind`: the cluster relay speaks an interfaces release below the one
|
|
15
|
+
* that carries the restore check, so Cloudly does not dispatch the restore; the relay states
|
|
16
|
+
* `relayInterfacesVersion` (`null` when it stated none) and the restore needs
|
|
17
|
+
* `minimumInterfacesVersion`.
|
|
18
|
+
* - `restore-operation-unavailable`: Cloudly refuses every in-place restore because a restore is not
|
|
19
|
+
* yet an operation the node owns: a restore relayed as one request is cut at the transport
|
|
20
|
+
* timeout, so Cloudly cannot learn whether it finished. It names the missing capability, not a
|
|
21
|
+
* version, and stays in this contract permanently because stored restore events carry it.
|
|
22
|
+
* Cloudly is its only producer and reads its own restore history, so it carries this reader
|
|
23
|
+
* itself: readers of restore events upgrade before Cloudly, and Cloudly is not rolled back below
|
|
24
|
+
* a release that stored it.
|
|
25
|
+
*
|
|
26
|
+
* It travels as `TypedResponseError.errorData` of a refused restore and is recorded as the
|
|
27
|
+
* `refusal` of the backup's `refused` restore event (`IBackupRestoreEvent`). Each refusal is
|
|
28
|
+
* exactly its schema: `readBackupRestoreRefusal` refuses any other member, so a producer never adds
|
|
29
|
+
* one without gating it on the reader's interfaces version.
|
|
30
|
+
*/
|
|
31
|
+
export declare const backupRestoreRefusalNames: readonly ["restore-snapshot-unavailable", "restore-preflight-unsupported", "restore-relay-contract-behind", "restore-operation-unavailable"];
|
|
32
|
+
export type TBackupRestoreRefusalName = typeof backupRestoreRefusalNames[number];
|
|
33
|
+
export declare const backupRestoreRefusalContract: Readonly<{
|
|
34
|
+
/** At most this many snapshots are named in one `restore-snapshot-unavailable`. */
|
|
35
|
+
readonly maximumMissingSnapshots: 256;
|
|
36
|
+
/** A snapshot id or resource name is printable ASCII without spaces, at most this long. */
|
|
37
|
+
readonly maximumNameLength: 255;
|
|
38
|
+
}>;
|
|
39
|
+
/** One snapshot a restore names that the node's Corestore cannot read. */
|
|
40
|
+
export interface IBackupRestoreMissingSnapshot {
|
|
41
|
+
snapshotId: string;
|
|
42
|
+
/** The snapshot's resource type (`TBackupSnapshot['type']`). */
|
|
43
|
+
capability: TBackupResourceType;
|
|
44
|
+
/** The Corestore resource of a database or object-storage snapshot, when it is known. */
|
|
45
|
+
resourceName?: string;
|
|
46
|
+
}
|
|
47
|
+
export interface IBackupRestoreSnapshotUnavailableRefusal {
|
|
48
|
+
name: 'restore-snapshot-unavailable';
|
|
49
|
+
/** Non-empty; each snapshot once. */
|
|
50
|
+
missing: IBackupRestoreMissingSnapshot[];
|
|
51
|
+
}
|
|
52
|
+
export interface IBackupRestorePreflightUnsupportedRefusal {
|
|
53
|
+
name: 'restore-preflight-unsupported';
|
|
54
|
+
}
|
|
55
|
+
export interface IBackupRestoreRelayContractBehindRefusal {
|
|
56
|
+
name: 'restore-relay-contract-behind';
|
|
57
|
+
/** The interfaces release the relay speaks, or `null` when it stated none. */
|
|
58
|
+
relayInterfacesVersion: string | null;
|
|
59
|
+
/** The lowest interfaces release a relay must speak to carry a restore. */
|
|
60
|
+
minimumInterfacesVersion: string;
|
|
61
|
+
}
|
|
62
|
+
export interface IBackupRestoreOperationUnavailableRefusal {
|
|
63
|
+
name: 'restore-operation-unavailable';
|
|
64
|
+
}
|
|
65
|
+
/** A refused restore, discriminated by `name` (`backupRestoreRefusalNames`). */
|
|
66
|
+
export type TBackupRestoreRefusal = IBackupRestoreSnapshotUnavailableRefusal | IBackupRestorePreflightUnsupportedRefusal | IBackupRestoreRelayContractBehindRefusal | IBackupRestoreOperationUnavailableRefusal;
|
|
67
|
+
/** Validates one restore refusal against its exact schema and names every reason it is refused. */
|
|
68
|
+
export declare const validateBackupRestoreRefusal: (valueArg: unknown, pathArg?: string) => string[];
|
|
69
|
+
/** Whether a value is exactly one restore refusal. */
|
|
70
|
+
export declare const isBackupRestoreRefusal: (valueArg: unknown) => valueArg is TBackupRestoreRefusal;
|
|
71
|
+
/**
|
|
72
|
+
* The restore refusal inside an error payload or a stored restore event, as an inert frozen copy,
|
|
73
|
+
* or `null` when the value is not exactly one. It reads own data properties only, so a refusal
|
|
74
|
+
* read here carries nothing that runs.
|
|
75
|
+
*/
|
|
76
|
+
export declare const readBackupRestoreRefusal: (valueArg: unknown) => TBackupRestoreRefusal | null;
|
|
77
|
+
/** `restore-snapshot-unavailable` naming each snapshot the node's Corestore cannot read. */
|
|
78
|
+
export declare const buildBackupRestoreSnapshotUnavailableRefusal: (missingArg: IBackupRestoreMissingSnapshot[]) => IBackupRestoreSnapshotUnavailableRefusal;
|
|
79
|
+
/** `restore-preflight-unsupported`: the node's Corestore offers no restore check. */
|
|
80
|
+
export declare const buildBackupRestorePreflightUnsupportedRefusal: () => IBackupRestorePreflightUnsupportedRefusal;
|
|
81
|
+
/**
|
|
82
|
+
* `restore-relay-contract-behind`: the relay speaks an interfaces release below the minimum.
|
|
83
|
+
*
|
|
84
|
+
* Throws when a stated version is not a release version or is not below the minimum, so a caller
|
|
85
|
+
* passes `null` for a relay whose stated version it cannot read.
|
|
86
|
+
*/
|
|
87
|
+
export declare const buildBackupRestoreRelayContractBehindRefusal: (relayInterfacesVersionArg: string | null, minimumInterfacesVersionArg: string) => IBackupRestoreRelayContractBehindRefusal;
|
|
88
|
+
/**
|
|
89
|
+
* `restore-operation-unavailable`: Cloudly refuses the in-place restore because a restore is not
|
|
90
|
+
* yet a node-owned operation.
|
|
91
|
+
*/
|
|
92
|
+
export declare const buildBackupRestoreOperationUnavailableRefusal: () => IBackupRestoreOperationUnavailableRefusal;
|
|
93
|
+
/**
|
|
94
|
+
* Validates what this contract adds to a restore event of a backup's `restoreHistory` and names
|
|
95
|
+
* every reason it is refused: `status` is `restored`, `failed` or `refused`, and `refusal` is
|
|
96
|
+
* present exactly when `status` is `refused`, as one restore refusal. Every other member is left as
|
|
97
|
+
* it was written (records Cloudly already stores keep any `restoredAt`, `errorText`, `partial` and
|
|
98
|
+
* `resourceTypes` they hold), so every stored event stays valid.
|
|
99
|
+
*/
|
|
100
|
+
export declare const validateBackupRestoreEvent: (valueArg: unknown, pathArg?: string) => string[];
|
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
import { hasExactKeys, readDataElements, readDataProperties, readExactDataRecord, } from '../private/dataproperties.js';
|
|
2
|
+
import { compareReleaseVersions, parseReleaseVersion } from './releaseversion.js';
|
|
3
|
+
/**
|
|
4
|
+
* Why an in-place restore of a backup (`IReq_Any_Cloudly_RestoreServiceBackup`) was refused before
|
|
5
|
+
* it wrote anything. A restore is all-or-nothing: Corestore checks that it can read every snapshot
|
|
6
|
+
* the restore names before it replaces any data, and Cloudly dispatches the restore only to a relay
|
|
7
|
+
* whose contract carries that check. A refusal therefore always means nothing was written; the
|
|
8
|
+
* service keeps the data it had.
|
|
9
|
+
*
|
|
10
|
+
* - `restore-snapshot-unavailable`: the node's Corestore cannot read one or more snapshots the
|
|
11
|
+
* restore names; `missing` names each of them.
|
|
12
|
+
* - `restore-preflight-unsupported`: the node's Corestore does not offer the restore check (a
|
|
13
|
+
* Corestore built before it answers the check route with HTTP 404), so it cannot prove the
|
|
14
|
+
* restore would be whole and nothing is restored.
|
|
15
|
+
* - `restore-relay-contract-behind`: the cluster relay speaks an interfaces release below the one
|
|
16
|
+
* that carries the restore check, so Cloudly does not dispatch the restore; the relay states
|
|
17
|
+
* `relayInterfacesVersion` (`null` when it stated none) and the restore needs
|
|
18
|
+
* `minimumInterfacesVersion`.
|
|
19
|
+
* - `restore-operation-unavailable`: Cloudly refuses every in-place restore because a restore is not
|
|
20
|
+
* yet an operation the node owns: a restore relayed as one request is cut at the transport
|
|
21
|
+
* timeout, so Cloudly cannot learn whether it finished. It names the missing capability, not a
|
|
22
|
+
* version, and stays in this contract permanently because stored restore events carry it.
|
|
23
|
+
* Cloudly is its only producer and reads its own restore history, so it carries this reader
|
|
24
|
+
* itself: readers of restore events upgrade before Cloudly, and Cloudly is not rolled back below
|
|
25
|
+
* a release that stored it.
|
|
26
|
+
*
|
|
27
|
+
* It travels as `TypedResponseError.errorData` of a refused restore and is recorded as the
|
|
28
|
+
* `refusal` of the backup's `refused` restore event (`IBackupRestoreEvent`). Each refusal is
|
|
29
|
+
* exactly its schema: `readBackupRestoreRefusal` refuses any other member, so a producer never adds
|
|
30
|
+
* one without gating it on the reader's interfaces version.
|
|
31
|
+
*/
|
|
32
|
+
export const backupRestoreRefusalNames = Object.freeze([
|
|
33
|
+
'restore-snapshot-unavailable',
|
|
34
|
+
'restore-preflight-unsupported',
|
|
35
|
+
'restore-relay-contract-behind',
|
|
36
|
+
'restore-operation-unavailable',
|
|
37
|
+
]);
|
|
38
|
+
export const backupRestoreRefusalContract = Object.freeze({
|
|
39
|
+
/** At most this many snapshots are named in one `restore-snapshot-unavailable`. */
|
|
40
|
+
maximumMissingSnapshots: 256,
|
|
41
|
+
/** A snapshot id or resource name is printable ASCII without spaces, at most this long. */
|
|
42
|
+
maximumNameLength: 255,
|
|
43
|
+
});
|
|
44
|
+
/** Every resource type, keyed so a new `TBackupResourceType` member fails to compile here. */
|
|
45
|
+
const backupResourceTypeKeys = Object.freeze({
|
|
46
|
+
volume: true,
|
|
47
|
+
database: true,
|
|
48
|
+
objectstorage: true,
|
|
49
|
+
});
|
|
50
|
+
const isBackupResourceType = (valueArg) => (typeof valueArg === 'string' && Object.hasOwn(backupResourceTypeKeys, valueArg));
|
|
51
|
+
const printableNamePattern = /^[\x21-\x7e]+$/;
|
|
52
|
+
const isRestoreName = (valueArg) => (typeof valueArg === 'string'
|
|
53
|
+
&& valueArg.length <= backupRestoreRefusalContract.maximumNameLength
|
|
54
|
+
&& printableNamePattern.test(valueArg));
|
|
55
|
+
const nameRule = 'must be printable ASCII without spaces, at most '
|
|
56
|
+
+ `${backupRestoreRefusalContract.maximumNameLength} characters`;
|
|
57
|
+
const isRefusalName = (valueArg) => (typeof valueArg === 'string'
|
|
58
|
+
&& backupRestoreRefusalNames.includes(valueArg));
|
|
59
|
+
const validateMissingSnapshot = (valueArg, pathArg) => {
|
|
60
|
+
const snapshot = readDataProperties(valueArg);
|
|
61
|
+
if (snapshot === null || !hasExactKeys(snapshot, ['snapshotId', 'capability'], ['resourceName'])) {
|
|
62
|
+
return [`${pathArg} must use its exact schema`];
|
|
63
|
+
}
|
|
64
|
+
const errors = [];
|
|
65
|
+
if (!isRestoreName(snapshot.snapshotId)) {
|
|
66
|
+
errors.push(`${pathArg}.snapshotId ${nameRule}`);
|
|
67
|
+
}
|
|
68
|
+
if (!isBackupResourceType(snapshot.capability)) {
|
|
69
|
+
errors.push(`${pathArg}.capability must be volume, database or objectstorage`);
|
|
70
|
+
}
|
|
71
|
+
if (Object.hasOwn(snapshot, 'resourceName') && !isRestoreName(snapshot.resourceName)) {
|
|
72
|
+
errors.push(`${pathArg}.resourceName ${nameRule}`);
|
|
73
|
+
}
|
|
74
|
+
return errors;
|
|
75
|
+
};
|
|
76
|
+
const validateMissingSnapshots = (valueArg, pathArg) => {
|
|
77
|
+
const elements = readDataElements(valueArg, backupRestoreRefusalContract.maximumMissingSnapshots);
|
|
78
|
+
if (elements === null) {
|
|
79
|
+
return [`${pathArg} must be a list of at most `
|
|
80
|
+
+ `${backupRestoreRefusalContract.maximumMissingSnapshots} snapshots`];
|
|
81
|
+
}
|
|
82
|
+
if (elements.length === 0) {
|
|
83
|
+
return [`${pathArg} must name at least one snapshot`];
|
|
84
|
+
}
|
|
85
|
+
const errors = elements.flatMap((elementArg, indexArg) => (validateMissingSnapshot(elementArg, `${pathArg}[${indexArg}]`)));
|
|
86
|
+
if (errors.length > 0) {
|
|
87
|
+
return errors;
|
|
88
|
+
}
|
|
89
|
+
const snapshotIds = elements.map((elementArg) => elementArg.snapshotId);
|
|
90
|
+
if (new Set(snapshotIds).size !== snapshotIds.length) {
|
|
91
|
+
errors.push(`${pathArg} must name each snapshot once`);
|
|
92
|
+
}
|
|
93
|
+
return errors;
|
|
94
|
+
};
|
|
95
|
+
const validateRelayContractBehind = (refusalArg, pathArg) => {
|
|
96
|
+
const relay = refusalArg.relayInterfacesVersion === null
|
|
97
|
+
? null
|
|
98
|
+
: parseReleaseVersion(refusalArg.relayInterfacesVersion);
|
|
99
|
+
const minimum = parseReleaseVersion(refusalArg.minimumInterfacesVersion);
|
|
100
|
+
const errors = [];
|
|
101
|
+
if (refusalArg.relayInterfacesVersion !== null && relay === null) {
|
|
102
|
+
errors.push(`${pathArg} relayInterfacesVersion must be a release version or null`);
|
|
103
|
+
}
|
|
104
|
+
if (minimum === null) {
|
|
105
|
+
errors.push(`${pathArg} minimumInterfacesVersion must be a release version`);
|
|
106
|
+
}
|
|
107
|
+
if (relay !== null && minimum !== null && compareReleaseVersions(relay, minimum) >= 0) {
|
|
108
|
+
errors.push(`${pathArg} relayInterfacesVersion must be below minimumInterfacesVersion`);
|
|
109
|
+
}
|
|
110
|
+
return errors;
|
|
111
|
+
};
|
|
112
|
+
/** The exact keys of each refusal. */
|
|
113
|
+
const refusalKeys = Object.freeze({
|
|
114
|
+
'restore-snapshot-unavailable': Object.freeze(['name', 'missing']),
|
|
115
|
+
'restore-preflight-unsupported': Object.freeze(['name']),
|
|
116
|
+
'restore-relay-contract-behind': Object.freeze([
|
|
117
|
+
'name', 'relayInterfacesVersion', 'minimumInterfacesVersion',
|
|
118
|
+
]),
|
|
119
|
+
'restore-operation-unavailable': Object.freeze(['name']),
|
|
120
|
+
});
|
|
121
|
+
/** Validates one restore refusal against its exact schema and names every reason it is refused. */
|
|
122
|
+
export const validateBackupRestoreRefusal = (valueArg, pathArg = 'backup restore refusal') => {
|
|
123
|
+
const properties = readDataProperties(valueArg);
|
|
124
|
+
if (properties === null || !isRefusalName(properties.name)) {
|
|
125
|
+
return [`${pathArg} name must be one of ${backupRestoreRefusalNames.join(', ')}`];
|
|
126
|
+
}
|
|
127
|
+
const refusal = readExactDataRecord(valueArg, refusalKeys[properties.name]);
|
|
128
|
+
if (refusal === null) {
|
|
129
|
+
return [`${pathArg} must use the exact schema of ${properties.name}`];
|
|
130
|
+
}
|
|
131
|
+
switch (properties.name) {
|
|
132
|
+
case 'restore-snapshot-unavailable':
|
|
133
|
+
return validateMissingSnapshots(refusal.missing, `${pathArg} missing`);
|
|
134
|
+
case 'restore-preflight-unsupported':
|
|
135
|
+
return [];
|
|
136
|
+
case 'restore-relay-contract-behind':
|
|
137
|
+
return validateRelayContractBehind(refusal, pathArg);
|
|
138
|
+
case 'restore-operation-unavailable':
|
|
139
|
+
return [];
|
|
140
|
+
}
|
|
141
|
+
};
|
|
142
|
+
/** Whether a value is exactly one restore refusal. */
|
|
143
|
+
export const isBackupRestoreRefusal = (valueArg) => (validateBackupRestoreRefusal(valueArg).length === 0);
|
|
144
|
+
const freezeMissingSnapshot = (snapshotArg) => Object.freeze({
|
|
145
|
+
snapshotId: snapshotArg.snapshotId,
|
|
146
|
+
capability: snapshotArg.capability,
|
|
147
|
+
...(snapshotArg.resourceName === undefined ? {} : { resourceName: snapshotArg.resourceName }),
|
|
148
|
+
});
|
|
149
|
+
const copySnapshotUnavailableRefusal = (refusalArg) => {
|
|
150
|
+
const missing = refusalArg.missing.map(freezeMissingSnapshot);
|
|
151
|
+
Object.freeze(missing);
|
|
152
|
+
return Object.freeze({ name: refusalArg.name, missing });
|
|
153
|
+
};
|
|
154
|
+
const copyPreflightUnsupportedRefusal = (refusalArg) => Object.freeze({ name: refusalArg.name });
|
|
155
|
+
const copyRelayContractBehindRefusal = (refusalArg) => Object.freeze({
|
|
156
|
+
name: refusalArg.name,
|
|
157
|
+
relayInterfacesVersion: refusalArg.relayInterfacesVersion,
|
|
158
|
+
minimumInterfacesVersion: refusalArg.minimumInterfacesVersion,
|
|
159
|
+
});
|
|
160
|
+
const copyOperationUnavailableRefusal = (refusalArg) => Object.freeze({ name: refusalArg.name });
|
|
161
|
+
/** An inert frozen copy of a refusal that passed `validateBackupRestoreRefusal`. */
|
|
162
|
+
const copyRefusal = (refusalArg) => {
|
|
163
|
+
switch (refusalArg.name) {
|
|
164
|
+
case 'restore-snapshot-unavailable':
|
|
165
|
+
return copySnapshotUnavailableRefusal(refusalArg);
|
|
166
|
+
case 'restore-preflight-unsupported':
|
|
167
|
+
return copyPreflightUnsupportedRefusal(refusalArg);
|
|
168
|
+
case 'restore-relay-contract-behind':
|
|
169
|
+
return copyRelayContractBehindRefusal(refusalArg);
|
|
170
|
+
case 'restore-operation-unavailable':
|
|
171
|
+
return copyOperationUnavailableRefusal(refusalArg);
|
|
172
|
+
}
|
|
173
|
+
};
|
|
174
|
+
/**
|
|
175
|
+
* The restore refusal inside an error payload or a stored restore event, as an inert frozen copy,
|
|
176
|
+
* or `null` when the value is not exactly one. It reads own data properties only, so a refusal
|
|
177
|
+
* read here carries nothing that runs.
|
|
178
|
+
*/
|
|
179
|
+
export const readBackupRestoreRefusal = (valueArg) => (
|
|
180
|
+
// A value that passed holds own data properties of exactly the refusal's schema at every level,
|
|
181
|
+
// so the copy reads the same data the validation judged.
|
|
182
|
+
isBackupRestoreRefusal(valueArg) ? copyRefusal(valueArg) : null);
|
|
183
|
+
/** The refusal a builder composed, unchanged, or an error naming every reason it is refused. */
|
|
184
|
+
const requireRefusal = (refusalArg) => {
|
|
185
|
+
const errors = validateBackupRestoreRefusal(refusalArg);
|
|
186
|
+
if (errors.length > 0) {
|
|
187
|
+
throw new Error(errors.join('; '));
|
|
188
|
+
}
|
|
189
|
+
return refusalArg;
|
|
190
|
+
};
|
|
191
|
+
/** `restore-snapshot-unavailable` naming each snapshot the node's Corestore cannot read. */
|
|
192
|
+
export const buildBackupRestoreSnapshotUnavailableRefusal = (missingArg) => copySnapshotUnavailableRefusal(requireRefusal({
|
|
193
|
+
name: 'restore-snapshot-unavailable',
|
|
194
|
+
missing: missingArg.map((snapshotArg) => ({ ...snapshotArg })),
|
|
195
|
+
}));
|
|
196
|
+
/** `restore-preflight-unsupported`: the node's Corestore offers no restore check. */
|
|
197
|
+
export const buildBackupRestorePreflightUnsupportedRefusal = () => (copyPreflightUnsupportedRefusal(requireRefusal({ name: 'restore-preflight-unsupported' })));
|
|
198
|
+
/**
|
|
199
|
+
* `restore-relay-contract-behind`: the relay speaks an interfaces release below the minimum.
|
|
200
|
+
*
|
|
201
|
+
* Throws when a stated version is not a release version or is not below the minimum, so a caller
|
|
202
|
+
* passes `null` for a relay whose stated version it cannot read.
|
|
203
|
+
*/
|
|
204
|
+
export const buildBackupRestoreRelayContractBehindRefusal = (relayInterfacesVersionArg, minimumInterfacesVersionArg) => copyRelayContractBehindRefusal(requireRefusal({
|
|
205
|
+
name: 'restore-relay-contract-behind',
|
|
206
|
+
relayInterfacesVersion: relayInterfacesVersionArg,
|
|
207
|
+
minimumInterfacesVersion: minimumInterfacesVersionArg,
|
|
208
|
+
}));
|
|
209
|
+
/**
|
|
210
|
+
* `restore-operation-unavailable`: Cloudly refuses the in-place restore because a restore is not
|
|
211
|
+
* yet a node-owned operation.
|
|
212
|
+
*/
|
|
213
|
+
export const buildBackupRestoreOperationUnavailableRefusal = () => (copyOperationUnavailableRefusal(requireRefusal({ name: 'restore-operation-unavailable' })));
|
|
214
|
+
const restoreEventStatuses = Object.freeze([
|
|
215
|
+
'restored', 'failed', 'refused',
|
|
216
|
+
]);
|
|
217
|
+
/**
|
|
218
|
+
* Validates what this contract adds to a restore event of a backup's `restoreHistory` and names
|
|
219
|
+
* every reason it is refused: `status` is `restored`, `failed` or `refused`, and `refusal` is
|
|
220
|
+
* present exactly when `status` is `refused`, as one restore refusal. Every other member is left as
|
|
221
|
+
* it was written (records Cloudly already stores keep any `restoredAt`, `errorText`, `partial` and
|
|
222
|
+
* `resourceTypes` they hold), so every stored event stays valid.
|
|
223
|
+
*/
|
|
224
|
+
export const validateBackupRestoreEvent = (valueArg, pathArg = 'backup restore event') => {
|
|
225
|
+
const event = readDataProperties(valueArg);
|
|
226
|
+
if (event === null) {
|
|
227
|
+
return [`${pathArg} must be a plain record of data properties`];
|
|
228
|
+
}
|
|
229
|
+
const errors = [];
|
|
230
|
+
if (!restoreEventStatuses.includes(event.status)) {
|
|
231
|
+
errors.push(`${pathArg} status must be one of ${restoreEventStatuses.join(', ')}`);
|
|
232
|
+
}
|
|
233
|
+
const hasRefusal = Object.hasOwn(event, 'refusal');
|
|
234
|
+
if (event.status === 'refused') {
|
|
235
|
+
if (!hasRefusal) {
|
|
236
|
+
errors.push(`${pathArg} refusal is required when status is refused`);
|
|
237
|
+
}
|
|
238
|
+
else {
|
|
239
|
+
errors.push(...validateBackupRestoreRefusal(event.refusal, `${pathArg} refusal`));
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
else if (hasRefusal) {
|
|
243
|
+
errors.push(`${pathArg} refusal is allowed only when status is refused`);
|
|
244
|
+
}
|
|
245
|
+
return errors;
|
|
246
|
+
};
|
|
247
|
+
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiYmFja3VwcmVzdG9yZXJlZnVzYWwuanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuLi8uLi90cy9kYXRhL2JhY2t1cHJlc3RvcmVyZWZ1c2FsLnRzIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiJBQUNBLE9BQU8sRUFDTCxZQUFZLEVBQUUsZ0JBQWdCLEVBQUUsa0JBQWtCLEVBQUUsbUJBQW1CLEdBQ3hFLE1BQU0sOEJBQThCLENBQUM7QUFDdEMsT0FBTyxFQUFFLHNCQUFzQixFQUFFLG1CQUFtQixFQUFFLE1BQU0scUJBQXFCLENBQUM7QUFFbEY7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7R0E0Qkc7QUFDSCxNQUFNLENBQUMsTUFBTSx5QkFBeUIsR0FBRyxNQUFNLENBQUMsTUFBTSxDQUFDO0lBQ3JELDhCQUE4QjtJQUM5QiwrQkFBK0I7SUFDL0IsK0JBQStCO0lBQy9CLCtCQUErQjtDQUN2QixDQUFDLENBQUM7QUFJWixNQUFNLENBQUMsTUFBTSw0QkFBNEIsR0FBRyxNQUFNLENBQUMsTUFBTSxDQUFDO0lBQ3hELG1GQUFtRjtJQUNuRix1QkFBdUIsRUFBRSxHQUFHO0lBQzVCLDJGQUEyRjtJQUMzRixpQkFBaUIsRUFBRSxHQUFHO0NBQ2QsQ0FBQyxDQUFDO0FBd0NaLDhGQUE4RjtBQUM5RixNQUFNLHNCQUFzQixHQUFnRCxNQUFNLENBQUMsTUFBTSxDQUFDO0lBQ3hGLE1BQU0sRUFBRSxJQUFJO0lBQ1osUUFBUSxFQUFFLElBQUk7SUFDZCxhQUFhLEVBQUUsSUFBSTtDQUNwQixDQUFDLENBQUM7QUFFSCxNQUFNLG9CQUFvQixHQUFHLENBQUMsUUFBaUIsRUFBbUMsRUFBRSxDQUFDLENBQ25GLE9BQU8sUUFBUSxLQUFLLFFBQVEsSUFBSSxNQUFNLENBQUMsTUFBTSxDQUFDLHNCQUFzQixFQUFFLFFBQVEsQ0FBQyxDQUNoRixDQUFDO0FBRUYsTUFBTSxvQkFBb0IsR0FBRyxnQkFBZ0IsQ0FBQztBQUU5QyxNQUFNLGFBQWEsR0FBRyxDQUFDLFFBQWlCLEVBQXNCLEVBQUUsQ0FBQyxDQUMvRCxPQUFPLFFBQVEsS0FBSyxRQUFRO09BQ3pCLFFBQVEsQ0FBQyxNQUFNLElBQUksNEJBQTRCLENBQUMsaUJBQWlCO09BQ2pFLG9CQUFvQixDQUFDLElBQUksQ0FBQyxRQUFRLENBQUMsQ0FDdkMsQ0FBQztBQUVGLE1BQU0sUUFBUSxHQUFHLGtEQUFrRDtNQUMvRCxHQUFHLDRCQUE0QixDQUFDLGlCQUFpQixhQUFhLENBQUM7QUFFbkUsTUFBTSxhQUFhLEdBQUcsQ0FBQyxRQUFpQixFQUF5QyxFQUFFLENBQUMsQ0FDbEYsT0FBTyxRQUFRLEtBQUssUUFBUTtPQUN4Qix5QkFBK0MsQ0FBQyxRQUFRLENBQUMsUUFBUSxDQUFDLENBQ3ZFLENBQUM7QUFFRixNQUFNLHVCQUF1QixHQUFHLENBQUMsUUFBaUIsRUFBRSxPQUFlLEVBQVksRUFBRTtJQUMvRSxNQUFNLFFBQVEsR0FBRyxrQkFBa0IsQ0FBQyxRQUFRLENBQUMsQ0FBQztJQUM5QyxJQUFJLFFBQVEsS0FBSyxJQUFJLElBQUksQ0FBQyxZQUFZLENBQUMsUUFBUSxFQUFFLENBQUMsWUFBWSxFQUFFLFlBQVksQ0FBQyxFQUFFLENBQUMsY0FBYyxDQUFDLENBQUMsRUFBRSxDQUFDO1FBQ2pHLE9BQU8sQ0FBQyxHQUFHLE9BQU8sNEJBQTRCLENBQUMsQ0FBQztJQUNsRCxDQUFDO0lBQ0QsTUFBTSxNQUFNLEdBQWEsRUFBRSxDQUFDO0lBQzVCLElBQUksQ0FBQyxhQUFhLENBQUMsUUFBUSxDQUFDLFVBQVUsQ0FBQyxFQUFFLENBQUM7UUFDeEMsTUFBTSxDQUFDLElBQUksQ0FBQyxHQUFHLE9BQU8sZUFBZSxRQUFRLEVBQUUsQ0FBQyxDQUFDO0lBQ25ELENBQUM7SUFDRCxJQUFJLENBQUMsb0JBQW9CLENBQUMsUUFBUSxDQUFDLFVBQVUsQ0FBQyxFQUFFLENBQUM7UUFDL0MsTUFBTSxDQUFDLElBQUksQ0FBQyxHQUFHLE9BQU8sdURBQXVELENBQUMsQ0FBQztJQUNqRixDQUFDO0lBQ0QsSUFBSSxNQUFNLENBQUMsTUFBTSxDQUFDLFFBQVEsRUFBRSxjQUFjLENBQUMsSUFBSSxDQUFDLGFBQWEsQ0FBQyxRQUFRLENBQUMsWUFBWSxDQUFDLEVBQUUsQ0FBQztRQUNyRixNQUFNLENBQUMsSUFBSSxDQUFDLEdBQUcsT0FBTyxpQkFBaUIsUUFBUSxFQUFFLENBQUMsQ0FBQztJQUNyRCxDQUFDO0lBQ0QsT0FBTyxNQUFNLENBQUM7QUFDaEIsQ0FBQyxDQUFDO0FBRUYsTUFBTSx3QkFBd0IsR0FBRyxDQUFDLFFBQWlCLEVBQUUsT0FBZSxFQUFZLEVBQUU7SUFDaEYsTUFBTSxRQUFRLEdBQUcsZ0JBQWdCLENBQUMsUUFBUSxFQUFFLDRCQUE0QixDQUFDLHVCQUF1QixDQUFDLENBQUM7SUFDbEcsSUFBSSxRQUFRLEtBQUssSUFBSSxFQUFFLENBQUM7UUFDdEIsT0FBTyxDQUFDLEdBQUcsT0FBTyw2QkFBNkI7a0JBQzNDLEdBQUcsNEJBQTRCLENBQUMsdUJBQXVCLFlBQVksQ0FBQyxDQUFDO0lBQzNFLENBQUM7SUFDRCxJQUFJLFFBQVEsQ0FBQyxNQUFNLEtBQUssQ0FBQyxFQUFFLENBQUM7UUFDMUIsT0FBTyxDQUFDLEdBQUcsT0FBTyxrQ0FBa0MsQ0FBQyxDQUFDO0lBQ3hELENBQUM7SUFDRCxNQUFNLE1BQU0sR0FBRyxRQUFRLENBQUMsT0FBTyxDQUFDLENBQUMsVUFBVSxFQUFFLFFBQVEsRUFBRSxFQUFFLENBQUMsQ0FDeEQsdUJBQXVCLENBQUMsVUFBVSxFQUFFLEdBQUcsT0FBTyxJQUFJLFFBQVEsR0FBRyxDQUFDLENBQy9ELENBQUMsQ0FBQztJQUNILElBQUksTUFBTSxDQUFDLE1BQU0sR0FBRyxDQUFDLEVBQUUsQ0FBQztRQUN0QixPQUFPLE1BQU0sQ0FBQztJQUNoQixDQUFDO0lBQ0QsTUFBTSxXQUFXLEdBQUcsUUFBUSxDQUFDLEdBQUcsQ0FBQyxDQUFDLFVBQVUsRUFBRSxFQUFFLENBQUUsVUFBNEMsQ0FBQyxVQUFVLENBQUMsQ0FBQztJQUMzRyxJQUFJLElBQUksR0FBRyxDQUFDLFdBQVcsQ0FBQyxDQUFDLElBQUksS0FBSyxXQUFXLENBQUMsTUFBTSxFQUFFLENBQUM7UUFDckQsTUFBTSxDQUFDLElBQUksQ0FBQyxHQUFHLE9BQU8sK0JBQStCLENBQUMsQ0FBQztJQUN6RCxDQUFDO0lBQ0QsT0FBTyxNQUFNLENBQUM7QUFDaEIsQ0FBQyxDQUFDO0FBRUYsTUFBTSwyQkFBMkIsR0FBRyxDQUNsQyxVQUFtQyxFQUNuQyxPQUFlLEVBQ0wsRUFBRTtJQUNaLE1BQU0sS0FBSyxHQUFHLFVBQVUsQ0FBQyxzQkFBc0IsS0FBSyxJQUFJO1FBQ3RELENBQUMsQ0FBQyxJQUFJO1FBQ04sQ0FBQyxDQUFDLG1CQUFtQixDQUFDLFVBQVUsQ0FBQyxzQkFBc0IsQ0FBQyxDQUFDO0lBQzNELE1BQU0sT0FBTyxHQUFHLG1CQUFtQixDQUFDLFVBQVUsQ0FBQyx3QkFBd0IsQ0FBQyxDQUFDO0lBQ3pFLE1BQU0sTUFBTSxHQUFhLEVBQUUsQ0FBQztJQUM1QixJQUFJLFVBQVUsQ0FBQyxzQkFBc0IsS0FBSyxJQUFJLElBQUksS0FBSyxLQUFLLElBQUksRUFBRSxDQUFDO1FBQ2pFLE1BQU0sQ0FBQyxJQUFJLENBQUMsR0FBRyxPQUFPLDJEQUEyRCxDQUFDLENBQUM7SUFDckYsQ0FBQztJQUNELElBQUksT0FBTyxLQUFLLElBQUksRUFBRSxDQUFDO1FBQ3JCLE1BQU0sQ0FBQyxJQUFJLENBQUMsR0FBRyxPQUFPLHFEQUFxRCxDQUFDLENBQUM7SUFDL0UsQ0FBQztJQUNELElBQUksS0FBSyxLQUFLLElBQUksSUFBSSxPQUFPLEtBQUssSUFBSSxJQUFJLHNCQUFzQixDQUFDLEtBQUssRUFBRSxPQUFPLENBQUMsSUFBSSxDQUFDLEVBQUUsQ0FBQztRQUN0RixNQUFNLENBQUMsSUFBSSxDQUFDLEdBQUcsT0FBTyxnRUFBZ0UsQ0FBQyxDQUFDO0lBQzFGLENBQUM7SUFDRCxPQUFPLE1BQU0sQ0FBQztBQUNoQixDQUFDLENBQUM7QUFFRixzQ0FBc0M7QUFDdEMsTUFBTSxXQUFXLEdBQW1FLE1BQU0sQ0FBQyxNQUFNLENBQUM7SUFDaEcsOEJBQThCLEVBQUUsTUFBTSxDQUFDLE1BQU0sQ0FBQyxDQUFDLE1BQU0sRUFBRSxTQUFTLENBQUMsQ0FBQztJQUNsRSwrQkFBK0IsRUFBRSxNQUFNLENBQUMsTUFBTSxDQUFDLENBQUMsTUFBTSxDQUFDLENBQUM7SUFDeEQsK0JBQStCLEVBQUUsTUFBTSxDQUFDLE1BQU0sQ0FBQztRQUM3QyxNQUFNLEVBQUUsd0JBQXdCLEVBQUUsMEJBQTBCO0tBQzdELENBQUM7SUFDRiwrQkFBK0IsRUFBRSxNQUFNLENBQUMsTUFBTSxDQUFDLENBQUMsTUFBTSxDQUFDLENBQUM7Q0FDekQsQ0FBQyxDQUFDO0FBRUgsbUdBQW1HO0FBQ25HLE1BQU0sQ0FBQyxNQUFNLDRCQUE0QixHQUFHLENBQzFDLFFBQWlCLEVBQ2pCLE9BQU8sR0FBRyx3QkFBd0IsRUFDeEIsRUFBRTtJQUNaLE1BQU0sVUFBVSxHQUFHLGtCQUFrQixDQUFDLFFBQVEsQ0FBQyxDQUFDO0lBQ2hELElBQUksVUFBVSxLQUFLLElBQUksSUFBSSxDQUFDLGFBQWEsQ0FBQyxVQUFVLENBQUMsSUFBSSxDQUFDLEVBQUUsQ0FBQztRQUMzRCxPQUFPLENBQUMsR0FBRyxPQUFPLHdCQUF3Qix5QkFBeUIsQ0FBQyxJQUFJLENBQUMsSUFBSSxDQUFDLEVBQUUsQ0FBQyxDQUFDO0lBQ3BGLENBQUM7SUFDRCxNQUFNLE9BQU8sR0FBRyxtQkFBbUIsQ0FBQyxRQUFRLEVBQUUsV0FBVyxDQUFDLFVBQVUsQ0FBQyxJQUFJLENBQUMsQ0FBQyxDQUFDO0lBQzVFLElBQUksT0FBTyxLQUFLLElBQUksRUFBRSxDQUFDO1FBQ3JCLE9BQU8sQ0FBQyxHQUFHLE9BQU8saUNBQWlDLFVBQVUsQ0FBQyxJQUFJLEVBQUUsQ0FBQyxDQUFDO0lBQ3hFLENBQUM7SUFDRCxRQUFRLFVBQVUsQ0FBQyxJQUFJLEVBQUUsQ0FBQztRQUN4QixLQUFLLDhCQUE4QjtZQUNqQyxPQUFPLHdCQUF3QixDQUFDLE9BQU8sQ0FBQyxPQUFPLEVBQUUsR0FBRyxPQUFPLFVBQVUsQ0FBQyxDQUFDO1FBQ3pFLEtBQUssK0JBQStCO1lBQ2xDLE9BQU8sRUFBRSxDQUFDO1FBQ1osS0FBSywrQkFBK0I7WUFDbEMsT0FBTywyQkFBMkIsQ0FBQyxPQUFPLEVBQUUsT0FBTyxDQUFDLENBQUM7UUFDdkQsS0FBSywrQkFBK0I7WUFDbEMsT0FBTyxFQUFFLENBQUM7SUFDZCxDQUFDO0FBQ0gsQ0FBQyxDQUFDO0FBRUYsc0RBQXNEO0FBQ3RELE1BQU0sQ0FBQyxNQUFNLHNCQUFzQixHQUFHLENBQUMsUUFBaUIsRUFBcUMsRUFBRSxDQUFDLENBQzlGLDRCQUE0QixDQUFDLFFBQVEsQ0FBQyxDQUFDLE1BQU0sS0FBSyxDQUFDLENBQ3BELENBQUM7QUFFRixNQUFNLHFCQUFxQixHQUFHLENBQzVCLFdBQTBDLEVBQ1gsRUFBRSxDQUFDLE1BQU0sQ0FBQyxNQUFNLENBQUM7SUFDaEQsVUFBVSxFQUFFLFdBQVcsQ0FBQyxVQUFVO0lBQ2xDLFVBQVUsRUFBRSxXQUFXLENBQUMsVUFBVTtJQUNsQyxHQUFHLENBQUMsV0FBVyxDQUFDLFlBQVksS0FBSyxTQUFTLENBQUMsQ0FBQyxDQUFDLEVBQUUsQ0FBQyxDQUFDLENBQUMsRUFBRSxZQUFZLEVBQUUsV0FBVyxDQUFDLFlBQVksRUFBRSxDQUFDO0NBQzlGLENBQUMsQ0FBQztBQUVILE1BQU0sOEJBQThCLEdBQUcsQ0FDckMsVUFBb0QsRUFDVixFQUFFO0lBQzVDLE1BQU0sT0FBTyxHQUFHLFVBQVUsQ0FBQyxPQUFPLENBQUMsR0FBRyxDQUFDLHFCQUFxQixDQUFDLENBQUM7SUFDOUQsTUFBTSxDQUFDLE1BQU0sQ0FBQyxPQUFPLENBQUMsQ0FBQztJQUN2QixPQUFPLE1BQU0sQ0FBQyxNQUFNLENBQUMsRUFBRSxJQUFJLEVBQUUsVUFBVSxDQUFDLElBQUksRUFBRSxPQUFPLEVBQUUsQ0FBQyxDQUFDO0FBQzNELENBQUMsQ0FBQztBQUVGLE1BQU0sK0JBQStCLEdBQUcsQ0FDdEMsVUFBcUQsRUFDVixFQUFFLENBQUMsTUFBTSxDQUFDLE1BQU0sQ0FBQyxFQUFFLElBQUksRUFBRSxVQUFVLENBQUMsSUFBSSxFQUFFLENBQUMsQ0FBQztBQUV6RixNQUFNLDhCQUE4QixHQUFHLENBQ3JDLFVBQW9ELEVBQ1YsRUFBRSxDQUFDLE1BQU0sQ0FBQyxNQUFNLENBQUM7SUFDM0QsSUFBSSxFQUFFLFVBQVUsQ0FBQyxJQUFJO0lBQ3JCLHNCQUFzQixFQUFFLFVBQVUsQ0FBQyxzQkFBc0I7SUFDekQsd0JBQXdCLEVBQUUsVUFBVSxDQUFDLHdCQUF3QjtDQUM5RCxDQUFDLENBQUM7QUFFSCxNQUFNLCtCQUErQixHQUFHLENBQ3RDLFVBQXFELEVBQ1YsRUFBRSxDQUFDLE1BQU0sQ0FBQyxNQUFNLENBQUMsRUFBRSxJQUFJLEVBQUUsVUFBVSxDQUFDLElBQUksRUFBRSxDQUFDLENBQUM7QUFFekYsb0ZBQW9GO0FBQ3BGLE1BQU0sV0FBVyxHQUFHLENBQUMsVUFBaUMsRUFBeUIsRUFBRTtJQUMvRSxRQUFRLFVBQVUsQ0FBQyxJQUFJLEVBQUUsQ0FBQztRQUN4QixLQUFLLDhCQUE4QjtZQUNqQyxPQUFPLDhCQUE4QixDQUFDLFVBQVUsQ0FBQyxDQUFDO1FBQ3BELEtBQUssK0JBQStCO1lBQ2xDLE9BQU8sK0JBQStCLENBQUMsVUFBVSxDQUFDLENBQUM7UUFDckQsS0FBSywrQkFBK0I7WUFDbEMsT0FBTyw4QkFBOEIsQ0FBQyxVQUFVLENBQUMsQ0FBQztRQUNwRCxLQUFLLCtCQUErQjtZQUNsQyxPQUFPLCtCQUErQixDQUFDLFVBQVUsQ0FBQyxDQUFDO0lBQ3ZELENBQUM7QUFDSCxDQUFDLENBQUM7QUFFRjs7OztHQUlHO0FBQ0gsTUFBTSxDQUFDLE1BQU0sd0JBQXdCLEdBQUcsQ0FBQyxRQUFpQixFQUFnQyxFQUFFLENBQUM7QUFDM0YsZ0dBQWdHO0FBQ2hHLHlEQUF5RDtBQUN6RCxzQkFBc0IsQ0FBQyxRQUFRLENBQUMsQ0FBQyxDQUFDLENBQUMsV0FBVyxDQUFDLFFBQVEsQ0FBQyxDQUFDLENBQUMsQ0FBQyxJQUFJLENBQ2hFLENBQUM7QUFFRixnR0FBZ0c7QUFDaEcsTUFBTSxjQUFjLEdBQUcsQ0FBa0MsVUFBYSxFQUFLLEVBQUU7SUFDM0UsTUFBTSxNQUFNLEdBQUcsNEJBQTRCLENBQUMsVUFBVSxDQUFDLENBQUM7SUFDeEQsSUFBSSxNQUFNLENBQUMsTUFBTSxHQUFHLENBQUMsRUFBRSxDQUFDO1FBQ3RCLE1BQU0sSUFBSSxLQUFLLENBQUMsTUFBTSxDQUFDLElBQUksQ0FBQyxJQUFJLENBQUMsQ0FBQyxDQUFDO0lBQ3JDLENBQUM7SUFDRCxPQUFPLFVBQVUsQ0FBQztBQUNwQixDQUFDLENBQUM7QUFFRiw0RkFBNEY7QUFDNUYsTUFBTSxDQUFDLE1BQU0sNENBQTRDLEdBQUcsQ0FDMUQsVUFBMkMsRUFDRCxFQUFFLENBQUMsOEJBQThCLENBQUMsY0FBYyxDQUFDO0lBQzNGLElBQUksRUFBRSw4QkFBOEI7SUFDcEMsT0FBTyxFQUFFLFVBQVUsQ0FBQyxHQUFHLENBQUMsQ0FBQyxXQUFXLEVBQUUsRUFBRSxDQUFDLENBQUMsRUFBRSxHQUFHLFdBQVcsRUFBRSxDQUFDLENBQUM7Q0FDL0QsQ0FBQyxDQUFDLENBQUM7QUFFSixxRkFBcUY7QUFDckYsTUFBTSxDQUFDLE1BQU0sNkNBQTZDLEdBQUcsR0FBOEMsRUFBRSxDQUFDLENBQzVHLCtCQUErQixDQUFDLGNBQWMsQ0FBQyxFQUFFLElBQUksRUFBRSwrQkFBK0IsRUFBRSxDQUFDLENBQUMsQ0FDM0YsQ0FBQztBQUVGOzs7OztHQUtHO0FBQ0gsTUFBTSxDQUFDLE1BQU0sNENBQTRDLEdBQUcsQ0FDMUQseUJBQXdDLEVBQ3hDLDJCQUFtQyxFQUNPLEVBQUUsQ0FBQyw4QkFBOEIsQ0FBQyxjQUFjLENBQUM7SUFDM0YsSUFBSSxFQUFFLCtCQUErQjtJQUNyQyxzQkFBc0IsRUFBRSx5QkFBeUI7SUFDakQsd0JBQXdCLEVBQUUsMkJBQTJCO0NBQ3RELENBQUMsQ0FBQyxDQUFDO0FBRUo7OztHQUdHO0FBQ0gsTUFBTSxDQUFDLE1BQU0sNkNBQTZDLEdBQUcsR0FBOEMsRUFBRSxDQUFDLENBQzVHLCtCQUErQixDQUFDLGNBQWMsQ0FBQyxFQUFFLElBQUksRUFBRSwrQkFBK0IsRUFBRSxDQUFDLENBQUMsQ0FDM0YsQ0FBQztBQUVGLE1BQU0sb0JBQW9CLEdBQTZDLE1BQU0sQ0FBQyxNQUFNLENBQUM7SUFDbkYsVUFBVSxFQUFFLFFBQVEsRUFBRSxTQUFTO0NBQ2hDLENBQUMsQ0FBQztBQUVIOzs7Ozs7R0FNRztBQUNILE1BQU0sQ0FBQyxNQUFNLDBCQUEwQixHQUFHLENBQ3hDLFFBQWlCLEVBQ2pCLE9BQU8sR0FBRyxzQkFBc0IsRUFDdEIsRUFBRTtJQUNaLE1BQU0sS0FBSyxHQUFHLGtCQUFrQixDQUFDLFFBQVEsQ0FBQyxDQUFDO0lBQzNDLElBQUksS0FBSyxLQUFLLElBQUksRUFBRSxDQUFDO1FBQ25CLE9BQU8sQ0FBQyxHQUFHLE9BQU8sNENBQTRDLENBQUMsQ0FBQztJQUNsRSxDQUFDO0lBQ0QsTUFBTSxNQUFNLEdBQWEsRUFBRSxDQUFDO0lBQzVCLElBQUksQ0FBRSxvQkFBMkMsQ0FBQyxRQUFRLENBQUMsS0FBSyxDQUFDLE1BQU0sQ0FBQyxFQUFFLENBQUM7UUFDekUsTUFBTSxDQUFDLElBQUksQ0FBQyxHQUFHLE9BQU8sMEJBQTBCLG9CQUFvQixDQUFDLElBQUksQ0FBQyxJQUFJLENBQUMsRUFBRSxDQUFDLENBQUM7SUFDckYsQ0FBQztJQUNELE1BQU0sVUFBVSxHQUFHLE1BQU0sQ0FBQyxNQUFNLENBQUMsS0FBSyxFQUFFLFNBQVMsQ0FBQyxDQUFDO0lBQ25ELElBQUksS0FBSyxDQUFDLE1BQU0sS0FBSyxTQUFTLEVBQUUsQ0FBQztRQUMvQixJQUFJLENBQUMsVUFBVSxFQUFFLENBQUM7WUFDaEIsTUFBTSxDQUFDLElBQUksQ0FBQyxHQUFHLE9BQU8sNkNBQTZDLENBQUMsQ0FBQztRQUN2RSxDQUFDO2FBQU0sQ0FBQztZQUNOLE1BQU0sQ0FBQyxJQUFJLENBQUMsR0FBRyw0QkFBNEIsQ0FBQyxLQUFLLENBQUMsT0FBTyxFQUFFLEdBQUcsT0FBTyxVQUFVLENBQUMsQ0FBQyxDQUFDO1FBQ3BGLENBQUM7SUFDSCxDQUFDO1NBQU0sSUFBSSxVQUFVLEVBQUUsQ0FBQztRQUN0QixNQUFNLENBQUMsSUFBSSxDQUFDLEdBQUcsT0FBTyxpREFBaUQsQ0FBQyxDQUFDO0lBQzNFLENBQUM7SUFDRCxPQUFPLE1BQU0sQ0FBQztBQUNoQixDQUFDLENBQUMifQ==
|
package/dist_ts/data/index.d.ts
CHANGED
|
@@ -75,6 +75,7 @@ export * from './secret.js';
|
|
|
75
75
|
export * from './baremetal.js';
|
|
76
76
|
export * from './baseos.js';
|
|
77
77
|
export * from './backup.js';
|
|
78
|
+
export * from './backuprestorerefusal.js';
|
|
78
79
|
export * from './clusternode.js';
|
|
79
80
|
export * from './clusternodehostname.js';
|
|
80
81
|
export * from './settings.js';
|
package/dist_ts/data/index.js
CHANGED
|
@@ -70,6 +70,7 @@ export * from './secret.js';
|
|
|
70
70
|
export * from './baremetal.js';
|
|
71
71
|
export * from './baseos.js';
|
|
72
72
|
export * from './backup.js';
|
|
73
|
+
export * from './backuprestorerefusal.js';
|
|
73
74
|
export * from './clusternode.js';
|
|
74
75
|
export * from './clusternodehostname.js';
|
|
75
76
|
export * from './settings.js';
|
|
@@ -91,4 +92,4 @@ export * from './user.js';
|
|
|
91
92
|
export * from './version.js';
|
|
92
93
|
export * from './webpush.js';
|
|
93
94
|
export * from './workloadreplicaidentity.js';
|
|
94
|
-
//# sourceMappingURL=data:application/json;base64,
|
|
95
|
+
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiaW5kZXguanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuLi8uLi90cy9kYXRhL2luZGV4LnRzIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiJBQUFBLGNBQWMsb0JBQW9CLENBQUM7QUFDbkMsY0FBYyxjQUFjLENBQUM7QUFDN0IsY0FBYyxxQkFBcUIsQ0FBQztBQUNwQyxjQUFjLGlCQUFpQixDQUFDO0FBQ2hDLGNBQWMscUJBQXFCLENBQUM7QUFDcEMsY0FBYyxnQ0FBZ0MsQ0FBQztBQUMvQyxjQUFjLDZCQUE2QixDQUFDO0FBQzVDLGNBQWMsYUFBYSxDQUFDO0FBQzVCLGNBQWMsZ0JBQWdCLENBQUM7QUFDL0IsY0FBYyw0QkFBNEIsQ0FBQztBQUMzQyxjQUFjLDhCQUE4QixDQUFDO0FBQzdDLGNBQWMsMEJBQTBCLENBQUM7QUFDekMsY0FBYyxlQUFlLENBQUM7QUFDOUIsY0FBYyx1QkFBdUIsQ0FBQztBQUN0QyxjQUFjLGlCQUFpQixDQUFDO0FBQ2hDLGNBQWMseUJBQXlCLENBQUM7QUFDeEMsY0FBYyw2QkFBNkIsQ0FBQztBQUM1QyxjQUFjLDRCQUE0QixDQUFDO0FBQzNDLGNBQWMsMEJBQTBCLENBQUM7QUFDekMsY0FBYywwQkFBMEIsQ0FBQztBQUN6QyxjQUFjLGdDQUFnQyxDQUFDO0FBQy9DLGNBQWMsZ0JBQWdCLENBQUM7QUFDL0IsY0FBYyxVQUFVLENBQUM7QUFDekIsY0FBYyxhQUFhLENBQUM7QUFDNUIsY0FBYyxhQUFhLENBQUM7QUFDNUIsY0FBYyxZQUFZLENBQUM7QUFDM0IsY0FBYyx1QkFBdUIsQ0FBQztBQUN0QyxjQUFjLG1CQUFtQixDQUFDO0FBQ2xDLGNBQWMsY0FBYyxDQUFDO0FBQzdCLGNBQWMsZ0JBQWdCLENBQUM7QUFDL0IsY0FBYyxZQUFZLENBQUM7QUFDM0IsY0FBYyxxQkFBcUIsQ0FBQztBQUNwQyxjQUFjLHNCQUFzQixDQUFDO0FBQ3JDLGNBQWMsNkJBQTZCLENBQUM7QUFDNUMsY0FBYyxXQUFXLENBQUM7QUFDMUIsY0FBYyxxQkFBcUIsQ0FBQztBQUNwQyxjQUFjLHFCQUFxQixDQUFDO0FBQ3BDLGNBQWMseUJBQXlCLENBQUM7QUFDeEMsY0FBYyxxQkFBcUIsQ0FBQztBQUNwQyxjQUFjLHlCQUF5QixDQUFDO0FBQ3hDLGNBQWMseUJBQXlCLENBQUM7QUFDeEMsY0FBYyx1QkFBdUIsQ0FBQztBQUN0QyxjQUFjLHdCQUF3QixDQUFDO0FBQ3ZDLGNBQWMsc0JBQXNCLENBQUM7QUFDckMsY0FBYyw4QkFBOEIsQ0FBQztBQUM3QyxjQUFjLG1CQUFtQixDQUFDO0FBQ2xDLGNBQWMsa0JBQWtCLENBQUM7QUFDakMsY0FBYyx3QkFBd0IsQ0FBQztBQUl2QyxPQUFPLEVBQUUsMEJBQTBCLEVBQUUsTUFBTSxrQkFBa0IsQ0FBQztBQUc5RCxPQUFPLEVBQUUscUNBQXFDLEVBQUUsTUFBTSw2QkFBNkIsQ0FBQztBQUVwRixPQUFPLEVBQUUsMENBQTBDLEVBQUUsMENBQTBDLEVBQUUsTUFBTSxrQ0FBa0MsQ0FBQztBQUMxSSxjQUFjLHdCQUF3QixDQUFDO0FBQ3ZDLGNBQWMscUJBQXFCLENBQUM7QUFDcEMsY0FBYyxxQkFBcUIsQ0FBQztBQUNwQyxjQUFjLGVBQWUsQ0FBQztBQUM5QixjQUFjLHFCQUFxQixDQUFDO0FBQ3BDLGNBQWMsd0JBQXdCLENBQUM7QUFDdkMsY0FBYyw0QkFBNEIsQ0FBQztBQUMzQyxjQUFjLDRCQUE0QixDQUFDO0FBQzNDLGNBQWMsNkJBQTZCLENBQUM7QUFDNUMsY0FBYyxvQkFBb0IsQ0FBQztBQUNuQyxjQUFjLDBCQUEwQixDQUFDO0FBQ3pDLGNBQWMsK0JBQStCLENBQUM7QUFDOUMsY0FBYyxnQ0FBZ0MsQ0FBQztBQUMvQyxjQUFjLHFCQUFxQixDQUFDO0FBQ3BDLGNBQWMseUJBQXlCLENBQUM7QUFDeEMsY0FBYyw0QkFBNEIsQ0FBQztBQUMzQyxjQUFjLDZCQUE2QixDQUFDO0FBQzVDLGNBQWMsYUFBYSxDQUFDO0FBQzVCLGNBQWMsZ0JBQWdCLENBQUM7QUFDL0IsY0FBYyxhQUFhLENBQUM7QUFDNUIsY0FBYyxhQUFhLENBQUM7QUFDNUIsY0FBYywyQkFBMkIsQ0FBQztBQUMxQyxjQUFjLGtCQUFrQixDQUFDO0FBQ2pDLGNBQWMsMEJBQTBCLENBQUM7QUFDekMsY0FBYyxlQUFlLENBQUM7QUFDOUIsY0FBYyw0QkFBNEIsQ0FBQztBQUMzQyxjQUFjLHNCQUFzQixDQUFDO0FBQ3JDLGNBQWMsaUJBQWlCLENBQUM7QUFDaEMsY0FBYyx1QkFBdUIsQ0FBQztBQUN0QyxjQUFjLGNBQWMsQ0FBQztBQUM3QixjQUFjLHdCQUF3QixDQUFDO0FBQ3ZDLGNBQWMsK0JBQStCLENBQUM7QUFDOUMsY0FBYyxtQkFBbUIsQ0FBQztBQUNsQyxjQUFjLHFCQUFxQixDQUFDO0FBQ3BDLGNBQWMsMEJBQTBCLENBQUM7QUFDekMsY0FBYyxrQ0FBa0MsQ0FBQztBQUNqRCxjQUFjLGFBQWEsQ0FBQztBQUM1QixjQUFjLG9CQUFvQixDQUFDO0FBQ25DLGNBQWMsY0FBYyxDQUFDO0FBQzdCLGNBQWMsV0FBVyxDQUFDO0FBQzFCLGNBQWMsY0FBYyxDQUFDO0FBQzdCLGNBQWMsY0FBYyxDQUFDO0FBQzdCLGNBQWMsOEJBQThCLENBQUMifQ==
|
package/package.json
CHANGED
package/ts/00_commitinfo_data.ts
CHANGED
package/ts/data/backup.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { TBackupRestoreRefusal } from './backuprestorerefusal.js';
|
|
1
2
|
import type { ICorestoreDatabaseAllocationReference } from './corestore.js';
|
|
2
3
|
|
|
3
4
|
export type TBackupStatus =
|
|
@@ -101,9 +102,17 @@ export type TBackupFailure =
|
|
|
101
102
|
| { kind: 'incomplete'; uncaptured: TBackupUncapturedResource[] }
|
|
102
103
|
| { kind: 'archive-unreadable' };
|
|
103
104
|
|
|
105
|
+
/**
|
|
106
|
+
* One restore of a backup, appended to its `restoreHistory`; `validateBackupRestoreEvent` judges its
|
|
107
|
+
* `status` and `refusal`.
|
|
108
|
+
*
|
|
109
|
+
* `refused`: the restore was refused before it wrote anything (`refusal` says why), so the service
|
|
110
|
+
* keeps the data it had. `failed`: it failed after it began writing.
|
|
111
|
+
*/
|
|
104
112
|
export interface IBackupRestoreEvent {
|
|
113
|
+
/** When the restore ended, or when it was refused. */
|
|
105
114
|
restoredAt: number;
|
|
106
|
-
status: 'restored' | 'failed';
|
|
115
|
+
status: 'restored' | 'failed' | 'refused';
|
|
107
116
|
errorText?: string;
|
|
108
117
|
/** The resource types the restore was limited to; absent when it restored every snapshot. */
|
|
109
118
|
resourceTypes?: TBackupResourceType[];
|
|
@@ -112,6 +121,8 @@ export interface IBackupRestoreEvent {
|
|
|
112
121
|
* none of the backup's `failure.uncaptured`.
|
|
113
122
|
*/
|
|
114
123
|
partial?: true;
|
|
124
|
+
/** Why a `refused` restore wrote nothing; present exactly when `status` is `refused`. */
|
|
125
|
+
refusal?: TBackupRestoreRefusal;
|
|
115
126
|
}
|
|
116
127
|
|
|
117
128
|
/**
|
|
@@ -0,0 +1,354 @@
|
|
|
1
|
+
import type { IBackupRestoreEvent, TBackupResourceType } from './backup.js';
|
|
2
|
+
import {
|
|
3
|
+
hasExactKeys, readDataElements, readDataProperties, readExactDataRecord,
|
|
4
|
+
} from '../private/dataproperties.js';
|
|
5
|
+
import { compareReleaseVersions, parseReleaseVersion } from './releaseversion.js';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Why an in-place restore of a backup (`IReq_Any_Cloudly_RestoreServiceBackup`) was refused before
|
|
9
|
+
* it wrote anything. A restore is all-or-nothing: Corestore checks that it can read every snapshot
|
|
10
|
+
* the restore names before it replaces any data, and Cloudly dispatches the restore only to a relay
|
|
11
|
+
* whose contract carries that check. A refusal therefore always means nothing was written; the
|
|
12
|
+
* service keeps the data it had.
|
|
13
|
+
*
|
|
14
|
+
* - `restore-snapshot-unavailable`: the node's Corestore cannot read one or more snapshots the
|
|
15
|
+
* restore names; `missing` names each of them.
|
|
16
|
+
* - `restore-preflight-unsupported`: the node's Corestore does not offer the restore check (a
|
|
17
|
+
* Corestore built before it answers the check route with HTTP 404), so it cannot prove the
|
|
18
|
+
* restore would be whole and nothing is restored.
|
|
19
|
+
* - `restore-relay-contract-behind`: the cluster relay speaks an interfaces release below the one
|
|
20
|
+
* that carries the restore check, so Cloudly does not dispatch the restore; the relay states
|
|
21
|
+
* `relayInterfacesVersion` (`null` when it stated none) and the restore needs
|
|
22
|
+
* `minimumInterfacesVersion`.
|
|
23
|
+
* - `restore-operation-unavailable`: Cloudly refuses every in-place restore because a restore is not
|
|
24
|
+
* yet an operation the node owns: a restore relayed as one request is cut at the transport
|
|
25
|
+
* timeout, so Cloudly cannot learn whether it finished. It names the missing capability, not a
|
|
26
|
+
* version, and stays in this contract permanently because stored restore events carry it.
|
|
27
|
+
* Cloudly is its only producer and reads its own restore history, so it carries this reader
|
|
28
|
+
* itself: readers of restore events upgrade before Cloudly, and Cloudly is not rolled back below
|
|
29
|
+
* a release that stored it.
|
|
30
|
+
*
|
|
31
|
+
* It travels as `TypedResponseError.errorData` of a refused restore and is recorded as the
|
|
32
|
+
* `refusal` of the backup's `refused` restore event (`IBackupRestoreEvent`). Each refusal is
|
|
33
|
+
* exactly its schema: `readBackupRestoreRefusal` refuses any other member, so a producer never adds
|
|
34
|
+
* one without gating it on the reader's interfaces version.
|
|
35
|
+
*/
|
|
36
|
+
export const backupRestoreRefusalNames = Object.freeze([
|
|
37
|
+
'restore-snapshot-unavailable',
|
|
38
|
+
'restore-preflight-unsupported',
|
|
39
|
+
'restore-relay-contract-behind',
|
|
40
|
+
'restore-operation-unavailable',
|
|
41
|
+
] as const);
|
|
42
|
+
|
|
43
|
+
export type TBackupRestoreRefusalName = typeof backupRestoreRefusalNames[number];
|
|
44
|
+
|
|
45
|
+
export const backupRestoreRefusalContract = Object.freeze({
|
|
46
|
+
/** At most this many snapshots are named in one `restore-snapshot-unavailable`. */
|
|
47
|
+
maximumMissingSnapshots: 256,
|
|
48
|
+
/** A snapshot id or resource name is printable ASCII without spaces, at most this long. */
|
|
49
|
+
maximumNameLength: 255,
|
|
50
|
+
} as const);
|
|
51
|
+
|
|
52
|
+
/** One snapshot a restore names that the node's Corestore cannot read. */
|
|
53
|
+
export interface IBackupRestoreMissingSnapshot {
|
|
54
|
+
snapshotId: string;
|
|
55
|
+
/** The snapshot's resource type (`TBackupSnapshot['type']`). */
|
|
56
|
+
capability: TBackupResourceType;
|
|
57
|
+
/** The Corestore resource of a database or object-storage snapshot, when it is known. */
|
|
58
|
+
resourceName?: string;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export interface IBackupRestoreSnapshotUnavailableRefusal {
|
|
62
|
+
name: 'restore-snapshot-unavailable';
|
|
63
|
+
/** Non-empty; each snapshot once. */
|
|
64
|
+
missing: IBackupRestoreMissingSnapshot[];
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export interface IBackupRestorePreflightUnsupportedRefusal {
|
|
68
|
+
name: 'restore-preflight-unsupported';
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export interface IBackupRestoreRelayContractBehindRefusal {
|
|
72
|
+
name: 'restore-relay-contract-behind';
|
|
73
|
+
/** The interfaces release the relay speaks, or `null` when it stated none. */
|
|
74
|
+
relayInterfacesVersion: string | null;
|
|
75
|
+
/** The lowest interfaces release a relay must speak to carry a restore. */
|
|
76
|
+
minimumInterfacesVersion: string;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export interface IBackupRestoreOperationUnavailableRefusal {
|
|
80
|
+
name: 'restore-operation-unavailable';
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/** A refused restore, discriminated by `name` (`backupRestoreRefusalNames`). */
|
|
84
|
+
export type TBackupRestoreRefusal =
|
|
85
|
+
| IBackupRestoreSnapshotUnavailableRefusal
|
|
86
|
+
| IBackupRestorePreflightUnsupportedRefusal
|
|
87
|
+
| IBackupRestoreRelayContractBehindRefusal
|
|
88
|
+
| IBackupRestoreOperationUnavailableRefusal;
|
|
89
|
+
|
|
90
|
+
/** Every resource type, keyed so a new `TBackupResourceType` member fails to compile here. */
|
|
91
|
+
const backupResourceTypeKeys: Readonly<Record<TBackupResourceType, true>> = Object.freeze({
|
|
92
|
+
volume: true,
|
|
93
|
+
database: true,
|
|
94
|
+
objectstorage: true,
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
const isBackupResourceType = (valueArg: unknown): valueArg is TBackupResourceType => (
|
|
98
|
+
typeof valueArg === 'string' && Object.hasOwn(backupResourceTypeKeys, valueArg)
|
|
99
|
+
);
|
|
100
|
+
|
|
101
|
+
const printableNamePattern = /^[\x21-\x7e]+$/;
|
|
102
|
+
|
|
103
|
+
const isRestoreName = (valueArg: unknown): valueArg is string => (
|
|
104
|
+
typeof valueArg === 'string'
|
|
105
|
+
&& valueArg.length <= backupRestoreRefusalContract.maximumNameLength
|
|
106
|
+
&& printableNamePattern.test(valueArg)
|
|
107
|
+
);
|
|
108
|
+
|
|
109
|
+
const nameRule = 'must be printable ASCII without spaces, at most '
|
|
110
|
+
+ `${backupRestoreRefusalContract.maximumNameLength} characters`;
|
|
111
|
+
|
|
112
|
+
const isRefusalName = (valueArg: unknown): valueArg is TBackupRestoreRefusalName => (
|
|
113
|
+
typeof valueArg === 'string'
|
|
114
|
+
&& (backupRestoreRefusalNames as readonly string[]).includes(valueArg)
|
|
115
|
+
);
|
|
116
|
+
|
|
117
|
+
const validateMissingSnapshot = (valueArg: unknown, pathArg: string): string[] => {
|
|
118
|
+
const snapshot = readDataProperties(valueArg);
|
|
119
|
+
if (snapshot === null || !hasExactKeys(snapshot, ['snapshotId', 'capability'], ['resourceName'])) {
|
|
120
|
+
return [`${pathArg} must use its exact schema`];
|
|
121
|
+
}
|
|
122
|
+
const errors: string[] = [];
|
|
123
|
+
if (!isRestoreName(snapshot.snapshotId)) {
|
|
124
|
+
errors.push(`${pathArg}.snapshotId ${nameRule}`);
|
|
125
|
+
}
|
|
126
|
+
if (!isBackupResourceType(snapshot.capability)) {
|
|
127
|
+
errors.push(`${pathArg}.capability must be volume, database or objectstorage`);
|
|
128
|
+
}
|
|
129
|
+
if (Object.hasOwn(snapshot, 'resourceName') && !isRestoreName(snapshot.resourceName)) {
|
|
130
|
+
errors.push(`${pathArg}.resourceName ${nameRule}`);
|
|
131
|
+
}
|
|
132
|
+
return errors;
|
|
133
|
+
};
|
|
134
|
+
|
|
135
|
+
const validateMissingSnapshots = (valueArg: unknown, pathArg: string): string[] => {
|
|
136
|
+
const elements = readDataElements(valueArg, backupRestoreRefusalContract.maximumMissingSnapshots);
|
|
137
|
+
if (elements === null) {
|
|
138
|
+
return [`${pathArg} must be a list of at most `
|
|
139
|
+
+ `${backupRestoreRefusalContract.maximumMissingSnapshots} snapshots`];
|
|
140
|
+
}
|
|
141
|
+
if (elements.length === 0) {
|
|
142
|
+
return [`${pathArg} must name at least one snapshot`];
|
|
143
|
+
}
|
|
144
|
+
const errors = elements.flatMap((elementArg, indexArg) => (
|
|
145
|
+
validateMissingSnapshot(elementArg, `${pathArg}[${indexArg}]`)
|
|
146
|
+
));
|
|
147
|
+
if (errors.length > 0) {
|
|
148
|
+
return errors;
|
|
149
|
+
}
|
|
150
|
+
const snapshotIds = elements.map((elementArg) => (elementArg as IBackupRestoreMissingSnapshot).snapshotId);
|
|
151
|
+
if (new Set(snapshotIds).size !== snapshotIds.length) {
|
|
152
|
+
errors.push(`${pathArg} must name each snapshot once`);
|
|
153
|
+
}
|
|
154
|
+
return errors;
|
|
155
|
+
};
|
|
156
|
+
|
|
157
|
+
const validateRelayContractBehind = (
|
|
158
|
+
refusalArg: Record<string, unknown>,
|
|
159
|
+
pathArg: string,
|
|
160
|
+
): string[] => {
|
|
161
|
+
const relay = refusalArg.relayInterfacesVersion === null
|
|
162
|
+
? null
|
|
163
|
+
: parseReleaseVersion(refusalArg.relayInterfacesVersion);
|
|
164
|
+
const minimum = parseReleaseVersion(refusalArg.minimumInterfacesVersion);
|
|
165
|
+
const errors: string[] = [];
|
|
166
|
+
if (refusalArg.relayInterfacesVersion !== null && relay === null) {
|
|
167
|
+
errors.push(`${pathArg} relayInterfacesVersion must be a release version or null`);
|
|
168
|
+
}
|
|
169
|
+
if (minimum === null) {
|
|
170
|
+
errors.push(`${pathArg} minimumInterfacesVersion must be a release version`);
|
|
171
|
+
}
|
|
172
|
+
if (relay !== null && minimum !== null && compareReleaseVersions(relay, minimum) >= 0) {
|
|
173
|
+
errors.push(`${pathArg} relayInterfacesVersion must be below minimumInterfacesVersion`);
|
|
174
|
+
}
|
|
175
|
+
return errors;
|
|
176
|
+
};
|
|
177
|
+
|
|
178
|
+
/** The exact keys of each refusal. */
|
|
179
|
+
const refusalKeys: Readonly<Record<TBackupRestoreRefusalName, readonly string[]>> = Object.freeze({
|
|
180
|
+
'restore-snapshot-unavailable': Object.freeze(['name', 'missing']),
|
|
181
|
+
'restore-preflight-unsupported': Object.freeze(['name']),
|
|
182
|
+
'restore-relay-contract-behind': Object.freeze([
|
|
183
|
+
'name', 'relayInterfacesVersion', 'minimumInterfacesVersion',
|
|
184
|
+
]),
|
|
185
|
+
'restore-operation-unavailable': Object.freeze(['name']),
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
/** Validates one restore refusal against its exact schema and names every reason it is refused. */
|
|
189
|
+
export const validateBackupRestoreRefusal = (
|
|
190
|
+
valueArg: unknown,
|
|
191
|
+
pathArg = 'backup restore refusal',
|
|
192
|
+
): string[] => {
|
|
193
|
+
const properties = readDataProperties(valueArg);
|
|
194
|
+
if (properties === null || !isRefusalName(properties.name)) {
|
|
195
|
+
return [`${pathArg} name must be one of ${backupRestoreRefusalNames.join(', ')}`];
|
|
196
|
+
}
|
|
197
|
+
const refusal = readExactDataRecord(valueArg, refusalKeys[properties.name]);
|
|
198
|
+
if (refusal === null) {
|
|
199
|
+
return [`${pathArg} must use the exact schema of ${properties.name}`];
|
|
200
|
+
}
|
|
201
|
+
switch (properties.name) {
|
|
202
|
+
case 'restore-snapshot-unavailable':
|
|
203
|
+
return validateMissingSnapshots(refusal.missing, `${pathArg} missing`);
|
|
204
|
+
case 'restore-preflight-unsupported':
|
|
205
|
+
return [];
|
|
206
|
+
case 'restore-relay-contract-behind':
|
|
207
|
+
return validateRelayContractBehind(refusal, pathArg);
|
|
208
|
+
case 'restore-operation-unavailable':
|
|
209
|
+
return [];
|
|
210
|
+
}
|
|
211
|
+
};
|
|
212
|
+
|
|
213
|
+
/** Whether a value is exactly one restore refusal. */
|
|
214
|
+
export const isBackupRestoreRefusal = (valueArg: unknown): valueArg is TBackupRestoreRefusal => (
|
|
215
|
+
validateBackupRestoreRefusal(valueArg).length === 0
|
|
216
|
+
);
|
|
217
|
+
|
|
218
|
+
const freezeMissingSnapshot = (
|
|
219
|
+
snapshotArg: IBackupRestoreMissingSnapshot,
|
|
220
|
+
): IBackupRestoreMissingSnapshot => Object.freeze({
|
|
221
|
+
snapshotId: snapshotArg.snapshotId,
|
|
222
|
+
capability: snapshotArg.capability,
|
|
223
|
+
...(snapshotArg.resourceName === undefined ? {} : { resourceName: snapshotArg.resourceName }),
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
const copySnapshotUnavailableRefusal = (
|
|
227
|
+
refusalArg: IBackupRestoreSnapshotUnavailableRefusal,
|
|
228
|
+
): IBackupRestoreSnapshotUnavailableRefusal => {
|
|
229
|
+
const missing = refusalArg.missing.map(freezeMissingSnapshot);
|
|
230
|
+
Object.freeze(missing);
|
|
231
|
+
return Object.freeze({ name: refusalArg.name, missing });
|
|
232
|
+
};
|
|
233
|
+
|
|
234
|
+
const copyPreflightUnsupportedRefusal = (
|
|
235
|
+
refusalArg: IBackupRestorePreflightUnsupportedRefusal,
|
|
236
|
+
): IBackupRestorePreflightUnsupportedRefusal => Object.freeze({ name: refusalArg.name });
|
|
237
|
+
|
|
238
|
+
const copyRelayContractBehindRefusal = (
|
|
239
|
+
refusalArg: IBackupRestoreRelayContractBehindRefusal,
|
|
240
|
+
): IBackupRestoreRelayContractBehindRefusal => Object.freeze({
|
|
241
|
+
name: refusalArg.name,
|
|
242
|
+
relayInterfacesVersion: refusalArg.relayInterfacesVersion,
|
|
243
|
+
minimumInterfacesVersion: refusalArg.minimumInterfacesVersion,
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
const copyOperationUnavailableRefusal = (
|
|
247
|
+
refusalArg: IBackupRestoreOperationUnavailableRefusal,
|
|
248
|
+
): IBackupRestoreOperationUnavailableRefusal => Object.freeze({ name: refusalArg.name });
|
|
249
|
+
|
|
250
|
+
/** An inert frozen copy of a refusal that passed `validateBackupRestoreRefusal`. */
|
|
251
|
+
const copyRefusal = (refusalArg: TBackupRestoreRefusal): TBackupRestoreRefusal => {
|
|
252
|
+
switch (refusalArg.name) {
|
|
253
|
+
case 'restore-snapshot-unavailable':
|
|
254
|
+
return copySnapshotUnavailableRefusal(refusalArg);
|
|
255
|
+
case 'restore-preflight-unsupported':
|
|
256
|
+
return copyPreflightUnsupportedRefusal(refusalArg);
|
|
257
|
+
case 'restore-relay-contract-behind':
|
|
258
|
+
return copyRelayContractBehindRefusal(refusalArg);
|
|
259
|
+
case 'restore-operation-unavailable':
|
|
260
|
+
return copyOperationUnavailableRefusal(refusalArg);
|
|
261
|
+
}
|
|
262
|
+
};
|
|
263
|
+
|
|
264
|
+
/**
|
|
265
|
+
* The restore refusal inside an error payload or a stored restore event, as an inert frozen copy,
|
|
266
|
+
* or `null` when the value is not exactly one. It reads own data properties only, so a refusal
|
|
267
|
+
* read here carries nothing that runs.
|
|
268
|
+
*/
|
|
269
|
+
export const readBackupRestoreRefusal = (valueArg: unknown): TBackupRestoreRefusal | null => (
|
|
270
|
+
// A value that passed holds own data properties of exactly the refusal's schema at every level,
|
|
271
|
+
// so the copy reads the same data the validation judged.
|
|
272
|
+
isBackupRestoreRefusal(valueArg) ? copyRefusal(valueArg) : null
|
|
273
|
+
);
|
|
274
|
+
|
|
275
|
+
/** The refusal a builder composed, unchanged, or an error naming every reason it is refused. */
|
|
276
|
+
const requireRefusal = <T extends TBackupRestoreRefusal>(refusalArg: T): T => {
|
|
277
|
+
const errors = validateBackupRestoreRefusal(refusalArg);
|
|
278
|
+
if (errors.length > 0) {
|
|
279
|
+
throw new Error(errors.join('; '));
|
|
280
|
+
}
|
|
281
|
+
return refusalArg;
|
|
282
|
+
};
|
|
283
|
+
|
|
284
|
+
/** `restore-snapshot-unavailable` naming each snapshot the node's Corestore cannot read. */
|
|
285
|
+
export const buildBackupRestoreSnapshotUnavailableRefusal = (
|
|
286
|
+
missingArg: IBackupRestoreMissingSnapshot[],
|
|
287
|
+
): IBackupRestoreSnapshotUnavailableRefusal => copySnapshotUnavailableRefusal(requireRefusal({
|
|
288
|
+
name: 'restore-snapshot-unavailable',
|
|
289
|
+
missing: missingArg.map((snapshotArg) => ({ ...snapshotArg })),
|
|
290
|
+
}));
|
|
291
|
+
|
|
292
|
+
/** `restore-preflight-unsupported`: the node's Corestore offers no restore check. */
|
|
293
|
+
export const buildBackupRestorePreflightUnsupportedRefusal = (): IBackupRestorePreflightUnsupportedRefusal => (
|
|
294
|
+
copyPreflightUnsupportedRefusal(requireRefusal({ name: 'restore-preflight-unsupported' }))
|
|
295
|
+
);
|
|
296
|
+
|
|
297
|
+
/**
|
|
298
|
+
* `restore-relay-contract-behind`: the relay speaks an interfaces release below the minimum.
|
|
299
|
+
*
|
|
300
|
+
* Throws when a stated version is not a release version or is not below the minimum, so a caller
|
|
301
|
+
* passes `null` for a relay whose stated version it cannot read.
|
|
302
|
+
*/
|
|
303
|
+
export const buildBackupRestoreRelayContractBehindRefusal = (
|
|
304
|
+
relayInterfacesVersionArg: string | null,
|
|
305
|
+
minimumInterfacesVersionArg: string,
|
|
306
|
+
): IBackupRestoreRelayContractBehindRefusal => copyRelayContractBehindRefusal(requireRefusal({
|
|
307
|
+
name: 'restore-relay-contract-behind',
|
|
308
|
+
relayInterfacesVersion: relayInterfacesVersionArg,
|
|
309
|
+
minimumInterfacesVersion: minimumInterfacesVersionArg,
|
|
310
|
+
}));
|
|
311
|
+
|
|
312
|
+
/**
|
|
313
|
+
* `restore-operation-unavailable`: Cloudly refuses the in-place restore because a restore is not
|
|
314
|
+
* yet a node-owned operation.
|
|
315
|
+
*/
|
|
316
|
+
export const buildBackupRestoreOperationUnavailableRefusal = (): IBackupRestoreOperationUnavailableRefusal => (
|
|
317
|
+
copyOperationUnavailableRefusal(requireRefusal({ name: 'restore-operation-unavailable' }))
|
|
318
|
+
);
|
|
319
|
+
|
|
320
|
+
const restoreEventStatuses: readonly IBackupRestoreEvent['status'][] = Object.freeze([
|
|
321
|
+
'restored', 'failed', 'refused',
|
|
322
|
+
]);
|
|
323
|
+
|
|
324
|
+
/**
|
|
325
|
+
* Validates what this contract adds to a restore event of a backup's `restoreHistory` and names
|
|
326
|
+
* every reason it is refused: `status` is `restored`, `failed` or `refused`, and `refusal` is
|
|
327
|
+
* present exactly when `status` is `refused`, as one restore refusal. Every other member is left as
|
|
328
|
+
* it was written (records Cloudly already stores keep any `restoredAt`, `errorText`, `partial` and
|
|
329
|
+
* `resourceTypes` they hold), so every stored event stays valid.
|
|
330
|
+
*/
|
|
331
|
+
export const validateBackupRestoreEvent = (
|
|
332
|
+
valueArg: unknown,
|
|
333
|
+
pathArg = 'backup restore event',
|
|
334
|
+
): string[] => {
|
|
335
|
+
const event = readDataProperties(valueArg);
|
|
336
|
+
if (event === null) {
|
|
337
|
+
return [`${pathArg} must be a plain record of data properties`];
|
|
338
|
+
}
|
|
339
|
+
const errors: string[] = [];
|
|
340
|
+
if (!(restoreEventStatuses as readonly unknown[]).includes(event.status)) {
|
|
341
|
+
errors.push(`${pathArg} status must be one of ${restoreEventStatuses.join(', ')}`);
|
|
342
|
+
}
|
|
343
|
+
const hasRefusal = Object.hasOwn(event, 'refusal');
|
|
344
|
+
if (event.status === 'refused') {
|
|
345
|
+
if (!hasRefusal) {
|
|
346
|
+
errors.push(`${pathArg} refusal is required when status is refused`);
|
|
347
|
+
} else {
|
|
348
|
+
errors.push(...validateBackupRestoreRefusal(event.refusal, `${pathArg} refusal`));
|
|
349
|
+
}
|
|
350
|
+
} else if (hasRefusal) {
|
|
351
|
+
errors.push(`${pathArg} refusal is allowed only when status is refused`);
|
|
352
|
+
}
|
|
353
|
+
return errors;
|
|
354
|
+
};
|
package/ts/data/index.ts
CHANGED
|
@@ -76,6 +76,7 @@ export * from './secret.js';
|
|
|
76
76
|
export * from './baremetal.js';
|
|
77
77
|
export * from './baseos.js';
|
|
78
78
|
export * from './backup.js';
|
|
79
|
+
export * from './backuprestorerefusal.js';
|
|
79
80
|
export * from './clusternode.js';
|
|
80
81
|
export * from './clusternodehostname.js';
|
|
81
82
|
export * from './settings.js';
|