skuba 12.1.0-hoist-less-20250722131939 → 12.1.0-main-20250810101347

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (39) hide show
  1. package/README.md +1 -2
  2. package/config/tsconfig.json +3 -2
  3. package/lib/cli/build/tsc.d.ts +5 -1
  4. package/lib/cli/build/tsc.js +12 -0
  5. package/lib/cli/build/tsc.js.map +3 -3
  6. package/lib/cli/lint/internalLints/upgrade/patches/12.0.2/index.d.ts +2 -0
  7. package/lib/cli/lint/internalLints/upgrade/patches/12.0.2/index.js +35 -0
  8. package/lib/cli/lint/internalLints/upgrade/patches/12.0.2/index.js.map +7 -0
  9. package/lib/cli/lint/internalLints/upgrade/patches/12.0.2/unhandledRejections.d.ts +4 -0
  10. package/lib/cli/lint/internalLints/upgrade/patches/12.0.2/unhandledRejections.js +162 -0
  11. package/lib/cli/lint/internalLints/upgrade/patches/12.0.2/unhandledRejections.js.map +7 -0
  12. package/lib/cli/node/index.js +8 -2
  13. package/lib/cli/node/index.js.map +2 -2
  14. package/lib/cli/start/index.js +8 -2
  15. package/lib/cli/start/index.js.map +2 -2
  16. package/lib/cli/test/index.d.ts +1 -1
  17. package/lib/cli/test/index.js +18 -4
  18. package/lib/cli/test/index.js.map +2 -2
  19. package/lib/utils/args.d.ts +2 -0
  20. package/lib/utils/args.js +5 -0
  21. package/lib/utils/args.js.map +2 -2
  22. package/package.json +14 -15
  23. package/template/base/_pnpm-workspace.yaml +1 -0
  24. package/template/express-rest-api/package.json +4 -4
  25. package/template/express-rest-api/src/listen.ts +6 -0
  26. package/template/greeter/package.json +2 -2
  27. package/template/koa-rest-api/package.json +9 -9
  28. package/template/koa-rest-api/src/framework/server.test.ts +0 -1
  29. package/template/koa-rest-api/src/listen.ts +6 -0
  30. package/template/lambda-sqs-worker-cdk/infra/__snapshots__/appStack.test.ts.snap +16 -2
  31. package/template/lambda-sqs-worker-cdk/infra/appStack.ts +5 -1
  32. package/template/lambda-sqs-worker-cdk/infra/config.ts +4 -1
  33. package/template/lambda-sqs-worker-cdk/package.json +5 -5
  34. package/template/lambda-sqs-worker-cdk/src/app.test.ts +88 -48
  35. package/template/lambda-sqs-worker-cdk/src/app.ts +7 -9
  36. package/template/lambda-sqs-worker-cdk/src/framework/handler.test.ts +8 -3
  37. package/template/lambda-sqs-worker-cdk/src/framework/handler.ts +38 -5
  38. package/template/lambda-sqs-worker-cdk/src/framework/logging.ts +11 -3
  39. package/template/lambda-sqs-worker-cdk/src/testing/handler.ts +4 -1
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "skuba",
3
- "version": "12.1.0-hoist-less-20250722131939",
3
+ "version": "12.1.0-main-20250810101347",
4
4
  "private": false,
5
5
  "description": "SEEK development toolkit for backend applications and packages",
6
6
  "homepage": "https://github.com/seek-oss/skuba#readme",
@@ -61,7 +61,6 @@
61
61
  "@types/node": "^22.0.0",
62
62
  "chalk": "^4.1.0",
63
63
  "concurrently": "^9.0.0",
64
- "dotenv": "^16.0.0",
65
64
  "ejs": "^3.1.6",
66
65
  "enquirer": "^2.3.6",
67
66
  "esbuild": "~0.25.0",
@@ -96,38 +95,38 @@
96
95
  "tsconfig-paths": "^4.0.0",
97
96
  "tsconfig-seek": "2.0.0",
98
97
  "tsx": "^4.16.2",
99
- "typescript": "~5.8.0",
100
- "zod": "^3.25.67",
101
- "eslint-config-skuba": "7.0.2"
98
+ "typescript": "~5.9.0",
99
+ "zod": "^4.0.0",
100
+ "eslint-config-skuba": "7.1.0-main-20250810101347"
102
101
  },
