Skip to content

Commit 4edbe04

Browse files
committed
fix(yoga): use the onContextBuilding hook
Use the onContextBuilding to load the data into an existing federated token instead of the onRequest variant which created one if it didnt exist. This moves the initial creation of the federated token to the implementation side and thus more straightforward
1 parent cb1ec98 commit 4edbe04

File tree

4 files changed

+167
-17
lines changed

4 files changed

+167
-17
lines changed

.changeset/fluffy-rivers-explode.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@labdigital/federated-token-yoga": minor
3+
---
4+
5+
Require the FederatedToken to already exist on the context

.eslintrc.cjs

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,6 @@ module.exports = {
1212
},
1313
],
1414
"unused-imports/no-unused-imports": "error",
15-
"arrow-body-style": ["error", "as-needed"],
1615
"no-console": [
1716
"error",
1817
{

packages/yoga/src/index.test.ts

Lines changed: 146 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,146 @@
1+
import { createSchema, createYoga } from "graphql-yoga";
2+
import { describe, it, expect } from "vitest";
3+
import { useFederatedToken } from ".";
4+
import { FederatedToken } from "@labdigital/federated-token";
5+
6+
type FederatedTokenContext = {
7+
federatedToken: FederatedToken;
8+
};
9+
10+
export function assertSingleValue<TValue extends object>(
11+
value: TValue | AsyncIterable<TValue>,
12+
): asserts value is TValue {
13+
if (Symbol.asyncIterator in value) {
14+
throw new Error("Expected single value");
15+
}
16+
}
17+
18+
19+
describe("useFederatdToken()", () => {
20+
const schemaFactory = async () => {
21+
return createSchema<FederatedTokenContext>({
22+
typeDefs: /* GraphQL */ `
23+
type Query {
24+
tokenData: String
25+
}
26+
type Mutation {
27+
createToken(service: String!): Boolean
28+
}
29+
`,
30+
resolvers: {
31+
Query: {
32+
tokenData: (parent, args, context) => {
33+
return JSON.stringify({
34+
tokens: context.federatedToken.tokens,
35+
values: context.federatedToken.values,
36+
refreshTokens: context.federatedToken.refreshTokens,
37+
});
38+
},
39+
},
40+
Mutation: {
41+
createToken: (parent, args, context) => {
42+
context.federatedToken.setAccessToken(args.service, {
43+
token: "test",
44+
exp: 123,
45+
sub: "test",
46+
});
47+
context.federatedToken.setRefreshToken(args.service, "refresh-token");
48+
return true;
49+
},
50+
}
51+
},
52+
});
53+
};
54+
55+
const yoga = createYoga<FederatedTokenContext>({
56+
schema: schemaFactory,
57+
plugins: [useFederatedToken()],
58+
context: () => {
59+
return {
60+
federatedToken: new FederatedToken(),
61+
};
62+
},
63+
});
64+
65+
it("should handle missing tokens", async () => {
66+
const response = await yoga.fetch("http://localhost:3000/graphql", {
67+
method: "POST",
68+
headers: {
69+
"Content-Type": "application/json",
70+
},
71+
body: JSON.stringify({
72+
query: "{ tokenData }",
73+
}),
74+
});
75+
const content = await response.json();
76+
assertSingleValue(content);
77+
});
78+
79+
it("should load access and refresh tokens", async () => {
80+
const token = new FederatedToken();
81+
token.setValue("my-service", { foo: "bar" });
82+
token.setAccessToken("my-service", { token: "bar", exp: 123, sub: "test" });
83+
token.setRefreshToken("my-service", "refresh-token");
84+
85+
const response = await yoga.fetch("http://localhost:3000/graphql", {
86+
method: "POST",
87+
headers: {
88+
"Content-Type": "application/json",
89+
"X-Access-Token": token.serializeAccessToken() ?? "",
90+
"X-Refresh-Token": token.dumpRefreshToken() ?? "",
91+
},
92+
body: JSON.stringify({
93+
query: "{ tokenData }",
94+
}),
95+
});
96+
const content = await response.json();
97+
98+
assertSingleValue(content);
99+
100+
const result = JSON.parse(content.data.tokenData);
101+
expect(result).toEqual({
102+
tokens: {
103+
"my-service": {
104+
token: "bar",
105+
exp: 123,
106+
sub: "test",
107+
},
108+
},
109+
values: {
110+
"my-service": {
111+
foo: "bar",
112+
},
113+
},
114+
refreshTokens: {
115+
"my-service": "refresh-token",
116+
},
117+
});
118+
119+
expect(response.headers.has("X-Access-Token")).toBe(false);
120+
expect(response.headers.has("X-Refresh-Token")).toBe(false);
121+
});
122+
123+
it("should create new token", async () => {
124+
const response = await yoga.fetch("http://localhost:3000/graphql", {
125+
method: "POST",
126+
headers: {
127+
"Content-Type": "application/json",
128+
},
129+
body: JSON.stringify({
130+
query: `mutation { createToken(service: "foobar") }`,
131+
}),
132+
});
133+
const content = await response.json();
134+
assertSingleValue(content);
135+
136+
expect(response.headers.has("X-Access-Token")).toBe(true);
137+
expect(response.headers.has("X-Refresh-Token")).toBe(true);
138+
139+
const accessToken = response.headers.get("X-Access-Token");
140+
const refreshToken = response.headers.get("X-Refresh-Token");
141+
142+
expect(accessToken).toBeDefined();
143+
expect(refreshToken).toBeDefined();
144+
});
145+
146+
});

packages/yoga/src/index.ts

Lines changed: 16 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -6,27 +6,27 @@ type FederatedTokenContext = {
66
};
77

88
export const useFederatedToken = <T extends FederatedTokenContext>(): Plugin<
9-
object,
9+
T,
1010
T
1111
> => ({
12-
onRequest: ({ request, serverContext }) => {
13-
// Initialize FederatedToken and add it to the context
14-
const federatedToken = serverContext.federatedToken ?? new FederatedToken();
15-
16-
// Retrieve tokens from headers using the serverContext
17-
const accessToken = request.headers.get("x-access-token") as string;
18-
const refreshToken = request.headers.get("x-refresh-token") as string;
19-
20-
if (accessToken) {
21-
federatedToken.deserializeAccessToken(accessToken);
12+
onContextBuilding: ({ context, extendContext }) => {
13+
if (!context.federatedToken) {
14+
throw new Error("Federated token not found in context");
2215
}
23-
24-
if (refreshToken) {
25-
federatedToken.loadRefreshToken(refreshToken);
16+
if (!context.request) {
17+
throw new Error("Request not found in context");
2618
}
2719

28-
serverContext.federatedToken = federatedToken;
29-
},
20+
const {request, federatedToken } = context
21+
const accessToken = request.headers.get("x-access-token");
22+
const refreshToken = request.headers.get("x-refresh-token");
23+
if (accessToken) {
24+
federatedToken.deserializeAccessToken(accessToken);
25+
}
26+
if (refreshToken) {
27+
federatedToken.loadRefreshToken(refreshToken);
28+
}
29+
},
3030

3131
onResponse: async ({ response, serverContext }) => {
3232
const { federatedToken } = serverContext;

0 commit comments

Comments
 (0)