skuba 11.1.0-jest30-20250620003740 → 11.2.0-lfs-20250711064710

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 (64) hide show
  1. package/jest/moduleNameMapper.js +4 -1
  2. package/lib/api/git/getChangedFiles.js +27 -2
  3. package/lib/api/git/getChangedFiles.js.map +3 -3
  4. package/lib/cli/configure/ensureTemplateCompletion.js +4 -2
  5. package/lib/cli/configure/ensureTemplateCompletion.js.map +2 -2
  6. package/lib/cli/init/getConfig.d.ts +6 -7
  7. package/lib/cli/init/types.d.ts +13 -109
  8. package/lib/cli/init/types.js +30 -20
  9. package/lib/cli/init/types.js.map +2 -2
  10. package/lib/cli/lint/internalLints/patchRenovateConfig.js +3 -3
  11. package/lib/cli/lint/internalLints/patchRenovateConfig.js.map +2 -2
  12. package/lib/cli/migrate/nodeVersion/checks.d.ts +3 -3
  13. package/lib/cli/migrate/nodeVersion/checks.js +11 -11
  14. package/lib/cli/migrate/nodeVersion/checks.js.map +2 -2
  15. package/lib/cli/migrate/nodeVersion/index.js +18 -18
  16. package/lib/cli/migrate/nodeVersion/index.js.map +2 -2
  17. package/lib/utils/error.d.ts +3 -23
  18. package/lib/utils/error.js +18 -8
  19. package/lib/utils/error.js.map +2 -2
  20. package/lib/utils/manifest.d.ts +2 -2
  21. package/lib/utils/manifest.js +4 -4
  22. package/lib/utils/manifest.js.map +2 -2
  23. package/lib/utils/packageManager.d.ts +5 -2
  24. package/lib/utils/packageManager.js +2 -2
  25. package/lib/utils/packageManager.js.map +2 -2
  26. package/lib/utils/template.d.ts +9 -38
  27. package/lib/utils/template.js +22 -10
  28. package/lib/utils/template.js.map +2 -2
  29. package/lib/utils/version.d.ts +2 -10
  30. package/lib/utils/version.js +9 -9
  31. package/lib/utils/version.js.map +2 -2
  32. package/package.json +15 -15
  33. package/template/express-rest-api/.buildkite/pipeline.yml +1 -1
  34. package/template/express-rest-api/Dockerfile.dev-deps +1 -1
  35. package/template/express-rest-api/package.json +1 -1
  36. package/template/greeter/.buildkite/pipeline.yml +1 -1
  37. package/template/greeter/Dockerfile +1 -1
  38. package/template/greeter/package.json +2 -2
  39. package/template/koa-rest-api/.buildkite/pipeline.yml +1 -1
  40. package/template/koa-rest-api/Dockerfile.dev-deps +1 -1
  41. package/template/koa-rest-api/package.json +7 -7
  42. package/template/koa-rest-api/src/api/jobs/postJob.test.ts +1 -1
  43. package/template/koa-rest-api/src/config.ts +1 -1
  44. package/template/koa-rest-api/src/framework/logging.ts +21 -12
  45. package/template/koa-rest-api/src/framework/server.test.ts +91 -60
  46. package/template/koa-rest-api/src/framework/validation.test.ts +31 -31
  47. package/template/koa-rest-api/src/framework/validation.ts +17 -23
  48. package/template/koa-rest-api/src/testing/types.ts +1 -1
  49. package/template/koa-rest-api/src/types/jobs.ts +1 -1
  50. package/template/lambda-sqs-worker-cdk/.buildkite/pipeline.yml +2 -2
  51. package/template/lambda-sqs-worker-cdk/Dockerfile +1 -1
  52. package/template/lambda-sqs-worker-cdk/infra/__snapshots__/appStack.test.ts.snap +6 -0
  53. package/template/lambda-sqs-worker-cdk/package.json +5 -5
  54. package/template/lambda-sqs-worker-cdk/src/app.test.ts +76 -17
  55. package/template/lambda-sqs-worker-cdk/src/config.ts +1 -1
  56. package/template/lambda-sqs-worker-cdk/src/framework/handler.test.ts +35 -15
  57. package/template/lambda-sqs-worker-cdk/src/framework/logging.ts +22 -11
  58. package/template/lambda-sqs-worker-cdk/src/framework/validation.test.ts +6 -9
  59. package/template/lambda-sqs-worker-cdk/src/framework/validation.ts +4 -8
  60. package/template/lambda-sqs-worker-cdk/src/testing/types.ts +1 -1
  61. package/template/lambda-sqs-worker-cdk/src/types/jobScorer.ts +1 -1
  62. package/template/lambda-sqs-worker-cdk/src/types/pipelineEvents.ts +1 -1
  63. package/template/koa-rest-api/src/testing/logging.ts +0 -16
  64. package/template/lambda-sqs-worker-cdk/src/testing/logging.ts +0 -19
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "skuba",
3
- "version": "11.1.0-jest30-20250620003740",
3
+ "version": "11.2.0-lfs-20250711064710",
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",
@@ -55,7 +55,7 @@
55
55
  "@jest/types": "^30.0.0",
56
56
  "@octokit/graphql": "^9.0.0",
57
57
  "@octokit/graphql-schema": "^15.3.0",
58
- "@octokit/rest": "^21.0.0",
58
+ "@octokit/rest": "^22.0.0",
59
59
  "@octokit/types": "^14.0.0",
60
60
  "@types/jest": "^30.0.0",
61
61
  "@types/node": "^22.0.0",
@@ -85,7 +85,7 @@
85
85
  "npm-run-path": "^4.0.1",
86
86
  "npm-which": "^3.0.1",
87
87
  "picomatch": "^4.0.0",
88
- "prettier": "~3.5.0",
88
+ "prettier": "~3.6.0",
89
89
  "prettier-plugin-packagejson": "^2.4.10",
90
90
  "read-pkg-up": "^7.0.1",
91
91
  "semantic-release": "^24.2.3",
@@ -97,37 +97,37 @@
97
97
  "tsconfig-seek": "2.0.0",
98
98
  "tsx": "^4.16.2",
99
99
  "typescript": "~5.8.0",
100
- "zod": "^3.22.4",
101
- "eslint-config-skuba": "6.1.0"
100
+ "zod": "^3.25.67",
101
+ "eslint-config-skuba": "6.1.1"
102
102
  },
103
103
  "devDependencies": {
104
- "@changesets/cli": "2.29.3",
104
+ "@changesets/cli": "2.29.5",
105
105
  "@changesets/get-github-info": "0.6.0",
106
106
  "@jest/reporters": "30.0.2",
107
107
  "@jest/test-result": "30.0.2",
108
108
  "@types/ejs": "3.1.5",
109
- "@types/express": "5.0.1",
109
+ "@types/express": "5.0.3",
110
110
  "@types/fs-extra": "11.0.4",
111
111
  "@types/koa": "2.15.0",
112
112
  "@types/lodash.mergewith": "4.6.9",
113
113
  "@types/minimist": "1.2.5",
114
114
  "@types/module-alias": "2.0.4",
115
- "@types/npm-registry-fetch": "8.0.7",
116
- "@types/npm-which": "3.0.3",
115
+ "@types/npm-registry-fetch": "8.0.8",
116
+ "@types/npm-which": "3.0.4",
117
117
  "@types/picomatch": "4.0.0",
118
118
  "@types/semver": "7.7.0",
119
119
  "@types/supertest": "6.0.3",
120
- "enhanced-resolve": "5.18.1",
120
+ "enhanced-resolve": "5.18.2",
121
121
  "express": "5.1.0",
122
- "fastify": "5.3.2",
123
- "jest-diff": "30.0.2",
122
+ "fastify": "5.4.0",
123
+ "jest-diff": "30.0.3",
124
124
  "jsonfile": "6.1.0",
125
125
  "koa": "3.0.0",
126
- "memfs": "4.17.1",
126
+ "memfs": "4.17.2",
127
127
  "remark-cli": "12.0.1",
128
128
  "remark-preset-lint-recommended": "7.0.1",
129
- "semver": "7.7.1",
130
- "supertest": "7.1.0",
129
+ "semver": "7.7.2",
130
+ "supertest": "7.1.1",
131
131
  "type-fest": "2.19.0"
132
132
  },
