skuba 4.4.0 → 5.0.0-beta.0

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 (321) hide show
  1. package/config/tsconfig.json +1 -0
  2. package/jest/moduleNameMapper.js +50 -78
  3. package/jest/moduleNameMapper.test.ts +4 -14
  4. package/jest/transform.js +10 -1
  5. package/jest/tsConfig.js +26 -0
  6. package/lib/api/buildkite/annotate.js +46 -33
  7. package/lib/api/buildkite/annotate.js.map +7 -1
  8. package/lib/api/buildkite/index.js +31 -7
  9. package/lib/api/buildkite/index.js.map +7 -1
  10. package/lib/api/buildkite/md.js +31 -8
  11. package/lib/api/buildkite/md.js.map +7 -1
  12. package/lib/api/git/commit.js +45 -17
  13. package/lib/api/git/commit.js.map +7 -1
  14. package/lib/api/git/commitAllChanges.js +57 -27
  15. package/lib/api/git/commitAllChanges.js.map +7 -1
  16. package/lib/api/git/currentBranch.js +50 -28
  17. package/lib/api/git/currentBranch.js.map +7 -1
  18. package/lib/api/git/getChangedFiles.js +49 -27
  19. package/lib/api/git/getChangedFiles.js.map +7 -1
  20. package/lib/api/git/index.js +54 -22
  21. package/lib/api/git/index.js.map +7 -1
  22. package/lib/api/git/log.js +63 -42
  23. package/lib/api/git/log.js.map +7 -1
  24. package/lib/api/git/pull.js +62 -31
  25. package/lib/api/git/pull.js.map +7 -1
  26. package/lib/api/git/push.js +63 -31
  27. package/lib/api/git/push.js.map +7 -1
  28. package/lib/api/git/remote.js +59 -56
  29. package/lib/api/git/remote.js.map +7 -1
  30. package/lib/api/git/reset.js +55 -27
  31. package/lib/api/git/reset.js.map +7 -1
  32. package/lib/api/git/statusMatrix.js +46 -13
  33. package/lib/api/git/statusMatrix.js.map +7 -1
  34. package/lib/api/github/checkRun.js +70 -79
  35. package/lib/api/github/checkRun.js.map +7 -1
  36. package/lib/api/github/environment.js +40 -33
  37. package/lib/api/github/environment.js.map +7 -1
  38. package/lib/api/github/index.js +47 -17
  39. package/lib/api/github/index.js.map +7 -1
  40. package/lib/api/github/issueComment.js +73 -81
  41. package/lib/api/github/issueComment.js.map +7 -1
  42. package/lib/api/github/pullRequest.js +60 -61
  43. package/lib/api/github/pullRequest.js.map +7 -1
  44. package/lib/api/github/push.js +138 -133
  45. package/lib/api/github/push.js.map +7 -1
  46. package/lib/api/jest/index.d.ts +2 -1
  47. package/lib/api/jest/index.js +35 -14
  48. package/lib/api/jest/index.js.map +7 -1
  49. package/lib/api/net/compose.js +45 -17
  50. package/lib/api/net/compose.js.map +7 -1
  51. package/lib/api/net/index.js +28 -5
  52. package/lib/api/net/index.js.map +7 -1
  53. package/lib/api/net/socket.js +58 -36
  54. package/lib/api/net/socket.js.map +7 -1
  55. package/lib/api/net/waitFor.js +38 -18
  56. package/lib/api/net/waitFor.js.map +7 -1
  57. package/lib/cli/adapter/eslint.js +95 -72
  58. package/lib/cli/adapter/eslint.js.map +7 -1
  59. package/lib/cli/adapter/prettier.js +126 -124
  60. package/lib/cli/adapter/prettier.js.map +7 -1
  61. package/lib/cli/build/args.d.ts +7 -0
  62. package/lib/cli/build/args.js +69 -0
  63. package/lib/cli/build/args.js.map +7 -0
  64. package/lib/cli/build/esbuild.d.ts +5 -0
  65. package/lib/cli/build/esbuild.js +128 -0
  66. package/lib/cli/build/esbuild.js.map +7 -0
  67. package/lib/cli/build/index.d.ts +1 -1
  68. package/lib/cli/build/index.js +68 -5
  69. package/lib/cli/build/index.js.map +7 -1
  70. package/lib/cli/build/tsc.d.ts +1 -1
  71. package/lib/cli/build/tsc.js +34 -23
  72. package/lib/cli/build/tsc.js.map +7 -1
  73. package/lib/cli/buildPackage.js +53 -23
  74. package/lib/cli/buildPackage.js.map +7 -1
  75. package/lib/cli/configure/addEmptyExports.d.ts +5 -0
  76. package/lib/cli/configure/addEmptyExports.js +67 -0
  77. package/lib/cli/configure/addEmptyExports.js.map +7 -0
  78. package/lib/cli/configure/analyseConfiguration.js +61 -31
  79. package/lib/cli/configure/analyseConfiguration.js.map +7 -1
  80. package/lib/cli/configure/analyseDependencies.js +122 -113
  81. package/lib/cli/configure/analyseDependencies.js.map +7 -1
  82. package/lib/cli/configure/analysis/diff.js +37 -11
  83. package/lib/cli/configure/analysis/diff.js.map +7 -1
  84. package/lib/cli/configure/analysis/files.js +49 -22
  85. package/lib/cli/configure/analysis/files.js.map +7 -1
  86. package/lib/cli/configure/analysis/git.js +46 -16
  87. package/lib/cli/configure/analysis/git.js.map +7 -1
  88. package/lib/cli/configure/analysis/package.js +99 -52
  89. package/lib/cli/configure/analysis/package.js.map +7 -1
  90. package/lib/cli/configure/analysis/project.js +90 -54
  91. package/lib/cli/configure/analysis/project.js.map +7 -1
  92. package/lib/cli/configure/dependencies/index.js +40 -13
  93. package/lib/cli/configure/dependencies/index.js.map +7 -1
  94. package/lib/cli/configure/dependencies/seekDatadogCustomMetrics.js +57 -32
  95. package/lib/cli/configure/dependencies/seekDatadogCustomMetrics.js.map +7 -1
  96. package/lib/cli/configure/dependencies/seekKoala.js +53 -31
  97. package/lib/cli/configure/dependencies/seekKoala.js.map +7 -1
  98. package/lib/cli/configure/dependencies/skuba.js +49 -27
  99. package/lib/cli/configure/dependencies/skuba.js.map +7 -1
  100. package/lib/cli/configure/dependencies/skubaDeps.js +49 -28
  101. package/lib/cli/configure/dependencies/skubaDeps.js.map +7 -1
  102. package/lib/cli/configure/dependencies/skubaDive.js +65 -38
  103. package/lib/cli/configure/dependencies/skubaDive.js.map +7 -1
  104. package/lib/cli/configure/ensureTemplateCompletion.js +69 -41
  105. package/lib/cli/configure/ensureTemplateCompletion.js.map +7 -1
  106. package/lib/cli/configure/getEntryPoint.js +62 -32
  107. package/lib/cli/configure/getEntryPoint.js.map +7 -1
  108. package/lib/cli/configure/getProjectType.js +50 -28
  109. package/lib/cli/configure/getProjectType.js.map +7 -1
  110. package/lib/cli/configure/index.js +134 -109
  111. package/lib/cli/configure/index.js.map +7 -1
  112. package/lib/cli/configure/modules/eslint.js +57 -26
  113. package/lib/cli/configure/modules/eslint.js.map +7 -1
  114. package/lib/cli/configure/modules/ignore.js +37 -14
  115. package/lib/cli/configure/modules/ignore.js.map +7 -1
  116. package/lib/cli/configure/modules/index.js +53 -28
  117. package/lib/cli/configure/modules/index.js.map +7 -1
  118. package/lib/cli/configure/modules/jest.js +84 -47
  119. package/lib/cli/configure/modules/jest.js.map +7 -1
  120. package/lib/cli/configure/modules/nodemon.js +29 -6
  121. package/lib/cli/configure/modules/nodemon.js.map +7 -1
  122. package/lib/cli/configure/modules/package.js +113 -92
  123. package/lib/cli/configure/modules/package.js.map +7 -1
  124. package/lib/cli/configure/modules/prettier.js +48 -19
  125. package/lib/cli/configure/modules/prettier.js.map +7 -1
  126. package/lib/cli/configure/modules/renovate.js +52 -39
  127. package/lib/cli/configure/modules/renovate.js.map +7 -1
  128. package/lib/cli/configure/modules/serverless.js +33 -15
  129. package/lib/cli/configure/modules/serverless.js.map +7 -1
  130. package/lib/cli/configure/modules/skubaDive.js +63 -37
  131. package/lib/cli/configure/modules/skubaDive.js.map +7 -1
  132. package/lib/cli/configure/modules/tsconfig.js +79 -63
  133. package/lib/cli/configure/modules/tsconfig.js.map +7 -1
  134. package/lib/cli/configure/modules/tslint.js +29 -6
  135. package/lib/cli/configure/modules/tslint.js.map +7 -1
  136. package/lib/cli/configure/processing/deleteFiles.js +30 -8
  137. package/lib/cli/configure/processing/deleteFiles.js.map +7 -1
  138. package/lib/cli/configure/processing/ignoreFile.js +65 -59
  139. package/lib/cli/configure/processing/ignoreFile.js.map +7 -1
  140. package/lib/cli/configure/processing/javascript.js +35 -15
  141. package/lib/cli/configure/processing/javascript.js.map +7 -1
  142. package/lib/cli/configure/processing/json.js +51 -20
  143. package/lib/cli/configure/processing/json.js.map +7 -1
  144. package/lib/cli/configure/processing/loadFiles.js +30 -8
  145. package/lib/cli/configure/processing/loadFiles.js.map +7 -1
  146. package/lib/cli/configure/processing/module.js +37 -14
  147. package/lib/cli/configure/processing/module.js.map +7 -1
  148. package/lib/cli/configure/processing/package.js +73 -43
  149. package/lib/cli/configure/processing/package.js.map +7 -1
  150. package/lib/cli/configure/processing/prettier.js +37 -11
  151. package/lib/cli/configure/processing/prettier.js.map +7 -1
  152. package/lib/cli/configure/processing/record.js +54 -31
  153. package/lib/cli/configure/processing/record.js.map +7 -1
  154. package/lib/cli/configure/processing/typescript.js +176 -158
  155. package/lib/cli/configure/processing/typescript.js.map +7 -1
  156. package/lib/cli/configure/refreshIgnoreFiles.js +64 -40
  157. package/lib/cli/configure/refreshIgnoreFiles.js.map +7 -1
  158. package/lib/cli/configure/types.js +16 -2
  159. package/lib/cli/configure/types.js.map +7 -1
  160. package/lib/cli/format.js +58 -31
  161. package/lib/cli/format.js.map +7 -1
  162. package/lib/cli/help.js +31 -8
  163. package/lib/cli/help.js.map +7 -1
  164. package/lib/cli/init/getConfig.js +238 -207
  165. package/lib/cli/init/getConfig.js.map +7 -1
  166. package/lib/cli/init/git.js +70 -63
  167. package/lib/cli/init/git.js.map +7 -1
  168. package/lib/cli/init/index.js +133 -100
  169. package/lib/cli/init/index.js.map +7 -1
  170. package/lib/cli/init/prompts.js +84 -54
  171. package/lib/cli/init/prompts.js.map +7 -1
  172. package/lib/cli/init/types.js +52 -52
  173. package/lib/cli/init/types.js.map +7 -1
  174. package/lib/cli/init/validation.js +34 -12
  175. package/lib/cli/init/validation.js.map +7 -1
  176. package/lib/cli/init/writePackageJson.js +51 -22
  177. package/lib/cli/init/writePackageJson.js.map +7 -1
  178. package/lib/cli/lint/annotate/buildkite/eslint.js +34 -28
  179. package/lib/cli/lint/annotate/buildkite/eslint.js.map +7 -1
  180. package/lib/cli/lint/annotate/buildkite/index.js +50 -44
  181. package/lib/cli/lint/annotate/buildkite/index.js.map +7 -1
  182. package/lib/cli/lint/annotate/buildkite/prettier.js +41 -35
  183. package/lib/cli/lint/annotate/buildkite/prettier.js.map +7 -1
  184. package/lib/cli/lint/annotate/buildkite/tsc.js +39 -39
  185. package/lib/cli/lint/annotate/buildkite/tsc.js.map +7 -1
  186. package/lib/cli/lint/annotate/github/eslint.js +40 -16
  187. package/lib/cli/lint/annotate/github/eslint.js.map +7 -1
  188. package/lib/cli/lint/annotate/github/index.js +55 -51
  189. package/lib/cli/lint/annotate/github/index.js.map +7 -1
  190. package/lib/cli/lint/annotate/github/prettier.js +36 -13
  191. package/lib/cli/lint/annotate/github/prettier.js.map +7 -1
  192. package/lib/cli/lint/annotate/github/tsc.js +52 -52
  193. package/lib/cli/lint/annotate/github/tsc.js.map +7 -1
  194. package/lib/cli/lint/annotate/index.js +33 -10
  195. package/lib/cli/lint/annotate/index.js.map +7 -1
  196. package/lib/cli/lint/autofix.js +122 -117
  197. package/lib/cli/lint/autofix.js.map +7 -1
  198. package/lib/cli/lint/eslint.js +48 -18
  199. package/lib/cli/lint/eslint.js.map +7 -1
  200. package/lib/cli/lint/external.js +102 -81
  201. package/lib/cli/lint/external.js.map +7 -1
  202. package/lib/cli/lint/index.js +42 -18
  203. package/lib/cli/lint/index.js.map +7 -1
  204. package/lib/cli/lint/internal.js +54 -21
  205. package/lib/cli/lint/internal.js.map +7 -1
  206. package/lib/cli/lint/prettier.js +48 -18
  207. package/lib/cli/lint/prettier.js.map +7 -1
  208. package/lib/cli/lint/tsc.js +56 -32
  209. package/lib/cli/lint/tsc.js.map +7 -1
  210. package/lib/cli/lint/types.js +16 -2
  211. package/lib/cli/lint/types.js.map +7 -1
  212. package/lib/cli/node.js +65 -58
  213. package/lib/cli/node.js.map +7 -1
  214. package/lib/cli/release.js +29 -6
  215. package/lib/cli/release.js.map +7 -1
  216. package/lib/cli/start.js +61 -24
  217. package/lib/cli/start.js.map +7 -1
  218. package/lib/cli/test/index.js +35 -11
  219. package/lib/cli/test/index.js.map +7 -1
  220. package/lib/cli/test/reporters/github/annotations.js +89 -92
  221. package/lib/cli/test/reporters/github/annotations.js.map +7 -1
  222. package/lib/cli/test/reporters/github/index.js +68 -68
  223. package/lib/cli/test/reporters/github/index.js.map +7 -1
  224. package/lib/cli/version.js +31 -8
  225. package/lib/cli/version.js.map +7 -1
  226. package/lib/enquirer.d.js +2 -0
  227. package/lib/enquirer.d.js.map +7 -0
  228. package/lib/index.d.ts +6 -0
  229. package/lib/index.js +45 -41
  230. package/lib/index.js.map +7 -1
  231. package/lib/skuba.js +45 -38
  232. package/lib/skuba.js.map +7 -1
  233. package/lib/utils/args.js +97 -96
  234. package/lib/utils/args.js.map +7 -1
  235. package/lib/utils/command.js +64 -33
  236. package/lib/utils/command.js.map +7 -1
  237. package/lib/utils/copy.js +88 -51
  238. package/lib/utils/copy.js.map +7 -1
  239. package/lib/utils/dir.js +79 -67
  240. package/lib/utils/dir.js.map +7 -1
  241. package/lib/utils/env.js +27 -7
  242. package/lib/utils/env.js.map +7 -1
  243. package/lib/utils/error.js +58 -56
  244. package/lib/utils/error.js.map +7 -1
  245. package/lib/utils/exec.js +139 -110
  246. package/lib/utils/exec.js.map +7 -1
  247. package/lib/utils/help.js +31 -8
  248. package/lib/utils/help.js.map +7 -1
  249. package/lib/utils/logging.js +52 -24
  250. package/lib/utils/logging.js.map +7 -1
  251. package/lib/utils/logo.js +65 -27
  252. package/lib/utils/logo.js.map +7 -1
  253. package/lib/utils/manifest.d.ts +1 -0
  254. package/lib/utils/manifest.js +65 -51
  255. package/lib/utils/manifest.js.map +7 -1
  256. package/lib/utils/port.js +38 -17
  257. package/lib/utils/port.js.map +7 -1
  258. package/lib/utils/template.js +106 -87
  259. package/lib/utils/template.js.map +7 -1
  260. package/lib/utils/validation.js +43 -18
  261. package/lib/utils/validation.js.map +7 -1
  262. package/lib/utils/version.js +73 -40
  263. package/lib/utils/version.js.map +7 -1
  264. package/lib/utils/wait.js +52 -25
  265. package/lib/utils/wait.js.map +7 -1
  266. package/lib/utils/worker.js +59 -44
  267. package/lib/utils/worker.js.map +7 -1
  268. package/lib/wrapper/function-arguments.d.js +2 -0
  269. package/lib/wrapper/function-arguments.d.js.map +7 -0
  270. package/lib/wrapper/functionHandler.js +56 -29
  271. package/lib/wrapper/functionHandler.js.map +7 -1
  272. package/lib/wrapper/http.js +66 -56
  273. package/lib/wrapper/http.js.map +7 -1
  274. package/lib/wrapper/index.js +9 -21
  275. package/lib/wrapper/index.js.map +7 -1
  276. package/lib/wrapper/main.js +39 -20
  277. package/lib/wrapper/main.js.map +7 -1
  278. package/lib/wrapper/requestListener.js +50 -35
  279. package/lib/wrapper/requestListener.js.map +7 -1
  280. package/package.json +21 -14
  281. package/template/base/jest.config.ts +0 -6
  282. package/template/base/jest.setup.ts +2 -0
  283. package/template/express-rest-api/.buildkite/pipeline.yml +1 -1
  284. package/template/express-rest-api/package.json +1 -1
  285. package/template/greeter/.buildkite/pipeline.yml +1 -1
  286. package/template/greeter/package.json +1 -1
  287. package/template/greeter/src/app.test.ts +3 -1
  288. package/template/koa-rest-api/.buildkite/pipeline.yml +1 -1
  289. package/template/koa-rest-api/package.json +6 -9
  290. package/template/koa-rest-api/src/api/jobs/postJob.test.ts +3 -7
  291. package/template/koa-rest-api/src/api/jobs/postJob.ts +2 -2
  292. package/template/koa-rest-api/src/framework/validation.test.ts +17 -15
  293. package/template/koa-rest-api/src/framework/validation.ts +50 -9
  294. package/template/koa-rest-api/src/storage/jobs.ts +2 -2
  295. package/template/koa-rest-api/src/testing/types.ts +5 -10
  296. package/template/koa-rest-api/src/types/jobs.ts +5 -10
  297. package/template/lambda-sqs-worker/.buildkite/pipeline.yml +3 -3
  298. package/template/lambda-sqs-worker/README.md +1 -1
  299. package/template/lambda-sqs-worker/package.json +4 -4
  300. package/template/lambda-sqs-worker/serverless.yml +17 -2
  301. package/template/lambda-sqs-worker/src/app.test.ts +19 -17
  302. package/template/lambda-sqs-worker/src/app.ts +8 -8
  303. package/template/lambda-sqs-worker/src/config.ts +3 -0
  304. package/template/lambda-sqs-worker/src/framework/handler.test.ts +7 -7
  305. package/template/lambda-sqs-worker/src/framework/handler.ts +31 -8
  306. package/template/lambda-sqs-worker/src/framework/metrics.ts +10 -6
  307. package/template/lambda-sqs-worker/src/framework/validation.test.ts +37 -17
  308. package/template/lambda-sqs-worker/src/framework/validation.ts +4 -2
  309. package/template/lambda-sqs-worker/src/services/jobScorer.ts +2 -2
  310. package/template/lambda-sqs-worker/src/testing/types.ts +5 -10
  311. package/template/lambda-sqs-worker/src/types/jobScorer.ts +9 -16
  312. package/template/lambda-sqs-worker/src/types/pipelineEvents.ts +13 -20
  313. package/template/lambda-sqs-worker-cdk/.buildkite/pipeline.yml +3 -3
  314. package/template/lambda-sqs-worker-cdk/infra/__snapshots__/appStack.test.ts.snap +6 -0
  315. package/template/lambda-sqs-worker-cdk/infra/appStack.ts +3 -3
  316. package/template/lambda-sqs-worker-cdk/infra/index.ts +2 -2
  317. package/template/lambda-sqs-worker-cdk/package.json +2 -2
  318. package/template/lambda-sqs-worker-cdk/shared/context-types.ts +16 -25
  319. package/template/oss-npm-package/_package.json +1 -1
  320. package/template/private-npm-package/_package.json +1 -1
  321. package/jest/resolver.js +0 -24
