Skip to content

Commit 8e16f7e

Browse files
committed
chore: most of NextSteps - done. Next is better error narrowing
1 parent 57e2abd commit 8e16f7e

File tree

5 files changed

+897
-61
lines changed

5 files changed

+897
-61
lines changed

src-v4/NEXT_STEPS.md

Lines changed: 25 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,15 +4,19 @@
44

55
Updated to specify deep cloning for context merging (not just spreading).
66

7-
### 1. Add `.callable(context?: TContext)` Method
7+
DONE
8+
### 1. Add `.callable(context?: TContext)` Method
9+
DONE
810
- **Goal**: Separate procedure definition from execution; `.handler` defines the handler and returns the builder instance, while `.callable` returns the actual callable procedure with optional context.
911
- **Implementation**:
1012
- Add `.callable<TContext>(context?: TContext)` method to the builder.
1113
- It should finalize the builder and return a callable function (the "procedure") that accepts procedure args and executes the wrapped logic.
1214
- Ensure context is passed to the procedure for use in `options.context`.
1315
- This is the first step to establish the builder pattern clearly.
1416

17+
DONE
1518
### 2. Context Support with `.context<TInitialContext>(initialContext?)`
19+
DONE
1620
- **Goal**: Allow defining an initial context type and value for `options.context` via a method, which can be merged with context passed to `.callable`.
1721
- **Implementation**:
1822
- Add `.context<TInitialContext>(initialContext?: TInitialContext)` method to the builder, storing the type and initial value.
@@ -21,23 +25,29 @@ Updated to specify deep cloning for context merging (not just spreading).
2125
- Pass the merged context to `options.context` in the procedure.
2226
- Ensure type inference propagates to `ProcedureOptions`.
2327

24-
### 3. Options Parameter in Handlers
28+
DONE
29+
### 3. Options Parameter in Handlers
30+
DONE
2531
- **Goal**: Always pass `options: { errors: ErrorHelpers, context: Context }` as the first param to handlers, where `errors` are helpers that throw structured objects (not Errors).
2632
- **Implementation**:
2733
- Define `ProcedureOptions<TContext, TErrors>` as `{ errors: Record<keyof TErrors, (data: any) => { type: keyof TErrors, ...data }>, context: TContext }`.
2834
- Use `createErrorHelpers` to generate `errors` object; each helper validates against the schema and throws `{ type: key, ...validatedData }`.
2935
- Ensure `type` is inferred from the key and required in schemas (e.g., `type: z.literal('NOT_FOUND')`).
3036
- Pass `options` as the first arg to `fn(options, ...finalArgs)`.
3137

38+
DONE
3239
### 4. Support for Output Schema with `.output`
40+
DONE
3341
- **Goal**: Add `.output<T extends AnySchema>(schema: T)` to validate/transform handler return values.
3442
- **Implementation**:
3543
- Add `.output<T>(schema: T)` method, storing `TOutputSchema`.
3644
- In `wrapped`, after handler call, validate the result against `TOutputSchema` using `schema['~standard'].validate`.
3745
- If async, handle with `.then()`; on failure, treat as error (via `createResult`).
3846
- Update generics: `Procedure` should reflect output types.
3947

48+
DONE
4049
### 5. Handle Async Validation (schema.validate can return Promise)
50+
DONE
4151
- **Goal**: Support both sync and async validation at the type level, with runtime handling via `instanceof Promise` checks and `.then()` chains (no `async/await`).
4252
- **Implementation**:
4353
- Introduce `type IsPromise<T> = T extends Promise<any> ? true : false;`.
@@ -46,7 +56,9 @@ Updated to specify deep cloning for context merging (not just spreading).
4656
- If validation is async, the overall return becomes a Promise. Update return types to `ReturnType<TFn> | Promise<ReturnType<TFn>>`.
4757
- Handle errors in `.then()` chains by rejecting or wrapping in results.
4858

