Skip to content

Fix document list --issue, which never worked - #257

Merged
schpet merged 1 commit into
mainfrom
fix-document-list-issue-filter
Aug 4, 2026
Merged

Fix document list --issue, which never worked#257
schpet merged 1 commit into
mainfrom
fix-document-list-issue-filter

Conversation

@schpet

@schpet schpet commented Aug 4, 2026

Copy link
Copy Markdown
Owner

The bug

document list --issue <ID> has never worked. The filter was built as:

issue: { identifier: { eq: issue.toUpperCase() } }

but IssueFilter has no identifier field — the comparator for the human
identifier is spelled id. Linear rejects the variable during coercion, so every
invocation failed before reaching the resolver:

$ linear document list --issue TC-123
✗ Failed to list documents: Variable "$filter" got invalid value { identifier: { eq: "TC-123" } } at "filter.issue"; Field "identifier" is not defined by type "IssueFilter".

It fails regardless of whether the issue exists or has documents attached. Broken
since the command was introduced, so this is not a regression. --project on the
same command is unaffected.

The fix

issue: { id: { eq: ... } }. IDComparator takes the human identifier directly,
so no identifier parsing is needed.

The filter local was let filter: any = undefined with no-explicit-any
suppressed, which is why the wrong field name compiled. It is now built as a
single annotated DocumentFilter | undefined expression, and the lint suppression
is gone. With the annotation in place deno check flags the original bug directly:

TS2353 [ERROR]: Object literal may only specify known properties, and 'identifier'
does not exist in type 'IssueFilter'.

The filter still stays undefined when neither flag is passed, so an unfiltered
list sends no filter at all.

Test

The earlier filter tests here were removed for rendering relative timestamps
(non-deterministic, and fakeTime hangs against the mock server). The new test
goes through --json instead, which prints raw timestamps, and declares the exact
expected request variables so MockLinearServer only matches the correct filter
shape.

It is verified to actually guard the bug, not just to pass:

  • Restoring the complete pre-fix block makes it fail at runtime with
    No mock response configured for this query, while the other three tests pass.
  • It also fails when run with -- --update, so a careless snapshot regeneration
    cannot silently re-record a broken filter as expected output. (The mock miss
    exits non-zero, and cliffy fails on that regardless of update mode.)

Verification

Every --project/--issue combination was exercised against a local capture
server to confirm what actually goes over the wire:

Case filter sent
no flags (key absent)
--issue TC-123 {"issue":{"id":{"eq":"TC-123"}}}
--issue tc-123 {"issue":{"id":{"eq":"TC-123"}}}
--project p {"project":{"slugId":{"eq":"p"}}}
both both keys

The project/issue keys are set to undefined when their flag is absent and are
dropped during serialization, so no phantom key reaches the API. deno task check,
deno lint, deno fmt --check and the full suite (509 tests) pass.

Known limitation, unchanged by this PR

.toUpperCase() is applied to the identifier, which also uppercases a UUID if one
is passed. IDComparator accepts UUIDs as well as human identifiers, and
uppercased UUIDs were reported to still match, but that was not independently
verified here. The uppercasing predates this change and is kept so --issue tc-123
works; making it conditional would mean adding identifier-vs-UUID parsing, which
felt out of scope for a bug fix.

Follow-ups (not in this PR)

  • Same any-typed-filter pattern in src/commands/label/label-list.ts:61 and
    src/commands/initiative/initiative-list.ts:123.
  • MockLinearServer does not validate variables against the schema, so
    wrong-but-parseable filters can only be caught by pinning variables per test.
  • document list --json prints the whole connection ({nodes, pageInfo}), not a
    bare array — worth a line in the --json help text.

The filter was built as issue: { identifier: { eq: ... } }, but IssueFilter
has no identifier field — the comparator for the human identifier is spelled
id. Linear rejected the variable during coercion, so every invocation of the
flag failed before reaching the resolver, regardless of whether the issue
existed. The flag has been broken since the command was introduced.

Type the filter local as DocumentFilter instead of any, which turns this class
of mistake into a compile error rather than a runtime API rejection; deno check
flags the bad field directly. Building the filter as a single annotated
expression also drops the deno-lint-ignore and keeps it undefined when neither
--project nor --issue is passed, so an unfiltered list still sends no filter.

The previous tests here were removed for rendering relative timestamps, which
are non-deterministic. The regression test instead goes through --json, which
prints raw timestamps, and declares the exact request variables so the mock
only matches the correct filter shape — verified by restoring the pre-fix code
and watching it fail.
@schpet
schpet merged commit fe6c8b0 into main Aug 4, 2026
15 checks passed
@schpet
schpet deleted the fix-document-list-issue-filter branch August 4, 2026 20:38
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant