@crustjs/store 0.1.0 → 0.2.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 +195 -155
- package/dist/index.d.ts +119 -107
- package/dist/index.js +3 -3
- package/package.json +4 -3
package/README.md
CHANGED
|
@@ -25,12 +25,12 @@ pnpm add @crustjs/store
|
|
|
25
25
|
import { createStore, configDir } from "@crustjs/store";
|
|
26
26
|
|
|
27
27
|
const store = createStore({
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
28
|
+
dirPath: configDir("my-cli"),
|
|
29
|
+
fields: {
|
|
30
|
+
theme: { type: "string", default: "light" },
|
|
31
|
+
fontSize: { type: "number", default: 14 },
|
|
32
|
+
verbose: { type: "boolean", default: false },
|
|
33
|
+
},
|
|
34
34
|
});
|
|
35
35
|
|
|
36
36
|
// Read state (returns defaults when no persisted file exists)
|
|
@@ -64,47 +64,41 @@ No explicit generics needed — types are inferred from your `fields` definition
|
|
|
64
64
|
| `cacheDir` | Regenerable cached data | HTTP caches, compiled assets |
|
|
65
65
|
|
|
66
66
|
```ts
|
|
67
|
-
import {
|
|
68
|
-
createStore,
|
|
69
|
-
configDir,
|
|
70
|
-
dataDir,
|
|
71
|
-
stateDir,
|
|
72
|
-
cacheDir,
|
|
73
|
-
} from "@crustjs/store";
|
|
67
|
+
import { createStore, configDir, dataDir, stateDir, cacheDir } from "@crustjs/store";
|
|
74
68
|
|
|
75
69
|
// User preferences → ~/.config/my-cli/config.json
|
|
76
70
|
const config = createStore({
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
71
|
+
dirPath: configDir("my-cli"),
|
|
72
|
+
fields: {
|
|
73
|
+
theme: { type: "string", default: "light" },
|
|
74
|
+
verbose: { type: "boolean", default: false },
|
|
75
|
+
},
|
|
82
76
|
});
|
|
83
77
|
|
|
84
78
|
// App data → ~/.local/share/my-cli/config.json
|
|
85
79
|
const data = createStore({
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
80
|
+
dirPath: dataDir("my-cli"),
|
|
81
|
+
fields: {
|
|
82
|
+
bookmarks: { type: "string", array: true, default: [] },
|
|
83
|
+
},
|
|
90
84
|
});
|
|
91
85
|
|
|
92
86
|
// Runtime state → ~/.local/state/my-cli/config.json
|
|
93
87
|
const state = createStore({
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
88
|
+
dirPath: stateDir("my-cli"),
|
|
89
|
+
fields: {
|
|
90
|
+
lastOpened: { type: "string", default: "" },
|
|
91
|
+
scrollY: { type: "number", default: 0 },
|
|
92
|
+
},
|
|
99
93
|
});
|
|
100
94
|
|
|
101
95
|
// Cache → ~/.cache/my-cli/config.json
|
|
102
96
|
const cache = createStore({
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
97
|
+
dirPath: cacheDir("my-cli"),
|
|
98
|
+
fields: {
|
|
99
|
+
etag: { type: "string", default: "" },
|
|
100
|
+
payload: { type: "string", default: "" },
|
|
101
|
+
},
|
|
108
102
|
});
|
|
109
103
|
```
|
|
110
104
|
|
|
@@ -137,12 +131,55 @@ const store = createStore(options);
|
|
|
137
131
|
|
|
138
132
|
#### Options
|
|
139
133
|
|
|
140
|
-
| Option | Type
|
|
141
|
-
| -------------- |
|
|
142
|
-
| `dirPath` | `string`
|
|
143
|
-
| `name` | `string`
|
|
144
|
-
| `fields` | `FieldsDef`
|
|
145
|
-
| `pruneUnknown` | `boolean`
|
|
134
|
+
| Option | Type | Required | Description |
|
|
135
|
+
| -------------- | ----------------------------------------------------------------- | -------- | ------------------------------------------------------------------------------------------------------------ |
|
|
136
|
+
| `dirPath` | `string` | Yes | Absolute directory path where the JSON file is stored. |
|
|
137
|
+
| `name` | `string` | No | Store name used as filename (default `"config"` → `config.json`). |
|
|
138
|
+
| `fields` | `FieldsDef` | Yes | Field definitions defining the store's data shape, types, defaults, and optional validation. |
|
|
139
|
+
| `pruneUnknown` | `boolean` | No | Drop unknown persisted keys on read (default `true`). |
|
|
140
|
+
| `access` | `"default" \| "private" \| { file?: number; directory?: number }` | No | Persistence visibility. Use `"private"` for owner-only secret stores, or pass explicit Unix permission bits. |
|
|
141
|
+
|
|
142
|
+
#### Securing secrets
|
|
143
|
+
|
|
144
|
+
Config/state stores that hold tokens or API keys should not be world-readable. Pass `access: "private"` so the file is owner-only on Unix:
|
|
145
|
+
|
|
146
|
+
```ts
|
|
147
|
+
const auth = createStore({
|
|
148
|
+
dirPath: configDir("my-cli"),
|
|
149
|
+
name: "auth",
|
|
150
|
+
fields: { token: { type: "string" } },
|
|
151
|
+
access: "private",
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
await auth.write({ token: "secret" }); // auth.json is 0600 even under a permissive umask
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
`access: "private"` maps to `0600` for the file (`rw-------`) and `0700` for the parent directory (`rwx------`, only when created). The file mode is enforced on the temp file before the atomic rename, so the persisted file is never momentarily group/other-readable.
|
|
158
|
+
|
|
159
|
+
The built-in string presets are intentionally small:
|
|
160
|
+
|
|
161
|
+
| Access | File | Directory | Use case |
|
|
162
|
+
| --------------------- | ----------------------------------------------------- | ----------------------------------------------------- | ------------------------------------------------------------------------- |
|
|
163
|
+
| omitted / `"default"` | Platform default, usually `0644` after a common umask | Platform default, usually `0755` after a common umask | Non-secret stores where the user's environment should decide permissions. |
|
|
164
|
+
| `"private"` | `0600` | `0700` | Secret-bearing stores such as tokens, API keys, and local auth state. |
|
|
165
|
+
|
|
166
|
+
Advanced callers can provide explicit permission bits without adding more named presets:
|
|
167
|
+
|
|
168
|
+
```ts
|
|
169
|
+
const store = createStore({
|
|
170
|
+
dirPath: configDir("my-cli"),
|
|
171
|
+
name: "auth",
|
|
172
|
+
fields: { token: { type: "string" } },
|
|
173
|
+
// Owner-only secret store (same as access: "private")
|
|
174
|
+
access: { file: 0o600, directory: 0o700 },
|
|
175
|
+
// Group-readable store; group ownership is managed outside @crustjs/store
|
|
176
|
+
// access: { file: 0o640, directory: 0o750 },
|
|
177
|
+
// World-readable non-secret store
|
|
178
|
+
// access: { file: 0o644, directory: 0o755 },
|
|
179
|
+
});
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
**Platform behavior:** `access` permission bits are enforced on **macOS and Linux** (and other Unix systems), where POSIX permission bits are checked by the OS. On **Windows** they are **not enforced** — Windows uses ACLs, not Unix bits, so `access: "private"` does not make a file owner-only there. The store does not throw or warn in this case. On Windows, confidentiality relies on the ACL inherited from the parent directory; the per-user profile location resolved by `configDir` / `stateDir` (under `%APPDATA%` / `%LOCALAPPDATA%`) is already restricted to the user. If you need strong secret confidentiality on Windows, use the OS credential store (Windows Credential Manager / DPAPI) rather than file permissions.
|
|
146
183
|
|
|
147
184
|
### `store.read()`
|
|
148
185
|
|
|
@@ -170,8 +207,8 @@ Reads the current effective state, applies the updater function, and atomically
|
|
|
170
207
|
|
|
171
208
|
```ts
|
|
172
209
|
await store.update((current) => ({
|
|
173
|
-
|
|
174
|
-
|
|
210
|
+
...current,
|
|
211
|
+
theme: "dark",
|
|
175
212
|
}));
|
|
176
213
|
```
|
|
177
214
|
|
|
@@ -260,21 +297,21 @@ const dir = configDir("my-cli");
|
|
|
260
297
|
|
|
261
298
|
// Default store → ~/.config/my-cli/config.json
|
|
262
299
|
const settingsStore = createStore({
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
300
|
+
dirPath: dir,
|
|
301
|
+
fields: {
|
|
302
|
+
theme: { type: "string", default: "light" },
|
|
303
|
+
fontSize: { type: "number", default: 14 },
|
|
304
|
+
verbose: { type: "boolean", default: false },
|
|
305
|
+
},
|
|
269
306
|
});
|
|
270
307
|
|
|
271
308
|
// Auth store → ~/.config/my-cli/auth.json
|
|
272
309
|
const authStore = createStore({
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
310
|
+
dirPath: dir,
|
|
311
|
+
name: "auth",
|
|
312
|
+
fields: {
|
|
313
|
+
token: { type: "string", default: "" },
|
|
314
|
+
},
|
|
278
315
|
});
|
|
279
316
|
```
|
|
280
317
|
|
|
@@ -288,12 +325,12 @@ When a persisted file exists, `read()` fills missing keys from field `defaults`:
|
|
|
288
325
|
|
|
289
326
|
```ts
|
|
290
327
|
const store = createStore({
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
328
|
+
dirPath: configDir("my-cli"),
|
|
329
|
+
fields: {
|
|
330
|
+
theme: { type: "string", default: "light" },
|
|
331
|
+
fontSize: { type: "number", default: 14 },
|
|
332
|
+
verbose: { type: "boolean", default: false },
|
|
333
|
+
},
|
|
297
334
|
});
|
|
298
335
|
|
|
299
336
|
// Persisted file contains: { "theme": "dark", "verbose": true }
|
|
@@ -320,19 +357,20 @@ Add per-field validation to enforce config integrity on every read, write, updat
|
|
|
320
357
|
### Schema-driven validation
|
|
321
358
|
|
|
322
359
|
The easiest way to add validation is with the built-in `field()` factory.
|
|
323
|
-
It builds a
|
|
324
|
-
|
|
360
|
+
It builds a `FieldDef` from any [Standard Schema v1](https://standardschema.dev/)
|
|
361
|
+
object. Crust stores raw values; the schema validates and transforms them on
|
|
362
|
+
read/write.
|
|
325
363
|
|
|
326
364
|
```ts
|
|
327
365
|
import { z } from "zod";
|
|
328
366
|
import { createStore, configDir, field } from "@crustjs/store";
|
|
329
367
|
|
|
330
368
|
const store = createStore({
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
369
|
+
dirPath: configDir("my-cli"),
|
|
370
|
+
fields: {
|
|
371
|
+
theme: field(z.enum(["light", "dark"]).default("light")),
|
|
372
|
+
verbose: { type: "boolean", default: false },
|
|
373
|
+
},
|
|
336
374
|
});
|
|
337
375
|
|
|
338
376
|
// write() validates before persisting
|
|
@@ -351,40 +389,41 @@ import * as Schema from "effect/Schema";
|
|
|
351
389
|
import { createStore, configDir, field } from "@crustjs/store";
|
|
352
390
|
|
|
353
391
|
const store = createStore({
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
392
|
+
dirPath: configDir("my-cli"),
|
|
393
|
+
fields: {
|
|
394
|
+
theme: field(Schema.standardSchemaV1(Schema.Literal("light", "dark"))),
|
|
395
|
+
},
|
|
358
396
|
});
|
|
359
397
|
```
|
|
360
398
|
|
|
399
|
+
Missing persisted values flow through the schema as `undefined`:
|
|
400
|
+
|
|
401
|
+
- `field(z.string().default("x"))` returns `"x"` on read when the field
|
|
402
|
+
is missing from disk.
|
|
403
|
+
- `field(z.string().optional())` returns `undefined`.
|
|
404
|
+
- `field(z.string())` fails read-time validation with a `VALIDATION`
|
|
405
|
+
error when the field is missing.
|
|
406
|
+
|
|
361
407
|
#### Schema-derived defaults and TypeScript
|
|
362
408
|
|
|
363
409
|
Standard Schema v1 has no spec-portable type-level access to schema
|
|
364
|
-
defaults.
|
|
410
|
+
defaults. Crust does not inspect schemas to populate runtime metadata;
|
|
411
|
+
defaults are materialized on read by validating `undefined` through the
|
|
412
|
+
schema. As a result:
|
|
365
413
|
|
|
366
|
-
- `field(z.string().default("x"))`
|
|
367
|
-
|
|
368
|
-
- `field(z.string(), { default: "x" })`
|
|
369
|
-
narrows the inferred config type to `string`.
|
|
414
|
+
- `field(z.string().default("x"))` returns `"x"` on read, but the
|
|
415
|
+
inferred config type is `string | undefined` (NOT narrowed).
|
|
416
|
+
- `field(z.string(), { default: "x" })` keeps `"x"` as the Crust-level
|
|
417
|
+
default AND narrows the inferred config type to `string`.
|
|
370
418
|
|
|
371
419
|
Prefer the explicit form when the field is required to always have a
|
|
372
420
|
value at use sites.
|
|
373
421
|
|
|
374
422
|
#### `field()` throws on invalid input
|
|
375
423
|
|
|
376
|
-
`field()` throws `CrustStoreError("DEFINITION")` when handed a
|
|
377
|
-
|
|
378
|
-
|
|
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.
|
|
424
|
+
`field()` throws `CrustStoreError("DEFINITION")` when handed a value that
|
|
425
|
+
is not a Standard Schema v1 object. `type` is optional for schema-backed
|
|
426
|
+
fields; pass it through `opts` only as legacy metadata for tooling.
|
|
388
427
|
|
|
389
428
|
### Custom validators
|
|
390
429
|
|
|
@@ -395,18 +434,18 @@ issue path.
|
|
|
395
434
|
|
|
396
435
|
```ts
|
|
397
436
|
const store = createStore({
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
437
|
+
dirPath: configDir("my-cli"),
|
|
438
|
+
fields: {
|
|
439
|
+
theme: {
|
|
440
|
+
type: "string",
|
|
441
|
+
default: "light",
|
|
442
|
+
validate(value) {
|
|
443
|
+
if (value !== "light" && value !== "dark") {
|
|
444
|
+
throw new Error('Must be "light" or "dark"');
|
|
445
|
+
}
|
|
446
|
+
},
|
|
447
|
+
},
|
|
448
|
+
},
|
|
410
449
|
});
|
|
411
450
|
```
|
|
412
451
|
|
|
@@ -448,15 +487,15 @@ with `CrustStoreError("VALIDATION")` and an issue message tagged
|
|
|
448
487
|
import { CrustStoreError } from "@crustjs/store";
|
|
449
488
|
|
|
450
489
|
try {
|
|
451
|
-
|
|
490
|
+
await store.read();
|
|
452
491
|
} catch (err) {
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
492
|
+
if (err instanceof CrustStoreError && err.is("VALIDATION")) {
|
|
493
|
+
// err.details is { operation: "read" | "write" | "update" | "patch", issues: StoreValidatorIssue[] }
|
|
494
|
+
console.error(`Validation failed during ${err.details.operation}:`);
|
|
495
|
+
for (const issue of err.details.issues) {
|
|
496
|
+
console.error(` ${issue.path || "(root)"}: ${issue.message}`);
|
|
497
|
+
}
|
|
498
|
+
}
|
|
460
499
|
}
|
|
461
500
|
```
|
|
462
501
|
|
|
@@ -464,12 +503,13 @@ try {
|
|
|
464
503
|
|
|
465
504
|
All errors thrown by `@crustjs/store` are instances of `CrustStoreError` with a typed `code` property:
|
|
466
505
|
|
|
467
|
-
| Code | When
|
|
468
|
-
| ------------ |
|
|
469
|
-
| `PATH` | Invalid `dirPath`, invalid `name`, unsupported platform
|
|
470
|
-
| `PARSE` | Malformed JSON in persisted config file
|
|
471
|
-
| `IO` | Filesystem read, write, or delete failure
|
|
472
|
-
| `VALIDATION` | Config fails validator on read, write, update, or patch
|
|
506
|
+
| Code | When | Details |
|
|
507
|
+
| ------------ | --------------------------------------------------------- | ----------------------- |
|
|
508
|
+
| `PATH` | Invalid `dirPath`, invalid `name`, unsupported platform | `{ path: string }` |
|
|
509
|
+
| `PARSE` | Malformed JSON in persisted config file | `{ path: string }` |
|
|
510
|
+
| `IO` | Filesystem read, write, or delete failure | `{ path, operation }` |
|
|
511
|
+
| `VALIDATION` | Config fails validator on read, write, update, or patch | `{ operation, issues }` |
|
|
512
|
+
| `DEFINITION` | `field()` received an invalid (non-Standard-Schema) input | `{ vendor? }` |
|
|
473
513
|
|
|
474
514
|
### Catching errors by code
|
|
475
515
|
|
|
@@ -477,19 +517,19 @@ All errors thrown by `@crustjs/store` are instances of `CrustStoreError` with a
|
|
|
477
517
|
import { CrustStoreError } from "@crustjs/store";
|
|
478
518
|
|
|
479
519
|
try {
|
|
480
|
-
|
|
520
|
+
const state = await store.read();
|
|
481
521
|
} catch (err) {
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
522
|
+
if (err instanceof CrustStoreError) {
|
|
523
|
+
switch (err.code) {
|
|
524
|
+
case "PARSE":
|
|
525
|
+
console.error(`Corrupt file at ${err.details.path}`);
|
|
526
|
+
console.error("Delete the file and retry, or fix the JSON manually.");
|
|
527
|
+
break;
|
|
528
|
+
case "IO":
|
|
529
|
+
console.error(`File ${err.details.operation} failed: ${err.message}`);
|
|
530
|
+
break;
|
|
531
|
+
}
|
|
532
|
+
}
|
|
493
533
|
}
|
|
494
534
|
```
|
|
495
535
|
|
|
@@ -499,8 +539,8 @@ The `.is()` method narrows the error type so `details` is fully typed:
|
|
|
499
539
|
|
|
500
540
|
```ts
|
|
501
541
|
if (err instanceof CrustStoreError && err.is("IO")) {
|
|
502
|
-
|
|
503
|
-
|
|
542
|
+
// err.details is { path: string; operation: "read" | "write" | "delete" }
|
|
543
|
+
console.error(err.details.operation, err.details.path);
|
|
504
544
|
}
|
|
505
545
|
```
|
|
506
546
|
|
|
@@ -510,7 +550,7 @@ if (err instanceof CrustStoreError && err.is("IO")) {
|
|
|
510
550
|
|
|
511
551
|
```ts
|
|
512
552
|
if (err instanceof CrustStoreError && err.is("VALIDATION")) {
|
|
513
|
-
|
|
553
|
+
console.error("Validation error:", err.cause);
|
|
514
554
|
}
|
|
515
555
|
```
|
|
516
556
|
|
|
@@ -524,38 +564,38 @@ All types are exported for use in your application:
|
|
|
524
564
|
|
|
525
565
|
```ts
|
|
526
566
|
import type {
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
567
|
+
CreateStoreOptions,
|
|
568
|
+
Store,
|
|
569
|
+
StoreUpdater,
|
|
570
|
+
FieldDef,
|
|
571
|
+
FieldsDef,
|
|
572
|
+
FieldOptions,
|
|
573
|
+
InferStoreConfig,
|
|
574
|
+
StoreValidatorIssue,
|
|
575
|
+
StoreErrorCode,
|
|
576
|
+
ValidationErrorDetails,
|
|
577
|
+
DefinitionErrorDetails,
|
|
578
|
+
ValueType,
|
|
579
|
+
PlatformEnv,
|
|
540
580
|
} from "@crustjs/store";
|
|
541
581
|
```
|
|
542
582
|
|
|
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
|
-
| `
|
|
551
|
-
| `
|
|
552
|
-
| `
|
|
553
|
-
| `
|
|
554
|
-
| `
|
|
555
|
-
| `
|
|
556
|
-
| `
|
|
557
|
-
| `PlatformEnv` | Injectable platform environment for testing path helpers.
|
|
558
|
-
| `CrustStoreError` | Typed error class with `code`, `details`, and `cause`.
|
|
583
|
+
| Type | Description |
|
|
584
|
+
| ------------------------ | ------------------------------------------------------------------------------------------------------ |
|
|
585
|
+
| `CreateStoreOptions` | Options object for `createStore()`. |
|
|
586
|
+
| `Store` | Store instance with `read`, `write`, `update`, `patch`, `reset`. |
|
|
587
|
+
| `StoreUpdater` | Updater function type `(current: T) => T`. |
|
|
588
|
+
| `FieldDef` | Single field definition with `type`, optional `default`, and optional `validate`. |
|
|
589
|
+
| `FieldsDef` | Record of field names to `FieldDef` definitions. |
|
|
590
|
+
| `FieldOptions` | Optional Crust metadata accepted by the `field()` factory (`type`, `default`, `array`, `description`). |
|
|
591
|
+
| `InferStoreConfig` | Inferred store state type from a `FieldsDef` definition. |
|
|
592
|
+
| `StoreValidatorIssue` | `{ message: string, path: string }`. |
|
|
593
|
+
| `ValidationErrorDetails` | Error details for `VALIDATION` code: `{ operation, issues }`. |
|
|
594
|
+
| `DefinitionErrorDetails` | Error details for `DEFINITION` code: `{ vendor? }`. |
|
|
595
|
+
| `StoreErrorCode` | Union of error codes: `"PATH" \| "PARSE" \| "IO" \| "VALIDATION" \| "DEFINITION"`. |
|
|
596
|
+
| `ValueType` | Supported field type literals: `"string" \| "number" \| "boolean"`. |
|
|
597
|
+
| `PlatformEnv` | Injectable platform environment for testing path helpers. |
|
|
598
|
+
| `CrustStoreError` | Typed error class with `code`, `details`, and `cause`. |
|
|
559
599
|
|
|
560
600
|
## Scope & Non-Goals
|
|
561
601
|
|
package/dist/index.d.ts
CHANGED
|
@@ -1,13 +1,6 @@
|
|
|
1
|
+
import { BaseValueType, ResolvePrimitive } from "@crustjs/utils";
|
|
1
2
|
/** Supported type literals for store fields. */
|
|
2
|
-
type ValueType =
|
|
3
|
-
/**
|
|
4
|
-
* Resolves a type literal to its corresponding TypeScript primitive type.
|
|
5
|
-
*
|
|
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;
|
|
3
|
+
type ValueType = BaseValueType;
|
|
11
4
|
/** Shared fields present on every store field definition. */
|
|
12
5
|
interface FieldDefBase<V> {
|
|
13
6
|
/** Human-readable description for documentation and tooling. */
|
|
@@ -41,6 +34,8 @@ interface FieldDefBase<V> {
|
|
|
41
34
|
} | Promise<{
|
|
42
35
|
value: unknown;
|
|
43
36
|
}>;
|
|
37
|
+
/** Internal marker: validate missing values as `undefined` for schema-backed fields. */
|
|
38
|
+
validateMissing?: true;
|
|
44
39
|
}
|
|
45
40
|
/** Base for scalar (non-array) fields — `array` must be omitted. */
|
|
46
41
|
interface ScalarFieldBase<V> extends FieldDefBase<V> {
|
|
@@ -110,9 +105,18 @@ interface BooleanArrayFieldDef extends ArrayFieldBase<boolean[]> {
|
|
|
110
105
|
* } satisfies FieldsDef;
|
|
111
106
|
* ```
|
|
112
107
|
*/
|
|
113
|
-
|
|
108
|
+
interface RawScalarFieldDef extends ScalarFieldBase<unknown> {
|
|
109
|
+
type?: never;
|
|
110
|
+
default?: unknown;
|
|
111
|
+
}
|
|
112
|
+
interface RawArrayFieldDef extends ArrayFieldBase<unknown[]> {
|
|
113
|
+
type?: never;
|
|
114
|
+
default?: readonly unknown[];
|
|
115
|
+
}
|
|
116
|
+
type FieldDef = StringFieldDef | NumberFieldDef | BooleanFieldDef | StringArrayFieldDef | NumberArrayFieldDef | BooleanArrayFieldDef | RawScalarFieldDef | RawArrayFieldDef;
|
|
114
117
|
/** Record mapping field names to their definitions. */
|
|
115
118
|
type FieldsDef = Record<string, FieldDef>;
|
|
119
|
+
declare const FIELD_SCHEMA_OUTPUT: unique symbol;
|
|
116
120
|
/**
|
|
117
121
|
* Infer the resolved type for a single field definition.
|
|
118
122
|
*
|
|
@@ -121,12 +125,20 @@ type FieldsDef = Record<string, FieldDef>;
|
|
|
121
125
|
* - **no default** → `primitive | undefined` (optional)
|
|
122
126
|
*/
|
|
123
127
|
type InferFieldValue<F extends FieldDef> = F extends {
|
|
128
|
+
readonly [FIELD_SCHEMA_OUTPUT]?: infer O;
|
|
129
|
+
} ? F extends {
|
|
130
|
+
default: infer D;
|
|
131
|
+
} ? D : O | undefined : F extends {
|
|
132
|
+
type: ValueType;
|
|
133
|
+
} ? F extends {
|
|
124
134
|
array: true;
|
|
125
135
|
} ? F extends {
|
|
126
136
|
default: readonly ResolvePrimitive<F["type"]>[];
|
|
127
137
|
} ? ResolvePrimitive<F["type"]>[] : ResolvePrimitive<F["type"]>[] | undefined : F extends {
|
|
128
138
|
default: ResolvePrimitive<F["type"]>;
|
|
129
|
-
} ? ResolvePrimitive<F["type"]> : ResolvePrimitive<F["type"]> | undefined
|
|
139
|
+
} ? ResolvePrimitive<F["type"]> : ResolvePrimitive<F["type"]> | undefined : F extends {
|
|
140
|
+
default: infer D;
|
|
141
|
+
} ? D : unknown;
|
|
130
142
|
/**
|
|
131
143
|
* Maps a full {@link FieldsDef} record to the inferred config object type.
|
|
132
144
|
*
|
|
@@ -149,6 +161,22 @@ interface StoreValidatorIssue {
|
|
|
149
161
|
/** Field name where the issue occurred. */
|
|
150
162
|
path: string;
|
|
151
163
|
}
|
|
164
|
+
/** Explicit Unix permission bits for store persistence. */
|
|
165
|
+
interface StorePermissionBits {
|
|
166
|
+
/** Permission bits for the persisted store file (e.g. `0o600`). */
|
|
167
|
+
file?: number;
|
|
168
|
+
/** Permission bits for the store's parent directory (e.g. `0o700`). */
|
|
169
|
+
directory?: number;
|
|
170
|
+
}
|
|
171
|
+
/**
|
|
172
|
+
* Persistence visibility for store writes.
|
|
173
|
+
*
|
|
174
|
+
* - `"default"` or omitted uses platform defaults.
|
|
175
|
+
* - `"private"` keeps the store owner-only on Unix (`0600` file, `0700` directory).
|
|
176
|
+
* - An object lets advanced callers provide explicit Unix permission bits for
|
|
177
|
+
* group-readable or public non-secret stores without expanding the preset list.
|
|
178
|
+
*/
|
|
179
|
+
type StoreAccess = "default" | "private" | StorePermissionBits;
|
|
152
180
|
/**
|
|
153
181
|
* Options for {@link createStore}.
|
|
154
182
|
*
|
|
@@ -198,6 +226,25 @@ interface CreateStoreOptions<F extends FieldsDef> {
|
|
|
198
226
|
* @default true
|
|
199
227
|
*/
|
|
200
228
|
pruneUnknown?: boolean;
|
|
229
|
+
/**
|
|
230
|
+
* Persistence visibility for store writes.
|
|
231
|
+
*
|
|
232
|
+
* Use `"private"` for config/state stores holding secrets (tokens, API keys)
|
|
233
|
+
* that should stay owner-only. On Unix this writes the file as `0600` and
|
|
234
|
+
* creates the parent directory as `0700`; on Windows it is not enforced
|
|
235
|
+
* because Windows uses ACLs instead of Unix permission bits.
|
|
236
|
+
*
|
|
237
|
+
* The only built-in presets are `"default"` and `"private"`. Advanced callers
|
|
238
|
+
* can pass explicit bits with `{ file, directory }` for group-readable or public
|
|
239
|
+
* non-secret stores. The file bits are enforced exactly on every write
|
|
240
|
+
* regardless of `umask`. Directory bits apply only when a write creates the
|
|
241
|
+
* directory; pre-existing directories are left untouched.
|
|
242
|
+
*
|
|
243
|
+
* @default "default"
|
|
244
|
+
* @example "private"
|
|
245
|
+
* @example { file: 0o600, directory: 0o700 }
|
|
246
|
+
*/
|
|
247
|
+
access?: StoreAccess;
|
|
201
248
|
}
|
|
202
249
|
/**
|
|
203
250
|
* Receives the current effective config and returns an updated config.
|
|
@@ -339,11 +386,10 @@ interface ValidationErrorDetails {
|
|
|
339
386
|
*
|
|
340
387
|
* Returned when a `field()` call cannot produce a valid `FieldDef` from the
|
|
341
388
|
* provided schema and options — e.g. the input is not a Standard Schema v1
|
|
342
|
-
* object
|
|
343
|
-
* supplied so the runtime CLI type cannot be inferred.
|
|
389
|
+
* object.
|
|
344
390
|
*/
|
|
345
391
|
interface DefinitionErrorDetails {
|
|
346
|
-
/** The schema vendor name, when
|
|
392
|
+
/** The schema vendor name, when available. */
|
|
347
393
|
vendor?: string;
|
|
348
394
|
}
|
|
349
395
|
/**
|
|
@@ -453,16 +499,14 @@ declare class CrustStoreError<C extends StoreErrorCode = StoreErrorCode> extends
|
|
|
453
499
|
*/
|
|
454
500
|
withCause(cause: unknown): this;
|
|
455
501
|
}
|
|
456
|
-
import {
|
|
502
|
+
import { BaseValueType as BaseValueType2 } from "@crustjs/utils";
|
|
503
|
+
import { InferOutput, StandardSchema } from "@crustjs/utils/schema";
|
|
457
504
|
/**
|
|
458
|
-
* Optional
|
|
459
|
-
* {@link field}.
|
|
505
|
+
* Optional Crust metadata for the `FieldDef` shape produced by {@link field}.
|
|
460
506
|
*
|
|
461
|
-
* Every key is optional.
|
|
462
|
-
*
|
|
463
|
-
*
|
|
464
|
-
* Valibot, ArkType), `type` MUST be supplied explicitly because no
|
|
465
|
-
* inference is available.
|
|
507
|
+
* Every key is optional. Crust does not infer metadata from schemas; schemas
|
|
508
|
+
* validate and transform actual runtime values, including missing values passed
|
|
509
|
+
* as `undefined`.
|
|
466
510
|
*
|
|
467
511
|
* No `validate` key — validation flows exclusively through the schema.
|
|
468
512
|
* If extra checks are needed, refine the schema with `.refine(...)`
|
|
@@ -472,122 +516,79 @@ import { InferOutput, StandardSchema } from "@crustjs/schema-utils";
|
|
|
472
516
|
* `default` key when the user wants a non-`undefined` field.
|
|
473
517
|
*/
|
|
474
518
|
interface FieldOptions<T = unknown> {
|
|
475
|
-
type?:
|
|
519
|
+
type?: BaseValueType2;
|
|
476
520
|
description?: string;
|
|
477
521
|
/**
|
|
478
|
-
*
|
|
479
|
-
* a value for it.
|
|
480
|
-
*
|
|
481
|
-
*
|
|
482
|
-
* NOT narrow the TypeScript type — pass it explicitly here for tight
|
|
483
|
-
* typing.
|
|
522
|
+
* Crust metadata default for this field when the persisted state does not
|
|
523
|
+
* contain a value for it. Schema defaults should usually live in the schema;
|
|
524
|
+
* missing persisted values are validated as `undefined`, so `.default()` works
|
|
525
|
+
* naturally at read time.
|
|
484
526
|
*/
|
|
485
527
|
default?: T;
|
|
486
528
|
/** Mark this field as an array (collects values into an array). */
|
|
487
529
|
array?: true;
|
|
488
530
|
}
|
|
489
|
-
type
|
|
490
|
-
type
|
|
491
|
-
|
|
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;
|
|
531
|
+
type RawSchemaFieldDef<Out> = {
|
|
532
|
+
readonly type?: never;
|
|
533
|
+
readonly array?: never;
|
|
516
534
|
readonly description?: string;
|
|
517
|
-
readonly default: D;
|
|
518
535
|
readonly validate: (value: unknown) => Promise<{
|
|
519
536
|
value: unknown;
|
|
520
537
|
}>;
|
|
538
|
+
readonly validateMissing: true;
|
|
539
|
+
readonly [FIELD_SCHEMA_OUTPUT]?: Out;
|
|
521
540
|
};
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
541
|
+
type SchemaFieldDefWithOptions<
|
|
542
|
+
Out,
|
|
543
|
+
Type extends FieldOptions["type"],
|
|
544
|
+
Array extends true | undefined,
|
|
545
|
+
D = never
|
|
546
|
+
> = Omit<RawSchemaFieldDef<Out>, "type" | "array"> & (Type extends NonNullable<FieldOptions["type"]> ? {
|
|
547
|
+
readonly type: Type;
|
|
548
|
+
} : {
|
|
549
|
+
readonly type?: never;
|
|
550
|
+
}) & (Array extends true ? {
|
|
525
551
|
readonly array: true;
|
|
526
|
-
|
|
527
|
-
readonly
|
|
528
|
-
|
|
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;
|
|
552
|
+
} : {
|
|
553
|
+
readonly array?: never;
|
|
554
|
+
}) & ([D] extends [never] ? unknown : {
|
|
539
555
|
readonly default: D;
|
|
540
|
-
|
|
541
|
-
value: unknown;
|
|
542
|
-
}>;
|
|
543
|
-
};
|
|
556
|
+
});
|
|
544
557
|
/**
|
|
545
558
|
* `FieldDef` inferred from a Standard Schema, with no narrowed default.
|
|
546
559
|
*
|
|
547
|
-
* Schema
|
|
548
|
-
*
|
|
549
|
-
*
|
|
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.
|
|
560
|
+
* Standard Schema v1 has no spec-portable type-level access to defaults, so
|
|
561
|
+
* schema `.default()` does not narrow the inferred TypeScript type. Pass
|
|
562
|
+
* `field(schema, { default: x })` explicitly to narrow.
|
|
556
563
|
*/
|
|
557
|
-
type
|
|
558
|
-
S extends StandardSchema,
|
|
559
|
-
D
|
|
560
|
-
> = IsArrayOutput<S> extends true ? ArrayFieldDefWithDefault<PrimitiveToValueType<StripUndefined<ArrayElementOutput<S>>>, D> : ScalarFieldDefWithDefault<ResolveScalarType<S>, D>;
|
|
564
|
+
type SchemaFieldDef<S extends StandardSchema> = RawSchemaFieldDef<InferOutput<S>>;
|
|
561
565
|
/**
|
|
562
566
|
* Define a `@crustjs/store` field from any Standard Schema v1.
|
|
563
567
|
*
|
|
564
568
|
* Returns a value that satisfies store's `FieldDef` discriminated union.
|
|
565
|
-
*
|
|
566
|
-
*
|
|
567
|
-
*
|
|
568
|
-
* any key explicitly — explicit values win silently.
|
|
569
|
+
* Crust stores raw values and lets the schema validate and transform them on
|
|
570
|
+
* read/write; missing persisted values flow through the schema as `undefined`,
|
|
571
|
+
* so `.optional()` and `.default()` behave naturally.
|
|
569
572
|
*
|
|
570
573
|
* The returned `validate` is an async function that throws an `Error` with
|
|
571
574
|
* the schema's normalized issue messages on failure (matches store's
|
|
572
575
|
* `FieldDef.validate` contract).
|
|
573
576
|
*
|
|
574
|
-
* **Type-level defaults**:
|
|
575
|
-
*
|
|
576
|
-
*
|
|
577
|
+
* **Type-level defaults**: Standard Schema v1 has no spec-portable type-level
|
|
578
|
+
* access to defaults. For tight typing of default-bearing fields, pass
|
|
579
|
+
* `default` via `opts`:
|
|
577
580
|
*
|
|
578
581
|
* ```ts
|
|
579
|
-
* field(z.string().default("x"))
|
|
580
|
-
* field(z.string(), { default: "x" })
|
|
582
|
+
* field(z.string().default("x")) // value materializes on read; state: string | undefined
|
|
583
|
+
* field(z.string(), { default: "x" }) // Crust-level default; state: string
|
|
581
584
|
* ```
|
|
582
585
|
*
|
|
583
586
|
* @param schema - Any Standard Schema v1 object (Zod schemas natively;
|
|
584
587
|
* Effect schemas wrapped via `Schema.standardSchemaV1`;
|
|
585
588
|
* Valibot/ArkType/Sury/etc. as-is)
|
|
586
|
-
* @param opts - Optional
|
|
587
|
-
* the introspected values silently
|
|
589
|
+
* @param opts - Optional Crust-level metadata
|
|
588
590
|
* @throws {CrustStoreError} With code `"DEFINITION"` when the input is not
|
|
589
|
-
* a Standard Schema
|
|
590
|
-
* and `opts.type` was not supplied.
|
|
591
|
+
* a Standard Schema v1 object.
|
|
591
592
|
*
|
|
592
593
|
* @example Zod
|
|
593
594
|
* ```ts
|
|
@@ -620,11 +621,22 @@ type SchemaFieldDefWithDefault<
|
|
|
620
621
|
declare function field<S extends StandardSchema>(schema: S): SchemaFieldDef<S>;
|
|
621
622
|
declare function field<
|
|
622
623
|
S extends StandardSchema,
|
|
623
|
-
D extends InferOutput<S
|
|
624
|
+
D extends InferOutput<S>,
|
|
625
|
+
const Type extends FieldOptions<InferOutput<S>>["type"] = undefined,
|
|
626
|
+
const Array extends true | undefined = undefined
|
|
624
627
|
>(schema: S, opts: FieldOptions<InferOutput<S>> & {
|
|
625
628
|
default: D;
|
|
626
|
-
|
|
627
|
-
|
|
629
|
+
type?: Type;
|
|
630
|
+
array?: Array;
|
|
631
|
+
}): SchemaFieldDefWithOptions<InferOutput<S>, Type, Array, D>;
|
|
632
|
+
declare function field<
|
|
633
|
+
S extends StandardSchema,
|
|
634
|
+
const Type extends FieldOptions<InferOutput<S>>["type"] = undefined,
|
|
635
|
+
const Array extends true | undefined = undefined
|
|
636
|
+
>(schema: S, opts: FieldOptions<InferOutput<S>> & {
|
|
637
|
+
type?: Type;
|
|
638
|
+
array?: Array;
|
|
639
|
+
}): SchemaFieldDefWithOptions<InferOutput<S>, Type, Array>;
|
|
628
640
|
/**
|
|
629
641
|
* Encapsulates runtime environment values needed for path resolution.
|
|
630
642
|
*
|
|
@@ -779,4 +791,4 @@ declare function cacheDir(appName: string, env?: PlatformEnv): string;
|
|
|
779
791
|
* ```
|
|
780
792
|
*/
|
|
781
793
|
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 };
|
|
794
|
+
export { stateDir, field, dataDir, createStore, configDir, cacheDir, ValueType, ValidationErrorDetails, StoreValidatorIssue, StoreUpdater, StorePermissionBits, StoreErrorCode, StoreAccess, 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
|
-
`)}`;throw new
|
|
2
|
+
class X 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{isStandardSchema as d,normalizeStandardIssues as o}from"@crustjs/utils/schema";function l(q){return async(H)=>{let G=await q["~standard"].validate(H);if(G.issues){let Q=o(G.issues).map((Y)=>Y.path?`${Y.path}: ${Y.message}`:Y.message);throw Error(Q.join("; "))}return{value:G.value}}}function i(q,H){if(!d(q))throw new X("DEFINITION",`field(): argument must be a Standard Schema v1 object (got ${typeof q})`,{});let K={validate:l(q),validateMissing:!0};if(H?.type!==void 0)K.type=H.type;if(H?.array===!0)K.array=!0;if(H?.description!==void 0)K.description=H.description;if(H&&"default"in H)K.default=H.default;return K}import{homedir as s}from"os";import{join as J}from"path";var t="config";function b(){return{platform:process.platform,env:process.env,homedir:s()}}function j(q){if(!q||q.trim().length===0)throw new X("PATH","appName must be a non-empty string",{path:q??""});if(q.includes("/")||q.includes("\\"))throw new X("PATH","appName must not contain path separators",{path:q})}function r(q){if(!q||q.trim().length===0)throw new X("PATH","name must be a non-empty string",{path:q??""});if(q.includes("/")||q.includes("\\"))throw new X("PATH","name must not contain path separators",{path:q});if(q.endsWith(".json"))throw new X("PATH","name must not include the .json extension",{path:q})}function a(q){if(!q||q.trim().length===0)throw new X("PATH","dirPath must be a non-empty string",{path:q??""});if(!(q.startsWith("/")||/^[A-Za-z]:[/\\]/.test(q)))throw new X("PATH","dirPath must be an absolute path",{path:q});if(q.endsWith(".json"))throw new X("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 Q=q.env[H],Y=Q&&Q.trim().length>0?Q:J(q.homedir,...G);return J(Y,K)}function D(q){throw new X("PATH",`Unsupported platform: ${q}`,{path:q})}function e(q,H){j(q);let G=H??b();switch(G.platform){case"linux":case"darwin":return C(G,"XDG_CONFIG_HOME",[".config"],q);case"win32":{let K=G.env.APPDATA,Q=K&&K.trim().length>0?K:J(G.homedir,"AppData","Roaming");return J(Q,q)}default:D(G.platform)}}function qq(q,H){j(q);let G=H??b();switch(G.platform){case"linux":case"darwin":return C(G,"XDG_DATA_HOME",[".local","share"],q);case"win32":{let K=G.env.LOCALAPPDATA,Q=K&&K.trim().length>0?K:J(G.homedir,"AppData","Local");return J(Q,q,"Data")}default:D(G.platform)}}function Gq(q,H){j(q);let G=H??b();switch(G.platform){case"linux":case"darwin":return C(G,"XDG_STATE_HOME",[".local","state"],q);case"win32":{let K=G.env.LOCALAPPDATA,Q=K&&K.trim().length>0?K:J(G.homedir,"AppData","Local");return J(Q,q,"State")}default:D(G.platform)}}function Hq(q,H){j(q);let G=H??b();switch(G.platform){case"linux":case"darwin":return C(G,"XDG_CACHE_HOME",[".cache"],q);case"win32":{let K=G.env.LOCALAPPDATA,Q=K&&K.trim().length>0?K:J(G.homedir,"AppData","Local");return J(Q,q,"Cache")}default:D(G.platform)}}function S(q,H){a(q);let G=H??t;if(H!==void 0)r(H);return J(q,`${G}.json`)}import{coerceBooleanString as Iq,tryCoerceNumber as Wq}from"@crustjs/utils";function y(q,H,G=!0){let K={};for(let[Q,Y]of Object.entries(H))if(q&&Q in q){let Z=q[Q];K[Q]=Array.isArray(Z)?[...Z]:Z}else if("default"in Y&&Y.default!==void 0)K[Q]=Array.isArray(Y.default)?[...Y.default]:Y.default;if(!G&&q!==void 0){for(let[Q,Y]of Object.entries(q))if(!(Q in H))K[Q]=Array.isArray(Y)?[...Y]:Y}return K}import{randomUUID as Kq}from"crypto";import{chmod as g,mkdir as Qq,readFile as Xq,rename as Yq,rm as $q,unlink as Lq,writeFile as Bq}from"fs/promises";import{dirname as Zq,join as _q}from"path";async function E(q){let H;try{H=await Xq(q,"utf-8")}catch(G){if(v(G))return;throw new X("IO",`Failed to read config file: ${q}`,{path:q,operation:"read"}).withCause(G)}try{return JSON.parse(H)}catch(G){throw new X("PARSE",`Malformed JSON in config file: ${q}`,{path:q}).withCause(G)}}async function z(q,H,G={}){let K=process.platform==="win32",Q=K?void 0:G.fileMode,Y=K?void 0:G.directoryMode,Z=Zq(q),T=_q(Z,`.config-${Kq()}.tmp`),V;try{V=await Qq(Z,{recursive:!0,mode:Y})}catch(I){throw new X("IO",`Failed to create config directory: ${Z}`,{path:q,operation:"write"}).withCause(I)}if(Y!==void 0&&V!==void 0)try{await g(Z,Y)}catch(I){throw new X("IO",`Failed to set permissions on config directory: ${Z}`,{path:q,operation:"write"}).withCause(I)}try{let I=JSON.stringify(H,null,"\t");if(await Bq(T,I,{encoding:"utf-8",mode:Q}),Q!==void 0)await g(T,Q)}catch(I){throw await N(T),new X("IO",`Failed to write config file: ${q}`,{path:q,operation:"write"}).withCause(I)}try{await Yq(T,q)}catch(I){throw await N(T),new X("IO",`Failed to finalize config file: ${q}`,{path:q,operation:"write"}).withCause(I)}}async function P(q){try{await Lq(q)}catch(H){if(v(H))return;throw new X("IO",`Failed to delete config file: ${q}`,{path:q,operation:"delete"}).withCause(H)}}function v(q){return q!==null&&typeof q==="object"&&"code"in q&&q.code==="ENOENT"}async function N(q){try{await $q(q,{force:!0})}catch{}}function f(q){return typeof q==="object"&&q!==null&&"value"in q&&!("ok"in q)}function Jq(q){if(q==="private")return{fileMode:384,directoryMode:448};if(q===void 0||q==="default")return{};return{fileMode:q.file,directoryMode:q.directory}}function Oq(q){let{dirPath:H,name:G,fields:K,pruneUnknown:Q,access:Y}=q,Z=S(H,G),T=Q??!0,V=Jq(Y);function I($,L){if(L===void 0)return $;if(L==="number"&&typeof $==="string")return Wq($)??$;if(L==="boolean"&&typeof $==="string")return Iq($);return $}function R($){let B={...$};for(let[_,W]of Object.entries(K)){if(!(_ in B))continue;let O=B[_];if(W.array===!0&&Array.isArray(O)){B[_]=O.map((M)=>I(M,W.type));continue}B[_]=I(O,W.type)}return B}async function U($,L){let B=[],_=$;for(let[W,O]of Object.entries(K)){if(!O.validate)continue;let M=_[W];if(M===void 0&&O.validateMissing!==!0)continue;let A;try{A=await O.validate(M)}catch(x){let F=x instanceof Error?x.message:"Validation failed";B.push({message:F,path:W});continue}if(A===void 0)continue;if(f(A)){let x=A.value;if(L==="read"){if(M===void 0)_[W]=x;continue}if(!Bun.deepEquals(x,M)){let F;try{F=await O.validate(x)}catch(w){let n=w instanceof Error?w.message:"re-validation failed";B.push({message:`read-unstable transform: ${n}`,path:W});continue}if(F!==void 0&&(!f(F)||!Bun.deepEquals(F.value,x))){B.push({message:"read-unstable transform: output would be transformed again on re-read",path:W});continue}_[W]=x}continue}throw TypeError("FieldDef.validate must return void or { value } (see @crustjs/store validation docs).")}if(B.length>0){let W=B.map((M)=>` - ${M.path}: ${M.message}`),O=`Store validation failed (${L})
|
|
3
|
+
${W.join(`
|
|
4
|
+
`)}`;throw new X("VALIDATION",O,{operation:L,issues:B})}}async function k(){let $=await E(Z),L=y($,K,T);return R(L)}async function c(){let $=await k();return await U($,"read"),$}async function h($){let L=R($);await U(L,"write"),await z(Z,L,V)}async function u($){let L=await k(),B=$(L),_=R(B);await U(_,"update"),await z(Z,_,V)}async function m($){let B={...await k(),...$},_=R(B);await U(_,"patch"),await z(Z,_,V)}async function p(){await P(Z)}return{read:c,write:h,update:u,patch:m,reset:p}}export{Gq as stateDir,i as field,qq as dataDir,Oq as createStore,e as configDir,Hq as cacheDir,X as CrustStoreError};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@crustjs/store",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.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",
|
|
@@ -48,13 +48,14 @@
|
|
|
48
48
|
"devDependencies": {
|
|
49
49
|
"@crustjs/config": "0.0.0",
|
|
50
50
|
"bunup": "^0.16.31",
|
|
51
|
-
"effect": "3.
|
|
51
|
+
"effect": "3.21.2",
|
|
52
52
|
"zod": "^4.0.0"
|
|
53
53
|
},
|
|
54
54
|
"peerDependencies": {
|
|
55
55
|
"typescript": "^6.0.3"
|
|
56
56
|
},
|
|
57
57
|
"dependencies": {
|
|
58
|
-
"@crustjs/
|
|
58
|
+
"@crustjs/utils": "0.0.3",
|
|
59
|
+
"@standard-schema/spec": "^1.1.0"
|
|
59
60
|
}
|
|
60
61
|
}
|