skuba 9.0.0-renovate-eslint-9.x-20240923103202 → 9.0.1-upgrade-cdk-template-20241002233314

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 (68) hide show
  1. package/lib/api/github/issueComment.js.map +2 -2
  2. package/lib/cli/adapter/eslint.js +4 -4
  3. package/lib/cli/adapter/eslint.js.map +2 -2
  4. package/lib/cli/lint/autofix.js +0 -15
  5. package/lib/cli/lint/autofix.js.map +2 -2
  6. package/lib/cli/lint/internalLints/upgrade/patches/8.2.1/collapseDuplicateMergeKeys.js +15 -6
  7. package/lib/cli/lint/internalLints/upgrade/patches/8.2.1/collapseDuplicateMergeKeys.js.map +2 -2
  8. package/lib/cli/lint/internalLints/upgrade/patches/8.2.1/index.js +10 -0
  9. package/lib/cli/lint/internalLints/upgrade/patches/8.2.1/index.js.map +2 -2
  10. package/lib/cli/lint/internalLints/upgrade/patches/8.2.1/moveNpmrcMounts.d.ts +2 -0
  11. package/lib/cli/lint/internalLints/upgrade/patches/8.2.1/moveNpmrcMounts.js +82 -0
  12. package/lib/cli/lint/internalLints/upgrade/patches/8.2.1/moveNpmrcMounts.js.map +7 -0
  13. package/lib/cli/lint/internalLints/upgrade/patches/8.2.1/patchDockerCompose.js +1 -3
  14. package/lib/cli/lint/internalLints/upgrade/patches/8.2.1/patchDockerCompose.js.map +2 -2
  15. package/lib/cli/lint/internalLints/upgrade/patches/8.2.1/patchDockerImages.d.ts +2 -0
  16. package/lib/cli/lint/internalLints/upgrade/patches/8.2.1/patchDockerImages.js +141 -0
  17. package/lib/cli/lint/internalLints/upgrade/patches/8.2.1/patchDockerImages.js.map +7 -0
  18. package/lib/cli/lint/internalLints/upgrade/patches/8.2.1/upgradeESLint.js +5 -3
  19. package/lib/cli/lint/internalLints/upgrade/patches/8.2.1/upgradeESLint.js.map +2 -2
  20. package/lib/wrapper/requestListener.js.map +2 -2
  21. package/package.json +9 -9
  22. package/template/express-rest-api/.buildkite/pipeline.yml +2 -2
  23. package/template/express-rest-api/Dockerfile +1 -1
  24. package/template/express-rest-api/Dockerfile.dev-deps +1 -1
  25. package/template/express-rest-api/package.json +1 -1
  26. package/template/greeter/.buildkite/pipeline.yml +2 -2
  27. package/template/greeter/Dockerfile +1 -1
  28. package/template/greeter/package.json +1 -1
  29. package/template/koa-rest-api/.buildkite/pipeline.yml +2 -2
  30. package/template/koa-rest-api/Dockerfile +1 -1
  31. package/template/koa-rest-api/Dockerfile.dev-deps +1 -1
  32. package/template/koa-rest-api/package.json +4 -4
  33. package/template/koa-rest-api/src/framework/bodyParser.ts +1 -1
  34. package/template/koa-rest-api/src/framework/server.test.ts +0 -1
  35. package/template/lambda-sqs-worker/.buildkite/pipeline.yml +2 -2
  36. package/template/lambda-sqs-worker/Dockerfile +1 -1
  37. package/template/lambda-sqs-worker/package.json +2 -2
  38. package/template/lambda-sqs-worker/serverless.yml +1 -1
  39. package/template/lambda-sqs-worker-cdk/.buildkite/pipeline.yml +2 -2
  40. package/template/lambda-sqs-worker-cdk/.env +1 -0
  41. package/template/lambda-sqs-worker-cdk/Dockerfile +1 -1
  42. package/template/lambda-sqs-worker-cdk/README.md +145 -0
  43. package/template/lambda-sqs-worker-cdk/infra/__snapshots__/appStack.test.ts.snap +102 -134
  44. package/template/lambda-sqs-worker-cdk/infra/appStack.test.ts +13 -2
  45. package/template/lambda-sqs-worker-cdk/infra/appStack.ts +52 -6
  46. package/template/lambda-sqs-worker-cdk/infra/config.ts +3 -0
  47. package/template/lambda-sqs-worker-cdk/package.json +9 -2
  48. package/template/lambda-sqs-worker-cdk/src/app.test.ts +116 -0
  49. package/template/lambda-sqs-worker-cdk/src/app.ts +43 -21
  50. package/template/lambda-sqs-worker-cdk/src/config.ts +15 -0
  51. package/template/lambda-sqs-worker-cdk/src/framework/handler.test.ts +61 -0
  52. package/template/lambda-sqs-worker-cdk/src/framework/handler.ts +43 -0
  53. package/template/lambda-sqs-worker-cdk/src/framework/logging.ts +27 -0
  54. package/template/lambda-sqs-worker-cdk/src/framework/metrics.ts +14 -0
  55. package/template/lambda-sqs-worker-cdk/src/framework/validation.test.ts +84 -0
  56. package/template/lambda-sqs-worker-cdk/src/framework/validation.ts +10 -0
  57. package/template/lambda-sqs-worker-cdk/src/mapping/jobScorer.ts +22 -0
  58. package/template/lambda-sqs-worker-cdk/src/services/aws.ts +5 -0
  59. package/template/lambda-sqs-worker-cdk/src/services/jobScorer.test.ts +44 -0
  60. package/template/lambda-sqs-worker-cdk/src/services/jobScorer.ts +59 -0
  61. package/template/lambda-sqs-worker-cdk/src/services/pipelineEventSender.test.ts +40 -0
  62. package/template/lambda-sqs-worker-cdk/src/services/pipelineEventSender.ts +33 -0
  63. package/template/lambda-sqs-worker-cdk/src/testing/handler.ts +13 -0
  64. package/template/lambda-sqs-worker-cdk/src/testing/logging.ts +19 -0
  65. package/template/lambda-sqs-worker-cdk/src/testing/services.ts +28 -0
  66. package/template/lambda-sqs-worker-cdk/src/testing/types.ts +33 -0
  67. package/template/lambda-sqs-worker-cdk/src/types/jobScorer.ts +15 -0
  68. package/template/lambda-sqs-worker-cdk/src/types/pipelineEvents.ts +21 -0
