Skip to content

Commit e412aab

Browse files
committed
fix: resolve PickDeep type regression and add test suite
- use overload signatures to keep generic return type while the implementation signature is checked against a concrete type, fixing the type-fest 4.41 PickDeep assignability break - improve JSDoc for pickEventContextV2 - add 20 unit tests covering utils and decode helpers
1 parent 6d7a0aa commit e412aab

2 files changed

Lines changed: 172 additions & 4 deletions

File tree

‎src/utils.test.ts‎

Lines changed: 165 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,165 @@
1+
import type { APIGatewayProxyEvent, APIGatewayProxyEventV2 } from 'aws-lambda'
2+
import { Buffer } from 'node:buffer'
3+
import { describe, expect, it } from 'vitest'
4+
import {
5+
compressV2,
6+
compressV2Sync,
7+
decodePayload,
8+
decodeResponseV2,
9+
decodeResponseV2Sync,
10+
decompress,
11+
decompressSync,
12+
eventMethodUrl,
13+
fakeEvent,
14+
fakeEventV1,
15+
fakeEventV2,
16+
pickEventContextV2,
17+
} from './index'
18+
19+
describe('fakeEvent', () => {
20+
it('fakeEventV2 builds a v2 request event', () => {
21+
const event = fakeEventV2('GET', '/hello')
22+
expect(event.rawPath).toBe('/hello')
23+
expect(event.requestContext.http.method).toBe('GET')
24+
})
25+
26+
it('fakeEventV2 merges custom spread', () => {
27+
const event = fakeEventV2('POST', '/submit', { body: 'payload' })
28+
expect(event.rawPath).toBe('/submit')
29+
expect(event.body).toBe('payload')
30+
})
31+
32+
it('fakeEventV1 builds a v1 request event', () => {
33+
const event = fakeEventV1('GET', '/hello')
34+
expect(event.path).toBe('/hello')
35+
expect(event.httpMethod).toBe('GET')
36+
})
37+
38+
it('fakeEvent merges v1 and v2 shapes', () => {
39+
const event = fakeEvent('PUT', '/item/1')
40+
expect(event.rawPath).toBe('/item/1')
41+
expect(event.path).toBe('/item/1')
42+
expect(event.httpMethod).toBe('PUT')
43+
expect(event.requestContext.http.method).toBe('PUT')
44+
})
45+
})
46+
47+
describe('eventMethodUrl', () => {
48+
it('reads method and url from a v2 event', () => {
49+
const event = { rawPath: '/v2', requestContext: { http: { method: 'GET' } } } as unknown as APIGatewayProxyEventV2
50+
expect(eventMethodUrl(event)).toEqual(['GET', '/v2'])
51+
})
52+
53+
it('reads method and url from a v1 event', () => {
54+
const event = { path: '/v1', httpMethod: 'DELETE' } as unknown as APIGatewayProxyEvent
55+
expect(eventMethodUrl(event)).toEqual(['DELETE', '/v1'])
56+
})
57+
58+
it('throws on an invalid event', () => {
59+
expect(() => eventMethodUrl({} as any)).toThrow('Invalid event')
60+
})
61+
})
62+
63+
describe('pickEventContextV2', () => {
64+
const event = {
65+
...fakeEventV2('GET', '/full'),
66+
rawQueryString: 'a=1',
67+
routeKey: 'GET /full',
68+
headers: { 'x-test': 'yes' },
69+
isBase64Encoded: false,
70+
} as APIGatewayProxyEventV2
71+
72+
it('picks the full context by default', () => {
73+
const picked = pickEventContextV2(event)
74+
expect(picked).toHaveProperty('routeKey')
75+
expect(picked).toHaveProperty('rawPath')
76+
expect(picked).toHaveProperty('rawQueryString')
77+
expect(picked).toHaveProperty('headers')
78+
expect(picked).toHaveProperty('requestContext')
79+
expect(picked).toHaveProperty('isBase64Encoded')
80+
})
81+
82+
it('picks the minimal context when requested', () => {
83+
const picked = pickEventContextV2(event, { minimal: true })
84+
expect(picked.requestContext.http.method).toBe('GET')
85+
})
86+
})
87+
88+
describe('compressV2 / compressV2Sync', () => {
89+
it('compresses with gzip and mutates the response', async () => {
90+
const response: any = {}
91+
const { encoding, data } = await compressV2('hello world', { response, acceptEncoding: 'gzip' })
92+
expect(encoding).toBe('gzip')
93+
expect(Buffer.isBuffer(data)).toBe(true)
94+
expect(response.body).toBe(data.toString('base64'))
95+
expect(response.isBase64Encoded).toBe(true)
96+
expect(response.headers['Content-Encoding']).toBe('gzip')
97+
})
98+
99+
it('compresses with brotli when preferred', async () => {
100+
const { encoding } = await compressV2('hello world', { acceptEncoding: 'br' })
101+
expect(encoding).toBe('br')
102+
})
103+
104+
it('throws on unknown accept-encoding', async () => {
105+
await expect(compressV2('data', { acceptEncoding: 'identity' })).rejects.toThrow('accept-encoding')
106+
})
107+
108+
it('sync variant round-trips with decompressSync', () => {
109+
const { data } = compressV2Sync('roundtrip', { acceptEncoding: 'gzip' })
110+
expect(decompressSync(data, { contentEncoding: 'gzip' })).toBe('roundtrip')
111+
})
112+
113+
it('async variant round-trips with decompress', async () => {
114+
const { data } = await compressV2('roundtrip', { acceptEncoding: 'gzip' })
115+
expect(await decompress(data, { contentEncoding: 'gzip' })).toBe('roundtrip')
116+
})
117+
})
118+
119+
describe('decodePayload', () => {
120+
it('parses JSON payload by default', () => {
121+
const payload = Buffer.from('{"a":1}')
122+
expect(decodePayload(payload as any)).toEqual({ a: 1 })
123+
})
124+
125+
it('returns the raw string when parse is disabled', () => {
126+
const payload = Buffer.from('{"a":1}')
127+
expect(decodePayload(payload as any, { parse: false })).toBe('{"a":1}')
128+
})
129+
130+
it('throws on empty payload', () => {
131+
expect(() => decodePayload(undefined)).toThrow()
132+
})
133+
})
134+
135+
describe('decodeResponseV2 / decodeResponseV2Sync', () => {
136+
it('leaves an uncompressed body untouched', () => {
137+
const response = { statusCode: 200, body: '{"ok":true}' } as any
138+
const decoded = decodeResponseV2Sync(response)
139+
expect(decoded.body).toEqual({ ok: true })
140+
})
141+
142+
it('sync variant decompresses an encoded body', () => {
143+
const { data } = compressV2Sync('{"ok":true}', { acceptEncoding: 'gzip' })
144+
const response = {
145+
statusCode: 200,
146+
body: data.toString('base64'),
147+
headers: { 'Content-Encoding': 'gzip' },
148+
isBase64Encoded: true,
149+
} as any
150+
const decoded = decodeResponseV2Sync(response)
151+
expect(decoded.body).toEqual({ ok: true })
152+
})
153+
154+
it('async variant decompresses an encoded body', async () => {
155+
const { data } = await compressV2('{"ok":true}', { acceptEncoding: 'gzip' })
156+
const response = {
157+
statusCode: 200,
158+
body: data.toString('base64'),
159+
headers: { 'Content-Encoding': 'gzip' },
160+
isBase64Encoded: true,
161+
} as any
162+
const decoded = await decodeResponseV2(response)
163+
expect(decoded.body).toEqual({ ok: true })
164+
})
165+
})