@@ -1,23 +1,64 @@
1
+ import { ErrorMiddleware } from 'seek-koala';
2
+ import { z } from 'zod';
3
+
1
4
  import { Context } from 'src/types/koa';
2
5
 
6
+ /**
7
+ * Converts a `ZodError` into an `invalidFields` object
8
+ *
9
+ * For example, the `ZodError`:
10
+ *
11
+ * ```json
12
+ * {
13
+ * "issues": [
14
+ * {
15
+ * "code": "invalid_type",
16
+ * "expected": "string",
17
+ * "received": "undefined",
18
+ * "path": ["advertiserId"],
19
+ * "message": "advertiserId is required in the URL"
20
+ * }
21
+ * ],
22
+ * "name": "ZodError"
23
+ * }
24
+ * ```
25
+ *
26
+ * Returns:
27
+ *
28
+ * ```json
29
+ * { "/advertiserId": "advertiserId is required in the URL" }
30
+ * ```
31
+ */
32
+ const parseInvalidFieldsFromError = ({
33
+ errors,
34
+ }: z.ZodError): Record<string, string> =>
35
+ Object.fromEntries(
36
+ errors.map((err) => [`/${err.path.join('/')}`, err.message]),
37
+ );
38
+
3
39
  export const validate = <T>({
4
40
  ctx,
5
41
  input,
6
- filter,
42
+ schema,
7
43
  }: {
8
44
  ctx: Context;
9
45
  input: unknown;
10
- filter: (data: unknown) => T;
46
+ schema: z.ZodSchema<T>;
11
47
  }) => {
12
- try {
13
- return filter(input);
14
- } catch (err) {
15
- // TODO: consider providing structured error messages for your consumers.
16
- return ctx.throw(422, err instanceof Error ? err.message : String(err));
48
+ const parseResult = schema.safeParse(input);
49
+ if (parseResult.success === false) {
50
+ return ctx.throw(
51
+ 422,
52
+ new ErrorMiddleware.JsonResponse('Input validation failed', {
53
+ message: 'Input validation failed',
54
+ invalidFields: parseInvalidFieldsFromError(parseResult.error),
55
+ }),
56
+ );
17
57
  }
58
+ return parseResult.data;
18
59
  };
19
60
 
20
61
  export const validateRequestBody = <T>(
21
62
  ctx: Context,
22
- filter: (input: unknown) => T,
23
- ): T => validate({ ctx, input: ctx.request.body as unknown, filter });
63
+ schema: z.ZodSchema<T>,
64
+ ): T => validate<T>({ ctx, input: ctx.request.body as unknown, schema });
@@ -1,11 +1,11 @@
1
- import { v4 as uuidv4 } from 'uuid';
1
+ import { randomUUID } from 'crypto';
2
2
 