59+
DONE
4960
### 6. Add createResult Utility for Never-Throwing Functions
61+
DONE
5062
- **Goal**: Implement `createResult` to return `{ data, error, isTypedError }` and `[data, error, isTypedError]` patterns, ensuring handlers never throw by catching all errors.
5163
- **Implementation**:
5264
- Use the provided `createResult(data: any, error: any, isTypedError: boolean)` function, which returns an array with object properties.
@@ -55,7 +67,9 @@ Updated to specify deep cloning for context merging (not just spreading).
5567
- Support both sync and async contexts; if handler returns a Promise, resolve it before creating the result.
5668
- Ensure type safety: generics should reflect the result structure.
5769

70+
DONE
5871
### 7. Support Sync and Async Handlers
72+
DONE
5973
- **Goal**: Allow handlers to be sync or async, detected via generics, and handle with `instanceof Promise` and `.then()` chains (no `async/await`).
6074
- **Implementation**:
6175
- Update the `handler` method signature with generics for async detection (reference example: use something like `TIsAsync extends boolean = IsPromise<TReturn>` to infer async behavior).
@@ -64,7 +78,9 @@ Updated to specify deep cloning for context merging (not just spreading).
6478
- Ensure the final return is a Promise if `TIsAsync` is true.
6579
- Update types: `Procedure` should have a generic for async, affecting return types.
6680

81+
DONE
6782
### 8. Careful Argument Handling (Procedure Args vs. Handler Args)
83+
DONE
6884
- **Goal**: Clearly distinguish "procedure args" (inputs to the final callable) from "handler args" (passed to `.handler`), ensuring handler args are based on schema output after defaults, with proper sync/async generic handling.
6985
- **Implementation**:
7086
- **Procedure Args**: The inputs to the callable (e.g., `proc("Alice")`); validated against `TInputSchema`.
@@ -73,14 +89,21 @@ Updated to specify deep cloning for context merging (not just spreading).
7389
- Generics: Define `THandlerArgs` as the spread of output types; ensure `TIsAsync` propagates to avoid type mismatches.
7490
- Handle edge cases: empty args, single args, tuple defaults.
7591

92+
DONE
7693
### 9. Error Map Support with `.errors(errorMap)`
94+
DONE
7795
- **Goal**: Support `.errors<T extends Record<string, AnySchema>>(errorMap: T)` to define typed error schemas.
7896
- **Implementation**:
7997
- Add `.errors<T>(map: T)` method, storing the map.
8098
- Use `createErrorHelpers(map, isAsync)` to generate helpers; enforce schemas include `type: z.literal(key)`.
8199
- Update builder generics for `TErrors = T`.
82100
- Integrate with `options.errors`; ensure helpers throw validated objects.
83101

102+
TODO - BETTER ERROR NARROWING - when `.errors()` is called, switch to have internal typed UNKNOWN_ERROR
103+
TODO - BETTER ERROR NARROWING - when `.errors()` is called, switch to have internal typed UNKNOWN_ERROR
104+
TODO - BETTER ERROR NARROWING - when `.errors()` is called,
105+
switch to have internal typed UNKNOWN_ERROR instead of ZagoraError
106+
84107
### 10. Update createResult for isTypedError
85108
- **Goal**: Enhance `createResult` to include `isTypedError` for guarding typed errors, with potential for `isValidationError` later.
86109
- **Implementation**:

src-v4/ss-valibot.ts

Lines changed: 21 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,35 +1,37 @@
11
import * as v from "valibot";
2-
import { builder } from "./sscore";
2+
import { zagora } from "./sscore";
33

44
// ============================================================================
55
// TESTS
66
// ============================================================================
77

88
// Test 1: Tuple with defaults
9-
const proc1 = builder()
9+
const proc1 = zagora()
1010
.input(v.tuple([v.string(), v.optional(v.number(), 42)]))
11-
.handler((name, age) => ({
11+
.handler((options, name, age) => ({
1212
// passing!
1313
// name: string
1414
// age: number - must be, because it's defaulted to 42
1515
message: `${name} is ${age}`,
16-
}));
16+
}))
17+
.callable();
1718

1819
console.log("Test 1a:", proc1("Alice"));
1920
console.log("Test 1b:", proc1("Bob", 30));
2021