103
102
  "devDependencies": {
104
103
  "@changesets/cli": "2.29.5",
105
104
  "@changesets/get-github-info": "0.6.0",
106
- "@jest/reporters": "30.0.4",
107
- "@jest/test-result": "30.0.4",
105
+ "@jest/reporters": "30.0.5",
106
+ "@jest/test-result": "30.0.5",
108
107
  "@types/ejs": "3.1.5",
109
108
  "@types/express": "5.0.3",
110
109
  "@types/fs-extra": "11.0.4",
111
- "@types/koa": "2.15.0",
110
+ "@types/koa": "3.0.0",
112
111
  "@types/lodash.mergewith": "4.6.9",
113
112
  "@types/minimist": "1.2.5",
114
113
  "@types/module-alias": "2.0.4",
115
114
  "@types/npm-registry-fetch": "8.0.8",
116
115
  "@types/npm-which": "3.0.4",
117
- "@types/picomatch": "4.0.0",
116
+ "@types/picomatch": "4.0.2",
118
117
  "@types/semver": "7.7.0",
119
118
  "@types/supertest": "6.0.3",
120
119
  "enhanced-resolve": "5.18.2",
121
120
  "express": "5.1.0",
122
121
  "fastify": "5.4.0",
123
- "jest-diff": "30.0.4",
122
+ "jest-diff": "30.0.5",
124
123
  "jsonfile": "6.1.0",
125
- "koa": "3.0.0",
126
- "memfs": "4.17.2",
124
+ "koa": "3.0.1",
125
+ "memfs": "4.36.0",
127
126
  "remark-cli": "12.0.1",
128
127
  "remark-preset-lint-recommended": "7.0.1",
129
128
  "semver": "7.7.2",
130
- "supertest": "7.1.3",
129
+ "supertest": "7.1.4",
131
130
  "type-fest": "2.19.0"
132
131
  },
133
132
  "peerDependencies": {
@@ -149,7 +148,7 @@
149
148
  "entryPoint": "src/index.ts",
150
149
  "template": null,
151
150
  "type": "package",
152
- "version": "11.1.0"
151
+ "version": "12.0.2"
153
152
  },
154
153
  "scripts": {
155
154
  "build": "scripts/build.sh",
@@ -163,7 +162,7 @@
163
162
  "lint:packages": "pnpm --filter '!./template/**' lint",
164
163
  "release": "pnpm --silent build && changeset publish",
165
164
  "skuba": "pnpm --silent build && pnpm --silent skuba:exec",
166
- "skuba:exec": "node --experimental-vm-modules --no-warnings=ExperimentalWarning lib/skuba",
165
+ "skuba:exec": "node lib/skuba",
167
166
  "stage": "changeset version && node ./.changeset/inject.js && pnpm format",
168
167
  "test": "pnpm --silent skuba test --selectProjects unit",
169
168
  "test:ci": "pnpm --silent skuba test --runInBand",
@@ -8,4 +8,5 @@ publicHoistPattern:
8
8
  - esbuild
9
9
  - jest
10
10
  - tsconfig-seek
11
+ - typescript
11
12
  # end managed by skuba
@@ -13,10 +13,10 @@
13
13
  "test:watch": "skuba test --watch"
14
14
  },
15
15
  "dependencies": {
16
- "@seek/logger": "^10.0.0",
16
+ "@seek/logger": "master",
17
17
  "express": "^5.0.0",
18
- "hot-shots": "^10.0.0",
19
- "seek-datadog-custom-metrics": "^4.6.3",
18
+ "hot-shots": "^11.0.0",
19
+ "seek-datadog-custom-metrics": "^5.0.0",
20
20
  "skuba-dive": "^2.0.0"
21
21
  },
22
22
  "devDependencies": {
@@ -28,7 +28,7 @@
28
28
  "skuba": "*",
29
29
  "supertest": "^7.0.0"
30
30
  },
31
- "packageManager": "pnpm@10.12.4",
31
+ "packageManager": "pnpm@10.14.0",
32
32
  "engines": {
33
33
  "node": ">=22"
34
34
  }