@@ -10,6 +10,40 @@ exports[`returns expected CloudFormation stack for dev 1`] = `
10
10
  },
11
11
  },
12
12
  "Resources": {
13
+ "datadogapikeysecret046FEF06": {
14
+ "DeletionPolicy": "Delete",
15
+ "Properties": {
16
+ "GenerateSecretString": {},
17
+ },
18
+ "Type": "AWS::SecretsManager::Secret",
19
+ "UpdateReplacePolicy": "Delete",
20
+ },
21
+ "destinationtopicDCE2E0B8": {
22
+ "Properties": {
23
+ "KmsMasterKeyId": {
24
+ "Fn::Join": [
25
+ "",
26
+ [
27
+ "arn:",
28
+ {
29
+ "Ref": "AWS::Partition",
30
+ },
31
+ ":kms:",
32
+ {
33
+ "Ref": "AWS::Region",
34
+ },
35
+ ":",
36
+ {
37
+ "Ref": "AWS::AccountId",
38
+ },
39
+ ":alias/aws/sns",
40
+ ],
41
+ ],
42
+ },
43
+ "TopicName": "serviceName",
44
+ },
45
+ "Type": "AWS::SNS::Topic",
46
+ },
13
47
  "kmskey49FBC3B3": {
14
48
  "DeletionPolicy": "Retain",
15
49
  "Properties": {
@@ -105,17 +139,6 @@ exports[`returns expected CloudFormation stack for dev 1`] = `
105
139
  },
106
140
  "Resource": "*",
107
141
  },
108
- {
109
- "Action": [
110
- "kms:Decrypt",
111
- "kms:GenerateDataKey",
112
- ],
113
- "Effect": "Allow",
114
- "Principal": {
115
- "Service": "sns.amazonaws.com",
116
- },
117
- "Resource": "*",
118
- },
119
142
  ],
120
143
  "Version": "2012-10-17",
121
144
  },
