@nwire/apollo 0.10.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 +103 -0
- package/dist/apollo-interop.d.ts +191 -0
- package/dist/apollo-interop.js +148 -0
- package/dist/index.d.ts +19 -0
- package/dist/index.js +19 -0
- package/package.json +52 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Alex Gefter / 200apps Ltd.
|
|
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
ADDED
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
# @nwire/apollo
|
|
2
|
+
|
|
3
|
+
> Apollo Server (v4) interop — plug Nwire actions into a GraphQL schema as field resolvers; reuse a hand-authored schema you already maintain.
|
|
4
|
+
|
|
5
|
+
## What it is
|
|
6
|
+
|
|
7
|
+
A thin adapter (~60 LOC of real code). `actionResolver(action)` wraps a Nwire `ActionDefinition` so it serves as a GraphQL field resolver. `nwireApolloContext({ runtime })` injects the runtime + a fresh envelope onto every request. `mountNwireOnApollo(...)` bundles the context factory with Nwire-aware error formatting so `defineError` errors surface with stable `extensions.code`.
|
|
8
|
+
|
|
9
|
+
Same migration story Express got: keep the schema and gateway you already have, stop hand-writing resolver bodies that just call services.
|
|
10
|
+
|
|
11
|
+
> A native GraphQL transport (schema generated from actions/queries automatically, subscriptions on top of `@nwire/bus`) is a separate roadmap item. **This package is for teams already running Apollo with a hand-authored schema.** Cross-link: [`@nwire/express`](../nwire-http-express/README.md).
|
|
12
|
+
|
|
13
|
+
## Install
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
pnpm add @nwire/apollo @apollo/server graphql
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
`@apollo/server` (^4) and `graphql` (^16) are peer deps — bring whatever versions your Apollo setup already uses. Apollo v3 is EOL and unsupported.
|
|
20
|
+
|
|
21
|
+
## Quickstart
|
|
22
|
+
|
|
23
|
+
```ts
|
|
24
|
+
import { z } from "zod";
|
|
25
|
+
import { ApolloServer } from "@apollo/server";
|
|
26
|
+
import { startStandaloneServer } from "@apollo/server/standalone";
|
|
27
|
+
import gql from "graphql-tag";
|
|
28
|
+
import { defineAction, Runtime } from "@nwire/forge";
|
|
29
|
+
import { actionResolver, mountNwireOnApollo } from "@nwire/apollo";
|
|
30
|
+
|
|
31
|
+
const submitAnswer = defineAction({
|
|
32
|
+
name: "submissions.submit-answer",
|
|
33
|
+
schema: z.object({ questionId: z.string(), answer: z.string() }),
|
|
34
|
+
handler: async (input, ctx) => ({
|
|
35
|
+
id: "sub-1",
|
|
36
|
+
...input,
|
|
37
|
+
student: ctx.envelope.userId,
|
|
38
|
+
}),
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
const runtime = new Runtime();
|
|
42
|
+
runtime.registerHandler(submitAnswer.handler!);
|
|
43
|
+
|
|
44
|
+
const typeDefs = gql`
|
|
45
|
+
input SubmitAnswerInput {
|
|
46
|
+
questionId: String!
|
|
47
|
+
answer: String!
|
|
48
|
+
}
|
|
49
|
+
type Submission {
|
|
50
|
+
id: ID!
|
|
51
|
+
questionId: String!
|
|
52
|
+
answer: String!
|
|
53
|
+
student: String
|
|
54
|
+
}
|
|
55
|
+
type Mutation {
|
|
56
|
+
submitAnswer(input: SubmitAnswerInput!): Submission!
|
|
57
|
+
}
|
|
58
|
+
type Query {
|
|
59
|
+
_: Boolean
|
|
60
|
+
}
|
|
61
|
+
`;
|
|
62
|
+
|
|
63
|
+
const apollo = new ApolloServer({
|
|
64
|
+
typeDefs,
|
|
65
|
+
resolvers: {
|
|
66
|
+
Mutation: { submitAnswer: actionResolver(submitAnswer) },
|
|
67
|
+
},
|
|
68
|
+
...mountNwireOnApollo({ runtime }),
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
await startStandaloneServer(apollo, { listen: { port: 4000 } });
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
`actionResolver` reads `args.input` by default (matches GraphQL convention for mutation inputs); customise via `extractInput` for query-style flat args. Dispatched actions return their handler value verbatim to the GraphQL response — emit events, no events, return a plain object, whatever the handler does.
|
|
75
|
+
|
|
76
|
+
## API
|
|
77
|
+
|
|
78
|
+
- `actionResolver(action, options?)` — `ActionDefinition` → GraphQL field resolver. Options: `extractInput`, `extractEnvelope`, `resolveRuntime`.
|
|
79
|
+
- `nwireApolloContext({ runtime, extractEnvelope?, extractUser? })` — Apollo `context` factory that injects `runtime` + envelope per request.
|
|
80
|
+
- `mountNwireOnApollo({ runtime, ... })` — convenience bundle: returns `{ context, formatError }` you spread into `ApolloServer` constructor + `startStandaloneServer` options.
|
|
81
|
+
- `formatNwireError(formattedError, rawError)` — standalone `formatError` callback. Use directly if you compose your own error formatter.
|
|
82
|
+
|
|
83
|
+
## Errors
|
|
84
|
+
|
|
85
|
+
When a handler throws a `defineError`-style value (e.g. `QuestionLocked`), GraphQL clients receive:
|
|
86
|
+
|
|
87
|
+
```json
|
|
88
|
+
{
|
|
89
|
+
"errors": [
|
|
90
|
+
{
|
|
91
|
+
"message": "Question is locked for further submissions.",
|
|
92
|
+
"extensions": { "code": "QUESTION_LOCKED", "status": 423 }
|
|
93
|
+
}
|
|
94
|
+
]
|
|
95
|
+
}
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
Same stable `code` the REST transport emits — clients can branch on `extensions.code` and ignore wire format.
|
|
99
|
+
|
|
100
|
+
## See also
|
|
101
|
+
|
|
102
|
+
- [`@nwire/express`](../nwire-http-express/README.md) — same architectural pattern for Express interop.
|
|
103
|
+
- [`docs/recipes/apollo-interop.md`](../../docs/recipes/apollo-interop.md) — full recipe with auth header extraction + per-resolver overrides.
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `@nwire/apollo` — Apollo Server (v4) interop, adapter form.
|
|
3
|
+
*
|
|
4
|
+
* Three exports, each tiny:
|
|
5
|
+
*
|
|
6
|
+
* - `actionResolver(action, options?)` — wraps a Nwire `ActionDefinition`
|
|
7
|
+
* so it can be plugged into a GraphQL schema as a field resolver. The
|
|
8
|
+
* returned function reads input from `args`, builds an envelope from
|
|
9
|
+
* Apollo's per-request context (tenant / user — caller-controlled via
|
|
10
|
+
* `extractEnvelope`), dispatches via the runtime found on the GraphQL
|
|
11
|
+
* context, and returns the action result.
|
|
12
|
+
*
|
|
13
|
+
* - `nwireApolloContext({ runtime })` — Apollo `context` callback factory.
|
|
14
|
+
* Returns a function that injects `runtime` + a freshly-seeded envelope
|
|
15
|
+
* onto every request's GraphQL context. Resolvers built by
|
|
16
|
+
* `actionResolver` read both off this context; user-authored resolvers
|
|
17
|
+
* can use `ctx.runtime.dispatch(...)` directly.
|
|
18
|
+
*
|
|
19
|
+
* - `mountNwireOnApollo({ apollo, runtime })` — convenience constructor
|
|
20
|
+
* options builder. Returns the option bag (`context`, `formatError`)
|
|
21
|
+
* you spread into Apollo's HTTP integration (e.g.
|
|
22
|
+
* `startStandaloneServer(server, mountNwireOnApollo({...}))`). The
|
|
23
|
+
* `formatError` step recognises `NwireError` (from `defineError`) and
|
|
24
|
+
* surfaces its `code` in `extensions.code`, mapping to the stable error
|
|
25
|
+
* contract the rest of the framework already exposes over REST.
|
|
26
|
+
*
|
|
27
|
+
* The whole package is ~60 LOC of real code. Same architectural invariant
|
|
28
|
+
* as `@nwire/express`: with the Runtime as the universal dispatch
|
|
29
|
+
* joint, the adapter is plumbing — no shadow execution, no proxy, no
|
|
30
|
+
* translation layer. An action stays the source of truth; Apollo is just
|
|
31
|
+
* one more way to invoke it.
|
|
32
|
+
*
|
|
33
|
+
* ## Why an adapter, not a transport
|
|
34
|
+
*
|
|
35
|
+
* `@nwire/graphql` ships a native GraphQL transport — schema generated
|
|
36
|
+
* from actions/queries automatically, with subscriptions on top of
|
|
37
|
+
* `@nwire/bus`. That story is for greenfield Nwire services that pick
|
|
38
|
+
* GraphQL as their primary interface.
|
|
39
|
+
*
|
|
40
|
+
* This package is for the OTHER case: a team already running Apollo with
|
|
41
|
+
* a hand-authored schema. They want to keep that schema (and their gateway,
|
|
42
|
+
* persisted queries, federation, cache hints — everything) and just stop
|
|
43
|
+
* writing boilerplate resolvers. `actionResolver(submitAnswer)` is the
|
|
44
|
+
* one-liner that says "this GraphQL field is implemented by this Nwire
|
|
45
|
+
* action" — same migration story Express got.
|
|
46
|
+
*/
|
|
47
|
+
import type { ActionDefinition, ForgeApp } from "@nwire/forge";
|
|
48
|
+
import { type MessageEnvelope } from "@nwire/envelope";
|
|
49
|
+
import type { ApolloServerOptions, BaseContext } from "@apollo/server";
|
|
50
|
+
import type { GraphQLFormattedError } from "graphql";
|
|
51
|
+
/**
|
|
52
|
+
* The minimal GraphQL context surface that Nwire-aware resolvers expect.
|
|
53
|
+
* `nwireApolloContext` produces exactly this shape; user-extended contexts
|
|
54
|
+
* should intersect it (`type MyCtx = NwireApolloContext & { …app fields }`).
|
|
55
|
+
*/
|
|
56
|
+
export interface NwireApolloContext extends BaseContext {
|
|
57
|
+
/** The Nwire app to dispatch actions through. */
|
|
58
|
+
readonly app: ForgeApp;
|
|
59
|
+
/**
|
|
60
|
+
* The fresh envelope seeded for this GraphQL request. Resolvers can
|
|
61
|
+
* dispatch through `runtime` and the envelope will be derived as a
|
|
62
|
+
* child of this one, so the entire request stays correlated.
|
|
63
|
+
*/
|
|
64
|
+
readonly envelope: MessageEnvelope;
|
|
65
|
+
/**
|
|
66
|
+
* Authenticated principal, if any. Free-form `unknown` here — the
|
|
67
|
+
* upstream integration (an Apollo plugin / @nwire/auth-better-auth
|
|
68
|
+
* verifier / a custom middleware) is responsible for typing it.
|
|
69
|
+
*/
|
|
70
|
+
readonly user?: unknown;
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Options for `actionResolver`. All optional.
|
|
74
|
+
*
|
|
75
|
+
* - `extractInput` — how to derive the action input from `args`. Default:
|
|
76
|
+
* pull `args.input` if present, else the whole `args` object. The
|
|
77
|
+
* action's zod schema is the source of truth either way (`runtime.
|
|
78
|
+
* dispatch` re-validates), so callers only need this to flatten
|
|
79
|
+
* non-standard arg shapes.
|
|
80
|
+
*
|
|
81
|
+
* - `extractEnvelope` — how to derive envelope overrides (tenant, userId,
|
|
82
|
+
* user) from the GraphQL context. Default: read `context.envelope` if
|
|
83
|
+
* present, else seed a fresh envelope from `context.user`. Override to
|
|
84
|
+
* plug in your own auth shape (`ctx.session.userId`, `ctx.tenant`, etc).
|
|
85
|
+
*/
|
|
86
|
+
export interface ActionResolverOptions<TContext extends BaseContext = NwireApolloContext> {
|
|
87
|
+
readonly extractInput?: (args: Record<string, unknown>, context: TContext) => unknown;
|
|
88
|
+
readonly extractEnvelope?: (context: TContext) => MessageEnvelope;
|
|
89
|
+
/**
|
|
90
|
+
* How to find the app. Defaults to `context.app`. Provide this if
|
|
91
|
+
* your context shape names the app differently or pulls it from a
|
|
92
|
+
* DI container.
|
|
93
|
+
*/
|
|
94
|
+
readonly resolveApp?: (context: TContext) => ForgeApp;
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* Wrap a Nwire `ActionDefinition` as a GraphQL field resolver.
|
|
98
|
+
*
|
|
99
|
+
* ```ts
|
|
100
|
+
* import { actionResolver, nwireApolloContext } from "@nwire/apollo"
|
|
101
|
+
*
|
|
102
|
+
* const resolvers = {
|
|
103
|
+
* Mutation: {
|
|
104
|
+
* submitAnswer: actionResolver(submitAnswer),
|
|
105
|
+
* },
|
|
106
|
+
* }
|
|
107
|
+
*
|
|
108
|
+
* const apollo = new ApolloServer({ typeDefs, resolvers })
|
|
109
|
+
* await startStandaloneServer(apollo, {
|
|
110
|
+
* context: nwireApolloContext({ runtime }),
|
|
111
|
+
* })
|
|
112
|
+
* ```
|
|
113
|
+
*
|
|
114
|
+
* The resolver signature returned matches Apollo's standard
|
|
115
|
+
* `(parent, args, context, info)` — it's a plain function. Errors thrown
|
|
116
|
+
* by the handler (including `defineError` values) propagate to Apollo as
|
|
117
|
+
* normal; pair with `mountNwireOnApollo`'s `formatError` to surface the
|
|
118
|
+
* stable error code in `extensions.code`.
|
|
119
|
+
*/
|
|
120
|
+
export declare function actionResolver<TContext extends NwireApolloContext = NwireApolloContext>(action: ActionDefinition, options?: ActionResolverOptions<TContext>): (parent: unknown, args: Record<string, unknown>, context: TContext) => Promise<unknown>;
|
|
121
|
+
/**
|
|
122
|
+
* Build an Apollo `context` callback that injects `runtime` + a fresh
|
|
123
|
+
* envelope onto every request's GraphQL context.
|
|
124
|
+
*
|
|
125
|
+
* ```ts
|
|
126
|
+
* await startStandaloneServer(apollo, {
|
|
127
|
+
* context: nwireApolloContext({
|
|
128
|
+
* runtime,
|
|
129
|
+
* // Optional: derive tenant + user from the HTTP request headers.
|
|
130
|
+
* extractEnvelope: ({ req }) => seedEnvelope({
|
|
131
|
+
* tenant: req.headers["x-tenant-id"] as string | undefined,
|
|
132
|
+
* userId: req.headers["x-user-id"] as string | undefined,
|
|
133
|
+
* }),
|
|
134
|
+
* }),
|
|
135
|
+
* })
|
|
136
|
+
* ```
|
|
137
|
+
*
|
|
138
|
+
* Generic over the integration-specific arg shape Apollo passes to the
|
|
139
|
+
* context callback (e.g. `{ req, res }` for express middleware,
|
|
140
|
+
* `{ req }` for the standalone server). Default `unknown` keeps the
|
|
141
|
+
* type usable in any integration without a forced cast.
|
|
142
|
+
*/
|
|
143
|
+
export interface NwireApolloContextOptions<TArgs = unknown> {
|
|
144
|
+
readonly app: ForgeApp;
|
|
145
|
+
/**
|
|
146
|
+
* Build a request-scoped envelope. The integration arg (`req`, headers,
|
|
147
|
+
* etc.) is available so caller can read auth headers. Defaults to a
|
|
148
|
+
* fresh seeded envelope with no tenant / user — fine for unauth dev
|
|
149
|
+
* workflows.
|
|
150
|
+
*/
|
|
151
|
+
readonly extractEnvelope?: (args: TArgs) => MessageEnvelope;
|
|
152
|
+
/** Build an arbitrary `user` value alongside the envelope. */
|
|
153
|
+
readonly extractUser?: (args: TArgs) => unknown;
|
|
154
|
+
}
|
|
155
|
+
export declare function nwireApolloContext<TArgs = unknown>(options: NwireApolloContextOptions<TArgs>): (args: TArgs) => Promise<NwireApolloContext>;
|
|
156
|
+
/**
|
|
157
|
+
* Build the Apollo Server constructor options bundle that registers Nwire
|
|
158
|
+
* error formatting. Spread the result into `ApolloServerOptions` (or use
|
|
159
|
+
* the individual exports `formatNwireError` / `nwireApolloContext`
|
|
160
|
+
* directly).
|
|
161
|
+
*
|
|
162
|
+
* ```ts
|
|
163
|
+
* const apollo = new ApolloServer({
|
|
164
|
+
* typeDefs,
|
|
165
|
+
* resolvers,
|
|
166
|
+
* ...mountNwireOnApollo({ runtime }),
|
|
167
|
+
* })
|
|
168
|
+
* ```
|
|
169
|
+
*
|
|
170
|
+
* What it sets up:
|
|
171
|
+
* - `formatError` — recognises `NwireError` (`defineError` instances)
|
|
172
|
+
* and surfaces `code` in `extensions.code`, plus `status` and `tags`
|
|
173
|
+
* for clients that care. Non-Nwire errors pass through unchanged.
|
|
174
|
+
* - The matching `context` factory you'd pass to your HTTP integration
|
|
175
|
+
* is included as `context` for convenience; pull it out and pass to
|
|
176
|
+
* `startStandaloneServer` / `expressMiddleware` as appropriate.
|
|
177
|
+
*/
|
|
178
|
+
export interface MountNwireOptions<TArgs = unknown> {
|
|
179
|
+
readonly app: ForgeApp;
|
|
180
|
+
readonly extractEnvelope?: (args: TArgs) => MessageEnvelope;
|
|
181
|
+
readonly extractUser?: (args: TArgs) => unknown;
|
|
182
|
+
}
|
|
183
|
+
export declare function mountNwireOnApollo<TArgs = unknown>(options: MountNwireOptions<TArgs>): Pick<ApolloServerOptions<NwireApolloContext>, "formatError"> & {
|
|
184
|
+
context: (args: TArgs) => Promise<NwireApolloContext>;
|
|
185
|
+
};
|
|
186
|
+
/**
|
|
187
|
+
* Apollo `formatError` callback that maps `NwireError` instances onto
|
|
188
|
+
* GraphQL error extensions. Exposed standalone so consumers using their
|
|
189
|
+
* own custom `formatError` can compose it.
|
|
190
|
+
*/
|
|
191
|
+
export declare function formatNwireError(formattedError: GraphQLFormattedError, rawError: unknown): GraphQLFormattedError;
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `@nwire/apollo` — Apollo Server (v4) interop, adapter form.
|
|
3
|
+
*
|
|
4
|
+
* Three exports, each tiny:
|
|
5
|
+
*
|
|
6
|
+
* - `actionResolver(action, options?)` — wraps a Nwire `ActionDefinition`
|
|
7
|
+
* so it can be plugged into a GraphQL schema as a field resolver. The
|
|
8
|
+
* returned function reads input from `args`, builds an envelope from
|
|
9
|
+
* Apollo's per-request context (tenant / user — caller-controlled via
|
|
10
|
+
* `extractEnvelope`), dispatches via the runtime found on the GraphQL
|
|
11
|
+
* context, and returns the action result.
|
|
12
|
+
*
|
|
13
|
+
* - `nwireApolloContext({ runtime })` — Apollo `context` callback factory.
|
|
14
|
+
* Returns a function that injects `runtime` + a freshly-seeded envelope
|
|
15
|
+
* onto every request's GraphQL context. Resolvers built by
|
|
16
|
+
* `actionResolver` read both off this context; user-authored resolvers
|
|
17
|
+
* can use `ctx.runtime.dispatch(...)` directly.
|
|
18
|
+
*
|
|
19
|
+
* - `mountNwireOnApollo({ apollo, runtime })` — convenience constructor
|
|
20
|
+
* options builder. Returns the option bag (`context`, `formatError`)
|
|
21
|
+
* you spread into Apollo's HTTP integration (e.g.
|
|
22
|
+
* `startStandaloneServer(server, mountNwireOnApollo({...}))`). The
|
|
23
|
+
* `formatError` step recognises `NwireError` (from `defineError`) and
|
|
24
|
+
* surfaces its `code` in `extensions.code`, mapping to the stable error
|
|
25
|
+
* contract the rest of the framework already exposes over REST.
|
|
26
|
+
*
|
|
27
|
+
* The whole package is ~60 LOC of real code. Same architectural invariant
|
|
28
|
+
* as `@nwire/express`: with the Runtime as the universal dispatch
|
|
29
|
+
* joint, the adapter is plumbing — no shadow execution, no proxy, no
|
|
30
|
+
* translation layer. An action stays the source of truth; Apollo is just
|
|
31
|
+
* one more way to invoke it.
|
|
32
|
+
*
|
|
33
|
+
* ## Why an adapter, not a transport
|
|
34
|
+
*
|
|
35
|
+
* `@nwire/graphql` ships a native GraphQL transport — schema generated
|
|
36
|
+
* from actions/queries automatically, with subscriptions on top of
|
|
37
|
+
* `@nwire/bus`. That story is for greenfield Nwire services that pick
|
|
38
|
+
* GraphQL as their primary interface.
|
|
39
|
+
*
|
|
40
|
+
* This package is for the OTHER case: a team already running Apollo with
|
|
41
|
+
* a hand-authored schema. They want to keep that schema (and their gateway,
|
|
42
|
+
* persisted queries, federation, cache hints — everything) and just stop
|
|
43
|
+
* writing boilerplate resolvers. `actionResolver(submitAnswer)` is the
|
|
44
|
+
* one-liner that says "this GraphQL field is implemented by this Nwire
|
|
45
|
+
* action" — same migration story Express got.
|
|
46
|
+
*/
|
|
47
|
+
import { isNwireError } from "@nwire/forge";
|
|
48
|
+
import { seedEnvelope } from "@nwire/envelope";
|
|
49
|
+
/**
|
|
50
|
+
* Wrap a Nwire `ActionDefinition` as a GraphQL field resolver.
|
|
51
|
+
*
|
|
52
|
+
* ```ts
|
|
53
|
+
* import { actionResolver, nwireApolloContext } from "@nwire/apollo"
|
|
54
|
+
*
|
|
55
|
+
* const resolvers = {
|
|
56
|
+
* Mutation: {
|
|
57
|
+
* submitAnswer: actionResolver(submitAnswer),
|
|
58
|
+
* },
|
|
59
|
+
* }
|
|
60
|
+
*
|
|
61
|
+
* const apollo = new ApolloServer({ typeDefs, resolvers })
|
|
62
|
+
* await startStandaloneServer(apollo, {
|
|
63
|
+
* context: nwireApolloContext({ runtime }),
|
|
64
|
+
* })
|
|
65
|
+
* ```
|
|
66
|
+
*
|
|
67
|
+
* The resolver signature returned matches Apollo's standard
|
|
68
|
+
* `(parent, args, context, info)` — it's a plain function. Errors thrown
|
|
69
|
+
* by the handler (including `defineError` values) propagate to Apollo as
|
|
70
|
+
* normal; pair with `mountNwireOnApollo`'s `formatError` to surface the
|
|
71
|
+
* stable error code in `extensions.code`.
|
|
72
|
+
*/
|
|
73
|
+
export function actionResolver(action, options = {}) {
|
|
74
|
+
const extractInput = options.extractInput ??
|
|
75
|
+
((args) =>
|
|
76
|
+
// GraphQL convention: mutation inputs land under `input`. Fall back
|
|
77
|
+
// to the whole args object so query-style resolvers (where each arg
|
|
78
|
+
// is named separately) work without configuration.
|
|
79
|
+
"input" in args ? args.input : args);
|
|
80
|
+
const resolveApp = options.resolveApp ?? ((ctx) => ctx.app);
|
|
81
|
+
const extractEnvelope = options.extractEnvelope ??
|
|
82
|
+
((ctx) =>
|
|
83
|
+
// If `nwireApolloContext` is used the envelope is already on the
|
|
84
|
+
// context; otherwise fall back to seeding one from the loose user
|
|
85
|
+
// field. Either way `runtime.dispatch` derives a child envelope for
|
|
86
|
+
// the actual action, so this just sets the root of the chain.
|
|
87
|
+
ctx.envelope ?? seedEnvelope({ user: ctx.user }));
|
|
88
|
+
return async (_parent, args, context) => {
|
|
89
|
+
const app = resolveApp(context);
|
|
90
|
+
const envelope = extractEnvelope(context);
|
|
91
|
+
const input = extractInput(args, context);
|
|
92
|
+
return app.dispatch(action, input, envelope);
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
export function nwireApolloContext(options) {
|
|
96
|
+
const { app, extractEnvelope, extractUser } = options;
|
|
97
|
+
return async (args) => {
|
|
98
|
+
const user = extractUser?.(args);
|
|
99
|
+
const envelope = extractEnvelope?.(args) ?? seedEnvelope(user !== undefined ? { user } : {});
|
|
100
|
+
return { app, envelope, user };
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
export function mountNwireOnApollo(options) {
|
|
104
|
+
return {
|
|
105
|
+
formatError: formatNwireError,
|
|
106
|
+
context: nwireApolloContext({
|
|
107
|
+
app: options.app,
|
|
108
|
+
extractEnvelope: options.extractEnvelope,
|
|
109
|
+
extractUser: options.extractUser,
|
|
110
|
+
}),
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* Apollo `formatError` callback that maps `NwireError` instances onto
|
|
115
|
+
* GraphQL error extensions. Exposed standalone so consumers using their
|
|
116
|
+
* own custom `formatError` can compose it.
|
|
117
|
+
*/
|
|
118
|
+
export function formatNwireError(formattedError, rawError) {
|
|
119
|
+
// Apollo wraps the thrown error inside GraphQLError.originalError —
|
|
120
|
+
// we accept it on the raw error or one level deep.
|
|
121
|
+
const candidate = pickNwireError(rawError);
|
|
122
|
+
if (!candidate)
|
|
123
|
+
return formattedError;
|
|
124
|
+
return {
|
|
125
|
+
...formattedError,
|
|
126
|
+
message: candidate.summary,
|
|
127
|
+
extensions: {
|
|
128
|
+
...formattedError.extensions,
|
|
129
|
+
code: candidate.code,
|
|
130
|
+
status: candidate.status,
|
|
131
|
+
...(candidate.tags ? { tags: candidate.tags } : {}),
|
|
132
|
+
},
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
/**
|
|
136
|
+
* Look at the value Apollo handed `formatError` and return a `NwireError`
|
|
137
|
+
* if there's one to find — either directly or as the wrapped
|
|
138
|
+
* `originalError` of a `GraphQLError`. Returns `undefined` otherwise so
|
|
139
|
+
* callers can fall through to default formatting.
|
|
140
|
+
*/
|
|
141
|
+
function pickNwireError(raw) {
|
|
142
|
+
if (isNwireError(raw))
|
|
143
|
+
return raw;
|
|
144
|
+
const wrapped = raw?.originalError;
|
|
145
|
+
if (isNwireError(wrapped))
|
|
146
|
+
return wrapped;
|
|
147
|
+
return undefined;
|
|
148
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `@nwire/apollo` — Apollo Server (v4) interop adapter.
|
|
3
|
+
*
|
|
4
|
+
* Three exports cover the surface:
|
|
5
|
+
*
|
|
6
|
+
* - `actionResolver(action)` — wrap a Nwire ActionDefinition as a GraphQL
|
|
7
|
+
* field resolver. The action stays the source of truth; Apollo gets a
|
|
8
|
+
* thin resolver that dispatches through the runtime.
|
|
9
|
+
*
|
|
10
|
+
* - `nwireApolloContext({ runtime, ... })` — Apollo `context` factory
|
|
11
|
+
* that injects `runtime` + a fresh envelope onto every request.
|
|
12
|
+
*
|
|
13
|
+
* - `mountNwireOnApollo({ runtime, ... })` — convenience bundle: pairs
|
|
14
|
+
* the context factory with Nwire-aware error formatting so
|
|
15
|
+
* `defineError` values surface with stable `extensions.code`.
|
|
16
|
+
*
|
|
17
|
+
* See the README + `docs/recipes/apollo-interop.md` for the quickstart.
|
|
18
|
+
*/
|
|
19
|
+
export { actionResolver, formatNwireError, mountNwireOnApollo, nwireApolloContext, type ActionResolverOptions, type MountNwireOptions, type NwireApolloContext, type NwireApolloContextOptions, } from "./apollo-interop.js";
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `@nwire/apollo` — Apollo Server (v4) interop adapter.
|
|
3
|
+
*
|
|
4
|
+
* Three exports cover the surface:
|
|
5
|
+
*
|
|
6
|
+
* - `actionResolver(action)` — wrap a Nwire ActionDefinition as a GraphQL
|
|
7
|
+
* field resolver. The action stays the source of truth; Apollo gets a
|
|
8
|
+
* thin resolver that dispatches through the runtime.
|
|
9
|
+
*
|
|
10
|
+
* - `nwireApolloContext({ runtime, ... })` — Apollo `context` factory
|
|
11
|
+
* that injects `runtime` + a fresh envelope onto every request.
|
|
12
|
+
*
|
|
13
|
+
* - `mountNwireOnApollo({ runtime, ... })` — convenience bundle: pairs
|
|
14
|
+
* the context factory with Nwire-aware error formatting so
|
|
15
|
+
* `defineError` values surface with stable `extensions.code`.
|
|
16
|
+
*
|
|
17
|
+
* See the README + `docs/recipes/apollo-interop.md` for the quickstart.
|
|
18
|
+
*/
|
|
19
|
+
export { actionResolver, formatNwireError, mountNwireOnApollo, nwireApolloContext, } from "./apollo-interop.js";
|
package/package.json
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@nwire/apollo",
|
|
3
|
+
"version": "0.10.0",
|
|
4
|
+
"description": "Nwire — Apollo Server (v4) interop adapter. actionResolver(action) plugs a Nwire ActionDefinition into a GraphQL schema as a field resolver; nwireApolloContext({ runtime }) adds runtime.dispatch + envelope to Apollo's per-request context; mountNwireOnApollo() registers Nwire-aware error mapping (defineError -> extensions.code). Opt-in, peer dep, ~60 LOC of real code.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"adapter",
|
|
7
|
+
"apollo",
|
|
8
|
+
"graphql",
|
|
9
|
+
"interop",
|
|
10
|
+
"nwire"
|
|
11
|
+
],
|
|
12
|
+
"license": "MIT",
|
|
13
|
+
"files": [
|
|
14
|
+
"dist",
|
|
15
|
+
"README.md",
|
|
16
|
+
"LICENSE"
|
|
17
|
+
],
|
|
18
|
+
"type": "module",
|
|
19
|
+
"main": "./dist/index.js",
|
|
20
|
+
"types": "./dist/index.d.ts",
|
|
21
|
+
"exports": {
|
|
22
|
+
".": {
|
|
23
|
+
"import": "./dist/index.js",
|
|
24
|
+
"types": "./dist/index.d.ts"
|
|
25
|
+
}
|
|
26
|
+
},
|
|
27
|
+
"publishConfig": {
|
|
28
|
+
"access": "public"
|
|
29
|
+
},
|
|
30
|
+
"dependencies": {
|
|
31
|
+
"@nwire/forge": "0.10.0",
|
|
32
|
+
"@nwire/envelope": "0.10.0"
|
|
33
|
+
},
|
|
34
|
+
"devDependencies": {
|
|
35
|
+
"@apollo/server": "^4.11.0",
|
|
36
|
+
"@types/node": "^22.19.9",
|
|
37
|
+
"graphql": "^16.9.0",
|
|
38
|
+
"graphql-tag": "^2.12.6",
|
|
39
|
+
"typescript": "^5.9.3",
|
|
40
|
+
"vitest": "^4.0.18",
|
|
41
|
+
"zod": "^4.0.0"
|
|
42
|
+
},
|
|
43
|
+
"peerDependencies": {
|
|
44
|
+
"@apollo/server": "^4.0.0",
|
|
45
|
+
"graphql": "^16.0.0"
|
|
46
|
+
},
|
|
47
|
+
"scripts": {
|
|
48
|
+
"build": "tsc && node ../../scripts/fix-dist-extensions.mjs dist",
|
|
49
|
+
"dev": "tsc --watch",
|
|
50
|
+
"typecheck": "tsc --noEmit"
|
|
51
|
+
}
|
|
52
|
+
}
|