@@ -21,3 +21,9 @@ const listener = app.listen(config.port, () => {
21
21
  // https://docs.aws.amazon.com/elasticloadbalancing/latest/application/application-load-balancers.html#connection-idle-timeout
22
22
  // AWS recommends setting an application timeout larger than the load balancer
23
23
  listener.keepAliveTimeout = 31000;
24
+
25
+ // Report unhandled rejections instead of crashing the process
26
+ // Make sure to monitor these reports and alert as appropriate
27
+ process.on('unhandledRejection', (err) =>
28
+ logger.error(err, 'Unhandled promise rejection'),
29
+ );
@@ -17,9 +17,9 @@
17
17
  },
18
18
  "devDependencies": {
19
19
  "@types/node": "^22.13.10",
20
- "skuba": "12.1.0-hoist-less-20250722131939"
20
+ "skuba": "12.1.0-main-20250810101347"
21
21
  },
22
- "packageManager": "pnpm@10.12.4",
22
+ "packageManager": "pnpm@10.14.0",
23
23
  "engines": {
24
24
  "node": ">=22"
25
25
  }
@@ -14,7 +14,7 @@
14
14
  },
15
15
  "dependencies": {
16
16
  "@koa/bodyparser": "^6.0.0",
17
- "@koa/router": "^13.0.0",
17
+ "@koa/router": "^14.0.0",
18
18
  "@opentelemetry/api": "^1.9.0",
19
19
  "@opentelemetry/core": "^2.0.0",
20
20
  "@opentelemetry/exporter-trace-otlp-grpc": "^0.203.0",
@@ -22,19 +22,19 @@
22
22
  "@opentelemetry/instrumentation-http": "^0.203.0",
23
23
  "@opentelemetry/propagator-b3": "^2.0.0",
24
24
  "@opentelemetry/sdk-node": "^0.203.0",
25
- "@seek/logger": "^10.0.0",
26
- "hot-shots": "^10.0.0",
27
- "koa": "^2.16.1",
25
+ "@seek/logger": "master",
26
+ "hot-shots": "^11.0.0",
27
+ "koa": "^3.0.1",
28
28
  "koa-compose": "^4.1.0",
29
- "seek-datadog-custom-metrics": "^4.6.3",
30
- "seek-koala": "^7.0.0",
29
+ "seek-datadog-custom-metrics": "^5.0.0",
30
+ "seek-koala": "^7.1.0",
31
31
  "skuba-dive": "^2.0.0",
32
- "zod": "^3.25.67"
32
+ "zod": "^4.0.0"
33
33
  },
34
34
  "devDependencies": {
35
35
  "@types/chance": "^1.1.3",
36
36
  "@types/co-body": "^6.1.3",
37
- "@types/koa": "^2.13.4",
37
+ "@types/koa": "^3.0.0",
38
38
  "@types/koa__router": "^12.0.0",
39
39
  "@types/node": "^22.13.10",
40
40
  "@types/supertest": "^6.0.0",
@@ -44,7 +44,7 @@
44
44
  "skuba": "*",
45
45
  "supertest": "^7.0.0"
46
46
  },
47
- "packageManager": "pnpm@10.12.4",
47
+ "packageManager": "pnpm@10.14.0",
48
48
  "engines": {
49
49
  "node": ">=22"
50
50
  }
