Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .github/workflows/docs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ on:
- 'typedoc.json'
- 'package.json'
- 'pnpm-lock.yaml'
- 'pnpm-workspace.yaml'
- 'patches/**'
- '.github/workflows/docs.yml'
workflow_dispatch:

Expand Down
57 changes: 31 additions & 26 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,30 +1,12 @@
# Trello.js changelog

## Unreleased
## v2.3.0 (2026-09-10)

### Fixed

- `Organization` gained `trial`, the object Trello now returns on every workspace: `{ eligible: boolean, endDate: Date | null }`. It was silently stripped in normal mode and raised `ZodError: unrecognized_keys` in strict/audit mode (`pnpm audit:schemas`), breaking `getMemberOrganizations`. `endDate` reads as `null` on every workspace reachable from this account, so its populated shape is still unobserved.
- `Prefs.invitations` is now `string` instead of `unknown`, which it was typed on the assumption the name implied a list. Trello sends `"members"` on every board reachable from this account, so the value was there all along and unusable without a cast.

### Deprecated

- **`Organization.eligibleForTrial`.** Trello stopped sending it, and `trial.eligible` replaces it. The field stays optional so reading it still compiles, and goes at the next major version.

### Added

- **Request cancellation.** Every endpoint method takes an optional last argument, `{ signal }`, typed by the new `RequestOptions` exported from `trello.js/core`. The signal reaches `fetch` unchanged, and it also cuts short a 429 backoff still counting down instead of letting it run its full 2/4/8 seconds. The argument is optional everywhere, so existing calls are unaffected; `batch.run` is the one exception, since its requests share a single HTTP call.

```ts
const board = await trello.boards.getBoard({ id }, { signal: AbortSignal.timeout(5_000) });
```

- `SendRequestOptions` gained `signal`, so a custom `Client` implementation receives it too.
- **`entities` on the five actions-list endpoints.** `getBoardActions`, `getCardActions`, `getListActions`, `getMemberActions` and `getOrganizationActions` take `entities: boolean`. Trello sends `Action.entities` only when it is asked for, so the field the schema declared was unreachable through this client.
**Read this one if you touch `action.data` anywhere.** `Action` is now a union discriminated on `type`, so `data` carries the shape its type actually sends instead of `Record<string, any>`, and reading a field off it without narrowing on `type` first stops compiling. That is a compile break in a minor release, which is worth naming rather than burying: nothing changes at runtime, every response that parsed before still parses, and the fix is an `if` on `type`. If the deadline is today, `ActionUnknown` is exported and `(action as ActionUnknown).data.text` reads `any` again. It hands back exactly the blindness this release removes, so treat it as a bookmark rather than a fix.

### Changed

- **`Action` is now a union discriminated on `type`.** Twenty-nine action types carry their own `data` shape, so `action.data.text` on a `commentCard` is a `string` rather than `any`, and reading a field that type does not have is a compile error instead of `undefined` at runtime.
- **`Action` is a union discriminated on `type`.** Twenty-nine action types carry their own `data` shape, so `action.data.text` on a `commentCard` is a `string`, and reading a field that type does not have is a compile error instead of `undefined` at runtime.

```ts
const action = await trello.actions.getAction({ id });
Expand All @@ -34,15 +16,15 @@
}
```

This breaks code that reads `action.data.<field>` without narrowing first: `data` no longer has an index signature on the branched types. Narrowing on `type` is the fix, and it is the only one, because the field genuinely is not there on the other twenty-eight.
A branched type's `data` no longer carries an index signature, so narrowing is the fix and the only honest one: on the other twenty-eight types the field genuinely is not there.