@@ -135,9 +158,6 @@ exports[`returns expected CloudFormation stack for dev 1`] = `
135
158
  },
136
159
  "Type": "AWS::KMS::Alias",
137
160
  },
138
- "sourcetopic7C3DC892": {
139
- "Type": "AWS::SNS::Topic",
140
- },
141
161
  "worker28EA3E30": {
142
162
  "DependsOn": [
143
163
  "workerServiceRoleDefaultPolicyBA498553",
@@ -156,6 +176,9 @@ exports[`returns expected CloudFormation stack for dev 1`] = `
156
176
  "Description": "Updated at 1212-12-12T12:12:12.121Z",
157
177
  "Environment": {
158
178
  "Variables": {
179
+ "DESTINATION_SNS_TOPIC_ARN": {
180
+ "Ref": "destinationtopicDCE2E0B8",
181
+ },
159
182
  "ENVIRONMENT": "dev",
160
183
  "NODE_ENV": "production",
161
184
  "NODE_OPTIONS": "--enable-source-maps",
@@ -171,6 +194,20 @@ exports[`returns expected CloudFormation stack for dev 1`] = `
171
194
  "Arn",
172
195
  ],
173
196
  },
197
+ "Layers": [
198
+ {
199
+ "Fn::Join": [
200
+ "",
201
+ [
202
+ "arn:aws:lambda:",
203
+ {
204
+ "Ref": "AWS::Region",
205
+ },
206
+ ":464622532012:layer:Datadog-Extension-ARM:58",
207
+ ],
208
+ ],
209
+ },
210
+ ],
174
211
  "ReservedConcurrentExecutions": 2,
175
212
  "Role": {
176
213
  "Fn::GetAtt": [
@@ -611,59 +648,6 @@ exports[`returns expected CloudFormation stack for dev 1`] = `
611
648
  "Type": "AWS::SQS::Queue",
612
649
  "UpdateReplacePolicy": "Delete",
613
650
  },
614
- "workerqueuePolicy97054CB4": {
615
- "Properties": {
616
- "PolicyDocument": {
617
- "Statement": [
618
- {
619
- "Action": "sqs:SendMessage",
620
- "Condition": {
621
- "ArnEquals": {
622
- "aws:SourceArn": {
623
- "Ref": "sourcetopic7C3DC892",
624
- },
625
- },
626
- },
627
- "Effect": "Allow",
628
- "Principal": {
629
- "Service": "sns.amazonaws.com",
630
- },
631
- "Resource": {
632
- "Fn::GetAtt": [
633
- "workerqueueA05CE5C6",
634
- "Arn",
635
- ],
636
- },
637
- },
638
- ],
639
- "Version": "2012-10-17",
640
- },
641
- "Queues": [
642
- {
643
- "Ref": "workerqueueA05CE5C6",
644
- },
645
- ],
646
- },
647
- "Type": "AWS::SQS::QueuePolicy",
648
- },
649
- "workerqueueappStacksourcetopic613C6BDBD2F224F5": {
650
- "DependsOn": [
651
- "workerqueuePolicy97054CB4",
652
- ],
653
- "Properties": {
654
- "Endpoint": {
655
- "Fn::GetAtt": [
656
- "workerqueueA05CE5C6",
657
- "Arn",
658
- ],
659
- },
660
- "Protocol": "sqs",
661
- "TopicArn": {
662
- "Ref": "sourcetopic7C3DC892",
663
- },
664
- },
665
- "Type": "AWS::SNS::Subscription",
666
- },
667
651
  "workerqueuedeadletters83F3505C": {
668
652
  "DeletionPolicy": "Delete",
669
653
  "Properties": {
@@ -719,6 +703,40 @@ exports[`returns expected CloudFormation stack for prod 1`] = `
719
703
  },
720
704
  },
721
705
  "Resources": {
706
+ "datadogapikeysecret046FEF06": {
707
+ "DeletionPolicy": "Delete",
708
+ "Properties": {
709
+ "GenerateSecretString": {},
710
+ },
711
+ "Type": "AWS::SecretsManager::Secret",
712
+ "UpdateReplacePolicy": "Delete",
713
+ },
714
+ "destinationtopicDCE2E0B8": {
715
+ "Properties": {
716
+ "KmsMasterKeyId": {
717
+ "Fn::Join": [
718
+ "",
719
+ [
720
+ "arn:",
721
+ {
722
+ "Ref": "AWS::Partition",
723
+ },
724
+ ":kms:",
725
+ {
726
+ "Ref": "AWS::Region",
727
+ },
728
+ ":",
729
+ {
730
+ "Ref": "AWS::AccountId",
731
+ },
732
+ ":alias/aws/sns",
733
+ ],
734
+ ],
735
+ },
736
+ "TopicName": "serviceName",
737
+ },
738
+ "Type": "AWS::SNS::Topic",
739
+ },
722
740
  "kmskey49FBC3B3": {
723
741
  "DeletionPolicy": "Retain",
724
742
  "Properties": {
@@ -814,17 +832,6 @@ exports[`returns expected CloudFormation stack for prod 1`] = `
814
832
  },
815
833
  "Resource": "*",
816
834
  },
817
- {
818
- "Action": [
819
- "kms:Decrypt",
820
- "kms:GenerateDataKey",
821
- ],
822
- "Effect": "Allow",
823
- "Principal": {
824
- "Service": "sns.amazonaws.com",
825
- },
826
- "Resource": "*",
827
- },
828
835
  ],