@@ -32,7 +32,6 @@ describe('createApp', () => {
32
32
 
33
33
  expect(stdoutMock.calls).toHaveLength(0);
34
34
 
35
- metricsClient.expectTagSubset(['env:test', 'version:test']);
36
35
  metricsClient.expectTagSubset([
37
36
  'http_method:get',
38
37
  'http_status:200',
@@ -22,3 +22,9 @@ const listener = app.listen(config.port, () => {
22
22
  // https://docs.aws.amazon.com/elasticloadbalancing/latest/application/application-load-balancers.html#connection-idle-timeout
23
23
  // AWS recommends setting an application timeout larger than the load balancer
24
24
  listener.keepAliveTimeout = 31000;
25
+
26
+ // Report unhandled rejections instead of crashing the process
27
+ // Make sure to monitor these reports and alert as appropriate
28
+ process.on('unhandledRejection', (err) =>
29
+ logger.error(err, 'Unhandled promise rejection'),
30
+ );
@@ -1,4 +1,4 @@
1
- // Jest Snapshot v1, https://goo.gl/fbAQLP
1
+ // Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing
2
2
 
3
3
  exports[`returns expected CloudFormation stack for dev 1`] = `
4
4
  {
@@ -222,7 +222,7 @@ exports[`returns expected CloudFormation stack for dev 1`] = `
222
222
  ],
223
223
  },
224
224
  ],
225
- "ReservedConcurrentExecutions": 2,
225
+ "ReservedConcurrentExecutions": 3,
226
226
  "Role": {
227
227
  "Fn::GetAtt": [
228
228
  "workerServiceRole2130CC7F",
@@ -332,6 +332,7 @@ exports[`returns expected CloudFormation stack for dev 1`] = `
332
332
  },
333
333
  "workerAliasLiveSqsEventSourceappStackworkerqueue8281B9F443B0CF93": {
334
334
  "Properties": {
335
+ "BatchSize": 10,
335
336
  "EventSourceArn": {
336
337
  "Fn::GetAtt": [
337
338
  "workerqueueA05CE5C6",
@@ -359,6 +360,12 @@ exports[`returns expected CloudFormation stack for dev 1`] = `
359
360
  ],
360
361
  ],
361
362
  },
363
+ "FunctionResponseTypes": [
364
+ "ReportBatchItemFailures",
365
+ ],
366
+ "ScalingConfig": {
367
+ "MaximumConcurrency": 2,
368
+ },
362
369
  "Tags": [
363
370
  {
364
371
  "Key": "aws-codedeploy-hooks",
@@ -1065,6 +1072,7 @@ exports[`returns expected CloudFormation stack for prod 1`] = `
1065
1072
  },
1066
1073
  "workerAliasLiveSqsEventSourceappStackworkerqueue8281B9F443B0CF93": {
1067
1074
  "Properties": {
1075
+ "BatchSize": 10,
1068
1076
  "EventSourceArn": {
1069
1077
  "Fn::GetAtt": [
1070
1078
  "workerqueueA05CE5C6",
@@ -1092,6 +1100,12 @@ exports[`returns expected CloudFormation stack for prod 1`] = `
1092
1100
  ],
1093
1101
  ],
1094
1102
  },
1103
+ "FunctionResponseTypes": [
1104
+ "ReportBatchItemFailures",
1105
+ ],
1106
+ "ScalingConfig": {
1107
+ "MaximumConcurrency": 19,
1108
+ },
1095
1109
  "Tags": [
1096
1110
  {
1097
1111
  "Key": "aws-codedeploy-hooks",
@@ -141,7 +141,11 @@ export class AppStack extends Stack {
141
141
  });
142
142
 
143
143
  workerDeployment.alias.addEventSource(
144
- new aws_lambda_event_sources.SqsEventSource(queue),
144
+ new aws_lambda_event_sources.SqsEventSource(queue, {
145
+ maxConcurrency: config.workerLambda.reservedConcurrency - 1, // Ensure we have capacity reserved for our blue/green deployment
146
+ batchSize: config.workerLambda.batchSize,
147
+ reportBatchItemFailures: true,
148
+ }),
145
149
  );
146
150
  }
147
151
  }