2122
// Test 1b: Tuple with default + optional
22-
const proc1b = builder()
23+
const proc1b = zagora()
2324
.input(
2425
v.tuple([v.string(), v.optional(v.number(), 42), v.optional(v.boolean())]),
2526
)
26-
.handler((name, age, verified) => ({
27+
.handler((options, name, age, verified) => ({
2728
// passing!
2829
// name: string
2930
// age: number - must be, because it's defaulted to 42
3031
// verified: boolean | undefined - because it's optional
3132
message: `${name} is ${age}, verified: ${verified ?? false}`,
32-
}));
33+
}))
34+
.callable();
3335

3436
// should pass because verified is optional
3537
// and because age is defaulted to 42
@@ -40,28 +42,30 @@ console.log("Test 1b2:", proc1b("Bob", 30));
4042
console.log("Test 1b3:", proc1b("Carol", 25, true));
4143

4244
// Test 2: Primitive input
43-
const proc2 = builder()
45+
const proc2 = zagora()
4446
.input(v.string())
45-
.handler((name) => {
47+
.handler((options, name) => {
4648
// passing!
4749
// name: string
4850
return `Hello ${name}!`;
49-
});
51+
})
52+
.callable();
5053

5154
console.log("Test 2:", proc2("World"));
5255

5356
// Test 3: Object input
54-
const proc3 = builder()
57+
const proc3 = zagora()
5558
.input(v.object({ x: v.number(), y: v.optional(v.number(), 2) }))
56-
.handler((input) => {
59+
.handler((options, input) => {
5760
// input: { x: number, y: number } - because y is defaulted to 2
5861
return input.x + input.y;
59-
});
62+
})
63+
.callable();
6064

6165
console.log("Test 3:", proc3({ x: 5, y: 3 }));
6266

6367
// Test 4: Object with defaults and optionals
64-
const proc4 = builder()
68+
const proc4 = zagora()
6569
.input(
6670
v.tuple([
6771
v.string(),
@@ -72,11 +76,12 @@ const proc4 = builder()
7276
}),
7377
]),
7478
)
75-
.handler((name, config) => ({
79+
.handler((options, name, config) => ({
7680
// passing!
7781
// config: { user: string, role: string, paid: boolean | undefined }
7882
result: `${name}: role=${config.role}, paid=${config.paid ?? false}`,
79-
}));
83+
}))
84+
.callable();
8085

8186
// SHOULD NOT REQUIRE `role` to be passed because it has a default value!
8287
console.log("Test 4a:", proc4("Alice", { user: "alice" }));

src-v4/ss-zod.ts

Lines changed: 70 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,33 +1,36 @@
11
import z from "zod";
2-
import { builder } from "./sscore";
2+
import { zagora } from "./sscore";
33

44
// ============================================================================
55
// TESTS
66
// ============================================================================
77

88
// Test 1: Tuple with defaults
9-
const proc1 = builder()
9+
const proc1 = zagora()
10+
.context<{ foo: string }>()
1011
.input(z.tuple([z.string(), z.number().default(42)]))
11-
.handler((name, age) => ({
12+
.handler((opts, name, age) => ({
1213
// passing!
1314
// name: string
1415
// age: number - must be, because it's defaulted to 42
1516
message: `${name} is ${age}`,
16-
}));
17+
}))
18+
.callable({ foo: "bar", sasa: 123 });
1719

1820
console.log("Test 1a:", proc1("Alice"));
1921
console.log("Test 1b:", proc1("Bob", 30));
2022

2123
// Test 1b: Tuple with default + optional
22-
const proc1b = builder()
24+
const proc1b = zagora()
2325
.input(z.tuple([z.string(), z.number().default(42), z.boolean().optional()]))
24-
.handler((name, age, verified) => ({
26+
.handler((options, name, age, verified) => ({
2527
// passing!
2628
// name: string
2729
// age: number - must be, because it's defaulted to 42
2830
// verified: boolean | undefined - because it's optional
2931
message: `${name} is ${age}, verified: ${verified ?? false}`,
30-
}));
32+
}))
33+
.callable();
3134

