wellcrafted 0.27.0 → 0.29.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 +70 -56
- package/dist/error/index.d.ts +68 -45
- package/dist/error/index.d.ts.map +1 -1
- package/dist/error/index.js +15 -7
- package/dist/error/index.js.map +1 -1
- package/dist/query/index.d.ts +5 -5
- package/dist/query/index.d.ts.map +1 -1
- package/dist/query/index.js +7 -7
- package/dist/query/index.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -54,12 +54,18 @@ function getUser(id: UserId) { /* ... */ }
|
|
|
54
54
|
```
|
|
55
55
|
|
|
56
56
|
### 📋 Tagged Errors
|
|
57
|
-
Structured, serializable errors with
|
|
57
|
+
Structured, serializable errors with a fluent API
|
|
58
58
|
```typescript
|
|
59
59
|
import { createTaggedError } from "wellcrafted/error";
|
|
60
60
|
|
|
61
|
-
|
|
62
|
-
|
|
61
|
+
// Minimal by default - only name and message
|
|
62
|
+
const { ValidationError } = createTaggedError("ValidationError");
|
|
63
|
+
ValidationError({ message: "Email is required" });
|
|
64
|
+
|
|
65
|
+
// Chain to add context and cause when needed
|
|
66
|
+
const { ApiError } = createTaggedError("ApiError")
|
|
67
|
+
.withContext<{ endpoint: string }>()
|
|
68
|
+
.withCause<NetworkError | undefined>();
|
|
63
69
|
```
|
|
64
70
|
|
|
65
71
|
### 🔄 Query Integration
|
|
@@ -74,7 +80,7 @@ const { defineQuery, defineMutation } = createQueryFactories(queryClient);
|
|
|
74
80
|
// Define operations that return Result types
|
|
75
81
|
const userQuery = defineQuery({
|
|
76
82
|
queryKey: ['users', userId],
|
|
77
|
-
|
|
83
|
+
queryFn: () => getUserFromAPI(userId) // Returns Result<User, ApiError>
|
|
78
84
|
});
|
|
79
85
|
|
|
80
86
|
// Use reactively in components with automatic state management
|
|
@@ -100,19 +106,21 @@ npm install wellcrafted
|
|
|
100
106
|
|
|
101
107
|
```typescript
|
|
102
108
|
import { tryAsync } from "wellcrafted/result";
|
|
103
|
-
import { createTaggedError } from "wellcrafted/error";
|
|
109
|
+
import { createTaggedError, type AnyTaggedError } from "wellcrafted/error";
|
|
104
110
|
|
|
105
111
|
// Define your error with factory function
|
|
106
|
-
const { ApiError, ApiErr } = createTaggedError("ApiError")
|
|
112
|
+
const { ApiError, ApiErr } = createTaggedError("ApiError")
|
|
113
|
+
.withContext<{ endpoint: string }>()
|
|
114
|
+
.withCause<AnyTaggedError | undefined>();
|
|
107
115
|
type ApiError = ReturnType<typeof ApiError>;
|
|
108
116
|
|
|
109
117
|
// Wrap any throwing operation
|
|
110
118
|
const { data, error } = await tryAsync({
|
|
111
119
|
try: () => fetch('/api/user').then(r => r.json()),
|
|
112
|
-
catch: (
|
|
120
|
+
catch: (e) => ApiErr({
|
|
113
121
|
message: "Failed to fetch user",
|
|
114
122
|
context: { endpoint: '/api/user' },
|
|
115
|
-
cause:
|
|
123
|
+
cause: { name: "FetchError", message: String(e) }
|
|
116
124
|
})
|
|
117
125
|
});
|
|
118
126
|
|
|
@@ -206,25 +214,28 @@ if (error) {
|
|
|
206
214
|
### Wrap Unsafe Operations
|
|
207
215
|
|
|
208
216
|
```typescript
|
|
217
|
+
// Define errors with context and cause
|
|
218
|
+
const { ParseError, ParseErr } = createTaggedError("ParseError")
|
|
219
|
+
.withContext<{ input: string }>();
|
|
220
|
+
|
|
221
|
+
const { NetworkError, NetworkErr } = createTaggedError("NetworkError")
|
|
222
|
+
.withContext<{ url: string }>();
|
|
223
|
+
|
|
209
224
|
// Synchronous
|
|
210
225
|
const result = trySync({
|
|
211
226
|
try: () => JSON.parse(jsonString),
|
|
212
|
-
catch: (
|
|
213
|
-
name: "ParseError",
|
|
227
|
+
catch: () => ParseErr({
|
|
214
228
|
message: "Invalid JSON",
|
|
215
|
-
context: { input: jsonString }
|
|
216
|
-
cause: error
|
|
229
|
+
context: { input: jsonString }
|
|
217
230
|
})
|
|
218
231
|
});
|
|
219
232
|
|
|
220
|
-
// Asynchronous
|
|
233
|
+
// Asynchronous
|
|
221
234
|
const result = await tryAsync({
|
|
222
235
|
try: () => fetch(url),
|
|
223
|
-
catch: (
|
|
224
|
-
name: "NetworkError",
|
|
236
|
+
catch: () => NetworkErr({
|
|
225
237
|
message: "Request failed",
|
|
226
|
-
context: { url }
|
|
227
|
-
cause: error
|
|
238
|
+
context: { url }
|
|
228
239
|
})
|
|
229
240
|
});
|
|
230
241
|
```
|
|
@@ -234,9 +245,10 @@ const result = await tryAsync({
|
|
|
234
245
|
```typescript
|
|
235
246
|
// 1. Service Layer - Pure business logic
|
|
236
247
|
import { createTaggedError } from "wellcrafted/error";
|
|
237
|
-
import { tryAsync, Result } from "wellcrafted/result";
|
|
248
|
+
import { tryAsync, Result, Ok } from "wellcrafted/result";
|
|
238
249
|
|
|
239
|
-
const { RecorderServiceError, RecorderServiceErr } = createTaggedError("RecorderServiceError")
|
|
250
|
+
const { RecorderServiceError, RecorderServiceErr } = createTaggedError("RecorderServiceError")
|
|
251
|
+
.withContext<{ currentState?: string; permissions?: string }>();
|
|
240
252
|
type RecorderServiceError = ReturnType<typeof RecorderServiceError>;
|
|
241
253
|
|
|
242
254
|
export function createRecorderService() {
|
|
@@ -248,8 +260,7 @@ export function createRecorderService() {
|
|
|
248
260
|
if (isRecording) {
|
|
249
261
|
return RecorderServiceErr({
|
|
250
262
|
message: "Already recording",
|
|
251
|
-
context: { currentState: 'recording' }
|
|
252
|
-
cause: undefined
|
|
263
|
+
context: { currentState: 'recording' }
|
|
253
264
|
});
|
|
254
265
|
}
|
|
255
266
|
|
|
@@ -260,10 +271,9 @@ export function createRecorderService() {
|
|
|
260
271
|
// ... recording setup
|
|
261
272
|
isRecording = true;
|
|
262
273
|
},
|
|
263
|
-
catch: (
|
|
274
|
+
catch: () => RecorderServiceErr({
|
|
264
275
|
message: "Failed to start recording",
|
|
265
|
-
context: { permissions: 'microphone' }
|
|
266
|
-
cause: error
|
|
276
|
+
context: { permissions: 'microphone' }
|
|
267
277
|
})
|
|
268
278
|
});
|
|
269
279
|
},
|
|
@@ -272,11 +282,10 @@ export function createRecorderService() {
|
|
|
272
282
|
if (!isRecording) {
|
|
273
283
|
return RecorderServiceErr({
|
|
274
284
|
message: "Not currently recording",
|
|
275
|
-
context: { currentState: 'idle' }
|
|
276
|
-
cause: undefined
|
|
285
|
+
context: { currentState: 'idle' }
|
|
277
286
|
});
|
|
278
287
|
}
|
|
279
|
-
|
|
288
|
+
|
|
280
289
|
// Stop recording and return blob...
|
|
281
290
|
isRecording = false;
|
|
282
291
|
return Ok(currentBlob!);
|
|
@@ -292,7 +301,7 @@ const { defineQuery, defineMutation } = createQueryFactories(queryClient);
|
|
|
292
301
|
export const recorder = {
|
|
293
302
|
getRecorderState: defineQuery({
|
|
294
303
|
queryKey: ['recorder', 'state'],
|
|
295
|
-
|
|
304
|
+
queryFn: async () => {
|
|
296
305
|
const { data, error } = await services.recorder.getState();
|
|
297
306
|
if (error) {
|
|
298
307
|
// Transform service error to UI-friendly error
|
|
@@ -309,7 +318,7 @@ export const recorder = {
|
|
|
309
318
|
|
|
310
319
|
startRecording: defineMutation({
|
|
311
320
|
mutationKey: ['recorder', 'start'],
|
|
312
|
-
|
|
321
|
+
mutationFn: async () => {
|
|
313
322
|
const { error } = await services.recorder.startRecording();
|
|
314
323
|
if (error) {
|
|
315
324
|
return Err({
|
|
@@ -367,10 +376,12 @@ const { data: parsed } = trySync({
|
|
|
367
376
|
|
|
368
377
|
### Propagation Pattern (May Fail)
|
|
369
378
|
```typescript
|
|
370
|
-
|
|
379
|
+
const { ParseError, ParseErr } = createTaggedError("ParseError");
|
|
380
|
+
|
|
381
|
+
// When catch can return Err<E>, function returns Result<T, E>
|
|
371
382
|
const mayFail = trySync({
|
|
372
383
|
try: () => JSON.parse(riskyJson),
|
|
373
|
-
catch: (
|
|
384
|
+
catch: () => ParseErr({ message: "Invalid JSON" })
|
|
374
385
|
});
|
|
375
386
|
// mayFail: Result<object, ParseError> - Must check for errors
|
|
376
387
|
if (isOk(mayFail)) {
|
|
@@ -382,13 +393,13 @@ if (isOk(mayFail)) {
|
|
|
382
393
|
```typescript
|
|
383
394
|
const smartParse = trySync({
|
|
384
395
|
try: () => JSON.parse(input),
|
|
385
|
-
catch: (
|
|
396
|
+
catch: () => {
|
|
386
397
|
// Recover from empty input
|
|
387
398
|
if (input.trim() === "") {
|
|
388
399
|
return Ok({}); // Return Ok<T> for fallback
|
|
389
400
|
}
|
|
390
|
-
// Propagate other errors
|
|
391
|
-
return ParseErr({ message: "Parse failed"
|
|
401
|
+
// Propagate other errors
|
|
402
|
+
return ParseErr({ message: "Parse failed" });
|
|
392
403
|
}
|
|
393
404
|
});
|
|
394
405
|
// smartParse: Result<object, ParseError> - Mixed handling = Result type
|
|
@@ -419,40 +430,40 @@ Based on real-world usage, here's the recommended pattern for creating services
|
|
|
419
430
|
|
|
420
431
|
```typescript
|
|
421
432
|
import { createTaggedError } from "wellcrafted/error";
|
|
433
|
+
import { Result, Ok } from "wellcrafted/result";
|
|
422
434
|
|
|
423
|
-
// 1. Define service-specific errors
|
|
424
|
-
const { RecorderServiceError, RecorderServiceErr } = createTaggedError("RecorderServiceError")
|
|
435
|
+
// 1. Define service-specific errors with typed context
|
|
436
|
+
const { RecorderServiceError, RecorderServiceErr } = createTaggedError("RecorderServiceError")
|
|
437
|
+
.withContext<{ isRecording: boolean }>();
|
|
425
438
|
type RecorderServiceError = ReturnType<typeof RecorderServiceError>;
|
|
426
439
|
|
|
427
440
|
// 2. Create service with factory function
|
|
428
441
|
export function createRecorderService() {
|
|
429
442
|
// Private state in closure
|
|
430
443
|
let isRecording = false;
|
|
431
|
-
|
|
444
|
+
|
|
432
445
|
// Return object with methods
|
|
433
446
|
return {
|
|
434
447
|
startRecording(): Result<void, RecorderServiceError> {
|
|
435
448
|
if (isRecording) {
|
|
436
449
|
return RecorderServiceErr({
|
|
437
450
|
message: "Already recording",
|
|
438
|
-
context: { isRecording }
|
|
439
|
-
cause: undefined
|
|
451
|
+
context: { isRecording }
|
|
440
452
|
});
|
|
441
453
|
}
|
|
442
|
-
|
|
454
|
+
|
|
443
455
|
isRecording = true;
|
|
444
456
|
return Ok(undefined);
|
|
445
457
|
},
|
|
446
|
-
|
|
458
|
+
|
|
447
459
|
stopRecording(): Result<Blob, RecorderServiceError> {
|
|
448
460
|
if (!isRecording) {
|
|
449
461
|
return RecorderServiceErr({
|
|
450
|
-
message: "Not currently recording",
|
|
451
|
-
context: { isRecording }
|
|
452
|
-
cause: undefined
|
|
462
|
+
message: "Not currently recording",
|
|
463
|
+
context: { isRecording }
|
|
453
464
|
});
|
|
454
465
|
}
|
|
455
|
-
|
|
466
|
+
|
|
456
467
|
isRecording = false;
|
|
457
468
|
return Ok(new Blob(["audio data"]));
|
|
458
469
|
}
|
|
@@ -534,22 +545,23 @@ export async function GET(request: Request) {
|
|
|
534
545
|
<summary><b>Form Validation</b></summary>
|
|
535
546
|
|
|
536
547
|
```typescript
|
|
548
|
+
const { FormError, FormErr } = createTaggedError("FormError")
|
|
549
|
+
.withContext<{ fields: Record<string, string[]> }>();
|
|
550
|
+
|
|
537
551
|
function validateLoginForm(data: unknown): Result<LoginData, FormError> {
|
|
538
552
|
const errors: Record<string, string[]> = {};
|
|
539
|
-
|
|
553
|
+
|
|
540
554
|
if (!isValidEmail(data?.email)) {
|
|
541
555
|
errors.email = ["Invalid email format"];
|
|
542
556
|
}
|
|
543
|
-
|
|
557
|
+
|
|
544
558
|
if (Object.keys(errors).length > 0) {
|
|
545
|
-
return
|
|
546
|
-
name: "FormError",
|
|
559
|
+
return FormErr({
|
|
547
560
|
message: "Validation failed",
|
|
548
|
-
context: { fields: errors }
|
|
549
|
-
cause: undefined
|
|
561
|
+
context: { fields: errors }
|
|
550
562
|
});
|
|
551
563
|
}
|
|
552
|
-
|
|
564
|
+
|
|
553
565
|
return Ok(data as LoginData);
|
|
554
566
|
}
|
|
555
567
|
```
|
|
@@ -615,10 +627,12 @@ For comprehensive examples, service layer patterns, framework integrations, and
|
|
|
615
627
|
- **`defineMutation(options)`** - Define a mutation with dual interface (`.options()` + `.execute()`)
|
|
616
628
|
|
|
617
629
|
### Error Functions
|
|
618
|
-
- **`createTaggedError(name)`** - Creates error factory functions
|
|
619
|
-
- Returns
|
|
620
|
-
-
|
|
621
|
-
-
|
|
630
|
+
- **`createTaggedError(name)`** - Creates error factory functions with fluent API
|
|
631
|
+
- Returns `{ErrorName}` (plain error) and `{ErrorName}Err` (Err-wrapped)
|
|
632
|
+
- **Default**: minimal errors with only `name` and `message`
|
|
633
|
+
- Chain `.withContext<T>()` to add typed context
|
|
634
|
+
- Chain `.withCause<T>()` to add typed cause
|
|
635
|
+
- Include `| undefined` in type to make property optional but typed
|
|
622
636
|
- **`extractErrorMessage(error)`** - Extract readable message from unknown error
|
|
623
637
|
|
|
624
638
|
### Types
|
package/dist/error/index.d.ts
CHANGED
|
@@ -10,81 +10,77 @@ type AnyTaggedError = {
|
|
|
10
10
|
};
|
|
11
11
|
/**
|
|
12
12
|
* Helper type that adds a context property.
|
|
13
|
-
* - When TContext is undefined (default): context
|
|
13
|
+
* - When TContext is undefined (default): NO context property (explicit opt-in)
|
|
14
14
|
* - When TContext includes undefined (e.g., `{ foo: string } | undefined`): context is OPTIONAL but typed
|
|
15
15
|
* - When TContext is a specific type without undefined: context is REQUIRED with that exact type
|
|
16
16
|
*
|
|
17
|
-
* This
|
|
17
|
+
* This follows Rust's explicit error philosophy: context must be explicitly added via .withContext<T>().
|
|
18
18
|
*/
|
|
19
|
-
type WithContext<TContext> = [TContext] extends [undefined] ? {
|
|
20
|
-
context?: Record<string, unknown>;
|
|
21
|
-
} : [undefined] extends [TContext] ? {
|
|
19
|
+
type WithContext<TContext> = [TContext] extends [undefined] ? {} : [undefined] extends [TContext] ? {
|
|
22
20
|
context?: Exclude<TContext, undefined>;
|
|
23
21
|
} : {
|
|
24
22
|
context: TContext;
|
|
25
23
|
};
|
|
26
24
|
/**
|
|
27
25
|
* Helper type that adds a cause property.
|
|
28
|
-
* - When TCause is undefined (default): cause
|
|
26
|
+
* - When TCause is undefined (default): NO cause property (explicit opt-in)
|
|
29
27
|
* - When TCause includes undefined (e.g., `NetworkError | undefined`): cause is OPTIONAL, constrained
|
|
30
|
-
* - When TCause is a specific type: cause is
|
|
28
|
+
* - When TCause is a specific type without undefined: cause is REQUIRED
|
|
31
29
|
*
|
|
32
|
-
*
|
|
33
|
-
* but when TCause is specified, it constrains what cause types are allowed.
|
|
30
|
+
* This follows Rust's explicit error philosophy: cause must be explicitly added via .withCause<T>().
|
|
34
31
|
* Using brackets to prevent distributive conditional behavior with union types.
|
|
35
32
|
*/
|
|
36
|
-
type WithCause<TCause> = [TCause] extends [undefined] ? {
|
|
37
|
-
cause?: AnyTaggedError;
|
|
38
|
-
} : [undefined] extends [TCause] ? {
|
|
33
|
+
type WithCause<TCause> = [TCause] extends [undefined] ? {} : [undefined] extends [TCause] ? {
|
|
39
34
|
cause?: Exclude<TCause, undefined>;
|
|
40
35
|
} : {
|
|
41
|
-
cause
|
|
36
|
+
cause: TCause;
|
|
42
37
|
};
|
|
43
38
|
/**
|
|
44
39
|
* Creates a tagged error type for type-safe error handling.
|
|
45
40
|
* Uses the `name` property as a discriminator for tagged unions.
|
|
46
41
|
*
|
|
47
|
-
*
|
|
48
|
-
*
|
|
49
|
-
*
|
|
42
|
+
* **Explicit Opt-In Philosophy (Rust-inspired):**
|
|
43
|
+
* By default, errors only have `name` and `message`. Context and cause must be
|
|
44
|
+
* explicitly added via type parameters. This follows Rust's thiserror pattern
|
|
45
|
+
* where error properties are intentional architectural decisions.
|
|
50
46
|
*
|
|
51
47
|
* **Type Parameter Behavior:**
|
|
52
|
-
* - When `TContext` is `undefined` (default):
|
|
53
|
-
* - When `TContext` is `{ ... } | undefined`: `context` is OPTIONAL but typed
|
|
54
|
-
* - When `TContext` is specified without undefined: `context` is REQUIRED
|
|
55
|
-
* - When `TCause` is `undefined` (default):
|
|
56
|
-
* - When `TCause` is
|
|
48
|
+
* - When `TContext` is `undefined` (default): NO context property
|
|
49
|
+
* - When `TContext` is `{ ... } | undefined`: `context` is OPTIONAL but typed
|
|
50
|
+
* - When `TContext` is specified without undefined: `context` is REQUIRED
|
|
51
|
+
* - When `TCause` is `undefined` (default): NO cause property
|
|
52
|
+
* - When `TCause` is `{ ... } | undefined`: `cause` is OPTIONAL but typed
|
|
53
|
+
* - When `TCause` is specified without undefined: `cause` is REQUIRED
|
|
57
54
|
*
|
|
58
55
|
* @template TName - The error name (discriminator for tagged unions)
|
|
59
|
-
* @template TContext - Additional context data for the error (default: undefined =
|
|
60
|
-
* @template TCause - The type of error that caused this error (default: undefined =
|
|
56
|
+
* @template TContext - Additional context data for the error (default: undefined = no context)
|
|
57
|
+
* @template TCause - The type of error that caused this error (default: undefined = no cause)
|
|
61
58
|
*
|
|
62
59
|
* @example
|
|
63
60
|
* ```ts
|
|
64
|
-
* //
|
|
61
|
+
* // Minimal error (no context, no cause)
|
|
65
62
|
* type ValidationError = TaggedError<"ValidationError">;
|
|
66
63
|
* const validationError: ValidationError = {
|
|
67
64
|
* name: "ValidationError",
|
|
68
65
|
* message: "Input is required"
|
|
69
66
|
* };
|
|
70
|
-
* // validationError
|
|
67
|
+
* // validationError only has name and message
|
|
71
68
|
*
|
|
72
|
-
* // Error with required context
|
|
69
|
+
* // Error with required context
|
|
73
70
|
* type NetworkError = TaggedError<"NetworkError", { host: string; port: number }>;
|
|
74
71
|
* const networkError: NetworkError = {
|
|
75
72
|
* name: "NetworkError",
|
|
76
73
|
* message: "Socket timeout",
|
|
77
74
|
* context: { host: "db.example.com", port: 5432 } // Required!
|
|
78
75
|
* };
|
|
79
|
-
* const host = networkError.context.host; // Type-safe, no optional chaining needed
|
|
80
76
|
*
|
|
81
77
|
* // Error with OPTIONAL but TYPED context (union with undefined)
|
|
82
78
|
* type LogError = TaggedError<"LogError", { file: string; line: number } | undefined>;
|
|
83
|
-
* const logError1: LogError = { name: "LogError", message: "Parse failed" }; // OK
|
|
84
|
-
* const logError2: LogError = { name: "LogError", message: "Parse failed", context: { file: "app.ts", line: 42 } }; // OK
|
|
79
|
+
* const logError1: LogError = { name: "LogError", message: "Parse failed" }; // OK
|
|
80
|
+
* const logError2: LogError = { name: "LogError", message: "Parse failed", context: { file: "app.ts", line: 42 } }; // OK
|
|
85
81
|
*
|
|
86
|
-
* // Error with
|
|
87
|
-
* type DatabaseError = TaggedError<"DatabaseError", { operation: string }, NetworkError>;
|
|
82
|
+
* // Error with required context and optional cause
|
|
83
|
+
* type DatabaseError = TaggedError<"DatabaseError", { operation: string }, NetworkError | undefined>;
|
|
88
84
|
* const dbError: DatabaseError = {
|
|
89
85
|
* name: "DatabaseError",
|
|
90
86
|
* message: "Failed to connect to database",
|
|
@@ -166,14 +162,15 @@ type ReplaceErrorWithErr<T extends `${string}Error`> = T extends `${infer TBase}
|
|
|
166
162
|
type OptionalIfUndefined<T, TKey extends string> = undefined extends T ? { [K in TKey]?: Exclude<T, undefined> } : { [K in TKey]: T };
|
|
167
163
|
/**
|
|
168
164
|
* Input type for error constructors with fluent API context/cause handling.
|
|
165
|
+
*
|
|
166
|
+
* Follows explicit opt-in philosophy:
|
|
167
|
+
* - When TContext/TCause is undefined: property doesn't exist
|
|
168
|
+
* - When TContext/TCause includes undefined: property is optional but typed
|
|
169
|
+
* - When TContext/TCause is a specific type: property is required
|
|
169
170
|
*/
|
|
170
171
|
type ErrorInput<TContext extends Record<string, unknown> | undefined, TCause extends AnyTaggedError | undefined> = {
|
|
171
172
|
message: string;
|
|
172
|
-
} & (TContext extends undefined ? {
|
|
173
|
-
context?: Record<string, unknown>;
|
|
174
|
-
} : OptionalIfUndefined<TContext, "context">) & (TCause extends undefined ? {
|
|
175
|
-
cause?: AnyTaggedError;
|
|
176
|
-
} : OptionalIfUndefined<TCause, "cause">);
|
|
173
|
+
} & (TContext extends undefined ? {} : OptionalIfUndefined<TContext, "context">) & (TCause extends undefined ? {} : OptionalIfUndefined<TCause, "cause">);
|
|
177
174
|
/**
|
|
178
175
|
* The factories object returned by createTaggedError and its builder methods.
|
|
179
176
|
*/
|
|
@@ -209,8 +206,17 @@ type ErrorBuilder<TName extends `${string}Error`, TContext extends Record<string
|
|
|
209
206
|
* LogError({ message: 'Parse error' }) // OK
|
|
210
207
|
* LogError({ message: 'Parse error', context: { file: 'app.ts', line: 42 } }) // OK
|
|
211
208
|
* ```
|
|
209
|
+
*
|
|
210
|
+
* @example Default (no generic): permissive optional context
|
|
211
|
+
* ```ts
|
|
212
|
+
* const { FlexError } = createTaggedError('FlexError')
|
|
213
|
+
* .withContext() // Defaults to Record<string, unknown> | undefined
|
|
214
|
+
*
|
|
215
|
+
* FlexError({ message: 'Error' }) // OK - context is optional
|
|
216
|
+
* FlexError({ message: 'Error', context: { anything: 'works' } }) // OK
|
|
217
|
+
* ```
|
|
212
218
|
*/
|
|
213
|
-
withContext<T extends Record<string, unknown> | undefined>(): ErrorBuilder<TName, T, TCause>;
|
|
219
|
+
withContext<T extends Record<string, unknown> | undefined = Record<string, unknown> | undefined>(): ErrorBuilder<TName, T, TCause>;
|
|
214
220
|
/**
|
|
215
221
|
* Constrains the cause type for this error.
|
|
216
222
|
*
|
|
@@ -239,8 +245,17 @@ type ErrorBuilder<TName extends `${string}Error`, TContext extends Record<string
|
|
|
239
245
|
* UnhandledError({ message: 'Unexpected', cause: originalError }) // OK
|
|
240
246
|
* UnhandledError({ message: 'Unexpected' }) // Type error: cause required
|
|
241
247
|
* ```
|
|
248
|
+
*
|
|
249
|
+
* @example Default (no generic): permissive optional cause
|
|
250
|
+
* ```ts
|
|
251
|
+
* const { FlexError } = createTaggedError('FlexError')
|
|
252
|
+
* .withCause() // Defaults to AnyTaggedError | undefined
|
|
253
|
+
*
|
|
254
|
+
* FlexError({ message: 'Error' }) // OK - cause is optional
|
|
255
|
+
* FlexError({ message: 'Error', cause: anyTaggedError }) // OK
|
|
256
|
+
* ```
|
|
242
257
|
*/
|
|
243
|
-
withCause<T extends AnyTaggedError | undefined>(): ErrorBuilder<TName, TContext, T>;
|
|
258
|
+
withCause<T extends AnyTaggedError | undefined = AnyTaggedError | undefined>(): ErrorBuilder<TName, TContext, T>;
|
|
244
259
|
};
|
|
245
260
|
/**
|
|
246
261
|
* Creates a new tagged error type with a fluent builder API.
|
|
@@ -248,12 +263,13 @@ type ErrorBuilder<TName extends `${string}Error`, TContext extends Record<string
|
|
|
248
263
|
* Returns an object containing:
|
|
249
264
|
* - `{Name}Error`: Factory function that creates plain TaggedError objects
|
|
250
265
|
* - `{Name}Err`: Factory function that creates Err-wrapped TaggedError objects
|
|
251
|
-
* - `withContext<T>()`: Chain method to
|
|
252
|
-
* - `withCause<T>()`: Chain method to
|
|
266
|
+
* - `withContext<T>()`: Chain method to add context type
|
|
267
|
+
* - `withCause<T>()`: Chain method to add cause type
|
|
253
268
|
*
|
|
254
|
-
* **
|
|
255
|
-
*
|
|
256
|
-
*
|
|
269
|
+
* **Explicit Opt-In (Rust-inspired):**
|
|
270
|
+
* By default, errors only have `{ name, message }`. Context and cause must be
|
|
271
|
+
* explicitly added via `.withContext<T>()` and `.withCause<T>()`. This follows
|
|
272
|
+
* Rust's thiserror pattern where error properties are intentional decisions.
|
|
257
273
|
*
|
|
258
274
|
* **Optionality via type unions:**
|
|
259
275
|
* Both `withContext` and `withCause` determine optionality based on whether
|
|
@@ -264,12 +280,12 @@ type ErrorBuilder<TName extends `${string}Error`, TContext extends Record<string
|
|
|
264
280
|
* @template TName - The name of the error type (must end with "Error")
|
|
265
281
|
* @param name - The name of the error type
|
|
266
282
|
*
|
|
267
|
-
* @example
|
|
283
|
+
* @example Minimal error (no context, no cause)
|
|
268
284
|
* ```ts
|
|
269
285
|
* const { NetworkError, NetworkErr } = createTaggedError('NetworkError')
|
|
270
286
|
*
|
|
271
287
|
* NetworkError({ message: 'Connection failed' })
|
|
272
|
-
*
|
|
288
|
+
* // Error only has { name: 'NetworkError', message: 'Connection failed' }
|
|
273
289
|
* ```
|
|
274
290
|
*
|
|
275
291
|
* @example Required context
|
|
@@ -299,6 +315,13 @@ type ErrorBuilder<TName extends `${string}Error`, TContext extends Record<string
|
|
|
299
315
|
* // Type extraction works
|
|
300
316
|
* type UserServiceError = ReturnType<typeof UserServiceError>
|
|
301
317
|
* ```
|
|
318
|
+
*
|
|
319
|
+
* @example Permissive mode (if you want the old behavior)
|
|
320
|
+
* ```ts
|
|
321
|
+
* const { FlexibleError } = createTaggedError('FlexibleError')
|
|
322
|
+
* .withContext<Record<string, unknown> | undefined>()
|
|
323
|
+
* .withCause<AnyTaggedError | undefined>()
|
|
324
|
+
* ```
|
|
302
325
|
*/
|
|
303
326
|
declare function createTaggedError<TName extends `${string}Error`>(name: TName): ErrorBuilder<TName>;
|
|
304
327
|
//#endregion
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","names":[],"sources":["../../src/error/types.ts","../../src/error/utils.ts"],"sourcesContent":[],"mappings":";;;;;;AAGY,KAAA,cAAA,GAAc;EAUrB,IAAA,EAAA,MAAA;EAAW,OAAA,EAAA,MAAA;CAAA
|
|
1
|
+
{"version":3,"file":"index.d.ts","names":[],"sources":["../../src/error/types.ts","../../src/error/utils.ts"],"sourcesContent":[],"mappings":";;;;;;AAGY,KAAA,cAAA,GAAc;EAUrB,IAAA,EAAA,MAAA;EAAW,OAAA,EAAA,MAAA;CAAA;;;;;AAIO;AAAA;;;KAJlB,WAiBmB,CAAA,QAAA,CAAA,GAAA,CAjBM,QAiBN,CAAA,SAAA,CAAA,SAAA,CAAA,GAAA,CAAA,CAAA,GAAA,CAAA,SAAA,CAAA,SAAA,CAfA,QAeA,CAAA,GAAA;EAAM,OACR,CAAA,EAfN,OAeM,CAfE,QAeF,EAAA,SAAA,CAAA;CAAM,GAAA;EAAP,OACR,EAfE,QAeF;AAAM,CAAA;AAkEnB;;;;;;;;;KAtEK,SA0ED,CAAA,MAAA,CAAA,GAAA,CA1EsB,MA0EtB,CAAA,SAAA,CAAA,SAAA,CAAA,GAAA,CAAA,CAAA,GAAA,CAAA,SAAA,CAAA,SAAA,CAxEoB,MAwEpB,CAAA,GAAA;EAAQ,KAAA,CAAA,EAvEE,OAuEF,CAvEU,MAuEV,EAAA,SAAA,CAAA;;SAtEC;;ACMb;AAkDC;;;;AAe0C;AAAA;;;;;;;;AAaxB;AAAA;;;;;;;;;;AAkBI;AAAA;;;;;;;;;;;;;;;;;;;;;;AAgBd;AAAA;;;;;;;;;;;;;;;AA2FgC,KD/I7B,WC+I6B,CAAA,cAAA,MAAA,GAAA,MAAA,EAAA,iBD7IvB,MC6IuB,CAAA,MAAA,EAAA,OAAA,CAAA,GAAA,SAAA,GAAA,SAAA,EAAA,eD5IzB,cC4IyB,GAAA,SAAA,GAAA,SAAA,CAAA,GD3IrC,QC2IqC,CAAA;EAAc,IACpC,ED1IX,KC0IW;EAAK,OAAE,EAAA,MAAA;CAAQ,GDxI7B,WCwI+B,CDxInB,QCwImB,CAAA,GDvIlC,SCuIkC,CDvIxB,MCuIwB,CAAA,CAAA;;;;AD/OpC;AAA+D;;;;;;;AAcxC;AAAA;;;;;;;AAeJ;AAkEnB;;;;;;;;;;AAIY;;;;AChEZ;AAkDC;;AAeA,iBAjEe,mBAAA,CAiEf,KAAA,EAAA,OAAA,CAAA,EAAA,MAAA;;AAA0C;AAAA;;;;;;;;AAaxB;AAAA;KAdd,mBAwBU,CAAA,UAAA,GAAA,MAAA,OAAA,CAAA,GAvBd,CAuBc,SAAA,GAAA,KAAA,MAAA,OAAA,GAAA,GAvBuB,KAuBvB,KAAA,GAAA,KAAA;;;;;;KAZV,mBAkBH,CAAA,CAAA,EAAA,aAAA,MAAA,CAAA,GAAA,SAAA,SAlBmE,CAkBnE,GAAA,QAjBS,IAmBa,IAnBL,OAmBK,CAnBG,CAmBH,EAAA,SAAA,CAAA,EAAM,GAAA,QAlBnB,IAkBY,GAlBL,CAkBK,EAAA;;;;;;;;;KARlB,UAoBoB,CAAA,iBAnBP,MAmBO,CAAA,MAAA,EAAA,OAAA,CAAA,GAAA,SAAA,EAAA,eAlBT,cAkBS,GAAA,SAAA,CAAA,GAAA;EAAQ,OAAE,EAAA,MAAA;CAAM,GAAA,CAjBd,QAiBrB,SAAA,SAAA,GAAA,CAAA,CAAA,GAfH,mBAeG,CAfiB,QAejB,EAAA,SAAA,CAAA,CAAA,GAAA,CAdJ,MAcI,SAAA,SAAA,GAAA,CAAA,CAAA,GAZF,mBAYE,CAZkB,MAYlB,EAAA,OAAA,CAAA,CAAA;;;;KAPD,cAUyB,CAAA,cAAA,GAAA,MAAA,OAAA,EAAA,iBARZ,MAQY,CAAA,MAAA,EAAA,OAAA,CAAA,GAAA,SAAA,EAAA,eAPd,cAOc,GAAA,SAAA,CAAA,GAAA,QALvB,KAKE,GAAA,CAAA,KAAA,EAJA,UAIA,CAJW,QAIX,EAJqB,MAIrB,CAAA,EAAA,GAHH,WAGG,CAHS,KAGT,EAHgB,QAGhB,EAH0B,MAG1B,CAAA,EAAU,GAAA,QADZ,mBAEsB,CAFF,KAEE,CAAA,GAAA,CAAA,KAAA,EADpB,UACoB,CADT,QACS,EADC,MACD,CAAA,EAAA,GAAvB,GAAuB,CAAnB,WAAmB,CAAP,KAAO,EAAA,QAAA,EAAU,MAAV,CAAA,CAAA,EAAQ;;;AAA5B;AAAA;KAOJ,YAAY,CAAA,cAAA,GAAA,MAAA,OAAA,EAAA,iBAEC,MAFD,CAAA,MAAA,EAAA,OAAA,CAAA,GAAA,SAAA,GAAA,SAAA,EAAA,eAGD,cAHC,GAAA,SAAA,GAAA,SAAA,CAAA,GAIb,cAJa,CAIE,KAJF,EAIS,QAJT,EAImB,MAJnB,CAAA,GAAA;EAAA;;;;;;;;;;;;;;;;;;AAqFC;AAyElB;;;;;AAEe;;;;;;;;;;;;wBAtHH,sCAAsC,wCAC5C,aAAa,OAAO,GAAG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;sBAyCjB,6BAA6B,+BACnC,aAAa,OAAO,UAAU;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;iBAyEpB,wDACT,QACJ,aAAa"}
|
package/dist/error/index.js
CHANGED
|
@@ -69,12 +69,13 @@ function extractErrorMessage(error) {
|
|
|
69
69
|
* Returns an object containing:
|
|
70
70
|
* - `{Name}Error`: Factory function that creates plain TaggedError objects
|
|
71
71
|
* - `{Name}Err`: Factory function that creates Err-wrapped TaggedError objects
|
|
72
|
-
* - `withContext<T>()`: Chain method to
|
|
73
|
-
* - `withCause<T>()`: Chain method to
|
|
72
|
+
* - `withContext<T>()`: Chain method to add context type
|
|
73
|
+
* - `withCause<T>()`: Chain method to add cause type
|
|
74
74
|
*
|
|
75
|
-
* **
|
|
76
|
-
*
|
|
77
|
-
*
|
|
75
|
+
* **Explicit Opt-In (Rust-inspired):**
|
|
76
|
+
* By default, errors only have `{ name, message }`. Context and cause must be
|
|
77
|
+
* explicitly added via `.withContext<T>()` and `.withCause<T>()`. This follows
|
|
78
|
+
* Rust's thiserror pattern where error properties are intentional decisions.
|
|
78
79
|
*
|
|
79
80
|
* **Optionality via type unions:**
|
|
80
81
|
* Both `withContext` and `withCause` determine optionality based on whether
|
|
@@ -85,12 +86,12 @@ function extractErrorMessage(error) {
|
|
|
85
86
|
* @template TName - The name of the error type (must end with "Error")
|
|
86
87
|
* @param name - The name of the error type
|
|
87
88
|
*
|
|
88
|
-
* @example
|
|
89
|
+
* @example Minimal error (no context, no cause)
|
|
89
90
|
* ```ts
|
|
90
91
|
* const { NetworkError, NetworkErr } = createTaggedError('NetworkError')
|
|
91
92
|
*
|
|
92
93
|
* NetworkError({ message: 'Connection failed' })
|
|
93
|
-
*
|
|
94
|
+
* // Error only has { name: 'NetworkError', message: 'Connection failed' }
|
|
94
95
|
* ```
|
|
95
96
|
*
|
|
96
97
|
* @example Required context
|
|
@@ -120,6 +121,13 @@ function extractErrorMessage(error) {
|
|
|
120
121
|
* // Type extraction works
|
|
121
122
|
* type UserServiceError = ReturnType<typeof UserServiceError>
|
|
122
123
|
* ```
|
|
124
|
+
*
|
|
125
|
+
* @example Permissive mode (if you want the old behavior)
|
|
126
|
+
* ```ts
|
|
127
|
+
* const { FlexibleError } = createTaggedError('FlexibleError')
|
|
128
|
+
* .withContext<Record<string, unknown> | undefined>()
|
|
129
|
+
* .withCause<AnyTaggedError | undefined>()
|
|
130
|
+
* ```
|
|
123
131
|
*/
|
|
124
132
|
function createTaggedError(name) {
|
|
125
133
|
const createBuilder = () => {
|
package/dist/error/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","names":["error: unknown","name: TName","input: ErrorInput<TContext, TCause>"],"sources":["../../src/error/utils.ts"],"sourcesContent":["import type { TaggedError, AnyTaggedError } from \"./types.js\";\nimport { Err } from \"../result/result.js\";\n\n/**\n * Extracts a readable error message from an unknown error value\n *\n * This utility is commonly used in mapErr functions when converting\n * unknown errors to typed error objects in the Result system.\n *\n * @param error - The unknown error to extract a message from\n * @returns A string representation of the error\n *\n * @example\n * ```ts\n * // With native Error\n * const error = new Error(\"Something went wrong\");\n * const message = extractErrorMessage(error); // \"Something went wrong\"\n *\n * // With string error\n * const stringError = \"String error\";\n * const message2 = extractErrorMessage(stringError); // \"String error\"\n *\n * // With object error\n * const unknownError = { code: 500, details: \"Server error\" };\n * const message3 = extractErrorMessage(unknownError); // '{\"code\":500,\"details\":\"Server error\"}'\n *\n * // Used in mapErr function\n * const result = await tryAsync({\n * try: () => riskyOperation(),\n * mapErr: (error) => Err({\n * name: \"NetworkError\",\n * message: extractErrorMessage(error),\n * context: { operation: \"riskyOperation\" },\n * cause: error,\n * }),\n * });\n * ```\n */\nexport function extractErrorMessage(error: unknown): string {\n\t// Handle Error instances\n\tif (error instanceof Error) {\n\t\treturn error.message;\n\t}\n\n\t// Handle primitives\n\tif (typeof error === \"string\") return error;\n\tif (\n\t\ttypeof error === \"number\" ||\n\t\ttypeof error === \"boolean\" ||\n\t\ttypeof error === \"bigint\"\n\t)\n\t\treturn String(error);\n\tif (typeof error === \"symbol\") return error.toString();\n\tif (error === null) return \"null\";\n\tif (error === undefined) return \"undefined\";\n\n\t// Handle arrays\n\tif (Array.isArray(error)) return JSON.stringify(error);\n\n\t// Handle plain objects\n\tif (typeof error === \"object\") {\n\t\tconst errorObj = error as Record<string, unknown>;\n\n\t\t// Check common error properties\n\t\tconst messageProps = [\n\t\t\t\"message\",\n\t\t\t\"error\",\n\t\t\t\"description\",\n\t\t\t\"title\",\n\t\t\t\"reason\",\n\t\t\t\"details\",\n\t\t] as const;\n\t\tfor (const prop of messageProps) {\n\t\t\tif (prop in errorObj && typeof errorObj[prop] === \"string\") {\n\t\t\t\treturn errorObj[prop];\n\t\t\t}\n\t\t}\n\n\t\t// Fallback to JSON stringification\n\t\ttry {\n\t\t\treturn JSON.stringify(error);\n\t\t} catch {\n\t\t\treturn String(error);\n\t\t}\n\t}\n\n\t// Final fallback\n\treturn String(error);\n}\n\n/**\n * Replaces the \"Error\" suffix with \"Err\" suffix in error type names.\n *\n * @template T - An error type name that must end with \"Error\"\n * @returns The type name with \"Error\" replaced by \"Err\"\n *\n * @example\n * ```ts\n * type NetworkErr = ReplaceErrorWithErr<\"NetworkError\">; // \"NetworkErr\"\n * type ValidationErr = ReplaceErrorWithErr<\"ValidationError\">; // \"ValidationErr\"\n * ```\n */\ntype ReplaceErrorWithErr<T extends `${string}Error`> =\n\tT extends `${infer TBase}Error` ? `${TBase}Err` : never;\n\n// =============================================================================\n// Fluent API Types\n// =============================================================================\n\n/**\n * Helper type that determines optionality based on whether T includes undefined.\n * - If T includes undefined → property is optional\n * - If T does not include undefined → property is required\n */\ntype OptionalIfUndefined<T, TKey extends string> = undefined extends T\n\t? { [K in TKey]?: Exclude<T, undefined> }\n\t: { [K in TKey]: T };\n\n/**\n * Input type for error constructors with fluent API context/cause handling.\n */\ntype ErrorInput<\n\tTContext extends Record<string, unknown> | undefined,\n\tTCause extends AnyTaggedError | undefined,\n> = { message: string } & (TContext extends undefined\n\t? { context?: Record<string, unknown> }\n\t: OptionalIfUndefined<TContext, \"context\">) &\n\t(TCause extends undefined\n\t\t? { cause?: AnyTaggedError }\n\t\t: OptionalIfUndefined<TCause, \"cause\">);\n\n/**\n * The factories object returned by createTaggedError and its builder methods.\n */\ntype ErrorFactories<\n\tTName extends `${string}Error`,\n\tTContext extends Record<string, unknown> | undefined,\n\tTCause extends AnyTaggedError | undefined,\n> = {\n\t[K in TName]: (\n\t\tinput: ErrorInput<TContext, TCause>,\n\t) => TaggedError<TName, TContext, TCause>;\n} & {\n\t[K in ReplaceErrorWithErr<TName>]: (\n\t\tinput: ErrorInput<TContext, TCause>,\n\t) => Err<TaggedError<TName, TContext, TCause>>;\n};\n\n/**\n * Builder interface for the fluent createTaggedError API.\n * Provides chaining methods and the error factories.\n */\ntype ErrorBuilder<\n\tTName extends `${string}Error`,\n\tTContext extends Record<string, unknown> | undefined = undefined,\n\tTCause extends AnyTaggedError | undefined = undefined,\n> = ErrorFactories<TName, TContext, TCause> & {\n\t/**\n\t * Constrains the context type for this error.\n\t *\n\t * Optionality is determined by whether the type includes `undefined`:\n\t * - `withContext<T>()` where T doesn't include undefined → context is **required**\n\t * - `withContext<T | undefined>()` → context is **optional** but typed when provided\n\t *\n\t * @typeParam T - The shape of the context object. Include `| undefined` to make optional.\n\t *\n\t * @example Required context\n\t * ```ts\n\t * const { FileError } = createTaggedError('FileError')\n\t * .withContext<{ path: string }>()\n\t *\n\t * FileError({ message: 'Not found', context: { path: '/etc/config' } }) // OK\n\t * FileError({ message: 'Not found' }) // Type error: context required\n\t * ```\n\t *\n\t * @example Optional but typed context\n\t * ```ts\n\t * const { LogError } = createTaggedError('LogError')\n\t * .withContext<{ file: string; line: number } | undefined>()\n\t *\n\t * LogError({ message: 'Parse error' }) // OK\n\t * LogError({ message: 'Parse error', context: { file: 'app.ts', line: 42 } }) // OK\n\t * ```\n\t */\n\twithContext<T extends Record<string, unknown> | undefined>(): ErrorBuilder<\n\t\tTName,\n\t\tT,\n\t\tTCause\n\t>;\n\n\t/**\n\t * Constrains the cause type for this error.\n\t *\n\t * Optionality is determined by whether the type includes `undefined`:\n\t * - `withCause<T>()` where T doesn't include undefined → cause is **required**\n\t * - `withCause<T | undefined>()` → cause is **optional** but typed when provided\n\t *\n\t * Since cause is typically optional, include `| undefined` in most cases.\n\t *\n\t * @typeParam T - The allowed cause type(s). Include `| undefined` to make optional.\n\t *\n\t * @example Optional typed cause (common)\n\t * ```ts\n\t * const { ServiceError } = createTaggedError('ServiceError')\n\t * .withCause<DbError | CacheError | undefined>()\n\t *\n\t * ServiceError({ message: 'Failed' }) // OK\n\t * ServiceError({ message: 'Failed', cause: dbError }) // OK\n\t * ```\n\t *\n\t * @example Required cause (for wrapper errors)\n\t * ```ts\n\t * const { UnhandledError } = createTaggedError('UnhandledError')\n\t * .withCause<AnyTaggedError>()\n\t *\n\t * UnhandledError({ message: 'Unexpected', cause: originalError }) // OK\n\t * UnhandledError({ message: 'Unexpected' }) // Type error: cause required\n\t * ```\n\t */\n\twithCause<T extends AnyTaggedError | undefined>(): ErrorBuilder<\n\t\tTName,\n\t\tTContext,\n\t\tT\n\t>;\n};\n\n// =============================================================================\n// Fluent API Implementation\n// =============================================================================\n\n/**\n * Creates a new tagged error type with a fluent builder API.\n *\n * Returns an object containing:\n * - `{Name}Error`: Factory function that creates plain TaggedError objects\n * - `{Name}Err`: Factory function that creates Err-wrapped TaggedError objects\n * - `withContext<T>()`: Chain method to constrain context type\n * - `withCause<T>()`: Chain method to constrain cause type\n *\n * **Default behavior (no chaining):**\n * - `context` is optional and accepts any `Record<string, unknown>`\n * - `cause` is optional and accepts any `AnyTaggedError`\n *\n * **Optionality via type unions:**\n * Both `withContext` and `withCause` determine optionality based on whether\n * the type includes `undefined`:\n * - `T` without undefined → property is required\n * - `T | undefined` → property is optional but typed when provided\n *\n * @template TName - The name of the error type (must end with \"Error\")\n * @param name - The name of the error type\n *\n * @example Simple error (flexible mode)\n * ```ts\n * const { NetworkError, NetworkErr } = createTaggedError('NetworkError')\n *\n * NetworkError({ message: 'Connection failed' })\n * NetworkError({ message: 'Timeout', context: { url: 'https://...' } })\n * ```\n *\n * @example Required context\n * ```ts\n * const { ApiError, ApiErr } = createTaggedError('ApiError')\n * .withContext<{ endpoint: string; status: number }>()\n *\n * ApiError({ message: 'Failed', context: { endpoint: '/users', status: 500 } })\n * // ApiError({ message: 'Failed' }) // Type error: context required\n * ```\n *\n * @example Optional typed cause\n * ```ts\n * const { ServiceError } = createTaggedError('ServiceError')\n * .withCause<DbError | CacheError | undefined>()\n *\n * ServiceError({ message: 'Failed' }) // OK\n * ServiceError({ message: 'Failed', cause: dbError }) // OK, typed\n * ```\n *\n * @example Full example with both\n * ```ts\n * const { UserServiceError } = createTaggedError('UserServiceError')\n * .withContext<{ userId: string }>()\n * .withCause<RepoError | undefined>()\n *\n * // Type extraction works\n * type UserServiceError = ReturnType<typeof UserServiceError>\n * ```\n */\nexport function createTaggedError<TName extends `${string}Error`>(\n\tname: TName,\n): ErrorBuilder<TName> {\n\tconst createBuilder = <\n\t\tTContext extends Record<string, unknown> | undefined = undefined,\n\t\tTCause extends AnyTaggedError | undefined = undefined,\n\t>(): ErrorBuilder<TName, TContext, TCause> => {\n\t\tconst errorConstructor = (input: ErrorInput<TContext, TCause>) =>\n\t\t\t({ name, ...input }) as unknown as TaggedError<TName, TContext, TCause>;\n\n\t\tconst errName = name.replace(\n\t\t\t/Error$/,\n\t\t\t\"Err\",\n\t\t) as ReplaceErrorWithErr<TName>;\n\t\tconst errConstructor = (input: ErrorInput<TContext, TCause>) =>\n\t\t\tErr(errorConstructor(input));\n\n\t\treturn {\n\t\t\t[name]: errorConstructor,\n\t\t\t[errName]: errConstructor,\n\t\t\twithContext<T extends Record<string, unknown> | undefined>() {\n\t\t\t\treturn createBuilder<T, TCause>();\n\t\t\t},\n\t\t\twithCause<T extends AnyTaggedError | undefined>() {\n\t\t\t\treturn createBuilder<TContext, T>();\n\t\t\t},\n\t\t} as ErrorBuilder<TName, TContext, TCause>;\n\t};\n\n\treturn createBuilder();\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAsCA,SAAgB,oBAAoBA,OAAwB;AAE3D,KAAI,iBAAiB,MACpB,QAAO,MAAM;AAId,YAAW,UAAU,SAAU,QAAO;AACtC,YACQ,UAAU,mBACV,UAAU,oBACV,UAAU,SAEjB,QAAO,OAAO,MAAM;AACrB,YAAW,UAAU,SAAU,QAAO,MAAM,UAAU;AACtD,KAAI,UAAU,KAAM,QAAO;AAC3B,KAAI,iBAAqB,QAAO;AAGhC,KAAI,MAAM,QAAQ,MAAM,CAAE,QAAO,KAAK,UAAU,MAAM;AAGtD,YAAW,UAAU,UAAU;EAC9B,MAAM,WAAW;EAGjB,MAAM,eAAe;GACpB;GACA;GACA;GACA;GACA;GACA;EACA;AACD,OAAK,MAAM,QAAQ,aAClB,KAAI,QAAQ,mBAAmB,SAAS,UAAU,SACjD,QAAO,SAAS;AAKlB,MAAI;AACH,UAAO,KAAK,UAAU,MAAM;EAC5B,QAAO;AACP,UAAO,OAAO,MAAM;EACpB;CACD;AAGD,QAAO,OAAO,MAAM;AACpB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAwMD,SAAgB,kBACfC,MACsB;CACtB,MAAM,gBAAgB,MAGwB;EAC7C,MAAM,mBAAmB,CAACC,WACxB;GAAE;GAAM,GAAG;EAAO;EAEpB,MAAM,UAAU,KAAK,QACpB,UACA,MACA;EACD,MAAM,iBAAiB,CAACA,UACvB,IAAI,iBAAiB,MAAM,CAAC;AAE7B,SAAO;IACL,OAAO;IACP,UAAU;GACX,cAA6D;AAC5D,WAAO,eAA0B;GACjC;GACD,YAAkD;AACjD,WAAO,eAA4B;GACnC;EACD;CACD;AAED,QAAO,eAAe;AACtB"}
|
|
1
|
+
{"version":3,"file":"index.js","names":["error: unknown","name: TName","input: ErrorInput<TContext, TCause>"],"sources":["../../src/error/utils.ts"],"sourcesContent":["import type { TaggedError, AnyTaggedError } from \"./types.js\";\nimport { Err } from \"../result/result.js\";\n\n/**\n * Extracts a readable error message from an unknown error value\n *\n * This utility is commonly used in mapErr functions when converting\n * unknown errors to typed error objects in the Result system.\n *\n * @param error - The unknown error to extract a message from\n * @returns A string representation of the error\n *\n * @example\n * ```ts\n * // With native Error\n * const error = new Error(\"Something went wrong\");\n * const message = extractErrorMessage(error); // \"Something went wrong\"\n *\n * // With string error\n * const stringError = \"String error\";\n * const message2 = extractErrorMessage(stringError); // \"String error\"\n *\n * // With object error\n * const unknownError = { code: 500, details: \"Server error\" };\n * const message3 = extractErrorMessage(unknownError); // '{\"code\":500,\"details\":\"Server error\"}'\n *\n * // Used in mapErr function\n * const result = await tryAsync({\n * try: () => riskyOperation(),\n * mapErr: (error) => Err({\n * name: \"NetworkError\",\n * message: extractErrorMessage(error),\n * context: { operation: \"riskyOperation\" },\n * cause: error,\n * }),\n * });\n * ```\n */\nexport function extractErrorMessage(error: unknown): string {\n\t// Handle Error instances\n\tif (error instanceof Error) {\n\t\treturn error.message;\n\t}\n\n\t// Handle primitives\n\tif (typeof error === \"string\") return error;\n\tif (\n\t\ttypeof error === \"number\" ||\n\t\ttypeof error === \"boolean\" ||\n\t\ttypeof error === \"bigint\"\n\t)\n\t\treturn String(error);\n\tif (typeof error === \"symbol\") return error.toString();\n\tif (error === null) return \"null\";\n\tif (error === undefined) return \"undefined\";\n\n\t// Handle arrays\n\tif (Array.isArray(error)) return JSON.stringify(error);\n\n\t// Handle plain objects\n\tif (typeof error === \"object\") {\n\t\tconst errorObj = error as Record<string, unknown>;\n\n\t\t// Check common error properties\n\t\tconst messageProps = [\n\t\t\t\"message\",\n\t\t\t\"error\",\n\t\t\t\"description\",\n\t\t\t\"title\",\n\t\t\t\"reason\",\n\t\t\t\"details\",\n\t\t] as const;\n\t\tfor (const prop of messageProps) {\n\t\t\tif (prop in errorObj && typeof errorObj[prop] === \"string\") {\n\t\t\t\treturn errorObj[prop];\n\t\t\t}\n\t\t}\n\n\t\t// Fallback to JSON stringification\n\t\ttry {\n\t\t\treturn JSON.stringify(error);\n\t\t} catch {\n\t\t\treturn String(error);\n\t\t}\n\t}\n\n\t// Final fallback\n\treturn String(error);\n}\n\n/**\n * Replaces the \"Error\" suffix with \"Err\" suffix in error type names.\n *\n * @template T - An error type name that must end with \"Error\"\n * @returns The type name with \"Error\" replaced by \"Err\"\n *\n * @example\n * ```ts\n * type NetworkErr = ReplaceErrorWithErr<\"NetworkError\">; // \"NetworkErr\"\n * type ValidationErr = ReplaceErrorWithErr<\"ValidationError\">; // \"ValidationErr\"\n * ```\n */\ntype ReplaceErrorWithErr<T extends `${string}Error`> =\n\tT extends `${infer TBase}Error` ? `${TBase}Err` : never;\n\n// =============================================================================\n// Fluent API Types\n// =============================================================================\n\n/**\n * Helper type that determines optionality based on whether T includes undefined.\n * - If T includes undefined → property is optional\n * - If T does not include undefined → property is required\n */\ntype OptionalIfUndefined<T, TKey extends string> = undefined extends T\n\t? { [K in TKey]?: Exclude<T, undefined> }\n\t: { [K in TKey]: T };\n\n/**\n * Input type for error constructors with fluent API context/cause handling.\n *\n * Follows explicit opt-in philosophy:\n * - When TContext/TCause is undefined: property doesn't exist\n * - When TContext/TCause includes undefined: property is optional but typed\n * - When TContext/TCause is a specific type: property is required\n */\ntype ErrorInput<\n\tTContext extends Record<string, unknown> | undefined,\n\tTCause extends AnyTaggedError | undefined,\n> = { message: string } & (TContext extends undefined\n\t? {}\n\t: OptionalIfUndefined<TContext, \"context\">) &\n\t(TCause extends undefined\n\t\t? {}\n\t\t: OptionalIfUndefined<TCause, \"cause\">);\n\n/**\n * The factories object returned by createTaggedError and its builder methods.\n */\ntype ErrorFactories<\n\tTName extends `${string}Error`,\n\tTContext extends Record<string, unknown> | undefined,\n\tTCause extends AnyTaggedError | undefined,\n> = {\n\t[K in TName]: (\n\t\tinput: ErrorInput<TContext, TCause>,\n\t) => TaggedError<TName, TContext, TCause>;\n} & {\n\t[K in ReplaceErrorWithErr<TName>]: (\n\t\tinput: ErrorInput<TContext, TCause>,\n\t) => Err<TaggedError<TName, TContext, TCause>>;\n};\n\n/**\n * Builder interface for the fluent createTaggedError API.\n * Provides chaining methods and the error factories.\n */\ntype ErrorBuilder<\n\tTName extends `${string}Error`,\n\tTContext extends Record<string, unknown> | undefined = undefined,\n\tTCause extends AnyTaggedError | undefined = undefined,\n> = ErrorFactories<TName, TContext, TCause> & {\n\t/**\n\t * Constrains the context type for this error.\n\t *\n\t * Optionality is determined by whether the type includes `undefined`:\n\t * - `withContext<T>()` where T doesn't include undefined → context is **required**\n\t * - `withContext<T | undefined>()` → context is **optional** but typed when provided\n\t *\n\t * @typeParam T - The shape of the context object. Include `| undefined` to make optional.\n\t *\n\t * @example Required context\n\t * ```ts\n\t * const { FileError } = createTaggedError('FileError')\n\t * .withContext<{ path: string }>()\n\t *\n\t * FileError({ message: 'Not found', context: { path: '/etc/config' } }) // OK\n\t * FileError({ message: 'Not found' }) // Type error: context required\n\t * ```\n\t *\n\t * @example Optional but typed context\n\t * ```ts\n\t * const { LogError } = createTaggedError('LogError')\n\t * .withContext<{ file: string; line: number } | undefined>()\n\t *\n\t * LogError({ message: 'Parse error' }) // OK\n\t * LogError({ message: 'Parse error', context: { file: 'app.ts', line: 42 } }) // OK\n\t * ```\n\t *\n\t * @example Default (no generic): permissive optional context\n\t * ```ts\n\t * const { FlexError } = createTaggedError('FlexError')\n\t * .withContext() // Defaults to Record<string, unknown> | undefined\n\t *\n\t * FlexError({ message: 'Error' }) // OK - context is optional\n\t * FlexError({ message: 'Error', context: { anything: 'works' } }) // OK\n\t * ```\n\t */\n\twithContext<\n\t\tT extends Record<string, unknown> | undefined = Record<string, unknown> | undefined,\n\t>(): ErrorBuilder<TName, T, TCause>;\n\n\t/**\n\t * Constrains the cause type for this error.\n\t *\n\t * Optionality is determined by whether the type includes `undefined`:\n\t * - `withCause<T>()` where T doesn't include undefined → cause is **required**\n\t * - `withCause<T | undefined>()` → cause is **optional** but typed when provided\n\t *\n\t * Since cause is typically optional, include `| undefined` in most cases.\n\t *\n\t * @typeParam T - The allowed cause type(s). Include `| undefined` to make optional.\n\t *\n\t * @example Optional typed cause (common)\n\t * ```ts\n\t * const { ServiceError } = createTaggedError('ServiceError')\n\t * .withCause<DbError | CacheError | undefined>()\n\t *\n\t * ServiceError({ message: 'Failed' }) // OK\n\t * ServiceError({ message: 'Failed', cause: dbError }) // OK\n\t * ```\n\t *\n\t * @example Required cause (for wrapper errors)\n\t * ```ts\n\t * const { UnhandledError } = createTaggedError('UnhandledError')\n\t * .withCause<AnyTaggedError>()\n\t *\n\t * UnhandledError({ message: 'Unexpected', cause: originalError }) // OK\n\t * UnhandledError({ message: 'Unexpected' }) // Type error: cause required\n\t * ```\n\t *\n\t * @example Default (no generic): permissive optional cause\n\t * ```ts\n\t * const { FlexError } = createTaggedError('FlexError')\n\t * .withCause() // Defaults to AnyTaggedError | undefined\n\t *\n\t * FlexError({ message: 'Error' }) // OK - cause is optional\n\t * FlexError({ message: 'Error', cause: anyTaggedError }) // OK\n\t * ```\n\t */\n\twithCause<\n\t\tT extends AnyTaggedError | undefined = AnyTaggedError | undefined,\n\t>(): ErrorBuilder<TName, TContext, T>;\n};\n\n// =============================================================================\n// Fluent API Implementation\n// =============================================================================\n\n/**\n * Creates a new tagged error type with a fluent builder API.\n *\n * Returns an object containing:\n * - `{Name}Error`: Factory function that creates plain TaggedError objects\n * - `{Name}Err`: Factory function that creates Err-wrapped TaggedError objects\n * - `withContext<T>()`: Chain method to add context type\n * - `withCause<T>()`: Chain method to add cause type\n *\n * **Explicit Opt-In (Rust-inspired):**\n * By default, errors only have `{ name, message }`. Context and cause must be\n * explicitly added via `.withContext<T>()` and `.withCause<T>()`. This follows\n * Rust's thiserror pattern where error properties are intentional decisions.\n *\n * **Optionality via type unions:**\n * Both `withContext` and `withCause` determine optionality based on whether\n * the type includes `undefined`:\n * - `T` without undefined → property is required\n * - `T | undefined` → property is optional but typed when provided\n *\n * @template TName - The name of the error type (must end with \"Error\")\n * @param name - The name of the error type\n *\n * @example Minimal error (no context, no cause)\n * ```ts\n * const { NetworkError, NetworkErr } = createTaggedError('NetworkError')\n *\n * NetworkError({ message: 'Connection failed' })\n * // Error only has { name: 'NetworkError', message: 'Connection failed' }\n * ```\n *\n * @example Required context\n * ```ts\n * const { ApiError, ApiErr } = createTaggedError('ApiError')\n * .withContext<{ endpoint: string; status: number }>()\n *\n * ApiError({ message: 'Failed', context: { endpoint: '/users', status: 500 } })\n * // ApiError({ message: 'Failed' }) // Type error: context required\n * ```\n *\n * @example Optional typed cause\n * ```ts\n * const { ServiceError } = createTaggedError('ServiceError')\n * .withCause<DbError | CacheError | undefined>()\n *\n * ServiceError({ message: 'Failed' }) // OK\n * ServiceError({ message: 'Failed', cause: dbError }) // OK, typed\n * ```\n *\n * @example Full example with both\n * ```ts\n * const { UserServiceError } = createTaggedError('UserServiceError')\n * .withContext<{ userId: string }>()\n * .withCause<RepoError | undefined>()\n *\n * // Type extraction works\n * type UserServiceError = ReturnType<typeof UserServiceError>\n * ```\n *\n * @example Permissive mode (if you want the old behavior)\n * ```ts\n * const { FlexibleError } = createTaggedError('FlexibleError')\n * .withContext<Record<string, unknown> | undefined>()\n * .withCause<AnyTaggedError | undefined>()\n * ```\n */\nexport function createTaggedError<TName extends `${string}Error`>(\n\tname: TName,\n): ErrorBuilder<TName> {\n\tconst createBuilder = <\n\t\tTContext extends Record<string, unknown> | undefined = undefined,\n\t\tTCause extends AnyTaggedError | undefined = undefined,\n\t>(): ErrorBuilder<TName, TContext, TCause> => {\n\t\tconst errorConstructor = (input: ErrorInput<TContext, TCause>) =>\n\t\t\t({ name, ...input }) as unknown as TaggedError<TName, TContext, TCause>;\n\n\t\tconst errName = name.replace(\n\t\t\t/Error$/,\n\t\t\t\"Err\",\n\t\t) as ReplaceErrorWithErr<TName>;\n\t\tconst errConstructor = (input: ErrorInput<TContext, TCause>) =>\n\t\t\tErr(errorConstructor(input));\n\n\t\treturn {\n\t\t\t[name]: errorConstructor,\n\t\t\t[errName]: errConstructor,\n\t\t\twithContext<\n\t\t\t\tT extends Record<string, unknown> | undefined = Record<string, unknown> | undefined,\n\t\t\t>() {\n\t\t\t\treturn createBuilder<T, TCause>();\n\t\t\t},\n\t\t\twithCause<\n\t\t\t\tT extends AnyTaggedError | undefined = AnyTaggedError | undefined,\n\t\t\t>() {\n\t\t\t\treturn createBuilder<TContext, T>();\n\t\t\t},\n\t\t} as ErrorBuilder<TName, TContext, TCause>;\n\t};\n\n\treturn createBuilder();\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAsCA,SAAgB,oBAAoBA,OAAwB;AAE3D,KAAI,iBAAiB,MACpB,QAAO,MAAM;AAId,YAAW,UAAU,SAAU,QAAO;AACtC,YACQ,UAAU,mBACV,UAAU,oBACV,UAAU,SAEjB,QAAO,OAAO,MAAM;AACrB,YAAW,UAAU,SAAU,QAAO,MAAM,UAAU;AACtD,KAAI,UAAU,KAAM,QAAO;AAC3B,KAAI,iBAAqB,QAAO;AAGhC,KAAI,MAAM,QAAQ,MAAM,CAAE,QAAO,KAAK,UAAU,MAAM;AAGtD,YAAW,UAAU,UAAU;EAC9B,MAAM,WAAW;EAGjB,MAAM,eAAe;GACpB;GACA;GACA;GACA;GACA;GACA;EACA;AACD,OAAK,MAAM,QAAQ,aAClB,KAAI,QAAQ,mBAAmB,SAAS,UAAU,SACjD,QAAO,SAAS;AAKlB,MAAI;AACH,UAAO,KAAK,UAAU,MAAM;EAC5B,QAAO;AACP,UAAO,OAAO,MAAM;EACpB;CACD;AAGD,QAAO,OAAO,MAAM;AACpB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAmOD,SAAgB,kBACfC,MACsB;CACtB,MAAM,gBAAgB,MAGwB;EAC7C,MAAM,mBAAmB,CAACC,WACxB;GAAE;GAAM,GAAG;EAAO;EAEpB,MAAM,UAAU,KAAK,QACpB,UACA,MACA;EACD,MAAM,iBAAiB,CAACA,UACvB,IAAI,iBAAiB,MAAM,CAAC;AAE7B,SAAO;IACL,OAAO;IACP,UAAU;GACX,cAEI;AACH,WAAO,eAA0B;GACjC;GACD,YAEI;AACH,WAAO,eAA4B;GACnC;EACD;CACD;AAED,QAAO,eAAe;AACtB"}
|
package/dist/query/index.d.ts
CHANGED
|
@@ -7,7 +7,7 @@ import { DefaultError, MutationFunction, MutationKey, MutationOptions, QueryClie
|
|
|
7
7
|
/**
|
|
8
8
|
* Input options for defining a query.
|
|
9
9
|
*
|
|
10
|
-
* Extends TanStack Query's QueryObserverOptions but
|
|
10
|
+
* Extends TanStack Query's QueryObserverOptions but expects queryFn to return a Result type.
|
|
11
11
|
* This type represents the configuration for creating a query definition with both
|
|
12
12
|
* reactive and imperative interfaces for data fetching.
|
|
13
13
|
*
|
|
@@ -18,7 +18,7 @@ import { DefaultError, MutationFunction, MutationKey, MutationOptions, QueryClie
|
|
|
18
18
|
*/
|
|
19
19
|
type DefineQueryInput<TQueryFnData = unknown, TError = DefaultError, TData = TQueryFnData, TQueryData = TQueryFnData, TQueryKey extends QueryKey = QueryKey> = Omit<QueryObserverOptions<TQueryFnData, TError, TData, TQueryData, TQueryKey>, "queryFn"> & {
|
|
20
20
|
queryKey: TQueryKey;
|
|
21
|
-
|
|
21
|
+
queryFn: QueryFunction<Result<TQueryFnData, TError>, TQueryKey>;
|
|
22
22
|
};
|
|
23
23
|
/**
|
|
24
24
|
* Output of defineQuery function.
|
|
@@ -60,7 +60,7 @@ type DefineQueryOutput<TQueryFnData = unknown, TError = DefaultError, TData = TQ
|
|
|
60
60
|
/**
|
|
61
61
|
* Input options for defining a mutation.
|
|
62
62
|
*
|
|
63
|
-
* Extends TanStack Query's MutationOptions but
|
|
63
|
+
* Extends TanStack Query's MutationOptions but expects mutationFn to return a Result type.
|
|
64
64
|
* This type represents the configuration for creating a mutation definition with both
|
|
65
65
|
* reactive and imperative interfaces for data mutations.
|
|
66
66
|
*
|
|
@@ -71,7 +71,7 @@ type DefineQueryOutput<TQueryFnData = unknown, TError = DefaultError, TData = TQ
|
|
|
71
71
|
*/
|
|
72
72
|
type DefineMutationInput<TData, TError, TVariables = void, TContext = unknown> = Omit<MutationOptions<TData, TError, TVariables, TContext>, "mutationFn"> & {
|
|
73
73
|
mutationKey: MutationKey;
|
|
74
|
-
|
|
74
|
+
mutationFn: MutationFunction<Result<TData, TError>, TVariables>;
|
|
75
75
|
};
|
|
76
76
|
/**
|
|
77
77
|
* Output of defineMutation function.
|
|
@@ -138,7 +138,7 @@ type DefineMutationOutput<TData, TError, TVariables = void, TContext = unknown>
|
|
|
138
138
|
* // Now use defineQuery and defineMutation as before
|
|
139
139
|
* const userQuery = defineQuery({
|
|
140
140
|
* queryKey: ['user', userId],
|
|
141
|
-
*
|
|
141
|
+
* queryFn: () => services.getUser(userId)
|
|
142
142
|
* });
|
|
143
143
|
*
|
|
144
144
|
* // Use in components
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","names":[],"sources":["../../src/query/utils.ts"],"sourcesContent":[],"mappings":";;;;;;;;;AAwBA;;;;;;;;;AAO4C,KAPhC,gBAOgC,CAAA,eAAA,OAAA,EAAA,SALlC,YAKkC,EAAA,QAJnC,YAImC,EAAA,aAH9B,YAG8B,EAAA,kBAFzB,QAEyB,GAFd,QAEc,CAAA,GADxC,IACwC,CAA3C,oBAA2C,CAAtB,YAAsB,EAAR,MAAQ,EAAA,KAAA,EAAO,UAAP,EAAmB,SAAnB,CAAA,EAAA,SAAA,CAAA,GAAA;EAAK,QAAE,EAGxC,SAHwC;EAAU,
|
|
1
|
+
{"version":3,"file":"index.d.ts","names":[],"sources":["../../src/query/utils.ts"],"sourcesContent":[],"mappings":";;;;;;;;;AAwBA;;;;;;;;;AAO4C,KAPhC,gBAOgC,CAAA,eAAA,OAAA,EAAA,SALlC,YAKkC,EAAA,QAJnC,YAImC,EAAA,aAH9B,YAG8B,EAAA,kBAFzB,QAEyB,GAFd,QAEc,CAAA,GADxC,IACwC,CAA3C,oBAA2C,CAAtB,YAAsB,EAAR,MAAQ,EAAA,KAAA,EAAO,UAAP,EAAmB,SAAnB,CAAA,EAAA,SAAA,CAAA,GAAA;EAAK,QAAE,EAGxC,SAHwC;EAAU,OAAE,EAIrD,aAJqD,CAIvC,MAJuC,CAIhC,YAJgC,EAIlB,MAJkB,CAAA,EAIT,SAJS,CAAA;CAAS;;;;;;;;AAIjD;AAmCvB;;;;;;;;;;;;;;;;;;;;;;;;AAee,KAfH,iBAeG,CAAA,eAAA,OAAA,EAAA,SAbL,YAaK,EAAA,QAZN,YAYM,EAAA,aAXD,YAWC,EAAA,kBAVI,QAUJ,GAVe,QAUf,CAAA,GAAA,CAAA,GAAA,GATJ,OASI,CATI,MASJ,CATW,UASX,EATuB,MASvB,CAAA,CAAA,CAAA,GAAA;EAAO,OAAA,EARZ,oBAQY,CAPpB,YAOoB,EANpB,MAMoB,EALpB,KAKoB,EAJpB,UAIoB,EAHpB,SAGoB,CAAA;EAeV,KAAA,EAAA,GAAA,GAhBE,OAgBF,CAhBU,MAgBS,CAhBF,UAgBE,EAhBU,MAgBV,CAAA,CAAA;EAAA,MAAA,EAAA,GAAA,GAfhB,OAegB,CAfR,MAeQ,CAfD,UAeC,EAfW,MAeX,CAAA,CAAA;CAAA;;;;;;;;;;;;AAOF;AAiCjB,KAxCA,mBAwCoB,CAAA,KAAA,EAAA,MAAA,EAAA,aAAA,IAAA,EAAA,WAAA,OAAA,CAAA,GAnC5B,IAmC4B,CAnCvB,eAmCuB,CAnCP,KAmCO,EAnCA,MAmCA,EAnCQ,UAmCR,EAnCoB,QAmCpB,CAAA,EAAA,YAAA,CAAA,GAAA;EAAA,WAAA,EAlClB,WAkCkB;EAAA,UAKf,EAtCJ,gBAsCI,CAtCa,MAsCb,CAtCoB,KAsCpB,EAtC2B,MAsC3B,CAAA,EAtCoC,UAsCpC,CAAA;CAAU;;;;;;;;;;;;;;AAEiB;AA4C5C;;;;;;;;;;;;;;;;AA+EmD,KAlIvC,oBAkIuC,CAAA,KAAA,EAAA,MAAA,EAAA,aAAA,IAAA,EAAA,WAAA,OAAA,CAAA,GAAA,CAAA,CAAA,SAAA,EA7HlC,UA6HkC,EAAA,GA7HnB,OA6HmB,CA7HX,MA6HW,CA7HJ,KA6HI,EA7HG,MA6HH,CAAA,CAAA,CAAA,GAAA;EAAU,OAAE,EA5HrD,eA4HqD,CA5HrC,KA4HqC,EA5H9B,MA4H8B,EA5HtB,UA4HsB,EA5HV,QA4HU,CAAA;EAAS,OAApE,EAAA,CAAA,SAAA,EA3HkB,UA2HlB,EAAA,GA3HiC,OA2HjC,CA3HyC,MA2HzC,CA3HgD,KA2HhD,EA3HuD,MA2HvD,CAAA,CAAA;CAAiB;;;;;;;;;;AA+LG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;iBA9QR,oBAAA,cAAkC;iDAmE1C,eACD,2BACK,gCACQ,wCAET,iBACR,cACA,QACA,OACA,YACA,eAEC,kBAAkB,cAAc,QAAQ,OAAO,YAAY;kFA8LpD,oBAAoB,OAAO,QAAQ,YAAY,cACtD,qBAAqB,OAAO,QAAQ,YAAY"}
|
package/dist/query/index.js
CHANGED
|
@@ -33,7 +33,7 @@ import "../result-DfuKgZo9.js";
|
|
|
33
33
|
* // Now use defineQuery and defineMutation as before
|
|
34
34
|
* const userQuery = defineQuery({
|
|
35
35
|
* queryKey: ['user', userId],
|
|
36
|
-
*
|
|
36
|
+
* queryFn: () => services.getUser(userId)
|
|
37
37
|
* });
|
|
38
38
|
*
|
|
39
39
|
* // Use in components
|
|
@@ -69,7 +69,7 @@ function createQueryFactories(queryClient) {
|
|
|
69
69
|
*
|
|
70
70
|
* @param options - Query configuration object
|
|
71
71
|
* @param options.queryKey - Unique key for this query (used for caching and refetching)
|
|
72
|
-
* @param options.
|
|
72
|
+
* @param options.queryFn - Function that fetches data and returns a Result type
|
|
73
73
|
* @param options.* - Any other TanStack Query options (staleTime, refetchInterval, etc.)
|
|
74
74
|
*
|
|
75
75
|
* @returns Callable query definition with:
|
|
@@ -83,7 +83,7 @@ function createQueryFactories(queryClient) {
|
|
|
83
83
|
* // Step 1: Define your query in the query layer
|
|
84
84
|
* const userQuery = defineQuery({
|
|
85
85
|
* queryKey: ['users', userId],
|
|
86
|
-
*
|
|
86
|
+
* queryFn: () => services.getUser(userId), // Returns Result<User, ApiError>
|
|
87
87
|
* staleTime: 5 * 60 * 1000, // Consider data fresh for 5 minutes
|
|
88
88
|
* });
|
|
89
89
|
*
|
|
@@ -112,7 +112,7 @@ function createQueryFactories(queryClient) {
|
|
|
112
112
|
const newOptions = {
|
|
113
113
|
...options,
|
|
114
114
|
queryFn: async (context) => {
|
|
115
|
-
let result = options.
|
|
115
|
+
let result = options.queryFn(context);
|
|
116
116
|
if (result instanceof Promise) result = await result;
|
|
117
117
|
return resolve(result);
|
|
118
118
|
}
|
|
@@ -224,7 +224,7 @@ function createQueryFactories(queryClient) {
|
|
|
224
224
|
*
|
|
225
225
|
* @param options - Mutation configuration object
|
|
226
226
|
* @param options.mutationKey - Unique key for this mutation (used for tracking in-flight state)
|
|
227
|
-
* @param options.
|
|
227
|
+
* @param options.mutationFn - Function that performs the mutation and returns a Result type
|
|
228
228
|
* @param options.* - Any other TanStack Mutation options (onSuccess, onError, etc.)
|
|
229
229
|
*
|
|
230
230
|
* @returns Callable mutation definition with:
|
|
@@ -237,7 +237,7 @@ function createQueryFactories(queryClient) {
|
|
|
237
237
|
* // Step 1: Define your mutation with cache updates
|
|
238
238
|
* const createRecording = defineMutation({
|
|
239
239
|
* mutationKey: ['recordings', 'create'],
|
|
240
|
-
*
|
|
240
|
+
* mutationFn: async (recording: Recording) => {
|
|
241
241
|
* // Call the service
|
|
242
242
|
* const result = await services.db.createRecording(recording);
|
|
243
243
|
* if (result.error) return Err(result.error);
|
|
@@ -275,7 +275,7 @@ function createQueryFactories(queryClient) {
|
|
|
275
275
|
const newOptions = {
|
|
276
276
|
...options,
|
|
277
277
|
mutationFn: async (variables) => {
|
|
278
|
-
return resolve(await options.
|
|
278
|
+
return resolve(await options.mutationFn(variables));
|
|
279
279
|
}
|
|
280
280
|
};
|
|
281
281
|
/**
|
package/dist/query/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","names":["queryClient: QueryClient","options: DefineQueryInput<\n\t\t\tTQueryFnData,\n\t\t\tTError,\n\t\t\tTData,\n\t\t\tTQueryData,\n\t\t\tTQueryKey\n\t\t>","options: DefineMutationInput<TData, TError, TVariables, TContext>","variables: TVariables","options: MutationOptions<TData, TError, TVariables, TContext>"],"sources":["../../src/query/utils.ts"],"sourcesContent":["import type {\n\tDefaultError,\n\tMutationFunction,\n\tMutationKey,\n\tMutationOptions,\n\tQueryClient,\n\tQueryFunction,\n\tQueryKey,\n\tQueryObserverOptions,\n} from \"@tanstack/query-core\";\nimport { Err, Ok, type Result, resolve } from \"../result/index.js\";\n\n/**\n * Input options for defining a query.\n *\n * Extends TanStack Query's QueryObserverOptions but replaces queryFn with resultQueryFn.\n * This type represents the configuration for creating a query definition with both\n * reactive and imperative interfaces for data fetching.\n *\n * @template TQueryFnData - The type of data returned by the query function\n * @template TError - The type of error that can be thrown\n * @template TData - The type of data returned by the query (after select transform)\n * @template TQueryKey - The type of the query key\n */\nexport type DefineQueryInput<\n\tTQueryFnData = unknown,\n\tTError = DefaultError,\n\tTData = TQueryFnData,\n\tTQueryData = TQueryFnData,\n\tTQueryKey extends QueryKey = QueryKey,\n> = Omit<\n\tQueryObserverOptions<TQueryFnData, TError, TData, TQueryData, TQueryKey>,\n\t\"queryFn\"\n> & {\n\tqueryKey: TQueryKey;\n\tresultQueryFn: QueryFunction<Result<TQueryFnData, TError>, TQueryKey>;\n};\n\n/**\n * Output of defineQuery function.\n *\n * The query definition is directly callable and defaults to `ensure()` behavior,\n * which is recommended for most imperative use cases like preloaders.\n *\n * Provides both reactive and imperative interfaces for data fetching:\n * - `()` (callable): Same as `ensure()` - returns cached data if available, fetches if not\n * - `options`: Returns config for use with useQuery() or createQuery()\n * - `fetch()`: Always attempts to fetch data (from cache if fresh, network if stale)\n * - `ensure()`: Guarantees data availability, preferring cached data (recommended for preloaders)\n *\n * @template TQueryFnData - The type of data returned by the query function\n * @template TError - The type of error that can be thrown\n * @template TData - The type of data returned by the query (after select transform)\n * @template TQueryKey - The type of the query key\n *\n * @example\n * ```typescript\n * const userQuery = defineQuery({...});\n *\n * // Directly callable (same as .ensure())\n * const { data, error } = await userQuery();\n *\n * // Or use explicit methods\n * const { data, error } = await userQuery.ensure();\n * const { data, error } = await userQuery.fetch();\n *\n * // For reactive usage\n * const query = createQuery(userQuery.options);\n * ```\n */\nexport type DefineQueryOutput<\n\tTQueryFnData = unknown,\n\tTError = DefaultError,\n\tTData = TQueryFnData,\n\tTQueryData = TQueryFnData,\n\tTQueryKey extends QueryKey = QueryKey,\n> = (() => Promise<Result<TQueryData, TError>>) & {\n\toptions: QueryObserverOptions<\n\t\tTQueryFnData,\n\t\tTError,\n\t\tTData,\n\t\tTQueryData,\n\t\tTQueryKey\n\t>;\n\tfetch: () => Promise<Result<TQueryData, TError>>;\n\tensure: () => Promise<Result<TQueryData, TError>>;\n};\n\n/**\n * Input options for defining a mutation.\n *\n * Extends TanStack Query's MutationOptions but replaces mutationFn with resultMutationFn.\n * This type represents the configuration for creating a mutation definition with both\n * reactive and imperative interfaces for data mutations.\n *\n * @template TData - The type of data returned by the mutation\n * @template TError - The type of error that can be thrown\n * @template TVariables - The type of variables passed to the mutation\n * @template TContext - The type of context data for optimistic updates\n */\nexport type DefineMutationInput<\n\tTData,\n\tTError,\n\tTVariables = void,\n\tTContext = unknown,\n> = Omit<MutationOptions<TData, TError, TVariables, TContext>, \"mutationFn\"> & {\n\tmutationKey: MutationKey;\n\tresultMutationFn: MutationFunction<Result<TData, TError>, TVariables>;\n};\n\n/**\n * Output of defineMutation function.\n *\n * The mutation definition is directly callable, which executes the mutation\n * and returns a Result. This is equivalent to calling `.execute()`.\n *\n * Provides both reactive and imperative interfaces for data mutations:\n * - `(variables)` (callable): Same as `execute()` - directly executes the mutation\n * - `options`: Returns config for use with useMutation() or createMutation()\n * - `execute(variables)`: Directly executes the mutation and returns a Result\n *\n * @template TData - The type of data returned by the mutation\n * @template TError - The type of error that can be thrown\n * @template TVariables - The type of variables passed to the mutation\n * @template TContext - The type of context data for optimistic updates\n *\n * @example\n * ```typescript\n * const createUser = defineMutation({...});\n *\n * // Directly callable (same as .execute())\n * const { data, error } = await createUser({ name: 'John' });\n *\n * // Or use explicit method\n * const { data, error } = await createUser.execute({ name: 'John' });\n *\n * // For reactive usage\n * const mutation = createMutation(createUser.options);\n * ```\n */\nexport type DefineMutationOutput<\n\tTData,\n\tTError,\n\tTVariables = void,\n\tTContext = unknown,\n> = ((variables: TVariables) => Promise<Result<TData, TError>>) & {\n\toptions: MutationOptions<TData, TError, TVariables, TContext>;\n\texecute: (variables: TVariables) => Promise<Result<TData, TError>>;\n};\n\n/**\n * Creates factory functions for defining queries and mutations bound to a specific QueryClient.\n *\n * This factory pattern allows you to create isolated query/mutation definitions that are\n * bound to a specific QueryClient instance, enabling:\n * - Multiple query clients in the same application\n * - Testing with isolated query clients\n * - Framework-agnostic query definitions\n * - Proper separation of concerns between query logic and client instances\n *\n * The returned functions handle Result types automatically, unwrapping them for TanStack Query\n * while maintaining type safety throughout your application.\n *\n * @param queryClient - The QueryClient instance to bind the factories to\n * @returns An object containing defineQuery and defineMutation functions bound to the provided client\n *\n * @example\n * ```typescript\n * // Create your query client\n * const queryClient = new QueryClient({\n * defaultOptions: {\n * queries: { staleTime: 5 * 60 * 1000 }\n * }\n * });\n *\n * // Create the factory functions\n * const { defineQuery, defineMutation } = createQueryFactories(queryClient);\n *\n * // Now use defineQuery and defineMutation as before\n * const userQuery = defineQuery({\n * queryKey: ['user', userId],\n * resultQueryFn: () => services.getUser(userId)\n * });\n *\n * // Use in components\n * const query = createQuery(userQuery.options);\n *\n * // Or imperatively\n * const { data, error } = await userQuery.fetch();\n * ```\n */\nexport function createQueryFactories(queryClient: QueryClient) {\n\t/**\n\t * Creates a query definition that bridges the gap between pure service functions and reactive UI components.\n\t *\n\t * This factory function is the cornerstone of our data fetching architecture. It wraps service calls\n\t * with TanStack Query superpowers while maintaining type safety through Result types.\n\t *\n\t * The returned query definition is **directly callable** and defaults to `ensure()` behavior,\n\t * which is recommended for most imperative use cases like preloaders.\n\t *\n\t * ## Why use defineQuery?\n\t *\n\t * 1. **Callable**: Call directly like `userQuery()` for imperative data fetching\n\t * 2. **Dual Interface**: Also provides reactive (`.options`) and explicit imperative (`.fetch()`, `.ensure()`) APIs\n\t * 3. **Automatic Error Handling**: Service functions return `Result<T, E>` types which are automatically\n\t * unwrapped by TanStack Query, giving you proper error states in your components\n\t * 4. **Type Safety**: Full TypeScript support with proper inference for data and error types\n\t * 5. **Consistency**: Every query in the app follows the same pattern, making it easy to understand\n\t *\n\t * @template TQueryFnData - The type of data returned by the query function\n\t * @template TError - The type of error that can be thrown\n\t * @template TData - The type of data returned by the query (after select transform)\n\t * @template TQueryKey - The type of the query key\n\t *\n\t * @param options - Query configuration object\n\t * @param options.queryKey - Unique key for this query (used for caching and refetching)\n\t * @param options.resultQueryFn - Function that fetches data and returns a Result type\n\t * @param options.* - Any other TanStack Query options (staleTime, refetchInterval, etc.)\n\t *\n\t * @returns Callable query definition with:\n\t * - `()` (callable): Same as `ensure()` - returns cached data if available, fetches if not\n\t * - `.options`: Config for use with useQuery() or createQuery()\n\t * - `.fetch()`: Always attempts to fetch (from cache if fresh, network if stale)\n\t * - `.ensure()`: Guarantees data availability, preferring cached data (recommended for preloaders)\n\t *\n\t * @example\n\t * ```typescript\n\t * // Step 1: Define your query in the query layer\n\t * const userQuery = defineQuery({\n\t * queryKey: ['users', userId],\n\t * resultQueryFn: () => services.getUser(userId), // Returns Result<User, ApiError>\n\t * staleTime: 5 * 60 * 1000, // Consider data fresh for 5 minutes\n\t * });\n\t *\n\t * // Step 2a: Use reactively in a Svelte component\n\t * const query = createQuery(userQuery.options);\n\t * // $query.data is User | undefined\n\t * // $query.error is ApiError | null\n\t *\n\t * // Step 2b: Call directly in preloaders (recommended)\n\t * export const load = async () => {\n\t * const { data, error } = await userQuery(); // Same as userQuery.ensure()\n\t * if (error) throw error;\n\t * return { user: data };\n\t * };\n\t *\n\t * // Step 2c: Use explicit methods when needed\n\t * async function refreshUser() {\n\t * const { data, error } = await userQuery.fetch(); // Force fresh fetch\n\t * if (error) {\n\t * console.error('Failed to fetch user:', error);\n\t * }\n\t * }\n\t * ```\n\t */\n\tconst defineQuery = <\n\t\tTQueryFnData = unknown,\n\t\tTError = DefaultError,\n\t\tTData = TQueryFnData,\n\t\tTQueryData = TQueryFnData,\n\t\tTQueryKey extends QueryKey = QueryKey,\n\t>(\n\t\toptions: DefineQueryInput<\n\t\t\tTQueryFnData,\n\t\t\tTError,\n\t\t\tTData,\n\t\t\tTQueryData,\n\t\t\tTQueryKey\n\t\t>,\n\t): DefineQueryOutput<TQueryFnData, TError, TData, TQueryData, TQueryKey> => {\n\t\tconst newOptions = {\n\t\t\t...options,\n\t\t\tqueryFn: async (context) => {\n\t\t\t\tlet result = options.resultQueryFn(context);\n\t\t\t\tif (result instanceof Promise) result = await result;\n\t\t\t\treturn resolve(result);\n\t\t\t},\n\t\t} satisfies QueryObserverOptions<\n\t\t\tTQueryFnData,\n\t\t\tTError,\n\t\t\tTData,\n\t\t\tTQueryData,\n\t\t\tTQueryKey\n\t\t>;\n\n\t\t/**\n\t\t * Fetches data for this query using queryClient.fetchQuery().\n\t\t *\n\t\t * This method ALWAYS evaluates freshness and will refetch if data is stale.\n\t\t * It wraps TanStack Query's fetchQuery method, which returns cached data if fresh\n\t\t * or makes a network request if the data is stale or missing.\n\t\t *\n\t\t * **When to use fetch():**\n\t\t * - When you explicitly want to check data freshness\n\t\t * - For user-triggered refresh actions\n\t\t * - When you need the most up-to-date data\n\t\t *\n\t\t * **For preloaders, use ensure() instead** - it's more efficient for initial data loading.\n\t\t *\n\t\t * @returns Promise that resolves with a Result containing either the data or an error\n\t\t *\n\t\t * @example\n\t\t * // Good for user-triggered refresh\n\t\t * const { data, error } = await userQuery.fetch();\n\t\t * if (error) {\n\t\t * console.error('Failed to load user:', error);\n\t\t * }\n\t\t */\n\t\tasync function fetch(): Promise<Result<TQueryData, TError>> {\n\t\t\ttry {\n\t\t\t\treturn Ok(\n\t\t\t\t\tawait queryClient.fetchQuery<\n\t\t\t\t\t\tTQueryFnData,\n\t\t\t\t\t\tTError,\n\t\t\t\t\t\tTQueryData,\n\t\t\t\t\t\tTQueryKey\n\t\t\t\t\t>({\n\t\t\t\t\t\tqueryKey: newOptions.queryKey,\n\t\t\t\t\t\tqueryFn: newOptions.queryFn,\n\t\t\t\t\t}),\n\t\t\t\t);\n\t\t\t} catch (error) {\n\t\t\t\treturn Err(error as TError);\n\t\t\t}\n\t\t}\n\n\t\t/**\n\t\t * Ensures data is available for this query using queryClient.ensureQueryData().\n\t\t *\n\t\t * This method PRIORITIZES cached data and only calls fetchQuery internally if no cached\n\t\t * data exists. It wraps TanStack Query's ensureQueryData method, which is perfect for\n\t\t * guaranteeing data availability with minimal network requests.\n\t\t *\n\t\t * **This is the RECOMMENDED method for preloaders** because:\n\t\t * - It returns cached data immediately if available\n\t\t * - It updates the query client cache properly\n\t\t * - It minimizes network requests during navigation\n\t\t * - It ensures components have data ready when they mount\n\t\t *\n\t\t * **When to use ensure():**\n\t\t * - Route preloaders and data loading functions\n\t\t * - Initial component data requirements\n\t\t * - When cached data is acceptable for immediate display\n\t\t *\n\t\t * This is also the default behavior when calling the query directly.\n\t\t *\n\t\t * @returns Promise that resolves with a Result containing either the data or an error\n\t\t *\n\t\t * @example\n\t\t * // Perfect for preloaders\n\t\t * export const load = async () => {\n\t\t * const { data, error } = await userQuery.ensure();\n\t\t * // Or simply: await userQuery();\n\t\t * if (error) {\n\t\t * throw error;\n\t\t * }\n\t\t * return { user: data };\n\t\t * };\n\t\t */\n\t\tasync function ensure(): Promise<Result<TQueryData, TError>> {\n\t\t\ttry {\n\t\t\t\treturn Ok(\n\t\t\t\t\tawait queryClient.ensureQueryData<\n\t\t\t\t\t\tTQueryFnData,\n\t\t\t\t\t\tTError,\n\t\t\t\t\t\tTQueryData,\n\t\t\t\t\t\tTQueryKey\n\t\t\t\t\t>({\n\t\t\t\t\t\tqueryKey: newOptions.queryKey,\n\t\t\t\t\t\tqueryFn: newOptions.queryFn,\n\t\t\t\t\t}),\n\t\t\t\t);\n\t\t\t} catch (error) {\n\t\t\t\treturn Err(error as TError);\n\t\t\t}\n\t\t}\n\n\t\t// Create a callable function that defaults to ensure() behavior\n\t\t// and attach options, fetch, and ensure as properties\n\t\treturn Object.assign(ensure, {\n\t\t\toptions: newOptions,\n\t\t\tfetch,\n\t\t\tensure,\n\t\t});\n\t};\n\n\t/**\n\t * Creates a mutation definition for operations that modify data (create, update, delete).\n\t *\n\t * This factory function is the mutation counterpart to defineQuery. It provides a clean way to\n\t * wrap service functions that perform side effects, while maintaining the same dual interface\n\t * pattern for maximum flexibility.\n\t *\n\t * The returned mutation definition is **directly callable**, which executes the mutation\n\t * and returns a Result. This is equivalent to calling `.execute()`.\n\t *\n\t * ## Why use defineMutation?\n\t *\n\t * 1. **Callable**: Call directly like `createUser({ name: 'John' })` for imperative execution\n\t * 2. **Dual Interface**: Also provides reactive (`.options`) and explicit imperative (`.execute()`) APIs\n\t * 3. **Consistent Error Handling**: Service functions return `Result<T, E>` types, ensuring\n\t * errors are handled consistently throughout the app\n\t * 4. **Cache Management**: Mutations often update the cache after success (see examples)\n\t *\n\t * @template TData - The type of data returned by the mutation\n\t * @template TError - The type of error that can be thrown\n\t * @template TVariables - The type of variables passed to the mutation\n\t * @template TContext - The type of context data for optimistic updates\n\t *\n\t * @param options - Mutation configuration object\n\t * @param options.mutationKey - Unique key for this mutation (used for tracking in-flight state)\n\t * @param options.resultMutationFn - Function that performs the mutation and returns a Result type\n\t * @param options.* - Any other TanStack Mutation options (onSuccess, onError, etc.)\n\t *\n\t * @returns Callable mutation definition with:\n\t * - `(variables)` (callable): Same as `execute()` - directly executes the mutation\n\t * - `.options`: Config for use with useMutation() or createMutation()\n\t * - `.execute(variables)`: Directly executes the mutation and returns a Result\n\t *\n\t * @example\n\t * ```typescript\n\t * // Step 1: Define your mutation with cache updates\n\t * const createRecording = defineMutation({\n\t * mutationKey: ['recordings', 'create'],\n\t * resultMutationFn: async (recording: Recording) => {\n\t * // Call the service\n\t * const result = await services.db.createRecording(recording);\n\t * if (result.error) return Err(result.error);\n\t *\n\t * // Update cache on success\n\t * queryClient.setQueryData(['recordings'], (old) =>\n\t * [...(old || []), recording]\n\t * );\n\t *\n\t * return Ok(result.data);\n\t * }\n\t * });\n\t *\n\t * // Step 2a: Use reactively in a component\n\t * const mutation = createMutation(createRecording.options);\n\t * // Call with: $mutation.mutate(recordingData)\n\t *\n\t * // Step 2b: Call directly in an action (recommended)\n\t * async function saveRecording(data: Recording) {\n\t * const { error } = await createRecording(data); // Same as createRecording.execute(data)\n\t * if (error) {\n\t * notify.error({ title: 'Failed to save', description: error.message });\n\t * } else {\n\t * notify.success({ title: 'Recording saved!' });\n\t * }\n\t * }\n\t * ```\n\t *\n\t * @tip Calling directly is especially useful for:\n\t * - Event handlers that need to await the result\n\t * - Sequential operations that depend on each other\n\t * - Non-component code that needs to trigger mutations\n\t */\n\tconst defineMutation = <TData, TError, TVariables = void, TContext = unknown>(\n\t\toptions: DefineMutationInput<TData, TError, TVariables, TContext>,\n\t): DefineMutationOutput<TData, TError, TVariables, TContext> => {\n\t\tconst newOptions = {\n\t\t\t...options,\n\t\t\tmutationFn: async (variables: TVariables) => {\n\t\t\t\treturn resolve(await options.resultMutationFn(variables));\n\t\t\t},\n\t\t} satisfies MutationOptions<TData, TError, TVariables, TContext>;\n\n\t\t/**\n\t\t * Executes the mutation imperatively and returns a Result.\n\t\t *\n\t\t * This is the recommended way to trigger mutations from:\n\t\t * - Button click handlers\n\t\t * - Form submissions\n\t\t * - Keyboard shortcuts\n\t\t * - Any non-component code\n\t\t *\n\t\t * The method automatically wraps the result in a Result type, so you always\n\t\t * get back `{ data, error }` for consistent error handling.\n\t\t *\n\t\t * This is also the default behavior when calling the mutation directly.\n\t\t *\n\t\t * @param variables - The variables to pass to the mutation function\n\t\t * @returns Promise that resolves with a Result containing either the data or an error\n\t\t *\n\t\t * @example\n\t\t * // In an event handler\n\t\t * async function handleSubmit(formData: FormData) {\n\t\t * const { data, error } = await createUser.execute(formData);\n\t\t * // Or simply: await createUser(formData);\n\t\t * if (error) {\n\t\t * notify.error({ title: 'Failed to create user', description: error.message });\n\t\t * return;\n\t\t * }\n\t\t * goto(`/users/${data.id}`);\n\t\t * }\n\t\t */\n\t\tasync function execute(variables: TVariables) {\n\t\t\ttry {\n\t\t\t\treturn Ok(await runMutation(queryClient, newOptions, variables));\n\t\t\t} catch (error) {\n\t\t\t\treturn Err(error as TError);\n\t\t\t}\n\t\t}\n\n\t\t// Create a callable function that executes the mutation\n\t\t// and attach options and execute as properties\n\t\treturn Object.assign(execute, {\n\t\t\toptions: newOptions,\n\t\t\texecute,\n\t\t});\n\t};\n\n\treturn {\n\t\tdefineQuery,\n\t\tdefineMutation,\n\t};\n}\n\n/**\n * Internal helper that executes a mutation directly using the query client's mutation cache.\n *\n * This is what powers the callable behavior and `.execute()` method on mutations.\n * It bypasses the reactive mutation hooks and runs the mutation imperatively,\n * which is perfect for event handlers and other imperative code.\n *\n * @internal\n * @template TData - The type of data returned by the mutation\n * @template TError - The type of error that can be thrown\n * @template TVariables - The type of variables passed to the mutation\n * @template TContext - The type of context data\n * @param queryClient - The query client instance to use\n * @param options - The mutation options including mutationFn and mutationKey\n * @param variables - The variables to pass to the mutation function\n * @returns Promise that resolves with the mutation result\n */\nfunction runMutation<TData, TError, TVariables, TContext>(\n\tqueryClient: QueryClient,\n\toptions: MutationOptions<TData, TError, TVariables, TContext>,\n\tvariables: TVariables,\n) {\n\tconst mutation = queryClient.getMutationCache().build(queryClient, options);\n\treturn mutation.execute(variables);\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA+LA,SAAgB,qBAAqBA,aAA0B;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAiE9D,MAAM,cAAc,CAOnBC,YAO2E;EAC3E,MAAM,aAAa;GAClB,GAAG;GACH,SAAS,OAAO,YAAY;IAC3B,IAAI,SAAS,QAAQ,cAAc,QAAQ;AAC3C,QAAI,kBAAkB,QAAS,UAAS,MAAM;AAC9C,WAAO,QAAQ,OAAO;GACtB;EACD;;;;;;;;;;;;;;;;;;;;;;;;EA+BD,eAAe,QAA6C;AAC3D,OAAI;AACH,WAAO,GACN,MAAM,YAAY,WAKhB;KACD,UAAU,WAAW;KACrB,SAAS,WAAW;IACpB,EAAC,CACF;GACD,SAAQ,OAAO;AACf,WAAO,IAAI,MAAgB;GAC3B;EACD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAmCD,eAAe,SAA8C;AAC5D,OAAI;AACH,WAAO,GACN,MAAM,YAAY,gBAKhB;KACD,UAAU,WAAW;KACrB,SAAS,WAAW;IACpB,EAAC,CACF;GACD,SAAQ,OAAO;AACf,WAAO,IAAI,MAAgB;GAC3B;EACD;AAID,SAAO,OAAO,OAAO,QAAQ;GAC5B,SAAS;GACT;GACA;EACA,EAAC;CACF;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA0ED,MAAM,iBAAiB,CACtBC,YAC+D;EAC/D,MAAM,aAAa;GAClB,GAAG;GACH,YAAY,OAAOC,cAA0B;AAC5C,WAAO,QAAQ,MAAM,QAAQ,iBAAiB,UAAU,CAAC;GACzD;EACD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EA+BD,eAAe,QAAQA,WAAuB;AAC7C,OAAI;AACH,WAAO,GAAG,MAAM,YAAY,aAAa,YAAY,UAAU,CAAC;GAChE,SAAQ,OAAO;AACf,WAAO,IAAI,MAAgB;GAC3B;EACD;AAID,SAAO,OAAO,OAAO,SAAS;GAC7B,SAAS;GACT;EACA,EAAC;CACF;AAED,QAAO;EACN;EACA;CACA;AACD;;;;;;;;;;;;;;;;;;AAmBD,SAAS,YACRH,aACAI,SACAD,WACC;CACD,MAAM,WAAW,YAAY,kBAAkB,CAAC,MAAM,aAAa,QAAQ;AAC3E,QAAO,SAAS,QAAQ,UAAU;AAClC"}
|
|
1
|
+
{"version":3,"file":"index.js","names":["queryClient: QueryClient","options: DefineQueryInput<\n\t\t\tTQueryFnData,\n\t\t\tTError,\n\t\t\tTData,\n\t\t\tTQueryData,\n\t\t\tTQueryKey\n\t\t>","options: DefineMutationInput<TData, TError, TVariables, TContext>","variables: TVariables","options: MutationOptions<TData, TError, TVariables, TContext>"],"sources":["../../src/query/utils.ts"],"sourcesContent":["import type {\n\tDefaultError,\n\tMutationFunction,\n\tMutationKey,\n\tMutationOptions,\n\tQueryClient,\n\tQueryFunction,\n\tQueryKey,\n\tQueryObserverOptions,\n} from \"@tanstack/query-core\";\nimport { Err, Ok, type Result, resolve } from \"../result/index.js\";\n\n/**\n * Input options for defining a query.\n *\n * Extends TanStack Query's QueryObserverOptions but expects queryFn to return a Result type.\n * This type represents the configuration for creating a query definition with both\n * reactive and imperative interfaces for data fetching.\n *\n * @template TQueryFnData - The type of data returned by the query function\n * @template TError - The type of error that can be thrown\n * @template TData - The type of data returned by the query (after select transform)\n * @template TQueryKey - The type of the query key\n */\nexport type DefineQueryInput<\n\tTQueryFnData = unknown,\n\tTError = DefaultError,\n\tTData = TQueryFnData,\n\tTQueryData = TQueryFnData,\n\tTQueryKey extends QueryKey = QueryKey,\n> = Omit<\n\tQueryObserverOptions<TQueryFnData, TError, TData, TQueryData, TQueryKey>,\n\t\"queryFn\"\n> & {\n\tqueryKey: TQueryKey;\n\tqueryFn: QueryFunction<Result<TQueryFnData, TError>, TQueryKey>;\n};\n\n/**\n * Output of defineQuery function.\n *\n * The query definition is directly callable and defaults to `ensure()` behavior,\n * which is recommended for most imperative use cases like preloaders.\n *\n * Provides both reactive and imperative interfaces for data fetching:\n * - `()` (callable): Same as `ensure()` - returns cached data if available, fetches if not\n * - `options`: Returns config for use with useQuery() or createQuery()\n * - `fetch()`: Always attempts to fetch data (from cache if fresh, network if stale)\n * - `ensure()`: Guarantees data availability, preferring cached data (recommended for preloaders)\n *\n * @template TQueryFnData - The type of data returned by the query function\n * @template TError - The type of error that can be thrown\n * @template TData - The type of data returned by the query (after select transform)\n * @template TQueryKey - The type of the query key\n *\n * @example\n * ```typescript\n * const userQuery = defineQuery({...});\n *\n * // Directly callable (same as .ensure())\n * const { data, error } = await userQuery();\n *\n * // Or use explicit methods\n * const { data, error } = await userQuery.ensure();\n * const { data, error } = await userQuery.fetch();\n *\n * // For reactive usage\n * const query = createQuery(userQuery.options);\n * ```\n */\nexport type DefineQueryOutput<\n\tTQueryFnData = unknown,\n\tTError = DefaultError,\n\tTData = TQueryFnData,\n\tTQueryData = TQueryFnData,\n\tTQueryKey extends QueryKey = QueryKey,\n> = (() => Promise<Result<TQueryData, TError>>) & {\n\toptions: QueryObserverOptions<\n\t\tTQueryFnData,\n\t\tTError,\n\t\tTData,\n\t\tTQueryData,\n\t\tTQueryKey\n\t>;\n\tfetch: () => Promise<Result<TQueryData, TError>>;\n\tensure: () => Promise<Result<TQueryData, TError>>;\n};\n\n/**\n * Input options for defining a mutation.\n *\n * Extends TanStack Query's MutationOptions but expects mutationFn to return a Result type.\n * This type represents the configuration for creating a mutation definition with both\n * reactive and imperative interfaces for data mutations.\n *\n * @template TData - The type of data returned by the mutation\n * @template TError - The type of error that can be thrown\n * @template TVariables - The type of variables passed to the mutation\n * @template TContext - The type of context data for optimistic updates\n */\nexport type DefineMutationInput<\n\tTData,\n\tTError,\n\tTVariables = void,\n\tTContext = unknown,\n> = Omit<MutationOptions<TData, TError, TVariables, TContext>, \"mutationFn\"> & {\n\tmutationKey: MutationKey;\n\tmutationFn: MutationFunction<Result<TData, TError>, TVariables>;\n};\n\n/**\n * Output of defineMutation function.\n *\n * The mutation definition is directly callable, which executes the mutation\n * and returns a Result. This is equivalent to calling `.execute()`.\n *\n * Provides both reactive and imperative interfaces for data mutations:\n * - `(variables)` (callable): Same as `execute()` - directly executes the mutation\n * - `options`: Returns config for use with useMutation() or createMutation()\n * - `execute(variables)`: Directly executes the mutation and returns a Result\n *\n * @template TData - The type of data returned by the mutation\n * @template TError - The type of error that can be thrown\n * @template TVariables - The type of variables passed to the mutation\n * @template TContext - The type of context data for optimistic updates\n *\n * @example\n * ```typescript\n * const createUser = defineMutation({...});\n *\n * // Directly callable (same as .execute())\n * const { data, error } = await createUser({ name: 'John' });\n *\n * // Or use explicit method\n * const { data, error } = await createUser.execute({ name: 'John' });\n *\n * // For reactive usage\n * const mutation = createMutation(createUser.options);\n * ```\n */\nexport type DefineMutationOutput<\n\tTData,\n\tTError,\n\tTVariables = void,\n\tTContext = unknown,\n> = ((variables: TVariables) => Promise<Result<TData, TError>>) & {\n\toptions: MutationOptions<TData, TError, TVariables, TContext>;\n\texecute: (variables: TVariables) => Promise<Result<TData, TError>>;\n};\n\n/**\n * Creates factory functions for defining queries and mutations bound to a specific QueryClient.\n *\n * This factory pattern allows you to create isolated query/mutation definitions that are\n * bound to a specific QueryClient instance, enabling:\n * - Multiple query clients in the same application\n * - Testing with isolated query clients\n * - Framework-agnostic query definitions\n * - Proper separation of concerns between query logic and client instances\n *\n * The returned functions handle Result types automatically, unwrapping them for TanStack Query\n * while maintaining type safety throughout your application.\n *\n * @param queryClient - The QueryClient instance to bind the factories to\n * @returns An object containing defineQuery and defineMutation functions bound to the provided client\n *\n * @example\n * ```typescript\n * // Create your query client\n * const queryClient = new QueryClient({\n * defaultOptions: {\n * queries: { staleTime: 5 * 60 * 1000 }\n * }\n * });\n *\n * // Create the factory functions\n * const { defineQuery, defineMutation } = createQueryFactories(queryClient);\n *\n * // Now use defineQuery and defineMutation as before\n * const userQuery = defineQuery({\n * queryKey: ['user', userId],\n * queryFn: () => services.getUser(userId)\n * });\n *\n * // Use in components\n * const query = createQuery(userQuery.options);\n *\n * // Or imperatively\n * const { data, error } = await userQuery.fetch();\n * ```\n */\nexport function createQueryFactories(queryClient: QueryClient) {\n\t/**\n\t * Creates a query definition that bridges the gap between pure service functions and reactive UI components.\n\t *\n\t * This factory function is the cornerstone of our data fetching architecture. It wraps service calls\n\t * with TanStack Query superpowers while maintaining type safety through Result types.\n\t *\n\t * The returned query definition is **directly callable** and defaults to `ensure()` behavior,\n\t * which is recommended for most imperative use cases like preloaders.\n\t *\n\t * ## Why use defineQuery?\n\t *\n\t * 1. **Callable**: Call directly like `userQuery()` for imperative data fetching\n\t * 2. **Dual Interface**: Also provides reactive (`.options`) and explicit imperative (`.fetch()`, `.ensure()`) APIs\n\t * 3. **Automatic Error Handling**: Service functions return `Result<T, E>` types which are automatically\n\t * unwrapped by TanStack Query, giving you proper error states in your components\n\t * 4. **Type Safety**: Full TypeScript support with proper inference for data and error types\n\t * 5. **Consistency**: Every query in the app follows the same pattern, making it easy to understand\n\t *\n\t * @template TQueryFnData - The type of data returned by the query function\n\t * @template TError - The type of error that can be thrown\n\t * @template TData - The type of data returned by the query (after select transform)\n\t * @template TQueryKey - The type of the query key\n\t *\n\t * @param options - Query configuration object\n\t * @param options.queryKey - Unique key for this query (used for caching and refetching)\n\t * @param options.queryFn - Function that fetches data and returns a Result type\n\t * @param options.* - Any other TanStack Query options (staleTime, refetchInterval, etc.)\n\t *\n\t * @returns Callable query definition with:\n\t * - `()` (callable): Same as `ensure()` - returns cached data if available, fetches if not\n\t * - `.options`: Config for use with useQuery() or createQuery()\n\t * - `.fetch()`: Always attempts to fetch (from cache if fresh, network if stale)\n\t * - `.ensure()`: Guarantees data availability, preferring cached data (recommended for preloaders)\n\t *\n\t * @example\n\t * ```typescript\n\t * // Step 1: Define your query in the query layer\n\t * const userQuery = defineQuery({\n\t * queryKey: ['users', userId],\n\t * queryFn: () => services.getUser(userId), // Returns Result<User, ApiError>\n\t * staleTime: 5 * 60 * 1000, // Consider data fresh for 5 minutes\n\t * });\n\t *\n\t * // Step 2a: Use reactively in a Svelte component\n\t * const query = createQuery(userQuery.options);\n\t * // $query.data is User | undefined\n\t * // $query.error is ApiError | null\n\t *\n\t * // Step 2b: Call directly in preloaders (recommended)\n\t * export const load = async () => {\n\t * const { data, error } = await userQuery(); // Same as userQuery.ensure()\n\t * if (error) throw error;\n\t * return { user: data };\n\t * };\n\t *\n\t * // Step 2c: Use explicit methods when needed\n\t * async function refreshUser() {\n\t * const { data, error } = await userQuery.fetch(); // Force fresh fetch\n\t * if (error) {\n\t * console.error('Failed to fetch user:', error);\n\t * }\n\t * }\n\t * ```\n\t */\n\tconst defineQuery = <\n\t\tTQueryFnData = unknown,\n\t\tTError = DefaultError,\n\t\tTData = TQueryFnData,\n\t\tTQueryData = TQueryFnData,\n\t\tTQueryKey extends QueryKey = QueryKey,\n\t>(\n\t\toptions: DefineQueryInput<\n\t\t\tTQueryFnData,\n\t\t\tTError,\n\t\t\tTData,\n\t\t\tTQueryData,\n\t\t\tTQueryKey\n\t\t>,\n\t): DefineQueryOutput<TQueryFnData, TError, TData, TQueryData, TQueryKey> => {\n\t\tconst newOptions = {\n\t\t\t...options,\n\t\t\tqueryFn: async (context) => {\n\t\t\t\tlet result = options.queryFn(context);\n\t\t\t\tif (result instanceof Promise) result = await result;\n\t\t\t\treturn resolve(result);\n\t\t\t},\n\t\t} satisfies QueryObserverOptions<\n\t\t\tTQueryFnData,\n\t\t\tTError,\n\t\t\tTData,\n\t\t\tTQueryData,\n\t\t\tTQueryKey\n\t\t>;\n\n\t\t/**\n\t\t * Fetches data for this query using queryClient.fetchQuery().\n\t\t *\n\t\t * This method ALWAYS evaluates freshness and will refetch if data is stale.\n\t\t * It wraps TanStack Query's fetchQuery method, which returns cached data if fresh\n\t\t * or makes a network request if the data is stale or missing.\n\t\t *\n\t\t * **When to use fetch():**\n\t\t * - When you explicitly want to check data freshness\n\t\t * - For user-triggered refresh actions\n\t\t * - When you need the most up-to-date data\n\t\t *\n\t\t * **For preloaders, use ensure() instead** - it's more efficient for initial data loading.\n\t\t *\n\t\t * @returns Promise that resolves with a Result containing either the data or an error\n\t\t *\n\t\t * @example\n\t\t * // Good for user-triggered refresh\n\t\t * const { data, error } = await userQuery.fetch();\n\t\t * if (error) {\n\t\t * console.error('Failed to load user:', error);\n\t\t * }\n\t\t */\n\t\tasync function fetch(): Promise<Result<TQueryData, TError>> {\n\t\t\ttry {\n\t\t\t\treturn Ok(\n\t\t\t\t\tawait queryClient.fetchQuery<\n\t\t\t\t\t\tTQueryFnData,\n\t\t\t\t\t\tTError,\n\t\t\t\t\t\tTQueryData,\n\t\t\t\t\t\tTQueryKey\n\t\t\t\t\t>({\n\t\t\t\t\t\tqueryKey: newOptions.queryKey,\n\t\t\t\t\t\tqueryFn: newOptions.queryFn,\n\t\t\t\t\t}),\n\t\t\t\t);\n\t\t\t} catch (error) {\n\t\t\t\treturn Err(error as TError);\n\t\t\t}\n\t\t}\n\n\t\t/**\n\t\t * Ensures data is available for this query using queryClient.ensureQueryData().\n\t\t *\n\t\t * This method PRIORITIZES cached data and only calls fetchQuery internally if no cached\n\t\t * data exists. It wraps TanStack Query's ensureQueryData method, which is perfect for\n\t\t * guaranteeing data availability with minimal network requests.\n\t\t *\n\t\t * **This is the RECOMMENDED method for preloaders** because:\n\t\t * - It returns cached data immediately if available\n\t\t * - It updates the query client cache properly\n\t\t * - It minimizes network requests during navigation\n\t\t * - It ensures components have data ready when they mount\n\t\t *\n\t\t * **When to use ensure():**\n\t\t * - Route preloaders and data loading functions\n\t\t * - Initial component data requirements\n\t\t * - When cached data is acceptable for immediate display\n\t\t *\n\t\t * This is also the default behavior when calling the query directly.\n\t\t *\n\t\t * @returns Promise that resolves with a Result containing either the data or an error\n\t\t *\n\t\t * @example\n\t\t * // Perfect for preloaders\n\t\t * export const load = async () => {\n\t\t * const { data, error } = await userQuery.ensure();\n\t\t * // Or simply: await userQuery();\n\t\t * if (error) {\n\t\t * throw error;\n\t\t * }\n\t\t * return { user: data };\n\t\t * };\n\t\t */\n\t\tasync function ensure(): Promise<Result<TQueryData, TError>> {\n\t\t\ttry {\n\t\t\t\treturn Ok(\n\t\t\t\t\tawait queryClient.ensureQueryData<\n\t\t\t\t\t\tTQueryFnData,\n\t\t\t\t\t\tTError,\n\t\t\t\t\t\tTQueryData,\n\t\t\t\t\t\tTQueryKey\n\t\t\t\t\t>({\n\t\t\t\t\t\tqueryKey: newOptions.queryKey,\n\t\t\t\t\t\tqueryFn: newOptions.queryFn,\n\t\t\t\t\t}),\n\t\t\t\t);\n\t\t\t} catch (error) {\n\t\t\t\treturn Err(error as TError);\n\t\t\t}\n\t\t}\n\n\t\t// Create a callable function that defaults to ensure() behavior\n\t\t// and attach options, fetch, and ensure as properties\n\t\treturn Object.assign(ensure, {\n\t\t\toptions: newOptions,\n\t\t\tfetch,\n\t\t\tensure,\n\t\t});\n\t};\n\n\t/**\n\t * Creates a mutation definition for operations that modify data (create, update, delete).\n\t *\n\t * This factory function is the mutation counterpart to defineQuery. It provides a clean way to\n\t * wrap service functions that perform side effects, while maintaining the same dual interface\n\t * pattern for maximum flexibility.\n\t *\n\t * The returned mutation definition is **directly callable**, which executes the mutation\n\t * and returns a Result. This is equivalent to calling `.execute()`.\n\t *\n\t * ## Why use defineMutation?\n\t *\n\t * 1. **Callable**: Call directly like `createUser({ name: 'John' })` for imperative execution\n\t * 2. **Dual Interface**: Also provides reactive (`.options`) and explicit imperative (`.execute()`) APIs\n\t * 3. **Consistent Error Handling**: Service functions return `Result<T, E>` types, ensuring\n\t * errors are handled consistently throughout the app\n\t * 4. **Cache Management**: Mutations often update the cache after success (see examples)\n\t *\n\t * @template TData - The type of data returned by the mutation\n\t * @template TError - The type of error that can be thrown\n\t * @template TVariables - The type of variables passed to the mutation\n\t * @template TContext - The type of context data for optimistic updates\n\t *\n\t * @param options - Mutation configuration object\n\t * @param options.mutationKey - Unique key for this mutation (used for tracking in-flight state)\n\t * @param options.mutationFn - Function that performs the mutation and returns a Result type\n\t * @param options.* - Any other TanStack Mutation options (onSuccess, onError, etc.)\n\t *\n\t * @returns Callable mutation definition with:\n\t * - `(variables)` (callable): Same as `execute()` - directly executes the mutation\n\t * - `.options`: Config for use with useMutation() or createMutation()\n\t * - `.execute(variables)`: Directly executes the mutation and returns a Result\n\t *\n\t * @example\n\t * ```typescript\n\t * // Step 1: Define your mutation with cache updates\n\t * const createRecording = defineMutation({\n\t * mutationKey: ['recordings', 'create'],\n\t * mutationFn: async (recording: Recording) => {\n\t * // Call the service\n\t * const result = await services.db.createRecording(recording);\n\t * if (result.error) return Err(result.error);\n\t *\n\t * // Update cache on success\n\t * queryClient.setQueryData(['recordings'], (old) =>\n\t * [...(old || []), recording]\n\t * );\n\t *\n\t * return Ok(result.data);\n\t * }\n\t * });\n\t *\n\t * // Step 2a: Use reactively in a component\n\t * const mutation = createMutation(createRecording.options);\n\t * // Call with: $mutation.mutate(recordingData)\n\t *\n\t * // Step 2b: Call directly in an action (recommended)\n\t * async function saveRecording(data: Recording) {\n\t * const { error } = await createRecording(data); // Same as createRecording.execute(data)\n\t * if (error) {\n\t * notify.error({ title: 'Failed to save', description: error.message });\n\t * } else {\n\t * notify.success({ title: 'Recording saved!' });\n\t * }\n\t * }\n\t * ```\n\t *\n\t * @tip Calling directly is especially useful for:\n\t * - Event handlers that need to await the result\n\t * - Sequential operations that depend on each other\n\t * - Non-component code that needs to trigger mutations\n\t */\n\tconst defineMutation = <TData, TError, TVariables = void, TContext = unknown>(\n\t\toptions: DefineMutationInput<TData, TError, TVariables, TContext>,\n\t): DefineMutationOutput<TData, TError, TVariables, TContext> => {\n\t\tconst newOptions = {\n\t\t\t...options,\n\t\t\tmutationFn: async (variables: TVariables) => {\n\t\t\t\treturn resolve(await options.mutationFn(variables));\n\t\t\t},\n\t\t} satisfies MutationOptions<TData, TError, TVariables, TContext>;\n\n\t\t/**\n\t\t * Executes the mutation imperatively and returns a Result.\n\t\t *\n\t\t * This is the recommended way to trigger mutations from:\n\t\t * - Button click handlers\n\t\t * - Form submissions\n\t\t * - Keyboard shortcuts\n\t\t * - Any non-component code\n\t\t *\n\t\t * The method automatically wraps the result in a Result type, so you always\n\t\t * get back `{ data, error }` for consistent error handling.\n\t\t *\n\t\t * This is also the default behavior when calling the mutation directly.\n\t\t *\n\t\t * @param variables - The variables to pass to the mutation function\n\t\t * @returns Promise that resolves with a Result containing either the data or an error\n\t\t *\n\t\t * @example\n\t\t * // In an event handler\n\t\t * async function handleSubmit(formData: FormData) {\n\t\t * const { data, error } = await createUser.execute(formData);\n\t\t * // Or simply: await createUser(formData);\n\t\t * if (error) {\n\t\t * notify.error({ title: 'Failed to create user', description: error.message });\n\t\t * return;\n\t\t * }\n\t\t * goto(`/users/${data.id}`);\n\t\t * }\n\t\t */\n\t\tasync function execute(variables: TVariables) {\n\t\t\ttry {\n\t\t\t\treturn Ok(await runMutation(queryClient, newOptions, variables));\n\t\t\t} catch (error) {\n\t\t\t\treturn Err(error as TError);\n\t\t\t}\n\t\t}\n\n\t\t// Create a callable function that executes the mutation\n\t\t// and attach options and execute as properties\n\t\treturn Object.assign(execute, {\n\t\t\toptions: newOptions,\n\t\t\texecute,\n\t\t});\n\t};\n\n\treturn {\n\t\tdefineQuery,\n\t\tdefineMutation,\n\t};\n}\n\n/**\n * Internal helper that executes a mutation directly using the query client's mutation cache.\n *\n * This is what powers the callable behavior and `.execute()` method on mutations.\n * It bypasses the reactive mutation hooks and runs the mutation imperatively,\n * which is perfect for event handlers and other imperative code.\n *\n * @internal\n * @template TData - The type of data returned by the mutation\n * @template TError - The type of error that can be thrown\n * @template TVariables - The type of variables passed to the mutation\n * @template TContext - The type of context data\n * @param queryClient - The query client instance to use\n * @param options - The mutation options including mutationFn and mutationKey\n * @param variables - The variables to pass to the mutation function\n * @returns Promise that resolves with the mutation result\n */\nfunction runMutation<TData, TError, TVariables, TContext>(\n\tqueryClient: QueryClient,\n\toptions: MutationOptions<TData, TError, TVariables, TContext>,\n\tvariables: TVariables,\n) {\n\tconst mutation = queryClient.getMutationCache().build(queryClient, options);\n\treturn mutation.execute(variables);\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA+LA,SAAgB,qBAAqBA,aAA0B;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAiE9D,MAAM,cAAc,CAOnBC,YAO2E;EAC3E,MAAM,aAAa;GAClB,GAAG;GACH,SAAS,OAAO,YAAY;IAC3B,IAAI,SAAS,QAAQ,QAAQ,QAAQ;AACrC,QAAI,kBAAkB,QAAS,UAAS,MAAM;AAC9C,WAAO,QAAQ,OAAO;GACtB;EACD;;;;;;;;;;;;;;;;;;;;;;;;EA+BD,eAAe,QAA6C;AAC3D,OAAI;AACH,WAAO,GACN,MAAM,YAAY,WAKhB;KACD,UAAU,WAAW;KACrB,SAAS,WAAW;IACpB,EAAC,CACF;GACD,SAAQ,OAAO;AACf,WAAO,IAAI,MAAgB;GAC3B;EACD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAmCD,eAAe,SAA8C;AAC5D,OAAI;AACH,WAAO,GACN,MAAM,YAAY,gBAKhB;KACD,UAAU,WAAW;KACrB,SAAS,WAAW;IACpB,EAAC,CACF;GACD,SAAQ,OAAO;AACf,WAAO,IAAI,MAAgB;GAC3B;EACD;AAID,SAAO,OAAO,OAAO,QAAQ;GAC5B,SAAS;GACT;GACA;EACA,EAAC;CACF;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA0ED,MAAM,iBAAiB,CACtBC,YAC+D;EAC/D,MAAM,aAAa;GAClB,GAAG;GACH,YAAY,OAAOC,cAA0B;AAC5C,WAAO,QAAQ,MAAM,QAAQ,WAAW,UAAU,CAAC;GACnD;EACD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EA+BD,eAAe,QAAQA,WAAuB;AAC7C,OAAI;AACH,WAAO,GAAG,MAAM,YAAY,aAAa,YAAY,UAAU,CAAC;GAChE,SAAQ,OAAO;AACf,WAAO,IAAI,MAAgB;GAC3B;EACD;AAID,SAAO,OAAO,OAAO,SAAS;GAC7B,SAAS;GACT;EACA,EAAC;CACF;AAED,QAAO;EACN;EACA;CACA;AACD;;;;;;;;;;;;;;;;;;AAmBD,SAAS,YACRH,aACAI,SACAD,WACC;CACD,MAAM,WAAW,YAAY,kBAAkB,CAAC,MAAM,aAAa,QAAQ;AAC3E,QAAO,SAAS,QAAQ,UAAU;AAClC"}
|