@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 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 defaults object, pick a storage intent, and get a typed store with read/write/update/patch/reset — no manual type annotations needed.
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
- defaults: {
30
- ui: { theme: "light" as string, fontSize: 14 },
31
- verbose: false,
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
- // → { ui: { theme: "light", fontSize: 14 }, verbose: false }
38
+ // → { theme: "light", fontSize: 14, verbose: false }
38
39
 
39
40
  // Write a full state object
40
- await store.write({ ui: { theme: "dark", fontSize: 16 }, verbose: true });
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 nested keys
46
- await store.patch({ ui: { theme: "solarized" } });
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 `defaults` object.
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
- defaults: { theme: "light" as string, verbose: false },
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
- defaults: { bookmarks: [] as string[] },
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
- defaults: { lastOpened: "" as string, scrollY: 0 },
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
- defaults: { etag: "" as string, payload: "" as string },
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
- | `defaults` | `T` | Yes | Default values defining the store's data shape and fallbacks. |
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 `defaults` via deep merge. When no file exists, returns `defaults`.
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({ ui: { theme: "dark", fontSize: 14 }, verbose: true });
156
+ await store.write({ theme: "dark", fontSize: 14, verbose: true });
146
157
  ```
147
158
 
148
- Calls `validator` (if provided) before writing. Parent directories are created if missing.
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
- ui: { ...current.ui, theme: "dark" },
168
+ theme: "dark",
158
169
  }));
159
170
  ```
160
171
 
161
- When no persisted file exists, the updater receives `defaults` as the current value. Calls `validator` before writing.
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 deep partial update to the current state and persists. Only the provided keys are updated; everything else is preserved.
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({ ui: { theme: "solarized" } });
179
+ await store.patch({ theme: "solarized" });
169
180
  ```
170
181
 
171
- Arrays are replaced wholesale (not merged element-by-element). Calls `validator` before writing.
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
- defaults: {
248
- ui: { theme: "light" as string, fontSize: 14 },
249
- verbose: false,
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
- defaults: { token: "" as string },
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()` deep-merges defaults for any missing keys:
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
- defaults: {
273
- ui: { theme: "light", fontSize: 14 },
274
- verbose: false,
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: { "ui": { "theme": "dark" }, "verbose": true }
293
+ // Persisted file contains: { "theme": "dark", "verbose": true }
279
294
  const state = await store.read();
280
- // → { ui: { theme: "dark", fontSize: 14 }, verbose: true }
295
+ // → { theme: "dark", fontSize: 14, verbose: true }
281
296
  ```
282
297
 
283
298
  ### Merge Rules
284
299
 
285
- - Persisted values override defaults at every nesting level.
286
- - Missing keys fall back to defaults (recursively for nested objects).
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 schema validation to enforce config integrity on every read, write, update, and patch. When a `validator` is configured, validation is **strict by default** — invalid config fails loudly.
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 store adapters from `@crustjs/validate`:
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 { storeValidator } from "@crustjs/validate/zod";
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
- defaults: { theme: "light" as string, verbose: false },
312
- validator: storeValidator(
313
- z.object({
314
- theme: z.enum(["light", "dark"]),
315
- verbose: z.boolean(),
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
- `storeValidatorSync()` is also available for synchronous schemas.
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 { storeValidator } from "@crustjs/validate/effect";
350
+ import { field } from "@crustjs/validate/effect";
336
351
 
337
352
  const store = createStore({
338
353
  dirPath: configDir("my-cli"),
339
- defaults: { theme: "light" as string },
340
- validator: storeValidator(
341
- Schema.standardSchemaV1(
342
- Schema.Struct({ theme: Schema.Literal("light", "dark") }),
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 without `@crustjs/validate`:
370
+ You can also provide a validator function directly on each field:
351
371
 
352
372
  ```ts
353
- import type { StoreValidator } from "@crustjs/store";
354
-
355
- const validator: StoreValidator<{ theme: string }> = (value) => {
356
- const config = value as { theme: string };
357
- if (config.theme !== "light" && config.theme !== "dark") {
358
- return {
359
- ok: false,
360
- issues: [{ message: 'Must be "light" or "dark"', path: "theme" }],
361
- };
362
- }
363
- return { ok: true, value: config };
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 is `(value: unknown) => StoreValidatorResult<T> | Promise<StoreValidatorResult<T>>`, where `StoreValidatorResult<T>` is `{ ok: true, value: T } | { ok: false, issues: StoreValidatorIssue[] }`.
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 the validator transforms the value, the transformed result is persisted.
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 the result, then persists.
374
- - **Patch**: Reads raw config, applies deep partial merge, validates the result, then persists.
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: `StoreValidatorSuccess \| StoreValidatorFailure`. |
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
- * Recursively makes all properties of `T` optional.
4
+ * Resolves a type literal to its corresponding TypeScript primitive type.
3
5
  *
4
- * - Plain objects are recursed into — each nested key becomes optional.
5
- * - Arrays are left as-is (replaced wholesale, not partially updated).
6
- * - Primitives pass through unchanged.
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
- * Used by {@link Store.patch} for deep partial updates.
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
- * type Config = { ui: { theme: string; fontSize: number }; verbose: boolean };
13
- * type Partial = DeepPartial<Config>;
14
- * // → { ui?: { theme?: string; fontSize?: number }; verbose?: boolean }
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 DeepPartial<T> = T extends readonly unknown[] ? T : T extends Record<string, unknown> ? { [K in keyof T]? : DeepPartial<T[K]> } : T;
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
- * Result returned by a {@link StoreValidator} when validation succeeds.
100
+ * Infer the resolved type for a single field definition.
20
101
  *
21
- * Carries the (possibly transformed) config value from the schema.
102
+ * - **array** → `primitive[]` (or `primitive[] | undefined` if no default)
103
+ * - **has default** → `primitive` (guaranteed present)
104
+ * - **no default** → `primitive | undefined` (optional)
22
105
  */
23
- interface StoreValidatorSuccess<T> {
24
- readonly ok: true;
25
- readonly value: T;
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
- * A single validation issue reported by a {@link StoreValidator}.
114
+ * Maps a full {@link FieldsDef} record to the inferred config object type.
29
115
  *
30
- * Compatible with the canonical `ValidationIssue` shape used across the Crust
31
- * validation platform.
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
- /** Dot-path to the invalid field, or empty string for root-level issues. */
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 T - The store data shape, inferred from `defaults`.
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
- * defaults: {
84
- * ui: { theme: "light", fontSize: 14 },
85
- * verbose: false,
86
- * tags: ["default"],
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<T extends Record<string, unknown>> {
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
- * Default values for the store. Defines the data shape and provides
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
- * 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";
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
- validator?: StoreValidator<T>;
176
+ fields: F;
140
177
  /**
141
- * When `true` (the default), persisted keys not present in `defaults`
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 state and returns an updated state.
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
- * ui: { ...current.ui, theme: "dark" },
194
+ * theme: "dark",
158
195
  * }));
159
196
  * ```
160
197
  */
161
- type StoreUpdater<T> = (current: T) => NoInfer<T>;
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 state object persisted as JSON on the local filesystem.
203
+ * single typed config object persisted as JSON on the local filesystem.
167
204
  *
168
- * @typeParam T - The store data shape, inferred from `defaults`.
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
- * defaults: {
175
- * ui: { theme: "light", fontSize: 14 },
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 state = await store.read();
181
- * await store.write({ ui: { theme: "dark", fontSize: 14 }, verbose: true });
182
- * await store.update((s) => ({ ...s, verbose: false }));
183
- * await store.patch({ ui: { theme: "dark" } });
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<T> {
224
+ interface Store<TConfig> {
188
225
  /**
189
- * Reads the persisted state, merging defaults for missing keys.
226
+ * Reads the persisted config, applying field defaults for missing keys.
190
227
  *
191
- * Always returns a complete `T` — missing persisted keys are filled
192
- * from `defaults`.
228
+ * Always returns a value — fields with defaults are guaranteed present,
229
+ * fields without defaults may be `undefined`.
193
230
  *
194
- * When a validator is configured, the merged config is validated after
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 (possibly transformed by the validator).
234
+ * @returns The effective config value.
198
235
  * @throws {CrustStoreError} `PARSE` if persisted JSON is malformed.
199
- * @throws {CrustStoreError} `VALIDATION` if the config fails validation.
236
+ * @throws {CrustStoreError} `VALIDATION` if field validation fails.
200
237
  * @throws {CrustStoreError} `IO` on filesystem read failures.
201
238
  */
202
- read(): Promise<T>;
239
+ read(): Promise<TConfig>;
203
240
  /**
204
- * Atomically persists the full state object.
241
+ * Atomically persists the full config object.
205
242
  *
206
- * When a validator is configured, the config is validated before persistence.
243
+ * When fields have `validate` functions, the config is validated before
244
+ * persistence.
207
245
  *
208
- * @param state - The complete state to persist.
209
- * @throws {CrustStoreError} `VALIDATION` if the config fails validation.
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(state: NoInfer<T>): Promise<void>;
250
+ write(config: NoInfer<TConfig>): Promise<void>;
213
251
  /**
214
- * Reads current effective state, applies the updater, and persists.
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 state and returning updated state.
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 the updated config fails validation.
256
+ * @throws {CrustStoreError} `VALIDATION` if field validation fails.
225
257
  * @throws {CrustStoreError} `IO` on filesystem failures.
226
258
  */
227
- update(updater: StoreUpdater<T>): Promise<void>;
259
+ update(updater: StoreUpdater<TConfig>): Promise<void>;
228
260
  /**
229
- * Applies a deep partial update to the current state and persists.
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 deep-partial subset of the state to merge in.
235
- * @throws {CrustStoreError} `VALIDATION` if the merged config fails validation.
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: DeepPartial<NoInfer<T>>): Promise<void>;
270
+ patch(partial: Partial<NoInfer<TConfig>>): Promise<void>;
240
271
  /**
241
- * Removes the persisted state file, returning the store to
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`. The `defaults` object declares the data shape — its
517
- * TypeScript type determines the store's type parameter `T`.
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 T - The store data shape (inferred from `defaults`).
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
- * defaults: {
531
- * ui: { theme: "light", fontSize: 14 },
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 state = await store.read();
537
- * // → { ui: { theme: "light", fontSize: 14 }, verbose: false }
569
+ * const config = await store.read();
570
+ * // → { theme: "light", verbose: false, token: undefined }
538
571
  *
539
- * await store.write({ ui: { theme: "dark", fontSize: 14 }, verbose: true });
540
- * await store.update((s) => ({ ...s, verbose: false }));
541
- * await store.patch({ ui: { theme: "dark" } });
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 T extends Record<string, unknown>>(options: CreateStoreOptions<T>): Store<T>;
546
- export { stateDir, dataDir, createStore, configDir, cacheDir, ValidationErrorDetails, StoreValidatorSuccess, StoreValidatorResult, StoreValidatorIssue, StoreValidatorFailure, StoreValidator, StoreUpdater, StoreErrorCode, Store, PlatformEnv, CrustStoreError, CreateStoreOptions };
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 K extends Error{code;details;cause;constructor(q,B,z){super(B);this.name="CrustStoreError",this.code=q,this.details=z}is(q){return this.code===q}withCause(q){return this.cause=q,this}}import{homedir as f}from"os";import{join as Q}from"path";var P="config";function _(){return{platform:process.platform,env:process.env,homedir:f()}}function T(q){if(!q||q.trim().length===0)throw new K("PATH","appName must be a non-empty string",{path:q??""});if(q.includes("/")||q.includes("\\"))throw new K("PATH","appName must not contain path separators",{path:q})}function h(q){if(!q||q.trim().length===0)throw new K("PATH","name must be a non-empty string",{path:q??""});if(q.includes("/")||q.includes("\\"))throw new K("PATH","name must not contain path separators",{path:q});if(q.endsWith(".json"))throw new K("PATH","name must not include the .json extension",{path:q})}function v(q){if(!q||q.trim().length===0)throw new K("PATH","dirPath must be a non-empty string",{path:q??""});if(!(q.startsWith("/")||/^[A-Za-z]:[/\\]/.test(q)))throw new K("PATH","dirPath must be an absolute path",{path:q});if(q.endsWith(".json"))throw new K("PATH","dirPath must be a directory path, not a file path (should not end with .json)",{path:q})}function F(q,B,z,G){let H=q.env[B],W=H&&H.trim().length>0?H:Q(q.homedir,...z);return Q(W,G)}function O(q){throw new K("PATH",`Unsupported platform: ${q}`,{path:q})}function m(q,B){T(q);let z=B??_();switch(z.platform){case"linux":case"darwin":return F(z,"XDG_CONFIG_HOME",[".config"],q);case"win32":{let G=z.env.APPDATA,H=G&&G.trim().length>0?G:Q(z.homedir,"AppData","Roaming");return Q(H,q)}default:O(z.platform)}}function u(q,B){T(q);let z=B??_();switch(z.platform){case"linux":case"darwin":return F(z,"XDG_DATA_HOME",[".local","share"],q);case"win32":{let G=z.env.LOCALAPPDATA,H=G&&G.trim().length>0?G:Q(z.homedir,"AppData","Local");return Q(H,q,"Data")}default:O(z.platform)}}function d(q,B){T(q);let z=B??_();switch(z.platform){case"linux":case"darwin":return F(z,"XDG_STATE_HOME",[".local","state"],q);case"win32":{let G=z.env.LOCALAPPDATA,H=G&&G.trim().length>0?G:Q(z.homedir,"AppData","Local");return Q(H,q,"State")}default:O(z.platform)}}function o(q,B){T(q);let z=B??_();switch(z.platform){case"linux":case"darwin":return F(z,"XDG_CACHE_HOME",[".cache"],q);case"win32":{let G=z.env.LOCALAPPDATA,H=G&&G.trim().length>0?G:Q(z.homedir,"AppData","Local");return Q(H,q,"Cache")}default:O(z.platform)}}function b(q,B){v(q);let z=B??P;if(B!==void 0)h(B);return Q(q,`${z}.json`)}function V(q){return q!==null&&typeof q==="object"&&!Array.isArray(q)}function x(q){if(Array.isArray(q))return[...q];if(V(q)){let B={};for(let[z,G]of Object.entries(q))B[z]=x(G);return B}return q}function J(q,B,z=!0){let G={};for(let[H,W]of Object.entries(B))if(q!==void 0&&H in q){let X=q[H];if(V(W)&&V(X))G[H]=J(X,W,z);else G[H]=x(X)}else G[H]=x(W);if(!z&&q!==void 0){for(let[H,W]of Object.entries(q))if(!(H in B))G[H]=x(W)}return G}import{randomUUID as s}from"crypto";import{mkdir as i,readFile as p,rename as n,rm as l,unlink as t,writeFile as r}from"fs/promises";import{dirname as a,join as e}from"path";async function j(q){let B;try{B=await p(q,"utf-8")}catch(z){if(D(z))return;throw new K("IO",`Failed to read config file: ${q}`,{path:q,operation:"read"}).withCause(z)}try{return JSON.parse(B)}catch(z){throw new K("PARSE",`Malformed JSON in config file: ${q}`,{path:q}).withCause(z)}}async function R(q,B){let z=a(q),G=e(z,`.config-${s()}.tmp`);try{await i(z,{recursive:!0})}catch(H){throw new K("IO",`Failed to create config directory: ${z}`,{path:q,operation:"write"}).withCause(H)}try{let H=JSON.stringify(B,null,"\t");await r(G,H,"utf-8")}catch(H){throw await U(G),new K("IO",`Failed to write config file: ${q}`,{path:q,operation:"write"}).withCause(H)}try{await n(G,q)}catch(H){throw await U(G),new K("IO",`Failed to finalize config file: ${q}`,{path:q,operation:"write"}).withCause(H)}}async function A(q){try{await t(q)}catch(B){if(D(B))return;throw new K("IO",`Failed to delete config file: ${q}`,{path:q,operation:"delete"}).withCause(B)}}function D(q){return q!==null&&typeof q==="object"&&"code"in q&&q.code==="ENOENT"}async function U(q){try{await l(q,{force:!0})}catch{}}function qq(q){let{dirPath:B,name:z,defaults:G,validator:H,pruneUnknown:W}=q,X=b(B,z),C=W??!0;async function M(L,Y){if(!H)return L;let Z=await H(L);if(Z.ok)return Z.value;let I=Z.issues.map(($)=>({message:$.message,path:$.path})),y=I.map(($)=>$.path?` - ${$.path}: ${$.message}`:` - ${$.message}`),N=y.length>0?`Store validation failed (${Y})
3
- ${y.join(`
4
- `)}`:`Store validation failed (${Y})`;throw new K("VALIDATION",N,{operation:Y,issues:I})}async function S(){let L=await j(X);return J(L,G,C)}async function k(){let L=await S();return M(L,"read")}async function g(L){let Y=await M(L,"write");await R(X,Y)}async function c(L){let Y=await S(),Z=L(Y),I=await M(Z,"update");await R(X,I)}async function w(L){let Y=await S(),Z=J(L,Y,!1),I=await M(Z,"patch");await R(X,I)}async function E(){await A(X)}return{read:k,write:g,update:c,patch:w,reset:E}}export{d as stateDir,u as dataDir,qq as createStore,m as configDir,o as cacheDir,K as CrustStoreError};
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};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@crustjs/store",
3
- "version": "0.0.3",
3
+ "version": "0.0.4",
4
4
  "description": "DX-first, typed persistence for CLI apps with config/data/state/cache separation",
5
5
  "type": "module",
6
6
  "license": "MIT",