133
133
  "peerDependencies": {
@@ -48,7 +48,7 @@ steps:
48
48
  GET_NPM_TOKEN: please
49
49
  plugins:
50
50
  - *docker-ecr-cache
51
- - docker-compose#v5.9.0:
51
+ - docker-compose#v5.10.0:
52
52
  run: app
53
53
  environment:
54
54
  - GITHUB_API_TOKEN
@@ -1,4 +1,4 @@
1
- # syntax=docker/dockerfile:1.16
1
+ # syntax=docker/dockerfile:1.17
2
2
 
3
3
  FROM public.ecr.aws/docker/library/node:22-alpine AS dev-deps
4
4
 
@@ -28,7 +28,7 @@
28
28
  "skuba": "*",
29
29
  "supertest": "^7.0.0"
30
30
  },
31
- "packageManager": "pnpm@10.12.1",
31
+ "packageManager": "pnpm@10.12.4",
32
32
  "engines": {
33
33
  "node": ">=22"
34
34
  }
@@ -29,7 +29,7 @@ steps:
29
29
  GET_NPM_TOKEN: please
30
30
  plugins:
31
31
  - *docker-ecr-cache
32
- - docker-compose#v5.9.0:
32
+ - docker-compose#v5.10.0:
33
33
  run: app
34
34
  environment:
35
35
  - GITHUB_API_TOKEN
@@ -1,4 +1,4 @@
1
- # syntax=docker/dockerfile:1.16
1
+ # syntax=docker/dockerfile:1.17
2
2
 
3
3
  FROM public.ecr.aws/docker/library/node:22-alpine AS dev-deps
4
4
 
@@ -17,9 +17,9 @@
17
17
  },
18
18
  "devDependencies": {
19
19
  "@types/node": "^22.13.10",
20
- "skuba": "11.1.0-jest30-20250620003740"
20
+ "skuba": "11.2.0-lfs-20250711064710"
21
21
  },
22
- "packageManager": "pnpm@10.12.1",
22
+ "packageManager": "pnpm@10.12.4",
23
23
  "engines": {
24
24
  "node": ">=22"
25
25
  }
@@ -48,7 +48,7 @@ steps:
48
48
  GET_NPM_TOKEN: please
49
49
  plugins:
50
50
  - *docker-ecr-cache
51
- - docker-compose#v5.9.0:
51
+ - docker-compose#v5.10.0:
52
52
  run: app
53
53
  environment:
54
54
  - GITHUB_API_TOKEN
@@ -1,4 +1,4 @@
1
- # syntax=docker/dockerfile:1.16
1
+ # syntax=docker/dockerfile:1.17
2
2
 
3
3
  FROM public.ecr.aws/docker/library/node:22-alpine AS dev-deps
4
4
 
@@ -13,15 +13,15 @@
13
13
  "test:watch": "skuba test --watch"
14
14
  },
15
15
  "dependencies": {
16
- "@koa/bodyparser": "^5.1.1",
16
+ "@koa/bodyparser": "^6.0.0",
17
17
  "@koa/router": "^13.0.0",
18
18
  "@opentelemetry/api": "^1.9.0",
19
19
  "@opentelemetry/core": "^2.0.0",
20
- "@opentelemetry/exporter-trace-otlp-grpc": "^0.201.0",
21
- "@opentelemetry/instrumentation-aws-sdk": "^0.53.0",
22
- "@opentelemetry/instrumentation-http": "^0.201.0",
20
+ "@opentelemetry/exporter-trace-otlp-grpc": "^0.202.0",
21
+ "@opentelemetry/instrumentation-aws-sdk": "^0.54.0",
22
+ "@opentelemetry/instrumentation-http": "^0.202.0",
23
23
  "@opentelemetry/propagator-b3": "^2.0.0",
24
- "@opentelemetry/sdk-node": "^0.201.0",
24
+ "@opentelemetry/sdk-node": "^0.202.0",
25
25
  "@seek/logger": "^10.0.0",
26
26
  "hot-shots": "^10.0.0",
27
27
  "koa": "^2.16.1",
@@ -29,7 +29,7 @@
29
29
  "seek-datadog-custom-metrics": "^4.6.3",
30
30
  "seek-koala": "^7.0.0",
31
31
  "skuba-dive": "^2.0.0",
32
- "zod": "^3.19.1"
32
+ "zod": "^3.25.67"
33
33
  },
34
34
  "devDependencies": {
35
35
  "@types/chance": "^1.1.3",
@@ -44,7 +44,7 @@
44
44
  "skuba": "*",
45
45
  "supertest": "^7.0.0"
46
46
  },
47
- "packageManager": "pnpm@10.12.1",
47
+ "packageManager": "pnpm@10.12.4",
48
48
  "engines": {
49
49
  "node": ">=22"
50
50
  }
