├── .eslintignore
├── .eslintrc.yml
├── .github
├── ISSUE_TEMPLATE
│ ├── bug_report.yml
│ ├── feature_improvement.yml
│ └── feature_requests.yml
├── labels.yml
└── workflows
│ ├── build.yml
│ ├── ci.yml
│ ├── labels.yml
│ ├── milestones.yml
│ └── versioning.yml
├── .gitignore
├── .prettierrc.yml
├── .release-it.json
├── CODE_OF_CONDUCT.md
├── LICENSE
├── README.md
├── __tests__
├── common.test.ts
└── properties.test.ts
├── action.yml
├── dist
└── index.js
├── jest.config.js
├── package-lock.json
├── package.json
├── src
├── action.ts
├── api-types.ts
├── common.ts
├── index.ts
├── properties.ts
└── sync.ts
└── tsconfig.json
/.eslintignore:
--------------------------------------------------------------------------------
1 | dist/
2 |
--------------------------------------------------------------------------------
/.eslintrc.yml:
--------------------------------------------------------------------------------
1 | extends:
2 | - './node_modules/gts/'
3 | rules:
4 | '@typescript-eslint/no-namespace': off
5 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/bug_report.yml:
--------------------------------------------------------------------------------
1 | name: Bug report
2 | description: Create a report to help us improve
3 | labels: [bug]
4 | body:
5 | - type: input
6 | attributes:
7 | label: Describe the bug
8 | description: A clear and concise description of what the bug is.
9 | validations:
10 | required: true
11 | - type: textarea
12 | attributes:
13 | label: Steps to reproduce
14 | description: Steps to reproduce the bug.
15 | placeholder: |
16 | 1. Go to '...'
17 | 2. Click on '....'
18 | 3. Scroll down to '....'
19 | 4. See error
20 | validations:
21 | required: true
22 | - type: textarea
23 | attributes:
24 | label: Expected behavior
25 | description: A clear and concise description of what you expected to happen.
26 | validations:
27 | required: true
28 | - type: textarea
29 | attributes:
30 | label: Additional context
31 | description: Add any other context or screenshots about the feature request here.
32 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/feature_improvement.yml:
--------------------------------------------------------------------------------
1 | name: Feature improvement
2 | description: Suggest an idea for improving an existing feature
3 | body:
4 | - type: textarea
5 | attributes:
6 | label: Is your feature improvement related to a problem? Please describe.
7 | description: A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
8 | validations:
9 | required: true
10 | - type: textarea
11 | attributes:
12 | label: Describe the solution you'd like
13 | description: A clear and concise description of what you want to happen.
14 | validations:
15 | required: true
16 | - type: textarea
17 | attributes:
18 | label: Describe alternatives you've considered
19 | description: A clear and concise description of any alternative solutions or features you've considered.
20 | - type: textarea
21 | attributes:
22 | label: Additional context
23 | description: Add any other context or screenshots about the feature request here.
24 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/feature_requests.yml:
--------------------------------------------------------------------------------
1 | name: Feature request
2 | description: Suggest an idea for this project
3 | labels: ['feature request']
4 | body:
5 | - type: textarea
6 | attributes:
7 | label: Is your feature request related to a problem? Please describe.
8 | description: A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
9 | validations:
10 | required: true
11 | - type: textarea
12 | attributes:
13 | label: Describe the solution you'd like
14 | description: A clear and concise description of what you want to happen.
15 | validations:
16 | required: true
17 | - type: textarea
18 | attributes:
19 | label: Describe alternatives you've considered
20 | description: A clear and concise description of any alternative solutions or features you've considered.
21 | - type: textarea
22 | attributes:
23 | label: Additional context
24 | description: Add any other context or screenshots about the feature request here.
25 |
--------------------------------------------------------------------------------
/.github/labels.yml:
--------------------------------------------------------------------------------
1 | # The GitHub labels to be used by this repo
2 | - name: bug
3 | description: If this is a bug
4 | color: 800000
5 | - name: security/privacy
6 | description: If it involves securing user information
7 | color: 000000
8 | - name: design
9 | description: If it needs a large amount of design
10 | color: f032e6
11 | - name: optimization
12 | description: If it's related to performance
13 | color: ffe119
14 | - name: copy
15 | description: If it's only a copy change, or needs a large amount of copy
16 | color: 469990
17 | - name: onboarding
18 | description: If it has to do with educating users on using the product
19 | color: aaffc3
20 | - name: feature request
21 | description: If it's a feature request from a user
22 | color: e6beff
23 |
--------------------------------------------------------------------------------
/.github/workflows/build.yml:
--------------------------------------------------------------------------------
1 | name: Build
2 |
3 | on:
4 | push:
5 | branches:
6 | - master
7 | workflow_dispatch:
8 |
9 | jobs:
10 | build:
11 | runs-on: ubuntu-latest
12 |
13 | steps:
14 | - name: Check out Git repository
15 | uses: actions/checkout@v2
16 | with:
17 | # Custom token to allow commits trigger other workflows.
18 | token: ${{ secrets.GITHUB_TOKEN }}
19 |
20 | - name: Set up Node.js
21 | uses: actions/setup-node@v2.1.5
22 | with:
23 | node-version: 12
24 |
25 | - name: Cache node modules
26 | uses: actions/cache@v2
27 | env:
28 | cache-name: cache-node-modules
29 | with:
30 | # npm cache files are stored in `~/.npm` on Linux/macOS
31 | path: ~/.npm
32 | key: ${{ runner.os }}-build-${{ env.cache-name }}-${{ hashFiles('**/package-lock.json') }}
33 | restore-keys: |
34 | ${{ runner.os }}-build-${{ env.cache-name }}-
35 | ${{ runner.os }}-build-
36 | ${{ runner.os }}-
37 |
38 | - name: Install Dependencies
39 | run: npm install
40 |
41 | - name: Build dist
42 | run: npm run build
43 |
44 | - name: Commit dist
45 | uses: EndBug/add-and-commit@v7
46 | with:
47 | add: "dist"
48 | author_name: github-actions[bot]
49 | author_email: github-actions[bot]@users.noreply.github.com
50 | message: "[auto] Update compiled version"
51 |
--------------------------------------------------------------------------------
/.github/workflows/ci.yml:
--------------------------------------------------------------------------------
1 | name: Test
2 |
3 | on:
4 | push:
5 | pull_request:
6 | workflow_dispatch:
7 |
8 | jobs:
9 | lint:
10 | name: Lint
11 | runs-on: ubuntu-latest
12 | steps:
13 | - uses: actions/checkout@v2
14 | - uses: actions/setup-node@v2
15 | with:
16 | node-version: '14'
17 | - run: npm ci
18 | - run: npm run lint
19 |
20 | jest:
21 | name: Jest
22 | runs-on: ubuntu-latest
23 | steps:
24 | - uses: actions/checkout@v2
25 | - uses: actions/setup-node@v2
26 | with:
27 | node-version: '14'
28 | - run: npm ci
29 | - run: npm test
30 |
31 | build:
32 | name: Build
33 | runs-on: ubuntu-latest
34 | steps:
35 | - uses: actions/checkout@v2
36 | - uses: actions/setup-node@v2
37 | with:
38 | node-version: '14'
39 | - run: npm ci
40 | - run: npm run build
41 |
--------------------------------------------------------------------------------
/.github/workflows/labels.yml:
--------------------------------------------------------------------------------
1 | name: Put labels on autopilot
2 | on:
3 | push:
4 | branches:
5 | - master
6 | paths:
7 | - .github/labels.yml
8 | jobs:
9 | build:
10 | runs-on: ubuntu-latest
11 | steps:
12 | - uses: actions/checkout@v2
13 | - uses: micnncim/action-label-syncer@v1
14 | env:
15 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
16 | with:
17 | manifest: .github/labels.yml
18 |
--------------------------------------------------------------------------------
/.github/workflows/milestones.yml:
--------------------------------------------------------------------------------
1 | name: "Milestones on autopilot"
2 | on:
3 | schedule:
4 | - cron: "*/20 * * * *"
5 |
6 | jobs:
7 | milestones:
8 | runs-on: ubuntu-latest
9 | steps:
10 | - uses: instantish/memorable-milestones@2.0.1
11 | with:
12 | repo-token: ${{ secrets.GITHUB_TOKEN }}
13 |
--------------------------------------------------------------------------------
/.github/workflows/versioning.yml:
--------------------------------------------------------------------------------
1 | name: Update major-version tags
2 |
3 | on:
4 | release:
5 | types: [published, edited]
6 |
7 | jobs:
8 | actions-tagger:
9 | runs-on: windows-latest
10 | steps:
11 | - uses: Actions-R-Us/actions-tagger@v2
12 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | /node_modules/
2 | /.idea/
3 |
--------------------------------------------------------------------------------
/.prettierrc.yml:
--------------------------------------------------------------------------------
1 | singleQuote: true
2 | semi: true
3 | trailingComma: es5
4 | printWidth: 100
5 | arrowParens: avoid
6 | bracketSpacing: false
7 |
--------------------------------------------------------------------------------
/.release-it.json:
--------------------------------------------------------------------------------
1 | {
2 | "git": {
3 | "commitMessage": "chore: release v${version}"
4 | },
5 | "github": {
6 | "release": true
7 | },
8 | "npm": {
9 | "publish": false
10 | }
11 | }
12 |
13 |
--------------------------------------------------------------------------------
/CODE_OF_CONDUCT.md:
--------------------------------------------------------------------------------
1 | # Contributor Covenant Code of Conduct
2 |
3 | ## Our Pledge
4 |
5 | We as members, contributors, and leaders pledge to make participation in our
6 | community a harassment-free experience for everyone, regardless of age, body
7 | size, visible or invisible disability, ethnicity, sex characteristics, gender
8 | identity and expression, level of experience, education, socio-economic status,
9 | nationality, personal appearance, race, religion, or sexual identity
10 | and orientation.
11 |
12 | We pledge to act and interact in ways that contribute to an open, welcoming,
13 | diverse, inclusive, and healthy community.
14 |
15 | ## Our Standards
16 |
17 | Examples of behavior that contributes to a positive environment for our
18 | community include:
19 |
20 | * Demonstrating empathy and kindness toward other people
21 | * Being respectful of differing opinions, viewpoints, and experiences
22 | * Giving and gracefully accepting constructive feedback
23 | * Accepting responsibility and apologizing to those affected by our mistakes,
24 | and learning from the experience
25 | * Focusing on what is best not just for us as individuals, but for the
26 | overall community
27 |
28 | Examples of unacceptable behavior include:
29 |
30 | * The use of sexualized language or imagery, and sexual attention or
31 | advances of any kind
32 | * Trolling, insulting or derogatory comments, and personal or political attacks
33 | * Public or private harassment
34 | * Publishing others' private information, such as a physical or email
35 | address, without their explicit permission
36 | * Other conduct which could reasonably be considered inappropriate in a
37 | professional setting
38 |
39 | ## Enforcement Responsibilities
40 |
41 | Community leaders are responsible for clarifying and enforcing our standards of
42 | acceptable behavior and will take appropriate and fair corrective action in
43 | response to any behavior that they deem inappropriate, threatening, offensive,
44 | or harmful.
45 |
46 | Community leaders have the right and responsibility to remove, edit, or reject
47 | comments, commits, code, wiki edits, issues, and other contributions that are
48 | not aligned to this Code of Conduct, and will communicate reasons for moderation
49 | decisions when appropriate.
50 |
51 | ## Scope
52 |
53 | This Code of Conduct applies within all community spaces, and also applies when
54 | an individual is officially representing the community in public spaces.
55 | Examples of representing our community include using an official e-mail address,
56 | posting via an official social media account, or acting as an appointed
57 | representative at an online or offline event.
58 |
59 | ## Enforcement
60 |
61 | Instances of abusive, harassing, or otherwise unacceptable behavior may be
62 | reported to the community leaders responsible for enforcement at
63 | support@itsinstantish.com.
64 | All complaints will be reviewed and investigated promptly and fairly.
65 |
66 | All community leaders are obligated to respect the privacy and security of the
67 | reporter of any incident.
68 |
69 | ## Enforcement Guidelines
70 |
71 | Community leaders will follow these Community Impact Guidelines in determining
72 | the consequences for any action they deem in violation of this Code of Conduct:
73 |
74 | ### 1. Correction
75 |
76 | **Community Impact**: Use of inappropriate language or other behavior deemed
77 | unprofessional or unwelcome in the community.
78 |
79 | **Consequence**: A private, written warning from community leaders, providing
80 | clarity around the nature of the violation and an explanation of why the
81 | behavior was inappropriate. A public apology may be requested.
82 |
83 | ### 2. Warning
84 |
85 | **Community Impact**: A violation through a single incident or series
86 | of actions.
87 |
88 | **Consequence**: A warning with consequences for continued behavior. No
89 | interaction with the people involved, including unsolicited interaction with
90 | those enforcing the Code of Conduct, for a specified period of time. This
91 | includes avoiding interactions in community spaces as well as external channels
92 | like social media. Violating these terms may lead to a temporary or
93 | permanent ban.
94 |
95 | ### 3. Temporary Ban
96 |
97 | **Community Impact**: A serious violation of community standards, including
98 | sustained inappropriate behavior.
99 |
100 | **Consequence**: A temporary ban from any sort of interaction or public
101 | communication with the community for a specified period of time. No public or
102 | private interaction with the people involved, including unsolicited interaction
103 | with those enforcing the Code of Conduct, is allowed during this period.
104 | Violating these terms may lead to a permanent ban.
105 |
106 | ### 4. Permanent Ban
107 |
108 | **Community Impact**: Demonstrating a pattern of violation of community
109 | standards, including sustained inappropriate behavior, harassment of an
110 | individual, or aggression toward or disparagement of classes of individuals.
111 |
112 | **Consequence**: A permanent ban from any sort of public interaction within
113 | the community.
114 |
115 | ## Attribution
116 |
117 | This Code of Conduct is adapted from the [Contributor Covenant][homepage],
118 | version 2.0, available at
119 | https://www.contributor-covenant.org/version/2/0/code_of_conduct.html.
120 |
121 | Community Impact Guidelines were inspired by [Mozilla's code of conduct
122 | enforcement ladder](https://github.com/mozilla/diversity).
123 |
124 | [homepage]: https://www.contributor-covenant.org
125 |
126 | For answers to common questions about this code of conduct, see the FAQ at
127 | https://www.contributor-covenant.org/faq. Translations are available at
128 | https://www.contributor-covenant.org/translations.
129 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2021 Instantish, Inc.
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 | # Notion x GitHub Action
2 |
3 | [](https://github.com/google/gts)
4 |
5 | Connect your GitHub issues to a Notion database.
6 |
7 | **Like this GitHub Action?** Give us a ⭐️ and [follow us on Twitter for more drops 🪂](https://twitter.com/tryfabric).
8 |
9 | ---
10 |
11 | ## Quick Start
12 |
13 | 1. [Create a new internal Notion integration](https://www.notion.so/my-integrations) and note the value of the Internal Integration Token.
14 | 2. In your GitHub repository, go to `Settings` > `Secrets`, and add a `New repository secret`. Set the `Name` to `NOTION_TOKEN` and the `Value` to the Internal Integration Token you created in the previous step.
15 | 3. Set up your Notion Database. Use [this template](https://www.notion.so/tryfabric/067329ee12044ec49b7db6b39b26920a?v=38dd7e968874436c810a5e604d2cf12c) and duplicate it to your workspace.
16 | 4. In your Notion Database page's `Share` menu, add the Notion integration you created as a member with the `Can edit` privilege. You may have to type your integration's name in the `Invite` field.
17 | 5. Find the ID of your Database by copying the link to it. The link will have the format
18 |
19 | ```
20 | https://www.notion.so/abc?v=123
21 | ```
22 |
23 | where `abc` is the database id.
24 |
25 | 6. Add the Database's ID as a repository secret for your GitHub repository. Set the `Name` to `NOTION_DATABASE` and the `Value` to the id of your Database.
26 |
27 | 7. In your GitHub repository, create a GitHub workflow file at the path `.github/workflows/issues-notion-sync.yml`.
28 |
29 | ```yaml
30 | name: Notion Sync
31 |
32 | on:
33 | workflow_dispatch:
34 | issues:
35 | types:
36 | [
37 | opened,
38 | edited,
39 | labeled,
40 | unlabeled,
41 | assigned,
42 | unassigned,
43 | milestoned,
44 | demilestoned,
45 | reopened,
46 | closed,
47 | ]
48 |
49 | jobs:
50 | notion_job:
51 | runs-on: ubuntu-latest
52 | name: Add GitHub Issues to Notion
53 | steps:
54 | - name: Add GitHub Issues to Notion
55 | uses: tryfabric/notion-github-action@v1
56 | with:
57 | notion-token: ${{ secrets.NOTION_TOKEN }}
58 | notion-db: ${{ secrets.NOTION_DATABASE }}
59 | ```
60 |
61 | 8. (Optional) If your Github repository has any preexisting issues that you would like to sync to your new Notion Database you can trigger a manual workflow. Make sure your organization's default `GITHUB_TOKEN` has [read and write permissions](https://docs.github.com/en/organizations/managing-organization-settings/disabling-or-limiting-github-actions-for-your-organization#setting-the-permissions-of-the-github_token-for-your-organization) then follow [these intructions](https://docs.github.com/en/actions/managing-workflow-runs/manually-running-a-workflow) to run the `Notion Job` workflow.
62 |
63 | _Note: The manual workflow will only work on Notion Databases created from the templated linked above._
64 |
65 | ## Using `release-it`
66 |
67 | 1. Locally, on `master` (make sure it's up to date), execute `GITHUB_TOKEN= release-it`. (Alternatively, set `GITHUB_TOKEN` as a system environment variable)
68 | 2. Follow the interactive prompts, selecting `Yes` for all options.
69 | 3. When selecting the increment, choose `patch` when the release is only bug fixes. For new features, choose `minor`. For major changes, choose `major`.
70 |
71 | Release-It will then automatically generate a GitHub release with the changelog inside.
72 |
73 | ---
74 |
75 | Built with 💙 by the team behind [Fabric](https://tryfabric.com).
76 |
--------------------------------------------------------------------------------
/__tests__/common.test.ts:
--------------------------------------------------------------------------------
1 | import {common, RICH_TEXT_CONTENT_CHARACTERS_LIMIT} from '../src/common';
2 |
3 | describe('richText', () => {
4 | const res = common.richText(Array(2000).fill('a').join(''), {
5 | annotations: {
6 | // @ts-expect-error test flag
7 | color: 'custom',
8 | },
9 | url: 'abc',
10 | })[0];
11 |
12 | it('should have have a proper format', () => {
13 | expect(res.type).toBe('text');
14 | expect(typeof res.annotations).toBe('object');
15 | expect(res.annotations?.color).toBe('custom');
16 | if ('text' in res) {
17 | expect(typeof res.text).toBe('object');
18 | expect(typeof res.text.content).toBe('string');
19 | expect(typeof res.text.link).toBe('object');
20 | expect(res.text.link?.url).toBe('abc');
21 | } else fail('res did not contain the "text" key');
22 | });
23 |
24 | it('should have truncated long text', () => {
25 | if ('text' in res) expect(res.text.content.length).toBe(RICH_TEXT_CONTENT_CHARACTERS_LIMIT);
26 | else fail('res did not contain the "text" key');
27 | });
28 | });
29 |
--------------------------------------------------------------------------------
/__tests__/properties.test.ts:
--------------------------------------------------------------------------------
1 | import {RichTextItemRequest} from '../src/api-types';
2 | import {RICH_TEXT_CONTENT_CHARACTERS_LIMIT} from '../src/common';
3 | import {properties} from '../src/properties';
4 |
5 | describe('text', () => {
6 | it('should convert a string to RichText', () => {
7 | const res = properties.text('abc');
8 |
9 | expect(typeof res).toBe('object');
10 | expect(res.type).toBe('rich_text');
11 | expect(res.rich_text).toBeInstanceOf(Array);
12 | expect(res.rich_text.length).toBe(1);
13 | expect(res.rich_text[0].type).toBe('text');
14 | });
15 |
16 | it('should truncate long strings', () => {
17 | const veryLongString = Array(2000).fill('a').join('');
18 | const res = properties.text(veryLongString);
19 |
20 | expect(typeof res).toBe('object');
21 | expect(res.type).toBe('rich_text');
22 | expect(res.rich_text).toBeInstanceOf(Array);
23 | expect(res.rich_text.length).toBe(1);
24 | if ('text' in res.rich_text[0])
25 | expect(res.rich_text[0].text.content.length).toBe(RICH_TEXT_CONTENT_CHARACTERS_LIMIT);
26 | else fail('res.rich_text[0] did not contain the "text" key');
27 | });
28 | });
29 |
30 | describe('richText', () => {
31 | it('should correctly handle rich text', () => {
32 | const annotations = {
33 | bold: false,
34 | strikethrough: false,
35 | underline: false,
36 | italic: false,
37 | code: false,
38 | color: 'default',
39 | } as const;
40 | const input: RichTextItemRequest[] = [
41 | {
42 | type: 'text',
43 | text: {
44 | content: 'abc',
45 | },
46 | annotations,
47 | },
48 | {
49 | type: 'equation',
50 | annotations,
51 | equation: {
52 | expression: 'abc',
53 | },
54 | },
55 | ];
56 |
57 | const res = properties.richText(input);
58 | expect(typeof res).toBe('object');
59 | expect(res.type).toBe('rich_text');
60 | expect(res.rich_text).toBeInstanceOf(Array);
61 | expect(res.rich_text.length).toBe(2);
62 | expect(res.rich_text[0].type).toBe('text');
63 | expect(res.rich_text[1].type).toBe('equation');
64 | });
65 | });
66 |
67 | describe('title', () => {
68 | it('should convert a string to a Notion title', () => {
69 | const res = properties.title('abc');
70 |
71 | expect(typeof res).toBe('object');
72 | expect(res.type).toBe('title');
73 | expect(res.title).toBeInstanceOf(Array);
74 | expect(res.title.length).toBe(1);
75 | expect(res.title[0].type).toBe('text');
76 | if ('text' in res.title[0]) expect(res.title[0].text.content).toBe('abc');
77 | else fail('res.rich_text[0] did not contain the "text" key');
78 | });
79 | });
80 |
81 | describe('number', () => {
82 | it('should convert a number to a Notion number', () => {
83 | const res = properties.number(123);
84 |
85 | expect(typeof res).toBe('object');
86 | expect(res.type).toBe('number');
87 | expect(typeof res.number).toBe('number');
88 | });
89 | });
90 |
91 | describe('date', () => {
92 | it('should convert a string to a Notion date', () => {
93 | const res = properties.date('abc');
94 |
95 | expect(typeof res).toBe('object');
96 | expect(res.type).toBe('date');
97 | expect(typeof res.date).toBe('object');
98 | expect(res.date?.start).toBe('abc');
99 | });
100 | });
101 |
102 | describe('getStatusSelectOption', () => {
103 | for (const status of ['open', 'closed'] as const) {
104 | it(`should return a select for ${status}`, () => {
105 | const res = properties.getStatusSelectOption(status);
106 |
107 | expect(typeof res).toBe('object');
108 | expect(res.type).toBe('select');
109 | expect(typeof res.select).toBe('object');
110 | expect(typeof res.select?.name).toBe('string');
111 | expect(typeof res.select?.color).toBe('string');
112 | });
113 | }
114 | });
115 |
116 | describe('select', () => {
117 | it('should correctly create the desidered Select element', () => {
118 | const res = properties.select('abc', 'default');
119 |
120 | expect(typeof res).toBe('object');
121 | expect(res.type).toBe('select');
122 | expect(typeof res.select).toBe('object');
123 | expect(res.select?.name).toBe('abc');
124 | expect(res.select?.color).toBe('default');
125 | });
126 | });
127 |
128 | describe('multiSelect', () => {
129 | it('should correctly create the desidered multi_select element', () => {
130 | const arr = ['first', 'second', 'third'];
131 | const res = properties.multiSelect(arr);
132 |
133 | expect(typeof res).toBe('object');
134 | expect(res.type).toBe('multi_select');
135 | expect(res.multi_select).toBeInstanceOf(Array);
136 | arr.forEach(element => {
137 | expect(res.multi_select).toContainEqual({name: element});
138 | });
139 | });
140 | });
141 |
142 | describe('url', () => {
143 | it('should correctly create the desidered url element', () => {
144 | const res = properties.url('abc');
145 |
146 | expect(typeof res).toBe('object');
147 | expect(res.type).toBe('url');
148 | expect(res.url).toBe('abc');
149 | });
150 | });
151 |
--------------------------------------------------------------------------------
/action.yml:
--------------------------------------------------------------------------------
1 | name: 'Notion x GitHub Action'
2 | description: 'Sync GitHub issues to a Notion database'
3 | inputs:
4 | notion-token:
5 | description: 'Your Notion API Token'
6 | required: true
7 | notion-db:
8 | description: 'The Notion database id'
9 | required: true
10 | github-token:
11 | description: 'Your GitHub personal access token'
12 | required: false
13 | default: ${{ github.token }}
14 |
15 | runs:
16 | using: 'node12'
17 | main: 'dist/index.js'
18 | branding:
19 | icon: zap
20 | color: gray-dark
21 |
--------------------------------------------------------------------------------
/jest.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | clearMocks: true,
3 | moduleFileExtensions: ['js', 'ts'],
4 | testEnvironment: 'node',
5 | testMatch: ['**/*.test.ts'],
6 | testRunner: 'jest-circus/runner',
7 | transform: {
8 | '^.+\\.ts$': 'ts-jest',
9 | },
10 | verbose: true,
11 | };
12 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@instantish/notion-github-action",
3 | "version": "1.2.3",
4 | "private": true,
5 | "description": "A GitHub Action that syncs issues to a Notion database. Multi-repository friendly.",
6 | "main": "dist/index.js",
7 | "scripts": {
8 | "test": "jest",
9 | "build": "ncc build src/index.ts",
10 | "lint": "gts lint src/*",
11 | "release": "release-it"
12 | },
13 | "dependencies": {
14 | "@actions/core": "^1.4.0",
15 | "@actions/github": "^5.0.0",
16 | "@notionhq/client": "^0.4.13",
17 | "@octokit/webhooks-definitions": "^3.67.3",
18 | "@tryfabric/martian": "^1.1.1",
19 | "octokit": "^1.1.0"
20 | },
21 | "devDependencies": {
22 | "@types/jest": "^27.0.3",
23 | "@types/node": "^15.12.2",
24 | "@vercel/ncc": "^0.28.6",
25 | "gts": "^3.1.0",
26 | "jest": "^27.4.5",
27 | "jest-circus": "^27.4.5",
28 | "prettier": "^2.3.1",
29 | "release-it": "^14.11.6",
30 | "ts-jest": "^27.1.2",
31 | "typescript": "^4.3.2"
32 | },
33 | "author": "Richard Robinson",
34 | "license": "MIT",
35 | "keywords": [
36 | "notion",
37 | "github",
38 | "issues",
39 | "issue-management",
40 | "notion-api"
41 | ],
42 | "repository": {
43 | "type": "git",
44 | "url": "git+https://github.com/instantish/notion-github-action.git"
45 | },
46 | "bugs": {
47 | "url": "https://github.com/instantish/notion-github-action/issues"
48 | },
49 | "homepage": "https://github.com/instantish/notion-github-action#readme"
50 | }
51 |
--------------------------------------------------------------------------------
/src/action.ts:
--------------------------------------------------------------------------------
1 | import {Client, LogLevel} from '@notionhq/client/build/src';
2 | import * as core from '@actions/core';
3 | import type {IssuesEvent, IssuesOpenedEvent} from '@octokit/webhooks-definitions/schema';
4 | import type {WebhookPayload} from '@actions/github/lib/interfaces';
5 | import {CustomValueMap, properties} from './properties';
6 | import {createIssueMapping, syncNotionDBWithGitHub} from './sync';
7 | import {Octokit} from 'octokit';
8 | import {markdownToRichText} from '@tryfabric/martian';
9 | import {CustomTypes, RichTextItemResponse} from './api-types';
10 | import {CreatePageParameters} from '@notionhq/client/build/src/api-endpoints';
11 |
12 | function removeHTML(text?: string): string {
13 | return text?.replace(/<.*>.*<\/.*>/g, '') ?? '';
14 | }
15 |
16 | interface PayloadParsingOptions {
17 | payload: IssuesEvent;
18 | octokit: Octokit;
19 | possibleProject?: ProjectData;
20 | }
21 | async function parsePropertiesFromPayload(options: PayloadParsingOptions): Promise {
22 | const {payload, octokit, possibleProject} = options;
23 |
24 | payload.issue.labels?.map(label => label.color);
25 |
26 | const projectData = await getProjectData({
27 | octokit,
28 | githubRepo: payload.repository.full_name,
29 | issueNumber: payload.issue.number,
30 | possible: possibleProject,
31 | });
32 |
33 | core.debug(`Current project data: ${JSON.stringify(projectData, null, 2)}`);
34 |
35 | const result: CustomValueMap = {
36 | Name: properties.title(payload.issue.title),
37 | Status: properties.getStatusSelectOption(payload.issue.state!),
38 | Organization: properties.text(payload.organization?.login ?? ''),
39 | Repository: properties.text(payload.repository.name),
40 | Number: properties.number(payload.issue.number),
41 | Body: properties.richText(parseBodyRichText(payload.issue.body)),
42 | Assignees: properties.multiSelect(payload.issue.assignees.map(assignee => assignee.login)),
43 | Milestone: properties.text(payload.issue.milestone?.title ?? ''),
44 | Labels: properties.multiSelect(payload.issue.labels?.map(label => label.name) ?? []),
45 | Author: properties.text(payload.issue.user.login),
46 | Created: properties.date(payload.issue.created_at),
47 | Updated: properties.date(payload.issue.updated_at),
48 | ID: properties.number(payload.issue.id),
49 | Link: properties.url(payload.issue.html_url),
50 | Project: properties.text(projectData?.name || ''),
51 | 'Project Column': properties.text(projectData?.columnName || ''),
52 | };
53 |
54 | return result;
55 | }
56 |
57 | interface ProjectData {
58 | name?: string;
59 | columnName?: string;
60 | }
61 | interface GetProjectDataOptions {
62 | octokit: Octokit;
63 | githubRepo: string;
64 | issueNumber: number;
65 | possible?: ProjectData;
66 | }
67 | export async function getProjectData(
68 | options: GetProjectDataOptions
69 | ): Promise {
70 | const {octokit, githubRepo, issueNumber, possible} = options;
71 |
72 | const projects =
73 | (
74 | await octokit.rest.projects.listForRepo({
75 | owner: githubRepo.split('/')[0],
76 | repo: githubRepo.split('/')[1],
77 | })
78 | ).data || [];
79 | projects.sort(p => (p.name === possible?.name ? -1 : 1));
80 |
81 | core.debug(`Found ${projects.length} projects.`);
82 |
83 | for (const project of projects) {
84 | const columns = (await octokit.rest.projects.listColumns({project_id: project.id})).data || [];
85 | if (possible?.name === project.name)
86 | columns.sort(c => (c.name === possible.columnName ? -1 : 1));
87 |
88 | for (const column of columns) {
89 | const cards = (await octokit.rest.projects.listCards({column_id: column.id})).data,
90 | card =
91 | cards && cards.find(c => Number(c.content_url?.split('/issues/')[1]) === issueNumber);
92 |
93 | if (card)
94 | return {
95 | name: project.name,
96 | columnName: column.name,
97 | };
98 | }
99 | }
100 |
101 | return undefined;
102 | }
103 |
104 | export function parseBodyRichText(body: string) {
105 | try {
106 | return markdownToRichText(removeHTML(body)) as CustomTypes.RichText['rich_text'];
107 | } catch {
108 | return [];
109 | }
110 | }
111 |
112 | function getBodyChildrenBlocks(body: string): Exclude {
113 | // We're currently using only one paragraph block, but this could be extended to multiple kinds of blocks.
114 | return [
115 | {
116 | type: 'paragraph',
117 | paragraph: {
118 | text: parseBodyRichText(body),
119 | },
120 | },
121 | ];
122 | }
123 |
124 | interface IssueOpenedOptions {
125 | notion: {
126 | client: Client;
127 | databaseId: string;
128 | };
129 | payload: IssuesOpenedEvent;
130 | octokit: Octokit;
131 | }
132 |
133 | async function handleIssueOpened(options: IssueOpenedOptions) {
134 | const {notion, payload} = options;
135 |
136 | core.info(`Creating page for issue #${payload.issue.number}`);
137 |
138 | await notion.client.pages.create({
139 | parent: {
140 | database_id: notion.databaseId,
141 | },
142 | properties: await parsePropertiesFromPayload({
143 | payload,
144 | octokit: options.octokit,
145 | }),
146 | children: getBodyChildrenBlocks(payload.issue.body),
147 | });
148 | }
149 |
150 | interface IssueEditedOptions {
151 | notion: {
152 | client: Client;
153 | databaseId: string;
154 | };
155 | payload: IssuesEvent;
156 | octokit: Octokit;
157 | }
158 |
159 | async function handleIssueEdited(options: IssueEditedOptions) {
160 | const {notion, payload, octokit} = options;
161 |
162 | core.info(`Querying database for page with github id ${payload.issue.id}`);
163 |
164 | const query = await notion.client.databases.query({
165 | database_id: notion.databaseId,
166 | filter: {
167 | property: 'ID',
168 | number: {
169 | equals: payload.issue.id,
170 | },
171 | },
172 | page_size: 1,
173 | });
174 |
175 | const bodyBlocks = getBodyChildrenBlocks(payload.issue.body);
176 |
177 | if (query.results.length > 0) {
178 | const pageId = query.results[0].id;
179 |
180 | core.info(`Query successful: Page ${pageId}`);
181 | core.info(`Updating page for issue #${payload.issue.number}`);
182 |
183 | await notion.client.pages.update({
184 | page_id: pageId,
185 | properties: await parsePropertiesFromPayload({payload, octokit}),
186 | });
187 |
188 | const existingBlocks = (
189 | await notion.client.blocks.children.list({
190 | block_id: pageId,
191 | })
192 | ).results;
193 |
194 | const overlap = Math.min(bodyBlocks.length, existingBlocks.length);
195 |
196 | await Promise.all(
197 | bodyBlocks.slice(0, overlap).map((block, index) =>
198 | notion.client.blocks.update({
199 | block_id: existingBlocks[index].id,
200 | ...block,
201 | })
202 | )
203 | );
204 |
205 | if (bodyBlocks.length > existingBlocks.length) {
206 | await notion.client.blocks.children.append({
207 | block_id: pageId,
208 | children: bodyBlocks.slice(overlap),
209 | });
210 | } else if (bodyBlocks.length < existingBlocks.length) {
211 | await Promise.all(
212 | existingBlocks
213 | .slice(overlap)
214 | .map(block => notion.client.blocks.delete({block_id: block.id}))
215 | );
216 | }
217 | } else {
218 | core.warning(`Could not find page with github id ${payload.issue.id}, creating a new one`);
219 |
220 | await notion.client.pages.create({
221 | parent: {
222 | database_id: notion.databaseId,
223 | },
224 | properties: await parsePropertiesFromPayload({payload, octokit}),
225 | children: bodyBlocks,
226 | });
227 | }
228 |
229 | // eslint-disable-next-line @typescript-eslint/no-explicit-any
230 | const result = query.results[0] as any,
231 | pageId = result.id,
232 | possible: ProjectData | undefined = result
233 | ? {
234 | name: ((result.properties as CustomValueMap).Project.rich_text[0] as RichTextItemResponse)
235 | ?.plain_text,
236 | columnName: (
237 | (result.properties as CustomValueMap)['Project Column']
238 | .rich_text[0] as RichTextItemResponse
239 | )?.plain_text,
240 | }
241 | : undefined;
242 |
243 | core.info(`Query successful: Page ${pageId}`);
244 | core.info(`Updating page for issue #${payload.issue.number}`);
245 |
246 | await notion.client.pages.update({
247 | page_id: pageId,
248 | properties: await parsePropertiesFromPayload({
249 | payload,
250 | octokit: options.octokit,
251 | possibleProject: possible,
252 | }),
253 | });
254 | }
255 |
256 | interface Options {
257 | notion: {
258 | token: string;
259 | databaseId: string;
260 | };
261 | github: {
262 | payload: WebhookPayload;
263 | eventName: string;
264 | token: string;
265 | };
266 | }
267 |
268 | export async function run(options: Options) {
269 | const {notion, github} = options;
270 |
271 | core.info('Starting...');
272 |
273 | const notionClient = new Client({
274 | auth: notion.token,
275 | logLevel: core.isDebug() ? LogLevel.DEBUG : LogLevel.WARN,
276 | });
277 | const octokit = new Octokit({auth: github.token});
278 |
279 | if (github.payload.action === 'opened') {
280 | await handleIssueOpened({
281 | notion: {
282 | client: notionClient,
283 | databaseId: notion.databaseId,
284 | },
285 | payload: github.payload as IssuesOpenedEvent,
286 | octokit,
287 | });
288 | } else if (github.eventName === 'workflow_dispatch') {
289 | const notion = new Client({auth: options.notion.token});
290 | const {databaseId} = options.notion;
291 | const issuePageIds = await createIssueMapping(notion, databaseId);
292 | if (!github.payload.repository?.full_name) {
293 | throw new Error('Unable to find repository name in github webhook context');
294 | }
295 | const githubRepo = github.payload.repository.full_name;
296 | await syncNotionDBWithGitHub(issuePageIds, octokit, notion, databaseId, githubRepo);
297 | } else {
298 | await handleIssueEdited({
299 | notion: {
300 | client: notionClient,
301 | databaseId: notion.databaseId,
302 | },
303 | payload: github.payload as IssuesEvent,
304 | octokit,
305 | });
306 | }
307 |
308 | core.info('Complete!');
309 | }
310 |
--------------------------------------------------------------------------------
/src/api-types.ts:
--------------------------------------------------------------------------------
1 | export namespace CustomTypes {
2 | export type RichText = {
3 | rich_text: Array;
4 | type?: 'rich_text';
5 | };
6 | export type Title = {
7 | title: Array;
8 | type?: 'title';
9 | };
10 | export type Number = {
11 | number: number | null;
12 | type?: 'number';
13 | };
14 | export type Date = {
15 | date: DateRequest | null;
16 | type?: 'date';
17 | };
18 | export type Select = {
19 | select:
20 | | {
21 | id: StringRequest;
22 | name?: StringRequest;
23 | color?: SelectColor;
24 | }
25 | | null
26 | | {
27 | name: StringRequest;
28 | id?: StringRequest;
29 | color?: SelectColor;
30 | }
31 | | null;
32 | type?: 'select';
33 | };
34 | export type MultiSelect = {
35 | multi_select: Array<
36 | | {
37 | id: StringRequest;
38 | name?: StringRequest;
39 | color?: SelectColor;
40 | }
41 | | {
42 | name: StringRequest;
43 | id?: StringRequest;
44 | color?: SelectColor;
45 | }
46 | >;
47 | type?: 'multi_select';
48 | };
49 | export type URL = {
50 | url: TextRequest | null;
51 | type?: 'url';
52 | };
53 | }
54 |
55 | export type RichTextItemRequest =
56 | | {
57 | text: {
58 | content: string;
59 | link?: {
60 | url: TextRequest;
61 | } | null;
62 | };
63 | type?: 'text';
64 | annotations?: {
65 | bold?: boolean;
66 | italic?: boolean;
67 | strikethrough?: boolean;
68 | underline?: boolean;
69 | code?: boolean;
70 | color?:
71 | | 'default'
72 | | 'gray'
73 | | 'brown'
74 | | 'orange'
75 | | 'yellow'
76 | | 'green'
77 | | 'blue'
78 | | 'purple'
79 | | 'pink'
80 | | 'red'
81 | | 'gray_background'
82 | | 'brown_background'
83 | | 'orange_background'
84 | | 'yellow_background'
85 | | 'green_background'
86 | | 'blue_background'
87 | | 'purple_background'
88 | | 'pink_background'
89 | | 'red_background';
90 | };
91 | }
92 | | {
93 | mention:
94 | | {
95 | user:
96 | | {
97 | id: IdRequest;
98 | }
99 | | {
100 | person: {
101 | email?: string;
102 | };
103 | id: IdRequest;
104 | type?: 'person';
105 | name?: string | null;
106 | avatar_url?: string | null;
107 | object?: 'user';
108 | }
109 | | {
110 | bot:
111 | | EmptyObject
112 | | {
113 | owner:
114 | | {
115 | type: 'user';
116 | user:
117 | | {
118 | type: 'person';
119 | person: {
120 | email: string;
121 | };
122 | name: string | null;
123 | avatar_url: string | null;
124 | id: IdRequest;
125 | object: 'user';
126 | }
127 | | {
128 | id: IdRequest;
129 | object: 'user';
130 | };
131 | }
132 | | {
133 | type: 'workspace';
134 | workspace: true;
135 | };
136 | };
137 | id: IdRequest;
138 | type?: 'bot';
139 | name?: string | null;
140 | avatar_url?: string | null;
141 | object?: 'user';
142 | };
143 | }
144 | | {
145 | date: DateRequest;
146 | }
147 | | {
148 | page: {
149 | id: IdRequest;
150 | };
151 | }
152 | | {
153 | database: {
154 | id: IdRequest;
155 | };
156 | };
157 | type?: 'mention';
158 | annotations?: {
159 | bold?: boolean;
160 | italic?: boolean;
161 | strikethrough?: boolean;
162 | underline?: boolean;
163 | code?: boolean;
164 | color?:
165 | | 'default'
166 | | 'gray'
167 | | 'brown'
168 | | 'orange'
169 | | 'yellow'
170 | | 'green'
171 | | 'blue'
172 | | 'purple'
173 | | 'pink'
174 | | 'red'
175 | | 'gray_background'
176 | | 'brown_background'
177 | | 'orange_background'
178 | | 'yellow_background'
179 | | 'green_background'
180 | | 'blue_background'
181 | | 'purple_background'
182 | | 'pink_background'
183 | | 'red_background';
184 | };
185 | }
186 | | {
187 | equation: {
188 | expression: TextRequest;
189 | };
190 | type?: 'equation';
191 | annotations?: {
192 | bold?: boolean;
193 | italic?: boolean;
194 | strikethrough?: boolean;
195 | underline?: boolean;
196 | code?: boolean;
197 | color?:
198 | | 'default'
199 | | 'gray'
200 | | 'brown'
201 | | 'orange'
202 | | 'yellow'
203 | | 'green'
204 | | 'blue'
205 | | 'purple'
206 | | 'pink'
207 | | 'red'
208 | | 'gray_background'
209 | | 'brown_background'
210 | | 'orange_background'
211 | | 'yellow_background'
212 | | 'green_background'
213 | | 'blue_background'
214 | | 'purple_background'
215 | | 'pink_background'
216 | | 'red_background';
217 | };
218 | };
219 |
220 | export type RichTextItemResponse =
221 | | {
222 | type: 'text';
223 | text: {
224 | content: string;
225 | link: {
226 | url: TextRequest;
227 | } | null;
228 | };
229 | annotations: {
230 | bold: boolean;
231 | italic: boolean;
232 | strikethrough: boolean;
233 | underline: boolean;
234 | code: boolean;
235 | color:
236 | | 'default'
237 | | 'gray'
238 | | 'brown'
239 | | 'orange'
240 | | 'yellow'
241 | | 'green'
242 | | 'blue'
243 | | 'purple'
244 | | 'pink'
245 | | 'red'
246 | | 'gray_background'
247 | | 'brown_background'
248 | | 'orange_background'
249 | | 'yellow_background'
250 | | 'green_background'
251 | | 'blue_background'
252 | | 'purple_background'
253 | | 'pink_background'
254 | | 'red_background';
255 | };
256 | plain_text: string;
257 | href: string | null;
258 | }
259 | | {
260 | type: 'mention';
261 | mention:
262 | | {
263 | type: 'user';
264 | user: PartialUserObjectResponse;
265 | }
266 | | {
267 | type: 'date';
268 | date: DateResponse;
269 | }
270 | | {
271 | type: 'link_preview';
272 | link_preview: {
273 | url: TextRequest;
274 | };
275 | }
276 | | {
277 | type: 'page';
278 | page: {
279 | id: IdRequest;
280 | };
281 | }
282 | | {
283 | type: 'database';
284 | database: {
285 | id: IdRequest;
286 | };
287 | };
288 | annotations: {
289 | bold: boolean;
290 | italic: boolean;
291 | strikethrough: boolean;
292 | underline: boolean;
293 | code: boolean;
294 | color:
295 | | 'default'
296 | | 'gray'
297 | | 'brown'
298 | | 'orange'
299 | | 'yellow'
300 | | 'green'
301 | | 'blue'
302 | | 'purple'
303 | | 'pink'
304 | | 'red'
305 | | 'gray_background'
306 | | 'brown_background'
307 | | 'orange_background'
308 | | 'yellow_background'
309 | | 'green_background'
310 | | 'blue_background'
311 | | 'purple_background'
312 | | 'pink_background'
313 | | 'red_background';
314 | };
315 | plain_text: string;
316 | href: string | null;
317 | }
318 | | {
319 | type: 'equation';
320 | equation: {
321 | expression: TextRequest;
322 | };
323 | annotations: {
324 | bold: boolean;
325 | italic: boolean;
326 | strikethrough: boolean;
327 | underline: boolean;
328 | code: boolean;
329 | color:
330 | | 'default'
331 | | 'gray'
332 | | 'brown'
333 | | 'orange'
334 | | 'yellow'
335 | | 'green'
336 | | 'blue'
337 | | 'purple'
338 | | 'pink'
339 | | 'red'
340 | | 'gray_background'
341 | | 'brown_background'
342 | | 'orange_background'
343 | | 'yellow_background'
344 | | 'green_background'
345 | | 'blue_background'
346 | | 'purple_background'
347 | | 'pink_background'
348 | | 'red_background';
349 | };
350 | plain_text: string;
351 | href: string | null;
352 | };
353 |
354 | declare type UserObjectResponse =
355 | | {
356 | type: 'person';
357 | person: {
358 | email?: string;
359 | };
360 | name: string | null;
361 | avatar_url: string | null;
362 | id: IdRequest;
363 | object: 'user';
364 | }
365 | | {
366 | type: 'bot';
367 | bot:
368 | | EmptyObject
369 | | {
370 | owner:
371 | | {
372 | type: 'user';
373 | user:
374 | | {
375 | type: 'person';
376 | person: {
377 | email: string;
378 | };
379 | name: string | null;
380 | avatar_url: string | null;
381 | id: IdRequest;
382 | object: 'user';
383 | }
384 | | {
385 | id: IdRequest;
386 | object: 'user';
387 | };
388 | }
389 | | {
390 | type: 'workspace';
391 | workspace: true;
392 | };
393 | };
394 | name: string | null;
395 | avatar_url: string | null;
396 | id: IdRequest;
397 | object: 'user';
398 | };
399 |
400 | declare type PartialUserObjectResponse =
401 | | {
402 | id: IdRequest;
403 | object: 'user';
404 | }
405 | | UserObjectResponse;
406 |
407 | export type DateRequest = {
408 | start: string;
409 | end?: string | null;
410 | time_zone?: TimeZoneRequest | null;
411 | };
412 |
413 | declare type DateResponse = {
414 | start: string;
415 | end: string | null;
416 | time_zone: TimeZoneRequest | null;
417 | };
418 |
419 | export type StringRequest = string;
420 |
421 | export type SelectColor =
422 | | 'default'
423 | | 'gray'
424 | | 'brown'
425 | | 'orange'
426 | | 'yellow'
427 | | 'green'
428 | | 'blue'
429 | | 'purple'
430 | | 'pink'
431 | | 'red';
432 |
433 | export type IdRequest = string | string;
434 |
435 | export type EmptyObject = Record;
436 |
437 | export type TextRequest = string;
438 |
439 | export type TimeZoneRequest =
440 | | 'Africa/Abidjan'
441 | | 'Africa/Accra'
442 | | 'Africa/Addis_Ababa'
443 | | 'Africa/Algiers'
444 | | 'Africa/Asmara'
445 | | 'Africa/Asmera'
446 | | 'Africa/Bamako'
447 | | 'Africa/Bangui'
448 | | 'Africa/Banjul'
449 | | 'Africa/Bissau'
450 | | 'Africa/Blantyre'
451 | | 'Africa/Brazzaville'
452 | | 'Africa/Bujumbura'
453 | | 'Africa/Cairo'
454 | | 'Africa/Casablanca'
455 | | 'Africa/Ceuta'
456 | | 'Africa/Conakry'
457 | | 'Africa/Dakar'
458 | | 'Africa/Dar_es_Salaam'
459 | | 'Africa/Djibouti'
460 | | 'Africa/Douala'
461 | | 'Africa/El_Aaiun'
462 | | 'Africa/Freetown'
463 | | 'Africa/Gaborone'
464 | | 'Africa/Harare'
465 | | 'Africa/Johannesburg'
466 | | 'Africa/Juba'
467 | | 'Africa/Kampala'
468 | | 'Africa/Khartoum'
469 | | 'Africa/Kigali'
470 | | 'Africa/Kinshasa'
471 | | 'Africa/Lagos'
472 | | 'Africa/Libreville'
473 | | 'Africa/Lome'
474 | | 'Africa/Luanda'
475 | | 'Africa/Lubumbashi'
476 | | 'Africa/Lusaka'
477 | | 'Africa/Malabo'
478 | | 'Africa/Maputo'
479 | | 'Africa/Maseru'
480 | | 'Africa/Mbabane'
481 | | 'Africa/Mogadishu'
482 | | 'Africa/Monrovia'
483 | | 'Africa/Nairobi'
484 | | 'Africa/Ndjamena'
485 | | 'Africa/Niamey'
486 | | 'Africa/Nouakchott'
487 | | 'Africa/Ouagadougou'
488 | | 'Africa/Porto-Novo'
489 | | 'Africa/Sao_Tome'
490 | | 'Africa/Timbuktu'
491 | | 'Africa/Tripoli'
492 | | 'Africa/Tunis'
493 | | 'Africa/Windhoek'
494 | | 'America/Adak'
495 | | 'America/Anchorage'
496 | | 'America/Anguilla'
497 | | 'America/Antigua'
498 | | 'America/Araguaina'
499 | | 'America/Argentina/Buenos_Aires'
500 | | 'America/Argentina/Catamarca'
501 | | 'America/Argentina/ComodRivadavia'
502 | | 'America/Argentina/Cordoba'
503 | | 'America/Argentina/Jujuy'
504 | | 'America/Argentina/La_Rioja'
505 | | 'America/Argentina/Mendoza'
506 | | 'America/Argentina/Rio_Gallegos'
507 | | 'America/Argentina/Salta'
508 | | 'America/Argentina/San_Juan'
509 | | 'America/Argentina/San_Luis'
510 | | 'America/Argentina/Tucuman'
511 | | 'America/Argentina/Ushuaia'
512 | | 'America/Aruba'
513 | | 'America/Asuncion'
514 | | 'America/Atikokan'
515 | | 'America/Atka'
516 | | 'America/Bahia'
517 | | 'America/Bahia_Banderas'
518 | | 'America/Barbados'
519 | | 'America/Belem'
520 | | 'America/Belize'
521 | | 'America/Blanc-Sablon'
522 | | 'America/Boa_Vista'
523 | | 'America/Bogota'
524 | | 'America/Boise'
525 | | 'America/Buenos_Aires'
526 | | 'America/Cambridge_Bay'
527 | | 'America/Campo_Grande'
528 | | 'America/Cancun'
529 | | 'America/Caracas'
530 | | 'America/Catamarca'
531 | | 'America/Cayenne'
532 | | 'America/Cayman'
533 | | 'America/Chicago'
534 | | 'America/Chihuahua'
535 | | 'America/Coral_Harbour'
536 | | 'America/Cordoba'
537 | | 'America/Costa_Rica'
538 | | 'America/Creston'
539 | | 'America/Cuiaba'
540 | | 'America/Curacao'
541 | | 'America/Danmarkshavn'
542 | | 'America/Dawson'
543 | | 'America/Dawson_Creek'
544 | | 'America/Denver'
545 | | 'America/Detroit'
546 | | 'America/Dominica'
547 | | 'America/Edmonton'
548 | | 'America/Eirunepe'
549 | | 'America/El_Salvador'
550 | | 'America/Ensenada'
551 | | 'America/Fort_Nelson'
552 | | 'America/Fort_Wayne'
553 | | 'America/Fortaleza'
554 | | 'America/Glace_Bay'
555 | | 'America/Godthab'
556 | | 'America/Goose_Bay'
557 | | 'America/Grand_Turk'
558 | | 'America/Grenada'
559 | | 'America/Guadeloupe'
560 | | 'America/Guatemala'
561 | | 'America/Guayaquil'
562 | | 'America/Guyana'
563 | | 'America/Halifax'
564 | | 'America/Havana'
565 | | 'America/Hermosillo'
566 | | 'America/Indiana/Indianapolis'
567 | | 'America/Indiana/Knox'
568 | | 'America/Indiana/Marengo'
569 | | 'America/Indiana/Petersburg'
570 | | 'America/Indiana/Tell_City'
571 | | 'America/Indiana/Vevay'
572 | | 'America/Indiana/Vincennes'
573 | | 'America/Indiana/Winamac'
574 | | 'America/Indianapolis'
575 | | 'America/Inuvik'
576 | | 'America/Iqaluit'
577 | | 'America/Jamaica'
578 | | 'America/Jujuy'
579 | | 'America/Juneau'
580 | | 'America/Kentucky/Louisville'
581 | | 'America/Kentucky/Monticello'
582 | | 'America/Knox_IN'
583 | | 'America/Kralendijk'
584 | | 'America/La_Paz'
585 | | 'America/Lima'
586 | | 'America/Los_Angeles'
587 | | 'America/Louisville'
588 | | 'America/Lower_Princes'
589 | | 'America/Maceio'
590 | | 'America/Managua'
591 | | 'America/Manaus'
592 | | 'America/Marigot'
593 | | 'America/Martinique'
594 | | 'America/Matamoros'
595 | | 'America/Mazatlan'
596 | | 'America/Mendoza'
597 | | 'America/Menominee'
598 | | 'America/Merida'
599 | | 'America/Metlakatla'
600 | | 'America/Mexico_City'
601 | | 'America/Miquelon'
602 | | 'America/Moncton'
603 | | 'America/Monterrey'
604 | | 'America/Montevideo'
605 | | 'America/Montreal'
606 | | 'America/Montserrat'
607 | | 'America/Nassau'
608 | | 'America/New_York'
609 | | 'America/Nipigon'
610 | | 'America/Nome'
611 | | 'America/Noronha'
612 | | 'America/North_Dakota/Beulah'
613 | | 'America/North_Dakota/Center'
614 | | 'America/North_Dakota/New_Salem'
615 | | 'America/Ojinaga'
616 | | 'America/Panama'
617 | | 'America/Pangnirtung'
618 | | 'America/Paramaribo'
619 | | 'America/Phoenix'
620 | | 'America/Port-au-Prince'
621 | | 'America/Port_of_Spain'
622 | | 'America/Porto_Acre'
623 | | 'America/Porto_Velho'
624 | | 'America/Puerto_Rico'
625 | | 'America/Punta_Arenas'
626 | | 'America/Rainy_River'
627 | | 'America/Rankin_Inlet'
628 | | 'America/Recife'
629 | | 'America/Regina'
630 | | 'America/Resolute'
631 | | 'America/Rio_Branco'
632 | | 'America/Rosario'
633 | | 'America/Santa_Isabel'
634 | | 'America/Santarem'
635 | | 'America/Santiago'
636 | | 'America/Santo_Domingo'
637 | | 'America/Sao_Paulo'
638 | | 'America/Scoresbysund'
639 | | 'America/Shiprock'
640 | | 'America/Sitka'
641 | | 'America/St_Barthelemy'
642 | | 'America/St_Johns'
643 | | 'America/St_Kitts'
644 | | 'America/St_Lucia'
645 | | 'America/St_Thomas'
646 | | 'America/St_Vincent'
647 | | 'America/Swift_Current'
648 | | 'America/Tegucigalpa'
649 | | 'America/Thule'
650 | | 'America/Thunder_Bay'
651 | | 'America/Tijuana'
652 | | 'America/Toronto'
653 | | 'America/Tortola'
654 | | 'America/Vancouver'
655 | | 'America/Virgin'
656 | | 'America/Whitehorse'
657 | | 'America/Winnipeg'
658 | | 'America/Yakutat'
659 | | 'America/Yellowknife'
660 | | 'Antarctica/Casey'
661 | | 'Antarctica/Davis'
662 | | 'Antarctica/DumontDUrville'
663 | | 'Antarctica/Macquarie'
664 | | 'Antarctica/Mawson'
665 | | 'Antarctica/McMurdo'
666 | | 'Antarctica/Palmer'
667 | | 'Antarctica/Rothera'
668 | | 'Antarctica/South_Pole'
669 | | 'Antarctica/Syowa'
670 | | 'Antarctica/Troll'
671 | | 'Antarctica/Vostok'
672 | | 'Arctic/Longyearbyen'
673 | | 'Asia/Aden'
674 | | 'Asia/Almaty'
675 | | 'Asia/Amman'
676 | | 'Asia/Anadyr'
677 | | 'Asia/Aqtau'
678 | | 'Asia/Aqtobe'
679 | | 'Asia/Ashgabat'
680 | | 'Asia/Ashkhabad'
681 | | 'Asia/Atyrau'
682 | | 'Asia/Baghdad'
683 | | 'Asia/Bahrain'
684 | | 'Asia/Baku'
685 | | 'Asia/Bangkok'
686 | | 'Asia/Barnaul'
687 | | 'Asia/Beirut'
688 | | 'Asia/Bishkek'
689 | | 'Asia/Brunei'
690 | | 'Asia/Calcutta'
691 | | 'Asia/Chita'
692 | | 'Asia/Choibalsan'
693 | | 'Asia/Chongqing'
694 | | 'Asia/Chungking'
695 | | 'Asia/Colombo'
696 | | 'Asia/Dacca'
697 | | 'Asia/Damascus'
698 | | 'Asia/Dhaka'
699 | | 'Asia/Dili'
700 | | 'Asia/Dubai'
701 | | 'Asia/Dushanbe'
702 | | 'Asia/Famagusta'
703 | | 'Asia/Gaza'
704 | | 'Asia/Harbin'
705 | | 'Asia/Hebron'
706 | | 'Asia/Ho_Chi_Minh'
707 | | 'Asia/Hong_Kong'
708 | | 'Asia/Hovd'
709 | | 'Asia/Irkutsk'
710 | | 'Asia/Istanbul'
711 | | 'Asia/Jakarta'
712 | | 'Asia/Jayapura'
713 | | 'Asia/Jerusalem'
714 | | 'Asia/Kabul'
715 | | 'Asia/Kamchatka'
716 | | 'Asia/Karachi'
717 | | 'Asia/Kashgar'
718 | | 'Asia/Kathmandu'
719 | | 'Asia/Katmandu'
720 | | 'Asia/Khandyga'
721 | | 'Asia/Kolkata'
722 | | 'Asia/Krasnoyarsk'
723 | | 'Asia/Kuala_Lumpur'
724 | | 'Asia/Kuching'
725 | | 'Asia/Kuwait'
726 | | 'Asia/Macao'
727 | | 'Asia/Macau'
728 | | 'Asia/Magadan'
729 | | 'Asia/Makassar'
730 | | 'Asia/Manila'
731 | | 'Asia/Muscat'
732 | | 'Asia/Nicosia'
733 | | 'Asia/Novokuznetsk'
734 | | 'Asia/Novosibirsk'
735 | | 'Asia/Omsk'
736 | | 'Asia/Oral'
737 | | 'Asia/Phnom_Penh'
738 | | 'Asia/Pontianak'
739 | | 'Asia/Pyongyang'
740 | | 'Asia/Qatar'
741 | | 'Asia/Qostanay'
742 | | 'Asia/Qyzylorda'
743 | | 'Asia/Rangoon'
744 | | 'Asia/Riyadh'
745 | | 'Asia/Saigon'
746 | | 'Asia/Sakhalin'
747 | | 'Asia/Samarkand'
748 | | 'Asia/Seoul'
749 | | 'Asia/Shanghai'
750 | | 'Asia/Singapore'
751 | | 'Asia/Srednekolymsk'
752 | | 'Asia/Taipei'
753 | | 'Asia/Tashkent'
754 | | 'Asia/Tbilisi'
755 | | 'Asia/Tehran'
756 | | 'Asia/Tel_Aviv'
757 | | 'Asia/Thimbu'
758 | | 'Asia/Thimphu'
759 | | 'Asia/Tokyo'
760 | | 'Asia/Tomsk'
761 | | 'Asia/Ujung_Pandang'
762 | | 'Asia/Ulaanbaatar'
763 | | 'Asia/Ulan_Bator'
764 | | 'Asia/Urumqi'
765 | | 'Asia/Ust-Nera'
766 | | 'Asia/Vientiane'
767 | | 'Asia/Vladivostok'
768 | | 'Asia/Yakutsk'
769 | | 'Asia/Yangon'
770 | | 'Asia/Yekaterinburg'
771 | | 'Asia/Yerevan'
772 | | 'Atlantic/Azores'
773 | | 'Atlantic/Bermuda'
774 | | 'Atlantic/Canary'
775 | | 'Atlantic/Cape_Verde'
776 | | 'Atlantic/Faeroe'
777 | | 'Atlantic/Faroe'
778 | | 'Atlantic/Jan_Mayen'
779 | | 'Atlantic/Madeira'
780 | | 'Atlantic/Reykjavik'
781 | | 'Atlantic/South_Georgia'
782 | | 'Atlantic/St_Helena'
783 | | 'Atlantic/Stanley'
784 | | 'Australia/ACT'
785 | | 'Australia/Adelaide'
786 | | 'Australia/Brisbane'
787 | | 'Australia/Broken_Hill'
788 | | 'Australia/Canberra'
789 | | 'Australia/Currie'
790 | | 'Australia/Darwin'
791 | | 'Australia/Eucla'
792 | | 'Australia/Hobart'
793 | | 'Australia/LHI'
794 | | 'Australia/Lindeman'
795 | | 'Australia/Lord_Howe'
796 | | 'Australia/Melbourne'
797 | | 'Australia/NSW'
798 | | 'Australia/North'
799 | | 'Australia/Perth'
800 | | 'Australia/Queensland'
801 | | 'Australia/South'
802 | | 'Australia/Sydney'
803 | | 'Australia/Tasmania'
804 | | 'Australia/Victoria'
805 | | 'Australia/West'
806 | | 'Australia/Yancowinna'
807 | | 'Brazil/Acre'
808 | | 'Brazil/DeNoronha'
809 | | 'Brazil/East'
810 | | 'Brazil/West'
811 | | 'CET'
812 | | 'CST6CDT'
813 | | 'Canada/Atlantic'
814 | | 'Canada/Central'
815 | | 'Canada/Eastern'
816 | | 'Canada/Mountain'
817 | | 'Canada/Newfoundland'
818 | | 'Canada/Pacific'
819 | | 'Canada/Saskatchewan'
820 | | 'Canada/Yukon'
821 | | 'Chile/Continental'
822 | | 'Chile/EasterIsland'
823 | | 'Cuba'
824 | | 'EET'
825 | | 'EST'
826 | | 'EST5EDT'
827 | | 'Egypt'
828 | | 'Eire'
829 | | 'Etc/GMT'
830 | | 'Etc/GMT+0'
831 | | 'Etc/GMT+1'
832 | | 'Etc/GMT+10'
833 | | 'Etc/GMT+11'
834 | | 'Etc/GMT+12'
835 | | 'Etc/GMT+2'
836 | | 'Etc/GMT+3'
837 | | 'Etc/GMT+4'
838 | | 'Etc/GMT+5'
839 | | 'Etc/GMT+6'
840 | | 'Etc/GMT+7'
841 | | 'Etc/GMT+8'
842 | | 'Etc/GMT+9'
843 | | 'Etc/GMT-0'
844 | | 'Etc/GMT-1'
845 | | 'Etc/GMT-10'
846 | | 'Etc/GMT-11'
847 | | 'Etc/GMT-12'
848 | | 'Etc/GMT-13'
849 | | 'Etc/GMT-14'
850 | | 'Etc/GMT-2'
851 | | 'Etc/GMT-3'
852 | | 'Etc/GMT-4'
853 | | 'Etc/GMT-5'
854 | | 'Etc/GMT-6'
855 | | 'Etc/GMT-7'
856 | | 'Etc/GMT-8'
857 | | 'Etc/GMT-9'
858 | | 'Etc/GMT0'
859 | | 'Etc/Greenwich'
860 | | 'Etc/UCT'
861 | | 'Etc/UTC'
862 | | 'Etc/Universal'
863 | | 'Etc/Zulu'
864 | | 'Europe/Amsterdam'
865 | | 'Europe/Andorra'
866 | | 'Europe/Astrakhan'
867 | | 'Europe/Athens'
868 | | 'Europe/Belfast'
869 | | 'Europe/Belgrade'
870 | | 'Europe/Berlin'
871 | | 'Europe/Bratislava'
872 | | 'Europe/Brussels'
873 | | 'Europe/Bucharest'
874 | | 'Europe/Budapest'
875 | | 'Europe/Busingen'
876 | | 'Europe/Chisinau'
877 | | 'Europe/Copenhagen'
878 | | 'Europe/Dublin'
879 | | 'Europe/Gibraltar'
880 | | 'Europe/Guernsey'
881 | | 'Europe/Helsinki'
882 | | 'Europe/Isle_of_Man'
883 | | 'Europe/Istanbul'
884 | | 'Europe/Jersey'
885 | | 'Europe/Kaliningrad'
886 | | 'Europe/Kiev'
887 | | 'Europe/Kirov'
888 | | 'Europe/Lisbon'
889 | | 'Europe/Ljubljana'
890 | | 'Europe/London'
891 | | 'Europe/Luxembourg'
892 | | 'Europe/Madrid'
893 | | 'Europe/Malta'
894 | | 'Europe/Mariehamn'
895 | | 'Europe/Minsk'
896 | | 'Europe/Monaco'
897 | | 'Europe/Moscow'
898 | | 'Europe/Nicosia'
899 | | 'Europe/Oslo'
900 | | 'Europe/Paris'
901 | | 'Europe/Podgorica'
902 | | 'Europe/Prague'
903 | | 'Europe/Riga'
904 | | 'Europe/Rome'
905 | | 'Europe/Samara'
906 | | 'Europe/San_Marino'
907 | | 'Europe/Sarajevo'
908 | | 'Europe/Saratov'
909 | | 'Europe/Simferopol'
910 | | 'Europe/Skopje'
911 | | 'Europe/Sofia'
912 | | 'Europe/Stockholm'
913 | | 'Europe/Tallinn'
914 | | 'Europe/Tirane'
915 | | 'Europe/Tiraspol'
916 | | 'Europe/Ulyanovsk'
917 | | 'Europe/Uzhgorod'
918 | | 'Europe/Vaduz'
919 | | 'Europe/Vatican'
920 | | 'Europe/Vienna'
921 | | 'Europe/Vilnius'
922 | | 'Europe/Volgograd'
923 | | 'Europe/Warsaw'
924 | | 'Europe/Zagreb'
925 | | 'Europe/Zaporozhye'
926 | | 'Europe/Zurich'
927 | | 'GB'
928 | | 'GB-Eire'
929 | | 'GMT'
930 | | 'GMT+0'
931 | | 'GMT-0'
932 | | 'GMT0'
933 | | 'Greenwich'
934 | | 'HST'
935 | | 'Hongkong'
936 | | 'Iceland'
937 | | 'Indian/Antananarivo'
938 | | 'Indian/Chagos'
939 | | 'Indian/Christmas'
940 | | 'Indian/Cocos'
941 | | 'Indian/Comoro'
942 | | 'Indian/Kerguelen'
943 | | 'Indian/Mahe'
944 | | 'Indian/Maldives'
945 | | 'Indian/Mauritius'
946 | | 'Indian/Mayotte'
947 | | 'Indian/Reunion'
948 | | 'Iran'
949 | | 'Israel'
950 | | 'Jamaica'
951 | | 'Japan'
952 | | 'Kwajalein'
953 | | 'Libya'
954 | | 'MET'
955 | | 'MST'
956 | | 'MST7MDT'
957 | | 'Mexico/BajaNorte'
958 | | 'Mexico/BajaSur'
959 | | 'Mexico/General'
960 | | 'NZ'
961 | | 'NZ-CHAT'
962 | | 'Navajo'
963 | | 'PRC'
964 | | 'PST8PDT'
965 | | 'Pacific/Apia'
966 | | 'Pacific/Auckland'
967 | | 'Pacific/Bougainville'
968 | | 'Pacific/Chatham'
969 | | 'Pacific/Chuuk'
970 | | 'Pacific/Easter'
971 | | 'Pacific/Efate'
972 | | 'Pacific/Enderbury'
973 | | 'Pacific/Fakaofo'
974 | | 'Pacific/Fiji'
975 | | 'Pacific/Funafuti'
976 | | 'Pacific/Galapagos'
977 | | 'Pacific/Gambier'
978 | | 'Pacific/Guadalcanal'
979 | | 'Pacific/Guam'
980 | | 'Pacific/Honolulu'
981 | | 'Pacific/Johnston'
982 | | 'Pacific/Kiritimati'
983 | | 'Pacific/Kosrae'
984 | | 'Pacific/Kwajalein'
985 | | 'Pacific/Majuro'
986 | | 'Pacific/Marquesas'
987 | | 'Pacific/Midway'
988 | | 'Pacific/Nauru'
989 | | 'Pacific/Niue'
990 | | 'Pacific/Norfolk'
991 | | 'Pacific/Noumea'
992 | | 'Pacific/Pago_Pago'
993 | | 'Pacific/Palau'
994 | | 'Pacific/Pitcairn'
995 | | 'Pacific/Pohnpei'
996 | | 'Pacific/Ponape'
997 | | 'Pacific/Port_Moresby'
998 | | 'Pacific/Rarotonga'
999 | | 'Pacific/Saipan'
1000 | | 'Pacific/Samoa'
1001 | | 'Pacific/Tahiti'
1002 | | 'Pacific/Tarawa'
1003 | | 'Pacific/Tongatapu'
1004 | | 'Pacific/Truk'
1005 | | 'Pacific/Wake'
1006 | | 'Pacific/Wallis'
1007 | | 'Pacific/Yap'
1008 | | 'Poland'
1009 | | 'Portugal'
1010 | | 'ROC'
1011 | | 'ROK'
1012 | | 'Singapore'
1013 | | 'Turkey'
1014 | | 'UCT'
1015 | | 'US/Alaska'
1016 | | 'US/Aleutian'
1017 | | 'US/Arizona'
1018 | | 'US/Central'
1019 | | 'US/East-Indiana'
1020 | | 'US/Eastern'
1021 | | 'US/Hawaii'
1022 | | 'US/Indiana-Starke'
1023 | | 'US/Michigan'
1024 | | 'US/Mountain'
1025 | | 'US/Pacific'
1026 | | 'US/Pacific-New'
1027 | | 'US/Samoa'
1028 | | 'UTC'
1029 | | 'Universal'
1030 | | 'W-SU'
1031 | | 'WET'
1032 | | 'Zulu';
1033 |
--------------------------------------------------------------------------------
/src/common.ts:
--------------------------------------------------------------------------------
1 | import {CustomTypes, RichTextItemRequest} from './api-types';
2 |
3 | // https://developers.notion.com/reference/errors#limits-for-property-values
4 | export const RICH_TEXT_CONTENT_CHARACTERS_LIMIT = 1000;
5 |
6 | function truncateTextContent(text: string): string {
7 | return text.length > RICH_TEXT_CONTENT_CHARACTERS_LIMIT
8 | ? text.substring(0, RICH_TEXT_CONTENT_CHARACTERS_LIMIT - 1) + '…'
9 | : text;
10 | }
11 |
12 | export namespace common {
13 | export interface RichTextOptions {
14 | annotations?: RichTextItemRequest['annotations'];
15 | url?: string;
16 | }
17 |
18 | export function richText(
19 | content: string,
20 | options: RichTextOptions = {}
21 | ): CustomTypes.RichText['rich_text'] {
22 | const annotations = options.annotations ?? {};
23 | const truncated = truncateTextContent(content);
24 |
25 | return [
26 | {
27 | type: 'text',
28 | annotations: {
29 | bold: false,
30 | strikethrough: false,
31 | underline: false,
32 | italic: false,
33 | code: false,
34 | color: 'default',
35 | ...annotations,
36 | },
37 | text: {
38 | content: truncated,
39 | link: options.url ? {url: options.url} : undefined,
40 | },
41 | },
42 | ];
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/src/index.ts:
--------------------------------------------------------------------------------
1 | import * as core from '@actions/core';
2 | import * as github from '@actions/github';
3 | import {run} from './action';
4 |
5 | const INPUTS = {
6 | NOTION_TOKEN: 'notion-token',
7 | NOTION_DB: 'notion-db',
8 | GITHUB_TOKEN: 'github-token',
9 | };
10 |
11 | async function start() {
12 | try {
13 | const notionToken = core.getInput(INPUTS.NOTION_TOKEN, {required: true});
14 | const notionDb = core.getInput(INPUTS.NOTION_DB, {required: true});
15 | const githubToken = core.getInput(INPUTS.GITHUB_TOKEN, {required: true});
16 |
17 | core.info(`context event: ${github.context.eventName}`);
18 | core.info(`context action: ${github.context.action}`);
19 | core.info(`payload action: ${github.context.payload.action}`);
20 | const options = {
21 | notion: {
22 | token: notionToken,
23 | databaseId: notionDb,
24 | },
25 | github: {
26 | payload: github.context.payload,
27 | eventName: github.context.eventName,
28 | token: githubToken,
29 | },
30 | };
31 |
32 | await run(options);
33 | } catch (e) {
34 | core.setFailed(e instanceof Error ? e.message : e + '');
35 | }
36 | }
37 |
38 | (async () => {
39 | await start();
40 | })();
41 |
--------------------------------------------------------------------------------
/src/properties.ts:
--------------------------------------------------------------------------------
1 | import {CustomTypes, SelectColor} from './api-types';
2 | import {common} from './common';
3 |
4 | export type CustomValueMap = {
5 | Name: CustomTypes.Title;
6 | Status: CustomTypes.Select;
7 | Organization: CustomTypes.RichText;
8 | Repository: CustomTypes.RichText;
9 | Number: CustomTypes.Number;
10 | Body: CustomTypes.RichText;
11 | Assignees: CustomTypes.MultiSelect;
12 | Milestone: CustomTypes.RichText;
13 | Labels: CustomTypes.MultiSelect;
14 | Author: CustomTypes.RichText;
15 | Created: CustomTypes.Date;
16 | Updated: CustomTypes.Date;
17 | ID: CustomTypes.Number;
18 | Link: CustomTypes.URL;
19 | Project: CustomTypes.RichText;
20 | 'Project Column': CustomTypes.RichText;
21 | };
22 |
23 | export namespace properties {
24 | export function text(text: string): CustomTypes.RichText {
25 | return {
26 | type: 'rich_text',
27 | rich_text: text ? common.richText(text) : [],
28 | };
29 | }
30 |
31 | export function richText(text: CustomTypes.RichText['rich_text']): CustomTypes.RichText {
32 | return {
33 | type: 'rich_text',
34 | rich_text: text,
35 | };
36 | }
37 |
38 | export function title(text: string): CustomTypes.Title {
39 | return {
40 | type: 'title',
41 | title: [
42 | {
43 | type: 'text',
44 | text: {
45 | content: text,
46 | },
47 | },
48 | ],
49 | };
50 | }
51 |
52 | export function number(number: number): CustomTypes.Number {
53 | return {
54 | type: 'number',
55 | number: number,
56 | };
57 | }
58 |
59 | export function date(time: string): CustomTypes.Date {
60 | return {
61 | type: 'date',
62 | date: {
63 | start: time,
64 | },
65 | };
66 | }
67 |
68 | export function getStatusSelectOption(state: 'open' | 'closed'): CustomTypes.Select {
69 | switch (state) {
70 | case 'open':
71 | return select('Open', 'green');
72 | case 'closed':
73 | return select('Closed', 'red');
74 | }
75 | }
76 |
77 | export function select(name: string, color: SelectColor = 'default'): CustomTypes.Select {
78 | return {
79 | type: 'select',
80 | select: {
81 | name: name,
82 | color: color,
83 | },
84 | };
85 | }
86 |
87 | export function multiSelect(names: string[]): CustomTypes.MultiSelect {
88 | return {
89 | type: 'multi_select',
90 | multi_select: names.map(name => {
91 | return {
92 | name: name,
93 | };
94 | }),
95 | };
96 | }
97 |
98 | export function url(url: string): CustomTypes.URL {
99 | return {
100 | type: 'url',
101 | url,
102 | };
103 | }
104 | }
105 |
--------------------------------------------------------------------------------
/src/sync.ts:
--------------------------------------------------------------------------------
1 | import {Client} from '@notionhq/client/build/src';
2 | import {Issue} from '@octokit/webhooks-types/schema';
3 | import * as core from '@actions/core';
4 | import {Octokit} from 'octokit';
5 | import {CustomValueMap, properties} from './properties';
6 | import {getProjectData} from './action';
7 | import {QueryDatabaseResponse} from '@notionhq/client/build/src/api-endpoints';
8 | import {CustomTypes} from './api-types';
9 | import {parseBodyRichText} from './action';
10 |
11 | type PageIdAndIssueNumber = {
12 | pageId: string;
13 | issueNumber: number;
14 | };
15 |
16 | export async function createIssueMapping(
17 | notion: Client,
18 | databaseId: string
19 | ): Promise