From c124ad25b67582df8423ffb03f2b092348cef89f Mon Sep 17 00:00:00 2001 From: Michael van Tellingen Date: Mon, 2 Dec 2024 08:08:50 +0100 Subject: [PATCH] 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 --- .changeset/fluffy-rivers-explode.md | 5 + .eslintrc.cjs | 1 - packages/yoga/src/index.test.ts | 147 ++++++++++++++++++++++++++++ packages/yoga/src/index.ts | 22 ++--- 4 files changed, 163 insertions(+), 12 deletions(-) create mode 100644 .changeset/fluffy-rivers-explode.md create mode 100644 packages/yoga/src/index.test.ts diff --git a/.changeset/fluffy-rivers-explode.md b/.changeset/fluffy-rivers-explode.md new file mode 100644 index 0000000..c7bee30 --- /dev/null +++ b/.changeset/fluffy-rivers-explode.md @@ -0,0 +1,5 @@ +--- +"@labdigital/federated-token-yoga": minor +--- + +Require the FederatedToken to already exist on the context diff --git a/.eslintrc.cjs b/.eslintrc.cjs index b0cf6b4..f2f87af 100644 --- a/.eslintrc.cjs +++ b/.eslintrc.cjs @@ -12,7 +12,6 @@ module.exports = { }, ], "unused-imports/no-unused-imports": "error", - "arrow-body-style": ["error", "as-needed"], "no-console": [ "error", { diff --git a/packages/yoga/src/index.test.ts b/packages/yoga/src/index.test.ts new file mode 100644 index 0000000..5008428 --- /dev/null +++ b/packages/yoga/src/index.test.ts @@ -0,0 +1,147 @@ +import { createSchema, createYoga } from "graphql-yoga"; +import { describe, it, expect } from "vitest"; +import { useFederatedToken } from "."; +import { FederatedToken } from "@labdigital/federated-token"; + +type FederatedTokenContext = { + federatedToken: FederatedToken; +}; + +export function assertSingleValue( + value: TValue | AsyncIterable, +): asserts value is TValue { + if (Symbol.asyncIterator in value) { + throw new Error("Expected single value"); + } +} + +describe("useFederatdToken()", () => { + const schemaFactory = async () => { + return createSchema({ + typeDefs: /* GraphQL */ ` + type Query { + tokenData: String + } + type Mutation { + createToken(service: String!): Boolean + } + `, + resolvers: { + Query: { + tokenData: (parent, args, context) => { + return JSON.stringify({ + tokens: context.federatedToken.tokens, + values: context.federatedToken.values, + refreshTokens: context.federatedToken.refreshTokens, + }); + }, + }, + Mutation: { + createToken: (parent, args, context) => { + context.federatedToken.setAccessToken(args.service, { + token: "test", + exp: 123, + sub: "test", + }); + context.federatedToken.setRefreshToken( + args.service, + "refresh-token", + ); + return true; + }, + }, + }, + }); + }; + + const yoga = createYoga({ + schema: schemaFactory, + plugins: [useFederatedToken()], + context: () => { + return { + federatedToken: new FederatedToken(), + }; + }, + }); + + it("should handle missing tokens", async () => { + const response = await yoga.fetch("http://localhost:3000/graphql", { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ + query: "{ tokenData }", + }), + }); + const content = await response.json(); + assertSingleValue(content); + }); + + it("should load access and refresh tokens", async () => { + const token = new FederatedToken(); + token.setValue("my-service", { foo: "bar" }); + token.setAccessToken("my-service", { token: "bar", exp: 123, sub: "test" }); + token.setRefreshToken("my-service", "refresh-token"); + + const response = await yoga.fetch("http://localhost:3000/graphql", { + method: "POST", + headers: { + "Content-Type": "application/json", + "X-Access-Token": token.serializeAccessToken() ?? "", + "X-Refresh-Token": token.dumpRefreshToken() ?? "", + }, + body: JSON.stringify({ + query: "{ tokenData }", + }), + }); + const content = await response.json(); + + assertSingleValue(content); + + const result = JSON.parse(content.data.tokenData); + expect(result).toEqual({ + tokens: { + "my-service": { + token: "bar", + exp: 123, + sub: "test", + }, + }, + values: { + "my-service": { + foo: "bar", + }, + }, + refreshTokens: { + "my-service": "refresh-token", + }, + }); + + expect(response.headers.has("X-Access-Token")).toBe(false); + expect(response.headers.has("X-Refresh-Token")).toBe(false); + }); + + it("should create new token", async () => { + const response = await yoga.fetch("http://localhost:3000/graphql", { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ + query: `mutation { createToken(service: "foobar") }`, + }), + }); + const content = await response.json(); + assertSingleValue(content); + + expect(response.headers.has("X-Access-Token")).toBe(true); + expect(response.headers.has("X-Refresh-Token")).toBe(true); + + const accessToken = response.headers.get("X-Access-Token"); + const refreshToken = response.headers.get("X-Refresh-Token"); + + expect(accessToken).toBeDefined(); + expect(refreshToken).toBeDefined(); + }); +}); diff --git a/packages/yoga/src/index.ts b/packages/yoga/src/index.ts index 641c155..812260e 100644 --- a/packages/yoga/src/index.ts +++ b/packages/yoga/src/index.ts @@ -6,26 +6,26 @@ type FederatedTokenContext = { }; export const useFederatedToken = (): Plugin< - object, + T, T > => ({ - onRequest: ({ request, serverContext }) => { - // Initialize FederatedToken and add it to the context - const federatedToken = serverContext.federatedToken ?? new FederatedToken(); - - // Retrieve tokens from headers using the serverContext - const accessToken = request.headers.get("x-access-token") as string; - const refreshToken = request.headers.get("x-refresh-token") as string; + onContextBuilding: ({ context, extendContext }) => { + if (!context.federatedToken) { + throw new Error("Federated token not found in context"); + } + if (!context.request) { + throw new Error("Request not found in context"); + } + const { request, federatedToken } = context; + const accessToken = request.headers.get("x-access-token"); + const refreshToken = request.headers.get("x-refresh-token"); if (accessToken) { federatedToken.deserializeAccessToken(accessToken); } - if (refreshToken) { federatedToken.loadRefreshToken(refreshToken); } - - serverContext.federatedToken = federatedToken; }, onResponse: async ({ response, serverContext }) => {