├── .env.example ├── .github └── workflows │ └── tests.yml ├── .gitignore ├── .prettierignore ├── .prettierrc ├── .vscode └── settings.json ├── LICENSE ├── README.md ├── codegen.yml ├── cover.png ├── docs ├── README.md ├── classes │ └── GithubBlog.md └── enums │ └── Reaction.md ├── e2e ├── getComments.test.ts ├── getLabels.test.ts ├── getPinnedPosts.test.ts ├── getPost.test.ts └── getPosts.test.ts ├── package.json ├── pnpm-lock.yaml ├── src ├── __generated__ │ ├── index.ts │ └── schema.graphql ├── core │ ├── datatype.ts │ └── sdk.ts ├── datatypes │ ├── Author.ts │ ├── Comment.ts │ ├── Label.ts │ ├── Labels.ts │ ├── PageInfo.ts │ ├── Post.ts │ ├── PostReduced.ts │ └── Reactions.ts ├── github-blog.ts ├── index.ts ├── methods │ ├── getComments.ts │ ├── getLabels.ts │ ├── getPinnedPosts.ts │ ├── getPost.ts │ └── getPosts.ts ├── public-types.ts ├── types │ └── index.ts └── utils │ ├── frontmatter.test.ts │ ├── frontmatter.ts │ ├── func.ts │ ├── github-query.ts │ └── pager.ts ├── tsconfig.build.json └── tsconfig.json /.env.example: -------------------------------------------------------------------------------- 1 | GITHUB_TOKEN= 2 | GITHUB_INTROSPECTION_TOKEN= 3 | GITHUB_E2E_TESTS_TOKEN= -------------------------------------------------------------------------------- /.github/workflows/tests.yml: -------------------------------------------------------------------------------- 1 | name: Tests 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | pull_request: 8 | branches: 9 | - master 10 | 11 | jobs: 12 | tests: 13 | runs-on: ubuntu-latest 14 | 15 | strategy: 16 | matrix: 17 | node-version: [20] 18 | 19 | steps: 20 | - name: Checkout 21 | uses: actions/checkout@v4 22 | 23 | - name: Install pnpm 24 | uses: pnpm/action-setup@v4 25 | 26 | - name: Use Node.js ${{ matrix.node-version }} 27 | uses: actions/setup-node@v4 28 | with: 29 | node-version: ${{ matrix.node-version }} 30 | cache: "pnpm" 31 | 32 | - name: Install dependencies 33 | run: pnpm install 34 | 35 | - name: Type check 36 | run: pnpm type-check 37 | 38 | - name: Run tests 39 | run: pnpm test 40 | env: 41 | GITHUB_INTROSPECTION_TOKEN: ${{ secrets.GITHUB_TOKEN }} 42 | GITHUB_E2E_TESTS_TOKEN: ${{ secrets.GITHUB_TOKEN }} 43 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .env 2 | node_modules 3 | tmp 4 | dist 5 | *.log -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | src/__generated__/index.ts -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 100 3 | } 4 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "typescript.tsdk": "node_modules/typescript/lib" 3 | } 4 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Renato Ribeiro 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Github Blog 2 | 3 |

4 | 5 |

6 | 7 |

8 | Turn your github issues into a CMS for your blog. 9 |