829
836
  "Version": "2012-10-17",
830
837
  },
@@ -844,9 +851,6 @@ exports[`returns expected CloudFormation stack for prod 1`] = `
844
851
  },
845
852
  "Type": "AWS::KMS::Alias",
846
853
  },
847
- "sourcetopic7C3DC892": {
848
- "Type": "AWS::SNS::Topic",
849
- },
850
854
  "worker28EA3E30": {
851
855
  "DependsOn": [
852
856
  "workerServiceRoleDefaultPolicyBA498553",
@@ -865,6 +869,9 @@ exports[`returns expected CloudFormation stack for prod 1`] = `
865
869
  "Description": "Updated at 1212-12-12T12:12:12.121Z",
866
870
  "Environment": {
867
871
  "Variables": {
872
+ "DESTINATION_SNS_TOPIC_ARN": {
873
+ "Ref": "destinationtopicDCE2E0B8",
874
+ },
868
875
  "ENVIRONMENT": "prod",
869
876
  "NODE_ENV": "production",
870
877
  "NODE_OPTIONS": "--enable-source-maps",
@@ -880,6 +887,20 @@ exports[`returns expected CloudFormation stack for prod 1`] = `
880
887
  "Arn",
881
888
  ],
882
889
  },
890
+ "Layers": [
891
+ {
892
+ "Fn::Join": [
893
+ "",
894
+ [
895
+ "arn:aws:lambda:",
896
+ {
897
+ "Ref": "AWS::Region",
898
+ },
899
+ ":464622532012:layer:Datadog-Extension-ARM:58",
900
+ ],
901
+ ],
902
+ },
903
+ ],
883
904
  "ReservedConcurrentExecutions": 20,
884
905
  "Role": {
885
906
  "Fn::GetAtt": [
@@ -1320,59 +1341,6 @@ exports[`returns expected CloudFormation stack for prod 1`] = `
1320
1341
  "Type": "AWS::SQS::Queue",
1321
1342
  "UpdateReplacePolicy": "Delete",
1322
1343
  },
1323
- "workerqueuePolicy97054CB4": {
1324
- "Properties": {
1325
- "PolicyDocument": {
1326
- "Statement": [
1327
- {
1328
- "Action": "sqs:SendMessage",
1329
- "Condition": {
1330
- "ArnEquals": {
1331
- "aws:SourceArn": {
1332
- "Ref": "sourcetopic7C3DC892",
1333
- },
1334
- },
1335
- },
1336
- "Effect": "Allow",
1337
- "Principal": {
1338
- "Service": "sns.amazonaws.com",
1339
- },
1340
- "Resource": {
1341
- "Fn::GetAtt": [
1342
- "workerqueueA05CE5C6",
1343
- "Arn",
1344
- ],
1345
- },
1346
- },
1347
- ],
1348
- "Version": "2012-10-17",
1349
- },
1350
- "Queues": [
1351
- {
1352
- "Ref": "workerqueueA05CE5C6",
1353
- },
1354
- ],
1355
- },
1356
- "Type": "AWS::SQS::QueuePolicy",
1357
- },
1358
- "workerqueueappStacksourcetopic613C6BDBD2F224F5": {
1359
- "DependsOn": [
1360
- "workerqueuePolicy97054CB4",
1361
- ],
1362
- "Properties": {
1363
- "Endpoint": {
1364
- "Fn::GetAtt": [
1365
- "workerqueueA05CE5C6",
1366
- "Arn",
1367
- ],
1368
- },
1369
- "Protocol": "sqs",
1370
- "TopicArn": {
1371
- "Ref": "sourcetopic7C3DC892",
1372
- },
1373
- },
1374
- "Type": "AWS::SNS::Subscription",
1375
- },
1376
1344
  "workerqueuedeadletters83F3505C": {
1377
1345
  "DeletionPolicy": "Delete",
1378
1346
  "Properties": {
@@ -1,4 +1,4 @@
1
- import { App, aws_sns } from 'aws-cdk-lib';
1
+ import { App, aws_secretsmanager, aws_sns } from 'aws-cdk-lib';
2
2
  import { Template } from 'aws-cdk-lib/assertions';
3
3
 
4
4
  const currentDate = '1212-12-12T12:12:12.121Z';
@@ -36,6 +36,12 @@ it.each(['dev', 'prod'])(
36
36
  .spyOn(aws_sns.Topic, 'fromTopicArn')
37
37
  .mockImplementation((scope, id) => new aws_sns.Topic(scope, id));
38
38
 
39
+ jest
40
+ .spyOn(aws_secretsmanager.Secret, 'fromSecretPartialArn')
41
+ .mockImplementation(
42
+ (scope, id) => new aws_secretsmanager.Secret(scope, id),
43
+ );
44
+
39
45
  const app = new App();
40
46
 
41
47
  const stack = new AppStack(app, 'appStack');
@@ -47,13 +53,18 @@ it.each(['dev', 'prod'])(
47
53
  /"S3Key":"([0-9a-f]+)\.zip"/g,
48
54
  (_, hash) => `"S3Key":"${'x'.repeat(hash.length)}.zip"`,
49
55
  )
50
- .replaceAll(
56
+ .replace(
51
57
  /workerCurrentVersion([0-9a-zA-Z]+)"/g,
52
58
  (_, hash) => `workerCurrentVersion${'x'.repeat(hash.length)}"`,
53
59
  )
54
60
  .replaceAll(
55
61
  /"Value":"\d+\.\d+\.\d+-([^"]+)"/g,
56
62
  (_, hash) => `"Value": "x.x.x-${'x'.repeat(hash.length)}"`,
63
+ )
64
+ .replace(
65
+ /"DD_TAGS":"git.commit.sha:([0-9a-f]+),git.repository_url:([^\"]+)"/g,
66
+ (_, sha, url) =>
67
+ `"DD_TAGS":"git.commit.sha:${'x'.repeat(sha.length)},git.repository_url:${'x'.repeat(url.length)}"`,
57
68
  );
58
69
  expect(JSON.parse(json)).toMatchSnapshot();
59
70
  },
@@ -8,11 +8,12 @@ import {
8
8
  aws_lambda,
9
9
  aws_lambda_event_sources,
10
10
  aws_lambda_nodejs,
11
+ aws_secretsmanager,
11
12
  aws_sns,
12
- aws_sns_subscriptions,
13
13
  aws_sqs,
14
14
  } from 'aws-cdk-lib';
15
15
  import type { Construct } from 'constructs';
16
+ import { Datadog, getExtensionLayerArn } from 'datadog-cdk-constructs-v2';
16
17
 
17
18
  import { config } from './config';
18
19
 
@@ -49,13 +50,42 @@ export class AppStack extends Stack {
49
50
  encryptionMasterKey: kmsKey,
50
51
  });
51
52
 
52
- const topic = aws_sns.Topic.fromTopicArn(
53
+ // const topic = aws_sns.Topic.fromTopicArn(
54
+ // this,
55
+ // 'source-topic',
56
+ // config.sourceSnsTopicArn,
57
+ // );
58
+
59
+ // topic.addSubscription(
60
+ // new aws_sns_subscriptions.SqsSubscription(queue, {
61
+ // rawMessageDelivery: true, // Remove this property if you require end to end datadog tracing
62
+ // }),
63
+ // );
64
+
65
+ const snsKey = aws_kms.Alias.fromAliasName(
53
66
  this,
54
- 'source-topic',
55
- config.sourceSnsTopicArn,
67
+ 'alias-aws-sns',
68
+ 'alias/aws/sns',
56
69
  );
57
70
 
58
- topic.addSubscription(new aws_sns_subscriptions.SqsSubscription(queue));
71
+ const destinationTopic = new aws_sns.Topic(this, 'destination-topic', {
72
+ masterKey: snsKey,
73
+ topicName: '<%- serviceName %>',
74
+ });
75
+
76
+ const datadogSecret = aws_secretsmanager.Secret.fromSecretPartialArn(
77
+ this,
78
+ 'datadog-api-key-secret',
79
+ config.datadogApiKeySecretArn,
80
+ );
81
+
82
+ const datadog = new Datadog(this, 'datadog', {
83
+ apiKeySecret: datadogSecret,
84
+ addLayers: false,
85
+ enableDatadogLogs: false,
86
+ flushMetricsToLogs: false,
87
+ extensionLayerVersion: 58,
88
+ });
59
89
 
60
90
  const architecture = '<%- lambdaCdkArchitecture %>';
61
91
 
@@ -85,17 +115,33 @@ export class AppStack extends Stack {
85
115
  ...defaultWorkerConfig,
86
116
  entry: './src/app.ts',
87
117
  timeout: Duration.seconds(30),
88
- bundling: defaultWorkerBundlingConfig,
118
+ bundling: {
119
+ ...defaultWorkerBundlingConfig,
120
+ nodeModules: ['datadog-lambda-js', 'dd-trace'],
121
+ },
89
122
  functionName: '<%- serviceName %>',
90
123
  environment: {
91
124
  ...defaultWorkerEnvironment,
92
125
  ...config.workerLambda.environment,
126
+ DESTINATION_SNS_TOPIC_ARN: destinationTopic.topicArn,
93
127
  },
94
128
  // https://github.com/aws/aws-cdk/issues/28237
95
129
  // This forces the lambda to be updated on every deployment
96
130
  // If you do not wish to use hotswap, you can remove the new Date().toISOString() from the description
97
131
  description: `Updated at ${new Date().toISOString()}`,
98
132
  reservedConcurrentExecutions: config.workerLambda.reservedConcurrency,
133
+ layers: [
134
+ // Workaround for https://github.com/DataDog/datadog-cdk-constructs/issues/201
135
+ aws_lambda.LayerVersion.fromLayerVersionArn(
136
+ this,
137
+ 'datadog-layer',
138
+ getExtensionLayerArn(
139
+ this.region,
140
+ datadog.props.extensionLayerVersion as number,
141
+ defaultWorkerConfig.architecture === aws_lambda.Architecture.ARM_64,
142
+ ),
143
+ ),
144
+ ],
99
145
  });
100
146
 
101
147
  const workerDeployment = new LambdaDeployment(this, 'workerDeployment', {
@@ -16,6 +16,7 @@ interface Config {
16
16
  VERSION: string;
17
17
  };
18
18
  };
19
+ datadogApiKeySecretArn: string;
19
20
  sourceSnsTopicArn: string;
20
21
  }
21
22
 
@@ -30,6 +31,7 @@ const configs: Record<Environment, Config> = {
30
31
  VERSION: Env.string('VERSION', { default: 'local' }),
31
32
  },
32
33
  },