Each branch is its own exported model and schema, `ActionCommentCard` with `ActionCommentCardSchema` and so on for all twenty-nine, built over the shared `ActionDataBoard`, `ActionDataCard`, `ActionDataList`, `ActionDataOrganization`, `ActionDataMember`, `ActionDataChecklist`, `ActionDataCustomField`, `ActionDataAttachment` and `ActionDataCheckItem` shapes. The branched types are `addAttachmentToCard`, `addChecklistToCard`, `addMemberToBoard`, `addMemberToCard`, `addToOrganizationBoard`, `commentCard`, `convertToCardFromCheckItem`, `copyCard`, `copyCommentCard`, `createBoard`, `createCard`, `createCustomField`, `createList`, `createOrganization`, `deleteAttachmentFromCard`, `deleteCard`, `makeAdminOfBoard`, `makeNormalMemberOfBoard`, `moveCardFromBoard`, `moveCardToBoard`, `moveListFromBoard`, `moveListToBoard`, `removeChecklistFromCard`, `removeMemberFromCard`, `updateBoard`, `updateCard`, `updateCheckItemStateOnCard`, `updateList` and `updateOrganization`.
Each branch is an exported model and schema of its own, `ActionCommentCard` with `ActionCommentCardSchema` and so on for all twenty-nine, built over the shared `ActionDataBoard`, `ActionDataCard`, `ActionDataList`, `ActionDataOrganization`, `ActionDataMember`, `ActionDataChecklist`, `ActionDataCustomField`, `ActionDataAttachment` and `ActionDataCheckItem` shapes. The branched types are `addAttachmentToCard`, `addChecklistToCard`, `addMemberToBoard`, `addMemberToCard`, `addToOrganizationBoard`, `commentCard`, `convertToCardFromCheckItem`, `copyCard`, `copyCommentCard`, `createBoard`, `createCard`, `createCustomField`, `createList`, `createOrganization`, `deleteAttachmentFromCard`, `deleteCard`, `makeAdminOfBoard`, `makeNormalMemberOfBoard`, `moveCardFromBoard`, `moveCardToBoard`, `moveListFromBoard`, `moveListToBoard`, `removeChecklistFromCard`, `removeMemberFromCard`, `updateBoard`, `updateCard`, `updateCheckItemStateOnCard`, `updateList` and `updateOrganization`.

The union ends in an open branch, `ActionUnknown`, whose `data` stays `Record<string, any>`. An action type Trello adds tomorrow still parses, and an account whose history holds a type this list does not name still reads without a `ZodError`. The cost is that the schema cannot itself report drift inside a branched type, so a live test does that instead: it builds a workspace that produces most of the twenty-nine types, reads the account history as well, parses every action against its own branch, and prints the types it saw that no branch covers.
The union ends in an open branch, `ActionUnknown`, whose `data` stays `Record<string, any>`. An action type Trello ships next month parses, and so does an account whose history holds one of the many types this list does not name. The cost is that the schema can no longer report drift inside a branched type, so a live suite does that instead: it builds a workspace that produces most of the twenty-nine, reads the account history as well, parses every action against its own branch, and prints the types no branch covers without failing on them.

The shapes are observations, not readings of the spec, which documents `data` as a free-form object. A key present in every sample of its type is required and everything else is optional, on 249 actions from a purpose-built fixture and from this account's history. Adding or removing a label is not its own type, for one: it arrives as `updateCard` carrying `old.idLabels`.
The shapes are observations, not readings of the spec, which documents `data` as a free-form object. A key present in every sample of its type is required and everything else optional, across 249 actions from a purpose-built fixture and from account history. Adding or removing a label is not its own action type, for one: it arrives as `updateCard` carrying `old.idLabels`.

- **Fifteen fields that were typed `unknown`, `unknown[]` or `Record<string, any>` now carry the shape the live API sends.** The shapes are observations rather than readings of the spec: only fields the live suites could populate were typed. Everything still unobservable from this account (`Organization.powerUps`, `Card.customFieldItems`, `Token.webhooks`, the enterprise and licence fields) is left exactly as it was.
- **Fifteen fields that were typed `unknown`, `unknown[]` or `Record<string, any>` now carry the shape the live API sends.** These are observations rather than readings of the spec: only fields the live suites could populate were typed. Everything still unobservable from this account (`Organization.powerUps`, `Card.customFieldItems`, `Token.webhooks`, the enterprise and licence fields) is left exactly as it was.

| Model | Field | Was | Now |
| --- | --- | --- | --- |
Expand All @@ -62,6 +44,29 @@

