@tilewise/source-github
v0.1.2
Published
**A GitHub repository's issues as a resource: listed and filtered through GitHub's issue search, read and written through its issues API.**
Readme
🐙 @tilewise/source-github
A GitHub repository's issues as a resource: listed and filtered through GitHub's issue search, read and written through its issues API.
GitHub's issue search is the one list that filters by more than a state, so the resource's queries go there, each filter said as search qualifiers. A read, a create and a change go through the issues API. GitHub serves less than the contract allows, and the metadata says so field by field: a client only ever offers what GitHub answers exactly.
✨ Highlights
- 🔎 Filters as qualifiers:
alljoins by a space andanybyOR(GitHub's advanced search), each value quoted as a qualifier takes it. - 🧹 Simplified as it's built: a condition every issue meets says nothing, and one no issue can meet asks GitHub nothing at all.
- 📜 Typed from GitHub's own spec: every route's request and answer
through
@octokit/types. - 🏢 GitHub Enterprise: one
baseUrl, and the API version on every request.
🔄 Where each operation goes
flowchart LR
query["POST /issues/query"] --> clause["filters.ts<br/>a qualifier per condition"]
clause --> text["search-text.ts<br/>repo:… is:issue …"]
text --> search[("GET /search/issues")]
read["GET · POST · PATCH<br/>/issues/:number"] --> api[("issues API")]
classDef tilewise fill:#0F766E,stroke:#115E59,color:#fff
classDef gh fill:#24292F,stroke:#57606A,color:#fff
classDef route fill:#6366F1,stroke:#4338CA,color:#fff
class query,read route
class clause,text tilewise
class search,api gh🚀 Quick start
import { router, serve } from '@tilewise/source';
import { githubIssues } from '@tilewise/source-github';
const { definition, source } = githubIssues({
repo: 'tilewise/app',
token: process.env.GITHUB_TOKEN, // reads issues; writes them for create and update
});
const route = router([serve(definition, source)]);On GitHub Enterprise, give its API:
githubIssues({
repo: 'team/app',
token,
baseUrl: 'https://github.example.com/api/v3',
});🔎 A filter becomes a search
The query a client sends:
// Assigned to ada, or to no one
const adaOrNobody = {
any: [
{ field: 'assignees', op: 'in', value: ['ada'] },
{ field: 'assignees', op: 'empty' },
],
};
await issues.query({
filter: {
all: [
{ field: 'state', op: 'equals', value: 'open' },
{ field: 'labels', op: 'in', value: ['bug', 'good first issue'] },
adaOrNobody,
{ field: 'comments', op: 'gte', value: 2 },
{ field: 'createdAt', op: 'inLast', value: 7 },
{ field: 'author', op: 'notIn', value: ['bot'] },
],
},
sort: [{ field: 'updatedAt', dir: 'asc' }],
page: { skip: 20, take: 10 },
});…and what GitHub is asked (shown unencoded):
GET /search/issues
?q=repo:tilewise/app is:issue is:open label:"bug","good first issue" (assignee:ada OR no:assignee) comments:>=2 created:>2026-10-07T12:00:00Z -author:bot
&advanced_search=true&sort=updated&order=asc&page=3&per_page=10🧮 What each field filters by
Each field's operators are those its search qualifiers say exactly:
| Field | Filters | Qualifier |
| ------------------------ | ------------------------------------------ | --------------------- |
| state | is, is not, in, none of, empty | is:open |
| locked | is, in, none of, empty | is:locked |
| labels | has any of, has none of, empty | label:"a","b" |
| assignees | has any of, has none of, empty | assignee:ada |
| author, milestone | is, is not, in, none of; a milestone empty | author:ada |
| comments, reactions | is, is not, in, none of, the ranges | comments:>3 |
| createdAt, updatedAt | the ranges, in the last, not in the last | created:>2026-10-07 |
| closedAt | the ranges, in the last | closed:<=2026-10-01 |
A query sorts by createdAt or updatedAt, one at a time. A page is a whole
page of at most 100, and a search's first 1000 issues are all it answers, so
it counts no more.
[!NOTE] GitHub matches a login and a label whatever their case, where the contract's
isdoesn't; a value picked from the rows matches either way.
✍️ Writes
| Field | Create | Update |
| ----------- | -------- | -------- |
| title | required | required |
| body | ✅ | ✅ |
| state | | required |
| labels | ✅ | ✅ |
| assignees | ✅ | ✅ |
const issue = await issues.create({
values: { title: 'Crash on start', labels: ['bug'] },
});
await issues.update(issue.number, {
values: { title: 'Crash on start', state: 'closed' },
});A value GitHub refuses (its 422) answers 400 invalid with GitHub's own
words, and a missing issue (404, or 410 when deleted) is a 404.
⚙️ Options
| Option | Default | What |
| ---------- | ------------------------ | -------------------------------------------- |
| repo | | 'owner/name' |
| token | none | reads the issues; writes them for the writes |
| resource | 'issues' | the resource's name, its path under /api/ |
| baseUrl | https://api.github.com | GitHub Enterprise's API |
| fetch | the global one | the fetch Octokit calls, for a test's double |
🚫 What it leaves out
[!WARNING]
- Search: GitHub matches whole words, and the contract's search is any piece of text.
- Sorting by comments or reactions: GitHub breaks their ties in its own order, not by number.
- Deleting an issue, which its REST API doesn't do.
- Pull requests, which the issues API answers too: they aren't rows.
🗂️ Layout
src/
├── index.ts the package's exports
├── types/
│ ├── issue.ts an issue and a search's page, from Octokit's types
│ ├── request.ts a query's part in GitHub's words
│ └── body.ts a write's body
├── client/
│ └── client.ts Octokit, and GitHub's errors as ours
├── search/
│ ├── clause.ts a filter as qualifiers
│ └── search-text.ts a clause as the search's text
└── issues/
├── issues.ts githubIssues
├── definition.ts an issue's fields
├── row.ts an issue as our row
├── filters.ts each field's operators
└── issues.test.ts the adapter over a fake fetch🔑 Key files
issues/issues.ts: the source itself. Its query goes to the search with the repository's scope, its read, create and update to the issues API, and its metadata says exactly how far GitHub goes.issues/filters.ts: each field's operators as the qualifier that says it exactly: a value named (author:ada), compared (comments:>3), or an instant to the second.search/clause.ts: a filter as a tree of qualifiers, simplified as it's joined, sotrueandfalseconditions vanish or short-circuit before GitHub is asked.search/search-text.ts: the clause as the search's text, within GitHub's limits of five operators and 256 characters.client/client.ts: Octokit with the token, the base URL and the API version, and GitHub's answers as ours: a missing issue is none, a refused value400 invalid.
🧱 Built on
| Library | Used for | | --------------------------------------------------------------------- | ------------------------------------------------------------ | | @octokit/core | each request: its token, its base URL, the API version | | @octokit/types | each route's request and answer, from GitHub's OpenAPI spec | | @octokit/request-error | what a refused request throws, its status and GitHub's words |
