Skip to content

Commit

Permalink
Merge pull request #27 from nathonius/feat/25/dedicated-endpoints
Browse files Browse the repository at this point in the history
✨ feat: #25 Use dedicated endpoints for listing issues and PRs
  • Loading branch information
nathonius authored Feb 9, 2024
2 parents 3023f21 + d203b3c commit 6d4570e
Show file tree
Hide file tree
Showing 20 changed files with 428 additions and 100 deletions.
12 changes: 8 additions & 4 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,12 +19,16 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

<!-- ## Unreleased -->

## [0.2.0] - 2024-02-09

### Added

- Added columns for issue results
- Added support for simpler queries using dedicated endpoints for issues and pull requests

## [0.1.0] - 2024-02-01

Initial release.

<!-- [2.0.0]: https://github.com/OfficerHalf/obsidian-jira-cloud/compare/1.2.0...2.0.0
[1.2.0]: https://github.com/OfficerHalf/obsidian-jira-cloud/compare/1.1.0...1.2.0
[1.1.0]: https://github.com/OfficerHalf/obsidian-jira-cloud/compare/1.0.0...1.1.0 -->

[0.2.0]: https://github.com/nathonius/obsidian-github-link/compare/0.1.0...0.2.0
[0.1.0]: https://github.com/nathonius/obsidian-github-link/releases/tag/0.1.0
62 changes: 56 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,12 +31,62 @@ This produces a table of results that refreshes upon opening the note.

The codeblock must be valid YAML. The following options are currently supported:

