refactor!: validate arguments with zod instead of ow - #986
Conversation
|
See more at https://github.com/apify/apify-client-js/actions/runs/32120638897#summary-95660035693 |
4e8b5c1 to
b74a66e
Compare
I would rather fix it here before it gets merged. We don't want to use any deprecated methods, and this PR introduces the bundle size issue. |
OK, I'll check it out |
aa2684d to
41d769b
Compare
|
@B4nan it's ready for a re-check |
Replaces the remaining `ow`-based argument validation with `zod` across all packages and reworks how validation results are consumed and reported. Closes #3716 ## What changed - **`ow` is gone** — every argument check now goes through `parseArgument(value, schema, label?)` from `@crawlee/utils`, backed by shared zod schemas (`schemas`, exported via `@crawlee/utils/internal`). The `@sapphire/shapeshift` checks in `@crawlee/fs-storage` were converted too, so a single validation library remains. - **Parse results are used everywhere** — option defaults moved from destructuring into the schemas (`.default(...)`), and call sites destructure the typed parse result. `parseArgument` returns `TValue & z.output<TSchema>`, so call sites keep their declared TS types while gaining the defaults. - **Schemas are built once** — all per-call schemas are hoisted to module scope; crawler/launcher classes build their strict options schema once as a `static optionsSchema` next to `optionsShape`. The `urlPatternSchema` for `include`/`exclude` lives in `enqueue_links/shared.ts`, next to the type it validates. - **Specific validators instead of `anyObject`** — class-typed options use `z.instanceof(...)` (`BaseHttpClient`, `Configuration`, `EventManager`), interface-typed ones use duck-typed `objectWithKeys` validators (`storageBackend`, `requestManager`, `logger`, …), and element-typed arrays use the new `schemas.arrayOf(item, 'numbers')`. ## Error messages `ArgumentValidationError` (replacing ow's `ArgumentError`) renders one line per issue: the expected type, the received type and value folded into one clause, the offending field path, and the validated interface: ```text // v3 (ow) — first issue only Expected property `maxRequestRetries` to be of type `number` but received type `string` in object `HttpCrawlerOptions` // v4 (zod) — every issue, one line each Invalid input: expected number, received the string `many` at `maxRequestRetries` in `HttpCrawlerOptions` Invalid input: expected an array of numbers, received the number `500` at `additionalHttpErrorStatusCodes` in `HttpCrawlerOptions` Invalid input: expected boolean, received the string `yes` at `retryOnBlocked` in `HttpCrawlerOptions` ``` Details worth knowing: - Union failures expand into one line per failed arm (zod's own message is a bare "Invalid input"). - `NaN` is named as itself, an empty string renders as `''`, and arrays name their element type (``expected an array of URL patterns``) — none of which ow or stock zod reported. - `new Request('https://…')` gets a targeted hint pointing at the `{ url }` object form. - For programmatic handling, the error exposes zod's structured output: `error.issues` and the raw `ZodError` as a typed `cause`. The migration is documented in the v4 upgrading guide (`docs/upgrading/upgrading_v4.md`), including a rename-cheat-sheet entry. ## Notes - Custom HTTP clients must now **extend `BaseHttpClient`** from `@crawlee/http-client` rather than just implementing the interface (all shipped clients already do; `LazyDefaultHttpClient` was converted). Same applies to test mocks — `Object.create(BaseHttpClient.prototype)` works. - One caveat of consuming parse results: zod object schemas return a pruned plain copy, so options holding class instances are validated with passthrough schemas (`z.custom`-based) to keep their prototypes — there are comments at the relevant schemas. - Fixes a few latent gaps surfaced along the way: `Request.state` now accepts `RequestState.SKIPPED` (validated via `z.enum(RequestState)`), and the publish-time catalog inlining covers `optionalDependencies`. - `ArgumentValidationError` and its formatter are intentionally kept close to the copy in apify/apify-client-js#986 — a follow-up may extract them into a shared package. --------- Co-authored-by: Martin Adámek <banan23@gmail.com>
Replaces the remaining `ow`-based argument validation with `zod` across all packages and reworks how validation results are consumed and reported. Closes #3716 - **`ow` is gone** — every argument check now goes through `parseArgument(value, schema, label?)` from `@crawlee/utils`, backed by shared zod schemas (`schemas`, exported via `@crawlee/utils/internal`). The `@sapphire/shapeshift` checks in `@crawlee/fs-storage` were converted too, so a single validation library remains. - **Parse results are used everywhere** — option defaults moved from destructuring into the schemas (`.default(...)`), and call sites destructure the typed parse result. `parseArgument` returns `TValue & z.output<TSchema>`, so call sites keep their declared TS types while gaining the defaults. - **Schemas are built once** — all per-call schemas are hoisted to module scope; crawler/launcher classes build their strict options schema once as a `static optionsSchema` next to `optionsShape`. The `urlPatternSchema` for `include`/`exclude` lives in `enqueue_links/shared.ts`, next to the type it validates. - **Specific validators instead of `anyObject`** — class-typed options use `z.instanceof(...)` (`BaseHttpClient`, `Configuration`, `EventManager`), interface-typed ones use duck-typed `objectWithKeys` validators (`storageBackend`, `requestManager`, `logger`, …), and element-typed arrays use the new `schemas.arrayOf(item, 'numbers')`. `ArgumentValidationError` (replacing ow's `ArgumentError`) renders one line per issue: the expected type, the received type and value folded into one clause, the offending field path, and the validated interface: ```text // v3 (ow) — first issue only Expected property `maxRequestRetries` to be of type `number` but received type `string` in object `HttpCrawlerOptions` // v4 (zod) — every issue, one line each Invalid input: expected number, received the string `many` at `maxRequestRetries` in `HttpCrawlerOptions` Invalid input: expected an array of numbers, received the number `500` at `additionalHttpErrorStatusCodes` in `HttpCrawlerOptions` Invalid input: expected boolean, received the string `yes` at `retryOnBlocked` in `HttpCrawlerOptions` ``` Details worth knowing: - Union failures expand into one line per failed arm (zod's own message is a bare "Invalid input"). - `NaN` is named as itself, an empty string renders as `''`, and arrays name their element type (``expected an array of URL patterns``) — none of which ow or stock zod reported. - `new Request('https://…')` gets a targeted hint pointing at the `{ url }` object form. - For programmatic handling, the error exposes zod's structured output: `error.issues` and the raw `ZodError` as a typed `cause`. The migration is documented in the v4 upgrading guide (`docs/upgrading/upgrading_v4.md`), including a rename-cheat-sheet entry. - Custom HTTP clients must now **extend `BaseHttpClient`** from `@crawlee/http-client` rather than just implementing the interface (all shipped clients already do; `LazyDefaultHttpClient` was converted). Same applies to test mocks — `Object.create(BaseHttpClient.prototype)` works. - One caveat of consuming parse results: zod object schemas return a pruned plain copy, so options holding class instances are validated with passthrough schemas (`z.custom`-based) to keep their prototypes — there are comments at the relevant schemas. - Fixes a few latent gaps surfaced along the way: `Request.state` now accepts `RequestState.SKIPPED` (validated via `z.enum(RequestState)`), and the publish-time catalog inlining covers `optionalDependencies`. - `ArgumentValidationError` and its formatter are intentionally kept close to the copy in apify/apify-client-js#986 — a follow-up may extract them into a shared package. --------- Co-authored-by: Martin Adámek <banan23@gmail.com>
BREAKING CHANGE: runtime argument validation switched from `ow` to `zod`, so every invalid-argument error message changed, and the thrown error is now an `ArgumentValidationError` (newly exported from `apify-client`) instead of `ow`'s `ArgumentError`. It exposes the structured zod issues on `issues` and keeps the original `ZodError` on `cause`, so you can branch on them instead of parsing the message. Values that `ow.object` accepted only incidentally are now rejected: arrays no longer pass as objects for `update()` / `create()` fields, for `TaskClient.start()` / `call()` input, for the storage `schema` option, or as `DatasetClient.pushItems()` array items (which must be objects or strings).
The ow-based validation rejected symbol and bigint values loudly, but the zod replacement only checked for undefined. A symbol value would then pass validation, serialize to undefined, and silently PUT an empty record body.
Also fixes a pre-existing "validatioon" typo carried through two comments.
`describeReceived('')` used to produce bare backticks with nothing
between them, e.g. for `client.actor('')`.
c9b3ae3 to
9f26108
Compare
Replaces
owwithzodfor runtime argument validation. Input validation only — response validation is a separate PR.How it works
ArgumentValidationErrorandvalidate()live in this package:apify-clientsits below@crawlee/coreand the SDK in the dependency graph, so it cannot import theirs.z.strictObject/z.looseObject/z.enum, not the deprecated.strict()/.passthrough()/z.nativeEnum().chunkSizenow works on every paginatinglist()whose schema spreads the sharedpaginationOptionsShape. It used to type-check but throw;ow'sexactShapehad the same gap.Browser bundle
rsbuild.config.ts, off since the webpack-to-rsbuild migration in chore: update eslint, adopt prettier and rsbuild #671. Now 288 kB raw / 87 kB gzip, from 1439 kB / 273 kB — below the 946 kB / 203 kB before this PR. A 320 kB budget fails the build, so it cannot grow unnoticed again.Breaking changes
ArgumentValidationError(exported fromapify-client), notow'sArgumentError— different messages, the zod issues onissues, the originalZodErroroncause. A message renders at most 10 problems, then... and N more.update()/create()fields,TaskClient.start()/call()input, the storageschemaoption,DatasetClient.pushItems()items,RequestQueueClient.addRequest()/batchAddRequests()requests.Infinityno longer passes on numeric options such aswaitSecs,timeoutormemory, and an invalidDateno longer passes onstartedBefore/startedAfter—z.number()requires a finite number andz.date()a valid date, whereowonly checked the type.KeyValueStoreClient.setRecord()rejectsInfinityas a record value, which v2 accepted and stored asnull.chunkSizeondownloadItems()andcreateItemsPublicUrl(),signatureoncreateItemsPublicUrl()andcreateKeysPublicUrl()— a compile error now instead of a throw.Date,Map,Setand other class instances still pass as objects, as underow.✍️ Drafted by Claude Code