@btravstack/pothos 0.0.0-stage → 0.27.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +31 -2
- package/dist/index.d.mts +159 -0
- package/dist/index.mjs +112 -0
- package/package.json +78 -3
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Benoit TRAVERS
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,32 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @btravstack/pothos
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
[Pothos](https://pothos-graphql.dev) fields that call
|
|
4
|
+
[oRPC](https://orpc.dev) procedures, for a GraphQL gateway in front of
|
|
5
|
+
contract-first services. The default export is the plugin, listed after
|
|
6
|
+
`@unthrown/pothos`'s:
|
|
7
|
+
`plugins: [RelayPlugin, ErrorsPlugin, UnthrownPlugin, OrpcPlugin]`.
|
|
8
|
+
|
|
9
|
+
- `t.procedureField(procedure, { type, call, input, output })` validates the
|
|
10
|
+
field's input through the procedure's own contract before calling it,
|
|
11
|
+
decodes the answer with `output`, and answers each error the procedure
|
|
12
|
+
declares as the GraphQL error class the builder's `orpc: { refusals }`
|
|
13
|
+
registers for its code — a member of the field's result union. A registry
|
|
14
|
+
that misses a code `call` can fail with does not compile.
|
|
15
|
+
- `t.procedureConnection(procedure, { …, connection })` is the same for a
|
|
16
|
+
Relay connection, with the decoded page folded by `connection`.
|
|
17
|
+
- `UNAUTHORIZED` and `FORBIDDEN` are answered as GraphQL's own error, with
|
|
18
|
+
`extensions.code`, whether the procedure declares them or not.
|
|
19
|
+
- `loadProcedure` and `procedureErrors` are the same answer through a
|
|
20
|
+
DataLoader; `contractCall`, `contractInput` and `contractOutput` are the
|
|
21
|
+
contract half on its own.
|
|
22
|
+
|
|
23
|
+
Every dependency is a peer: Pothos plugins and `graphql` each need one copy.
|
|
24
|
+
|
|
25
|
+
```sh
|
|
26
|
+
pnpm add @btravstack/pothos @pothos/core @pothos/plugin-errors @pothos/plugin-relay \
|
|
27
|
+
@unthrown/pothos@^0.1.0 graphql unthrown zod \
|
|
28
|
+
@orpc/client@2.0.0-beta.28 @orpc/contract@2.0.0-beta.28
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
See
|
|
32
|
+
[Resolve GraphQL fields from oRPC procedures](https://btravstack.github.io/btravstack/how-to/resolve-graphql-fields-from-orpc).
|
package/dist/index.d.mts
ADDED
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
import { BasePlugin, FieldKind, FieldNullability, InputFieldMap, InputShapeFromFields, ObjectRef, OutputType, SchemaTypes, ShapeFromTypeParam, TypeParam } from "@pothos/core";
|
|
2
|
+
import { ResultConnectionOptions } from "@unthrown/pothos/relay";
|
|
3
|
+
import { CaseOf, Caseable, Refusals, ResultFieldOptions } from "@unthrown/pothos";
|
|
4
|
+
import { AsyncResult } from "unthrown";
|
|
5
|
+
import { z } from "zod";
|
|
6
|
+
import { AnySchema, ErrorMap, ProcedureContract } from "@orpc/contract";
|
|
7
|
+
import { ConnectionShapeForType, DefaultConnectionArguments } from "@pothos/plugin-relay";
|
|
8
|
+
//#region src/plugin.d.ts
|
|
9
|
+
declare const pluginName = "orpc";
|
|
10
|
+
declare class PothosOrpcPlugin<Types extends SchemaTypes> extends BasePlugin<Types> {}
|
|
11
|
+
declare global {
|
|
12
|
+
export namespace PothosSchemaTypes {
|
|
13
|
+
interface Plugins<Types extends SchemaTypes> {
|
|
14
|
+
orpc: PothosOrpcPlugin<Types>;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
//#endregion
|
|
19
|
+
//#region src/contract.d.ts
|
|
20
|
+
/**
|
|
21
|
+
* The one Zod schema a procedure's contract declares for its input, typed as
|
|
22
|
+
* declared — to derive a GraphQL argument's shape from it (`.shape`, a
|
|
23
|
+
* `z.preprocess` around it) when the schema is built.
|
|
24
|
+
*
|
|
25
|
+
* A procedure whose contract holds anything but exactly one Zod input schema
|
|
26
|
+
* is a mistake in the schema's source, not a request's: it throws, once, as
|
|
27
|
+
* the schema is built.
|
|
28
|
+
*/
|
|
29
|
+
export declare const contractInput: <Input extends z.ZodType, Output extends AnySchema, Errors extends ErrorMap>(procedure: ProcedureContract<Input, Output, Errors>) => Input;
|
|
30
|
+
declare const InvalidInput_base: import("unthrown").TaggedErrorConstructor<"InvalidInput">;
|
|
31
|
+
/**
|
|
32
|
+
* An input the procedure's contract refuses, caught before the procedure is
|
|
33
|
+
* called: each of the contract's issues, with its message and path.
|
|
34
|
+
*/
|
|
35
|
+
export declare class InvalidInput extends InvalidInput_base<{
|
|
36
|
+
readonly issues: readonly {
|
|
37
|
+
readonly message: string;
|
|
38
|
+
readonly path: readonly PropertyKey[];
|
|
39
|
+
}[];
|
|
40
|
+
}> {
|
|
41
|
+
message: string;
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* `call`, handed the input as the procedure's contract parses it and encodes
|
|
45
|
+
* it back for the wire — its defaults applied, its codecs run, asynchronous
|
|
46
|
+
* refinements included — or `InvalidInput` without calling it. A schema with a
|
|
47
|
+
* transform cannot be encoded back: `call` gets the input as given, which the
|
|
48
|
+
* contract admitted. A contract whose input is not one Zod
|
|
49
|
+
* schema is a defect.
|
|
50
|
+
*/
|
|
51
|
+
export declare const contractCall: <Schema extends z.ZodType, OutputSchema extends AnySchema, Errors extends ErrorMap, Output, Failure>(procedure: ProcedureContract<Schema, OutputSchema, Errors>, input: unknown, call: (validated: z.input<Schema>) => AsyncResult<Output, Failure>) => AsyncResult<Output, Failure | InvalidInput>;
|
|
52
|
+
/**
|
|
53
|
+
* A procedure's answer decoded by `schema` — the shape the GraphQL field
|
|
54
|
+
* resolves, codecs run. An answer the schema refuses is a defect: the service
|
|
55
|
+
* broke its own contract.
|
|
56
|
+
*/
|
|
57
|
+
export declare const contractOutput: <Schema extends z.ZodType>(schema: Schema, value: unknown) => AsyncResult<z.infer<Schema>, never>;
|
|
58
|
+
//#endregion
|
|
59
|
+
//#region src/procedure-field.d.ts
|
|
60
|
+
type ErrorClass = new (failure: never) => Error;
|
|
61
|
+
type Access = {
|
|
62
|
+
readonly code: "UNAUTHORIZED" | "FORBIDDEN";
|
|
63
|
+
};
|
|
64
|
+
type Procedure<Schema extends z.ZodType> = ProcedureContract<Schema, AnySchema, ErrorMap>;
|
|
65
|
+
type AnyProcedure = Procedure<z.ZodType>;
|
|
66
|
+
type Call<Context, Input, Failure> = (context: Context) => (input: Input) => AsyncResult<unknown, Failure>;
|
|
67
|
+
type Covered<Registry, Failure> = Registry extends Refusals<Failure> ? {} : {
|
|
68
|
+
readonly uncovered: Exclude<CaseOf<Failure>, keyof Registry>;
|
|
69
|
+
};
|
|
70
|
+
/**
|
|
71
|
+
* `t.procedureField`'s options: `resultField`'s, with `call`, `input` and
|
|
72
|
+
* `output` in place of `resolve`, and the builder's `orpc.refusals` in place
|
|
73
|
+
* of `refusals`. The registry must cover every error `call` can return but
|
|
74
|
+
* the access refusals, and `InvalidInput`; an uncovered case is a compile
|
|
75
|
+
* error naming it. A declared error `call` handles itself needs no entry.
|
|
76
|
+
*/
|
|
77
|
+
type ProcedureFieldOptions<Types extends SchemaTypes, ParentShape, Type extends TypeParam<Types>, Nullable extends FieldNullability<Type>, Args extends InputFieldMap, Kind extends FieldKind, Schema extends z.ZodType, Failure> = Omit<ResultFieldOptions<Types, ParentShape, Type, Nullable, Args, Kind, Failure>, "refusals" | "resolve"> & {
|
|
78
|
+
readonly call: Call<Types["Context"], z.input<Schema>, Failure>;
|
|
79
|
+
readonly input: (args: InputShapeFromFields<Args>, parent: ParentShape) => NoInfer<z.input<Schema>>;
|
|
80
|
+
readonly output: z.ZodType<ShapeFromTypeParam<Types, Type, false>>;
|
|
81
|
+
readonly tap?: (value: ShapeFromTypeParam<Types, Type, false>, context: Types["Context"]) => void;
|
|
82
|
+
} & Covered<Types["Refusals"], Exclude<Failure, Access> | InvalidInput>;
|
|
83
|
+
/**
|
|
84
|
+
* `t.procedureConnection`'s options: `resultConnection`'s, with `call`,
|
|
85
|
+
* `input` and `output` in place of `resolve`, `connection` folding the
|
|
86
|
+
* decoded page into the connection's shape, and an optional `connectionType`
|
|
87
|
+
* for fields beside its edges and page info.
|
|
88
|
+
*/
|
|
89
|
+
type ProcedureConnectionOptions<Types extends SchemaTypes, ParentShape, Type extends OutputType<Types>, Nullable extends boolean, EdgeNullability extends FieldNullability<[unknown]>, NodeNullability extends boolean, Args extends InputFieldMap, Kind extends FieldKind, Schema extends z.ZodType, Page, Failure, Connection extends ConnectionShapeForType<Types, Type, false, EdgeNullability, NodeNullability> = ConnectionShapeForType<Types, Type, false, EdgeNullability, NodeNullability>> = Omit<ResultConnectionOptions<Types, ParentShape, Type, Nullable, EdgeNullability, NodeNullability, Args, Kind, Failure, Connection>, "refusals" | "resolve"> & {
|
|
90
|
+
readonly call: Call<Types["Context"], z.input<Schema>, Failure>;
|
|
91
|
+
readonly input: (args: DefaultConnectionArguments & InputShapeFromFields<Args>, parent: ParentShape) => NoInfer<z.input<Schema>>;
|
|
92
|
+
readonly output: z.ZodType<Page>;
|
|
93
|
+
readonly connectionType?: ObjectRef<Types, Connection>;
|
|
94
|
+
readonly connection: (page: Page, args: DefaultConnectionArguments & InputShapeFromFields<Args>) => Connection;
|
|
95
|
+
} & Covered<Types["Refusals"], Exclude<Failure, Access> | InvalidInput>;
|
|
96
|
+
declare global {
|
|
97
|
+
export namespace PothosSchemaTypes {
|
|
98
|
+
interface UserSchemaTypes {
|
|
99
|
+
Refusals: Readonly<Record<string, ErrorClass>>;
|
|
100
|
+
}
|
|
101
|
+
interface ExtendDefaultTypes<PartialTypes extends Partial<UserSchemaTypes>> {
|
|
102
|
+
Refusals: PartialTypes["Refusals"] & {};
|
|
103
|
+
}
|
|
104
|
+
interface SchemaBuilderOptions<Types extends SchemaTypes> {
|
|
105
|
+
orpc?: {
|
|
106
|
+
readonly refusals: Types["Refusals"];
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
interface RootFieldBuilder<Types extends SchemaTypes, ParentShape, Kind extends FieldKind = FieldKind> {
|
|
110
|
+
/**
|
|
111
|
+
* A field answering an oRPC procedure: `input` maps the field's
|
|
112
|
+
* arguments to the procedure's input, the contract validates it before
|
|
113
|
+
* `call` runs, and `output` decodes the answer. A declared error is
|
|
114
|
+
* answered as its registered refusal, a member of the field's result
|
|
115
|
+
* union; the service refusing access is GraphQL's own error, with
|
|
116
|
+
* `extensions.code`.
|
|
117
|
+
*/
|
|
118
|
+
procedureField: <Type extends TypeParam<Types>, Schema extends z.ZodType, Failure, Nullable extends FieldNullability<Type> = Types["DefaultFieldNullability"], Args extends InputFieldMap = {}>(procedure: Procedure<Schema>, options: ProcedureFieldOptions<Types, ParentShape, Type, Nullable, Args, Kind, Schema, Failure>) => FieldRef<Types, ShapeFromTypeParam<Types, Type, Nullable>, Kind>;
|
|
119
|
+
/**
|
|
120
|
+
* A Relay connection answering an oRPC procedure: `procedureField`'s
|
|
121
|
+
* contract, with the decoded page folded by `connection`.
|
|
122
|
+
*/
|
|
123
|
+
procedureConnection: <Type extends OutputType<Types>, Schema extends z.ZodType, Page, Failure, Nullable extends boolean, Args extends InputFieldMap = {}, EdgeNullability extends FieldNullability<[unknown]> = Types["DefaultEdgesNullability"], NodeNullability extends boolean = Types["DefaultNodeNullability"], Connection extends ConnectionShapeForType<Types, Type, false, EdgeNullability, NodeNullability> = ConnectionShapeForType<Types, Type, false, EdgeNullability, NodeNullability>>(procedure: Procedure<Schema>, options: ProcedureConnectionOptions<Types, ParentShape, Type, Nullable, EdgeNullability, NodeNullability, Args, Kind, Schema, Page, Failure, Connection>) => FieldRef<Types, ConnectionShapeForType<Types, Type, Nullable, EdgeNullability, NodeNullability>>;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
/**
|
|
128
|
+
* The refusals a procedure can answer, read from `registry`: one per error
|
|
129
|
+
* code its contract declares, and `InvalidInput`. A code the registry lacks is
|
|
130
|
+
* left out, and so are `UNAUTHORIZED` and `FORBIDDEN`, answered as GraphQL's
|
|
131
|
+
* own error.
|
|
132
|
+
*/
|
|
133
|
+
export declare const refusalsOf: <Failure>(registry: Readonly<Record<string, ErrorClass>>, procedure: AnyProcedure) => Refusals<Failure>;
|
|
134
|
+
/**
|
|
135
|
+
* `call` handed the input the procedure's contract accepts, its answer decoded
|
|
136
|
+
* by `output`. `UNAUTHORIZED` and `FORBIDDEN`, declared or not, leave the
|
|
137
|
+
* error channel: they travel as a `GraphQLError` carrying the code in
|
|
138
|
+
* `extensions.code`, an error of the operation, never a refusal of the field.
|
|
139
|
+
*/
|
|
140
|
+
export declare const callProcedure: <Schema extends z.ZodType, Output, Failure>(procedure: Procedure<Schema>, input: unknown, call: (input: z.input<Schema>) => AsyncResult<unknown, Failure>, output: z.ZodType<Output>) => AsyncResult<Output, Exclude<Failure, Access> | InvalidInput>;
|
|
141
|
+
/**
|
|
142
|
+
* What a DataLoader answers for one key read through `procedure`: the decoded
|
|
143
|
+
* value, or an `Error` — the registered refusal for a declared error, the
|
|
144
|
+
* defect otherwise — so one key's failure never fails the batch.
|
|
145
|
+
*
|
|
146
|
+
* **It answers a `Promise`, and that is the boundary**: a DataLoader's batch
|
|
147
|
+
* function answers a value or an `Error` per key.
|
|
148
|
+
*/
|
|
149
|
+
export declare const loadProcedure: <Schema extends z.ZodType, Output, Failure extends Caseable>(registry: Readonly<Record<string, ErrorClass>>, procedure: Procedure<Schema>, call: (input: z.input<Schema>) => AsyncResult<unknown, Failure>, input: unknown, output: z.ZodType<Output>) => Promise<Output | Error>;
|
|
150
|
+
/**
|
|
151
|
+
* The `errors` option of a field that reads `procedure` through
|
|
152
|
+
* `loadProcedure`: the registered refusals it can answer, as the field's
|
|
153
|
+
* result union.
|
|
154
|
+
*/
|
|
155
|
+
export declare const procedureErrors: (registry: Readonly<Record<string, ErrorClass>>, procedure: AnyProcedure) => {
|
|
156
|
+
readonly types: (new (...args: never[]) => Error)[];
|
|
157
|
+
};
|
|
158
|
+
//#endregion
|
|
159
|
+
export { type ProcedureConnectionOptions, type ProcedureFieldOptions, pluginName as default };
|
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import SchemaBuilder, { BasePlugin, RootFieldBuilder } from "@pothos/core";
|
|
2
|
+
import { ORPCError } from "@orpc/client";
|
|
3
|
+
import "@unthrown/pothos/relay";
|
|
4
|
+
import { outcomeOf } from "@unthrown/pothos";
|
|
5
|
+
import { GraphQLError } from "graphql";
|
|
6
|
+
import { Err, P, TaggedError, fromPromise, fromSafePromise, fromSafeThrowable } from "unthrown";
|
|
7
|
+
import { z } from "zod";
|
|
8
|
+
//#region src/contract.ts
|
|
9
|
+
/**
|
|
10
|
+
* The one Zod schema a procedure's contract declares for its input, typed as
|
|
11
|
+
* declared — to derive a GraphQL argument's shape from it (`.shape`, a
|
|
12
|
+
* `z.preprocess` around it) when the schema is built.
|
|
13
|
+
*
|
|
14
|
+
* A procedure whose contract holds anything but exactly one Zod input schema
|
|
15
|
+
* is a mistake in the schema's source, not a request's: it throws, once, as
|
|
16
|
+
* the schema is built.
|
|
17
|
+
*/
|
|
18
|
+
const contractInput = (procedure) => {
|
|
19
|
+
const [schema] = z.tuple([z.custom((value) => value instanceof z.ZodType)]).parse(procedure["~orpc"].inputSchemas);
|
|
20
|
+
return schema;
|
|
21
|
+
};
|
|
22
|
+
/**
|
|
23
|
+
* An input the procedure's contract refuses, caught before the procedure is
|
|
24
|
+
* called: each of the contract's issues, with its message and path.
|
|
25
|
+
*/
|
|
26
|
+
var InvalidInput = class extends TaggedError("InvalidInput") {
|
|
27
|
+
message = "Input does not satisfy the service contract";
|
|
28
|
+
};
|
|
29
|
+
/**
|
|
30
|
+
* `call`, handed the input as the procedure's contract parses it and encodes
|
|
31
|
+
* it back for the wire — its defaults applied, its codecs run, asynchronous
|
|
32
|
+
* refinements included — or `InvalidInput` without calling it. A schema with a
|
|
33
|
+
* transform cannot be encoded back: `call` gets the input as given, which the
|
|
34
|
+
* contract admitted. A contract whose input is not one Zod
|
|
35
|
+
* schema is a defect.
|
|
36
|
+
*/
|
|
37
|
+
const contractCall = (procedure, input, call) => fromSafeThrowable(() => contractInput(procedure))().toAsync().flatMap((schema) => fromSafePromise(schema.safeParseAsync(input)).flatMap((parsed) => parsed.success ? wireInput(schema, parsed.data, input) : Err(new InvalidInput({ issues: parsed.error.issues })))).flatMap((validated) => call(validated));
|
|
38
|
+
const wireInput = (schema, parsed, given) => fromPromise(z.encodeAsync(schema, parsed), (cause, defect) => cause instanceof z.core.$ZodEncodeError ? "Unencodable" : defect(cause)).recoverErrCases((matcher) => matcher.with("Unencodable", () => given));
|
|
39
|
+
/**
|
|
40
|
+
* A procedure's answer decoded by `schema` — the shape the GraphQL field
|
|
41
|
+
* resolves, codecs run. An answer the schema refuses is a defect: the service
|
|
42
|
+
* broke its own contract.
|
|
43
|
+
*/
|
|
44
|
+
const contractOutput = (schema, value) => fromSafePromise(schema.parseAsync(value));
|
|
45
|
+
//#endregion
|
|
46
|
+
//#region src/procedure-field.ts
|
|
47
|
+
const ACCESS_CODES = /* @__PURE__ */ new Set(["UNAUTHORIZED", "FORBIDDEN"]);
|
|
48
|
+
/**
|
|
49
|
+
* The refusals a procedure can answer, read from `registry`: one per error
|
|
50
|
+
* code its contract declares, and `InvalidInput`. A code the registry lacks is
|
|
51
|
+
* left out, and so are `UNAUTHORIZED` and `FORBIDDEN`, answered as GraphQL's
|
|
52
|
+
* own error.
|
|
53
|
+
*/
|
|
54
|
+
const refusalsOf = (registry, procedure) => Object.fromEntries([...Object.keys(procedure["~orpc"].errorMap), "InvalidInput"].flatMap((code) => {
|
|
55
|
+
const Refusal = ACCESS_CODES.has(code) ? void 0 : registry[code];
|
|
56
|
+
return Refusal === void 0 ? [] : [[code, Refusal]];
|
|
57
|
+
}));
|
|
58
|
+
const accessRefusalOf = (cause) => cause instanceof ORPCError && (cause.code === "UNAUTHORIZED" || cause.code === "FORBIDDEN") ? new GraphQLError("Service refused access", { extensions: { code: cause.code } }) : void 0;
|
|
59
|
+
/**
|
|
60
|
+
* `call` handed the input the procedure's contract accepts, its answer decoded
|
|
61
|
+
* by `output`. `UNAUTHORIZED` and `FORBIDDEN`, declared or not, leave the
|
|
62
|
+
* error channel: they travel as a `GraphQLError` carrying the code in
|
|
63
|
+
* `extensions.code`, an error of the operation, never a refusal of the field.
|
|
64
|
+
*/
|
|
65
|
+
const callProcedure = (procedure, input, call, output) => contractCall(procedure, input, (validated) => withoutAccess(call(validated))).flatMap((value) => contractOutput(output, value)).recoverDefect((cause) => {
|
|
66
|
+
throw accessRefusalOf(cause) ?? cause;
|
|
67
|
+
});
|
|
68
|
+
function withoutAccess(called) {
|
|
69
|
+
return called.flatMapErrCases((matcher, defect) => matcher.with({ code: "UNAUTHORIZED" }, { code: "FORBIDDEN" }, (failure) => defect(failure)).with(P._, (failure) => Err(failure)));
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* What a DataLoader answers for one key read through `procedure`: the decoded
|
|
73
|
+
* value, or an `Error` — the registered refusal for a declared error, the
|
|
74
|
+
* defect otherwise — so one key's failure never fails the batch.
|
|
75
|
+
*
|
|
76
|
+
* **It answers a `Promise`, and that is the boundary**: a DataLoader's batch
|
|
77
|
+
* function answers a value or an `Error` per key.
|
|
78
|
+
*/
|
|
79
|
+
const loadProcedure = async (registry, procedure, call, input, output) => outcomeOf(callProcedure(procedure, input, call, output), refusalsOf(registry, procedure));
|
|
80
|
+
/**
|
|
81
|
+
* The `errors` option of a field that reads `procedure` through
|
|
82
|
+
* `loadProcedure`: the registered refusals it can answer, as the field's
|
|
83
|
+
* result union.
|
|
84
|
+
*/
|
|
85
|
+
const procedureErrors = (registry, procedure) => ({ types: Object.values(refusalsOf(registry, procedure)) });
|
|
86
|
+
const defineProcedureFields = () => {
|
|
87
|
+
const fieldBuilder = RootFieldBuilder.prototype;
|
|
88
|
+
fieldBuilder.procedureField = function procedureField(procedure, { call, input, output, tap, ...options }) {
|
|
89
|
+
return this.resultField({
|
|
90
|
+
...options,
|
|
91
|
+
refusals: refusalsOf(this.builder.options.orpc?.refusals ?? {}, procedure),
|
|
92
|
+
resolve: (parent, args, context) => callProcedure(procedure, input(args, parent), call(context), output).tap((value) => {
|
|
93
|
+
tap?.(value, context);
|
|
94
|
+
})
|
|
95
|
+
});
|
|
96
|
+
};
|
|
97
|
+
fieldBuilder.procedureConnection = function procedureConnection(procedure, { call, input, output, connection, connectionType, ...options }) {
|
|
98
|
+
return this.resultConnection({
|
|
99
|
+
...options,
|
|
100
|
+
refusals: refusalsOf(this.builder.options.orpc?.refusals ?? {}, procedure),
|
|
101
|
+
resolve: (parent, args, context) => callProcedure(procedure, input(args, parent), call(context), output).map((page) => connection(page, args))
|
|
102
|
+
}, connectionType);
|
|
103
|
+
};
|
|
104
|
+
};
|
|
105
|
+
//#endregion
|
|
106
|
+
//#region src/plugin.ts
|
|
107
|
+
const pluginName = "orpc";
|
|
108
|
+
var PothosOrpcPlugin = class extends BasePlugin {};
|
|
109
|
+
SchemaBuilder.registerPlugin(pluginName, PothosOrpcPlugin);
|
|
110
|
+
defineProcedureFields();
|
|
111
|
+
//#endregion
|
|
112
|
+
export { InvalidInput, callProcedure, contractCall, contractInput, contractOutput, pluginName as default, loadProcedure, procedureErrors, refusalsOf };
|
package/package.json
CHANGED
|
@@ -1,6 +1,81 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@btravstack/pothos",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
3
|
+
"version": "0.27.0",
|
|
4
|
+
"description": "Pothos fields that call oRPC procedures, each declared error answered as a typed GraphQL refusal",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"graphql",
|
|
7
|
+
"orpc",
|
|
8
|
+
"pothos",
|
|
9
|
+
"typescript",
|
|
10
|
+
"unthrown"
|
|
11
|
+
],
|
|
12
|
+
"homepage": "https://github.com/btravstack/btravstack#readme",
|
|
13
|
+
"bugs": {
|
|
14
|
+
"url": "https://github.com/btravstack/btravstack/issues"
|
|
15
|
+
},
|
|
16
|
+
"license": "MIT",
|
|
17
|
+
"author": "Benoit TRAVERS <benoit.travers.fr@gmail.com>",
|
|
18
|
+
"repository": {
|
|
19
|
+
"type": "git",
|
|
20
|
+
"url": "https://github.com/btravstack/btravstack.git",
|
|
21
|
+
"directory": "packages/pothos"
|
|
22
|
+
},
|
|
23
|
+
"files": [
|
|
24
|
+
"dist"
|
|
25
|
+
],
|
|
26
|
+
"type": "module",
|
|
27
|
+
"sideEffects": [
|
|
28
|
+
"./dist/index.mjs"
|
|
29
|
+
],
|
|
30
|
+
"main": "./dist/index.mjs",
|
|
31
|
+
"module": "./dist/index.mjs",
|
|
32
|
+
"types": "./dist/index.d.mts",
|
|
33
|
+
"exports": {
|
|
34
|
+
".": {
|
|
35
|
+
"import": {
|
|
36
|
+
"types": "./dist/index.d.mts",
|
|
37
|
+
"default": "./dist/index.mjs"
|
|
38
|
+
}
|
|
39
|
+
},
|
|
40
|
+
"./package.json": "./package.json"
|
|
41
|
+
},
|
|
42
|
+
"devDependencies": {
|
|
43
|
+
"@btravstack/tsconfig": "0.4.0",
|
|
44
|
+
"@orpc/client": "2.0.0-beta.28",
|
|
45
|
+
"@orpc/contract": "2.0.0-beta.28",
|
|
46
|
+
"@pothos/core": "4.15.1",
|
|
47
|
+
"@pothos/plugin-errors": "4.9.3",
|
|
48
|
+
"@pothos/plugin-relay": "4.8.1",
|
|
49
|
+
"@types/node": "26.6.4",
|
|
50
|
+
"@unthrown/pothos": "0.1.0",
|
|
51
|
+
"@unthrown/vitest": "5.12.0",
|
|
52
|
+
"@vitest/coverage-v8": "5.0.3",
|
|
53
|
+
"graphql": "17.0.2",
|
|
54
|
+
"tsdown": "0.23.0",
|
|
55
|
+
"typescript": "7.0.2",
|
|
56
|
+
"typescript-consumer": "npm:typescript@5.9.3",
|
|
57
|
+
"unthrown": "5.12.0",
|
|
58
|
+
"vitest": "5.0.3",
|
|
59
|
+
"zod": "4.6.5"
|
|
60
|
+
},
|
|
61
|
+
"peerDependencies": {
|
|
62
|
+
"@orpc/client": "^2.0.0-beta.28",
|
|
63
|
+
"@orpc/contract": "^2.0.0-beta.28",
|
|
64
|
+
"@pothos/core": "^4.15.0",
|
|
65
|
+
"@pothos/plugin-errors": "^4.9.0",
|
|
66
|
+
"@pothos/plugin-relay": "^4.8.0",
|
|
67
|
+
"@unthrown/pothos": "^0.1.0",
|
|
68
|
+
"graphql": "^16.10.0 || ^17.0.0",
|
|
69
|
+
"unthrown": "^5.12.0",
|
|
70
|
+
"zod": "^4.3.0"
|
|
71
|
+
},
|
|
72
|
+
"engines": {
|
|
73
|
+
"node": ">=22"
|
|
74
|
+
},
|
|
75
|
+
"scripts": {
|
|
76
|
+
"build": "tsdown src/index.ts --format esm --dts --clean",
|
|
77
|
+
"dev": "tsdown src/index.ts --format esm --dts --watch",
|
|
78
|
+
"test": "vitest run --coverage",
|
|
79
|
+
"typecheck": "tsc --noEmit && tsc --noEmit -p tsconfig.test-d.json && node ./node_modules/typescript-consumer/bin/tsc --noEmit -p tsconfig.test-d.json"
|
|
80
|
+
}
|
|
6
81
|
}
|