@microck/canonfig 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +263 -0
- package/dist/agent/agent-resolution.errors.js +42 -0
- package/dist/agent/agent-resolution.layer.js +204 -0
- package/dist/agent/agent-resolution.service.js +2259 -0
- package/dist/agent/agent-resolution.types.js +1 -0
- package/dist/agent/controlled-executor.js +704 -0
- package/dist/agent/harness-adapters.js +85 -0
- package/dist/cli/cli.js +618 -0
- package/dist/cli/exit-codes.js +28 -0
- package/dist/cli/follower-commands.js +3 -0
- package/dist/cli/render.js +56 -0
- package/dist/cli/source-commands.js +5 -0
- package/dist/domain/brand.js +29 -0
- package/dist/domain/identity.js +31 -0
- package/dist/domain/npm-package-spec.js +186 -0
- package/dist/domain/profile.js +950 -0
- package/dist/domain/recipe-versions.js +297 -0
- package/dist/domain/resource.js +259 -0
- package/dist/domain/synchronization.js +346 -0
- package/dist/enrollment/enrollment.errors.js +43 -0
- package/dist/enrollment/enrollment.layer.js +724 -0
- package/dist/enrollment/enrollment.service.js +3 -0
- package/dist/enrollment/enrollment.types.js +59 -0
- package/dist/enrollment/follower-client.js +585 -0
- package/dist/enrollment/source-server.js +313 -0
- package/dist/machine/linux.layer.js +1183 -0
- package/dist/machine/machine-state.errors.js +52 -0
- package/dist/machine/machine-state.service.js +3 -0
- package/dist/machine/machine-state.types.js +1 -0
- package/dist/machine/macos.layer.js +470 -0
- package/dist/machine/windows.layer.js +879 -0
- package/dist/profile/discovery.js +740 -0
- package/dist/profile/profile-catalog.errors.js +50 -0
- package/dist/profile/profile-catalog.layer.js +20 -0
- package/dist/profile/profile-catalog.service.js +7 -0
- package/dist/profile/profile-codec.js +153 -0
- package/dist/profile/publication.js +298 -0
- package/dist/profile/tool-catalog.js +384 -0
- package/dist/runtime/doctor.js +306 -0
- package/dist/runtime/layers.js +706 -0
- package/dist/runtime/main.js +38 -0
- package/dist/schedule/linux-schedule.js +24 -0
- package/dist/schedule/macos-schedule.js +25 -0
- package/dist/schedule/schedule-manager.errors.js +17 -0
- package/dist/schedule/schedule-manager.layer.js +205 -0
- package/dist/schedule/schedule-manager.service.js +3 -0
- package/dist/schedule/schedule-manager.types.js +114 -0
- package/dist/schedule/windows-schedule.js +25 -0
- package/dist/state/state-repository.errors.js +55 -0
- package/dist/state/state-repository.layer.js +1507 -0
- package/dist/state/state-repository.service.js +3 -0
- package/dist/state/state-repository.types.js +1 -0
- package/dist/state/state-schema.js +298 -0
- package/dist/synchronization/config-codec.js +97 -0
- package/dist/synchronization/executor.js +700 -0
- package/dist/synchronization/follower-orchestration.js +939 -0
- package/dist/synchronization/follower-sync-config.js +81 -0
- package/dist/synchronization/npm-artifact.js +670 -0
- package/dist/synchronization/planner.js +378 -0
- package/dist/synchronization/recovery.js +397 -0
- package/dist/synchronization/resource-executors.js +1198 -0
- package/dist/synchronization/resource-plans.js +645 -0
- package/dist/synchronization/synchronization.errors.js +102 -0
- package/dist/synchronization/synchronization.layer.js +97 -0
- package/dist/synchronization/synchronization.service.js +11 -0
- package/dist/synchronization/synchronization.types.js +1 -0
- package/package.json +66 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,298 @@
|
|
|
1
|
+
import { Effect } from "effect";
|
|
2
|
+
import { SqliteMigrator } from "@effect/sql-sqlite-node";
|
|
3
|
+
import { SqlClient } from "effect/unstable/sql";
|
|
4
|
+
/**
|
|
5
|
+
* Explicit, append-only migration set. Migrations are tracked by Effect SQL,
|
|
6
|
+
* while each DDL statement remains idempotent for interrupted initialization.
|
|
7
|
+
*/
|
|
8
|
+
export const stateMigrations = SqliteMigrator.fromRecord({
|
|
9
|
+
"0001_state_repository": Effect.gen(function* () {
|
|
10
|
+
const sql = yield* SqlClient.SqlClient;
|
|
11
|
+
yield* sql `PRAGMA foreign_keys = ON`;
|
|
12
|
+
yield* sql `
|
|
13
|
+
CREATE TABLE IF NOT EXISTS source_identity (
|
|
14
|
+
singleton INTEGER PRIMARY KEY CHECK (singleton = 1),
|
|
15
|
+
key_id TEXT NOT NULL,
|
|
16
|
+
public_key_fingerprint TEXT NOT NULL
|
|
17
|
+
)
|
|
18
|
+
`;
|
|
19
|
+
yield* sql `
|
|
20
|
+
CREATE TABLE IF NOT EXISTS followers (
|
|
21
|
+
id TEXT PRIMARY KEY,
|
|
22
|
+
name TEXT NOT NULL,
|
|
23
|
+
groups_json TEXT NOT NULL,
|
|
24
|
+
revoked INTEGER NOT NULL CHECK (revoked IN (0, 1)),
|
|
25
|
+
credential_reference TEXT NOT NULL,
|
|
26
|
+
enrolled_at TEXT NOT NULL
|
|
27
|
+
)
|
|
28
|
+
`;
|
|
29
|
+
yield* sql `
|
|
30
|
+
CREATE TABLE IF NOT EXISTS profile_revisions (
|
|
31
|
+
id TEXT PRIMARY KEY,
|
|
32
|
+
profile_id TEXT NOT NULL,
|
|
33
|
+
sequence INTEGER NOT NULL,
|
|
34
|
+
canonical_bytes TEXT NOT NULL,
|
|
35
|
+
digest TEXT NOT NULL,
|
|
36
|
+
signature TEXT NOT NULL,
|
|
37
|
+
published_at TEXT NOT NULL,
|
|
38
|
+
revision_json TEXT NOT NULL,
|
|
39
|
+
UNIQUE (profile_id, sequence)
|
|
40
|
+
)
|
|
41
|
+
`;
|
|
42
|
+
yield* sql `
|
|
43
|
+
CREATE TRIGGER IF NOT EXISTS profile_revisions_immutable_update
|
|
44
|
+
BEFORE UPDATE ON profile_revisions
|
|
45
|
+
BEGIN
|
|
46
|
+
SELECT RAISE(ABORT, 'profile revisions are immutable');
|
|
47
|
+
END
|
|
48
|
+
`;
|
|
49
|
+
yield* sql `
|
|
50
|
+
CREATE TRIGGER IF NOT EXISTS profile_revisions_immutable_delete
|
|
51
|
+
BEFORE DELETE ON profile_revisions
|
|
52
|
+
BEGIN
|
|
53
|
+
SELECT RAISE(ABORT, 'profile revisions are immutable');
|
|
54
|
+
END
|
|
55
|
+
`;
|
|
56
|
+
yield* sql `
|
|
57
|
+
CREATE TABLE IF NOT EXISTS synchronization_runs (
|
|
58
|
+
id TEXT PRIMARY KEY,
|
|
59
|
+
follower_id TEXT NOT NULL REFERENCES followers(id),
|
|
60
|
+
revision_id TEXT NOT NULL REFERENCES profile_revisions(id),
|
|
61
|
+
status TEXT NOT NULL CHECK (
|
|
62
|
+
status IN (
|
|
63
|
+
'applying',
|
|
64
|
+
'Converged',
|
|
65
|
+
'HumanActionRequired',
|
|
66
|
+
'FollowerDrift',
|
|
67
|
+
'Failed',
|
|
68
|
+
'Interrupted'
|
|
69
|
+
)
|
|
70
|
+
),
|
|
71
|
+
plan_json TEXT NOT NULL,
|
|
72
|
+
started_at TEXT NOT NULL,
|
|
73
|
+
completed_at TEXT,
|
|
74
|
+
outcome_json TEXT
|
|
75
|
+
)
|
|
76
|
+
`;
|
|
77
|
+
yield* sql `
|
|
78
|
+
CREATE UNIQUE INDEX IF NOT EXISTS one_active_applying_run_per_follower
|
|
79
|
+
ON synchronization_runs(follower_id)
|
|
80
|
+
WHERE status = 'applying'
|
|
81
|
+
`;
|
|
82
|
+
yield* sql `
|
|
83
|
+
CREATE TABLE IF NOT EXISTS run_actions (
|
|
84
|
+
run_id TEXT NOT NULL REFERENCES synchronization_runs(id) ON DELETE RESTRICT,
|
|
85
|
+
action_id TEXT NOT NULL,
|
|
86
|
+
plan_ordinal INTEGER NOT NULL,
|
|
87
|
+
PRIMARY KEY (run_id, action_id),
|
|
88
|
+
UNIQUE (run_id, plan_ordinal)
|
|
89
|
+
)
|
|
90
|
+
`;
|
|
91
|
+
yield* sql `
|
|
92
|
+
CREATE TABLE IF NOT EXISTS action_journal (
|
|
93
|
+
id INTEGER PRIMARY KEY,
|
|
94
|
+
run_id TEXT NOT NULL,
|
|
95
|
+
action_id TEXT NOT NULL,
|
|
96
|
+
sequence INTEGER NOT NULL,
|
|
97
|
+
state TEXT NOT NULL CHECK (
|
|
98
|
+
state IN ('pending', 'running', 'succeeded', 'failed', 'skipped')
|
|
99
|
+
),
|
|
100
|
+
recorded_at TEXT NOT NULL,
|
|
101
|
+
attempt INTEGER NOT NULL CHECK (attempt >= 0),
|
|
102
|
+
verification_json TEXT,
|
|
103
|
+
rollback_reference TEXT,
|
|
104
|
+
FOREIGN KEY (run_id, action_id)
|
|
105
|
+
REFERENCES run_actions(run_id, action_id),
|
|
106
|
+
UNIQUE (run_id, sequence)
|
|
107
|
+
)
|
|
108
|
+
`;
|
|
109
|
+
yield* sql `
|
|
110
|
+
CREATE TABLE IF NOT EXISTS drift_records (
|
|
111
|
+
id INTEGER PRIMARY KEY,
|
|
112
|
+
run_id TEXT NOT NULL REFERENCES synchronization_runs(id),
|
|
113
|
+
sequence INTEGER NOT NULL,
|
|
114
|
+
conflict_json TEXT NOT NULL,
|
|
115
|
+
recorded_at TEXT NOT NULL,
|
|
116
|
+
UNIQUE (run_id, sequence)
|
|
117
|
+
)
|
|
118
|
+
`;
|
|
119
|
+
yield* sql `
|
|
120
|
+
CREATE TABLE IF NOT EXISTS applied_resources (
|
|
121
|
+
follower_id TEXT NOT NULL REFERENCES followers(id),
|
|
122
|
+
resource_id TEXT NOT NULL,
|
|
123
|
+
revision_id TEXT NOT NULL REFERENCES profile_revisions(id),
|
|
124
|
+
digest TEXT NOT NULL,
|
|
125
|
+
applied_at TEXT NOT NULL,
|
|
126
|
+
PRIMARY KEY (follower_id, resource_id)
|
|
127
|
+
)
|
|
128
|
+
`;
|
|
129
|
+
}),
|
|
130
|
+
"0002_enrollment": Effect.gen(function* () {
|
|
131
|
+
const sql = yield* SqlClient.SqlClient;
|
|
132
|
+
yield* sql `
|
|
133
|
+
CREATE TABLE IF NOT EXISTS enrollment_source (
|
|
134
|
+
singleton INTEGER PRIMARY KEY CHECK (singleton = 1),
|
|
135
|
+
signing_key_reference TEXT NOT NULL,
|
|
136
|
+
tls_key_reference TEXT NOT NULL,
|
|
137
|
+
tls_certificate_reference TEXT NOT NULL,
|
|
138
|
+
tls_fingerprint TEXT NOT NULL
|
|
139
|
+
)
|
|
140
|
+
`;
|
|
141
|
+
yield* sql `
|
|
142
|
+
CREATE TABLE IF NOT EXISTS enrollment_invitations (
|
|
143
|
+
code_digest TEXT PRIMARY KEY,
|
|
144
|
+
nonce_digest TEXT NOT NULL UNIQUE,
|
|
145
|
+
intended_source_fingerprint TEXT NOT NULL,
|
|
146
|
+
tls_fingerprint TEXT NOT NULL,
|
|
147
|
+
endpoint TEXT NOT NULL,
|
|
148
|
+
groups_json TEXT NOT NULL,
|
|
149
|
+
expires_at TEXT NOT NULL,
|
|
150
|
+
used_at TEXT
|
|
151
|
+
)
|
|
152
|
+
`;
|
|
153
|
+
yield* sql `
|
|
154
|
+
CREATE TABLE IF NOT EXISTS follower_credentials (
|
|
155
|
+
follower_id TEXT PRIMARY KEY REFERENCES followers(id) ON DELETE RESTRICT,
|
|
156
|
+
credential_digest TEXT NOT NULL UNIQUE,
|
|
157
|
+
credential_reference TEXT NOT NULL UNIQUE
|
|
158
|
+
)
|
|
159
|
+
`;
|
|
160
|
+
}),
|
|
161
|
+
"0003_follower_synchronization_configuration": Effect.gen(function* () {
|
|
162
|
+
const sql = yield* SqlClient.SqlClient;
|
|
163
|
+
yield* sql `
|
|
164
|
+
CREATE TABLE IF NOT EXISTS follower_sync_configuration (
|
|
165
|
+
singleton INTEGER PRIMARY KEY CHECK (singleton = 1),
|
|
166
|
+
follower_id TEXT NOT NULL REFERENCES followers(id) ON DELETE RESTRICT,
|
|
167
|
+
configuration_json TEXT NOT NULL,
|
|
168
|
+
updated_at TEXT NOT NULL
|
|
169
|
+
)
|
|
170
|
+
`;
|
|
171
|
+
}),
|
|
172
|
+
"0004_applied_resource_owned_files": Effect.gen(function* () {
|
|
173
|
+
const sql = yield* SqlClient.SqlClient;
|
|
174
|
+
yield* sql `
|
|
175
|
+
ALTER TABLE applied_resources
|
|
176
|
+
ADD COLUMN owned_files_json TEXT
|
|
177
|
+
`;
|
|
178
|
+
}),
|
|
179
|
+
"0005_applied_resource_schedule": Effect.gen(function* () {
|
|
180
|
+
const sql = yield* SqlClient.SqlClient;
|
|
181
|
+
yield* sql `
|
|
182
|
+
ALTER TABLE applied_resources
|
|
183
|
+
ADD COLUMN schedule_json TEXT
|
|
184
|
+
`;
|
|
185
|
+
}),
|
|
186
|
+
"0006_applied_resource_removal_ownership": Effect.gen(function* () {
|
|
187
|
+
const sql = yield* SqlClient.SqlClient;
|
|
188
|
+
yield* sql `
|
|
189
|
+
ALTER TABLE applied_resources
|
|
190
|
+
ADD COLUMN kind TEXT
|
|
191
|
+
`;
|
|
192
|
+
yield* sql `
|
|
193
|
+
ALTER TABLE applied_resources
|
|
194
|
+
ADD COLUMN policy TEXT
|
|
195
|
+
`;
|
|
196
|
+
yield* sql `
|
|
197
|
+
ALTER TABLE applied_resources
|
|
198
|
+
ADD COLUMN target TEXT
|
|
199
|
+
`;
|
|
200
|
+
yield* sql `
|
|
201
|
+
ALTER TABLE applied_resources
|
|
202
|
+
ADD COLUMN owned_keys_json TEXT
|
|
203
|
+
`;
|
|
204
|
+
yield* sql `
|
|
205
|
+
ALTER TABLE applied_resources
|
|
206
|
+
ADD COLUMN config_format TEXT
|
|
207
|
+
`;
|
|
208
|
+
}),
|
|
209
|
+
"0007_applied_resource_file_ownership": Effect.gen(function* () {
|
|
210
|
+
const sql = yield* SqlClient.SqlClient;
|
|
211
|
+
yield* sql `
|
|
212
|
+
ALTER TABLE applied_resources
|
|
213
|
+
ADD COLUMN executable INTEGER
|
|
214
|
+
`;
|
|
215
|
+
yield* sql `
|
|
216
|
+
ALTER TABLE applied_resources
|
|
217
|
+
ADD COLUMN symlink_target TEXT
|
|
218
|
+
`;
|
|
219
|
+
}),
|
|
220
|
+
"0008_action_journal_removed_resource": Effect.gen(function* () {
|
|
221
|
+
const sql = yield* SqlClient.SqlClient;
|
|
222
|
+
yield* sql `
|
|
223
|
+
ALTER TABLE action_journal
|
|
224
|
+
ADD COLUMN removed_resource_json TEXT
|
|
225
|
+
`;
|
|
226
|
+
}),
|
|
227
|
+
"0009_recoverable_run_blocks_new_runs": Effect.gen(function* () {
|
|
228
|
+
const sql = yield* SqlClient.SqlClient;
|
|
229
|
+
// Interrupted runs retain rollback and journal evidence and are therefore
|
|
230
|
+
// still recoverable work, not terminal history. Keep the original unique
|
|
231
|
+
// index for concurrent applying inserts and add a durable guard for
|
|
232
|
+
// interrupted recovery. This remains migratable even when older databases
|
|
233
|
+
// already contain more than one historical Interrupted row.
|
|
234
|
+
yield* sql `
|
|
235
|
+
CREATE INDEX IF NOT EXISTS recoverable_runs_by_follower
|
|
236
|
+
ON synchronization_runs(follower_id, status)
|
|
237
|
+
WHERE status IN ('applying', 'Interrupted')
|
|
238
|
+
`;
|
|
239
|
+
yield* sql `
|
|
240
|
+
CREATE TRIGGER IF NOT EXISTS block_run_while_recoverable
|
|
241
|
+
BEFORE INSERT ON synchronization_runs
|
|
242
|
+
WHEN EXISTS (
|
|
243
|
+
SELECT 1
|
|
244
|
+
FROM synchronization_runs
|
|
245
|
+
WHERE follower_id = NEW.follower_id
|
|
246
|
+
AND status IN ('applying', 'Interrupted')
|
|
247
|
+
)
|
|
248
|
+
BEGIN
|
|
249
|
+
SELECT RAISE(ABORT, 'recoverable synchronization run exists');
|
|
250
|
+
END
|
|
251
|
+
`;
|
|
252
|
+
}),
|
|
253
|
+
"0010_transactional_enrollment_and_local_overlays": Effect.gen(function* () {
|
|
254
|
+
const sql = yield* SqlClient.SqlClient;
|
|
255
|
+
yield* sql `
|
|
256
|
+
CREATE TABLE IF NOT EXISTS pending_enrollments (
|
|
257
|
+
follower_id TEXT PRIMARY KEY,
|
|
258
|
+
code_digest TEXT NOT NULL UNIQUE,
|
|
259
|
+
credential_digest TEXT NOT NULL UNIQUE,
|
|
260
|
+
credential_reference TEXT NOT NULL UNIQUE,
|
|
261
|
+
follower_json TEXT NOT NULL,
|
|
262
|
+
created_at TEXT NOT NULL
|
|
263
|
+
)
|
|
264
|
+
`;
|
|
265
|
+
}),
|
|
266
|
+
"0011_profile_revision_blob_index": Effect.gen(function* () {
|
|
267
|
+
const sql = yield* SqlClient.SqlClient;
|
|
268
|
+
yield* sql `
|
|
269
|
+
CREATE TABLE IF NOT EXISTS profile_revision_blobs (
|
|
270
|
+
blob_id TEXT NOT NULL,
|
|
271
|
+
revision_id TEXT NOT NULL REFERENCES profile_revisions(id) ON DELETE CASCADE,
|
|
272
|
+
resource_id TEXT NOT NULL,
|
|
273
|
+
PRIMARY KEY (blob_id, revision_id, resource_id)
|
|
274
|
+
)
|
|
275
|
+
`;
|
|
276
|
+
yield* sql `
|
|
277
|
+
CREATE INDEX IF NOT EXISTS profile_revision_blobs_by_blob
|
|
278
|
+
ON profile_revision_blobs(blob_id, revision_id, resource_id)
|
|
279
|
+
`;
|
|
280
|
+
// Backfill the derived lookup for databases that already contain
|
|
281
|
+
// immutable revisions. The signed revision remains the source of truth;
|
|
282
|
+
// this table only narrows the set of candidates that must be verified.
|
|
283
|
+
yield* sql `
|
|
284
|
+
INSERT OR IGNORE INTO profile_revision_blobs (blob_id, revision_id, resource_id)
|
|
285
|
+
SELECT
|
|
286
|
+
blob.value,
|
|
287
|
+
profile_revisions.id,
|
|
288
|
+
json_extract(resource.value, '$.id')
|
|
289
|
+
FROM profile_revisions
|
|
290
|
+
CROSS JOIN json_each(
|
|
291
|
+
json_extract(profile_revisions.revision_json, '$.resources')
|
|
292
|
+
) AS resource
|
|
293
|
+
CROSS JOIN json_each(
|
|
294
|
+
json_extract(resource.value, '$.blobs')
|
|
295
|
+
) AS blob
|
|
296
|
+
`;
|
|
297
|
+
}),
|
|
298
|
+
});
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import { parse as parseToml, stringify as stringifyToml } from "smol-toml";
|
|
2
|
+
import { parse as parseYaml, stringify as stringifyYaml } from "yaml";
|
|
3
|
+
import { Option, Schema } from "effect";
|
|
4
|
+
const ConfigDocumentSchema = Schema.Record(Schema.String, Schema.MutableJson);
|
|
5
|
+
export const parseConfigDocument = (format, text) => {
|
|
6
|
+
switch (format) {
|
|
7
|
+
case "json": {
|
|
8
|
+
const decoded = Schema.decodeUnknownSync(ConfigDocumentSchema)(JSON.parse(text));
|
|
9
|
+
return { ...decoded };
|
|
10
|
+
}
|
|
11
|
+
case "toml": {
|
|
12
|
+
const decoded = Schema.decodeUnknownSync(ConfigDocumentSchema)(parseToml(text));
|
|
13
|
+
return { ...decoded };
|
|
14
|
+
}
|
|
15
|
+
case "yaml": {
|
|
16
|
+
const decoded = Schema.decodeUnknownSync(ConfigDocumentSchema)(parseYaml(text));
|
|
17
|
+
return { ...decoded };
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
};
|
|
21
|
+
export const serializeConfigDocument = (format, document) => {
|
|
22
|
+
switch (format) {
|
|
23
|
+
case "json":
|
|
24
|
+
return `${JSON.stringify(document, undefined, 2)}\n`;
|
|
25
|
+
case "toml":
|
|
26
|
+
return stringifyToml(document);
|
|
27
|
+
case "yaml":
|
|
28
|
+
return stringifyYaml(document);
|
|
29
|
+
}
|
|
30
|
+
};
|
|
31
|
+
const pathSegments = (path) => {
|
|
32
|
+
const segments = path.split(".");
|
|
33
|
+
if (segments.some((segment) => segment.length === 0)) {
|
|
34
|
+
throw new TypeError(`config key path contains an empty segment: ${path}`);
|
|
35
|
+
}
|
|
36
|
+
return segments;
|
|
37
|
+
};
|
|
38
|
+
export const setConfigPath = (document, path, value) => {
|
|
39
|
+
const segments = pathSegments(path);
|
|
40
|
+
const parsedValue = Schema.decodeUnknownSync(Schema.MutableJson)(value);
|
|
41
|
+
let parent = document;
|
|
42
|
+
for (const segment of segments.slice(0, -1)) {
|
|
43
|
+
const child = parent[segment];
|
|
44
|
+
if (child === undefined) {
|
|
45
|
+
const created = {};
|
|
46
|
+
parent[segment] = created;
|
|
47
|
+
parent = created;
|
|
48
|
+
continue;
|
|
49
|
+
}
|
|
50
|
+
const nested = Option.getOrUndefined(Schema.decodeUnknownOption(ConfigDocumentSchema)(child));
|
|
51
|
+
if (nested === undefined) {
|
|
52
|
+
throw new TypeError(`config key path crosses a non-object value: ${path}`);
|
|
53
|
+
}
|
|
54
|
+
const mutableNested = { ...nested };
|
|
55
|
+
parent[segment] = mutableNested;
|
|
56
|
+
parent = mutableNested;
|
|
57
|
+
}
|
|
58
|
+
parent[segments.at(-1)] = parsedValue;
|
|
59
|
+
};
|
|
60
|
+
export const getConfigPath = (document, path) => {
|
|
61
|
+
const segments = pathSegments(path);
|
|
62
|
+
let value = document;
|
|
63
|
+
for (const segment of segments) {
|
|
64
|
+
const parent = Option.getOrUndefined(Schema.decodeUnknownOption(ConfigDocumentSchema)(value));
|
|
65
|
+
if (parent === undefined)
|
|
66
|
+
return undefined;
|
|
67
|
+
value = parent[segment];
|
|
68
|
+
if (value === undefined)
|
|
69
|
+
return undefined;
|
|
70
|
+
}
|
|
71
|
+
return value;
|
|
72
|
+
};
|
|
73
|
+
export const removeConfigPath = (document, path) => {
|
|
74
|
+
const segments = pathSegments(path);
|
|
75
|
+
const parents = [];
|
|
76
|
+
let current = document;
|
|
77
|
+
for (const segment of segments.slice(0, -1)) {
|
|
78
|
+
const nested = Option.getOrUndefined(Schema.decodeUnknownOption(ConfigDocumentSchema)(current[segment]));
|
|
79
|
+
if (nested === undefined)
|
|
80
|
+
return;
|
|
81
|
+
parents.push({ document: current, segment });
|
|
82
|
+
const mutableNested = { ...nested };
|
|
83
|
+
current[segment] = mutableNested;
|
|
84
|
+
current = mutableNested;
|
|
85
|
+
}
|
|
86
|
+
delete current[segments.at(-1)];
|
|
87
|
+
for (const parent of parents.reverse()) {
|
|
88
|
+
const child = parent.document[parent.segment];
|
|
89
|
+
const nested = child === undefined
|
|
90
|
+
? undefined
|
|
91
|
+
: Option.getOrUndefined(Schema.decodeUnknownOption(ConfigDocumentSchema)(child));
|
|
92
|
+
if (nested !== undefined
|
|
93
|
+
&& Object.keys(nested).length === 0) {
|
|
94
|
+
delete parent.document[parent.segment];
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
};
|