Skip to content

feat(sections,projects): model description on read and write types#620

Merged
lmjabreu merged 6 commits into
mainfrom
lmjabreu/section-project-descriptions
Jun 11, 2026
Merged

feat(sections,projects): model description on read and write types#620
lmjabreu merged 6 commits into
mainfrom
lmjabreu/section-project-descriptions

Conversation

@lmjabreu

Copy link
Copy Markdown
Contributor

What

Models project and section description (26Q2 Project Essentials) on the SDK's read and write types. The backend shipped both (sections landed 2026-06-10 via Todoist#28403), but the SDK didn't model the section side and omitted description from the write args — so the CLI and MCP currently need casts and can't read section descriptions at all. This unblocks them.

Changes

Sections

  • SectionBaseSchema gains description: z.string().nullable() — the backend column is str | None (null when unset), so REST and sync section reads now carry it.
  • WebhookSectionSchema overrides description to tolerate omission (.nullish()null), mirroring the existing updatedAt handling, so leaner webhook payloads still parse.
  • AddSectionArgs gains optional description; UpdateSectionArgs makes name optional and adds description?: string | null (null clears, matching the backend's NULL_CLEARS), enabling description-only updates.

Projects

  • AddProjectArgs / UpdateProjectArgs gain optional description (empty string clears on update, matching NULL_KEEPS_UNCHANGED). The project read schema already carried description, so no read change there.

Clear-semantics note (intentional asymmetry, from the backend)

  • Project update: omit description to keep, "" to clear.
  • Section update: omit to keep, null to clear.

These mirror the backend's NULL_KEEPS_UNCHANGED (projects) vs NULL_CLEARS (sections) contracts and are documented in the arg JSDoc.

Testing

  • Read parsing for description (string and null).
  • Webhook tolerance: present description surfaces; missing key normalises to null.
  • addSection / updateSection and addProject / updateProject forward description in the request body, including the section null-clear and project ""-clear paths, plus a description-only section update (no name).
  • Full suite green (633 tests), lint/format, build all pass. (The 4 pre-existing ts-compile-check errors in apps/ui-extensions tests are unrelated to this change and present on main.)

Downstream

Unblocks the draft consumer PRs: CLI todoist-cli#395/#396 and MCP todoist-mcp#500/#501. Draft until reviewed; once released, bump the pins there to drop the temporary casts.

🤖 Generated with Claude Code

Backend shipped project and section descriptions (26Q2 Project Essentials),
but the SDK didn't model the section side and omitted `description` from the
write args. This adds it so consumers (CLI, MCP) can read and write it without
casts.

Sections:
- `SectionBaseSchema` gains `description: z.string().nullable()` (the column is
  nullable, null when unset), so REST and sync section reads carry it.
- `WebhookSectionSchema` overrides `description` to tolerate omission, mirroring
  the existing `updatedAt` handling — leaner webhook payloads still parse.
- `AddSectionArgs` gains optional `description`; `UpdateSectionArgs` makes `name`
  optional and adds `description?: string | null` (null clears, per the backend's
  NULL_CLEARS semantics), enabling description-only updates.

Projects:
- `AddProjectArgs` / `UpdateProjectArgs` gain optional `description` (empty string
  clears on update, per NULL_KEEPS_UNCHANGED). The project read schema already
  carried `description`, so no read change there.

Tests cover read parsing (string + null), webhook tolerance, and that add/update
forward `description` (and the section null-clear / project empty-string-clear
contracts) in the request body.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@lmjabreu

Copy link
Copy Markdown
Contributor Author

@doistbot /review

@doistbot doistbot left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This introduces the description field for both projects and sections across read and write types, properly accommodating the API's update asymmetry and webhook structures.

Few things worth tightening:

  • Wrap UpdateSectionArgs in a RequireAtLeastOne utility to prevent empty update requests now that name is optional.
  • Add a strictness test to ensure non-webhook section reads actively reject payloads missing a description, preventing accidental schema loosening in the future.

Share FeedbackReview Logs

Comment thread src/types/sections/requests.ts Outdated
Comment thread src/types/webhooks/sections.test.ts
Address review feedback on the description support:

- Wrap `UpdateSectionArgs` in a new `RequireAtLeastOne` type utility so that,
  now that `name` is optional, an empty `{}` update no longer type-checks and
  can't send an empty POST body.
- Add a strictness test asserting non-webhook section reads reject a payload
  missing `description`, pinning the contract against accidental loosening (the
  webhook variant keeps its tolerant override).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@lmjabreu

Copy link
Copy Markdown
Contributor Author

@doistbot /review

@doistbot doistbot left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This adds solid support for reading and writing project and section descriptions while correctly navigating the backend's asymmetric clearing contracts.

Few things worth tightening:

  • The RequireAtLeastOne guard still allows effectively empty updates (like { name: undefined }), which can be sealed by applying it to a required base shape or stripping undefined.
  • The webhook test covers the omitted-key branch but misses explicit nulls; adding a test case for this will prevent accidental regressions if the schema ever changes from .nullish() to .optional().

I also included a few optional follow-up notes in the details below.

Optional follow-up note (1)
  • [P3] src/types/sections/requests.ts:44: RequireAtLeastOne is the key safety net here, but the suite doesn't pin it. The repo already uses @ts-expect-error assertions for type contracts; add one for api.updateSection('123', {}) (or {} as UpdateSectionArgs) so the non-empty update guarantee can't silently regress.

Share FeedbackReview Logs

Comment thread src/types/sections/requests.ts
Comment thread src/types/webhooks/sections.test.ts
Round-2 review follow-ups:

- `RequireAtLeastOne` now requires the chosen key to carry a *defined* value, so
  `{ name: undefined }` (which serializes to an empty body) no longer satisfies
  `UpdateSectionArgs`. `null` is preserved where the field allows it (the
  `description` clear).
- Add a `@ts-expect-error` type-contract test pinning that `{}` and
  `{ name: undefined }` are rejected while `{ name }` / `{ description: null }`
  are accepted.
- Add a webhook test for an explicit `description: null` (complementing the
  omitted-key case), so a future `.nullish()` -> `.optional()` change can't slip
  through.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@lmjabreu

Copy link
Copy Markdown
Contributor Author

@doistbot /review

@doistbot doistbot left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This cleanly models the new description field for projects and sections across the SDK's read and write types, effectively handling the backend's asymmetric update contracts and webhook edge cases. No issues were flagged.

Share FeedbackReview Logs

@lmjabreu lmjabreu marked this pull request as ready for review June 10, 2026 22:30

@doistbot doistbot left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This PR cleanly models the new description field for projects and sections across the SDK's read and write interfaces.

Few things worth tightening:

  • Pin the project clear semantics at the type level by adding a @ts-expect-error test for { description: null }.

I also included a few optional follow-up notes in the details below.

Optional follow-up note (1)
  • [P3] src/types/common.ts:17: This introduces a local RequireAtLeastOne, but the repo already depends on type-fest and uses its request-shape helpers in sibling files like types/tasks/requests.ts and types/comments/requests.ts. Keeping a second in-house version of the same abstraction adds maintenance cost and makes the rules harder to remember. You can preserve the current “selected key must be defined” behavior by using type-fest’s RequireAtLeastOne on a required base shape instead (for example, RequireAtLeastOne<{ name: string; description: string | null }> in UpdateSectionArgs).

Share FeedbackReview Logs

Comment thread src/todoist-api.projects.test.ts
Address the remaining review thread: add a `@ts-expect-error` type-contract
test asserting `{ description: null }` does not type-check for projects. Projects
use NULL_KEEPS_UNCHANGED (clear with `""`), so `null` must not be accepted; this
locks the project/section clear asymmetry at the type level and would break if
`UpdateProjectArgs.description` were ever widened to `string | null`.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@lmjabreu

Copy link
Copy Markdown
Contributor Author

@doistbot /review

…Args

Address review follow-up: the repo already depends on type-fest and uses its
request-shape helpers in sibling files (tasks, comments), so drop the in-house
`RequireAtLeastOne` in favour of type-fest's. Applying it to a *required* base
shape (`{ name: string; description: string | null }`) preserves the same
"selected key must be defined" guarantee, so the existing `@ts-expect-error`
contract tests (rejecting `{}` and `{ name: undefined }`) still hold.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@lmjabreu lmjabreu requested a review from scottlovegrove June 11, 2026 05:54
Simplify: the description-only and null-clear update tests shared near-identical
MSW boilerplate. Combine them into a single set-and-clear test using the
`capturedBodies` array pattern already used by the `folderId` test, asserting
both the description set (with no `name` sent) and the null clear. Net -24 lines,
same coverage.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@lmjabreu lmjabreu merged commit f319d96 into main Jun 11, 2026
4 checks passed
@lmjabreu lmjabreu deleted the lmjabreu/section-project-descriptions branch June 11, 2026 10:51
doist-release-bot Bot added a commit that referenced this pull request Jun 11, 2026
## [10.4.0](v10.3.3...v10.4.0) (2026-06-11)

### Features

* **sections,projects:** model description on read and write types ([#620](#620)) ([f319d96](f319d96))
@doist-release-bot

Copy link
Copy Markdown
Contributor

🎉 This PR is included in version 10.4.0 🎉

The release is available on:

Your semantic-release bot 📦🚀

lmjabreu added a commit to Doist/todoist-cli that referenced this pull request Jun 11, 2026
…asts

SDK 10.4.0 ships `description` on AddProjectArgs and UpdateProjectArgs (via
Doist/todoist-sdk-typescript#620), so the temporary type workarounds in
project create/update can go. Removed both casts; the SDK signature now
covers the full payload.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
lmjabreu added a commit to Doist/todoist-cli that referenced this pull request Jun 11, 2026
…lear

SDK 10.4.0 (Doist/todoist-sdk-typescript#620) models section descriptions:
`AddSectionArgs` / `UpdateSectionArgs` carry `description`, `UpdateSectionArgs`
makes `name` optional (RequireAtLeastOne), and the section read schema exposes
`description` — so section JSON output now surfaces it for real.

- Drop the create-section description cast (now typed by the SDK).
- Update-section: empty `--stdin` now clears via `null` (backend NULL_CLEARS),
  not an empty string. The remaining cast bridges RequireAtLeastOne, which a
  dynamically-built partial can't satisfy statically (the NO_CHANGES guard
  guarantees at least one field at runtime).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
lmjabreu added a commit to Doist/todoist-mcp that referenced this pull request Jun 11, 2026
…asts

SDK 10.4.0 (Doist/todoist-sdk-typescript#620) types `description` on
AddProjectArgs / UpdateProjectArgs, so the temporary casts in add-projects and
update-projects can go.

The SDK's `Section` read type now requires `description`, so the shared section
test fixtures (createMockSection helpers and a couple of inline Section literals)
gain a `description: null` default to satisfy the stricter type.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
lmjabreu added a commit to Doist/todoist-mcp that referenced this pull request Jun 11, 2026
…ypes

SDK 10.4.0 (Doist/todoist-sdk-typescript#620) models section descriptions:
the `Section` read type carries `description`, and `UpdateSectionArgs` is now
RequireAtLeastOne with `description: string | null`.

- Drop the widened type on `toSectionSummary` (the SDK `Section` now has
  `description`); it maps the read's `string | null` to the output's optional
  string (Gemini-compatible).
- update-sections keeps a single cast bridging RequireAtLeastOne (a dynamically
  built partial can't satisfy the union statically; at least one field is always
  present). `"remove"`/legacy `null` still clears via `null`.
- Add `description: null` defaults to the shared section test fixtures the
  stricter `Section` type now requires.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants