@camstack/system 1.2.460 → 1.2.462
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/dist/addon-runner.js +21 -276
- package/dist/addon-runner.mjs +21 -276
- package/dist/addon-utils.js +1 -1
- package/dist/addon-utils.mjs +1 -1
- package/dist/{agent-https-Dnr_nIVx.js → agent-https-C6M6apPV.js} +1 -1
- package/dist/{agent-https-Iw3RSQup.mjs → agent-https-CC7jpktS.mjs} +1 -1
- package/dist/builtins/addon-pages-aggregator/addon-pages-aggregator.addon.js +1 -1
- package/dist/builtins/addon-pages-aggregator/addon-pages-aggregator.addon.mjs +1 -1
- package/dist/builtins/addon-widgets-aggregator/addon-widgets-aggregator.addon.js +1 -1
- package/dist/builtins/addon-widgets-aggregator/addon-widgets-aggregator.addon.mjs +1 -1
- package/dist/builtins/alerts/alerts.addon.js +1 -1
- package/dist/builtins/alerts/alerts.addon.mjs +1 -1
- package/dist/builtins/autotrack/index.js +1 -1
- package/dist/builtins/autotrack/index.mjs +1 -1
- package/dist/builtins/backup-orchestrator/backup-orchestrator.addon.d.ts +1 -3
- package/dist/builtins/backup-orchestrator/backup-orchestrator.addon.js +51 -502
- package/dist/builtins/backup-orchestrator/backup-orchestrator.addon.mjs +51 -502
- package/dist/builtins/backup-orchestrator/destination-policy.d.ts +5 -21
- package/dist/builtins/backup-orchestrator/record-kinds.d.ts +0 -2
- package/dist/builtins/backup-orchestrator/schedule-store.d.ts +5 -13
- package/dist/builtins/backup-orchestrator/seed-marker-store.d.ts +9 -19
- package/dist/builtins/calendars/calendars.addon.js +1 -1
- package/dist/builtins/calendars/calendars.addon.mjs +1 -1
- package/dist/builtins/camera-grid/index.js +1 -1
- package/dist/builtins/camera-grid/index.mjs +1 -1
- package/dist/builtins/cluster-agent/record-kinds.d.ts +0 -1
- package/dist/builtins/composer/composer.addon.js +2 -2
- package/dist/builtins/composer/composer.addon.mjs +2 -2
- package/dist/builtins/console-logging/index.js +1 -1
- package/dist/builtins/console-logging/index.mjs +1 -1
- package/dist/builtins/core-blocks/core-blocks.addon.js +2 -2
- package/dist/builtins/core-blocks/core-blocks.addon.mjs +2 -2
- package/dist/builtins/device-history/device-history.addon.js +1 -1
- package/dist/builtins/device-history/device-history.addon.mjs +1 -1
- package/dist/builtins/device-manager/device-manager.addon.js +2 -2
- package/dist/builtins/device-manager/device-manager.addon.mjs +2 -2
- package/dist/builtins/hub-forwarder/index.js +1 -1
- package/dist/builtins/hub-forwarder/index.mjs +1 -1
- package/dist/builtins/keychain/index.js +3 -3
- package/dist/builtins/keychain/index.mjs +3 -3
- package/dist/builtins/liveness-monitor/liveness-monitor.addon.js +1 -1
- package/dist/builtins/liveness-monitor/liveness-monitor.addon.mjs +1 -1
- package/dist/builtins/local-auth/auth-schema.d.ts +2 -12
- package/dist/builtins/local-auth/local-auth.addon.js +52 -191
- package/dist/builtins/local-auth/local-auth.addon.mjs +52 -191
- package/dist/builtins/local-auth/oauth-session-manager.d.ts +13 -4
- package/dist/builtins/local-auth/record-kinds.d.ts +1 -1
- package/dist/builtins/local-network/local-network.addon.js +2 -2
- package/dist/builtins/local-network/local-network.addon.mjs +2 -2
- package/dist/builtins/loki-logging/index.js +1 -1
- package/dist/builtins/loki-logging/index.mjs +1 -1
- package/dist/builtins/native-metrics/native-metrics.addon.js +1 -1
- package/dist/builtins/native-metrics/native-metrics.addon.mjs +1 -1
- package/dist/builtins/platform-probe/index.js +1 -1
- package/dist/builtins/platform-probe/index.mjs +1 -1
- package/dist/builtins/record-entries/index.js +1 -1
- package/dist/builtins/record-entries/index.mjs +1 -1
- package/dist/builtins/remote-access-orchestrator/remote-access-orchestrator.addon.js +1 -1
- package/dist/builtins/remote-access-orchestrator/remote-access-orchestrator.addon.mjs +1 -1
- package/dist/builtins/snapshot/index.js +1 -1
- package/dist/builtins/snapshot/index.mjs +1 -1
- package/dist/builtins/sqlite-storage/file-host-protocol.d.ts +0 -9
- package/dist/builtins/sqlite-storage/filesystem-storage.addon.js +1 -1
- package/dist/builtins/sqlite-storage/filesystem-storage.addon.mjs +1 -1
- package/dist/builtins/sqlite-storage/index.js +1 -1
- package/dist/builtins/sqlite-storage/index.mjs +1 -1
- package/dist/builtins/sqlite-storage/owned-sqlite-file.d.ts +0 -8
- package/dist/builtins/sqlite-storage/records/records-sql.d.ts +0 -2
- package/dist/builtins/sqlite-storage/sqlite-settings-backend.d.ts +0 -28
- package/dist/builtins/sqlite-storage/sqlite-settings.addon.d.ts +1 -2
- package/dist/builtins/sqlite-storage/sqlite-settings.addon.js +6 -170
- package/dist/builtins/sqlite-storage/sqlite-settings.addon.mjs +6 -170
- package/dist/builtins/sqlite-storage/storage-admin-provider.d.ts +3 -17
- package/dist/builtins/storage-orchestrator/storage-orchestrator.addon.js +1 -1
- package/dist/builtins/storage-orchestrator/storage-orchestrator.addon.mjs +1 -1
- package/dist/builtins/system-config/system-config.addon.js +3 -3
- package/dist/builtins/system-config/system-config.addon.mjs +280 -280
- package/dist/builtins/winston-logging/index.js +1 -1
- package/dist/builtins/winston-logging/index.mjs +1 -1
- package/dist/{composition-sources-b0e_2I-D.js → composition-sources-BPnpB9XG.js} +1 -1
- package/dist/{composition-sources-DNRZWR1j.mjs → composition-sources-BlcYzFwY.mjs} +1 -1
- package/dist/{constants-6OeJiZgi.js → constants-7ZMxWr8Z.js} +1 -1
- package/dist/{constants-BnkOdj9Q.mjs → constants-CH5UXe_G.mjs} +1 -1
- package/dist/{custom-action-registry-DStvrQKI.mjs → custom-action-registry-DZSCLBuT.mjs} +2 -0
- package/dist/{custom-action-registry-COyb49zm.js → custom-action-registry-MRyes5j0.js} +2 -0
- package/dist/{device-ca-Bnkw8GyU.mjs → device-ca-BU_gnd-q.mjs} +1 -1
- package/dist/{device-ca-CPTDs--d.js → device-ca-CUEx7sti.js} +1 -1
- package/dist/{dist-Wy8cTxg6.mjs → dist-8tWIDoWp.mjs} +15 -55
- package/dist/{dist-Bn-RNanh.js → dist-Ct-4tTeX.js} +14 -66
- package/dist/file-host-worker.js +1 -1
- package/dist/file-host-worker.mjs +1 -1
- package/dist/{hub-mdns-ciao-CJu0nhla.mjs → hub-mdns-ciao-Cuy7LUdz.mjs} +2 -2
- package/dist/{hub-mdns-ciao-CTBTlia4.js → hub-mdns-ciao-DJ-Amqd7.js} +2 -2
- package/dist/index.d.ts +1 -1
- package/dist/index.js +89 -241
- package/dist/index.mjs +90 -237
- package/dist/kernel/index.d.ts +0 -1
- package/dist/kernel/moleculer/runner-addon-settings.d.ts +1 -6
- package/dist/kernel/moleculer/runner-device-settings.d.ts +2 -2
- package/dist/{restore-archive-CM23Pq9B.mjs → restore-archive-BDua3jO6.mjs} +78 -78
- package/dist/{restore-archive-DM1OBYq4.js → restore-archive-HwOA83v3.js} +1 -1
- package/dist/{retired-settings-keys-D_VwZiDt.js → retired-settings-keys-CGUqX-Oz.js} +0 -6
- package/dist/{retired-settings-keys-Bhwe4ymb.mjs → retired-settings-keys-C_Kjp-Bj.mjs} +1 -1
- package/dist/{wal-maintenance-Bj_MmDkr.mjs → wal-maintenance-BvzVehem.mjs} +0 -0
- package/dist/{wal-maintenance-CaJjuLHq.js → wal-maintenance-DBvuVU9s.js} +0 -0
- package/package.json +1 -1
- package/dist/builtins/sqlite-storage/retired-collections.d.ts +0 -146
- package/dist/kernel/legacy-blob-guard.d.ts +0 -63
|
@@ -3,9 +3,9 @@ Object.defineProperties(exports, {
|
|
|
3
3
|
[Symbol.toStringTag]: { value: "Module" }
|
|
4
4
|
});
|
|
5
5
|
const require_chunk = require("../../chunk-Cek0wNdY.js");
|
|
6
|
-
const require_dist = require("../../dist-
|
|
6
|
+
const require_dist = require("../../dist-Ct-4tTeX.js");
|
|
7
7
|
const require_read_all_rows$1 = require("../../read-all-rows-CVoO1ONM.js");
|
|
8
|
-
const require_restore_archive = require("../../restore-archive-
|
|
8
|
+
const require_restore_archive = require("../../restore-archive-HwOA83v3.js");
|
|
9
9
|
let node_crypto = require("node:crypto");
|
|
10
10
|
let node_fs_promises = require("node:fs/promises");
|
|
11
11
|
node_fs_promises = require_chunk.__toESM(node_fs_promises);
|
|
@@ -87,75 +87,26 @@ async function seedWeeklySchedule(store, markers) {
|
|
|
87
87
|
//#endregion
|
|
88
88
|
//#region src/builtins/backup-orchestrator/seed-marker-store.ts
|
|
89
89
|
/**
|
|
90
|
-
*
|
|
91
|
-
*
|
|
92
|
-
* seed is created (or found already created); never updated"
|
|
93
|
-
* retention="none — one tiny row per seed ever made; deleting it lets the
|
|
94
|
-
* seed be re-created once more on the next boot"
|
|
90
|
+
* One `seed-markers` row per seed id (D902: no `backup_seed_markers` table).
|
|
91
|
+
* Written once, never updated; deleting one lets that seed run once more.
|
|
95
92
|
*/
|
|
96
|
-
var BACKUP_SEED_MARKERS_COLLECTION = "backup_seed_markers";
|
|
97
|
-
var COLUMNS$1 = [{
|
|
98
|
-
name: "seed_id",
|
|
99
|
-
type: "TEXT",
|
|
100
|
-
primaryKey: true
|
|
101
|
-
}, {
|
|
102
|
-
name: "seeded_at",
|
|
103
|
-
type: "INTEGER",
|
|
104
|
-
notNull: true
|
|
105
|
-
}];
|
|
106
93
|
var BackupSeedMarkerStore = class {
|
|
107
|
-
store;
|
|
108
|
-
now;
|
|
109
|
-
declared = false;
|
|
110
94
|
markersKind;
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
this.now = now;
|
|
95
|
+
now;
|
|
96
|
+
constructor(markersKind, now = Date.now) {
|
|
114
97
|
this.markersKind = markersKind;
|
|
115
|
-
|
|
116
|
-
async ensureDeclared() {
|
|
117
|
-
if (this.markersKind !== null) return;
|
|
118
|
-
if (this.declared) return;
|
|
119
|
-
await this.store.declareCollection({
|
|
120
|
-
collection: BACKUP_SEED_MARKERS_COLLECTION,
|
|
121
|
-
columns: COLUMNS$1
|
|
122
|
-
});
|
|
123
|
-
this.declared = true;
|
|
98
|
+
this.now = now;
|
|
124
99
|
}
|
|
125
100
|
async has(seedId) {
|
|
126
|
-
await this.
|
|
127
|
-
const kind = this.markersKind;
|
|
128
|
-
if (kind !== null) return await kind.get(seedId) !== null;
|
|
129
|
-
return (await this.store.query({
|
|
130
|
-
collection: BACKUP_SEED_MARKERS_COLLECTION,
|
|
131
|
-
filter: {
|
|
132
|
-
where: { seed_id: seedId },
|
|
133
|
-
limit: 1
|
|
134
|
-
}
|
|
135
|
-
})).length > 0;
|
|
101
|
+
return await this.markersKind.get(seedId) !== null;
|
|
136
102
|
}
|
|
137
103
|
async mark(seedId) {
|
|
138
|
-
await this.ensureDeclared();
|
|
139
104
|
if (await this.has(seedId)) return;
|
|
140
|
-
const
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
id: seedId,
|
|
146
|
-
data
|
|
147
|
-
});
|
|
148
|
-
return;
|
|
149
|
-
}
|
|
150
|
-
await this.store.insert({
|
|
151
|
-
collection: BACKUP_SEED_MARKERS_COLLECTION,
|
|
152
|
-
record: {
|
|
153
|
-
id: seedId,
|
|
154
|
-
data: {
|
|
155
|
-
seed_id: seedId,
|
|
156
|
-
seeded_at: this.now()
|
|
157
|
-
}
|
|
158
|
-
}
|
|
105
|
+
const body = { seededAt: this.now() };
|
|
106
|
+
const data = require_restore_archive.backupOrchestratorRecordKinds["seed-markers"].schema.parse(body);
|
|
107
|
+
await this.markersKind.put({
|
|
108
|
+
id: seedId,
|
|
109
|
+
data
|
|
159
110
|
});
|
|
160
111
|
}
|
|
161
112
|
};
|
|
@@ -955,165 +906,35 @@ function previewNextRuns(cron, count, after) {
|
|
|
955
906
|
//#endregion
|
|
956
907
|
//#region src/builtins/backup-orchestrator/destination-policy.ts
|
|
957
908
|
var import_read_all_rows = require_read_all_rows$1.require_read_all_rows();
|
|
958
|
-
/**
|
|
959
|
-
* @durable class=config owner=backup-orchestrator
|
|
960
|
-
* write="`ensureDefault` auto-creates one row the first time a `backups` storage
|
|
961
|
-
* location is listed (enabled, retentionCount 7, no cadence of its own), an operator
|
|
962
|
-
* edit rewrites it (`upsert` = delete + insert, carrying `last_run_at` forward so a
|
|
963
|
-
* label change does not reset the baseline), and `recordAttempt` stamps
|
|
964
|
-
* `last_run_at` + `last_error` on every attempted write to the destination"
|
|
965
|
-
* retention="none — `delete(locationId)` exists but NOTHING calls it, so a policy row
|
|
966
|
-
* outlives the `StorageLocation` it names and the table only ever grows, by one row
|
|
967
|
-
* per backups location ever seen. Losing a row is recoverable: the next
|
|
968
|
-
* `listDestinations` pass re-creates it from the defaults, which silently discards
|
|
969
|
-
* the operator's own retention count. The legacy `cron` column is written NULL and
|
|
970
|
-
* never read — it scheduled nothing after 2026-07-28 and reading it made a
|
|
971
|
-
* destination advertise a nightly run nobody performed (D384)."
|
|
972
|
-
*/
|
|
973
|
-
var BACKUP_DESTINATION_POLICIES = "backup_destination_policies";
|
|
974
|
-
var BACKUP_DESTINATION_POLICIES_COLUMNS = [
|
|
975
|
-
{
|
|
976
|
-
name: "location_id",
|
|
977
|
-
type: "TEXT",
|
|
978
|
-
primaryKey: true
|
|
979
|
-
},
|
|
980
|
-
{
|
|
981
|
-
name: "enabled",
|
|
982
|
-
type: "INTEGER",
|
|
983
|
-
notNull: true,
|
|
984
|
-
defaultValue: 1
|
|
985
|
-
},
|
|
986
|
-
{
|
|
987
|
-
name: "retention_count",
|
|
988
|
-
type: "INTEGER",
|
|
989
|
-
notNull: true,
|
|
990
|
-
defaultValue: 7
|
|
991
|
-
},
|
|
992
|
-
{
|
|
993
|
-
name: "label",
|
|
994
|
-
type: "TEXT"
|
|
995
|
-
},
|
|
996
|
-
{
|
|
997
|
-
name: "cron",
|
|
998
|
-
type: "TEXT"
|
|
999
|
-
},
|
|
1000
|
-
{
|
|
1001
|
-
name: "last_run_at",
|
|
1002
|
-
type: "INTEGER"
|
|
1003
|
-
},
|
|
1004
|
-
{
|
|
1005
|
-
name: "last_error",
|
|
1006
|
-
type: "TEXT"
|
|
1007
|
-
}
|
|
1008
|
-
];
|
|
1009
909
|
var BackupDestinationPolicyService = class {
|
|
1010
|
-
store;
|
|
1011
|
-
declared = false;
|
|
1012
910
|
policiesKind;
|
|
1013
|
-
constructor(
|
|
1014
|
-
this.store = store;
|
|
911
|
+
constructor(policiesKind) {
|
|
1015
912
|
this.policiesKind = policiesKind;
|
|
1016
913
|
}
|
|
1017
|
-
/**
|
|
1018
|
-
* Lazy `declareCollection` — called from every read/write path.
|
|
1019
|
-
* `declareCollection` is idempotent, so repeated calls collapse to a no-op
|
|
1020
|
-
* after the first. This keeps the constructor synchronous and avoids a
|
|
1021
|
-
* separate `initialize()` step that callers could forget.
|
|
1022
|
-
*/
|
|
1023
|
-
async initialize() {
|
|
1024
|
-
await this.ensureDeclared();
|
|
1025
|
-
}
|
|
1026
|
-
async ensureDeclared() {
|
|
1027
|
-
if (this.policiesKind !== null) return;
|
|
1028
|
-
if (this.declared) return;
|
|
1029
|
-
await this.store.declareCollection({
|
|
1030
|
-
collection: BACKUP_DESTINATION_POLICIES,
|
|
1031
|
-
columns: BACKUP_DESTINATION_POLICIES_COLUMNS
|
|
1032
|
-
});
|
|
1033
|
-
this.declared = true;
|
|
1034
|
-
}
|
|
1035
914
|
async list() {
|
|
1036
|
-
await this.
|
|
1037
|
-
|
|
1038
|
-
if (kind !== null) {
|
|
1039
|
-
const rows = await (0, import_read_all_rows.readAllRows)(kind);
|
|
1040
|
-
rows.sort((a, b) => a.id.localeCompare(b.id));
|
|
1041
|
-
const out = [];
|
|
1042
|
-
for (const row of rows) {
|
|
1043
|
-
const mapped = bodyToPolicy(row.id, row.data);
|
|
1044
|
-
if (mapped) out.push(mapped);
|
|
1045
|
-
}
|
|
1046
|
-
return out;
|
|
1047
|
-
}
|
|
1048
|
-
const rows = await this.store.query({
|
|
1049
|
-
collection: BACKUP_DESTINATION_POLICIES,
|
|
1050
|
-
filter: { orderBy: {
|
|
1051
|
-
field: "location_id",
|
|
1052
|
-
direction: "asc"
|
|
1053
|
-
} }
|
|
1054
|
-
});
|
|
915
|
+
const rows = await (0, import_read_all_rows.readAllRows)(this.policiesKind);
|
|
916
|
+
rows.sort((a, b) => a.id.localeCompare(b.id));
|
|
1055
917
|
const out = [];
|
|
1056
918
|
for (const row of rows) {
|
|
1057
|
-
const mapped =
|
|
919
|
+
const mapped = bodyToPolicy(row.id, row.data);
|
|
1058
920
|
if (mapped) out.push(mapped);
|
|
1059
921
|
}
|
|
1060
922
|
return out;
|
|
1061
923
|
}
|
|
1062
924
|
async get(locationId) {
|
|
1063
|
-
await this.
|
|
1064
|
-
|
|
1065
|
-
if (kind !== null) {
|
|
1066
|
-
const row = await kind.get(locationId);
|
|
1067
|
-
return row === null ? null : bodyToPolicy(locationId, row.data);
|
|
1068
|
-
}
|
|
1069
|
-
const rows = await this.store.query({
|
|
1070
|
-
collection: BACKUP_DESTINATION_POLICIES,
|
|
1071
|
-
filter: {
|
|
1072
|
-
where: { location_id: locationId },
|
|
1073
|
-
limit: 1
|
|
1074
|
-
}
|
|
1075
|
-
});
|
|
1076
|
-
return rows[0] ? mapRowToPolicy(rows[0].data) : null;
|
|
925
|
+
const row = await this.policiesKind.get(locationId);
|
|
926
|
+
return row === null ? null : bodyToPolicy(locationId, row.data);
|
|
1077
927
|
}
|
|
1078
|
-
/**
|
|
1079
|
-
* Replace-or-insert. Implemented as delete + insert, mirroring the
|
|
1080
|
-
* `SqliteLocationStore` contract.
|
|
1081
|
-
*/
|
|
928
|
+
/** Replace-or-insert: one full `put` of the row. */
|
|
1082
929
|
async upsert(p) {
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
const data = require_restore_archive.backupOrchestratorRecordKinds["destination-policies"].schema.parse(body);
|
|
1088
|
-
await kind.put({
|
|
1089
|
-
id: p.locationId,
|
|
1090
|
-
data
|
|
1091
|
-
});
|
|
1092
|
-
return;
|
|
1093
|
-
}
|
|
1094
|
-
await this.store.deleteWhere({
|
|
1095
|
-
collection: BACKUP_DESTINATION_POLICIES,
|
|
1096
|
-
filter: { where: { location_id: p.locationId } }
|
|
1097
|
-
});
|
|
1098
|
-
await this.store.insert({
|
|
1099
|
-
collection: BACKUP_DESTINATION_POLICIES,
|
|
1100
|
-
record: {
|
|
1101
|
-
id: p.locationId,
|
|
1102
|
-
data: mapPolicyToRow(p)
|
|
1103
|
-
}
|
|
930
|
+
const data = require_restore_archive.backupOrchestratorRecordKinds["destination-policies"].schema.parse(policyToBody(p));
|
|
931
|
+
await this.policiesKind.put({
|
|
932
|
+
id: p.locationId,
|
|
933
|
+
data
|
|
1104
934
|
});
|
|
1105
935
|
}
|
|
1106
936
|
async delete(locationId) {
|
|
1107
|
-
await this.
|
|
1108
|
-
const kind = this.policiesKind;
|
|
1109
|
-
if (kind !== null) {
|
|
1110
|
-
await kind.delete(locationId);
|
|
1111
|
-
return;
|
|
1112
|
-
}
|
|
1113
|
-
await this.store.deleteWhere({
|
|
1114
|
-
collection: BACKUP_DESTINATION_POLICIES,
|
|
1115
|
-
filter: { where: { location_id: locationId } }
|
|
1116
|
-
});
|
|
937
|
+
await this.policiesKind.delete(locationId);
|
|
1117
938
|
}
|
|
1118
939
|
/**
|
|
1119
940
|
* Auto-create a default policy for a `backups` location seen for the
|
|
@@ -1181,41 +1002,6 @@ function bodyToPolicy(locationId, body) {
|
|
|
1181
1002
|
...body.lastError !== void 0 ? { lastError: body.lastError } : {}
|
|
1182
1003
|
};
|
|
1183
1004
|
}
|
|
1184
|
-
function mapPolicyToRow(p) {
|
|
1185
|
-
return {
|
|
1186
|
-
location_id: p.locationId,
|
|
1187
|
-
enabled: p.enabled ? 1 : 0,
|
|
1188
|
-
retention_count: p.retentionCount,
|
|
1189
|
-
label: p.label ?? null,
|
|
1190
|
-
cron: null,
|
|
1191
|
-
last_run_at: p.lastAttemptAt ?? null,
|
|
1192
|
-
last_error: p.lastError ?? null
|
|
1193
|
-
};
|
|
1194
|
-
}
|
|
1195
|
-
/**
|
|
1196
|
-
* Decode a row pulled from the structured table. Defensive against
|
|
1197
|
-
* schema drift / partial migrations: returns `null` on missing
|
|
1198
|
-
* `location_id` so corrupt rows skip the result list rather than
|
|
1199
|
-
* throwing the whole orchestrator down at boot.
|
|
1200
|
-
*/
|
|
1201
|
-
function mapRowToPolicy(row) {
|
|
1202
|
-
const locationId = String(row["location_id"] ?? "");
|
|
1203
|
-
if (!locationId) return null;
|
|
1204
|
-
const labelRaw = row["label"];
|
|
1205
|
-
const label = typeof labelRaw === "string" && labelRaw.length > 0 ? labelRaw : void 0;
|
|
1206
|
-
const lastRunRaw = row["last_run_at"];
|
|
1207
|
-
const lastAttemptAt = typeof lastRunRaw === "number" && lastRunRaw > 0 ? lastRunRaw : void 0;
|
|
1208
|
-
const lastErrorRaw = row["last_error"];
|
|
1209
|
-
const lastError = typeof lastErrorRaw === "string" && lastErrorRaw.length > 0 ? lastErrorRaw : void 0;
|
|
1210
|
-
return {
|
|
1211
|
-
locationId,
|
|
1212
|
-
enabled: row["enabled"] === 1 || row["enabled"] === true,
|
|
1213
|
-
retentionCount: Number(row["retention_count"] ?? 0),
|
|
1214
|
-
...label !== void 0 ? { label } : {},
|
|
1215
|
-
...lastAttemptAt !== void 0 ? { lastAttemptAt } : {},
|
|
1216
|
-
...lastError !== void 0 ? { lastError } : {}
|
|
1217
|
-
};
|
|
1218
|
-
}
|
|
1219
1005
|
//#endregion
|
|
1220
1006
|
//#region src/builtins/backup-orchestrator/download-helpers.ts
|
|
1221
1007
|
/**
|
|
@@ -1908,176 +1694,35 @@ function requestSignature(request) {
|
|
|
1908
1694
|
}
|
|
1909
1695
|
//#endregion
|
|
1910
1696
|
//#region src/builtins/backup-orchestrator/schedule-store.ts
|
|
1911
|
-
/**
|
|
1912
|
-
* @durable class=config owner=backup-orchestrator
|
|
1913
|
-
* write="an operator creating or editing a schedule (`upsert`, implemented as
|
|
1914
|
-
* delete + insert: cron, enabled, the fan-out `location_ids`, `retention_count`,
|
|
1915
|
-
* optional `data_sources`), plus a `last_run_at` stamp the orchestrator writes on
|
|
1916
|
-
* every due tick BEFORE the archive runs, so an upload outliving the tick cannot
|
|
1917
|
-
* fire the same schedule twice, and a `last_success_at` / `last_error` pair it
|
|
1918
|
-
* writes AFTER the run resolves"
|
|
1919
|
-
* retention="only the operator's `delete(id)` — no sweep, no expiry, and `retention_count`
|
|
1920
|
-
* bounds the ARCHIVES at the destination, never these rows. Losing the table stops
|
|
1921
|
-
* every scheduled backup silently: no cron remains to fire, and the fan-out sets and
|
|
1922
|
-
* retention counts are gone with it."
|
|
1923
|
-
*/
|
|
1924
|
-
var BACKUP_SCHEDULES = "backup_schedules";
|
|
1925
|
-
var BACKUP_SCHEDULES_COLUMNS = [
|
|
1926
|
-
{
|
|
1927
|
-
name: "id",
|
|
1928
|
-
type: "TEXT",
|
|
1929
|
-
primaryKey: true
|
|
1930
|
-
},
|
|
1931
|
-
{
|
|
1932
|
-
name: "label",
|
|
1933
|
-
type: "TEXT",
|
|
1934
|
-
notNull: true,
|
|
1935
|
-
defaultValue: ""
|
|
1936
|
-
},
|
|
1937
|
-
{
|
|
1938
|
-
name: "cron",
|
|
1939
|
-
type: "TEXT",
|
|
1940
|
-
notNull: true,
|
|
1941
|
-
defaultValue: ""
|
|
1942
|
-
},
|
|
1943
|
-
{
|
|
1944
|
-
name: "enabled",
|
|
1945
|
-
type: "INTEGER",
|
|
1946
|
-
notNull: true,
|
|
1947
|
-
defaultValue: 1
|
|
1948
|
-
},
|
|
1949
|
-
{
|
|
1950
|
-
name: "location_ids",
|
|
1951
|
-
type: "JSON",
|
|
1952
|
-
notNull: true,
|
|
1953
|
-
defaultValue: "[]"
|
|
1954
|
-
},
|
|
1955
|
-
{
|
|
1956
|
-
name: "retention_count",
|
|
1957
|
-
type: "INTEGER",
|
|
1958
|
-
notNull: true,
|
|
1959
|
-
defaultValue: 7
|
|
1960
|
-
},
|
|
1961
|
-
{
|
|
1962
|
-
name: "data_sources",
|
|
1963
|
-
type: "JSON"
|
|
1964
|
-
},
|
|
1965
|
-
{
|
|
1966
|
-
name: "databases",
|
|
1967
|
-
type: "JSON"
|
|
1968
|
-
},
|
|
1969
|
-
{
|
|
1970
|
-
name: "last_run_at",
|
|
1971
|
-
type: "INTEGER"
|
|
1972
|
-
},
|
|
1973
|
-
{
|
|
1974
|
-
name: "last_success_at",
|
|
1975
|
-
type: "INTEGER"
|
|
1976
|
-
},
|
|
1977
|
-
{
|
|
1978
|
-
name: "last_error",
|
|
1979
|
-
type: "TEXT"
|
|
1980
|
-
}
|
|
1981
|
-
];
|
|
1982
1697
|
var BackupScheduleService = class {
|
|
1983
|
-
store;
|
|
1984
|
-
declared = false;
|
|
1985
1698
|
schedulesKind;
|
|
1986
|
-
constructor(
|
|
1987
|
-
this.store = store;
|
|
1699
|
+
constructor(schedulesKind) {
|
|
1988
1700
|
this.schedulesKind = schedulesKind;
|
|
1989
1701
|
}
|
|
1990
|
-
async initialize() {
|
|
1991
|
-
await this.ensureDeclared();
|
|
1992
|
-
}
|
|
1993
|
-
async ensureDeclared() {
|
|
1994
|
-
if (this.schedulesKind !== null) return;
|
|
1995
|
-
if (this.declared) return;
|
|
1996
|
-
await this.store.declareCollection({
|
|
1997
|
-
collection: BACKUP_SCHEDULES,
|
|
1998
|
-
columns: BACKUP_SCHEDULES_COLUMNS
|
|
1999
|
-
});
|
|
2000
|
-
this.declared = true;
|
|
2001
|
-
}
|
|
2002
1702
|
async list() {
|
|
2003
|
-
await this.
|
|
2004
|
-
|
|
2005
|
-
if (kind !== null) {
|
|
2006
|
-
const rows = await (0, import_read_all_rows.readAllRows)(kind);
|
|
2007
|
-
rows.sort((a, b) => a.id.localeCompare(b.id));
|
|
2008
|
-
const out = [];
|
|
2009
|
-
for (const row of rows) {
|
|
2010
|
-
const mapped = bodyToSchedule(row.id, row.data);
|
|
2011
|
-
if (mapped) out.push(mapped);
|
|
2012
|
-
}
|
|
2013
|
-
return out;
|
|
2014
|
-
}
|
|
2015
|
-
const rows = await this.store.query({
|
|
2016
|
-
collection: BACKUP_SCHEDULES,
|
|
2017
|
-
filter: { orderBy: {
|
|
2018
|
-
field: "id",
|
|
2019
|
-
direction: "asc"
|
|
2020
|
-
} }
|
|
2021
|
-
});
|
|
1703
|
+
const rows = await (0, import_read_all_rows.readAllRows)(this.schedulesKind);
|
|
1704
|
+
rows.sort((a, b) => a.id.localeCompare(b.id));
|
|
2022
1705
|
const out = [];
|
|
2023
1706
|
for (const row of rows) {
|
|
2024
|
-
const mapped =
|
|
1707
|
+
const mapped = bodyToSchedule(row.id, row.data);
|
|
2025
1708
|
if (mapped) out.push(mapped);
|
|
2026
1709
|
}
|
|
2027
1710
|
return out;
|
|
2028
1711
|
}
|
|
2029
1712
|
async get(id) {
|
|
2030
|
-
await this.
|
|
2031
|
-
|
|
2032
|
-
if (kind !== null) {
|
|
2033
|
-
const row = await kind.get(id);
|
|
2034
|
-
return row === null ? null : bodyToSchedule(id, row.data);
|
|
2035
|
-
}
|
|
2036
|
-
const rows = await this.store.query({
|
|
2037
|
-
collection: BACKUP_SCHEDULES,
|
|
2038
|
-
filter: {
|
|
2039
|
-
where: { id },
|
|
2040
|
-
limit: 1
|
|
2041
|
-
}
|
|
2042
|
-
});
|
|
2043
|
-
return rows[0] ? mapRowToSchedule(rows[0].data) : null;
|
|
1713
|
+
const row = await this.schedulesKind.get(id);
|
|
1714
|
+
return row === null ? null : bodyToSchedule(id, row.data);
|
|
2044
1715
|
}
|
|
2045
|
-
/** Replace-or-insert
|
|
1716
|
+
/** Replace-or-insert: one full `put` of the row. */
|
|
2046
1717
|
async upsert(s) {
|
|
2047
|
-
|
|
2048
|
-
|
|
2049
|
-
|
|
2050
|
-
|
|
2051
|
-
const data = require_restore_archive.backupOrchestratorRecordKinds.schedules.schema.parse(body);
|
|
2052
|
-
await kind.put({
|
|
2053
|
-
id: s.id,
|
|
2054
|
-
data
|
|
2055
|
-
});
|
|
2056
|
-
return;
|
|
2057
|
-
}
|
|
2058
|
-
await this.store.deleteWhere({
|
|
2059
|
-
collection: BACKUP_SCHEDULES,
|
|
2060
|
-
filter: { where: { id: s.id } }
|
|
2061
|
-
});
|
|
2062
|
-
await this.store.insert({
|
|
2063
|
-
collection: BACKUP_SCHEDULES,
|
|
2064
|
-
record: {
|
|
2065
|
-
id: s.id,
|
|
2066
|
-
data: mapScheduleToRow(s)
|
|
2067
|
-
}
|
|
1718
|
+
const data = require_restore_archive.backupOrchestratorRecordKinds.schedules.schema.parse(scheduleToBody(s));
|
|
1719
|
+
await this.schedulesKind.put({
|
|
1720
|
+
id: s.id,
|
|
1721
|
+
data
|
|
2068
1722
|
});
|
|
2069
1723
|
}
|
|
2070
1724
|
async delete(id) {
|
|
2071
|
-
await this.
|
|
2072
|
-
const kind = this.schedulesKind;
|
|
2073
|
-
if (kind !== null) {
|
|
2074
|
-
await kind.delete(id);
|
|
2075
|
-
return;
|
|
2076
|
-
}
|
|
2077
|
-
await this.store.deleteWhere({
|
|
2078
|
-
collection: BACKUP_SCHEDULES,
|
|
2079
|
-
filter: { where: { id } }
|
|
2080
|
-
});
|
|
1725
|
+
await this.schedulesKind.delete(id);
|
|
2081
1726
|
}
|
|
2082
1727
|
};
|
|
2083
1728
|
function scheduleToBody(s) {
|
|
@@ -2110,73 +1755,6 @@ function bodyToSchedule(id, body) {
|
|
|
2110
1755
|
...body.lastError !== void 0 ? { lastError: body.lastError } : {}
|
|
2111
1756
|
};
|
|
2112
1757
|
}
|
|
2113
|
-
function mapScheduleToRow(s) {
|
|
2114
|
-
return {
|
|
2115
|
-
id: s.id,
|
|
2116
|
-
label: s.label,
|
|
2117
|
-
cron: s.cron,
|
|
2118
|
-
enabled: s.enabled ? 1 : 0,
|
|
2119
|
-
location_ids: [...s.locationIds],
|
|
2120
|
-
retention_count: s.retentionCount,
|
|
2121
|
-
data_sources: s.dataSources ? [...s.dataSources] : null,
|
|
2122
|
-
databases: s.databases ? [...s.databases] : null,
|
|
2123
|
-
last_run_at: s.lastAttemptAt ?? null,
|
|
2124
|
-
last_success_at: s.lastSuccessAt ?? null,
|
|
2125
|
-
last_error: s.lastError ?? null
|
|
2126
|
-
};
|
|
2127
|
-
}
|
|
2128
|
-
/**
|
|
2129
|
-
* `location_ids` / `data_sources` are `JSON` columns, so the surface hands
|
|
2130
|
-
* back real arrays. A STRING still arrives for rows an older build wrote and
|
|
2131
|
-
* nothing has rewritten since — a schedule whose fan-out set silently read as
|
|
2132
|
-
* empty would back up to nowhere, so both shapes are accepted.
|
|
2133
|
-
*/
|
|
2134
|
-
function parseStringArray(raw) {
|
|
2135
|
-
const value = typeof raw === "string" ? tryParseJson(raw) : raw;
|
|
2136
|
-
if (!Array.isArray(value)) return [];
|
|
2137
|
-
return value.filter((v) => typeof v === "string");
|
|
2138
|
-
}
|
|
2139
|
-
/** Known database ids only: an unknown name in a stored row is dropped, not carried. */
|
|
2140
|
-
function parseDatabaseIds(raw) {
|
|
2141
|
-
const out = [];
|
|
2142
|
-
for (const id of parseStringArray(raw)) if (id === "settings" || id === "events" || id === "history") out.push(id);
|
|
2143
|
-
return out;
|
|
2144
|
-
}
|
|
2145
|
-
function tryParseJson(raw) {
|
|
2146
|
-
if (raw.length === 0) return null;
|
|
2147
|
-
try {
|
|
2148
|
-
return JSON.parse(raw);
|
|
2149
|
-
} catch {
|
|
2150
|
-
return null;
|
|
2151
|
-
}
|
|
2152
|
-
}
|
|
2153
|
-
function mapRowToSchedule(row) {
|
|
2154
|
-
const id = String(row["id"] ?? "");
|
|
2155
|
-
if (!id) return null;
|
|
2156
|
-
const dataSourcesRaw = row["data_sources"];
|
|
2157
|
-
const dataSources = dataSourcesRaw === null || dataSourcesRaw === void 0 ? void 0 : parseStringArray(dataSourcesRaw);
|
|
2158
|
-
const databasesRaw = row["databases"];
|
|
2159
|
-
const databases = databasesRaw === null || databasesRaw === void 0 ? void 0 : parseDatabaseIds(databasesRaw);
|
|
2160
|
-
const lastRunRaw = row["last_run_at"];
|
|
2161
|
-
const lastAttemptAt = typeof lastRunRaw === "number" && lastRunRaw > 0 ? lastRunRaw : void 0;
|
|
2162
|
-
const lastSuccessRaw = row["last_success_at"];
|
|
2163
|
-
const lastSuccessAt = typeof lastSuccessRaw === "number" && lastSuccessRaw > 0 ? lastSuccessRaw : void 0;
|
|
2164
|
-
const lastErrorRaw = row["last_error"];
|
|
2165
|
-
const lastError = typeof lastErrorRaw === "string" && lastErrorRaw.length > 0 ? lastErrorRaw : void 0;
|
|
2166
|
-
return {
|
|
2167
|
-
id,
|
|
2168
|
-
label: typeof row["label"] === "string" ? String(row["label"]) : "",
|
|
2169
|
-
cron: typeof row["cron"] === "string" ? String(row["cron"]) : "",
|
|
2170
|
-
enabled: row["enabled"] === 1 || row["enabled"] === true,
|
|
2171
|
-
locationIds: parseStringArray(row["location_ids"]),
|
|
2172
|
-
retentionCount: Number(row["retention_count"] ?? 7),
|
|
2173
|
-
...dataSources !== void 0 ? { dataSources } : {},
|
|
2174
|
-
...databases !== void 0 && databases.length > 0 ? { databases } : {},
|
|
2175
|
-
...lastAttemptAt !== void 0 ? { lastAttemptAt } : {},
|
|
2176
|
-
...lastSuccessAt !== void 0 ? { lastSuccessAt } : {},
|
|
2177
|
-
...lastError !== void 0 ? { lastError } : {}
|
|
2178
|
-
};
|
|
2179
|
-
}
|
|
2180
1758
|
//#endregion
|
|
2181
1759
|
//#region src/builtins/backup-orchestrator/staging-paths.ts
|
|
2182
1760
|
/** Basename of the staging dir under the node data root. */
|
|
@@ -2308,8 +1886,8 @@ function selectBackupTargets(input) {
|
|
|
2308
1886
|
* Responsibilities:
|
|
2309
1887
|
* - Schedule (cron-like timer at top of hour, daily/weekly cadence)
|
|
2310
1888
|
* - Per-destination policy (enable/disable, retention count, label)
|
|
2311
|
-
* persisted in `
|
|
2312
|
-
* the
|
|
1889
|
+
* persisted in the `destination-policies` records kind (this builtin
|
|
1890
|
+
* owns the kind).
|
|
2313
1891
|
* - Archive build (one tar.gz with embedded manifest, fanned out to
|
|
2314
1892
|
* every selected `backups`-typed `StorageLocation` via the
|
|
2315
1893
|
* consumer-facing `storage` cap's chunked upload protocol).
|
|
@@ -2320,7 +1898,7 @@ function selectBackupTargets(input) {
|
|
|
2320
1898
|
* Storage routing: destinations are
|
|
2321
1899
|
* `api.storage.listLocations({ type: 'backups' })` — every operator-
|
|
2322
1900
|
* visible `backups` location IS a destination. The orchestrator joins
|
|
2323
|
-
* that list with the `
|
|
1901
|
+
* that list with the `destination-policies` records to surface
|
|
2324
1902
|
* `enabled` / `retentionCount` / `label`. The actual archive bytes
|
|
2325
1903
|
* travel through `api.storage.beginUpload` / `writeChunk` /
|
|
2326
1904
|
* `finalizeUpload`, with the staging archive built into
|
|
@@ -2416,19 +1994,11 @@ var BackupOrchestratorAddon = class extends require_dist.BaseAddon {
|
|
|
2416
1994
|
this.systemBackup = new require_restore_archive.SystemBackupService(this.dataDir, this.ctx.logger);
|
|
2417
1995
|
this.runQueue = new BackupRunQueue((request, signal, progress) => this.runBackupBuild(request, signal, progress));
|
|
2418
1996
|
await this.cleanStagingArea();
|
|
2419
|
-
|
|
2420
|
-
if (this.policies
|
|
2421
|
-
|
|
2422
|
-
|
|
2423
|
-
this.
|
|
2424
|
-
this.policies = null;
|
|
2425
|
-
}
|
|
2426
|
-
this.schedules = this.resolveScheduleService();
|
|
2427
|
-
if (this.schedules) try {
|
|
2428
|
-
await this.schedules.initialize();
|
|
2429
|
-
} catch (err) {
|
|
2430
|
-
this.ctx.logger.error("backup-orchestrator: schedule table init failed", { meta: { error: err instanceof Error ? err.message : String(err) } });
|
|
2431
|
-
this.schedules = null;
|
|
1997
|
+
const backupRecords = this.backupRecords;
|
|
1998
|
+
if (backupRecords === null) this.ctx.logger.warn("backup-orchestrator: no ctx.records — schedules and destination policies will not persist");
|
|
1999
|
+
else {
|
|
2000
|
+
this.policies = new BackupDestinationPolicyService(backupRecords.destinationPolicies);
|
|
2001
|
+
this.schedules = new BackupScheduleService(backupRecords.schedules);
|
|
2432
2002
|
}
|
|
2433
2003
|
await this.seedWeekly();
|
|
2434
2004
|
const settingsStore = this.ctx.kernel.capabilityRegistry?.getSingleton("settings-store");
|
|
@@ -2542,7 +2112,7 @@ var BackupOrchestratorAddon = class extends require_dist.BaseAddon {
|
|
|
2542
2112
|
* lazily seeding a default for any `locationId` we haven't seen
|
|
2543
2113
|
* before. Idempotent — `ensureDefault` no-ops on already-persisted
|
|
2544
2114
|
* rows. Falls back to the in-memory default policy when the policy
|
|
2545
|
-
* service isn't wired (no
|
|
2115
|
+
* service isn't wired (no `ctx.records`).
|
|
2546
2116
|
*/
|
|
2547
2117
|
async loadPoliciesIndexed(locationIds) {
|
|
2548
2118
|
const out = /* @__PURE__ */ new Map();
|
|
@@ -3048,7 +2618,7 @@ var BackupOrchestratorAddon = class extends require_dist.BaseAddon {
|
|
|
3048
2618
|
*/
|
|
3049
2619
|
async upsertDestinationPolicy(input) {
|
|
3050
2620
|
if (!this.policies) {
|
|
3051
|
-
this.ctx.logger.warn("backup upsertDestinationPolicy: no
|
|
2621
|
+
this.ctx.logger.warn("backup upsertDestinationPolicy: no ctx.records — change will not persist", { meta: { locationId: input.locationId } });
|
|
3052
2622
|
return;
|
|
3053
2623
|
}
|
|
3054
2624
|
const existing = await this.policies.get(input.locationId).catch(() => null);
|
|
@@ -3431,11 +3001,10 @@ var BackupOrchestratorAddon = class extends require_dist.BaseAddon {
|
|
|
3431
3001
|
* anything the operator has: see `planWeeklySeed`.
|
|
3432
3002
|
*/
|
|
3433
3003
|
async seedWeekly() {
|
|
3434
|
-
|
|
3004
|
+
const markers = this.backupRecords?.seedMarkers;
|
|
3005
|
+
if (!this.schedules || markers === void 0) return;
|
|
3435
3006
|
try {
|
|
3436
|
-
const
|
|
3437
|
-
if (!store) return;
|
|
3438
|
-
const seed = await seedWeeklySchedule(this.schedules, new BackupSeedMarkerStore(store, Date.now, this.backupRecords?.seedMarkers ?? null));
|
|
3007
|
+
const seed = await seedWeeklySchedule(this.schedules, new BackupSeedMarkerStore(markers));
|
|
3439
3008
|
if (seed === null) return;
|
|
3440
3009
|
this.ctx.logger.info("backup: seeded the weekly events & history schedule", { meta: {
|
|
3441
3010
|
scheduleId: seed.id,
|
|
@@ -3541,7 +3110,7 @@ var BackupOrchestratorAddon = class extends require_dist.BaseAddon {
|
|
|
3541
3110
|
});
|
|
3542
3111
|
}
|
|
3543
3112
|
async upsertSchedule(input) {
|
|
3544
|
-
if (!this.schedules) throw new Error("backup upsertSchedule: no
|
|
3113
|
+
if (!this.schedules) throw new Error("backup upsertSchedule: no ctx.records — schedule cannot be persisted");
|
|
3545
3114
|
const cron = input.cron.trim();
|
|
3546
3115
|
if (cron.length > 0 && !isValidCron(cron)) throw new Error(`Invalid cron expression: "${cron}"`);
|
|
3547
3116
|
const id = input.id && input.id.length > 0 ? input.id : (0, node_crypto.randomUUID)();
|
|
@@ -3584,26 +3153,6 @@ var BackupOrchestratorAddon = class extends require_dist.BaseAddon {
|
|
|
3584
3153
|
if (!this.schedules) return;
|
|
3585
3154
|
await this.schedules.delete(input.id);
|
|
3586
3155
|
}
|
|
3587
|
-
resolveScheduleService() {
|
|
3588
|
-
const reg = this.ctx.kernel.capabilityRegistry;
|
|
3589
|
-
if (!reg) return null;
|
|
3590
|
-
const store = reg.getSingleton("settings-store");
|
|
3591
|
-
if (!store) {
|
|
3592
|
-
this.ctx.logger.warn("backup-orchestrator: no settings-store provider — backup schedules will not persist");
|
|
3593
|
-
return null;
|
|
3594
|
-
}
|
|
3595
|
-
return new BackupScheduleService(store, this.backupRecords?.schedules ?? null);
|
|
3596
|
-
}
|
|
3597
|
-
resolvePolicyService() {
|
|
3598
|
-
const reg = this.ctx.kernel.capabilityRegistry;
|
|
3599
|
-
if (!reg) return null;
|
|
3600
|
-
const store = reg.getSingleton("settings-store");
|
|
3601
|
-
if (!store) {
|
|
3602
|
-
this.ctx.logger.warn("backup-orchestrator: no settings-store provider — destination policies will not persist");
|
|
3603
|
-
return null;
|
|
3604
|
-
}
|
|
3605
|
-
return new BackupDestinationPolicyService(store, this.backupRecords?.destinationPolicies ?? null);
|
|
3606
|
-
}
|
|
3607
3156
|
/**
|
|
3608
3157
|
* Narrow `ctx.api.storage` to the subset the manifest helpers need.
|
|
3609
3158
|
* Lets the helpers be unit-tested with a Map-backed fake without
|