feat!: generate all output models from the OpenAPI spec - #985
Draft
vdusek wants to merge 14 commits into
Draft
Conversation
Lands only the input artifact. Nothing reads the snapshot yet -- the type generator, the generated types and the scheduled sync workflow follow separately, so that change can land as one complete functional unit. The path is src/generated/ even though nothing generates the file today; it is where the generator will look for it, and matching it now avoids a rename later. scripts/fetch-spec.mts writes the response body verbatim instead of re-serializing it. The endpoint already pretty-prints with a 2-space indent and LF, so passing the bytes through keeps refreshes free of formatting-only diffs. It validates the body -- parseable JSON, openapi 3.1.x, non-empty paths and components.schemas -- before touching the target, and writes through a temp file plus rename, so a docs outage serving an error page with a 200 cannot land as a corrupt snapshot. .gitattributes pins LF for the whole repo. All 156 tracked text files are already LF in the index, so it renormalizes nothing; it exists so a clone made with core.autocrlf=true cannot turn a refresh into 29k lines of line-ending churn. tsconfig.scripts.json needs an explicit `types: ["node"]`. This setup gets no automatic @types inclusion, and unlike src/ a script importing only node: builtins pulls in nothing transitively, so without it the Node globals go unresolved.
Adds openapi-typescript as a dev dependency and generates src/generated/api.ts from the committed spec snapshot, then adapts the Dataset and WebhookDispatch models on top of it. Types only: nothing lands in the runtime. The generated file is api.ts rather than api.d.ts on purpose. tsc does not copy .d.ts inputs into outDir, so a declaration file would leave every published type referencing a path that does not exist in dist. `additionalProperties` is deliberately off. It is often taken for the analogue of the Python client's `extra_fields = "allow"`, but that setting relaxes runtime validation while this flag relaxes static typing, appending an index signature to every object. Responses are never validated at runtime here, so unknown server fields already pass through untouched and the flag would only make every property typo type-check. Models are declared with `interface ... extends`, never as type aliases: the docs plugin only emits API-reference pages for classes, interfaces and enums, so an alias silently deletes a model's page. src/spec_guards.ts turns spec drift into a build failure. It is not re-exported from src/index.ts, so its exports satisfy noUnusedLocals without growing the public API or the rendered reference. Covered: a field an override block replaces being dropped or renamed, a documented spec gap being filled, and the shared @apify/consts enums diverging from the spec. Scoping the js-yaml override in pnpm-workspace.yaml is a prerequisite, not a drive-by. The v4 line was raised to ^5.0.0 as a routine major in #947, having started as a ^4.2.0 advisory floor in #942; js-yaml 5 dropped the `types` export that @redocly/openapi-core reads at import time, so openapi-typescript cannot run in this repo without the exemption. The generator's pure helpers are unit tested from test/spec_transform.test.mts. The `.mts` extension is required, not cosmetic: a CommonJS test cannot `require` the ESM script it imports, so vitest's include glob gained `mts` alongside `js` and `ts`. BREAKING CHANGE: Dataset, DatasetStats, DatasetStatistics, FieldStatistics, WebhookDispatch, WebhookDispatchCall and WebhookDispatchEventData now follow the spec's nullability and optionality instead of the previous hand-written shapes. Dataset.name, Dataset.actId and Dataset.actRunId gain `| null`; Dataset.fields becomes optional and nullable; Dataset.stats and Dataset.itemsPublicUrl become optional; DatasetStatistics.fieldStatistics becomes optional and nullable; FieldStatistics.min, max, nullCount and emptyCount gain `| null`; WebhookDispatch.calls and WebhookDispatch.eventData become optional; and WebhookDispatch.webhook changes from Pick<Webhook, 'requestUrl' | 'isAdHoc'> to WebhookDispatchWebhookSummary, which is nullable and also carries actionType and condition. Newly exposed: Dataset.consoleUrl, Dataset.schema and DatasetStats.inflatedBytes.
Adopting the spec fixes a type lie that `listKeys()` already worked around at runtime: `nextExclusiveStartKey` was published as a required `string`, while the pagination loop has always compared it against `null` and the API omits it entirely for a listing that was not truncated. The loop's check widens from `!== null` to `!= null` to match. `title` is the only field of the store that the spec still does not describe, so it is the whole of KeyValueStoreSpecGaps; `username` moved out of the gap list because the spec covers it now. `generalAccess` keeps its `| null`, which the spec omits, for the same reason as on Dataset -- a storage may follow the owner's user setting. BREAKING CHANGE: KeyValueStore, KeyValueStoreStats, KeyValueListItem and KeyValueClientListKeysResult now follow the spec's nullability and optionality instead of the previous hand-written shapes. KeyValueStore.name, actId, actRunId and username gain `| null`; KeyValueStore.userId becomes optional and nullable; KeyValueStore.keysPublicUrl becomes optional; KeyValueClientListKeysResult.exclusiveStartKey and nextExclusiveStartKey become optional and nullable. Newly exposed: KeyValueStore.consoleUrl, KeyValueStore.recordsPublicUrl, KeyValueStore.schema and KeyValueStoreStats.s3StorageBytes.
The spec models a version as a single flat object with all four source locations optional and `sourceType` nullable. The published discriminated union is kept regardless, because it narrows the source location down to the one field that actually applies -- so the four locations are dropped from the shared base and reinstated, as required, by the variant whose `sourceType` implies them. Dropping the spec's `sourceType: null` is the one deliberate narrowing here: a version with no source type carries no usable source location either, so adopting the `null` would make every variant unreachable and buy nothing. `ActorSourceType` gains `SOURCE_CODE`. Both the spec and `@apify/consts` list it, so a version built that way was previously unrepresentable; `ActorVersionSourceCode` is the matching variant and adds no source location of its own. `spec_guards.ts` now pins the enum from both sides, the same way `WebhookDispatchStatus` already was. The `sourceFiles` element type also gains a folder shape. `SourceCodeFolder` was always part of that list -- the API distinguishes folders with a `folder` flag rather than by nesting -- but the hand-written `ActorVersionSourceFile` could not express one. BREAKING CHANGE: ActorVersion, its four source-type variants, ActorVersionSourceFile, ActorEnvironmentVariable and FinalActorVersion now follow the spec's nullability and optionality. BaseActorVersion.versionNumber becomes required; buildTag, applyEnvVarsToBuild and envVars gain `| null`; ActorVersionSourceFile.format and content become optional, and its `format` is the spec's `SourceCodeFileFormat` rather than an inline `'TEXT' | 'BASE64'`; ActorEnvironmentVariable.name becomes required and isSecret gains `| null`; ActorVersionSourceFiles.sourceFiles accepts ActorVersionSourceFolder entries as well; and FinalActorVersion.buildTag is `string | null` rather than `string`. Newly added: ActorSourceType.SourceCode, ActorVersionSourceCode and ActorVersionSourceFolder.
The pricing union transfers wholesale. The spec models it as a `oneOf` with a `pricingModel` discriminator and an OpenAPI 3.1 `const` per variant, so `openapi-typescript` emits literal types and the published union keeps narrowing exactly as before -- no override needed. That also brings tiered pricing in: `PricePerDatasetItemActorPricingInfo.tieredPricing` and `ActorChargeEvent.eventTieredPricingUsd`, which overlaps what #970 adds by hand against master. `ActorDefinition.output` is the one field of the definition the spec still omits, so it is the whole of ActorDefinitionSpecGaps. Its `readme`, `input` and `changelog` keep the `| null` the spec drops -- they are paths into the Actor's source tree and the API reports `null` for a file that is absent, which is what the hand-written definition already recorded. `Actor.versions` is excluded from the width guard on purpose. It re-points at the discriminated `ActorVersion` union, which is deliberately narrower than the spec's flat `Version`, so the one-directional assignability rule does not apply to it. BREAKING CHANGE: Actor, ActorStats, ActorStandby, ActorDefaultRunOptions, ActorExampleRunInput, ActorTaggedBuild, ActorTaggedBuilds, ActorDefinition, ActorChargeEvent, ActorCollectionListItem and the four pricing-info shapes now follow the spec's nullability and optionality. Actor.actorStandby loses the `& { isEnabled: boolean }` intersection and gains `| null`; Actor.deploymentKey and actorPermissionLevel become optional; Actor.description, title, seoTitle, seoDescription, isDeprecated, exampleRunInput and taggedBuilds gain `| null`; every field of ActorStats becomes optional; every field of ActorDefaultRunOptions becomes optional; ActorExampleRunInput.body and contentType become optional; ActorTaggedBuilds values may be `null`; ActorDefinition.actorSpecification, name and version become optional; ActorChargeEvent.eventDescription becomes required while eventPriceUsd becomes optional; FlatPricePerMonthActorPricingInfo.trialMinutes and PricePerDatasetItemActorPricingInfo.unitName become required, while the latter's pricePerUnitUsd becomes optional. Newly exposed: Actor.pictureUrl, standbyUrl, notice, isCritical, isGeneric, isSourceCodeHidden and hasNoDataset; ActorStats.actorReviewCount, actorReviewRating, bookmarkCount and publicActorRunStats30Days; ActorDefaultRunOptions.maxItems and forcePermissionLevel; ActorDefinition.defaultMemoryMbytes; ActorTaggedBuild.buildNumberInt; ActorChargeEvent.isPrimaryEvent and isOneTimeEvent; ActorCollectionListItem.title and stats; and the TieredPricingPerDatasetItem and TieredPricingPerEvent types.
`Build.status` was typed as `ACT_JOB_TERMINAL_STATUSES[number]`, so the four terminal statuses
only. That contradicted the client's own documentation: `waitForFinish()` states the returned
build "will have status READY or RUNNING" when the wait times out, and those two were not in the
type. It now uses the full `ACTOR_JOB_STATUSES` union, checked against the spec's `ActorJobStatus`.
`BuildMeta.origin` moves from a bare `string` to the shared `META_ORIGINS` union, likewise
checked against the spec's `RunOrigin`.
`BuildCollectionClientListItem` was assembled out of `Pick<Build, ...>` guesses. It now comes
from `BuildShort`, the schema the list endpoint actually returns, which is where `buildNumber`
and `buildNumberInt` come from.
`OpenApiDefinition` stays hand-written. `GET /v2/actors/{actorId}/builds/{buildId}/openapi.json`
is typed as a bare `object` in the spec, so there is nothing to adapt it to.
BREAKING CHANGE: Build, BuildMeta, BuildStats, BuildUsage, BuildOptions and
BuildCollectionClientListItem now follow the spec's nullability and optionality.
Build.status widens to all eight Actor job statuses; Build.finishedAt, stats, options, usage,
usageUsd, usageTotalUsd, inputSchema, readme and actorDefinition gain `| null`;
BuildMeta.clientIp and userAgent become optional while origin narrows from `string` to the
META_ORIGINS union; every field of BuildStats becomes optional; BuildUsage.ACTOR_COMPUTE_UNITS
and every field of BuildOptions gain `| null`; and BuildCollectionClientListItem is now derived
from the spec's BuildShort, so actId and userId become optional, meta stays optional and
usageTotalUsd and buildNumber become required. Newly exposed: Build.actVersion,
BuildStats.imageSizeBytes and BuildCollectionClientListItem.buildNumberInt.
`ActorRun` no longer extends `ActorRunListItem`. The spec describes the run and the list item as two separate schemas that genuinely disagree -- `usageTotalUsd` is required on the list item and optional-nullable on the full resource -- so the inheritance could only be kept by narrowing one of them to fit the other. Both are now derived independently. `ActorRun.generalAccess` is the one deliberate narrowing. The spec reuses the storage-wide `GeneralAccess` schema, which also lists `ANYONE_WITH_NAME_CAN_READ`; a run has no name to be addressed by, which is exactly why `@apify/consts` declares a separate three-member `RUN_GENERAL_ACCESS`. That stays the published type, and `spec_guards.ts` asserts subset rather than equality for it. `ActorRunOptions.restartOnError` is the one field the spec omits, so it is the whole of ActorRunOptionsSpecGaps. `ActorRunMeta.origin` moves from a bare `string` to the shared `META_ORIGINS` union. `ActorRunUsage` is deliberately still one type. The spec splits the same twelve fields into `RunUsage` and `RunUsageUsd`, which are structurally identical, so `usage` and `usageUsd` both keep pointing at the single published name. BREAKING CHANGE: ActorRun, ActorRunListItem, ActorRunMeta, ActorRunStats, ActorRunOptions, ActorRunUsage and ActorRunStorageIds now follow the spec's nullability and optionality, and ActorRun no longer extends ActorRunListItem. ActorRun.containerUrl becomes optional; finishedAt, statusMessage, exitCode, buildNumber, gitBranchName, usage, usageUsd and usageTotalUsd gain `| null`; ActorRunListItem.finishedAt becomes optional and nullable while usageTotalUsd becomes required and userId becomes optional; ActorRunMeta.userAgent becomes optional and gains `| null`, clientIp gains `| null`, and origin narrows from `string` to the META_ORIGINS union; every field of ActorRunStats becomes optional and inputBodyLen gains `| null`; ActorRunOptions.maxItems and maxTotalChargeUsd gain `| null`; every field of ActorRunUsage gains `| null`; and ActorRunStorageIds no longer guarantees a `default` alias in any of its three groups, nor the groups themselves. Newly exposed: ActorRun.isStatusMessageTerminal, metamorphs and platformUsageBillingModel; ActorRunListItem.buildNumberInt; ActorRunMeta.scheduleId and scheduledAt; ActorRunStats.migrationCount and rebootCount; and the ActorRunMetamorph type.
`Task.description` is the one field the spec omits, and `TaskList` inherits the same gap, so
`spec_guards.ts` asserts it against both `Task` and `TaskShort`.
`Task.input` keeps the array form the spec drops. The spec models the input as a plain JSON
object, but this same type backs `TaskUpdateData`, so narrowing it would start rejecting
`update({ input: [...] })` calls that work today.
`TaskList` was `Omit<Task, 'options' | 'input'>`, a guess at what the list endpoint returns. It
now comes from `TaskShort`, which is where `actName` and `actUsername` come from.
`PricingInfo` was a one-field `{ pricingModel: string }` placeholder. The spec describes the
whole `CurrentPricingInfo` shape, so the Store listing now carries the trial, unit and
per-event price fields as well. It stays a flat summary rather than one of the
`ActorRunPricingInfo` variants -- that is how the Store endpoint reports it.
BREAKING CHANGE: Task, TaskStats, TaskOptions, TaskList, ActorStoreList and PricingInfo now
follow the spec's nullability and optionality. Task.stats becomes optional and nullable;
Task.username, title, options, input and actorStandby gain `| null`; Task.actorStandby is the
full ActorStandby rather than `Partial<ActorStandby>`; TaskStats.totalRuns becomes optional;
every field of TaskOptions gains `| null`; TaskList is now derived from the spec's TaskShort, so
it no longer carries `title` as an inherited required field and drops nothing else;
ActorStoreList.title becomes required while url and currentPricingInfo become optional, and
description, pictureUrl and userPictureUrl gain `| null`. Newly exposed: Task.removedAt and
standbyUrl; TaskOptions.maxItems and maxTotalChargeUsd; TaskList.actName and actUsername;
ActorStoreList.userFullName, categories, notice, isWhiteListedForAgenticPayments,
actorReviewCount, actorReviewRating, bookmarkCount and badge; and the full set of
PricingInfo fields.
`Webhook.lastDispatch` was typed as `string`. The API returns an object -- the spec's `ExampleWebhookDispatch`, published here as `WebhookLastDispatch` -- carrying the dispatch status and its finish and removal times. The old type could not have been right for any caller. `WebhookCondition` keeps its union of single-id variants. The spec models it as one flat object with all three ids optional and nullable, but this type also backs `WebhookUpdateData`, where the flat shape would let a caller send none of the three or all three at once. `isApifyIntegration` is the one field the spec omits from the full `Webhook` schema while carrying it on `WebhookShort`, the listing shape, so it is the whole of WebhookSpecGaps. This also settles the TODO that the Dataset and WebhookDispatch commit left in `spec_guards.ts`. With `Webhook` spec-derived, `requestUrl` can be pinned against `WebhookDispatchWebhookSummary` again -- nullable on both sides, because a Slack or email hook action has no URL. `isAdHoc` is deliberately left unpinned: the spec types it nullable on the webhook and non-nullable on the summary, and asserting that would only encode the inconsistency. `WebhookEventType` now lives in `src/models.ts` and is re-exported from `src/resource_clients/webhook`. The duplicate declaration that existed to avoid an import cycle is gone, along with the guard that kept the two copies in step. BREAKING CHANGE: Webhook, WebhookStats and the three WebhookCondition variants now follow the spec's nullability and optionality. Webhook.lastDispatch changes from `string` to `WebhookLastDispatch | null` and becomes optional; Webhook.isAdHoc, doNotRetry, shouldInterpolateStrings, payloadTemplate and requestUrl become optional and nullable; Webhook.stats becomes optional and nullable; Webhook.headersTemplate and description gain `| null`; and WebhookStats.totalDispatches becomes optional. Newly added: the WebhookLastDispatch type.
`Schedule.nextRunAt` and `lastRunAt` were typed as `string`. Both end in `At`, so `parseDateFields()` has always converted them to `Date` before the caller sees them -- the published type contradicted the client's own runtime behaviour. They are now `Date | null`. The action union transfers wholesale. The spec models it as a `oneOf` discriminated on `type` with a `const` per variant, so only the `type` field itself is overridden, to keep publishing the `ScheduleActions` runtime enum rather than a bare string literal. `spec_guards.ts` pins both constants; unlike the other two published enums there is no `@apify/consts` equivalent to check against as well. `Schedule.timezone` keeps the curated IANA union from `src/timezones.ts`. The spec types it as a bare `string`, and this type also backs `ScheduleCreateOrUpdateData`, so widening it would drop the completion and typo-checking the union exists for. `ScheduledActorRunOptions` is now the spec's `TaskOptions`, which is the schema the API really uses for a scheduled run's options; that is where `maxItems` and `maxTotalChargeUsd` come from. BREAKING CHANGE: Schedule, ScheduleAction and its two variants, ScheduledActorRunInput and ScheduledActorRunOptions now follow the spec's nullability and optionality. Schedule.nextRunAt and lastRunAt change from `string` to `Date | null` and become optional; Schedule.title and description gain `| null`; Schedule.notifications becomes optional and its `email` becomes optional; ScheduleActionRunActor.runInput and runOptions gain `| null`; ScheduleActionRunActorTask.input changes from `string` to `object | null`; ScheduledActorRunInput.body and contentType become optional and nullable; and ScheduledActorRunOptions.build, timeoutSecs and memoryMbytes become optional and nullable. Newly exposed: ScheduledActorRunOptions.maxItems and maxTotalChargeUsd.
`UserPlan.enabledPlatformFeatures` is no longer `PlatformFeature[]`. The spec types it as a plain
`string[]`, and the enum was demonstrably incomplete: `PROXY_RESIDENTIAL`, `ACTORS_PUBLIC_ALL` and
`ACTORS_PUBLIC_DEVELOPER` all appear as keys of `EffectivePlatformFeatures` but never made it into
the enum, so using it as the element type promised a completeness that was not real. The enum
stays published, for comparisons.
`MonthlyUsage.dailyServiceUsages[].date` changes from `Date` to `string`. `parseDateFields()` only
converts keys ending in `At`, so the value was never a `Date` at runtime.
`User` is built on `UserPrivateInfo`, with `plan`, `effectivePlatformFeatures` and `isPaying`
widened back to optional. All three are required in that schema, but the same published type also
describes `GET /v2/users/{userId}`, whose `UserPublicInfo` response carries none of them.
`EffectivePlatformFeature`, `EffectivePlatformFeatures` and the user profile shape were private
interfaces. They are now published as named types -- the profile as `UserProfile` -- so the
reference can link to them instead of inlining.
BREAKING CHANGE: User, UserProxy, ProxyGroup, UserPlan, MonthlyUsage, UsageCycle,
AccountAndUsageLimits, MonthlyUsageCycle, Limits and Current now follow the spec's nullability
and optionality. User.profile becomes optional; UserPlan.enabledPlatformFeatures changes from
PlatformFeature[] to string[] and every other field of UserPlan becomes optional;
ProxyGroup.description gains `| null`; UserProfile.bio, pictureUrl, githubUsername, websiteUrl
and twitterUsername gain `| null`; and the daily usage entries carry `date` as a string. Newly
exposed: UserPlan.tier, apiRateLimitBoosts, maxScheduleCount, maxConcurrentActorRuns and
planPricing; Limits.maxScheduleCount; Current.scheduleCount; and the UserProfile,
EffectivePlatformFeature and EffectivePlatformFeatures types.
Three published types were describing the wrong shape.
`RequestQueueClientGetRequestResult` was `Omit<RequestQueueClientListItem, 'retryCount'>`, a
queue-head projection, while `GET .../requests/{requestId}` answers with the whole request.
`RequestQueueClientRequestSchema.handledAt` was `string` although it ends in `At`, so
`parseDateFields()` has always handed the caller a `Date`. And `RequestQueueClientListItem`
conflated the plain and locked head requests, which the spec separates -- the locked one is now
published as `RequestQueueClientLockedListItem`, and `lockExpiresAt` is required on it rather
than optional on both.
`RequestQueueClientListAndLockHeadResult` no longer extends `RequestQueueClientListHeadResult`.
The spec describes the two as separate schemas that disagree about which fields are required, and
the locked variant carries a different element type.
Batch and add methods now take `RequestQueueClientRequestToAdd` rather than
`Omit<RequestQueueClientRequestSchema, 'id'>`. Once `uniqueKey` and `url` became optional on the
stored request -- which is correct for a response -- the old input type allowed submissions the
API rejects, and `batchAddRequests()` could no longer build a valid retry list from them.
`title` is absent from every queue schema in the spec; `username` and `expireAt` are carried on
`RequestQueueShort`, the listing shape, and omitted from the full `RequestQueue`. All three are
tracked as spec gaps. `consoleUrl` is kept optional for the same reason as on Dataset: the client
types `requestQueues().list()` items as this model, and the listing does not carry it.
Known follow-up, unchanged by this commit: `RequestQueueClientBatchRequestsOperationResult` is
derived from the spec's `BatchAddResult` and is still used as the return type of the batch delete
path too, where the API answers with `BatchDeleteResult` and reports processed requests by id or
unique key rather than as added requests.
BREAKING CHANGE: RequestQueue, RequestQueueStats, RequestQueueClientListItem,
RequestQueueClientListHeadResult, RequestQueueClientListAndLockHeadResult,
RequestQueueClientListRequestsResult, RequestQueueClientRequestSchema,
RequestQueueClientGetRequestResult, RequestQueueClientAddRequestResult,
RequestQueueClientProlongRequestLockResult, RequestQueueClientUnlockRequestsResult,
RequestQueueClientBatchRequestsOperationResult and RequestQueueClientRequestToDelete now follow
the spec. RequestQueue.name, actId and actRunId gain `| null` and stats becomes optional;
RequestQueueClientListItem.retryCount becomes optional and loses `lockExpiresAt`;
RequestQueueClientRequestSchema.id, uniqueKey and url become optional, handledAt changes from
`string` to `Date | null`, payload widens to `string | object | null`, headers to `object | null`,
and errorMessages, noRetry and loadedUrl gain `| null`; RequestQueueClientGetRequestResult is now
the full request schema; RequestQueueClientListAndLockHeadResult.clientKey and
queueHasLockedRequests become optional; and the four batch and add methods require `uniqueKey`
and `url` on every submitted request. Newly exposed: RequestQueue.consoleUrl,
RequestQueueClientLockedListItem and RequestQueueClientRequestToAdd.
…uild stamp Renovate cannot see the spec -- it is a live document, not an npm dependency -- so without this workflow the committed snapshot never moves and none of the drift guards in src/spec_guards.ts can ever fire. The refresh cannot compare the file byte for byte. Apify's `info.version` is an apify-docs *build* stamp (`v2-2026-07-28T083939Z`), not an API version, so it changes on every docs deploy whether or not a single endpoint moved; a byte comparison would open a pull request nearly every week whose whole diff is that one line. `isOnlyBuildStampChange()` compares the two revisions with that field dropped, and a stamp-only refresh is discarded without regenerating anything. An unparseable revision counts as a real change on purpose. If docs.apify.com answers with an error page, that should surface as a pull request to look at rather than be silently thrown away. Deliberately not automerged. A spec change can widen nullability across hundreds of fields or fill a gap that makes a `*SpecGaps` entry stale, and both need a human to read the diff.
Contributor
|
See more at https://github.com/apify/apify-client-js/actions/runs/30617512394#summary-91114025972 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Generates every output model from the committed OpenAPI snapshot via
openapi-typescriptinstead ofhand-writing it, and adds a scheduled job that keeps the snapshot fresh.
Important
Base needs retargeting. This branch sits on
feat/openapi-spec-snapshot, which is not pushedyet, so the first commit here (
chore: vendor the Apify API OpenAPI spec and add a refresh script)belongs to that PR. Review from the second commit onward.
How it works
pnpm generate:typesturnssrc/generated/openapi.jsonintosrc/generated/api.ts. Nothingre-exports that file --
src/models.tsdeclares each published model on top of a generated schema, andsrc/spec_guards.tsasserts every deviation at compile time, so an invalidating spec change failspnpm build:node. One commit per resource group.Published types that were wrong
Seven, four of them contradicting the client's own runtime:
nextExclusiveStartKeywas a requiredstring;listKeys()has always compared it tonull.Webhook.lastDispatchwasstring; the API returns an object.Schedule.nextRunAt,Schedule.lastRunAtandRequestQueueClientRequestSchema.handledAtwerestring, butparseDateFields()had already converted them toDate.MonthlyUsage.dailyServiceUsages[].datewasDatebut never converted -- it does not end inAt.Build.statusomittedREADYandRUNNING, whichwaitForFinish()documents.RequestQueueClientGetRequestResultwas a queue-head projection; the endpoint returns the whole request.UserPlan.enabledPlatformFeaturesused an enum missing three features that appear inEffectivePlatformFeatures. Now the spec'sstring[].Where the spec is deliberately not adopted
Argued at each declaration, excluded from the width guard:
ActorVersionkeeps its discriminated union(the flat spec shape leaves every variant unreachable),
WebhookConditionkeeps its single-id variants(the flat shape would allow none or all three ids),
ActorRun.generalAccesskeepsRUN_GENERAL_ACCESS(the spec reuses storage-wide
GeneralAccess, which listsANYONE_WITH_NAME_CAN_READ; a run has noname).
Schedule.timezonestays the curated IANA union.Notes
fire. A byte comparison would open a weekly noise PR --
info.versionis an apify-docs build stamp,not an API version -- so
isOnlyBuildStampChange()ignores that one field. Not automerged.into
models.ts.pnpm generate:typesreproduces the committedapi.tsbyte for byte, enforced by the newgenerated_typesjob from the snapshot, never fromdocs.apify.com. Each guard rule was verifiednegatively by breaking it on purpose.
Open questions
ActorVersionunion vs. the spec's flat shape is the one decision that changes public APIergonomics rather than just accuracy.
RequestQueueClientBatchRequestsOperationResultstill types the batch delete path, where the APIanswers with
BatchDeleteResult. Fixing it needs a new published type and a changed return type.✍️ Drafted by Claude Code