3
3
  import { Job, JobInput } from 'src/types/jobs';
4
4
 
5
5
  const jobStore: Record<string, Job> = {};
6
6
 
7
7
  export const createJob = (jobInput: JobInput): Promise<Job> => {
8
- const id = uuidv4();
8
+ const id = randomUUID();
9
9
 
10
10
  const job = { ...jobInput, id };
11
11
 
@@ -1,20 +1,15 @@
1
- /* eslint-disable new-cap */
2
-
3
1
  import { Chance } from 'chance';
4
- import * as t from 'runtypes';
5
- import checkFilter from 'runtypes-filter';
2
+ import { z } from 'zod';
6
3
 
7
4
  import { JobInput } from 'src/types/jobs';
8
5
 
9
- export type IdDescription = t.Static<typeof IdDescription>;
6
+ export type IdDescription = z.infer<typeof IdDescriptionSchema>;
10
7
 
11
- const IdDescription = t.Record({
12
- id: t.String,
13
- description: t.String,
8
+ export const IdDescriptionSchema = z.object({
9
+ id: z.string(),
10
+ description: z.string(),
14
11
  });
15
12
 
16
- export const filterIdDescription = checkFilter(IdDescription);
17
-
18
13
  export const chance = new Chance();
19
14
 
20
15
  export const mockIdDescription = (): IdDescription => ({
@@ -1,7 +1,4 @@
1
- /* eslint-disable new-cap */
2
-
3
- import * as t from 'runtypes';
4
- import checkFilter from 'runtypes-filter';
1
+ import { z } from 'zod';
5
2
 
6
3
  export interface Job {
7
4
  id: string;
@@ -11,12 +8,10 @@ export interface Job {
11
8
  };
12
9
  }
13
10
 
14
- export type JobInput = t.Static<typeof JobInput>;
11
+ export type JobInput = z.infer<typeof JobInputSchema>;
15
12
 
16
- const JobInput = t.Record({
17
- hirer: t.Record({
18
- id: t.String,
13
+ export const JobInputSchema = z.object({
14
+ hirer: z.object({
15
+ id: z.string(),
19
16
  }),
20
17
  });
21
-
22
- export const filterJobInput = checkFilter(JobInput);
@@ -26,13 +26,13 @@ configs:
26
26
  - yarn deploy
27
27
  concurrency: 1
28
28
  plugins:
29
- - artifacts#v1.5.0:
29
+ - artifacts#v1.7.0:
30
30
  build: ${BUILDKITE_BUILD_ID}
31
31
  download: lib/*
32
32
  - *aws-sm
33
33
  - *private-npm
34
34
  - *docker-ecr-cache
35
- - docker-compose#v3.10.0:
35
+ - docker-compose#v4.5.0:
36
36
  dependencies: false
37
37
  run: app
38
38
  retry:
@@ -60,7 +60,7 @@ steps:
60
60
  - *aws-sm
61
61
  - *private-npm
62
62
  - *docker-ecr-cache
63
- - docker-compose#v3.10.0:
63
+ - docker-compose#v4.5.0:
64
64
  run: app
65
65
  timeout_in_minutes: 10
66
66
 
@@ -15,7 +15,7 @@ Next steps:
15
15
  3. [ ] Create a new repository in the appropriate GitHub organisation.
16
16
  4. [ ] Add the repository to BuildAgency;
17
17
  see [Builds at SEEK] for more information.
18
- 5. [ ] Add deployment bucket configuration and data classification tags to [serverless.yml](serverless.yml).
18
+ 5. [ ] Add Datadog extension, deployment bucket configuration and data classification tags to [serverless.yml](serverless.yml).
19
19
  6. [ ] Push local commits to the upstream GitHub branch.
20
20
  7. [ ] Configure [GitHub repository settings].
21
21
  8. [ ] Delete this checklist 😌.
@@ -2,19 +2,19 @@
2
2
  "dependencies": {
3
3
  "@seek/logger": "^5.0.1",
4
4
  "aws-sdk": "^2.1011.0",
5
- "seek-datadog-custom-metrics": "^4.0.0",
5
+ "datadog-lambda-js": "^6.83.0",
6
6
  "skuba-dive": "^2.0.0",
7
- "runtypes": "^6.4.1",
8
- "runtypes-filter": "^0.6.0"
7
+ "zod": "^3.19.1"
9
8
  },
10
9
  "devDependencies": {
11
10
  "@types/aws-lambda": "^8.10.84",
12
11
  "@types/chance": "^1.1.3",
13
- "@types/node": "^16.0.0",
12
+ "@types/node": "16.11.64",
14
13
  "chance": "^1.1.8",
15
14
  "pino-pretty": "^9.0.0",
16
15
  "serverless": "^3.17.0",
17
16
  "serverless-plugin-canary-deployments": "^0.8.0",
17
+ "serverless-plugin-datadog": "^5.7.0",
18
18
  "serverless-prune-plugin": "^2.0.0",
19
19
  "skuba": "*"
20
20
  },
@@ -4,21 +4,33 @@ configValidationMode: error
4
4
 
5
5
  params:
6
6
  default:
7
+ datadogApiKeySecretArn: 'TODO: arn:aws:secretsmanager:${aws:region}:${aws:accountId}:secret:SECRET-NAME'
7
8
  description: <%- description %>
8
9
  dev:
9
- deploymentBucket: 'TODO: deploymentBucketName'
10
+ deploymentBucket: 'TODO: deployment-bucket-name'
10
11
  isProduction: false
11
12
  prod:
12
- deploymentBucket: 'TODO: deploymentBucketName'
13
+ deploymentBucket: 'TODO: deployment-bucket-name'
13
14
  isProduction: true
14
15
 
15
16
  custom:
17
+ datadog:
18
+ addLayers: false
19
+ apiKeySecretArn: ${param:datadogApiKeySecretArn}
20
+ enableDDLogs: false
21
+ # TODO: enable Datadog extension
22
+ enabled: false
23
+ exclude:
24
+ - WorkerPreHook
25
+ injectLogContext: false
26
+ version: ${env:VERSION}
16
27
  prune:
17
28
  automatic: true
18
29
  number: 3
19
30
 
20
31
  plugins:
21
32
  - serverless-plugin-canary-deployments
33
+ - serverless-plugin-datadog
22
34
  - serverless-prune-plugin
23
35
 
24
36
  provider:
@@ -51,6 +63,9 @@ provider:
51
63
  - Action: lambda:InvokeFunction
52
64
  Effect: Allow
53
65
  Resource: !Sub arn:aws:lambda:${AWS::Region}:${AWS::AccountId}:function:${self:functions.Worker.name}
66
+ - Action: secretsmanager:GetSecretValue
67
+ Effect: Allow
68
+ Resource: ${param:datadogApiKeySecretArn}-??????
54
69
  - Action: sns:Publish
55
70
  Effect: Allow
56
71
  Resource: !Ref DestinationTopic
@@ -17,7 +17,9 @@ describe('handler', () => {
17
17
 
18
18
  const score = chance.floating({ max: 1, min: 0 });
19
19
 
20
- const increment = jest.spyOn(metricsClient, 'increment').mockReturnValue();
20
+ const distribution = jest
21
+ .spyOn(metricsClient, 'distribution')
22
+ .mockReturnValue();
21
23
 
22
24
  beforeAll(logger.spy);
23
25
  beforeAll(scoringService.spy);
@@ -32,7 +34,7 @@ describe('handler', () => {
32
34
 
33
35
  afterEach(() => {
34
36
  logger.clear();
35
- increment.mockClear();
37
+ distribution.mockClear();
36
38
  scoringService.clear();
37
39
  sns.clear();
38
40
  });
@@ -47,12 +49,12 @@ describe('handler', () => {
47
49
  expect(logger.error).not.toHaveBeenCalled();
48
50
 
49
51
  expect(logger.info.mock.calls).toEqual([
50
- [{ count: 1 }, 'received jobs'],
51
- [{ snsMessageId: expect.any(String) }, 'scored job'],
52
- ['request'],
52
+ [{ count: 1 }, 'Received jobs'],
53
+ [{ snsMessageId: expect.any(String) }, 'Scored job'],
54
+ ['Function succeeded'],
53
55
  ]);
54
56
 
55
- expect(increment.mock.calls).toEqual([
57
+ expect(distribution.mock.calls).toEqual([
56
58
  ['job.received', 1],
57
59
  ['job.scored', 1],
58
60
  ]);
@@ -63,7 +65,7 @@ describe('handler', () => {
63
65
  it('throws on invalid input', () => {
64
66
  const event = createSqsEvent(['}']);
65
67
 
66
- return expect(app.handler(event, ctx)).rejects.toThrow('invoke error');
68
+ return expect(app.handler(event, ctx)).rejects.toThrow('Function failed');
67
69
  });
68
70
 
69
71
  it('bubbles up scoring service error', async () => {
@@ -73,9 +75,9 @@ describe('handler', () => {
73
75
 
74
76
  const event = createSqsEvent([JSON.stringify(jobPublished)]);
75
77
 
76
- await expect(app.handler(event, ctx)).rejects.toThrow('invoke error');
78
+ await expect(app.handler(event, ctx)).rejects.toThrow('Function failed');
77
79
 
78
- expect(logger.error).toHaveBeenCalledWith({ err }, 'request');
80
+ expect(logger.error).toHaveBeenCalledWith({ err }, 'Function failed');
79
81
  });
80
82
 
81
83
  it('bubbles up SNS error', async () => {
@@ -85,31 +87,31 @@ describe('handler', () => {
85
87
 
86
88
  const event = createSqsEvent([JSON.stringify(jobPublished)]);
87
89
 
88
- await expect(app.handler(event, ctx)).rejects.toThrow('invoke error');
90
+ await expect(app.handler(event, ctx)).rejects.toThrow('Function failed');
89
91
 
90
- expect(logger.error).toHaveBeenCalledWith({ err }, 'request');
92
+ expect(logger.error).toHaveBeenCalledWith({ err }, 'Function failed');
91
93
  });
92
94
 
93
95
  it('throws on zero records', async () => {
94
- const err = new Error('received 0 records');
96
+ const err = new Error('Received 0 records');
95
97
 
96
98
  const event = createSqsEvent([]);
97
99
 
98
- await expect(app.handler(event, ctx)).rejects.toThrow('invoke error');
100
+ await expect(app.handler(event, ctx)).rejects.toThrow('Function failed');
99
101
 
100
- expect(logger.error).toHaveBeenCalledWith({ err }, 'request');
102
+ expect(logger.error).toHaveBeenCalledWith({ err }, 'Function failed');
101
103
  });
102
104
 
103
105
  it('throws on multiple records', async () => {
104
- const err = new Error('received 2 records');
106
+ const err = new Error('Received 2 records');
105
107
 
106
108
  const event = createSqsEvent([
107
109
  JSON.stringify(jobPublished),
108
110
  JSON.stringify(jobPublished),
109
111
  ]);
110
112
 
111
- await expect(app.handler(event, ctx)).rejects.toThrow('invoke error');
113
+ await expect(app.handler(event, ctx)).rejects.toThrow('Function failed');
112
114
 
113
- expect(logger.error).toHaveBeenCalledWith({ err }, 'request');
115
+ expect(logger.error).toHaveBeenCalledWith({ err }, 'Function failed');
114
116
  });
115
117
  });
@@ -8,7 +8,7 @@ import { metricsClient } from 'src/framework/metrics';
8
8
  import { validateJson } from 'src/framework/validation';
9
9
  import { scoreJobPublishedEvent, scoringService } from 'src/services/jobScorer';
10
10
  import { sendPipelineEvent } from 'src/services/pipelineEventSender';
11
- import { filterJobPublishedEvent } from 'src/types/pipelineEvents';
11
+ import { JobPublishedEventSchema } from 'src/types/pipelineEvents';
12
12
 
13
13
  /**
14
14
  * Tests connectivity to ensure appropriate access and network configuration.
@@ -20,19 +20,19 @@ const smokeTest = async () => {
20
20
  export const handler = createHandler<SQSEvent>(async (event) => {
21
21
  // Treat an empty object as our smoke test event.
22
22
  if (!Object.keys(event).length) {
23
- logger.info('received smoke test request');
23
+ logger.info('Received smoke test request');
24
24
  return smokeTest();
25
25
  }
26
26
 
27
27
  const count = event.Records.length;
28
28
 
29
29
  if (count !== 1) {
30
- throw Error(`received ${count} records`);
30
+ throw Error(`Received ${count} records`);
31
31
  }
32
32
 
33
- logger.info({ count }, 'received jobs');
33
+ logger.info({ count }, 'Received jobs');
34
34
 
35
- metricsClient.increment('job.received', event.Records.length);
35
+ metricsClient.distribution('job.received', event.Records.length);
36
36
 
37
37
  const record = event.Records[0];
38
38
 
@@ -40,13 +40,13 @@ export const handler = createHandler<SQSEvent>(async (event) => {
40
40
  // the event and eventually send it to your dead-letter queue. If you don't
41
41
  // trust your source to provide consistently well-formed input, consider
42
42
  // catching and handling this error in code.
43
- const publishedJob = validateJson(record.body, filterJobPublishedEvent);
43
+ const publishedJob = validateJson(record.body, JobPublishedEventSchema);
44
44
 
45
45
  const scoredJob = await scoreJobPublishedEvent(publishedJob);
46
46
 
47
47
  const snsMessageId = await sendPipelineEvent(scoredJob);
48
48
 
49
- logger.info({ snsMessageId }, 'scored job');
49
+ logger.info({ snsMessageId }, 'Scored job');
50
50
 
51
- metricsClient.increment('job.scored', 1);
51
+ metricsClient.distribution('job.scored', 1);
52
52
  });
@@ -4,6 +4,7 @@ interface Config {
4
4
  environment: Environment;
5
5
 
6
6
  logLevel: string;
7
+ metrics: boolean;
7
8
  name: string;
8
9
  version: string;
9
10
 
@@ -20,6 +21,7 @@ const environment = Env.oneOf(environments)('ENVIRONMENT');
20
21
  const configs: Record<Environment, () => Omit<Config, 'environment'>> = {
21
22
  local: () => ({
22
23
  logLevel: 'debug',
24
+ metrics: false,
23
25
  name: '<%- serviceName %>',
24
26
  version: 'local',
25
27
 
@@ -41,6 +43,7 @@ const configs: Record<Environment, () => Omit<Config, 'environment'>> = {
41
43
 
42
44
  prod: () => ({
43
45
  logLevel: 'info',
46
+ metrics: true,
44
47
  name: Env.string('SERVICE'),
45
48
  version: Env.string('VERSION'),
46
49
 
@@ -18,7 +18,7 @@ describe('createHandler', () => {
18
18
  const handler = createHandler((event) => {
19
19
  expect(event).toBe(input);
20
20
 
21
- logger.info('hello from handler');
21
+ logger.info('Handler invoked');
22
22
 
23
23
  return Promise.resolve(output);
24
24
  });
@@ -28,8 +28,8 @@ describe('createHandler', () => {
28
28
  expect(logger.error).not.toHaveBeenCalled();
29
29
 
30
30
  expect(logger.info.mock.calls).toEqual([
31
- ['hello from handler'],
32
- ['request'],
31
+ ['Handler invoked'],
32
+ ['Function succeeded'],
33
33
  ]);
34
34
  });
35
35
 
@@ -38,9 +38,9 @@ describe('createHandler', () => {
38
38
 
39
39
  const handler = createHandler(() => Promise.reject(err));
40
40
 
41
- await expect(handler(input, ctx)).rejects.toThrow('invoke error');
41
+ await expect(handler(input, ctx)).rejects.toThrow('Function failed');
42
42
 
43
- expect(logger.error.mock.calls).toEqual([[{ err }, 'request']]);
43
+ expect(logger.error.mock.calls).toEqual([[{ err }, 'Function failed']]);
44
44
 
45
45
  expect(logger.info).not.toHaveBeenCalled();
46
46
  });
@@ -52,9 +52,9 @@ describe('createHandler', () => {
52
52
  throw err;
53
53
  });
54
54
 
55
- await expect(handler(input, ctx)).rejects.toThrow('invoke error');
55
+ await expect(handler(input, ctx)).rejects.toThrow('Function failed');
56
56
 
57
- expect(logger.error.mock.calls).toEqual([[{ err }, 'request']]);
57
+ expect(logger.error.mock.calls).toEqual([[{ err }, 'Function failed']]);
58
58
 
59
59
  expect(logger.info).not.toHaveBeenCalled();
60
60
  });
@@ -1,20 +1,43 @@
1
- import { Context } from 'aws-lambda';
1
+ import { datadog } from 'datadog-lambda-js';
2
2
 
3
+ import { config } from 'src/config';
3
4
  import { logger, loggerContext } from 'src/framework/logging';
4
5
 
5
- export const createHandler =
6
- <Event, Output = unknown>(fn: (event: Event) => Promise<Output>) =>
7
- (event: Event, { awsRequestId }: Context) =>
6
+ interface LambdaContext {
7
+ awsRequestId: string;
8
+ }
9
+
10
+ type Handler<Event, Output> = (
11
+ event: Event,
12
+ ctx: LambdaContext,
13
+ ) => Promise<Output>;
14
+
15
+ /**
16
+ * Conditionally applies the Datadog wrapper to a Lambda handler.
17
+ *
18
+ * This also "fixes" its broken type definitions.
19
+ */
20
+ const withDatadog = <Event, Output = unknown>(
21
+ fn: Handler<Event, Output>,
22
+ ): Handler<Event, Output> =>
23
+ // istanbul ignore next
24
+ config.metrics ? (datadog(fn) as Handler<Event, Output>) : fn;
25
+
26
+ export const createHandler = <Event, Output = unknown>(
27
+ fn: (event: Event) => Promise<Output>,
28
+ ) =>
29
+ withDatadog<Event>((event, { awsRequestId }) =>
8
30
  loggerContext.run({ awsRequestId }, async () => {
9
31
  try {
10
32
  const output = await fn(event);
11
33
 
12
- logger.info('request');
34
+ logger.info('Function succeeded');
13
35
 
14
36
  return output;
15
37
  } catch (err) {
16
- logger.error({ err }, 'request');
38
+ logger.error({ err }, 'Function failed');
17
39
 
18
- throw new Error('invoke error');
40
+ throw new Error('Function failed');
19
41
  }
20
- });
42
+ }),
43
+ );
@@ -1,10 +1,14 @@
1
- import {
2
- createCloudWatchClient,
3
- createTimedSpan,
4
- } from 'seek-datadog-custom-metrics';
1
+ import { sendDistributionMetric } from 'datadog-lambda-js';
5
2
 
6
3
  import { config } from 'src/config';
7
4
 
8
- export const metricsClient = createCloudWatchClient(config);
5
+ const prefix = `${config.name}.`;
9
6
 
10
- export const timedSpan = createTimedSpan(metricsClient);
7
+ export const metricsClient = {
8
+ distribution: (
9
+ ...[name, ...rest]: Parameters<typeof sendDistributionMetric>
10
+ ) =>
11
+ config.metrics
12
+ ? sendDistributionMetric(`${prefix}${name}`, ...rest)
13
+ : undefined,
14
+ };
@@ -1,6 +1,6 @@
1
1
  import {
2
+ IdDescriptionSchema,
2
3
  chance,
3
- filterIdDescription,
4
4
  mockIdDescription,
5
5
  } from 'src/testing/types';
6
6
 
@@ -12,7 +12,7 @@ describe('validateJson', () => {
12
12
  it('permits valid input', () => {
13
13
  const input = JSON.stringify(idDescription);
14
14
 
15
- expect(validateJson(input, filterIdDescription)).toStrictEqual(
15
+ expect(validateJson(input, IdDescriptionSchema)).toStrictEqual(
16
16
  idDescription,
17
17
  );
18
18
  });
@@ -20,7 +20,7 @@ describe('validateJson', () => {
20
20
  it('filters additional properties', () => {
21
21
  const input = JSON.stringify({ ...idDescription, hacker: chance.name() });
22
22
 
23
- expect(validateJson(input, filterIdDescription)).toStrictEqual(
23
+ expect(validateJson(input, IdDescriptionSchema)).toStrictEqual(
24
24
  idDescription,
25
25
  );
26
26
  });
@@ -28,27 +28,47 @@ describe('validateJson', () => {
28
28
  it('blocks mistyped prop', () => {
29
29
  const input = JSON.stringify({ ...idDescription, id: null });
30
30
 
31
- expect(() => validateJson(input, filterIdDescription))
31
+ expect(() => validateJson(input, IdDescriptionSchema))
32
32
  .toThrowErrorMatchingInlineSnapshot(`
33
- "Validation failed:
34
- {
35
- "id": "Expected string, but was null"
36
- }.
37
- Object should match { id: string; description: string; }"
33
+ "[
34
+ {
35
+ "code": "invalid_type",
36
+ "expected": "string",
37
+ "received": "null",
38
+ "path": [
39
+ "id"
40
+ ],
41
+ "message": "Expected string, received null"
42
+ }
43
+ ]"
38
44
  `);
39
45
  });
40
46
 
41
47
  it('blocks missing prop', () => {
42
48
  const input = '{}';
43
49
 
44
- expect(() => validateJson(input, filterIdDescription))
50
+ expect(() => validateJson(input, IdDescriptionSchema))
45
51
  .toThrowErrorMatchingInlineSnapshot(`
46
- "Validation failed:
47
- {
48
- "id": "Expected string, but was missing",
49
- "description": "Expected string, but was missing"
50
- }.
51
- Object should match { id: string; description: string; }"
52
+ "[
53
+ {
54
+ "code": "invalid_type",
55
+ "expected": "string",
56
+ "received": "undefined",
57
+ "path": [
58
+ "id"
59
+ ],
60
+ "message": "Required"
61
+ },
62
+ {
63
+ "code": "invalid_type",
64
+ "expected": "string",
65
+ "received": "undefined",
66
+ "path": [
67
+ "description"
68
+ ],
69
+ "message": "Required"
70
+ }
71
+ ]"
52
72
  `);
53
73
  });
54
74
 
@@ -56,7 +76,7 @@ describe('validateJson', () => {
56
76
  const input = '}';
57
77
 
58
78
  expect(() =>
59
- validateJson(input, filterIdDescription),
79
+ validateJson(input, IdDescriptionSchema),
60
80
  ).toThrowErrorMatchingInlineSnapshot(
61
81
  `"Unexpected token } in JSON at position 0"`,
62
82
  );
@@ -1,2 +1,4 @@
1
- export const validateJson = <T>(input: string, filter: (data: unknown) => T) =>
2
- filter(JSON.parse(input));
1
+ import { z } from 'zod';
2
+
3
+ export const validateJson = <T>(input: string, schema: z.ZodSchema<T>) =>
4
+ schema.parse(JSON.parse(input));
@@ -5,7 +5,7 @@ import {
5
5
  import {
6
6
  JobScorerInput,
7
7
  JobScorerOutput,
8
- filterJobScorerOutput,
8
+ JobScorerOutputSchema,
9
9
  } from 'src/types/jobScorer';
10
10
  import { JobPublishedEvent, JobScoredEvent } from 'src/types/pipelineEvents';
11
11
 
@@ -39,7 +39,7 @@ const scoreJob = async ({
39
39
  }: JobScorerInput): Promise<JobScorerOutput> => {
40
40
  const score = await scoringService.request(details);
41
41
 
42
- return filterJobScorerOutput({
42
+ return JobScorerOutputSchema.parse({
43
43
  id,
44
44
  score,
45
45
  });