34
+ datadogApiKeySecretArn: 'TODO: datadogApiKeySecretArn',
33
35
  sourceSnsTopicArn: 'TODO: sourceSnsTopicArn',
34
36
  },
35
37
  prod: {
@@ -42,6 +44,7 @@ const configs: Record<Environment, Config> = {
42
44
  VERSION: Env.string('VERSION', { default: 'local' }),
43
45
  },
44
46
  },
47
+ datadogApiKeySecretArn: 'TODO: datadogApiKeySecretArn',
45
48
  sourceSnsTopicArn: 'TODO: sourceSnsTopicArn',
46
49
  },
47
50
  };
@@ -16,19 +16,26 @@
16
16
  "@aws-sdk/client-codedeploy": "^3.363.0",
17
17
  "@aws-sdk/client-lambda": "^3.363.0",
18
18
  "@aws-sdk/client-sns": "^3.363.0",
19
- "@seek/logger": "^6.0.0",
19
+ "@seek/logger": "^9.0.0",
20
+ "datadog-lambda-js": "^8.0.0",
21
+ "dd-trace": "^5.0.0",
20
22
  "skuba-dive": "^2.0.0",
21
23
  "zod": "^3.19.1"
22
24
  },
23
25
  "devDependencies": {
24
26
  "@seek/aws-codedeploy-infra": "^2.1.0",
25
27
  "@types/aws-lambda": "^8.10.82",
28
+ "@types/chance": "^1.1.3",
26
29
  "@types/node": "^20.16.5",
27
30
  "aws-cdk": "^2.109.0",
28
31
  "aws-cdk-lib": "^2.109.0",
32
+ "aws-sdk-client-mock": "^4.0.0",
33
+ "aws-sdk-client-mock-jest": "^4.0.0",
34
+ "chance": "^1.1.8",
29
35
  "constructs": "^10.0.17",
36
+ "datadog-cdk-constructs-v2": "^1.13.0",
30
37
  "pino-pretty": "^11.0.0",
31
- "skuba": "9.0.0-renovate-eslint-9.x-20240923103202"
38
+ "skuba": "9.0.1-upgrade-cdk-template-20241002233314"
32
39
  },
