@crustjs/store 0.0.3 → 0.0.4
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 +106 -77
- package/dist/index.d.ts +179 -146
- package/dist/index.js +3 -3
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
DX-first, typed persistence for CLI apps with clear config/data/state/cache separation.
|
|
4
4
|
|
|
5
|
-
`@crustjs/store` gives your CLI production-ready local persistence with near-zero setup. Provide a
|
|
5
|
+
`@crustjs/store` gives your CLI production-ready local persistence with near-zero setup. Provide a `fields` definition, pick a storage intent, and get a typed store with read/write/update/patch/reset — no manual type annotations needed.
|
|
6
6
|
|
|
7
7
|
## Install
|
|
8
8
|
|
|
@@ -26,30 +26,31 @@ import { createStore, configDir } from "@crustjs/store";
|
|
|
26
26
|
|
|
27
27
|
const store = createStore({
|
|
28
28
|
dirPath: configDir("my-cli"),
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
29
|
+
fields: {
|
|
30
|
+
theme: { type: "string", default: "light" },
|
|
31
|
+
fontSize: { type: "number", default: 14 },
|
|
32
|
+
verbose: { type: "boolean", default: false },
|
|
32
33
|
},
|
|
33
34
|
});
|
|
34
35
|
|
|
35
36
|
// Read state (returns defaults when no persisted file exists)
|
|
36
37
|
const state = await store.read();
|
|
37
|
-
// → {
|
|
38
|
+
// → { theme: "light", fontSize: 14, verbose: false }
|
|
38
39
|
|
|
39
40
|
// Write a full state object
|
|
40
|
-
await store.write({
|
|
41
|
+
await store.write({ theme: "dark", fontSize: 16, verbose: true });
|
|
41
42
|
|
|
42
43
|
// Update with a function
|
|
43
44
|
await store.update((current) => ({ ...current, verbose: false }));
|
|
44
45
|
|
|
45
|
-
// Patch only specific
|
|
46
|
-
await store.patch({
|
|
46
|
+
// Patch only specific keys
|
|
47
|
+
await store.patch({ theme: "solarized" });
|
|
47
48
|
|
|
48
49
|
// Reset to defaults (removes persisted file)
|
|
49
50
|
await store.reset();
|
|
50
51
|
```
|
|
51
52
|
|
|
52
|
-
No explicit generics needed — types are inferred from your `
|
|
53
|
+
No explicit generics needed — types are inferred from your `fields` definition.
|
|
53
54
|
|
|
54
55
|
## Storage Intent: Choosing a Directory
|
|
55
56
|
|
|
@@ -68,25 +69,36 @@ import { createStore, configDir, dataDir, stateDir, cacheDir } from "@crustjs/st
|
|
|
68
69
|
// User preferences → ~/.config/my-cli/config.json
|
|
69
70
|
const config = createStore({
|
|
70
71
|
dirPath: configDir("my-cli"),
|
|
71
|
-
|
|
72
|
+
fields: {
|
|
73
|
+
theme: { type: "string", default: "light" },
|
|
74
|
+
verbose: { type: "boolean", default: false },
|
|
75
|
+
},
|
|
72
76
|
});
|
|
73
77
|
|
|
74
78
|
// App data → ~/.local/share/my-cli/config.json
|
|
75
79
|
const data = createStore({
|
|
76
80
|
dirPath: dataDir("my-cli"),
|
|
77
|
-
|
|
81
|
+
fields: {
|
|
82
|
+
bookmarks: { type: "array", default: [] },
|
|
83
|
+
},
|
|
78
84
|
});
|
|
79
85
|
|
|
80
86
|
// Runtime state → ~/.local/state/my-cli/config.json
|
|
81
87
|
const state = createStore({
|
|
82
88
|
dirPath: stateDir("my-cli"),
|
|
83
|
-
|
|
89
|
+
fields: {
|
|
90
|
+
lastOpened: { type: "string", default: "" },
|
|
91
|
+
scrollY: { type: "number", default: 0 },
|
|
92
|
+
},
|
|
84
93
|
});
|
|
85
94
|
|
|
86
95
|
// Cache → ~/.cache/my-cli/config.json
|
|
87
96
|
const cache = createStore({
|
|
88
97
|
dirPath: cacheDir("my-cli"),
|
|
89
|
-
|
|
98
|
+
fields: {
|
|
99
|
+
etag: { type: "string", default: "" },
|
|
100
|
+
payload: { type: "string", default: "" },
|
|
101
|
+
},
|
|
90
102
|
});
|
|
91
103
|
```
|
|
92
104
|
|
|
@@ -123,13 +135,12 @@ const store = createStore(options);
|
|
|
123
135
|
| -------------- | -------------------------- | -------- | ------------------------------------------------------------------ |
|
|
124
136
|
| `dirPath` | `string` | Yes | Absolute directory path where the JSON file is stored. |
|
|
125
137
|
| `name` | `string` | No | Store name used as filename (default `"config"` → `config.json`). |
|
|
126
|
-
| `
|
|
127
|
-
| `validator` | `StoreValidator<T>` | No | Validation function applied on read, write, update, and patch. |
|
|
138
|
+
| `fields` | `FieldsDef` | Yes | Field definitions defining the store's data shape, types, defaults, and optional validation. |
|
|
128
139
|
| `pruneUnknown` | `boolean` | No | Drop unknown persisted keys on read (default `true`). |
|
|
129
140
|
|
|
130
141
|
### `store.read()`
|
|
131
142
|
|
|
132
|
-
Reads the persisted state file. Missing keys are filled from
|
|
143
|
+
Reads the persisted state file. Missing keys are filled from field `defaults`.
|
|
133
144
|
|
|
134
145
|
```ts
|
|
135
146
|
const state = await store.read();
|
|
@@ -142,10 +153,10 @@ Always returns a value — never `undefined`. Does not write merged defaults bac
|
|
|
142
153
|
Atomically persists a full state object. The entire previous state is replaced.
|
|
143
154
|
|
|
144
155
|
```ts
|
|
145
|
-
await store.write({
|
|
156
|
+
await store.write({ theme: "dark", fontSize: 14, verbose: true });
|
|
146
157
|
```
|
|
147
158
|
|
|
148
|
-
Calls `
|
|
159
|
+
Calls per-field `validate` functions (if provided) before writing. Parent directories are created if missing.
|
|
149
160
|
|
|
150
161
|
### `store.update(updater)`
|
|
151
162
|
|
|
@@ -154,21 +165,21 @@ Reads the current effective state, applies the updater function, and atomically
|
|
|
154
165
|
```ts
|
|
155
166
|
await store.update((current) => ({
|
|
156
167
|
...current,
|
|
157
|
-
|
|
168
|
+
theme: "dark",
|
|
158
169
|
}));
|
|
159
170
|
```
|
|
160
171
|
|
|
161
|
-
When no persisted file exists, the updater receives `defaults` as the current value. Calls `
|
|
172
|
+
When no persisted file exists, the updater receives field `defaults` as the current value. Calls per-field `validate` functions before writing.
|
|
162
173
|
|
|
163
174
|
### `store.patch(partial)`
|
|
164
175
|
|
|
165
|
-
Applies a
|
|
176
|
+
Applies a shallow partial update to the current state and persists. Only the provided keys are updated; everything else is preserved.
|
|
166
177
|
|
|
167
178
|
```ts
|
|
168
|
-
await store.patch({
|
|
179
|
+
await store.patch({ theme: "solarized" });
|
|
169
180
|
```
|
|
170
181
|
|
|
171
|
-
|
|
182
|
+
Calls per-field `validate` functions before writing.
|
|
172
183
|
|
|
173
184
|
### `store.reset()`
|
|
174
185
|
|
|
@@ -244,9 +255,10 @@ const dir = configDir("my-cli");
|
|
|
244
255
|
// Default store → ~/.config/my-cli/config.json
|
|
245
256
|
const settingsStore = createStore({
|
|
246
257
|
dirPath: dir,
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
258
|
+
fields: {
|
|
259
|
+
theme: { type: "string", default: "light" },
|
|
260
|
+
fontSize: { type: "number", default: 14 },
|
|
261
|
+
verbose: { type: "boolean", default: false },
|
|
250
262
|
},
|
|
251
263
|
});
|
|
252
264
|
|
|
@@ -254,7 +266,9 @@ const settingsStore = createStore({
|
|
|
254
266
|
const authStore = createStore({
|
|
255
267
|
dirPath: dir,
|
|
256
268
|
name: "auth",
|
|
257
|
-
|
|
269
|
+
fields: {
|
|
270
|
+
token: { type: "string", default: "" },
|
|
271
|
+
},
|
|
258
272
|
});
|
|
259
273
|
```
|
|
260
274
|
|
|
@@ -264,27 +278,27 @@ The `name` must not contain path separators or the `.json` extension.
|
|
|
264
278
|
|
|
265
279
|
## Defaults & Merge
|
|
266
280
|
|
|
267
|
-
When a persisted file exists, `read()`
|
|
281
|
+
When a persisted file exists, `read()` fills missing keys from field `defaults`:
|
|
268
282
|
|
|
269
283
|
```ts
|
|
270
284
|
const store = createStore({
|
|
271
285
|
dirPath: configDir("my-cli"),
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
286
|
+
fields: {
|
|
287
|
+
theme: { type: "string", default: "light" },
|
|
288
|
+
fontSize: { type: "number", default: 14 },
|
|
289
|
+
verbose: { type: "boolean", default: false },
|
|
275
290
|
},
|
|
276
291
|
});
|
|
277
292
|
|
|
278
|
-
// Persisted file contains: { "
|
|
293
|
+
// Persisted file contains: { "theme": "dark", "verbose": true }
|
|
279
294
|
const state = await store.read();
|
|
280
|
-
// → {
|
|
295
|
+
// → { theme: "dark", fontSize: 14, verbose: true }
|
|
281
296
|
```
|
|
282
297
|
|
|
283
298
|
### Merge Rules
|
|
284
299
|
|
|
285
|
-
- Persisted values override defaults
|
|
286
|
-
- Missing keys fall back to defaults
|
|
287
|
-
- Arrays are replaced wholesale — not merged element-by-element.
|
|
300
|
+
- Persisted values override defaults.
|
|
301
|
+
- Missing keys fall back to defaults.
|
|
288
302
|
- Unknown persisted keys are dropped by default (`pruneUnknown: true`). Set `pruneUnknown: false` to preserve them.
|
|
289
303
|
- All values are deep-cloned to prevent shared-reference mutation from defaults.
|
|
290
304
|
- Falsy values (`null`, `0`, `""`, `false`) in the persisted file are preserved — only truly missing keys trigger defaults.
|
|
@@ -295,26 +309,27 @@ Merged defaults exist only in memory. The persisted file remains unchanged until
|
|
|
295
309
|
|
|
296
310
|
## Validation
|
|
297
311
|
|
|
298
|
-
Add
|
|
312
|
+
Add per-field validation to enforce config integrity on every read, write, update, and patch. When a `validate` function is configured on a field, validation is **strict by default** — invalid values fail loudly.
|
|
299
313
|
|
|
300
314
|
### Using `@crustjs/validate`
|
|
301
315
|
|
|
302
|
-
The easiest way to add validation is with
|
|
316
|
+
The easiest way to add validation is with field adapters from `@crustjs/validate`:
|
|
303
317
|
|
|
304
318
|
```ts
|
|
305
319
|
import { z } from "zod";
|
|
306
|
-
import {
|
|
320
|
+
import { field } from "@crustjs/validate/zod";
|
|
307
321
|
import { createStore, configDir } from "@crustjs/store";
|
|
308
322
|
|
|
309
323
|
const store = createStore({
|
|
310
324
|
dirPath: configDir("my-cli"),
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
}
|
|
317
|
-
|
|
325
|
+
fields: {
|
|
326
|
+
theme: {
|
|
327
|
+
type: "string",
|
|
328
|
+
default: "light",
|
|
329
|
+
validate: field(z.enum(["light", "dark"])),
|
|
330
|
+
},
|
|
331
|
+
verbose: { type: "boolean", default: false },
|
|
332
|
+
},
|
|
318
333
|
});
|
|
319
334
|
|
|
320
335
|
// write() validates before persisting
|
|
@@ -326,52 +341,64 @@ const config = await store.read();
|
|
|
326
341
|
// → throws if persisted config is invalid
|
|
327
342
|
```
|
|
328
343
|
|
|
329
|
-
`
|
|
344
|
+
`fieldSync()` is also available for synchronous schemas.
|
|
330
345
|
|
|
331
346
|
For Effect schemas, wrap with `Schema.standardSchemaV1()`:
|
|
332
347
|
|
|
333
348
|
```ts
|
|
334
349
|
import * as Schema from "effect/Schema";
|
|
335
|
-
import {
|
|
350
|
+
import { field } from "@crustjs/validate/effect";
|
|
336
351
|
|
|
337
352
|
const store = createStore({
|
|
338
353
|
dirPath: configDir("my-cli"),
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
354
|
+
fields: {
|
|
355
|
+
theme: {
|
|
356
|
+
type: "string",
|
|
357
|
+
default: "light",
|
|
358
|
+
validate: field(
|
|
359
|
+
Schema.standardSchemaV1(
|
|
360
|
+
Schema.Literal("light", "dark"),
|
|
361
|
+
),
|
|
362
|
+
),
|
|
363
|
+
},
|
|
364
|
+
},
|
|
345
365
|
});
|
|
346
366
|
```
|
|
347
367
|
|
|
348
368
|
### Custom validators
|
|
349
369
|
|
|
350
|
-
You can also provide a validator function directly
|
|
370
|
+
You can also provide a validator function directly on each field:
|
|
351
371
|
|
|
352
372
|
```ts
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
}
|
|
373
|
+
const store = createStore({
|
|
374
|
+
dirPath: configDir("my-cli"),
|
|
375
|
+
fields: {
|
|
376
|
+
theme: {
|
|
377
|
+
type: "string",
|
|
378
|
+
default: "light",
|
|
379
|
+
validate(value) {
|
|
380
|
+
const str = value as string;
|
|
381
|
+
if (str !== "light" && str !== "dark") {
|
|
382
|
+
return {
|
|
383
|
+
ok: false,
|
|
384
|
+
issues: [{ message: 'Must be "light" or "dark"', path: "" }],
|
|
385
|
+
};
|
|
386
|
+
}
|
|
387
|
+
return { ok: true, value: str };
|
|
388
|
+
},
|
|
389
|
+
},
|
|
390
|
+
},
|
|
391
|
+
});
|
|
365
392
|
```
|
|
366
393
|
|
|
367
|
-
The `StoreValidator<T>` contract
|
|
394
|
+
The per-field `validate` function follows the `StoreValidator<T>` contract: `(value: unknown) => StoreValidatorResult<T> | Promise<StoreValidatorResult<T>>`, where `StoreValidatorResult<T>` is `{ ok: true, value: T } | { ok: false, issues: StoreValidatorIssue[] }`.
|
|
368
395
|
|
|
369
396
|
### Validation behavior
|
|
370
397
|
|
|
371
|
-
- **Write**: Validates before persisting. If
|
|
372
|
-
- **Read**: Validates after applying defaults. Invalid persisted config fails loudly — no silent fallback to defaults.
|
|
373
|
-
- **Update**: Reads raw config (no validation), applies updater, validates
|
|
374
|
-
- **Patch**: Reads raw config, applies
|
|
398
|
+
- **Write**: Validates each field before persisting. If a validator transforms the value, the transformed result is persisted.
|
|
399
|
+
- **Read**: Validates each field after applying defaults. Invalid persisted config fails loudly — no silent fallback to defaults.
|
|
400
|
+
- **Update**: Reads raw config (no validation), applies updater, validates each field, then persists.
|
|
401
|
+
- **Patch**: Reads raw config, applies shallow partial merge, validates each field, then persists.
|
|
375
402
|
- **Reset**: No validation — just removes the persisted file.
|
|
376
403
|
|
|
377
404
|
### Catching validation errors
|
|
@@ -459,10 +486,11 @@ import type {
|
|
|
459
486
|
CreateStoreOptions,
|
|
460
487
|
Store,
|
|
461
488
|
StoreUpdater,
|
|
489
|
+
FieldDef,
|
|
490
|
+
FieldsDef,
|
|
491
|
+
InferStoreConfig,
|
|
462
492
|
StoreValidator,
|
|
463
493
|
StoreValidatorResult,
|
|
464
|
-
StoreValidatorSuccess,
|
|
465
|
-
StoreValidatorFailure,
|
|
466
494
|
StoreValidatorIssue,
|
|
467
495
|
StoreErrorCode,
|
|
468
496
|
StoreValidationIssue,
|
|
@@ -476,10 +504,11 @@ import type {
|
|
|
476
504
|
| `CreateStoreOptions` | Options object for `createStore()`. |
|
|
477
505
|
| `Store` | Store instance with `read`, `write`, `update`, `patch`, `reset`. |
|
|
478
506
|
| `StoreUpdater` | Updater function type `(current: T) => T`. |
|
|
507
|
+
| `FieldDef` | Single field definition with `type`, optional `default`, and optional `validate`. |
|
|
508
|
+
| `FieldsDef` | Record of field names to `FieldDef` definitions. |
|
|
509
|
+
| `InferStoreConfig` | Inferred store state type from a `FieldsDef` definition. |
|
|
479
510
|
| `StoreValidator` | Validator function contract `(value: unknown) => StoreValidatorResult`. |
|
|
480
|
-
| `StoreValidatorResult` | Discriminated union: `
|
|
481
|
-
| `StoreValidatorSuccess` | `{ ok: true, value: T }`. |
|
|
482
|
-
| `StoreValidatorFailure` | `{ ok: false, issues: StoreValidatorIssue[] }`. |
|
|
511
|
+
| `StoreValidatorResult` | Discriminated union: `{ ok: true, value: T } \| { ok: false, issues: StoreValidatorIssue[] }`. |
|
|
483
512
|
| `StoreValidatorIssue` | `{ message: string, path: string }`. |
|
|
484
513
|
| `StoreValidationIssue` | Validation issue in error details payload. |
|
|
485
514
|
| `ValidationErrorDetails` | Error details for `VALIDATION` code: `{ operation, issues }`. |
|
package/dist/index.d.ts
CHANGED
|
@@ -1,94 +1,155 @@
|
|
|
1
|
+
/** Supported type literals for store fields. */
|
|
2
|
+
type ValueType = "string" | "number" | "boolean";
|
|
1
3
|
/**
|
|
2
|
-
*
|
|
4
|
+
* Resolves a type literal to its corresponding TypeScript primitive type.
|
|
3
5
|
*
|
|
4
|
-
* -
|
|
5
|
-
* -
|
|
6
|
-
* -
|
|
6
|
+
* - `"string"` → `string`
|
|
7
|
+
* - `"number"` → `number`
|
|
8
|
+
* - `"boolean"` → `boolean`
|
|
9
|
+
*/
|
|
10
|
+
type ResolvePrimitive<T extends ValueType> = T extends "string" ? string : T extends "number" ? number : T extends "boolean" ? boolean : never;
|
|
11
|
+
/** Shared fields present on every store field definition. */
|
|
12
|
+
interface FieldDefBase<V> {
|
|
13
|
+
/** Human-readable description for documentation and tooling. */
|
|
14
|
+
description?: string;
|
|
15
|
+
/**
|
|
16
|
+
* Optional per-field validation function.
|
|
17
|
+
*
|
|
18
|
+
* Called during `read`, `write`, `update`, and `patch` operations when
|
|
19
|
+
* the field has a value (not `undefined`). Throw an error to reject the
|
|
20
|
+
* value.
|
|
21
|
+
*
|
|
22
|
+
* @param value - The field value to validate.
|
|
23
|
+
* @throws When the value is invalid — the error message is captured as a
|
|
24
|
+
* validation issue with the field name as `path`.
|
|
25
|
+
*/
|
|
26
|
+
validate?: (value: V) => void | Promise<void>;
|
|
27
|
+
}
|
|
28
|
+
/** Base for scalar (non-array) fields — `array` must be omitted. */
|
|
29
|
+
interface ScalarFieldBase<V> extends FieldDefBase<V> {
|
|
30
|
+
/** Must be omitted for scalar fields — set to `true` for array fields. */
|
|
31
|
+
array?: never;
|
|
32
|
+
}
|
|
33
|
+
/** A scalar string field. */
|
|
34
|
+
interface StringFieldDef extends ScalarFieldBase<string> {
|
|
35
|
+
type: "string";
|
|
36
|
+
/** Default string value when the field is not persisted. */
|
|
37
|
+
default?: string;
|
|
38
|
+
}
|
|
39
|
+
/** A scalar number field. */
|
|
40
|
+
interface NumberFieldDef extends ScalarFieldBase<number> {
|
|
41
|
+
type: "number";
|
|
42
|
+
/** Default number value when the field is not persisted. */
|
|
43
|
+
default?: number;
|
|
44
|
+
}
|
|
45
|
+
/** A scalar boolean field. */
|
|
46
|
+
interface BooleanFieldDef extends ScalarFieldBase<boolean> {
|
|
47
|
+
type: "boolean";
|
|
48
|
+
/** Default boolean value when the field is not persisted. */
|
|
49
|
+
default?: boolean;
|
|
50
|
+
}
|
|
51
|
+
/** Base for array fields — `array` is required as `true`. */
|
|
52
|
+
interface ArrayFieldBase<V> extends FieldDefBase<V> {
|
|
53
|
+
/** Collect values into an array. */
|
|
54
|
+
array: true;
|
|
55
|
+
}
|
|
56
|
+
/** An array of strings field. */
|
|
57
|
+
interface StringArrayFieldDef extends ArrayFieldBase<string[]> {
|
|
58
|
+
type: "string";
|
|
59
|
+
/** Default string array value when the field is not persisted. */
|
|
60
|
+
default?: readonly string[];
|
|
61
|
+
}
|
|
62
|
+
/** An array of numbers field. */
|
|
63
|
+
interface NumberArrayFieldDef extends ArrayFieldBase<number[]> {
|
|
64
|
+
type: "number";
|
|
65
|
+
/** Default number array value when the field is not persisted. */
|
|
66
|
+
default?: readonly number[];
|
|
67
|
+
}
|
|
68
|
+
/** An array of booleans field. */
|
|
69
|
+
interface BooleanArrayFieldDef extends ArrayFieldBase<boolean[]> {
|
|
70
|
+
type: "boolean";
|
|
71
|
+
/** Default boolean array value when the field is not persisted. */
|
|
72
|
+
default?: readonly boolean[];
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Defines a single field in a store's config schema.
|
|
7
76
|
*
|
|
8
|
-
*
|
|
77
|
+
* Discriminated by `type` and `array` for type-safe `default` values.
|
|
78
|
+
* Each field can optionally include a `validate` function for per-field
|
|
79
|
+
* validation and a `default` value for fallback behavior.
|
|
9
80
|
*
|
|
10
81
|
* @example
|
|
11
82
|
* ```ts
|
|
12
|
-
*
|
|
13
|
-
* type
|
|
14
|
-
*
|
|
83
|
+
* const fields = {
|
|
84
|
+
* theme: { type: "string", default: "light" },
|
|
85
|
+
* verbose: { type: "boolean", default: false },
|
|
86
|
+
* tags: { type: "string", array: true, default: [] },
|
|
87
|
+
* token: { type: "string" }, // optional — no default
|
|
88
|
+
* port: {
|
|
89
|
+
* type: "number",
|
|
90
|
+
* default: 3000,
|
|
91
|
+
* validate: (v) => { if (v < 1 || v > 65535) throw new Error("invalid port"); },
|
|
92
|
+
* },
|
|
93
|
+
* } satisfies FieldsDef;
|
|
15
94
|
* ```
|
|
16
95
|
*/
|
|
17
|
-
type
|
|
96
|
+
type FieldDef = StringFieldDef | NumberFieldDef | BooleanFieldDef | StringArrayFieldDef | NumberArrayFieldDef | BooleanArrayFieldDef;
|
|
97
|
+
/** Record mapping field names to their definitions. */
|
|
98
|
+
type FieldsDef = Record<string, FieldDef>;
|
|
18
99
|
/**
|
|
19
|
-
*
|
|
100
|
+
* Infer the resolved type for a single field definition.
|
|
20
101
|
*
|
|
21
|
-
*
|
|
102
|
+
* - **array** → `primitive[]` (or `primitive[] | undefined` if no default)
|
|
103
|
+
* - **has default** → `primitive` (guaranteed present)
|
|
104
|
+
* - **no default** → `primitive | undefined` (optional)
|
|
22
105
|
*/
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
106
|
+
type InferFieldValue<F extends FieldDef> = F extends {
|
|
107
|
+
array: true;
|
|
108
|
+
} ? F extends {
|
|
109
|
+
default: readonly ResolvePrimitive<F["type"]>[];
|
|
110
|
+
} ? ResolvePrimitive<F["type"]>[] : ResolvePrimitive<F["type"]>[] | undefined : F extends {
|
|
111
|
+
default: ResolvePrimitive<F["type"]>;
|
|
112
|
+
} ? ResolvePrimitive<F["type"]> : ResolvePrimitive<F["type"]> | undefined;
|
|
27
113
|
/**
|
|
28
|
-
*
|
|
114
|
+
* Maps a full {@link FieldsDef} record to the inferred config object type.
|
|
29
115
|
*
|
|
30
|
-
*
|
|
31
|
-
*
|
|
116
|
+
* @example
|
|
117
|
+
* ```ts
|
|
118
|
+
* type Config = InferStoreConfig<{
|
|
119
|
+
* theme: { type: "string"; default: "light" };
|
|
120
|
+
* verbose: { type: "boolean" };
|
|
121
|
+
* }>;
|
|
122
|
+
* // → { theme: string; verbose: boolean | undefined }
|
|
123
|
+
* ```
|
|
124
|
+
*/
|
|
125
|
+
type InferStoreConfig<F extends FieldsDef> = { [K in keyof F] : InferFieldValue<F[K]> };
|
|
126
|
+
/**
|
|
127
|
+
* A single validation issue reported during store operations.
|
|
32
128
|
*/
|
|
33
129
|
interface StoreValidatorIssue {
|
|
34
130
|
/** Human-readable description of the validation failure. */
|
|
35
131
|
message: string;
|
|
36
|
-
/**
|
|
132
|
+
/** Field name where the issue occurred. */
|
|
37
133
|
path: string;
|
|
38
134
|
}
|
|
39
135
|
/**
|
|
40
|
-
* Result returned by a {@link StoreValidator} when validation fails.
|
|
41
|
-
*
|
|
42
|
-
* Carries structured issues for programmatic handling and error rendering.
|
|
43
|
-
*/
|
|
44
|
-
interface StoreValidatorFailure {
|
|
45
|
-
readonly ok: false;
|
|
46
|
-
readonly issues: readonly StoreValidatorIssue[];
|
|
47
|
-
}
|
|
48
|
-
/**
|
|
49
|
-
* Discriminated union result from a {@link StoreValidator}.
|
|
50
|
-
*/
|
|
51
|
-
type StoreValidatorResult<T> = StoreValidatorSuccess<T> | StoreValidatorFailure;
|
|
52
|
-
/**
|
|
53
|
-
* A provider-agnostic validator function for full config objects.
|
|
54
|
-
*
|
|
55
|
-
* Accepts a config value and returns a discriminated result indicating
|
|
56
|
-
* success (with a possibly-transformed value) or failure (with structured issues).
|
|
57
|
-
*
|
|
58
|
-
* Store adapters in `@crustjs/validate` produce functions matching this contract
|
|
59
|
-
* from Standard Schema, Zod, or Effect schemas.
|
|
60
|
-
*
|
|
61
|
-
* @typeParam T - The expected config type.
|
|
62
|
-
*
|
|
63
|
-
* @example
|
|
64
|
-
* ```ts
|
|
65
|
-
* const validator: StoreValidator<Config> = (config) => {
|
|
66
|
-
* if (typeof config.theme !== "string") {
|
|
67
|
-
* return { ok: false, issues: [{ message: "Expected string", path: "theme" }] };
|
|
68
|
-
* }
|
|
69
|
-
* return { ok: true, value: config };
|
|
70
|
-
* };
|
|
71
|
-
* ```
|
|
72
|
-
*/
|
|
73
|
-
type StoreValidator<T> = (value: unknown) => StoreValidatorResult<T> | Promise<StoreValidatorResult<T>>;
|
|
74
|
-
/**
|
|
75
136
|
* Options for {@link createStore}.
|
|
76
137
|
*
|
|
77
|
-
* @typeParam
|
|
138
|
+
* @typeParam F - The field definitions record (inferred via `const` generic).
|
|
78
139
|
*
|
|
79
140
|
* @example
|
|
80
141
|
* ```ts
|
|
81
142
|
* const store = createStore({
|
|
82
143
|
* dirPath: configDir("my-cli"),
|
|
83
|
-
*
|
|
84
|
-
*
|
|
85
|
-
* verbose: false,
|
|
86
|
-
*
|
|
144
|
+
* fields: {
|
|
145
|
+
* theme: { type: "string", default: "light" },
|
|
146
|
+
* verbose: { type: "boolean", default: false },
|
|
147
|
+
* token: { type: "string" },
|
|
87
148
|
* },
|
|
88
149
|
* });
|
|
89
150
|
* ```
|
|
90
151
|
*/
|
|
91
|
-
interface CreateStoreOptions<
|
|
152
|
+
interface CreateStoreOptions<F extends FieldsDef> {
|
|
92
153
|
/**
|
|
93
154
|
* Absolute directory path where the store JSON file is persisted.
|
|
94
155
|
*
|
|
@@ -107,38 +168,14 @@ interface CreateStoreOptions<T extends Record<string, unknown>> {
|
|
|
107
168
|
*/
|
|
108
169
|
name?: string;
|
|
109
170
|
/**
|
|
110
|
-
*
|
|
111
|
-
* fallback values when no persisted file exists or keys are missing.
|
|
112
|
-
*
|
|
113
|
-
* The TypeScript type of `defaults` determines the store's `T` parameter.
|
|
114
|
-
*/
|
|
115
|
-
defaults: T;
|
|
116
|
-
/**
|
|
117
|
-
* Optional validator function applied to config objects during read, write,
|
|
118
|
-
* update, and patch operations.
|
|
119
|
-
*
|
|
120
|
-
* When provided, validation is **strict by default**: invalid config will
|
|
121
|
-
* cause a `CrustStoreError` with `VALIDATION` code to be thrown on both
|
|
122
|
-
* persistence (write/update/patch) and load (read) paths.
|
|
171
|
+
* Field definitions that declare the store's config schema.
|
|
123
172
|
*
|
|
124
|
-
*
|
|
125
|
-
*
|
|
126
|
-
*
|
|
127
|
-
* @example
|
|
128
|
-
* ```ts
|
|
129
|
-
* import { storeValidator } from "@crustjs/validate/zod";
|
|
130
|
-
* import { z } from "zod";
|
|
131
|
-
*
|
|
132
|
-
* const store = createStore({
|
|
133
|
-
* dirPath: configDir("my-cli"),
|
|
134
|
-
* defaults: { theme: "light", verbose: false },
|
|
135
|
-
* validator: storeValidator(z.object({ theme: z.enum(["light", "dark"]) })),
|
|
136
|
-
* });
|
|
137
|
-
* ```
|
|
173
|
+
* Each key maps to a {@link FieldDef} with a `type` discriminant and optional
|
|
174
|
+
* `default` and `validate`. Fields without a `default` are optional (`T | undefined`).
|
|
138
175
|
*/
|
|
139
|
-
|
|
176
|
+
fields: F;
|
|
140
177
|
/**
|
|
141
|
-
* When `true` (the default), persisted keys not present in `
|
|
178
|
+
* When `true` (the default), persisted keys not present in `fields`
|
|
142
179
|
* are dropped on read. Set to `false` to preserve unknown keys.
|
|
143
180
|
*
|
|
144
181
|
* @default true
|
|
@@ -146,7 +183,7 @@ interface CreateStoreOptions<T extends Record<string, unknown>> {
|
|
|
146
183
|
pruneUnknown?: boolean;
|
|
147
184
|
}
|
|
148
185
|
/**
|
|
149
|
-
* Receives the current effective
|
|
186
|
+
* Receives the current effective config and returns an updated config.
|
|
150
187
|
*
|
|
151
188
|
* Used by {@link Store.update} to apply mutations atomically.
|
|
152
189
|
*
|
|
@@ -154,91 +191,85 @@ interface CreateStoreOptions<T extends Record<string, unknown>> {
|
|
|
154
191
|
* ```ts
|
|
155
192
|
* await store.update((current) => ({
|
|
156
193
|
* ...current,
|
|
157
|
-
*
|
|
194
|
+
* theme: "dark",
|
|
158
195
|
* }));
|
|
159
196
|
* ```
|
|
160
197
|
*/
|
|
161
|
-
type StoreUpdater<
|
|
198
|
+
type StoreUpdater<TConfig> = (current: TConfig) => NoInfer<TConfig>;
|
|
162
199
|
/**
|
|
163
|
-
* A typed async store returned by {@link createStore}.
|
|
200
|
+
* A typed async config store returned by {@link createStore}.
|
|
164
201
|
*
|
|
165
202
|
* Provides `read`, `write`, `update`, `patch`, and `reset` operations for a
|
|
166
|
-
* typed
|
|
203
|
+
* single typed config object persisted as JSON on the local filesystem.
|
|
167
204
|
*
|
|
168
|
-
* @typeParam
|
|
205
|
+
* @typeParam TConfig - The inferred config shape from field definitions.
|
|
169
206
|
*
|
|
170
207
|
* @example
|
|
171
208
|
* ```ts
|
|
172
209
|
* const store = createStore({
|
|
173
210
|
* dirPath: configDir("my-cli"),
|
|
174
|
-
*
|
|
175
|
-
*
|
|
176
|
-
* verbose: false,
|
|
211
|
+
* fields: {
|
|
212
|
+
* theme: { type: "string", default: "light" },
|
|
213
|
+
* verbose: { type: "boolean", default: false },
|
|
177
214
|
* },
|
|
178
215
|
* });
|
|
179
216
|
*
|
|
180
|
-
* const
|
|
181
|
-
* await store.write({
|
|
182
|
-
* await store.update((
|
|
183
|
-
* await store.patch({
|
|
217
|
+
* const config = await store.read();
|
|
218
|
+
* await store.write({ theme: "dark", verbose: true });
|
|
219
|
+
* await store.update((c) => ({ ...c, theme: "dark" }));
|
|
220
|
+
* await store.patch({ theme: "dark" });
|
|
184
221
|
* await store.reset();
|
|
185
222
|
* ```
|
|
186
223
|
*/
|
|
187
|
-
interface Store<
|
|
224
|
+
interface Store<TConfig> {
|
|
188
225
|
/**
|
|
189
|
-
* Reads the persisted
|
|
226
|
+
* Reads the persisted config, applying field defaults for missing keys.
|
|
190
227
|
*
|
|
191
|
-
* Always returns a
|
|
192
|
-
*
|
|
228
|
+
* Always returns a value — fields with defaults are guaranteed present,
|
|
229
|
+
* fields without defaults may be `undefined`.
|
|
193
230
|
*
|
|
194
|
-
* When
|
|
195
|
-
* defaults are applied. Invalid persisted config fails loudly.
|
|
231
|
+
* When fields have `validate` functions, the merged config is validated
|
|
232
|
+
* after defaults are applied. Invalid persisted config fails loudly.
|
|
196
233
|
*
|
|
197
|
-
* @returns The effective config value
|
|
234
|
+
* @returns The effective config value.
|
|
198
235
|
* @throws {CrustStoreError} `PARSE` if persisted JSON is malformed.
|
|
199
|
-
* @throws {CrustStoreError} `VALIDATION` if
|
|
236
|
+
* @throws {CrustStoreError} `VALIDATION` if field validation fails.
|
|
200
237
|
* @throws {CrustStoreError} `IO` on filesystem read failures.
|
|
201
238
|
*/
|
|
202
|
-
read(): Promise<
|
|
239
|
+
read(): Promise<TConfig>;
|
|
203
240
|
/**
|
|
204
|
-
* Atomically persists the full
|
|
241
|
+
* Atomically persists the full config object.
|
|
205
242
|
*
|
|
206
|
-
* When
|
|
243
|
+
* When fields have `validate` functions, the config is validated before
|
|
244
|
+
* persistence.
|
|
207
245
|
*
|
|
208
|
-
* @param
|
|
209
|
-
* @throws {CrustStoreError} `VALIDATION` if
|
|
246
|
+
* @param config - The complete config to persist.
|
|
247
|
+
* @throws {CrustStoreError} `VALIDATION` if field validation fails.
|
|
210
248
|
* @throws {CrustStoreError} `IO` on filesystem write failures.
|
|
211
249
|
*/
|
|
212
|
-
write(
|
|
250
|
+
write(config: NoInfer<TConfig>): Promise<void>;
|
|
213
251
|
/**
|
|
214
|
-
* Reads current effective
|
|
215
|
-
*
|
|
216
|
-
* When a validator is configured, the updated config is validated before
|
|
217
|
-
* persistence. The read step during update does **not** validate separately
|
|
218
|
-
* to avoid double-validation overhead — the `current` value passed to the
|
|
219
|
-
* updater may not have been validated (e.g. if the persisted file was
|
|
220
|
-
* manually edited).
|
|
252
|
+
* Reads current effective config, applies the updater, and persists.
|
|
221
253
|
*
|
|
222
|
-
* @param updater - Function receiving current
|
|
254
|
+
* @param updater - Function receiving current config and returning updated config.
|
|
223
255
|
* @throws {CrustStoreError} `PARSE` if persisted JSON is malformed.
|
|
224
|
-
* @throws {CrustStoreError} `VALIDATION` if
|
|
256
|
+
* @throws {CrustStoreError} `VALIDATION` if field validation fails.
|
|
225
257
|
* @throws {CrustStoreError} `IO` on filesystem failures.
|
|
226
258
|
*/
|
|
227
|
-
update(updater: StoreUpdater<
|
|
259
|
+
update(updater: StoreUpdater<TConfig>): Promise<void>;
|
|
228
260
|
/**
|
|
229
|
-
* Applies a
|
|
261
|
+
* Applies a partial update to the current config and persists.
|
|
230
262
|
*
|
|
231
263
|
* Only the provided keys are updated; everything else is preserved.
|
|
232
|
-
* Arrays are replaced wholesale (not merged element-by-element).
|
|
233
264
|
*
|
|
234
|
-
* @param partial - A
|
|
235
|
-
* @throws {CrustStoreError} `VALIDATION` if
|
|
265
|
+
* @param partial - A partial subset of the config to merge in.
|
|
266
|
+
* @throws {CrustStoreError} `VALIDATION` if field validation fails.
|
|
236
267
|
* @throws {CrustStoreError} `PARSE` if persisted JSON is malformed.
|
|
237
268
|
* @throws {CrustStoreError} `IO` on filesystem failures.
|
|
238
269
|
*/
|
|
239
|
-
patch(partial:
|
|
270
|
+
patch(partial: Partial<NoInfer<TConfig>>): Promise<void>;
|
|
240
271
|
/**
|
|
241
|
-
* Removes the persisted
|
|
272
|
+
* Removes the persisted config file, returning the store to
|
|
242
273
|
* defaults-on-read behavior.
|
|
243
274
|
*
|
|
244
275
|
* @throws {CrustStoreError} `IO` on filesystem deletion failures.
|
|
@@ -510,13 +541,14 @@ declare function stateDir(appName: string, env?: PlatformEnv): string;
|
|
|
510
541
|
*/
|
|
511
542
|
declare function cacheDir(appName: string, env?: PlatformEnv): string;
|
|
512
543
|
/**
|
|
513
|
-
* Creates a typed async store backed by a local JSON file.
|
|
544
|
+
* Creates a typed async config store backed by a local JSON file.
|
|
514
545
|
*
|
|
515
546
|
* The store resolves its file path once at creation time from `dirPath` and
|
|
516
|
-
* optional `name`.
|
|
517
|
-
* TypeScript type
|
|
547
|
+
* optional `name`. Field definitions declare the config schema — each field's
|
|
548
|
+
* `type` determines its TypeScript type, and the presence of `default`
|
|
549
|
+
* determines whether the field is guaranteed or optional (`T | undefined`).
|
|
518
550
|
*
|
|
519
|
-
* @typeParam
|
|
551
|
+
* @typeParam F - Field definitions record (inferred via `const` generic).
|
|
520
552
|
* @param options - Store configuration options.
|
|
521
553
|
* @returns A {@link Store} instance with `read`, `write`, `update`, `patch`, and `reset` methods.
|
|
522
554
|
* @throws {CrustStoreError} `PATH` if `dirPath` or `name` is invalid.
|
|
@@ -527,20 +559,21 @@ declare function cacheDir(appName: string, env?: PlatformEnv): string;
|
|
|
527
559
|
*
|
|
528
560
|
* const store = createStore({
|
|
529
561
|
* dirPath: configDir("my-cli"),
|
|
530
|
-
*
|
|
531
|
-
*
|
|
532
|
-
* verbose: false,
|
|
562
|
+
* fields: {
|
|
563
|
+
* theme: { type: "string", default: "light" },
|
|
564
|
+
* verbose: { type: "boolean", default: false },
|
|
565
|
+
* token: { type: "string" },
|
|
533
566
|
* },
|
|
534
567
|
* });
|
|
535
568
|
*
|
|
536
|
-
* const
|
|
537
|
-
* // → {
|
|
569
|
+
* const config = await store.read();
|
|
570
|
+
* // → { theme: "light", verbose: false, token: undefined }
|
|
538
571
|
*
|
|
539
|
-
* await store.write({
|
|
540
|
-
* await store.update((
|
|
541
|
-
* await store.patch({
|
|
572
|
+
* await store.write({ theme: "dark", verbose: true, token: "abc" });
|
|
573
|
+
* await store.update((c) => ({ ...c, theme: "light" }));
|
|
574
|
+
* await store.patch({ theme: "solarized" });
|
|
542
575
|
* await store.reset();
|
|
543
576
|
* ```
|
|
544
577
|
*/
|
|
545
|
-
declare function createStore<const
|
|
546
|
-
export { stateDir, dataDir, createStore, configDir, cacheDir,
|
|
578
|
+
declare function createStore<const F extends FieldsDef>(options: CreateStoreOptions<F>): Store<InferStoreConfig<F>>;
|
|
579
|
+
export { stateDir, dataDir, createStore, configDir, cacheDir, ValueType, ValidationErrorDetails, StoreValidatorIssue, StoreUpdater, StoreErrorCode, Store, PlatformEnv, InferStoreConfig, FieldsDef, FieldDef, CrustStoreError, CreateStoreOptions };
|
package/dist/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
// @bun
|
|
2
|
-
class
|
|
3
|
-
${
|
|
4
|
-
`)}
|
|
2
|
+
class Q extends Error{code;details;cause;constructor(q,I,G){super(I);this.name="CrustStoreError",this.code=q,this.details=G}is(q){return this.code===q}withCause(q){return this.cause=q,this}}import{homedir as P}from"os";import{join as $}from"path";var h="config";function V(){return{platform:process.platform,env:process.env,homedir:P()}}function F(q){if(!q||q.trim().length===0)throw new Q("PATH","appName must be a non-empty string",{path:q??""});if(q.includes("/")||q.includes("\\"))throw new Q("PATH","appName must not contain path separators",{path:q})}function u(q){if(!q||q.trim().length===0)throw new Q("PATH","name must be a non-empty string",{path:q??""});if(q.includes("/")||q.includes("\\"))throw new Q("PATH","name must not contain path separators",{path:q});if(q.endsWith(".json"))throw new Q("PATH","name must not include the .json extension",{path:q})}function v(q){if(!q||q.trim().length===0)throw new Q("PATH","dirPath must be a non-empty string",{path:q??""});if(!(q.startsWith("/")||/^[A-Za-z]:[/\\]/.test(q)))throw new Q("PATH","dirPath must be an absolute path",{path:q});if(q.endsWith(".json"))throw new Q("PATH","dirPath must be a directory path, not a file path (should not end with .json)",{path:q})}function T(q,I,G,H){let K=q.env[I],W=K&&K.trim().length>0?K:$(q.homedir,...G);return $(W,H)}function b(q){throw new Q("PATH",`Unsupported platform: ${q}`,{path:q})}function m(q,I){F(q);let G=I??V();switch(G.platform){case"linux":case"darwin":return T(G,"XDG_CONFIG_HOME",[".config"],q);case"win32":{let H=G.env.APPDATA,K=H&&H.trim().length>0?H:$(G.homedir,"AppData","Roaming");return $(K,q)}default:b(G.platform)}}function d(q,I){F(q);let G=I??V();switch(G.platform){case"linux":case"darwin":return T(G,"XDG_DATA_HOME",[".local","share"],q);case"win32":{let H=G.env.LOCALAPPDATA,K=H&&H.trim().length>0?H:$(G.homedir,"AppData","Local");return $(K,q,"Data")}default:b(G.platform)}}function o(q,I){F(q);let G=I??V();switch(G.platform){case"linux":case"darwin":return T(G,"XDG_STATE_HOME",[".local","state"],q);case"win32":{let H=G.env.LOCALAPPDATA,K=H&&H.trim().length>0?H:$(G.homedir,"AppData","Local");return $(K,q,"State")}default:b(G.platform)}}function s(q,I){F(q);let G=I??V();switch(G.platform){case"linux":case"darwin":return T(G,"XDG_CACHE_HOME",[".cache"],q);case"win32":{let H=G.env.LOCALAPPDATA,K=H&&H.trim().length>0?H:$(G.homedir,"AppData","Local");return $(K,q,"Cache")}default:b(G.platform)}}function U(q,I){v(q);let G=I??h;if(I!==void 0)u(I);return $(q,`${G}.json`)}function z(q,I,G=!0){let H={};for(let[K,W]of Object.entries(I))if(q&&K in q){let _=q[K];H[K]=Array.isArray(_)?[..._]:_}else if("default"in W&&W.default!==void 0)H[K]=Array.isArray(W.default)?[...W.default]:W.default;if(!G&&q!==void 0){for(let[K,W]of Object.entries(q))if(!(K in I))H[K]=Array.isArray(W)?[...W]:W}return H}import{randomUUID as p}from"crypto";import{mkdir as n,readFile as i,rename as l,rm as t,unlink as r,writeFile as a}from"fs/promises";import{dirname as e,join as qq}from"path";async function S(q){let I;try{I=await i(q,"utf-8")}catch(G){if(y(G))return;throw new Q("IO",`Failed to read config file: ${q}`,{path:q,operation:"read"}).withCause(G)}try{return JSON.parse(I)}catch(G){throw new Q("PARSE",`Malformed JSON in config file: ${q}`,{path:q}).withCause(G)}}async function R(q,I){let G=e(q),H=qq(G,`.config-${p()}.tmp`);try{await n(G,{recursive:!0})}catch(K){throw new Q("IO",`Failed to create config directory: ${G}`,{path:q,operation:"write"}).withCause(K)}try{let K=JSON.stringify(I,null,"\t");await a(H,K,"utf-8")}catch(K){throw await A(H),new Q("IO",`Failed to write config file: ${q}`,{path:q,operation:"write"}).withCause(K)}try{await l(H,q)}catch(K){throw await A(H),new Q("IO",`Failed to finalize config file: ${q}`,{path:q,operation:"write"}).withCause(K)}}async function k(q){try{await r(q)}catch(I){if(y(I))return;throw new Q("IO",`Failed to delete config file: ${q}`,{path:q,operation:"delete"}).withCause(I)}}function y(q){return q!==null&&typeof q==="object"&&"code"in q&&q.code==="ENOENT"}async function A(q){try{await t(q,{force:!0})}catch{}}function Gq(q){let{dirPath:I,name:G,fields:H,pruneUnknown:K}=q,W=U(I,G),_=K??!0;function C(L,Y){if(Y==="number"&&typeof L==="string"){let X=Number(L);return Number.isNaN(X)?L:X}if(Y==="boolean"&&typeof L==="string")return L==="true"||L==="1";return L}function x(L){let X={...L};for(let[Z,M]of Object.entries(H)){if(!(Z in X))continue;let O=X[Z];if(M.array===!0&&Array.isArray(O)){X[Z]=O.map((B)=>C(B,M.type));continue}X[Z]=C(O,M.type)}return X}async function J(L,Y){let X=[],Z=L;for(let[M,O]of Object.entries(H)){if(!O.validate)continue;let B=Z[M];if(B===void 0)continue;try{await O.validate(B)}catch(D){let f=D instanceof Error?D.message:"Validation failed";X.push({message:f,path:M})}}if(X.length>0){let M=X.map((B)=>` - ${B.path}: ${B.message}`),O=`Store validation failed (${Y})
|
|
3
|
+
${M.join(`
|
|
4
|
+
`)}`;throw new Q("VALIDATION",O,{operation:Y,issues:X})}}async function j(){let L=await S(W),Y=z(L,H,_);return x(Y)}async function g(){let L=await j();return await J(L,"read"),L}async function w(L){let Y=x(L);await J(Y,"write"),await R(W,Y)}async function c(L){let Y=await j(),X=L(Y),Z=x(X);await J(Z,"update"),await R(W,Z)}async function E(L){let X={...await j(),...L},Z=x(X);await J(Z,"patch"),await R(W,Z)}async function N(){await k(W)}return{read:g,write:w,update:c,patch:E,reset:N}}export{o as stateDir,d as dataDir,Gq as createStore,m as configDir,s as cacheDir,Q as CrustStoreError};
|