@@ -27,7 +27,7 @@ describe('postJobHandler', () => {
27
27
  .expect(422)
28
28
  .expect(({ text }) =>
29
29
  expect(text).toMatchInlineSnapshot(
30
- `"{"message":"Input validation failed","invalidFields":{"/hirer":"Required"}}"`,
30
+ `"{"message":"Input validation failed","invalidFields":{"/hirer":"Invalid input: expected object, received undefined"}}"`,
31
31
  ),
32
32
  );
33
33
  });
@@ -32,7 +32,7 @@ const configs: Record<Environment, () => Omit<Config, 'environment'>> = {
32
32
  }),
33
33
 
34
34
  test: () => ({
35
- logLevel: Env.string('LOG_LEVEL', { default: 'silent' }),
35
+ logLevel: 'debug',
36
36
  name: '<%- serviceName %>',
37
37
  version: 'test',
38
38
 
@@ -1,4 +1,4 @@
1
- import createLogger from '@seek/logger';
1
+ import createLogger, { createDestination } from '@seek/logger';
2
2
  import { RequestLogging } from 'seek-koala';
3
3
 
4
4
  import { config } from 'src/config';
@@ -8,18 +8,27 @@ const { createContextMiddleware, mixin } =
8
8
 
9
9
  export const contextMiddleware = createContextMiddleware();
10
10
 
11
- export const logger = createLogger({
12
- base: {
13
- environment: config.environment,
14
- version: config.version,
15
- },
11
+ const { destination, stdoutMock } = createDestination({
12
+ mock: config.environment === 'test',
13
+ });
16
14
 
17
- mixin,
15
+ export { stdoutMock };
18
16
 
19
- level: config.logLevel,
17
+ export const logger = createLogger(
18
+ {
19
+ base: {
20
+ environment: config.environment,
21
+ version: config.version,
22
+ },
20
23
 
21
- name: config.name,
24
+ mixin,
22
25
 
23
- transport:
24
- config.environment === 'local' ? { target: 'pino-pretty' } : undefined,
25
- });
26
+ level: config.logLevel,
27
+
28
+ name: config.name,
29
+
30
+ transport:
31
+ config.environment === 'local' ? { target: 'pino-pretty' } : undefined,
32
+ },
33
+ destination,
34
+ );
@@ -1,11 +1,12 @@
1
1
  import Router from '@koa/router';
2
2
 
3
- import { logger } from 'src/testing/logging';
4
3
  import { metricsClient } from 'src/testing/metrics';
5
4
  import { agentFromRouter } from 'src/testing/server';
6
5
  import { chance } from 'src/testing/types';
7
6
  import type { Middleware } from 'src/types/koa';
8
7
 
8
+ import { stdoutMock } from './logging';
9
+
9
10
  const middleware = jest.fn<void, Parameters<Middleware>>();
10
11
 
11
12
  const router = new Router()
@@ -15,10 +16,10 @@ const router = new Router()
15
16
  const agent = agentFromRouter(router);
16
17
 
17
18
  describe('createApp', () => {
18
- beforeAll(logger.spy);
19
-
20
- afterEach(metricsClient.clear);
21
- afterEach(logger.clear);
19
+ afterEach(() => {
20
+ metricsClient.clear();
21
+ stdoutMock.clear();
22
+ });
22
23
 
23
24
  it('handles root route', async () => {
24
25
  middleware.mockImplementation((ctx) => (ctx.body = ''));
@@ -29,9 +30,7 @@ describe('createApp', () => {
29
30
  .expect('server', /.+/)
30
31
  .expect('x-api-version', /.+/);
31
32
 
32
- expect(logger.error).not.toHaveBeenCalled();
33
-
34
- expect(logger.info).not.toHaveBeenCalled();
33
+ expect(stdoutMock.calls).toHaveLength(0);
35
34
 
36
35
  metricsClient.expectTagSubset(['env:test', 'version:test']);
37
36
  metricsClient.expectTagSubset([
@@ -51,9 +50,7 @@ describe('createApp', () => {
51
50
  .expect('server', /.+/)
52
51
  .expect('x-api-version', /.+/);
53
52
 
54
- expect(logger.error).not.toHaveBeenCalled();
55
-
56
- expect(logger.info).not.toHaveBeenCalled();
53
+ expect(stdoutMock.calls).toHaveLength(0);
57
54
 
58
55
  metricsClient.expectTagSubset([
59
56
  'http_method:put',
@@ -72,13 +69,15 @@ describe('createApp', () => {
72
69
  .expect('server', /.+/)
73
70
  .expect('x-api-version', /.+/);
74
71
 
75
- expect(logger.error).not.toHaveBeenCalled();
76
-
77
- expect(logger.info).toHaveBeenNthCalledWith(
78
- 1,
79
- expect.objectContaining({ status: 404 }),
80
- 'Client error',
81
- );
72
+ expect(stdoutMock.calls).toMatchObject([
73
+ {
74
+ level: 30,
75
+ method: 'GET',
76
+ msg: 'Client error',
77
+ status: 404,
78
+ url: '/unknown',
79
+ },
80
+ ]);
82
81
 
83
82
  metricsClient.expectTagSubset([
84
83
  'http_method:get',
@@ -102,13 +101,16 @@ describe('createApp', () => {
102
101
  .expect('server', /.+/)
103
102
  .expect('x-api-version', /.+/);
104
103
 
105
- expect(logger.error).not.toHaveBeenCalled();
106
-
107
- expect(logger.info).toHaveBeenNthCalledWith(
108
- 1,
109
- expect.objectContaining({ status: 400 }),
110
- 'Client error',
111
- );
104
+ expect(stdoutMock.calls).toMatchObject([
105
+ {
106
+ level: 30,
107
+ method: 'GET',
108
+ msg: 'Client error',
109
+ route: '/',
110
+ status: 400,
111
+ url: '/',
112
+ },
113
+ ]);
112
114
 
113
115
  metricsClient.expectTagSubset([
114
116
  'http_method:get',
@@ -129,13 +131,20 @@ describe('createApp', () => {
129
131
  .expect('server', /.+/)
130
132
  .expect('x-api-version', /.+/);
131
133
 
132
- expect(logger.error).not.toHaveBeenCalled();
133
-
134
- expect(logger.info).toHaveBeenNthCalledWith(
135
- 1,
136
- expect.objectContaining({ err: expect.any(Error), status: 400 }),
137
- 'Client error',
138
- );
134
+ expect(stdoutMock.calls).toMatchObject([
135
+ {
136
+ err: {
137
+ statusCode: 400,
138
+ type: 'BadRequestError',
139
+ },
140
+ level: 30,
141
+ method: 'GET',
142
+ msg: 'Client error',
143
+ route: '/',
144
+ status: 400,
145
+ url: '/',
146
+ },
147
+ ]);
139
148
 
140
149
  metricsClient.expectTagSubset([
141
150
  'http_method:get',
@@ -156,13 +165,20 @@ describe('createApp', () => {
156
165
  .expect('server', /.+/)
157
166
  .expect('x-api-version', /.+/);
158
167
 
159
- expect(logger.error).toHaveBeenNthCalledWith(
160
- 1,
161
- expect.objectContaining({ err: expect.any(Error), status: 500 }),
162
- 'Server error',
163
- );
164
-
165
- expect(logger.info).not.toHaveBeenCalled();
168
+ expect(stdoutMock.calls).toMatchObject([
169
+ {
170
+ err: {
171
+ statusCode: 500,
172
+ type: 'InternalServerError',
173
+ },
174
+ level: 50,
175
+ method: 'GET',
176
+ msg: 'Server error',
177
+ route: '/',
178
+ status: 500,
179
+ url: '/',
180
+ },
181
+ ]);
166
182
 
167
183
  metricsClient.expectTagSubset([
168
184
  'http_method:get',
@@ -185,13 +201,20 @@ describe('createApp', () => {
185
201
  .expect('server', /.+/)
186
202
  .expect('x-api-version', /.+/);
187
203
 
188
- expect(logger.error).toHaveBeenNthCalledWith(
189
- 1,
190
- expect.objectContaining({ err, status: 500 }),
191
- 'Server error',
192
- );
193
-
194
- expect(logger.info).not.toHaveBeenCalled();
204
+ expect(stdoutMock.calls).toMatchObject([
205
+ {
206
+ err: {
207
+ message: err.message,
208
+ type: 'Error',
209
+ },
210
+ level: 50,
211
+ method: 'GET',
212
+ msg: 'Server error',
213
+ route: '/',
214
+ status: 500,
215
+ url: '/',
216
+ },
217
+ ]);
195
218
 
196
219
  metricsClient.expectTagSubset([
197
220
  'http_method:get',
@@ -212,13 +235,17 @@ describe('createApp', () => {
212
235
  .expect('server', /.+/)
213
236
  .expect('x-api-version', /.+/);
214
237
 
215
- expect(logger.error).toHaveBeenNthCalledWith(
216
- 1,
217
- expect.objectContaining({ err: null, status: 500 }),
218
- 'Server error',
219
- );
220
-
221
- expect(logger.info).not.toHaveBeenCalled();
238
+ expect(stdoutMock.calls).toMatchObject([
239
+ {
240
+ err: null,
241
+ level: 50,
242
+ method: 'GET',
243
+ msg: 'Server error',
244
+ route: '/',
245
+ status: 500,
246
+ url: '/',
247
+ },
248
+ ]);
222
249
 
223
250
  metricsClient.expectTagSubset([
224
251
  'http_method:get',
@@ -241,13 +268,17 @@ describe('createApp', () => {
241
268
  .expect('server', /.+/)
242
269
  .expect('x-api-version', /.+/);
243
270
 
244
- expect(logger.error).toHaveBeenNthCalledWith(
245
- 1,
246
- expect.objectContaining({ err, status: 500 }),
247
- 'Server error',
248
- );
249
-
250
- expect(logger.info).not.toHaveBeenCalled();
271
+ expect(stdoutMock.calls).toMatchObject([
272
+ {
273
+ err,
274
+ level: 50,
275
+ method: 'GET',
276
+ msg: 'Server error',
277
+ route: '/',
278
+ status: 500,
279
+ url: '/',
280
+ },
281
+ ]);
251
282
 
252
283
  metricsClient.expectTagSubset([
253
284
  'http_method:get',
@@ -43,15 +43,15 @@ describe('validate', () => {
43
43
  .expect(422)
44
44
  .expect(({ body }) =>
45
45
  expect(body).toMatchInlineSnapshot(`
46
- {
47
- "invalidFields": {
48
- "~union0/id": "Expected string, received null",
49
- "~union1/id": "Expected number, received null",
50
- "~union1/summary": "Required",
51
- },
52
- "message": "Input validation failed",
53
- }
54
- `),
46
+ {
47
+ "invalidFields": {
48
+ "~union0/id": "Invalid input: expected string, received null",
49
+ "~union1/id": "Invalid input: expected number, received null",
50
+ "~union1/summary": "Invalid input: expected string, received undefined",
51
+ },
52
+ "message": "Input validation failed",
53
+ }
54
+ `),
55
55
  );
56
56
  });
57
57
 
@@ -62,17 +62,17 @@ describe('validate', () => {
62
62
  .expect(422)
63
63
  .expect(({ body }) =>
64
64
  expect(body).toMatchInlineSnapshot(`
65
- {
66
- "invalidFields": {
67
- "~union0/description~union0": "Required",
68
- "~union0/description~union1": "Required",
69
- "~union0/id": "Required",
70
- "~union1/id": "Required",
71
- "~union1/summary": "Required",
72
- },
73
- "message": "Input validation failed",
74
- }
75
- `),
65
+ {
66
+ "invalidFields": {
67
+ "~union0/description~union0": "Invalid input: expected string, received undefined",
68
+ "~union0/description~union1": "Invalid input: expected object, received undefined",
69
+ "~union0/id": "Invalid input: expected string, received undefined",
70
+ "~union1/id": "Invalid input: expected number, received undefined",
71
+ "~union1/summary": "Invalid input: expected string, received undefined",
72
+ },
73
+ "message": "Input validation failed",
74
+ }
75
+ `),
76
76
  ));
77
77
 
78
78
  it('blocks invalid nested union prop', () => {
@@ -89,17 +89,17 @@ describe('validate', () => {
89
89
  .expect(422)
90
90
  .expect(({ body }) =>
91
91
  expect(body).toMatchInlineSnapshot(`
92
- {
93
- "invalidFields": {
94
- "~union0/description~union0": "Expected string, received object",
95
- "~union0/description~union1/content": "Required",
96
- "~union0/id": "Expected string, received null",
97
- "~union1/id": "Expected number, received null",
98
- "~union1/summary": "Required",
99
- },
100
- "message": "Input validation failed",
101
- }
102
- `),
92
+ {
93
+ "invalidFields": {
94
+ "~union0/description~union0": "Invalid input: expected string, received object",
95
+ "~union0/description~union1/content": "Invalid input: expected string, received undefined",
96
+ "~union0/id": "Invalid input: expected string, received null",
97
+ "~union1/id": "Invalid input: expected number, received null",
98
+ "~union1/summary": "Invalid input: expected string, received undefined",
99
+ },
100
+ "message": "Input validation failed",
101
+ }
102
+ `),
103
103
  );
104
104
  });
105
105
  });
@@ -1,5 +1,6 @@
1
1
  import { ErrorMiddleware } from 'seek-koala';
2
- import { ZodIssueCode, type z } from 'zod';
2
+ import type * as z from 'zod/v4';
3
+ import type * as core from 'zod/v4/core';
3
4
 
4
5
  import type { Context } from 'src/types/koa';
5
6
 
@@ -35,42 +36,39 @@ type InvalidFields = Record<string, string>;
35
36
  * @see [union error example](./validation.test.ts)
36
37
  */
37
38
  const parseInvalidFieldsFromError = (err: z.ZodError): InvalidFields =>
38
- Object.fromEntries(parseTuples(err, {}));
39
+ Object.fromEntries(parseTuples(err.issues));
39
40
 
40
41
  const parseTuples = (
41
- { errors }: z.ZodError,
42
- unions: Record<number, number[]>,
42
+ errors: core.$ZodIssue[],
43
+ basePath: Array<string | number | symbol> = [],
44
+ unions: Record<number, number[]> = {},
43
45
  ): Array<readonly [string, string]> =>
44
46
  errors.flatMap((issue) => {
45
- if (issue.code === ZodIssueCode.invalid_union) {
46
- return issue.unionErrors.flatMap((err, idx) =>
47
- parseTuples(err, {
47
+ if (issue.code === 'invalid_union') {
48
+ return issue.errors.flatMap((err, idx) =>
49
+ parseTuples(err, issue.path, {
48
50
  ...unions,
49
51
  [issue.path.length]: [...(unions[issue.path.length] ?? []), idx],
50
52
  }),
51
53
  );
52
54
  }
53
55
 
54
- const path = ['', ...issue.path]
56
+ const path = ['', ...basePath, ...issue.path]
55
57
  .map((prop, idx) => [prop, ...(unions[idx] ?? [])].join('~union'))
56
58
  .join('/');
57
59
 
58
60
  return [[path, issue.message]] as const;
59
61
  });
60
62
 
61
- export const validate = <
62
- Output,
63
- Def extends z.ZodTypeDef = z.ZodTypeDef,
64
- Input = Output,
65
- >({
63
+ export const validate = <T extends z.ZodType>({
66
64
  ctx,
67
65
  input,
68
66
  schema,
69
67
  }: {
70
68
  ctx: Context;
71
69
  input: unknown;
72
- schema: z.ZodSchema<Output, Def, Input>;
73
- }): Output => {
70
+ schema: T;
71
+ }): z.infer<T> => {
74
72
  const parseResult = schema.safeParse(input);
75
73
  if (parseResult.success === false) {
76
74
  const invalidFields = parseInvalidFieldsFromError(parseResult.error);
@@ -85,15 +83,11 @@ export const validate = <
85
83
  return parseResult.data;
86
84
  };
87
85
 
88
- export const validateRequestBody = <
89
- Output,
90
- Def extends z.ZodTypeDef = z.ZodTypeDef,
91
- Input = Output,
92
- >(
86
+ export const validateRequestBody = <T extends z.ZodType>(
93
87
  ctx: Context,
94
- schema: z.ZodSchema<Output, Def, Input>,
95
- ): Output =>
96
- validate<Output, Def, Input>({
88
+ schema: T,
89
+ ): z.infer<T> =>
90
+ validate({
97
91
  ctx,
98
92
  input: ctx.request.body as unknown,
99
93
  schema,
@@ -1,5 +1,5 @@
1
1
  import { Chance } from 'chance';
2
- import { z } from 'zod';
2
+ import * as z from 'zod/v4';
3
3
 
4
4
  import type { JobInput } from 'src/types/jobs';
5
5
 
@@ -1,4 +1,4 @@
1
- import { z } from 'zod';
1
+ import * as z from 'zod/v4';
2
2
 
3
3
  export interface Job {
4
4
  id: string;