@crustjs/store 0.2.0 → 0.3.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/LICENSE +21 -0
- package/README.md +3 -604
- package/dist/index.d.ts +624 -717
- package/dist/index.js +727 -4
- package/package.json +21 -13
package/dist/index.d.ts
CHANGED
|
@@ -1,794 +1,701 @@
|
|
|
1
|
-
|
|
1
|
+
//#region ../utils/src/json.d.ts
|
|
2
|
+
/** Any value representable in a JSON document. */
|
|
3
|
+
type JsonValue = string | number | boolean | null | readonly JsonValue[] | JsonObject;
|
|
4
|
+
interface JsonObject {
|
|
5
|
+
[key: string]: JsonValue;
|
|
6
|
+
}
|
|
7
|
+
/** Preserve a type when all of its properties are recursively JSON-compatible. */
|
|
8
|
+
type JsonCompatible<T> = T extends JsonValue ? T : T extends ((...arguments_: never[]) => void) ? never : T extends readonly unknown[] ? { [K in keyof T]: JsonCompatible<T[K]>; } : T extends object ? { [K in keyof T]: JsonCompatible<T[K]>; } : never;
|
|
9
|
+
//#endregion
|
|
10
|
+
//#region ../utils/src/primitive.d.ts
|
|
11
|
+
/** Supported primitive type literals shared by Crust packages. */
|
|
12
|
+
type BaseValueType = "string" | "number" | "boolean";
|
|
13
|
+
/**
|
|
14
|
+
* Resolves a primitive type literal to its corresponding TypeScript type.
|
|
15
|
+
*
|
|
16
|
+
* @example
|
|
17
|
+
* ```ts
|
|
18
|
+
* type Port = ResolvePrimitive<"number">;
|
|
19
|
+
* // ^? number
|
|
20
|
+
* ```
|
|
21
|
+
*/
|
|
22
|
+
type ResolvePrimitive<T extends BaseValueType> = T extends "string" ? string : T extends "number" ? number : T extends "boolean" ? boolean : never;
|
|
23
|
+
//#endregion
|
|
24
|
+
//#region ../utils/src/schema.d.ts
|
|
25
|
+
/** The Standard Typed types interface. */
|
|
26
|
+
interface StandardSchemaTypes<Input = unknown, Output = Input> {
|
|
27
|
+
/** The input type of the schema. */
|
|
28
|
+
readonly input: Input;
|
|
29
|
+
/** The output type of the schema. */
|
|
30
|
+
readonly output: Output;
|
|
31
|
+
}
|
|
32
|
+
/** The result interface if validation succeeds. */
|
|
33
|
+
interface StandardSchemaSuccessResult<Output> {
|
|
34
|
+
/** The typed output value. */
|
|
35
|
+
readonly value: Output;
|
|
36
|
+
/** A falsy value for `issues` indicates success. */
|
|
37
|
+
readonly issues?: undefined;
|
|
38
|
+
}
|
|
39
|
+
/** The result interface if validation fails. */
|
|
40
|
+
interface StandardSchemaFailureResult {
|
|
41
|
+
/** The issues of failed validation. */
|
|
42
|
+
readonly issues: ReadonlyArray<StandardSchemaIssue>;
|
|
43
|
+
}
|
|
44
|
+
/** The result interface of the validate function. */
|
|
45
|
+
type StandardSchemaResult<Output> = StandardSchemaSuccessResult<Output> | StandardSchemaFailureResult;
|
|
46
|
+
/** The issue interface of the failure output. */
|
|
47
|
+
interface StandardSchemaIssue {
|
|
48
|
+
/** The error message of the issue. */
|
|
49
|
+
readonly message: string;
|
|
50
|
+
/** The path of the issue, if any. */
|
|
51
|
+
readonly path?: ReadonlyArray<PropertyKey | StandardSchemaPathSegment> | undefined;
|
|
52
|
+
}
|
|
53
|
+
/** The path segment interface of the issue. */
|
|
54
|
+
interface StandardSchemaPathSegment {
|
|
55
|
+
/** The key representing a path segment. */
|
|
56
|
+
readonly key: PropertyKey;
|
|
57
|
+
}
|
|
58
|
+
/** The Standard Schema properties interface. */
|
|
59
|
+
interface StandardSchemaProps<Input = unknown, Output = Input> {
|
|
60
|
+
/** The version number of the standard. */
|
|
61
|
+
readonly version: 1;
|
|
62
|
+
/** The vendor name of the schema library. */
|
|
63
|
+
readonly vendor: string;
|
|
64
|
+
/** Inferred types associated with the schema. */
|
|
65
|
+
readonly types?: StandardSchemaTypes<Input, Output> | undefined;
|
|
66
|
+
/** Validates unknown input values. */
|
|
67
|
+
readonly validate: (value: StandardSchemaTypes["input"]) => StandardSchemaResult<Output> | Promise<StandardSchemaResult<Output>>;
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* A Standard Schema-compatible schema object.
|
|
71
|
+
*
|
|
72
|
+
* Any schema library implementing the Standard Schema v1 spec
|
|
73
|
+
* (Zod, Effect, Valibot, ArkType, etc.) produces objects matching this type.
|
|
74
|
+
*/
|
|
75
|
+
type StandardSchema<Input = unknown, Output = Input> = {
|
|
76
|
+
/** The Standard Schema properties. */
|
|
77
|
+
readonly "~standard": StandardSchemaProps<Input, Output>;
|
|
78
|
+
};
|
|
79
|
+
/** Infer the output type produced by a Standard Schema on success. */
|
|
80
|
+
type InferOutput<S extends StandardSchema> = NonNullable<S["~standard"]["types"]>["output"];
|
|
81
|
+
//#endregion
|
|
82
|
+
//#region src/types.d.ts
|
|
2
83
|
/** Supported type literals for store fields. */
|
|
3
84
|
type ValueType = BaseValueType;
|
|
4
85
|
/** Shared fields present on every store field definition. */
|
|
5
86
|
interface FieldDefBase<V> {
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
} | Promise<{
|
|
35
|
-
value: unknown;
|
|
36
|
-
}>;
|
|
37
|
-
/** Internal marker: validate missing values as `undefined` for schema-backed fields. */
|
|
38
|
-
validateMissing?: true;
|
|
87
|
+
/** Standard Schemas are an exclusive definition mode. */
|
|
88
|
+
schema?: never;
|
|
89
|
+
/**
|
|
90
|
+
* Optional per-field validation function.
|
|
91
|
+
*
|
|
92
|
+
* Called during `read`, `write`, `update`, and `patch` operations when
|
|
93
|
+
* the field has a value (not `undefined`).
|
|
94
|
+
*
|
|
95
|
+
* Return shapes:
|
|
96
|
+
* - `void` (or `Promise<void>`) — validation-only; the input value is
|
|
97
|
+
* persisted as-is.
|
|
98
|
+
* - `{ value }` (or `Promise<{ value }>`) — a transform produced an
|
|
99
|
+
* output within the field's declared type. On `write` / `update` /
|
|
100
|
+
* `patch`, the transformed value replaces the input before
|
|
101
|
+
* persistence and is re-validated once to catch read-unstable
|
|
102
|
+
* transforms. Outputs outside the declared type are rejected with
|
|
103
|
+
* `VALIDATION`. On `read`, the transformed value is discarded —
|
|
104
|
+
* reads always return the on-disk value verbatim.
|
|
105
|
+
* - Throwing an error — the value is rejected; the error message is
|
|
106
|
+
* captured as a validation issue with the field name as `path`.
|
|
107
|
+
*
|
|
108
|
+
* @param value - The field value to validate.
|
|
109
|
+
*/
|
|
110
|
+
validate?: (value: V) => void | Promise<void> | {
|
|
111
|
+
value: V;
|
|
112
|
+
} | Promise<{
|
|
113
|
+
value: V;
|
|
114
|
+
}>;
|
|
39
115
|
}
|
|
40
116
|
/** Base for scalar (non-array) fields — `array` must be omitted. */
|
|
41
117
|
interface ScalarFieldBase<V> extends FieldDefBase<V> {
|
|
42
|
-
|
|
43
|
-
|
|
118
|
+
/** Must be omitted for scalar fields — set to `true` for array fields. */
|
|
119
|
+
array?: never;
|
|
44
120
|
}
|
|
45
121
|
/** A scalar string field. */
|
|
46
122
|
interface StringFieldDef extends ScalarFieldBase<string> {
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
123
|
+
type: "string";
|
|
124
|
+
/** Default string value when the field is not persisted. */
|
|
125
|
+
default?: string;
|
|
50
126
|
}
|
|
51
127
|
/** A scalar number field. */
|
|
52
128
|
interface NumberFieldDef extends ScalarFieldBase<number> {
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
129
|
+
type: "number";
|
|
130
|
+
/** Default number value when the field is not persisted. */
|
|
131
|
+
default?: number;
|
|
56
132
|
}
|
|
57
133
|
/** A scalar boolean field. */
|
|
58
134
|
interface BooleanFieldDef extends ScalarFieldBase<boolean> {
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
135
|
+
type: "boolean";
|
|
136
|
+
/** Default boolean value when the field is not persisted. */
|
|
137
|
+
default?: boolean;
|
|
62
138
|
}
|
|
63
139
|
/** Base for array fields — `array` is required as `true`. */
|
|
64
140
|
interface ArrayFieldBase<V> extends FieldDefBase<V> {
|
|
65
|
-
|
|
66
|
-
|
|
141
|
+
/** Collect values into an array. */
|
|
142
|
+
array: true;
|
|
67
143
|
}
|
|
68
144
|
/** An array of strings field. */
|
|
69
145
|
interface StringArrayFieldDef extends ArrayFieldBase<string[]> {
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
146
|
+
type: "string";
|
|
147
|
+
/** Default string array value when the field is not persisted. */
|
|
148
|
+
default?: readonly string[];
|
|
73
149
|
}
|
|
74
150
|
/** An array of numbers field. */
|
|
75
151
|
interface NumberArrayFieldDef extends ArrayFieldBase<number[]> {
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
152
|
+
type: "number";
|
|
153
|
+
/** Default number array value when the field is not persisted. */
|
|
154
|
+
default?: readonly number[];
|
|
79
155
|
}
|
|
80
156
|
/** An array of booleans field. */
|
|
81
157
|
interface BooleanArrayFieldDef extends ArrayFieldBase<boolean[]> {
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
158
|
+
type: "boolean";
|
|
159
|
+
/** Default boolean array value when the field is not persisted. */
|
|
160
|
+
default?: readonly boolean[];
|
|
85
161
|
}
|
|
86
|
-
/**
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
* ```ts
|
|
95
|
-
* const fields = {
|
|
96
|
-
* theme: { type: "string", default: "light" },
|
|
97
|
-
* verbose: { type: "boolean", default: false },
|
|
98
|
-
* tags: { type: "string", array: true, default: [] },
|
|
99
|
-
* token: { type: "string" }, // optional — no default
|
|
100
|
-
* port: {
|
|
101
|
-
* type: "number",
|
|
102
|
-
* default: 3000,
|
|
103
|
-
* validate: (v) => { if (v < 1 || v > 65535) throw new Error("invalid port"); },
|
|
104
|
-
* },
|
|
105
|
-
* } satisfies FieldsDef;
|
|
106
|
-
* ```
|
|
107
|
-
*/
|
|
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[];
|
|
162
|
+
/** A field whose Standard Schema exclusively owns its value semantics. */
|
|
163
|
+
interface SchemaFieldDef {
|
|
164
|
+
/** Standard Schema that owns validation, transformation, defaults, and optionality. */
|
|
165
|
+
schema: StandardSchema<unknown, unknown>;
|
|
166
|
+
type?: BaseValueType;
|
|
167
|
+
array?: true;
|
|
168
|
+
default?: never;
|
|
169
|
+
validate?: never;
|
|
115
170
|
}
|
|
116
|
-
|
|
171
|
+
/**
|
|
172
|
+
* Defines a single field in a store's config schema.
|
|
173
|
+
*
|
|
174
|
+
* Core definitions are discriminated by `type` and `array`. Schema-backed
|
|
175
|
+
* definitions use a `schema` key and cannot mix schema semantics with
|
|
176
|
+
* `default` or `validate`.
|
|
177
|
+
*
|
|
178
|
+
* @example
|
|
179
|
+
* ```ts
|
|
180
|
+
* const fields = {
|
|
181
|
+
* theme: { type: "string", default: "light" },
|
|
182
|
+
* port: { schema: z.coerce.number().int().positive() },
|
|
183
|
+
* } satisfies FieldsDef;
|
|
184
|
+
* ```
|
|
185
|
+
*/
|
|
186
|
+
type FieldDef = StringFieldDef | NumberFieldDef | BooleanFieldDef | StringArrayFieldDef | NumberArrayFieldDef | BooleanArrayFieldDef | SchemaFieldDef;
|
|
117
187
|
/** Record mapping field names to their definitions. */
|
|
118
188
|
type FieldsDef = Record<string, FieldDef>;
|
|
119
|
-
|
|
189
|
+
type JsonCompatibleFields<F extends FieldsDef> = { [K in keyof F]: F[K] extends {
|
|
190
|
+
schema: StandardSchema<unknown, infer Output>;
|
|
191
|
+
} ? Exclude<Output, undefined> extends JsonCompatible<Exclude<Output, undefined>> ? F[K] : never : F[K]; };
|
|
120
192
|
/**
|
|
121
|
-
* Infer the resolved type for a single field definition.
|
|
122
|
-
*
|
|
123
|
-
* - **
|
|
124
|
-
* - **
|
|
125
|
-
* - **
|
|
126
|
-
|
|
193
|
+
* Infer the resolved type for a single field definition.
|
|
194
|
+
*
|
|
195
|
+
* - **schema** → the schema's exact output type
|
|
196
|
+
* - **array** → `primitive[]` (or `primitive[] | undefined` if no default)
|
|
197
|
+
* - **has default** → `primitive` (guaranteed present)
|
|
198
|
+
* - **no default** → `primitive | undefined` (optional)
|
|
199
|
+
*/
|
|
127
200
|
type InferFieldValue<F extends FieldDef> = F extends {
|
|
128
|
-
|
|
201
|
+
schema: infer S extends StandardSchema;
|
|
202
|
+
} ? InferOutput<S> : F extends {
|
|
203
|
+
type: ValueType;
|
|
129
204
|
} ? F extends {
|
|
130
|
-
|
|
131
|
-
} ? D : O | undefined : F extends {
|
|
132
|
-
type: ValueType;
|
|
205
|
+
array: true;
|
|
133
206
|
} ? F extends {
|
|
134
|
-
|
|
135
|
-
} ? F extends {
|
|
136
|
-
default: readonly ResolvePrimitive<F["type"]>[];
|
|
207
|
+
default: readonly ResolvePrimitive<F["type"]>[];
|
|
137
208
|
} ? ResolvePrimitive<F["type"]>[] : ResolvePrimitive<F["type"]>[] | undefined : F extends {
|
|
138
|
-
|
|
139
|
-
} ? ResolvePrimitive<F["type"]> : ResolvePrimitive<F["type"]> | undefined :
|
|
140
|
-
default: infer D;
|
|
141
|
-
} ? D : unknown;
|
|
209
|
+
default: ResolvePrimitive<F["type"]>;
|
|
210
|
+
} ? ResolvePrimitive<F["type"]> : ResolvePrimitive<F["type"]> | undefined : never;
|
|
142
211
|
/**
|
|
143
|
-
* Maps a full {@link FieldsDef} record to the inferred config object type.
|
|
144
|
-
*
|
|
145
|
-
* @example
|
|
146
|
-
* ```ts
|
|
147
|
-
* type Config = InferStoreConfig<{
|
|
148
|
-
* theme: { type: "string"; default: "light" };
|
|
149
|
-
* verbose: { type: "boolean" };
|
|
150
|
-
* }>;
|
|
151
|
-
* // → { theme: string; verbose: boolean | undefined }
|
|
152
|
-
* ```
|
|
153
|
-
*/
|
|
154
|
-
type InferStoreConfig<F extends FieldsDef> = { [K in keyof F]
|
|
212
|
+
* Maps a full {@link FieldsDef} record to the inferred config object type.
|
|
213
|
+
*
|
|
214
|
+
* @example
|
|
215
|
+
* ```ts
|
|
216
|
+
* type Config = InferStoreConfig<{
|
|
217
|
+
* theme: { type: "string"; default: "light" };
|
|
218
|
+
* verbose: { type: "boolean" };
|
|
219
|
+
* }>;
|
|
220
|
+
* // → { theme: string; verbose: boolean | undefined }
|
|
221
|
+
* ```
|
|
222
|
+
*/
|
|
223
|
+
type InferStoreConfig<F extends FieldsDef> = { [K in keyof F]: InferFieldValue<F[K]>; };
|
|
155
224
|
/**
|
|
156
|
-
* A single validation issue reported during store operations.
|
|
157
|
-
*/
|
|
225
|
+
* A single validation issue reported during store operations.
|
|
226
|
+
*/
|
|
158
227
|
interface StoreValidatorIssue {
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
228
|
+
/** Human-readable description of the validation failure. */
|
|
229
|
+
message: string;
|
|
230
|
+
/** Field name where the issue occurred. */
|
|
231
|
+
path: string;
|
|
163
232
|
}
|
|
164
233
|
/** Explicit Unix permission bits for store persistence. */
|
|
165
234
|
interface StorePermissionBits {
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
235
|
+
/** Permission bits for the persisted store file (e.g. `0o600`). */
|
|
236
|
+
file?: number;
|
|
237
|
+
/** Permission bits for the store's parent directory (e.g. `0o700`). */
|
|
238
|
+
directory?: number;
|
|
170
239
|
}
|
|
171
240
|
/**
|
|
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
|
-
*/
|
|
241
|
+
* Persistence visibility for store writes.
|
|
242
|
+
*
|
|
243
|
+
* - `"default"` or omitted uses platform defaults.
|
|
244
|
+
* - `"private"` keeps the store owner-only on Unix (`0600` file, `0700` directory).
|
|
245
|
+
* - An object lets advanced callers provide explicit Unix permission bits for
|
|
246
|
+
* group-readable or public non-secret stores without expanding the preset list.
|
|
247
|
+
*/
|
|
179
248
|
type StoreAccess = "default" | "private" | StorePermissionBits;
|
|
180
249
|
/**
|
|
181
|
-
* Options for {@link createStore}.
|
|
182
|
-
*
|
|
183
|
-
* @typeParam F - The field definitions record (inferred via `const` generic).
|
|
184
|
-
*
|
|
185
|
-
* @example
|
|
186
|
-
* ```ts
|
|
187
|
-
* const store = createStore({
|
|
188
|
-
* dirPath: configDir("my-cli"),
|
|
189
|
-
*
|
|
190
|
-
*
|
|
191
|
-
*
|
|
192
|
-
*
|
|
193
|
-
*
|
|
194
|
-
*
|
|
195
|
-
*
|
|
196
|
-
|
|
250
|
+
* Options for {@link createStore}.
|
|
251
|
+
*
|
|
252
|
+
* @typeParam F - The field definitions record (inferred via `const` generic).
|
|
253
|
+
*
|
|
254
|
+
* @example
|
|
255
|
+
* ```ts
|
|
256
|
+
* const store = createStore({
|
|
257
|
+
* dirPath: configDir("my-cli"),
|
|
258
|
+
* name: "config",
|
|
259
|
+
* fields: {
|
|
260
|
+
* theme: { type: "string", default: "light" },
|
|
261
|
+
* verbose: { type: "boolean", default: false },
|
|
262
|
+
* token: { type: "string" },
|
|
263
|
+
* },
|
|
264
|
+
* });
|
|
265
|
+
* ```
|
|
266
|
+
*/
|
|
197
267
|
interface CreateStoreOptions<F extends FieldsDef> {
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
*/
|
|
247
|
-
access?: StoreAccess;
|
|
268
|
+
/**
|
|
269
|
+
* Absolute directory path where the store JSON file is persisted.
|
|
270
|
+
*
|
|
271
|
+
* Use a path helper ({@link configDir}, {@link dataDir}, etc.) to resolve
|
|
272
|
+
* the platform-standard directory from an app name.
|
|
273
|
+
*/
|
|
274
|
+
dirPath: string;
|
|
275
|
+
/**
|
|
276
|
+
* Store name used as the JSON filename in the directory.
|
|
277
|
+
*
|
|
278
|
+
* For example, `name: "auth"` produces `auth.json`. Must not contain path
|
|
279
|
+
* separators or the `.json` extension.
|
|
280
|
+
*/
|
|
281
|
+
name: string;
|
|
282
|
+
/**
|
|
283
|
+
* Field definitions that declare the store's config schema.
|
|
284
|
+
*
|
|
285
|
+
* Each key maps to a {@link FieldDef}: either a core definition or a
|
|
286
|
+
* schema-backed definition. Core fields without `default` are optional;
|
|
287
|
+
* schema fields use the schema's exact output type.
|
|
288
|
+
*/
|
|
289
|
+
fields: F & JsonCompatibleFields<F>;
|
|
290
|
+
/**
|
|
291
|
+
* When `true` (the default), persisted keys not present in `fields`
|
|
292
|
+
* are dropped on read. Set to `false` to preserve unknown keys.
|
|
293
|
+
*
|
|
294
|
+
* @default true
|
|
295
|
+
*/
|
|
296
|
+
pruneUnknown?: boolean;
|
|
297
|
+
/**
|
|
298
|
+
* Persistence visibility for store writes.
|
|
299
|
+
*
|
|
300
|
+
* Use `"private"` for config/state stores holding secrets (tokens, API keys)
|
|
301
|
+
* that should stay owner-only. On Unix this writes the file as `0600` and
|
|
302
|
+
* creates the parent directory as `0700`; on Windows it is not enforced
|
|
303
|
+
* because Windows uses ACLs instead of Unix permission bits.
|
|
304
|
+
*
|
|
305
|
+
* The only built-in presets are `"default"` and `"private"`. Advanced callers
|
|
306
|
+
* can pass explicit bits with `{ file, directory }` for group-readable or public
|
|
307
|
+
* non-secret stores. The file bits are enforced exactly on every write
|
|
308
|
+
* regardless of `umask`. Directory bits apply only when a write creates the
|
|
309
|
+
* directory; pre-existing directories are left untouched.
|
|
310
|
+
*
|
|
311
|
+
* @default "default"
|
|
312
|
+
* @example "private"
|
|
313
|
+
* @example { file: 0o600, directory: 0o700 }
|
|
314
|
+
*/
|
|
315
|
+
access?: StoreAccess;
|
|
248
316
|
}
|
|
249
317
|
/**
|
|
250
|
-
* Receives the current
|
|
251
|
-
*
|
|
252
|
-
*
|
|
253
|
-
*
|
|
254
|
-
* @
|
|
255
|
-
*
|
|
256
|
-
*
|
|
257
|
-
*
|
|
258
|
-
*
|
|
259
|
-
*
|
|
260
|
-
*
|
|
261
|
-
|
|
318
|
+
* Receives the current config with defaults and core coercion applied,
|
|
319
|
+
* but without full field validation, and returns an updated config.
|
|
320
|
+
* Persisted values may not yet satisfy their declared types or validators.
|
|
321
|
+
*
|
|
322
|
+
* Used by {@link Store.update}, which validates the result before persistence.
|
|
323
|
+
*
|
|
324
|
+
* @example
|
|
325
|
+
* ```ts
|
|
326
|
+
* await store.update((current) => ({
|
|
327
|
+
* ...current,
|
|
328
|
+
* theme: "dark",
|
|
329
|
+
* }));
|
|
330
|
+
* ```
|
|
331
|
+
*/
|
|
262
332
|
type StoreUpdater<TConfig> = (current: TConfig) => NoInfer<TConfig>;
|
|
263
333
|
/**
|
|
264
|
-
* A typed async config store returned by {@link createStore}.
|
|
265
|
-
*
|
|
266
|
-
* Provides `read`, `write`, `update`, `patch`, and `reset` operations for a
|
|
267
|
-
* single typed config object persisted as JSON on the local filesystem.
|
|
268
|
-
*
|
|
269
|
-
*
|
|
270
|
-
*
|
|
271
|
-
* @
|
|
272
|
-
*
|
|
273
|
-
*
|
|
274
|
-
*
|
|
275
|
-
*
|
|
276
|
-
*
|
|
277
|
-
*
|
|
278
|
-
*
|
|
279
|
-
* }
|
|
280
|
-
*
|
|
281
|
-
*
|
|
282
|
-
*
|
|
283
|
-
*
|
|
284
|
-
* await store.
|
|
285
|
-
* await store.
|
|
286
|
-
*
|
|
287
|
-
|
|
334
|
+
* A typed async config store returned by {@link createStore}.
|
|
335
|
+
*
|
|
336
|
+
* Provides `read`, `write`, `update`, `patch`, and `reset` operations for a
|
|
337
|
+
* single typed config object persisted as JSON on the local filesystem.
|
|
338
|
+
* Mutations are not serialized, including overlapping calls in one process.
|
|
339
|
+
* Await mutations sequentially for each file; coordinate multiple processes externally.
|
|
340
|
+
*
|
|
341
|
+
* @typeParam TConfig - The inferred config shape from field definitions.
|
|
342
|
+
*
|
|
343
|
+
* @example
|
|
344
|
+
* ```ts
|
|
345
|
+
* const store = createStore({
|
|
346
|
+
* dirPath: configDir("my-cli"),
|
|
347
|
+
* name: "config",
|
|
348
|
+
* fields: {
|
|
349
|
+
* theme: { type: "string", default: "light" },
|
|
350
|
+
* verbose: { type: "boolean", default: false },
|
|
351
|
+
* },
|
|
352
|
+
* });
|
|
353
|
+
*
|
|
354
|
+
* const config = await store.read();
|
|
355
|
+
* await store.write({ theme: "dark", verbose: true });
|
|
356
|
+
* await store.update((c) => ({ ...c, theme: "dark" }));
|
|
357
|
+
* await store.patch({ theme: "dark" });
|
|
358
|
+
* await store.reset();
|
|
359
|
+
* ```
|
|
360
|
+
*/
|
|
288
361
|
interface Store<TConfig> {
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
}
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
362
|
+
/**
|
|
363
|
+
* Reads the persisted config, applying field defaults for missing keys.
|
|
364
|
+
*
|
|
365
|
+
* Always returns a value — fields with defaults are guaranteed present,
|
|
366
|
+
* fields without defaults may be `undefined`.
|
|
367
|
+
*
|
|
368
|
+
* When fields have `validate` functions, the merged config is validated
|
|
369
|
+
* after defaults are applied. Invalid persisted config fails loudly.
|
|
370
|
+
*
|
|
371
|
+
* @returns The effective config value.
|
|
372
|
+
* @throws {CrustStoreError} `PARSE` if persisted JSON is malformed or its root is not an object.
|
|
373
|
+
* @throws {CrustStoreError} `VALIDATION` if field validation fails.
|
|
374
|
+
* @throws {CrustStoreError} `IO` on filesystem read failures.
|
|
375
|
+
*/
|
|
376
|
+
read(): Promise<TConfig>;
|
|
377
|
+
/**
|
|
378
|
+
* Atomically persists the full config object.
|
|
379
|
+
*
|
|
380
|
+
* When fields have `validate` functions, the config is validated before
|
|
381
|
+
* persistence.
|
|
382
|
+
*
|
|
383
|
+
* @param config - The complete config to persist.
|
|
384
|
+
* @returns The persisted config, including schema transformations.
|
|
385
|
+
* @throws {CrustStoreError} `VALIDATION` if field validation fails.
|
|
386
|
+
* @throws {CrustStoreError} `IO` on filesystem write failures.
|
|
387
|
+
*/
|
|
388
|
+
write(config: NoInfer<TConfig>): Promise<TConfig>;
|
|
389
|
+
/**
|
|
390
|
+
* Reads current config with defaults and core coercion applied, without
|
|
391
|
+
* full field validation. Applies the updater, validates the result, and persists.
|
|
392
|
+
*
|
|
393
|
+
* The final file replacement is atomic, but the read-modify-write sequence
|
|
394
|
+
* is not serialized, even within one process. Await mutations sequentially
|
|
395
|
+
* to avoid lost updates; coordinate multiple processes externally.
|
|
396
|
+
*
|
|
397
|
+
* @param updater - Function receiving current config and returning updated config.
|
|
398
|
+
* @returns The persisted config, including schema transformations.
|
|
399
|
+
* @throws {CrustStoreError} `PARSE` if persisted JSON is malformed or its root is not an object.
|
|
400
|
+
* @throws {CrustStoreError} `VALIDATION` if field validation fails.
|
|
401
|
+
* @throws {CrustStoreError} `IO` on filesystem failures.
|
|
402
|
+
*/
|
|
403
|
+
update(updater: StoreUpdater<TConfig>): Promise<TConfig>;
|
|
404
|
+
/**
|
|
405
|
+
* Applies a partial update to the current config and persists.
|
|
406
|
+
*
|
|
407
|
+
* Only the provided keys are updated; everything else is preserved.
|
|
408
|
+
* Like {@link Store.update}, the current config has defaults and core coercion
|
|
409
|
+
* applied but has not undergone full field validation. The merged result is
|
|
410
|
+
* validated before persistence. Await mutations sequentially to avoid lost updates.
|
|
411
|
+
*
|
|
412
|
+
* @param partial - A partial subset of the config to merge in.
|
|
413
|
+
* @returns The persisted config, including schema transformations.
|
|
414
|
+
* @throws {CrustStoreError} `VALIDATION` if field validation fails.
|
|
415
|
+
* @throws {CrustStoreError} `PARSE` if persisted JSON is malformed or its root is not an object.
|
|
416
|
+
* @throws {CrustStoreError} `IO` on filesystem failures.
|
|
417
|
+
*/
|
|
418
|
+
patch(partial: Partial<NoInfer<TConfig>>): Promise<TConfig>;
|
|
419
|
+
/**
|
|
420
|
+
* Removes the persisted config file, returning the store to
|
|
421
|
+
* defaults-on-read behavior.
|
|
422
|
+
*
|
|
423
|
+
* @throws {CrustStoreError} `IO` on filesystem deletion failures.
|
|
424
|
+
*/
|
|
425
|
+
reset(): Promise<void>;
|
|
351
426
|
}
|
|
427
|
+
//#endregion
|
|
428
|
+
//#region src/errors.d.ts
|
|
352
429
|
/**
|
|
353
|
-
* Contextual details attached to
|
|
354
|
-
*
|
|
355
|
-
* Returned
|
|
356
|
-
*/
|
|
357
|
-
interface ParseErrorDetails {
|
|
358
|
-
/** Absolute path to the file that failed parsing. */
|
|
359
|
-
path: string;
|
|
360
|
-
}
|
|
361
|
-
/**
|
|
362
|
-
* Contextual details attached to an `IO` error.
|
|
363
|
-
*
|
|
364
|
-
* Returned on filesystem read, write, or delete failures.
|
|
365
|
-
*/
|
|
430
|
+
* Contextual details attached to an `IO` error.
|
|
431
|
+
*
|
|
432
|
+
* Returned on filesystem read, write, or delete failures.
|
|
433
|
+
*/
|
|
366
434
|
interface IOErrorDetails {
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
435
|
+
/** Absolute path to the file involved in the failed operation. */
|
|
436
|
+
path: string;
|
|
437
|
+
/** The filesystem operation that failed (e.g. `"read"`, `"write"`, `"delete"`). */
|
|
438
|
+
operation: "read" | "write" | "delete";
|
|
371
439
|
}
|
|
372
440
|
/**
|
|
373
|
-
* Contextual details attached to a `VALIDATION` error.
|
|
374
|
-
*
|
|
375
|
-
* Returned when a store validator rejects a config object during
|
|
376
|
-
* read, write, update, or patch operations.
|
|
377
|
-
*/
|
|
441
|
+
* Contextual details attached to a `VALIDATION` error.
|
|
442
|
+
*
|
|
443
|
+
* Returned when a store validator rejects a config object during
|
|
444
|
+
* read, write, update, or patch operations.
|
|
445
|
+
*/
|
|
378
446
|
interface ValidationErrorDetails {
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
}
|
|
384
|
-
/**
|
|
385
|
-
* Contextual details attached to a `DEFINITION` error.
|
|
386
|
-
*
|
|
387
|
-
* Returned when a `field()` call cannot produce a valid `FieldDef` from the
|
|
388
|
-
* provided schema and options — e.g. the input is not a Standard Schema v1
|
|
389
|
-
* object.
|
|
390
|
-
*/
|
|
391
|
-
interface DefinitionErrorDetails {
|
|
392
|
-
/** The schema vendor name, when available. */
|
|
393
|
-
vendor?: string;
|
|
447
|
+
/** The store operation during which validation failed. */
|
|
448
|
+
operation: "read" | "write" | "update" | "patch";
|
|
449
|
+
/** Structured validation issues for programmatic handling. */
|
|
450
|
+
issues: readonly StoreValidatorIssue[];
|
|
394
451
|
}
|
|
395
452
|
/**
|
|
396
|
-
* Maps each {@link StoreErrorCode} to its structured details type.
|
|
397
|
-
*
|
|
398
|
-
* Used internally for conditional constructor parameters and type narrowing.
|
|
399
|
-
*/
|
|
453
|
+
* Maps each {@link StoreErrorCode} to its structured details type.
|
|
454
|
+
*
|
|
455
|
+
* Used internally for conditional constructor parameters and type narrowing.
|
|
456
|
+
*/
|
|
400
457
|
interface StoreErrorDetailsMap {
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
458
|
+
PATH: {
|
|
459
|
+
path: string;
|
|
460
|
+
};
|
|
461
|
+
PARSE: {
|
|
462
|
+
path: string;
|
|
463
|
+
};
|
|
464
|
+
IO: IOErrorDetails;
|
|
465
|
+
VALIDATION: ValidationErrorDetails;
|
|
466
|
+
DEFINITION: undefined;
|
|
406
467
|
}
|
|
407
468
|
/**
|
|
408
|
-
* All possible error codes emitted by `@crustjs/store`.
|
|
409
|
-
*
|
|
410
|
-
* - `PATH` — Invalid or unsupported config file path
|
|
411
|
-
* - `PARSE` — Malformed JSON in persisted config file
|
|
412
|
-
* - `IO` — Filesystem read, write, or delete failure
|
|
413
|
-
* - `VALIDATION` — Config object rejected by a store validator
|
|
414
|
-
* - `DEFINITION` —
|
|
415
|
-
*
|
|
416
|
-
* @example
|
|
417
|
-
* ```ts
|
|
418
|
-
* import { CrustStoreError } from "@crustjs/store";
|
|
419
|
-
*
|
|
420
|
-
* try {
|
|
421
|
-
* const config = await store.read();
|
|
422
|
-
* } catch (err) {
|
|
423
|
-
* if (err instanceof CrustStoreError) {
|
|
424
|
-
*
|
|
425
|
-
*
|
|
426
|
-
*
|
|
427
|
-
*
|
|
428
|
-
*
|
|
429
|
-
*
|
|
430
|
-
*
|
|
431
|
-
*
|
|
432
|
-
*
|
|
433
|
-
*
|
|
434
|
-
|
|
435
|
-
* break;
|
|
436
|
-
* }
|
|
437
|
-
* }
|
|
438
|
-
* }
|
|
439
|
-
* ```
|
|
440
|
-
*/
|
|
469
|
+
* All possible error codes emitted by `@crustjs/store`.
|
|
470
|
+
*
|
|
471
|
+
* - `PATH` — Invalid or unsupported config file path
|
|
472
|
+
* - `PARSE` — Malformed JSON in persisted config file
|
|
473
|
+
* - `IO` — Filesystem read, write, or delete failure
|
|
474
|
+
* - `VALIDATION` — Config object rejected by a store validator
|
|
475
|
+
* - `DEFINITION` — A field mixes mutually exclusive definition modes
|
|
476
|
+
*
|
|
477
|
+
* @example
|
|
478
|
+
* ```ts
|
|
479
|
+
* import { CrustStoreError } from "@crustjs/store";
|
|
480
|
+
*
|
|
481
|
+
* try {
|
|
482
|
+
* const config = await store.read();
|
|
483
|
+
* } catch (err) {
|
|
484
|
+
* if (err instanceof CrustStoreError && err.is("PARSE")) {
|
|
485
|
+
* console.error(`Corrupt config at ${err.details.path}`);
|
|
486
|
+
* } else if (err instanceof CrustStoreError && err.is("VALIDATION")) {
|
|
487
|
+
* for (const issue of err.details.issues) {
|
|
488
|
+
* console.error(`${issue.path}: ${issue.message}`);
|
|
489
|
+
* }
|
|
490
|
+
* } else if (err instanceof CrustStoreError && err.is("IO")) {
|
|
491
|
+
* console.error(`File ${err.details.operation} failed: ${err.message}`);
|
|
492
|
+
* }
|
|
493
|
+
* }
|
|
494
|
+
* ```
|
|
495
|
+
*/
|
|
441
496
|
type StoreErrorCode = keyof StoreErrorDetailsMap;
|
|
442
497
|
/** Resolves the details type for a given {@link StoreErrorCode}. */
|
|
443
498
|
type StoreErrorDetails<C extends StoreErrorCode> = StoreErrorDetailsMap[C];
|
|
444
499
|
/**
|
|
445
|
-
* A typed error thrown by `@crustjs/store` for path, parse, IO, and validation failures.
|
|
446
|
-
*
|
|
447
|
-
* Every `CrustStoreError` carries a {@link StoreErrorCode} that identifies the failure
|
|
448
|
-
* category
|
|
449
|
-
* without fragile message parsing.
|
|
450
|
-
*
|
|
451
|
-
* @example
|
|
452
|
-
* ```ts
|
|
453
|
-
* import { CrustStoreError } from "@crustjs/store";
|
|
454
|
-
*
|
|
455
|
-
* try {
|
|
456
|
-
* const config = await store.read();
|
|
457
|
-
* } catch (err) {
|
|
458
|
-
* if (err instanceof CrustStoreError && err.is("PARSE")) {
|
|
459
|
-
* console.error(`Corrupt config at ${err.details.path}`);
|
|
460
|
-
* }
|
|
461
|
-
* }
|
|
462
|
-
* ```
|
|
463
|
-
*/
|
|
464
|
-
declare class CrustStoreError<C extends StoreErrorCode = StoreErrorCode> extends Error {
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
*/
|
|
486
|
-
is<T extends StoreErrorCode>(code: T): this is CrustStoreError<T>;
|
|
487
|
-
/**
|
|
488
|
-
* Attaches a cause to this error for stack-trace chaining.
|
|
489
|
-
*
|
|
490
|
-
* Returns `this` for fluent usage.
|
|
491
|
-
*
|
|
492
|
-
* @example
|
|
493
|
-
* ```ts
|
|
494
|
-
* throw new CrustStoreError("IO", "write failed", {
|
|
495
|
-
* path: "/path/to/config.json",
|
|
496
|
-
* operation: "write",
|
|
497
|
-
* }).withCause(originalError);
|
|
498
|
-
* ```
|
|
499
|
-
*/
|
|
500
|
-
withCause(cause: unknown): this;
|
|
501
|
-
}
|
|
502
|
-
import { BaseValueType as BaseValueType2 } from "@crustjs/utils";
|
|
503
|
-
import { InferOutput, StandardSchema } from "@crustjs/utils/schema";
|
|
504
|
-
/**
|
|
505
|
-
* Optional Crust metadata for the `FieldDef` shape produced by {@link field}.
|
|
506
|
-
*
|
|
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`.
|
|
510
|
-
*
|
|
511
|
-
* No `validate` key — validation flows exclusively through the schema.
|
|
512
|
-
* If extra checks are needed, refine the schema with `.refine(...)`
|
|
513
|
-
* (Zod), `Schema.filter(...)` (Effect), etc.
|
|
514
|
-
*
|
|
515
|
-
* @typeParam T - The schema's output value type. Used to type-tighten the
|
|
516
|
-
* `default` key when the user wants a non-`undefined` field.
|
|
517
|
-
*/
|
|
518
|
-
interface FieldOptions<T = unknown> {
|
|
519
|
-
type?: BaseValueType2;
|
|
520
|
-
description?: string;
|
|
521
|
-
/**
|
|
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.
|
|
526
|
-
*/
|
|
527
|
-
default?: T;
|
|
528
|
-
/** Mark this field as an array (collects values into an array). */
|
|
529
|
-
array?: true;
|
|
500
|
+
* A typed error thrown by `@crustjs/store` for path, parse, IO, and validation failures.
|
|
501
|
+
*
|
|
502
|
+
* Every `CrustStoreError` carries a {@link StoreErrorCode} that identifies the failure
|
|
503
|
+
* category. Codes other than `DEFINITION` include structured {@link StoreErrorDetails}
|
|
504
|
+
* for programmatic handling without fragile message parsing.
|
|
505
|
+
*
|
|
506
|
+
* @example
|
|
507
|
+
* ```ts
|
|
508
|
+
* import { CrustStoreError } from "@crustjs/store";
|
|
509
|
+
*
|
|
510
|
+
* try {
|
|
511
|
+
* const config = await store.read();
|
|
512
|
+
* } catch (err) {
|
|
513
|
+
* if (err instanceof CrustStoreError && err.is("PARSE")) {
|
|
514
|
+
* console.error(`Corrupt config at ${err.details.path}`);
|
|
515
|
+
* }
|
|
516
|
+
* }
|
|
517
|
+
* ```
|
|
518
|
+
*/
|
|
519
|
+
export declare class CrustStoreError<C extends StoreErrorCode = StoreErrorCode> extends Error {
|
|
520
|
+
/** Machine-readable error code for programmatic handling. */
|
|
521
|
+
readonly code: C;
|
|
522
|
+
/** Structured payload with context about the failure. */
|
|
523
|
+
readonly details: StoreErrorDetails<C>;
|
|
524
|
+
constructor(code: C, message: string, ...[details, cause]: StoreErrorDetails<C> extends undefined ? [details?: undefined, cause?: unknown] : [details: StoreErrorDetails<C>, cause?: unknown]);
|
|
525
|
+
/**
|
|
526
|
+
* Type-guard that narrows this error to a specific {@link StoreErrorCode}.
|
|
527
|
+
*
|
|
528
|
+
* When the guard returns `true`, TypeScript narrows the `details` property
|
|
529
|
+
* to the corresponding details type.
|
|
530
|
+
*
|
|
531
|
+
* @example
|
|
532
|
+
* ```ts
|
|
533
|
+
* if (err.is("IO")) {
|
|
534
|
+
* // err.details is IOErrorDetails
|
|
535
|
+
* console.error(err.details.operation, err.details.path);
|
|
536
|
+
* }
|
|
537
|
+
* ```
|
|
538
|
+
*/
|
|
539
|
+
is<T extends StoreErrorCode>(code: T): this is CrustStoreError<T>;
|
|
530
540
|
}
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
readonly array?: never;
|
|
534
|
-
readonly description?: string;
|
|
535
|
-
readonly validate: (value: unknown) => Promise<{
|
|
536
|
-
value: unknown;
|
|
537
|
-
}>;
|
|
538
|
-
readonly validateMissing: true;
|
|
539
|
-
readonly [FIELD_SCHEMA_OUTPUT]?: Out;
|
|
540
|
-
};
|
|
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 ? {
|
|
551
|
-
readonly array: true;
|
|
552
|
-
} : {
|
|
553
|
-
readonly array?: never;
|
|
554
|
-
}) & ([D] extends [never] ? unknown : {
|
|
555
|
-
readonly default: D;
|
|
556
|
-
});
|
|
557
|
-
/**
|
|
558
|
-
* `FieldDef` inferred from a Standard Schema, with no narrowed default.
|
|
559
|
-
*
|
|
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.
|
|
563
|
-
*/
|
|
564
|
-
type SchemaFieldDef<S extends StandardSchema> = RawSchemaFieldDef<InferOutput<S>>;
|
|
565
|
-
/**
|
|
566
|
-
* Define a `@crustjs/store` field from any Standard Schema v1.
|
|
567
|
-
*
|
|
568
|
-
* Returns a value that satisfies store's `FieldDef` discriminated union.
|
|
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.
|
|
572
|
-
*
|
|
573
|
-
* The returned `validate` is an async function that throws an `Error` with
|
|
574
|
-
* the schema's normalized issue messages on failure (matches store's
|
|
575
|
-
* `FieldDef.validate` contract).
|
|
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`:
|
|
580
|
-
*
|
|
581
|
-
* ```ts
|
|
582
|
-
* field(z.string().default("x")) // value materializes on read; state: string | undefined
|
|
583
|
-
* field(z.string(), { default: "x" }) // Crust-level default; state: string
|
|
584
|
-
* ```
|
|
585
|
-
*
|
|
586
|
-
* @param schema - Any Standard Schema v1 object (Zod schemas natively;
|
|
587
|
-
* Effect schemas wrapped via `Schema.standardSchemaV1`;
|
|
588
|
-
* Valibot/ArkType/Sury/etc. as-is)
|
|
589
|
-
* @param opts - Optional Crust-level metadata
|
|
590
|
-
* @throws {CrustStoreError} With code `"DEFINITION"` when the input is not
|
|
591
|
-
* a Standard Schema v1 object.
|
|
592
|
-
*
|
|
593
|
-
* @example Zod
|
|
594
|
-
* ```ts
|
|
595
|
-
* import { z } from "zod";
|
|
596
|
-
* import { createStore, configDir, field } from "@crustjs/store";
|
|
597
|
-
*
|
|
598
|
-
* const store = createStore({
|
|
599
|
-
* dirPath: configDir("my-cli"),
|
|
600
|
-
* fields: {
|
|
601
|
-
* theme: field(z.enum(["light", "dark"]).default("light")),
|
|
602
|
-
* verbose: field(z.boolean().default(false)),
|
|
603
|
-
* tags: field(z.array(z.string()).default([])),
|
|
604
|
-
* },
|
|
605
|
-
* });
|
|
606
|
-
* ```
|
|
607
|
-
*
|
|
608
|
-
* @example Effect
|
|
609
|
-
* ```ts
|
|
610
|
-
* import * as Schema from "effect/Schema";
|
|
611
|
-
* import { createStore, configDir, field } from "@crustjs/store";
|
|
612
|
-
*
|
|
613
|
-
* const store = createStore({
|
|
614
|
-
* dirPath: configDir("my-cli"),
|
|
615
|
-
* fields: {
|
|
616
|
-
* theme: field(Schema.standardSchemaV1(Schema.Literal("light", "dark"))),
|
|
617
|
-
* },
|
|
618
|
-
* });
|
|
619
|
-
* ```
|
|
620
|
-
*/
|
|
621
|
-
declare function field<S extends StandardSchema>(schema: S): SchemaFieldDef<S>;
|
|
622
|
-
declare function field<
|
|
623
|
-
S extends StandardSchema,
|
|
624
|
-
D extends InferOutput<S>,
|
|
625
|
-
const Type extends FieldOptions<InferOutput<S>>["type"] = undefined,
|
|
626
|
-
const Array extends true | undefined = undefined
|
|
627
|
-
>(schema: S, opts: FieldOptions<InferOutput<S>> & {
|
|
628
|
-
default: D;
|
|
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>;
|
|
541
|
+
//#endregion
|
|
542
|
+
//#region src/path.d.ts
|
|
640
543
|
/**
|
|
641
|
-
* Encapsulates runtime environment values needed for path resolution.
|
|
642
|
-
*
|
|
643
|
-
* Exposed as a parameter to allow deterministic testing without mutating
|
|
644
|
-
* `process.env` or `process.platform`.
|
|
645
|
-
*/
|
|
544
|
+
* Encapsulates runtime environment values needed for path resolution.
|
|
545
|
+
*
|
|
546
|
+
* Exposed as a parameter to allow deterministic testing without mutating
|
|
547
|
+
* `process.env` or `process.platform`.
|
|
548
|
+
*/
|
|
646
549
|
interface PlatformEnv {
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
550
|
+
/** Operating system platform identifier (e.g. `"linux"`, `"darwin"`, `"win32"`). */
|
|
551
|
+
platform: string;
|
|
552
|
+
/** Environment variables map. */
|
|
553
|
+
env: Record<string, string | undefined>;
|
|
554
|
+
/** User home directory path. */
|
|
555
|
+
homedir: string;
|
|
653
556
|
}
|
|
654
557
|
/**
|
|
655
|
-
* Resolves the platform-standard config directory for the given app.
|
|
656
|
-
*
|
|
657
|
-
* Platform conventions:
|
|
658
|
-
* - **Linux / macOS**: `$XDG_CONFIG_HOME/<appName>` or `~/.config/<appName>`
|
|
659
|
-
* - **Windows**: `%APPDATA%/<appName>` or `~/AppData/Roaming/<appName>`
|
|
660
|
-
*
|
|
661
|
-
* macOS uses XDG conventions for consistency with Linux.
|
|
662
|
-
*
|
|
663
|
-
* @param appName - Application name used as directory name. Must be a non-empty
|
|
664
|
-
* string without path separators.
|
|
665
|
-
* @param env - Optional platform environment override for testing.
|
|
666
|
-
* @returns Absolute path to the app's config directory.
|
|
667
|
-
* @throws {CrustStoreError} `PATH` if `appName` is invalid or platform is unsupported.
|
|
668
|
-
*
|
|
669
|
-
* @example
|
|
670
|
-
* ```ts
|
|
671
|
-
* import { configDir } from "@crustjs/store";
|
|
672
|
-
*
|
|
673
|
-
* const dir = configDir("my-cli");
|
|
674
|
-
* // → "/home/user/.config/my-cli" (Linux)
|
|
675
|
-
* // → "/Users/user/.config/my-cli" (macOS)
|
|
676
|
-
* // → "C:\\Users\\user\\AppData\\Roaming\\my-cli" (Windows)
|
|
677
|
-
* ```
|
|
678
|
-
*/
|
|
679
|
-
declare function configDir(appName: string, env?: PlatformEnv): string;
|
|
558
|
+
* Resolves the platform-standard config directory for the given app.
|
|
559
|
+
*
|
|
560
|
+
* Platform conventions:
|
|
561
|
+
* - **Linux / macOS**: `$XDG_CONFIG_HOME/<appName>` or `~/.config/<appName>`
|
|
562
|
+
* - **Windows**: `%APPDATA%/<appName>` or `~/AppData/Roaming/<appName>`
|
|
563
|
+
*
|
|
564
|
+
* macOS uses XDG conventions for consistency with Linux.
|
|
565
|
+
*
|
|
566
|
+
* @param appName - Application name used as directory name. Must be a non-empty
|
|
567
|
+
* string without path separators.
|
|
568
|
+
* @param env - Optional platform environment override for testing.
|
|
569
|
+
* @returns Absolute path to the app's config directory.
|
|
570
|
+
* @throws {CrustStoreError} `PATH` if `appName` is invalid or platform is unsupported.
|
|
571
|
+
*
|
|
572
|
+
* @example
|
|
573
|
+
* ```ts
|
|
574
|
+
* import { configDir } from "@crustjs/store";
|
|
575
|
+
*
|
|
576
|
+
* const dir = configDir("my-cli");
|
|
577
|
+
* // → "/home/user/.config/my-cli" (Linux)
|
|
578
|
+
* // → "/Users/user/.config/my-cli" (macOS)
|
|
579
|
+
* // → "C:\\Users\\user\\AppData\\Roaming\\my-cli" (Windows)
|
|
580
|
+
* ```
|
|
581
|
+
*/
|
|
582
|
+
export declare function configDir(appName: string, env?: PlatformEnv): string;
|
|
680
583
|
/**
|
|
681
|
-
* Resolves the platform-standard data directory for the given app.
|
|
682
|
-
*
|
|
683
|
-
* Platform conventions:
|
|
684
|
-
* - **Linux / macOS**: `$XDG_DATA_HOME/<appName>` or `~/.local/share/<appName>`
|
|
685
|
-
* - **Windows**: `%LOCALAPPDATA%/<appName>/Data` or `~/AppData/Local/<appName>/Data`
|
|
686
|
-
*
|
|
687
|
-
* macOS uses XDG conventions for consistency with Linux.
|
|
688
|
-
*
|
|
689
|
-
* @param appName - Application name used as directory name. Must be a non-empty
|
|
690
|
-
* string without path separators.
|
|
691
|
-
* @param env - Optional platform environment override for testing.
|
|
692
|
-
* @returns Absolute path to the app's data directory.
|
|
693
|
-
* @throws {CrustStoreError} `PATH` if `appName` is invalid or platform is unsupported.
|
|
694
|
-
*
|
|
695
|
-
* @example
|
|
696
|
-
* ```ts
|
|
697
|
-
* import { dataDir } from "@crustjs/store";
|
|
698
|
-
*
|
|
699
|
-
* const dir = dataDir("my-cli");
|
|
700
|
-
* // → "/home/user/.local/share/my-cli" (Linux)
|
|
701
|
-
* // → "/Users/user/.local/share/my-cli" (macOS)
|
|
702
|
-
* // → "C:\\Users\\user\\AppData\\Local\\my-cli\\Data" (Windows)
|
|
703
|
-
* ```
|
|
704
|
-
*/
|
|
705
|
-
declare function dataDir(appName: string, env?: PlatformEnv): string;
|
|
584
|
+
* Resolves the platform-standard data directory for the given app.
|
|
585
|
+
*
|
|
586
|
+
* Platform conventions:
|
|
587
|
+
* - **Linux / macOS**: `$XDG_DATA_HOME/<appName>` or `~/.local/share/<appName>`
|
|
588
|
+
* - **Windows**: `%LOCALAPPDATA%/<appName>/Data` or `~/AppData/Local/<appName>/Data`
|
|
589
|
+
*
|
|
590
|
+
* macOS uses XDG conventions for consistency with Linux.
|
|
591
|
+
*
|
|
592
|
+
* @param appName - Application name used as directory name. Must be a non-empty
|
|
593
|
+
* string without path separators.
|
|
594
|
+
* @param env - Optional platform environment override for testing.
|
|
595
|
+
* @returns Absolute path to the app's data directory.
|
|
596
|
+
* @throws {CrustStoreError} `PATH` if `appName` is invalid or platform is unsupported.
|
|
597
|
+
*
|
|
598
|
+
* @example
|
|
599
|
+
* ```ts
|
|
600
|
+
* import { dataDir } from "@crustjs/store";
|
|
601
|
+
*
|
|
602
|
+
* const dir = dataDir("my-cli");
|
|
603
|
+
* // → "/home/user/.local/share/my-cli" (Linux)
|
|
604
|
+
* // → "/Users/user/.local/share/my-cli" (macOS)
|
|
605
|
+
* // → "C:\\Users\\user\\AppData\\Local\\my-cli\\Data" (Windows)
|
|
606
|
+
* ```
|
|
607
|
+
*/
|
|
608
|
+
export declare function dataDir(appName: string, env?: PlatformEnv): string;
|
|
706
609
|
/**
|
|
707
|
-
* Resolves the platform-standard state directory for the given app.
|
|
708
|
-
*
|
|
709
|
-
* Platform conventions:
|
|
710
|
-
* - **Linux / macOS**: `$XDG_STATE_HOME/<appName>` or `~/.local/state/<appName>`
|
|
711
|
-
* - **Windows**: `%LOCALAPPDATA%/<appName>/State` or `~/AppData/Local/<appName>/State`
|
|
712
|
-
*
|
|
713
|
-
* macOS uses XDG conventions for consistency with Linux.
|
|
714
|
-
*
|
|
715
|
-
* @param appName - Application name used as directory name. Must be a non-empty
|
|
716
|
-
* string without path separators.
|
|
717
|
-
* @param env - Optional platform environment override for testing.
|
|
718
|
-
* @returns Absolute path to the app's state directory.
|
|
719
|
-
* @throws {CrustStoreError} `PATH` if `appName` is invalid or platform is unsupported.
|
|
720
|
-
*
|
|
721
|
-
* @example
|
|
722
|
-
* ```ts
|
|
723
|
-
* import { stateDir } from "@crustjs/store";
|
|
724
|
-
*
|
|
725
|
-
* const dir = stateDir("my-cli");
|
|
726
|
-
* // → "/home/user/.local/state/my-cli" (Linux)
|
|
727
|
-
* // → "/Users/user/.local/state/my-cli" (macOS)
|
|
728
|
-
* // → "C:\\Users\\user\\AppData\\Local\\my-cli\\State" (Windows)
|
|
729
|
-
* ```
|
|
730
|
-
*/
|
|
731
|
-
declare function stateDir(appName: string, env?: PlatformEnv): string;
|
|
610
|
+
* Resolves the platform-standard state directory for the given app.
|
|
611
|
+
*
|
|
612
|
+
* Platform conventions:
|
|
613
|
+
* - **Linux / macOS**: `$XDG_STATE_HOME/<appName>` or `~/.local/state/<appName>`
|
|
614
|
+
* - **Windows**: `%LOCALAPPDATA%/<appName>/State` or `~/AppData/Local/<appName>/State`
|
|
615
|
+
*
|
|
616
|
+
* macOS uses XDG conventions for consistency with Linux.
|
|
617
|
+
*
|
|
618
|
+
* @param appName - Application name used as directory name. Must be a non-empty
|
|
619
|
+
* string without path separators.
|
|
620
|
+
* @param env - Optional platform environment override for testing.
|
|
621
|
+
* @returns Absolute path to the app's state directory.
|
|
622
|
+
* @throws {CrustStoreError} `PATH` if `appName` is invalid or platform is unsupported.
|
|
623
|
+
*
|
|
624
|
+
* @example
|
|
625
|
+
* ```ts
|
|
626
|
+
* import { stateDir } from "@crustjs/store";
|
|
627
|
+
*
|
|
628
|
+
* const dir = stateDir("my-cli");
|
|
629
|
+
* // → "/home/user/.local/state/my-cli" (Linux)
|
|
630
|
+
* // → "/Users/user/.local/state/my-cli" (macOS)
|
|
631
|
+
* // → "C:\\Users\\user\\AppData\\Local\\my-cli\\State" (Windows)
|
|
632
|
+
* ```
|
|
633
|
+
*/
|
|
634
|
+
export declare function stateDir(appName: string, env?: PlatformEnv): string;
|
|
732
635
|
/**
|
|
733
|
-
* Resolves the platform-standard cache directory for the given app.
|
|
734
|
-
*
|
|
735
|
-
* Platform conventions:
|
|
736
|
-
* - **Linux / macOS**: `$XDG_CACHE_HOME/<appName>` or `~/.cache/<appName>`
|
|
737
|
-
* - **Windows**: `%LOCALAPPDATA%/<appName>/Cache` or `~/AppData/Local/<appName>/Cache`
|
|
738
|
-
*
|
|
739
|
-
* macOS uses XDG conventions for consistency with Linux.
|
|
740
|
-
*
|
|
741
|
-
* @param appName - Application name used as directory name. Must be a non-empty
|
|
742
|
-
* string without path separators.
|
|
743
|
-
* @param env - Optional platform environment override for testing.
|
|
744
|
-
* @returns Absolute path to the app's cache directory.
|
|
745
|
-
* @throws {CrustStoreError} `PATH` if `appName` is invalid or platform is unsupported.
|
|
746
|
-
*
|
|
747
|
-
* @example
|
|
748
|
-
* ```ts
|
|
749
|
-
* import { cacheDir } from "@crustjs/store";
|
|
750
|
-
*
|
|
751
|
-
* const dir = cacheDir("my-cli");
|
|
752
|
-
* // → "/home/user/.cache/my-cli" (Linux)
|
|
753
|
-
* // → "/Users/user/.cache/my-cli" (macOS)
|
|
754
|
-
* // → "C:\\Users\\user\\AppData\\Local\\my-cli\\Cache" (Windows)
|
|
755
|
-
* ```
|
|
756
|
-
*/
|
|
757
|
-
declare function cacheDir(appName: string, env?: PlatformEnv): string;
|
|
636
|
+
* Resolves the platform-standard cache directory for the given app.
|
|
637
|
+
*
|
|
638
|
+
* Platform conventions:
|
|
639
|
+
* - **Linux / macOS**: `$XDG_CACHE_HOME/<appName>` or `~/.cache/<appName>`
|
|
640
|
+
* - **Windows**: `%LOCALAPPDATA%/<appName>/Cache` or `~/AppData/Local/<appName>/Cache`
|
|
641
|
+
*
|
|
642
|
+
* macOS uses XDG conventions for consistency with Linux.
|
|
643
|
+
*
|
|
644
|
+
* @param appName - Application name used as directory name. Must be a non-empty
|
|
645
|
+
* string without path separators.
|
|
646
|
+
* @param env - Optional platform environment override for testing.
|
|
647
|
+
* @returns Absolute path to the app's cache directory.
|
|
648
|
+
* @throws {CrustStoreError} `PATH` if `appName` is invalid or platform is unsupported.
|
|
649
|
+
*
|
|
650
|
+
* @example
|
|
651
|
+
* ```ts
|
|
652
|
+
* import { cacheDir } from "@crustjs/store";
|
|
653
|
+
*
|
|
654
|
+
* const dir = cacheDir("my-cli");
|
|
655
|
+
* // → "/home/user/.cache/my-cli" (Linux)
|
|
656
|
+
* // → "/Users/user/.cache/my-cli" (macOS)
|
|
657
|
+
* // → "C:\\Users\\user\\AppData\\Local\\my-cli\\Cache" (Windows)
|
|
658
|
+
* ```
|
|
659
|
+
*/
|
|
660
|
+
export declare function cacheDir(appName: string, env?: PlatformEnv): string;
|
|
661
|
+
//#endregion
|
|
662
|
+
//#region src/store.d.ts
|
|
758
663
|
/**
|
|
759
|
-
* Creates a typed async config store backed by a local JSON file.
|
|
760
|
-
*
|
|
761
|
-
* The store resolves its file path once at creation time from `dirPath` and
|
|
762
|
-
*
|
|
763
|
-
*
|
|
764
|
-
*
|
|
765
|
-
*
|
|
766
|
-
* @
|
|
767
|
-
* @
|
|
768
|
-
* @
|
|
769
|
-
* @throws {CrustStoreError} `
|
|
770
|
-
*
|
|
771
|
-
* @example
|
|
772
|
-
* ```ts
|
|
773
|
-
* import { createStore, configDir } from "@crustjs/store";
|
|
774
|
-
*
|
|
775
|
-
* const store = createStore({
|
|
776
|
-
* dirPath: configDir("my-cli"),
|
|
777
|
-
*
|
|
778
|
-
*
|
|
779
|
-
*
|
|
780
|
-
*
|
|
781
|
-
*
|
|
782
|
-
*
|
|
783
|
-
*
|
|
784
|
-
*
|
|
785
|
-
*
|
|
786
|
-
*
|
|
787
|
-
*
|
|
788
|
-
* await store.
|
|
789
|
-
* await store.
|
|
790
|
-
* await store.
|
|
791
|
-
*
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
export
|
|
664
|
+
* Creates a typed async config store backed by a local JSON file.
|
|
665
|
+
*
|
|
666
|
+
* The store resolves its file path once at creation time from `dirPath` and
|
|
667
|
+
* `name`. Core field definitions infer from `type` and `default`;
|
|
668
|
+
* schema-backed fields use the Standard Schema's exact output type.
|
|
669
|
+
*
|
|
670
|
+
* @typeParam F - Field definitions record (inferred via `const` generic).
|
|
671
|
+
* @param options - Store configuration options.
|
|
672
|
+
* @returns A {@link Store} instance with `read`, `write`, `update`, `patch`, and `reset` methods.
|
|
673
|
+
* @throws {CrustStoreError} `PATH` if `dirPath` or `name` is invalid.
|
|
674
|
+
* @throws {CrustStoreError} `DEFINITION` if a field mixes `schema` with `default` or `validate`.
|
|
675
|
+
*
|
|
676
|
+
* @example
|
|
677
|
+
* ```ts
|
|
678
|
+
* import { createStore, configDir } from "@crustjs/store";
|
|
679
|
+
*
|
|
680
|
+
* const store = createStore({
|
|
681
|
+
* dirPath: configDir("my-cli"),
|
|
682
|
+
* name: "config",
|
|
683
|
+
* fields: {
|
|
684
|
+
* theme: { type: "string", default: "light" },
|
|
685
|
+
* verbose: { type: "boolean", default: false },
|
|
686
|
+
* token: { type: "string" },
|
|
687
|
+
* },
|
|
688
|
+
* });
|
|
689
|
+
*
|
|
690
|
+
* const config = await store.read();
|
|
691
|
+
* // → { theme: "light", verbose: false, token: undefined }
|
|
692
|
+
*
|
|
693
|
+
* await store.write({ theme: "dark", verbose: true, token: "abc" });
|
|
694
|
+
* await store.update((c) => ({ ...c, theme: "light" }));
|
|
695
|
+
* await store.patch({ theme: "solarized" });
|
|
696
|
+
* await store.reset();
|
|
697
|
+
* ```
|
|
698
|
+
*/
|
|
699
|
+
export declare function createStore<const F extends FieldsDef>(options: CreateStoreOptions<F>): Store<InferStoreConfig<F>>;
|
|
700
|
+
//#endregion
|
|
701
|
+
export type { CreateStoreOptions, FieldDef, FieldsDef, InferStoreConfig, PlatformEnv, Store, StoreAccess, StoreErrorCode, StorePermissionBits, StoreUpdater, StoreValidatorIssue, ValidationErrorDetails, ValueType };
|