| Option | Values | Description |
| ------------ | ---------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `outputType` | `table` | Required. Only table is currently supported. |
| `queryType` | `pull-request` | Required. Only pull requests are currently supported. |
| `query` | A valid GitHub search query. | Required. See the [GitHub docs](https://docs.github.com/en/search-github/searching-on-github/searching-issues-and-pull-requests) for more information. |
| `columns` | See list below. | Required. Should be an array of values. |
| Option | Values | Description |
| ------------ | --------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `outputType` | `table` | Required. Only table is currently supported. |
| `queryType` | `issue`, `pull-request` | Required. |
| `columns` | See Supported Columns list below. | Required. Should be an array of values. |
| `query` | A valid GitHub search query. | Required for custom queries, overrides other params. See the [GitHub docs](https://docs.github.com/en/search-github/searching-on-github/searching-issues-and-pull-requests) for more information. |

Other params will depend on the type of query. If the `query` parameter is provided, these other parameters will be ignored. Each section below describes a set of parameters for a certain query profile.

#### List My Assigned Issues

**Note:** this requires a valid token.

| Option | Values | Description |
| ----------- | ---------------------------------------------------------------- | ------------------------------------------------------------------------------------------------ |
| `org` | Name of a user or organization. | Will use token for default account if not given. |
| `filter` | `assigned`, `created`, `mentioned`, `subscribed`, `repos`, `all` | What type of issues to return. `all` and `repos` return all issues, regardless of participation. |
| `state` | `open`, `closed`, `all` | |
| `labels` | Label or array of issue labels. | Only matching issues will be included. |
| `sort` | `created`, `updated`, `comments` | |
| `direction` | `desc`, `asc` | |
| `since` | `YYYY-MM-DDTHH:MM:SSZ` | Minimum update date, in full ISO format. |
| `per_page` | Integer. | Number of items to return per-page. |
| `page` | Integer. | Page of results to use. |

#### List Issues For Repo

| Option | Values | Description |
| ----------- | ----------------------------------------------- | ---------------------------------------- |
| `org` | Name of a user or organization. | Required. |
| `repo` | Repository name. | Required. |
| `milestone` | Issue milestone, milestone number, `*`, `none`. | |
| `state` | `open`, `closed`, `all` | |
| `assignee` | Name of a user, `*`, `none`. | |
| `creator` | Name of a user. | |
| `mentioned` | Name of a user. | |
| `labels` | Label or array of issue labels. | Only matching issues will be included. |
| `sort` | `created`, `updated`, `comments` | |
| `direction` | `desc`, `asc` | |
| `since` | `YYYY-MM-DDTHH:MM:SSZ` | Minimum update date, in full ISO format. |
| `per_page` | Integer. | Number of items to return per-page. |
| `page` | Integer. | Page of results to use. |

#### List Pull Requests For Repo

| Option | Values | Description |
| ----------- | -------------------------------------------------- | -------------------------------------------- |
| `org` | Name of a user or organization. | Required. |
| `repo` | Repository name. | Required. |
| `state` | `open`, `closed`, `all` | |
| `head` | `user:ref-name` or `org:ref-name`. | Filter to head user or org with branch name. |
| `base` | `branch-name-base` | Filter by base branch name. |
| `sort` | `created`, `updated`, `popularity`, `long-running` | |
| `direction` | `desc`, `asc` | |
| `per_page` | Integer. | Number of items to return per-page. |
| `page` | Integer. | Page of results to use. |

#### Supported Columns

Expand Down
2 changes: 1 addition & 1 deletion manifest.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"id": "github-link",
"name": "GitHub Link",
"version": "0.1.0",
"version": "0.2.0",
"minAppVersion": "0.15.0",
"description": "Enrich your notes with content from GitHub REST API",
"author": "Nathonius",
Expand Down
4 changes: 2 additions & 2 deletions package-lock.json

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

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "obsidian-github-link",
"version": "0.1.0",
"version": "0.2.0",
"description": "An Obsidian plugin enriching notes with data from GitHub's REST API",
"main": "main.js",
"scripts": {
Expand Down
70 changes: 60 additions & 10 deletions src/github/api.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,32 @@
import type { CodeResponse, IssueResponse, PullResponse, SearchIssueResponse, SearchRepoResponse } from "./response";
import type {
CodeResponse,
IssueListParams,
IssueListResponse,
IssueResponse,
IssueSearchResponse,
PullListParams,
PullListResponse,
PullResponse,
RepoSearchResponse,
} from "./response";
import type { RequestUrlParam, RequestUrlResponse } from "obsidian";

import type { RequestUrlParam } from "obsidian";
import { RequestError } from "src/util";
import { requestUrl } from "obsidian";

const debug = true;
const debug = false;

const baseApi = "https://api.github.com";

export async function githubRequest(config: RequestUrlParam, token?: string) {
export function addParams(href: string, params: Record<string, unknown>): string {
const url = new URL(href);
for (const [key, value] of Object.entries(params)) {
url.searchParams.set(key, `${value}`);
}
return url.toString();
}

export async function githubRequest(config: RequestUrlParam, token?: string): Promise<RequestUrlResponse> {
if (!config.headers) {
config.headers = {};
}
Expand All @@ -26,16 +45,33 @@ export async function githubRequest(config: RequestUrlParam, token?: string) {
}
return response;
} catch (err) {
console.error(err);
throw err;
throw new RequestError(err as Error);
}
}

async function getIssue(org: string, repo: string, issue: number, token?: string): Promise<IssueResponse> {
const result = await githubRequest({ url: `${baseApi}/repos/${org}/${repo}/issues/${issue}` }, token);

return result.json as IssueResponse;
}

async function listIssuesForToken(params: IssueListParams = {}, token: string): Promise<IssueListResponse> {
const url = addParams(`${baseApi}/issues`, params as Record<string, unknown>);
const result = await githubRequest({ url }, token);
return result.json as IssueListResponse;
}

async function listIssuesForRepo(
org: string,
repo: string,
params: IssueListParams = {},
token?: string,
): Promise<IssueListResponse> {
const url = addParams(`${baseApi}/repos/${org}/${repo}/issues`, params as Record<string, unknown>);
const result = await githubRequest({ url }, token);
return result.json as IssueListResponse;
}

async function getPullRequest(org: string, repo: string, pr: number, token?: string): Promise<PullResponse> {
const result = await githubRequest(
{
Expand All @@ -46,6 +82,17 @@ async function getPullRequest(org: string, repo: string, pr: number, token?: str
return result.json as PullResponse;
}

async function listPullRequestsForRepo(
org: string,
repo: string,
params: PullListParams = {},
token?: string,
): Promise<PullListResponse> {
const url = addParams(`${baseApi}/repos/${org}/${repo}/pulls`, params as Record<string, unknown>);
const result = await githubRequest({ url }, token);
return result.json as PullListResponse;
}

async function getCode(org: string, repo: string, path: string, branch: string, token?: string): Promise<CodeResponse> {
const result = await githubRequest(
{
Expand All @@ -56,19 +103,22 @@ async function getCode(org: string, repo: string, path: string, branch: string,
return result.json as CodeResponse;
}

async function searchRepos(query: string, token?: string): Promise<SearchRepoResponse> {
async function searchRepos(query: string, token?: string): Promise<RepoSearchResponse> {
const result = await githubRequest({ url: `${baseApi}/search/repositories?q=${encodeURIComponent(query)}` }, token);
return result.json as SearchRepoResponse;
return result.json as RepoSearchResponse;
}

async function searchIssues(query: string, token?: string): Promise<SearchIssueResponse> {
async function searchIssues(query: string, token?: string): Promise<IssueSearchResponse> {
const result = await githubRequest({ url: `${baseApi}/search/issues?q=${encodeURIComponent(query)}` }, token);
return result.json as SearchIssueResponse;
return result.json as IssueSearchResponse;
}

export const api = {
getIssue,
listIssuesForToken,
listIssuesForRepo,
getPullRequest,
listPullRequestsForRepo,
getCode,
searchIssues,
searchRepos,
Expand Down
67 changes: 57 additions & 10 deletions src/github/cache.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,13 @@
import type { IssueResponse, PullResponse, SearchIssueResponse, SearchRepoResponse } from "./response";
import type {
IssueListParams,
IssueListResponse,
IssueResponse,
IssueSearchResponse,
PullListParams,
PullListResponse,
PullResponse,
RepoSearchResponse,
} from "./response";

class CacheEntry<T> {
constructor(
Expand All @@ -15,17 +24,20 @@ class CacheEntry<T> {
}

class QueryCache {
public readonly issueCache: Record<string, CacheEntry<SearchIssueResponse>> = {};
public readonly repoCache: Record<string, CacheEntry<SearchRepoResponse>> = {};
public readonly issueCache: Record<string, CacheEntry<IssueSearchResponse>> = {};
public readonly repoCache: Record<string, CacheEntry<RepoSearchResponse>> = {};
}

class RepoCache {
public readonly issueCache: Record<number, CacheEntry<IssueResponse>> = {};
public readonly issueListForRepoCache: Record<string, CacheEntry<IssueListResponse>> = {};
public readonly pullCache: Record<string, CacheEntry<PullResponse>> = {};
public readonly pullListForRepoCache: Record<string, CacheEntry<PullListResponse>> = {};
}

class OrgCache {
public readonly repos: Record<string, RepoCache> = {};
public readonly issueList: Record<string, CacheEntry<IssueListResponse>> = {};
}

export class Cache {
Expand Down Expand Up @@ -58,11 +70,41 @@ export class Cache {
}
}

getIssueList(org: string, params: IssueListParams): IssueListResponse | null {
const orgCache = this.getOrgCache(org);
return this.getCacheValue(orgCache.issueList[JSON.stringify(params)] ?? null);
}

setIssueList(org: string, params: IssueListParams, value: IssueListResponse): void {
const orgCache = this.getOrgCache(org);
orgCache.issueList[JSON.stringify(params)] = new CacheEntry(value);
}

getIssueListForRepo(org: string, repo: string, params: IssueListParams): IssueListResponse | null {
const repoCache = this.getRepoCache(org, repo);
return this.getCacheValue(repoCache.issueListForRepoCache[JSON.stringify(params)] ?? null);
}

setIssueListForRepo(org: string, repo: string, params: IssueListParams, value: IssueListResponse): void {
const repoCache = this.getRepoCache(org, repo);
repoCache.issueListForRepoCache[JSON.stringify(params)] = new CacheEntry(value);
}

getPullRequest(org: string, repo: string, pullRequest: number): PullResponse | null {
const repoCache = this.getRepoCache(org, repo);
return this.getCacheValue(repoCache.pullCache[pullRequest] ?? null);
}

getPullListForRepo(org: string, repo: string, params: PullListParams): PullListResponse | null {
const repoCache = this.getRepoCache(org, repo);
return this.getCacheValue(repoCache.pullListForRepoCache[JSON.stringify(params)] ?? null);
}

setPullListForRepo(org: string, repo: string, params: PullListParams, value: PullListResponse): void {
const repoCache = this.getRepoCache(org, repo);
repoCache.pullListForRepoCache[JSON.stringify(params)] = new CacheEntry(value);
}

setPullRequest(org: string, repo: string, pullRequest: PullResponse): void {
const pullCache = this.getRepoCache(org, repo).pullCache;
const existingCache = pullCache[pullRequest.id];
Expand All @@ -75,27 +117,32 @@ export class Cache {
}
}

getIssueQuery(query: string): SearchIssueResponse | null {
getIssueQuery(query: string): IssueSearchResponse | null {
return this.getCacheValue(this.queries.issueCache[query] ?? null);
}

setIssueQuery(query: string, result: SearchIssueResponse): void {
this.queries.issueCache[query] = new CacheEntry<SearchIssueResponse>(result);
setIssueQuery(query: string, result: IssueSearchResponse): void {
this.queries.issueCache[query] = new CacheEntry<IssueSearchResponse>(result);
}

getRepoQuery(query: string): SearchRepoResponse | null {
getRepoQuery(query: string): RepoSearchResponse | null {
return this.getCacheValue(this.queries.repoCache[query] ?? null);
}

setRepoQuery(query: string, result: SearchRepoResponse): void {
this.queries.repoCache[query] = new CacheEntry<SearchRepoResponse>(result);
setRepoQuery(query: string, result: RepoSearchResponse): void {
this.queries.repoCache[query] = new CacheEntry<RepoSearchResponse>(result);
}

private getRepoCache(org: string, repo: string) {
private getOrgCache(org: string): OrgCache {
let orgCache = this.orgs[org];
if (!orgCache) {
orgCache = this.orgs[org] = new OrgCache();
}
return orgCache;
}

private getRepoCache(org: string, repo: string) {
const orgCache = this.getOrgCache(org);
let repoCache = orgCache.repos[repo];
if (!repoCache) {
repoCache = orgCache.repos[repo] = new RepoCache();
Expand Down
Loading

0 comments on commit 6d4570e

Please sign in to comment.