‎src/utils.ts‎

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -68,15 +68,18 @@ export function eventMethodUrl(event: LambdaRequestEvent): [string, string] {
6868

6969
/**
7070
* This utility picks the following keys from the event: routeKey, rawPath, rawQueryString, headers, requestContext, isBase64Encoded
71-
*
72-
* @param {boolean} [options.minimal] - pick only: routeKey, rawPath, headers, requestContext.http.method
71+
*
72+
* @param event - the API Gateway v2 event to pick context from
73+
* @param options - control which fields are picked
74+
* @param options.minimal - pick only: routeKey, rawPath, headers, requestContext.http.method
7375
*/
74-
export function pickEventContextV2<M extends boolean = false>(event: APIGatewayProxyEventV2, options: { minimal?: M } = {}): PickDeep<
76+
export function pickEventContextV2<M extends boolean = false>(event: APIGatewayProxyEventV2, options?: { minimal?: M }): PickDeep<
7577
APIGatewayProxyEventV2,
7678
M extends false
7779
? ('routeKey' | 'rawPath' | 'rawQueryString' | 'headers' | 'requestContext' | 'isBase64Encoded')
7880
: ('routeKey' | 'rawPath' | 'headers' | 'requestContext.http.method')
79-
> {
81+
>
82+
export function pickEventContextV2(event: APIGatewayProxyEventV2, options: { minimal?: boolean } = {}): PickDeep<APIGatewayProxyEventV2, 'routeKey' | 'rawPath' | 'rawQueryString' | 'headers' | 'requestContext' | 'isBase64Encoded'> {
8083
return options.minimal
8184
// @ts-expect-error Some weird PickDeep bug
8285
? { ...objectPick(event, ['routeKey', 'rawPath', 'rawQueryString', 'headers']), requestContext: { http: { method: event.requestContext?.http?.method } } }

0 commit comments

Comments
 (0)