10 | 11 | ```sh 12 | npm install @rena.to/github-blog 13 | ``` 14 | 15 | ## API Only 16 | 17 | **This repository is just about the API.** 18 | 19 | Note: 20 | 21 | > If you're looking for something more 'high level', like a full-featured blog application, I'm working on a starter template using Next.js, TypeScript and Tailwindcss. [Follow me on twitter](https://twitter.com/renatorib_) to follow up and receive updates. 22 | 23 | ## Concept 24 | 25 | The main idea is simple: each issue is a blog post entity. 26 | 27 | Taxonomy is managed by **labels** and have `:` structure. Like `type:post`, `tag:javascript`, etc. Labels can be used to filter posts on querying, but is also available on post too. So you can use to add any kind of flags to your post. 28 | 29 | The built-in label keys are: `type`, `state`, `tag`, `flag` and `slug`. 30 | 31 | - Use **type** labels to differentiate _post_ from _article_, for example. 32 | - Use **state** labels to handle _published_ and _draft_. 33 | - Use **tag** labels to add tags to your posts, like _typescript_. 34 | - Use **flag** labels to add any kind of flag to your post, like _outdated_ to mark post as outdated. 35 | - Use **slug** label to define an slug to your post. [Read about slug problem](#slug-problem). 36 | 37 | You can also add any **k:v** labels to your post, like `foo:bar`. 38 | 39 | ## Table of Contents 40 | 41 | - [Getting Started](#getting-started) 42 | - [Repository](#repository) 43 | - [Issue](#issue) 44 | - [Fetch](#fetch) 45 | - [Guide](#guide) 46 | - [Querying](#querying) 47 | - [Searching](#searching) 48 | - [Sorting](#sorting) 49 | - [Pagination](#pagination) 50 | - [Defaults](#defaults) 51 | - [Comments](#comments) 52 | - [Problems](#problems) 53 | - [Slug Problem](#slug-problem) 54 | - [API Reference](#api-reference) 55 | 56 | ## Getting Started 57 | 58 | Let's create your first blog post. 59 | You will need: 1) a repository, 2) an issue with some labels 60 | 61 | #### Repository 62 | 63 | First, you will need to create a repository to publish your posts. 64 | 65 | It can be private, but I recommend you to create a public since it will allow people comment and react to your posts. 66 | Random people will be able to create issues but they can't add labels. So you can control what posts will be shown using some label like `type:post` for example. It will prevent random people to post on your blog. Also, by core github-blog only fetches by opened issues. You can close any random issue opened by others to keep posts organized. 67 | 68 | ![image](https://user-images.githubusercontent.com/3277185/115134566-a8039180-9fe7-11eb-9e74-eb23b488e860.png) 69 | 70 | #### Issue 71 | 72 | Create a issue with your content and add the labels `state:published`, `type:post`. 73 | Also add an label to your slug like `slug:my-first-post`. 74 | 75 | > Tip: Your issue content can have frontmatter data 76 | 77 | ![image](https://user-images.githubusercontent.com/3277185/115800402-ec5cac00-a3b0-11eb-9523-49dbaa341354.png) 78 | 79 | #### Fetch 80 | 81 | Here comes github-blog. First install 82 | 83 | ```sh 84 | npm install @rena.to/github-blog 85 | ``` 86 | 87 | Now create a new blog instance passing your repo and your github token. 88 | [Create your token here ⟶](https://github.com/settings/tokens). 89 | 90 | ```ts 91 | import { value GithubBlog } from "@rena.to/github-blog"; 92 | 93 | const blog = new GithubBlog({ 94 | repo: "/", // e.g.: "renatorib/posts" 95 | token: "", 96 | }); 97 | ``` 98 | 99 | Fetch your post using getPost: 100 | 101 | ```ts 102 | const post = await blog.getPost({ 103 | query: { slug: "my-first-post" }, 104 | }); 105 | ``` 106 | 107 | Fetch post comments using getComments: 108 | 109 | ```ts 110 | const comments = await blog.getComments({ 111 | query: { slug: "my-first-post" }, 112 | pager: { first: 100 }, 113 | }); 114 | ``` 115 | 116 | Fetch all your posts using getPosts: 117 | 118 | ```ts 119 | const posts = await blog.getPosts({ 120 | query: { type: "post", state: "published" }, 121 | pager: { limit: 10, offset: 0 }, 122 | }); 123 | ``` 124 | 125 | That's all. 126 | 127 | ## Guides 128 | 129 | ### Querying 130 | 131 | All query works by AND logic. You can't query by OR because of the nature and limitations of github search. 132 | But you can exclude results using prefix `not` (`notType`, `notState`, etc.) 133 | E.g: If you want to query posts with type _post_ but it can't have a flag _outdated_, you can use: 134 | 135 | ```ts 136 | const posts = await blog.getPosts({ 137 | query: { type: "post", notFlag: "outdated" }, 138 | pager: { limit: 10, offset: 0 }, 139 | }); 140 | ``` 141 | 142 | You can also pass an array to most of query params: 143 | 144 | ```ts 145 | const posts = await blog.getPosts({ 146 | query: { type: ["post", "article"], tag: ["javascript", "react"] }, 147 | pager: { limit: 10, offset: 0 }, 148 | }); 149 | ``` 150 | 151 | ### Searching 152 | 153 | You can also search for post that contain terms using `query.search` param: 154 | 155 | ```ts 156 | const posts = await blog.getPosts({ 157 | query: { type: "post", state: "published", search: "compiler" }, 158 | pager: { limit: 10, offset: 0 }, 159 | }); 160 | ``` 161 | 162 | ### Sorting 163 | 164 | You can sort results by `interactions`, `reactions`, `author-date`, `created`, `updated`. 165 | All of them are desc by default but you can suffix with `-asc`. See all [in docs](/docs) 166 | 167 | ```ts 168 | const posts = await blog.getPosts({ 169 | query: { type: "post", sort: "interactions" }, 170 | pager: { limit: 10, offset: 0 }, 171 | }); 172 | ``` 173 | 174 | ### Pagination 175 | 176 | You can paginate using `pager.limit` and `pager.offset` as you saw before, but you can also paginate using cursors with the pager params `after`, `before`, `first` and `last`. 177 | 178 | ```ts 179 | // first 10 posts 180 | const posts = await blog.getPosts({ 181 | query: { type: "post" }, 182 | pager: { first: 10 }, 183 | }); 184 | 185 | // more 10 posts 186 | const morePosts = await blog.getPosts({ 187 | query: { type: "post" }, 188 | pager: { first: 10, after: posts.pageInfo.endCursor }, 189 | }); 190 | ``` 191 | 192 | > **NOTE:** `limit` and `offset` uses `first` and `after` under the hood. 193 | > So if you pass both `limit` and `first` or `offset` and `after`, limit and offset will be ignored. 194 | 195 | ### Defaults 196 | 197 | You can set some defaults for querying right in your blog instance, if you want to avoid some query repetition: 198 | 199 | ```ts 200 | const blog = new GithubBlog({ 201 | repo: "renatorib/posts", 202 | token: process.env.GITHUB_TOKEN, 203 | queryDefaults: { 204 | state: "published", 205 | type: "post", 206 | }, 207 | }); 208 | 209 | const posts = await blog.getPosts({ 210 | pager: { first: 10, offset: 0 }, 211 | }); 212 | ``` 213 | 214 | ### Comments 215 | 216 | You can fetch all post comments using `getComments` method 217 | 218 | ```ts 219 | // first 10 comments 220 | const comments = await blog.getComments({ 221 | query: { slug: "my-first-post" }, 222 | pager: { first: 10 }, 223 | }); 224 | 225 | // more 10 posts 226 | const moreComments = await blog.getComments({ 227 | query: { slug: "my-first-post" }, 228 | pager: { first: 10, after: comments.pageInfo.endCursor }, 229 | }); 230 | ``` 231 | 232 | > **NOTE:** Comment pagination by _limit_ and _offset_ is still not possible while I figure out on how generate v2 cursors based on offset. 233 | > Read more about this issue here, maybe you can help. 234 | 235 | ## Problems 236 | 237 | Github issues and Github API of course isn't designed to this kind of usage. So I ended up bumping into some limitations during the design and construction of the project. Here I list some of them and try to describe the problem and how I tried to get around. 238 | 239 | ### Slug Problem 240 | 241 | One of my biggest disappointments. It's impossible to create a safe and unique slug for your posts. 242 | 243 | My first attempt was to use issue title to slug, and define the actual post title into issue's frontmatter. 244 | But it does not worked because: 245 | 246 | Github only let you query for an exact repo/issue using the number of it, and I don't want to put id/number into my urls. 247 | 248 | ```graphql 249 | query { 250 | repository(owner: "renatorib", name: "posts") { 251 | issue(number: 1) { // get issue at https://github.com/renatorib/posts/issue/1 252 | title 253 | } 254 | } 255 | } 256 | ``` 257 | 258 | Github repository issues only allow you to filter using labels, states (closed/open), assignee, dates, etc. Nothing that let me use the title. 259 | 260 | ```graphql 261 | query { 262 | repository(owner: "renatorib", name: "posts") { 263 | issues(...filters) { // some specific filters, nothing useful 264 | title 265 | } 266 | } 267 | } 268 | ``` 269 | 270 | So I was forced to use the [query search](https://docs.github.com/en/github/searching-for-information-on-github/getting-started-with-searching-on-github/understanding-the-search-syntax) that I find more powerful and I could filter by `repo:owner/name` 271 | Now I can find the issue using title this way: 272 | 273 | ```graphql 274 | query { 275 | search(type: ISSUE, first: 1, query: "repo:renatorib/posts slug-name") { 276 | nodes { 277 | ... on Issue { 278 | title 279 | } 280 | } 281 | } 282 | } 283 | ``` 284 | 285 | But it isn't _reliable_. I can't search for an _exact_ title with query search and it could return an issue with title of `slug-name-foo` instead of the `slug-name` depending on the sort rules. 286 | 287 | I gave up and ended using labels for that. Now I can query by exact slug: 288 | 289 | ```graphql 290 | query { 291 | search(type: ISSUE, first: 1, query: "repo:renatorib/posts label:slug:slug-name") { 292 | nodes { 293 | ... on Issue { 294 | title 295 | } 296 | } 297 | } 298 | } 299 | ``` 300 | 301 | It works. But the problem is that it isn't the ideal. Each post is a new label, it don't scale well. 302 | 303 | ### Pagination by limit/offset problem 304 | 305 | TODO 306 | 307 | ## API Reference 308 | 309 | See at [/docs](/docs) (auto-generated from typescript types) 310 | -------------------------------------------------------------------------------- /codegen.yml: -------------------------------------------------------------------------------- 1 | documents: "./src/**/*.ts" 2 | schema: "./src/__generated__/schema.graphql" 3 | generates: 4 | ./src/__generated__/index.ts: 5 | config: 6 | preResolveTypes: true 7 | onlyOperationTypes: true 8 | enumsAsTypes: true 9 | documentMode: "string" 10 | dedupeFragments: true 11 | plugins: 12 | - typescript 13 | - typescript-operations 14 | - typescript-generic-sdk 15 | -------------------------------------------------------------------------------- /cover.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/renatorib/github-blog/4f2a482c69578212bc4f91c44091de69dfc2a99a/cover.png -------------------------------------------------------------------------------- /docs/README.md: -------------------------------------------------------------------------------- 1 | @rena.to/github-blog 2 | 3 | # @rena.to/github-blog 4 | 5 | ## Table of contents 6 | 7 | ### Enumerations 8 | 9 | - [Reaction](enums/Reaction.md) 10 | 11 | ### Classes 12 | 13 | - [GithubBlog](classes/GithubBlog.md) 14 | 15 | ### Type aliases 16 | 17 | - [Author](README.md#author) 18 | - [Comment](README.md#comment) 19 | - [GetComments](README.md#getcomments) 20 | - [GetCommentsParams](README.md#getcommentsparams) 21 | - [GetCommentsResult](README.md#getcommentsresult) 22 | - [GetLabels](README.md#getlabels) 23 | - [GetLabelsParams](README.md#getlabelsparams) 24 | - [GetLabelsResult](README.md#getlabelsresult) 25 | - [GetPinnedPosts](README.md#getpinnedposts) 26 | - [GetPinnedPostsParams](README.md#getpinnedpostsparams) 27 | - [GetPinnedPostsResult](README.md#getpinnedpostsresult) 28 | - [GetPost](README.md#getpost) 29 | - [GetPostParams](README.md#getpostparams) 30 | - [GetPostResult](README.md#getpostresult) 31 | - [GetPosts](README.md#getposts) 32 | - [GetPostsParams](README.md#getpostsparams) 33 | - [GetPostsResult](README.md#getpostsresult) 34 | - [GithubBlogParams](README.md#githubblogparams) 35 | - [GithubQueryParams](README.md#githubqueryparams) 36 | - [Label](README.md#label) 37 | - [Labels](README.md#labels) 38 | - [PagerParams](README.md#pagerparams) 39 | - [Post](README.md#post) 40 | - [PostReduced](README.md#postreduced) 41 | - [Reactions](README.md#reactions) 42 | 43 | ## Type aliases 44 | 45 | ### Author 46 | 47 | Ƭ **Author**: typeof `Author.Type` 48 | 49 | #### Defined in 50 | 51 | [public-types.ts:4](https://github.com/renatorib/github-blog/blob/694d2f5/src/public-types.ts#L4) 52 | 53 | --- 54 | 55 | ### Comment 56 | 57 | Ƭ **Comment**: typeof `Comment.Type` 58 | 59 | #### Defined in 60 | 61 | [public-types.ts:7](https://github.com/renatorib/github-blog/blob/694d2f5/src/public-types.ts#L7) 62 | 63 | --- 64 | 65 | ### GetComments 66 | 67 | Ƭ **GetComments**: `ReturnType` 68 | 69 | #### Defined in 70 | 71 | [methods/getComments.ts:74](https://github.com/renatorib/github-blog/blob/694d2f5/src/methods/getComments.ts#L74) 72 | 73 | --- 74 | 75 | ### GetCommentsParams 76 | 77 | Ƭ **GetCommentsParams**: `Object` 78 | 79 | #### Type declaration 80 | 81 | | Name | Type | Description | 82 | | :------- | :----------------------------------------------------------------------- | :----------------------------------------------------------------------------- | 83 | | `pager?` | `Omit`<[`PagerParams`](README.md#pagerparams), `"limit"` \| `"offset"`\> | Pagination with limit and offset don't work in comments. Use cursor pagination | 84 | | `query?` | [`GithubQueryParams`](README.md#githubqueryparams) | - | 85 | 86 | #### Defined in 87 | 88 | [methods/getComments.ts:33](https://github.com/renatorib/github-blog/blob/694d2f5/src/methods/getComments.ts#L33) 89 | 90 | --- 91 | 92 | ### GetCommentsResult 93 | 94 | Ƭ **GetCommentsResult**: `Unwrap`<`ReturnType`<[`GetComments`](README.md#getcomments)\>\> 95 | 96 | #### Defined in 97 | 98 | [methods/getComments.ts:76](https://github.com/renatorib/github-blog/blob/694d2f5/src/methods/getComments.ts#L76) 99 | 100 | --- 101 | 102 | ### GetLabels 103 | 104 | Ƭ **GetLabels**: `ReturnType` 105 | 106 | #### Defined in 107 | 108 | [methods/getLabels.ts:71](https://github.com/renatorib/github-blog/blob/694d2f5/src/methods/getLabels.ts#L71) 109 | 110 | --- 111 | 112 | ### GetLabelsParams 113 | 114 | Ƭ **GetLabelsParams**: `Object` 115 | 116 | #### Type declaration 117 | 118 | | Name | Type | 119 | | :------- | :------------------------------------- | 120 | | `pager?` | [`PagerParams`](README.md#pagerparams) | 121 | | `query?` | `string` | 122 | 123 | #### Defined in 124 | 125 | [methods/getLabels.ts:36](https://github.com/renatorib/github-blog/blob/694d2f5/src/methods/getLabels.ts#L36) 126 | 127 | --- 128 | 129 | ### GetLabelsResult 130 | 131 | Ƭ **GetLabelsResult**: `Unwrap`<`ReturnType`<[`GetLabels`](README.md#getlabels)\>\> 132 | 133 | #### Defined in 134 | 135 | [methods/getLabels.ts:73](https://github.com/renatorib/github-blog/blob/694d2f5/src/methods/getLabels.ts#L73) 136 | 137 | --- 138 | 139 | ### GetPinnedPosts 140 | 141 | Ƭ **GetPinnedPosts**: `ReturnType` 142 | 143 | #### Defined in 144 | 145 | [methods/getPinnedPosts.ts:41](https://github.com/renatorib/github-blog/blob/694d2f5/src/methods/getPinnedPosts.ts#L41) 146 | 147 | --- 148 | 149 | ### GetPinnedPostsParams 150 | 151 | Ƭ **GetPinnedPostsParams**: `never` 152 | 153 | #### Defined in 154 | 155 | [methods/getPinnedPosts.ts:25](https://github.com/renatorib/github-blog/blob/694d2f5/src/methods/getPinnedPosts.ts#L25) 156 | 157 | --- 158 | 159 | ### GetPinnedPostsResult 160 | 161 | Ƭ **GetPinnedPostsResult**: `Unwrap`<`ReturnType`<[`GetPinnedPosts`](README.md#getpinnedposts)\>\> 162 | 163 | #### Defined in 164 | 165 | [methods/getPinnedPosts.ts:43](https://github.com/renatorib/github-blog/blob/694d2f5/src/methods/getPinnedPosts.ts#L43) 166 | 167 | --- 168 | 169 | ### GetPost 170 | 171 | Ƭ **GetPost**: `ReturnType` 172 | 173 | #### Defined in 174 | 175 | [methods/getPost.ts:39](https://github.com/renatorib/github-blog/blob/694d2f5/src/methods/getPost.ts#L39) 176 | 177 | --- 178 | 179 | ### GetPostParams 180 | 181 | Ƭ **GetPostParams**: `Object` 182 | 183 | #### Type declaration 184 | 185 | | Name | Type | 186 | | :------- | :------------------------------------------------- | 187 | | `query?` | [`GithubQueryParams`](README.md#githubqueryparams) | 188 | 189 | #### Defined in 190 | 191 | [methods/getPost.ts:19](https://github.com/renatorib/github-blog/blob/694d2f5/src/methods/getPost.ts#L19) 192 | 193 | --- 194 | 195 | ### GetPostResult 196 | 197 | Ƭ **GetPostResult**: `Unwrap`<`ReturnType`<[`GetPost`](README.md#getpost)\>\> 198 | 199 | #### Defined in 200 | 201 | [methods/getPost.ts:41](https://github.com/renatorib/github-blog/blob/694d2f5/src/methods/getPost.ts#L41) 202 | 203 | --- 204 | 205 | ### GetPosts 206 | 207 | Ƭ **GetPosts**: `ReturnType` 208 | 209 | #### Defined in 210 | 211 | [methods/getPosts.ts:66](https://github.com/renatorib/github-blog/blob/694d2f5/src/methods/getPosts.ts#L66) 212 | 213 | --- 214 | 215 | ### GetPostsParams 216 | 217 | Ƭ **GetPostsParams**: `Object` 218 | 219 | #### Type declaration 220 | 221 | | Name | Type | 222 | | :------- | :------------------------------------------------- | 223 | | `pager?` | [`PagerParams`](README.md#pagerparams) | 224 | | `query?` | [`GithubQueryParams`](README.md#githubqueryparams) | 225 | 226 | #### Defined in 227 | 228 | [methods/getPosts.ts:29](https://github.com/renatorib/github-blog/blob/694d2f5/src/methods/getPosts.ts#L29) 229 | 230 | --- 231 | 232 | ### GetPostsResult 233 | 234 | Ƭ **GetPostsResult**: `Unwrap`<`ReturnType`<[`GetPosts`](README.md#getposts)\>\> 235 | 236 | #### Defined in 237 | 238 | [methods/getPosts.ts:68](https://github.com/renatorib/github-blog/blob/694d2f5/src/methods/getPosts.ts#L68) 239 | 240 | --- 241 | 242 | ### GithubBlogParams 243 | 244 | Ƭ **GithubBlogParams**: `Object` 245 | 246 | #### Type declaration 247 | 248 | | Name | Type | 249 | | :-------------------- | :------------------------------------------------------------- | 250 | | `paginationDefaults?` | `Partial`<[`PagerParams`](README.md#pagerparams)\> | 251 | | `queryDefaults?` | `Partial`<[`GithubQueryParams`](README.md#githubqueryparams)\> | 252 | | `repo` | `string` | 253 | | `token` | `string` | 254 | 255 | #### Defined in 256 | 257 | [github-blog.ts:13](https://github.com/renatorib/github-blog/blob/694d2f5/src/github-blog.ts#L13) 258 | 259 | --- 260 | 261 | ### GithubQueryParams 262 | 263 | Ƭ **GithubQueryParams**: `Object` 264 | 265 | #### Type declaration 266 | 267 | | Name | Type | 268 | | :----------- | :--------------------- | 269 | | `author?` | `string` \| `string`[] | 270 | | `flag?` | `string` \| `string`[] | 271 | | `notAuthor?` | `string` \| `string`[] | 272 | | `notFlag?` | `string` \| `string`[] | 273 | | `notState?` | `string` \| `string`[] | 274 | | `notTag?` | `string` \| `string`[] | 275 | | `notType?` | `string` \| `string`[] | 276 | | `overrides?` | `string` | 277 | | `search?` | `string` | 278 | | `slug?` | `string` | 279 | | `sort?` | `Sort` | 280 | | `state?` | `string` \| `string`[] | 281 | | `tag?` | `string` \| `string`[] | 282 | | `type?` | `string` \| `string`[] | 283 | 284 | #### Defined in 285 | 286 | [utils/github-query.ts:12](https://github.com/renatorib/github-blog/blob/694d2f5/src/utils/github-query.ts#L12) 287 | 288 | --- 289 | 290 | ### Label 291 | 292 | Ƭ **Label**: typeof `Label.Type` 293 | 294 | #### Defined in 295 | 296 | [public-types.ts:10](https://github.com/renatorib/github-blog/blob/694d2f5/src/public-types.ts#L10) 297 | 298 | --- 299 | 300 | ### Labels 301 | 302 | Ƭ **Labels**: typeof `Labels.Type` 303 | 304 | #### Defined in 305 | 306 | [public-types.ts:13](https://github.com/renatorib/github-blog/blob/694d2f5/src/public-types.ts#L13) 307 | 308 | --- 309 | 310 | ### PagerParams 311 | 312 | Ƭ **PagerParams**: `Object` 313 | 314 | #### Type declaration 315 | 316 | | Name | Type | 317 | | :-------- | :------- | 318 | | `after?` | `string` | 319 | | `before?` | `string` | 320 | | `first?` | `number` | 321 | | `last?` | `number` | 322 | | `limit?` | `number` | 323 | | `offset?` | `number` | 324 | 325 | #### Defined in 326 | 327 | [utils/pager.ts:1](https://github.com/renatorib/github-blog/blob/694d2f5/src/utils/pager.ts#L1) 328 | 329 | --- 330 | 331 | ### Post 332 | 333 | Ƭ **Post**: typeof `Post.Type` 334 | 335 | #### Defined in 336 | 337 | [public-types.ts:16](https://github.com/renatorib/github-blog/blob/694d2f5/src/public-types.ts#L16) 338 | 339 | --- 340 | 341 | ### PostReduced 342 | 343 | Ƭ **PostReduced**: typeof `PostReduced.Type` 344 | 345 | #### Defined in 346 | 347 | [public-types.ts:19](https://github.com/renatorib/github-blog/blob/694d2f5/src/public-types.ts#L19) 348 | 349 | --- 350 | 351 | ### Reactions 352 | 353 | Ƭ **Reactions**: typeof `Reactions.Type` 354 | 355 | #### Defined in 356 | 357 | [public-types.ts:22](https://github.com/renatorib/github-blog/blob/694d2f5/src/public-types.ts#L22) 358 | -------------------------------------------------------------------------------- /docs/classes/GithubBlog.md: -------------------------------------------------------------------------------- 1 | [@rena.to/github-blog](../README.md) / GithubBlog 2 | 3 | # Class: GithubBlog 4 | 5 | ## Table of contents 6 | 7 | ### Constructors 8 | 9 | - [constructor](GithubBlog.md#constructor) 10 | 11 | ### Properties 12 | 13 | - [buildPager](GithubBlog.md#buildpager) 14 | - [buildQuery](GithubBlog.md#buildquery) 15 | - [client](GithubBlog.md#client) 16 | - [getComments](GithubBlog.md#getcomments) 17 | - [getLabels](GithubBlog.md#getlabels) 18 | - [getPinnedPosts](GithubBlog.md#getpinnedposts) 19 | - [getPost](GithubBlog.md#getpost) 20 | - [getPosts](GithubBlog.md#getposts) 21 | - [repo](GithubBlog.md#repo) 22 | - [sdk](GithubBlog.md#sdk) 23 | 24 | ## Constructors 25 | 26 | ### constructor 27 | 28 | • **new GithubBlog**(`params`) 29 | 30 | #### Parameters 31 | 32 | | Name | Type | 33 | | :------- | :-------------------------------------------------- | 34 | | `params` | [`GithubBlogParams`](../README.md#githubblogparams) | 35 | 36 | #### Defined in 37 | 38 | [github-blog.ts:26](https://github.com/renatorib/github-blog/blob/694d2f5/src/github-blog.ts#L26) 39 | 40 | ## Properties 41 | 42 | ### buildPager 43 | 44 | • **buildPager**: (`args?`: [`PagerParams`](../README.md#pagerparams)) => `Omit`<[`PagerParams`](../README.md#pagerparams), `"offset"`\> 45 | 46 | #### Type declaration 47 | 48 | ▸ (`args?`): `Omit`<[`PagerParams`](../README.md#pagerparams), `"offset"`\> 49 | 50 | ##### Parameters 51 | 52 | | Name | Type | 53 | | :------ | :---------------------------------------- | 54 | | `args?` | [`PagerParams`](../README.md#pagerparams) | 55 | 56 | ##### Returns 57 | 58 | `Omit`<[`PagerParams`](../README.md#pagerparams), `"offset"`\> 59 | 60 | #### Defined in 61 | 62 | [github-blog.ts:24](https://github.com/renatorib/github-blog/blob/694d2f5/src/github-blog.ts#L24) 63 | 64 | --- 65 | 66 | ### buildQuery 67 | 68 | • **buildQuery**: (`args?`: [`GithubQueryParams`](../README.md#githubqueryparams)) => `string` 69 | 70 | #### Type declaration 71 | 72 | ▸ (`args?`): `string` 73 | 74 | ##### Parameters 75 | 76 | | Name | Type | 77 | | :------ | :---------------------------------------------------- | 78 | | `args?` | [`GithubQueryParams`](../README.md#githubqueryparams) | 79 | 80 | ##### Returns 81 | 82 | `string` 83 | 84 | #### Defined in 85 | 86 | [github-blog.ts:23](https://github.com/renatorib/github-blog/blob/694d2f5/src/github-blog.ts#L23) 87 | 88 | --- 89 | 90 | ### client 91 | 92 | • **client**: `GraphQLClient` 93 | 94 | #### Defined in 95 | 96 | [github-blog.ts:20](https://github.com/renatorib/github-blog/blob/694d2f5/src/github-blog.ts#L20) 97 | 98 | --- 99 | 100 | ### getComments 101 | 102 | • **getComments**: (`params`: [`GetCommentsParams`](../README.md#getcommentsparams)) => `Promise`<`Object`\> 103 | 104 | #### Type declaration 105 | 106 | ▸ (`params`): `Promise`<`Object`\> 107 | 108 | ##### Parameters 109 | 110 | | Name | Type | 111 | | :------- | :---------------------------------------------------- | 112 | | `params` | [`GetCommentsParams`](../README.md#getcommentsparams) | 113 | 114 | ##### Returns 115 | 116 | `Promise`<`Object`\> 117 | 118 | #### Defined in 119 | 120 | [github-blog.ts:39](https://github.com/renatorib/github-blog/blob/694d2f5/src/github-blog.ts#L39) 121 | 122 | --- 123 | 124 | ### getLabels 125 | 126 | • **getLabels**: (`params?`: [`GetLabelsParams`](../README.md#getlabelsparams)) => `Promise`<`Object`\> 127 | 128 | #### Type declaration 129 | 130 | ▸ (`params?`): `Promise`<`Object`\> 131 | 132 | ##### Parameters 133 | 134 | | Name | Type | 135 | | :-------- | :------------------------------------------------ | 136 | | `params?` | [`GetLabelsParams`](../README.md#getlabelsparams) | 137 | 138 | ##### Returns 139 | 140 | `Promise`<`Object`\> 141 | 142 | #### Defined in 143 | 144 | [github-blog.ts:40](https://github.com/renatorib/github-blog/blob/694d2f5/src/github-blog.ts#L40) 145 | 146 | --- 147 | 148 | ### getPinnedPosts 149 | 150 | • **getPinnedPosts**: () => `Promise`<`Object`\> 151 | 152 | #### Type declaration 153 | 154 | ▸ (): `Promise`<`Object`\> 155 | 156 | ##### Returns 157 | 158 | `Promise`<`Object`\> 159 | 160 | #### Defined in 161 | 162 | [github-blog.ts:41](https://github.com/renatorib/github-blog/blob/694d2f5/src/github-blog.ts#L41) 163 | 164 | --- 165 | 166 | ### getPost 167 | 168 | • **getPost**: (`params`: [`GetPostParams`](../README.md#getpostparams)) => `Promise`<{ `post`: `null` = null } \| { `post`: `Post` }\> 169 | 170 | #### Type declaration 171 | 172 | ▸ (`params`): `Promise`<{ `post`: `null` = null } \| { `post`: `Post` }\> 173 | 174 | ##### Parameters 175 | 176 | | Name | Type | 177 | | :------- | :-------------------------------------------- | 178 | | `params` | [`GetPostParams`](../README.md#getpostparams) | 179 | 180 | ##### Returns 181 | 182 | `Promise`<{ `post`: `null` = null } \| { `post`: `Post` }\> 183 | 184 | #### Defined in 185 | 186 | [github-blog.ts:38](https://github.com/renatorib/github-blog/blob/694d2f5/src/github-blog.ts#L38) 187 | 188 | --- 189 | 190 | ### getPosts 191 | 192 | • **getPosts**: (`params`: [`GetPostsParams`](../README.md#getpostsparams)) => `Promise`<`Object`\> 193 | 194 | #### Type declaration 195 | 196 | ▸ (`params`): `Promise`<`Object`\> 197 | 198 | ##### Parameters 199 | 200 | | Name | Type | 201 | | :------- | :---------------------------------------------- | 202 | | `params` | [`GetPostsParams`](../README.md#getpostsparams) | 203 | 204 | ##### Returns 205 | 206 | `Promise`<`Object`\> 207 | 208 | #### Defined in 209 | 210 | [github-blog.ts:37](https://github.com/renatorib/github-blog/blob/694d2f5/src/github-blog.ts#L37) 211 | 212 | --- 213 | 214 | ### repo 215 | 216 | • **repo**: `string` 217 | 218 | #### Defined in 219 | 220 | [github-blog.ts:22](https://github.com/renatorib/github-blog/blob/694d2f5/src/github-blog.ts#L22) 221 | 222 | --- 223 | 224 | ### sdk 225 | 226 | • **sdk**: `Object` 227 | 228 | #### Type declaration 229 | 230 | | Name | Type | 231 | | :--------------- | :------------------------------------------------------------------------------------------------------- | 232 | | `GetComments` | (`variables`: `Exact`<`Object`\>, `requestHeaders?`: `HeadersInit`) => `Promise`<`GetCommentsQuery`\> | 233 | | `GetLabels` | (`variables`: `Exact`<`Object`\>, `requestHeaders?`: `HeadersInit`) => `Promise`<`GetLabelsQuery`\> | 234 | | `GetPinnedPosts` | (`variables`: `Exact`<`Object`\>, `requestHeaders?`: `HeadersInit`) => `Promise`<`GetPinnedPostsQuery`\> | 235 | | `GetPost` | (`variables`: `Exact`<`Object`\>, `requestHeaders?`: `HeadersInit`) => `Promise`<`GetPostQuery`\> | 236 | | `GetPosts` | (`variables`: `Exact`<`Object`\>, `requestHeaders?`: `HeadersInit`) => `Promise`<`GetPostsQuery`\> | 237 | 238 | #### Defined in 239 | 240 | [github-blog.ts:21](https://github.com/renatorib/github-blog/blob/694d2f5/src/github-blog.ts#L21) 241 | -------------------------------------------------------------------------------- /docs/enums/Reaction.md: -------------------------------------------------------------------------------- 1 | [@rena.to/github-blog](../README.md) / Reaction 2 | 3 | # Enumeration: Reaction 4 | 5 | ## Table of contents 6 | 7 | ### Enumeration members 8 | 9 | - [Confused](Reaction.md#confused) 10 | - [Eyes](Reaction.md#eyes) 11 | - [Heart](Reaction.md#heart) 12 | - [Hooray](Reaction.md#hooray) 13 | - [Laugh](Reaction.md#laugh) 14 | - [Rocket](Reaction.md#rocket) 15 | - [Smile](Reaction.md#smile) 16 | - [Tada](Reaction.md#tada) 17 | - [ThumbsDown](Reaction.md#thumbsdown) 18 | - [ThumbsUp](Reaction.md#thumbsup) 19 | 20 | ## Enumeration members 21 | 22 | ### Confused 23 | 24 | • **Confused** = `"CONFUSED"` 25 | 26 | #### Defined in 27 | 28 | [datatypes/Reactions.ts:12](https://github.com/renatorib/github-blog/blob/694d2f5/src/datatypes/Reactions.ts#L12) 29 | 30 | --- 31 | 32 | ### Eyes 33 | 34 | • **Eyes** = `"EYES"` 35 | 36 | #### Defined in 37 | 38 | [datatypes/Reactions.ts:15](https://github.com/renatorib/github-blog/blob/694d2f5/src/datatypes/Reactions.ts#L15) 39 | 40 | --- 41 | 42 | ### Heart 43 | 44 | • **Heart** = `"HEART"` 45 | 46 | #### Defined in 47 | 48 | [datatypes/Reactions.ts:13](https://github.com/renatorib/github-blog/blob/694d2f5/src/datatypes/Reactions.ts#L13) 49 | 50 | --- 51 | 52 | ### Hooray 53 | 54 | • **Hooray** = `"HOORAY"` 55 | 56 | #### Defined in 57 | 58 | [datatypes/Reactions.ts:10](https://github.com/renatorib/github-blog/blob/694d2f5/src/datatypes/Reactions.ts#L10) 59 | 60 | --- 61 | 62 | ### Laugh 63 | 64 | • **Laugh** = `"LAUGH"` 65 | 66 | #### Defined in 67 | 68 | [datatypes/Reactions.ts:8](https://github.com/renatorib/github-blog/blob/694d2f5/src/datatypes/Reactions.ts#L8) 69 | 70 | --- 71 | 72 | ### Rocket 73 | 74 | • **Rocket** = `"ROCKET"` 75 | 76 | #### Defined in 77 | 78 | [datatypes/Reactions.ts:14](https://github.com/renatorib/github-blog/blob/694d2f5/src/datatypes/Reactions.ts#L14) 79 | 80 | --- 81 | 82 | ### Smile 83 | 84 | • **Smile** = `"LAUGH"` 85 | 86 | #### Defined in 87 | 88 | [datatypes/Reactions.ts:9](https://github.com/renatorib/github-blog/blob/694d2f5/src/datatypes/Reactions.ts#L9) 89 | 90 | --- 91 | 92 | ### Tada 93 | 94 | • **Tada** = `"HOORAY"` 95 | 96 | #### Defined in 97 | 98 | [datatypes/Reactions.ts:11](https://github.com/renatorib/github-blog/blob/694d2f5/src/datatypes/Reactions.ts#L11) 99 | 100 | --- 101 | 102 | ### ThumbsDown 103 | 104 | • **ThumbsDown** = `"THUMBS_DOWN"` 105 | 106 | #### Defined in 107 | 108 | [datatypes/Reactions.ts:7](https://github.com/renatorib/github-blog/blob/694d2f5/src/datatypes/Reactions.ts#L7) 109 | 110 | --- 111 | 112 | ### ThumbsUp 113 | 114 | • **ThumbsUp** = `"THUMBS_UP"` 115 | 116 | #### Defined in 117 | 118 | [datatypes/Reactions.ts:6](https://github.com/renatorib/github-blog/blob/694d2f5/src/datatypes/Reactions.ts#L6) 119 | -------------------------------------------------------------------------------- /e2e/getComments.test.ts: -------------------------------------------------------------------------------- 1 | import { GithubBlog } from "../src/github-blog"; 2 | 3 | const blog = new GithubBlog({ 4 | repo: "renatorib/github-blog-tests", 5 | token: process.env.GITHUB_E2E_TESTS_TOKEN || process.env.GITHUB_TOKEN!, 6 | }); 7 | 8 | describe("getComments", () => { 9 | test("get post comments by slug", async () => { 10 | const result = await blog.getComments({ query: { slug: "first-post" }, pager: { first: 10 } }); 11 | 12 | expect(result.totalCount).toBeGreaterThan(1); 13 | expect(result.edges[0].comment.body).toBe("hi from comments"); 14 | expect(result.edges[0].comment.author.login).toBe("renatorib"); 15 | expect(result.edges[0].comment.isMinimized).toBe(false); 16 | 17 | expect(result.edges[1].comment.isMinimized).toBe(true); 18 | expect(result.edges[1].comment.body).toBe("minimized"); 19 | expect(result.edges[1].comment.minimizedReason).toBe("outdated"); 20 | }); 21 | }); 22 | -------------------------------------------------------------------------------- /e2e/getLabels.test.ts: -------------------------------------------------------------------------------- 1 | import { GithubBlog } from "../src/github-blog"; 2 | 3 | const blog = new GithubBlog({ 4 | repo: "renatorib/github-blog-tests", 5 | token: process.env.GITHUB_E2E_TESTS_TOKEN || process.env.GITHUB_TOKEN!, 6 | }); 7 | 8 | describe("getLabels", () => { 9 | test("get all labels", async () => { 10 | const result = await blog.getLabels(); 11 | 12 | expect(result.totalCount).toBeGreaterThan(6); 13 | expect(result.edges).toEqual( 14 | expect.arrayContaining([ 15 | expect.objectContaining({ 16 | cursor: expect.any(String), 17 | label: { 18 | id: expect.any(String), 19 | name: expect.any(String), 20 | prefix: expect.any(String), 21 | fullName: expect.any(String), 22 | color: expect.any(String), 23 | quantity: expect.any(Number), 24 | }, 25 | }), 26 | ]) 27 | ); 28 | }); 29 | 30 | test("get labels by query", async () => { 31 | const result = await blog.getLabels({ query: "tag:" }); 32 | 33 | expect(result.edges).toEqual( 34 | expect.arrayContaining([ 35 | expect.objectContaining({ 36 | cursor: expect.any(String), 37 | label: { 38 | id: expect.any(String), 39 | name: expect.any(String), 40 | prefix: expect.stringMatching("tag"), 41 | fullName: expect.stringContaining("tag:"), 42 | color: expect.any(String), 43 | quantity: expect.any(Number), 44 | }, 45 | }), 46 | ]) 47 | ); 48 | }); 49 | }); 50 | -------------------------------------------------------------------------------- /e2e/getPinnedPosts.test.ts: -------------------------------------------------------------------------------- 1 | import { GithubBlog } from "../src/github-blog"; 2 | 3 | const blog = new GithubBlog({ 4 | repo: "renatorib/github-blog-tests", 5 | token: process.env.GITHUB_E2E_TESTS_TOKEN || process.env.GITHUB_TOKEN!, 6 | }); 7 | 8 | describe("getPinnedPosts", () => { 9 | test("get pinned posts", async () => { 10 | const result = await blog.getPinnedPosts(); 11 | 12 | expect(result.pinnedPosts.length).toBe(1); 13 | expect(result.pinnedPosts[0].pinnedBy.login).toBe("renatorib"); 14 | expect(result.pinnedPosts[0].post.title).toBe("Pinned post"); 15 | }); 16 | }); 17 | -------------------------------------------------------------------------------- /e2e/getPost.test.ts: -------------------------------------------------------------------------------- 1 | import { GithubBlog } from "../src/github-blog"; 2 | 3 | const blog = new GithubBlog({ 4 | repo: "renatorib/github-blog-tests", 5 | token: process.env.GITHUB_E2E_TESTS_TOKEN || process.env.GITHUB_TOKEN!, 6 | }); 7 | 8 | describe("getPost", () => { 9 | test("get post by slug", async () => { 10 | const result = await blog.getPost({ query: { slug: "first-post" } }); 11 | 12 | expect(result.post?.number).toBe(1); 13 | expect(result.post?.title).toBe("First post"); 14 | expect(result.post?.frontmatter).toStrictEqual({ meta: "data" }); 15 | expect(result.post?.body).toBe("\r\nHi"); 16 | expect(result.post?.totalComments).toBeGreaterThan(0); 17 | expect(result.post?.totalReactions).toBeGreaterThan(0); 18 | expect(result.post?.reactions.THUMBS_UP).toBeGreaterThan(0); 19 | }); 20 | 21 | test("get post by id", async () => { 22 | const result = await blog.getPost({ id: "MDU6SXNzdWU5MzYwOTEyMDc=" }); 23 | 24 | expect(result.post?.title).toBe("First post"); 25 | expect(result.post?.frontmatter).toStrictEqual({ meta: "data" }); 26 | expect(result.post?.body).toBe("\r\nHi"); 27 | expect(result.post?.totalComments).toBeGreaterThan(0); 28 | expect(result.post?.totalReactions).toBeGreaterThan(0); 29 | expect(result.post?.reactions.THUMBS_UP).toBeGreaterThan(0); 30 | }); 31 | 32 | test("get post by number", async () => { 33 | const result = await blog.getPost({ number: 1 }); 34 | 35 | expect(result.post?.title).toBe("First post"); 36 | expect(result.post?.frontmatter).toStrictEqual({ meta: "data" }); 37 | expect(result.post?.body).toBe("\r\nHi"); 38 | expect(result.post?.totalComments).toBeGreaterThan(0); 39 | expect(result.post?.totalReactions).toBeGreaterThan(0); 40 | expect(result.post?.reactions.THUMBS_UP).toBeGreaterThan(0); 41 | }); 42 | }); 43 | -------------------------------------------------------------------------------- /e2e/getPosts.test.ts: -------------------------------------------------------------------------------- 1 | import { GithubBlog } from "../src/github-blog"; 2 | 3 | const blog = new GithubBlog({ 4 | repo: "renatorib/github-blog-tests", 5 | token: process.env.GITHUB_E2E_TESTS_TOKEN || process.env.GITHUB_TOKEN!, 6 | }); 7 | 8 | describe("getPosts", () => { 9 | test("get posts by tag", async () => { 10 | const result = await blog.getPosts({ 11 | query: { tag: "foo" }, 12 | pager: { limit: 10, offset: 0 }, 13 | }); 14 | 15 | expect(result.edges.map((edge) => edge.post.labels.tag)).toEqual( 16 | expect.arrayContaining([expect.arrayContaining(["foo"])]) 17 | ); 18 | }); 19 | 20 | test("get posts by state", async () => { 21 | const result = await blog.getPosts({ 22 | query: { state: "published" }, 23 | pager: { limit: 10, offset: 0 }, 24 | }); 25 | 26 | expect(result.edges.map((edge) => edge.post.labels.state)).toEqual( 27 | expect.arrayContaining([expect.arrayContaining(["published"])]) 28 | ); 29 | }); 30 | }); 31 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@rena.to/github-blog", 3 | "version": "0.6.1", 4 | "description": "Turn your github issues in CMS for your blog", 5 | "main": "dist/index.js", 6 | "repository": "https://github.com/renatorib/github-blog", 7 | "author": "Renato Ribeiro ", 8 | "license": "MIT", 9 | "scripts": { 10 | "codegen": "graphql-codegen -r dotenv-flow/config", 11 | "type-check": "tsc --noEmit", 12 | "dev": "pnpm codegen --watch", 13 | "build": "rm -rf ./dist && pnpm tsc --project tsconfig.build.json", 14 | "test": "pnpm codegen && pnpm jest", 15 | "prepublishOnly": "pnpm test && pnpm build", 16 | "debug": "node -r dotenv-flow/config src/tmp/debug.js", 17 | "docs": "typedoc src --readme none --githubPages false", 18 | "prepare": "simple-git-hooks" 19 | }, 20 | "simple-git-hooks": { 21 | "pre-commit": "./node_modules/.bin/nano-staged" 22 | }, 23 | "nano-staged": { 24 | "*.{ts,css,md}": "prettier --write" 25 | }, 26 | "jest": { 27 | "preset": "ts-jest", 28 | "setupFiles": [ 29 | "dotenv-flow/config" 30 | ], 31 | "testPathIgnorePatterns": [ 32 | "/node_modules/", 33 | "/dist/" 34 | ] 35 | }, 36 | "files": [ 37 | "package.json", 38 | "README.md", 39 | "dist" 40 | ], 41 | "dependencies": { 42 | "classnames": "^2.3.1", 43 | "code-tag": "^1.1.0", 44 | "undici": "^5.22.1", 45 | "yaml": "^2.3.1" 46 | }, 47 | "devDependencies": { 48 | "@graphql-codegen/cli": "^4.0.1", 49 | "@graphql-codegen/typescript": "^4.0.0", 50 | "@graphql-codegen/typescript-generic-sdk": "^3.1.0", 51 | "@graphql-codegen/typescript-operations": "^4.0.0", 52 | "@types/jest": "^29.5.2", 53 | "dotenv-flow": "^3.2.0", 54 | "eslint": "^7.23.0", 55 | "graphql": "^15.5.0", 56 | "jest": "^29.5.0", 57 | "nano-staged": "^0.8.0", 58 | "prettier": "^2.2.1", 59 | "simple-git-hooks": "^2.8.1", 60 | "ts-jest": "^29.1.0", 61 | "typedoc": "^0.22.4", 62 | "typedoc-plugin-markdown": "^3.11.2", 63 | "typescript": "^5.1.3" 64 | }, 65 | "packageManager": "pnpm@10.7.1+sha512.2d92c86b7928dc8284f53494fb4201f983da65f0fb4f0d40baafa5cf628fa31dae3e5968f12466f17df7e97310e30f343a648baea1b9b350685dafafffdf5808" 66 | } 67 | -------------------------------------------------------------------------------- /src/core/datatype.ts: -------------------------------------------------------------------------------- 1 | import { gql } from "code-tag"; 2 | 3 | type Or = Type | Or; 4 | type Maybe = Or; 5 | 6 | type TranslatorType = (input: Input) => Output; 7 | 8 | export const createDataType = < 9 | Input, 10 | Output, 11 | Fallback extends Maybe> = Output 12 | >(config: { 13 | fragment?: ReturnType; 14 | translator: TranslatorType; 15 | fallback?: Fallback; 16 | }) => { 17 | const Type = {} as Or; 18 | const FallbackType = {} as Fallback; 19 | const OutputType = {} as Output; 20 | const InputType = {} as Input; 21 | 22 | const fallback = (config.fallback ?? null) as Fallback; 23 | 24 | const translate: TranslatorType, typeof Type> = (input) => { 25 | if (input == null) { 26 | return fallback; 27 | } 28 | 29 | try { 30 | return config.translator(input); 31 | } catch (error) { 32 | if (process.env.NODE_ENV === "debug") { 33 | console.log(`[GithugBlog] DataType translator failed with error: `, error); 34 | } 35 | return fallback; 36 | } 37 | }; 38 | 39 | return { 40 | Type, 41 | FallbackType, 42 | OutputType, 43 | InputType, 44 | 45 | translate, 46 | ...(config.fragment ? { fragment: config.fragment } : {}), 47 | }; 48 | }; 49 | -------------------------------------------------------------------------------- /src/core/sdk.ts: -------------------------------------------------------------------------------- 1 | export { getSdk } from "../__generated__"; 2 | export type { Requester } from "../__generated__"; 3 | -------------------------------------------------------------------------------- /src/datatypes/Author.ts: -------------------------------------------------------------------------------- 1 | import { gql } from "code-tag"; 2 | import { createDataType } from "../core/datatype"; 3 | import { Author_ActorFragment } from "../types"; 4 | 5 | type Author = { 6 | avatarUrl: string | null; 7 | name: string; 8 | login: string | null; 9 | twitterUsername: string | null; 10 | }; 11 | 12 | type AuthorInput = Author_ActorFragment; 13 | 14 | export const Author = createDataType({ 15 | fragment: gql` 16 | fragment Author_Actor on Actor { 17 | ... on User { 18 | avatarUrl 19 | name 20 | login 21 | twitterUsername 22 | } 23 | ... on Organization { 24 | avatarUrl 25 | name 26 | login 27 | twitterUsername 28 | } 29 | ... on EnterpriseUserAccount { 30 | avatarUrl 31 | name 32 | login 33 | } 34 | ... on Bot { 35 | avatarUrl 36 | login 37 | } 38 | } 39 | `, 40 | translator: (actor) => { 41 | return { 42 | avatarUrl: "avatarUrl" in actor ? actor.avatarUrl : null, 43 | login: "login" in actor ? actor.login : null, 44 | name: "name" in actor && actor.name ? actor.name : "login" in actor ? actor.login : "Unknown", 45 | twitterUsername: "twitterUsername" in actor ? actor.twitterUsername ?? null : null, 46 | }; 47 | }, 48 | fallback: { 49 | avatarUrl: null, 50 | login: null, 51 | name: "Unknown", 52 | twitterUsername: null, 53 | }, 54 | }); 55 | -------------------------------------------------------------------------------- /src/datatypes/Comment.ts: -------------------------------------------------------------------------------- 1 | import { gql } from "code-tag"; 2 | import { createDataType } from "../core/datatype"; 3 | import { Comment_IssueCommentFragment } from "../types"; 4 | import { Author } from "./Author"; 5 | import { Reactions } from "./Reactions"; 6 | 7 | type Comment = { 8 | id: string; 9 | body: string; 10 | createdAt: string; 11 | lastEditedAt: string | null; 12 | isMinimized: boolean; 13 | minimizedReason: string | null; 14 | author: typeof Author.Type; 15 | reactions: typeof Reactions.Type; 16 | }; 17 | 18 | type CommentInput = Comment_IssueCommentFragment; 19 | 20 | export const Comment = createDataType({ 21 | fragment: gql` 22 | fragment Comment_IssueComment on IssueComment { 23 | id 24 | body 25 | createdAt 26 | lastEditedAt 27 | isMinimized 28 | minimizedReason 29 | reactions { 30 | totalCount 31 | } 32 | reactionGroups { 33 | ...Reactions_ReactionGroup 34 | } 35 | author { 36 | ...Author_Actor 37 | } 38 | } 39 | `, 40 | translator: (issue) => { 41 | return { 42 | id: issue.id, 43 | body: issue.body, 44 | createdAt: issue.createdAt.toString(), 45 | lastEditedAt: issue.lastEditedAt?.toString() ?? null, 46 | isMinimized: issue.isMinimized, 47 | minimizedReason: issue.minimizedReason ?? null, 48 | author: Author.translate(issue.author), 49 | reactions: Reactions.translate(issue.reactionGroups), 50 | totalReactions: issue.reactions.totalCount, 51 | }; 52 | }, 53 | }); 54 | -------------------------------------------------------------------------------- /src/datatypes/Label.ts: -------------------------------------------------------------------------------- 1 | import { gql } from "code-tag"; 2 | import { createDataType } from "../core/datatype"; 3 | import { Label_LabelFragment } from "../types"; 4 | 5 | type Label = { 6 | id: string; 7 | name: string; 8 | prefix: string; 9 | fullName: string; 10 | color: string; 11 | quantity: number; 12 | }; 13 | 14 | export const Label = createDataType({ 15 | fragment: gql` 16 | fragment Label_Label on Label { 17 | id 18 | name 19 | color 20 | issues { 21 | totalCount 22 | } 23 | } 24 | `, 25 | translator: (label) => { 26 | const [prefix, ...name] = label.name.split(":"); 27 | return { 28 | id: label.id, 29 | name: name.join(":"), 30 | prefix: prefix, 31 | fullName: label.name, 32 | color: label.color, 33 | quantity: label.issues.totalCount, 34 | }; 35 | }, 36 | }); 37 | -------------------------------------------------------------------------------- /src/datatypes/Labels.ts: -------------------------------------------------------------------------------- 1 | import { gql } from "code-tag"; 2 | import { isNonNull } from "../utils/func"; 3 | import { createDataType } from "../core/datatype"; 4 | import { Labels_LabelConnectionFragment } from "../types"; 5 | 6 | type Labels = { 7 | state?: string[]; 8 | type?: string[]; 9 | tag?: string[]; 10 | flag?: string[]; 11 | slug?: string[]; 12 | [k: string]: string[] | undefined; 13 | }; 14 | 15 | type LabelsInput = Labels_LabelConnectionFragment; 16 | 17 | export const Labels = createDataType({ 18 | fragment: gql` 19 | fragment Labels_LabelConnection on LabelConnection { 20 | nodes { 21 | name 22 | } 23 | } 24 | `, 25 | translator: ({ nodes }) => { 26 | return (nodes ?? []) 27 | .filter(isNonNull) 28 | .map((label) => label.name) 29 | .reduce((acc, curr) => { 30 | const [_prop, value] = curr.split(":"); 31 | const prop = _prop as keyof Labels; 32 | return { 33 | ...acc, 34 | [prop]: acc[prop] ? [...acc[prop]!, value] : [value], 35 | }; 36 | }, {}); 37 | }, 38 | fallback: {}, 39 | }); 40 | -------------------------------------------------------------------------------- /src/datatypes/PageInfo.ts: -------------------------------------------------------------------------------- 1 | import { gql } from "code-tag"; 2 | import { createDataType } from "../core/datatype"; 3 | import { PageInfo_PageInfoFragment } from "../types"; 4 | 5 | type PageInfo = { 6 | endCursor?: string; 7 | startCursor?: string; 8 | hasNextPage?: boolean; 9 | hasPreviousPage?: boolean; 10 | }; 11 | 12 | export const PageInfo = createDataType({ 13 | fragment: gql` 14 | fragment PageInfo_PageInfo on PageInfo { 15 | endCursor 16 | startCursor 17 | hasNextPage 18 | hasPreviousPage 19 | } 20 | `, 21 | translator: (pageInfo) => { 22 | return { 23 | endCursor: pageInfo.endCursor ?? undefined, 24 | startCursor: pageInfo.startCursor ?? undefined, 25 | hasNextPage: pageInfo.hasNextPage ?? undefined, 26 | hasPreviousPage: pageInfo.hasPreviousPage ?? undefined, 27 | }; 28 | }, 29 | }); 30 | -------------------------------------------------------------------------------- /src/datatypes/Post.ts: -------------------------------------------------------------------------------- 1 | import { gql } from "code-tag"; 2 | import { createDataType } from "../core/datatype"; 3 | import { Post_IssueFragment } from "../types"; 4 | import { frontmatter } from "../utils/frontmatter"; 5 | 6 | import { Reactions } from "./Reactions"; 7 | import { Labels } from "./Labels"; 8 | import { Author } from "./Author"; 9 | 10 | type Post = { 11 | id: string; 12 | number: number; 13 | url: string; 14 | updatedAt: string; 15 | createdAt: string; 16 | title: string; 17 | author: typeof Author.Type; 18 | body: string; 19 | frontmatter: { [key: string]: string }; 20 | labels: typeof Labels.Type; 21 | reactions: typeof Reactions.Type; 22 | totalComments: number; 23 | totalReactions: number; 24 | }; 25 | 26 | type PostInput = Post_IssueFragment; 27 | 28 | export const Post = createDataType({ 29 | fragment: gql` 30 | fragment Post_Issue on Issue { 31 | id 32 | number 33 | url 34 | updatedAt 35 | createdAt 36 | title 37 | body 38 | author { 39 | ...Author_Actor 40 | } 41 | reactionGroups { 42 | ...Reactions_ReactionGroup 43 | } 44 | labels(first: 100) { 45 | ...Labels_LabelConnection 46 | } 47 | comments { 48 | totalCount 49 | } 50 | reactions { 51 | totalCount 52 | } 53 | } 54 | `, 55 | translator: (issue) => { 56 | const { data, content } = frontmatter(issue.body); 57 | 58 | return { 59 | id: issue.id, 60 | number: issue.number, 61 | url: issue.url, 62 | updatedAt: issue.updatedAt, 63 | createdAt: issue.createdAt, 64 | frontmatter: data, 65 | body: content, 66 | title: issue.title, 67 | author: Author.translate(issue.author), 68 | labels: Labels.translate(issue.labels), 69 | totalComments: issue.comments.totalCount, 70 | reactions: Reactions.translate(issue.reactionGroups), 71 | totalReactions: issue.reactions.totalCount, 72 | }; 73 | }, 74 | }); 75 | -------------------------------------------------------------------------------- /src/datatypes/PostReduced.ts: -------------------------------------------------------------------------------- 1 | import { gql } from "code-tag"; 2 | import { createDataType } from "../core/datatype"; 3 | 4 | import { Post } from "./Post"; 5 | 6 | type PostReduced = Omit; 7 | type PostReducedInput = typeof Post.InputType; 8 | 9 | export const PostReduced = createDataType({ 10 | fragment: gql` 11 | fragment PostReduced_Issue on Issue { 12 | ...Post_Issue 13 | } 14 | `, 15 | translator: (issue) => { 16 | const { body, ...post } = Post.translate(issue); 17 | return post; 18 | }, 19 | }); 20 | -------------------------------------------------------------------------------- /src/datatypes/Reactions.ts: -------------------------------------------------------------------------------- 1 | import { gql } from "code-tag"; 2 | import { createDataType } from "../core/datatype"; 3 | import { Reactions_ReactionGroupFragment, ReactionContent } from "../types"; 4 | 5 | export enum Reaction { 6 | ThumbsUp = "THUMBS_UP", 7 | ThumbsDown = "THUMBS_DOWN", 8 | Laugh = "LAUGH", 9 | Smile = "LAUGH", 10 | Hooray = "HOORAY", 11 | Tada = "HOORAY", 12 | Confused = "CONFUSED", 13 | Heart = "HEART", 14 | Rocket = "ROCKET", 15 | Eyes = "EYES", 16 | } 17 | 18 | type Reactions = { 19 | THUMBS_UP: number; 20 | THUMBS_DOWN: number; 21 | LAUGH: number; 22 | HOORAY: number; 23 | CONFUSED: number; 24 | HEART: number; 25 | ROCKET: number; 26 | EYES: number; 27 | }; 28 | 29 | type ReactionsInput = Reactions_ReactionGroupFragment[]; 30 | 31 | export const Reactions = createDataType({ 32 | fragment: gql` 33 | fragment Reactions_ReactionGroup on ReactionGroup { 34 | content 35 | users { 36 | totalCount 37 | } 38 | } 39 | `, 40 | translator: (reactionGroups) => { 41 | return (reactionGroups ?? []).reduce( 42 | (acc, curr) => ({ 43 | ...acc, 44 | [curr.content]: curr.users?.totalCount, 45 | }), 46 | {} 47 | ) as Reactions; 48 | }, 49 | fallback: { 50 | THUMBS_UP: 0, 51 | THUMBS_DOWN: 0, 52 | LAUGH: 0, 53 | HOORAY: 0, 54 | CONFUSED: 0, 55 | HEART: 0, 56 | ROCKET: 0, 57 | EYES: 0, 58 | }, 59 | }); 60 | -------------------------------------------------------------------------------- /src/github-blog.ts: -------------------------------------------------------------------------------- 1 | import { fetch } from "undici"; 2 | import { GithubQueryParams, githubQueryBuilder } from "./utils/github-query"; 3 | import { PagerParams, buildPager } from "./utils/pager"; 4 | 5 | import { getSdk, Requester } from "./core/sdk"; 6 | 7 | import { getPosts } from "./methods/getPosts"; 8 | import { getPinnedPosts } from "./methods/getPinnedPosts"; 9 | import { getPost } from "./methods/getPost"; 10 | import { getComments } from "./methods/getComments"; 11 | import { getLabels } from "./methods/getLabels"; 12 | 13 | export type GithubBlogParams = { 14 | token: string; 15 | repo: string; 16 | queryDefaults?: Partial; 17 | paginationDefaults?: Partial; 18 | }; 19 | export class GithubBlog { 20 | sdk: ReturnType; 21 | repo: string; 22 | buildQuery: (args?: GithubQueryParams) => ReturnType>; 23 | buildPager: (args?: PagerParams) => ReturnType; 24 | 25 | constructor(params: GithubBlogParams) { 26 | this.repo = params.repo; 27 | const request: Requester = async (query: string, variables: unknown) => { 28 | const body = JSON.stringify({ 29 | query, 30 | variables, 31 | }); 32 | const response = await fetch("https://api.github.com/graphql", { 33 | method: "POST", 34 | headers: { 35 | "Content-Type": "application/json", 36 | Authorization: `Bearer ${params.token}`, 37 | }, 38 | body, 39 | }); 40 | const result = (await response.json()) as { data: any; errors: any }; 41 | if (result.data) { 42 | return result.data; 43 | } 44 | const status = `${response.status} ${response.statusText}`; 45 | throw Error(`${status}\n${body}\n${JSON.stringify(result)}`); 46 | }; 47 | this.sdk = getSdk(request); 48 | const buildQuery = githubQueryBuilder(this.repo); 49 | this.buildQuery = (args) => buildQuery(args, params.queryDefaults); 50 | this.buildPager = (args) => buildPager(args, params.paginationDefaults); 51 | } 52 | 53 | getPosts = getPosts(this); 54 | getPost = getPost(this); 55 | getComments = getComments(this); 56 | getLabels = getLabels(this); 57 | getPinnedPosts = getPinnedPosts(this); 58 | } 59 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | export { GithubBlog } from "./github-blog"; 2 | export * from "./public-types"; 3 | 4 | /* Extras */ 5 | export { Reaction } from "./datatypes/Reactions"; 6 | -------------------------------------------------------------------------------- /src/methods/getComments.ts: -------------------------------------------------------------------------------- 1 | import { gql } from "code-tag"; 2 | import type { GithubBlog } from "../github-blog"; 3 | import { GithubQueryParams } from "../utils/github-query"; 4 | import { PagerParams } from "../utils/pager"; 5 | import { isNonNull } from "../utils/func"; 6 | import { Comment } from "../datatypes/Comment"; 7 | import { PageInfo } from "../datatypes/PageInfo"; 8 | 9 | gql` 10 | query GetComments($query: String!, $first: Int, $last: Int, $before: String, $after: String) { 11 | search(first: 1, type: ISSUE, query: $query) { 12 | nodes { 13 | ... on Issue { 14 | comments(first: $first, last: $last, before: $before, after: $after) { 15 | totalCount 16 | pageInfo { 17 | ...PageInfo_PageInfo 18 | } 19 | edges { 20 | cursor 21 | node { 22 | ...Comment_IssueComment 23 | } 24 | } 25 | } 26 | } 27 | } 28 | } 29 | } 30 | `; 31 | 32 | export type GetCommentsParams = { 33 | query?: GithubQueryParams; 34 | /** 35 | * Pagination with limit and offset don't work in comments. Use cursor pagination 36 | */ 37 | pager?: Omit; 38 | }; 39 | 40 | export const getComments = (blog: GithubBlog) => async (params: GetCommentsParams) => { 41 | const query = blog.buildQuery(params.query); 42 | const pager = blog.buildPager(params.pager); 43 | 44 | const result = await blog.sdk.GetComments({ query, ...pager }); 45 | 46 | const issue = result.search.nodes?.[0]; 47 | 48 | if (!issue) { 49 | return { 50 | pageInfo: {}, 51 | totalCount: 0, 52 | edges: [], 53 | }; 54 | } 55 | 56 | const connection = (issue as Extract).comments; 57 | const edges = connection.edges ?? []; 58 | const pageInfo = connection.pageInfo ?? {}; 59 | const totalCount = connection.totalCount ?? 0; 60 | 61 | return { 62 | totalCount, 63 | pageInfo: PageInfo.translate(pageInfo), 64 | edges: edges.filter(isNonNull).map((edge) => { 65 | return { 66 | cursor: edge.cursor, 67 | comment: Comment.translate(edge.node), 68 | }; 69 | }), 70 | }; 71 | }; 72 | 73 | export type GetComments = ReturnType; 74 | 75 | export type GetCommentsResult = Awaited>; 76 | -------------------------------------------------------------------------------- /src/methods/getLabels.ts: -------------------------------------------------------------------------------- 1 | import { gql } from "code-tag"; 2 | import type { GithubBlog } from "../github-blog"; 3 | import { isNonNull } from "../utils/func"; 4 | import { PagerParams } from "../utils/pager"; 5 | import { Label } from "../datatypes/Label"; 6 | import { PageInfo } from "../datatypes/PageInfo"; 7 | 8 | gql` 9 | query GetLabels( 10 | $query: String 11 | $name: String! 12 | $owner: String! 13 | $first: Int 14 | $last: Int 15 | $before: String 16 | $after: String 17 | ) { 18 | repository(name: $name, owner: $owner) { 19 | labels(query: $query, first: $first, last: $last, before: $before, after: $after) { 20 | totalCount 21 | pageInfo { 22 | ...PageInfo_PageInfo 23 | } 24 | edges { 25 | cursor 26 | node { 27 | ...Label_Label 28 | } 29 | } 30 | } 31 | } 32 | } 33 | `; 34 | 35 | export type GetLabelsParams = { 36 | query?: string; 37 | pager?: PagerParams; 38 | }; 39 | 40 | export const getLabels = (blog: GithubBlog) => async (params?: GetLabelsParams) => { 41 | const [owner, name] = blog.repo.split("/"); 42 | const pager = blog.buildPager(params?.pager); 43 | const result = await blog.sdk.GetLabels({ owner, name, ...pager, first: pager.first ?? 100 }); 44 | 45 | const labels = result.repository?.labels; 46 | if (!labels) { 47 | return { 48 | totalCount: 0, 49 | pageInfo: {}, 50 | edges: [], 51 | }; 52 | } 53 | 54 | const totalCount = labels.totalCount ?? 0; 55 | const pageInfo = labels.pageInfo ?? {}; 56 | const edges = labels.edges ?? []; 57 | 58 | return { 59 | totalCount, 60 | pageInfo: PageInfo.translate(pageInfo), 61 | edges: edges.filter(isNonNull).map((edge) => { 62 | return { 63 | cursor: edge.cursor, 64 | label: Label.translate(edge.node), 65 | }; 66 | }), 67 | }; 68 | }; 69 | 70 | export type GetLabels = ReturnType; 71 | 72 | export type GetLabelsResult = Awaited>; 73 | -------------------------------------------------------------------------------- /src/methods/getPinnedPosts.ts: -------------------------------------------------------------------------------- 1 | import { gql } from "code-tag"; 2 | import type { GithubBlog } from "../github-blog"; 3 | import { isNonNull } from "../utils/func"; 4 | import { PostReduced } from "../datatypes/PostReduced"; 5 | import { Author } from "../datatypes/Author"; 6 | 7 | gql` 8 | query GetPinnedPosts($owner: String!, $name: String!) { 9 | repository(owner: $owner, name: $name) { 10 | pinnedIssues(first: 3) { 11 | nodes { 12 | pinnedBy { 13 | ...Author_Actor 14 | } 15 | issue { 16 | ...Post_Issue 17 | } 18 | } 19 | } 20 | } 21 | } 22 | `; 23 | 24 | export type GetPinnedPostsParams = never; 25 | 26 | export const getPinnedPosts = (blog: GithubBlog) => async () => { 27 | const [owner, name] = blog.repo.split("/"); 28 | 29 | const result = await blog.sdk.GetPinnedPosts({ owner, name }); 30 | const nodes = result.repository?.pinnedIssues?.nodes ?? []; 31 | 32 | return { 33 | pinnedPosts: nodes.filter(isNonNull).map((pinnedIssue) => ({ 34 | pinnedBy: Author.translate(pinnedIssue.pinnedBy), 35 | post: PostReduced.translate(pinnedIssue.issue), 36 | })), 37 | }; 38 | }; 39 | 40 | export type GetPinnedPosts = ReturnType; 41 | 42 | export type GetPinnedPostsResult = Awaited>; 43 | -------------------------------------------------------------------------------- /src/methods/getPost.ts: -------------------------------------------------------------------------------- 1 | import { gql } from "code-tag"; 2 | import type { GithubBlog } from "../github-blog"; 3 | import { Post } from "../datatypes/Post"; 4 | import { GithubQueryParams } from "../utils/github-query"; 5 | 6 | gql` 7 | query GetPost($query: String!) { 8 | search(first: 1, type: ISSUE, query: $query) { 9 | nodes { 10 | __typename 11 | ... on Issue { 12 | ...Post_Issue 13 | } 14 | } 15 | } 16 | } 17 | `; 18 | 19 | gql` 20 | query GetPostByNumber($owner: String!, $name: String!, $number: Int!) { 21 | repository(owner: $owner, name: $name) { 22 | issue(number: $number) { 23 | ...Post_Issue 24 | } 25 | } 26 | } 27 | `; 28 | 29 | gql` 30 | query GetPostById($id: ID!) { 31 | node(id: $id) { 32 | __typename 33 | ...Post_Issue 34 | } 35 | } 36 | `; 37 | 38 | export type GetPostParams = { id: string } | { number: number } | { query?: GithubQueryParams }; 39 | 40 | export const getPost = (blog: GithubBlog) => async (params: GetPostParams) => { 41 | if ("id" in params) { 42 | const result = await blog.sdk.GetPostById({ id: params.id }); 43 | const issue = result.node?.__typename === "Issue" ? result.node : null; 44 | return { post: issue ? Post.translate(issue) : null }; 45 | } 46 | 47 | if ("number" in params) { 48 | const [owner, name] = blog.repo.split("/"); 49 | const result = await blog.sdk.GetPostByNumber({ number: params.number, owner, name }); 50 | const issue = result.repository?.issue; 51 | return { post: issue ? Post.translate(issue) : null }; 52 | } 53 | 54 | const query = blog.buildQuery(params.query); 55 | const result = await blog.sdk.GetPost({ query }); 56 | const nodes = result.search.nodes ?? []; 57 | const issue = nodes[0] as Extract; 58 | return { post: issue ? Post.translate(issue) : null }; 59 | }; 60 | 61 | export type GetPost = ReturnType; 62 | 63 | export type GetPostResult = Awaited>; 64 | -------------------------------------------------------------------------------- /src/methods/getPosts.ts: -------------------------------------------------------------------------------- 1 | import { gql } from "code-tag"; 2 | import type { GithubBlog } from "../github-blog"; 3 | import { GithubQueryParams } from "../utils/github-query"; 4 | import { isNonNull } from "../utils/func"; 5 | import { PostReduced } from "../datatypes/PostReduced"; 6 | import { PagerParams } from "../utils/pager"; 7 | 8 | gql` 9 | query GetPosts($query: String!, $first: Int, $last: Int, $before: String, $after: String) { 10 | search(query: $query, first: $first, last: $last, before: $before, after: $after, type: ISSUE) { 11 | issueCount 12 | pageInfo { 13 | endCursor 14 | startCursor 15 | hasNextPage 16 | hasPreviousPage 17 | } 18 | edges { 19 | cursor 20 | node { 21 | ...Post_Issue 22 | } 23 | } 24 | } 25 | } 26 | `; 27 | 28 | export type GetPostsParams = { 29 | query?: GithubQueryParams; 30 | pager?: PagerParams; 31 | }; 32 | 33 | export const getPosts = (blog: GithubBlog) => async (params: GetPostsParams) => { 34 | const query = blog.buildQuery(params.query); 35 | const pager = blog.buildPager(params.pager); 36 | 37 | const result = await blog.sdk.GetPosts({ 38 | query, 39 | ...pager, 40 | }); 41 | 42 | const edges = result.search.edges ?? []; 43 | const pageInfo = result.search.pageInfo ?? {}; 44 | const totalCount = result.search.issueCount ?? 0; 45 | 46 | return { 47 | totalCount, 48 | pageInfo: { 49 | endCursor: pageInfo.endCursor, 50 | startCursor: pageInfo.startCursor, 51 | hasNextPage: pageInfo.hasNextPage, 52 | hasPreviousPage: pageInfo.hasPreviousPage, 53 | }, 54 | edges: edges.filter(isNonNull).map((edge) => { 55 | return { 56 | cursor: edge.cursor, 57 | post: PostReduced.translate( 58 | edge.node as Extract 59 | ), 60 | }; 61 | }), 62 | }; 63 | }; 64 | 65 | export type GetPosts = ReturnType; 66 | 67 | export type GetPostsResult = Awaited>; 68 | -------------------------------------------------------------------------------- /src/public-types.ts: -------------------------------------------------------------------------------- 1 | /* Datatypes Types */ 2 | 3 | import { Author } from "./datatypes/Author"; 4 | export type Author = typeof Author.Type; 5 | 6 | import { Comment } from "./datatypes/Comment"; 7 | export type Comment = typeof Comment.Type; 8 | 9 | import { Label } from "./datatypes/Label"; 10 | export type Label = typeof Label.Type; 11 | 12 | import { Labels } from "./datatypes/Labels"; 13 | export type Labels = typeof Labels.Type; 14 | 15 | import { Post } from "./datatypes/Post"; 16 | export type Post = typeof Post.Type; 17 | 18 | import { PostReduced } from "./datatypes/PostReduced"; 19 | export type PostReduced = typeof PostReduced.Type; 20 | 21 | import { Reactions } from "./datatypes/Reactions"; 22 | export type Reactions = typeof Reactions.Type; 23 | 24 | /* GithubBlog Types */ 25 | export type { GithubBlog, GithubBlogParams } from "./github-blog"; 26 | 27 | /* Methods Types */ 28 | 29 | export type { GetComments, GetCommentsParams, GetCommentsResult } from "./methods/getComments"; 30 | export type { GetLabels, GetLabelsParams, GetLabelsResult } from "./methods/getLabels"; 31 | // prettier-ignore 32 | export type { GetPinnedPosts, GetPinnedPostsParams, GetPinnedPostsResult } from "./methods/getPinnedPosts"; 33 | export type { GetPost, GetPostParams, GetPostResult } from "./methods/getPost"; 34 | export type { GetPosts, GetPostsParams, GetPostsResult } from "./methods/getPosts"; 35 | 36 | /* Utils & Extras */ 37 | 38 | export type { GithubQueryParams } from "./utils/github-query"; 39 | export type { PagerParams } from "./utils/pager"; 40 | -------------------------------------------------------------------------------- /src/types/index.ts: -------------------------------------------------------------------------------- 1 | export * from "../__generated__/index"; 2 | -------------------------------------------------------------------------------- /src/utils/frontmatter.test.ts: -------------------------------------------------------------------------------- 1 | import { frontmatter } from "./frontmatter"; 2 | 3 | const someYaml = "---\nkey: value\nlist:\n - 1\n - 2\n---"; 4 | const someData = { key: "value", list: [1, 2] }; 5 | const doc = "Here is a document\nMore of the document\nOther lines\n"; 6 | const both = someYaml + "\n" + doc; 7 | 8 | test("should parse and strip frontmatter", () => { 9 | expect(frontmatter(both)).toEqual({ 10 | data: someData, 11 | content: doc, 12 | }); 13 | }); 14 | 15 | test("should support no matter", () => { 16 | expect(frontmatter(doc)).toEqual({ 17 | data: {}, 18 | content: doc, 19 | }); 20 | }); 21 | 22 | test("should strip matter completely", () => { 23 | expect(frontmatter(someYaml)).toEqual({ 24 | data: someData, 25 | content: "", 26 | }); 27 | }); 28 | 29 | test("should handle thematic breaks", () => { 30 | const extra = "Here is a thematic break\n---\nEnd"; 31 | expect(frontmatter(both + extra)).toEqual({ 32 | data: someData, 33 | content: doc + extra, 34 | }); 35 | }); 36 | 37 | test("should support additional newline before closing matter", () => { 38 | expect(frontmatter("---\nkey: value\n\n---\n" + doc)).toEqual({ 39 | data: { key: "value" }, 40 | content: doc, 41 | }); 42 | }); 43 | -------------------------------------------------------------------------------- /src/utils/frontmatter.ts: -------------------------------------------------------------------------------- 1 | // inspired by https://github.com/vfile/vfile-matter 2 | 3 | import { parse } from "yaml"; 4 | 5 | export const frontmatter = (content: string) => { 6 | const match = /^---(?:\r?\n|\r)(?:([\s\S]*?)(?:\r?\n|\r))?---(?:\r?\n|\r|$)/.exec(content); 7 | if (match) { 8 | return { 9 | data: parse(match[1]), 10 | content: content.slice(match[0].length), 11 | }; 12 | } 13 | return { 14 | data: {}, 15 | content, 16 | }; 17 | }; 18 | -------------------------------------------------------------------------------- /src/utils/func.ts: -------------------------------------------------------------------------------- 1 | export const isNonNull = (value: T | null | undefined): value is T => { 2 | return value != null; 3 | }; 4 | -------------------------------------------------------------------------------- /src/utils/github-query.ts: -------------------------------------------------------------------------------- 1 | import cn from "classnames"; 2 | 3 | export type SortDescAsc = "interactions" | "reactions" | "author-date" | "created" | "updated"; 4 | export type SortReaction = 5 | | "reactions-+1" 6 | | "reactions--1" 7 | | "reactions-smile" 8 | | "reactions-tada" 9 | | "reactions-heart"; 10 | export type Sort = SortDescAsc | `${SortDescAsc}-asc` | `${SortDescAsc}-desc` | SortReaction; 11 | 12 | export type GithubQueryParams = { 13 | tag?: string | string[]; 14 | notTag?: string | string[]; 15 | flag?: string | string[]; 16 | notFlag?: string | string[]; 17 | state?: string | string[]; 18 | notState?: string | string[]; 19 | type?: string | string[]; 20 | notType?: string | string[]; 21 | author?: string | string[]; 22 | notAuthor?: string | string[]; 23 | 24 | sort?: Sort; 25 | slug?: string; 26 | search?: string; 27 | overrides?: string; 28 | }; 29 | 30 | const multiple = (prefix: string, value: string | string[] | undefined) => { 31 | if (!value) return null; 32 | return (Array.isArray(value) ? value : [value]).map((v) => `${prefix}:${v}`); 33 | }; 34 | 35 | const single = (prefix: string, value: string | undefined) => { 36 | if (!value) return null; 37 | return `${prefix}:${value}`; 38 | }; 39 | 40 | export const githubQueryBuilder = (repo: string) => ( 41 | _args?: GithubQueryParams | undefined, 42 | _defaults?: Partial | undefined 43 | ): string => { 44 | const args = _args ?? {}; 45 | const defaults = _defaults ?? {}; 46 | 47 | const query = cn( 48 | `repo:${repo}`, // Should search on instance repo 49 | `type:issue`, // Should search for issues only 50 | 51 | args.overrides ?? defaults.overrides, // Overrides should came before as the former has priority 52 | `is:open`, // Search for opened issues only 53 | multiple("label:tag", args.tag ?? defaults.tag), 54 | multiple("-label:tag", args.notTag ?? defaults.notTag), 55 | multiple("label:flag", args.flag ?? defaults.flag), 56 | multiple("-label:flag", args.notFlag ?? defaults.notFlag), 57 | multiple("label:state", args.state ?? defaults.state), 58 | multiple("-label:state", args.notState ?? defaults.notState), 59 | multiple("label:type", args.type ?? defaults.type), 60 | multiple("-label:type", args.notType ?? defaults.notType), 61 | single("label:slug", args.slug ?? defaults.slug), 62 | multiple("author", args.author ?? defaults.author), 63 | multiple("-author", args.notAuthor ?? defaults.notAuthor), 64 | single("sort", args.sort ?? defaults.sort ?? "created"), 65 | args.search ?? defaults.search // Free field that can be used to search for terms 66 | ); 67 | 68 | return query; 69 | }; 70 | -------------------------------------------------------------------------------- /src/utils/pager.ts: -------------------------------------------------------------------------------- 1 | export type PagerParams = { 2 | before?: string; 3 | after?: string; 4 | first?: number; 5 | last?: number; 6 | limit?: number; 7 | offset?: number; 8 | }; 9 | 10 | const btoa = 11 | typeof window !== "undefined" 12 | ? window.btoa 13 | : (text: string) => Buffer.from(text).toString("base64"); 14 | 15 | const cursor = (n: number) => btoa(`cursor:${n}`); 16 | 17 | export const buildPager = ( 18 | _args?: PagerParams | undefined, 19 | _defaults?: Partial | undefined 20 | ): Omit => { 21 | const args = { 22 | ...(_defaults ?? {}), 23 | ...(_args ?? {}), 24 | }; 25 | 26 | const offset = args?.offset ?? 0; 27 | 28 | return { 29 | after: args.after ?? (offset > 0 ? cursor(offset) : undefined), 30 | before: args.before, 31 | first: args.first ?? args.limit, 32 | last: args.last, 33 | }; 34 | }; 35 | -------------------------------------------------------------------------------- /tsconfig.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "rootDir": "./src" 5 | }, 6 | // unsupported by cli so need to use separate config 7 | "include": ["./src"] 8 | } 9 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es6", 4 | "module": "commonjs", 5 | "declaration": true, 6 | "moduleResolution": "node", 7 | "skipLibCheck": true, 8 | "strict": true, 9 | "forceConsistentCasingInFileNames": true, 10 | "esModuleInterop": true, 11 | "resolveJsonModule": true, 12 | "isolatedModules": true, 13 | "outDir": "./dist" 14 | } 15 | } 16 | --------------------------------------------------------------------------------