From 69c8a05ca8904f092fb994a4fd43c942d8ccd971 Mon Sep 17 00:00:00 2001 From: RYN BSD Date: Tue, 18 Nov 2025 14:36:55 +0100 Subject: [PATCH] feat: add transform method to request context - Add `transform` method that allows functional updates to context values - Method takes a key and transformation function for immutable updates - Support complex data structures and object transformations - Maintain request isolation and work with default store values - Add TypeScript definitions for type-safe transformations - Include comprehensive test coverage for various use cases --- index.js | 8 +- test-tap/requestContextPlugin.e2e.test.js | 123 ++++++++++++++++++++++ types/index.d.ts | 4 + 3 files changed, 132 insertions(+), 3 deletions(-) diff --git a/index.js b/index.js index cb86172..1554337 100644 --- a/index.js +++ b/index.js @@ -15,9 +15,11 @@ const requestContext = { }, set: (key, value) => { const store = asyncLocalStorage.getStore() - if (store) { - store[key] = value - } + if (store) store[key] = value + }, + transform: (key, fn) => { + const store = asyncLocalStorage.getStore() + if (store) store[key] = fn(store[key]) }, getStore: () => { return asyncLocalStorage.getStore() diff --git a/test-tap/requestContextPlugin.e2e.test.js b/test-tap/requestContextPlugin.e2e.test.js index 36e0fa5..d01c274 100644 --- a/test-tap/requestContextPlugin.e2e.test.js +++ b/test-tap/requestContextPlugin.e2e.test.js @@ -367,6 +367,129 @@ test('passing a custom resource factory function when create as AsyncResource', }) }) +test('transform method correctly updates context values using transformation function', (t) => { + t.plan(1) + + const route = (req) => { + // Set initial value + req.requestContext.set('counter', 5) + + // Use transform to increment the value + req.requestContext.transform('counter', (oldValue) => oldValue + 3) + + const finalValue = req.requestContext.get('counter') + return Promise.resolve({ value: finalValue }) + } + + app = initAppGetWithDefaultStoreValues(route, undefined) + + return app.listen({ port: 0, host: '127.0.0.1' }).then(() => { + const { address, port } = app.server.address() + const url = `http://${address}:${port}` + + return request('GET', url).then((response) => { + t.assert.strictEqual(response.body.value, 8) + }) + }) +}) + +test('transform method works with objects and complex data structures', (t) => { + t.plan(2) + + const route = (req) => { + req.requestContext.set('user', { id: 'system', permissions: ['read'] }) + + // Add a new permission using transform + req.requestContext.transform('user', (oldUser) => ({ + ...oldUser, + permissions: [...oldUser.permissions, 'write'], + })) + + const user = req.requestContext.get('user') + return Promise.resolve({ + id: user.id, + permissions: user.permissions, + }) + } + + app = initAppGetWithDefaultStoreValues(route, undefined) + + return app.listen({ port: 0, host: '127.0.0.1' }).then(() => { + const { address, port } = app.server.address() + const url = `http://${address}:${port}` + + return request('GET', url).then((response) => { + t.assert.strictEqual(response.body.id, 'system') + t.assert.deepStrictEqual(response.body.permissions, ['read', 'write']) + }) + }) +}) + +test('transform method preserves values within single request without affecting others', (t) => { + t.plan(2) + + const route = (req) => { + const { action } = req.query + + if (action === 'increment') { + req.requestContext.transform('counter', (oldValue = 0) => oldValue + 1) + } + + return Promise.resolve({ count: req.requestContext.get('counter') }) + } + + app = initAppGetWithDefaultStoreValues(route, undefined) + + return app.listen({ port: 0, host: '127.0.0.1' }).then(() => { + const { address, port } = app.server.address() + const url = `http://${address}:${port}` + + // First request with increment + return request('GET', url) + .query({ action: 'increment' }) + .then((response1) => { + t.assert.strictEqual(response1.body.count, 1) + + // Second request without increment should not see first request's value + return request('GET', url).then((response2) => { + t.assert.ok(!response2.body.count) + }) + }) + }) +}) + +test('transform method works with default store values', (t) => { + t.plan(2) + + const route = (req) => { + // Transform the default value + req.requestContext.transform('user', (oldUser) => ({ + ...oldUser, + status: 'active', + })) + + const user = req.requestContext.get('user') + return Promise.resolve({ + id: user.id, + status: user.status, + }) + } + + app = initAppGetWithDefaultStoreValues(route, { + user: { id: 'system', status: 'inactive' }, + }) + + return app.listen({ port: 0, host: '127.0.0.1' }).then(() => { + const { address, port } = app.server.address() + const url = `http://${address}:${port}` + + return request('GET', url).then((response) => { + t.assert.strictEqual(response.body.id, 'system') + t.assert.strictEqual(response.body.status, 'active') + }) + }) +}) + test('returns the store', (t) => { t.plan(2) diff --git a/types/index.d.ts b/types/index.d.ts index 303cedc..0b2c3a5 100644 --- a/types/index.d.ts +++ b/types/index.d.ts @@ -22,6 +22,10 @@ declare namespace fastifyRequestContext { export interface RequestContext { get(key: K): RequestContextData[K] | undefined set(key: K, value: RequestContextData[K]): void + transform( + key: K, + fn: (oldValue: RequestContextData[K]) => RequestContextData[K], + ): void getStore(): RequestContextData | undefined }