33
40
  "packageManager": "pnpm@9.11.0",
34
41
  "engines": {
@@ -0,0 +1,116 @@
1
+ import { PublishCommand } from '@aws-sdk/client-sns';
2
+
3
+ import { metricsClient } from 'src/framework/metrics';
4
+ import { createCtx, createSqsEvent } from 'src/testing/handler';
5
+ import { logger } from 'src/testing/logging';
6
+ import { scoringService, sns } from 'src/testing/services';
7
+ import { chance, mockJobPublishedEvent } from 'src/testing/types';
8
+
9
+ import * as app from './app';
10
+
11
+ describe('app', () => {
12
+ it('exports a handler', () => expect(app).toHaveProperty('handler'));
13
+ });
14
+
15
+ describe('handler', () => {
16
+ const ctx = createCtx();
17
+
18
+ const jobPublished = mockJobPublishedEvent({ entityId: chance.name() });
19
+
20
+ const score = chance.floating({ max: 1, min: 0 });
21
+
22
+ const distribution = jest
23
+ .spyOn(metricsClient, 'distribution')
24
+ .mockReturnValue();
25
+
26
+ beforeAll(logger.spy);
27
+ beforeAll(scoringService.spy);
28
+
29
+ beforeEach(() => {
30
+ scoringService.request.mockResolvedValue(score);
31
+ sns.publish.resolves({ MessageId: chance.guid({ version: 4 }) });
32
+ });
33
+
34
+ afterEach(() => {
35
+ logger.clear();
36
+ distribution.mockClear();
37
+ scoringService.clear();
38
+ sns.clear();
39
+ });
40
+
41
+ it('handles one record', async () => {
42
+ const event = createSqsEvent([JSON.stringify(jobPublished)]);
43
+
44
+ await expect(app.handler(event, ctx)).resolves.toBeUndefined();
45
+
46
+ expect(scoringService.request).toHaveBeenCalledTimes(1);
47
+
48
+ expect(logger.error).not.toHaveBeenCalled();
49
+
50
+ expect(logger.debug.mock.calls).toEqual([
51
+ [{ count: 1 }, 'Received jobs'],
52
+ [{ snsMessageId: expect.any(String) }, 'Scored job'],
53
+ ['Function succeeded'],
54
+ ]);
55
+
56
+ expect(distribution.mock.calls).toEqual([
57
+ ['job.received', 1],
58
+ ['job.scored', 1],
59
+ ]);
60
+
61
+ expect(sns.client).toReceiveCommandTimes(PublishCommand, 1);
62
+ });
63
+
64
+ it('throws on invalid input', () => {
65
+ const event = createSqsEvent(['}']);
66
+
67
+ return expect(app.handler(event, ctx)).rejects.toThrow('Function failed');
68
+ });
69
+
70
+ it('bubbles up scoring service error', async () => {
71
+ const err = Error(chance.sentence());
72
+
73
+ scoringService.request.mockRejectedValue(err);
74
+
75
+ const event = createSqsEvent([JSON.stringify(jobPublished)]);
76
+
77
+ await expect(app.handler(event, ctx)).rejects.toThrow('Function failed');
78
+
79
+ expect(logger.error).toHaveBeenCalledWith({ err }, 'Function failed');
80
+ });
81
+
82
+ it('bubbles up SNS error', async () => {
83
+ const err = Error(chance.sentence());
84
+
85
+ sns.publish.rejects(err);
86
+
87
+ const event = createSqsEvent([JSON.stringify(jobPublished)]);
88
+
89
+ await expect(app.handler(event, ctx)).rejects.toThrow('Function failed');
90
+
91
+ expect(logger.error).toHaveBeenCalledWith({ err }, 'Function failed');
92
+ });
93
+
94
+ it('throws on zero records', async () => {
95
+ const err = new Error('Received 0 records');
96
+
97
+ const event = createSqsEvent([]);
98
+
99
+ await expect(app.handler(event, ctx)).rejects.toThrow('Function failed');
100
+
101
+ expect(logger.error).toHaveBeenCalledWith({ err }, 'Function failed');
102
+ });
103
+
104
+ it('throws on multiple records', async () => {
105
+ const err = new Error('Received 2 records');
106
+
107
+ const event = createSqsEvent([
108
+ JSON.stringify(jobPublished),
109
+ JSON.stringify(jobPublished),
110
+ ]);
111
+
112
+ await expect(app.handler(event, ctx)).rejects.toThrow('Function failed');
113
+
114
+ expect(logger.error).toHaveBeenCalledWith({ err }, 'Function failed');
115
+ });
116
+ });
@@ -1,35 +1,57 @@
1
- import createLogger from '@seek/logger';
2
- import type { SQSEvent, SQSHandler } from 'aws-lambda';
1
+ import 'skuba-dive/register';
3
2
 
4
- import { config } from './config';
3
+ import type { SQSEvent } from 'aws-lambda';
5
4
 
6
- export const logger = createLogger({
7
- base: {
8
- environment: config.environment,
9
- version: config.version,
10
- },
11
-
12
- level: config.logLevel,
13
-
14
- name: config.name,
15
-
16
- transport:
17
- config.environment === 'local' ? { target: 'pino-pretty' } : undefined,
18
- });
5
+ import { createHandler } from 'src/framework/handler';
6
+ import { logger } from 'src/framework/logging';
7
+ import { metricsClient } from 'src/framework/metrics';
8
+ import { validateJson } from 'src/framework/validation';
9
+ import { scoreJobPublishedEvent, scoringService } from 'src/services/jobScorer';
10
+ import { sendPipelineEvent } from 'src/services/pipelineEventSender';
11
+ import { JobPublishedEventSchema } from 'src/types/pipelineEvents';
19
12
 
20
13
  /**
21
14
  * Tests connectivity to ensure appropriate access and network configuration.
22
15
  */
23
- const smokeTest = async () => Promise.resolve();
16
+ const smokeTest = async () => {
17
+ await Promise.all([scoringService.smokeTest(), sendPipelineEvent({}, true)]);
18
+ };
24
19
 
25
- export const handler: SQSHandler = (event: SQSEvent) => {
20
+ export const handler = createHandler<SQSEvent>(async (event) => {
26
21
  // Treat an empty object as our smoke test event.
27
22
  if (!Object.keys(event).length) {
28
23
  logger.debug('Received smoke test request');
29
24
  return smokeTest();
30
25
  }
31
26
 
32
- logger.info('Hello World!');
27
+ const count = event.Records.length;
33
28
 
34
- return;
35
- };
29
+ if (count !== 1) {
30
+ throw Error(`Received ${count} records`);
31
+ }
32
+
33
+ logger.debug({ count }, 'Received jobs');
34
+
35
+ metricsClient.distribution('job.received', event.Records.length);
36
+
37
+ const record = event.Records[0];
38
+ if (!record) {
39
+ throw new Error('Malformed SQS event with no records');
40
+ }
41
+
42
+ const { body } = record;
43
+
44
+ // TODO: this throws an error, which will cause the Lambda function to retry
45
+ // the event and eventually send it to your dead-letter queue. If you don't
46
+ // trust your source to provide consistently well-formed input, consider
47
+ // catching and handling this error in code.
48
+ const publishedJob = validateJson(body, JobPublishedEventSchema);
49
+
50
+ const scoredJob = await scoreJobPublishedEvent(publishedJob);
51
+
52
+ const snsMessageId = await sendPipelineEvent(scoredJob);
53
+
54
+ logger.debug({ snsMessageId }, 'Scored job');
55
+
56
+ metricsClient.distribution('job.scored', 1);
57
+ });