@shivaedev/effect-trpc 0.0.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/CHANGELOG.md +6 -0
- package/LICENSE +21 -0
- package/README.md +142 -0
- package/dist/adapter.d.ts +16 -0
- package/dist/adapter.d.ts.map +1 -0
- package/dist/adapter.js +12 -0
- package/dist/adapter.js.map +1 -0
- package/dist/errors.d.ts +11 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +11 -0
- package/dist/errors.js.map +1 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +4 -0
- package/dist/index.js.map +1 -0
- package/dist/internal/context-bridge.d.ts +7 -0
- package/dist/internal/context-bridge.d.ts.map +1 -0
- package/dist/internal/context-bridge.js +9 -0
- package/dist/internal/context-bridge.js.map +1 -0
- package/dist/internal/procedure-handler.d.ts +13 -0
- package/dist/internal/procedure-handler.d.ts.map +1 -0
- package/dist/internal/procedure-handler.js +12 -0
- package/dist/internal/procedure-handler.js.map +1 -0
- package/dist/internal/procedure-types.d.ts +9 -0
- package/dist/internal/procedure-types.d.ts.map +1 -0
- package/dist/internal/procedure-types.js +2 -0
- package/dist/internal/procedure-types.js.map +1 -0
- package/dist/internal/runtime.d.ts +16 -0
- package/dist/internal/runtime.d.ts.map +1 -0
- package/dist/internal/runtime.js +60 -0
- package/dist/internal/runtime.js.map +1 -0
- package/dist/internal/stack-trace.d.ts +2 -0
- package/dist/internal/stack-trace.d.ts.map +1 -0
- package/dist/internal/stack-trace.js +19 -0
- package/dist/internal/stack-trace.js.map +1 -0
- package/dist/internal/vitest-trpc.d.ts +18 -0
- package/dist/internal/vitest-trpc.d.ts.map +1 -0
- package/dist/internal/vitest-trpc.js +70 -0
- package/dist/internal/vitest-trpc.js.map +1 -0
- package/dist/procedure.d.ts +27 -0
- package/dist/procedure.d.ts.map +1 -0
- package/dist/procedure.js +34 -0
- package/dist/procedure.js.map +1 -0
- package/dist/request-services.d.ts +7 -0
- package/dist/request-services.d.ts.map +1 -0
- package/dist/request-services.js +4 -0
- package/dist/request-services.js.map +1 -0
- package/dist/testing/caller.d.ts +10 -0
- package/dist/testing/caller.d.ts.map +1 -0
- package/dist/testing/caller.js +41 -0
- package/dist/testing/caller.js.map +1 -0
- package/dist/testing/types.d.ts +31 -0
- package/dist/testing/types.d.ts.map +1 -0
- package/dist/testing/types.js +2 -0
- package/dist/testing/types.js.map +1 -0
- package/dist/testing/vitest.d.ts +4 -0
- package/dist/testing/vitest.d.ts.map +1 -0
- package/dist/testing/vitest.js +28 -0
- package/dist/testing/vitest.js.map +1 -0
- package/dist/testing.d.ts +4 -0
- package/dist/testing.d.ts.map +1 -0
- package/dist/testing.js +3 -0
- package/dist/testing.js.map +1 -0
- package/dist/types.d.ts +14 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -0
- package/package.json +73 -0
- package/src/adapter.ts +85 -0
- package/src/errors.ts +30 -0
- package/src/index.ts +28 -0
- package/src/internal/context-bridge.ts +20 -0
- package/src/internal/procedure-handler.ts +48 -0
- package/src/internal/procedure-types.ts +49 -0
- package/src/internal/runtime.ts +131 -0
- package/src/internal/stack-trace.ts +19 -0
- package/src/internal/vitest-trpc.ts +202 -0
- package/src/procedure.ts +191 -0
- package/src/request-services.ts +43 -0
- package/src/testing/caller.ts +80 -0
- package/src/testing/types.ts +81 -0
- package/src/testing/vitest.ts +64 -0
- package/src/testing.ts +13 -0
- package/src/types.ts +24 -0
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import { TRPCError } from "@trpc/server";
|
|
2
|
+
import {
|
|
3
|
+
Cause,
|
|
4
|
+
Context,
|
|
5
|
+
Effect,
|
|
6
|
+
Exit,
|
|
7
|
+
type ManagedRuntime,
|
|
8
|
+
Option,
|
|
9
|
+
Result,
|
|
10
|
+
} from "effect";
|
|
11
|
+
import type {
|
|
12
|
+
EffectTRPCErrorMapper,
|
|
13
|
+
EffectTRPCInstrument,
|
|
14
|
+
ProcedureInfo,
|
|
15
|
+
} from "../types.js";
|
|
16
|
+
import type { ContextBridge } from "./context-bridge.js";
|
|
17
|
+
|
|
18
|
+
interface RunEffectOptions {
|
|
19
|
+
readonly procedure: ProcedureInfo;
|
|
20
|
+
readonly signal?: AbortSignal;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface RuntimeBridge<Requirements> {
|
|
24
|
+
readonly runEffect: <Value>(
|
|
25
|
+
effect: Effect.Effect<Value, unknown, Requirements>,
|
|
26
|
+
options: RunEffectOptions,
|
|
27
|
+
) => Promise<Value>;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const internalError = (cause?: unknown): TRPCError =>
|
|
31
|
+
new TRPCError({
|
|
32
|
+
cause,
|
|
33
|
+
code: "INTERNAL_SERVER_ERROR",
|
|
34
|
+
message: "Internal server error",
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
const mapError = (
|
|
38
|
+
error: unknown,
|
|
39
|
+
origin: "defect" | "failure",
|
|
40
|
+
procedure: ProcedureInfo,
|
|
41
|
+
consumerMapper: EffectTRPCErrorMapper | undefined,
|
|
42
|
+
): TRPCError => {
|
|
43
|
+
if (error instanceof TRPCError) {
|
|
44
|
+
return error;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
try {
|
|
48
|
+
return (
|
|
49
|
+
consumerMapper?.(error, { ...procedure, origin }) ?? internalError(error)
|
|
50
|
+
);
|
|
51
|
+
} catch (mapperDefect) {
|
|
52
|
+
return internalError(mapperDefect);
|
|
53
|
+
}
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
export const makeRuntimeBridge = <Requirements, RuntimeError>(
|
|
57
|
+
runtime: ManagedRuntime.ManagedRuntime<Requirements, RuntimeError>,
|
|
58
|
+
contextBridge: ContextBridge,
|
|
59
|
+
options: {
|
|
60
|
+
readonly instrument?: EffectTRPCInstrument;
|
|
61
|
+
readonly mapError?: EffectTRPCErrorMapper;
|
|
62
|
+
},
|
|
63
|
+
): RuntimeBridge<Requirements> => ({
|
|
64
|
+
runEffect: async (effect, runOptions) => {
|
|
65
|
+
const traced = effect.pipe(
|
|
66
|
+
Effect.withSpan(
|
|
67
|
+
runOptions.procedure.path,
|
|
68
|
+
{
|
|
69
|
+
attributes: {
|
|
70
|
+
"rpc.method": runOptions.procedure.path,
|
|
71
|
+
"rpc.system": "trpc",
|
|
72
|
+
"trpc.type": runOptions.procedure.type,
|
|
73
|
+
},
|
|
74
|
+
},
|
|
75
|
+
{
|
|
76
|
+
captureStackTrace: runOptions.procedure.captureStackTrace,
|
|
77
|
+
},
|
|
78
|
+
),
|
|
79
|
+
);
|
|
80
|
+
const instrumented = Effect.suspend(() =>
|
|
81
|
+
options.instrument === undefined
|
|
82
|
+
? traced
|
|
83
|
+
: options.instrument(traced, runOptions.procedure),
|
|
84
|
+
);
|
|
85
|
+
const ambient = contextBridge.current();
|
|
86
|
+
const runnable =
|
|
87
|
+
ambient === undefined
|
|
88
|
+
? instrumented
|
|
89
|
+
: Effect.flatMap(runtime.contextEffect, (base) =>
|
|
90
|
+
Effect.provideContext(instrumented, Context.merge(base, ambient)),
|
|
91
|
+
);
|
|
92
|
+
const exit = await runtime.runPromiseExit(
|
|
93
|
+
runnable,
|
|
94
|
+
runOptions.signal === undefined
|
|
95
|
+
? undefined
|
|
96
|
+
: { signal: runOptions.signal },
|
|
97
|
+
);
|
|
98
|
+
|
|
99
|
+
if (Exit.isSuccess(exit)) {
|
|
100
|
+
return exit.value;
|
|
101
|
+
}
|
|
102
|
+
if (Cause.hasInterruptsOnly(exit.cause)) {
|
|
103
|
+
throw new TRPCError({
|
|
104
|
+
code: "CLIENT_CLOSED_REQUEST",
|
|
105
|
+
message: "Request cancelled",
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const failure = Cause.findErrorOption(exit.cause);
|
|
110
|
+
if (Option.isSome(failure)) {
|
|
111
|
+
throw mapError(
|
|
112
|
+
failure.value,
|
|
113
|
+
"failure",
|
|
114
|
+
runOptions.procedure,
|
|
115
|
+
options.mapError,
|
|
116
|
+
);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const defect = Cause.findDefect(exit.cause);
|
|
120
|
+
if (Result.isSuccess(defect)) {
|
|
121
|
+
throw mapError(
|
|
122
|
+
defect.success,
|
|
123
|
+
"defect",
|
|
124
|
+
runOptions.procedure,
|
|
125
|
+
options.mapError,
|
|
126
|
+
);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
throw internalError();
|
|
130
|
+
},
|
|
131
|
+
});
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
export const captureStackTrace = (): (() => string | undefined) => {
|
|
2
|
+
const limit = Error.stackTraceLimit;
|
|
3
|
+
Error.stackTraceLimit = 3;
|
|
4
|
+
const traceError = new Error();
|
|
5
|
+
Error.stackTraceLimit = limit;
|
|
6
|
+
|
|
7
|
+
let cache: false | string = false;
|
|
8
|
+
return () => {
|
|
9
|
+
if (cache !== false) {
|
|
10
|
+
return cache;
|
|
11
|
+
}
|
|
12
|
+
const frame = traceError.stack?.split("\n")[3];
|
|
13
|
+
if (frame !== undefined) {
|
|
14
|
+
cache = frame.trim();
|
|
15
|
+
return cache;
|
|
16
|
+
}
|
|
17
|
+
return undefined;
|
|
18
|
+
};
|
|
19
|
+
};
|
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
import type { TestContext, TestOptions } from "@effect/vitest";
|
|
2
|
+
import { Cause, type Context, Effect, Exit, Layer, type Scope } from "effect";
|
|
3
|
+
import * as TestClock from "effect/testing/TestClock";
|
|
4
|
+
import * as TestConsole from "effect/testing/TestConsole";
|
|
5
|
+
import { makeEffectCallerFactory } from "../testing/caller.js";
|
|
6
|
+
import type {
|
|
7
|
+
TrpcTest,
|
|
8
|
+
TrpcTester,
|
|
9
|
+
TrpcTestRuntimeOptions,
|
|
10
|
+
} from "../testing/types.js";
|
|
11
|
+
|
|
12
|
+
export const fixtureName = "__effectTrpcContext";
|
|
13
|
+
|
|
14
|
+
export type TrpcFixture<Provided> = TestContext & {
|
|
15
|
+
readonly [fixtureName]: Context.Context<Provided>;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
export interface FixtureTestApi<Provided> {
|
|
19
|
+
(
|
|
20
|
+
name: string,
|
|
21
|
+
options: TestOptions,
|
|
22
|
+
body: (context: TrpcFixture<Provided>) => Promise<unknown>,
|
|
23
|
+
): void;
|
|
24
|
+
readonly skip: FixtureTestApi<Provided>;
|
|
25
|
+
readonly skipIf: (condition: unknown) => FixtureTestApi<Provided>;
|
|
26
|
+
readonly runIf: (condition: unknown) => FixtureTestApi<Provided>;
|
|
27
|
+
readonly only: FixtureTestApi<Provided>;
|
|
28
|
+
readonly fails: FixtureTestApi<Provided>;
|
|
29
|
+
readonly for: <Item>(
|
|
30
|
+
cases: ReadonlyArray<Item>,
|
|
31
|
+
) => (
|
|
32
|
+
name: string,
|
|
33
|
+
options: TestOptions,
|
|
34
|
+
body: (item: Item, context: TrpcFixture<Provided>) => Promise<unknown>,
|
|
35
|
+
) => void;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const TestEnvironment = Layer.mergeAll(TestConsole.layer, TestClock.layer());
|
|
39
|
+
|
|
40
|
+
const normalizeTestOptions = (
|
|
41
|
+
options: number | TestOptions | undefined,
|
|
42
|
+
): TestOptions =>
|
|
43
|
+
typeof options === "number" ? { timeout: options } : (options ?? {});
|
|
44
|
+
|
|
45
|
+
const restoreContext = <Provided>(
|
|
46
|
+
fixture: Context.Context<Provided>,
|
|
47
|
+
context: Omit<TrpcFixture<Provided>, typeof fixtureName>,
|
|
48
|
+
): TrpcFixture<Provided> =>
|
|
49
|
+
({ ...context, [fixtureName]: fixture }) as TrpcFixture<Provided>;
|
|
50
|
+
|
|
51
|
+
const contextFrom = <Provided>(
|
|
52
|
+
context: TestContext,
|
|
53
|
+
): Context.Context<Provided> => (context as TrpcFixture<Provided>)[fixtureName];
|
|
54
|
+
|
|
55
|
+
const runEffectTest = <A, E>(
|
|
56
|
+
effect: Effect.Effect<A, E, Scope.Scope>,
|
|
57
|
+
context: TestContext,
|
|
58
|
+
): Promise<A> =>
|
|
59
|
+
Effect.runPromise(
|
|
60
|
+
Effect.gen(function* () {
|
|
61
|
+
const exit = yield* Effect.exit(effect);
|
|
62
|
+
if (Exit.isFailure(exit)) {
|
|
63
|
+
for (const error of Cause.prettyErrors(exit.cause)) {
|
|
64
|
+
yield* Effect.logError(error);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
return yield* exit;
|
|
68
|
+
}).pipe(Effect.scoped, Effect.provide(TestEnvironment)),
|
|
69
|
+
{ signal: context.signal },
|
|
70
|
+
);
|
|
71
|
+
|
|
72
|
+
export const makeTrpcTester = <
|
|
73
|
+
Options,
|
|
74
|
+
Caller extends object,
|
|
75
|
+
Provided,
|
|
76
|
+
LayerError,
|
|
77
|
+
>(
|
|
78
|
+
fixtureIt: FixtureTestApi<Provided>,
|
|
79
|
+
options: TrpcTestRuntimeOptions<Options, Caller, Provided, LayerError>,
|
|
80
|
+
): TrpcTester<Options, Caller, Provided> => {
|
|
81
|
+
const run = <A, Eff extends Effect.Effect<unknown, unknown, Provided>>(
|
|
82
|
+
body: (
|
|
83
|
+
trpc: ReturnType<
|
|
84
|
+
typeof makeEffectCallerFactory<Options, Caller, Provided>
|
|
85
|
+
>,
|
|
86
|
+
context: TestContext,
|
|
87
|
+
) => Generator<Eff, A, never>,
|
|
88
|
+
context: TestContext,
|
|
89
|
+
): Effect.Effect<A, unknown, Scope.Scope> => {
|
|
90
|
+
const program = Effect.gen(function* () {
|
|
91
|
+
const services = yield* Effect.context<Provided>();
|
|
92
|
+
const trpc = makeEffectCallerFactory(
|
|
93
|
+
options.adapter,
|
|
94
|
+
options.createCaller,
|
|
95
|
+
services,
|
|
96
|
+
);
|
|
97
|
+
return yield* Effect.gen(() => body(trpc, context));
|
|
98
|
+
});
|
|
99
|
+
// The body constraint proves every yielded service is part of Provided.
|
|
100
|
+
// TypeScript cannot reduce that generic generator requirement here.
|
|
101
|
+
const ready = program as Effect.Effect<A, unknown, Provided>;
|
|
102
|
+
const wrapped = options.around?.(ready) ?? ready;
|
|
103
|
+
return wrapped.pipe(
|
|
104
|
+
Effect.provide(contextFrom<Provided>(context)),
|
|
105
|
+
) as Effect.Effect<A, unknown, Scope.Scope>;
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
const register =
|
|
109
|
+
(current: FixtureTestApi<Provided>): TrpcTest<Options, Caller, Provided> =>
|
|
110
|
+
(name, body, testOptions) =>
|
|
111
|
+
current(
|
|
112
|
+
name,
|
|
113
|
+
normalizeTestOptions(testOptions),
|
|
114
|
+
({
|
|
115
|
+
__effectTrpcContext,
|
|
116
|
+
task,
|
|
117
|
+
signal,
|
|
118
|
+
onTestFailed,
|
|
119
|
+
onTestFinished,
|
|
120
|
+
skip,
|
|
121
|
+
annotate,
|
|
122
|
+
expect,
|
|
123
|
+
_local,
|
|
124
|
+
}: TrpcFixture<Provided>) => {
|
|
125
|
+
const context = restoreContext(__effectTrpcContext, {
|
|
126
|
+
task,
|
|
127
|
+
signal,
|
|
128
|
+
onTestFailed,
|
|
129
|
+
onTestFinished,
|
|
130
|
+
skip,
|
|
131
|
+
annotate,
|
|
132
|
+
expect,
|
|
133
|
+
_local,
|
|
134
|
+
});
|
|
135
|
+
return runEffectTest(run(body, context), context);
|
|
136
|
+
},
|
|
137
|
+
);
|
|
138
|
+
|
|
139
|
+
const make = (
|
|
140
|
+
current: FixtureTestApi<Provided>,
|
|
141
|
+
): TrpcTester<Options, Caller, Provided> => {
|
|
142
|
+
const test = register(current);
|
|
143
|
+
return Object.assign(test, {
|
|
144
|
+
skip: register(current.skip),
|
|
145
|
+
skipIf: (condition: unknown) => register(current.skipIf(condition)),
|
|
146
|
+
runIf: (condition: unknown) => register(current.runIf(condition)),
|
|
147
|
+
only: register(current.only),
|
|
148
|
+
each:
|
|
149
|
+
<Item>(cases: ReadonlyArray<Item>) =>
|
|
150
|
+
<A, Eff extends Effect.Effect<unknown, unknown, Provided>>(
|
|
151
|
+
name: string,
|
|
152
|
+
body: (
|
|
153
|
+
item: Item,
|
|
154
|
+
trpc: ReturnType<
|
|
155
|
+
typeof makeEffectCallerFactory<Options, Caller, Provided>
|
|
156
|
+
>,
|
|
157
|
+
context: TestContext,
|
|
158
|
+
) => Generator<Eff, A, never>,
|
|
159
|
+
testOptions?: number | TestOptions,
|
|
160
|
+
) =>
|
|
161
|
+
fixtureIt.for(cases)(
|
|
162
|
+
name,
|
|
163
|
+
normalizeTestOptions(testOptions),
|
|
164
|
+
(
|
|
165
|
+
item,
|
|
166
|
+
{
|
|
167
|
+
__effectTrpcContext,
|
|
168
|
+
task,
|
|
169
|
+
signal,
|
|
170
|
+
onTestFailed,
|
|
171
|
+
onTestFinished,
|
|
172
|
+
skip,
|
|
173
|
+
annotate,
|
|
174
|
+
expect,
|
|
175
|
+
_local,
|
|
176
|
+
}: TrpcFixture<Provided>,
|
|
177
|
+
) => {
|
|
178
|
+
const context = restoreContext(__effectTrpcContext, {
|
|
179
|
+
task,
|
|
180
|
+
signal,
|
|
181
|
+
onTestFailed,
|
|
182
|
+
onTestFinished,
|
|
183
|
+
skip,
|
|
184
|
+
annotate,
|
|
185
|
+
expect,
|
|
186
|
+
_local,
|
|
187
|
+
});
|
|
188
|
+
return runEffectTest(
|
|
189
|
+
run(
|
|
190
|
+
(trpc, testContext) => body(item, trpc, testContext),
|
|
191
|
+
context,
|
|
192
|
+
),
|
|
193
|
+
context,
|
|
194
|
+
);
|
|
195
|
+
},
|
|
196
|
+
),
|
|
197
|
+
fails: register(current.fails),
|
|
198
|
+
});
|
|
199
|
+
};
|
|
200
|
+
|
|
201
|
+
return make(fixtureIt);
|
|
202
|
+
};
|
package/src/procedure.ts
ADDED
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
TRPCMutationProcedure,
|
|
3
|
+
TRPCProcedureBuilder,
|
|
4
|
+
TRPCQueryProcedure,
|
|
5
|
+
} from "@trpc/server";
|
|
6
|
+
import { Schema } from "effect";
|
|
7
|
+
import { makeProcedureHandler } from "./internal/procedure-handler.js";
|
|
8
|
+
import type {
|
|
9
|
+
DefaultValue,
|
|
10
|
+
EffectProcedureResolver,
|
|
11
|
+
IntersectIfDefined,
|
|
12
|
+
ResolverContext,
|
|
13
|
+
} from "./internal/procedure-types.js";
|
|
14
|
+
import type { RuntimeBridge } from "./internal/runtime.js";
|
|
15
|
+
import { captureStackTrace } from "./internal/stack-trace.js";
|
|
16
|
+
import type { EffectProcedureRequestServices } from "./request-services.js";
|
|
17
|
+
|
|
18
|
+
type Builder<
|
|
19
|
+
Context,
|
|
20
|
+
Meta,
|
|
21
|
+
ContextOverrides,
|
|
22
|
+
InputIn,
|
|
23
|
+
InputOut,
|
|
24
|
+
OutputIn,
|
|
25
|
+
OutputOut,
|
|
26
|
+
> = TRPCProcedureBuilder<
|
|
27
|
+
Context,
|
|
28
|
+
Meta,
|
|
29
|
+
ContextOverrides,
|
|
30
|
+
InputIn,
|
|
31
|
+
InputOut,
|
|
32
|
+
OutputIn,
|
|
33
|
+
OutputOut,
|
|
34
|
+
false
|
|
35
|
+
>;
|
|
36
|
+
|
|
37
|
+
export class EffectProcedureBuilder<
|
|
38
|
+
Context,
|
|
39
|
+
Meta,
|
|
40
|
+
ContextOverrides,
|
|
41
|
+
InputIn,
|
|
42
|
+
InputOut,
|
|
43
|
+
OutputIn,
|
|
44
|
+
OutputOut,
|
|
45
|
+
ProvidedServices,
|
|
46
|
+
LayerError,
|
|
47
|
+
RuntimeRequirements,
|
|
48
|
+
> {
|
|
49
|
+
constructor(
|
|
50
|
+
private readonly builder: Builder<
|
|
51
|
+
Context,
|
|
52
|
+
Meta,
|
|
53
|
+
ContextOverrides,
|
|
54
|
+
InputIn,
|
|
55
|
+
InputOut,
|
|
56
|
+
OutputIn,
|
|
57
|
+
OutputOut
|
|
58
|
+
>,
|
|
59
|
+
private readonly requestServices: EffectProcedureRequestServices<
|
|
60
|
+
ResolverContext<
|
|
61
|
+
Builder<
|
|
62
|
+
Context,
|
|
63
|
+
Meta,
|
|
64
|
+
ContextOverrides,
|
|
65
|
+
InputIn,
|
|
66
|
+
InputOut,
|
|
67
|
+
OutputIn,
|
|
68
|
+
OutputOut
|
|
69
|
+
>
|
|
70
|
+
>,
|
|
71
|
+
ProvidedServices,
|
|
72
|
+
LayerError
|
|
73
|
+
>,
|
|
74
|
+
private readonly runtime: RuntimeBridge<RuntimeRequirements>,
|
|
75
|
+
) {}
|
|
76
|
+
|
|
77
|
+
input<SchemaValue extends Schema.ConstraintDecoder<unknown>>(
|
|
78
|
+
schema: SchemaValue,
|
|
79
|
+
): EffectProcedureBuilder<
|
|
80
|
+
Context,
|
|
81
|
+
Meta,
|
|
82
|
+
ContextOverrides,
|
|
83
|
+
IntersectIfDefined<InputIn, SchemaValue["Encoded"]>,
|
|
84
|
+
IntersectIfDefined<InputOut, SchemaValue["Type"]>,
|
|
85
|
+
OutputIn,
|
|
86
|
+
OutputOut,
|
|
87
|
+
ProvidedServices,
|
|
88
|
+
LayerError,
|
|
89
|
+
RuntimeRequirements
|
|
90
|
+
> {
|
|
91
|
+
const next = this.builder.input(Schema.toStandardSchemaV1(schema) as never);
|
|
92
|
+
return new EffectProcedureBuilder(
|
|
93
|
+
next as unknown as Builder<
|
|
94
|
+
Context,
|
|
95
|
+
Meta,
|
|
96
|
+
ContextOverrides,
|
|
97
|
+
IntersectIfDefined<InputIn, SchemaValue["Encoded"]>,
|
|
98
|
+
IntersectIfDefined<InputOut, SchemaValue["Type"]>,
|
|
99
|
+
OutputIn,
|
|
100
|
+
OutputOut
|
|
101
|
+
>,
|
|
102
|
+
this.requestServices,
|
|
103
|
+
this.runtime,
|
|
104
|
+
);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
output<SchemaValue extends Schema.ConstraintDecoder<unknown>>(
|
|
108
|
+
schema: SchemaValue,
|
|
109
|
+
): EffectProcedureBuilder<
|
|
110
|
+
Context,
|
|
111
|
+
Meta,
|
|
112
|
+
ContextOverrides,
|
|
113
|
+
InputIn,
|
|
114
|
+
InputOut,
|
|
115
|
+
IntersectIfDefined<OutputIn, SchemaValue["Encoded"]>,
|
|
116
|
+
IntersectIfDefined<OutputOut, SchemaValue["Type"]>,
|
|
117
|
+
ProvidedServices,
|
|
118
|
+
LayerError,
|
|
119
|
+
RuntimeRequirements
|
|
120
|
+
> {
|
|
121
|
+
const next = this.builder.output(Schema.toStandardSchemaV1(schema));
|
|
122
|
+
return new EffectProcedureBuilder(
|
|
123
|
+
next as unknown as Builder<
|
|
124
|
+
Context,
|
|
125
|
+
Meta,
|
|
126
|
+
ContextOverrides,
|
|
127
|
+
InputIn,
|
|
128
|
+
InputOut,
|
|
129
|
+
IntersectIfDefined<OutputIn, SchemaValue["Encoded"]>,
|
|
130
|
+
IntersectIfDefined<OutputOut, SchemaValue["Type"]>
|
|
131
|
+
>,
|
|
132
|
+
this.requestServices,
|
|
133
|
+
this.runtime,
|
|
134
|
+
);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
query<Output>(
|
|
138
|
+
resolver: EffectProcedureResolver<
|
|
139
|
+
DefaultValue<InputOut, undefined>,
|
|
140
|
+
ProvidedServices | NoInfer<RuntimeRequirements>,
|
|
141
|
+
DefaultValue<OutputIn, Output>
|
|
142
|
+
>,
|
|
143
|
+
): TRPCQueryProcedure<{
|
|
144
|
+
input: DefaultValue<InputIn, void>;
|
|
145
|
+
output: DefaultValue<OutputOut, Output>;
|
|
146
|
+
meta: Meta;
|
|
147
|
+
}> {
|
|
148
|
+
return this.dispatch("query", resolver) as TRPCQueryProcedure<{
|
|
149
|
+
input: DefaultValue<InputIn, void>;
|
|
150
|
+
output: DefaultValue<OutputOut, Output>;
|
|
151
|
+
meta: Meta;
|
|
152
|
+
}>;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
mutation<Output>(
|
|
156
|
+
resolver: EffectProcedureResolver<
|
|
157
|
+
DefaultValue<InputOut, undefined>,
|
|
158
|
+
ProvidedServices | NoInfer<RuntimeRequirements>,
|
|
159
|
+
DefaultValue<OutputIn, Output>
|
|
160
|
+
>,
|
|
161
|
+
): TRPCMutationProcedure<{
|
|
162
|
+
input: DefaultValue<InputIn, void>;
|
|
163
|
+
output: DefaultValue<OutputOut, Output>;
|
|
164
|
+
meta: Meta;
|
|
165
|
+
}> {
|
|
166
|
+
return this.dispatch("mutation", resolver) as TRPCMutationProcedure<{
|
|
167
|
+
input: DefaultValue<InputIn, void>;
|
|
168
|
+
output: DefaultValue<OutputOut, Output>;
|
|
169
|
+
meta: Meta;
|
|
170
|
+
}>;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
private dispatch<Output>(
|
|
174
|
+
type: "mutation" | "query",
|
|
175
|
+
resolver: EffectProcedureResolver<
|
|
176
|
+
DefaultValue<InputOut, undefined>,
|
|
177
|
+
ProvidedServices | NoInfer<RuntimeRequirements>,
|
|
178
|
+
Output
|
|
179
|
+
>,
|
|
180
|
+
): unknown {
|
|
181
|
+
const handler = makeProcedureHandler(
|
|
182
|
+
this.runtime,
|
|
183
|
+
resolver,
|
|
184
|
+
this.requestServices,
|
|
185
|
+
{ captureStackTrace: captureStackTrace(), type },
|
|
186
|
+
) as never;
|
|
187
|
+
return type === "query"
|
|
188
|
+
? this.builder.query(handler)
|
|
189
|
+
: this.builder.mutation(handler);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { type Layer as EffectLayer, Layer } from "effect";
|
|
2
|
+
|
|
3
|
+
export interface EffectProcedureRequestServices<
|
|
4
|
+
Context,
|
|
5
|
+
Requirements,
|
|
6
|
+
Error = never,
|
|
7
|
+
> {
|
|
8
|
+
readonly layer: (context: Context) => EffectLayer.Layer<Requirements, Error>;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export const makeRequestServices = <Context, Requirements, Error = never>(
|
|
12
|
+
layer: (context: Context) => EffectLayer.Layer<Requirements, Error>,
|
|
13
|
+
): EffectProcedureRequestServices<Context, Requirements, Error> => ({ layer });
|
|
14
|
+
|
|
15
|
+
export const extendRequestServices = <
|
|
16
|
+
BaseContext,
|
|
17
|
+
Context extends BaseContext,
|
|
18
|
+
BaseRequirements,
|
|
19
|
+
BaseError,
|
|
20
|
+
AdditionalRequirements,
|
|
21
|
+
AdditionalError,
|
|
22
|
+
AdditionalDependencies extends BaseRequirements = BaseRequirements,
|
|
23
|
+
>(
|
|
24
|
+
base: EffectProcedureRequestServices<
|
|
25
|
+
BaseContext,
|
|
26
|
+
BaseRequirements,
|
|
27
|
+
BaseError
|
|
28
|
+
>,
|
|
29
|
+
additional: (
|
|
30
|
+
context: Context,
|
|
31
|
+
) => EffectLayer.Layer<
|
|
32
|
+
AdditionalRequirements,
|
|
33
|
+
AdditionalError,
|
|
34
|
+
AdditionalDependencies
|
|
35
|
+
>,
|
|
36
|
+
): EffectProcedureRequestServices<
|
|
37
|
+
Context,
|
|
38
|
+
BaseRequirements | AdditionalRequirements,
|
|
39
|
+
BaseError | AdditionalError
|
|
40
|
+
> =>
|
|
41
|
+
makeRequestServices((context: Context) =>
|
|
42
|
+
Layer.provideMerge(additional(context), base.layer(context)),
|
|
43
|
+
);
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { getTRPCErrorFromUnknown, type TRPCError } from "@trpc/server";
|
|
2
|
+
import { type Context, Effect } from "effect";
|
|
3
|
+
import type { EffectTRPCAdapter } from "../adapter.js";
|
|
4
|
+
|
|
5
|
+
export type EffectCaller<Caller> = {
|
|
6
|
+
readonly [Key in keyof Caller]: Caller[Key] extends (
|
|
7
|
+
...arguments_: infer Arguments
|
|
8
|
+
) => Promise<infer Result>
|
|
9
|
+
? (...arguments_: Arguments) => Effect.Effect<Result, TRPCError>
|
|
10
|
+
: Caller[Key] extends (...arguments_: infer Arguments) => infer Result
|
|
11
|
+
? (...arguments_: Arguments) => Result
|
|
12
|
+
: Caller[Key] extends Record<string, unknown>
|
|
13
|
+
? EffectCaller<Caller[Key]>
|
|
14
|
+
: Caller[Key];
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
export type EffectCallerFactory<Options, Caller> = EffectCaller<Caller> &
|
|
18
|
+
((options?: Options) => EffectCaller<Caller>);
|
|
19
|
+
|
|
20
|
+
export const makeEffectCaller = <Caller extends object, Services>(
|
|
21
|
+
adapter: Pick<EffectTRPCAdapter<never>, "runWithServices">,
|
|
22
|
+
promiseCaller: Caller,
|
|
23
|
+
services: Context.Context<Services>,
|
|
24
|
+
): EffectCaller<Caller> => {
|
|
25
|
+
const build = (path: ReadonlyArray<string>): unknown =>
|
|
26
|
+
new Proxy(
|
|
27
|
+
Object.assign(() => undefined, { path }),
|
|
28
|
+
{
|
|
29
|
+
apply(_target, _this, argumentsList) {
|
|
30
|
+
return Effect.tryPromise({
|
|
31
|
+
catch: getTRPCErrorFromUnknown,
|
|
32
|
+
try: () =>
|
|
33
|
+
adapter.runWithServices(services, () => {
|
|
34
|
+
let leaf: unknown = promiseCaller;
|
|
35
|
+
for (const segment of path) {
|
|
36
|
+
leaf = (leaf as Record<string, unknown>)[segment];
|
|
37
|
+
}
|
|
38
|
+
return (leaf as (...args: unknown[]) => Promise<unknown>)(
|
|
39
|
+
...argumentsList,
|
|
40
|
+
);
|
|
41
|
+
}),
|
|
42
|
+
});
|
|
43
|
+
},
|
|
44
|
+
get(_target, property) {
|
|
45
|
+
if (typeof property !== "string" || property === "then") {
|
|
46
|
+
return undefined;
|
|
47
|
+
}
|
|
48
|
+
return build([...path, property]);
|
|
49
|
+
},
|
|
50
|
+
},
|
|
51
|
+
);
|
|
52
|
+
|
|
53
|
+
return build([]) as EffectCaller<Caller>;
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
export const makeEffectCallerFactory = <
|
|
57
|
+
Options,
|
|
58
|
+
Caller extends object,
|
|
59
|
+
Services,
|
|
60
|
+
>(
|
|
61
|
+
adapter: Pick<EffectTRPCAdapter<never>, "runWithServices">,
|
|
62
|
+
createCaller: (options?: Options) => Caller,
|
|
63
|
+
services: Context.Context<Services>,
|
|
64
|
+
): EffectCallerFactory<Options, Caller> => {
|
|
65
|
+
const defaultCaller = makeEffectCaller(adapter, createCaller(), services);
|
|
66
|
+
const target = (options?: Options) =>
|
|
67
|
+
makeEffectCaller(adapter, createCaller(options), services);
|
|
68
|
+
|
|
69
|
+
return new Proxy(target, {
|
|
70
|
+
get(_target, property, receiver) {
|
|
71
|
+
if (property === "then") {
|
|
72
|
+
return undefined;
|
|
73
|
+
}
|
|
74
|
+
if (typeof property !== "string") {
|
|
75
|
+
return Reflect.get(_target, property, receiver);
|
|
76
|
+
}
|
|
77
|
+
return Reflect.get(defaultCaller, property);
|
|
78
|
+
},
|
|
79
|
+
}) as EffectCallerFactory<Options, Caller>;
|
|
80
|
+
};
|