Nothing at the top level became required, because Trello's `fields` parameter prunes it and nothing but `id` survives `?fields=name`. Nested properties are not pruned, so ones present in every sample are typed as required, `Action.entities[].type` and `Organization.credits[].applied` among them. That is the one risk here: should Trello stop sending one, the response raises `ZodError` instead of parsing without it.

### Added

- **Request cancellation.** Every endpoint method takes an optional last argument, `{ signal }`, typed by the new `RequestOptions` exported from `trello.js/core`. The signal reaches `fetch` unchanged, and it also cuts short a 429 backoff still counting down instead of letting it run its full 2/4/8 seconds. The argument is optional everywhere, so existing calls are unaffected; `batch.run` is the one exception, since its requests share a single HTTP call.

```ts
const board = await trello.boards.getBoard({ id }, { signal: AbortSignal.timeout(5_000) });
```

- `SendRequestOptions` gained `signal`, so a custom `Client` implementation receives it too.
- **`entities` on the five actions-list endpoints.** `getBoardActions`, `getCardActions`, `getListActions`, `getMemberActions` and `getOrganizationActions` take `entities: boolean`. Trello sends `Action.entities` only when it is asked for, so the field the schema declared was unreachable through this client.

### Fixed

- **`fields` on six endpoints asked for a whole object instead of a list of field names.** `getBoardActions`, `getCardActions`, `getListActions`, `getMemberActions` and `getOrganizationActions` typed it as `Action`; `getBoardLabels` typed it as `Label`. Trello's spec is the source: it points those parameters at the object schema rather than at the matching `ActionFields` and `LabelFields` enums. All six now take the shape the other thirty-five `fields` parameters in this client already had, `string | string[]` widened with the documented names for autocomplete, so `fields: 'id,name'` and `fields: ['id', 'name']` both work. Code that passed an `Action` or a `Label` object stops compiling, which is the point: the request it produced was never one the API could answer.
- **The documented values were missing from the suggestions on every parameter that lists them.** Eighty schemas paired an `openEnum` with a plain `z.string()` branch in the same union, and one model did the same. The branch accepted nothing the open enum did not already accept, and in the type a bare `string` swallowed the literal names next to it, so an editor offered nothing for `fields: '…'`, `filter: '…'` or `pos: '…'` — only the array form still suggested anything. The branch is gone. Any string is still accepted, which is the whole point of an open enum, and nothing that compiled before stops compiling.
- `Organization` gained `trial`, the object Trello now returns on every workspace: `{ eligible: boolean, endDate: Date | null }`. It was silently stripped in normal mode and raised `ZodError: unrecognized_keys` in strict/audit mode (`pnpm audit:schemas`), breaking `getMemberOrganizations`. `endDate` reads as `null` on every workspace reachable from this account, so its populated shape is still unobserved.
- `Prefs.invitations` is now `string` rather than `unknown`, which it was typed on the assumption the name implied a list. Trello sends `"members"` on every board reachable from this account, so the value was there all along and unusable without a cast.
- **The published API reference did not build.** Every parameter and model type was expanded inline on the endpoint pages instead of linked, which put `docs/api` at 22 MB and pushed VitePress past an 8 GB heap. Types are references now, and `typedoc-plugin-zod` fills the pages they point at with the fields and descriptions the schemas already carried. The reference is 9.0 MB and builds under 2.4 GB. The fix behind it is a local patch to typedoc, which drops the written name of a type alias whenever that alias resolves through a conditional type.

### Deprecated

- **`Organization.eligibleForTrial`.** Trello stopped sending it, and `trial.eligible` replaces it. The field stays optional so reading it still compiles, and goes at the next major version.

## v2.2.0 (2026-08-20)

