refactor!: validate arguments with zod instead of ow - #986
Conversation
|
See more at https://github.com/apify/apify-client-js/actions/runs/32276952895#summary-96146513823 |
Raises the minimum supported Node.js version to 22, matching the Crawlee
v4 floor. `apify-client` sits below `@crawlee/core`, so its minimum must
not be higher than Crawlee's, and there is no reason for it to be lower.
- **`package.json`** — added `engines: { "node": ">=22.0.0" }`.
- **`check.yaml`** — `build_and_test` matrix `[20, 22, 24]` -> `[22, 24,
26]`, so the client is now also tested against Node.js 26. The remaining
jobs stay on 24, as Node.js 26 does not become LTS until October 2026.
- **`tsconfig.json`** — dropped the `module: "Node16"` and
`moduleResolution` overrides, so the `@apify/tsconfig` base values
(`node20`/`nodenext`) apply; `lib` -> `["ES2024", "DOM"]`, `target:
"ES2024"`. ES2024 rather than Crawlee's `ESNext`, because Node 22 does
not implement ES2025 syntax such as `using`.
- **Docs** — updated `docs/01_introduction/index.md` and
`CONTRIBUTING.md`. `website/versioned_docs/version-2/**` is untouched,
as it is a frozen v2 snapshot.
Drive-by: `pnpm tsc-check-tests` was already failing on `v3`, mostly
with `TS1541` from the old `module: Node16`. Nothing ran it in CI, so it
went unnoticed; it is fixed and now part of the `lint` job.
BREAKING CHANGE: Node.js 18 and 20 are no longer supported. The minimum
supported version is now Node.js 22.
*✍️ Drafted by Claude Code*
---------
Co-authored-by: Martin Adámek <banan23@gmail.com>
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
…t/replace-ow-with-zod
JSON.stringify(fn) returns undefined instead of throwing, so a function value would silently PUT an empty request body.
Port of the integration test suite from the Python API client. Adds an integration test suite that executes the client against the live Apify API. This is the same approach as in the Python API client: real API calls with a test user token, resources created under unique names and cleaned up afterwards, and eventual consistency handled by polling helpers rather than sleeps or retries. Merge this **before** #985, #986 and the rest of the v3 work, so those changes have some end-to-end test coverage. ## Coverage 196 tests over Actors, Actor versions, Actor env vars, builds, runs, logs, tasks, schedules, webhooks, webhook dispatches, datasets, key-value stores, request queues, the store, and users. ## Test tiers `vitest.config.mts` now defines two projects, so the existing unit tests stay fast and offline: - `pnpm test` - unit tests only (unchanged behavior) - `pnpm test:integration` - integration tier only - `pnpm test:all` - both ## Credentials The tier needs `APIFY_TEST_USER_API_TOKEN`. ## CI A new `integration_tests` job in `check.yaml` runs the tier on Node 26. It is skipped for fork PRs, where repository secrets are unavailable, and can be triggered on demand via `workflow_dispatch`. *✍️ Drafted by Claude Code*
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>
Port of the integration test suite from the Python API client. Adds an integration test suite that executes the client against the live Apify API. This is the same approach as in the Python API client: real API calls with a test user token, resources created under unique names and cleaned up afterwards, and eventual consistency handled by polling helpers rather than sleeps or retries. Merge this **before** #985, #986 and the rest of the v3 work, so those changes have some end-to-end test coverage. 196 tests over Actors, Actor versions, Actor env vars, builds, runs, logs, tasks, schedules, webhooks, webhook dispatches, datasets, key-value stores, request queues, the store, and users. `vitest.config.mts` now defines two projects, so the existing unit tests stay fast and offline: - `pnpm test` - unit tests only (unchanged behavior) - `pnpm test:integration` - integration tier only - `pnpm test:all` - both The tier needs `APIFY_TEST_USER_API_TOKEN`. A new `integration_tests` job in `check.yaml` runs the tier on Node 26. It is skipped for fork PRs, where repository secrets are unavailable, and can be triggered on demand via `workflow_dispatch`. *✍️ Drafted by Claude Code*
# Conflicts: # CONTRIBUTING.md # src/resource_clients/dataset.ts # src/resource_clients/key_value_store.ts
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