@rezwanul7/settings 0.0.0-stage → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +388 -2
- package/dist/index.cjs +443 -0
- package/dist/index.d.cts +348 -0
- package/dist/index.d.ts +348 -0
- package/dist/index.js +436 -0
- package/dist/prisma.cjs +55 -0
- package/dist/prisma.d.cts +148 -0
- package/dist/prisma.d.ts +148 -0
- package/dist/prisma.js +53 -0
- package/dist/store-B4pWek75.d.cts +61 -0
- package/dist/store-B4pWek75.d.ts +61 -0
- package/package.json +66 -3
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,443 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// src/definition.ts
|
|
4
|
+
function defineSettings(definition) {
|
|
5
|
+
return {
|
|
6
|
+
schema: definition.schema,
|
|
7
|
+
defaults: definition.defaults,
|
|
8
|
+
singleOwner: definition.singleOwner ?? false
|
|
9
|
+
};
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
// src/errors.ts
|
|
13
|
+
var SettingsValidationError = class extends Error {
|
|
14
|
+
/** The Standard Schema issues the Effective settings failed with. */
|
|
15
|
+
issues;
|
|
16
|
+
/**
|
|
17
|
+
* @param issues - The Standard Schema issues.
|
|
18
|
+
*/
|
|
19
|
+
constructor(issues) {
|
|
20
|
+
super(`The settings are invalid: ${issues.map(describeIssue).join("; ")}`);
|
|
21
|
+
this.name = "SettingsValidationError";
|
|
22
|
+
this.issues = issues;
|
|
23
|
+
}
|
|
24
|
+
};
|
|
25
|
+
var SettingsConflictError = class extends Error {
|
|
26
|
+
/** The Owner whose Overrides couldn't be written. */
|
|
27
|
+
owner;
|
|
28
|
+
/**
|
|
29
|
+
* @param owner - The Owner whose Overrides couldn't be written.
|
|
30
|
+
* @param retries - How many times the write re-read and applied its change again.
|
|
31
|
+
*/
|
|
32
|
+
constructor(owner, retries) {
|
|
33
|
+
super(
|
|
34
|
+
`Writing the Overrides of ${describeOwner(owner)} conflicted with other writes ${retries + 1} times in a row.`
|
|
35
|
+
);
|
|
36
|
+
this.name = "SettingsConflictError";
|
|
37
|
+
this.owner = owner;
|
|
38
|
+
}
|
|
39
|
+
};
|
|
40
|
+
var SettingsDefinitionError = class extends Error {
|
|
41
|
+
/** The Scope name the error is about. */
|
|
42
|
+
scope;
|
|
43
|
+
/** The Standard Schema issues, when the error is a failed validation. */
|
|
44
|
+
issues;
|
|
45
|
+
/**
|
|
46
|
+
* @param message - The error message.
|
|
47
|
+
* @param scope - The Scope name.
|
|
48
|
+
* @param issues - The Standard Schema issues, if any.
|
|
49
|
+
*/
|
|
50
|
+
constructor(message, scope, issues = []) {
|
|
51
|
+
super(message);
|
|
52
|
+
this.name = "SettingsDefinitionError";
|
|
53
|
+
this.scope = scope;
|
|
54
|
+
this.issues = issues;
|
|
55
|
+
}
|
|
56
|
+
};
|
|
57
|
+
function describeOwner(owner) {
|
|
58
|
+
return `${owner.scope} "${owner.id}"`;
|
|
59
|
+
}
|
|
60
|
+
function issuePath(issue) {
|
|
61
|
+
return (issue.path ?? []).map(
|
|
62
|
+
(segment) => String(typeof segment === "object" ? segment.key : segment)
|
|
63
|
+
);
|
|
64
|
+
}
|
|
65
|
+
function describeIssue(issue) {
|
|
66
|
+
const path = issuePath(issue).join(".");
|
|
67
|
+
return path === "" ? issue.message : `${path}: ${issue.message}`;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// src/merge.ts
|
|
71
|
+
function isPlainObject(value) {
|
|
72
|
+
if (typeof value !== "object" || value === null) return false;
|
|
73
|
+
const prototype = Object.getPrototypeOf(value);
|
|
74
|
+
return prototype === Object.prototype || prototype === null;
|
|
75
|
+
}
|
|
76
|
+
function ownValue(object, key2) {
|
|
77
|
+
return Object.hasOwn(object, key2) ? object[key2] : void 0;
|
|
78
|
+
}
|
|
79
|
+
function setOwn(object, key2, value) {
|
|
80
|
+
Object.defineProperty(object, key2, {
|
|
81
|
+
value,
|
|
82
|
+
writable: true,
|
|
83
|
+
enumerable: true,
|
|
84
|
+
configurable: true
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
function withoutUndefined(partial) {
|
|
88
|
+
if (!isPlainObject(partial)) return partial;
|
|
89
|
+
const result = {};
|
|
90
|
+
for (const [key2, value] of Object.entries(partial)) {
|
|
91
|
+
if (value === void 0) continue;
|
|
92
|
+
const kept = withoutUndefined(value);
|
|
93
|
+
if (isPlainObject(value) && Object.keys(value).length > 0 && Object.keys(kept).length === 0) {
|
|
94
|
+
continue;
|
|
95
|
+
}
|
|
96
|
+
setOwn(result, key2, kept);
|
|
97
|
+
}
|
|
98
|
+
return result;
|
|
99
|
+
}
|
|
100
|
+
function mergeOverrides(defaults, overrides) {
|
|
101
|
+
if (overrides === void 0) return structuredClone(defaults);
|
|
102
|
+
if (!isPlainObject(overrides)) return structuredClone(overrides);
|
|
103
|
+
const base = isPlainObject(defaults) ? defaults : {};
|
|
104
|
+
const merged = {};
|
|
105
|
+
for (const [key2, value] of Object.entries(base)) {
|
|
106
|
+
setOwn(merged, key2, mergeOverrides(value, ownValue(overrides, key2)));
|
|
107
|
+
}
|
|
108
|
+
for (const [key2, value] of Object.entries(overrides)) {
|
|
109
|
+
if (value === void 0 || Object.hasOwn(base, key2)) continue;
|
|
110
|
+
setOwn(merged, key2, mergeOverrides(void 0, value));
|
|
111
|
+
}
|
|
112
|
+
return merged;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// src/overrides.ts
|
|
116
|
+
var FORBIDDEN_KEYS = /* @__PURE__ */ new Set([
|
|
117
|
+
"__proto__",
|
|
118
|
+
"constructor",
|
|
119
|
+
"prototype"
|
|
120
|
+
]);
|
|
121
|
+
function keysOf(path) {
|
|
122
|
+
return path.split(".");
|
|
123
|
+
}
|
|
124
|
+
function forbiddenKeys(path, value) {
|
|
125
|
+
const keys = path === void 0 ? [] : keysOf(path);
|
|
126
|
+
const forbidden = keys.findIndex((key2) => FORBIDDEN_KEYS.has(key2));
|
|
127
|
+
if (forbidden !== -1) {
|
|
128
|
+
return [{ message: "Forbidden key", path: keys.slice(0, forbidden + 1) }];
|
|
129
|
+
}
|
|
130
|
+
return forbiddenKeysIn(value, keys);
|
|
131
|
+
}
|
|
132
|
+
function forbiddenKeysIn(value, path) {
|
|
133
|
+
if (Array.isArray(value)) {
|
|
134
|
+
return value.flatMap(
|
|
135
|
+
(item, index) => forbiddenKeysIn(item, [...path, index])
|
|
136
|
+
);
|
|
137
|
+
}
|
|
138
|
+
if (!isPlainObject(value)) return [];
|
|
139
|
+
return Object.entries(value).flatMap(
|
|
140
|
+
([key2, child]) => FORBIDDEN_KEYS.has(key2) ? [{ message: "Forbidden key", path: [...path, key2] }] : forbiddenKeysIn(child, [...path, key2])
|
|
141
|
+
);
|
|
142
|
+
}
|
|
143
|
+
function getAt(value, path) {
|
|
144
|
+
let current = value;
|
|
145
|
+
for (const key2 of keysOf(path)) {
|
|
146
|
+
if (!isPlainObject(current)) return void 0;
|
|
147
|
+
current = ownValue(current, key2);
|
|
148
|
+
}
|
|
149
|
+
return current;
|
|
150
|
+
}
|
|
151
|
+
function hasAt(overrides, path) {
|
|
152
|
+
let current = overrides;
|
|
153
|
+
for (const key2 of keysOf(path)) {
|
|
154
|
+
if (!isPlainObject(current) || !Object.hasOwn(current, key2)) return false;
|
|
155
|
+
current = current[key2];
|
|
156
|
+
}
|
|
157
|
+
return true;
|
|
158
|
+
}
|
|
159
|
+
function setAt(overrides, path, value) {
|
|
160
|
+
const result = structuredClone(overrides);
|
|
161
|
+
const keys = keysOf(path);
|
|
162
|
+
const last = keys.pop();
|
|
163
|
+
let parent = result;
|
|
164
|
+
for (const key2 of keys) {
|
|
165
|
+
const child = ownValue(parent, key2);
|
|
166
|
+
if (isPlainObject(child)) {
|
|
167
|
+
parent = child;
|
|
168
|
+
} else {
|
|
169
|
+
const created = {};
|
|
170
|
+
setOwn(parent, key2, created);
|
|
171
|
+
parent = created;
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
setOwn(parent, last, structuredClone(value));
|
|
175
|
+
return result;
|
|
176
|
+
}
|
|
177
|
+
function removeAt(overrides, path) {
|
|
178
|
+
return removeKeys(overrides, keysOf(path));
|
|
179
|
+
}
|
|
180
|
+
function removeKeys(overrides, path) {
|
|
181
|
+
if (path.length === 0) return {};
|
|
182
|
+
const result = structuredClone(overrides);
|
|
183
|
+
const keys = [...path];
|
|
184
|
+
const last = keys.pop();
|
|
185
|
+
const parents = [result];
|
|
186
|
+
for (const key2 of keys) {
|
|
187
|
+
const child = ownValue(parents[parents.length - 1] ?? {}, key2);
|
|
188
|
+
if (!isPlainObject(child)) return result;
|
|
189
|
+
parents.push(child);
|
|
190
|
+
}
|
|
191
|
+
delete parents[parents.length - 1]?.[last];
|
|
192
|
+
for (let depth = keys.length - 1; depth >= 0; depth--) {
|
|
193
|
+
const parent = parents[depth];
|
|
194
|
+
const key2 = keys[depth];
|
|
195
|
+
if (Object.keys(parent[key2]).length > 0) break;
|
|
196
|
+
delete parent[key2];
|
|
197
|
+
}
|
|
198
|
+
return result;
|
|
199
|
+
}
|
|
200
|
+
function withoutIssues(overrides, issues) {
|
|
201
|
+
let result = overrides;
|
|
202
|
+
for (const path of issues.flatMap(pathsOfIssue)) {
|
|
203
|
+
const covering = coveringOverride(result, path);
|
|
204
|
+
if (covering !== void 0) result = removeKeys(result, covering);
|
|
205
|
+
}
|
|
206
|
+
return result;
|
|
207
|
+
}
|
|
208
|
+
function pathsOfIssue(issue) {
|
|
209
|
+
const path = issuePath(issue);
|
|
210
|
+
const { keys } = issue;
|
|
211
|
+
if (Array.isArray(keys) && keys.every((key2) => typeof key2 === "string")) {
|
|
212
|
+
return keys.map((key2) => [...path, key2]);
|
|
213
|
+
}
|
|
214
|
+
return [path];
|
|
215
|
+
}
|
|
216
|
+
function unknownKeys(overrides, output, path = []) {
|
|
217
|
+
if (!isPlainObject(output)) return [];
|
|
218
|
+
return Object.entries(overrides).flatMap(([key2, value]) => {
|
|
219
|
+
if (!Object.hasOwn(output, key2)) return [[...path, key2]];
|
|
220
|
+
return isPlainObject(value) ? unknownKeys(value, output[key2], [...path, key2]) : [];
|
|
221
|
+
});
|
|
222
|
+
}
|
|
223
|
+
function coveringOverride(overrides, keys) {
|
|
224
|
+
let current = overrides;
|
|
225
|
+
for (const [depth, key2] of keys.entries()) {
|
|
226
|
+
if (!isPlainObject(current)) return keys.slice(0, depth);
|
|
227
|
+
if (!Object.hasOwn(current, key2)) return void 0;
|
|
228
|
+
current = current[key2];
|
|
229
|
+
}
|
|
230
|
+
return [...keys];
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
// src/settings.ts
|
|
234
|
+
var SINGLE_OWNER_ID = "default";
|
|
235
|
+
function createSettings(options) {
|
|
236
|
+
const { definitions, store, onInvalid, maxRetries = 3 } = options;
|
|
237
|
+
if (!Number.isInteger(maxRetries) || maxRetries < 0) {
|
|
238
|
+
throw new RangeError(
|
|
239
|
+
`maxRetries must be a whole number, 0 or more; got ${maxRetries}.`
|
|
240
|
+
);
|
|
241
|
+
}
|
|
242
|
+
const checkedDefaults = /* @__PURE__ */ new Map();
|
|
243
|
+
function checkDefaults(name, definition) {
|
|
244
|
+
let check = checkedDefaults.get(name);
|
|
245
|
+
if (check === void 0) {
|
|
246
|
+
check = validate(definition.schema, definition.defaults).then(
|
|
247
|
+
(result) => {
|
|
248
|
+
if (result.issues === void 0) return;
|
|
249
|
+
throw new SettingsDefinitionError(
|
|
250
|
+
`The defaults of Scope "${name}" fail its schema: ${result.issues.map(describeIssue).join("; ")}`,
|
|
251
|
+
name,
|
|
252
|
+
result.issues
|
|
253
|
+
);
|
|
254
|
+
}
|
|
255
|
+
);
|
|
256
|
+
checkedDefaults.set(name, check);
|
|
257
|
+
const pending = check;
|
|
258
|
+
pending.catch((error) => {
|
|
259
|
+
if (!(error instanceof SettingsDefinitionError) && checkedDefaults.get(name) === pending) {
|
|
260
|
+
checkedDefaults.delete(name);
|
|
261
|
+
}
|
|
262
|
+
});
|
|
263
|
+
}
|
|
264
|
+
return check;
|
|
265
|
+
}
|
|
266
|
+
return {
|
|
267
|
+
scope(name, ...[id]) {
|
|
268
|
+
if (!Object.hasOwn(definitions, name)) {
|
|
269
|
+
throw new SettingsDefinitionError(`Unknown Scope "${name}".`, name);
|
|
270
|
+
}
|
|
271
|
+
const definition = definitions[name];
|
|
272
|
+
const owner = ownerOf(name, definition, id);
|
|
273
|
+
async function readOverrides() {
|
|
274
|
+
await checkDefaults(name, definition);
|
|
275
|
+
return store.read(owner);
|
|
276
|
+
}
|
|
277
|
+
async function withoutStale(overrides) {
|
|
278
|
+
let kept = overrides;
|
|
279
|
+
const ignored = [];
|
|
280
|
+
let result = await validate(
|
|
281
|
+
definition.schema,
|
|
282
|
+
mergeOverrides(definition.defaults, kept)
|
|
283
|
+
);
|
|
284
|
+
while (result.issues !== void 0) {
|
|
285
|
+
const next = withoutIssues(kept, result.issues);
|
|
286
|
+
if (next === kept) break;
|
|
287
|
+
ignored.push(...result.issues);
|
|
288
|
+
kept = next;
|
|
289
|
+
result = await validate(
|
|
290
|
+
definition.schema,
|
|
291
|
+
mergeOverrides(definition.defaults, kept)
|
|
292
|
+
);
|
|
293
|
+
}
|
|
294
|
+
if (result.issues === void 0) {
|
|
295
|
+
const unknown = unknownSettings(kept, result.value);
|
|
296
|
+
if (unknown.length > 0) {
|
|
297
|
+
ignored.push(...unknown);
|
|
298
|
+
kept = withoutIssues(kept, unknown);
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
if (ignored.length > 0) onInvalid?.(owner, ignored);
|
|
302
|
+
return { overrides: kept, result };
|
|
303
|
+
}
|
|
304
|
+
async function effective() {
|
|
305
|
+
const stored = await readOverrides();
|
|
306
|
+
const { result } = await withoutStale(stored?.overrides ?? {});
|
|
307
|
+
if (result.issues !== void 0) {
|
|
308
|
+
throw new SettingsDefinitionError(
|
|
309
|
+
`The Effective settings of ${describeOwner(owner)} fail the Scope's schema: ${result.issues.map(describeIssue).join("; ")}`,
|
|
310
|
+
name,
|
|
311
|
+
result.issues
|
|
312
|
+
);
|
|
313
|
+
}
|
|
314
|
+
return result.value;
|
|
315
|
+
}
|
|
316
|
+
async function change(apply, {
|
|
317
|
+
onlyIf
|
|
318
|
+
} = {}) {
|
|
319
|
+
for (let retries = 0; ; retries++) {
|
|
320
|
+
const stored = await readOverrides();
|
|
321
|
+
if (onlyIf !== void 0 && !onlyIf(stored?.overrides ?? {})) return;
|
|
322
|
+
const usable = (await withoutStale(stored?.overrides ?? {})).overrides;
|
|
323
|
+
const next = apply(usable);
|
|
324
|
+
const result = await validate(
|
|
325
|
+
definition.schema,
|
|
326
|
+
mergeOverrides(definition.defaults, next)
|
|
327
|
+
);
|
|
328
|
+
if (result.issues !== void 0) {
|
|
329
|
+
throw new SettingsValidationError(result.issues);
|
|
330
|
+
}
|
|
331
|
+
const unknown = unknownSettings(next, result.value);
|
|
332
|
+
if (unknown.length > 0) throw new SettingsValidationError(unknown);
|
|
333
|
+
const version = await store.write(
|
|
334
|
+
owner,
|
|
335
|
+
next,
|
|
336
|
+
stored?.version ?? null
|
|
337
|
+
);
|
|
338
|
+
if (version !== null) return;
|
|
339
|
+
if (retries >= maxRetries) {
|
|
340
|
+
throw new SettingsConflictError(owner, retries);
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
return {
|
|
345
|
+
owner,
|
|
346
|
+
async get(path) {
|
|
347
|
+
const settings = await effective();
|
|
348
|
+
return path === void 0 ? settings : getAt(settings, path);
|
|
349
|
+
},
|
|
350
|
+
async set(path, value) {
|
|
351
|
+
rejectForbiddenKeys(path, value);
|
|
352
|
+
await change((overrides) => setAt(overrides, path, value));
|
|
353
|
+
},
|
|
354
|
+
async update(partial) {
|
|
355
|
+
rejectForbiddenKeys(void 0, partial);
|
|
356
|
+
await change(
|
|
357
|
+
(overrides) => mergeOverrides(overrides, withoutUndefined(partial))
|
|
358
|
+
);
|
|
359
|
+
},
|
|
360
|
+
async getOverrides() {
|
|
361
|
+
return (await readOverrides())?.overrides ?? {};
|
|
362
|
+
},
|
|
363
|
+
async reset(path) {
|
|
364
|
+
if (path === void 0) {
|
|
365
|
+
await checkDefaults(name, definition);
|
|
366
|
+
await store.delete(owner);
|
|
367
|
+
return;
|
|
368
|
+
}
|
|
369
|
+
rejectForbiddenKeys(path, void 0);
|
|
370
|
+
await change((overrides) => removeAt(overrides, path), {
|
|
371
|
+
onlyIf: (stored) => hasAt(stored, path)
|
|
372
|
+
});
|
|
373
|
+
}
|
|
374
|
+
};
|
|
375
|
+
}
|
|
376
|
+
};
|
|
377
|
+
}
|
|
378
|
+
function ownerOf(scope, definition, id) {
|
|
379
|
+
if (definition.singleOwner) {
|
|
380
|
+
if (id !== void 0) {
|
|
381
|
+
throw new TypeError(
|
|
382
|
+
`Scope "${scope}" has a single Owner and takes no id.`
|
|
383
|
+
);
|
|
384
|
+
}
|
|
385
|
+
return { scope, id: SINGLE_OWNER_ID };
|
|
386
|
+
}
|
|
387
|
+
if (typeof id !== "string") {
|
|
388
|
+
throw new TypeError(`Scope "${scope}" needs an Owner id.`);
|
|
389
|
+
}
|
|
390
|
+
return { scope, id };
|
|
391
|
+
}
|
|
392
|
+
function rejectForbiddenKeys(path, value) {
|
|
393
|
+
const issues = forbiddenKeys(path, value);
|
|
394
|
+
if (issues.length > 0) throw new SettingsValidationError(issues);
|
|
395
|
+
}
|
|
396
|
+
function unknownSettings(overrides, output) {
|
|
397
|
+
return unknownKeys(overrides, output).map((path) => ({
|
|
398
|
+
message: "Unknown setting",
|
|
399
|
+
path
|
|
400
|
+
}));
|
|
401
|
+
}
|
|
402
|
+
async function validate(schema, value) {
|
|
403
|
+
return schema["~standard"].validate(value);
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
// src/memory-store.ts
|
|
407
|
+
var MemoryStore = class {
|
|
408
|
+
#documents = /* @__PURE__ */ new Map();
|
|
409
|
+
// The last version each Owner had, kept across deletes so a recreated document never reuses a version.
|
|
410
|
+
#lastVersions = /* @__PURE__ */ new Map();
|
|
411
|
+
/** {@inheritDoc SettingsStore.read} */
|
|
412
|
+
async read(owner) {
|
|
413
|
+
const stored = this.#documents.get(key(owner));
|
|
414
|
+
return stored === void 0 ? null : structuredClone(stored);
|
|
415
|
+
}
|
|
416
|
+
/** {@inheritDoc SettingsStore.write} */
|
|
417
|
+
async write(owner, overrides, expectedVersion) {
|
|
418
|
+
const ownerKey = key(owner);
|
|
419
|
+
const currentVersion = this.#documents.get(ownerKey)?.version ?? null;
|
|
420
|
+
if (currentVersion !== expectedVersion) return null;
|
|
421
|
+
const version = (this.#lastVersions.get(ownerKey) ?? 0) + 1;
|
|
422
|
+
this.#documents.set(ownerKey, { overrides: asJson(overrides), version });
|
|
423
|
+
this.#lastVersions.set(ownerKey, version);
|
|
424
|
+
return version;
|
|
425
|
+
}
|
|
426
|
+
/** {@inheritDoc SettingsStore.delete} */
|
|
427
|
+
async delete(owner) {
|
|
428
|
+
this.#documents.delete(key(owner));
|
|
429
|
+
}
|
|
430
|
+
};
|
|
431
|
+
function key(owner) {
|
|
432
|
+
return JSON.stringify([owner.scope, owner.id]);
|
|
433
|
+
}
|
|
434
|
+
function asJson(overrides) {
|
|
435
|
+
return JSON.parse(JSON.stringify(overrides));
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
exports.MemoryStore = MemoryStore;
|
|
439
|
+
exports.SettingsConflictError = SettingsConflictError;
|
|
440
|
+
exports.SettingsDefinitionError = SettingsDefinitionError;
|
|
441
|
+
exports.SettingsValidationError = SettingsValidationError;
|
|
442
|
+
exports.createSettings = createSettings;
|
|
443
|
+
exports.defineSettings = defineSettings;
|