@@ -9,6 +9,7 @@ const environment = Env.oneOf(ENVIRONMENTS)('ENVIRONMENT');
9
9
  interface Config {
10
10
  appName: string;
11
11
  workerLambda: {
12
+ batchSize: number;
12
13
  reservedConcurrency: number;
13
14
  environment: {
14
15
  ENVIRONMENT: Environment;
@@ -24,7 +25,8 @@ const configs: Record<Environment, Config> = {
24
25
  dev: {
25
26
  appName: '<%- serviceName %>',
26
27
  workerLambda: {
27
- reservedConcurrency: 2,
28
+ batchSize: 10,
29
+ reservedConcurrency: 3,
28
30
  environment: {
29
31
  ENVIRONMENT: 'dev',
30
32
  SERVICE: '<%- serviceName %>',
@@ -37,6 +39,7 @@ const configs: Record<Environment, Config> = {
37
39
  prod: {
38
40
  appName: '<%- serviceName %>',
39
41
  workerLambda: {
42
+ batchSize: 10,
40
43
  reservedConcurrency: 20,
41
44
  environment: {
42
45
  ENVIRONMENT: 'prod',
@@ -18,11 +18,11 @@
18
18
  "@aws-sdk/client-lambda": "^3.363.0",
19
19
  "@aws-sdk/client-sns": "^3.363.0",
20
20
  "@seek/aws-codedeploy-hooks": "^2.0.0",
21
- "@seek/logger": "^10.0.0",
22
- "datadog-lambda-js": "^10.0.0",
21
+ "@seek/logger": "master",
22
+ "datadog-lambda-js": "^12.0.0",
23
23
  "dd-trace": "^5.0.0",
24
24
  "skuba-dive": "^2.0.0",
25
- "zod": "^3.25.67"
25
+ "zod": "^4.0.0"
26
26
  },
27
27
  "devDependencies": {
28
28
  "@seek/aws-codedeploy-infra": "^3.0.0",
@@ -37,9 +37,9 @@
37
37
  "constructs": "^10.0.17",
38
38
  "datadog-cdk-constructs-v2": "^2.0.0",
39
39
  "pino-pretty": "^13.0.0",
40
- "skuba": "12.1.0-hoist-less-20250722131939"
40
+ "skuba": "12.1.0-main-20250810101347"
41
41
  },
42
- "packageManager": "pnpm@10.12.4",
42
+ "packageManager": "pnpm@10.14.0",
43
43
  "engines": {
44
44
  "node": ">=22"
45
45
  }
@@ -1,4 +1,5 @@
1
1
  import { PublishCommand } from '@aws-sdk/client-sns';
2
+ import type { SQSBatchResponse } from 'aws-lambda';
2
3
 
3
4
  import { metricsClient } from 'src/framework/metrics.js';
4
5
  import { createCtx, createSqsEvent } from 'src/testing/handler.js';
@@ -40,42 +41,100 @@ describe('handler', () => {
40
41
  it('handles one record', async () => {
41
42
  const event = createSqsEvent([JSON.stringify(jobPublished)]);
42
43
 
43
- await expect(app.handler(event, ctx)).resolves.toBeUndefined();
44
+ await expect(app.handler(event, ctx)).resolves.toEqual<SQSBatchResponse>({
45
+ batchItemFailures: [],
46
+ });
44
47
 
45
48
  expect(scoringService.request).toHaveBeenCalledTimes(1);
46
49
 
47
50
  expect(stdoutMock.calls).toMatchObject([
51
+ { count: 1, level: 20, msg: 'Received jobs' },
48
52
  {
49
- awsRequestId: '-',
50
- count: 1,
51
53
  level: 20,
52
- msg: 'Received jobs',
54
+ msg: 'Scored job',
55
+ snsMessageId: expect.any(String),
56
+ sqsMessageId: event.Records[0]!.messageId,
53
57
  },
58
+ { level: 20, msg: 'Function completed' },
59
+ ]);
60
+
61
+ expect(distribution.mock.calls).toEqual([
62
+ ['job.received', 1],
63
+ ['job.scored', 1],
64
+ ]);
65
+
66
+ expect(sns.client).toReceiveCommandTimes(PublishCommand, 1);
67
+ });
68
+
69
+ it('handles multiple records', async () => {
70
+ const event = createSqsEvent([
71
+ JSON.stringify(jobPublished),
72
+ JSON.stringify(jobPublished),
73
+ ]);
74
+
75
+ await expect(app.handler(event, ctx)).resolves.toEqual<SQSBatchResponse>({
76
+ batchItemFailures: [],
77
+ });
78
+
79
+ expect(stdoutMock.calls).toMatchObject([
80
+ { count: 2, level: 20, msg: 'Received jobs' },
54
81
  {
55
- awsRequestId: '-',
56
82
  level: 20,
57
83
  msg: 'Scored job',
58
84
  snsMessageId: expect.any(String),
85
+ sqsMessageId: event.Records[0]!.messageId,
59
86
  },
60
87
  {
61
- awsRequestId: '-',
62
88
  level: 20,
63
- msg: 'Function succeeded',
89
+ msg: 'Scored job',
90
+ snsMessageId: expect.any(String),
91
+ sqsMessageId: event.Records[1]!.messageId,
64
92
  },
93
+ { level: 20, msg: 'Function completed' },
65
94
  ]);
95
+ });
66
96
 
67
- expect(distribution.mock.calls).toEqual([
68
- ['job.received', 1],
69
- ['job.scored', 1],
97
+ it('handles partial batch failure', async () => {
98
+ const event = createSqsEvent([
99
+ JSON.stringify('}'),
100
+ JSON.stringify(jobPublished),
70
101
  ]);
71
102
 
72
- expect(sns.client).toReceiveCommandTimes(PublishCommand, 1);
103
+ await expect(app.handler(event, ctx)).resolves.toEqual<SQSBatchResponse>({
104
+ batchItemFailures: [{ itemIdentifier: event.Records[0]!.messageId }],
105
+ });
106
+
107
+ expect(stdoutMock.calls).toMatchObject([
108
+ { count: 2, level: 20, msg: 'Received jobs' },
109
+ {
110
+ err: {
111
+ name: 'ZodError',
112
+ type: 'ZodError',
113
+ },
114
+ level: 50,
115
+ msg: 'Processing record failed',
116
+ sqsMessageId: event.Records[0]!.messageId,
117
+ },
118
+ {
119
+ level: 20,
120
+ msg: 'Scored job',
121
+ snsMessageId: expect.any(String),
122
+ sqsMessageId: event.Records[1]!.messageId,
123
+ },
124
+ { level: 20, msg: 'Function completed' },
125
+ ]);
73
126
  });
74
127
 
75
- it('throws on invalid input', () => {
128
+ it('returns a batchItemFailure on invalid input', () => {
76
129
  const event = createSqsEvent(['}']);
77
130
 
78
- return expect(app.handler(event, ctx)).rejects.toThrow('Function failed');
131
+ return expect(app.handler(event, ctx)).resolves.toEqual<SQSBatchResponse>({
132
+ batchItemFailures: [
133
+ {
134
+ itemIdentifier: event.Records[0]!.messageId,
135
+ },
136
+ ],
137
+ });
79
138
  });
80
139
 
81
140
  it('bubbles up scoring service error', async () => {
@@ -85,24 +144,22 @@ describe('handler', () => {
85
144
 
86
145
  const event = createSqsEvent([JSON.stringify(jobPublished)]);
87
146
 
88
- await expect(app.handler(event, ctx)).rejects.toThrow('Function failed');
147
+ await expect(app.handler(event, ctx)).resolves.toEqual<SQSBatchResponse>({
148
+ batchItemFailures: [{ itemIdentifier: event.Records[0]!.messageId }],
149
+ });
89
150
 
90
151
  expect(stdoutMock.calls).toMatchObject([
152
+ { count: 1, level: 20, msg: 'Received jobs' },
91
153
  {
92
- awsRequestId: '-',
93
- count: 1,
94
- level: 20,
95
- msg: 'Received jobs',
96
- },
97
- {
98
- awsRequestId: '-',
99
154
  err: {
100
155
  message: err.message,
101
156
  type: 'Error',
102
157
  },
103
158
  level: 50,
104
- msg: 'Function failed',
159
+ msg: 'Processing record failed',
160
+ sqsMessageId: event.Records[0]!.messageId,
105
161
  },
162
+ { level: 20, msg: 'Function completed' },
106
163
  ]);
107
164
  });
108
165
 
@@ -113,23 +170,28 @@ describe('handler', () => {
113
170
 
114
171
  const event = createSqsEvent([JSON.stringify(jobPublished)]);
115
172
 
116
- await expect(app.handler(event, ctx)).rejects.toThrow('Function failed');
173
+ await expect(app.handler(event, ctx)).resolves.toEqual<SQSBatchResponse>({
174
+ batchItemFailures: [{ itemIdentifier: event.Records[0]!.messageId }],
175
+ });
117
176
 
118
177
  expect(stdoutMock.calls).toMatchObject([
119
178
  {
120
- awsRequestId: '-',
121
179
  count: 1,
122
180
  level: 20,
123
181
  msg: 'Received jobs',
124
182
  },
125
183
  {
126
- awsRequestId: '-',
127
184
  err: {
128
185
  message: err.message,
129
186
  type: 'Error',
130
187
  },
131
188
  level: 50,
132
- msg: 'Function failed',
189
+ msg: 'Processing record failed',
190
+ sqsMessageId: event.Records[0]!.messageId,
191
+ },
192
+ {
193
+ level: 20,
194
+ msg: 'Function completed',
133
195
  },
134
196
  ]);
135
197
  });
@@ -141,7 +203,6 @@ describe('handler', () => {
141
203
 
142
204
  expect(stdoutMock.calls).toMatchObject([
143
205
  {
144
- awsRequestId: '-',
145
206
  err: {
146
207
  message: 'Received 0 records',
147
208
  type: 'Error',
@@ -151,25 +212,4 @@ describe('handler', () => {
151
212
  },
152
213
  ]);
153
214
  });
154
-
155
- it('throws on multiple records', async () => {
156
- const event = createSqsEvent([
157
- JSON.stringify(jobPublished),
158
- JSON.stringify(jobPublished),
159
- ]);
160
-
161
- await expect(app.handler(event, ctx)).rejects.toThrow('Function failed');
162
-
163
- expect(stdoutMock.calls).toMatchObject([
164
- {
165
- awsRequestId: '-',
166
- err: {
167
- message: 'Received 2 records',
168
- type: 'Error',
169
- },
170
- level: 50,
171
- msg: 'Function failed',
172
- },
173
- ]);
174
- });
175
215
  });
@@ -3,7 +3,7 @@ import 'skuba-dive/register';
3
3
  import { isLambdaHook } from '@seek/aws-codedeploy-hooks';
4
4
  import type { SQSEvent } from 'aws-lambda';
5
5
 
6
- import { createHandler } from 'src/framework/handler.js';
6
+ import { createBatchSQSHandler, createHandler } from 'src/framework/handler.js';
7
7
  import { logger } from 'src/framework/logging.js';
8
8
  import { metricsClient } from 'src/framework/metrics.js';
9
9
  import { validateJson } from 'src/framework/validation.js';
@@ -36,19 +36,17 @@ export const handler = createHandler<SQSEvent>(async (event, ctx) => {
36
36
 
37
37
  const count = event.Records.length;
38
38
 
39
- if (count !== 1) {
40
- throw Error(`Received ${count} records`);
39
+ if (!count) {
40
+ throw Error('Received 0 records');
41
41
  }
42
-
43
42
  logger.debug({ count }, 'Received jobs');
44
43
 
45
- metricsClient.distribution('job.received', event.Records.length);
44
+ metricsClient.distribution('job.received', count);
46
45
 
47
- const record = event.Records[0];
48
- if (!record) {
49
- throw new Error('Malformed SQS event with no records');
50
- }
46
+ return recordHandler(event, ctx);
47
+ });
51
48
 
49
+ const recordHandler = createBatchSQSHandler(async (record, _ctx) => {
52
50
  const { body } = record;
53
51
 
54
52
  // TODO: this throws an error, which will cause the Lambda function to retry
@@ -1,3 +1,5 @@
1
+ import type { SQSEvent } from 'aws-lambda';
2
+
1
3
  import { createCtx } from 'src/testing/handler.js';
2
4
  import { chance } from 'src/testing/types.js';
3
5
 
@@ -6,12 +8,14 @@ import { logger, stdoutMock } from './logging.js';
6
8
 
7
9
  describe('createHandler', () => {
8
10
  const ctx = createCtx();
9
- const input = chance.paragraph();
11
+ const input: SQSEvent = {
12
+ Records: [],
13
+ };
10
14
 
11
15
  afterEach(stdoutMock.clear);
12
16
 
13
17
  it('handles happy path', async () => {
14
- const output = chance.paragraph();
18
+ const output = chance.sentence();
15
19
 
16
20
  const handler = createHandler((event) => {
17
21
  expect(event).toBe(input);
@@ -32,7 +36,8 @@ describe('createHandler', () => {
32
36
  {
33
37
  awsRequestId: '-',
34
38
  level: 20,
35
- msg: 'Function succeeded',
39
+ output,
40
+ msg: 'Function completed',
36
41
  },
37
42
  ]);
38
43
  });