@crustjs/store 0.0.3 → 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/README.md +188 -118
- package/dist/index.d.ts +382 -146
- package/dist/index.js +3 -3
- package/package.json +10 -4
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,67 +26,85 @@ 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
|
|
|
56
57
|
`@crustjs/store` provides four path helpers that resolve platform-standard directories for different storage intents. Use the one that matches what you're persisting:
|
|
57
58
|
|
|
58
|
-
| Helper | Intent
|
|
59
|
-
| ----------- |
|
|
60
|
-
| `configDir` | User preferences & config
|
|
61
|
-
| `dataDir` | Important app data
|
|
62
|
-
| `stateDir` | Runtime state
|
|
63
|
-
| `cacheDir` | Regenerable cached data
|
|
59
|
+
| Helper | Intent | Example Use Case |
|
|
60
|
+
| ----------- | ------------------------- | -------------------------------------- |
|
|
61
|
+
| `configDir` | User preferences & config | Theme, editor settings, API keys |
|
|
62
|
+
| `dataDir` | Important app data | Local databases, downloaded resources |
|
|
63
|
+
| `stateDir` | Runtime state | Window positions, scroll offsets, undo |
|
|
64
|
+
| `cacheDir` | Regenerable cached data | HTTP caches, compiled assets |
|
|
64
65
|
|
|
65
66
|
```ts
|
|
66
|
-
import {
|
|
67
|
+
import {
|
|
68
|
+
createStore,
|
|
69
|
+
configDir,
|
|
70
|
+
dataDir,
|
|
71
|
+
stateDir,
|
|
72
|
+
cacheDir,
|
|
73
|
+
} from "@crustjs/store";
|
|
67
74
|
|
|
68
75
|
// User preferences → ~/.config/my-cli/config.json
|
|
69
76
|
const config = createStore({
|
|
70
77
|
dirPath: configDir("my-cli"),
|
|
71
|
-
|
|
78
|
+
fields: {
|
|
79
|
+
theme: { type: "string", default: "light" },
|
|
80
|
+
verbose: { type: "boolean", default: false },
|
|
81
|
+
},
|
|
72
82
|
});
|
|
73
83
|
|
|
74
84
|
// App data → ~/.local/share/my-cli/config.json
|
|
75
85
|
const data = createStore({
|
|
76
86
|
dirPath: dataDir("my-cli"),
|
|
77
|
-
|
|
87
|
+
fields: {
|
|
88
|
+
bookmarks: { type: "array", default: [] },
|
|
89
|
+
},
|
|
78
90
|
});
|
|
79
91
|
|
|
80
92
|
// Runtime state → ~/.local/state/my-cli/config.json
|
|
81
93
|
const state = createStore({
|
|
82
94
|
dirPath: stateDir("my-cli"),
|
|
83
|
-
|
|
95
|
+
fields: {
|
|
96
|
+
lastOpened: { type: "string", default: "" },
|
|
97
|
+
scrollY: { type: "number", default: 0 },
|
|
98
|
+
},
|
|
84
99
|
});
|
|
85
100
|
|
|
86
101
|
// Cache → ~/.cache/my-cli/config.json
|
|
87
102
|
const cache = createStore({
|
|
88
103
|
dirPath: cacheDir("my-cli"),
|
|
89
|
-
|
|
104
|
+
fields: {
|
|
105
|
+
etag: { type: "string", default: "" },
|
|
106
|
+
payload: { type: "string", default: "" },
|
|
107
|
+
},
|
|
90
108
|
});
|
|
91
109
|
```
|
|
92
110
|
|
|
@@ -94,12 +112,12 @@ const cache = createStore({
|
|
|
94
112
|
|
|
95
113
|
All four helpers follow XDG conventions on Linux and macOS, and Windows-native conventions on Windows:
|
|
96
114
|
|
|
97
|
-
| Helper | Linux / macOS
|
|
98
|
-
| ----------- |
|
|
99
|
-
| `configDir` | `~/.config/<app>`
|
|
100
|
-
| `dataDir` | `~/.local/share/<app>`
|
|
101
|
-
| `stateDir` | `~/.local/state/<app>`
|
|
102
|
-
| `cacheDir` | `~/.cache/<app>`
|
|
115
|
+
| Helper | Linux / macOS | Windows | Env Override |
|
|
116
|
+
| ----------- | ---------------------- | ---------------------------- | ------------------ |
|
|
117
|
+
| `configDir` | `~/.config/<app>` | `%APPDATA%\<app>` | `$XDG_CONFIG_HOME` |
|
|
118
|
+
| `dataDir` | `~/.local/share/<app>` | `%LOCALAPPDATA%\<app>\Data` | `$XDG_DATA_HOME` |
|
|
119
|
+
| `stateDir` | `~/.local/state/<app>` | `%LOCALAPPDATA%\<app>\State` | `$XDG_STATE_HOME` |
|
|
120
|
+
| `cacheDir` | `~/.cache/<app>` | `%LOCALAPPDATA%\<app>\Cache` | `$XDG_CACHE_HOME` |
|
|
103
121
|
|
|
104
122
|
macOS uses XDG conventions (same as Linux) for a unified Unix mental model.
|
|
105
123
|
|
|
@@ -119,17 +137,16 @@ const store = createStore(options);
|
|
|
119
137
|
|
|
120
138
|
#### Options
|
|
121
139
|
|
|
122
|
-
| Option | Type
|
|
123
|
-
| -------------- |
|
|
124
|
-
| `dirPath` | `string`
|
|
125
|
-
| `name` | `string`
|
|
126
|
-
| `
|
|
127
|
-
| `
|
|
128
|
-
| `pruneUnknown` | `boolean` | No | Drop unknown persisted keys on read (default `true`). |
|
|
140
|
+
| Option | Type | Required | Description |
|
|
141
|
+
| -------------- | ----------- | -------- | -------------------------------------------------------------------------------------------- |
|
|
142
|
+
| `dirPath` | `string` | Yes | Absolute directory path where the JSON file is stored. |
|
|
143
|
+
| `name` | `string` | No | Store name used as filename (default `"config"` → `config.json`). |
|
|
144
|
+
| `fields` | `FieldsDef` | Yes | Field definitions defining the store's data shape, types, defaults, and optional validation. |
|
|
145
|
+
| `pruneUnknown` | `boolean` | No | Drop unknown persisted keys on read (default `true`). |
|
|
129
146
|
|
|
130
147
|
### `store.read()`
|
|
131
148
|
|
|
132
|
-
Reads the persisted state file. Missing keys are filled from
|
|
149
|
+
Reads the persisted state file. Missing keys are filled from field `defaults`.
|
|
133
150
|
|
|
134
151
|
```ts
|
|
135
152
|
const state = await store.read();
|
|
@@ -142,10 +159,10 @@ Always returns a value — never `undefined`. Does not write merged defaults bac
|
|
|
142
159
|
Atomically persists a full state object. The entire previous state is replaced.
|
|
143
160
|
|
|
144
161
|
```ts
|
|
145
|
-
await store.write({
|
|
162
|
+
await store.write({ theme: "dark", fontSize: 14, verbose: true });
|
|
146
163
|
```
|
|
147
164
|
|
|
148
|
-
Calls `
|
|
165
|
+
Calls per-field `validate` functions (if provided) before writing. Parent directories are created if missing.
|
|
149
166
|
|
|
150
167
|
### `store.update(updater)`
|
|
151
168
|
|
|
@@ -154,21 +171,21 @@ Reads the current effective state, applies the updater function, and atomically
|
|
|
154
171
|
```ts
|
|
155
172
|
await store.update((current) => ({
|
|
156
173
|
...current,
|
|
157
|
-
|
|
174
|
+
theme: "dark",
|
|
158
175
|
}));
|
|
159
176
|
```
|
|
160
177
|
|
|
161
|
-
When no persisted file exists, the updater receives `defaults` as the current value. Calls `
|
|
178
|
+
When no persisted file exists, the updater receives field `defaults` as the current value. Calls per-field `validate` functions before writing.
|
|
162
179
|
|
|
163
180
|
### `store.patch(partial)`
|
|
164
181
|
|
|
165
|
-
Applies a
|
|
182
|
+
Applies a shallow partial update to the current state and persists. Only the provided keys are updated; everything else is preserved.
|
|
166
183
|
|
|
167
184
|
```ts
|
|
168
|
-
await store.patch({
|
|
185
|
+
await store.patch({ theme: "solarized" });
|
|
169
186
|
```
|
|
170
187
|
|
|
171
|
-
|
|
188
|
+
Calls per-field `validate` functions before writing.
|
|
172
189
|
|
|
173
190
|
### `store.reset()`
|
|
174
191
|
|
|
@@ -244,9 +261,10 @@ const dir = configDir("my-cli");
|
|
|
244
261
|
// Default store → ~/.config/my-cli/config.json
|
|
245
262
|
const settingsStore = createStore({
|
|
246
263
|
dirPath: dir,
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
264
|
+
fields: {
|
|
265
|
+
theme: { type: "string", default: "light" },
|
|
266
|
+
fontSize: { type: "number", default: 14 },
|
|
267
|
+
verbose: { type: "boolean", default: false },
|
|
250
268
|
},
|
|
251
269
|
});
|
|
252
270
|
|
|
@@ -254,7 +272,9 @@ const settingsStore = createStore({
|
|
|
254
272
|
const authStore = createStore({
|
|
255
273
|
dirPath: dir,
|
|
256
274
|
name: "auth",
|
|
257
|
-
|
|
275
|
+
fields: {
|
|
276
|
+
token: { type: "string", default: "" },
|
|
277
|
+
},
|
|
258
278
|
});
|
|
259
279
|
```
|
|
260
280
|
|
|
@@ -264,27 +284,27 @@ The `name` must not contain path separators or the `.json` extension.
|
|
|
264
284
|
|
|
265
285
|
## Defaults & Merge
|
|
266
286
|
|
|
267
|
-
When a persisted file exists, `read()`
|
|
287
|
+
When a persisted file exists, `read()` fills missing keys from field `defaults`:
|
|
268
288
|
|
|
269
289
|
```ts
|
|
270
290
|
const store = createStore({
|
|
271
291
|
dirPath: configDir("my-cli"),
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
292
|
+
fields: {
|
|
293
|
+
theme: { type: "string", default: "light" },
|
|
294
|
+
fontSize: { type: "number", default: 14 },
|
|
295
|
+
verbose: { type: "boolean", default: false },
|
|
275
296
|
},
|
|
276
297
|
});
|
|
277
298
|
|
|
278
|
-
// Persisted file contains: { "
|
|
299
|
+
// Persisted file contains: { "theme": "dark", "verbose": true }
|
|
279
300
|
const state = await store.read();
|
|
280
|
-
// → {
|
|
301
|
+
// → { theme: "dark", fontSize: 14, verbose: true }
|
|
281
302
|
```
|
|
282
303
|
|
|
283
304
|
### Merge Rules
|
|
284
305
|
|
|
285
|
-
- Persisted values override defaults
|
|
286
|
-
- Missing keys fall back to defaults
|
|
287
|
-
- Arrays are replaced wholesale — not merged element-by-element.
|
|
306
|
+
- Persisted values override defaults.
|
|
307
|
+
- Missing keys fall back to defaults.
|
|
288
308
|
- Unknown persisted keys are dropped by default (`pruneUnknown: true`). Set `pruneUnknown: false` to preserve them.
|
|
289
309
|
- All values are deep-cloned to prevent shared-reference mutation from defaults.
|
|
290
310
|
- Falsy values (`null`, `0`, `""`, `false`) in the persisted file are preserved — only truly missing keys trigger defaults.
|
|
@@ -295,26 +315,24 @@ Merged defaults exist only in memory. The persisted file remains unchanged until
|
|
|
295
315
|
|
|
296
316
|
## Validation
|
|
297
317
|
|
|
298
|
-
Add
|
|
318
|
+
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
319
|
|
|
300
|
-
###
|
|
320
|
+
### Schema-driven validation
|
|
301
321
|
|
|
302
|
-
The easiest way to add validation is with
|
|
322
|
+
The easiest way to add validation is with the built-in `field()` factory.
|
|
323
|
+
It builds a complete `FieldDef` from any Standard Schema — type, default,
|
|
324
|
+
description, and the per-field validator are all derived in one call:
|
|
303
325
|
|
|
304
326
|
```ts
|
|
305
327
|
import { z } from "zod";
|
|
306
|
-
import {
|
|
307
|
-
import { createStore, configDir } from "@crustjs/store";
|
|
328
|
+
import { createStore, configDir, field } from "@crustjs/store";
|
|
308
329
|
|
|
309
330
|
const store = createStore({
|
|
310
331
|
dirPath: configDir("my-cli"),
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
verbose: z.boolean(),
|
|
316
|
-
}),
|
|
317
|
-
),
|
|
332
|
+
fields: {
|
|
333
|
+
theme: field(z.enum(["light", "dark"]).default("light")),
|
|
334
|
+
verbose: { type: "boolean", default: false },
|
|
335
|
+
},
|
|
318
336
|
});
|
|
319
337
|
|
|
320
338
|
// write() validates before persisting
|
|
@@ -326,52 +344,102 @@ const config = await store.read();
|
|
|
326
344
|
// → throws if persisted config is invalid
|
|
327
345
|
```
|
|
328
346
|
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
For Effect schemas, wrap with `Schema.standardSchemaV1()`:
|
|
347
|
+
For Effect schemas, wrap once with `Schema.standardSchemaV1()`:
|
|
332
348
|
|
|
333
349
|
```ts
|
|
334
350
|
import * as Schema from "effect/Schema";
|
|
335
|
-
import {
|
|
351
|
+
import { createStore, configDir, field } from "@crustjs/store";
|
|
336
352
|
|
|
337
353
|
const store = createStore({
|
|
338
354
|
dirPath: configDir("my-cli"),
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
Schema.Struct({ theme: Schema.Literal("light", "dark") }),
|
|
343
|
-
),
|
|
344
|
-
),
|
|
355
|
+
fields: {
|
|
356
|
+
theme: field(Schema.standardSchemaV1(Schema.Literal("light", "dark"))),
|
|
357
|
+
},
|
|
345
358
|
});
|
|
346
359
|
```
|
|
347
360
|
|
|
361
|
+
#### Schema-derived defaults and TypeScript
|
|
362
|
+
|
|
363
|
+
Standard Schema v1 has no spec-portable type-level access to schema
|
|
364
|
+
defaults. As a result:
|
|
365
|
+
|
|
366
|
+
- `field(z.string().default("x"))` populates `default: "x"` at runtime,
|
|
367
|
+
but the inferred config type is `string | undefined` (NOT narrowed).
|
|
368
|
+
- `field(z.string(), { default: "x" })` populates `default: "x"` AND
|
|
369
|
+
narrows the inferred config type to `string`.
|
|
370
|
+
|
|
371
|
+
Prefer the explicit form when the field is required to always have a
|
|
372
|
+
value at use sites.
|
|
373
|
+
|
|
374
|
+
#### `field()` throws on invalid input
|
|
375
|
+
|
|
376
|
+
`field()` throws `CrustStoreError("DEFINITION")` when handed a
|
|
377
|
+
non-Standard-Schema value, or when the runtime CLI type cannot be
|
|
378
|
+
inferred from the schema and `opts.type` was not supplied. The thrown
|
|
379
|
+
error's `details.vendor` field carries the schema's vendor name (when
|
|
380
|
+
available) to help locate the offending call site.
|
|
381
|
+
|
|
382
|
+
> **Effect ≥ 3.14.2 required.** Effect 3.14.2 made `standardSchemaV1(...)`
|
|
383
|
+
> wrappers expose `.ast`, which is what the validate registry walks. On
|
|
384
|
+
> Effect 3.14.0 / 3.14.1 the wrapper is a plain object and introspection
|
|
385
|
+
> silently fails. See
|
|
386
|
+
> [`@crustjs/validate` Effect setup](https://crustjs.com/docs/modules/validate#quick-start--effect)
|
|
387
|
+
> for the floor and a workaround.
|
|
388
|
+
|
|
348
389
|
### Custom validators
|
|
349
390
|
|
|
350
|
-
You can also provide a validator function directly
|
|
391
|
+
You can also provide a validator function directly on each field. Validators
|
|
392
|
+
return `void` on success and **throw** on failure — the thrown `Error`'s
|
|
393
|
+
`message` is captured as the issue text and the field name is used as the
|
|
394
|
+
issue path.
|
|
351
395
|
|
|
352
396
|
```ts
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
}
|
|
397
|
+
const store = createStore({
|
|
398
|
+
dirPath: configDir("my-cli"),
|
|
399
|
+
fields: {
|
|
400
|
+
theme: {
|
|
401
|
+
type: "string",
|
|
402
|
+
default: "light",
|
|
403
|
+
validate(value) {
|
|
404
|
+
if (value !== "light" && value !== "dark") {
|
|
405
|
+
throw new Error('Must be "light" or "dark"');
|
|
406
|
+
}
|
|
407
|
+
},
|
|
408
|
+
},
|
|
409
|
+
},
|
|
410
|
+
});
|
|
365
411
|
```
|
|
366
412
|
|
|
367
|
-
|
|
413
|
+
Hand-rolled `validate` callbacks are `(value: V) => void | Promise<void>`.
|
|
414
|
+
Return `void` (or `Promise<void>`) to accept; throw an `Error` to reject.
|
|
415
|
+
These remain validation-only — they cannot transform the persisted value.
|
|
416
|
+
|
|
417
|
+
Fields built via the `field()` factory carry a Standard Schema and DO
|
|
418
|
+
persist transforms on `write` / `update` / `patch` (e.g.
|
|
419
|
+
`field(z.string().transform(s => s.trim()))` writes the trimmed value to
|
|
420
|
+
disk). On `read`, the schema still validates the persisted value but its
|
|
421
|
+
transform output is discarded — the on-disk value is returned unchanged.
|
|
422
|
+
Existing on-disk values survive unchanged across upgrades; the first
|
|
423
|
+
subsequent write canonicalizes them through the transform.
|
|
424
|
+
|
|
425
|
+
A write-time **read-stability guard** rejects cross-type transforms whose
|
|
426
|
+
output would fail the schema's own re-validation. For example,
|
|
427
|
+
`field(z.string().transform(Number))` (string in, number out) is rejected
|
|
428
|
+
with `CrustStoreError("VALIDATION")` and an issue message tagged
|
|
429
|
+
`read-unstable transform`. No on-disk write occurs.
|
|
368
430
|
|
|
369
431
|
### Validation behavior
|
|
370
432
|
|
|
371
|
-
- **Write**: Validates before persisting.
|
|
372
|
-
|
|
373
|
-
-
|
|
374
|
-
- **
|
|
433
|
+
- **Write**: Validates each field before persisting. Schema-driven
|
|
434
|
+
transforms (via `field()`) replace the input value with the transformed
|
|
435
|
+
output; hand-rolled validators cannot transform.
|
|
436
|
+
- **Read**: Validates each field after applying defaults. Invalid persisted
|
|
437
|
+
config fails loudly — no silent fallback to defaults. Schema transform
|
|
438
|
+
output is discarded on read; the returned value matches what is on disk.
|
|
439
|
+
- **Update**: Reads raw config (no validation), applies updater, validates
|
|
440
|
+
each field (persisting any schema-transformed outputs), then persists.
|
|
441
|
+
- **Patch**: Reads raw config, applies shallow partial merge, validates each
|
|
442
|
+
field (persisting any schema-transformed outputs), then persists.
|
|
375
443
|
- **Reset**: No validation — just removes the persisted file.
|
|
376
444
|
|
|
377
445
|
### Catching validation errors
|
|
@@ -383,7 +451,7 @@ try {
|
|
|
383
451
|
await store.read();
|
|
384
452
|
} catch (err) {
|
|
385
453
|
if (err instanceof CrustStoreError && err.is("VALIDATION")) {
|
|
386
|
-
// err.details is { operation: "read" | "write" | "update" | "patch", issues:
|
|
454
|
+
// err.details is { operation: "read" | "write" | "update" | "patch", issues: StoreValidatorIssue[] }
|
|
387
455
|
console.error(`Validation failed during ${err.details.operation}:`);
|
|
388
456
|
for (const issue of err.details.issues) {
|
|
389
457
|
console.error(` ${issue.path || "(root)"}: ${issue.message}`);
|
|
@@ -396,12 +464,12 @@ try {
|
|
|
396
464
|
|
|
397
465
|
All errors thrown by `@crustjs/store` are instances of `CrustStoreError` with a typed `code` property:
|
|
398
466
|
|
|
399
|
-
| Code | When
|
|
400
|
-
| ------------ |
|
|
401
|
-
| `PATH` | Invalid `dirPath`, invalid `name`, unsupported platform
|
|
402
|
-
| `PARSE` | Malformed JSON in persisted config file
|
|
403
|
-
| `IO` | Filesystem read, write, or delete failure
|
|
404
|
-
| `VALIDATION` | Config fails validator on read, write, update, or patch
|
|
467
|
+
| Code | When | Details |
|
|
468
|
+
| ------------ | ------------------------------------------------------- | ----------------------- |
|
|
469
|
+
| `PATH` | Invalid `dirPath`, invalid `name`, unsupported platform | `{ path: string }` |
|
|
470
|
+
| `PARSE` | Malformed JSON in persisted config file | `{ path: string }` |
|
|
471
|
+
| `IO` | Filesystem read, write, or delete failure | `{ path, operation }` |
|
|
472
|
+
| `VALIDATION` | Config fails validator on read, write, update, or patch | `{ operation, issues }` |
|
|
405
473
|
|
|
406
474
|
### Catching errors by code
|
|
407
475
|
|
|
@@ -459,10 +527,11 @@ import type {
|
|
|
459
527
|
CreateStoreOptions,
|
|
460
528
|
Store,
|
|
461
529
|
StoreUpdater,
|
|
530
|
+
FieldDef,
|
|
531
|
+
FieldsDef,
|
|
532
|
+
InferStoreConfig,
|
|
462
533
|
StoreValidator,
|
|
463
534
|
StoreValidatorResult,
|
|
464
|
-
StoreValidatorSuccess,
|
|
465
|
-
StoreValidatorFailure,
|
|
466
535
|
StoreValidatorIssue,
|
|
467
536
|
StoreErrorCode,
|
|
468
537
|
StoreValidationIssue,
|
|
@@ -471,21 +540,22 @@ import type {
|
|
|
471
540
|
} from "@crustjs/store";
|
|
472
541
|
```
|
|
473
542
|
|
|
474
|
-
| Type
|
|
475
|
-
|
|
|
476
|
-
| `CreateStoreOptions`
|
|
477
|
-
| `Store`
|
|
478
|
-
| `StoreUpdater`
|
|
479
|
-
| `
|
|
480
|
-
| `
|
|
481
|
-
| `
|
|
482
|
-
| `
|
|
483
|
-
| `
|
|
484
|
-
| `
|
|
485
|
-
| `
|
|
486
|
-
| `
|
|
487
|
-
| `
|
|
488
|
-
| `
|
|
543
|
+
| Type | Description |
|
|
544
|
+
| ------------------------ | ---------------------------------------------------------------------------------------------- |
|
|
545
|
+
| `CreateStoreOptions` | Options object for `createStore()`. |
|
|
546
|
+
| `Store` | Store instance with `read`, `write`, `update`, `patch`, `reset`. |
|
|
547
|
+
| `StoreUpdater` | Updater function type `(current: T) => T`. |
|
|
548
|
+
| `FieldDef` | Single field definition with `type`, optional `default`, and optional `validate`. |
|
|
549
|
+
| `FieldsDef` | Record of field names to `FieldDef` definitions. |
|
|
550
|
+
| `InferStoreConfig` | Inferred store state type from a `FieldsDef` definition. |
|
|
551
|
+
| `StoreValidator` | Validator function contract `(value: unknown) => StoreValidatorResult`. |
|
|
552
|
+
| `StoreValidatorResult` | Discriminated union: `{ ok: true, value: T } \| { ok: false, issues: StoreValidatorIssue[] }`. |
|
|
553
|
+
| `StoreValidatorIssue` | `{ message: string, path: string }`. |
|
|
554
|
+
| `StoreValidationIssue` | Validation issue in error details payload. |
|
|
555
|
+
| `ValidationErrorDetails` | Error details for `VALIDATION` code: `{ operation, issues }`. |
|
|
556
|
+
| `StoreErrorCode` | Union of error codes: `"PATH" \| "PARSE" \| "IO" \| "VALIDATION"`. |
|
|
557
|
+
| `PlatformEnv` | Injectable platform environment for testing path helpers. |
|
|
558
|
+
| `CrustStoreError` | Typed error class with `code`, `details`, and `cause`. |
|
|
489
559
|
|
|
490
560
|
## Scope & Non-Goals
|
|
491
561
|
|
package/dist/index.d.ts
CHANGED
|
@@ -1,94 +1,172 @@
|
|
|
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`).
|
|
20
|
+
*
|
|
21
|
+
* Return shapes:
|
|
22
|
+
* - `void` (or `Promise<void>`) — validation-only; the input value is
|
|
23
|
+
* persisted as-is. This is the contract for hand-rolled validators.
|
|
24
|
+
* - `{ value }` (or `Promise<{ value }>`) — the schema (or transform)
|
|
25
|
+
* produced an output value (typed as `unknown` because schema
|
|
26
|
+
* transforms may emit a value of a different runtime type than
|
|
27
|
+
* `V`; the read-stability guard enforces shape correctness at
|
|
28
|
+
* write time). On `write` / `update` / `patch`, the transformed
|
|
29
|
+
* value replaces the input before persistence and is re-validated
|
|
30
|
+
* once to catch read-unstable transforms (cross-type transforms
|
|
31
|
+
* whose output would fail the schema on the next read). On `read`,
|
|
32
|
+
* the transformed value is discarded — reads always return the
|
|
33
|
+
* on-disk value verbatim.
|
|
34
|
+
* - Throwing an error — the value is rejected; the error message is
|
|
35
|
+
* captured as a validation issue with the field name as `path`.
|
|
36
|
+
*
|
|
37
|
+
* @param value - The field value to validate.
|
|
38
|
+
*/
|
|
39
|
+
validate?: (value: V) => void | Promise<void> | {
|
|
40
|
+
value: unknown;
|
|
41
|
+
} | Promise<{
|
|
42
|
+
value: unknown;
|
|
43
|
+
}>;
|
|
44
|
+
}
|
|
45
|
+
/** Base for scalar (non-array) fields — `array` must be omitted. */
|
|
46
|
+
interface ScalarFieldBase<V> extends FieldDefBase<V> {
|
|
47
|
+
/** Must be omitted for scalar fields — set to `true` for array fields. */
|
|
48
|
+
array?: never;
|
|
49
|
+
}
|
|
50
|
+
/** A scalar string field. */
|
|
51
|
+
interface StringFieldDef extends ScalarFieldBase<string> {
|
|
52
|
+
type: "string";
|
|
53
|
+
/** Default string value when the field is not persisted. */
|
|
54
|
+
default?: string;
|
|
55
|
+
}
|
|
56
|
+
/** A scalar number field. */
|
|
57
|
+
interface NumberFieldDef extends ScalarFieldBase<number> {
|
|
58
|
+
type: "number";
|
|
59
|
+
/** Default number value when the field is not persisted. */
|
|
60
|
+
default?: number;
|
|
61
|
+
}
|
|
62
|
+
/** A scalar boolean field. */
|
|
63
|
+
interface BooleanFieldDef extends ScalarFieldBase<boolean> {
|
|
64
|
+
type: "boolean";
|
|
65
|
+
/** Default boolean value when the field is not persisted. */
|
|
66
|
+
default?: boolean;
|
|
67
|
+
}
|
|
68
|
+
/** Base for array fields — `array` is required as `true`. */
|
|
69
|
+
interface ArrayFieldBase<V> extends FieldDefBase<V> {
|
|
70
|
+
/** Collect values into an array. */
|
|
71
|
+
array: true;
|
|
72
|
+
}
|
|
73
|
+
/** An array of strings field. */
|
|
74
|
+
interface StringArrayFieldDef extends ArrayFieldBase<string[]> {
|
|
75
|
+
type: "string";
|
|
76
|
+
/** Default string array value when the field is not persisted. */
|
|
77
|
+
default?: readonly string[];
|
|
78
|
+
}
|
|
79
|
+
/** An array of numbers field. */
|
|
80
|
+
interface NumberArrayFieldDef extends ArrayFieldBase<number[]> {
|
|
81
|
+
type: "number";
|
|
82
|
+
/** Default number array value when the field is not persisted. */
|
|
83
|
+
default?: readonly number[];
|
|
84
|
+
}
|
|
85
|
+
/** An array of booleans field. */
|
|
86
|
+
interface BooleanArrayFieldDef extends ArrayFieldBase<boolean[]> {
|
|
87
|
+
type: "boolean";
|
|
88
|
+
/** Default boolean array value when the field is not persisted. */
|
|
89
|
+
default?: readonly boolean[];
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* Defines a single field in a store's config schema.
|
|
7
93
|
*
|
|
8
|
-
*
|
|
94
|
+
* Discriminated by `type` and `array` for type-safe `default` values.
|
|
95
|
+
* Each field can optionally include a `validate` function for per-field
|
|
96
|
+
* validation and a `default` value for fallback behavior.
|
|
9
97
|
*
|
|
10
98
|
* @example
|
|
11
99
|
* ```ts
|
|
12
|
-
*
|
|
13
|
-
* type
|
|
14
|
-
*
|
|
100
|
+
* const fields = {
|
|
101
|
+
* theme: { type: "string", default: "light" },
|
|
102
|
+
* verbose: { type: "boolean", default: false },
|
|
103
|
+
* tags: { type: "string", array: true, default: [] },
|
|
104
|
+
* token: { type: "string" }, // optional — no default
|
|
105
|
+
* port: {
|
|
106
|
+
* type: "number",
|
|
107
|
+
* default: 3000,
|
|
108
|
+
* validate: (v) => { if (v < 1 || v > 65535) throw new Error("invalid port"); },
|
|
109
|
+
* },
|
|
110
|
+
* } satisfies FieldsDef;
|
|
15
111
|
* ```
|
|
16
112
|
*/
|
|
17
|
-
type
|
|
113
|
+
type FieldDef = StringFieldDef | NumberFieldDef | BooleanFieldDef | StringArrayFieldDef | NumberArrayFieldDef | BooleanArrayFieldDef;
|
|
114
|
+
/** Record mapping field names to their definitions. */
|
|
115
|
+
type FieldsDef = Record<string, FieldDef>;
|
|
18
116
|
/**
|
|
19
|
-
*
|
|
117
|
+
* Infer the resolved type for a single field definition.
|
|
20
118
|
*
|
|
21
|
-
*
|
|
119
|
+
* - **array** → `primitive[]` (or `primitive[] | undefined` if no default)
|
|
120
|
+
* - **has default** → `primitive` (guaranteed present)
|
|
121
|
+
* - **no default** → `primitive | undefined` (optional)
|
|
22
122
|
*/
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
123
|
+
type InferFieldValue<F extends FieldDef> = F extends {
|
|
124
|
+
array: true;
|
|
125
|
+
} ? F extends {
|
|
126
|
+
default: readonly ResolvePrimitive<F["type"]>[];
|
|
127
|
+
} ? ResolvePrimitive<F["type"]>[] : ResolvePrimitive<F["type"]>[] | undefined : F extends {
|
|
128
|
+
default: ResolvePrimitive<F["type"]>;
|
|
129
|
+
} ? ResolvePrimitive<F["type"]> : ResolvePrimitive<F["type"]> | undefined;
|
|
27
130
|
/**
|
|
28
|
-
*
|
|
131
|
+
* Maps a full {@link FieldsDef} record to the inferred config object type.
|
|
29
132
|
*
|
|
30
|
-
*
|
|
31
|
-
*
|
|
133
|
+
* @example
|
|
134
|
+
* ```ts
|
|
135
|
+
* type Config = InferStoreConfig<{
|
|
136
|
+
* theme: { type: "string"; default: "light" };
|
|
137
|
+
* verbose: { type: "boolean" };
|
|
138
|
+
* }>;
|
|
139
|
+
* // → { theme: string; verbose: boolean | undefined }
|
|
140
|
+
* ```
|
|
141
|
+
*/
|
|
142
|
+
type InferStoreConfig<F extends FieldsDef> = { [K in keyof F] : InferFieldValue<F[K]> };
|
|
143
|
+
/**
|
|
144
|
+
* A single validation issue reported during store operations.
|
|
32
145
|
*/
|
|
33
146
|
interface StoreValidatorIssue {
|
|
34
147
|
/** Human-readable description of the validation failure. */
|
|
35
148
|
message: string;
|
|
36
|
-
/**
|
|
149
|
+
/** Field name where the issue occurred. */
|
|
37
150
|
path: string;
|
|
38
151
|
}
|
|
39
152
|
/**
|
|
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
153
|
* Options for {@link createStore}.
|
|
76
154
|
*
|
|
77
|
-
* @typeParam
|
|
155
|
+
* @typeParam F - The field definitions record (inferred via `const` generic).
|
|
78
156
|
*
|
|
79
157
|
* @example
|
|
80
158
|
* ```ts
|
|
81
159
|
* const store = createStore({
|
|
82
160
|
* dirPath: configDir("my-cli"),
|
|
83
|
-
*
|
|
84
|
-
*
|
|
85
|
-
* verbose: false,
|
|
86
|
-
*
|
|
161
|
+
* fields: {
|
|
162
|
+
* theme: { type: "string", default: "light" },
|
|
163
|
+
* verbose: { type: "boolean", default: false },
|
|
164
|
+
* token: { type: "string" },
|
|
87
165
|
* },
|
|
88
166
|
* });
|
|
89
167
|
* ```
|
|
90
168
|
*/
|
|
91
|
-
interface CreateStoreOptions<
|
|
169
|
+
interface CreateStoreOptions<F extends FieldsDef> {
|
|
92
170
|
/**
|
|
93
171
|
* Absolute directory path where the store JSON file is persisted.
|
|
94
172
|
*
|
|
@@ -107,38 +185,14 @@ interface CreateStoreOptions<T extends Record<string, unknown>> {
|
|
|
107
185
|
*/
|
|
108
186
|
name?: string;
|
|
109
187
|
/**
|
|
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.
|
|
123
|
-
*
|
|
124
|
-
* Use store adapters from `@crustjs/validate/standard`, `@crustjs/validate/zod`,
|
|
125
|
-
* or `@crustjs/validate/effect` to create validators from schema definitions.
|
|
126
|
-
*
|
|
127
|
-
* @example
|
|
128
|
-
* ```ts
|
|
129
|
-
* import { storeValidator } from "@crustjs/validate/zod";
|
|
130
|
-
* import { z } from "zod";
|
|
188
|
+
* Field definitions that declare the store's config schema.
|
|
131
189
|
*
|
|
132
|
-
*
|
|
133
|
-
*
|
|
134
|
-
* defaults: { theme: "light", verbose: false },
|
|
135
|
-
* validator: storeValidator(z.object({ theme: z.enum(["light", "dark"]) })),
|
|
136
|
-
* });
|
|
137
|
-
* ```
|
|
190
|
+
* Each key maps to a {@link FieldDef} with a `type` discriminant and optional
|
|
191
|
+
* `default` and `validate`. Fields without a `default` are optional (`T | undefined`).
|
|
138
192
|
*/
|
|
139
|
-
|
|
193
|
+
fields: F;
|
|
140
194
|
/**
|
|
141
|
-
* When `true` (the default), persisted keys not present in `
|
|
195
|
+
* When `true` (the default), persisted keys not present in `fields`
|
|
142
196
|
* are dropped on read. Set to `false` to preserve unknown keys.
|
|
143
197
|
*
|
|
144
198
|
* @default true
|
|
@@ -146,7 +200,7 @@ interface CreateStoreOptions<T extends Record<string, unknown>> {
|
|
|
146
200
|
pruneUnknown?: boolean;
|
|
147
201
|
}
|
|
148
202
|
/**
|
|
149
|
-
* Receives the current effective
|
|
203
|
+
* Receives the current effective config and returns an updated config.
|
|
150
204
|
*
|
|
151
205
|
* Used by {@link Store.update} to apply mutations atomically.
|
|
152
206
|
*
|
|
@@ -154,91 +208,85 @@ interface CreateStoreOptions<T extends Record<string, unknown>> {
|
|
|
154
208
|
* ```ts
|
|
155
209
|
* await store.update((current) => ({
|
|
156
210
|
* ...current,
|
|
157
|
-
*
|
|
211
|
+
* theme: "dark",
|
|
158
212
|
* }));
|
|
159
213
|
* ```
|
|
160
214
|
*/
|
|
161
|
-
type StoreUpdater<
|
|
215
|
+
type StoreUpdater<TConfig> = (current: TConfig) => NoInfer<TConfig>;
|
|
162
216
|
/**
|
|
163
|
-
* A typed async store returned by {@link createStore}.
|
|
217
|
+
* A typed async config store returned by {@link createStore}.
|
|
164
218
|
*
|
|
165
219
|
* Provides `read`, `write`, `update`, `patch`, and `reset` operations for a
|
|
166
|
-
* typed
|
|
220
|
+
* single typed config object persisted as JSON on the local filesystem.
|
|
167
221
|
*
|
|
168
|
-
* @typeParam
|
|
222
|
+
* @typeParam TConfig - The inferred config shape from field definitions.
|
|
169
223
|
*
|
|
170
224
|
* @example
|
|
171
225
|
* ```ts
|
|
172
226
|
* const store = createStore({
|
|
173
227
|
* dirPath: configDir("my-cli"),
|
|
174
|
-
*
|
|
175
|
-
*
|
|
176
|
-
* verbose: false,
|
|
228
|
+
* fields: {
|
|
229
|
+
* theme: { type: "string", default: "light" },
|
|
230
|
+
* verbose: { type: "boolean", default: false },
|
|
177
231
|
* },
|
|
178
232
|
* });
|
|
179
233
|
*
|
|
180
|
-
* const
|
|
181
|
-
* await store.write({
|
|
182
|
-
* await store.update((
|
|
183
|
-
* await store.patch({
|
|
234
|
+
* const config = await store.read();
|
|
235
|
+
* await store.write({ theme: "dark", verbose: true });
|
|
236
|
+
* await store.update((c) => ({ ...c, theme: "dark" }));
|
|
237
|
+
* await store.patch({ theme: "dark" });
|
|
184
238
|
* await store.reset();
|
|
185
239
|
* ```
|
|
186
240
|
*/
|
|
187
|
-
interface Store<
|
|
241
|
+
interface Store<TConfig> {
|
|
188
242
|
/**
|
|
189
|
-
* Reads the persisted
|
|
243
|
+
* Reads the persisted config, applying field defaults for missing keys.
|
|
190
244
|
*
|
|
191
|
-
* Always returns a
|
|
192
|
-
*
|
|
245
|
+
* Always returns a value — fields with defaults are guaranteed present,
|
|
246
|
+
* fields without defaults may be `undefined`.
|
|
193
247
|
*
|
|
194
|
-
* When
|
|
195
|
-
* defaults are applied. Invalid persisted config fails loudly.
|
|
248
|
+
* When fields have `validate` functions, the merged config is validated
|
|
249
|
+
* after defaults are applied. Invalid persisted config fails loudly.
|
|
196
250
|
*
|
|
197
|
-
* @returns The effective config value
|
|
251
|
+
* @returns The effective config value.
|
|
198
252
|
* @throws {CrustStoreError} `PARSE` if persisted JSON is malformed.
|
|
199
|
-
* @throws {CrustStoreError} `VALIDATION` if
|
|
253
|
+
* @throws {CrustStoreError} `VALIDATION` if field validation fails.
|
|
200
254
|
* @throws {CrustStoreError} `IO` on filesystem read failures.
|
|
201
255
|
*/
|
|
202
|
-
read(): Promise<
|
|
256
|
+
read(): Promise<TConfig>;
|
|
203
257
|
/**
|
|
204
|
-
* Atomically persists the full
|
|
258
|
+
* Atomically persists the full config object.
|
|
205
259
|
*
|
|
206
|
-
* When
|
|
260
|
+
* When fields have `validate` functions, the config is validated before
|
|
261
|
+
* persistence.
|
|
207
262
|
*
|
|
208
|
-
* @param
|
|
209
|
-
* @throws {CrustStoreError} `VALIDATION` if
|
|
263
|
+
* @param config - The complete config to persist.
|
|
264
|
+
* @throws {CrustStoreError} `VALIDATION` if field validation fails.
|
|
210
265
|
* @throws {CrustStoreError} `IO` on filesystem write failures.
|
|
211
266
|
*/
|
|
212
|
-
write(
|
|
267
|
+
write(config: NoInfer<TConfig>): Promise<void>;
|
|
213
268
|
/**
|
|
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).
|
|
269
|
+
* Reads current effective config, applies the updater, and persists.
|
|
221
270
|
*
|
|
222
|
-
* @param updater - Function receiving current
|
|
271
|
+
* @param updater - Function receiving current config and returning updated config.
|
|
223
272
|
* @throws {CrustStoreError} `PARSE` if persisted JSON is malformed.
|
|
224
|
-
* @throws {CrustStoreError} `VALIDATION` if
|
|
273
|
+
* @throws {CrustStoreError} `VALIDATION` if field validation fails.
|
|
225
274
|
* @throws {CrustStoreError} `IO` on filesystem failures.
|
|
226
275
|
*/
|
|
227
|
-
update(updater: StoreUpdater<
|
|
276
|
+
update(updater: StoreUpdater<TConfig>): Promise<void>;
|
|
228
277
|
/**
|
|
229
|
-
* Applies a
|
|
278
|
+
* Applies a partial update to the current config and persists.
|
|
230
279
|
*
|
|
231
280
|
* Only the provided keys are updated; everything else is preserved.
|
|
232
|
-
* Arrays are replaced wholesale (not merged element-by-element).
|
|
233
281
|
*
|
|
234
|
-
* @param partial - A
|
|
235
|
-
* @throws {CrustStoreError} `VALIDATION` if
|
|
282
|
+
* @param partial - A partial subset of the config to merge in.
|
|
283
|
+
* @throws {CrustStoreError} `VALIDATION` if field validation fails.
|
|
236
284
|
* @throws {CrustStoreError} `PARSE` if persisted JSON is malformed.
|
|
237
285
|
* @throws {CrustStoreError} `IO` on filesystem failures.
|
|
238
286
|
*/
|
|
239
|
-
patch(partial:
|
|
287
|
+
patch(partial: Partial<NoInfer<TConfig>>): Promise<void>;
|
|
240
288
|
/**
|
|
241
|
-
* Removes the persisted
|
|
289
|
+
* Removes the persisted config file, returning the store to
|
|
242
290
|
* defaults-on-read behavior.
|
|
243
291
|
*
|
|
244
292
|
* @throws {CrustStoreError} `IO` on filesystem deletion failures.
|
|
@@ -287,6 +335,18 @@ interface ValidationErrorDetails {
|
|
|
287
335
|
issues: readonly StoreValidatorIssue[];
|
|
288
336
|
}
|
|
289
337
|
/**
|
|
338
|
+
* Contextual details attached to a `DEFINITION` error.
|
|
339
|
+
*
|
|
340
|
+
* Returned when a `field()` call cannot produce a valid `FieldDef` from the
|
|
341
|
+
* provided schema and options — e.g. the input is not a Standard Schema v1
|
|
342
|
+
* object, or the schema is of an unknown vendor and `opts.type` was not
|
|
343
|
+
* supplied so the runtime CLI type cannot be inferred.
|
|
344
|
+
*/
|
|
345
|
+
interface DefinitionErrorDetails {
|
|
346
|
+
/** The schema vendor name, when the failing input was a Standard Schema. */
|
|
347
|
+
vendor?: string;
|
|
348
|
+
}
|
|
349
|
+
/**
|
|
290
350
|
* Maps each {@link StoreErrorCode} to its structured details type.
|
|
291
351
|
*
|
|
292
352
|
* Used internally for conditional constructor parameters and type narrowing.
|
|
@@ -296,6 +356,7 @@ interface StoreErrorDetailsMap {
|
|
|
296
356
|
PARSE: ParseErrorDetails;
|
|
297
357
|
IO: IOErrorDetails;
|
|
298
358
|
VALIDATION: ValidationErrorDetails;
|
|
359
|
+
DEFINITION: DefinitionErrorDetails;
|
|
299
360
|
}
|
|
300
361
|
/**
|
|
301
362
|
* All possible error codes emitted by `@crustjs/store`.
|
|
@@ -304,6 +365,7 @@ interface StoreErrorDetailsMap {
|
|
|
304
365
|
* - `PARSE` — Malformed JSON in persisted config file
|
|
305
366
|
* - `IO` — Filesystem read, write, or delete failure
|
|
306
367
|
* - `VALIDATION` — Config object rejected by a store validator
|
|
368
|
+
* - `DEFINITION` — `field()` cannot produce a valid `FieldDef` from the given schema/options
|
|
307
369
|
*
|
|
308
370
|
* @example
|
|
309
371
|
* ```ts
|
|
@@ -391,6 +453,178 @@ declare class CrustStoreError<C extends StoreErrorCode = StoreErrorCode> extends
|
|
|
391
453
|
*/
|
|
392
454
|
withCause(cause: unknown): this;
|
|
393
455
|
}
|
|
456
|
+
import { InferOutput, StandardSchema } from "@crustjs/schema-utils";
|
|
457
|
+
/**
|
|
458
|
+
* Optional overrides for the introspected `FieldDef` shape produced by
|
|
459
|
+
* {@link field}.
|
|
460
|
+
*
|
|
461
|
+
* Every key is optional. When automatic introspection covers a field
|
|
462
|
+
* (`type`, `description`, `default`, `array`), explicit values override
|
|
463
|
+
* the introspection silently. For schemas with unknown vendors (e.g.
|
|
464
|
+
* Valibot, ArkType), `type` MUST be supplied explicitly because no
|
|
465
|
+
* inference is available.
|
|
466
|
+
*
|
|
467
|
+
* No `validate` key — validation flows exclusively through the schema.
|
|
468
|
+
* If extra checks are needed, refine the schema with `.refine(...)`
|
|
469
|
+
* (Zod), `Schema.filter(...)` (Effect), etc.
|
|
470
|
+
*
|
|
471
|
+
* @typeParam T - The schema's output value type. Used to type-tighten the
|
|
472
|
+
* `default` key when the user wants a non-`undefined` field.
|
|
473
|
+
*/
|
|
474
|
+
interface FieldOptions<T = unknown> {
|
|
475
|
+
type?: "string" | "number" | "boolean";
|
|
476
|
+
description?: string;
|
|
477
|
+
/**
|
|
478
|
+
* Default value for this field when the persisted state does not contain
|
|
479
|
+
* a value for it. Passing `default` explicitly here narrows the inferred
|
|
480
|
+
* config type from `T | undefined` to `T`. Schema-derived defaults
|
|
481
|
+
* (e.g. `z.string().default("x")`) populate the runtime default but do
|
|
482
|
+
* NOT narrow the TypeScript type — pass it explicitly here for tight
|
|
483
|
+
* typing.
|
|
484
|
+
*/
|
|
485
|
+
default?: T;
|
|
486
|
+
/** Mark this field as an array (collects values into an array). */
|
|
487
|
+
array?: true;
|
|
488
|
+
}
|
|
489
|
+
type ValueType2 = "string" | "number" | "boolean";
|
|
490
|
+
type StripUndefined<T> = Exclude<T, undefined>;
|
|
491
|
+
type PrimitiveToValueType<T> = [T] extends [string] ? "string" : [T] extends [number] ? "number" : [T] extends [boolean] ? "boolean" : ValueType2;
|
|
492
|
+
/**
|
|
493
|
+
* Resolve the runtime CLI value-type from a Standard Schema's output type.
|
|
494
|
+
*
|
|
495
|
+
* Array schemas are detected via `IsArrayOutput`; their element type drives
|
|
496
|
+
* the value-type literal so `field(z.array(z.string()))` resolves to
|
|
497
|
+
* `{ type: "string"; array: true }`.
|
|
498
|
+
*/
|
|
499
|
+
type IsArrayOutput<S> = S extends StandardSchema<infer _I, infer Out> ? StripUndefined<Out> extends readonly unknown[] ? true : false : false;
|
|
500
|
+
type ArrayElementOutput<S> = S extends StandardSchema<infer _I, infer Out> ? StripUndefined<Out> extends readonly (infer E)[] ? E : never : never;
|
|
501
|
+
type ResolveScalarType<S> = S extends StandardSchema<infer _I, infer Out> ? PrimitiveToValueType<StripUndefined<Out>> : ValueType2;
|
|
502
|
+
/** A scalar `FieldDef` with no narrowed default. */
|
|
503
|
+
type ScalarFieldDef<T extends ValueType2> = {
|
|
504
|
+
readonly type: T;
|
|
505
|
+
readonly description?: string;
|
|
506
|
+
readonly validate: (value: unknown) => Promise<{
|
|
507
|
+
value: unknown;
|
|
508
|
+
}>;
|
|
509
|
+
};
|
|
510
|
+
/** A scalar `FieldDef` with a narrowed default. */
|
|
511
|
+
type ScalarFieldDefWithDefault<
|
|
512
|
+
T extends ValueType2,
|
|
513
|
+
D
|
|
514
|
+
> = {
|
|
515
|
+
readonly type: T;
|
|
516
|
+
readonly description?: string;
|
|
517
|
+
readonly default: D;
|
|
518
|
+
readonly validate: (value: unknown) => Promise<{
|
|
519
|
+
value: unknown;
|
|
520
|
+
}>;
|
|
521
|
+
};
|
|
522
|
+
/** An array `FieldDef` with no narrowed default. */
|
|
523
|
+
type ArrayFieldDef<T extends ValueType2> = {
|
|
524
|
+
readonly type: T;
|
|
525
|
+
readonly array: true;
|
|
526
|
+
readonly description?: string;
|
|
527
|
+
readonly validate: (value: unknown) => Promise<{
|
|
528
|
+
value: unknown;
|
|
529
|
+
}>;
|
|
530
|
+
};
|
|
531
|
+
/** An array `FieldDef` with a narrowed default. */
|
|
532
|
+
type ArrayFieldDefWithDefault<
|
|
533
|
+
T extends ValueType2,
|
|
534
|
+
D
|
|
535
|
+
> = {
|
|
536
|
+
readonly type: T;
|
|
537
|
+
readonly array: true;
|
|
538
|
+
readonly description?: string;
|
|
539
|
+
readonly default: D;
|
|
540
|
+
readonly validate: (value: unknown) => Promise<{
|
|
541
|
+
value: unknown;
|
|
542
|
+
}>;
|
|
543
|
+
};
|
|
544
|
+
/**
|
|
545
|
+
* `FieldDef` inferred from a Standard Schema, with no narrowed default.
|
|
546
|
+
*
|
|
547
|
+
* Schema-derived defaults are populated at runtime but do NOT narrow the
|
|
548
|
+
* TypeScript type — Standard Schema v1 has no spec-portable type-level
|
|
549
|
+
* access to defaults. Pass `field(schema, { default: x })` explicitly to
|
|
550
|
+
* narrow.
|
|
551
|
+
*/
|
|
552
|
+
type SchemaFieldDef<S extends StandardSchema> = IsArrayOutput<S> extends true ? ArrayFieldDef<PrimitiveToValueType<StripUndefined<ArrayElementOutput<S>>>> : ScalarFieldDef<ResolveScalarType<S>>;
|
|
553
|
+
/**
|
|
554
|
+
* `FieldDef` inferred from a Standard Schema, with the explicit `opts.default`
|
|
555
|
+
* narrowed into the type.
|
|
556
|
+
*/
|
|
557
|
+
type SchemaFieldDefWithDefault<
|
|
558
|
+
S extends StandardSchema,
|
|
559
|
+
D
|
|
560
|
+
> = IsArrayOutput<S> extends true ? ArrayFieldDefWithDefault<PrimitiveToValueType<StripUndefined<ArrayElementOutput<S>>>, D> : ScalarFieldDefWithDefault<ResolveScalarType<S>, D>;
|
|
561
|
+
/**
|
|
562
|
+
* Define a `@crustjs/store` field from any Standard Schema v1.
|
|
563
|
+
*
|
|
564
|
+
* Returns a value that satisfies store's `FieldDef` discriminated union.
|
|
565
|
+
* Auto-derives `type`, `array`, `description`, and `default` from the
|
|
566
|
+
* schema (Zod and Effect natively; Valibot/ArkType via the
|
|
567
|
+
* `validate(undefined)` fallback for defaults). Pass `opts` to override
|
|
568
|
+
* any key explicitly — explicit values win silently.
|
|
569
|
+
*
|
|
570
|
+
* The returned `validate` is an async function that throws an `Error` with
|
|
571
|
+
* the schema's normalized issue messages on failure (matches store's
|
|
572
|
+
* `FieldDef.validate` contract).
|
|
573
|
+
*
|
|
574
|
+
* **Type-level defaults**: schema-derived defaults populate at runtime but
|
|
575
|
+
* do NOT narrow the inferred config type. For tight typing of
|
|
576
|
+
* default-bearing fields, pass `default` via `opts`:
|
|
577
|
+
*
|
|
578
|
+
* ```ts
|
|
579
|
+
* field(z.string().default("x")) // state: string | undefined
|
|
580
|
+
* field(z.string(), { default: "x" }) // state: string
|
|
581
|
+
* ```
|
|
582
|
+
*
|
|
583
|
+
* @param schema - Any Standard Schema v1 object (Zod schemas natively;
|
|
584
|
+
* Effect schemas wrapped via `Schema.standardSchemaV1`;
|
|
585
|
+
* Valibot/ArkType/Sury/etc. as-is)
|
|
586
|
+
* @param opts - Optional store-field metadata; explicit keys override
|
|
587
|
+
* the introspected values silently
|
|
588
|
+
* @throws {CrustStoreError} With code `"DEFINITION"` when the input is not
|
|
589
|
+
* a Standard Schema, or when the runtime CLI type cannot be inferred
|
|
590
|
+
* and `opts.type` was not supplied.
|
|
591
|
+
*
|
|
592
|
+
* @example Zod
|
|
593
|
+
* ```ts
|
|
594
|
+
* import { z } from "zod";
|
|
595
|
+
* import { createStore, configDir, field } from "@crustjs/store";
|
|
596
|
+
*
|
|
597
|
+
* const store = createStore({
|
|
598
|
+
* dirPath: configDir("my-cli"),
|
|
599
|
+
* fields: {
|
|
600
|
+
* theme: field(z.enum(["light", "dark"]).default("light")),
|
|
601
|
+
* verbose: field(z.boolean().default(false)),
|
|
602
|
+
* tags: field(z.array(z.string()).default([])),
|
|
603
|
+
* },
|
|
604
|
+
* });
|
|
605
|
+
* ```
|
|
606
|
+
*
|
|
607
|
+
* @example Effect
|
|
608
|
+
* ```ts
|
|
609
|
+
* import * as Schema from "effect/Schema";
|
|
610
|
+
* import { createStore, configDir, field } from "@crustjs/store";
|
|
611
|
+
*
|
|
612
|
+
* const store = createStore({
|
|
613
|
+
* dirPath: configDir("my-cli"),
|
|
614
|
+
* fields: {
|
|
615
|
+
* theme: field(Schema.standardSchemaV1(Schema.Literal("light", "dark"))),
|
|
616
|
+
* },
|
|
617
|
+
* });
|
|
618
|
+
* ```
|
|
619
|
+
*/
|
|
620
|
+
declare function field<S extends StandardSchema>(schema: S): SchemaFieldDef<S>;
|
|
621
|
+
declare function field<
|
|
622
|
+
S extends StandardSchema,
|
|
623
|
+
D extends InferOutput<S>
|
|
624
|
+
>(schema: S, opts: FieldOptions<InferOutput<S>> & {
|
|
625
|
+
default: D;
|
|
626
|
+
}): SchemaFieldDefWithDefault<S, D>;
|
|
627
|
+
declare function field<S extends StandardSchema>(schema: S, opts: FieldOptions<InferOutput<S>>): SchemaFieldDef<S>;
|
|
394
628
|
/**
|
|
395
629
|
* Encapsulates runtime environment values needed for path resolution.
|
|
396
630
|
*
|
|
@@ -510,13 +744,14 @@ declare function stateDir(appName: string, env?: PlatformEnv): string;
|
|
|
510
744
|
*/
|
|
511
745
|
declare function cacheDir(appName: string, env?: PlatformEnv): string;
|
|
512
746
|
/**
|
|
513
|
-
* Creates a typed async store backed by a local JSON file.
|
|
747
|
+
* Creates a typed async config store backed by a local JSON file.
|
|
514
748
|
*
|
|
515
749
|
* The store resolves its file path once at creation time from `dirPath` and
|
|
516
|
-
* optional `name`.
|
|
517
|
-
* TypeScript type
|
|
750
|
+
* optional `name`. Field definitions declare the config schema — each field's
|
|
751
|
+
* `type` determines its TypeScript type, and the presence of `default`
|
|
752
|
+
* determines whether the field is guaranteed or optional (`T | undefined`).
|
|
518
753
|
*
|
|
519
|
-
* @typeParam
|
|
754
|
+
* @typeParam F - Field definitions record (inferred via `const` generic).
|
|
520
755
|
* @param options - Store configuration options.
|
|
521
756
|
* @returns A {@link Store} instance with `read`, `write`, `update`, `patch`, and `reset` methods.
|
|
522
757
|
* @throws {CrustStoreError} `PATH` if `dirPath` or `name` is invalid.
|
|
@@ -527,20 +762,21 @@ declare function cacheDir(appName: string, env?: PlatformEnv): string;
|
|
|
527
762
|
*
|
|
528
763
|
* const store = createStore({
|
|
529
764
|
* dirPath: configDir("my-cli"),
|
|
530
|
-
*
|
|
531
|
-
*
|
|
532
|
-
* verbose: false,
|
|
765
|
+
* fields: {
|
|
766
|
+
* theme: { type: "string", default: "light" },
|
|
767
|
+
* verbose: { type: "boolean", default: false },
|
|
768
|
+
* token: { type: "string" },
|
|
533
769
|
* },
|
|
534
770
|
* });
|
|
535
771
|
*
|
|
536
|
-
* const
|
|
537
|
-
* // → {
|
|
772
|
+
* const config = await store.read();
|
|
773
|
+
* // → { theme: "light", verbose: false, token: undefined }
|
|
538
774
|
*
|
|
539
|
-
* await store.write({
|
|
540
|
-
* await store.update((
|
|
541
|
-
* await store.patch({
|
|
775
|
+
* await store.write({ theme: "dark", verbose: true, token: "abc" });
|
|
776
|
+
* await store.update((c) => ({ ...c, theme: "light" }));
|
|
777
|
+
* await store.patch({ theme: "solarized" });
|
|
542
778
|
* await store.reset();
|
|
543
779
|
* ```
|
|
544
780
|
*/
|
|
545
|
-
declare function createStore<const
|
|
546
|
-
export { stateDir, dataDir, createStore, configDir, cacheDir, ValidationErrorDetails,
|
|
781
|
+
declare function createStore<const F extends FieldsDef>(options: CreateStoreOptions<F>): Store<InferStoreConfig<F>>;
|
|
782
|
+
export { stateDir, field, dataDir, createStore, configDir, cacheDir, ValueType, ValidationErrorDetails, StoreValidatorIssue, StoreUpdater, StoreErrorCode, Store, PlatformEnv, InferStoreConfig, FieldsDef, FieldOptions, FieldDef, DefinitionErrorDetails, CrustStoreError, CreateStoreOptions };
|
package/dist/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
// @bun
|
|
2
|
-
class
|
|
3
|
-
${
|
|
4
|
-
`)}
|
|
2
|
+
class W extends Error{code;details;cause;constructor(q,H,G){super(H);this.name="CrustStoreError",this.code=q,this.details=G}is(q){return this.code===q}withCause(q){return this.cause=q,this}}import{extractDefault as m,inferOptions as d,isStandardSchema as p,normalizeStandardIssues as o}from"@crustjs/schema-utils";function n(q){return async(H)=>{let G=await q["~standard"].validate(H);if(G.issues){let L=o(G.issues).map((Q)=>Q.path?`${Q.path}: ${Q.message}`:Q.message);throw Error(L.join("; "))}return{value:G.value}}}function i(q,H){if(!p(q))throw new W("DEFINITION",`field(): argument must be a Standard Schema v1 object (got ${typeof q})`,{});let G=q["~standard"]?.vendor??"unknown",K=`field (vendor: "${G}")`,L=d(q,"field",K),Q=H?.type??L.type;if(!Q)throw new W("DEFINITION",`${K}: unable to infer field type from schema. Pass an explicit { type: "string" | "number" | "boolean" } in options. If this is an Effect schema, wrap it with Schema.standardSchemaV1(...) before passing it here.`,{vendor:G});let J=H?.array===!0||L.multiple===!0,V=H?.description??L.description,x=H&&"default"in H?{ok:!0,value:H.default}:m(q),T=n(q),O={type:Q,validate:T};if(J)O.array=!0;if(V!==void 0)O.description=V;if(x.ok)O.default=x.value;return O}import{homedir as l}from"os";import{join as B}from"path";var s="config";function U(){return{platform:process.platform,env:process.env,homedir:l()}}function j(q){if(!q||q.trim().length===0)throw new W("PATH","appName must be a non-empty string",{path:q??""});if(q.includes("/")||q.includes("\\"))throw new W("PATH","appName must not contain path separators",{path:q})}function t(q){if(!q||q.trim().length===0)throw new W("PATH","name must be a non-empty string",{path:q??""});if(q.includes("/")||q.includes("\\"))throw new W("PATH","name must not contain path separators",{path:q});if(q.endsWith(".json"))throw new W("PATH","name must not include the .json extension",{path:q})}function r(q){if(!q||q.trim().length===0)throw new W("PATH","dirPath must be a non-empty string",{path:q??""});if(!(q.startsWith("/")||/^[A-Za-z]:[/\\]/.test(q)))throw new W("PATH","dirPath must be an absolute path",{path:q});if(q.endsWith(".json"))throw new W("PATH","dirPath must be a directory path, not a file path (should not end with .json)",{path:q})}function C(q,H,G,K){let L=q.env[H],Q=L&&L.trim().length>0?L:B(q.homedir,...G);return B(Q,K)}function D(q){throw new W("PATH",`Unsupported platform: ${q}`,{path:q})}function a(q,H){j(q);let G=H??U();switch(G.platform){case"linux":case"darwin":return C(G,"XDG_CONFIG_HOME",[".config"],q);case"win32":{let K=G.env.APPDATA,L=K&&K.trim().length>0?K:B(G.homedir,"AppData","Roaming");return B(L,q)}default:D(G.platform)}}function e(q,H){j(q);let G=H??U();switch(G.platform){case"linux":case"darwin":return C(G,"XDG_DATA_HOME",[".local","share"],q);case"win32":{let K=G.env.LOCALAPPDATA,L=K&&K.trim().length>0?K:B(G.homedir,"AppData","Local");return B(L,q,"Data")}default:D(G.platform)}}function qq(q,H){j(q);let G=H??U();switch(G.platform){case"linux":case"darwin":return C(G,"XDG_STATE_HOME",[".local","state"],q);case"win32":{let K=G.env.LOCALAPPDATA,L=K&&K.trim().length>0?K:B(G.homedir,"AppData","Local");return B(L,q,"State")}default:D(G.platform)}}function Gq(q,H){j(q);let G=H??U();switch(G.platform){case"linux":case"darwin":return C(G,"XDG_CACHE_HOME",[".cache"],q);case"win32":{let K=G.env.LOCALAPPDATA,L=K&&K.trim().length>0?K:B(G.homedir,"AppData","Local");return B(L,q,"Cache")}default:D(G.platform)}}function S(q,H){r(q);let G=H??s;if(H!==void 0)t(H);return B(q,`${G}.json`)}function w(q,H,G=!0){let K={};for(let[L,Q]of Object.entries(H))if(q&&L in q){let J=q[L];K[L]=Array.isArray(J)?[...J]:J}else if("default"in Q&&Q.default!==void 0)K[L]=Array.isArray(Q.default)?[...Q.default]:Q.default;if(!G&&q!==void 0){for(let[L,Q]of Object.entries(q))if(!(L in H))K[L]=Array.isArray(Q)?[...Q]:Q}return K}import{randomUUID as Hq}from"crypto";import{mkdir as Kq,readFile as Lq,rename as Qq,rm as Wq,unlink as Xq,writeFile as Yq}from"fs/promises";import{dirname as Zq,join as $q}from"path";async function y(q){let H;try{H=await Lq(q,"utf-8")}catch(G){if(E(G))return;throw new W("IO",`Failed to read config file: ${q}`,{path:q,operation:"read"}).withCause(G)}try{return JSON.parse(H)}catch(G){throw new W("PARSE",`Malformed JSON in config file: ${q}`,{path:q}).withCause(G)}}async function z(q,H){let G=Zq(q),K=$q(G,`.config-${Hq()}.tmp`);try{await Kq(G,{recursive:!0})}catch(L){throw new W("IO",`Failed to create config directory: ${G}`,{path:q,operation:"write"}).withCause(L)}try{let L=JSON.stringify(H,null,"\t");await Yq(K,L,"utf-8")}catch(L){throw await g(K),new W("IO",`Failed to write config file: ${q}`,{path:q,operation:"write"}).withCause(L)}try{await Qq(K,q)}catch(L){throw await g(K),new W("IO",`Failed to finalize config file: ${q}`,{path:q,operation:"write"}).withCause(L)}}async function k(q){try{await Xq(q)}catch(H){if(E(H))return;throw new W("IO",`Failed to delete config file: ${q}`,{path:q,operation:"delete"}).withCause(H)}}function E(q){return q!==null&&typeof q==="object"&&"code"in q&&q.code==="ENOENT"}async function g(q){try{await Wq(q,{force:!0})}catch{}}function N(q){return typeof q==="object"&&q!==null&&"value"in q&&!("ok"in q)}function Mq(q){let{dirPath:H,name:G,fields:K,pruneUnknown:L}=q,Q=S(H,G),J=L??!0;function V(X,Z){if(Z==="number"&&typeof X==="string"){let Y=Number(X);return Number.isNaN(Y)?X:Y}if(Z==="boolean"&&typeof X==="string")return X==="true"||X==="1";return X}function x(X){let Y={...X};for(let[$,M]of Object.entries(K)){if(!($ in Y))continue;let I=Y[$];if(M.array===!0&&Array.isArray(I)){Y[$]=I.map((_)=>V(_,M.type));continue}Y[$]=V(I,M.type)}return Y}async function T(X,Z){let Y=[],$=X;for(let[M,I]of Object.entries(K)){if(!I.validate)continue;let _=$[M];if(_===void 0)continue;let R;try{R=await I.validate(_)}catch(F){let b=F instanceof Error?F.message:"Validation failed";Y.push({message:b,path:M});continue}if(R===void 0)continue;if(N(R)){let F=R.value;if(Z==="read")continue;if(!Bun.deepEquals(F,_)){let b;try{b=await I.validate(F)}catch(A){let u=A instanceof Error?A.message:"re-validation failed";Y.push({message:`read-unstable transform: ${u}`,path:M});continue}if(b!==void 0&&(!N(b)||!Bun.deepEquals(b.value,F))){Y.push({message:"read-unstable transform: output would be transformed again on re-read",path:M});continue}$[M]=F}continue}throw TypeError("FieldDef.validate must return void or { value } (see @crustjs/store validation docs).")}if(Y.length>0){let M=Y.map((_)=>` - ${_.path}: ${_.message}`),I=`Store validation failed (${Z})
|
|
3
|
+
${M.join(`
|
|
4
|
+
`)}`;throw new W("VALIDATION",I,{operation:Z,issues:Y})}}async function O(){let X=await y(Q),Z=w(X,K,J);return x(Z)}async function P(){let X=await O();return await T(X,"read"),X}async function c(X){let Z=x(X);await T(Z,"write"),await z(Q,Z)}async function f(X){let Z=await O(),Y=X(Z),$=x(Y);await T($,"update"),await z(Q,$)}async function h(X){let Y={...await O(),...X},$=x(Y);await T($,"patch"),await z(Q,$)}async function v(){await k(Q)}return{read:P,write:c,update:f,patch:h,reset:v}}export{qq as stateDir,i as field,e as dataDir,Mq as createStore,a as configDir,Gq as cacheDir,W as CrustStoreError};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@crustjs/store",
|
|
3
|
-
"version": "0.0
|
|
3
|
+
"version": "0.1.0",
|
|
4
4
|
"description": "DX-first, typed persistence for CLI apps with config/data/state/cache separation",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -42,13 +42,19 @@
|
|
|
42
42
|
"build": "bunup",
|
|
43
43
|
"dev": "bunup --watch",
|
|
44
44
|
"check:types": "tsc --noEmit",
|
|
45
|
-
"test": "bun test"
|
|
45
|
+
"test": "bun test",
|
|
46
|
+
"publish": "bun publish --no-git-checks || true"
|
|
46
47
|
},
|
|
47
48
|
"devDependencies": {
|
|
48
49
|
"@crustjs/config": "0.0.0",
|
|
49
|
-
"bunup": "^0.16.
|
|
50
|
+
"bunup": "^0.16.31",
|
|
51
|
+
"effect": "3.14.2",
|
|
52
|
+
"zod": "^4.0.0"
|
|
50
53
|
},
|
|
51
54
|
"peerDependencies": {
|
|
52
|
-
"typescript": "^
|
|
55
|
+
"typescript": "^6.0.3"
|
|
56
|
+
},
|
|
57
|
+
"dependencies": {
|
|
58
|
+
"@crustjs/schema-utils": "0.0.1"
|
|
53
59
|
}
|
|
54
60
|
}
|