@rezwanul7/settings 1.1.0 → 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/README.md +117 -48
- package/dist/index.cjs +165 -20
- package/dist/index.d.cts +124 -29
- package/dist/index.d.ts +124 -29
- package/dist/index.js +165 -20
- package/dist/prisma.cjs +4 -1
- package/dist/prisma.d.cts +10 -10
- package/dist/prisma.d.ts +10 -10
- package/dist/prisma.js +4 -1
- package/package.json +1 -1
package/dist/index.d.cts
CHANGED
|
@@ -76,11 +76,43 @@ interface SettingsDefinition<T = unknown, SingleOwner extends boolean = boolean>
|
|
|
76
76
|
/** Whether the Scope has exactly one Owner, stored with the id `"default"`. */
|
|
77
77
|
readonly singleOwner: SingleOwner;
|
|
78
78
|
}
|
|
79
|
+
/** The settings type of a Settings definition. Internal: not exported from the package entry. */
|
|
80
|
+
type SettingsOf<D> = D extends SettingsDefinition<infer T> ? T : never;
|
|
81
|
+
/** Counts down the depth {@link NonJsonPaths} still walks, so a recursive settings type can't recurse forever. */
|
|
82
|
+
type Depth = [never, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9];
|
|
83
|
+
/** Whether an object type has a method, as a `Date`, `Map`, `Set` or class instance does. */
|
|
84
|
+
type HasMethod<T> = true extends {
|
|
85
|
+
[K in keyof T]-?: 0 extends 1 & T[K] ? false : T[K] extends (...args: never[]) => unknown ? true : false;
|
|
86
|
+
}[keyof T] ? true : false;
|
|
87
|
+
/**
|
|
88
|
+
* The dot paths in `T` whose values can't be kept as JSON, such as a `Date`, `Map`, `Set`, `bigint` or class
|
|
89
|
+
* instance; `never` when every value can. An array's elements are named `path[]`. `unknown` and `any` pass, since
|
|
90
|
+
* the runtime check covers them, and so does anything deeper than the walk goes.
|
|
91
|
+
*/
|
|
92
|
+
type NonJsonPaths<T, P extends string, D extends number = 10> = [D] extends [
|
|
93
|
+
never
|
|
94
|
+
] ? never : 0 extends 1 & T ? never : unknown extends T ? never : T extends string | number | boolean | null | undefined ? never : T extends bigint | symbol | ((...args: never[]) => unknown) ? P : T extends readonly (infer E)[] ? NonJsonPaths<E, `${P}[]`, Depth[D]> : T extends object ? HasMethod<T> extends true ? P : {
|
|
95
|
+
[K in keyof T & string]-?: NonJsonPaths<T[K], P extends "" ? K : `${P}.${K}`, Depth[D]>;
|
|
96
|
+
}[keyof T & string] : P;
|
|
97
|
+
/**
|
|
98
|
+
* Nothing extra when the schema's output can be kept as JSON. Otherwise a property no schema has, whose name states
|
|
99
|
+
* the rule and whose type names the offending paths, so the compile error at `schema` reads as the reason.
|
|
100
|
+
*/
|
|
101
|
+
type JsonOutputCheck<Schema extends StandardSchemaV1> = [
|
|
102
|
+
NonJsonPaths<StandardSchemaV1.InferOutput<Schema>, "">
|
|
103
|
+
] extends [never] ? unknown : {
|
|
104
|
+
readonly "Not a JSON value, so it can't be stored at": NonJsonPaths<StandardSchemaV1.InferOutput<Schema>, ""> extends "" ? "(the whole settings)" : NonJsonPaths<StandardSchemaV1.InferOutput<Schema>, "">;
|
|
105
|
+
};
|
|
79
106
|
/**
|
|
80
107
|
* Declares a Scope's Settings definition. The settings type is inferred from the schema's output type, and
|
|
81
108
|
* `defaults` must be a complete value of it; optional schema fields may be left out. Any Standard Schema works, such
|
|
82
109
|
* as one from Zod, Valibot or ArkType.
|
|
83
110
|
*
|
|
111
|
+
* Overrides are kept as JSON, so a schema whose output has a value that isn't, such as a `Date` from
|
|
112
|
+
* `z.coerce.date()`, a `Map`, a `Set`, a `bigint` or a class instance, is a compile error at `schema` that names the
|
|
113
|
+
* path. Store a date as its string and convert it where the setting is used. Output typed `unknown` or `any` passes;
|
|
114
|
+
* writes still check their values at runtime.
|
|
115
|
+
*
|
|
84
116
|
* The defaults are checked against the schema on the first operation on the Scope, because Standard Schema
|
|
85
117
|
* validation may be async; `createSettings` throws `SettingsDefinitionError` there if they fail.
|
|
86
118
|
*
|
|
@@ -88,27 +120,27 @@ interface SettingsDefinition<T = unknown, SingleOwner extends boolean = boolean>
|
|
|
88
120
|
* @returns The Settings definition, to pass to `createSettings` under its Scope name.
|
|
89
121
|
*
|
|
90
122
|
* @example
|
|
91
|
-
* const
|
|
123
|
+
* const userDefinition = defineSettings({
|
|
92
124
|
* schema: z.object({ theme: z.enum(["light", "dark"]), language: z.string() }),
|
|
93
125
|
* defaults: { theme: "light", language: "en" },
|
|
94
126
|
* });
|
|
95
|
-
* const
|
|
127
|
+
* const globalDefinition = defineSettings({
|
|
96
128
|
* schema: z.object({ maintenanceBanner: z.string().nullable() }),
|
|
97
129
|
* defaults: { maintenanceBanner: null },
|
|
98
130
|
* singleOwner: true,
|
|
99
131
|
* });
|
|
100
132
|
*/
|
|
101
133
|
declare function defineSettings<Schema extends StandardSchemaV1, SingleOwner extends boolean = false>(definition: {
|
|
102
|
-
/** The Standard Schema every Owner's Effective settings must pass. */
|
|
103
|
-
readonly schema: Schema
|
|
134
|
+
/** The Standard Schema every Owner's Effective settings must pass. Its output must be JSON. */
|
|
135
|
+
readonly schema: Schema & JsonOutputCheck<Schema>;
|
|
104
136
|
/** The complete settings used where an Owner has no Override. */
|
|
105
137
|
readonly defaults: StandardSchemaV1.InferOutput<Schema>;
|
|
106
138
|
/** `true` for a Scope with exactly one Owner, which takes no id. Defaults to `false`. */
|
|
107
139
|
readonly singleOwner?: SingleOwner;
|
|
108
140
|
}): SettingsDefinition<StandardSchemaV1.InferOutput<Schema>, SingleOwner>;
|
|
109
141
|
|
|
110
|
-
/** Objects whose keys a dot path walks through; arrays
|
|
111
|
-
type Walkable<T> = T extends readonly unknown[] |
|
|
142
|
+
/** Objects whose keys a dot path walks through; arrays and functions are values, never walked into. */
|
|
143
|
+
type Walkable<T> = T extends readonly unknown[] | ((...args: never[]) => unknown) ? never : T extends object ? T : never;
|
|
112
144
|
/**
|
|
113
145
|
* The union of dot paths through the nested plain objects of `T`, such as `"theme" | "notifications" |
|
|
114
146
|
* "notifications.email"`. A path never descends into an array: an array is one value, set and reset as a whole.
|
|
@@ -128,11 +160,11 @@ type Path<T> = [Walkable<T>] extends [never] ? never : {
|
|
|
128
160
|
type PathValue<T, P extends string> = P extends keyof Walkable<T> ? Walkable<T>[P] : P extends `${infer K}.${infer Rest}` ? K extends keyof Walkable<T> ? PathValue<NonNullable<Walkable<T>[K]>, Rest> | (null extends Walkable<T>[K] ? undefined : never) | (undefined extends Walkable<T>[K] ? undefined : never) : never : never;
|
|
129
161
|
/**
|
|
130
162
|
* A partial of `T` at any depth, for `update`: every key of a nested plain object may be left out or `undefined`.
|
|
131
|
-
* Arrays
|
|
163
|
+
* Arrays and functions are whole values, never partial.
|
|
132
164
|
*
|
|
133
165
|
* @typeParam T - The settings type.
|
|
134
166
|
*/
|
|
135
|
-
type DeepPartial<T> = T extends readonly unknown[] |
|
|
167
|
+
type DeepPartial<T> = T extends readonly unknown[] | ((...args: never[]) => unknown) ? T : T extends object ? {
|
|
136
168
|
[K in keyof T]?: DeepPartial<T[K]> | undefined;
|
|
137
169
|
} : T;
|
|
138
170
|
|
|
@@ -152,6 +184,10 @@ interface SettingsOptions<Definitions extends Record<string, SettingsDefinition>
|
|
|
152
184
|
* next successful write drops them. Writes read the Overrides first, so it can also be called during a write, again
|
|
153
185
|
* on each retry. Without it, ignored Overrides are dropped silently.
|
|
154
186
|
*
|
|
187
|
+
* It isn't awaited, and it can't change the result of the read or write that calls it: if it throws or returns a
|
|
188
|
+
* rejected promise, the operation goes on as if it had succeeded, and the failure goes to `console.warn` the first
|
|
189
|
+
* time only for each {@link createSettings} result.
|
|
190
|
+
*
|
|
155
191
|
* @param owner - The Owner whose Overrides were ignored.
|
|
156
192
|
* @param issues - The Standard Schema issues the Effective settings failed with, naming the ignored paths.
|
|
157
193
|
*/
|
|
@@ -259,8 +295,6 @@ interface ScopedSettings<T> {
|
|
|
259
295
|
*/
|
|
260
296
|
reset(): Promise<void>;
|
|
261
297
|
}
|
|
262
|
-
/** The settings type of a Settings definition. */
|
|
263
|
-
type SettingsOf<D> = D extends SettingsDefinition<infer T> ? T : never;
|
|
264
298
|
/** The id argument of `scope()`: none for a single-owner Scope, a string for any other. */
|
|
265
299
|
type OwnerIdArgs<D> = D extends SettingsDefinition<unknown, true> ? [] : [id: string];
|
|
266
300
|
/**
|
|
@@ -279,7 +313,10 @@ type OwnerIdArgs<D> = D extends SettingsDefinition<unknown, true> ? [] : [id: st
|
|
|
279
313
|
* @throws `RangeError` If `maxRetries` isn't a whole number, 0 or more.
|
|
280
314
|
*
|
|
281
315
|
* @example
|
|
282
|
-
* const settings = createSettings({
|
|
316
|
+
* const settings = createSettings({
|
|
317
|
+
* definitions: { user: userDefinition, global: globalDefinition },
|
|
318
|
+
* store: new MemoryStore(),
|
|
319
|
+
* });
|
|
283
320
|
* const theme = await settings.scope("user", "1").get("theme");
|
|
284
321
|
* await settings.scope("global").set("maintenanceBanner", "Back at 10:00");
|
|
285
322
|
*/
|
|
@@ -288,8 +325,11 @@ declare function createSettings<Definitions extends Record<string, SettingsDefin
|
|
|
288
325
|
* Returns the settings of one Owner of the Scope `name`.
|
|
289
326
|
*
|
|
290
327
|
* @param name - The Scope name, a key of `definitions`.
|
|
291
|
-
* @param id - The Owner's id; left out for a single-owner Scope.
|
|
328
|
+
* @param id - The Owner's id, any non-empty string; left out for a single-owner Scope. The package doesn't interpret
|
|
329
|
+
* it, so ids that differ in any way, such as `"1"` and `" 1"`, are different Owners.
|
|
292
330
|
* @throws {@link SettingsDefinitionError} If `name` isn't a key of `definitions`.
|
|
331
|
+
* @throws `TypeError` If `id` is the empty string, is missing for a normal Scope, or is given for a single-owner
|
|
332
|
+
* Scope.
|
|
293
333
|
*/
|
|
294
334
|
scope<Name extends keyof Definitions & string>(name: Name, ...id: OwnerIdArgs<Definitions[Name]>): ScopedSettings<SettingsOf<Definitions[Name]>>;
|
|
295
335
|
};
|
|
@@ -302,7 +342,7 @@ declare function createSettings<Definitions extends Record<string, SettingsDefin
|
|
|
302
342
|
* write that read a version before a delete is a conflict; the Store remembers one number per Owner it ever stored.
|
|
303
343
|
*
|
|
304
344
|
* @example
|
|
305
|
-
* const settings = createSettings({ definitions: { user }, store: new MemoryStore() });
|
|
345
|
+
* const settings = createSettings({ definitions: { user: userDefinition }, store: new MemoryStore() });
|
|
306
346
|
*/
|
|
307
347
|
declare class MemoryStore implements SettingsStore {
|
|
308
348
|
#private;
|
|
@@ -344,17 +384,37 @@ interface SealingVault {
|
|
|
344
384
|
*/
|
|
345
385
|
isSealed?(value: unknown): boolean;
|
|
346
386
|
}
|
|
347
|
-
/**
|
|
348
|
-
|
|
387
|
+
/**
|
|
388
|
+
* The paths to keep sealed, by Scope name, each checked against its Scope's settings type, such as
|
|
389
|
+
* `{ organization: ["smtp.password"] }`. A Scope may be left out.
|
|
390
|
+
*
|
|
391
|
+
* @typeParam Definitions - The Settings definitions by Scope name.
|
|
392
|
+
*/
|
|
393
|
+
type SealedPaths<Definitions extends Record<string, SettingsDefinition>> = {
|
|
394
|
+
readonly [Scope in keyof Definitions & string]?: readonly Path<SettingsOf<Definitions[Scope]>>[];
|
|
395
|
+
};
|
|
396
|
+
/**
|
|
397
|
+
* Options of {@link SealedStore}.
|
|
398
|
+
*
|
|
399
|
+
* @typeParam Definitions - The Settings definitions by Scope name, the same ones passed to `createSettings`.
|
|
400
|
+
*/
|
|
401
|
+
interface SealedStoreOptions<Definitions extends Record<string, SettingsDefinition>> {
|
|
349
402
|
/** The Store that keeps the Sealed values. */
|
|
350
403
|
readonly store: SettingsStore;
|
|
351
404
|
/** Seals and opens values; a Secret Vault `Vault` fits. */
|
|
352
405
|
readonly vault: SealingVault;
|
|
353
406
|
/**
|
|
354
|
-
*
|
|
355
|
-
*
|
|
407
|
+
* Each Scope's Settings definition, by Scope name: the same object passed to `createSettings`. Only used to check
|
|
408
|
+
* `paths`.
|
|
409
|
+
*/
|
|
410
|
+
readonly definitions: Definitions;
|
|
411
|
+
/**
|
|
412
|
+
* Dot paths into the settings to keep sealed, by Scope name, such as `{ organization: ["smtp.password"] }`. A path
|
|
413
|
+
* may name an object, which is sealed whole as JSON. No path may be inside another of the same Scope; two Scopes may
|
|
414
|
+
* list the same path. A Scope left out has nothing sealed: a Sealed value stored there reads back as its raw
|
|
415
|
+
* `vault:…` string.
|
|
356
416
|
*/
|
|
357
|
-
readonly paths:
|
|
417
|
+
readonly paths: NoInfer<SealedPaths<Definitions>>;
|
|
358
418
|
}
|
|
359
419
|
/**
|
|
360
420
|
* A Store that keeps the Overrides at the listed paths as Sealed values in the Store it wraps, and hands the core
|
|
@@ -365,26 +425,53 @@ interface SealedStoreOptions {
|
|
|
365
425
|
* replace the secret with its default. A plain value at a listed path reads as it is and is sealed on the Owner's next
|
|
366
426
|
* write. Values at unlisted paths are never opened.
|
|
367
427
|
*
|
|
428
|
+
* Paths are listed per Scope and checked against each Scope's settings type, so a misspelled path doesn't compile. Only
|
|
429
|
+
* the paths listed for an Owner's Scope are sealed and opened; the same path in another Scope stays plain.
|
|
430
|
+
*
|
|
368
431
|
* Nest it outermost, around a {@link CachedStore}, so the cache holds only Sealed values.
|
|
369
432
|
*
|
|
433
|
+
* @typeParam Definitions - The Settings definitions by Scope name.
|
|
434
|
+
*
|
|
370
435
|
* @example
|
|
371
436
|
* const store = new SealedStore({
|
|
372
437
|
* store: new PrismaStore({ delegate: prisma.setting }),
|
|
373
438
|
* vault,
|
|
374
|
-
*
|
|
439
|
+
* definitions: { user, organization },
|
|
440
|
+
* paths: { organization: ["smtp.password"] },
|
|
375
441
|
* });
|
|
376
442
|
*/
|
|
377
|
-
declare class SealedStore implements SettingsStore {
|
|
443
|
+
declare class SealedStore<Definitions extends Record<string, SettingsDefinition> = Record<string, SettingsDefinition>> implements SettingsStore {
|
|
378
444
|
#private;
|
|
379
445
|
/**
|
|
380
|
-
* @param options - The Store to wrap, the Vault and the sealed paths.
|
|
381
|
-
* @throws `TypeError` when a path is empty or
|
|
446
|
+
* @param options - The Store to wrap, the Vault, the Settings definitions and the sealed paths by Scope.
|
|
447
|
+
* @throws `TypeError` when no Scope lists a path, a Scope in `paths` isn't in `definitions`, a path is empty, or a
|
|
448
|
+
* path is inside another of the same Scope.
|
|
382
449
|
*/
|
|
383
|
-
constructor(options: SealedStoreOptions);
|
|
450
|
+
constructor(options: SealedStoreOptions<Definitions>);
|
|
384
451
|
/** {@inheritDoc SettingsStore.read} */
|
|
385
452
|
read(owner: Owner): Promise<StoredOverrides | null>;
|
|
386
453
|
/** {@inheritDoc SettingsStore.write} */
|
|
387
454
|
write(owner: Owner, overrides: Record<string, unknown>, expectedVersion: number | null): Promise<number | null>;
|
|
455
|
+
/**
|
|
456
|
+
* Removes one Sealed value that won't open, so the Owner can read its settings and set a new secret again; its
|
|
457
|
+
* other Sealed values stay sealed and aren't opened, and a plain value at another listed path is sealed, as on any
|
|
458
|
+
* write. Its other Overrides stay as they are. A value that opens, a
|
|
459
|
+
* plain value and an empty path are left alone, so running it again after the Owner set a new secret changes
|
|
460
|
+
* nothing. Any rejection from `decryptJson` counts as not opening.
|
|
461
|
+
*
|
|
462
|
+
* **Check the Vault secret first.** With a wrong or missing secret every Sealed value fails to open, so `discard`
|
|
463
|
+
* would remove secrets that fixing the configuration would have recovered.
|
|
464
|
+
*
|
|
465
|
+
* It reads the wrapped Store and writes with the version it read. On a conflict it reads again and retries, up to 3
|
|
466
|
+
* times; a {@link CachedStore} underneath evicts on the conflict, so the retry reads fresh Overrides.
|
|
467
|
+
*
|
|
468
|
+
* @param owner - The Owner whose Sealed value to remove.
|
|
469
|
+
* @param path - A path listed for the Owner's Scope, such as `"smtp.password"`.
|
|
470
|
+
* @returns `true` when a Sealed value that won't open was removed, `false` when nothing was changed.
|
|
471
|
+
* @throws `TypeError` when `path` isn't listed for the Owner's Scope.
|
|
472
|
+
* @throws {@link SettingsConflictError} when every write lost the version race to another write.
|
|
473
|
+
*/
|
|
474
|
+
discard(owner: Owner, path: string): Promise<boolean>;
|
|
388
475
|
/** {@inheritDoc SettingsStore.delete} */
|
|
389
476
|
delete(owner: Owner): Promise<void>;
|
|
390
477
|
}
|
|
@@ -426,17 +513,25 @@ interface CachedStoreOptions {
|
|
|
426
513
|
readonly ttl: number;
|
|
427
514
|
/** Starts every cache key. Defaults to `"settings"`. */
|
|
428
515
|
readonly prefix?: string;
|
|
429
|
-
/**
|
|
516
|
+
/**
|
|
517
|
+
* Called with an error a cache method threw; the Store then answers instead.
|
|
518
|
+
*
|
|
519
|
+
* It isn't awaited, and it can't change the result of the read, write or delete that calls it: if it throws or
|
|
520
|
+
* returns a rejected promise, the operation goes on as if it had succeeded, so a write still returns the wrapped
|
|
521
|
+
* Store's result. The failure goes to `console.warn` the first time only for each {@link CachedStore}.
|
|
522
|
+
*/
|
|
430
523
|
readonly onError?: (error: unknown) => void;
|
|
431
524
|
}
|
|
432
525
|
/**
|
|
433
526
|
* A Store that keeps what the Store it wraps returns in a cache for `ttl` seconds, an Owner with nothing stored
|
|
434
|
-
* included. Every write, conflict and delete evicts the Owner, so a write
|
|
435
|
-
*
|
|
436
|
-
*
|
|
527
|
+
* included. Every write, conflict and delete evicts the Owner, so a conflicting write retries against fresh Overrides,
|
|
528
|
+
* and a write on this instance is read back at once by this instance, even when one of its reads of that Owner was in
|
|
529
|
+
* flight during the write: a read that an eviction overtook returns what it read but doesn't cache it. A cache that
|
|
530
|
+
* throws is reported to `onError` and the Store answers instead.
|
|
437
531
|
*
|
|
438
532
|
* With a per-process cache and several instances, another instance can read Overrides up to `ttl` seconds old. Share
|
|
439
|
-
* a cache, such as Redis, to avoid it.
|
|
533
|
+
* a cache, such as Redis, to avoid most of it. Even then, a read on another instance that was in flight during the
|
|
534
|
+
* write can still cache the old Overrides, which are then read for up to `ttl` seconds.
|
|
440
535
|
*
|
|
441
536
|
* @example
|
|
442
537
|
* const store = new CachedStore({ store: prismaStore, cache: new MemoryCache(), ttl: 60 });
|
|
@@ -527,4 +622,4 @@ declare class SettingsDefinitionError extends Error {
|
|
|
527
622
|
constructor(message: string, scope: string, issues?: ReadonlyArray<StandardSchemaV1.Issue>);
|
|
528
623
|
}
|
|
529
624
|
|
|
530
|
-
export { CachedStore, type CachedStoreOptions, MemoryCache, type MemoryCacheOptions, MemoryStore, Owner, type Path, type PathValue, type ScopedSettings, SealedStore, type SealedStoreOptions, type SealingVault, type SettingsCache, SettingsConflictError, type SettingsDefinition, SettingsDefinitionError, type SettingsOptions, SettingsStore, SettingsValidationError, StoredOverrides, createSettings, defineSettings };
|
|
625
|
+
export { CachedStore, type CachedStoreOptions, MemoryCache, type MemoryCacheOptions, MemoryStore, Owner, type Path, type PathValue, type ScopedSettings, type SealedPaths, SealedStore, type SealedStoreOptions, type SealingVault, type SettingsCache, SettingsConflictError, type SettingsDefinition, SettingsDefinitionError, type SettingsOptions, SettingsStore, SettingsValidationError, StoredOverrides, createSettings, defineSettings };
|
package/dist/index.d.ts
CHANGED
|
@@ -76,11 +76,43 @@ interface SettingsDefinition<T = unknown, SingleOwner extends boolean = boolean>
|
|
|
76
76
|
/** Whether the Scope has exactly one Owner, stored with the id `"default"`. */
|
|
77
77
|
readonly singleOwner: SingleOwner;
|
|
78
78
|
}
|
|
79
|
+
/** The settings type of a Settings definition. Internal: not exported from the package entry. */
|
|
80
|
+
type SettingsOf<D> = D extends SettingsDefinition<infer T> ? T : never;
|
|
81
|
+
/** Counts down the depth {@link NonJsonPaths} still walks, so a recursive settings type can't recurse forever. */
|
|
82
|
+
type Depth = [never, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9];
|
|
83
|
+
/** Whether an object type has a method, as a `Date`, `Map`, `Set` or class instance does. */
|
|
84
|
+
type HasMethod<T> = true extends {
|
|
85
|
+
[K in keyof T]-?: 0 extends 1 & T[K] ? false : T[K] extends (...args: never[]) => unknown ? true : false;
|
|
86
|
+
}[keyof T] ? true : false;
|
|
87
|
+
/**
|
|
88
|
+
* The dot paths in `T` whose values can't be kept as JSON, such as a `Date`, `Map`, `Set`, `bigint` or class
|
|
89
|
+
* instance; `never` when every value can. An array's elements are named `path[]`. `unknown` and `any` pass, since
|
|
90
|
+
* the runtime check covers them, and so does anything deeper than the walk goes.
|
|
91
|
+
*/
|
|
92
|
+
type NonJsonPaths<T, P extends string, D extends number = 10> = [D] extends [
|
|
93
|
+
never
|
|
94
|
+
] ? never : 0 extends 1 & T ? never : unknown extends T ? never : T extends string | number | boolean | null | undefined ? never : T extends bigint | symbol | ((...args: never[]) => unknown) ? P : T extends readonly (infer E)[] ? NonJsonPaths<E, `${P}[]`, Depth[D]> : T extends object ? HasMethod<T> extends true ? P : {
|
|
95
|
+
[K in keyof T & string]-?: NonJsonPaths<T[K], P extends "" ? K : `${P}.${K}`, Depth[D]>;
|
|
96
|
+
}[keyof T & string] : P;
|
|
97
|
+
/**
|
|
98
|
+
* Nothing extra when the schema's output can be kept as JSON. Otherwise a property no schema has, whose name states
|
|
99
|
+
* the rule and whose type names the offending paths, so the compile error at `schema` reads as the reason.
|
|
100
|
+
*/
|
|
101
|
+
type JsonOutputCheck<Schema extends StandardSchemaV1> = [
|
|
102
|
+
NonJsonPaths<StandardSchemaV1.InferOutput<Schema>, "">
|
|
103
|
+
] extends [never] ? unknown : {
|
|
104
|
+
readonly "Not a JSON value, so it can't be stored at": NonJsonPaths<StandardSchemaV1.InferOutput<Schema>, ""> extends "" ? "(the whole settings)" : NonJsonPaths<StandardSchemaV1.InferOutput<Schema>, "">;
|
|
105
|
+
};
|
|
79
106
|
/**
|
|
80
107
|
* Declares a Scope's Settings definition. The settings type is inferred from the schema's output type, and
|
|
81
108
|
* `defaults` must be a complete value of it; optional schema fields may be left out. Any Standard Schema works, such
|
|
82
109
|
* as one from Zod, Valibot or ArkType.
|
|
83
110
|
*
|
|
111
|
+
* Overrides are kept as JSON, so a schema whose output has a value that isn't, such as a `Date` from
|
|
112
|
+
* `z.coerce.date()`, a `Map`, a `Set`, a `bigint` or a class instance, is a compile error at `schema` that names the
|
|
113
|
+
* path. Store a date as its string and convert it where the setting is used. Output typed `unknown` or `any` passes;
|
|
114
|
+
* writes still check their values at runtime.
|
|
115
|
+
*
|
|
84
116
|
* The defaults are checked against the schema on the first operation on the Scope, because Standard Schema
|
|
85
117
|
* validation may be async; `createSettings` throws `SettingsDefinitionError` there if they fail.
|
|
86
118
|
*
|
|
@@ -88,27 +120,27 @@ interface SettingsDefinition<T = unknown, SingleOwner extends boolean = boolean>
|
|
|
88
120
|
* @returns The Settings definition, to pass to `createSettings` under its Scope name.
|
|
89
121
|
*
|
|
90
122
|
* @example
|
|
91
|
-
* const
|
|
123
|
+
* const userDefinition = defineSettings({
|
|
92
124
|
* schema: z.object({ theme: z.enum(["light", "dark"]), language: z.string() }),
|
|
93
125
|
* defaults: { theme: "light", language: "en" },
|
|
94
126
|
* });
|
|
95
|
-
* const
|
|
127
|
+
* const globalDefinition = defineSettings({
|
|
96
128
|
* schema: z.object({ maintenanceBanner: z.string().nullable() }),
|
|
97
129
|
* defaults: { maintenanceBanner: null },
|
|
98
130
|
* singleOwner: true,
|
|
99
131
|
* });
|
|
100
132
|
*/
|
|
101
133
|
declare function defineSettings<Schema extends StandardSchemaV1, SingleOwner extends boolean = false>(definition: {
|
|
102
|
-
/** The Standard Schema every Owner's Effective settings must pass. */
|
|
103
|
-
readonly schema: Schema
|
|
134
|
+
/** The Standard Schema every Owner's Effective settings must pass. Its output must be JSON. */
|
|
135
|
+
readonly schema: Schema & JsonOutputCheck<Schema>;
|
|
104
136
|
/** The complete settings used where an Owner has no Override. */
|
|
105
137
|
readonly defaults: StandardSchemaV1.InferOutput<Schema>;
|
|
106
138
|
/** `true` for a Scope with exactly one Owner, which takes no id. Defaults to `false`. */
|
|
107
139
|
readonly singleOwner?: SingleOwner;
|
|
108
140
|
}): SettingsDefinition<StandardSchemaV1.InferOutput<Schema>, SingleOwner>;
|
|
109
141
|
|
|
110
|
-
/** Objects whose keys a dot path walks through; arrays
|
|
111
|
-
type Walkable<T> = T extends readonly unknown[] |
|
|
142
|
+
/** Objects whose keys a dot path walks through; arrays and functions are values, never walked into. */
|
|
143
|
+
type Walkable<T> = T extends readonly unknown[] | ((...args: never[]) => unknown) ? never : T extends object ? T : never;
|
|
112
144
|
/**
|
|
113
145
|
* The union of dot paths through the nested plain objects of `T`, such as `"theme" | "notifications" |
|
|
114
146
|
* "notifications.email"`. A path never descends into an array: an array is one value, set and reset as a whole.
|
|
@@ -128,11 +160,11 @@ type Path<T> = [Walkable<T>] extends [never] ? never : {
|
|
|
128
160
|
type PathValue<T, P extends string> = P extends keyof Walkable<T> ? Walkable<T>[P] : P extends `${infer K}.${infer Rest}` ? K extends keyof Walkable<T> ? PathValue<NonNullable<Walkable<T>[K]>, Rest> | (null extends Walkable<T>[K] ? undefined : never) | (undefined extends Walkable<T>[K] ? undefined : never) : never : never;
|
|
129
161
|
/**
|
|
130
162
|
* A partial of `T` at any depth, for `update`: every key of a nested plain object may be left out or `undefined`.
|
|
131
|
-
* Arrays
|
|
163
|
+
* Arrays and functions are whole values, never partial.
|
|
132
164
|
*
|
|
133
165
|
* @typeParam T - The settings type.
|
|
134
166
|
*/
|
|
135
|
-
type DeepPartial<T> = T extends readonly unknown[] |
|
|
167
|
+
type DeepPartial<T> = T extends readonly unknown[] | ((...args: never[]) => unknown) ? T : T extends object ? {
|
|
136
168
|
[K in keyof T]?: DeepPartial<T[K]> | undefined;
|
|
137
169
|
} : T;
|
|
138
170
|
|
|
@@ -152,6 +184,10 @@ interface SettingsOptions<Definitions extends Record<string, SettingsDefinition>
|
|
|
152
184
|
* next successful write drops them. Writes read the Overrides first, so it can also be called during a write, again
|
|
153
185
|
* on each retry. Without it, ignored Overrides are dropped silently.
|
|
154
186
|
*
|
|
187
|
+
* It isn't awaited, and it can't change the result of the read or write that calls it: if it throws or returns a
|
|
188
|
+
* rejected promise, the operation goes on as if it had succeeded, and the failure goes to `console.warn` the first
|
|
189
|
+
* time only for each {@link createSettings} result.
|
|
190
|
+
*
|
|
155
191
|
* @param owner - The Owner whose Overrides were ignored.
|
|
156
192
|
* @param issues - The Standard Schema issues the Effective settings failed with, naming the ignored paths.
|
|
157
193
|
*/
|
|
@@ -259,8 +295,6 @@ interface ScopedSettings<T> {
|
|
|
259
295
|
*/
|
|
260
296
|
reset(): Promise<void>;
|
|
261
297
|
}
|
|
262
|
-
/** The settings type of a Settings definition. */
|
|
263
|
-
type SettingsOf<D> = D extends SettingsDefinition<infer T> ? T : never;
|
|
264
298
|
/** The id argument of `scope()`: none for a single-owner Scope, a string for any other. */
|
|
265
299
|
type OwnerIdArgs<D> = D extends SettingsDefinition<unknown, true> ? [] : [id: string];
|
|
266
300
|
/**
|
|
@@ -279,7 +313,10 @@ type OwnerIdArgs<D> = D extends SettingsDefinition<unknown, true> ? [] : [id: st
|
|
|
279
313
|
* @throws `RangeError` If `maxRetries` isn't a whole number, 0 or more.
|
|
280
314
|
*
|
|
281
315
|
* @example
|
|
282
|
-
* const settings = createSettings({
|
|
316
|
+
* const settings = createSettings({
|
|
317
|
+
* definitions: { user: userDefinition, global: globalDefinition },
|
|
318
|
+
* store: new MemoryStore(),
|
|
319
|
+
* });
|
|
283
320
|
* const theme = await settings.scope("user", "1").get("theme");
|
|
284
321
|
* await settings.scope("global").set("maintenanceBanner", "Back at 10:00");
|
|
285
322
|
*/
|
|
@@ -288,8 +325,11 @@ declare function createSettings<Definitions extends Record<string, SettingsDefin
|
|
|
288
325
|
* Returns the settings of one Owner of the Scope `name`.
|
|
289
326
|
*
|
|
290
327
|
* @param name - The Scope name, a key of `definitions`.
|
|
291
|
-
* @param id - The Owner's id; left out for a single-owner Scope.
|
|
328
|
+
* @param id - The Owner's id, any non-empty string; left out for a single-owner Scope. The package doesn't interpret
|
|
329
|
+
* it, so ids that differ in any way, such as `"1"` and `" 1"`, are different Owners.
|
|
292
330
|
* @throws {@link SettingsDefinitionError} If `name` isn't a key of `definitions`.
|
|
331
|
+
* @throws `TypeError` If `id` is the empty string, is missing for a normal Scope, or is given for a single-owner
|
|
332
|
+
* Scope.
|
|
293
333
|
*/
|
|
294
334
|
scope<Name extends keyof Definitions & string>(name: Name, ...id: OwnerIdArgs<Definitions[Name]>): ScopedSettings<SettingsOf<Definitions[Name]>>;
|
|
295
335
|
};
|
|
@@ -302,7 +342,7 @@ declare function createSettings<Definitions extends Record<string, SettingsDefin
|
|
|
302
342
|
* write that read a version before a delete is a conflict; the Store remembers one number per Owner it ever stored.
|
|
303
343
|
*
|
|
304
344
|
* @example
|
|
305
|
-
* const settings = createSettings({ definitions: { user }, store: new MemoryStore() });
|
|
345
|
+
* const settings = createSettings({ definitions: { user: userDefinition }, store: new MemoryStore() });
|
|
306
346
|
*/
|
|
307
347
|
declare class MemoryStore implements SettingsStore {
|
|
308
348
|
#private;
|
|
@@ -344,17 +384,37 @@ interface SealingVault {
|
|
|
344
384
|
*/
|
|
345
385
|
isSealed?(value: unknown): boolean;
|
|
346
386
|
}
|
|
347
|
-
/**
|
|
348
|
-
|
|
387
|
+
/**
|
|
388
|
+
* The paths to keep sealed, by Scope name, each checked against its Scope's settings type, such as
|
|
389
|
+
* `{ organization: ["smtp.password"] }`. A Scope may be left out.
|
|
390
|
+
*
|
|
391
|
+
* @typeParam Definitions - The Settings definitions by Scope name.
|
|
392
|
+
*/
|
|
393
|
+
type SealedPaths<Definitions extends Record<string, SettingsDefinition>> = {
|
|
394
|
+
readonly [Scope in keyof Definitions & string]?: readonly Path<SettingsOf<Definitions[Scope]>>[];
|
|
395
|
+
};
|
|
396
|
+
/**
|
|
397
|
+
* Options of {@link SealedStore}.
|
|
398
|
+
*
|
|
399
|
+
* @typeParam Definitions - The Settings definitions by Scope name, the same ones passed to `createSettings`.
|
|
400
|
+
*/
|
|
401
|
+
interface SealedStoreOptions<Definitions extends Record<string, SettingsDefinition>> {
|
|
349
402
|
/** The Store that keeps the Sealed values. */
|
|
350
403
|
readonly store: SettingsStore;
|
|
351
404
|
/** Seals and opens values; a Secret Vault `Vault` fits. */
|
|
352
405
|
readonly vault: SealingVault;
|
|
353
406
|
/**
|
|
354
|
-
*
|
|
355
|
-
*
|
|
407
|
+
* Each Scope's Settings definition, by Scope name: the same object passed to `createSettings`. Only used to check
|
|
408
|
+
* `paths`.
|
|
409
|
+
*/
|
|
410
|
+
readonly definitions: Definitions;
|
|
411
|
+
/**
|
|
412
|
+
* Dot paths into the settings to keep sealed, by Scope name, such as `{ organization: ["smtp.password"] }`. A path
|
|
413
|
+
* may name an object, which is sealed whole as JSON. No path may be inside another of the same Scope; two Scopes may
|
|
414
|
+
* list the same path. A Scope left out has nothing sealed: a Sealed value stored there reads back as its raw
|
|
415
|
+
* `vault:…` string.
|
|
356
416
|
*/
|
|
357
|
-
readonly paths:
|
|
417
|
+
readonly paths: NoInfer<SealedPaths<Definitions>>;
|
|
358
418
|
}
|
|
359
419
|
/**
|
|
360
420
|
* A Store that keeps the Overrides at the listed paths as Sealed values in the Store it wraps, and hands the core
|
|
@@ -365,26 +425,53 @@ interface SealedStoreOptions {
|
|
|
365
425
|
* replace the secret with its default. A plain value at a listed path reads as it is and is sealed on the Owner's next
|
|
366
426
|
* write. Values at unlisted paths are never opened.
|
|
367
427
|
*
|
|
428
|
+
* Paths are listed per Scope and checked against each Scope's settings type, so a misspelled path doesn't compile. Only
|
|
429
|
+
* the paths listed for an Owner's Scope are sealed and opened; the same path in another Scope stays plain.
|
|
430
|
+
*
|
|
368
431
|
* Nest it outermost, around a {@link CachedStore}, so the cache holds only Sealed values.
|
|
369
432
|
*
|
|
433
|
+
* @typeParam Definitions - The Settings definitions by Scope name.
|
|
434
|
+
*
|
|
370
435
|
* @example
|
|
371
436
|
* const store = new SealedStore({
|
|
372
437
|
* store: new PrismaStore({ delegate: prisma.setting }),
|
|
373
438
|
* vault,
|
|
374
|
-
*
|
|
439
|
+
* definitions: { user, organization },
|
|
440
|
+
* paths: { organization: ["smtp.password"] },
|
|
375
441
|
* });
|
|
376
442
|
*/
|
|
377
|
-
declare class SealedStore implements SettingsStore {
|
|
443
|
+
declare class SealedStore<Definitions extends Record<string, SettingsDefinition> = Record<string, SettingsDefinition>> implements SettingsStore {
|
|
378
444
|
#private;
|
|
379
445
|
/**
|
|
380
|
-
* @param options - The Store to wrap, the Vault and the sealed paths.
|
|
381
|
-
* @throws `TypeError` when a path is empty or
|
|
446
|
+
* @param options - The Store to wrap, the Vault, the Settings definitions and the sealed paths by Scope.
|
|
447
|
+
* @throws `TypeError` when no Scope lists a path, a Scope in `paths` isn't in `definitions`, a path is empty, or a
|
|
448
|
+
* path is inside another of the same Scope.
|
|
382
449
|
*/
|
|
383
|
-
constructor(options: SealedStoreOptions);
|
|
450
|
+
constructor(options: SealedStoreOptions<Definitions>);
|
|
384
451
|
/** {@inheritDoc SettingsStore.read} */
|
|
385
452
|
read(owner: Owner): Promise<StoredOverrides | null>;
|
|
386
453
|
/** {@inheritDoc SettingsStore.write} */
|
|
387
454
|
write(owner: Owner, overrides: Record<string, unknown>, expectedVersion: number | null): Promise<number | null>;
|
|
455
|
+
/**
|
|
456
|
+
* Removes one Sealed value that won't open, so the Owner can read its settings and set a new secret again; its
|
|
457
|
+
* other Sealed values stay sealed and aren't opened, and a plain value at another listed path is sealed, as on any
|
|
458
|
+
* write. Its other Overrides stay as they are. A value that opens, a
|
|
459
|
+
* plain value and an empty path are left alone, so running it again after the Owner set a new secret changes
|
|
460
|
+
* nothing. Any rejection from `decryptJson` counts as not opening.
|
|
461
|
+
*
|
|
462
|
+
* **Check the Vault secret first.** With a wrong or missing secret every Sealed value fails to open, so `discard`
|
|
463
|
+
* would remove secrets that fixing the configuration would have recovered.
|
|
464
|
+
*
|
|
465
|
+
* It reads the wrapped Store and writes with the version it read. On a conflict it reads again and retries, up to 3
|
|
466
|
+
* times; a {@link CachedStore} underneath evicts on the conflict, so the retry reads fresh Overrides.
|
|
467
|
+
*
|
|
468
|
+
* @param owner - The Owner whose Sealed value to remove.
|
|
469
|
+
* @param path - A path listed for the Owner's Scope, such as `"smtp.password"`.
|
|
470
|
+
* @returns `true` when a Sealed value that won't open was removed, `false` when nothing was changed.
|
|
471
|
+
* @throws `TypeError` when `path` isn't listed for the Owner's Scope.
|
|
472
|
+
* @throws {@link SettingsConflictError} when every write lost the version race to another write.
|
|
473
|
+
*/
|
|
474
|
+
discard(owner: Owner, path: string): Promise<boolean>;
|
|
388
475
|
/** {@inheritDoc SettingsStore.delete} */
|
|
389
476
|
delete(owner: Owner): Promise<void>;
|
|
390
477
|
}
|
|
@@ -426,17 +513,25 @@ interface CachedStoreOptions {
|
|
|
426
513
|
readonly ttl: number;
|
|
427
514
|
/** Starts every cache key. Defaults to `"settings"`. */
|
|
428
515
|
readonly prefix?: string;
|
|
429
|
-
/**
|
|
516
|
+
/**
|
|
517
|
+
* Called with an error a cache method threw; the Store then answers instead.
|
|
518
|
+
*
|
|
519
|
+
* It isn't awaited, and it can't change the result of the read, write or delete that calls it: if it throws or
|
|
520
|
+
* returns a rejected promise, the operation goes on as if it had succeeded, so a write still returns the wrapped
|
|
521
|
+
* Store's result. The failure goes to `console.warn` the first time only for each {@link CachedStore}.
|
|
522
|
+
*/
|
|
430
523
|
readonly onError?: (error: unknown) => void;
|
|
431
524
|
}
|
|
432
525
|
/**
|
|
433
526
|
* A Store that keeps what the Store it wraps returns in a cache for `ttl` seconds, an Owner with nothing stored
|
|
434
|
-
* included. Every write, conflict and delete evicts the Owner, so a write
|
|
435
|
-
*
|
|
436
|
-
*
|
|
527
|
+
* included. Every write, conflict and delete evicts the Owner, so a conflicting write retries against fresh Overrides,
|
|
528
|
+
* and a write on this instance is read back at once by this instance, even when one of its reads of that Owner was in
|
|
529
|
+
* flight during the write: a read that an eviction overtook returns what it read but doesn't cache it. A cache that
|
|
530
|
+
* throws is reported to `onError` and the Store answers instead.
|
|
437
531
|
*
|
|
438
532
|
* With a per-process cache and several instances, another instance can read Overrides up to `ttl` seconds old. Share
|
|
439
|
-
* a cache, such as Redis, to avoid it.
|
|
533
|
+
* a cache, such as Redis, to avoid most of it. Even then, a read on another instance that was in flight during the
|
|
534
|
+
* write can still cache the old Overrides, which are then read for up to `ttl` seconds.
|
|
440
535
|
*
|
|
441
536
|
* @example
|
|
442
537
|
* const store = new CachedStore({ store: prismaStore, cache: new MemoryCache(), ttl: 60 });
|
|
@@ -527,4 +622,4 @@ declare class SettingsDefinitionError extends Error {
|
|
|
527
622
|
constructor(message: string, scope: string, issues?: ReadonlyArray<StandardSchemaV1.Issue>);
|
|
528
623
|
}
|
|
529
624
|
|
|
530
|
-
export { CachedStore, type CachedStoreOptions, MemoryCache, type MemoryCacheOptions, MemoryStore, Owner, type Path, type PathValue, type ScopedSettings, SealedStore, type SealedStoreOptions, type SealingVault, type SettingsCache, SettingsConflictError, type SettingsDefinition, SettingsDefinitionError, type SettingsOptions, SettingsStore, SettingsValidationError, StoredOverrides, createSettings, defineSettings };
|
|
625
|
+
export { CachedStore, type CachedStoreOptions, MemoryCache, type MemoryCacheOptions, MemoryStore, Owner, type Path, type PathValue, type ScopedSettings, type SealedPaths, SealedStore, type SealedStoreOptions, type SealingVault, type SettingsCache, SettingsConflictError, type SettingsDefinition, SettingsDefinitionError, type SettingsOptions, SettingsStore, SettingsValidationError, StoredOverrides, createSettings, defineSettings };
|