**Read this one if you `switch` exhaustively over a value this client returns.** The enum-shaped types are now open, `Color` and `CardAging` among them, so a `switch` with no `default` branch stops type-checking. That ships in a minor release on purpose: the values belong to Trello, not to this client. The `_light` / `_dark` label shades reached the live API long before the spec named them and broke `getBoardCards` for every consumer until v2.1.6 added the 30 values by hand, so a closed set was never a promise a wrapper could keep.
Expand Down
5 changes: 3 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
"imports": {
"#/*": "./src/*"
},
"version": "2.2.0",
"version": "2.3.0",
"description": "Type-safe Trello REST API client for TypeScript and JavaScript. ESM-only, tree-shakable, runtime-validated by Zod 4. Full coverage of boards, cards, lists, checklists, members, webhooks, organizations. Atlassian Trello SDK for Node.js 22+ and modern browsers.",
"author": "Vladislav Tupikin <vladislav.tupikin@icloud.com>",
"license": "MIT",
Expand Down Expand Up @@ -187,8 +187,9 @@
"sitemap": "^9.0.1",
"tsc-alias": "^1.9.2",
"tsx": "^4.23.12",
"typedoc": "^0.28.20",
"typedoc": "0.28.20",
"typedoc-plugin-markdown": "^4.12.0",
"typedoc-plugin-zod": "^1.4.3",
"typedoc-vitepress-theme": "^1.1.3",
"typescript": "^6.0.3",
"typescript-eslint": "^8.67.0",
Expand Down
24 changes: 24 additions & 0 deletions patches/typedoc@0.28.20.patch
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
diff --git a/dist/index.js b/dist/index.js
index daa82424664dcf1236746613e27a3e79abf96bdd..ff2c2e8cdc21f1c31a8299c315d700077e78f9fb 100644
--- a/dist/index.js
+++ b/dist/index.js
@@ -5371,6 +5371,19 @@ function convertType(context, typeOrNode, maybeNode) {
}
return requestBugReport(context, typeOrNode);
}
+ if (maybeNode && ts13.isTypeReferenceNode(maybeNode) && ts13.isIdentifier(maybeNode.typeName)) {
+ try {
+ const writtenSymbol = context.expectSymbolAtLocation(maybeNode.typeName);
+ const writtenName = maybeNode.typeName.text;
+ if (writtenSymbol && !context.shouldInline(writtenSymbol, writtenName)) {
+ ++typeConversionDepth;
+ const written = referenceConverter.convert(context, maybeNode);
+ --typeConversionDepth;
+ return written;
+ }
+ } catch {
+ }
+ }
if (typeOrNode.isUnion() && typeOrNode.origin && !typeOrNode.aliasSymbol) {
return convertType(context, typeOrNode.origin);
}
33 changes: 24 additions & 9 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions pnpm-workspace.yaml
Original file line number Diff line number Diff line change
@@ -1,2 +1,4 @@
allowBuilds:
esbuild: true
patchedDependencies:
typedoc@0.28.20: patches/typedoc@0.28.20.patch
2 changes: 1 addition & 1 deletion src/models/tokenPermission.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { z } from 'zod';
import { apiObject, openEnum } from '#/core';

export const TokenPermissionSchema = apiObject({
idModel: z.union([z.string(), openEnum(['*'])]).optional(),
idModel: openEnum(['*']).optional(),
modelType: openEnum(['Board', 'Member', 'Organization', 'Enterprise']).optional(),
read: z.boolean().optional(),
write: z.boolean().optional(),
Expand Down
2 changes: 1 addition & 1 deletion src/parameters/createBoardList.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ export const CreateBoardListSchema = z.object({
/** The name of the list to be created. 1 to 16384 characters long. */
name: z.string(),
/** Determines the position of the list. Valid values: `top`, `bottom`, or a positive number. */
pos: z.union([z.string(), z.number(), openEnum(['top', 'bottom'])]).optional(),
pos: z.union([z.number(), openEnum(['top', 'bottom'])]).optional(),
/** The ID of the board */
id: z.string(),
});
Expand Down
2 changes: 1 addition & 1 deletion src/parameters/createCardChecklist.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ export const CreateCardChecklistSchema = z.object({
/** The ID of a source checklist to copy into the new one */
idChecklistSource: z.string().optional(),
/** The position of the checklist on the card. One of: `top`, `bottom`, or a positive number. */
pos: z.union([z.string(), z.number(), openEnum(['top', 'bottom'])]).optional(),
pos: z.union([z.number(), openEnum(['top', 'bottom'])]).optional(),
});

export type CreateCardChecklist = z.input<typeof CreateCardChecklistSchema>;
Loading
Loading