3235
// should pass because verified is optional
3336
// and because age is defaulted to 42
@@ -38,28 +41,34 @@ console.log("Test 1b2:", proc1b("Bob", 30));
3841
console.log("Test 1b3:", proc1b("Carol", 25, true));
3942

4043
// Test 2: Primitive input
41-
const proc2 = builder()
44+
const proc2 = zagora()
45+
.context<{ foo?: string; bar: string }>({ bar: "quxie" })
4246
.input(z.string())
43-
.handler((name) => {
47+
.handler((options, name) => {
48+
options;
4449
// passing!
4550
// name: string
4651
return `Hello ${name}!`;
47-
});
52+
})
53+
.callable();
4854

4955
console.log("Test 2:", proc2("World"));
5056

5157
// Test 3: Object input
52-
const proc3 = builder()
58+
const proc3 = zagora()
5359
.input(z.object({ x: z.number(), y: z.number().default(2) }))
54-
.handler((input) => {
60+
.handler((options, input) => {
61+
options;
62+
5563
// input: { x: number, y: number } - because y is defaulted to 2
5664
return input.x + input.y;
57-
});
65+
})
66+
.callable();
5867

5968
console.log("Test 3:", proc3({ x: 5, y: 3 }));
6069

6170
// Test 4: Object with defaults and optionals
62-
const proc4 = builder()
71+
const proc4 = zagora()
6372
.input(
6473
z.tuple([
6574
z.string(),
@@ -70,11 +79,14 @@ const proc4 = builder()
7079
}),
7180
]),
7281
)
73-
.handler((name, config) => ({
82+
.handler((options, name, config) => {
83+
options;
84+
7485
// passing!
7586
// config: { user: string, role: string, paid: boolean | undefined }
76-
result: `${name}: role=${config.role}, paid=${config.paid ?? false}`,
77-
}));
87+
return `${name}: role=${config.role}, paid=${config.paid ?? false}`;
88+
})
89+
.callable();
7890

7991
// SHOULD NOT REQUIRE `role` to be passed because it has a default value!
8092
console.log("Test 4a:", proc4("Alice", { user: "alice" }));
@@ -84,4 +96,45 @@ console.log(
8496
proc4("Carol", { user: "carol", role: "admin", paid: true }),
8597
);
8698

99+
const ping = zagora()
100+
.input(z.number())
101+
.output(z.object({ pong: z.number() }))
102+
.errors({
103+
NOT_FOUND: z.object({ type: z.literal("NOT_FOUND"), userId: z.string() }),
104+
AUTH_ERR: z.object({ type: z.literal("AUTH_ERR"), retryAfter: z.number() }),
105+
})
106+
.handler(({ errors }, id) => {
107+
if (id === 42) {
108+
throw errors.AUTH_ERR({ retryAfter: Date.now() + 1000 });
109+
}
110+
if (id <= 20) {
111+
throw errors.NOT_FOUND({ userId: "123" });
112+
}
113+
114+
return { pong: id + 1 };
115+
})
116+
.callable();
117+
118+
// Call synchronously, no try-catch needed
119+
const resPing = ping(42);
120+
console.log("Test 5 ping-pong:", resPing);
121+
122+
if (
123+
resPing.error &&
124+
resPing.isTypedError &&
125+
!(resPing.error instanceof Error)
126+
) {
127+
console.log(
128+
"foo::::",
129+
// Type checking now correctly infers resPing.error as PingCallableError
130+
// and then further narrows it based on the 'type' property.
131+
resPing.error.type === "AUTH_ERR" ? resPing.error.retryAfter : "sasa",
132+
"<<<",
133+
);
134+
} else if (resPing.error && resPing.error instanceof Error) {
135+
// This block handles cases where resPing has a generic error object (e.g., an Error instance)
136+
// which is not a specific typed error as defined in the .errors() method.
137+
console.log("unknown err:", resPing.error);
138+
}
139+
87140
console.log("\nAll tests passed!");

0 commit comments

Comments
 (0)