feat!: generate all output models from the OpenAPI spec - #985
Conversation
|
See more at https://github.com/apify/apify-client-js/actions/runs/32135970079#summary-95707249991 |
db33e50 to
8ca8555
Compare
8ca8555 to
057a750
Compare
|
Regarding the single generated file size; What we can do:
Let me know @B4nan WDYT. |
|
Let's leave it, it's true that it's just a huge file, but the code is far away from complex. |
|
Let's wait for #995 and then rebase upon it, so that we can better test this. |
c9b3ae3 to
9f26108
Compare
116f40b to
b0e4dd2
Compare
Every published output model is now declared on top of a type generated from the published
OpenAPI specification instead of being hand-written. `pnpm generate:types` downloads the
specification into git-ignored `tmp/`, turns it into `src/generated/api.ts` with
`openapi-typescript`, and records the specification's version in `package.json`. The
specification itself is never committed -- only the version it was generated from, matching
apify-client-python.
Nothing re-exports the generated file. `src/models.ts` declares each model with
`interface ... extends` over a generated schema, never as a type alias: the docs plugin only
emits API-reference pages for classes, interfaces and enums, so an alias silently deletes a
model's page. Every deviation from the specification is argued at its declaration and asserted
in `src/spec_guards.ts`, so a spec change that invalidates one fails `pnpm build:node`. That
file 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.
A nightly workflow regenerates on `master` and opens a pull request when `src/generated/api.ts`
changed. Renovate cannot do this -- the specification is a live document, not an npm dependency
-- so without it the generated types never move and no guard can ever fire. Gating on the
generated output rather than on the specification version is what keeps it quiet: `info.version`
is an apify-docs build stamp, not an API version, so it moves on every docs redeploy.
Nine published types were wrong, six of them contradicting the client's own runtime.
`nextExclusiveStartKey` was a required `string` while `listKeys()` has always compared it to
`null`. `Webhook.lastDispatch` was a `string` while the API returns an object.
`Schedule.nextRunAt`, `Schedule.lastRunAt` and `RequestQueueClientRequestSchema.handledAt` were
`string` although `parseDateFields()` had already converted them to `Date`.
`MonthlyUsage.dailyServiceUsages[].date` was a `string` while `UserClient.monthlyUsage()` passes
a matcher that converts it, and `RequestQueue.expireAt` was a `string` although the key ends in
`At`, so `parseDateFields()` has always converted it. `Build.status` omitted `READY` and
`RUNNING`, which `waitForFinish()` documents. `RequestQueueClientGetRequestResult` was a
queue-head projection while the endpoint returns the whole request. And
`UserPlan.enabledPlatformFeatures` used an enum missing three features that appear as keys of
`EffectivePlatformFeatures`.
Four places deliberately keep the hand-written shape, each argued at its declaration and
excluded from the width guard. `ActorVersion` keeps its discriminated union, because the flat
spec shape has `sourceType` nullable and all four source locations optional, which leaves every
variant unreachable. `WebhookCondition` keeps its single-id variants, because the flat shape
would let a caller send none of the three ids or all of them. `ActorRun.generalAccess` keeps
`RUN_GENERAL_ACCESS`, because the spec reuses the storage-wide `GeneralAccess`, which also lists
`ANYONE_WITH_NAME_CAN_READ` -- a run has no name to be addressed by. `Schedule.timezone` keeps
the curated IANA union from `src/timezones.ts`, which the spec types as a bare `string`.
`models.ts` classifies each deviation by kind: `*RePointed` for a field aimed at a published type
rather than the generated one, `*SpecNarrowings` for the spec being narrower than the API,
`*ClientNarrowings` for the published type being narrower on purpose, `*ClientConversions` for a
value the client converts before handing it over, and `*SpecGaps` for a field the API returns that
the spec does not describe yet. Each gap is asserted still missing upstream, so the day the spec
covers it the build says so.
Types only, with two runtime exceptions. `parseDateFields()`' depth limit goes from 3 to 4,
because a list response nests one level deeper than the single resource it wraps: at the previous
limit `dispatches().list()` returned `calls[].startedAt` as a raw string while the published type
promised a `Date`, even though the same field came back as a `Date` from
`webhookDispatch(id).get()`. And the key-value store's pagination loop widens its
`nextExclusiveStartKey` check from `!== null` to `!= null`, so an omitted key ends the listing
instead of restarting it.
The v3 upgrading guide gains a section walking through the breaks that need more than a null
check.
BREAKING CHANGE: every published output model now follows the specification's nullability and
optionality instead of the previous hand-written shape. Per resource group:
Dataset and WebhookDispatch: Dataset.name, actId and actRunId gain `| null`; Dataset.fields
becomes optional and nullable; Dataset.stats and itemsPublicUrl become optional;
DatasetStatistics.fieldStatistics becomes optional and nullable; FieldStatistics.min, max,
nullCount and emptyCount gain `| null`; WebhookDispatch.calls and 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. The deeper
parseDateFields traversal also reaches one level further into caller-owned blobs the API stores
verbatim, so a listed request's `userData.foo.somethingAt` now comes back as a `Date` rather than
the string it was written as.
KeyValueStore: KeyValueStore.name, actId, actRunId and username gain `| null`; userId becomes
optional and nullable; keysPublicUrl becomes optional; KeyValueClientListKeysResult
.exclusiveStartKey and nextExclusiveStartKey become optional and nullable, and the pagination
loop's check widens from `!== null` to `!= null` to match. Newly exposed:
KeyValueStore.consoleUrl, recordsPublicUrl and schema, and KeyValueStoreStats.s3StorageBytes.
RequestQueue: RequestQueue.expireAt changes from `string` to `Date`;
RequestQueueClientRequestSchema.handledAt changes from `string` to `Date | null`, on the way in as
well as out, because the same type describes `updateRequest()`'s argument; its url and uniqueKey
become optional, since the spec describes the stored request rather than a submission; and
RequestQueueClientGetRequestResult is the whole request rather than the queue-head projection.
Newly added: RequestQueueClientRequestToAdd and RequestQueueClientRequestToUpdate, which keep the
fields each submission genuinely requires, and the split of the queue head into HeadRequest and
LockedHeadRequest, so only the locked variant carries lockExpiresAt.
Actor versions and environment variables: BaseActorVersion.versionNumber becomes required;
buildTag, applyEnvVarsToBuild and envVars gain `| null`; ActorVersionSourceFile.format and
content become optional, and `format` is the spec's SourceCodeFileFormat rather than an inline
`'TEXT' | 'BASE64'`; ActorEnvironmentVariable.name becomes required and isSecret gains `| null`;
and ActorVersionSourceFiles.sourceFiles accepts ActorVersionSourceFolder entries as well. Newly
added: ActorSourceType.SourceCode, ActorVersionSourceCode and ActorVersionSourceFolder.
Actor: Actor.actorStandby loses the `& { isEnabled: boolean }` intersection and gains `| null`;
deploymentKey and actorPermissionLevel become optional; description, title, seoTitle,
seoDescription, isDeprecated, exampleRunInput and taggedBuilds gain `| null`; every field of
ActorStats and 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; PricePerDatasetItemActorPricingInfo.tieredPricing and
ActorChargeEvent.eventTieredPricingUsd; and the TieredPricingPerDatasetItem and
TieredPricingPerEvent types.
Build: Build.status widens from the four terminal statuses to all eight Actor job statuses;
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. A Build
is therefore still not assignable to a BuildCollectionClientListItem, which requires the
usageTotalUsd that only the list endpoint always returns. Newly exposed: Build.actVersion,
BuildStats.imageSizeBytes and BuildCollectionClientListItem.buildNumberInt.
ActorRun: ActorRun no longer extends ActorRunListItem, because the spec describes the run and
the list item as two schemas that genuinely disagree. 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 and Store: 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 drops description and
actorStandby, which the list endpoint does not return, and keeps title, which it does, through a
spec-gap block; 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, which was a one-field `{ pricingModel: string }` placeholder before.
Webhook: Webhook.lastDispatch changes from `string` to `WebhookLastDispatch | null` and becomes
optional; isAdHoc, doNotRetry, shouldInterpolateStrings, payloadTemplate and requestUrl become
optional and nullable; stats becomes optional and nullable; headersTemplate and description gain
`| null`; and WebhookStats.totalDispatches becomes optional. Newly added: the
WebhookLastDispatch type. WebhookEventType now lives in `src/models.ts` and is re-exported from
`src/resource_clients/webhook`, replacing the duplicate declaration that existed to avoid an
import cycle.
Schedule: Schedule.nextRunAt and lastRunAt change from `string` to `Date | null` and become
optional; title and description gain `| null`; notifications becomes optional and its `email`
becomes optional; ScheduleActionRunActor.runInput and runOptions gain `| null`;
ScheduleActionRunActorTask.input changes from `string` to `object | null`, which is an input
break as well as an output one, because ScheduleCreateOrUpdateData is picked from Schedule;
ScheduledActorRunInput.body and contentType become optional and nullable; and
ScheduledActorRunOptions is now the spec's TaskOptions, so build, timeoutSecs and memoryMbytes
become optional and nullable. Newly exposed: ScheduledActorRunOptions.maxItems and
maxTotalChargeUsd.
User and usage: MonthlyUsage.dailyServiceUsages[].date stays a `Date`, now published as one
through a `*ClientConversions` block rather than as the `string` it was typed as;
MonthlyUsage.monthlyServiceUsage is the published ServiceUsage rather than an inline map; and
UserPlan.enabledPlatformFeatures is `string[]` rather than the PlatformFeature enum, which was
missing three features the platform has. Newly exposed: UsageItem.priceTiers, and the
DailyServiceUsage, ServiceUsage, UsageItem and PriceTier types, which were private interfaces
before, plus TieredPricingPerDatasetItemEntry and TieredPricingPerEventEntry.
d9b0c0e to
238b51d
Compare
Generates every output model from the published OpenAPI specification via
openapi-typescriptinstead of hand-writing it.How it works
pnpm generate:typesdownloads the specification into git-ignoredtmp/and writessrc/generated/api.ts. The specification is not committed -- only the version it was generated from, inapify.openapiSpec.versioninpackage.json.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.masterand opens a pull request whensrc/generated/api.tschanged. Not automerged. Same layout as apify-client-python.Notes
BREAKING CHANGE:footer has the per-resource breakdown, and the v3 upgrading guide covers the breaks that need more than a null check.parseDateFields()' depth limit goes from 3 to 4 so a list response gets the sameDateconversion as the single resource it wraps, and the key-value store'snextExclusiveStartKeycheck widens to!= nullso an omitted key ends the listing instead of restarting it.docs.apify.comand would go red on any docs redeploy. The nightly run is the mechanism, as in the Python client.Important
notify_on_failureneeds aSLACK_WEBHOOK_URLrepository secret, which this repo does not have yet. Without it a failed nightly run reports nowhere.Follow-ups
RequestQueueClientBatchRequestsOperationResultstill types the batch delete path, where the API answers withBatchDeleteResult. Fixing it needs a new published type and a changed return type.scripts/openapi_spec.mtshas no tests. It runs its CLI at import time, so its pure helpers need extracting into a sibling module first, the wayspec_transform.mtsalready is.isPublicon a task andstats.lastRunStartedAton a listed one. Neither is in the specification, so neither is typed -- as in v2.✍️ Drafted by Claude Code