Skip to content

Commit 89a6281

Browse files
authored
fix(parser): min array length on Records (#3521)
1 parent 937be64 commit 89a6281

14 files changed

+223
-250
lines changed

Diff for: packages/parser/src/schemas/ses.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -173,7 +173,7 @@ const SesRecordSchema = z.object({
173173
* @see {@link https://docs.aws.amazon.com/ses/latest/dg/receiving-email-notifications-examples.html}
174174
*/
175175
const SesSchema = z.object({
176-
Records: z.array(SesRecordSchema),
176+
Records: z.array(SesRecordSchema).min(1),
177177
});
178178

179179
export { SesSchema, SesRecordSchema };

Diff for: packages/parser/tests/unit/envelopes/sqs.test.ts

+2-1
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,8 @@ describe('Envelope: SqsEnvelope', () => {
5151
expect(result).toStrictEqual([{ message: 'hello' }, { message: 'foo1' }]);
5252
});
5353
});
54-
describe('safeParse', () => {
54+
55+
describe('Method: safeParse', () => {
5556
it('parses an SQS event', () => {
5657
// Prepare
5758
const event = structuredClone(baseEvent);

Diff for: packages/parser/tests/unit/parser.decorator.test.ts

+65-115
Original file line numberDiff line numberDiff line change
@@ -1,159 +1,109 @@
1-
import { generateMock } from '@anatine/zod-mock';
2-
import type { LambdaInterface } from '@aws-lambda-powertools/commons/lib/esm/types';
1+
import type { LambdaInterface } from '@aws-lambda-powertools/commons/types';
32
import type { Context } from 'aws-lambda';
43
import { describe, expect, it } from 'vitest';
5-
import type { z } from 'zod';
4+
import { type ZodSchema, z } from 'zod';
65
import { EventBridgeEnvelope } from '../../src/envelopes/index.js';
76
import { ParseError } from '../../src/errors.js';
87
import { parser } from '../../src/index.js';
98
import { EventBridgeSchema } from '../../src/schemas/index.js';
10-
import type { EventBridgeEvent, ParsedResult } from '../../src/types';
11-
import { TestSchema, getTestEvent } from './schema/utils';
9+
import type { EventBridgeEvent, ParsedResult } from '../../src/types/index.js';
10+
import { getTestEvent } from './schema/utils.js';
1211

13-
describe('Parser Decorator', () => {
14-
const customEventBridgeSchema = EventBridgeSchema.extend({
15-
detail: TestSchema,
12+
describe('Decorator: parser', () => {
13+
const schema = z.object({
14+
name: z.string(),
15+
age: z.number(),
16+
});
17+
const payload = {
18+
name: 'John Doe',
19+
age: 30,
20+
};
21+
const extendedSchema = EventBridgeSchema.extend({
22+
detail: schema,
23+
});
24+
type event = z.infer<typeof extendedSchema>;
25+
const baseEvent = getTestEvent<EventBridgeEvent>({
26+
eventsPath: 'eventbridge',
27+
filename: 'base',
1628
});
17-
18-
type TestEvent = z.infer<typeof TestSchema>;
1929

2030
class TestClass implements LambdaInterface {
21-
@parser({ schema: TestSchema })
22-
public async handler(
23-
event: TestEvent,
24-
_context: Context
25-
): Promise<TestEvent> {
26-
return event;
27-
}
28-
29-
@parser({ schema: customEventBridgeSchema })
30-
public async handlerWithCustomSchema(
31-
event: unknown,
32-
_context: Context
33-
): Promise<unknown> {
31+
@parser({ schema: extendedSchema })
32+
public async handler(event: event, _context: Context): Promise<event> {
3433
return event;
3534
}
3635

37-
@parser({ schema: TestSchema, envelope: EventBridgeEnvelope })
36+
@parser({ schema, envelope: EventBridgeEnvelope })
3837
public async handlerWithParserCallsAnotherMethod(
39-
event: TestEvent,
38+
event: z.infer<typeof schema>,
4039
_context: Context
4140
): Promise<unknown> {
4241
return this.anotherMethod(event);
4342
}
4443

45-
@parser({ schema: TestSchema, envelope: EventBridgeEnvelope })
46-
public async handlerWithSchemaAndEnvelope(
47-
event: TestEvent,
48-
_context: Context
49-
): Promise<unknown> {
50-
return event;
51-
}
52-
5344
@parser({
54-
schema: TestSchema,
45+
schema,
5546
safeParse: true,
5647
})
5748
public async handlerWithSchemaAndSafeParse(
58-
event: ParsedResult<unknown, TestEvent>,
49+
event: ParsedResult<unknown, event>,
5950
_context: Context
60-
): Promise<ParsedResult> {
51+
): Promise<ParsedResult<unknown, event>> {
6152
return event;
6253
}
6354

6455
@parser({
65-
schema: TestSchema,
56+
schema,
6657
envelope: EventBridgeEnvelope,
6758
safeParse: true,
6859
})
6960
public async harndlerWithEnvelopeAndSafeParse(
70-
event: ParsedResult<TestEvent, TestEvent>,
61+
event: ParsedResult<event, event>,
7162
_context: Context
7263
): Promise<ParsedResult> {
7364
return event;
7465
}
7566

76-
private async anotherMethod(event: TestEvent): Promise<TestEvent> {
67+
private async anotherMethod<T extends ZodSchema>(
68+
event: z.infer<T>
69+
): Promise<z.infer<T>> {
7770
return event;
7871
}
7972
}
80-
8173
const lambda = new TestClass();
8274

83-
it('should parse custom schema event', async () => {
84-
const testEvent = generateMock(TestSchema);
75+
it('parses the event using the schema provided', async () => {
76+
// Prepare
77+
const event = structuredClone(baseEvent);
78+
event.detail = payload;
8579

86-
const resp = await lambda.handler(testEvent, {} as Context);
80+
// Act
81+
// @ts-expect-error - extended schema
82+
const result = await lambda.handler(event, {} as Context);
8783

88-
expect(resp).toEqual(testEvent);
84+
// Assess
85+
expect(result).toEqual(event);
8986
});
9087

91-
it('should parse custom schema with envelope event', async () => {
92-
const customPayload = generateMock(TestSchema);
93-
const testEvent = getTestEvent<EventBridgeEvent>({
94-
eventsPath: 'eventbridge',
95-
filename: 'base',
96-
});
97-
testEvent.detail = customPayload;
88+
it('preserves the class method scope when decorated', async () => {
89+
// Prepare
90+
const event = structuredClone(baseEvent);
91+
event.detail = payload;
9892

99-
const resp = await lambda.handlerWithSchemaAndEnvelope(
100-
testEvent as unknown as TestEvent,
93+
const result = await lambda.handlerWithParserCallsAnotherMethod(
94+
// @ts-expect-error - extended schema
95+
event,
10196
{} as Context
10297
);
10398

104-
expect(resp).toEqual(customPayload);
99+
expect(result).toEqual(event.detail);
105100
});
106101

107-
it('should parse extended envelope event', async () => {
108-
const customPayload = generateMock(TestSchema);
109-
110-
const testEvent = generateMock(customEventBridgeSchema);
111-
testEvent.detail = customPayload;
112-
113-
const resp: z.infer<typeof customEventBridgeSchema> =
114-
(await lambda.handlerWithCustomSchema(
115-
testEvent,
116-
{} as Context
117-
)) as z.infer<typeof customEventBridgeSchema>;
118-
119-
expect(customEventBridgeSchema.parse(resp)).toEqual(testEvent);
120-
expect(resp.detail).toEqual(customPayload);
121-
});
122-
123-
it('should parse and call private async method', async () => {
124-
const customPayload = generateMock(TestSchema);
125-
const testEvent = getTestEvent<EventBridgeEvent>({
126-
eventsPath: 'eventbridge',
127-
filename: 'base',
128-
});
129-
testEvent.detail = customPayload;
130-
131-
const resp = await lambda.handlerWithParserCallsAnotherMethod(
132-
testEvent as unknown as TestEvent,
133-
{} as Context
134-
);
135-
136-
expect(resp).toEqual(customPayload);
137-
});
138-
139-
it('should parse event with schema and safeParse', async () => {
140-
const testEvent = generateMock(TestSchema);
141-
142-
const resp = await lambda.handlerWithSchemaAndSafeParse(
143-
testEvent as unknown as ParsedResult<unknown, TestEvent>,
144-
{} as Context
145-
);
146-
147-
expect(resp).toEqual({
148-
success: true,
149-
data: testEvent,
150-
});
151-
});
152-
153-
it('should parse event with schema and safeParse and return error', async () => {
102+
it('returns a parse error when schema validation fails with safeParse enabled', async () => {
103+
// Act & Assess
154104
expect(
155105
await lambda.handlerWithSchemaAndSafeParse(
156-
{ foo: 'bar' } as unknown as ParsedResult<unknown, TestEvent>,
106+
{ foo: 'bar' } as unknown as ParsedResult<unknown, event>,
157107
{} as Context
158108
)
159109
).toEqual({
@@ -163,29 +113,29 @@ describe('Parser Decorator', () => {
163113
});
164114
});
165115

166-
it('should parse event with envelope and safeParse', async () => {
167-
const testEvent = generateMock(TestSchema);
168-
const event = getTestEvent<EventBridgeEvent>({
169-
eventsPath: 'eventbridge',
170-
filename: 'base',
171-
});
172-
event.detail = testEvent;
116+
it('parses the event with envelope and safeParse', async () => {
117+
// Prepare
118+
const event = structuredClone(baseEvent);
119+
event.detail = payload;
173120

174-
const resp = await lambda.harndlerWithEnvelopeAndSafeParse(
175-
event as unknown as ParsedResult<TestEvent, TestEvent>,
121+
// Act
122+
const result = await lambda.harndlerWithEnvelopeAndSafeParse(
123+
event as unknown as ParsedResult<event, event>,
176124
{} as Context
177125
);
178126

179-
expect(resp).toEqual({
127+
// Assess
128+
expect(result).toEqual({
180129
success: true,
181-
data: testEvent,
130+
data: event.detail,
182131
});
183132
});
184133

185-
it('should parse event with envelope and safeParse and return error', async () => {
134+
it('returns a parse error when schema/envelope validation fails with safeParse enabled', async () => {
135+
// Act & Assess
186136
expect(
187137
await lambda.harndlerWithEnvelopeAndSafeParse(
188-
{ foo: 'bar' } as unknown as ParsedResult<TestEvent, TestEvent>,
138+
{ foo: 'bar' } as unknown as ParsedResult<event, event>,
189139
{} as Context
190140
)
191141
).toEqual({

Diff for: packages/parser/tests/unit/schema/appsync.test.ts

+27-72
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,19 @@
1-
/**
2-
* Test built-in AppSync resolver schemas
3-
*/
4-
51
import { describe, expect, it } from 'vitest';
62
import {
73
AppSyncBatchResolverSchema,
84
AppSyncResolverSchema,
9-
} from '../../../src/schemas/appsync';
10-
import type { AppSyncResolverEvent } from '../../../src/types';
11-
import { getTestEvent, omit } from './utils';
5+
} from '../../../src/schemas/appsync.js';
6+
import type { AppSyncResolverEvent } from '../../../src/types/schema.js';
7+
import { getTestEvent, omit } from './utils.js';
128

13-
describe('AppSync Resolver Schemas', () => {
9+
describe('Schema: AppSync Resolver', () => {
1410
const eventsPath = 'appsync';
15-
16-
const appSyncResolverEvent: AppSyncResolverEvent = getTestEvent({
11+
const appSyncResolverEvent = getTestEvent<AppSyncResolverEvent>({
1712
eventsPath,
1813
filename: 'resolver',
1914
});
2015

21-
const table = [
16+
const events = [
2217
{
2318
name: 'null source',
2419
event: {
@@ -119,73 +114,33 @@ describe('AppSync Resolver Schemas', () => {
119114
},
120115
];
121116

122-
describe('AppSync Resolver Schema', () => {
123-
it('should return validation error when the event is invalid', () => {
124-
const { error } = AppSyncResolverSchema.safeParse(
125-
omit(['request', 'info'], appSyncResolverEvent)
126-
);
117+
it.each(events)('parses an AppSyn resolver event with $name', ({ event }) => {
118+
// Assess
119+
const result = AppSyncResolverSchema.parse(event);
127120

128-
expect(error?.issues).toEqual([
129-
{
130-
code: 'invalid_type',
131-
expected: 'object',
132-
received: 'undefined',
133-
path: ['request'],
134-
message: 'Required',
135-
},
136-
{
137-
code: 'invalid_type',
138-
expected: 'object',
139-
received: 'undefined',
140-
path: ['info'],
141-
message: 'Required',
142-
},
143-
]);
144-
});
121+
// Assess
122+
expect(result).toEqual(event);
123+
});
145124

146-
it('should parse resolver event without identity field', () => {
147-
const event: Omit<AppSyncResolverEvent, 'identity'> = omit(
148-
['identity'],
149-
appSyncResolverEvent
150-
);
151-
const parsedEvent = AppSyncResolverSchema.parse(event);
152-
expect(parsedEvent).toEqual(event);
153-
});
125+
it('throws when the event is not an AppSync resolver event', () => {
126+
// Prepare
127+
const event = omit(
128+
['request', 'info'],
129+
structuredClone(appSyncResolverEvent)
130+
);
154131

155-
it.each(table)('should parse resolver event with $name', ({ event }) => {
156-
const parsedEvent = AppSyncResolverSchema.parse(event);
157-
expect(parsedEvent).toEqual(event);
158-
});
132+
// Act & Assess
133+
expect(() => AppSyncResolverSchema.parse(event)).toThrow();
159134
});
160135

161-
describe('Batch AppSync Resolver Schema', () => {
162-
it('should return validation error when the event is invalid', () => {
163-
const event = omit(['request', 'info'], appSyncResolverEvent);
164-
165-
const { error } = AppSyncBatchResolverSchema.safeParse([event]);
136+
it('parses batches of AppSync resolver events', () => {
137+
// Prepare
138+
const event = events.map((event) => structuredClone(event.event));
166139

167-
expect(error?.issues).toEqual([
168-
{
169-
code: 'invalid_type',
170-
expected: 'object',
171-
received: 'undefined',
172-
path: [0, 'request'],
173-
message: 'Required',
174-
},
175-
{
176-
code: 'invalid_type',
177-
expected: 'object',
178-
received: 'undefined',
179-
path: [0, 'info'],
180-
message: 'Required',
181-
},
182-
]);
183-
});
140+
// Act
141+
const result = AppSyncBatchResolverSchema.parse(event);
184142

185-
it('should parse batches of appsync resolver events', () => {
186-
const events = table.map((table) => table.event);
187-
const parsedEvent = AppSyncBatchResolverSchema.parse(events);
188-
expect(parsedEvent).toEqual(events);
189-
});
143+
// Assess
144+
expect(result).toEqual(event);
190145
});
191146
});

0 commit comments

Comments
 (0)