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